From 843f99649ede6cc029e865682cfcf5d34a745c8b Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 31 Mar 2025 18:32:22 +0200 Subject: [PATCH 001/448] Scenarios: Prepare DataModel (#213) * Update core.py to work with another dimension * Add scenarios to TimeSeries * Update TimeSeriesCollection * Update get_numeric_stats() to return values per scenario * Update repr and str * Improve stats * Add utility methods to analyze data * Move test insto class * Improve DataConverter * Improve DataConverter * Improve conversion and copying * Improve conversion and copying * Update tests * Update test * Bugfix stats * Bugfix stored_data.setter * Improve __str__ of TimeSeries * Bugfixes * Add tests * Temp * Simplify the TImeSeriesCollection * Simplify the TImeSeriesCollection * Add test script * Improve TImeSeriesAllocator * Update TimeSeries * Update TimeSeries * Update selection * Renaming * Update TimeSeriesAllocator * Update TimeSeriesAllocator * Update TimeSeriesAllocator * Update TimeSeriesAllocator * Update selection * Improve selection * Improve validation of Timesteps * Improve TimeSeries * Improve TimeSeriesAllocator * Update calculation and FlowSystem * rename active_data to selected_data * Add property * Improve type hints * Improve type hints * Add options to get data without extra timestep * Rename * Update tests * Bugfix for TImeSeriesData to work * Update calculation.py * Bugfix * Improve as_dataset to improve aggregation * Bugfix * Update test * Remove test script * ruff check * Revert some renaming * Bugfix in test --- flixopt/calculation.py | 24 +- flixopt/components.py | 24 +- flixopt/core.py | 1354 ++++++++++++++++++++++++----------- flixopt/effects.py | 6 +- flixopt/elements.py | 8 +- flixopt/features.py | 4 +- flixopt/flow_system.py | 24 +- flixopt/io.py | 2 +- flixopt/structure.py | 2 +- tests/test_dataconverter.py | 871 ++++++++++++++++++++-- tests/test_timeseries.py | 716 ++++++++++-------- 11 files changed, 2244 insertions(+), 791 deletions(-) diff --git a/flixopt/calculation.py b/flixopt/calculation.py index c7367cad2..03cf8b9a6 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -183,8 +183,8 @@ def solve(self, solver: _Solver, log_file: Optional[pathlib.Path] = None, log_ma def _activate_time_series(self): self.flow_system.transform_data() - self.flow_system.time_series_collection.activate_timesteps( - active_timesteps=self.active_timesteps, + self.flow_system.time_series_collection.set_selection( + timesteps=self.active_timesteps ) @@ -217,6 +217,8 @@ def __init__( list with indices, which should be used for calculation. If None, then all timesteps are used. folder: folder where results should be saved. If None, then the current working directory is used. """ + if flow_system.time_series_collection.scenarios is not None: + raise ValueError('Aggregation is not supported for scenarios yet. Please use FullCalculation instead.') super().__init__(name, flow_system, active_timesteps, folder=folder) self.aggregation_parameters = aggregation_parameters self.components_to_clusterize = components_to_clusterize @@ -272,9 +274,9 @@ def _perform_aggregation(self): # Aggregation - creation of aggregated timeseries: self.aggregation = Aggregation( - original_data=self.flow_system.time_series_collection.to_dataframe( - include_extra_timestep=False - ), # Exclude last row (NaN) + original_data=self.flow_system.time_series_collection.as_dataset( + with_extra_timestep=False, with_constants=False + ).to_dataframe(), hours_per_time_step=float(dt_min), hours_per_period=self.aggregation_parameters.hours_per_period, nr_of_periods=self.aggregation_parameters.nr_of_periods, @@ -286,9 +288,11 @@ def _perform_aggregation(self): self.aggregation.cluster() self.aggregation.plot(show=True, save=self.folder / 'aggregation.html') if self.aggregation_parameters.aggregate_data_and_fix_non_binary_vars: - self.flow_system.time_series_collection.insert_new_data( - self.aggregation.aggregated_data, include_extra_timestep=False - ) + for col in self.aggregation.aggregated_data.columns: + data = self.aggregation.aggregated_data[col].values + if col in self.flow_system.time_series_collection._has_extra_timestep: + data = np.append(data, data[-1]) + self.flow_system.time_series_collection.update_time_series(col, data) self.durations['aggregation'] = round(timeit.default_timer() - t_start_agg, 2) @@ -327,8 +331,8 @@ def __init__( self.nr_of_previous_values = nr_of_previous_values self.sub_calculations: List[FullCalculation] = [] - self.all_timesteps = self.flow_system.time_series_collection.all_timesteps - self.all_timesteps_extra = self.flow_system.time_series_collection.all_timesteps_extra + self.all_timesteps = self.flow_system.time_series_collection._full_timesteps + self.all_timesteps_extra = self.flow_system.time_series_collection._full_timesteps_extra self.segment_names = [ f'Segment_{i + 1}' for i in range(math.ceil(len(self.all_timesteps) / self.timesteps_per_segment)) diff --git a/flixopt/components.py b/flixopt/components.py index d5d1df12d..2a69c6165 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -194,12 +194,12 @@ def transform_data(self, flow_system: 'FlowSystem') -> None: self.relative_minimum_charge_state = flow_system.create_time_series( f'{self.label_full}|relative_minimum_charge_state', self.relative_minimum_charge_state, - needs_extra_timestep=True, + has_extra_timestep=True, ) self.relative_maximum_charge_state = flow_system.create_time_series( f'{self.label_full}|relative_maximum_charge_state', self.relative_maximum_charge_state, - needs_extra_timestep=True, + has_extra_timestep=True, ) self.eta_charge = flow_system.create_time_series(f'{self.label_full}|eta_charge', self.eta_charge) self.eta_discharge = flow_system.create_time_series(f'{self.label_full}|eta_discharge', self.eta_discharge) @@ -342,7 +342,7 @@ def __init__(self, model: SystemModel, element: Transmission): def do_modeling(self): """Initiates all FlowModels""" # Force On Variable if absolute losses are present - if (self.element.absolute_losses is not None) and np.any(self.element.absolute_losses.active_data != 0): + if (self.element.absolute_losses is not None) and np.any(self.element.absolute_losses.selected_data != 0): for flow in self.element.inputs + self.element.outputs: if flow.on_off_parameters is None: flow.on_off_parameters = OnOffParameters() @@ -379,14 +379,14 @@ def create_transmission_equation(self, name: str, in_flow: Flow, out_flow: Flow) # eq: out(t) + on(t)*loss_abs(t) = in(t)*(1 - loss_rel(t)) con_transmission = self.add( self._model.add_constraints( - out_flow.model.flow_rate == -in_flow.model.flow_rate * (self.element.relative_losses.active_data - 1), + out_flow.model.flow_rate == -in_flow.model.flow_rate * (self.element.relative_losses.selected_data - 1), name=f'{self.label_full}|{name}', ), name, ) if self.element.absolute_losses is not None: - con_transmission.lhs += in_flow.model.on_off.on * self.element.absolute_losses.active_data + con_transmission.lhs += in_flow.model.on_off.on * self.element.absolute_losses.selected_data return con_transmission @@ -413,8 +413,8 @@ def do_modeling(self): self.add( self._model.add_constraints( - sum([flow.model.flow_rate * conv_factors[flow.label].active_data for flow in used_inputs]) - == sum([flow.model.flow_rate * conv_factors[flow.label].active_data for flow in used_outputs]), + sum([flow.model.flow_rate * conv_factors[flow.label].selected_data for flow in used_inputs]) + == sum([flow.model.flow_rate * conv_factors[flow.label].selected_data for flow in used_outputs]), name=f'{self.label_full}|conversion_{i}', ) ) @@ -474,12 +474,12 @@ def do_modeling(self): ) charge_state = self.charge_state - rel_loss = self.element.relative_loss_per_hour.active_data + rel_loss = self.element.relative_loss_per_hour.selected_data hours_per_step = self._model.hours_per_step charge_rate = self.element.charging.model.flow_rate discharge_rate = self.element.discharging.model.flow_rate - eff_charge = self.element.eta_charge.active_data - eff_discharge = self.element.eta_discharge.active_data + eff_charge = self.element.eta_charge.selected_data + eff_discharge = self.element.eta_discharge.selected_data self.add( self._model.add_constraints( @@ -565,8 +565,8 @@ def absolute_charge_state_bounds(self) -> Tuple[NumericData, NumericData]: @property def relative_charge_state_bounds(self) -> Tuple[NumericData, NumericData]: return ( - self.element.relative_minimum_charge_state.active_data, - self.element.relative_maximum_charge_state.active_data, + self.element.relative_minimum_charge_state.selected_data, + self.element.relative_maximum_charge_state.selected_data, ) diff --git a/flixopt/core.py b/flixopt/core.py index 379828554..d2a8edd59 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -7,6 +7,7 @@ import json import logging import pathlib +import textwrap from collections import Counter from typing import Any, Dict, Iterator, List, Literal, Optional, Tuple, Union @@ -40,56 +41,563 @@ class ConversionError(Exception): class DataConverter: """ - Converts various data types into xarray.DataArray with a timesteps index. - - Supports: scalars, arrays, Series, DataFrames, and DataArrays. + Converts various data types into xarray.DataArray with timesteps and optional scenarios dimensions. + + Supports: + - Scalar values (broadcast to all timesteps/scenarios) + - 1D arrays (mapped to timesteps, broadcast to scenarios if provided) + - 2D arrays (mapped to scenarios × timesteps if dimensions match) + - Series with time index (broadcast to scenarios if provided) + - DataFrames with time index and a single column (broadcast to scenarios if provided) + - Series/DataFrames with MultiIndex (scenario, time) + - Existing DataArrays """ + #TODO: Allow DataFrame with scenarios as columns + @staticmethod - def as_dataarray(data: NumericData, timesteps: pd.DatetimeIndex) -> xr.DataArray: - """Convert data to xarray.DataArray with specified timesteps index.""" - if not isinstance(timesteps, pd.DatetimeIndex) or len(timesteps) == 0: - raise ValueError(f'Timesteps must be a non-empty DatetimeIndex, got {type(timesteps).__name__}') - if not timesteps.name == 'time': - raise ConversionError(f'DatetimeIndex is not named correctly. Must be named "time", got {timesteps.name=}') + def as_dataarray( + data: NumericData, timesteps: pd.DatetimeIndex, scenarios: Optional[pd.Index] = None + ) -> xr.DataArray: + """ + Convert data to xarray.DataArray with specified timesteps and optional scenarios dimensions. + + Args: + data: The data to convert (scalar, array, Series, DataFrame, or DataArray) + timesteps: DatetimeIndex representing the time dimension (must be named 'time') + scenarios: Optional Index representing scenarios (must be named 'scenario') + + Returns: + DataArray with the converted data + + Raises: + ValueError: If timesteps or scenarios are invalid + ConversionError: If the data cannot be converted to the expected dimensions + """ + # Validate inputs + DataConverter._validate_timesteps(timesteps) + if scenarios is not None: + DataConverter._validate_scenarios(scenarios) - coords = [timesteps] - dims = ['time'] - expected_shape = (len(timesteps),) + # Determine dimensions and coordinates + coords, dims, expected_shape = DataConverter._get_dimensions(timesteps, scenarios) try: + # Convert different data types using specialized methods if isinstance(data, (int, float, np.integer, np.floating)): - return xr.DataArray(data, coords=coords, dims=dims) + return DataConverter._convert_scalar(data, coords, dims) + elif isinstance(data, pd.DataFrame): - if not data.index.equals(timesteps): - raise ConversionError("DataFrame index doesn't match timesteps index") - if not len(data.columns) == 1: - raise ConversionError('DataFrame must have exactly one column') - return xr.DataArray(data.values.flatten(), coords=coords, dims=dims) + return DataConverter._convert_dataframe(data, timesteps, scenarios, coords, dims) + elif isinstance(data, pd.Series): - if not data.index.equals(timesteps): - raise ConversionError("Series index doesn't match timesteps index") - return xr.DataArray(data.values, coords=coords, dims=dims) + return DataConverter._convert_series(data, timesteps, scenarios, coords, dims) + elif isinstance(data, np.ndarray): - if data.ndim != 1: - raise ConversionError(f'Array must be 1-dimensional, got {data.ndim}') - elif data.shape[0] != expected_shape[0]: - raise ConversionError(f"Array shape {data.shape} doesn't match expected {expected_shape}") - return xr.DataArray(data, coords=coords, dims=dims) + return DataConverter._convert_ndarray(data, timesteps, scenarios, coords, dims, expected_shape) + elif isinstance(data, xr.DataArray): - if data.dims != tuple(dims): - raise ConversionError(f"DataArray dimensions {data.dims} don't match expected {dims}") - if data.sizes[dims[0]] != len(coords[0]): - raise ConversionError( - f"DataArray length {data.sizes[dims[0]]} doesn't match expected {len(coords[0])}" - ) - return data.copy(deep=True) + return DataConverter._convert_dataarray(data, timesteps, scenarios, coords, dims) + else: raise ConversionError(f'Unsupported type: {type(data).__name__}') + except Exception as e: if isinstance(e, ConversionError): raise - raise ConversionError(f'Converting data {type(data)} to xarray.Dataset raised an error: {str(e)}') from e + raise ConversionError(f'Converting {type(data)} to DataArray raised an error: {str(e)}') from e + + @staticmethod + def _validate_timesteps(timesteps: pd.DatetimeIndex) -> None: + """ + Validate that timesteps is a properly named non-empty DatetimeIndex. + + Args: + timesteps: The DatetimeIndex to validate + + Raises: + ValueError: If timesteps is not a non-empty DatetimeIndex + ConversionError: If timesteps is not named 'time' + """ + if not isinstance(timesteps, pd.DatetimeIndex) or len(timesteps) == 0: + raise ValueError(f'Timesteps must be a non-empty DatetimeIndex, got {type(timesteps).__name__}') + if timesteps.name != 'time': + raise ConversionError(f'DatetimeIndex must be named "time", got {timesteps.name=}') + + @staticmethod + def _validate_scenarios(scenarios: pd.Index) -> None: + """ + Validate that scenarios is a properly named non-empty Index. + + Args: + scenarios: The Index to validate + + Raises: + ValueError: If scenarios is not a non-empty Index + ConversionError: If scenarios is not named 'scenario' + """ + if not isinstance(scenarios, pd.Index) or len(scenarios) == 0: + raise ValueError(f'Scenarios must be a non-empty Index, got {type(scenarios).__name__}') + if scenarios.name != 'scenario': + raise ConversionError(f'Scenarios Index must be named "scenario", got {scenarios.name=}') + + @staticmethod + def _get_dimensions( + timesteps: pd.DatetimeIndex, scenarios: Optional[pd.Index] = None + ) -> Tuple[Dict[str, pd.Index], Tuple[str, ...], Tuple[int, ...]]: + """ + Create the coordinates, dimensions, and expected shape for the output DataArray. + + Args: + timesteps: The time index + scenarios: Optional scenario index + + Returns: + Tuple containing: + - Dict mapping dimension names to coordinate indexes + - Tuple of dimension names + - Tuple of expected shape + """ + if scenarios is not None: + coords = {'scenario': scenarios, 'time': timesteps} + dims = ('scenario', 'time') + expected_shape = (len(scenarios), len(timesteps)) + else: + coords = {'time': timesteps} + dims = ('time',) + expected_shape = (len(timesteps),) + + return coords, dims, expected_shape + + @staticmethod + def _convert_scalar( + data: Union[int, float, np.integer, np.floating], coords: Dict[str, pd.Index], dims: Tuple[str, ...] + ) -> xr.DataArray: + """ + Convert a scalar value to a DataArray. + + Args: + data: The scalar value to convert + coords: Dictionary mapping dimension names to coordinate indexes + dims: Tuple of dimension names + + Returns: + DataArray with the scalar value broadcast to all coordinates + """ + return xr.DataArray(data, coords=coords, dims=dims) + + @staticmethod + def _convert_dataframe( + df: pd.DataFrame, + timesteps: pd.DatetimeIndex, + scenarios: Optional[pd.Index], + coords: Dict[str, pd.Index], + dims: Tuple[str, ...], + ) -> xr.DataArray: + """ + Convert a DataFrame to a DataArray. + + Args: + df: The DataFrame to convert + timesteps: The time index + scenarios: Optional scenario index + coords: Dictionary mapping dimension names to coordinate indexes + dims: Tuple of dimension names + + Returns: + DataArray created from the DataFrame + + Raises: + ConversionError: If the DataFrame cannot be converted to the expected dimensions + """ + # Case 1: DataFrame with MultiIndex (scenario, time) + if ( + isinstance(df.index, pd.MultiIndex) + and len(df.index.names) == 2 + and 'scenario' in df.index.names + and 'time' in df.index.names + and scenarios is not None + ): + return DataConverter._convert_multi_index_dataframe(df, timesteps, scenarios, coords, dims) + + # Case 2: Standard DataFrame with time index + elif not isinstance(df.index, pd.MultiIndex): + return DataConverter._convert_standard_dataframe(df, timesteps, scenarios, coords, dims) + + else: + raise ConversionError(f'Unsupported DataFrame index structure: {df}') + + @staticmethod + def _convert_multi_index_dataframe( + df: pd.DataFrame, + timesteps: pd.DatetimeIndex, + scenarios: pd.Index, + coords: Dict[str, pd.Index], + dims: Tuple[str, ...], + ) -> xr.DataArray: + """ + Convert a DataFrame with MultiIndex (scenario, time) to a DataArray. + + Args: + df: The DataFrame with MultiIndex to convert + timesteps: The time index + scenarios: The scenario index + coords: Dictionary mapping dimension names to coordinate indexes + dims: Tuple of dimension names + + Returns: + DataArray created from the MultiIndex DataFrame + + Raises: + ConversionError: If the DataFrame's index doesn't match expected or has multiple columns + """ + # Validate that the index contains the expected values + if not set(df.index.get_level_values('time')).issubset(set(timesteps)): + raise ConversionError("DataFrame time index doesn't match or isn't a subset of timesteps") + if not set(df.index.get_level_values('scenario')).issubset(set(scenarios)): + raise ConversionError("DataFrame scenario index doesn't match or isn't a subset of scenarios") + + # Ensure single column + if len(df.columns) != 1: + raise ConversionError('DataFrame must have exactly one column') + + # Reindex to ensure complete coverage and correct order + multi_idx = pd.MultiIndex.from_product([scenarios, timesteps], names=['scenario', 'time']) + reindexed = df.reindex(multi_idx).iloc[:, 0] + + # Reshape to 2D array + reshaped = reindexed.values.reshape(len(scenarios), len(timesteps)) + return xr.DataArray(reshaped, coords=coords, dims=dims) + + @staticmethod + def _convert_standard_dataframe( + df: pd.DataFrame, + timesteps: pd.DatetimeIndex, + scenarios: Optional[pd.Index], + coords: Dict[str, pd.Index], + dims: Tuple[str, ...], + ) -> xr.DataArray: + """ + Convert a standard DataFrame with time index to a DataArray. + + Args: + df: The DataFrame to convert + timesteps: The time index + scenarios: Optional scenario index + coords: Dictionary mapping dimension names to coordinate indexes + dims: Tuple of dimension names + + Returns: + DataArray created from the DataFrame + + Raises: + ConversionError: If the DataFrame's index doesn't match timesteps or has multiple columns + """ + if not df.index.equals(timesteps): + raise ConversionError("DataFrame index doesn't match timesteps index") + if len(df.columns) != 1: + raise ConversionError('DataFrame must have exactly one column') + + # Get values + values = df.values.flatten() + + if scenarios is not None: + # Broadcast to scenarios dimension + values = np.tile(values, (len(scenarios), 1)) + + return xr.DataArray(values, coords=coords, dims=dims) + + @staticmethod + def _convert_series( + series: pd.Series, + timesteps: pd.DatetimeIndex, + scenarios: Optional[pd.Index], + coords: Dict[str, pd.Index], + dims: Tuple[str, ...], + ) -> xr.DataArray: + """ + Convert a Series to a DataArray. + + Args: + series: The Series to convert + timesteps: The time index + scenarios: Optional scenario index + coords: Dictionary mapping dimension names to coordinate indexes + dims: Tuple of dimension names + + Returns: + DataArray created from the Series + + Raises: + ConversionError: If the Series cannot be converted to the expected dimensions + """ + # Case 1: Series with MultiIndex (scenario, time) + if ( + isinstance(series.index, pd.MultiIndex) + and len(series.index.names) == 2 + and 'scenario' in series.index.names + and 'time' in series.index.names + and scenarios is not None + ): + return DataConverter._convert_multi_index_series(series, timesteps, scenarios, coords, dims) + + # Case 2: Standard Series with time index + elif not isinstance(series.index, pd.MultiIndex): + return DataConverter._convert_standard_series(series, timesteps, scenarios, coords, dims) + + else: + raise ConversionError('Unsupported Series index structure') + + @staticmethod + def _convert_multi_index_series( + series: pd.Series, + timesteps: pd.DatetimeIndex, + scenarios: pd.Index, + coords: Dict[str, pd.Index], + dims: Tuple[str, ...], + ) -> xr.DataArray: + """ + Convert a Series with MultiIndex (scenario, time) to a DataArray. + + Args: + series: The Series with MultiIndex to convert + timesteps: The time index + scenarios: The scenario index + coords: Dictionary mapping dimension names to coordinate indexes + dims: Tuple of dimension names + + Returns: + DataArray created from the MultiIndex Series + + Raises: + ConversionError: If the Series' index doesn't match expected + """ + # Validate that the index contains the expected values + if not set(series.index.get_level_values('time')).issubset(set(timesteps)): + raise ConversionError("Series time index doesn't match or isn't a subset of timesteps") + if not set(series.index.get_level_values('scenario')).issubset(set(scenarios)): + raise ConversionError("Series scenario index doesn't match or isn't a subset of scenarios") + + # Reindex to ensure complete coverage and correct order + multi_idx = pd.MultiIndex.from_product([scenarios, timesteps], names=['scenario', 'time']) + reindexed = series.reindex(multi_idx) + + # Reshape to 2D array + reshaped = reindexed.values.reshape(len(scenarios), len(timesteps)) + return xr.DataArray(reshaped, coords=coords, dims=dims) + + @staticmethod + def _convert_standard_series( + series: pd.Series, + timesteps: pd.DatetimeIndex, + scenarios: Optional[pd.Index], + coords: Dict[str, pd.Index], + dims: Tuple[str, ...], + ) -> xr.DataArray: + """ + Convert a standard Series with time index to a DataArray. + + Args: + series: The Series to convert + timesteps: The time index + scenarios: Optional scenario index + coords: Dictionary mapping dimension names to coordinate indexes + dims: Tuple of dimension names + + Returns: + DataArray created from the Series + + Raises: + ConversionError: If the Series' index doesn't match timesteps + """ + if not series.index.equals(timesteps): + raise ConversionError("Series index doesn't match timesteps index") + + # Get values + values = series.values + + if scenarios is not None: + # Broadcast to scenarios dimension + values = np.tile(values, (len(scenarios), 1)) + + return xr.DataArray(values, coords=coords, dims=dims) + + @staticmethod + def _convert_ndarray( + arr: np.ndarray, + timesteps: pd.DatetimeIndex, + scenarios: Optional[pd.Index], + coords: Dict[str, pd.Index], + dims: Tuple[str, ...], + expected_shape: Tuple[int, ...], + ) -> xr.DataArray: + """ + Convert a numpy array to a DataArray. + + Args: + arr: The numpy array to convert + timesteps: The time index + scenarios: Optional scenario index + coords: Dictionary mapping dimension names to coordinate indexes + dims: Tuple of dimension names + expected_shape: Expected shape of the resulting array + + Returns: + DataArray created from the numpy array + + Raises: + ConversionError: If the array cannot be converted to the expected dimensions + """ + # Case 1: With scenarios - array can be 1D or 2D + if scenarios is not None: + return DataConverter._convert_ndarray_with_scenarios( + arr, timesteps, scenarios, coords, dims, expected_shape + ) + + # Case 2: Without scenarios - array must be 1D + else: + return DataConverter._convert_ndarray_without_scenarios(arr, timesteps, coords, dims) + + @staticmethod + def _convert_ndarray_with_scenarios( + arr: np.ndarray, + timesteps: pd.DatetimeIndex, + scenarios: pd.Index, + coords: Dict[str, pd.Index], + dims: Tuple[str, ...], + expected_shape: Tuple[int, ...], + ) -> xr.DataArray: + """ + Convert a numpy array to a DataArray with scenarios dimension. + + Args: + arr: The numpy array to convert + timesteps: The time index + scenarios: The scenario index + coords: Dictionary mapping dimension names to coordinate indexes + dims: Tuple of dimension names + expected_shape: Expected shape (scenarios, timesteps) + + Returns: + DataArray created from the numpy array + + Raises: + ConversionError: If the array dimensions don't match expected + """ + if arr.ndim == 1: + # 1D array should match timesteps and be broadcast to scenarios + if arr.shape[0] != len(timesteps): + raise ConversionError(f"1D array length {arr.shape[0]} doesn't match timesteps length {len(timesteps)}") + # Broadcast to scenarios + values = np.tile(arr, (len(scenarios), 1)) + return xr.DataArray(values, coords=coords, dims=dims) + + elif arr.ndim == 2: + # 2D array should match (scenarios, timesteps) + if arr.shape != expected_shape: + raise ConversionError(f"2D array shape {arr.shape} doesn't match expected shape {expected_shape}") + return xr.DataArray(arr, coords=coords, dims=dims) + + else: + raise ConversionError(f'Array must be 1D or 2D, got {arr.ndim}D') + + @staticmethod + def _convert_ndarray_without_scenarios( + arr: np.ndarray, timesteps: pd.DatetimeIndex, coords: Dict[str, pd.Index], dims: Tuple[str, ...] + ) -> xr.DataArray: + """ + Convert a numpy array to a DataArray without scenarios dimension. + + Args: + arr: The numpy array to convert + timesteps: The time index + coords: Dictionary mapping dimension names to coordinate indexes + dims: Tuple of dimension names + + Returns: + DataArray created from the numpy array + + Raises: + ConversionError: If the array isn't 1D or doesn't match timesteps length + """ + if arr.ndim != 1: + raise ConversionError(f'Without scenarios, array must be 1D, got {arr.ndim}D') + if arr.shape[0] != len(timesteps): + raise ConversionError(f"Array shape {arr.shape} doesn't match expected length {len(timesteps)}") + return xr.DataArray(arr, coords=coords, dims=dims) + + @staticmethod + def _convert_dataarray( + da: xr.DataArray, + timesteps: pd.DatetimeIndex, + scenarios: Optional[pd.Index], + coords: Dict[str, pd.Index], + dims: Tuple[str, ...], + ) -> xr.DataArray: + """ + Convert an existing DataArray to a new DataArray with the desired dimensions. + + Args: + da: The DataArray to convert + timesteps: The time index + scenarios: Optional scenario index + coords: Dictionary mapping dimension names to coordinate indexes + dims: Tuple of dimension names + + Returns: + New DataArray with the specified coordinates and dimensions + + Raises: + ConversionError: If the DataArray dimensions don't match expected + """ + # Case 1: DataArray with only time dimension when scenarios are provided + if scenarios is not None and set(da.dims) == {'time'}: + return DataConverter._broadcast_time_only_dataarray(da, timesteps, scenarios, coords, dims) + + # Case 2: DataArray dimensions should match expected + elif set(da.dims) != set(dims): + raise ConversionError(f"DataArray dimensions {da.dims} don't match expected {dims}") + + # Validate dimensions sizes + for dim in dims: + if not np.array_equal(da.coords[dim].values, coords[dim].values): + raise ConversionError(f"DataArray dimension '{dim}' doesn't match expected {coords[dim]}") + + # Create a new DataArray with our coordinates to ensure consistency + result = xr.DataArray(da.values.copy(), coords=coords, dims=dims) + return result + + @staticmethod + def _broadcast_time_only_dataarray( + da: xr.DataArray, + timesteps: pd.DatetimeIndex, + scenarios: pd.Index, + coords: Dict[str, pd.Index], + dims: Tuple[str, ...], + ) -> xr.DataArray: + """ + Broadcast a time-only DataArray to include the scenarios dimension. + + Args: + da: The DataArray with only time dimension + timesteps: The time index + scenarios: The scenario index + coords: Dictionary mapping dimension names to coordinate indexes + dims: Tuple of dimension names + + Returns: + DataArray with the data broadcast to include scenarios dimension + + Raises: + ConversionError: If the DataArray time coordinates aren't compatible with timesteps + """ + # Ensure the time dimension is compatible + if not np.array_equal(da.coords['time'].values, timesteps.values): + raise ConversionError("DataArray time coordinates aren't compatible with timesteps") + + # Broadcast to scenarios + values = np.tile(da.values.copy(), (len(scenarios), 1)) + return xr.DataArray(values, coords=coords, dims=dims) class TimeSeriesData: @@ -146,18 +654,19 @@ class TimeSeries: name (str): The name of the time series aggregation_weight (Optional[float]): Weight used for aggregation aggregation_group (Optional[str]): Group name for shared aggregation weighting - needs_extra_timestep (bool): Whether this series needs an extra timestep + has_extra_timestep (bool): Whether this series needs an extra timestep """ @classmethod def from_datasource( cls, - data: NumericData, + data: NumericDataTS, name: str, timesteps: pd.DatetimeIndex, + scenarios: Optional[pd.Index] = None, aggregation_weight: Optional[float] = None, aggregation_group: Optional[str] = None, - needs_extra_timestep: bool = False, + has_extra_timestep: bool = False, ) -> 'TimeSeries': """ Initialize the TimeSeries from multiple data sources. @@ -166,19 +675,20 @@ def from_datasource( data: The time series data name: The name of the TimeSeries timesteps: The timesteps of the TimeSeries + scenarios: The scenarios of the TimeSeries aggregation_weight: The weight in aggregation calculations aggregation_group: Group this TimeSeries belongs to for aggregation weight sharing - needs_extra_timestep: Whether this series requires an extra timestep + has_extra_timestep: Whether this series requires an extra timestep Returns: A new TimeSeries instance """ return cls( - DataConverter.as_dataarray(data, timesteps), + DataConverter.as_dataarray(data, timesteps, scenarios), name, aggregation_weight, aggregation_group, - needs_extra_timestep, + has_extra_timestep, ) @classmethod @@ -212,7 +722,7 @@ def from_json(cls, data: Optional[Dict[str, Any]] = None, path: Optional[str] = name=data['name'], aggregation_weight=data['aggregation_weight'], aggregation_group=data['aggregation_group'], - needs_extra_timestep=data['needs_extra_timestep'], + has_extra_timestep=data['has_extra_timestep'], ) def __init__( @@ -221,7 +731,7 @@ def __init__( name: str, aggregation_weight: Optional[float] = None, aggregation_group: Optional[str] = None, - needs_extra_timestep: bool = False, + has_extra_timestep: bool = False, ): """ Initialize a TimeSeries with a DataArray. @@ -231,35 +741,42 @@ def __init__( name: The name of the TimeSeries aggregation_weight: The weight in aggregation calculations aggregation_group: Group this TimeSeries belongs to for weight sharing - needs_extra_timestep: Whether this series requires an extra timestep + has_extra_timestep: Whether this series requires an extra timestep Raises: - ValueError: If data doesn't have a 'time' index or has more than 1 dimension + ValueError: If data doesn't have a 'time' index or has unsupported dimensions """ if 'time' not in data.indexes: raise ValueError(f'DataArray must have a "time" index. Got {data.indexes}') - if data.ndim > 1: - raise ValueError(f'Number of dimensions of DataArray must be 1. Got {data.ndim}') + + allowed_dims = {'time', 'scenario'} + if not set(data.dims).issubset(allowed_dims): + raise ValueError(f'DataArray dimensions must be subset of {allowed_dims}. Got {data.dims}') self.name = name self.aggregation_weight = aggregation_weight self.aggregation_group = aggregation_group - self.needs_extra_timestep = needs_extra_timestep + self.has_extra_timestep = has_extra_timestep # Data management self._stored_data = data.copy(deep=True) self._backup = self._stored_data.copy(deep=True) - self._active_timesteps = self._stored_data.indexes['time'] - self._active_data = None - self._update_active_data() - def reset(self): + # Selection state + self._selected_timesteps: Optional[pd.DatetimeIndex] = None + self._selected_scenarios: Optional[pd.Index] = None + + # Flag for whether this series has scenarios + self._has_scenarios = 'scenario' in data.dims + + def reset(self) -> None: """ - Reset active timesteps to the full set of stored timesteps. + Reset selections to include all timesteps and scenarios. + This is equivalent to clearing all selections. """ - self.active_timesteps = None + self.clear_selection() - def restore_data(self): + def restore_data(self) -> None: """ Restore stored_data from the backup and reset active timesteps. """ @@ -280,8 +797,8 @@ def to_json(self, path: Optional[pathlib.Path] = None) -> Dict[str, Any]: 'name': self.name, 'aggregation_weight': self.aggregation_weight, 'aggregation_group': self.aggregation_group, - 'needs_extra_timestep': self.needs_extra_timestep, - 'data': self.active_data.to_dict(), + 'has_extra_timestep': self.has_extra_timestep, + 'data': self.selected_data.to_dict(), } # Convert datetime objects to ISO strings @@ -303,84 +820,100 @@ def stats(self) -> str: Returns: String representation of data statistics """ - return get_numeric_stats(self.active_data, padd=0) - - def _update_active_data(self): - """ - Update the active data based on active_timesteps. - """ - self._active_data = self._stored_data.sel(time=self.active_timesteps) + return get_numeric_stats(self.selected_data, padd=0, by_scenario=True) @property def all_equal(self) -> bool: """Check if all values in the series are equal.""" - return np.unique(self.active_data.values).size == 1 + return np.unique(self.selected_data.values).size == 1 @property - def active_timesteps(self) -> pd.DatetimeIndex: - """Get the current active timesteps.""" - return self._active_timesteps - - @active_timesteps.setter - def active_timesteps(self, timesteps: Optional[pd.DatetimeIndex]): + def selected_data(self) -> xr.DataArray: """ - Set active_timesteps and refresh active_data. - - Args: - timesteps: New timesteps to activate, or None to use all stored timesteps - - Raises: - TypeError: If timesteps is not a pandas DatetimeIndex or None + Get a view of stored_data based on current selections. + This computes the view dynamically based on the current selection state. """ - if timesteps is None: - self._active_timesteps = self.stored_data.indexes['time'] - elif isinstance(timesteps, pd.DatetimeIndex): - self._active_timesteps = timesteps - else: - raise TypeError('active_timesteps must be a pandas DatetimeIndex or None') + return self._stored_data.sel(**self._valid_selector) - self._update_active_data() + @property + def active_timesteps(self) -> pd.DatetimeIndex: + """Get the current active timesteps.""" + if self._selected_timesteps is None: + return self._stored_data.indexes['time'] + return self._selected_timesteps @property - def active_data(self) -> xr.DataArray: - """Get a view of stored_data based on active_timesteps.""" - return self._active_data + def active_scenarios(self) -> Optional[pd.Index]: + """Get the current active scenarios.""" + if not self._has_scenarios: + return None + if self._selected_scenarios is None: + return self._stored_data.indexes['scenario'] + return self._selected_scenarios @property def stored_data(self) -> xr.DataArray: """Get a copy of the full stored data.""" return self._stored_data.copy() - @stored_data.setter - def stored_data(self, value: NumericData): + def update_stored_data(self, value: xr.DataArray) -> None: """ - Update stored_data and refresh active_data. + Update stored_data and refresh selected_data. Args: value: New data to store """ - new_data = DataConverter.as_dataarray(value, timesteps=self.active_timesteps) + new_data = DataConverter.as_dataarray( + value, + timesteps=self.active_timesteps, + scenarios=self.active_scenarios if self._has_scenarios else None + ) # Skip if data is unchanged to avoid overwriting backup if new_data.equals(self._stored_data): return self._stored_data = new_data - self.active_timesteps = None # Reset to full timeline + self.clear_selection() # Reset selections to full dataset + + def clear_selection(self, timesteps: bool = True, scenarios: bool = True) -> None: + if timesteps: + self._selected_timesteps = None + if scenarios: + self._selected_scenarios = None + + def set_selection(self, timesteps: Optional[pd.DatetimeIndex] = None, scenarios: Optional[pd.Index] = None) -> None: + if timesteps is None: + self.clear_selection(timesteps=True, scenarios=False) + else: + self._selected_timesteps = timesteps + + if scenarios is None: + self.clear_selection(timesteps=False, scenarios=True) + else: + self._selected_scenarios = scenarios @property def sel(self): - return self.active_data.sel + """Direct access to the selected_data's sel method for convenience.""" + return self.selected_data.sel @property def isel(self): - return self.active_data.isel + """Direct access to the selected_data's isel method for convenience.""" + return self.selected_data.isel + + @property + def _valid_selector(self) -> Dict[str, pd.Index]: + """Get the current selection as a dictionary.""" + full_selection = {'time': self._selected_timesteps, 'scenario': self._selected_scenarios} + return {dim: sel for dim, sel in full_selection.items() if dim in self._stored_data.dims and sel is not None} def _apply_operation(self, other, op): """Apply an operation between this TimeSeries and another object.""" if isinstance(other, TimeSeries): - other = other.active_data - return op(self.active_data, other) + other = other.selected_data + return op(self.selected_data, other) def __add__(self, other): return self._apply_operation(other, lambda x, y: x + y) @@ -395,25 +928,25 @@ def __truediv__(self, other): return self._apply_operation(other, lambda x, y: x / y) def __radd__(self, other): - return other + self.active_data + return other + self.selected_data def __rsub__(self, other): - return other - self.active_data + return other - self.selected_data def __rmul__(self, other): - return other * self.active_data + return other * self.selected_data def __rtruediv__(self, other): - return other / self.active_data + return other / self.selected_data def __neg__(self) -> xr.DataArray: - return -self.active_data + return -self.selected_data def __pos__(self) -> xr.DataArray: - return +self.active_data + return +self.selected_data def __abs__(self) -> xr.DataArray: - return abs(self.active_data) + return abs(self.selected_data) def __gt__(self, other): """ @@ -426,7 +959,7 @@ def __gt__(self, other): True if all values in this TimeSeries are greater than other """ if isinstance(other, TimeSeries): - return (self.active_data > other.active_data).all().item() + return (self.selected_data > other.selected_data).all().item() return NotImplemented def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): @@ -435,8 +968,8 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): This allows NumPy functions to work with TimeSeries objects. """ - # Convert any TimeSeries inputs to their active_data - inputs = [x.active_data if isinstance(x, TimeSeries) else x for x in inputs] + # Convert any TimeSeries inputs to their selected_data + inputs = [x.selected_data if isinstance(x, TimeSeries) else x for x in inputs] return getattr(ufunc, method)(*inputs, **kwargs) def __repr__(self): @@ -450,10 +983,10 @@ def __repr__(self): 'name': self.name, 'aggregation_weight': self.aggregation_weight, 'aggregation_group': self.aggregation_group, - 'needs_extra_timestep': self.needs_extra_timestep, - 'shape': self.active_data.shape, - 'time_range': f'{self.active_timesteps[0]} to {self.active_timesteps[-1]}', + 'has_extra_timestep': self.has_extra_timestep, + 'shape': self.selected_data.shape, } + attr_str = ', '.join(f'{k}={repr(v)}' for k, v in attrs.items()) return f'TimeSeries({attr_str})' @@ -464,281 +997,329 @@ def __str__(self): Returns: Descriptive string with statistics """ - return f"TimeSeries '{self.name}': {self.stats}" + return f'TimeSeries "{self.name}":\n{textwrap.indent(self.stats, " ")}' class TimeSeriesCollection: """ - Collection of TimeSeries objects with shared timestep management. + Simplified central manager for time series data with reference tracking. - TimeSeriesCollection handles multiple TimeSeries objects with synchronized - timesteps, provides operations on collections, and manages extra timesteps. + Provides a way to store time series data and work with subsets of dimensions + that automatically update all references when changed. """ - def __init__( self, timesteps: pd.DatetimeIndex, + scenarios: Optional[pd.Index] = None, hours_of_last_timestep: Optional[float] = None, hours_of_previous_timesteps: Optional[Union[float, np.ndarray]] = None, ): - """ - Args: - timesteps: The timesteps of the Collection. - hours_of_last_timestep: The duration of the last time step. Uses the last time interval if not specified - hours_of_previous_timesteps: The duration of previous timesteps. - If None, the first time increment of time_series is used. - This is needed to calculate previous durations (for example consecutive_on_hours). - If you use an array, take care that its long enough to cover all previous values! - """ - # Prepare and validate timesteps + """Initialize a TimeSeriesCollection.""" self._validate_timesteps(timesteps) self.hours_of_previous_timesteps = self._calculate_hours_of_previous_timesteps( timesteps, hours_of_previous_timesteps - ) + ) #TODO: Make dynamic - # Set up timesteps and hours - self.all_timesteps = timesteps - self.all_timesteps_extra = self._create_timesteps_with_extra(timesteps, hours_of_last_timestep) - self.all_hours_per_timestep = self.calculate_hours_per_timestep(self.all_timesteps_extra) + self._full_timesteps = timesteps + self._full_timesteps_extra = self._create_timesteps_with_extra(timesteps, hours_of_last_timestep) + self._full_hours_per_timestep = self.calculate_hours_per_timestep(self._full_timesteps_extra) - # Active timestep tracking - self._active_timesteps = None - self._active_timesteps_extra = None - self._active_hours_per_timestep = None + self._full_scenarios = scenarios - # Dictionary of time series by name - self.time_series_data: Dict[str, TimeSeries] = {} + # Series that need extra timestep + self._has_extra_timestep: set = set() - # Aggregation - self.group_weights: Dict[str, float] = {} - self.weights: Dict[str, float] = {} + # Storage for TimeSeries objects + self._time_series: Dict[str, TimeSeries] = {} - @classmethod - def with_uniform_timesteps( - cls, start_time: pd.Timestamp, periods: int, freq: str, hours_per_step: Optional[float] = None - ) -> 'TimeSeriesCollection': - """Create a collection with uniform timesteps.""" - timesteps = pd.date_range(start_time, periods=periods, freq=freq, name='time') - return cls(timesteps, hours_of_previous_timesteps=hours_per_step) - - def create_time_series( - self, data: Union[NumericData, TimeSeriesData], name: str, needs_extra_timestep: bool = False + # Active subset selectors + self._selected_timesteps: Optional[pd.DatetimeIndex] = None + self._selected_scenarios: Optional[pd.Index] = None + self._selected_timesteps_extra: Optional[pd.DatetimeIndex] = None + self._selected_hours_per_timestep: Optional[xr.DataArray] = None + + def add_time_series( + self, + name: str, + data: Union[NumericDataTS, TimeSeries], + aggregation_weight: Optional[float] = None, + aggregation_group: Optional[str] = None, + has_extra_timestep: bool = False, ) -> TimeSeries: """ - Creates a TimeSeries from the given data and adds it to the collection. + Add a new TimeSeries to the allocator. Args: - data: The data to create the TimeSeries from. - name: The name of the TimeSeries. - needs_extra_timestep: Whether to create an additional timestep at the end of the timesteps. - The data to create the TimeSeries from. + name: Name of the time series + data: Data for the time series (can be raw data or an existing TimeSeries) + aggregation_weight: Weight used for aggregation + aggregation_group: Group name for shared aggregation weighting + has_extra_timestep: Whether this series needs an extra timestep Returns: - The created TimeSeries. - + The created TimeSeries object """ - # Check for duplicate name - if name in self.time_series_data: - raise ValueError(f"TimeSeries '{name}' already exists in this collection") - - # Determine which timesteps to use - timesteps_to_use = self.timesteps_extra if needs_extra_timestep else self.timesteps - - # Create the time series - if isinstance(data, TimeSeriesData): - time_series = TimeSeries.from_datasource( + if name in self._time_series: + raise KeyError(f"TimeSeries '{name}' already exists in allocator") + + # Choose which timesteps to use + target_timesteps = self.timesteps_extra if has_extra_timestep else self.timesteps + + # Create or adapt the TimeSeries object + if isinstance(data, TimeSeries): + # Use the existing TimeSeries but update its parameters + time_series = data + # Update the stored data to use our timesteps and scenarios + data_array = DataConverter.as_dataarray( + time_series.stored_data, timesteps=target_timesteps, scenarios=self.scenarios + ) + time_series = TimeSeries( + data=data_array, name=name, - data=data.data, - timesteps=timesteps_to_use, - aggregation_weight=data.agg_weight, - aggregation_group=data.agg_group, - needs_extra_timestep=needs_extra_timestep, + aggregation_weight=aggregation_weight or time_series.aggregation_weight, + aggregation_group=aggregation_group or time_series.aggregation_group, + has_extra_timestep=has_extra_timestep or time_series.has_extra_timestep, ) - # Connect the user time series to the created TimeSeries - data.label = name else: + # Create a new TimeSeries from raw data time_series = TimeSeries.from_datasource( - name=name, data=data, timesteps=timesteps_to_use, needs_extra_timestep=needs_extra_timestep + data=data, + name=name, + timesteps=target_timesteps, + scenarios=self.scenarios, + aggregation_weight=aggregation_weight, + aggregation_group=aggregation_group, + has_extra_timestep=has_extra_timestep, ) - # Add to the collection - self.add_time_series(time_series) + # Add to storage + self._time_series[name] = time_series - return time_series + # Track if it needs extra timestep + if has_extra_timestep: + self._has_extra_timestep.add(name) - def calculate_aggregation_weights(self) -> Dict[str, float]: - """Calculate and return aggregation weights for all time series.""" - self.group_weights = self._calculate_group_weights() - self.weights = self._calculate_weights() - - if np.all(np.isclose(list(self.weights.values()), 1, atol=1e-6)): - logger.info('All Aggregation weights were set to 1') - - return self.weights + # Return the TimeSeries object + return time_series - def activate_timesteps(self, active_timesteps: Optional[pd.DatetimeIndex] = None): + def clear_selection(self, timesteps: bool = True, scenarios: bool = True) -> None: """ - Update active timesteps for the collection and all time series. - If no arguments are provided, the active timesteps are reset. + Clear selection for timesteps and/or scenarios. Args: - active_timesteps: The active timesteps of the model. - If None, the all timesteps of the TimeSeriesCollection are taken. + timesteps: Whether to clear timesteps selection + scenarios: Whether to clear scenarios selection """ - if active_timesteps is None: - return self.reset() + if timesteps: + self._update_selected_timesteps(timesteps=None) + if scenarios: + self._selected_scenarios = None - if not np.all(np.isin(active_timesteps, self.all_timesteps)): - raise ValueError('active_timesteps must be a subset of the timesteps of the TimeSeriesCollection') + # Apply the selection to all TimeSeries objects + self._propagate_selection_to_time_series() - # Calculate derived timesteps - self._active_timesteps = active_timesteps - first_ts_index = np.where(self.all_timesteps == active_timesteps[0])[0][0] - last_ts_idx = np.where(self.all_timesteps == active_timesteps[-1])[0][0] - self._active_timesteps_extra = self.all_timesteps_extra[first_ts_index : last_ts_idx + 2] - self._active_hours_per_timestep = self.all_hours_per_timestep.isel(time=slice(first_ts_index, last_ts_idx + 1)) + def set_selection(self, timesteps: Optional[pd.DatetimeIndex] = None, scenarios: Optional[pd.Index] = None) -> None: + """ + Set active subset for timesteps and scenarios. - # Update all time series - self._update_time_series_timesteps() + Args: + timesteps: Timesteps to activate, or None to clear + scenarios: Scenarios to activate, or None to clear + """ + if timesteps is None: + self.clear_selection(timesteps=True, scenarios=False) + else: + self._update_selected_timesteps(timesteps) - def reset(self): - """Reset active timesteps to defaults for all time series.""" - self._active_timesteps = None - self._active_timesteps_extra = None - self._active_hours_per_timestep = None + if scenarios is None: + self.clear_selection(timesteps=False, scenarios=True) + else: + self._selected_scenarios = scenarios - for time_series in self.time_series_data.values(): - time_series.reset() + # Apply the selection to all TimeSeries objects + self._propagate_selection_to_time_series() - def restore_data(self): - """Restore original data for all time series.""" - for time_series in self.time_series_data.values(): - time_series.restore_data() + def _update_selected_timesteps(self, timesteps: Optional[pd.DatetimeIndex]) -> None: + """ + Updates the timestep and related metrics (timesteps_extra, hours_per_timestep) based on the current selection. + """ + if timesteps is None: + self._selected_timesteps = None + self._selected_timesteps_extra = None + self._selected_hours_per_timestep = None + return - def add_time_series(self, time_series: TimeSeries): - """Add an existing TimeSeries to the collection.""" - if time_series.name in self.time_series_data: - raise ValueError(f"TimeSeries '{time_series.name}' already exists in this collection") + self._validate_timesteps(timesteps, self._full_timesteps) - self.time_series_data[time_series.name] = time_series + self._selected_timesteps = timesteps + self._selected_hours_per_timestep = self._full_hours_per_timestep.sel(time=timesteps) + self._selected_timesteps_extra = self._create_timesteps_with_extra( + timesteps, self._selected_hours_per_timestep.isel(time=-1).max().item() + ) - def insert_new_data(self, data: pd.DataFrame, include_extra_timestep: bool = False): + def as_dataset(self, with_extra_timestep: bool = True, with_constants: bool = True) -> xr.Dataset: """ - Update time series with new data from a DataFrame. + Convert the TimeSeriesCollection to a xarray Dataset, containing the data of each TimeSeries. Args: - data: DataFrame containing new data with timestamps as index - include_extra_timestep: Whether the provided data already includes the extra timestep, by default False + with_extra_timestep: Whether to exclude the extra timesteps. + Effectively, this removes the last timestep for certain TImeSeries, but mitigates the presence of NANs in others. + with_constants: Whether to exclude TimeSeries with a constant value from the dataset. """ - if not isinstance(data, pd.DataFrame): - raise TypeError(f'data must be a pandas DataFrame, got {type(data).__name__}') + if self.scenarios is None: + ds = xr.Dataset(coords={'time': self.timesteps_extra}) + else: + ds = xr.Dataset(coords={'scenario': self.scenarios, 'time': self.timesteps_extra}) + + for ts in self._time_series.values(): + if not with_constants and ts.all_equal: + continue + ds[ts.name] = ts.selected_data - # Check if the DataFrame index matches the expected timesteps - expected_timesteps = self.timesteps_extra if include_extra_timestep else self.timesteps - if not data.index.equals(expected_timesteps): - raise ValueError( - f'DataFrame index must match {"collection timesteps with extra timestep" if include_extra_timestep else "collection timesteps"}' + if not with_extra_timestep: + return ds.sel(time=self.timesteps) + + return ds + + @property + def timesteps(self) -> pd.DatetimeIndex: + """Get the current active timesteps.""" + if self._selected_timesteps is None: + return self._full_timesteps + return self._selected_timesteps + + @property + def timesteps_extra(self) -> pd.DatetimeIndex: + """Get the current active timesteps with extra timestep.""" + if self._selected_timesteps_extra is None: + return self._full_timesteps_extra + return self._selected_timesteps_extra + + @property + def hours_per_timestep(self) -> xr.DataArray: + """Get the current active hours per timestep.""" + if self._selected_hours_per_timestep is None: + return self._full_hours_per_timestep + return self._selected_hours_per_timestep + + @property + def scenarios(self) -> Optional[pd.Index]: + """Get the current active scenarios.""" + if self._selected_scenarios is None: + return self._full_scenarios + return self._selected_scenarios + + def _propagate_selection_to_time_series(self) -> None: + """Apply the current selection to all TimeSeries objects.""" + for ts_name, ts in self._time_series.items(): + timesteps = self._selected_timesteps_extra if ts_name in self._has_extra_timestep else self._selected_timesteps + ts.set_selection( + timesteps=timesteps, + scenarios=self._selected_scenarios ) - for name, ts in self.time_series_data.items(): - if name in data.columns: - if not ts.needs_extra_timestep: - # For time series without extra timestep - if include_extra_timestep: - # If data includes extra timestep but series doesn't need it, exclude the last point - ts.stored_data = data[name].iloc[:-1] - else: - # Use data as is - ts.stored_data = data[name] - else: - # For time series with extra timestep - if include_extra_timestep: - # Data already includes extra timestep - ts.stored_data = data[name] - else: - # Need to add extra timestep - extrapolate from the last value - extra_step_value = data[name].iloc[-1] - extra_step_index = pd.DatetimeIndex([self.timesteps_extra[-1]], name='time') - extra_step_series = pd.Series([extra_step_value], index=extra_step_index) - - # Combine the regular data with the extra timestep - ts.stored_data = pd.concat([data[name], extra_step_series]) - - logger.debug(f'Updated data for {name}') - - def to_dataframe( - self, filtered: Literal['all', 'constant', 'non_constant'] = 'non_constant', include_extra_timestep: bool = True - ) -> pd.DataFrame: - """ - Convert collection to DataFrame with optional filtering and timestep control. + def __getitem__(self, name: str) -> TimeSeries: + """ + Get a reference to a time series or data array. Args: - filtered: Filter time series by variability, by default 'non_constant' - include_extra_timestep: Whether to include the extra timestep in the result, by default True + name: Name of the data array or time series Returns: - DataFrame representation of the collection + TimeSeries object if it exists, otherwise DataArray with current selection applied """ - include_constants = filtered != 'non_constant' - ds = self.to_dataset(include_constants=include_constants) - - if not include_extra_timestep: - ds = ds.isel(time=slice(None, -1)) + # First check if this is a TimeSeries + if name in self._time_series: + # Return the TimeSeries object (it will handle selection internally) + return self._time_series[name] + raise ValueError(f'No TimeSeries named "{name}" found') + + def __contains__(self, value) -> bool: + if isinstance(value, str): + return value in self._time_series + elif isinstance(value, TimeSeries): + return value.name in self._time_series + raise TypeError(f'Invalid type for __contains__ of {self.__class__.__name__}: {type(value)}') - df = ds.to_dataframe() - - # Apply filtering - if filtered == 'all': - return df - elif filtered == 'constant': - return df.loc[:, df.nunique() == 1] - elif filtered == 'non_constant': - return df.loc[:, df.nunique() > 1] - else: - raise ValueError("filtered must be one of: 'all', 'constant', 'non_constant'") + def __iter__(self) -> Iterator[TimeSeries]: + """Iterate over TimeSeries objects.""" + return iter(self._time_series.values()) - def to_dataset(self, include_constants: bool = True) -> xr.Dataset: + def update_time_series(self, name: str, data: NumericData) -> TimeSeries: """ - Combine all time series into a single Dataset with all timesteps. + Update an existing TimeSeries with new data. Args: - include_constants: Whether to include time series with constant values, by default True + name: Name of the TimeSeries to update + data: New data to assign Returns: - Dataset containing all selected time series with all timesteps - """ - # Determine which series to include - if include_constants: - series_to_include = self.time_series_data.values() - else: - series_to_include = self.non_constants + The updated TimeSeries - # Create individual datasets and merge them - ds = xr.merge([ts.active_data.to_dataset(name=ts.name) for ts in series_to_include]) + Raises: + KeyError: If no TimeSeries with the given name exists + """ + if name not in self._time_series: + raise KeyError(f"No TimeSeries named '{name}' found") - # Ensure the correct time coordinates - ds = ds.reindex(time=self.timesteps_extra) + # Get the TimeSeries + ts = self._time_series[name] - ds.attrs.update( - { - 'timesteps_extra': f'{self.timesteps_extra[0]} ... {self.timesteps_extra[-1]} | len={len(self.timesteps_extra)}', - 'hours_per_timestep': self._format_stats(self.hours_per_timestep), - } + # Convert data to proper format + data_array = DataConverter.as_dataarray( + data, + self.timesteps_extra if name in self._has_extra_timestep else self.timesteps, + self.scenarios ) - return ds + # Update the TimeSeries + ts.update_stored_data(data_array) + + return ts + + def calculate_aggregation_weights(self) -> Dict[str, float]: + """Calculate and return aggregation weights for all time series.""" + group_weights = self._calculate_group_weights() - def _update_time_series_timesteps(self): - """Update active timesteps for all time series.""" - for ts in self.time_series_data.values(): - if ts.needs_extra_timestep: - ts.active_timesteps = self.timesteps_extra + weights = {} + for name, ts in self._time_series.items(): + if ts.aggregation_group is not None: + # Use group weight + weights[name] = group_weights.get(ts.aggregation_group, 1) else: - ts.active_timesteps = self.timesteps + # Use individual weight or default to 1 + weights[name] = ts.aggregation_weight or 1 + + if np.all(np.isclose(list(weights.values()), 1, atol=1e-6)): + logger.info('All Aggregation weights were set to 1') + + return weights + + def _calculate_group_weights(self) -> Dict[str, float]: + """Calculate weights for aggregation groups.""" + # Count series in each group + groups = [ts.aggregation_group for ts in self._time_series.values() if ts.aggregation_group is not None] + group_counts = Counter(groups) + + # Calculate weight for each group (1/count) + return {group: 1 / count for group, count in group_counts.items()} @staticmethod - def _validate_timesteps(timesteps: pd.DatetimeIndex): - """Validate timesteps format and rename if needed.""" + def _validate_timesteps(timesteps: pd.DatetimeIndex, present_timesteps: Optional[pd.DatetimeIndex] = None): + """ + Validate timesteps format and rename if needed. + Args: + timesteps: The timesteps to validate + present_timesteps: The timesteps that are present in the dataset + + Raises: + ValueError: If timesteps is not a pandas DatetimeIndex + ValueError: If timesteps is not at least 2 timestamps + ValueError: If timesteps has a different name than 'time' + ValueError: If timesteps is not sorted + ValueError: If timesteps contains duplicates + ValueError: If timesteps is not a subset of present_timesteps + """ if not isinstance(timesteps, pd.DatetimeIndex): raise TypeError('timesteps must be a pandas DatetimeIndex') @@ -750,6 +1331,18 @@ def _validate_timesteps(timesteps: pd.DatetimeIndex): logger.warning('Renamed timesteps to "time" (was "%s")', timesteps.name) timesteps.name = 'time' + # Ensure timesteps is sorted + if not timesteps.is_monotonic_increasing: + raise ValueError('timesteps must be sorted') + + # Ensure timesteps has no duplicates + if len(timesteps) != len(timesteps.drop_duplicates()): + raise ValueError('timesteps must not contain duplicates') + + # Ensure timesteps is a subset of present_timesteps + if present_timesteps is not None and not set(timesteps).issubset(set(present_timesteps)): + raise ValueError('timesteps must be a subset of present_timesteps') + @staticmethod def _create_timesteps_with_extra( timesteps: pd.DatetimeIndex, hours_of_last_timestep: Optional[float] @@ -787,128 +1380,49 @@ def calculate_hours_per_timestep(timesteps_extra: pd.DatetimeIndex) -> xr.DataAr data=hours_per_step, coords={'time': timesteps_extra[:-1]}, dims=('time',), name='hours_per_step' ) - def _calculate_group_weights(self) -> Dict[str, float]: - """Calculate weights for aggregation groups.""" - # Count series in each group - groups = [ts.aggregation_group for ts in self.time_series_data.values() if ts.aggregation_group is not None] - group_counts = Counter(groups) - # Calculate weight for each group (1/count) - return {group: 1 / count for group, count in group_counts.items()} - - def _calculate_weights(self) -> Dict[str, float]: - """Calculate weights for all time series.""" - # Calculate weight for each time series - weights = {} - for name, ts in self.time_series_data.items(): - if ts.aggregation_group is not None: - # Use group weight - weights[name] = self.group_weights.get(ts.aggregation_group, 1) - else: - # Use individual weight or default to 1 - weights[name] = ts.aggregation_weight or 1 - - return weights - - def _format_stats(self, data) -> str: - """Format statistics for a data array.""" - if hasattr(data, 'values'): - values = data.values - else: - values = np.asarray(data) - - mean_val = np.mean(values) - min_val = np.min(values) - max_val = np.max(values) - - return f'mean: {mean_val:.2f}, min: {min_val:.2f}, max: {max_val:.2f}' - - def __getitem__(self, name: str) -> TimeSeries: - """Get a TimeSeries by name.""" - try: - return self.time_series_data[name] - except KeyError as e: - raise KeyError(f'TimeSeries "{name}" not found in the TimeSeriesCollection') from e - - def __iter__(self) -> Iterator[TimeSeries]: - """Iterate through all TimeSeries in the collection.""" - return iter(self.time_series_data.values()) - - def __len__(self) -> int: - """Get the number of TimeSeries in the collection.""" - return len(self.time_series_data) - - def __contains__(self, item: Union[str, TimeSeries]) -> bool: - """Check if a TimeSeries exists in the collection.""" - if isinstance(item, str): - return item in self.time_series_data - elif isinstance(item, TimeSeries): - return item in self.time_series_data.values() - return False - - @property - def non_constants(self) -> List[TimeSeries]: - """Get time series with varying values.""" - return [ts for ts in self.time_series_data.values() if not ts.all_equal] - - @property - def constants(self) -> List[TimeSeries]: - """Get time series with constant values.""" - return [ts for ts in self.time_series_data.values() if ts.all_equal] - - @property - def timesteps(self) -> pd.DatetimeIndex: - """Get the active timesteps.""" - return self.all_timesteps if self._active_timesteps is None else self._active_timesteps - - @property - def timesteps_extra(self) -> pd.DatetimeIndex: - """Get the active timesteps with extra step.""" - return self.all_timesteps_extra if self._active_timesteps_extra is None else self._active_timesteps_extra - - @property - def hours_per_timestep(self) -> xr.DataArray: - """Get the duration of each active timestep.""" - return ( - self.all_hours_per_timestep if self._active_hours_per_timestep is None else self._active_hours_per_timestep - ) - - @property - def hours_of_last_timestep(self) -> float: - """Get the duration of the last timestep.""" - return float(self.hours_per_timestep[-1].item()) - - def __repr__(self): - return f'TimeSeriesCollection:\n{self.to_dataset()}' - - def __str__(self): - longest_name = max([time_series.name for time_series in self.time_series_data], key=len) - - stats_summary = '\n'.join( - [ - f' - {time_series.name:<{len(longest_name)}}: {get_numeric_stats(time_series.active_data)}' - for time_series in self.time_series_data - ] - ) - - return ( - f'TimeSeriesCollection with {len(self.time_series_data)} series\n' - f' Time Range: {self.timesteps[0]} → {self.timesteps[-1]}\n' - f' No. of timesteps: {len(self.timesteps)} + 1 extra\n' - f' Hours per timestep: {get_numeric_stats(self.hours_per_timestep)}\n' - f' Time Series Data:\n' - f'{stats_summary}' - ) +def get_numeric_stats(data: xr.DataArray, decimals: int = 2, padd: int = 10, by_scenario: bool = False) -> str: + """ + Calculates the mean, median, min, max, and standard deviation of a numeric DataArray. + Args: + data: The DataArray to analyze + decimals: Number of decimal places to show + padd: Padding for alignment + by_scenario: Whether to break down stats by scenario -def get_numeric_stats(data: xr.DataArray, decimals: int = 2, padd: int = 10) -> str: - """Calculates the mean, median, min, max, and standard deviation of a numeric DataArray.""" + Returns: + String representation of data statistics + """ format_spec = f'>{padd}.{decimals}f' if padd else f'.{decimals}f' + + # If by_scenario is True and there's a scenario dimension with multiple values + if by_scenario and 'scenario' in data.dims and data.sizes['scenario'] > 1: + results = [] + for scenario in data.coords['scenario'].values: + scenario_data = data.sel(scenario=scenario) + if np.unique(scenario_data).size == 1: + results.append(f' {scenario}: {scenario_data.max().item():{format_spec}} (constant)') + else: + mean = scenario_data.mean().item() + median = scenario_data.median().item() + min_val = scenario_data.min().item() + max_val = scenario_data.max().item() + std = scenario_data.std().item() + results.append( + f' {scenario}: {mean:{format_spec}} (mean), {median:{format_spec}} (median), ' + f'{min_val:{format_spec}} (min), {max_val:{format_spec}} (max), {std:{format_spec}} (std)' + ) + return '\n'.join(['By scenario:'] + results) + + # Standard logic for non-scenario data or aggregated stats if np.unique(data).size == 1: return f'{data.max().item():{format_spec}} (constant)' + mean = data.mean().item() median = data.median().item() min_val = data.min().item() max_val = data.max().item() std = data.std().item() + return f'{mean:{format_spec}} (mean), {median:{format_spec}} (median), {min_val:{format_spec}} (min), {max_val:{format_spec}} (max), {std:{format_spec}} (std)' diff --git a/flixopt/effects.py b/flixopt/effects.py index 82aa63a43..9b5ea41d6 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -137,10 +137,10 @@ def __init__(self, model: SystemModel, element: Effect): label_full=f'{self.label_full}(operation)', total_max=self.element.maximum_operation, total_min=self.element.minimum_operation, - min_per_hour=self.element.minimum_operation_per_hour.active_data + min_per_hour=self.element.minimum_operation_per_hour.selected_data if self.element.minimum_operation_per_hour is not None else None, - max_per_hour=self.element.maximum_operation_per_hour.active_data + max_per_hour=self.element.maximum_operation_per_hour.selected_data if self.element.maximum_operation_per_hour is not None else None, ) @@ -376,7 +376,7 @@ def _add_share_between_effects(self): for target_effect, time_series in origin_effect.specific_share_to_other_effects_operation.items(): self.effects[target_effect].model.operation.add_share( origin_effect.model.operation.label_full, - origin_effect.model.operation.total_per_timestep * time_series.active_data, + origin_effect.model.operation.total_per_timestep * time_series.selected_data, ) # 2. invest: -> hier ist es Scalar (share) for target_effect, factor in origin_effect.specific_share_to_other_effects_invest.items(): diff --git a/flixopt/elements.py b/flixopt/elements.py index 05898d4e5..95536b910 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -374,7 +374,7 @@ def _create_shares(self): self._model.effects.add_share_to_effects( name=self.label_full, # Use the full label of the element expressions={ - effect: self.flow_rate * self._model.hours_per_step * factor.active_data + effect: self.flow_rate * self._model.hours_per_step * factor.selected_data for effect, factor in self.element.effects_per_flow_hour.items() }, target='operation', @@ -429,8 +429,8 @@ def relative_flow_rate_bounds(self) -> Tuple[NumericData, NumericData]: """Returns relative flow rate bounds.""" fixed_profile = self.element.fixed_relative_profile if fixed_profile is None: - return self.element.relative_minimum.active_data, self.element.relative_maximum.active_data - return fixed_profile.active_data, fixed_profile.active_data + return self.element.relative_minimum.selected_data, self.element.relative_maximum.selected_data + return fixed_profile.selected_data, fixed_profile.selected_data class BusModel(ElementModel): @@ -451,7 +451,7 @@ def do_modeling(self) -> None: # Fehlerplus/-minus: if self.element.with_excess: excess_penalty = np.multiply( - self._model.hours_per_step, self.element.excess_penalty_per_flow_hour.active_data + self._model.hours_per_step, self.element.excess_penalty_per_flow_hour.selected_data ) self.excess_input = self.add( self._model.add_variables(lower=0, coords=self._model.coords, name=f'{self.label_full}|excess_input'), diff --git a/flixopt/features.py b/flixopt/features.py index 92caf9dc2..32c382486 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -441,7 +441,7 @@ def _get_duration_in_hours( if previous_duration + self._model.hours_per_step[0] > first_step_max: logger.warning( - f'The maximum duration of "{variable_name}" is set to {maximum_duration.active_data}h, ' + f'The maximum duration of "{variable_name}" is set to {maximum_duration.selected_data}h, ' f'but the consecutive_duration previous to this model is {previous_duration}h. ' f'This forces "{binary_variable.name} = 0" in the first time step ' f'(dt={self._model.hours_per_step[0]}h)!' @@ -450,7 +450,7 @@ def _get_duration_in_hours( duration_in_hours = self.add( self._model.add_variables( lower=0, - upper=maximum_duration.active_data if maximum_duration is not None else mega, + upper=maximum_duration.selected_data if maximum_duration is not None else mega, coords=self._model.coords, name=f'{self.label_full}|{variable_name}', ), diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 93720de60..e39d71e94 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -35,12 +35,14 @@ class FlowSystem: def __init__( self, timesteps: pd.DatetimeIndex, + scenarios: Optional[pd.Index] = None, hours_of_last_timestep: Optional[float] = None, hours_of_previous_timesteps: Optional[Union[int, float, np.ndarray]] = None, ): """ Args: timesteps: The timesteps of the model. + scenarios: The scenarios of the model. hours_of_last_timestep: The duration of the last time step. Uses the last time interval if not specified hours_of_previous_timesteps: The duration of previous timesteps. If None, the first time increment of time_series is used. @@ -49,6 +51,7 @@ def __init__( """ self.time_series_collection = TimeSeriesCollection( timesteps=timesteps, + scenarios=scenarios, hours_of_last_timestep=hours_of_last_timestep, hours_of_previous_timesteps=hours_of_previous_timesteps, ) @@ -184,7 +187,7 @@ def as_dataset(self, constants_in_dataset: bool = False) -> xr.Dataset: Args: constants_in_dataset: If True, constants are included as Dataset variables. """ - ds = self.time_series_collection.to_dataset(include_constants=constants_in_dataset) + ds = self.time_series_collection.as_dataset() ds.attrs = self.as_dict(data_mode='name') return ds @@ -275,7 +278,7 @@ def create_time_series( self, name: str, data: Optional[Union[NumericData, TimeSeriesData, TimeSeries]], - needs_extra_timestep: bool = False, + has_extra_timestep: bool = False, ) -> Optional[TimeSeries]: """ Tries to create a TimeSeries from NumericData Data and adds it to the time_series_collection @@ -290,11 +293,20 @@ def create_time_series( data.restore_data() if data in self.time_series_collection: return data - return self.time_series_collection.create_time_series( - data=data.active_data, name=name, needs_extra_timestep=needs_extra_timestep + return self.time_series_collection.add_time_series( + data=data.selected_data, name=name, has_extra_timestep=has_extra_timestep ) - return self.time_series_collection.create_time_series( - data=data, name=name, needs_extra_timestep=needs_extra_timestep + elif isinstance(data, TimeSeriesData): + data.label = name + return self.time_series_collection.add_time_series( + data=data.data, + name=name, + has_extra_timestep=has_extra_timestep, + aggregation_weight=data.agg_weight, + aggregation_group=data.agg_group + ) + return self.time_series_collection.add_time_series( + data=data, name=name, has_extra_timestep=has_extra_timestep ) def create_effect_time_series( diff --git a/flixopt/io.py b/flixopt/io.py index 35d927136..5cc353836 100644 --- a/flixopt/io.py +++ b/flixopt/io.py @@ -23,7 +23,7 @@ def replace_timeseries(obj, mode: Literal['name', 'stats', 'data'] = 'name'): return [replace_timeseries(v, mode) for v in obj] elif isinstance(obj, TimeSeries): # Adjust this based on the actual class if obj.all_equal: - return obj.active_data.values[0].item() + return obj.selected_data.values[0].item() elif mode == 'name': return f'::::{obj.name}' elif mode == 'stats': diff --git a/flixopt/structure.py b/flixopt/structure.py index e7f1c62a4..2e136c652 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -534,7 +534,7 @@ def copy_and_convert_datatypes(data: Any, use_numpy: bool = True, use_element_la return copy_and_convert_datatypes(data.tolist(), use_numpy, use_element_label) elif isinstance(data, TimeSeries): - return copy_and_convert_datatypes(data.active_data, use_numpy, use_element_label) + return copy_and_convert_datatypes(data.selected_data, use_numpy, use_element_label) elif isinstance(data, TimeSeriesData): return copy_and_convert_datatypes(data.data, use_numpy, use_element_label) diff --git a/tests/test_dataconverter.py b/tests/test_dataconverter.py index 49f1438e7..579de9c00 100644 --- a/tests/test_dataconverter.py +++ b/tests/test_dataconverter.py @@ -3,69 +3,848 @@ import pytest import xarray as xr -from flixopt.core import ConversionError, DataConverter # Adjust this import to match your project structure +from flixopt.core import ( # Adjust this import to match your project structure + ConversionError, + DataConverter, + TimeSeries, +) @pytest.fixture -def sample_time_index(request): - index = pd.date_range('2024-01-01', periods=5, freq='D', name='time') - return index +def sample_time_index(): + return pd.date_range('2024-01-01', periods=5, freq='D', name='time') -def test_scalar_conversion(sample_time_index): - # Test scalar conversion - result = DataConverter.as_dataarray(42, sample_time_index) - assert isinstance(result, xr.DataArray) - assert result.shape == (len(sample_time_index),) - assert result.dims == ('time',) - assert np.all(result.values == 42) +@pytest.fixture +def sample_scenario_index(): + return pd.Index(['baseline', 'high_demand', 'low_price'], name='scenario') + + +@pytest.fixture +def multi_index(sample_time_index, sample_scenario_index): + """Create a sample MultiIndex combining scenarios and times.""" + return pd.MultiIndex.from_product([sample_scenario_index, sample_time_index], names=['scenario', 'time']) + + +class TestSingleDimensionConversion: + """Tests for converting data without scenarios (1D: time only).""" + + def test_scalar_conversion(self, sample_time_index): + """Test converting a scalar value.""" + # Test with integer + result = DataConverter.as_dataarray(42, sample_time_index) + assert isinstance(result, xr.DataArray) + assert result.shape == (len(sample_time_index),) + assert result.dims == ('time',) + assert np.all(result.values == 42) + + # Test with float + result = DataConverter.as_dataarray(42.5, sample_time_index) + assert np.all(result.values == 42.5) + + # Test with numpy scalar types + result = DataConverter.as_dataarray(np.int64(42), sample_time_index) + assert np.all(result.values == 42) + result = DataConverter.as_dataarray(np.float32(42.5), sample_time_index) + assert np.all(result.values == 42.5) + + def test_series_conversion(self, sample_time_index): + """Test converting a pandas Series.""" + # Test with integer values + series = pd.Series([1, 2, 3, 4, 5], index=sample_time_index) + result = DataConverter.as_dataarray(series, sample_time_index) + assert isinstance(result, xr.DataArray) + assert result.shape == (5,) + assert result.dims == ('time',) + assert np.array_equal(result.values, series.values) + + # Test with float values + series = pd.Series([1.1, 2.2, 3.3, 4.4, 5.5], index=sample_time_index) + result = DataConverter.as_dataarray(series, sample_time_index) + assert np.array_equal(result.values, series.values) + + # Test with mixed NA values + series = pd.Series([1, np.nan, 3, None, 5], index=sample_time_index) + result = DataConverter.as_dataarray(series, sample_time_index) + assert np.array_equal(np.isnan(result.values), np.isnan(series.values)) + assert np.array_equal(result.values[~np.isnan(result.values)], series.values[~np.isnan(series.values)]) + + def test_dataframe_conversion(self, sample_time_index): + """Test converting a pandas DataFrame.""" + # Test with a single-column DataFrame + df = pd.DataFrame({'A': [1, 2, 3, 4, 5]}, index=sample_time_index) + result = DataConverter.as_dataarray(df, sample_time_index) + assert isinstance(result, xr.DataArray) + assert result.shape == (5,) + assert result.dims == ('time',) + assert np.array_equal(result.values.flatten(), df['A'].values) + + # Test with float values + df = pd.DataFrame({'A': [1.1, 2.2, 3.3, 4.4, 5.5]}, index=sample_time_index) + result = DataConverter.as_dataarray(df, sample_time_index) + assert np.array_equal(result.values.flatten(), df['A'].values) + + # Test with NA values + df = pd.DataFrame({'A': [1, np.nan, 3, None, 5]}, index=sample_time_index) + result = DataConverter.as_dataarray(df, sample_time_index) + assert np.array_equal(np.isnan(result.values), np.isnan(df['A'].values)) + assert np.array_equal(result.values[~np.isnan(result.values)], df['A'].values[~np.isnan(df['A'].values)]) + + def test_ndarray_conversion(self, sample_time_index): + """Test converting a numpy ndarray.""" + # Test with integer 1D array + arr_1d = np.array([1, 2, 3, 4, 5]) + result = DataConverter.as_dataarray(arr_1d, sample_time_index) + assert result.shape == (5,) + assert result.dims == ('time',) + assert np.array_equal(result.values, arr_1d) + + # Test with float 1D array + arr_1d = np.array([1.1, 2.2, 3.3, 4.4, 5.5]) + result = DataConverter.as_dataarray(arr_1d, sample_time_index) + assert np.array_equal(result.values, arr_1d) + + # Test with array containing NaN + arr_1d = np.array([1, np.nan, 3, np.nan, 5]) + result = DataConverter.as_dataarray(arr_1d, sample_time_index) + assert np.array_equal(np.isnan(result.values), np.isnan(arr_1d)) + assert np.array_equal(result.values[~np.isnan(result.values)], arr_1d[~np.isnan(arr_1d)]) + + def test_dataarray_conversion(self, sample_time_index): + """Test converting an existing xarray DataArray.""" + # Create original DataArray + original = xr.DataArray(data=np.array([1, 2, 3, 4, 5]), coords={'time': sample_time_index}, dims=['time']) + + # Convert and check + result = DataConverter.as_dataarray(original, sample_time_index) + assert result.shape == (5,) + assert result.dims == ('time',) + assert np.array_equal(result.values, original.values) + + # Ensure it's a copy + result[0] = 999 + assert original[0].item() == 1 # Original should be unchanged + + # Test with different time coordinates but same length + different_times = pd.date_range('2025-01-01', periods=5, freq='D', name='time') + original = xr.DataArray(data=np.array([1, 2, 3, 4, 5]), coords={'time': different_times}, dims=['time']) + + # Should raise an error for mismatched time coordinates + with pytest.raises(ConversionError): + DataConverter.as_dataarray(original, sample_time_index) + + +class TestMultiDimensionConversion: + """Tests for converting data with scenarios (2D: scenario × time).""" + + def test_scalar_with_scenarios(self, sample_time_index, sample_scenario_index): + """Test converting scalar values with scenario dimension.""" + # Test with integer + result = DataConverter.as_dataarray(42, sample_time_index, sample_scenario_index) + + assert isinstance(result, xr.DataArray) + assert result.shape == (len(sample_scenario_index), len(sample_time_index)) + assert result.dims == ('scenario', 'time') + assert np.all(result.values == 42) + assert set(result.coords['scenario'].values) == set(sample_scenario_index.values) + assert set(result.coords['time'].values) == set(sample_time_index.values) + + # Test with float + result = DataConverter.as_dataarray(42.5, sample_time_index, sample_scenario_index) + assert np.all(result.values == 42.5) + + def test_series_with_scenarios(self, sample_time_index, sample_scenario_index): + """Test converting Series with scenario dimension.""" + # Create time series data + series = pd.Series([1, 2, 3, 4, 5], index=sample_time_index) + + # Convert with scenario dimension + result = DataConverter.as_dataarray(series, sample_time_index, sample_scenario_index) + + assert result.shape == (len(sample_scenario_index), len(sample_time_index)) + assert result.dims == ('scenario', 'time') + + # Values should be broadcast to all scenarios + for scenario in sample_scenario_index: + scenario_slice = result.sel(scenario=scenario) + assert np.array_equal(scenario_slice.values, series.values) + + # Test with series containing NaN + series = pd.Series([1, np.nan, 3, np.nan, 5], index=sample_time_index) + result = DataConverter.as_dataarray(series, sample_time_index, sample_scenario_index) + + # Each scenario should have the same pattern of NaNs + for scenario in sample_scenario_index: + scenario_slice = result.sel(scenario=scenario) + assert np.array_equal(np.isnan(scenario_slice.values), np.isnan(series.values)) + assert np.array_equal( + scenario_slice.values[~np.isnan(scenario_slice.values)], series.values[~np.isnan(series.values)] + ) + + def test_multi_index_series(self, sample_time_index, sample_scenario_index, multi_index): + """Test converting a Series with MultiIndex (scenario, time).""" + # Create a MultiIndex Series with scenario-specific values + values = [ + # baseline scenario + 10, + 20, + 30, + 40, + 50, + # high_demand scenario + 15, + 25, + 35, + 45, + 55, + # low_price scenario + 5, + 15, + 25, + 35, + 45, + ] + series_multi = pd.Series(values, index=multi_index) + + # Convert the MultiIndex Series + result = DataConverter.as_dataarray(series_multi, sample_time_index, sample_scenario_index) + + assert result.shape == (len(sample_scenario_index), len(sample_time_index)) + assert result.dims == ('scenario', 'time') + + # Check values for each scenario + baseline_values = result.sel(scenario='baseline').values + assert np.array_equal(baseline_values, [10, 20, 30, 40, 50]) + + high_demand_values = result.sel(scenario='high_demand').values + assert np.array_equal(high_demand_values, [15, 25, 35, 45, 55]) + + low_price_values = result.sel(scenario='low_price').values + assert np.array_equal(low_price_values, [5, 15, 25, 35, 45]) + + # Test with some missing values in the MultiIndex + incomplete_index = multi_index[:-2] # Remove last two entries + incomplete_values = values[:-2] # Remove corresponding values + incomplete_series = pd.Series(incomplete_values, index=incomplete_index) + + result = DataConverter.as_dataarray(incomplete_series, sample_time_index, sample_scenario_index) + + # The last value of low_price scenario should be NaN + assert np.isnan(result.sel(scenario='low_price').values[-1]) + + def test_dataframe_with_scenarios(self, sample_time_index, sample_scenario_index): + """Test converting DataFrame with scenario dimension.""" + # Create a single-column DataFrame + df = pd.DataFrame({'A': [1, 2, 3, 4, 5]}, index=sample_time_index) + + # Convert with scenario dimension + result = DataConverter.as_dataarray(df, sample_time_index, sample_scenario_index) + + assert result.shape == (len(sample_scenario_index), len(sample_time_index)) + assert result.dims == ('scenario', 'time') + + # Values should be broadcast to all scenarios + for scenario in sample_scenario_index: + scenario_slice = result.sel(scenario=scenario) + assert np.array_equal(scenario_slice.values, df['A'].values) + + def test_multi_index_dataframe(self, sample_time_index, sample_scenario_index, multi_index): + """Test converting a DataFrame with MultiIndex (scenario, time).""" + # Create a MultiIndex DataFrame with scenario-specific values + values = [ + # baseline scenario + 10, + 20, + 30, + 40, + 50, + # high_demand scenario + 15, + 25, + 35, + 45, + 55, + # low_price scenario + 5, + 15, + 25, + 35, + 45, + ] + df_multi = pd.DataFrame({'A': values}, index=multi_index) + + # Convert the MultiIndex DataFrame + result = DataConverter.as_dataarray(df_multi, sample_time_index, sample_scenario_index) + + assert result.shape == (len(sample_scenario_index), len(sample_time_index)) + assert result.dims == ('scenario', 'time') + + # Check values for each scenario + baseline_values = result.sel(scenario='baseline').values + assert np.array_equal(baseline_values, [10, 20, 30, 40, 50]) + + high_demand_values = result.sel(scenario='high_demand').values + assert np.array_equal(high_demand_values, [15, 25, 35, 45, 55]) + + low_price_values = result.sel(scenario='low_price').values + assert np.array_equal(low_price_values, [5, 15, 25, 35, 45]) + + # Test with missing values + incomplete_index = multi_index[:-2] # Remove last two entries + incomplete_values = values[:-2] # Remove corresponding values + incomplete_df = pd.DataFrame({'A': incomplete_values}, index=incomplete_index) + + result = DataConverter.as_dataarray(incomplete_df, sample_time_index, sample_scenario_index) + + # The last value of low_price scenario should be NaN + assert np.isnan(result.sel(scenario='low_price').values[-1]) + + # Test with multiple columns (should raise error) + df_multi_col = pd.DataFrame({'A': values, 'B': [v * 2 for v in values]}, index=multi_index) + + with pytest.raises(ConversionError): + DataConverter.as_dataarray(df_multi_col, sample_time_index, sample_scenario_index) + + def test_1d_array_with_scenarios(self, sample_time_index, sample_scenario_index): + """Test converting 1D array with scenario dimension (broadcasting).""" + # Create 1D array matching timesteps length + arr_1d = np.array([1, 2, 3, 4, 5]) + + # Convert with scenarios + result = DataConverter.as_dataarray(arr_1d, sample_time_index, sample_scenario_index) + + assert result.shape == (len(sample_scenario_index), len(sample_time_index)) + assert result.dims == ('scenario', 'time') + + # Each scenario should have the same values (broadcasting) + for scenario in sample_scenario_index: + scenario_slice = result.sel(scenario=scenario) + assert np.array_equal(scenario_slice.values, arr_1d) + + def test_2d_array_with_scenarios(self, sample_time_index, sample_scenario_index): + """Test converting 2D array with scenario dimension.""" + # Create 2D array with different values per scenario + arr_2d = np.array( + [ + [1, 2, 3, 4, 5], # baseline scenario + [6, 7, 8, 9, 10], # high_demand scenario + [11, 12, 13, 14, 15], # low_price scenario + ] + ) + + # Convert to DataArray + result = DataConverter.as_dataarray(arr_2d, sample_time_index, sample_scenario_index) + + assert result.shape == (3, 5) + assert result.dims == ('scenario', 'time') + + # Check that each scenario has correct values + assert np.array_equal(result.sel(scenario='baseline').values, arr_2d[0]) + assert np.array_equal(result.sel(scenario='high_demand').values, arr_2d[1]) + assert np.array_equal(result.sel(scenario='low_price').values, arr_2d[2]) + + def test_dataarray_with_scenarios(self, sample_time_index, sample_scenario_index): + """Test converting an existing DataArray with scenarios.""" + # Create a multi-scenario DataArray + original = xr.DataArray( + data=np.array([[1, 2, 3, 4, 5], [6, 7, 8, 9, 10], [11, 12, 13, 14, 15]]), + coords={'scenario': sample_scenario_index, 'time': sample_time_index}, + dims=['scenario', 'time'], + ) + + # Test conversion + result = DataConverter.as_dataarray(original, sample_time_index, sample_scenario_index) + + assert result.shape == (3, 5) + assert result.dims == ('scenario', 'time') + assert np.array_equal(result.values, original.values) + + # Ensure it's a copy + result.loc['baseline'] = 999 + assert original.sel(scenario='baseline')[0].item() == 1 # Original should be unchanged + + def test_time_only_dataarray_with_scenarios(self, sample_time_index, sample_scenario_index): + """Test broadcasting a time-only DataArray to scenarios.""" + # Create a DataArray with only time dimension + time_only = xr.DataArray(data=np.array([1, 2, 3, 4, 5]), coords={'time': sample_time_index}, dims=['time']) + + # Convert with scenarios - should broadcast to all scenarios + result = DataConverter.as_dataarray(time_only, sample_time_index, sample_scenario_index) + + assert result.shape == (3, 5) + assert result.dims == ('scenario', 'time') + + # Each scenario should have same values + for scenario in sample_scenario_index: + assert np.array_equal(result.sel(scenario=scenario).values, time_only.values) + + +class TestInvalidInputs: + """Tests for invalid inputs and error handling.""" + + def test_time_index_validation(self): + """Test validation of time index.""" + # Test with unnamed index + unnamed_index = pd.date_range('2024-01-01', periods=5, freq='D') + with pytest.raises(ConversionError): + DataConverter.as_dataarray(42, unnamed_index) + + # Test with empty index + empty_index = pd.DatetimeIndex([], name='time') + with pytest.raises(ValueError): + DataConverter.as_dataarray(42, empty_index) + + # Test with non-DatetimeIndex + wrong_type_index = pd.Index([1, 2, 3, 4, 5], name='time') + with pytest.raises(ValueError): + DataConverter.as_dataarray(42, wrong_type_index) + + def test_scenario_index_validation(self, sample_time_index): + """Test validation of scenario index.""" + # Test with unnamed scenario index + unnamed_index = pd.Index(['baseline', 'high_demand']) + with pytest.raises(ConversionError): + DataConverter.as_dataarray(42, sample_time_index, unnamed_index) + # Test with empty scenario index + empty_index = pd.Index([], name='scenario') + with pytest.raises(ValueError): + DataConverter.as_dataarray(42, sample_time_index, empty_index) -def test_series_conversion(sample_time_index): - series = pd.Series([1, 2, 3, 4, 5], index=sample_time_index) + # Test with non-Index scenario + with pytest.raises(ValueError): + DataConverter.as_dataarray(42, sample_time_index, ['baseline', 'high_demand']) - # Test Series conversion - result = DataConverter.as_dataarray(series, sample_time_index) - assert isinstance(result, xr.DataArray) - assert result.shape == (5,) - assert result.dims == ('time',) - assert np.array_equal(result.values, series.values) + def test_invalid_data_types(self, sample_time_index, sample_scenario_index): + """Test handling of invalid data types.""" + # Test invalid input type (string) + with pytest.raises(ConversionError): + DataConverter.as_dataarray('invalid_string', sample_time_index) + # Test invalid input type with scenarios + with pytest.raises(ConversionError): + DataConverter.as_dataarray('invalid_string', sample_time_index, sample_scenario_index) -def test_dataframe_conversion(sample_time_index): - # Create a single-column DataFrame - df = pd.DataFrame({'A': [1, 2, 3, 4, 5]}, index=sample_time_index) + # Test unsupported complex object + with pytest.raises(ConversionError): + DataConverter.as_dataarray(object(), sample_time_index) - # Test DataFrame conversion - result = DataConverter.as_dataarray(df, sample_time_index) - assert isinstance(result, xr.DataArray) - assert result.shape == (5,) - assert result.dims == ('time',) - assert np.array_equal(result.values.flatten(), df['A'].values) + # Test None value + with pytest.raises(ConversionError): + DataConverter.as_dataarray(None, sample_time_index) + def test_mismatched_input_dimensions(self, sample_time_index, sample_scenario_index): + """Test handling of mismatched input dimensions.""" + # Test mismatched Series index + mismatched_series = pd.Series( + [1, 2, 3, 4, 5, 6], index=pd.date_range('2025-01-01', periods=6, freq='D', name='time') + ) + with pytest.raises(ConversionError): + DataConverter.as_dataarray(mismatched_series, sample_time_index) -def test_ndarray_conversion(sample_time_index): - # Test 1D array conversion - arr_1d = np.array([1, 2, 3, 4, 5]) - result = DataConverter.as_dataarray(arr_1d, sample_time_index) - assert result.shape == (5,) - assert result.dims == ('time',) - assert np.array_equal(result.values, arr_1d) + # Test DataFrame with multiple columns + df_multi_col = pd.DataFrame({'A': [1, 2, 3, 4, 5], 'B': [6, 7, 8, 9, 10]}, index=sample_time_index) + with pytest.raises(ConversionError): + DataConverter.as_dataarray(df_multi_col, sample_time_index) + # Test mismatched array shape for time-only + with pytest.raises(ConversionError): + DataConverter.as_dataarray(np.array([1, 2, 3]), sample_time_index) # Wrong length -def test_dataarray_conversion(sample_time_index): - # Create a DataArray - original = xr.DataArray(data=np.array([1, 2, 3, 4, 5]), coords={'time': sample_time_index}, dims=['time']) + # Test mismatched array shape for scenario × time + # Array shape should be (n_scenarios, n_timesteps) + wrong_shape_array = np.array( + [ + [1, 2, 3, 4], # Missing a timestep + [5, 6, 7, 8], + [9, 10, 11, 12], + ] + ) + with pytest.raises(ConversionError): + DataConverter.as_dataarray(wrong_shape_array, sample_time_index, sample_scenario_index) - # Test DataArray conversion - result = DataConverter.as_dataarray(original, sample_time_index) - assert result.shape == (5,) - assert result.dims == ('time',) - assert np.array_equal(result.values, original.values) + # Test array with too many dimensions + with pytest.raises(ConversionError): + # 3D array not allowed + DataConverter.as_dataarray(np.ones((3, 5, 2)), sample_time_index, sample_scenario_index) - # Ensure it's a copy - result[0] = 999 - assert original[0].item() == 1 # Original should be unchanged + def test_dataarray_dimension_mismatch(self, sample_time_index, sample_scenario_index): + """Test handling of mismatched DataArray dimensions.""" + # Create DataArray with wrong dimensions + wrong_dims = xr.DataArray(data=np.array([1, 2, 3, 4, 5]), coords={'wrong_dim': range(5)}, dims=['wrong_dim']) + with pytest.raises(ConversionError): + DataConverter.as_dataarray(wrong_dims, sample_time_index) + + # Create DataArray with scenario but no time + wrong_dims_2 = xr.DataArray(data=np.array([1, 2, 3]), coords={'scenario': ['a', 'b', 'c']}, dims=['scenario']) + with pytest.raises(ConversionError): + DataConverter.as_dataarray(wrong_dims_2, sample_time_index, sample_scenario_index) + + # Create DataArray with right dims but wrong length + wrong_length = xr.DataArray( + data=np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]), + coords={ + 'scenario': sample_scenario_index, + 'time': pd.date_range('2024-01-01', periods=3, freq='D', name='time'), + }, + dims=['scenario', 'time'], + ) + with pytest.raises(ConversionError): + DataConverter.as_dataarray(wrong_length, sample_time_index, sample_scenario_index) + + +class TestEdgeCases: + """Tests for edge cases and special scenarios.""" + + def test_single_timestep(self, sample_scenario_index): + """Test with a single timestep.""" + # Test with only one timestep + single_timestep = pd.DatetimeIndex(['2024-01-01'], name='time') + + # Scalar conversion + result = DataConverter.as_dataarray(42, single_timestep) + assert result.shape == (1,) + assert result.dims == ('time',) + + # With scenarios + result_with_scenarios = DataConverter.as_dataarray(42, single_timestep, sample_scenario_index) + assert result_with_scenarios.shape == (len(sample_scenario_index), 1) + assert result_with_scenarios.dims == ('scenario', 'time') + + def test_single_scenario(self, sample_time_index): + """Test with a single scenario.""" + # Test with only one scenario + single_scenario = pd.Index(['baseline'], name='scenario') + + # Scalar conversion with single scenario + result = DataConverter.as_dataarray(42, sample_time_index, single_scenario) + assert result.shape == (1, len(sample_time_index)) + assert result.dims == ('scenario', 'time') + + # Array conversion with single scenario + arr = np.array([1, 2, 3, 4, 5]) + result_arr = DataConverter.as_dataarray(arr, sample_time_index, single_scenario) + assert result_arr.shape == (1, 5) + assert np.array_equal(result_arr.sel(scenario='baseline').values, arr) + + # 2D array with single scenario + arr_2d = np.array([[1, 2, 3, 4, 5]]) # Note the extra dimension + result_arr_2d = DataConverter.as_dataarray(arr_2d, sample_time_index, single_scenario) + assert result_arr_2d.shape == (1, 5) + assert np.array_equal(result_arr_2d.sel(scenario='baseline').values, arr_2d[0]) + + def test_different_scenario_order(self, sample_time_index): + """Test that scenario order is preserved.""" + # Test with different scenario orders + scenarios1 = pd.Index(['a', 'b', 'c'], name='scenario') + scenarios2 = pd.Index(['c', 'b', 'a'], name='scenario') + + # Create DataArray with first order + data = np.array( + [ + [1, 2, 3, 4, 5], # a + [6, 7, 8, 9, 10], # b + [11, 12, 13, 14, 15], # c + ] + ) + + result1 = DataConverter.as_dataarray(data, sample_time_index, scenarios1) + assert np.array_equal(result1.sel(scenario='a').values, [1, 2, 3, 4, 5]) + assert np.array_equal(result1.sel(scenario='c').values, [11, 12, 13, 14, 15]) + + # Create DataArray with second order + result2 = DataConverter.as_dataarray(data, sample_time_index, scenarios2) + # First row should match 'c' now + assert np.array_equal(result2.sel(scenario='c').values, [1, 2, 3, 4, 5]) + # Last row should match 'a' now + assert np.array_equal(result2.sel(scenario='a').values, [11, 12, 13, 14, 15]) + + def test_all_nan_data(self, sample_time_index, sample_scenario_index): + """Test handling of all-NaN data.""" + # Create array of all NaNs + all_nan_array = np.full(5, np.nan) + result = DataConverter.as_dataarray(all_nan_array, sample_time_index) + assert np.all(np.isnan(result.values)) + + # With scenarios + result = DataConverter.as_dataarray(all_nan_array, sample_time_index, sample_scenario_index) + assert result.shape == (len(sample_scenario_index), len(sample_time_index)) + assert np.all(np.isnan(result.values)) + + # Series of all NaNs + all_nan_series = pd.Series([np.nan, np.nan, np.nan, np.nan, np.nan], index=sample_time_index) + result = DataConverter.as_dataarray(all_nan_series, sample_time_index, sample_scenario_index) + assert np.all(np.isnan(result.values)) + + def test_subset_index_multiindex(self, sample_time_index, sample_scenario_index): + """Test handling of MultiIndex Series/DataFrames with subset of expected indices.""" + # Create a subset of the expected indexes + subset_time = sample_time_index[1:4] # Middle subset + subset_scenarios = sample_scenario_index[0:2] # First two scenarios + + # Create MultiIndex with subset + subset_multi_index = pd.MultiIndex.from_product([subset_scenarios, subset_time], names=['scenario', 'time']) + + # Create Series with subset of data + values = [ + # baseline (3 values) + 20, + 30, + 40, + # high_demand (3 values) + 25, + 35, + 45, + ] + subset_series = pd.Series(values, index=subset_multi_index) + + # Convert and test + result = DataConverter.as_dataarray(subset_series, sample_time_index, sample_scenario_index) + + # Shape should be full size + assert result.shape == (len(sample_scenario_index), len(sample_time_index)) + + # Check values - present values should match + assert result.sel(scenario='baseline', time=subset_time[0]).item() == 20 + assert result.sel(scenario='high_demand', time=subset_time[1]).item() == 35 + + # Missing values should be NaN + assert np.isnan(result.sel(scenario='baseline', time=sample_time_index[0]).item()) + assert np.isnan(result.sel(scenario='low_price', time=sample_time_index[2]).item()) + + def test_mixed_data_types(self, sample_time_index, sample_scenario_index): + """Test conversion of mixed integer and float data.""" + # Create array with mixed types + mixed_array = np.array([1, 2.5, 3, 4.5, 5]) + result = DataConverter.as_dataarray(mixed_array, sample_time_index) + + # Result should be float dtype + assert np.issubdtype(result.dtype, np.floating) + assert np.array_equal(result.values, mixed_array) + + # With scenarios + result = DataConverter.as_dataarray(mixed_array, sample_time_index, sample_scenario_index) + assert np.issubdtype(result.dtype, np.floating) + for scenario in sample_scenario_index: + assert np.array_equal(result.sel(scenario=scenario).values, mixed_array) + + +class TestFunctionalUseCase: + """Tests for realistic use cases combining multiple features.""" + + def test_multiindex_with_nans_and_partial_data(self, sample_time_index, sample_scenario_index): + """Test MultiIndex Series with partial data and NaN values.""" + # Create a MultiIndex Series with missing values and partial coverage + time_subset = sample_time_index[1:4] # Middle 3 timestamps only + + # Build index with holes + idx_tuples = [] + for scenario in sample_scenario_index: + for time in time_subset: + # Skip some combinations to create holes + if scenario == 'baseline' and time == time_subset[0]: + continue + if scenario == 'high_demand' and time == time_subset[2]: + continue + idx_tuples.append((scenario, time)) + + partial_idx = pd.MultiIndex.from_tuples(idx_tuples, names=['scenario', 'time']) + + # Create values with some NaNs + values = [ + # baseline (2 values, skipping first) + 30, + 40, + # high_demand (2 values, skipping last) + 25, + 35, + # low_price (3 values) + 15, + np.nan, + 35, + ] + + # Create Series + partial_series = pd.Series(values, index=partial_idx) + + # Convert and test + result = DataConverter.as_dataarray(partial_series, sample_time_index, sample_scenario_index) + + # Shape should be full size + assert result.shape == (len(sample_scenario_index), len(sample_time_index)) + + # Check specific values + assert result.sel(scenario='baseline', time=time_subset[1]).item() == 30 + assert result.sel(scenario='high_demand', time=time_subset[0]).item() == 25 + assert np.isnan(result.sel(scenario='low_price', time=time_subset[1]).item()) + + # All skipped combinations should be NaN + assert np.isnan(result.sel(scenario='baseline', time=time_subset[0]).item()) + assert np.isnan(result.sel(scenario='high_demand', time=time_subset[2]).item()) + + # First and last timestamps should all be NaN (not in original subset) + assert np.all(np.isnan(result.sel(time=sample_time_index[0]).values)) + assert np.all(np.isnan(result.sel(time=sample_time_index[-1]).values)) + + def test_scenario_broadcast_with_nan_values(self, sample_time_index, sample_scenario_index): + """Test broadcasting a Series with NaN values to scenarios.""" + # Create Series with some NaN values + series = pd.Series([1, np.nan, 3, np.nan, 5], index=sample_time_index) + + # Convert with scenario broadcasting + result = DataConverter.as_dataarray(series, sample_time_index, sample_scenario_index) + + # All scenarios should have the same pattern of NaN values + for scenario in sample_scenario_index: + scenario_data = result.sel(scenario=scenario) + assert np.isnan(scenario_data[1].item()) + assert np.isnan(scenario_data[3].item()) + assert scenario_data[0].item() == 1 + assert scenario_data[2].item() == 3 + assert scenario_data[4].item() == 5 + + def test_large_dataset(self, sample_scenario_index): + """Test with a larger dataset to ensure performance.""" + # Create a larger timestep array (e.g., hourly for a year) + large_timesteps = pd.date_range( + '2024-01-01', + periods=8760, # Hours in a year + freq='H', + name='time', + ) + + # Create large 2D array (3 scenarios × 8760 hours) + large_data = np.random.rand(len(sample_scenario_index), len(large_timesteps)) + + # Convert and check + result = DataConverter.as_dataarray(large_data, large_timesteps, sample_scenario_index) + + assert result.shape == (len(sample_scenario_index), len(large_timesteps)) + assert result.dims == ('scenario', 'time') + assert np.array_equal(result.values, large_data) + + +class TestMultiScenarioArrayConversion: + """Tests specifically focused on array conversion with scenarios.""" + + def test_1d_array_broadcasting(self, sample_time_index, sample_scenario_index): + """Test that 1D arrays are properly broadcast to all scenarios.""" + arr_1d = np.array([1, 2, 3, 4, 5]) + result = DataConverter.as_dataarray(arr_1d, sample_time_index, sample_scenario_index) + + assert result.shape == (len(sample_scenario_index), len(sample_time_index)) + + # Each scenario should have identical values + for i, scenario in enumerate(sample_scenario_index): + assert np.array_equal(result.sel(scenario=scenario).values, arr_1d) + + # Modify one scenario's values + result.loc[dict(scenario=scenario)] = np.ones(len(sample_time_index)) * i + + # Ensure modifications are isolated to each scenario + for i, scenario in enumerate(sample_scenario_index): + assert np.all(result.sel(scenario=scenario).values == i) + + def test_2d_array_different_shapes(self, sample_time_index): + """Test different scenario shapes with 2D arrays.""" + # Test with 1 scenario + single_scenario = pd.Index(['baseline'], name='scenario') + arr_1_scenario = np.array([[1, 2, 3, 4, 5]]) + + result = DataConverter.as_dataarray(arr_1_scenario, sample_time_index, single_scenario) + assert result.shape == (1, len(sample_time_index)) + + # Test with 2 scenarios + two_scenarios = pd.Index(['baseline', 'high_demand'], name='scenario') + arr_2_scenarios = np.array([[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]]) + + result = DataConverter.as_dataarray(arr_2_scenarios, sample_time_index, two_scenarios) + assert result.shape == (2, len(sample_time_index)) + assert np.array_equal(result.sel(scenario='baseline').values, arr_2_scenarios[0]) + assert np.array_equal(result.sel(scenario='high_demand').values, arr_2_scenarios[1]) + + # Test mismatched scenarios count + three_scenarios = pd.Index(['a', 'b', 'c'], name='scenario') + with pytest.raises(ConversionError): + DataConverter.as_dataarray(arr_2_scenarios, sample_time_index, three_scenarios) + + def test_array_handling_edge_cases(self, sample_time_index, sample_scenario_index): + """Test array edge cases.""" + # Test with boolean array + bool_array = np.array([True, False, True, False, True]) + result = DataConverter.as_dataarray(bool_array, sample_time_index, sample_scenario_index) + assert result.dtype == bool + assert result.shape == (len(sample_scenario_index), len(sample_time_index)) + + # Test with array containing infinite values + inf_array = np.array([1, np.inf, 3, -np.inf, 5]) + result = DataConverter.as_dataarray(inf_array, sample_time_index, sample_scenario_index) + for scenario in sample_scenario_index: + scenario_data = result.sel(scenario=scenario) + assert np.isinf(scenario_data[1].item()) + assert np.isinf(scenario_data[3].item()) + assert scenario_data[3].item() < 0 # Negative infinity + + +class TestScenarioReindexing: + """Tests for reindexing and coordinate preservation in DataConverter.""" + + def test_preserving_scenario_order(self, sample_time_index): + """Test that scenario order is preserved in converted DataArrays.""" + # Define scenarios in a specific order + scenarios = pd.Index(['scenario3', 'scenario1', 'scenario2'], name='scenario') + + # Create 2D array + data = np.array( + [ + [1, 2, 3, 4, 5], # scenario3 + [6, 7, 8, 9, 10], # scenario1 + [11, 12, 13, 14, 15], # scenario2 + ] + ) + + # Convert to DataArray + result = DataConverter.as_dataarray(data, sample_time_index, scenarios) + + # Verify order of scenarios is preserved + assert list(result.coords['scenario'].values) == list(scenarios) + + # Verify data for each scenario + assert np.array_equal(result.sel(scenario='scenario3').values, data[0]) + assert np.array_equal(result.sel(scenario='scenario1').values, data[1]) + assert np.array_equal(result.sel(scenario='scenario2').values, data[2]) + + def test_multiindex_reindexing(self, sample_time_index): + """Test reindexing of MultiIndex Series.""" + # Create scenarios with intentional different order + scenarios = pd.Index(['z_scenario', 'a_scenario', 'm_scenario'], name='scenario') + + # Create MultiIndex with different order than the target + source_scenarios = pd.Index(['a_scenario', 'm_scenario', 'z_scenario'], name='scenario') + multi_idx = pd.MultiIndex.from_product([source_scenarios, sample_time_index], names=['scenario', 'time']) + + # Create values - order should match the source index + values = [] + for i, _ in enumerate(source_scenarios): + values.extend([i * 10 + j for j in range(1, len(sample_time_index) + 1)]) + + # Create Series + series = pd.Series(values, index=multi_idx) + + # Convert using the target scenario order + result = DataConverter.as_dataarray(series, sample_time_index, scenarios) + + # Verify scenario order matches the target + assert list(result.coords['scenario'].values) == list(scenarios) + + # Verify values are correctly indexed + assert np.array_equal(result.sel(scenario='a_scenario').values, [1, 2, 3, 4, 5]) + assert np.array_equal(result.sel(scenario='m_scenario').values, [11, 12, 13, 14, 15]) + assert np.array_equal(result.sel(scenario='z_scenario').values, [21, 22, 23, 24, 25]) + + +if __name__ == '__main__': + pytest.main() def test_invalid_inputs(sample_time_index): diff --git a/tests/test_timeseries.py b/tests/test_timeseries.py index 48c7ab7b2..50136536b 100644 --- a/tests/test_timeseries.py +++ b/tests/test_timeseries.py @@ -8,7 +8,7 @@ import pytest import xarray as xr -from flixopt.core import ConversionError, DataConverter, TimeSeries, TimeSeriesCollection, TimeSeriesData +from flixopt.core import ConversionError, DataConverter, TimeSeries, TimeSeriesCollection @pytest.fixture @@ -44,13 +44,13 @@ def test_initialization(self, simple_dataarray): # Check data initialization assert isinstance(ts.stored_data, xr.DataArray) assert ts.stored_data.equals(simple_dataarray) - assert ts.active_data.equals(simple_dataarray) + assert ts.selected_data.equals(simple_dataarray) # Check backup was created assert ts._backup.equals(simple_dataarray) # Check active timesteps - assert ts.active_timesteps.equals(simple_dataarray.indexes['time']) + assert ts._valid_selector == {} # No selections initially def test_initialization_with_aggregation_params(self, simple_dataarray): """Test initialization with aggregation parameters.""" @@ -73,53 +73,51 @@ def test_initialization_validation(self, sample_timesteps): multi_dim_data = xr.DataArray( [[1, 2, 3], [4, 5, 6]], coords={'dim1': [0, 1], 'time': sample_timesteps[:3]}, dims=['dim1', 'time'] ) - with pytest.raises(ValueError, match='dimensions of DataArray must be 1'): + with pytest.raises(ValueError, match='DataArray dimensions must be subset of'): TimeSeries(multi_dim_data, name='Multi-dim Series') - def test_active_timesteps_getter_setter(self, sample_timeseries, sample_timesteps): - """Test active_timesteps getter and setter.""" - # Initial state should use all timesteps - assert sample_timeseries.active_timesteps.equals(sample_timesteps) + def test_selection_methods(self, sample_timeseries, sample_timesteps): + """Test selection methods.""" + # Initial state should have no selections + assert sample_timeseries._selected_timesteps is None + assert sample_timeseries._selected_scenarios is None # Set to a subset subset_index = sample_timesteps[1:3] - sample_timeseries.active_timesteps = subset_index - assert sample_timeseries.active_timesteps.equals(subset_index) + sample_timeseries.set_selection(timesteps=subset_index) + assert sample_timeseries._selected_timesteps.equals(subset_index) # Active data should reflect the subset - assert sample_timeseries.active_data.equals(sample_timeseries.stored_data.sel(time=subset_index)) + assert sample_timeseries.selected_data.equals(sample_timeseries.stored_data.sel(time=subset_index)) - # Reset to full index - sample_timeseries.active_timesteps = None - assert sample_timeseries.active_timesteps.equals(sample_timesteps) - - # Test invalid type - with pytest.raises(TypeError, match='must be a pandas DatetimeIndex'): - sample_timeseries.active_timesteps = 'invalid' + # Clear selection + sample_timeseries.clear_selection() + assert sample_timeseries._selected_timesteps is None + assert sample_timeseries.selected_data.equals(sample_timeseries.stored_data) def test_reset(self, sample_timeseries, sample_timesteps): """Test reset method.""" # Set to subset first subset_index = sample_timesteps[1:3] - sample_timeseries.active_timesteps = subset_index + sample_timeseries.set_selection(timesteps=subset_index) # Reset sample_timeseries.reset() - # Should be back to full index - assert sample_timeseries.active_timesteps.equals(sample_timesteps) - assert sample_timeseries.active_data.equals(sample_timeseries.stored_data) + # Should be back to full index (all selections cleared) + assert sample_timeseries._selected_timesteps is None + assert sample_timeseries.selected_data.equals(sample_timeseries.stored_data) def test_restore_data(self, sample_timeseries, simple_dataarray): """Test restore_data method.""" # Modify the stored data - new_data = xr.DataArray([1, 2, 3, 4, 5], coords={'time': sample_timeseries.active_timesteps}, dims=['time']) + new_data = xr.DataArray([1, 2, 3, 4, 5], coords={'time': sample_timeseries.stored_data.coords['time']}, dims=['time']) # Store original data for comparison original_data = sample_timeseries.stored_data - # Set new data - sample_timeseries.stored_data = new_data + # Update data + sample_timeseries.update_stored_data(new_data) assert sample_timeseries.stored_data.equals(new_data) # Restore from backup @@ -127,42 +125,42 @@ def test_restore_data(self, sample_timeseries, simple_dataarray): # Should be back to original data assert sample_timeseries.stored_data.equals(original_data) - assert sample_timeseries.active_data.equals(original_data) + assert sample_timeseries.selected_data.equals(original_data) - def test_stored_data_setter(self, sample_timeseries, sample_timesteps): - """Test stored_data setter with different data types.""" + def test_update_stored_data(self, sample_timeseries, sample_timesteps): + """Test update_stored_data method with different data types.""" # Test with a Series series_data = pd.Series([5, 6, 7, 8, 9], index=sample_timesteps) - sample_timeseries.stored_data = series_data + sample_timeseries.update_stored_data(series_data) assert np.array_equal(sample_timeseries.stored_data.values, series_data.values) # Test with a single-column DataFrame df_data = pd.DataFrame({'col1': [15, 16, 17, 18, 19]}, index=sample_timesteps) - sample_timeseries.stored_data = df_data + sample_timeseries.update_stored_data(df_data) assert np.array_equal(sample_timeseries.stored_data.values, df_data['col1'].values) # Test with a NumPy array array_data = np.array([25, 26, 27, 28, 29]) - sample_timeseries.stored_data = array_data + sample_timeseries.update_stored_data(array_data) assert np.array_equal(sample_timeseries.stored_data.values, array_data) # Test with a scalar - sample_timeseries.stored_data = 42 + sample_timeseries.update_stored_data(42) assert np.all(sample_timeseries.stored_data.values == 42) # Test with another DataArray another_dataarray = xr.DataArray([30, 31, 32, 33, 34], coords={'time': sample_timesteps}, dims=['time']) - sample_timeseries.stored_data = another_dataarray + sample_timeseries.update_stored_data(another_dataarray) assert sample_timeseries.stored_data.equals(another_dataarray) def test_stored_data_setter_no_change(self, sample_timeseries): - """Test stored_data setter when data doesn't change.""" + """Test update_stored_data method when data doesn't change.""" # Get current data current_data = sample_timeseries.stored_data current_backup = sample_timeseries._backup # Set the same data - sample_timeseries.stored_data = current_data + sample_timeseries.update_stored_data(current_data) # Backup shouldn't change assert sample_timeseries._backup is current_backup # Should be the same object @@ -229,35 +227,35 @@ def test_all_equal(self, sample_timesteps): def test_arithmetic_operations(self, sample_timeseries): """Test arithmetic operations.""" # Create a second TimeSeries for testing - data2 = xr.DataArray([1, 2, 3, 4, 5], coords={'time': sample_timeseries.active_timesteps}, dims=['time']) + data2 = xr.DataArray([1, 2, 3, 4, 5], coords={'time': sample_timeseries.stored_data.coords['time']}, dims=['time']) ts2 = TimeSeries(data2, 'Second Series') # Test operations between two TimeSeries objects assert np.array_equal( - (sample_timeseries + ts2).values, sample_timeseries.active_data.values + ts2.active_data.values + (sample_timeseries + ts2).values, sample_timeseries.selected_data.values + ts2.selected_data.values ) assert np.array_equal( - (sample_timeseries - ts2).values, sample_timeseries.active_data.values - ts2.active_data.values + (sample_timeseries - ts2).values, sample_timeseries.selected_data.values - ts2.selected_data.values ) assert np.array_equal( - (sample_timeseries * ts2).values, sample_timeseries.active_data.values * ts2.active_data.values + (sample_timeseries * ts2).values, sample_timeseries.selected_data.values * ts2.selected_data.values ) assert np.array_equal( - (sample_timeseries / ts2).values, sample_timeseries.active_data.values / ts2.active_data.values + (sample_timeseries / ts2).values, sample_timeseries.selected_data.values / ts2.selected_data.values ) # Test operations with DataArrays - assert np.array_equal((sample_timeseries + data2).values, sample_timeseries.active_data.values + data2.values) - assert np.array_equal((data2 + sample_timeseries).values, data2.values + sample_timeseries.active_data.values) + assert np.array_equal((sample_timeseries + data2).values, sample_timeseries.selected_data.values + data2.values) + assert np.array_equal((data2 + sample_timeseries).values, data2.values + sample_timeseries.selected_data.values) # Test operations with scalars - assert np.array_equal((sample_timeseries + 5).values, sample_timeseries.active_data.values + 5) - assert np.array_equal((5 + sample_timeseries).values, 5 + sample_timeseries.active_data.values) + assert np.array_equal((sample_timeseries + 5).values, sample_timeseries.selected_data.values + 5) + assert np.array_equal((5 + sample_timeseries).values, 5 + sample_timeseries.selected_data.values) # Test unary operations - assert np.array_equal((-sample_timeseries).values, -sample_timeseries.active_data.values) - assert np.array_equal((+sample_timeseries).values, +sample_timeseries.active_data.values) - assert np.array_equal((abs(sample_timeseries)).values, abs(sample_timeseries.active_data.values)) + assert np.array_equal((-sample_timeseries).values, -sample_timeseries.selected_data.values) + assert np.array_equal((+sample_timeseries).values, +sample_timeseries.selected_data.values) + assert np.array_equal((abs(sample_timeseries)).values, abs(sample_timeseries.selected_data.values)) def test_comparison_operations(self, sample_timesteps): """Test comparison operations.""" @@ -279,327 +277,473 @@ def test_comparison_operations(self, sample_timesteps): def test_numpy_ufunc(self, sample_timeseries): """Test numpy ufunc compatibility.""" # Test basic numpy functions - assert np.array_equal(np.add(sample_timeseries, 5).values, np.add(sample_timeseries.active_data, 5).values) + assert np.array_equal(np.add(sample_timeseries, 5).values, np.add(sample_timeseries.selected_data, 5).values) assert np.array_equal( - np.multiply(sample_timeseries, 2).values, np.multiply(sample_timeseries.active_data, 2).values + np.multiply(sample_timeseries, 2).values, np.multiply(sample_timeseries.selected_data, 2).values ) # Test with two TimeSeries objects - data2 = xr.DataArray([1, 2, 3, 4, 5], coords={'time': sample_timeseries.active_timesteps}, dims=['time']) + data2 = xr.DataArray([1, 2, 3, 4, 5], coords={'time': sample_timeseries.stored_data.coords['time']}, dims=['time']) ts2 = TimeSeries(data2, 'Second Series') assert np.array_equal( - np.add(sample_timeseries, ts2).values, np.add(sample_timeseries.active_data, ts2.active_data).values + np.add(sample_timeseries, ts2).values, np.add(sample_timeseries.selected_data, ts2.selected_data).values ) def test_sel_and_isel_properties(self, sample_timeseries): """Test sel and isel properties.""" # Test that sel property works - selected = sample_timeseries.sel(time=sample_timeseries.active_timesteps[0]) - assert selected.item() == sample_timeseries.active_data.values[0] + selected = sample_timeseries.sel(time=sample_timeseries.stored_data.coords['time'][0]) + assert selected.item() == sample_timeseries.selected_data.values[0] # Test that isel property works indexed = sample_timeseries.isel(time=0) - assert indexed.item() == sample_timeseries.active_data.values[0] + assert indexed.item() == sample_timeseries.selected_data.values[0] + + +@pytest.fixture +def sample_scenario_index(): + """Create a sample scenario index with the required 'scenario' name.""" + return pd.Index(['baseline', 'high_demand', 'low_price'], name='scenario') + + +@pytest.fixture +def simple_scenario_dataarray(sample_timesteps, sample_scenario_index): + """Create a DataArray with both scenario and time dimensions.""" + data = np.array([ + [10, 20, 30, 40, 50], # baseline + [15, 25, 35, 45, 55], # high_demand + [5, 15, 25, 35, 45] # low_price + ]) + return xr.DataArray( + data=data, + coords={'scenario': sample_scenario_index, 'time': sample_timesteps}, + dims=['scenario', 'time'] + ) @pytest.fixture -def sample_collection(sample_timesteps): +def sample_scenario_timeseries(simple_scenario_dataarray): + """Create a sample TimeSeries object with scenario dimension.""" + return TimeSeries(simple_scenario_dataarray, name='Test Scenario Series') + + +@pytest.fixture +def sample_allocator(sample_timesteps): """Create a sample TimeSeriesCollection.""" return TimeSeriesCollection(sample_timesteps) @pytest.fixture -def populated_collection(sample_collection): - """Create a TimeSeriesCollection with test data.""" - # Add a constant time series - sample_collection.create_time_series(42, 'constant_series') - - # Add a varying time series - varying_data = np.array([10, 20, 30, 40, 50]) - sample_collection.create_time_series(varying_data, 'varying_series') - - # Add a time series with extra timestep - sample_collection.create_time_series( - np.array([1, 2, 3, 4, 5, 6]), 'extra_timestep_series', needs_extra_timestep=True - ) +def sample_scenario_allocator(sample_timesteps, sample_scenario_index): + """Create a sample TimeSeriesCollection with scenarios.""" + return TimeSeriesCollection(sample_timesteps, scenarios=sample_scenario_index) - # Add series with aggregation settings - sample_collection.create_time_series( - TimeSeriesData(np.array([5, 5, 5, 5, 5]), agg_group='group1'), 'group1_series1' - ) - sample_collection.create_time_series( - TimeSeriesData(np.array([6, 6, 6, 6, 6]), agg_group='group1'), 'group1_series2' - ) - sample_collection.create_time_series( - TimeSeriesData(np.array([10, 10, 10, 10, 10]), agg_weight=0.5), 'weighted_series' - ) - return sample_collection +class TestTimeSeriesWithScenarios: + """Test suite for TimeSeries class with scenarios.""" + + def test_initialization_with_scenarios(self, simple_scenario_dataarray): + """Test initialization of TimeSeries with scenario dimension.""" + ts = TimeSeries(simple_scenario_dataarray, name='Scenario Series') + # Check basic properties + assert ts.name == 'Scenario Series' + assert ts._has_scenarios is True + assert ts._selected_scenarios is None # No selection initially + + # Check data initialization + assert isinstance(ts.stored_data, xr.DataArray) + assert ts.stored_data.equals(simple_scenario_dataarray) + assert ts.selected_data.equals(simple_scenario_dataarray) + + # Check backup was created + assert ts._backup.equals(simple_scenario_dataarray) + + def test_reset_with_scenarios(self, sample_scenario_timeseries, simple_scenario_dataarray): + """Test reset method with scenarios.""" + # Get original full indexes + full_timesteps = simple_scenario_dataarray.coords['time'] + full_scenarios = simple_scenario_dataarray.coords['scenario'] + + # Set to subset timesteps and scenarios + subset_timesteps = full_timesteps[1:3] + subset_scenarios = full_scenarios[:2] -class TestTimeSeriesCollection: - """Test suite for TimeSeriesCollection.""" + sample_scenario_timeseries.set_selection(timesteps=subset_timesteps, scenarios=subset_scenarios) + + # Verify subsets were set + assert sample_scenario_timeseries._selected_timesteps.equals(subset_timesteps) + assert sample_scenario_timeseries._selected_scenarios.equals(subset_scenarios) + assert sample_scenario_timeseries.selected_data.shape == (len(subset_scenarios), len(subset_timesteps)) + + # Reset + sample_scenario_timeseries.reset() + + # Should be back to full indexes + assert sample_scenario_timeseries._selected_timesteps is None + assert sample_scenario_timeseries._selected_scenarios is None + assert sample_scenario_timeseries.selected_data.shape == (len(full_scenarios), len(full_timesteps)) + + def test_scenario_selection(self, sample_scenario_timeseries, sample_scenario_index): + """Test scenario selection.""" + # Initial state should use all scenarios + assert sample_scenario_timeseries._selected_scenarios is None + + # Set to a subset + subset_index = sample_scenario_index[:2] # First two scenarios + sample_scenario_timeseries.set_selection(scenarios=subset_index) + assert sample_scenario_timeseries._selected_scenarios.equals(subset_index) + + # Active data should reflect the subset + assert sample_scenario_timeseries.selected_data.equals( + sample_scenario_timeseries.stored_data.sel(scenario=subset_index) + ) + + # Clear selection + sample_scenario_timeseries.clear_selection(timesteps=False, scenarios=True) + assert sample_scenario_timeseries._selected_scenarios is None + + def test_all_equal_with_scenarios(self, sample_timesteps, sample_scenario_index): + """Test all_equal property with scenarios.""" + # All values equal across all scenarios + equal_data = np.full((3, 5), 5) # All values are 5 + equal_dataarray = xr.DataArray( + data=equal_data, + coords={'scenario': sample_scenario_index, 'time': sample_timesteps}, + dims=['scenario', 'time'] + ) + ts_equal = TimeSeries(equal_dataarray, 'Equal Scenario Series') + assert ts_equal.all_equal is True + + # Equal within each scenario but different between scenarios + per_scenario_equal = np.array([ + [5, 5, 5, 5, 5], # baseline - all 5 + [10, 10, 10, 10, 10], # high_demand - all 10 + [15, 15, 15, 15, 15] # low_price - all 15 + ]) + per_scenario_dataarray = xr.DataArray( + data=per_scenario_equal, + coords={'scenario': sample_scenario_index, 'time': sample_timesteps}, + dims=['scenario', 'time'] + ) + ts_per_scenario = TimeSeries(per_scenario_dataarray, 'Per-Scenario Equal Series') + assert ts_per_scenario.all_equal is False + + def test_arithmetic_with_scenarios(self, sample_scenario_timeseries, sample_timesteps, sample_scenario_index): + """Test arithmetic operations with scenarios.""" + # Create a second TimeSeries with scenarios + data2 = np.ones((3, 5)) # All ones + second_dataarray = xr.DataArray( + data=data2, + coords={'scenario': sample_scenario_index, 'time': sample_timesteps}, + dims=['scenario', 'time'] + ) + ts2 = TimeSeries(second_dataarray, 'Second Series') + + # Test operations between two scenario TimeSeries objects + result = sample_scenario_timeseries + ts2 + assert result.shape == (3, 5) + assert result.dims == ('scenario', 'time') + + # First scenario values should be increased by 1 + baseline_original = sample_scenario_timeseries.sel(scenario='baseline').values + baseline_result = result.sel(scenario='baseline').values + assert np.array_equal(baseline_result, baseline_original + 1) + + +class TestTimeSeriesAllocator: + """Test suite for TimeSeriesCollection class.""" def test_initialization(self, sample_timesteps): """Test basic initialization.""" - collection = TimeSeriesCollection(sample_timesteps) + allocator = TimeSeriesCollection(sample_timesteps) - assert collection.all_timesteps.equals(sample_timesteps) - assert len(collection.all_timesteps_extra) == len(sample_timesteps) + 1 - assert isinstance(collection.all_hours_per_timestep, xr.DataArray) - assert len(collection) == 0 + assert allocator.timesteps.equals(sample_timesteps) + assert len(allocator.timesteps_extra) == len(sample_timesteps) + 1 + assert isinstance(allocator.hours_per_timestep, xr.DataArray) + assert len(allocator._time_series) == 0 def test_initialization_with_custom_hours(self, sample_timesteps): """Test initialization with custom hour settings.""" # Test with last timestep duration last_timestep_hours = 12 - collection = TimeSeriesCollection(sample_timesteps, hours_of_last_timestep=last_timestep_hours) + allocator = TimeSeriesCollection(sample_timesteps, hours_of_last_timestep=last_timestep_hours) # Verify the last timestep duration - extra_step_delta = collection.all_timesteps_extra[-1] - collection.all_timesteps_extra[-2] + extra_step_delta = allocator.timesteps_extra[-1] - allocator.timesteps_extra[-2] assert extra_step_delta == pd.Timedelta(hours=last_timestep_hours) # Test with previous timestep duration hours_per_step = 8 - collection2 = TimeSeriesCollection(sample_timesteps, hours_of_previous_timesteps=hours_per_step) + allocator2 = TimeSeriesCollection(sample_timesteps, hours_of_previous_timesteps=hours_per_step) - assert collection2.hours_of_previous_timesteps == hours_per_step + assert allocator2.hours_of_previous_timesteps == hours_per_step - def test_create_time_series(self, sample_collection): - """Test creating time series.""" + def test_add_time_series(self, sample_allocator, sample_timesteps): + """Test adding time series.""" # Test scalar - ts1 = sample_collection.create_time_series(42, 'scalar_series') + ts1 = sample_allocator.add_time_series('scalar_series', 42) assert ts1.name == 'scalar_series' - assert np.all(ts1.active_data.values == 42) + assert np.all(ts1.selected_data.values == 42) # Test numpy array data = np.array([1, 2, 3, 4, 5]) - ts2 = sample_collection.create_time_series(data, 'array_series') - assert np.array_equal(ts2.active_data.values, data) + ts2 = sample_allocator.add_time_series('array_series', data) + assert np.array_equal(ts2.selected_data.values, data) - # Test with TimeSeriesData - ts3 = sample_collection.create_time_series(TimeSeriesData(10, agg_weight=0.7), 'weighted_series') - assert ts3.aggregation_weight == 0.7 + # Test with existing TimeSeries + existing_ts = TimeSeries.from_datasource(10, 'original_name', sample_timesteps, aggregation_weight=0.7) + ts3 = sample_allocator.add_time_series('weighted_series', existing_ts) + assert ts3.name == 'weighted_series' # Name changed + assert ts3.aggregation_weight == 0.7 # Weight preserved # Test with extra timestep - ts4 = sample_collection.create_time_series(5, 'extra_series', needs_extra_timestep=True) - assert ts4.needs_extra_timestep - assert len(ts4.active_data) == len(sample_collection.timesteps_extra) + ts4 = sample_allocator.add_time_series('extra_series', 5, has_extra_timestep=True) + assert ts4.name == 'extra_series' + assert ts4.has_extra_timestep + assert len(ts4.selected_data) == len(sample_allocator.timesteps_extra) # Test duplicate name - with pytest.raises(ValueError, match='already exists'): - sample_collection.create_time_series(1, 'scalar_series') + with pytest.raises(KeyError, match='already exists'): + sample_allocator.add_time_series('scalar_series', 1) - def test_access_time_series(self, populated_collection): + def test_access_time_series(self, sample_allocator): """Test accessing time series.""" + # Add a few time series + sample_allocator.add_time_series('series1', 42) + sample_allocator.add_time_series('series2', np.array([1, 2, 3, 4, 5])) + # Test __getitem__ - ts = populated_collection['varying_series'] - assert ts.name == 'varying_series' + ts = sample_allocator['series1'] + assert ts.name == 'series1' # Test __contains__ with string - assert 'constant_series' in populated_collection - assert 'nonexistent_series' not in populated_collection + assert 'series1' in sample_allocator + assert 'nonexistent_series' not in sample_allocator # Test __contains__ with TimeSeries object - assert populated_collection['varying_series'] in populated_collection - - # Test __iter__ - names = [ts.name for ts in populated_collection] - assert len(names) == 6 - assert 'varying_series' in names + assert sample_allocator['series2'] in sample_allocator # Test access to non-existent series - with pytest.raises(KeyError): - populated_collection['nonexistent_series'] - - def test_constants_and_non_constants(self, populated_collection): - """Test constants and non_constants properties.""" - # Test constants - constants = populated_collection.constants - assert len(constants) == 4 # constant_series, group1_series1, group1_series2, weighted_series - assert all(ts.all_equal for ts in constants) - - # Test non_constants - non_constants = populated_collection.non_constants - assert len(non_constants) == 2 # varying_series, extra_timestep_series - assert all(not ts.all_equal for ts in non_constants) - - # Test modifying a series changes the results - populated_collection['constant_series'].stored_data = np.array([1, 2, 3, 4, 5]) - updated_constants = populated_collection.constants - assert len(updated_constants) == 3 # One less constant - assert 'constant_series' not in [ts.name for ts in updated_constants] - - def test_timesteps_properties(self, populated_collection, sample_timesteps): - """Test timestep-related properties.""" - # Test default (all) timesteps - assert populated_collection.timesteps.equals(sample_timesteps) - assert len(populated_collection.timesteps_extra) == len(sample_timesteps) + 1 - - # Test activating a subset - subset = sample_timesteps[1:3] - populated_collection.activate_timesteps(subset) - - assert populated_collection.timesteps.equals(subset) - assert len(populated_collection.timesteps_extra) == len(subset) + 1 - - # Check that time series were updated - assert populated_collection['varying_series'].active_timesteps.equals(subset) - assert populated_collection['extra_timestep_series'].active_timesteps.equals( - populated_collection.timesteps_extra - ) - - # Test reset - populated_collection.reset() - assert populated_collection.timesteps.equals(sample_timesteps) + with pytest.raises(ValueError): + sample_allocator['nonexistent_series'] - def test_to_dataframe_and_dataset(self, populated_collection): - """Test conversion to DataFrame and Dataset.""" - # Test to_dataset - ds = populated_collection.to_dataset() - assert isinstance(ds, xr.Dataset) - assert len(ds.data_vars) == 6 + def test_selection_propagation(self, sample_allocator, sample_timesteps): + """Test that selections propagate to TimeSeries.""" + # Add a few time series + ts1 = sample_allocator.add_time_series('series1', 42) + ts2 = sample_allocator.add_time_series('series2', np.array([1, 2, 3, 4, 5])) + ts3 = sample_allocator.add_time_series('series3', 5, has_extra_timestep=True) - # Test to_dataframe with different filters - df_all = populated_collection.to_dataframe(filtered='all') - assert len(df_all.columns) == 6 + # Initially no selections + assert ts1._selected_timesteps is None + assert ts2._selected_timesteps is None + assert ts3._selected_timesteps is None - df_constant = populated_collection.to_dataframe(filtered='constant') - assert len(df_constant.columns) == 4 + # Apply selection + subset_timesteps = sample_timesteps[1:3] + sample_allocator.set_selection(timesteps=subset_timesteps) - df_non_constant = populated_collection.to_dataframe(filtered='non_constant') - assert len(df_non_constant.columns) == 2 + # Check selection propagated to regular time series + assert ts1._selected_timesteps.equals(subset_timesteps) + assert ts2._selected_timesteps.equals(subset_timesteps) - # Test invalid filter - with pytest.raises(ValueError): - populated_collection.to_dataframe(filtered='invalid') - - def test_calculate_aggregation_weights(self, populated_collection): - """Test aggregation weight calculation.""" - weights = populated_collection.calculate_aggregation_weights() - - # Group weights should be 0.5 each (1/2) - assert populated_collection.group_weights['group1'] == 0.5 - - # Series in group1 should have weight 0.5 - assert weights['group1_series1'] == 0.5 - assert weights['group1_series2'] == 0.5 - - # Series with explicit weight should have that weight - assert weights['weighted_series'] == 0.5 - - # Series without group or weight should have weight 1 - assert weights['constant_series'] == 1 - - def test_insert_new_data(self, populated_collection, sample_timesteps): - """Test inserting new data.""" - # Create new data - new_data = pd.DataFrame( - { - 'constant_series': [100, 100, 100, 100, 100], - 'varying_series': [5, 10, 15, 20, 25], - # extra_timestep_series is omitted to test partial updates - }, - index=sample_timesteps, - ) + # Check selection with extra timestep + assert ts3._selected_timesteps is not None + assert len(ts3._selected_timesteps) == len(subset_timesteps) + 1 - # Insert data - populated_collection.insert_new_data(new_data) + # Clear selection + sample_allocator.clear_selection() - # Verify updates - assert np.all(populated_collection['constant_series'].active_data.values == 100) - assert np.array_equal(populated_collection['varying_series'].active_data.values, np.array([5, 10, 15, 20, 25])) + # Check selection cleared + assert ts1._selected_timesteps is None + assert ts2._selected_timesteps is None + assert ts3._selected_timesteps is None - # Series not in the DataFrame should be unchanged - assert np.array_equal( - populated_collection['extra_timestep_series'].active_data.values[:-1], np.array([1, 2, 3, 4, 5]) - ) + def test_update_time_series(self, sample_allocator): + """Test updating a time series.""" + # Add a time series + ts = sample_allocator.add_time_series('series', 42) - # Test with mismatched index - bad_index = pd.date_range('2023-02-01', periods=5, freq='D', name='time') - bad_data = pd.DataFrame({'constant_series': [1, 1, 1, 1, 1]}, index=bad_index) - - with pytest.raises(ValueError, match='must match collection timesteps'): - populated_collection.insert_new_data(bad_data) - - def test_restore_data(self, populated_collection): - """Test restoring original data.""" - # Capture original data - original_values = {name: ts.stored_data.copy() for name, ts in populated_collection.time_series_data.items()} - - # Modify data - new_data = pd.DataFrame( - { - name: np.ones(len(populated_collection.timesteps)) * 999 - for name in populated_collection.time_series_data - if not populated_collection[name].needs_extra_timestep - }, - index=populated_collection.timesteps, - ) + # Update it + sample_allocator.update_time_series('series', np.array([1, 2, 3, 4, 5])) - populated_collection.insert_new_data(new_data) + # Check update was applied + assert np.array_equal(ts.selected_data.values, np.array([1, 2, 3, 4, 5])) - # Verify data was changed - assert np.all(populated_collection['constant_series'].active_data.values == 999) + # Test updating non-existent series + with pytest.raises(KeyError): + sample_allocator.update_time_series('nonexistent', 42) - # Restore data - populated_collection.restore_data() + def test_as_dataset(self, sample_allocator): + """Test as_dataset method.""" + # Add some time series + sample_allocator.add_time_series('series1', 42) + sample_allocator.add_time_series('series2', np.array([1, 2, 3, 4, 5])) - # Verify data was restored - for name, original in original_values.items(): - restored = populated_collection[name].stored_data - assert np.array_equal(restored.values, original.values) + # Get dataset + ds = sample_allocator.as_dataset(with_extra_timestep=False) - def test_class_method_with_uniform_timesteps(self): - """Test the with_uniform_timesteps class method.""" - collection = TimeSeriesCollection.with_uniform_timesteps( - start_time=pd.Timestamp('2023-01-01'), periods=24, freq='H', hours_per_step=1 - ) + # Check dataset contents + assert isinstance(ds, xr.Dataset) + assert 'series1' in ds + assert 'series2' in ds + assert np.all(ds['series1'].values == 42) + assert np.array_equal(ds['series2'].values, np.array([1, 2, 3, 4, 5])) - assert len(collection.timesteps) == 24 - assert collection.hours_of_previous_timesteps == 1 - assert (collection.timesteps[1] - collection.timesteps[0]) == pd.Timedelta(hours=1) - - def test_hours_per_timestep(self, populated_collection): - """Test hours_per_timestep calculation.""" - # Standard case - uniform timesteps - hours = populated_collection.hours_per_timestep.values - assert np.allclose(hours, 24) # Default is daily timesteps - - # Create non-uniform timesteps - non_uniform_times = pd.DatetimeIndex( - [ - pd.Timestamp('2023-01-01'), - pd.Timestamp('2023-01-02'), - pd.Timestamp('2023-01-03 12:00:00'), # 1.5 days from previous - pd.Timestamp('2023-01-04'), # 0.5 days from previous - pd.Timestamp('2023-01-06'), # 2 days from previous - ], - name='time', - ) - collection = TimeSeriesCollection(non_uniform_times) - hours = collection.hours_per_timestep.values +class TestTimeSeriesAllocatorWithScenarios: + """Test suite for TimeSeriesCollection with scenarios.""" - # Expected hours between timestamps - expected = np.array([24, 36, 12, 48, 48]) - assert np.allclose(hours, expected) + def test_initialization_with_scenarios(self, sample_timesteps, sample_scenario_index): + """Test initialization with scenarios.""" + allocator = TimeSeriesCollection(sample_timesteps, scenarios=sample_scenario_index) - def test_validation_and_errors(self, sample_timesteps): - """Test validation and error handling.""" - # Test non-DatetimeIndex - with pytest.raises(TypeError, match='must be a pandas DatetimeIndex'): - TimeSeriesCollection(pd.Index([1, 2, 3, 4, 5])) + assert allocator.timesteps.equals(sample_timesteps) + assert allocator.scenarios.equals(sample_scenario_index) + assert len(allocator._time_series) == 0 - # Test too few timesteps - with pytest.raises(ValueError, match='must contain at least 2 timestamps'): - TimeSeriesCollection(pd.DatetimeIndex([pd.Timestamp('2023-01-01')], name='time')) + def test_add_time_series_with_scenarios(self, sample_scenario_allocator): + """Test creating time series with scenarios.""" + # Test scalar (broadcasts to all scenarios) + ts1 = sample_scenario_allocator.add_time_series('scalar_series', 42) + assert ts1._has_scenarios + assert ts1.name == 'scalar_series' + assert ts1.selected_data.shape == (3, 5) # 3 scenarios, 5 timesteps + assert np.all(ts1.selected_data.values == 42) - # Test invalid active_timesteps - collection = TimeSeriesCollection(sample_timesteps) - invalid_timesteps = pd.date_range('2024-01-01', periods=3, freq='D', name='time') + # Test 1D array (broadcasts to all scenarios) + data = np.array([1, 2, 3, 4, 5]) + ts2 = sample_scenario_allocator.add_time_series('array_series', data) + assert ts2._has_scenarios + assert ts2.selected_data.shape == (3, 5) + # Each scenario should have the same values + for scenario in sample_scenario_allocator.scenarios: + assert np.array_equal(ts2.sel(scenario=scenario).values, data) + + # Test 2D array (one row per scenario) + data_2d = np.array([ + [10, 20, 30, 40, 50], + [15, 25, 35, 45, 55], + [5, 15, 25, 35, 45] + ]) + ts3 = sample_scenario_allocator.add_time_series('scenario_specific_series', data_2d) + assert ts3._has_scenarios + assert ts3.selected_data.shape == (3, 5) + # Each scenario should have its own values + assert np.array_equal(ts3.sel(scenario='baseline').values, data_2d[0]) + assert np.array_equal(ts3.sel(scenario='high_demand').values, data_2d[1]) + assert np.array_equal(ts3.sel(scenario='low_price').values, data_2d[2]) + + def test_selection_propagation_with_scenarios(self, sample_scenario_allocator, sample_timesteps, sample_scenario_index): + """Test scenario selection propagation.""" + # Add some time series + ts1 = sample_scenario_allocator.add_time_series('series1', 42) + ts2 = sample_scenario_allocator.add_time_series('series2', np.array([1, 2, 3, 4, 5])) + + # Initial state - no selections + assert ts1._selected_scenarios is None + assert ts2._selected_scenarios is None + + # Select scenarios + subset_scenarios = sample_scenario_index[:2] + sample_scenario_allocator.set_selection(scenarios=subset_scenarios) + + # Check selections propagated + assert ts1._selected_scenarios.equals(subset_scenarios) + assert ts2._selected_scenarios.equals(subset_scenarios) + + # Check data is filtered + assert ts1.selected_data.shape == (2, 5) # 2 scenarios, 5 timesteps + assert ts2.selected_data.shape == (2, 5) + + # Apply combined selection + subset_timesteps = sample_timesteps[1:3] + sample_scenario_allocator.set_selection(timesteps=subset_timesteps, scenarios=subset_scenarios) + + # Check combined selection applied + assert ts1._selected_timesteps.equals(subset_timesteps) + assert ts1._selected_scenarios.equals(subset_scenarios) + assert ts1.selected_data.shape == (2, 2) # 2 scenarios, 2 timesteps + + # Clear selections + sample_scenario_allocator.clear_selection() + assert ts1._selected_timesteps is None + assert ts1._selected_scenarios is None + assert ts1.selected_data.shape == (3, 5) # Back to full shape + + def test_as_dataset_with_scenarios(self, sample_scenario_allocator): + """Test as_dataset method with scenarios.""" + # Add some time series + sample_scenario_allocator.add_time_series('scalar_series', 42) + sample_scenario_allocator.add_time_series( + 'varying_series', + np.array([ + [10, 20, 30, 40, 50], + [15, 25, 35, 45, 55], + [5, 15, 25, 35, 45] + ]) + ) - with pytest.raises(ValueError, match='must be a subset'): - collection.activate_timesteps(invalid_timesteps) + # Get dataset + ds = sample_scenario_allocator.as_dataset(with_extra_timestep=False) + + # Check dataset dimensions + assert 'scenario' in ds.dims + assert 'time' in ds.dims + assert ds.dims['scenario'] == 3 + assert ds.dims['time'] == 5 + + # Check dataset variables + assert 'scalar_series' in ds + assert 'varying_series' in ds + + # Check values + assert np.all(ds['scalar_series'].values == 42) + baseline_values = ds['varying_series'].sel(scenario='baseline').values + assert np.array_equal(baseline_values, np.array([10, 20, 30, 40, 50])) + + def test_contains_and_iteration(self, sample_scenario_allocator): + """Test __contains__ and __iter__ methods.""" + # Add some time series + ts1 = sample_scenario_allocator.add_time_series('series1', 42) + sample_scenario_allocator.add_time_series('series2', 10) + + # Test __contains__ + assert 'series1' in sample_scenario_allocator + assert ts1 in sample_scenario_allocator + assert 'nonexistent' not in sample_scenario_allocator + + # Test behavior with invalid type + with pytest.raises(TypeError): + assert 42 in sample_scenario_allocator + + def test_update_time_series_with_scenarios(self, sample_scenario_allocator, sample_scenario_index): + """Test updating a time series with scenarios.""" + # Add a time series + ts = sample_scenario_allocator.add_time_series('series', 42) + assert ts._has_scenarios + assert np.all(ts.selected_data.values == 42) + + # Update with scenario-specific data + new_data = np.array([ + [1, 2, 3, 4, 5], + [6, 7, 8, 9, 10], + [11, 12, 13, 14, 15] + ]) + sample_scenario_allocator.update_time_series('series', new_data) + + # Check update was applied + assert np.array_equal(ts.selected_data.values, new_data) + assert ts._has_scenarios + + # Check scenario-specific values + assert np.array_equal(ts.sel(scenario='baseline').values, new_data[0]) + assert np.array_equal(ts.sel(scenario='high_demand').values, new_data[1]) + assert np.array_equal(ts.sel(scenario='low_price').values, new_data[2]) + + +if __name__ == '__main__': + pytest.main() From 8c4a45bf59773e72858ff4a01058d0619ba97a6c Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 1 Apr 2025 18:42:26 +0200 Subject: [PATCH 002/448] Feature/scenarios Transform data and update type hints (#215) * Update TImeSeries to work with only scenario data * Get TImeSeriesCollection for Scenario data without time index * Simpliefy dataconverter * Drop support for pandas dataframe and Series for now * Remove test for pandas * ruff check * remove weird file * Update methods to create timeseries in FLowSystem * Bugfix * Add new Datatypes * Rename NumericData to TimestepData * Update Datatypes * Update Datatypes * Update create_time_series() * Add dimension data to Piece interfaces * Update transform_data() * Modify how time dimension is determined in Piecewise * Update OnOffParameters * Update typehints * Update Flow * Update Storage * Update typehints * Update Storage * Bugfix * Bugfix * Make sure TImeSeries are only created if needed * Bugfix * Bugfix * Bugfix and improve * Use function to get the coords of the linopy model * Updae method to determine what coords to use * Bugfix --- flixopt/components.py | 81 ++-- flixopt/core.py | 749 ++++++++++++------------------- flixopt/effects.py | 36 +- flixopt/elements.py | 47 +- flixopt/features.py | 38 +- flixopt/flow_system.py | 67 ++- flixopt/interface.py | 142 ++++-- flixopt/io.py | 2 +- flixopt/structure.py | 33 +- flixopt/utils.py | 8 - site/release-notes/_template.txt | 32 -- tests/run_all_tests.py | 2 +- tests/test_dataconverter.py | 344 +------------- 13 files changed, 590 insertions(+), 991 deletions(-) delete mode 100644 site/release-notes/_template.txt diff --git a/flixopt/components.py b/flixopt/components.py index 2a69c6165..4726ca0f4 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -9,7 +9,7 @@ import numpy as np from . import utils -from .core import NumericData, NumericDataTS, PlausibilityError, Scalar, TimeSeries +from .core import TimestepData, PlausibilityError, Scalar, TimeSeries, ScenarioData from .elements import Component, ComponentModel, Flow from .features import InvestmentModel, OnOffModel, PiecewiseModel from .interface import InvestParameters, OnOffParameters, PiecewiseConversion @@ -34,7 +34,7 @@ def __init__( inputs: List[Flow], outputs: List[Flow], on_off_parameters: OnOffParameters = None, - conversion_factors: List[Dict[str, NumericDataTS]] = None, + conversion_factors: List[Dict[str, TimestepData]] = None, piecewise_conversion: Optional[PiecewiseConversion] = None, meta_data: Optional[Dict] = None, ): @@ -92,6 +92,7 @@ def transform_data(self, flow_system: 'FlowSystem'): if self.conversion_factors: self.conversion_factors = self._transform_conversion_factors(flow_system) if self.piecewise_conversion: + self.piecewise_conversion.has_time_dim = True self.piecewise_conversion.transform_data(flow_system, f'{self.label_full}|PiecewiseConversion') def _transform_conversion_factors(self, flow_system: 'FlowSystem') -> List[Dict[str, TimeSeries]]: @@ -124,14 +125,14 @@ def __init__( charging: Flow, discharging: Flow, capacity_in_flow_hours: Union[Scalar, InvestParameters], - relative_minimum_charge_state: NumericData = 0, - relative_maximum_charge_state: NumericData = 1, - initial_charge_state: Union[Scalar, Literal['lastValueOfSim']] = 0, - minimal_final_charge_state: Optional[Scalar] = None, - maximal_final_charge_state: Optional[Scalar] = None, - eta_charge: NumericData = 1, - eta_discharge: NumericData = 1, - relative_loss_per_hour: NumericData = 0, + relative_minimum_charge_state: TimestepData = 0, + relative_maximum_charge_state: TimestepData = 1, + initial_charge_state: Union[ScenarioData, Literal['lastValueOfSim']] = 0, + minimal_final_charge_state: Optional[ScenarioData] = None, + maximal_final_charge_state: Optional[ScenarioData] = None, + eta_charge: TimestepData = 1, + eta_discharge: TimestepData = 1, + relative_loss_per_hour: TimestepData = 0, prevent_simultaneous_charge_and_discharge: bool = True, meta_data: Optional[Dict] = None, ): @@ -172,16 +173,16 @@ def __init__( self.charging = charging self.discharging = discharging self.capacity_in_flow_hours = capacity_in_flow_hours - self.relative_minimum_charge_state: NumericDataTS = relative_minimum_charge_state - self.relative_maximum_charge_state: NumericDataTS = relative_maximum_charge_state + self.relative_minimum_charge_state: TimestepData = relative_minimum_charge_state + self.relative_maximum_charge_state: TimestepData = relative_maximum_charge_state self.initial_charge_state = initial_charge_state self.minimal_final_charge_state = minimal_final_charge_state self.maximal_final_charge_state = maximal_final_charge_state - self.eta_charge: NumericDataTS = eta_charge - self.eta_discharge: NumericDataTS = eta_discharge - self.relative_loss_per_hour: NumericDataTS = relative_loss_per_hour + self.eta_charge: TimestepData = eta_charge + self.eta_discharge: TimestepData = eta_discharge + self.relative_loss_per_hour: TimestepData = relative_loss_per_hour self.prevent_simultaneous_charge_and_discharge = prevent_simultaneous_charge_and_discharge def create_model(self, model: SystemModel) -> 'StorageModel': @@ -206,14 +207,28 @@ def transform_data(self, flow_system: 'FlowSystem') -> None: self.relative_loss_per_hour = flow_system.create_time_series( f'{self.label_full}|relative_loss_per_hour', self.relative_loss_per_hour ) + if self.initial_charge_state != 'lastValueOfSim': + self.initial_charge_state = flow_system.create_time_series( + f'{self.label_full}|initial_charge_state', self.initial_charge_state, has_time_dim=False + ) + self.minimal_final_charge_state = flow_system.create_time_series( + f'{self.label_full}|minimal_final_charge_state', self.minimal_final_charge_state, has_time_dim=False + ) + self.maximal_final_charge_state = flow_system.create_time_series( + f'{self.label_full}|maximal_final_charge_state', self.maximal_final_charge_state, has_time_dim=False + ) if isinstance(self.capacity_in_flow_hours, InvestParameters): - self.capacity_in_flow_hours.transform_data(flow_system) + self.capacity_in_flow_hours.transform_data(flow_system, f'{self.label_full}|InvestParameters') def _plausibility_checks(self) -> None: """ Check for infeasible or uncommon combinations of parameters """ - if utils.is_number(self.initial_charge_state): + if isinstance(self.initial_charge_state, str) and not self.initial_charge_state == 'lastValueOfSim': + raise PlausibilityError( + f'initial_charge_state has undefined value: {self.initial_charge_state}' + ) + else: if isinstance(self.capacity_in_flow_hours, InvestParameters): if self.capacity_in_flow_hours.fixed_size is None: maximum_capacity = self.capacity_in_flow_hours.maximum_size @@ -229,20 +244,18 @@ def _plausibility_checks(self) -> None: minimum_inital_capacity = maximum_capacity * self.relative_minimum_charge_state.isel(time=1) # initial capacity <= allowed max for minimum_size: maximum_inital_capacity = minimum_capacity * self.relative_maximum_charge_state.isel(time=1) + #TODO: index=1 ??? I think index 0 - if self.initial_charge_state > maximum_inital_capacity: + if (self.initial_charge_state > maximum_inital_capacity).any(): raise ValueError( f'{self.label_full}: {self.initial_charge_state=} ' f'is above allowed maximum charge_state {maximum_inital_capacity}' ) - if self.initial_charge_state < minimum_inital_capacity: + if (self.initial_charge_state < minimum_inital_capacity).any(): raise ValueError( f'{self.label_full}: {self.initial_charge_state=} ' f'is below allowed minimum charge_state {minimum_inital_capacity}' ) - elif self.initial_charge_state != 'lastValueOfSim': - raise ValueError(f'{self.label_full}: {self.initial_charge_state=} has an invalid value') - @register_class_for_io class Transmission(Component): @@ -259,8 +272,8 @@ def __init__( out1: Flow, in2: Optional[Flow] = None, out2: Optional[Flow] = None, - relative_losses: Optional[NumericDataTS] = None, - absolute_losses: Optional[NumericDataTS] = None, + relative_losses: Optional[TimestepData] = None, + absolute_losses: Optional[TimestepData] = None, on_off_parameters: OnOffParameters = None, prevent_simultaneous_flows_in_both_directions: bool = True, meta_data: Optional[Dict] = None, @@ -454,12 +467,12 @@ def do_modeling(self): lb, ub = self.absolute_charge_state_bounds self.charge_state = self.add( self._model.add_variables( - lower=lb, upper=ub, coords=self._model.coords_extra, name=f'{self.label_full}|charge_state' + lower=lb, upper=ub, coords=self._model.get_coords(extra_timestep=True), name=f'{self.label_full}|charge_state' ), 'charge_state', ) self.netto_discharge = self.add( - self._model.add_variables(coords=self._model.coords, name=f'{self.label_full}|netto_discharge'), + self._model.add_variables(coords=self._model.get_coords(), name=f'{self.label_full}|netto_discharge'), 'netto_discharge', ) # netto_discharge: @@ -511,24 +524,20 @@ def _initial_and_final_charge_state(self): name_short = 'initial_charge_state' name = f'{self.label_full}|{name_short}' - if utils.is_number(self.element.initial_charge_state): + if self.element.initial_charge_state == 'lastValueOfSim': self.add( self._model.add_constraints( - self.charge_state.isel(time=0) == self.element.initial_charge_state, name=name + self.charge_state.isel(time=0) == self.charge_state.isel(time=-1), name=name ), name_short, ) - elif self.element.initial_charge_state == 'lastValueOfSim': + else: self.add( self._model.add_constraints( - self.charge_state.isel(time=0) == self.charge_state.isel(time=-1), name=name + self.charge_state.isel(time=0) == self.element.initial_charge_state, name=name ), name_short, ) - else: # TODO: Validation in Storage Class, not in Model - raise PlausibilityError( - f'initial_charge_state has undefined value: {self.element.initial_charge_state}' - ) if self.element.maximal_final_charge_state is not None: self.add( @@ -549,7 +558,7 @@ def _initial_and_final_charge_state(self): ) @property - def absolute_charge_state_bounds(self) -> Tuple[NumericData, NumericData]: + def absolute_charge_state_bounds(self) -> Tuple[TimestepData, TimestepData]: relative_lower_bound, relative_upper_bound = self.relative_charge_state_bounds if not isinstance(self.element.capacity_in_flow_hours, InvestParameters): return ( @@ -563,7 +572,7 @@ def absolute_charge_state_bounds(self) -> Tuple[NumericData, NumericData]: ) @property - def relative_charge_state_bounds(self) -> Tuple[NumericData, NumericData]: + def relative_charge_state_bounds(self) -> Tuple[TimestepData, TimestepData]: return ( self.element.relative_minimum_charge_state.selected_data, self.element.relative_maximum_charge_state.selected_data, diff --git a/flixopt/core.py b/flixopt/core.py index d2a8edd59..185236b3a 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -26,6 +26,12 @@ NumericDataTS = Union[NumericData, 'TimeSeriesData'] """Represents either standard numeric data or TimeSeriesData.""" +TimestepData = NumericData +"""Represents any form of numeric data that corresponds to timesteps.""" + +ScenarioData = NumericData +"""Represents any form of numeric data that corresponds to scenarios.""" + class PlausibilityError(Exception): """Error for a failing Plausibility check.""" @@ -41,568 +47,322 @@ class ConversionError(Exception): class DataConverter: """ - Converts various data types into xarray.DataArray with timesteps and optional scenarios dimensions. - - Supports: - - Scalar values (broadcast to all timesteps/scenarios) - - 1D arrays (mapped to timesteps, broadcast to scenarios if provided) - - 2D arrays (mapped to scenarios × timesteps if dimensions match) - - Series with time index (broadcast to scenarios if provided) - - DataFrames with time index and a single column (broadcast to scenarios if provided) - - Series/DataFrames with MultiIndex (scenario, time) - - Existing DataArrays - """ + Converts various data types into xarray.DataArray with optional time and scenario dimension. - #TODO: Allow DataFrame with scenarios as columns + Current implementation handles: + - Scalar values + - NumPy arrays + - xarray.DataArray + """ @staticmethod def as_dataarray( - data: NumericData, timesteps: pd.DatetimeIndex, scenarios: Optional[pd.Index] = None + data: TimestepData, timesteps: Optional[pd.DatetimeIndex] = None, scenarios: Optional[pd.Index] = None ) -> xr.DataArray: """ - Convert data to xarray.DataArray with specified timesteps and optional scenarios dimensions. + Convert data to xarray.DataArray with specified dimensions. Args: - data: The data to convert (scalar, array, Series, DataFrame, or DataArray) - timesteps: DatetimeIndex representing the time dimension (must be named 'time') - scenarios: Optional Index representing scenarios (must be named 'scenario') + data: The data to convert (scalar, array, or DataArray) + timesteps: Optional DatetimeIndex for time dimension + scenarios: Optional Index for scenario dimension Returns: DataArray with the converted data - - Raises: - ValueError: If timesteps or scenarios are invalid - ConversionError: If the data cannot be converted to the expected dimensions """ - # Validate inputs - DataConverter._validate_timesteps(timesteps) - if scenarios is not None: - DataConverter._validate_scenarios(scenarios) - - # Determine dimensions and coordinates - coords, dims, expected_shape = DataConverter._get_dimensions(timesteps, scenarios) - - try: - # Convert different data types using specialized methods - if isinstance(data, (int, float, np.integer, np.floating)): - return DataConverter._convert_scalar(data, coords, dims) + # Prepare dimensions and coordinates + coords, dims = DataConverter._prepare_dimensions(timesteps, scenarios) - elif isinstance(data, pd.DataFrame): - return DataConverter._convert_dataframe(data, timesteps, scenarios, coords, dims) + # Select appropriate converter based on data type + if isinstance(data, (int, float, np.integer, np.floating)): + return DataConverter._convert_scalar(data, coords, dims) - elif isinstance(data, pd.Series): - return DataConverter._convert_series(data, timesteps, scenarios, coords, dims) + elif isinstance(data, xr.DataArray): + return DataConverter._convert_dataarray(data, coords, dims) - elif isinstance(data, np.ndarray): - return DataConverter._convert_ndarray(data, timesteps, scenarios, coords, dims, expected_shape) + elif isinstance(data, np.ndarray): + return DataConverter._convert_ndarray(data, coords, dims) - elif isinstance(data, xr.DataArray): - return DataConverter._convert_dataarray(data, timesteps, scenarios, coords, dims) - - else: - raise ConversionError(f'Unsupported type: {type(data).__name__}') - - except Exception as e: - if isinstance(e, ConversionError): - raise - raise ConversionError(f'Converting {type(data)} to DataArray raised an error: {str(e)}') from e + else: + raise ConversionError(f'Unsupported data type: {type(data).__name__}') @staticmethod - def _validate_timesteps(timesteps: pd.DatetimeIndex) -> None: + def _validate_timesteps(timesteps: pd.DatetimeIndex) -> pd.DatetimeIndex: """ - Validate that timesteps is a properly named non-empty DatetimeIndex. + Validate and prepare time index. Args: - timesteps: The DatetimeIndex to validate + timesteps: The time index to validate - Raises: - ValueError: If timesteps is not a non-empty DatetimeIndex - ConversionError: If timesteps is not named 'time' + Returns: + Validated time index """ if not isinstance(timesteps, pd.DatetimeIndex) or len(timesteps) == 0: - raise ValueError(f'Timesteps must be a non-empty DatetimeIndex, got {type(timesteps).__name__}') - if timesteps.name != 'time': - raise ConversionError(f'DatetimeIndex must be named "time", got {timesteps.name=}') + raise ConversionError('Timesteps must be a non-empty DatetimeIndex') - @staticmethod - def _validate_scenarios(scenarios: pd.Index) -> None: - """ - Validate that scenarios is a properly named non-empty Index. + if not timesteps.name == 'time': + raise ConversionError(f'Scenarios must be named "time", got "{timesteps.name}"') - Args: - scenarios: The Index to validate - - Raises: - ValueError: If scenarios is not a non-empty Index - ConversionError: If scenarios is not named 'scenario' - """ - if not isinstance(scenarios, pd.Index) or len(scenarios) == 0: - raise ValueError(f'Scenarios must be a non-empty Index, got {type(scenarios).__name__}') - if scenarios.name != 'scenario': - raise ConversionError(f'Scenarios Index must be named "scenario", got {scenarios.name=}') + return timesteps @staticmethod - def _get_dimensions( - timesteps: pd.DatetimeIndex, scenarios: Optional[pd.Index] = None - ) -> Tuple[Dict[str, pd.Index], Tuple[str, ...], Tuple[int, ...]]: + def _validate_scenarios(scenarios: pd.Index) -> pd.Index: """ - Create the coordinates, dimensions, and expected shape for the output DataArray. + Validate and prepare scenario index. Args: - timesteps: The time index - scenarios: Optional scenario index - - Returns: - Tuple containing: - - Dict mapping dimension names to coordinate indexes - - Tuple of dimension names - - Tuple of expected shape - """ - if scenarios is not None: - coords = {'scenario': scenarios, 'time': timesteps} - dims = ('scenario', 'time') - expected_shape = (len(scenarios), len(timesteps)) - else: - coords = {'time': timesteps} - dims = ('time',) - expected_shape = (len(timesteps),) - - return coords, dims, expected_shape - - @staticmethod - def _convert_scalar( - data: Union[int, float, np.integer, np.floating], coords: Dict[str, pd.Index], dims: Tuple[str, ...] - ) -> xr.DataArray: + scenarios: The scenario index to validate """ - Convert a scalar value to a DataArray. + if not isinstance(scenarios, pd.Index) or len(scenarios) == 0: + raise ConversionError('Scenarios must be a non-empty Index') - Args: - data: The scalar value to convert - coords: Dictionary mapping dimension names to coordinate indexes - dims: Tuple of dimension names + if not scenarios.name == 'scenario': + raise ConversionError(f'Scenarios must be named "scenario", got "{scenarios.name}"') - Returns: - DataArray with the scalar value broadcast to all coordinates - """ - return xr.DataArray(data, coords=coords, dims=dims) + return scenarios @staticmethod - def _convert_dataframe( - df: pd.DataFrame, - timesteps: pd.DatetimeIndex, - scenarios: Optional[pd.Index], - coords: Dict[str, pd.Index], - dims: Tuple[str, ...], - ) -> xr.DataArray: + def _prepare_dimensions( + timesteps: Optional[pd.DatetimeIndex], scenarios: Optional[pd.Index] + ) -> Tuple[Dict[str, pd.Index], Tuple[str, ...]]: """ - Convert a DataFrame to a DataArray. + Prepare coordinates and dimensions for the DataArray. Args: - df: The DataFrame to convert - timesteps: The time index + timesteps: Optional time index scenarios: Optional scenario index - coords: Dictionary mapping dimension names to coordinate indexes - dims: Tuple of dimension names Returns: - DataArray created from the DataFrame - - Raises: - ConversionError: If the DataFrame cannot be converted to the expected dimensions - """ - # Case 1: DataFrame with MultiIndex (scenario, time) - if ( - isinstance(df.index, pd.MultiIndex) - and len(df.index.names) == 2 - and 'scenario' in df.index.names - and 'time' in df.index.names - and scenarios is not None - ): - return DataConverter._convert_multi_index_dataframe(df, timesteps, scenarios, coords, dims) - - # Case 2: Standard DataFrame with time index - elif not isinstance(df.index, pd.MultiIndex): - return DataConverter._convert_standard_dataframe(df, timesteps, scenarios, coords, dims) - - else: - raise ConversionError(f'Unsupported DataFrame index structure: {df}') - - @staticmethod - def _convert_multi_index_dataframe( - df: pd.DataFrame, - timesteps: pd.DatetimeIndex, - scenarios: pd.Index, - coords: Dict[str, pd.Index], - dims: Tuple[str, ...], - ) -> xr.DataArray: + Tuple of (coordinates dict, dimensions tuple) """ - Convert a DataFrame with MultiIndex (scenario, time) to a DataArray. + # Validate inputs if provided + if timesteps is not None: + timesteps = DataConverter._validate_timesteps(timesteps) - Args: - df: The DataFrame with MultiIndex to convert - timesteps: The time index - scenarios: The scenario index - coords: Dictionary mapping dimension names to coordinate indexes - dims: Tuple of dimension names - - Returns: - DataArray created from the MultiIndex DataFrame + if scenarios is not None: + scenarios = DataConverter._validate_scenarios(scenarios) - Raises: - ConversionError: If the DataFrame's index doesn't match expected or has multiple columns - """ - # Validate that the index contains the expected values - if not set(df.index.get_level_values('time')).issubset(set(timesteps)): - raise ConversionError("DataFrame time index doesn't match or isn't a subset of timesteps") - if not set(df.index.get_level_values('scenario')).issubset(set(scenarios)): - raise ConversionError("DataFrame scenario index doesn't match or isn't a subset of scenarios") + # Build coordinates and dimensions + coords = {} + dims = [] - # Ensure single column - if len(df.columns) != 1: - raise ConversionError('DataFrame must have exactly one column') + if scenarios is not None: + coords['scenario'] = scenarios + dims.append('scenario') - # Reindex to ensure complete coverage and correct order - multi_idx = pd.MultiIndex.from_product([scenarios, timesteps], names=['scenario', 'time']) - reindexed = df.reindex(multi_idx).iloc[:, 0] + if timesteps is not None: + coords['time'] = timesteps + dims.append('time') - # Reshape to 2D array - reshaped = reindexed.values.reshape(len(scenarios), len(timesteps)) - return xr.DataArray(reshaped, coords=coords, dims=dims) + return coords, tuple(dims) @staticmethod - def _convert_standard_dataframe( - df: pd.DataFrame, - timesteps: pd.DatetimeIndex, - scenarios: Optional[pd.Index], - coords: Dict[str, pd.Index], - dims: Tuple[str, ...], + def _convert_scalar( + data: Union[int, float, np.integer, np.floating], coords: Dict[str, pd.Index], dims: Tuple[str, ...] ) -> xr.DataArray: """ - Convert a standard DataFrame with time index to a DataArray. + Convert a scalar value to a DataArray. Args: - df: The DataFrame to convert - timesteps: The time index - scenarios: Optional scenario index - coords: Dictionary mapping dimension names to coordinate indexes - dims: Tuple of dimension names + data: The scalar value + coords: Coordinate dictionary + dims: Dimension names Returns: - DataArray created from the DataFrame - - Raises: - ConversionError: If the DataFrame's index doesn't match timesteps or has multiple columns + DataArray with the scalar value """ - if not df.index.equals(timesteps): - raise ConversionError("DataFrame index doesn't match timesteps index") - if len(df.columns) != 1: - raise ConversionError('DataFrame must have exactly one column') - - # Get values - values = df.values.flatten() - - if scenarios is not None: - # Broadcast to scenarios dimension - values = np.tile(values, (len(scenarios), 1)) - - return xr.DataArray(values, coords=coords, dims=dims) + return xr.DataArray(data, coords=coords, dims=dims) @staticmethod - def _convert_series( - series: pd.Series, - timesteps: pd.DatetimeIndex, - scenarios: Optional[pd.Index], - coords: Dict[str, pd.Index], - dims: Tuple[str, ...], - ) -> xr.DataArray: + def _convert_dataarray(data: xr.DataArray, coords: Dict[str, pd.Index], dims: Tuple[str, ...]) -> xr.DataArray: """ - Convert a Series to a DataArray. + Convert an existing DataArray to desired dimensions. Args: - series: The Series to convert - timesteps: The time index - scenarios: Optional scenario index - coords: Dictionary mapping dimension names to coordinate indexes - dims: Tuple of dimension names + data: The source DataArray + coords: Target coordinates + dims: Target dimensions Returns: - DataArray created from the Series - - Raises: - ConversionError: If the Series cannot be converted to the expected dimensions - """ - # Case 1: Series with MultiIndex (scenario, time) - if ( - isinstance(series.index, pd.MultiIndex) - and len(series.index.names) == 2 - and 'scenario' in series.index.names - and 'time' in series.index.names - and scenarios is not None - ): - return DataConverter._convert_multi_index_series(series, timesteps, scenarios, coords, dims) - - # Case 2: Standard Series with time index - elif not isinstance(series.index, pd.MultiIndex): - return DataConverter._convert_standard_series(series, timesteps, scenarios, coords, dims) - - else: - raise ConversionError('Unsupported Series index structure') + DataArray with the target dimensions + """ + # No dimensions case + if len(dims) == 0: + if data.size != 1: + raise ConversionError('When converting to dimensionless DataArray, source must be scalar') + return xr.DataArray(data.values.item()) + + # Check if data already has matching dimensions + if set(data.dims) == set(dims): + # Check if coordinates match + is_compatible = True + for dim in dims: + if dim in data.dims and not np.array_equal(data.coords[dim].values, coords[dim].values): + is_compatible = False + break + + if is_compatible: + # Return existing DataArray if compatible + return data.copy(deep=True) + + # Handle dimension broadcasting + if len(data.dims) == 1 and len(dims) == 2: + # Single dimension to two dimensions + if data.dims[0] == 'time' and 'scenario' in dims: + # Broadcast time dimension to include scenarios + return DataConverter._broadcast_time_to_scenarios(data, coords, dims) + + elif data.dims[0] == 'scenario' and 'time' in dims: + # Broadcast scenario dimension to include time + return DataConverter._broadcast_scenario_to_time(data, coords, dims) + + raise ConversionError(f'Cannot convert {data.dims} to {dims}') @staticmethod - def _convert_multi_index_series( - series: pd.Series, - timesteps: pd.DatetimeIndex, - scenarios: pd.Index, - coords: Dict[str, pd.Index], - dims: Tuple[str, ...], + def _broadcast_time_to_scenarios( + data: xr.DataArray, coords: Dict[str, pd.Index], dims: Tuple[str, ...] ) -> xr.DataArray: """ - Convert a Series with MultiIndex (scenario, time) to a DataArray. + Broadcast a time-only DataArray to include scenarios. Args: - series: The Series with MultiIndex to convert - timesteps: The time index - scenarios: The scenario index - coords: Dictionary mapping dimension names to coordinate indexes - dims: Tuple of dimension names + data: The time-indexed DataArray + coords: Target coordinates + dims: Target dimensions Returns: - DataArray created from the MultiIndex Series - - Raises: - ConversionError: If the Series' index doesn't match expected + DataArray with time and scenario dimensions """ - # Validate that the index contains the expected values - if not set(series.index.get_level_values('time')).issubset(set(timesteps)): - raise ConversionError("Series time index doesn't match or isn't a subset of timesteps") - if not set(series.index.get_level_values('scenario')).issubset(set(scenarios)): - raise ConversionError("Series scenario index doesn't match or isn't a subset of scenarios") + # Check compatibility + if not np.array_equal(data.coords['time'].values, coords['time'].values): + raise ConversionError("Source time coordinates don't match target time coordinates") - # Reindex to ensure complete coverage and correct order - multi_idx = pd.MultiIndex.from_product([scenarios, timesteps], names=['scenario', 'time']) - reindexed = series.reindex(multi_idx) - - # Reshape to 2D array - reshaped = reindexed.values.reshape(len(scenarios), len(timesteps)) - return xr.DataArray(reshaped, coords=coords, dims=dims) + # Broadcast values + values = np.tile(data.values, (len(coords['scenario']), 1)) + return xr.DataArray(values, coords=coords, dims=dims) @staticmethod - def _convert_standard_series( - series: pd.Series, - timesteps: pd.DatetimeIndex, - scenarios: Optional[pd.Index], - coords: Dict[str, pd.Index], - dims: Tuple[str, ...], + def _broadcast_scenario_to_time( + data: xr.DataArray, coords: Dict[str, pd.Index], dims: Tuple[str, ...] ) -> xr.DataArray: """ - Convert a standard Series with time index to a DataArray. + Broadcast a scenario-only DataArray to include time. Args: - series: The Series to convert - timesteps: The time index - scenarios: Optional scenario index - coords: Dictionary mapping dimension names to coordinate indexes - dims: Tuple of dimension names + data: The scenario-indexed DataArray + coords: Target coordinates + dims: Target dimensions Returns: - DataArray created from the Series - - Raises: - ConversionError: If the Series' index doesn't match timesteps + DataArray with time and scenario dimensions """ - if not series.index.equals(timesteps): - raise ConversionError("Series index doesn't match timesteps index") - - # Get values - values = series.values - - if scenarios is not None: - # Broadcast to scenarios dimension - values = np.tile(values, (len(scenarios), 1)) + # Check compatibility + if not np.array_equal(data.coords['scenario'].values, coords['scenario'].values): + raise ConversionError("Source scenario coordinates don't match target scenario coordinates") + # Broadcast values + values = np.repeat(data.values[:, np.newaxis], len(coords['time']), axis=1) return xr.DataArray(values, coords=coords, dims=dims) @staticmethod - def _convert_ndarray( - arr: np.ndarray, - timesteps: pd.DatetimeIndex, - scenarios: Optional[pd.Index], - coords: Dict[str, pd.Index], - dims: Tuple[str, ...], - expected_shape: Tuple[int, ...], - ) -> xr.DataArray: + def _convert_ndarray(data: np.ndarray, coords: Dict[str, pd.Index], dims: Tuple[str, ...]) -> xr.DataArray: """ - Convert a numpy array to a DataArray. + Convert a NumPy array to a DataArray. Args: - arr: The numpy array to convert - timesteps: The time index - scenarios: Optional scenario index - coords: Dictionary mapping dimension names to coordinate indexes - dims: Tuple of dimension names - expected_shape: Expected shape of the resulting array + data: The NumPy array + coords: Target coordinates + dims: Target dimensions Returns: - DataArray created from the numpy array - - Raises: - ConversionError: If the array cannot be converted to the expected dimensions - """ - # Case 1: With scenarios - array can be 1D or 2D - if scenarios is not None: - return DataConverter._convert_ndarray_with_scenarios( - arr, timesteps, scenarios, coords, dims, expected_shape - ) - - # Case 2: Without scenarios - array must be 1D - else: - return DataConverter._convert_ndarray_without_scenarios(arr, timesteps, coords, dims) - - @staticmethod - def _convert_ndarray_with_scenarios( - arr: np.ndarray, - timesteps: pd.DatetimeIndex, - scenarios: pd.Index, - coords: Dict[str, pd.Index], - dims: Tuple[str, ...], - expected_shape: Tuple[int, ...], - ) -> xr.DataArray: + DataArray from the NumPy array """ - Convert a numpy array to a DataArray with scenarios dimension. - - Args: - arr: The numpy array to convert - timesteps: The time index - scenarios: The scenario index - coords: Dictionary mapping dimension names to coordinate indexes - dims: Tuple of dimension names - expected_shape: Expected shape (scenarios, timesteps) + # Handle dimensionless case + if len(dims) == 0: + if data.size != 1: + raise ConversionError('Without dimensions, can only convert scalar arrays') + return xr.DataArray(data.item()) - Returns: - DataArray created from the numpy array + # Handle single dimension + elif len(dims) == 1: + return DataConverter._convert_ndarray_single_dim(data, coords, dims) - Raises: - ConversionError: If the array dimensions don't match expected - """ - if arr.ndim == 1: - # 1D array should match timesteps and be broadcast to scenarios - if arr.shape[0] != len(timesteps): - raise ConversionError(f"1D array length {arr.shape[0]} doesn't match timesteps length {len(timesteps)}") - # Broadcast to scenarios - values = np.tile(arr, (len(scenarios), 1)) - return xr.DataArray(values, coords=coords, dims=dims) - - elif arr.ndim == 2: - # 2D array should match (scenarios, timesteps) - if arr.shape != expected_shape: - raise ConversionError(f"2D array shape {arr.shape} doesn't match expected shape {expected_shape}") - return xr.DataArray(arr, coords=coords, dims=dims) + # Handle two dimensions + elif len(dims) == 2: + return DataConverter._convert_ndarray_two_dims(data, coords, dims) else: - raise ConversionError(f'Array must be 1D or 2D, got {arr.ndim}D') - - @staticmethod - def _convert_ndarray_without_scenarios( - arr: np.ndarray, timesteps: pd.DatetimeIndex, coords: Dict[str, pd.Index], dims: Tuple[str, ...] - ) -> xr.DataArray: - """ - Convert a numpy array to a DataArray without scenarios dimension. - - Args: - arr: The numpy array to convert - timesteps: The time index - coords: Dictionary mapping dimension names to coordinate indexes - dims: Tuple of dimension names - - Returns: - DataArray created from the numpy array - - Raises: - ConversionError: If the array isn't 1D or doesn't match timesteps length - """ - if arr.ndim != 1: - raise ConversionError(f'Without scenarios, array must be 1D, got {arr.ndim}D') - if arr.shape[0] != len(timesteps): - raise ConversionError(f"Array shape {arr.shape} doesn't match expected length {len(timesteps)}") - return xr.DataArray(arr, coords=coords, dims=dims) + raise ConversionError('Maximum 2 dimensions supported') @staticmethod - def _convert_dataarray( - da: xr.DataArray, - timesteps: pd.DatetimeIndex, - scenarios: Optional[pd.Index], - coords: Dict[str, pd.Index], - dims: Tuple[str, ...], + def _convert_ndarray_single_dim( + data: np.ndarray, coords: Dict[str, pd.Index], dims: Tuple[str, ...] ) -> xr.DataArray: """ - Convert an existing DataArray to a new DataArray with the desired dimensions. + Convert a NumPy array to a single-dimension DataArray. Args: - da: The DataArray to convert - timesteps: The time index - scenarios: Optional scenario index - coords: Dictionary mapping dimension names to coordinate indexes - dims: Tuple of dimension names + data: The NumPy array + coords: Target coordinates + dims: Target dimensions (length 1) Returns: - New DataArray with the specified coordinates and dimensions - - Raises: - ConversionError: If the DataArray dimensions don't match expected + DataArray with single dimension """ - # Case 1: DataArray with only time dimension when scenarios are provided - if scenarios is not None and set(da.dims) == {'time'}: - return DataConverter._broadcast_time_only_dataarray(da, timesteps, scenarios, coords, dims) - - # Case 2: DataArray dimensions should match expected - elif set(da.dims) != set(dims): - raise ConversionError(f"DataArray dimensions {da.dims} don't match expected {dims}") + dim_name = dims[0] + dim_length = len(coords[dim_name]) - # Validate dimensions sizes - for dim in dims: - if not np.array_equal(da.coords[dim].values, coords[dim].values): - raise ConversionError(f"DataArray dimension '{dim}' doesn't match expected {coords[dim]}") - - # Create a new DataArray with our coordinates to ensure consistency - result = xr.DataArray(da.values.copy(), coords=coords, dims=dims) - return result + if data.ndim == 1: + # 1D array must match dimension length + if data.shape[0] != dim_length: + raise ConversionError(f"Array length {data.shape[0]} doesn't match {dim_name} length {dim_length}") + return xr.DataArray(data, coords=coords, dims=dims) + else: + raise ConversionError(f'Expected 1D array for single dimension, got {data.ndim}D') @staticmethod - def _broadcast_time_only_dataarray( - da: xr.DataArray, - timesteps: pd.DatetimeIndex, - scenarios: pd.Index, - coords: Dict[str, pd.Index], - dims: Tuple[str, ...], - ) -> xr.DataArray: + def _convert_ndarray_two_dims(data: np.ndarray, coords: Dict[str, pd.Index], dims: Tuple[str, ...]) -> xr.DataArray: """ - Broadcast a time-only DataArray to include the scenarios dimension. + Convert a NumPy array to a two-dimension DataArray. Args: - da: The DataArray with only time dimension - timesteps: The time index - scenarios: The scenario index - coords: Dictionary mapping dimension names to coordinate indexes - dims: Tuple of dimension names + data: The NumPy array + coords: Target coordinates + dims: Target dimensions (length 2) Returns: - DataArray with the data broadcast to include scenarios dimension + DataArray with two dimensions + """ + scenario_length = len(coords['scenario']) + time_length = len(coords['time']) + + if data.ndim == 1: + # For 1D array, create 2D array based on which dimension it matches + if data.shape[0] == time_length: + # Broadcast across scenarios + values = np.tile(data, (scenario_length, 1)) + return xr.DataArray(values, coords=coords, dims=dims) + elif data.shape[0] == scenario_length: + # Broadcast across time + values = np.repeat(data[:, np.newaxis], time_length, axis=1) + return xr.DataArray(values, coords=coords, dims=dims) + else: + raise ConversionError(f"1D array length {data.shape[0]} doesn't match either dimension") - Raises: - ConversionError: If the DataArray time coordinates aren't compatible with timesteps - """ - # Ensure the time dimension is compatible - if not np.array_equal(da.coords['time'].values, timesteps.values): - raise ConversionError("DataArray time coordinates aren't compatible with timesteps") + elif data.ndim == 2: + # For 2D array, shape must match dimensions + expected_shape = (scenario_length, time_length) + if data.shape != expected_shape: + raise ConversionError(f"2D array shape {data.shape} doesn't match expected shape {expected_shape}") + return xr.DataArray(data, coords=coords, dims=dims) - # Broadcast to scenarios - values = np.tile(da.values.copy(), (len(scenarios), 1)) - return xr.DataArray(values, coords=coords, dims=dims) + else: + raise ConversionError(f'Expected 1D or 2D array for two dimensions, got {data.ndim}D') class TimeSeriesData: # TODO: Move to Interface.py - def __init__(self, data: NumericData, agg_group: Optional[str] = None, agg_weight: Optional[float] = None): + def __init__(self, data: TimestepData, agg_group: Optional[str] = None, agg_weight: Optional[float] = None): """ timeseries class for transmit timeseries AND special characteristics of timeseries, i.g. to define weights needed in calculation_type 'aggregated' @@ -744,11 +504,8 @@ def __init__( has_extra_timestep: Whether this series requires an extra timestep Raises: - ValueError: If data doesn't have a 'time' index or has unsupported dimensions + ValueError: If data has unsupported dimensions """ - if 'time' not in data.indexes: - raise ValueError(f'DataArray must have a "time" index. Got {data.indexes}') - allowed_dims = {'time', 'scenario'} if not set(data.dims).issubset(allowed_dims): raise ValueError(f'DataArray dimensions must be subset of {allowed_dims}. Got {data.dims}') @@ -766,8 +523,9 @@ def __init__( self._selected_timesteps: Optional[pd.DatetimeIndex] = None self._selected_scenarios: Optional[pd.Index] = None - # Flag for whether this series has scenarios - self._has_scenarios = 'scenario' in data.dims + # Flag for whether this series has various dimensions + self.has_time_dim = 'time' in data.dims + self.has_scenario_dim = 'scenario' in data.dims def reset(self) -> None: """ @@ -836,16 +594,18 @@ def selected_data(self) -> xr.DataArray: return self._stored_data.sel(**self._valid_selector) @property - def active_timesteps(self) -> pd.DatetimeIndex: - """Get the current active timesteps.""" + def active_timesteps(self) -> Optional[pd.DatetimeIndex]: + """Get the current active timesteps, or None if no time dimension.""" + if not self.has_time_dim: + return None if self._selected_timesteps is None: return self._stored_data.indexes['time'] return self._selected_timesteps @property def active_scenarios(self) -> Optional[pd.Index]: - """Get the current active scenarios.""" - if not self._has_scenarios: + """Get the current active scenarios, or None if no scenario dimension.""" + if not self.has_scenario_dim: return None if self._selected_scenarios is None: return self._stored_data.indexes['scenario'] @@ -865,8 +625,8 @@ def update_stored_data(self, value: xr.DataArray) -> None: """ new_data = DataConverter.as_dataarray( value, - timesteps=self.active_timesteps, - scenarios=self.active_scenarios if self._has_scenarios else None + timesteps=self.active_timesteps if self.has_time_dim else None, + scenarios=self.active_scenarios if self.has_scenario_dim else None, ) # Skip if data is unchanged to avoid overwriting backup @@ -883,15 +643,26 @@ def clear_selection(self, timesteps: bool = True, scenarios: bool = True) -> Non self._selected_scenarios = None def set_selection(self, timesteps: Optional[pd.DatetimeIndex] = None, scenarios: Optional[pd.Index] = None) -> None: - if timesteps is None: - self.clear_selection(timesteps=True, scenarios=False) - else: - self._selected_timesteps = timesteps + """ + Set active subset for timesteps and scenarios. - if scenarios is None: - self.clear_selection(timesteps=False, scenarios=True) - else: - self._selected_scenarios = scenarios + Args: + timesteps: Timesteps to activate, or None to clear. Ignored if series has no time dimension. + scenarios: Scenarios to activate, or None to clear. Ignored if series has no scenario dimension. + """ + # Only update timesteps if the series has time dimension + if self.has_time_dim: + if timesteps is None: + self.clear_selection(timesteps=True, scenarios=False) + else: + self._selected_timesteps = timesteps + + # Only update scenarios if the series has scenario dimension + if self.has_scenario_dim: + if scenarios is None: + self.clear_selection(timesteps=False, scenarios=True) + else: + self._selected_scenarios = scenarios @property def sel(self): @@ -906,8 +677,17 @@ def isel(self): @property def _valid_selector(self) -> Dict[str, pd.Index]: """Get the current selection as a dictionary.""" - full_selection = {'time': self._selected_timesteps, 'scenario': self._selected_scenarios} - return {dim: sel for dim, sel in full_selection.items() if dim in self._stored_data.dims and sel is not None} + selector = {} + + # Only include time in selector if series has time dimension + if self.has_time_dim and self._selected_timesteps is not None: + selector['time'] = self._selected_timesteps + + # Only include scenario in selector if series has scenario dimension + if self.has_scenario_dim and self._selected_scenarios is not None: + selector['scenario'] = self._selected_scenarios + + return selector def _apply_operation(self, other, op): """Apply an operation between this TimeSeries and another object.""" @@ -1042,6 +822,8 @@ def add_time_series( self, name: str, data: Union[NumericDataTS, TimeSeries], + has_time_dim: bool = True, + has_scenario_dim: bool = True, aggregation_weight: Optional[float] = None, aggregation_group: Optional[str] = None, has_extra_timestep: bool = False, @@ -1052,6 +834,8 @@ def add_time_series( Args: name: Name of the time series data: Data for the time series (can be raw data or an existing TimeSeries) + has_time_dim: Whether the TimeSeries has a time dimension + has_scenario_dim: Whether the TimeSeries has a scenario dimension aggregation_weight: Weight used for aggregation aggregation_group: Group name for shared aggregation weighting has_extra_timestep: Whether this series needs an extra timestep @@ -1061,9 +845,16 @@ def add_time_series( """ if name in self._time_series: raise KeyError(f"TimeSeries '{name}' already exists in allocator") + if not has_time_dim and has_extra_timestep: + raise ValueError('A not time-indexed TimeSeries cannot have an extra timestep') # Choose which timesteps to use - target_timesteps = self.timesteps_extra if has_extra_timestep else self.timesteps + if has_time_dim: + target_timesteps = self.timesteps_extra if has_extra_timestep else self.timesteps + else: + target_timesteps = None + + target_scenarios = self.scenarios if has_scenario_dim else None # Create or adapt the TimeSeries object if isinstance(data, TimeSeries): @@ -1071,7 +862,7 @@ def add_time_series( time_series = data # Update the stored data to use our timesteps and scenarios data_array = DataConverter.as_dataarray( - time_series.stored_data, timesteps=target_timesteps, scenarios=self.scenarios + time_series.stored_data, timesteps=target_timesteps, scenarios=target_scenarios ) time_series = TimeSeries( data=data_array, @@ -1086,7 +877,7 @@ def add_time_series( data=data, name=name, timesteps=target_timesteps, - scenarios=self.scenarios, + scenarios=target_scenarios, aggregation_weight=aggregation_weight, aggregation_group=aggregation_group, has_extra_timestep=has_extra_timestep, @@ -1163,7 +954,7 @@ def as_dataset(self, with_extra_timestep: bool = True, with_constants: bool = Tr Args: with_extra_timestep: Whether to exclude the extra timesteps. - Effectively, this removes the last timestep for certain TImeSeries, but mitigates the presence of NANs in others. + Effectively, this removes the last timestep for certain TimeSeries, but mitigates the presence of NANs in others. with_constants: Whether to exclude TimeSeries with a constant value from the dataset. """ if self.scenarios is None: @@ -1212,10 +1003,16 @@ def scenarios(self) -> Optional[pd.Index]: def _propagate_selection_to_time_series(self) -> None: """Apply the current selection to all TimeSeries objects.""" for ts_name, ts in self._time_series.items(): - timesteps = self._selected_timesteps_extra if ts_name in self._has_extra_timestep else self._selected_timesteps + if ts.has_time_dim: + timesteps = ( + self.timesteps_extra if ts_name in self._has_extra_timestep else self.timesteps + ) + else: + timesteps = None + ts.set_selection( timesteps=timesteps, - scenarios=self._selected_scenarios + scenarios=self.scenarios if ts.has_scenario_dim else None ) def __getitem__(self, name: str) -> TimeSeries: @@ -1245,7 +1042,7 @@ def __iter__(self) -> Iterator[TimeSeries]: """Iterate over TimeSeries objects.""" return iter(self._time_series.values()) - def update_time_series(self, name: str, data: NumericData) -> TimeSeries: + def update_time_series(self, name: str, data: TimestepData) -> TimeSeries: """ Update an existing TimeSeries with new data. @@ -1265,11 +1062,17 @@ def update_time_series(self, name: str, data: NumericData) -> TimeSeries: # Get the TimeSeries ts = self._time_series[name] + # Determine which timesteps to use if the series has a time dimension + if ts.has_time_dim: + target_timesteps = self.timesteps_extra if name in self._has_extra_timestep else self.timesteps + else: + target_timesteps = None + # Convert data to proper format data_array = DataConverter.as_dataarray( data, - self.timesteps_extra if name in self._has_extra_timestep else self.timesteps, - self.scenarios + timesteps=target_timesteps, + scenarios=self.scenarios if ts.has_scenario_dim else None ) # Update the TimeSeries diff --git a/flixopt/effects.py b/flixopt/effects.py index 9b5ea41d6..e834e339e 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -13,7 +13,7 @@ import numpy as np import pandas as pd -from .core import NumericData, NumericDataTS, Scalar, TimeSeries, TimeSeriesCollection +from .core import TimestepData, NumericDataTS, Scalar, TimeSeries, TimeSeriesCollection, ScenarioData, TimestepData from .features import ShareAllocationModel from .structure import Element, ElementModel, Interface, Model, SystemModel, register_class_for_io @@ -38,16 +38,16 @@ def __init__( meta_data: Optional[Dict] = None, is_standard: bool = False, is_objective: bool = False, - specific_share_to_other_effects_operation: Optional['EffectValuesUser'] = None, - specific_share_to_other_effects_invest: Optional['EffectValuesUser'] = None, - minimum_operation: Optional[Scalar] = None, - maximum_operation: Optional[Scalar] = None, - minimum_invest: Optional[Scalar] = None, - maximum_invest: Optional[Scalar] = None, + specific_share_to_other_effects_operation: Optional['EffectValuesUserTimestep'] = None, + specific_share_to_other_effects_invest: Optional['EffectValuesUserScenario'] = None, + minimum_operation: Optional[ScenarioData] = None, + maximum_operation: Optional[ScenarioData] = None, + minimum_invest: Optional[ScenarioData] = None, + maximum_invest: Optional[ScenarioData] = None, minimum_operation_per_hour: Optional[NumericDataTS] = None, maximum_operation_per_hour: Optional[NumericDataTS] = None, - minimum_total: Optional[Scalar] = None, - maximum_total: Optional[Scalar] = None, + minimum_total: Optional[ScenarioData] = None, + maximum_total: Optional[ScenarioData] = None, ): """ Args: @@ -76,10 +76,10 @@ def __init__( self.description = description self.is_standard = is_standard self.is_objective = is_objective - self.specific_share_to_other_effects_operation: EffectValuesUser = ( + self.specific_share_to_other_effects_operation: EffectValuesUserTimestep = ( specific_share_to_other_effects_operation or {} ) - self.specific_share_to_other_effects_invest: EffectValuesUser = specific_share_to_other_effects_invest or {} + self.specific_share_to_other_effects_invest: EffectValuesUserTimestep = specific_share_to_other_effects_invest or {} self.minimum_operation = minimum_operation self.maximum_operation = maximum_operation self.minimum_operation_per_hour: NumericDataTS = minimum_operation_per_hour @@ -171,11 +171,12 @@ def do_modeling(self): EffectValuesExpr = Dict[str, linopy.LinearExpression] # Used to create Shares EffectTimeSeries = Dict[str, TimeSeries] # Used internally to index values EffectValuesDict = Dict[str, NumericDataTS] # How effect values are stored -EffectValuesUser = Union[NumericDataTS, Dict[str, NumericDataTS]] # User-specified Shares to Effects -""" This datatype is used to define the share to an effect by a certain attribute. """ -EffectValuesUserScalar = Union[Scalar, Dict[str, Scalar]] # User-specified Shares to Effects -""" This datatype is used to define the share to an effect by a certain attribute. Only scalars are allowed. """ +EffectValuesUserScenario = Union[ScenarioData, Dict[str, ScenarioData]] +""" This datatype is used to define the share to an effect for every scenario. """ + +EffectValuesUserTimestep = Union[TimestepData, Dict[str, TimestepData]] +""" This datatype is used to define the share to an effect for every timestep. """ class EffectCollection: @@ -207,7 +208,10 @@ def add_effects(self, *effects: Effect) -> None: self._effects[effect.label] = effect logger.info(f'Registered new Effect: {effect.label}') - def create_effect_values_dict(self, effect_values_user: EffectValuesUser) -> Optional[EffectValuesDict]: + def create_effect_values_dict( + self, + effect_values_user: Union[EffectValuesUserScenario, EffectValuesUserTimestep] + ) -> Optional[EffectValuesDict]: """ Converts effect values into a dictionary. If a scalar is provided, it is associated with a default effect type. diff --git a/flixopt/elements.py b/flixopt/elements.py index 95536b910..b6de8c7c2 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -10,8 +10,8 @@ import numpy as np from .config import CONFIG -from .core import NumericData, NumericDataTS, PlausibilityError, Scalar, TimeSeriesCollection -from .effects import EffectValuesUser +from .core import TimestepData, NumericDataTS, PlausibilityError, Scalar, ScenarioData +from .effects import EffectValuesUserTimestep from .features import InvestmentModel, OnOffModel, PreventSimultaneousUsageModel from .interface import InvestParameters, OnOffParameters from .structure import Element, ElementModel, SystemModel, register_class_for_io @@ -148,16 +148,16 @@ def __init__( label: str, bus: str, size: Union[Scalar, InvestParameters] = None, - fixed_relative_profile: Optional[NumericDataTS] = None, - relative_minimum: NumericDataTS = 0, - relative_maximum: NumericDataTS = 1, - effects_per_flow_hour: Optional[EffectValuesUser] = None, + fixed_relative_profile: Optional[TimestepData] = None, + relative_minimum: TimestepData = 0, + relative_maximum: TimestepData = 1, + effects_per_flow_hour: Optional[EffectValuesUserTimestep] = None, on_off_parameters: Optional[OnOffParameters] = None, - flow_hours_total_max: Optional[Scalar] = None, - flow_hours_total_min: Optional[Scalar] = None, - load_factor_min: Optional[Scalar] = None, - load_factor_max: Optional[Scalar] = None, - previous_flow_rate: Optional[NumericData] = None, + flow_hours_total_max: Optional[ScenarioData] = None, + flow_hours_total_min: Optional[ScenarioData] = None, + load_factor_min: Optional[ScenarioData] = None, + load_factor_max: Optional[ScenarioData] = None, + previous_flow_rate: Optional[ScenarioData] = None, meta_data: Optional[Dict] = None, ): r""" @@ -240,10 +240,23 @@ def transform_data(self, flow_system: 'FlowSystem'): self.effects_per_flow_hour = flow_system.create_effect_time_series( self.label_full, self.effects_per_flow_hour, 'per_flow_hour' ) + self.flow_hours_total_max = flow_system.create_time_series( + f'{self.label_full}|flow_hours_total_max', self.flow_hours_total_max, has_time_dim=False + ) + self.flow_hours_total_min = flow_system.create_time_series( + f'{self.label_full}|flow_hours_total_min', self.flow_hours_total_min, has_time_dim=False + ) + self.load_factor_max = flow_system.create_time_series( + f'{self.label_full}|load_factor_max', self.load_factor_max, has_time_dim=False + ) + self.load_factor_min = flow_system.create_time_series( + f'{self.label_full}|load_factor_min', self.load_factor_min, has_time_dim=False + ) + if self.on_off_parameters is not None: self.on_off_parameters.transform_data(flow_system, self.label_full) if isinstance(self.size, InvestParameters): - self.size.transform_data(flow_system) + self.size.transform_data(flow_system, self.label_full) def infos(self, use_numpy: bool = True, use_element_label: bool = False) -> Dict: infos = super().infos(use_numpy, use_element_label) @@ -308,7 +321,7 @@ def do_modeling(self): self._model.add_variables( lower=self.absolute_flow_rate_bounds[0] if self.element.on_off_parameters is None else 0, upper=self.absolute_flow_rate_bounds[1], - coords=self._model.coords, + coords=self._model.get_coords(), name=f'{self.label_full}|flow_rate', ), 'flow_rate', @@ -414,7 +427,7 @@ def _create_bounds_for_load_factor(self): ) @property - def absolute_flow_rate_bounds(self) -> Tuple[NumericData, NumericData]: + def absolute_flow_rate_bounds(self) -> Tuple[TimestepData, TimestepData]: """Returns absolute flow rate bounds. Important for OnOffModel""" relative_minimum, relative_maximum = self.relative_flow_rate_bounds size = self.element.size @@ -425,7 +438,7 @@ def absolute_flow_rate_bounds(self) -> Tuple[NumericData, NumericData]: return relative_minimum * size.minimum_size, relative_maximum * size.maximum_size @property - def relative_flow_rate_bounds(self) -> Tuple[NumericData, NumericData]: + def relative_flow_rate_bounds(self) -> Tuple[TimestepData, TimestepData]: """Returns relative flow rate bounds.""" fixed_profile = self.element.fixed_relative_profile if fixed_profile is None: @@ -454,11 +467,11 @@ def do_modeling(self) -> None: self._model.hours_per_step, self.element.excess_penalty_per_flow_hour.selected_data ) self.excess_input = self.add( - self._model.add_variables(lower=0, coords=self._model.coords, name=f'{self.label_full}|excess_input'), + self._model.add_variables(lower=0, coords=self._model.get_coords(), name=f'{self.label_full}|excess_input'), 'excess_input', ) self.excess_output = self.add( - self._model.add_variables(lower=0, coords=self._model.coords, name=f'{self.label_full}|excess_output'), + self._model.add_variables(lower=0, coords=self._model.get_coords(), name=f'{self.label_full}|excess_output'), 'excess_output', ) eq_bus_balance.lhs -= -self.excess_input + self.excess_output diff --git a/flixopt/features.py b/flixopt/features.py index 32c382486..7b92396fd 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -11,7 +11,7 @@ from . import utils from .config import CONFIG -from .core import NumericData, Scalar, TimeSeries +from .core import TimestepData, Scalar, TimeSeries from .interface import InvestParameters, OnOffParameters, Piece, Piecewise, PiecewiseConversion, PiecewiseEffects from .structure import Model, SystemModel @@ -27,7 +27,7 @@ def __init__( label_of_element: str, parameters: InvestParameters, defining_variable: [linopy.Variable], - relative_bounds_of_defining_variable: Tuple[NumericData, NumericData], + relative_bounds_of_defining_variable: Tuple[TimestepData, TimestepData], label: Optional[str] = None, on_variable: Optional[linopy.Variable] = None, ): @@ -205,8 +205,8 @@ def __init__( on_off_parameters: OnOffParameters, label_of_element: str, defining_variables: List[linopy.Variable], - defining_bounds: List[Tuple[NumericData, NumericData]], - previous_values: List[Optional[NumericData]], + defining_bounds: List[Tuple[TimestepData, TimestepData]], + previous_values: List[Optional[TimestepData]], label: Optional[str] = None, ): """ @@ -246,7 +246,7 @@ def do_modeling(self): self._model.add_variables( name=f'{self.label_full}|on', binary=True, - coords=self._model.coords, + coords=self._model.get_coords(), ), 'on', ) @@ -275,7 +275,7 @@ def do_modeling(self): self._model.add_variables( name=f'{self.label_full}|off', binary=True, - coords=self._model.coords, + coords=self._model.get_coords(), ), 'off', ) @@ -303,12 +303,12 @@ def do_modeling(self): if self.parameters.use_switch_on: self.switch_on = self.add( - self._model.add_variables(binary=True, name=f'{self.label_full}|switch_on', coords=self._model.coords), + self._model.add_variables(binary=True, name=f'{self.label_full}|switch_on', coords=self._model.get_coords()), 'switch_on', ) self.switch_off = self.add( - self._model.add_variables(binary=True, name=f'{self.label_full}|switch_off', coords=self._model.coords), + self._model.add_variables(binary=True, name=f'{self.label_full}|switch_off', coords=self._model.get_coords()), 'switch_off', ) @@ -451,7 +451,7 @@ def _get_duration_in_hours( self._model.add_variables( lower=0, upper=maximum_duration.selected_data if maximum_duration is not None else mega, - coords=self._model.coords, + coords=self._model.get_coords(), name=f'{self.label_full}|{variable_name}', ), variable_name, @@ -623,7 +623,7 @@ def previous_consecutive_off_hours(self) -> Scalar: return self.compute_consecutive_duration(self.previous_off_values, self._model.hours_per_step) @staticmethod - def compute_previous_on_states(previous_values: List[Optional[NumericData]], epsilon: float = 1e-5) -> np.ndarray: + def compute_previous_on_states(previous_values: List[Optional[TimestepData]], epsilon: float = 1e-5) -> np.ndarray: """ Computes the previous 'on' states {0, 1} of defining variables as a binary array from their previous values. @@ -647,7 +647,7 @@ def compute_previous_on_states(previous_values: List[Optional[NumericData]], eps @staticmethod def compute_consecutive_duration( - binary_values: NumericData, hours_per_timestep: Union[int, float, np.ndarray] + binary_values: TimestepData, hours_per_timestep: Union[int, float, np.ndarray] ) -> Scalar: """ Computes the final consecutive duration in State 'on' (=1) in hours, from a binary. @@ -716,7 +716,7 @@ def do_modeling(self): self._model.add_variables( binary=True, name=f'{self.label_full}|inside_piece', - coords=self._model.coords if self._as_time_series else None, + coords=self._model.get_coords(time_dim=self._as_time_series), ), 'inside_piece', ) @@ -726,7 +726,7 @@ def do_modeling(self): lower=0, upper=1, name=f'{self.label_full}|lambda0', - coords=self._model.coords if self._as_time_series else None, + coords=self._model.get_coords(time_dim=self._as_time_series), ), 'lambda0', ) @@ -736,7 +736,7 @@ def do_modeling(self): lower=0, upper=1, name=f'{self.label_full}|lambda1', - coords=self._model.coords if self._as_time_series else None, + coords=self._model.get_coords(time_dim=self._as_time_series), ), 'lambda1', ) @@ -820,7 +820,7 @@ def do_modeling(self): elif self._zero_point is True: self.zero_point = self.add( self._model.add_variables( - coords=self._model.coords, binary=True, name=f'{self.label_full}|zero_point' + coords=self._model.get_coords(), binary=True, name=f'{self.label_full}|zero_point' ), 'zero_point', ) @@ -847,8 +847,8 @@ def __init__( label_full: Optional[str] = None, total_max: Optional[Scalar] = None, total_min: Optional[Scalar] = None, - max_per_hour: Optional[NumericData] = None, - min_per_hour: Optional[NumericData] = None, + max_per_hour: Optional[TimestepData] = None, + min_per_hour: Optional[TimestepData] = None, ): super().__init__(model, label_of_element=label_of_element, label=label, label_full=label_full) if not shares_are_time_series: # If the condition is True @@ -891,7 +891,7 @@ def do_modeling(self): upper=np.inf if (self._max_per_hour is None) else np.multiply(self._max_per_hour, self._model.hours_per_step), - coords=self._model.coords, + coords=self._model.get_coords(), name=f'{self.label_full}|total_per_timestep', ), 'total_per_timestep', @@ -929,7 +929,7 @@ def add_share( if isinstance(expression, linopy.LinearExpression) and expression.ndim == 0 or not isinstance(expression, linopy.LinearExpression) - else self._model.coords, + else self._model.get_coords(), #TODO: Add logic on what coords to use name=f'{name}->{self.label_full}', ), name, diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index e39d71e94..a4705371c 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -16,10 +16,10 @@ from rich.pretty import Pretty from . import io as fx_io -from .core import NumericData, NumericDataTS, TimeSeries, TimeSeriesCollection, TimeSeriesData -from .effects import Effect, EffectCollection, EffectTimeSeries, EffectValuesDict, EffectValuesUser +from .core import TimestepData, TimeSeries, TimeSeriesCollection, TimeSeriesData, Scalar +from .effects import Effect, EffectCollection, EffectTimeSeries, EffectValuesDict, EffectValuesUserScenario, EffectValuesUserTimestep from .elements import Bus, Component, Flow -from .structure import CLASS_REGISTRY, Element, SystemModel, get_compact_representation, get_str_representation +from .structure import CLASS_REGISTRY, Element, SystemModel if TYPE_CHECKING: import pyvis @@ -277,44 +277,71 @@ def transform_data(self): def create_time_series( self, name: str, - data: Optional[Union[NumericData, TimeSeriesData, TimeSeries]], + data: Optional[Union[TimestepData, TimeSeriesData, TimeSeries]], + has_time_dim: bool = True, + has_scenario_dim: bool = True, has_extra_timestep: bool = False, - ) -> Optional[TimeSeries]: + ) -> Optional[Union[Scalar, TimeSeries]]: """ Tries to create a TimeSeries from NumericData Data and adds it to the time_series_collection If the data already is a TimeSeries, nothing happens and the TimeSeries gets reset and returned If the data is a TimeSeriesData, it is converted to a TimeSeries, and the aggregation weights are applied. If the data is None, nothing happens. + + Args: + name: The name of the TimeSeries + data: The data to create a TimeSeries from + has_time_dim: Whether the data has a time dimension + has_scenario_dim: Whether the data has a scenario dimension + has_extra_timestep: Whether the data has an extra timestep """ + if not has_time_dim and not has_scenario_dim: + raise ValueError("At least one of the dimensions must be present") if data is None: return None - elif isinstance(data, TimeSeries): + + if not has_time_dim and self.time_series_collection.scenarios is None: + return data + + if isinstance(data, TimeSeries): data.restore_data() if data in self.time_series_collection: return data return self.time_series_collection.add_time_series( - data=data.selected_data, name=name, has_extra_timestep=has_extra_timestep + data=data.selected_data, + name=name, + has_time_dim=has_time_dim, + has_scenario_dim=has_scenario_dim, + has_extra_timestep=has_extra_timestep, ) elif isinstance(data, TimeSeriesData): data.label = name return self.time_series_collection.add_time_series( data=data.data, name=name, + has_time_dim=has_time_dim, + has_scenario_dim=has_scenario_dim, has_extra_timestep=has_extra_timestep, aggregation_weight=data.agg_weight, aggregation_group=data.agg_group ) return self.time_series_collection.add_time_series( - data=data, name=name, has_extra_timestep=has_extra_timestep + data=data, + name=name, + has_time_dim=has_time_dim, + has_scenario_dim=has_scenario_dim, + has_extra_timestep=has_extra_timestep, ) def create_effect_time_series( self, label_prefix: Optional[str], - effect_values: EffectValuesUser, + effect_values: Union[EffectValuesUserScenario, EffectValuesUserTimestep], label_suffix: Optional[str] = None, - ) -> Optional[EffectTimeSeries]: + has_time_dim: bool = True, + has_scenario_dim: bool = True, + ) -> Optional[Union[EffectTimeSeries, EffectValuesDict]]: """ Transform EffectValues to EffectTimeSeries. Creates a TimeSeries for each key in the nested_values dictionary, using the value as the data. @@ -322,13 +349,31 @@ def create_effect_time_series( The resulting label of the TimeSeries is the label of the parent_element, followed by the label of the Effect in the nested_values and the label_suffix. If the key in the EffectValues is None, the alias 'Standard_Effect' is used + + Args: + label_prefix: Prefix for the TimeSeries name + effect_values: Dictionary of EffectValues + label_suffix: Suffix for the TimeSeries name + has_time_dim: Whether the data has a time dimension + has_scenario_dim: Whether the data has a scenario dimension """ + if not has_time_dim and not has_scenario_dim: + raise ValueError("At least one of the dimensions must be present") + effect_values: Optional[EffectValuesDict] = self.effects.create_effect_values_dict(effect_values) if effect_values is None: return None + if not has_time_dim and self.time_series_collection.scenarios is None: + return effect_values + return { - effect: self.create_time_series('|'.join(filter(None, [label_prefix, effect, label_suffix])), value) + effect: self.create_time_series( + name='|'.join(filter(None, [label_prefix, effect, label_suffix])), + data=value, + has_time_dim=has_time_dim, + has_scenario_dim=has_scenario_dim, + ) for effect, value in effect_values.items() } diff --git a/flixopt/interface.py b/flixopt/interface.py index f9dbeb518..f57362ee3 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -7,11 +7,11 @@ from typing import TYPE_CHECKING, Dict, Iterator, List, Optional, Union from .config import CONFIG -from .core import NumericData, NumericDataTS, Scalar +from .core import TimestepData, NumericDataTS, Scalar, ScenarioData from .structure import Interface, register_class_for_io if TYPE_CHECKING: # for type checking and preventing circular imports - from .effects import EffectValuesUser, EffectValuesUserScalar + from .effects import EffectValuesUserScenario, EffectValuesUserTimestep from .flow_system import FlowSystem @@ -20,7 +20,7 @@ @register_class_for_io class Piece(Interface): - def __init__(self, start: NumericData, end: NumericData): + def __init__(self, start: TimestepData, end: TimestepData): """ Define a Piece, which is part of a Piecewise object. @@ -30,10 +30,21 @@ def __init__(self, start: NumericData, end: NumericData): """ self.start = start self.end = end + self.has_time_dim = False def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): - self.start = flow_system.create_time_series(f'{name_prefix}|start', self.start) - self.end = flow_system.create_time_series(f'{name_prefix}|end', self.end) + self.start = flow_system.create_time_series( + name=f'{name_prefix}|start', + data=self.start, + has_time_dim=self.has_time_dim, + has_scenario_dim=True + ) + self.end = flow_system.create_time_series( + name=f'{name_prefix}|end', + data=self.end, + has_time_dim=self.has_time_dim, + has_scenario_dim=True + ) @register_class_for_io @@ -46,6 +57,17 @@ def __init__(self, pieces: List[Piece]): pieces: The pieces of the piecewise. """ self.pieces = pieces + self._has_time_dim = False + + @property + def has_time_dim(self): + return self._has_time_dim + + @has_time_dim.setter + def has_time_dim(self, value): + self._has_time_dim = value + for piece in self.pieces: + piece.has_time_dim = value def __len__(self): return len(self.pieces) @@ -73,6 +95,18 @@ def __init__(self, piecewises: Dict[str, Piecewise]): piecewises: Dict of Piecewises defining the conversion factors. flow labels as keys, piecewise as values """ self.piecewises = piecewises + self._has_time_dim = True + self.has_time_dim = True # Inital propagation + + @property + def has_time_dim(self): + return self._has_time_dim + + @has_time_dim.setter + def has_time_dim(self, value): + self._has_time_dim = value + for piecewise in self.piecewises.values(): + piecewise.has_time_dim = value def items(self): return self.piecewises.items() @@ -94,12 +128,24 @@ def __init__(self, piecewise_origin: Piecewise, piecewise_shares: Dict[str, Piec """ self.piecewise_origin = piecewise_origin self.piecewise_shares = piecewise_shares + self._has_time_dim = False + self.has_time_dim = False # Inital propagation + + @property + def has_time_dim(self): + return self._has_time_dim + + @has_time_dim.setter + def has_time_dim(self, value): + self._has_time_dim = value + self.piecewise_origin.has_time_dim = value + for piecewise in self.piecewise_shares.values(): + piecewise.has_time_dim = value def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): - raise NotImplementedError('PiecewiseEffects is not yet implemented for non scalar shares') - # self.piecewise_origin.transform_data(flow_system, f'{name_prefix}|PiecewiseEffects|origin') - # for name, piecewise in self.piecewise_shares.items(): - # piecewise.transform_data(flow_system, f'{name_prefix}|PiecewiseEffects|{name}') + self.piecewise_origin.transform_data(flow_system, f'{name_prefix}|PiecewiseEffects|origin') + for effect, piecewise in self.piecewise_shares.items(): + piecewise.transform_data(flow_system, f'{name_prefix}|PiecewiseEffects|{effect}') @register_class_for_io @@ -110,14 +156,14 @@ class InvestParameters(Interface): def __init__( self, - fixed_size: Optional[Union[int, float]] = None, - minimum_size: Union[int, float] = 0, # TODO: Use EPSILON? - maximum_size: Optional[Union[int, float]] = None, + fixed_size: Optional[Scalar] = None, + minimum_size: Scalar = 0, # TODO: Use EPSILON? + maximum_size: Optional[Scalar] = None, optional: bool = True, # Investition ist weglassbar - fix_effects: Optional['EffectValuesUserScalar'] = None, - specific_effects: Optional['EffectValuesUserScalar'] = None, # costs per Flow-Unit/Storage-Size/... + fix_effects: Optional['EffectValuesUserScenario'] = None, + specific_effects: Optional['EffectValuesUserScenario'] = None, # costs per Flow-Unit/Storage-Size/... piecewise_effects: Optional[PiecewiseEffects] = None, - divest_effects: Optional['EffectValuesUserScalar'] = None, + divest_effects: Optional['EffectValuesUserScenario'] = None, ): """ Args: @@ -144,19 +190,40 @@ def __init__( minimum_size: Min nominal value (only if: size_is_fixed = False). maximum_size: Max nominal value (only if: size_is_fixed = False). """ - self.fix_effects: EffectValuesUser = fix_effects or {} - self.divest_effects: EffectValuesUser = divest_effects or {} + self.fix_effects: EffectValuesUserScenario = fix_effects or {} + self.divest_effects: EffectValuesUserScenario = divest_effects or {} self.fixed_size = fixed_size self.optional = optional - self.specific_effects: EffectValuesUser = specific_effects or {} + self.specific_effects: EffectValuesUserScenario = specific_effects or {} self.piecewise_effects = piecewise_effects self._minimum_size = minimum_size self._maximum_size = maximum_size or CONFIG.modeling.BIG # default maximum - def transform_data(self, flow_system: 'FlowSystem'): - self.fix_effects = flow_system.effects.create_effect_values_dict(self.fix_effects) - self.divest_effects = flow_system.effects.create_effect_values_dict(self.divest_effects) - self.specific_effects = flow_system.effects.create_effect_values_dict(self.specific_effects) + def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): + self.fix_effects = flow_system.create_effect_time_series( + label_prefix=name_prefix, + effect_values=self.fix_effects, + label_suffix='fix_effects', + has_time_dim=False, + has_scenario_dim=True, + ) + self.divest_effects = flow_system.create_effect_time_series( + label_prefix=name_prefix, + effect_values=self.divest_effects, + label_suffix='divest_effects', + has_time_dim=False, + has_scenario_dim=True, + ) + self.specific_effects = flow_system.create_effect_time_series( + label_prefix=name_prefix, + effect_values=self.specific_effects, + label_suffix='specific_effects', + has_time_dim=False, + has_scenario_dim=True, + ) + if self.piecewise_effects is not None: + self.piecewise_effects.has_time_dim=False + self.piecewise_effects.transform_data(flow_system, f'{name_prefix}|PiecewiseEffects') @property def minimum_size(self): @@ -171,15 +238,15 @@ def maximum_size(self): class OnOffParameters(Interface): def __init__( self, - effects_per_switch_on: Optional['EffectValuesUser'] = None, - effects_per_running_hour: Optional['EffectValuesUser'] = None, - on_hours_total_min: Optional[int] = None, - on_hours_total_max: Optional[int] = None, - consecutive_on_hours_min: Optional[NumericData] = None, - consecutive_on_hours_max: Optional[NumericData] = None, - consecutive_off_hours_min: Optional[NumericData] = None, - consecutive_off_hours_max: Optional[NumericData] = None, - switch_on_total_max: Optional[int] = None, + effects_per_switch_on: Optional['EffectValuesUserTimestep'] = None, + effects_per_running_hour: Optional['EffectValuesUserTimestep'] = None, + on_hours_total_min: Optional[ScenarioData] = None, + on_hours_total_max: Optional[ScenarioData] = None, + consecutive_on_hours_min: Optional[TimestepData] = None, + consecutive_on_hours_max: Optional[TimestepData] = None, + consecutive_off_hours_min: Optional[TimestepData] = None, + consecutive_off_hours_max: Optional[TimestepData] = None, + switch_on_total_max: Optional[ScenarioData] = None, force_switch_on: bool = False, ): """ @@ -202,8 +269,8 @@ def __init__( switch_on_total_max: max nr of switchOn operations force_switch_on: force creation of switch on variable, even if there is no switch_on_total_max """ - self.effects_per_switch_on: EffectValuesUser = effects_per_switch_on or {} - self.effects_per_running_hour: EffectValuesUser = effects_per_running_hour or {} + self.effects_per_switch_on: EffectValuesUserTimestep = effects_per_switch_on or {} + self.effects_per_running_hour: EffectValuesUserTimestep = effects_per_running_hour or {} self.on_hours_total_min: Scalar = on_hours_total_min self.on_hours_total_max: Scalar = on_hours_total_max self.consecutive_on_hours_min: NumericDataTS = consecutive_on_hours_min @@ -232,6 +299,15 @@ def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): self.consecutive_off_hours_max = flow_system.create_time_series( f'{name_prefix}|consecutive_off_hours_max', self.consecutive_off_hours_max ) + self.on_hours_total_max = flow_system.create_time_series( + f'{name_prefix}|on_hours_total_max', self.on_hours_total_max, has_time_dim=False + ) + self.on_hours_total_min = flow_system.create_time_series( + f'{name_prefix}|on_hours_total_min', self.on_hours_total_min, has_time_dim=False + ) + self.switch_on_total_max = flow_system.create_time_series( + f'{name_prefix}|switch_on_total_max', self.switch_on_total_max, has_time_dim=False + ) @property def use_off(self) -> bool: diff --git a/flixopt/io.py b/flixopt/io.py index 5cc353836..adaf52f55 100644 --- a/flixopt/io.py +++ b/flixopt/io.py @@ -23,7 +23,7 @@ def replace_timeseries(obj, mode: Literal['name', 'stats', 'data'] = 'name'): return [replace_timeseries(v, mode) for v in obj] elif isinstance(obj, TimeSeries): # Adjust this based on the actual class if obj.all_equal: - return obj.selected_data.values[0].item() + return obj.selected_data.values.max().item() elif mode == 'name': return f'::::{obj.name}' elif mode == 'stats': diff --git a/flixopt/structure.py b/flixopt/structure.py index 2e136c652..7306c97d5 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -19,7 +19,7 @@ from rich.pretty import Pretty from .config import CONFIG -from .core import NumericData, Scalar, TimeSeries, TimeSeriesCollection, TimeSeriesData +from .core import TimestepData, Scalar, TimeSeries, TimeSeriesCollection, TimeSeriesData if TYPE_CHECKING: # for type checking and preventing circular imports from .effects import EffectCollectionModel @@ -98,13 +98,32 @@ def hours_per_step(self): def hours_of_previous_timesteps(self): return self.time_series_collection.hours_of_previous_timesteps - @property - def coords(self) -> Tuple[pd.DatetimeIndex]: - return (self.time_series_collection.timesteps,) + def get_coords( + self, + scenario_dim = True, + time_dim = True, + extra_timestep = False + ) -> Optional[Union[Tuple[pd.Index], Tuple[pd.Index, pd.Index]]]: + """ + Returns the coordinates of the model - @property - def coords_extra(self) -> Tuple[pd.DatetimeIndex]: - return (self.time_series_collection.timesteps_extra,) + Args: + scenario_dim: If True, the scenario dimension is included in the coordinates + time_dim: If True, the time dimension is included in the coordinates + extra_timestep: If True, the extra timesteps are used instead of the regular timesteps + + Returns: + The coordinates of the model. Might also be None if no scenarios are present and time_dim is False + """ + scenarios = self.time_series_collection.scenarios + timesteps = self.time_series_collection.timesteps if not extra_timestep else self.time_series_collection.timesteps_extra + if scenarios is None: + if time_dim: + return (timesteps,) + return None + if scenario_dim and not time_dim: + return (scenarios,) + return scenarios, timesteps class Interface: diff --git a/flixopt/utils.py b/flixopt/utils.py index bb6e8ec40..6b5d88693 100644 --- a/flixopt/utils.py +++ b/flixopt/utils.py @@ -11,14 +11,6 @@ logger = logging.getLogger('flixopt') -def is_number(number_alias: Union[int, float, str]): - """Returns True is string is a number.""" - try: - float(number_alias) - return True - except ValueError: - return False - def round_floats(obj, decimals=2): if isinstance(obj, dict): diff --git a/site/release-notes/_template.txt b/site/release-notes/_template.txt deleted file mode 100644 index fe85a0554..000000000 --- a/site/release-notes/_template.txt +++ /dev/null @@ -1,32 +0,0 @@ -# Release v{version} - -**Release Date:** YYYY-MM-DD - -## What's New - -* Feature 1 - Description -* Feature 2 - Description - -## Improvements - -* Improvement 1 - Description -* Improvement 2 - Description - -## Bug Fixes - -* Fixed issue with X -* Resolved problem with Y - -## Breaking Changes - -* Change 1 - Migration instructions -* Change 2 - Migration instructions - -## Deprecations - -* Feature X will be removed in v{next_version} - -## Dependencies - -* Added dependency X v1.2.3 -* Updated dependency Y to v2.0.0 \ No newline at end of file diff --git a/tests/run_all_tests.py b/tests/run_all_tests.py index 5597a47f3..83b6dfacf 100644 --- a/tests/run_all_tests.py +++ b/tests/run_all_tests.py @@ -7,4 +7,4 @@ import pytest if __name__ == '__main__': - pytest.main(['test_functional.py', '--disable-warnings']) + pytest.main(['test_integration.py', '--disable-warnings']) diff --git a/tests/test_dataconverter.py b/tests/test_dataconverter.py index 579de9c00..0466f3a2e 100644 --- a/tests/test_dataconverter.py +++ b/tests/test_dataconverter.py @@ -48,48 +48,6 @@ def test_scalar_conversion(self, sample_time_index): result = DataConverter.as_dataarray(np.float32(42.5), sample_time_index) assert np.all(result.values == 42.5) - def test_series_conversion(self, sample_time_index): - """Test converting a pandas Series.""" - # Test with integer values - series = pd.Series([1, 2, 3, 4, 5], index=sample_time_index) - result = DataConverter.as_dataarray(series, sample_time_index) - assert isinstance(result, xr.DataArray) - assert result.shape == (5,) - assert result.dims == ('time',) - assert np.array_equal(result.values, series.values) - - # Test with float values - series = pd.Series([1.1, 2.2, 3.3, 4.4, 5.5], index=sample_time_index) - result = DataConverter.as_dataarray(series, sample_time_index) - assert np.array_equal(result.values, series.values) - - # Test with mixed NA values - series = pd.Series([1, np.nan, 3, None, 5], index=sample_time_index) - result = DataConverter.as_dataarray(series, sample_time_index) - assert np.array_equal(np.isnan(result.values), np.isnan(series.values)) - assert np.array_equal(result.values[~np.isnan(result.values)], series.values[~np.isnan(series.values)]) - - def test_dataframe_conversion(self, sample_time_index): - """Test converting a pandas DataFrame.""" - # Test with a single-column DataFrame - df = pd.DataFrame({'A': [1, 2, 3, 4, 5]}, index=sample_time_index) - result = DataConverter.as_dataarray(df, sample_time_index) - assert isinstance(result, xr.DataArray) - assert result.shape == (5,) - assert result.dims == ('time',) - assert np.array_equal(result.values.flatten(), df['A'].values) - - # Test with float values - df = pd.DataFrame({'A': [1.1, 2.2, 3.3, 4.4, 5.5]}, index=sample_time_index) - result = DataConverter.as_dataarray(df, sample_time_index) - assert np.array_equal(result.values.flatten(), df['A'].values) - - # Test with NA values - df = pd.DataFrame({'A': [1, np.nan, 3, None, 5]}, index=sample_time_index) - result = DataConverter.as_dataarray(df, sample_time_index) - assert np.array_equal(np.isnan(result.values), np.isnan(df['A'].values)) - assert np.array_equal(result.values[~np.isnan(result.values)], df['A'].values[~np.isnan(df['A'].values)]) - def test_ndarray_conversion(self, sample_time_index): """Test converting a numpy ndarray.""" # Test with integer 1D array @@ -153,158 +111,6 @@ def test_scalar_with_scenarios(self, sample_time_index, sample_scenario_index): result = DataConverter.as_dataarray(42.5, sample_time_index, sample_scenario_index) assert np.all(result.values == 42.5) - def test_series_with_scenarios(self, sample_time_index, sample_scenario_index): - """Test converting Series with scenario dimension.""" - # Create time series data - series = pd.Series([1, 2, 3, 4, 5], index=sample_time_index) - - # Convert with scenario dimension - result = DataConverter.as_dataarray(series, sample_time_index, sample_scenario_index) - - assert result.shape == (len(sample_scenario_index), len(sample_time_index)) - assert result.dims == ('scenario', 'time') - - # Values should be broadcast to all scenarios - for scenario in sample_scenario_index: - scenario_slice = result.sel(scenario=scenario) - assert np.array_equal(scenario_slice.values, series.values) - - # Test with series containing NaN - series = pd.Series([1, np.nan, 3, np.nan, 5], index=sample_time_index) - result = DataConverter.as_dataarray(series, sample_time_index, sample_scenario_index) - - # Each scenario should have the same pattern of NaNs - for scenario in sample_scenario_index: - scenario_slice = result.sel(scenario=scenario) - assert np.array_equal(np.isnan(scenario_slice.values), np.isnan(series.values)) - assert np.array_equal( - scenario_slice.values[~np.isnan(scenario_slice.values)], series.values[~np.isnan(series.values)] - ) - - def test_multi_index_series(self, sample_time_index, sample_scenario_index, multi_index): - """Test converting a Series with MultiIndex (scenario, time).""" - # Create a MultiIndex Series with scenario-specific values - values = [ - # baseline scenario - 10, - 20, - 30, - 40, - 50, - # high_demand scenario - 15, - 25, - 35, - 45, - 55, - # low_price scenario - 5, - 15, - 25, - 35, - 45, - ] - series_multi = pd.Series(values, index=multi_index) - - # Convert the MultiIndex Series - result = DataConverter.as_dataarray(series_multi, sample_time_index, sample_scenario_index) - - assert result.shape == (len(sample_scenario_index), len(sample_time_index)) - assert result.dims == ('scenario', 'time') - - # Check values for each scenario - baseline_values = result.sel(scenario='baseline').values - assert np.array_equal(baseline_values, [10, 20, 30, 40, 50]) - - high_demand_values = result.sel(scenario='high_demand').values - assert np.array_equal(high_demand_values, [15, 25, 35, 45, 55]) - - low_price_values = result.sel(scenario='low_price').values - assert np.array_equal(low_price_values, [5, 15, 25, 35, 45]) - - # Test with some missing values in the MultiIndex - incomplete_index = multi_index[:-2] # Remove last two entries - incomplete_values = values[:-2] # Remove corresponding values - incomplete_series = pd.Series(incomplete_values, index=incomplete_index) - - result = DataConverter.as_dataarray(incomplete_series, sample_time_index, sample_scenario_index) - - # The last value of low_price scenario should be NaN - assert np.isnan(result.sel(scenario='low_price').values[-1]) - - def test_dataframe_with_scenarios(self, sample_time_index, sample_scenario_index): - """Test converting DataFrame with scenario dimension.""" - # Create a single-column DataFrame - df = pd.DataFrame({'A': [1, 2, 3, 4, 5]}, index=sample_time_index) - - # Convert with scenario dimension - result = DataConverter.as_dataarray(df, sample_time_index, sample_scenario_index) - - assert result.shape == (len(sample_scenario_index), len(sample_time_index)) - assert result.dims == ('scenario', 'time') - - # Values should be broadcast to all scenarios - for scenario in sample_scenario_index: - scenario_slice = result.sel(scenario=scenario) - assert np.array_equal(scenario_slice.values, df['A'].values) - - def test_multi_index_dataframe(self, sample_time_index, sample_scenario_index, multi_index): - """Test converting a DataFrame with MultiIndex (scenario, time).""" - # Create a MultiIndex DataFrame with scenario-specific values - values = [ - # baseline scenario - 10, - 20, - 30, - 40, - 50, - # high_demand scenario - 15, - 25, - 35, - 45, - 55, - # low_price scenario - 5, - 15, - 25, - 35, - 45, - ] - df_multi = pd.DataFrame({'A': values}, index=multi_index) - - # Convert the MultiIndex DataFrame - result = DataConverter.as_dataarray(df_multi, sample_time_index, sample_scenario_index) - - assert result.shape == (len(sample_scenario_index), len(sample_time_index)) - assert result.dims == ('scenario', 'time') - - # Check values for each scenario - baseline_values = result.sel(scenario='baseline').values - assert np.array_equal(baseline_values, [10, 20, 30, 40, 50]) - - high_demand_values = result.sel(scenario='high_demand').values - assert np.array_equal(high_demand_values, [15, 25, 35, 45, 55]) - - low_price_values = result.sel(scenario='low_price').values - assert np.array_equal(low_price_values, [5, 15, 25, 35, 45]) - - # Test with missing values - incomplete_index = multi_index[:-2] # Remove last two entries - incomplete_values = values[:-2] # Remove corresponding values - incomplete_df = pd.DataFrame({'A': incomplete_values}, index=incomplete_index) - - result = DataConverter.as_dataarray(incomplete_df, sample_time_index, sample_scenario_index) - - # The last value of low_price scenario should be NaN - assert np.isnan(result.sel(scenario='low_price').values[-1]) - - # Test with multiple columns (should raise error) - df_multi_col = pd.DataFrame({'A': values, 'B': [v * 2 for v in values]}, index=multi_index) - - with pytest.raises(ConversionError): - DataConverter.as_dataarray(df_multi_col, sample_time_index, sample_scenario_index) - def test_1d_array_with_scenarios(self, sample_time_index, sample_scenario_index): """Test converting 1D array with scenario dimension (broadcasting).""" # Create 1D array matching timesteps length @@ -391,12 +197,12 @@ def test_time_index_validation(self): # Test with empty index empty_index = pd.DatetimeIndex([], name='time') - with pytest.raises(ValueError): + with pytest.raises(ConversionError): DataConverter.as_dataarray(42, empty_index) # Test with non-DatetimeIndex wrong_type_index = pd.Index([1, 2, 3, 4, 5], name='time') - with pytest.raises(ValueError): + with pytest.raises(ConversionError): DataConverter.as_dataarray(42, wrong_type_index) def test_scenario_index_validation(self, sample_time_index): @@ -408,11 +214,11 @@ def test_scenario_index_validation(self, sample_time_index): # Test with empty scenario index empty_index = pd.Index([], name='scenario') - with pytest.raises(ValueError): + with pytest.raises(ConversionError): DataConverter.as_dataarray(42, sample_time_index, empty_index) # Test with non-Index scenario - with pytest.raises(ValueError): + with pytest.raises(ConversionError): DataConverter.as_dataarray(42, sample_time_index, ['baseline', 'high_demand']) def test_invalid_data_types(self, sample_time_index, sample_scenario_index): @@ -572,46 +378,9 @@ def test_all_nan_data(self, sample_time_index, sample_scenario_index): assert np.all(np.isnan(result.values)) # Series of all NaNs - all_nan_series = pd.Series([np.nan, np.nan, np.nan, np.nan, np.nan], index=sample_time_index) - result = DataConverter.as_dataarray(all_nan_series, sample_time_index, sample_scenario_index) + result = DataConverter.as_dataarray(np.array([np.nan, np.nan, np.nan, np.nan, np.nan]), sample_time_index, sample_scenario_index) assert np.all(np.isnan(result.values)) - def test_subset_index_multiindex(self, sample_time_index, sample_scenario_index): - """Test handling of MultiIndex Series/DataFrames with subset of expected indices.""" - # Create a subset of the expected indexes - subset_time = sample_time_index[1:4] # Middle subset - subset_scenarios = sample_scenario_index[0:2] # First two scenarios - - # Create MultiIndex with subset - subset_multi_index = pd.MultiIndex.from_product([subset_scenarios, subset_time], names=['scenario', 'time']) - - # Create Series with subset of data - values = [ - # baseline (3 values) - 20, - 30, - 40, - # high_demand (3 values) - 25, - 35, - 45, - ] - subset_series = pd.Series(values, index=subset_multi_index) - - # Convert and test - result = DataConverter.as_dataarray(subset_series, sample_time_index, sample_scenario_index) - - # Shape should be full size - assert result.shape == (len(sample_scenario_index), len(sample_time_index)) - - # Check values - present values should match - assert result.sel(scenario='baseline', time=subset_time[0]).item() == 20 - assert result.sel(scenario='high_demand', time=subset_time[1]).item() == 35 - - # Missing values should be NaN - assert np.isnan(result.sel(scenario='baseline', time=sample_time_index[0]).item()) - assert np.isnan(result.sel(scenario='low_price', time=sample_time_index[2]).item()) - def test_mixed_data_types(self, sample_time_index, sample_scenario_index): """Test conversion of mixed integer and float data.""" # Create array with mixed types @@ -632,77 +401,6 @@ def test_mixed_data_types(self, sample_time_index, sample_scenario_index): class TestFunctionalUseCase: """Tests for realistic use cases combining multiple features.""" - def test_multiindex_with_nans_and_partial_data(self, sample_time_index, sample_scenario_index): - """Test MultiIndex Series with partial data and NaN values.""" - # Create a MultiIndex Series with missing values and partial coverage - time_subset = sample_time_index[1:4] # Middle 3 timestamps only - - # Build index with holes - idx_tuples = [] - for scenario in sample_scenario_index: - for time in time_subset: - # Skip some combinations to create holes - if scenario == 'baseline' and time == time_subset[0]: - continue - if scenario == 'high_demand' and time == time_subset[2]: - continue - idx_tuples.append((scenario, time)) - - partial_idx = pd.MultiIndex.from_tuples(idx_tuples, names=['scenario', 'time']) - - # Create values with some NaNs - values = [ - # baseline (2 values, skipping first) - 30, - 40, - # high_demand (2 values, skipping last) - 25, - 35, - # low_price (3 values) - 15, - np.nan, - 35, - ] - - # Create Series - partial_series = pd.Series(values, index=partial_idx) - - # Convert and test - result = DataConverter.as_dataarray(partial_series, sample_time_index, sample_scenario_index) - - # Shape should be full size - assert result.shape == (len(sample_scenario_index), len(sample_time_index)) - - # Check specific values - assert result.sel(scenario='baseline', time=time_subset[1]).item() == 30 - assert result.sel(scenario='high_demand', time=time_subset[0]).item() == 25 - assert np.isnan(result.sel(scenario='low_price', time=time_subset[1]).item()) - - # All skipped combinations should be NaN - assert np.isnan(result.sel(scenario='baseline', time=time_subset[0]).item()) - assert np.isnan(result.sel(scenario='high_demand', time=time_subset[2]).item()) - - # First and last timestamps should all be NaN (not in original subset) - assert np.all(np.isnan(result.sel(time=sample_time_index[0]).values)) - assert np.all(np.isnan(result.sel(time=sample_time_index[-1]).values)) - - def test_scenario_broadcast_with_nan_values(self, sample_time_index, sample_scenario_index): - """Test broadcasting a Series with NaN values to scenarios.""" - # Create Series with some NaN values - series = pd.Series([1, np.nan, 3, np.nan, 5], index=sample_time_index) - - # Convert with scenario broadcasting - result = DataConverter.as_dataarray(series, sample_time_index, sample_scenario_index) - - # All scenarios should have the same pattern of NaN values - for scenario in sample_scenario_index: - scenario_data = result.sel(scenario=scenario) - assert np.isnan(scenario_data[1].item()) - assert np.isnan(scenario_data[3].item()) - assert scenario_data[0].item() == 1 - assert scenario_data[2].item() == 3 - assert scenario_data[4].item() == 5 - def test_large_dataset(self, sample_scenario_index): """Test with a larger dataset to ensure performance.""" # Create a larger timestep array (e.g., hourly for a year) @@ -814,34 +512,6 @@ def test_preserving_scenario_order(self, sample_time_index): assert np.array_equal(result.sel(scenario='scenario1').values, data[1]) assert np.array_equal(result.sel(scenario='scenario2').values, data[2]) - def test_multiindex_reindexing(self, sample_time_index): - """Test reindexing of MultiIndex Series.""" - # Create scenarios with intentional different order - scenarios = pd.Index(['z_scenario', 'a_scenario', 'm_scenario'], name='scenario') - - # Create MultiIndex with different order than the target - source_scenarios = pd.Index(['a_scenario', 'm_scenario', 'z_scenario'], name='scenario') - multi_idx = pd.MultiIndex.from_product([source_scenarios, sample_time_index], names=['scenario', 'time']) - - # Create values - order should match the source index - values = [] - for i, _ in enumerate(source_scenarios): - values.extend([i * 10 + j for j in range(1, len(sample_time_index) + 1)]) - - # Create Series - series = pd.Series(values, index=multi_idx) - - # Convert using the target scenario order - result = DataConverter.as_dataarray(series, sample_time_index, scenarios) - - # Verify scenario order matches the target - assert list(result.coords['scenario'].values) == list(scenarios) - - # Verify values are correctly indexed - assert np.array_equal(result.sel(scenario='a_scenario').values, [1, 2, 3, 4, 5]) - assert np.array_equal(result.sel(scenario='m_scenario').values, [11, 12, 13, 14, 15]) - assert np.array_equal(result.sel(scenario='z_scenario').values, [21, 22, 23, 24, 25]) - if __name__ == '__main__': pytest.main() @@ -879,12 +549,12 @@ def test_time_index_validation(): # Test with empty index empty_index = pd.DatetimeIndex([], name='time') - with pytest.raises(ValueError): + with pytest.raises(ConversionError): DataConverter.as_dataarray(42, empty_index) # Test with non-DatetimeIndex wrong_type_index = pd.Index([1, 2, 3, 4, 5], name='time') - with pytest.raises(ValueError): + with pytest.raises(ConversionError): DataConverter.as_dataarray(42, wrong_type_index) From 99057901f13bcdcf48c82b9962e73135c4445782 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 2 Apr 2025 14:33:36 +0200 Subject: [PATCH 003/448] Feature/scenarios effects (#216) * Update ShareAllocationModel * Update EffectModel * Update Objective * Improve float conversion in main results * Update dims in ShareAllocationModel * Update dims in ShareAllocationModel * Specify dimensions for ShareAllocationModel * Improve logic to get coords * Bugfix * Apply a scaling factor to the objective if ycenarios are used * Bugfix mein results: always as floats and list of floats * Add scenarios to calculation.py * Improve timestep indexing. Now adjust duration for each timestep accordingly to the new index * Improve validation of Indexes and Bugfix calculate_hours_per_timestep() * Add example # Changes: - change how main results are converted to floats - Improved logic to get coords for vars and constraints - Add scaling factor for the objective to have a better scaled model - Change tilmestep indexing to also update the hours_per_timestep accordingly - Added minimal example --- examples/04_Scenarios/scenario_example.py | 122 ++++++++++++++++++ flixopt/calculation.py | 36 +++--- flixopt/core.py | 143 ++++++++++++++++++---- flixopt/effects.py | 51 +++++--- flixopt/features.py | 40 +++--- flixopt/structure.py | 17 ++- flixopt/utils.py | 6 + 7 files changed, 336 insertions(+), 79 deletions(-) create mode 100644 examples/04_Scenarios/scenario_example.py diff --git a/examples/04_Scenarios/scenario_example.py b/examples/04_Scenarios/scenario_example.py new file mode 100644 index 000000000..d834ff5f0 --- /dev/null +++ b/examples/04_Scenarios/scenario_example.py @@ -0,0 +1,122 @@ +""" +This script shows how to use the flixopt framework to model a simple energy system. +""" + +import numpy as np +import pandas as pd +from rich.pretty import pprint # Used for pretty printing + +import flixopt as fx + +if __name__ == '__main__': + # --- Create Time Series Data --- + # Heat demand profile (e.g., kW) over time and corresponding power prices + heat_demand_per_h = np.array([[30, 0, 90, 110, 110, 20, 20, 20, 20], + [30, 0, 100, 118, 125, 20, 20, 20, 20]]) + power_prices = np.ones(9) * 0.08 + + # Create datetime array starting from '2020-01-01' for the given time period + timesteps = pd.date_range('2020-01-01', periods=9, freq='h') + scenarios = pd.Index(['Base Case', 'High Demand']) + flow_system = fx.FlowSystem(timesteps=timesteps, scenarios=scenarios) + + # --- Define Energy Buses --- + # These represent nodes, where the used medias are balanced (electricity, heat, and gas) + flow_system.add_elements(fx.Bus(label='Strom'), fx.Bus(label='Fernwärme'), fx.Bus(label='Gas')) + + # --- Define Effects (Objective and CO2 Emissions) --- + # Cost effect: used as the optimization objective --> minimizing costs + costs = fx.Effect( + label='costs', + unit='€', + description='Kosten', + is_standard=True, # standard effect: no explicit value needed for costs + is_objective=True, # Minimizing costs as the optimization objective + ) + + # CO2 emissions effect with an associated cost impact + CO2 = fx.Effect( + label='CO2', + unit='kg', + description='CO2_e-Emissionen', + specific_share_to_other_effects_operation={costs.label: 0.2}, + maximum_operation_per_hour=1000, # Max CO2 emissions per hour + ) + + # --- Define Flow System Components --- + # Boiler: Converts fuel (gas) into thermal energy (heat) + boiler = fx.linear_converters.Boiler( + label='Boiler', + eta=0.5, + Q_th=fx.Flow(label='Q_th', bus='Fernwärme', size=50, relative_minimum=0.1, relative_maximum=1), + Q_fu=fx.Flow(label='Q_fu', bus='Gas'), + ) + + # Combined Heat and Power (CHP): Generates both electricity and heat from fuel + chp = fx.linear_converters.CHP( + label='CHP', + eta_th=0.5, + eta_el=0.4, + P_el=fx.Flow('P_el', bus='Strom', size=60, relative_minimum=5 / 60), + Q_th=fx.Flow('Q_th', bus='Fernwärme'), + Q_fu=fx.Flow('Q_fu', bus='Gas'), + ) + + # Storage: Energy storage system with charging and discharging capabilities + storage = fx.Storage( + label='Storage', + charging=fx.Flow('Q_th_load', bus='Fernwärme', size=1000), + discharging=fx.Flow('Q_th_unload', bus='Fernwärme', size=1000), + capacity_in_flow_hours=fx.InvestParameters(fix_effects=20, fixed_size=30, optional=False), + initial_charge_state=0, # Initial storage state: empty + relative_maximum_charge_state=np.array([80, 70, 80, 80, 80, 80, 80, 80, 80, 80]) * 0.01, + eta_charge=0.9, + eta_discharge=1, # Efficiency factors for charging/discharging + relative_loss_per_hour=0.08, # 8% loss per hour. Absolute loss depends on current charge state + prevent_simultaneous_charge_and_discharge=True, # Prevent charging and discharging at the same time + ) + + # Heat Demand Sink: Represents a fixed heat demand profile + heat_sink = fx.Sink( + label='Heat Demand', + sink=fx.Flow(label='Q_th_Last', bus='Fernwärme', size=1, fixed_relative_profile=heat_demand_per_h), + ) + + # Gas Source: Gas tariff source with associated costs and CO2 emissions + gas_source = fx.Source( + label='Gastarif', + source=fx.Flow(label='Q_Gas', bus='Gas', size=1000, effects_per_flow_hour={costs.label: 0.04, CO2.label: 0.3}), + ) + + # Power Sink: Represents the export of electricity to the grid + power_sink = fx.Sink( + label='Einspeisung', sink=fx.Flow(label='P_el', bus='Strom', effects_per_flow_hour=-1 * power_prices) + ) + + # --- Build the Flow System --- + # Add all defined components and effects to the flow system + flow_system.add_elements(costs, CO2, boiler, storage, chp, heat_sink, gas_source, power_sink) + + # Visualize the flow system for validation purposes + flow_system.plot_network(show=True) + + # --- Define and Run Calculation --- + # Create a calculation object to model the Flow System + calculation = fx.FullCalculation(name='Sim1', flow_system=flow_system) + calculation.do_modeling() # Translate the model to a solvable form, creating equations and Variables + + # --- Solve the Calculation and Save Results --- + calculation.solve(fx.solvers.HighsSolver(mip_gap=0, time_limit_seconds=30)) + + # --- Analyze Results --- + calculation.results['Fernwärme'].plot_node_balance_pie() + calculation.results['Fernwärme'].plot_node_balance() + calculation.results['Storage'].plot_node_balance() + calculation.results.plot_heatmap('CHP(Q_th)|flow_rate') + + # Convert the results for the storage component to a dataframe and display + df = calculation.results['Storage'].node_balance_with_charge_state() + print(df) + + # Save results to file for later usage + calculation.results.to_file() diff --git a/flixopt/calculation.py b/flixopt/calculation.py index 03cf8b9a6..2dbb6af19 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -44,19 +44,22 @@ def __init__( name: str, flow_system: FlowSystem, active_timesteps: Optional[pd.DatetimeIndex] = None, + selected_scenarios: Optional[pd.Index] = None, folder: Optional[pathlib.Path] = None, ): """ Args: name: name of calculation flow_system: flow_system which should be calculated - active_timesteps: list with indices, which should be used for calculation. If None, then all timesteps are used. + active_timesteps: timesteps which should be used for calculation. If None, then all timesteps are used. + selected_scenarios: scenarios which should be used for calculation. If None, then all scenarios are used. folder: folder where results should be saved. If None, then the current working directory is used. """ self.name = name self.flow_system = flow_system self.model: Optional[SystemModel] = None self.active_timesteps = active_timesteps + self.selected_scenarios = selected_scenarios self.durations = {'modeling': 0.0, 'solving': 0.0, 'saving': 0.0} self.folder = pathlib.Path.cwd() / 'results' if folder is None else pathlib.Path(folder) @@ -73,48 +76,49 @@ def __init__( @property def main_results(self) -> Dict[str, Union[Scalar, Dict]]: from flixopt.features import InvestmentModel - - return { + main_results = { 'Objective': self.model.objective.value, - 'Penalty': float(self.model.effects.penalty.total.solution.values), + 'Penalty': self.model.effects.penalty.total.solution.values, 'Effects': { f'{effect.label} [{effect.unit}]': { - 'operation': float(effect.model.operation.total.solution.values), - 'invest': float(effect.model.invest.total.solution.values), - 'total': float(effect.model.total.solution.values), + 'operation': effect.model.operation.total.solution.values, + 'invest': effect.model.invest.total.solution.values, + 'total': effect.model.total.solution.values, } for effect in self.flow_system.effects }, 'Invest-Decisions': { 'Invested': { - model.label_of_element: float(model.size.solution) + model.label_of_element: model.size.solution for component in self.flow_system.components.values() for model in component.model.all_sub_models - if isinstance(model, InvestmentModel) and float(model.size.solution) >= CONFIG.modeling.EPSILON + if isinstance(model, InvestmentModel) and model.size.solution >= CONFIG.modeling.EPSILON }, 'Not invested': { - model.label_of_element: float(model.size.solution) + model.label_of_element: model.size.solution for component in self.flow_system.components.values() for model in component.model.all_sub_models - if isinstance(model, InvestmentModel) and float(model.size.solution) < CONFIG.modeling.EPSILON + if isinstance(model, InvestmentModel) and model.size.solution < CONFIG.modeling.EPSILON }, }, 'Buses with excess': [ { bus.label_full: { - 'input': float(np.sum(bus.model.excess_input.solution.values)), - 'output': float(np.sum(bus.model.excess_output.solution.values)), + 'input': np.sum(bus.model.excess_input.solution.values), + 'output': np.sum(bus.model.excess_output.solution.values), } } for bus in self.flow_system.buses.values() if bus.with_excess and ( - float(np.sum(bus.model.excess_input.solution.values)) > 1e-3 - or float(np.sum(bus.model.excess_output.solution.values)) > 1e-3 + np.sum(bus.model.excess_input.solution.values) > 1e-3 + or np.sum(bus.model.excess_output.solution.values) > 1e-3 ) ], } + return utils.round_floats(main_results) + @property def summary(self): return { @@ -184,7 +188,7 @@ def solve(self, solver: _Solver, log_file: Optional[pathlib.Path] = None, log_ma def _activate_time_series(self): self.flow_system.transform_data() self.flow_system.time_series_collection.set_selection( - timesteps=self.active_timesteps + timesteps=self.active_timesteps, scenarios=self.selected_scenarios ) diff --git a/flixopt/core.py b/flixopt/core.py index 185236b3a..ac62bc33a 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -795,17 +795,19 @@ def __init__( hours_of_previous_timesteps: Optional[Union[float, np.ndarray]] = None, ): """Initialize a TimeSeriesCollection.""" - self._validate_timesteps(timesteps) + self._full_timesteps = self._validate_timesteps(timesteps) + self._full_scenarios = self._validate_scenarios(scenarios) + + self._full_timesteps_extra = self._create_timesteps_with_extra( + self._full_timesteps, + self._calculate_hours_of_final_timestep(self._full_timesteps, hours_of_final_timestep=hours_of_last_timestep) + ) + self._full_hours_per_timestep = self.calculate_hours_per_timestep(self._full_timesteps_extra, self._full_scenarios) + self.hours_of_previous_timesteps = self._calculate_hours_of_previous_timesteps( timesteps, hours_of_previous_timesteps ) #TODO: Make dynamic - self._full_timesteps = timesteps - self._full_timesteps_extra = self._create_timesteps_with_extra(timesteps, hours_of_last_timestep) - self._full_hours_per_timestep = self.calculate_hours_per_timestep(self._full_timesteps_extra) - - self._full_scenarios = scenarios - # Series that need extra timestep self._has_extra_timestep: set = set() @@ -940,13 +942,13 @@ def _update_selected_timesteps(self, timesteps: Optional[pd.DatetimeIndex]) -> N self._selected_hours_per_timestep = None return - self._validate_timesteps(timesteps, self._full_timesteps) - - self._selected_timesteps = timesteps - self._selected_hours_per_timestep = self._full_hours_per_timestep.sel(time=timesteps) + self._selected_timesteps = self._validate_timesteps(timesteps, self._full_timesteps) self._selected_timesteps_extra = self._create_timesteps_with_extra( - timesteps, self._selected_hours_per_timestep.isel(time=-1).max().item() + timesteps, + self._calculate_hours_of_final_timestep(timesteps, self._full_timesteps) ) + self._selected_hours_per_timestep = self.calculate_hours_per_timestep(self._selected_timesteps_extra, + self._selected_scenarios) def as_dataset(self, with_extra_timestep: bool = True, with_constants: bool = True) -> xr.Dataset: """ @@ -1108,7 +1110,10 @@ def _calculate_group_weights(self) -> Dict[str, float]: return {group: 1 / count for group, count in group_counts.items()} @staticmethod - def _validate_timesteps(timesteps: pd.DatetimeIndex, present_timesteps: Optional[pd.DatetimeIndex] = None): + def _validate_timesteps( + timesteps: pd.DatetimeIndex, + present_timesteps: Optional[pd.DatetimeIndex] = None + ) -> pd.DatetimeIndex: """ Validate timesteps format and rename if needed. Args: @@ -1131,7 +1136,7 @@ def _validate_timesteps(timesteps: pd.DatetimeIndex, present_timesteps: Optional # Ensure timesteps has the required name if timesteps.name != 'time': - logger.warning('Renamed timesteps to "time" (was "%s")', timesteps.name) + logger.debug('Renamed timesteps to "time" (was "%s")', timesteps.name) timesteps.name = 'time' # Ensure timesteps is sorted @@ -1146,19 +1151,56 @@ def _validate_timesteps(timesteps: pd.DatetimeIndex, present_timesteps: Optional if present_timesteps is not None and not set(timesteps).issubset(set(present_timesteps)): raise ValueError('timesteps must be a subset of present_timesteps') + return timesteps + + @staticmethod + def _validate_scenarios( + scenarios: pd.Index, + present_scenarios: Optional[pd.Index] = None + ) -> Optional[pd.Index]: + """ + Validate scenario format and rename if needed. + Args: + scenarios: The scenarios to validate + present_scenarios: The present_scenarios that are present in the dataset + + Raises: + ValueError: If timesteps is not a pandas DatetimeIndex + ValueError: If timesteps is not at least 2 timestamps + ValueError: If timesteps has a different name than 'time' + ValueError: If timesteps is not sorted + ValueError: If timesteps contains duplicates + ValueError: If timesteps is not a subset of present_timesteps + """ + if scenarios is None: + return None + + if not isinstance(scenarios, pd.Index): + logger.warning('Converting scenarios to pandas.Index') + scenarios = pd.Index(scenarios, name='scenario') + + if len(scenarios) < 2: + logger.warning('scenarios must contain at least 2 scenarios') + raise ValueError('timesteps must contain at least 2 timestamps') + + # Ensure timesteps has the required name + if scenarios.name != 'scenario': + logger.debug('Renamed scenarios to "scneario" (was "%s")', scenarios.name) + scenarios.name = 'scenario' + + # Ensure timesteps is a subset of present_timesteps + if present_scenarios is not None and not set(scenarios).issubset(set(present_scenarios)): + raise ValueError('scenarios must be a subset of present_scenarios') + + return scenarios + @staticmethod def _create_timesteps_with_extra( - timesteps: pd.DatetimeIndex, hours_of_last_timestep: Optional[float] + timesteps: pd.DatetimeIndex, + hours_of_last_timestep: float ) -> pd.DatetimeIndex: """Create timesteps with an extra step at the end.""" - if hours_of_last_timestep is not None: - # Create the extra timestep using the specified duration - last_date = pd.DatetimeIndex([timesteps[-1] + pd.Timedelta(hours=hours_of_last_timestep)], name='time') - else: - # Use the last interval as the extra timestep duration - last_date = pd.DatetimeIndex([timesteps[-1] + (timesteps[-1] - timesteps[-2])], name='time') - - # Combine with original timesteps + last_date = pd.DatetimeIndex([timesteps[-1] + pd.Timedelta(hours=hours_of_last_timestep)], name='time') return pd.DatetimeIndex(timesteps.append(last_date), name='time') @staticmethod @@ -1174,14 +1216,61 @@ def _calculate_hours_of_previous_timesteps( return first_interval.total_seconds() / 3600 # Convert to hours @staticmethod - def calculate_hours_per_timestep(timesteps_extra: pd.DatetimeIndex) -> xr.DataArray: + def _calculate_hours_of_final_timestep( + timesteps: pd.DatetimeIndex, + timesteps_superset: Optional[pd.DatetimeIndex] = None, + hours_of_final_timestep: Optional[float] = None, + ) -> float: + """ + Calculate duration of the final timestep. + If timesteps_subset is provided, the final timestep is calculated for this subset. + The hours_of_final_timestep is only used if the final timestep cant be determined from the timesteps. + + Args: + timesteps: The full timesteps + timesteps_subset: The subset of timesteps + hours_of_final_timestep: The duration of the final timestep, if already known + + Returns: + The duration of the final timestep in hours + + Raises: + ValueError: If the provided timesteps_subset does not end before the timesteps superset + """ + if timesteps_superset is None: + if hours_of_final_timestep is not None: + return hours_of_final_timestep + return (timesteps[-1] - timesteps[-2]) / pd.Timedelta(hours=1) + + final_timestep = timesteps[-1] + + if timesteps_superset[-1] == final_timestep: + if hours_of_final_timestep is not None: + return hours_of_final_timestep + return (timesteps_superset[-1] - timesteps_superset[-2]) / pd.Timedelta(hours=1) + + elif timesteps_superset[-1] <= final_timestep: + raise ValueError(f'The provided timesteps ({timesteps}) end ' + f'after the provided timesteps_superset ({timesteps_superset})') + else: + # Get the first timestep in the superset that is after the final timestep of the subset + extra_timestep = timesteps_superset[timesteps_superset > final_timestep].min() + return (extra_timestep - final_timestep) / pd.Timedelta(hours=1) + + @staticmethod + def calculate_hours_per_timestep( + timesteps_extra: pd.DatetimeIndex, + scenarios: Optional[pd.Index] = None + ) -> xr.DataArray: """Calculate duration of each timestep.""" # Calculate differences between consecutive timestamps hours_per_step = np.diff(timesteps_extra) / pd.Timedelta(hours=1) - return xr.DataArray( - data=hours_per_step, coords={'time': timesteps_extra[:-1]}, dims=('time',), name='hours_per_step' - ) + return DataConverter.as_dataarray( + hours_per_step, + timesteps=timesteps_extra[:-1], + scenarios=scenarios, + ).rename('hours_per_step') def get_numeric_stats(data: xr.DataArray, decimals: int = 2, padd: int = 10, by_scenario: bool = False) -> str: diff --git a/flixopt/effects.py b/flixopt/effects.py index e834e339e..e15fa16b1 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -118,10 +118,11 @@ def __init__(self, model: SystemModel, element: Effect): self.total: Optional[linopy.Variable] = None self.invest: ShareAllocationModel = self.add( ShareAllocationModel( - self._model, - False, - self.label_of_element, - 'invest', + model=self._model, + has_time_dim=False, + has_scenario_dim=True, + label_of_element=self.label_of_element, + label='invest', label_full=f'{self.label_full}(invest)', total_max=self.element.maximum_invest, total_min=self.element.minimum_invest, @@ -130,10 +131,11 @@ def __init__(self, model: SystemModel, element: Effect): self.operation: ShareAllocationModel = self.add( ShareAllocationModel( - self._model, - True, - self.label_of_element, - 'operation', + model=self._model, + has_time_dim=True, + has_scenario_dim=True, + label_of_element=self.label_of_element, + label='operation', label_full=f'{self.label_full}(operation)', total_max=self.element.maximum_operation, total_min=self.element.minimum_operation, @@ -154,7 +156,7 @@ def do_modeling(self): self._model.add_variables( lower=self.element.minimum_total if self.element.minimum_total is not None else -np.inf, upper=self.element.maximum_total if self.element.maximum_total is not None else np.inf, - coords=None, + coords=self._model.get_coords(time_dim=False), name=f'{self.label_full}|total', ), 'total', @@ -162,7 +164,7 @@ def do_modeling(self): self.add( self._model.add_constraints( - self.total == self.operation.total.sum() + self.invest.total.sum(), name=f'{self.label_full}|total' + self.total == self.operation.total + self.invest.total, name=f'{self.label_full}|total' ), 'total', ) @@ -350,29 +352,42 @@ def add_share_to_effects( ) -> None: for effect, expression in expressions.items(): if target == 'operation': - self.effects[effect].model.operation.add_share(name, expression) + self.effects[effect].model.operation.add_share( + name, + expression, + has_time_dim=True, + has_scenario_dim=True, + ) elif target == 'invest': - self.effects[effect].model.invest.add_share(name, expression) + self.effects[effect].model.invest.add_share( + name, + expression, + has_time_dim=False, + has_scenario_dim=True, + ) else: raise ValueError(f'Target {target} not supported!') def add_share_to_penalty(self, name: str, expression: linopy.LinearExpression) -> None: if expression.ndim != 0: raise TypeError(f'Penalty shares must be scalar expressions! ({expression.ndim=})') - self.penalty.add_share(name, expression) + self.penalty.add_share(name, expression, has_time_dim=False, has_scenario_dim=False) def do_modeling(self): for effect in self.effects: effect.create_model(self._model) self.penalty = self.add( - ShareAllocationModel(self._model, shares_are_time_series=False, label_of_element='Penalty') + ShareAllocationModel(self._model, has_time_dim=False, has_scenario_dim=False, label_of_element='Penalty') ) for model in [effect.model for effect in self.effects] + [self.penalty]: model.do_modeling() self._add_share_between_effects() - - self._model.add_objective(self.effects.objective_effect.model.total + self.penalty.total) + scaling_factor = len(self._model.time_series_collection.scenarios) if self._model.time_series_collection.scenarios is not None else 1 + self._model.add_objective( + (self.effects.objective_effect.model.total / scaling_factor).sum() + + (self.penalty.total / scaling_factor).sum() + ) def _add_share_between_effects(self): for origin_effect in self.effects: @@ -381,10 +396,14 @@ def _add_share_between_effects(self): self.effects[target_effect].model.operation.add_share( origin_effect.model.operation.label_full, origin_effect.model.operation.total_per_timestep * time_series.selected_data, + has_time_dim=True, + has_scenario_dim=True, ) # 2. invest: -> hier ist es Scalar (share) for target_effect, factor in origin_effect.specific_share_to_other_effects_invest.items(): self.effects[target_effect].model.invest.add_share( origin_effect.model.invest.label_full, origin_effect.model.invest.total * factor, + has_time_dim=False, + has_scenario_dim=True, ) diff --git a/flixopt/features.py b/flixopt/features.py index 7b92396fd..a9f50aba6 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -841,7 +841,8 @@ class ShareAllocationModel(Model): def __init__( self, model: SystemModel, - shares_are_time_series: bool, + has_time_dim: bool, + has_scenario_dim: bool, label_of_element: Optional[str] = None, label: Optional[str] = None, label_full: Optional[str] = None, @@ -851,9 +852,9 @@ def __init__( min_per_hour: Optional[TimestepData] = None, ): super().__init__(model, label_of_element=label_of_element, label=label, label_full=label_full) - if not shares_are_time_series: # If the condition is True + if not has_time_dim: # If the condition is True assert max_per_hour is None and min_per_hour is None, ( - 'Both max_per_hour and min_per_hour cannot be used when shares_are_time_series is False' + 'Both max_per_hour and min_per_hour cannot be used when has_time_dim is False' ) self.total_per_timestep: Optional[linopy.Variable] = None self.total: Optional[linopy.Variable] = None @@ -864,7 +865,8 @@ def __init__( self._eq_total: Optional[linopy.Constraint] = None # Parameters - self._shares_are_time_series = shares_are_time_series + self._has_time_dim = has_time_dim + self._has_scenario_dim = has_scenario_dim self._total_max = total_max if total_min is not None else np.inf self._total_min = total_min if total_min is not None else -np.inf self._max_per_hour = max_per_hour if max_per_hour is not None else np.inf @@ -873,7 +875,10 @@ def __init__( def do_modeling(self): self.total = self.add( self._model.add_variables( - lower=self._total_min, upper=self._total_max, coords=None, name=f'{self.label_full}|total' + lower=self._total_min, + upper=self._total_max, + coords=self._model.get_coords(time_dim=False, scenario_dim=self._has_scenario_dim), + name=f'{self.label_full}|total' ), 'total', ) @@ -882,16 +887,16 @@ def do_modeling(self): self._model.add_constraints(self.total == 0, name=f'{self.label_full}|total'), 'total' ) - if self._shares_are_time_series: + if self._has_time_dim: self.total_per_timestep = self.add( self._model.add_variables( lower=-np.inf if (self._min_per_hour is None) - else np.multiply(self._min_per_hour, self._model.hours_per_step), + else self._min_per_hour * self._model.hours_per_step, upper=np.inf if (self._max_per_hour is None) - else np.multiply(self._max_per_hour, self._model.hours_per_step), - coords=self._model.get_coords(), + else self._max_per_hour * self._model.hours_per_step, + coords=self._model.get_coords(time_dim=True, scenario_dim=self._has_scenario_dim), name=f'{self.label_full}|total_per_timestep', ), 'total_per_timestep', @@ -903,12 +908,14 @@ def do_modeling(self): ) # Add it to the total - self._eq_total.lhs -= self.total_per_timestep.sum() + self._eq_total.lhs -= self.total_per_timestep.sum(dim='time') def add_share( self, name: str, expression: linopy.LinearExpression, + has_time_dim: bool, + has_scenario_dim: bool, ): """ Add a share to the share allocation model. If the share already exists, the expression is added to the existing share. @@ -920,16 +927,17 @@ def add_share( name: The name of the share. expression: The expression of the share. Added to the right hand side of the constraint. """ + if has_time_dim and not self._has_time_dim: + raise ValueError('Cannot add share with time_dim=True to a model without time_dim') + if has_scenario_dim and not self._has_scenario_dim: + raise ValueError('Cannot add share with scenario_dim=True to a model without scenario_dim') + if name in self.shares: self.share_constraints[name].lhs -= expression else: self.shares[name] = self.add( self._model.add_variables( - coords=None - if isinstance(expression, linopy.LinearExpression) - and expression.ndim == 0 - or not isinstance(expression, linopy.LinearExpression) - else self._model.get_coords(), #TODO: Add logic on what coords to use + coords=self._model.get_coords(time_dim=has_time_dim, scenario_dim=has_scenario_dim), name=f'{name}->{self.label_full}', ), name, @@ -937,7 +945,7 @@ def add_share( self.share_constraints[name] = self.add( self._model.add_constraints(self.shares[name] == expression, name=f'{name}->{self.label_full}'), name ) - if self.shares[name].ndim == 0: + if not has_time_dim: self._eq_total.lhs -= self.shares[name] else: self._eq_total_per_timestep.lhs -= self.shares[name] diff --git a/flixopt/structure.py b/flixopt/structure.py index 7306c97d5..91fc6c1d0 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -115,15 +115,24 @@ def get_coords( Returns: The coordinates of the model. Might also be None if no scenarios are present and time_dim is False """ + if not scenario_dim and not time_dim: + return None scenarios = self.time_series_collection.scenarios timesteps = self.time_series_collection.timesteps if not extra_timestep else self.time_series_collection.timesteps_extra - if scenarios is None: - if time_dim: + + if scenario_dim and time_dim: + if scenarios is None: return (timesteps,) - return None + return scenarios, timesteps + if scenario_dim and not time_dim: + if scenarios is None: + return None return (scenarios,) - return scenarios, timesteps + if time_dim and not scenario_dim: + return (timesteps,) + + raise ValueError(f'Cannot get coordinates with both {scenario_dim=} and {time_dim=}') class Interface: diff --git a/flixopt/utils.py b/flixopt/utils.py index 6b5d88693..542f87942 100644 --- a/flixopt/utils.py +++ b/flixopt/utils.py @@ -19,6 +19,12 @@ def round_floats(obj, decimals=2): return [round_floats(v, decimals) for v in obj] elif isinstance(obj, float): return round(obj, decimals) + elif isinstance(obj, int): + return obj + elif isinstance(obj, np.ndarray): + return np.round(obj, decimals).tolist() + elif isinstance(obj, xr.DataArray): + return obj.round(decimals).values.tolist() return obj From 24af8c60ba5fc2ce008af89e8dc45cefd0344a98 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 2 Apr 2025 14:38:08 +0200 Subject: [PATCH 004/448] ruff check and format --- examples/04_Scenarios/scenario_example.py | 3 +- flixopt/calculation.py | 1 + flixopt/components.py | 18 +++--- flixopt/core.py | 53 +++++++---------- flixopt/effects.py | 16 +++-- flixopt/elements.py | 10 +++- flixopt/features.py | 21 ++++--- flixopt/flow_system.py | 17 ++++-- flixopt/interface.py | 14 ++--- flixopt/structure.py | 11 ++-- flixopt/utils.py | 1 - tests/test_dataconverter.py | 4 +- tests/test_timeseries.py | 71 +++++++++++------------ 13 files changed, 119 insertions(+), 121 deletions(-) diff --git a/examples/04_Scenarios/scenario_example.py b/examples/04_Scenarios/scenario_example.py index d834ff5f0..a004d1851 100644 --- a/examples/04_Scenarios/scenario_example.py +++ b/examples/04_Scenarios/scenario_example.py @@ -11,8 +11,7 @@ if __name__ == '__main__': # --- Create Time Series Data --- # Heat demand profile (e.g., kW) over time and corresponding power prices - heat_demand_per_h = np.array([[30, 0, 90, 110, 110, 20, 20, 20, 20], - [30, 0, 100, 118, 125, 20, 20, 20, 20]]) + heat_demand_per_h = np.array([[30, 0, 90, 110, 110, 20, 20, 20, 20], [30, 0, 100, 118, 125, 20, 20, 20, 20]]) power_prices = np.ones(9) * 0.08 # Create datetime array starting from '2020-01-01' for the given time period diff --git a/flixopt/calculation.py b/flixopt/calculation.py index 2dbb6af19..962d2c95f 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -76,6 +76,7 @@ def __init__( @property def main_results(self) -> Dict[str, Union[Scalar, Dict]]: from flixopt.features import InvestmentModel + main_results = { 'Objective': self.model.objective.value, 'Penalty': self.model.effects.penalty.total.solution.values, diff --git a/flixopt/components.py b/flixopt/components.py index 4726ca0f4..69b0fe47a 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -9,7 +9,7 @@ import numpy as np from . import utils -from .core import TimestepData, PlausibilityError, Scalar, TimeSeries, ScenarioData +from .core import PlausibilityError, Scalar, ScenarioData, TimeSeries, TimestepData from .elements import Component, ComponentModel, Flow from .features import InvestmentModel, OnOffModel, PiecewiseModel from .interface import InvestParameters, OnOffParameters, PiecewiseConversion @@ -225,9 +225,7 @@ def _plausibility_checks(self) -> None: Check for infeasible or uncommon combinations of parameters """ if isinstance(self.initial_charge_state, str) and not self.initial_charge_state == 'lastValueOfSim': - raise PlausibilityError( - f'initial_charge_state has undefined value: {self.initial_charge_state}' - ) + raise PlausibilityError(f'initial_charge_state has undefined value: {self.initial_charge_state}') else: if isinstance(self.capacity_in_flow_hours, InvestParameters): if self.capacity_in_flow_hours.fixed_size is None: @@ -244,7 +242,7 @@ def _plausibility_checks(self) -> None: minimum_inital_capacity = maximum_capacity * self.relative_minimum_charge_state.isel(time=1) # initial capacity <= allowed max for minimum_size: maximum_inital_capacity = minimum_capacity * self.relative_maximum_charge_state.isel(time=1) - #TODO: index=1 ??? I think index 0 + # TODO: index=1 ??? I think index 0 if (self.initial_charge_state > maximum_inital_capacity).any(): raise ValueError( @@ -257,6 +255,7 @@ def _plausibility_checks(self) -> None: f'is below allowed minimum charge_state {minimum_inital_capacity}' ) + @register_class_for_io class Transmission(Component): # TODO: automatic on-Value in Flows if loss_abs @@ -427,7 +426,9 @@ def do_modeling(self): self.add( self._model.add_constraints( sum([flow.model.flow_rate * conv_factors[flow.label].selected_data for flow in used_inputs]) - == sum([flow.model.flow_rate * conv_factors[flow.label].selected_data for flow in used_outputs]), + == sum( + [flow.model.flow_rate * conv_factors[flow.label].selected_data for flow in used_outputs] + ), name=f'{self.label_full}|conversion_{i}', ) ) @@ -467,7 +468,10 @@ def do_modeling(self): lb, ub = self.absolute_charge_state_bounds self.charge_state = self.add( self._model.add_variables( - lower=lb, upper=ub, coords=self._model.get_coords(extra_timestep=True), name=f'{self.label_full}|charge_state' + lower=lb, + upper=ub, + coords=self._model.get_coords(extra_timestep=True), + name=f'{self.label_full}|charge_state', ), 'charge_state', ) diff --git a/flixopt/core.py b/flixopt/core.py index ac62bc33a..68d1ddaad 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -787,6 +787,7 @@ class TimeSeriesCollection: Provides a way to store time series data and work with subsets of dimensions that automatically update all references when changed. """ + def __init__( self, timesteps: pd.DatetimeIndex, @@ -800,13 +801,17 @@ def __init__( self._full_timesteps_extra = self._create_timesteps_with_extra( self._full_timesteps, - self._calculate_hours_of_final_timestep(self._full_timesteps, hours_of_final_timestep=hours_of_last_timestep) + self._calculate_hours_of_final_timestep( + self._full_timesteps, hours_of_final_timestep=hours_of_last_timestep + ), + ) + self._full_hours_per_timestep = self.calculate_hours_per_timestep( + self._full_timesteps_extra, self._full_scenarios ) - self._full_hours_per_timestep = self.calculate_hours_per_timestep(self._full_timesteps_extra, self._full_scenarios) self.hours_of_previous_timesteps = self._calculate_hours_of_previous_timesteps( timesteps, hours_of_previous_timesteps - ) #TODO: Make dynamic + ) # TODO: Make dynamic # Series that need extra timestep self._has_extra_timestep: set = set() @@ -944,11 +949,11 @@ def _update_selected_timesteps(self, timesteps: Optional[pd.DatetimeIndex]) -> N self._selected_timesteps = self._validate_timesteps(timesteps, self._full_timesteps) self._selected_timesteps_extra = self._create_timesteps_with_extra( - timesteps, - self._calculate_hours_of_final_timestep(timesteps, self._full_timesteps) + timesteps, self._calculate_hours_of_final_timestep(timesteps, self._full_timesteps) + ) + self._selected_hours_per_timestep = self.calculate_hours_per_timestep( + self._selected_timesteps_extra, self._selected_scenarios ) - self._selected_hours_per_timestep = self.calculate_hours_per_timestep(self._selected_timesteps_extra, - self._selected_scenarios) def as_dataset(self, with_extra_timestep: bool = True, with_constants: bool = True) -> xr.Dataset: """ @@ -1006,16 +1011,11 @@ def _propagate_selection_to_time_series(self) -> None: """Apply the current selection to all TimeSeries objects.""" for ts_name, ts in self._time_series.items(): if ts.has_time_dim: - timesteps = ( - self.timesteps_extra if ts_name in self._has_extra_timestep else self.timesteps - ) + timesteps = self.timesteps_extra if ts_name in self._has_extra_timestep else self.timesteps else: timesteps = None - ts.set_selection( - timesteps=timesteps, - scenarios=self.scenarios if ts.has_scenario_dim else None - ) + ts.set_selection(timesteps=timesteps, scenarios=self.scenarios if ts.has_scenario_dim else None) def __getitem__(self, name: str) -> TimeSeries: """ @@ -1072,9 +1072,7 @@ def update_time_series(self, name: str, data: TimestepData) -> TimeSeries: # Convert data to proper format data_array = DataConverter.as_dataarray( - data, - timesteps=target_timesteps, - scenarios=self.scenarios if ts.has_scenario_dim else None + data, timesteps=target_timesteps, scenarios=self.scenarios if ts.has_scenario_dim else None ) # Update the TimeSeries @@ -1111,8 +1109,7 @@ def _calculate_group_weights(self) -> Dict[str, float]: @staticmethod def _validate_timesteps( - timesteps: pd.DatetimeIndex, - present_timesteps: Optional[pd.DatetimeIndex] = None + timesteps: pd.DatetimeIndex, present_timesteps: Optional[pd.DatetimeIndex] = None ) -> pd.DatetimeIndex: """ Validate timesteps format and rename if needed. @@ -1154,10 +1151,7 @@ def _validate_timesteps( return timesteps @staticmethod - def _validate_scenarios( - scenarios: pd.Index, - present_scenarios: Optional[pd.Index] = None - ) -> Optional[pd.Index]: + def _validate_scenarios(scenarios: pd.Index, present_scenarios: Optional[pd.Index] = None) -> Optional[pd.Index]: """ Validate scenario format and rename if needed. Args: @@ -1195,10 +1189,7 @@ def _validate_scenarios( return scenarios @staticmethod - def _create_timesteps_with_extra( - timesteps: pd.DatetimeIndex, - hours_of_last_timestep: float - ) -> pd.DatetimeIndex: + def _create_timesteps_with_extra(timesteps: pd.DatetimeIndex, hours_of_last_timestep: float) -> pd.DatetimeIndex: """Create timesteps with an extra step at the end.""" last_date = pd.DatetimeIndex([timesteps[-1] + pd.Timedelta(hours=hours_of_last_timestep)], name='time') return pd.DatetimeIndex(timesteps.append(last_date), name='time') @@ -1250,8 +1241,9 @@ def _calculate_hours_of_final_timestep( return (timesteps_superset[-1] - timesteps_superset[-2]) / pd.Timedelta(hours=1) elif timesteps_superset[-1] <= final_timestep: - raise ValueError(f'The provided timesteps ({timesteps}) end ' - f'after the provided timesteps_superset ({timesteps_superset})') + raise ValueError( + f'The provided timesteps ({timesteps}) end after the provided timesteps_superset ({timesteps_superset})' + ) else: # Get the first timestep in the superset that is after the final timestep of the subset extra_timestep = timesteps_superset[timesteps_superset > final_timestep].min() @@ -1259,8 +1251,7 @@ def _calculate_hours_of_final_timestep( @staticmethod def calculate_hours_per_timestep( - timesteps_extra: pd.DatetimeIndex, - scenarios: Optional[pd.Index] = None + timesteps_extra: pd.DatetimeIndex, scenarios: Optional[pd.Index] = None ) -> xr.DataArray: """Calculate duration of each timestep.""" # Calculate differences between consecutive timestamps diff --git a/flixopt/effects.py b/flixopt/effects.py index e15fa16b1..226e59a0f 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -11,9 +11,8 @@ import linopy import numpy as np -import pandas as pd -from .core import TimestepData, NumericDataTS, Scalar, TimeSeries, TimeSeriesCollection, ScenarioData, TimestepData +from .core import NumericDataTS, ScenarioData, TimeSeries, TimeSeriesCollection, TimestepData from .features import ShareAllocationModel from .structure import Element, ElementModel, Interface, Model, SystemModel, register_class_for_io @@ -79,7 +78,9 @@ def __init__( self.specific_share_to_other_effects_operation: EffectValuesUserTimestep = ( specific_share_to_other_effects_operation or {} ) - self.specific_share_to_other_effects_invest: EffectValuesUserTimestep = specific_share_to_other_effects_invest or {} + self.specific_share_to_other_effects_invest: EffectValuesUserTimestep = ( + specific_share_to_other_effects_invest or {} + ) self.minimum_operation = minimum_operation self.maximum_operation = maximum_operation self.minimum_operation_per_hour: NumericDataTS = minimum_operation_per_hour @@ -211,8 +212,7 @@ def add_effects(self, *effects: Effect) -> None: logger.info(f'Registered new Effect: {effect.label}') def create_effect_values_dict( - self, - effect_values_user: Union[EffectValuesUserScenario, EffectValuesUserTimestep] + self, effect_values_user: Union[EffectValuesUserScenario, EffectValuesUserTimestep] ) -> Optional[EffectValuesDict]: """ Converts effect values into a dictionary. If a scalar is provided, it is associated with a default effect type. @@ -383,7 +383,11 @@ def do_modeling(self): model.do_modeling() self._add_share_between_effects() - scaling_factor = len(self._model.time_series_collection.scenarios) if self._model.time_series_collection.scenarios is not None else 1 + scaling_factor = ( + len(self._model.time_series_collection.scenarios) + if self._model.time_series_collection.scenarios is not None + else 1 + ) self._model.add_objective( (self.effects.objective_effect.model.total / scaling_factor).sum() + (self.penalty.total / scaling_factor).sum() diff --git a/flixopt/elements.py b/flixopt/elements.py index b6de8c7c2..a98edff9d 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -10,7 +10,7 @@ import numpy as np from .config import CONFIG -from .core import TimestepData, NumericDataTS, PlausibilityError, Scalar, ScenarioData +from .core import NumericDataTS, PlausibilityError, Scalar, ScenarioData, TimestepData from .effects import EffectValuesUserTimestep from .features import InvestmentModel, OnOffModel, PreventSimultaneousUsageModel from .interface import InvestParameters, OnOffParameters @@ -467,11 +467,15 @@ def do_modeling(self) -> None: self._model.hours_per_step, self.element.excess_penalty_per_flow_hour.selected_data ) self.excess_input = self.add( - self._model.add_variables(lower=0, coords=self._model.get_coords(), name=f'{self.label_full}|excess_input'), + self._model.add_variables( + lower=0, coords=self._model.get_coords(), name=f'{self.label_full}|excess_input' + ), 'excess_input', ) self.excess_output = self.add( - self._model.add_variables(lower=0, coords=self._model.get_coords(), name=f'{self.label_full}|excess_output'), + self._model.add_variables( + lower=0, coords=self._model.get_coords(), name=f'{self.label_full}|excess_output' + ), 'excess_output', ) eq_bus_balance.lhs -= -self.excess_input + self.excess_output diff --git a/flixopt/features.py b/flixopt/features.py index a9f50aba6..a122a9dac 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -9,9 +9,8 @@ import linopy import numpy as np -from . import utils from .config import CONFIG -from .core import TimestepData, Scalar, TimeSeries +from .core import Scalar, TimeSeries, TimestepData from .interface import InvestParameters, OnOffParameters, Piece, Piecewise, PiecewiseConversion, PiecewiseEffects from .structure import Model, SystemModel @@ -303,12 +302,16 @@ def do_modeling(self): if self.parameters.use_switch_on: self.switch_on = self.add( - self._model.add_variables(binary=True, name=f'{self.label_full}|switch_on', coords=self._model.get_coords()), + self._model.add_variables( + binary=True, name=f'{self.label_full}|switch_on', coords=self._model.get_coords() + ), 'switch_on', ) self.switch_off = self.add( - self._model.add_variables(binary=True, name=f'{self.label_full}|switch_off', coords=self._model.get_coords()), + self._model.add_variables( + binary=True, name=f'{self.label_full}|switch_off', coords=self._model.get_coords() + ), 'switch_off', ) @@ -878,7 +881,7 @@ def do_modeling(self): lower=self._total_min, upper=self._total_max, coords=self._model.get_coords(time_dim=False, scenario_dim=self._has_scenario_dim), - name=f'{self.label_full}|total' + name=f'{self.label_full}|total', ), 'total', ) @@ -890,12 +893,8 @@ def do_modeling(self): if self._has_time_dim: self.total_per_timestep = self.add( self._model.add_variables( - lower=-np.inf - if (self._min_per_hour is None) - else self._min_per_hour * self._model.hours_per_step, - upper=np.inf - if (self._max_per_hour is None) - else self._max_per_hour * self._model.hours_per_step, + lower=-np.inf if (self._min_per_hour is None) else self._min_per_hour * self._model.hours_per_step, + upper=np.inf if (self._max_per_hour is None) else self._max_per_hour * self._model.hours_per_step, coords=self._model.get_coords(time_dim=True, scenario_dim=self._has_scenario_dim), name=f'{self.label_full}|total_per_timestep', ), diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index a4705371c..6985572c1 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -16,8 +16,15 @@ from rich.pretty import Pretty from . import io as fx_io -from .core import TimestepData, TimeSeries, TimeSeriesCollection, TimeSeriesData, Scalar -from .effects import Effect, EffectCollection, EffectTimeSeries, EffectValuesDict, EffectValuesUserScenario, EffectValuesUserTimestep +from .core import Scalar, TimeSeries, TimeSeriesCollection, TimeSeriesData, TimestepData +from .effects import ( + Effect, + EffectCollection, + EffectTimeSeries, + EffectValuesDict, + EffectValuesUserScenario, + EffectValuesUserTimestep, +) from .elements import Bus, Component, Flow from .structure import CLASS_REGISTRY, Element, SystemModel @@ -296,7 +303,7 @@ def create_time_series( has_extra_timestep: Whether the data has an extra timestep """ if not has_time_dim and not has_scenario_dim: - raise ValueError("At least one of the dimensions must be present") + raise ValueError('At least one of the dimensions must be present') if data is None: return None @@ -324,7 +331,7 @@ def create_time_series( has_scenario_dim=has_scenario_dim, has_extra_timestep=has_extra_timestep, aggregation_weight=data.agg_weight, - aggregation_group=data.agg_group + aggregation_group=data.agg_group, ) return self.time_series_collection.add_time_series( data=data, @@ -358,7 +365,7 @@ def create_effect_time_series( has_scenario_dim: Whether the data has a scenario dimension """ if not has_time_dim and not has_scenario_dim: - raise ValueError("At least one of the dimensions must be present") + raise ValueError('At least one of the dimensions must be present') effect_values: Optional[EffectValuesDict] = self.effects.create_effect_values_dict(effect_values) if effect_values is None: diff --git a/flixopt/interface.py b/flixopt/interface.py index f57362ee3..b6eb80c54 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, Dict, Iterator, List, Optional, Union from .config import CONFIG -from .core import TimestepData, NumericDataTS, Scalar, ScenarioData +from .core import NumericDataTS, Scalar, ScenarioData, TimestepData from .structure import Interface, register_class_for_io if TYPE_CHECKING: # for type checking and preventing circular imports @@ -34,16 +34,10 @@ def __init__(self, start: TimestepData, end: TimestepData): def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): self.start = flow_system.create_time_series( - name=f'{name_prefix}|start', - data=self.start, - has_time_dim=self.has_time_dim, - has_scenario_dim=True + name=f'{name_prefix}|start', data=self.start, has_time_dim=self.has_time_dim, has_scenario_dim=True ) self.end = flow_system.create_time_series( - name=f'{name_prefix}|end', - data=self.end, - has_time_dim=self.has_time_dim, - has_scenario_dim=True + name=f'{name_prefix}|end', data=self.end, has_time_dim=self.has_time_dim, has_scenario_dim=True ) @@ -222,7 +216,7 @@ def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): has_scenario_dim=True, ) if self.piecewise_effects is not None: - self.piecewise_effects.has_time_dim=False + self.piecewise_effects.has_time_dim = False self.piecewise_effects.transform_data(flow_system, f'{name_prefix}|PiecewiseEffects') @property diff --git a/flixopt/structure.py b/flixopt/structure.py index 91fc6c1d0..37b02b122 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -19,7 +19,7 @@ from rich.pretty import Pretty from .config import CONFIG -from .core import TimestepData, Scalar, TimeSeries, TimeSeriesCollection, TimeSeriesData +from .core import Scalar, TimeSeries, TimeSeriesCollection, TimeSeriesData, TimestepData if TYPE_CHECKING: # for type checking and preventing circular imports from .effects import EffectCollectionModel @@ -99,10 +99,7 @@ def hours_of_previous_timesteps(self): return self.time_series_collection.hours_of_previous_timesteps def get_coords( - self, - scenario_dim = True, - time_dim = True, - extra_timestep = False + self, scenario_dim=True, time_dim=True, extra_timestep=False ) -> Optional[Union[Tuple[pd.Index], Tuple[pd.Index, pd.Index]]]: """ Returns the coordinates of the model @@ -118,7 +115,9 @@ def get_coords( if not scenario_dim and not time_dim: return None scenarios = self.time_series_collection.scenarios - timesteps = self.time_series_collection.timesteps if not extra_timestep else self.time_series_collection.timesteps_extra + timesteps = ( + self.time_series_collection.timesteps if not extra_timestep else self.time_series_collection.timesteps_extra + ) if scenario_dim and time_dim: if scenarios is None: diff --git a/flixopt/utils.py b/flixopt/utils.py index 542f87942..3e65328a4 100644 --- a/flixopt/utils.py +++ b/flixopt/utils.py @@ -11,7 +11,6 @@ logger = logging.getLogger('flixopt') - def round_floats(obj, decimals=2): if isinstance(obj, dict): return {k: round_floats(v, decimals) for k, v in obj.items()} diff --git a/tests/test_dataconverter.py b/tests/test_dataconverter.py index 0466f3a2e..a023b8e58 100644 --- a/tests/test_dataconverter.py +++ b/tests/test_dataconverter.py @@ -378,7 +378,9 @@ def test_all_nan_data(self, sample_time_index, sample_scenario_index): assert np.all(np.isnan(result.values)) # Series of all NaNs - result = DataConverter.as_dataarray(np.array([np.nan, np.nan, np.nan, np.nan, np.nan]), sample_time_index, sample_scenario_index) + result = DataConverter.as_dataarray( + np.array([np.nan, np.nan, np.nan, np.nan, np.nan]), sample_time_index, sample_scenario_index + ) assert np.all(np.isnan(result.values)) def test_mixed_data_types(self, sample_time_index, sample_scenario_index): diff --git a/tests/test_timeseries.py b/tests/test_timeseries.py index 50136536b..d64c13d85 100644 --- a/tests/test_timeseries.py +++ b/tests/test_timeseries.py @@ -111,7 +111,9 @@ def test_reset(self, sample_timeseries, sample_timesteps): def test_restore_data(self, sample_timeseries, simple_dataarray): """Test restore_data method.""" # Modify the stored data - new_data = xr.DataArray([1, 2, 3, 4, 5], coords={'time': sample_timeseries.stored_data.coords['time']}, dims=['time']) + new_data = xr.DataArray( + [1, 2, 3, 4, 5], coords={'time': sample_timeseries.stored_data.coords['time']}, dims=['time'] + ) # Store original data for comparison original_data = sample_timeseries.stored_data @@ -227,7 +229,9 @@ def test_all_equal(self, sample_timesteps): def test_arithmetic_operations(self, sample_timeseries): """Test arithmetic operations.""" # Create a second TimeSeries for testing - data2 = xr.DataArray([1, 2, 3, 4, 5], coords={'time': sample_timeseries.stored_data.coords['time']}, dims=['time']) + data2 = xr.DataArray( + [1, 2, 3, 4, 5], coords={'time': sample_timeseries.stored_data.coords['time']}, dims=['time'] + ) ts2 = TimeSeries(data2, 'Second Series') # Test operations between two TimeSeries objects @@ -284,7 +288,9 @@ def test_numpy_ufunc(self, sample_timeseries): ) # Test with two TimeSeries objects - data2 = xr.DataArray([1, 2, 3, 4, 5], coords={'time': sample_timeseries.stored_data.coords['time']}, dims=['time']) + data2 = xr.DataArray( + [1, 2, 3, 4, 5], coords={'time': sample_timeseries.stored_data.coords['time']}, dims=['time'] + ) ts2 = TimeSeries(data2, 'Second Series') assert np.array_equal( @@ -311,15 +317,15 @@ def sample_scenario_index(): @pytest.fixture def simple_scenario_dataarray(sample_timesteps, sample_scenario_index): """Create a DataArray with both scenario and time dimensions.""" - data = np.array([ - [10, 20, 30, 40, 50], # baseline - [15, 25, 35, 45, 55], # high_demand - [5, 15, 25, 35, 45] # low_price - ]) + data = np.array( + [ + [10, 20, 30, 40, 50], # baseline + [15, 25, 35, 45, 55], # high_demand + [5, 15, 25, 35, 45], # low_price + ] + ) return xr.DataArray( - data=data, - coords={'scenario': sample_scenario_index, 'time': sample_timesteps}, - dims=['scenario', 'time'] + data=data, coords={'scenario': sample_scenario_index, 'time': sample_timesteps}, dims=['scenario', 'time'] ) @@ -412,21 +418,23 @@ def test_all_equal_with_scenarios(self, sample_timesteps, sample_scenario_index) equal_dataarray = xr.DataArray( data=equal_data, coords={'scenario': sample_scenario_index, 'time': sample_timesteps}, - dims=['scenario', 'time'] + dims=['scenario', 'time'], ) ts_equal = TimeSeries(equal_dataarray, 'Equal Scenario Series') assert ts_equal.all_equal is True # Equal within each scenario but different between scenarios - per_scenario_equal = np.array([ - [5, 5, 5, 5, 5], # baseline - all 5 - [10, 10, 10, 10, 10], # high_demand - all 10 - [15, 15, 15, 15, 15] # low_price - all 15 - ]) + per_scenario_equal = np.array( + [ + [5, 5, 5, 5, 5], # baseline - all 5 + [10, 10, 10, 10, 10], # high_demand - all 10 + [15, 15, 15, 15, 15], # low_price - all 15 + ] + ) per_scenario_dataarray = xr.DataArray( data=per_scenario_equal, coords={'scenario': sample_scenario_index, 'time': sample_timesteps}, - dims=['scenario', 'time'] + dims=['scenario', 'time'], ) ts_per_scenario = TimeSeries(per_scenario_dataarray, 'Per-Scenario Equal Series') assert ts_per_scenario.all_equal is False @@ -436,9 +444,7 @@ def test_arithmetic_with_scenarios(self, sample_scenario_timeseries, sample_time # Create a second TimeSeries with scenarios data2 = np.ones((3, 5)) # All ones second_dataarray = xr.DataArray( - data=data2, - coords={'scenario': sample_scenario_index, 'time': sample_timesteps}, - dims=['scenario', 'time'] + data=data2, coords={'scenario': sample_scenario_index, 'time': sample_timesteps}, dims=['scenario', 'time'] ) ts2 = TimeSeries(second_dataarray, 'Second Series') @@ -624,11 +630,7 @@ def test_add_time_series_with_scenarios(self, sample_scenario_allocator): assert np.array_equal(ts2.sel(scenario=scenario).values, data) # Test 2D array (one row per scenario) - data_2d = np.array([ - [10, 20, 30, 40, 50], - [15, 25, 35, 45, 55], - [5, 15, 25, 35, 45] - ]) + data_2d = np.array([[10, 20, 30, 40, 50], [15, 25, 35, 45, 55], [5, 15, 25, 35, 45]]) ts3 = sample_scenario_allocator.add_time_series('scenario_specific_series', data_2d) assert ts3._has_scenarios assert ts3.selected_data.shape == (3, 5) @@ -637,7 +639,9 @@ def test_add_time_series_with_scenarios(self, sample_scenario_allocator): assert np.array_equal(ts3.sel(scenario='high_demand').values, data_2d[1]) assert np.array_equal(ts3.sel(scenario='low_price').values, data_2d[2]) - def test_selection_propagation_with_scenarios(self, sample_scenario_allocator, sample_timesteps, sample_scenario_index): + def test_selection_propagation_with_scenarios( + self, sample_scenario_allocator, sample_timesteps, sample_scenario_index + ): """Test scenario selection propagation.""" # Add some time series ts1 = sample_scenario_allocator.add_time_series('series1', 42) @@ -679,12 +683,7 @@ def test_as_dataset_with_scenarios(self, sample_scenario_allocator): # Add some time series sample_scenario_allocator.add_time_series('scalar_series', 42) sample_scenario_allocator.add_time_series( - 'varying_series', - np.array([ - [10, 20, 30, 40, 50], - [15, 25, 35, 45, 55], - [5, 15, 25, 35, 45] - ]) + 'varying_series', np.array([[10, 20, 30, 40, 50], [15, 25, 35, 45, 55], [5, 15, 25, 35, 45]]) ) # Get dataset @@ -728,11 +727,7 @@ def test_update_time_series_with_scenarios(self, sample_scenario_allocator, samp assert np.all(ts.selected_data.values == 42) # Update with scenario-specific data - new_data = np.array([ - [1, 2, 3, 4, 5], - [6, 7, 8, 9, 10], - [11, 12, 13, 14, 15] - ]) + new_data = np.array([[1, 2, 3, 4, 5], [6, 7, 8, 9, 10], [11, 12, 13, 14, 15]]) sample_scenario_allocator.update_time_series('series', new_data) # Check update was applied From 379252330ebd264b09147de924d45ee35691938b Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 2 Apr 2025 16:36:40 +0200 Subject: [PATCH 005/448] Fix coords in constraints and variables --- flixopt/elements.py | 8 ++++---- flixopt/features.py | 14 ++++++++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/flixopt/elements.py b/flixopt/elements.py index a98edff9d..78dada129 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -361,7 +361,7 @@ def do_modeling(self): self._model.add_variables( lower=self.element.flow_hours_total_min if self.element.flow_hours_total_min is not None else -np.inf, upper=self.element.flow_hours_total_max if self.element.flow_hours_total_max is not None else np.inf, - coords=None, + coords=self._model.get_coords(time_dim=False), name=f'{self.label_full}|total_flow_hours', ), 'total_flow_hours', @@ -369,7 +369,7 @@ def do_modeling(self): self.add( self._model.add_constraints( - self.total_flow_hours == (self.flow_rate * self._model.hours_per_step).sum(), + self.total_flow_hours == (self.flow_rate * self._model.hours_per_step).sum('time'), name=f'{self.label_full}|total_flow_hours', ), 'total_flow_hours', @@ -399,7 +399,7 @@ def _create_bounds_for_load_factor(self): # eq: var_sumFlowHours <= size * dt_tot * load_factor_max if self.element.load_factor_max is not None: name_short = 'load_factor_max' - flow_hours_per_size_max = self._model.hours_per_step.sum() * self.element.load_factor_max + flow_hours_per_size_max = self._model.hours_per_step.sum('time') * self.element.load_factor_max size = self.element.size if self._investment is None else self._investment.size if self._investment is not None: @@ -414,7 +414,7 @@ def _create_bounds_for_load_factor(self): # eq: size * sum(dt)* load_factor_min <= var_sumFlowHours if self.element.load_factor_min is not None: name_short = 'load_factor_min' - flow_hours_per_size_min = self._model.hours_per_step.sum() * self.element.load_factor_min + flow_hours_per_size_min = self._model.hours_per_step.sum('time') * self.element.load_factor_min size = self.element.size if self._investment is None else self._investment.size if self._investment is not None: diff --git a/flixopt/features.py b/flixopt/features.py index a122a9dac..317c0d36f 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -254,6 +254,7 @@ def do_modeling(self): self._model.add_variables( lower=self.parameters.on_hours_total_min if self.parameters.on_hours_total_min is not None else 0, upper=self.parameters.on_hours_total_max if self.parameters.on_hours_total_max is not None else np.inf, + coords=self._model.get_coords(time_dim=False), name=f'{self.label_full}|on_hours_total', ), 'on_hours_total', @@ -261,7 +262,7 @@ def do_modeling(self): self.add( self._model.add_constraints( - self.total_on_hours == (self.on * self._model.hours_per_step).sum(), + self.total_on_hours == (self.on * self._model.hours_per_step).sum('time'), name=f'{self.label_full}|on_hours_total', ), 'on_hours_total', @@ -437,7 +438,7 @@ def _get_duration_in_hours( """ assert binary_variable is not None, f'Duration Variable of {self.label_full} must be defined to add constraints' - mega = self._model.hours_per_step.sum() + previous_duration + mega = self._model.hours_per_step.sum('time') + previous_duration if maximum_duration is not None: first_step_max: Scalar = maximum_duration.isel(time=0) @@ -582,7 +583,7 @@ def _add_switch_constraints(self): # eq: nrSwitchOn = sum(SwitchOn(t)) self.add( self._model.add_constraints( - self.switch_on_nr == self.switch_on.sum(), name=f'{self.label_full}|switch_on_nr' + self.switch_on_nr == self.switch_on.sum('time'), name=f'{self.label_full}|switch_on_nr' ), 'switch_on_nr', ) @@ -973,7 +974,12 @@ def __init__( def do_modeling(self): self.shares = { - effect: self.add(self._model.add_variables(coords=None, name=f'{self.label_full}|{effect}'), f'{effect}') + effect: self.add( + self._model.add_variables( + coords=self._model.get_coords(time_dim=False), + name=f'{self.label_full}|{effect}' + ), + f'{effect}') for effect in self._piecewise_shares } From dcc86f48ea887ede6e03378ab35da6d5e19083c4 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 5 Apr 2025 19:54:47 +0200 Subject: [PATCH 006/448] Feature/scenarios results (#220) # Improvements - Add scenarios to CalculationResults - Add Scenarios to all plotting options, defaulting to the first scenario available - Add the scenario to the plot title and filename if scenario is used - Improve `filter_solution`: Filter by time steps, scenarios and variable dims - Add options `mode` to `node_balance()` and corresponding plotting methods, to allow to get/plot the flow hours instead of the flow_rate. The plot_pie() always does that - Improve docstrings in general --- flixopt/io.py | 2 +- flixopt/results.py | 279 ++++++++++++++++++++++++++++++++++++--------- 2 files changed, 227 insertions(+), 54 deletions(-) diff --git a/flixopt/io.py b/flixopt/io.py index adaf52f55..1ef9578e5 100644 --- a/flixopt/io.py +++ b/flixopt/io.py @@ -44,7 +44,7 @@ def insert_dataarray(obj, ds: xr.Dataset): return [insert_dataarray(v, ds) for v in obj] elif isinstance(obj, str) and obj.startswith('::::'): da = ds[obj[4:]] - if da.isel(time=-1).isnull(): + if 'time' in da.dims and da.isel(time=-1).isnull().any().item(): return da.isel(time=slice(0, -1)) return da else: diff --git a/flixopt/results.py b/flixopt/results.py index d9eb5a654..bbf2dcd7a 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -161,6 +161,7 @@ def __init__( self.timesteps_extra = self.solution.indexes['time'] self.hours_per_timestep = TimeSeriesCollection.calculate_hours_per_timestep(self.timesteps_extra) + self.scenarios = self.solution.indexes['scenario'] if 'scenario' in self.solution.indexes else None def __getitem__(self, key: str) -> Union['ComponentResults', 'BusResults', 'EffectResults']: if key in self.components: @@ -196,19 +197,38 @@ def constraints(self) -> linopy.Constraints: return self.model.constraints def filter_solution( - self, variable_dims: Optional[Literal['scalar', 'time']] = None, element: Optional[str] = None + self, + variable_dims: Optional[Literal['scalar', 'time', 'scenario', 'timeonly']] = None, + element: Optional[str] = None, + timesteps: Optional[pd.DatetimeIndex] = None, + scenarios: Optional[pd.Index] = None, ) -> xr.Dataset: """ Filter the solution to a specific variable dimension and element. If no element is specified, all elements are included. Args: - variable_dims: The dimension of the variables to filter for. + variable_dims: The dimension of which to get variables from. + - 'scalar': Get scalar variables (without dimensions) + - 'time': Get time-dependent variables (with a time dimension) + - 'scenario': Get scenario-dependent variables (with ONLY a scenario dimension) + - 'timeonly': Get time-dependent variables (with ONLY a time dimension) element: The element to filter for. + timesteps: Optional time indexes to select. Can be: + - pd.DatetimeIndex: Multiple timesteps + - str/pd.Timestamp: Single timestep + Defaults to all available timesteps. + scenarios: Optional scenario indexes to select. Can be: + - pd.Index: Multiple scenarios + - str/int: Single scenario (int is treated as a label, not an index position) + Defaults to all available scenarios. """ - if element is not None: - return filter_dataset(self[element].solution, variable_dims) - return filter_dataset(self.solution, variable_dims) + return filter_dataset( + self.solution if element is None else self[element].solution, + variable_dims=variable_dims, + timesteps=timesteps, + scenarios=scenarios, + ) def plot_heatmap( self, @@ -219,10 +239,32 @@ def plot_heatmap( save: Union[bool, pathlib.Path] = False, show: bool = True, engine: plotting.PlottingEngine = 'plotly', + scenario: Optional[Union[str, int]] = None, ) -> Union[plotly.graph_objs.Figure, Tuple[plt.Figure, plt.Axes]]: + """ + Plots a heatmap of the solution of a variable. + + Args: + variable_name: The name of the variable to plot. + heatmap_timeframes: The timeframes to use for the heatmap. + heatmap_timesteps_per_frame: The timesteps per frame to use for the heatmap. + color_map: The color map to use for the heatmap. + save: Whether to save the plot or not. If a path is provided, the plot will be saved at that location. + show: Whether to show the plot or not. + engine: The engine to use for plotting. Can be either 'plotly' or 'matplotlib'. + scenario: The scenario to plot. Defaults to the first scenario. Has no effect without scenarios present + """ + dataarray = self.solution[variable_name] + + scenario_suffix = '' + if 'scenario' in dataarray.indexes: + chosen_scenario = scenario or self.scenarios[0] + dataarray = dataarray.sel(scenario=chosen_scenario).drop_vars('scenario') + scenario_suffix = f'--{chosen_scenario}' + return plot_heatmap( - dataarray=self.solution[variable_name], - name=variable_name, + dataarray=dataarray, + name=f'{variable_name}{scenario_suffix}', folder=self.folder, heatmap_timeframes=heatmap_timeframes, heatmap_timesteps_per_frame=heatmap_timesteps_per_frame, @@ -345,14 +387,37 @@ def constraints(self) -> linopy.Constraints: raise ValueError('The linopy model is not available.') return self._calculation_results.model.constraints[self._variable_names] - def filter_solution(self, variable_dims: Optional[Literal['scalar', 'time']] = None) -> xr.Dataset: + def filter_solution( + self, + variable_dims: Optional[Literal['scalar', 'time', 'scenario', 'timeonly']] = None, + timesteps: Optional[pd.DatetimeIndex] = None, + scenarios: Optional[pd.Index] = None, + ) -> xr.Dataset: """ - Filter the solution of the element by dimension. + Filter the solution to a specific variable dimension and element. + If no element is specified, all elements are included. Args: - variable_dims: The dimension of the variables to filter for. + variable_dims: The dimension of which to get variables from. + - 'scalar': Get scalar variables (without dimensions) + - 'time': Get time-dependent variables (with a time dimension) + - 'scenario': Get scenario-dependent variables (with ONLY a scenario dimension) + - 'timeonly': Get time-dependent variables (with ONLY a time dimension) + timesteps: Optional time indexes to select. Can be: + - pd.DatetimeIndex: Multiple timesteps + - str/pd.Timestamp: Single timestep + Defaults to all available timesteps. + scenarios: Optional scenario indexes to select. Can be: + - pd.Index: Multiple scenarios + - str/int: Single scenario (int is treated as a label, not an index position) + Defaults to all available scenarios. """ - return filter_dataset(self.solution, variable_dims) + return filter_dataset( + self.solution, + variable_dims=variable_dims, + timesteps=timesteps, + scenarios=scenarios, + ) class _NodeResults(_ElementResults): @@ -386,28 +451,46 @@ def plot_node_balance( show: bool = True, colors: plotting.ColorType = 'viridis', engine: plotting.PlottingEngine = 'plotly', + scenario: Optional[Union[str, int]] = None, + mode: Literal["flow_rate", "flow_hours"] = 'flow_rate', + drop_suffix: bool = True, ) -> Union[plotly.graph_objs.Figure, Tuple[plt.Figure, plt.Axes]]: """ Plots the node balance of the Component or Bus. Args: save: Whether to save the plot or not. If a path is provided, the plot will be saved at that location. show: Whether to show the plot or not. + colors: The colors to use for the plot. See `flixopt.plotting.ColorType` for options. engine: The engine to use for plotting. Can be either 'plotly' or 'matplotlib'. + scenario: The scenario to plot. Defaults to the first scenario. Has no effect without scenarios present + mode: The mode to use for the dataset. Can be 'flow_rate' or 'flow_hours'. + - 'flow_rate': Returns the flow_rates of the Node. + - 'flow_hours': Returns the flow_hours of the Node. [flow_hours(t) = flow_rate(t) * dt(t)]. Renames suffixes to |flow_hours. + drop_suffix: Whether to drop the suffix from the variable names. """ + ds = self.node_balance(with_last_timestep=True, mode=mode, drop_suffix=drop_suffix) + + title = f'{self.label} (flow rates)' if mode == 'flow_rate' else f'{self.label} (flow hours)' + + if 'scenario' in ds.indexes: + chosen_scenario = scenario or self._calculation_results.scenarios[0] + ds = ds.sel(scenario=chosen_scenario).drop_vars('scenario') + title = f'{title} - {chosen_scenario}' + if engine == 'plotly': figure_like = plotting.with_plotly( - self.node_balance(with_last_timestep=True).to_dataframe(), + ds.to_dataframe(), colors=colors, mode='area', - title=f'Flow rates of {self.label}', + title=title, ) default_filetype = '.html' elif engine == 'matplotlib': figure_like = plotting.with_matplotlib( - self.node_balance(with_last_timestep=True).to_dataframe(), + ds.to_dataframe(), colors=colors, mode='bar', - title=f'Flow rates of {self.label}', + title=title, ) default_filetype = '.png' else: @@ -415,7 +498,7 @@ def plot_node_balance( return plotting.export_figure( figure_like=figure_like, - default_path=self._calculation_results.folder / f'{self.label} (flow rates)', + default_path=self._calculation_results.folder / title, default_filetype=default_filetype, user_path=None if isinstance(save, bool) else pathlib.Path(save), show=show, @@ -430,6 +513,8 @@ def plot_node_balance_pie( save: Union[bool, pathlib.Path] = False, show: bool = True, engine: plotting.PlottingEngine = 'plotly', + scenario: Optional[Union[str, int]] = None, + drop_suffix: bool = True, ) -> plotly.graph_objects.Figure: """ Plots a pie chart of the flow hours of the inputs and outputs of buses or components. @@ -441,32 +526,45 @@ def plot_node_balance_pie( save: Whether to save the figure. show: Whether to show the figure. engine: Plotting engine to use. Only 'plotly' is implemented atm. + scenario: If scenarios are present: The scenario to plot. If None, the first scenario is used. + drop_suffix: Whether to drop the suffix from the variable names. """ - inputs = ( - sanitize_dataset( - ds=self.solution[self.inputs], - threshold=1e-5, - drop_small_vars=True, - zero_small_values=True, - ) - * self._calculation_results.hours_per_timestep + inputs = sanitize_dataset( + ds=self.solution[self.inputs], + threshold=1e-5, + drop_small_vars=True, + zero_small_values=True, ) - outputs = ( - sanitize_dataset( - ds=self.solution[self.outputs], - threshold=1e-5, - drop_small_vars=True, - zero_small_values=True, - ) - * self._calculation_results.hours_per_timestep + outputs = sanitize_dataset( + ds=self.solution[self.outputs], + threshold=1e-5, + drop_small_vars=True, + zero_small_values=True, ) + inputs = inputs.sum('time') + outputs = outputs.sum('time') + + title = f'{self.label} (total flow hours)' + + if 'scenario' in inputs.indexes: + chosen_scenario = scenario or self._calculation_results.scenarios[0] + inputs = inputs.sel(scenario=chosen_scenario).drop_vars('scenario') + outputs = outputs.sel(scenario=chosen_scenario).drop_vars('scenario') + title = f'{title} - {chosen_scenario}' + + if drop_suffix: + inputs = inputs.rename_vars({var: var.split('|flow_rate')[0] for var in inputs}) + outputs = outputs.rename_vars({var: var.split('|flow_rate')[0] for var in outputs}) + else: + inputs = inputs.rename_vars({var: var.replace('flow_rate', 'flow_hours') for var in inputs}) + outputs = outputs.rename_vars({var: var.replace('flow_rate', 'flow_hours') for var in outputs}) if engine == 'plotly': figure_like = plotting.dual_pie_with_plotly( - inputs.to_dataframe().sum(), - outputs.to_dataframe().sum(), + data_left=inputs.to_pandas(), + data_right=outputs.to_pandas(), colors=colors, - title=f'Flow hours of {self.label}', + title=title, text_info=text_info, subtitles=('Inputs', 'Outputs'), legend_title='Flows', @@ -476,10 +574,10 @@ def plot_node_balance_pie( elif engine == 'matplotlib': logger.debug('Parameter text_info is not supported for matplotlib') figure_like = plotting.dual_pie_with_matplotlib( - inputs.to_dataframe().sum(), - outputs.to_dataframe().sum(), + data_left=inputs.to_pandas(), + data_right=outputs.to_pandas(), colors=colors, - title=f'Total flow hours of {self.label}', + title=title, subtitles=('Inputs', 'Outputs'), legend_title='Flows', lower_percentage_group=lower_percentage_group, @@ -490,7 +588,7 @@ def plot_node_balance_pie( return plotting.export_figure( figure_like=figure_like, - default_path=self._calculation_results.folder / f'{self.label} (total flow hours)', + default_path=self._calculation_results.folder / title, default_filetype=default_filetype, user_path=None if isinstance(save, bool) else pathlib.Path(save), show=show, @@ -503,9 +601,29 @@ def node_balance( negate_outputs: bool = False, threshold: Optional[float] = 1e-5, with_last_timestep: bool = False, + mode: Literal["flow_rate", "flow_hours"] = 'flow_rate', + drop_suffix: bool = False, ) -> xr.Dataset: + """ + Returns a dataset with the node balance of the Component or Bus. + Args: + negate_inputs: Whether to negate the input flow_rates of the Node. + negate_outputs: Whether to negate the output flow_rates of the Node. + threshold: The threshold for small values. Variables with all values below the threshold are dropped. + with_last_timestep: Whether to include the last timestep in the dataset. + mode: The mode to use for the dataset. Can be 'flow_rate' or 'flow_hours'. + - 'flow_rate': Returns the flow_rates of the Node. + - 'flow_hours': Returns the flow_hours of the Node. [flow_hours(t) = flow_rate(t) * dt(t)]. Renames suffixes to |flow_hours. + drop_suffix: Whether to drop the suffix from the variable names. + """ + ds = self.solution[self.inputs + self.outputs] + if drop_suffix: + ds = ds.rename_vars({var: var.split('|flow_hours')[0] for var in ds.data_vars}) + if mode == 'flow_hours': + ds = ds * self._calculation_results.hours_per_timestep + ds = ds.rename_vars({var: var.replace('flow_rate', 'flow_hours') for var in ds.data_vars}) return sanitize_dataset( - ds=self.solution[self.inputs + self.outputs], + ds=ds, threshold=threshold, timesteps=self._calculation_results.timesteps_extra if with_last_timestep else None, negate=( @@ -548,6 +666,7 @@ def plot_charge_state( show: bool = True, colors: plotting.ColorType = 'viridis', engine: plotting.PlottingEngine = 'plotly', + scenario: Optional[Union[str, int]] = None, ) -> plotly.graph_objs.Figure: """ Plots the charge state of a Storage. @@ -556,6 +675,7 @@ def plot_charge_state( show: Whether to show the plot or not. colors: The c engine: Plotting engine to use. Only 'plotly' is implemented atm. + scenario: The scenario to plot. Defaults to the first scenario. Has no effect without scenarios present Raises: ValueError: If the Component is not a Storage. @@ -568,16 +688,26 @@ def plot_charge_state( if not self.is_storage: raise ValueError(f'Cant plot charge_state. "{self.label}" is not a storage') + ds = self.node_balance(with_last_timestep=True) + charge_state = self.charge_state + + scenario_suffix = '' + if 'scenario' in ds.indexes: + chosen_scenario = scenario or self._calculation_results.scenarios[0] + ds = ds.sel(scenario=chosen_scenario).drop_vars('scenario') + charge_state = charge_state.sel(scenario=chosen_scenario).drop_vars('scenario') + scenario_suffix = f'--{chosen_scenario}' + fig = plotting.with_plotly( - self.node_balance(with_last_timestep=True).to_dataframe(), + ds.to_dataframe(), colors=colors, mode='area', - title=f'Operation Balance of {self.label}', + title=f'Operation Balance of {self.label}{scenario_suffix}', ) # TODO: Use colors for charge state? - charge_state = self.charge_state.to_dataframe() + charge_state = charge_state.to_dataframe() fig.add_trace( plotly.graph_objs.Scatter( x=charge_state.index, y=charge_state.values.flatten(), mode='lines', name=self._charge_state @@ -586,7 +716,7 @@ def plot_charge_state( return plotting.export_figure( fig, - default_path=self._calculation_results.folder / f'{self.label} (charge state)', + default_path=self._calculation_results.folder / f'{self.label} (charge state){scenario_suffix}', default_filetype='.html', user_path=None if isinstance(save, bool) else pathlib.Path(save), show=show, @@ -878,21 +1008,64 @@ def sanitize_dataset( def filter_dataset( ds: xr.Dataset, - variable_dims: Optional[Literal['scalar', 'time']] = None, + variable_dims: Optional[Literal['scalar', 'time', 'scenario', 'timeonly']] = None, + timesteps: Optional[Union[pd.DatetimeIndex, str, pd.Timestamp]] = None, + scenarios: Optional[Union[pd.Index, str, int]] = None, ) -> xr.Dataset: """ - Filters a dataset by its dimensions. + Filters a dataset by its dimensions and optionally selects specific indexes. Args: ds: The dataset to filter. - variable_dims: The dimension of the variables to filter for. + variable_dims: The dimension of which to get variables from. + - 'scalar': Get scalar variables (without dimensions) + - 'time': Get time-dependent variables (with a time dimension) + - 'scenario': Get scenario-dependent variables (with ONLY a scenario dimension) + - 'timeonly': Get time-dependent variables (with ONLY a time dimension) + timesteps: Optional time indexes to select. Can be: + - pd.DatetimeIndex: Multiple timesteps + - str/pd.Timestamp: Single timestep + Defaults to all available timesteps. + scenarios: Optional scenario indexes to select. Can be: + - pd.Index: Multiple scenarios + - str/int: Single scenario (int is treated as a label, not an index position) + Defaults to all available scenarios. + + Returns: + Filtered dataset with specified variables and indexes. """ + # Return the full dataset if all dimension types are included if variable_dims is None: - return ds - - if variable_dims == 'scalar': - return ds[[name for name, da in ds.data_vars.items() if len(da.dims) == 0]] + pass + elif variable_dims == 'scalar': + ds = ds[[v for v in ds.data_vars if not ds[v].dims]] elif variable_dims == 'time': - return ds[[name for name, da in ds.data_vars.items() if 'time' in da.dims]] + ds = ds[[v for v in ds.data_vars if 'time' in ds[v].dims]] + elif variable_dims == 'scenario': + ds = ds[[v for v in ds.data_vars if ds[v].dims == ('scenario',)]] + elif variable_dims == 'timeonly': + ds = ds[[v for v in ds.data_vars if ds[v].dims == ('time',)]] else: - raise ValueError(f'Not allowed value for "filter_dataset()": {variable_dims=}') + raise ValueError(f'Unknown variable_dims "{variable_dims}" for filter_dataset') + + # Handle time selection if needed + if timesteps is not None and 'time' in ds.dims: + try: + ds = ds.sel(time=timesteps) + except KeyError: + available_times = set(ds.indexes['time']) + requested_times = set([timesteps]) if not isinstance(timesteps, pd.Index) else set(timesteps) + missing_times = requested_times - available_times + raise ValueError(f'Timesteps not found in dataset: {missing_times}. Available times: {available_times}') + + # Handle scenario selection if needed + if scenarios is not None and 'scenario' in ds.dims: + try: + ds = ds.sel(scenario=scenarios) + except KeyError: + available_scenarios = set(ds.indexes['scenario']) + requested_scenarios = set([scenarios]) if not isinstance(scenarios, pd.Index) else set(scenarios) + missing_scenarios = requested_scenarios - available_scenarios + raise ValueError(f'Scenarios not found in dataset: {missing_scenarios}. Available scenarios: {available_scenarios}') + + return ds From bd1d2b6c076bb4b956a024cd767daf7f693aaca7 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 5 Apr 2025 20:10:09 +0200 Subject: [PATCH 007/448] ruff check and format --- flixopt/features.py | 6 +++--- flixopt/results.py | 16 ++++++++++------ 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index 317c0d36f..e12c0b20f 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -976,10 +976,10 @@ def do_modeling(self): self.shares = { effect: self.add( self._model.add_variables( - coords=self._model.get_coords(time_dim=False), - name=f'{self.label_full}|{effect}' + coords=self._model.get_coords(time_dim=False), name=f'{self.label_full}|{effect}' ), - f'{effect}') + f'{effect}', + ) for effect in self._piecewise_shares } diff --git a/flixopt/results.py b/flixopt/results.py index bbf2dcd7a..757adb790 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -452,7 +452,7 @@ def plot_node_balance( colors: plotting.ColorType = 'viridis', engine: plotting.PlottingEngine = 'plotly', scenario: Optional[Union[str, int]] = None, - mode: Literal["flow_rate", "flow_hours"] = 'flow_rate', + mode: Literal['flow_rate', 'flow_hours'] = 'flow_rate', drop_suffix: bool = True, ) -> Union[plotly.graph_objs.Figure, Tuple[plt.Figure, plt.Axes]]: """ @@ -601,7 +601,7 @@ def node_balance( negate_outputs: bool = False, threshold: Optional[float] = 1e-5, with_last_timestep: bool = False, - mode: Literal["flow_rate", "flow_hours"] = 'flow_rate', + mode: Literal['flow_rate', 'flow_hours'] = 'flow_rate', drop_suffix: bool = False, ) -> xr.Dataset: """ @@ -1052,20 +1052,24 @@ def filter_dataset( if timesteps is not None and 'time' in ds.dims: try: ds = ds.sel(time=timesteps) - except KeyError: + except KeyError as e: available_times = set(ds.indexes['time']) requested_times = set([timesteps]) if not isinstance(timesteps, pd.Index) else set(timesteps) missing_times = requested_times - available_times - raise ValueError(f'Timesteps not found in dataset: {missing_times}. Available times: {available_times}') + raise ValueError( + f'Timesteps not found in dataset: {missing_times}. Available times: {available_times}' + ) from e # Handle scenario selection if needed if scenarios is not None and 'scenario' in ds.dims: try: ds = ds.sel(scenario=scenarios) - except KeyError: + except KeyError as e: available_scenarios = set(ds.indexes['scenario']) requested_scenarios = set([scenarios]) if not isinstance(scenarios, pd.Index) else set(scenarios) missing_scenarios = requested_scenarios - available_scenarios - raise ValueError(f'Scenarios not found in dataset: {missing_scenarios}. Available scenarios: {available_scenarios}') + raise ValueError( + f'Scenarios not found in dataset: {missing_scenarios}. Available scenarios: {available_scenarios}' + ) from e return ds From 28a46dc8ce9060237b1f2df416210521165d4800 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 5 Apr 2025 20:48:01 +0200 Subject: [PATCH 008/448] Feature/scenarios dims order (#219) Make time to always be the first dimension, improving output and consistency across the code --- examples/04_Scenarios/scenario_example.py | 3 ++- flixopt/core.py | 14 +++++++------- flixopt/results.py | 20 ++++++++++++++++---- flixopt/structure.py | 2 +- 4 files changed, 26 insertions(+), 13 deletions(-) diff --git a/examples/04_Scenarios/scenario_example.py b/examples/04_Scenarios/scenario_example.py index a004d1851..8e3349a4a 100644 --- a/examples/04_Scenarios/scenario_example.py +++ b/examples/04_Scenarios/scenario_example.py @@ -11,7 +11,8 @@ if __name__ == '__main__': # --- Create Time Series Data --- # Heat demand profile (e.g., kW) over time and corresponding power prices - heat_demand_per_h = np.array([[30, 0, 90, 110, 110, 20, 20, 20, 20], [30, 0, 100, 118, 125, 20, 20, 20, 20]]) + heat_demand_per_h = np.array([[30, 0, 90, 110, 110, 20, 20, 20, 20], + [30, 0, 100, 118, 125, 20, 20, 20, 20]]).T power_prices = np.ones(9) * 0.08 # Create datetime array starting from '2020-01-01' for the given time period diff --git a/flixopt/core.py b/flixopt/core.py index 68d1ddaad..386a1d873 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -146,14 +146,14 @@ def _prepare_dimensions( coords = {} dims = [] - if scenarios is not None: - coords['scenario'] = scenarios - dims.append('scenario') - if timesteps is not None: coords['time'] = timesteps dims.append('time') + if scenarios is not None: + coords['scenario'] = scenarios + dims.append('scenario') + return coords, tuple(dims) @staticmethod @@ -340,18 +340,18 @@ def _convert_ndarray_two_dims(data: np.ndarray, coords: Dict[str, pd.Index], dim # For 1D array, create 2D array based on which dimension it matches if data.shape[0] == time_length: # Broadcast across scenarios - values = np.tile(data, (scenario_length, 1)) + values = np.repeat(data[:, np.newaxis], scenario_length, axis=1) return xr.DataArray(values, coords=coords, dims=dims) elif data.shape[0] == scenario_length: # Broadcast across time - values = np.repeat(data[:, np.newaxis], time_length, axis=1) + values = np.tile(data, (time_length, 1)) return xr.DataArray(values, coords=coords, dims=dims) else: raise ConversionError(f"1D array length {data.shape[0]} doesn't match either dimension") elif data.ndim == 2: # For 2D array, shape must match dimensions - expected_shape = (scenario_length, time_length) + expected_shape = (time_length, scenario_length) if data.shape != expected_shape: raise ConversionError(f"2D array shape {data.shape} doesn't match expected shape {expected_shape}") return xr.DataArray(data, coords=coords, dims=dims) diff --git a/flixopt/results.py b/flixopt/results.py index 757adb790..280f20d0d 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -198,7 +198,7 @@ def constraints(self) -> linopy.Constraints: def filter_solution( self, - variable_dims: Optional[Literal['scalar', 'time', 'scenario', 'timeonly']] = None, + variable_dims: Optional[Literal['scalar', 'time', 'scenario', 'timeonly', 'scenarioonly']] = None, element: Optional[str] = None, timesteps: Optional[pd.DatetimeIndex] = None, scenarios: Optional[pd.Index] = None, @@ -213,6 +213,7 @@ def filter_solution( - 'time': Get time-dependent variables (with a time dimension) - 'scenario': Get scenario-dependent variables (with ONLY a scenario dimension) - 'timeonly': Get time-dependent variables (with ONLY a time dimension) + - 'scenarioonly': Get scenario-dependent variables (with ONLY a scenario dimension) element: The element to filter for. timesteps: Optional time indexes to select. Can be: - pd.DatetimeIndex: Multiple timesteps @@ -389,7 +390,7 @@ def constraints(self) -> linopy.Constraints: def filter_solution( self, - variable_dims: Optional[Literal['scalar', 'time', 'scenario', 'timeonly']] = None, + variable_dims: Optional[Literal['scalar', 'time', 'scenario', 'timeonly', 'scenarioonly']] = None, timesteps: Optional[pd.DatetimeIndex] = None, scenarios: Optional[pd.Index] = None, ) -> xr.Dataset: @@ -403,6 +404,7 @@ def filter_solution( - 'time': Get time-dependent variables (with a time dimension) - 'scenario': Get scenario-dependent variables (with ONLY a scenario dimension) - 'timeonly': Get time-dependent variables (with ONLY a time dimension) + - 'scenarioonly': Get scenario-dependent variables (with ONLY a scenario dimension) timesteps: Optional time indexes to select. Can be: - pd.DatetimeIndex: Multiple timesteps - str/pd.Timestamp: Single timestep @@ -559,6 +561,13 @@ def plot_node_balance_pie( inputs = inputs.rename_vars({var: var.replace('flow_rate', 'flow_hours') for var in inputs}) outputs = outputs.rename_vars({var: var.replace('flow_rate', 'flow_hours') for var in outputs}) + scenario_suffix = '' + if 'scenario' in inputs.indexes: + chosen_scenario = scenario or self._calculation_results.scenarios[0] + inputs = inputs.sel(scenario=chosen_scenario).drop_vars('scenario') + outputs = outputs.sel(scenario=chosen_scenario).drop_vars('scenario') + scenario_suffix = f'--{chosen_scenario}' + if engine == 'plotly': figure_like = plotting.dual_pie_with_plotly( data_left=inputs.to_pandas(), @@ -1008,7 +1017,7 @@ def sanitize_dataset( def filter_dataset( ds: xr.Dataset, - variable_dims: Optional[Literal['scalar', 'time', 'scenario', 'timeonly']] = None, + variable_dims: Optional[Literal['scalar', 'time', 'scenario', 'timeonly', 'scenarioonly']] = None, timesteps: Optional[Union[pd.DatetimeIndex, str, pd.Timestamp]] = None, scenarios: Optional[Union[pd.Index, str, int]] = None, ) -> xr.Dataset: @@ -1022,6 +1031,7 @@ def filter_dataset( - 'time': Get time-dependent variables (with a time dimension) - 'scenario': Get scenario-dependent variables (with ONLY a scenario dimension) - 'timeonly': Get time-dependent variables (with ONLY a time dimension) + - 'scenarioonly': Get scenario-dependent variables (with ONLY a scenario dimension) timesteps: Optional time indexes to select. Can be: - pd.DatetimeIndex: Multiple timesteps - str/pd.Timestamp: Single timestep @@ -1042,9 +1052,11 @@ def filter_dataset( elif variable_dims == 'time': ds = ds[[v for v in ds.data_vars if 'time' in ds[v].dims]] elif variable_dims == 'scenario': - ds = ds[[v for v in ds.data_vars if ds[v].dims == ('scenario',)]] + ds = ds[[v for v in ds.data_vars if 'scenario' in ds[v].dims]] elif variable_dims == 'timeonly': ds = ds[[v for v in ds.data_vars if ds[v].dims == ('time',)]] + elif variable_dims == 'scenarioonly': + ds = ds[[v for v in ds.data_vars if ds[v].dims == ('scenario',)]] else: raise ValueError(f'Unknown variable_dims "{variable_dims}" for filter_dataset') diff --git a/flixopt/structure.py b/flixopt/structure.py index 37b02b122..7282d3b7c 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -122,7 +122,7 @@ def get_coords( if scenario_dim and time_dim: if scenarios is None: return (timesteps,) - return scenarios, timesteps + return timesteps, scenarios if scenario_dim and not time_dim: if scenarios is None: From 479c1eb4f1c62cba5cfeaa689f258c9808144a0d Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 9 Apr 2025 09:44:02 +0200 Subject: [PATCH 009/448] Bugfix main results --- flixopt/calculation.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/flixopt/calculation.py b/flixopt/calculation.py index 962d2c95f..5329bc0f9 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -105,15 +105,15 @@ def main_results(self) -> Dict[str, Union[Scalar, Dict]]: 'Buses with excess': [ { bus.label_full: { - 'input': np.sum(bus.model.excess_input.solution.values), - 'output': np.sum(bus.model.excess_output.solution.values), + 'input': bus.model.excess_input.solution.sum('time'), + 'output': bus.model.excess_output.solution.sum('time'), } } for bus in self.flow_system.buses.values() if bus.with_excess and ( - np.sum(bus.model.excess_input.solution.values) > 1e-3 - or np.sum(bus.model.excess_output.solution.values) > 1e-3 + bus.model.excess_input.solution.sum() > 1e-3 + or bus.model.excess_output.solution.sum() > 1e-3 ) ], } From a611672c39e7a0a824ee421e4c72af6d493c8c6b Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 9 Apr 2025 09:47:58 +0200 Subject: [PATCH 010/448] Remove code duplicate --- flixopt/results.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/flixopt/results.py b/flixopt/results.py index 280f20d0d..ae54b9e2e 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -561,13 +561,6 @@ def plot_node_balance_pie( inputs = inputs.rename_vars({var: var.replace('flow_rate', 'flow_hours') for var in inputs}) outputs = outputs.rename_vars({var: var.replace('flow_rate', 'flow_hours') for var in outputs}) - scenario_suffix = '' - if 'scenario' in inputs.indexes: - chosen_scenario = scenario or self._calculation_results.scenarios[0] - inputs = inputs.sel(scenario=chosen_scenario).drop_vars('scenario') - outputs = outputs.sel(scenario=chosen_scenario).drop_vars('scenario') - scenario_suffix = f'--{chosen_scenario}' - if engine == 'plotly': figure_like = plotting.dual_pie_with_plotly( data_left=inputs.to_pandas(), From 9a8724e94c2d53a0ea9a13fce5bd14474087ffc8 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 9 Apr 2025 09:57:37 +0200 Subject: [PATCH 011/448] Feature/scenarios invest (#227) Add different modes to handle the size per scenario. --- .../Mathematical Notation/Investment.md | 115 ++++++++++++++++++ flixopt/calculation.py | 4 +- flixopt/components.py | 4 +- flixopt/elements.py | 4 +- flixopt/features.py | 88 +++++++++++++- flixopt/interface.py | 56 +++++---- 6 files changed, 235 insertions(+), 36 deletions(-) create mode 100644 docs/user-guide/Mathematical Notation/Investment.md diff --git a/docs/user-guide/Mathematical Notation/Investment.md b/docs/user-guide/Mathematical Notation/Investment.md new file mode 100644 index 000000000..64467261d --- /dev/null +++ b/docs/user-guide/Mathematical Notation/Investment.md @@ -0,0 +1,115 @@ +# Investments + +## Current state +$$ +\beta_{\text{invest}} \cdot \text{max}(\epsilon, \text V^{\text L}) \leq V \leq \beta_{\text{invest}} \cdot \text V^{\text U} +$$ +With: +- $V$ = size +- $V^{\text L}$ = minimum size +- $V^{\text U}$ = maximum size +- $\epsilon$ = epsilon, a small number (such as $1e^{-5}$) +- $\beta_{invest} \in {0,1}$ = wether the size is invested or not + +_Please edit the use cases as needed_ +## Quickfix 1: Optimize the single best size overall +### Single variable +This is already possible and should be, as this is a needed use case +An additional factor to when the size is actually available might me practical (Which indicates the (fixed) time of investment) +## Math +$$ +V(p) = V * a(p) +$$ +with: +- $V$ = size +- $a(p)$ = factor for availlability per period + +Factor $a(p)$ is simply multiplied with relative minimum or maximum(t). This is already possible by doing this yourself. +Effectively, the relative minimum or maximum are altered before using the same constraiints as before. +THis might lead to some issues regariding minimum_load factor, or others, as the size is not 0 in a scenario where the component cant produce. +**Therefore this might not be the best choice. See (#Variable per Scenario) + +## Variable per Scenario +- **size** and **invest** as a variable per period $V(s)$ and $\beta_{invest}(s)$ +- with scenario $s \in S$ + +### Usecase 1: Optimize the size for each Scenario independently +Restrictions are seperatly for each scenario +No changes needed. This could be the default behaviour. + +### Usecase 2: Optimize ONE size for ALL scenarios +The size is the same globally, but not a scalar, but a variable per scenario $V(s)$ +#### 2a: The same size in all scenarios +$$ +V(s) = V(s') \quad \forall s,s' \in S +$$ + +With: +- $V(s)$ and $V(s')$ = size +- $S$ = set of scenarios + +#### 2b: The same size, but can be 0 prior to the first increment +- Find the Optimal time of investment. +- Force an investment in a certain scenario (parameter optional as a list/array ob booleans) +- Combine optional and minimum/maximum size to force an investment inside a range if scenarios + +$$ +\beta_{\text{invest}}(s) \leq \beta_{\text{invest}}(s+1) \quad \forall s \in \{1,2,\ldots,S-1\} +$$ + +$$ +V(s') - V(s) \leq M \cdot (2 - \beta_{\text{invest}}(s) - \beta_{\text{invest}}(s')) \quad \forall s, s' \in S +$$ +$$ +V(s') - V(s) \geq M \cdot (2 - \beta_{\text{invest}}(s) - \beta_{\text{invest}}(s')) \quad \forall s, s' \in S +$$ + +This could be the default behaviour. (which would be consistent with other variables) + + +### Switch + +$$ +\begin{aligned} +& \text{SWITCH}_s \in \{0,1\} \quad \forall s \in \{1,2,\ldots,S\} \\ +& \sum_{s=1}^{S} \text{SWITCH}_s = 1 \\ +& \beta_{\text{invest}}(s) = \sum_{s'=1}^{s} \text{SWITCH}_{s'} \quad \forall s \in \{1,2,\ldots,S\} \\ +\end{aligned} +$$ + +$$ +\begin{aligned} +& V(s) \leq V_{\text{actual}} \quad \forall s \in \{1,2,\ldots,S\} \\ +& V(s) \geq V_{\text{actual}} - M \cdot (1 - \beta_{\text{invest}}(s)) \quad \forall s \in \{1,2,\ldots,S\} +\end{aligned} +$$ + + + + +### Usecase 3: Find the best scenario to increment the size (Timing of the investment) +The size can only increment once (based on a starting point). This allows to optimize the timing of an investment. +#### Math +Treat $\beta_{invest}$ like an ON/OFF variable, and introduce a SwitchOn, that can only be active once. + +*Thoughts:* +- Treating $\beta_{invest}$ like an ON/OFF variable suggest using the already presentconstraints linked to On/OffModel +- The timing could be constraint to be first in scenario x, or last in scenario y +- Restrict the number of consecutive scenarios +THis might needs the OnOffModel to be more generic (HOURS). Further, the span between scenarios needs to be weighted (like dt_in_hours), or the scenarios need to be measureable (integers) + + +### Others + +#### Usecase 4: Only increase/decrease the size +Start from a certain size. For each scenario, the size can increase, but never decrease. (Or the other way around). +This would mean that a size expansion is possible, + +#### Usecase 5: Restrict the increment in size per scenario +Restrict how much the size can increase/decrease for in scenario, based on the prior scenario. + + + + + +Many more are possible diff --git a/flixopt/calculation.py b/flixopt/calculation.py index 5329bc0f9..2024739ea 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -93,13 +93,13 @@ def main_results(self) -> Dict[str, Union[Scalar, Dict]]: model.label_of_element: model.size.solution for component in self.flow_system.components.values() for model in component.model.all_sub_models - if isinstance(model, InvestmentModel) and model.size.solution >= CONFIG.modeling.EPSILON + if isinstance(model, InvestmentModel) and model.size.solution.max() >= CONFIG.modeling.EPSILON }, 'Not invested': { model.label_of_element: model.size.solution for component in self.flow_system.components.values() for model in component.model.all_sub_models - if isinstance(model, InvestmentModel) and model.size.solution < CONFIG.modeling.EPSILON + if isinstance(model, InvestmentModel) and model.size.solution.max() < CONFIG.modeling.EPSILON }, }, 'Buses with excess': [ diff --git a/flixopt/components.py b/flixopt/components.py index 69b0fe47a..598ff06ab 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -578,8 +578,8 @@ def absolute_charge_state_bounds(self) -> Tuple[TimestepData, TimestepData]: @property def relative_charge_state_bounds(self) -> Tuple[TimestepData, TimestepData]: return ( - self.element.relative_minimum_charge_state.selected_data, - self.element.relative_maximum_charge_state.selected_data, + self.element.relative_minimum_charge_state, + self.element.relative_maximum_charge_state, ) diff --git a/flixopt/elements.py b/flixopt/elements.py index 78dada129..aa1c8e69b 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -434,8 +434,8 @@ def absolute_flow_rate_bounds(self) -> Tuple[TimestepData, TimestepData]: if not isinstance(size, InvestParameters): return relative_minimum * size, relative_maximum * size if size.fixed_size is not None: - return relative_minimum * size.fixed_size, relative_maximum * size.fixed_size - return relative_minimum * size.minimum_size, relative_maximum * size.maximum_size + return size.fixed_size * relative_minimum, size.fixed_size * relative_maximum + return size.minimum_size * relative_minimum, size.maximum_size * relative_maximum @property def relative_flow_rate_bounds(self) -> Tuple[TimestepData, TimestepData]: diff --git a/flixopt/features.py b/flixopt/features.py index e12c0b20f..3d2984393 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -33,6 +33,7 @@ def __init__( super().__init__(model, label_of_element, label) self.size: Optional[Union[Scalar, linopy.Variable]] = None self.is_invested: Optional[linopy.Variable] = None + self.scenario_of_investment: Optional[linopy.Variable] = None self.piecewise_effects: Optional[PiecewiseEffectsModel] = None @@ -45,16 +46,18 @@ def do_modeling(self): if self.parameters.fixed_size and not self.parameters.optional: self.size = self.add( self._model.add_variables( - lower=self.parameters.fixed_size, upper=self.parameters.fixed_size, name=f'{self.label_full}|size' + lower=self.parameters.fixed_size, upper=self.parameters.fixed_size, name=f'{self.label_full}|size', + coords=self._model.get_coords(time_dim=False), ), 'size', ) else: self.size = self.add( self._model.add_variables( - lower=0 if self.parameters.optional else self.parameters.minimum_size, - upper=self.parameters.maximum_size, + lower=0 if self.parameters.optional else self.parameters.minimum_size*1, + upper=self.parameters.maximum_size*1, name=f'{self.label_full}|size', + coords=self._model.get_coords(time_dim=False), ), 'size', ) @@ -62,11 +65,19 @@ def do_modeling(self): # Optional if self.parameters.optional: self.is_invested = self.add( - self._model.add_variables(binary=True, name=f'{self.label_full}|is_invested'), 'is_invested' + self._model.add_variables( + binary=True, + name=f'{self.label_full}|is_invested', + coords=self._model.get_coords(time_dim=False), + ), + 'is_invested', ) self._create_bounds_for_optional_investment() + if self._model.time_series_collection.scenarios is not None: + self._create_bounds_for_scenarios() + # Bounds for defining variable self._create_bounds_for_defining_variable() @@ -181,7 +192,7 @@ def _create_bounds_for_defining_variable(self): # ... mit mega = relative_maximum * maximum_size # äquivalent zu:. # eq: - defining_variable(t) + mega * On(t) + size * relative_minimum(t) <= + mega - mega = lb_relative * self.parameters.maximum_size + mega = self.parameters.maximum_size * lb_relative on = self._on_variable self.add( self._model.add_constraints( @@ -191,6 +202,73 @@ def _create_bounds_for_defining_variable(self): ) # anmerkung: Glg bei Spezialfall relative_minimum = 0 redundant zu OnOff ?? + def _create_bounds_for_scenarios(self): + if self.parameters.size_per_scenario == 'equal': + self.add( + self._model.add_constraints( + self.size.isel(scenario=slice(None, -1)) == self.size.isel(scenario=slice(1, None)), + name=f'{self.label_full}|equalize_size_per_scenario', + ), + 'equalize_size_per_scenario', + ) + elif self.parameters.size_per_scenario == 'increment_once': + if not self.parameters.optional: + raise ValueError('Increment once can only be used if the Investment is optional') + + self.scenario_of_investment = self.add( + self._model.add_variables( + binary=True, + name=f'{self.label_full}|scenario_of_investment', + coords=self._model.get_coords(time_dim=False), + ), + 'scenario_of_investment', + ) + + # eq: scenario_of_investment(t) = is_invested(t) - is_invested(t-1) + self.add( + self._model.add_constraints( + self.scenario_of_investment.isel(scenario=slice(1, None)) + == self.is_invested.isel(scenario=slice(1, None)) - self.is_invested.isel(scenario=slice(None, -1)), + name=f'{self.label_full}|scenario_of_investment', + ), + 'scenario_of_investment', + ) + + # eq: scenario_of_investment(t=0) = is_invested(t=0) + self.add( + self._model.add_constraints( + self.scenario_of_investment.isel(scenario=0) + == self.is_invested.isel(scenario=0), + name=f'{self.label_full}|initial_scenario_of_investment', + ), + 'initial_scenario_of_investment', + ) + + big_m = self.parameters.maximum_size.isel(scenario=slice(1, None)) + + self.add( + self._model.add_constraints( + self.size.isel(scenario=slice(1, None)) - self.size.isel(scenario=slice(None, -1)) + <= self.scenario_of_investment.isel(scenario=slice(1, None)) * big_m, + name=f'{self.label_full}|invest_once_1a', + ), + 'invest_once_1a', + ) + + self.add( + self._model.add_constraints( + self.size.isel(scenario=slice(1, None)) - self.size.isel(scenario=slice(None, -1)) + >= self.scenario_of_investment.isel(scenario=slice(1, None)) * big_m, + name=f'{self.label_full}|invest_once_1b', + ), + 'invest_once_1b', + ) + + elif self.parameters.size_per_scenario == 'individual': + pass + else: + raise ValueError(f'Invalid value for size_per_scenario: {self.parameters.size_per_scenario}') + class OnOffModel(Model): """ diff --git a/flixopt/interface.py b/flixopt/interface.py index b6eb80c54..2bece9943 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -4,7 +4,7 @@ """ import logging -from typing import TYPE_CHECKING, Dict, Iterator, List, Optional, Union +from typing import TYPE_CHECKING, Dict, Iterator, List, Optional, Union, Literal from .config import CONFIG from .core import NumericDataTS, Scalar, ScenarioData, TimestepData @@ -150,14 +150,15 @@ class InvestParameters(Interface): def __init__( self, - fixed_size: Optional[Scalar] = None, - minimum_size: Scalar = 0, # TODO: Use EPSILON? - maximum_size: Optional[Scalar] = None, + fixed_size: Optional[ScenarioData] = None, + minimum_size: ScenarioData = 0, # TODO: Use EPSILON? + maximum_size: Optional[ScenarioData] = None, optional: bool = True, # Investition ist weglassbar fix_effects: Optional['EffectValuesUserScenario'] = None, specific_effects: Optional['EffectValuesUserScenario'] = None, # costs per Flow-Unit/Storage-Size/... piecewise_effects: Optional[PiecewiseEffects] = None, divest_effects: Optional['EffectValuesUserScenario'] = None, + size_per_scenario: Literal['equal', 'individual', 'increment_once'] = 'equal', ): """ Args: @@ -168,30 +169,24 @@ def __init__( specific_effects: Specific costs, e.g., in €/kW_nominal or €/m²_nominal. Example: {costs: 3, CO2: 0.3} with costs and CO2 representing an Object of class Effect (Attention: Annualize costs to chosen period!) - piecewise_effects: Linear piecewise relation [invest_pieces, cost_pieces]. - Example 1: - [ [5, 25, 25, 100], # size in kW - {costs: [50,250,250,800], # € - PE: [5, 25, 25, 100] # kWh_PrimaryEnergy - } - ] - Example 2 (if only standard-effect): - [ [5, 25, 25, 100], # kW # size in kW - [50,250,250,800] # value for standart effect, typically € - ] # € - (Attention: Annualize costs to chosen period!) - (Args 'specific_effects' and 'fix_effects' can be used in parallel to Investsizepieces) - minimum_size: Min nominal value (only if: size_is_fixed = False). - maximum_size: Max nominal value (only if: size_is_fixed = False). + piecewise_effects: Define the effects of the investment as a piecewise function of the size of the investment. + minimum_size: Minimum possible size of the investment. + maximum_size: Maximum possible size of the investment. + size_per_scenario: How to treat the size in each scenario + - 'equal': Equalize the size of all scenarios + - 'individual': Optimize the size of each scenario individually + - 'increment_once': Allow the size to increase only once. This is useful if the scenarios are related to + different periods (years, months). Tune the timing by setting the maximum size to 0 in the first scenarios. """ - self.fix_effects: EffectValuesUserScenario = fix_effects or {} - self.divest_effects: EffectValuesUserScenario = divest_effects or {} + self.fix_effects: EffectValuesUserScenario = fix_effects if fix_effects is not None else {} + self.divest_effects: EffectValuesUserScenario = divest_effects if divest_effects is not None else {} self.fixed_size = fixed_size self.optional = optional - self.specific_effects: EffectValuesUserScenario = specific_effects or {} + self.specific_effects: EffectValuesUserScenario = specific_effects if specific_effects is not None else {} self.piecewise_effects = piecewise_effects self._minimum_size = minimum_size - self._maximum_size = maximum_size or CONFIG.modeling.BIG # default maximum + self._maximum_size = CONFIG.modeling.BIG if maximum_size is None else maximum_size # default maximum + self.size_per_scenario = size_per_scenario def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): self.fix_effects = flow_system.create_effect_time_series( @@ -219,13 +214,24 @@ def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): self.piecewise_effects.has_time_dim = False self.piecewise_effects.transform_data(flow_system, f'{name_prefix}|PiecewiseEffects') + self._minimum_size = flow_system.create_time_series( + f'{name_prefix}|minimum_size', self.minimum_size, has_time_dim=False, has_scenario_dim=True + ) + self._maximum_size = flow_system.create_time_series( + f'{name_prefix}|maximum_size', self.maximum_size, has_time_dim=False, has_scenario_dim=True + ) + if self.fixed_size is not None: + self.fixed_size = flow_system.create_time_series( + f'{name_prefix}|fixed_size', self.fixed_size, has_time_dim=False, has_scenario_dim=True + ) + @property def minimum_size(self): - return self.fixed_size or self._minimum_size + return self.fixed_size if self.fixed_size is not None else self._minimum_size @property def maximum_size(self): - return self.fixed_size or self._maximum_size + return self.fixed_size if self.fixed_size is not None else self._maximum_size @register_class_for_io From 39f9e4bbaacb52676f30fbfeb3be90630e466e7c Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 9 Apr 2025 15:03:05 +0200 Subject: [PATCH 012/448] Feature/scenarios weights (#228) * Simplify InvestmentModel * Add Scenario Weights to the SystemModel --- flixopt/effects.py | 10 +++------- flixopt/features.py | 27 +++++++++------------------ flixopt/flow_system.py | 8 +++++++- flixopt/structure.py | 19 +++++++++++++++++++ 4 files changed, 38 insertions(+), 26 deletions(-) diff --git a/flixopt/effects.py b/flixopt/effects.py index 226e59a0f..0cf165d66 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -383,14 +383,10 @@ def do_modeling(self): model.do_modeling() self._add_share_between_effects() - scaling_factor = ( - len(self._model.time_series_collection.scenarios) - if self._model.time_series_collection.scenarios is not None - else 1 - ) + self._model.add_objective( - (self.effects.objective_effect.model.total / scaling_factor).sum() - + (self.penalty.total / scaling_factor).sum() + (self.effects.objective_effect.model.total * self._model.scenario_weights).sum() + + self.penalty.total.sum() ) def _add_share_between_effects(self): diff --git a/flixopt/features.py b/flixopt/features.py index 3d2984393..425f9382a 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -43,24 +43,15 @@ def __init__( self.parameters = parameters def do_modeling(self): - if self.parameters.fixed_size and not self.parameters.optional: - self.size = self.add( - self._model.add_variables( - lower=self.parameters.fixed_size, upper=self.parameters.fixed_size, name=f'{self.label_full}|size', - coords=self._model.get_coords(time_dim=False), - ), - 'size', - ) - else: - self.size = self.add( - self._model.add_variables( - lower=0 if self.parameters.optional else self.parameters.minimum_size*1, - upper=self.parameters.maximum_size*1, - name=f'{self.label_full}|size', - coords=self._model.get_coords(time_dim=False), - ), - 'size', - ) + self.size = self.add( + self._model.add_variables( + lower=0 if self.parameters.optional else self.parameters.minimum_size*1, + upper=self.parameters.maximum_size*1, + name=f'{self.label_full}|size', + coords=self._model.get_coords(time_dim=False), + ), + 'size', + ) # Optional if self.parameters.optional: diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 6985572c1..a36a14af1 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -16,7 +16,7 @@ from rich.pretty import Pretty from . import io as fx_io -from .core import Scalar, TimeSeries, TimeSeriesCollection, TimeSeriesData, TimestepData +from .core import Scalar, TimeSeries, TimeSeriesCollection, TimeSeriesData, TimestepData, ScenarioData from .effects import ( Effect, EffectCollection, @@ -45,6 +45,7 @@ def __init__( scenarios: Optional[pd.Index] = None, hours_of_last_timestep: Optional[float] = None, hours_of_previous_timesteps: Optional[Union[int, float, np.ndarray]] = None, + scenario_weights: Optional[ScenarioData] = None, ): """ Args: @@ -55,6 +56,7 @@ def __init__( If None, the first time increment of time_series is used. This is needed to calculate previous durations (for example consecutive_on_hours). If you use an array, take care that its long enough to cover all previous values! + scenario_weights: The weights of the scenarios. If None, all scenarios have the same weight. All weights are normalized to 1. """ self.time_series_collection = TimeSeriesCollection( timesteps=timesteps, @@ -62,6 +64,7 @@ def __init__( hours_of_last_timestep=hours_of_last_timestep, hours_of_previous_timesteps=hours_of_previous_timesteps, ) + self.scenario_weights = scenario_weights # defaults: self.components: Dict[str, Component] = {} @@ -278,6 +281,9 @@ def network_infos(self) -> Tuple[Dict[str, Dict[str, str]], Dict[str, Dict[str, def transform_data(self): if not self._connected: self._connect_network() + self.scenario_weights = self.create_time_series( + 'scenario_weights', self.scenario_weights, has_time_dim=False, has_scenario_dim=True + ) for element in self.all_elements.values(): element.transform_data(self) diff --git a/flixopt/structure.py b/flixopt/structure.py index 7282d3b7c..6830dbb1c 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -58,6 +58,7 @@ def __init__(self, flow_system: 'FlowSystem'): self.flow_system = flow_system self.time_series_collection = flow_system.time_series_collection self.effects: Optional[EffectCollectionModel] = None + self.scenario_weights = self._calculate_scenario_weights(flow_system.scenario_weights) def do_modeling(self): self.effects = self.flow_system.effects.create_model(self) @@ -69,6 +70,24 @@ def do_modeling(self): for bus_model in bus_models: # Buses after Components, because FlowModels are created in ComponentModels bus_model.do_modeling() + def _calculate_scenario_weights(self, weights: Optional[TimeSeries] = None) -> xr.DataArray: + """Calculates the weights of the scenarios. If None, all scenarios have the same weight. All weights are normalized to 1. + If no scenarios are present, s single weight of 1 is returned. + """ + if weights is not None and not isinstance(weights, TimeSeries): + raise TypeError(f'Weights must be a TimeSeries or None, got {type(weights)}') + if self.time_series_collection.scenarios is None: + return xr.DataArray(1) + if weights is None: + weights = xr.DataArray( + np.ones(len(self.time_series_collection.scenarios)), + coords={'scenario': self.time_series_collection.scenarios} + ) + elif isinstance(weights, TimeSeries): + weights = weights.selected_data + + return weights / weights.sum() + @property def solution(self): solution = super().solution From 75cb399799c1c239c24237b56c7d39d5a69abdc1 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 9 Apr 2025 17:16:49 +0200 Subject: [PATCH 013/448] Feature/scenarios tests pandas (#229) Integrate Pandas datatypes into Conversion and update tests --- examples/04_Scenarios/scenario_example.py | 13 +- flixopt/core.py | 139 ++++++++++++- flixopt/structure.py | 3 + tests/test_dataconverter.py | 235 ++++++++++++++++++---- tests/test_timeseries.py | 48 ++--- 5 files changed, 359 insertions(+), 79 deletions(-) diff --git a/examples/04_Scenarios/scenario_example.py b/examples/04_Scenarios/scenario_example.py index 8e3349a4a..03c2a5be0 100644 --- a/examples/04_Scenarios/scenario_example.py +++ b/examples/04_Scenarios/scenario_example.py @@ -9,15 +9,16 @@ import flixopt as fx if __name__ == '__main__': - # --- Create Time Series Data --- - # Heat demand profile (e.g., kW) over time and corresponding power prices - heat_demand_per_h = np.array([[30, 0, 90, 110, 110, 20, 20, 20, 20], - [30, 0, 100, 118, 125, 20, 20, 20, 20]]).T - power_prices = np.ones(9) * 0.08 - # Create datetime array starting from '2020-01-01' for the given time period timesteps = pd.date_range('2020-01-01', periods=9, freq='h') scenarios = pd.Index(['Base Case', 'High Demand']) + + # --- Create Time Series Data --- + # Heat demand profile (e.g., kW) over time and corresponding power prices + heat_demand_per_h = pd.DataFrame({'Base Case':[30, 0, 90, 110, 110, 20, 20, 20, 20], + 'High Demand':[30, 0, 100, 118, 125, 20, 20, 20, 20]}, index=timesteps) + power_prices = np.array([0.08, 0.09]) + flow_system = fx.FlowSystem(timesteps=timesteps, scenarios=scenarios) # --- Define Energy Buses --- diff --git a/flixopt/core.py b/flixopt/core.py index 386a1d873..304048201 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -83,6 +83,12 @@ def as_dataarray( elif isinstance(data, np.ndarray): return DataConverter._convert_ndarray(data, coords, dims) + elif isinstance(data, pd.Series): + return DataConverter._convert_series(data, coords, dims) + + elif isinstance(data, pd.DataFrame): + return DataConverter._convert_dataframe(data, coords, dims) + else: raise ConversionError(f'Unsupported data type: {type(data).__name__}') @@ -171,6 +177,8 @@ def _convert_scalar( Returns: DataArray with the scalar value """ + if isinstance(data, (np.integer, np.floating)): + data = data.item() return xr.DataArray(data, coords=coords, dims=dims) @staticmethod @@ -192,7 +200,7 @@ def _convert_dataarray(data: xr.DataArray, coords: Dict[str, pd.Index], dims: Tu raise ConversionError('When converting to dimensionless DataArray, source must be scalar') return xr.DataArray(data.values.item()) - # Check if data already has matching dimensions + # Check if data already has matching dimensions and coordinates if set(data.dims) == set(dims): # Check if coordinates match is_compatible = True @@ -202,8 +210,13 @@ def _convert_dataarray(data: xr.DataArray, coords: Dict[str, pd.Index], dims: Tu break if is_compatible: - # Return existing DataArray if compatible - return data.copy(deep=True) + # Ensure dimensions are in the correct order + if data.dims != dims: + # Transpose to get dimensions in the right order + return data.transpose(*dims).copy(deep=True) + else: + # Return existing DataArray if compatible and order is correct + return data.copy(deep=True) # Handle dimension broadcasting if len(data.dims) == 1 and len(dims) == 2: @@ -216,8 +229,9 @@ def _convert_dataarray(data: xr.DataArray, coords: Dict[str, pd.Index], dims: Tu # Broadcast scenario dimension to include time return DataConverter._broadcast_scenario_to_time(data, coords, dims) - raise ConversionError(f'Cannot convert {data.dims} to {dims}') - + raise ConversionError( + f'Cannot convert {data.dims} to {dims}. Source coordinates: {data.coords}, Target coordinates: {coords}' + ) @staticmethod def _broadcast_time_to_scenarios( data: xr.DataArray, coords: Dict[str, pd.Index], dims: Tuple[str, ...] @@ -239,7 +253,7 @@ def _broadcast_time_to_scenarios( # Broadcast values values = np.tile(data.values, (len(coords['scenario']), 1)) - return xr.DataArray(values, coords=coords, dims=dims) + return xr.DataArray(values.copy(), coords=coords, dims=dims) @staticmethod def _broadcast_scenario_to_time( @@ -262,7 +276,7 @@ def _broadcast_scenario_to_time( # Broadcast values values = np.repeat(data.values[:, np.newaxis], len(coords['time']), axis=1) - return xr.DataArray(values, coords=coords, dims=dims) + return xr.DataArray(values.copy(), coords=coords, dims=dims) @staticmethod def _convert_ndarray(data: np.ndarray, coords: Dict[str, pd.Index], dims: Tuple[str, ...]) -> xr.DataArray: @@ -359,6 +373,113 @@ def _convert_ndarray_two_dims(data: np.ndarray, coords: Dict[str, pd.Index], dim else: raise ConversionError(f'Expected 1D or 2D array for two dimensions, got {data.ndim}D') + @staticmethod + def _convert_series(data: pd.Series, coords: Dict[str, pd.Index], dims: Tuple[str, ...]) -> xr.DataArray: + """ + Convert pandas Series to xarray DataArray. + + Args: + data: pandas Series to convert + coords: Target coordinates + dims: Target dimensions + + Returns: + DataArray from the pandas Series + """ + # Handle single dimension case + if len(dims) == 1: + dim_name = dims[0] + + # Check if series index matches the dimension + if data.index.equals(coords[dim_name]): + return xr.DataArray(data.values.copy(), coords=coords, dims=dims) + else: + raise ConversionError( + f"Series index doesn't match {dim_name} coordinates.\n" + f'Series index: {data.index}\n' + f'Target {dim_name} coordinates: {coords[dim_name]}' + ) + + # Handle two dimensions case + elif len(dims) == 2: + # Check if dimensions are time and scenario + if dims != ('time', 'scenario'): + raise ConversionError( + f'Two-dimensional conversion only supports time and scenario dimensions, got {dims}' + ) + + # Case 1: Series is indexed by time + if data.index.equals(coords['time']): + # Broadcast across scenarios + values = np.tile(data.values[:, np.newaxis], (1, len(coords['scenario']))) + return xr.DataArray(values.copy(), coords=coords, dims=dims) + + # Case 2: Series is indexed by scenario + elif data.index.equals(coords['scenario']): + # Broadcast across time + values = np.repeat(data.values[np.newaxis, :], len(coords['time']), axis=0) + return xr.DataArray(values.copy(), coords=coords, dims=dims) + + else: + raise ConversionError( + "Series index must match either 'time' or 'scenario' coordinates.\n" + f'Series index: {data.index}\n' + f'Target time coordinates: {coords["time"]}\n' + f'Target scenario coordinates: {coords["scenario"]}' + ) + + else: + raise ConversionError(f'Maximum 2 dimensions supported, got {len(dims)}') + + @staticmethod + def _convert_dataframe(data: pd.DataFrame, coords: Dict[str, pd.Index], dims: Tuple[str, ...]) -> xr.DataArray: + """ + Convert pandas DataFrame to xarray DataArray. + Only allows time as index and scenarios as columns. + + Args: + data: pandas DataFrame to convert + coords: Target coordinates + dims: Target dimensions + + Returns: + DataArray from the pandas DataFrame + """ + # Single dimension case + if len(dims) == 1: + # If DataFrame has one column, treat it like a Series + if len(data.columns) == 1: + series = data.iloc[:, 0] + return DataConverter._convert_series(series, coords, dims) + + raise ConversionError( + f'When converting DataFrame to single-dimension DataArray, DataFrame must have exactly one column, got {len(data.columns)}' + ) + + # Two dimensions case + elif len(dims) == 2: + # Check if dimensions are time and scenario + if dims != ('time', 'scenario'): + raise ConversionError( + f'Two-dimensional conversion only supports time and scenario dimensions, got {dims}' + ) + + # DataFrame must have time as index and scenarios as columns + if data.index.equals(coords['time']) and data.columns.equals(coords['scenario']): + # Create DataArray with proper dimension order + return xr.DataArray(data.values.copy(), coords=coords, dims=dims) + else: + raise ConversionError( + 'DataFrame must have time as index and scenarios as columns.\n' + f'DataFrame index: {data.index}\n' + f'DataFrame columns: {data.columns}\n' + f'Target time coordinates: {coords["time"]}\n' + f'Target scenario coordinates: {coords["scenario"]}' + ) + + else: + raise ConversionError(f'Maximum 2 dimensions supported, got {len(dims)}') + class TimeSeriesData: # TODO: Move to Interface.py @@ -913,8 +1034,8 @@ def clear_selection(self, timesteps: bool = True, scenarios: bool = True) -> Non if scenarios: self._selected_scenarios = None - # Apply the selection to all TimeSeries objects - self._propagate_selection_to_time_series() + for ts in self._time_series.values(): + ts.clear_selection(timesteps=timesteps, scenarios=scenarios) def set_selection(self, timesteps: Optional[pd.DatetimeIndex] = None, scenarios: Optional[pd.Index] = None) -> None: """ diff --git a/flixopt/structure.py b/flixopt/structure.py index 6830dbb1c..a1c9ffa0d 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -583,6 +583,9 @@ def copy_and_convert_datatypes(data: Any, use_numpy: bool = True, use_element_la return copy_and_convert_datatypes(data.selected_data, use_numpy, use_element_label) elif isinstance(data, TimeSeriesData): return copy_and_convert_datatypes(data.data, use_numpy, use_element_label) + elif isinstance(data, (pd.Series, pd.DataFrame)): + #TODO: This can be improved + return copy_and_convert_datatypes(data.values, use_numpy, use_element_label) elif isinstance(data, Interface): if use_element_label and isinstance(data, Element): diff --git a/tests/test_dataconverter.py b/tests/test_dataconverter.py index a023b8e58..61adcb284 100644 --- a/tests/test_dataconverter.py +++ b/tests/test_dataconverter.py @@ -101,8 +101,8 @@ def test_scalar_with_scenarios(self, sample_time_index, sample_scenario_index): result = DataConverter.as_dataarray(42, sample_time_index, sample_scenario_index) assert isinstance(result, xr.DataArray) - assert result.shape == (len(sample_scenario_index), len(sample_time_index)) - assert result.dims == ('scenario', 'time') + assert result.shape == (len(sample_time_index), len(sample_scenario_index)) + assert result.dims == ('time', 'scenario') assert np.all(result.values == 42) assert set(result.coords['scenario'].values) == set(sample_scenario_index.values) assert set(result.coords['time'].values) == set(sample_time_index.values) @@ -119,8 +119,8 @@ def test_1d_array_with_scenarios(self, sample_time_index, sample_scenario_index) # Convert with scenarios result = DataConverter.as_dataarray(arr_1d, sample_time_index, sample_scenario_index) - assert result.shape == (len(sample_scenario_index), len(sample_time_index)) - assert result.dims == ('scenario', 'time') + assert result.shape == (len(sample_time_index), len(sample_scenario_index)) + assert result.dims == ('time', 'scenario') # Each scenario should have the same values (broadcasting) for scenario in sample_scenario_index: @@ -139,10 +139,10 @@ def test_2d_array_with_scenarios(self, sample_time_index, sample_scenario_index) ) # Convert to DataArray - result = DataConverter.as_dataarray(arr_2d, sample_time_index, sample_scenario_index) + result = DataConverter.as_dataarray(arr_2d.T, sample_time_index, sample_scenario_index) - assert result.shape == (3, 5) - assert result.dims == ('scenario', 'time') + assert result.shape == (5, 3) + assert result.dims == ('time', 'scenario') # Check that each scenario has correct values assert np.array_equal(result.sel(scenario='baseline').values, arr_2d[0]) @@ -161,28 +161,181 @@ def test_dataarray_with_scenarios(self, sample_time_index, sample_scenario_index # Test conversion result = DataConverter.as_dataarray(original, sample_time_index, sample_scenario_index) - assert result.shape == (3, 5) - assert result.dims == ('scenario', 'time') - assert np.array_equal(result.values, original.values) + assert result.shape == (5, 3) + assert result.dims == ('time', 'scenario') + assert np.array_equal(result.values, original.values.T) # Ensure it's a copy - result.loc['baseline'] = 999 + result.loc[:, 'baseline'] = 999 assert original.sel(scenario='baseline')[0].item() == 1 # Original should be unchanged - def test_time_only_dataarray_with_scenarios(self, sample_time_index, sample_scenario_index): - """Test broadcasting a time-only DataArray to scenarios.""" - # Create a DataArray with only time dimension - time_only = xr.DataArray(data=np.array([1, 2, 3, 4, 5]), coords={'time': sample_time_index}, dims=['time']) - # Convert with scenarios - should broadcast to all scenarios - result = DataConverter.as_dataarray(time_only, sample_time_index, sample_scenario_index) +class TestSeriesConversion: + """Tests for converting pandas Series to DataArray.""" + + def test_series_single_dimension(self, sample_time_index): + """Test converting a pandas Series with time index.""" + # Create a Series with matching time index + series = pd.Series([10, 20, 30, 40, 50], index=sample_time_index) + + # Convert and check + result = DataConverter.as_dataarray(series, sample_time_index) + assert isinstance(result, xr.DataArray) + assert result.shape == (5,) + assert result.dims == ('time',) + assert np.array_equal(result.values, series.values) + assert np.array_equal(result.coords['time'].values, sample_time_index.values) + + # Test with scenario index + scenario_index = pd.Index(['baseline', 'high_demand', 'low_price'], name='scenario') + series = pd.Series([100, 200, 300], index=scenario_index) + + result = DataConverter.as_dataarray(series, scenarios=scenario_index) + assert result.shape == (3,) + assert result.dims == ('scenario',) + assert np.array_equal(result.values, series.values) + assert np.array_equal(result.coords['scenario'].values, scenario_index.values) + + def test_series_mismatched_index(self, sample_time_index): + """Test converting a Series with mismatched index.""" + # Create Series with different time index + different_times = pd.date_range('2025-01-01', periods=5, freq='D', name='time') + series = pd.Series([10, 20, 30, 40, 50], index=different_times) + + # Should raise error for mismatched index + with pytest.raises(ConversionError): + DataConverter.as_dataarray(series, sample_time_index) + + def test_series_broadcast_to_scenarios(self, sample_time_index, sample_scenario_index): + """Test broadcasting a time-indexed Series across scenarios.""" + # Create a Series with time index + series = pd.Series([10, 20, 30, 40, 50], index=sample_time_index) + + # Convert with scenarios + result = DataConverter.as_dataarray(series, sample_time_index, sample_scenario_index) - assert result.shape == (3, 5) - assert result.dims == ('scenario', 'time') + assert result.shape == (5, 3) + assert result.dims == ('time', 'scenario') - # Each scenario should have same values + # Check broadcasting - each scenario should have the same values for scenario in sample_scenario_index: - assert np.array_equal(result.sel(scenario=scenario).values, time_only.values) + scenario_slice = result.sel(scenario=scenario) + assert np.array_equal(scenario_slice.values, series.values) + + def test_series_broadcast_to_time(self, sample_time_index, sample_scenario_index): + """Test broadcasting a scenario-indexed Series across time.""" + # Create a Series with scenario index + series = pd.Series([100, 200, 300], index=sample_scenario_index) + + # Convert with time + result = DataConverter.as_dataarray(series, sample_time_index, sample_scenario_index) + + assert result.shape == (5, 3) + assert result.dims == ('time', 'scenario') + + # Check broadcasting - each time should have the same scenario values + for i, time in enumerate(sample_time_index): + time_slice = result.sel(time=time) + assert np.array_equal(time_slice.values, series.values) + + def test_series_dimension_order(self, sample_time_index, sample_scenario_index): + """Test that dimension order is respected with Series conversions.""" + # Create custom dimensions tuple with reversed order + dims = ('scenario', 'time',) + coords = {'time': sample_time_index, 'scenario': sample_scenario_index} + + # Time-indexed series + series = pd.Series([10, 20, 30, 40, 50], index=sample_time_index) + with pytest.raises(ConversionError, match="only supports time and scenario dimensions"): + _ = DataConverter._convert_series(series, coords, dims) + + # Scenario-indexed series + series = pd.Series([100, 200, 300], index=sample_scenario_index) + with pytest.raises(ConversionError, match="only supports time and scenario dimensions"): + _ = DataConverter._convert_series(series, coords, dims) + + +class TestDataFrameConversion: + """Tests for converting pandas DataFrame to DataArray.""" + + def test_dataframe_single_column(self, sample_time_index): + """Test converting a DataFrame with a single column.""" + # Create DataFrame with one column + df = pd.DataFrame({'value': [10, 20, 30, 40, 50]}, index=sample_time_index) + + # Convert and check + result = DataConverter.as_dataarray(df, sample_time_index) + assert isinstance(result, xr.DataArray) + assert result.shape == (5,) + assert result.dims == ('time',) + assert np.array_equal(result.values, df['value'].values) + + def test_dataframe_multi_column_fails(self, sample_time_index): + """Test that converting a multi-column DataFrame to 1D fails.""" + # Create DataFrame with multiple columns + df = pd.DataFrame({'val1': [10, 20, 30, 40, 50], 'val2': [15, 25, 35, 45, 55]}, index=sample_time_index) + + # Should raise error + with pytest.raises(ConversionError): + DataConverter.as_dataarray(df, sample_time_index) + + def test_dataframe_time_scenario(self, sample_time_index, sample_scenario_index): + """Test converting a DataFrame with time index and scenario columns.""" + # Create DataFrame with time as index and scenarios as columns + data = {'baseline': [10, 20, 30, 40, 50], 'high_demand': [15, 25, 35, 45, 55], 'low_price': [5, 15, 25, 35, 45]} + df = pd.DataFrame(data, index=sample_time_index) + + # Make sure columns are named properly + df.columns.name = 'scenario' + + # Convert and check + result = DataConverter.as_dataarray(df, sample_time_index, sample_scenario_index) + + assert result.shape == (5, 3) + assert result.dims == ('time', 'scenario') + assert np.array_equal(result.values, df.values) + + # Check values for specific scenarios + assert np.array_equal(result.sel(scenario='baseline').values, df['baseline'].values) + assert np.array_equal(result.sel(scenario='high_demand').values, df['high_demand'].values) + + def test_dataframe_mismatched_coordinates(self, sample_time_index, sample_scenario_index): + """Test conversion fails with mismatched coordinates.""" + # Create DataFrame with different time index + different_times = pd.date_range('2025-01-01', periods=5, freq='D', name='time') + data = {'baseline': [10, 20, 30, 40, 50], 'high_demand': [15, 25, 35, 45, 55], 'low_price': [5, 15, 25, 35, 45]} + df = pd.DataFrame(data, index=different_times) + df.columns = sample_scenario_index + + # Should raise error + with pytest.raises(ConversionError): + DataConverter.as_dataarray(df, sample_time_index, sample_scenario_index) + + # Create DataFrame with different scenario columns + different_scenarios = pd.Index(['scenario1', 'scenario2', 'scenario3'], name='scenario') + data = {'scenario1': [10, 20, 30, 40, 50], 'scenario2': [15, 25, 35, 45, 55], 'scenario3': [5, 15, 25, 35, 45]} + df = pd.DataFrame(data, index=sample_time_index) + df.columns = different_scenarios + + # Should raise error + with pytest.raises(ConversionError): + DataConverter.as_dataarray(df, sample_time_index, sample_scenario_index) + + def test_ensure_copy(self, sample_time_index, sample_scenario_index): + """Test that the returned DataArray is a copy.""" + # Create DataFrame + data = {'baseline': [10, 20, 30, 40, 50], 'high_demand': [15, 25, 35, 45, 55], 'low_price': [5, 15, 25, 35, 45]} + df = pd.DataFrame(data, index=sample_time_index) + df.columns = sample_scenario_index + + # Convert + result = DataConverter.as_dataarray(df, sample_time_index, sample_scenario_index) + + # Modify the result + result.loc[dict(time=sample_time_index[0], scenario='baseline')] = 999 + + # Original should be unchanged + assert df.loc[sample_time_index[0], 'baseline'] == 10 class TestInvalidInputs: @@ -314,8 +467,8 @@ def test_single_timestep(self, sample_scenario_index): # With scenarios result_with_scenarios = DataConverter.as_dataarray(42, single_timestep, sample_scenario_index) - assert result_with_scenarios.shape == (len(sample_scenario_index), 1) - assert result_with_scenarios.dims == ('scenario', 'time') + assert result_with_scenarios.shape == (1, len(sample_scenario_index)) + assert result_with_scenarios.dims == ('time', 'scenario') def test_single_scenario(self, sample_time_index): """Test with a single scenario.""" @@ -324,19 +477,19 @@ def test_single_scenario(self, sample_time_index): # Scalar conversion with single scenario result = DataConverter.as_dataarray(42, sample_time_index, single_scenario) - assert result.shape == (1, len(sample_time_index)) - assert result.dims == ('scenario', 'time') + assert result.shape == (len(sample_time_index), 1) + assert result.dims == ('time', 'scenario') # Array conversion with single scenario arr = np.array([1, 2, 3, 4, 5]) result_arr = DataConverter.as_dataarray(arr, sample_time_index, single_scenario) - assert result_arr.shape == (1, 5) + assert result_arr.shape == (5, 1) assert np.array_equal(result_arr.sel(scenario='baseline').values, arr) # 2D array with single scenario arr_2d = np.array([[1, 2, 3, 4, 5]]) # Note the extra dimension - result_arr_2d = DataConverter.as_dataarray(arr_2d, sample_time_index, single_scenario) - assert result_arr_2d.shape == (1, 5) + result_arr_2d = DataConverter.as_dataarray(arr_2d.T, sample_time_index, single_scenario) + assert result_arr_2d.shape == (5, 1) assert np.array_equal(result_arr_2d.sel(scenario='baseline').values, arr_2d[0]) def test_different_scenario_order(self, sample_time_index): @@ -352,7 +505,7 @@ def test_different_scenario_order(self, sample_time_index): [6, 7, 8, 9, 10], # b [11, 12, 13, 14, 15], # c ] - ) + ).T result1 = DataConverter.as_dataarray(data, sample_time_index, scenarios1) assert np.array_equal(result1.sel(scenario='a').values, [1, 2, 3, 4, 5]) @@ -374,7 +527,7 @@ def test_all_nan_data(self, sample_time_index, sample_scenario_index): # With scenarios result = DataConverter.as_dataarray(all_nan_array, sample_time_index, sample_scenario_index) - assert result.shape == (len(sample_scenario_index), len(sample_time_index)) + assert result.shape == (len(sample_time_index), len(sample_scenario_index)) assert np.all(np.isnan(result.values)) # Series of all NaNs @@ -417,11 +570,11 @@ def test_large_dataset(self, sample_scenario_index): large_data = np.random.rand(len(sample_scenario_index), len(large_timesteps)) # Convert and check - result = DataConverter.as_dataarray(large_data, large_timesteps, sample_scenario_index) + result = DataConverter.as_dataarray(large_data.T, large_timesteps, sample_scenario_index) - assert result.shape == (len(sample_scenario_index), len(large_timesteps)) - assert result.dims == ('scenario', 'time') - assert np.array_equal(result.values, large_data) + assert result.shape == (len(large_timesteps), len(sample_scenario_index)) + assert result.dims == ('time', 'scenario') + assert np.array_equal(result.values, large_data.T) class TestMultiScenarioArrayConversion: @@ -432,7 +585,7 @@ def test_1d_array_broadcasting(self, sample_time_index, sample_scenario_index): arr_1d = np.array([1, 2, 3, 4, 5]) result = DataConverter.as_dataarray(arr_1d, sample_time_index, sample_scenario_index) - assert result.shape == (len(sample_scenario_index), len(sample_time_index)) + assert result.shape == (len(sample_time_index), len(sample_scenario_index)) # Each scenario should have identical values for i, scenario in enumerate(sample_scenario_index): @@ -451,15 +604,15 @@ def test_2d_array_different_shapes(self, sample_time_index): single_scenario = pd.Index(['baseline'], name='scenario') arr_1_scenario = np.array([[1, 2, 3, 4, 5]]) - result = DataConverter.as_dataarray(arr_1_scenario, sample_time_index, single_scenario) - assert result.shape == (1, len(sample_time_index)) + result = DataConverter.as_dataarray(arr_1_scenario.T, sample_time_index, single_scenario) + assert result.shape == (len(sample_time_index), 1) # Test with 2 scenarios two_scenarios = pd.Index(['baseline', 'high_demand'], name='scenario') arr_2_scenarios = np.array([[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]]) - result = DataConverter.as_dataarray(arr_2_scenarios, sample_time_index, two_scenarios) - assert result.shape == (2, len(sample_time_index)) + result = DataConverter.as_dataarray(arr_2_scenarios.T, sample_time_index, two_scenarios) + assert result.shape == (len(sample_time_index), 2) assert np.array_equal(result.sel(scenario='baseline').values, arr_2_scenarios[0]) assert np.array_equal(result.sel(scenario='high_demand').values, arr_2_scenarios[1]) @@ -474,7 +627,7 @@ def test_array_handling_edge_cases(self, sample_time_index, sample_scenario_inde bool_array = np.array([True, False, True, False, True]) result = DataConverter.as_dataarray(bool_array, sample_time_index, sample_scenario_index) assert result.dtype == bool - assert result.shape == (len(sample_scenario_index), len(sample_time_index)) + assert result.shape == (len(sample_time_index), len(sample_scenario_index)) # Test with array containing infinite values inf_array = np.array([1, np.inf, 3, -np.inf, 5]) @@ -504,7 +657,7 @@ def test_preserving_scenario_order(self, sample_time_index): ) # Convert to DataArray - result = DataConverter.as_dataarray(data, sample_time_index, scenarios) + result = DataConverter.as_dataarray(data.T, sample_time_index, scenarios) # Verify order of scenarios is preserved assert list(result.coords['scenario'].values) == list(scenarios) diff --git a/tests/test_timeseries.py b/tests/test_timeseries.py index d64c13d85..8237cf293 100644 --- a/tests/test_timeseries.py +++ b/tests/test_timeseries.py @@ -66,7 +66,7 @@ def test_initialization_validation(self, sample_timesteps): """Test validation during initialization.""" # Test missing time dimension invalid_data = xr.DataArray([1, 2, 3], dims=['invalid_dim']) - with pytest.raises(ValueError, match='must have a "time" index'): + with pytest.raises(ValueError, match='DataArray dimensions must be subset of'): TimeSeries(invalid_data, name='Invalid Series') # Test multi-dimensional data @@ -356,7 +356,7 @@ def test_initialization_with_scenarios(self, simple_scenario_dataarray): # Check basic properties assert ts.name == 'Scenario Series' - assert ts._has_scenarios is True + assert ts.has_scenario_dim is True assert ts._selected_scenarios is None # No selection initially # Check data initialization @@ -615,29 +615,29 @@ def test_add_time_series_with_scenarios(self, sample_scenario_allocator): """Test creating time series with scenarios.""" # Test scalar (broadcasts to all scenarios) ts1 = sample_scenario_allocator.add_time_series('scalar_series', 42) - assert ts1._has_scenarios + assert ts1.has_scenario_dim assert ts1.name == 'scalar_series' - assert ts1.selected_data.shape == (3, 5) # 3 scenarios, 5 timesteps + assert ts1.selected_data.shape == (5, 3) # 5 timesteps, 3 scenarios assert np.all(ts1.selected_data.values == 42) # Test 1D array (broadcasts to all scenarios) data = np.array([1, 2, 3, 4, 5]) ts2 = sample_scenario_allocator.add_time_series('array_series', data) - assert ts2._has_scenarios - assert ts2.selected_data.shape == (3, 5) + assert ts2.has_scenario_dim + assert ts2.selected_data.shape == (5, 3) # Each scenario should have the same values for scenario in sample_scenario_allocator.scenarios: assert np.array_equal(ts2.sel(scenario=scenario).values, data) # Test 2D array (one row per scenario) - data_2d = np.array([[10, 20, 30, 40, 50], [15, 25, 35, 45, 55], [5, 15, 25, 35, 45]]) + data_2d = np.array([[10, 20, 30, 40, 50], [15, 25, 35, 45, 55], [5, 15, 25, 35, 45]]).T ts3 = sample_scenario_allocator.add_time_series('scenario_specific_series', data_2d) - assert ts3._has_scenarios - assert ts3.selected_data.shape == (3, 5) + assert ts3.has_scenario_dim + assert ts3.selected_data.shape == (5, 3) # Each scenario should have its own values - assert np.array_equal(ts3.sel(scenario='baseline').values, data_2d[0]) - assert np.array_equal(ts3.sel(scenario='high_demand').values, data_2d[1]) - assert np.array_equal(ts3.sel(scenario='low_price').values, data_2d[2]) + assert np.array_equal(ts3.sel(scenario='baseline').values, data_2d[:,0]) + assert np.array_equal(ts3.sel(scenario='high_demand').values, data_2d[:,1]) + assert np.array_equal(ts3.sel(scenario='low_price').values, data_2d[:,2]) def test_selection_propagation_with_scenarios( self, sample_scenario_allocator, sample_timesteps, sample_scenario_index @@ -660,8 +660,8 @@ def test_selection_propagation_with_scenarios( assert ts2._selected_scenarios.equals(subset_scenarios) # Check data is filtered - assert ts1.selected_data.shape == (2, 5) # 2 scenarios, 5 timesteps - assert ts2.selected_data.shape == (2, 5) + assert ts1.selected_data.shape == (5, 2) # 5 timesteps, 2 scenarios + assert ts2.selected_data.shape == (5, 2) # Apply combined selection subset_timesteps = sample_timesteps[1:3] @@ -670,20 +670,22 @@ def test_selection_propagation_with_scenarios( # Check combined selection applied assert ts1._selected_timesteps.equals(subset_timesteps) assert ts1._selected_scenarios.equals(subset_scenarios) - assert ts1.selected_data.shape == (2, 2) # 2 scenarios, 2 timesteps + assert ts1.selected_data.shape == (2, 2) # 2 timesteps, 2 scenarios # Clear selections sample_scenario_allocator.clear_selection() assert ts1._selected_timesteps is None + assert ts1.active_timesteps.equals(sample_scenario_allocator.timesteps) assert ts1._selected_scenarios is None - assert ts1.selected_data.shape == (3, 5) # Back to full shape + assert ts1.active_scenarios.equals(sample_scenario_allocator.scenarios) + assert ts1.selected_data.shape == (5, 3) # Back to full shape def test_as_dataset_with_scenarios(self, sample_scenario_allocator): """Test as_dataset method with scenarios.""" # Add some time series sample_scenario_allocator.add_time_series('scalar_series', 42) sample_scenario_allocator.add_time_series( - 'varying_series', np.array([[10, 20, 30, 40, 50], [15, 25, 35, 45, 55], [5, 15, 25, 35, 45]]) + 'varying_series', np.array([[10, 20, 30, 40, 50], [15, 25, 35, 45, 55], [5, 15, 25, 35, 45]]).T ) # Get dataset @@ -723,21 +725,21 @@ def test_update_time_series_with_scenarios(self, sample_scenario_allocator, samp """Test updating a time series with scenarios.""" # Add a time series ts = sample_scenario_allocator.add_time_series('series', 42) - assert ts._has_scenarios + assert ts.has_scenario_dim assert np.all(ts.selected_data.values == 42) # Update with scenario-specific data - new_data = np.array([[1, 2, 3, 4, 5], [6, 7, 8, 9, 10], [11, 12, 13, 14, 15]]) + new_data = np.array([[1, 2, 3, 4, 5], [6, 7, 8, 9, 10], [11, 12, 13, 14, 15]]).T sample_scenario_allocator.update_time_series('series', new_data) # Check update was applied assert np.array_equal(ts.selected_data.values, new_data) - assert ts._has_scenarios + assert ts.has_scenario_dim # Check scenario-specific values - assert np.array_equal(ts.sel(scenario='baseline').values, new_data[0]) - assert np.array_equal(ts.sel(scenario='high_demand').values, new_data[1]) - assert np.array_equal(ts.sel(scenario='low_price').values, new_data[2]) + assert np.array_equal(ts.sel(scenario='baseline').values, new_data[:,0]) + assert np.array_equal(ts.sel(scenario='high_demand').values, new_data[:,1]) + assert np.array_equal(ts.sel(scenario='low_price').values, new_data[:,2]) if __name__ == '__main__': From 26bc4478ccb0d96fc743f7d7c69f2d68ead1f0eb Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 9 Apr 2025 17:42:11 +0200 Subject: [PATCH 014/448] ruff check --- flixopt/flow_system.py | 2 +- flixopt/interface.py | 2 +- tests/test_dataconverter.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index a36a14af1..d62f018bf 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -16,7 +16,7 @@ from rich.pretty import Pretty from . import io as fx_io -from .core import Scalar, TimeSeries, TimeSeriesCollection, TimeSeriesData, TimestepData, ScenarioData +from .core import Scalar, ScenarioData, TimeSeries, TimeSeriesCollection, TimeSeriesData, TimestepData from .effects import ( Effect, EffectCollection, diff --git a/flixopt/interface.py b/flixopt/interface.py index 2bece9943..a7b254fb6 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -4,7 +4,7 @@ """ import logging -from typing import TYPE_CHECKING, Dict, Iterator, List, Optional, Union, Literal +from typing import TYPE_CHECKING, Dict, Iterator, List, Literal, Optional, Union from .config import CONFIG from .core import NumericDataTS, Scalar, ScenarioData, TimestepData diff --git a/tests/test_dataconverter.py b/tests/test_dataconverter.py index 61adcb284..a50754301 100644 --- a/tests/test_dataconverter.py +++ b/tests/test_dataconverter.py @@ -234,7 +234,7 @@ def test_series_broadcast_to_time(self, sample_time_index, sample_scenario_index assert result.dims == ('time', 'scenario') # Check broadcasting - each time should have the same scenario values - for i, time in enumerate(sample_time_index): + for time in sample_time_index: time_slice = result.sel(time=time) assert np.array_equal(time_slice.values, series.values) From 6eeea72548210e7adbd898767019f34f6f229551 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 11 Apr 2025 17:34:11 +0200 Subject: [PATCH 015/448] Bugfix plausibility in Storage --- flixopt/components.py | 59 ++++++++++++++++++++++--------------------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/flixopt/components.py b/flixopt/components.py index 358b66e1b..30c562543 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -226,36 +226,37 @@ def _plausibility_checks(self) -> None: Check for infeasible or uncommon combinations of parameters """ super()._plausibility_checks() - if isinstance(self.initial_charge_state, str) and not self.initial_charge_state == 'lastValueOfSim': - raise PlausibilityError(f'initial_charge_state has undefined value: {self.initial_charge_state}') - else: - if isinstance(self.capacity_in_flow_hours, InvestParameters): - if self.capacity_in_flow_hours.fixed_size is None: - maximum_capacity = self.capacity_in_flow_hours.maximum_size - minimum_capacity = self.capacity_in_flow_hours.minimum_size - else: - maximum_capacity = self.capacity_in_flow_hours.fixed_size - minimum_capacity = self.capacity_in_flow_hours.fixed_size + if isinstance(self.initial_charge_state, str): + if self.initial_charge_state != 'lastValueOfSim': + raise PlausibilityError(f'initial_charge_state has undefined value: {self.initial_charge_state}') + return + if isinstance(self.capacity_in_flow_hours, InvestParameters): + if self.capacity_in_flow_hours.fixed_size is None: + maximum_capacity = self.capacity_in_flow_hours.maximum_size + minimum_capacity = self.capacity_in_flow_hours.minimum_size else: - maximum_capacity = self.capacity_in_flow_hours - minimum_capacity = self.capacity_in_flow_hours - - # initial capacity >= allowed min for maximum_size: - minimum_inital_capacity = maximum_capacity * self.relative_minimum_charge_state.isel(time=1) - # initial capacity <= allowed max for minimum_size: - maximum_inital_capacity = minimum_capacity * self.relative_maximum_charge_state.isel(time=1) - # TODO: index=1 ??? I think index 0 - - if (self.initial_charge_state > maximum_inital_capacity).any(): - raise ValueError( - f'{self.label_full}: {self.initial_charge_state=} ' - f'is above allowed maximum charge_state {maximum_inital_capacity}' - ) - if (self.initial_charge_state < minimum_inital_capacity).any(): - raise ValueError( - f'{self.label_full}: {self.initial_charge_state=} ' - f'is below allowed minimum charge_state {minimum_inital_capacity}' - ) + maximum_capacity = self.capacity_in_flow_hours.fixed_size + minimum_capacity = self.capacity_in_flow_hours.fixed_size + else: + maximum_capacity = self.capacity_in_flow_hours + minimum_capacity = self.capacity_in_flow_hours + + # initial capacity >= allowed min for maximum_size: + minimum_inital_capacity = maximum_capacity * self.relative_minimum_charge_state.isel(time=1) + # initial capacity <= allowed max for minimum_size: + maximum_inital_capacity = minimum_capacity * self.relative_maximum_charge_state.isel(time=1) + # TODO: index=1 ??? I think index 0 + + if (self.initial_charge_state > maximum_inital_capacity).any(): + raise ValueError( + f'{self.label_full}: {self.initial_charge_state=} ' + f'is above allowed maximum charge_state {maximum_inital_capacity}' + ) + if (self.initial_charge_state < minimum_inital_capacity).any(): + raise ValueError( + f'{self.label_full}: {self.initial_charge_state=} ' + f'is below allowed minimum charge_state {minimum_inital_capacity}' + ) @register_class_for_io From ecf64d2b74503b2dc1ba8a363d72916d4f05d527 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 11 Apr 2025 17:40:01 +0200 Subject: [PATCH 016/448] Bugfix check in Storage Model --- flixopt/components.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flixopt/components.py b/flixopt/components.py index 30c562543..32b28308e 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -533,7 +533,7 @@ def _initial_and_final_charge_state(self): name_short = 'initial_charge_state' name = f'{self.label_full}|{name_short}' - if self.element.initial_charge_state == 'lastValueOfSim': + if isinstance(self.element.initial_charge_state, str): self.add( self._model.add_constraints( self.charge_state.isel(time=0) == self.charge_state.isel(time=-1), name=name From 091ab71b3103f40d81dd5df8a08130e7815af78b Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 11 Apr 2025 17:40:08 +0200 Subject: [PATCH 017/448] Improve example --- examples/04_Scenarios/scenario_example.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/04_Scenarios/scenario_example.py b/examples/04_Scenarios/scenario_example.py index 03c2a5be0..c68d1bbe5 100644 --- a/examples/04_Scenarios/scenario_example.py +++ b/examples/04_Scenarios/scenario_example.py @@ -49,7 +49,7 @@ boiler = fx.linear_converters.Boiler( label='Boiler', eta=0.5, - Q_th=fx.Flow(label='Q_th', bus='Fernwärme', size=50, relative_minimum=0.1, relative_maximum=1), + Q_th=fx.Flow(label='Q_th', bus='Fernwärme', size=50, relative_minimum=0.1, relative_maximum=1, on_off_parameters=fx.OnOffParameters()), Q_fu=fx.Flow(label='Q_fu', bus='Gas'), ) @@ -58,7 +58,7 @@ label='CHP', eta_th=0.5, eta_el=0.4, - P_el=fx.Flow('P_el', bus='Strom', size=60, relative_minimum=5 / 60), + P_el=fx.Flow('P_el', bus='Strom', size=60, relative_minimum=5 / 60, on_off_parameters=fx.OnOffParameters()), Q_th=fx.Flow('Q_th', bus='Fernwärme'), Q_fu=fx.Flow('Q_fu', bus='Gas'), ) From 0a7e3367fe659b2d8d204ccf0465195ace2c31f9 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 11 Apr 2025 17:43:38 +0200 Subject: [PATCH 018/448] ruff check --- flixopt/elements.py | 2 +- flixopt/features.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/flixopt/elements.py b/flixopt/elements.py index b5b4f2344..3cc775674 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -10,7 +10,7 @@ import numpy as np from .config import CONFIG -from .core import TimestepData, PlausibilityError, Scalar, ScenarioData, TimestepData +from .core import PlausibilityError, Scalar, ScenarioData, TimestepData from .effects import EffectValuesUserTimestep from .features import InvestmentModel, OnOffModel, PreventSimultaneousUsageModel from .interface import InvestParameters, OnOffParameters diff --git a/flixopt/features.py b/flixopt/features.py index 52b49e960..0ccc7be2a 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -10,7 +10,7 @@ import numpy as np from .config import CONFIG -from .core import Scalar, TimeSeries, TimestepData, ScenarioData +from .core import Scalar, ScenarioData, TimeSeries, TimestepData from .interface import InvestParameters, OnOffParameters, Piecewise from .structure import Model, SystemModel From 8b5d2fcc06869df575f8b37bc848b98777a82764 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 14 Apr 2025 18:20:09 +0200 Subject: [PATCH 019/448] Simplifying the investment with scenarios, by a simpler approach --- flixopt/features.py | 71 ++++++++++++++------------------------------ flixopt/interface.py | 27 +++++++++++++---- 2 files changed, 43 insertions(+), 55 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index 0ccc7be2a..17ef9928a 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -194,7 +194,10 @@ def _create_bounds_for_defining_variable(self): # anmerkung: Glg bei Spezialfall relative_minimum = 0 redundant zu OnOff ?? def _create_bounds_for_scenarios(self): - if self.parameters.size_per_scenario == 'equal': + if self.parameters.investment_scenarios == 'individual': + return + + if self.parameters.investment_scenarios is None: self.add( self._model.add_constraints( self.size.isel(scenario=slice(None, -1)) == self.size.isel(scenario=slice(1, None)), @@ -202,64 +205,34 @@ def _create_bounds_for_scenarios(self): ), 'equalize_size_per_scenario', ) - elif self.parameters.size_per_scenario == 'increment_once': - if not self.parameters.optional: - raise ValueError('Increment once can only be used if the Investment is optional') - - self.scenario_of_investment = self.add( - self._model.add_variables( - binary=True, - name=f'{self.label_full}|scenario_of_investment', - coords=self._model.get_coords(time_dim=False), - ), - 'scenario_of_investment', - ) - - # eq: scenario_of_investment(t) = is_invested(t) - is_invested(t-1) - self.add( - self._model.add_constraints( - self.scenario_of_investment.isel(scenario=slice(1, None)) - == self.is_invested.isel(scenario=slice(1, None)) - self.is_invested.isel(scenario=slice(None, -1)), - name=f'{self.label_full}|scenario_of_investment', - ), - 'scenario_of_investment', - ) - - # eq: scenario_of_investment(t=0) = is_invested(t=0) - self.add( - self._model.add_constraints( - self.scenario_of_investment.isel(scenario=0) - == self.is_invested.isel(scenario=0), - name=f'{self.label_full}|initial_scenario_of_investment', - ), - 'initial_scenario_of_investment', - ) + return + if not isinstance(self.parameters.investment_scenarios, list): + raise ValueError(f'Invalid value for investment_scenarios: {self.parameters.investment_scenarios}') + if not all(scenario in self._model.time_series_collection.scenarios for scenario in self.parameters.investment_scenarios): + raise ValueError(f'Some scenarios in investment_scenarios are not present in the time_series_collection: {self.parameters.investment_scenarios}') - big_m = self.parameters.maximum_size.isel(scenario=slice(1, None)) + investment_scenarios = self._model.time_series_collection.scenarios.intersection(self.parameters.investment_scenarios) + no_investment_scenarios = self._model.time_series_collection.scenarios.difference(self.parameters.investment_scenarios) + # eq: size(s) = size(s') for s, s' in investment_scenarios + if len(investment_scenarios) > 1: self.add( self._model.add_constraints( - self.size.isel(scenario=slice(1, None)) - self.size.isel(scenario=slice(None, -1)) - <= self.scenario_of_investment.isel(scenario=slice(1, None)) * big_m, - name=f'{self.label_full}|invest_once_1a', - ), - 'invest_once_1a', + self.size.sel(scenario=investment_scenarios[:-1]) == self.size.sel(scenario=investment_scenarios[1:]), + name=f'{self.label_full}|investment_scenarios', + ), + 'investment_scenarios', ) + if len(no_investment_scenarios) >= 1: self.add( self._model.add_constraints( - self.size.isel(scenario=slice(1, None)) - self.size.isel(scenario=slice(None, -1)) - >= self.scenario_of_investment.isel(scenario=slice(1, None)) * big_m, - name=f'{self.label_full}|invest_once_1b', - ), - 'invest_once_1b', + self.size.sel(scenario=no_investment_scenarios) == 0, + name=f'{self.label_full}|no_investment_scenarios', + ), + 'no_investment_scenarios', ) - elif self.parameters.size_per_scenario == 'individual': - pass - else: - raise ValueError(f'Invalid value for size_per_scenario: {self.parameters.size_per_scenario}') - class StateModel(Model): """ diff --git a/flixopt/interface.py b/flixopt/interface.py index 6ede2f2b9..a4936e844 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -158,7 +158,7 @@ def __init__( specific_effects: Optional['EffectValuesUserScenario'] = None, # costs per Flow-Unit/Storage-Size/... piecewise_effects: Optional[PiecewiseEffects] = None, divest_effects: Optional['EffectValuesUserScenario'] = None, - size_per_scenario: Literal['equal', 'individual', 'increment_once'] = 'equal', + investment_scenarios: Optional[Union[Literal['individual'], List[Union[int, str]]]] = None, ): """ Args: @@ -172,11 +172,10 @@ def __init__( piecewise_effects: Define the effects of the investment as a piecewise function of the size of the investment. minimum_size: Minimum possible size of the investment. maximum_size: Maximum possible size of the investment. - size_per_scenario: How to treat the size in each scenario - - 'equal': Equalize the size of all scenarios + investment_scenarios: For which scenarios to optimize the size for. - 'individual': Optimize the size of each scenario individually - - 'increment_once': Allow the size to increase only once. This is useful if the scenarios are related to - different periods (years, months). Tune the timing by setting the maximum size to 0 in the first scenarios. + - List of scenario names: Optimize the size for the passed scenario names (equal size in all). All other scenarios will have the size 0. + - None: Equals to a list of all scenarios (default) """ self.fix_effects: EffectValuesUserScenario = fix_effects if fix_effects is not None else {} self.divest_effects: EffectValuesUserScenario = divest_effects if divest_effects is not None else {} @@ -186,9 +185,10 @@ def __init__( self.piecewise_effects = piecewise_effects self._minimum_size = minimum_size if minimum_size is not None else CONFIG.modeling.EPSILON self._maximum_size = maximum_size if maximum_size is not None else CONFIG.modeling.BIG # default maximum - self.size_per_scenario = size_per_scenario + self.investment_scenarios = investment_scenarios def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): + self._plausibility_checks(flow_system) self.fix_effects = flow_system.create_effect_time_series( label_prefix=name_prefix, effect_values=self.fix_effects, @@ -225,6 +225,21 @@ def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): f'{name_prefix}|fixed_size', self.fixed_size, has_time_dim=False, has_scenario_dim=True ) + def _plausibility_checks(self, flow_system): + if isinstance(self.investment_scenarios, list): + if not set(self.investment_scenarios).issubset(flow_system.time_series_collection.scenarios): + raise ValueError( + f'Some scenarios in investment_scenarios are not present in the time_series_collection: ' + f'{set(self.investment_scenarios) - set(flow_system.time_series_collection.scenarios)}' + ) + if self.investment_scenarios is not None: + if not self.optional: + if self.minimum_size is not None or self.fixed_size is not None: + logger.warning( + f'When using investment_scenarios, minimum_size and fixed_size should only ne used if optional is True.' + f'Otherwise the investment cannot be 0 incertain scenarios while being non-zero in others.' + ) + @property def minimum_size(self): return self.fixed_size if self.fixed_size is not None else self._minimum_size From f28db64ddb0dc3f8d2074896e1a0a55608342968 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 14 Apr 2025 19:30:58 +0200 Subject: [PATCH 020/448] Simplifying the investment with scenarios --- flixopt/features.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/flixopt/features.py b/flixopt/features.py index 17ef9928a..31c1dbadb 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -209,7 +209,9 @@ def _create_bounds_for_scenarios(self): if not isinstance(self.parameters.investment_scenarios, list): raise ValueError(f'Invalid value for investment_scenarios: {self.parameters.investment_scenarios}') if not all(scenario in self._model.time_series_collection.scenarios for scenario in self.parameters.investment_scenarios): - raise ValueError(f'Some scenarios in investment_scenarios are not present in the time_series_collection: {self.parameters.investment_scenarios}') + raise ValueError(f'Some scenarios in investment_scenarios are not present in the time_series_collection: ' + f'{self.parameters.investment_scenarios}. This might be due to selecting a subset of ' + f'all scenarios, which is not yet supported.') investment_scenarios = self._model.time_series_collection.scenarios.intersection(self.parameters.investment_scenarios) no_investment_scenarios = self._model.time_series_collection.scenarios.difference(self.parameters.investment_scenarios) From 8696430dff19e0f969df320ae8114516cf54e873 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 14 Apr 2025 19:31:10 +0200 Subject: [PATCH 021/448] Bugfix in scenario selection --- flixopt/core.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/flixopt/core.py b/flixopt/core.py index 14b95ea5a..e1c3c361d 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -1109,7 +1109,7 @@ def set_selection(self, timesteps: Optional[pd.DatetimeIndex] = None, scenarios: if scenarios is None: self.clear_selection(timesteps=False, scenarios=True) else: - self._selected_scenarios = scenarios + self._selected_scenarios = self._validate_scenarios(scenarios) # Apply the selection to all TimeSeries objects self._propagate_selection_to_time_series() @@ -1350,10 +1350,6 @@ def _validate_scenarios(scenarios: pd.Index, present_scenarios: Optional[pd.Inde logger.warning('Converting scenarios to pandas.Index') scenarios = pd.Index(scenarios, name='scenario') - if len(scenarios) < 2: - logger.warning('scenarios must contain at least 2 scenarios') - raise ValueError('timesteps must contain at least 2 timestamps') - # Ensure timesteps has the required name if scenarios.name != 'scenario': logger.debug('Renamed scenarios to "scneario" (was "%s")', scenarios.name) From 2d3f0ada33061ad9fa66266b0df3fd2742ac68df Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 14 Apr 2025 19:40:11 +0200 Subject: [PATCH 022/448] ruff check --- flixopt/interface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flixopt/interface.py b/flixopt/interface.py index a4936e844..4b7634e96 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -236,8 +236,8 @@ def _plausibility_checks(self, flow_system): if not self.optional: if self.minimum_size is not None or self.fixed_size is not None: logger.warning( - f'When using investment_scenarios, minimum_size and fixed_size should only ne used if optional is True.' - f'Otherwise the investment cannot be 0 incertain scenarios while being non-zero in others.' + 'When using investment_scenarios, minimum_size and fixed_size should only ne used if optional is True.' + 'Otherwise the investment cannot be 0 incertain scenarios while being non-zero in others.' ) @property From c730b87dfcca74be20730c061598146580fff4f3 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 15 Apr 2025 16:35:16 +0200 Subject: [PATCH 023/448] Scenarios/io (#244) * Add scenarios to io of flow_system.py * Add test for io of scenarios * Fix tests and docstrings (#242) * Bugfix testing fixture * Bugfix tests and add new tests to check for previous states/flow_rates * Bugfix tests and add new tests to check for previous states/flow_rates * Add comment for previous flow_rates * Add comment for OnOffParameters in Component and LinearConverter * Bugfix io --- flixopt/flow_system.py | 15 ++++++++- tests/conftest.py | 74 ++++++++++++++++++++++++++++++++++++++++++ tests/test_io.py | 3 +- 3 files changed, 90 insertions(+), 2 deletions(-) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 7b83b2005..591b55e06 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -64,7 +64,9 @@ def __init__( hours_of_last_timestep=hours_of_last_timestep, hours_of_previous_timesteps=hours_of_previous_timesteps, ) - self.scenario_weights = scenario_weights + self.scenario_weights = self.create_time_series( + 'scenario_weights', scenario_weights, has_time_dim=False, has_scenario_dim=True + ) # defaults: self.components: Dict[str, Component] = {} @@ -79,10 +81,15 @@ def from_dataset(cls, ds: xr.Dataset): timesteps_extra = pd.DatetimeIndex(ds.attrs['timesteps_extra'], name='time') hours_of_last_timestep = TimeSeriesCollection.calculate_hours_per_timestep(timesteps_extra).isel(time=-1).item() + scenarios = pd.Index(ds.attrs['scenarios'], name='scenario') if ds.attrs.get('scenarios') is not None else None + scenario_weights = fx_io.insert_dataarray(ds.attrs['scenario_weights'], ds) + flow_system = FlowSystem( timesteps=timesteps_extra[:-1], hours_of_last_timestep=hours_of_last_timestep, hours_of_previous_timesteps=ds.attrs['hours_of_previous_timesteps'], + scenarios=scenarios, + scenario_weights=scenario_weights, ) structure = fx_io.insert_dataarray({key: ds.attrs[key] for key in ['components', 'buses', 'effects']}, ds) @@ -103,11 +110,15 @@ def from_dict(cls, data: Dict) -> 'FlowSystem': """ timesteps_extra = pd.DatetimeIndex(data['timesteps_extra'], name='time') hours_of_last_timestep = TimeSeriesCollection.calculate_hours_per_timestep(timesteps_extra).isel(time=-1).item() + scenarios = pd.Index(data['scenarios'], name='scenario') if data.get('scenarios') is not None else None + scenario_weights = data.get('scenario_weights').selected_data if data.get('scenario_weights') is not None else None flow_system = FlowSystem( timesteps=timesteps_extra[:-1], hours_of_last_timestep=hours_of_last_timestep, hours_of_previous_timesteps=data['hours_of_previous_timesteps'], + scenarios=scenarios, + scenario_weights=scenario_weights, ) flow_system.add_elements(*[Bus.from_dict(bus) for bus in data['buses'].values()]) @@ -183,6 +194,8 @@ def as_dict(self, data_mode: Literal['data', 'name', 'stats'] = 'data') -> Dict: }, 'timesteps_extra': [date.isoformat() for date in self.time_series_collection.timesteps_extra], 'hours_of_previous_timesteps': self.time_series_collection.hours_of_previous_timesteps, + 'scenarios': self.time_series_collection.scenarios.tolist() if self.time_series_collection.scenarios is not None else None, + 'scenario_weights': self.scenario_weights, } if data_mode == 'data': return fx_io.replace_timeseries(data, 'data') diff --git a/tests/conftest.py b/tests/conftest.py index 5399be72a..b2ceb1c1d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -121,6 +121,80 @@ def simple_flow_system() -> fx.FlowSystem: return flow_system +@pytest.fixture +def simple_flow_system_scenarios() -> fx.FlowSystem: + """ + Create a simple energy system for testing + """ + base_thermal_load = np.array([30.0, 0.0, 90.0, 110, 110, 20, 20, 20, 20]) + base_electrical_price = np.array([0.08, 0.1, 0.15]) + base_timesteps = pd.date_range('2020-01-01', periods=9, freq='h', name='time') + # Define effects + costs = fx.Effect('costs', '€', 'Kosten', is_standard=True, is_objective=True) + co2 = fx.Effect( + 'CO2', + 'kg', + 'CO2_e-Emissionen', + specific_share_to_other_effects_operation={costs.label: 0.2}, + maximum_operation_per_hour=1000, + ) + + # Create components + boiler = fx.linear_converters.Boiler( + 'Boiler', + eta=0.5, + Q_th=fx.Flow( + 'Q_th', + bus='Fernwärme', + size=50, + relative_minimum=5 / 50, + relative_maximum=1, + on_off_parameters=fx.OnOffParameters(), + ), + Q_fu=fx.Flow('Q_fu', bus='Gas'), + ) + + chp = fx.linear_converters.CHP( + 'CHP_unit', + eta_th=0.5, + eta_el=0.4, + P_el=fx.Flow('P_el', bus='Strom', size=60, relative_minimum=5 / 60, on_off_parameters=fx.OnOffParameters()), + Q_th=fx.Flow('Q_th', bus='Fernwärme'), + Q_fu=fx.Flow('Q_fu', bus='Gas'), + ) + + storage = fx.Storage( + 'Speicher', + charging=fx.Flow('Q_th_load', bus='Fernwärme', size=1e4), + discharging=fx.Flow('Q_th_unload', bus='Fernwärme', size=1e4), + capacity_in_flow_hours=fx.InvestParameters(fix_effects=20, fixed_size=30, optional=False), + initial_charge_state=0, + relative_maximum_charge_state=1 / 100 * np.array([80.0, 70.0, 80.0, 80, 80, 80, 80, 80, 80, 80]), + eta_charge=0.9, + eta_discharge=1, + relative_loss_per_hour=0.08, + prevent_simultaneous_charge_and_discharge=True, + ) + + heat_load = fx.Sink( + 'Wärmelast', sink=fx.Flow('Q_th_Last', bus='Fernwärme', size=1, fixed_relative_profile=base_thermal_load) + ) + + gas_tariff = fx.Source( + 'Gastarif', source=fx.Flow('Q_Gas', bus='Gas', size=1000, effects_per_flow_hour={'costs': 0.04, 'CO2': 0.3}) + ) + + electricity_feed_in = fx.Sink( + 'Einspeisung', sink=fx.Flow('P_el', bus='Strom', effects_per_flow_hour=-1 * base_electrical_price) + ) + + # Create flow system + flow_system = fx.FlowSystem(base_timesteps, scenarios=pd.Index(['A', 'B', 'C']), scenario_weights=np.array([0.5, 0.25, 0.25])) + flow_system.add_elements(fx.Bus('Strom'), fx.Bus('Fernwärme'), fx.Bus('Gas')) + flow_system.add_elements(storage, costs, co2, boiler, heat_load, gas_tariff, electricity_feed_in, chp) + + return flow_system + @pytest.fixture def basic_flow_system() -> fx.FlowSystem: diff --git a/tests/test_io.py b/tests/test_io.py index 2e6c61ccf..2b3a03399 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -11,10 +11,11 @@ flow_system_long, flow_system_segments_of_flows_2, simple_flow_system, + simple_flow_system_scenarios, ) -@pytest.fixture(params=[flow_system_base, flow_system_segments_of_flows_2, simple_flow_system, flow_system_long]) +@pytest.fixture(params=[flow_system_base, simple_flow_system_scenarios, flow_system_segments_of_flows_2, simple_flow_system, flow_system_long]) def flow_system(request): fs = request.getfixturevalue(request.param.__name__) if isinstance(fs, fx.FlowSystem): From 50bb559f813d6a6e0e40864a10763dfc8fa10cbb Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 15 Apr 2025 16:42:10 +0200 Subject: [PATCH 024/448] Scenarios/testing (#246) * Add tests for scenario calculations * utility method to get TimeSeries as a DataArray --- examples/04_Scenarios/scenario_example.py | 2 +- flixopt/components.py | 2 +- flixopt/core.py | 100 ++++--- flixopt/effects.py | 22 +- flixopt/elements.py | 22 +- flixopt/features.py | 30 +- tests/test_scenarios.py | 333 ++++++++++++++++++++++ tests/test_timeseries.py | 8 +- 8 files changed, 423 insertions(+), 96 deletions(-) create mode 100644 tests/test_scenarios.py diff --git a/examples/04_Scenarios/scenario_example.py b/examples/04_Scenarios/scenario_example.py index c68d1bbe5..3edb6e7c0 100644 --- a/examples/04_Scenarios/scenario_example.py +++ b/examples/04_Scenarios/scenario_example.py @@ -19,7 +19,7 @@ 'High Demand':[30, 0, 100, 118, 125, 20, 20, 20, 20]}, index=timesteps) power_prices = np.array([0.08, 0.09]) - flow_system = fx.FlowSystem(timesteps=timesteps, scenarios=scenarios) + flow_system = fx.FlowSystem(timesteps=timesteps, scenarios=scenarios, scenario_weights=np.array([0.5, 0.6])) # --- Define Energy Buses --- # These represent nodes, where the used medias are balanced (electricity, heat, and gas) diff --git a/flixopt/components.py b/flixopt/components.py index d471dcf6e..1b745b54c 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -211,7 +211,7 @@ def transform_data(self, flow_system: 'FlowSystem') -> None: self.relative_loss_per_hour = flow_system.create_time_series( f'{self.label_full}|relative_loss_per_hour', self.relative_loss_per_hour ) - if self.initial_charge_state != 'lastValueOfSim': + if not isinstance(self.initial_charge_state, str): self.initial_charge_state = flow_system.create_time_series( f'{self.label_full}|initial_charge_state', self.initial_charge_state, has_time_dim=False ) diff --git a/flixopt/core.py b/flixopt/core.py index e1c3c361d..850e01c04 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -653,7 +653,7 @@ def reset(self) -> None: Reset selections to include all timesteps and scenarios. This is equivalent to clearing all selections. """ - self.clear_selection() + self.set_selection(None, None) def restore_data(self) -> None: """ @@ -755,13 +755,7 @@ def update_stored_data(self, value: xr.DataArray) -> None: return self._stored_data = new_data - self.clear_selection() # Reset selections to full dataset - - def clear_selection(self, timesteps: bool = True, scenarios: bool = True) -> None: - if timesteps: - self._selected_timesteps = None - if scenarios: - self._selected_scenarios = None + self.set_selection(None, None) # Reset selections to full dataset def set_selection(self, timesteps: Optional[pd.DatetimeIndex] = None, scenarios: Optional[pd.Index] = None) -> None: """ @@ -773,15 +767,15 @@ def set_selection(self, timesteps: Optional[pd.DatetimeIndex] = None, scenarios: """ # Only update timesteps if the series has time dimension if self.has_time_dim: - if timesteps is None: - self.clear_selection(timesteps=True, scenarios=False) + if timesteps is None or timesteps.equals(self._stored_data.indexes['time']): + self._selected_timesteps = None else: self._selected_timesteps = timesteps # Only update scenarios if the series has scenario dimension if self.has_scenario_dim: - if scenarios is None: - self.clear_selection(timesteps=False, scenarios=True) + if scenarios is None or scenarios.equals(self._stored_data.indexes['scenario']): + self._selected_scenarios = None else: self._selected_scenarios = scenarios @@ -1077,22 +1071,6 @@ def add_time_series( # Return the TimeSeries object return time_series - def clear_selection(self, timesteps: bool = True, scenarios: bool = True) -> None: - """ - Clear selection for timesteps and/or scenarios. - - Args: - timesteps: Whether to clear timesteps selection - scenarios: Whether to clear scenarios selection - """ - if timesteps: - self._update_selected_timesteps(timesteps=None) - if scenarios: - self._selected_scenarios = None - - for ts in self._time_series.values(): - ts.clear_selection(timesteps=timesteps, scenarios=scenarios) - def set_selection(self, timesteps: Optional[pd.DatetimeIndex] = None, scenarios: Optional[pd.Index] = None) -> None: """ Set active subset for timesteps and scenarios. @@ -1102,35 +1080,30 @@ def set_selection(self, timesteps: Optional[pd.DatetimeIndex] = None, scenarios: scenarios: Scenarios to activate, or None to clear """ if timesteps is None: - self.clear_selection(timesteps=True, scenarios=False) + self._selected_timesteps = None + self._selected_timesteps_extra = None else: - self._update_selected_timesteps(timesteps) + self._selected_timesteps = self._validate_timesteps(timesteps, self._full_timesteps) + self._selected_timesteps_extra = self._create_timesteps_with_extra( + timesteps, self._calculate_hours_of_final_timestep(timesteps, self._full_timesteps) + ) if scenarios is None: - self.clear_selection(timesteps=False, scenarios=True) + self._selected_scenarios = None else: - self._selected_scenarios = self._validate_scenarios(scenarios) + self._selected_scenarios = self._validate_scenarios(scenarios, self._full_scenarios) - # Apply the selection to all TimeSeries objects - self._propagate_selection_to_time_series() + self._selected_hours_per_timestep = self.calculate_hours_per_timestep(self.timesteps_extra, self.scenarios) - def _update_selected_timesteps(self, timesteps: Optional[pd.DatetimeIndex]) -> None: - """ - Updates the timestep and related metrics (timesteps_extra, hours_per_timestep) based on the current selection. - """ - if timesteps is None: - self._selected_timesteps = None - self._selected_timesteps_extra = None - self._selected_hours_per_timestep = None - return + # Apply the selection to all TimeSeries objects + for ts_name, ts in self._time_series.items(): + if ts.has_time_dim: + timesteps = self.timesteps_extra if ts_name in self._has_extra_timestep else self.timesteps + else: + timesteps = None - self._selected_timesteps = self._validate_timesteps(timesteps, self._full_timesteps) - self._selected_timesteps_extra = self._create_timesteps_with_extra( - timesteps, self._calculate_hours_of_final_timestep(timesteps, self._full_timesteps) - ) - self._selected_hours_per_timestep = self.calculate_hours_per_timestep( - self._selected_timesteps_extra, self._selected_scenarios - ) + ts.set_selection(timesteps=timesteps, scenarios=self.scenarios if ts.has_scenario_dim else None) + self._propagate_selection_to_time_series() def as_dataset(self, with_extra_timestep: bool = True, with_constants: bool = True) -> xr.Dataset: """ @@ -1188,7 +1161,7 @@ def _propagate_selection_to_time_series(self) -> None: """Apply the current selection to all TimeSeries objects.""" for ts_name, ts in self._time_series.items(): if ts.has_time_dim: - timesteps = self.timesteps_extra if ts_name in self._has_extra_timestep else self.timesteps + timesteps = self.timesteps_extra if ts_name in self._has_extra_timestep else self.timesteps else: timesteps = None @@ -1482,3 +1455,28 @@ def get_numeric_stats(data: xr.DataArray, decimals: int = 2, padd: int = 10, by_ std = data.std().item() return f'{mean:{format_spec}} (mean), {median:{format_spec}} (median), {min_val:{format_spec}} (min), {max_val:{format_spec}} (max), {std:{format_spec}} (std)' + + +def extract_data( + data: Optional[Union[int, float, xr.DataArray, TimeSeries]], + if_none: Any = None +) -> Any: + """ + Convert data to xr.DataArray. + + Args: + data: The data to convert (scalar, array, or DataArray) + if_none: The value to return if data is None + + Returns: + DataArray with the converted data, or the value specified by if_none + """ + if data is None: + return if_none + if isinstance(data, TimeSeries): + return data.selected_data + if isinstance(data, xr.DataArray): + return data + if isinstance(data, (int, float, np.integer, np.floating)): + return xr.DataArray(data) + raise TypeError(f'Unsupported data type: {type(data).__name__}') \ No newline at end of file diff --git a/flixopt/effects.py b/flixopt/effects.py index 0cf165d66..2da561a36 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -12,7 +12,7 @@ import linopy import numpy as np -from .core import NumericDataTS, ScenarioData, TimeSeries, TimeSeriesCollection, TimestepData +from .core import NumericDataTS, ScenarioData, TimeSeries, TimeSeriesCollection, TimestepData, extract_data from .features import ShareAllocationModel from .structure import Element, ElementModel, Interface, Model, SystemModel, register_class_for_io @@ -125,8 +125,8 @@ def __init__(self, model: SystemModel, element: Effect): label_of_element=self.label_of_element, label='invest', label_full=f'{self.label_full}(invest)', - total_max=self.element.maximum_invest, - total_min=self.element.minimum_invest, + total_max=extract_data(self.element.maximum_invest), + total_min=extract_data(self.element.minimum_invest), ) ) @@ -138,14 +138,10 @@ def __init__(self, model: SystemModel, element: Effect): label_of_element=self.label_of_element, label='operation', label_full=f'{self.label_full}(operation)', - total_max=self.element.maximum_operation, - total_min=self.element.minimum_operation, - min_per_hour=self.element.minimum_operation_per_hour.selected_data - if self.element.minimum_operation_per_hour is not None - else None, - max_per_hour=self.element.maximum_operation_per_hour.selected_data - if self.element.maximum_operation_per_hour is not None - else None, + total_max=extract_data(self.element.maximum_operation), + total_min=extract_data(self.element.minimum_operation), + min_per_hour=extract_data(self.element.minimum_operation_per_hour), + max_per_hour=extract_data(self.element.maximum_operation_per_hour), ) ) @@ -155,8 +151,8 @@ def do_modeling(self): self.total = self.add( self._model.add_variables( - lower=self.element.minimum_total if self.element.minimum_total is not None else -np.inf, - upper=self.element.maximum_total if self.element.maximum_total is not None else np.inf, + lower=extract_data(self.element.minimum_total, if_none=-np.inf), + upper=extract_data(self.element.maximum_total, if_none=np.inf), coords=self._model.get_coords(time_dim=False), name=f'{self.label_full}|total', ), diff --git a/flixopt/elements.py b/flixopt/elements.py index 80085cd0c..2ff49567e 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -10,7 +10,7 @@ import numpy as np from .config import CONFIG -from .core import PlausibilityError, Scalar, ScenarioData, TimestepData +from .core import PlausibilityError, Scalar, ScenarioData, TimestepData, extract_data from .effects import EffectValuesUserTimestep from .features import InvestmentModel, OnOffModel, PreventSimultaneousUsageModel from .interface import InvestParameters, OnOffParameters @@ -375,8 +375,8 @@ def do_modeling(self): self.total_flow_hours = self.add( self._model.add_variables( - lower=self.element.flow_hours_total_min if self.element.flow_hours_total_min is not None else 0, - upper=self.element.flow_hours_total_max if self.element.flow_hours_total_max is not None else np.inf, + lower=extract_data(self.element.flow_hours_total_min, 0), + upper=extract_data(self.element.flow_hours_total_max, np.inf), coords=self._model.get_coords(time_dim=False), name=f'{self.label_full}|total_flow_hours', ), @@ -456,16 +456,16 @@ def flow_rate_lower_bound_relative(self) -> TimestepData: """Returns the lower bound of the flow_rate relative to its size""" fixed_profile = self.element.fixed_relative_profile if fixed_profile is None: - return self.element.relative_minimum.selected_data - return fixed_profile.selected_data + return extract_data(self.element.relative_minimum) + return extract_data(fixed_profile) @property def flow_rate_upper_bound_relative(self) -> TimestepData: """ Returns the upper bound of the flow_rate relative to its size""" fixed_profile = self.element.fixed_relative_profile if fixed_profile is None: - return self.element.relative_maximum.selected_data - return fixed_profile.selected_data + return extract_data(self.element.relative_maximum) + return extract_data(fixed_profile) @property def flow_rate_lower_bound(self) -> TimestepData: @@ -478,8 +478,8 @@ def flow_rate_lower_bound(self) -> TimestepData: if isinstance(self.element.size, InvestParameters): if self.element.size.optional: return 0 - return self.flow_rate_lower_bound_relative * self.element.size.minimum_size - return self.flow_rate_lower_bound_relative * self.element.size + return self.flow_rate_lower_bound_relative * extract_data(self.element.size.minimum_size) + return self.flow_rate_lower_bound_relative * extract_data(self.element.size) @property def flow_rate_upper_bound(self) -> TimestepData: @@ -488,8 +488,8 @@ def flow_rate_upper_bound(self) -> TimestepData: Further constraining might be done in OnOffModel and InvestmentModel """ if isinstance(self.element.size, InvestParameters): - return self.flow_rate_upper_bound_relative * self.element.size.maximum_size - return self.flow_rate_upper_bound_relative * self.element.size + return self.flow_rate_upper_bound_relative * extract_data(self.element.size.maximum_size) + return self.flow_rate_upper_bound_relative * extract_data(self.element.size) class BusModel(ElementModel): diff --git a/flixopt/features.py b/flixopt/features.py index 31c1dbadb..b8243d794 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -10,7 +10,7 @@ import numpy as np from .config import CONFIG -from .core import Scalar, ScenarioData, TimeSeries, TimestepData +from .core import Scalar, ScenarioData, TimeSeries, TimestepData, extract_data from .interface import InvestParameters, OnOffParameters, Piecewise from .structure import Model, SystemModel @@ -45,8 +45,8 @@ def __init__( def do_modeling(self): self.size = self.add( self._model.add_variables( - lower=0 if self.parameters.optional else self.parameters.minimum_size*1, - upper=self.parameters.maximum_size*1, + lower=0 if self.parameters.optional else extract_data(self.parameters.minimum_size), + upper=extract_data(self.parameters.maximum_size), name=f'{self.label_full}|size', coords=self._model.get_coords(time_dim=False), ), @@ -295,8 +295,8 @@ def do_modeling(self): self.total_on_hours = self.add( self._model.add_variables( - lower=self._on_hours_total_min, - upper=self._on_hours_total_max, + lower=extract_data(self._on_hours_total_min), + upper=extract_data(self._on_hours_total_max), coords=self._model.get_coords(time_dim=False), name=f'{self.label_full}|on_hours_total', ), @@ -440,7 +440,7 @@ def do_modeling(self): # Create count variable for number of switches self.switch_on_nr = self.add( self._model.add_variables( - upper=self._switch_on_max, + upper=extract_data(self._switch_on_max), lower=0, name=f'{self.label_full}|switch_on_nr', ), @@ -534,7 +534,7 @@ def do_modeling(self): self.duration = self.add( self._model.add_variables( lower=0, - upper=self._maximum_duration if self._maximum_duration is not None else mega, + upper=extract_data(self._maximum_duration, mega), coords=self._model.get_coords(), name=f'{self.label_full}|hours', ), @@ -588,7 +588,7 @@ def do_modeling(self): ) # Handle initial condition - if 0 < self.previous_duration < self._minimum_duration.isel(time=0): + if 0 < self.previous_duration < self._minimum_duration.isel(time=0).max(): self.add( self._model.add_constraints( self._state_variable.isel(time=0) == 1, name=f'{self.label_full}|initial_minimum' @@ -613,7 +613,7 @@ def previous_duration(self) -> Scalar: """Computes the previous duration of the state variable""" #TODO: Allow for other/dynamic timestep resolutions return ConsecutiveStateModel.compute_consecutive_hours_in_state( - self._previous_states, self._model.hours_per_step.isel(time=0).item() + self._previous_states, self._model.hours_per_step.isel(time=0).values.flatten()[0] ) @staticmethod @@ -715,8 +715,8 @@ def do_modeling(self): defining_bounds=self._defining_bounds, previous_values=self._previous_values, use_off=self.parameters.use_off, - on_hours_total_min=self.parameters.on_hours_total_min, - on_hours_total_max=self.parameters.on_hours_total_max, + on_hours_total_min=extract_data(self.parameters.on_hours_total_min), + on_hours_total_max=extract_data(self.parameters.on_hours_total_max), effects_per_running_hour=self.parameters.effects_per_running_hour, ) self.add(self.state_model) @@ -965,8 +965,8 @@ def __init__( label_of_element: Optional[str] = None, label: Optional[str] = None, label_full: Optional[str] = None, - total_max: Optional[Scalar] = None, - total_min: Optional[Scalar] = None, + total_max: Optional[ScenarioData] = None, + total_min: Optional[ScenarioData] = None, max_per_hour: Optional[TimestepData] = None, min_per_hour: Optional[TimestepData] = None, ): @@ -1009,8 +1009,8 @@ def do_modeling(self): if self._has_time_dim: self.total_per_timestep = self.add( self._model.add_variables( - lower=-np.inf if (self._min_per_hour is None) else self._min_per_hour * self._model.hours_per_step, - upper=np.inf if (self._max_per_hour is None) else self._max_per_hour * self._model.hours_per_step, + lower=-np.inf if (self._min_per_hour is None) else extract_data(self._min_per_hour) * self._model.hours_per_step, + upper=np.inf if (self._max_per_hour is None) else extract_data(self._max_per_hour) * self._model.hours_per_step, coords=self._model.get_coords(time_dim=True, scenario_dim=self._has_scenario_dim), name=f'{self.label_full}|total_per_timestep', ), diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py new file mode 100644 index 000000000..5b9105a68 --- /dev/null +++ b/tests/test_scenarios.py @@ -0,0 +1,333 @@ +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd +from linopy.testing import assert_linequal +import xarray as xr +import pytest +import flixopt as fx + +from flixopt.commons import Effect, FullCalculation, InvestParameters, Sink, Source, Storage, TimeSeriesData, solvers +from flixopt.elements import Bus, Flow +from flixopt.flow_system import FlowSystem + +from .conftest import create_linopy_model, create_calculation_and_solve + + +@pytest.fixture +def test_system(): + """Create a basic test system with scenarios.""" + # Create a two-day time index with hourly resolution + timesteps = pd.date_range( + "2023-01-01", periods=48, freq="h", name="time" + ) + + # Create two scenarios + scenarios = pd.Index(["Scenario A", "Scenario B"], name="scenario") + + # Create scenario weights as TimeSeriesData + # Using TimeSeriesData to avoid conversion issues + scenario_weights = TimeSeriesData(np.array([0.7, 0.3])) + + # Create a flow system with scenarios + flow_system = FlowSystem( + timesteps=timesteps, + scenarios=scenarios, + scenario_weights=scenario_weights # Use TimeSeriesData for weights + ) + + # Create demand profiles that differ between scenarios + # Scenario A: Higher demand in first day, lower in second day + # Scenario B: Lower demand in first day, higher in second day + demand_profile_a = np.concatenate([ + np.sin(np.linspace(0, 2*np.pi, 24)) * 5 + 10, # Day 1, max ~15 + np.sin(np.linspace(0, 2*np.pi, 24)) * 2 + 5 # Day 2, max ~7 + ]) + + demand_profile_b = np.concatenate([ + np.sin(np.linspace(0, 2*np.pi, 24)) * 2 + 5, # Day 1, max ~7 + np.sin(np.linspace(0, 2*np.pi, 24)) * 5 + 10 # Day 2, max ~15 + ]) + + # Stack the profiles into a 2D array (time, scenario) + demand_profiles = np.column_stack([demand_profile_a, demand_profile_b]) + + # Create the necessary model elements + # Create buses + electricity_bus = Bus("Electricity") + + # Create a demand sink with scenario-dependent profiles + demand = Flow( + label="Demand", + bus=electricity_bus.label_full, + fixed_relative_profile=demand_profiles + ) + demand_sink = Sink("Demand", sink=demand) + + # Create a power source with investment option + power_gen = Flow( + label="Generation", + bus=electricity_bus.label_full, + size=InvestParameters( + minimum_size=0, + maximum_size=20, + specific_effects={"Costs": 100} # €/kW + ), + effects_per_flow_hour={"Costs": 20} # €/MWh + ) + generator = Source("Generator", source=power_gen) + + # Create a storage for electricity + storage_charge = Flow( + label="Charge", + bus=electricity_bus.label_full, + size=10 + ) + storage_discharge = Flow( + label="Discharge", + bus=electricity_bus.label_full, + size=10 + ) + storage = Storage( + label="Battery", + charging=storage_charge, + discharging=storage_discharge, + capacity_in_flow_hours=InvestParameters( + minimum_size=0, + maximum_size=50, + specific_effects={"Costs": 50} # €/kWh + ), + eta_charge=0.95, + eta_discharge=0.95, + initial_charge_state="lastValueOfSim" + ) + + # Create effects and objective + cost_effect = Effect( + label="Costs", + unit="€", + description="Total costs", + is_standard=True, + is_objective=True + ) + + # Add all elements to the flow system + flow_system.add_elements( + electricity_bus, + generator, + demand_sink, + storage, + cost_effect + ) + + # Return the created system and its components + return { + "flow_system": flow_system, + "timesteps": timesteps, + "scenarios": scenarios, + "electricity_bus": electricity_bus, + "demand": demand, + "demand_sink": demand_sink, + "generator": generator, + "power_gen": power_gen, + "storage": storage, + "storage_charge": storage_charge, + "storage_discharge": storage_discharge, + "cost_effect": cost_effect + } + +@pytest.fixture +def flow_system_complex_scenarios() -> fx.FlowSystem: + """ + Helper method to create a base model with configurable parameters + """ + thermal_load = np.array([30, 0, 90, 110, 110, 20, 20, 20, 20]) + electrical_load = np.array([40, 40, 40, 40, 40, 40, 40, 40, 40]) + flow_system = fx.FlowSystem(pd.date_range('2020-01-01', periods=9, freq='h', name='time'), + pd.Index(['A', 'B', 'C'], name='scenario')) + # Define the components and flow_system + flow_system.add_elements( + fx.Effect('costs', '€', 'Kosten', is_standard=True, is_objective=True), + fx.Effect('CO2', 'kg', 'CO2_e-Emissionen', specific_share_to_other_effects_operation={'costs': 0.2}), + fx.Effect('PE', 'kWh_PE', 'Primärenergie', maximum_total=3.5e3), + fx.Bus('Strom'), + fx.Bus('Fernwärme'), + fx.Bus('Gas'), + fx.Sink('Wärmelast', sink=fx.Flow('Q_th_Last', 'Fernwärme', size=1, fixed_relative_profile=thermal_load)), + fx.Source( + 'Gastarif', source=fx.Flow('Q_Gas', 'Gas', size=1000, effects_per_flow_hour={'costs': 0.04, 'CO2': 0.3}) + ), + fx.Sink('Einspeisung', sink=fx.Flow('P_el', 'Strom', effects_per_flow_hour=-1 * electrical_load)), + ) + + boiler = fx.linear_converters.Boiler( + 'Kessel', + eta=0.5, + on_off_parameters=fx.OnOffParameters(effects_per_running_hour={'costs': 0, 'CO2': 1000}), + Q_th=fx.Flow( + 'Q_th', + bus='Fernwärme', + load_factor_max=1.0, + load_factor_min=0.1, + relative_minimum=5 / 50, + relative_maximum=1, + previous_flow_rate=50, + size=fx.InvestParameters( + fix_effects=1000, fixed_size=50, optional=False, specific_effects={'costs': 10, 'PE': 2} + ), + on_off_parameters=fx.OnOffParameters( + on_hours_total_min=0, + on_hours_total_max=1000, + consecutive_on_hours_max=10, + consecutive_on_hours_min=1, + consecutive_off_hours_max=10, + effects_per_switch_on=0.01, + switch_on_total_max=1000, + ), + flow_hours_total_max=1e6, + ), + Q_fu=fx.Flow('Q_fu', bus='Gas', size=200, relative_minimum=0, relative_maximum=1), + ) + + invest_speicher = fx.InvestParameters( + fix_effects=0, + piecewise_effects=fx.PiecewiseEffects( + piecewise_origin=fx.Piecewise([fx.Piece(5, 25), fx.Piece(25, 100)]), + piecewise_shares={ + 'costs': fx.Piecewise([fx.Piece(50, 250), fx.Piece(250, 800)]), + 'PE': fx.Piecewise([fx.Piece(5, 25), fx.Piece(25, 100)]), + }, + ), + optional=False, + specific_effects={'costs': 0.01, 'CO2': 0.01}, + minimum_size=0, + maximum_size=1000, + ) + speicher = fx.Storage( + 'Speicher', + charging=fx.Flow('Q_th_load', bus='Fernwärme', size=1e4), + discharging=fx.Flow('Q_th_unload', bus='Fernwärme', size=1e4), + capacity_in_flow_hours=invest_speicher, + initial_charge_state=0, + maximal_final_charge_state=10, + eta_charge=0.9, + eta_discharge=1, + relative_loss_per_hour=0.08, + prevent_simultaneous_charge_and_discharge=True, + ) + + flow_system.add_elements(boiler, speicher) + + return flow_system + + +@pytest.fixture +def flow_system_piecewise_conversion_scenarios(flow_system_complex_scenarios) -> fx.FlowSystem: + """ + Use segments/Piecewise with numeric data + """ + flow_system = flow_system_complex_scenarios + + flow_system.add_elements( + fx.LinearConverter( + 'KWK', + inputs=[fx.Flow('Q_fu', bus='Gas')], + outputs=[ + fx.Flow('P_el', bus='Strom', size=60, relative_maximum=55, previous_flow_rate=10), + fx.Flow('Q_th', bus='Fernwärme'), + ], + piecewise_conversion=fx.PiecewiseConversion( + { + 'P_el': fx.Piecewise( + [ + fx.Piece(np.linspace(5, 6, len(flow_system.time_series_collection.timesteps)), 30), + fx.Piece(40, np.linspace(60, 70, len(flow_system.time_series_collection.timesteps))), + ] + ), + 'Q_th': fx.Piecewise([fx.Piece(6, 35), fx.Piece(45, 100)]), + 'Q_fu': fx.Piecewise([fx.Piece(12, 70), fx.Piece(90, 200)]), + } + ), + on_off_parameters=fx.OnOffParameters(effects_per_switch_on=0.01), + ) + ) + + return flow_system + + +def test_scenario_weights(flow_system_piecewise_conversion_scenarios): + """Test that scenario weights are correctly used in the model.""" + scenarios = flow_system_piecewise_conversion_scenarios.time_series_collection.scenarios + weights = np.linspace(0.5, 1, len(scenarios)) / np.sum(np.linspace(0.5, 1, len(scenarios))) + flow_system_piecewise_conversion_scenarios.scenario_weights = weights + model = create_linopy_model(flow_system_piecewise_conversion_scenarios) + np.testing.assert_allclose(model.scenario_weights.values, weights) + assert_linequal(model.objective.expression, + (model.variables['costs|total'] * weights).sum() + model.variables['Penalty|total']) + assert np.isclose(model.scenario_weights.sum().item(), 1.0) + +def test_scenario_dimensions_in_variables(flow_system_piecewise_conversion_scenarios): + """Test that all time variables are correctly broadcasted to scenario dimensions.""" + model = create_linopy_model(flow_system_piecewise_conversion_scenarios) + for var in model.variables: + assert model.variables[var].dims in [('time', 'scenario'), ('scenario',), ()] + +def test_full_scenario_optimization(flow_system_piecewise_conversion_scenarios): + """Test a full optimization with scenarios and verify results.""" + scenarios = flow_system_piecewise_conversion_scenarios.time_series_collection.scenarios + weights = np.linspace(0.5, 1, len(scenarios)) / np.sum(np.linspace(0.5, 1, len(scenarios))) + flow_system_piecewise_conversion_scenarios.scenario_weights = weights + calc = create_calculation_and_solve(flow_system_piecewise_conversion_scenarios, + solver=fx.solvers.GurobiSolver(mip_gap=0.01, time_limit_seconds=60), + name='test_full_scenario') + calc.results.to_file() + + res = fx.results.CalculationResults.from_file('results', 'test_full_scenario') + fx.FlowSystem.from_dataset(res.flow_system) + calc = create_calculation_and_solve( + flow_system_piecewise_conversion_scenarios, + solver=fx.solvers.GurobiSolver(mip_gap=0.01, time_limit_seconds=60), + name='test_full_scenario', + ) + +@pytest.mark.slow +def test_io_persistance(flow_system_piecewise_conversion_scenarios): + """Test a full optimization with scenarios and verify results.""" + scenarios = flow_system_piecewise_conversion_scenarios.time_series_collection.scenarios + weights = np.linspace(0.5, 1, len(scenarios)) / np.sum(np.linspace(0.5, 1, len(scenarios))) + flow_system_piecewise_conversion_scenarios.scenario_weights = weights + calc = create_calculation_and_solve(flow_system_piecewise_conversion_scenarios, + solver=fx.solvers.HighsSolver(mip_gap=0.001, time_limit_seconds=60), + name='test_full_scenario') + calc.results.to_file() + + res = fx.results.CalculationResults.from_file('results', 'test_full_scenario') + flow_system_2 = fx.FlowSystem.from_dataset(res.flow_system) + calc_2 = create_calculation_and_solve( + flow_system_2, + solver=fx.solvers.HighsSolver(mip_gap=0.001, time_limit_seconds=60), + name='test_full_scenario_2', + ) + + np.testing.assert_allclose(calc.results.objective, calc_2.results.objective, rtol=0.001) + + +def test_scenarios_selection(flow_system_piecewise_conversion_scenarios): + flow_system = flow_system_piecewise_conversion_scenarios + scenarios = flow_system_piecewise_conversion_scenarios.time_series_collection.scenarios + weights = np.linspace(0.5, 1, len(scenarios)) / np.sum(np.linspace(0.5, 1, len(scenarios))) + flow_system_piecewise_conversion_scenarios.scenario_weights = weights + calc = fx.FullCalculation(flow_system=flow_system_piecewise_conversion_scenarios, + selected_scenarios=flow_system.time_series_collection.scenarios[0:2], + name='test_full_scenario') + calc.do_modeling() + calc.solve(fx.solvers.GurobiSolver(mip_gap=0.01, time_limit_seconds=60)) + + calc.results.to_file() + flow_system_2 = fx.FlowSystem.from_dataset(calc.results.flow_system) + + assert calc.results.solution.indexes['scenario'].equals(flow_system.time_series_collection.scenarios[0:2]) + + assert flow_system_2.time_series_collection.scenarios.equals(flow_system.time_series_collection.scenarios[0:2]) + + np.testing.assert_allclose(flow_system_2.scenario_weights.selected_data.values, weights[0:2]) + diff --git a/tests/test_timeseries.py b/tests/test_timeseries.py index e2432e784..237935e59 100644 --- a/tests/test_timeseries.py +++ b/tests/test_timeseries.py @@ -91,7 +91,7 @@ def test_selection_methods(self, sample_timeseries, sample_timesteps): assert sample_timeseries.selected_data.equals(sample_timeseries.stored_data.sel(time=subset_index)) # Clear selection - sample_timeseries.clear_selection() + sample_timeseries.set_selection() assert sample_timeseries._selected_timesteps is None assert sample_timeseries.selected_data.equals(sample_timeseries.stored_data) @@ -408,7 +408,7 @@ def test_scenario_selection(self, sample_scenario_timeseries, sample_scenario_in ) # Clear selection - sample_scenario_timeseries.clear_selection(timesteps=False, scenarios=True) + sample_scenario_timeseries.set_selection() assert sample_scenario_timeseries._selected_scenarios is None def test_all_equal_with_scenarios(self, sample_timesteps, sample_scenario_index): @@ -561,7 +561,7 @@ def test_selection_propagation(self, sample_allocator, sample_timesteps): assert len(ts3._selected_timesteps) == len(subset_timesteps) + 1 # Clear selection - sample_allocator.clear_selection() + sample_allocator.set_selection() # Check selection cleared assert ts1._selected_timesteps is None @@ -673,7 +673,7 @@ def test_selection_propagation_with_scenarios( assert ts1.selected_data.shape == (2, 2) # 2 timesteps, 2 scenarios # Clear selections - sample_scenario_allocator.clear_selection() + sample_scenario_allocator.set_selection() assert ts1._selected_timesteps is None assert ts1.active_timesteps.equals(sample_scenario_allocator.timesteps) assert ts1._selected_scenarios is None From 9ea8fba7fc8c4d9d46f76391b8fd3deceb93bcf9 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 15 Apr 2025 16:43:04 +0200 Subject: [PATCH 025/448] ruff check --- flixopt/core.py | 2 +- tests/test_scenarios.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/flixopt/core.py b/flixopt/core.py index 850e01c04..eab45e239 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -1479,4 +1479,4 @@ def extract_data( return data if isinstance(data, (int, float, np.integer, np.floating)): return xr.DataArray(data) - raise TypeError(f'Unsupported data type: {type(data).__name__}') \ No newline at end of file + raise TypeError(f'Unsupported data type: {type(data).__name__}') diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py index 5b9105a68..62b0d3e29 100644 --- a/tests/test_scenarios.py +++ b/tests/test_scenarios.py @@ -1,16 +1,16 @@ import matplotlib.pyplot as plt import numpy as np import pandas as pd -from linopy.testing import assert_linequal -import xarray as xr import pytest -import flixopt as fx +import xarray as xr +from linopy.testing import assert_linequal +import flixopt as fx from flixopt.commons import Effect, FullCalculation, InvestParameters, Sink, Source, Storage, TimeSeriesData, solvers from flixopt.elements import Bus, Flow from flixopt.flow_system import FlowSystem -from .conftest import create_linopy_model, create_calculation_and_solve +from .conftest import create_calculation_and_solve, create_linopy_model @pytest.fixture From 7f4fddb942bda850f8cdbd25c09e8ccf34951011 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 15 Apr 2025 22:40:23 +0200 Subject: [PATCH 026/448] Bugfix in _create_bounds_for_scenarios() --- flixopt/features.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index b8243d794..4eeb46337 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -194,8 +194,10 @@ def _create_bounds_for_defining_variable(self): # anmerkung: Glg bei Spezialfall relative_minimum = 0 redundant zu OnOff ?? def _create_bounds_for_scenarios(self): - if self.parameters.investment_scenarios == 'individual': - return + if isinstance(self.parameters.investment_scenarios, str): + if self.parameters.investment_scenarios == 'individual': + return + raise ValueError(f'Invalid value for investment_scenarios: {self.parameters.investment_scenarios}') if self.parameters.investment_scenarios is None: self.add( From e2192daba3f20349bbf4d16482068aa26385ee46 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 16 Apr 2025 09:46:52 +0200 Subject: [PATCH 027/448] exclude super long test --- tests/test_scenarios.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py index 62b0d3e29..76da9e7ac 100644 --- a/tests/test_scenarios.py +++ b/tests/test_scenarios.py @@ -289,7 +289,7 @@ def test_full_scenario_optimization(flow_system_piecewise_conversion_scenarios): name='test_full_scenario', ) -@pytest.mark.slow +@pytest.skip def test_io_persistance(flow_system_piecewise_conversion_scenarios): """Test a full optimization with scenarios and verify results.""" scenarios = flow_system_piecewise_conversion_scenarios.time_series_collection.scenarios From 4ad86d5a3f7bf1ca2b47d97ec82349726c88023a Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 16 Apr 2025 10:10:15 +0200 Subject: [PATCH 028/448] exclude super long test - fix --- tests/test_scenarios.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py index 76da9e7ac..89b5feced 100644 --- a/tests/test_scenarios.py +++ b/tests/test_scenarios.py @@ -289,7 +289,7 @@ def test_full_scenario_optimization(flow_system_piecewise_conversion_scenarios): name='test_full_scenario', ) -@pytest.skip +@pytest.mark.skip(reason="This test is taking too long with highs and is too big for gurobipy free") def test_io_persistance(flow_system_piecewise_conversion_scenarios): """Test a full optimization with scenarios and verify results.""" scenarios = flow_system_piecewise_conversion_scenarios.time_series_collection.scenarios @@ -330,4 +330,3 @@ def test_scenarios_selection(flow_system_piecewise_conversion_scenarios): assert flow_system_2.time_series_collection.scenarios.equals(flow_system.time_series_collection.scenarios[0:2]) np.testing.assert_allclose(flow_system_2.scenario_weights.selected_data.values, weights[0:2]) - From 967174c1f33224f2fb06b15d318fe4340ee90323 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 17 Apr 2025 15:01:19 +0200 Subject: [PATCH 029/448] Bugfix plot_node_balance_pie() --- flixopt/results.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flixopt/results.py b/flixopt/results.py index ae54b9e2e..f5765286a 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -532,13 +532,13 @@ def plot_node_balance_pie( drop_suffix: Whether to drop the suffix from the variable names. """ inputs = sanitize_dataset( - ds=self.solution[self.inputs], + ds=self.solution[self.inputs] * self._calculation_results.hours_per_timestep, threshold=1e-5, drop_small_vars=True, zero_small_values=True, ) outputs = sanitize_dataset( - ds=self.solution[self.outputs], + ds=self.solution[self.outputs] * self._calculation_results.hours_per_timestep, threshold=1e-5, drop_small_vars=True, zero_small_values=True, From d24b5e77a57a39b767843880008e5d1a18653a4a Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 22 Apr 2025 10:28:29 +0200 Subject: [PATCH 030/448] Scenarios/fixes (#252) * BUGFIX missing conversion to TimeSeries * BUGFIX missing conversion to TimeSeries * Bugfix node_balance with flow_hours: Negate correctly --- flixopt/effects.py | 24 +++++++++++++++++++++++- flixopt/results.py | 12 ++++++++---- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/flixopt/effects.py b/flixopt/effects.py index 2da561a36..3d93ee76a 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -97,11 +97,33 @@ def transform_data(self, flow_system: 'FlowSystem'): self.maximum_operation_per_hour = flow_system.create_time_series( f'{self.label_full}|maximum_operation_per_hour', self.maximum_operation_per_hour, flow_system ) - self.specific_share_to_other_effects_operation = flow_system.create_effect_time_series( f'{self.label_full}|operation->', self.specific_share_to_other_effects_operation, 'operation' ) + self.minimum_operation = flow_system.create_time_series( + f'{self.label_full}|minimum_operation', self.minimum_operation, has_time_dim=False + ) + self.maximum_operation = flow_system.create_time_series( + f'{self.label_full}|maximum_operation', self.maximum_operation, has_time_dim=False + ) + self.minimum_invest = flow_system.create_time_series( + f'{self.label_full}|minimum_invest', self.minimum_invest, has_time_dim=False + ) + self.maximum_invest = flow_system.create_time_series( + f'{self.label_full}|maximum_invest', self.maximum_invest, has_time_dim=False + ) + self.minimum_total = flow_system.create_time_series( + f'{self.label_full}|minimum_total', self.minimum_total, has_time_dim=False, + ) + self.maximum_total = flow_system.create_time_series( + f'{self.label_full}|maximum_total', self.maximum_total, has_time_dim=False + ) + self.specific_share_to_other_effects_invest = flow_system.create_effect_time_series( + f'{self.label_full}|invest->', self.specific_share_to_other_effects_invest, 'invest', + has_time_dim=False + ) + def create_model(self, model: SystemModel) -> 'EffectModel': self._plausibility_checks() self.model = EffectModel(model, self) diff --git a/flixopt/results.py b/flixopt/results.py index f5765286a..b57af1fb7 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -621,10 +621,8 @@ def node_balance( ds = self.solution[self.inputs + self.outputs] if drop_suffix: ds = ds.rename_vars({var: var.split('|flow_hours')[0] for var in ds.data_vars}) - if mode == 'flow_hours': - ds = ds * self._calculation_results.hours_per_timestep - ds = ds.rename_vars({var: var.replace('flow_rate', 'flow_hours') for var in ds.data_vars}) - return sanitize_dataset( + + ds = sanitize_dataset( ds=ds, threshold=threshold, timesteps=self._calculation_results.timesteps_extra if with_last_timestep else None, @@ -639,6 +637,12 @@ def node_balance( ), ) + if mode == 'flow_hours': + ds = ds * self._calculation_results.hours_per_timestep + ds = ds.rename_vars({var: var.replace('flow_rate', 'flow_hours') for var in ds.data_vars}) + + return ds + class BusResults(_NodeResults): """Results for a Bus""" From a3c7d472cbaf514a79eba4f655186908da19c765 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 22 Apr 2025 10:28:49 +0200 Subject: [PATCH 031/448] Scenarios/filter (#253) * Add containts and startswith to filter_solution --- flixopt/results.py | 90 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 67 insertions(+), 23 deletions(-) diff --git a/flixopt/results.py b/flixopt/results.py index b57af1fb7..1be662975 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -202,6 +202,8 @@ def filter_solution( element: Optional[str] = None, timesteps: Optional[pd.DatetimeIndex] = None, scenarios: Optional[pd.Index] = None, + contains: Optional[Union[str, List[str]]] = None, + startswith: Optional[Union[str, List[str]]] = None, ) -> xr.Dataset: """ Filter the solution to a specific variable dimension and element. @@ -223,12 +225,18 @@ def filter_solution( - pd.Index: Multiple scenarios - str/int: Single scenario (int is treated as a label, not an index position) Defaults to all available scenarios. + contains: Filter variables that contain this string or strings. + If a list is provided, variables must contain ALL strings in the list. + startswith: Filter variables that start with this string or strings. + If a list is provided, variables must start with ANY of the strings in the list. """ return filter_dataset( self.solution if element is None else self[element].solution, variable_dims=variable_dims, timesteps=timesteps, scenarios=scenarios, + contains=contains, + startswith=startswith, ) def plot_heatmap( @@ -393,6 +401,8 @@ def filter_solution( variable_dims: Optional[Literal['scalar', 'time', 'scenario', 'timeonly', 'scenarioonly']] = None, timesteps: Optional[pd.DatetimeIndex] = None, scenarios: Optional[pd.Index] = None, + contains: Optional[Union[str, List[str]]] = None, + startswith: Optional[Union[str, List[str]]] = None, ) -> xr.Dataset: """ Filter the solution to a specific variable dimension and element. @@ -413,12 +423,18 @@ def filter_solution( - pd.Index: Multiple scenarios - str/int: Single scenario (int is treated as a label, not an index position) Defaults to all available scenarios. + contains: Filter variables that contain this string or strings. + If a list is provided, variables must contain ALL strings in the list. + startswith: Filter variables that start with this string or strings. + If a list is provided, variables must start with ANY of the strings in the list. """ return filter_dataset( self.solution, variable_dims=variable_dims, timesteps=timesteps, scenarios=scenarios, + contains=contains, + startswith=startswith, ) @@ -1017,9 +1033,11 @@ def filter_dataset( variable_dims: Optional[Literal['scalar', 'time', 'scenario', 'timeonly', 'scenarioonly']] = None, timesteps: Optional[Union[pd.DatetimeIndex, str, pd.Timestamp]] = None, scenarios: Optional[Union[pd.Index, str, int]] = None, + contains: Optional[Union[str, List[str]]] = None, + startswith: Optional[Union[str, List[str]]] = None, ) -> xr.Dataset: """ - Filters a dataset by its dimensions and optionally selects specific indexes. + Filters a dataset by its dimensions, indexes, and with string filters for variable names. Args: ds: The dataset to filter. @@ -1037,32 +1055,58 @@ def filter_dataset( - pd.Index: Multiple scenarios - str/int: Single scenario (int is treated as a label, not an index position) Defaults to all available scenarios. + contains: Filter variables that contain this string or strings. + If a list is provided, variables must contain ALL strings in the list. + startswith: Filter variables that start with this string or strings. + If a list is provided, variables must start with ANY of the strings in the list. Returns: Filtered dataset with specified variables and indexes. """ - # Return the full dataset if all dimension types are included - if variable_dims is None: - pass - elif variable_dims == 'scalar': - ds = ds[[v for v in ds.data_vars if not ds[v].dims]] - elif variable_dims == 'time': - ds = ds[[v for v in ds.data_vars if 'time' in ds[v].dims]] - elif variable_dims == 'scenario': - ds = ds[[v for v in ds.data_vars if 'scenario' in ds[v].dims]] - elif variable_dims == 'timeonly': - ds = ds[[v for v in ds.data_vars if ds[v].dims == ('time',)]] - elif variable_dims == 'scenarioonly': - ds = ds[[v for v in ds.data_vars if ds[v].dims == ('scenario',)]] - else: - raise ValueError(f'Unknown variable_dims "{variable_dims}" for filter_dataset') + # First filter by dimensions + filtered_ds = ds.copy() + if variable_dims is not None: + if variable_dims == 'scalar': + filtered_ds = filtered_ds[[v for v in filtered_ds.data_vars if not filtered_ds[v].dims]] + elif variable_dims == 'time': + filtered_ds = filtered_ds[[v for v in filtered_ds.data_vars if 'time' in filtered_ds[v].dims]] + elif variable_dims == 'scenario': + filtered_ds = filtered_ds[[v for v in filtered_ds.data_vars if 'scenario' in filtered_ds[v].dims]] + elif variable_dims == 'timeonly': + filtered_ds = filtered_ds[[v for v in filtered_ds.data_vars if filtered_ds[v].dims == ('time',)]] + elif variable_dims == 'scenarioonly': + filtered_ds = filtered_ds[[v for v in filtered_ds.data_vars if filtered_ds[v].dims == ('scenario',)]] + else: + raise ValueError(f'Unknown variable_dims "{variable_dims}" for filter_dataset') + + # Filter by 'contains' parameter + if contains is not None: + if isinstance(contains, str): + # Single string - keep variables that contain this string + filtered_ds = filtered_ds[[v for v in filtered_ds.data_vars if contains in v]] + elif isinstance(contains, list) and all(isinstance(s, str) for s in contains): + # List of strings - keep variables that contain ALL strings in the list + filtered_ds = filtered_ds[[v for v in filtered_ds.data_vars if all(s in v for s in contains)]] + else: + raise TypeError(f"'contains' must be a string or list of strings, got {type(contains)}") + + # Filter by 'startswith' parameter + if startswith is not None: + if isinstance(startswith, str): + # Single string - keep variables that start with this string + filtered_ds = filtered_ds[[v for v in filtered_ds.data_vars if v.startswith(startswith)]] + elif isinstance(startswith, list) and all(isinstance(s, str) for s in startswith): + # List of strings - keep variables that start with ANY of the strings in the list + filtered_ds = filtered_ds[[v for v in filtered_ds.data_vars if any(v.startswith(s) for s in startswith)]] + else: + raise TypeError(f"'startswith' must be a string or list of strings, got {type(startswith)}") # Handle time selection if needed - if timesteps is not None and 'time' in ds.dims: + if timesteps is not None and 'time' in filtered_ds.dims: try: - ds = ds.sel(time=timesteps) + filtered_ds = filtered_ds.sel(time=timesteps) except KeyError as e: - available_times = set(ds.indexes['time']) + available_times = set(filtered_ds.indexes['time']) requested_times = set([timesteps]) if not isinstance(timesteps, pd.Index) else set(timesteps) missing_times = requested_times - available_times raise ValueError( @@ -1070,15 +1114,15 @@ def filter_dataset( ) from e # Handle scenario selection if needed - if scenarios is not None and 'scenario' in ds.dims: + if scenarios is not None and 'scenario' in filtered_ds.dims: try: - ds = ds.sel(scenario=scenarios) + filtered_ds = filtered_ds.sel(scenario=scenarios) except KeyError as e: - available_scenarios = set(ds.indexes['scenario']) + available_scenarios = set(filtered_ds.indexes['scenario']) requested_scenarios = set([scenarios]) if not isinstance(scenarios, pd.Index) else set(scenarios) missing_scenarios = requested_scenarios - available_scenarios raise ValueError( f'Scenarios not found in dataset: {missing_scenarios}. Available scenarios: {available_scenarios}' ) from e - return ds + return filtered_ds From 0977c1fffd20383aab98a45d88af0703f448ba8c Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 22 Apr 2025 10:32:50 +0200 Subject: [PATCH 032/448] Scenarios/drop suffix (#251) Drop suffixes in plots and add the option to drop suffixes to sanitize_dataset() --- flixopt/results.py | 41 ++++++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/flixopt/results.py b/flixopt/results.py index 1be662975..3a4989672 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -532,7 +532,6 @@ def plot_node_balance_pie( show: bool = True, engine: plotting.PlottingEngine = 'plotly', scenario: Optional[Union[str, int]] = None, - drop_suffix: bool = True, ) -> plotly.graph_objects.Figure: """ Plots a pie chart of the flow hours of the inputs and outputs of buses or components. @@ -552,12 +551,14 @@ def plot_node_balance_pie( threshold=1e-5, drop_small_vars=True, zero_small_values=True, + drop_suffix='|', ) outputs = sanitize_dataset( ds=self.solution[self.outputs] * self._calculation_results.hours_per_timestep, threshold=1e-5, drop_small_vars=True, zero_small_values=True, + drop_suffix='|', ) inputs = inputs.sum('time') outputs = outputs.sum('time') @@ -570,13 +571,6 @@ def plot_node_balance_pie( outputs = outputs.sel(scenario=chosen_scenario).drop_vars('scenario') title = f'{title} - {chosen_scenario}' - if drop_suffix: - inputs = inputs.rename_vars({var: var.split('|flow_rate')[0] for var in inputs}) - outputs = outputs.rename_vars({var: var.split('|flow_rate')[0] for var in outputs}) - else: - inputs = inputs.rename_vars({var: var.replace('flow_rate', 'flow_hours') for var in inputs}) - outputs = outputs.rename_vars({var: var.replace('flow_rate', 'flow_hours') for var in outputs}) - if engine == 'plotly': figure_like = plotting.dual_pie_with_plotly( data_left=inputs.to_pandas(), @@ -635,10 +629,12 @@ def node_balance( drop_suffix: Whether to drop the suffix from the variable names. """ ds = self.solution[self.inputs + self.outputs] - if drop_suffix: - ds = ds.rename_vars({var: var.split('|flow_hours')[0] for var in ds.data_vars}) - ds = sanitize_dataset( + if mode == 'flow_hours': + ds = ds * self._calculation_results.hours_per_timestep + ds = ds.rename_vars({var: var.replace('flow_rate', 'flow_hours') for var in ds.data_vars}) + + return sanitize_dataset( ds=ds, threshold=threshold, timesteps=self._calculation_results.timesteps_extra if with_last_timestep else None, @@ -651,14 +647,9 @@ def node_balance( if negate_inputs else None ), + drop_suffix='|' if drop_suffix else None, ) - if mode == 'flow_hours': - ds = ds * self._calculation_results.hours_per_timestep - ds = ds.rename_vars({var: var.replace('flow_rate', 'flow_hours') for var in ds.data_vars}) - - return ds - class BusResults(_NodeResults): """Results for a Bus""" @@ -976,6 +967,7 @@ def sanitize_dataset( negate: Optional[List[str]] = None, drop_small_vars: bool = True, zero_small_values: bool = False, + drop_suffix: Optional[str] = None, ) -> xr.Dataset: """ Sanitizes a dataset by handling small values (dropping or zeroing) and optionally reindexing the time axis. @@ -987,6 +979,7 @@ def sanitize_dataset( negate: The variables to negate. If None, no variables are negated. drop_small_vars: If True, drops variables where all values are below threshold. zero_small_values: If True, sets values below threshold to zero. + drop_suffix: Drop suffix of data var names. Split by the provided str. Returns: xr.Dataset: The sanitized dataset. @@ -1025,6 +1018,20 @@ def sanitize_dataset( if timesteps is not None and not ds.indexes['time'].equals(timesteps): ds = ds.reindex({'time': timesteps}, fill_value=np.nan) + if drop_suffix is not None: + if not isinstance(drop_suffix, str): + raise ValueError(f'Only pass str values to drop suffixes. Got {drop_suffix}') + unique_dict = {} + for var in ds.data_vars: + new_name = var.split(drop_suffix)[0] + + # If name already exists, keep original name + if new_name in unique_dict.values(): + unique_dict[var] = var + else: + unique_dict[var] = new_name + ds = ds.rename(unique_dict) + return ds From 5cf6e0eb070f7dc2d318d1050b04dc7e37e86989 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 22 Apr 2025 14:05:35 +0200 Subject: [PATCH 033/448] Scenarios/bar plot (#254) * Add stacked bar style to plotting methods * Rename mode to style (line, bar, area, ...) --- .../example_calculation_types.py | 8 +-- examples/04_Scenarios/scenario_example.py | 4 +- flixopt/plotting.py | 34 ++++++------- flixopt/results.py | 50 +++++++++++-------- 4 files changed, 54 insertions(+), 42 deletions(-) diff --git a/examples/03_Calculation_types/example_calculation_types.py b/examples/03_Calculation_types/example_calculation_types.py index 97b18e3c0..3a15b4e28 100644 --- a/examples/03_Calculation_types/example_calculation_types.py +++ b/examples/03_Calculation_types/example_calculation_types.py @@ -194,28 +194,28 @@ def get_solutions(calcs: List, variable: str) -> xr.Dataset: # --- Plotting for comparison --- fx.plotting.with_plotly( get_solutions(calculations, 'Speicher|charge_state').to_dataframe(), - mode='line', + style='line', title='Charge State Comparison', ylabel='Charge state', ).write_html('results/Charge State.html') fx.plotting.with_plotly( get_solutions(calculations, 'BHKW2(Q_th)|flow_rate').to_dataframe(), - mode='line', + style='line', title='BHKW2(Q_th) Flow Rate Comparison', ylabel='Flow rate', ).write_html('results/BHKW2 Thermal Power.html') fx.plotting.with_plotly( get_solutions(calculations, 'costs(operation)|total_per_timestep').to_dataframe(), - mode='line', + style='line', title='Operation Cost Comparison', ylabel='Costs [€]', ).write_html('results/Operation Costs.html') fx.plotting.with_plotly( pd.DataFrame(get_solutions(calculations, 'costs(operation)|total_per_timestep').to_dataframe().sum()).T, - mode='bar', + style='bar', title='Total Cost Comparison', ylabel='Costs [€]', ).update_layout(barmode='group').write_html('results/Total Costs.html') diff --git a/examples/04_Scenarios/scenario_example.py b/examples/04_Scenarios/scenario_example.py index 3edb6e7c0..b9932a016 100644 --- a/examples/04_Scenarios/scenario_example.py +++ b/examples/04_Scenarios/scenario_example.py @@ -111,13 +111,15 @@ # --- Analyze Results --- calculation.results['Fernwärme'].plot_node_balance_pie() - calculation.results['Fernwärme'].plot_node_balance() + calculation.results['Fernwärme'].plot_node_balance(style='stacked_bar') calculation.results['Storage'].plot_node_balance() calculation.results.plot_heatmap('CHP(Q_th)|flow_rate') # Convert the results for the storage component to a dataframe and display df = calculation.results['Storage'].node_balance_with_charge_state() print(df) + calculation.results['Storage'].plot_charge_state(engine='matplotlib') # Save results to file for later usage calculation.results.to_file() + fig, ax = calculation.results['Storage'].plot_charge_state(engine='matplotlib') diff --git a/flixopt/plotting.py b/flixopt/plotting.py index e4c440aaf..9ea19a686 100644 --- a/flixopt/plotting.py +++ b/flixopt/plotting.py @@ -209,7 +209,7 @@ def process_colors( def with_plotly( data: pd.DataFrame, - mode: Literal['bar', 'line', 'area'] = 'area', + style: Literal['stacked_bar', 'line', 'area'] = 'area', colors: ColorType = 'viridis', title: str = '', ylabel: str = '', @@ -222,7 +222,7 @@ def with_plotly( Args: data: A DataFrame containing the data to plot, where the index represents time (e.g., hours), and each column represents a separate data series. - mode: The plotting mode. Use 'bar' for stacked bar charts, 'line' for stepped lines, + style: The plotting style. Use 'stacked_bar' for stacked bar charts, 'line' for stepped lines, or 'area' for stacked area charts. colors: Color specification, can be: - A string with a colorscale name (e.g., 'viridis', 'plasma') @@ -235,7 +235,7 @@ def with_plotly( Returns: A Plotly figure object containing the generated plot. """ - assert mode in ['bar', 'line', 'area'], f"'mode' must be one of {['bar', 'line', 'area']}" + assert style in ['stacked_bar', 'line', 'area'], f"'style' must be one of {['stacked_bar', 'line', 'area']}" if data.empty: return go.Figure() @@ -243,7 +243,7 @@ def with_plotly( fig = fig if fig is not None else go.Figure() - if mode == 'bar': + if style == 'stacked_bar': for i, column in enumerate(data.columns): fig.add_trace( go.Bar( @@ -255,22 +255,22 @@ def with_plotly( ) fig.update_layout( - barmode='relative' if mode == 'bar' else None, + barmode='relative' if style == 'stacked_bar' else None, bargap=0, # No space between bars bargroupgap=0, # No space between groups of bars ) - elif mode == 'line': + elif style == 'line': for i, column in enumerate(data.columns): fig.add_trace( go.Scatter( x=data.index, y=data[column], - mode='lines', + style='lines', name=column, line=dict(shape='hv', color=processed_colors[i]), ) ) - elif mode == 'area': + elif style == 'area': data = data.copy() data[(data > -1e-5) & (data < 1e-5)] = 0 # Preventing issues with plotting # Split columns into positive, negative, and mixed categories @@ -293,7 +293,7 @@ def with_plotly( go.Scatter( x=data.index, y=data[column], - mode='lines', + style='lines', name=column, line=dict(shape='hv', color=colors_stacked[column]), fill='tonexty', @@ -306,7 +306,7 @@ def with_plotly( go.Scatter( x=data.index, y=data[column], - mode='lines', + style='lines', name=column, line=dict(shape='hv', color=colors_stacked[column], dash='dash'), ) @@ -345,7 +345,7 @@ def with_plotly( def with_matplotlib( data: pd.DataFrame, - mode: Literal['bar', 'line'] = 'bar', + style: Literal['stacked_bar', 'line'] = 'stacked_bar', colors: ColorType = 'viridis', title: str = '', ylabel: str = '', @@ -360,7 +360,7 @@ def with_matplotlib( Args: data: A DataFrame containing the data to plot. The index should represent time (e.g., hours), and each column represents a separate data series. - mode: Plotting mode. Use 'bar' for stacked bar charts or 'line' for stepped lines. + style: Plotting style. Use 'stacked_bar' for stacked bar charts or 'line' for stepped lines. colors: Color specification, can be: - A string with a colormap name (e.g., 'viridis', 'plasma') - A list of color strings (e.g., ['#ff0000', '#00ff00']) @@ -376,19 +376,19 @@ def with_matplotlib( A tuple containing the Matplotlib figure and axes objects used for the plot. Notes: - - If `mode` is 'bar', bars are stacked for both positive and negative values. + - If `style` is 'stacked_bar', bars are stacked for both positive and negative values. Negative values are stacked separately without extra labels in the legend. - - If `mode` is 'line', stepped lines are drawn for each data series. + - If `style` is 'line', stepped lines are drawn for each data series. - The legend is placed below the plot to accommodate multiple data series. """ - assert mode in ['bar', 'line'], f"'mode' must be one of {['bar', 'line']} for matplotlib" + assert style in ['stacked_bar', 'line'], f"'style' must be one of {['stacked_bar', 'line']} for matplotlib" if fig is None or ax is None: fig, ax = plt.subplots(figsize=figsize) processed_colors = ColorProcessor(engine='matplotlib').process_colors(colors, list(data.columns)) - if mode == 'bar': + if style == 'stacked_bar': cumulative_positive = np.zeros(len(data)) cumulative_negative = np.zeros(len(data)) width = data.index.to_series().diff().dropna().min() # Minimum time difference @@ -419,7 +419,7 @@ def with_matplotlib( ) cumulative_negative += negative_values.values - elif mode == 'line': + elif style == 'line': for i, column in enumerate(data.columns): ax.step(data.index, data[column], where='post', color=processed_colors[i], label=column) diff --git a/flixopt/results.py b/flixopt/results.py index 3a4989672..47c47f288 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -471,6 +471,7 @@ def plot_node_balance( engine: plotting.PlottingEngine = 'plotly', scenario: Optional[Union[str, int]] = None, mode: Literal['flow_rate', 'flow_hours'] = 'flow_rate', + style: Literal['area', 'stacked_bar', 'line'] = 'stacked_bar', drop_suffix: bool = True, ) -> Union[plotly.graph_objs.Figure, Tuple[plt.Figure, plt.Axes]]: """ @@ -499,7 +500,7 @@ def plot_node_balance( figure_like = plotting.with_plotly( ds.to_dataframe(), colors=colors, - mode='area', + style=style, title=title, ) default_filetype = '.html' @@ -507,7 +508,7 @@ def plot_node_balance( figure_like = plotting.with_matplotlib( ds.to_dataframe(), colors=colors, - mode='bar', + style=style, title=title, ) default_filetype = '.png' @@ -679,6 +680,7 @@ def plot_charge_state( show: bool = True, colors: plotting.ColorType = 'viridis', engine: plotting.PlottingEngine = 'plotly', + style: Literal['area', 'stacked_bar', 'line'] = 'stacked_bar', scenario: Optional[Union[str, int]] = None, ) -> plotly.graph_objs.Figure: """ @@ -688,16 +690,12 @@ def plot_charge_state( show: Whether to show the plot or not. colors: The c engine: Plotting engine to use. Only 'plotly' is implemented atm. + style: The plotting mode for the flow_rate scenario: The scenario to plot. Defaults to the first scenario. Has no effect without scenarios present Raises: ValueError: If the Component is not a Storage. """ - if engine != 'plotly': - raise NotImplementedError( - f'Plotting engine "{engine}" not implemented for ComponentResults.plot_charge_state.' - ) - if not self.is_storage: raise ValueError(f'Cant plot charge_state. "{self.label}" is not a storage') @@ -710,22 +708,34 @@ def plot_charge_state( ds = ds.sel(scenario=chosen_scenario).drop_vars('scenario') charge_state = charge_state.sel(scenario=chosen_scenario).drop_vars('scenario') scenario_suffix = f'--{chosen_scenario}' + if engine == 'plotly': + fig = plotting.with_plotly( + ds.to_dataframe(), + colors=colors, + style=style, + title=f'Operation Balance of {self.label}{scenario_suffix}', + ) - fig = plotting.with_plotly( - ds.to_dataframe(), - colors=colors, - mode='area', - title=f'Operation Balance of {self.label}{scenario_suffix}', - ) - - # TODO: Use colors for charge state? + # TODO: Use colors for charge state? - charge_state = charge_state.to_dataframe() - fig.add_trace( - plotly.graph_objs.Scatter( - x=charge_state.index, y=charge_state.values.flatten(), mode='lines', name=self._charge_state + charge_state = charge_state.to_dataframe() + fig.add_trace( + plotly.graph_objs.Scatter( + x=charge_state.index, y=charge_state.values.flatten(), mode='lines', name=self._charge_state + ) ) - ) + elif engine=='matplotlib': + fig, ax = plotting.with_matplotlib( + ds.to_dataframe(), + colors=colors, + style=style, + title=f'Operation Balance of {self.label}{scenario_suffix}', + ) + + charge_state = charge_state.to_dataframe() + ax.plot(charge_state.index, charge_state.values.flatten(), label=self._charge_state) + fig.tight_layout() + fig = fig, ax return plotting.export_figure( fig, From 4cfa27fcf8e0a29e015759c23c8ee98db5a248f4 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 22 Apr 2025 14:18:43 +0200 Subject: [PATCH 034/448] Bugfix plotting --- flixopt/plotting.py | 6 +++--- tests/test_plots.py | 8 ++++---- tests/test_results_plots.py | 7 +------ 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/flixopt/plotting.py b/flixopt/plotting.py index 9ea19a686..d5b4aef0d 100644 --- a/flixopt/plotting.py +++ b/flixopt/plotting.py @@ -265,7 +265,7 @@ def with_plotly( go.Scatter( x=data.index, y=data[column], - style='lines', + mode='lines', name=column, line=dict(shape='hv', color=processed_colors[i]), ) @@ -293,7 +293,7 @@ def with_plotly( go.Scatter( x=data.index, y=data[column], - style='lines', + mode='lines', name=column, line=dict(shape='hv', color=colors_stacked[column]), fill='tonexty', @@ -306,7 +306,7 @@ def with_plotly( go.Scatter( x=data.index, y=data[column], - style='lines', + mode='lines', name=column, line=dict(shape='hv', color=colors_stacked[column], dash='dash'), ) diff --git a/tests/test_plots.py b/tests/test_plots.py index 840b4e7b3..4e00f9a51 100644 --- a/tests/test_plots.py +++ b/tests/test_plots.py @@ -53,15 +53,15 @@ def get_sample_data( def test_bar_plots(self): data = self.get_sample_data(nr_of_columns=10, nr_of_periods=1, time_steps_per_period=24) - plotly.offline.plot(plotting.with_plotly(data, 'bar')) - plotting.with_matplotlib(data, 'bar') + plotly.offline.plot(plotting.with_plotly(data, 'stacked_bar')) + plotting.with_matplotlib(data, 'stacked_bar') plt.show() data = self.get_sample_data( nr_of_columns=10, nr_of_periods=5, time_steps_per_period=24, drop_fraction_of_indices=0.3 ) - plotly.offline.plot(plotting.with_plotly(data, 'bar')) - plotting.with_matplotlib(data, 'bar') + plotly.offline.plot(plotting.with_plotly(data, 'stacked_bar')) + plotting.with_matplotlib(data, 'stacked_bar') plt.show() def test_line_plots(self): diff --git a/tests/test_results_plots.py b/tests/test_results_plots.py index 855944a48..fe8d27c3b 100644 --- a/tests/test_results_plots.py +++ b/tests/test_results_plots.py @@ -58,12 +58,7 @@ def test_results_plots(flow_system, plotting_engine, show, save, color_spec): ) results['Speicher'].plot_node_balance_pie(engine=plotting_engine, save=save, show=show, colors=color_spec) - - if plotting_engine == 'matplotlib': - with pytest.raises(NotImplementedError): - results['Speicher'].plot_charge_state(engine=plotting_engine) - else: - results['Speicher'].plot_charge_state(engine=plotting_engine) + results['Speicher'].plot_charge_state(engine=plotting_engine) plt.close('all') From 16fd74c306bb19d2788ea0b5a4cc2bfe87c66a8e Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 22 Apr 2025 14:45:11 +0200 Subject: [PATCH 035/448] Fix example_calculation_types.py --- examples/03_Calculation_types/example_calculation_types.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/03_Calculation_types/example_calculation_types.py b/examples/03_Calculation_types/example_calculation_types.py index 3a15b4e28..e9f5604f9 100644 --- a/examples/03_Calculation_types/example_calculation_types.py +++ b/examples/03_Calculation_types/example_calculation_types.py @@ -215,13 +215,13 @@ def get_solutions(calcs: List, variable: str) -> xr.Dataset: fx.plotting.with_plotly( pd.DataFrame(get_solutions(calculations, 'costs(operation)|total_per_timestep').to_dataframe().sum()).T, - style='bar', + style='stacked_bar', title='Total Cost Comparison', ylabel='Costs [€]', ).update_layout(barmode='group').write_html('results/Total Costs.html') fx.plotting.with_plotly( - pd.DataFrame([calc.durations for calc in calculations], index=[calc.name for calc in calculations]), 'bar' + pd.DataFrame([calc.durations for calc in calculations], index=[calc.name for calc in calculations]), 'stacked_bar' ).update_layout(title='Duration Comparison', xaxis_title='Calculation type', yaxis_title='Time (s)').write_html( 'results/Speed Comparison.html' ) From 67d1716a6b7280373a1c4171471f042b4f7ca984 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 24 Apr 2025 20:45:03 +0200 Subject: [PATCH 036/448] Scenarios/fixes (#255) * Fix indexing issue with only one scenario * Bugfix Cooling Tower * Add option for balanced Storage Flows (equalize size of charging and discharging) * Add option for balanced Storage Flows * Change error to warning (non-fixed size with piecewise conversion AND fixed_flow_rate with OnOff) * Bugfix in DataConverter * BUGFIX: Typo (total_max/total_min in Effect) * Bugfix in node_balance() (negating did not work when using flow_hours mode --- flixopt/components.py | 28 ++++++++++++++++++++++++++-- flixopt/core.py | 5 ++++- flixopt/elements.py | 7 +++---- flixopt/features.py | 7 +------ flixopt/linear_converters.py | 6 +++--- flixopt/results.py | 12 +++++++----- 6 files changed, 44 insertions(+), 21 deletions(-) diff --git a/flixopt/components.py b/flixopt/components.py index 1b745b54c..234418694 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -86,8 +86,8 @@ def _plausibility_checks(self) -> None: if self.piecewise_conversion: for flow in self.flows.values(): if isinstance(flow.size, InvestParameters) and flow.size.fixed_size is None: - raise PlausibilityError( - f'piecewise_conversion (in {self.label_full}) and variable size ' + logger.warning( + f'Piecewise_conversion (in {self.label_full}) and variable size ' f'(in flow {flow.label_full}) do not make sense together!' ) @@ -138,6 +138,7 @@ def __init__( eta_discharge: TimestepData = 1, relative_loss_per_hour: TimestepData = 0, prevent_simultaneous_charge_and_discharge: bool = True, + balanced: bool = False, meta_data: Optional[Dict] = None, ): """ @@ -163,6 +164,7 @@ def __init__( relative_loss_per_hour: loss per chargeState-Unit per hour. The default is 0. prevent_simultaneous_charge_and_discharge: If True, loading and unloading at the same time is not possible. Increases the number of binary variables, but is recommended for easier evaluation. The default is True. + balanced: Wether to equate the size of the charging and discharging flow. Only if not fixed. meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types. """ # TODO: fixed_relative_chargeState implementieren @@ -188,6 +190,7 @@ def __init__( self.eta_discharge: TimestepData = eta_discharge self.relative_loss_per_hour: TimestepData = relative_loss_per_hour self.prevent_simultaneous_charge_and_discharge = prevent_simultaneous_charge_and_discharge + self.balanced = balanced def create_model(self, model: SystemModel) -> 'StorageModel': self._plausibility_checks() @@ -261,6 +264,18 @@ def _plausibility_checks(self) -> None: f'is below allowed minimum charge_state {minimum_inital_capacity}' ) + if self.balanced: + if not isinstance(self.charging.size, InvestParameters) or not isinstance(self.discharging.size, InvestParameters): + raise PlausibilityError( + f'Balancing charging and discharging Flows in {self.label_full} ' + f'is only possible with Investments.') + if (self.charging.size.minimum_size > self.discharging.size.maximum_size or + self.charging.size.maximum_size < self.discharging.size.minimum_size): + raise PlausibilityError( + f'Balancing charging and discharging Flows in {self.label_full} need compatible minimum and maximum sizes.' + f'Got: {self.charging.size.minimum_size=}, {self.charging.size.maximum_size=} and ' + f'{self.charging.size.minimum_size=}, {self.charging.size.maximum_size=}.') + @register_class_for_io class Transmission(Component): @@ -531,6 +546,15 @@ def do_modeling(self): # Initial charge state self._initial_and_final_charge_state() + if self.element.balanced: + self.add( + self._model.add_constraints( + self.element.charging.model._investment.size * 1 == self.element.discharging.model._investment.size * 1, + name=f'{self.label_full}|balanced_sizes', + ), + 'balanced_sizes' + ) + def _initial_and_final_charge_state(self): if self.element.initial_charge_state is not None: name_short = 'initial_charge_state' diff --git a/flixopt/core.py b/flixopt/core.py index eab45e239..5d24e46e4 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -251,8 +251,11 @@ def _broadcast_time_to_scenarios( if not np.array_equal(data.coords['time'].values, coords['time'].values): raise ConversionError("Source time coordinates don't match target time coordinates") + if len(coords['scenario']) <= 1: + return data.copy(deep=True) + # Broadcast values - values = np.tile(data.values, (len(coords['scenario']), 1)) + values = np.tile(data.values, (len(coords['scenario']), 1)).T # Tile seems to be faster than repeat() return xr.DataArray(values.copy(), coords=coords, dims=dims) @staticmethod diff --git a/flixopt/elements.py b/flixopt/elements.py index 2ff49567e..7dda3e9cf 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -292,10 +292,9 @@ def _plausibility_checks(self) -> None: ) if self.fixed_relative_profile is not None and self.on_off_parameters is not None: - raise ValueError( - f'Flow {self.label} has both a fixed_relative_profile and an on_off_parameters. This is not supported. ' - f'Use relative_minimum and relative_maximum instead, ' - f'if you want to allow flows to be switched on and off.' + logger.warning( + f'Flow {self.label} has both a fixed_relative_profile and an on_off_parameters.' + f'This will allow the flow to be switched on and off, effectively differing from the fixed_flow_rate.' ) if (self.relative_minimum > 0).any() and self.on_off_parameters is None: diff --git a/flixopt/features.py b/flixopt/features.py index 4eeb46337..94231ccec 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -154,11 +154,6 @@ def _create_bounds_for_defining_variable(self): ), f'fix_{variable.name}', ) - if self._on_variable is not None: - raise ValueError( - f'Flow {self.label_full} has a fixed relative flow rate and an on_variable.' - f'This combination is currently not supported.' - ) return # eq: defining_variable(t) <= size * upper_bound(t) @@ -988,7 +983,7 @@ def __init__( # Parameters self._has_time_dim = has_time_dim self._has_scenario_dim = has_scenario_dim - self._total_max = total_max if total_min is not None else np.inf + self._total_max = total_max if total_max is not None else np.inf self._total_min = total_min if total_min is not None else -np.inf self._max_per_hour = max_per_hour if max_per_hour is not None else np.inf self._min_per_hour = min_per_hour if min_per_hour is not None else -np.inf diff --git a/flixopt/linear_converters.py b/flixopt/linear_converters.py index 3fd032632..b096921f0 100644 --- a/flixopt/linear_converters.py +++ b/flixopt/linear_converters.py @@ -165,7 +165,7 @@ def __init__( label, inputs=[P_el, Q_th], outputs=[], - conversion_factors=[{P_el.label: 1, Q_th.label: -specific_electricity_demand}], + conversion_factors=[{P_el.label: -1, Q_th.label: specific_electricity_demand}], on_off_parameters=on_off_parameters, meta_data=meta_data, ) @@ -177,12 +177,12 @@ def __init__( @property def specific_electricity_demand(self): - return -self.conversion_factors[0][self.Q_th.label] + return self.conversion_factors[0][self.Q_th.label] @specific_electricity_demand.setter def specific_electricity_demand(self, value): check_bounds(value, 'specific_electricity_demand', self.label_full, 0, 1) - self.conversion_factors[0][self.Q_th.label] = -value + self.conversion_factors[0][self.Q_th.label] = value @register_class_for_io diff --git a/flixopt/results.py b/flixopt/results.py index 47c47f288..4f2c7d856 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -631,11 +631,7 @@ def node_balance( """ ds = self.solution[self.inputs + self.outputs] - if mode == 'flow_hours': - ds = ds * self._calculation_results.hours_per_timestep - ds = ds.rename_vars({var: var.replace('flow_rate', 'flow_hours') for var in ds.data_vars}) - - return sanitize_dataset( + ds = sanitize_dataset( ds=ds, threshold=threshold, timesteps=self._calculation_results.timesteps_extra if with_last_timestep else None, @@ -651,6 +647,12 @@ def node_balance( drop_suffix='|' if drop_suffix else None, ) + if mode == 'flow_hours': + ds = ds * self._calculation_results.hours_per_timestep + ds = ds.rename_vars({var: var.replace('flow_rate', 'flow_hours') for var in ds.data_vars}) + + return ds + class BusResults(_NodeResults): """Results for a Bus""" From b96802757cd4019ae858c242f2331e925129942f Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 25 Apr 2025 12:32:31 +0200 Subject: [PATCH 037/448] Scenarios/effects (#256) * Add methods to track effect shares of components and Flows * Add option to include flows when retrieving effects * Add properties and methods to store effect results in a dataset * Reorder methods * Rename and improve docs * Bugfix test class name * Fix the Network algorithm to calculate the sum of parallel paths, and be independent on nr of nodes and complexity of the network * Add tests for the newtork chaining and the results of effect shares * Add methods to check for circular references * Add test to check for circular references * Update cycle checker to return the found cycles * Add checks in results to confirm effects are computed correctly * BUGFIX: Remove +1 from prior testing * Add option for grouped bars to plotting.with_plotly() and make lines of stacked bar plots invisible * Reconstruct FlowSystem in CalculationResults on demand. DEPRECATION in CalculationResults * ruff check * Bugfix: save flow_system data, not the flow_system * Update tests --- flixopt/effects.py | 197 +++++++++++++++++++-- flixopt/plotting.py | 30 +++- flixopt/results.py | 219 +++++++++++++++++++++-- tests/test_cycle_detection.py | 226 +++++++++++++++++++++++ tests/test_effect.py | 86 ++++++++- tests/test_effects_shares_summation.py | 236 +++++++++++++++++++++++++ tests/test_scenarios.py | 6 +- 7 files changed, 956 insertions(+), 44 deletions(-) create mode 100644 tests/test_cycle_detection.py create mode 100644 tests/test_effects_shares_summation.py diff --git a/flixopt/effects.py b/flixopt/effects.py index 3d93ee76a..914100362 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -7,10 +7,11 @@ import logging import warnings -from typing import TYPE_CHECKING, Dict, Iterator, List, Literal, Optional, Union +from typing import TYPE_CHECKING, Dict, Iterator, List, Literal, Optional, Set, Tuple, Union import linopy import numpy as np +import xarray as xr from .core import NumericDataTS, ScenarioData, TimeSeries, TimeSeriesCollection, TimestepData, extract_data from .features import ShareAllocationModel @@ -268,26 +269,18 @@ def get_effect_label(eff: Union[Effect, str]) -> str: def _plausibility_checks(self) -> None: # Check circular loops in effects: - # TODO: Improve checks!! Only most basic case covered... + operation, invest = self.calculate_effect_share_factors() - def error_str(effect_label: str, share_ffect_label: str): - return ( - f' {effect_label} -> has share in: {share_ffect_label}\n' - f' {share_ffect_label} -> has share in: {effect_label}' - ) + operation_cycles = detect_cycles(tuples_to_adjacency_list([key for key in operation])) + invest_cycles = detect_cycles(tuples_to_adjacency_list([key for key in invest])) - for effect in self.effects.values(): - # Effekt darf nicht selber als Share in seinen ShareEffekten auftauchen: - # operation: - for target_effect in effect.specific_share_to_other_effects_operation.keys(): - assert effect not in self[target_effect].specific_share_to_other_effects_operation.keys(), ( - f'Error: circular operation-shares \n{error_str(target_effect.label, target_effect.label)}' - ) - # invest: - for target_effect in effect.specific_share_to_other_effects_invest.keys(): - assert effect not in self[target_effect].specific_share_to_other_effects_invest.keys(), ( - f'Error: circular invest-shares \n{error_str(target_effect.label, target_effect.label)}' - ) + if operation_cycles: + cycle_str = "\n".join([" -> ".join(cycle) for cycle in operation_cycles]) + raise ValueError(f'Error: circular operation-shares detected:\n{cycle_str}') + + if invest_cycles: + cycle_str = "\n".join([" -> ".join(cycle) for cycle in invest_cycles]) + raise ValueError(f'Error: circular invest-shares detected:\n{cycle_str}') def __getitem__(self, effect: Union[str, Effect]) -> 'Effect': """ @@ -351,6 +344,30 @@ def objective_effect(self, value: Effect) -> None: raise ValueError(f'An objective-effect already exists! ({self._objective_effect.label=})') self._objective_effect = value + def calculate_effect_share_factors(self) -> Tuple[ + Dict[Tuple[str, str], xr.DataArray], + Dict[Tuple[str, str], xr.DataArray], + ]: + shares_invest = {} + for name, effect in self.effects.items(): + if effect.specific_share_to_other_effects_invest: + shares_invest[name] = { + target: extract_data(data) + for target, data in effect.specific_share_to_other_effects_invest.items() + } + shares_invest = calculate_all_conversion_paths(shares_invest) + + shares_operation = {} + for name, effect in self.effects.items(): + if effect.specific_share_to_other_effects_operation: + shares_operation[name] = { + target: extract_data(data) + for target, data in effect.specific_share_to_other_effects_operation.items() + } + shares_operation = calculate_all_conversion_paths(shares_operation) + + return shares_operation, shares_invest + class EffectCollectionModel(Model): """ @@ -425,3 +442,145 @@ def _add_share_between_effects(self): has_time_dim=False, has_scenario_dim=True, ) + + +def calculate_all_conversion_paths( + conversion_dict: Dict[str, Dict[str, xr.DataArray]], +) -> Dict[Tuple[str, str], xr.DataArray]: + """ + Calculates all possible direct and indirect conversion factors between units/domains. + This function uses Breadth-First Search (BFS) to find all possible conversion paths + between different units or domains in a conversion graph. It computes both direct + conversions (explicitly provided in the input) and indirect conversions (derived + through intermediate units). + Args: + conversion_dict: A nested dictionary where: + - Outer keys represent origin units/domains + - Inner dictionaries map target units/domains to their conversion factors + - Conversion factors can be integers, floats, or numpy arrays + Returns: + A dictionary mapping (origin, target) tuples to their respective conversion factors. + Each key is a tuple of strings representing the origin and target units/domains. + Each value is the conversion factor (int, float, or numpy array) from origin to target. + """ + # Initialize the result dictionary to accumulate all paths + result = {} + + # Add direct connections to the result first + for origin, targets in conversion_dict.items(): + for target, factor in targets.items(): + result[(origin, target)] = factor + + # Track all paths by keeping path history to avoid cycles + # Iterate over each domain in the dictionary + for origin in conversion_dict: + # Keep track of visited paths to avoid repeating calculations + processed_paths = set() + # Use a queue with (current_domain, factor, path_history) + queue = [(origin, 1, [origin])] + + while queue: + current_domain, factor, path = queue.pop(0) + + # Skip if we've processed this exact path before + path_key = tuple(path) + if path_key in processed_paths: + continue + processed_paths.add(path_key) + + # Iterate over the neighbors of the current domain + for target, conversion_factor in conversion_dict.get(current_domain, {}).items(): + # Skip if target would create a cycle + if target in path: + continue + + # Calculate the indirect conversion factor + indirect_factor = factor * conversion_factor + new_path = path + [target] + + # Only consider paths starting at origin and ending at some target + if len(new_path) > 2 and new_path[0] == origin: + # Update the result dictionary - accumulate factors from different paths + if (origin, target) in result: + result[(origin, target)] = result[(origin, target)] + indirect_factor + else: + result[(origin, target)] = indirect_factor + + # Add new path to queue for further exploration + queue.append((target, indirect_factor, new_path)) + + return result + + +def detect_cycles(graph: Dict[str, List[str]]) -> List[List[str]]: + """ + Detects cycles in a directed graph using DFS. + + Args: + graph: Adjacency list representation of the graph + + Returns: + List of cycles found, where each cycle is a list of nodes + """ + # Track nodes in current recursion stack + visiting = set() + # Track nodes that have been fully explored + visited = set() + # Store all found cycles + cycles = [] + + def dfs_find_cycles(node, path=None): + if path is None: + path = [] + + # Current path to this node + current_path = path + [node] + # Add node to current recursion stack + visiting.add(node) + + # Check all neighbors + for neighbor in graph.get(node, []): + # If neighbor is in current path, we found a cycle + if neighbor in visiting: + # Get the cycle by extracting the relevant portion of the path + cycle_start = current_path.index(neighbor) + cycle = current_path[cycle_start:] + [neighbor] + cycles.append(cycle) + # If neighbor hasn't been fully explored, check it + elif neighbor not in visited: + dfs_find_cycles(neighbor, current_path) + + # Remove node from current path and mark as fully explored + visiting.remove(node) + visited.add(node) + + # Check each unvisited node + for node in graph: + if node not in visited: + dfs_find_cycles(node) + + return cycles + + +def tuples_to_adjacency_list(edges: List[Tuple[str, str]]) -> Dict[str, List[str]]: + """ + Converts a list of edge tuples (source, target) to an adjacency list representation. + + Args: + edges: List of (source, target) tuples representing directed edges + + Returns: + Dictionary mapping each source node to a list of its target nodes + """ + graph = {} + + for source, target in edges: + if source not in graph: + graph[source] = [] + graph[source].append(target) + + # Ensure target nodes with no outgoing edges are in the graph + if target not in graph: + graph[target] = [] + + return graph diff --git a/flixopt/plotting.py b/flixopt/plotting.py index d5b4aef0d..6a2170674 100644 --- a/flixopt/plotting.py +++ b/flixopt/plotting.py @@ -209,7 +209,7 @@ def process_colors( def with_plotly( data: pd.DataFrame, - style: Literal['stacked_bar', 'line', 'area'] = 'area', + style: Literal['stacked_bar', 'line', 'area', 'grouped_bar'] = 'stacked_bar', colors: ColorType = 'viridis', title: str = '', ylabel: str = '', @@ -235,7 +235,8 @@ def with_plotly( Returns: A Plotly figure object containing the generated plot. """ - assert style in ['stacked_bar', 'line', 'area'], f"'style' must be one of {['stacked_bar', 'line', 'area']}" + if style not in ['stacked_bar', 'line', 'area', 'grouped_bar']: + raise ValueError(f"'style' must be one of {['stacked_bar', 'line', 'area', 'grouped_bar']}") if data.empty: return go.Figure() @@ -250,14 +251,31 @@ def with_plotly( x=data.index, y=data[column], name=column, - marker=dict(color=processed_colors[i]), + marker=dict(color=processed_colors[i], + line=dict(width=0, color='rgba(0,0,0,0)')), #Transparent line with 0 width ) ) fig.update_layout( - barmode='relative' if style == 'stacked_bar' else None, - bargap=0, # No space between bars - bargroupgap=0, # No space between groups of bars + barmode='relative', + bargap=0.2, # No space between bars + bargroupgap=0, # No space between grouped bars + ) + if style == 'grouped_bar': + for i, column in enumerate(data.columns): + fig.add_trace( + go.Bar( + x=data.index, + y=data[column], + name=column, + marker=dict(color=processed_colors[i]) + ) + ) + + fig.update_layout( + barmode='group', + bargap=0.2, # No space between bars + bargroupgap=0, # space between grouped bars ) elif style == 'line': for i, column in enumerate(data.columns): diff --git a/flixopt/results.py b/flixopt/results.py index 4f2c7d856..0f300bbcb 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -92,7 +92,7 @@ def from_file(cls, folder: Union[str, pathlib.Path], name: str): return cls( solution=fx_io.load_dataset_from_netcdf(paths.solution), - flow_system=fx_io.load_dataset_from_netcdf(paths.flow_system), + flow_system_data=fx_io.load_dataset_from_netcdf(paths.flow_system), name=name, folder=folder, model=model, @@ -118,7 +118,7 @@ def from_calculation(cls, calculation: 'Calculation'): """ return cls( solution=calculation.model.solution, - flow_system=calculation.flow_system.as_dataset(constants_in_dataset=True), + flow_system_data=calculation.flow_system.as_dataset(constants_in_dataset=True), summary=calculation.summary, model=calculation.model, name=calculation.name, @@ -128,7 +128,7 @@ def from_calculation(cls, calculation: 'Calculation'): def __init__( self, solution: xr.Dataset, - flow_system: xr.Dataset, + flow_system_data: xr.Dataset, name: str, summary: Dict, folder: Optional[pathlib.Path] = None, @@ -137,14 +137,16 @@ def __init__( """ Args: solution: The solution of the optimization. - flow_system: The flow_system that was used to create the calculation as a datatset. + flow_system_data: The flow_system that was used to create the calculation as a datatset. name: The name of the calculation. summary: Information about the calculation, folder: The folder where the results are saved. model: The linopy model that was used to solve the calculation. + Deprecated: + flow_system: Use flow_system_data instead. """ self.solution = solution - self.flow_system = flow_system + self.flow_system_data = flow_system_data self.summary = summary self.name = name self.model = model @@ -163,6 +165,10 @@ def __init__( self.hours_per_timestep = TimeSeriesCollection.calculate_hours_per_timestep(self.timesteps_extra) self.scenarios = self.solution.indexes['scenario'] if 'scenario' in self.solution.indexes else None + self._effect_share_factors = None + self._flow_system = None + self._effects_per_component = {'operation': None, 'invest': None, 'total': None} + def __getitem__(self, key: str) -> Union['ComponentResults', 'BusResults', 'EffectResults']: if key in self.components: return self.components[key] @@ -196,6 +202,26 @@ def constraints(self) -> linopy.Constraints: raise ValueError('The linopy model is not available.') return self.model.constraints + @property + def effect_share_factors(self): + if self._effect_share_factors is None: + effect_share_factors = self.flow_system.effects.calculate_effect_share_factors() + self._effect_share_factors = {'operation': effect_share_factors[0], + 'invest': effect_share_factors[1]} + return self._effect_share_factors + + @property + def flow_system(self): + """ The restored flow_system that was used to create the calculation. + Contains all input parameters.""" + if self._flow_system is None: + from . import FlowSystem + current_logger_level = logger.getEffectiveLevel() + logger.setLevel(logging.CRITICAL) + self._flow_system = FlowSystem.from_dataset(self.flow_system_data) + logger.setLevel(current_logger_level) + return self._flow_system + def filter_solution( self, variable_dims: Optional[Literal['scalar', 'time', 'scenario', 'timeonly', 'scenarioonly']] = None, @@ -239,6 +265,178 @@ def filter_solution( startswith=startswith, ) + def get_effects_per_component(self, mode: Literal['operation', 'invest', 'total'] = 'total') -> xr.Dataset: + """Returns a dataset containing effect totals for each components (including their flows). + + Args: + mode: Which effects to contain. (operation, invest, total) + + Returns: + An xarray Dataset with an additional component dimension and effects as variables. + """ + if mode not in ['operation', 'invest', 'total']: + raise ValueError(f'Invalid mode {mode}') + if self._effects_per_component[mode] is None: + self._effects_per_component[mode] = self._create_effects_dataset(mode) + return self._effects_per_component[mode] + + def get_effect_shares( + self, + element: str, + effect: str, + mode: Optional[Literal['operation', 'invest']] = None, + include_flows: bool = False + ) -> xr.Dataset: + """Retrieves individual effect shares for a specific element and effect. + Either for operation, investment, or both modes combined. + Only includes the direct shares. + + Args: + element: The element identifier for which to retrieve effect shares. + effect: The effect identifier for which to retrieve shares. + mode: Optional. The mode to retrieve shares for. Can be 'operation', 'invest', + or None to retrieve both. Defaults to None. + + Returns: + An xarray Dataset containing the requested effect shares. If mode is None, + returns a merged Dataset containing both operation and investment shares. + + Raises: + ValueError: If the specified effect is not available or if mode is invalid. + """ + if effect not in self.effects: + raise ValueError(f'Effect {effect} is not available.') + + if mode is None: + return xr.merge([self.get_effect_shares(element=element, effect=effect, mode='operation', include_flows=include_flows), + self.get_effect_shares(element=element, effect=effect, mode='invest', include_flows=include_flows)]) + + if mode not in ['operation', 'invest']: + raise ValueError(f'Mode {mode} is not available. Choose between "operation" and "invest".') + + ds = xr.Dataset() + + label = f'{element}->{effect}({mode})' + if label in self.solution: + ds = xr.Dataset({label: self.solution[label]}) + + if include_flows: + if element not in self.components: + raise ValueError(f'Only use Components when retrieving Effects including flows. Got {element}') + flows = [label.split('|')[0] for label in self.components[element].inputs + self.components[element].outputs] + return xr.merge( + [ds] + [self.get_effect_shares(element=flow, effect=effect, mode=mode, include_flows=False) + for flow in flows] + ) + + return ds + + def _compute_effect_total( + self, + element: str, + effect: str, + mode: Literal['operation', 'invest', 'total'] = 'total', + include_flows: bool = False + ) -> xr.DataArray: + """Calculates the total effect for a specific element and effect. + + This method computes the total direct and indirect effects for a given element + and effect, considering the conversion factors between different effects. + + Args: + element: The element identifier for which to calculate total effects. + effect: The effect identifier to calculate. + mode: The calculation mode. Options are: + 'operation': Returns operation-specific effects. + 'invest': Returns investment-specific effects. + 'total': Returns the sum of operation effects (across all timesteps) + and investment effects. Defaults to 'total'. + + Returns: + An xarray DataArray containing the total effects, named with pattern + '{element}->{effect}' for mode='total' or '{element}->{effect}({mode})' + for other modes. + + Raises: + ValueError: If the specified effect is not available. + """ + if effect not in self.effects: + raise ValueError(f'Effect {effect} is not available.') + + if mode == 'total': + operation = self._compute_effect_total(element=element, effect=effect, mode='operation', include_flows=include_flows) + if len(operation.indexes) > 0: + operation = operation.sum('time') + return (operation + self._compute_effect_total(element=element, effect=effect, mode='invest', include_flows=include_flows) + ).rename(f'{element}->{effect}') + + total = xr.DataArray(0) + + relevant_conversion_factors = { + key[0]: value for key, value in self.effect_share_factors[mode].items() if key[1] == effect + } + relevant_conversion_factors[effect] = 1 # Share to itself is 1 + + for target_effect, conversion_factor in relevant_conversion_factors.items(): + label = f'{element}->{target_effect}({mode})' + if label in self.solution: + da = self.solution[label] + total = total + da * conversion_factor + + if include_flows: + if element not in self.components: + raise ValueError(f'Only use Components when retrieving Effects including flows. Got {element}') + flows = [label.split('|')[0] for label in + self.components[element].inputs + self.components[element].outputs] + for flow in flows: + label = f'{flow}->{target_effect}({mode})' + if label in self.solution: + da = self.solution[label] + total = total + da * conversion_factor + + return total.rename(f'{element}->{effect}({mode})') + + def _create_effects_dataset(self, mode: Literal['operation', 'invest', 'total'] = 'total') -> xr.Dataset: + """Creates a dataset containing effect totals for all components (including their flows). + The dataset does contain the direct as well as the indirect effects of each component. + + Args: + mode: The calculation mode ('operation', 'invest', or 'total'). + + Returns: + An xarray Dataset with components as a dimension and effects as variables. + """ + data_vars = {} + + for effect in self.effects: + # Create a list of DataArrays, one for each component + effect_arrays = [ + self._compute_effect_total(element=component, effect=effect, mode=mode, include_flows=True) + .expand_dims(component=[component]) # Add component dimension to each array + for component in list(self.components) + ] + + # Combine all components into one DataArray for this effect + if effect_arrays: + data_vars[effect] = xr.concat(effect_arrays, dim="component", coords='minimal') + + ds = xr.Dataset(data_vars) + + # For now include a test to ensure correctness + suffix = {'operation': '(operation)|total_per_timestep', + 'invest': '(invest)|total', + 'total': '|total', + } + for effect in self.effects: + label = f'{effect}{suffix[mode]}' + computed = ds.sum('component')[effect] + found = self.solution[label] + if not np.allclose(computed.values, found.fillna(0).values): + logger.critical(f'Results for {effect}({mode}) in effects_dataset doesnt match {label}\n' + f'{computed=}\n, {found=}') + + return ds + def plot_heatmap( self, variable_name: str, @@ -295,16 +493,9 @@ def plot_network( show: bool = False, ) -> 'pyvis.network.Network': """See flixopt.flow_system.FlowSystem.plot_network""" - try: - from .flow_system import FlowSystem - - flow_system = FlowSystem.from_dataset(self.flow_system) - except Exception as e: - logger.critical(f'Could not reconstruct the flow_system from dataset: {e}') - return None if path is None: path = self.folder / f'{self.name}--network.html' - return flow_system.plot_network(controls=controls, path=path, show=show) + return self.flow_system.plot_network(controls=controls, path=path, show=show) def to_file( self, @@ -337,7 +528,7 @@ def to_file( paths = fx_io.CalculationResultsPaths(folder, name) fx_io.save_dataset_to_netcdf(self.solution, paths.solution, compression=compression) - fx_io.save_dataset_to_netcdf(self.flow_system, paths.flow_system, compression=compression) + fx_io.save_dataset_to_netcdf(self.flow_system_data, paths.flow_system, compression=compression) with open(paths.summary, 'w', encoding='utf-8') as f: yaml.dump(self.summary, f, allow_unicode=True, sort_keys=False, indent=4, width=1000) diff --git a/tests/test_cycle_detection.py b/tests/test_cycle_detection.py new file mode 100644 index 000000000..71c775b99 --- /dev/null +++ b/tests/test_cycle_detection.py @@ -0,0 +1,226 @@ +import pytest + +from flixopt.effects import detect_cycles + + +def test_empty_graph(): + """Test that an empty graph has no cycles.""" + assert detect_cycles({}) == [] + + +def test_single_node(): + """Test that a graph with a single node and no edges has no cycles.""" + assert detect_cycles({"A": []}) == [] + + +def test_self_loop(): + """Test that a graph with a self-loop has a cycle.""" + cycles = detect_cycles({"A": ["A"]}) + assert len(cycles) == 1 + assert cycles[0] == ["A", "A"] + + +def test_simple_cycle(): + """Test that a simple cycle is detected.""" + graph = { + "A": ["B"], + "B": ["C"], + "C": ["A"] + } + cycles = detect_cycles(graph) + assert len(cycles) == 1 + assert cycles[0] == ["A", "B", "C", "A"] or cycles[0] == ["B", "C", "A", "B"] or cycles[0] == ["C", "A", "B", "C"] + + +def test_no_cycles(): + """Test that a directed acyclic graph has no cycles.""" + graph = { + "A": ["B", "C"], + "B": ["D", "E"], + "C": ["F"], + "D": [], + "E": [], + "F": [] + } + assert detect_cycles(graph) == [] + + +def test_multiple_cycles(): + """Test that a graph with multiple cycles is detected.""" + graph = { + "A": ["B", "D"], + "B": ["C"], + "C": ["A"], + "D": ["E"], + "E": ["D"] + } + cycles = detect_cycles(graph) + assert len(cycles) == 2 + + # Check that both cycles are detected (order might vary) + cycle_strings = [",".join(cycle) for cycle in cycles] + assert any("A,B,C,A" in s for s in cycle_strings) or any("B,C,A,B" in s for s in cycle_strings) or any( + "C,A,B,C" in s for s in cycle_strings) + assert any("D,E,D" in s for s in cycle_strings) or any("E,D,E" in s for s in cycle_strings) + + +def test_hidden_cycle(): + """Test that a cycle hidden deep in the graph is detected.""" + graph = { + "A": ["B", "C"], + "B": ["D"], + "C": ["E"], + "D": ["F"], + "E": ["G"], + "F": ["H"], + "G": ["I"], + "H": ["J"], + "I": ["K"], + "J": ["L"], + "K": ["M"], + "L": ["N"], + "M": ["N"], + "N": ["O"], + "O": ["P"], + "P": ["Q"], + "Q": ["O"] # Hidden cycle O->P->Q->O + } + cycles = detect_cycles(graph) + assert len(cycles) == 1 + + # Check that the O-P-Q cycle is detected + cycle = cycles[0] + assert "O" in cycle and "P" in cycle and "Q" in cycle + + # Check that they appear in the correct order + o_index = cycle.index("O") + p_index = cycle.index("P") + q_index = cycle.index("Q") + + # Check the cycle order is correct (allowing for different starting points) + cycle_len = len(cycle) + assert (p_index == (o_index + 1) % cycle_len and q_index == (p_index + 1) % cycle_len) or \ + (q_index == (o_index + 1) % cycle_len and p_index == (q_index + 1) % cycle_len) or \ + (o_index == (p_index + 1) % cycle_len and q_index == (o_index + 1) % cycle_len) + + +def test_disconnected_graph(): + """Test with a disconnected graph.""" + graph = { + "A": ["B"], + "B": ["C"], + "C": [], + "D": ["E"], + "E": ["F"], + "F": [] + } + assert detect_cycles(graph) == [] + + +def test_disconnected_graph_with_cycle(): + """Test with a disconnected graph containing a cycle in one component.""" + graph = { + "A": ["B"], + "B": ["C"], + "C": [], + "D": ["E"], + "E": ["F"], + "F": ["D"] # Cycle in D->E->F->D + } + cycles = detect_cycles(graph) + assert len(cycles) == 1 + + # Check that the D-E-F cycle is detected + cycle = cycles[0] + assert "D" in cycle and "E" in cycle and "F" in cycle + + # Check if they appear in the correct order + d_index = cycle.index("D") + e_index = cycle.index("E") + f_index = cycle.index("F") + + # Check the cycle order is correct (allowing for different starting points) + cycle_len = len(cycle) + assert (e_index == (d_index + 1) % cycle_len and f_index == (e_index + 1) % cycle_len) or \ + (f_index == (d_index + 1) % cycle_len and e_index == (f_index + 1) % cycle_len) or \ + (d_index == (e_index + 1) % cycle_len and f_index == (d_index + 1) % cycle_len) + + +def test_complex_dag(): + """Test with a complex directed acyclic graph.""" + graph = { + "A": ["B", "C", "D"], + "B": ["E", "F"], + "C": ["E", "G"], + "D": ["G", "H"], + "E": ["I", "J"], + "F": ["J", "K"], + "G": ["K", "L"], + "H": ["L", "M"], + "I": ["N"], + "J": ["N", "O"], + "K": ["O", "P"], + "L": ["P", "Q"], + "M": ["Q"], + "N": ["R"], + "O": ["R", "S"], + "P": ["S"], + "Q": ["S"], + "R": [], + "S": [] + } + assert detect_cycles(graph) == [] + + +def test_missing_node_in_connections(): + """Test behavior when a node referenced in edges doesn't have its own key.""" + graph = { + "A": ["B", "C"], + "B": ["D"] + # C and D don't have their own entries + } + assert detect_cycles(graph) == [] + + +def test_non_string_keys(): + """Test with non-string keys to ensure the algorithm is generic.""" + graph = { + 1: [2, 3], + 2: [4], + 3: [4], + 4: [] + } + assert detect_cycles(graph) == [] + + graph_with_cycle = { + 1: [2], + 2: [3], + 3: [1] + } + cycles = detect_cycles(graph_with_cycle) + assert len(cycles) == 1 + assert cycles[0] == [1, 2, 3, 1] or cycles[0] == [2, 3, 1, 2] or cycles[0] == [3, 1, 2, 3] + + +def test_complex_network_with_many_nodes(): + """Test with a large network to check performance and correctness.""" + graph = {} + # Create a large DAG + for i in range(100): + # Connect each node to the next few nodes + graph[i] = [j for j in range(i + 1, min(i + 5, 100))] + + # No cycles in this arrangement + assert detect_cycles(graph) == [] + + # Add a single back edge to create a cycle + graph[99] = [0] # This creates a cycle + cycles = detect_cycles(graph) + assert len(cycles) >= 1 + # The cycle might include many nodes, but must contain both 0 and 99 + any_cycle_has_both = any(0 in cycle and 99 in cycle for cycle in cycles) + assert any_cycle_has_both + + +if __name__ == "__main__": + pytest.main(["-v"]) diff --git a/tests/test_effect.py b/tests/test_effect.py index 5cbc04ac6..93f417f22 100644 --- a/tests/test_effect.py +++ b/tests/test_effect.py @@ -5,10 +5,10 @@ import flixopt as fx -from .conftest import assert_conequal, assert_var_equal, create_linopy_model +from .conftest import assert_conequal, assert_var_equal, create_calculation_and_solve, create_linopy_model -class TestBusModel: +class TestEffectModel: """Test the FlowModel class.""" def test_minimal(self, basic_flow_system_linopy): @@ -140,3 +140,85 @@ def test_shares(self, basic_flow_system_linopy): ) +class TestEffectResults: + def test_shares(self, basic_flow_system_linopy): + flow_system = basic_flow_system_linopy + flow_system.effects['Costs'].specific_share_to_other_effects_operation['Effect1'] = 0.5 + flow_system.add_elements( + fx.Effect('Effect1', '€', 'Testing Effect', + specific_share_to_other_effects_operation={ + 'Effect2': 1.1, + 'Effect3': 1.2 + }, + specific_share_to_other_effects_invest={ + 'Effect2': 2.1, + 'Effect3': 2.2 + } + ), + fx.Effect('Effect2', '€', 'Testing Effect', specific_share_to_other_effects_operation={'Effect3': 5}), + fx.Effect('Effect3', '€', 'Testing Effect'), + fx.linear_converters.Boiler( + 'Boiler', eta=0.5, Q_th=fx.Flow('Q_th', bus='Fernwärme', ),Q_fu=fx.Flow('Q_fu', bus='Gas'), + ) + ) + + results = create_calculation_and_solve(flow_system, fx.solvers.HighsSolver(0.01, 60), 'Sim1').results + + effect_share_factors = { + 'operation': { + ('Costs', 'Effect1'): 0.5, + ('Costs', 'Effect2'): 0.5 * 1.1, + ('Costs', 'Effect3'): 0.5 * 1.1 * 5 + 0.5 * 1.2, #This is where the issue lies + ('Effect1', 'Effect2'): 1.1, + ('Effect1', 'Effect3'): 1.2 + 1.1 * 5, + ('Effect2', 'Effect3'): 5, + }, + 'invest': { + ('Effect1', 'Effect2'): 2.1, + ('Effect1', 'Effect3'): 2.2, + } + } + for key, value in effect_share_factors['operation'].items(): + np.testing.assert_allclose(results.effect_share_factors['operation'][key].values, value) + + for key, value in effect_share_factors['invest'].items(): + np.testing.assert_allclose(results.effect_share_factors['invest'][key].values, value) + + xr.testing.assert_allclose(results.get_effects_per_component('operation').sum('component')['Costs'], + results.solution['Costs(operation)|total_per_timestep'].fillna(0)) + + xr.testing.assert_allclose(results.get_effects_per_component('operation').sum('component')['Effect1'], + results.solution['Effect1(operation)|total_per_timestep'].fillna(0)) + + xr.testing.assert_allclose(results.get_effects_per_component('operation').sum('component')['Effect2'], + results.solution['Effect2(operation)|total_per_timestep'].fillna(0)) + + xr.testing.assert_allclose(results.get_effects_per_component('operation').sum('component')['Effect3'], + results.solution['Effect3(operation)|total_per_timestep'].fillna(0)) + + # Invest mode checks + xr.testing.assert_allclose(results.get_effects_per_component('invest').sum('component')['Costs'], + results.solution['Costs(invest)|total']) + + xr.testing.assert_allclose(results.get_effects_per_component('invest').sum('component')['Effect1'], + results.solution['Effect1(invest)|total']) + + xr.testing.assert_allclose(results.get_effects_per_component('invest').sum('component')['Effect2'], + results.solution['Effect2(invest)|total']) + + xr.testing.assert_allclose(results.get_effects_per_component('invest').sum('component')['Effect3'], + results.solution['Effect3(invest)|total']) + + # Total mode checks + xr.testing.assert_allclose(results.get_effects_per_component('total').sum('component')['Costs'], + results.solution['Costs|total']) + + xr.testing.assert_allclose(results.get_effects_per_component('total').sum('component')['Effect1'], + results.solution['Effect1|total']) + + xr.testing.assert_allclose(results.get_effects_per_component('total').sum('component')['Effect2'], + results.solution['Effect2|total']) + + xr.testing.assert_allclose(results.get_effects_per_component('total').sum('component')['Effect3'], + results.solution['Effect3|total']) + diff --git a/tests/test_effects_shares_summation.py b/tests/test_effects_shares_summation.py new file mode 100644 index 000000000..e2dada7e9 --- /dev/null +++ b/tests/test_effects_shares_summation.py @@ -0,0 +1,236 @@ +from typing import Dict, Tuple + +import numpy as np +import pytest +import xarray as xr + +from flixopt.effects import calculate_all_conversion_paths + + +def test_direct_conversions(): + """Test direct conversions with simple scalar values.""" + conversion_dict = { + 'A': {'B': xr.DataArray(2.0)}, + 'B': {'C': xr.DataArray(3.0)} + } + + result = calculate_all_conversion_paths(conversion_dict) + + # Check direct conversions + assert ('A', 'B') in result + assert ('B', 'C') in result + assert result[('A', 'B')].item() == 2.0 + assert result[('B', 'C')].item() == 3.0 + + # Check indirect conversion + assert ('A', 'C') in result + assert result[('A', 'C')].item() == 6.0 # 2.0 * 3.0 + + +def test_multiple_paths(): + """Test multiple paths between nodes that should be summed.""" + conversion_dict = { + 'A': {'B': xr.DataArray(2.0), 'C': xr.DataArray(3.0)}, + 'B': {'D': xr.DataArray(4.0)}, + 'C': {'D': xr.DataArray(5.0)} + } + + result = calculate_all_conversion_paths(conversion_dict) + + # A to D should sum two paths: A->B->D (2*4=8) and A->C->D (3*5=15) + assert ('A', 'D') in result + assert result[('A', 'D')].item() == 8.0 + 15.0 + + +def test_xarray_conversions(): + """Test with xarray DataArrays that have dimensions.""" + # Create DataArrays with a time dimension + time_points = [1, 2, 3] + a_to_b = xr.DataArray([2.0, 2.1, 2.2], dims=['time'], coords={'time': time_points}) + b_to_c = xr.DataArray([3.0, 3.1, 3.2], dims=['time'], coords={'time': time_points}) + + conversion_dict = { + 'A': {'B': a_to_b}, + 'B': {'C': b_to_c} + } + + result = calculate_all_conversion_paths(conversion_dict) + + # Check indirect conversion preserves dimensions + assert ('A', 'C') in result + assert result[('A', 'C')].dims == ('time',) + + # Check values at each time point + for i, t in enumerate(time_points): + expected = a_to_b.values[i] * b_to_c.values[i] + assert pytest.approx(result[('A', 'C')].sel(time=t).item()) == expected + + +def test_long_paths(): + """Test with longer paths (more than one intermediate node).""" + conversion_dict = { + 'A': {'B': xr.DataArray(2.0)}, + 'B': {'C': xr.DataArray(3.0)}, + 'C': {'D': xr.DataArray(4.0)}, + 'D': {'E': xr.DataArray(5.0)} + } + + result = calculate_all_conversion_paths(conversion_dict) + + # Check the full path A->B->C->D->E + assert ('A', 'E') in result + expected = 2.0 * 3.0 * 4.0 * 5.0 # 120.0 + assert result[('A', 'E')].item() == expected + + +def test_diamond_paths(): + """Test with a diamond shape graph with multiple paths to the same destination.""" + conversion_dict = { + 'A': {'B': xr.DataArray(2.0), 'C': xr.DataArray(3.0)}, + 'B': {'D': xr.DataArray(4.0)}, + 'C': {'D': xr.DataArray(5.0)}, + 'D': {'E': xr.DataArray(6.0)} + } + + result = calculate_all_conversion_paths(conversion_dict) + + # A to E should go through both paths: + # A->B->D->E (2*4*6=48) and A->C->D->E (3*5*6=90) + assert ('A', 'E') in result + expected = 48.0 + 90.0 # 138.0 + assert result[('A', 'E')].item() == expected + + +def test_effect_shares_example(): + """Test the specific example from the effects share factors test.""" + # Create the conversion dictionary based on test example + conversion_dict = { + 'Costs': {'Effect1': xr.DataArray(0.5)}, + 'Effect1': {'Effect2': xr.DataArray(1.1), 'Effect3': xr.DataArray(1.2)}, + 'Effect2': {'Effect3': xr.DataArray(5.0)} + } + + result = calculate_all_conversion_paths(conversion_dict) + + # Test direct paths + assert result[('Costs', 'Effect1')].item() == 0.5 + assert result[('Effect1', 'Effect2')].item() == 1.1 + assert result[('Effect2', 'Effect3')].item() == 5.0 + + # Test indirect paths + # Costs -> Effect2 = Costs -> Effect1 -> Effect2 = 0.5 * 1.1 + assert result[('Costs', 'Effect2')].item() == 0.5 * 1.1 + + # Costs -> Effect3 has two paths: + # 1. Costs -> Effect1 -> Effect3 = 0.5 * 1.2 = 0.6 + # 2. Costs -> Effect1 -> Effect2 -> Effect3 = 0.5 * 1.1 * 5 = 2.75 + # Total = 0.6 + 2.75 = 3.35 + assert result[('Costs', 'Effect3')].item() == 0.5 * 1.2 + 0.5 * 1.1 * 5 + + # Effect1 -> Effect3 has two paths: + # 1. Effect1 -> Effect2 -> Effect3 = 1.1 * 5.0 = 5.5 + # 2. Effect1 -> Effect3 = 1.2 + # Total = 0.6 + 2.75 = 3.35 + assert result[('Effect1', 'Effect3')].item() == 1.2 + 1.1 * 5.0 + + +def test_empty_conversion_dict(): + """Test with an empty conversion dictionary.""" + result = calculate_all_conversion_paths({}) + assert len(result) == 0 + + +def test_no_indirect_paths(): + """Test with a dictionary that has no indirect paths.""" + conversion_dict = { + 'A': {'B': xr.DataArray(2.0)}, + 'C': {'D': xr.DataArray(3.0)} + } + + result = calculate_all_conversion_paths(conversion_dict) + + # Only direct paths should exist + assert len(result) == 2 + assert ('A', 'B') in result + assert ('C', 'D') in result + assert result[('A', 'B')].item() == 2.0 + assert result[('C', 'D')].item() == 3.0 + + +def test_complex_network(): + """Test with a complex network of many nodes and multiple paths, without circular references.""" + # Create a directed acyclic graph with many nodes + # Structure resembles a layered network with multiple paths + conversion_dict = { + 'A': {'B': xr.DataArray(1.5), 'C': xr.DataArray(2.0), 'D': xr.DataArray(0.5)}, + 'B': {'E': xr.DataArray(3.0), 'F': xr.DataArray(1.2)}, + 'C': {'E': xr.DataArray(0.8), 'G': xr.DataArray(2.5)}, + 'D': {'G': xr.DataArray(1.8), 'H': xr.DataArray(3.2)}, + 'E': {'I': xr.DataArray(0.7), 'J': xr.DataArray(1.4)}, + 'F': {'J': xr.DataArray(2.2), 'K': xr.DataArray(0.9)}, + 'G': {'K': xr.DataArray(1.6), 'L': xr.DataArray(2.8)}, + 'H': {'L': xr.DataArray(0.4), 'M': xr.DataArray(1.1)}, + 'I': {'N': xr.DataArray(2.3)}, + 'J': {'N': xr.DataArray(1.9), 'O': xr.DataArray(0.6)}, + 'K': {'O': xr.DataArray(3.5), 'P': xr.DataArray(1.3)}, + 'L': {'P': xr.DataArray(2.7), 'Q': xr.DataArray(0.8)}, + 'M': {'Q': xr.DataArray(2.1)}, + 'N': {'R': xr.DataArray(1.7)}, + 'O': {'R': xr.DataArray(2.9), 'S': xr.DataArray(1.0)}, + 'P': {'S': xr.DataArray(2.4)}, + 'Q': {'S': xr.DataArray(1.5)} + } + + result = calculate_all_conversion_paths(conversion_dict) + + # Check some direct paths + assert result[('A', 'B')].item() == 1.5 + assert result[('D', 'H')].item() == 3.2 + assert result[('G', 'L')].item() == 2.8 + + # Check some two-step paths + assert result[('A', 'E')].item() == 1.5 * 3.0 + 2.0 * 0.8 # A->B->E + A->C->E + assert result[('B', 'J')].item() == 3.0 * 1.4 + 1.2 * 2.2 # B->E->J + B->F->J + + # Check some three-step paths + # A->B->E->I + # A->C->E->I + expected_a_to_i = 1.5 * 3.0 * 0.7 + 2.0 * 0.8 * 0.7 + assert pytest.approx(result[('A', 'I')].item()) == expected_a_to_i + + # Check some four-step paths + # A->B->E->I->N + # A->C->E->I->N + expected_a_to_n = 1.5 * 3.0 * 0.7 * 2.3 + 2.0 * 0.8 * 0.7 * 2.3 + expected_a_to_n += 1.5 * 3.0 * 1.4 * 1.9 + 2.0 * 0.8 * 1.4 * 1.9 # A->B->E->J->N + A->C->E->J->N + expected_a_to_n += 1.5 * 1.2 * 2.2 * 1.9 # A->B->F->J->N + assert pytest.approx(result[('A', 'N')].item()) == expected_a_to_n + + # Check a very long path from A to S + # This should include: + # A->B->E->J->O->S + # A->B->F->K->O->S + # A->C->E->J->O->S + # A->C->G->K->O->S + # A->D->G->K->O->S + # A->D->H->L->P->S + # A->D->H->M->Q->S + # And many more + assert ('A', 'S') in result + + # There are many paths to R from A - check their existence + assert ('A', 'R') in result + + # Check that there's no direct path from A to R + # But there should be indirect paths + assert ('A', 'R') in result + assert 'A' not in conversion_dict.get('R', {}) + + # Count the number of paths calculated to verify algorithm explored all connections + # In a DAG with 19 nodes (A through S), the maximum number of pairs is 19*18 = 342 + # But we won't have all possible connections due to the structure + # Just verify we have a reasonable number + assert len(result) > 50 + +if __name__ == '__main__': + pytest.main() diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py index 89b5feced..4abdafa5f 100644 --- a/tests/test_scenarios.py +++ b/tests/test_scenarios.py @@ -282,7 +282,7 @@ def test_full_scenario_optimization(flow_system_piecewise_conversion_scenarios): calc.results.to_file() res = fx.results.CalculationResults.from_file('results', 'test_full_scenario') - fx.FlowSystem.from_dataset(res.flow_system) + fx.FlowSystem.from_dataset(res.flow_system_data) calc = create_calculation_and_solve( flow_system_piecewise_conversion_scenarios, solver=fx.solvers.GurobiSolver(mip_gap=0.01, time_limit_seconds=60), @@ -301,7 +301,7 @@ def test_io_persistance(flow_system_piecewise_conversion_scenarios): calc.results.to_file() res = fx.results.CalculationResults.from_file('results', 'test_full_scenario') - flow_system_2 = fx.FlowSystem.from_dataset(res.flow_system) + flow_system_2 = fx.FlowSystem.from_dataset(res.flow_system_data) calc_2 = create_calculation_and_solve( flow_system_2, solver=fx.solvers.HighsSolver(mip_gap=0.001, time_limit_seconds=60), @@ -323,7 +323,7 @@ def test_scenarios_selection(flow_system_piecewise_conversion_scenarios): calc.solve(fx.solvers.GurobiSolver(mip_gap=0.01, time_limit_seconds=60)) calc.results.to_file() - flow_system_2 = fx.FlowSystem.from_dataset(calc.results.flow_system) + flow_system_2 = fx.FlowSystem.from_dataset(calc.results.flow_system_data) assert calc.results.solution.indexes['scenario'].equals(flow_system.time_series_collection.scenarios[0:2]) From 9f2f38b88a95bb0ffbd572c8a10eda0fba180c83 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 28 Apr 2025 20:26:34 +0200 Subject: [PATCH 038/448] Scenarios/datasets results (#257) * Use dataarray instead of dataset * Change effects dataset to dataarray and use nan when no share was found * Add method for flow_rates dataset * Add methods to get flow_rates and flow_hours as datasets * Rename the dataarrays to the flow * Preserve index order * Improve filter_edges_dataset() * Simplify _create_flow_rates_dataarray() * Add dataset for sizes of Flows * Extend results structure to contain flows AND start/end infos * Add FlowResults Object * BUGFIX:Typo in _ElementResults.constraints * Add flows to results of Nodes * Simplify dataarray creation and improve FlowResults * Add nice docstrings * Improve filtering of flow results * Improve filtering of flow results. Add attribute of component * Add big dataarray with all variables but indexed * Revert "Add big dataarray with all variables but indexed" This reverts commit 08cd8a14fcf28248bf4a4c0a0fe1bae718269731. * Improve filtering method for coords filter and add error handling for restoring the flow system * Remove unnecessary methods in results .from_json() * Ensure consistent coord ordering in Effects dataarray * Rename get_effects_per_component() * Make effects_per_component() a dataset instead of a dataarray * Improve backwards compatability --- flixopt/elements.py | 12 +- flixopt/results.py | 356 ++++++++++++++++++++++++++++++++++++------- flixopt/structure.py | 7 +- tests/test_effect.py | 24 +-- 4 files changed, 332 insertions(+), 67 deletions(-) diff --git a/flixopt/elements.py b/flixopt/elements.py index 7dda3e9cf..11246e6d9 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -396,6 +396,14 @@ def do_modeling(self): # Shares self._create_shares() + def results_structure(self): + return { + **super().results_structure(), + 'start': self.element.bus if self.element.is_input_in_component else self.element.component, + 'end': self.element.component if self.element.is_input_in_component else self.element.bus, + 'component': self.element.component, + } + def _create_shares(self): # Arbeitskosten: if self.element.effects_per_flow_hour != {}: @@ -535,7 +543,8 @@ def results_structure(self): inputs.append(self.excess_input.name) if self.excess_output is not None: outputs.append(self.excess_output.name) - return {**super().results_structure(), 'inputs': inputs, 'outputs': outputs} + return {**super().results_structure(), 'inputs': inputs, 'outputs': outputs, + 'flows': [flow.label_full for flow in self.element.inputs + self.element.outputs]} class ComponentModel(ElementModel): @@ -588,4 +597,5 @@ def results_structure(self): **super().results_structure(), 'inputs': [flow.model.flow_rate.name for flow in self.element.inputs], 'outputs': [flow.model.flow_rate.name for flow in self.element.outputs], + 'flows': [flow.label_full for flow in self.element.inputs + self.element.outputs], } diff --git a/flixopt/results.py b/flixopt/results.py index 0f300bbcb..b1e72f4be 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -2,7 +2,8 @@ import json import logging import pathlib -from typing import TYPE_CHECKING, Dict, List, Literal, Optional, Tuple, Union +import warnings +from typing import TYPE_CHECKING, Dict, List, Literal, Optional, Tuple, Union, Any import linopy import matplotlib.pyplot as plt @@ -25,6 +26,11 @@ logger = logging.getLogger('flixopt') +class _FlowSystemRestorationError(Exception): + """Exception raised when a FlowSystem cannot be restored from dataset.""" + pass + + class CalculationResults: """Results container for Calculation results. @@ -37,7 +43,7 @@ class CalculationResults: Attributes: solution (xr.Dataset): Dataset containing optimization results. - flow_system (xr.Dataset): Dataset containing the flow system. + flow_system_data (xr.Dataset): Dataset containing the flow system. summary (Dict): Information about the calculation. name (str): Name identifier for the calculation. model (linopy.Model): The optimization model (if available). @@ -133,6 +139,7 @@ def __init__( summary: Dict, folder: Optional[pathlib.Path] = None, model: Optional[linopy.Model] = None, + **kwargs, # To accept old "flow_system" parameter ): """ Args: @@ -145,6 +152,16 @@ def __init__( Deprecated: flow_system: Use flow_system_data instead. """ + # Handle potential old "flow_system" parameter for backward compatibility + if 'flow_system' in kwargs and flow_system_data is None: + flow_system_data = kwargs.pop('flow_system') + warnings.warn( + "The 'flow_system' parameter is deprecated. Use 'flow_system_data' instead." + "Acess is now by '.flow_system_data', while '.flow_system' returns the restored FlowSystem.", + DeprecationWarning, + stacklevel=2, + ) + self.solution = solution self.flow_system_data = flow_system_data self.summary = summary @@ -152,30 +169,47 @@ def __init__( self.model = model self.folder = pathlib.Path(folder) if folder is not None else pathlib.Path.cwd() / 'results' self.components = { - label: ComponentResults.from_json(self, infos) for label, infos in self.solution.attrs['Components'].items() + label: ComponentResults(self, **infos) for label, infos in self.solution.attrs['Components'].items() } - self.buses = {label: BusResults.from_json(self, infos) for label, infos in self.solution.attrs['Buses'].items()} + self.buses = {label: BusResults(self, **infos) for label, infos in self.solution.attrs['Buses'].items()} self.effects = { - label: EffectResults.from_json(self, infos) for label, infos in self.solution.attrs['Effects'].items() + label: EffectResults(self, **infos) for label, infos in self.solution.attrs['Effects'].items() } + if 'Flows' not in self.solution.attrs: + warnings.warn( + 'No Data about flows found in the results. This data is only included since v2.2.0. Some functionality ' + 'is not availlable. We recommend to evaluate your results with a version <2.2.0.') + self.flows = {} + else: + self.flows = { + label: FlowResults(self, **infos) for label, infos in self.solution.attrs.get('Flows', {}).items() + } + self.timesteps_extra = self.solution.indexes['time'] self.hours_per_timestep = TimeSeriesCollection.calculate_hours_per_timestep(self.timesteps_extra) self.scenarios = self.solution.indexes['scenario'] if 'scenario' in self.solution.indexes else None self._effect_share_factors = None self._flow_system = None + + self._flow_rates = None + self._flow_hours = None + self._sizes = None self._effects_per_component = {'operation': None, 'invest': None, 'total': None} + self._flow_network_info_ = None - def __getitem__(self, key: str) -> Union['ComponentResults', 'BusResults', 'EffectResults']: + def __getitem__(self, key: str) -> Union['ComponentResults', 'BusResults', 'EffectResults', 'FlowResults']: if key in self.components: return self.components[key] if key in self.buses: return self.buses[key] if key in self.effects: return self.effects[key] + if key in self.flows: + return self.flows[key] raise KeyError(f'No element with label {key} found.') @property @@ -211,15 +245,20 @@ def effect_share_factors(self): return self._effect_share_factors @property - def flow_system(self): + def flow_system(self) -> 'FlowSystem': """ The restored flow_system that was used to create the calculation. Contains all input parameters.""" if self._flow_system is None: - from . import FlowSystem - current_logger_level = logger.getEffectiveLevel() - logger.setLevel(logging.CRITICAL) - self._flow_system = FlowSystem.from_dataset(self.flow_system_data) - logger.setLevel(current_logger_level) + try: + from . import FlowSystem + current_logger_level = logger.getEffectiveLevel() + logger.setLevel(logging.CRITICAL) + self._flow_system = FlowSystem.from_dataset(self.flow_system_data) + self._flow_system._connect_network() + logger.setLevel(current_logger_level) + except Exception as e: + logger.critical(f'Not able to restore FlowSystem from dataset. Some functionality is not availlable. {e}') + raise _FlowSystemRestorationError(f'Not able to restore FlowSystem from dataset. {e}') from e return self._flow_system def filter_solution( @@ -265,7 +304,7 @@ def filter_solution( startswith=startswith, ) - def get_effects_per_component(self, mode: Literal['operation', 'invest', 'total'] = 'total') -> xr.Dataset: + def effects_per_component(self, mode: Literal['operation', 'invest', 'total'] = 'total') -> xr.Dataset: """Returns a dataset containing effect totals for each components (including their flows). Args: @@ -280,6 +319,120 @@ def get_effects_per_component(self, mode: Literal['operation', 'invest', 'total' self._effects_per_component[mode] = self._create_effects_dataset(mode) return self._effects_per_component[mode] + def flow_rates( + self, + start: Optional[Union[str, List[str]]] = None, + end: Optional[Union[str, List[str]]] = None, + component: Optional[Union[str, List[str]]] = None, + ) -> xr.DataArray: + """Returns a DataArray containing the flow rates of each Flow. + + Args: + start: Optional source node(s) to filter by. Can be a single node name or a list of names. + end: Optional destination node(s) to filter by. Can be a single node name or a list of names. + component: Optional component(s) to filter by. Can be a single component name or a list of names. + + Further usage: + Convert the dataarray to a dataframe: + >>>results.flow_rates().to_pandas() + Get the max or min over time: + >>>results.flow_rates().max('time') + Sum up the flow rates of flows with the same start and end: + >>>results.flow_rates(end='Fernwärme').groupby('start').sum(dim='flow') + To recombine filtered dataarrays, use `xr.concat` with dim 'flow': + >>>xr.concat([results.flow_rates(start='Fernwärme'), results.flow_rates(end='Fernwärme')], dim='flow') + """ + if self._flow_rates is None: + self._flow_rates = self._assign_flow_coords( + xr.concat([flow.flow_rate.rename(flow.label) for flow in self.flows.values()], + dim=pd.Index(self.flows.keys(), name='flow')) + ).rename('flow_rates') + filters = {k: v for k, v in {'start': start, 'end': end, 'component': component}.items() if v is not None} + return filter_dataarray_by_coord(self._flow_rates, **filters) + + def flow_hours( + self, + start: Optional[Union[str, List[str]]] = None, + end: Optional[Union[str, List[str]]] = None, + component: Optional[Union[str, List[str]]] = None, + ) -> xr.DataArray: + """Returns a DataArray containing the flow hours of each Flow. + + Flow hours represent the total energy/material transferred over time, + calculated by multiplying flow rates by the duration of each timestep. + + Args: + start: Optional source node(s) to filter by. Can be a single node name or a list of names. + end: Optional destination node(s) to filter by. Can be a single node name or a list of names. + component: Optional component(s) to filter by. Can be a single component name or a list of names. + + Further usage: + Convert the dataarray to a dataframe: + >>>results.flow_hours().to_pandas() + Sum up the flow hours over time: + >>>results.flow_hours().sum('time') + Sum up the flow hours of flows with the same start and end: + >>>results.flow_hours(end='Fernwärme').groupby('start').sum(dim='flow') + To recombine filtered dataarrays, use `xr.concat` with dim 'flow': + >>>xr.concat([results.flow_hours(start='Fernwärme'), results.flow_hours(end='Fernwärme')], dim='flow') + + """ + if self._flow_hours is None: + self._flow_hours = (self.flow_rates() * self.hours_per_timestep).rename('flow_hours') + filters = {k: v for k, v in {'start': start, 'end': end, 'component': component}.items() if v is not None} + return filter_dataarray_by_coord(self._flow_hours, **filters) + + def sizes( + self, + start: Optional[Union[str, List[str]]] = None, + end: Optional[Union[str, List[str]]] = None, + component: Optional[Union[str, List[str]]] = None + ) -> xr.DataArray: + """Returns a dataset with the sizes of the Flows. + Args: + start: Optional source node(s) to filter by. Can be a single node name or a list of names. + end: Optional destination node(s) to filter by. Can be a single node name or a list of names. + component: Optional component(s) to filter by. Can be a single component name or a list of names. + + Further usage: + Convert the dataarray to a dataframe: + >>>results.sizes().to_pandas() + To recombine filtered dataarrays, use `xr.concat` with dim 'flow': + >>>xr.concat([results.sizes(start='Fernwärme'), results.sizes(end='Fernwärme')], dim='flow') + + """ + if self._sizes is None: + self._sizes = self._assign_flow_coords( + xr.concat([flow.size.rename(flow.label) for flow in self.flows.values()], + dim=pd.Index(self.flows.keys(), name='flow')) + ).rename('flow_sizes') + filters = {k: v for k, v in {'start': start, 'end': end, 'component': component}.items() if v is not None} + return filter_dataarray_by_coord(self._sizes, **filters) + + def _assign_flow_coords(self, da: xr.DataArray): + # Add start and end coordinates + da = da.assign_coords({ + 'start': ('flow', [flow.start for flow in self.flows.values()]), + 'end': ('flow', [flow.end for flow in self.flows.values()]), + 'component': ('flow', [flow.component for flow in self.flows.values()]), + }) + + # Ensure flow is the last dimension if needed + existing_dims = [d for d in da.dims if d != 'flow'] + da = da.transpose(*(existing_dims + ['flow'])) + return da + + def _get_flow_network_info(self) -> Dict[str, Dict[str, str]]: + flow_network_info = {} + + for flow in self.flows.values(): + flow_network_info[flow.label] = { + 'label': flow.label, + 'start': flow.start, + 'end': flow.end, + } + return flow_network_info + def get_effect_shares( self, element: str, @@ -336,7 +489,7 @@ def _compute_effect_total( element: str, effect: str, mode: Literal['operation', 'invest', 'total'] = 'total', - include_flows: bool = False + include_flows: bool = False, ) -> xr.DataArray: """Calculates the total effect for a specific element and effect. @@ -351,6 +504,7 @@ def _compute_effect_total( 'invest': Returns investment-specific effects. 'total': Returns the sum of operation effects (across all timesteps) and investment effects. Defaults to 'total'. + include_flows: Whether to include effects from flows connected to this element. Returns: An xarray DataArray containing the total effects, named with pattern @@ -365,12 +519,20 @@ def _compute_effect_total( if mode == 'total': operation = self._compute_effect_total(element=element, effect=effect, mode='operation', include_flows=include_flows) - if len(operation.indexes) > 0: + invest = self._compute_effect_total(element=element, effect=effect, mode='invest', include_flows=include_flows) + if invest.isnull().all() and operation.isnull().all(): + return xr.DataArray(np.nan) + if operation.isnull().all(): + return invest.rename(f'{element}->{effect}') + operation = operation.sum('time') + if invest.isnull().all(): + return operation.rename(f'{element}->{effect}') + if 'time' in operation.indexes: operation = operation.sum('time') - return (operation + self._compute_effect_total(element=element, effect=effect, mode='invest', include_flows=include_flows) - ).rename(f'{element}->{effect}') + return invest + operation total = xr.DataArray(0) + share_exists = False relevant_conversion_factors = { key[0]: value for key, value in self.effect_share_factors[mode].items() if key[1] == effect @@ -380,8 +542,9 @@ def _compute_effect_total( for target_effect, conversion_factor in relevant_conversion_factors.items(): label = f'{element}->{target_effect}({mode})' if label in self.solution: + share_exists = True da = self.solution[label] - total = total + da * conversion_factor + total = da * conversion_factor + total if include_flows: if element not in self.components: @@ -391,9 +554,11 @@ def _compute_effect_total( for flow in flows: label = f'{flow}->{target_effect}({mode})' if label in self.solution: + share_exists = True da = self.solution[label] - total = total + da * conversion_factor - + total = da * conversion_factor + total + if not share_exists: + total = xr.DataArray(np.nan) return total.rename(f'{element}->{effect}({mode})') def _create_effects_dataset(self, mode: Literal['operation', 'invest', 'total'] = 'total') -> xr.Dataset: @@ -404,36 +569,41 @@ def _create_effects_dataset(self, mode: Literal['operation', 'invest', 'total'] mode: The calculation mode ('operation', 'invest', or 'total'). Returns: - An xarray Dataset with components as a dimension and effects as variables. + An xarray Dataset with components as dimension and effects as variables. """ - data_vars = {} + # Create an empty dataset + ds = xr.Dataset() + # Add each effect as a variable to the dataset for effect in self.effects: # Create a list of DataArrays, one for each component - effect_arrays = [ - self._compute_effect_total(element=component, effect=effect, mode=mode, include_flows=True) - .expand_dims(component=[component]) # Add component dimension to each array + component_arrays = [ + self._compute_effect_total(element=component, effect=effect, mode=mode, include_flows=True).expand_dims( + component=[component] + ) # Add component dimension to each array for component in list(self.components) ] # Combine all components into one DataArray for this effect - if effect_arrays: - data_vars[effect] = xr.concat(effect_arrays, dim="component", coords='minimal') - - ds = xr.Dataset(data_vars) + if component_arrays: + effect_array = xr.concat(component_arrays, dim='component', coords='minimal') + # Add this effect as a variable to the dataset + ds[effect] = effect_array # For now include a test to ensure correctness - suffix = {'operation': '(operation)|total_per_timestep', - 'invest': '(invest)|total', - 'total': '|total', - } + suffix = { + 'operation': '(operation)|total_per_timestep', + 'invest': '(invest)|total', + 'total': '|total', + } for effect in self.effects: label = f'{effect}{suffix[mode]}' - computed = ds.sum('component')[effect] + computed = ds[effect].sum('component') found = self.solution[label] if not np.allclose(computed.values, found.fillna(0).values): - logger.critical(f'Results for {effect}({mode}) in effects_dataset doesnt match {label}\n' - f'{computed=}\n, {found=}') + logger.critical( + f'Results for {effect}({mode}) in effects_dataset doesnt match {label}\n{computed=}\n, {found=}' + ) return ds @@ -549,10 +719,6 @@ def to_file( class _ElementResults: - @classmethod - def from_json(cls, calculation_results, json_data: Dict) -> '_ElementResults': - return cls(calculation_results, json_data['label'], json_data['variables'], json_data['constraints']) - def __init__( self, calculation_results: CalculationResults, label: str, variables: List[str], constraints: List[str] ): @@ -585,7 +751,7 @@ def constraints(self) -> linopy.Constraints: """ if self._calculation_results.model is None: raise ValueError('The linopy model is not available.') - return self._calculation_results.model.constraints[self._variable_names] + return self._calculation_results.model.constraints[self._constraint_names] def filter_solution( self, @@ -630,17 +796,6 @@ def filter_solution( class _NodeResults(_ElementResults): - @classmethod - def from_json(cls, calculation_results, json_data: Dict) -> '_NodeResults': - return cls( - calculation_results, - json_data['label'], - json_data['variables'], - json_data['constraints'], - json_data['inputs'], - json_data['outputs'], - ) - def __init__( self, calculation_results: CalculationResults, @@ -649,10 +804,12 @@ def __init__( constraints: List[str], inputs: List[str], outputs: List[str], + flows: List[str], ): super().__init__(calculation_results, label, variables, constraints) self.inputs = inputs self.outputs = outputs + self.flows = flows def plot_node_balance( self, @@ -979,6 +1136,42 @@ def get_shares_from(self, element: str): return self.solution[[name for name in self._variable_names if name.startswith(f'{element}->')]] +class FlowResults(_ElementResults): + def __init__( + self, + calculation_results: CalculationResults, + label: str, + variables: List[str], + constraints: List[str], + start: str, + end: str, + component: str, + ): + super().__init__(calculation_results, label, variables, constraints) + self.start = start + self.end = end + self.component = component + + @property + def flow_rate(self) -> xr.DataArray: + return self.solution[f'{self.label}|flow_rate'] + + @property + def flow_hours(self) -> xr.DataArray: + return (self.flow_rate * self._calculation_results.hours_per_timestep).rename(f'{self.label}|flow_hours') + + @property + def size(self) -> xr.DataArray: + name = f'{self.label}|size' + if name in self.solution: + return self.solution[name] + try: + return xr.DataArray(self._calculation_results.flow_system.flows[self.label].size).rename(name) + except _FlowSystemRestorationError: + logger.critical(f'Size of flow {self.label}.size not availlable. Returning NaN') + return xr.DataArray(np.nan).rename(name) + + class SegmentedCalculationResults: """ Class to store the results of a SegmentedCalculation. @@ -1336,3 +1529,62 @@ def filter_dataset( ) from e return filtered_ds + + +def filter_dataarray_by_coord( + da: xr.DataArray, + **kwargs: Optional[Union[str, List[str]]] +) -> xr.DataArray: + """Filter flows by node and component attributes. + + Filters are applied in the order they are specified. All filters must match for an edge to be included. + + To recombine filtered dataarrays, use `xr.concat`. + + xr.concat([res.sizes(start='Fernwärme'), res.sizes(end='Fernwärme')], dim='flow') + + Args: + da: Flow DataArray with network metadata coordinates. + **kwargs: Coord filters as name=value pairs. + + Returns: + Filtered DataArray with matching edges. + + Raises: + AttributeError: If required coordinates are missing. + ValueError: If specified nodes don't exist or no matches found. + """ + # Helper function to process filters + def apply_filter(array, coord_name: str, coord_values: Union[Any, List[Any]]): + # Verify coord exists + if coord_name not in array.coords: + raise AttributeError(f"Missing required coordinate '{coord_name}'") + + # Convert single value to list + val_list = [coord_values] if isinstance(coord_values, str) else coord_values + + # Verify coord_values exist + available = set(array[coord_name].values) + missing = [v for v in val_list if v not in available] + if missing: + raise ValueError(f"{coord_name.title()} value(s) not found: {missing}") + + # Apply filter + return array.where( + array[coord_name].isin(val_list) if isinstance(coord_values, list) else array[coord_name] == coord_values, + drop=True + ) + + # Apply filters from kwargs + filters = {k: v for k, v in kwargs.items() if v is not None} + try: + for coord, values in filters.items(): + da = apply_filter(da, coord, values) + except ValueError as e: + raise ValueError(f"No edges match criteria: {filters}") + + # Verify results exist + if da.size == 0: + raise ValueError(f"No edges match criteria: {filters}") + + return da diff --git a/flixopt/structure.py b/flixopt/structure.py index 1285dc885..7c7772dad 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -106,6 +106,10 @@ def solution(self): effect.label_full: effect.model.results_structure() for effect in sorted(self.flow_system.effects, key=lambda effect: effect.label_full.upper()) }, + 'Flows': { + flow.label_full: flow.model.results_structure() + for flow in sorted(self.flow_system.flows.values(), key=lambda flow: flow.label_full.upper()) + }, } return solution.reindex(time=self.time_series_collection.timesteps_extra) @@ -495,8 +499,7 @@ def __init__(self, model: SystemModel, element: Element): def results_structure(self): return { - 'label': self.label, - 'label_full': self.label_full, + 'label': self.label_full, 'variables': list(self.variables), 'constraints': list(self.constraints), } diff --git a/tests/test_effect.py b/tests/test_effect.py index 93f417f22..ff85d0556 100644 --- a/tests/test_effect.py +++ b/tests/test_effect.py @@ -184,41 +184,41 @@ def test_shares(self, basic_flow_system_linopy): for key, value in effect_share_factors['invest'].items(): np.testing.assert_allclose(results.effect_share_factors['invest'][key].values, value) - xr.testing.assert_allclose(results.get_effects_per_component('operation').sum('component')['Costs'], + xr.testing.assert_allclose(results.effects_per_component('operation').sum('component')['Costs'], results.solution['Costs(operation)|total_per_timestep'].fillna(0)) - xr.testing.assert_allclose(results.get_effects_per_component('operation').sum('component')['Effect1'], + xr.testing.assert_allclose(results.effects_per_component('operation').sum('component')['Effect1'], results.solution['Effect1(operation)|total_per_timestep'].fillna(0)) - xr.testing.assert_allclose(results.get_effects_per_component('operation').sum('component')['Effect2'], + xr.testing.assert_allclose(results.effects_per_component('operation').sum('component')['Effect2'], results.solution['Effect2(operation)|total_per_timestep'].fillna(0)) - xr.testing.assert_allclose(results.get_effects_per_component('operation').sum('component')['Effect3'], + xr.testing.assert_allclose(results.effects_per_component('operation').sum('component')['Effect3'], results.solution['Effect3(operation)|total_per_timestep'].fillna(0)) # Invest mode checks - xr.testing.assert_allclose(results.get_effects_per_component('invest').sum('component')['Costs'], + xr.testing.assert_allclose(results.effects_per_component('invest').sum('component')['Costs'], results.solution['Costs(invest)|total']) - xr.testing.assert_allclose(results.get_effects_per_component('invest').sum('component')['Effect1'], + xr.testing.assert_allclose(results.effects_per_component('invest').sum('component')['Effect1'], results.solution['Effect1(invest)|total']) - xr.testing.assert_allclose(results.get_effects_per_component('invest').sum('component')['Effect2'], + xr.testing.assert_allclose(results.effects_per_component('invest').sum('component')['Effect2'], results.solution['Effect2(invest)|total']) - xr.testing.assert_allclose(results.get_effects_per_component('invest').sum('component')['Effect3'], + xr.testing.assert_allclose(results.effects_per_component('invest').sum('component')['Effect3'], results.solution['Effect3(invest)|total']) # Total mode checks - xr.testing.assert_allclose(results.get_effects_per_component('total').sum('component')['Costs'], + xr.testing.assert_allclose(results.effects_per_component('total').sum('component')['Costs'], results.solution['Costs|total']) - xr.testing.assert_allclose(results.get_effects_per_component('total').sum('component')['Effect1'], + xr.testing.assert_allclose(results.effects_per_component('total').sum('component')['Effect1'], results.solution['Effect1|total']) - xr.testing.assert_allclose(results.get_effects_per_component('total').sum('component')['Effect2'], + xr.testing.assert_allclose(results.effects_per_component('total').sum('component')['Effect2'], results.solution['Effect2|total']) - xr.testing.assert_allclose(results.get_effects_per_component('total').sum('component')['Effect3'], + xr.testing.assert_allclose(results.effects_per_component('total').sum('component')['Effect3'], results.solution['Effect3|total']) From dbfb1b53ccc6c7f8c3d8ad169bee50b7d2d14937 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 28 Apr 2025 20:27:56 +0200 Subject: [PATCH 039/448] ruff check --- flixopt/results.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flixopt/results.py b/flixopt/results.py index b1e72f4be..e08e14636 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -3,7 +3,7 @@ import logging import pathlib import warnings -from typing import TYPE_CHECKING, Dict, List, Literal, Optional, Tuple, Union, Any +from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Tuple, Union import linopy import matplotlib.pyplot as plt @@ -1580,7 +1580,7 @@ def apply_filter(array, coord_name: str, coord_values: Union[Any, List[Any]]): try: for coord, values in filters.items(): da = apply_filter(da, coord, values) - except ValueError as e: + except ValueError: raise ValueError(f"No edges match criteria: {filters}") # Verify results exist From 6738d34c396e608d49f0206a01528a9efb4e3968 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 28 Apr 2025 20:31:46 +0200 Subject: [PATCH 040/448] ruff check --- flixopt/results.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/flixopt/results.py b/flixopt/results.py index e08e14636..41c98be15 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -16,6 +16,7 @@ from . import io as fx_io from . import plotting from .core import TimeSeriesCollection +from .flow_system import FlowSystem if TYPE_CHECKING: import pyvis @@ -181,7 +182,9 @@ def __init__( if 'Flows' not in self.solution.attrs: warnings.warn( 'No Data about flows found in the results. This data is only included since v2.2.0. Some functionality ' - 'is not availlable. We recommend to evaluate your results with a version <2.2.0.') + 'is not availlable. We recommend to evaluate your results with a version <2.2.0.', + stacklevel=2, + ) self.flows = {} else: self.flows = { @@ -393,7 +396,7 @@ def sizes( start: Optional source node(s) to filter by. Can be a single node name or a list of names. end: Optional destination node(s) to filter by. Can be a single node name or a list of names. component: Optional component(s) to filter by. Can be a single component name or a list of names. - + Further usage: Convert the dataarray to a dataframe: >>>results.sizes().to_pandas() @@ -1536,11 +1539,11 @@ def filter_dataarray_by_coord( **kwargs: Optional[Union[str, List[str]]] ) -> xr.DataArray: """Filter flows by node and component attributes. - + Filters are applied in the order they are specified. All filters must match for an edge to be included. - + To recombine filtered dataarrays, use `xr.concat`. - + xr.concat([res.sizes(start='Fernwärme'), res.sizes(end='Fernwärme')], dim='flow') Args: @@ -1580,8 +1583,8 @@ def apply_filter(array, coord_name: str, coord_values: Union[Any, List[Any]]): try: for coord, values in filters.items(): da = apply_filter(da, coord, values) - except ValueError: - raise ValueError(f"No edges match criteria: {filters}") + except ValueError as e: + raise ValueError(f"No edges match criteria: {filters}") from e # Verify results exist if da.size == 0: From c64d12eef6bf7f3d92aa326e4e0d46924f744d6c Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 28 Apr 2025 22:04:00 +0200 Subject: [PATCH 041/448] Scenarios/deprecation (#258) * Deprecate .active_timesteps * Improve logger warning * Starting release notes --- docs/release-notes/v2.2.0.md | 55 ++++++++++++++++++++++++++++++++++++ flixopt/calculation.py | 49 ++++++++++++++++++++++---------- flixopt/components.py | 4 +-- flixopt/core.py | 6 ++-- tests/test_timeseries.py | 2 +- 5 files changed, 95 insertions(+), 21 deletions(-) create mode 100644 docs/release-notes/v2.2.0.md diff --git a/docs/release-notes/v2.2.0.md b/docs/release-notes/v2.2.0.md new file mode 100644 index 000000000..3cf7eef8d --- /dev/null +++ b/docs/release-notes/v2.2.0.md @@ -0,0 +1,55 @@ +# Release v2.2.0 + +**Release Date:** YYYY-MM-DD + +## What's New + +### Scenarios +Scenarios are a new feature of flixopt. They can be used to model uncertainties in the flow system, such as: +* Different demand profiles +* Different price forecasts +* Different weather conditions +* Different climate conditions +The might also be used to model an evolving system with multiple investment periods. Each **scenario** might be a new year, a new month, or a new day, with a different set of investment decisions to take. + +The weighted sum of the total objective effect of each scenario is used as the objective of the optimization. + +#### Investments and scenarios +Scenarios allow for more flexibility in investment decisions. +You can decide to allow different investment decisions for each scenario, or to allow a single investment decision for a subset of all scnarios, while not allowing for an invest in others. +This enables the following use cases: +* Find the best investment decision for each scenario individually +* Find the best overall investment decision for possible scenarios (robust decision-making) +* Find the best overall investment decision for a subset of all scenarios + +The last one might be useful if you want to model a system with multiple investment periods, where one investment decision is made for more than one scenario. +This might occur when scenarios represent years or months, while an investment decision influences the system for multiple years or months. + + +## Other new features +* Balanced storage - Storage charging and discharging sizes can now be forced to be equal in when optimizing their size. +* Feature 2 - Description + +## Improvements + +* Improvement 1 - Description +* Improvement 2 - Description + +## Bug Fixes + +* Fixed issue with X +* Resolved problem with Y + +## Breaking Changes + +* Change 1 - Migration instructions +* Change 2 - Migration instructions + +## Deprecations + +* Feature X will be removed in v{next_version} + +## Dependencies + +* Added dependency X v1.2.3 +* Updated dependency Y to v2.0.0 \ No newline at end of file diff --git a/flixopt/calculation.py b/flixopt/calculation.py index 2024739ea..942b63a81 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -12,6 +12,7 @@ import math import pathlib import timeit +import warnings from typing import Any, Dict, List, Optional, Union import numpy as np @@ -43,22 +44,31 @@ def __init__( self, name: str, flow_system: FlowSystem, - active_timesteps: Optional[pd.DatetimeIndex] = None, + selected_timesteps: Optional[pd.DatetimeIndex] = None, selected_scenarios: Optional[pd.Index] = None, folder: Optional[pathlib.Path] = None, + active_timesteps: Optional[pd.DatetimeIndex] = None, ): """ Args: name: name of calculation flow_system: flow_system which should be calculated - active_timesteps: timesteps which should be used for calculation. If None, then all timesteps are used. + selected_timesteps: timesteps which should be used for calculation. If None, then all timesteps are used. selected_scenarios: scenarios which should be used for calculation. If None, then all scenarios are used. folder: folder where results should be saved. If None, then the current working directory is used. + active_timesteps: Deprecated. Use selected_timesteps instead. """ + if active_timesteps is not None: + warnings.warn( + 'active_timesteps is deprecated. Use selected_timesteps instead.', + DeprecationWarning, + stacklevel=2, + ) + selected_timesteps = active_timesteps self.name = name self.flow_system = flow_system self.model: Optional[SystemModel] = None - self.active_timesteps = active_timesteps + self.selected_timesteps = selected_timesteps self.selected_scenarios = selected_scenarios self.durations = {'modeling': 0.0, 'solving': 0.0, 'saving': 0.0} @@ -133,6 +143,15 @@ def summary(self): 'Config': CONFIG.to_dict(), } + @property + def active_timesteps(self) -> pd.DatetimeIndex: + warnings.warn( + 'active_timesteps is deprecated. Use selected_timesteps instead.', + DeprecationWarning, + stacklevel=2, + ) + return self.selected_timesteps + class FullCalculation(Calculation): """ @@ -189,7 +208,7 @@ def solve(self, solver: _Solver, log_file: Optional[pathlib.Path] = None, log_ma def _activate_time_series(self): self.flow_system.transform_data() self.flow_system.time_series_collection.set_selection( - timesteps=self.active_timesteps, scenarios=self.selected_scenarios + timesteps=self.selected_timesteps, scenarios=self.selected_scenarios ) @@ -204,7 +223,7 @@ def __init__( flow_system: FlowSystem, aggregation_parameters: AggregationParameters, components_to_clusterize: Optional[List[Component]] = None, - active_timesteps: Optional[pd.DatetimeIndex] = None, + selected_timesteps: Optional[pd.DatetimeIndex] = None, folder: Optional[pathlib.Path] = None, ): """ @@ -218,13 +237,13 @@ def __init__( components_to_clusterize: List of Components to perform aggregation on. If None, then all components are aggregated. This means, teh variables in the components are equalized to each other, according to the typical periods computed in the DataAggregation - active_timesteps: pd.DatetimeIndex or None + selected_timesteps: pd.DatetimeIndex or None list with indices, which should be used for calculation. If None, then all timesteps are used. folder: folder where results should be saved. If None, then the current working directory is used. """ if flow_system.time_series_collection.scenarios is not None: raise ValueError('Aggregation is not supported for scenarios yet. Please use FullCalculation instead.') - super().__init__(name, flow_system, active_timesteps, folder=folder) + super().__init__(name, flow_system, selected_timesteps, folder=folder) self.aggregation_parameters = aggregation_parameters self.components_to_clusterize = components_to_clusterize self.aggregation = None @@ -342,7 +361,7 @@ def __init__( self.segment_names = [ f'Segment_{i + 1}' for i in range(math.ceil(len(self.all_timesteps) / self.timesteps_per_segment)) ] - self.active_timesteps_per_segment = self._calculate_timesteps_of_segment() + self.selected_timesteps_per_segment = self._calculate_timesteps_of_segment() assert timesteps_per_segment > 2, 'The Segment length must be greater 2, due to unwanted internal side effects' assert self.timesteps_per_segment_with_overlap <= len(self.all_timesteps), ( @@ -368,7 +387,7 @@ def do_modeling_and_solve( logger.info(f'{" Segmented Solving ":#^80}') for i, (segment_name, timesteps_of_segment) in enumerate( - zip(self.segment_names, self.active_timesteps_per_segment, strict=False) + zip(self.segment_names, self.selected_timesteps_per_segment, strict=False) ): if self.sub_calculations: self._transfer_start_values(i) @@ -379,7 +398,7 @@ def do_modeling_and_solve( ) calculation = FullCalculation( - f'{self.name}-{segment_name}', self.flow_system, active_timesteps=timesteps_of_segment + f'{self.name}-{segment_name}', self.flow_system, selected_timesteps=timesteps_of_segment ) self.sub_calculations.append(calculation) calculation.do_modeling() @@ -413,9 +432,9 @@ def _transfer_start_values(self, segment_index: int): This function gets the last values of the previous solved segment and inserts them as start values for the next segment """ - timesteps_of_prior_segment = self.active_timesteps_per_segment[segment_index - 1] + timesteps_of_prior_segment = self.selected_timesteps_per_segment[segment_index - 1] - start = self.active_timesteps_per_segment[segment_index][0] + start = self.selected_timesteps_per_segment[segment_index][0] start_previous_values = timesteps_of_prior_segment[self.timesteps_per_segment - self.nr_of_previous_values] end_previous_values = timesteps_of_prior_segment[self.timesteps_per_segment - 1] @@ -444,12 +463,12 @@ def _reset_start_values(self): comp.initial_charge_state = self._original_start_values[comp.label_full] def _calculate_timesteps_of_segment(self) -> List[pd.DatetimeIndex]: - active_timesteps_per_segment = [] + selected_timesteps_per_segment = [] for i, _ in enumerate(self.segment_names): start = self.timesteps_per_segment * i end = min(start + self.timesteps_per_segment_with_overlap, len(self.all_timesteps)) - active_timesteps_per_segment.append(self.all_timesteps[start:end]) - return active_timesteps_per_segment + selected_timesteps_per_segment.append(self.all_timesteps[start:end]) + return selected_timesteps_per_segment @property def timesteps_per_segment_with_overlap(self): diff --git a/flixopt/components.py b/flixopt/components.py index 234418694..e02f5c03a 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -87,8 +87,8 @@ def _plausibility_checks(self) -> None: for flow in self.flows.values(): if isinstance(flow.size, InvestParameters) and flow.size.fixed_size is None: logger.warning( - f'Piecewise_conversion (in {self.label_full}) and variable size ' - f'(in flow {flow.label_full}) do not make sense together!' + f'Using a FLow with a fixed size ({flow.label_full}) AND a piecewise_conversion ' + f'(in {self.label_full}) and variable size is uncommon. Please check if this is intended!' ) def transform_data(self, flow_system: 'FlowSystem'): diff --git a/flixopt/core.py b/flixopt/core.py index 5d24e46e4..758deb1aa 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -688,7 +688,7 @@ def to_json(self, path: Optional[pathlib.Path] = None) -> Dict[str, Any]: # Save to file if path is provided if path is not None: - indent = 4 if len(self.active_timesteps) <= 480 else None + indent = 4 if len(self.selected_timesteps) <= 480 else None with open(path, 'w', encoding='utf-8') as f: json.dump(data, f, indent=indent, ensure_ascii=False) @@ -718,7 +718,7 @@ def selected_data(self) -> xr.DataArray: return self._stored_data.sel(**self._valid_selector) @property - def active_timesteps(self) -> Optional[pd.DatetimeIndex]: + def selected_timesteps(self) -> Optional[pd.DatetimeIndex]: """Get the current active timesteps, or None if no time dimension.""" if not self.has_time_dim: return None @@ -749,7 +749,7 @@ def update_stored_data(self, value: xr.DataArray) -> None: """ new_data = DataConverter.as_dataarray( value, - timesteps=self.active_timesteps if self.has_time_dim else None, + timesteps=self.selected_timesteps if self.has_time_dim else None, scenarios=self.active_scenarios if self.has_scenario_dim else None, ) diff --git a/tests/test_timeseries.py b/tests/test_timeseries.py index 237935e59..bb35231a6 100644 --- a/tests/test_timeseries.py +++ b/tests/test_timeseries.py @@ -675,7 +675,7 @@ def test_selection_propagation_with_scenarios( # Clear selections sample_scenario_allocator.set_selection() assert ts1._selected_timesteps is None - assert ts1.active_timesteps.equals(sample_scenario_allocator.timesteps) + assert ts1.selected_timesteps.equals(sample_scenario_allocator.timesteps) assert ts1._selected_scenarios is None assert ts1.active_scenarios.equals(sample_scenario_allocator.scenarios) assert ts1.selected_data.shape == (5, 3) # Back to full shape From 0499497d3c7dc026b8e7348cd4af9177eea83917 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 28 Apr 2025 22:13:48 +0200 Subject: [PATCH 042/448] Bugfix in plausibility_check: Index 0 --- flixopt/components.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flixopt/components.py b/flixopt/components.py index e02f5c03a..e4c220e05 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -248,9 +248,9 @@ def _plausibility_checks(self) -> None: minimum_capacity = self.capacity_in_flow_hours # initial capacity >= allowed min for maximum_size: - minimum_inital_capacity = maximum_capacity * self.relative_minimum_charge_state.isel(time=1) + minimum_inital_capacity = maximum_capacity * self.relative_minimum_charge_state.isel(time=0) # initial capacity <= allowed max for minimum_size: - maximum_inital_capacity = minimum_capacity * self.relative_maximum_charge_state.isel(time=1) + maximum_inital_capacity = minimum_capacity * self.relative_maximum_charge_state.isel(time=0) # TODO: index=1 ??? I think index 0 if (self.initial_charge_state > maximum_inital_capacity).any(): From ee005777eb8a363691cde276666c95832005b056 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 29 Apr 2025 10:32:58 +0200 Subject: [PATCH 043/448] Set bargap to 0 in stacked bars --- flixopt/plotting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flixopt/plotting.py b/flixopt/plotting.py index 6a2170674..8537d3815 100644 --- a/flixopt/plotting.py +++ b/flixopt/plotting.py @@ -258,7 +258,7 @@ def with_plotly( fig.update_layout( barmode='relative', - bargap=0.2, # No space between bars + bargap=0, # No space between bars bargroupgap=0, # No space between grouped bars ) if style == 'grouped_bar': From a11ed92a20bacf46605d504a938f506aa12922b1 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 29 Apr 2025 11:05:03 +0200 Subject: [PATCH 044/448] Ensure the size is always properly indexed in results. --- flixopt/results.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/flixopt/results.py b/flixopt/results.py index 41c98be15..26dd9a770 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -15,7 +15,7 @@ from . import io as fx_io from . import plotting -from .core import TimeSeriesCollection +from .core import TimeSeriesCollection, DataConverter from .flow_system import FlowSystem if TYPE_CHECKING: @@ -1169,7 +1169,10 @@ def size(self) -> xr.DataArray: if name in self.solution: return self.solution[name] try: - return xr.DataArray(self._calculation_results.flow_system.flows[self.label].size).rename(name) + return DataConverter.as_dataarray( + self._calculation_results.flow_system.flows[self.label].size, + scenarios=self._calculation_results.scenarios + ).rename(name) except _FlowSystemRestorationError: logger.critical(f'Size of flow {self.label}.size not availlable. Returning NaN') return xr.DataArray(np.nan).rename(name) From 240024405e1d9500aca527727f866d5db3fe981d Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 29 Apr 2025 13:48:52 +0200 Subject: [PATCH 045/448] ruff check --- flixopt/results.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flixopt/results.py b/flixopt/results.py index 26dd9a770..f0f0b2b0e 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -15,7 +15,7 @@ from . import io as fx_io from . import plotting -from .core import TimeSeriesCollection, DataConverter +from .core import DataConverter, TimeSeriesCollection from .flow_system import FlowSystem if TYPE_CHECKING: From 0f9b30ab8fc201d681721889bb92064e46618d7b Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 5 May 2025 14:28:29 +0200 Subject: [PATCH 046/448] BUGFIX in extract data, that causes coords in linopy to be incorrect (scalar xarray.DataArrays) --- flixopt/core.py | 2 +- flixopt/effects.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/flixopt/core.py b/flixopt/core.py index 758deb1aa..bec472aaa 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -1481,5 +1481,5 @@ def extract_data( if isinstance(data, xr.DataArray): return data if isinstance(data, (int, float, np.integer, np.floating)): - return xr.DataArray(data) + return data raise TypeError(f'Unsupported data type: {type(data).__name__}') diff --git a/flixopt/effects.py b/flixopt/effects.py index 914100362..44687fffe 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -509,6 +509,10 @@ def calculate_all_conversion_paths( # Add new path to queue for further exploration queue.append((target, indirect_factor, new_path)) + # Convert all values to DataArrays + result = {key: value if isinstance(value, xr.DataArray) else xr.DataArray(value) + for key, value in result.items()} + return result From 26e89a95083ae76c88059d9c42e30a621d8749b9 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 5 May 2025 14:35:23 +0200 Subject: [PATCH 047/448] Improve yaml formatting for model documentation (#259) --- flixopt/io.py | 79 +++++++++++++++++++++++++++++++++++---------------- 1 file changed, 54 insertions(+), 25 deletions(-) diff --git a/flixopt/io.py b/flixopt/io.py index 1ef9578e5..05ae6741d 100644 --- a/flixopt/io.py +++ b/flixopt/io.py @@ -79,15 +79,17 @@ def _save_to_yaml(data, output_file='formatted_output.yaml'): output_file (str): Path to output YAML file """ # Process strings to normalize all newlines and handle special patterns - processed_data = _process_complex_strings(data) + processed_data = _normalize_complex_data(data) # Define a custom representer for strings def represent_str(dumper, data): - # Use literal block style (|) for any string with newlines + # Use literal block style (|) for multi-line strings if '\n' in data: + # Clean up formatting for literal block style + data = data.strip() # Remove leading/trailing whitespace return dumper.represent_scalar('tag:yaml.org,2002:str', data, style='|') - # Use quoted style for strings with special characters to ensure proper parsing + # Use quoted style for strings with special characters elif any(char in data for char in ':`{}[]#,&*!|>%@'): return dumper.represent_scalar('tag:yaml.org,2002:str', data, style='"') @@ -97,53 +99,80 @@ def represent_str(dumper, data): # Add the string representer to SafeDumper yaml.add_representer(str, represent_str, Dumper=yaml.SafeDumper) + # Configure dumper options for better formatting + class CustomDumper(yaml.SafeDumper): + def increase_indent(self, flow=False, indentless=False): + return super(CustomDumper, self).increase_indent(flow, False) + # Write to file with settings that ensure proper formatting with open(output_file, 'w', encoding='utf-8') as file: yaml.dump( processed_data, file, - Dumper=yaml.SafeDumper, + Dumper=CustomDumper, sort_keys=False, # Preserve dictionary order default_flow_style=False, # Use block style for mappings - width=float('inf'), # Don't wrap long lines + width=1000, # Set a reasonable line width allow_unicode=True, # Support Unicode characters + indent=2, # Set consistent indentation ) -def _process_complex_strings(data): +def _normalize_complex_data(data): """ - Process dictionary data recursively with comprehensive string normalization. - Handles various types of strings and special formatting. + Recursively normalize strings in complex data structures. + + Handles dictionaries, lists, and strings, applying various text normalization + rules while preserving important formatting elements. Args: - data: The data to process (dict, list, str, or other) + data: Any data type (dict, list, str, or primitive) Returns: - Processed data with normalized strings + Data with all strings normalized according to defined rules """ if isinstance(data, dict): - return {k: _process_complex_strings(v) for k, v in data.items()} + return {key: _normalize_complex_data(value) for key, value in data.items()} + elif isinstance(data, list): - return [_process_complex_strings(item) for item in data] + return [_normalize_complex_data(item) for item in data] + elif isinstance(data, str): - # Step 1: Normalize line endings to \n - normalized = data.replace('\r\n', '\n').replace('\r', '\n') + return _normalize_string_content(data) - # Step 2: Handle escaped newlines with robust regex - normalized = re.sub(r'(? Dict[str, str]: From c0cbaaeb88cb3100866085a7ff6d79a4321634fd Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 5 May 2025 15:04:57 +0200 Subject: [PATCH 048/448] Make the size/capacity a TimeSeries (#260) --- flixopt/components.py | 6 +++++- flixopt/elements.py | 16 ++++++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/flixopt/components.py b/flixopt/components.py index e4c220e05..5a986ede0 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -128,7 +128,7 @@ def __init__( label: str, charging: Flow, discharging: Flow, - capacity_in_flow_hours: Union[Scalar, InvestParameters], + capacity_in_flow_hours: Union[ScenarioData, InvestParameters], relative_minimum_charge_state: TimestepData = 0, relative_maximum_charge_state: TimestepData = 1, initial_charge_state: Union[ScenarioData, Literal['lastValueOfSim']] = 0, @@ -226,6 +226,10 @@ def transform_data(self, flow_system: 'FlowSystem') -> None: ) if isinstance(self.capacity_in_flow_hours, InvestParameters): self.capacity_in_flow_hours.transform_data(flow_system, f'{self.label_full}|InvestParameters') + else: + self.capacity_in_flow_hours = flow_system.create_time_series( + f'{self.label_full}|capacity_in_flow_hours', self.capacity_in_flow_hours, has_time_dim=False + ) def _plausibility_checks(self) -> None: """ diff --git a/flixopt/elements.py b/flixopt/elements.py index 11246e6d9..d216c2ca7 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -154,7 +154,7 @@ def __init__( self, label: str, bus: str, - size: Union[Scalar, InvestParameters] = None, + size: Union[ScenarioData, InvestParameters] = None, fixed_relative_profile: Optional[TimestepData] = None, relative_minimum: TimestepData = 0, relative_maximum: TimestepData = 1, @@ -265,6 +265,8 @@ def transform_data(self, flow_system: 'FlowSystem'): self.on_off_parameters.transform_data(flow_system, self.label_full) if isinstance(self.size, InvestParameters): self.size.transform_data(flow_system, self.label_full) + else: + self.size = flow_system.create_time_series(f'{self.label_full}|size', self.size, has_time_dim=False) def infos(self, use_numpy: bool = True, use_element_label: bool = False) -> Dict: infos = super().infos(use_numpy, use_element_label) @@ -282,8 +284,8 @@ def _plausibility_checks(self) -> None: if np.any(self.relative_minimum > self.relative_maximum): raise PlausibilityError(self.label_full + ': Take care, that relative_minimum <= relative_maximum!') - if ( - self.size == CONFIG.modeling.BIG and self.fixed_relative_profile is not None + if not isinstance(self.size, InvestParameters) and ( + np.any(self.size == CONFIG.modeling.BIG) and self.fixed_relative_profile is not None ): # Default Size --> Most likely by accident logger.warning( f'Flow "{self.label}" has no size assigned, but a "fixed_relative_profile". ' @@ -453,10 +455,12 @@ def flow_rate_bounds_on(self) -> Tuple[TimestepData, TimestepData]: relative_minimum, relative_maximum = self.flow_rate_lower_bound_relative, self.flow_rate_upper_bound_relative size = self.element.size if not isinstance(size, InvestParameters): - return relative_minimum * size, relative_maximum * size + return relative_minimum * extract_data(size), relative_maximum * extract_data(size) if size.fixed_size is not None: - return size.fixed_size * relative_minimum, size.fixed_size * relative_maximum - return size.minimum_size * relative_minimum, size.maximum_size * relative_maximum + return (relative_minimum * extract_data(size.fixed_size), + relative_maximum * extract_data(size.fixed_size)) + return (relative_minimum * extract_data(size.minimum_size), + relative_maximum * extract_data(size.maximum_size)) @property def flow_rate_lower_bound_relative(self) -> TimestepData: From 67ebfbbe75870a3f9226f9435c00748f30cc554d Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 13 May 2025 11:55:29 +0200 Subject: [PATCH 049/448] Scenarios/plot network (#262) * Catch bug in plot_network with 2D arrays * Add plot_network() to test_io.py --- flixopt/structure.py | 2 ++ tests/test_io.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/flixopt/structure.py b/flixopt/structure.py index 7c7772dad..8d4779457 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -636,6 +636,8 @@ def describe_numpy_arrays(arr: np.ndarray) -> Union[str, List]: def normalized_center_of_mass(array: Any) -> float: # position in array (0 bis 1 normiert) + if array.ndim >= 2: # No good way to calculate center of mass for 2D arrays + return np.nan positions = np.linspace(0, 1, len(array)) # weights w_i # mass center if np.sum(array) == 0: diff --git a/tests/test_io.py b/tests/test_io.py index 2b3a03399..541e5b2a4 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -28,6 +28,7 @@ def test_flow_system_file_io(flow_system, highs_solver): calculation_0 = fx.FullCalculation('IO', flow_system=flow_system) calculation_0.do_modeling() calculation_0.solve(highs_solver) + calculation_0.flow_system.plot_network() calculation_0.results.to_file() paths = CalculationResultsPaths(calculation_0.folder, calculation_0.name) @@ -36,6 +37,7 @@ def test_flow_system_file_io(flow_system, highs_solver): calculation_1 = fx.FullCalculation('Loaded_IO', flow_system=flow_system_1) calculation_1.do_modeling() calculation_1.solve(highs_solver) + calculation_1.flow_system.plot_network() assert_almost_equal_numeric( calculation_0.results.model.objective.value, From 9edd1fa13da2aeb527007e1e6e80f31a75ec9b0b Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 13 May 2025 12:58:11 +0200 Subject: [PATCH 050/448] Update deploy-docs.yaml: Run on Release publishing instead of creation and only run for stable releases (vx.y.z) --- .github/workflows/deploy-docs.yaml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy-docs.yaml b/.github/workflows/deploy-docs.yaml index 73a3f0b1f..d582bb3eb 100644 --- a/.github/workflows/deploy-docs.yaml +++ b/.github/workflows/deploy-docs.yaml @@ -1,9 +1,11 @@ -name: Documentation +name: Deploy Stable Documentation on: release: - types: [created] # Automatically deploy docs on release - workflow_dispatch: # Allow manual triggering + types: [published] + tags: + # Only match stable version patterns (no pre-release identifiers) + - 'v[0-9]+.[0-9]+.[0-9]+' jobs: deploy-docs: From d3c0c48b77817495971c44da3cc10713ac0912bf Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 13 May 2025 13:42:57 +0200 Subject: [PATCH 051/448] Bugfix DataConverter and add tests (#263) --- flixopt/core.py | 8 ++++---- tests/test_dataconverter.py | 39 +++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/flixopt/core.py b/flixopt/core.py index bec472aaa..ea447e652 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -255,7 +255,7 @@ def _broadcast_time_to_scenarios( return data.copy(deep=True) # Broadcast values - values = np.tile(data.values, (len(coords['scenario']), 1)).T # Tile seems to be faster than repeat() + values = np.repeat(data.values[:, np.newaxis], len(coords['scenario']), axis=1) return xr.DataArray(values.copy(), coords=coords, dims=dims) @staticmethod @@ -278,7 +278,7 @@ def _broadcast_scenario_to_time( raise ConversionError("Source scenario coordinates don't match target scenario coordinates") # Broadcast values - values = np.repeat(data.values[:, np.newaxis], len(coords['time']), axis=1) + values = np.repeat(data.values[:, np.newaxis], len(coords['time']), axis=1).T return xr.DataArray(values.copy(), coords=coords, dims=dims) @staticmethod @@ -361,7 +361,7 @@ def _convert_ndarray_two_dims(data: np.ndarray, coords: Dict[str, pd.Index], dim return xr.DataArray(values, coords=coords, dims=dims) elif data.shape[0] == scenario_length: # Broadcast across time - values = np.tile(data, (time_length, 1)) + values = np.repeat(data[np.newaxis, :], time_length, axis=0) return xr.DataArray(values, coords=coords, dims=dims) else: raise ConversionError(f"1D array length {data.shape[0]} doesn't match either dimension") @@ -414,7 +414,7 @@ def _convert_series(data: pd.Series, coords: Dict[str, pd.Index], dims: Tuple[st # Case 1: Series is indexed by time if data.index.equals(coords['time']): # Broadcast across scenarios - values = np.tile(data.values[:, np.newaxis], (1, len(coords['scenario']))) + values = np.repeat(data.values[:, np.newaxis], len(coords['scenario']), axis=1) return xr.DataArray(values.copy(), coords=coords, dims=dims) # Case 2: Series is indexed by scenario diff --git a/tests/test_dataconverter.py b/tests/test_dataconverter.py index a50754301..0484d4aac 100644 --- a/tests/test_dataconverter.py +++ b/tests/test_dataconverter.py @@ -451,6 +451,45 @@ def test_dataarray_dimension_mismatch(self, sample_time_index, sample_scenario_i with pytest.raises(ConversionError): DataConverter.as_dataarray(wrong_length, sample_time_index, sample_scenario_index) +class TestDataArrayBroadcasting: + """Tests for broadcasting DataArrays.""" + def test_broadcast_1d_array_to_2d(self, sample_time_index, sample_scenario_index): + """Test broadcasting a 1D array to all scenarios.""" + arr_1d = np.array([1, 2, 3, 4, 5]) + + xr.testing.assert_equal( + DataConverter.as_dataarray(arr_1d, sample_time_index, sample_scenario_index), + xr.DataArray( + np.array([arr_1d] * 3).T, + coords=(sample_time_index, sample_scenario_index) + ) + ) + + arr_1d = np.array([1, 2, 3]) + xr.testing.assert_equal( + DataConverter.as_dataarray(arr_1d, sample_time_index, sample_scenario_index), + xr.DataArray( + np.array([arr_1d] * 5), + coords=(sample_time_index, sample_scenario_index) + ) + ) + + def test_broadcast_1d_array_to_1d(self, sample_time_index,): + """Test broadcasting a 1D array to all scenarios.""" + arr_1d = np.array([1, 2, 3, 4, 5]) + + xr.testing.assert_equal( + DataConverter.as_dataarray(arr_1d, sample_time_index), + xr.DataArray( + arr_1d, + coords=(sample_time_index,) + ) + ) + + arr_1d = np.array([1, 2, 3]) + with pytest.raises(ConversionError): + DataConverter.as_dataarray(arr_1d, sample_time_index) + class TestEdgeCases: """Tests for edge cases and special scenarios.""" From e6e680cb66423379c089e675e536f5d028838d9a Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 13 May 2025 14:28:13 +0200 Subject: [PATCH 052/448] Fix doc deployment to not publish on non stable releases --- .github/workflows/deploy-docs.yaml | 50 +++++++++++++++++++++++++----- 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/.github/workflows/deploy-docs.yaml b/.github/workflows/deploy-docs.yaml index d582bb3eb..dfb3f3f87 100644 --- a/.github/workflows/deploy-docs.yaml +++ b/.github/workflows/deploy-docs.yaml @@ -3,15 +3,38 @@ name: Deploy Stable Documentation on: release: types: [published] - tags: - # Only match stable version patterns (no pre-release identifiers) - - 'v[0-9]+.[0-9]+.[0-9]+' jobs: + check-release: + runs-on: ubuntu-latest + outputs: + is_stable: ${{ steps.check_version.outputs.is_stable }} + version: ${{ steps.check_version.outputs.version }} + steps: + - name: Check if stable release + id: check_version + run: | + # Extract version from the tag + VERSION="${GITHUB_REF#refs/tags/v}" + echo "Raw version: $VERSION" + + # Check if version contains any pre-release identifiers using regex + if [[ $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "is_stable=true" >> $GITHUB_OUTPUT + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Stable version detected: $VERSION" + else + echo "is_stable=false" >> $GITHUB_OUTPUT + echo "Pre-release version detected: $VERSION" + fi + deploy-docs: + needs: check-release + if: needs.check-release.outputs.is_stable == 'true' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - name: Checkout repository + uses: actions/checkout@v4 with: fetch-depth: 0 # Fetch all history for proper versioning @@ -20,7 +43,8 @@ jobs: git config user.name github-actions[bot] git config user.email 41898282+github-actions[bot]@users.noreply.github.com - - uses: actions/setup-python@v5 + - name: Set up Python + uses: actions/setup-python@v5 with: python-version: 3.11 @@ -31,5 +55,17 @@ jobs: - name: Deploy docs run: | - VERSION=${GITHUB_REF#refs/tags/v} - mike deploy --push --update-aliases $VERSION latest \ No newline at end of file + VERSION="${{ needs.check-release.outputs.version }}" + echo "Deploying documentation for version $VERSION" + mike deploy --push --update-aliases $VERSION latest + + - name: Verify deployment + run: | + # Simple verification that the deployment succeeded + git checkout gh-pages + if [ -d "${{ needs.check-release.outputs.version }}" ]; then + echo "Documentation successfully deployed" + else + echo "Documentation deployment failed!" + exit 1 + fi \ No newline at end of file From 3d89b74f0dcab9f9bfffac7fab2d556861528baa Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 15 May 2025 15:16:23 +0200 Subject: [PATCH 053/448] Remove unused code --- flixopt/results.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/flixopt/results.py b/flixopt/results.py index f0f0b2b0e..bd0abaa5e 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -202,7 +202,6 @@ def __init__( self._flow_hours = None self._sizes = None self._effects_per_component = {'operation': None, 'invest': None, 'total': None} - self._flow_network_info_ = None def __getitem__(self, key: str) -> Union['ComponentResults', 'BusResults', 'EffectResults', 'FlowResults']: if key in self.components: @@ -425,17 +424,6 @@ def _assign_flow_coords(self, da: xr.DataArray): da = da.transpose(*(existing_dims + ['flow'])) return da - def _get_flow_network_info(self) -> Dict[str, Dict[str, str]]: - flow_network_info = {} - - for flow in self.flows.values(): - flow_network_info[flow.label] = { - 'label': flow.label, - 'start': flow.start, - 'end': flow.end, - } - return flow_network_info - def get_effect_shares( self, element: str, From cc772a46528f1d7361efa38f0fae90ab2eca3842 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 16 May 2025 12:17:29 +0200 Subject: [PATCH 054/448] Remove legend placing for better auto placing in plotly --- flixopt/plotting.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/flixopt/plotting.py b/flixopt/plotting.py index 8537d3815..91fc5e7e7 100644 --- a/flixopt/plotting.py +++ b/flixopt/plotting.py @@ -348,14 +348,6 @@ def with_plotly( plot_bgcolor='rgba(0,0,0,0)', # Transparent background paper_bgcolor='rgba(0,0,0,0)', # Transparent paper background font=dict(size=14), # Increase font size for better readability - legend=dict( - orientation='h', # Horizontal legend - yanchor='bottom', - y=-0.3, # Adjusts how far below the plot it appears - xanchor='center', - x=0.5, - title_text=None, # Removes legend title for a cleaner look - ), ) return fig @@ -397,7 +389,6 @@ def with_matplotlib( - If `style` is 'stacked_bar', bars are stacked for both positive and negative values. Negative values are stacked separately without extra labels in the legend. - If `style` is 'line', stepped lines are drawn for each data series. - - The legend is placed below the plot to accommodate multiple data series. """ assert style in ['stacked_bar', 'line'], f"'style' must be one of {['stacked_bar', 'line']} for matplotlib" @@ -1137,7 +1128,6 @@ def create_pie_trace(data_series, side): paper_bgcolor='rgba(0,0,0,0)', # Transparent paper background font=dict(size=14), margin=dict(t=80, b=50, l=30, r=30), - legend=dict(orientation='h', yanchor='bottom', y=-0.2, xanchor='center', x=0.5, font=dict(size=12)), ) return fig From d92349ed19be9d0a774bdb5446fbc5d20d42ac4a Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 16 May 2025 13:12:57 +0200 Subject: [PATCH 055/448] Fix plotly dependency --- pyproject.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f3b48273f..0078d11a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,10 +38,10 @@ dependencies = [ "linopy >= 0.5.1", "netcdf4 >= 1.6.1", "rich >= 13.0.1", - "highspy >= 1.5.3", # Default solver - "pandas >= 2, < 3", # Used in post-processing + "highspy >= 1.5.3", # Default solver + "pandas >= 2, < 3", # Used in post-processing "matplotlib >= 3.5.2", # Used in post-processing - "plotly >= 5.15", # Used in post-processing + "plotly >= 5.15, < 6", # Used in post-processing "tomli >= 2.0.1" # TOML parser (only needed until python 3.11) ] From 5c2900a4290479af584bb9ca3d78474be6138752 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 19 May 2025 21:45:08 +0200 Subject: [PATCH 056/448] Improve validation when adding new effects --- flixopt/effects.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/flixopt/effects.py b/flixopt/effects.py index 44687fffe..8affc933b 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -313,7 +313,10 @@ def __contains__(self, item: Union[str, 'Effect']) -> bool: if isinstance(item, str): return item in self.effects # Check if the label exists elif isinstance(item, Effect): - return item in self.effects.values() # Check if the object exists + if item.label_full in self.effects: + return True + if item in self.effects.values(): # Check if the object exists + return True return False @property From 8d3bbe920296928086f9a098cb52698cbcbde886 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 14 Jun 2025 17:35:02 +0200 Subject: [PATCH 057/448] Moved release notes to CHANGELOG.md --- CHANGELOG.md | 27 ++++++++++++++++++ docs/release-notes/v2.2.0.md | 55 ------------------------------------ 2 files changed, 27 insertions(+), 55 deletions(-) delete mode 100644 docs/release-notes/v2.2.0.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d692d5e5..ff01fa498 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## What's New + +### Scenarios +Scenarios are a new feature of flixopt. They can be used to model uncertainties in the flow system, such as: +* Different demand profiles +* Different price forecasts +* Different weather conditions +They might also be used to model an evolving system with multiple investment periods. Each **scenario** might be a new year, a new month, or a new day, with a different set of investment decisions to take. + +The weighted sum of the total objective effect of each scenario is used as the objective of the optimization. + +#### Investments and scenarios +Scenarios allow for more flexibility in investment decisions. +You can decide to allow different investment decisions for each scenario, or to allow a single investment decision for a subset of all scenarios, while not allowing for an invest in others. +This enables the following use cases: +* Find the best investment decision for each scenario individually +* Find the best overall investment decision for possible scenarios (robust decision-making) +* Find the best overall investment decision for a subset of all scenarios + +The last one might be useful if you want to model a system with multiple investment periods, where one investment decision is made for more than one scenario. +This might occur when scenarios represent years or months, while an investment decision influences the system for multiple years or months. + + +### Other new features +* Balanced storage - Storage charging and discharging sizes can now be forced to be equal in when optimizing their size. +* Feature 2 - Description + ## [2.1.2] - 2025-06-14 ### Fixed diff --git a/docs/release-notes/v2.2.0.md b/docs/release-notes/v2.2.0.md deleted file mode 100644 index 3cf7eef8d..000000000 --- a/docs/release-notes/v2.2.0.md +++ /dev/null @@ -1,55 +0,0 @@ -# Release v2.2.0 - -**Release Date:** YYYY-MM-DD - -## What's New - -### Scenarios -Scenarios are a new feature of flixopt. They can be used to model uncertainties in the flow system, such as: -* Different demand profiles -* Different price forecasts -* Different weather conditions -* Different climate conditions -The might also be used to model an evolving system with multiple investment periods. Each **scenario** might be a new year, a new month, or a new day, with a different set of investment decisions to take. - -The weighted sum of the total objective effect of each scenario is used as the objective of the optimization. - -#### Investments and scenarios -Scenarios allow for more flexibility in investment decisions. -You can decide to allow different investment decisions for each scenario, or to allow a single investment decision for a subset of all scnarios, while not allowing for an invest in others. -This enables the following use cases: -* Find the best investment decision for each scenario individually -* Find the best overall investment decision for possible scenarios (robust decision-making) -* Find the best overall investment decision for a subset of all scenarios - -The last one might be useful if you want to model a system with multiple investment periods, where one investment decision is made for more than one scenario. -This might occur when scenarios represent years or months, while an investment decision influences the system for multiple years or months. - - -## Other new features -* Balanced storage - Storage charging and discharging sizes can now be forced to be equal in when optimizing their size. -* Feature 2 - Description - -## Improvements - -* Improvement 1 - Description -* Improvement 2 - Description - -## Bug Fixes - -* Fixed issue with X -* Resolved problem with Y - -## Breaking Changes - -* Change 1 - Migration instructions -* Change 2 - Migration instructions - -## Deprecations - -* Feature X will be removed in v{next_version} - -## Dependencies - -* Added dependency X v1.2.3 -* Updated dependency Y to v2.0.0 \ No newline at end of file From be6572de5552c31c94b0ed9b9d7777eea27cc4aa Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 23 Jun 2025 14:35:49 +0200 Subject: [PATCH 058/448] Try to add to_dataset to Elements --- flixopt/flow_system.py | 521 +++++++++++++++++++++++++++-------------- flixopt/structure.py | 381 +++++++++++++++++++++++------- 2 files changed, 647 insertions(+), 255 deletions(-) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 93720de60..8887a6eae 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -30,6 +30,7 @@ class FlowSystem: """ A FlowSystem organizes the high level Elements (Components & Effects). + Uses xr.Dataset directly from its Interface elements instead of TimeSeriesCollection. """ def __init__( @@ -47,13 +48,15 @@ def __init__( This is needed to calculate previous durations (for example consecutive_on_hours). If you use an array, take care that its long enough to cover all previous values! """ - self.time_series_collection = TimeSeriesCollection( - timesteps=timesteps, - hours_of_last_timestep=hours_of_last_timestep, - hours_of_previous_timesteps=hours_of_previous_timesteps, + # Store timing information directly + self.timesteps = self._validate_timesteps(timesteps) + self.timesteps_extra = self._create_timesteps_with_extra(timesteps, hours_of_last_timestep) + self.hours_per_timestep = self._calculate_hours_per_timestep(self.timesteps_extra) + self.hours_of_previous_timesteps = self._calculate_hours_of_previous_timesteps( + timesteps, hours_of_previous_timesteps ) - # defaults: + # Element collections self.components: Dict[str, Component] = {} self.buses: Dict[str, Bus] = {} self.effects: EffectCollection = EffectCollection() @@ -61,60 +64,373 @@ def __init__( self._connected = False + @staticmethod + def _validate_timesteps(timesteps: pd.DatetimeIndex) -> pd.DatetimeIndex: + """Validate timesteps format and rename if needed.""" + if not isinstance(timesteps, pd.DatetimeIndex): + raise TypeError('timesteps must be a pandas DatetimeIndex') + if len(timesteps) < 2: + raise ValueError('timesteps must contain at least 2 timestamps') + if timesteps.name != 'time': + timesteps.name = 'time' + if not timesteps.is_monotonic_increasing: + raise ValueError('timesteps must be sorted') + return timesteps + + @staticmethod + def _create_timesteps_with_extra( + timesteps: pd.DatetimeIndex, hours_of_last_timestep: Optional[float] + ) -> pd.DatetimeIndex: + """Create timesteps with an extra step at the end.""" + if hours_of_last_timestep is None: + hours_of_last_timestep = (timesteps[-1] - timesteps[-2]) / pd.Timedelta(hours=1) + + last_date = pd.DatetimeIndex([timesteps[-1] + pd.Timedelta(hours=hours_of_last_timestep)], name='time') + return pd.DatetimeIndex(timesteps.append(last_date), name='time') + + @staticmethod + def _calculate_hours_per_timestep(timesteps_extra: pd.DatetimeIndex) -> xr.DataArray: + """Calculate duration of each timestep.""" + hours_per_step = np.diff(timesteps_extra) / pd.Timedelta(hours=1) + return xr.DataArray( + hours_per_step, coords={'time': timesteps_extra[:-1]}, dims=['time'], name='hours_per_timestep' + ) + + @staticmethod + def _calculate_hours_of_previous_timesteps( + timesteps: pd.DatetimeIndex, hours_of_previous_timesteps: Optional[Union[float, np.ndarray]] + ) -> Union[float, np.ndarray]: + """Calculate duration of regular timesteps.""" + if hours_of_previous_timesteps is not None: + return hours_of_previous_timesteps + # Calculate from the first interval + first_interval = timesteps[1] - timesteps[0] + return first_interval.total_seconds() / 3600 # Convert to hours + + def _create_reference_structure(self) -> Tuple[Dict, Dict[str, xr.DataArray]]: + """ + Create reference structure for FlowSystem following the Interface pattern. + Extracts all DataArrays from components, buses, and effects. + + Returns: + Tuple of (reference_structure, extracted_arrays_dict) + """ + reference_structure = { + '__class__': self.__class__.__name__, + 'timesteps_extra': [date.isoformat() for date in self.timesteps_extra], + 'hours_of_previous_timesteps': self.hours_of_previous_timesteps, + } + + all_extracted_arrays = {} + + # Add timing arrays directly + all_extracted_arrays['hours_per_timestep'] = self.hours_per_timestep + + # Extract from components + components_structure = {} + for comp_label, component in self.components.items(): + comp_structure, comp_arrays = self._extract_from_interface(component) + all_extracted_arrays.update(comp_arrays) + components_structure[comp_label] = comp_structure + reference_structure['components'] = components_structure + + # Extract from buses + buses_structure = {} + for bus_label, bus in self.buses.items(): + bus_structure, bus_arrays = self._extract_from_interface(bus) + all_extracted_arrays.update(bus_arrays) + buses_structure[bus_label] = bus_structure + reference_structure['buses'] = buses_structure + + # Extract from effects + effects_structure = {} + for effect in self.effects: + effect_structure, effect_arrays = self._extract_from_interface(effect) + all_extracted_arrays.update(effect_arrays) + effects_structure[effect.label] = effect_structure + reference_structure['effects'] = effects_structure + + return reference_structure, all_extracted_arrays + + def _extract_from_interface(self, interface_obj) -> Tuple[Dict, Dict[str, xr.DataArray]]: + """Extract arrays from an Interface object using its reference system.""" + if hasattr(interface_obj, '_create_reference_structure'): + return interface_obj._create_reference_structure() + else: + # Fallback for objects that don't have the new Interface methods + logger.warning(f"Object {interface_obj} doesn't have _create_reference_structure method") + return interface_obj.to_dict(), {} + + @classmethod + def _resolve_reference_structure(cls, structure, arrays_dict: Dict[str, xr.DataArray]): + """ + Resolve reference structure back to actual objects. + Reuses the Interface pattern for consistency. + """ + if isinstance(structure, str) and structure.startswith(':::'): + # This is a reference to a DataArray + array_name = structure[3:] # Remove ":::" prefix + if array_name in arrays_dict: + return arrays_dict[array_name] + else: + logger.critical(f"Referenced DataArray '{array_name}' not found in dataset") + return None + + elif isinstance(structure, list): + resolved_list = [] + for item in structure: + resolved_item = cls._resolve_reference_structure(item, arrays_dict) + if resolved_item is not None: + resolved_list.append(resolved_item) + return resolved_list + + elif isinstance(structure, dict): + # Check if this is a serialized Interface object + if structure.get('__class__') and structure['__class__'] in CLASS_REGISTRY: + # This is a nested Interface object - restore it recursively + nested_class = CLASS_REGISTRY[structure['__class__']] + # Remove the __class__ key and process the rest + nested_data = {k: v for k, v in structure.items() if k != '__class__'} + # Resolve references in the nested data + resolved_nested_data = cls._resolve_reference_structure(nested_data, arrays_dict) + # Create the nested Interface object + return nested_class(**resolved_nested_data) + else: + # Regular dictionary - resolve references in values + resolved_dict = {} + for key, value in structure.items(): + resolved_value = cls._resolve_reference_structure(value, arrays_dict) + if resolved_value is not None or value is None: + resolved_dict[key] = resolved_value + return resolved_dict + + else: + return structure + + def to_dataset(self, constants_in_dataset: bool = True) -> xr.Dataset: + """ + Convert the FlowSystem to an xarray Dataset using the Interface pattern. + All DataArrays become dataset variables, structure goes to attrs. + + Args: + constants_in_dataset: If True, constants are included as Dataset variables. + + Returns: + xr.Dataset: Dataset containing all DataArrays with structure in attributes + """ + reference_structure, extracted_arrays = self._create_reference_structure() + + # Create the dataset with extracted arrays as variables and structure as attrs + ds = xr.Dataset(extracted_arrays, attrs=reference_structure) + return ds + + def as_dict(self, data_mode: Literal['data', 'name', 'stats'] = 'data') -> Dict: + """ + Convert the object to a dictionary representation. + Now builds on the reference structure for consistency. + """ + reference_structure, _ = self._create_reference_structure() + + if data_mode == 'data': + return reference_structure + elif data_mode == 'stats': + # For stats mode, we might want to process the structure further + return fx_io.remove_none_and_empty(reference_structure) + else: # name mode + return reference_structure + @classmethod - def from_dataset(cls, ds: xr.Dataset): - timesteps_extra = pd.DatetimeIndex(ds.attrs['timesteps_extra'], name='time') - hours_of_last_timestep = TimeSeriesCollection.calculate_hours_per_timestep(timesteps_extra).isel(time=-1).item() + def from_dataset(cls, ds: xr.Dataset) -> 'FlowSystem': + """ + Create a FlowSystem from an xarray Dataset using the Interface pattern. - flow_system = FlowSystem( + Args: + ds: Dataset containing the FlowSystem data + + Returns: + FlowSystem instance + """ + # Get the reference structure from attrs + reference_structure = dict(ds.attrs) + + # Extract FlowSystem constructor parameters + timesteps_extra = pd.DatetimeIndex(reference_structure['timesteps_extra'], name='time') + hours_of_previous_timesteps = reference_structure['hours_of_previous_timesteps'] + + # Calculate hours_of_last_timestep from the timesteps + hours_of_last_timestep = float((timesteps_extra[-1] - timesteps_extra[-2]) / pd.Timedelta(hours=1)) + + # Create FlowSystem instance + flow_system = cls( timesteps=timesteps_extra[:-1], hours_of_last_timestep=hours_of_last_timestep, - hours_of_previous_timesteps=ds.attrs['hours_of_previous_timesteps'], + hours_of_previous_timesteps=hours_of_previous_timesteps, ) - structure = fx_io.insert_dataarray({key: ds.attrs[key] for key in ['components', 'buses', 'effects']}, ds) - flow_system.add_elements( - *[Bus.from_dict(bus) for bus in structure['buses'].values()] - + [Effect.from_dict(effect) for effect in structure['effects'].values()] - + [CLASS_REGISTRY[comp['__class__']].from_dict(comp) for comp in structure['components'].values()] - ) + # Create arrays dictionary from dataset variables + arrays_dict = {name: array for name, array in ds.data_vars.items()} + + # Restore components + components_structure = reference_structure.get('components', {}) + for comp_label, comp_data in components_structure.items(): + component = cls._resolve_reference_structure(comp_data, arrays_dict) + if not isinstance(component, Component): + logger.critical(f'Restoring component {comp_label} failed.') + flow_system._add_components(component) + + # Restore buses + buses_structure = reference_structure.get('buses', {}) + for bus_label, bus_data in buses_structure.items(): + bus = cls._resolve_reference_structure(bus_data, arrays_dict) + if not isinstance(bus, Bus): + logger.critical(f'Restoring component {bus_label} failed.') + flow_system._add_buses(bus) + + # Restore effects + effects_structure = reference_structure.get('effects', {}) + for effect_label, effect_data in effects_structure.items(): + effect = cls._resolve_reference_structure(effect_data, arrays_dict) + + if not isinstance(effect, Effect): + logger.critical(f'Restoring component {effect_label} failed.') + flow_system._add_effects(effect) + return flow_system @classmethod def from_dict(cls, data: Dict) -> 'FlowSystem': """ - Load a FlowSystem from a dictionary. + Load a FlowSystem from a dictionary using the Interface pattern. Args: data: Dictionary containing the FlowSystem data. """ - timesteps_extra = pd.DatetimeIndex(data['timesteps_extra'], name='time') - hours_of_last_timestep = TimeSeriesCollection.calculate_hours_per_timestep(timesteps_extra).isel(time=-1).item() + # For dict format, resolve with empty arrays (references may not be used) + resolved_data = cls._resolve_reference_structure(data, {}) - flow_system = FlowSystem( + # Extract constructor parameters + timesteps_extra = pd.DatetimeIndex(resolved_data['timesteps_extra'], name='time') + hours_of_last_timestep = float((timesteps_extra[-1] - timesteps_extra[-2]) / pd.Timedelta(hours=1)) + + flow_system = cls( timesteps=timesteps_extra[:-1], hours_of_last_timestep=hours_of_last_timestep, - hours_of_previous_timesteps=data['hours_of_previous_timesteps'], + hours_of_previous_timesteps=resolved_data['hours_of_previous_timesteps'], ) - flow_system.add_elements(*[Bus.from_dict(bus) for bus in data['buses'].values()]) + # Add elements using resolved data + for bus_data in resolved_data.get('buses', {}).values(): + bus = Bus.from_dict(bus_data) + flow_system.add_elements(bus) - flow_system.add_elements(*[Effect.from_dict(effect) for effect in data['effects'].values()]) + for effect_data in resolved_data.get('effects', {}).values(): + effect = Effect.from_dict(effect_data) + flow_system.add_elements(effect) - flow_system.add_elements( - *[CLASS_REGISTRY[comp['__class__']].from_dict(comp) for comp in data['components'].values()] - ) + for comp_data in resolved_data.get('components', {}).values(): + component = CLASS_REGISTRY[comp_data['__class__']].from_dict(comp_data) + flow_system.add_elements(component) flow_system.transform_data() - return flow_system @classmethod - def from_netcdf(cls, path: Union[str, pathlib.Path]): + def from_netcdf(cls, path: Union[str, pathlib.Path]) -> 'FlowSystem': + """ + Load a FlowSystem from a netcdf file using the Interface pattern. + """ + ds = fx_io.load_dataset_from_netcdf(path) + return cls.from_dataset(ds) + + def to_netcdf(self, path: Union[str, pathlib.Path], compression: int = 0, constants_in_dataset: bool = True): + """ + Save the FlowSystem to a NetCDF file using the Interface pattern. + + Args: + path: The path to the netCDF file. + compression: The compression level to use when saving the file. + constants_in_dataset: If True, constants are included as Dataset variables. + """ + ds = self.to_dataset(constants_in_dataset=constants_in_dataset) + fx_io.save_dataset_to_netcdf(ds, path, compression=compression) + logger.info(f'Saved FlowSystem to {path}') + + def to_json(self, path: Union[str, pathlib.Path]): + """ + Save the flow system to a JSON file using the Interface pattern. + This is meant for documentation and comparison, not for reloading. + + Args: + path: The path to the JSON file. + """ + # Use the stats mode for JSON export (cleaner output) + data = get_compact_representation(self.as_dict('stats')) + with open(path, 'w', encoding='utf-8') as f: + json.dump(data, f, indent=4, ensure_ascii=False) + + def create_time_series( + self, + name: str, + data: Optional[Union[NumericData, TimeSeriesData, TimeSeries]], + needs_extra_timestep: bool = False, + ) -> Optional[TimeSeries]: + """ + Create a TimeSeries-like object (now just an xr.DataArray with proper coordinates). + This method is kept for API compatibility but simplified. + + Args: + name: Name of the time series + data: Data to convert + needs_extra_timestep: Whether to use timesteps_extra + + Returns: + xr.DataArray with proper time coordinates + """ + if data is None: + return None + + # Choose appropriate timesteps + target_timesteps = self.timesteps_extra if needs_extra_timestep else self.timesteps + + if isinstance(data, TimeSeries): + # Extract the data and rename + return data.selected_data.rename(name) + elif isinstance(data, TimeSeriesData): + # Convert TimeSeriesData to DataArray + from .core import DataConverter # Assuming this exists + + return DataConverter.as_dataarray(data.data, timesteps=target_timesteps).rename(name) + else: + # Convert other data types to DataArray + from .core import DataConverter # Assuming this exists + + return DataConverter.as_dataarray(data, timesteps=target_timesteps).rename(name) + + def create_effect_time_series( + self, + label_prefix: Optional[str], + effect_values: EffectValuesUser, + label_suffix: Optional[str] = None, + ) -> Optional[Dict[str, xr.DataArray]]: """ - Load a FlowSystem from a netcdf file + Transform EffectValues to effect DataArrays. + Simplified version that returns DataArrays directly. """ - return cls.from_dataset(fx_io.load_dataset_from_netcdf(path)) + effect_values_dict: Optional[EffectValuesDict] = self.effects.create_effect_values_dict(effect_values) + if effect_values_dict is None: + return None + + return { + effect: self.create_time_series('|'.join(filter(None, [label_prefix, effect, label_suffix])), value) + for effect, value in effect_values_dict.items() + } + + def transform_data(self): + """Transform data for all elements using the new simplified approach.""" + if not self._connected: + self._connect_network() + for element in self.all_elements.values(): + element.transform_data(self) def add_elements(self, *elements: Element) -> None: """ @@ -142,63 +458,11 @@ def add_elements(self, *elements: Element) -> None: f'Tried to add incompatible object to FlowSystem: {type(new_element)=}: {new_element=} ' ) - def to_json(self, path: Union[str, pathlib.Path]): - """ - Saves the flow system to a json file. - This not meant to be reloaded and recreate the object, - but rather used to document or compare the flow_system to others. - - Args: - path: The path to the json file. - """ - with open(path, 'w', encoding='utf-8') as f: - json.dump(self.as_dict('stats'), f, indent=4, ensure_ascii=False) - - def as_dict(self, data_mode: Literal['data', 'name', 'stats'] = 'data') -> Dict: - """Convert the object to a dictionary representation.""" - data = { - 'components': { - comp.label: comp.to_dict() - for comp in sorted(self.components.values(), key=lambda component: component.label.upper()) - }, - 'buses': { - bus.label: bus.to_dict() for bus in sorted(self.buses.values(), key=lambda bus: bus.label.upper()) - }, - 'effects': { - effect.label: effect.to_dict() - for effect in sorted(self.effects, key=lambda effect: effect.label.upper()) - }, - 'timesteps_extra': [date.isoformat() for date in self.time_series_collection.timesteps_extra], - 'hours_of_previous_timesteps': self.time_series_collection.hours_of_previous_timesteps, - } - if data_mode == 'data': - return fx_io.replace_timeseries(data, 'data') - elif data_mode == 'stats': - return fx_io.remove_none_and_empty(fx_io.replace_timeseries(data, data_mode)) - return fx_io.replace_timeseries(data, data_mode) - - def as_dataset(self, constants_in_dataset: bool = False) -> xr.Dataset: - """ - Convert the FlowSystem to a xarray Dataset. - - Args: - constants_in_dataset: If True, constants are included as Dataset variables. - """ - ds = self.time_series_collection.to_dataset(include_constants=constants_in_dataset) - ds.attrs = self.as_dict(data_mode='name') - return ds - - def to_netcdf(self, path: Union[str, pathlib.Path], compression: int = 0, constants_in_dataset: bool = True): - """ - Saves the FlowSystem to a netCDF file. - Args: - path: The path to the netCDF file. - compression: The compression level to use when saving the file. - constants_in_dataset: If True, constants are included as Dataset variables. - """ - ds = self.as_dataset(constants_in_dataset=constants_in_dataset) - fx_io.save_dataset_to_netcdf(ds, path, compression=compression) - logger.info(f'Saved FlowSystem to {path}') + def create_model(self) -> SystemModel: + if not self._connected: + raise RuntimeError('FlowSystem is not connected. Call FlowSystem.connect() first.') + self.model = SystemModel(self) + return self.model def plot_network( self, @@ -213,28 +477,6 @@ def plot_network( ) -> Optional['pyvis.network.Network']: """ Visualizes the network structure of a FlowSystem using PyVis, saving it as an interactive HTML file. - - Args: - path: Path to save the HTML visualization. - - `False`: Visualization is created but not saved. - - `str` or `Path`: Specifies file path (default: 'flow_system.html'). - controls: UI controls to add to the visualization. - - `True`: Enables all available controls. - - `List`: Specify controls, e.g., ['nodes', 'layout']. - - Options: 'nodes', 'edges', 'layout', 'interaction', 'manipulation', 'physics', 'selection', 'renderer'. - show: Whether to open the visualization in the web browser. - - Returns: - - Optional[pyvis.network.Network]: The `Network` instance representing the visualization, or `None` if `pyvis` is not installed. - - Examples: - >>> flow_system.plot_network() - >>> flow_system.plot_network(show=False) - >>> flow_system.plot_network(path='output/custom_network.html', controls=['nodes', 'layout']) - - Notes: - - This function requires `pyvis`. If not installed, the function prints a warning and returns `None`. - - Nodes are styled based on type (e.g., circles for buses, boxes for components) and annotated with node information. """ from . import plotting @@ -265,67 +507,6 @@ def network_infos(self) -> Tuple[Dict[str, Dict[str, str]], Dict[str, Dict[str, return nodes, edges - def transform_data(self): - if not self._connected: - self._connect_network() - for element in self.all_elements.values(): - element.transform_data(self) - - def create_time_series( - self, - name: str, - data: Optional[Union[NumericData, TimeSeriesData, TimeSeries]], - needs_extra_timestep: bool = False, - ) -> Optional[TimeSeries]: - """ - Tries to create a TimeSeries from NumericData Data and adds it to the time_series_collection - If the data already is a TimeSeries, nothing happens and the TimeSeries gets reset and returned - If the data is a TimeSeriesData, it is converted to a TimeSeries, and the aggregation weights are applied. - If the data is None, nothing happens. - """ - - if data is None: - return None - elif isinstance(data, TimeSeries): - data.restore_data() - if data in self.time_series_collection: - return data - return self.time_series_collection.create_time_series( - data=data.active_data, name=name, needs_extra_timestep=needs_extra_timestep - ) - return self.time_series_collection.create_time_series( - data=data, name=name, needs_extra_timestep=needs_extra_timestep - ) - - def create_effect_time_series( - self, - label_prefix: Optional[str], - effect_values: EffectValuesUser, - label_suffix: Optional[str] = None, - ) -> Optional[EffectTimeSeries]: - """ - Transform EffectValues to EffectTimeSeries. - Creates a TimeSeries for each key in the nested_values dictionary, using the value as the data. - - The resulting label of the TimeSeries is the label of the parent_element, - followed by the label of the Effect in the nested_values and the label_suffix. - If the key in the EffectValues is None, the alias 'Standard_Effect' is used - """ - effect_values: Optional[EffectValuesDict] = self.effects.create_effect_values_dict(effect_values) - if effect_values is None: - return None - - return { - effect: self.create_time_series('|'.join(filter(None, [label_prefix, effect, label_suffix])), value) - for effect, value in effect_values.items() - } - - def create_model(self) -> SystemModel: - if not self._connected: - raise RuntimeError('FlowSystem is not connected. Call FlowSystem.connect() first.') - self.model = SystemModel(self) - return self.model - def _check_if_element_is_unique(self, element: Element) -> None: """ checks if element or label of element already exists in list diff --git a/flixopt/structure.py b/flixopt/structure.py index 1d0f2324f..b9dbd889c 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -116,130 +116,341 @@ def transform_data(self, flow_system: 'FlowSystem'): """Transforms the data of the interface to match the FlowSystem's dimensions""" raise NotImplementedError('Every Interface needs a transform_data() method') + def _create_reference_structure(self) -> Tuple[Dict, Dict[str, xr.DataArray]]: + """ + Convert all DataArrays/TimeSeries to references and extract them. + This is the core method that both to_dict() and to_dataset() build upon. + + Returns: + Tuple of (reference_structure, extracted_arrays_dict) + """ + # Get constructor parameters + init_params = inspect.signature(self.__init__).parameters + + # Process all constructor parameters + reference_structure = {'__class__': self.__class__.__name__} + all_extracted_arrays = {} + + for name in init_params: + if name == 'self': + continue + + value = getattr(self, name, None) + if value is None: + continue + + # Extract arrays and get reference structure + processed_value, extracted_arrays = self._extract_dataarrays_recursive(value) + + # Add extracted arrays to the collection + all_extracted_arrays.update(extracted_arrays) + + # Only store in structure if it's not None/empty after processing + if processed_value is not None and not (isinstance(processed_value, (dict, list)) and not processed_value): + reference_structure[name] = processed_value + + return reference_structure, all_extracted_arrays + + def _extract_dataarrays_recursive(self, obj) -> Tuple[Any, Dict[str, xr.DataArray]]: + """ + Recursively extract DataArrays/TimeSeries from nested structures. + + Args: + obj: Object to process + + Returns: + Tuple of (processed_object_with_references, extracted_arrays_dict) + """ + extracted_arrays = {} + + # Handle TimeSeries objects - extract their data using their unique name + if isinstance(obj, TimeSeries): + data_array = obj.active_data.rename(obj.name) + extracted_arrays[obj.name] = data_array + return f':::{obj.name}', extracted_arrays + + # Handle DataArrays directly - use their unique name + elif isinstance(obj, xr.DataArray): + if not obj.name: + raise ValueError('DataArray must have a unique name for serialization') + extracted_arrays[obj.name] = obj + return f':::{obj.name}', extracted_arrays + + # Handle Interface objects - extract their DataArrays too + elif isinstance(obj, Interface): + # Get the Interface's reference structure and arrays + interface_structure, interface_arrays = obj._create_reference_structure() + + # Add all extracted arrays from the nested Interface + extracted_arrays.update(interface_arrays) + + return interface_structure, extracted_arrays + + # Handle lists + elif isinstance(obj, list): + processed_list = [] + for item in obj: + processed_item, nested_arrays = self._extract_dataarrays_recursive(item) + extracted_arrays.update(nested_arrays) + processed_list.append(processed_item) + return processed_list, extracted_arrays + + # Handle dictionaries + elif isinstance(obj, dict): + processed_dict = {} + for key, value in obj.items(): + processed_value, nested_arrays = self._extract_dataarrays_recursive(value) + extracted_arrays.update(nested_arrays) + processed_dict[key] = processed_value + return processed_dict, extracted_arrays + + # Handle tuples (convert to list for JSON compatibility) + elif isinstance(obj, tuple): + processed_list = [] + for item in obj: + processed_item, nested_arrays = self._extract_dataarrays_recursive(item) + extracted_arrays.update(nested_arrays) + processed_list.append(processed_item) + return processed_list, extracted_arrays + + # For all other types, serialize to basic types + else: + return self._serialize_to_basic_types(obj), extracted_arrays + + @classmethod + def _resolve_reference_structure(cls, structure, arrays_dict: Dict[str, xr.DataArray]): + """ + Convert reference structure back to actual objects using provided arrays. + + Args: + structure: Structure containing references (:::name) + arrays_dict: Dictionary of available DataArrays + + Returns: + Structure with references resolved to actual DataArrays + """ + if isinstance(structure, str) and structure.startswith(':::'): + # This is a reference to a DataArray + array_name = structure[3:] # Remove ":::" prefix + if array_name in arrays_dict: + return arrays_dict[array_name] + else: + logger.critical(f"Referenced DataArray '{array_name}' not found in dataset") + return None + + elif isinstance(structure, list): + resolved_list = [] + for item in structure: + resolved_item = cls._resolve_reference_structure(item, arrays_dict) + if resolved_item is not None: # Filter out None values from missing references + resolved_list.append(resolved_item) + return resolved_list + + elif isinstance(structure, dict): + # Check if this is a serialized Interface object + if structure.get('__class__') and structure['__class__'] in CLASS_REGISTRY: + # This is a nested Interface object - restore it recursively + nested_class = CLASS_REGISTRY[structure['__class__']] + # Remove the __class__ key and process the rest + nested_data = {k: v for k, v in structure.items() if k != '__class__'} + # Resolve references in the nested data + resolved_nested_data = cls._resolve_reference_structure(nested_data, arrays_dict) + # Create the nested Interface object + return nested_class(**resolved_nested_data) + else: + # Regular dictionary - resolve references in values + resolved_dict = {} + for key, value in structure.items(): + resolved_value = cls._resolve_reference_structure(value, arrays_dict) + if resolved_value is not None or value is None: # Keep None values if they were originally None + resolved_dict[key] = resolved_value + return resolved_dict + + else: + return structure + + def _serialize_to_basic_types(self, obj): + """Convert object to basic Python types only (no DataArrays, no custom objects).""" + if obj is None or isinstance(obj, (str, int, float, bool)): + return obj + elif isinstance(obj, np.integer): + return int(obj) + elif isinstance(obj, np.floating): + return float(obj) + elif isinstance(obj, (np.ndarray, pd.Series, pd.DataFrame)): + return obj.tolist() if hasattr(obj, 'tolist') else list(obj) + elif isinstance(obj, dict): + return {k: self._serialize_to_basic_types(v) for k, v in obj.items()} + elif isinstance(obj, (list, tuple)): + return [self._serialize_to_basic_types(item) for item in obj] + elif hasattr(obj, 'isoformat'): # datetime objects + return obj.isoformat() + else: + # For any other object, try to convert to string as fallback + logger.warning(f'Converting unknown type {type(obj)} to string: {obj}') + return str(obj) + + def to_dataset(self) -> xr.Dataset: + """ + Convert the object to an xarray Dataset representation. + All DataArrays and TimeSeries become dataset variables, everything else goes to attrs. + + Returns: + xr.Dataset: Dataset containing all DataArrays with basic objects only in attributes + """ + reference_structure, extracted_arrays = self._create_reference_structure() + + # Create the dataset with extracted arrays as variables and structure as attrs + ds = xr.Dataset(extracted_arrays, attrs=reference_structure) + return ds + + def to_dict(self) -> Dict: + """ + Convert the object to a dictionary representation. + DataArrays/TimeSeries are converted to references, but structure is preserved. + + Returns: + Dict: Dictionary with references to DataArrays/TimeSeries + """ + reference_structure, _ = self._create_reference_structure() + return reference_structure + def infos(self, use_numpy: bool = True, use_element_label: bool = False) -> Dict: """ Generate a dictionary representation of the object's constructor arguments. - Excludes default values and empty dictionaries and lists. - Converts data to be compatible with JSON. + Built on top of dataset creation for better consistency and analytics capabilities. Args: use_numpy: Whether to convert NumPy arrays to lists. Defaults to True. - If True, numeric numpy arrays (`np.ndarray`) are preserved as-is. + If True, numeric numpy arrays are preserved as-is. If False, they are converted to lists. - use_element_label: Whether to use the element label instead of the infos of the element. Defaults to False. - Note that Elements used as keys in dictionaries are always converted to their labels. + use_element_label: Whether to use element labels instead of full infos for nested objects. Returns: - A dictionary representation of the object's constructor arguments. - + A dictionary representation optimized for documentation and analysis. """ - # Get the constructor arguments and their default values - init_params = sorted( - inspect.signature(self.__init__).parameters.items(), - key=lambda x: (x[0].lower() != 'label', x[0].lower()), # Prioritize 'label' - ) - # Build a dict of attribute=value pairs, excluding defaults - details = {'class': ':'.join([cls.__name__ for cls in self.__class__.__mro__])} - for name, param in init_params: - if name == 'self': - continue - value, default = getattr(self, name, None), param.default - # Ignore default values and empty dicts and list - if np.all(value == default) or (isinstance(value, (dict, list)) and not value): - continue - details[name] = copy_and_convert_datatypes(value, use_numpy, use_element_label) - return details + # Get the core dataset representation + ds = self.to_dataset() + + # Start with the reference structure from attrs + info_dict = dict(ds.attrs) + + # Process DataArrays in the dataset based on preferences + for var_name, data_array in ds.data_vars.items(): + if use_numpy: + # Keep as DataArray/numpy for analysis + info_dict[f'_data_{var_name}'] = data_array + else: + # Convert to lists for JSON compatibility + info_dict[f'_data_{var_name}'] = data_array.values.tolist() + + # Apply element label preference to nested structures + if use_element_label: + info_dict = self._apply_element_label_preference(info_dict) + + return info_dict + + def _apply_element_label_preference(self, obj): + """Apply element label preference to nested structures.""" + if isinstance(obj, dict): + if obj.get('__class__') and 'label' in obj: + # This looks like an Interface with a label - return just the label + return obj.get('label', obj.get('__class__')) + else: + return {k: self._apply_element_label_preference(v) for k, v in obj.items()} + elif isinstance(obj, list): + return [self._apply_element_label_preference(item) for item in obj] + else: + return obj def to_json(self, path: Union[str, pathlib.Path]): """ - Saves the element to a json file. - This not meant to be reloaded and recreate the object, but rather used to document or compare the object. + Save the element to a JSON file for documentation purposes. + Uses the infos() method for consistent representation. Args: - path: The path to the json file. + path: The path to the JSON file. """ - data = get_compact_representation(self.infos(use_numpy=True, use_element_label=True)) + data = get_compact_representation(self.infos(use_numpy=False, use_element_label=True)) with open(path, 'w', encoding='utf-8') as f: json.dump(data, f, indent=4, ensure_ascii=False) - def to_dict(self) -> Dict: - """Convert the object to a dictionary representation.""" - data = {'__class__': self.__class__.__name__} + def to_netcdf(self, path: Union[str, pathlib.Path], compression: int = 0): + """ + Save the object to a NetCDF file. - # Get the constructor parameters - init_params = inspect.signature(self.__init__).parameters + Args: + path: Path to save the NetCDF file + compression: Compression level (0-9) + """ + from . import io as fx_io # Assuming fx_io is available - for name in init_params: - if name == 'self': - continue + ds = self.to_dataset() + fx_io.save_dataset_to_netcdf(ds, path, compression=compression) - value = getattr(self, name, None) - data[name] = self._serialize_value(value) + @classmethod + def from_dataset(cls, ds: xr.Dataset) -> 'Interface': + """ + Create an instance from an xarray Dataset. - return data + Args: + ds: Dataset containing the object data - def _serialize_value(self, value: Any): - """Helper method to serialize a value based on its type.""" - if value is None: - return None - elif isinstance(value, Interface): - return value.to_dict() - elif isinstance(value, (list, tuple)): - return self._serialize_list(value) - elif isinstance(value, dict): - return self._serialize_dict(value) - else: - return value + Returns: + Interface instance + """ + # Get class name and verify it matches + class_name = ds.attrs.get('__class__') + if class_name != cls.__name__: + logger.warning(f"Dataset class '{class_name}' doesn't match target class '{cls.__name__}'") - def _serialize_list(self, items): - """Serialize a list of items.""" - return [self._serialize_value(item) for item in items] + # Get the reference structure from attrs + reference_structure = dict(ds.attrs) - def _serialize_dict(self, d): - """Serialize a dictionary of items.""" - return {k: self._serialize_value(v) for k, v in d.items()} + # Remove the class name since it's not a constructor parameter + reference_structure.pop('__class__', None) - @classmethod - def _deserialize_dict(cls, data: Dict) -> Union[Dict, 'Interface']: - if '__class__' in data: - class_name = data.pop('__class__') - try: - class_type = CLASS_REGISTRY[class_name] - if issubclass(class_type, Interface): - # Use _deserialize_dict to process the arguments - processed_data = {k: cls._deserialize_value(v) for k, v in data.items()} - return class_type(**processed_data) - else: - raise ValueError(f'Class "{class_name}" is not an Interface.') - except (AttributeError, KeyError) as e: - raise ValueError(f'Class "{class_name}" could not get reconstructed.') from e - else: - return {k: cls._deserialize_value(v) for k, v in data.items()} + # Create arrays dictionary from dataset variables + arrays_dict = {name: array for name, array in ds.data_vars.items()} - @classmethod - def _deserialize_list(cls, data: List) -> List: - return [cls._deserialize_value(value) for value in data] + # Resolve all references using the centralized method + resolved_params = cls._resolve_reference_structure(reference_structure, arrays_dict) + + return cls(**resolved_params) @classmethod - def _deserialize_value(cls, value: Any): - """Helper method to deserialize a value based on its type.""" - if value is None: - return None - elif isinstance(value, dict): - return cls._deserialize_dict(value) - elif isinstance(value, list): - return cls._deserialize_list(value) - return value + def from_netcdf(cls, path: Union[str, pathlib.Path]) -> 'Interface': + """ + Load an instance from a NetCDF file. + + Args: + path: Path to the NetCDF file + + Returns: + Interface instance + """ + from . import io as fx_io # Assuming fx_io is available + + ds = fx_io.load_dataset_from_netcdf(path) + return cls.from_dataset(ds) @classmethod def from_dict(cls, data: Dict) -> 'Interface': """ Create an instance from a dictionary representation. + This is now a thin wrapper around the reference resolution system. Args: data: Dictionary containing the data for the object. """ - return cls._deserialize_dict(data) + class_name = data.pop('__class__', None) + if class_name and class_name != cls.__name__: + logger.warning(f"Dict class '{class_name}' doesn't match target class '{cls.__name__}'") + + # Since dict format doesn't separate arrays, resolve with empty arrays dict + # References in dict format would need to be handled differently if they exist + resolved_params = cls._resolve_reference_structure(data, {}) + return cls(**resolved_params) def __repr__(self): # Get the constructor arguments and their current values From f63db8b54004cc2a2618c20cb561dff299dc2ce3 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 23 Jun 2025 17:04:06 +0200 Subject: [PATCH 059/448] Remove TimeSeries --- examples/01_Simple/simple_example.py | 5 + flixopt/calculation.py | 3 - flixopt/components.py | 20 +- flixopt/core.py | 815 +-------------------------- flixopt/effects.py | 8 +- flixopt/elements.py | 14 +- flixopt/features.py | 5 - flixopt/flow_system.py | 2 +- flixopt/io.py | 2 +- flixopt/structure.py | 17 +- 10 files changed, 40 insertions(+), 851 deletions(-) diff --git a/examples/01_Simple/simple_example.py b/examples/01_Simple/simple_example.py index 45550c9cc..da10aed62 100644 --- a/examples/01_Simple/simple_example.py +++ b/examples/01_Simple/simple_example.py @@ -103,9 +103,14 @@ calculation = fx.FullCalculation(name='Sim1', flow_system=flow_system) calculation.do_modeling() # Translate the model to a solvable form, creating equations and Variables + calculation2 = fx.FullCalculation(name='Sim2', flow_system=flow_system) + calculation2.do_modeling() # Translate the model to a solvable form, creating equations and Variables + # --- Solve the Calculation and Save Results --- calculation.solve(fx.solvers.HighsSolver(mip_gap=0, time_limit_seconds=30)) + calculation2.solve(fx.solvers.HighsSolver(mip_gap=0, time_limit_seconds=30)) + # --- Analyze Results --- calculation.results['Fernwärme'].plot_node_balance_pie() calculation.results['Fernwärme'].plot_node_balance() diff --git a/flixopt/calculation.py b/flixopt/calculation.py index c7367cad2..2f08dd457 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -183,9 +183,6 @@ def solve(self, solver: _Solver, log_file: Optional[pathlib.Path] = None, log_ma def _activate_time_series(self): self.flow_system.transform_data() - self.flow_system.time_series_collection.activate_timesteps( - active_timesteps=self.active_timesteps, - ) class AggregatedCalculation(FullCalculation): diff --git a/flixopt/components.py b/flixopt/components.py index 1f5fe5ece..81baaeea5 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -348,7 +348,7 @@ def __init__(self, model: SystemModel, element: Transmission): def do_modeling(self): """Initiates all FlowModels""" # Force On Variable if absolute losses are present - if (self.element.absolute_losses is not None) and np.any(self.element.absolute_losses.active_data != 0): + if (self.element.absolute_losses is not None) and np.any(self.element.absolute_losses != 0): for flow in self.element.inputs + self.element.outputs: if flow.on_off_parameters is None: flow.on_off_parameters = OnOffParameters() @@ -385,14 +385,14 @@ def create_transmission_equation(self, name: str, in_flow: Flow, out_flow: Flow) # eq: out(t) + on(t)*loss_abs(t) = in(t)*(1 - loss_rel(t)) con_transmission = self.add( self._model.add_constraints( - out_flow.model.flow_rate == -in_flow.model.flow_rate * (self.element.relative_losses.active_data - 1), + out_flow.model.flow_rate == -in_flow.model.flow_rate * (self.element.relative_losses - 1), name=f'{self.label_full}|{name}', ), name, ) if self.element.absolute_losses is not None: - con_transmission.lhs += in_flow.model.on_off.on * self.element.absolute_losses.active_data + con_transmission.lhs += in_flow.model.on_off.on * self.element.absolute_losses return con_transmission @@ -420,8 +420,8 @@ def do_modeling(self): self.add( self._model.add_constraints( - sum([flow.model.flow_rate * conv_factors[flow.label].active_data for flow in used_inputs]) - == sum([flow.model.flow_rate * conv_factors[flow.label].active_data for flow in used_outputs]), + sum([flow.model.flow_rate * conv_factors[flow.label] for flow in used_inputs]) + == sum([flow.model.flow_rate * conv_factors[flow.label] for flow in used_outputs]), name=f'{self.label_full}|conversion_{i}', ) ) @@ -481,12 +481,12 @@ def do_modeling(self): ) charge_state = self.charge_state - rel_loss = self.element.relative_loss_per_hour.active_data + rel_loss = self.element.relative_loss_per_hour hours_per_step = self._model.hours_per_step charge_rate = self.element.charging.model.flow_rate discharge_rate = self.element.discharging.model.flow_rate - eff_charge = self.element.eta_charge.active_data - eff_discharge = self.element.eta_discharge.active_data + eff_charge = self.element.eta_charge + eff_discharge = self.element.eta_discharge self.add( self._model.add_constraints( @@ -572,8 +572,8 @@ def absolute_charge_state_bounds(self) -> Tuple[NumericData, NumericData]: @property def relative_charge_state_bounds(self) -> Tuple[NumericData, NumericData]: return ( - self.element.relative_minimum_charge_state.active_data, - self.element.relative_maximum_charge_state.active_data, + self.element.relative_minimum_charge_state, + self.element.relative_maximum_charge_state, ) diff --git a/flixopt/core.py b/flixopt/core.py index 08be18f1d..022bf8e6f 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -136,392 +136,8 @@ def __str__(self): class TimeSeries: - """ - A class representing time series data with active and stored states. - - TimeSeries provides a way to store time-indexed data and work with temporal subsets. - It supports arithmetic operations, aggregation, and JSON serialization. - - Attributes: - name (str): The name of the time series - aggregation_weight (Optional[float]): Weight used for aggregation - aggregation_group (Optional[str]): Group name for shared aggregation weighting - needs_extra_timestep (bool): Whether this series needs an extra timestep - """ - - @classmethod - def from_datasource( - cls, - data: NumericData, - name: str, - timesteps: pd.DatetimeIndex, - aggregation_weight: Optional[float] = None, - aggregation_group: Optional[str] = None, - needs_extra_timestep: bool = False, - ) -> 'TimeSeries': - """ - Initialize the TimeSeries from multiple data sources. - - Args: - data: The time series data - name: The name of the TimeSeries - timesteps: The timesteps of the TimeSeries - aggregation_weight: The weight in aggregation calculations - aggregation_group: Group this TimeSeries belongs to for aggregation weight sharing - needs_extra_timestep: Whether this series requires an extra timestep - - Returns: - A new TimeSeries instance - """ - return cls( - DataConverter.as_dataarray(data, timesteps), - name, - aggregation_weight, - aggregation_group, - needs_extra_timestep, - ) - - @classmethod - def from_json(cls, data: Optional[Dict[str, Any]] = None, path: Optional[str] = None) -> 'TimeSeries': - """ - Load a TimeSeries from a dictionary or json file. - - Args: - data: Dictionary containing TimeSeries data - path: Path to a JSON file containing TimeSeries data - - Returns: - A new TimeSeries instance - - Raises: - ValueError: If both path and data are provided or neither is provided - """ - if (path is None and data is None) or (path is not None and data is not None): - raise ValueError("Exactly one of 'path' or 'data' must be provided") - - if path is not None: - with open(path, 'r') as f: - data = json.load(f) - - # Convert ISO date strings to datetime objects - data['data']['coords']['time']['data'] = pd.to_datetime(data['data']['coords']['time']['data']) - - # Create the TimeSeries instance - return cls( - data=xr.DataArray.from_dict(data['data']), - name=data['name'], - aggregation_weight=data['aggregation_weight'], - aggregation_group=data['aggregation_group'], - needs_extra_timestep=data['needs_extra_timestep'], - ) - - def __init__( - self, - data: xr.DataArray, - name: str, - aggregation_weight: Optional[float] = None, - aggregation_group: Optional[str] = None, - needs_extra_timestep: bool = False, - ): - """ - Initialize a TimeSeries with a DataArray. - - Args: - data: The DataArray containing time series data - name: The name of the TimeSeries - aggregation_weight: The weight in aggregation calculations - aggregation_group: Group this TimeSeries belongs to for weight sharing - needs_extra_timestep: Whether this series requires an extra timestep - - Raises: - ValueError: If data doesn't have a 'time' index or has more than 1 dimension - """ - if 'time' not in data.indexes: - raise ValueError(f'DataArray must have a "time" index. Got {data.indexes}') - if data.ndim > 1: - raise ValueError(f'Number of dimensions of DataArray must be 1. Got {data.ndim}') - - self.name = name - self.aggregation_weight = aggregation_weight - self.aggregation_group = aggregation_group - self.needs_extra_timestep = needs_extra_timestep - - # Data management - self._stored_data = data.copy(deep=True) - self._backup = self._stored_data.copy(deep=True) - self._active_timesteps = self._stored_data.indexes['time'] - self._active_data = None - self._update_active_data() - - def reset(self): - """ - Reset active timesteps to the full set of stored timesteps. - """ - self.active_timesteps = None - - def restore_data(self): - """ - Restore stored_data from the backup and reset active timesteps. - """ - self._stored_data = self._backup.copy(deep=True) - self.reset() - - def to_json(self, path: Optional[pathlib.Path] = None) -> Dict[str, Any]: - """ - Save the TimeSeries to a dictionary or JSON file. - - Args: - path: Optional path to save JSON file - - Returns: - Dictionary representation of the TimeSeries - """ - data = { - 'name': self.name, - 'aggregation_weight': self.aggregation_weight, - 'aggregation_group': self.aggregation_group, - 'needs_extra_timestep': self.needs_extra_timestep, - 'data': self.active_data.to_dict(), - } - - # Convert datetime objects to ISO strings - data['data']['coords']['time']['data'] = [date.isoformat() for date in data['data']['coords']['time']['data']] - - # Save to file if path is provided - if path is not None: - indent = 4 if len(self.active_timesteps) <= 480 else None - with open(path, 'w', encoding='utf-8') as f: - json.dump(data, f, indent=indent, ensure_ascii=False) - - return data - - @property - def stats(self) -> str: - """ - Return a statistical summary of the active data. - - Returns: - String representation of data statistics - """ - return get_numeric_stats(self.active_data, padd=0) - - def _update_active_data(self): - """ - Update the active data based on active_timesteps. - """ - self._active_data = self._stored_data.sel(time=self.active_timesteps) - - @property - def all_equal(self) -> bool: - """Check if all values in the series are equal.""" - return np.unique(self.active_data.values).size == 1 - - @property - def active_timesteps(self) -> pd.DatetimeIndex: - """Get the current active timesteps.""" - return self._active_timesteps - - @active_timesteps.setter - def active_timesteps(self, timesteps: Optional[pd.DatetimeIndex]): - """ - Set active_timesteps and refresh active_data. - - Args: - timesteps: New timesteps to activate, or None to use all stored timesteps - - Raises: - TypeError: If timesteps is not a pandas DatetimeIndex or None - """ - if timesteps is None: - self._active_timesteps = self.stored_data.indexes['time'] - elif isinstance(timesteps, pd.DatetimeIndex): - self._active_timesteps = timesteps - else: - raise TypeError('active_timesteps must be a pandas DatetimeIndex or None') - - self._update_active_data() - - @property - def active_data(self) -> xr.DataArray: - """Get a view of stored_data based on active_timesteps.""" - return self._active_data - - @property - def stored_data(self) -> xr.DataArray: - """Get a copy of the full stored data.""" - return self._stored_data.copy() - - @stored_data.setter - def stored_data(self, value: NumericData): - """ - Update stored_data and refresh active_data. - - Args: - value: New data to store - """ - new_data = DataConverter.as_dataarray(value, timesteps=self.active_timesteps) - - # Skip if data is unchanged to avoid overwriting backup - if new_data.equals(self._stored_data): - return - - self._stored_data = new_data - self.active_timesteps = None # Reset to full timeline - - @property - def sel(self): - return self.active_data.sel - - @property - def isel(self): - return self.active_data.isel - - def _apply_operation(self, other, op): - """Apply an operation between this TimeSeries and another object.""" - if isinstance(other, TimeSeries): - other = other.active_data - return op(self.active_data, other) - - def __add__(self, other): - return self._apply_operation(other, lambda x, y: x + y) - - def __sub__(self, other): - return self._apply_operation(other, lambda x, y: x - y) - - def __mul__(self, other): - return self._apply_operation(other, lambda x, y: x * y) - - def __truediv__(self, other): - return self._apply_operation(other, lambda x, y: x / y) - - def __radd__(self, other): - return other + self.active_data - - def __rsub__(self, other): - return other - self.active_data - - def __rmul__(self, other): - return other * self.active_data - - def __rtruediv__(self, other): - return other / self.active_data - - def __neg__(self) -> xr.DataArray: - return -self.active_data - - def __pos__(self) -> xr.DataArray: - return +self.active_data - - def __abs__(self) -> xr.DataArray: - return abs(self.active_data) - - def __gt__(self, other): - """ - Compare if this TimeSeries is greater than another. - - Args: - other: Another TimeSeries to compare with - - Returns: - True if all values in this TimeSeries are greater than other - """ - if isinstance(other, TimeSeries): - return self.active_data > other.active_data - return self.active_data > other - - def __ge__(self, other): - """ - Compare if this TimeSeries is greater than or equal to another. - - Args: - other: Another TimeSeries to compare with - - Returns: - True if all values in this TimeSeries are greater than or equal to other - """ - if isinstance(other, TimeSeries): - return self.active_data >= other.active_data - return self.active_data >= other - - def __lt__(self, other): - """ - Compare if this TimeSeries is less than another. - - Args: - other: Another TimeSeries to compare with - - Returns: - True if all values in this TimeSeries are less than other - """ - if isinstance(other, TimeSeries): - return self.active_data < other.active_data - return self.active_data < other - - def __le__(self, other): - """ - Compare if this TimeSeries is less than or equal to another. - - Args: - other: Another TimeSeries to compare with - - Returns: - True if all values in this TimeSeries are less than or equal to other - """ - if isinstance(other, TimeSeries): - return self.active_data <= other.active_data - return self.active_data <= other - - def __eq__(self, other): - """ - Compare if this TimeSeries is equal to another. - - Args: - other: Another TimeSeries to compare with - - Returns: - True if all values in this TimeSeries are equal to other - """ - if isinstance(other, TimeSeries): - return self.active_data == other.active_data - return self.active_data == other - - def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): - """ - Handle NumPy universal functions. - - This allows NumPy functions to work with TimeSeries objects. - """ - # Convert any TimeSeries inputs to their active_data - inputs = [x.active_data if isinstance(x, TimeSeries) else x for x in inputs] - return getattr(ufunc, method)(*inputs, **kwargs) - - def __repr__(self): - """ - Get a string representation of the TimeSeries. - - Returns: - String showing TimeSeries details - """ - attrs = { - 'name': self.name, - 'aggregation_weight': self.aggregation_weight, - 'aggregation_group': self.aggregation_group, - 'needs_extra_timestep': self.needs_extra_timestep, - 'shape': self.active_data.shape, - 'time_range': f'{self.active_timesteps[0]} to {self.active_timesteps[-1]}', - } - attr_str = ', '.join(f'{k}={repr(v)}' for k, v in attrs.items()) - return f'TimeSeries({attr_str})' - - def __str__(self): - """ - Get a human-readable string representation. - - Returns: - Descriptive string with statistics - """ - return f"TimeSeries '{self.name}': {self.stats}" - + def __init__(self): + raise NotImplementedError('TimeSeries was removed') class TimeSeriesCollection: """ @@ -531,431 +147,8 @@ class TimeSeriesCollection: timesteps, provides operations on collections, and manages extra timesteps. """ - def __init__( - self, - timesteps: pd.DatetimeIndex, - hours_of_last_timestep: Optional[float] = None, - hours_of_previous_timesteps: Optional[Union[float, np.ndarray]] = None, - ): - """ - Args: - timesteps: The timesteps of the Collection. - hours_of_last_timestep: The duration of the last time step. Uses the last time interval if not specified - hours_of_previous_timesteps: The duration of previous timesteps. - If None, the first time increment of time_series is used. - This is needed to calculate previous durations (for example consecutive_on_hours). - If you use an array, take care that its long enough to cover all previous values! - """ - # Prepare and validate timesteps - self._validate_timesteps(timesteps) - self.hours_of_previous_timesteps = self._calculate_hours_of_previous_timesteps( - timesteps, hours_of_previous_timesteps - ) - - # Set up timesteps and hours - self.all_timesteps = timesteps - self.all_timesteps_extra = self._create_timesteps_with_extra(timesteps, hours_of_last_timestep) - self.all_hours_per_timestep = self.calculate_hours_per_timestep(self.all_timesteps_extra) - - # Active timestep tracking - self._active_timesteps = None - self._active_timesteps_extra = None - self._active_hours_per_timestep = None - - # Dictionary of time series by name - self.time_series_data: Dict[str, TimeSeries] = {} - - # Aggregation - self.group_weights: Dict[str, float] = {} - self.weights: Dict[str, float] = {} - - @classmethod - def with_uniform_timesteps( - cls, start_time: pd.Timestamp, periods: int, freq: str, hours_per_step: Optional[float] = None - ) -> 'TimeSeriesCollection': - """Create a collection with uniform timesteps.""" - timesteps = pd.date_range(start_time, periods=periods, freq=freq, name='time') - return cls(timesteps, hours_of_previous_timesteps=hours_per_step) - - def create_time_series( - self, data: Union[NumericData, TimeSeriesData], name: str, needs_extra_timestep: bool = False - ) -> TimeSeries: - """ - Creates a TimeSeries from the given data and adds it to the collection. - - Args: - data: The data to create the TimeSeries from. - name: The name of the TimeSeries. - needs_extra_timestep: Whether to create an additional timestep at the end of the timesteps. - The data to create the TimeSeries from. - - Returns: - The created TimeSeries. - - """ - # Check for duplicate name - if name in self.time_series_data: - raise ValueError(f"TimeSeries '{name}' already exists in this collection") - - # Determine which timesteps to use - timesteps_to_use = self.timesteps_extra if needs_extra_timestep else self.timesteps - - # Create the time series - if isinstance(data, TimeSeriesData): - time_series = TimeSeries.from_datasource( - name=name, - data=data.data, - timesteps=timesteps_to_use, - aggregation_weight=data.agg_weight, - aggregation_group=data.agg_group, - needs_extra_timestep=needs_extra_timestep, - ) - # Connect the user time series to the created TimeSeries - data.label = name - else: - time_series = TimeSeries.from_datasource( - name=name, data=data, timesteps=timesteps_to_use, needs_extra_timestep=needs_extra_timestep - ) - - # Add to the collection - self.add_time_series(time_series) - - return time_series - - def calculate_aggregation_weights(self) -> Dict[str, float]: - """Calculate and return aggregation weights for all time series.""" - self.group_weights = self._calculate_group_weights() - self.weights = self._calculate_weights() - - if np.all(np.isclose(list(self.weights.values()), 1, atol=1e-6)): - logger.info('All Aggregation weights were set to 1') - - return self.weights - - def activate_timesteps(self, active_timesteps: Optional[pd.DatetimeIndex] = None): - """ - Update active timesteps for the collection and all time series. - If no arguments are provided, the active timesteps are reset. - - Args: - active_timesteps: The active timesteps of the model. - If None, the all timesteps of the TimeSeriesCollection are taken. - """ - if active_timesteps is None: - return self.reset() - - if not np.all(np.isin(active_timesteps, self.all_timesteps)): - raise ValueError('active_timesteps must be a subset of the timesteps of the TimeSeriesCollection') - - # Calculate derived timesteps - self._active_timesteps = active_timesteps - first_ts_index = np.where(self.all_timesteps == active_timesteps[0])[0][0] - last_ts_idx = np.where(self.all_timesteps == active_timesteps[-1])[0][0] - self._active_timesteps_extra = self.all_timesteps_extra[first_ts_index : last_ts_idx + 2] - self._active_hours_per_timestep = self.all_hours_per_timestep.isel(time=slice(first_ts_index, last_ts_idx + 1)) - - # Update all time series - self._update_time_series_timesteps() - - def reset(self): - """Reset active timesteps to defaults for all time series.""" - self._active_timesteps = None - self._active_timesteps_extra = None - self._active_hours_per_timestep = None - - for time_series in self.time_series_data.values(): - time_series.reset() - - def restore_data(self): - """Restore original data for all time series.""" - for time_series in self.time_series_data.values(): - time_series.restore_data() - - def add_time_series(self, time_series: TimeSeries): - """Add an existing TimeSeries to the collection.""" - if time_series.name in self.time_series_data: - raise ValueError(f"TimeSeries '{time_series.name}' already exists in this collection") - - self.time_series_data[time_series.name] = time_series - - def insert_new_data(self, data: pd.DataFrame, include_extra_timestep: bool = False): - """ - Update time series with new data from a DataFrame. - - Args: - data: DataFrame containing new data with timestamps as index - include_extra_timestep: Whether the provided data already includes the extra timestep, by default False - """ - if not isinstance(data, pd.DataFrame): - raise TypeError(f'data must be a pandas DataFrame, got {type(data).__name__}') - - # Check if the DataFrame index matches the expected timesteps - expected_timesteps = self.timesteps_extra if include_extra_timestep else self.timesteps - if not data.index.equals(expected_timesteps): - raise ValueError( - f'DataFrame index must match {"collection timesteps with extra timestep" if include_extra_timestep else "collection timesteps"}' - ) - - for name, ts in self.time_series_data.items(): - if name in data.columns: - if not ts.needs_extra_timestep: - # For time series without extra timestep - if include_extra_timestep: - # If data includes extra timestep but series doesn't need it, exclude the last point - ts.stored_data = data[name].iloc[:-1] - else: - # Use data as is - ts.stored_data = data[name] - else: - # For time series with extra timestep - if include_extra_timestep: - # Data already includes extra timestep - ts.stored_data = data[name] - else: - # Need to add extra timestep - extrapolate from the last value - extra_step_value = data[name].iloc[-1] - extra_step_index = pd.DatetimeIndex([self.timesteps_extra[-1]], name='time') - extra_step_series = pd.Series([extra_step_value], index=extra_step_index) - - # Combine the regular data with the extra timestep - ts.stored_data = pd.concat([data[name], extra_step_series]) - - logger.debug(f'Updated data for {name}') - - def to_dataframe( - self, filtered: Literal['all', 'constant', 'non_constant'] = 'non_constant', include_extra_timestep: bool = True - ) -> pd.DataFrame: - """ - Convert collection to DataFrame with optional filtering and timestep control. - - Args: - filtered: Filter time series by variability, by default 'non_constant' - include_extra_timestep: Whether to include the extra timestep in the result, by default True - - Returns: - DataFrame representation of the collection - """ - include_constants = filtered != 'non_constant' - ds = self.to_dataset(include_constants=include_constants) - - if not include_extra_timestep: - ds = ds.isel(time=slice(None, -1)) - - df = ds.to_dataframe() - - # Apply filtering - if filtered == 'all': - return df - elif filtered == 'constant': - return df.loc[:, df.nunique() == 1] - elif filtered == 'non_constant': - return df.loc[:, df.nunique() > 1] - else: - raise ValueError("filtered must be one of: 'all', 'constant', 'non_constant'") - - def to_dataset(self, include_constants: bool = True) -> xr.Dataset: - """ - Combine all time series into a single Dataset with all timesteps. - - Args: - include_constants: Whether to include time series with constant values, by default True - - Returns: - Dataset containing all selected time series with all timesteps - """ - # Determine which series to include - if include_constants: - series_to_include = self.time_series_data.values() - else: - series_to_include = self.non_constants - - # Create individual datasets and merge them - ds = xr.merge([ts.active_data.to_dataset(name=ts.name) for ts in series_to_include]) - - # Ensure the correct time coordinates - ds = ds.reindex(time=self.timesteps_extra) - - ds.attrs.update( - { - 'timesteps_extra': f'{self.timesteps_extra[0]} ... {self.timesteps_extra[-1]} | len={len(self.timesteps_extra)}', - 'hours_per_timestep': self._format_stats(self.hours_per_timestep), - } - ) - - return ds - - def _update_time_series_timesteps(self): - """Update active timesteps for all time series.""" - for ts in self.time_series_data.values(): - if ts.needs_extra_timestep: - ts.active_timesteps = self.timesteps_extra - else: - ts.active_timesteps = self.timesteps - - @staticmethod - def _validate_timesteps(timesteps: pd.DatetimeIndex): - """Validate timesteps format and rename if needed.""" - if not isinstance(timesteps, pd.DatetimeIndex): - raise TypeError('timesteps must be a pandas DatetimeIndex') - - if len(timesteps) < 2: - raise ValueError('timesteps must contain at least 2 timestamps') - - # Ensure timesteps has the required name - if timesteps.name != 'time': - logger.warning('Renamed timesteps to "time" (was "%s")', timesteps.name) - timesteps.name = 'time' - - @staticmethod - def _create_timesteps_with_extra( - timesteps: pd.DatetimeIndex, hours_of_last_timestep: Optional[float] - ) -> pd.DatetimeIndex: - """Create timesteps with an extra step at the end.""" - if hours_of_last_timestep is not None: - # Create the extra timestep using the specified duration - last_date = pd.DatetimeIndex([timesteps[-1] + pd.Timedelta(hours=hours_of_last_timestep)], name='time') - else: - # Use the last interval as the extra timestep duration - last_date = pd.DatetimeIndex([timesteps[-1] + (timesteps[-1] - timesteps[-2])], name='time') - - # Combine with original timesteps - return pd.DatetimeIndex(timesteps.append(last_date), name='time') - - @staticmethod - def _calculate_hours_of_previous_timesteps( - timesteps: pd.DatetimeIndex, hours_of_previous_timesteps: Optional[Union[float, np.ndarray]] - ) -> Union[float, np.ndarray]: - """Calculate duration of regular timesteps.""" - if hours_of_previous_timesteps is not None: - return hours_of_previous_timesteps - - # Calculate from the first interval - first_interval = timesteps[1] - timesteps[0] - return first_interval.total_seconds() / 3600 # Convert to hours - - @staticmethod - def calculate_hours_per_timestep(timesteps_extra: pd.DatetimeIndex) -> xr.DataArray: - """Calculate duration of each timestep.""" - # Calculate differences between consecutive timestamps - hours_per_step = np.diff(timesteps_extra) / pd.Timedelta(hours=1) - - return xr.DataArray( - data=hours_per_step, coords={'time': timesteps_extra[:-1]}, dims=('time',), name='hours_per_step' - ) - - def _calculate_group_weights(self) -> Dict[str, float]: - """Calculate weights for aggregation groups.""" - # Count series in each group - groups = [ts.aggregation_group for ts in self.time_series_data.values() if ts.aggregation_group is not None] - group_counts = Counter(groups) - - # Calculate weight for each group (1/count) - return {group: 1 / count for group, count in group_counts.items()} - - def _calculate_weights(self) -> Dict[str, float]: - """Calculate weights for all time series.""" - # Calculate weight for each time series - weights = {} - for name, ts in self.time_series_data.items(): - if ts.aggregation_group is not None: - # Use group weight - weights[name] = self.group_weights.get(ts.aggregation_group, 1) - else: - # Use individual weight or default to 1 - weights[name] = ts.aggregation_weight or 1 - - return weights - - def _format_stats(self, data) -> str: - """Format statistics for a data array.""" - if hasattr(data, 'values'): - values = data.values - else: - values = np.asarray(data) - - mean_val = np.mean(values) - min_val = np.min(values) - max_val = np.max(values) - - return f'mean: {mean_val:.2f}, min: {min_val:.2f}, max: {max_val:.2f}' - - def __getitem__(self, name: str) -> TimeSeries: - """Get a TimeSeries by name.""" - try: - return self.time_series_data[name] - except KeyError as e: - raise KeyError(f'TimeSeries "{name}" not found in the TimeSeriesCollection') from e - - def __iter__(self) -> Iterator[TimeSeries]: - """Iterate through all TimeSeries in the collection.""" - return iter(self.time_series_data.values()) - - def __len__(self) -> int: - """Get the number of TimeSeries in the collection.""" - return len(self.time_series_data) - - def __contains__(self, item: Union[str, TimeSeries]) -> bool: - """Check if a TimeSeries exists in the collection.""" - if isinstance(item, str): - return item in self.time_series_data - elif isinstance(item, TimeSeries): - return any([item is ts for ts in self.time_series_data.values()]) - return False - - @property - def non_constants(self) -> List[TimeSeries]: - """Get time series with varying values.""" - return [ts for ts in self.time_series_data.values() if not ts.all_equal] - - @property - def constants(self) -> List[TimeSeries]: - """Get time series with constant values.""" - return [ts for ts in self.time_series_data.values() if ts.all_equal] - - @property - def timesteps(self) -> pd.DatetimeIndex: - """Get the active timesteps.""" - return self.all_timesteps if self._active_timesteps is None else self._active_timesteps - - @property - def timesteps_extra(self) -> pd.DatetimeIndex: - """Get the active timesteps with extra step.""" - return self.all_timesteps_extra if self._active_timesteps_extra is None else self._active_timesteps_extra - - @property - def hours_per_timestep(self) -> xr.DataArray: - """Get the duration of each active timestep.""" - return ( - self.all_hours_per_timestep if self._active_hours_per_timestep is None else self._active_hours_per_timestep - ) - - @property - def hours_of_last_timestep(self) -> float: - """Get the duration of the last timestep.""" - return float(self.hours_per_timestep[-1].item()) - - def __repr__(self): - return f'TimeSeriesCollection:\n{self.to_dataset()}' - - def __str__(self): - longest_name = max([time_series.name for time_series in self.time_series_data], key=len) - - stats_summary = '\n'.join( - [ - f' - {time_series.name:<{len(longest_name)}}: {get_numeric_stats(time_series.active_data)}' - for time_series in self.time_series_data - ] - ) - - return ( - f'TimeSeriesCollection with {len(self.time_series_data)} series\n' - f' Time Range: {self.timesteps[0]} → {self.timesteps[-1]}\n' - f' No. of timesteps: {len(self.timesteps)} + 1 extra\n' - f' Hours per timestep: {get_numeric_stats(self.hours_per_timestep)}\n' - f' Time Series Data:\n' - f'{stats_summary}' - ) - + def __init__(self): + raise NotImplementedError('TimeSeriesCollection was removed') def get_numeric_stats(data: xr.DataArray, decimals: int = 2, padd: int = 10) -> str: """Calculates the mean, median, min, max, and standard deviation of a numeric DataArray.""" diff --git a/flixopt/effects.py b/flixopt/effects.py index 82aa63a43..b043f4492 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -13,7 +13,7 @@ import numpy as np import pandas as pd -from .core import NumericData, NumericDataTS, Scalar, TimeSeries, TimeSeriesCollection +from .core import NumericData, NumericDataTS, Scalar, TimeSeriesCollection, TimeSeries from .features import ShareAllocationModel from .structure import Element, ElementModel, Interface, Model, SystemModel, register_class_for_io @@ -137,10 +137,10 @@ def __init__(self, model: SystemModel, element: Effect): label_full=f'{self.label_full}(operation)', total_max=self.element.maximum_operation, total_min=self.element.minimum_operation, - min_per_hour=self.element.minimum_operation_per_hour.active_data + min_per_hour=self.element.minimum_operation_per_hour if self.element.minimum_operation_per_hour is not None else None, - max_per_hour=self.element.maximum_operation_per_hour.active_data + max_per_hour=self.element.maximum_operation_per_hour if self.element.maximum_operation_per_hour is not None else None, ) @@ -376,7 +376,7 @@ def _add_share_between_effects(self): for target_effect, time_series in origin_effect.specific_share_to_other_effects_operation.items(): self.effects[target_effect].model.operation.add_share( origin_effect.model.operation.label_full, - origin_effect.model.operation.total_per_timestep * time_series.active_data, + origin_effect.model.operation.total_per_timestep * time_series, ) # 2. invest: -> hier ist es Scalar (share) for target_effect, factor in origin_effect.specific_share_to_other_effects_invest.items(): diff --git a/flixopt/elements.py b/flixopt/elements.py index a0bd8c91f..3ea29a09f 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -287,7 +287,7 @@ def _plausibility_checks(self) -> None: if (self.relative_minimum > 0).any() and self.on_off_parameters is None: logger.warning( - f'Flow {self.label} has a relative_minimum of {self.relative_minimum.active_data} and no on_off_parameters. ' + f'Flow {self.label} has a relative_minimum of {self.relative_minimum} and no on_off_parameters. ' f'This prevents the flow_rate from switching off (flow_rate = 0). ' f'Consider using on_off_parameters to allow the flow to be switched on and off.' ) @@ -390,7 +390,7 @@ def _create_shares(self): self._model.effects.add_share_to_effects( name=self.label_full, # Use the full label of the element expressions={ - effect: self.flow_rate * self._model.hours_per_step * factor.active_data + effect: self.flow_rate * self._model.hours_per_step * factor for effect, factor in self.element.effects_per_flow_hour.items() }, target='operation', @@ -443,16 +443,16 @@ def flow_rate_lower_bound_relative(self) -> NumericData: """Returns the lower bound of the flow_rate relative to its size""" fixed_profile = self.element.fixed_relative_profile if fixed_profile is None: - return self.element.relative_minimum.active_data - return fixed_profile.active_data + return self.element.relative_minimum + return fixed_profile @property def flow_rate_upper_bound_relative(self) -> NumericData: """ Returns the upper bound of the flow_rate relative to its size""" fixed_profile = self.element.fixed_relative_profile if fixed_profile is None: - return self.element.relative_maximum.active_data - return fixed_profile.active_data + return self.element.relative_maximum + return fixed_profile @property def flow_rate_lower_bound(self) -> NumericData: @@ -497,7 +497,7 @@ def do_modeling(self) -> None: # Fehlerplus/-minus: if self.element.with_excess: excess_penalty = np.multiply( - self._model.hours_per_step, self.element.excess_penalty_per_flow_hour.active_data + self._model.hours_per_step, self.element.excess_penalty_per_flow_hour ) self.excess_input = self.add( self._model.add_variables(lower=0, coords=self._model.coords, name=f'{self.label_full}|excess_input'), diff --git a/flixopt/features.py b/flixopt/features.py index c2a62adb1..dc719a2a6 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -474,11 +474,6 @@ def __init__( self._minimum_duration = minimum_duration self._maximum_duration = maximum_duration - if isinstance(self._minimum_duration, TimeSeries): - self._minimum_duration = self._minimum_duration.active_data - if isinstance(self._maximum_duration, TimeSeries): - self._maximum_duration = self._maximum_duration.active_data - self.duration = None def do_modeling(self): diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 8887a6eae..ae9df6407 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -16,7 +16,7 @@ from rich.pretty import Pretty from . import io as fx_io -from .core import NumericData, NumericDataTS, TimeSeries, TimeSeriesCollection, TimeSeriesData +from .core import NumericData, NumericDataTS, TimeSeriesCollection, TimeSeriesData, TimeSeries from .effects import Effect, EffectCollection, EffectTimeSeries, EffectValuesDict, EffectValuesUser from .elements import Bus, Component, Flow from .structure import CLASS_REGISTRY, Element, SystemModel, get_compact_representation, get_str_representation diff --git a/flixopt/io.py b/flixopt/io.py index 35d927136..1376cafae 100644 --- a/flixopt/io.py +++ b/flixopt/io.py @@ -23,7 +23,7 @@ def replace_timeseries(obj, mode: Literal['name', 'stats', 'data'] = 'name'): return [replace_timeseries(v, mode) for v in obj] elif isinstance(obj, TimeSeries): # Adjust this based on the actual class if obj.all_equal: - return obj.active_data.values[0].item() + return obj.values[0].item() elif mode == 'name': return f'::::{obj.name}' elif mode == 'stats': diff --git a/flixopt/structure.py b/flixopt/structure.py index b9dbd889c..71efe31df 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -19,7 +19,7 @@ from rich.pretty import Pretty from .config import CONFIG -from .core import NumericData, Scalar, TimeSeries, TimeSeriesCollection, TimeSeriesData +from .core import NumericData, Scalar, TimeSeriesCollection, TimeSeriesData, TimeSeries if TYPE_CHECKING: # for type checking and preventing circular imports from .effects import EffectCollectionModel @@ -56,7 +56,6 @@ def __init__(self, flow_system: 'FlowSystem'): """ super().__init__(force_dim_names=True) self.flow_system = flow_system - self.time_series_collection = flow_system.time_series_collection self.effects: Optional[EffectCollectionModel] = None def do_modeling(self): @@ -88,23 +87,23 @@ def solution(self): for effect in sorted(self.flow_system.effects, key=lambda effect: effect.label_full.upper()) }, } - return solution.reindex(time=self.time_series_collection.timesteps_extra) + return solution.reindex(time=self.flow_system.timesteps_extra) @property def hours_per_step(self): - return self.time_series_collection.hours_per_timestep + return self.flow_system.hours_per_timestep @property def hours_of_previous_timesteps(self): - return self.time_series_collection.hours_of_previous_timesteps + return self.flow_system.hours_of_previous_timesteps @property def coords(self) -> Tuple[pd.DatetimeIndex]: - return (self.time_series_collection.timesteps,) + return (self.flow_system.timesteps,) @property def coords_extra(self) -> Tuple[pd.DatetimeIndex]: - return (self.time_series_collection.timesteps_extra,) + return (self.flow_system.timesteps_extra,) class Interface: @@ -165,7 +164,7 @@ def _extract_dataarrays_recursive(self, obj) -> Tuple[Any, Dict[str, xr.DataArra # Handle TimeSeries objects - extract their data using their unique name if isinstance(obj, TimeSeries): - data_array = obj.active_data.rename(obj.name) + data_array = obj.rename(obj.name) extracted_arrays[obj.name] = data_array return f':::{obj.name}', extracted_arrays @@ -745,7 +744,7 @@ def copy_and_convert_datatypes(data: Any, use_numpy: bool = True, use_element_la return copy_and_convert_datatypes(data.tolist(), use_numpy, use_element_label) elif isinstance(data, TimeSeries): - return copy_and_convert_datatypes(data.active_data, use_numpy, use_element_label) + return copy_and_convert_datatypes(data, use_numpy, use_element_label) elif isinstance(data, TimeSeriesData): return copy_and_convert_datatypes(data.data, use_numpy, use_element_label) From 167fb2ca59dc6f9ae157e64e990f8a31fba6bdc8 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 23 Jun 2025 17:05:21 +0200 Subject: [PATCH 060/448] Remove TimeSeries --- flixopt/calculation.py | 20 ++++++------ tests/conftest.py | 4 +-- tests/test_bus.py | 2 +- tests/test_component.py | 4 +-- tests/test_effect.py | 4 +-- tests/test_flow.py | 36 +++++++++++----------- tests/test_linear_converter.py | 8 ++--- tests/test_storage.py | 8 ++--- tests/test_timeseries.py | 56 +++++++++++++++++----------------- 9 files changed, 71 insertions(+), 71 deletions(-) diff --git a/flixopt/calculation.py b/flixopt/calculation.py index 2f08dd457..8439142c1 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -119,7 +119,7 @@ def main_results(self) -> Dict[str, Union[Scalar, Dict]]: def summary(self): return { 'Name': self.name, - 'Number of timesteps': len(self.flow_system.time_series_collection.timesteps), + 'Number of timesteps': len(self.flow_system.timesteps), 'Calculation Type': self.__class__.__name__, 'Constraints': self.model.constraints.ncons, 'Variables': self.model.variables.nvars, @@ -242,8 +242,8 @@ def _perform_aggregation(self): # Validation dt_min, dt_max = ( - np.min(self.flow_system.time_series_collection.hours_per_timestep), - np.max(self.flow_system.time_series_collection.hours_per_timestep), + np.min(self.flow_system.hours_per_timestep), + np.max(self.flow_system.hours_per_timestep), ) if not dt_min == dt_max: raise ValueError( @@ -252,11 +252,11 @@ def _perform_aggregation(self): ) steps_per_period = ( self.aggregation_parameters.hours_per_period - / self.flow_system.time_series_collection.hours_per_timestep.max() + / self.flow_system.hours_per_timestep.max() ) is_integer = ( self.aggregation_parameters.hours_per_period - % self.flow_system.time_series_collection.hours_per_timestep.max() + % self.flow_system.hours_per_timestep.max() ).item() == 0 if not (steps_per_period.size == 1 and is_integer): raise ValueError( @@ -269,13 +269,13 @@ def _perform_aggregation(self): # Aggregation - creation of aggregated timeseries: self.aggregation = Aggregation( - original_data=self.flow_system.time_series_collection.to_dataframe( + original_data=self.flow_system.to_dataframe( include_extra_timestep=False ), # Exclude last row (NaN) hours_per_time_step=float(dt_min), hours_per_period=self.aggregation_parameters.hours_per_period, nr_of_periods=self.aggregation_parameters.nr_of_periods, - weights=self.flow_system.time_series_collection.calculate_aggregation_weights(), + weights=self.flow_system.calculate_aggregation_weights(), time_series_for_high_peaks=self.aggregation_parameters.labels_for_high_peaks, time_series_for_low_peaks=self.aggregation_parameters.labels_for_low_peaks, ) @@ -283,7 +283,7 @@ def _perform_aggregation(self): self.aggregation.cluster() self.aggregation.plot(show=True, save=self.folder / 'aggregation.html') if self.aggregation_parameters.aggregate_data_and_fix_non_binary_vars: - self.flow_system.time_series_collection.insert_new_data( + self.flow_system.insert_new_data( self.aggregation.aggregated_data, include_extra_timestep=False ) self.durations['aggregation'] = round(timeit.default_timer() - t_start_agg, 2) @@ -324,8 +324,8 @@ def __init__( self.nr_of_previous_values = nr_of_previous_values self.sub_calculations: List[FullCalculation] = [] - self.all_timesteps = self.flow_system.time_series_collection.all_timesteps - self.all_timesteps_extra = self.flow_system.time_series_collection.all_timesteps_extra + self.all_timesteps = self.flow_system.all_timesteps + self.all_timesteps_extra = self.flow_system.all_timesteps_extra self.segment_names = [ f'Segment_{i + 1}' for i in range(math.ceil(len(self.all_timesteps) / self.timesteps_per_segment)) diff --git a/tests/conftest.py b/tests/conftest.py index 5399be72a..43f9f8bae 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -293,8 +293,8 @@ def flow_system_segments_of_flows_2(flow_system_complex) -> fx.FlowSystem: { 'P_el': fx.Piecewise( [ - fx.Piece(np.linspace(5, 6, len(flow_system.time_series_collection.timesteps)), 30), - fx.Piece(40, np.linspace(60, 70, len(flow_system.time_series_collection.timesteps))), + fx.Piece(np.linspace(5, 6, len(flow_system.timesteps)), 30), + fx.Piece(40, np.linspace(60, 70, len(flow_system.timesteps))), ] ), 'Q_th': fx.Piecewise([fx.Piece(6, 35), fx.Piece(45, 100)]), diff --git a/tests/test_bus.py b/tests/test_bus.py index 4a41a9f9e..136f9d2cc 100644 --- a/tests/test_bus.py +++ b/tests/test_bus.py @@ -31,7 +31,7 @@ def test_bus(self, basic_flow_system_linopy): def test_bus_penalty(self, basic_flow_system_linopy): """Test that flow model constraints are correctly generated.""" flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + timesteps = flow_system.timesteps bus = fx.Bus('TestBus') flow_system.add_elements(bus, fx.Sink('WärmelastTest', sink=fx.Flow('Q_th_Last', 'TestBus')), diff --git a/tests/test_component.py b/tests/test_component.py index d87a28c29..18ceb717a 100644 --- a/tests/test_component.py +++ b/tests/test_component.py @@ -57,7 +57,7 @@ def test_component(self, basic_flow_system_linopy): def test_on_with_multiple_flows(self, basic_flow_system_linopy): """Test that flow model constraints are correctly generated.""" flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + timesteps = flow_system.timesteps ub_out2 = np.linspace(1, 1.5, 10).round(2) inputs = [ fx.Flow('In1', 'Fernwärme', relative_minimum=np.ones(10) * 0.1, size=100), @@ -128,7 +128,7 @@ def test_on_with_multiple_flows(self, basic_flow_system_linopy): def test_on_with_single_flow(self, basic_flow_system_linopy): """Test that flow model constraints are correctly generated.""" flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + timesteps = flow_system.timesteps inputs = [ fx.Flow('In1', 'Fernwärme', relative_minimum=np.ones(10) * 0.1, size=100), ] diff --git a/tests/test_effect.py b/tests/test_effect.py index 5cbc04ac6..9b4e1012a 100644 --- a/tests/test_effect.py +++ b/tests/test_effect.py @@ -13,7 +13,7 @@ class TestBusModel: def test_minimal(self, basic_flow_system_linopy): flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + timesteps = flow_system.timesteps effect = fx.Effect('Effect1', '€', 'Testing Effect') flow_system.add_elements(effect) @@ -43,7 +43,7 @@ def test_minimal(self, basic_flow_system_linopy): def test_bounds(self, basic_flow_system_linopy): flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + timesteps = flow_system.timesteps effect = fx.Effect('Effect1', '€', 'Testing Effect', minimum_operation=1.0, maximum_operation=1.1, diff --git a/tests/test_flow.py b/tests/test_flow.py index 2308dbd31..cce10b21a 100644 --- a/tests/test_flow.py +++ b/tests/test_flow.py @@ -14,7 +14,7 @@ class TestFlowModel: def test_flow_minimal(self, basic_flow_system_linopy): """Test that flow model constraints are correctly generated.""" flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + timesteps = flow_system.timesteps flow = fx.Flow('Wärme', bus='Fernwärme', size=100) flow_system.add_elements(fx.Sink('Sink', sink=flow)) @@ -34,7 +34,7 @@ def test_flow_minimal(self, basic_flow_system_linopy): def test_flow(self, basic_flow_system_linopy): flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + timesteps = flow_system.timesteps flow = fx.Flow( 'Wärme', bus='Fernwärme', @@ -86,7 +86,7 @@ def test_flow(self, basic_flow_system_linopy): def test_effects_per_flow_hour(self, basic_flow_system_linopy): flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + timesteps = flow_system.timesteps costs_per_flow_hour = xr.DataArray(np.linspace(1,2,timesteps.size), coords=(timesteps,)) co2_per_flow_hour = xr.DataArray(np.linspace(4, 5, timesteps.size), coords=(timesteps,)) @@ -120,7 +120,7 @@ class TestFlowInvestModel: def test_flow_invest(self, basic_flow_system_linopy): flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + timesteps = flow_system.timesteps flow = fx.Flow( 'Wärme', @@ -175,7 +175,7 @@ def test_flow_invest(self, basic_flow_system_linopy): def test_flow_invest_optional(self, basic_flow_system_linopy): flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + timesteps = flow_system.timesteps flow = fx.Flow( 'Wärme', @@ -239,7 +239,7 @@ def test_flow_invest_optional(self, basic_flow_system_linopy): def test_flow_invest_optional_wo_min_size(self, basic_flow_system_linopy): flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + timesteps = flow_system.timesteps flow = fx.Flow( 'Wärme', @@ -303,7 +303,7 @@ def test_flow_invest_optional_wo_min_size(self, basic_flow_system_linopy): def test_flow_invest_wo_min_size_non_optional(self, basic_flow_system_linopy): flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + timesteps = flow_system.timesteps flow = fx.Flow( 'Wärme', @@ -354,7 +354,7 @@ def test_flow_invest_wo_min_size_non_optional(self, basic_flow_system_linopy): def test_flow_invest_fixed_size(self, basic_flow_system_linopy): """Test flow with fixed size investment.""" flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + timesteps = flow_system.timesteps flow = fx.Flow( 'Wärme', @@ -446,7 +446,7 @@ class TestFlowOnModel: def test_flow_on(self, basic_flow_system_linopy): flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + timesteps = flow_system.timesteps flow = fx.Flow( 'Wärme', bus='Fernwärme', @@ -506,7 +506,7 @@ def test_flow_on(self, basic_flow_system_linopy): def test_effects_per_running_hour(self, basic_flow_system_linopy): flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + timesteps = flow_system.timesteps costs_per_running_hour = xr.DataArray(np.linspace(1, 2, timesteps.size), coords=(timesteps,)) co2_per_running_hour = xr.DataArray(np.linspace(4, 5, timesteps.size), coords=(timesteps,)) @@ -553,7 +553,7 @@ def test_effects_per_running_hour(self, basic_flow_system_linopy): def test_consecutive_on_hours(self, basic_flow_system_linopy): """Test flow with minimum and maximum consecutive on hours.""" flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + timesteps = flow_system.timesteps flow = fx.Flow( 'Wärme', @@ -619,7 +619,7 @@ def test_consecutive_on_hours(self, basic_flow_system_linopy): def test_consecutive_on_hours_previous(self, basic_flow_system_linopy): """Test flow with minimum and maximum consecutive on hours.""" flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + timesteps = flow_system.timesteps flow = fx.Flow( 'Wärme', @@ -686,7 +686,7 @@ def test_consecutive_on_hours_previous(self, basic_flow_system_linopy): def test_consecutive_off_hours(self, basic_flow_system_linopy): """Test flow with minimum and maximum consecutive off hours.""" flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + timesteps = flow_system.timesteps flow = fx.Flow( 'Wärme', @@ -753,7 +753,7 @@ def test_consecutive_off_hours(self, basic_flow_system_linopy): def test_consecutive_off_hours_previous(self, basic_flow_system_linopy): """Test flow with minimum and maximum consecutive off hours.""" flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + timesteps = flow_system.timesteps flow = fx.Flow( 'Wärme', @@ -906,7 +906,7 @@ class TestFlowOnInvestModel: def test_flow_on_invest_optional(self, basic_flow_system_linopy): flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + timesteps = flow_system.timesteps flow = fx.Flow( 'Wärme', bus='Fernwärme', @@ -991,7 +991,7 @@ def test_flow_on_invest_optional(self, basic_flow_system_linopy): def test_flow_on_invest_non_optional(self, basic_flow_system_linopy): flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + timesteps = flow_system.timesteps flow = fx.Flow( 'Wärme', bus='Fernwärme', @@ -1078,7 +1078,7 @@ class TestFlowWithFixedProfile: def test_fixed_relative_profile(self, basic_flow_system_linopy): """Test flow with a fixed relative profile.""" flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + timesteps = flow_system.timesteps # Create a time-varying profile (e.g., for a load or renewable generation) profile = np.sin(np.linspace(0, 2 * np.pi, len(timesteps))) * 0.5 + 0.5 # Values between 0 and 1 @@ -1100,7 +1100,7 @@ def test_fixed_relative_profile(self, basic_flow_system_linopy): def test_fixed_profile_with_investment(self, basic_flow_system_linopy): """Test flow with fixed profile and investment.""" flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + timesteps = flow_system.timesteps # Create a fixed profile profile = np.sin(np.linspace(0, 2 * np.pi, len(timesteps))) * 0.5 + 0.5 diff --git a/tests/test_linear_converter.py b/tests/test_linear_converter.py index aaab60dcc..a01c17ef2 100644 --- a/tests/test_linear_converter.py +++ b/tests/test_linear_converter.py @@ -52,7 +52,7 @@ def test_basic_linear_converter(self, basic_flow_system_linopy): def test_linear_converter_time_varying(self, basic_flow_system_linopy): """Test a LinearConverter with time-varying conversion factors.""" flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + timesteps = flow_system.timesteps # Create time-varying efficiency (e.g., temperature-dependent) varying_efficiency = np.linspace(0.7, 0.9, len(timesteps)) @@ -268,7 +268,7 @@ def test_linear_converter_multidimensional(self, basic_flow_system_linopy): def test_edge_case_time_varying_conversion(self, basic_flow_system_linopy): """Test edge case with extreme time-varying conversion factors.""" flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + timesteps = flow_system.timesteps # Create fluctuating conversion efficiency (e.g., for a heat pump) # Values range from very low (0.1) to very high (5.0) @@ -317,7 +317,7 @@ def test_edge_case_time_varying_conversion(self, basic_flow_system_linopy): def test_piecewise_conversion(self, basic_flow_system_linopy): """Test a LinearConverter with PiecewiseConversion.""" flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + timesteps = flow_system.timesteps # Create input and output flows input_flow = fx.Flow('input', bus='input_bus', size=100) @@ -423,7 +423,7 @@ def test_piecewise_conversion(self, basic_flow_system_linopy): def test_piecewise_conversion_with_onoff(self, basic_flow_system_linopy): """Test a LinearConverter with PiecewiseConversion and OnOffParameters.""" flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + timesteps = flow_system.timesteps # Create input and output flows input_flow = fx.Flow('input', bus='input_bus', size=100) diff --git a/tests/test_storage.py b/tests/test_storage.py index a3b453c2b..472ba4add 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -14,8 +14,8 @@ class TestStorageModel: def test_basic_storage(self, basic_flow_system_linopy): """Test that basic storage model variables and constraints are correctly generated.""" flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps - timesteps_extra = flow_system.time_series_collection.timesteps_extra + timesteps = flow_system.timesteps + timesteps_extra = flow_system.timesteps_extra # Create a simple storage storage = fx.Storage( @@ -91,8 +91,8 @@ def test_basic_storage(self, basic_flow_system_linopy): def test_lossy_storage(self, basic_flow_system_linopy): """Test that basic storage model variables and constraints are correctly generated.""" flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps - timesteps_extra = flow_system.time_series_collection.timesteps_extra + timesteps = flow_system.timesteps + timesteps_extra = flow_system.timesteps_extra # Create a simple storage storage = fx.Storage( diff --git a/tests/test_timeseries.py b/tests/test_timeseries.py index a8bc5fa85..8702a57fe 100644 --- a/tests/test_timeseries.py +++ b/tests/test_timeseries.py @@ -8,7 +8,7 @@ import pytest import xarray as xr -from flixopt.core import ConversionError, DataConverter, TimeSeries, TimeSeriesCollection, TimeSeriesData +from flixopt.core import ConversionError, DataConverter, TimeSeriesCollection, TimeSeriesData @pytest.fixture @@ -44,7 +44,7 @@ def test_initialization(self, simple_dataarray): # Check data initialization assert isinstance(ts.stored_data, xr.DataArray) assert ts.stored_data.equals(simple_dataarray) - assert ts.active_data.equals(simple_dataarray) + assert ts.equals(simple_dataarray) # Check backup was created assert ts._backup.equals(simple_dataarray) @@ -87,7 +87,7 @@ def test_active_timesteps_getter_setter(self, sample_timeseries, sample_timestep assert sample_timeseries.active_timesteps.equals(subset_index) # Active data should reflect the subset - assert sample_timeseries.active_data.equals(sample_timeseries.stored_data.sel(time=subset_index)) + assert sample_timeseries.equals(sample_timeseries.stored_data.sel(time=subset_index)) # Reset to full index sample_timeseries.active_timesteps = None @@ -108,7 +108,7 @@ def test_reset(self, sample_timeseries, sample_timesteps): # Should be back to full index assert sample_timeseries.active_timesteps.equals(sample_timesteps) - assert sample_timeseries.active_data.equals(sample_timeseries.stored_data) + assert sample_timeseries.equals(sample_timeseries.stored_data) def test_restore_data(self, sample_timeseries, simple_dataarray): """Test restore_data method.""" @@ -127,7 +127,7 @@ def test_restore_data(self, sample_timeseries, simple_dataarray): # Should be back to original data assert sample_timeseries.stored_data.equals(original_data) - assert sample_timeseries.active_data.equals(original_data) + assert sample_timeseries.equals(original_data) def test_stored_data_setter(self, sample_timeseries, sample_timesteps): """Test stored_data setter with different data types.""" @@ -234,30 +234,30 @@ def test_arithmetic_operations(self, sample_timeseries): # Test operations between two TimeSeries objects assert np.array_equal( - (sample_timeseries + ts2).values, sample_timeseries.active_data.values + ts2.active_data.values + (sample_timeseries + ts2).values, sample_timeseries.values + ts2.values ) assert np.array_equal( - (sample_timeseries - ts2).values, sample_timeseries.active_data.values - ts2.active_data.values + (sample_timeseries - ts2).values, sample_timeseries.values - ts2.values ) assert np.array_equal( - (sample_timeseries * ts2).values, sample_timeseries.active_data.values * ts2.active_data.values + (sample_timeseries * ts2).values, sample_timeseries.values * ts2.values ) assert np.array_equal( - (sample_timeseries / ts2).values, sample_timeseries.active_data.values / ts2.active_data.values + (sample_timeseries / ts2).values, sample_timeseries.values / ts2.values ) # Test operations with DataArrays - assert np.array_equal((sample_timeseries + data2).values, sample_timeseries.active_data.values + data2.values) - assert np.array_equal((data2 + sample_timeseries).values, data2.values + sample_timeseries.active_data.values) + assert np.array_equal((sample_timeseries + data2).values, sample_timeseries.values + data2.values) + assert np.array_equal((data2 + sample_timeseries).values, data2.values + sample_timeseries.values) # Test operations with scalars - assert np.array_equal((sample_timeseries + 5).values, sample_timeseries.active_data.values + 5) - assert np.array_equal((5 + sample_timeseries).values, 5 + sample_timeseries.active_data.values) + assert np.array_equal((sample_timeseries + 5).values, sample_timeseries.values + 5) + assert np.array_equal((5 + sample_timeseries).values, 5 + sample_timeseries.values) # Test unary operations - assert np.array_equal((-sample_timeseries).values, -sample_timeseries.active_data.values) - assert np.array_equal((+sample_timeseries).values, +sample_timeseries.active_data.values) - assert np.array_equal((abs(sample_timeseries)).values, abs(sample_timeseries.active_data.values)) + assert np.array_equal((-sample_timeseries).values, -sample_timeseries.values) + assert np.array_equal((+sample_timeseries).values, +sample_timeseries.values) + assert np.array_equal((abs(sample_timeseries)).values, abs(sample_timeseries.values)) def test_comparison_operations(self, sample_timesteps): """Test comparison operations.""" @@ -279,10 +279,10 @@ def test_comparison_operations(self, sample_timesteps): def test_numpy_ufunc(self, sample_timeseries): """Test numpy ufunc compatibility.""" # Test basic numpy functions - assert np.array_equal(np.add(sample_timeseries, 5).values, np.add(sample_timeseries.active_data, 5).values) + assert np.array_equal(np.add(sample_timeseries, 5).values, np.add(sample_timeseries, 5).values) assert np.array_equal( - np.multiply(sample_timeseries, 2).values, np.multiply(sample_timeseries.active_data, 2).values + np.multiply(sample_timeseries, 2).values, np.multiply(sample_timeseries, 2).values ) # Test with two TimeSeries objects @@ -290,18 +290,18 @@ def test_numpy_ufunc(self, sample_timeseries): ts2 = TimeSeries(data2, 'Second Series') assert np.array_equal( - np.add(sample_timeseries, ts2).values, np.add(sample_timeseries.active_data, ts2.active_data).values + np.add(sample_timeseries, ts2).values, np.add(sample_timeseries, ts2).values ) def test_sel_and_isel_properties(self, sample_timeseries): """Test sel and isel properties.""" # Test that sel property works selected = sample_timeseries.sel(time=sample_timeseries.active_timesteps[0]) - assert selected.item() == sample_timeseries.active_data.values[0] + assert selected.item() == sample_timeseries.values[0] # Test that isel property works indexed = sample_timeseries.isel(time=0) - assert indexed.item() == sample_timeseries.active_data.values[0] + assert indexed.item() == sample_timeseries.values[0] @pytest.fixture @@ -372,12 +372,12 @@ def test_create_time_series(self, sample_collection): # Test scalar ts1 = sample_collection.create_time_series(42, 'scalar_series') assert ts1.name == 'scalar_series' - assert np.all(ts1.active_data.values == 42) + assert np.all(ts1.values == 42) # Test numpy array data = np.array([1, 2, 3, 4, 5]) ts2 = sample_collection.create_time_series(data, 'array_series') - assert np.array_equal(ts2.active_data.values, data) + assert np.array_equal(ts2.values, data) # Test with TimeSeriesData ts3 = sample_collection.create_time_series(TimeSeriesData(10, agg_weight=0.7), 'weighted_series') @@ -386,7 +386,7 @@ def test_create_time_series(self, sample_collection): # Test with extra timestep ts4 = sample_collection.create_time_series(5, 'extra_series', needs_extra_timestep=True) assert ts4.needs_extra_timestep - assert len(ts4.active_data) == len(sample_collection.timesteps_extra) + assert len(ts4) == len(sample_collection.timesteps_extra) # Test duplicate name with pytest.raises(ValueError, match='already exists'): @@ -509,12 +509,12 @@ def test_insert_new_data(self, populated_collection, sample_timesteps): populated_collection.insert_new_data(new_data) # Verify updates - assert np.all(populated_collection['constant_series'].active_data.values == 100) - assert np.array_equal(populated_collection['varying_series'].active_data.values, np.array([5, 10, 15, 20, 25])) + assert np.all(populated_collection['constant_series'].values == 100) + assert np.array_equal(populated_collection['varying_series'].values, np.array([5, 10, 15, 20, 25])) # Series not in the DataFrame should be unchanged assert np.array_equal( - populated_collection['extra_timestep_series'].active_data.values[:-1], np.array([1, 2, 3, 4, 5]) + populated_collection['extra_timestep_series'].values[:-1], np.array([1, 2, 3, 4, 5]) ) # Test with mismatched index @@ -542,7 +542,7 @@ def test_restore_data(self, populated_collection): populated_collection.insert_new_data(new_data) # Verify data was changed - assert np.all(populated_collection['constant_series'].active_data.values == 999) + assert np.all(populated_collection['constant_series'].values == 999) # Restore data populated_collection.restore_data() From fc76adf7e2a9aa9010cb9a04dc57fd65ce3829f2 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 23 Jun 2025 17:22:00 +0200 Subject: [PATCH 061/448] Rename conversion method to pattern: to_... --- flixopt/core.py | 2 +- flixopt/flow_system.py | 10 +++++----- flixopt/results.py | 2 +- tests/test_dataconverter.py | 26 +++++++++++++------------- tests/test_io.py | 4 ++-- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/flixopt/core.py b/flixopt/core.py index 022bf8e6f..73ad098ba 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -46,7 +46,7 @@ class DataConverter: """ @staticmethod - def as_dataarray(data: NumericData, timesteps: pd.DatetimeIndex) -> xr.DataArray: + def to_dataarray(data: NumericData, timesteps: pd.DatetimeIndex) -> xr.DataArray: """Convert data to xarray.DataArray with specified timesteps index.""" if not isinstance(timesteps, pd.DatetimeIndex) or len(timesteps) == 0: raise ValueError(f'Timesteps must be a non-empty DatetimeIndex, got {type(timesteps).__name__}') diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index ae9df6407..de94c14e5 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -224,7 +224,7 @@ def to_dataset(self, constants_in_dataset: bool = True) -> xr.Dataset: ds = xr.Dataset(extracted_arrays, attrs=reference_structure) return ds - def as_dict(self, data_mode: Literal['data', 'name', 'stats'] = 'data') -> Dict: + def to_dict(self, data_mode: Literal['data', 'name', 'stats'] = 'data') -> Dict: """ Convert the object to a dictionary representation. Now builds on the reference structure for consistency. @@ -364,7 +364,7 @@ def to_json(self, path: Union[str, pathlib.Path]): path: The path to the JSON file. """ # Use the stats mode for JSON export (cleaner output) - data = get_compact_representation(self.as_dict('stats')) + data = get_compact_representation(self.to_dict('stats')) with open(path, 'w', encoding='utf-8') as f: json.dump(data, f, indent=4, ensure_ascii=False) @@ -399,12 +399,12 @@ def create_time_series( # Convert TimeSeriesData to DataArray from .core import DataConverter # Assuming this exists - return DataConverter.as_dataarray(data.data, timesteps=target_timesteps).rename(name) + return DataConverter.to_dataarray(data.data, timesteps=target_timesteps).rename(name) else: # Convert other data types to DataArray from .core import DataConverter # Assuming this exists - return DataConverter.as_dataarray(data, timesteps=target_timesteps).rename(name) + return DataConverter.to_dataarray(data, timesteps=target_timesteps).rename(name) def create_effect_time_series( self, @@ -576,7 +576,7 @@ def __repr__(self): def __str__(self): with StringIO() as output_buffer: console = Console(file=output_buffer, width=1000) # Adjust width as needed - console.print(Pretty(self.as_dict('stats'), expand_all=True, indent_guides=True)) + console.print(Pretty(self.to_dict('stats'), expand_all=True, indent_guides=True)) value = output_buffer.getvalue() return value diff --git a/flixopt/results.py b/flixopt/results.py index 223e3708e..9c0f7245b 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -118,7 +118,7 @@ def from_calculation(cls, calculation: 'Calculation'): """ return cls( solution=calculation.model.solution, - flow_system=calculation.flow_system.as_dataset(constants_in_dataset=True), + flow_system=calculation.flow_system.to_dataset(constants_in_dataset=True), summary=calculation.summary, model=calculation.model, name=calculation.name, diff --git a/tests/test_dataconverter.py b/tests/test_dataconverter.py index 49f1438e7..329da7f92 100644 --- a/tests/test_dataconverter.py +++ b/tests/test_dataconverter.py @@ -14,7 +14,7 @@ def sample_time_index(request): def test_scalar_conversion(sample_time_index): # Test scalar conversion - result = DataConverter.as_dataarray(42, sample_time_index) + result = DataConverter.to_dataarray(42, sample_time_index) assert isinstance(result, xr.DataArray) assert result.shape == (len(sample_time_index),) assert result.dims == ('time',) @@ -25,7 +25,7 @@ def test_series_conversion(sample_time_index): series = pd.Series([1, 2, 3, 4, 5], index=sample_time_index) # Test Series conversion - result = DataConverter.as_dataarray(series, sample_time_index) + result = DataConverter.to_dataarray(series, sample_time_index) assert isinstance(result, xr.DataArray) assert result.shape == (5,) assert result.dims == ('time',) @@ -37,7 +37,7 @@ def test_dataframe_conversion(sample_time_index): df = pd.DataFrame({'A': [1, 2, 3, 4, 5]}, index=sample_time_index) # Test DataFrame conversion - result = DataConverter.as_dataarray(df, sample_time_index) + result = DataConverter.to_dataarray(df, sample_time_index) assert isinstance(result, xr.DataArray) assert result.shape == (5,) assert result.dims == ('time',) @@ -47,7 +47,7 @@ def test_dataframe_conversion(sample_time_index): def test_ndarray_conversion(sample_time_index): # Test 1D array conversion arr_1d = np.array([1, 2, 3, 4, 5]) - result = DataConverter.as_dataarray(arr_1d, sample_time_index) + result = DataConverter.to_dataarray(arr_1d, sample_time_index) assert result.shape == (5,) assert result.dims == ('time',) assert np.array_equal(result.values, arr_1d) @@ -58,7 +58,7 @@ def test_dataarray_conversion(sample_time_index): original = xr.DataArray(data=np.array([1, 2, 3, 4, 5]), coords={'time': sample_time_index}, dims=['time']) # Test DataArray conversion - result = DataConverter.as_dataarray(original, sample_time_index) + result = DataConverter.to_dataarray(original, sample_time_index) assert result.shape == (5,) assert result.dims == ('time',) assert np.array_equal(result.values, original.values) @@ -71,42 +71,42 @@ def test_dataarray_conversion(sample_time_index): def test_invalid_inputs(sample_time_index): # Test invalid input type with pytest.raises(ConversionError): - DataConverter.as_dataarray('invalid_string', sample_time_index) + DataConverter.to_dataarray('invalid_string', sample_time_index) # Test mismatched Series index mismatched_series = pd.Series([1, 2, 3, 4, 5, 6], index=pd.date_range('2025-01-01', periods=6, freq='D')) with pytest.raises(ConversionError): - DataConverter.as_dataarray(mismatched_series, sample_time_index) + DataConverter.to_dataarray(mismatched_series, sample_time_index) # Test DataFrame with multiple columns df_multi_col = pd.DataFrame({'A': [1, 2, 3, 4, 5], 'B': [6, 7, 8, 9, 10]}, index=sample_time_index) with pytest.raises(ConversionError): - DataConverter.as_dataarray(df_multi_col, sample_time_index) + DataConverter.to_dataarray(df_multi_col, sample_time_index) # Test mismatched array shape with pytest.raises(ConversionError): - DataConverter.as_dataarray(np.array([1, 2, 3]), sample_time_index) # Wrong length + DataConverter.to_dataarray(np.array([1, 2, 3]), sample_time_index) # Wrong length # Test multi-dimensional array with pytest.raises(ConversionError): - DataConverter.as_dataarray(np.array([[1, 2], [3, 4]]), sample_time_index) # 2D array not allowed + DataConverter.to_dataarray(np.array([[1, 2], [3, 4]]), sample_time_index) # 2D array not allowed def test_time_index_validation(): # Test with unnamed index unnamed_index = pd.date_range('2024-01-01', periods=5, freq='D') with pytest.raises(ConversionError): - DataConverter.as_dataarray(42, unnamed_index) + DataConverter.to_dataarray(42, unnamed_index) # Test with empty index empty_index = pd.DatetimeIndex([], name='time') with pytest.raises(ValueError): - DataConverter.as_dataarray(42, empty_index) + DataConverter.to_dataarray(42, empty_index) # Test with non-DatetimeIndex wrong_type_index = pd.Index([1, 2, 3, 4, 5], name='time') with pytest.raises(ValueError): - DataConverter.as_dataarray(42, wrong_type_index) + DataConverter.to_dataarray(42, wrong_type_index) if __name__ == '__main__': diff --git a/tests/test_io.py b/tests/test_io.py index 2e6c61ccf..8bcdb050e 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -50,10 +50,10 @@ def test_flow_system_file_io(flow_system, highs_solver): def test_flow_system_io(flow_system): - di = flow_system.as_dict() + di = flow_system.to_dict() _ = fx.FlowSystem.from_dict(di) - ds = flow_system.as_dataset() + ds = flow_system.to_dataset() _ = fx.FlowSystem.from_dataset(ds) print(flow_system) From cc7b15555e321cf3779edba21cb8cd7b6eeb860f Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 23 Jun 2025 17:23:49 +0200 Subject: [PATCH 062/448] Move methods to FlowSystem --- flixopt/flow_system.py | 4 ++-- flixopt/results.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index de94c14e5..6b65d8d00 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -51,7 +51,7 @@ def __init__( # Store timing information directly self.timesteps = self._validate_timesteps(timesteps) self.timesteps_extra = self._create_timesteps_with_extra(timesteps, hours_of_last_timestep) - self.hours_per_timestep = self._calculate_hours_per_timestep(self.timesteps_extra) + self.hours_per_timestep = self.calculate_hours_per_timestep(self.timesteps_extra) self.hours_of_previous_timesteps = self._calculate_hours_of_previous_timesteps( timesteps, hours_of_previous_timesteps ) @@ -89,7 +89,7 @@ def _create_timesteps_with_extra( return pd.DatetimeIndex(timesteps.append(last_date), name='time') @staticmethod - def _calculate_hours_per_timestep(timesteps_extra: pd.DatetimeIndex) -> xr.DataArray: + def calculate_hours_per_timestep(timesteps_extra: pd.DatetimeIndex) -> xr.DataArray: """Calculate duration of each timestep.""" hours_per_step = np.diff(timesteps_extra) / pd.Timedelta(hours=1) return xr.DataArray( diff --git a/flixopt/results.py b/flixopt/results.py index 9c0f7245b..232aaf5af 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -14,7 +14,7 @@ from . import io as fx_io from . import plotting -from .core import TimeSeriesCollection +from .flow_system import FlowSystem if TYPE_CHECKING: import pyvis @@ -160,7 +160,7 @@ def __init__( } self.timesteps_extra = self.solution.indexes['time'] - self.hours_per_timestep = TimeSeriesCollection.calculate_hours_per_timestep(self.timesteps_extra) + self.hours_per_timestep = FlowSystem.calculate_hours_per_timestep(self.timesteps_extra) def __getitem__(self, key: str) -> Union['ComponentResults', 'BusResults', 'EffectResults']: if key in self.components: @@ -684,7 +684,7 @@ def __init__( self.overlap_timesteps = overlap_timesteps self.name = name self.folder = pathlib.Path(folder) if folder is not None else pathlib.Path.cwd() / 'results' - self.hours_per_timestep = TimeSeriesCollection.calculate_hours_per_timestep(self.all_timesteps) + self.hours_per_timestep = FlowSystem.calculate_hours_per_timestep(self.all_timesteps) @property def meta_data(self) -> Dict[str, Union[int, List[str]]]: From ec6e792bf059a641e29fce72b34ee8d5761174de Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 23 Jun 2025 17:41:59 +0200 Subject: [PATCH 063/448] Drop nan values across time dimension if present --- flixopt/flow_system.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 6b65d8d00..039cd2bfa 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -171,7 +171,12 @@ def _resolve_reference_structure(cls, structure, arrays_dict: Dict[str, xr.DataA # This is a reference to a DataArray array_name = structure[3:] # Remove ":::" prefix if array_name in arrays_dict: - return arrays_dict[array_name] + #TODO: Improve this! + da = arrays_dict[array_name] + if da.isnull().any(): + logger.warning(f"DataArray '{array_name}' contains null values. Dropping them.") + return da.dropna(dim='time', how='all') + return da else: logger.critical(f"Referenced DataArray '{array_name}' not found in dataset") return None From b42aad2b1dbecd3cfad88ebe201e846acee57de6 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 24 Jun 2025 09:12:17 +0200 Subject: [PATCH 064/448] Allow lists of values to create DataArray --- flixopt/core.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/flixopt/core.py b/flixopt/core.py index 73ad098ba..d629787bb 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -84,6 +84,9 @@ def to_dataarray(data: NumericData, timesteps: pd.DatetimeIndex) -> xr.DataArray f"DataArray length {data.sizes[dims[0]]} doesn't match expected {len(coords[0])}" ) return data.copy(deep=True) + elif isinstance(data, list): + logger.warning(f'Converting list to DataArray. This is not reccomended.') + return xr.DataArray(data, coords=coords, dims=dims) else: raise ConversionError(f'Unsupported type: {type(data).__name__}') except Exception as e: From b55af45a2e6d3538e098dda4586c519237239da9 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 24 Jun 2025 09:12:32 +0200 Subject: [PATCH 065/448] Update resolving of FlowSystem --- flixopt/flow_system.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 039cd2bfa..9a28e1ad0 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -324,16 +324,13 @@ def from_dict(cls, data: Dict) -> 'FlowSystem': ) # Add elements using resolved data - for bus_data in resolved_data.get('buses', {}).values(): - bus = Bus.from_dict(bus_data) + for bus in resolved_data.get('buses', {}).values(): flow_system.add_elements(bus) - for effect_data in resolved_data.get('effects', {}).values(): - effect = Effect.from_dict(effect_data) + for effect in resolved_data.get('effects', {}).values(): flow_system.add_elements(effect) - for comp_data in resolved_data.get('components', {}).values(): - component = CLASS_REGISTRY[comp_data['__class__']].from_dict(comp_data) + for component in resolved_data.get('components', {}).values(): flow_system.add_elements(component) flow_system.transform_data() From d5ace96959015aabe4f869f4e9a12fb1f0e8419f Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 24 Jun 2025 09:12:45 +0200 Subject: [PATCH 066/448] Simplify TimeSeriesData --- flixopt/core.py | 81 +++++++++++++++++++++++++++++++------------------ 1 file changed, 51 insertions(+), 30 deletions(-) diff --git a/flixopt/core.py b/flixopt/core.py index d629787bb..3aad560b2 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -96,43 +96,64 @@ def to_dataarray(data: NumericData, timesteps: pd.DatetimeIndex) -> xr.DataArray class TimeSeriesData: - # TODO: Move to Interface.py - def __init__(self, data: NumericData, agg_group: Optional[str] = None, agg_weight: Optional[float] = None): + """Minimal wrapper around xr.DataArray with aggregation metadata.""" + + def __init__( + self, + data: Union[NumericData, xr.DataArray], + agg_group: Optional[str] = None, + agg_weight: Optional[float] = None, + ): """ - timeseries class for transmit timeseries AND special characteristics of timeseries, - i.g. to define weights needed in calculation_type 'aggregated' - EXAMPLE solar: - you have several solar timeseries. These should not be overweighted - compared to the remaining timeseries (i.g. heat load, price)! - fixed_relative_profile_solar1 = TimeSeriesData(sol_array_1, type = 'solar') - fixed_relative_profile_solar2 = TimeSeriesData(sol_array_2, type = 'solar') - fixed_relative_profile_solar3 = TimeSeriesData(sol_array_3, type = 'solar') - --> this 3 series of same type share one weight, i.e. internally assigned each weight = 1/3 - (instead of standard weight = 1) - Args: - data: The timeseries data, which can be a scalar, array, or numpy array. - agg_group: The group this TimeSeriesData is a part of. agg_weight is split between members of a group. Default is None. - agg_weight: The weight for calculation_type 'aggregated', should be between 0 and 1. Default is None. - - Raises: - Exception: If both agg_group and agg_weight are set, an exception is raised. + data: Numeric data or DataArray + agg_group: Aggregation group name + agg_weight: Aggregation weight (0-1) """ - self.data = data + if (agg_group is not None) and (agg_weight is not None): + raise ValueError('Use either agg_group or agg_weight, not both') + self.agg_group = agg_group self.agg_weight = agg_weight - if (agg_group is not None) and (agg_weight is not None): - raise ValueError('Either or explicit can be used. Not both!') - self.label: Optional[str] = None - def __repr__(self): - # Get the constructor arguments and their current values - init_signature = inspect.signature(self.__init__) - init_args = init_signature.parameters + # Store as DataArray + if isinstance(data, xr.DataArray): + self.data = data + else: + # Simple conversion - let caller handle timesteps/coords + self.data = xr.DataArray(np.asarray(data)) + + @property + def label(self) -> Optional[str]: + return self.data.name + + @label.setter + def label(self, value: Optional[str]): + self.data.name = value + + def to_dataarray(self) -> xr.DataArray: + """Return the DataArray with metadata in attrs.""" + attrs = {} + if self.agg_group is not None: + attrs['agg_group'] = self.agg_group + if self.agg_weight is not None: + attrs['agg_weight'] = self.agg_weight + + da = self.data.copy() + da.attrs.update(attrs) + return da + + @classmethod + def from_dataarray(cls, da: xr.DataArray) -> 'TimeSeriesData': + """Create from DataArray, extracting metadata from attrs.""" + return cls(data=da, agg_group=da.attrs.get('agg_group'), agg_weight=da.attrs.get('agg_weight')) + + def __getattr__(self, name): + """Delegate to underlying DataArray.""" + return getattr(self.data, name) - # Create a dictionary with argument names and their values - args_str = ', '.join(f'{name}={repr(getattr(self, name, None))}' for name in init_args if name != 'self') - return f'{self.__class__.__name__}({args_str})' + def __repr__(self): + return f'TimeSeriesData(agg_group={self.agg_group!r}, agg_weight={self.agg_weight!r})' def __str__(self): return str(self.data) From 4187f305f4d6d73a71aea686604772908525197f Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 24 Jun 2025 09:54:02 +0200 Subject: [PATCH 067/448] Move TImeSeriesData to Structure and simplyfy to inherrit from xarray.DataArray --- flixopt/aggregation.py | 3 +- flixopt/commons.py | 2 +- flixopt/core.py | 66 +----------------------------- flixopt/flow_system.py | 4 +- flixopt/linear_converters.py | 4 +- flixopt/structure.py | 79 +++++++++++++++++++++++++++++++----- 6 files changed, 77 insertions(+), 81 deletions(-) diff --git a/flixopt/aggregation.py b/flixopt/aggregation.py index f149d5f20..e558dc19b 100644 --- a/flixopt/aggregation.py +++ b/flixopt/aggregation.py @@ -22,13 +22,14 @@ TSAM_AVAILABLE = False from .components import Storage -from .core import Scalar, TimeSeriesData +from .core import Scalar from .elements import Component from .flow_system import FlowSystem from .structure import ( Element, Model, SystemModel, + TimeSeriesData, ) if TYPE_CHECKING: diff --git a/flixopt/commons.py b/flixopt/commons.py index 68412d6fe..7d03909c0 100644 --- a/flixopt/commons.py +++ b/flixopt/commons.py @@ -14,11 +14,11 @@ Transmission, ) from .config import CONFIG, change_logging_level -from .core import TimeSeriesData from .effects import Effect from .elements import Bus, Flow from .flow_system import FlowSystem from .interface import InvestParameters, OnOffParameters, Piece, Piecewise, PiecewiseConversion, PiecewiseEffects +from .structure import TimeSeriesData __all__ = [ 'TimeSeriesData', diff --git a/flixopt/core.py b/flixopt/core.py index 3aad560b2..43056cedb 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -95,74 +95,11 @@ def to_dataarray(data: NumericData, timesteps: pd.DatetimeIndex) -> xr.DataArray raise ConversionError(f'Converting data {type(data)} to xarray.Dataset raised an error: {str(e)}') from e -class TimeSeriesData: - """Minimal wrapper around xr.DataArray with aggregation metadata.""" - - def __init__( - self, - data: Union[NumericData, xr.DataArray], - agg_group: Optional[str] = None, - agg_weight: Optional[float] = None, - ): - """ - Args: - data: Numeric data or DataArray - agg_group: Aggregation group name - agg_weight: Aggregation weight (0-1) - """ - if (agg_group is not None) and (agg_weight is not None): - raise ValueError('Use either agg_group or agg_weight, not both') - - self.agg_group = agg_group - self.agg_weight = agg_weight - - # Store as DataArray - if isinstance(data, xr.DataArray): - self.data = data - else: - # Simple conversion - let caller handle timesteps/coords - self.data = xr.DataArray(np.asarray(data)) - - @property - def label(self) -> Optional[str]: - return self.data.name - - @label.setter - def label(self, value: Optional[str]): - self.data.name = value - - def to_dataarray(self) -> xr.DataArray: - """Return the DataArray with metadata in attrs.""" - attrs = {} - if self.agg_group is not None: - attrs['agg_group'] = self.agg_group - if self.agg_weight is not None: - attrs['agg_weight'] = self.agg_weight - - da = self.data.copy() - da.attrs.update(attrs) - return da - - @classmethod - def from_dataarray(cls, da: xr.DataArray) -> 'TimeSeriesData': - """Create from DataArray, extracting metadata from attrs.""" - return cls(data=da, agg_group=da.attrs.get('agg_group'), agg_weight=da.attrs.get('agg_weight')) - - def __getattr__(self, name): - """Delegate to underlying DataArray.""" - return getattr(self.data, name) - - def __repr__(self): - return f'TimeSeriesData(agg_group={self.agg_group!r}, agg_weight={self.agg_weight!r})' - - def __str__(self): - return str(self.data) - - class TimeSeries: def __init__(self): raise NotImplementedError('TimeSeries was removed') + class TimeSeriesCollection: """ Collection of TimeSeries objects with shared timestep management. @@ -174,6 +111,7 @@ class TimeSeriesCollection: def __init__(self): raise NotImplementedError('TimeSeriesCollection was removed') + def get_numeric_stats(data: xr.DataArray, decimals: int = 2, padd: int = 10) -> str: """Calculates the mean, median, min, max, and standard deviation of a numeric DataArray.""" format_spec = f'>{padd}.{decimals}f' if padd else f'.{decimals}f' diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 9a28e1ad0..097b3af83 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -16,10 +16,10 @@ from rich.pretty import Pretty from . import io as fx_io -from .core import NumericData, NumericDataTS, TimeSeriesCollection, TimeSeriesData, TimeSeries +from .core import NumericData, NumericDataTS, TimeSeriesCollection, TimeSeries from .effects import Effect, EffectCollection, EffectTimeSeries, EffectValuesDict, EffectValuesUser from .elements import Bus, Component, Flow -from .structure import CLASS_REGISTRY, Element, SystemModel, get_compact_representation, get_str_representation +from .structure import CLASS_REGISTRY, Element, SystemModel, get_compact_representation, get_str_representation, TimeSeriesData if TYPE_CHECKING: import pyvis diff --git a/flixopt/linear_converters.py b/flixopt/linear_converters.py index 3fd032632..83527fef0 100644 --- a/flixopt/linear_converters.py +++ b/flixopt/linear_converters.py @@ -8,10 +8,10 @@ import numpy as np from .components import LinearConverter -from .core import NumericDataTS, TimeSeriesData +from .core import NumericDataTS from .elements import Flow from .interface import OnOffParameters -from .structure import register_class_for_io +from .structure import register_class_for_io, TimeSeriesData logger = logging.getLogger('flixopt') diff --git a/flixopt/structure.py b/flixopt/structure.py index 71efe31df..fadc1a06f 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -19,7 +19,7 @@ from rich.pretty import Pretty from .config import CONFIG -from .core import NumericData, Scalar, TimeSeriesCollection, TimeSeriesData, TimeSeries +from .core import NumericData, Scalar, TimeSeriesCollection, TimeSeries if TYPE_CHECKING: # for type checking and preventing circular imports from .effects import EffectCollectionModel @@ -162,14 +162,8 @@ def _extract_dataarrays_recursive(self, obj) -> Tuple[Any, Dict[str, xr.DataArra """ extracted_arrays = {} - # Handle TimeSeries objects - extract their data using their unique name - if isinstance(obj, TimeSeries): - data_array = obj.rename(obj.name) - extracted_arrays[obj.name] = data_array - return f':::{obj.name}', extracted_arrays - # Handle DataArrays directly - use their unique name - elif isinstance(obj, xr.DataArray): + if isinstance(obj, xr.DataArray): if not obj.name: raise ValueError('DataArray must have a unique name for serialization') extracted_arrays[obj.name] = obj @@ -222,12 +216,13 @@ def _resolve_reference_structure(cls, structure, arrays_dict: Dict[str, xr.DataA Convert reference structure back to actual objects using provided arrays. Args: - structure: Structure containing references (:::name) + structure: Structure containing references (:::name) or special type markers arrays_dict: Dictionary of available DataArrays Returns: - Structure with references resolved to actual DataArrays + Structure with references resolved to actual DataArrays or TimeSeriesData objects """ + # Handle regular DataArray references if isinstance(structure, str) and structure.startswith(':::'): # This is a reference to a DataArray array_name = structure[3:] # Remove ":::" prefix @@ -246,7 +241,6 @@ def _resolve_reference_structure(cls, structure, arrays_dict: Dict[str, xr.DataA return resolved_list elif isinstance(structure, dict): - # Check if this is a serialized Interface object if structure.get('__class__') and structure['__class__'] in CLASS_REGISTRY: # This is a nested Interface object - restore it recursively nested_class = CLASS_REGISTRY[structure['__class__']] @@ -256,6 +250,7 @@ def _resolve_reference_structure(cls, structure, arrays_dict: Dict[str, xr.DataA resolved_nested_data = cls._resolve_reference_structure(nested_data, arrays_dict) # Create the nested Interface object return nested_class(**resolved_nested_data) + else: # Regular dictionary - resolve references in values resolved_dict = {} @@ -355,6 +350,9 @@ def _apply_element_label_preference(self, obj): if obj.get('__class__') and 'label' in obj: # This looks like an Interface with a label - return just the label return obj.get('label', obj.get('__class__')) + elif obj.get('__class__') == 'TimeSeriesData': + # For TimeSeriesData, show a compact representation + return f'TimeSeriesData(agg_group={obj.get("agg_group")}, agg_weight={obj.get("agg_weight")})' else: return {k: self._apply_element_label_preference(v) for k, v in obj.items()} elif isinstance(obj, list): @@ -666,6 +664,65 @@ def results_structure(self): } +class TimeSeriesData(xr.DataArray): + """Minimal TimeSeriesData that inherits from xr.DataArray with aggregation metadata.""" + + def __init__(self, *args, agg_group: Optional[str] = None, agg_weight: Optional[float] = None, **kwargs): + """ + Args: + *args: Arguments passed to DataArray + agg_group: Aggregation group name + agg_weight: Aggregation weight (0-1) + **kwargs: Additional arguments passed to DataArray + """ + if (agg_group is not None) and (agg_weight is not None): + raise ValueError('Use either agg_group or agg_weight, not both') + + # Let xarray handle all the initialization complexity + super().__init__(*args, **kwargs) + + # Add our metadata to attrs after initialization + if agg_group is not None: + self.attrs['agg_group'] = agg_group + if agg_weight is not None: + self.attrs['agg_weight'] = agg_weight + + # Always mark as TimeSeriesData + self.attrs['__timeseries_data__'] = True + + @property + def agg_group(self) -> Optional[str]: + return self.attrs.get('agg_group') + + @property + def agg_weight(self) -> Optional[float]: + return self.attrs.get('agg_weight') + + @classmethod + def from_dataarray(cls, da: xr.DataArray, agg_group: Optional[str] = None, agg_weight: Optional[float] = None): + """Create TimeSeriesData from DataArray, extracting metadata from attrs.""" + # Get aggregation metadata from attrs or parameters + final_agg_group = agg_group if agg_group is not None else da.attrs.get('agg_group') + final_agg_weight = agg_weight if agg_weight is not None else da.attrs.get('agg_weight') + + return cls(da, agg_group=final_agg_group, agg_weight=final_agg_weight) + + @classmethod + def is_timeseries_data(cls, obj) -> bool: + """Check if an object is TimeSeriesData.""" + return isinstance(obj, xr.DataArray) and obj.attrs.get('__timeseries_data__', False) + + def __repr__(self): + agg_info = [] + if self.agg_group: + agg_info.append(f"agg_group='{self.agg_group}'") + if self.agg_weight is not None: + agg_info.append(f'agg_weight={self.agg_weight}') + + info_str = f'TimeSeriesData({", ".join(agg_info)})' if agg_info else 'TimeSeriesData' + return f'{info_str}\n{super().__repr__()}' + + def copy_and_convert_datatypes(data: Any, use_numpy: bool = True, use_element_label: bool = False) -> Any: """ Converts values in a nested data structure into JSON-compatible types while preserving or transforming numpy arrays From 617600fe833fc4ee4448bd1936a0bbb484e44212 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 24 Jun 2025 09:58:05 +0200 Subject: [PATCH 068/448] Adjust IO --- flixopt/structure.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/flixopt/structure.py b/flixopt/structure.py index fadc1a06f..166d2182c 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -176,7 +176,6 @@ def _extract_dataarrays_recursive(self, obj) -> Tuple[Any, Dict[str, xr.DataArra # Add all extracted arrays from the nested Interface extracted_arrays.update(interface_arrays) - return interface_structure, extracted_arrays # Handle lists @@ -222,12 +221,17 @@ def _resolve_reference_structure(cls, structure, arrays_dict: Dict[str, xr.DataA Returns: Structure with references resolved to actual DataArrays or TimeSeriesData objects """ - # Handle regular DataArray references + # Handle DataArray references (including TimeSeriesData) if isinstance(structure, str) and structure.startswith(':::'): - # This is a reference to a DataArray array_name = structure[3:] # Remove ":::" prefix if array_name in arrays_dict: - return arrays_dict[array_name] + array = arrays_dict[array_name] + + # Check if this should be restored as TimeSeriesData + if TimeSeriesData.is_timeseries_data(array): + return TimeSeriesData.from_dataarray(array) + else: + return array else: logger.critical(f"Referenced DataArray '{array_name}' not found in dataset") return None @@ -250,7 +254,6 @@ def _resolve_reference_structure(cls, structure, arrays_dict: Dict[str, xr.DataA resolved_nested_data = cls._resolve_reference_structure(nested_data, arrays_dict) # Create the nested Interface object return nested_class(**resolved_nested_data) - else: # Regular dictionary - resolve references in values resolved_dict = {} @@ -350,9 +353,6 @@ def _apply_element_label_preference(self, obj): if obj.get('__class__') and 'label' in obj: # This looks like an Interface with a label - return just the label return obj.get('label', obj.get('__class__')) - elif obj.get('__class__') == 'TimeSeriesData': - # For TimeSeriesData, show a compact representation - return f'TimeSeriesData(agg_group={obj.get("agg_group")}, agg_weight={obj.get("agg_weight")})' else: return {k: self._apply_element_label_preference(v) for k, v in obj.items()} elif isinstance(obj, list): From e80bba0dadc9ec4246a95b40de6e53882cb35286 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 24 Jun 2025 10:22:30 +0200 Subject: [PATCH 069/448] Move TimeSeriesData back to core.py and fix Conversion --- flixopt/aggregation.py | 3 +- flixopt/commons.py | 2 +- flixopt/core.py | 149 +++++++++++++++++++++++++++++++++++++++-- flixopt/flow_system.py | 24 +++---- flixopt/structure.py | 63 +---------------- 5 files changed, 160 insertions(+), 81 deletions(-) diff --git a/flixopt/aggregation.py b/flixopt/aggregation.py index e558dc19b..f149d5f20 100644 --- a/flixopt/aggregation.py +++ b/flixopt/aggregation.py @@ -22,14 +22,13 @@ TSAM_AVAILABLE = False from .components import Storage -from .core import Scalar +from .core import Scalar, TimeSeriesData from .elements import Component from .flow_system import FlowSystem from .structure import ( Element, Model, SystemModel, - TimeSeriesData, ) if TYPE_CHECKING: diff --git a/flixopt/commons.py b/flixopt/commons.py index 7d03909c0..222c07324 100644 --- a/flixopt/commons.py +++ b/flixopt/commons.py @@ -18,7 +18,7 @@ from .elements import Bus, Flow from .flow_system import FlowSystem from .interface import InvestParameters, OnOffParameters, Piece, Piecewise, PiecewiseConversion, PiecewiseEffects -from .structure import TimeSeriesData +from .core import TimeSeriesData __all__ = [ 'TimeSeriesData', diff --git a/flixopt/core.py b/flixopt/core.py index 43056cedb..31738f6c7 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -37,14 +37,134 @@ class ConversionError(Exception): pass +class TimeSeriesData(xr.DataArray): + """Minimal TimeSeriesData that inherits from xr.DataArray with aggregation metadata.""" + + __slots__ = () # No additional instance attributes - everything goes in attrs + + def __init__(self, *args, agg_group: Optional[str] = None, agg_weight: Optional[float] = None, **kwargs): + """ + Args: + *args: Arguments passed to DataArray + agg_group: Aggregation group name + agg_weight: Aggregation weight (0-1) + **kwargs: Additional arguments passed to DataArray + """ + if (agg_group is not None) and (agg_weight is not None): + raise ValueError('Use either agg_group or agg_weight, not both') + + # Let xarray handle all the initialization complexity + super().__init__(*args, **kwargs) + + # Add our metadata to attrs after initialization + if agg_group is not None: + self.attrs['agg_group'] = agg_group + if agg_weight is not None: + self.attrs['agg_weight'] = agg_weight + + # Always mark as TimeSeriesData + self.attrs['__timeseries_data__'] = True + + @property + def agg_group(self) -> Optional[str]: + return self.attrs.get('agg_group') + + @property + def agg_weight(self) -> Optional[float]: + return self.attrs.get('agg_weight') + + @classmethod + def from_dataarray(cls, da: xr.DataArray, agg_group: Optional[str] = None, agg_weight: Optional[float] = None): + """Create TimeSeriesData from DataArray, extracting metadata from attrs.""" + # Get aggregation metadata from attrs or parameters + final_agg_group = agg_group if agg_group is not None else da.attrs.get('agg_group') + final_agg_weight = agg_weight if agg_weight is not None else da.attrs.get('agg_weight') + + return cls(da, agg_group=final_agg_group, agg_weight=final_agg_weight) + + @classmethod + def is_timeseries_data(cls, obj) -> bool: + """Check if an object is TimeSeriesData.""" + return isinstance(obj, xr.DataArray) and obj.attrs.get('__timeseries_data__', False) + + def __repr__(self): + agg_info = [] + if self.agg_group: + agg_info.append(f"agg_group='{self.agg_group}'") + if self.agg_weight is not None: + agg_info.append(f'agg_weight={self.agg_weight}') + + info_str = f'TimeSeriesData({", ".join(agg_info)})' if agg_info else 'TimeSeriesData' + return f'{info_str}\n{super().__repr__()}' + class DataConverter: """ Converts various data types into xarray.DataArray with a timesteps index. - Supports: scalars, arrays, Series, DataFrames, and DataArrays. + Supports: scalars, arrays, Series, DataFrames, DataArrays, and TimeSeriesData. """ + @staticmethod + def _fix_timeseries_data_indexing( + data: TimeSeriesData, timesteps: pd.DatetimeIndex, dims: list, coords: list + ) -> TimeSeriesData: + """ + Fix TimeSeriesData indexing issues and return properly indexed TimeSeriesData. + + Args: + data: TimeSeriesData that might have indexing issues + timesteps: Target timesteps + dims: Expected dimensions + coords: Expected coordinates + + Returns: + TimeSeriesData with correct indexing + + Raises: + ConversionError: If data cannot be fixed to match expected indexing + """ + expected_shape = (len(timesteps),) + + # Check if dimensions match + if data.dims != tuple(dims): + logger.warning(f'TimeSeriesData has dimensions {data.dims}, expected {dims}. Reshaping to match timesteps.') + # Try to reshape the data to match expected dimensions + if data.size != len(timesteps): + raise ConversionError( + f'TimeSeriesData has {data.size} elements, cannot reshape to match {len(timesteps)} timesteps' + ) + # Create new DataArray with correct coordinates, preserving metadata + reshaped_data = xr.DataArray( + data.values.reshape(expected_shape), coords=coords, dims=dims, name=data.name, attrs=data.attrs.copy() + ) + return TimeSeriesData(reshaped_data) + + # Check if time coordinate length matches + elif data.sizes[dims[0]] != len(coords[0]): + logger.warning( + f'TimeSeriesData has {data.sizes[dims[0]]} time points, ' + f"expected {len(coords[0])}. Cannot reindex - lengths don't match." + ) + raise ConversionError( + f"TimeSeriesData length {data.sizes[dims[0]]} doesn't match expected {len(coords[0])}" + ) + + # Check if time coordinates are identical + elif not data.coords['time'].equals(timesteps): + logger.warning( + f'TimeSeriesData has different time coordinates than expected. Replacing with provided timesteps.' + ) + # Replace time coordinates while preserving data and metadata + recoordinated_data = xr.DataArray( + data.values, coords=coords, dims=dims, name=data.name, attrs=data.attrs.copy() + ) + return TimeSeriesData(recoordinated_data) + + else: + # Everything matches - return copy to avoid modifying original + return data.copy(deep=True) + @staticmethod def to_dataarray(data: NumericData, timesteps: pd.DatetimeIndex) -> xr.DataArray: """Convert data to xarray.DataArray with specified timesteps index.""" @@ -58,24 +178,38 @@ def to_dataarray(data: NumericData, timesteps: pd.DatetimeIndex) -> xr.DataArray expected_shape = (len(timesteps),) try: - if isinstance(data, (int, float, np.integer, np.floating)): - return xr.DataArray(data, coords=coords, dims=dims) + # Handle TimeSeriesData first (before generic DataArray check) + if isinstance(data, TimeSeriesData): + return DataConverter._fix_timeseries_data_indexing(data, timesteps, dims, coords) + + elif isinstance(data, TimeSeries): + # Handle TimeSeries objects (your existing logic) + pass # Add your TimeSeries handling here + + elif isinstance(data, (int, float, np.integer, np.floating)): + # Scalar: broadcast to all timesteps + scalar_data = np.full(expected_shape, data) + return xr.DataArray(scalar_data, coords=coords, dims=dims) + elif isinstance(data, pd.DataFrame): if not data.index.equals(timesteps): raise ConversionError("DataFrame index doesn't match timesteps index") if not len(data.columns) == 1: raise ConversionError('DataFrame must have exactly one column') return xr.DataArray(data.values.flatten(), coords=coords, dims=dims) + elif isinstance(data, pd.Series): if not data.index.equals(timesteps): raise ConversionError("Series index doesn't match timesteps index") return xr.DataArray(data.values, coords=coords, dims=dims) + elif isinstance(data, np.ndarray): if data.ndim != 1: raise ConversionError(f'Array must be 1-dimensional, got {data.ndim}') elif data.shape[0] != expected_shape[0]: raise ConversionError(f"Array shape {data.shape} doesn't match expected {expected_shape}") return xr.DataArray(data, coords=coords, dims=dims) + elif isinstance(data, xr.DataArray): if data.dims != tuple(dims): raise ConversionError(f"DataArray dimensions {data.dims} don't match expected {dims}") @@ -84,15 +218,20 @@ def to_dataarray(data: NumericData, timesteps: pd.DatetimeIndex) -> xr.DataArray f"DataArray length {data.sizes[dims[0]]} doesn't match expected {len(coords[0])}" ) return data.copy(deep=True) + elif isinstance(data, list): - logger.warning(f'Converting list to DataArray. This is not reccomended.') + logger.warning(f'Converting list to DataArray. This is not recommended.') + if len(data) != expected_shape[0]: + raise ConversionError(f"List length {len(data)} doesn't match expected {expected_shape[0]}") return xr.DataArray(data, coords=coords, dims=dims) + else: raise ConversionError(f'Unsupported type: {type(data).__name__}') + except Exception as e: if isinstance(e, ConversionError): raise - raise ConversionError(f'Converting data {type(data)} to xarray.Dataset raised an error: {str(e)}') from e + raise ConversionError(f'Converting data {type(data)} to xarray.DataArray raised an error: {str(e)}') from e class TimeSeries: diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 097b3af83..48b9d5296 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -16,10 +16,10 @@ from rich.pretty import Pretty from . import io as fx_io -from .core import NumericData, NumericDataTS, TimeSeriesCollection, TimeSeries +from .core import NumericData, NumericDataTS, TimeSeriesCollection, TimeSeries, DataConverter, ConversionError, TimeSeriesData from .effects import Effect, EffectCollection, EffectTimeSeries, EffectValuesDict, EffectValuesUser from .elements import Bus, Component, Flow -from .structure import CLASS_REGISTRY, Element, SystemModel, get_compact_representation, get_str_representation, TimeSeriesData +from .structure import CLASS_REGISTRY, Element, SystemModel, get_compact_representation, get_str_representation if TYPE_CHECKING: import pyvis @@ -394,18 +394,16 @@ def create_time_series( # Choose appropriate timesteps target_timesteps = self.timesteps_extra if needs_extra_timestep else self.timesteps - if isinstance(data, TimeSeries): - # Extract the data and rename - return data.selected_data.rename(name) - elif isinstance(data, TimeSeriesData): - # Convert TimeSeriesData to DataArray - from .core import DataConverter # Assuming this exists - - return DataConverter.to_dataarray(data.data, timesteps=target_timesteps).rename(name) + if isinstance(data, TimeSeriesData): + try: + return TimeSeriesData( + DataConverter.to_dataarray(data, timesteps=target_timesteps), + agg_group=data.agg_group, agg_weight=data.agg_weight + ).rename(name) + except ConversionError as e: + logger.critical(f'Could not convert time series data "{name}" to DataArray: {e}. \n' + f'Take care to use the correct (time) index.') else: - # Convert other data types to DataArray - from .core import DataConverter # Assuming this exists - return DataConverter.to_dataarray(data, timesteps=target_timesteps).rename(name) def create_effect_time_series( diff --git a/flixopt/structure.py b/flixopt/structure.py index 166d2182c..e39a7d0ac 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -165,7 +165,9 @@ def _extract_dataarrays_recursive(self, obj) -> Tuple[Any, Dict[str, xr.DataArra # Handle DataArrays directly - use their unique name if isinstance(obj, xr.DataArray): if not obj.name: - raise ValueError('DataArray must have a unique name for serialization') + raise ValueError(f'DataArrays must have a unique name for serialization. Unnamed DataArrays are not supported. {obj}') + if obj.name in extracted_arrays: + raise ValueError(f' must have a unique name for serialization. "{obj.name}" is a duplicate. {obj}') extracted_arrays[obj.name] = obj return f':::{obj.name}', extracted_arrays @@ -664,65 +666,6 @@ def results_structure(self): } -class TimeSeriesData(xr.DataArray): - """Minimal TimeSeriesData that inherits from xr.DataArray with aggregation metadata.""" - - def __init__(self, *args, agg_group: Optional[str] = None, agg_weight: Optional[float] = None, **kwargs): - """ - Args: - *args: Arguments passed to DataArray - agg_group: Aggregation group name - agg_weight: Aggregation weight (0-1) - **kwargs: Additional arguments passed to DataArray - """ - if (agg_group is not None) and (agg_weight is not None): - raise ValueError('Use either agg_group or agg_weight, not both') - - # Let xarray handle all the initialization complexity - super().__init__(*args, **kwargs) - - # Add our metadata to attrs after initialization - if agg_group is not None: - self.attrs['agg_group'] = agg_group - if agg_weight is not None: - self.attrs['agg_weight'] = agg_weight - - # Always mark as TimeSeriesData - self.attrs['__timeseries_data__'] = True - - @property - def agg_group(self) -> Optional[str]: - return self.attrs.get('agg_group') - - @property - def agg_weight(self) -> Optional[float]: - return self.attrs.get('agg_weight') - - @classmethod - def from_dataarray(cls, da: xr.DataArray, agg_group: Optional[str] = None, agg_weight: Optional[float] = None): - """Create TimeSeriesData from DataArray, extracting metadata from attrs.""" - # Get aggregation metadata from attrs or parameters - final_agg_group = agg_group if agg_group is not None else da.attrs.get('agg_group') - final_agg_weight = agg_weight if agg_weight is not None else da.attrs.get('agg_weight') - - return cls(da, agg_group=final_agg_group, agg_weight=final_agg_weight) - - @classmethod - def is_timeseries_data(cls, obj) -> bool: - """Check if an object is TimeSeriesData.""" - return isinstance(obj, xr.DataArray) and obj.attrs.get('__timeseries_data__', False) - - def __repr__(self): - agg_info = [] - if self.agg_group: - agg_info.append(f"agg_group='{self.agg_group}'") - if self.agg_weight is not None: - agg_info.append(f'agg_weight={self.agg_weight}') - - info_str = f'TimeSeriesData({", ".join(agg_info)})' if agg_info else 'TimeSeriesData' - return f'{info_str}\n{super().__repr__()}' - - def copy_and_convert_datatypes(data: Any, use_numpy: bool = True, use_element_label: bool = False) -> Any: """ Converts values in a nested data structure into JSON-compatible types while preserving or transforming numpy arrays From 387cac64cd0e874788ad16edca1ed77a774b7e26 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 24 Jun 2025 10:31:04 +0200 Subject: [PATCH 070/448] Adjust IO to account for attrs of DataArrays in a Dataset --- flixopt/io.py | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/flixopt/io.py b/flixopt/io.py index 1376cafae..23b06cacd 100644 --- a/flixopt/io.py +++ b/flixopt/io.py @@ -206,7 +206,7 @@ def save_dataset_to_netcdf( compression: int = 0, ) -> None: """ - Save a dataset to a netcdf file. Store the attrs as a json string in the 'attrs' attribute. + Save a dataset to a netcdf file. Store all attrs as JSON strings in 'attrs' attributes. Args: ds: Dataset to save. @@ -216,6 +216,7 @@ def save_dataset_to_netcdf( Raises: ValueError: If the path has an invalid file extension. """ + path = pathlib.Path(path) if path.suffix not in ['.nc', '.nc4']: raise ValueError(f'Invalid file extension for path {path}. Only .nc and .nc4 are supported') @@ -228,8 +229,20 @@ def save_dataset_to_netcdf( 'Dataset was exported without compression due to missing dependency "netcdf4".' 'Install netcdf4 via `pip install netcdf4`.' ) + ds = ds.copy(deep=True) ds.attrs = {'attrs': json.dumps(ds.attrs)} + + # Convert all DataArray attrs to JSON strings + for var_name, data_var in ds.data_vars.items(): + if data_var.attrs: # Only if there are attrs + ds[var_name].attrs = {'attrs': json.dumps(data_var.attrs)} + + # Also handle coordinate attrs if they exist + for coord_name, coord_var in ds.coords.items(): + if hasattr(coord_var, 'attrs') and coord_var.attrs: + ds[coord_name].attrs = {'attrs': json.dumps(coord_var.attrs)} + ds.to_netcdf( path, encoding=None @@ -240,16 +253,30 @@ def save_dataset_to_netcdf( def load_dataset_from_netcdf(path: Union[str, pathlib.Path]) -> xr.Dataset: """ - Load a dataset from a netcdf file. Load the attrs from the 'attrs' attribute. + Load a dataset from a netcdf file. Load all attrs from 'attrs' attributes. Args: path: Path to load the dataset from. Returns: - Dataset: Loaded dataset. + Dataset: Loaded dataset with restored attrs. """ ds = xr.load_dataset(path) - ds.attrs = json.loads(ds.attrs['attrs']) + + # Restore Dataset attrs + if 'attrs' in ds.attrs: + ds.attrs = json.loads(ds.attrs['attrs']) + + # Restore DataArray attrs + for var_name, data_var in ds.data_vars.items(): + if 'attrs' in data_var.attrs: + ds[var_name].attrs = json.loads(data_var.attrs['attrs']) + + # Restore coordinate attrs + for coord_name, coord_var in ds.coords.items(): + if hasattr(coord_var, 'attrs') and 'attrs' in coord_var.attrs: + ds[coord_name].attrs = json.loads(coord_var.attrs['attrs']) + return ds From 27734cf67a3ac69a3d4977dfdc477898d956bf98 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 24 Jun 2025 10:42:24 +0200 Subject: [PATCH 071/448] Rename transforming and connection methods in FlowSystem --- flixopt/calculation.py | 7 ++----- flixopt/flow_system.py | 40 +++++++++++++++++++++++++++------------- 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/flixopt/calculation.py b/flixopt/calculation.py index 8439142c1..e477f6c11 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -136,7 +136,7 @@ class for defined way of solving a flow_system optimization def do_modeling(self) -> SystemModel: t_start = timeit.default_timer() - self._activate_time_series() + self.flow_system.connect_and_transform() self.model = self.flow_system.create_model() self.model.do_modeling() @@ -181,9 +181,6 @@ def solve(self, solver: _Solver, log_file: Optional[pathlib.Path] = None, log_ma self.results = CalculationResults.from_calculation(self) - def _activate_time_series(self): - self.flow_system.transform_data() - class AggregatedCalculation(FullCalculation): """ @@ -221,7 +218,7 @@ def __init__( def do_modeling(self) -> SystemModel: t_start = timeit.default_timer() - self._activate_time_series() + self.flow_system.connect_and_transform() self._perform_aggregation() # Model the System diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 48b9d5296..ed374319d 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -62,7 +62,7 @@ def __init__( self.effects: EffectCollection = EffectCollection() self.model: Optional[SystemModel] = None - self._connected = False + self._connected_and_transformed = False @staticmethod def _validate_timesteps(timesteps: pd.DatetimeIndex) -> pd.DatetimeIndex: @@ -223,6 +223,10 @@ def to_dataset(self, constants_in_dataset: bool = True) -> xr.Dataset: Returns: xr.Dataset: Dataset containing all DataArrays with structure in attributes """ + if not self._connected_and_transformed: + logger.warning('FlowSystem is not connected_and_transformed..') + self.connect_and_transform() + reference_structure, extracted_arrays = self._create_reference_structure() # Create the dataset with extracted arrays as variables and structure as attrs @@ -234,6 +238,10 @@ def to_dict(self, data_mode: Literal['data', 'name', 'stats'] = 'data') -> Dict: Convert the object to a dictionary representation. Now builds on the reference structure for consistency. """ + if not self._connected_and_transformed: + logger.warning('FlowSystem is not connected. Calling connect() now.') + self.connect_and_transform() + reference_structure, _ = self._create_reference_structure() if data_mode == 'data': @@ -333,7 +341,7 @@ def from_dict(cls, data: Dict) -> 'FlowSystem': for component in resolved_data.get('components', {}).values(): flow_system.add_elements(component) - flow_system.transform_data() + flow_system.connect_and_transform() return flow_system @classmethod @@ -353,6 +361,10 @@ def to_netcdf(self, path: Union[str, pathlib.Path], compression: int = 0, consta compression: The compression level to use when saving the file. constants_in_dataset: If True, constants are included as Dataset variables. """ + if not self._connected_and_transformed: + logger.warning('FlowSystem is not connected. Calling connect() now.') + self.connect_and_transform() + ds = self.to_dataset(constants_in_dataset=constants_in_dataset) fx_io.save_dataset_to_netcdf(ds, path, compression=compression) logger.info(f'Saved FlowSystem to {path}') @@ -365,6 +377,9 @@ def to_json(self, path: Union[str, pathlib.Path]): Args: path: The path to the JSON file. """ + if not self._connected_and_transformed: + logger.warning('FlowSystem needs to be connected and transformed before saving to JSON. Calling connect_and_transform() now.') + self.connect_and_transform() # Use the stats mode for JSON export (cleaner output) data = get_compact_representation(self.to_dict('stats')) with open(path, 'w', encoding='utf-8') as f: @@ -425,12 +440,12 @@ def create_effect_time_series( for effect, value in effect_values_dict.items() } - def transform_data(self): + def connect_and_transform(self): """Transform data for all elements using the new simplified approach.""" - if not self._connected: + if not self._connected_and_transformed: self._connect_network() - for element in self.all_elements.values(): - element.transform_data(self) + for element in self.all_elements.values(): + element.transform_data(self) def add_elements(self, *elements: Element) -> None: """ @@ -440,12 +455,12 @@ def add_elements(self, *elements: Element) -> None: *elements: childs of Element like Boiler, HeatPump, Bus,... modeling Elements """ - if self._connected: + if self._connected_and_transformed: warnings.warn( 'You are adding elements to an already connected FlowSystem. This is not recommended (But it works).', stacklevel=2, ) - self._connected = False + self._connected_and_transformed = False for new_element in list(elements): if isinstance(new_element, Component): self._add_components(new_element) @@ -459,8 +474,8 @@ def add_elements(self, *elements: Element) -> None: ) def create_model(self) -> SystemModel: - if not self._connected: - raise RuntimeError('FlowSystem is not connected. Call FlowSystem.connect() first.') + if not self._connected_and_transformed: + raise RuntimeError('FlowSystem is not connected_and_transformed. Call FlowSystem.connect_and_transform() first.') self.model = SystemModel(self) return self.model @@ -484,8 +499,8 @@ def plot_network( return plotting.plot_network(node_infos, edge_infos, path, controls, show) def network_infos(self) -> Tuple[Dict[str, Dict[str, str]], Dict[str, Dict[str, str]]]: - if not self._connected: - self._connect_network() + if not self._connected_and_transformed: + self.connect_and_transform() nodes = { node.label_full: { 'label': node.label, @@ -568,7 +583,6 @@ def _connect_network(self): f'Connected {len(self.buses)} Buses and {len(self.components)} ' f'via {len(self.flows)} Flows inside the FlowSystem.' ) - self._connected = True def __repr__(self): return f'<{self.__class__.__name__} with {len(self.components)} components and {len(self.effects)} effects>' From 4915b81f876e30578f977ea201ac2de030510d70 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 24 Jun 2025 11:27:00 +0200 Subject: [PATCH 072/448] Compacted IO methods --- flixopt/flow_system.py | 114 ++++++++++++++--------------------- flixopt/linear_converters.py | 4 +- flixopt/results.py | 2 +- flixopt/structure.py | 23 ------- 4 files changed, 47 insertions(+), 96 deletions(-) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index ed374319d..3737c6e58 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -129,7 +129,7 @@ def _create_reference_structure(self) -> Tuple[Dict, Dict[str, xr.DataArray]]: # Extract from components components_structure = {} for comp_label, component in self.components.items(): - comp_structure, comp_arrays = self._extract_from_interface(component) + comp_structure, comp_arrays = component._create_reference_structure() all_extracted_arrays.update(comp_arrays) components_structure[comp_label] = comp_structure reference_structure['components'] = components_structure @@ -137,7 +137,7 @@ def _create_reference_structure(self) -> Tuple[Dict, Dict[str, xr.DataArray]]: # Extract from buses buses_structure = {} for bus_label, bus in self.buses.items(): - bus_structure, bus_arrays = self._extract_from_interface(bus) + bus_structure, bus_arrays = bus._create_reference_structure() all_extracted_arrays.update(bus_arrays) buses_structure[bus_label] = bus_structure reference_structure['buses'] = buses_structure @@ -145,22 +145,13 @@ def _create_reference_structure(self) -> Tuple[Dict, Dict[str, xr.DataArray]]: # Extract from effects effects_structure = {} for effect in self.effects: - effect_structure, effect_arrays = self._extract_from_interface(effect) + effect_structure, effect_arrays = effect._create_reference_structure() all_extracted_arrays.update(effect_arrays) effects_structure[effect.label] = effect_structure reference_structure['effects'] = effects_structure return reference_structure, all_extracted_arrays - def _extract_from_interface(self, interface_obj) -> Tuple[Dict, Dict[str, xr.DataArray]]: - """Extract arrays from an Interface object using its reference system.""" - if hasattr(interface_obj, '_create_reference_structure'): - return interface_obj._create_reference_structure() - else: - # Fallback for objects that don't have the new Interface methods - logger.warning(f"Object {interface_obj} doesn't have _create_reference_structure method") - return interface_obj.to_dict(), {} - @classmethod def _resolve_reference_structure(cls, structure, arrays_dict: Dict[str, xr.DataArray]): """ @@ -212,7 +203,7 @@ def _resolve_reference_structure(cls, structure, arrays_dict: Dict[str, xr.DataA else: return structure - def to_dataset(self, constants_in_dataset: bool = True) -> xr.Dataset: + def to_dataset(self) -> xr.Dataset: """ Convert the FlowSystem to an xarray Dataset using the Interface pattern. All DataArrays become dataset variables, structure goes to attrs. @@ -233,25 +224,6 @@ def to_dataset(self, constants_in_dataset: bool = True) -> xr.Dataset: ds = xr.Dataset(extracted_arrays, attrs=reference_structure) return ds - def to_dict(self, data_mode: Literal['data', 'name', 'stats'] = 'data') -> Dict: - """ - Convert the object to a dictionary representation. - Now builds on the reference structure for consistency. - """ - if not self._connected_and_transformed: - logger.warning('FlowSystem is not connected. Calling connect() now.') - self.connect_and_transform() - - reference_structure, _ = self._create_reference_structure() - - if data_mode == 'data': - return reference_structure - elif data_mode == 'stats': - # For stats mode, we might want to process the structure further - return fx_io.remove_none_and_empty(reference_structure) - else: # name mode - return reference_structure - @classmethod def from_dataset(cls, ds: xr.Dataset) -> 'FlowSystem': """ @@ -310,39 +282,22 @@ def from_dataset(cls, ds: xr.Dataset) -> 'FlowSystem': return flow_system - @classmethod - def from_dict(cls, data: Dict) -> 'FlowSystem': + def to_netcdf(self, path: Union[str, pathlib.Path], compression: int = 0): """ - Load a FlowSystem from a dictionary using the Interface pattern. + Save the FlowSystem to a NetCDF file using the Interface pattern. Args: - data: Dictionary containing the FlowSystem data. + path: The path to the netCDF file. + compression: The compression level to use when saving the file. + constants_in_dataset: If True, constants are included as Dataset variables. """ - # For dict format, resolve with empty arrays (references may not be used) - resolved_data = cls._resolve_reference_structure(data, {}) - - # Extract constructor parameters - timesteps_extra = pd.DatetimeIndex(resolved_data['timesteps_extra'], name='time') - hours_of_last_timestep = float((timesteps_extra[-1] - timesteps_extra[-2]) / pd.Timedelta(hours=1)) - - flow_system = cls( - timesteps=timesteps_extra[:-1], - hours_of_last_timestep=hours_of_last_timestep, - hours_of_previous_timesteps=resolved_data['hours_of_previous_timesteps'], - ) - - # Add elements using resolved data - for bus in resolved_data.get('buses', {}).values(): - flow_system.add_elements(bus) - - for effect in resolved_data.get('effects', {}).values(): - flow_system.add_elements(effect) - - for component in resolved_data.get('components', {}).values(): - flow_system.add_elements(component) + if not self._connected_and_transformed: + logger.warning('FlowSystem is not connected. Calling connect() now.') + self.connect_and_transform() - flow_system.connect_and_transform() - return flow_system + ds = self.to_dataset() + fx_io.save_dataset_to_netcdf(ds, path, compression=compression) + logger.info(f'Saved FlowSystem to {path}') @classmethod def from_netcdf(cls, path: Union[str, pathlib.Path]) -> 'FlowSystem': @@ -352,22 +307,22 @@ def from_netcdf(cls, path: Union[str, pathlib.Path]) -> 'FlowSystem': ds = fx_io.load_dataset_from_netcdf(path) return cls.from_dataset(ds) - def to_netcdf(self, path: Union[str, pathlib.Path], compression: int = 0, constants_in_dataset: bool = True): + def get_structure(self, clean: bool = False) -> Dict: """ - Save the FlowSystem to a NetCDF file using the Interface pattern. + Get FlowSystem structure. Args: - path: The path to the netCDF file. - compression: The compression level to use when saving the file. - constants_in_dataset: If True, constants are included as Dataset variables. + clean: If True, remove None and empty dicts and lists. """ if not self._connected_and_transformed: logger.warning('FlowSystem is not connected. Calling connect() now.') self.connect_and_transform() - ds = self.to_dataset(constants_in_dataset=constants_in_dataset) - fx_io.save_dataset_to_netcdf(ds, path, compression=compression) - logger.info(f'Saved FlowSystem to {path}') + reference_structure, _ = self._create_reference_structure() + if clean: + return fx_io.remove_none_and_empty(reference_structure) + else: + return reference_structure def to_json(self, path: Union[str, pathlib.Path]): """ @@ -381,7 +336,7 @@ def to_json(self, path: Union[str, pathlib.Path]): logger.warning('FlowSystem needs to be connected and transformed before saving to JSON. Calling connect_and_transform() now.') self.connect_and_transform() # Use the stats mode for JSON export (cleaner output) - data = get_compact_representation(self.to_dict('stats')) + data = get_compact_representation(self.get_structure(clean=True)) with open(path, 'w', encoding='utf-8') as f: json.dump(data, f, indent=4, ensure_ascii=False) @@ -446,6 +401,7 @@ def connect_and_transform(self): self._connect_network() for element in self.all_elements.values(): element.transform_data(self) + self._connected_and_transformed = True def add_elements(self, *elements: Element) -> None: """ @@ -590,10 +546,28 @@ def __repr__(self): def __str__(self): with StringIO() as output_buffer: console = Console(file=output_buffer, width=1000) # Adjust width as needed - console.print(Pretty(self.to_dict('stats'), expand_all=True, indent_guides=True)) + console.print(Pretty(self.get_structure(clean=True), expand_all=True, indent_guides=True)) value = output_buffer.getvalue() return value + def __eq__(self, other: 'FlowSystem'): + """Check if two FlowSystems are equal by comparing their dataset representations.""" + if not isinstance(other, FlowSystem): + raise NotImplementedError('Comparison with other types is not implemented for class FlowSystem') + + ds_me = self.to_dataset() + ds_other = other.to_dataset() + + try: + xr.testing.assert_equal(ds_me, ds_other) + except AssertionError: + return False + + if ds_me.attrs != ds_other.attrs: + return False + + return True + @property def flows(self) -> Dict[str, Flow]: set_of_flows = {flow for comp in self.components.values() for flow in comp.inputs + comp.outputs} diff --git a/flixopt/linear_converters.py b/flixopt/linear_converters.py index 83527fef0..3fd032632 100644 --- a/flixopt/linear_converters.py +++ b/flixopt/linear_converters.py @@ -8,10 +8,10 @@ import numpy as np from .components import LinearConverter -from .core import NumericDataTS +from .core import NumericDataTS, TimeSeriesData from .elements import Flow from .interface import OnOffParameters -from .structure import register_class_for_io, TimeSeriesData +from .structure import register_class_for_io logger = logging.getLogger('flixopt') diff --git a/flixopt/results.py b/flixopt/results.py index 232aaf5af..e13cb0785 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -118,7 +118,7 @@ def from_calculation(cls, calculation: 'Calculation'): """ return cls( solution=calculation.model.solution, - flow_system=calculation.flow_system.to_dataset(constants_in_dataset=True), + flow_system=calculation.flow_system.to_dataset(), summary=calculation.summary, model=calculation.model, name=calculation.name, diff --git a/flixopt/structure.py b/flixopt/structure.py index e39a7d0ac..10ab7ad8c 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -303,17 +303,6 @@ def to_dataset(self) -> xr.Dataset: ds = xr.Dataset(extracted_arrays, attrs=reference_structure) return ds - def to_dict(self) -> Dict: - """ - Convert the object to a dictionary representation. - DataArrays/TimeSeries are converted to references, but structure is preserved. - - Returns: - Dict: Dictionary with references to DataArrays/TimeSeries - """ - reference_structure, _ = self._create_reference_structure() - return reference_structure - def infos(self, use_numpy: bool = True, use_element_label: bool = False) -> Dict: """ Generate a dictionary representation of the object's constructor arguments. @@ -362,18 +351,6 @@ def _apply_element_label_preference(self, obj): else: return obj - def to_json(self, path: Union[str, pathlib.Path]): - """ - Save the element to a JSON file for documentation purposes. - Uses the infos() method for consistent representation. - - Args: - path: The path to the JSON file. - """ - data = get_compact_representation(self.infos(use_numpy=False, use_element_label=True)) - with open(path, 'w', encoding='utf-8') as f: - json.dump(data, f, indent=4, ensure_ascii=False) - def to_netcdf(self, path: Union[str, pathlib.Path], compression: int = 0): """ Save the object to a NetCDF file. From fc5549a20a6d1e6ebc35d760b72a524ad18457fc Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 24 Jun 2025 11:33:41 +0200 Subject: [PATCH 073/448] Remove infos() --- flixopt/elements.py | 11 ----------- flixopt/structure.py | 35 ----------------------------------- 2 files changed, 46 deletions(-) diff --git a/flixopt/elements.py b/flixopt/elements.py index 3ea29a09f..ba74030cb 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -72,12 +72,6 @@ def transform_data(self, flow_system: 'FlowSystem') -> None: if self.on_off_parameters is not None: self.on_off_parameters.transform_data(flow_system, self.label_full) - def infos(self, use_numpy=True, use_element_label: bool = False) -> Dict: - infos = super().infos(use_numpy, use_element_label) - infos['inputs'] = [flow.infos(use_numpy, use_element_label) for flow in self.inputs] - infos['outputs'] = [flow.infos(use_numpy, use_element_label) for flow in self.outputs] - return infos - def _check_unique_flow_labels(self): all_flow_labels = [flow.label for flow in self.inputs + self.outputs] @@ -253,11 +247,6 @@ def transform_data(self, flow_system: 'FlowSystem'): if isinstance(self.size, InvestParameters): self.size.transform_data(flow_system) - def infos(self, use_numpy: bool = True, use_element_label: bool = False) -> Dict: - infos = super().infos(use_numpy, use_element_label) - infos['is_input_in_component'] = self.is_input_in_component - return infos - def to_dict(self) -> Dict: data = super().to_dict() if isinstance(data.get('previous_flow_rate'), np.ndarray): diff --git a/flixopt/structure.py b/flixopt/structure.py index 10ab7ad8c..abcfdf9d2 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -303,41 +303,6 @@ def to_dataset(self) -> xr.Dataset: ds = xr.Dataset(extracted_arrays, attrs=reference_structure) return ds - def infos(self, use_numpy: bool = True, use_element_label: bool = False) -> Dict: - """ - Generate a dictionary representation of the object's constructor arguments. - Built on top of dataset creation for better consistency and analytics capabilities. - - Args: - use_numpy: Whether to convert NumPy arrays to lists. Defaults to True. - If True, numeric numpy arrays are preserved as-is. - If False, they are converted to lists. - use_element_label: Whether to use element labels instead of full infos for nested objects. - - Returns: - A dictionary representation optimized for documentation and analysis. - """ - # Get the core dataset representation - ds = self.to_dataset() - - # Start with the reference structure from attrs - info_dict = dict(ds.attrs) - - # Process DataArrays in the dataset based on preferences - for var_name, data_array in ds.data_vars.items(): - if use_numpy: - # Keep as DataArray/numpy for analysis - info_dict[f'_data_{var_name}'] = data_array - else: - # Convert to lists for JSON compatibility - info_dict[f'_data_{var_name}'] = data_array.values.tolist() - - # Apply element label preference to nested structures - if use_element_label: - info_dict = self._apply_element_label_preference(info_dict) - - return info_dict - def _apply_element_label_preference(self, obj): """Apply element label preference to nested structures.""" if isinstance(obj, dict): From 299ff433e31682088a91f51f4e1669c513236a95 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 24 Jun 2025 11:35:15 +0200 Subject: [PATCH 074/448] remove from_dict() and to_dict() --- flixopt/elements.py | 6 ------ flixopt/structure.py | 31 ------------------------------- 2 files changed, 37 deletions(-) diff --git a/flixopt/elements.py b/flixopt/elements.py index ba74030cb..48e73ef76 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -247,12 +247,6 @@ def transform_data(self, flow_system: 'FlowSystem'): if isinstance(self.size, InvestParameters): self.size.transform_data(flow_system) - def to_dict(self) -> Dict: - data = super().to_dict() - if isinstance(data.get('previous_flow_rate'), np.ndarray): - data['previous_flow_rate'] = data['previous_flow_rate'].tolist() - return data - def _plausibility_checks(self) -> None: # TODO: Incorporate into Variable? (Lower_bound can not be greater than upper bound if np.any(self.relative_minimum > self.relative_maximum): diff --git a/flixopt/structure.py b/flixopt/structure.py index abcfdf9d2..4f94073e7 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -303,19 +303,6 @@ def to_dataset(self) -> xr.Dataset: ds = xr.Dataset(extracted_arrays, attrs=reference_structure) return ds - def _apply_element_label_preference(self, obj): - """Apply element label preference to nested structures.""" - if isinstance(obj, dict): - if obj.get('__class__') and 'label' in obj: - # This looks like an Interface with a label - return just the label - return obj.get('label', obj.get('__class__')) - else: - return {k: self._apply_element_label_preference(v) for k, v in obj.items()} - elif isinstance(obj, list): - return [self._apply_element_label_preference(item) for item in obj] - else: - return obj - def to_netcdf(self, path: Union[str, pathlib.Path], compression: int = 0): """ Save the object to a NetCDF file. @@ -375,24 +362,6 @@ def from_netcdf(cls, path: Union[str, pathlib.Path]) -> 'Interface': ds = fx_io.load_dataset_from_netcdf(path) return cls.from_dataset(ds) - @classmethod - def from_dict(cls, data: Dict) -> 'Interface': - """ - Create an instance from a dictionary representation. - This is now a thin wrapper around the reference resolution system. - - Args: - data: Dictionary containing the data for the object. - """ - class_name = data.pop('__class__', None) - if class_name and class_name != cls.__name__: - logger.warning(f"Dict class '{class_name}' doesn't match target class '{cls.__name__}'") - - # Since dict format doesn't separate arrays, resolve with empty arrays dict - # References in dict format would need to be handled differently if they exist - resolved_params = cls._resolve_reference_structure(data, {}) - return cls(**resolved_params) - def __repr__(self): # Get the constructor arguments and their current values init_signature = inspect.signature(self.__init__) From abc22b108b207072a53d8de4ac50b89c803e72ca Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 24 Jun 2025 11:39:22 +0200 Subject: [PATCH 075/448] Update __str__ of Interface --- flixopt/structure.py | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/flixopt/structure.py b/flixopt/structure.py index 4f94073e7..d19e371d1 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -20,6 +20,7 @@ from .config import CONFIG from .core import NumericData, Scalar, TimeSeriesCollection, TimeSeries +from . import io as fx_io if TYPE_CHECKING: # for type checking and preventing circular imports from .effects import EffectCollectionModel @@ -311,8 +312,6 @@ def to_netcdf(self, path: Union[str, pathlib.Path], compression: int = 0): path: Path to save the NetCDF file compression: Compression level (0-9) """ - from . import io as fx_io # Assuming fx_io is available - ds = self.to_dataset() fx_io.save_dataset_to_netcdf(ds, path, compression=compression) @@ -357,11 +356,35 @@ def from_netcdf(cls, path: Union[str, pathlib.Path]) -> 'Interface': Returns: Interface instance """ - from . import io as fx_io # Assuming fx_io is available - ds = fx_io.load_dataset_from_netcdf(path) return cls.from_dataset(ds) + def get_structure(self, clean: bool = False) -> Dict: + """ + Get FlowSystem structure. + + Args: + clean: If True, remove None and empty dicts and lists. + """ + + reference_structure, _ = self._create_reference_structure() + if clean: + return fx_io.remove_none_and_empty(reference_structure) + return reference_structure + + def to_json(self, path: Union[str, pathlib.Path]): + """ + Save the Element to a JSON file using the Interface pattern. + This is meant for documentation and comparison, not for reloading. + + Args: + path: The path to the JSON file. + """ + # Use the stats mode for JSON export (cleaner output) + data = get_compact_representation(self.get_structure(clean=True)) + with open(path, 'w', encoding='utf-8') as f: + json.dump(data, f, indent=4, ensure_ascii=False) + def __repr__(self): # Get the constructor arguments and their current values init_signature = inspect.signature(self.__init__) @@ -372,7 +395,7 @@ def __repr__(self): return f'{self.__class__.__name__}({args_str})' def __str__(self): - return get_str_representation(self.infos(use_numpy=True, use_element_label=True)) + return get_str_representation(self.get_structure(clean=True)) class Element(Interface): From 9b4c44c8315abf2cea89c953f96b5535158e0a2c Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 24 Jun 2025 14:01:06 +0200 Subject: [PATCH 076/448] Improve str and repr --- flixopt/flow_system.py | 41 ++++++++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 3737c6e58..f5077434d 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -540,15 +540,38 @@ def _connect_network(self): f'via {len(self.flows)} Flows inside the FlowSystem.' ) - def __repr__(self): - return f'<{self.__class__.__name__} with {len(self.components)} components and {len(self.effects)} effects>' - - def __str__(self): - with StringIO() as output_buffer: - console = Console(file=output_buffer, width=1000) # Adjust width as needed - console.print(Pretty(self.get_structure(clean=True), expand_all=True, indent_guides=True)) - value = output_buffer.getvalue() - return value + def __repr__(self) -> str: + """Compact representation for debugging.""" + status = '✓' if self._connected_and_transformed else '⚠' + return ( + f'FlowSystem({len(self.timesteps)} timesteps ' + f'[{self.timesteps[0].strftime("%Y-%m-%d")} to {self.timesteps[-1].strftime("%Y-%m-%d")}], ' + f'{len(self.components)} Components / {len(self.buses)} Buses / {len(self.effects)} Effects, {status})' + ) + + def __str__(self) -> str: + """Structured summary for users.""" + + def format_elements(parts: list, label: str): + if not parts: + return f'{label}:{"":>8} {len(parts)}' + name_list = ', '.join(parts[:3]) + if len(parts) > 3: + name_list += f' ... (+{len(parts) - 3} more)' + return f'{label}:{"":>8} {len(parts)} ({name_list})' + + lines = [ + f'FlowSystem Overview:', + f'{"─" * 50}', + f'Time period: {self.timesteps[0].date()} to {self.timesteps[-1].date()}', + f'Timesteps: {len(self.timesteps)} ({self.timesteps.freq or "irregular frequency"})', + format_elements(list(self.components), 'Components'), + format_elements(list(self.buses), 'Buses'), + format_elements(list(self.effects.effects), 'Effects'), + f'Status: {"Connected & Transformed" if self._connected_and_transformed else "Not connected"}', + ] + + return '\n'.join(lines) def __eq__(self, other: 'FlowSystem'): """Check if two FlowSystems are equal by comparing their dataset representations.""" From 0ab7ea6f75d2b3b6c68339a1e770386a2f5b6f62 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 24 Jun 2025 14:03:44 +0200 Subject: [PATCH 077/448] Improve str and repr --- flixopt/flow_system.py | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index f5077434d..7d62c35ca 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -546,28 +546,32 @@ def __repr__(self) -> str: return ( f'FlowSystem({len(self.timesteps)} timesteps ' f'[{self.timesteps[0].strftime("%Y-%m-%d")} to {self.timesteps[-1].strftime("%Y-%m-%d")}], ' - f'{len(self.components)} Components / {len(self.buses)} Buses / {len(self.effects)} Effects, {status})' + f'{len(self.components)} Components, {len(self.buses)} Buses, {len(self.effects)} Effects, {status})' ) def __str__(self) -> str: """Structured summary for users.""" - def format_elements(parts: list, label: str): - if not parts: - return f'{label}:{"":>8} {len(parts)}' - name_list = ', '.join(parts[:3]) - if len(parts) > 3: - name_list += f' ... (+{len(parts) - 3} more)' - return f'{label}:{"":>8} {len(parts)} ({name_list})' + def format_elements(element_names: list, label: str, alignment: int = 12): + name_list = ', '.join(element_names[:3]) + if len(element_names) > 3: + name_list += f' ... (+{len(element_names) - 3} more)' + + suffix = f' ({name_list})' if element_names else '' + padding = alignment - len(label) - 1 # -1 for the colon + return f'{label}:{"":<{padding}} {len(element_names)}{suffix}' + + time_period = f'Time period: {self.timesteps[0].date()} to {self.timesteps[-1].date()}' + freq_str = str(self.timesteps.freq).replace('<', '').replace('>', '') if self.timesteps.freq else 'irregular' lines = [ f'FlowSystem Overview:', f'{"─" * 50}', - f'Time period: {self.timesteps[0].date()} to {self.timesteps[-1].date()}', - f'Timesteps: {len(self.timesteps)} ({self.timesteps.freq or "irregular frequency"})', - format_elements(list(self.components), 'Components'), - format_elements(list(self.buses), 'Buses'), - format_elements(list(self.effects.effects), 'Effects'), + time_period, + f'Timesteps: {len(self.timesteps)} ({freq_str})', + format_elements(list(self.components.keys()), 'Components'), + format_elements(list(self.buses.keys()), 'Buses'), + format_elements(list(self.effects.effects.keys()), 'Effects'), f'Status: {"Connected & Transformed" if self._connected_and_transformed else "Not connected"}', ] From 1dcbbb05c25074cc4e0c4d5dd8463431f02d9527 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 24 Jun 2025 19:20:56 +0200 Subject: [PATCH 078/448] Add docstring --- flixopt/flow_system.py | 14 ++++++++++++-- flixopt/structure.py | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 7d62c35ca..4a227df9c 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -29,8 +29,18 @@ class FlowSystem: """ - A FlowSystem organizes the high level Elements (Components & Effects). - Uses xr.Dataset directly from its Interface elements instead of TimeSeriesCollection. + FlowSystem serves as the main container for energy system modeling, organizing + high-level elements including Components (like boilers, heat pumps, storages), + Buses (connection points), and Effects (system-wide influences). It handles + time series data management, network connectivity, and provides serialization + capabilities for saving and loading complete system configurations. + + The system uses xarray.Dataset for efficient time series data handling. It can be exported and restored to NETCDF. + + See Also: + Component: Base class for system components like boilers, heat pumps. + Bus: Connection points for flows between components. + Effect: System-wide effects, like the optimization objective. """ def __init__( diff --git a/flixopt/structure.py b/flixopt/structure.py index d19e371d1..7dc19318d 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -19,7 +19,7 @@ from rich.pretty import Pretty from .config import CONFIG -from .core import NumericData, Scalar, TimeSeriesCollection, TimeSeries +from .core import NumericData, Scalar, TimeSeriesCollection, TimeSeries, TimeSeriesData from . import io as fx_io if TYPE_CHECKING: # for type checking and preventing circular imports From 9aec99081486388b6152ac6ea748ec8fbf0851d6 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 24 Jun 2025 19:28:06 +0200 Subject: [PATCH 079/448] Unify IO stuff in Interface class --- flixopt/flow_system.py | 145 +++++++++++------------------------------ flixopt/structure.py | 6 ++ 2 files changed, 43 insertions(+), 108 deletions(-) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 4a227df9c..ff99725a5 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -19,7 +19,7 @@ from .core import NumericData, NumericDataTS, TimeSeriesCollection, TimeSeries, DataConverter, ConversionError, TimeSeriesData from .effects import Effect, EffectCollection, EffectTimeSeries, EffectValuesDict, EffectValuesUser from .elements import Bus, Component, Flow -from .structure import CLASS_REGISTRY, Element, SystemModel, get_compact_representation, get_str_representation +from .structure import CLASS_REGISTRY, Element, SystemModel, get_compact_representation, get_str_representation, Interface if TYPE_CHECKING: import pyvis @@ -27,7 +27,7 @@ logger = logging.getLogger('flixopt') -class FlowSystem: +class FlowSystem(Interface): """ FlowSystem serves as the main container for energy system modeling, organizing high-level elements including Components (like boilers, heat pumps, storages), @@ -44,10 +44,10 @@ class FlowSystem: """ def __init__( - self, - timesteps: pd.DatetimeIndex, - hours_of_last_timestep: Optional[float] = None, - hours_of_previous_timesteps: Optional[Union[int, float, np.ndarray]] = None, + self, + timesteps: pd.DatetimeIndex, + hours_of_last_timestep: Optional[float] = None, + hours_of_previous_timesteps: Optional[Union[int, float, np.ndarray]] = None, ): """ Args: @@ -89,7 +89,7 @@ def _validate_timesteps(timesteps: pd.DatetimeIndex) -> pd.DatetimeIndex: @staticmethod def _create_timesteps_with_extra( - timesteps: pd.DatetimeIndex, hours_of_last_timestep: Optional[float] + timesteps: pd.DatetimeIndex, hours_of_last_timestep: Optional[float] ) -> pd.DatetimeIndex: """Create timesteps with an extra step at the end.""" if hours_of_last_timestep is None: @@ -108,7 +108,7 @@ def calculate_hours_per_timestep(timesteps_extra: pd.DatetimeIndex) -> xr.DataAr @staticmethod def _calculate_hours_of_previous_timesteps( - timesteps: pd.DatetimeIndex, hours_of_previous_timesteps: Optional[Union[float, np.ndarray]] + timesteps: pd.DatetimeIndex, hours_of_previous_timesteps: Optional[Union[float, np.ndarray]] ) -> Union[float, np.ndarray]: """Calculate duration of regular timesteps.""" if hours_of_previous_timesteps is not None: @@ -119,21 +119,22 @@ def _calculate_hours_of_previous_timesteps( def _create_reference_structure(self) -> Tuple[Dict, Dict[str, xr.DataArray]]: """ - Create reference structure for FlowSystem following the Interface pattern. - Extracts all DataArrays from components, buses, and effects. + Override Interface method to handle FlowSystem-specific serialization. + Combines custom FlowSystem logic with Interface pattern for nested objects. Returns: Tuple of (reference_structure, extracted_arrays_dict) """ - reference_structure = { - '__class__': self.__class__.__name__, - 'timesteps_extra': [date.isoformat() for date in self.timesteps_extra], - 'hours_of_previous_timesteps': self.hours_of_previous_timesteps, - } + # Start with Interface base functionality for constructor parameters + reference_structure, all_extracted_arrays = super()._create_reference_structure() + + # Override timesteps serialization (we need timesteps_extra instead of timesteps) + reference_structure['timesteps_extra'] = [date.isoformat() for date in self.timesteps_extra] - all_extracted_arrays = {} + # Remove timesteps from structure since we're using timesteps_extra + reference_structure.pop('timesteps', None) - # Add timing arrays directly + # Add timing arrays directly (not handled by Interface introspection) all_extracted_arrays['hours_per_timestep'] = self.hours_per_timestep # Extract from components @@ -162,64 +163,10 @@ def _create_reference_structure(self) -> Tuple[Dict, Dict[str, xr.DataArray]]: return reference_structure, all_extracted_arrays - @classmethod - def _resolve_reference_structure(cls, structure, arrays_dict: Dict[str, xr.DataArray]): - """ - Resolve reference structure back to actual objects. - Reuses the Interface pattern for consistency. - """ - if isinstance(structure, str) and structure.startswith(':::'): - # This is a reference to a DataArray - array_name = structure[3:] # Remove ":::" prefix - if array_name in arrays_dict: - #TODO: Improve this! - da = arrays_dict[array_name] - if da.isnull().any(): - logger.warning(f"DataArray '{array_name}' contains null values. Dropping them.") - return da.dropna(dim='time', how='all') - return da - else: - logger.critical(f"Referenced DataArray '{array_name}' not found in dataset") - return None - - elif isinstance(structure, list): - resolved_list = [] - for item in structure: - resolved_item = cls._resolve_reference_structure(item, arrays_dict) - if resolved_item is not None: - resolved_list.append(resolved_item) - return resolved_list - - elif isinstance(structure, dict): - # Check if this is a serialized Interface object - if structure.get('__class__') and structure['__class__'] in CLASS_REGISTRY: - # This is a nested Interface object - restore it recursively - nested_class = CLASS_REGISTRY[structure['__class__']] - # Remove the __class__ key and process the rest - nested_data = {k: v for k, v in structure.items() if k != '__class__'} - # Resolve references in the nested data - resolved_nested_data = cls._resolve_reference_structure(nested_data, arrays_dict) - # Create the nested Interface object - return nested_class(**resolved_nested_data) - else: - # Regular dictionary - resolve references in values - resolved_dict = {} - for key, value in structure.items(): - resolved_value = cls._resolve_reference_structure(value, arrays_dict) - if resolved_value is not None or value is None: - resolved_dict[key] = resolved_value - return resolved_dict - - else: - return structure - def to_dataset(self) -> xr.Dataset: """ - Convert the FlowSystem to an xarray Dataset using the Interface pattern. - All DataArrays become dataset variables, structure goes to attrs. - - Args: - constants_in_dataset: If True, constants are included as Dataset variables. + Convert the FlowSystem to an xarray Dataset. + Ensures FlowSystem is connected before serialization. Returns: xr.Dataset: Dataset containing all DataArrays with structure in attributes @@ -228,16 +175,13 @@ def to_dataset(self) -> xr.Dataset: logger.warning('FlowSystem is not connected_and_transformed..') self.connect_and_transform() - reference_structure, extracted_arrays = self._create_reference_structure() - - # Create the dataset with extracted arrays as variables and structure as attrs - ds = xr.Dataset(extracted_arrays, attrs=reference_structure) - return ds + return super().to_dataset() @classmethod def from_dataset(cls, ds: xr.Dataset) -> 'FlowSystem': """ - Create a FlowSystem from an xarray Dataset using the Interface pattern. + Create a FlowSystem from an xarray Dataset. + Handles FlowSystem-specific reconstruction logic. Args: ds: Dataset containing the FlowSystem data @@ -255,7 +199,7 @@ def from_dataset(cls, ds: xr.Dataset) -> 'FlowSystem': # Calculate hours_of_last_timestep from the timesteps hours_of_last_timestep = float((timesteps_extra[-1] - timesteps_extra[-2]) / pd.Timedelta(hours=1)) - # Create FlowSystem instance + # Create FlowSystem instance with constructor parameters flow_system = cls( timesteps=timesteps_extra[:-1], hours_of_last_timestep=hours_of_last_timestep, @@ -278,66 +222,53 @@ def from_dataset(cls, ds: xr.Dataset) -> 'FlowSystem': for bus_label, bus_data in buses_structure.items(): bus = cls._resolve_reference_structure(bus_data, arrays_dict) if not isinstance(bus, Bus): - logger.critical(f'Restoring component {bus_label} failed.') + logger.critical(f'Restoring bus {bus_label} failed.') flow_system._add_buses(bus) # Restore effects effects_structure = reference_structure.get('effects', {}) for effect_label, effect_data in effects_structure.items(): effect = cls._resolve_reference_structure(effect_data, arrays_dict) - if not isinstance(effect, Effect): - logger.critical(f'Restoring component {effect_label} failed.') + logger.critical(f'Restoring effect {effect_label} failed.') flow_system._add_effects(effect) return flow_system def to_netcdf(self, path: Union[str, pathlib.Path], compression: int = 0): """ - Save the FlowSystem to a NetCDF file using the Interface pattern. + Save the FlowSystem to a NetCDF file. + Ensures FlowSystem is connected before saving. Args: path: The path to the netCDF file. compression: The compression level to use when saving the file. - constants_in_dataset: If True, constants are included as Dataset variables. """ if not self._connected_and_transformed: - logger.warning('FlowSystem is not connected. Calling connect() now.') + logger.warning('FlowSystem is not connected. Calling connect_and_transform() now.') self.connect_and_transform() - ds = self.to_dataset() - fx_io.save_dataset_to_netcdf(ds, path, compression=compression) + super().to_netcdf(path, compression) logger.info(f'Saved FlowSystem to {path}') - @classmethod - def from_netcdf(cls, path: Union[str, pathlib.Path]) -> 'FlowSystem': - """ - Load a FlowSystem from a netcdf file using the Interface pattern. - """ - ds = fx_io.load_dataset_from_netcdf(path) - return cls.from_dataset(ds) - def get_structure(self, clean: bool = False) -> Dict: """ Get FlowSystem structure. + Ensures FlowSystem is connected before getting structure. Args: clean: If True, remove None and empty dicts and lists. """ if not self._connected_and_transformed: - logger.warning('FlowSystem is not connected. Calling connect() now.') + logger.warning('FlowSystem is not connected. Calling connect_and_transform() now.') self.connect_and_transform() - reference_structure, _ = self._create_reference_structure() - if clean: - return fx_io.remove_none_and_empty(reference_structure) - else: - return reference_structure + return super().get_structure(clean) def to_json(self, path: Union[str, pathlib.Path]): """ - Save the flow system to a JSON file using the Interface pattern. - This is meant for documentation and comparison, not for reloading. + Save the flow system to a JSON file. + Ensures FlowSystem is connected before saving. Args: path: The path to the JSON file. @@ -345,10 +276,8 @@ def to_json(self, path: Union[str, pathlib.Path]): if not self._connected_and_transformed: logger.warning('FlowSystem needs to be connected and transformed before saving to JSON. Calling connect_and_transform() now.') self.connect_and_transform() - # Use the stats mode for JSON export (cleaner output) - data = get_compact_representation(self.get_structure(clean=True)) - with open(path, 'w', encoding='utf-8') as f: - json.dump(data, f, indent=4, ensure_ascii=False) + + super().to_json(path) def create_time_series( self, diff --git a/flixopt/structure.py b/flixopt/structure.py index 7dc19318d..55a347651 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -230,6 +230,12 @@ def _resolve_reference_structure(cls, structure, arrays_dict: Dict[str, xr.DataA if array_name in arrays_dict: array = arrays_dict[array_name] + #TODO: Improve this! + if array.isnull().any(): + logger.warning(f"DataArray '{array_name}' contains null values. Dropping them.") + return array.dropna(dim='time', how='all') + return array + # Check if this should be restored as TimeSeriesData if TimeSeriesData.is_timeseries_data(array): return TimeSeriesData.from_dataarray(array) From e3703117883822044a4e5497abc012abcfc26ad3 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 24 Jun 2025 19:29:16 +0200 Subject: [PATCH 080/448] Improve test tu utilize __eq__ method --- tests/test_io.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_io.py b/tests/test_io.py index 8bcdb050e..497b334c8 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -50,11 +50,12 @@ def test_flow_system_file_io(flow_system, highs_solver): def test_flow_system_io(flow_system): - di = flow_system.to_dict() - _ = fx.FlowSystem.from_dict(di) + flow_system.to_json('fs.json') ds = flow_system.to_dataset() - _ = fx.FlowSystem.from_dataset(ds) + new_fs = fx.FlowSystem.from_dataset(ds) + + assert flow_system == new_fs print(flow_system) flow_system.__repr__() From 793e820de5cba614ca2b10fd8b4b683d60fdc412 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 24 Jun 2025 21:42:27 +0200 Subject: [PATCH 081/448] Make Interface class more robust and improve exceptions --- flixopt/structure.py | 340 +++++++++++++++++++++++++++++++------------ 1 file changed, 243 insertions(+), 97 deletions(-) diff --git a/flixopt/structure.py b/flixopt/structure.py index 55a347651..36f723ad1 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -109,12 +109,46 @@ def coords_extra(self) -> Tuple[pd.DatetimeIndex]: class Interface: """ - This class is used to collect arguments about a Model. Its the base class for all Elements and Models in flixopt. + Base class for all Elements and Models in flixopt that provides serialization capabilities. + + This class enables automatic serialization/deserialization of objects containing xarray DataArrays + and nested Interface objects to/from xarray Datasets and NetCDF files. It uses introspection + of constructor parameters to automatically handle most serialization scenarios. + + Key Features: + - Automatic extraction and restoration of xarray DataArrays + - Support for nested Interface objects + - NetCDF and JSON export/import + - Recursive handling of complex nested structures + + Subclasses must implement: + transform_data(flow_system): Transform data to match FlowSystem dimensions + + Example: + >>> class MyComponent(Interface): + ... def __init__(self, name: str, power_data: xr.DataArray): + ... self.name = name + ... self.power_data = power_data + ... + ... def transform_data(self, flow_system): + ... # Transform power_data to match flow_system timesteps + ... pass + >>> + >>> component = MyComponent('gen1', power_array) + >>> component.to_netcdf('component.nc') # Save to file + >>> restored = MyComponent.from_netcdf('component.nc') # Load from file """ def transform_data(self, flow_system: 'FlowSystem'): - """Transforms the data of the interface to match the FlowSystem's dimensions""" - raise NotImplementedError('Every Interface needs a transform_data() method') + """Transform the data of the interface to match the FlowSystem's dimensions. + + Args: + flow_system: The FlowSystem containing timing and dimensional information + + Raises: + NotImplementedError: Must be implemented by subclasses + """ + raise NotImplementedError('Every Interface subclass needs a transform_data() method') def _create_reference_structure(self) -> Tuple[Dict, Dict[str, xr.DataArray]]: """ @@ -123,15 +157,19 @@ def _create_reference_structure(self) -> Tuple[Dict, Dict[str, xr.DataArray]]: Returns: Tuple of (reference_structure, extracted_arrays_dict) + + Raises: + ValueError: If DataArrays don't have unique names or are duplicated """ - # Get constructor parameters - init_params = inspect.signature(self.__init__).parameters + # Get constructor parameters using caching for performance + if not hasattr(self, '_cached_init_params'): + self._cached_init_params = list(inspect.signature(self.__init__).parameters.keys()) # Process all constructor parameters reference_structure = {'__class__': self.__class__.__name__} all_extracted_arrays = {} - for name in init_params: + for name in self._cached_init_params: if name == 'self': continue @@ -140,73 +178,102 @@ def _create_reference_structure(self) -> Tuple[Dict, Dict[str, xr.DataArray]]: continue # Extract arrays and get reference structure - processed_value, extracted_arrays = self._extract_dataarrays_recursive(value) + processed_value, extracted_arrays = self._extract_dataarrays_recursive(value, name) + + # Check for array name conflicts + conflicts = set(all_extracted_arrays.keys()) & set(extracted_arrays.keys()) + if conflicts: + raise ValueError( + f'DataArray name conflicts detected: {conflicts}. ' + f'Each DataArray must have a unique name for serialization.' + ) # Add extracted arrays to the collection all_extracted_arrays.update(extracted_arrays) # Only store in structure if it's not None/empty after processing - if processed_value is not None and not (isinstance(processed_value, (dict, list)) and not processed_value): + if processed_value is not None and not self._is_empty_container(processed_value): reference_structure[name] = processed_value return reference_structure, all_extracted_arrays - def _extract_dataarrays_recursive(self, obj) -> Tuple[Any, Dict[str, xr.DataArray]]: + @staticmethod + def _is_empty_container(obj) -> bool: + """Check if object is an empty container (dict, list, tuple, set).""" + return isinstance(obj, (dict, list, tuple, set)) and len(obj) == 0 + + def _extract_dataarrays_recursive(self, obj, context_name: str = '') -> Tuple[Any, Dict[str, xr.DataArray]]: """ Recursively extract DataArrays/TimeSeries from nested structures. Args: obj: Object to process + context_name: Name context for better error messages Returns: Tuple of (processed_object_with_references, extracted_arrays_dict) + + Raises: + ValueError: If DataArrays don't have unique names """ extracted_arrays = {} # Handle DataArrays directly - use their unique name if isinstance(obj, xr.DataArray): if not obj.name: - raise ValueError(f'DataArrays must have a unique name for serialization. Unnamed DataArrays are not supported. {obj}') - if obj.name in extracted_arrays: - raise ValueError(f' must have a unique name for serialization. "{obj.name}" is a duplicate. {obj}') - extracted_arrays[obj.name] = obj - return f':::{obj.name}', extracted_arrays + raise ValueError( + f'DataArrays must have a unique name for serialization. ' + f'Unnamed DataArray found in {context_name}. Please set array.name = "unique_name"' + ) + + array_name = str(obj.name) # Ensure string type + if array_name in extracted_arrays: + raise ValueError( + f'DataArray name "{array_name}" is duplicated in {context_name}. ' + f'Each DataArray must have a unique name for serialization.' + ) + + extracted_arrays[array_name] = obj + return f':::{array_name}', extracted_arrays # Handle Interface objects - extract their DataArrays too elif isinstance(obj, Interface): - # Get the Interface's reference structure and arrays - interface_structure, interface_arrays = obj._create_reference_structure() - - # Add all extracted arrays from the nested Interface - extracted_arrays.update(interface_arrays) - return interface_structure, extracted_arrays - - # Handle lists - elif isinstance(obj, list): - processed_list = [] - for item in obj: - processed_item, nested_arrays = self._extract_dataarrays_recursive(item) + try: + interface_structure, interface_arrays = obj._create_reference_structure() + extracted_arrays.update(interface_arrays) + return interface_structure, extracted_arrays + except Exception as e: + raise ValueError(f'Failed to process nested Interface object in {context_name}: {e}') from e + + # Handle sequences (lists, tuples) + elif isinstance(obj, (list, tuple)): + processed_items = [] + for i, item in enumerate(obj): + item_context = f'{context_name}[{i}]' if context_name else f'item[{i}]' + processed_item, nested_arrays = self._extract_dataarrays_recursive(item, item_context) extracted_arrays.update(nested_arrays) - processed_list.append(processed_item) - return processed_list, extracted_arrays + processed_items.append(processed_item) + return processed_items, extracted_arrays # Handle dictionaries elif isinstance(obj, dict): processed_dict = {} for key, value in obj.items(): - processed_value, nested_arrays = self._extract_dataarrays_recursive(value) + key_context = f'{context_name}.{key}' if context_name else str(key) + processed_value, nested_arrays = self._extract_dataarrays_recursive(value, key_context) extracted_arrays.update(nested_arrays) processed_dict[key] = processed_value return processed_dict, extracted_arrays - # Handle tuples (convert to list for JSON compatibility) - elif isinstance(obj, tuple): - processed_list = [] - for item in obj: - processed_item, nested_arrays = self._extract_dataarrays_recursive(item) + # Handle sets (convert to list for JSON compatibility) + elif isinstance(obj, set): + processed_items = [] + for i, item in enumerate(obj): + item_context = f'{context_name}.set_item[{i}]' if context_name else f'set_item[{i}]' + processed_item, nested_arrays = self._extract_dataarrays_recursive(item, item_context) extracted_arrays.update(nested_arrays) - processed_list.append(processed_item) - return processed_list, extracted_arrays + processed_items.append(processed_item) + return processed_items, extracted_arrays # For all other types, serialize to basic types else: @@ -222,28 +289,29 @@ def _resolve_reference_structure(cls, structure, arrays_dict: Dict[str, xr.DataA arrays_dict: Dictionary of available DataArrays Returns: - Structure with references resolved to actual DataArrays or TimeSeriesData objects + Structure with references resolved to actual DataArrays or objects + + Raises: + ValueError: If referenced arrays are not found or class is not registered """ - # Handle DataArray references (including TimeSeriesData) + # Handle DataArray references if isinstance(structure, str) and structure.startswith(':::'): array_name = structure[3:] # Remove ":::" prefix - if array_name in arrays_dict: - array = arrays_dict[array_name] - - #TODO: Improve this! - if array.isnull().any(): - logger.warning(f"DataArray '{array_name}' contains null values. Dropping them.") - return array.dropna(dim='time', how='all') - return array - - # Check if this should be restored as TimeSeriesData - if TimeSeriesData.is_timeseries_data(array): - return TimeSeriesData.from_dataarray(array) - else: - return array - else: - logger.critical(f"Referenced DataArray '{array_name}' not found in dataset") - return None + if array_name not in arrays_dict: + raise ValueError(f"Referenced DataArray '{array_name}' not found in dataset") + + array = arrays_dict[array_name] + + # Handle null values with warning + if array.isnull().any(): + logger.warning(f"DataArray '{array_name}' contains null values. Dropping them.") + array = array.dropna(dim='time', how='all') + + # Check if this should be restored as TimeSeriesData + if TimeSeriesData.is_timeseries_data(array): + return TimeSeriesData.from_dataarray(array) + + return array elif isinstance(structure, list): resolved_list = [] @@ -254,15 +322,25 @@ def _resolve_reference_structure(cls, structure, arrays_dict: Dict[str, xr.DataA return resolved_list elif isinstance(structure, dict): - if structure.get('__class__') and structure['__class__'] in CLASS_REGISTRY: + if structure.get('__class__'): + class_name = structure['__class__'] + if class_name not in CLASS_REGISTRY: + raise ValueError( + f"Class '{class_name}' not found in CLASS_REGISTRY. " + f'Available classes: {list(CLASS_REGISTRY.keys())}' + ) + # This is a nested Interface object - restore it recursively - nested_class = CLASS_REGISTRY[structure['__class__']] + nested_class = CLASS_REGISTRY[class_name] # Remove the __class__ key and process the rest nested_data = {k: v for k, v in structure.items() if k != '__class__'} # Resolve references in the nested data resolved_nested_data = cls._resolve_reference_structure(nested_data, arrays_dict) - # Create the nested Interface object - return nested_class(**resolved_nested_data) + + try: + return nested_class(**resolved_nested_data) + except Exception as e: + raise ValueError(f'Failed to create instance of {class_name}: {e}') from e else: # Regular dictionary - resolve references in values resolved_dict = {} @@ -276,21 +354,36 @@ def _resolve_reference_structure(cls, structure, arrays_dict: Dict[str, xr.DataA return structure def _serialize_to_basic_types(self, obj): - """Convert object to basic Python types only (no DataArrays, no custom objects).""" + """ + Convert object to basic Python types only (no DataArrays, no custom objects). + + Args: + obj: Object to serialize + + Returns: + Object converted to basic Python types (str, int, float, bool, list, dict) + """ if obj is None or isinstance(obj, (str, int, float, bool)): return obj elif isinstance(obj, np.integer): return int(obj) elif isinstance(obj, np.floating): return float(obj) + elif isinstance(obj, np.bool_): + return bool(obj) elif isinstance(obj, (np.ndarray, pd.Series, pd.DataFrame)): return obj.tolist() if hasattr(obj, 'tolist') else list(obj) elif isinstance(obj, dict): return {k: self._serialize_to_basic_types(v) for k, v in obj.items()} elif isinstance(obj, (list, tuple)): return [self._serialize_to_basic_types(item) for item in obj] + elif isinstance(obj, set): + return [self._serialize_to_basic_types(item) for item in obj] elif hasattr(obj, 'isoformat'): # datetime objects return obj.isoformat() + elif hasattr(obj, '__dict__'): # Custom objects with attributes + logger.warning(f'Converting custom object {type(obj)} to dict representation: {obj}') + return {str(k): self._serialize_to_basic_types(v) for k, v in obj.__dict__.items()} else: # For any other object, try to convert to string as fallback logger.warning(f'Converting unknown type {type(obj)} to string: {obj}') @@ -303,12 +396,16 @@ def to_dataset(self) -> xr.Dataset: Returns: xr.Dataset: Dataset containing all DataArrays with basic objects only in attributes - """ - reference_structure, extracted_arrays = self._create_reference_structure() - # Create the dataset with extracted arrays as variables and structure as attrs - ds = xr.Dataset(extracted_arrays, attrs=reference_structure) - return ds + Raises: + ValueError: If serialization fails due to naming conflicts or invalid data + """ + try: + reference_structure, extracted_arrays = self._create_reference_structure() + # Create the dataset with extracted arrays as variables and structure as attrs + return xr.Dataset(extracted_arrays, attrs=reference_structure) + except Exception as e: + raise ValueError(f'Failed to convert {self.__class__.__name__} to dataset: {e}') from e def to_netcdf(self, path: Union[str, pathlib.Path], compression: int = 0): """ @@ -317,9 +414,16 @@ def to_netcdf(self, path: Union[str, pathlib.Path], compression: int = 0): Args: path: Path to save the NetCDF file compression: Compression level (0-9) + + Raises: + ValueError: If serialization fails + IOError: If file cannot be written """ - ds = self.to_dataset() - fx_io.save_dataset_to_netcdf(ds, path, compression=compression) + try: + ds = self.to_dataset() + fx_io.save_dataset_to_netcdf(ds, path, compression=compression) + except Exception as e: + raise IOError(f'Failed to save {self.__class__.__name__} to NetCDF file {path}: {e}') from e @classmethod def from_dataset(cls, ds: xr.Dataset) -> 'Interface': @@ -331,25 +435,31 @@ def from_dataset(cls, ds: xr.Dataset) -> 'Interface': Returns: Interface instance + + Raises: + ValueError: If dataset format is invalid or class mismatch """ - # Get class name and verify it matches - class_name = ds.attrs.get('__class__') - if class_name != cls.__name__: - logger.warning(f"Dataset class '{class_name}' doesn't match target class '{cls.__name__}'") + try: + # Get class name and verify it matches + class_name = ds.attrs.get('__class__') + if class_name and class_name != cls.__name__: + logger.warning(f"Dataset class '{class_name}' doesn't match target class '{cls.__name__}'") - # Get the reference structure from attrs - reference_structure = dict(ds.attrs) + # Get the reference structure from attrs + reference_structure = dict(ds.attrs) - # Remove the class name since it's not a constructor parameter - reference_structure.pop('__class__', None) + # Remove the class name since it's not a constructor parameter + reference_structure.pop('__class__', None) - # Create arrays dictionary from dataset variables - arrays_dict = {name: array for name, array in ds.data_vars.items()} + # Create arrays dictionary from dataset variables + arrays_dict = {name: array for name, array in ds.data_vars.items()} - # Resolve all references using the centralized method - resolved_params = cls._resolve_reference_structure(reference_structure, arrays_dict) + # Resolve all references using the centralized method + resolved_params = cls._resolve_reference_structure(reference_structure, arrays_dict) - return cls(**resolved_params) + return cls(**resolved_params) + except Exception as e: + raise ValueError(f'Failed to create {cls.__name__} from dataset: {e}') from e @classmethod def from_netcdf(cls, path: Union[str, pathlib.Path]) -> 'Interface': @@ -361,18 +471,27 @@ def from_netcdf(cls, path: Union[str, pathlib.Path]) -> 'Interface': Returns: Interface instance + + Raises: + IOError: If file cannot be read + ValueError: If file format is invalid """ - ds = fx_io.load_dataset_from_netcdf(path) - return cls.from_dataset(ds) + try: + ds = fx_io.load_dataset_from_netcdf(path) + return cls.from_dataset(ds) + except Exception as e: + raise IOError(f'Failed to load {cls.__name__} from NetCDF file {path}: {e}') from e def get_structure(self, clean: bool = False) -> Dict: """ - Get FlowSystem structure. + Get object structure as a dictionary. Args: clean: If True, remove None and empty dicts and lists. - """ + Returns: + Dictionary representation of the object structure + """ reference_structure, _ = self._create_reference_structure() if clean: return fx_io.remove_none_and_empty(reference_structure) @@ -380,28 +499,55 @@ def get_structure(self, clean: bool = False) -> Dict: def to_json(self, path: Union[str, pathlib.Path]): """ - Save the Element to a JSON file using the Interface pattern. + Save the object to a JSON file. This is meant for documentation and comparison, not for reloading. Args: path: The path to the JSON file. + + Raises: + IOError: If file cannot be written """ - # Use the stats mode for JSON export (cleaner output) - data = get_compact_representation(self.get_structure(clean=True)) - with open(path, 'w', encoding='utf-8') as f: - json.dump(data, f, indent=4, ensure_ascii=False) + try: + # Use the stats mode for JSON export (cleaner output) + data = get_compact_representation(self.get_structure(clean=True)) + with open(path, 'w', encoding='utf-8') as f: + json.dump(data, f, indent=4, ensure_ascii=False) + except Exception as e: + raise IOError(f'Failed to save {self.__class__.__name__} to JSON file {path}: {e}') from e def __repr__(self): - # Get the constructor arguments and their current values - init_signature = inspect.signature(self.__init__) - init_args = init_signature.parameters - - # Create a dictionary with argument names and their values - args_str = ', '.join(f'{name}={repr(getattr(self, name, None))}' for name in init_args if name != 'self') - return f'{self.__class__.__name__}({args_str})' + """Return a detailed string representation for debugging.""" + try: + # Get the constructor arguments and their current values + init_signature = inspect.signature(self.__init__) + init_args = init_signature.parameters + + # Create a dictionary with argument names and their values, with better formatting + args_parts = [] + for name in init_args: + if name == 'self': + continue + value = getattr(self, name, None) + # Truncate long representations + value_repr = repr(value) + if len(value_repr) > 50: + value_repr = value_repr[:47] + '...' + args_parts.append(f'{name}={value_repr}') + + args_str = ', '.join(args_parts) + return f'{self.__class__.__name__}({args_str})' + except Exception: + # Fallback if introspection fails + return f'{self.__class__.__name__}()' def __str__(self): - return get_str_representation(self.get_structure(clean=True)) + """Return a user-friendly string representation.""" + try: + return get_str_representation(self.get_structure(clean=True)) + except Exception: + # Fallback if structure generation fails + return f'{self.__class__.__name__} instance' class Element(Interface): From b87d979bb797c0e02c7b547e62dff51375c90def Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 24 Jun 2025 21:50:31 +0200 Subject: [PATCH 082/448] Add option to copy Interfaces (And the FlowSystem) --- flixopt/structure.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/flixopt/structure.py b/flixopt/structure.py index 36f723ad1..9cb830ff0 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -549,6 +549,28 @@ def __str__(self): # Fallback if structure generation fails return f'{self.__class__.__name__} instance' + def copy(self) -> 'Interface': + """ + Create a copy of the Interface object. + + Uses the existing serialization infrastructure to ensure proper copying + of all DataArrays and nested objects. + + Returns: + A new instance of the same class with copied data. + """ + # Convert to dataset, copy it, and convert back + dataset = self.to_dataset().copy(deep=True) + return self.__class__.from_dataset(dataset) + + def __copy__(self): + """Support for copy.copy().""" + return self.copy() + + def __deepcopy__(self, memo): + """Support for copy.deepcopy().""" + return self.copy() + class Element(Interface): """This class is the basic Element of flixopt. Every Element has a label""" From 8ec265ec35d9f43de951884accd74c1ddf4de945 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 25 Jun 2025 11:01:46 +0200 Subject: [PATCH 083/448] Make a copy of a FLowSytsem that gets reused in a second Calculation --- flixopt/calculation.py | 6 ++++++ flixopt/flow_system.py | 13 +++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/flixopt/calculation.py b/flixopt/calculation.py index e477f6c11..f52c1ca19 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -54,7 +54,13 @@ def __init__( folder: folder where results should be saved. If None, then the current working directory is used. """ self.name = name + if flow_system.used_in_calculation: + logging.warning(f'FlowSystem {flow_system.name} is already used in a calculation. ' + f'Creating a copy for Calculation "{self.name}".') + flow_system = flow_system.copy() + self.flow_system = flow_system + self.flow_system._used_in_calculation = True self.model: Optional[SystemModel] = None self.active_timesteps = active_timesteps diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index ff99725a5..386f54a72 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -44,10 +44,10 @@ class FlowSystem(Interface): """ def __init__( - self, - timesteps: pd.DatetimeIndex, - hours_of_last_timestep: Optional[float] = None, - hours_of_previous_timesteps: Optional[Union[int, float, np.ndarray]] = None, + self, + timesteps: pd.DatetimeIndex, + hours_of_last_timestep: Optional[float] = None, + hours_of_previous_timesteps: Optional[Union[int, float, np.ndarray]] = None, ): """ Args: @@ -73,6 +73,7 @@ def __init__( self.model: Optional[SystemModel] = None self._connected_and_transformed = False + self._used_in_calculation = False @staticmethod def _validate_timesteps(timesteps: pd.DatetimeIndex) -> pd.DatetimeIndex: @@ -542,3 +543,7 @@ def flows(self) -> Dict[str, Flow]: @property def all_elements(self) -> Dict[str, Element]: return {**self.components, **self.effects.effects, **self.flows, **self.buses} + + @property + def used_in_calculation(self) -> bool: + return self._used_in_calculation From a46fe648af7c8c279449327b28ebb3832dc19c40 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 25 Jun 2025 11:02:35 +0200 Subject: [PATCH 084/448] Remove test_timeseries.py --- tests/test_timeseries.py | 605 --------------------------------------- 1 file changed, 605 deletions(-) delete mode 100644 tests/test_timeseries.py diff --git a/tests/test_timeseries.py b/tests/test_timeseries.py deleted file mode 100644 index 8702a57fe..000000000 --- a/tests/test_timeseries.py +++ /dev/null @@ -1,605 +0,0 @@ -import json -import tempfile -from pathlib import Path -from typing import Dict, List, Tuple - -import numpy as np -import pandas as pd -import pytest -import xarray as xr - -from flixopt.core import ConversionError, DataConverter, TimeSeriesCollection, TimeSeriesData - - -@pytest.fixture -def sample_timesteps(): - """Create a sample time index with the required 'time' name.""" - return pd.date_range('2023-01-01', periods=5, freq='D', name='time') - - -@pytest.fixture -def simple_dataarray(sample_timesteps): - """Create a simple DataArray with time dimension.""" - return xr.DataArray([10, 20, 30, 40, 50], coords={'time': sample_timesteps}, dims=['time']) - - -@pytest.fixture -def sample_timeseries(simple_dataarray): - """Create a sample TimeSeries object.""" - return TimeSeries(simple_dataarray, name='Test Series') - - -class TestTimeSeries: - """Test suite for TimeSeries class.""" - - def test_initialization(self, simple_dataarray): - """Test basic initialization of TimeSeries.""" - ts = TimeSeries(simple_dataarray, name='Test Series') - - # Check basic properties - assert ts.name == 'Test Series' - assert ts.aggregation_weight is None - assert ts.aggregation_group is None - - # Check data initialization - assert isinstance(ts.stored_data, xr.DataArray) - assert ts.stored_data.equals(simple_dataarray) - assert ts.equals(simple_dataarray) - - # Check backup was created - assert ts._backup.equals(simple_dataarray) - - # Check active timesteps - assert ts.active_timesteps.equals(simple_dataarray.indexes['time']) - - def test_initialization_with_aggregation_params(self, simple_dataarray): - """Test initialization with aggregation parameters.""" - ts = TimeSeries( - simple_dataarray, name='Weighted Series', aggregation_weight=0.5, aggregation_group='test_group' - ) - - assert ts.name == 'Weighted Series' - assert ts.aggregation_weight == 0.5 - assert ts.aggregation_group == 'test_group' - - def test_initialization_validation(self, sample_timesteps): - """Test validation during initialization.""" - # Test missing time dimension - invalid_data = xr.DataArray([1, 2, 3], dims=['invalid_dim']) - with pytest.raises(ValueError, match='must have a "time" index'): - TimeSeries(invalid_data, name='Invalid Series') - - # Test multi-dimensional data - multi_dim_data = xr.DataArray( - [[1, 2, 3], [4, 5, 6]], coords={'dim1': [0, 1], 'time': sample_timesteps[:3]}, dims=['dim1', 'time'] - ) - with pytest.raises(ValueError, match='dimensions of DataArray must be 1'): - TimeSeries(multi_dim_data, name='Multi-dim Series') - - def test_active_timesteps_getter_setter(self, sample_timeseries, sample_timesteps): - """Test active_timesteps getter and setter.""" - # Initial state should use all timesteps - assert sample_timeseries.active_timesteps.equals(sample_timesteps) - - # Set to a subset - subset_index = sample_timesteps[1:3] - sample_timeseries.active_timesteps = subset_index - assert sample_timeseries.active_timesteps.equals(subset_index) - - # Active data should reflect the subset - assert sample_timeseries.equals(sample_timeseries.stored_data.sel(time=subset_index)) - - # Reset to full index - sample_timeseries.active_timesteps = None - assert sample_timeseries.active_timesteps.equals(sample_timesteps) - - # Test invalid type - with pytest.raises(TypeError, match='must be a pandas DatetimeIndex'): - sample_timeseries.active_timesteps = 'invalid' - - def test_reset(self, sample_timeseries, sample_timesteps): - """Test reset method.""" - # Set to subset first - subset_index = sample_timesteps[1:3] - sample_timeseries.active_timesteps = subset_index - - # Reset - sample_timeseries.reset() - - # Should be back to full index - assert sample_timeseries.active_timesteps.equals(sample_timesteps) - assert sample_timeseries.equals(sample_timeseries.stored_data) - - def test_restore_data(self, sample_timeseries, simple_dataarray): - """Test restore_data method.""" - # Modify the stored data - new_data = xr.DataArray([1, 2, 3, 4, 5], coords={'time': sample_timeseries.active_timesteps}, dims=['time']) - - # Store original data for comparison - original_data = sample_timeseries.stored_data - - # Set new data - sample_timeseries.stored_data = new_data - assert sample_timeseries.stored_data.equals(new_data) - - # Restore from backup - sample_timeseries.restore_data() - - # Should be back to original data - assert sample_timeseries.stored_data.equals(original_data) - assert sample_timeseries.equals(original_data) - - def test_stored_data_setter(self, sample_timeseries, sample_timesteps): - """Test stored_data setter with different data types.""" - # Test with a Series - series_data = pd.Series([5, 6, 7, 8, 9], index=sample_timesteps) - sample_timeseries.stored_data = series_data - assert np.array_equal(sample_timeseries.stored_data.values, series_data.values) - - # Test with a single-column DataFrame - df_data = pd.DataFrame({'col1': [15, 16, 17, 18, 19]}, index=sample_timesteps) - sample_timeseries.stored_data = df_data - assert np.array_equal(sample_timeseries.stored_data.values, df_data['col1'].values) - - # Test with a NumPy array - array_data = np.array([25, 26, 27, 28, 29]) - sample_timeseries.stored_data = array_data - assert np.array_equal(sample_timeseries.stored_data.values, array_data) - - # Test with a scalar - sample_timeseries.stored_data = 42 - assert np.all(sample_timeseries.stored_data.values == 42) - - # Test with another DataArray - another_dataarray = xr.DataArray([30, 31, 32, 33, 34], coords={'time': sample_timesteps}, dims=['time']) - sample_timeseries.stored_data = another_dataarray - assert sample_timeseries.stored_data.equals(another_dataarray) - - def test_stored_data_setter_no_change(self, sample_timeseries): - """Test stored_data setter when data doesn't change.""" - # Get current data - current_data = sample_timeseries.stored_data - current_backup = sample_timeseries._backup - - # Set the same data - sample_timeseries.stored_data = current_data - - # Backup shouldn't change - assert sample_timeseries._backup is current_backup # Should be the same object - - def test_from_datasource(self, sample_timesteps): - """Test from_datasource class method.""" - # Test with scalar - ts_scalar = TimeSeries.from_datasource(42, 'Scalar Series', sample_timesteps) - assert np.all(ts_scalar.stored_data.values == 42) - - # Test with Series - series_data = pd.Series([1, 2, 3, 4, 5], index=sample_timesteps) - ts_series = TimeSeries.from_datasource(series_data, 'Series Data', sample_timesteps) - assert np.array_equal(ts_series.stored_data.values, series_data.values) - - # Test with aggregation parameters - ts_with_agg = TimeSeries.from_datasource( - series_data, 'Aggregated Series', sample_timesteps, aggregation_weight=0.7, aggregation_group='group1' - ) - assert ts_with_agg.aggregation_weight == 0.7 - assert ts_with_agg.aggregation_group == 'group1' - - def test_to_json_from_json(self, sample_timeseries): - """Test to_json and from_json methods.""" - # Test to_json (dictionary only) - json_dict = sample_timeseries.to_json() - assert json_dict['name'] == sample_timeseries.name - assert 'data' in json_dict - assert 'coords' in json_dict['data'] - assert 'time' in json_dict['data']['coords'] - - # Test to_json with file saving - with tempfile.TemporaryDirectory() as tmpdirname: - filepath = Path(tmpdirname) / 'timeseries.json' - sample_timeseries.to_json(filepath) - assert filepath.exists() - - # Test from_json with file loading - loaded_ts = TimeSeries.from_json(path=filepath) - assert loaded_ts.name == sample_timeseries.name - assert np.array_equal(loaded_ts.stored_data.values, sample_timeseries.stored_data.values) - - # Test from_json with dictionary - loaded_ts_dict = TimeSeries.from_json(data=json_dict) - assert loaded_ts_dict.name == sample_timeseries.name - assert np.array_equal(loaded_ts_dict.stored_data.values, sample_timeseries.stored_data.values) - - # Test validation in from_json - with pytest.raises(ValueError, match="one of 'path' or 'data'"): - TimeSeries.from_json(data=json_dict, path='dummy.json') - - def test_all_equal(self, sample_timesteps): - """Test all_equal property.""" - # All equal values - equal_data = xr.DataArray([5, 5, 5, 5, 5], coords={'time': sample_timesteps}, dims=['time']) - ts_equal = TimeSeries(equal_data, 'Equal Series') - assert ts_equal.all_equal is True - - # Not all equal - unequal_data = xr.DataArray([5, 5, 6, 5, 5], coords={'time': sample_timesteps}, dims=['time']) - ts_unequal = TimeSeries(unequal_data, 'Unequal Series') - assert ts_unequal.all_equal is False - - def test_arithmetic_operations(self, sample_timeseries): - """Test arithmetic operations.""" - # Create a second TimeSeries for testing - data2 = xr.DataArray([1, 2, 3, 4, 5], coords={'time': sample_timeseries.active_timesteps}, dims=['time']) - ts2 = TimeSeries(data2, 'Second Series') - - # Test operations between two TimeSeries objects - assert np.array_equal( - (sample_timeseries + ts2).values, sample_timeseries.values + ts2.values - ) - assert np.array_equal( - (sample_timeseries - ts2).values, sample_timeseries.values - ts2.values - ) - assert np.array_equal( - (sample_timeseries * ts2).values, sample_timeseries.values * ts2.values - ) - assert np.array_equal( - (sample_timeseries / ts2).values, sample_timeseries.values / ts2.values - ) - - # Test operations with DataArrays - assert np.array_equal((sample_timeseries + data2).values, sample_timeseries.values + data2.values) - assert np.array_equal((data2 + sample_timeseries).values, data2.values + sample_timeseries.values) - - # Test operations with scalars - assert np.array_equal((sample_timeseries + 5).values, sample_timeseries.values + 5) - assert np.array_equal((5 + sample_timeseries).values, 5 + sample_timeseries.values) - - # Test unary operations - assert np.array_equal((-sample_timeseries).values, -sample_timeseries.values) - assert np.array_equal((+sample_timeseries).values, +sample_timeseries.values) - assert np.array_equal((abs(sample_timeseries)).values, abs(sample_timeseries.values)) - - def test_comparison_operations(self, sample_timesteps): - """Test comparison operations.""" - data1 = xr.DataArray([10, 20, 30, 40, 50], coords={'time': sample_timesteps}, dims=['time']) - data2 = xr.DataArray([5, 10, 15, 20, 25], coords={'time': sample_timesteps}, dims=['time']) - - ts1 = TimeSeries(data1, 'Series 1') - ts2 = TimeSeries(data2, 'Series 2') - - # Test __gt__ method - assert (ts1 > ts2).all().item() - - # Test with mixed values - data3 = xr.DataArray([5, 25, 15, 45, 25], coords={'time': sample_timesteps}, dims=['time']) - ts3 = TimeSeries(data3, 'Series 3') - - assert not (ts1 > ts3).all().item() # Not all values in ts1 are greater than ts3 - - def test_numpy_ufunc(self, sample_timeseries): - """Test numpy ufunc compatibility.""" - # Test basic numpy functions - assert np.array_equal(np.add(sample_timeseries, 5).values, np.add(sample_timeseries, 5).values) - - assert np.array_equal( - np.multiply(sample_timeseries, 2).values, np.multiply(sample_timeseries, 2).values - ) - - # Test with two TimeSeries objects - data2 = xr.DataArray([1, 2, 3, 4, 5], coords={'time': sample_timeseries.active_timesteps}, dims=['time']) - ts2 = TimeSeries(data2, 'Second Series') - - assert np.array_equal( - np.add(sample_timeseries, ts2).values, np.add(sample_timeseries, ts2).values - ) - - def test_sel_and_isel_properties(self, sample_timeseries): - """Test sel and isel properties.""" - # Test that sel property works - selected = sample_timeseries.sel(time=sample_timeseries.active_timesteps[0]) - assert selected.item() == sample_timeseries.values[0] - - # Test that isel property works - indexed = sample_timeseries.isel(time=0) - assert indexed.item() == sample_timeseries.values[0] - - -@pytest.fixture -def sample_collection(sample_timesteps): - """Create a sample TimeSeriesCollection.""" - return TimeSeriesCollection(sample_timesteps) - - -@pytest.fixture -def populated_collection(sample_collection): - """Create a TimeSeriesCollection with test data.""" - # Add a constant time series - sample_collection.create_time_series(42, 'constant_series') - - # Add a varying time series - varying_data = np.array([10, 20, 30, 40, 50]) - sample_collection.create_time_series(varying_data, 'varying_series') - - # Add a time series with extra timestep - sample_collection.create_time_series( - np.array([1, 2, 3, 4, 5, 6]), 'extra_timestep_series', needs_extra_timestep=True - ) - - # Add series with aggregation settings - sample_collection.create_time_series( - TimeSeriesData(np.array([5, 5, 5, 5, 5]), agg_group='group1'), 'group1_series1' - ) - sample_collection.create_time_series( - TimeSeriesData(np.array([6, 6, 6, 6, 6]), agg_group='group1'), 'group1_series2' - ) - sample_collection.create_time_series( - TimeSeriesData(np.array([10, 10, 10, 10, 10]), agg_weight=0.5), 'weighted_series' - ) - - return sample_collection - - -class TestTimeSeriesCollection: - """Test suite for TimeSeriesCollection.""" - - def test_initialization(self, sample_timesteps): - """Test basic initialization.""" - collection = TimeSeriesCollection(sample_timesteps) - - assert collection.all_timesteps.equals(sample_timesteps) - assert len(collection.all_timesteps_extra) == len(sample_timesteps) + 1 - assert isinstance(collection.all_hours_per_timestep, xr.DataArray) - assert len(collection) == 0 - - def test_initialization_with_custom_hours(self, sample_timesteps): - """Test initialization with custom hour settings.""" - # Test with last timestep duration - last_timestep_hours = 12 - collection = TimeSeriesCollection(sample_timesteps, hours_of_last_timestep=last_timestep_hours) - - # Verify the last timestep duration - extra_step_delta = collection.all_timesteps_extra[-1] - collection.all_timesteps_extra[-2] - assert extra_step_delta == pd.Timedelta(hours=last_timestep_hours) - - # Test with previous timestep duration - hours_per_step = 8 - collection2 = TimeSeriesCollection(sample_timesteps, hours_of_previous_timesteps=hours_per_step) - - assert collection2.hours_of_previous_timesteps == hours_per_step - - def test_create_time_series(self, sample_collection): - """Test creating time series.""" - # Test scalar - ts1 = sample_collection.create_time_series(42, 'scalar_series') - assert ts1.name == 'scalar_series' - assert np.all(ts1.values == 42) - - # Test numpy array - data = np.array([1, 2, 3, 4, 5]) - ts2 = sample_collection.create_time_series(data, 'array_series') - assert np.array_equal(ts2.values, data) - - # Test with TimeSeriesData - ts3 = sample_collection.create_time_series(TimeSeriesData(10, agg_weight=0.7), 'weighted_series') - assert ts3.aggregation_weight == 0.7 - - # Test with extra timestep - ts4 = sample_collection.create_time_series(5, 'extra_series', needs_extra_timestep=True) - assert ts4.needs_extra_timestep - assert len(ts4) == len(sample_collection.timesteps_extra) - - # Test duplicate name - with pytest.raises(ValueError, match='already exists'): - sample_collection.create_time_series(1, 'scalar_series') - - def test_access_time_series(self, populated_collection): - """Test accessing time series.""" - # Test __getitem__ - ts = populated_collection['varying_series'] - assert ts.name == 'varying_series' - - # Test __contains__ with string - assert 'constant_series' in populated_collection - assert 'nonexistent_series' not in populated_collection - - # Test __contains__ with TimeSeries object - assert populated_collection['varying_series'] in populated_collection - - # Test __iter__ - names = [ts.name for ts in populated_collection] - assert len(names) == 6 - assert 'varying_series' in names - - # Test access to non-existent series - with pytest.raises(KeyError): - populated_collection['nonexistent_series'] - - def test_constants_and_non_constants(self, populated_collection): - """Test constants and non_constants properties.""" - # Test constants - constants = populated_collection.constants - assert len(constants) == 4 # constant_series, group1_series1, group1_series2, weighted_series - assert all(ts.all_equal for ts in constants) - - # Test non_constants - non_constants = populated_collection.non_constants - assert len(non_constants) == 2 # varying_series, extra_timestep_series - assert all(not ts.all_equal for ts in non_constants) - - # Test modifying a series changes the results - populated_collection['constant_series'].stored_data = np.array([1, 2, 3, 4, 5]) - updated_constants = populated_collection.constants - assert len(updated_constants) == 3 # One less constant - assert 'constant_series' not in [ts.name for ts in updated_constants] - - def test_timesteps_properties(self, populated_collection, sample_timesteps): - """Test timestep-related properties.""" - # Test default (all) timesteps - assert populated_collection.timesteps.equals(sample_timesteps) - assert len(populated_collection.timesteps_extra) == len(sample_timesteps) + 1 - - # Test activating a subset - subset = sample_timesteps[1:3] - populated_collection.activate_timesteps(subset) - - assert populated_collection.timesteps.equals(subset) - assert len(populated_collection.timesteps_extra) == len(subset) + 1 - - # Check that time series were updated - assert populated_collection['varying_series'].active_timesteps.equals(subset) - assert populated_collection['extra_timestep_series'].active_timesteps.equals( - populated_collection.timesteps_extra - ) - - # Test reset - populated_collection.reset() - assert populated_collection.timesteps.equals(sample_timesteps) - - def test_to_dataframe_and_dataset(self, populated_collection): - """Test conversion to DataFrame and Dataset.""" - # Test to_dataset - ds = populated_collection.to_dataset() - assert isinstance(ds, xr.Dataset) - assert len(ds.data_vars) == 6 - - # Test to_dataframe with different filters - df_all = populated_collection.to_dataframe(filtered='all') - assert len(df_all.columns) == 6 - - df_constant = populated_collection.to_dataframe(filtered='constant') - assert len(df_constant.columns) == 4 - - df_non_constant = populated_collection.to_dataframe(filtered='non_constant') - assert len(df_non_constant.columns) == 2 - - # Test invalid filter - with pytest.raises(ValueError): - populated_collection.to_dataframe(filtered='invalid') - - def test_calculate_aggregation_weights(self, populated_collection): - """Test aggregation weight calculation.""" - weights = populated_collection.calculate_aggregation_weights() - - # Group weights should be 0.5 each (1/2) - assert populated_collection.group_weights['group1'] == 0.5 - - # Series in group1 should have weight 0.5 - assert weights['group1_series1'] == 0.5 - assert weights['group1_series2'] == 0.5 - - # Series with explicit weight should have that weight - assert weights['weighted_series'] == 0.5 - - # Series without group or weight should have weight 1 - assert weights['constant_series'] == 1 - - def test_insert_new_data(self, populated_collection, sample_timesteps): - """Test inserting new data.""" - # Create new data - new_data = pd.DataFrame( - { - 'constant_series': [100, 100, 100, 100, 100], - 'varying_series': [5, 10, 15, 20, 25], - # extra_timestep_series is omitted to test partial updates - }, - index=sample_timesteps, - ) - - # Insert data - populated_collection.insert_new_data(new_data) - - # Verify updates - assert np.all(populated_collection['constant_series'].values == 100) - assert np.array_equal(populated_collection['varying_series'].values, np.array([5, 10, 15, 20, 25])) - - # Series not in the DataFrame should be unchanged - assert np.array_equal( - populated_collection['extra_timestep_series'].values[:-1], np.array([1, 2, 3, 4, 5]) - ) - - # Test with mismatched index - bad_index = pd.date_range('2023-02-01', periods=5, freq='D', name='time') - bad_data = pd.DataFrame({'constant_series': [1, 1, 1, 1, 1]}, index=bad_index) - - with pytest.raises(ValueError, match='must match collection timesteps'): - populated_collection.insert_new_data(bad_data) - - def test_restore_data(self, populated_collection): - """Test restoring original data.""" - # Capture original data - original_values = {name: ts.stored_data.copy() for name, ts in populated_collection.time_series_data.items()} - - # Modify data - new_data = pd.DataFrame( - { - name: np.ones(len(populated_collection.timesteps)) * 999 - for name in populated_collection.time_series_data - if not populated_collection[name].needs_extra_timestep - }, - index=populated_collection.timesteps, - ) - - populated_collection.insert_new_data(new_data) - - # Verify data was changed - assert np.all(populated_collection['constant_series'].values == 999) - - # Restore data - populated_collection.restore_data() - - # Verify data was restored - for name, original in original_values.items(): - restored = populated_collection[name].stored_data - assert np.array_equal(restored.values, original.values) - - def test_class_method_with_uniform_timesteps(self): - """Test the with_uniform_timesteps class method.""" - collection = TimeSeriesCollection.with_uniform_timesteps( - start_time=pd.Timestamp('2023-01-01'), periods=24, freq='h', hours_per_step=1 - ) - - assert len(collection.timesteps) == 24 - assert collection.hours_of_previous_timesteps == 1 - assert (collection.timesteps[1] - collection.timesteps[0]) == pd.Timedelta(hours=1) - - def test_hours_per_timestep(self, populated_collection): - """Test hours_per_timestep calculation.""" - # Standard case - uniform timesteps - hours = populated_collection.hours_per_timestep.values - assert np.allclose(hours, 24) # Default is daily timesteps - - # Create non-uniform timesteps - non_uniform_times = pd.DatetimeIndex( - [ - pd.Timestamp('2023-01-01'), - pd.Timestamp('2023-01-02'), - pd.Timestamp('2023-01-03 12:00:00'), # 1.5 days from previous - pd.Timestamp('2023-01-04'), # 0.5 days from previous - pd.Timestamp('2023-01-06'), # 2 days from previous - ], - name='time', - ) - - collection = TimeSeriesCollection(non_uniform_times) - hours = collection.hours_per_timestep.values - - # Expected hours between timestamps - expected = np.array([24, 36, 12, 48, 48]) - assert np.allclose(hours, expected) - - def test_validation_and_errors(self, sample_timesteps): - """Test validation and error handling.""" - # Test non-DatetimeIndex - with pytest.raises(TypeError, match='must be a pandas DatetimeIndex'): - TimeSeriesCollection(pd.Index([1, 2, 3, 4, 5])) - - # Test too few timesteps - with pytest.raises(ValueError, match='must contain at least 2 timestamps'): - TimeSeriesCollection(pd.DatetimeIndex([pd.Timestamp('2023-01-01')], name='time')) - - # Test invalid active_timesteps - collection = TimeSeriesCollection(sample_timesteps) - invalid_timesteps = pd.date_range('2024-01-01', periods=3, freq='D', name='time') - - with pytest.raises(ValueError, match='must be a subset'): - collection.activate_timesteps(invalid_timesteps) From 201d0667356e174f5f7f87effec54013bf14a767 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 25 Jun 2025 11:15:40 +0200 Subject: [PATCH 085/448] Reorganizing Datatypes --- flixopt/commons.py | 2 +- flixopt/components.py | 48 ++++++++++++++++++------------------ flixopt/core.py | 38 +++++++--------------------- flixopt/effects.py | 29 +++++++++++----------- flixopt/elements.py | 32 ++++++++++++------------ flixopt/features.py | 32 ++++++++++++------------ flixopt/flow_system.py | 48 ++++++++++++++++++++---------------- flixopt/interface.py | 36 +++++++++++++-------------- flixopt/linear_converters.py | 22 ++++++++--------- flixopt/structure.py | 6 ++--- 10 files changed, 139 insertions(+), 154 deletions(-) diff --git a/flixopt/commons.py b/flixopt/commons.py index 222c07324..68412d6fe 100644 --- a/flixopt/commons.py +++ b/flixopt/commons.py @@ -14,11 +14,11 @@ Transmission, ) from .config import CONFIG, change_logging_level +from .core import TimeSeriesData from .effects import Effect from .elements import Bus, Flow from .flow_system import FlowSystem from .interface import InvestParameters, OnOffParameters, Piece, Piecewise, PiecewiseConversion, PiecewiseEffects -from .core import TimeSeriesData __all__ = [ 'TimeSeriesData', diff --git a/flixopt/components.py b/flixopt/components.py index 81baaeea5..8e172d573 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -9,7 +9,7 @@ import numpy as np from . import utils -from .core import NumericData, NumericDataTS, PlausibilityError, Scalar, TimeSeries +from .core import NumericDataUser, PlausibilityError, Scalar, TimeSeries from .elements import Component, ComponentModel, Flow from .features import InvestmentModel, OnOffModel, PiecewiseModel from .interface import InvestParameters, OnOffParameters, PiecewiseConversion @@ -34,7 +34,7 @@ def __init__( inputs: List[Flow], outputs: List[Flow], on_off_parameters: OnOffParameters = None, - conversion_factors: List[Dict[str, NumericDataTS]] = None, + conversion_factors: List[Dict[str, NumericDataUser]] = None, piecewise_conversion: Optional[PiecewiseConversion] = None, meta_data: Optional[Dict] = None, ): @@ -105,7 +105,7 @@ def _transform_conversion_factors(self, flow_system: 'FlowSystem') -> List[Dict[ transformed_dict = {} for flow, values in conversion_factor.items(): # TODO: Might be better to use the label of the component instead of the flow - transformed_dict[flow] = flow_system.create_time_series( + transformed_dict[flow] = flow_system.fit_to_model_coords( f'{self.flows[flow].label_full}|conversion_factor{idx}', values ) list_of_conversion_factors.append(transformed_dict) @@ -128,14 +128,14 @@ def __init__( charging: Flow, discharging: Flow, capacity_in_flow_hours: Union[Scalar, InvestParameters], - relative_minimum_charge_state: NumericData = 0, - relative_maximum_charge_state: NumericData = 1, + relative_minimum_charge_state: NumericDataUser = 0, + relative_maximum_charge_state: NumericDataUser = 1, initial_charge_state: Union[Scalar, Literal['lastValueOfSim']] = 0, minimal_final_charge_state: Optional[Scalar] = None, maximal_final_charge_state: Optional[Scalar] = None, - eta_charge: NumericData = 1, - eta_discharge: NumericData = 1, - relative_loss_per_hour: NumericData = 0, + eta_charge: NumericDataUser = 1, + eta_discharge: NumericDataUser = 1, + relative_loss_per_hour: NumericDataUser = 0, prevent_simultaneous_charge_and_discharge: bool = True, meta_data: Optional[Dict] = None, ): @@ -176,16 +176,16 @@ def __init__( self.charging = charging self.discharging = discharging self.capacity_in_flow_hours = capacity_in_flow_hours - self.relative_minimum_charge_state: NumericDataTS = relative_minimum_charge_state - self.relative_maximum_charge_state: NumericDataTS = relative_maximum_charge_state + self.relative_minimum_charge_state: NumericDataUser = relative_minimum_charge_state + self.relative_maximum_charge_state: NumericDataUser = relative_maximum_charge_state self.initial_charge_state = initial_charge_state self.minimal_final_charge_state = minimal_final_charge_state self.maximal_final_charge_state = maximal_final_charge_state - self.eta_charge: NumericDataTS = eta_charge - self.eta_discharge: NumericDataTS = eta_discharge - self.relative_loss_per_hour: NumericDataTS = relative_loss_per_hour + self.eta_charge: NumericDataUser = eta_charge + self.eta_discharge: NumericDataUser = eta_discharge + self.relative_loss_per_hour: NumericDataUser = relative_loss_per_hour self.prevent_simultaneous_charge_and_discharge = prevent_simultaneous_charge_and_discharge def create_model(self, model: SystemModel) -> 'StorageModel': @@ -195,19 +195,19 @@ def create_model(self, model: SystemModel) -> 'StorageModel': def transform_data(self, flow_system: 'FlowSystem') -> None: super().transform_data(flow_system) - self.relative_minimum_charge_state = flow_system.create_time_series( + self.relative_minimum_charge_state = flow_system.fit_to_model_coords( f'{self.label_full}|relative_minimum_charge_state', self.relative_minimum_charge_state, needs_extra_timestep=True, ) - self.relative_maximum_charge_state = flow_system.create_time_series( + self.relative_maximum_charge_state = flow_system.fit_to_model_coords( f'{self.label_full}|relative_maximum_charge_state', self.relative_maximum_charge_state, needs_extra_timestep=True, ) - self.eta_charge = flow_system.create_time_series(f'{self.label_full}|eta_charge', self.eta_charge) - self.eta_discharge = flow_system.create_time_series(f'{self.label_full}|eta_discharge', self.eta_discharge) - self.relative_loss_per_hour = flow_system.create_time_series( + self.eta_charge = flow_system.fit_to_model_coords(f'{self.label_full}|eta_charge', self.eta_charge) + self.eta_discharge = flow_system.fit_to_model_coords(f'{self.label_full}|eta_discharge', self.eta_discharge) + self.relative_loss_per_hour = flow_system.fit_to_model_coords( f'{self.label_full}|relative_loss_per_hour', self.relative_loss_per_hour ) if isinstance(self.capacity_in_flow_hours, InvestParameters): @@ -264,8 +264,8 @@ def __init__( out1: Flow, in2: Optional[Flow] = None, out2: Optional[Flow] = None, - relative_losses: Optional[NumericDataTS] = None, - absolute_losses: Optional[NumericDataTS] = None, + relative_losses: Optional[NumericDataUser] = None, + absolute_losses: Optional[NumericDataUser] = None, on_off_parameters: OnOffParameters = None, prevent_simultaneous_flows_in_both_directions: bool = True, meta_data: Optional[Dict] = None, @@ -331,10 +331,10 @@ def create_model(self, model) -> 'TransmissionModel': def transform_data(self, flow_system: 'FlowSystem') -> None: super().transform_data(flow_system) - self.relative_losses = flow_system.create_time_series( + self.relative_losses = flow_system.fit_to_model_coords( f'{self.label_full}|relative_losses', self.relative_losses ) - self.absolute_losses = flow_system.create_time_series( + self.absolute_losses = flow_system.fit_to_model_coords( f'{self.label_full}|absolute_losses', self.absolute_losses ) @@ -556,7 +556,7 @@ def _initial_and_final_charge_state(self): ) @property - def absolute_charge_state_bounds(self) -> Tuple[NumericData, NumericData]: + def absolute_charge_state_bounds(self) -> Tuple[NumericDataUser, NumericDataUser]: relative_lower_bound, relative_upper_bound = self.relative_charge_state_bounds if not isinstance(self.element.capacity_in_flow_hours, InvestParameters): return ( @@ -570,7 +570,7 @@ def absolute_charge_state_bounds(self) -> Tuple[NumericData, NumericData]: ) @property - def relative_charge_state_bounds(self) -> Tuple[NumericData, NumericData]: + def relative_charge_state_bounds(self) -> Tuple[NumericDataUser, NumericDataUser]: return ( self.element.relative_minimum_charge_state, self.element.relative_maximum_charge_state, diff --git a/flixopt/core.py b/flixopt/core.py index 31738f6c7..4ab97b219 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -17,13 +17,13 @@ logger = logging.getLogger('flixopt') Scalar = Union[int, float] -"""A type representing a single number, either integer or float.""" +"""A single number, either integer or float.""" -NumericData = Union[int, float, np.integer, np.floating, np.ndarray, pd.Series, pd.DataFrame, xr.DataArray] -"""Represents any form of numeric data, from simple scalars to complex data structures.""" +NumericDataUser = Union[int, float, np.integer, np.floating, np.ndarray, pd.Series, pd.DataFrame, xr.DataArray, 'TimeSeriesData'] +"""Numeric data accepted in varios types. Will be converted to an xr.DataArray or Scalar internally.""" -NumericDataTS = Union[NumericData, 'TimeSeriesData'] -"""Represents either standard numeric data or TimeSeriesData.""" +NumericDataInternal = Union[int, float, xr.DataArray, 'TimeSeriesData'] +"""Internally used datatypes for numeric data.""" class PlausibilityError(Exception): @@ -37,6 +37,7 @@ class ConversionError(Exception): pass + class TimeSeriesData(xr.DataArray): """Minimal TimeSeriesData that inherits from xr.DataArray with aggregation metadata.""" @@ -153,7 +154,7 @@ def _fix_timeseries_data_indexing( # Check if time coordinates are identical elif not data.coords['time'].equals(timesteps): logger.warning( - f'TimeSeriesData has different time coordinates than expected. Replacing with provided timesteps.' + 'TimeSeriesData has different time coordinates than expected. Replacing with provided timesteps.' ) # Replace time coordinates while preserving data and metadata recoordinated_data = xr.DataArray( @@ -166,7 +167,7 @@ def _fix_timeseries_data_indexing( return data.copy(deep=True) @staticmethod - def to_dataarray(data: NumericData, timesteps: pd.DatetimeIndex) -> xr.DataArray: + def to_dataarray(data: NumericDataUser, timesteps: pd.DatetimeIndex) -> xr.DataArray: """Convert data to xarray.DataArray with specified timesteps index.""" if not isinstance(timesteps, pd.DatetimeIndex) or len(timesteps) == 0: raise ValueError(f'Timesteps must be a non-empty DatetimeIndex, got {type(timesteps).__name__}') @@ -182,10 +183,6 @@ def to_dataarray(data: NumericData, timesteps: pd.DatetimeIndex) -> xr.DataArray if isinstance(data, TimeSeriesData): return DataConverter._fix_timeseries_data_indexing(data, timesteps, dims, coords) - elif isinstance(data, TimeSeries): - # Handle TimeSeries objects (your existing logic) - pass # Add your TimeSeries handling here - elif isinstance(data, (int, float, np.integer, np.floating)): # Scalar: broadcast to all timesteps scalar_data = np.full(expected_shape, data) @@ -220,7 +217,7 @@ def to_dataarray(data: NumericData, timesteps: pd.DatetimeIndex) -> xr.DataArray return data.copy(deep=True) elif isinstance(data, list): - logger.warning(f'Converting list to DataArray. This is not recommended.') + logger.warning('Converting list to DataArray. This is not recommended.') if len(data) != expected_shape[0]: raise ConversionError(f"List length {len(data)} doesn't match expected {expected_shape[0]}") return xr.DataArray(data, coords=coords, dims=dims) @@ -234,23 +231,6 @@ def to_dataarray(data: NumericData, timesteps: pd.DatetimeIndex) -> xr.DataArray raise ConversionError(f'Converting data {type(data)} to xarray.DataArray raised an error: {str(e)}') from e -class TimeSeries: - def __init__(self): - raise NotImplementedError('TimeSeries was removed') - - -class TimeSeriesCollection: - """ - Collection of TimeSeries objects with shared timestep management. - - TimeSeriesCollection handles multiple TimeSeries objects with synchronized - timesteps, provides operations on collections, and manages extra timesteps. - """ - - def __init__(self): - raise NotImplementedError('TimeSeriesCollection was removed') - - def get_numeric_stats(data: xr.DataArray, decimals: int = 2, padd: int = 10) -> str: """Calculates the mean, median, min, max, and standard deviation of a numeric DataArray.""" format_spec = f'>{padd}.{decimals}f' if padd else f'.{decimals}f' diff --git a/flixopt/effects.py b/flixopt/effects.py index b043f4492..7fa136f5b 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -13,7 +13,7 @@ import numpy as np import pandas as pd -from .core import NumericData, NumericDataTS, Scalar, TimeSeriesCollection, TimeSeries +from .core import NumericDataInternal, NumericDataUser, Scalar from .features import ShareAllocationModel from .structure import Element, ElementModel, Interface, Model, SystemModel, register_class_for_io @@ -44,8 +44,8 @@ def __init__( maximum_operation: Optional[Scalar] = None, minimum_invest: Optional[Scalar] = None, maximum_invest: Optional[Scalar] = None, - minimum_operation_per_hour: Optional[NumericDataTS] = None, - maximum_operation_per_hour: Optional[NumericDataTS] = None, + minimum_operation_per_hour: Optional[NumericDataUser] = None, + maximum_operation_per_hour: Optional[NumericDataUser] = None, minimum_total: Optional[Scalar] = None, maximum_total: Optional[Scalar] = None, ): @@ -82,22 +82,22 @@ def __init__( self.specific_share_to_other_effects_invest: EffectValuesUser = specific_share_to_other_effects_invest or {} self.minimum_operation = minimum_operation self.maximum_operation = maximum_operation - self.minimum_operation_per_hour: NumericDataTS = minimum_operation_per_hour - self.maximum_operation_per_hour: NumericDataTS = maximum_operation_per_hour + self.minimum_operation_per_hour: NumericDataUser = minimum_operation_per_hour + self.maximum_operation_per_hour: NumericDataUser = maximum_operation_per_hour self.minimum_invest = minimum_invest self.maximum_invest = maximum_invest self.minimum_total = minimum_total self.maximum_total = maximum_total def transform_data(self, flow_system: 'FlowSystem'): - self.minimum_operation_per_hour = flow_system.create_time_series( + self.minimum_operation_per_hour = flow_system.fit_to_model_coords( f'{self.label_full}|minimum_operation_per_hour', self.minimum_operation_per_hour ) - self.maximum_operation_per_hour = flow_system.create_time_series( + self.maximum_operation_per_hour = flow_system.fit_to_model_coords( f'{self.label_full}|maximum_operation_per_hour', self.maximum_operation_per_hour, flow_system ) - self.specific_share_to_other_effects_operation = flow_system.create_effect_time_series( + self.specific_share_to_other_effects_operation = flow_system.fit_effects_to_model_coords( f'{self.label_full}|operation->', self.specific_share_to_other_effects_operation, 'operation' ) @@ -168,10 +168,9 @@ def do_modeling(self): ) -EffectValuesExpr = Dict[str, linopy.LinearExpression] # Used to create Shares -EffectTimeSeries = Dict[str, TimeSeries] # Used internally to index values -EffectValuesDict = Dict[str, NumericDataTS] # How effect values are stored -EffectValuesUser = Union[NumericDataTS, Dict[str, NumericDataTS]] # User-specified Shares to Effects +EffectExpr = Dict[str, linopy.LinearExpression] # Used to create Shares +EffectValuesInternal = Dict[str, NumericDataInternal] # Used internally to index values +EffectValuesUser = Union[NumericDataUser, Dict[str, NumericDataUser]] # User-specified Shares to Effects """ This datatype is used to define the share to an effect by a certain attribute. """ EffectValuesUserScalar = Union[Scalar, Dict[str, Scalar]] # User-specified Shares to Effects @@ -207,7 +206,7 @@ def add_effects(self, *effects: Effect) -> None: self._effects[effect.label] = effect logger.info(f'Registered new Effect: {effect.label}') - def create_effect_values_dict(self, effect_values_user: EffectValuesUser) -> Optional[EffectValuesDict]: + def create_effect_values_dict(self, effect_values_user: EffectValuesUser) -> Optional[Dict[str, NumericDataUser]]: """ Converts effect values into a dictionary. If a scalar is provided, it is associated with a default effect type. @@ -233,6 +232,8 @@ def get_effect_label(eff: Union[Effect, str]) -> str: stacklevel=2, ) return eff.label_full + elif eff is None: + return self.standard_effect.label_full else: return eff @@ -341,7 +342,7 @@ def __init__(self, model: SystemModel, effects: EffectCollection): def add_share_to_effects( self, name: str, - expressions: EffectValuesExpr, + expressions: EffectExpr, target: Literal['operation', 'invest'], ) -> None: for effect, expression in expressions.items(): diff --git a/flixopt/elements.py b/flixopt/elements.py index 48e73ef76..a2ba8f7c1 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -10,7 +10,7 @@ import numpy as np from .config import CONFIG -from .core import NumericData, NumericDataTS, PlausibilityError, Scalar, TimeSeriesCollection +from .core import NumericDataUser, PlausibilityError, Scalar, TimeSeriesCollection from .effects import EffectValuesUser from .features import InvestmentModel, OnOffModel, PreventSimultaneousUsageModel from .interface import InvestParameters, OnOffParameters @@ -90,7 +90,7 @@ class Bus(Element): """ def __init__( - self, label: str, excess_penalty_per_flow_hour: Optional[NumericDataTS] = 1e5, meta_data: Optional[Dict] = None + self, label: str, excess_penalty_per_flow_hour: Optional[NumericDataUser] = 1e5, meta_data: Optional[Dict] = None ): """ Args: @@ -111,7 +111,7 @@ def create_model(self, model: SystemModel) -> 'BusModel': return self.model def transform_data(self, flow_system: 'FlowSystem'): - self.excess_penalty_per_flow_hour = flow_system.create_time_series( + self.excess_penalty_per_flow_hour = flow_system.fit_to_model_coords( f'{self.label_full}|excess_penalty_per_flow_hour', self.excess_penalty_per_flow_hour ) @@ -149,16 +149,16 @@ def __init__( label: str, bus: str, size: Union[Scalar, InvestParameters] = None, - fixed_relative_profile: Optional[NumericDataTS] = None, - relative_minimum: NumericDataTS = 0, - relative_maximum: NumericDataTS = 1, + fixed_relative_profile: Optional[NumericDataUser] = None, + relative_minimum: NumericDataUser = 0, + relative_maximum: NumericDataUser = 1, effects_per_flow_hour: Optional[EffectValuesUser] = None, on_off_parameters: Optional[OnOffParameters] = None, flow_hours_total_max: Optional[Scalar] = None, flow_hours_total_min: Optional[Scalar] = None, load_factor_min: Optional[Scalar] = None, load_factor_max: Optional[Scalar] = None, - previous_flow_rate: Optional[NumericData] = None, + previous_flow_rate: Optional[NumericDataUser] = None, meta_data: Optional[Dict] = None, ): r""" @@ -230,16 +230,16 @@ def create_model(self, model: SystemModel) -> 'FlowModel': return self.model def transform_data(self, flow_system: 'FlowSystem'): - self.relative_minimum = flow_system.create_time_series( + self.relative_minimum = flow_system.fit_to_model_coords( f'{self.label_full}|relative_minimum', self.relative_minimum ) - self.relative_maximum = flow_system.create_time_series( + self.relative_maximum = flow_system.fit_to_model_coords( f'{self.label_full}|relative_maximum', self.relative_maximum ) - self.fixed_relative_profile = flow_system.create_time_series( + self.fixed_relative_profile = flow_system.fit_to_model_coords( f'{self.label_full}|fixed_relative_profile', self.fixed_relative_profile ) - self.effects_per_flow_hour = flow_system.create_effect_time_series( + self.effects_per_flow_hour = flow_system.fit_effects_to_model_coords( self.label_full, self.effects_per_flow_hour, 'per_flow_hour' ) if self.on_off_parameters is not None: @@ -411,7 +411,7 @@ def _create_bounds_for_load_factor(self): ) @property - def flow_rate_bounds_on(self) -> Tuple[NumericData, NumericData]: + def flow_rate_bounds_on(self) -> Tuple[NumericDataUser, NumericDataUser]: """Returns absolute flow rate bounds. Important for OnOffModel""" relative_minimum, relative_maximum = self.flow_rate_lower_bound_relative, self.flow_rate_upper_bound_relative size = self.element.size @@ -422,7 +422,7 @@ def flow_rate_bounds_on(self) -> Tuple[NumericData, NumericData]: return relative_minimum * size.minimum_size, relative_maximum * size.maximum_size @property - def flow_rate_lower_bound_relative(self) -> NumericData: + def flow_rate_lower_bound_relative(self) -> NumericDataUser: """Returns the lower bound of the flow_rate relative to its size""" fixed_profile = self.element.fixed_relative_profile if fixed_profile is None: @@ -430,7 +430,7 @@ def flow_rate_lower_bound_relative(self) -> NumericData: return fixed_profile @property - def flow_rate_upper_bound_relative(self) -> NumericData: + def flow_rate_upper_bound_relative(self) -> NumericDataUser: """ Returns the upper bound of the flow_rate relative to its size""" fixed_profile = self.element.fixed_relative_profile if fixed_profile is None: @@ -438,7 +438,7 @@ def flow_rate_upper_bound_relative(self) -> NumericData: return fixed_profile @property - def flow_rate_lower_bound(self) -> NumericData: + def flow_rate_lower_bound(self) -> NumericDataUser: """ Returns the minimum bound the flow_rate can reach. Further constraining might be done in OnOffModel and InvestmentModel @@ -452,7 +452,7 @@ def flow_rate_lower_bound(self) -> NumericData: return self.flow_rate_lower_bound_relative * self.element.size @property - def flow_rate_upper_bound(self) -> NumericData: + def flow_rate_upper_bound(self) -> NumericDataUser: """ Returns the maximum bound the flow_rate can reach. Further constraining might be done in OnOffModel and InvestmentModel diff --git a/flixopt/features.py b/flixopt/features.py index dc719a2a6..20412ed46 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -11,7 +11,7 @@ from . import utils from .config import CONFIG -from .core import NumericData, Scalar, TimeSeries +from .core import NumericDataUser, Scalar, TimeSeries from .interface import InvestParameters, OnOffParameters, Piecewise from .structure import Model, SystemModel @@ -27,7 +27,7 @@ def __init__( label_of_element: str, parameters: InvestParameters, defining_variable: [linopy.Variable], - relative_bounds_of_defining_variable: Tuple[NumericData, NumericData], + relative_bounds_of_defining_variable: Tuple[NumericDataUser, NumericDataUser], label: Optional[str] = None, on_variable: Optional[linopy.Variable] = None, ): @@ -203,12 +203,12 @@ def __init__( model: SystemModel, label_of_element: str, defining_variables: List[linopy.Variable], - defining_bounds: List[Tuple[NumericData, NumericData]], - previous_values: List[Optional[NumericData]] = None, + defining_bounds: List[Tuple[NumericDataUser, NumericDataUser]], + previous_values: List[Optional[NumericDataUser]] = None, use_off: bool = True, - on_hours_total_min: Optional[NumericData] = 0, - on_hours_total_max: Optional[NumericData] = None, - effects_per_running_hour: Dict[str, NumericData] = None, + on_hours_total_min: Optional[NumericDataUser] = 0, + on_hours_total_max: Optional[NumericDataUser] = None, + effects_per_running_hour: Dict[str, NumericDataUser] = None, label: Optional[str] = None, ): """ @@ -344,7 +344,7 @@ def previous_off_states(self): return 1 - self.previous_states @staticmethod - def compute_previous_states(previous_values: List[NumericData], epsilon: float = 1e-5) -> np.ndarray: + def compute_previous_states(previous_values: List[NumericDataUser], epsilon: float = 1e-5) -> np.ndarray: """Computes the previous states {0, 1} of defining variables as a binary array from their previous values.""" if not previous_values or all([val is None for val in previous_values]): return np.array([0]) @@ -451,9 +451,9 @@ def __init__( model: SystemModel, label_of_element: str, state_variable: linopy.Variable, - minimum_duration: Optional[NumericData] = None, - maximum_duration: Optional[NumericData] = None, - previous_states: Optional[NumericData] = None, + minimum_duration: Optional[NumericDataUser] = None, + maximum_duration: Optional[NumericDataUser] = None, + previous_states: Optional[NumericDataUser] = None, label: Optional[str] = None, ): """ @@ -570,7 +570,7 @@ def previous_duration(self) -> Scalar: @staticmethod def compute_consecutive_hours_in_state( - binary_values: NumericData, hours_per_timestep: Union[int, float, np.ndarray] + binary_values: NumericDataUser, hours_per_timestep: Union[int, float, np.ndarray] ) -> Scalar: """ Computes the final consecutive duration in state 'on' (=1) in hours, from a binary array. @@ -629,8 +629,8 @@ def __init__( on_off_parameters: OnOffParameters, label_of_element: str, defining_variables: List[linopy.Variable], - defining_bounds: List[Tuple[NumericData, NumericData]], - previous_values: List[Optional[NumericData]], + defining_bounds: List[Tuple[NumericDataUser, NumericDataUser]], + previous_values: List[Optional[NumericDataUser]], label: Optional[str] = None, ): """ @@ -918,8 +918,8 @@ def __init__( label_full: Optional[str] = None, total_max: Optional[Scalar] = None, total_min: Optional[Scalar] = None, - max_per_hour: Optional[NumericData] = None, - min_per_hour: Optional[NumericData] = None, + max_per_hour: Optional[NumericDataUser] = None, + min_per_hour: Optional[NumericDataUser] = None, ): super().__init__(model, label_of_element=label_of_element, label=label, label_full=label_full) if not shares_are_time_series: # If the condition is True diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 386f54a72..024d8b3c5 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -16,10 +16,17 @@ from rich.pretty import Pretty from . import io as fx_io -from .core import NumericData, NumericDataTS, TimeSeriesCollection, TimeSeries, DataConverter, ConversionError, TimeSeriesData -from .effects import Effect, EffectCollection, EffectTimeSeries, EffectValuesDict, EffectValuesUser +from .core import ConversionError, DataConverter, NumericDataInternal, NumericDataUser, TimeSeriesData +from .effects import Effect, EffectCollection, EffectValuesInternal, EffectValuesUser from .elements import Bus, Component, Flow -from .structure import CLASS_REGISTRY, Element, SystemModel, get_compact_representation, get_str_representation, Interface +from .structure import ( + CLASS_REGISTRY, + Element, + Interface, + SystemModel, + get_compact_representation, + get_str_representation, +) if TYPE_CHECKING: import pyvis @@ -280,23 +287,22 @@ def to_json(self, path: Union[str, pathlib.Path]): super().to_json(path) - def create_time_series( + def fit_to_model_coords( self, name: str, - data: Optional[Union[NumericData, TimeSeriesData, TimeSeries]], + data: Optional[NumericDataUser], needs_extra_timestep: bool = False, - ) -> Optional[TimeSeries]: + ) -> Optional[NumericDataInternal]: """ - Create a TimeSeries-like object (now just an xr.DataArray with proper coordinates). - This method is kept for API compatibility but simplified. + Fit data to model coordinate system (currently time, but extensible). Args: - name: Name of the time series - data: Data to convert - needs_extra_timestep: Whether to use timesteps_extra + name: Name of the data + data: Data to fit to model coordinates + needs_extra_timestep: Whether to use extended time coordinates Returns: - xr.DataArray with proper time coordinates + xr.DataArray aligned to model coordinate system """ if data is None: return None @@ -316,22 +322,22 @@ def create_time_series( else: return DataConverter.to_dataarray(data, timesteps=target_timesteps).rename(name) - def create_effect_time_series( + def fit_effects_to_model_coords( self, label_prefix: Optional[str], - effect_values: EffectValuesUser, + effect_values: Optional[EffectValuesUser], label_suffix: Optional[str] = None, - ) -> Optional[Dict[str, xr.DataArray]]: + ) -> Optional[EffectValuesInternal]: """ - Transform EffectValues to effect DataArrays. - Simplified version that returns DataArrays directly. + Transform EffectValues from the user to Internal Datatypes aligned with model coordinates. """ - effect_values_dict: Optional[EffectValuesDict] = self.effects.create_effect_values_dict(effect_values) - if effect_values_dict is None: + if effect_values is None: return None + effect_values_dict = self.effects.create_effect_values_dict(effect_values) + return { - effect: self.create_time_series('|'.join(filter(None, [label_prefix, effect, label_suffix])), value) + effect: self.fit_to_model_coords('|'.join(filter(None, [label_prefix, effect, label_suffix])), value) for effect, value in effect_values_dict.items() } @@ -505,7 +511,7 @@ def format_elements(element_names: list, label: str, alignment: int = 12): freq_str = str(self.timesteps.freq).replace('<', '').replace('>', '') if self.timesteps.freq else 'irregular' lines = [ - f'FlowSystem Overview:', + 'FlowSystem Overview:', f'{"─" * 50}', time_period, f'Timesteps: {len(self.timesteps)} ({freq_str})', diff --git a/flixopt/interface.py b/flixopt/interface.py index c38d6c619..e5ee962ed 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, Dict, Iterator, List, Optional, Union from .config import CONFIG -from .core import NumericData, NumericDataTS, Scalar +from .core import NumericDataUser, Scalar from .structure import Interface, register_class_for_io if TYPE_CHECKING: # for type checking and preventing circular imports @@ -20,7 +20,7 @@ @register_class_for_io class Piece(Interface): - def __init__(self, start: NumericData, end: NumericData): + def __init__(self, start: NumericDataUser, end: NumericDataUser): """ Define a Piece, which is part of a Piecewise object. @@ -32,8 +32,8 @@ def __init__(self, start: NumericData, end: NumericData): self.end = end def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): - self.start = flow_system.create_time_series(f'{name_prefix}|start', self.start) - self.end = flow_system.create_time_series(f'{name_prefix}|end', self.end) + self.start = flow_system.fit_to_model_coords(f'{name_prefix}|start', self.start) + self.end = flow_system.fit_to_model_coords(f'{name_prefix}|end', self.end) @register_class_for_io @@ -175,10 +175,10 @@ def __init__( effects_per_running_hour: Optional['EffectValuesUser'] = None, on_hours_total_min: Optional[int] = None, on_hours_total_max: Optional[int] = None, - consecutive_on_hours_min: Optional[NumericData] = None, - consecutive_on_hours_max: Optional[NumericData] = None, - consecutive_off_hours_min: Optional[NumericData] = None, - consecutive_off_hours_max: Optional[NumericData] = None, + consecutive_on_hours_min: Optional[NumericDataUser] = None, + consecutive_on_hours_max: Optional[NumericDataUser] = None, + consecutive_off_hours_min: Optional[NumericDataUser] = None, + consecutive_off_hours_max: Optional[NumericDataUser] = None, switch_on_total_max: Optional[int] = None, force_switch_on: bool = False, ): @@ -206,30 +206,30 @@ def __init__( self.effects_per_running_hour: EffectValuesUser = effects_per_running_hour or {} self.on_hours_total_min: Scalar = on_hours_total_min self.on_hours_total_max: Scalar = on_hours_total_max - self.consecutive_on_hours_min: NumericDataTS = consecutive_on_hours_min - self.consecutive_on_hours_max: NumericDataTS = consecutive_on_hours_max - self.consecutive_off_hours_min: NumericDataTS = consecutive_off_hours_min - self.consecutive_off_hours_max: NumericDataTS = consecutive_off_hours_max + self.consecutive_on_hours_min: NumericDataUser = consecutive_on_hours_min + self.consecutive_on_hours_max: NumericDataUser = consecutive_on_hours_max + self.consecutive_off_hours_min: NumericDataUser = consecutive_off_hours_min + self.consecutive_off_hours_max: NumericDataUser = consecutive_off_hours_max self.switch_on_total_max: Scalar = switch_on_total_max self.force_switch_on: bool = force_switch_on def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): - self.effects_per_switch_on = flow_system.create_effect_time_series( + self.effects_per_switch_on = flow_system.fit_effects_to_model_coords( name_prefix, self.effects_per_switch_on, 'per_switch_on' ) - self.effects_per_running_hour = flow_system.create_effect_time_series( + self.effects_per_running_hour = flow_system.fit_effects_to_model_coords( name_prefix, self.effects_per_running_hour, 'per_running_hour' ) - self.consecutive_on_hours_min = flow_system.create_time_series( + self.consecutive_on_hours_min = flow_system.fit_to_model_coords( f'{name_prefix}|consecutive_on_hours_min', self.consecutive_on_hours_min ) - self.consecutive_on_hours_max = flow_system.create_time_series( + self.consecutive_on_hours_max = flow_system.fit_to_model_coords( f'{name_prefix}|consecutive_on_hours_max', self.consecutive_on_hours_max ) - self.consecutive_off_hours_min = flow_system.create_time_series( + self.consecutive_off_hours_min = flow_system.fit_to_model_coords( f'{name_prefix}|consecutive_off_hours_min', self.consecutive_off_hours_min ) - self.consecutive_off_hours_max = flow_system.create_time_series( + self.consecutive_off_hours_max = flow_system.fit_to_model_coords( f'{name_prefix}|consecutive_off_hours_max', self.consecutive_off_hours_max ) diff --git a/flixopt/linear_converters.py b/flixopt/linear_converters.py index 3fd032632..94463c492 100644 --- a/flixopt/linear_converters.py +++ b/flixopt/linear_converters.py @@ -8,7 +8,7 @@ import numpy as np from .components import LinearConverter -from .core import NumericDataTS, TimeSeriesData +from .core import NumericDataUser, TimeSeriesData from .elements import Flow from .interface import OnOffParameters from .structure import register_class_for_io @@ -21,7 +21,7 @@ class Boiler(LinearConverter): def __init__( self, label: str, - eta: NumericDataTS, + eta: NumericDataUser, Q_fu: Flow, Q_th: Flow, on_off_parameters: OnOffParameters = None, @@ -62,7 +62,7 @@ class Power2Heat(LinearConverter): def __init__( self, label: str, - eta: NumericDataTS, + eta: NumericDataUser, P_el: Flow, Q_th: Flow, on_off_parameters: OnOffParameters = None, @@ -104,7 +104,7 @@ class HeatPump(LinearConverter): def __init__( self, label: str, - COP: NumericDataTS, + COP: NumericDataUser, P_el: Flow, Q_th: Flow, on_off_parameters: OnOffParameters = None, @@ -146,7 +146,7 @@ class CoolingTower(LinearConverter): def __init__( self, label: str, - specific_electricity_demand: NumericDataTS, + specific_electricity_demand: NumericDataUser, P_el: Flow, Q_th: Flow, on_off_parameters: OnOffParameters = None, @@ -190,8 +190,8 @@ class CHP(LinearConverter): def __init__( self, label: str, - eta_th: NumericDataTS, - eta_el: NumericDataTS, + eta_th: NumericDataUser, + eta_el: NumericDataUser, Q_fu: Flow, P_el: Flow, Q_th: Flow, @@ -251,7 +251,7 @@ class HeatPumpWithSource(LinearConverter): def __init__( self, label: str, - COP: NumericDataTS, + COP: NumericDataUser, P_el: Flow, Q_ab: Flow, Q_th: Flow, @@ -297,11 +297,11 @@ def COP(self, value): # noqa: N802 def check_bounds( - value: NumericDataTS, + value: NumericDataUser, parameter_label: str, element_label: str, - lower_bound: NumericDataTS, - upper_bound: NumericDataTS, + lower_bound: NumericDataUser, + upper_bound: NumericDataUser, ) -> None: """ Check if the value is within the bounds. The bounds are exclusive. diff --git a/flixopt/structure.py b/flixopt/structure.py index 9cb830ff0..1e3d2849e 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -18,9 +18,9 @@ from rich.console import Console from rich.pretty import Pretty -from .config import CONFIG -from .core import NumericData, Scalar, TimeSeriesCollection, TimeSeries, TimeSeriesData from . import io as fx_io +from .config import CONFIG +from .core import NumericDataUser, Scalar, TimeSeriesData if TYPE_CHECKING: # for type checking and preventing circular imports from .effects import EffectCollectionModel @@ -851,8 +851,6 @@ def copy_and_convert_datatypes(data: Any, use_numpy: bool = True, use_element_la ) return copy_and_convert_datatypes(data.tolist(), use_numpy, use_element_label) - elif isinstance(data, TimeSeries): - return copy_and_convert_datatypes(data, use_numpy, use_element_label) elif isinstance(data, TimeSeriesData): return copy_and_convert_datatypes(data.data, use_numpy, use_element_label) From 10d2925cec8639df06505829889f33b83cc99d4e Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 25 Jun 2025 11:50:23 +0200 Subject: [PATCH 086/448] Remove TImeSeries and TimeSeriesCollection entirely --- flixopt/components.py | 7 ++++--- flixopt/elements.py | 2 +- flixopt/features.py | 2 +- flixopt/io.py | 37 ------------------------------------- flixopt/structure.py | 6 +++--- 5 files changed, 9 insertions(+), 45 deletions(-) diff --git a/flixopt/components.py b/flixopt/components.py index 8e172d573..3f41783a8 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -7,9 +7,10 @@ import linopy import numpy as np +import xarray as xr from . import utils -from .core import NumericDataUser, PlausibilityError, Scalar, TimeSeries +from .core import NumericDataUser, PlausibilityError, Scalar from .elements import Component, ComponentModel, Flow from .features import InvestmentModel, OnOffModel, PiecewiseModel from .interface import InvestParameters, OnOffParameters, PiecewiseConversion @@ -98,8 +99,8 @@ def transform_data(self, flow_system: 'FlowSystem'): if self.piecewise_conversion: self.piecewise_conversion.transform_data(flow_system, f'{self.label_full}|PiecewiseConversion') - def _transform_conversion_factors(self, flow_system: 'FlowSystem') -> List[Dict[str, TimeSeries]]: - """macht alle Faktoren, die nicht TimeSeries sind, zu TimeSeries""" + def _transform_conversion_factors(self, flow_system: 'FlowSystem') -> List[Dict[str, xr.DataArray]]: + """Converts all conversion factors to internal datatypes""" list_of_conversion_factors = [] for idx, conversion_factor in enumerate(self.conversion_factors): transformed_dict = {} diff --git a/flixopt/elements.py b/flixopt/elements.py index a2ba8f7c1..061a00b65 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -10,7 +10,7 @@ import numpy as np from .config import CONFIG -from .core import NumericDataUser, PlausibilityError, Scalar, TimeSeriesCollection +from .core import NumericDataUser, PlausibilityError, Scalar from .effects import EffectValuesUser from .features import InvestmentModel, OnOffModel, PreventSimultaneousUsageModel from .interface import InvestParameters, OnOffParameters diff --git a/flixopt/features.py b/flixopt/features.py index 20412ed46..5bc8f7922 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -11,7 +11,7 @@ from . import utils from .config import CONFIG -from .core import NumericDataUser, Scalar, TimeSeries +from .core import NumericDataUser, Scalar from .interface import InvestParameters, OnOffParameters, Piecewise from .structure import Model, SystemModel diff --git a/flixopt/io.py b/flixopt/io.py index 23b06cacd..b01844f3a 100644 --- a/flixopt/io.py +++ b/flixopt/io.py @@ -10,47 +10,10 @@ import xarray as xr import yaml -from .core import TimeSeries logger = logging.getLogger('flixopt') -def replace_timeseries(obj, mode: Literal['name', 'stats', 'data'] = 'name'): - """Recursively replaces TimeSeries objects with their names prefixed by '::::'.""" - if isinstance(obj, dict): - return {k: replace_timeseries(v, mode) for k, v in obj.items()} - elif isinstance(obj, list): - return [replace_timeseries(v, mode) for v in obj] - elif isinstance(obj, TimeSeries): # Adjust this based on the actual class - if obj.all_equal: - return obj.values[0].item() - elif mode == 'name': - return f'::::{obj.name}' - elif mode == 'stats': - return obj.stats - elif mode == 'data': - return obj - else: - raise ValueError(f'Invalid mode {mode}') - else: - return obj - - -def insert_dataarray(obj, ds: xr.Dataset): - """Recursively inserts TimeSeries objects into a dataset.""" - if isinstance(obj, dict): - return {k: insert_dataarray(v, ds) for k, v in obj.items()} - elif isinstance(obj, list): - return [insert_dataarray(v, ds) for v in obj] - elif isinstance(obj, str) and obj.startswith('::::'): - da = ds[obj[4:]] - if da.isel(time=-1).isnull(): - return da.isel(time=slice(0, -1)) - return da - else: - return obj - - def remove_none_and_empty(obj): """Recursively removes None and empty dicts and lists values from a dictionary or list.""" diff --git a/flixopt/structure.py b/flixopt/structure.py index 1e3d2849e..cc7b166eb 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -152,7 +152,7 @@ def transform_data(self, flow_system: 'FlowSystem'): def _create_reference_structure(self) -> Tuple[Dict, Dict[str, xr.DataArray]]: """ - Convert all DataArrays/TimeSeries to references and extract them. + Convert all DataArrays to references and extract them. This is the core method that both to_dict() and to_dataset() build upon. Returns: @@ -204,7 +204,7 @@ def _is_empty_container(obj) -> bool: def _extract_dataarrays_recursive(self, obj, context_name: str = '') -> Tuple[Any, Dict[str, xr.DataArray]]: """ - Recursively extract DataArrays/TimeSeries from nested structures. + Recursively extract DataArrays from nested structures. Args: obj: Object to process @@ -392,7 +392,7 @@ def _serialize_to_basic_types(self, obj): def to_dataset(self) -> xr.Dataset: """ Convert the object to an xarray Dataset representation. - All DataArrays and TimeSeries become dataset variables, everything else goes to attrs. + All DataArrays become dataset variables, everything else goes to attrs. Returns: xr.Dataset: Dataset containing all DataArrays with basic objects only in attributes From cf9d17f4d34098985cda4be4ae24bcc7fc093594 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 25 Jun 2025 11:52:07 +0200 Subject: [PATCH 087/448] Remove old method --- flixopt/core.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/flixopt/core.py b/flixopt/core.py index 4ab97b219..1b91cc1cc 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -229,16 +229,3 @@ def to_dataarray(data: NumericDataUser, timesteps: pd.DatetimeIndex) -> xr.DataA if isinstance(e, ConversionError): raise raise ConversionError(f'Converting data {type(data)} to xarray.DataArray raised an error: {str(e)}') from e - - -def get_numeric_stats(data: xr.DataArray, decimals: int = 2, padd: int = 10) -> str: - """Calculates the mean, median, min, max, and standard deviation of a numeric DataArray.""" - format_spec = f'>{padd}.{decimals}f' if padd else f'.{decimals}f' - if np.unique(data).size == 1: - return f'{data.max().item():{format_spec}} (constant)' - mean = data.mean().item() - median = data.median().item() - min_val = data.min().item() - max_val = data.max().item() - std = data.std().item() - return f'{mean:{format_spec}} (mean), {median:{format_spec}} (median), {min_val:{format_spec}} (min), {max_val:{format_spec}} (max), {std:{format_spec}} (std)' From bd52e059a6bf7228f5865c2bbf5f75dcaf554103 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 25 Jun 2025 16:00:33 +0200 Subject: [PATCH 088/448] Add option to get structure with stats of dataarrays --- flixopt/core.py | 28 ++++++++++++++++++++++++++++ flixopt/flow_system.py | 5 +++-- flixopt/structure.py | 28 +++++++++++++++++++++++++--- 3 files changed, 56 insertions(+), 5 deletions(-) diff --git a/flixopt/core.py b/flixopt/core.py index 1b91cc1cc..61e951019 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -229,3 +229,31 @@ def to_dataarray(data: NumericDataUser, timesteps: pd.DatetimeIndex) -> xr.DataA if isinstance(e, ConversionError): raise raise ConversionError(f'Converting data {type(data)} to xarray.DataArray raised an error: {str(e)}') from e + + +def get_dataarray_stats(arr: xr.DataArray) -> Dict: + """Generate statistical summary of a DataArray.""" + stats = {} + + if arr.dtype.kind in 'biufc': # bool, int, uint, float, complex + try: + stats.update( + { + 'min': float(arr.min().values), + 'max': float(arr.max().values), + 'mean': float(arr.mean().values), + 'median': float(arr.median().values), + 'std': float(arr.std().values), + 'count': int(arr.count().values), # non-null count + } + ) + + # Add null count only if there are nulls + null_count = int(arr.isnull().sum().values) + if null_count > 0: + stats['nulls'] = null_count + + except Exception: + pass + + return stats diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 024d8b3c5..64f9b39bd 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -259,19 +259,20 @@ def to_netcdf(self, path: Union[str, pathlib.Path], compression: int = 0): super().to_netcdf(path, compression) logger.info(f'Saved FlowSystem to {path}') - def get_structure(self, clean: bool = False) -> Dict: + def get_structure(self, clean: bool = False, stats: bool = False) -> Dict: """ Get FlowSystem structure. Ensures FlowSystem is connected before getting structure. Args: clean: If True, remove None and empty dicts and lists. + stats: If True, replace DataArray references with statistics """ if not self._connected_and_transformed: logger.warning('FlowSystem is not connected. Calling connect_and_transform() now.') self.connect_and_transform() - return super().get_structure(clean) + return super().get_structure(clean, stats) def to_json(self, path: Union[str, pathlib.Path]): """ diff --git a/flixopt/structure.py b/flixopt/structure.py index cc7b166eb..651aa765a 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -20,7 +20,7 @@ from . import io as fx_io from .config import CONFIG -from .core import NumericDataUser, Scalar, TimeSeriesData +from .core import NumericDataUser, Scalar, TimeSeriesData, get_dataarray_stats if TYPE_CHECKING: # for type checking and preventing circular imports from .effects import EffectCollectionModel @@ -482,21 +482,43 @@ def from_netcdf(cls, path: Union[str, pathlib.Path]) -> 'Interface': except Exception as e: raise IOError(f'Failed to load {cls.__name__} from NetCDF file {path}: {e}') from e - def get_structure(self, clean: bool = False) -> Dict: + def get_structure(self, clean: bool = False, stats: bool = False) -> Dict: """ Get object structure as a dictionary. Args: clean: If True, remove None and empty dicts and lists. + stats: If True, replace DataArray references with statistics Returns: Dictionary representation of the object structure """ - reference_structure, _ = self._create_reference_structure() + reference_structure, extracted_arrays = self._create_reference_structure() + + if stats: + # Replace references with statistics + reference_structure = self._replace_references_with_stats(reference_structure, extracted_arrays) + if clean: return fx_io.remove_none_and_empty(reference_structure) return reference_structure + def _replace_references_with_stats(self, structure, arrays_dict: Dict[str, xr.DataArray]): + """Replace DataArray references with statistical summaries.""" + if isinstance(structure, str) and structure.startswith(':::'): + array_name = structure[3:] + if array_name in arrays_dict: + return get_dataarray_stats(arrays_dict[array_name]) + return structure + + elif isinstance(structure, dict): + return {k: self._replace_references_with_stats(v, arrays_dict) for k, v in structure.items()} + + elif isinstance(structure, list): + return [self._replace_references_with_stats(item, arrays_dict) for item in structure] + + return structure + def to_json(self, path: Union[str, pathlib.Path]): """ Save the object to a JSON file. From aa366892ae3ebbdf844932f9d442c5378edeba03 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 25 Jun 2025 16:22:49 +0200 Subject: [PATCH 089/448] Change __str__ method --- flixopt/structure.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/flixopt/structure.py b/flixopt/structure.py index 651aa765a..33817ec4f 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -532,7 +532,7 @@ def to_json(self, path: Union[str, pathlib.Path]): """ try: # Use the stats mode for JSON export (cleaner output) - data = get_compact_representation(self.get_structure(clean=True)) + data = self.get_structure(clean=True, stats=True) with open(path, 'w', encoding='utf-8') as f: json.dump(data, f, indent=4, ensure_ascii=False) except Exception as e: @@ -566,7 +566,11 @@ def __repr__(self): def __str__(self): """Return a user-friendly string representation.""" try: - return get_str_representation(self.get_structure(clean=True)) + data = self.get_structure(clean=True, stats=True) + with StringIO() as output_buffer: + console = Console(file=output_buffer, width=1000) # Adjust width as needed + console.print(Pretty(data, expand_all=True, indent_guides=True)) + return output_buffer.getvalue() except Exception: # Fallback if structure generation fails return f'{self.__class__.__name__} instance' From 63b1c926ea42b6cc9e374967237c4c6ee1ebc363 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 25 Jun 2025 16:23:14 +0200 Subject: [PATCH 090/448] Remove old methods --- flixopt/io.py | 1 - flixopt/structure.py | 186 ------------------------------------------- 2 files changed, 187 deletions(-) diff --git a/flixopt/io.py b/flixopt/io.py index b01844f3a..9527eb66a 100644 --- a/flixopt/io.py +++ b/flixopt/io.py @@ -10,7 +10,6 @@ import xarray as xr import yaml - logger = logging.getLogger('flixopt') diff --git a/flixopt/structure.py b/flixopt/structure.py index 33817ec4f..b4fcf7d38 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -123,20 +123,6 @@ class Interface: Subclasses must implement: transform_data(flow_system): Transform data to match FlowSystem dimensions - - Example: - >>> class MyComponent(Interface): - ... def __init__(self, name: str, power_data: xr.DataArray): - ... self.name = name - ... self.power_data = power_data - ... - ... def transform_data(self, flow_system): - ... # Transform power_data to match flow_system timesteps - ... pass - >>> - >>> component = MyComponent('gen1', power_array) - >>> component.to_netcdf('component.nc') # Save to file - >>> restored = MyComponent.from_netcdf('component.nc') # Load from file """ def transform_data(self, flow_system: 'FlowSystem'): @@ -798,175 +784,3 @@ def results_structure(self): 'variables': list(self.variables), 'constraints': list(self.constraints), } - - -def copy_and_convert_datatypes(data: Any, use_numpy: bool = True, use_element_label: bool = False) -> Any: - """ - Converts values in a nested data structure into JSON-compatible types while preserving or transforming numpy arrays - and custom `Element` objects based on the specified options. - - The function handles various data types and transforms them into a consistent, readable format: - - Primitive types (`int`, `float`, `str`, `bool`, `None`) are returned as-is. - - Numpy scalars are converted to their corresponding Python scalar types. - - Collections (`list`, `tuple`, `set`, `dict`) are recursively processed to ensure all elements are compatible. - - Numpy arrays are preserved or converted to lists, depending on `use_numpy`. - - Custom `Element` objects can be represented either by their `label` or their initialization parameters as a dictionary. - - Timestamps (`datetime`) are converted to ISO 8601 strings. - - Args: - data: The input data to process, which may be deeply nested and contain a mix of types. - use_numpy: If `True`, numeric numpy arrays (`np.ndarray`) are preserved as-is. If `False`, they are converted to lists. - Default is `True`. - use_element_label: If `True`, `Element` objects are represented by their `label`. If `False`, they are converted into a dictionary - based on their initialization parameters. Default is `False`. - - Returns: - A transformed version of the input data, containing only JSON-compatible types: - - `int`, `float`, `str`, `bool`, `None` - - `list`, `dict` - - `np.ndarray` (if `use_numpy=True`. This is NOT JSON-compatible) - - Raises: - TypeError: If the data cannot be converted to the specified types. - - Examples: - >>> copy_and_convert_datatypes({'a': np.array([1, 2, 3]), 'b': Element(label='example')}) - {'a': array([1, 2, 3]), 'b': {'class': 'Element', 'label': 'example'}} - - >>> copy_and_convert_datatypes({'a': np.array([1, 2, 3]), 'b': Element(label='example')}, use_numpy=False) - {'a': [1, 2, 3], 'b': {'class': 'Element', 'label': 'example'}} - - Notes: - - The function gracefully handles unexpected types by issuing a warning and returning a deep copy of the data. - - Empty collections (lists, dictionaries) and default parameter values in `Element` objects are omitted from the output. - - Numpy arrays with non-numeric data types are automatically converted to lists. - """ - if isinstance(data, np.integer): # This must be checked before checking for regular int and float! - return int(data) - elif isinstance(data, np.floating): - return float(data) - - elif isinstance(data, (int, float, str, bool, type(None))): - return data - elif isinstance(data, datetime): - return data.isoformat() - - elif isinstance(data, (tuple, set)): - return copy_and_convert_datatypes([item for item in data], use_numpy, use_element_label) - elif isinstance(data, dict): - return { - copy_and_convert_datatypes(key, use_numpy, use_element_label=True): copy_and_convert_datatypes( - value, use_numpy, use_element_label - ) - for key, value in data.items() - } - elif isinstance(data, list): # Shorten arrays/lists to be readable - if use_numpy and all([isinstance(value, (int, float)) for value in data]): - return np.array([item for item in data]) - else: - return [copy_and_convert_datatypes(item, use_numpy, use_element_label) for item in data] - - elif isinstance(data, np.ndarray): - if not use_numpy: - return copy_and_convert_datatypes(data.tolist(), use_numpy, use_element_label) - elif use_numpy and np.issubdtype(data.dtype, np.number): - return data - else: - logger.critical( - f'An np.array with non-numeric content was found: {data=}.It will be converted to a list instead' - ) - return copy_and_convert_datatypes(data.tolist(), use_numpy, use_element_label) - - elif isinstance(data, TimeSeriesData): - return copy_and_convert_datatypes(data.data, use_numpy, use_element_label) - - elif isinstance(data, Interface): - if use_element_label and isinstance(data, Element): - return data.label - return data.infos(use_numpy, use_element_label) - elif isinstance(data, xr.DataArray): - # TODO: This is a temporary basic work around - return copy_and_convert_datatypes(data.values, use_numpy, use_element_label) - else: - raise TypeError(f'copy_and_convert_datatypes() did get unexpected data of type "{type(data)}": {data=}') - - -def get_compact_representation(data: Any, array_threshold: int = 50, decimals: int = 2) -> Dict: - """ - Generate a compact json serializable representation of deeply nested data. - Numpy arrays are statistically described if they exceed a threshold and converted to lists. - - Args: - data (Any): The data to format and represent. - array_threshold (int): Maximum length of NumPy arrays to display. Longer arrays are statistically described. - decimals (int): Number of decimal places in which to describe the arrays. - - Returns: - Dict: A dictionary representation of the data - """ - - def format_np_array_if_found(value: Any) -> Any: - """Recursively processes the data, formatting NumPy arrays.""" - if isinstance(value, (int, float, str, bool, type(None))): - return value - elif isinstance(value, np.ndarray): - return describe_numpy_arrays(value) - elif isinstance(value, dict): - return {format_np_array_if_found(k): format_np_array_if_found(v) for k, v in value.items()} - elif isinstance(value, (list, tuple, set)): - return [format_np_array_if_found(v) for v in value] - else: - logger.warning( - f'Unexpected value found when trying to format numpy array numpy array: {type(value)=}; {value=}' - ) - return value - - def describe_numpy_arrays(arr: np.ndarray) -> Union[str, List]: - """Shortens NumPy arrays if they exceed the specified length.""" - - def normalized_center_of_mass(array: Any) -> float: - # position in array (0 bis 1 normiert) - positions = np.linspace(0, 1, len(array)) # weights w_i - # mass center - if np.sum(array) == 0: - return np.nan - else: - return np.sum(positions * array) / np.sum(array) - - if arr.size > array_threshold: # Calculate basic statistics - fmt = f'.{decimals}f' - return ( - f'Array (min={np.min(arr):{fmt}}, max={np.max(arr):{fmt}}, mean={np.mean(arr):{fmt}}, ' - f'median={np.median(arr):{fmt}}, std={np.std(arr):{fmt}}, len={len(arr)}, ' - f'center={normalized_center_of_mass(arr):{fmt}})' - ) - else: - return np.around(arr, decimals=decimals).tolist() - - # Process the data to handle NumPy arrays - formatted_data = format_np_array_if_found(copy_and_convert_datatypes(data, use_numpy=True)) - - return formatted_data - - -def get_str_representation(data: Any, array_threshold: int = 50, decimals: int = 2) -> str: - """ - Generate a string representation of deeply nested data using `rich.print`. - NumPy arrays are shortened to the specified length and converted to strings. - - Args: - data (Any): The data to format and represent. - array_threshold (int): Maximum length of NumPy arrays to display. Longer arrays are statistically described. - decimals (int): Number of decimal places in which to describe the arrays. - - Returns: - str: The formatted string representation of the data. - """ - - formatted_data = get_compact_representation(data, array_threshold, decimals) - - # Use Rich to format and print the data - with StringIO() as output_buffer: - console = Console(file=output_buffer, width=1000) # Adjust width as needed - console.print(Pretty(formatted_data, expand_all=True, indent_guides=True)) - return output_buffer.getvalue() From 29062fac6df49614955b33244e95ad55bee05225 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 25 Jun 2025 16:24:57 +0200 Subject: [PATCH 091/448] remove old imports --- flixopt/calculation.py | 2 +- flixopt/flow_system.py | 9 +-------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/flixopt/calculation.py b/flixopt/calculation.py index f52c1ca19..251a50075 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -29,7 +29,7 @@ from .flow_system import FlowSystem from .results import CalculationResults, SegmentedCalculationResults from .solvers import _Solver -from .structure import SystemModel, copy_and_convert_datatypes, get_compact_representation +from .structure import SystemModel logger = logging.getLogger('flixopt') diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 64f9b39bd..7724a9e61 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -19,14 +19,7 @@ from .core import ConversionError, DataConverter, NumericDataInternal, NumericDataUser, TimeSeriesData from .effects import Effect, EffectCollection, EffectValuesInternal, EffectValuesUser from .elements import Bus, Component, Flow -from .structure import ( - CLASS_REGISTRY, - Element, - Interface, - SystemModel, - get_compact_representation, - get_str_representation, -) +from .structure import Element, Interface, SystemModel if TYPE_CHECKING: import pyvis From 18c43e49d5e5adc2286354c35823031202ce555d Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 26 Jun 2025 08:59:32 +0200 Subject: [PATCH 092/448] Add isel, sel and resample methods to FlowSystem --- flixopt/core.py | 2 +- flixopt/flow_system.py | 140 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 140 insertions(+), 2 deletions(-) diff --git a/flixopt/core.py b/flixopt/core.py index 61e951019..831b90b37 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -212,7 +212,7 @@ def to_dataarray(data: NumericDataUser, timesteps: pd.DatetimeIndex) -> xr.DataA raise ConversionError(f"DataArray dimensions {data.dims} don't match expected {dims}") if data.sizes[dims[0]] != len(coords[0]): raise ConversionError( - f"DataArray length {data.sizes[dims[0]]} doesn't match expected {len(coords[0])}" + f"DataArray length {data.sizes[dims[0]]} doesn't match expected {len(coords[0])}: {data}" ) return data.copy(deep=True) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 7724a9e61..2bdfd0bbc 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -194,7 +194,7 @@ def from_dataset(cls, ds: xr.Dataset) -> 'FlowSystem': reference_structure = dict(ds.attrs) # Extract FlowSystem constructor parameters - timesteps_extra = pd.DatetimeIndex(reference_structure['timesteps_extra'], name='time') + timesteps_extra = ds.indexes['time'] hours_of_previous_timesteps = reference_structure['hours_of_previous_timesteps'] # Calculate hours_of_last_timestep from the timesteps @@ -547,3 +547,141 @@ def all_elements(self) -> Dict[str, Element]: @property def used_in_calculation(self) -> bool: return self._used_in_calculation + + def sel(self, **indexers) -> 'FlowSystem': + """Select a subset of the flowsystem like dataset.sel(time=slice('2023-01', '2023-06'))""" + if not self._connected_and_transformed: + self.connect_and_transform() + + # Convert to dataset, select, then convert back + dataset = self.to_dataset() + + # Extend time selection and handle NaN preservation + if 'time' in indexers: + indexers = self._extend_time_selection(indexers, dataset) + selected_dataset = dataset.sel(**indexers) + selected_dataset = self._preserve_nan_pattern(selected_dataset, dataset) + else: + selected_dataset = dataset.sel(**indexers) + + return self.__class__.from_dataset(selected_dataset) + + def isel(self, **indexers) -> 'FlowSystem': + """Select by integer index like dataset.isel(time=slice(0, 100))""" + if not self._connected_and_transformed: + self.connect_and_transform() + + # Convert to dataset, select, then convert back + dataset = self.to_dataset() + + # Extend time selection and handle NaN preservation + if 'time' in indexers: + indexers = self._extend_time_iselection(indexers, dataset) + selected_dataset = dataset.isel(**indexers) + selected_dataset = self._preserve_nan_pattern(selected_dataset, dataset) + else: + selected_dataset = dataset.isel(**indexers) + + return self.__class__.from_dataset(selected_dataset) + + def _preserve_nan_pattern(self, processed_dataset: xr.Dataset, original_dataset: xr.Dataset) -> xr.Dataset: + """ + Preserve NaN pattern at the last timestep for arrays that originally had NaN at the end. + Works for both selection and resampling operations. + """ + for var_name, processed_array in processed_dataset.data_vars.items(): + if var_name in original_dataset.data_vars: + original_array = original_dataset.data_vars[var_name] + + # Check if original array had NaN at the last timestep + if len(original_array.time) > 0 and len(processed_array.time) > 0: + last_original = original_array.isel(time=-1) + + if last_original.isnull().all(): # All values at last timestep are NaN + # Set all values at last timestep to NaN + processed_array = processed_array.copy() + processed_array.values[..., -1] = np.nan + processed_dataset[var_name] = processed_array + elif last_original.isnull().any(): # Some values at last timestep are NaN + # Preserve the specific NaN pattern (if dimensions allow) + processed_array = processed_array.copy() + try: + nan_mask = last_original.isnull().values + processed_array.values[..., -1][nan_mask] = np.nan + except (IndexError, ValueError): + # Fallback: set entire last timestep to NaN if dimensions don't match + processed_array.values[..., -1] = np.nan + processed_dataset[var_name] = processed_array + + return processed_dataset + + def _extend_time_selection(self, indexers: dict, dataset: xr.Dataset) -> dict: + """Extend time selection to include the next timestep for proper boundaries.""" + new_indexers = indexers.copy() + time_sel = indexers['time'] + + if isinstance(time_sel, slice): + # For slice, extend the stop point + if time_sel.stop is not None: + time_coord = dataset.coords['time'] + try: + # Find the index of the stop time and add 1 + stop_idx = time_coord.get_index('time').get_indexer([time_sel.stop], method='nearest')[0] + if stop_idx < len(time_coord) - 1: # Don't go beyond bounds + next_time = time_coord.isel(time=stop_idx + 1).values + new_indexers['time'] = slice(time_sel.start, next_time, time_sel.step) + except Exception: + pass # Keep original if extension fails + + return new_indexers + + def _extend_time_iselection(self, indexers: dict, dataset: xr.Dataset) -> dict: + """Extend integer time selection to include the next timestep.""" + new_indexers = indexers.copy() + time_sel = indexers['time'] + + if isinstance(time_sel, slice): + # For slice, extend the stop point by 1 + stop = time_sel.stop + if stop is not None and stop < len(dataset.coords['time']) - 1: + new_indexers['time'] = slice(time_sel.start, stop + 1, time_sel.step) + elif isinstance(time_sel, int): + # For single index, convert to slice including next + if time_sel < len(dataset.coords['time']) - 1: + new_indexers['time'] = slice(time_sel, time_sel + 2) + elif isinstance(time_sel, (list, np.ndarray)): + # For list/array of indices, add next indices + extended_indices = list(time_sel) + max_idx = len(dataset.coords['time']) - 1 + for idx in time_sel: + if isinstance(idx, int) and idx < max_idx and (idx + 1) not in extended_indices: + extended_indices.append(idx + 1) + new_indexers['time'] = sorted(extended_indices) + + return new_indexers + + def resample(self, time, method: str = 'mean', **kwargs) -> 'FlowSystem': + """ + Resample time dimension like dataset.resample(). + + Args: + time: Resampling frequency (e.g., '1H', '1D') + method: Resampling method ('mean', 'sum', 'max', 'min', 'first', 'last') + **kwargs: Additional arguments passed to xarray.resample() + """ + if not self._connected_and_transformed: + self.connect_and_transform() + + dataset = self.to_dataset() + resampler = dataset.resample(time=time, **kwargs) + + # Apply the specified method + if hasattr(resampler, method): + resampled_dataset = getattr(resampler, method)() + else: + raise ValueError(f'Unsupported resampling method: {method}') + + # Preserve NaN pattern at the last timestep + resampled_dataset = self._preserve_nan_pattern(resampled_dataset, dataset) + + return self.__class__.from_dataset(resampled_dataset) From 1f9ef072abb56a73efe04d57f27dd44287238412 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 26 Jun 2025 09:28:06 +0200 Subject: [PATCH 093/448] Remove need for timeseries with extra timestep --- flixopt/components.py | 25 ++++++++++++++++++++----- flixopt/effects.py | 2 +- flixopt/flow_system.py | 9 ++------- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/flixopt/components.py b/flixopt/components.py index 3f41783a8..ae8cdfbf0 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -134,6 +134,8 @@ def __init__( initial_charge_state: Union[Scalar, Literal['lastValueOfSim']] = 0, minimal_final_charge_state: Optional[Scalar] = None, maximal_final_charge_state: Optional[Scalar] = None, + relative_minimum_final_charge_state: Optional[Scalar] = None, + relative_maximum_final_charge_state: Optional[Scalar] = None, eta_charge: NumericDataUser = 1, eta_discharge: NumericDataUser = 1, relative_loss_per_hour: NumericDataUser = 0, @@ -158,6 +160,8 @@ def __init__( initial_charge_state: storage charge_state at the beginning. The default is 0. minimal_final_charge_state: minimal value of chargeState at the end of timeseries. maximal_final_charge_state: maximal value of chargeState at the end of timeseries. + minimal_final_charge_state: relative minimal value of chargeState at the end of timeseries. + maximal_final_charge_state: relative maximal value of chargeState at the end of timeseries. eta_charge: efficiency factor of charging/loading. The default is 1. eta_discharge: efficiency factor of uncharging/unloading. The default is 1. relative_loss_per_hour: loss per chargeState-Unit per hour. The default is 0. @@ -180,6 +184,9 @@ def __init__( self.relative_minimum_charge_state: NumericDataUser = relative_minimum_charge_state self.relative_maximum_charge_state: NumericDataUser = relative_maximum_charge_state + self.relative_minimum_final_charge_state: NumericDataUser = relative_minimum_final_charge_state + self.relative_maximum_final_charge_state: NumericDataUser = relative_maximum_final_charge_state + self.initial_charge_state = initial_charge_state self.minimal_final_charge_state = minimal_final_charge_state self.maximal_final_charge_state = maximal_final_charge_state @@ -199,12 +206,10 @@ def transform_data(self, flow_system: 'FlowSystem') -> None: self.relative_minimum_charge_state = flow_system.fit_to_model_coords( f'{self.label_full}|relative_minimum_charge_state', self.relative_minimum_charge_state, - needs_extra_timestep=True, ) self.relative_maximum_charge_state = flow_system.fit_to_model_coords( f'{self.label_full}|relative_maximum_charge_state', self.relative_maximum_charge_state, - needs_extra_timestep=True, ) self.eta_charge = flow_system.fit_to_model_coords(f'{self.label_full}|eta_charge', self.eta_charge) self.eta_discharge = flow_system.fit_to_model_coords(f'{self.label_full}|eta_discharge', self.eta_discharge) @@ -571,10 +576,20 @@ def absolute_charge_state_bounds(self) -> Tuple[NumericDataUser, NumericDataUser ) @property - def relative_charge_state_bounds(self) -> Tuple[NumericDataUser, NumericDataUser]: + def relative_charge_state_bounds(self) -> Tuple[xr.DataArray, xr.DataArray]: + relative_minimum_final_charge_state = ( + xr.DataArray([np.min(self.element.relative_minimum_charge_state)], coords={'time': [self._model.flow_system.timesteps_extra[-1]]}, dims=['time'] + ) if self.element.relative_minimum_final_charge_state is None else + self.element.relative_minimum_final_charge_state + ) + relative_maximum_final_charge_state = ( + xr.DataArray([np.max(self.element.relative_maximum_charge_state)], coords={'time': [self._model.flow_system.timesteps_extra[-1]]}, dims=['time'] + ) if self.element.relative_maximum_final_charge_state is None else + self.element.relative_maximum_final_charge_state + ) return ( - self.element.relative_minimum_charge_state, - self.element.relative_maximum_charge_state, + xr.concat([self.element.relative_minimum_charge_state, relative_minimum_final_charge_state], dim='time'), + xr.concat([self.element.relative_maximum_charge_state, relative_maximum_final_charge_state], dim='time'), ) diff --git a/flixopt/effects.py b/flixopt/effects.py index 7fa136f5b..89bc009bf 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -94,7 +94,7 @@ def transform_data(self, flow_system: 'FlowSystem'): f'{self.label_full}|minimum_operation_per_hour', self.minimum_operation_per_hour ) self.maximum_operation_per_hour = flow_system.fit_to_model_coords( - f'{self.label_full}|maximum_operation_per_hour', self.maximum_operation_per_hour, flow_system + f'{self.label_full}|maximum_operation_per_hour', self.maximum_operation_per_hour ) self.specific_share_to_other_effects_operation = flow_system.fit_effects_to_model_coords( diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 2bdfd0bbc..8b412cd07 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -285,7 +285,6 @@ def fit_to_model_coords( self, name: str, data: Optional[NumericDataUser], - needs_extra_timestep: bool = False, ) -> Optional[NumericDataInternal]: """ Fit data to model coordinate system (currently time, but extensible). @@ -293,7 +292,6 @@ def fit_to_model_coords( Args: name: Name of the data data: Data to fit to model coordinates - needs_extra_timestep: Whether to use extended time coordinates Returns: xr.DataArray aligned to model coordinate system @@ -301,20 +299,17 @@ def fit_to_model_coords( if data is None: return None - # Choose appropriate timesteps - target_timesteps = self.timesteps_extra if needs_extra_timestep else self.timesteps - if isinstance(data, TimeSeriesData): try: return TimeSeriesData( - DataConverter.to_dataarray(data, timesteps=target_timesteps), + DataConverter.to_dataarray(data, timesteps=self.timesteps), agg_group=data.agg_group, agg_weight=data.agg_weight ).rename(name) except ConversionError as e: logger.critical(f'Could not convert time series data "{name}" to DataArray: {e}. \n' f'Take care to use the correct (time) index.') else: - return DataConverter.to_dataarray(data, timesteps=target_timesteps).rename(name) + return DataConverter.to_dataarray(data, timesteps=self.timesteps).rename(name) def fit_effects_to_model_coords( self, From 5d88fde2a2a6d9c8b6e007e7f4be8a5a64f3d868 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 26 Jun 2025 10:09:48 +0200 Subject: [PATCH 094/448] Simplify IO of FLowSystem --- flixopt/flow_system.py | 120 +++-------------------------------------- 1 file changed, 6 insertions(+), 114 deletions(-) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 8b412cd07..aa2e261eb 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -129,15 +129,9 @@ def _create_reference_structure(self) -> Tuple[Dict, Dict[str, xr.DataArray]]: # Start with Interface base functionality for constructor parameters reference_structure, all_extracted_arrays = super()._create_reference_structure() - # Override timesteps serialization (we need timesteps_extra instead of timesteps) - reference_structure['timesteps_extra'] = [date.isoformat() for date in self.timesteps_extra] - - # Remove timesteps from structure since we're using timesteps_extra + # Remove timesteps, as it's directly stored in dataset index reference_structure.pop('timesteps', None) - # Add timing arrays directly (not handled by Interface introspection) - all_extracted_arrays['hours_per_timestep'] = self.hours_per_timestep - # Extract from components components_structure = {} for comp_label, component in self.components.items(): @@ -193,18 +187,11 @@ def from_dataset(cls, ds: xr.Dataset) -> 'FlowSystem': # Get the reference structure from attrs reference_structure = dict(ds.attrs) - # Extract FlowSystem constructor parameters - timesteps_extra = ds.indexes['time'] - hours_of_previous_timesteps = reference_structure['hours_of_previous_timesteps'] - - # Calculate hours_of_last_timestep from the timesteps - hours_of_last_timestep = float((timesteps_extra[-1] - timesteps_extra[-2]) / pd.Timedelta(hours=1)) - # Create FlowSystem instance with constructor parameters flow_system = cls( - timesteps=timesteps_extra[:-1], - hours_of_last_timestep=hours_of_last_timestep, - hours_of_previous_timesteps=hours_of_previous_timesteps, + timesteps=ds.indexes['time'], + hours_of_last_timestep=reference_structure.get('hours_of_last_timestep'), + hours_of_previous_timesteps=reference_structure.get('hours_of_previous_timesteps'), ) # Create arrays dictionary from dataset variables @@ -549,15 +536,7 @@ def sel(self, **indexers) -> 'FlowSystem': self.connect_and_transform() # Convert to dataset, select, then convert back - dataset = self.to_dataset() - - # Extend time selection and handle NaN preservation - if 'time' in indexers: - indexers = self._extend_time_selection(indexers, dataset) - selected_dataset = dataset.sel(**indexers) - selected_dataset = self._preserve_nan_pattern(selected_dataset, dataset) - else: - selected_dataset = dataset.sel(**indexers) + selected_dataset = self.to_dataset().sel(**indexers) return self.__class__.from_dataset(selected_dataset) @@ -567,94 +546,10 @@ def isel(self, **indexers) -> 'FlowSystem': self.connect_and_transform() # Convert to dataset, select, then convert back - dataset = self.to_dataset() - - # Extend time selection and handle NaN preservation - if 'time' in indexers: - indexers = self._extend_time_iselection(indexers, dataset) - selected_dataset = dataset.isel(**indexers) - selected_dataset = self._preserve_nan_pattern(selected_dataset, dataset) - else: - selected_dataset = dataset.isel(**indexers) + selected_dataset = self.to_dataset().isel(**indexers) return self.__class__.from_dataset(selected_dataset) - def _preserve_nan_pattern(self, processed_dataset: xr.Dataset, original_dataset: xr.Dataset) -> xr.Dataset: - """ - Preserve NaN pattern at the last timestep for arrays that originally had NaN at the end. - Works for both selection and resampling operations. - """ - for var_name, processed_array in processed_dataset.data_vars.items(): - if var_name in original_dataset.data_vars: - original_array = original_dataset.data_vars[var_name] - - # Check if original array had NaN at the last timestep - if len(original_array.time) > 0 and len(processed_array.time) > 0: - last_original = original_array.isel(time=-1) - - if last_original.isnull().all(): # All values at last timestep are NaN - # Set all values at last timestep to NaN - processed_array = processed_array.copy() - processed_array.values[..., -1] = np.nan - processed_dataset[var_name] = processed_array - elif last_original.isnull().any(): # Some values at last timestep are NaN - # Preserve the specific NaN pattern (if dimensions allow) - processed_array = processed_array.copy() - try: - nan_mask = last_original.isnull().values - processed_array.values[..., -1][nan_mask] = np.nan - except (IndexError, ValueError): - # Fallback: set entire last timestep to NaN if dimensions don't match - processed_array.values[..., -1] = np.nan - processed_dataset[var_name] = processed_array - - return processed_dataset - - def _extend_time_selection(self, indexers: dict, dataset: xr.Dataset) -> dict: - """Extend time selection to include the next timestep for proper boundaries.""" - new_indexers = indexers.copy() - time_sel = indexers['time'] - - if isinstance(time_sel, slice): - # For slice, extend the stop point - if time_sel.stop is not None: - time_coord = dataset.coords['time'] - try: - # Find the index of the stop time and add 1 - stop_idx = time_coord.get_index('time').get_indexer([time_sel.stop], method='nearest')[0] - if stop_idx < len(time_coord) - 1: # Don't go beyond bounds - next_time = time_coord.isel(time=stop_idx + 1).values - new_indexers['time'] = slice(time_sel.start, next_time, time_sel.step) - except Exception: - pass # Keep original if extension fails - - return new_indexers - - def _extend_time_iselection(self, indexers: dict, dataset: xr.Dataset) -> dict: - """Extend integer time selection to include the next timestep.""" - new_indexers = indexers.copy() - time_sel = indexers['time'] - - if isinstance(time_sel, slice): - # For slice, extend the stop point by 1 - stop = time_sel.stop - if stop is not None and stop < len(dataset.coords['time']) - 1: - new_indexers['time'] = slice(time_sel.start, stop + 1, time_sel.step) - elif isinstance(time_sel, int): - # For single index, convert to slice including next - if time_sel < len(dataset.coords['time']) - 1: - new_indexers['time'] = slice(time_sel, time_sel + 2) - elif isinstance(time_sel, (list, np.ndarray)): - # For list/array of indices, add next indices - extended_indices = list(time_sel) - max_idx = len(dataset.coords['time']) - 1 - for idx in time_sel: - if isinstance(idx, int) and idx < max_idx and (idx + 1) not in extended_indices: - extended_indices.append(idx + 1) - new_indexers['time'] = sorted(extended_indices) - - return new_indexers - def resample(self, time, method: str = 'mean', **kwargs) -> 'FlowSystem': """ Resample time dimension like dataset.resample(). @@ -676,7 +571,4 @@ def resample(self, time, method: str = 'mean', **kwargs) -> 'FlowSystem': else: raise ValueError(f'Unsupported resampling method: {method}') - # Preserve NaN pattern at the last timestep - resampled_dataset = self._preserve_nan_pattern(resampled_dataset, dataset) - return self.__class__.from_dataset(resampled_dataset) From 1e94de392b57e57b8b3600c4598fdad9a46014a7 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 26 Jun 2025 10:12:53 +0200 Subject: [PATCH 095/448] Remove parameter timesteps from IO --- flixopt/structure.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flixopt/structure.py b/flixopt/structure.py index b4fcf7d38..5a95b0a94 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -156,7 +156,7 @@ def _create_reference_structure(self) -> Tuple[Dict, Dict[str, xr.DataArray]]: all_extracted_arrays = {} for name in self._cached_init_params: - if name == 'self': + if name == 'self' or name == 'timesteps': # Skip self and timesteps. Timesteps are directly stored in Datasets continue value = getattr(self, name, None) From e5828ad78f813aff3d116e3696fb832d19b7bbe4 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 26 Jun 2025 10:26:54 +0200 Subject: [PATCH 096/448] Improve Exceptions and Docstrings --- flixopt/flow_system.py | 2 +- flixopt/structure.py | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index aa2e261eb..1d3bc4aa8 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -167,7 +167,7 @@ def to_dataset(self) -> xr.Dataset: xr.Dataset: Dataset containing all DataArrays with structure in attributes """ if not self._connected_and_transformed: - logger.warning('FlowSystem is not connected_and_transformed..') + logger.warning('FlowSystem is not connected_and_transformed. Connecting and transforming data now.') self.connect_and_transform() return super().to_dataset() diff --git a/flixopt/structure.py b/flixopt/structure.py index 5a95b0a94..3fb0be066 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -380,6 +380,9 @@ def to_dataset(self) -> xr.Dataset: Convert the object to an xarray Dataset representation. All DataArrays become dataset variables, everything else goes to attrs. + Its recommended to only call this method on Interfaces with all numeric data stored as xr.DataArrays. + Interfaces inside a FlowSystem are automatically converted this form after connecting and transforming the FlowSystem. + Returns: xr.Dataset: Dataset containing all DataArrays with basic objects only in attributes @@ -391,7 +394,10 @@ def to_dataset(self) -> xr.Dataset: # Create the dataset with extracted arrays as variables and structure as attrs return xr.Dataset(extracted_arrays, attrs=reference_structure) except Exception as e: - raise ValueError(f'Failed to convert {self.__class__.__name__} to dataset: {e}') from e + raise ValueError( + f'Failed to convert {self.__class__.__name__} to dataset. Its recommended to only call this method on ' + f'a fully connected and transformed FlowSystem, or Interfaces inside such a FlowSystem.' + f'Original Error: {e}') from e def to_netcdf(self, path: Union[str, pathlib.Path], compression: int = 0): """ From 870efeee484f39e63cf4b7adca6bc30000b613ac Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 26 Jun 2025 10:35:56 +0200 Subject: [PATCH 097/448] Improve isel sel and resample methods --- flixopt/flow_system.py | 66 ++++++++++++++++++++++++++++++++---------- 1 file changed, 51 insertions(+), 15 deletions(-) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 1d3bc4aa8..b146ef06a 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -7,7 +7,7 @@ import pathlib import warnings from io import StringIO -from typing import TYPE_CHECKING, Dict, List, Literal, Optional, Tuple, Union +from typing import TYPE_CHECKING, Dict, List, Literal, Optional, Tuple, Union, Any import numpy as np import pandas as pd @@ -530,34 +530,70 @@ def all_elements(self) -> Dict[str, Element]: def used_in_calculation(self) -> bool: return self._used_in_calculation - def sel(self, **indexers) -> 'FlowSystem': - """Select a subset of the flowsystem like dataset.sel(time=slice('2023-01', '2023-06'))""" + def sel(self, time: Optional[Union[str, slice, List[str], pd.Timestamp]] = None) -> 'FlowSystem': + """ + Select a subset of the flowsystem by the time coordinate. + + Args: + time: Time selection (e.g., slice('2023-01-01', '2023-12-31'), '2023-06-15', or list of times) + + Returns: + FlowSystem: New FlowSystem with selected data + """ if not self._connected_and_transformed: self.connect_and_transform() - # Convert to dataset, select, then convert back - selected_dataset = self.to_dataset().sel(**indexers) + # Build indexers dict from non-None parameters + indexers = {} + if time is not None: + indexers['time'] = time + + if not indexers: + return self.copy() # Return a copy when no selection + selected_dataset = self.to_dataset().sel(**indexers) return self.__class__.from_dataset(selected_dataset) - def isel(self, **indexers) -> 'FlowSystem': - """Select by integer index like dataset.isel(time=slice(0, 100))""" + def isel(self, time: Optional[Union[int, slice, List[int]]] = None) -> 'FlowSystem': + """ + Select a subset of the flowsystem by integer indices. + + Args: + time: Time selection by integer index (e.g., slice(0, 100), 50, or [0, 5, 10]) + + Returns: + FlowSystem: New FlowSystem with selected data + """ if not self._connected_and_transformed: self.connect_and_transform() - # Convert to dataset, select, then convert back - selected_dataset = self.to_dataset().isel(**indexers) + # Build indexers dict from non-None parameters + indexers = {} + if time is not None: + indexers['time'] = time + if not indexers: + return self.copy() # Return a copy when no selection + + selected_dataset = self.to_dataset().isel(**indexers) return self.__class__.from_dataset(selected_dataset) - def resample(self, time, method: str = 'mean', **kwargs) -> 'FlowSystem': + def resample( + self, + time: str, + method: Literal['mean', 'sum', 'max', 'min', 'first', 'last', 'std', 'var', 'median', 'count'] = 'mean', + **kwargs: Any + ) -> 'FlowSystem': """ - Resample time dimension like dataset.resample(). + Create a resampled FlowSystem by resampling data along the time dimension (like xr.Dataset.resample()). Args: - time: Resampling frequency (e.g., '1H', '1D') - method: Resampling method ('mean', 'sum', 'max', 'min', 'first', 'last') + time: Resampling frequency (e.g., '3h', '2D', '1M') + method: Resampling method. Recommended: 'mean', 'first', 'last', 'max', 'min' **kwargs: Additional arguments passed to xarray.resample() + + Returns: + FlowSystem: New FlowSystem with resampled data """ if not self._connected_and_transformed: self.connect_and_transform() @@ -565,10 +601,10 @@ def resample(self, time, method: str = 'mean', **kwargs) -> 'FlowSystem': dataset = self.to_dataset() resampler = dataset.resample(time=time, **kwargs) - # Apply the specified method if hasattr(resampler, method): resampled_dataset = getattr(resampler, method)() else: - raise ValueError(f'Unsupported resampling method: {method}') + available_methods = ['mean', 'sum', 'max', 'min', 'first', 'last', 'std', 'var', 'median', 'count'] + raise ValueError(f'Unsupported resampling method: {method}. Available: {available_methods}') return self.__class__.from_dataset(resampled_dataset) From e97ec5fcd7ce5085bd1418b4077fc8f35240fbbf Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 26 Jun 2025 10:48:35 +0200 Subject: [PATCH 098/448] Change test --- tests/conftest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 43f9f8bae..b705939cc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -95,7 +95,8 @@ def simple_flow_system() -> fx.FlowSystem: discharging=fx.Flow('Q_th_unload', bus='Fernwärme', size=1e4), capacity_in_flow_hours=fx.InvestParameters(fix_effects=20, fixed_size=30, optional=False), initial_charge_state=0, - relative_maximum_charge_state=1 / 100 * np.array([80.0, 70.0, 80.0, 80, 80, 80, 80, 80, 80, 80]), + relative_maximum_charge_state=1 / 100 * np.array([80.0, 70.0, 80.0, 80, 80, 80, 80, 80, 80]), + relative_maximum_final_charge_state=0.8, eta_charge=0.9, eta_discharge=1, relative_loss_per_hour=0.08, From f15113efaf16e448a87054741edaf6016eea60dc Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 26 Jun 2025 11:02:58 +0200 Subject: [PATCH 099/448] Bugfix --- flixopt/components.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/flixopt/components.py b/flixopt/components.py index ae8cdfbf0..be86457e6 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -577,16 +577,18 @@ def absolute_charge_state_bounds(self) -> Tuple[NumericDataUser, NumericDataUser @property def relative_charge_state_bounds(self) -> Tuple[xr.DataArray, xr.DataArray]: - relative_minimum_final_charge_state = ( - xr.DataArray([np.min(self.element.relative_minimum_charge_state)], coords={'time': [self._model.flow_system.timesteps_extra[-1]]}, dims=['time'] - ) if self.element.relative_minimum_final_charge_state is None else - self.element.relative_minimum_final_charge_state + relative_minimum_final_charge_state = xr.DataArray( + [self.element.relative_minimum_charge_state.max('time') if self.element.relative_minimum_final_charge_state is None else self.element.relative_minimum_final_charge_state], + coords={'time': [self._model.flow_system.timesteps_extra[-1]]}, + dims=['time'] ) - relative_maximum_final_charge_state = ( - xr.DataArray([np.max(self.element.relative_maximum_charge_state)], coords={'time': [self._model.flow_system.timesteps_extra[-1]]}, dims=['time'] - ) if self.element.relative_maximum_final_charge_state is None else - self.element.relative_maximum_final_charge_state + relative_maximum_final_charge_state = xr.DataArray( + [self.element.relative_maximum_charge_state.max('time') if self.element.relative_maximum_final_charge_state is None else + self.element.relative_maximum_final_charge_state], + coords={'time': [self._model.flow_system.timesteps_extra[-1]]}, + dims=['time'] ) + return ( xr.concat([self.element.relative_minimum_charge_state, relative_minimum_final_charge_state], dim='time'), xr.concat([self.element.relative_maximum_charge_state, relative_maximum_final_charge_state], dim='time'), From 284072e5680f1a2b09c03a53c71fa40e1164aa22 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 26 Jun 2025 11:24:32 +0200 Subject: [PATCH 100/448] Improve --- flixopt/components.py | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/flixopt/components.py b/flixopt/components.py index be86457e6..fe509c59d 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -237,9 +237,9 @@ def _plausibility_checks(self) -> None: minimum_capacity = self.capacity_in_flow_hours # initial capacity >= allowed min for maximum_size: - minimum_inital_capacity = maximum_capacity * self.relative_minimum_charge_state.isel(time=1) + minimum_inital_capacity = maximum_capacity * self.relative_minimum_charge_state.isel(time=0) # initial capacity <= allowed max for minimum_size: - maximum_inital_capacity = minimum_capacity * self.relative_maximum_charge_state.isel(time=1) + maximum_inital_capacity = minimum_capacity * self.relative_maximum_charge_state.isel(time=0) if self.initial_charge_state > maximum_inital_capacity: raise ValueError( @@ -577,17 +577,28 @@ def absolute_charge_state_bounds(self) -> Tuple[NumericDataUser, NumericDataUser @property def relative_charge_state_bounds(self) -> Tuple[xr.DataArray, xr.DataArray]: - relative_minimum_final_charge_state = xr.DataArray( - [self.element.relative_minimum_charge_state.max('time') if self.element.relative_minimum_final_charge_state is None else self.element.relative_minimum_final_charge_state], - coords={'time': [self._model.flow_system.timesteps_extra[-1]]}, - dims=['time'] - ) - relative_maximum_final_charge_state = xr.DataArray( - [self.element.relative_maximum_charge_state.max('time') if self.element.relative_maximum_final_charge_state is None else - self.element.relative_maximum_final_charge_state], - coords={'time': [self._model.flow_system.timesteps_extra[-1]]}, - dims=['time'] - ) + coords = {'time': self._model.flow_system.timesteps_extra[-1]} + if self.element.relative_minimum_final_charge_state is None: + relative_minimum_final_charge_state = self.element.relative_minimum_charge_state.isel( + time=-1 + ).assign_coords(time=self._model.flow_system.timesteps_extra[-1]) + else: + relative_minimum_final_charge_state = xr.DataArray( + [self.element.relative_minimum_final_charge_state], + coords=coords, + dims=['time'] + ) + + if self.element.relative_maximum_final_charge_state is None: + relative_maximum_final_charge_state = self.element.relative_maximum_charge_state.isel( + time=-1 + ).assign_coords(coords) + else: + relative_maximum_final_charge_state = xr.DataArray( + [self.element.relative_maximum_final_charge_state], + coords=coords, + dims=['time'] + ) return ( xr.concat([self.element.relative_minimum_charge_state, relative_minimum_final_charge_state], dim='time'), From ebbb5dd61140e5198299c7af1683ef1dddf345f2 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 26 Jun 2025 11:25:47 +0200 Subject: [PATCH 101/448] Improve --- flixopt/components.py | 41 ++++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/flixopt/components.py b/flixopt/components.py index fe509c59d..5e59b8bc5 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -577,33 +577,36 @@ def absolute_charge_state_bounds(self) -> Tuple[NumericDataUser, NumericDataUser @property def relative_charge_state_bounds(self) -> Tuple[xr.DataArray, xr.DataArray]: - coords = {'time': self._model.flow_system.timesteps_extra[-1]} + """ + Get relative charge state bounds with final timestep values. + + Returns: + Tuple of (minimum_bounds, maximum_bounds) DataArrays extending to final timestep + """ + final_timestep = self._model.flow_system.timesteps_extra[-1] + final_coords = {'time': final_timestep} + + # Get final minimum charge state if self.element.relative_minimum_final_charge_state is None: - relative_minimum_final_charge_state = self.element.relative_minimum_charge_state.isel( - time=-1 - ).assign_coords(time=self._model.flow_system.timesteps_extra[-1]) + min_final = self.element.relative_minimum_charge_state.isel(time=-1).assign_coords(time=final_timestep) else: - relative_minimum_final_charge_state = xr.DataArray( - [self.element.relative_minimum_final_charge_state], - coords=coords, - dims=['time'] + min_final = xr.DataArray( + [self.element.relative_minimum_final_charge_state], coords=final_coords, dims=['time'] ) + # Get final maximum charge state if self.element.relative_maximum_final_charge_state is None: - relative_maximum_final_charge_state = self.element.relative_maximum_charge_state.isel( - time=-1 - ).assign_coords(coords) + max_final = self.element.relative_maximum_charge_state.isel(time=-1).assign_coords(time=final_timestep) else: - relative_maximum_final_charge_state = xr.DataArray( - [self.element.relative_maximum_final_charge_state], - coords=coords, - dims=['time'] + max_final = xr.DataArray( + [self.element.relative_maximum_final_charge_state], coords=final_coords, dims=['time'] ) - return ( - xr.concat([self.element.relative_minimum_charge_state, relative_minimum_final_charge_state], dim='time'), - xr.concat([self.element.relative_maximum_charge_state, relative_maximum_final_charge_state], dim='time'), - ) + # Concatenate with original bounds + min_bounds = xr.concat([self.element.relative_minimum_charge_state, min_final], dim='time') + max_bounds = xr.concat([self.element.relative_maximum_charge_state, max_final], dim='time') + + return min_bounds, max_bounds @register_class_for_io From a501e05b7b06f8d5a916ade24880109ed5b960e6 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 26 Jun 2025 11:25:58 +0200 Subject: [PATCH 102/448] Add test for Storage Bounds --- tests/test_storage.py | 81 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/tests/test_storage.py b/tests/test_storage.py index 472ba4add..1b9b3b875 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -175,6 +175,87 @@ def test_lossy_storage(self, basic_flow_system_linopy): model.variables['TestStorage|charge_state'].isel(time=0) == 0 ) + def test_charge_state_bounds(self, basic_flow_system_linopy): + """Test that basic storage model variables and constraints are correctly generated.""" + flow_system = basic_flow_system_linopy + timesteps = flow_system.timesteps + timesteps_extra = flow_system.timesteps_extra + + # Create a simple storage + storage = fx.Storage( + 'TestStorage', + charging=fx.Flow('Q_th_in', bus='Fernwärme', size=20), + discharging=fx.Flow('Q_th_out', bus='Fernwärme', size=20), + capacity_in_flow_hours=30, # 30 kWh storage capacity + initial_charge_state=3, + prevent_simultaneous_charge_and_discharge=True, + relative_maximum_charge_state=np.array([0.14, 0.22, 0.3 , 0.38, 0.46, 0.54, 0.62, 0.7 , 0.78, 0.86]), + relative_minimum_charge_state=np.array([0.07, 0.11, 0.15, 0.19, 0.23, 0.27, 0.31, 0.35, 0.39, 0.43]), + ) + + flow_system.add_elements(storage) + model = create_linopy_model(flow_system) + + # Check that all expected variables exist - linopy model variables are accessed by indexing + expected_variables = { + 'TestStorage(Q_th_in)|flow_rate', + 'TestStorage(Q_th_in)|total_flow_hours', + 'TestStorage(Q_th_out)|flow_rate', + 'TestStorage(Q_th_out)|total_flow_hours', + 'TestStorage|charge_state', + 'TestStorage|netto_discharge', + } + for var_name in expected_variables: + assert var_name in model.variables, f"Missing variable: {var_name}" + + # Check that all expected constraints exist - linopy model constraints are accessed by indexing + expected_constraints = { + 'TestStorage(Q_th_in)|total_flow_hours', + 'TestStorage(Q_th_out)|total_flow_hours', + 'TestStorage|netto_discharge', + 'TestStorage|charge_state', + 'TestStorage|initial_charge_state', + } + for con_name in expected_constraints: + assert con_name in model.constraints, f"Missing constraint: {con_name}" + + # Check variable properties + assert_var_equal( + model['TestStorage(Q_th_in)|flow_rate'], + model.add_variables(lower=0, upper=20, coords=(timesteps,)) + ) + assert_var_equal( + model['TestStorage(Q_th_out)|flow_rate'], + model.add_variables(lower=0, upper=20, coords=(timesteps,)) + ) + assert_var_equal( + model['TestStorage|charge_state'], + model.add_variables(lower=np.array([0.07, 0.11, 0.15, 0.19, 0.23, 0.27, 0.31, 0.35, 0.39, 0.43, 0.43]) * 30, + upper=np.array([0.14, 0.22, 0.3 , 0.38, 0.46, 0.54, 0.62, 0.7 , 0.78, 0.86, 0.86]) * 30, + coords=(timesteps_extra,)) + ) + + # Check constraint formulations + assert_conequal( + model.constraints['TestStorage|netto_discharge'], + model.variables['TestStorage|netto_discharge'] == + model.variables['TestStorage(Q_th_out)|flow_rate'] - model.variables['TestStorage(Q_th_in)|flow_rate'] + ) + + charge_state = model.variables['TestStorage|charge_state'] + assert_conequal( + model.constraints['TestStorage|charge_state'], + charge_state.isel(time=slice(1, None)) + == charge_state.isel(time=slice(None, -1)) + + model.variables['TestStorage(Q_th_in)|flow_rate'] * model.hours_per_step + - model.variables['TestStorage(Q_th_out)|flow_rate'] * model.hours_per_step, + ) + # Check initial charge state constraint + assert_conequal( + model.constraints['TestStorage|initial_charge_state'], + model.variables['TestStorage|charge_state'].isel(time=0) == 3 + ) + def test_storage_with_investment(self, basic_flow_system_linopy): """Test storage with investment parameters.""" flow_system = basic_flow_system_linopy From 182508914ab9f8c4e4349e709baebd94847ba279 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 26 Jun 2025 11:34:45 +0200 Subject: [PATCH 103/448] Add test for Storage Bounds --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d692d5e5..545973095 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed +- `relative_minimum_charge_state` and `relative_maximum_charge_state` dont have an extra timestep anymore. The final charge state can be constrainted by parameters `relative_minimum_final_charge_state` and `relative_maximum_final_charge_state` instead. +- FlowSystems can not be shared across multiple Calculations anymore. A copy of the FLowSystem is created instead. THs makes every Calculation independent. +- THe above allowed to remove the intermediate classes `TimeSeries` and `TimeSeriesCollection` classes which orchestratet datahandling. + +### Added +- Added IO for all Interfaces and the FlowSystem +- Added `sel`, `isel` and `resample` methods to FlowSystem, allowing for a flexible data handling. + ## [2.1.2] - 2025-06-14 ### Fixed From 126b07f89dfafb6d53549271f441298e0fc43613 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 26 Jun 2025 11:51:49 +0200 Subject: [PATCH 104/448] CHANGELOG.md --- CHANGELOG.md | 41 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 545973095..bb95b3756 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,13 +8,44 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Changed -- `relative_minimum_charge_state` and `relative_maximum_charge_state` dont have an extra timestep anymore. The final charge state can be constrainted by parameters `relative_minimum_final_charge_state` and `relative_maximum_final_charge_state` instead. -- FlowSystems can not be shared across multiple Calculations anymore. A copy of the FLowSystem is created instead. THs makes every Calculation independent. -- THe above allowed to remove the intermediate classes `TimeSeries` and `TimeSeriesCollection` classes which orchestratet datahandling. +* **BREAKING**: FlowSystems can not be shared across multiple Calculations anymore. A copy of the FlowSystem is created instead, making every Calculation independent +* **BREAKING**: Type system overhaul - replaced `NumericDataTS` with `NumericDataUser` throughout codebase for better clarity +* **BREAKING**: `relative_minimum_charge_state` and `relative_maximum_charge_state` don't have an extra timestep anymore. The final charge state can now be constrained by parameters `relative_minimum_final_charge_state` and `relative_maximum_final_charge_state` instead +* FlowSystem data management simplified - removed `time_series_collection` pattern in favor of direct timestep properties +* Enhanced FlowSystem interface with improved `__repr__()` and `__str__()` methods +* *Internal*: Removed intermediate `TimeSeries` and `TimeSeriesCollection` classes, replaced directly with `xr.DataArray` or `TimeSeriesData` (inheriting from `xr.DataArray`) ### Added -- Added IO for all Interfaces and the FlowSystem -- Added `sel`, `isel` and `resample` methods to FlowSystem, allowing for a flexible data handling. +* **NEW**: Complete serialization infrastructure through `Interface` base class + * IO for all Interfaces and the FlowSystem with round-trip serialization support + * Automatic DataArray extraction and restoration + * NetCDF export/import capabilities for all Interface objects and FlowSystem + * JSON export for documentation purposes + * Recursive handling of nested Interface objects +* **NEW**: FlowSystem data manipulation methods + * `sel()` and `isel()` methods for temporal data selection + * `resample()` method for temporal resampling + * `copy()` method with deep copying support + * `__eq__()` method for FlowSystem comparison +* **NEW**: Storage component enhancements + * `relative_minimum_final_charge_state` parameter for final state control + * `relative_maximum_final_charge_state` parameter for final state control +* *Internal*: Enhanced data handling methods + * `fit_to_model_coords()` method for data alignment + * `fit_effects_to_model_coords()` method for effect data processing + * `connect_and_transform()` method replacing separate operations +* **NEW**: Core data handling improvements + * `get_dataarray_stats()` function for statistical summaries + * Enhanced `DataConverter` class with better TimeSeriesData support + +### Fixed +* Enhanced NetCDF I/O with proper attribute preservation for DataArrays +* Improved error handling and validation in serialization processes +* Better type consistency across all framework components + +### Know Issues +* Plotly >= 6 may raise errors if "nbformat" is not installed. We pinned plotly to <6, but this may be fixed in the future. +* IO for single Interfaces/Elemenets to Datasets might not work properly if the Interface/Element is not part of a fully transformed and connected FlowSystem. This arrises from Numeric Data not being stored as xr.DataArray by the user. TO avoid this, always use the `to_dataset()` on Elements inside a FlowSystem thats connected and transformed. ## [2.1.2] - 2025-06-14 From 94d841d9f2222b0736d30473d30403b77e5742f4 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 26 Jun 2025 12:00:50 +0200 Subject: [PATCH 105/448] ruff check --- flixopt/flow_system.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index b146ef06a..4ad935dc5 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -7,7 +7,7 @@ import pathlib import warnings from io import StringIO -from typing import TYPE_CHECKING, Dict, List, Literal, Optional, Tuple, Union, Any +from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Tuple, Union import numpy as np import pandas as pd From c19edc8e125e0815aa3c2e5450995e5938970e09 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 26 Jun 2025 14:26:41 +0200 Subject: [PATCH 106/448] Improve types --- .../example_calculation_types.py | 6 +-- flixopt/calculation.py | 42 +++++++++++++++---- flixopt/components.py | 34 +++++++-------- flixopt/core.py | 18 ++++---- flixopt/effects.py | 41 +++++++++++------- flixopt/elements.py | 26 ++++++------ flixopt/features.py | 32 +++++++------- flixopt/flow_system.py | 14 +++---- flixopt/interface.py | 20 ++++----- flixopt/linear_converters.py | 22 +++++----- flixopt/structure.py | 2 +- 11 files changed, 144 insertions(+), 113 deletions(-) diff --git a/examples/03_Calculation_types/example_calculation_types.py b/examples/03_Calculation_types/example_calculation_types.py index 97b18e3c0..b793e26ce 100644 --- a/examples/03_Calculation_types/example_calculation_types.py +++ b/examples/03_Calculation_types/example_calculation_types.py @@ -164,12 +164,12 @@ if full: calculation = fx.FullCalculation('Full', flow_system) calculation.do_modeling() - calculation.solve(fx.solvers.HighsSolver(0, 60)) + calculation.solve(fx.solvers.GurobiSolver(0.001, 60)) calculations.append(calculation) if segmented: calculation = fx.SegmentedCalculation('Segmented', flow_system, segment_length, overlap_length) - calculation.do_modeling_and_solve(fx.solvers.HighsSolver(0, 60)) + calculation.do_modeling_and_solve(fx.solvers.GurobiSolver(0.001, 60)) calculations.append(calculation) if aggregated: @@ -178,7 +178,7 @@ aggregation_parameters.time_series_for_low_peaks = [TS_electricity_demand, TS_heat_demand] calculation = fx.AggregatedCalculation('Aggregated', flow_system, aggregation_parameters) calculation.do_modeling() - calculation.solve(fx.solvers.HighsSolver(0, 60)) + calculation.solve(fx.solvers.GurobiSolver(0.001, 60)) calculations.append(calculation) # Get solutions for plotting for different calculations diff --git a/flixopt/calculation.py b/flixopt/calculation.py index 251a50075..60163b7a2 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -12,7 +12,8 @@ import math import pathlib import timeit -from typing import Any, Dict, List, Optional, Union +import warnings +from typing import Annotated, Any, Dict, List, Optional, Union import numpy as np import pandas as pd @@ -43,26 +44,39 @@ def __init__( self, name: str, flow_system: FlowSystem, - active_timesteps: Optional[pd.DatetimeIndex] = None, + active_timesteps: Annotated[ + Optional[pd.DatetimeIndex], + "DEPRECATED: Use flow_system.sel(time=...) or flow_system.isel(time=...) instead" + ] = None, folder: Optional[pathlib.Path] = None, ): """ Args: name: name of calculation flow_system: flow_system which should be calculated - active_timesteps: list with indices, which should be used for calculation. If None, then all timesteps are used. folder: folder where results should be saved. If None, then the current working directory is used. """ self.name = name if flow_system.used_in_calculation: - logging.warning(f'FlowSystem {flow_system.name} is already used in a calculation. ' + logging.warning(f'FlowSystem {flow_system} is already used in a calculation. ' f'Creating a copy for Calculation "{self.name}".') flow_system = flow_system.copy() + if active_timesteps is not None: + warnings.warn( + "The 'active_timesteps' parameter is deprecated and will be removed in a future version. " + 'Use flow_system.sel(time=timesteps) or flow_system.isel(time=indices) before passing ' + 'the FlowSystem to the Calculation instead.', + DeprecationWarning, + stacklevel=2, + ) + flow_system = flow_system.sel(time=active_timesteps) + + self.flow_system = flow_system self.flow_system._used_in_calculation = True self.model: Optional[SystemModel] = None - self.active_timesteps = active_timesteps + self._active_timesteps = active_timesteps # deprecated self.durations = {'modeling': 0.0, 'solving': 0.0, 'saving': 0.0} self.folder = pathlib.Path.cwd() / 'results' if folder is None else pathlib.Path(folder) @@ -134,6 +148,15 @@ def summary(self): 'Config': CONFIG.to_dict(), } + @property + def active_timesteps(self) -> pd.DatetimeIndex: + warnings.warn( + "The 'active_timesteps' is deprecated and will be removed in a future version.", + DeprecationWarning, + stacklevel=2, + ) + return self.flow_system.timesteps + class FullCalculation(Calculation): """ @@ -199,7 +222,10 @@ def __init__( flow_system: FlowSystem, aggregation_parameters: AggregationParameters, components_to_clusterize: Optional[List[Component]] = None, - active_timesteps: Optional[pd.DatetimeIndex] = None, + active_timesteps: Annotated[ + Optional[pd.DatetimeIndex], + 'DEPRECATED: Use flow_system.sel(time=...) or flow_system.isel(time=...) instead', + ] = None, folder: Optional[pathlib.Path] = None, ): """ @@ -213,8 +239,6 @@ def __init__( components_to_clusterize: List of Components to perform aggregation on. If None, then all components are aggregated. This means, teh variables in the components are equalized to each other, according to the typical periods computed in the DataAggregation - active_timesteps: pd.DatetimeIndex or None - list with indices, which should be used for calculation. If None, then all timesteps are used. folder: folder where results should be saved. If None, then the current working directory is used. """ super().__init__(name, flow_system, active_timesteps, folder=folder) @@ -370,7 +394,7 @@ def do_modeling_and_solve( ) calculation = FullCalculation( - f'{self.name}-{segment_name}', self.flow_system, active_timesteps=timesteps_of_segment + f'{self.name}-{segment_name}', self.flow_system.sel(timesteps_of_segment), ) self.sub_calculations.append(calculation) calculation.do_modeling() diff --git a/flixopt/components.py b/flixopt/components.py index 5e59b8bc5..49d6f5b31 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -10,7 +10,7 @@ import xarray as xr from . import utils -from .core import NumericDataUser, PlausibilityError, Scalar +from .core import PlausibilityError, Scalar, TemporalData, TemporalDataUser from .elements import Component, ComponentModel, Flow from .features import InvestmentModel, OnOffModel, PiecewiseModel from .interface import InvestParameters, OnOffParameters, PiecewiseConversion @@ -35,7 +35,7 @@ def __init__( inputs: List[Flow], outputs: List[Flow], on_off_parameters: OnOffParameters = None, - conversion_factors: List[Dict[str, NumericDataUser]] = None, + conversion_factors: List[Dict[str, TemporalDataUser]] = None, piecewise_conversion: Optional[PiecewiseConversion] = None, meta_data: Optional[Dict] = None, ): @@ -129,16 +129,16 @@ def __init__( charging: Flow, discharging: Flow, capacity_in_flow_hours: Union[Scalar, InvestParameters], - relative_minimum_charge_state: NumericDataUser = 0, - relative_maximum_charge_state: NumericDataUser = 1, + relative_minimum_charge_state: TemporalDataUser = 0, + relative_maximum_charge_state: TemporalDataUser = 1, initial_charge_state: Union[Scalar, Literal['lastValueOfSim']] = 0, minimal_final_charge_state: Optional[Scalar] = None, maximal_final_charge_state: Optional[Scalar] = None, relative_minimum_final_charge_state: Optional[Scalar] = None, relative_maximum_final_charge_state: Optional[Scalar] = None, - eta_charge: NumericDataUser = 1, - eta_discharge: NumericDataUser = 1, - relative_loss_per_hour: NumericDataUser = 0, + eta_charge: TemporalDataUser = 1, + eta_discharge: TemporalDataUser = 1, + relative_loss_per_hour: TemporalDataUser = 0, prevent_simultaneous_charge_and_discharge: bool = True, meta_data: Optional[Dict] = None, ): @@ -181,19 +181,19 @@ def __init__( self.charging = charging self.discharging = discharging self.capacity_in_flow_hours = capacity_in_flow_hours - self.relative_minimum_charge_state: NumericDataUser = relative_minimum_charge_state - self.relative_maximum_charge_state: NumericDataUser = relative_maximum_charge_state + self.relative_minimum_charge_state: TemporalDataUser = relative_minimum_charge_state + self.relative_maximum_charge_state: TemporalDataUser = relative_maximum_charge_state - self.relative_minimum_final_charge_state: NumericDataUser = relative_minimum_final_charge_state - self.relative_maximum_final_charge_state: NumericDataUser = relative_maximum_final_charge_state + self.relative_minimum_final_charge_state: Scalar = relative_minimum_final_charge_state + self.relative_maximum_final_charge_state: Scalar = relative_maximum_final_charge_state self.initial_charge_state = initial_charge_state self.minimal_final_charge_state = minimal_final_charge_state self.maximal_final_charge_state = maximal_final_charge_state - self.eta_charge: NumericDataUser = eta_charge - self.eta_discharge: NumericDataUser = eta_discharge - self.relative_loss_per_hour: NumericDataUser = relative_loss_per_hour + self.eta_charge: TemporalDataUser = eta_charge + self.eta_discharge: TemporalDataUser = eta_discharge + self.relative_loss_per_hour: TemporalDataUser = relative_loss_per_hour self.prevent_simultaneous_charge_and_discharge = prevent_simultaneous_charge_and_discharge def create_model(self, model: SystemModel) -> 'StorageModel': @@ -270,8 +270,8 @@ def __init__( out1: Flow, in2: Optional[Flow] = None, out2: Optional[Flow] = None, - relative_losses: Optional[NumericDataUser] = None, - absolute_losses: Optional[NumericDataUser] = None, + relative_losses: Optional[TemporalDataUser] = None, + absolute_losses: Optional[TemporalDataUser] = None, on_off_parameters: OnOffParameters = None, prevent_simultaneous_flows_in_both_directions: bool = True, meta_data: Optional[Dict] = None, @@ -562,7 +562,7 @@ def _initial_and_final_charge_state(self): ) @property - def absolute_charge_state_bounds(self) -> Tuple[NumericDataUser, NumericDataUser]: + def absolute_charge_state_bounds(self) -> Tuple[TemporalData, TemporalData]: relative_lower_bound, relative_upper_bound = self.relative_charge_state_bounds if not isinstance(self.element.capacity_in_flow_hours, InvestParameters): return ( diff --git a/flixopt/core.py b/flixopt/core.py index 831b90b37..41ee7b799 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -3,12 +3,8 @@ It provides Datatypes, logging functionality, and some functions to transform data structures. """ -import inspect -import json import logging -import pathlib -from collections import Counter -from typing import Any, Dict, Iterator, List, Literal, Optional, Tuple, Union +from typing import Dict, Optional, Union import numpy as np import pandas as pd @@ -19,11 +15,13 @@ Scalar = Union[int, float] """A single number, either integer or float.""" -NumericDataUser = Union[int, float, np.integer, np.floating, np.ndarray, pd.Series, pd.DataFrame, xr.DataArray, 'TimeSeriesData'] -"""Numeric data accepted in varios types. Will be converted to an xr.DataArray or Scalar internally.""" +TemporalDataUser = Union[ + int, float, np.integer, np.floating, np.ndarray, pd.Series, pd.DataFrame, xr.DataArray, 'TimeSeriesData' +] +"""User data which might have a time dimension. Internally converted to an xr.DataArray with time dimension.""" -NumericDataInternal = Union[int, float, xr.DataArray, 'TimeSeriesData'] -"""Internally used datatypes for numeric data.""" +TemporalData = Union[xr.DataArray, 'TimeSeriesData'] +"""Internally used datatypes for temporal data.""" class PlausibilityError(Exception): @@ -167,7 +165,7 @@ def _fix_timeseries_data_indexing( return data.copy(deep=True) @staticmethod - def to_dataarray(data: NumericDataUser, timesteps: pd.DatetimeIndex) -> xr.DataArray: + def to_dataarray(data: TemporalData, timesteps: pd.DatetimeIndex) -> xr.DataArray: """Convert data to xarray.DataArray with specified timesteps index.""" if not isinstance(timesteps, pd.DatetimeIndex) or len(timesteps) == 0: raise ValueError(f'Timesteps must be a non-empty DatetimeIndex, got {type(timesteps).__name__}') diff --git a/flixopt/effects.py b/flixopt/effects.py index 89bc009bf..1d1a5216c 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -13,7 +13,7 @@ import numpy as np import pandas as pd -from .core import NumericDataInternal, NumericDataUser, Scalar +from .core import Scalar, TemporalData, TemporalDataUser from .features import ShareAllocationModel from .structure import Element, ElementModel, Interface, Model, SystemModel, register_class_for_io @@ -38,14 +38,14 @@ def __init__( meta_data: Optional[Dict] = None, is_standard: bool = False, is_objective: bool = False, - specific_share_to_other_effects_operation: Optional['EffectValuesUser'] = None, - specific_share_to_other_effects_invest: Optional['EffectValuesUser'] = None, + specific_share_to_other_effects_operation: Optional['TemporalEffectsUser'] = None, + specific_share_to_other_effects_invest: Optional['ScalarEffectsUser'] = None, minimum_operation: Optional[Scalar] = None, maximum_operation: Optional[Scalar] = None, minimum_invest: Optional[Scalar] = None, maximum_invest: Optional[Scalar] = None, - minimum_operation_per_hour: Optional[NumericDataUser] = None, - maximum_operation_per_hour: Optional[NumericDataUser] = None, + minimum_operation_per_hour: Optional[TemporalDataUser] = None, + maximum_operation_per_hour: Optional[TemporalDataUser] = None, minimum_total: Optional[Scalar] = None, maximum_total: Optional[Scalar] = None, ): @@ -76,14 +76,14 @@ def __init__( self.description = description self.is_standard = is_standard self.is_objective = is_objective - self.specific_share_to_other_effects_operation: EffectValuesUser = ( + self.specific_share_to_other_effects_operation: TemporalEffectsUser = ( specific_share_to_other_effects_operation or {} ) - self.specific_share_to_other_effects_invest: EffectValuesUser = specific_share_to_other_effects_invest or {} + self.specific_share_to_other_effects_invest: ScalarEffectsUser = specific_share_to_other_effects_invest or {} self.minimum_operation = minimum_operation self.maximum_operation = maximum_operation - self.minimum_operation_per_hour: NumericDataUser = minimum_operation_per_hour - self.maximum_operation_per_hour: NumericDataUser = maximum_operation_per_hour + self.minimum_operation_per_hour: TemporalDataUser = minimum_operation_per_hour + self.maximum_operation_per_hour: TemporalDataUser = maximum_operation_per_hour self.minimum_invest = minimum_invest self.maximum_invest = maximum_invest self.minimum_total = minimum_total @@ -168,13 +168,19 @@ def do_modeling(self): ) -EffectExpr = Dict[str, linopy.LinearExpression] # Used to create Shares -EffectValuesInternal = Dict[str, NumericDataInternal] # Used internally to index values -EffectValuesUser = Union[NumericDataUser, Dict[str, NumericDataUser]] # User-specified Shares to Effects -""" This datatype is used to define the share to an effect by a certain attribute. """ +TemporalEffectsUser = Union[TemporalDataUser, Dict[str, TemporalDataUser]] # User-specified Shares to Effects +""" This datatype is used to define a temporal share to an effect by a certain attribute. """ + +ScalarEffectsUser = Union[Scalar, Dict[str, Scalar]] # User-specified Shares to Effects +""" This datatype is used to define a scalar share to an effect by a certain attribute. """ + +TemporalEffects = Dict[str, TemporalData] # User-specified Shares to Effects +""" This datatype is used internally to handle temporal shares to an effect. """ -EffectValuesUserScalar = Union[Scalar, Dict[str, Scalar]] # User-specified Shares to Effects -""" This datatype is used to define the share to an effect by a certain attribute. Only scalars are allowed. """ +ScalarEffects = Dict[str, Scalar] +""" This datatype is used internally to handle scalar shares to an effect. """ + +EffectExpr = Dict[str, linopy.LinearExpression] # Used to create Shares class EffectCollection: @@ -206,7 +212,10 @@ def add_effects(self, *effects: Effect) -> None: self._effects[effect.label] = effect logger.info(f'Registered new Effect: {effect.label}') - def create_effect_values_dict(self, effect_values_user: EffectValuesUser) -> Optional[Dict[str, NumericDataUser]]: + def create_effect_values_dict( + self, + effect_values_user: Union[ScalarEffectsUser, TemporalEffectsUser] + ) -> Optional[Dict[str, Union[Scalar, TemporalDataUser]]]: """ Converts effect values into a dictionary. If a scalar is provided, it is associated with a default effect type. diff --git a/flixopt/elements.py b/flixopt/elements.py index 061a00b65..d596333c3 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -10,8 +10,8 @@ import numpy as np from .config import CONFIG -from .core import NumericDataUser, PlausibilityError, Scalar -from .effects import EffectValuesUser +from .core import PlausibilityError, Scalar, TemporalData, TemporalDataUser +from .effects import TemporalEffectsUser from .features import InvestmentModel, OnOffModel, PreventSimultaneousUsageModel from .interface import InvestParameters, OnOffParameters from .structure import Element, ElementModel, SystemModel, register_class_for_io @@ -90,7 +90,7 @@ class Bus(Element): """ def __init__( - self, label: str, excess_penalty_per_flow_hour: Optional[NumericDataUser] = 1e5, meta_data: Optional[Dict] = None + self, label: str, excess_penalty_per_flow_hour: Optional[TemporalDataUser] = 1e5, meta_data: Optional[Dict] = None ): """ Args: @@ -149,16 +149,16 @@ def __init__( label: str, bus: str, size: Union[Scalar, InvestParameters] = None, - fixed_relative_profile: Optional[NumericDataUser] = None, - relative_minimum: NumericDataUser = 0, - relative_maximum: NumericDataUser = 1, - effects_per_flow_hour: Optional[EffectValuesUser] = None, + fixed_relative_profile: Optional[TemporalDataUser] = None, + relative_minimum: TemporalDataUser = 0, + relative_maximum: TemporalDataUser = 1, + effects_per_flow_hour: Optional[TemporalEffectsUser] = None, on_off_parameters: Optional[OnOffParameters] = None, flow_hours_total_max: Optional[Scalar] = None, flow_hours_total_min: Optional[Scalar] = None, load_factor_min: Optional[Scalar] = None, load_factor_max: Optional[Scalar] = None, - previous_flow_rate: Optional[NumericDataUser] = None, + previous_flow_rate: Optional[TemporalDataUser] = None, meta_data: Optional[Dict] = None, ): r""" @@ -411,7 +411,7 @@ def _create_bounds_for_load_factor(self): ) @property - def flow_rate_bounds_on(self) -> Tuple[NumericDataUser, NumericDataUser]: + def flow_rate_bounds_on(self) -> Tuple[TemporalData, TemporalData]: """Returns absolute flow rate bounds. Important for OnOffModel""" relative_minimum, relative_maximum = self.flow_rate_lower_bound_relative, self.flow_rate_upper_bound_relative size = self.element.size @@ -422,7 +422,7 @@ def flow_rate_bounds_on(self) -> Tuple[NumericDataUser, NumericDataUser]: return relative_minimum * size.minimum_size, relative_maximum * size.maximum_size @property - def flow_rate_lower_bound_relative(self) -> NumericDataUser: + def flow_rate_lower_bound_relative(self) -> TemporalData: """Returns the lower bound of the flow_rate relative to its size""" fixed_profile = self.element.fixed_relative_profile if fixed_profile is None: @@ -430,7 +430,7 @@ def flow_rate_lower_bound_relative(self) -> NumericDataUser: return fixed_profile @property - def flow_rate_upper_bound_relative(self) -> NumericDataUser: + def flow_rate_upper_bound_relative(self) -> TemporalData: """ Returns the upper bound of the flow_rate relative to its size""" fixed_profile = self.element.fixed_relative_profile if fixed_profile is None: @@ -438,7 +438,7 @@ def flow_rate_upper_bound_relative(self) -> NumericDataUser: return fixed_profile @property - def flow_rate_lower_bound(self) -> NumericDataUser: + def flow_rate_lower_bound(self) -> TemporalData: """ Returns the minimum bound the flow_rate can reach. Further constraining might be done in OnOffModel and InvestmentModel @@ -452,7 +452,7 @@ def flow_rate_lower_bound(self) -> NumericDataUser: return self.flow_rate_lower_bound_relative * self.element.size @property - def flow_rate_upper_bound(self) -> NumericDataUser: + def flow_rate_upper_bound(self) -> TemporalData: """ Returns the maximum bound the flow_rate can reach. Further constraining might be done in OnOffModel and InvestmentModel diff --git a/flixopt/features.py b/flixopt/features.py index 5bc8f7922..287f4e933 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -11,7 +11,7 @@ from . import utils from .config import CONFIG -from .core import NumericDataUser, Scalar +from .core import Scalar, TemporalData from .interface import InvestParameters, OnOffParameters, Piecewise from .structure import Model, SystemModel @@ -27,7 +27,7 @@ def __init__( label_of_element: str, parameters: InvestParameters, defining_variable: [linopy.Variable], - relative_bounds_of_defining_variable: Tuple[NumericDataUser, NumericDataUser], + relative_bounds_of_defining_variable: Tuple[TemporalData, TemporalData], label: Optional[str] = None, on_variable: Optional[linopy.Variable] = None, ): @@ -203,12 +203,12 @@ def __init__( model: SystemModel, label_of_element: str, defining_variables: List[linopy.Variable], - defining_bounds: List[Tuple[NumericDataUser, NumericDataUser]], - previous_values: List[Optional[NumericDataUser]] = None, + defining_bounds: List[Tuple[TemporalData, TemporalData]], + previous_values: List[Optional[TemporalData]] = None, use_off: bool = True, - on_hours_total_min: Optional[NumericDataUser] = 0, - on_hours_total_max: Optional[NumericDataUser] = None, - effects_per_running_hour: Dict[str, NumericDataUser] = None, + on_hours_total_min: Optional[TemporalData] = 0, + on_hours_total_max: Optional[TemporalData] = None, + effects_per_running_hour: Dict[str, TemporalData] = None, label: Optional[str] = None, ): """ @@ -344,7 +344,7 @@ def previous_off_states(self): return 1 - self.previous_states @staticmethod - def compute_previous_states(previous_values: List[NumericDataUser], epsilon: float = 1e-5) -> np.ndarray: + def compute_previous_states(previous_values: List[TemporalData], epsilon: float = 1e-5) -> np.ndarray: """Computes the previous states {0, 1} of defining variables as a binary array from their previous values.""" if not previous_values or all([val is None for val in previous_values]): return np.array([0]) @@ -451,9 +451,9 @@ def __init__( model: SystemModel, label_of_element: str, state_variable: linopy.Variable, - minimum_duration: Optional[NumericDataUser] = None, - maximum_duration: Optional[NumericDataUser] = None, - previous_states: Optional[NumericDataUser] = None, + minimum_duration: Optional[TemporalData] = None, + maximum_duration: Optional[TemporalData] = None, + previous_states: Optional[TemporalData] = None, label: Optional[str] = None, ): """ @@ -570,7 +570,7 @@ def previous_duration(self) -> Scalar: @staticmethod def compute_consecutive_hours_in_state( - binary_values: NumericDataUser, hours_per_timestep: Union[int, float, np.ndarray] + binary_values: TemporalData, hours_per_timestep: Union[int, float, np.ndarray] ) -> Scalar: """ Computes the final consecutive duration in state 'on' (=1) in hours, from a binary array. @@ -629,8 +629,8 @@ def __init__( on_off_parameters: OnOffParameters, label_of_element: str, defining_variables: List[linopy.Variable], - defining_bounds: List[Tuple[NumericDataUser, NumericDataUser]], - previous_values: List[Optional[NumericDataUser]], + defining_bounds: List[Tuple[TemporalData, TemporalData]], + previous_values: List[Optional[TemporalData]], label: Optional[str] = None, ): """ @@ -918,8 +918,8 @@ def __init__( label_full: Optional[str] = None, total_max: Optional[Scalar] = None, total_min: Optional[Scalar] = None, - max_per_hour: Optional[NumericDataUser] = None, - min_per_hour: Optional[NumericDataUser] = None, + max_per_hour: Optional[TemporalData] = None, + min_per_hour: Optional[TemporalData] = None, ): super().__init__(model, label_of_element=label_of_element, label=label, label_full=label_full) if not shares_are_time_series: # If the condition is True diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 4ad935dc5..9c181c8d3 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -16,8 +16,8 @@ from rich.pretty import Pretty from . import io as fx_io -from .core import ConversionError, DataConverter, NumericDataInternal, NumericDataUser, TimeSeriesData -from .effects import Effect, EffectCollection, EffectValuesInternal, EffectValuesUser +from .core import ConversionError, DataConverter, TemporalData, TemporalDataUser, TimeSeriesData +from .effects import Effect, EffectCollection, ScalarEffects, ScalarEffectsUser, TemporalEffects, TemporalEffectsUser from .elements import Bus, Component, Flow from .structure import Element, Interface, SystemModel @@ -271,8 +271,8 @@ def to_json(self, path: Union[str, pathlib.Path]): def fit_to_model_coords( self, name: str, - data: Optional[NumericDataUser], - ) -> Optional[NumericDataInternal]: + data: Optional[TemporalDataUser], + ) -> Optional[TemporalData]: """ Fit data to model coordinate system (currently time, but extensible). @@ -301,9 +301,9 @@ def fit_to_model_coords( def fit_effects_to_model_coords( self, label_prefix: Optional[str], - effect_values: Optional[EffectValuesUser], + effect_values: Optional[TemporalEffectsUser], label_suffix: Optional[str] = None, - ) -> Optional[EffectValuesInternal]: + ) -> Optional[TemporalEffects]: """ Transform EffectValues from the user to Internal Datatypes aligned with model coordinates. """ @@ -530,7 +530,7 @@ def all_elements(self) -> Dict[str, Element]: def used_in_calculation(self) -> bool: return self._used_in_calculation - def sel(self, time: Optional[Union[str, slice, List[str], pd.Timestamp]] = None) -> 'FlowSystem': + def sel(self, time: Optional[Union[str, slice, List[str], pd.Timestamp, pd.DatetimeIndex]] = None) -> 'FlowSystem': """ Select a subset of the flowsystem by the time coordinate. diff --git a/flixopt/interface.py b/flixopt/interface.py index e5ee962ed..ad331b904 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, Dict, Iterator, List, Optional, Union from .config import CONFIG -from .core import NumericDataUser, Scalar +from .core import Scalar, TemporalDataUser from .structure import Interface, register_class_for_io if TYPE_CHECKING: # for type checking and preventing circular imports @@ -20,7 +20,7 @@ @register_class_for_io class Piece(Interface): - def __init__(self, start: NumericDataUser, end: NumericDataUser): + def __init__(self, start: TemporalDataUser, end: TemporalDataUser): """ Define a Piece, which is part of a Piecewise object. @@ -175,10 +175,10 @@ def __init__( effects_per_running_hour: Optional['EffectValuesUser'] = None, on_hours_total_min: Optional[int] = None, on_hours_total_max: Optional[int] = None, - consecutive_on_hours_min: Optional[NumericDataUser] = None, - consecutive_on_hours_max: Optional[NumericDataUser] = None, - consecutive_off_hours_min: Optional[NumericDataUser] = None, - consecutive_off_hours_max: Optional[NumericDataUser] = None, + consecutive_on_hours_min: Optional[TemporalDataUser] = None, + consecutive_on_hours_max: Optional[TemporalDataUser] = None, + consecutive_off_hours_min: Optional[TemporalDataUser] = None, + consecutive_off_hours_max: Optional[TemporalDataUser] = None, switch_on_total_max: Optional[int] = None, force_switch_on: bool = False, ): @@ -206,10 +206,10 @@ def __init__( self.effects_per_running_hour: EffectValuesUser = effects_per_running_hour or {} self.on_hours_total_min: Scalar = on_hours_total_min self.on_hours_total_max: Scalar = on_hours_total_max - self.consecutive_on_hours_min: NumericDataUser = consecutive_on_hours_min - self.consecutive_on_hours_max: NumericDataUser = consecutive_on_hours_max - self.consecutive_off_hours_min: NumericDataUser = consecutive_off_hours_min - self.consecutive_off_hours_max: NumericDataUser = consecutive_off_hours_max + self.consecutive_on_hours_min: TemporalDataUser = consecutive_on_hours_min + self.consecutive_on_hours_max: TemporalDataUser = consecutive_on_hours_max + self.consecutive_off_hours_min: TemporalDataUser = consecutive_off_hours_min + self.consecutive_off_hours_max: TemporalDataUser = consecutive_off_hours_max self.switch_on_total_max: Scalar = switch_on_total_max self.force_switch_on: bool = force_switch_on diff --git a/flixopt/linear_converters.py b/flixopt/linear_converters.py index 94463c492..b137ad89a 100644 --- a/flixopt/linear_converters.py +++ b/flixopt/linear_converters.py @@ -8,7 +8,7 @@ import numpy as np from .components import LinearConverter -from .core import NumericDataUser, TimeSeriesData +from .core import TemporalDataUser, TimeSeriesData from .elements import Flow from .interface import OnOffParameters from .structure import register_class_for_io @@ -21,7 +21,7 @@ class Boiler(LinearConverter): def __init__( self, label: str, - eta: NumericDataUser, + eta: TemporalDataUser, Q_fu: Flow, Q_th: Flow, on_off_parameters: OnOffParameters = None, @@ -62,7 +62,7 @@ class Power2Heat(LinearConverter): def __init__( self, label: str, - eta: NumericDataUser, + eta: TemporalDataUser, P_el: Flow, Q_th: Flow, on_off_parameters: OnOffParameters = None, @@ -104,7 +104,7 @@ class HeatPump(LinearConverter): def __init__( self, label: str, - COP: NumericDataUser, + COP: TemporalDataUser, P_el: Flow, Q_th: Flow, on_off_parameters: OnOffParameters = None, @@ -146,7 +146,7 @@ class CoolingTower(LinearConverter): def __init__( self, label: str, - specific_electricity_demand: NumericDataUser, + specific_electricity_demand: TemporalDataUser, P_el: Flow, Q_th: Flow, on_off_parameters: OnOffParameters = None, @@ -190,8 +190,8 @@ class CHP(LinearConverter): def __init__( self, label: str, - eta_th: NumericDataUser, - eta_el: NumericDataUser, + eta_th: TemporalDataUser, + eta_el: TemporalDataUser, Q_fu: Flow, P_el: Flow, Q_th: Flow, @@ -251,7 +251,7 @@ class HeatPumpWithSource(LinearConverter): def __init__( self, label: str, - COP: NumericDataUser, + COP: TemporalDataUser, P_el: Flow, Q_ab: Flow, Q_th: Flow, @@ -297,11 +297,11 @@ def COP(self, value): # noqa: N802 def check_bounds( - value: NumericDataUser, + value: TemporalDataUser, parameter_label: str, element_label: str, - lower_bound: NumericDataUser, - upper_bound: NumericDataUser, + lower_bound: TemporalDataUser, + upper_bound: TemporalDataUser, ) -> None: """ Check if the value is within the bounds. The bounds are exclusive. diff --git a/flixopt/structure.py b/flixopt/structure.py index 3fb0be066..cc307a1e8 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -20,7 +20,7 @@ from . import io as fx_io from .config import CONFIG -from .core import NumericDataUser, Scalar, TimeSeriesData, get_dataarray_stats +from .core import Scalar, TemporalDataUser, TimeSeriesData, get_dataarray_stats if TYPE_CHECKING: # for type checking and preventing circular imports from .effects import EffectCollectionModel From 36cf47d5485c4921d5e9209e56312482aadd50ca Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 26 Jun 2025 14:27:42 +0200 Subject: [PATCH 107/448] CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb95b3756..1871de91a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed * **BREAKING**: FlowSystems can not be shared across multiple Calculations anymore. A copy of the FlowSystem is created instead, making every Calculation independent -* **BREAKING**: Type system overhaul - replaced `NumericDataTS` with `NumericDataUser` throughout codebase for better clarity +* **BREAKING**: Type system overhaul - added clear separation between temporal and non-temporal data throughout codebase for better clarity * **BREAKING**: `relative_minimum_charge_state` and `relative_maximum_charge_state` don't have an extra timestep anymore. The final charge state can now be constrained by parameters `relative_minimum_final_charge_state` and `relative_maximum_final_charge_state` instead * FlowSystem data management simplified - removed `time_series_collection` pattern in favor of direct timestep properties * Enhanced FlowSystem interface with improved `__repr__()` and `__str__()` methods From 8f1261db381706ce9270fdf571e7a92fdee39a8a Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 26 Jun 2025 14:36:09 +0200 Subject: [PATCH 108/448] Bugfix in Storage --- flixopt/components.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/flixopt/components.py b/flixopt/components.py index 49d6f5b31..639046cfc 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -584,11 +584,13 @@ def relative_charge_state_bounds(self) -> Tuple[xr.DataArray, xr.DataArray]: Tuple of (minimum_bounds, maximum_bounds) DataArrays extending to final timestep """ final_timestep = self._model.flow_system.timesteps_extra[-1] - final_coords = {'time': final_timestep} + final_coords = {'time': [final_timestep]} # Get final minimum charge state if self.element.relative_minimum_final_charge_state is None: - min_final = self.element.relative_minimum_charge_state.isel(time=-1).assign_coords(time=final_timestep) + min_final = self.element.relative_minimum_charge_state.isel( + time=-1, drop=True + ).assign_coords(time=final_timestep) else: min_final = xr.DataArray( [self.element.relative_minimum_final_charge_state], coords=final_coords, dims=['time'] @@ -596,7 +598,9 @@ def relative_charge_state_bounds(self) -> Tuple[xr.DataArray, xr.DataArray]: # Get final maximum charge state if self.element.relative_maximum_final_charge_state is None: - max_final = self.element.relative_maximum_charge_state.isel(time=-1).assign_coords(time=final_timestep) + max_final = self.element.relative_maximum_charge_state.isel( + time=-1, drop=True + ).assign_coords(time=final_timestep) else: max_final = xr.DataArray( [self.element.relative_maximum_final_charge_state], coords=final_coords, dims=['time'] From 89d69f0280e78220cbf7833dc177ddcfef04bad5 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 26 Jun 2025 14:38:29 +0200 Subject: [PATCH 109/448] Revert changes in example_calculation_types.py --- examples/03_Calculation_types/example_calculation_types.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/03_Calculation_types/example_calculation_types.py b/examples/03_Calculation_types/example_calculation_types.py index b793e26ce..ee61d6628 100644 --- a/examples/03_Calculation_types/example_calculation_types.py +++ b/examples/03_Calculation_types/example_calculation_types.py @@ -164,12 +164,12 @@ if full: calculation = fx.FullCalculation('Full', flow_system) calculation.do_modeling() - calculation.solve(fx.solvers.GurobiSolver(0.001, 60)) + calculation.solve(fx.solvers.HighsSolver(0.01/100, 60)) calculations.append(calculation) if segmented: calculation = fx.SegmentedCalculation('Segmented', flow_system, segment_length, overlap_length) - calculation.do_modeling_and_solve(fx.solvers.GurobiSolver(0.001, 60)) + calculation.do_modeling_and_solve(fx.solvers.HighsSolver(0.01/100, 60)) calculations.append(calculation) if aggregated: @@ -178,7 +178,7 @@ aggregation_parameters.time_series_for_low_peaks = [TS_electricity_demand, TS_heat_demand] calculation = fx.AggregatedCalculation('Aggregated', flow_system, aggregation_parameters) calculation.do_modeling() - calculation.solve(fx.solvers.GurobiSolver(0.001, 60)) + calculation.solve(fx.solvers.HighsSolver(0.01/100, 60)) calculations.append(calculation) # Get solutions for plotting for different calculations From 76f51a890f19c9d7de072e34bcc0a0736a468e1d Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 26 Jun 2025 14:39:22 +0200 Subject: [PATCH 110/448] Revert changes in simple_example.py --- examples/01_Simple/simple_example.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/examples/01_Simple/simple_example.py b/examples/01_Simple/simple_example.py index da10aed62..45550c9cc 100644 --- a/examples/01_Simple/simple_example.py +++ b/examples/01_Simple/simple_example.py @@ -103,14 +103,9 @@ calculation = fx.FullCalculation(name='Sim1', flow_system=flow_system) calculation.do_modeling() # Translate the model to a solvable form, creating equations and Variables - calculation2 = fx.FullCalculation(name='Sim2', flow_system=flow_system) - calculation2.do_modeling() # Translate the model to a solvable form, creating equations and Variables - # --- Solve the Calculation and Save Results --- calculation.solve(fx.solvers.HighsSolver(mip_gap=0, time_limit_seconds=30)) - calculation2.solve(fx.solvers.HighsSolver(mip_gap=0, time_limit_seconds=30)) - # --- Analyze Results --- calculation.results['Fernwärme'].plot_node_balance_pie() calculation.results['Fernwärme'].plot_node_balance() From 0ff4d29fd2ac41cdddc7caabd28fe315eb85fa82 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 26 Jun 2025 14:47:34 +0200 Subject: [PATCH 111/448] Add convenient access to Elements in FlowSystem --- flixopt/flow_system.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 9c181c8d3..49321ba82 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -517,6 +517,30 @@ def __eq__(self, other: 'FlowSystem'): return True + def __getitem__(self, item) -> Element: + """Get element by exact label with helpful error messages.""" + if item in self.all_elements: + return self.all_elements[item] + + # Provide helpful error with suggestions + from difflib import get_close_matches + + suggestions = get_close_matches(item, self.all_elements.keys(), n=3, cutoff=0.6) + + if suggestions: + suggestion_str = ', '.join(f"'{s}'" for s in suggestions) + raise KeyError(f"Element '{item}' not found. Did you mean: {suggestion_str}?") + else: + raise KeyError(f"Element '{item}' not found in FlowSystem") + + def __contains__(self, item: str) -> bool: + """Check if element exists in the FlowSystem.""" + return item in self.all_elements + + def __iter__(self): + """Iterate over element labels.""" + return iter(self.all_elements.keys()) + @property def flows(self) -> Dict[str, Flow]: set_of_flows = {flow for comp in self.components.values() for flow in comp.inputs + comp.outputs} From 84c850b5f0f0b9d849becde622b5ba10507d5961 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 27 Jun 2025 09:23:37 +0200 Subject: [PATCH 112/448] Get Aggregated Calculation Working --- .../example_calculation_types.py | 6 +- flixopt/aggregation.py | 4 +- flixopt/calculation.py | 50 +++++++++++++--- flixopt/core.py | 58 ++++++++++++------- flixopt/flow_system.py | 3 +- tests/conftest.py | 6 +- 6 files changed, 89 insertions(+), 38 deletions(-) diff --git a/examples/03_Calculation_types/example_calculation_types.py b/examples/03_Calculation_types/example_calculation_types.py index ee61d6628..cac628042 100644 --- a/examples/03_Calculation_types/example_calculation_types.py +++ b/examples/03_Calculation_types/example_calculation_types.py @@ -48,9 +48,9 @@ # TimeSeriesData objects TS_heat_demand = fx.TimeSeriesData(heat_demand) - TS_electricity_demand = fx.TimeSeriesData(electricity_demand, agg_weight=0.7) - TS_electricity_price_sell = fx.TimeSeriesData(-(electricity_demand - 0.5), agg_group='p_el') - TS_electricity_price_buy = fx.TimeSeriesData(electricity_price + 0.5, agg_group='p_el') + TS_electricity_demand = fx.TimeSeriesData(electricity_demand, aggregation_weight=0.7) + TS_electricity_price_sell = fx.TimeSeriesData(-(electricity_demand - 0.5), aggregation_group='p_el') + TS_electricity_price_buy = fx.TimeSeriesData(electricity_price + 0.5, aggregation_group='p_el') flow_system = fx.FlowSystem(timesteps) flow_system.add_elements( diff --git a/flixopt/aggregation.py b/flixopt/aggregation.py index f149d5f20..d47a42997 100644 --- a/flixopt/aggregation.py +++ b/flixopt/aggregation.py @@ -274,11 +274,11 @@ def use_extreme_periods(self): @property def labels_for_high_peaks(self) -> List[str]: - return [ts.label for ts in self.time_series_for_high_peaks] + return [ts.name for ts in self.time_series_for_high_peaks] @property def labels_for_low_peaks(self) -> List[str]: - return [ts.label for ts in self.time_series_for_low_peaks] + return [ts.name for ts in self.time_series_for_low_peaks] @property def use_low_peaks(self): diff --git a/flixopt/calculation.py b/flixopt/calculation.py index 60163b7a2..43884632f 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -14,17 +14,19 @@ import timeit import warnings from typing import Annotated, Any, Dict, List, Optional, Union +from collections import Counter import numpy as np import pandas as pd import yaml +import xarray as xr from . import io as fx_io from . import utils as utils from .aggregation import AggregationModel, AggregationParameters from .components import Storage from .config import CONFIG -from .core import Scalar +from .core import Scalar, DataConverter, drop_constant_arrays, TimeSeriesData from .elements import Component from .features import InvestmentModel from .flow_system import FlowSystem @@ -294,15 +296,17 @@ def _perform_aggregation(self): logger.info(f'{"":#^80}') logger.info(f'{" Aggregating TimeSeries Data ":#^80}') + ds = self.flow_system.to_dataset() + + temporaly_changing_ds = drop_constant_arrays(ds, dim='time') + # Aggregation - creation of aggregated timeseries: self.aggregation = Aggregation( - original_data=self.flow_system.to_dataframe( - include_extra_timestep=False - ), # Exclude last row (NaN) + original_data=temporaly_changing_ds.to_dataframe(), hours_per_time_step=float(dt_min), hours_per_period=self.aggregation_parameters.hours_per_period, nr_of_periods=self.aggregation_parameters.nr_of_periods, - weights=self.flow_system.calculate_aggregation_weights(), + weights=self.calculate_aggregation_weights(temporaly_changing_ds), time_series_for_high_peaks=self.aggregation_parameters.labels_for_high_peaks, time_series_for_low_peaks=self.aggregation_parameters.labels_for_low_peaks, ) @@ -310,11 +314,41 @@ def _perform_aggregation(self): self.aggregation.cluster() self.aggregation.plot(show=True, save=self.folder / 'aggregation.html') if self.aggregation_parameters.aggregate_data_and_fix_non_binary_vars: - self.flow_system.insert_new_data( - self.aggregation.aggregated_data, include_extra_timestep=False - ) + ds = self.flow_system.to_dataset() + for name, series in self.aggregation.aggregated_data.items(): + da = DataConverter.to_dataarray(series, timesteps=self.flow_system.timesteps).rename(name).assign_attrs(ds[name].attrs) + if TimeSeriesData.is_timeseries_data(da): + da = TimeSeriesData.from_dataarray(da) + + ds[name] = da + + self.flow_system = FlowSystem.from_dataset(ds) + self.flow_system.connect_and_transform() self.durations['aggregation'] = round(timeit.default_timer() - t_start_agg, 2) + @classmethod + def calculate_aggregation_weights(cls, ds: xr.Dataset) -> Dict[str, float]: + """Calculate weights for all datavars in the dataset. Weights are pulled from the attrs of the datavars.""" + + groups = [da.attrs['aggregation_group'] for da in ds.values() if 'aggregation_group' in da.attrs] + group_counts = Counter(groups) + + # Calculate weight for each group (1/count) + group_weights = {group: 1 / count for group, count in group_counts.items()} + + weights = {} + for name, da in ds.data_vars.items(): + group_weight = group_weights.get(da.attrs.get('aggregation_group')) + if group_weight is not None: + weights[name] = group_weight + else: + weights[name] = da.attrs.get('aggregation_weight', 1) + + if np.all(np.isclose(list(weights.values()), 1, atol=1e-6)): + logger.info('All Aggregation weights were set to 1') + + return weights + class SegmentedCalculation(Calculation): def __init__( diff --git a/flixopt/core.py b/flixopt/core.py index 41ee7b799..5bba418be 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -41,45 +41,45 @@ class TimeSeriesData(xr.DataArray): __slots__ = () # No additional instance attributes - everything goes in attrs - def __init__(self, *args, agg_group: Optional[str] = None, agg_weight: Optional[float] = None, **kwargs): + def __init__(self, *args, aggregation_group: Optional[str] = None, aggregation_weight: Optional[float] = None, **kwargs): """ Args: *args: Arguments passed to DataArray - agg_group: Aggregation group name - agg_weight: Aggregation weight (0-1) + aggregation_group: Aggregation group name + aggregation_weight: Aggregation weight (0-1) **kwargs: Additional arguments passed to DataArray """ - if (agg_group is not None) and (agg_weight is not None): - raise ValueError('Use either agg_group or agg_weight, not both') + if (aggregation_group is not None) and (aggregation_weight is not None): + raise ValueError('Use either aggregation_group or aggregation_weight, not both') # Let xarray handle all the initialization complexity super().__init__(*args, **kwargs) # Add our metadata to attrs after initialization - if agg_group is not None: - self.attrs['agg_group'] = agg_group - if agg_weight is not None: - self.attrs['agg_weight'] = agg_weight + if aggregation_group is not None: + self.attrs['aggregation_group'] = aggregation_group + if aggregation_weight is not None: + self.attrs['aggregation_weight'] = aggregation_weight # Always mark as TimeSeriesData self.attrs['__timeseries_data__'] = True @property - def agg_group(self) -> Optional[str]: - return self.attrs.get('agg_group') + def aggregation_group(self) -> Optional[str]: + return self.attrs.get('aggregation_group') @property - def agg_weight(self) -> Optional[float]: - return self.attrs.get('agg_weight') + def aggregation_weight(self) -> Optional[float]: + return self.attrs.get('aggregation_weight') @classmethod - def from_dataarray(cls, da: xr.DataArray, agg_group: Optional[str] = None, agg_weight: Optional[float] = None): + def from_dataarray(cls, da: xr.DataArray, aggregation_group: Optional[str] = None, aggregation_weight: Optional[float] = None): """Create TimeSeriesData from DataArray, extracting metadata from attrs.""" # Get aggregation metadata from attrs or parameters - final_agg_group = agg_group if agg_group is not None else da.attrs.get('agg_group') - final_agg_weight = agg_weight if agg_weight is not None else da.attrs.get('agg_weight') + final_aggregation_group = aggregation_group if aggregation_group is not None else da.attrs.get('aggregation_group') + final_aggregation_weight = aggregation_weight if aggregation_weight is not None else da.attrs.get('aggregation_weight') - return cls(da, agg_group=final_agg_group, agg_weight=final_agg_weight) + return cls(da, aggregation_group=final_aggregation_group, aggregation_weight=final_aggregation_weight) @classmethod def is_timeseries_data(cls, obj) -> bool: @@ -88,10 +88,10 @@ def is_timeseries_data(cls, obj) -> bool: def __repr__(self): agg_info = [] - if self.agg_group: - agg_info.append(f"agg_group='{self.agg_group}'") - if self.agg_weight is not None: - agg_info.append(f'agg_weight={self.agg_weight}') + if self.aggregation_group: + agg_info.append(f"aggregation_group='{self.aggregation_group}'") + if self.aggregation_weight is not None: + agg_info.append(f'aggregation_weight={self.aggregation_weight}') info_str = f'TimeSeriesData({", ".join(agg_info)})' if agg_info else 'TimeSeriesData' return f'{info_str}\n{super().__repr__()}' @@ -255,3 +255,19 @@ def get_dataarray_stats(arr: xr.DataArray) -> Dict: pass return stats + + +def drop_constant_arrays(ds: xr.Dataset, dim='time', drop_arrays_without_dim: bool = True): + """Drop variables with very low variance (near-constant).""" + drop_vars = [] + + for name, da in ds.data_vars.items(): + if dim in da.dims: + if da.max(dim) == da.min(dim): + drop_vars.append(name) + continue + elif drop_arrays_without_dim: + drop_vars.append(name) + + logger.debug(f'Dropping {len(drop_vars)} arrays with constant values') + return ds.drop_vars(drop_vars) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 49321ba82..560d740bd 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -288,9 +288,10 @@ def fit_to_model_coords( if isinstance(data, TimeSeriesData): try: + data.name = name # Set name of previous object! return TimeSeriesData( DataConverter.to_dataarray(data, timesteps=self.timesteps), - agg_group=data.agg_group, agg_weight=data.agg_weight + aggregation_group=data.aggregation_group, aggregation_weight=data.aggregation_weight ).rename(name) except ConversionError as e: logger.critical(f'Could not convert time series data "{name}" to DataArray: {e}. \n' diff --git a/tests/conftest.py b/tests/conftest.py index b705939cc..074c56efe 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -327,11 +327,11 @@ def flow_system_long(): thermal_load_ts, electrical_load_ts = ( fx.TimeSeriesData(thermal_load), - fx.TimeSeriesData(electrical_load, agg_weight=0.7), + fx.TimeSeriesData(electrical_load, aggregation_weight=0.7), ) p_feed_in, p_sell = ( - fx.TimeSeriesData(-(p_el - 0.5), agg_group='p_el'), - fx.TimeSeriesData(p_el + 0.5, agg_group='p_el'), + fx.TimeSeriesData(-(p_el - 0.5), aggregation_group='p_el'), + fx.TimeSeriesData(p_el + 0.5, aggregation_group='p_el'), ) flow_system = fx.FlowSystem(pd.DatetimeIndex(data.index)) From 8b9dabb7f917b8e06f2d30deffe740ab16df32de Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 27 Jun 2025 09:46:23 +0200 Subject: [PATCH 113/448] Segmented running with wrong results --- flixopt/calculation.py | 47 +++++++++++++++++++----------------------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/flixopt/calculation.py b/flixopt/calculation.py index 43884632f..5a1437ba9 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -385,8 +385,6 @@ def __init__( self.nr_of_previous_values = nr_of_previous_values self.sub_calculations: List[FullCalculation] = [] - self.all_timesteps = self.flow_system.all_timesteps - self.all_timesteps_extra = self.flow_system.all_timesteps_extra self.segment_names = [ f'Segment_{i + 1}' for i in range(math.ceil(len(self.all_timesteps) / self.timesteps_per_segment)) @@ -419,22 +417,22 @@ def do_modeling_and_solve( for i, (segment_name, timesteps_of_segment) in enumerate( zip(self.segment_names, self.active_timesteps_per_segment, strict=False) ): - if self.sub_calculations: - self._transfer_start_values(i) + calculation = FullCalculation( + f'{self.name}-{segment_name}', self.flow_system.sel(timesteps_of_segment), + ) + self.sub_calculations.append(calculation) logger.info( f'{segment_name} [{i + 1:>2}/{len(self.segment_names):<2}] ' f'({timesteps_of_segment[0]} -> {timesteps_of_segment[-1]}):' ) + if len(self.sub_calculations) >= 2: + self._transfer_start_values(i) - calculation = FullCalculation( - f'{self.name}-{segment_name}', self.flow_system.sel(timesteps_of_segment), - ) - self.sub_calculations.append(calculation) calculation.do_modeling() invest_elements = [ model.label_full - for component in self.flow_system.components.values() + for component in calculation.flow_system.components.values() for model in component.model.all_sub_models if isinstance(model, InvestmentModel) ] @@ -449,8 +447,6 @@ def do_modeling_and_solve( log_main_results=log_main_results, ) - self._reset_start_values() - for calc in self.sub_calculations: for key, value in calc.durations.items(): self.durations[key] += value @@ -471,27 +467,22 @@ def _transfer_start_values(self, segment_index: int): logger.debug( f'start of next segment: {start}. indices of previous values: {start_previous_values}:{end_previous_values}' ) + current_flow_system = self.sub_calculations[segment_index -1].flow_system + next_flow_system = self.sub_calculations[segment_index].flow_system + start_values_of_this_segment = {} - for flow in self.flow_system.flows.values(): - flow.previous_flow_rate = flow.model.flow_rate.solution.sel( + for current_flow, next_flow in zip(current_flow_system.flows.values(), next_flow_system.flows.values()): + next_flow.previous_flow_rate = current_flow.model.flow_rate.solution.sel( time=slice(start_previous_values, end_previous_values) ).values - start_values_of_this_segment[flow.label_full] = flow.previous_flow_rate - for comp in self.flow_system.components.values(): - if isinstance(comp, Storage): - comp.initial_charge_state = comp.model.charge_state.solution.sel(time=start).item() - start_values_of_this_segment[comp.label_full] = comp.initial_charge_state + start_values_of_this_segment[current_flow.label_full] = next_flow.previous_flow_rate + for current_comp, next_comp in zip(current_flow_system.components.values(), next_flow_system.components.values()): + if isinstance(next_comp, Storage): + next_comp.initial_charge_state = current_comp.model.charge_state.solution.sel(time=start).item() + start_values_of_this_segment[current_comp.label_full] = next_comp.initial_charge_state self._transfered_start_values.append(start_values_of_this_segment) - def _reset_start_values(self): - """This resets the start values of all Elements to its original state""" - for flow in self.flow_system.flows.values(): - flow.previous_flow_rate = self._original_start_values[flow.label_full] - for comp in self.flow_system.components.values(): - if isinstance(comp, Storage): - comp.initial_charge_state = self._original_start_values[comp.label_full] - def _calculate_timesteps_of_segment(self) -> List[pd.DatetimeIndex]: active_timesteps_per_segment = [] for i, _ in enumerate(self.segment_names): @@ -511,3 +502,7 @@ def start_values_of_segments(self) -> Dict[int, Dict[str, Any]]: 0: {element.label_full: value for element, value in self._original_start_values.items()}, **{i: start_values for i, start_values in enumerate(self._transfered_start_values, 1)}, } + + @property + def all_timesteps(self) -> pd.DatetimeIndex: + return self.flow_system.timesteps \ No newline at end of file From 7e72ab56d6f66b422212bb4c9468af52dcb86e85 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 27 Jun 2025 09:58:23 +0200 Subject: [PATCH 114/448] Use new persistent FLowSystem to create Calculations upfront --- flixopt/calculation.py | 59 ++++++++++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 23 deletions(-) diff --git a/flixopt/calculation.py b/flixopt/calculation.py index 5a1437ba9..fb5686e15 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -396,7 +396,7 @@ def __init__( f'{self.timesteps_per_segment_with_overlap=} cant be greater than the total length {len(self.all_timesteps)}' ) - self.flow_system._connect_network() # Connect network to ensure that all FLows know their Component + self.flow_system._connect_network() # Connect network to ensure that all Flows know their Component # Storing all original start values self._original_start_values = { **{flow.label_full: flow.previous_flow_rate for flow in self.flow_system.flows.values()}, @@ -408,39 +408,52 @@ def __init__( } self._transfered_start_values: List[Dict[str, Any]] = [] - def do_modeling_and_solve( - self, solver: _Solver, log_file: Optional[pathlib.Path] = None, log_main_results: bool = False - ): - logger.info(f'{"":#^80}') - logger.info(f'{" Segmented Solving ":#^80}') - + def _create_sub_calculations(self): for i, (segment_name, timesteps_of_segment) in enumerate( zip(self.segment_names, self.active_timesteps_per_segment, strict=False) ): - calculation = FullCalculation( - f'{self.name}-{segment_name}', self.flow_system.sel(timesteps_of_segment), + self.sub_calculations.append( + FullCalculation( + f'{self.name}-{segment_name}', self.flow_system.sel(timesteps_of_segment), + folder=self.folder / segment_name + ) ) - self.sub_calculations.append(calculation) - logger.info( f'{segment_name} [{i + 1:>2}/{len(self.segment_names):<2}] ' f'({timesteps_of_segment[0]} -> {timesteps_of_segment[-1]}):' ) + + def do_modeling_and_solve( + self, solver: _Solver, log_file: Optional[pathlib.Path] = None, log_main_results: bool = False + ): + logger.info(f'{"":#^80}') + logger.info(f'{" Segmented Solving ":#^80}') + + for i, calculation in enumerate(self.sub_calculations): + logger.info( + f'{self.segment_names[i]} [{i + 1:>2}/{len(self.segment_names):<2}] ' + f'({calculation.flow_system.timesteps[0]} -> {calculation.flow_system.timesteps[-1]}):' + ) + if len(self.sub_calculations) >= 2: self._transfer_start_values(i) calculation.do_modeling() - invest_elements = [ - model.label_full - for component in calculation.flow_system.components.values() - for model in component.model.all_sub_models - if isinstance(model, InvestmentModel) - ] - if invest_elements: - logger.critical( - f'Investments are not supported in Segmented Calculation! ' - f'Following InvestmentModels were found: {invest_elements}' - ) + + # Warn about Investments, but only in fist run + if i == 0: + invest_elements = [ + model.label_full + for component in calculation.flow_system.components.values() + for model in component.model.all_sub_models + if isinstance(model, InvestmentModel) + ] + if invest_elements: + logger.critical( + f'Investments are not supported in Segmented Calculation! ' + f'Following InvestmentModels were found: {invest_elements}' + ) + calculation.solve( solver, log_file=pathlib.Path(log_file) if log_file is not None else self.folder / f'{self.name}.log', @@ -458,7 +471,7 @@ def _transfer_start_values(self, segment_index: int): This function gets the last values of the previous solved segment and inserts them as start values for the next segment """ - timesteps_of_prior_segment = self.active_timesteps_per_segment[segment_index - 1] + timesteps_of_prior_segment = self.sub_calculations[segment_index - 1].flow_system.timesteps_extra start = self.active_timesteps_per_segment[segment_index][0] start_previous_values = timesteps_of_prior_segment[self.timesteps_per_segment - self.nr_of_previous_values] From 17632f36895e82f471b4f1562e543431838b6006 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 27 Jun 2025 10:22:52 +0200 Subject: [PATCH 115/448] Improve SegmentedCalcualtion --- flixopt/calculation.py | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/flixopt/calculation.py b/flixopt/calculation.py index fb5686e15..b0f71a40e 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -389,7 +389,7 @@ def __init__( self.segment_names = [ f'Segment_{i + 1}' for i in range(math.ceil(len(self.all_timesteps) / self.timesteps_per_segment)) ] - self.active_timesteps_per_segment = self._calculate_timesteps_of_segment() + self._timesteps_per_segment = self._calculate_timesteps_per_segment() assert timesteps_per_segment > 2, 'The Segment length must be greater 2, due to unwanted internal side effects' assert self.timesteps_per_segment_with_overlap <= len(self.all_timesteps), ( @@ -410,12 +410,11 @@ def __init__( def _create_sub_calculations(self): for i, (segment_name, timesteps_of_segment) in enumerate( - zip(self.segment_names, self.active_timesteps_per_segment, strict=False) + zip(self.segment_names, self._timesteps_per_segment, strict=False) ): self.sub_calculations.append( FullCalculation( f'{self.name}-{segment_name}', self.flow_system.sel(timesteps_of_segment), - folder=self.folder / segment_name ) ) logger.info( @@ -428,6 +427,7 @@ def do_modeling_and_solve( ): logger.info(f'{"":#^80}') logger.info(f'{" Segmented Solving ":#^80}') + self._create_sub_calculations() for i, calculation in enumerate(self.sub_calculations): logger.info( @@ -435,7 +435,7 @@ def do_modeling_and_solve( f'({calculation.flow_system.timesteps[0]} -> {calculation.flow_system.timesteps[-1]}):' ) - if len(self.sub_calculations) >= 2: + if i > 0: self._transfer_start_values(i) calculation.do_modeling() @@ -466,22 +466,22 @@ def do_modeling_and_solve( self.results = SegmentedCalculationResults.from_calculation(self) - def _transfer_start_values(self, segment_index: int): + def _transfer_start_values(self, i: int): """ This function gets the last values of the previous solved segment and inserts them as start values for the next segment """ - timesteps_of_prior_segment = self.sub_calculations[segment_index - 1].flow_system.timesteps_extra + timesteps_of_prior_segment = self.sub_calculations[i - 1].flow_system.timesteps_extra - start = self.active_timesteps_per_segment[segment_index][0] + start = self.sub_calculations[i].flow_system.timesteps[0] start_previous_values = timesteps_of_prior_segment[self.timesteps_per_segment - self.nr_of_previous_values] end_previous_values = timesteps_of_prior_segment[self.timesteps_per_segment - 1] logger.debug( f'start of next segment: {start}. indices of previous values: {start_previous_values}:{end_previous_values}' ) - current_flow_system = self.sub_calculations[segment_index -1].flow_system - next_flow_system = self.sub_calculations[segment_index].flow_system + current_flow_system = self.sub_calculations[i -1].flow_system + next_flow_system = self.sub_calculations[i].flow_system start_values_of_this_segment = {} for current_flow, next_flow in zip(current_flow_system.flows.values(), next_flow_system.flows.values()): @@ -496,25 +496,24 @@ def _transfer_start_values(self, segment_index: int): self._transfered_start_values.append(start_values_of_this_segment) - def _calculate_timesteps_of_segment(self) -> List[pd.DatetimeIndex]: - active_timesteps_per_segment = [] + def _calculate_timesteps_per_segment(self) -> List[pd.DatetimeIndex]: + timesteps_per_segment = [] for i, _ in enumerate(self.segment_names): start = self.timesteps_per_segment * i end = min(start + self.timesteps_per_segment_with_overlap, len(self.all_timesteps)) - active_timesteps_per_segment.append(self.all_timesteps[start:end]) - return active_timesteps_per_segment + timesteps_per_segment.append(self.all_timesteps[start:end]) + return timesteps_per_segment @property def timesteps_per_segment_with_overlap(self): return self.timesteps_per_segment + self.overlap_timesteps @property - def start_values_of_segments(self) -> Dict[int, Dict[str, Any]]: + def start_values_of_segments(self) -> List[Dict[str, Any]]: """Gives an overview of the start values of all Segments""" - return { - 0: {element.label_full: value for element, value in self._original_start_values.items()}, - **{i: start_values for i, start_values in enumerate(self._transfered_start_values, 1)}, - } + return [ + {name: value for name, value in self._original_start_values.items()} + ] + [start_values for start_values in self._transfered_start_values] @property def all_timesteps(self) -> pd.DatetimeIndex: From 3c355c9c70a2e67ff7c1208f6854c75dddf7d23f Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 27 Jun 2025 11:32:47 +0200 Subject: [PATCH 116/448] Improve SegmentedCalcualtion --- flixopt/calculation.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/flixopt/calculation.py b/flixopt/calculation.py index b0f71a40e..8e4a57100 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -410,13 +410,12 @@ def __init__( def _create_sub_calculations(self): for i, (segment_name, timesteps_of_segment) in enumerate( - zip(self.segment_names, self._timesteps_per_segment, strict=False) + zip(self.segment_names, self._timesteps_per_segment, strict=True) ): - self.sub_calculations.append( - FullCalculation( - f'{self.name}-{segment_name}', self.flow_system.sel(timesteps_of_segment), - ) - ) + calc = FullCalculation(f'{self.name}-{segment_name}', self.flow_system.sel(timesteps_of_segment)) + calc.flow_system._connect_network() # Connect to have Correct names of Flows! + + self.sub_calculations.append(calc) logger.info( f'{segment_name} [{i + 1:>2}/{len(self.segment_names):<2}] ' f'({timesteps_of_segment[0]} -> {timesteps_of_segment[-1]}):' @@ -435,7 +434,7 @@ def do_modeling_and_solve( f'({calculation.flow_system.timesteps[0]} -> {calculation.flow_system.timesteps[-1]}):' ) - if i > 0: + if i > 0 and self.nr_of_previous_values > 0: self._transfer_start_values(i) calculation.do_modeling() @@ -478,18 +477,22 @@ def _transfer_start_values(self, i: int): end_previous_values = timesteps_of_prior_segment[self.timesteps_per_segment - 1] logger.debug( - f'start of next segment: {start}. indices of previous values: {start_previous_values}:{end_previous_values}' + f'Start of next segment: {start}. Indices of previous values: {start_previous_values} -> {end_previous_values}' ) current_flow_system = self.sub_calculations[i -1].flow_system next_flow_system = self.sub_calculations[i].flow_system start_values_of_this_segment = {} - for current_flow, next_flow in zip(current_flow_system.flows.values(), next_flow_system.flows.values()): + + for current_flow in current_flow_system.flows.values(): + next_flow = next_flow_system.flows[current_flow.label_full] next_flow.previous_flow_rate = current_flow.model.flow_rate.solution.sel( time=slice(start_previous_values, end_previous_values) ).values start_values_of_this_segment[current_flow.label_full] = next_flow.previous_flow_rate - for current_comp, next_comp in zip(current_flow_system.components.values(), next_flow_system.components.values()): + + for current_comp in current_flow_system.components.values(): + next_comp = next_flow_system.components[current_comp.label_full] if isinstance(next_comp, Storage): next_comp.initial_charge_state = current_comp.model.charge_state.solution.sel(time=start).item() start_values_of_this_segment[current_comp.label_full] = next_comp.initial_charge_state From f473ce523adac4813648a8da11e52acbeacca0a0 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 27 Jun 2025 11:34:36 +0200 Subject: [PATCH 117/448] Fix SegmentedResults IO --- flixopt/results.py | 4 ++-- tests/test_integration.py | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/flixopt/results.py b/flixopt/results.py index e13cb0785..1dee9ac02 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -659,7 +659,7 @@ def from_file(cls, folder: Union[str, pathlib.Path], name: str): with open(path.with_suffix('.json'), 'r', encoding='utf-8') as f: meta_data = json.load(f) return cls( - [CalculationResults.from_file(folder, name) for name in meta_data['sub_calculations']], + [CalculationResults.from_file(folder, sub_name) for sub_name in meta_data['sub_calculations']], all_timesteps=pd.DatetimeIndex( [datetime.datetime.fromisoformat(date) for date in meta_data['all_timesteps']], name='time' ), @@ -756,7 +756,7 @@ def to_file( f'Folder {folder} and its parent do not exist. Please create them first.' ) from e for segment in self.segment_results: - segment.to_file(folder=folder, name=f'{name}-{segment.name}', compression=compression) + segment.to_file(folder=folder, name=segment.name, compression=compression) with open(path.with_suffix('.json'), 'w', encoding='utf-8') as f: json.dump(self.meta_data, f, indent=4, ensure_ascii=False) diff --git a/tests/test_integration.py b/tests/test_integration.py index dc203c33e..da473b4e6 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -420,6 +420,12 @@ def test_modeling_types_costs(self, modeling_calculation): f'Costs do not match for {modeling_type} modeling type', ) + def test_segmented_io(self, modeling_calculation): + calc, modeling_type = modeling_calculation + if modeling_type == 'segmented': + calc.results.to_file() + _ = fx.results.SegmentedCalculationResults.from_file(calc.folder, calc.name) + if __name__ == '__main__': pytest.main(['-v']) From 7869a7249617c686ccc39ad0794fc2cebe218d34 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 27 Jun 2025 11:47:30 +0200 Subject: [PATCH 118/448] ruff check --- flixopt/calculation.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/flixopt/calculation.py b/flixopt/calculation.py index 8e4a57100..0c844f78f 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -13,20 +13,20 @@ import pathlib import timeit import warnings -from typing import Annotated, Any, Dict, List, Optional, Union from collections import Counter +from typing import Annotated, Any, Dict, List, Optional, Union import numpy as np import pandas as pd -import yaml import xarray as xr +import yaml from . import io as fx_io from . import utils as utils from .aggregation import AggregationModel, AggregationParameters from .components import Storage from .config import CONFIG -from .core import Scalar, DataConverter, drop_constant_arrays, TimeSeriesData +from .core import DataConverter, Scalar, TimeSeriesData, drop_constant_arrays from .elements import Component from .features import InvestmentModel from .flow_system import FlowSystem @@ -520,4 +520,4 @@ def start_values_of_segments(self) -> List[Dict[str, Any]]: @property def all_timesteps(self) -> pd.DatetimeIndex: - return self.flow_system.timesteps \ No newline at end of file + return self.flow_system.timesteps From bb29ef254a6c410d43ddd72011060f7488d1ee35 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 27 Jun 2025 12:08:27 +0200 Subject: [PATCH 119/448] Update example --- examples/01_Simple/simple_example.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/01_Simple/simple_example.py b/examples/01_Simple/simple_example.py index 45550c9cc..963f2fbe1 100644 --- a/examples/01_Simple/simple_example.py +++ b/examples/01_Simple/simple_example.py @@ -67,7 +67,8 @@ discharging=fx.Flow('Q_th_unload', bus='Fernwärme', size=1000), capacity_in_flow_hours=fx.InvestParameters(fix_effects=20, fixed_size=30, optional=False), initial_charge_state=0, # Initial storage state: empty - relative_maximum_charge_state=1 / 100 * np.array([80, 70, 80, 80, 80, 80, 80, 80, 80, 80]), + relative_maximum_charge_state=1 / 100 * np.array([80, 70, 80, 80, 80, 80, 80, 80, 80]), + relative_maximum_final_charge_state=0.8, eta_charge=0.9, eta_discharge=1, # Efficiency factors for charging/discharging relative_loss_per_hour=0.08, # 8% loss per hour. Absolute loss depends on current charge state From 8d96a49b3a259ceb28540d526e0eb7c4c0f69613 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 27 Jun 2025 13:46:05 +0200 Subject: [PATCH 120/448] Updated logger essages to use .label_full instead of .label --- flixopt/elements.py | 8 ++++---- flixopt/flow_system.py | 14 +++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/flixopt/elements.py b/flixopt/elements.py index d596333c3..a49a12f0d 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -117,7 +117,7 @@ def transform_data(self, flow_system: 'FlowSystem'): def _plausibility_checks(self) -> None: if self.excess_penalty_per_flow_hour is not None and (self.excess_penalty_per_flow_hour == 0).all(): - logger.warning(f'In Bus {self.label}, the excess_penalty_per_flow_hour is 0. Use "None" or a value > 0.') + logger.warning(f'In Bus {self.label_full}, the excess_penalty_per_flow_hour is 0. Use "None" or a value > 0.') @property def with_excess(self) -> bool: @@ -256,21 +256,21 @@ def _plausibility_checks(self) -> None: self.size == CONFIG.modeling.BIG and self.fixed_relative_profile is not None ): # Default Size --> Most likely by accident logger.warning( - f'Flow "{self.label}" has no size assigned, but a "fixed_relative_profile". ' + f'Flow "{self.label_full}" has no size assigned, but a "fixed_relative_profile". ' f'The default size is {CONFIG.modeling.BIG}. As "flow_rate = size * fixed_relative_profile", ' f'the resulting flow_rate will be very high. To fix this, assign a size to the Flow {self}.' ) if self.fixed_relative_profile is not None and self.on_off_parameters is not None: raise ValueError( - f'Flow {self.label} has both a fixed_relative_profile and an on_off_parameters. This is not supported. ' + f'Flow {self.label_full} has both a fixed_relative_profile and an on_off_parameters. This is not supported. ' f'Use relative_minimum and relative_maximum instead, ' f'if you want to allow flows to be switched on and off.' ) if (self.relative_minimum > 0).any() and self.on_off_parameters is None: logger.warning( - f'Flow {self.label} has a relative_minimum of {self.relative_minimum} and no on_off_parameters. ' + f'Flow {self.label_full} has a relative_minimum of {self.relative_minimum} and no on_off_parameters. ' f'This prevents the flow_rate from switching off (flow_rate = 0). ' f'Consider using on_off_parameters to allow the flow to be switched on and off.' ) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 560d740bd..306872674 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -409,25 +409,25 @@ def _check_if_element_is_unique(self, element: Element) -> None: element: new element to check """ if element in self.all_elements.values(): - raise ValueError(f'Element {element.label} already added to FlowSystem!') + raise ValueError(f'Element {element.label_full} already added to FlowSystem!') # check if name is already used: if element.label_full in self.all_elements: - raise ValueError(f'Label of Element {element.label} already used in another element!') + raise ValueError(f'Label of Element {element.label_full} already used in another element!') def _add_effects(self, *args: Effect) -> None: self.effects.add_effects(*args) def _add_components(self, *components: Component) -> None: for new_component in list(components): - logger.info(f'Registered new Component: {new_component.label}') + logger.info(f'Registered new Component: {new_component.label_full}') self._check_if_element_is_unique(new_component) # check if already exists: - self.components[new_component.label] = new_component # Add to existing components + self.components[new_component.label_full] = new_component # Add to existing components def _add_buses(self, *buses: Bus): for new_bus in list(buses): - logger.info(f'Registered new Bus: {new_bus.label}') + logger.info(f'Registered new Bus: {new_bus.label_full}') self._check_if_element_is_unique(new_bus) # check if already exists: - self.buses[new_bus.label] = new_bus # Add to existing components + self.buses[new_bus.label_full] = new_bus # Add to existing components def _connect_network(self): """Connects the network of components and buses. Can be rerun without changes if no elements were added""" @@ -440,7 +440,7 @@ def _connect_network(self): if flow._bus_object is not None and flow._bus_object not in self.buses.values(): self._add_buses(flow._bus_object) warnings.warn( - f'The Bus {flow._bus_object.label} was added to the FlowSystem from {flow.label_full}.' + f'The Bus {flow._bus_object.label_full} was added to the FlowSystem from {flow.label_full}.' f'This is deprecated and will be removed in the future. ' f'Please pass the Bus.label to the Flow and the Bus to the FlowSystem instead.', UserWarning, From 8240da1046b979e0925a8e14ebd1fe7850b9ac1a Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 27 Jun 2025 13:59:02 +0200 Subject: [PATCH 121/448] Re-add parameters. Use deprecation warning instead --- flixopt/calculation.py | 2 +- flixopt/core.py | 23 ++++++++++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/flixopt/calculation.py b/flixopt/calculation.py index 0c844f78f..66a33497b 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -74,9 +74,9 @@ def __init__( ) flow_system = flow_system.sel(time=active_timesteps) + flow_system._used_in_calculation = True self.flow_system = flow_system - self.flow_system._used_in_calculation = True self.model: Optional[SystemModel] = None self._active_timesteps = active_timesteps # deprecated diff --git a/flixopt/core.py b/flixopt/core.py index 5bba418be..1aa175ed0 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -4,6 +4,7 @@ """ import logging +import warnings from typing import Dict, Optional, Union import numpy as np @@ -41,14 +42,24 @@ class TimeSeriesData(xr.DataArray): __slots__ = () # No additional instance attributes - everything goes in attrs - def __init__(self, *args, aggregation_group: Optional[str] = None, aggregation_weight: Optional[float] = None, **kwargs): + def __init__(self, *args, aggregation_group: Optional[str] = None, aggregation_weight: Optional[float] = None, + agg_group: Optional[str] = None, agg_weight: Optional[float] = None, **kwargs): """ Args: *args: Arguments passed to DataArray aggregation_group: Aggregation group name aggregation_weight: Aggregation weight (0-1) + agg_group: Deprecated, use aggregation_group instead + agg_weight: Deprecated, use aggregation_weight instead **kwargs: Additional arguments passed to DataArray """ + if agg_group is not None: + warnings.warn('agg_group is deprecated, use aggregation_group instead', DeprecationWarning, stacklevel=2) + aggregation_group = agg_group + if agg_weight is not None: + warnings.warn('agg_weight is deprecated, use aggregation_weight instead', DeprecationWarning, stacklevel=2) + aggregation_weight = agg_weight + if (aggregation_group is not None) and (aggregation_weight is not None): raise ValueError('Use either aggregation_group or aggregation_weight, not both') @@ -96,6 +107,16 @@ def __repr__(self): info_str = f'TimeSeriesData({", ".join(agg_info)})' if agg_info else 'TimeSeriesData' return f'{info_str}\n{super().__repr__()}' + @property + def agg_group(self): + warnings.warn('agg_group is deprecated, use aggregation_group instead', DeprecationWarning, stacklevel=2) + return self._aggregation_group + + @property + def agg_weight(self): + warnings.warn('agg_weight is deprecated, use aggregation_weight instead', DeprecationWarning, stacklevel=2) + return self._aggregation_weight + class DataConverter: """ From 8ac2664ce99b1a507f2314db55ab12b8bd88c331 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 27 Jun 2025 14:05:58 +0200 Subject: [PATCH 122/448] Update changelog --- CHANGELOG.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1871de91a..45121534f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,12 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Changed -* **BREAKING**: FlowSystems can not be shared across multiple Calculations anymore. A copy of the FlowSystem is created instead, making every Calculation independent -* **BREAKING**: Type system overhaul - added clear separation between temporal and non-temporal data throughout codebase for better clarity * **BREAKING**: `relative_minimum_charge_state` and `relative_maximum_charge_state` don't have an extra timestep anymore. The final charge state can now be constrained by parameters `relative_minimum_final_charge_state` and `relative_maximum_final_charge_state` instead +* FlowSystems can not be shared across multiple Calculations anymore. A copy of the FlowSystem is created instead, making every Calculation independent +* Type system overhaul - added clear separation between temporal and non-temporal data throughout codebase for better clarity * FlowSystem data management simplified - removed `time_series_collection` pattern in favor of direct timestep properties * Enhanced FlowSystem interface with improved `__repr__()` and `__str__()` methods -* *Internal*: Removed intermediate `TimeSeries` and `TimeSeriesCollection` classes, replaced directly with `xr.DataArray` or `TimeSeriesData` (inheriting from `xr.DataArray`) ### Added * **NEW**: Complete serialization infrastructure through `Interface` base class @@ -25,7 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * **NEW**: FlowSystem data manipulation methods * `sel()` and `isel()` methods for temporal data selection * `resample()` method for temporal resampling - * `copy()` method with deep copying support + * `copy()` method to create a copy of a FlowSystem, including all underlying Elements and their data * `__eq__()` method for FlowSystem comparison * **NEW**: Storage component enhancements * `relative_minimum_final_charge_state` parameter for final state control @@ -47,6 +46,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Plotly >= 6 may raise errors if "nbformat" is not installed. We pinned plotly to <6, but this may be fixed in the future. * IO for single Interfaces/Elemenets to Datasets might not work properly if the Interface/Element is not part of a fully transformed and connected FlowSystem. This arrises from Numeric Data not being stored as xr.DataArray by the user. TO avoid this, always use the `to_dataset()` on Elements inside a FlowSystem thats connected and transformed. +### Deprecated +* The `agg_group` and `agg_weight` parameters of `TimeSeriesData` are deprecated and will be removed in a future version. Use `aggregation_group` and `aggregation_weight` instead. +* The `active_timesteps` parameter of `Calculation` is deprecated and will be removed in a future version. Use the new `sel(time=...)` method on the FlowSystem instead. +* The assignment of Bus Objects to Flow.bus is deprecated and will be removed in a future version. Use the label of the Bus instead. +* The usage of Effects objects in Dicts to assign shares to Effects is deprecated and will be removed in a future version. Use the label of the Effect instead. + ## [2.1.2] - 2025-06-14 ### Fixed From 43a64eaf3f04f531217c2d560fc9813352a8a754 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 27 Jun 2025 14:17:02 +0200 Subject: [PATCH 123/448] Improve warning message --- flixopt/core.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/flixopt/core.py b/flixopt/core.py index 1aa175ed0..121c7fb12 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -148,7 +148,10 @@ def _fix_timeseries_data_indexing( # Check if dimensions match if data.dims != tuple(dims): - logger.warning(f'TimeSeriesData has dimensions {data.dims}, expected {dims}. Reshaping to match timesteps.') + logger.warning( + f'TimeSeriesData has dimensions {data.dims}, expected {dims}. Reshaping to match timesteps. To avoid ' + f'this warning, create a correctly shaped DataArray with the correct dimensions in the first place.' + ) # Try to reshape the data to match expected dimensions if data.size != len(timesteps): raise ConversionError( From b3fe443ea576db57ab452ab667cffa58a6e0eea9 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 27 Jun 2025 15:17:24 +0200 Subject: [PATCH 124/448] Merge --- flixopt/components.py | 2 +- flixopt/core.py | 12 ++++++------ flixopt/effects.py | 18 +++++++++--------- flixopt/features.py | 20 ++++++++++---------- flixopt/flow_system.py | 23 ++++++++++++----------- flixopt/interface.py | 39 ++++++++++++++++++--------------------- flixopt/structure.py | 19 ++++++++----------- tests/conftest.py | 2 +- 8 files changed, 65 insertions(+), 70 deletions(-) diff --git a/flixopt/components.py b/flixopt/components.py index db92067f5..c2787ef9a 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -10,7 +10,7 @@ import xarray as xr from . import utils -from .core import PlausibilityError, Scalar, TemporalData, TemporalDataUser, ScenarioData +from .core import PlausibilityError, Scalar, TemporalData, TemporalDataUser, NonTemporalDataUser from .elements import Component, ComponentModel, Flow from .features import InvestmentModel, OnOffModel, PiecewiseModel from .interface import InvestParameters, OnOffParameters, PiecewiseConversion diff --git a/flixopt/core.py b/flixopt/core.py index 1c58ed290..9c9d6f891 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -5,7 +5,7 @@ import logging import warnings -from typing import Dict, Optional, Union +from typing import Dict, Optional, Union, Tuple import numpy as np import pandas as pd @@ -24,11 +24,11 @@ TemporalData = Union[xr.DataArray, 'TimeSeriesData'] """Internally used datatypes for temporal data.""" -TimestepData = NumericData -"""Represents any form of numeric data that corresponds to timesteps.""" +NonTemporalDataUser = Union[int, float, np.integer, np.floating, np.ndarray, pd.Series, pd.DataFrame, xr.DataArray] +"""User data which has no time dimension. Internally converted to an xr.DataArray without a time dimension.""" -ScenarioData = NumericData -"""Represents any form of numeric data that corresponds to scenarios.""" +NonTemporalData = Union[Scalar, xr.DataArray] +"""Internally used datatypes for non-temporal data.""" class PlausibilityError(Exception): @@ -196,7 +196,7 @@ def _fix_timeseries_data_indexing( @staticmethod def to_dataarray( - data: TimestepData, timesteps: Optional[pd.DatetimeIndex] = None, scenarios: Optional[pd.Index] = None + data: Union[TemporalData, NonTemporalData], timesteps: Optional[pd.DatetimeIndex] = None, scenarios: Optional[pd.Index] = None ) -> xr.DataArray: """ Convert data to xarray.DataArray with specified dimensions. diff --git a/flixopt/effects.py b/flixopt/effects.py index 5270bf7fd..dc1d7d07f 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -39,7 +39,7 @@ def __init__( is_standard: bool = False, is_objective: bool = False, specific_share_to_other_effects_operation: Optional['TemporalEffectsUser'] = None, - specific_share_to_other_effects_invest: Optional['ScalarEffectsUser'] = None, + specific_share_to_other_effects_invest: Optional['NonTemporalEffectsUser'] = None, minimum_operation: Optional[Scalar] = None, maximum_operation: Optional[Scalar] = None, minimum_invest: Optional[Scalar] = None, @@ -79,7 +79,7 @@ def __init__( self.specific_share_to_other_effects_operation: TemporalEffectsUser = ( specific_share_to_other_effects_operation or {} ) - self.specific_share_to_other_effects_invest: ScalarEffectsUser = specific_share_to_other_effects_invest or {} + self.specific_share_to_other_effects_invest: NonTemporalEffectsUser = specific_share_to_other_effects_invest or {} self.minimum_operation = minimum_operation self.maximum_operation = maximum_operation self.minimum_operation_per_hour: TemporalDataUser = minimum_operation_per_hour @@ -148,8 +148,8 @@ def __init__(self, model: SystemModel, element: Effect): label_of_element=self.label_of_element, label='invest', label_full=f'{self.label_full}(invest)', - total_max=extract_data(self.element.maximum_invest), - total_min=extract_data(self.element.minimum_invest), + total_max=self.element.maximum_invest, + total_min=self.element.minimum_invest, ) ) @@ -197,13 +197,13 @@ def do_modeling(self): TemporalEffectsUser = Union[TemporalDataUser, Dict[str, TemporalDataUser]] # User-specified Shares to Effects """ This datatype is used to define a temporal share to an effect by a certain attribute. """ -ScalarEffectsUser = Union[Scalar, Dict[str, Scalar]] # User-specified Shares to Effects +NonTemporalEffectsUser = Union[Scalar, Dict[str, Scalar]] # User-specified Shares to Effects """ This datatype is used to define a scalar share to an effect by a certain attribute. """ TemporalEffects = Dict[str, TemporalData] # User-specified Shares to Effects """ This datatype is used internally to handle temporal shares to an effect. """ -ScalarEffects = Dict[str, Scalar] +NonTemporalEffects = Dict[str, Scalar] """ This datatype is used internally to handle scalar shares to an effect. """ EffectExpr = Dict[str, linopy.LinearExpression] # Used to create Shares @@ -240,7 +240,7 @@ def add_effects(self, *effects: Effect) -> None: def create_effect_values_dict( self, - effect_values_user: Union[ScalarEffectsUser, TemporalEffectsUser] + effect_values_user: Union[NonTemporalEffectsUser, TemporalEffectsUser] ) -> Optional[Dict[str, Union[Scalar, TemporalDataUser]]]: """ Converts effect values into a dictionary. If a scalar is provided, it is associated with a default effect type. @@ -366,7 +366,7 @@ def calculate_effect_share_factors(self) -> Tuple[ for name, effect in self.effects.items(): if effect.specific_share_to_other_effects_invest: shares_invest[name] = { - target: extract_data(data) + target: data for target, data in effect.specific_share_to_other_effects_invest.items() } shares_invest = calculate_all_conversion_paths(shares_invest) @@ -375,7 +375,7 @@ def calculate_effect_share_factors(self) -> Tuple[ for name, effect in self.effects.items(): if effect.specific_share_to_other_effects_operation: shares_operation[name] = { - target: extract_data(data) + target: data for target, data in effect.specific_share_to_other_effects_operation.items() } shares_operation = calculate_all_conversion_paths(shares_operation) diff --git a/flixopt/features.py b/flixopt/features.py index 8b80e94d4..3a2744dee 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -45,8 +45,8 @@ def __init__( def do_modeling(self): self.size = self.add( self._model.add_variables( - lower=0 if self.parameters.optional else extract_data(self.parameters.minimum_size), - upper=extract_data(self.parameters.maximum_size), + lower=0 if self.parameters.optional else self.parameters.minimum_size, + upper=self.parameters.maximum_size, name=f'{self.label_full}|size', coords=self._model.get_coords(time_dim=False), ), @@ -292,8 +292,8 @@ def do_modeling(self): self.total_on_hours = self.add( self._model.add_variables( - lower=extract_data(self._on_hours_total_min), - upper=extract_data(self._on_hours_total_max), + lower=self._on_hours_total_min, + upper=self._on_hours_total_max, coords=self._model.get_coords(time_dim=False), name=f'{self.label_full}|on_hours_total', ), @@ -437,7 +437,7 @@ def do_modeling(self): # Create count variable for number of switches self.switch_on_nr = self.add( self._model.add_variables( - upper=extract_data(self._switch_on_max), + upper=self._switch_on_max, lower=0, name=f'{self.label_full}|switch_on_nr', ), @@ -526,7 +526,7 @@ def do_modeling(self): self.duration = self.add( self._model.add_variables( lower=0, - upper=extract_data(self._maximum_duration, mega), + upper=self._maximum_duration if self._maximum_duration is not None else mega, coords=self._model.get_coords(), name=f'{self.label_full}|hours', ), @@ -707,8 +707,8 @@ def do_modeling(self): defining_bounds=self._defining_bounds, previous_values=self._previous_values, use_off=self.parameters.use_off, - on_hours_total_min=extract_data(self.parameters.on_hours_total_min), - on_hours_total_max=extract_data(self.parameters.on_hours_total_max), + on_hours_total_min=self.parameters.on_hours_total_min, + on_hours_total_max=self.parameters.on_hours_total_max, effects_per_running_hour=self.parameters.effects_per_running_hour, ) self.add(self.state_model) @@ -1001,8 +1001,8 @@ def do_modeling(self): if self._has_time_dim: self.total_per_timestep = self.add( self._model.add_variables( - lower=-np.inf if (self._min_per_hour is None) else extract_data(self._min_per_hour) * self._model.hours_per_step, - upper=np.inf if (self._max_per_hour is None) else extract_data(self._max_per_hour) * self._model.hours_per_step, + lower=-np.inf if (self._min_per_hour is None) else self._min_per_hour * self._model.hours_per_step, + upper=np.inf if (self._max_per_hour is None) else self._max_per_hour * self._model.hours_per_step, coords=self._model.get_coords(time_dim=True, scenario_dim=self._has_scenario_dim), name=f'{self.label_full}|total_per_timestep', ), diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 5734c6a1d..52371c9cb 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -16,8 +16,8 @@ from rich.pretty import Pretty from . import io as fx_io -from .core import ConversionError, DataConverter, TemporalData, TemporalDataUser, TimeSeriesData -from .effects import Effect, EffectCollection, ScalarEffects, ScalarEffectsUser, TemporalEffects, TemporalEffectsUser +from .core import ConversionError, DataConverter, TemporalData, TemporalDataUser, TimeSeriesData, NonTemporalDataUser +from .effects import Effect, EffectCollection, NonTemporalEffects, NonTemporalEffectsUser, TemporalEffects, TemporalEffectsUser from .elements import Bus, Component, Flow from .structure import Element, Interface, SystemModel @@ -49,7 +49,7 @@ def __init__( scenarios: Optional[pd.Index] = None, hours_of_last_timestep: Optional[float] = None, hours_of_previous_timesteps: Optional[Union[int, float, np.ndarray]] = None, - scenario_weights: Optional[ScenarioData] = None, + scenario_weights: Optional[NonTemporalDataUser] = None, ): """ Args: @@ -69,9 +69,8 @@ def __init__( self.hours_of_previous_timesteps = self._calculate_hours_of_previous_timesteps( timesteps, hours_of_previous_timesteps ) - self.scenario_weights = self.create_time_series( - 'scenario_weights', scenario_weights, has_time_dim=False, has_scenario_dim=True - ) + self.scenarios = scenarios + self.scenario_weights = self.fit_to_model_coords('scenario_weights', scenario_weights, has_time_dim=False) # Element collections self.components: Dict[str, Component] = {} @@ -279,6 +278,7 @@ def fit_to_model_coords( self, name: str, data: Optional[TemporalDataUser], + has_time_dim: bool = True, ) -> Optional[TemporalData]: """ Fit data to model coordinate system (currently time, but extensible). @@ -297,21 +297,22 @@ def fit_to_model_coords( try: data.name = name # Set name of previous object! return TimeSeriesData( - DataConverter.to_dataarray(data, timesteps=self.timesteps), + DataConverter.to_dataarray(data, timesteps=self.timesteps, scenarios=self.scenarios), aggregation_group=data.aggregation_group, aggregation_weight=data.aggregation_weight ).rename(name) except ConversionError as e: logger.critical(f'Could not convert time series data "{name}" to DataArray: {e}. \n' f'Take care to use the correct (time) index.') else: - return DataConverter.to_dataarray(data, timesteps=self.timesteps).rename(name) + return DataConverter.to_dataarray(data, timesteps=self.timesteps if has_time_dim else None, scenarios=self.scenarios).rename(name) def fit_effects_to_model_coords( self, label_prefix: Optional[str], effect_values: Optional[TemporalEffectsUser], label_suffix: Optional[str] = None, - ) -> Optional[TemporalEffects]: + has_time_dim: bool = True, + ) -> Optional[Union[TemporalEffects, NonTemporalEffects]]: """ Transform EffectValues from the user to Internal Datatypes aligned with model coordinates. """ @@ -321,14 +322,14 @@ def fit_effects_to_model_coords( effect_values_dict = self.effects.create_effect_values_dict(effect_values) return { - effect: self.fit_to_model_coords('|'.join(filter(None, [label_prefix, effect, label_suffix])), value) + effect: self.fit_to_model_coords('|'.join(filter(None, [label_prefix, effect, label_suffix])), value, has_time_dim=has_time_dim) for effect, value in effect_values_dict.items() } def connect_and_transform(self): """Transform data for all elements using the new simplified approach.""" self.scenario_weights = self.fit_to_model_coords( - 'scenario_weights', self.scenario_weights, has_time_dim=False, has_scenario_dim=True + 'scenario_weights', self.scenario_weights, has_time_dim=False ) if not self._connected_and_transformed: self._connect_network() diff --git a/flixopt/interface.py b/flixopt/interface.py index a8f209c8f..eb84a45e3 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, Dict, Iterator, List, Literal, Optional, Union from .config import CONFIG -from .core import Scalar, TemporalDataUser +from .core import Scalar, TemporalDataUser, NonTemporalDataUser from .structure import Interface, register_class_for_io if TYPE_CHECKING: # for type checking and preventing circular imports @@ -33,8 +33,8 @@ def __init__(self, start: TemporalDataUser, end: TemporalDataUser): self.has_time_dim = False def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): - self.start = flow_system.fit_to_model_coords(f'{name_prefix}|start', self.start, has_time_dim=self.has_time_dim, has_scenario_dim=True) - self.end = flow_system.fit_to_model_coords(f'{name_prefix}|end', self.end, has_time_dim=self.has_time_dim, has_scenario_dim=True) + self.start = flow_system.fit_to_model_coords(f'{name_prefix}|start', self.start, has_time_dim=self.has_time_dim) + self.end = flow_system.fit_to_model_coords(f'{name_prefix}|end', self.end, has_time_dim=self.has_time_dim) @register_class_for_io @@ -146,9 +146,9 @@ class InvestParameters(Interface): def __init__( self, - fixed_size: Optional[ScenarioData] = None, - minimum_size: Optional[ScenarioData] = None, - maximum_size: Optional[ScenarioData] = None, + fixed_size: Optional[NonTemporalDataUser] = None, + minimum_size: Optional[NonTemporalDataUser] = None, + maximum_size: Optional[NonTemporalDataUser] = None, optional: bool = True, # Investition ist weglassbar fix_effects: Optional['EffectValuesUserScenario'] = None, specific_effects: Optional['EffectValuesUserScenario'] = None, # costs per Flow-Unit/Storage-Size/... @@ -185,40 +185,37 @@ def __init__( def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): self._plausibility_checks(flow_system) - self.fix_effects = flow_system.create_effect_time_series( + self.fix_effects = flow_system.fit_effects_to_model_coords( label_prefix=name_prefix, effect_values=self.fix_effects, label_suffix='fix_effects', has_time_dim=False, - has_scenario_dim=True, ) - self.divest_effects = flow_system.create_effect_time_series( + self.divest_effects = flow_system.fit_effects_to_model_coords( label_prefix=name_prefix, effect_values=self.divest_effects, label_suffix='divest_effects', has_time_dim=False, - has_scenario_dim=True, ) - self.specific_effects = flow_system.create_effect_time_series( + self.specific_effects = flow_system.fit_effects_to_model_coords( label_prefix=name_prefix, effect_values=self.specific_effects, label_suffix='specific_effects', has_time_dim=False, - has_scenario_dim=True, ) if self.piecewise_effects is not None: self.piecewise_effects.has_time_dim = False self.piecewise_effects.transform_data(flow_system, f'{name_prefix}|PiecewiseEffects') - self._minimum_size = flow_system.create_time_series( - f'{name_prefix}|minimum_size', self.minimum_size, has_time_dim=False, has_scenario_dim=True + self._minimum_size = flow_system.fit_to_model_coords( + f'{name_prefix}|minimum_size', self.minimum_size, has_time_dim=False ) - self._maximum_size = flow_system.create_time_series( - f'{name_prefix}|maximum_size', self.maximum_size, has_time_dim=False, has_scenario_dim=True + self._maximum_size = flow_system.fit_to_model_coords( + f'{name_prefix}|maximum_size', self.maximum_size, has_time_dim=False ) if self.fixed_size is not None: - self.fixed_size = flow_system.create_time_series( - f'{name_prefix}|fixed_size', self.fixed_size, has_time_dim=False, has_scenario_dim=True + self.fixed_size = flow_system.fit_to_model_coords( + f'{name_prefix}|fixed_size', self.fixed_size, has_time_dim=False ) def _plausibility_checks(self, flow_system): @@ -310,13 +307,13 @@ def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): self.consecutive_off_hours_max = flow_system.fit_to_model_coords( f'{name_prefix}|consecutive_off_hours_max', self.consecutive_off_hours_max ) - self.on_hours_total_max = flow_system.create_time_series( + self.on_hours_total_max = flow_system.fit_to_model_coords( f'{name_prefix}|on_hours_total_max', self.on_hours_total_max, has_time_dim=False ) - self.on_hours_total_min = flow_system.create_time_series( + self.on_hours_total_min = flow_system.fit_to_model_coords( f'{name_prefix}|on_hours_total_min', self.on_hours_total_min, has_time_dim=False ) - self.switch_on_total_max = flow_system.create_time_series( + self.switch_on_total_max = flow_system.fit_to_model_coords( f'{name_prefix}|switch_on_total_max', self.switch_on_total_max, has_time_dim=False ) diff --git a/flixopt/structure.py b/flixopt/structure.py index 9fc12bcd6..d02a8d4e9 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -7,7 +7,6 @@ import json import logging import pathlib -from datetime import datetime from io import StringIO from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Tuple, Union @@ -20,7 +19,7 @@ from . import io as fx_io from .config import CONFIG -from .core import Scalar, TemporalDataUser, TimeSeriesData, get_dataarray_stats +from .core import Scalar, TemporalDataUser, TimeSeriesData, get_dataarray_stats, NonTemporalData if TYPE_CHECKING: # for type checking and preventing circular imports from .effects import EffectCollectionModel @@ -70,21 +69,19 @@ def do_modeling(self): for bus_model in bus_models: # Buses after Components, because FlowModels are created in ComponentModels bus_model.do_modeling() - def _calculate_scenario_weights(self, weights: Optional[TimeSeries] = None) -> xr.DataArray: + def _calculate_scenario_weights(self, weights: Optional[NonTemporalData] = None) -> xr.DataArray: """Calculates the weights of the scenarios. If None, all scenarios have the same weight. All weights are normalized to 1. If no scenarios are present, s single weight of 1 is returned. """ - if weights is not None and not isinstance(weights, TimeSeries): - raise TypeError(f'Weights must be a TimeSeries or None, got {type(weights)}') - if self.time_series_collection.scenarios is None: + if weights is not None and not isinstance(weights, xr.DataArray): + raise TypeError(f'Weights must be a xr.DataArray or None, got {type(weights)}') + if self.flow_system.scenarios is None: return xr.DataArray(1) if weights is None: weights = xr.DataArray( - np.ones(len(self.time_series_collection.scenarios)), - coords={'scenario': self.time_series_collection.scenarios} + np.ones(len(self.flow_system.scenarios)), + coords={'scenario': self.flow_system.scenarios} ) - elif isinstance(weights, TimeSeries): - weights = weights.selected_data return weights / weights.sum() @@ -137,7 +134,7 @@ def get_coords( """ if not scenario_dim and not time_dim: return None - scenarios = self.time_series_collection.scenarios + scenarios = self.flow_system.scenarios timesteps = ( self.flow_system.timesteps if not extra_timestep else self.flow_system.timesteps_extra ) diff --git a/tests/conftest.py b/tests/conftest.py index 198b9a92b..ae2cb8c25 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -16,7 +16,7 @@ from flixopt.structure import SystemModel -@pytest.fixture() +@pytest.fixture( def highs_solver(): return fx.solvers.HighsSolver(mip_gap=0, time_limit_seconds=300) From 483ba12c9099113b17000042e9f6f886992d520c Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 28 Jun 2025 21:40:06 +0200 Subject: [PATCH 125/448] Merge --- flixopt/components.py | 16 ++++++++-------- flixopt/results.py | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/flixopt/components.py b/flixopt/components.py index c2787ef9a..5b902b65b 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -257,20 +257,20 @@ def _plausibility_checks(self) -> None: maximum_capacity = self.capacity_in_flow_hours minimum_capacity = self.capacity_in_flow_hours - # initial capacity >= allowed min for maximum_size: - minimum_inital_capacity = maximum_capacity * self.relative_minimum_charge_state.isel(time=0) - # initial capacity <= allowed max for minimum_size: - maximum_inital_capacity = minimum_capacity * self.relative_maximum_charge_state.isel(time=0) + # initial capacity >= allowed min for maximum_size: + minimum_initial_capacity = maximum_capacity * self.relative_minimum_charge_state.isel(time=0) + # initial capacity <= allowed max for minimum_size: + maximum_initial_capacity = minimum_capacity * self.relative_maximum_charge_state.isel(time=0) - if (self.initial_charge_state > maximum_inital_capacity).any(): + if (self.initial_charge_state > maximum_initial_capacity).any(): raise ValueError( f'{self.label_full}: {self.initial_charge_state=} ' - f'is above allowed maximum charge_state {maximum_inital_capacity}' + f'is above allowed maximum charge_state {maximum_initial_capacity}' ) - if (self.initial_charge_state < minimum_inital_capacity).any(): + if (self.initial_charge_state < minimum_initial_capacity).any(): raise ValueError( f'{self.label_full}: {self.initial_charge_state=} ' - f'is below allowed minimum charge_state {minimum_inital_capacity}' + f'is below allowed minimum charge_state {minimum_initial_capacity}' ) if self.balanced: diff --git a/flixopt/results.py b/flixopt/results.py index 3eba91c8a..7a6f7926e 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -124,7 +124,7 @@ def from_calculation(cls, calculation: 'Calculation'): """ return cls( solution=calculation.model.solution, - flow_system=calculation.flow_system.to_dataset(), + flow_system_data=calculation.flow_system.to_dataset(), summary=calculation.summary, model=calculation.model, name=calculation.name, From dee1de4ebb0418f30cfcc54ba634f21c31fba0c6 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 28 Jun 2025 21:40:48 +0200 Subject: [PATCH 126/448] Fit scenario weights to model coords when transforming --- flixopt/flow_system.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 52371c9cb..dea0a40e6 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -70,7 +70,7 @@ def __init__( timesteps, hours_of_previous_timesteps ) self.scenarios = scenarios - self.scenario_weights = self.fit_to_model_coords('scenario_weights', scenario_weights, has_time_dim=False) + self.scenario_weights = scenario_weights # Element collections self.components: Dict[str, Component] = {} From 4ec39149705b5fb60886db33901bd47f05ba9ea9 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 28 Jun 2025 21:40:57 +0200 Subject: [PATCH 127/448] Merge --- flixopt/features.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flixopt/features.py b/flixopt/features.py index 3a2744dee..e9d936b74 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -66,7 +66,7 @@ def do_modeling(self): self._create_bounds_for_optional_investment() - if self._model.time_series_collection.scenarios is not None: + if self._model.flow_system.scenarios is not None: self._create_bounds_for_scenarios() # Bounds for defining variable From 2554d8a79f38eef6b1c89983137453f6a51eb3b8 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 28 Jun 2025 21:41:32 +0200 Subject: [PATCH 128/448] Removing logic between minimum, maximum and fixed size from InvestParameters --- flixopt/interface.py | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/flixopt/interface.py b/flixopt/interface.py index eb84a45e3..f8bf0b1bf 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -179,8 +179,8 @@ def __init__( self.optional = optional self.specific_effects: EffectValuesUserScenario = specific_effects if specific_effects is not None else {} self.piecewise_effects = piecewise_effects - self._minimum_size = minimum_size if minimum_size is not None else CONFIG.modeling.EPSILON - self._maximum_size = maximum_size if maximum_size is not None else CONFIG.modeling.BIG # default maximum + self.minimum_size = minimum_size if minimum_size is not None else CONFIG.modeling.EPSILON + self.maximum_size = maximum_size if maximum_size is not None else CONFIG.modeling.BIG # default maximum self.investment_scenarios = investment_scenarios def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): @@ -207,10 +207,10 @@ def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): self.piecewise_effects.has_time_dim = False self.piecewise_effects.transform_data(flow_system, f'{name_prefix}|PiecewiseEffects') - self._minimum_size = flow_system.fit_to_model_coords( + self.minimum_size = flow_system.fit_to_model_coords( f'{name_prefix}|minimum_size', self.minimum_size, has_time_dim=False ) - self._maximum_size = flow_system.fit_to_model_coords( + self.maximum_size = flow_system.fit_to_model_coords( f'{name_prefix}|maximum_size', self.maximum_size, has_time_dim=False ) if self.fixed_size is not None: @@ -220,10 +220,10 @@ def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): def _plausibility_checks(self, flow_system): if isinstance(self.investment_scenarios, list): - if not set(self.investment_scenarios).issubset(flow_system.time_series_collection.scenarios): + if not set(self.investment_scenarios).issubset(flow_system.scenarios): raise ValueError( f'Some scenarios in investment_scenarios are not present in the time_series_collection: ' - f'{set(self.investment_scenarios) - set(flow_system.time_series_collection.scenarios)}' + f'{set(self.investment_scenarios) - set(flow_system.scenarios)}' ) if self.investment_scenarios is not None: if not self.optional: @@ -233,14 +233,6 @@ def _plausibility_checks(self, flow_system): 'Otherwise the investment cannot be 0 incertain scenarios while being non-zero in others.' ) - @property - def minimum_size(self): - return self.fixed_size if self.fixed_size is not None else self._minimum_size - - @property - def maximum_size(self): - return self.fixed_size if self.fixed_size is not None else self._maximum_size - @register_class_for_io class OnOffParameters(Interface): From d062727476015049e0b36b274316acf7312c8062 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 28 Jun 2025 21:45:11 +0200 Subject: [PATCH 129/448] Remove selected_timesteps --- flixopt/calculation.py | 28 +++++++--------------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/flixopt/calculation.py b/flixopt/calculation.py index 5796cd70e..88e509438 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -46,30 +46,19 @@ def __init__( self, name: str, flow_system: FlowSystem, - selected_timesteps: Annotated[ + active_timesteps: Annotated[ Optional[pd.DatetimeIndex], 'DEPRECATED: Use flow_system.sel(time=...) or flow_system.isel(time=...) instead', ] = None, - selected_scenarios: Optional[pd.Index] = None, folder: Optional[pathlib.Path] = None, - active_timesteps: Optional[pd.DatetimeIndex] = None, ): """ Args: name: name of calculation flow_system: flow_system which should be calculated - selected_timesteps: timesteps which should be used for calculation. If None, then all timesteps are used. - selected_scenarios: scenarios which should be used for calculation. If None, then all scenarios are used. folder: folder where results should be saved. If None, then the current working directory is used. - active_timesteps: Deprecated. Use selected_timesteps instead. + active_timesteps: Deprecated. Use FLowSystem.sel(time=...) or FlowSystem.isel(time=...) instead. """ - if active_timesteps is not None: - warnings.warn( - 'active_timesteps is deprecated. Use selected_timesteps instead.', - DeprecationWarning, - stacklevel=2, - ) - selected_timesteps = active_timesteps self.name = name if flow_system.used_in_calculation: logging.warning(f'FlowSystem {flow_system} is already used in a calculation. ' @@ -85,14 +74,12 @@ def __init__( stacklevel=2, ) flow_system = flow_system.sel(time=active_timesteps) + self._active_timesteps = active_timesteps # deprecated flow_system._used_in_calculation = True self.flow_system = flow_system self.model: Optional[SystemModel] = None - self.selected_timesteps = selected_timesteps - self.selected_scenarios = selected_scenarios - self._active_timesteps = active_timesteps # deprecated self.durations = {'modeling': 0.0, 'solving': 0.0, 'saving': 0.0} self.folder = pathlib.Path.cwd() / 'results' if folder is None else pathlib.Path(folder) @@ -169,11 +156,11 @@ def summary(self): @property def active_timesteps(self) -> pd.DatetimeIndex: warnings.warn( - 'active_timesteps is deprecated. Use selected_timesteps instead.', + 'active_timesteps is deprecated. Use active_timesteps instead.', DeprecationWarning, stacklevel=2, ) - return self.selected_timesteps + return self._active_timesteps class FullCalculation(Calculation): @@ -240,7 +227,6 @@ def __init__( flow_system: FlowSystem, aggregation_parameters: AggregationParameters, components_to_clusterize: Optional[List[Component]] = None, - selected_timesteps: Optional[pd.DatetimeIndex] = None, active_timesteps: Annotated[ Optional[pd.DatetimeIndex], 'DEPRECATED: Use flow_system.sel(time=...) or flow_system.isel(time=...) instead', @@ -258,13 +244,13 @@ def __init__( components_to_clusterize: List of Components to perform aggregation on. If None, then all components are aggregated. This means, teh variables in the components are equalized to each other, according to the typical periods computed in the DataAggregation - selected_timesteps: pd.DatetimeIndex or None + active_timesteps: pd.DatetimeIndex or None list with indices, which should be used for calculation. If None, then all timesteps are used. folder: folder where results should be saved. If None, then the current working directory is used. """ if flow_system.scenarios is not None: raise ValueError('Aggregation is not supported for scenarios yet. Please use FullCalculation instead.') - super().__init__(name, flow_system, selected_timesteps, folder=folder, active_timesteps=active_timesteps) + super().__init__(name, flow_system, folder=folder, active_timesteps=active_timesteps) self.aggregation_parameters = aggregation_parameters self.components_to_clusterize = components_to_clusterize self.aggregation = None From 6dc23f587f4ff7517ffb0c551f0270214cd85190 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 28 Jun 2025 21:49:01 +0200 Subject: [PATCH 130/448] Improve TypeHints --- flixopt/core.py | 6 +++--- flixopt/flow_system.py | 9 +++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/flixopt/core.py b/flixopt/core.py index 9c9d6f891..648344098 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -22,13 +22,13 @@ """User data which might have a time dimension. Internally converted to an xr.DataArray with time dimension.""" TemporalData = Union[xr.DataArray, 'TimeSeriesData'] -"""Internally used datatypes for temporal data.""" +"""Internally used datatypes for temporal data (data with a time dimension).""" NonTemporalDataUser = Union[int, float, np.integer, np.floating, np.ndarray, pd.Series, pd.DataFrame, xr.DataArray] -"""User data which has no time dimension. Internally converted to an xr.DataArray without a time dimension.""" +"""User data which has no time dimension. Internally converted to a Scalar or an xr.DataArray without a time dimension.""" NonTemporalData = Union[Scalar, xr.DataArray] -"""Internally used datatypes for non-temporal data.""" +"""Internally used datatypes for non-temporal data. Can be a Scalar or an xr.DataArray.""" class PlausibilityError(Exception): diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index dea0a40e6..7849cdcea 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -16,7 +16,7 @@ from rich.pretty import Pretty from . import io as fx_io -from .core import ConversionError, DataConverter, TemporalData, TemporalDataUser, TimeSeriesData, NonTemporalDataUser +from .core import ConversionError, DataConverter, TemporalData, TemporalDataUser, TimeSeriesData, NonTemporalDataUser, NonTemporalData from .effects import Effect, EffectCollection, NonTemporalEffects, NonTemporalEffectsUser, TemporalEffects, TemporalEffectsUser from .elements import Bus, Component, Flow from .structure import Element, Interface, SystemModel @@ -277,15 +277,16 @@ def to_json(self, path: Union[str, pathlib.Path]): def fit_to_model_coords( self, name: str, - data: Optional[TemporalDataUser], + data: Optional[Union[TemporalDataUser, NonTemporalDataUser]], has_time_dim: bool = True, - ) -> Optional[TemporalData]: + ) -> Optional[Union[TemporalData, NonTemporalData]]: """ Fit data to model coordinate system (currently time, but extensible). Args: name: Name of the data data: Data to fit to model coordinates + has_time_dim: Whether the data has a time dimension Returns: xr.DataArray aligned to model coordinate system @@ -309,7 +310,7 @@ def fit_to_model_coords( def fit_effects_to_model_coords( self, label_prefix: Optional[str], - effect_values: Optional[TemporalEffectsUser], + effect_values: Optional[Union[TemporalEffectsUser, NonTemporalEffectsUser]], label_suffix: Optional[str] = None, has_time_dim: bool = True, ) -> Optional[Union[TemporalEffects, NonTemporalEffects]]: From e6100d66d64533bddc29de98e84b2a7650807c68 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 28 Jun 2025 22:05:42 +0200 Subject: [PATCH 131/448] New property on InvestParameters for min/max/fixed size --- flixopt/elements.py | 6 +++--- flixopt/features.py | 8 ++++---- flixopt/interface.py | 10 +++++++++- tests/conftest.py | 2 +- 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/flixopt/elements.py b/flixopt/elements.py index 9d0f91885..721c8d50d 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -443,7 +443,7 @@ def flow_rate_bounds_on(self) -> Tuple[TemporalData, TemporalData]: if size.fixed_size is not None: return relative_minimum * size.fixed_size, relative_maximum * size.fixed_size - return relative_minimum * size.minimum_size, relative_maximum * size.maximum_size + return relative_minimum * size.minimum_or_fixed_size, relative_maximum * size.maximum_or_fixed_size @property def flow_rate_lower_bound_relative(self) -> TemporalData: @@ -472,7 +472,7 @@ def flow_rate_lower_bound(self) -> TemporalData: if isinstance(self.element.size, InvestParameters): if self.element.size.optional: return 0 - return self.flow_rate_lower_bound_relative * self.element.size.minimum_size + return self.flow_rate_lower_bound_relative * self.element.size.minimum_or_fixed_size return self.flow_rate_lower_bound_relative * self.element.size @property @@ -482,7 +482,7 @@ def flow_rate_upper_bound(self) -> TemporalData: Further constraining might be done in OnOffModel and InvestmentModel """ if isinstance(self.element.size, InvestParameters): - return self.flow_rate_upper_bound_relative * self.element.size.maximum_size + return self.flow_rate_upper_bound_relative * self.element.size.maximum_or_fixed_size return self.flow_rate_upper_bound_relative * self.element.size diff --git a/flixopt/features.py b/flixopt/features.py index e9d936b74..5e0ed0e80 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -10,7 +10,7 @@ import numpy as np from .config import CONFIG -from .core import Scalar, TemporalData +from .core import Scalar, TemporalData, NonTemporalData from .interface import InvestParameters, OnOffParameters, Piecewise from .structure import Model, SystemModel @@ -45,8 +45,8 @@ def __init__( def do_modeling(self): self.size = self.add( self._model.add_variables( - lower=0 if self.parameters.optional else self.parameters.minimum_size, - upper=self.parameters.maximum_size, + lower=0 if self.parameters.optional else self.parameters.minimum_or_fixed_size, + upper=self.parameters.maximum_or_fixed_size, name=f'{self.label_full}|size', coords=self._model.get_coords(time_dim=False), ), @@ -138,7 +138,7 @@ def _create_bounds_for_optional_investment(self): # eq2: P_invest >= isInvested * max(epsilon, investSize_min) self.add( self._model.add_constraints( - self.size >= self.is_invested * np.maximum(CONFIG.modeling.EPSILON, self.parameters.minimum_size), + self.size >= self.is_invested * np.maximum(CONFIG.modeling.EPSILON, self.parameters.minimum_or_fixed_size), name=f'{self.label_full}|is_invested_lb', ), 'is_invested_lb', diff --git a/flixopt/interface.py b/flixopt/interface.py index f8bf0b1bf..81f351ebe 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, Dict, Iterator, List, Literal, Optional, Union from .config import CONFIG -from .core import Scalar, TemporalDataUser, NonTemporalDataUser +from .core import Scalar, TemporalDataUser, NonTemporalDataUser, NonTemporalData from .structure import Interface, register_class_for_io if TYPE_CHECKING: # for type checking and preventing circular imports @@ -233,6 +233,14 @@ def _plausibility_checks(self, flow_system): 'Otherwise the investment cannot be 0 incertain scenarios while being non-zero in others.' ) + @property + def minimum_or_fixed_size(self) -> NonTemporalData: + return self.fixed_size if self.fixed_size is not None else self.minimum_size + + @property + def maximum_or_fixed_size(self) -> NonTemporalData: + return self.fixed_size if self.fixed_size is not None else self.maximum_size + @register_class_for_io class OnOffParameters(Interface): diff --git a/tests/conftest.py b/tests/conftest.py index ae2cb8c25..198b9a92b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -16,7 +16,7 @@ from flixopt.structure import SystemModel -@pytest.fixture( +@pytest.fixture() def highs_solver(): return fx.solvers.HighsSolver(mip_gap=0, time_limit_seconds=300) From 46f20358e61236317fe95631a95c20bfee0b1c86 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 28 Jun 2025 22:29:06 +0200 Subject: [PATCH 132/448] Move logic for InvestParameters in Transmission to from Model to Interface --- flixopt/components.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/flixopt/components.py b/flixopt/components.py index 5b902b65b..2f566963b 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -358,7 +358,17 @@ def _plausibility_checks(self): if flow is not None and isinstance(flow.size, InvestParameters): raise ValueError( 'Transmission currently does not support separate InvestParameters for Flows. ' - 'Please use Flow in1. The size of in2 is equal to in1. THis is handled internally' + 'Please use Flow in1. The size of in2 is equal to in1. This is handled internally' + ) + + # Make sure either None or both in Flows have InvestParameters + if self.in2 is not None: + if isinstance(self.in1.size, InvestParameters) and not isinstance( + self.in2.size, InvestParameters + ): + array_name = self.in1.size.maximum_size.name.replace(self.in1, self.in2) + self.in2.size = InvestParameters( + maximum_size=self.in1.size.maximum_size.rename(array_name) ) def create_model(self, model) -> 'TransmissionModel': @@ -390,13 +400,6 @@ def do_modeling(self): if flow.on_off_parameters is None: flow.on_off_parameters = OnOffParameters() - # Make sure either None or both in Flows have InvestParameters - if self.element.in2 is not None: - if isinstance(self.element.in1.size, InvestParameters) and not isinstance( - self.element.in2.size, InvestParameters - ): - self.element.in2.size = InvestParameters(maximum_size=self.element.in1.size.maximum_size) - super().do_modeling() # first direction From 6baeb8e5c0837bcea51add5914594a68347b3a98 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 28 Jun 2025 22:29:46 +0200 Subject: [PATCH 133/448] Make transformation of data more hierarchical (Flows after Components) --- flixopt/elements.py | 3 +++ flixopt/flow_system.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/flixopt/elements.py b/flixopt/elements.py index 721c8d50d..9df367eec 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -72,6 +72,9 @@ def transform_data(self, flow_system: 'FlowSystem') -> None: if self.on_off_parameters is not None: self.on_off_parameters.transform_data(flow_system, self.label_full) + for flow in self.inputs + self.outputs: + flow.transform_data(flow_system) + def _check_unique_flow_labels(self): all_flow_labels = [flow.label for flow in self.inputs + self.outputs] diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 7849cdcea..200e8eb60 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -334,7 +334,7 @@ def connect_and_transform(self): ) if not self._connected_and_transformed: self._connect_network() - for element in self.all_elements.values(): + for element in list(self.components.values()) + list(self.effects.effects.values()) + list(self.buses.values()): element.transform_data(self) self._connected_and_transformed = True From aeaaa8356e60cabfccd06b27725c60660f6845e3 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 28 Jun 2025 22:47:23 +0200 Subject: [PATCH 134/448] Add scenario validation --- flixopt/flow_system.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 200e8eb60..b513372bf 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -69,7 +69,7 @@ def __init__( self.hours_of_previous_timesteps = self._calculate_hours_of_previous_timesteps( timesteps, hours_of_previous_timesteps ) - self.scenarios = scenarios + self.scenarios = None if scenarios is None else self._validate_scenarios(scenarios) self.scenario_weights = scenario_weights # Element collections @@ -94,6 +94,22 @@ def _validate_timesteps(timesteps: pd.DatetimeIndex) -> pd.DatetimeIndex: raise ValueError('timesteps must be sorted') return timesteps + @staticmethod + def _validate_scenarios(scenarios: pd.Index) -> pd.Index: + """ + Validate and prepare scenario index. + + Args: + scenarios: The scenario index to validate + """ + if not isinstance(scenarios, pd.Index) or len(scenarios) == 0: + raise ConversionError('Scenarios must be a non-empty Index') + + if not scenarios.name == 'scenario': + raise ConversionError(f'Scenarios must be named "scenario", got "{scenarios.name}"') + + return scenarios + @staticmethod def _create_timesteps_with_extra( timesteps: pd.DatetimeIndex, hours_of_last_timestep: Optional[float] From 15fd1241f2dc0fb93880b077b5272ce4eb837571 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 28 Jun 2025 22:57:10 +0200 Subject: [PATCH 135/448] Change Transmission to have a "balanced" attribute. Change Tests accordingly --- flixopt/components.py | 32 +++---- tests/test_component.py | 175 +++++++++++++++++++++++++++++++++++++- tests/test_integration.py | 104 +--------------------- 3 files changed, 191 insertions(+), 120 deletions(-) diff --git a/flixopt/components.py b/flixopt/components.py index 2f566963b..cf05af0ed 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -305,6 +305,7 @@ def __init__( absolute_losses: Optional[TemporalDataUser] = None, on_off_parameters: OnOffParameters = None, prevent_simultaneous_flows_in_both_directions: bool = True, + balanced: bool = False, meta_data: Optional[Dict] = None, ): """ @@ -322,6 +323,7 @@ def __init__( absolute_losses: The absolute loss, occur only when the Flow is on. Induces the creation of the ON-Variable on_off_parameters: Parameters defining the on/off behavior of the component. prevent_simultaneous_flows_in_both_directions: If True, inflow and outflow are not allowed to be both non-zero at same timestep. + balanced: Wether to equate the size of the in1 and in2 Flow. Needs InvestParameters in both Flows. meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types. """ super().__init__( @@ -341,6 +343,7 @@ def __init__( self.relative_losses = relative_losses self.absolute_losses = absolute_losses + self.balanced = balanced def _plausibility_checks(self): super()._plausibility_checks() @@ -353,23 +356,20 @@ def _plausibility_checks(self): assert self.out2.bus == self.in1.bus, ( f'Input 1 and Output 2 do not start/end at the same Bus: {self.in1.bus=}, {self.out2.bus=}' ) - # Check Investments - for flow in [self.out1, self.in2, self.out2]: - if flow is not None and isinstance(flow.size, InvestParameters): - raise ValueError( - 'Transmission currently does not support separate InvestParameters for Flows. ' - 'Please use Flow in1. The size of in2 is equal to in1. This is handled internally' - ) - # Make sure either None or both in Flows have InvestParameters - if self.in2 is not None: - if isinstance(self.in1.size, InvestParameters) and not isinstance( - self.in2.size, InvestParameters + if self.balanced: + if self.in2 is None: + raise ValueError('Balanced Transmission needs InvestParameters in both in-Flows') + if not isinstance(self.in1.size, InvestParameters) or not isinstance(self.in2.size, InvestParameters): + raise ValueError('Balanced Transmission needs InvestParameters in both in-Flows') + if ( + (self.in1.size.minimum_or_fixed_size > self.in2.size.maximum_or_fixed_size).any() or + (self.in1.size.maximum_or_fixed_size < self.in2.size.minimum_or_fixed_size).any() ): - array_name = self.in1.size.maximum_size.name.replace(self.in1, self.in2) - self.in2.size = InvestParameters( - maximum_size=self.in1.size.maximum_size.rename(array_name) - ) + raise ValueError( + f'Balanced Transmission needs compatible minimum and maximum sizes.' + f'Got: {self.in1.size.minimum_size=}, {self.in1.size.maximum_size=}, {self.in1.size.fixed_size=} and ' + f'{self.in2.size.minimum_size=}, {self.in2.size.maximum_size=}, {self.in2.size.fixed_size=}.') def create_model(self, model) -> 'TransmissionModel': self._plausibility_checks() @@ -410,7 +410,7 @@ def do_modeling(self): self.create_transmission_equation('dir2', self.element.in2, self.element.out2) # equate size of both directions - if isinstance(self.element.in1.size, InvestParameters) and self.element.in2 is not None: + if self.element.balanced: # eq: in1.size = in2.size self.add( self._model.add_constraints( diff --git a/tests/test_component.py b/tests/test_component.py index 18ceb717a..8a99b5d5b 100644 --- a/tests/test_component.py +++ b/tests/test_component.py @@ -6,7 +6,7 @@ import flixopt as fx import flixopt.elements -from .conftest import assert_conequal, assert_var_equal, create_linopy_model +from .conftest import assert_conequal, assert_var_equal, create_linopy_model, create_calculation_and_solve, assert_almost_equal_numeric class TestComponentModel: @@ -182,3 +182,176 @@ def test_on_with_single_flow(self, basic_flow_system_linopy): model.variables['TestComponent|on'] * 100 >= model.variables['TestComponent(In1)|flow_rate'], ) + +class TestTransmissionModel: + def test_transmission_basic(self, basic_flow_system, highs_solver): + """Test basic transmission functionality""" + flow_system = basic_flow_system + flow_system.add_elements(fx.Bus('Wärme lokal')) + + boiler = fx.linear_converters.Boiler( + 'Boiler', eta=0.5, Q_th=fx.Flow('Q_th', bus='Wärme lokal'), Q_fu=fx.Flow('Q_fu', bus='Gas') + ) + + transmission = fx.Transmission( + 'Rohr', + relative_losses=0.2, + absolute_losses=20, + in1=fx.Flow('Rohr1', 'Wärme lokal', size=fx.InvestParameters(specific_effects=5, maximum_size=1e6)), + out1=fx.Flow('Rohr2', 'Fernwärme', size=1000), + ) + + flow_system.add_elements(transmission, boiler) + + _ = create_calculation_and_solve(flow_system, highs_solver, 'test_transmission_basic') + + # Assertions + assert_almost_equal_numeric( + transmission.in1.model.on_off.on.solution.values, + np.array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1]), + 'On does not work properly', + ) + + assert_almost_equal_numeric( + transmission.in1.model.flow_rate.solution.values * 0.8 - 20, + transmission.out1.model.flow_rate.solution.values, + 'Losses are not computed correctly', + ) + + def test_transmission_balanced(self, basic_flow_system, highs_solver): + """Test advanced transmission functionality""" + flow_system = basic_flow_system + flow_system.add_elements(fx.Bus('Wärme lokal')) + + boiler = fx.linear_converters.Boiler( + 'Boiler_Standard', + eta=0.9, + Q_th=fx.Flow('Q_th', bus='Fernwärme', relative_maximum=np.array([0, 0, 0, 1, 1, 1, 1, 1, 1, 1])), + Q_fu=fx.Flow('Q_fu', bus='Gas'), + ) + + boiler2 = fx.linear_converters.Boiler( + 'Boiler_backup', eta=0.4, Q_th=fx.Flow('Q_th', bus='Wärme lokal'), Q_fu=fx.Flow('Q_fu', bus='Gas') + ) + + last2 = fx.Sink( + 'Wärmelast2', + sink=fx.Flow( + 'Q_th_Last', + bus='Wärme lokal', + size=1, + fixed_relative_profile=flow_system.components['Wärmelast'].sink.fixed_relative_profile + * np.array([0, 0, 0, 0, 0, 1, 1, 1, 1, 1]), + ), + ) + + transmission = fx.Transmission( + 'Rohr', + relative_losses=0.2, + absolute_losses=20, + in1=fx.Flow('Rohr1a', bus='Wärme lokal', size=fx.InvestParameters(specific_effects=5, maximum_size=1000)), + out1=fx.Flow('Rohr1b', 'Fernwärme', size=1000), + in2=fx.Flow('Rohr2a', 'Fernwärme', size=fx.InvestParameters()), + out2=fx.Flow('Rohr2b', bus='Wärme lokal', size=1000), + balanced=True, + ) + + flow_system.add_elements(transmission, boiler, boiler2, last2) + + calculation = create_calculation_and_solve(flow_system, highs_solver, 'test_transmission_advanced') + + # Assertions + assert_almost_equal_numeric( + transmission.in1.model.on_off.on.solution.values, + np.array([1, 1, 1, 0, 0, 0, 0, 0, 0, 0]), + 'On does not work properly', + ) + + assert_almost_equal_numeric( + calculation.results.model.variables['Rohr(Rohr1b)|flow_rate'].solution.values, + transmission.out1.model.flow_rate.solution.values, + 'Flow rate of Rohr__Rohr1b is not correct', + ) + + assert_almost_equal_numeric( + transmission.in1.model.flow_rate.solution.values * 0.8 + - np.array([20 if val > 0.1 else 0 for val in transmission.in1.model.flow_rate.solution.values]), + transmission.out1.model.flow_rate.solution.values, + 'Losses are not computed correctly', + ) + + assert_almost_equal_numeric( + transmission.in1.model._investment.size.solution.item(), + transmission.in2.model._investment.size.solution.item(), + 'The Investments are not equated correctly', + ) + + def test_transmission_unbalanced(self, basic_flow_system, highs_solver): + """Test advanced transmission functionality""" + flow_system = basic_flow_system + flow_system.add_elements(fx.Bus('Wärme lokal')) + + boiler = fx.linear_converters.Boiler( + 'Boiler_Standard', + eta=0.9, + Q_th=fx.Flow('Q_th', bus='Fernwärme', relative_maximum=np.array([0, 0, 0, 1, 1, 1, 1, 1, 1, 1])), + Q_fu=fx.Flow('Q_fu', bus='Gas'), + ) + + boiler2 = fx.linear_converters.Boiler( + 'Boiler_backup', eta=0.4, Q_th=fx.Flow('Q_th', bus='Wärme lokal'), Q_fu=fx.Flow('Q_fu', bus='Gas') + ) + + last2 = fx.Sink( + 'Wärmelast2', + sink=fx.Flow( + 'Q_th_Last', + bus='Wärme lokal', + size=1, + fixed_relative_profile=flow_system.components['Wärmelast'].sink.fixed_relative_profile + * np.array([0, 0, 0, 0, 0, 1, 1, 1, 1, 1]), + ), + ) + + transmission = fx.Transmission( + 'Rohr', + relative_losses=0.2, + absolute_losses=20, + in1=fx.Flow('Rohr1a', bus='Wärme lokal', size=fx.InvestParameters(specific_effects=50, maximum_size=1000)), + out1=fx.Flow('Rohr1b', 'Fernwärme', size=1000), + in2=fx.Flow('Rohr2a', 'Fernwärme', size=fx.InvestParameters(specific_effects=100, minimum_size=10, optional=False)), + out2=fx.Flow('Rohr2b', bus='Wärme lokal', size=1000), + balanced=False, + ) + + flow_system.add_elements(transmission, boiler, boiler2, last2) + + calculation = create_calculation_and_solve(flow_system, highs_solver, 'test_transmission_advanced') + + # Assertions + assert_almost_equal_numeric( + transmission.in1.model.on_off.on.solution.values, + np.array([1, 1, 1, 0, 0, 0, 0, 0, 0, 0]), + 'On does not work properly', + ) + + assert_almost_equal_numeric( + calculation.results.model.variables['Rohr(Rohr1b)|flow_rate'].solution.values, + transmission.out1.model.flow_rate.solution.values, + 'Flow rate of Rohr__Rohr1b is not correct', + ) + + assert_almost_equal_numeric( + transmission.in1.model.flow_rate.solution.values * 0.8 + - np.array([20 if val > 0.1 else 0 for val in transmission.in1.model.flow_rate.solution.values]), + transmission.out1.model.flow_rate.solution.values, + 'Losses are not computed correctly', + ) + + assert transmission.in1.model._investment.size.solution.item() > 11 + + assert_almost_equal_numeric( + transmission.in2.model._investment.size.solution.item(), + 10, + 'Sizing does not work properly', + ) diff --git a/tests/test_integration.py b/tests/test_integration.py index da473b4e6..e3d44d764 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -73,109 +73,6 @@ def test_results_persistence(self, simple_flow_system, highs_solver): assert_almost_equal_numeric(results.solution['CO2|total'].values, 255.09184, 'CO2 doesnt match expected value') -class TestComponents: - def test_transmission_basic(self, basic_flow_system, highs_solver): - """Test basic transmission functionality""" - flow_system = basic_flow_system - flow_system.add_elements(fx.Bus('Wärme lokal')) - - boiler = fx.linear_converters.Boiler( - 'Boiler', eta=0.5, Q_th=fx.Flow('Q_th', bus='Wärme lokal'), Q_fu=fx.Flow('Q_fu', bus='Gas') - ) - - transmission = fx.Transmission( - 'Rohr', - relative_losses=0.2, - absolute_losses=20, - in1=fx.Flow('Rohr1', 'Wärme lokal', size=fx.InvestParameters(specific_effects=5, maximum_size=1e6)), - out1=fx.Flow('Rohr2', 'Fernwärme', size=1000), - ) - - flow_system.add_elements(transmission, boiler) - - _ = create_calculation_and_solve(flow_system, highs_solver, 'test_transmission_basic') - - # Assertions - assert_almost_equal_numeric( - transmission.in1.model.on_off.on.solution.values, - np.array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1]), - 'On does not work properly', - ) - - assert_almost_equal_numeric( - transmission.in1.model.flow_rate.solution.values * 0.8 - 20, - transmission.out1.model.flow_rate.solution.values, - 'Losses are not computed correctly', - ) - - def test_transmission_advanced(self, basic_flow_system, highs_solver): - """Test advanced transmission functionality""" - flow_system = basic_flow_system - flow_system.add_elements(fx.Bus('Wärme lokal')) - - boiler = fx.linear_converters.Boiler( - 'Boiler_Standard', - eta=0.9, - Q_th=fx.Flow('Q_th', bus='Fernwärme', relative_maximum=np.array([0, 0, 0, 1, 1, 1, 1, 1, 1, 1])), - Q_fu=fx.Flow('Q_fu', bus='Gas'), - ) - - boiler2 = fx.linear_converters.Boiler( - 'Boiler_backup', eta=0.4, Q_th=fx.Flow('Q_th', bus='Wärme lokal'), Q_fu=fx.Flow('Q_fu', bus='Gas') - ) - - last2 = fx.Sink( - 'Wärmelast2', - sink=fx.Flow( - 'Q_th_Last', - bus='Wärme lokal', - size=1, - fixed_relative_profile=flow_system.components['Wärmelast'].sink.fixed_relative_profile - * np.array([0, 0, 0, 0, 0, 1, 1, 1, 1, 1]), - ), - ) - - transmission = fx.Transmission( - 'Rohr', - relative_losses=0.2, - absolute_losses=20, - in1=fx.Flow('Rohr1a', bus='Wärme lokal', size=fx.InvestParameters(specific_effects=5, maximum_size=1000)), - out1=fx.Flow('Rohr1b', 'Fernwärme', size=1000), - in2=fx.Flow('Rohr2a', 'Fernwärme', size=1000), - out2=fx.Flow('Rohr2b', bus='Wärme lokal', size=1000), - ) - - flow_system.add_elements(transmission, boiler, boiler2, last2) - - calculation = create_calculation_and_solve(flow_system, highs_solver, 'test_transmission_advanced') - - # Assertions - assert_almost_equal_numeric( - transmission.in1.model.on_off.on.solution.values, - np.array([1, 1, 1, 0, 0, 0, 0, 0, 0, 0]), - 'On does not work properly', - ) - - assert_almost_equal_numeric( - calculation.results.model.variables['Rohr(Rohr1b)|flow_rate'].solution.values, - transmission.out1.model.flow_rate.solution.values, - 'Flow rate of Rohr__Rohr1b is not correct', - ) - - assert_almost_equal_numeric( - transmission.in1.model.flow_rate.solution.values * 0.8 - - np.array([20 if val > 0.1 else 0 for val in transmission.in1.model.flow_rate.solution.values]), - transmission.out1.model.flow_rate.solution.values, - 'Losses are not computed correctly', - ) - - assert_almost_equal_numeric( - transmission.in1.model._investment.size.solution.item(), - transmission.in2.model._investment.size.solution.item(), - 'The Investments are not equated correctly', - ) - - class TestComplex: def test_basic_flow_system(self, flow_system_base, highs_solver): calculation = create_calculation_and_solve(flow_system_base, highs_solver, 'test_basic_flow_system') @@ -354,6 +251,7 @@ def test_piecewise_conversion(self, flow_system_piecewise_conversion, highs_solv 'Speicher investCosts_segmented_costs doesnt match expected value', ) + @pytest.mark.slow class TestModelingTypes: @pytest.fixture(params=['full', 'segmented', 'aggregated']) From d0b231dfba896b9b57045ce0d4b3fec0fe596fac Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 28 Jun 2025 22:57:23 +0200 Subject: [PATCH 136/448] Improve index validations --- flixopt/core.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/flixopt/core.py b/flixopt/core.py index 648344098..3b320276d 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -246,7 +246,8 @@ def _validate_timesteps(timesteps: pd.DatetimeIndex) -> pd.DatetimeIndex: raise ConversionError('Timesteps must be a non-empty DatetimeIndex') if not timesteps.name == 'time': - raise ConversionError(f'Scenarios must be named "time", got "{timesteps.name}"') + logger.warning(f'Timesteps must be named "time", got "{timesteps.name}". Renaming to "time".') + timesteps = timesteps.rename('time') return timesteps @@ -262,7 +263,8 @@ def _validate_scenarios(scenarios: pd.Index) -> pd.Index: raise ConversionError('Scenarios must be a non-empty Index') if not scenarios.name == 'scenario': - raise ConversionError(f'Scenarios must be named "scenario", got "{scenarios.name}"') + logger.warning(f'Scenarios must be named "scenario", got "{scenarios.name}". Renaming to "scenario".') + scenarios = scenarios.rename('scenario') return scenarios From 4ebe6a5343f8a303a9286a31399e568b437239a2 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 29 Jun 2025 20:50:58 +0200 Subject: [PATCH 137/448] rename method in tests --- tests/test_dataconverter.py | 151 ++++++++++++++++++------------------ 1 file changed, 75 insertions(+), 76 deletions(-) diff --git a/tests/test_dataconverter.py b/tests/test_dataconverter.py index 0484d4aac..1ad41a0d2 100644 --- a/tests/test_dataconverter.py +++ b/tests/test_dataconverter.py @@ -6,7 +6,6 @@ from flixopt.core import ( # Adjust this import to match your project structure ConversionError, DataConverter, - TimeSeries, ) @@ -32,39 +31,39 @@ class TestSingleDimensionConversion: def test_scalar_conversion(self, sample_time_index): """Test converting a scalar value.""" # Test with integer - result = DataConverter.as_dataarray(42, sample_time_index) + result = DataConverter.to_dataarray(42, sample_time_index) assert isinstance(result, xr.DataArray) assert result.shape == (len(sample_time_index),) assert result.dims == ('time',) assert np.all(result.values == 42) # Test with float - result = DataConverter.as_dataarray(42.5, sample_time_index) + result = DataConverter.to_dataarray(42.5, sample_time_index) assert np.all(result.values == 42.5) # Test with numpy scalar types - result = DataConverter.as_dataarray(np.int64(42), sample_time_index) + result = DataConverter.to_dataarray(np.int64(42), sample_time_index) assert np.all(result.values == 42) - result = DataConverter.as_dataarray(np.float32(42.5), sample_time_index) + result = DataConverter.to_dataarray(np.float32(42.5), sample_time_index) assert np.all(result.values == 42.5) def test_ndarray_conversion(self, sample_time_index): """Test converting a numpy ndarray.""" # Test with integer 1D array arr_1d = np.array([1, 2, 3, 4, 5]) - result = DataConverter.as_dataarray(arr_1d, sample_time_index) + result = DataConverter.to_dataarray(arr_1d, sample_time_index) assert result.shape == (5,) assert result.dims == ('time',) assert np.array_equal(result.values, arr_1d) # Test with float 1D array arr_1d = np.array([1.1, 2.2, 3.3, 4.4, 5.5]) - result = DataConverter.as_dataarray(arr_1d, sample_time_index) + result = DataConverter.to_dataarray(arr_1d, sample_time_index) assert np.array_equal(result.values, arr_1d) # Test with array containing NaN arr_1d = np.array([1, np.nan, 3, np.nan, 5]) - result = DataConverter.as_dataarray(arr_1d, sample_time_index) + result = DataConverter.to_dataarray(arr_1d, sample_time_index) assert np.array_equal(np.isnan(result.values), np.isnan(arr_1d)) assert np.array_equal(result.values[~np.isnan(result.values)], arr_1d[~np.isnan(arr_1d)]) @@ -74,7 +73,7 @@ def test_dataarray_conversion(self, sample_time_index): original = xr.DataArray(data=np.array([1, 2, 3, 4, 5]), coords={'time': sample_time_index}, dims=['time']) # Convert and check - result = DataConverter.as_dataarray(original, sample_time_index) + result = DataConverter.to_dataarray(original, sample_time_index) assert result.shape == (5,) assert result.dims == ('time',) assert np.array_equal(result.values, original.values) @@ -89,7 +88,7 @@ def test_dataarray_conversion(self, sample_time_index): # Should raise an error for mismatched time coordinates with pytest.raises(ConversionError): - DataConverter.as_dataarray(original, sample_time_index) + DataConverter.to_dataarray(original, sample_time_index) class TestMultiDimensionConversion: @@ -98,7 +97,7 @@ class TestMultiDimensionConversion: def test_scalar_with_scenarios(self, sample_time_index, sample_scenario_index): """Test converting scalar values with scenario dimension.""" # Test with integer - result = DataConverter.as_dataarray(42, sample_time_index, sample_scenario_index) + result = DataConverter.to_dataarray(42, sample_time_index, sample_scenario_index) assert isinstance(result, xr.DataArray) assert result.shape == (len(sample_time_index), len(sample_scenario_index)) @@ -108,7 +107,7 @@ def test_scalar_with_scenarios(self, sample_time_index, sample_scenario_index): assert set(result.coords['time'].values) == set(sample_time_index.values) # Test with float - result = DataConverter.as_dataarray(42.5, sample_time_index, sample_scenario_index) + result = DataConverter.to_dataarray(42.5, sample_time_index, sample_scenario_index) assert np.all(result.values == 42.5) def test_1d_array_with_scenarios(self, sample_time_index, sample_scenario_index): @@ -117,7 +116,7 @@ def test_1d_array_with_scenarios(self, sample_time_index, sample_scenario_index) arr_1d = np.array([1, 2, 3, 4, 5]) # Convert with scenarios - result = DataConverter.as_dataarray(arr_1d, sample_time_index, sample_scenario_index) + result = DataConverter.to_dataarray(arr_1d, sample_time_index, sample_scenario_index) assert result.shape == (len(sample_time_index), len(sample_scenario_index)) assert result.dims == ('time', 'scenario') @@ -139,7 +138,7 @@ def test_2d_array_with_scenarios(self, sample_time_index, sample_scenario_index) ) # Convert to DataArray - result = DataConverter.as_dataarray(arr_2d.T, sample_time_index, sample_scenario_index) + result = DataConverter.to_dataarray(arr_2d.T, sample_time_index, sample_scenario_index) assert result.shape == (5, 3) assert result.dims == ('time', 'scenario') @@ -159,7 +158,7 @@ def test_dataarray_with_scenarios(self, sample_time_index, sample_scenario_index ) # Test conversion - result = DataConverter.as_dataarray(original, sample_time_index, sample_scenario_index) + result = DataConverter.to_dataarray(original, sample_time_index, sample_scenario_index) assert result.shape == (5, 3) assert result.dims == ('time', 'scenario') @@ -179,7 +178,7 @@ def test_series_single_dimension(self, sample_time_index): series = pd.Series([10, 20, 30, 40, 50], index=sample_time_index) # Convert and check - result = DataConverter.as_dataarray(series, sample_time_index) + result = DataConverter.to_dataarray(series, sample_time_index) assert isinstance(result, xr.DataArray) assert result.shape == (5,) assert result.dims == ('time',) @@ -190,7 +189,7 @@ def test_series_single_dimension(self, sample_time_index): scenario_index = pd.Index(['baseline', 'high_demand', 'low_price'], name='scenario') series = pd.Series([100, 200, 300], index=scenario_index) - result = DataConverter.as_dataarray(series, scenarios=scenario_index) + result = DataConverter.to_dataarray(series, scenarios=scenario_index) assert result.shape == (3,) assert result.dims == ('scenario',) assert np.array_equal(result.values, series.values) @@ -204,7 +203,7 @@ def test_series_mismatched_index(self, sample_time_index): # Should raise error for mismatched index with pytest.raises(ConversionError): - DataConverter.as_dataarray(series, sample_time_index) + DataConverter.to_dataarray(series, sample_time_index) def test_series_broadcast_to_scenarios(self, sample_time_index, sample_scenario_index): """Test broadcasting a time-indexed Series across scenarios.""" @@ -212,7 +211,7 @@ def test_series_broadcast_to_scenarios(self, sample_time_index, sample_scenario_ series = pd.Series([10, 20, 30, 40, 50], index=sample_time_index) # Convert with scenarios - result = DataConverter.as_dataarray(series, sample_time_index, sample_scenario_index) + result = DataConverter.to_dataarray(series, sample_time_index, sample_scenario_index) assert result.shape == (5, 3) assert result.dims == ('time', 'scenario') @@ -228,7 +227,7 @@ def test_series_broadcast_to_time(self, sample_time_index, sample_scenario_index series = pd.Series([100, 200, 300], index=sample_scenario_index) # Convert with time - result = DataConverter.as_dataarray(series, sample_time_index, sample_scenario_index) + result = DataConverter.to_dataarray(series, sample_time_index, sample_scenario_index) assert result.shape == (5, 3) assert result.dims == ('time', 'scenario') @@ -264,7 +263,7 @@ def test_dataframe_single_column(self, sample_time_index): df = pd.DataFrame({'value': [10, 20, 30, 40, 50]}, index=sample_time_index) # Convert and check - result = DataConverter.as_dataarray(df, sample_time_index) + result = DataConverter.to_dataarray(df, sample_time_index) assert isinstance(result, xr.DataArray) assert result.shape == (5,) assert result.dims == ('time',) @@ -277,7 +276,7 @@ def test_dataframe_multi_column_fails(self, sample_time_index): # Should raise error with pytest.raises(ConversionError): - DataConverter.as_dataarray(df, sample_time_index) + DataConverter.to_dataarray(df, sample_time_index) def test_dataframe_time_scenario(self, sample_time_index, sample_scenario_index): """Test converting a DataFrame with time index and scenario columns.""" @@ -289,7 +288,7 @@ def test_dataframe_time_scenario(self, sample_time_index, sample_scenario_index) df.columns.name = 'scenario' # Convert and check - result = DataConverter.as_dataarray(df, sample_time_index, sample_scenario_index) + result = DataConverter.to_dataarray(df, sample_time_index, sample_scenario_index) assert result.shape == (5, 3) assert result.dims == ('time', 'scenario') @@ -309,7 +308,7 @@ def test_dataframe_mismatched_coordinates(self, sample_time_index, sample_scenar # Should raise error with pytest.raises(ConversionError): - DataConverter.as_dataarray(df, sample_time_index, sample_scenario_index) + DataConverter.to_dataarray(df, sample_time_index, sample_scenario_index) # Create DataFrame with different scenario columns different_scenarios = pd.Index(['scenario1', 'scenario2', 'scenario3'], name='scenario') @@ -319,7 +318,7 @@ def test_dataframe_mismatched_coordinates(self, sample_time_index, sample_scenar # Should raise error with pytest.raises(ConversionError): - DataConverter.as_dataarray(df, sample_time_index, sample_scenario_index) + DataConverter.to_dataarray(df, sample_time_index, sample_scenario_index) def test_ensure_copy(self, sample_time_index, sample_scenario_index): """Test that the returned DataArray is a copy.""" @@ -329,7 +328,7 @@ def test_ensure_copy(self, sample_time_index, sample_scenario_index): df.columns = sample_scenario_index # Convert - result = DataConverter.as_dataarray(df, sample_time_index, sample_scenario_index) + result = DataConverter.to_dataarray(df, sample_time_index, sample_scenario_index) # Modify the result result.loc[dict(time=sample_time_index[0], scenario='baseline')] = 999 @@ -346,51 +345,51 @@ def test_time_index_validation(self): # Test with unnamed index unnamed_index = pd.date_range('2024-01-01', periods=5, freq='D') with pytest.raises(ConversionError): - DataConverter.as_dataarray(42, unnamed_index) + DataConverter.to_dataarray(42, unnamed_index) # Test with empty index empty_index = pd.DatetimeIndex([], name='time') with pytest.raises(ConversionError): - DataConverter.as_dataarray(42, empty_index) + DataConverter.to_dataarray(42, empty_index) # Test with non-DatetimeIndex wrong_type_index = pd.Index([1, 2, 3, 4, 5], name='time') with pytest.raises(ConversionError): - DataConverter.as_dataarray(42, wrong_type_index) + DataConverter.to_dataarray(42, wrong_type_index) def test_scenario_index_validation(self, sample_time_index): """Test validation of scenario index.""" # Test with unnamed scenario index unnamed_index = pd.Index(['baseline', 'high_demand']) with pytest.raises(ConversionError): - DataConverter.as_dataarray(42, sample_time_index, unnamed_index) + DataConverter.to_dataarray(42, sample_time_index, unnamed_index) # Test with empty scenario index empty_index = pd.Index([], name='scenario') with pytest.raises(ConversionError): - DataConverter.as_dataarray(42, sample_time_index, empty_index) + DataConverter.to_dataarray(42, sample_time_index, empty_index) # Test with non-Index scenario with pytest.raises(ConversionError): - DataConverter.as_dataarray(42, sample_time_index, ['baseline', 'high_demand']) + DataConverter.to_dataarray(42, sample_time_index, ['baseline', 'high_demand']) def test_invalid_data_types(self, sample_time_index, sample_scenario_index): """Test handling of invalid data types.""" # Test invalid input type (string) with pytest.raises(ConversionError): - DataConverter.as_dataarray('invalid_string', sample_time_index) + DataConverter.to_dataarray('invalid_string', sample_time_index) # Test invalid input type with scenarios with pytest.raises(ConversionError): - DataConverter.as_dataarray('invalid_string', sample_time_index, sample_scenario_index) + DataConverter.to_dataarray('invalid_string', sample_time_index, sample_scenario_index) # Test unsupported complex object with pytest.raises(ConversionError): - DataConverter.as_dataarray(object(), sample_time_index) + DataConverter.to_dataarray(object(), sample_time_index) # Test None value with pytest.raises(ConversionError): - DataConverter.as_dataarray(None, sample_time_index) + DataConverter.to_dataarray(None, sample_time_index) def test_mismatched_input_dimensions(self, sample_time_index, sample_scenario_index): """Test handling of mismatched input dimensions.""" @@ -399,16 +398,16 @@ def test_mismatched_input_dimensions(self, sample_time_index, sample_scenario_in [1, 2, 3, 4, 5, 6], index=pd.date_range('2025-01-01', periods=6, freq='D', name='time') ) with pytest.raises(ConversionError): - DataConverter.as_dataarray(mismatched_series, sample_time_index) + DataConverter.to_dataarray(mismatched_series, sample_time_index) # Test DataFrame with multiple columns df_multi_col = pd.DataFrame({'A': [1, 2, 3, 4, 5], 'B': [6, 7, 8, 9, 10]}, index=sample_time_index) with pytest.raises(ConversionError): - DataConverter.as_dataarray(df_multi_col, sample_time_index) + DataConverter.to_dataarray(df_multi_col, sample_time_index) # Test mismatched array shape for time-only with pytest.raises(ConversionError): - DataConverter.as_dataarray(np.array([1, 2, 3]), sample_time_index) # Wrong length + DataConverter.to_dataarray(np.array([1, 2, 3]), sample_time_index) # Wrong length # Test mismatched array shape for scenario × time # Array shape should be (n_scenarios, n_timesteps) @@ -420,24 +419,24 @@ def test_mismatched_input_dimensions(self, sample_time_index, sample_scenario_in ] ) with pytest.raises(ConversionError): - DataConverter.as_dataarray(wrong_shape_array, sample_time_index, sample_scenario_index) + DataConverter.to_dataarray(wrong_shape_array, sample_time_index, sample_scenario_index) # Test array with too many dimensions with pytest.raises(ConversionError): # 3D array not allowed - DataConverter.as_dataarray(np.ones((3, 5, 2)), sample_time_index, sample_scenario_index) + DataConverter.to_dataarray(np.ones((3, 5, 2)), sample_time_index, sample_scenario_index) def test_dataarray_dimension_mismatch(self, sample_time_index, sample_scenario_index): """Test handling of mismatched DataArray dimensions.""" # Create DataArray with wrong dimensions wrong_dims = xr.DataArray(data=np.array([1, 2, 3, 4, 5]), coords={'wrong_dim': range(5)}, dims=['wrong_dim']) with pytest.raises(ConversionError): - DataConverter.as_dataarray(wrong_dims, sample_time_index) + DataConverter.to_dataarray(wrong_dims, sample_time_index) # Create DataArray with scenario but no time wrong_dims_2 = xr.DataArray(data=np.array([1, 2, 3]), coords={'scenario': ['a', 'b', 'c']}, dims=['scenario']) with pytest.raises(ConversionError): - DataConverter.as_dataarray(wrong_dims_2, sample_time_index, sample_scenario_index) + DataConverter.to_dataarray(wrong_dims_2, sample_time_index, sample_scenario_index) # Create DataArray with right dims but wrong length wrong_length = xr.DataArray( @@ -449,7 +448,7 @@ def test_dataarray_dimension_mismatch(self, sample_time_index, sample_scenario_i dims=['scenario', 'time'], ) with pytest.raises(ConversionError): - DataConverter.as_dataarray(wrong_length, sample_time_index, sample_scenario_index) + DataConverter.to_dataarray(wrong_length, sample_time_index, sample_scenario_index) class TestDataArrayBroadcasting: """Tests for broadcasting DataArrays.""" @@ -458,7 +457,7 @@ def test_broadcast_1d_array_to_2d(self, sample_time_index, sample_scenario_index arr_1d = np.array([1, 2, 3, 4, 5]) xr.testing.assert_equal( - DataConverter.as_dataarray(arr_1d, sample_time_index, sample_scenario_index), + DataConverter.to_dataarray(arr_1d, sample_time_index, sample_scenario_index), xr.DataArray( np.array([arr_1d] * 3).T, coords=(sample_time_index, sample_scenario_index) @@ -467,7 +466,7 @@ def test_broadcast_1d_array_to_2d(self, sample_time_index, sample_scenario_index arr_1d = np.array([1, 2, 3]) xr.testing.assert_equal( - DataConverter.as_dataarray(arr_1d, sample_time_index, sample_scenario_index), + DataConverter.to_dataarray(arr_1d, sample_time_index, sample_scenario_index), xr.DataArray( np.array([arr_1d] * 5), coords=(sample_time_index, sample_scenario_index) @@ -479,7 +478,7 @@ def test_broadcast_1d_array_to_1d(self, sample_time_index,): arr_1d = np.array([1, 2, 3, 4, 5]) xr.testing.assert_equal( - DataConverter.as_dataarray(arr_1d, sample_time_index), + DataConverter.to_dataarray(arr_1d, sample_time_index), xr.DataArray( arr_1d, coords=(sample_time_index,) @@ -488,7 +487,7 @@ def test_broadcast_1d_array_to_1d(self, sample_time_index,): arr_1d = np.array([1, 2, 3]) with pytest.raises(ConversionError): - DataConverter.as_dataarray(arr_1d, sample_time_index) + DataConverter.to_dataarray(arr_1d, sample_time_index) class TestEdgeCases: @@ -500,12 +499,12 @@ def test_single_timestep(self, sample_scenario_index): single_timestep = pd.DatetimeIndex(['2024-01-01'], name='time') # Scalar conversion - result = DataConverter.as_dataarray(42, single_timestep) + result = DataConverter.to_dataarray(42, single_timestep) assert result.shape == (1,) assert result.dims == ('time',) # With scenarios - result_with_scenarios = DataConverter.as_dataarray(42, single_timestep, sample_scenario_index) + result_with_scenarios = DataConverter.to_dataarray(42, single_timestep, sample_scenario_index) assert result_with_scenarios.shape == (1, len(sample_scenario_index)) assert result_with_scenarios.dims == ('time', 'scenario') @@ -515,19 +514,19 @@ def test_single_scenario(self, sample_time_index): single_scenario = pd.Index(['baseline'], name='scenario') # Scalar conversion with single scenario - result = DataConverter.as_dataarray(42, sample_time_index, single_scenario) + result = DataConverter.to_dataarray(42, sample_time_index, single_scenario) assert result.shape == (len(sample_time_index), 1) assert result.dims == ('time', 'scenario') # Array conversion with single scenario arr = np.array([1, 2, 3, 4, 5]) - result_arr = DataConverter.as_dataarray(arr, sample_time_index, single_scenario) + result_arr = DataConverter.to_dataarray(arr, sample_time_index, single_scenario) assert result_arr.shape == (5, 1) assert np.array_equal(result_arr.sel(scenario='baseline').values, arr) # 2D array with single scenario arr_2d = np.array([[1, 2, 3, 4, 5]]) # Note the extra dimension - result_arr_2d = DataConverter.as_dataarray(arr_2d.T, sample_time_index, single_scenario) + result_arr_2d = DataConverter.to_dataarray(arr_2d.T, sample_time_index, single_scenario) assert result_arr_2d.shape == (5, 1) assert np.array_equal(result_arr_2d.sel(scenario='baseline').values, arr_2d[0]) @@ -546,12 +545,12 @@ def test_different_scenario_order(self, sample_time_index): ] ).T - result1 = DataConverter.as_dataarray(data, sample_time_index, scenarios1) + result1 = DataConverter.to_dataarray(data, sample_time_index, scenarios1) assert np.array_equal(result1.sel(scenario='a').values, [1, 2, 3, 4, 5]) assert np.array_equal(result1.sel(scenario='c').values, [11, 12, 13, 14, 15]) # Create DataArray with second order - result2 = DataConverter.as_dataarray(data, sample_time_index, scenarios2) + result2 = DataConverter.to_dataarray(data, sample_time_index, scenarios2) # First row should match 'c' now assert np.array_equal(result2.sel(scenario='c').values, [1, 2, 3, 4, 5]) # Last row should match 'a' now @@ -561,16 +560,16 @@ def test_all_nan_data(self, sample_time_index, sample_scenario_index): """Test handling of all-NaN data.""" # Create array of all NaNs all_nan_array = np.full(5, np.nan) - result = DataConverter.as_dataarray(all_nan_array, sample_time_index) + result = DataConverter.to_dataarray(all_nan_array, sample_time_index) assert np.all(np.isnan(result.values)) # With scenarios - result = DataConverter.as_dataarray(all_nan_array, sample_time_index, sample_scenario_index) + result = DataConverter.to_dataarray(all_nan_array, sample_time_index, sample_scenario_index) assert result.shape == (len(sample_time_index), len(sample_scenario_index)) assert np.all(np.isnan(result.values)) # Series of all NaNs - result = DataConverter.as_dataarray( + result = DataConverter.to_dataarray( np.array([np.nan, np.nan, np.nan, np.nan, np.nan]), sample_time_index, sample_scenario_index ) assert np.all(np.isnan(result.values)) @@ -579,14 +578,14 @@ def test_mixed_data_types(self, sample_time_index, sample_scenario_index): """Test conversion of mixed integer and float data.""" # Create array with mixed types mixed_array = np.array([1, 2.5, 3, 4.5, 5]) - result = DataConverter.as_dataarray(mixed_array, sample_time_index) + result = DataConverter.to_dataarray(mixed_array, sample_time_index) # Result should be float dtype assert np.issubdtype(result.dtype, np.floating) assert np.array_equal(result.values, mixed_array) # With scenarios - result = DataConverter.as_dataarray(mixed_array, sample_time_index, sample_scenario_index) + result = DataConverter.to_dataarray(mixed_array, sample_time_index, sample_scenario_index) assert np.issubdtype(result.dtype, np.floating) for scenario in sample_scenario_index: assert np.array_equal(result.sel(scenario=scenario).values, mixed_array) @@ -609,7 +608,7 @@ def test_large_dataset(self, sample_scenario_index): large_data = np.random.rand(len(sample_scenario_index), len(large_timesteps)) # Convert and check - result = DataConverter.as_dataarray(large_data.T, large_timesteps, sample_scenario_index) + result = DataConverter.to_dataarray(large_data.T, large_timesteps, sample_scenario_index) assert result.shape == (len(large_timesteps), len(sample_scenario_index)) assert result.dims == ('time', 'scenario') @@ -622,7 +621,7 @@ class TestMultiScenarioArrayConversion: def test_1d_array_broadcasting(self, sample_time_index, sample_scenario_index): """Test that 1D arrays are properly broadcast to all scenarios.""" arr_1d = np.array([1, 2, 3, 4, 5]) - result = DataConverter.as_dataarray(arr_1d, sample_time_index, sample_scenario_index) + result = DataConverter.to_dataarray(arr_1d, sample_time_index, sample_scenario_index) assert result.shape == (len(sample_time_index), len(sample_scenario_index)) @@ -643,14 +642,14 @@ def test_2d_array_different_shapes(self, sample_time_index): single_scenario = pd.Index(['baseline'], name='scenario') arr_1_scenario = np.array([[1, 2, 3, 4, 5]]) - result = DataConverter.as_dataarray(arr_1_scenario.T, sample_time_index, single_scenario) + result = DataConverter.to_dataarray(arr_1_scenario.T, sample_time_index, single_scenario) assert result.shape == (len(sample_time_index), 1) # Test with 2 scenarios two_scenarios = pd.Index(['baseline', 'high_demand'], name='scenario') arr_2_scenarios = np.array([[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]]) - result = DataConverter.as_dataarray(arr_2_scenarios.T, sample_time_index, two_scenarios) + result = DataConverter.to_dataarray(arr_2_scenarios.T, sample_time_index, two_scenarios) assert result.shape == (len(sample_time_index), 2) assert np.array_equal(result.sel(scenario='baseline').values, arr_2_scenarios[0]) assert np.array_equal(result.sel(scenario='high_demand').values, arr_2_scenarios[1]) @@ -658,19 +657,19 @@ def test_2d_array_different_shapes(self, sample_time_index): # Test mismatched scenarios count three_scenarios = pd.Index(['a', 'b', 'c'], name='scenario') with pytest.raises(ConversionError): - DataConverter.as_dataarray(arr_2_scenarios, sample_time_index, three_scenarios) + DataConverter.to_dataarray(arr_2_scenarios, sample_time_index, three_scenarios) def test_array_handling_edge_cases(self, sample_time_index, sample_scenario_index): """Test array edge cases.""" # Test with boolean array bool_array = np.array([True, False, True, False, True]) - result = DataConverter.as_dataarray(bool_array, sample_time_index, sample_scenario_index) + result = DataConverter.to_dataarray(bool_array, sample_time_index, sample_scenario_index) assert result.dtype == bool assert result.shape == (len(sample_time_index), len(sample_scenario_index)) # Test with array containing infinite values inf_array = np.array([1, np.inf, 3, -np.inf, 5]) - result = DataConverter.as_dataarray(inf_array, sample_time_index, sample_scenario_index) + result = DataConverter.to_dataarray(inf_array, sample_time_index, sample_scenario_index) for scenario in sample_scenario_index: scenario_data = result.sel(scenario=scenario) assert np.isinf(scenario_data[1].item()) @@ -696,7 +695,7 @@ def test_preserving_scenario_order(self, sample_time_index): ) # Convert to DataArray - result = DataConverter.as_dataarray(data.T, sample_time_index, scenarios) + result = DataConverter.to_dataarray(data.T, sample_time_index, scenarios) # Verify order of scenarios is preserved assert list(result.coords['scenario'].values) == list(scenarios) @@ -714,42 +713,42 @@ def test_preserving_scenario_order(self, sample_time_index): def test_invalid_inputs(sample_time_index): # Test invalid input type with pytest.raises(ConversionError): - DataConverter.as_dataarray('invalid_string', sample_time_index) + DataConverter.to_dataarray('invalid_string', sample_time_index) # Test mismatched Series index mismatched_series = pd.Series([1, 2, 3, 4, 5, 6], index=pd.date_range('2025-01-01', periods=6, freq='D')) with pytest.raises(ConversionError): - DataConverter.as_dataarray(mismatched_series, sample_time_index) + DataConverter.to_dataarray(mismatched_series, sample_time_index) # Test DataFrame with multiple columns df_multi_col = pd.DataFrame({'A': [1, 2, 3, 4, 5], 'B': [6, 7, 8, 9, 10]}, index=sample_time_index) with pytest.raises(ConversionError): - DataConverter.as_dataarray(df_multi_col, sample_time_index) + DataConverter.to_dataarray(df_multi_col, sample_time_index) # Test mismatched array shape with pytest.raises(ConversionError): - DataConverter.as_dataarray(np.array([1, 2, 3]), sample_time_index) # Wrong length + DataConverter.to_dataarray(np.array([1, 2, 3]), sample_time_index) # Wrong length # Test multi-dimensional array with pytest.raises(ConversionError): - DataConverter.as_dataarray(np.array([[1, 2], [3, 4]]), sample_time_index) # 2D array not allowed + DataConverter.to_dataarray(np.array([[1, 2], [3, 4]]), sample_time_index) # 2D array not allowed def test_time_index_validation(): # Test with unnamed index unnamed_index = pd.date_range('2024-01-01', periods=5, freq='D') with pytest.raises(ConversionError): - DataConverter.as_dataarray(42, unnamed_index) + DataConverter.to_dataarray(42, unnamed_index) # Test with empty index empty_index = pd.DatetimeIndex([], name='time') with pytest.raises(ConversionError): - DataConverter.as_dataarray(42, empty_index) + DataConverter.to_dataarray(42, empty_index) # Test with non-DatetimeIndex wrong_type_index = pd.Index([1, 2, 3, 4, 5], name='time') with pytest.raises(ConversionError): - DataConverter.as_dataarray(42, wrong_type_index) + DataConverter.to_dataarray(42, wrong_type_index) if __name__ == '__main__': From 6b56dac3818771df98e4ace468a79a258d509e71 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 29 Jun 2025 20:52:42 +0200 Subject: [PATCH 138/448] Update DataConverter --- flixopt/core.py | 516 ++++++++--------------------------- tests/test_dataconverter.py | 522 ++++++++++-------------------------- 2 files changed, 255 insertions(+), 783 deletions(-) diff --git a/flixopt/core.py b/flixopt/core.py index 3b320276d..9ab71cab9 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -126,178 +126,77 @@ def agg_weight(self): class DataConverter: """ - Converts various data types into xarray.DataArray with optional time and scenario dimension. + Converts scalars and 1D data into xarray.DataArray with optional time and scenario dimensions. - Supports: scalars, arrays, Series, DataFrames, DataArrays, and TimeSeriesData. + Only handles: + - Scalars (int, float, np.number) + - 1D arrays (np.ndarray, pd.Series) + - xr.DataArray (for broadcasting/checking) """ - @staticmethod - def _fix_timeseries_data_indexing( - data: TimeSeriesData, timesteps: pd.DatetimeIndex, dims: list, coords: list - ) -> TimeSeriesData: - """ - Fix TimeSeriesData indexing issues and return properly indexed TimeSeriesData. - - Args: - data: TimeSeriesData that might have indexing issues - timesteps: Target timesteps - dims: Expected dimensions - coords: Expected coordinates - - Returns: - TimeSeriesData with correct indexing - - Raises: - ConversionError: If data cannot be fixed to match expected indexing - """ - expected_shape = (len(timesteps),) - - # Check if dimensions match - if data.dims != tuple(dims): - logger.warning( - f'TimeSeriesData has dimensions {data.dims}, expected {dims}. Reshaping to match timesteps. To avoid ' - f'this warning, create a correctly shaped DataArray with the correct dimensions in the first place.' - ) - # Try to reshape the data to match expected dimensions - if data.size != len(timesteps): - raise ConversionError( - f'TimeSeriesData has {data.size} elements, cannot reshape to match {len(timesteps)} timesteps' - ) - # Create new DataArray with correct coordinates, preserving metadata - reshaped_data = xr.DataArray( - data.values.reshape(expected_shape), coords=coords, dims=dims, name=data.name, attrs=data.attrs.copy() - ) - return TimeSeriesData(reshaped_data) - - # Check if time coordinate length matches - elif data.sizes[dims[0]] != len(coords[0]): - logger.warning( - f'TimeSeriesData has {data.sizes[dims[0]]} time points, ' - f"expected {len(coords[0])}. Cannot reindex - lengths don't match." - ) - raise ConversionError( - f"TimeSeriesData length {data.sizes[dims[0]]} doesn't match expected {len(coords[0])}" - ) - - # Check if time coordinates are identical - elif not data.coords['time'].equals(timesteps): - logger.warning( - 'TimeSeriesData has different time coordinates than expected. Replacing with provided timesteps.' - ) - # Replace time coordinates while preserving data and metadata - recoordinated_data = xr.DataArray( - data.values, coords=coords, dims=dims, name=data.name, attrs=data.attrs.copy() - ) - return TimeSeriesData(recoordinated_data) - - else: - # Everything matches - return copy to avoid modifying original - return data.copy(deep=True) - @staticmethod def to_dataarray( - data: Union[TemporalData, NonTemporalData], timesteps: Optional[pd.DatetimeIndex] = None, scenarios: Optional[pd.Index] = None + data: Union[Scalar, np.ndarray, pd.Series, xr.DataArray, TimeSeriesData], + timesteps: Optional[pd.DatetimeIndex] = None, + scenarios: Optional[pd.Index] = None, ) -> xr.DataArray: """ Convert data to xarray.DataArray with specified dimensions. Args: - data: The data to convert (scalar, array, or DataArray) + data: Scalar, 1D array/Series, or existing DataArray timesteps: Optional DatetimeIndex for time dimension scenarios: Optional Index for scenario dimension Returns: DataArray with the converted data """ - # Prepare dimensions and coordinates coords, dims = DataConverter._prepare_dimensions(timesteps, scenarios) - # Select appropriate converter based on data type + # Handle scalars if isinstance(data, (int, float, np.integer, np.floating)): return DataConverter._convert_scalar(data, coords, dims) - elif isinstance(data, xr.DataArray): - return DataConverter._convert_dataarray(data, coords, dims) - + # Handle 1D numpy arrays elif isinstance(data, np.ndarray): - return DataConverter._convert_ndarray(data, coords, dims) + if data.ndim != 1: + raise ConversionError(f'Only 1D arrays supported, got {data.ndim}D array') + return DataConverter._convert_1d_array(data, coords, dims) + # Handle pandas Series elif isinstance(data, pd.Series): return DataConverter._convert_series(data, coords, dims) - elif isinstance(data, pd.DataFrame): - return DataConverter._convert_dataframe(data, coords, dims) + # Handle existing DataArrays (including TimeSeriesData) + elif isinstance(data, xr.DataArray): + return DataConverter._handle_dataarray(data, coords, dims) else: - raise ConversionError(f'Unsupported data type: {type(data).__name__}') - - @staticmethod - def _validate_timesteps(timesteps: pd.DatetimeIndex) -> pd.DatetimeIndex: - """ - Validate and prepare time index. - - Args: - timesteps: The time index to validate - - Returns: - Validated time index - """ - if not isinstance(timesteps, pd.DatetimeIndex) or len(timesteps) == 0: - raise ConversionError('Timesteps must be a non-empty DatetimeIndex') - - if not timesteps.name == 'time': - logger.warning(f'Timesteps must be named "time", got "{timesteps.name}". Renaming to "time".') - timesteps = timesteps.rename('time') - - return timesteps - - @staticmethod - def _validate_scenarios(scenarios: pd.Index) -> pd.Index: - """ - Validate and prepare scenario index. - - Args: - scenarios: The scenario index to validate - """ - if not isinstance(scenarios, pd.Index) or len(scenarios) == 0: - raise ConversionError('Scenarios must be a non-empty Index') - - if not scenarios.name == 'scenario': - logger.warning(f'Scenarios must be named "scenario", got "{scenarios.name}". Renaming to "scenario".') - scenarios = scenarios.rename('scenario') - - return scenarios + raise ConversionError( + f'Unsupported data type: {type(data).__name__}. Only scalars, 1D arrays, Series, and DataArrays are supported.' + ) @staticmethod def _prepare_dimensions( timesteps: Optional[pd.DatetimeIndex], scenarios: Optional[pd.Index] ) -> Tuple[Dict[str, pd.Index], Tuple[str, ...]]: - """ - Prepare coordinates and dimensions for the DataArray. - - Args: - timesteps: Optional time index - scenarios: Optional scenario index - - Returns: - Tuple of (coordinates dict, dimensions tuple) - """ - # Validate inputs if provided - if timesteps is not None: - timesteps = DataConverter._validate_timesteps(timesteps) - - if scenarios is not None: - scenarios = DataConverter._validate_scenarios(scenarios) - - # Build coordinates and dimensions + """Prepare coordinates and dimensions.""" coords = {} dims = [] if timesteps is not None: + if not isinstance(timesteps, pd.DatetimeIndex) or len(timesteps) == 0: + raise ConversionError('Timesteps must be a non-empty DatetimeIndex') + if timesteps.name != 'time': + timesteps = timesteps.rename('time') coords['time'] = timesteps dims.append('time') if scenarios is not None: + if not isinstance(scenarios, pd.Index) or len(scenarios) == 0: + raise ConversionError('Scenarios must be a non-empty Index') + if scenarios.name != 'scenario': + scenarios = scenarios.rename('scenario') coords['scenario'] = scenarios dims.append('scenario') @@ -307,322 +206,129 @@ def _prepare_dimensions( def _convert_scalar( data: Union[int, float, np.integer, np.floating], coords: Dict[str, pd.Index], dims: Tuple[str, ...] ) -> xr.DataArray: - """ - Convert a scalar value to a DataArray. - - Args: - data: The scalar value - coords: Coordinate dictionary - dims: Dimension names - - Returns: - DataArray with the scalar value - """ + """Convert scalar to DataArray, broadcasting to all dimensions.""" if isinstance(data, (np.integer, np.floating)): data = data.item() return xr.DataArray(data, coords=coords, dims=dims) @staticmethod - def _convert_dataarray(data: xr.DataArray, coords: Dict[str, pd.Index], dims: Tuple[str, ...]) -> xr.DataArray: - """ - Convert an existing DataArray to desired dimensions. - - Args: - data: The source DataArray - coords: Target coordinates - dims: Target dimensions - - Returns: - DataArray with the target dimensions - """ - # No dimensions case - if len(dims) == 0: - if data.size != 1: - raise ConversionError('When converting to dimensionless DataArray, source must be scalar') - return xr.DataArray(data.values.item()) - - # Check if data already has matching dimensions and coordinates - if set(data.dims) == set(dims): - # Check if coordinates match - is_compatible = True - for dim in dims: - if dim in data.dims and not np.array_equal(data.coords[dim].values, coords[dim].values): - is_compatible = False - break - - if is_compatible: - # Ensure dimensions are in the correct order - if data.dims != dims: - # Transpose to get dimensions in the right order - return data.transpose(*dims).copy(deep=True) - else: - # Return existing DataArray if compatible and order is correct - return data.copy(deep=True) - - # Handle dimension broadcasting - if len(data.dims) == 1 and len(dims) == 2: - # Single dimension to two dimensions - if data.dims[0] == 'time' and 'scenario' in dims: - # Broadcast time dimension to include scenarios - return DataConverter._broadcast_time_to_scenarios(data, coords, dims) - - elif data.dims[0] == 'scenario' and 'time' in dims: - # Broadcast scenario dimension to include time - return DataConverter._broadcast_scenario_to_time(data, coords, dims) - - raise ConversionError( - f'Cannot convert {data.dims} to {dims}. Source coordinates: {data.coords}, Target coordinates: {coords}' - ) - @staticmethod - def _broadcast_time_to_scenarios( - data: xr.DataArray, coords: Dict[str, pd.Index], dims: Tuple[str, ...] - ) -> xr.DataArray: - """ - Broadcast a time-only DataArray to include scenarios. - - Args: - data: The time-indexed DataArray - coords: Target coordinates - dims: Target dimensions - - Returns: - DataArray with time and scenario dimensions - """ - # Check compatibility - if not np.array_equal(data.coords['time'].values, coords['time'].values): - raise ConversionError("Source time coordinates don't match target time coordinates") - - if len(coords['scenario']) <= 1: - return data.copy(deep=True) - - # Broadcast values - values = np.repeat(data.values[:, np.newaxis], len(coords['scenario']), axis=1) - return xr.DataArray(values.copy(), coords=coords, dims=dims) - - @staticmethod - def _broadcast_scenario_to_time( - data: xr.DataArray, coords: Dict[str, pd.Index], dims: Tuple[str, ...] - ) -> xr.DataArray: - """ - Broadcast a scenario-only DataArray to include time. - - Args: - data: The scenario-indexed DataArray - coords: Target coordinates - dims: Target dimensions - - Returns: - DataArray with time and scenario dimensions - """ - # Check compatibility - if not np.array_equal(data.coords['scenario'].values, coords['scenario'].values): - raise ConversionError("Source scenario coordinates don't match target scenario coordinates") - - # Broadcast values - values = np.repeat(data.values[:, np.newaxis], len(coords['time']), axis=1).T - return xr.DataArray(values.copy(), coords=coords, dims=dims) - - @staticmethod - def _convert_ndarray(data: np.ndarray, coords: Dict[str, pd.Index], dims: Tuple[str, ...]) -> xr.DataArray: - """ - Convert a NumPy array to a DataArray. - - Args: - data: The NumPy array - coords: Target coordinates - dims: Target dimensions - - Returns: - DataArray from the NumPy array - """ - # Handle dimensionless case + def _convert_1d_array(data: np.ndarray, coords: Dict[str, pd.Index], dims: Tuple[str, ...]) -> xr.DataArray: + """Convert 1D array to DataArray.""" if len(dims) == 0: - if data.size != 1: - raise ConversionError('Without dimensions, can only convert scalar arrays') - return xr.DataArray(data.item()) + if len(data) != 1: + raise ConversionError('Cannot convert multi-element array without dimensions') + return xr.DataArray(data[0]) - # Handle single dimension elif len(dims) == 1: - return DataConverter._convert_ndarray_single_dim(data, coords, dims) - - # Handle two dimensions - elif len(dims) == 2: - return DataConverter._convert_ndarray_two_dims(data, coords, dims) - - else: - raise ConversionError('Maximum 2 dimensions supported') - - @staticmethod - def _convert_ndarray_single_dim( - data: np.ndarray, coords: Dict[str, pd.Index], dims: Tuple[str, ...] - ) -> xr.DataArray: - """ - Convert a NumPy array to a single-dimension DataArray. - - Args: - data: The NumPy array - coords: Target coordinates - dims: Target dimensions (length 1) - - Returns: - DataArray with single dimension - """ - dim_name = dims[0] - dim_length = len(coords[dim_name]) - - if data.ndim == 1: - # 1D array must match dimension length - if data.shape[0] != dim_length: - raise ConversionError(f"Array length {data.shape[0]} doesn't match {dim_name} length {dim_length}") + dim_name = dims[0] + if len(data) != len(coords[dim_name]): + raise ConversionError( + f'Array length {len(data)} does not match {dim_name} length {len(coords[dim_name])}' + ) return xr.DataArray(data, coords=coords, dims=dims) - else: - raise ConversionError(f'Expected 1D array for single dimension, got {data.ndim}D') - - @staticmethod - def _convert_ndarray_two_dims(data: np.ndarray, coords: Dict[str, pd.Index], dims: Tuple[str, ...]) -> xr.DataArray: - """ - Convert a NumPy array to a two-dimension DataArray. - Args: - data: The NumPy array - coords: Target coordinates - dims: Target dimensions (length 2) - - Returns: - DataArray with two dimensions - """ - scenario_length = len(coords['scenario']) - time_length = len(coords['time']) + elif len(dims) == 2: + # Broadcast 1D array to 2D based on which dimension it matches + time_len = len(coords['time']) + scenario_len = len(coords['scenario']) - if data.ndim == 1: - # For 1D array, create 2D array based on which dimension it matches - if data.shape[0] == time_length: + if len(data) == time_len: # Broadcast across scenarios - values = np.repeat(data[:, np.newaxis], scenario_length, axis=1) + values = np.repeat(data[:, np.newaxis], scenario_len, axis=1) return xr.DataArray(values, coords=coords, dims=dims) - elif data.shape[0] == scenario_length: + elif len(data) == scenario_len: # Broadcast across time - values = np.repeat(data[np.newaxis, :], time_length, axis=0) + values = np.repeat(data[np.newaxis, :], time_len, axis=0) return xr.DataArray(values, coords=coords, dims=dims) else: - raise ConversionError(f"1D array length {data.shape[0]} doesn't match either dimension") - - elif data.ndim == 2: - # For 2D array, shape must match dimensions - expected_shape = (time_length, scenario_length) - if data.shape != expected_shape: - raise ConversionError(f"2D array shape {data.shape} doesn't match expected shape {expected_shape}") - return xr.DataArray(data, coords=coords, dims=dims) + raise ConversionError( + f'Array length {len(data)} matches neither time ({time_len}) nor scenario ({scenario_len}) dimensions' + ) else: - raise ConversionError(f'Expected 1D or 2D array for two dimensions, got {data.ndim}D') + raise ConversionError('Maximum 2 dimensions supported') @staticmethod def _convert_series(data: pd.Series, coords: Dict[str, pd.Index], dims: Tuple[str, ...]) -> xr.DataArray: - """ - Convert pandas Series to xarray DataArray. - - Args: - data: pandas Series to convert - coords: Target coordinates - dims: Target dimensions + """Convert pandas Series to DataArray.""" + if len(dims) == 0: + if len(data) != 1: + raise ConversionError('Cannot convert multi-element Series without dimensions') + return xr.DataArray(data.iloc[0]) - Returns: - DataArray from the pandas Series - """ - # Handle single dimension case - if len(dims) == 1: + elif len(dims) == 1: dim_name = dims[0] + if not data.index.equals(coords[dim_name]): + raise ConversionError(f'Series index does not match {dim_name} coordinates') + return xr.DataArray(data.values, coords=coords, dims=dims) - # Check if series index matches the dimension - if data.index.equals(coords[dim_name]): - return xr.DataArray(data.values.copy(), coords=coords, dims=dims) - else: - raise ConversionError( - f"Series index doesn't match {dim_name} coordinates.\n" - f'Series index: {data.index}\n' - f'Target {dim_name} coordinates: {coords[dim_name]}' - ) - - # Handle two dimensions case elif len(dims) == 2: - # Check if dimensions are time and scenario - if dims != ('time', 'scenario'): - raise ConversionError( - f'Two-dimensional conversion only supports time and scenario dimensions, got {dims}' - ) - - # Case 1: Series is indexed by time + # Check which dimension the Series index matches if data.index.equals(coords['time']): # Broadcast across scenarios values = np.repeat(data.values[:, np.newaxis], len(coords['scenario']), axis=1) - return xr.DataArray(values.copy(), coords=coords, dims=dims) - - # Case 2: Series is indexed by scenario + return xr.DataArray(values, coords=coords, dims=dims) elif data.index.equals(coords['scenario']): # Broadcast across time values = np.repeat(data.values[np.newaxis, :], len(coords['time']), axis=0) - return xr.DataArray(values.copy(), coords=coords, dims=dims) - + return xr.DataArray(values, coords=coords, dims=dims) else: - raise ConversionError( - "Series index must match either 'time' or 'scenario' coordinates.\n" - f'Series index: {data.index}\n' - f'Target time coordinates: {coords["time"]}\n' - f'Target scenario coordinates: {coords["scenario"]}' - ) + raise ConversionError('Series index must match either time or scenario coordinates') else: - raise ConversionError(f'Maximum 2 dimensions supported, got {len(dims)}') + raise ConversionError('Maximum 2 dimensions supported') @staticmethod - def _convert_dataframe(data: pd.DataFrame, coords: Dict[str, pd.Index], dims: Tuple[str, ...]) -> xr.DataArray: - """ - Convert pandas DataFrame to xarray DataArray. - Only allows time as index and scenarios as columns. + def _handle_dataarray(data: xr.DataArray, coords: Dict[str, pd.Index], dims: Tuple[str, ...]) -> xr.DataArray: + """Handle existing DataArray - check compatibility or broadcast.""" + # If no target dimensions, data must be scalar + if len(dims) == 0: + if data.size != 1: + raise ConversionError('DataArray must be scalar when no dimensions specified') + return xr.DataArray(data.values.item()) - Args: - data: pandas DataFrame to convert - coords: Target coordinates - dims: Target dimensions + # Check if already compatible + if data.dims == dims: + # Check if coordinates match + compatible = True + for dim in dims: + if not np.array_equal(data.coords[dim].values, coords[dim].values): + compatible = False + break + if compatible: + return data.copy() - Returns: - DataArray from the pandas DataFrame - """ - # Single dimension case - if len(dims) == 1: - # If DataFrame has one column, treat it like a Series - if len(data.columns) == 1: - series = data.iloc[:, 0] - return DataConverter._convert_series(series, coords, dims) + # Handle broadcasting from smaller to larger dimensions + if len(data.dims) < len(dims): + return DataConverter._broadcast_dataarray(data, coords, dims) - raise ConversionError( - f'When converting DataFrame to single-dimension DataArray, DataFrame must have exactly one column, got {len(data.columns)}' - ) + # If dimensions don't match and can't broadcast, raise error + raise ConversionError(f'Cannot convert DataArray with dims {data.dims} to target dims {dims}') - # Two dimensions case - elif len(dims) == 2: - # Check if dimensions are time and scenario - if dims != ('time', 'scenario'): - raise ConversionError( - f'Two-dimensional conversion only supports time and scenario dimensions, got {dims}' - ) + @staticmethod + def _broadcast_dataarray(data: xr.DataArray, coords: Dict[str, pd.Index], dims: Tuple[str, ...]) -> xr.DataArray: + """Broadcast DataArray to target dimensions.""" + if len(data.dims) == 0: + # Scalar DataArray - broadcast to all dimensions + return xr.DataArray(data.values.item(), coords=coords, dims=dims) - # DataFrame must have time as index and scenarios as columns - if data.index.equals(coords['time']) and data.columns.equals(coords['scenario']): - # Create DataArray with proper dimension order - return xr.DataArray(data.values.copy(), coords=coords, dims=dims) - else: - raise ConversionError( - 'DataFrame must have time as index and scenarios as columns.\n' - f'DataFrame index: {data.index}\n' - f'DataFrame columns: {data.columns}\n' - f'Target time coordinates: {coords["time"]}\n' - f'Target scenario coordinates: {coords["scenario"]}' - ) + elif len(data.dims) == 1 and len(dims) == 2: + source_dim = data.dims[0] - else: - raise ConversionError(f'Maximum 2 dimensions supported, got {len(dims)}') + # Check coordinate compatibility + if not np.array_equal(data.coords[source_dim].values, coords[source_dim].values): + raise ConversionError(f'Source {source_dim} coordinates do not match target coordinates') + + if source_dim == 'time': + # Broadcast time to include scenarios + values = np.repeat(data.values[:, np.newaxis], len(coords['scenario']), axis=1) + return xr.DataArray(values, coords=coords, dims=dims) + elif source_dim == 'scenario': + # Broadcast scenario to include time + values = np.repeat(data.values[np.newaxis, :], len(coords['time']), axis=0) + return xr.DataArray(values, coords=coords, dims=dims) + + raise ConversionError(f'Cannot broadcast from {data.dims} to {dims}') def get_dataarray_stats(arr: xr.DataArray) -> Dict: diff --git a/tests/test_dataconverter.py b/tests/test_dataconverter.py index 1ad41a0d2..d92077307 100644 --- a/tests/test_dataconverter.py +++ b/tests/test_dataconverter.py @@ -6,6 +6,7 @@ from flixopt.core import ( # Adjust this import to match your project structure ConversionError, DataConverter, + TimeSeriesData, ) @@ -19,12 +20,6 @@ def sample_scenario_index(): return pd.Index(['baseline', 'high_demand', 'low_price'], name='scenario') -@pytest.fixture -def multi_index(sample_time_index, sample_scenario_index): - """Create a sample MultiIndex combining scenarios and times.""" - return pd.MultiIndex.from_product([sample_scenario_index, sample_time_index], names=['scenario', 'time']) - - class TestSingleDimensionConversion: """Tests for converting data without scenarios (1D: time only).""" @@ -110,8 +105,8 @@ def test_scalar_with_scenarios(self, sample_time_index, sample_scenario_index): result = DataConverter.to_dataarray(42.5, sample_time_index, sample_scenario_index) assert np.all(result.values == 42.5) - def test_1d_array_with_scenarios(self, sample_time_index, sample_scenario_index): - """Test converting 1D array with scenario dimension (broadcasting).""" + def test_1d_array_with_scenarios_time_broadcast(self, sample_time_index, sample_scenario_index): + """Test converting 1D array matching time dimension (broadcasting across scenarios).""" # Create 1D array matching timesteps length arr_1d = np.array([1, 2, 3, 4, 5]) @@ -126,35 +121,29 @@ def test_1d_array_with_scenarios(self, sample_time_index, sample_scenario_index) scenario_slice = result.sel(scenario=scenario) assert np.array_equal(scenario_slice.values, arr_1d) - def test_2d_array_with_scenarios(self, sample_time_index, sample_scenario_index): - """Test converting 2D array with scenario dimension.""" - # Create 2D array with different values per scenario - arr_2d = np.array( - [ - [1, 2, 3, 4, 5], # baseline scenario - [6, 7, 8, 9, 10], # high_demand scenario - [11, 12, 13, 14, 15], # low_price scenario - ] - ) + def test_1d_array_with_scenarios_scenario_broadcast(self, sample_time_index, sample_scenario_index): + """Test converting 1D array matching scenario dimension (broadcasting across time).""" + # Create 1D array matching scenario length + arr_1d = np.array([10, 20, 30]) # 3 scenarios - # Convert to DataArray - result = DataConverter.to_dataarray(arr_2d.T, sample_time_index, sample_scenario_index) + # Convert with time and scenarios + result = DataConverter.to_dataarray(arr_1d, sample_time_index, sample_scenario_index) - assert result.shape == (5, 3) + assert result.shape == (len(sample_time_index), len(sample_scenario_index)) assert result.dims == ('time', 'scenario') - # Check that each scenario has correct values - assert np.array_equal(result.sel(scenario='baseline').values, arr_2d[0]) - assert np.array_equal(result.sel(scenario='high_demand').values, arr_2d[1]) - assert np.array_equal(result.sel(scenario='low_price').values, arr_2d[2]) + # Each time step should have the same scenario values (broadcasting) + for time in sample_time_index: + time_slice = result.sel(time=time) + assert np.array_equal(time_slice.values, arr_1d) def test_dataarray_with_scenarios(self, sample_time_index, sample_scenario_index): """Test converting an existing DataArray with scenarios.""" - # Create a multi-scenario DataArray + # Create a multi-scenario DataArray with dims in (time, scenario) order original = xr.DataArray( - data=np.array([[1, 2, 3, 4, 5], [6, 7, 8, 9, 10], [11, 12, 13, 14, 15]]), - coords={'scenario': sample_scenario_index, 'time': sample_time_index}, - dims=['scenario', 'time'], + data=np.array([[1, 6, 11], [2, 7, 12], [3, 8, 13], [4, 9, 14], [5, 10, 15]]), + coords={'time': sample_time_index, 'scenario': sample_scenario_index}, + dims=['time', 'scenario'], ) # Test conversion @@ -162,7 +151,7 @@ def test_dataarray_with_scenarios(self, sample_time_index, sample_scenario_index assert result.shape == (5, 3) assert result.dims == ('time', 'scenario') - assert np.array_equal(result.values, original.values.T) + assert np.array_equal(result.values, original.values) # Ensure it's a copy result.loc[:, 'baseline'] = 999 @@ -172,7 +161,7 @@ def test_dataarray_with_scenarios(self, sample_time_index, sample_scenario_index class TestSeriesConversion: """Tests for converting pandas Series to DataArray.""" - def test_series_single_dimension(self, sample_time_index): + def test_series_single_dimension_time(self, sample_time_index): """Test converting a pandas Series with time index.""" # Create a Series with matching time index series = pd.Series([10, 20, 30, 40, 50], index=sample_time_index) @@ -185,15 +174,16 @@ def test_series_single_dimension(self, sample_time_index): assert np.array_equal(result.values, series.values) assert np.array_equal(result.coords['time'].values, sample_time_index.values) - # Test with scenario index - scenario_index = pd.Index(['baseline', 'high_demand', 'low_price'], name='scenario') - series = pd.Series([100, 200, 300], index=scenario_index) + def test_series_single_dimension_scenario(self, sample_scenario_index): + """Test converting a pandas Series with scenario index.""" + # Create a Series with scenario index + series = pd.Series([100, 200, 300], index=sample_scenario_index) - result = DataConverter.to_dataarray(series, scenarios=scenario_index) + result = DataConverter.to_dataarray(series, scenarios=sample_scenario_index) assert result.shape == (3,) assert result.dims == ('scenario',) assert np.array_equal(result.values, series.values) - assert np.array_equal(result.coords['scenario'].values, scenario_index.values) + assert np.array_equal(result.coords['scenario'].values, sample_scenario_index.values) def test_series_mismatched_index(self, sample_time_index): """Test converting a Series with mismatched index.""" @@ -237,104 +227,39 @@ def test_series_broadcast_to_time(self, sample_time_index, sample_scenario_index time_slice = result.sel(time=time) assert np.array_equal(time_slice.values, series.values) - def test_series_dimension_order(self, sample_time_index, sample_scenario_index): - """Test that dimension order is respected with Series conversions.""" - # Create custom dimensions tuple with reversed order - dims = ('scenario', 'time',) - coords = {'time': sample_time_index, 'scenario': sample_scenario_index} - - # Time-indexed series - series = pd.Series([10, 20, 30, 40, 50], index=sample_time_index) - with pytest.raises(ConversionError, match="only supports time and scenario dimensions"): - _ = DataConverter._convert_series(series, coords, dims) - - # Scenario-indexed series - series = pd.Series([100, 200, 300], index=sample_scenario_index) - with pytest.raises(ConversionError, match="only supports time and scenario dimensions"): - _ = DataConverter._convert_series(series, coords, dims) +class TestTimeSeriesDataConversion: + """Tests for converting TimeSeriesData objects.""" -class TestDataFrameConversion: - """Tests for converting pandas DataFrame to DataArray.""" + def test_timeseries_data_conversion(self, sample_time_index): + """Test converting TimeSeriesData.""" + # Create TimeSeriesData + data_array = xr.DataArray([1, 2, 3, 4, 5], coords={'time': sample_time_index}, dims=['time']) + ts_data = TimeSeriesData(data_array, aggregation_group='test_group') - def test_dataframe_single_column(self, sample_time_index): - """Test converting a DataFrame with a single column.""" - # Create DataFrame with one column - df = pd.DataFrame({'value': [10, 20, 30, 40, 50]}, index=sample_time_index) + # Convert + result = DataConverter.to_dataarray(ts_data, sample_time_index) - # Convert and check - result = DataConverter.to_dataarray(df, sample_time_index) assert isinstance(result, xr.DataArray) assert result.shape == (5,) assert result.dims == ('time',) - assert np.array_equal(result.values, df['value'].values) - - def test_dataframe_multi_column_fails(self, sample_time_index): - """Test that converting a multi-column DataFrame to 1D fails.""" - # Create DataFrame with multiple columns - df = pd.DataFrame({'val1': [10, 20, 30, 40, 50], 'val2': [15, 25, 35, 45, 55]}, index=sample_time_index) + assert np.array_equal(result.values, [1, 2, 3, 4, 5]) - # Should raise error - with pytest.raises(ConversionError): - DataConverter.to_dataarray(df, sample_time_index) - - def test_dataframe_time_scenario(self, sample_time_index, sample_scenario_index): - """Test converting a DataFrame with time index and scenario columns.""" - # Create DataFrame with time as index and scenarios as columns - data = {'baseline': [10, 20, 30, 40, 50], 'high_demand': [15, 25, 35, 45, 55], 'low_price': [5, 15, 25, 35, 45]} - df = pd.DataFrame(data, index=sample_time_index) + def test_timeseries_data_with_scenarios(self, sample_time_index, sample_scenario_index): + """Test converting TimeSeriesData with broadcasting to scenarios.""" + # Create 1D TimeSeriesData + data_array = xr.DataArray([1, 2, 3, 4, 5], coords={'time': sample_time_index}, dims=['time']) + ts_data = TimeSeriesData(data_array) - # Make sure columns are named properly - df.columns.name = 'scenario' - - # Convert and check - result = DataConverter.to_dataarray(df, sample_time_index, sample_scenario_index) + # Convert with scenarios (should broadcast) + result = DataConverter.to_dataarray(ts_data, sample_time_index, sample_scenario_index) assert result.shape == (5, 3) assert result.dims == ('time', 'scenario') - assert np.array_equal(result.values, df.values) - # Check values for specific scenarios - assert np.array_equal(result.sel(scenario='baseline').values, df['baseline'].values) - assert np.array_equal(result.sel(scenario='high_demand').values, df['high_demand'].values) - - def test_dataframe_mismatched_coordinates(self, sample_time_index, sample_scenario_index): - """Test conversion fails with mismatched coordinates.""" - # Create DataFrame with different time index - different_times = pd.date_range('2025-01-01', periods=5, freq='D', name='time') - data = {'baseline': [10, 20, 30, 40, 50], 'high_demand': [15, 25, 35, 45, 55], 'low_price': [5, 15, 25, 35, 45]} - df = pd.DataFrame(data, index=different_times) - df.columns = sample_scenario_index - - # Should raise error - with pytest.raises(ConversionError): - DataConverter.to_dataarray(df, sample_time_index, sample_scenario_index) - - # Create DataFrame with different scenario columns - different_scenarios = pd.Index(['scenario1', 'scenario2', 'scenario3'], name='scenario') - data = {'scenario1': [10, 20, 30, 40, 50], 'scenario2': [15, 25, 35, 45, 55], 'scenario3': [5, 15, 25, 35, 45]} - df = pd.DataFrame(data, index=sample_time_index) - df.columns = different_scenarios - - # Should raise error - with pytest.raises(ConversionError): - DataConverter.to_dataarray(df, sample_time_index, sample_scenario_index) - - def test_ensure_copy(self, sample_time_index, sample_scenario_index): - """Test that the returned DataArray is a copy.""" - # Create DataFrame - data = {'baseline': [10, 20, 30, 40, 50], 'high_demand': [15, 25, 35, 45, 55], 'low_price': [5, 15, 25, 35, 45]} - df = pd.DataFrame(data, index=sample_time_index) - df.columns = sample_scenario_index - - # Convert - result = DataConverter.to_dataarray(df, sample_time_index, sample_scenario_index) - - # Modify the result - result.loc[dict(time=sample_time_index[0], scenario='baseline')] = 999 - - # Original should be unchanged - assert df.loc[sample_time_index[0], 'baseline'] == 10 + # Each scenario should have the same values + for scenario in sample_scenario_index: + assert np.array_equal(result.sel(scenario=scenario).values, [1, 2, 3, 4, 5]) class TestInvalidInputs: @@ -344,8 +269,9 @@ def test_time_index_validation(self): """Test validation of time index.""" # Test with unnamed index unnamed_index = pd.date_range('2024-01-01', periods=5, freq='D') - with pytest.raises(ConversionError): - DataConverter.to_dataarray(42, unnamed_index) + # Should automatically rename to 'time' with a warning, not raise error + result = DataConverter.to_dataarray(42, unnamed_index) + assert result.coords['time'].name == 'time' # Test with empty index empty_index = pd.DatetimeIndex([], name='time') @@ -361,8 +287,9 @@ def test_scenario_index_validation(self, sample_time_index): """Test validation of scenario index.""" # Test with unnamed scenario index unnamed_index = pd.Index(['baseline', 'high_demand']) - with pytest.raises(ConversionError): - DataConverter.to_dataarray(42, sample_time_index, unnamed_index) + # Should automatically rename to 'scenario' with a warning, not raise error + result = DataConverter.to_dataarray(42, sample_time_index, unnamed_index) + assert result.coords['scenario'].name == 'scenario' # Test with empty scenario index empty_index = pd.Index([], name='scenario') @@ -391,6 +318,18 @@ def test_invalid_data_types(self, sample_time_index, sample_scenario_index): with pytest.raises(ConversionError): DataConverter.to_dataarray(None, sample_time_index) + def test_multidimensional_array_rejection(self, sample_time_index, sample_scenario_index): + """Test that multidimensional arrays are rejected.""" + # Test 2D array (not supported in simplified version) + arr_2d = np.array([[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]]) + with pytest.raises(ConversionError, match="Only 1D arrays supported"): + DataConverter.to_dataarray(arr_2d, sample_time_index) + + # Test 3D array + arr_3d = np.ones((2, 3, 4)) + with pytest.raises(ConversionError, match="Only 1D arrays supported"): + DataConverter.to_dataarray(arr_3d, sample_time_index, sample_scenario_index) + def test_mismatched_input_dimensions(self, sample_time_index, sample_scenario_index): """Test handling of mismatched input dimensions.""" # Test mismatched Series index @@ -400,31 +339,14 @@ def test_mismatched_input_dimensions(self, sample_time_index, sample_scenario_in with pytest.raises(ConversionError): DataConverter.to_dataarray(mismatched_series, sample_time_index) - # Test DataFrame with multiple columns - df_multi_col = pd.DataFrame({'A': [1, 2, 3, 4, 5], 'B': [6, 7, 8, 9, 10]}, index=sample_time_index) - with pytest.raises(ConversionError): - DataConverter.to_dataarray(df_multi_col, sample_time_index) - - # Test mismatched array shape for time-only + # Test mismatched array length for time-only with pytest.raises(ConversionError): DataConverter.to_dataarray(np.array([1, 2, 3]), sample_time_index) # Wrong length - # Test mismatched array shape for scenario × time - # Array shape should be (n_scenarios, n_timesteps) - wrong_shape_array = np.array( - [ - [1, 2, 3, 4], # Missing a timestep - [5, 6, 7, 8], - [9, 10, 11, 12], - ] - ) - with pytest.raises(ConversionError): - DataConverter.to_dataarray(wrong_shape_array, sample_time_index, sample_scenario_index) - - # Test array with too many dimensions + # Test array that doesn't match either dimension + wrong_length_array = np.array([1, 2, 3, 4]) # Doesn't match time (5) or scenario (3) with pytest.raises(ConversionError): - # 3D array not allowed - DataConverter.to_dataarray(np.ones((3, 5, 2)), sample_time_index, sample_scenario_index) + DataConverter.to_dataarray(wrong_length_array, sample_time_index, sample_scenario_index) def test_dataarray_dimension_mismatch(self, sample_time_index, sample_scenario_index): """Test handling of mismatched DataArray dimensions.""" @@ -433,61 +355,58 @@ def test_dataarray_dimension_mismatch(self, sample_time_index, sample_scenario_i with pytest.raises(ConversionError): DataConverter.to_dataarray(wrong_dims, sample_time_index) - # Create DataArray with scenario but no time - wrong_dims_2 = xr.DataArray(data=np.array([1, 2, 3]), coords={'scenario': ['a', 'b', 'c']}, dims=['scenario']) - with pytest.raises(ConversionError): - DataConverter.to_dataarray(wrong_dims_2, sample_time_index, sample_scenario_index) - - # Create DataArray with right dims but wrong length - wrong_length = xr.DataArray( - data=np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]), - coords={ - 'scenario': sample_scenario_index, - 'time': pd.date_range('2024-01-01', periods=3, freq='D', name='time'), - }, - dims=['scenario', 'time'], + # Create DataArray with right dims but wrong coordinate values + wrong_coords = xr.DataArray( + data=np.array([1, 2, 3, 4, 5]), + coords={'time': pd.date_range('2025-01-01', periods=5, freq='D', name='time')}, + dims=['time'] ) with pytest.raises(ConversionError): - DataConverter.to_dataarray(wrong_length, sample_time_index, sample_scenario_index) + DataConverter.to_dataarray(wrong_coords, sample_time_index) + class TestDataArrayBroadcasting: """Tests for broadcasting DataArrays.""" - def test_broadcast_1d_array_to_2d(self, sample_time_index, sample_scenario_index): - """Test broadcasting a 1D array to all scenarios.""" + + def test_broadcast_1d_array_to_2d_time(self, sample_time_index, sample_scenario_index): + """Test broadcasting a 1D array (time) to 2D.""" arr_1d = np.array([1, 2, 3, 4, 5]) - xr.testing.assert_equal( - DataConverter.to_dataarray(arr_1d, sample_time_index, sample_scenario_index), - xr.DataArray( - np.array([arr_1d] * 3).T, - coords=(sample_time_index, sample_scenario_index) - ) - ) + result = DataConverter.to_dataarray(arr_1d, sample_time_index, sample_scenario_index) - arr_1d = np.array([1, 2, 3]) - xr.testing.assert_equal( - DataConverter.to_dataarray(arr_1d, sample_time_index, sample_scenario_index), - xr.DataArray( - np.array([arr_1d] * 5), - coords=(sample_time_index, sample_scenario_index) - ) - ) + # Should broadcast across scenarios + expected = np.repeat(arr_1d[:, np.newaxis], len(sample_scenario_index), axis=1) + assert np.array_equal(result.values, expected) + assert result.dims == ('time', 'scenario') - def test_broadcast_1d_array_to_1d(self, sample_time_index,): - """Test broadcasting a 1D array to all scenarios.""" + def test_broadcast_1d_array_to_2d_scenario(self, sample_time_index, sample_scenario_index): + """Test broadcasting a 1D array (scenario) to 2D.""" + arr_1d = np.array([1, 2, 3]) # Matches scenario length + + result = DataConverter.to_dataarray(arr_1d, sample_time_index, sample_scenario_index) + + # Should broadcast across time + expected = np.repeat(arr_1d[np.newaxis, :], len(sample_time_index), axis=0) + assert np.array_equal(result.values, expected) + assert result.dims == ('time', 'scenario') + + def test_broadcast_1d_array_to_1d(self, sample_time_index): + """Test that 1D array with matching dimension doesn't change.""" arr_1d = np.array([1, 2, 3, 4, 5]) - xr.testing.assert_equal( - DataConverter.to_dataarray(arr_1d, sample_time_index), - xr.DataArray( - arr_1d, - coords=(sample_time_index,) - ) - ) + result = DataConverter.to_dataarray(arr_1d, sample_time_index) - arr_1d = np.array([1, 2, 3]) - with pytest.raises(ConversionError): - DataConverter.to_dataarray(arr_1d, sample_time_index) + assert np.array_equal(result.values, arr_1d) + assert result.dims == ('time',) + + def test_scalar_dataarray_broadcasting(self, sample_time_index, sample_scenario_index): + """Test broadcasting scalar DataArray.""" + scalar_da = xr.DataArray(42) + + result = DataConverter.to_dataarray(scalar_da, sample_time_index, sample_scenario_index) + + assert result.shape == (len(sample_time_index), len(sample_scenario_index)) + assert np.all(result.values == 42) class TestEdgeCases: @@ -524,38 +443,6 @@ def test_single_scenario(self, sample_time_index): assert result_arr.shape == (5, 1) assert np.array_equal(result_arr.sel(scenario='baseline').values, arr) - # 2D array with single scenario - arr_2d = np.array([[1, 2, 3, 4, 5]]) # Note the extra dimension - result_arr_2d = DataConverter.to_dataarray(arr_2d.T, sample_time_index, single_scenario) - assert result_arr_2d.shape == (5, 1) - assert np.array_equal(result_arr_2d.sel(scenario='baseline').values, arr_2d[0]) - - def test_different_scenario_order(self, sample_time_index): - """Test that scenario order is preserved.""" - # Test with different scenario orders - scenarios1 = pd.Index(['a', 'b', 'c'], name='scenario') - scenarios2 = pd.Index(['c', 'b', 'a'], name='scenario') - - # Create DataArray with first order - data = np.array( - [ - [1, 2, 3, 4, 5], # a - [6, 7, 8, 9, 10], # b - [11, 12, 13, 14, 15], # c - ] - ).T - - result1 = DataConverter.to_dataarray(data, sample_time_index, scenarios1) - assert np.array_equal(result1.sel(scenario='a').values, [1, 2, 3, 4, 5]) - assert np.array_equal(result1.sel(scenario='c').values, [11, 12, 13, 14, 15]) - - # Create DataArray with second order - result2 = DataConverter.to_dataarray(data, sample_time_index, scenarios2) - # First row should match 'c' now - assert np.array_equal(result2.sel(scenario='c').values, [1, 2, 3, 4, 5]) - # Last row should match 'a' now - assert np.array_equal(result2.sel(scenario='a').values, [11, 12, 13, 14, 15]) - def test_all_nan_data(self, sample_time_index, sample_scenario_index): """Test handling of all-NaN data.""" # Create array of all NaNs @@ -568,12 +455,6 @@ def test_all_nan_data(self, sample_time_index, sample_scenario_index): assert result.shape == (len(sample_time_index), len(sample_scenario_index)) assert np.all(np.isnan(result.values)) - # Series of all NaNs - result = DataConverter.to_dataarray( - np.array([np.nan, np.nan, np.nan, np.nan, np.nan]), sample_time_index, sample_scenario_index - ) - assert np.all(np.isnan(result.values)) - def test_mixed_data_types(self, sample_time_index, sample_scenario_index): """Test conversion of mixed integer and float data.""" # Create array with mixed types @@ -590,165 +471,50 @@ def test_mixed_data_types(self, sample_time_index, sample_scenario_index): for scenario in sample_scenario_index: assert np.array_equal(result.sel(scenario=scenario).values, mixed_array) - -class TestFunctionalUseCase: - """Tests for realistic use cases combining multiple features.""" - - def test_large_dataset(self, sample_scenario_index): - """Test with a larger dataset to ensure performance.""" - # Create a larger timestep array (e.g., hourly for a year) - large_timesteps = pd.date_range( - '2024-01-01', - periods=8760, # Hours in a year - freq='H', - name='time', - ) - - # Create large 2D array (3 scenarios × 8760 hours) - large_data = np.random.rand(len(sample_scenario_index), len(large_timesteps)) - - # Convert and check - result = DataConverter.to_dataarray(large_data.T, large_timesteps, sample_scenario_index) - - assert result.shape == (len(large_timesteps), len(sample_scenario_index)) - assert result.dims == ('time', 'scenario') - assert np.array_equal(result.values, large_data.T) - - -class TestMultiScenarioArrayConversion: - """Tests specifically focused on array conversion with scenarios.""" - - def test_1d_array_broadcasting(self, sample_time_index, sample_scenario_index): - """Test that 1D arrays are properly broadcast to all scenarios.""" - arr_1d = np.array([1, 2, 3, 4, 5]) - result = DataConverter.to_dataarray(arr_1d, sample_time_index, sample_scenario_index) - - assert result.shape == (len(sample_time_index), len(sample_scenario_index)) - - # Each scenario should have identical values - for i, scenario in enumerate(sample_scenario_index): - assert np.array_equal(result.sel(scenario=scenario).values, arr_1d) - - # Modify one scenario's values - result.loc[dict(scenario=scenario)] = np.ones(len(sample_time_index)) * i - - # Ensure modifications are isolated to each scenario - for i, scenario in enumerate(sample_scenario_index): - assert np.all(result.sel(scenario=scenario).values == i) - - def test_2d_array_different_shapes(self, sample_time_index): - """Test different scenario shapes with 2D arrays.""" - # Test with 1 scenario - single_scenario = pd.Index(['baseline'], name='scenario') - arr_1_scenario = np.array([[1, 2, 3, 4, 5]]) - - result = DataConverter.to_dataarray(arr_1_scenario.T, sample_time_index, single_scenario) - assert result.shape == (len(sample_time_index), 1) - - # Test with 2 scenarios - two_scenarios = pd.Index(['baseline', 'high_demand'], name='scenario') - arr_2_scenarios = np.array([[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]]) - - result = DataConverter.to_dataarray(arr_2_scenarios.T, sample_time_index, two_scenarios) - assert result.shape == (len(sample_time_index), 2) - assert np.array_equal(result.sel(scenario='baseline').values, arr_2_scenarios[0]) - assert np.array_equal(result.sel(scenario='high_demand').values, arr_2_scenarios[1]) - - # Test mismatched scenarios count - three_scenarios = pd.Index(['a', 'b', 'c'], name='scenario') - with pytest.raises(ConversionError): - DataConverter.to_dataarray(arr_2_scenarios, sample_time_index, three_scenarios) - - def test_array_handling_edge_cases(self, sample_time_index, sample_scenario_index): - """Test array edge cases.""" - # Test with boolean array + def test_boolean_data(self, sample_time_index, sample_scenario_index): + """Test handling of boolean data.""" bool_array = np.array([True, False, True, False, True]) result = DataConverter.to_dataarray(bool_array, sample_time_index, sample_scenario_index) assert result.dtype == bool assert result.shape == (len(sample_time_index), len(sample_scenario_index)) - # Test with array containing infinite values - inf_array = np.array([1, np.inf, 3, -np.inf, 5]) - result = DataConverter.to_dataarray(inf_array, sample_time_index, sample_scenario_index) - for scenario in sample_scenario_index: - scenario_data = result.sel(scenario=scenario) - assert np.isinf(scenario_data[1].item()) - assert np.isinf(scenario_data[3].item()) - assert scenario_data[3].item() < 0 # Negative infinity - - -class TestScenarioReindexing: - """Tests for reindexing and coordinate preservation in DataConverter.""" - - def test_preserving_scenario_order(self, sample_time_index): - """Test that scenario order is preserved in converted DataArrays.""" - # Define scenarios in a specific order - scenarios = pd.Index(['scenario3', 'scenario1', 'scenario2'], name='scenario') - - # Create 2D array - data = np.array( - [ - [1, 2, 3, 4, 5], # scenario3 - [6, 7, 8, 9, 10], # scenario1 - [11, 12, 13, 14, 15], # scenario2 - ] - ) - - # Convert to DataArray - result = DataConverter.to_dataarray(data.T, sample_time_index, scenarios) - - # Verify order of scenarios is preserved - assert list(result.coords['scenario'].values) == list(scenarios) - - # Verify data for each scenario - assert np.array_equal(result.sel(scenario='scenario3').values, data[0]) - assert np.array_equal(result.sel(scenario='scenario1').values, data[1]) - assert np.array_equal(result.sel(scenario='scenario2').values, data[2]) +class TestNoIndexConversion: + """Tests for conversion without any indices (scalar results).""" -if __name__ == '__main__': - pytest.main() - - -def test_invalid_inputs(sample_time_index): - # Test invalid input type - with pytest.raises(ConversionError): - DataConverter.to_dataarray('invalid_string', sample_time_index) - - # Test mismatched Series index - mismatched_series = pd.Series([1, 2, 3, 4, 5, 6], index=pd.date_range('2025-01-01', periods=6, freq='D')) - with pytest.raises(ConversionError): - DataConverter.to_dataarray(mismatched_series, sample_time_index) - - # Test DataFrame with multiple columns - df_multi_col = pd.DataFrame({'A': [1, 2, 3, 4, 5], 'B': [6, 7, 8, 9, 10]}, index=sample_time_index) - with pytest.raises(ConversionError): - DataConverter.to_dataarray(df_multi_col, sample_time_index) - - # Test mismatched array shape - with pytest.raises(ConversionError): - DataConverter.to_dataarray(np.array([1, 2, 3]), sample_time_index) # Wrong length - - # Test multi-dimensional array - with pytest.raises(ConversionError): - DataConverter.to_dataarray(np.array([[1, 2], [3, 4]]), sample_time_index) # 2D array not allowed - - -def test_time_index_validation(): - # Test with unnamed index - unnamed_index = pd.date_range('2024-01-01', periods=5, freq='D') - with pytest.raises(ConversionError): - DataConverter.to_dataarray(42, unnamed_index) - - # Test with empty index - empty_index = pd.DatetimeIndex([], name='time') - with pytest.raises(ConversionError): - DataConverter.to_dataarray(42, empty_index) + def test_scalar_no_dimensions(self): + """Test scalar conversion without any dimensions.""" + result = DataConverter.to_dataarray(42) + assert isinstance(result, xr.DataArray) + assert result.shape == () + assert result.dims == () + assert result.item() == 42 + + def test_single_element_array_no_dimensions(self): + """Test single-element array without dimensions.""" + arr = np.array([42]) + result = DataConverter.to_dataarray(arr) + assert result.shape == () + assert result.item() == 42 + + def test_multi_element_array_no_dimensions_fails(self): + """Test that multi-element array fails without dimensions.""" + arr = np.array([1, 2, 3]) + with pytest.raises(ConversionError): + DataConverter.to_dataarray(arr) - # Test with non-DatetimeIndex - wrong_type_index = pd.Index([1, 2, 3, 4, 5], name='time') - with pytest.raises(ConversionError): - DataConverter.to_dataarray(42, wrong_type_index) + def test_series_no_dimensions_fails(self): + """Test that multi-element Series fails without dimensions.""" + series = pd.Series([1, 2, 3]) + with pytest.raises(ConversionError): + DataConverter.to_dataarray(series) + + def test_single_element_series_no_dimensions(self): + """Test single-element Series without dimensions.""" + series = pd.Series([42]) + result = DataConverter.to_dataarray(series) + assert result.shape == () + assert result.item() == 42 if __name__ == '__main__': From a7ec9943866959234fe1db079dfb421350b2ecf7 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 29 Jun 2025 20:58:22 +0200 Subject: [PATCH 139/448] Add DataFrame Support back --- flixopt/core.py | 21 ++++++- tests/test_dataconverter.py | 111 ++++++++++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+), 3 deletions(-) diff --git a/flixopt/core.py b/flixopt/core.py index 9ab71cab9..687200f21 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -134,9 +134,20 @@ class DataConverter: - xr.DataArray (for broadcasting/checking) """ + @staticmethod + def _convert_dataframe(data: pd.DataFrame, coords: Dict[str, pd.Index], dims: Tuple[str, ...]) -> xr.DataArray: + """Convert single-column pandas DataFrame to DataArray by treating it as a Series.""" + # Check that DataFrame has exactly one column + if len(data.columns) != 1: + raise ConversionError(f'Only single-column DataFrames are supported, got {len(data.columns)} columns') + + # Extract the single column as a Series and convert it + series = data.iloc[:, 0] + return DataConverter._convert_series(series, coords, dims) + @staticmethod def to_dataarray( - data: Union[Scalar, np.ndarray, pd.Series, xr.DataArray, TimeSeriesData], + data: Union[Scalar, np.ndarray, pd.Series, pd.DataFrame, xr.DataArray, TimeSeriesData], timesteps: Optional[pd.DatetimeIndex] = None, scenarios: Optional[pd.Index] = None, ) -> xr.DataArray: @@ -144,7 +155,7 @@ def to_dataarray( Convert data to xarray.DataArray with specified dimensions. Args: - data: Scalar, 1D array/Series, or existing DataArray + data: Scalar, 1D array/Series, single-column DataFrame, or existing DataArray timesteps: Optional DatetimeIndex for time dimension scenarios: Optional Index for scenario dimension @@ -167,13 +178,17 @@ def to_dataarray( elif isinstance(data, pd.Series): return DataConverter._convert_series(data, coords, dims) + # Handle pandas DataFrames (single column only) + elif isinstance(data, pd.DataFrame): + return DataConverter._convert_dataframe(data, coords, dims) + # Handle existing DataArrays (including TimeSeriesData) elif isinstance(data, xr.DataArray): return DataConverter._handle_dataarray(data, coords, dims) else: raise ConversionError( - f'Unsupported data type: {type(data).__name__}. Only scalars, 1D arrays, Series, and DataArrays are supported.' + f'Unsupported data type: {type(data).__name__}. Only scalars, 1D arrays, Series, single-column DataFrames, and DataArrays are supported.' ) @staticmethod diff --git a/tests/test_dataconverter.py b/tests/test_dataconverter.py index d92077307..5536b8cb2 100644 --- a/tests/test_dataconverter.py +++ b/tests/test_dataconverter.py @@ -262,6 +262,117 @@ def test_timeseries_data_with_scenarios(self, sample_time_index, sample_scenario assert np.array_equal(result.sel(scenario=scenario).values, [1, 2, 3, 4, 5]) +class TestDataFrameConversion: + """Tests for converting single-column pandas DataFrames to DataArray.""" + + def test_single_column_dataframe_time(self, sample_time_index): + """Test converting a single-column DataFrame with time index.""" + # Create DataFrame with one column + df = pd.DataFrame({'value': [10, 20, 30, 40, 50]}, index=sample_time_index) + + # Convert and check + result = DataConverter.to_dataarray(df, sample_time_index) + assert isinstance(result, xr.DataArray) + assert result.shape == (5,) + assert result.dims == ('time',) + assert np.array_equal(result.values, df['value'].values) + + def test_single_column_dataframe_scenario(self, sample_scenario_index): + """Test converting a single-column DataFrame with scenario index.""" + # Create DataFrame with one column and scenario index + df = pd.DataFrame({'value': [100, 200, 300]}, index=sample_scenario_index) + + result = DataConverter.to_dataarray(df, scenarios=sample_scenario_index) + assert result.shape == (3,) + assert result.dims == ('scenario',) + assert np.array_equal(result.values, df['value'].values) + + def test_dataframe_broadcast_to_scenarios(self, sample_time_index, sample_scenario_index): + """Test broadcasting a time-indexed DataFrame across scenarios.""" + # Create DataFrame with time index + df = pd.DataFrame({'power': [10, 20, 30, 40, 50]}, index=sample_time_index) + + # Convert with scenarios + result = DataConverter.to_dataarray(df, sample_time_index, sample_scenario_index) + + assert result.shape == (5, 3) + assert result.dims == ('time', 'scenario') + + # Check broadcasting - each scenario should have the same values + for scenario in sample_scenario_index: + scenario_slice = result.sel(scenario=scenario) + assert np.array_equal(scenario_slice.values, df['power'].values) + + def test_dataframe_broadcast_to_time(self, sample_time_index, sample_scenario_index): + """Test broadcasting a scenario-indexed DataFrame across time.""" + # Create DataFrame with scenario index + df = pd.DataFrame({'cost': [100, 200, 300]}, index=sample_scenario_index) + + # Convert with time + result = DataConverter.to_dataarray(df, sample_time_index, sample_scenario_index) + + assert result.shape == (5, 3) + assert result.dims == ('time', 'scenario') + + # Check broadcasting - each time should have the same scenario values + for time in sample_time_index: + time_slice = result.sel(time=time) + assert np.array_equal(time_slice.values, df['cost'].values) + + def test_multi_column_dataframe_fails(self, sample_time_index): + """Test that multi-column DataFrames are rejected.""" + # Create DataFrame with multiple columns + df = pd.DataFrame({ + 'value1': [10, 20, 30, 40, 50], + 'value2': [15, 25, 35, 45, 55] + }, index=sample_time_index) + + # Should raise error + with pytest.raises(ConversionError, match="Only single-column DataFrames are supported"): + DataConverter.to_dataarray(df, sample_time_index) + + def test_dataframe_mismatched_index(self, sample_time_index): + """Test DataFrame with mismatched index.""" + # Create DataFrame with different time index + different_times = pd.date_range('2025-01-01', periods=5, freq='D', name='time') + df = pd.DataFrame({'value': [10, 20, 30, 40, 50]}, index=different_times) + + # Should raise error for mismatched index + with pytest.raises(ConversionError): + DataConverter.to_dataarray(df, sample_time_index) + + def test_dataframe_copy_behavior(self, sample_time_index): + """Test that DataFrame conversion creates a copy.""" + # Create DataFrame + df = pd.DataFrame({'value': [10, 20, 30, 40, 50]}, index=sample_time_index) + + # Convert + result = DataConverter.to_dataarray(df, sample_time_index) + + # Modify the result + result[0] = 999 + + # Original DataFrame should be unchanged + assert df.loc[sample_time_index[0], 'value'] == 10 + + def test_empty_dataframe_fails(self, sample_time_index): + """Test that empty DataFrames are rejected.""" + # DataFrame with no columns + df = pd.DataFrame(index=sample_time_index) + + with pytest.raises(ConversionError, match="Only single-column DataFrames are supported"): + DataConverter.to_dataarray(df, sample_time_index) + + def test_dataframe_with_named_column(self, sample_time_index): + """Test DataFrame with a named column.""" + df = pd.DataFrame(index=sample_time_index) + df['energy_output'] = [100, 150, 200, 175, 125] + + result = DataConverter.to_dataarray(df, sample_time_index) + assert result.shape == (5,) + assert np.array_equal(result.values, [100, 150, 200, 175, 125]) + + class TestInvalidInputs: """Tests for invalid inputs and error handling.""" From 2a75ed3c1c87283ab65d89a8d47cb35e01f8d83e Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 29 Jun 2025 20:59:00 +0200 Subject: [PATCH 140/448] Add copy() to DataConverter --- flixopt/core.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/flixopt/core.py b/flixopt/core.py index 687200f21..9834d8f39 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -240,7 +240,7 @@ def _convert_1d_array(data: np.ndarray, coords: Dict[str, pd.Index], dims: Tuple raise ConversionError( f'Array length {len(data)} does not match {dim_name} length {len(coords[dim_name])}' ) - return xr.DataArray(data, coords=coords, dims=dims) + return xr.DataArray(data.copy(), coords=coords, dims=dims) elif len(dims) == 2: # Broadcast 1D array to 2D based on which dimension it matches @@ -250,11 +250,11 @@ def _convert_1d_array(data: np.ndarray, coords: Dict[str, pd.Index], dims: Tuple if len(data) == time_len: # Broadcast across scenarios values = np.repeat(data[:, np.newaxis], scenario_len, axis=1) - return xr.DataArray(values, coords=coords, dims=dims) + return xr.DataArray(values.copy(), coords=coords, dims=dims) elif len(data) == scenario_len: # Broadcast across time values = np.repeat(data[np.newaxis, :], time_len, axis=0) - return xr.DataArray(values, coords=coords, dims=dims) + return xr.DataArray(values.copy(), coords=coords, dims=dims) else: raise ConversionError( f'Array length {len(data)} matches neither time ({time_len}) nor scenario ({scenario_len}) dimensions' @@ -275,18 +275,18 @@ def _convert_series(data: pd.Series, coords: Dict[str, pd.Index], dims: Tuple[st dim_name = dims[0] if not data.index.equals(coords[dim_name]): raise ConversionError(f'Series index does not match {dim_name} coordinates') - return xr.DataArray(data.values, coords=coords, dims=dims) + return xr.DataArray(data.values.copy(), coords=coords, dims=dims) elif len(dims) == 2: # Check which dimension the Series index matches if data.index.equals(coords['time']): # Broadcast across scenarios values = np.repeat(data.values[:, np.newaxis], len(coords['scenario']), axis=1) - return xr.DataArray(values, coords=coords, dims=dims) + return xr.DataArray(values.copy(), coords=coords, dims=dims) elif data.index.equals(coords['scenario']): # Broadcast across time values = np.repeat(data.values[np.newaxis, :], len(coords['time']), axis=0) - return xr.DataArray(values, coords=coords, dims=dims) + return xr.DataArray(values.copy(), coords=coords, dims=dims) else: raise ConversionError('Series index must match either time or scenario coordinates') @@ -337,11 +337,11 @@ def _broadcast_dataarray(data: xr.DataArray, coords: Dict[str, pd.Index], dims: if source_dim == 'time': # Broadcast time to include scenarios values = np.repeat(data.values[:, np.newaxis], len(coords['scenario']), axis=1) - return xr.DataArray(values, coords=coords, dims=dims) + return xr.DataArray(values.copy(), coords=coords, dims=dims) elif source_dim == 'scenario': # Broadcast scenario to include time values = np.repeat(data.values[np.newaxis, :], len(coords['time']), axis=0) - return xr.DataArray(values, coords=coords, dims=dims) + return xr.DataArray(values.copy(), coords=coords, dims=dims) raise ConversionError(f'Cannot broadcast from {data.dims} to {dims}') From dae9f01baeb9f14ebfa90ae1485f8befb4bdb838 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 29 Jun 2025 21:06:42 +0200 Subject: [PATCH 141/448] Update fit_to_model_coords to take a list of coords --- flixopt/flow_system.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index b513372bf..6bf7502e5 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -294,7 +294,7 @@ def fit_to_model_coords( self, name: str, data: Optional[Union[TemporalDataUser, NonTemporalDataUser]], - has_time_dim: bool = True, + dimensions: Union[List[str], str] = 'time', # Default to time only ) -> Optional[Union[TemporalData, NonTemporalData]]: """ Fit data to model coordinate system (currently time, but extensible). @@ -310,18 +310,31 @@ def fit_to_model_coords( if data is None: return None + # Build coords from requested dimensions + if isinstance(dimensions, str): + dimensions = [dimensions] + + coords = {} + for dim in dimensions: + if dim == 'time' and self.timesteps is not None: + coords['time'] = self.timesteps + elif dim == 'scenario' and self.scenarios is not None: + coords['scenario'] = self.scenarios + # Future: elif dim == 'region' and self.regions is not None: ... + + # Rest of your method stays the same, just pass coords if isinstance(data, TimeSeriesData): try: data.name = name # Set name of previous object! return TimeSeriesData( - DataConverter.to_dataarray(data, timesteps=self.timesteps, scenarios=self.scenarios), + DataConverter.to_dataarray(data, coords=coords), aggregation_group=data.aggregation_group, aggregation_weight=data.aggregation_weight ).rename(name) except ConversionError as e: logger.critical(f'Could not convert time series data "{name}" to DataArray: {e}. \n' f'Take care to use the correct (time) index.') else: - return DataConverter.to_dataarray(data, timesteps=self.timesteps if has_time_dim else None, scenarios=self.scenarios).rename(name) + return DataConverter.to_dataarray(data, coords=coords).rename(name) def fit_effects_to_model_coords( self, From ba195ff45a7dc4f40f87dfe9ceb311765d0e5c88 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 29 Jun 2025 21:33:01 +0200 Subject: [PATCH 142/448] Make the DataConverter more universal by accepting a list of coords/dims --- flixopt/core.py | 148 ++++--- tests/test_dataconverter.py | 831 +++++++++++++++--------------------- 2 files changed, 437 insertions(+), 542 deletions(-) diff --git a/flixopt/core.py b/flixopt/core.py index 9834d8f39..c11ba3993 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -148,21 +148,23 @@ def _convert_dataframe(data: pd.DataFrame, coords: Dict[str, pd.Index], dims: Tu @staticmethod def to_dataarray( data: Union[Scalar, np.ndarray, pd.Series, pd.DataFrame, xr.DataArray, TimeSeriesData], - timesteps: Optional[pd.DatetimeIndex] = None, - scenarios: Optional[pd.Index] = None, + coords: Optional[Dict[str, pd.Index]] = None, ) -> xr.DataArray: """ - Convert data to xarray.DataArray with specified dimensions. + Convert data to xarray.DataArray with specified coordinates. Args: data: Scalar, 1D array/Series, single-column DataFrame, or existing DataArray - timesteps: Optional DatetimeIndex for time dimension - scenarios: Optional Index for scenario dimension + coords: Dictionary mapping dimension names to coordinate indices + e.g., {'time': timesteps, 'scenario': scenarios} Returns: DataArray with the converted data """ - coords, dims = DataConverter._prepare_dimensions(timesteps, scenarios) + if coords is None: + coords = {} + + coords, dims = DataConverter._prepare_dimensions(coords) # Handle scalars if isinstance(data, (int, float, np.integer, np.floating)): @@ -192,30 +194,40 @@ def to_dataarray( ) @staticmethod - def _prepare_dimensions( - timesteps: Optional[pd.DatetimeIndex], scenarios: Optional[pd.Index] - ) -> Tuple[Dict[str, pd.Index], Tuple[str, ...]]: - """Prepare coordinates and dimensions.""" - coords = {} + def _prepare_dimensions(coords: Dict[str, pd.Index]) -> Tuple[Dict[str, pd.Index], Tuple[str, ...]]: + """ + Prepare and validate coordinates for the DataArray. + + Args: + coords: Dictionary mapping dimension names to coordinate indices + + Returns: + Tuple of (validated coordinates dict, dimensions tuple) + """ + # Check dimension limit + if len(coords) > 2: + raise ConversionError(f'Maximum 2 dimensions currently supported, got {len(coords)}') + + validated_coords = {} dims = [] - if timesteps is not None: - if not isinstance(timesteps, pd.DatetimeIndex) or len(timesteps) == 0: - raise ConversionError('Timesteps must be a non-empty DatetimeIndex') - if timesteps.name != 'time': - timesteps = timesteps.rename('time') - coords['time'] = timesteps - dims.append('time') + for dim_name, coord_index in coords.items(): + # Validate coordinate index + if not isinstance(coord_index, pd.Index) or len(coord_index) == 0: + raise ConversionError(f'{dim_name} coordinates must be a non-empty pandas Index') + + # Ensure coordinate index has the correct name + if coord_index.name != dim_name: + coord_index = coord_index.rename(dim_name) + + # Special validation for time dimension + if dim_name == 'time' and not isinstance(coord_index, pd.DatetimeIndex): + raise ConversionError('time coordinates must be a DatetimeIndex') - if scenarios is not None: - if not isinstance(scenarios, pd.Index) or len(scenarios) == 0: - raise ConversionError('Scenarios must be a non-empty Index') - if scenarios.name != 'scenario': - scenarios = scenarios.rename('scenario') - coords['scenario'] = scenarios - dims.append('scenario') + validated_coords[dim_name] = coord_index + dims.append(dim_name) - return coords, tuple(dims) + return validated_coords, tuple(dims) @staticmethod def _convert_scalar( @@ -244,24 +256,31 @@ def _convert_1d_array(data: np.ndarray, coords: Dict[str, pd.Index], dims: Tuple elif len(dims) == 2: # Broadcast 1D array to 2D based on which dimension it matches - time_len = len(coords['time']) - scenario_len = len(coords['scenario']) - - if len(data) == time_len: - # Broadcast across scenarios - values = np.repeat(data[:, np.newaxis], scenario_len, axis=1) - return xr.DataArray(values.copy(), coords=coords, dims=dims) - elif len(data) == scenario_len: - # Broadcast across time - values = np.repeat(data[np.newaxis, :], time_len, axis=0) - return xr.DataArray(values.copy(), coords=coords, dims=dims) - else: + dim_lengths = {dim: len(coords[dim]) for dim in dims} + + # Find which dimension the array length matches + matching_dims = [dim for dim, length in dim_lengths.items() if len(data) == length] + + if len(matching_dims) == 0: + raise ConversionError(f'Array length {len(data)} matches none of the dimensions: {dim_lengths}') + elif len(matching_dims) > 1: raise ConversionError( - f'Array length {len(data)} matches neither time ({time_len}) nor scenario ({scenario_len}) dimensions' + f'Array length {len(data)} matches multiple dimensions: {matching_dims}. Cannot determine broadcasting direction.' ) + # Broadcast along the matching dimension + match_dim = matching_dims[0] + other_dim = [d for d in dims if d != match_dim][0] + + if dims.index(match_dim) == 0: # First dimension + values = np.repeat(data[:, np.newaxis], len(coords[other_dim]), axis=1) + else: # Second dimension + values = np.repeat(data[np.newaxis, :], len(coords[other_dim]), axis=0) + + return xr.DataArray(values.copy(), coords=coords, dims=dims) + else: - raise ConversionError('Maximum 2 dimensions supported') + raise ConversionError(f'Maximum 2 dimensions currently supported, got {len(dims)}') @staticmethod def _convert_series(data: pd.Series, coords: Dict[str, pd.Index], dims: Tuple[str, ...]) -> xr.DataArray: @@ -279,16 +298,23 @@ def _convert_series(data: pd.Series, coords: Dict[str, pd.Index], dims: Tuple[st elif len(dims) == 2: # Check which dimension the Series index matches - if data.index.equals(coords['time']): - # Broadcast across scenarios - values = np.repeat(data.values[:, np.newaxis], len(coords['scenario']), axis=1) - return xr.DataArray(values.copy(), coords=coords, dims=dims) - elif data.index.equals(coords['scenario']): - # Broadcast across time - values = np.repeat(data.values[np.newaxis, :], len(coords['time']), axis=0) - return xr.DataArray(values.copy(), coords=coords, dims=dims) - else: - raise ConversionError('Series index must match either time or scenario coordinates') + if 'time' in coords and data.index.equals(coords['time']): + # Broadcast across other dimensions + other_dims = [d for d in dims if d != 'time'] + if len(other_dims) == 1: + other_dim = other_dims[0] + values = np.repeat(data.values[:, np.newaxis], len(coords[other_dim]), axis=1) + return xr.DataArray(values.copy(), coords=coords, dims=dims) + + elif len([d for d in dims if d != 'time']) == 1: + # Check if Series matches the non-time dimension + other_dim = [d for d in dims if d != 'time'][0] + if data.index.equals(coords[other_dim]): + # Broadcast across time + values = np.repeat(data.values[np.newaxis, :], len(coords['time']), axis=0) + return xr.DataArray(values.copy(), coords=coords, dims=dims) + + raise ConversionError(f'Series index must match one of the target dimensions: {list(coords.keys())}') else: raise ConversionError('Maximum 2 dimensions supported') @@ -330,18 +356,24 @@ def _broadcast_dataarray(data: xr.DataArray, coords: Dict[str, pd.Index], dims: elif len(data.dims) == 1 and len(dims) == 2: source_dim = data.dims[0] + # Check if source dimension exists in target + if source_dim not in coords: + raise ConversionError(f'Source dimension "{source_dim}" not found in target coordinates') + # Check coordinate compatibility if not np.array_equal(data.coords[source_dim].values, coords[source_dim].values): raise ConversionError(f'Source {source_dim} coordinates do not match target coordinates') - if source_dim == 'time': - # Broadcast time to include scenarios - values = np.repeat(data.values[:, np.newaxis], len(coords['scenario']), axis=1) - return xr.DataArray(values.copy(), coords=coords, dims=dims) - elif source_dim == 'scenario': - # Broadcast scenario to include time - values = np.repeat(data.values[np.newaxis, :], len(coords['time']), axis=0) - return xr.DataArray(values.copy(), coords=coords, dims=dims) + # Find the other dimension to broadcast to + other_dim = [d for d in dims if d != source_dim][0] + + # Broadcast based on dimension order + if dims.index(source_dim) == 0: # Source is first dimension + values = np.repeat(data.values[:, np.newaxis], len(coords[other_dim]), axis=1) + else: # Source is second dimension + values = np.repeat(data.values[np.newaxis, :], len(coords[other_dim]), axis=0) + + return xr.DataArray(values.copy(), coords=coords, dims=dims) raise ConversionError(f'Cannot broadcast from {data.dims} to {dims}') diff --git a/tests/test_dataconverter.py b/tests/test_dataconverter.py index 5536b8cb2..eed6c1283 100644 --- a/tests/test_dataconverter.py +++ b/tests/test_dataconverter.py @@ -11,621 +11,484 @@ @pytest.fixture -def sample_time_index(): +def time_coords(): return pd.date_range('2024-01-01', periods=5, freq='D', name='time') @pytest.fixture -def sample_scenario_index(): - return pd.Index(['baseline', 'high_demand', 'low_price'], name='scenario') +def scenario_coords(): + return pd.Index(['baseline', 'high', 'low'], name='scenario') -class TestSingleDimensionConversion: - """Tests for converting data without scenarios (1D: time only).""" +@pytest.fixture +def region_coords(): + return pd.Index(['north', 'south', 'east'], name='region') - def test_scalar_conversion(self, sample_time_index): - """Test converting a scalar value.""" - # Test with integer - result = DataConverter.to_dataarray(42, sample_time_index) - assert isinstance(result, xr.DataArray) - assert result.shape == (len(sample_time_index),) - assert result.dims == ('time',) - assert np.all(result.values == 42) - # Test with float - result = DataConverter.to_dataarray(42.5, sample_time_index) - assert np.all(result.values == 42.5) +class TestBasicConversion: + """Test basic data type conversions with different coordinate configurations.""" - # Test with numpy scalar types - result = DataConverter.to_dataarray(np.int64(42), sample_time_index) - assert np.all(result.values == 42) - result = DataConverter.to_dataarray(np.float32(42.5), sample_time_index) - assert np.all(result.values == 42.5) - - def test_ndarray_conversion(self, sample_time_index): - """Test converting a numpy ndarray.""" - # Test with integer 1D array - arr_1d = np.array([1, 2, 3, 4, 5]) - result = DataConverter.to_dataarray(arr_1d, sample_time_index) - assert result.shape == (5,) - assert result.dims == ('time',) - assert np.array_equal(result.values, arr_1d) - - # Test with float 1D array - arr_1d = np.array([1.1, 2.2, 3.3, 4.4, 5.5]) - result = DataConverter.to_dataarray(arr_1d, sample_time_index) - assert np.array_equal(result.values, arr_1d) - - # Test with array containing NaN - arr_1d = np.array([1, np.nan, 3, np.nan, 5]) - result = DataConverter.to_dataarray(arr_1d, sample_time_index) - assert np.array_equal(np.isnan(result.values), np.isnan(arr_1d)) - assert np.array_equal(result.values[~np.isnan(result.values)], arr_1d[~np.isnan(arr_1d)]) - - def test_dataarray_conversion(self, sample_time_index): - """Test converting an existing xarray DataArray.""" - # Create original DataArray - original = xr.DataArray(data=np.array([1, 2, 3, 4, 5]), coords={'time': sample_time_index}, dims=['time']) - - # Convert and check - result = DataConverter.to_dataarray(original, sample_time_index) + def test_scalar_no_coords(self): + """Scalar without coordinates should create 0D DataArray.""" + result = DataConverter.to_dataarray(42) + assert result.shape == () + assert result.dims == () + assert result.item() == 42 + + def test_scalar_single_coord(self, time_coords): + """Scalar with single coordinate should broadcast.""" + result = DataConverter.to_dataarray(42, coords={'time': time_coords}) assert result.shape == (5,) assert result.dims == ('time',) - assert np.array_equal(result.values, original.values) - - # Ensure it's a copy - result[0] = 999 - assert original[0].item() == 1 # Original should be unchanged - - # Test with different time coordinates but same length - different_times = pd.date_range('2025-01-01', periods=5, freq='D', name='time') - original = xr.DataArray(data=np.array([1, 2, 3, 4, 5]), coords={'time': different_times}, dims=['time']) - - # Should raise an error for mismatched time coordinates - with pytest.raises(ConversionError): - DataConverter.to_dataarray(original, sample_time_index) - - -class TestMultiDimensionConversion: - """Tests for converting data with scenarios (2D: scenario × time).""" - - def test_scalar_with_scenarios(self, sample_time_index, sample_scenario_index): - """Test converting scalar values with scenario dimension.""" - # Test with integer - result = DataConverter.to_dataarray(42, sample_time_index, sample_scenario_index) + assert np.all(result.values == 42) - assert isinstance(result, xr.DataArray) - assert result.shape == (len(sample_time_index), len(sample_scenario_index)) + def test_scalar_multiple_coords(self, time_coords, scenario_coords): + """Scalar with multiple coordinates should broadcast to all.""" + result = DataConverter.to_dataarray(42, coords={'time': time_coords, 'scenario': scenario_coords}) + assert result.shape == (5, 3) assert result.dims == ('time', 'scenario') assert np.all(result.values == 42) - assert set(result.coords['scenario'].values) == set(sample_scenario_index.values) - assert set(result.coords['time'].values) == set(sample_time_index.values) - # Test with float - result = DataConverter.to_dataarray(42.5, sample_time_index, sample_scenario_index) - assert np.all(result.values == 42.5) + def test_numpy_scalars(self, time_coords): + """Test numpy scalar types.""" + for scalar in [np.int32(42), np.int64(42), np.float32(42.5), np.float64(42.5)]: + result = DataConverter.to_dataarray(scalar, coords={'time': time_coords}) + assert result.shape == (5,) + assert np.all(result.values == scalar.item()) - def test_1d_array_with_scenarios_time_broadcast(self, sample_time_index, sample_scenario_index): - """Test converting 1D array matching time dimension (broadcasting across scenarios).""" - # Create 1D array matching timesteps length - arr_1d = np.array([1, 2, 3, 4, 5]) - # Convert with scenarios - result = DataConverter.to_dataarray(arr_1d, sample_time_index, sample_scenario_index) +class TestArrayConversion: + """Test numpy array conversions.""" - assert result.shape == (len(sample_time_index), len(sample_scenario_index)) - assert result.dims == ('time', 'scenario') + def test_1d_array_no_coords(self): + """1D array without coords should fail unless single element.""" + # Multi-element fails + with pytest.raises(ConversionError): + DataConverter.to_dataarray(np.array([1, 2, 3])) - # Each scenario should have the same values (broadcasting) - for scenario in sample_scenario_index: - scenario_slice = result.sel(scenario=scenario) - assert np.array_equal(scenario_slice.values, arr_1d) + # Single element succeeds + result = DataConverter.to_dataarray(np.array([42])) + assert result.shape == () + assert result.item() == 42 - def test_1d_array_with_scenarios_scenario_broadcast(self, sample_time_index, sample_scenario_index): - """Test converting 1D array matching scenario dimension (broadcasting across time).""" - # Create 1D array matching scenario length - arr_1d = np.array([10, 20, 30]) # 3 scenarios + def test_1d_array_matching_coord(self, time_coords): + """1D array matching coordinate length should work.""" + arr = np.array([10, 20, 30, 40, 50]) + result = DataConverter.to_dataarray(arr, coords={'time': time_coords}) + assert result.shape == (5,) + assert result.dims == ('time',) + assert np.array_equal(result.values, arr) - # Convert with time and scenarios - result = DataConverter.to_dataarray(arr_1d, sample_time_index, sample_scenario_index) + def test_1d_array_mismatched_coord(self, time_coords): + """1D array not matching coordinate length should fail.""" + arr = np.array([10, 20, 30]) # Length 3, time_coords has length 5 + with pytest.raises(ConversionError): + DataConverter.to_dataarray(arr, coords={'time': time_coords}) - assert result.shape == (len(sample_time_index), len(sample_scenario_index)) + def test_1d_array_broadcast_to_multiple_coords(self, time_coords, scenario_coords): + """1D array should broadcast to matching dimension.""" + # Array matching time dimension + time_arr = np.array([10, 20, 30, 40, 50]) + result = DataConverter.to_dataarray(time_arr, coords={'time': time_coords, 'scenario': scenario_coords}) + assert result.shape == (5, 3) assert result.dims == ('time', 'scenario') - # Each time step should have the same scenario values (broadcasting) - for time in sample_time_index: - time_slice = result.sel(time=time) - assert np.array_equal(time_slice.values, arr_1d) - - def test_dataarray_with_scenarios(self, sample_time_index, sample_scenario_index): - """Test converting an existing DataArray with scenarios.""" - # Create a multi-scenario DataArray with dims in (time, scenario) order - original = xr.DataArray( - data=np.array([[1, 6, 11], [2, 7, 12], [3, 8, 13], [4, 9, 14], [5, 10, 15]]), - coords={'time': sample_time_index, 'scenario': sample_scenario_index}, - dims=['time', 'scenario'], - ) - - # Test conversion - result = DataConverter.to_dataarray(original, sample_time_index, sample_scenario_index) + # Each scenario should have the same time values + for scenario in scenario_coords: + assert np.array_equal(result.sel(scenario=scenario).values, time_arr) + # Array matching scenario dimension + scenario_arr = np.array([100, 200, 300]) + result = DataConverter.to_dataarray(scenario_arr, coords={'time': time_coords, 'scenario': scenario_coords}) assert result.shape == (5, 3) assert result.dims == ('time', 'scenario') - assert np.array_equal(result.values, original.values) - # Ensure it's a copy - result.loc[:, 'baseline'] = 999 - assert original.sel(scenario='baseline')[0].item() == 1 # Original should be unchanged + # Each time should have the same scenario values + for time in time_coords: + assert np.array_equal(result.sel(time=time).values, scenario_arr) + + def test_1d_array_ambiguous_length(self): + """Array length matching multiple dimensions should fail.""" + # Both dimensions have length 3 + coords_3x3 = { + 'time': pd.date_range('2024-01-01', periods=3, freq='D', name='time'), + 'scenario': pd.Index(['A', 'B', 'C'], name='scenario') + } + arr = np.array([1, 2, 3]) + + with pytest.raises(ConversionError, match="matches multiple dimensions"): + DataConverter.to_dataarray(arr, coords=coords_3x3) + + def test_multidimensional_array_rejected(self, time_coords): + """Multidimensional arrays should be rejected.""" + arr_2d = np.array([[1, 2, 3], [4, 5, 6]]) + with pytest.raises(ConversionError, match="Only 1D arrays supported"): + DataConverter.to_dataarray(arr_2d, coords={'time': time_coords}) class TestSeriesConversion: - """Tests for converting pandas Series to DataArray.""" + """Test pandas Series conversions.""" - def test_series_single_dimension_time(self, sample_time_index): - """Test converting a pandas Series with time index.""" - # Create a Series with matching time index - series = pd.Series([10, 20, 30, 40, 50], index=sample_time_index) + def test_series_no_coords(self): + """Series without coords should fail unless single element.""" + # Multi-element fails + series = pd.Series([1, 2, 3]) + with pytest.raises(ConversionError): + DataConverter.to_dataarray(series) - # Convert and check - result = DataConverter.to_dataarray(series, sample_time_index) - assert isinstance(result, xr.DataArray) + # Single element succeeds + single_series = pd.Series([42]) + result = DataConverter.to_dataarray(single_series) + assert result.shape == () + assert result.item() == 42 + + def test_series_matching_index(self, time_coords, scenario_coords): + """Series with matching index should work.""" + # Time-indexed series + time_series = pd.Series([10, 20, 30, 40, 50], index=time_coords) + result = DataConverter.to_dataarray(time_series, coords={'time': time_coords}) assert result.shape == (5,) assert result.dims == ('time',) - assert np.array_equal(result.values, series.values) - assert np.array_equal(result.coords['time'].values, sample_time_index.values) + assert np.array_equal(result.values, time_series.values) - def test_series_single_dimension_scenario(self, sample_scenario_index): - """Test converting a pandas Series with scenario index.""" - # Create a Series with scenario index - series = pd.Series([100, 200, 300], index=sample_scenario_index) - - result = DataConverter.to_dataarray(series, scenarios=sample_scenario_index) + # Scenario-indexed series + scenario_series = pd.Series([100, 200, 300], index=scenario_coords) + result = DataConverter.to_dataarray(scenario_series, coords={'scenario': scenario_coords}) assert result.shape == (3,) assert result.dims == ('scenario',) - assert np.array_equal(result.values, series.values) - assert np.array_equal(result.coords['scenario'].values, sample_scenario_index.values) + assert np.array_equal(result.values, scenario_series.values) - def test_series_mismatched_index(self, sample_time_index): - """Test converting a Series with mismatched index.""" - # Create Series with different time index - different_times = pd.date_range('2025-01-01', periods=5, freq='D', name='time') - series = pd.Series([10, 20, 30, 40, 50], index=different_times) + def test_series_mismatched_index(self, time_coords): + """Series with non-matching index should fail.""" + wrong_times = pd.date_range('2025-01-01', periods=5, freq='D', name='time') + series = pd.Series([10, 20, 30, 40, 50], index=wrong_times) - # Should raise error for mismatched index with pytest.raises(ConversionError): - DataConverter.to_dataarray(series, sample_time_index) + DataConverter.to_dataarray(series, coords={'time': time_coords}) - def test_series_broadcast_to_scenarios(self, sample_time_index, sample_scenario_index): - """Test broadcasting a time-indexed Series across scenarios.""" - # Create a Series with time index - series = pd.Series([10, 20, 30, 40, 50], index=sample_time_index) + def test_series_broadcast_to_multiple_coords(self, time_coords, scenario_coords): + """Series should broadcast to non-matching dimensions.""" + # Time series broadcast to scenarios + time_series = pd.Series([10, 20, 30, 40, 50], index=time_coords) + result = DataConverter.to_dataarray(time_series, coords={'time': time_coords, 'scenario': scenario_coords}) + assert result.shape == (5, 3) - # Convert with scenarios - result = DataConverter.to_dataarray(series, sample_time_index, sample_scenario_index) + for scenario in scenario_coords: + assert np.array_equal(result.sel(scenario=scenario).values, time_series.values) + # Scenario series broadcast to time + scenario_series = pd.Series([100, 200, 300], index=scenario_coords) + result = DataConverter.to_dataarray(scenario_series, coords={'time': time_coords, 'scenario': scenario_coords}) assert result.shape == (5, 3) - assert result.dims == ('time', 'scenario') - # Check broadcasting - each scenario should have the same values - for scenario in sample_scenario_index: - scenario_slice = result.sel(scenario=scenario) - assert np.array_equal(scenario_slice.values, series.values) + for time in time_coords: + assert np.array_equal(result.sel(time=time).values, scenario_series.values) - def test_series_broadcast_to_time(self, sample_time_index, sample_scenario_index): - """Test broadcasting a scenario-indexed Series across time.""" - # Create a Series with scenario index - series = pd.Series([100, 200, 300], index=sample_scenario_index) + def test_series_wrong_dimension(self, time_coords, region_coords): + """Series indexed by dimension not in coords should fail.""" + wrong_series = pd.Series([1, 2, 3], index=region_coords) - # Convert with time - result = DataConverter.to_dataarray(series, sample_time_index, sample_scenario_index) + with pytest.raises(ConversionError): + DataConverter.to_dataarray(wrong_series, coords={'time': time_coords}) - assert result.shape == (5, 3) - assert result.dims == ('time', 'scenario') - # Check broadcasting - each time should have the same scenario values - for time in sample_time_index: - time_slice = result.sel(time=time) - assert np.array_equal(time_slice.values, series.values) +class TestDataFrameConversion: + """Test pandas DataFrame conversions.""" + def test_single_column_dataframe(self, time_coords): + """Single-column DataFrame should work like Series.""" + df = pd.DataFrame({'value': [10, 20, 30, 40, 50]}, index=time_coords) + result = DataConverter.to_dataarray(df, coords={'time': time_coords}) -class TestTimeSeriesDataConversion: - """Tests for converting TimeSeriesData objects.""" + assert result.shape == (5,) + assert result.dims == ('time',) + assert np.array_equal(result.values, df['value'].values) - def test_timeseries_data_conversion(self, sample_time_index): - """Test converting TimeSeriesData.""" - # Create TimeSeriesData - data_array = xr.DataArray([1, 2, 3, 4, 5], coords={'time': sample_time_index}, dims=['time']) - ts_data = TimeSeriesData(data_array, aggregation_group='test_group') + def test_multi_column_dataframe_rejected(self, time_coords): + """Multi-column DataFrame should be rejected.""" + df = pd.DataFrame({ + 'value1': [10, 20, 30, 40, 50], + 'value2': [15, 25, 35, 45, 55] + }, index=time_coords) - # Convert - result = DataConverter.to_dataarray(ts_data, sample_time_index) + with pytest.raises(ConversionError, match="Only single-column DataFrames are supported"): + DataConverter.to_dataarray(df, coords={'time': time_coords}) - assert isinstance(result, xr.DataArray) - assert result.shape == (5,) - assert result.dims == ('time',) - assert np.array_equal(result.values, [1, 2, 3, 4, 5]) + def test_empty_dataframe_rejected(self, time_coords): + """Empty DataFrame should be rejected.""" + df = pd.DataFrame(index=time_coords) # No columns - def test_timeseries_data_with_scenarios(self, sample_time_index, sample_scenario_index): - """Test converting TimeSeriesData with broadcasting to scenarios.""" - # Create 1D TimeSeriesData - data_array = xr.DataArray([1, 2, 3, 4, 5], coords={'time': sample_time_index}, dims=['time']) - ts_data = TimeSeriesData(data_array) + with pytest.raises(ConversionError, match="Only single-column DataFrames are supported"): + DataConverter.to_dataarray(df, coords={'time': time_coords}) - # Convert with scenarios (should broadcast) - result = DataConverter.to_dataarray(ts_data, sample_time_index, sample_scenario_index) + def test_dataframe_broadcast(self, time_coords, scenario_coords): + """Single-column DataFrame should broadcast like Series.""" + df = pd.DataFrame({'power': [10, 20, 30, 40, 50]}, index=time_coords) + result = DataConverter.to_dataarray(df, coords={'time': time_coords, 'scenario': scenario_coords}) assert result.shape == (5, 3) - assert result.dims == ('time', 'scenario') + for scenario in scenario_coords: + assert np.array_equal(result.sel(scenario=scenario).values, df['power'].values) - # Each scenario should have the same values - for scenario in sample_scenario_index: - assert np.array_equal(result.sel(scenario=scenario).values, [1, 2, 3, 4, 5]) +class TestDataArrayConversion: + """Test xarray DataArray conversions.""" -class TestDataFrameConversion: - """Tests for converting single-column pandas DataFrames to DataArray.""" - - def test_single_column_dataframe_time(self, sample_time_index): - """Test converting a single-column DataFrame with time index.""" - # Create DataFrame with one column - df = pd.DataFrame({'value': [10, 20, 30, 40, 50]}, index=sample_time_index) + def test_compatible_dataarray(self, time_coords): + """Compatible DataArray should pass through.""" + original = xr.DataArray([10, 20, 30, 40, 50], coords={'time': time_coords}, dims=['time']) + result = DataConverter.to_dataarray(original, coords={'time': time_coords}) - # Convert and check - result = DataConverter.to_dataarray(df, sample_time_index) - assert isinstance(result, xr.DataArray) assert result.shape == (5,) assert result.dims == ('time',) - assert np.array_equal(result.values, df['value'].values) - - def test_single_column_dataframe_scenario(self, sample_scenario_index): - """Test converting a single-column DataFrame with scenario index.""" - # Create DataFrame with one column and scenario index - df = pd.DataFrame({'value': [100, 200, 300]}, index=sample_scenario_index) - - result = DataConverter.to_dataarray(df, scenarios=sample_scenario_index) - assert result.shape == (3,) - assert result.dims == ('scenario',) - assert np.array_equal(result.values, df['value'].values) + assert np.array_equal(result.values, original.values) - def test_dataframe_broadcast_to_scenarios(self, sample_time_index, sample_scenario_index): - """Test broadcasting a time-indexed DataFrame across scenarios.""" - # Create DataFrame with time index - df = pd.DataFrame({'power': [10, 20, 30, 40, 50]}, index=sample_time_index) + # Should be a copy + result[0] = 999 + assert original[0].item() == 10 - # Convert with scenarios - result = DataConverter.to_dataarray(df, sample_time_index, sample_scenario_index) + def test_incompatible_dataarray_coords(self, time_coords): + """DataArray with wrong coordinates should fail.""" + wrong_times = pd.date_range('2025-01-01', periods=5, freq='D', name='time') + original = xr.DataArray([10, 20, 30, 40, 50], coords={'time': wrong_times}, dims=['time']) - assert result.shape == (5, 3) - assert result.dims == ('time', 'scenario') + with pytest.raises(ConversionError): + DataConverter.to_dataarray(original, coords={'time': time_coords}) - # Check broadcasting - each scenario should have the same values - for scenario in sample_scenario_index: - scenario_slice = result.sel(scenario=scenario) - assert np.array_equal(scenario_slice.values, df['power'].values) + def test_incompatible_dataarray_dims(self, time_coords): + """DataArray with wrong dimensions should fail.""" + original = xr.DataArray([10, 20, 30, 40, 50], coords={'wrong_dim': range(5)}, dims=['wrong_dim']) - def test_dataframe_broadcast_to_time(self, sample_time_index, sample_scenario_index): - """Test broadcasting a scenario-indexed DataFrame across time.""" - # Create DataFrame with scenario index - df = pd.DataFrame({'cost': [100, 200, 300]}, index=sample_scenario_index) + with pytest.raises(ConversionError): + DataConverter.to_dataarray(original, coords={'time': time_coords}) - # Convert with time - result = DataConverter.to_dataarray(df, sample_time_index, sample_scenario_index) + def test_dataarray_broadcast(self, time_coords, scenario_coords): + """DataArray should broadcast to additional dimensions.""" + # 1D time DataArray to 2D time+scenario + original = xr.DataArray([10, 20, 30, 40, 50], coords={'time': time_coords}, dims=['time']) + result = DataConverter.to_dataarray(original, coords={'time': time_coords, 'scenario': scenario_coords}) assert result.shape == (5, 3) assert result.dims == ('time', 'scenario') - # Check broadcasting - each time should have the same scenario values - for time in sample_time_index: - time_slice = result.sel(time=time) - assert np.array_equal(time_slice.values, df['cost'].values) + for scenario in scenario_coords: + assert np.array_equal(result.sel(scenario=scenario).values, original.values) - def test_multi_column_dataframe_fails(self, sample_time_index): - """Test that multi-column DataFrames are rejected.""" - # Create DataFrame with multiple columns - df = pd.DataFrame({ - 'value1': [10, 20, 30, 40, 50], - 'value2': [15, 25, 35, 45, 55] - }, index=sample_time_index) + def test_scalar_dataarray_broadcast(self, time_coords, scenario_coords): + """Scalar DataArray should broadcast to all dimensions.""" + scalar_da = xr.DataArray(42) + result = DataConverter.to_dataarray(scalar_da, coords={'time': time_coords, 'scenario': scenario_coords}) - # Should raise error - with pytest.raises(ConversionError, match="Only single-column DataFrames are supported"): - DataConverter.to_dataarray(df, sample_time_index) + assert result.shape == (5, 3) + assert np.all(result.values == 42) - def test_dataframe_mismatched_index(self, sample_time_index): - """Test DataFrame with mismatched index.""" - # Create DataFrame with different time index - different_times = pd.date_range('2025-01-01', periods=5, freq='D', name='time') - df = pd.DataFrame({'value': [10, 20, 30, 40, 50]}, index=different_times) - # Should raise error for mismatched index - with pytest.raises(ConversionError): - DataConverter.to_dataarray(df, sample_time_index) +class TestTimeSeriesDataConversion: + """Test TimeSeriesData conversions.""" - def test_dataframe_copy_behavior(self, sample_time_index): - """Test that DataFrame conversion creates a copy.""" - # Create DataFrame - df = pd.DataFrame({'value': [10, 20, 30, 40, 50]}, index=sample_time_index) + def test_timeseries_data_basic(self, time_coords): + """TimeSeriesData should work like DataArray.""" + data_array = xr.DataArray([10, 20, 30, 40, 50], coords={'time': time_coords}, dims=['time']) + ts_data = TimeSeriesData(data_array, aggregation_group='test') - # Convert - result = DataConverter.to_dataarray(df, sample_time_index) + result = DataConverter.to_dataarray(ts_data, coords={'time': time_coords}) - # Modify the result - result[0] = 999 + assert result.shape == (5,) + assert result.dims == ('time',) + assert np.array_equal(result.values, [10, 20, 30, 40, 50]) - # Original DataFrame should be unchanged - assert df.loc[sample_time_index[0], 'value'] == 10 + def test_timeseries_data_broadcast(self, time_coords, scenario_coords): + """TimeSeriesData should broadcast to additional dimensions.""" + data_array = xr.DataArray([10, 20, 30, 40, 50], coords={'time': time_coords}, dims=['time']) + ts_data = TimeSeriesData(data_array) - def test_empty_dataframe_fails(self, sample_time_index): - """Test that empty DataFrames are rejected.""" - # DataFrame with no columns - df = pd.DataFrame(index=sample_time_index) + result = DataConverter.to_dataarray(ts_data, coords={'time': time_coords, 'scenario': scenario_coords}) - with pytest.raises(ConversionError, match="Only single-column DataFrames are supported"): - DataConverter.to_dataarray(df, sample_time_index) + assert result.shape == (5, 3) + for scenario in scenario_coords: + assert np.array_equal(result.sel(scenario=scenario).values, [10, 20, 30, 40, 50]) - def test_dataframe_with_named_column(self, sample_time_index): - """Test DataFrame with a named column.""" - df = pd.DataFrame(index=sample_time_index) - df['energy_output'] = [100, 150, 200, 175, 125] - result = DataConverter.to_dataarray(df, sample_time_index) - assert result.shape == (5,) - assert np.array_equal(result.values, [100, 150, 200, 175, 125]) +class TestCustomDimensions: + """Test with custom dimension names beyond time/scenario.""" + def test_custom_single_dimension(self, region_coords): + """Test with custom dimension name.""" + result = DataConverter.to_dataarray(42, coords={'region': region_coords}) + assert result.shape == (3,) + assert result.dims == ('region',) + assert np.all(result.values == 42) -class TestInvalidInputs: - """Tests for invalid inputs and error handling.""" + def test_custom_multiple_dimensions(self): + """Test with multiple custom dimensions.""" + products = pd.Index(['A', 'B'], name='product') + technologies = pd.Index(['solar', 'wind', 'gas'], name='technology') - def test_time_index_validation(self): - """Test validation of time index.""" - # Test with unnamed index - unnamed_index = pd.date_range('2024-01-01', periods=5, freq='D') - # Should automatically rename to 'time' with a warning, not raise error - result = DataConverter.to_dataarray(42, unnamed_index) - assert result.coords['time'].name == 'time' + # Array matching technology dimension + arr = np.array([100, 150, 80]) + result = DataConverter.to_dataarray(arr, coords={'product': products, 'technology': technologies}) - # Test with empty index - empty_index = pd.DatetimeIndex([], name='time') - with pytest.raises(ConversionError): - DataConverter.to_dataarray(42, empty_index) + assert result.shape == (2, 3) + assert result.dims == ('product', 'technology') - # Test with non-DatetimeIndex - wrong_type_index = pd.Index([1, 2, 3, 4, 5], name='time') - with pytest.raises(ConversionError): - DataConverter.to_dataarray(42, wrong_type_index) - - def test_scenario_index_validation(self, sample_time_index): - """Test validation of scenario index.""" - # Test with unnamed scenario index - unnamed_index = pd.Index(['baseline', 'high_demand']) - # Should automatically rename to 'scenario' with a warning, not raise error - result = DataConverter.to_dataarray(42, sample_time_index, unnamed_index) - assert result.coords['scenario'].name == 'scenario' - - # Test with empty scenario index - empty_index = pd.Index([], name='scenario') - with pytest.raises(ConversionError): - DataConverter.to_dataarray(42, sample_time_index, empty_index) + # Should broadcast across products + for product in products: + assert np.array_equal(result.sel(product=product).values, arr) - # Test with non-Index scenario - with pytest.raises(ConversionError): - DataConverter.to_dataarray(42, sample_time_index, ['baseline', 'high_demand']) + def test_mixed_dimension_types(self): + """Test mixing time dimension with custom dimensions.""" + time_coords = pd.date_range('2024-01-01', periods=3, freq='D', name='time') + regions = pd.Index(['north', 'south'], name='region') - def test_invalid_data_types(self, sample_time_index, sample_scenario_index): - """Test handling of invalid data types.""" - # Test invalid input type (string) - with pytest.raises(ConversionError): - DataConverter.to_dataarray('invalid_string', sample_time_index) + # Time series should broadcast to regions + time_series = pd.Series([10, 20, 30], index=time_coords) + result = DataConverter.to_dataarray(time_series, coords={'time': time_coords, 'region': regions}) - # Test invalid input type with scenarios - with pytest.raises(ConversionError): - DataConverter.to_dataarray('invalid_string', sample_time_index, sample_scenario_index) + assert result.shape == (3, 2) + assert result.dims == ('time', 'region') - # Test unsupported complex object - with pytest.raises(ConversionError): - DataConverter.to_dataarray(object(), sample_time_index) - # Test None value - with pytest.raises(ConversionError): - DataConverter.to_dataarray(None, sample_time_index) +class TestValidation: + """Test coordinate validation.""" - def test_multidimensional_array_rejection(self, sample_time_index, sample_scenario_index): - """Test that multidimensional arrays are rejected.""" - # Test 2D array (not supported in simplified version) - arr_2d = np.array([[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]]) - with pytest.raises(ConversionError, match="Only 1D arrays supported"): - DataConverter.to_dataarray(arr_2d, sample_time_index) + def test_empty_coords(self): + """Empty coordinates should work for scalars.""" + result = DataConverter.to_dataarray(42, coords={}) + assert result.shape == () + assert result.item() == 42 - # Test 3D array - arr_3d = np.ones((2, 3, 4)) - with pytest.raises(ConversionError, match="Only 1D arrays supported"): - DataConverter.to_dataarray(arr_3d, sample_time_index, sample_scenario_index) - - def test_mismatched_input_dimensions(self, sample_time_index, sample_scenario_index): - """Test handling of mismatched input dimensions.""" - # Test mismatched Series index - mismatched_series = pd.Series( - [1, 2, 3, 4, 5, 6], index=pd.date_range('2025-01-01', periods=6, freq='D', name='time') - ) + def test_invalid_coord_type(self): + """Non-pandas Index coordinates should fail.""" with pytest.raises(ConversionError): - DataConverter.to_dataarray(mismatched_series, sample_time_index) + DataConverter.to_dataarray(42, coords={'time': [1, 2, 3]}) - # Test mismatched array length for time-only + def test_empty_coord_index(self): + """Empty coordinate index should fail.""" + empty_index = pd.Index([], name='time') with pytest.raises(ConversionError): - DataConverter.to_dataarray(np.array([1, 2, 3]), sample_time_index) # Wrong length + DataConverter.to_dataarray(42, coords={'time': empty_index}) + + def test_time_coord_validation(self): + """Time coordinates must be DatetimeIndex.""" + # Non-datetime index with name 'time' should fail + wrong_time = pd.Index([1, 2, 3], name='time') + with pytest.raises(ConversionError, match="time coordinates must be a DatetimeIndex"): + DataConverter.to_dataarray(42, coords={'time': wrong_time}) + + def test_coord_naming(self, time_coords): + """Coordinates should be auto-renamed to match dimension.""" + # Unnamed time index should be renamed + unnamed_time = time_coords.rename(None) + result = DataConverter.to_dataarray(42, coords={'time': unnamed_time}) + assert result.coords['time'].name == 'time' - # Test array that doesn't match either dimension - wrong_length_array = np.array([1, 2, 3, 4]) # Doesn't match time (5) or scenario (3) - with pytest.raises(ConversionError): - DataConverter.to_dataarray(wrong_length_array, sample_time_index, sample_scenario_index) - def test_dataarray_dimension_mismatch(self, sample_time_index, sample_scenario_index): - """Test handling of mismatched DataArray dimensions.""" - # Create DataArray with wrong dimensions - wrong_dims = xr.DataArray(data=np.array([1, 2, 3, 4, 5]), coords={'wrong_dim': range(5)}, dims=['wrong_dim']) - with pytest.raises(ConversionError): - DataConverter.to_dataarray(wrong_dims, sample_time_index) - - # Create DataArray with right dims but wrong coordinate values - wrong_coords = xr.DataArray( - data=np.array([1, 2, 3, 4, 5]), - coords={'time': pd.date_range('2025-01-01', periods=5, freq='D', name='time')}, - dims=['time'] - ) - with pytest.raises(ConversionError): - DataConverter.to_dataarray(wrong_coords, sample_time_index) +class TestErrorHandling: + """Test error handling and edge cases.""" + def test_unsupported_data_types(self, time_coords): + """Unsupported data types should fail with clear messages.""" + unsupported = [ + 'string', + object(), + None, + {'dict': 'value'}, + [1, 2, 3] + ] -class TestDataArrayBroadcasting: - """Tests for broadcasting DataArrays.""" + for data in unsupported: + with pytest.raises(ConversionError): + DataConverter.to_dataarray(data, coords={'time': time_coords}) - def test_broadcast_1d_array_to_2d_time(self, sample_time_index, sample_scenario_index): - """Test broadcasting a 1D array (time) to 2D.""" - arr_1d = np.array([1, 2, 3, 4, 5]) + def test_dimension_mismatch_messages(self, time_coords, scenario_coords): + """Error messages should be informative.""" + # Array with wrong length + wrong_arr = np.array([1, 2]) # Length 2, but no dimension has length 2 + with pytest.raises(ConversionError, match="matches none of the dimensions"): + DataConverter.to_dataarray(wrong_arr, coords={'time': time_coords, 'scenario': scenario_coords}) - result = DataConverter.to_dataarray(arr_1d, sample_time_index, sample_scenario_index) + def test_maximum_dimensions(self): + """Should handle up to 2 dimensions currently.""" + coords = { + 'dim1': pd.Index(['a', 'b'], name='dim1'), + 'dim2': pd.Index(['x', 'y'], name='dim2'), + 'dim3': pd.Index(['1', '2'], name='dim3') + } - # Should broadcast across scenarios - expected = np.repeat(arr_1d[:, np.newaxis], len(sample_scenario_index), axis=1) - assert np.array_equal(result.values, expected) - assert result.dims == ('time', 'scenario') + with pytest.raises(ConversionError, match="Maximum 2 dimensions currently supported"): + DataConverter.to_dataarray(42, coords=coords) - def test_broadcast_1d_array_to_2d_scenario(self, sample_time_index, sample_scenario_index): - """Test broadcasting a 1D array (scenario) to 2D.""" - arr_1d = np.array([1, 2, 3]) # Matches scenario length - result = DataConverter.to_dataarray(arr_1d, sample_time_index, sample_scenario_index) +class TestDataIntegrity: + """Test data copying and integrity.""" - # Should broadcast across time - expected = np.repeat(arr_1d[np.newaxis, :], len(sample_time_index), axis=0) - assert np.array_equal(result.values, expected) - assert result.dims == ('time', 'scenario') + def test_array_copy_independence(self, time_coords): + """Converted arrays should be independent copies.""" + original_arr = np.array([10, 20, 30, 40, 50]) + result = DataConverter.to_dataarray(original_arr, coords={'time': time_coords}) - def test_broadcast_1d_array_to_1d(self, sample_time_index): - """Test that 1D array with matching dimension doesn't change.""" - arr_1d = np.array([1, 2, 3, 4, 5]) + # Modify result + result[0] = 999 - result = DataConverter.to_dataarray(arr_1d, sample_time_index) + # Original should be unchanged + assert original_arr[0] == 10 - assert np.array_equal(result.values, arr_1d) - assert result.dims == ('time',) + def test_series_copy_independence(self, time_coords): + """Converted Series should be independent copies.""" + original_series = pd.Series([10, 20, 30, 40, 50], index=time_coords) + result = DataConverter.to_dataarray(original_series, coords={'time': time_coords}) - def test_scalar_dataarray_broadcasting(self, sample_time_index, sample_scenario_index): - """Test broadcasting scalar DataArray.""" - scalar_da = xr.DataArray(42) + # Modify result + result[0] = 999 - result = DataConverter.to_dataarray(scalar_da, sample_time_index, sample_scenario_index) + # Original should be unchanged + assert original_series.iloc[0] == 10 - assert result.shape == (len(sample_time_index), len(sample_scenario_index)) - assert np.all(result.values == 42) + def test_dataframe_copy_independence(self, time_coords): + """Converted DataFrames should be independent copies.""" + original_df = pd.DataFrame({'value': [10, 20, 30, 40, 50]}, index=time_coords) + result = DataConverter.to_dataarray(original_df, coords={'time': time_coords}) + # Modify result + result[0] = 999 -class TestEdgeCases: - """Tests for edge cases and special scenarios.""" + # Original should be unchanged + assert original_df.loc[time_coords[0], 'value'] == 10 - def test_single_timestep(self, sample_scenario_index): - """Test with a single timestep.""" - # Test with only one timestep - single_timestep = pd.DatetimeIndex(['2024-01-01'], name='time') - # Scalar conversion - result = DataConverter.to_dataarray(42, single_timestep) - assert result.shape == (1,) - assert result.dims == ('time',) +class TestSpecialValues: + """Test handling of special numeric values.""" - # With scenarios - result_with_scenarios = DataConverter.to_dataarray(42, single_timestep, sample_scenario_index) - assert result_with_scenarios.shape == (1, len(sample_scenario_index)) - assert result_with_scenarios.dims == ('time', 'scenario') + def test_nan_values(self, time_coords): + """NaN values should be preserved.""" + arr_with_nan = np.array([1, np.nan, 3, np.nan, 5]) + result = DataConverter.to_dataarray(arr_with_nan, coords={'time': time_coords}) - def test_single_scenario(self, sample_time_index): - """Test with a single scenario.""" - # Test with only one scenario - single_scenario = pd.Index(['baseline'], name='scenario') + assert np.array_equal(np.isnan(result.values), np.isnan(arr_with_nan)) + assert np.array_equal(result.values[~np.isnan(result.values)], arr_with_nan[~np.isnan(arr_with_nan)]) - # Scalar conversion with single scenario - result = DataConverter.to_dataarray(42, sample_time_index, single_scenario) - assert result.shape == (len(sample_time_index), 1) - assert result.dims == ('time', 'scenario') + def test_infinite_values(self, time_coords): + """Infinite values should be preserved.""" + arr_with_inf = np.array([1, np.inf, 3, -np.inf, 5]) + result = DataConverter.to_dataarray(arr_with_inf, coords={'time': time_coords}) - # Array conversion with single scenario - arr = np.array([1, 2, 3, 4, 5]) - result_arr = DataConverter.to_dataarray(arr, sample_time_index, single_scenario) - assert result_arr.shape == (5, 1) - assert np.array_equal(result_arr.sel(scenario='baseline').values, arr) - - def test_all_nan_data(self, sample_time_index, sample_scenario_index): - """Test handling of all-NaN data.""" - # Create array of all NaNs - all_nan_array = np.full(5, np.nan) - result = DataConverter.to_dataarray(all_nan_array, sample_time_index) - assert np.all(np.isnan(result.values)) - - # With scenarios - result = DataConverter.to_dataarray(all_nan_array, sample_time_index, sample_scenario_index) - assert result.shape == (len(sample_time_index), len(sample_scenario_index)) - assert np.all(np.isnan(result.values)) - - def test_mixed_data_types(self, sample_time_index, sample_scenario_index): - """Test conversion of mixed integer and float data.""" - # Create array with mixed types - mixed_array = np.array([1, 2.5, 3, 4.5, 5]) - result = DataConverter.to_dataarray(mixed_array, sample_time_index) - - # Result should be float dtype - assert np.issubdtype(result.dtype, np.floating) - assert np.array_equal(result.values, mixed_array) + assert np.array_equal(result.values, arr_with_inf) - # With scenarios - result = DataConverter.to_dataarray(mixed_array, sample_time_index, sample_scenario_index) - assert np.issubdtype(result.dtype, np.floating) - for scenario in sample_scenario_index: - assert np.array_equal(result.sel(scenario=scenario).values, mixed_array) + def test_boolean_values(self, time_coords): + """Boolean values should be preserved.""" + bool_arr = np.array([True, False, True, False, True]) + result = DataConverter.to_dataarray(bool_arr, coords={'time': time_coords}) - def test_boolean_data(self, sample_time_index, sample_scenario_index): - """Test handling of boolean data.""" - bool_array = np.array([True, False, True, False, True]) - result = DataConverter.to_dataarray(bool_array, sample_time_index, sample_scenario_index) assert result.dtype == bool - assert result.shape == (len(sample_time_index), len(sample_scenario_index)) + assert np.array_equal(result.values, bool_arr) + def test_mixed_numeric_types(self, time_coords): + """Mixed integer/float should become float.""" + mixed_arr = np.array([1, 2.5, 3, 4.5, 5]) + result = DataConverter.to_dataarray(mixed_arr, coords={'time': time_coords}) -class TestNoIndexConversion: - """Tests for conversion without any indices (scalar results).""" - - def test_scalar_no_dimensions(self): - """Test scalar conversion without any dimensions.""" - result = DataConverter.to_dataarray(42) - assert isinstance(result, xr.DataArray) - assert result.shape == () - assert result.dims == () - assert result.item() == 42 - - def test_single_element_array_no_dimensions(self): - """Test single-element array without dimensions.""" - arr = np.array([42]) - result = DataConverter.to_dataarray(arr) - assert result.shape == () - assert result.item() == 42 - - def test_multi_element_array_no_dimensions_fails(self): - """Test that multi-element array fails without dimensions.""" - arr = np.array([1, 2, 3]) - with pytest.raises(ConversionError): - DataConverter.to_dataarray(arr) - - def test_series_no_dimensions_fails(self): - """Test that multi-element Series fails without dimensions.""" - series = pd.Series([1, 2, 3]) - with pytest.raises(ConversionError): - DataConverter.to_dataarray(series) - - def test_single_element_series_no_dimensions(self): - """Test single-element Series without dimensions.""" - series = pd.Series([42]) - result = DataConverter.to_dataarray(series) - assert result.shape == () - assert result.item() == 42 + assert np.issubdtype(result.dtype, np.floating) + assert np.array_equal(result.values, mixed_arr) if __name__ == '__main__': From 605f03469ecbc202cdf65841f19937566d6d8e41 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 29 Jun 2025 21:49:46 +0200 Subject: [PATCH 143/448] Update DataConverter for n-d arrays --- flixopt/core.py | 187 ++++++++++++++++++++++++++++++------ tests/test_dataconverter.py | 98 +++++++++++++++++++ 2 files changed, 255 insertions(+), 30 deletions(-) diff --git a/flixopt/core.py b/flixopt/core.py index c11ba3993..9278f079c 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -126,24 +126,148 @@ def agg_weight(self): class DataConverter: """ - Converts scalars and 1D data into xarray.DataArray with optional time and scenario dimensions. + Converts data into xarray.DataArray with specified coordinates. - Only handles: - - Scalars (int, float, np.number) - - 1D arrays (np.ndarray, pd.Series) - - xr.DataArray (for broadcasting/checking) + Supports: + - Scalars (broadcast to all dimensions) + - 1D data (np.ndarray, pd.Series, single-column DataFrame) + - xr.DataArray (validated and potentially broadcast) + + Simple 1D data is matched to one dimension and broadcast to others. + DataArrays can have any number of dimensions. """ @staticmethod - def _convert_dataframe(data: pd.DataFrame, coords: Dict[str, pd.Index], dims: Tuple[str, ...]) -> xr.DataArray: - """Convert single-column pandas DataFrame to DataArray by treating it as a Series.""" - # Check that DataFrame has exactly one column - if len(data.columns) != 1: - raise ConversionError(f'Only single-column DataFrames are supported, got {len(data.columns)} columns') + def _convert_1d_data_to_dataarray( + data: Union[np.ndarray, pd.Series], coords: Dict[str, pd.Index], target_dims: Tuple[str, ...] + ) -> xr.DataArray: + """ + Convert 1D data (array or Series) to DataArray by matching to one dimension. + + Args: + data: 1D numpy array or pandas Series + coords: Available coordinates + target_dims: Target dimension names + + Returns: + DataArray with the data matched to appropriate dimension + """ + if len(target_dims) == 0: + # No target dimensions - data must be single element + if len(data) != 1: + raise ConversionError('Cannot convert multi-element data without target dimensions') + return xr.DataArray(data[0] if isinstance(data, np.ndarray) else data.iloc[0]) + + # For Series, try to match index to coordinates + if isinstance(data, pd.Series): + for dim_name in target_dims: + if data.index.equals(coords[dim_name]): + return xr.DataArray(data.values.copy(), coords={dim_name: coords[dim_name]}, dims=[dim_name]) + + # If no index match, fall through to length matching + + # For arrays or unmatched Series, match by length + matching_dims = [] + for dim_name in target_dims: + if len(data) == len(coords[dim_name]): + matching_dims.append(dim_name) + + if len(matching_dims) == 0: + dim_info = {dim: len(coords[dim]) for dim in target_dims} + raise ConversionError(f'Data length {len(data)} matches none of the target dimensions: {dim_info}') + elif len(matching_dims) > 1: + raise ConversionError( + f'Data length {len(data)} matches multiple dimensions: {matching_dims}. Cannot determine which dimension to use.' + ) + + # Match to the single matching dimension + match_dim = matching_dims[0] + values = data.values.copy() if isinstance(data, pd.Series) else data.copy() + return xr.DataArray(values, coords={match_dim: coords[match_dim]}, dims=[match_dim]) + + @staticmethod + def _broadcast_to_target_dims( + data: xr.DataArray, coords: Dict[str, pd.Index], target_dims: Tuple[str, ...] + ) -> xr.DataArray: + """ + Broadcast DataArray to match target dimensions. + + Args: + data: Source DataArray + coords: Target coordinates + target_dims: Target dimension names + + Returns: + DataArray broadcast to target dimensions + """ + if len(target_dims) == 0: + # Target is scalar + if data.size != 1: + raise ConversionError('Cannot convert multi-element DataArray to scalar') + return xr.DataArray(data.values.item()) + + # If data already matches target, validate coordinates and return + if set(data.dims) == set(target_dims) and len(data.dims) == len(target_dims): + # Check coordinate compatibility + for dim in data.dims: + if dim in coords and not np.array_equal(data.coords[dim].values, coords[dim].values): + raise ConversionError(f'DataArray {dim} coordinates do not match target coordinates') + + # Ensure correct dimension order + if data.dims != target_dims: + data = data.transpose(*target_dims) + return data.copy() + + # Handle scalar data (0D) - broadcast to all dimensions + if data.ndim == 0: + return xr.DataArray(data.item(), coords=coords, dims=target_dims) + + # Handle broadcasting from fewer to more dimensions + if len(data.dims) < len(target_dims): + return DataConverter._broadcast_dataarray_to_more_dims(data, coords, target_dims) + + # Cannot handle more dimensions than target + if len(data.dims) > len(target_dims): + raise ConversionError(f'Cannot reduce DataArray from {len(data.dims)} to {len(target_dims)} dimensions') + + raise ConversionError(f'Cannot convert DataArray with dims {data.dims} to target dims {target_dims}') + + @staticmethod + def _broadcast_dataarray_to_more_dims( + data: xr.DataArray, coords: Dict[str, pd.Index], target_dims: Tuple[str, ...] + ) -> xr.DataArray: + """Broadcast DataArray to additional dimensions.""" + # Validate that all source dimensions exist in target + for dim in data.dims: + if dim not in target_dims: + raise ConversionError(f'Source dimension "{dim}" not found in target dimensions {target_dims}') + + # Check coordinate compatibility + if not np.array_equal(data.coords[dim].values, coords[dim].values): + raise ConversionError(f'Source {dim} coordinates do not match target coordinates') + + # Build the full coordinate system + full_coords = {} + for dim in target_dims: + full_coords[dim] = coords[dim] + + # Use xarray's broadcast_to functionality + # Create a template DataArray with target structure + template_data = np.broadcast_to(data.values, [len(coords[dim]) for dim in target_dims]) - # Extract the single column as a Series and convert it - series = data.iloc[:, 0] - return DataConverter._convert_series(series, coords, dims) + # Create mapping for broadcasting + # We need to insert new axes for missing dimensions + expanded_data = data.values + for i, dim in enumerate(target_dims): + if dim not in data.dims: + # Add new axis for this dimension + expanded_data = np.expand_dims(expanded_data, axis=i) + + # Now broadcast to full shape + target_shape = tuple(len(coords[dim]) for dim in target_dims) + broadcasted_data = np.broadcast_to(expanded_data, target_shape) + + return xr.DataArray(broadcasted_data.copy(), coords=full_coords, dims=target_dims) @staticmethod def to_dataarray( @@ -153,10 +277,14 @@ def to_dataarray( """ Convert data to xarray.DataArray with specified coordinates. + Accepts: + - Scalars (broadcast to all dimensions) + - 1D arrays, Series, or single-column DataFrames (matched to one dimension, broadcast to others) + - xr.DataArray (validated and potentially broadcast to additional dimensions) + Args: - data: Scalar, 1D array/Series, single-column DataFrame, or existing DataArray + data: Data to convert coords: Dictionary mapping dimension names to coordinate indices - e.g., {'time': timesteps, 'scenario': scenarios} Returns: DataArray with the converted data @@ -164,35 +292,38 @@ def to_dataarray( if coords is None: coords = {} - coords, dims = DataConverter._prepare_dimensions(coords) + validated_coords, target_dims = DataConverter._prepare_dimensions(coords) - # Handle scalars + # Step 1: Convert to DataArray (with safe 1D/2D logic for simple data) if isinstance(data, (int, float, np.integer, np.floating)): - return DataConverter._convert_scalar(data, coords, dims) + # Scalars: create 0D DataArray, will be broadcast later + intermediate = xr.DataArray(data.item() if hasattr(data, 'item') else data) - # Handle 1D numpy arrays elif isinstance(data, np.ndarray): if data.ndim != 1: raise ConversionError(f'Only 1D arrays supported, got {data.ndim}D array') - return DataConverter._convert_1d_array(data, coords, dims) + intermediate = DataConverter._convert_1d_data_to_dataarray(data, validated_coords, target_dims) - # Handle pandas Series elif isinstance(data, pd.Series): - return DataConverter._convert_series(data, coords, dims) + intermediate = DataConverter._convert_1d_data_to_dataarray(data, validated_coords, target_dims) - # Handle pandas DataFrames (single column only) elif isinstance(data, pd.DataFrame): - return DataConverter._convert_dataframe(data, coords, dims) + if len(data.columns) != 1: + raise ConversionError(f'Only single-column DataFrames are supported, got {len(data.columns)} columns') + series = data.iloc[:, 0] + intermediate = DataConverter._convert_1d_data_to_dataarray(series, validated_coords, target_dims) - # Handle existing DataArrays (including TimeSeriesData) elif isinstance(data, xr.DataArray): - return DataConverter._handle_dataarray(data, coords, dims) + intermediate = data.copy() else: raise ConversionError( f'Unsupported data type: {type(data).__name__}. Only scalars, 1D arrays, Series, single-column DataFrames, and DataArrays are supported.' ) + # Step 2: Broadcast to target dimensions if needed + return DataConverter._broadcast_to_target_dims(intermediate, validated_coords, target_dims) + @staticmethod def _prepare_dimensions(coords: Dict[str, pd.Index]) -> Tuple[Dict[str, pd.Index], Tuple[str, ...]]: """ @@ -204,10 +335,6 @@ def _prepare_dimensions(coords: Dict[str, pd.Index]) -> Tuple[Dict[str, pd.Index Returns: Tuple of (validated coordinates dict, dimensions tuple) """ - # Check dimension limit - if len(coords) > 2: - raise ConversionError(f'Maximum 2 dimensions currently supported, got {len(coords)}') - validated_coords = {} dims = [] diff --git a/tests/test_dataconverter.py b/tests/test_dataconverter.py index eed6c1283..d8e29014f 100644 --- a/tests/test_dataconverter.py +++ b/tests/test_dataconverter.py @@ -309,6 +309,104 @@ def test_timeseries_data_broadcast(self, time_coords, scenario_coords): assert np.array_equal(result.sel(scenario=scenario).values, [10, 20, 30, 40, 50]) +class TestMultipleDimensions: + """Test support for more than 2 dimensions.""" + + def test_scalar_many_dimensions(self): + """Scalar should broadcast to any number of dimensions.""" + coords = { + 'time': pd.date_range('2024-01-01', periods=2, freq='D', name='time'), + 'scenario': pd.Index(['A', 'B'], name='scenario'), + 'region': pd.Index(['north', 'south'], name='region'), + 'technology': pd.Index(['solar', 'wind'], name='technology') + } + + result = DataConverter.to_dataarray(42, coords=coords) + assert result.shape == (2, 2, 2, 2) + assert result.dims == ('time', 'scenario', 'region', 'technology') + assert np.all(result.values == 42) + + def test_1d_array_broadcast_to_many_dimensions(self): + """1D array should broadcast to many dimensions.""" + coords = { + 'time': pd.date_range('2024-01-01', periods=3, freq='D', name='time'), + 'scenario': pd.Index(['A', 'B'], name='scenario'), + 'region': pd.Index(['north', 'south'], name='region') + } + + # Array matching time dimension + time_arr = np.array([10, 20, 30]) + result = DataConverter.to_dataarray(time_arr, coords=coords) + + assert result.shape == (3, 2, 2) + assert result.dims == ('time', 'scenario', 'region') + + # Check broadcasting - all scenarios and regions should have same time values + for scenario in coords['scenario']: + for region in coords['region']: + assert np.array_equal( + result.sel(scenario=scenario, region=region).values, + time_arr + ) + + def test_series_broadcast_to_many_dimensions(self): + """Series should broadcast to many dimensions.""" + time_coords = pd.date_range('2024-01-01', periods=3, freq='D', name='time') + coords = { + 'time': time_coords, + 'scenario': pd.Index(['A', 'B'], name='scenario'), + 'region': pd.Index(['north', 'south'], name='region'), + 'product': pd.Index(['X', 'Y', 'Z'], name='product') + } + + # Time-indexed series + time_series = pd.Series([100, 200, 300], index=time_coords) + result = DataConverter.to_dataarray(time_series, coords=coords) + + assert result.shape == (3, 2, 2, 3) + assert result.dims == ('time', 'scenario', 'region', 'product') + + # Check that all non-time dimensions have the same time series values + for scenario in coords['scenario']: + for region in coords['region']: + for product in coords['product']: + assert np.array_equal( + result.sel(scenario=scenario, region=region, product=product).values, + time_series.values + ) + + def test_dataarray_broadcast_to_more_dimensions(self): + """DataArray should broadcast to additional dimensions.""" + time_coords = pd.date_range('2024-01-01', periods=2, freq='D', name='time') + scenario_coords = pd.Index(['A', 'B'], name='scenario') + + # Start with 2D DataArray + original = xr.DataArray( + [[10, 20], [30, 40]], + coords={'time': time_coords, 'scenario': scenario_coords}, + dims=['time', 'scenario'] + ) + + # Broadcast to 3D + coords = { + 'time': time_coords, + 'scenario': scenario_coords, + 'region': pd.Index(['north', 'south'], name='region') + } + + result = DataConverter.to_dataarray(original, coords=coords) + + assert result.shape == (2, 2, 2) + assert result.dims == ('time', 'scenario', 'region') + + # Check that all regions have the same time+scenario values + for region in coords['region']: + assert np.array_equal( + result.sel(region=region).values, + original.values + ) + + class TestCustomDimensions: """Test with custom dimension names beyond time/scenario.""" From 656000690d0586c18483013ad9024b286061eeb2 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 29 Jun 2025 21:58:06 +0200 Subject: [PATCH 144/448] Update DataConverter for n-d arrays --- flixopt/core.py | 53 +++++++++++++++++++++---------------- tests/test_dataconverter.py | 13 +-------- 2 files changed, 31 insertions(+), 35 deletions(-) diff --git a/flixopt/core.py b/flixopt/core.py index 9278f079c..2b841af0b 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -164,7 +164,8 @@ def _convert_1d_data_to_dataarray( if data.index.equals(coords[dim_name]): return xr.DataArray(data.values.copy(), coords={dim_name: coords[dim_name]}, dims=[dim_name]) - # If no index match, fall through to length matching + # If no index matches, raise error + raise ConversionError(f'Data {data} does not match any of the target dimensions: {target_dims}') # For arrays or unmatched Series, match by length matching_dims = [] @@ -246,28 +247,34 @@ def _broadcast_dataarray_to_more_dims( if not np.array_equal(data.coords[dim].values, coords[dim].values): raise ConversionError(f'Source {dim} coordinates do not match target coordinates') - # Build the full coordinate system - full_coords = {} - for dim in target_dims: - full_coords[dim] = coords[dim] - - # Use xarray's broadcast_to functionality - # Create a template DataArray with target structure - template_data = np.broadcast_to(data.values, [len(coords[dim]) for dim in target_dims]) - - # Create mapping for broadcasting - # We need to insert new axes for missing dimensions - expanded_data = data.values - for i, dim in enumerate(target_dims): - if dim not in data.dims: - # Add new axis for this dimension - expanded_data = np.expand_dims(expanded_data, axis=i) - - # Now broadcast to full shape - target_shape = tuple(len(coords[dim]) for dim in target_dims) - broadcasted_data = np.broadcast_to(expanded_data, target_shape) - - return xr.DataArray(broadcasted_data.copy(), coords=full_coords, dims=target_dims) + # Start with the original data + result_data = data.values + result_dims = list(data.dims) + result_coords = {dim: data.coords[dim] for dim in data.dims} + + # Add missing dimensions one by one + for target_dim in target_dims: + if target_dim not in result_dims: + # Add this dimension at the end + result_data = np.expand_dims(result_data, axis=-1) + result_dims.append(target_dim) + result_coords[target_dim] = coords[target_dim] + + # Broadcast along the new dimension + new_shape = list(result_data.shape) + new_shape[-1] = len(coords[target_dim]) + result_data = np.broadcast_to(result_data, new_shape) + + # Reorder dimensions to match target order + if tuple(result_dims) != target_dims: + # Create mapping from current to target order + dim_indices = [result_dims.index(dim) for dim in target_dims] + result_data = np.transpose(result_data, dim_indices) + + # Build final coordinates dict in target order + final_coords = {dim: coords[dim] for dim in target_dims} + + return xr.DataArray(result_data.copy(), coords=final_coords, dims=target_dims) @staticmethod def to_dataarray( diff --git a/tests/test_dataconverter.py b/tests/test_dataconverter.py index d8e29014f..175866c71 100644 --- a/tests/test_dataconverter.py +++ b/tests/test_dataconverter.py @@ -502,20 +502,9 @@ def test_dimension_mismatch_messages(self, time_coords, scenario_coords): """Error messages should be informative.""" # Array with wrong length wrong_arr = np.array([1, 2]) # Length 2, but no dimension has length 2 - with pytest.raises(ConversionError, match="matches none of the dimensions"): + with pytest.raises(ConversionError, match="matches none of the target dimensions"): DataConverter.to_dataarray(wrong_arr, coords={'time': time_coords, 'scenario': scenario_coords}) - def test_maximum_dimensions(self): - """Should handle up to 2 dimensions currently.""" - coords = { - 'dim1': pd.Index(['a', 'b'], name='dim1'), - 'dim2': pd.Index(['x', 'y'], name='dim2'), - 'dim3': pd.Index(['1', '2'], name='dim3') - } - - with pytest.raises(ConversionError, match="Maximum 2 dimensions currently supported"): - DataConverter.to_dataarray(42, coords=coords) - class TestDataIntegrity: """Test data copying and integrity.""" From 78132ef0e589bd8b627e7f5e0a023482c3b3b422 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 29 Jun 2025 22:04:22 +0200 Subject: [PATCH 145/448] Add extra tests for 3-dims --- flixopt/flow_system.py | 4 +- tests/test_dataconverter.py | 277 ++++++++++++++++++++++++++++++++++++ 2 files changed, 279 insertions(+), 2 deletions(-) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 6bf7502e5..42a287876 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -105,8 +105,8 @@ def _validate_scenarios(scenarios: pd.Index) -> pd.Index: if not isinstance(scenarios, pd.Index) or len(scenarios) == 0: raise ConversionError('Scenarios must be a non-empty Index') - if not scenarios.name == 'scenario': - raise ConversionError(f'Scenarios must be named "scenario", got "{scenarios.name}"') + if scenarios.name != 'scenario': + scenarios = scenarios.rename('scenario') return scenarios diff --git a/tests/test_dataconverter.py b/tests/test_dataconverter.py index 175866c71..56f36aabf 100644 --- a/tests/test_dataconverter.py +++ b/tests/test_dataconverter.py @@ -309,6 +309,283 @@ def test_timeseries_data_broadcast(self, time_coords, scenario_coords): assert np.array_equal(result.sel(scenario=scenario).values, [10, 20, 30, 40, 50]) +class TestThreeDimensionConversion: + """Test conversions with exactly 3 dimensions for all data types.""" + + @pytest.fixture + def three_d_coords(self, time_coords, scenario_coords): + """Standard 3D coordinate system with unique lengths.""" + return { + 'time': time_coords, # length 5 + 'scenario': scenario_coords, # length 3 + 'region': pd.Index(['north', 'south'], name='region') # length 2 - unique! + } + + def test_scalar_three_dimensions(self, three_d_coords): + """Scalar should broadcast to 3 dimensions.""" + result = DataConverter.to_dataarray(42, coords=three_d_coords) + + assert result.shape == (5, 3, 2) # time=5, scenario=3, region=2 + assert result.dims == ('time', 'scenario', 'region') + assert np.all(result.values == 42) + + # Verify all coordinates are correct + assert result.indexes['time'].equals(three_d_coords['time']) + assert result.indexes['scenario'].equals(three_d_coords['scenario']) + assert result.indexes['region'].equals(three_d_coords['region']) + + def test_numpy_scalar_three_dimensions(self, three_d_coords): + """Numpy scalars should broadcast to 3 dimensions.""" + for scalar in [np.int32(100), np.float64(3.14)]: + result = DataConverter.to_dataarray(scalar, coords=three_d_coords) + + assert result.shape == (5, 3, 2) + assert result.dims == ('time', 'scenario', 'region') + assert np.all(result.values == scalar.item()) + + def test_1d_array_time_to_three_dimensions(self, three_d_coords): + """1D array matching time should broadcast to 3D.""" + time_arr = np.array([10, 20, 30, 40, 50]) + result = DataConverter.to_dataarray(time_arr, coords=three_d_coords) + + assert result.shape == (5, 3, 2) + assert result.dims == ('time', 'scenario', 'region') + + # Check broadcasting across scenarios and regions + for scenario in three_d_coords['scenario']: + for region in three_d_coords['region']: + slice_data = result.sel(scenario=scenario, region=region) + assert np.array_equal(slice_data.values, time_arr) + + def test_1d_array_scenario_to_three_dimensions(self, three_d_coords): + """1D array matching scenario should broadcast to 3D.""" + scenario_arr = np.array([100, 200, 300]) + result = DataConverter.to_dataarray(scenario_arr, coords=three_d_coords) + + assert result.shape == (5, 3, 2) + assert result.dims == ('time', 'scenario', 'region') + + # Check broadcasting across time and regions + for time in three_d_coords['time']: + for region in three_d_coords['region']: + slice_data = result.sel(time=time, region=region) + assert np.array_equal(slice_data.values, scenario_arr) + + def test_1d_array_region_to_three_dimensions(self, three_d_coords): + """1D array matching region should broadcast to 3D.""" + region_arr = np.array([1000, 2000]) # Length 2 to match region + result = DataConverter.to_dataarray(region_arr, coords=three_d_coords) + + assert result.shape == (5, 3, 2) + assert result.dims == ('time', 'scenario', 'region') + + # Check broadcasting across time and scenarios + for time in three_d_coords['time']: + for scenario in three_d_coords['scenario']: + slice_data = result.sel(time=time, scenario=scenario) + assert np.array_equal(slice_data.values, region_arr) + + def test_series_time_to_three_dimensions(self, three_d_coords): + """Time-indexed Series should broadcast to 3D.""" + time_series = pd.Series([15, 25, 35, 45, 55], index=three_d_coords['time']) + result = DataConverter.to_dataarray(time_series, coords=three_d_coords) + + assert result.shape == (5, 3, 2) + assert result.dims == ('time', 'scenario', 'region') + + # Check broadcasting + for scenario in three_d_coords['scenario']: + for region in three_d_coords['region']: + slice_data = result.sel(scenario=scenario, region=region) + assert np.array_equal(slice_data.values, time_series.values) + + def test_series_scenario_to_three_dimensions(self, three_d_coords): + """Scenario-indexed Series should broadcast to 3D.""" + scenario_series = pd.Series([500, 600, 700], index=three_d_coords['scenario']) + result = DataConverter.to_dataarray(scenario_series, coords=three_d_coords) + + assert result.shape == (5, 3, 2) + assert result.dims == ('time', 'scenario', 'region') + + # Check broadcasting + for time in three_d_coords['time']: + for region in three_d_coords['region']: + slice_data = result.sel(time=time, region=region) + assert np.array_equal(slice_data.values, scenario_series.values) + + def test_series_region_to_three_dimensions(self, three_d_coords): + """Region-indexed Series should broadcast to 3D.""" + region_series = pd.Series([5000, 6000], index=three_d_coords['region']) # Length 2 + result = DataConverter.to_dataarray(region_series, coords=three_d_coords) + + assert result.shape == (5, 3, 2) + assert result.dims == ('time', 'scenario', 'region') + + # Check broadcasting + for time in three_d_coords['time']: + for scenario in three_d_coords['scenario']: + slice_data = result.sel(time=time, scenario=scenario) + assert np.array_equal(slice_data.values, region_series.values) + + def test_dataframe_time_to_three_dimensions(self, three_d_coords): + """Time-indexed DataFrame should broadcast to 3D.""" + df = pd.DataFrame({'power': [11, 22, 33, 44, 55]}, index=three_d_coords['time']) + result = DataConverter.to_dataarray(df, coords=three_d_coords) + + assert result.shape == (5, 3, 2) + assert result.dims == ('time', 'scenario', 'region') + + # Check broadcasting + for scenario in three_d_coords['scenario']: + for region in three_d_coords['region']: + slice_data = result.sel(scenario=scenario, region=region) + assert np.array_equal(slice_data.values, df['power'].values) + + def test_dataframe_scenario_to_three_dimensions(self, three_d_coords): + """Scenario-indexed DataFrame should broadcast to 3D.""" + df = pd.DataFrame({'cost': [1100, 1200, 1300]}, index=three_d_coords['scenario']) + result = DataConverter.to_dataarray(df, coords=three_d_coords) + + assert result.shape == (5, 3, 2) + assert result.dims == ('time', 'scenario', 'region') + + # Check broadcasting + for time in three_d_coords['time']: + for region in three_d_coords['region']: + slice_data = result.sel(time=time, region=region) + assert np.array_equal(slice_data.values, df['cost'].values) + + def test_1d_dataarray_time_to_three_dimensions(self, three_d_coords): + """1D time DataArray should broadcast to 3D.""" + original = xr.DataArray([101, 102, 103, 104, 105], + coords={'time': three_d_coords['time']}, + dims=['time']) + result = DataConverter.to_dataarray(original, coords=three_d_coords) + + assert result.shape == (5, 3, 2) + assert result.dims == ('time', 'scenario', 'region') + + # Check broadcasting + for scenario in three_d_coords['scenario']: + for region in three_d_coords['region']: + slice_data = result.sel(scenario=scenario, region=region) + assert np.array_equal(slice_data.values, original.values) + + def test_1d_dataarray_scenario_to_three_dimensions(self, three_d_coords): + """1D scenario DataArray should broadcast to 3D.""" + original = xr.DataArray([2001, 2002, 2003], + coords={'scenario': three_d_coords['scenario']}, + dims=['scenario']) + result = DataConverter.to_dataarray(original, coords=three_d_coords) + + assert result.shape == (5, 3, 2) + assert result.dims == ('time', 'scenario', 'region') + + # Check broadcasting + for time in three_d_coords['time']: + for region in three_d_coords['region']: + slice_data = result.sel(time=time, region=region) + assert np.array_equal(slice_data.values, original.values) + + def test_2d_dataarray_to_three_dimensions(self, three_d_coords): + """2D DataArray should broadcast to 3D.""" + # Create 2D time x scenario DataArray + data_2d = np.random.rand(5, 3) + original = xr.DataArray(data_2d, + coords={'time': three_d_coords['time'], + 'scenario': three_d_coords['scenario']}, + dims=['time', 'scenario']) + + result = DataConverter.to_dataarray(original, coords=three_d_coords) + + assert result.shape == (5, 3, 2) + assert result.dims == ('time', 'scenario', 'region') + + # Check that all regions have the same time x scenario data + for region in three_d_coords['region']: + slice_data = result.sel(region=region) + assert np.array_equal(slice_data.values, original.values) + + def test_timeseries_data_to_three_dimensions(self, three_d_coords): + """TimeSeriesData should broadcast to 3D.""" + data_array = xr.DataArray([99, 88, 77, 66, 55], + coords={'time': three_d_coords['time']}, + dims=['time']) + ts_data = TimeSeriesData(data_array, aggregation_group='test_3d') + + result = DataConverter.to_dataarray(ts_data, coords=three_d_coords) + + assert result.shape == (5, 3, 2) + assert result.dims == ('time', 'scenario', 'region') + + # Check broadcasting + for scenario in three_d_coords['scenario']: + for region in three_d_coords['region']: + slice_data = result.sel(scenario=scenario, region=region) + assert np.array_equal(slice_data.values, [99, 88, 77, 66, 55]) + + def test_three_d_copy_independence(self, three_d_coords): + """3D results should be independent copies.""" + original_arr = np.array([10, 20, 30, 40, 50]) + result = DataConverter.to_dataarray(original_arr, coords=three_d_coords) + + # Modify result + result[0, 0, 0] = 999 + + # Original should be unchanged + assert original_arr[0] == 10 + + def test_three_d_special_values(self, three_d_coords): + """3D conversion should preserve special values.""" + # Array with NaN and inf + special_arr = np.array([1, np.nan, np.inf, -np.inf, 5]) + result = DataConverter.to_dataarray(special_arr, coords=three_d_coords) + + assert result.shape == (5, 3, 2) + + # Check that special values are preserved in all broadcasts + for scenario in three_d_coords['scenario']: + for region in three_d_coords['region']: + slice_data = result.sel(scenario=scenario, region=region) + assert np.array_equal(np.isnan(slice_data.values), np.isnan(special_arr)) + assert np.array_equal(np.isinf(slice_data.values), np.isinf(special_arr)) + + def test_three_d_ambiguous_length_error(self): + """Should fail when array length matches multiple dimensions in 3D.""" + # All dimensions have length 3 + coords_3x3x3 = { + 'time': pd.date_range('2024-01-01', periods=3, freq='D', name='time'), + 'scenario': pd.Index(['A', 'B', 'C'], name='scenario'), + 'region': pd.Index(['X', 'Y', 'Z'], name='region') + } + + arr = np.array([1, 2, 3]) # Length 3 - matches all dimensions + + with pytest.raises(ConversionError, match="matches multiple dimensions"): + DataConverter.to_dataarray(arr, coords=coords_3x3x3) + + def test_three_d_custom_dimensions(self): + """3D conversion with custom dimension names.""" + coords = { + 'product': pd.Index(['A', 'B'], name='product'), + 'factory': pd.Index(['F1', 'F2', 'F3'], name='factory'), + 'quarter': pd.Index(['Q1', 'Q2', 'Q3', 'Q4'], name='quarter') + } + + # Array matching factory dimension + factory_arr = np.array([100, 200, 300]) + result = DataConverter.to_dataarray(factory_arr, coords=coords) + + assert result.shape == (2, 3, 4) + assert result.dims == ('product', 'factory', 'quarter') + + # Check broadcasting + for product in coords['product']: + for quarter in coords['quarter']: + slice_data = result.sel(product=product, quarter=quarter) + assert np.array_equal(slice_data.values, factory_arr) + + class TestMultipleDimensions: """Test support for more than 2 dimensions.""" From a53c116c8b69127c1849082a256f7e713d83a9cf Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 30 Jun 2025 17:53:13 +0200 Subject: [PATCH 146/448] Add FLowSystemDimension Type --- flixopt/core.py | 5 ++++- flixopt/flow_system.py | 10 ++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/flixopt/core.py b/flixopt/core.py index 2b841af0b..43b529421 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -5,7 +5,7 @@ import logging import warnings -from typing import Dict, Optional, Union, Tuple +from typing import Dict, Optional, Union, Tuple, Literal import numpy as np import pandas as pd @@ -30,6 +30,9 @@ NonTemporalData = Union[Scalar, xr.DataArray] """Internally used datatypes for non-temporal data. Can be a Scalar or an xr.DataArray.""" +FlowSystemDimensions = Literal['time', 'scenario'] +"""Possible dimensions of a FlowSystem.""" + class PlausibilityError(Exception): """Error for a failing Plausibility check.""" diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 42a287876..c753b8cc8 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -16,7 +16,7 @@ from rich.pretty import Pretty from . import io as fx_io -from .core import ConversionError, DataConverter, TemporalData, TemporalDataUser, TimeSeriesData, NonTemporalDataUser, NonTemporalData +from .core import ConversionError, DataConverter, TemporalData, TemporalDataUser, TimeSeriesData, NonTemporalDataUser, NonTemporalData, FlowSystemDimensions from .effects import Effect, EffectCollection, NonTemporalEffects, NonTemporalEffectsUser, TemporalEffects, TemporalEffectsUser from .elements import Bus, Component, Flow from .structure import Element, Interface, SystemModel @@ -294,7 +294,7 @@ def fit_to_model_coords( self, name: str, data: Optional[Union[TemporalDataUser, NonTemporalDataUser]], - dimensions: Union[List[str], str] = 'time', # Default to time only + dimensions: Optional[Union[List[FlowSystemDimensions], FlowSystemDimensions]] = None, # Default to time only ) -> Optional[Union[TemporalData, NonTemporalData]]: """ Fit data to model coordinate system (currently time, but extensible). @@ -302,7 +302,7 @@ def fit_to_model_coords( Args: name: Name of the data data: Data to fit to model coordinates - has_time_dim: Whether the data has a time dimension + dimensions: Dimensions to use for the DataArray Returns: xr.DataArray aligned to model coordinate system @@ -316,10 +316,12 @@ def fit_to_model_coords( coords = {} for dim in dimensions: - if dim == 'time' and self.timesteps is not None: + if dim == 'time': coords['time'] = self.timesteps elif dim == 'scenario' and self.scenarios is not None: coords['scenario'] = self.scenarios + else: + raise ValueError(f'Invalid flow system dimension "{dim}"') # Future: elif dim == 'region' and self.regions is not None: ... # Rest of your method stays the same, just pass coords From 2cb551b064a9803fe7c6271561ef3e30111ae060 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 30 Jun 2025 18:09:51 +0200 Subject: [PATCH 147/448] Revert some logic about the fit_to_model coords --- flixopt/flow_system.py | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index c753b8cc8..0e6c2cf6f 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -294,7 +294,7 @@ def fit_to_model_coords( self, name: str, data: Optional[Union[TemporalDataUser, NonTemporalDataUser]], - dimensions: Optional[Union[List[FlowSystemDimensions], FlowSystemDimensions]] = None, # Default to time only + has_time_dim: bool = True, ) -> Optional[Union[TemporalData, NonTemporalData]]: """ Fit data to model coordinate system (currently time, but extensible). @@ -302,7 +302,7 @@ def fit_to_model_coords( Args: name: Name of the data data: Data to fit to model coordinates - dimensions: Dimensions to use for the DataArray + has_time_dim: Wether to use the time dimension or not Returns: xr.DataArray aligned to model coordinate system @@ -310,19 +310,10 @@ def fit_to_model_coords( if data is None: return None - # Build coords from requested dimensions - if isinstance(dimensions, str): - dimensions = [dimensions] + coords = self.coords - coords = {} - for dim in dimensions: - if dim == 'time': - coords['time'] = self.timesteps - elif dim == 'scenario' and self.scenarios is not None: - coords['scenario'] = self.scenarios - else: - raise ValueError(f'Invalid flow system dimension "{dim}"') - # Future: elif dim == 'region' and self.regions is not None: ... + if not has_time_dim: + coords.pop('time') # Rest of your method stays the same, just pass coords if isinstance(data, TimeSeriesData): @@ -354,7 +345,11 @@ def fit_effects_to_model_coords( effect_values_dict = self.effects.create_effect_values_dict(effect_values) return { - effect: self.fit_to_model_coords('|'.join(filter(None, [label_prefix, effect, label_suffix])), value, has_time_dim=has_time_dim) + effect: self.fit_to_model_coords( + '|'.join(filter(None, [label_prefix, effect, label_suffix])), + value, + has_time_dim=has_time_dim + ) for effect, value in effect_values_dict.items() } @@ -594,6 +589,13 @@ def flows(self) -> Dict[str, Flow]: def all_elements(self) -> Dict[str, Element]: return {**self.components, **self.effects.effects, **self.flows, **self.buses} + @property + def coords(self) -> Dict[str, pd.Index]: + active_coords = {'time': self.timesteps} + if self.scenarios is not None: + active_coords['scenario'] = self.scenarios + return active_coords + @property def used_in_calculation(self) -> bool: return self._used_in_calculation From d7be7667e27a42f2bf784eab4631883650b3f8a9 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 30 Jun 2025 18:22:10 +0200 Subject: [PATCH 148/448] Adjust FLowSystem IO for scenarios --- flixopt/flow_system.py | 1 + flixopt/structure.py | 6 +++++- tests/conftest.py | 3 ++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 0e6c2cf6f..e2a39508d 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -212,6 +212,7 @@ def from_dataset(cls, ds: xr.Dataset) -> 'FlowSystem': # Create FlowSystem instance with constructor parameters flow_system = cls( timesteps=ds.indexes['time'], + scenarios=ds.indexes.get('scenario'), hours_of_last_timestep=reference_structure.get('hours_of_last_timestep'), hours_of_previous_timesteps=reference_structure.get('hours_of_previous_timesteps'), ) diff --git a/flixopt/structure.py b/flixopt/structure.py index d02a8d4e9..681c4dc0c 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -203,12 +203,16 @@ def _create_reference_structure(self) -> Tuple[Dict, Dict[str, xr.DataArray]]: all_extracted_arrays = {} for name in self._cached_init_params: - if name == 'self' or name == 'timesteps': # Skip self and timesteps. Timesteps are directly stored in Datasets + if name == 'self': # Skip self and timesteps. Timesteps are directly stored in Datasets continue value = getattr(self, name, None) + if value is None: continue + if isinstance(value, pd.Index): + logger.debug(f'Skipping {name=} because it is an Index') + continue # Extract arrays and get reference structure processed_value, extracted_arrays = self._extract_dataarrays_recursive(value, name) diff --git a/tests/conftest.py b/tests/conftest.py index 198b9a92b..55f17057d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -170,7 +170,8 @@ def simple_flow_system_scenarios() -> fx.FlowSystem: discharging=fx.Flow('Q_th_unload', bus='Fernwärme', size=1e4), capacity_in_flow_hours=fx.InvestParameters(fix_effects=20, fixed_size=30, optional=False), initial_charge_state=0, - relative_maximum_charge_state=1 / 100 * np.array([80.0, 70.0, 80.0, 80, 80, 80, 80, 80, 80, 80]), + relative_maximum_charge_state=1 / 100 * np.array([80.0, 70.0, 80.0, 80, 80, 80, 80, 80, 80]), + relative_maximum_final_charge_state=0.8, eta_charge=0.9, eta_discharge=1, relative_loss_per_hour=0.08, From e60dd079884d0784e3e3e87e0e15e1ffb1ef449b Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 30 Jun 2025 18:37:15 +0200 Subject: [PATCH 149/448] BUGFIX: Raise Exception instead of logging --- flixopt/flow_system.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index e2a39508d..1da0184a9 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -325,8 +325,8 @@ def fit_to_model_coords( aggregation_group=data.aggregation_group, aggregation_weight=data.aggregation_weight ).rename(name) except ConversionError as e: - logger.critical(f'Could not convert time series data "{name}" to DataArray: {e}. \n' - f'Take care to use the correct (time) index.') + raise ConversionError( + f'Could not convert time series data "{name}" to DataArray: Original Error: {e}') from e else: return DataConverter.to_dataarray(data, coords=coords).rename(name) From bd2f1b82fd1dbf934a7179726aefb280db2891eb Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 30 Jun 2025 18:40:26 +0200 Subject: [PATCH 150/448] Change usage of TimeSeriesData --- tests/conftest.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 55f17057d..9f247164f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -400,16 +400,17 @@ def flow_system_long(): p_el = data['Strompr.€/MWh'].values gas_price = data['Gaspr.€/MWh'].values + flow_system = fx.FlowSystem(pd.DatetimeIndex(data.index)) + thermal_load_ts, electrical_load_ts = ( - fx.TimeSeriesData(thermal_load), - fx.TimeSeriesData(electrical_load, aggregation_weight=0.7), + fx.TimeSeriesData(thermal_load, coords={'time': flow_system.timesteps}), + fx.TimeSeriesData(electrical_load, aggregation_weight=0.7, coords={'time': flow_system.timesteps}), ) p_feed_in, p_sell = ( - fx.TimeSeriesData(-(p_el - 0.5), aggregation_group='p_el'), - fx.TimeSeriesData(p_el + 0.5, aggregation_group='p_el'), + fx.TimeSeriesData(-(p_el - 0.5), aggregation_group='p_el', coords={'time': flow_system.timesteps}), + fx.TimeSeriesData(p_el + 0.5, aggregation_group='p_el', coords={'time': flow_system.timesteps}), ) - flow_system = fx.FlowSystem(pd.DatetimeIndex(data.index)) flow_system.add_elements( fx.Bus('Strom'), fx.Bus('Fernwärme'), From a7da9d286efd18766ea608d4f8328f54521bf982 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 30 Jun 2025 21:32:57 +0200 Subject: [PATCH 151/448] Adjust logic to handle non scalars --- flixopt/elements.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flixopt/elements.py b/flixopt/elements.py index 9df367eec..1daadeb55 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -195,7 +195,7 @@ def __init__( meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types. """ super().__init__(label, meta_data=meta_data) - self.size = size or CONFIG.modeling.BIG # Default size + self.size = size if size is not None else CONFIG.modeling.BIG # Default size self.relative_minimum = relative_minimum self.relative_maximum = relative_maximum self.fixed_relative_profile = fixed_relative_profile From b8f0e226a5f6024489e8e05bb31636f9c788d569 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 30 Jun 2025 21:33:49 +0200 Subject: [PATCH 152/448] Adjust logic to _resolve_dataarray_reference into separate method --- flixopt/structure.py | 51 ++++++++++++++++++++++++++++++-------------- 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/flixopt/structure.py b/flixopt/structure.py index 681c4dc0c..84b9c6cba 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -316,6 +316,40 @@ def _extract_dataarrays_recursive(self, obj, context_name: str = '') -> Tuple[An else: return self._serialize_to_basic_types(obj), extracted_arrays + @classmethod + def _resolve_dataarray_reference( + cls, reference: str, arrays_dict: Dict[str, xr.DataArray] + ) -> Union[xr.DataArray, TimeSeriesData]: + """ + Resolve a single DataArray reference (:::name) to actual DataArray or TimeSeriesData. + + Args: + reference: Reference string starting with ":::" + arrays_dict: Dictionary of available DataArrays + + Returns: + Resolved DataArray or TimeSeriesData object + + Raises: + ValueError: If referenced array is not found + """ + array_name = reference[3:] # Remove ":::" prefix + if array_name not in arrays_dict: + raise ValueError(f"Referenced DataArray '{array_name}' not found in dataset") + + array = arrays_dict[array_name] + + # Handle null values with warning + if array.isnull().any(): + logger.warning(f"DataArray '{array_name}' contains null values. Dropping them.") + array = array.dropna(dim='time', how='all') + + # Check if this should be restored as TimeSeriesData + if TimeSeriesData.is_timeseries_data(array): + return TimeSeriesData.from_dataarray(array) + + return array + @classmethod def _resolve_reference_structure(cls, structure, arrays_dict: Dict[str, xr.DataArray]): """ @@ -333,22 +367,7 @@ def _resolve_reference_structure(cls, structure, arrays_dict: Dict[str, xr.DataA """ # Handle DataArray references if isinstance(structure, str) and structure.startswith(':::'): - array_name = structure[3:] # Remove ":::" prefix - if array_name not in arrays_dict: - raise ValueError(f"Referenced DataArray '{array_name}' not found in dataset") - - array = arrays_dict[array_name] - - # Handle null values with warning - if array.isnull().any(): - logger.warning(f"DataArray '{array_name}' contains null values. Dropping them.") - array = array.dropna(dim='time', how='all') - - # Check if this should be restored as TimeSeriesData - if TimeSeriesData.is_timeseries_data(array): - return TimeSeriesData.from_dataarray(array) - - return array + return cls._resolve_dataarray_reference(structure, arrays_dict) elif isinstance(structure, list): resolved_list = [] From 7123b6b736cfbd55fbf8fd501bdd48f0ca37470d Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 30 Jun 2025 21:34:50 +0200 Subject: [PATCH 153/448] Update IO of FlowSystem --- flixopt/flow_system.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 1da0184a9..656aecd77 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -209,17 +209,20 @@ def from_dataset(cls, ds: xr.Dataset) -> 'FlowSystem': # Get the reference structure from attrs reference_structure = dict(ds.attrs) + # Create arrays dictionary from dataset variables + arrays_dict = {name: array for name, array in ds.data_vars.items()} + # Create FlowSystem instance with constructor parameters flow_system = cls( timesteps=ds.indexes['time'], scenarios=ds.indexes.get('scenario'), + scenario_weights=cls._resolve_dataarray_reference( + reference_structure['scenario_weights'], arrays_dict + ) if 'scenario_weights' in reference_structure else None, hours_of_last_timestep=reference_structure.get('hours_of_last_timestep'), hours_of_previous_timesteps=reference_structure.get('hours_of_previous_timesteps'), ) - # Create arrays dictionary from dataset variables - arrays_dict = {name: array for name, array in ds.data_vars.items()} - # Restore components components_structure = reference_structure.get('components', {}) for comp_label, comp_data in components_structure.items(): From fa5475d89982ee7ae9ba374b4d84ba73b75c9979 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 2 Jul 2025 10:03:12 +0200 Subject: [PATCH 154/448] Improve get_coords() --- flixopt/structure.py | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/flixopt/structure.py b/flixopt/structure.py index 84b9c6cba..7732ffeb4 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -132,26 +132,25 @@ def get_coords( Returns: The coordinates of the model. Might also be None if no scenarios are present and time_dim is False """ - if not scenario_dim and not time_dim: - return None - scenarios = self.flow_system.scenarios - timesteps = ( - self.flow_system.timesteps if not extra_timestep else self.flow_system.timesteps_extra - ) + if extra_timestep and not time_dim: + raise ValueError('extra_timestep=True requires time_dim=True') + + coords = self.flow_system.coords - if scenario_dim and time_dim: - if scenarios is None: - return (timesteps,) - return timesteps, scenarios + if not scenario_dim: + coords.pop('scenario', None) + if not time_dim: + coords.pop('time', None) + if extra_timestep: + coords['time'] = self.flow_system.timesteps_extra + + if not coords: + return None - if scenario_dim and not time_dim: - if scenarios is None: - return None - return (scenarios,) - if time_dim and not scenario_dim: - return (timesteps,) + if len(coords) == 1: + return (coords.popitem()[1],) - raise ValueError(f'Cannot get coordinates with both {scenario_dim=} and {time_dim=}') + return tuple(coords.values()) class Interface: From 80cb1610a5550e97149e5d2601c30c9f268d2ee2 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 2 Jul 2025 10:04:04 +0200 Subject: [PATCH 155/448] Adjust FlowSystem init for correct IO --- flixopt/flow_system.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 656aecd77..ecfca4ed2 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -62,15 +62,24 @@ def __init__( If you use an array, take care that its long enough to cover all previous values! scenario_weights: The weights of the scenarios. If None, all scenarios have the same weight. All weights are normalized to 1. """ - # Store timing information directly self.timesteps = self._validate_timesteps(timesteps) + self.timesteps_extra = self._create_timesteps_with_extra(timesteps, hours_of_last_timestep) - self.hours_per_timestep = self.calculate_hours_per_timestep(self.timesteps_extra) - self.hours_of_previous_timesteps = self._calculate_hours_of_previous_timesteps( - timesteps, hours_of_previous_timesteps - ) + self.hours_of_previous_timesteps = self._calculate_hours_of_previous_timesteps(timesteps, hours_of_previous_timesteps) + self.scenarios = None if scenarios is None else self._validate_scenarios(scenarios) - self.scenario_weights = scenario_weights + + hours_per_timestep = self.calculate_hours_per_timestep(self.timesteps_extra) + + self.hours_of_last_timestep = hours_per_timestep[-1].item() + + self.hours_per_timestep = self.fit_to_model_coords('hours_per_timestep', hours_per_timestep) + + self.scenario_weights = self.fit_to_model_coords( + 'scenario_weights', + scenario_weights, + has_time_dim=False, + ) # Element collections self.components: Dict[str, Component] = {} @@ -123,7 +132,7 @@ def _create_timesteps_with_extra( @staticmethod def calculate_hours_per_timestep(timesteps_extra: pd.DatetimeIndex) -> xr.DataArray: - """Calculate duration of each timestep.""" + """Calculate duration of each timestep as a 1D DataArray.""" hours_per_step = np.diff(timesteps_extra) / pd.Timedelta(hours=1) return xr.DataArray( hours_per_step, coords={'time': timesteps_extra[:-1]}, dims=['time'], name='hours_per_timestep' From 81ad3baf19bee65853854a20e706bcee19559aa1 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 2 Jul 2025 11:04:02 +0200 Subject: [PATCH 156/448] Add scenario to sel and isel methods, and dont normalize scenario weights --- flixopt/flow_system.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index ecfca4ed2..0d77d046e 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -60,7 +60,7 @@ def __init__( If None, the first time increment of time_series is used. This is needed to calculate previous durations (for example consecutive_on_hours). If you use an array, take care that its long enough to cover all previous values! - scenario_weights: The weights of the scenarios. If None, all scenarios have the same weight. All weights are normalized to 1. + scenario_weights: The weights of each scenarios. If None, all scenarios have the same weight (normalized to 1). Its recommended to scale the weights to sum up to 1. """ self.timesteps = self._validate_timesteps(timesteps) @@ -318,7 +318,7 @@ def fit_to_model_coords( has_time_dim: Wether to use the time dimension or not Returns: - xr.DataArray aligned to model coordinate system + xr.DataArray aligned to model coordinate system. If data is None, returns None. """ if data is None: return None @@ -613,12 +613,14 @@ def coords(self) -> Dict[str, pd.Index]: def used_in_calculation(self) -> bool: return self._used_in_calculation - def sel(self, time: Optional[Union[str, slice, List[str], pd.Timestamp, pd.DatetimeIndex]] = None) -> 'FlowSystem': + def sel(self, time: Optional[Union[str, slice, List[str], pd.Timestamp, pd.DatetimeIndex]] = None, + scenario: Optional[Union[str, slice, List[str], pd.Index]] = None) -> 'FlowSystem': """ Select a subset of the flowsystem by the time coordinate. Args: time: Time selection (e.g., slice('2023-01-01', '2023-12-31'), '2023-06-15', or list of times) + scenario: Scenario selection (e.g., slice('scenario1', 'scenario2'), or list of scenarios) Returns: FlowSystem: New FlowSystem with selected data @@ -631,18 +633,22 @@ def sel(self, time: Optional[Union[str, slice, List[str], pd.Timestamp, pd.Datet if time is not None: indexers['time'] = time + if scenario is not None: + indexers['scenario'] = scenario + if not indexers: return self.copy() # Return a copy when no selection selected_dataset = self.to_dataset().sel(**indexers) return self.__class__.from_dataset(selected_dataset) - def isel(self, time: Optional[Union[int, slice, List[int]]] = None) -> 'FlowSystem': + def isel(self, time: Optional[Union[int, slice, List[int]]] = None, scenario: Optional[Union[int, slice, List[int]]] = None) -> 'FlowSystem': """ Select a subset of the flowsystem by integer indices. Args: time: Time selection by integer index (e.g., slice(0, 100), 50, or [0, 5, 10]) + scenario: Scenario selection by integer index (e.g., slice(0, 3), 50, or [0, 5, 10]) Returns: FlowSystem: New FlowSystem with selected data @@ -655,6 +661,9 @@ def isel(self, time: Optional[Union[int, slice, List[int]]] = None) -> 'FlowSyst if time is not None: indexers['time'] = time + if scenario is not None: + indexers['scenario'] = scenario + if not indexers: return self.copy() # Return a copy when no selection From 691a45e40e2ac75b1c0a6403bf51173dd40fafa2 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 2 Jul 2025 11:14:15 +0200 Subject: [PATCH 157/448] Improve scenario_weights_handling --- flixopt/flow_system.py | 7 +------ flixopt/structure.py | 25 ++++++++----------------- 2 files changed, 9 insertions(+), 23 deletions(-) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 0d77d046e..fbf65e489 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -68,6 +68,7 @@ def __init__( self.hours_of_previous_timesteps = self._calculate_hours_of_previous_timesteps(timesteps, hours_of_previous_timesteps) self.scenarios = None if scenarios is None else self._validate_scenarios(scenarios) + self.scenario_weights = scenario_weights hours_per_timestep = self.calculate_hours_per_timestep(self.timesteps_extra) @@ -75,12 +76,6 @@ def __init__( self.hours_per_timestep = self.fit_to_model_coords('hours_per_timestep', hours_per_timestep) - self.scenario_weights = self.fit_to_model_coords( - 'scenario_weights', - scenario_weights, - has_time_dim=False, - ) - # Element collections self.components: Dict[str, Component] = {} self.buses: Dict[str, Bus] = {} diff --git a/flixopt/structure.py b/flixopt/structure.py index 7732ffeb4..cbf8af599 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -57,7 +57,6 @@ def __init__(self, flow_system: 'FlowSystem'): super().__init__(force_dim_names=True) self.flow_system = flow_system self.effects: Optional[EffectCollectionModel] = None - self.scenario_weights = self._calculate_scenario_weights(flow_system.scenario_weights) def do_modeling(self): self.effects = self.flow_system.effects.create_model(self) @@ -69,22 +68,6 @@ def do_modeling(self): for bus_model in bus_models: # Buses after Components, because FlowModels are created in ComponentModels bus_model.do_modeling() - def _calculate_scenario_weights(self, weights: Optional[NonTemporalData] = None) -> xr.DataArray: - """Calculates the weights of the scenarios. If None, all scenarios have the same weight. All weights are normalized to 1. - If no scenarios are present, s single weight of 1 is returned. - """ - if weights is not None and not isinstance(weights, xr.DataArray): - raise TypeError(f'Weights must be a xr.DataArray or None, got {type(weights)}') - if self.flow_system.scenarios is None: - return xr.DataArray(1) - if weights is None: - weights = xr.DataArray( - np.ones(len(self.flow_system.scenarios)), - coords={'scenario': self.flow_system.scenarios} - ) - - return weights / weights.sum() - @property def solution(self): solution = super().solution @@ -152,6 +135,14 @@ def get_coords( return tuple(coords.values()) + @property + def scenario_weights(self) -> xr.DataArray: + """Returns the scenario weights of the FlowSystem.""" + if self.flow_system.scenarios is None: + return xr.DataArray(1) + + return self.flow_system.scenario_weights + class Interface: """ From 3931ac52e816d8ddc8b5029dba811009cc70576e Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 2 Jul 2025 11:17:56 +0200 Subject: [PATCH 158/448] Add warning for not scaled weights --- flixopt/flow_system.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index fbf65e489..74492bcd3 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -366,6 +366,10 @@ def connect_and_transform(self): self.scenario_weights = self.fit_to_model_coords( 'scenario_weights', self.scenario_weights, has_time_dim=False ) + if self.scenario_weights is not None and self.scenario_weights.sum() != 1: + logger.warning(f'Scenario weights are not normalized to 1. This is reccomended for a better scaled model. ' + f'Sum of weights={self.scenario_weights.sum().item()}') + if not self._connected_and_transformed: self._connect_network() for element in list(self.components.values()) + list(self.effects.effects.values()) + list(self.buses.values()): From 75a45e1552db8cbc89a5793da9d6a5e32c2d2ca0 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 2 Jul 2025 15:41:24 +0200 Subject: [PATCH 159/448] Update test_scenarios.py --- tests/test_scenarios.py | 45 ++++++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py index 4abdafa5f..cc0be36c6 100644 --- a/tests/test_scenarios.py +++ b/tests/test_scenarios.py @@ -239,8 +239,8 @@ def flow_system_piecewise_conversion_scenarios(flow_system_complex_scenarios) -> { 'P_el': fx.Piecewise( [ - fx.Piece(np.linspace(5, 6, len(flow_system.time_series_collection.timesteps)), 30), - fx.Piece(40, np.linspace(60, 70, len(flow_system.time_series_collection.timesteps))), + fx.Piece(np.linspace(5, 6, len(flow_system.timesteps)), 30), + fx.Piece(40, np.linspace(60, 70, len(flow_system.timesteps))), ] ), 'Q_th': fx.Piecewise([fx.Piece(6, 35), fx.Piece(45, 100)]), @@ -256,7 +256,18 @@ def flow_system_piecewise_conversion_scenarios(flow_system_complex_scenarios) -> def test_scenario_weights(flow_system_piecewise_conversion_scenarios): """Test that scenario weights are correctly used in the model.""" - scenarios = flow_system_piecewise_conversion_scenarios.time_series_collection.scenarios + scenarios = flow_system_piecewise_conversion_scenarios.scenarios + weights = np.linspace(0.5, 1, len(scenarios)) + flow_system_piecewise_conversion_scenarios.scenario_weights = weights + model = create_linopy_model(flow_system_piecewise_conversion_scenarios) + np.testing.assert_allclose(model.scenario_weights.values, weights) + assert_linequal(model.objective.expression, + (model.variables['costs|total'] * weights).sum() + model.variables['Penalty|total']) + assert np.isclose(model.scenario_weights.sum().item(), 2.25) + +def test_scenario_weights_io(flow_system_piecewise_conversion_scenarios): + """Test that scenario weights are correctly used in the model.""" + scenarios = flow_system_piecewise_conversion_scenarios.scenarios weights = np.linspace(0.5, 1, len(scenarios)) / np.sum(np.linspace(0.5, 1, len(scenarios))) flow_system_piecewise_conversion_scenarios.scenario_weights = weights model = create_linopy_model(flow_system_piecewise_conversion_scenarios) @@ -273,7 +284,7 @@ def test_scenario_dimensions_in_variables(flow_system_piecewise_conversion_scena def test_full_scenario_optimization(flow_system_piecewise_conversion_scenarios): """Test a full optimization with scenarios and verify results.""" - scenarios = flow_system_piecewise_conversion_scenarios.time_series_collection.scenarios + scenarios = flow_system_piecewise_conversion_scenarios.scenarios weights = np.linspace(0.5, 1, len(scenarios)) / np.sum(np.linspace(0.5, 1, len(scenarios))) flow_system_piecewise_conversion_scenarios.scenario_weights = weights calc = create_calculation_and_solve(flow_system_piecewise_conversion_scenarios, @@ -292,7 +303,7 @@ def test_full_scenario_optimization(flow_system_piecewise_conversion_scenarios): @pytest.mark.skip(reason="This test is taking too long with highs and is too big for gurobipy free") def test_io_persistance(flow_system_piecewise_conversion_scenarios): """Test a full optimization with scenarios and verify results.""" - scenarios = flow_system_piecewise_conversion_scenarios.time_series_collection.scenarios + scenarios = flow_system_piecewise_conversion_scenarios.scenarios weights = np.linspace(0.5, 1, len(scenarios)) / np.sum(np.linspace(0.5, 1, len(scenarios))) flow_system_piecewise_conversion_scenarios.scenario_weights = weights calc = create_calculation_and_solve(flow_system_piecewise_conversion_scenarios, @@ -312,21 +323,23 @@ def test_io_persistance(flow_system_piecewise_conversion_scenarios): def test_scenarios_selection(flow_system_piecewise_conversion_scenarios): - flow_system = flow_system_piecewise_conversion_scenarios - scenarios = flow_system_piecewise_conversion_scenarios.time_series_collection.scenarios + flow_system_full = flow_system_piecewise_conversion_scenarios + scenarios = flow_system_full.scenarios weights = np.linspace(0.5, 1, len(scenarios)) / np.sum(np.linspace(0.5, 1, len(scenarios))) - flow_system_piecewise_conversion_scenarios.scenario_weights = weights - calc = fx.FullCalculation(flow_system=flow_system_piecewise_conversion_scenarios, - selected_scenarios=flow_system.time_series_collection.scenarios[0:2], - name='test_full_scenario') + flow_system_full.scenario_weights = weights + flow_system = flow_system_full.sel(scenario=scenarios[0:2]) + + assert flow_system.scenarios.equals(flow_system_full.scenarios[0:2]) + + np.testing.assert_allclose(flow_system.scenario_weights.values, flow_system_full.scenario_weights[0:2]) + + + calc = fx.FullCalculation(flow_system=flow_system, name='test_full_scenario') calc.do_modeling() calc.solve(fx.solvers.GurobiSolver(mip_gap=0.01, time_limit_seconds=60)) calc.results.to_file() - flow_system_2 = fx.FlowSystem.from_dataset(calc.results.flow_system_data) - - assert calc.results.solution.indexes['scenario'].equals(flow_system.time_series_collection.scenarios[0:2]) - assert flow_system_2.time_series_collection.scenarios.equals(flow_system.time_series_collection.scenarios[0:2]) + xr.testing.assert_allclose(calc.results.objective, calc.results.solution['costs|total'] * flow_system.scenario_weights) - np.testing.assert_allclose(flow_system_2.scenario_weights.selected_data.values, weights[0:2]) + assert calc.results.solution.indexes['scenario'].equals(flow_system_full.scenarios[0:2]) From 2882147805e0ebb3549ab790f435f7b40d097fab Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 2 Jul 2025 15:58:31 +0200 Subject: [PATCH 160/448] Improve util method --- flixopt/calculation.py | 4 ++-- flixopt/utils.py | 33 ++++++++++++++++++++++++++++++--- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/flixopt/calculation.py b/flixopt/calculation.py index 88e509438..b7d17eef6 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -138,7 +138,7 @@ def main_results(self) -> Dict[str, Union[Scalar, Dict]]: ], } - return utils.round_floats(main_results) + return utils.round_nested_floats(main_results) @property def summary(self): @@ -205,7 +205,7 @@ def solve(self, solver: _Solver, log_file: Optional[pathlib.Path] = None, log_ma logger.info( '\n' + yaml.dump( - utils.round_floats(self.main_results), + utils.round_nested_floats(self.main_results), default_flow_style=False, sort_keys=False, allow_unicode=True, diff --git a/flixopt/utils.py b/flixopt/utils.py index 3e65328a4..1b5ad4497 100644 --- a/flixopt/utils.py +++ b/flixopt/utils.py @@ -11,11 +11,38 @@ logger = logging.getLogger('flixopt') -def round_floats(obj, decimals=2): +def round_nested_floats(obj, decimals=2): + """Recursively round floating point numbers in nested data structures. + + This function traverses nested data structures (dictionaries, lists) and rounds + any floating point numbers to the specified number of decimal places. It handles + various data types including NumPy arrays and xarray DataArrays by converting + them to lists with rounded values. + + Args: + obj: The object to process. Can be a dict, list, float, int, numpy.ndarray, + xarray.DataArray, or any other type. + decimals (int, optional): Number of decimal places to round to. Defaults to 2. + + Returns: + The processed object with the same structure as the input, but with all + floating point numbers rounded to the specified precision. NumPy arrays + and xarray DataArrays are converted to lists. + + Examples: + >>> data = {'a': 3.14159, 'b': [1.234, 2.678]} + >>> round_nested_floats(data, decimals=2) + {'a': 3.14, 'b': [1.23, 2.68]} + + >>> import numpy as np + >>> arr = np.array([1.234, 5.678]) + >>> round_nested_floats(arr, decimals=1) + [1.2, 5.7] + """ if isinstance(obj, dict): - return {k: round_floats(v, decimals) for k, v in obj.items()} + return {k: round_nested_floats(v, decimals) for k, v in obj.items()} elif isinstance(obj, list): - return [round_floats(v, decimals) for v in obj] + return [round_nested_floats(v, decimals) for v in obj] elif isinstance(obj, float): return round(obj, decimals) elif isinstance(obj, int): From 50491ffd3ae770c75ad6bf0f177d962498b41372 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 2 Jul 2025 16:02:10 +0200 Subject: [PATCH 161/448] Add objective to solution dataset. --- flixopt/results.py | 7 ++++++- flixopt/structure.py | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/flixopt/results.py b/flixopt/results.py index 7a6f7926e..6f522e55c 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -221,7 +221,12 @@ def storages(self) -> List['ComponentResults']: @property def objective(self) -> float: """The objective result of the optimization.""" - return self.summary['Main Results']['Objective'] + # Deprecated. Fallback + if 'objective' not in self.solution: + logger.warning('Objective not found in solution. Fallback to summary (rounded value). This is deprecated') + return self.summary['Main Results']['Objective'] + + return self.solution['objective'].item() @property def variables(self) -> linopy.Variables: diff --git a/flixopt/structure.py b/flixopt/structure.py index cbf8af599..994ff5fc7 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -71,6 +71,7 @@ def do_modeling(self): @property def solution(self): solution = super().solution + solution['objective'] = self.objective.value solution.attrs = { 'Components': { comp.label_full: comp.model.results_structure() From 4f3a7981e01d7d16e7d4f977389bef04b0ee1886 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 2 Jul 2025 16:02:50 +0200 Subject: [PATCH 162/448] Update handling of scenario_weights update tests --- flixopt/structure.py | 6 +++--- tests/test_scenarios.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/flixopt/structure.py b/flixopt/structure.py index 994ff5fc7..b5b458cdb 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -137,10 +137,10 @@ def get_coords( return tuple(coords.values()) @property - def scenario_weights(self) -> xr.DataArray: + def scenario_weights(self) -> Union[int, xr.DataArray]: """Returns the scenario weights of the FlowSystem.""" - if self.flow_system.scenarios is None: - return xr.DataArray(1) + if self.flow_system.scenario_weights is None: + return 1 return self.flow_system.scenario_weights diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py index cc0be36c6..cd3de4407 100644 --- a/tests/test_scenarios.py +++ b/tests/test_scenarios.py @@ -340,6 +340,6 @@ def test_scenarios_selection(flow_system_piecewise_conversion_scenarios): calc.results.to_file() - xr.testing.assert_allclose(calc.results.objective, calc.results.solution['costs|total'] * flow_system.scenario_weights) + np.testing.assert_allclose(calc.results.objective, ((calc.results.solution['costs|total'] * flow_system.scenario_weights).sum() + calc.results.solution['Penalty|total']).item()) ## Acount for rounding errors assert calc.results.solution.indexes['scenario'].equals(flow_system_full.scenarios[0:2]) From 314aef9333e63a2898ea74cd9fb249c69080905f Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 2 Jul 2025 16:10:52 +0200 Subject: [PATCH 163/448] Ruff check. Fix type hints --- flixopt/components.py | 2 +- flixopt/core.py | 2 +- flixopt/features.py | 2 +- flixopt/flow_system.py | 20 ++++++++++++++++++-- flixopt/interface.py | 24 ++++++++++++------------ flixopt/results.py | 5 +---- flixopt/structure.py | 2 +- tests/test_component.py | 8 +++++++- 8 files changed, 42 insertions(+), 23 deletions(-) diff --git a/flixopt/components.py b/flixopt/components.py index cf05af0ed..00d0c073f 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -10,7 +10,7 @@ import xarray as xr from . import utils -from .core import PlausibilityError, Scalar, TemporalData, TemporalDataUser, NonTemporalDataUser +from .core import NonTemporalDataUser, PlausibilityError, Scalar, TemporalData, TemporalDataUser from .elements import Component, ComponentModel, Flow from .features import InvestmentModel, OnOffModel, PiecewiseModel from .interface import InvestParameters, OnOffParameters, PiecewiseConversion diff --git a/flixopt/core.py b/flixopt/core.py index 43b529421..7d8db491e 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -5,7 +5,7 @@ import logging import warnings -from typing import Dict, Optional, Union, Tuple, Literal +from typing import Dict, Literal, Optional, Tuple, Union import numpy as np import pandas as pd diff --git a/flixopt/features.py b/flixopt/features.py index 5e0ed0e80..17bb43bcd 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -10,7 +10,7 @@ import numpy as np from .config import CONFIG -from .core import Scalar, TemporalData, NonTemporalData +from .core import NonTemporalData, Scalar, TemporalData from .interface import InvestParameters, OnOffParameters, Piecewise from .structure import Model, SystemModel diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 74492bcd3..2cec86073 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -16,8 +16,24 @@ from rich.pretty import Pretty from . import io as fx_io -from .core import ConversionError, DataConverter, TemporalData, TemporalDataUser, TimeSeriesData, NonTemporalDataUser, NonTemporalData, FlowSystemDimensions -from .effects import Effect, EffectCollection, NonTemporalEffects, NonTemporalEffectsUser, TemporalEffects, TemporalEffectsUser +from .core import ( + ConversionError, + DataConverter, + FlowSystemDimensions, + NonTemporalData, + NonTemporalDataUser, + TemporalData, + TemporalDataUser, + TimeSeriesData, +) +from .effects import ( + Effect, + EffectCollection, + NonTemporalEffects, + NonTemporalEffectsUser, + TemporalEffects, + TemporalEffectsUser, +) from .elements import Bus, Component, Flow from .structure import Element, Interface, SystemModel diff --git a/flixopt/interface.py b/flixopt/interface.py index 81f351ebe..7abf574c6 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -7,11 +7,11 @@ from typing import TYPE_CHECKING, Dict, Iterator, List, Literal, Optional, Union from .config import CONFIG -from .core import Scalar, TemporalDataUser, NonTemporalDataUser, NonTemporalData +from .core import NonTemporalData, NonTemporalDataUser, Scalar, TemporalDataUser from .structure import Interface, register_class_for_io if TYPE_CHECKING: # for type checking and preventing circular imports - from .effects import EffectValuesUserScenario, EffectValuesUserTimestep + from .effects import NonTemporalEffectsUser, TemporalEffectsUser from .flow_system import FlowSystem @@ -150,10 +150,10 @@ def __init__( minimum_size: Optional[NonTemporalDataUser] = None, maximum_size: Optional[NonTemporalDataUser] = None, optional: bool = True, # Investition ist weglassbar - fix_effects: Optional['EffectValuesUserScenario'] = None, - specific_effects: Optional['EffectValuesUserScenario'] = None, # costs per Flow-Unit/Storage-Size/... + fix_effects: Optional[NonTemporalEffectsUser] = None, + specific_effects: Optional[NonTemporalEffectsUser] = None, # costs per Flow-Unit/Storage-Size/... piecewise_effects: Optional[PiecewiseEffects] = None, - divest_effects: Optional['EffectValuesUserScenario'] = None, + divest_effects: Optional[NonTemporalEffectsUser] = None, investment_scenarios: Optional[Union[Literal['individual'], List[Union[int, str]]]] = None, ): """ @@ -173,11 +173,11 @@ def __init__( - List of scenario names: Optimize the size for the passed scenario names (equal size in all). All other scenarios will have the size 0. - None: Equals to a list of all scenarios (default) """ - self.fix_effects: EffectValuesUserScenario = fix_effects if fix_effects is not None else {} - self.divest_effects: EffectValuesUserScenario = divest_effects if divest_effects is not None else {} + self.fix_effects: NonTemporalEffectsUser = fix_effects if fix_effects is not None else {} + self.divest_effects: NonTemporalEffectsUser = divest_effects if divest_effects is not None else {} self.fixed_size = fixed_size self.optional = optional - self.specific_effects: EffectValuesUserScenario = specific_effects if specific_effects is not None else {} + self.specific_effects: NonTemporalEffectsUser = specific_effects if specific_effects is not None else {} self.piecewise_effects = piecewise_effects self.minimum_size = minimum_size if minimum_size is not None else CONFIG.modeling.EPSILON self.maximum_size = maximum_size if maximum_size is not None else CONFIG.modeling.BIG # default maximum @@ -246,8 +246,8 @@ def maximum_or_fixed_size(self) -> NonTemporalData: class OnOffParameters(Interface): def __init__( self, - effects_per_switch_on: Optional['EffectValuesUser'] = None, - effects_per_running_hour: Optional['EffectValuesUser'] = None, + effects_per_switch_on: Optional[NonTemporalEffectsUser] = None, + effects_per_running_hour: Optional[NonTemporalEffectsUser] = None, on_hours_total_min: Optional[int] = None, on_hours_total_max: Optional[int] = None, consecutive_on_hours_min: Optional[TemporalDataUser] = None, @@ -277,8 +277,8 @@ def __init__( switch_on_total_max: max nr of switchOn operations force_switch_on: force creation of switch on variable, even if there is no switch_on_total_max """ - self.effects_per_switch_on: EffectValuesUserTimestep = effects_per_switch_on or {} - self.effects_per_running_hour: EffectValuesUserTimestep = effects_per_running_hour or {} + self.effects_per_switch_on: TemporalEffectsUser = effects_per_switch_on or {} + self.effects_per_running_hour: TemporalEffectsUser = effects_per_running_hour or {} self.on_hours_total_min: Scalar = on_hours_total_min self.on_hours_total_max: Scalar = on_hours_total_max self.consecutive_on_hours_min: TemporalDataUser = consecutive_on_hours_min diff --git a/flixopt/results.py b/flixopt/results.py index 6f522e55c..97dfc136a 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -1161,10 +1161,7 @@ def size(self) -> xr.DataArray: if name in self.solution: return self.solution[name] try: - return DataConverter.as_dataarray( - self._calculation_results.flow_system.flows[self.label].size, - scenarios=self._calculation_results.scenarios - ).rename(name) + return self._calculation_results.flow_system.flows[self.label].size.rename(name) except _FlowSystemRestorationError: logger.critical(f'Size of flow {self.label}.size not availlable. Returning NaN') return xr.DataArray(np.nan).rename(name) diff --git a/flixopt/structure.py b/flixopt/structure.py index b5b458cdb..5edee1bd3 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -19,7 +19,7 @@ from . import io as fx_io from .config import CONFIG -from .core import Scalar, TemporalDataUser, TimeSeriesData, get_dataarray_stats, NonTemporalData +from .core import NonTemporalData, Scalar, TemporalDataUser, TimeSeriesData, get_dataarray_stats if TYPE_CHECKING: # for type checking and preventing circular imports from .effects import EffectCollectionModel diff --git a/tests/test_component.py b/tests/test_component.py index 8a99b5d5b..11b5385c2 100644 --- a/tests/test_component.py +++ b/tests/test_component.py @@ -6,7 +6,13 @@ import flixopt as fx import flixopt.elements -from .conftest import assert_conequal, assert_var_equal, create_linopy_model, create_calculation_and_solve, assert_almost_equal_numeric +from .conftest import ( + assert_almost_equal_numeric, + assert_conequal, + assert_var_equal, + create_calculation_and_solve, + create_linopy_model, +) class TestComponentModel: From 0302947358667715af74531aca19b19330042203 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 3 Jul 2025 10:30:47 +0200 Subject: [PATCH 164/448] Fix type hints and improve None handling --- flixopt/effects.py | 6 ++++-- flixopt/features.py | 2 +- flixopt/interface.py | 20 ++++++++++---------- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/flixopt/effects.py b/flixopt/effects.py index dc1d7d07f..381b5a3de 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -77,9 +77,11 @@ def __init__( self.is_standard = is_standard self.is_objective = is_objective self.specific_share_to_other_effects_operation: TemporalEffectsUser = ( - specific_share_to_other_effects_operation or {} + specific_share_to_other_effects_operation if specific_share_to_other_effects_operation is not None else {} + ) + self.specific_share_to_other_effects_invest: NonTemporalEffectsUser = ( + specific_share_to_other_effects_invest if specific_share_to_other_effects_invest is not None else {} ) - self.specific_share_to_other_effects_invest: NonTemporalEffectsUser = specific_share_to_other_effects_invest or {} self.minimum_operation = minimum_operation self.maximum_operation = maximum_operation self.minimum_operation_per_hour: TemporalDataUser = minimum_operation_per_hour diff --git a/flixopt/features.py b/flixopt/features.py index 17bb43bcd..4e47ace7f 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -274,7 +274,7 @@ def __init__( self._on_hours_total_min = on_hours_total_min if on_hours_total_min is not None else 0 self._on_hours_total_max = on_hours_total_max if on_hours_total_max is not None else np.inf self._use_off = use_off - self._effects_per_running_hour = effects_per_running_hour or {} + self._effects_per_running_hour = effects_per_running_hour if effects_per_running_hour is not None else {} self.on = None self.total_on_hours: Optional[linopy.Variable] = None diff --git a/flixopt/interface.py b/flixopt/interface.py index 7abf574c6..76e74616b 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -150,10 +150,10 @@ def __init__( minimum_size: Optional[NonTemporalDataUser] = None, maximum_size: Optional[NonTemporalDataUser] = None, optional: bool = True, # Investition ist weglassbar - fix_effects: Optional[NonTemporalEffectsUser] = None, - specific_effects: Optional[NonTemporalEffectsUser] = None, # costs per Flow-Unit/Storage-Size/... + fix_effects: Optional['NonTemporalEffectsUser'] = None, + specific_effects: Optional['NonTemporalEffectsUser'] = None, # costs per Flow-Unit/Storage-Size/... piecewise_effects: Optional[PiecewiseEffects] = None, - divest_effects: Optional[NonTemporalEffectsUser] = None, + divest_effects: Optional['NonTemporalEffectsUser'] = None, investment_scenarios: Optional[Union[Literal['individual'], List[Union[int, str]]]] = None, ): """ @@ -173,11 +173,11 @@ def __init__( - List of scenario names: Optimize the size for the passed scenario names (equal size in all). All other scenarios will have the size 0. - None: Equals to a list of all scenarios (default) """ - self.fix_effects: NonTemporalEffectsUser = fix_effects if fix_effects is not None else {} - self.divest_effects: NonTemporalEffectsUser = divest_effects if divest_effects is not None else {} + self.fix_effects: 'NonTemporalEffectsUser' = fix_effects if fix_effects is not None else {} + self.divest_effects: 'NonTemporalEffectsUser' = divest_effects if divest_effects is not None else {} self.fixed_size = fixed_size self.optional = optional - self.specific_effects: NonTemporalEffectsUser = specific_effects if specific_effects is not None else {} + self.specific_effects: 'NonTemporalEffectsUser' = specific_effects if specific_effects is not None else {} self.piecewise_effects = piecewise_effects self.minimum_size = minimum_size if minimum_size is not None else CONFIG.modeling.EPSILON self.maximum_size = maximum_size if maximum_size is not None else CONFIG.modeling.BIG # default maximum @@ -246,8 +246,8 @@ def maximum_or_fixed_size(self) -> NonTemporalData: class OnOffParameters(Interface): def __init__( self, - effects_per_switch_on: Optional[NonTemporalEffectsUser] = None, - effects_per_running_hour: Optional[NonTemporalEffectsUser] = None, + effects_per_switch_on: Optional['NonTemporalEffectsUser'] = None, + effects_per_running_hour: Optional['NonTemporalEffectsUser'] = None, on_hours_total_min: Optional[int] = None, on_hours_total_max: Optional[int] = None, consecutive_on_hours_min: Optional[TemporalDataUser] = None, @@ -277,8 +277,8 @@ def __init__( switch_on_total_max: max nr of switchOn operations force_switch_on: force creation of switch on variable, even if there is no switch_on_total_max """ - self.effects_per_switch_on: TemporalEffectsUser = effects_per_switch_on or {} - self.effects_per_running_hour: TemporalEffectsUser = effects_per_running_hour or {} + self.effects_per_switch_on: 'TemporalEffectsUser' = effects_per_switch_on if effects_per_switch_on is not None else {} + self.effects_per_running_hour: 'TemporalEffectsUser' = effects_per_running_hour if effects_per_running_hour is not None else {} self.on_hours_total_min: Scalar = on_hours_total_min self.on_hours_total_max: Scalar = on_hours_total_max self.consecutive_on_hours_min: TemporalDataUser = consecutive_on_hours_min From 40c437a533fc74710f4e5f075a9f1c7a5f9989d8 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 3 Jul 2025 10:52:48 +0200 Subject: [PATCH 165/448] Fix coords in AggregatedCalculation --- flixopt/calculation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flixopt/calculation.py b/flixopt/calculation.py index b7d17eef6..e035aaa13 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -323,7 +323,7 @@ def _perform_aggregation(self): if self.aggregation_parameters.aggregate_data_and_fix_non_binary_vars: ds = self.flow_system.to_dataset() for name, series in self.aggregation.aggregated_data.items(): - da = DataConverter.to_dataarray(series, timesteps=self.flow_system.timesteps).rename(name).assign_attrs(ds[name].attrs) + da = DataConverter.to_dataarray(series, self.flow_system.coords).rename(name).assign_attrs(ds[name].attrs) if TimeSeriesData.is_timeseries_data(da): da = TimeSeriesData.from_dataarray(da) From 8d2d208d4bce947c2487ceebb6c33cad0d210fe2 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 3 Jul 2025 11:23:14 +0200 Subject: [PATCH 166/448] Improve Error Messages of DataConversion --- flixopt/flow_system.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 2cec86073..0568f3b3a 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -349,9 +349,13 @@ def fit_to_model_coords( ).rename(name) except ConversionError as e: raise ConversionError( - f'Could not convert time series data "{name}" to DataArray: Original Error: {e}') from e + f'Could not convert time series data "{name}" to DataArray:\n{data}\nOriginal Error: {e}') from e else: - return DataConverter.to_dataarray(data, coords=coords).rename(name) + try: + return DataConverter.to_dataarray(data, coords=coords).rename(name) + except ConversionError as e: + raise ConversionError( + f'Could not convert data "{name}" to DataArray:\n{data}\nOriginal Error: {e}') from e def fit_effects_to_model_coords( self, From 9c0c95fe86b51acc719c258265fd6fd38e18accc Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 3 Jul 2025 12:59:44 +0200 Subject: [PATCH 167/448] Allow multi dim data conversion and broadcasting by length --- flixopt/core.py | 89 ++++++++-- tests/test_dataconverter.py | 330 +++++++++++++++++++++++++++++++++++- 2 files changed, 407 insertions(+), 12 deletions(-) diff --git a/flixopt/core.py b/flixopt/core.py index 7d8db491e..ad5b011ba 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -279,6 +279,63 @@ def _broadcast_dataarray_to_more_dims( return xr.DataArray(result_data.copy(), coords=final_coords, dims=target_dims) + @staticmethod + def _convert_multid_array_to_dataarray( + data: np.ndarray, coords: Dict[str, pd.Index], target_dims: Tuple[str, ...] + ) -> xr.DataArray: + """ + Convert multi-dimensional numpy array to DataArray by matching dimensions by length. + + Args: + data: Multi-dimensional numpy array + coords: Available coordinates + target_dims: Target dimension names + + Returns: + DataArray with dimensions matched by length + + Raises: + ConversionError: If array dimensions cannot be uniquely matched to coordinates + """ + if len(target_dims) == 0: + if data.size != 1: + raise ConversionError('Cannot convert multi-element array without target dimensions') + return xr.DataArray(data.item()) + + if data.ndim != len(target_dims): + raise ConversionError(f'Array has {data.ndim} dimensions but {len(target_dims)} target dimensions provided') + + # Get lengths of each dimension + array_shape = data.shape + coord_lengths = {dim: len(coords[dim]) for dim in target_dims} + + # Try to find a unique mapping from array dimensions to coordinate dimensions + possible_mappings = [] + + # Generate all possible permutations of target dimensions + from itertools import permutations + + for dim_order in permutations(target_dims): + # Check if this permutation matches the array shape + if all(array_shape[i] == coord_lengths[dim_order[i]] for i in range(len(dim_order))): + possible_mappings.append(dim_order) + + if len(possible_mappings) == 0: + shape_info = f'Array shape: {array_shape}, Coordinate lengths: {coord_lengths}' + raise ConversionError(f'Array dimensions do not match any coordinate lengths. {shape_info}') + + if len(possible_mappings) > 1: + raise ConversionError( + f'Array shape {array_shape} matches multiple dimension orders: {possible_mappings}. ' + 'Cannot uniquely determine dimension mapping.' + ) + + # Use the unique mapping found + matched_dims = possible_mappings[0] + matched_coords = {dim: coords[dim] for dim in matched_dims} + + return xr.DataArray(data.copy(), coords=matched_coords, dims=matched_dims) + @staticmethod def to_dataarray( data: Union[Scalar, np.ndarray, pd.Series, pd.DataFrame, xr.DataArray, TimeSeriesData], @@ -289,7 +346,8 @@ def to_dataarray( Accepts: - Scalars (broadcast to all dimensions) - - 1D arrays, Series, or single-column DataFrames (matched to one dimension, broadcast to others) + - 1D arrays or Series (matched to one dimension, broadcast to others) + - Multi-D arrays or DataFrames (dimensions matched by length) - xr.DataArray (validated and potentially broadcast to additional dimensions) Args: @@ -304,31 +362,42 @@ def to_dataarray( validated_coords, target_dims = DataConverter._prepare_dimensions(coords) - # Step 1: Convert to DataArray (with safe 1D/2D logic for simple data) + # Step 1: Convert to DataArray if isinstance(data, (int, float, np.integer, np.floating)): # Scalars: create 0D DataArray, will be broadcast later intermediate = xr.DataArray(data.item() if hasattr(data, 'item') else data) elif isinstance(data, np.ndarray): - if data.ndim != 1: - raise ConversionError(f'Only 1D arrays supported, got {data.ndim}D array') - intermediate = DataConverter._convert_1d_data_to_dataarray(data, validated_coords, target_dims) + if data.ndim == 1: + intermediate = DataConverter._convert_1d_data_to_dataarray(data, validated_coords, target_dims) + else: + # Handle multi-dimensional arrays + intermediate = DataConverter._convert_multid_array_to_dataarray(data, validated_coords, target_dims) elif isinstance(data, pd.Series): + if isinstance(data.index, pd.MultiIndex): + raise ConversionError('Series index must be a single level Index. Multi-index Series are not supported.') intermediate = DataConverter._convert_1d_data_to_dataarray(data, validated_coords, target_dims) elif isinstance(data, pd.DataFrame): - if len(data.columns) != 1: - raise ConversionError(f'Only single-column DataFrames are supported, got {len(data.columns)} columns') - series = data.iloc[:, 0] - intermediate = DataConverter._convert_1d_data_to_dataarray(series, validated_coords, target_dims) + if isinstance(data.index, pd.MultiIndex): + raise ConversionError('DataFrame index must be a single level Index. Multi-index DataFrames are not supported.') + if len(data.columns) == 0 or data.empty: + raise ConversionError('DataFrame must have at least one column.') + + if len(data.columns) == 1: + intermediate = DataConverter._convert_1d_data_to_dataarray(data.iloc[:, 0], validated_coords, target_dims) + else: + # Handle multi-column DataFrames + logger.warning(f'Converting multi-column DataFrame to xr.DataArray. We advise to do this manually.') + intermediate = DataConverter._convert_multid_array_to_dataarray(data.to_numpy(), validated_coords, target_dims) elif isinstance(data, xr.DataArray): intermediate = data.copy() else: raise ConversionError( - f'Unsupported data type: {type(data).__name__}. Only scalars, 1D arrays, Series, single-column DataFrames, and DataArrays are supported.' + f'Unsupported data type: {type(data).__name__}. Only scalars, arrays, Series, DataFrames, and DataArrays are supported.' ) # Step 2: Broadcast to target dimensions if needed diff --git a/tests/test_dataconverter.py b/tests/test_dataconverter.py index 56f36aabf..659528c4d 100644 --- a/tests/test_dataconverter.py +++ b/tests/test_dataconverter.py @@ -211,14 +211,14 @@ def test_multi_column_dataframe_rejected(self, time_coords): 'value2': [15, 25, 35, 45, 55] }, index=time_coords) - with pytest.raises(ConversionError, match="Only single-column DataFrames are supported"): + with pytest.raises(ConversionError, match="Array has 2 dimensions but 1 target "): DataConverter.to_dataarray(df, coords={'time': time_coords}) def test_empty_dataframe_rejected(self, time_coords): """Empty DataFrame should be rejected.""" df = pd.DataFrame(index=time_coords) # No columns - with pytest.raises(ConversionError, match="Only single-column DataFrames are supported"): + with pytest.raises(ConversionError, match="DataFrame must have at least one"): DataConverter.to_dataarray(df, coords={'time': time_coords}) def test_dataframe_broadcast(self, time_coords, scenario_coords): @@ -855,5 +855,331 @@ def test_mixed_numeric_types(self, time_coords): assert np.array_equal(result.values, mixed_arr) +class TestMultiDimensionalArrayConversion: + """Test multi-dimensional numpy array conversions.""" + + @pytest.fixture + def standard_coords(self): + """Standard coordinates with unique lengths for easy testing.""" + return { + 'time': pd.date_range('2024-01-01', periods=5, freq='D', name='time'), # length 5 + 'scenario': pd.Index(['A', 'B', 'C'], name='scenario'), # length 3 + 'region': pd.Index(['north', 'south'], name='region') # length 2 + } + + def test_2d_array_unique_dimensions(self, standard_coords): + """2D array with unique dimension lengths should work.""" + # 5x3 array should map to time x scenario + data_2d = np.random.rand(5, 3) + result = DataConverter.to_dataarray(data_2d, coords={ + 'time': standard_coords['time'], + 'scenario': standard_coords['scenario'] + }) + + assert result.shape == (5, 3) + assert result.dims == ('time', 'scenario') + assert np.array_equal(result.values, data_2d) + + # 3x5 array should map to scenario x time + data_2d_flipped = np.random.rand(3, 5) + result_flipped = DataConverter.to_dataarray(data_2d_flipped, coords={ + 'time': standard_coords['time'], + 'scenario': standard_coords['scenario'] + }) + + assert result_flipped.shape == (3, 5) + assert result_flipped.dims == ('scenario', 'time') + assert np.array_equal(result_flipped.values, data_2d_flipped) + + def test_3d_array_unique_dimensions(self, standard_coords): + """3D array with unique dimension lengths should work.""" + # 5x3x2 array should map to time x scenario x region + data_3d = np.random.rand(5, 3, 2) + result = DataConverter.to_dataarray(data_3d, coords=standard_coords) + + assert result.shape == (5, 3, 2) + assert result.dims == ('time', 'scenario', 'region') + assert np.array_equal(result.values, data_3d) + + def test_3d_array_different_permutation(self, standard_coords): + """3D array with different dimension order should work.""" + # 2x5x3 array should map to region x time x scenario + data_3d = np.random.rand(2, 5, 3) + result = DataConverter.to_dataarray(data_3d, coords=standard_coords) + + assert result.shape == (2, 5, 3) + assert result.dims == ('region', 'time', 'scenario') + assert np.array_equal(result.values, data_3d) + + def test_4d_array_unique_dimensions(self): + """4D array with unique dimension lengths should work.""" + coords = { + 'time': pd.date_range('2024-01-01', periods=2, freq='D', name='time'), # length 2 + 'scenario': pd.Index(['A', 'B', 'C'], name='scenario'), # length 3 + 'region': pd.Index(['north', 'south', 'east', 'west'], name='region'), # length 4 + 'technology': pd.Index(['solar', 'wind', 'gas', 'coal', 'hydro'], name='technology') # length 5 + } + + # 3x5x2x4 array should map to scenario x technology x time x region + data_4d = np.random.rand(3, 5, 2, 4) + result = DataConverter.to_dataarray(data_4d, coords=coords) + + assert result.shape == (3, 5, 2, 4) + assert result.dims == ('scenario', 'technology', 'time', 'region') + assert np.array_equal(result.values, data_4d) + + def test_2d_array_ambiguous_dimensions_error(self): + """2D array with ambiguous dimension lengths should fail.""" + # Both dimensions have length 3 + coords_ambiguous = { + 'scenario': pd.Index(['A', 'B', 'C'], name='scenario'), # length 3 + 'region': pd.Index(['north', 'south', 'east'], name='region') # length 3 + } + + data_2d = np.random.rand(3, 3) + with pytest.raises(ConversionError, match="matches multiple dimension orders"): + DataConverter.to_dataarray(data_2d, coords=coords_ambiguous) + + def test_3d_array_ambiguous_dimensions_error(self): + """3D array with ambiguous dimension lengths should fail.""" + # All dimensions have length 2 + coords_ambiguous = { + 'scenario': pd.Index(['A', 'B'], name='scenario'), # length 2 + 'region': pd.Index(['north', 'south'], name='region'), # length 2 + 'technology': pd.Index(['solar', 'wind'], name='technology') # length 2 + } + + data_3d = np.random.rand(2, 2, 2) + with pytest.raises(ConversionError, match="matches multiple dimension orders"): + DataConverter.to_dataarray(data_3d, coords=coords_ambiguous) + + def test_array_dimension_count_mismatch_error(self, standard_coords): + """Array with wrong number of dimensions should fail.""" + # 2D array with 3D coordinates + data_2d = np.random.rand(5, 3) + with pytest.raises(ConversionError, match="Array has 2 dimensions but 3 target dimensions provided"): + DataConverter.to_dataarray(data_2d, coords=standard_coords) + + # 4D array with 3D coordinates + data_4d = np.random.rand(5, 3, 2, 4) + with pytest.raises(ConversionError, match="Array has 4 dimensions but 3 target dimensions provided"): + DataConverter.to_dataarray(data_4d, coords=standard_coords) + + def test_array_no_matching_dimensions_error(self, standard_coords): + """Array with no matching dimension lengths should fail.""" + # 7x8 array - no dimension has length 7 or 8 + data_2d = np.random.rand(7, 8) + coords_2d = { + 'time': standard_coords['time'], # length 5 + 'scenario': standard_coords['scenario'] # length 3 + } + + with pytest.raises(ConversionError, match="Array dimensions do not match any coordinate lengths"): + DataConverter.to_dataarray(data_2d, coords=coords_2d) + + def test_2d_array_custom_dimensions(self): + """2D array with custom dimension names should work.""" + coords = { + 'product': pd.Index(['A', 'B', 'C', 'D'], name='product'), # length 4 + 'factory': pd.Index(['F1', 'F2', 'F3'], name='factory') # length 3 + } + + # 4x3 array should map to product x factory + data_2d = np.array([[10, 11, 12], + [20, 21, 22], + [30, 31, 32], + [40, 41, 42]]) + + result = DataConverter.to_dataarray(data_2d, coords=coords) + + assert result.shape == (4, 3) + assert result.dims == ('product', 'factory') + assert np.array_equal(result.values, data_2d) + + # Verify coordinates are correct + assert result.indexes['product'].equals(coords['product']) + assert result.indexes['factory'].equals(coords['factory']) + + def test_multid_array_copy_independence(self, standard_coords): + """Multi-D arrays should be independent copies.""" + original_data = np.random.rand(5, 3) + result = DataConverter.to_dataarray(original_data, coords={ + 'time': standard_coords['time'], + 'scenario': standard_coords['scenario'] + }) + + # Modify result + result[0, 0] = 999 + + # Original should be unchanged + assert original_data[0, 0] != 999 + + def test_multid_array_special_values(self, standard_coords): + """Multi-D arrays should preserve special values.""" + # Create 2D array with special values + data_2d = np.array([[1.0, np.nan, 3.0], + [np.inf, 5.0, -np.inf], + [7.0, 8.0, 9.0], + [10.0, np.nan, 12.0], + [13.0, 14.0, np.inf]]) + + result = DataConverter.to_dataarray(data_2d, coords={ + 'time': standard_coords['time'], + 'scenario': standard_coords['scenario'] + }) + + assert result.shape == (5, 3) + assert np.array_equal(np.isnan(result.values), np.isnan(data_2d)) + assert np.array_equal(np.isinf(result.values), np.isinf(data_2d)) + + def test_multid_array_with_time_dimension(self): + """Multi-D arrays should work with time dimension.""" + time_coords = pd.date_range('2024-01-01', periods=4, freq='H', name='time') + scenario_coords = pd.Index(['base', 'high', 'low'], name='scenario') + + # 4x3 time series data + data_2d = np.array([[100, 110, 120], + [200, 210, 220], + [300, 310, 320], + [400, 410, 420]]) + + result = DataConverter.to_dataarray(data_2d, coords={ + 'time': time_coords, + 'scenario': scenario_coords + }) + + assert result.shape == (4, 3) + assert result.dims == ('time', 'scenario') + assert isinstance(result.indexes['time'], pd.DatetimeIndex) + assert np.array_equal(result.values, data_2d) + + def test_multid_array_dtype_preservation(self, standard_coords): + """Multi-D arrays should preserve data types.""" + # Integer array + int_data = np.array([[1, 2, 3], + [4, 5, 6], + [7, 8, 9], + [10, 11, 12], + [13, 14, 15]], dtype=np.int32) + + result_int = DataConverter.to_dataarray(int_data, coords={ + 'time': standard_coords['time'], + 'scenario': standard_coords['scenario'] + }) + + assert result_int.dtype == np.int32 + assert np.array_equal(result_int.values, int_data) + + # Float array + float_data = np.array([[1.1, 2.2, 3.3], + [4.4, 5.5, 6.6], + [7.7, 8.8, 9.9], + [10.1, 11.1, 12.1], + [13.1, 14.1, 15.1]], dtype=np.float64) + + result_float = DataConverter.to_dataarray(float_data, coords={ + 'time': standard_coords['time'], + 'scenario': standard_coords['scenario'] + }) + + assert result_float.dtype == np.float64 + assert np.array_equal(result_float.values, float_data) + + # Boolean array + bool_data = np.array([[True, False, True], + [False, True, False], + [True, True, False], + [False, False, True], + [True, False, True]]) + + result_bool = DataConverter.to_dataarray(bool_data, coords={ + 'time': standard_coords['time'], + 'scenario': standard_coords['scenario'] + }) + + assert result_bool.dtype == bool + assert np.array_equal(result_bool.values, bool_data) + + def test_multid_array_no_coords(self): + """Multi-D arrays without coords should fail unless scalar.""" + # Multi-element fails + data_2d = np.random.rand(2, 3) + with pytest.raises(ConversionError, match="Cannot convert multi-element array without target dimensions"): + DataConverter.to_dataarray(data_2d) + + # Single element succeeds + single_element = np.array([[42]]) + result = DataConverter.to_dataarray(single_element) + assert result.shape == () + assert result.item() == 42 + + def test_multid_array_empty_coords(self, standard_coords): + """Multi-D arrays with empty coords should fail.""" + data_2d = np.random.rand(5, 3) + with pytest.raises(ConversionError, match="Cannot convert multi-element array without target dimensions"): + DataConverter.to_dataarray(data_2d, coords={}) + + def test_multid_array_coordinate_validation(self): + """Multi-D arrays should validate coordinates properly.""" + # Test with time coordinate that's not DatetimeIndex + wrong_time = pd.Index([1, 2, 3, 4, 5], name='time') + scenario_coords = pd.Index(['A', 'B', 'C'], name='scenario') + + data_2d = np.random.rand(5, 3) + with pytest.raises(ConversionError, match="time coordinates must be a DatetimeIndex"): + DataConverter.to_dataarray(data_2d, coords={ + 'time': wrong_time, + 'scenario': scenario_coords + }) + + def test_multid_array_complex_scenario(self): + """Complex real-world scenario with multi-D array.""" + # Energy system data: time x technology x region + coords = { + 'time': pd.date_range('2024-01-01', periods=8760, freq='H', name='time'), # 1 year hourly + 'technology': pd.Index(['solar', 'wind', 'gas', 'coal'], name='technology'), # 4 technologies + 'region': pd.Index(['north', 'south', 'east'], name='region') # 3 regions + } + + # Capacity factors: 8760 x 4 x 3 + capacity_factors = np.random.rand(8760, 4, 3) + + result = DataConverter.to_dataarray(capacity_factors, coords=coords) + + assert result.shape == (8760, 4, 3) + assert result.dims == ('time', 'technology', 'region') + assert isinstance(result.indexes['time'], pd.DatetimeIndex) + assert len(result.indexes['time']) == 8760 + assert len(result.indexes['technology']) == 4 + assert len(result.indexes['region']) == 3 + + def test_multid_array_edge_cases(self): + """Test edge cases for multi-D arrays.""" + # Single dimension with multi-D array should fail + coords_1d = {'time': pd.date_range('2024-01-01', periods=5, freq='D', name='time')} + data_2d = np.random.rand(5, 3) + + with pytest.raises(ConversionError, match="Array has 2 dimensions but 1 target dimensions provided"): + DataConverter.to_dataarray(data_2d, coords=coords_1d) + + # Zero dimensions with multi-D array should fail + data_1d = np.array([1, 2, 3]) + with pytest.raises(ConversionError, match="Cannot convert multi-element array without target dimensions"): + DataConverter.to_dataarray(data_1d, coords={}) + + def test_multid_array_partial_dimension_match(self): + """Array where only some dimensions match should fail.""" + coords = { + 'time': pd.date_range('2024-01-01', periods=5, freq='D', name='time'), # length 5 + 'scenario': pd.Index(['A', 'B', 'C'], name='scenario') # length 3 + } + + # 5x7 array - first dimension matches time (5) but second doesn't match scenario (3) + data_2d = np.random.rand(5, 7) + with pytest.raises(ConversionError, match="Array dimensions do not match any coordinate lengths"): + DataConverter.to_dataarray(data_2d, coords=coords) + + + if __name__ == '__main__': pytest.main() From d568ad605659c81f1dd3de40625be774129a94d0 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 3 Jul 2025 14:13:49 +0200 Subject: [PATCH 168/448] Improve DataConverter to handle multi-dim arrays --- flixopt/core.py | 47 +- tests/test_dataconverter.py | 1139 +++++++++++++---------------------- 2 files changed, 451 insertions(+), 735 deletions(-) diff --git a/flixopt/core.py b/flixopt/core.py index ad5b011ba..97e91c274 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -285,6 +285,7 @@ def _convert_multid_array_to_dataarray( ) -> xr.DataArray: """ Convert multi-dimensional numpy array to DataArray by matching dimensions by length. + Returns a DataArray that may need further broadcasting to target dimensions. Args: data: Multi-dimensional numpy array @@ -292,7 +293,7 @@ def _convert_multid_array_to_dataarray( target_dims: Target dimension names Returns: - DataArray with dimensions matched by length + DataArray with dimensions matched by length (may be subset of target_dims) Raises: ConversionError: If array dimensions cannot be uniquely matched to coordinates @@ -302,23 +303,19 @@ def _convert_multid_array_to_dataarray( raise ConversionError('Cannot convert multi-element array without target dimensions') return xr.DataArray(data.item()) - if data.ndim != len(target_dims): - raise ConversionError(f'Array has {data.ndim} dimensions but {len(target_dims)} target dimensions provided') - # Get lengths of each dimension array_shape = data.shape coord_lengths = {dim: len(coords[dim]) for dim in target_dims} - # Try to find a unique mapping from array dimensions to coordinate dimensions - possible_mappings = [] - - # Generate all possible permutations of target dimensions + # Find all possible ways to match array dimensions to available coordinates from itertools import permutations - for dim_order in permutations(target_dims): + # Try all permutations of target_dims that match the array's number of dimensions + possible_mappings = [] + for dim_subset in permutations(target_dims, data.ndim): # Check if this permutation matches the array shape - if all(array_shape[i] == coord_lengths[dim_order[i]] for i in range(len(dim_order))): - possible_mappings.append(dim_order) + if all(array_shape[i] == coord_lengths[dim_subset[i]] for i in range(len(dim_subset))): + possible_mappings.append(dim_subset) if len(possible_mappings) == 0: shape_info = f'Array shape: {array_shape}, Coordinate lengths: {coord_lengths}' @@ -334,6 +331,7 @@ def _convert_multid_array_to_dataarray( matched_dims = possible_mappings[0] matched_coords = {dim: coords[dim] for dim in matched_dims} + # Return DataArray with matched dimensions - broadcasting will happen later if needed return xr.DataArray(data.copy(), coords=matched_coords, dims=matched_dims) @staticmethod @@ -347,7 +345,7 @@ def to_dataarray( Accepts: - Scalars (broadcast to all dimensions) - 1D arrays or Series (matched to one dimension, broadcast to others) - - Multi-D arrays or DataFrames (dimensions matched by length) + - Multi-D arrays or DataFrames (dimensions matched by length, broadcast to remaining) - xr.DataArray (validated and potentially broadcast to additional dimensions) Args: @@ -362,7 +360,7 @@ def to_dataarray( validated_coords, target_dims = DataConverter._prepare_dimensions(coords) - # Step 1: Convert to DataArray + # Step 1: Convert to DataArray (may have fewer dimensions than target) if isinstance(data, (int, float, np.integer, np.floating)): # Scalars: create 0D DataArray, will be broadcast later intermediate = xr.DataArray(data.item() if hasattr(data, 'item') else data) @@ -371,26 +369,34 @@ def to_dataarray( if data.ndim == 1: intermediate = DataConverter._convert_1d_data_to_dataarray(data, validated_coords, target_dims) else: - # Handle multi-dimensional arrays + # Handle multi-dimensional arrays - this now allows partial matching intermediate = DataConverter._convert_multid_array_to_dataarray(data, validated_coords, target_dims) elif isinstance(data, pd.Series): if isinstance(data.index, pd.MultiIndex): - raise ConversionError('Series index must be a single level Index. Multi-index Series are not supported.') + raise ConversionError( + 'Series index must be a single level Index. Multi-index Series are not supported.' + ) intermediate = DataConverter._convert_1d_data_to_dataarray(data, validated_coords, target_dims) elif isinstance(data, pd.DataFrame): if isinstance(data.index, pd.MultiIndex): - raise ConversionError('DataFrame index must be a single level Index. Multi-index DataFrames are not supported.') + raise ConversionError( + 'DataFrame index must be a single level Index. Multi-index DataFrames are not supported.' + ) if len(data.columns) == 0 or data.empty: raise ConversionError('DataFrame must have at least one column.') if len(data.columns) == 1: - intermediate = DataConverter._convert_1d_data_to_dataarray(data.iloc[:, 0], validated_coords, target_dims) + intermediate = DataConverter._convert_1d_data_to_dataarray( + data.iloc[:, 0], validated_coords, target_dims + ) else: - # Handle multi-column DataFrames - logger.warning(f'Converting multi-column DataFrame to xr.DataArray. We advise to do this manually.') - intermediate = DataConverter._convert_multid_array_to_dataarray(data.to_numpy(), validated_coords, target_dims) + # Handle multi-column DataFrames - this now allows partial matching + logger.warning('Converting multi-column DataFrame to xr.DataArray. We advise to do this manually.') + intermediate = DataConverter._convert_multid_array_to_dataarray( + data.to_numpy(), validated_coords, target_dims + ) elif isinstance(data, xr.DataArray): intermediate = data.copy() @@ -401,6 +407,7 @@ def to_dataarray( ) # Step 2: Broadcast to target dimensions if needed + # This now handles cases where intermediate has some but not all target dimensions return DataConverter._broadcast_to_target_dims(intermediate, validated_coords, target_dims) @staticmethod diff --git a/tests/test_dataconverter.py b/tests/test_dataconverter.py index 659528c4d..c174aebe8 100644 --- a/tests/test_dataconverter.py +++ b/tests/test_dataconverter.py @@ -25,8 +25,18 @@ def region_coords(): return pd.Index(['north', 'south', 'east'], name='region') -class TestBasicConversion: - """Test basic data type conversions with different coordinate configurations.""" +@pytest.fixture +def standard_coords(): + """Standard coordinates with unique lengths for easy testing.""" + return { + 'time': pd.date_range('2024-01-01', periods=5, freq='D', name='time'), # length 5 + 'scenario': pd.Index(['A', 'B', 'C'], name='scenario'), # length 3 + 'region': pd.Index(['north', 'south'], name='region') # length 2 + } + + +class TestScalarConversion: + """Test scalar data conversions with different coordinate configurations.""" def test_scalar_no_coords(self): """Scalar without coordinates should create 0D DataArray.""" @@ -56,9 +66,21 @@ def test_numpy_scalars(self, time_coords): assert result.shape == (5,) assert np.all(result.values == scalar.item()) + def test_scalar_many_dimensions(self, standard_coords): + """Scalar should broadcast to any number of dimensions.""" + coords = { + **standard_coords, + 'technology': pd.Index(['solar', 'wind'], name='technology') + } + + result = DataConverter.to_dataarray(42, coords=coords) + assert result.shape == (5, 3, 2, 2) + assert result.dims == ('time', 'scenario', 'region', 'technology') + assert np.all(result.values == 42) + -class TestArrayConversion: - """Test numpy array conversions.""" +class TestOneDimensionalArrayConversion: + """Test 1D numpy array and pandas Series conversions.""" def test_1d_array_no_coords(self): """1D array without coords should fail unless single element.""" @@ -119,11 +141,22 @@ def test_1d_array_ambiguous_length(self): with pytest.raises(ConversionError, match="matches multiple dimensions"): DataConverter.to_dataarray(arr, coords=coords_3x3) - def test_multidimensional_array_rejected(self, time_coords): - """Multidimensional arrays should be rejected.""" - arr_2d = np.array([[1, 2, 3], [4, 5, 6]]) - with pytest.raises(ConversionError, match="Only 1D arrays supported"): - DataConverter.to_dataarray(arr_2d, coords={'time': time_coords}) + def test_1d_array_broadcast_to_many_dimensions(self, standard_coords): + """1D array should broadcast to many dimensions.""" + # Array matching time dimension + time_arr = np.array([10, 20, 30, 40, 50]) + result = DataConverter.to_dataarray(time_arr, coords=standard_coords) + + assert result.shape == (5, 3, 2) + assert result.dims == ('time', 'scenario', 'region') + + # Check broadcasting - all scenarios and regions should have same time values + for scenario in standard_coords['scenario']: + for region in standard_coords['region']: + assert np.array_equal( + result.sel(scenario=scenario, region=region).values, + time_arr + ) class TestSeriesConversion: @@ -176,14 +209,6 @@ def test_series_broadcast_to_multiple_coords(self, time_coords, scenario_coords) for scenario in scenario_coords: assert np.array_equal(result.sel(scenario=scenario).values, time_series.values) - # Scenario series broadcast to time - scenario_series = pd.Series([100, 200, 300], index=scenario_coords) - result = DataConverter.to_dataarray(scenario_series, coords={'time': time_coords, 'scenario': scenario_coords}) - assert result.shape == (5, 3) - - for time in time_coords: - assert np.array_equal(result.sel(time=time).values, scenario_series.values) - def test_series_wrong_dimension(self, time_coords, region_coords): """Series indexed by dimension not in coords should fail.""" wrong_series = pd.Series([1, 2, 3], index=region_coords) @@ -191,6 +216,22 @@ def test_series_wrong_dimension(self, time_coords, region_coords): with pytest.raises(ConversionError): DataConverter.to_dataarray(wrong_series, coords={'time': time_coords}) + def test_series_broadcast_to_many_dimensions(self, standard_coords): + """Series should broadcast to many dimensions.""" + time_series = pd.Series([100, 200, 300, 400, 500], index=standard_coords['time']) + result = DataConverter.to_dataarray(time_series, coords=standard_coords) + + assert result.shape == (5, 3, 2) + assert result.dims == ('time', 'scenario', 'region') + + # Check that all non-time dimensions have the same time series values + for scenario in standard_coords['scenario']: + for region in standard_coords['region']: + assert np.array_equal( + result.sel(scenario=scenario, region=region).values, + time_series.values + ) + class TestDataFrameConversion: """Test pandas DataFrame conversions.""" @@ -204,21 +245,26 @@ def test_single_column_dataframe(self, time_coords): assert result.dims == ('time',) assert np.array_equal(result.values, df['value'].values) - def test_multi_column_dataframe_rejected(self, time_coords): - """Multi-column DataFrame should be rejected.""" + def test_multi_column_dataframe_accepted(self, time_coords, scenario_coords): + """Multi-column DataFrame should now be accepted and converted via numpy array path.""" df = pd.DataFrame({ 'value1': [10, 20, 30, 40, 50], - 'value2': [15, 25, 35, 45, 55] + 'value2': [15, 25, 35, 45, 55], + 'value3': [12, 22, 32, 42, 52] }, index=time_coords) - with pytest.raises(ConversionError, match="Array has 2 dimensions but 1 target "): - DataConverter.to_dataarray(df, coords={'time': time_coords}) + # Should work by converting to numpy array (5x3) and matching to time x scenario + result = DataConverter.to_dataarray(df, coords={'time': time_coords, 'scenario': scenario_coords}) + + assert result.shape == (5, 3) + assert result.dims == ('time', 'scenario') + assert np.array_equal(result.values, df.to_numpy()) def test_empty_dataframe_rejected(self, time_coords): """Empty DataFrame should be rejected.""" df = pd.DataFrame(index=time_coords) # No columns - with pytest.raises(ConversionError, match="DataFrame must have at least one"): + with pytest.raises(ConversionError, match="DataFrame must have at least one column"): DataConverter.to_dataarray(df, coords={'time': time_coords}) def test_dataframe_broadcast(self, time_coords, scenario_coords): @@ -231,6 +277,171 @@ def test_dataframe_broadcast(self, time_coords, scenario_coords): assert np.array_equal(result.sel(scenario=scenario).values, df['power'].values) +class TestMultiDimensionalArrayConversion: + """Test multi-dimensional numpy array conversions.""" + + def test_2d_array_unique_dimensions(self, standard_coords): + """2D array with unique dimension lengths should work.""" + # 5x3 array should map to time x scenario + data_2d = np.random.rand(5, 3) + result = DataConverter.to_dataarray(data_2d, coords={ + 'time': standard_coords['time'], + 'scenario': standard_coords['scenario'] + }) + + assert result.shape == (5, 3) + assert result.dims == ('time', 'scenario') + assert np.array_equal(result.values, data_2d) + + # 3x5 array should map to scenario x time + data_2d_flipped = np.random.rand(3, 5) + result_flipped = DataConverter.to_dataarray(data_2d_flipped, coords={ + 'time': standard_coords['time'], + 'scenario': standard_coords['scenario'] + }) + + assert result_flipped.shape == (5, 3) + assert result_flipped.dims == ('time', 'scenario') + assert np.array_equal(result_flipped.values.transpose(), data_2d_flipped) + + def test_2d_array_broadcast_to_3d(self, standard_coords): + """2D array should broadcast to additional dimensions when using partial matching.""" + # With improved integration, 2D array (5x3) should match time×scenario and broadcast to region + data_2d = np.random.rand(5, 3) + result = DataConverter.to_dataarray(data_2d, coords=standard_coords) + + assert result.shape == (5, 3, 2) + assert result.dims == ('time', 'scenario', 'region') + + # Check that all regions have the same time x scenario data + for region in standard_coords['region']: + assert np.array_equal(result.sel(region=region).values, data_2d) + + def test_3d_array_unique_dimensions(self, standard_coords): + """3D array with unique dimension lengths should work.""" + # 5x3x2 array should map to time x scenario x region + data_3d = np.random.rand(5, 3, 2) + result = DataConverter.to_dataarray(data_3d, coords=standard_coords) + + assert result.shape == (5, 3, 2) + assert result.dims == ('time', 'scenario', 'region') + assert np.array_equal(result.values, data_3d) + + def test_3d_array_different_permutation(self, standard_coords): + """3D array with different dimension order should work.""" + # 2x5x3 array should map to region x time x scenario + data_3d = np.random.rand(2, 5, 3) + result = DataConverter.to_dataarray(data_3d, coords=standard_coords) + + assert result.shape == (5, 3, 2) + assert result.dims == ('time', 'scenario', 'region') + assert np.array_equal(result.transpose('region', 'time', 'scenario').values, data_3d) + + def test_4d_array_unique_dimensions(self): + """4D array with unique dimension lengths should work.""" + coords = { + 'time': pd.date_range('2024-01-01', periods=2, freq='D', name='time'), # length 2 + 'scenario': pd.Index(['A', 'B', 'C'], name='scenario'), # length 3 + 'region': pd.Index(['north', 'south', 'east', 'west'], name='region'), # length 4 + 'technology': pd.Index(['solar', 'wind', 'gas', 'coal', 'hydro'], name='technology') # length 5 + } + + # 3x5x2x4 array should map to scenario x technology x time x region + data_4d = np.random.rand(3, 5, 2, 4) + result = DataConverter.to_dataarray(data_4d, coords=coords) + + assert result.shape == (2,3,4,5) + assert result.dims == ('time', 'scenario', 'region', 'technology') + assert np.array_equal(result.transpose('scenario', 'technology', 'time', 'region').values, data_4d) + + def test_2d_array_ambiguous_dimensions_error(self): + """2D array with ambiguous dimension lengths should fail.""" + # Both dimensions have length 3 + coords_ambiguous = { + 'scenario': pd.Index(['A', 'B', 'C'], name='scenario'), # length 3 + 'region': pd.Index(['north', 'south', 'east'], name='region') # length 3 + } + + data_2d = np.random.rand(3, 3) + with pytest.raises(ConversionError, match="matches multiple dimension orders"): + DataConverter.to_dataarray(data_2d, coords=coords_ambiguous) + + def test_multid_array_no_coords(self): + """Multi-D arrays without coords should fail unless scalar.""" + # Multi-element fails + data_2d = np.random.rand(2, 3) + with pytest.raises(ConversionError, match="Cannot convert multi-element array without target dimensions"): + DataConverter.to_dataarray(data_2d) + + # Single element succeeds + single_element = np.array([[42]]) + result = DataConverter.to_dataarray(single_element) + assert result.shape == () + assert result.item() == 42 + + def test_array_no_matching_dimensions_error(self, standard_coords): + """Array with no matching dimension lengths should fail.""" + # 7x8 array - no dimension has length 7 or 8 + data_2d = np.random.rand(7, 8) + coords_2d = { + 'time': standard_coords['time'], # length 5 + 'scenario': standard_coords['scenario'] # length 3 + } + + with pytest.raises(ConversionError, match="Array dimensions do not match any coordinate lengths"): + DataConverter.to_dataarray(data_2d, coords=coords_2d) + + def test_multid_array_special_values(self, standard_coords): + """Multi-D arrays should preserve special values.""" + # Create 2D array with special values + data_2d = np.array([[1.0, np.nan, 3.0], + [np.inf, 5.0, -np.inf], + [7.0, 8.0, 9.0], + [10.0, np.nan, 12.0], + [13.0, 14.0, np.inf]]) + + result = DataConverter.to_dataarray(data_2d, coords={ + 'time': standard_coords['time'], + 'scenario': standard_coords['scenario'] + }) + + assert result.shape == (5, 3) + assert np.array_equal(np.isnan(result.values), np.isnan(data_2d)) + assert np.array_equal(np.isinf(result.values), np.isinf(data_2d)) + + def test_multid_array_dtype_preservation(self, standard_coords): + """Multi-D arrays should preserve data types.""" + # Integer array + int_data = np.array([[1, 2, 3], + [4, 5, 6], + [7, 8, 9], + [10, 11, 12], + [13, 14, 15]], dtype=np.int32) + + result_int = DataConverter.to_dataarray(int_data, coords={ + 'time': standard_coords['time'], + 'scenario': standard_coords['scenario'] + }) + + assert result_int.dtype == np.int32 + assert np.array_equal(result_int.values, int_data) + + # Boolean array + bool_data = np.array([[True, False, True], + [False, True, False], + [True, True, False], + [False, False, True], + [True, False, True]]) + + result_bool = DataConverter.to_dataarray(bool_data, coords={ + 'time': standard_coords['time'], + 'scenario': standard_coords['scenario'] + }) + + assert result_bool.dtype == bool + assert np.array_equal(result_bool.values, bool_data) + + class TestDataArrayConversion: """Test xarray DataArray conversions.""" @@ -282,6 +493,28 @@ def test_scalar_dataarray_broadcast(self, time_coords, scenario_coords): assert result.shape == (5, 3) assert np.all(result.values == 42) + def test_2d_dataarray_broadcast_to_more_dimensions(self, standard_coords): + """DataArray should broadcast to additional dimensions.""" + # Start with 2D DataArray + original = xr.DataArray( + [[10, 20, 30], [40, 50, 60], [70, 80, 90], [100, 110, 120], [130, 140, 150]], + coords={'time': standard_coords['time'], 'scenario': standard_coords['scenario']}, + dims=['time', 'scenario'] + ) + + # Broadcast to 3D + result = DataConverter.to_dataarray(original, coords=standard_coords) + + assert result.shape == (5, 3, 2) + assert result.dims == ('time', 'scenario', 'region') + + # Check that all regions have the same time+scenario values + for region in standard_coords['region']: + assert np.array_equal( + result.sel(region=region).values, + original.values + ) + class TestTimeSeriesDataConversion: """Test TimeSeriesData conversions.""" @@ -309,275 +542,58 @@ def test_timeseries_data_broadcast(self, time_coords, scenario_coords): assert np.array_equal(result.sel(scenario=scenario).values, [10, 20, 30, 40, 50]) -class TestThreeDimensionConversion: - """Test conversions with exactly 3 dimensions for all data types.""" - - @pytest.fixture - def three_d_coords(self, time_coords, scenario_coords): - """Standard 3D coordinate system with unique lengths.""" - return { - 'time': time_coords, # length 5 - 'scenario': scenario_coords, # length 3 - 'region': pd.Index(['north', 'south'], name='region') # length 2 - unique! - } - - def test_scalar_three_dimensions(self, three_d_coords): - """Scalar should broadcast to 3 dimensions.""" - result = DataConverter.to_dataarray(42, coords=three_d_coords) +class TestCustomDimensions: + """Test with custom dimension names beyond time/scenario.""" - assert result.shape == (5, 3, 2) # time=5, scenario=3, region=2 - assert result.dims == ('time', 'scenario', 'region') + def test_custom_single_dimension(self, region_coords): + """Test with custom dimension name.""" + result = DataConverter.to_dataarray(42, coords={'region': region_coords}) + assert result.shape == (3,) + assert result.dims == ('region',) assert np.all(result.values == 42) - # Verify all coordinates are correct - assert result.indexes['time'].equals(three_d_coords['time']) - assert result.indexes['scenario'].equals(three_d_coords['scenario']) - assert result.indexes['region'].equals(three_d_coords['region']) - - def test_numpy_scalar_three_dimensions(self, three_d_coords): - """Numpy scalars should broadcast to 3 dimensions.""" - for scalar in [np.int32(100), np.float64(3.14)]: - result = DataConverter.to_dataarray(scalar, coords=three_d_coords) - - assert result.shape == (5, 3, 2) - assert result.dims == ('time', 'scenario', 'region') - assert np.all(result.values == scalar.item()) + def test_custom_multiple_dimensions(self): + """Test with multiple custom dimensions.""" + products = pd.Index(['A', 'B'], name='product') + technologies = pd.Index(['solar', 'wind', 'gas'], name='technology') - def test_1d_array_time_to_three_dimensions(self, three_d_coords): - """1D array matching time should broadcast to 3D.""" - time_arr = np.array([10, 20, 30, 40, 50]) - result = DataConverter.to_dataarray(time_arr, coords=three_d_coords) + # Array matching technology dimension + arr = np.array([100, 150, 80]) + result = DataConverter.to_dataarray(arr, coords={'product': products, 'technology': technologies}) - assert result.shape == (5, 3, 2) - assert result.dims == ('time', 'scenario', 'region') + assert result.shape == (2, 3) + assert result.dims == ('product', 'technology') - # Check broadcasting across scenarios and regions - for scenario in three_d_coords['scenario']: - for region in three_d_coords['region']: - slice_data = result.sel(scenario=scenario, region=region) - assert np.array_equal(slice_data.values, time_arr) + # Should broadcast across products + for product in products: + assert np.array_equal(result.sel(product=product).values, arr) - def test_1d_array_scenario_to_three_dimensions(self, three_d_coords): - """1D array matching scenario should broadcast to 3D.""" - scenario_arr = np.array([100, 200, 300]) - result = DataConverter.to_dataarray(scenario_arr, coords=three_d_coords) + def test_mixed_dimension_types(self): + """Test mixing time dimension with custom dimensions.""" + time_coords = pd.date_range('2024-01-01', periods=3, freq='D', name='time') + regions = pd.Index(['north', 'south'], name='region') - assert result.shape == (5, 3, 2) - assert result.dims == ('time', 'scenario', 'region') + # Time series should broadcast to regions + time_series = pd.Series([10, 20, 30], index=time_coords) + result = DataConverter.to_dataarray(time_series, coords={'time': time_coords, 'region': regions}) - # Check broadcasting across time and regions - for time in three_d_coords['time']: - for region in three_d_coords['region']: - slice_data = result.sel(time=time, region=region) - assert np.array_equal(slice_data.values, scenario_arr) + assert result.shape == (3, 2) + assert result.dims == ('time', 'region') - def test_1d_array_region_to_three_dimensions(self, three_d_coords): - """1D array matching region should broadcast to 3D.""" - region_arr = np.array([1000, 2000]) # Length 2 to match region - result = DataConverter.to_dataarray(region_arr, coords=three_d_coords) + def test_custom_dimensions_complex(self): + """Test complex scenario with custom dimensions.""" + coords = { + 'product': pd.Index(['A', 'B'], name='product'), + 'factory': pd.Index(['F1', 'F2', 'F3'], name='factory'), + 'quarter': pd.Index(['Q1', 'Q2', 'Q3', 'Q4'], name='quarter') + } - assert result.shape == (5, 3, 2) - assert result.dims == ('time', 'scenario', 'region') + # Array matching factory dimension + factory_arr = np.array([100, 200, 300]) + result = DataConverter.to_dataarray(factory_arr, coords=coords) - # Check broadcasting across time and scenarios - for time in three_d_coords['time']: - for scenario in three_d_coords['scenario']: - slice_data = result.sel(time=time, scenario=scenario) - assert np.array_equal(slice_data.values, region_arr) - - def test_series_time_to_three_dimensions(self, three_d_coords): - """Time-indexed Series should broadcast to 3D.""" - time_series = pd.Series([15, 25, 35, 45, 55], index=three_d_coords['time']) - result = DataConverter.to_dataarray(time_series, coords=three_d_coords) - - assert result.shape == (5, 3, 2) - assert result.dims == ('time', 'scenario', 'region') - - # Check broadcasting - for scenario in three_d_coords['scenario']: - for region in three_d_coords['region']: - slice_data = result.sel(scenario=scenario, region=region) - assert np.array_equal(slice_data.values, time_series.values) - - def test_series_scenario_to_three_dimensions(self, three_d_coords): - """Scenario-indexed Series should broadcast to 3D.""" - scenario_series = pd.Series([500, 600, 700], index=three_d_coords['scenario']) - result = DataConverter.to_dataarray(scenario_series, coords=three_d_coords) - - assert result.shape == (5, 3, 2) - assert result.dims == ('time', 'scenario', 'region') - - # Check broadcasting - for time in three_d_coords['time']: - for region in three_d_coords['region']: - slice_data = result.sel(time=time, region=region) - assert np.array_equal(slice_data.values, scenario_series.values) - - def test_series_region_to_three_dimensions(self, three_d_coords): - """Region-indexed Series should broadcast to 3D.""" - region_series = pd.Series([5000, 6000], index=three_d_coords['region']) # Length 2 - result = DataConverter.to_dataarray(region_series, coords=three_d_coords) - - assert result.shape == (5, 3, 2) - assert result.dims == ('time', 'scenario', 'region') - - # Check broadcasting - for time in three_d_coords['time']: - for scenario in three_d_coords['scenario']: - slice_data = result.sel(time=time, scenario=scenario) - assert np.array_equal(slice_data.values, region_series.values) - - def test_dataframe_time_to_three_dimensions(self, three_d_coords): - """Time-indexed DataFrame should broadcast to 3D.""" - df = pd.DataFrame({'power': [11, 22, 33, 44, 55]}, index=three_d_coords['time']) - result = DataConverter.to_dataarray(df, coords=three_d_coords) - - assert result.shape == (5, 3, 2) - assert result.dims == ('time', 'scenario', 'region') - - # Check broadcasting - for scenario in three_d_coords['scenario']: - for region in three_d_coords['region']: - slice_data = result.sel(scenario=scenario, region=region) - assert np.array_equal(slice_data.values, df['power'].values) - - def test_dataframe_scenario_to_three_dimensions(self, three_d_coords): - """Scenario-indexed DataFrame should broadcast to 3D.""" - df = pd.DataFrame({'cost': [1100, 1200, 1300]}, index=three_d_coords['scenario']) - result = DataConverter.to_dataarray(df, coords=three_d_coords) - - assert result.shape == (5, 3, 2) - assert result.dims == ('time', 'scenario', 'region') - - # Check broadcasting - for time in three_d_coords['time']: - for region in three_d_coords['region']: - slice_data = result.sel(time=time, region=region) - assert np.array_equal(slice_data.values, df['cost'].values) - - def test_1d_dataarray_time_to_three_dimensions(self, three_d_coords): - """1D time DataArray should broadcast to 3D.""" - original = xr.DataArray([101, 102, 103, 104, 105], - coords={'time': three_d_coords['time']}, - dims=['time']) - result = DataConverter.to_dataarray(original, coords=three_d_coords) - - assert result.shape == (5, 3, 2) - assert result.dims == ('time', 'scenario', 'region') - - # Check broadcasting - for scenario in three_d_coords['scenario']: - for region in three_d_coords['region']: - slice_data = result.sel(scenario=scenario, region=region) - assert np.array_equal(slice_data.values, original.values) - - def test_1d_dataarray_scenario_to_three_dimensions(self, three_d_coords): - """1D scenario DataArray should broadcast to 3D.""" - original = xr.DataArray([2001, 2002, 2003], - coords={'scenario': three_d_coords['scenario']}, - dims=['scenario']) - result = DataConverter.to_dataarray(original, coords=three_d_coords) - - assert result.shape == (5, 3, 2) - assert result.dims == ('time', 'scenario', 'region') - - # Check broadcasting - for time in three_d_coords['time']: - for region in three_d_coords['region']: - slice_data = result.sel(time=time, region=region) - assert np.array_equal(slice_data.values, original.values) - - def test_2d_dataarray_to_three_dimensions(self, three_d_coords): - """2D DataArray should broadcast to 3D.""" - # Create 2D time x scenario DataArray - data_2d = np.random.rand(5, 3) - original = xr.DataArray(data_2d, - coords={'time': three_d_coords['time'], - 'scenario': three_d_coords['scenario']}, - dims=['time', 'scenario']) - - result = DataConverter.to_dataarray(original, coords=three_d_coords) - - assert result.shape == (5, 3, 2) - assert result.dims == ('time', 'scenario', 'region') - - # Check that all regions have the same time x scenario data - for region in three_d_coords['region']: - slice_data = result.sel(region=region) - assert np.array_equal(slice_data.values, original.values) - - def test_timeseries_data_to_three_dimensions(self, three_d_coords): - """TimeSeriesData should broadcast to 3D.""" - data_array = xr.DataArray([99, 88, 77, 66, 55], - coords={'time': three_d_coords['time']}, - dims=['time']) - ts_data = TimeSeriesData(data_array, aggregation_group='test_3d') - - result = DataConverter.to_dataarray(ts_data, coords=three_d_coords) - - assert result.shape == (5, 3, 2) - assert result.dims == ('time', 'scenario', 'region') - - # Check broadcasting - for scenario in three_d_coords['scenario']: - for region in three_d_coords['region']: - slice_data = result.sel(scenario=scenario, region=region) - assert np.array_equal(slice_data.values, [99, 88, 77, 66, 55]) - - def test_three_d_copy_independence(self, three_d_coords): - """3D results should be independent copies.""" - original_arr = np.array([10, 20, 30, 40, 50]) - result = DataConverter.to_dataarray(original_arr, coords=three_d_coords) - - # Modify result - result[0, 0, 0] = 999 - - # Original should be unchanged - assert original_arr[0] == 10 - - def test_three_d_special_values(self, three_d_coords): - """3D conversion should preserve special values.""" - # Array with NaN and inf - special_arr = np.array([1, np.nan, np.inf, -np.inf, 5]) - result = DataConverter.to_dataarray(special_arr, coords=three_d_coords) - - assert result.shape == (5, 3, 2) - - # Check that special values are preserved in all broadcasts - for scenario in three_d_coords['scenario']: - for region in three_d_coords['region']: - slice_data = result.sel(scenario=scenario, region=region) - assert np.array_equal(np.isnan(slice_data.values), np.isnan(special_arr)) - assert np.array_equal(np.isinf(slice_data.values), np.isinf(special_arr)) - - def test_three_d_ambiguous_length_error(self): - """Should fail when array length matches multiple dimensions in 3D.""" - # All dimensions have length 3 - coords_3x3x3 = { - 'time': pd.date_range('2024-01-01', periods=3, freq='D', name='time'), - 'scenario': pd.Index(['A', 'B', 'C'], name='scenario'), - 'region': pd.Index(['X', 'Y', 'Z'], name='region') - } - - arr = np.array([1, 2, 3]) # Length 3 - matches all dimensions - - with pytest.raises(ConversionError, match="matches multiple dimensions"): - DataConverter.to_dataarray(arr, coords=coords_3x3x3) - - def test_three_d_custom_dimensions(self): - """3D conversion with custom dimension names.""" - coords = { - 'product': pd.Index(['A', 'B'], name='product'), - 'factory': pd.Index(['F1', 'F2', 'F3'], name='factory'), - 'quarter': pd.Index(['Q1', 'Q2', 'Q3', 'Q4'], name='quarter') - } - - # Array matching factory dimension - factory_arr = np.array([100, 200, 300]) - result = DataConverter.to_dataarray(factory_arr, coords=coords) - - assert result.shape == (2, 3, 4) - assert result.dims == ('product', 'factory', 'quarter') + assert result.shape == (2, 3, 4) + assert result.dims == ('product', 'factory', 'quarter') # Check broadcasting for product in coords['product']: @@ -586,143 +602,6 @@ def test_three_d_custom_dimensions(self): assert np.array_equal(slice_data.values, factory_arr) -class TestMultipleDimensions: - """Test support for more than 2 dimensions.""" - - def test_scalar_many_dimensions(self): - """Scalar should broadcast to any number of dimensions.""" - coords = { - 'time': pd.date_range('2024-01-01', periods=2, freq='D', name='time'), - 'scenario': pd.Index(['A', 'B'], name='scenario'), - 'region': pd.Index(['north', 'south'], name='region'), - 'technology': pd.Index(['solar', 'wind'], name='technology') - } - - result = DataConverter.to_dataarray(42, coords=coords) - assert result.shape == (2, 2, 2, 2) - assert result.dims == ('time', 'scenario', 'region', 'technology') - assert np.all(result.values == 42) - - def test_1d_array_broadcast_to_many_dimensions(self): - """1D array should broadcast to many dimensions.""" - coords = { - 'time': pd.date_range('2024-01-01', periods=3, freq='D', name='time'), - 'scenario': pd.Index(['A', 'B'], name='scenario'), - 'region': pd.Index(['north', 'south'], name='region') - } - - # Array matching time dimension - time_arr = np.array([10, 20, 30]) - result = DataConverter.to_dataarray(time_arr, coords=coords) - - assert result.shape == (3, 2, 2) - assert result.dims == ('time', 'scenario', 'region') - - # Check broadcasting - all scenarios and regions should have same time values - for scenario in coords['scenario']: - for region in coords['region']: - assert np.array_equal( - result.sel(scenario=scenario, region=region).values, - time_arr - ) - - def test_series_broadcast_to_many_dimensions(self): - """Series should broadcast to many dimensions.""" - time_coords = pd.date_range('2024-01-01', periods=3, freq='D', name='time') - coords = { - 'time': time_coords, - 'scenario': pd.Index(['A', 'B'], name='scenario'), - 'region': pd.Index(['north', 'south'], name='region'), - 'product': pd.Index(['X', 'Y', 'Z'], name='product') - } - - # Time-indexed series - time_series = pd.Series([100, 200, 300], index=time_coords) - result = DataConverter.to_dataarray(time_series, coords=coords) - - assert result.shape == (3, 2, 2, 3) - assert result.dims == ('time', 'scenario', 'region', 'product') - - # Check that all non-time dimensions have the same time series values - for scenario in coords['scenario']: - for region in coords['region']: - for product in coords['product']: - assert np.array_equal( - result.sel(scenario=scenario, region=region, product=product).values, - time_series.values - ) - - def test_dataarray_broadcast_to_more_dimensions(self): - """DataArray should broadcast to additional dimensions.""" - time_coords = pd.date_range('2024-01-01', periods=2, freq='D', name='time') - scenario_coords = pd.Index(['A', 'B'], name='scenario') - - # Start with 2D DataArray - original = xr.DataArray( - [[10, 20], [30, 40]], - coords={'time': time_coords, 'scenario': scenario_coords}, - dims=['time', 'scenario'] - ) - - # Broadcast to 3D - coords = { - 'time': time_coords, - 'scenario': scenario_coords, - 'region': pd.Index(['north', 'south'], name='region') - } - - result = DataConverter.to_dataarray(original, coords=coords) - - assert result.shape == (2, 2, 2) - assert result.dims == ('time', 'scenario', 'region') - - # Check that all regions have the same time+scenario values - for region in coords['region']: - assert np.array_equal( - result.sel(region=region).values, - original.values - ) - - -class TestCustomDimensions: - """Test with custom dimension names beyond time/scenario.""" - - def test_custom_single_dimension(self, region_coords): - """Test with custom dimension name.""" - result = DataConverter.to_dataarray(42, coords={'region': region_coords}) - assert result.shape == (3,) - assert result.dims == ('region',) - assert np.all(result.values == 42) - - def test_custom_multiple_dimensions(self): - """Test with multiple custom dimensions.""" - products = pd.Index(['A', 'B'], name='product') - technologies = pd.Index(['solar', 'wind', 'gas'], name='technology') - - # Array matching technology dimension - arr = np.array([100, 150, 80]) - result = DataConverter.to_dataarray(arr, coords={'product': products, 'technology': technologies}) - - assert result.shape == (2, 3) - assert result.dims == ('product', 'technology') - - # Should broadcast across products - for product in products: - assert np.array_equal(result.sel(product=product).values, arr) - - def test_mixed_dimension_types(self): - """Test mixing time dimension with custom dimensions.""" - time_coords = pd.date_range('2024-01-01', periods=3, freq='D', name='time') - regions = pd.Index(['north', 'south'], name='region') - - # Time series should broadcast to regions - time_series = pd.Series([10, 20, 30], index=time_coords) - result = DataConverter.to_dataarray(time_series, coords={'time': time_coords, 'region': regions}) - - assert result.shape == (3, 2) - assert result.dims == ('time', 'region') - - class TestValidation: """Test coordinate validation.""" @@ -782,6 +661,30 @@ def test_dimension_mismatch_messages(self, time_coords, scenario_coords): with pytest.raises(ConversionError, match="matches none of the target dimensions"): DataConverter.to_dataarray(wrong_arr, coords={'time': time_coords, 'scenario': scenario_coords}) + def test_multidimensional_array_dimension_count_mismatch(self, standard_coords): + """Array with wrong number of dimensions should fail with clear error.""" + # 4D array with 3D coordinates + data_4d = np.random.rand(5, 3, 2, 4) + with pytest.raises(ConversionError, match="matches multiple dimension orders|Array dimensions do not match"): + DataConverter.to_dataarray(data_4d, coords=standard_coords) + + def test_error_message_quality(self, standard_coords): + """Error messages should include helpful information.""" + # Wrong shape array + data_2d = np.random.rand(7, 8) + coords_2d = { + 'time': standard_coords['time'], # length 5 + 'scenario': standard_coords['scenario'] # length 3 + } + + try: + DataConverter.to_dataarray(data_2d, coords=coords_2d) + assert False, "Should have raised ConversionError" + except ConversionError as e: + error_msg = str(e) + assert "Array shape: (7, 8)" in error_msg + assert "Coordinate lengths:" in error_msg + class TestDataIntegrity: """Test data copying and integrity.""" @@ -819,6 +722,20 @@ def test_dataframe_copy_independence(self, time_coords): # Original should be unchanged assert original_df.loc[time_coords[0], 'value'] == 10 + def test_multid_array_copy_independence(self, standard_coords): + """Multi-D arrays should be independent copies.""" + original_data = np.random.rand(5, 3) + result = DataConverter.to_dataarray(original_data, coords={ + 'time': standard_coords['time'], + 'scenario': standard_coords['scenario'] + }) + + # Modify result + result[0, 0] = 999 + + # Original should be unchanged + assert original_data[0, 0] != 999 + class TestSpecialValues: """Test handling of special numeric values.""" @@ -854,331 +771,123 @@ def test_mixed_numeric_types(self, time_coords): assert np.issubdtype(result.dtype, np.floating) assert np.array_equal(result.values, mixed_arr) + def test_special_values_in_multid_arrays(self, standard_coords): + """Special values should be preserved in multi-D arrays and broadcasting.""" + # Array with NaN and inf + special_arr = np.array([1, np.nan, np.inf, -np.inf, 5]) + result = DataConverter.to_dataarray(special_arr, coords=standard_coords) -class TestMultiDimensionalArrayConversion: - """Test multi-dimensional numpy array conversions.""" - - @pytest.fixture - def standard_coords(self): - """Standard coordinates with unique lengths for easy testing.""" - return { - 'time': pd.date_range('2024-01-01', periods=5, freq='D', name='time'), # length 5 - 'scenario': pd.Index(['A', 'B', 'C'], name='scenario'), # length 3 - 'region': pd.Index(['north', 'south'], name='region') # length 2 - } - - def test_2d_array_unique_dimensions(self, standard_coords): - """2D array with unique dimension lengths should work.""" - # 5x3 array should map to time x scenario - data_2d = np.random.rand(5, 3) - result = DataConverter.to_dataarray(data_2d, coords={ - 'time': standard_coords['time'], - 'scenario': standard_coords['scenario'] - }) + assert result.shape == (5, 3, 2) - assert result.shape == (5, 3) - assert result.dims == ('time', 'scenario') - assert np.array_equal(result.values, data_2d) + # Check that special values are preserved in all broadcasts + for scenario in standard_coords['scenario']: + for region in standard_coords['region']: + slice_data = result.sel(scenario=scenario, region=region) + assert np.array_equal(np.isnan(slice_data.values), np.isnan(special_arr)) + assert np.array_equal(np.isinf(slice_data.values), np.isinf(special_arr)) - # 3x5 array should map to scenario x time - data_2d_flipped = np.random.rand(3, 5) - result_flipped = DataConverter.to_dataarray(data_2d_flipped, coords={ - 'time': standard_coords['time'], - 'scenario': standard_coords['scenario'] - }) - assert result_flipped.shape == (3, 5) - assert result_flipped.dims == ('scenario', 'time') - assert np.array_equal(result_flipped.values, data_2d_flipped) +class TestAdvancedBroadcasting: + """Test advanced broadcasting scenarios and edge cases.""" - def test_3d_array_unique_dimensions(self, standard_coords): - """3D array with unique dimension lengths should work.""" - # 5x3x2 array should map to time x scenario x region - data_3d = np.random.rand(5, 3, 2) - result = DataConverter.to_dataarray(data_3d, coords=standard_coords) + def test_partial_dimension_matching_with_broadcasting(self, standard_coords): + """Test that partial dimension matching works with the improved integration.""" + # 1D array matching one dimension should broadcast to all target dimensions + time_arr = np.array([10, 20, 30, 40, 50]) # matches time (length 5) + result = DataConverter.to_dataarray(time_arr, coords=standard_coords) assert result.shape == (5, 3, 2) assert result.dims == ('time', 'scenario', 'region') - assert np.array_equal(result.values, data_3d) - - def test_3d_array_different_permutation(self, standard_coords): - """3D array with different dimension order should work.""" - # 2x5x3 array should map to region x time x scenario - data_3d = np.random.rand(2, 5, 3) - result = DataConverter.to_dataarray(data_3d, coords=standard_coords) - - assert result.shape == (2, 5, 3) - assert result.dims == ('region', 'time', 'scenario') - assert np.array_equal(result.values, data_3d) - - def test_4d_array_unique_dimensions(self): - """4D array with unique dimension lengths should work.""" - coords = { - 'time': pd.date_range('2024-01-01', periods=2, freq='D', name='time'), # length 2 - 'scenario': pd.Index(['A', 'B', 'C'], name='scenario'), # length 3 - 'region': pd.Index(['north', 'south', 'east', 'west'], name='region'), # length 4 - 'technology': pd.Index(['solar', 'wind', 'gas', 'coal', 'hydro'], name='technology') # length 5 - } - - # 3x5x2x4 array should map to scenario x technology x time x region - data_4d = np.random.rand(3, 5, 2, 4) - result = DataConverter.to_dataarray(data_4d, coords=coords) - - assert result.shape == (3, 5, 2, 4) - assert result.dims == ('scenario', 'technology', 'time', 'region') - assert np.array_equal(result.values, data_4d) - - def test_2d_array_ambiguous_dimensions_error(self): - """2D array with ambiguous dimension lengths should fail.""" - # Both dimensions have length 3 - coords_ambiguous = { - 'scenario': pd.Index(['A', 'B', 'C'], name='scenario'), # length 3 - 'region': pd.Index(['north', 'south', 'east'], name='region') # length 3 - } - - data_2d = np.random.rand(3, 3) - with pytest.raises(ConversionError, match="matches multiple dimension orders"): - DataConverter.to_dataarray(data_2d, coords=coords_ambiguous) - - def test_3d_array_ambiguous_dimensions_error(self): - """3D array with ambiguous dimension lengths should fail.""" - # All dimensions have length 2 - coords_ambiguous = { - 'scenario': pd.Index(['A', 'B'], name='scenario'), # length 2 - 'region': pd.Index(['north', 'south'], name='region'), # length 2 - 'technology': pd.Index(['solar', 'wind'], name='technology') # length 2 - } - - data_3d = np.random.rand(2, 2, 2) - with pytest.raises(ConversionError, match="matches multiple dimension orders"): - DataConverter.to_dataarray(data_3d, coords=coords_ambiguous) - - def test_array_dimension_count_mismatch_error(self, standard_coords): - """Array with wrong number of dimensions should fail.""" - # 2D array with 3D coordinates - data_2d = np.random.rand(5, 3) - with pytest.raises(ConversionError, match="Array has 2 dimensions but 3 target dimensions provided"): - DataConverter.to_dataarray(data_2d, coords=standard_coords) - - # 4D array with 3D coordinates - data_4d = np.random.rand(5, 3, 2, 4) - with pytest.raises(ConversionError, match="Array has 4 dimensions but 3 target dimensions provided"): - DataConverter.to_dataarray(data_4d, coords=standard_coords) - - def test_array_no_matching_dimensions_error(self, standard_coords): - """Array with no matching dimension lengths should fail.""" - # 7x8 array - no dimension has length 7 or 8 - data_2d = np.random.rand(7, 8) - coords_2d = { - 'time': standard_coords['time'], # length 5 - 'scenario': standard_coords['scenario'] # length 3 - } - - with pytest.raises(ConversionError, match="Array dimensions do not match any coordinate lengths"): - DataConverter.to_dataarray(data_2d, coords=coords_2d) - - def test_2d_array_custom_dimensions(self): - """2D array with custom dimension names should work.""" - coords = { - 'product': pd.Index(['A', 'B', 'C', 'D'], name='product'), # length 4 - 'factory': pd.Index(['F1', 'F2', 'F3'], name='factory') # length 3 - } - - # 4x3 array should map to product x factory - data_2d = np.array([[10, 11, 12], - [20, 21, 22], - [30, 31, 32], - [40, 41, 42]]) - - result = DataConverter.to_dataarray(data_2d, coords=coords) - - assert result.shape == (4, 3) - assert result.dims == ('product', 'factory') - assert np.array_equal(result.values, data_2d) - - # Verify coordinates are correct - assert result.indexes['product'].equals(coords['product']) - assert result.indexes['factory'].equals(coords['factory']) - - def test_multid_array_copy_independence(self, standard_coords): - """Multi-D arrays should be independent copies.""" - original_data = np.random.rand(5, 3) - result = DataConverter.to_dataarray(original_data, coords={ - 'time': standard_coords['time'], - 'scenario': standard_coords['scenario'] - }) - - # Modify result - result[0, 0] = 999 - - # Original should be unchanged - assert original_data[0, 0] != 999 - - def test_multid_array_special_values(self, standard_coords): - """Multi-D arrays should preserve special values.""" - # Create 2D array with special values - data_2d = np.array([[1.0, np.nan, 3.0], - [np.inf, 5.0, -np.inf], - [7.0, 8.0, 9.0], - [10.0, np.nan, 12.0], - [13.0, 14.0, np.inf]]) - - result = DataConverter.to_dataarray(data_2d, coords={ - 'time': standard_coords['time'], - 'scenario': standard_coords['scenario'] - }) - - assert result.shape == (5, 3) - assert np.array_equal(np.isnan(result.values), np.isnan(data_2d)) - assert np.array_equal(np.isinf(result.values), np.isinf(data_2d)) - - def test_multid_array_with_time_dimension(self): - """Multi-D arrays should work with time dimension.""" - time_coords = pd.date_range('2024-01-01', periods=4, freq='H', name='time') - scenario_coords = pd.Index(['base', 'high', 'low'], name='scenario') - - # 4x3 time series data - data_2d = np.array([[100, 110, 120], - [200, 210, 220], - [300, 310, 320], - [400, 410, 420]]) - - result = DataConverter.to_dataarray(data_2d, coords={ - 'time': time_coords, - 'scenario': scenario_coords - }) - - assert result.shape == (4, 3) - assert result.dims == ('time', 'scenario') - assert isinstance(result.indexes['time'], pd.DatetimeIndex) - assert np.array_equal(result.values, data_2d) - - def test_multid_array_dtype_preservation(self, standard_coords): - """Multi-D arrays should preserve data types.""" - # Integer array - int_data = np.array([[1, 2, 3], - [4, 5, 6], - [7, 8, 9], - [10, 11, 12], - [13, 14, 15]], dtype=np.int32) - - result_int = DataConverter.to_dataarray(int_data, coords={ - 'time': standard_coords['time'], - 'scenario': standard_coords['scenario'] - }) - - assert result_int.dtype == np.int32 - assert np.array_equal(result_int.values, int_data) - - # Float array - float_data = np.array([[1.1, 2.2, 3.3], - [4.4, 5.5, 6.6], - [7.7, 8.8, 9.9], - [10.1, 11.1, 12.1], - [13.1, 14.1, 15.1]], dtype=np.float64) - - result_float = DataConverter.to_dataarray(float_data, coords={ - 'time': standard_coords['time'], - 'scenario': standard_coords['scenario'] - }) - - assert result_float.dtype == np.float64 - assert np.array_equal(result_float.values, float_data) - # Boolean array - bool_data = np.array([[True, False, True], - [False, True, False], - [True, True, False], - [False, False, True], - [True, False, True]]) - - result_bool = DataConverter.to_dataarray(bool_data, coords={ - 'time': standard_coords['time'], - 'scenario': standard_coords['scenario'] - }) - - assert result_bool.dtype == bool - assert np.array_equal(result_bool.values, bool_data) - - def test_multid_array_no_coords(self): - """Multi-D arrays without coords should fail unless scalar.""" - # Multi-element fails - data_2d = np.random.rand(2, 3) - with pytest.raises(ConversionError, match="Cannot convert multi-element array without target dimensions"): - DataConverter.to_dataarray(data_2d) - - # Single element succeeds - single_element = np.array([[42]]) - result = DataConverter.to_dataarray(single_element) - assert result.shape == () - assert result.item() == 42 + # Verify broadcasting + for scenario in standard_coords['scenario']: + for region in standard_coords['region']: + assert np.array_equal(result.sel(scenario=scenario, region=region).values, time_arr) - def test_multid_array_empty_coords(self, standard_coords): - """Multi-D arrays with empty coords should fail.""" - data_2d = np.random.rand(5, 3) - with pytest.raises(ConversionError, match="Cannot convert multi-element array without target dimensions"): - DataConverter.to_dataarray(data_2d, coords={}) - - def test_multid_array_coordinate_validation(self): - """Multi-D arrays should validate coordinates properly.""" - # Test with time coordinate that's not DatetimeIndex - wrong_time = pd.Index([1, 2, 3, 4, 5], name='time') - scenario_coords = pd.Index(['A', 'B', 'C'], name='scenario') - - data_2d = np.random.rand(5, 3) - with pytest.raises(ConversionError, match="time coordinates must be a DatetimeIndex"): - DataConverter.to_dataarray(data_2d, coords={ - 'time': wrong_time, - 'scenario': scenario_coords - }) - - def test_multid_array_complex_scenario(self): - """Complex real-world scenario with multi-D array.""" - # Energy system data: time x technology x region + def test_complex_multid_scenario(self): + """Complex real-world scenario with multi-D array and broadcasting.""" + # Energy system data: time x technology, broadcast to regions coords = { - 'time': pd.date_range('2024-01-01', periods=8760, freq='H', name='time'), # 1 year hourly + 'time': pd.date_range('2024-01-01', periods=24, freq='H', name='time'), # 24 hours 'technology': pd.Index(['solar', 'wind', 'gas', 'coal'], name='technology'), # 4 technologies 'region': pd.Index(['north', 'south', 'east'], name='region') # 3 regions } - # Capacity factors: 8760 x 4 x 3 - capacity_factors = np.random.rand(8760, 4, 3) + # Capacity factors: 24 x 4 (will broadcast to 24 x 4 x 3) + capacity_factors = np.random.rand(24, 4) result = DataConverter.to_dataarray(capacity_factors, coords=coords) - assert result.shape == (8760, 4, 3) + assert result.shape == (24, 4, 3) assert result.dims == ('time', 'technology', 'region') assert isinstance(result.indexes['time'], pd.DatetimeIndex) - assert len(result.indexes['time']) == 8760 - assert len(result.indexes['technology']) == 4 - assert len(result.indexes['region']) == 3 - - def test_multid_array_edge_cases(self): - """Test edge cases for multi-D arrays.""" - # Single dimension with multi-D array should fail - coords_1d = {'time': pd.date_range('2024-01-01', periods=5, freq='D', name='time')} - data_2d = np.random.rand(5, 3) - with pytest.raises(ConversionError, match="Array has 2 dimensions but 1 target dimensions provided"): - DataConverter.to_dataarray(data_2d, coords=coords_1d) + # Verify broadcasting: all regions should have same time×technology data + for region in coords['region']: + assert np.array_equal(result.sel(region=region).values, capacity_factors) - # Zero dimensions with multi-D array should fail - data_1d = np.array([1, 2, 3]) - with pytest.raises(ConversionError, match="Cannot convert multi-element array without target dimensions"): - DataConverter.to_dataarray(data_1d, coords={}) + def test_ambiguous_length_handling(self): + """Test handling of ambiguous length scenarios across different data types.""" + # All dimensions have length 3 + coords_3x3x3 = { + 'time': pd.date_range('2024-01-01', periods=3, freq='D', name='time'), + 'scenario': pd.Index(['A', 'B', 'C'], name='scenario'), + 'region': pd.Index(['X', 'Y', 'Z'], name='region') + } + + # 1D array - should fail + arr_1d = np.array([1, 2, 3]) + with pytest.raises(ConversionError, match="matches multiple dimensions"): + DataConverter.to_dataarray(arr_1d, coords=coords_3x3x3) + + # 2D array - should fail + arr_2d = np.random.rand(3, 3) + with pytest.raises(ConversionError, match="matches multiple dimension orders"): + DataConverter.to_dataarray(arr_2d, coords=coords_3x3x3) - def test_multid_array_partial_dimension_match(self): - """Array where only some dimensions match should fail.""" + # 3D array - should fail + arr_3d = np.random.rand(3, 3, 3) + with pytest.raises(ConversionError, match="matches multiple dimension orders"): + DataConverter.to_dataarray(arr_3d, coords=coords_3x3x3) + + def test_mixed_broadcasting_scenarios(self): + """Test various broadcasting scenarios with different input types.""" coords = { - 'time': pd.date_range('2024-01-01', periods=5, freq='D', name='time'), # length 5 - 'scenario': pd.Index(['A', 'B', 'C'], name='scenario') # length 3 + 'time': pd.date_range('2024-01-01', periods=4, freq='D', name='time'), # length 4 + 'scenario': pd.Index(['A', 'B'], name='scenario'), # length 2 + 'region': pd.Index(['north', 'south', 'east'], name='region'), # length 3 + 'product': pd.Index(['X', 'Y', 'Z', 'W', 'V'], name='product') # length 5 } - # 5x7 array - first dimension matches time (5) but second doesn't match scenario (3) - data_2d = np.random.rand(5, 7) - with pytest.raises(ConversionError, match="Array dimensions do not match any coordinate lengths"): - DataConverter.to_dataarray(data_2d, coords=coords) + # Scalar to 4D + scalar_result = DataConverter.to_dataarray(42, coords=coords) + assert scalar_result.shape == (4, 2, 3, 5) + assert np.all(scalar_result.values == 42) + # 1D array (length 4, matches time) to 4D + arr_1d = np.array([10, 20, 30, 40]) + arr_result = DataConverter.to_dataarray(arr_1d, coords=coords) + assert arr_result.shape == (4, 2, 3, 5) + # Verify broadcasting + for scenario in coords['scenario']: + for region in coords['region']: + for product in coords['product']: + assert np.array_equal( + arr_result.sel(scenario=scenario, region=region, product=product).values, + arr_1d + ) + + # 2D array (4x2, matches time×scenario) to 4D + arr_2d = np.random.rand(4, 2) + arr_2d_result = DataConverter.to_dataarray(arr_2d, coords=coords) + assert arr_2d_result.shape == (4, 2, 3, 5) + # Verify broadcasting + for region in coords['region']: + for product in coords['product']: + assert np.array_equal( + arr_2d_result.sel(region=region, product=product).values, + arr_2d + ) if __name__ == '__main__': From ffc196a4f70efbe3a7165bcf7ac98777c43bb25d Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 5 Jul 2025 08:36:49 +0200 Subject: [PATCH 169/448] Rename methods and remove unused code --- flixopt/core.py | 192 ++++++------------------------------------------ 1 file changed, 22 insertions(+), 170 deletions(-) diff --git a/flixopt/core.py b/flixopt/core.py index 97e91c274..d4f83f5a1 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -141,11 +141,11 @@ class DataConverter: """ @staticmethod - def _convert_1d_data_to_dataarray( + def _convert_1d_with_index_matching( data: Union[np.ndarray, pd.Series], coords: Dict[str, pd.Index], target_dims: Tuple[str, ...] ) -> xr.DataArray: """ - Convert 1D data (array or Series) to DataArray by matching to one dimension. + Convert 1D data to DataArray, trying index matching for Series first, then length matching. Args: data: 1D numpy array or pandas Series @@ -161,16 +161,16 @@ def _convert_1d_data_to_dataarray( raise ConversionError('Cannot convert multi-element data without target dimensions') return xr.DataArray(data[0] if isinstance(data, np.ndarray) else data.iloc[0]) - # For Series, try to match index to coordinates + # For Series, try to match index to coordinates first if isinstance(data, pd.Series): for dim_name in target_dims: if data.index.equals(coords[dim_name]): return xr.DataArray(data.values.copy(), coords={dim_name: coords[dim_name]}, dims=[dim_name]) - # If no index matches, raise error - raise ConversionError(f'Data {data} does not match any of the target dimensions: {target_dims}') + # If no index matches, raise error for Series (they should match by index) + raise ConversionError(f'Series index does not match any target dimension coordinates: {target_dims}') - # For arrays or unmatched Series, match by length + # For arrays, match by length matching_dims = [] for dim_name in target_dims: if len(data) == len(coords[dim_name]): @@ -178,10 +178,10 @@ def _convert_1d_data_to_dataarray( if len(matching_dims) == 0: dim_info = {dim: len(coords[dim]) for dim in target_dims} - raise ConversionError(f'Data length {len(data)} matches none of the target dimensions: {dim_info}') + raise ConversionError(f'Array length {len(data)} matches none of the target dimensions: {dim_info}') elif len(matching_dims) > 1: raise ConversionError( - f'Data length {len(data)} matches multiple dimensions: {matching_dims}. Cannot determine which dimension to use.' + f'Array length {len(data)} matches multiple dimensions: {matching_dims}. Cannot determine which dimension to use.' ) # Match to the single matching dimension @@ -228,7 +228,7 @@ def _broadcast_to_target_dims( # Handle broadcasting from fewer to more dimensions if len(data.dims) < len(target_dims): - return DataConverter._broadcast_dataarray_to_more_dims(data, coords, target_dims) + return DataConverter._expand_to_more_dims(data, coords, target_dims) # Cannot handle more dimensions than target if len(data.dims) > len(target_dims): @@ -237,10 +237,10 @@ def _broadcast_to_target_dims( raise ConversionError(f'Cannot convert DataArray with dims {data.dims} to target dims {target_dims}') @staticmethod - def _broadcast_dataarray_to_more_dims( + def _expand_to_more_dims( data: xr.DataArray, coords: Dict[str, pd.Index], target_dims: Tuple[str, ...] ) -> xr.DataArray: - """Broadcast DataArray to additional dimensions.""" + """Expand DataArray to additional dimensions by broadcasting.""" # Validate that all source dimensions exist in target for dim in data.dims: if dim not in target_dims: @@ -280,11 +280,11 @@ def _broadcast_dataarray_to_more_dims( return xr.DataArray(result_data.copy(), coords=final_coords, dims=target_dims) @staticmethod - def _convert_multid_array_to_dataarray( + def _convert_multid_array_by_shape( data: np.ndarray, coords: Dict[str, pd.Index], target_dims: Tuple[str, ...] ) -> xr.DataArray: """ - Convert multi-dimensional numpy array to DataArray by matching dimensions by length. + Convert multi-dimensional numpy array to DataArray by matching dimensions by shape. Returns a DataArray that may need further broadcasting to target dimensions. Args: @@ -293,7 +293,7 @@ def _convert_multid_array_to_dataarray( target_dims: Target dimension names Returns: - DataArray with dimensions matched by length (may be subset of target_dims) + DataArray with dimensions matched by shape (may be subset of target_dims) Raises: ConversionError: If array dimensions cannot be uniquely matched to coordinates @@ -358,7 +358,7 @@ def to_dataarray( if coords is None: coords = {} - validated_coords, target_dims = DataConverter._prepare_dimensions(coords) + validated_coords, target_dims = DataConverter._validate_and_prepare_coords(coords) # Step 1: Convert to DataArray (may have fewer dimensions than target) if isinstance(data, (int, float, np.integer, np.floating)): @@ -367,17 +367,17 @@ def to_dataarray( elif isinstance(data, np.ndarray): if data.ndim == 1: - intermediate = DataConverter._convert_1d_data_to_dataarray(data, validated_coords, target_dims) + intermediate = DataConverter._convert_1d_with_index_matching(data, validated_coords, target_dims) else: # Handle multi-dimensional arrays - this now allows partial matching - intermediate = DataConverter._convert_multid_array_to_dataarray(data, validated_coords, target_dims) + intermediate = DataConverter._convert_multid_array_by_shape(data, validated_coords, target_dims) elif isinstance(data, pd.Series): if isinstance(data.index, pd.MultiIndex): raise ConversionError( 'Series index must be a single level Index. Multi-index Series are not supported.' ) - intermediate = DataConverter._convert_1d_data_to_dataarray(data, validated_coords, target_dims) + intermediate = DataConverter._convert_1d_with_index_matching(data, validated_coords, target_dims) elif isinstance(data, pd.DataFrame): if isinstance(data.index, pd.MultiIndex): @@ -388,13 +388,13 @@ def to_dataarray( raise ConversionError('DataFrame must have at least one column.') if len(data.columns) == 1: - intermediate = DataConverter._convert_1d_data_to_dataarray( + intermediate = DataConverter._convert_1d_with_index_matching( data.iloc[:, 0], validated_coords, target_dims ) else: # Handle multi-column DataFrames - this now allows partial matching logger.warning('Converting multi-column DataFrame to xr.DataArray. We advise to do this manually.') - intermediate = DataConverter._convert_multid_array_to_dataarray( + intermediate = DataConverter._convert_multid_array_by_shape( data.to_numpy(), validated_coords, target_dims ) @@ -411,9 +411,9 @@ def to_dataarray( return DataConverter._broadcast_to_target_dims(intermediate, validated_coords, target_dims) @staticmethod - def _prepare_dimensions(coords: Dict[str, pd.Index]) -> Tuple[Dict[str, pd.Index], Tuple[str, ...]]: + def _validate_and_prepare_coords(coords: Dict[str, pd.Index]) -> Tuple[Dict[str, pd.Index], Tuple[str, ...]]: """ - Prepare and validate coordinates for the DataArray. + Validate and prepare coordinates for the DataArray. Args: coords: Dictionary mapping dimension names to coordinate indices @@ -442,154 +442,6 @@ def _prepare_dimensions(coords: Dict[str, pd.Index]) -> Tuple[Dict[str, pd.Index return validated_coords, tuple(dims) - @staticmethod - def _convert_scalar( - data: Union[int, float, np.integer, np.floating], coords: Dict[str, pd.Index], dims: Tuple[str, ...] - ) -> xr.DataArray: - """Convert scalar to DataArray, broadcasting to all dimensions.""" - if isinstance(data, (np.integer, np.floating)): - data = data.item() - return xr.DataArray(data, coords=coords, dims=dims) - - @staticmethod - def _convert_1d_array(data: np.ndarray, coords: Dict[str, pd.Index], dims: Tuple[str, ...]) -> xr.DataArray: - """Convert 1D array to DataArray.""" - if len(dims) == 0: - if len(data) != 1: - raise ConversionError('Cannot convert multi-element array without dimensions') - return xr.DataArray(data[0]) - - elif len(dims) == 1: - dim_name = dims[0] - if len(data) != len(coords[dim_name]): - raise ConversionError( - f'Array length {len(data)} does not match {dim_name} length {len(coords[dim_name])}' - ) - return xr.DataArray(data.copy(), coords=coords, dims=dims) - - elif len(dims) == 2: - # Broadcast 1D array to 2D based on which dimension it matches - dim_lengths = {dim: len(coords[dim]) for dim in dims} - - # Find which dimension the array length matches - matching_dims = [dim for dim, length in dim_lengths.items() if len(data) == length] - - if len(matching_dims) == 0: - raise ConversionError(f'Array length {len(data)} matches none of the dimensions: {dim_lengths}') - elif len(matching_dims) > 1: - raise ConversionError( - f'Array length {len(data)} matches multiple dimensions: {matching_dims}. Cannot determine broadcasting direction.' - ) - - # Broadcast along the matching dimension - match_dim = matching_dims[0] - other_dim = [d for d in dims if d != match_dim][0] - - if dims.index(match_dim) == 0: # First dimension - values = np.repeat(data[:, np.newaxis], len(coords[other_dim]), axis=1) - else: # Second dimension - values = np.repeat(data[np.newaxis, :], len(coords[other_dim]), axis=0) - - return xr.DataArray(values.copy(), coords=coords, dims=dims) - - else: - raise ConversionError(f'Maximum 2 dimensions currently supported, got {len(dims)}') - - @staticmethod - def _convert_series(data: pd.Series, coords: Dict[str, pd.Index], dims: Tuple[str, ...]) -> xr.DataArray: - """Convert pandas Series to DataArray.""" - if len(dims) == 0: - if len(data) != 1: - raise ConversionError('Cannot convert multi-element Series without dimensions') - return xr.DataArray(data.iloc[0]) - - elif len(dims) == 1: - dim_name = dims[0] - if not data.index.equals(coords[dim_name]): - raise ConversionError(f'Series index does not match {dim_name} coordinates') - return xr.DataArray(data.values.copy(), coords=coords, dims=dims) - - elif len(dims) == 2: - # Check which dimension the Series index matches - if 'time' in coords and data.index.equals(coords['time']): - # Broadcast across other dimensions - other_dims = [d for d in dims if d != 'time'] - if len(other_dims) == 1: - other_dim = other_dims[0] - values = np.repeat(data.values[:, np.newaxis], len(coords[other_dim]), axis=1) - return xr.DataArray(values.copy(), coords=coords, dims=dims) - - elif len([d for d in dims if d != 'time']) == 1: - # Check if Series matches the non-time dimension - other_dim = [d for d in dims if d != 'time'][0] - if data.index.equals(coords[other_dim]): - # Broadcast across time - values = np.repeat(data.values[np.newaxis, :], len(coords['time']), axis=0) - return xr.DataArray(values.copy(), coords=coords, dims=dims) - - raise ConversionError(f'Series index must match one of the target dimensions: {list(coords.keys())}') - - else: - raise ConversionError('Maximum 2 dimensions supported') - - @staticmethod - def _handle_dataarray(data: xr.DataArray, coords: Dict[str, pd.Index], dims: Tuple[str, ...]) -> xr.DataArray: - """Handle existing DataArray - check compatibility or broadcast.""" - # If no target dimensions, data must be scalar - if len(dims) == 0: - if data.size != 1: - raise ConversionError('DataArray must be scalar when no dimensions specified') - return xr.DataArray(data.values.item()) - - # Check if already compatible - if data.dims == dims: - # Check if coordinates match - compatible = True - for dim in dims: - if not np.array_equal(data.coords[dim].values, coords[dim].values): - compatible = False - break - if compatible: - return data.copy() - - # Handle broadcasting from smaller to larger dimensions - if len(data.dims) < len(dims): - return DataConverter._broadcast_dataarray(data, coords, dims) - - # If dimensions don't match and can't broadcast, raise error - raise ConversionError(f'Cannot convert DataArray with dims {data.dims} to target dims {dims}') - - @staticmethod - def _broadcast_dataarray(data: xr.DataArray, coords: Dict[str, pd.Index], dims: Tuple[str, ...]) -> xr.DataArray: - """Broadcast DataArray to target dimensions.""" - if len(data.dims) == 0: - # Scalar DataArray - broadcast to all dimensions - return xr.DataArray(data.values.item(), coords=coords, dims=dims) - - elif len(data.dims) == 1 and len(dims) == 2: - source_dim = data.dims[0] - - # Check if source dimension exists in target - if source_dim not in coords: - raise ConversionError(f'Source dimension "{source_dim}" not found in target coordinates') - - # Check coordinate compatibility - if not np.array_equal(data.coords[source_dim].values, coords[source_dim].values): - raise ConversionError(f'Source {source_dim} coordinates do not match target coordinates') - - # Find the other dimension to broadcast to - other_dim = [d for d in dims if d != source_dim][0] - - # Broadcast based on dimension order - if dims.index(source_dim) == 0: # Source is first dimension - values = np.repeat(data.values[:, np.newaxis], len(coords[other_dim]), axis=1) - else: # Source is second dimension - values = np.repeat(data.values[np.newaxis, :], len(coords[other_dim]), axis=0) - - return xr.DataArray(values.copy(), coords=coords, dims=dims) - - raise ConversionError(f'Cannot broadcast from {data.dims} to {dims}') - def get_dataarray_stats(arr: xr.DataArray) -> Dict: """Generate statistical summary of a DataArray.""" From 6142f44ca44df5aec97f87533db68d97a539d246 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 5 Jul 2025 08:47:32 +0200 Subject: [PATCH 170/448] Improve DataConverter by better splitting handling per datatype. Series only matches index (for one dim). Numpy matches shape --- flixopt/core.py | 80 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 53 insertions(+), 27 deletions(-) diff --git a/flixopt/core.py b/flixopt/core.py index d4f83f5a1..d1e61b4c5 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -141,36 +141,60 @@ class DataConverter: """ @staticmethod - def _convert_1d_with_index_matching( - data: Union[np.ndarray, pd.Series], coords: Dict[str, pd.Index], target_dims: Tuple[str, ...] + def _convert_series_by_index( + data: pd.Series, coords: Dict[str, pd.Index], target_dims: Tuple[str, ...] ) -> xr.DataArray: """ - Convert 1D data to DataArray, trying index matching for Series first, then length matching. + Convert pandas Series to DataArray by matching index to coordinates. Args: - data: 1D numpy array or pandas Series + data: pandas Series coords: Available coordinates target_dims: Target dimension names Returns: - DataArray with the data matched to appropriate dimension + DataArray with the Series matched to appropriate dimension by index + + Raises: + ConversionError: If Series index doesn't match any target dimension coordinates """ if len(target_dims) == 0: - # No target dimensions - data must be single element if len(data) != 1: - raise ConversionError('Cannot convert multi-element data without target dimensions') - return xr.DataArray(data[0] if isinstance(data, np.ndarray) else data.iloc[0]) + raise ConversionError('Cannot convert multi-element Series without target dimensions') + return xr.DataArray(data.iloc[0]) + + # Try to match Series index to coordinates + for dim_name in target_dims: + if data.index.equals(coords[dim_name]): + return xr.DataArray(data.values.copy(), coords={dim_name: coords[dim_name]}, dims=[dim_name]) + + # If no index matches, raise error + raise ConversionError(f'Series index does not match any target dimension coordinates: {target_dims}') + + @staticmethod + def _convert_1d_array_by_length( + data: np.ndarray, coords: Dict[str, pd.Index], target_dims: Tuple[str, ...] + ) -> xr.DataArray: + """ + Convert 1D numpy array to DataArray by matching length to coordinates. + + Args: + data: 1D numpy array + coords: Available coordinates + target_dims: Target dimension names - # For Series, try to match index to coordinates first - if isinstance(data, pd.Series): - for dim_name in target_dims: - if data.index.equals(coords[dim_name]): - return xr.DataArray(data.values.copy(), coords={dim_name: coords[dim_name]}, dims=[dim_name]) + Returns: + DataArray with the array matched to appropriate dimension by length - # If no index matches, raise error for Series (they should match by index) - raise ConversionError(f'Series index does not match any target dimension coordinates: {target_dims}') + Raises: + ConversionError: If array length doesn't uniquely match a target dimension + """ + if len(target_dims) == 0: + if len(data) != 1: + raise ConversionError('Cannot convert multi-element array without target dimensions') + return xr.DataArray(data[0]) - # For arrays, match by length + # Match by length matching_dims = [] for dim_name in target_dims: if len(data) == len(coords[dim_name]): @@ -181,13 +205,14 @@ def _convert_1d_with_index_matching( raise ConversionError(f'Array length {len(data)} matches none of the target dimensions: {dim_info}') elif len(matching_dims) > 1: raise ConversionError( - f'Array length {len(data)} matches multiple dimensions: {matching_dims}. Cannot determine which dimension to use.' + f'Array length {len(data)} matches multiple dimensions: {matching_dims}. Cannot determine which ' + f'dimension to use. To avoid this error, convert the array to a DataArray with the correct dimensions ' + f'yourself.' ) # Match to the single matching dimension match_dim = matching_dims[0] - values = data.values.copy() if isinstance(data, pd.Series) else data.copy() - return xr.DataArray(values, coords={match_dim: coords[match_dim]}, dims=[match_dim]) + return xr.DataArray(data.copy(), coords={match_dim: coords[match_dim]}, dims=[match_dim]) @staticmethod def _broadcast_to_target_dims( @@ -334,8 +359,9 @@ def _convert_multid_array_by_shape( # Return DataArray with matched dimensions - broadcasting will happen later if needed return xr.DataArray(data.copy(), coords=matched_coords, dims=matched_dims) - @staticmethod + @classmethod def to_dataarray( + cls, data: Union[Scalar, np.ndarray, pd.Series, pd.DataFrame, xr.DataArray, TimeSeriesData], coords: Optional[Dict[str, pd.Index]] = None, ) -> xr.DataArray: @@ -358,7 +384,7 @@ def to_dataarray( if coords is None: coords = {} - validated_coords, target_dims = DataConverter._validate_and_prepare_coords(coords) + validated_coords, target_dims = cls._validate_and_prepare_coords(coords) # Step 1: Convert to DataArray (may have fewer dimensions than target) if isinstance(data, (int, float, np.integer, np.floating)): @@ -367,17 +393,17 @@ def to_dataarray( elif isinstance(data, np.ndarray): if data.ndim == 1: - intermediate = DataConverter._convert_1d_with_index_matching(data, validated_coords, target_dims) + intermediate = cls._convert_1d_array_by_length(data, validated_coords, target_dims) else: # Handle multi-dimensional arrays - this now allows partial matching - intermediate = DataConverter._convert_multid_array_by_shape(data, validated_coords, target_dims) + intermediate = cls._convert_multid_array_by_shape(data, validated_coords, target_dims) elif isinstance(data, pd.Series): if isinstance(data.index, pd.MultiIndex): raise ConversionError( 'Series index must be a single level Index. Multi-index Series are not supported.' ) - intermediate = DataConverter._convert_1d_with_index_matching(data, validated_coords, target_dims) + intermediate = cls._convert_series_by_index(data, validated_coords, target_dims) elif isinstance(data, pd.DataFrame): if isinstance(data.index, pd.MultiIndex): @@ -388,13 +414,13 @@ def to_dataarray( raise ConversionError('DataFrame must have at least one column.') if len(data.columns) == 1: - intermediate = DataConverter._convert_1d_with_index_matching( + intermediate = cls._convert_series_by_index( data.iloc[:, 0], validated_coords, target_dims ) else: # Handle multi-column DataFrames - this now allows partial matching logger.warning('Converting multi-column DataFrame to xr.DataArray. We advise to do this manually.') - intermediate = DataConverter._convert_multid_array_by_shape( + intermediate = cls._convert_multid_array_by_shape( data.to_numpy(), validated_coords, target_dims ) @@ -408,7 +434,7 @@ def to_dataarray( # Step 2: Broadcast to target dimensions if needed # This now handles cases where intermediate has some but not all target dimensions - return DataConverter._broadcast_to_target_dims(intermediate, validated_coords, target_dims) + return cls._broadcast_to_target_dims(intermediate, validated_coords, target_dims) @staticmethod def _validate_and_prepare_coords(coords: Dict[str, pd.Index]) -> Tuple[Dict[str, pd.Index], Tuple[str, ...]]: From cec7367687f16b08cc12973cc7bca943457f9e4f Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 5 Jul 2025 08:53:23 +0200 Subject: [PATCH 171/448] Add test for error handling --- tests/test_dataconverter.py | 271 +++++++++++++++++++++++++++++++++++- 1 file changed, 270 insertions(+), 1 deletion(-) diff --git a/tests/test_dataconverter.py b/tests/test_dataconverter.py index c174aebe8..2fbad4a13 100644 --- a/tests/test_dataconverter.py +++ b/tests/test_dataconverter.py @@ -679,7 +679,7 @@ def test_error_message_quality(self, standard_coords): try: DataConverter.to_dataarray(data_2d, coords=coords_2d) - assert False, "Should have raised ConversionError" + raise AssertionError('Should have raised ConversionError') except ConversionError as e: error_msg = str(e) assert "Array shape: (7, 8)" in error_msg @@ -889,6 +889,275 @@ def test_mixed_broadcasting_scenarios(self): arr_2d ) +class TestAmbiguousDimensionLengthHandling: + """Test that DataConverter correctly raises errors when multiple dimensions have the same length.""" + + def test_1d_array_ambiguous_dimensions_simple(self): + """Test 1D array with two dimensions of same length should fail.""" + # Both dimensions have length 3 + coords_ambiguous = { + 'scenario': pd.Index(['A', 'B', 'C'], name='scenario'), # length 3 + 'region': pd.Index(['north', 'south', 'east'], name='region'), # length 3 + } + + arr_1d = np.array([1, 2, 3]) # length 3 - matches both dimensions + + with pytest.raises(ConversionError, match='matches multiple dimensions'): + DataConverter.to_dataarray(arr_1d, coords=coords_ambiguous) + + def test_1d_array_ambiguous_dimensions_complex(self): + """Test 1D array with multiple dimensions of same length.""" + # Three dimensions have length 4 + coords_4x4x4 = { + 'time': pd.date_range('2024-01-01', periods=4, freq='D', name='time'), # length 4 + 'scenario': pd.Index(['A', 'B', 'C', 'D'], name='scenario'), # length 4 + 'region': pd.Index(['north', 'south', 'east', 'west'], name='region'), # length 4 + 'product': pd.Index(['X', 'Y'], name='product'), # length 2 - unique + } + + # Array matching the ambiguous length + arr_1d = np.array([10, 20, 30, 40]) # length 4 - matches time, scenario, region + + with pytest.raises(ConversionError, match='matches multiple dimensions'): + DataConverter.to_dataarray(arr_1d, coords=coords_4x4x4) + + # Array matching the unique length should work + arr_1d_unique = np.array([100, 200]) # length 2 - matches only product + result = DataConverter.to_dataarray(arr_1d_unique, coords=coords_4x4x4) + assert result.shape == (4, 4, 4, 2) # broadcast to all dimensions + assert result.dims == ('time', 'scenario', 'region', 'product') + + def test_2d_array_ambiguous_dimensions_both_same(self): + """Test 2D array where both dimensions have the same ambiguous length.""" + # All dimensions have length 3 + coords_3x3x3 = { + 'time': pd.date_range('2024-01-01', periods=3, freq='D', name='time'), # length 3 + 'scenario': pd.Index(['A', 'B', 'C'], name='scenario'), # length 3 + 'region': pd.Index(['X', 'Y', 'Z'], name='region'), # length 3 + } + + # 3x3 array - could be any combination of the three dimensions + arr_2d = np.random.rand(3, 3) + + with pytest.raises(ConversionError, match='matches multiple dimension orders'): + DataConverter.to_dataarray(arr_2d, coords=coords_3x3x3) + + def test_2d_array_one_dimension_ambiguous(self): + """Test 2D array where only one dimension length is ambiguous.""" + coords_mixed = { + 'time': pd.date_range('2024-01-01', periods=5, freq='D', name='time'), # length 5 - unique + 'scenario': pd.Index(['A', 'B', 'C'], name='scenario'), # length 3 + 'region': pd.Index(['X', 'Y', 'Z'], name='region'), # length 3 - same as scenario + 'product': pd.Index(['P1', 'P2'], name='product'), # length 2 - unique + } + + # 5x3 array - first dimension clearly maps to time (unique length 5) + # but second dimension could be scenario or region (both length 3) + arr_5x3 = np.random.rand(5, 3) + + with pytest.raises(ConversionError, match='matches multiple dimension orders'): + DataConverter.to_dataarray(arr_5x3, coords=coords_mixed) + + # 5x2 array should work - dimensions are unambiguous + arr_5x2 = np.random.rand(5, 2) + result = DataConverter.to_dataarray( + arr_5x2, coords={'time': coords_mixed['time'], 'product': coords_mixed['product']} + ) + assert result.shape == (5, 2) + assert result.dims == ('time', 'product') + + def test_3d_array_all_dimensions_ambiguous(self): + """Test 3D array where all dimension lengths are ambiguous.""" + # All dimensions have length 2 + coords_2x2x2x2 = { + 'scenario': pd.Index(['A', 'B'], name='scenario'), # length 2 + 'region': pd.Index(['north', 'south'], name='region'), # length 2 + 'technology': pd.Index(['solar', 'wind'], name='technology'), # length 2 + 'product': pd.Index(['X', 'Y'], name='product'), # length 2 + } + + # 2x2x2 array - could be any combination of 3 dimensions from the 4 available + arr_3d = np.random.rand(2, 2, 2) + + with pytest.raises(ConversionError, match='matches multiple dimension orders'): + DataConverter.to_dataarray(arr_3d, coords=coords_2x2x2x2) + + def test_3d_array_partial_ambiguity(self): + """Test 3D array with partial dimension ambiguity.""" + coords_partial = { + 'time': pd.date_range('2024-01-01', periods=4, freq='D', name='time'), # length 4 - unique + 'scenario': pd.Index(['A', 'B', 'C'], name='scenario'), # length 3 + 'region': pd.Index(['X', 'Y', 'Z'], name='region'), # length 3 - same as scenario + 'technology': pd.Index(['solar', 'wind'], name='technology'), # length 2 - unique + } + + # 4x3x2 array - first and third dimensions are unique, middle is ambiguous + # This should still fail because middle dimension (length 3) could be scenario or region + arr_4x3x2 = np.random.rand(4, 3, 2) + + with pytest.raises(ConversionError, match='matches multiple dimension orders'): + DataConverter.to_dataarray(arr_4x3x2, coords=coords_partial) + + def test_pandas_series_ambiguous_dimensions(self): + """Test pandas Series with ambiguous dimension lengths.""" + coords_ambiguous = { + 'scenario': pd.Index(['A', 'B', 'C'], name='scenario'), # length 3 + 'region': pd.Index(['north', 'south', 'east'], name='region'), # length 3 + } + + # Series with length 3 but index that doesn't match either coordinate exactly + generic_series = pd.Series([10, 20, 30], index=[0, 1, 2]) + + # Should fail because length matches multiple dimensions and index doesn't match any + with pytest.raises(ConversionError, match='index does not match any target dimension'): + DataConverter.to_dataarray(generic_series, coords=coords_ambiguous) + + # Series with index that matches one of the ambiguous coordinates should work + scenario_series = pd.Series([10, 20, 30], index=coords_ambiguous['scenario']) + result = DataConverter.to_dataarray(scenario_series, coords=coords_ambiguous) + assert result.shape == (3, 3) # should broadcast to both dimensions + assert result.dims == ('scenario', 'region') + + def test_edge_case_many_same_lengths(self): + """Test edge case with many dimensions having the same length.""" + # Five dimensions all have length 2 + coords_many = { + 'dim1': pd.Index(['A', 'B'], name='dim1'), + 'dim2': pd.Index(['X', 'Y'], name='dim2'), + 'dim3': pd.Index(['P', 'Q'], name='dim3'), + 'dim4': pd.Index(['M', 'N'], name='dim4'), + 'dim5': pd.Index(['U', 'V'], name='dim5'), + } + + # 1D array + arr_1d = np.array([1, 2]) + with pytest.raises(ConversionError, match='matches multiple dimensions'): + DataConverter.to_dataarray(arr_1d, coords=coords_many) + + # 2D array + arr_2d = np.random.rand(2, 2) + with pytest.raises(ConversionError, match='matches multiple dimension orders'): + DataConverter.to_dataarray(arr_2d, coords=coords_many) + + # 3D array + arr_3d = np.random.rand(2, 2, 2) + with pytest.raises(ConversionError, match='matches multiple dimension orders'): + DataConverter.to_dataarray(arr_3d, coords=coords_many) + + def test_mixed_lengths_with_duplicates(self): + """Test mixed scenario with some duplicate and some unique lengths.""" + coords_mixed = { + 'time': pd.date_range('2024-01-01', periods=8, freq='D', name='time'), # length 8 - unique + 'scenario': pd.Index(['A', 'B', 'C'], name='scenario'), # length 3 + 'region': pd.Index(['X', 'Y', 'Z'], name='region'), # length 3 - same as scenario + 'technology': pd.Index(['solar'], name='technology'), # length 1 - unique + 'product': pd.Index(['P1', 'P2', 'P3', 'P4', 'P5'], name='product'), # length 5 - unique + } + + # Arrays with unique lengths should work + arr_8 = np.arange(8) + result_8 = DataConverter.to_dataarray(arr_8, coords=coords_mixed) + assert result_8.dims == ('time', 'scenario', 'region', 'technology', 'product') + + arr_1 = np.array([42]) + result_1 = DataConverter.to_dataarray(arr_1, coords={'technology': coords_mixed['technology']}) + assert result_1.shape == (1,) + + arr_5 = np.arange(5) + result_5 = DataConverter.to_dataarray(arr_5, coords={'product': coords_mixed['product']}) + assert result_5.shape == (5,) + + # Arrays with ambiguous length should fail + arr_3 = np.array([1, 2, 3]) # matches both scenario and region + with pytest.raises(ConversionError, match='matches multiple dimensions'): + DataConverter.to_dataarray(arr_3, coords=coords_mixed) + + def test_dataframe_with_ambiguous_dimensions(self): + """Test DataFrame handling with ambiguous dimensions.""" + coords_ambiguous = { + 'scenario': pd.Index(['A', 'B', 'C'], name='scenario'), # length 3 + 'region': pd.Index(['X', 'Y', 'Z'], name='region'), # length 3 + } + + # Multi-column DataFrame with ambiguous dimensions + df = pd.DataFrame({'col1': [1, 2, 3], 'col2': [4, 5, 6], 'col3': [7, 8, 9]}) # 3x3 DataFrame + + # Should fail due to ambiguous dimensions + with pytest.raises(ConversionError, match='matches multiple dimension orders'): + DataConverter.to_dataarray(df, coords=coords_ambiguous) + + def test_error_message_quality_for_ambiguous_dimensions(self): + """Test that error messages for ambiguous dimensions are helpful.""" + coords_ambiguous = { + 'scenario': pd.Index(['A', 'B', 'C'], name='scenario'), + 'region': pd.Index(['north', 'south', 'east'], name='region'), + 'technology': pd.Index(['solar', 'wind', 'gas'], name='technology'), + } + + # 1D array case + arr_1d = np.array([1, 2, 3]) + try: + DataConverter.to_dataarray(arr_1d, coords=coords_ambiguous) + raise AssertionError('Should have raised ConversionError') + except ConversionError as e: + error_msg = str(e) + assert 'matches multiple dimensions' in error_msg + assert 'scenario' in error_msg + assert 'region' in error_msg + assert 'technology' in error_msg + + # 2D array case + arr_2d = np.random.rand(3, 3) + try: + DataConverter.to_dataarray(arr_2d, coords=coords_ambiguous) + raise AssertionError('Should have raised ConversionError') + except ConversionError as e: + error_msg = str(e) + assert 'matches multiple dimension orders' in error_msg + assert '(3, 3)' in error_msg + + def test_ambiguous_with_broadcasting_target(self): + """Test ambiguous dimensions when target includes broadcasting.""" + coords_ambiguous_plus = { + 'time': pd.date_range('2024-01-01', periods=5, freq='D', name='time'), # length 5 + 'scenario': pd.Index(['A', 'B', 'C'], name='scenario'), # length 3 + 'region': pd.Index(['X', 'Y', 'Z'], name='region'), # length 3 - same as scenario + 'technology': pd.Index(['solar', 'wind'], name='technology'), # length 2 + } + + # 1D array with ambiguous length, but targeting broadcast scenario + arr_3 = np.array([10, 20, 30]) # length 3, matches scenario and region + + # Should fail even though it would broadcast to other dimensions + with pytest.raises(ConversionError, match='matches multiple dimensions'): + DataConverter.to_dataarray(arr_3, coords=coords_ambiguous_plus) + + # 2D array with one ambiguous dimension + arr_5x3 = np.random.rand(5, 3) # 5 is unique (time), 3 is ambiguous (scenario/region) + + with pytest.raises(ConversionError, match='matches multiple dimension orders'): + DataConverter.to_dataarray(arr_5x3, coords=coords_ambiguous_plus) + + def test_time_dimension_ambiguity(self): + """Test ambiguity specifically involving time dimension.""" + # Create scenario where time has same length as another dimension + coords_time_ambiguous = { + 'time': pd.date_range('2024-01-01', periods=3, freq='D', name='time'), # length 3 + 'scenario': pd.Index(['base', 'high', 'low'], name='scenario'), # length 3 - same as time + 'region': pd.Index(['north', 'south'], name='region'), # length 2 - unique + } + + # Time-indexed series should work even with ambiguous lengths (index matching takes precedence) + time_series = pd.Series([100, 200, 300], index=coords_time_ambiguous['time']) + result = DataConverter.to_dataarray(time_series, coords=coords_time_ambiguous) + assert result.shape == (3, 3, 2) + assert result.dims == ('time', 'scenario', 'region') + + # But generic array with length 3 should still fail + generic_array = np.array([100, 200, 300]) + with pytest.raises(ConversionError, match='matches multiple dimensions'): + DataConverter.to_dataarray(generic_array, coords=coords_time_ambiguous) + if __name__ == '__main__': pytest.main() From eba1ec436ad4156051582b001f6f7ef6c290c737 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 5 Jul 2025 08:53:36 +0200 Subject: [PATCH 172/448] Update scenario example --- examples/04_Scenarios/scenario_example.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/04_Scenarios/scenario_example.py b/examples/04_Scenarios/scenario_example.py index b9932a016..ae53cc1ff 100644 --- a/examples/04_Scenarios/scenario_example.py +++ b/examples/04_Scenarios/scenario_example.py @@ -70,7 +70,8 @@ discharging=fx.Flow('Q_th_unload', bus='Fernwärme', size=1000), capacity_in_flow_hours=fx.InvestParameters(fix_effects=20, fixed_size=30, optional=False), initial_charge_state=0, # Initial storage state: empty - relative_maximum_charge_state=np.array([80, 70, 80, 80, 80, 80, 80, 80, 80, 80]) * 0.01, + relative_maximum_charge_state=np.array([80, 70, 80, 80, 80, 80, 80, 80, 80]) * 0.01, + relative_maximum_final_charge_state=0.8, eta_charge=0.9, eta_discharge=1, # Efficiency factors for charging/discharging relative_loss_per_hour=0.08, # 8% loss per hour. Absolute loss depends on current charge state From 5f97bf3cab08c1efa628b96430f791e8fbbad478 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 7 Jul 2025 14:10:16 +0200 Subject: [PATCH 173/448] Fix Handling of TimeSeriesData --- flixopt/calculation.py | 6 ++++-- flixopt/core.py | 28 ++++++++++++++++++++++++++-- flixopt/flow_system.py | 5 +---- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/flixopt/calculation.py b/flixopt/calculation.py index e035aaa13..b3eec2d06 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -61,8 +61,10 @@ def __init__( """ self.name = name if flow_system.used_in_calculation: - logging.warning(f'FlowSystem {flow_system} is already used in a calculation. ' - f'Creating a copy for Calculation "{self.name}".') + logger.warning( + f'FlowSystem {flow_system} is already used in a calculation. ' + f'Creating a copy of the FlowSystem for Calculation "{self.name}".' + ) flow_system = flow_system.copy() if active_timesteps is not None: diff --git a/flixopt/core.py b/flixopt/core.py index d1e61b4c5..39490f536 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -51,8 +51,15 @@ class TimeSeriesData(xr.DataArray): __slots__ = () # No additional instance attributes - everything goes in attrs - def __init__(self, *args, aggregation_group: Optional[str] = None, aggregation_weight: Optional[float] = None, - agg_group: Optional[str] = None, agg_weight: Optional[float] = None, **kwargs): + def __init__( + self, + *args, + aggregation_group: Optional[str] = None, + aggregation_weight: Optional[float] = None, + agg_group: Optional[str] = None, + agg_weight: Optional[float] = None, + **kwargs + ): """ Args: *args: Arguments passed to DataArray @@ -84,6 +91,23 @@ def __init__(self, *args, aggregation_group: Optional[str] = None, aggregation_w # Always mark as TimeSeriesData self.attrs['__timeseries_data__'] = True + def fit_to_coords( + self, + coords: Dict[str, pd.Index], + name: Optional[str] = None, + ) -> 'TimeSeriesData': + """Fit the data to the given coordinates. Returns a new TimeSeriesData object if the current coords are different.""" + if self.coords.equals(xr.Coordinates(coords)): + return self + + da = DataConverter.to_dataarray(self.data, coords=coords) + return self.__class__( + da, + aggregation_group=self.aggregation_group, + aggregation_weight=self.aggregation_weight, + name=name if name is not None else self.name + ) + @property def aggregation_group(self) -> Optional[str]: return self.attrs.get('aggregation_group') diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 0568f3b3a..3fa920b84 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -343,10 +343,7 @@ def fit_to_model_coords( if isinstance(data, TimeSeriesData): try: data.name = name # Set name of previous object! - return TimeSeriesData( - DataConverter.to_dataarray(data, coords=coords), - aggregation_group=data.aggregation_group, aggregation_weight=data.aggregation_weight - ).rename(name) + return data.fit_to_coords(coords) except ConversionError as e: raise ConversionError( f'Could not convert time series data "{name}" to DataArray:\n{data}\nOriginal Error: {e}') from e From 9351083c65bb893863d040f96852e517f918f048 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 7 Jul 2025 14:31:05 +0200 Subject: [PATCH 174/448] Improve DataConverter --- flixopt/core.py | 207 +++++++++++++++--------------------------------- 1 file changed, 66 insertions(+), 141 deletions(-) diff --git a/flixopt/core.py b/flixopt/core.py index 39490f536..ee0ef0540 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -158,6 +158,7 @@ class DataConverter: Supports: - Scalars (broadcast to all dimensions) - 1D data (np.ndarray, pd.Series, single-column DataFrame) + - Multi-dimensional arrays - xr.DataArray (validated and potentially broadcast) Simple 1D data is matched to one dimension and broadcast to others. @@ -165,11 +166,11 @@ class DataConverter: """ @staticmethod - def _convert_series_by_index( + def _match_series_to_dimension( data: pd.Series, coords: Dict[str, pd.Index], target_dims: Tuple[str, ...] ) -> xr.DataArray: """ - Convert pandas Series to DataArray by matching index to coordinates. + Match pandas Series to a dimension by comparing its index to coordinates. Args: data: pandas Series @@ -196,11 +197,11 @@ def _convert_series_by_index( raise ConversionError(f'Series index does not match any target dimension coordinates: {target_dims}') @staticmethod - def _convert_1d_array_by_length( + def _match_array_to_dimension( data: np.ndarray, coords: Dict[str, pd.Index], target_dims: Tuple[str, ...] ) -> xr.DataArray: """ - Convert 1D numpy array to DataArray by matching length to coordinates. + Match 1D numpy array to a dimension by comparing its length to coordinate lengths. Args: data: 1D numpy array @@ -218,7 +219,7 @@ def _convert_1d_array_by_length( raise ConversionError('Cannot convert multi-element array without target dimensions') return xr.DataArray(data[0]) - # Match by length + # Find dimensions with matching lengths matching_dims = [] for dim_name in target_dims: if len(data) == len(coords[dim_name]): @@ -239,102 +240,11 @@ def _convert_1d_array_by_length( return xr.DataArray(data.copy(), coords={match_dim: coords[match_dim]}, dims=[match_dim]) @staticmethod - def _broadcast_to_target_dims( - data: xr.DataArray, coords: Dict[str, pd.Index], target_dims: Tuple[str, ...] - ) -> xr.DataArray: - """ - Broadcast DataArray to match target dimensions. - - Args: - data: Source DataArray - coords: Target coordinates - target_dims: Target dimension names - - Returns: - DataArray broadcast to target dimensions - """ - if len(target_dims) == 0: - # Target is scalar - if data.size != 1: - raise ConversionError('Cannot convert multi-element DataArray to scalar') - return xr.DataArray(data.values.item()) - - # If data already matches target, validate coordinates and return - if set(data.dims) == set(target_dims) and len(data.dims) == len(target_dims): - # Check coordinate compatibility - for dim in data.dims: - if dim in coords and not np.array_equal(data.coords[dim].values, coords[dim].values): - raise ConversionError(f'DataArray {dim} coordinates do not match target coordinates') - - # Ensure correct dimension order - if data.dims != target_dims: - data = data.transpose(*target_dims) - return data.copy() - - # Handle scalar data (0D) - broadcast to all dimensions - if data.ndim == 0: - return xr.DataArray(data.item(), coords=coords, dims=target_dims) - - # Handle broadcasting from fewer to more dimensions - if len(data.dims) < len(target_dims): - return DataConverter._expand_to_more_dims(data, coords, target_dims) - - # Cannot handle more dimensions than target - if len(data.dims) > len(target_dims): - raise ConversionError(f'Cannot reduce DataArray from {len(data.dims)} to {len(target_dims)} dimensions') - - raise ConversionError(f'Cannot convert DataArray with dims {data.dims} to target dims {target_dims}') - - @staticmethod - def _expand_to_more_dims( - data: xr.DataArray, coords: Dict[str, pd.Index], target_dims: Tuple[str, ...] - ) -> xr.DataArray: - """Expand DataArray to additional dimensions by broadcasting.""" - # Validate that all source dimensions exist in target - for dim in data.dims: - if dim not in target_dims: - raise ConversionError(f'Source dimension "{dim}" not found in target dimensions {target_dims}') - - # Check coordinate compatibility - if not np.array_equal(data.coords[dim].values, coords[dim].values): - raise ConversionError(f'Source {dim} coordinates do not match target coordinates') - - # Start with the original data - result_data = data.values - result_dims = list(data.dims) - result_coords = {dim: data.coords[dim] for dim in data.dims} - - # Add missing dimensions one by one - for target_dim in target_dims: - if target_dim not in result_dims: - # Add this dimension at the end - result_data = np.expand_dims(result_data, axis=-1) - result_dims.append(target_dim) - result_coords[target_dim] = coords[target_dim] - - # Broadcast along the new dimension - new_shape = list(result_data.shape) - new_shape[-1] = len(coords[target_dim]) - result_data = np.broadcast_to(result_data, new_shape) - - # Reorder dimensions to match target order - if tuple(result_dims) != target_dims: - # Create mapping from current to target order - dim_indices = [result_dims.index(dim) for dim in target_dims] - result_data = np.transpose(result_data, dim_indices) - - # Build final coordinates dict in target order - final_coords = {dim: coords[dim] for dim in target_dims} - - return xr.DataArray(result_data.copy(), coords=final_coords, dims=target_dims) - - @staticmethod - def _convert_multid_array_by_shape( + def _match_multidim_array_to_dimensions( data: np.ndarray, coords: Dict[str, pd.Index], target_dims: Tuple[str, ...] ) -> xr.DataArray: """ - Convert multi-dimensional numpy array to DataArray by matching dimensions by shape. - Returns a DataArray that may need further broadcasting to target dimensions. + Match multi-dimensional numpy array to dimensions by finding the correct shape permutation. Args: data: Multi-dimensional numpy array @@ -342,7 +252,7 @@ def _convert_multid_array_by_shape( target_dims: Target dimension names Returns: - DataArray with dimensions matched by shape (may be subset of target_dims) + DataArray with dimensions matched by shape Raises: ConversionError: If array dimensions cannot be uniquely matched to coordinates @@ -352,17 +262,14 @@ def _convert_multid_array_by_shape( raise ConversionError('Cannot convert multi-element array without target dimensions') return xr.DataArray(data.item()) - # Get lengths of each dimension + from itertools import permutations + array_shape = data.shape coord_lengths = {dim: len(coords[dim]) for dim in target_dims} - # Find all possible ways to match array dimensions to available coordinates - from itertools import permutations - - # Try all permutations of target_dims that match the array's number of dimensions + # Find all possible dimension mappings possible_mappings = [] for dim_subset in permutations(target_dims, data.ndim): - # Check if this permutation matches the array shape if all(array_shape[i] == coord_lengths[dim_subset[i]] for i in range(len(dim_subset))): possible_mappings.append(dim_subset) @@ -376,58 +283,80 @@ def _convert_multid_array_by_shape( 'Cannot uniquely determine dimension mapping.' ) - # Use the unique mapping found matched_dims = possible_mappings[0] matched_coords = {dim: coords[dim] for dim in matched_dims} - # Return DataArray with matched dimensions - broadcasting will happen later if needed return xr.DataArray(data.copy(), coords=matched_coords, dims=matched_dims) + @staticmethod + def _broadcast_to_target( + data: xr.DataArray, coords: Dict[str, pd.Index], target_dims: Tuple[str, ...] + ) -> xr.DataArray: + """ + Broadcast DataArray to target dimensions with validation. + + Handles all cases: scalar expansion, dimension validation, coordinate matching, + and broadcasting to additional dimensions using xarray's capabilities. + """ + # Cannot reduce dimensions of data + if len(data.dims) > len(target_dims): + raise ConversionError(f'Cannot reduce DataArray from {len(data.dims)} to {len(target_dims)} dimensions') + + # Validate coordinate compatibility + for dim in data.dims: + if dim not in target_dims: + raise ConversionError(f'Source dimension "{dim}" not found in target dimensions {target_dims}') + + if not np.array_equal(data.coords[dim].values, coords[dim].values): + raise ConversionError(f'DataArray {dim} coordinates do not match target coordinates') + + # Use xarray's broadcast_like for efficient expansion and broadcasting + target_template = xr.DataArray( + np.empty([len(coords[dim]) for dim in target_dims]), coords=coords, dims=target_dims + ) + return data.broadcast_like(target_template).transpose(*target_dims) + @classmethod def to_dataarray( cls, - data: Union[Scalar, np.ndarray, pd.Series, pd.DataFrame, xr.DataArray, TimeSeriesData], + data: Union[float, int, np.ndarray, pd.Series, pd.DataFrame, xr.DataArray], coords: Optional[Dict[str, pd.Index]] = None, ) -> xr.DataArray: """ - Convert data to xarray.DataArray with specified coordinates. - - Accepts: - - Scalars (broadcast to all dimensions) - - 1D arrays or Series (matched to one dimension, broadcast to others) - - Multi-D arrays or DataFrames (dimensions matched by length, broadcast to remaining) - - xr.DataArray (validated and potentially broadcast to additional dimensions) + Convert various data types to xarray.DataArray with specified coordinates. Args: - data: Data to convert + data: Data to convert (scalar, array, Series, DataFrame, or DataArray) coords: Dictionary mapping dimension names to coordinate indices Returns: - DataArray with the converted data + DataArray with the converted data broadcast to target dimensions + + Raises: + ConversionError: If data cannot be converted or dimensions are ambiguous """ if coords is None: coords = {} - validated_coords, target_dims = cls._validate_and_prepare_coords(coords) + validated_coords, target_dims = cls._prepare_coordinates(coords) - # Step 1: Convert to DataArray (may have fewer dimensions than target) + # Step 1: Convert input data to initial DataArray if isinstance(data, (int, float, np.integer, np.floating)): - # Scalars: create 0D DataArray, will be broadcast later + # Scalar values intermediate = xr.DataArray(data.item() if hasattr(data, 'item') else data) elif isinstance(data, np.ndarray): if data.ndim == 1: - intermediate = cls._convert_1d_array_by_length(data, validated_coords, target_dims) + intermediate = cls._match_array_to_dimension(data, validated_coords, target_dims) else: - # Handle multi-dimensional arrays - this now allows partial matching - intermediate = cls._convert_multid_array_by_shape(data, validated_coords, target_dims) + intermediate = cls._match_multidim_array_to_dimensions(data, validated_coords, target_dims) elif isinstance(data, pd.Series): if isinstance(data.index, pd.MultiIndex): raise ConversionError( 'Series index must be a single level Index. Multi-index Series are not supported.' ) - intermediate = cls._convert_series_by_index(data, validated_coords, target_dims) + intermediate = cls._match_series_to_dimension(data, validated_coords, target_dims) elif isinstance(data, pd.DataFrame): if isinstance(data.index, pd.MultiIndex): @@ -438,44 +367,40 @@ def to_dataarray( raise ConversionError('DataFrame must have at least one column.') if len(data.columns) == 1: - intermediate = cls._convert_series_by_index( - data.iloc[:, 0], validated_coords, target_dims - ) + # Single-column DataFrame - treat as Series + intermediate = cls._match_series_to_dimension(data.iloc[:, 0], validated_coords, target_dims) else: - # Handle multi-column DataFrames - this now allows partial matching - logger.warning('Converting multi-column DataFrame to xr.DataArray. We advise to do this manually.') - intermediate = cls._convert_multid_array_by_shape( - data.to_numpy(), validated_coords, target_dims - ) + # Multi-column DataFrame - treat as multi-dimensional array + intermediate = cls._match_multidim_array_to_dimensions(data.to_numpy(), validated_coords, target_dims) elif isinstance(data, xr.DataArray): intermediate = data.copy() else: - raise ConversionError( - f'Unsupported data type: {type(data).__name__}. Only scalars, arrays, Series, DataFrames, and DataArrays are supported.' - ) + raise ConversionError(f'Unsupported data type: {type(data).__name__}.') - # Step 2: Broadcast to target dimensions if needed - # This now handles cases where intermediate has some but not all target dimensions - return cls._broadcast_to_target_dims(intermediate, validated_coords, target_dims) + # Step 2: Broadcast to target dimensions + return cls._broadcast_to_target(intermediate, validated_coords, target_dims) @staticmethod - def _validate_and_prepare_coords(coords: Dict[str, pd.Index]) -> Tuple[Dict[str, pd.Index], Tuple[str, ...]]: + def _prepare_coordinates(coords: Dict[str, pd.Index]) -> Tuple[Dict[str, pd.Index], Tuple[str, ...]]: """ - Validate and prepare coordinates for the DataArray. + Validate coordinates and prepare them for DataArray creation. Args: coords: Dictionary mapping dimension names to coordinate indices Returns: Tuple of (validated coordinates dict, dimensions tuple) + + Raises: + ConversionError: If coordinates are invalid """ validated_coords = {} dims = [] for dim_name, coord_index in coords.items(): - # Validate coordinate index + # Basic validation if not isinstance(coord_index, pd.Index) or len(coord_index) == 0: raise ConversionError(f'{dim_name} coordinates must be a non-empty pandas Index') From 99e6b1956f88f16e95818111286c86b2b606c87c Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 15 Jul 2025 15:19:32 +0200 Subject: [PATCH 175/448] Fix resampling of the FlowSystem --- flixopt/flow_system.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 3fa920b84..7001ca9e3 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -694,6 +694,7 @@ def resample( ) -> 'FlowSystem': """ Create a resampled FlowSystem by resampling data along the time dimension (like xr.Dataset.resample()). + Only resamples data variables that have a time dimension. Args: time: Resampling frequency (e.g., '3h', '2D', '1M') @@ -707,12 +708,32 @@ def resample( self.connect_and_transform() dataset = self.to_dataset() - resampler = dataset.resample(time=time, **kwargs) + + # Separate variables with and without time dimension + time_vars = {} + non_time_vars = {} + + for var_name, var in dataset.data_vars.items(): + if 'time' in var.dims: + time_vars[var_name] = var + else: + non_time_vars[var_name] = var + + # Only resample variables that have time dimension + time_dataset = dataset[list(time_vars.keys())] + resampler = time_dataset.resample(time=time, **kwargs) if hasattr(resampler, method): - resampled_dataset = getattr(resampler, method)() + resampled_time_data = getattr(resampler, method)() else: available_methods = ['mean', 'sum', 'max', 'min', 'first', 'last', 'std', 'var', 'median', 'count'] raise ValueError(f'Unsupported resampling method: {method}. Available: {available_methods}') + # Combine resampled time variables with non-time variables + if non_time_vars: + non_time_dataset = dataset[list(non_time_vars.keys())] + resampled_dataset = xr.merge([resampled_time_data, non_time_dataset]) + else: + resampled_dataset = resampled_time_data + return self.__class__.from_dataset(resampled_dataset) From 4981a9c87222b6103e8a293a9fb596d6455b3236 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 15 Jul 2025 15:35:30 +0200 Subject: [PATCH 176/448] Improve Warning Message --- flixopt/calculation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flixopt/calculation.py b/flixopt/calculation.py index b3eec2d06..0fb735bef 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -62,7 +62,7 @@ def __init__( self.name = name if flow_system.used_in_calculation: logger.warning( - f'FlowSystem {flow_system} is already used in a calculation. ' + f'This FlowSystem is already used in a calculation:\n{flow_system}\n' f'Creating a copy of the FlowSystem for Calculation "{self.name}".' ) flow_system = flow_system.copy() From 516f45b4b356b0f7db04670707e5506bab4540da Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 15 Jul 2025 15:39:25 +0200 Subject: [PATCH 177/448] Add example that leverages resampling --- .../Zeitreihen2020.csv | 35137 ++++++++++++++++ .../two_stage_optimization.py | 148 + 2 files changed, 35285 insertions(+) create mode 100644 examples/05_Two-stage-optimization/Zeitreihen2020.csv create mode 100644 examples/05_Two-stage-optimization/two_stage_optimization.py diff --git a/examples/05_Two-stage-optimization/Zeitreihen2020.csv b/examples/05_Two-stage-optimization/Zeitreihen2020.csv new file mode 100644 index 000000000..9b660ef9c --- /dev/null +++ b/examples/05_Two-stage-optimization/Zeitreihen2020.csv @@ -0,0 +1,35137 @@ +Zeit,P_Netz/MW,Q_Netz/MW,Strompr.€/MWh,Gaspr.€/MWh +2020-01-01 00:00:00,58.39,127.059,7.461,32.459 +2020-01-01 00:15:00,58.36,122.156,7.461,32.459 +2020-01-01 00:30:00,58.11,124.412,7.461,32.459 +2020-01-01 00:45:00,57.71,127.713,7.461,32.459 +2020-01-01 01:00:00,55.53,130.69899999999998,2.65,32.459 +2020-01-01 01:15:00,56.24,132.166,2.65,32.459 +2020-01-01 01:30:00,55.17,132.394,2.65,32.459 +2020-01-01 01:45:00,54.5,132.431,2.65,32.459 +2020-01-01 02:00:00,52.95,134.403,-2.949,32.459 +2020-01-01 02:15:00,51.75,134.755,-2.949,32.459 +2020-01-01 02:30:00,50.7,135.631,-2.949,32.459 +2020-01-01 02:45:00,50.33,138.345,-2.949,32.459 +2020-01-01 03:00:00,47.11,141.071,-3.2680000000000002,32.459 +2020-01-01 03:15:00,49.35,141.319,-3.2680000000000002,32.459 +2020-01-01 03:30:00,48.33,143.024,-3.2680000000000002,32.459 +2020-01-01 03:45:00,48.73,144.697,-3.2680000000000002,32.459 +2020-01-01 04:00:00,46.6,152.569,-3.2680000000000002,32.459 +2020-01-01 04:15:00,47.33,160.688,-3.2680000000000002,32.459 +2020-01-01 04:30:00,47.5,161.673,-3.2680000000000002,32.459 +2020-01-01 04:45:00,48.62,163.165,-3.2680000000000002,32.459 +2020-01-01 05:00:00,49.18,176.398,-3.2680000000000002,32.459 +2020-01-01 05:15:00,50.0,184.233,-3.2680000000000002,32.459 +2020-01-01 05:30:00,50.1,181.12,-3.2680000000000002,32.459 +2020-01-01 05:45:00,50.36,179.642,-3.2680000000000002,32.459 +2020-01-01 06:00:00,50.32,196.364,-3.2680000000000002,32.459 +2020-01-01 06:15:00,50.22,215.918,-3.2680000000000002,32.459 +2020-01-01 06:30:00,52.17,211.0,-3.2680000000000002,32.459 +2020-01-01 06:45:00,54.2,206.111,-3.2680000000000002,32.459 +2020-01-01 07:00:00,55.6,202.635,-3.2680000000000002,32.459 +2020-01-01 07:15:00,55.35,207.093,-3.2680000000000002,32.459 +2020-01-01 07:30:00,55.44,211.667,-3.2680000000000002,32.459 +2020-01-01 07:45:00,55.15,215.882,-3.2680000000000002,32.459 +2020-01-01 08:00:00,56.21,219.793,-2.146,32.459 +2020-01-01 08:15:00,55.94,223.388,-2.146,32.459 +2020-01-01 08:30:00,58.2,226.081,-2.146,32.459 +2020-01-01 08:45:00,56.11,227.016,-2.146,32.459 +2020-01-01 09:00:00,61.62,222.30700000000002,1.7519999999999998,32.459 +2020-01-01 09:15:00,63.5,221.018,1.7519999999999998,32.459 +2020-01-01 09:30:00,64.24,219.09,1.7519999999999998,32.459 +2020-01-01 09:45:00,64.02,215.80700000000002,1.7519999999999998,32.459 +2020-01-01 10:00:00,61.38,212.231,4.19,32.459 +2020-01-01 10:15:00,62.05,209.50099999999998,4.19,32.459 +2020-01-01 10:30:00,61.84,206.963,4.19,32.459 +2020-01-01 10:45:00,65.9,204.111,4.19,32.459 +2020-01-01 11:00:00,67.33,203.37599999999998,5.517,32.459 +2020-01-01 11:15:00,68.34,200.575,5.517,32.459 +2020-01-01 11:30:00,67.43,198.755,5.517,32.459 +2020-01-01 11:45:00,68.73,197.22099999999998,5.517,32.459 +2020-01-01 12:00:00,74.2,191.555,4.27,32.459 +2020-01-01 12:15:00,69.91,191.37,4.27,32.459 +2020-01-01 12:30:00,68.83,190.28799999999998,4.27,32.459 +2020-01-01 12:45:00,70.01,189.851,4.27,32.459 +2020-01-01 13:00:00,69.02,188.09599999999998,3.484,32.459 +2020-01-01 13:15:00,68.68,189.604,3.484,32.459 +2020-01-01 13:30:00,68.45,188.61,3.484,32.459 +2020-01-01 13:45:00,68.18,188.303,3.484,32.459 +2020-01-01 14:00:00,68.94,187.555,2.523,32.459 +2020-01-01 14:15:00,66.1,188.747,2.523,32.459 +2020-01-01 14:30:00,70.61,189.53099999999998,2.523,32.459 +2020-01-01 14:45:00,70.58,189.93,2.523,32.459 +2020-01-01 15:00:00,73.81,189.581,5.667999999999999,32.459 +2020-01-01 15:15:00,69.6,191.454,5.667999999999999,32.459 +2020-01-01 15:30:00,68.66,194.55599999999998,5.667999999999999,32.459 +2020-01-01 15:45:00,71.32,197.227,5.667999999999999,32.459 +2020-01-01 16:00:00,73.3,197.49900000000002,12.109000000000002,32.459 +2020-01-01 16:15:00,77.24,199.38099999999997,12.109000000000002,32.459 +2020-01-01 16:30:00,83.2,202.511,12.109000000000002,32.459 +2020-01-01 16:45:00,86.91,205.047,12.109000000000002,32.459 +2020-01-01 17:00:00,89.85,207.06099999999998,22.824,32.459 +2020-01-01 17:15:00,90.61,208.69799999999998,22.824,32.459 +2020-01-01 17:30:00,92.03,209.25099999999998,22.824,32.459 +2020-01-01 17:45:00,93.49,210.672,22.824,32.459 +2020-01-01 18:00:00,95.17,211.632,21.656,32.459 +2020-01-01 18:15:00,94.51,211.645,21.656,32.459 +2020-01-01 18:30:00,93.87,209.94,21.656,32.459 +2020-01-01 18:45:00,93.48,208.489,21.656,32.459 +2020-01-01 19:00:00,92.14,209.826,19.749000000000002,32.459 +2020-01-01 19:15:00,90.85,207.487,19.749000000000002,32.459 +2020-01-01 19:30:00,89.81,204.959,19.749000000000002,32.459 +2020-01-01 19:45:00,88.14,202.29,19.749000000000002,32.459 +2020-01-01 20:00:00,84.73,200.835,24.274,32.459 +2020-01-01 20:15:00,83.37,197.709,24.274,32.459 +2020-01-01 20:30:00,82.23,194.57299999999998,24.274,32.459 +2020-01-01 20:45:00,80.34,191.52200000000002,24.274,32.459 +2020-01-01 21:00:00,78.43,188.843,23.044,32.459 +2020-01-01 21:15:00,77.95,186.56099999999998,23.044,32.459 +2020-01-01 21:30:00,77.04,186.183,23.044,32.459 +2020-01-01 21:45:00,76.57,184.824,23.044,32.459 +2020-01-01 22:00:00,72.56,178.928,25.155,32.459 +2020-01-01 22:15:00,72.48,174.81799999999998,25.155,32.459 +2020-01-01 22:30:00,70.04,170.76,25.155,32.459 +2020-01-01 22:45:00,67.68,167.32,25.155,32.459 +2020-01-01 23:00:00,59.3,159.845,20.101,32.459 +2020-01-01 23:15:00,63.51,156.17700000000002,20.101,32.459 +2020-01-01 23:30:00,61.08,153.764,20.101,32.459 +2020-01-01 23:45:00,60.81,150.845,20.101,32.459 +2020-01-02 00:00:00,72.56,131.099,38.399,32.641 +2020-01-02 00:15:00,75.51,130.822,38.399,32.641 +2020-01-02 00:30:00,76.2,132.15,38.399,32.641 +2020-01-02 00:45:00,79.94,133.753,38.399,32.641 +2020-01-02 01:00:00,76.22,136.549,36.94,32.641 +2020-01-02 01:15:00,75.52,136.971,36.94,32.641 +2020-01-02 01:30:00,69.71,137.433,36.94,32.641 +2020-01-02 01:45:00,72.15,137.96200000000002,36.94,32.641 +2020-01-02 02:00:00,68.46,139.933,35.275,32.641 +2020-01-02 02:15:00,68.93,141.73,35.275,32.641 +2020-01-02 02:30:00,69.13,142.344,35.275,32.641 +2020-01-02 02:45:00,71.05,144.38,35.275,32.641 +2020-01-02 03:00:00,77.24,147.17700000000002,35.329,32.641 +2020-01-02 03:15:00,78.88,148.17700000000002,35.329,32.641 +2020-01-02 03:30:00,79.6,150.07,35.329,32.641 +2020-01-02 03:45:00,73.47,151.487,35.329,32.641 +2020-01-02 04:00:00,71.66,163.75,36.275,32.641 +2020-01-02 04:15:00,72.6,175.77599999999998,36.275,32.641 +2020-01-02 04:30:00,74.43,178.856,36.275,32.641 +2020-01-02 04:45:00,76.31,181.783,36.275,32.641 +2020-01-02 05:00:00,80.9,217.042,42.193999999999996,32.641 +2020-01-02 05:15:00,83.91,245.78799999999998,42.193999999999996,32.641 +2020-01-02 05:30:00,88.55,241.4,42.193999999999996,32.641 +2020-01-02 05:45:00,92.98,234.083,42.193999999999996,32.641 +2020-01-02 06:00:00,101.92,230.607,56.422,32.641 +2020-01-02 06:15:00,106.02,236.44099999999997,56.422,32.641 +2020-01-02 06:30:00,110.67,239.31400000000002,56.422,32.641 +2020-01-02 06:45:00,115.36,243.33,56.422,32.641 +2020-01-02 07:00:00,121.21,242.197,72.569,32.641 +2020-01-02 07:15:00,125.44,247.68200000000002,72.569,32.641 +2020-01-02 07:30:00,127.04,250.858,72.569,32.641 +2020-01-02 07:45:00,129.3,252.55900000000003,72.569,32.641 +2020-01-02 08:00:00,131.83,251.364,67.704,32.641 +2020-01-02 08:15:00,129.92,251.618,67.704,32.641 +2020-01-02 08:30:00,130.03,249.708,67.704,32.641 +2020-01-02 08:45:00,129.65,246.764,67.704,32.641 +2020-01-02 09:00:00,130.4,240.053,63.434,32.641 +2020-01-02 09:15:00,132.18,236.87900000000002,63.434,32.641 +2020-01-02 09:30:00,133.68,234.63099999999997,63.434,32.641 +2020-01-02 09:45:00,133.99,231.472,63.434,32.641 +2020-01-02 10:00:00,133.44,226.187,61.88399999999999,32.641 +2020-01-02 10:15:00,135.64,221.955,61.88399999999999,32.641 +2020-01-02 10:30:00,134.92,218.68400000000003,61.88399999999999,32.641 +2020-01-02 10:45:00,136.65,216.98,61.88399999999999,32.641 +2020-01-02 11:00:00,136.01,214.97299999999998,61.481,32.641 +2020-01-02 11:15:00,135.6,213.702,61.481,32.641 +2020-01-02 11:30:00,135.49,212.15200000000002,61.481,32.641 +2020-01-02 11:45:00,136.56,210.945,61.481,32.641 +2020-01-02 12:00:00,136.24,205.8,59.527,32.641 +2020-01-02 12:15:00,135.89,205.143,59.527,32.641 +2020-01-02 12:30:00,133.92,205.12400000000002,59.527,32.641 +2020-01-02 12:45:00,134.1,205.96099999999998,59.527,32.641 +2020-01-02 13:00:00,130.94,204.35,58.794,32.641 +2020-01-02 13:15:00,129.98,203.88099999999997,58.794,32.641 +2020-01-02 13:30:00,129.31,203.575,58.794,32.641 +2020-01-02 13:45:00,127.59,203.519,58.794,32.641 +2020-01-02 14:00:00,126.5,202.428,60.32,32.641 +2020-01-02 14:15:00,129.81,203.06900000000002,60.32,32.641 +2020-01-02 14:30:00,128.98,203.95,60.32,32.641 +2020-01-02 14:45:00,127.66,204.167,60.32,32.641 +2020-01-02 15:00:00,131.69,205.303,62.52,32.641 +2020-01-02 15:15:00,134.08,205.93900000000002,62.52,32.641 +2020-01-02 15:30:00,130.49,208.357,62.52,32.641 +2020-01-02 15:45:00,132.71,210.109,62.52,32.641 +2020-01-02 16:00:00,134.12,210.895,64.199,32.641 +2020-01-02 16:15:00,134.79,212.476,64.199,32.641 +2020-01-02 16:30:00,137.68,215.345,64.199,32.641 +2020-01-02 16:45:00,138.3,216.87599999999998,64.199,32.641 +2020-01-02 17:00:00,141.23,219.28900000000002,68.19800000000001,32.641 +2020-01-02 17:15:00,140.55,219.918,68.19800000000001,32.641 +2020-01-02 17:30:00,141.63,220.68200000000002,68.19800000000001,32.641 +2020-01-02 17:45:00,141.55,220.424,68.19800000000001,32.641 +2020-01-02 18:00:00,140.02,221.84400000000002,67.899,32.641 +2020-01-02 18:15:00,138.28,218.955,67.899,32.641 +2020-01-02 18:30:00,137.19,217.683,67.899,32.641 +2020-01-02 18:45:00,137.6,217.707,67.899,32.641 +2020-01-02 19:00:00,134.26,217.453,64.72399999999999,32.641 +2020-01-02 19:15:00,133.22,213.481,64.72399999999999,32.641 +2020-01-02 19:30:00,134.64,210.797,64.72399999999999,32.641 +2020-01-02 19:45:00,136.88,207.28599999999997,64.72399999999999,32.641 +2020-01-02 20:00:00,130.24,203.518,64.062,32.641 +2020-01-02 20:15:00,119.33,197.062,64.062,32.641 +2020-01-02 20:30:00,118.31,192.979,64.062,32.641 +2020-01-02 20:45:00,112.7,191.03099999999998,64.062,32.641 +2020-01-02 21:00:00,107.44,188.12900000000002,57.971000000000004,32.641 +2020-01-02 21:15:00,112.86,185.61900000000003,57.971000000000004,32.641 +2020-01-02 21:30:00,111.93,183.535,57.971000000000004,32.641 +2020-01-02 21:45:00,108.29,181.894,57.971000000000004,32.641 +2020-01-02 22:00:00,98.52,174.86,53.715,32.641 +2020-01-02 22:15:00,94.85,168.98,53.715,32.641 +2020-01-02 22:30:00,98.1,155.042,53.715,32.641 +2020-01-02 22:45:00,96.62,146.733,53.715,32.641 +2020-01-02 23:00:00,92.22,140.148,47.8,32.641 +2020-01-02 23:15:00,84.12,138.344,47.8,32.641 +2020-01-02 23:30:00,79.31,138.518,47.8,32.641 +2020-01-02 23:45:00,83.54,137.971,47.8,32.641 +2020-01-03 00:00:00,83.23,130.233,43.656000000000006,32.641 +2020-01-03 00:15:00,82.94,130.13299999999998,43.656000000000006,32.641 +2020-01-03 00:30:00,78.25,131.279,43.656000000000006,32.641 +2020-01-03 00:45:00,76.9,132.954,43.656000000000006,32.641 +2020-01-03 01:00:00,71.11,135.453,41.263000000000005,32.641 +2020-01-03 01:15:00,78.36,136.91899999999998,41.263000000000005,32.641 +2020-01-03 01:30:00,78.99,137.079,41.263000000000005,32.641 +2020-01-03 01:45:00,77.55,137.732,41.263000000000005,32.641 +2020-01-03 02:00:00,71.19,139.738,40.799,32.641 +2020-01-03 02:15:00,72.17,141.41299999999998,40.799,32.641 +2020-01-03 02:30:00,74.79,142.542,40.799,32.641 +2020-01-03 02:45:00,78.06,144.681,40.799,32.641 +2020-01-03 03:00:00,76.51,146.311,41.398,32.641 +2020-01-03 03:15:00,74.89,148.47,41.398,32.641 +2020-01-03 03:30:00,79.0,150.364,41.398,32.641 +2020-01-03 03:45:00,77.85,152.072,41.398,32.641 +2020-01-03 04:00:00,73.23,164.549,42.38,32.641 +2020-01-03 04:15:00,74.31,176.43200000000002,42.38,32.641 +2020-01-03 04:30:00,74.97,179.67700000000002,42.38,32.641 +2020-01-03 04:45:00,78.19,181.39700000000002,42.38,32.641 +2020-01-03 05:00:00,82.49,215.25400000000002,46.181000000000004,32.641 +2020-01-03 05:15:00,85.31,245.535,46.181000000000004,32.641 +2020-01-03 05:30:00,88.01,242.33599999999998,46.181000000000004,32.641 +2020-01-03 05:45:00,93.72,235.011,46.181000000000004,32.641 +2020-01-03 06:00:00,101.82,232.016,59.33,32.641 +2020-01-03 06:15:00,106.19,236.21900000000002,59.33,32.641 +2020-01-03 06:30:00,110.85,238.166,59.33,32.641 +2020-01-03 06:45:00,115.45,244.024,59.33,32.641 +2020-01-03 07:00:00,122.71,241.924,72.454,32.641 +2020-01-03 07:15:00,123.17,248.451,72.454,32.641 +2020-01-03 07:30:00,125.55,251.585,72.454,32.641 +2020-01-03 07:45:00,128.9,252.31099999999998,72.454,32.641 +2020-01-03 08:00:00,130.12,249.83599999999998,67.175,32.641 +2020-01-03 08:15:00,128.89,249.597,67.175,32.641 +2020-01-03 08:30:00,128.21,248.743,67.175,32.641 +2020-01-03 08:45:00,128.6,244.05900000000003,67.175,32.641 +2020-01-03 09:00:00,128.71,238.011,65.365,32.641 +2020-01-03 09:15:00,131.57,235.324,65.365,32.641 +2020-01-03 09:30:00,133.77,232.672,65.365,32.641 +2020-01-03 09:45:00,136.47,229.359,65.365,32.641 +2020-01-03 10:00:00,135.77,222.843,63.95,32.641 +2020-01-03 10:15:00,137.43,219.39,63.95,32.641 +2020-01-03 10:30:00,136.81,215.986,63.95,32.641 +2020-01-03 10:45:00,137.07,213.808,63.95,32.641 +2020-01-03 11:00:00,136.69,211.75,63.92100000000001,32.641 +2020-01-03 11:15:00,137.82,209.576,63.92100000000001,32.641 +2020-01-03 11:30:00,137.25,209.956,63.92100000000001,32.641 +2020-01-03 11:45:00,136.75,208.86900000000003,63.92100000000001,32.641 +2020-01-03 12:00:00,135.72,204.88400000000001,60.79600000000001,32.641 +2020-01-03 12:15:00,135.16,202.011,60.79600000000001,32.641 +2020-01-03 12:30:00,133.9,202.153,60.79600000000001,32.641 +2020-01-03 12:45:00,134.58,203.607,60.79600000000001,32.641 +2020-01-03 13:00:00,130.87,202.96200000000002,59.393,32.641 +2020-01-03 13:15:00,131.78,203.36,59.393,32.641 +2020-01-03 13:30:00,130.35,203.00799999999998,59.393,32.641 +2020-01-03 13:45:00,131.11,202.855,59.393,32.641 +2020-01-03 14:00:00,130.67,200.58700000000002,57.943999999999996,32.641 +2020-01-03 14:15:00,130.14,201.0,57.943999999999996,32.641 +2020-01-03 14:30:00,129.02,202.34799999999998,57.943999999999996,32.641 +2020-01-03 14:45:00,129.81,202.954,57.943999999999996,32.641 +2020-01-03 15:00:00,130.32,203.59099999999998,60.153999999999996,32.641 +2020-01-03 15:15:00,129.09,203.75099999999998,60.153999999999996,32.641 +2020-01-03 15:30:00,128.33,204.56099999999998,60.153999999999996,32.641 +2020-01-03 15:45:00,128.17,206.41299999999998,60.153999999999996,32.641 +2020-01-03 16:00:00,131.49,205.998,62.933,32.641 +2020-01-03 16:15:00,133.11,207.87400000000002,62.933,32.641 +2020-01-03 16:30:00,134.7,210.864,62.933,32.641 +2020-01-03 16:45:00,133.81,212.332,62.933,32.641 +2020-01-03 17:00:00,137.76,214.84900000000002,68.657,32.641 +2020-01-03 17:15:00,136.16,215.076,68.657,32.641 +2020-01-03 17:30:00,140.54,215.513,68.657,32.641 +2020-01-03 17:45:00,137.57,215.02900000000002,68.657,32.641 +2020-01-03 18:00:00,136.81,217.225,67.111,32.641 +2020-01-03 18:15:00,136.99,213.96900000000002,67.111,32.641 +2020-01-03 18:30:00,135.65,213.13,67.111,32.641 +2020-01-03 18:45:00,136.2,213.142,67.111,32.641 +2020-01-03 19:00:00,132.87,213.791,62.434,32.641 +2020-01-03 19:15:00,131.87,211.248,62.434,32.641 +2020-01-03 19:30:00,132.37,208.11900000000003,62.434,32.641 +2020-01-03 19:45:00,134.77,204.162,62.434,32.641 +2020-01-03 20:00:00,128.17,200.44400000000002,61.763000000000005,32.641 +2020-01-03 20:15:00,120.91,193.95,61.763000000000005,32.641 +2020-01-03 20:30:00,117.84,189.834,61.763000000000005,32.641 +2020-01-03 20:45:00,113.28,188.551,61.763000000000005,32.641 +2020-01-03 21:00:00,110.73,186.10299999999998,56.785,32.641 +2020-01-03 21:15:00,113.2,183.95,56.785,32.641 +2020-01-03 21:30:00,110.05,181.924,56.785,32.641 +2020-01-03 21:45:00,104.45,180.86900000000003,56.785,32.641 +2020-01-03 22:00:00,98.56,174.898,52.693000000000005,32.641 +2020-01-03 22:15:00,102.19,168.90400000000002,52.693000000000005,32.641 +2020-01-03 22:30:00,100.03,161.57,52.693000000000005,32.641 +2020-01-03 22:45:00,96.9,157.013,52.693000000000005,32.641 +2020-01-03 23:00:00,89.83,149.83100000000002,45.443999999999996,32.641 +2020-01-03 23:15:00,91.91,146.02700000000002,45.443999999999996,32.641 +2020-01-03 23:30:00,88.18,144.751,45.443999999999996,32.641 +2020-01-03 23:45:00,85.54,143.495,45.443999999999996,32.641 +2020-01-04 00:00:00,82.22,127.199,44.738,32.459 +2020-01-04 00:15:00,83.18,122.48899999999999,44.738,32.459 +2020-01-04 00:30:00,81.48,125.102,44.738,32.459 +2020-01-04 00:45:00,73.34,127.589,44.738,32.459 +2020-01-04 01:00:00,72.21,130.76,40.303000000000004,32.459 +2020-01-04 01:15:00,77.36,131.101,40.303000000000004,32.459 +2020-01-04 01:30:00,78.27,130.754,40.303000000000004,32.459 +2020-01-04 01:45:00,75.98,131.06799999999998,40.303000000000004,32.459 +2020-01-04 02:00:00,69.25,133.903,38.61,32.459 +2020-01-04 02:15:00,74.64,135.233,38.61,32.459 +2020-01-04 02:30:00,74.85,135.217,38.61,32.459 +2020-01-04 02:45:00,73.61,137.407,38.61,32.459 +2020-01-04 03:00:00,68.1,139.812,37.554,32.459 +2020-01-04 03:15:00,73.13,140.717,37.554,32.459 +2020-01-04 03:30:00,75.4,140.84799999999998,37.554,32.459 +2020-01-04 03:45:00,70.56,142.566,37.554,32.459 +2020-01-04 04:00:00,67.07,150.611,37.176,32.459 +2020-01-04 04:15:00,67.97,159.77100000000002,37.176,32.459 +2020-01-04 04:30:00,67.71,160.769,37.176,32.459 +2020-01-04 04:45:00,68.5,161.921,37.176,32.459 +2020-01-04 05:00:00,67.87,178.856,36.893,32.459 +2020-01-04 05:15:00,67.75,189.139,36.893,32.459 +2020-01-04 05:30:00,66.1,186.215,36.893,32.459 +2020-01-04 05:45:00,67.33,184.52,36.893,32.459 +2020-01-04 06:00:00,69.91,201.26,37.803000000000004,32.459 +2020-01-04 06:15:00,69.0,222.77200000000002,37.803000000000004,32.459 +2020-01-04 06:30:00,69.5,219.078,37.803000000000004,32.459 +2020-01-04 06:45:00,71.88,215.377,37.803000000000004,32.459 +2020-01-04 07:00:00,76.68,209.287,41.086999999999996,32.459 +2020-01-04 07:15:00,77.71,214.53599999999997,41.086999999999996,32.459 +2020-01-04 07:30:00,80.15,220.502,41.086999999999996,32.459 +2020-01-04 07:45:00,83.14,225.497,41.086999999999996,32.459 +2020-01-04 08:00:00,85.43,227.446,48.222,32.459 +2020-01-04 08:15:00,85.69,231.175,48.222,32.459 +2020-01-04 08:30:00,89.46,232.06099999999998,48.222,32.459 +2020-01-04 08:45:00,91.91,230.729,48.222,32.459 +2020-01-04 09:00:00,92.65,226.387,52.791000000000004,32.459 +2020-01-04 09:15:00,93.59,224.49200000000002,52.791000000000004,32.459 +2020-01-04 09:30:00,94.68,222.80200000000002,52.791000000000004,32.459 +2020-01-04 09:45:00,96.53,219.69,52.791000000000004,32.459 +2020-01-04 10:00:00,98.05,213.424,54.341,32.459 +2020-01-04 10:15:00,95.0,210.122,54.341,32.459 +2020-01-04 10:30:00,94.99,206.91099999999997,54.341,32.459 +2020-01-04 10:45:00,92.74,206.176,54.341,32.459 +2020-01-04 11:00:00,96.61,204.363,51.94,32.459 +2020-01-04 11:15:00,100.28,201.382,51.94,32.459 +2020-01-04 11:30:00,99.55,200.535,51.94,32.459 +2020-01-04 11:45:00,98.58,198.36700000000002,51.94,32.459 +2020-01-04 12:00:00,96.09,193.4,50.973,32.459 +2020-01-04 12:15:00,98.67,191.18099999999998,50.973,32.459 +2020-01-04 12:30:00,96.52,191.675,50.973,32.459 +2020-01-04 12:45:00,95.72,192.248,50.973,32.459 +2020-01-04 13:00:00,93.57,191.208,48.06399999999999,32.459 +2020-01-04 13:15:00,92.81,189.408,48.06399999999999,32.459 +2020-01-04 13:30:00,91.55,188.543,48.06399999999999,32.459 +2020-01-04 13:45:00,89.25,188.99,48.06399999999999,32.459 +2020-01-04 14:00:00,89.5,188.077,45.707,32.459 +2020-01-04 14:15:00,89.52,187.99,45.707,32.459 +2020-01-04 14:30:00,90.0,187.408,45.707,32.459 +2020-01-04 14:45:00,91.09,188.234,45.707,32.459 +2020-01-04 15:00:00,91.5,189.60299999999998,47.567,32.459 +2020-01-04 15:15:00,91.94,190.57,47.567,32.459 +2020-01-04 15:30:00,92.53,193.02900000000002,47.567,32.459 +2020-01-04 15:45:00,94.28,194.947,47.567,32.459 +2020-01-04 16:00:00,96.74,193.149,52.031000000000006,32.459 +2020-01-04 16:15:00,95.42,196.053,52.031000000000006,32.459 +2020-01-04 16:30:00,100.94,198.982,52.031000000000006,32.459 +2020-01-04 16:45:00,102.46,201.418,52.031000000000006,32.459 +2020-01-04 17:00:00,105.49,203.416,58.218999999999994,32.459 +2020-01-04 17:15:00,104.81,205.575,58.218999999999994,32.459 +2020-01-04 17:30:00,107.13,205.94299999999998,58.218999999999994,32.459 +2020-01-04 17:45:00,108.26,205.013,58.218999999999994,32.459 +2020-01-04 18:00:00,110.16,206.662,57.65,32.459 +2020-01-04 18:15:00,109.91,205.239,57.65,32.459 +2020-01-04 18:30:00,109.66,205.75799999999998,57.65,32.459 +2020-01-04 18:45:00,109.05,202.394,57.65,32.459 +2020-01-04 19:00:00,107.78,204.105,51.261,32.459 +2020-01-04 19:15:00,103.06,201.082,51.261,32.459 +2020-01-04 19:30:00,101.89,198.71599999999998,51.261,32.459 +2020-01-04 19:45:00,102.31,194.47799999999998,51.261,32.459 +2020-01-04 20:00:00,94.91,193.0,44.068000000000005,32.459 +2020-01-04 20:15:00,93.02,188.81400000000002,44.068000000000005,32.459 +2020-01-04 20:30:00,88.29,184.359,44.068000000000005,32.459 +2020-01-04 20:45:00,87.9,182.563,44.068000000000005,32.459 +2020-01-04 21:00:00,84.51,182.58599999999998,38.861,32.459 +2020-01-04 21:15:00,83.28,180.90400000000002,38.861,32.459 +2020-01-04 21:30:00,82.28,180.18200000000002,38.861,32.459 +2020-01-04 21:45:00,81.25,178.737,38.861,32.459 +2020-01-04 22:00:00,77.7,174.207,39.485,32.459 +2020-01-04 22:15:00,77.25,170.87,39.485,32.459 +2020-01-04 22:30:00,74.24,170.22299999999998,39.485,32.459 +2020-01-04 22:45:00,73.18,167.65200000000002,39.485,32.459 +2020-01-04 23:00:00,68.36,163.07399999999998,32.027,32.459 +2020-01-04 23:15:00,68.86,157.509,32.027,32.459 +2020-01-04 23:30:00,66.13,154.256,32.027,32.459 +2020-01-04 23:45:00,65.23,150.387,32.027,32.459 +2020-01-05 00:00:00,61.83,127.708,26.96,32.459 +2020-01-05 00:15:00,59.65,122.706,26.96,32.459 +2020-01-05 00:30:00,56.73,124.90899999999999,26.96,32.459 +2020-01-05 00:45:00,58.04,128.141,26.96,32.459 +2020-01-05 01:00:00,54.64,131.15200000000002,24.295,32.459 +2020-01-05 01:15:00,53.12,132.619,24.295,32.459 +2020-01-05 01:30:00,54.89,132.84799999999998,24.295,32.459 +2020-01-05 01:45:00,55.39,132.83700000000002,24.295,32.459 +2020-01-05 02:00:00,52.81,134.87,24.268,32.459 +2020-01-05 02:15:00,53.85,135.224,24.268,32.459 +2020-01-05 02:30:00,53.36,136.131,24.268,32.459 +2020-01-05 02:45:00,53.22,138.842,24.268,32.459 +2020-01-05 03:00:00,51.24,141.54,23.373,32.459 +2020-01-05 03:15:00,52.77,141.878,23.373,32.459 +2020-01-05 03:30:00,53.33,143.584,23.373,32.459 +2020-01-05 03:45:00,52.95,145.284,23.373,32.459 +2020-01-05 04:00:00,53.02,153.043,23.874000000000002,32.459 +2020-01-05 04:15:00,54.36,161.123,23.874000000000002,32.459 +2020-01-05 04:30:00,55.19,162.089,23.874000000000002,32.459 +2020-01-05 04:45:00,54.83,163.567,23.874000000000002,32.459 +2020-01-05 05:00:00,56.2,176.618,24.871,32.459 +2020-01-05 05:15:00,57.61,184.28099999999998,24.871,32.459 +2020-01-05 05:30:00,58.05,181.218,24.871,32.459 +2020-01-05 05:45:00,58.68,179.833,24.871,32.459 +2020-01-05 06:00:00,58.97,196.65900000000002,23.84,32.459 +2020-01-05 06:15:00,60.37,216.24099999999999,23.84,32.459 +2020-01-05 06:30:00,60.21,211.415,23.84,32.459 +2020-01-05 06:45:00,60.83,206.671,23.84,32.459 +2020-01-05 07:00:00,63.06,203.24599999999998,27.430999999999997,32.459 +2020-01-05 07:15:00,64.35,207.676,27.430999999999997,32.459 +2020-01-05 07:30:00,66.38,212.19400000000002,27.430999999999997,32.459 +2020-01-05 07:45:00,68.85,216.345,27.430999999999997,32.459 +2020-01-05 08:00:00,71.87,220.25099999999998,33.891999999999996,32.459 +2020-01-05 08:15:00,72.96,223.791,33.891999999999996,32.459 +2020-01-05 08:30:00,75.42,226.391,33.891999999999996,32.459 +2020-01-05 08:45:00,77.7,227.232,33.891999999999996,32.459 +2020-01-05 09:00:00,79.34,222.44799999999998,37.571,32.459 +2020-01-05 09:15:00,79.42,221.187,37.571,32.459 +2020-01-05 09:30:00,80.81,219.317,37.571,32.459 +2020-01-05 09:45:00,82.3,216.00799999999998,37.571,32.459 +2020-01-05 10:00:00,83.72,212.428,40.594,32.459 +2020-01-05 10:15:00,84.02,209.692,40.594,32.459 +2020-01-05 10:30:00,86.92,207.107,40.594,32.459 +2020-01-05 10:45:00,89.06,204.26,40.594,32.459 +2020-01-05 11:00:00,91.79,203.445,44.133,32.459 +2020-01-05 11:15:00,98.14,200.63099999999997,44.133,32.459 +2020-01-05 11:30:00,99.24,198.81799999999998,44.133,32.459 +2020-01-05 11:45:00,100.24,197.28900000000002,44.133,32.459 +2020-01-05 12:00:00,99.8,191.658,41.198,32.459 +2020-01-05 12:15:00,96.2,191.544,41.198,32.459 +2020-01-05 12:30:00,92.34,190.454,41.198,32.459 +2020-01-05 12:45:00,91.8,190.02599999999998,41.198,32.459 +2020-01-05 13:00:00,86.98,188.218,37.014,32.459 +2020-01-05 13:15:00,86.22,189.688,37.014,32.459 +2020-01-05 13:30:00,85.32,188.65200000000002,37.014,32.459 +2020-01-05 13:45:00,85.43,188.31799999999998,37.014,32.459 +2020-01-05 14:00:00,83.74,187.613,34.934,32.459 +2020-01-05 14:15:00,83.86,188.78400000000002,34.934,32.459 +2020-01-05 14:30:00,84.5,189.609,34.934,32.459 +2020-01-05 14:45:00,84.63,190.06799999999998,34.934,32.459 +2020-01-05 15:00:00,85.54,189.8,34.588,32.459 +2020-01-05 15:15:00,85.23,191.607,34.588,32.459 +2020-01-05 15:30:00,84.85,194.704,34.588,32.459 +2020-01-05 15:45:00,85.8,197.34099999999998,34.588,32.459 +2020-01-05 16:00:00,87.47,197.61700000000002,37.874,32.459 +2020-01-05 16:15:00,90.49,199.547,37.874,32.459 +2020-01-05 16:30:00,92.21,202.707,37.874,32.459 +2020-01-05 16:45:00,93.29,205.295,37.874,32.459 +2020-01-05 17:00:00,98.04,207.237,47.303999999999995,32.459 +2020-01-05 17:15:00,97.7,208.99900000000002,47.303999999999995,32.459 +2020-01-05 17:30:00,99.31,209.662,47.303999999999995,32.459 +2020-01-05 17:45:00,100.34,211.15,47.303999999999995,32.459 +2020-01-05 18:00:00,102.25,212.196,48.879,32.459 +2020-01-05 18:15:00,100.54,212.206,48.879,32.459 +2020-01-05 18:30:00,100.22,210.523,48.879,32.459 +2020-01-05 18:45:00,99.47,209.142,48.879,32.459 +2020-01-05 19:00:00,99.14,210.358,44.826,32.459 +2020-01-05 19:15:00,98.01,208.015,44.826,32.459 +2020-01-05 19:30:00,96.07,205.486,44.826,32.459 +2020-01-05 19:45:00,94.14,202.801,44.826,32.459 +2020-01-05 20:00:00,93.2,201.287,40.154,32.459 +2020-01-05 20:15:00,91.82,198.157,40.154,32.459 +2020-01-05 20:30:00,90.16,194.97099999999998,40.154,32.459 +2020-01-05 20:45:00,88.47,191.995,40.154,32.459 +2020-01-05 21:00:00,84.27,189.24099999999999,36.549,32.459 +2020-01-05 21:15:00,84.17,186.889,36.549,32.459 +2020-01-05 21:30:00,84.43,186.51,36.549,32.459 +2020-01-05 21:45:00,85.26,185.207,36.549,32.459 +2020-01-05 22:00:00,83.7,179.30900000000003,37.663000000000004,32.459 +2020-01-05 22:15:00,82.23,175.25599999999997,37.663000000000004,32.459 +2020-01-05 22:30:00,80.43,171.27900000000002,37.663000000000004,32.459 +2020-01-05 22:45:00,79.58,167.87099999999998,37.663000000000004,32.459 +2020-01-05 23:00:00,75.67,160.305,31.945,32.459 +2020-01-05 23:15:00,77.15,156.664,31.945,32.459 +2020-01-05 23:30:00,74.59,154.317,31.945,32.459 +2020-01-05 23:45:00,73.41,151.388,31.945,32.459 +2020-01-06 00:00:00,70.86,132.295,31.533,32.641 +2020-01-06 00:15:00,69.7,130.435,31.533,32.641 +2020-01-06 00:30:00,69.11,132.78799999999998,31.533,32.641 +2020-01-06 00:45:00,68.33,135.447,31.533,32.641 +2020-01-06 01:00:00,67.32,138.469,30.56,32.641 +2020-01-06 01:15:00,67.69,139.35399999999998,30.56,32.641 +2020-01-06 01:30:00,67.44,139.616,30.56,32.641 +2020-01-06 01:45:00,67.1,139.725,30.56,32.641 +2020-01-06 02:00:00,67.52,141.722,29.55,32.641 +2020-01-06 02:15:00,67.29,143.738,29.55,32.641 +2020-01-06 02:30:00,67.05,145.003,29.55,32.641 +2020-01-06 02:45:00,67.2,147.05200000000002,29.55,32.641 +2020-01-06 03:00:00,68.35,151.1,27.059,32.641 +2020-01-06 03:15:00,68.78,153.201,27.059,32.641 +2020-01-06 03:30:00,69.82,154.562,27.059,32.641 +2020-01-06 03:45:00,69.92,155.701,27.059,32.641 +2020-01-06 04:00:00,67.73,167.963,28.384,32.641 +2020-01-06 04:15:00,71.53,180.334,28.384,32.641 +2020-01-06 04:30:00,73.1,183.72799999999998,28.384,32.641 +2020-01-06 04:45:00,76.51,185.351,28.384,32.641 +2020-01-06 05:00:00,81.52,215.055,35.915,32.641 +2020-01-06 05:15:00,84.58,243.92,35.915,32.641 +2020-01-06 05:30:00,89.42,241.291,35.915,32.641 +2020-01-06 05:45:00,94.89,233.987,35.915,32.641 +2020-01-06 06:00:00,103.99,232.019,56.18,32.641 +2020-01-06 06:15:00,110.77,236.107,56.18,32.641 +2020-01-06 06:30:00,116.07,239.75,56.18,32.641 +2020-01-06 06:45:00,119.86,244.24,56.18,32.641 +2020-01-06 07:00:00,126.05,243.37599999999998,70.877,32.641 +2020-01-06 07:15:00,129.26,248.982,70.877,32.641 +2020-01-06 07:30:00,131.09,252.704,70.877,32.641 +2020-01-06 07:45:00,132.63,254.012,70.877,32.641 +2020-01-06 08:00:00,136.79,252.68599999999998,65.65,32.641 +2020-01-06 08:15:00,137.86,253.967,65.65,32.641 +2020-01-06 08:30:00,137.45,252.168,65.65,32.641 +2020-01-06 08:45:00,136.78,249.345,65.65,32.641 +2020-01-06 09:00:00,137.29,243.503,62.037,32.641 +2020-01-06 09:15:00,138.96,238.595,62.037,32.641 +2020-01-06 09:30:00,139.49,235.72099999999998,62.037,32.641 +2020-01-06 09:45:00,139.62,232.888,62.037,32.641 +2020-01-06 10:00:00,140.37,228.12599999999998,60.409,32.641 +2020-01-06 10:15:00,140.63,225.046,60.409,32.641 +2020-01-06 10:30:00,138.89,221.533,60.409,32.641 +2020-01-06 10:45:00,138.54,219.613,60.409,32.641 +2020-01-06 11:00:00,137.59,215.89700000000002,60.211999999999996,32.641 +2020-01-06 11:15:00,138.33,215.011,60.211999999999996,32.641 +2020-01-06 11:30:00,139.52,214.644,60.211999999999996,32.641 +2020-01-06 11:45:00,136.44,212.643,60.211999999999996,32.641 +2020-01-06 12:00:00,137.83,209.015,57.733000000000004,32.641 +2020-01-06 12:15:00,134.41,208.912,57.733000000000004,32.641 +2020-01-06 12:30:00,133.37,208.162,57.733000000000004,32.641 +2020-01-06 12:45:00,131.24,209.385,57.733000000000004,32.641 +2020-01-06 13:00:00,130.49,208.141,58.695,32.641 +2020-01-06 13:15:00,127.09,208.16099999999997,58.695,32.641 +2020-01-06 13:30:00,123.67,206.52900000000002,58.695,32.641 +2020-01-06 13:45:00,127.05,206.16299999999998,58.695,32.641 +2020-01-06 14:00:00,129.68,204.84099999999998,59.505,32.641 +2020-01-06 14:15:00,129.73,205.275,59.505,32.641 +2020-01-06 14:30:00,130.81,205.53400000000002,59.505,32.641 +2020-01-06 14:45:00,131.3,205.83599999999998,59.505,32.641 +2020-01-06 15:00:00,131.31,207.50599999999997,59.946000000000005,32.641 +2020-01-06 15:15:00,131.43,207.78400000000002,59.946000000000005,32.641 +2020-01-06 15:30:00,130.17,209.935,59.946000000000005,32.641 +2020-01-06 15:45:00,131.03,212.113,59.946000000000005,32.641 +2020-01-06 16:00:00,134.41,212.476,61.766999999999996,32.641 +2020-01-06 16:15:00,134.55,213.59400000000002,61.766999999999996,32.641 +2020-01-06 16:30:00,140.59,215.763,61.766999999999996,32.641 +2020-01-06 16:45:00,140.87,217.083,61.766999999999996,32.641 +2020-01-06 17:00:00,140.83,218.85299999999998,67.85600000000001,32.641 +2020-01-06 17:15:00,140.84,219.60299999999998,67.85600000000001,32.641 +2020-01-06 17:30:00,142.16,219.729,67.85600000000001,32.641 +2020-01-06 17:45:00,140.41,219.657,67.85600000000001,32.641 +2020-01-06 18:00:00,137.78,221.21599999999998,64.564,32.641 +2020-01-06 18:15:00,136.67,218.96900000000002,64.564,32.641 +2020-01-06 18:30:00,133.62,218.032,64.564,32.641 +2020-01-06 18:45:00,134.52,217.257,64.564,32.641 +2020-01-06 19:00:00,131.97,216.72299999999998,58.536,32.641 +2020-01-06 19:15:00,129.61,213.06,58.536,32.641 +2020-01-06 19:30:00,131.14,211.083,58.536,32.641 +2020-01-06 19:45:00,133.72,207.54,58.536,32.641 +2020-01-06 20:00:00,128.55,203.563,59.888999999999996,32.641 +2020-01-06 20:15:00,117.39,197.7,59.888999999999996,32.641 +2020-01-06 20:30:00,115.28,192.479,59.888999999999996,32.641 +2020-01-06 20:45:00,111.64,191.25099999999998,59.888999999999996,32.641 +2020-01-06 21:00:00,108.63,189.085,52.652,32.641 +2020-01-06 21:15:00,110.16,185.43099999999998,52.652,32.641 +2020-01-06 21:30:00,111.85,184.145,52.652,32.641 +2020-01-06 21:45:00,110.02,182.327,52.652,32.641 +2020-01-06 22:00:00,103.97,173.459,46.17,32.641 +2020-01-06 22:15:00,101.93,167.895,46.17,32.641 +2020-01-06 22:30:00,100.68,153.954,46.17,32.641 +2020-01-06 22:45:00,97.59,145.379,46.17,32.641 +2020-01-06 23:00:00,90.0,138.627,36.281,32.641 +2020-01-06 23:15:00,87.29,137.92700000000002,36.281,32.641 +2020-01-06 23:30:00,83.89,138.554,36.281,32.641 +2020-01-06 23:45:00,88.74,138.484,36.281,32.641 +2020-01-07 00:00:00,86.26,131.85,38.821999999999996,32.641 +2020-01-07 00:15:00,81.91,131.452,38.821999999999996,32.641 +2020-01-07 00:30:00,81.41,132.71200000000002,38.821999999999996,32.641 +2020-01-07 00:45:00,83.42,134.23,38.821999999999996,32.641 +2020-01-07 01:00:00,82.73,137.08700000000002,36.936,32.641 +2020-01-07 01:15:00,80.08,137.469,36.936,32.641 +2020-01-07 01:30:00,78.9,137.929,36.936,32.641 +2020-01-07 01:45:00,81.69,138.401,36.936,32.641 +2020-01-07 02:00:00,81.57,140.446,34.42,32.641 +2020-01-07 02:15:00,80.71,142.245,34.42,32.641 +2020-01-07 02:30:00,78.77,142.898,34.42,32.641 +2020-01-07 02:45:00,81.99,144.93200000000002,34.42,32.641 +2020-01-07 03:00:00,82.0,147.695,33.585,32.641 +2020-01-07 03:15:00,79.21,148.804,33.585,32.641 +2020-01-07 03:30:00,82.09,150.697,33.585,32.641 +2020-01-07 03:45:00,82.69,152.15,33.585,32.641 +2020-01-07 04:00:00,77.46,164.275,35.622,32.641 +2020-01-07 04:15:00,77.37,176.252,35.622,32.641 +2020-01-07 04:30:00,77.18,179.312,35.622,32.641 +2020-01-07 04:45:00,80.05,182.22,35.622,32.641 +2020-01-07 05:00:00,84.68,217.25599999999997,40.599000000000004,32.641 +2020-01-07 05:15:00,88.21,245.796,40.599000000000004,32.641 +2020-01-07 05:30:00,91.8,241.465,40.599000000000004,32.641 +2020-01-07 05:45:00,96.21,234.25900000000001,40.599000000000004,32.641 +2020-01-07 06:00:00,104.74,230.912,55.203,32.641 +2020-01-07 06:15:00,111.62,236.783,55.203,32.641 +2020-01-07 06:30:00,115.53,239.761,55.203,32.641 +2020-01-07 06:45:00,119.58,243.951,55.203,32.641 +2020-01-07 07:00:00,121.42,242.885,69.029,32.641 +2020-01-07 07:15:00,129.39,248.329,69.029,32.641 +2020-01-07 07:30:00,132.48,251.43200000000002,69.029,32.641 +2020-01-07 07:45:00,134.56,253.047,69.029,32.641 +2020-01-07 08:00:00,136.12,251.842,65.85300000000001,32.641 +2020-01-07 08:15:00,135.08,252.024,65.85300000000001,32.641 +2020-01-07 08:30:00,135.3,249.989,65.85300000000001,32.641 +2020-01-07 08:45:00,134.8,246.93400000000003,65.85300000000001,32.641 +2020-01-07 09:00:00,135.59,240.132,61.566,32.641 +2020-01-07 09:15:00,137.31,236.993,61.566,32.641 +2020-01-07 09:30:00,139.13,234.81900000000002,61.566,32.641 +2020-01-07 09:45:00,138.88,231.628,61.566,32.641 +2020-01-07 10:00:00,138.47,226.34,61.244,32.641 +2020-01-07 10:15:00,136.8,222.109,61.244,32.641 +2020-01-07 10:30:00,135.87,218.78,61.244,32.641 +2020-01-07 10:45:00,136.63,217.085,61.244,32.641 +2020-01-07 11:00:00,136.36,214.982,61.16,32.641 +2020-01-07 11:15:00,139.62,213.697,61.16,32.641 +2020-01-07 11:30:00,138.15,212.15400000000002,61.16,32.641 +2020-01-07 11:45:00,134.33,210.957,61.16,32.641 +2020-01-07 12:00:00,132.11,205.859,59.09,32.641 +2020-01-07 12:15:00,133.48,205.292,59.09,32.641 +2020-01-07 12:30:00,132.6,205.255,59.09,32.641 +2020-01-07 12:45:00,132.01,206.10299999999998,59.09,32.641 +2020-01-07 13:00:00,132.8,204.433,60.21,32.641 +2020-01-07 13:15:00,132.97,203.91299999999998,60.21,32.641 +2020-01-07 13:30:00,131.24,203.553,60.21,32.641 +2020-01-07 13:45:00,130.77,203.46599999999998,60.21,32.641 +2020-01-07 14:00:00,129.75,202.437,60.673,32.641 +2020-01-07 14:15:00,129.53,203.049,60.673,32.641 +2020-01-07 14:30:00,124.81,203.976,60.673,32.641 +2020-01-07 14:45:00,126.69,204.27,60.673,32.641 +2020-01-07 15:00:00,127.02,205.505,62.232,32.641 +2020-01-07 15:15:00,126.96,206.055,62.232,32.641 +2020-01-07 15:30:00,127.74,208.46,62.232,32.641 +2020-01-07 15:45:00,128.19,210.165,62.232,32.641 +2020-01-07 16:00:00,130.28,210.956,63.611999999999995,32.641 +2020-01-07 16:15:00,132.81,212.595,63.611999999999995,32.641 +2020-01-07 16:30:00,135.09,215.49900000000002,63.611999999999995,32.641 +2020-01-07 16:45:00,135.68,217.08900000000003,63.611999999999995,32.641 +2020-01-07 17:00:00,136.74,219.416,70.658,32.641 +2020-01-07 17:15:00,137.73,220.201,70.658,32.641 +2020-01-07 17:30:00,140.3,221.10299999999998,70.658,32.641 +2020-01-07 17:45:00,142.14,220.933,70.658,32.641 +2020-01-07 18:00:00,139.97,222.459,68.361,32.641 +2020-01-07 18:15:00,138.96,219.579,68.361,32.641 +2020-01-07 18:30:00,138.24,218.333,68.361,32.641 +2020-01-07 18:45:00,139.74,218.446,68.361,32.641 +2020-01-07 19:00:00,132.95,218.03900000000002,62.922,32.641 +2020-01-07 19:15:00,131.59,214.065,62.922,32.641 +2020-01-07 19:30:00,137.99,211.38400000000001,62.922,32.641 +2020-01-07 19:45:00,136.02,207.86,62.922,32.641 +2020-01-07 20:00:00,126.68,204.014,63.251999999999995,32.641 +2020-01-07 20:15:00,117.77,197.558,63.251999999999995,32.641 +2020-01-07 20:30:00,114.85,193.41400000000002,63.251999999999995,32.641 +2020-01-07 20:45:00,111.71,191.56,63.251999999999995,32.641 +2020-01-07 21:00:00,108.54,188.565,54.47,32.641 +2020-01-07 21:15:00,108.54,185.96599999999998,54.47,32.641 +2020-01-07 21:30:00,111.64,183.882,54.47,32.641 +2020-01-07 21:45:00,109.59,182.312,54.47,32.641 +2020-01-07 22:00:00,99.32,175.27200000000002,51.12,32.641 +2020-01-07 22:15:00,98.89,169.467,51.12,32.641 +2020-01-07 22:30:00,93.04,155.618,51.12,32.641 +2020-01-07 22:45:00,92.29,147.349,51.12,32.641 +2020-01-07 23:00:00,94.19,140.65200000000002,42.156000000000006,32.641 +2020-01-07 23:15:00,93.18,138.885,42.156000000000006,32.641 +2020-01-07 23:30:00,88.85,139.142,42.156000000000006,32.641 +2020-01-07 23:45:00,80.72,138.58700000000002,42.156000000000006,32.641 +2020-01-08 00:00:00,80.57,131.975,37.192,32.641 +2020-01-08 00:15:00,77.7,131.555,37.192,32.641 +2020-01-08 00:30:00,82.22,132.80200000000002,37.192,32.641 +2020-01-08 00:45:00,83.18,134.30200000000002,37.192,32.641 +2020-01-08 01:00:00,80.31,137.168,32.24,32.641 +2020-01-08 01:15:00,80.35,137.54,32.24,32.641 +2020-01-08 01:30:00,81.14,138.001,32.24,32.641 +2020-01-08 01:45:00,83.23,138.461,32.24,32.641 +2020-01-08 02:00:00,82.4,140.52,30.34,32.641 +2020-01-08 02:15:00,76.87,142.319,30.34,32.641 +2020-01-08 02:30:00,81.52,142.981,30.34,32.641 +2020-01-08 02:45:00,82.44,145.014,30.34,32.641 +2020-01-08 03:00:00,82.38,147.77100000000002,29.129,32.641 +2020-01-08 03:15:00,76.92,148.901,29.129,32.641 +2020-01-08 03:30:00,80.45,150.79399999999998,29.129,32.641 +2020-01-08 03:45:00,83.8,152.255,29.129,32.641 +2020-01-08 04:00:00,82.13,164.354,30.075,32.641 +2020-01-08 04:15:00,80.51,176.32,30.075,32.641 +2020-01-08 04:30:00,79.95,179.37900000000002,30.075,32.641 +2020-01-08 04:45:00,82.2,182.28,30.075,32.641 +2020-01-08 05:00:00,86.19,217.273,35.684,32.641 +2020-01-08 05:15:00,86.69,245.778,35.684,32.641 +2020-01-08 05:30:00,92.91,241.455,35.684,32.641 +2020-01-08 05:45:00,97.02,234.27,35.684,32.641 +2020-01-08 06:00:00,105.5,230.947,51.49,32.641 +2020-01-08 06:15:00,111.76,236.826,51.49,32.641 +2020-01-08 06:30:00,116.68,239.821,51.49,32.641 +2020-01-08 06:45:00,121.17,244.044,51.49,32.641 +2020-01-08 07:00:00,125.64,242.99099999999999,68.242,32.641 +2020-01-08 07:15:00,129.08,248.426,68.242,32.641 +2020-01-08 07:30:00,132.63,251.513,68.242,32.641 +2020-01-08 07:45:00,135.49,253.108,68.242,32.641 +2020-01-08 08:00:00,134.07,251.9,63.619,32.641 +2020-01-08 08:15:00,136.8,252.067,63.619,32.641 +2020-01-08 08:30:00,136.39,250.003,63.619,32.641 +2020-01-08 08:45:00,135.84,246.926,63.619,32.641 +2020-01-08 09:00:00,136.47,240.108,61.333,32.641 +2020-01-08 09:15:00,137.33,236.976,61.333,32.641 +2020-01-08 09:30:00,137.5,234.81799999999998,61.333,32.641 +2020-01-08 09:45:00,137.48,231.62099999999998,61.333,32.641 +2020-01-08 10:00:00,134.72,226.333,59.663000000000004,32.641 +2020-01-08 10:15:00,132.18,222.105,59.663000000000004,32.641 +2020-01-08 10:30:00,130.36,218.767,59.663000000000004,32.641 +2020-01-08 10:45:00,128.21,217.075,59.663000000000004,32.641 +2020-01-08 11:00:00,129.5,214.952,59.771,32.641 +2020-01-08 11:15:00,129.67,213.665,59.771,32.641 +2020-01-08 11:30:00,130.99,212.125,59.771,32.641 +2020-01-08 11:45:00,126.7,210.93099999999998,59.771,32.641 +2020-01-08 12:00:00,126.02,205.842,58.723,32.641 +2020-01-08 12:15:00,124.09,205.294,58.723,32.641 +2020-01-08 12:30:00,124.12,205.25099999999998,58.723,32.641 +2020-01-08 12:45:00,123.16,206.101,58.723,32.641 +2020-01-08 13:00:00,121.39,204.422,58.727,32.641 +2020-01-08 13:15:00,121.87,203.891,58.727,32.641 +2020-01-08 13:30:00,120.34,203.52,58.727,32.641 +2020-01-08 13:45:00,120.5,203.426,58.727,32.641 +2020-01-08 14:00:00,121.17,202.41299999999998,59.803999999999995,32.641 +2020-01-08 14:15:00,121.88,203.019,59.803999999999995,32.641 +2020-01-08 14:30:00,122.69,203.952,59.803999999999995,32.641 +2020-01-08 14:45:00,123.96,204.262,59.803999999999995,32.641 +2020-01-08 15:00:00,124.01,205.516,61.05,32.641 +2020-01-08 15:15:00,124.57,206.048,61.05,32.641 +2020-01-08 15:30:00,121.73,208.44799999999998,61.05,32.641 +2020-01-08 15:45:00,124.55,210.141,61.05,32.641 +2020-01-08 16:00:00,126.01,210.935,64.012,32.641 +2020-01-08 16:15:00,128.33,212.583,64.012,32.641 +2020-01-08 16:30:00,131.93,215.49400000000003,64.012,32.641 +2020-01-08 16:45:00,136.83,217.09400000000002,64.012,32.641 +2020-01-08 17:00:00,138.72,219.403,66.751,32.641 +2020-01-08 17:15:00,141.55,220.22,66.751,32.641 +2020-01-08 17:30:00,141.31,221.15200000000002,66.751,32.641 +2020-01-08 17:45:00,141.4,220.99900000000002,66.751,32.641 +2020-01-08 18:00:00,140.41,222.546,65.91199999999999,32.641 +2020-01-08 18:15:00,138.77,219.672,65.91199999999999,32.641 +2020-01-08 18:30:00,137.47,218.43200000000002,65.91199999999999,32.641 +2020-01-08 18:45:00,137.79,218.563,65.91199999999999,32.641 +2020-01-08 19:00:00,135.13,218.12400000000002,63.324,32.641 +2020-01-08 19:15:00,133.38,214.15,63.324,32.641 +2020-01-08 19:30:00,137.98,211.472,63.324,32.641 +2020-01-08 19:45:00,138.21,207.94799999999998,63.324,32.641 +2020-01-08 20:00:00,130.16,204.08599999999998,63.573,32.641 +2020-01-08 20:15:00,121.28,197.63099999999997,63.573,32.641 +2020-01-08 20:30:00,114.19,193.47799999999998,63.573,32.641 +2020-01-08 20:45:00,114.46,191.641,63.573,32.641 +2020-01-08 21:00:00,111.07,188.627,55.073,32.641 +2020-01-08 21:15:00,113.94,186.01,55.073,32.641 +2020-01-08 21:30:00,114.23,183.926,55.073,32.641 +2020-01-08 21:45:00,109.84,182.37,55.073,32.641 +2020-01-08 22:00:00,103.51,175.329,51.321999999999996,32.641 +2020-01-08 22:15:00,99.13,169.53799999999998,51.321999999999996,32.641 +2020-01-08 22:30:00,95.71,155.703,51.321999999999996,32.641 +2020-01-08 22:45:00,101.09,147.44299999999998,51.321999999999996,32.641 +2020-01-08 23:00:00,97.81,140.725,42.09,32.641 +2020-01-08 23:15:00,93.34,138.966,42.09,32.641 +2020-01-08 23:30:00,90.01,139.239,42.09,32.641 +2020-01-08 23:45:00,90.33,138.685,42.09,32.641 +2020-01-09 00:00:00,89.51,132.093,38.399,32.641 +2020-01-09 00:15:00,88.2,131.65,38.399,32.641 +2020-01-09 00:30:00,82.53,132.88299999999998,38.399,32.641 +2020-01-09 00:45:00,87.05,134.366,38.399,32.641 +2020-01-09 01:00:00,85.85,137.24,36.94,32.641 +2020-01-09 01:15:00,83.8,137.60399999999998,36.94,32.641 +2020-01-09 01:30:00,79.51,138.062,36.94,32.641 +2020-01-09 01:45:00,77.14,138.512,36.94,32.641 +2020-01-09 02:00:00,82.48,140.584,35.275,32.641 +2020-01-09 02:15:00,82.45,142.384,35.275,32.641 +2020-01-09 02:30:00,84.42,143.055,35.275,32.641 +2020-01-09 02:45:00,79.29,145.088,35.275,32.641 +2020-01-09 03:00:00,83.67,147.839,35.329,32.641 +2020-01-09 03:15:00,85.27,148.987,35.329,32.641 +2020-01-09 03:30:00,86.25,150.881,35.329,32.641 +2020-01-09 03:45:00,82.05,152.349,35.329,32.641 +2020-01-09 04:00:00,85.8,164.422,36.275,32.641 +2020-01-09 04:15:00,85.66,176.38,36.275,32.641 +2020-01-09 04:30:00,84.93,179.43599999999998,36.275,32.641 +2020-01-09 04:45:00,85.52,182.334,36.275,32.641 +2020-01-09 05:00:00,88.07,217.282,42.193999999999996,32.641 +2020-01-09 05:15:00,91.08,245.75099999999998,42.193999999999996,32.641 +2020-01-09 05:30:00,94.82,241.43599999999998,42.193999999999996,32.641 +2020-01-09 05:45:00,99.1,234.27200000000002,42.193999999999996,32.641 +2020-01-09 06:00:00,105.02,230.97299999999998,56.422,32.641 +2020-01-09 06:15:00,114.36,236.86,56.422,32.641 +2020-01-09 06:30:00,118.47,239.872,56.422,32.641 +2020-01-09 06:45:00,123.49,244.12599999999998,56.422,32.641 +2020-01-09 07:00:00,128.98,243.08900000000003,72.569,32.641 +2020-01-09 07:15:00,133.18,248.513,72.569,32.641 +2020-01-09 07:30:00,135.84,251.582,72.569,32.641 +2020-01-09 07:45:00,136.0,253.157,72.569,32.641 +2020-01-09 08:00:00,136.35,251.945,67.704,32.641 +2020-01-09 08:15:00,136.41,252.09599999999998,67.704,32.641 +2020-01-09 08:30:00,136.61,250.002,67.704,32.641 +2020-01-09 08:45:00,135.57,246.90599999999998,67.704,32.641 +2020-01-09 09:00:00,134.91,240.072,63.434,32.641 +2020-01-09 09:15:00,135.13,236.947,63.434,32.641 +2020-01-09 09:30:00,132.82,234.805,63.434,32.641 +2020-01-09 09:45:00,134.04,231.602,63.434,32.641 +2020-01-09 10:00:00,133.79,226.315,61.88399999999999,32.641 +2020-01-09 10:15:00,132.44,222.09,61.88399999999999,32.641 +2020-01-09 10:30:00,131.18,218.74200000000002,61.88399999999999,32.641 +2020-01-09 10:45:00,129.86,217.054,61.88399999999999,32.641 +2020-01-09 11:00:00,129.31,214.91099999999997,61.481,32.641 +2020-01-09 11:15:00,129.73,213.62400000000002,61.481,32.641 +2020-01-09 11:30:00,129.62,212.08599999999998,61.481,32.641 +2020-01-09 11:45:00,128.23,210.895,61.481,32.641 +2020-01-09 12:00:00,128.19,205.817,59.527,32.641 +2020-01-09 12:15:00,126.99,205.28599999999997,59.527,32.641 +2020-01-09 12:30:00,126.29,205.238,59.527,32.641 +2020-01-09 12:45:00,125.88,206.08900000000003,59.527,32.641 +2020-01-09 13:00:00,125.21,204.40099999999998,58.794,32.641 +2020-01-09 13:15:00,125.26,203.859,58.794,32.641 +2020-01-09 13:30:00,120.6,203.476,58.794,32.641 +2020-01-09 13:45:00,123.97,203.37599999999998,58.794,32.641 +2020-01-09 14:00:00,127.41,202.38099999999997,60.32,32.641 +2020-01-09 14:15:00,128.59,202.979,60.32,32.641 +2020-01-09 14:30:00,127.7,203.919,60.32,32.641 +2020-01-09 14:45:00,128.7,204.245,60.32,32.641 +2020-01-09 15:00:00,129.88,205.519,62.52,32.641 +2020-01-09 15:15:00,129.65,206.03,62.52,32.641 +2020-01-09 15:30:00,128.81,208.423,62.52,32.641 +2020-01-09 15:45:00,129.3,210.108,62.52,32.641 +2020-01-09 16:00:00,130.58,210.90200000000002,64.199,32.641 +2020-01-09 16:15:00,132.89,212.55900000000003,64.199,32.641 +2020-01-09 16:30:00,135.51,215.477,64.199,32.641 +2020-01-09 16:45:00,138.94,217.085,64.199,32.641 +2020-01-09 17:00:00,140.54,219.38,68.19800000000001,32.641 +2020-01-09 17:15:00,141.52,220.227,68.19800000000001,32.641 +2020-01-09 17:30:00,142.06,221.187,68.19800000000001,32.641 +2020-01-09 17:45:00,142.3,221.053,68.19800000000001,32.641 +2020-01-09 18:00:00,140.27,222.622,67.899,32.641 +2020-01-09 18:15:00,139.4,219.75400000000002,67.899,32.641 +2020-01-09 18:30:00,137.51,218.52,67.899,32.641 +2020-01-09 18:45:00,137.88,218.67,67.899,32.641 +2020-01-09 19:00:00,135.66,218.199,64.72399999999999,32.641 +2020-01-09 19:15:00,133.63,214.226,64.72399999999999,32.641 +2020-01-09 19:30:00,136.63,211.551,64.72399999999999,32.641 +2020-01-09 19:45:00,136.33,208.028,64.72399999999999,32.641 +2020-01-09 20:00:00,130.0,204.149,64.062,32.641 +2020-01-09 20:15:00,120.43,197.695,64.062,32.641 +2020-01-09 20:30:00,115.58,193.532,64.062,32.641 +2020-01-09 20:45:00,114.76,191.713,64.062,32.641 +2020-01-09 21:00:00,111.28,188.68,57.971000000000004,32.641 +2020-01-09 21:15:00,114.3,186.046,57.971000000000004,32.641 +2020-01-09 21:30:00,112.74,183.96200000000002,57.971000000000004,32.641 +2020-01-09 21:45:00,107.12,182.422,57.971000000000004,32.641 +2020-01-09 22:00:00,103.37,175.37599999999998,53.715,32.641 +2020-01-09 22:15:00,97.81,169.60299999999998,53.715,32.641 +2020-01-09 22:30:00,96.27,155.78,53.715,32.641 +2020-01-09 22:45:00,99.06,147.52700000000002,53.715,32.641 +2020-01-09 23:00:00,96.54,140.78799999999998,47.8,32.641 +2020-01-09 23:15:00,90.59,139.037,47.8,32.641 +2020-01-09 23:30:00,83.45,139.326,47.8,32.641 +2020-01-09 23:45:00,85.64,138.773,47.8,32.641 +2020-01-10 00:00:00,84.68,131.172,43.656000000000006,32.641 +2020-01-10 00:15:00,87.29,130.907,43.656000000000006,32.641 +2020-01-10 00:30:00,86.37,131.95600000000002,43.656000000000006,32.641 +2020-01-10 00:45:00,81.92,133.513,43.656000000000006,32.641 +2020-01-10 01:00:00,80.49,136.08100000000002,41.263000000000005,32.641 +2020-01-10 01:15:00,84.15,137.489,41.263000000000005,32.641 +2020-01-10 01:30:00,82.86,137.643,41.263000000000005,32.641 +2020-01-10 01:45:00,80.26,138.219,41.263000000000005,32.641 +2020-01-10 02:00:00,82.41,140.32299999999998,40.799,32.641 +2020-01-10 02:15:00,82.9,142.0,40.799,32.641 +2020-01-10 02:30:00,82.52,143.188,40.799,32.641 +2020-01-10 02:45:00,78.57,145.32399999999998,40.799,32.641 +2020-01-10 03:00:00,82.79,146.91,41.398,32.641 +2020-01-10 03:15:00,83.34,149.214,41.398,32.641 +2020-01-10 03:30:00,81.33,151.107,41.398,32.641 +2020-01-10 03:45:00,83.38,152.868,41.398,32.641 +2020-01-10 04:00:00,84.86,165.15900000000002,42.38,32.641 +2020-01-10 04:15:00,82.09,176.97400000000002,42.38,32.641 +2020-01-10 04:30:00,80.74,180.197,42.38,32.641 +2020-01-10 04:45:00,81.91,181.887,42.38,32.641 +2020-01-10 05:00:00,85.77,215.43599999999998,46.181000000000004,32.641 +2020-01-10 05:15:00,88.51,245.451,46.181000000000004,32.641 +2020-01-10 05:30:00,92.4,242.31599999999997,46.181000000000004,32.641 +2020-01-10 05:45:00,97.86,235.141,46.181000000000004,32.641 +2020-01-10 06:00:00,105.68,232.324,59.33,32.641 +2020-01-10 06:15:00,110.88,236.578,59.33,32.641 +2020-01-10 06:30:00,114.95,238.657,59.33,32.641 +2020-01-10 06:45:00,120.02,244.748,59.33,32.641 +2020-01-10 07:00:00,125.32,242.745,72.454,32.641 +2020-01-10 07:15:00,129.26,249.206,72.454,32.641 +2020-01-10 07:30:00,133.08,252.229,72.454,32.641 +2020-01-10 07:45:00,137.34,252.824,72.454,32.641 +2020-01-10 08:00:00,138.92,250.329,67.175,32.641 +2020-01-10 08:15:00,139.19,249.984,67.175,32.641 +2020-01-10 08:30:00,142.05,248.93900000000002,67.175,32.641 +2020-01-10 08:45:00,139.98,244.104,67.175,32.641 +2020-01-10 09:00:00,139.84,237.938,65.365,32.641 +2020-01-10 09:15:00,142.47,235.299,65.365,32.641 +2020-01-10 09:30:00,143.87,232.75599999999997,65.365,32.641 +2020-01-10 09:45:00,144.34,229.4,65.365,32.641 +2020-01-10 10:00:00,143.86,222.885,63.95,32.641 +2020-01-10 10:15:00,145.24,219.442,63.95,32.641 +2020-01-10 10:30:00,146.08,215.968,63.95,32.641 +2020-01-10 10:45:00,145.52,213.80700000000002,63.95,32.641 +2020-01-10 11:00:00,143.53,211.615,63.92100000000001,32.641 +2020-01-10 11:15:00,144.41,209.428,63.92100000000001,32.641 +2020-01-10 11:30:00,142.84,209.82,63.92100000000001,32.641 +2020-01-10 11:45:00,140.57,208.75099999999998,63.92100000000001,32.641 +2020-01-10 12:00:00,139.98,204.835,60.79600000000001,32.641 +2020-01-10 12:15:00,139.61,202.088,60.79600000000001,32.641 +2020-01-10 12:30:00,138.38,202.197,60.79600000000001,32.641 +2020-01-10 12:45:00,137.77,203.66400000000002,60.79600000000001,32.641 +2020-01-10 13:00:00,135.76,202.949,59.393,32.641 +2020-01-10 13:15:00,137.41,203.271,59.393,32.641 +2020-01-10 13:30:00,133.35,202.84099999999998,59.393,32.641 +2020-01-10 13:45:00,131.39,202.644,59.393,32.641 +2020-01-10 14:00:00,131.97,200.481,57.943999999999996,32.641 +2020-01-10 14:15:00,132.24,200.84900000000002,57.943999999999996,32.641 +2020-01-10 14:30:00,130.8,202.25,57.943999999999996,32.641 +2020-01-10 14:45:00,131.39,202.96599999999998,57.943999999999996,32.641 +2020-01-10 15:00:00,131.3,203.739,60.153999999999996,32.641 +2020-01-10 15:15:00,130.95,203.773,60.153999999999996,32.641 +2020-01-10 15:30:00,130.15,204.55,60.153999999999996,32.641 +2020-01-10 15:45:00,129.69,206.333,60.153999999999996,32.641 +2020-01-10 16:00:00,130.75,205.926,62.933,32.641 +2020-01-10 16:15:00,132.79,207.87400000000002,62.933,32.641 +2020-01-10 16:30:00,135.68,210.912,62.933,32.641 +2020-01-10 16:45:00,137.72,212.451,62.933,32.641 +2020-01-10 17:00:00,139.5,214.852,68.657,32.641 +2020-01-10 17:15:00,139.88,215.297,68.657,32.641 +2020-01-10 17:30:00,142.57,215.93400000000003,68.657,32.641 +2020-01-10 17:45:00,139.22,215.576,68.657,32.641 +2020-01-10 18:00:00,136.73,217.919,67.111,32.641 +2020-01-10 18:15:00,136.11,214.695,67.111,32.641 +2020-01-10 18:30:00,134.73,213.893,67.111,32.641 +2020-01-10 18:45:00,135.6,214.03400000000002,67.111,32.641 +2020-01-10 19:00:00,132.42,214.46200000000002,62.434,32.641 +2020-01-10 19:15:00,130.68,211.921,62.434,32.641 +2020-01-10 19:30:00,135.53,208.804,62.434,32.641 +2020-01-10 19:45:00,135.12,204.845,62.434,32.641 +2020-01-10 20:00:00,127.21,201.00900000000001,61.763000000000005,32.641 +2020-01-10 20:15:00,118.7,194.521,61.763000000000005,32.641 +2020-01-10 20:30:00,112.79,190.331,61.763000000000005,32.641 +2020-01-10 20:45:00,111.77,189.175,61.763000000000005,32.641 +2020-01-10 21:00:00,108.32,186.59400000000002,56.785,32.641 +2020-01-10 21:15:00,110.88,184.317,56.785,32.641 +2020-01-10 21:30:00,102.73,182.29,56.785,32.641 +2020-01-10 21:45:00,100.86,181.33900000000003,56.785,32.641 +2020-01-10 22:00:00,96.64,175.354,52.693000000000005,32.641 +2020-01-10 22:15:00,93.07,169.468,52.693000000000005,32.641 +2020-01-10 22:30:00,92.88,162.24,52.693000000000005,32.641 +2020-01-10 22:45:00,93.82,157.739,52.693000000000005,32.641 +2020-01-10 23:00:00,88.99,150.405,45.443999999999996,32.641 +2020-01-10 23:15:00,89.2,146.657,45.443999999999996,32.641 +2020-01-10 23:30:00,85.72,145.495,45.443999999999996,32.641 +2020-01-10 23:45:00,80.79,144.237,45.443999999999996,32.641 +2020-01-11 00:00:00,75.56,128.079,44.738,32.459 +2020-01-11 00:15:00,80.09,123.209,44.738,32.459 +2020-01-11 00:30:00,80.07,125.723,44.738,32.459 +2020-01-11 00:45:00,77.68,128.094,44.738,32.459 +2020-01-11 01:00:00,71.96,131.326,40.303000000000004,32.459 +2020-01-11 01:15:00,70.77,131.606,40.303000000000004,32.459 +2020-01-11 01:30:00,68.37,131.253,40.303000000000004,32.459 +2020-01-11 01:45:00,68.2,131.491,40.303000000000004,32.459 +2020-01-11 02:00:00,69.18,134.41899999999998,38.61,32.459 +2020-01-11 02:15:00,74.74,135.753,38.61,32.459 +2020-01-11 02:30:00,74.65,135.797,38.61,32.459 +2020-01-11 02:45:00,74.36,137.984,38.61,32.459 +2020-01-11 03:00:00,67.49,140.34799999999998,37.554,32.459 +2020-01-11 03:15:00,72.14,141.393,37.554,32.459 +2020-01-11 03:30:00,74.34,141.52200000000002,37.554,32.459 +2020-01-11 03:45:00,74.02,143.29399999999998,37.554,32.459 +2020-01-11 04:00:00,69.83,151.159,37.176,32.459 +2020-01-11 04:15:00,66.48,160.25,37.176,32.459 +2020-01-11 04:30:00,66.65,161.23,37.176,32.459 +2020-01-11 04:45:00,68.43,162.349,37.176,32.459 +2020-01-11 05:00:00,69.3,178.98,36.893,32.459 +2020-01-11 05:15:00,68.52,189.00599999999997,36.893,32.459 +2020-01-11 05:30:00,68.35,186.139,36.893,32.459 +2020-01-11 05:45:00,69.45,184.59400000000002,36.893,32.459 +2020-01-11 06:00:00,71.01,201.507,37.803000000000004,32.459 +2020-01-11 06:15:00,71.8,223.074,37.803000000000004,32.459 +2020-01-11 06:30:00,72.53,219.50099999999998,37.803000000000004,32.459 +2020-01-11 06:45:00,74.42,216.02700000000002,37.803000000000004,32.459 +2020-01-11 07:00:00,77.16,210.03799999999998,41.086999999999996,32.459 +2020-01-11 07:15:00,79.89,215.21599999999998,41.086999999999996,32.459 +2020-01-11 07:30:00,82.88,221.06599999999997,41.086999999999996,32.459 +2020-01-11 07:45:00,87.25,225.924,41.086999999999996,32.459 +2020-01-11 08:00:00,89.61,227.84799999999998,48.222,32.459 +2020-01-11 08:15:00,91.87,231.47099999999998,48.222,32.459 +2020-01-11 08:30:00,94.54,232.15900000000002,48.222,32.459 +2020-01-11 08:45:00,97.83,230.68099999999998,48.222,32.459 +2020-01-11 09:00:00,99.9,226.22400000000002,52.791000000000004,32.459 +2020-01-11 09:15:00,102.11,224.375,52.791000000000004,32.459 +2020-01-11 09:30:00,103.03,222.796,52.791000000000004,32.459 +2020-01-11 09:45:00,104.11,219.641,52.791000000000004,32.459 +2020-01-11 10:00:00,105.67,213.37900000000002,54.341,32.459 +2020-01-11 10:15:00,106.52,210.09400000000002,54.341,32.459 +2020-01-11 10:30:00,107.3,206.815,54.341,32.459 +2020-01-11 10:45:00,107.94,206.1,54.341,32.459 +2020-01-11 11:00:00,109.65,204.15400000000002,51.94,32.459 +2020-01-11 11:15:00,111.25,201.162,51.94,32.459 +2020-01-11 11:30:00,111.53,200.33,51.94,32.459 +2020-01-11 11:45:00,111.53,198.183,51.94,32.459 +2020-01-11 12:00:00,110.54,193.285,50.973,32.459 +2020-01-11 12:15:00,109.01,191.19299999999998,50.973,32.459 +2020-01-11 12:30:00,105.95,191.648,50.973,32.459 +2020-01-11 12:45:00,104.43,192.233,50.973,32.459 +2020-01-11 13:00:00,101.49,191.13,48.06399999999999,32.459 +2020-01-11 13:15:00,99.78,189.25,48.06399999999999,32.459 +2020-01-11 13:30:00,98.12,188.30700000000002,48.06399999999999,32.459 +2020-01-11 13:45:00,96.61,188.71200000000002,48.06399999999999,32.459 +2020-01-11 14:00:00,95.14,187.91299999999998,45.707,32.459 +2020-01-11 14:15:00,94.37,187.77599999999998,45.707,32.459 +2020-01-11 14:30:00,94.65,187.243,45.707,32.459 +2020-01-11 14:45:00,94.99,188.178,45.707,32.459 +2020-01-11 15:00:00,94.68,189.683,47.567,32.459 +2020-01-11 15:15:00,94.55,190.521,47.567,32.459 +2020-01-11 15:30:00,93.53,192.94,47.567,32.459 +2020-01-11 15:45:00,96.02,194.787,47.567,32.459 +2020-01-11 16:00:00,97.55,192.99599999999998,52.031000000000006,32.459 +2020-01-11 16:15:00,98.75,195.96900000000002,52.031000000000006,32.459 +2020-01-11 16:30:00,102.68,198.946,52.031000000000006,32.459 +2020-01-11 16:45:00,104.37,201.447,52.031000000000006,32.459 +2020-01-11 17:00:00,106.29,203.333,58.218999999999994,32.459 +2020-01-11 17:15:00,107.17,205.708,58.218999999999994,32.459 +2020-01-11 17:30:00,108.36,206.278,58.218999999999994,32.459 +2020-01-11 17:45:00,109.4,205.47799999999998,58.218999999999994,32.459 +2020-01-11 18:00:00,108.57,207.27200000000002,57.65,32.459 +2020-01-11 18:15:00,108.91,205.892,57.65,32.459 +2020-01-11 18:30:00,108.15,206.44799999999998,57.65,32.459 +2020-01-11 18:45:00,107.83,203.21400000000003,57.65,32.459 +2020-01-11 19:00:00,105.71,204.699,51.261,32.459 +2020-01-11 19:15:00,103.93,201.683,51.261,32.459 +2020-01-11 19:30:00,102.6,199.333,51.261,32.459 +2020-01-11 19:45:00,101.86,195.09900000000002,51.261,32.459 +2020-01-11 20:00:00,97.56,193.503,44.068000000000005,32.459 +2020-01-11 20:15:00,92.87,189.32299999999998,44.068000000000005,32.459 +2020-01-11 20:30:00,90.31,184.799,44.068000000000005,32.459 +2020-01-11 20:45:00,88.48,183.128,44.068000000000005,32.459 +2020-01-11 21:00:00,86.43,183.017,38.861,32.459 +2020-01-11 21:15:00,84.3,181.21200000000002,38.861,32.459 +2020-01-11 21:30:00,83.24,180.489,38.861,32.459 +2020-01-11 21:45:00,82.41,179.149,38.861,32.459 +2020-01-11 22:00:00,79.49,174.601,39.485,32.459 +2020-01-11 22:15:00,77.77,171.377,39.485,32.459 +2020-01-11 22:30:00,76.13,170.824,39.485,32.459 +2020-01-11 22:45:00,75.32,168.31,39.485,32.459 +2020-01-11 23:00:00,72.71,163.583,32.027,32.459 +2020-01-11 23:15:00,71.28,158.07399999999998,32.027,32.459 +2020-01-11 23:30:00,69.11,154.937,32.027,32.459 +2020-01-11 23:45:00,66.62,151.07,32.027,32.459 +2020-01-12 00:00:00,64.99,128.533,26.96,32.459 +2020-01-12 00:15:00,63.46,123.37200000000001,26.96,32.459 +2020-01-12 00:30:00,61.72,125.475,26.96,32.459 +2020-01-12 00:45:00,60.63,128.591,26.96,32.459 +2020-01-12 01:00:00,59.84,131.655,24.295,32.459 +2020-01-12 01:15:00,59.13,133.06,24.295,32.459 +2020-01-12 01:30:00,58.42,133.279,24.295,32.459 +2020-01-12 01:45:00,57.74,133.195,24.295,32.459 +2020-01-12 02:00:00,57.23,135.319,24.268,32.459 +2020-01-12 02:15:00,56.86,135.67700000000002,24.268,32.459 +2020-01-12 02:30:00,56.65,136.64600000000002,24.268,32.459 +2020-01-12 02:45:00,56.47,139.35399999999998,24.268,32.459 +2020-01-12 03:00:00,56.4,142.011,23.373,32.459 +2020-01-12 03:15:00,56.47,142.487,23.373,32.459 +2020-01-12 03:30:00,56.48,144.191,23.373,32.459 +2020-01-12 03:45:00,56.69,145.944,23.373,32.459 +2020-01-12 04:00:00,57.05,153.52700000000002,23.874000000000002,32.459 +2020-01-12 04:15:00,56.86,161.537,23.874000000000002,32.459 +2020-01-12 04:30:00,57.57,162.489,23.874000000000002,32.459 +2020-01-12 04:45:00,58.1,163.93400000000003,23.874000000000002,32.459 +2020-01-12 05:00:00,58.61,176.683,24.871,32.459 +2020-01-12 05:15:00,59.09,184.1,24.871,32.459 +2020-01-12 05:30:00,58.93,181.088,24.871,32.459 +2020-01-12 05:45:00,59.64,179.84799999999998,24.871,32.459 +2020-01-12 06:00:00,60.42,196.84599999999998,23.84,32.459 +2020-01-12 06:15:00,60.79,216.484,23.84,32.459 +2020-01-12 06:30:00,60.98,211.77,23.84,32.459 +2020-01-12 06:45:00,62.52,207.247,23.84,32.459 +2020-01-12 07:00:00,64.57,203.924,27.430999999999997,32.459 +2020-01-12 07:15:00,66.17,208.28099999999998,27.430999999999997,32.459 +2020-01-12 07:30:00,67.93,212.678,27.430999999999997,32.459 +2020-01-12 07:45:00,69.79,216.687,27.430999999999997,32.459 +2020-01-12 08:00:00,71.93,220.56599999999997,33.891999999999996,32.459 +2020-01-12 08:15:00,74.46,223.995,33.891999999999996,32.459 +2020-01-12 08:30:00,76.78,226.389,33.891999999999996,32.459 +2020-01-12 08:45:00,78.97,227.08900000000003,33.891999999999996,32.459 +2020-01-12 09:00:00,80.51,222.19299999999998,37.571,32.459 +2020-01-12 09:15:00,81.13,220.979,37.571,32.459 +2020-01-12 09:30:00,81.84,219.22099999999998,37.571,32.459 +2020-01-12 09:45:00,82.37,215.87099999999998,37.571,32.459 +2020-01-12 10:00:00,81.27,212.296,40.594,32.459 +2020-01-12 10:15:00,79.61,209.584,40.594,32.459 +2020-01-12 10:30:00,79.77,206.93400000000003,40.594,32.459 +2020-01-12 10:45:00,85.58,204.109,40.594,32.459 +2020-01-12 11:00:00,86.51,203.16299999999998,44.133,32.459 +2020-01-12 11:15:00,88.46,200.34099999999998,44.133,32.459 +2020-01-12 11:30:00,91.87,198.543,44.133,32.459 +2020-01-12 11:45:00,93.46,197.037,44.133,32.459 +2020-01-12 12:00:00,91.63,191.477,41.198,32.459 +2020-01-12 12:15:00,90.3,191.49099999999999,41.198,32.459 +2020-01-12 12:30:00,88.06,190.357,41.198,32.459 +2020-01-12 12:45:00,86.68,189.94,41.198,32.459 +2020-01-12 13:00:00,85.19,188.076,37.014,32.459 +2020-01-12 13:15:00,83.88,189.463,37.014,32.459 +2020-01-12 13:30:00,82.6,188.347,37.014,32.459 +2020-01-12 13:45:00,82.08,187.97099999999998,37.014,32.459 +2020-01-12 14:00:00,82.67,187.389,34.934,32.459 +2020-01-12 14:15:00,82.79,188.51,34.934,32.459 +2020-01-12 14:30:00,82.64,189.378,34.934,32.459 +2020-01-12 14:45:00,85.76,189.947,34.934,32.459 +2020-01-12 15:00:00,83.46,189.813,34.588,32.459 +2020-01-12 15:15:00,83.64,191.487,34.588,32.459 +2020-01-12 15:30:00,84.7,194.537,34.588,32.459 +2020-01-12 15:45:00,85.25,197.101,34.588,32.459 +2020-01-12 16:00:00,86.79,197.385,37.874,32.459 +2020-01-12 16:15:00,88.0,199.38,37.874,32.459 +2020-01-12 16:30:00,92.21,202.588,37.874,32.459 +2020-01-12 16:45:00,94.95,205.236,37.874,32.459 +2020-01-12 17:00:00,97.3,207.06599999999997,47.303999999999995,32.459 +2020-01-12 17:15:00,99.15,209.045,47.303999999999995,32.459 +2020-01-12 17:30:00,100.86,209.912,47.303999999999995,32.459 +2020-01-12 17:45:00,102.12,211.532,47.303999999999995,32.459 +2020-01-12 18:00:00,102.63,212.722,48.879,32.459 +2020-01-12 18:15:00,102.78,212.78599999999997,48.879,32.459 +2020-01-12 18:30:00,101.93,211.138,48.879,32.459 +2020-01-12 18:45:00,100.6,209.889,48.879,32.459 +2020-01-12 19:00:00,99.22,210.878,44.826,32.459 +2020-01-12 19:15:00,96.93,208.544,44.826,32.459 +2020-01-12 19:30:00,98.09,206.03599999999997,44.826,32.459 +2020-01-12 19:45:00,94.04,203.36,44.826,32.459 +2020-01-12 20:00:00,92.48,201.725,40.154,32.459 +2020-01-12 20:15:00,90.52,198.604,40.154,32.459 +2020-01-12 20:30:00,88.61,195.35299999999998,40.154,32.459 +2020-01-12 20:45:00,86.91,192.50099999999998,40.154,32.459 +2020-01-12 21:00:00,86.27,189.613,36.549,32.459 +2020-01-12 21:15:00,85.53,187.137,36.549,32.459 +2020-01-12 21:30:00,85.67,186.76,36.549,32.459 +2020-01-12 21:45:00,86.26,185.56099999999998,36.549,32.459 +2020-01-12 22:00:00,84.68,179.642,37.663000000000004,32.459 +2020-01-12 22:15:00,84.59,175.704,37.663000000000004,32.459 +2020-01-12 22:30:00,81.91,171.81099999999998,37.663000000000004,32.459 +2020-01-12 22:45:00,80.26,168.46,37.663000000000004,32.459 +2020-01-12 23:00:00,77.38,160.747,31.945,32.459 +2020-01-12 23:15:00,76.18,157.165,31.945,32.459 +2020-01-12 23:30:00,75.03,154.933,31.945,32.459 +2020-01-12 23:45:00,75.75,152.011,31.945,32.459 +2020-01-13 00:00:00,70.95,133.061,31.533,32.641 +2020-01-13 00:15:00,70.9,131.047,31.533,32.641 +2020-01-13 00:30:00,70.47,133.298,31.533,32.641 +2020-01-13 00:45:00,69.99,135.843,31.533,32.641 +2020-01-13 01:00:00,67.64,138.91,30.56,32.641 +2020-01-13 01:15:00,66.93,139.731,30.56,32.641 +2020-01-13 01:30:00,67.33,139.983,30.56,32.641 +2020-01-13 01:45:00,67.11,140.019,30.56,32.641 +2020-01-13 02:00:00,66.76,142.105,29.55,32.641 +2020-01-13 02:15:00,67.45,144.125,29.55,32.641 +2020-01-13 02:30:00,67.68,145.453,29.55,32.641 +2020-01-13 02:45:00,67.43,147.497,29.55,32.641 +2020-01-13 03:00:00,67.66,151.507,27.059,32.641 +2020-01-13 03:15:00,68.77,153.741,27.059,32.641 +2020-01-13 03:30:00,68.92,155.1,27.059,32.641 +2020-01-13 03:45:00,68.84,156.29399999999998,27.059,32.641 +2020-01-13 04:00:00,70.83,168.38299999999998,28.384,32.641 +2020-01-13 04:15:00,73.73,180.68599999999998,28.384,32.641 +2020-01-13 04:30:00,73.75,184.06799999999998,28.384,32.641 +2020-01-13 04:45:00,77.08,185.655,28.384,32.641 +2020-01-13 05:00:00,81.53,215.06400000000002,35.915,32.641 +2020-01-13 05:15:00,84.06,243.69099999999997,35.915,32.641 +2020-01-13 05:30:00,88.5,241.104,35.915,32.641 +2020-01-13 05:45:00,94.62,233.94400000000002,35.915,32.641 +2020-01-13 06:00:00,104.44,232.146,56.18,32.641 +2020-01-13 06:15:00,112.23,236.292,56.18,32.641 +2020-01-13 06:30:00,114.44,240.037,56.18,32.641 +2020-01-13 06:45:00,119.14,244.743,56.18,32.641 +2020-01-13 07:00:00,126.22,243.983,70.877,32.641 +2020-01-13 07:15:00,129.2,249.511,70.877,32.641 +2020-01-13 07:30:00,131.57,253.109,70.877,32.641 +2020-01-13 07:45:00,131.68,254.268,70.877,32.641 +2020-01-13 08:00:00,135.22,252.912,65.65,32.641 +2020-01-13 08:15:00,134.08,254.079,65.65,32.641 +2020-01-13 08:30:00,135.2,252.06799999999998,65.65,32.641 +2020-01-13 08:45:00,132.28,249.106,65.65,32.641 +2020-01-13 09:00:00,133.04,243.158,62.037,32.641 +2020-01-13 09:15:00,133.98,238.296,62.037,32.641 +2020-01-13 09:30:00,132.22,235.535,62.037,32.641 +2020-01-13 09:45:00,130.3,232.66299999999998,62.037,32.641 +2020-01-13 10:00:00,129.71,227.908,60.409,32.641 +2020-01-13 10:15:00,129.27,224.858,60.409,32.641 +2020-01-13 10:30:00,127.57,221.285,60.409,32.641 +2020-01-13 10:45:00,130.5,219.388,60.409,32.641 +2020-01-13 11:00:00,125.92,215.54,60.211999999999996,32.641 +2020-01-13 11:15:00,126.17,214.65,60.211999999999996,32.641 +2020-01-13 11:30:00,125.52,214.301,60.211999999999996,32.641 +2020-01-13 11:45:00,127.51,212.32299999999998,60.211999999999996,32.641 +2020-01-13 12:00:00,124.16,208.769,57.733000000000004,32.641 +2020-01-13 12:15:00,123.73,208.793,57.733000000000004,32.641 +2020-01-13 12:30:00,123.65,207.995,57.733000000000004,32.641 +2020-01-13 12:45:00,119.92,209.227,57.733000000000004,32.641 +2020-01-13 13:00:00,118.02,207.935,58.695,32.641 +2020-01-13 13:15:00,118.69,207.868,58.695,32.641 +2020-01-13 13:30:00,116.48,206.155,58.695,32.641 +2020-01-13 13:45:00,117.81,205.75,58.695,32.641 +2020-01-13 14:00:00,124.13,204.56,59.505,32.641 +2020-01-13 14:15:00,122.13,204.937,59.505,32.641 +2020-01-13 14:30:00,125.07,205.237,59.505,32.641 +2020-01-13 14:45:00,121.35,205.65,59.505,32.641 +2020-01-13 15:00:00,123.98,207.451,59.946000000000005,32.641 +2020-01-13 15:15:00,125.58,207.59400000000002,59.946000000000005,32.641 +2020-01-13 15:30:00,124.7,209.69,59.946000000000005,32.641 +2020-01-13 15:45:00,124.42,211.794,59.946000000000005,32.641 +2020-01-13 16:00:00,126.95,212.16400000000002,61.766999999999996,32.641 +2020-01-13 16:15:00,128.1,213.343,61.766999999999996,32.641 +2020-01-13 16:30:00,133.84,215.56,61.766999999999996,32.641 +2020-01-13 16:45:00,134.97,216.93400000000003,61.766999999999996,32.641 +2020-01-13 17:00:00,137.72,218.595,67.85600000000001,32.641 +2020-01-13 17:15:00,139.22,219.562,67.85600000000001,32.641 +2020-01-13 17:30:00,140.31,219.893,67.85600000000001,32.641 +2020-01-13 17:45:00,138.79,219.953,67.85600000000001,32.641 +2020-01-13 18:00:00,138.41,221.658,64.564,32.641 +2020-01-13 18:15:00,136.09,219.475,64.564,32.641 +2020-01-13 18:30:00,135.55,218.57299999999998,64.564,32.641 +2020-01-13 18:45:00,135.23,217.93200000000002,64.564,32.641 +2020-01-13 19:00:00,132.57,217.167,58.536,32.641 +2020-01-13 19:15:00,131.58,213.516,58.536,32.641 +2020-01-13 19:30:00,129.12,211.56400000000002,58.536,32.641 +2020-01-13 19:45:00,134.32,208.04,58.536,32.641 +2020-01-13 20:00:00,128.64,203.938,59.888999999999996,32.641 +2020-01-13 20:15:00,125.25,198.085,59.888999999999996,32.641 +2020-01-13 20:30:00,116.68,192.804,59.888999999999996,32.641 +2020-01-13 20:45:00,117.43,191.696,59.888999999999996,32.641 +2020-01-13 21:00:00,109.83,189.396,52.652,32.641 +2020-01-13 21:15:00,114.34,185.62,52.652,32.641 +2020-01-13 21:30:00,113.42,184.334,52.652,32.641 +2020-01-13 21:45:00,107.41,182.62400000000002,52.652,32.641 +2020-01-13 22:00:00,100.72,173.732,46.17,32.641 +2020-01-13 22:15:00,98.52,168.285,46.17,32.641 +2020-01-13 22:30:00,100.74,154.417,46.17,32.641 +2020-01-13 22:45:00,100.33,145.899,46.17,32.641 +2020-01-13 23:00:00,94.57,139.002,36.281,32.641 +2020-01-13 23:15:00,89.14,138.363,36.281,32.641 +2020-01-13 23:30:00,91.26,139.106,36.281,32.641 +2020-01-13 23:45:00,91.53,139.046,36.281,32.641 +2020-01-14 00:00:00,87.82,132.558,38.821999999999996,32.641 +2020-01-14 00:15:00,81.82,132.01,38.821999999999996,32.641 +2020-01-14 00:30:00,84.32,133.167,38.821999999999996,32.641 +2020-01-14 00:45:00,87.32,134.571,38.821999999999996,32.641 +2020-01-14 01:00:00,84.42,137.464,36.936,32.641 +2020-01-14 01:15:00,79.89,137.782,36.936,32.641 +2020-01-14 01:30:00,77.82,138.22899999999998,36.936,32.641 +2020-01-14 01:45:00,79.93,138.631,36.936,32.641 +2020-01-14 02:00:00,82.47,140.761,34.42,32.641 +2020-01-14 02:15:00,81.18,142.563,34.42,32.641 +2020-01-14 02:30:00,78.27,143.282,34.42,32.641 +2020-01-14 02:45:00,83.03,145.312,34.42,32.641 +2020-01-14 03:00:00,82.63,148.03799999999998,33.585,32.641 +2020-01-14 03:15:00,80.68,149.27700000000002,33.585,32.641 +2020-01-14 03:30:00,74.84,151.167,33.585,32.641 +2020-01-14 03:45:00,75.84,152.67600000000002,33.585,32.641 +2020-01-14 04:00:00,76.53,164.63299999999998,35.622,32.641 +2020-01-14 04:15:00,77.34,176.54,35.622,32.641 +2020-01-14 04:30:00,78.06,179.59400000000002,35.622,32.641 +2020-01-14 04:45:00,80.59,182.463,35.622,32.641 +2020-01-14 05:00:00,83.73,217.205,40.599000000000004,32.641 +2020-01-14 05:15:00,86.8,245.519,40.599000000000004,32.641 +2020-01-14 05:30:00,90.08,241.22400000000002,40.599000000000004,32.641 +2020-01-14 05:45:00,95.43,234.15900000000002,40.599000000000004,32.641 +2020-01-14 06:00:00,104.29,230.98,55.203,32.641 +2020-01-14 06:15:00,109.76,236.90900000000002,55.203,32.641 +2020-01-14 06:30:00,114.01,239.98,55.203,32.641 +2020-01-14 06:45:00,118.59,244.38,55.203,32.641 +2020-01-14 07:00:00,125.25,243.42,69.029,32.641 +2020-01-14 07:15:00,128.89,248.783,69.029,32.641 +2020-01-14 07:30:00,129.58,251.75599999999997,69.029,32.641 +2020-01-14 07:45:00,135.04,253.217,69.029,32.641 +2020-01-14 08:00:00,137.03,251.979,65.85300000000001,32.641 +2020-01-14 08:15:00,136.03,252.045,65.85300000000001,32.641 +2020-01-14 08:30:00,135.16,249.79,65.85300000000001,32.641 +2020-01-14 08:45:00,132.47,246.59900000000002,65.85300000000001,32.641 +2020-01-14 09:00:00,135.18,239.695,61.566,32.641 +2020-01-14 09:15:00,137.97,236.602,61.566,32.641 +2020-01-14 09:30:00,137.36,234.543,61.566,32.641 +2020-01-14 09:45:00,138.26,231.315,61.566,32.641 +2020-01-14 10:00:00,140.95,226.03599999999997,61.244,32.641 +2020-01-14 10:15:00,144.05,221.83900000000003,61.244,32.641 +2020-01-14 10:30:00,146.07,218.455,61.244,32.641 +2020-01-14 10:45:00,144.01,216.787,61.244,32.641 +2020-01-14 11:00:00,132.46,214.55200000000002,61.16,32.641 +2020-01-14 11:15:00,135.5,213.266,61.16,32.641 +2020-01-14 11:30:00,136.58,211.74099999999999,61.16,32.641 +2020-01-14 11:45:00,138.0,210.571,61.16,32.641 +2020-01-14 12:00:00,137.63,205.547,59.09,32.641 +2020-01-14 12:15:00,135.35,205.107,59.09,32.641 +2020-01-14 12:30:00,134.93,205.018,59.09,32.641 +2020-01-14 12:45:00,134.94,205.87400000000002,59.09,32.641 +2020-01-14 13:00:00,133.76,204.162,60.21,32.641 +2020-01-14 13:15:00,131.09,203.554,60.21,32.641 +2020-01-14 13:30:00,129.61,203.111,60.21,32.641 +2020-01-14 13:45:00,134.96,202.985,60.21,32.641 +2020-01-14 14:00:00,133.08,202.09599999999998,60.673,32.641 +2020-01-14 14:15:00,132.64,202.65099999999998,60.673,32.641 +2020-01-14 14:30:00,131.69,203.612,60.673,32.641 +2020-01-14 14:45:00,133.24,204.018,60.673,32.641 +2020-01-14 15:00:00,134.24,205.382,62.232,32.641 +2020-01-14 15:15:00,133.79,205.793,62.232,32.641 +2020-01-14 15:30:00,132.79,208.138,62.232,32.641 +2020-01-14 15:45:00,132.99,209.766,62.232,32.641 +2020-01-14 16:00:00,134.62,210.56599999999997,63.611999999999995,32.641 +2020-01-14 16:15:00,133.49,212.261,63.611999999999995,32.641 +2020-01-14 16:30:00,136.5,215.21200000000002,63.611999999999995,32.641 +2020-01-14 16:45:00,140.39,216.851,63.611999999999995,32.641 +2020-01-14 17:00:00,141.25,219.071,70.658,32.641 +2020-01-14 17:15:00,141.82,220.072,70.658,32.641 +2020-01-14 17:30:00,142.91,221.18200000000002,70.658,32.641 +2020-01-14 17:45:00,141.94,221.146,70.658,32.641 +2020-01-14 18:00:00,140.95,222.817,68.361,32.641 +2020-01-14 18:15:00,139.33,220.011,68.361,32.641 +2020-01-14 18:30:00,138.51,218.8,68.361,32.641 +2020-01-14 18:45:00,138.78,219.049,68.361,32.641 +2020-01-14 19:00:00,134.6,218.407,62.922,32.641 +2020-01-14 19:15:00,133.06,214.447,62.922,32.641 +2020-01-14 19:30:00,130.38,211.797,62.922,32.641 +2020-01-14 19:45:00,135.52,208.298,62.922,32.641 +2020-01-14 20:00:00,131.04,204.324,63.251999999999995,32.641 +2020-01-14 20:15:00,124.9,197.88099999999997,63.251999999999995,32.641 +2020-01-14 20:30:00,119.8,193.68200000000002,63.251999999999995,32.641 +2020-01-14 20:45:00,113.77,191.946,63.251999999999995,32.641 +2020-01-14 21:00:00,111.04,188.81599999999997,54.47,32.641 +2020-01-14 21:15:00,115.36,186.09599999999998,54.47,32.641 +2020-01-14 21:30:00,114.69,184.012,54.47,32.641 +2020-01-14 21:45:00,110.23,182.55,54.47,32.641 +2020-01-14 22:00:00,103.08,175.484,51.12,32.641 +2020-01-14 22:15:00,98.43,169.798,51.12,32.641 +2020-01-14 22:30:00,97.92,156.011,51.12,32.641 +2020-01-14 22:45:00,100.32,147.8,51.12,32.641 +2020-01-14 23:00:00,96.47,140.96200000000002,42.156000000000006,32.641 +2020-01-14 23:15:00,92.03,139.256,42.156000000000006,32.641 +2020-01-14 23:30:00,86.85,139.628,42.156000000000006,32.641 +2020-01-14 23:45:00,87.06,139.088,42.156000000000006,32.641 +2020-01-15 00:00:00,86.86,132.626,37.192,32.641 +2020-01-15 00:15:00,87.23,132.058,37.192,32.641 +2020-01-15 00:30:00,85.9,133.19799999999998,37.192,32.641 +2020-01-15 00:45:00,82.51,134.589,37.192,32.641 +2020-01-15 01:00:00,83.73,137.483,32.24,32.641 +2020-01-15 01:15:00,83.66,137.791,32.24,32.641 +2020-01-15 01:30:00,80.75,138.233,32.24,32.641 +2020-01-15 01:45:00,76.71,138.627,32.24,32.641 +2020-01-15 02:00:00,83.36,140.768,30.34,32.641 +2020-01-15 02:15:00,84.01,142.57,30.34,32.641 +2020-01-15 02:30:00,81.99,143.299,30.34,32.641 +2020-01-15 02:45:00,77.46,145.328,30.34,32.641 +2020-01-15 03:00:00,75.23,148.05,29.129,32.641 +2020-01-15 03:15:00,76.54,149.305,29.129,32.641 +2020-01-15 03:30:00,77.67,151.195,29.129,32.641 +2020-01-15 03:45:00,77.78,152.713,29.129,32.641 +2020-01-15 04:00:00,83.87,164.64700000000002,30.075,32.641 +2020-01-15 04:15:00,85.62,176.545,30.075,32.641 +2020-01-15 04:30:00,87.03,179.59900000000002,30.075,32.641 +2020-01-15 04:45:00,87.26,182.463,30.075,32.641 +2020-01-15 05:00:00,92.08,217.16400000000002,35.684,32.641 +2020-01-15 05:15:00,95.05,245.451,35.684,32.641 +2020-01-15 05:30:00,93.94,241.158,35.684,32.641 +2020-01-15 05:45:00,97.28,234.112,35.684,32.641 +2020-01-15 06:00:00,105.8,230.955,51.49,32.641 +2020-01-15 06:15:00,110.68,236.893,51.49,32.641 +2020-01-15 06:30:00,116.49,239.972,51.49,32.641 +2020-01-15 06:45:00,120.13,244.398,51.49,32.641 +2020-01-15 07:00:00,127.25,243.456,68.242,32.641 +2020-01-15 07:15:00,130.04,248.804,68.242,32.641 +2020-01-15 07:30:00,133.29,251.75599999999997,68.242,32.641 +2020-01-15 07:45:00,136.04,253.19299999999998,68.242,32.641 +2020-01-15 08:00:00,138.64,251.947,63.619,32.641 +2020-01-15 08:15:00,135.35,251.99599999999998,63.619,32.641 +2020-01-15 08:30:00,135.07,249.706,63.619,32.641 +2020-01-15 08:45:00,135.36,246.498,63.619,32.641 +2020-01-15 09:00:00,138.48,239.581,61.333,32.641 +2020-01-15 09:15:00,137.35,236.493,61.333,32.641 +2020-01-15 09:30:00,139.08,234.452,61.333,32.641 +2020-01-15 09:45:00,139.42,231.21900000000002,61.333,32.641 +2020-01-15 10:00:00,139.42,225.94299999999998,59.663000000000004,32.641 +2020-01-15 10:15:00,139.9,221.75400000000002,59.663000000000004,32.641 +2020-01-15 10:30:00,142.04,218.365,59.663000000000004,32.641 +2020-01-15 10:45:00,143.07,216.702,59.663000000000004,32.641 +2020-01-15 11:00:00,139.95,214.44799999999998,59.771,32.641 +2020-01-15 11:15:00,139.24,213.165,59.771,32.641 +2020-01-15 11:30:00,137.7,211.642,59.771,32.641 +2020-01-15 11:45:00,137.77,210.477,59.771,32.641 +2020-01-15 12:00:00,137.15,205.465,58.723,32.641 +2020-01-15 12:15:00,135.27,205.044,58.723,32.641 +2020-01-15 12:30:00,135.67,204.94400000000002,58.723,32.641 +2020-01-15 12:45:00,137.77,205.801,58.723,32.641 +2020-01-15 13:00:00,136.15,204.08700000000002,58.727,32.641 +2020-01-15 13:15:00,136.78,203.465,58.727,32.641 +2020-01-15 13:30:00,136.5,203.01,58.727,32.641 +2020-01-15 13:45:00,136.87,202.877,58.727,32.641 +2020-01-15 14:00:00,136.85,202.014,59.803999999999995,32.641 +2020-01-15 14:15:00,134.91,202.55900000000003,59.803999999999995,32.641 +2020-01-15 14:30:00,134.1,203.52200000000002,59.803999999999995,32.641 +2020-01-15 14:45:00,135.12,203.94400000000002,59.803999999999995,32.641 +2020-01-15 15:00:00,135.73,205.327,61.05,32.641 +2020-01-15 15:15:00,134.79,205.715,61.05,32.641 +2020-01-15 15:30:00,132.55,208.048,61.05,32.641 +2020-01-15 15:45:00,131.89,209.665,61.05,32.641 +2020-01-15 16:00:00,132.28,210.465,64.012,32.641 +2020-01-15 16:15:00,134.26,212.166,64.012,32.641 +2020-01-15 16:30:00,136.16,215.123,64.012,32.641 +2020-01-15 16:45:00,138.74,216.766,64.012,32.641 +2020-01-15 17:00:00,141.87,218.972,66.751,32.641 +2020-01-15 17:15:00,142.23,220.003,66.751,32.641 +2020-01-15 17:30:00,143.82,221.144,66.751,32.641 +2020-01-15 17:45:00,143.42,221.12900000000002,66.751,32.641 +2020-01-15 18:00:00,142.04,222.82,65.91199999999999,32.641 +2020-01-15 18:15:00,140.16,220.03099999999998,65.91199999999999,32.641 +2020-01-15 18:30:00,139.4,218.824,65.91199999999999,32.641 +2020-01-15 18:45:00,140.52,219.09400000000002,65.91199999999999,32.641 +2020-01-15 19:00:00,137.94,218.416,63.324,32.641 +2020-01-15 19:15:00,133.64,214.46099999999998,63.324,32.641 +2020-01-15 19:30:00,130.94,211.817,63.324,32.641 +2020-01-15 19:45:00,129.29,208.326,63.324,32.641 +2020-01-15 20:00:00,123.9,204.331,63.573,32.641 +2020-01-15 20:15:00,121.7,197.892,63.573,32.641 +2020-01-15 20:30:00,116.5,193.687,63.573,32.641 +2020-01-15 20:45:00,115.14,191.967,63.573,32.641 +2020-01-15 21:00:00,110.98,188.81799999999998,55.073,32.641 +2020-01-15 21:15:00,115.16,186.081,55.073,32.641 +2020-01-15 21:30:00,114.07,183.99599999999998,55.073,32.641 +2020-01-15 21:45:00,110.47,182.551,55.073,32.641 +2020-01-15 22:00:00,101.67,175.48,51.321999999999996,32.641 +2020-01-15 22:15:00,100.33,169.81099999999998,51.321999999999996,32.641 +2020-01-15 22:30:00,96.77,156.029,51.321999999999996,32.641 +2020-01-15 22:45:00,95.32,147.825,51.321999999999996,32.641 +2020-01-15 23:00:00,97.65,140.968,42.09,32.641 +2020-01-15 23:15:00,96.42,139.27200000000002,42.09,32.641 +2020-01-15 23:30:00,91.99,139.661,42.09,32.641 +2020-01-15 23:45:00,86.16,139.125,42.09,32.641 +2020-01-16 00:00:00,87.33,139.56,38.399,32.641 +2020-01-16 00:15:00,88.49,139.33,38.399,32.641 +2020-01-16 00:30:00,88.26,140.88299999999998,38.399,32.641 +2020-01-16 00:45:00,83.0,142.724,38.399,32.641 +2020-01-16 01:00:00,83.99,145.79399999999998,36.94,32.641 +2020-01-16 01:15:00,85.07,145.787,36.94,32.641 +2020-01-16 01:30:00,84.84,146.149,36.94,32.641 +2020-01-16 01:45:00,79.18,146.644,36.94,32.641 +2020-01-16 02:00:00,80.14,148.93200000000002,35.275,32.641 +2020-01-16 02:15:00,85.15,151.02200000000002,35.275,32.641 +2020-01-16 02:30:00,84.32,152.084,35.275,32.641 +2020-01-16 02:45:00,82.42,154.27100000000002,35.275,32.641 +2020-01-16 03:00:00,77.71,157.27,35.329,32.641 +2020-01-16 03:15:00,84.16,158.311,35.329,32.641 +2020-01-16 03:30:00,85.53,160.137,35.329,32.641 +2020-01-16 03:45:00,85.45,162.055,35.329,32.641 +2020-01-16 04:00:00,78.58,173.658,36.275,32.641 +2020-01-16 04:15:00,79.47,185.22099999999998,36.275,32.641 +2020-01-16 04:30:00,81.13,188.989,36.275,32.641 +2020-01-16 04:45:00,82.77,192.137,36.275,32.641 +2020-01-16 05:00:00,86.6,227.294,42.193999999999996,32.641 +2020-01-16 05:15:00,89.37,255.28799999999998,42.193999999999996,32.641 +2020-01-16 05:30:00,93.2,250.99099999999999,42.193999999999996,32.641 +2020-01-16 05:45:00,98.25,244.456,42.193999999999996,32.641 +2020-01-16 06:00:00,106.36,241.44099999999997,56.422,32.641 +2020-01-16 06:15:00,111.91,247.579,56.422,32.641 +2020-01-16 06:30:00,115.81,250.795,56.422,32.641 +2020-01-16 06:45:00,121.56,255.86900000000003,56.422,32.641 +2020-01-16 07:00:00,125.43,254.107,72.569,32.641 +2020-01-16 07:15:00,128.69,259.99,72.569,32.641 +2020-01-16 07:30:00,132.2,263.454,72.569,32.641 +2020-01-16 07:45:00,136.04,265.41200000000003,72.569,32.641 +2020-01-16 08:00:00,137.28,264.069,67.704,32.641 +2020-01-16 08:15:00,136.74,264.55400000000003,67.704,32.641 +2020-01-16 08:30:00,137.81,262.16900000000004,67.704,32.641 +2020-01-16 08:45:00,135.89,259.421,67.704,32.641 +2020-01-16 09:00:00,137.97,252.798,63.434,32.641 +2020-01-16 09:15:00,140.1,249.953,63.434,32.641 +2020-01-16 09:30:00,141.78,248.072,63.434,32.641 +2020-01-16 09:45:00,141.49,244.673,63.434,32.641 +2020-01-16 10:00:00,142.24,238.75599999999997,61.88399999999999,32.641 +2020-01-16 10:15:00,140.48,234.90400000000002,61.88399999999999,32.641 +2020-01-16 10:30:00,140.15,230.903,61.88399999999999,32.641 +2020-01-16 10:45:00,140.59,229.00799999999998,61.88399999999999,32.641 +2020-01-16 11:00:00,140.53,225.988,61.481,32.641 +2020-01-16 11:15:00,141.45,224.646,61.481,32.641 +2020-01-16 11:30:00,141.71,222.722,61.481,32.641 +2020-01-16 11:45:00,139.21,222.058,61.481,32.641 +2020-01-16 12:00:00,136.2,218.021,59.527,32.641 +2020-01-16 12:15:00,135.53,217.612,59.527,32.641 +2020-01-16 12:30:00,135.7,217.34900000000002,59.527,32.641 +2020-01-16 12:45:00,136.9,217.868,59.527,32.641 +2020-01-16 13:00:00,135.4,215.40400000000002,58.794,32.641 +2020-01-16 13:15:00,134.78,214.74099999999999,58.794,32.641 +2020-01-16 13:30:00,132.47,213.917,58.794,32.641 +2020-01-16 13:45:00,132.22,214.22,58.794,32.641 +2020-01-16 14:00:00,128.13,213.833,60.32,32.641 +2020-01-16 14:15:00,126.7,214.40200000000002,60.32,32.641 +2020-01-16 14:30:00,124.84,215.28599999999997,60.32,32.641 +2020-01-16 14:45:00,124.2,215.984,60.32,32.641 +2020-01-16 15:00:00,125.48,216.856,62.52,32.641 +2020-01-16 15:15:00,125.05,217.75799999999998,62.52,32.641 +2020-01-16 15:30:00,126.77,219.547,62.52,32.641 +2020-01-16 15:45:00,129.95,220.50099999999998,62.52,32.641 +2020-01-16 16:00:00,132.47,222.225,64.199,32.641 +2020-01-16 16:15:00,131.37,224.09099999999998,64.199,32.641 +2020-01-16 16:30:00,133.46,226.852,64.199,32.641 +2020-01-16 16:45:00,136.14,228.108,64.199,32.641 +2020-01-16 17:00:00,140.04,230.606,68.19800000000001,32.641 +2020-01-16 17:15:00,139.73,231.262,68.19800000000001,32.641 +2020-01-16 17:30:00,140.43,232.16,68.19800000000001,32.641 +2020-01-16 17:45:00,140.42,231.972,68.19800000000001,32.641 +2020-01-16 18:00:00,138.81,233.65400000000002,67.899,32.641 +2020-01-16 18:15:00,136.53,230.218,67.899,32.641 +2020-01-16 18:30:00,137.16,229.18400000000003,67.899,32.641 +2020-01-16 18:45:00,136.92,229.485,67.899,32.641 +2020-01-16 19:00:00,133.32,227.865,64.72399999999999,32.641 +2020-01-16 19:15:00,131.23,223.705,64.72399999999999,32.641 +2020-01-16 19:30:00,133.3,220.62099999999998,64.72399999999999,32.641 +2020-01-16 19:45:00,135.03,217.56900000000002,64.72399999999999,32.641 +2020-01-16 20:00:00,127.62,213.46200000000002,64.062,32.641 +2020-01-16 20:15:00,119.26,206.58700000000002,64.062,32.641 +2020-01-16 20:30:00,116.31,202.333,64.062,32.641 +2020-01-16 20:45:00,111.77,201.076,64.062,32.641 +2020-01-16 21:00:00,108.51,197.449,57.971000000000004,32.641 +2020-01-16 21:15:00,112.3,194.61,57.971000000000004,32.641 +2020-01-16 21:30:00,110.27,192.56900000000002,57.971000000000004,32.641 +2020-01-16 21:45:00,106.27,190.99400000000003,57.971000000000004,32.641 +2020-01-16 22:00:00,97.28,183.658,53.715,32.641 +2020-01-16 22:15:00,96.36,177.688,53.715,32.641 +2020-01-16 22:30:00,94.99,163.819,53.715,32.641 +2020-01-16 22:45:00,98.2,155.327,53.715,32.641 +2020-01-16 23:00:00,94.28,147.961,47.8,32.641 +2020-01-16 23:15:00,91.98,146.525,47.8,32.641 +2020-01-16 23:30:00,84.84,146.689,47.8,32.641 +2020-01-16 23:45:00,82.59,146.202,47.8,32.641 +2020-01-17 00:00:00,85.95,138.651,43.656000000000006,32.641 +2020-01-17 00:15:00,86.53,138.59799999999998,43.656000000000006,32.641 +2020-01-17 00:30:00,85.43,139.931,43.656000000000006,32.641 +2020-01-17 00:45:00,79.36,141.822,43.656000000000006,32.641 +2020-01-17 01:00:00,80.97,144.585,41.263000000000005,32.641 +2020-01-17 01:15:00,83.5,145.747,41.263000000000005,32.641 +2020-01-17 01:30:00,83.45,145.722,41.263000000000005,32.641 +2020-01-17 01:45:00,80.4,146.374,41.263000000000005,32.641 +2020-01-17 02:00:00,78.23,148.619,40.799,32.641 +2020-01-17 02:15:00,74.91,150.584,40.799,32.641 +2020-01-17 02:30:00,73.54,152.134,40.799,32.641 +2020-01-17 02:45:00,74.37,154.489,40.799,32.641 +2020-01-17 03:00:00,79.04,156.16899999999998,41.398,32.641 +2020-01-17 03:15:00,83.49,158.55200000000002,41.398,32.641 +2020-01-17 03:30:00,84.6,160.399,41.398,32.641 +2020-01-17 03:45:00,82.01,162.561,41.398,32.641 +2020-01-17 04:00:00,77.58,174.391,42.38,32.641 +2020-01-17 04:15:00,78.35,185.93099999999998,42.38,32.641 +2020-01-17 04:30:00,79.29,189.803,42.38,32.641 +2020-01-17 04:45:00,82.16,191.699,42.38,32.641 +2020-01-17 05:00:00,85.97,225.387,46.181000000000004,32.641 +2020-01-17 05:15:00,87.85,254.97799999999998,46.181000000000004,32.641 +2020-01-17 05:30:00,94.06,251.938,46.181000000000004,32.641 +2020-01-17 05:45:00,97.27,245.42700000000002,46.181000000000004,32.641 +2020-01-17 06:00:00,106.38,242.907,59.33,32.641 +2020-01-17 06:15:00,111.42,247.27,59.33,32.641 +2020-01-17 06:30:00,115.82,249.47,59.33,32.641 +2020-01-17 06:45:00,119.88,256.536,59.33,32.641 +2020-01-17 07:00:00,127.7,253.679,72.454,32.641 +2020-01-17 07:15:00,130.26,260.601,72.454,32.641 +2020-01-17 07:30:00,132.73,264.178,72.454,32.641 +2020-01-17 07:45:00,134.99,265.091,72.454,32.641 +2020-01-17 08:00:00,136.8,262.293,67.175,32.641 +2020-01-17 08:15:00,135.31,262.188,67.175,32.641 +2020-01-17 08:30:00,136.28,260.932,67.175,32.641 +2020-01-17 08:45:00,136.61,256.327,67.175,32.641 +2020-01-17 09:00:00,136.32,250.61700000000002,65.365,32.641 +2020-01-17 09:15:00,137.58,248.14,65.365,32.641 +2020-01-17 09:30:00,139.47,245.88400000000001,65.365,32.641 +2020-01-17 09:45:00,139.06,242.29,65.365,32.641 +2020-01-17 10:00:00,137.15,235.06400000000002,63.95,32.641 +2020-01-17 10:15:00,136.31,232.081,63.95,32.641 +2020-01-17 10:30:00,137.15,227.899,63.95,32.641 +2020-01-17 10:45:00,138.46,225.50599999999997,63.95,32.641 +2020-01-17 11:00:00,138.45,222.419,63.92100000000001,32.641 +2020-01-17 11:15:00,140.66,220.19799999999998,63.92100000000001,32.641 +2020-01-17 11:30:00,142.55,220.382,63.92100000000001,32.641 +2020-01-17 11:45:00,141.43,219.925,63.92100000000001,32.641 +2020-01-17 12:00:00,140.64,217.108,60.79600000000001,32.641 +2020-01-17 12:15:00,141.7,214.36599999999999,60.79600000000001,32.641 +2020-01-17 12:30:00,140.79,214.257,60.79600000000001,32.641 +2020-01-17 12:45:00,140.1,215.50099999999998,60.79600000000001,32.641 +2020-01-17 13:00:00,138.62,214.043,59.393,32.641 +2020-01-17 13:15:00,137.23,214.293,59.393,32.641 +2020-01-17 13:30:00,134.36,213.351,59.393,32.641 +2020-01-17 13:45:00,134.11,213.528,59.393,32.641 +2020-01-17 14:00:00,133.51,211.956,57.943999999999996,32.641 +2020-01-17 14:15:00,133.72,212.239,57.943999999999996,32.641 +2020-01-17 14:30:00,131.64,213.493,57.943999999999996,32.641 +2020-01-17 14:45:00,132.21,214.667,57.943999999999996,32.641 +2020-01-17 15:00:00,131.29,215.00400000000002,60.153999999999996,32.641 +2020-01-17 15:15:00,129.71,215.408,60.153999999999996,32.641 +2020-01-17 15:30:00,128.64,215.49900000000002,60.153999999999996,32.641 +2020-01-17 15:45:00,127.96,216.497,60.153999999999996,32.641 +2020-01-17 16:00:00,129.48,217.007,62.933,32.641 +2020-01-17 16:15:00,129.9,219.145,62.933,32.641 +2020-01-17 16:30:00,132.34,222.05,62.933,32.641 +2020-01-17 16:45:00,137.1,223.28400000000002,62.933,32.641 +2020-01-17 17:00:00,139.8,225.78599999999997,68.657,32.641 +2020-01-17 17:15:00,138.22,226.025,68.657,32.641 +2020-01-17 17:30:00,139.12,226.56900000000002,68.657,32.641 +2020-01-17 17:45:00,139.16,226.158,68.657,32.641 +2020-01-17 18:00:00,138.22,228.674,67.111,32.641 +2020-01-17 18:15:00,136.69,224.929,67.111,32.641 +2020-01-17 18:30:00,135.95,224.361,67.111,32.641 +2020-01-17 18:45:00,135.85,224.62599999999998,67.111,32.641 +2020-01-17 19:00:00,132.67,223.908,62.434,32.641 +2020-01-17 19:15:00,129.94,221.23,62.434,32.641 +2020-01-17 19:30:00,127.9,217.68,62.434,32.641 +2020-01-17 19:45:00,126.36,214.235,62.434,32.641 +2020-01-17 20:00:00,119.39,210.176,61.763000000000005,32.641 +2020-01-17 20:15:00,116.31,203.215,61.763000000000005,32.641 +2020-01-17 20:30:00,112.3,198.968,61.763000000000005,32.641 +2020-01-17 20:45:00,109.84,198.465,61.763000000000005,32.641 +2020-01-17 21:00:00,104.58,195.234,56.785,32.641 +2020-01-17 21:15:00,101.37,192.667,56.785,32.641 +2020-01-17 21:30:00,99.26,190.696,56.785,32.641 +2020-01-17 21:45:00,97.84,189.733,56.785,32.641 +2020-01-17 22:00:00,96.2,183.52700000000002,52.693000000000005,32.641 +2020-01-17 22:15:00,91.12,177.456,52.693000000000005,32.641 +2020-01-17 22:30:00,88.41,170.262,52.693000000000005,32.641 +2020-01-17 22:45:00,87.24,165.695,52.693000000000005,32.641 +2020-01-17 23:00:00,83.19,157.593,45.443999999999996,32.641 +2020-01-17 23:15:00,82.01,154.141,45.443999999999996,32.641 +2020-01-17 23:30:00,80.42,152.884,45.443999999999996,32.641 +2020-01-17 23:45:00,79.42,151.662,45.443999999999996,32.641 +2020-01-18 00:00:00,75.81,135.17700000000002,44.738,32.459 +2020-01-18 00:15:00,73.43,130.239,44.738,32.459 +2020-01-18 00:30:00,72.12,133.179,44.738,32.459 +2020-01-18 00:45:00,70.8,136.001,44.738,32.459 +2020-01-18 01:00:00,68.46,139.438,40.303000000000004,32.459 +2020-01-18 01:15:00,68.68,139.34,40.303000000000004,32.459 +2020-01-18 01:30:00,68.0,138.83100000000002,40.303000000000004,32.459 +2020-01-18 01:45:00,67.64,139.023,40.303000000000004,32.459 +2020-01-18 02:00:00,66.26,142.22299999999998,38.61,32.459 +2020-01-18 02:15:00,66.19,143.881,38.61,32.459 +2020-01-18 02:30:00,65.47,144.265,38.61,32.459 +2020-01-18 02:45:00,65.76,146.61,38.61,32.459 +2020-01-18 03:00:00,65.61,149.232,37.554,32.459 +2020-01-18 03:15:00,65.84,150.313,37.554,32.459 +2020-01-18 03:30:00,64.93,150.23,37.554,32.459 +2020-01-18 03:45:00,66.07,152.283,37.554,32.459 +2020-01-18 04:00:00,65.87,159.47899999999998,37.176,32.459 +2020-01-18 04:15:00,66.39,168.15,37.176,32.459 +2020-01-18 04:30:00,65.9,169.74400000000003,37.176,32.459 +2020-01-18 04:45:00,68.46,170.998,37.176,32.459 +2020-01-18 05:00:00,67.62,187.06400000000002,36.893,32.459 +2020-01-18 05:15:00,67.44,195.977,36.893,32.459 +2020-01-18 05:30:00,68.22,193.09,36.893,32.459 +2020-01-18 05:45:00,68.87,192.283,36.893,32.459 +2020-01-18 06:00:00,70.41,210.16299999999998,37.803000000000004,32.459 +2020-01-18 06:15:00,71.3,232.584,37.803000000000004,32.459 +2020-01-18 06:30:00,73.03,228.908,37.803000000000004,32.459 +2020-01-18 06:45:00,75.0,225.896,37.803000000000004,32.459 +2020-01-18 07:00:00,78.98,218.862,41.086999999999996,32.459 +2020-01-18 07:15:00,80.53,224.50599999999997,41.086999999999996,32.459 +2020-01-18 07:30:00,83.81,231.007,41.086999999999996,32.459 +2020-01-18 07:45:00,85.94,236.44799999999998,41.086999999999996,32.459 +2020-01-18 08:00:00,88.7,238.415,48.222,32.459 +2020-01-18 08:15:00,89.91,242.604,48.222,32.459 +2020-01-18 08:30:00,92.41,243.21,48.222,32.459 +2020-01-18 08:45:00,95.69,242.15099999999998,48.222,32.459 +2020-01-18 09:00:00,98.41,238.05700000000002,52.791000000000004,32.459 +2020-01-18 09:15:00,98.48,236.391,52.791000000000004,32.459 +2020-01-18 09:30:00,102.21,235.132,52.791000000000004,32.459 +2020-01-18 09:45:00,100.36,231.793,52.791000000000004,32.459 +2020-01-18 10:00:00,100.12,224.771,54.341,32.459 +2020-01-18 10:15:00,100.75,221.933,54.341,32.459 +2020-01-18 10:30:00,101.87,217.99599999999998,54.341,32.459 +2020-01-18 10:45:00,102.85,217.19299999999998,54.341,32.459 +2020-01-18 11:00:00,101.48,214.399,51.94,32.459 +2020-01-18 11:15:00,103.12,211.248,51.94,32.459 +2020-01-18 11:30:00,103.04,210.08900000000003,51.94,32.459 +2020-01-18 11:45:00,100.05,208.422,51.94,32.459 +2020-01-18 12:00:00,97.04,204.50599999999997,50.973,32.459 +2020-01-18 12:15:00,95.62,202.4,50.973,32.459 +2020-01-18 12:30:00,92.58,202.668,50.973,32.459 +2020-01-18 12:45:00,92.09,202.90200000000002,50.973,32.459 +2020-01-18 13:00:00,90.24,201.095,48.06399999999999,32.459 +2020-01-18 13:15:00,87.19,199.011,48.06399999999999,32.459 +2020-01-18 13:30:00,86.44,197.50099999999998,48.06399999999999,32.459 +2020-01-18 13:45:00,89.07,198.437,48.06399999999999,32.459 +2020-01-18 14:00:00,85.42,198.355,45.707,32.459 +2020-01-18 14:15:00,85.02,198.19299999999998,45.707,32.459 +2020-01-18 14:30:00,88.14,197.393,45.707,32.459 +2020-01-18 14:45:00,88.52,198.765,45.707,32.459 +2020-01-18 15:00:00,88.89,199.88299999999998,47.567,32.459 +2020-01-18 15:15:00,89.04,201.095,47.567,32.459 +2020-01-18 15:30:00,91.84,202.926,47.567,32.459 +2020-01-18 15:45:00,89.9,204.063,47.567,32.459 +2020-01-18 16:00:00,91.8,202.982,52.031000000000006,32.459 +2020-01-18 16:15:00,92.59,206.278,52.031000000000006,32.459 +2020-01-18 16:30:00,94.37,209.09900000000002,52.031000000000006,32.459 +2020-01-18 16:45:00,98.68,211.359,52.031000000000006,32.459 +2020-01-18 17:00:00,104.16,213.451,58.218999999999994,32.459 +2020-01-18 17:15:00,105.49,215.894,58.218999999999994,32.459 +2020-01-18 17:30:00,106.99,216.37900000000002,58.218999999999994,32.459 +2020-01-18 17:45:00,107.98,215.463,58.218999999999994,32.459 +2020-01-18 18:00:00,108.94,217.34,57.65,32.459 +2020-01-18 18:15:00,110.75,215.455,57.65,32.459 +2020-01-18 18:30:00,108.32,216.24099999999999,57.65,32.459 +2020-01-18 18:45:00,107.67,213.148,57.65,32.459 +2020-01-18 19:00:00,105.41,213.674,51.261,32.459 +2020-01-18 19:15:00,104.23,210.55700000000002,51.261,32.459 +2020-01-18 19:30:00,103.1,207.775,51.261,32.459 +2020-01-18 19:45:00,104.08,203.93900000000002,51.261,32.459 +2020-01-18 20:00:00,96.04,202.172,44.068000000000005,32.459 +2020-01-18 20:15:00,93.37,197.706,44.068000000000005,32.459 +2020-01-18 20:30:00,90.26,193.16400000000002,44.068000000000005,32.459 +2020-01-18 20:45:00,88.26,191.99400000000003,44.068000000000005,32.459 +2020-01-18 21:00:00,85.63,191.48,38.861,32.459 +2020-01-18 21:15:00,83.97,189.435,38.861,32.459 +2020-01-18 21:30:00,82.78,188.83599999999998,38.861,32.459 +2020-01-18 21:45:00,81.38,187.49400000000003,38.861,32.459 +2020-01-18 22:00:00,78.87,182.81099999999998,39.485,32.459 +2020-01-18 22:15:00,77.45,179.549,39.485,32.459 +2020-01-18 22:30:00,74.7,179.446,39.485,32.459 +2020-01-18 22:45:00,73.83,176.96,39.485,32.459 +2020-01-18 23:00:00,70.86,171.66400000000002,32.027,32.459 +2020-01-18 23:15:00,70.25,166.322,32.027,32.459 +2020-01-18 23:30:00,67.49,162.817,32.027,32.459 +2020-01-18 23:45:00,66.15,158.811,32.027,32.459 +2020-01-19 00:00:00,63.53,135.497,26.96,32.459 +2020-01-19 00:15:00,61.94,130.341,26.96,32.459 +2020-01-19 00:30:00,60.49,132.85,26.96,32.459 +2020-01-19 00:45:00,59.53,136.483,26.96,32.459 +2020-01-19 01:00:00,58.12,139.71,24.295,32.459 +2020-01-19 01:15:00,58.27,140.829,24.295,32.459 +2020-01-19 01:30:00,57.64,140.945,24.295,32.459 +2020-01-19 01:45:00,57.35,140.821,24.295,32.459 +2020-01-19 02:00:00,56.61,143.149,24.268,32.459 +2020-01-19 02:15:00,56.08,143.701,24.268,32.459 +2020-01-19 02:30:00,55.8,145.066,24.268,32.459 +2020-01-19 02:45:00,56.32,147.996,24.268,32.459 +2020-01-19 03:00:00,55.66,150.884,23.373,32.459 +2020-01-19 03:15:00,55.71,151.326,23.373,32.459 +2020-01-19 03:30:00,55.81,153.001,23.373,32.459 +2020-01-19 03:45:00,56.11,155.10299999999998,23.373,32.459 +2020-01-19 04:00:00,55.85,162.005,23.874000000000002,32.459 +2020-01-19 04:15:00,56.49,169.545,23.874000000000002,32.459 +2020-01-19 04:30:00,57.02,170.997,23.874000000000002,32.459 +2020-01-19 04:45:00,57.49,172.642,23.874000000000002,32.459 +2020-01-19 05:00:00,58.49,184.49599999999998,24.871,32.459 +2020-01-19 05:15:00,58.83,190.658,24.871,32.459 +2020-01-19 05:30:00,58.88,187.644,24.871,32.459 +2020-01-19 05:45:00,59.64,187.18900000000002,24.871,32.459 +2020-01-19 06:00:00,59.96,205.362,23.84,32.459 +2020-01-19 06:15:00,60.41,225.62599999999998,23.84,32.459 +2020-01-19 06:30:00,60.71,220.766,23.84,32.459 +2020-01-19 06:45:00,64.2,216.699,23.84,32.459 +2020-01-19 07:00:00,64.77,212.528,27.430999999999997,32.459 +2020-01-19 07:15:00,65.87,217.419,27.430999999999997,32.459 +2020-01-19 07:30:00,68.27,222.238,27.430999999999997,32.459 +2020-01-19 07:45:00,70.01,226.76,27.430999999999997,32.459 +2020-01-19 08:00:00,72.98,230.78099999999998,33.891999999999996,32.459 +2020-01-19 08:15:00,74.51,234.66099999999997,33.891999999999996,32.459 +2020-01-19 08:30:00,77.33,237.02900000000002,33.891999999999996,32.459 +2020-01-19 08:45:00,79.56,238.33,33.891999999999996,32.459 +2020-01-19 09:00:00,81.57,233.78099999999998,37.571,32.459 +2020-01-19 09:15:00,83.04,232.83599999999998,37.571,32.459 +2020-01-19 09:30:00,86.55,231.352,37.571,32.459 +2020-01-19 09:45:00,86.23,227.72299999999998,37.571,32.459 +2020-01-19 10:00:00,87.7,223.55200000000002,40.594,32.459 +2020-01-19 10:15:00,89.59,221.326,40.594,32.459 +2020-01-19 10:30:00,90.48,218.054,40.594,32.459 +2020-01-19 10:45:00,92.41,214.893,40.594,32.459 +2020-01-19 11:00:00,94.56,213.213,44.133,32.459 +2020-01-19 11:15:00,98.06,210.283,44.133,32.459 +2020-01-19 11:30:00,99.96,208.044,44.133,32.459 +2020-01-19 11:45:00,100.24,207.048,44.133,32.459 +2020-01-19 12:00:00,97.6,202.331,41.198,32.459 +2020-01-19 12:15:00,96.12,202.545,41.198,32.459 +2020-01-19 12:30:00,92.64,201.079,41.198,32.459 +2020-01-19 12:45:00,92.83,200.282,41.198,32.459 +2020-01-19 13:00:00,87.94,197.68400000000003,37.014,32.459 +2020-01-19 13:15:00,87.17,199.167,37.014,32.459 +2020-01-19 13:30:00,85.79,197.56099999999998,37.014,32.459 +2020-01-19 13:45:00,85.29,197.56599999999997,37.014,32.459 +2020-01-19 14:00:00,84.54,197.63099999999997,34.934,32.459 +2020-01-19 14:15:00,83.97,198.785,34.934,32.459 +2020-01-19 14:30:00,84.35,199.588,34.934,32.459 +2020-01-19 14:45:00,85.14,200.653,34.934,32.459 +2020-01-19 15:00:00,84.99,199.99,34.588,32.459 +2020-01-19 15:15:00,84.82,202.16,34.588,32.459 +2020-01-19 15:30:00,85.3,204.67700000000002,34.588,32.459 +2020-01-19 15:45:00,84.88,206.55900000000003,34.588,32.459 +2020-01-19 16:00:00,86.12,207.805,37.874,32.459 +2020-01-19 16:15:00,86.01,210.03400000000002,37.874,32.459 +2020-01-19 16:30:00,87.08,213.02700000000002,37.874,32.459 +2020-01-19 16:45:00,89.57,215.435,37.874,32.459 +2020-01-19 17:00:00,97.87,217.421,47.303999999999995,32.459 +2020-01-19 17:15:00,102.85,219.338,47.303999999999995,32.459 +2020-01-19 17:30:00,105.44,220.085,47.303999999999995,32.459 +2020-01-19 17:45:00,106.76,221.72400000000002,47.303999999999995,32.459 +2020-01-19 18:00:00,106.95,222.90200000000002,48.879,32.459 +2020-01-19 18:15:00,108.0,222.58700000000002,48.879,32.459 +2020-01-19 18:30:00,104.37,221.048,48.879,32.459 +2020-01-19 18:45:00,103.14,220.05900000000003,48.879,32.459 +2020-01-19 19:00:00,101.77,219.907,44.826,32.459 +2020-01-19 19:15:00,99.54,217.583,44.826,32.459 +2020-01-19 19:30:00,98.11,214.65400000000002,44.826,32.459 +2020-01-19 19:45:00,96.83,212.50400000000002,44.826,32.459 +2020-01-19 20:00:00,98.04,210.7,40.154,32.459 +2020-01-19 20:15:00,101.48,207.36900000000003,40.154,32.459 +2020-01-19 20:30:00,97.9,204.132,40.154,32.459 +2020-01-19 20:45:00,89.29,201.812,40.154,32.459 +2020-01-19 21:00:00,89.58,198.354,36.549,32.459 +2020-01-19 21:15:00,86.78,195.61900000000003,36.549,32.459 +2020-01-19 21:30:00,86.94,195.425,36.549,32.459 +2020-01-19 21:45:00,87.39,194.21200000000002,36.549,32.459 +2020-01-19 22:00:00,87.92,187.94099999999997,37.663000000000004,32.459 +2020-01-19 22:15:00,85.91,184.03,37.663000000000004,32.459 +2020-01-19 22:30:00,86.09,180.403,37.663000000000004,32.459 +2020-01-19 22:45:00,90.89,177.109,37.663000000000004,32.459 +2020-01-19 23:00:00,87.77,168.668,31.945,32.459 +2020-01-19 23:15:00,86.14,165.292,31.945,32.459 +2020-01-19 23:30:00,80.64,162.781,31.945,32.459 +2020-01-19 23:45:00,82.46,159.77200000000002,31.945,32.459 +2020-01-20 00:00:00,84.43,140.184,31.533,32.641 +2020-01-20 00:15:00,82.35,138.34799999999998,31.533,32.641 +2020-01-20 00:30:00,79.55,141.045,31.533,32.641 +2020-01-20 00:45:00,74.22,144.09799999999998,31.533,32.641 +2020-01-20 01:00:00,68.93,147.299,30.56,32.641 +2020-01-20 01:15:00,74.43,147.789,30.56,32.641 +2020-01-20 01:30:00,78.16,147.909,30.56,32.641 +2020-01-20 01:45:00,77.86,147.925,30.56,32.641 +2020-01-20 02:00:00,74.11,150.179,29.55,32.641 +2020-01-20 02:15:00,78.18,152.602,29.55,32.641 +2020-01-20 02:30:00,78.02,154.341,29.55,32.641 +2020-01-20 02:45:00,78.43,156.56799999999998,29.55,32.641 +2020-01-20 03:00:00,75.4,160.888,27.059,32.641 +2020-01-20 03:15:00,72.87,163.166,27.059,32.641 +2020-01-20 03:30:00,73.43,164.412,27.059,32.641 +2020-01-20 03:45:00,74.76,165.95,27.059,32.641 +2020-01-20 04:00:00,81.45,177.48,28.384,32.641 +2020-01-20 04:15:00,80.59,189.41,28.384,32.641 +2020-01-20 04:30:00,78.24,193.53599999999997,28.384,32.641 +2020-01-20 04:45:00,80.69,195.301,28.384,32.641 +2020-01-20 05:00:00,85.34,224.606,35.915,32.641 +2020-01-20 05:15:00,87.03,252.854,35.915,32.641 +2020-01-20 05:30:00,92.23,250.43200000000002,35.915,32.641 +2020-01-20 05:45:00,96.51,243.827,35.915,32.641 +2020-01-20 06:00:00,105.11,242.514,56.18,32.641 +2020-01-20 06:15:00,109.59,246.791,56.18,32.641 +2020-01-20 06:30:00,116.86,250.736,56.18,32.641 +2020-01-20 06:45:00,120.28,256.20099999999996,56.18,32.641 +2020-01-20 07:00:00,128.01,254.76,70.877,32.641 +2020-01-20 07:15:00,133.06,260.723,70.877,32.641 +2020-01-20 07:30:00,134.78,264.745,70.877,32.641 +2020-01-20 07:45:00,134.45,266.157,70.877,32.641 +2020-01-20 08:00:00,137.24,264.639,65.65,32.641 +2020-01-20 08:15:00,136.02,266.177,65.65,32.641 +2020-01-20 08:30:00,137.76,263.861,65.65,32.641 +2020-01-20 08:45:00,135.54,261.198,65.65,32.641 +2020-01-20 09:00:00,137.3,255.574,62.037,32.641 +2020-01-20 09:15:00,140.43,250.84400000000002,62.037,32.641 +2020-01-20 09:30:00,141.76,248.355,62.037,32.641 +2020-01-20 09:45:00,141.7,245.425,62.037,32.641 +2020-01-20 10:00:00,140.32,239.98,60.409,32.641 +2020-01-20 10:15:00,143.66,237.407,60.409,32.641 +2020-01-20 10:30:00,143.46,233.18,60.409,32.641 +2020-01-20 10:45:00,142.34,231.155,60.409,32.641 +2020-01-20 11:00:00,141.51,226.299,60.211999999999996,32.641 +2020-01-20 11:15:00,141.96,225.43,60.211999999999996,32.641 +2020-01-20 11:30:00,141.04,224.695,60.211999999999996,32.641 +2020-01-20 11:45:00,140.21,223.153,60.211999999999996,32.641 +2020-01-20 12:00:00,137.28,220.753,57.733000000000004,32.641 +2020-01-20 12:15:00,135.08,220.96900000000002,57.733000000000004,32.641 +2020-01-20 12:30:00,134.0,219.947,57.733000000000004,32.641 +2020-01-20 12:45:00,133.78,220.92700000000002,57.733000000000004,32.641 +2020-01-20 13:00:00,132.27,218.886,58.695,32.641 +2020-01-20 13:15:00,132.8,218.87,58.695,32.641 +2020-01-20 13:30:00,131.49,216.606,58.695,32.641 +2020-01-20 13:45:00,135.03,216.507,58.695,32.641 +2020-01-20 14:00:00,134.0,215.979,59.505,32.641 +2020-01-20 14:15:00,133.27,216.283,59.505,32.641 +2020-01-20 14:30:00,132.16,216.48,59.505,32.641 +2020-01-20 14:45:00,133.29,217.21,59.505,32.641 +2020-01-20 15:00:00,134.34,218.625,59.946000000000005,32.641 +2020-01-20 15:15:00,131.87,219.19099999999997,59.946000000000005,32.641 +2020-01-20 15:30:00,131.59,220.61599999999999,59.946000000000005,32.641 +2020-01-20 15:45:00,130.93,222.04,59.946000000000005,32.641 +2020-01-20 16:00:00,132.11,223.24900000000002,61.766999999999996,32.641 +2020-01-20 16:15:00,130.14,224.595,61.766999999999996,32.641 +2020-01-20 16:30:00,131.7,226.576,61.766999999999996,32.641 +2020-01-20 16:45:00,135.3,227.615,61.766999999999996,32.641 +2020-01-20 17:00:00,141.09,229.46099999999998,67.85600000000001,32.641 +2020-01-20 17:15:00,139.31,230.275,67.85600000000001,32.641 +2020-01-20 17:30:00,138.86,230.47799999999998,67.85600000000001,32.641 +2020-01-20 17:45:00,138.41,230.486,67.85600000000001,32.641 +2020-01-20 18:00:00,138.69,232.252,64.564,32.641 +2020-01-20 18:15:00,137.29,229.68,64.564,32.641 +2020-01-20 18:30:00,136.61,228.98,64.564,32.641 +2020-01-20 18:45:00,137.05,228.43400000000003,64.564,32.641 +2020-01-20 19:00:00,133.91,226.423,58.536,32.641 +2020-01-20 19:15:00,132.7,222.625,58.536,32.641 +2020-01-20 19:30:00,128.93,220.31099999999998,58.536,32.641 +2020-01-20 19:45:00,128.61,217.30700000000002,58.536,32.641 +2020-01-20 20:00:00,121.04,212.972,59.888999999999996,32.641 +2020-01-20 20:15:00,117.83,206.653,59.888999999999996,32.641 +2020-01-20 20:30:00,115.43,201.231,59.888999999999996,32.641 +2020-01-20 20:45:00,114.45,200.747,59.888999999999996,32.641 +2020-01-20 21:00:00,108.29,197.956,52.652,32.641 +2020-01-20 21:15:00,112.06,193.812,52.652,32.641 +2020-01-20 21:30:00,112.01,192.627,52.652,32.641 +2020-01-20 21:45:00,109.57,190.88400000000001,52.652,32.641 +2020-01-20 22:00:00,100.81,181.58599999999998,46.17,32.641 +2020-01-20 22:15:00,98.31,175.95,46.17,32.641 +2020-01-20 22:30:00,96.61,162.003,46.17,32.641 +2020-01-20 22:45:00,99.56,153.227,46.17,32.641 +2020-01-20 23:00:00,96.3,145.66,36.281,32.641 +2020-01-20 23:15:00,94.34,145.513,36.281,32.641 +2020-01-20 23:30:00,84.13,146.162,36.281,32.641 +2020-01-20 23:45:00,85.68,146.227,36.281,32.641 +2020-01-21 00:00:00,88.44,139.76,38.821999999999996,32.641 +2020-01-21 00:15:00,87.49,139.435,38.821999999999996,32.641 +2020-01-21 00:30:00,86.84,140.901,38.821999999999996,32.641 +2020-01-21 00:45:00,82.6,142.671,38.821999999999996,32.641 +2020-01-21 01:00:00,84.63,145.717,36.936,32.641 +2020-01-21 01:15:00,85.35,145.653,36.936,32.641 +2020-01-21 01:30:00,83.29,145.993,36.936,32.641 +2020-01-21 01:45:00,81.04,146.444,36.936,32.641 +2020-01-21 02:00:00,83.81,148.781,34.42,32.641 +2020-01-21 02:15:00,86.37,150.878,34.42,32.641 +2020-01-21 02:30:00,82.71,151.991,34.42,32.641 +2020-01-21 02:45:00,82.96,154.17600000000002,34.42,32.641 +2020-01-21 03:00:00,82.05,157.155,33.585,32.641 +2020-01-21 03:15:00,84.36,158.278,33.585,32.641 +2020-01-21 03:30:00,78.36,160.099,33.585,32.641 +2020-01-21 03:45:00,85.53,162.072,33.585,32.641 +2020-01-21 04:00:00,85.62,173.56400000000002,35.622,32.641 +2020-01-21 04:15:00,85.41,185.06599999999997,35.622,32.641 +2020-01-21 04:30:00,84.33,188.84900000000002,35.622,32.641 +2020-01-21 04:45:00,89.36,191.958,35.622,32.641 +2020-01-21 05:00:00,95.38,226.903,40.599000000000004,32.641 +2020-01-21 05:15:00,91.27,254.767,40.599000000000004,32.641 +2020-01-21 05:30:00,93.24,250.47400000000002,40.599000000000004,32.641 +2020-01-21 05:45:00,98.12,244.033,40.599000000000004,32.641 +2020-01-21 06:00:00,108.67,241.128,55.203,32.641 +2020-01-21 06:15:00,112.52,247.321,55.203,32.641 +2020-01-21 06:30:00,114.67,250.555,55.203,32.641 +2020-01-21 06:45:00,119.03,255.76,55.203,32.641 +2020-01-21 07:00:00,126.8,254.093,69.029,32.641 +2020-01-21 07:15:00,129.56,259.89599999999996,69.029,32.641 +2020-01-21 07:30:00,135.6,263.238,69.029,32.641 +2020-01-21 07:45:00,132.37,265.056,69.029,32.641 +2020-01-21 08:00:00,134.51,263.666,65.85300000000001,32.641 +2020-01-21 08:15:00,134.53,264.052,65.85300000000001,32.641 +2020-01-21 08:30:00,135.3,261.45099999999996,65.85300000000001,32.641 +2020-01-21 08:45:00,136.72,258.621,65.85300000000001,32.641 +2020-01-21 09:00:00,137.69,251.945,61.566,32.641 +2020-01-21 09:15:00,138.99,249.12900000000002,61.566,32.641 +2020-01-21 09:30:00,140.22,247.34099999999998,61.566,32.641 +2020-01-21 09:45:00,141.41,243.92,61.566,32.641 +2020-01-21 10:00:00,138.32,238.024,61.244,32.641 +2020-01-21 10:15:00,140.84,234.231,61.244,32.641 +2020-01-21 10:30:00,140.25,230.209,61.244,32.641 +2020-01-21 10:45:00,139.51,228.35,61.244,32.641 +2020-01-21 11:00:00,139.77,225.236,61.16,32.641 +2020-01-21 11:15:00,140.28,223.91,61.16,32.641 +2020-01-21 11:30:00,141.43,222.003,61.16,32.641 +2020-01-21 11:45:00,141.15,221.368,61.16,32.641 +2020-01-21 12:00:00,137.98,217.40200000000002,59.09,32.641 +2020-01-21 12:15:00,136.33,217.08900000000003,59.09,32.641 +2020-01-21 12:30:00,134.76,216.75599999999997,59.09,32.641 +2020-01-21 12:45:00,135.83,217.27200000000002,59.09,32.641 +2020-01-21 13:00:00,136.03,214.812,60.21,32.641 +2020-01-21 13:15:00,137.42,214.06099999999998,60.21,32.641 +2020-01-21 13:30:00,136.56,213.168,60.21,32.641 +2020-01-21 13:45:00,136.51,213.452,60.21,32.641 +2020-01-21 14:00:00,133.43,213.22400000000002,60.673,32.641 +2020-01-21 14:15:00,132.93,213.732,60.673,32.641 +2020-01-21 14:30:00,131.47,214.611,60.673,32.641 +2020-01-21 14:45:00,132.26,215.398,60.673,32.641 +2020-01-21 15:00:00,133.45,216.356,62.232,32.641 +2020-01-21 15:15:00,131.81,217.135,62.232,32.641 +2020-01-21 15:30:00,129.92,218.83700000000002,62.232,32.641 +2020-01-21 15:45:00,129.62,219.72,62.232,32.641 +2020-01-21 16:00:00,130.89,221.453,63.611999999999995,32.641 +2020-01-21 16:15:00,130.05,223.34099999999998,63.611999999999995,32.641 +2020-01-21 16:30:00,131.99,226.138,63.611999999999995,32.641 +2020-01-21 16:45:00,133.7,227.4,63.611999999999995,32.641 +2020-01-21 17:00:00,141.22,229.834,70.658,32.641 +2020-01-21 17:15:00,139.73,230.655,70.658,32.641 +2020-01-21 17:30:00,139.98,231.72299999999998,70.658,32.641 +2020-01-21 17:45:00,140.83,231.653,70.658,32.641 +2020-01-21 18:00:00,139.47,233.438,68.361,32.641 +2020-01-21 18:15:00,137.77,230.11900000000003,68.361,32.641 +2020-01-21 18:30:00,136.43,229.109,68.361,32.641 +2020-01-21 18:45:00,136.05,229.52200000000002,68.361,32.641 +2020-01-21 19:00:00,132.77,227.708,62.922,32.641 +2020-01-21 19:15:00,131.36,223.574,62.922,32.641 +2020-01-21 19:30:00,136.19,220.53799999999998,62.922,32.641 +2020-01-21 19:45:00,137.49,217.548,62.922,32.641 +2020-01-21 20:00:00,125.64,213.32299999999998,63.251999999999995,32.641 +2020-01-21 20:15:00,119.24,206.47299999999998,63.251999999999995,32.641 +2020-01-21 20:30:00,114.43,202.202,63.251999999999995,32.641 +2020-01-21 20:45:00,114.99,201.021,63.251999999999995,32.641 +2020-01-21 21:00:00,107.66,197.293,54.47,32.641 +2020-01-21 21:15:00,113.17,194.36599999999999,54.47,32.641 +2020-01-21 21:30:00,112.31,192.32,54.47,32.641 +2020-01-21 21:45:00,106.88,190.829,54.47,32.641 +2020-01-21 22:00:00,100.25,183.455,51.12,32.641 +2020-01-21 22:15:00,96.44,177.588,51.12,32.641 +2020-01-21 22:30:00,95.56,163.696,51.12,32.641 +2020-01-21 22:45:00,98.08,155.245,51.12,32.641 +2020-01-21 23:00:00,92.47,147.793,42.156000000000006,32.641 +2020-01-21 23:15:00,91.54,146.417,42.156000000000006,32.641 +2020-01-21 23:30:00,85.61,146.672,42.156000000000006,32.641 +2020-01-21 23:45:00,89.01,146.222,42.156000000000006,32.641 +2020-01-22 00:00:00,85.62,139.774,37.192,32.641 +2020-01-22 00:15:00,81.83,139.43200000000002,37.192,32.641 +2020-01-22 00:30:00,78.79,140.88,37.192,32.641 +2020-01-22 00:45:00,84.5,142.636,37.192,32.641 +2020-01-22 01:00:00,81.61,145.673,32.24,32.641 +2020-01-22 01:15:00,82.94,145.59799999999998,32.24,32.641 +2020-01-22 01:30:00,75.82,145.93200000000002,32.24,32.641 +2020-01-22 01:45:00,81.67,146.376,32.24,32.641 +2020-01-22 02:00:00,79.49,148.722,30.34,32.641 +2020-01-22 02:15:00,76.77,150.81799999999998,30.34,32.641 +2020-01-22 02:30:00,76.68,151.94299999999998,30.34,32.641 +2020-01-22 02:45:00,75.36,154.127,30.34,32.641 +2020-01-22 03:00:00,80.37,157.10399999999998,29.129,32.641 +2020-01-22 03:15:00,81.93,158.24,29.129,32.641 +2020-01-22 03:30:00,80.31,160.061,29.129,32.641 +2020-01-22 03:45:00,81.37,162.04399999999998,29.129,32.641 +2020-01-22 04:00:00,84.25,173.516,30.075,32.641 +2020-01-22 04:15:00,85.11,185.007,30.075,32.641 +2020-01-22 04:30:00,83.8,188.795,30.075,32.641 +2020-01-22 04:45:00,87.55,191.895,30.075,32.641 +2020-01-22 05:00:00,91.73,226.801,35.684,32.641 +2020-01-22 05:15:00,94.75,254.644,35.684,32.641 +2020-01-22 05:30:00,92.38,250.34900000000002,35.684,32.641 +2020-01-22 05:45:00,96.78,243.924,35.684,32.641 +2020-01-22 06:00:00,106.02,241.03900000000002,51.49,32.641 +2020-01-22 06:15:00,111.14,247.24400000000003,51.49,32.641 +2020-01-22 06:30:00,115.37,250.47799999999998,51.49,32.641 +2020-01-22 06:45:00,120.3,255.707,51.49,32.641 +2020-01-22 07:00:00,127.38,254.05900000000003,68.242,32.641 +2020-01-22 07:15:00,127.79,259.843,68.242,32.641 +2020-01-22 07:30:00,130.4,263.16,68.242,32.641 +2020-01-22 07:45:00,128.61,264.947,68.242,32.641 +2020-01-22 08:00:00,135.29,263.547,63.619,32.641 +2020-01-22 08:15:00,132.82,263.91200000000003,63.619,32.641 +2020-01-22 08:30:00,133.56,261.265,63.619,32.641 +2020-01-22 08:45:00,131.49,258.42,63.619,32.641 +2020-01-22 09:00:00,132.7,251.735,61.333,32.641 +2020-01-22 09:15:00,134.67,248.925,61.333,32.641 +2020-01-22 09:30:00,136.96,247.155,61.333,32.641 +2020-01-22 09:45:00,137.72,243.732,61.333,32.641 +2020-01-22 10:00:00,135.4,237.84,59.663000000000004,32.641 +2020-01-22 10:15:00,135.86,234.06,59.663000000000004,32.641 +2020-01-22 10:30:00,135.23,230.037,59.663000000000004,32.641 +2020-01-22 10:45:00,135.61,228.18599999999998,59.663000000000004,32.641 +2020-01-22 11:00:00,134.31,225.054,59.771,32.641 +2020-01-22 11:15:00,134.36,223.733,59.771,32.641 +2020-01-22 11:30:00,133.1,221.829,59.771,32.641 +2020-01-22 11:45:00,129.04,221.201,59.771,32.641 +2020-01-22 12:00:00,125.42,217.24900000000002,58.723,32.641 +2020-01-22 12:15:00,124.01,216.957,58.723,32.641 +2020-01-22 12:30:00,120.56,216.608,58.723,32.641 +2020-01-22 12:45:00,119.44,217.122,58.723,32.641 +2020-01-22 13:00:00,117.85,214.667,58.727,32.641 +2020-01-22 13:15:00,117.79,213.896,58.727,32.641 +2020-01-22 13:30:00,112.65,212.987,58.727,32.641 +2020-01-22 13:45:00,116.56,213.269,58.727,32.641 +2020-01-22 14:00:00,117.4,213.078,59.803999999999995,32.641 +2020-01-22 14:15:00,121.18,213.571,59.803999999999995,32.641 +2020-01-22 14:30:00,122.83,214.44799999999998,59.803999999999995,32.641 +2020-01-22 14:45:00,124.29,215.252,59.803999999999995,32.641 +2020-01-22 15:00:00,121.07,216.226,61.05,32.641 +2020-01-22 15:15:00,118.8,216.98,61.05,32.641 +2020-01-22 15:30:00,120.0,218.66099999999997,61.05,32.641 +2020-01-22 15:45:00,121.57,219.53,61.05,32.641 +2020-01-22 16:00:00,124.33,221.263,64.012,32.641 +2020-01-22 16:15:00,123.18,223.155,64.012,32.641 +2020-01-22 16:30:00,126.48,225.959,64.012,32.641 +2020-01-22 16:45:00,129.37,227.22,64.012,32.641 +2020-01-22 17:00:00,135.39,229.643,66.751,32.641 +2020-01-22 17:15:00,135.01,230.495,66.751,32.641 +2020-01-22 17:30:00,137.03,231.59799999999998,66.751,32.641 +2020-01-22 17:45:00,138.51,231.55200000000002,66.751,32.641 +2020-01-22 18:00:00,137.18,233.358,65.91199999999999,32.641 +2020-01-22 18:15:00,135.84,230.067,65.91199999999999,32.641 +2020-01-22 18:30:00,133.59,229.06,65.91199999999999,32.641 +2020-01-22 18:45:00,134.99,229.49599999999998,65.91199999999999,32.641 +2020-01-22 19:00:00,132.74,227.643,63.324,32.641 +2020-01-22 19:15:00,130.93,223.517,63.324,32.641 +2020-01-22 19:30:00,136.71,220.49200000000002,63.324,32.641 +2020-01-22 19:45:00,135.36,217.517,63.324,32.641 +2020-01-22 20:00:00,124.6,213.267,63.573,32.641 +2020-01-22 20:15:00,117.48,206.424,63.573,32.641 +2020-01-22 20:30:00,112.16,202.15200000000002,63.573,32.641 +2020-01-22 20:45:00,113.32,200.983,63.573,32.641 +2020-01-22 21:00:00,105.36,197.236,55.073,32.641 +2020-01-22 21:15:00,110.61,194.291,55.073,32.641 +2020-01-22 21:30:00,110.22,192.24599999999998,55.073,32.641 +2020-01-22 21:45:00,104.07,190.769,55.073,32.641 +2020-01-22 22:00:00,99.2,183.387,51.321999999999996,32.641 +2020-01-22 22:15:00,94.35,177.542,51.321999999999996,32.641 +2020-01-22 22:30:00,90.06,163.64,51.321999999999996,32.641 +2020-01-22 22:45:00,89.79,155.197,51.321999999999996,32.641 +2020-01-22 23:00:00,82.0,147.72899999999998,42.09,32.641 +2020-01-22 23:15:00,82.55,146.36700000000002,42.09,32.641 +2020-01-22 23:30:00,87.0,146.639,42.09,32.641 +2020-01-22 23:45:00,88.39,146.19899999999998,42.09,32.641 +2020-01-23 00:00:00,83.32,139.778,38.399,32.641 +2020-01-23 00:15:00,78.11,139.42,38.399,32.641 +2020-01-23 00:30:00,80.92,140.849,38.399,32.641 +2020-01-23 00:45:00,83.96,142.593,38.399,32.641 +2020-01-23 01:00:00,80.81,145.619,36.94,32.641 +2020-01-23 01:15:00,75.12,145.533,36.94,32.641 +2020-01-23 01:30:00,75.03,145.861,36.94,32.641 +2020-01-23 01:45:00,72.02,146.297,36.94,32.641 +2020-01-23 02:00:00,77.81,148.651,35.275,32.641 +2020-01-23 02:15:00,79.24,150.75,35.275,32.641 +2020-01-23 02:30:00,80.05,151.886,35.275,32.641 +2020-01-23 02:45:00,75.79,154.06799999999998,35.275,32.641 +2020-01-23 03:00:00,71.98,157.043,35.329,32.641 +2020-01-23 03:15:00,76.32,158.19299999999998,35.329,32.641 +2020-01-23 03:30:00,81.55,160.012,35.329,32.641 +2020-01-23 03:45:00,81.7,162.007,35.329,32.641 +2020-01-23 04:00:00,79.93,173.46,36.275,32.641 +2020-01-23 04:15:00,78.74,184.93900000000002,36.275,32.641 +2020-01-23 04:30:00,85.06,188.732,36.275,32.641 +2020-01-23 04:45:00,87.85,191.82299999999998,36.275,32.641 +2020-01-23 05:00:00,88.57,226.69,42.193999999999996,32.641 +2020-01-23 05:15:00,86.51,254.516,42.193999999999996,32.641 +2020-01-23 05:30:00,89.63,250.215,42.193999999999996,32.641 +2020-01-23 05:45:00,95.12,243.808,42.193999999999996,32.641 +2020-01-23 06:00:00,102.82,240.94299999999998,56.422,32.641 +2020-01-23 06:15:00,109.43,247.16,56.422,32.641 +2020-01-23 06:30:00,111.99,250.392,56.422,32.641 +2020-01-23 06:45:00,117.62,255.642,56.422,32.641 +2020-01-23 07:00:00,125.86,254.014,72.569,32.641 +2020-01-23 07:15:00,132.26,259.78,72.569,32.641 +2020-01-23 07:30:00,134.8,263.07,72.569,32.641 +2020-01-23 07:45:00,132.33,264.827,72.569,32.641 +2020-01-23 08:00:00,134.07,263.414,67.704,32.641 +2020-01-23 08:15:00,132.87,263.758,67.704,32.641 +2020-01-23 08:30:00,136.34,261.065,67.704,32.641 +2020-01-23 08:45:00,132.86,258.20599999999996,67.704,32.641 +2020-01-23 09:00:00,133.61,251.513,63.434,32.641 +2020-01-23 09:15:00,133.41,248.707,63.434,32.641 +2020-01-23 09:30:00,135.43,246.957,63.434,32.641 +2020-01-23 09:45:00,135.05,243.53,63.434,32.641 +2020-01-23 10:00:00,134.75,237.644,61.88399999999999,32.641 +2020-01-23 10:15:00,136.32,233.87900000000002,61.88399999999999,32.641 +2020-01-23 10:30:00,134.68,229.854,61.88399999999999,32.641 +2020-01-23 10:45:00,136.01,228.01,61.88399999999999,32.641 +2020-01-23 11:00:00,136.01,224.862,61.481,32.641 +2020-01-23 11:15:00,134.32,223.547,61.481,32.641 +2020-01-23 11:30:00,130.61,221.64700000000002,61.481,32.641 +2020-01-23 11:45:00,133.98,221.024,61.481,32.641 +2020-01-23 12:00:00,134.51,217.088,59.527,32.641 +2020-01-23 12:15:00,132.67,216.815,59.527,32.641 +2020-01-23 12:30:00,132.94,216.449,59.527,32.641 +2020-01-23 12:45:00,133.32,216.96200000000002,59.527,32.641 +2020-01-23 13:00:00,132.61,214.511,58.794,32.641 +2020-01-23 13:15:00,132.78,213.72099999999998,58.794,32.641 +2020-01-23 13:30:00,128.88,212.799,58.794,32.641 +2020-01-23 13:45:00,128.87,213.078,58.794,32.641 +2020-01-23 14:00:00,127.62,212.922,60.32,32.641 +2020-01-23 14:15:00,129.42,213.40200000000002,60.32,32.641 +2020-01-23 14:30:00,128.31,214.275,60.32,32.641 +2020-01-23 14:45:00,128.39,215.09599999999998,60.32,32.641 +2020-01-23 15:00:00,129.2,216.08700000000002,62.52,32.641 +2020-01-23 15:15:00,128.37,216.81400000000002,62.52,32.641 +2020-01-23 15:30:00,125.84,218.475,62.52,32.641 +2020-01-23 15:45:00,125.84,219.328,62.52,32.641 +2020-01-23 16:00:00,127.22,221.063,64.199,32.641 +2020-01-23 16:15:00,127.3,222.956,64.199,32.641 +2020-01-23 16:30:00,128.99,225.766,64.199,32.641 +2020-01-23 16:45:00,131.38,227.025,64.199,32.641 +2020-01-23 17:00:00,137.71,229.438,68.19800000000001,32.641 +2020-01-23 17:15:00,137.9,230.322,68.19800000000001,32.641 +2020-01-23 17:30:00,140.75,231.459,68.19800000000001,32.641 +2020-01-23 17:45:00,138.94,231.43900000000002,68.19800000000001,32.641 +2020-01-23 18:00:00,139.21,233.265,67.899,32.641 +2020-01-23 18:15:00,140.6,230.005,67.899,32.641 +2020-01-23 18:30:00,135.9,229.002,67.899,32.641 +2020-01-23 18:45:00,134.83,229.46099999999998,67.899,32.641 +2020-01-23 19:00:00,132.4,227.56799999999998,64.72399999999999,32.641 +2020-01-23 19:15:00,130.3,223.447,64.72399999999999,32.641 +2020-01-23 19:30:00,136.27,220.435,64.72399999999999,32.641 +2020-01-23 19:45:00,136.38,217.476,64.72399999999999,32.641 +2020-01-23 20:00:00,124.03,213.202,64.062,32.641 +2020-01-23 20:15:00,118.44,206.364,64.062,32.641 +2020-01-23 20:30:00,114.15,202.092,64.062,32.641 +2020-01-23 20:45:00,111.79,200.937,64.062,32.641 +2020-01-23 21:00:00,107.54,197.17,57.971000000000004,32.641 +2020-01-23 21:15:00,110.3,194.208,57.971000000000004,32.641 +2020-01-23 21:30:00,111.1,192.162,57.971000000000004,32.641 +2020-01-23 21:45:00,106.2,190.703,57.971000000000004,32.641 +2020-01-23 22:00:00,98.72,183.31099999999998,53.715,32.641 +2020-01-23 22:15:00,96.15,177.487,53.715,32.641 +2020-01-23 22:30:00,92.08,163.576,53.715,32.641 +2020-01-23 22:45:00,91.81,155.14,53.715,32.641 +2020-01-23 23:00:00,94.02,147.657,47.8,32.641 +2020-01-23 23:15:00,94.02,146.306,47.8,32.641 +2020-01-23 23:30:00,89.04,146.597,47.8,32.641 +2020-01-23 23:45:00,85.37,146.167,47.8,32.641 +2020-01-24 00:00:00,86.99,138.808,43.656000000000006,32.641 +2020-01-24 00:15:00,84.7,138.631,43.656000000000006,32.641 +2020-01-24 00:30:00,84.43,139.84,43.656000000000006,32.641 +2020-01-24 00:45:00,78.25,141.634,43.656000000000006,32.641 +2020-01-24 01:00:00,78.02,144.345,41.263000000000005,32.641 +2020-01-24 01:15:00,81.93,145.425,41.263000000000005,32.641 +2020-01-24 01:30:00,81.42,145.365,41.263000000000005,32.641 +2020-01-24 01:45:00,76.21,145.961,41.263000000000005,32.641 +2020-01-24 02:00:00,78.49,148.269,40.799,32.641 +2020-01-24 02:15:00,80.51,150.241,40.799,32.641 +2020-01-24 02:30:00,80.14,151.866,40.799,32.641 +2020-01-24 02:45:00,75.42,154.216,40.799,32.641 +2020-01-24 03:00:00,74.46,155.875,41.398,32.641 +2020-01-24 03:15:00,81.27,158.363,41.398,32.641 +2020-01-24 03:30:00,82.2,160.202,41.398,32.641 +2020-01-24 03:45:00,82.59,162.441,41.398,32.641 +2020-01-24 04:00:00,78.55,174.128,42.38,32.641 +2020-01-24 04:15:00,81.62,185.585,42.38,32.641 +2020-01-24 04:30:00,86.32,189.485,42.38,32.641 +2020-01-24 04:45:00,88.91,191.32299999999998,42.38,32.641 +2020-01-24 05:00:00,90.44,224.72799999999998,46.181000000000004,32.641 +2020-01-24 05:15:00,88.25,254.162,46.181000000000004,32.641 +2020-01-24 05:30:00,91.02,251.11,46.181000000000004,32.641 +2020-01-24 05:45:00,95.66,244.72299999999998,46.181000000000004,32.641 +2020-01-24 06:00:00,105.46,242.351,59.33,32.641 +2020-01-24 06:15:00,108.13,246.793,59.33,32.641 +2020-01-24 06:30:00,112.97,249.0,59.33,32.641 +2020-01-24 06:45:00,118.74,256.23400000000004,59.33,32.641 +2020-01-24 07:00:00,125.57,253.513,72.454,32.641 +2020-01-24 07:15:00,127.25,260.314,72.454,32.641 +2020-01-24 07:30:00,132.41,263.71299999999997,72.454,32.641 +2020-01-24 07:45:00,133.17,264.42,72.454,32.641 +2020-01-24 08:00:00,137.53,261.54900000000004,67.175,32.641 +2020-01-24 08:15:00,135.9,261.298,67.175,32.641 +2020-01-24 08:30:00,136.3,259.728,67.175,32.641 +2020-01-24 08:45:00,136.6,255.017,67.175,32.641 +2020-01-24 09:00:00,137.1,249.24099999999999,65.365,32.641 +2020-01-24 09:15:00,138.33,246.803,65.365,32.641 +2020-01-24 09:30:00,139.13,244.678,65.365,32.641 +2020-01-24 09:45:00,139.22,241.058,65.365,32.641 +2020-01-24 10:00:00,138.08,233.864,63.95,32.641 +2020-01-24 10:15:00,138.61,230.97299999999998,63.95,32.641 +2020-01-24 10:30:00,138.28,226.773,63.95,32.641 +2020-01-24 10:45:00,137.57,224.433,63.95,32.641 +2020-01-24 11:00:00,138.32,221.21900000000002,63.92100000000001,32.641 +2020-01-24 11:15:00,138.78,219.02900000000002,63.92100000000001,32.641 +2020-01-24 11:30:00,139.38,219.236,63.92100000000001,32.641 +2020-01-24 11:45:00,138.06,218.825,63.92100000000001,32.641 +2020-01-24 12:00:00,136.09,216.109,60.79600000000001,32.641 +2020-01-24 12:15:00,134.0,213.50099999999998,60.79600000000001,32.641 +2020-01-24 12:30:00,131.28,213.28599999999997,60.79600000000001,32.641 +2020-01-24 12:45:00,132.51,214.524,60.79600000000001,32.641 +2020-01-24 13:00:00,130.47,213.08599999999998,59.393,32.641 +2020-01-24 13:15:00,129.98,213.205,59.393,32.641 +2020-01-24 13:30:00,128.13,212.16400000000002,59.393,32.641 +2020-01-24 13:45:00,128.71,212.31799999999998,59.393,32.641 +2020-01-24 14:00:00,126.8,210.986,57.943999999999996,32.641 +2020-01-24 14:15:00,129.56,211.17700000000002,57.943999999999996,32.641 +2020-01-24 14:30:00,127.19,212.415,57.943999999999996,32.641 +2020-01-24 14:45:00,127.61,213.71200000000002,57.943999999999996,32.641 +2020-01-24 15:00:00,129.17,214.165,60.153999999999996,32.641 +2020-01-24 15:15:00,125.93,214.391,60.153999999999996,32.641 +2020-01-24 15:30:00,127.46,214.347,60.153999999999996,32.641 +2020-01-24 15:45:00,126.28,215.24400000000003,60.153999999999996,32.641 +2020-01-24 16:00:00,127.34,215.765,62.933,32.641 +2020-01-24 16:15:00,126.66,217.925,62.933,32.641 +2020-01-24 16:30:00,128.07,220.878,62.933,32.641 +2020-01-24 16:45:00,131.69,222.109,62.933,32.641 +2020-01-24 17:00:00,137.39,224.52900000000002,68.657,32.641 +2020-01-24 17:15:00,135.66,224.99599999999998,68.657,32.641 +2020-01-24 17:30:00,133.93,225.78099999999998,68.657,32.641 +2020-01-24 17:45:00,135.2,225.53799999999998,68.657,32.641 +2020-01-24 18:00:00,136.43,228.196,67.111,32.641 +2020-01-24 18:15:00,134.39,224.639,67.111,32.641 +2020-01-24 18:30:00,133.68,224.102,67.111,32.641 +2020-01-24 18:45:00,133.53,224.52700000000002,67.111,32.641 +2020-01-24 19:00:00,130.37,223.533,62.434,32.641 +2020-01-24 19:15:00,128.1,220.89700000000002,62.434,32.641 +2020-01-24 19:30:00,130.9,217.423,62.434,32.641 +2020-01-24 19:45:00,136.04,214.078,62.434,32.641 +2020-01-24 20:00:00,125.76,209.85,61.763000000000005,32.641 +2020-01-24 20:15:00,117.6,202.93,61.763000000000005,32.641 +2020-01-24 20:30:00,114.72,198.669,61.763000000000005,32.641 +2020-01-24 20:45:00,110.33,198.265,61.763000000000005,32.641 +2020-01-24 21:00:00,104.51,194.895,56.785,32.641 +2020-01-24 21:15:00,102.91,192.205,56.785,32.641 +2020-01-24 21:30:00,102.15,190.22799999999998,56.785,32.641 +2020-01-24 21:45:00,104.05,189.38099999999997,56.785,32.641 +2020-01-24 22:00:00,99.83,183.11700000000002,52.693000000000005,32.641 +2020-01-24 22:15:00,94.61,177.195,52.693000000000005,32.641 +2020-01-24 22:30:00,89.29,169.947,52.693000000000005,32.641 +2020-01-24 22:45:00,85.86,165.43400000000003,52.693000000000005,32.641 +2020-01-24 23:00:00,82.29,157.219,45.443999999999996,32.641 +2020-01-24 23:15:00,81.42,153.85399999999998,45.443999999999996,32.641 +2020-01-24 23:30:00,80.16,152.725,45.443999999999996,32.641 +2020-01-24 23:45:00,77.47,151.564,45.443999999999996,32.641 +2020-01-25 00:00:00,72.4,135.27200000000002,44.738,32.459 +2020-01-25 00:15:00,71.11,130.214,44.738,32.459 +2020-01-25 00:30:00,72.19,133.029,44.738,32.459 +2020-01-25 00:45:00,70.85,135.756,44.738,32.459 +2020-01-25 01:00:00,71.22,139.132,40.303000000000004,32.459 +2020-01-25 01:15:00,72.27,138.951,40.303000000000004,32.459 +2020-01-25 01:30:00,76.25,138.406,40.303000000000004,32.459 +2020-01-25 01:45:00,75.27,138.54399999999998,40.303000000000004,32.459 +2020-01-25 02:00:00,69.95,141.80200000000002,38.61,32.459 +2020-01-25 02:15:00,68.12,143.468,38.61,32.459 +2020-01-25 02:30:00,65.73,143.929,38.61,32.459 +2020-01-25 02:45:00,65.96,146.268,38.61,32.459 +2020-01-25 03:00:00,64.75,148.87,37.554,32.459 +2020-01-25 03:15:00,65.49,150.054,37.554,32.459 +2020-01-25 03:30:00,64.8,149.96,37.554,32.459 +2020-01-25 03:45:00,65.17,152.093,37.554,32.459 +2020-01-25 04:00:00,64.99,159.15,37.176,32.459 +2020-01-25 04:15:00,65.24,167.739,37.176,32.459 +2020-01-25 04:30:00,65.75,169.365,37.176,32.459 +2020-01-25 04:45:00,66.44,170.56,37.176,32.459 +2020-01-25 05:00:00,66.97,186.34799999999998,36.893,32.459 +2020-01-25 05:15:00,67.29,195.11700000000002,36.893,32.459 +2020-01-25 05:30:00,68.56,192.21200000000002,36.893,32.459 +2020-01-25 05:45:00,69.68,191.523,36.893,32.459 +2020-01-25 06:00:00,69.94,209.548,37.803000000000004,32.459 +2020-01-25 06:15:00,70.69,232.051,37.803000000000004,32.459 +2020-01-25 06:30:00,71.35,228.36900000000003,37.803000000000004,32.459 +2020-01-25 06:45:00,73.5,225.519,37.803000000000004,32.459 +2020-01-25 07:00:00,77.71,218.623,41.086999999999996,32.459 +2020-01-25 07:15:00,78.56,224.142,41.086999999999996,32.459 +2020-01-25 07:30:00,82.94,230.46200000000002,41.086999999999996,32.459 +2020-01-25 07:45:00,84.55,235.69099999999997,41.086999999999996,32.459 +2020-01-25 08:00:00,88.98,237.581,48.222,32.459 +2020-01-25 08:15:00,90.69,241.622,48.222,32.459 +2020-01-25 08:30:00,92.22,241.90599999999998,48.222,32.459 +2020-01-25 08:45:00,95.97,240.745,48.222,32.459 +2020-01-25 09:00:00,98.38,236.59099999999998,52.791000000000004,32.459 +2020-01-25 09:15:00,98.75,234.963,52.791000000000004,32.459 +2020-01-25 09:30:00,99.49,233.83700000000002,52.791000000000004,32.459 +2020-01-25 09:45:00,101.44,230.472,52.791000000000004,32.459 +2020-01-25 10:00:00,102.45,223.484,54.341,32.459 +2020-01-25 10:15:00,103.89,220.745,54.341,32.459 +2020-01-25 10:30:00,102.9,216.793,54.341,32.459 +2020-01-25 10:45:00,104.81,216.046,54.341,32.459 +2020-01-25 11:00:00,103.72,213.127,51.94,32.459 +2020-01-25 11:15:00,105.87,210.00900000000001,51.94,32.459 +2020-01-25 11:30:00,107.38,208.875,51.94,32.459 +2020-01-25 11:45:00,106.98,207.255,51.94,32.459 +2020-01-25 12:00:00,104.05,203.44,50.973,32.459 +2020-01-25 12:15:00,103.31,201.46900000000002,50.973,32.459 +2020-01-25 12:30:00,100.71,201.627,50.973,32.459 +2020-01-25 12:45:00,99.96,201.85299999999998,50.973,32.459 +2020-01-25 13:00:00,97.04,200.072,48.06399999999999,32.459 +2020-01-25 13:15:00,97.34,197.856,48.06399999999999,32.459 +2020-01-25 13:30:00,95.63,196.245,48.06399999999999,32.459 +2020-01-25 13:45:00,94.67,197.16,48.06399999999999,32.459 +2020-01-25 14:00:00,91.46,197.327,45.707,32.459 +2020-01-25 14:15:00,92.11,197.06900000000002,45.707,32.459 +2020-01-25 14:30:00,91.72,196.25,45.707,32.459 +2020-01-25 14:45:00,91.73,197.745,45.707,32.459 +2020-01-25 15:00:00,90.8,198.975,47.567,32.459 +2020-01-25 15:15:00,90.03,200.00599999999997,47.567,32.459 +2020-01-25 15:30:00,89.45,201.695,47.567,32.459 +2020-01-25 15:45:00,88.86,202.729,47.567,32.459 +2020-01-25 16:00:00,90.25,201.65900000000002,52.031000000000006,32.459 +2020-01-25 16:15:00,89.46,204.97400000000002,52.031000000000006,32.459 +2020-01-25 16:30:00,90.4,207.842,52.031000000000006,32.459 +2020-01-25 16:45:00,93.84,210.09400000000002,52.031000000000006,32.459 +2020-01-25 17:00:00,100.62,212.105,58.218999999999994,32.459 +2020-01-25 17:15:00,102.8,214.77700000000002,58.218999999999994,32.459 +2020-01-25 17:30:00,106.7,215.502,58.218999999999994,32.459 +2020-01-25 17:45:00,106.99,214.757,58.218999999999994,32.459 +2020-01-25 18:00:00,107.5,216.775,57.65,32.459 +2020-01-25 18:15:00,107.19,215.09,57.65,32.459 +2020-01-25 18:30:00,109.91,215.905,57.65,32.459 +2020-01-25 18:45:00,105.29,212.97400000000002,57.65,32.459 +2020-01-25 19:00:00,104.0,213.22099999999998,51.261,32.459 +2020-01-25 19:15:00,102.55,210.149,51.261,32.459 +2020-01-25 19:30:00,101.86,207.44799999999998,51.261,32.459 +2020-01-25 19:45:00,102.2,203.72,51.261,32.459 +2020-01-25 20:00:00,94.93,201.78,44.068000000000005,32.459 +2020-01-25 20:15:00,92.01,197.358,44.068000000000005,32.459 +2020-01-25 20:30:00,88.94,192.80700000000002,44.068000000000005,32.459 +2020-01-25 20:45:00,87.29,191.732,44.068000000000005,32.459 +2020-01-25 21:00:00,83.53,191.08,38.861,32.459 +2020-01-25 21:15:00,82.66,188.91299999999998,38.861,32.459 +2020-01-25 21:30:00,80.26,188.308,38.861,32.459 +2020-01-25 21:45:00,80.05,187.084,38.861,32.459 +2020-01-25 22:00:00,77.81,182.33900000000003,39.485,32.459 +2020-01-25 22:15:00,76.34,179.227,39.485,32.459 +2020-01-25 22:30:00,73.65,179.05900000000003,39.485,32.459 +2020-01-25 22:45:00,72.2,176.628,39.485,32.459 +2020-01-25 23:00:00,67.88,171.222,32.027,32.459 +2020-01-25 23:15:00,68.21,165.968,32.027,32.459 +2020-01-25 23:30:00,62.43,162.59,32.027,32.459 +2020-01-25 23:45:00,63.07,158.65,32.027,32.459 +2020-01-26 00:00:00,60.64,135.531,26.96,32.459 +2020-01-26 00:15:00,59.92,130.25799999999998,26.96,32.459 +2020-01-26 00:30:00,59.03,132.641,26.96,32.459 +2020-01-26 00:45:00,58.62,136.18200000000002,26.96,32.459 +2020-01-26 01:00:00,54.93,139.338,24.295,32.459 +2020-01-26 01:15:00,55.99,140.374,24.295,32.459 +2020-01-26 01:30:00,55.67,140.451,24.295,32.459 +2020-01-26 01:45:00,54.2,140.276,24.295,32.459 +2020-01-26 02:00:00,53.39,142.659,24.268,32.459 +2020-01-26 02:15:00,54.08,143.218,24.268,32.459 +2020-01-26 02:30:00,52.98,144.661,24.268,32.459 +2020-01-26 02:45:00,53.02,147.586,24.268,32.459 +2020-01-26 03:00:00,53.08,150.45600000000002,23.373,32.459 +2020-01-26 03:15:00,50.37,150.995,23.373,32.459 +2020-01-26 03:30:00,52.45,152.66,23.373,32.459 +2020-01-26 03:45:00,52.94,154.84,23.373,32.459 +2020-01-26 04:00:00,52.89,161.61,23.874000000000002,32.459 +2020-01-26 04:15:00,53.22,169.06799999999998,23.874000000000002,32.459 +2020-01-26 04:30:00,54.0,170.55700000000002,23.874000000000002,32.459 +2020-01-26 04:45:00,54.34,172.14,23.874000000000002,32.459 +2020-01-26 05:00:00,55.82,183.72400000000002,24.871,32.459 +2020-01-26 05:15:00,56.59,189.75599999999997,24.871,32.459 +2020-01-26 05:30:00,55.44,186.71400000000003,24.871,32.459 +2020-01-26 05:45:00,56.61,186.375,24.871,32.459 +2020-01-26 06:00:00,56.99,204.688,23.84,32.459 +2020-01-26 06:15:00,56.78,225.035,23.84,32.459 +2020-01-26 06:30:00,57.8,220.16099999999997,23.84,32.459 +2020-01-26 06:45:00,58.26,216.248,23.84,32.459 +2020-01-26 07:00:00,61.45,212.21599999999998,27.430999999999997,32.459 +2020-01-26 07:15:00,62.62,216.979,27.430999999999997,32.459 +2020-01-26 07:30:00,64.42,221.612,27.430999999999997,32.459 +2020-01-26 07:45:00,65.23,225.917,27.430999999999997,32.459 +2020-01-26 08:00:00,67.41,229.857,33.891999999999996,32.459 +2020-01-26 08:15:00,70.28,233.58700000000002,33.891999999999996,32.459 +2020-01-26 08:30:00,72.53,235.627,33.891999999999996,32.459 +2020-01-26 08:45:00,74.96,236.83,33.891999999999996,32.459 +2020-01-26 09:00:00,76.29,232.225,37.571,32.459 +2020-01-26 09:15:00,77.45,231.31799999999998,37.571,32.459 +2020-01-26 09:30:00,78.94,229.967,37.571,32.459 +2020-01-26 09:45:00,80.75,226.315,37.571,32.459 +2020-01-26 10:00:00,81.35,222.179,40.594,32.459 +2020-01-26 10:15:00,83.76,220.058,40.594,32.459 +2020-01-26 10:30:00,85.49,216.775,40.594,32.459 +2020-01-26 10:45:00,88.12,213.672,40.594,32.459 +2020-01-26 11:00:00,88.23,211.86900000000003,44.133,32.459 +2020-01-26 11:15:00,92.2,208.975,44.133,32.459 +2020-01-26 11:30:00,92.74,206.762,44.133,32.459 +2020-01-26 11:45:00,93.24,205.81400000000002,44.133,32.459 +2020-01-26 12:00:00,91.73,201.201,41.198,32.459 +2020-01-26 12:15:00,89.36,201.549,41.198,32.459 +2020-01-26 12:30:00,84.83,199.968,41.198,32.459 +2020-01-26 12:45:00,83.74,199.162,41.198,32.459 +2020-01-26 13:00:00,80.41,196.597,37.014,32.459 +2020-01-26 13:15:00,80.17,197.945,37.014,32.459 +2020-01-26 13:30:00,81.07,196.236,37.014,32.459 +2020-01-26 13:45:00,80.59,196.222,37.014,32.459 +2020-01-26 14:00:00,79.11,196.545,34.934,32.459 +2020-01-26 14:15:00,78.6,197.6,34.934,32.459 +2020-01-26 14:30:00,78.67,198.37900000000002,34.934,32.459 +2020-01-26 14:45:00,79.38,199.56599999999997,34.934,32.459 +2020-01-26 15:00:00,79.93,199.014,34.588,32.459 +2020-01-26 15:15:00,78.78,201.0,34.588,32.459 +2020-01-26 15:30:00,78.6,203.368,34.588,32.459 +2020-01-26 15:45:00,77.74,205.146,34.588,32.459 +2020-01-26 16:00:00,80.38,206.40099999999998,37.874,32.459 +2020-01-26 16:15:00,79.54,208.645,37.874,32.459 +2020-01-26 16:30:00,81.49,211.685,37.874,32.459 +2020-01-26 16:45:00,84.68,214.078,37.874,32.459 +2020-01-26 17:00:00,92.75,215.987,47.303999999999995,32.459 +2020-01-26 17:15:00,94.5,218.13099999999997,47.303999999999995,32.459 +2020-01-26 17:30:00,96.47,219.12099999999998,47.303999999999995,32.459 +2020-01-26 17:45:00,98.7,220.93200000000002,47.303999999999995,32.459 +2020-01-26 18:00:00,100.59,222.25099999999998,48.879,32.459 +2020-01-26 18:15:00,99.21,222.146,48.879,32.459 +2020-01-26 18:30:00,99.63,220.636,48.879,32.459 +2020-01-26 18:45:00,98.32,219.80900000000003,48.879,32.459 +2020-01-26 19:00:00,96.9,219.37599999999998,44.826,32.459 +2020-01-26 19:15:00,95.53,217.101,44.826,32.459 +2020-01-26 19:30:00,98.34,214.25599999999997,44.826,32.459 +2020-01-26 19:45:00,100.34,212.222,44.826,32.459 +2020-01-26 20:00:00,98.08,210.243,40.154,32.459 +2020-01-26 20:15:00,89.23,206.959,40.154,32.459 +2020-01-26 20:30:00,88.79,203.717,40.154,32.459 +2020-01-26 20:45:00,85.18,201.489,40.154,32.459 +2020-01-26 21:00:00,83.88,197.894,36.549,32.459 +2020-01-26 21:15:00,90.22,195.03599999999997,36.549,32.459 +2020-01-26 21:30:00,90.09,194.83700000000002,36.549,32.459 +2020-01-26 21:45:00,86.75,193.743,36.549,32.459 +2020-01-26 22:00:00,84.32,187.405,37.663000000000004,32.459 +2020-01-26 22:15:00,87.31,183.648,37.663000000000004,32.459 +2020-01-26 22:30:00,87.15,179.94400000000002,37.663000000000004,32.459 +2020-01-26 22:45:00,85.87,176.705,37.663000000000004,32.459 +2020-01-26 23:00:00,78.89,168.15599999999998,31.945,32.459 +2020-01-26 23:15:00,82.8,164.87,31.945,32.459 +2020-01-26 23:30:00,81.36,162.487,31.945,32.459 +2020-01-26 23:45:00,80.85,159.548,31.945,32.459 +2020-01-27 00:00:00,72.01,140.157,31.533,32.641 +2020-01-27 00:15:00,74.98,138.209,31.533,32.641 +2020-01-27 00:30:00,75.17,140.77700000000002,31.533,32.641 +2020-01-27 00:45:00,73.37,143.741,31.533,32.641 +2020-01-27 01:00:00,65.99,146.861,30.56,32.641 +2020-01-27 01:15:00,72.24,147.267,30.56,32.641 +2020-01-27 01:30:00,72.74,147.346,30.56,32.641 +2020-01-27 01:45:00,73.19,147.313,30.56,32.641 +2020-01-27 02:00:00,68.81,149.619,29.55,32.641 +2020-01-27 02:15:00,72.66,152.05,29.55,32.641 +2020-01-27 02:30:00,73.33,153.86700000000002,29.55,32.641 +2020-01-27 02:45:00,72.31,156.089,29.55,32.641 +2020-01-27 03:00:00,67.49,160.394,27.059,32.641 +2020-01-27 03:15:00,73.56,162.764,27.059,32.641 +2020-01-27 03:30:00,75.02,163.99900000000002,27.059,32.641 +2020-01-27 03:45:00,71.92,165.61599999999999,27.059,32.641 +2020-01-27 04:00:00,66.96,177.02,28.384,32.641 +2020-01-27 04:15:00,68.37,188.86900000000003,28.384,32.641 +2020-01-27 04:30:00,70.66,193.035,28.384,32.641 +2020-01-27 04:45:00,72.68,194.738,28.384,32.641 +2020-01-27 05:00:00,77.25,223.778,35.915,32.641 +2020-01-27 05:15:00,79.29,251.91099999999997,35.915,32.641 +2020-01-27 05:30:00,84.6,249.452,35.915,32.641 +2020-01-27 05:45:00,89.92,242.958,35.915,32.641 +2020-01-27 06:00:00,99.95,241.782,56.18,32.641 +2020-01-27 06:15:00,104.93,246.144,56.18,32.641 +2020-01-27 06:30:00,110.18,250.06400000000002,56.18,32.641 +2020-01-27 06:45:00,113.96,255.674,56.18,32.641 +2020-01-27 07:00:00,121.51,254.377,70.877,32.641 +2020-01-27 07:15:00,123.92,260.205,70.877,32.641 +2020-01-27 07:30:00,123.09,264.038,70.877,32.641 +2020-01-27 07:45:00,125.61,265.228,70.877,32.641 +2020-01-27 08:00:00,127.27,263.626,65.65,32.641 +2020-01-27 08:15:00,127.65,265.01099999999997,65.65,32.641 +2020-01-27 08:30:00,129.37,262.36,65.65,32.641 +2020-01-27 08:45:00,125.27,259.60400000000004,65.65,32.641 +2020-01-27 09:00:00,124.61,253.929,62.037,32.641 +2020-01-27 09:15:00,126.22,249.234,62.037,32.641 +2020-01-27 09:30:00,126.26,246.88,62.037,32.641 +2020-01-27 09:45:00,122.4,243.92700000000002,62.037,32.641 +2020-01-27 10:00:00,124.71,238.52,60.409,32.641 +2020-01-27 10:15:00,124.92,236.058,60.409,32.641 +2020-01-27 10:30:00,126.43,231.824,60.409,32.641 +2020-01-27 10:45:00,127.62,229.86,60.409,32.641 +2020-01-27 11:00:00,128.14,224.88299999999998,60.211999999999996,32.641 +2020-01-27 11:15:00,129.57,224.053,60.211999999999996,32.641 +2020-01-27 11:30:00,130.44,223.34599999999998,60.211999999999996,32.641 +2020-01-27 11:45:00,132.01,221.85299999999998,60.211999999999996,32.641 +2020-01-27 12:00:00,131.87,219.55700000000002,57.733000000000004,32.641 +2020-01-27 12:15:00,130.69,219.908,57.733000000000004,32.641 +2020-01-27 12:30:00,127.99,218.766,57.733000000000004,32.641 +2020-01-27 12:45:00,128.57,219.735,57.733000000000004,32.641 +2020-01-27 13:00:00,127.25,217.735,58.695,32.641 +2020-01-27 13:15:00,127.54,217.582,58.695,32.641 +2020-01-27 13:30:00,126.3,215.213,58.695,32.641 +2020-01-27 13:45:00,124.1,215.097,58.695,32.641 +2020-01-27 14:00:00,120.85,214.834,59.505,32.641 +2020-01-27 14:15:00,122.78,215.037,59.505,32.641 +2020-01-27 14:30:00,121.73,215.206,59.505,32.641 +2020-01-27 14:45:00,122.16,216.058,59.505,32.641 +2020-01-27 15:00:00,122.79,217.581,59.946000000000005,32.641 +2020-01-27 15:15:00,122.37,217.96099999999998,59.946000000000005,32.641 +2020-01-27 15:30:00,120.21,219.23,59.946000000000005,32.641 +2020-01-27 15:45:00,121.58,220.549,59.946000000000005,32.641 +2020-01-27 16:00:00,125.44,221.766,61.766999999999996,32.641 +2020-01-27 16:15:00,124.32,223.122,61.766999999999996,32.641 +2020-01-27 16:30:00,125.38,225.149,61.766999999999996,32.641 +2020-01-27 16:45:00,128.84,226.167,61.766999999999996,32.641 +2020-01-27 17:00:00,134.98,227.94099999999997,67.85600000000001,32.641 +2020-01-27 17:15:00,133.74,228.979,67.85600000000001,32.641 +2020-01-27 17:30:00,134.91,229.42700000000002,67.85600000000001,32.641 +2020-01-27 17:45:00,134.47,229.61,67.85600000000001,32.641 +2020-01-27 18:00:00,134.73,231.514,64.564,32.641 +2020-01-27 18:15:00,131.96,229.16400000000002,64.564,32.641 +2020-01-27 18:30:00,133.73,228.49200000000002,64.564,32.641 +2020-01-27 18:45:00,132.73,228.109,64.564,32.641 +2020-01-27 19:00:00,129.78,225.815,58.536,32.641 +2020-01-27 19:15:00,126.63,222.06799999999998,58.536,32.641 +2020-01-27 19:30:00,126.74,219.84400000000002,58.536,32.641 +2020-01-27 19:45:00,131.0,216.96200000000002,58.536,32.641 +2020-01-27 20:00:00,124.95,212.45,59.888999999999996,32.641 +2020-01-27 20:15:00,116.65,206.18,59.888999999999996,32.641 +2020-01-27 20:30:00,113.27,200.757,59.888999999999996,32.641 +2020-01-27 20:45:00,109.79,200.363,59.888999999999996,32.641 +2020-01-27 21:00:00,104.5,197.43400000000003,52.652,32.641 +2020-01-27 21:15:00,102.34,193.169,52.652,32.641 +2020-01-27 21:30:00,105.41,191.98,52.652,32.641 +2020-01-27 21:45:00,106.02,190.355,52.652,32.641 +2020-01-27 22:00:00,103.67,180.989,46.17,32.641 +2020-01-27 22:15:00,92.1,175.507,46.17,32.641 +2020-01-27 22:30:00,90.73,161.47299999999998,46.17,32.641 +2020-01-27 22:45:00,86.82,152.751,46.17,32.641 +2020-01-27 23:00:00,85.71,145.082,36.281,32.641 +2020-01-27 23:15:00,90.44,145.025,36.281,32.641 +2020-01-27 23:30:00,87.95,145.799,36.281,32.641 +2020-01-27 23:45:00,82.47,145.94,36.281,32.641 +2020-01-28 00:00:00,78.26,139.671,38.821999999999996,32.641 +2020-01-28 00:15:00,75.34,139.238,38.821999999999996,32.641 +2020-01-28 00:30:00,79.52,140.57399999999998,38.821999999999996,32.641 +2020-01-28 00:45:00,82.22,142.25799999999998,38.821999999999996,32.641 +2020-01-28 01:00:00,79.73,145.21200000000002,36.936,32.641 +2020-01-28 01:15:00,76.69,145.066,36.936,32.641 +2020-01-28 01:30:00,75.14,145.361,36.936,32.641 +2020-01-28 01:45:00,76.58,145.766,36.936,32.641 +2020-01-28 02:00:00,79.49,148.15200000000002,34.42,32.641 +2020-01-28 02:15:00,79.78,150.255,34.42,32.641 +2020-01-28 02:30:00,77.63,151.44899999999998,34.42,32.641 +2020-01-28 02:45:00,72.78,153.629,34.42,32.641 +2020-01-28 03:00:00,78.0,156.595,33.585,32.641 +2020-01-28 03:15:00,81.7,157.804,33.585,32.641 +2020-01-28 03:30:00,83.51,159.614,33.585,32.641 +2020-01-28 03:45:00,82.44,161.667,33.585,32.641 +2020-01-28 04:00:00,77.73,173.03900000000002,35.622,32.641 +2020-01-28 04:15:00,84.2,184.46099999999998,35.622,32.641 +2020-01-28 04:30:00,87.03,188.28799999999998,35.622,32.641 +2020-01-28 04:45:00,89.61,191.332,35.622,32.641 +2020-01-28 05:00:00,89.09,226.02,40.599000000000004,32.641 +2020-01-28 05:15:00,87.98,253.782,40.599000000000004,32.641 +2020-01-28 05:30:00,94.26,249.44400000000002,40.599000000000004,32.641 +2020-01-28 05:45:00,98.87,243.109,40.599000000000004,32.641 +2020-01-28 06:00:00,105.04,240.338,55.203,32.641 +2020-01-28 06:15:00,109.62,246.618,55.203,32.641 +2020-01-28 06:30:00,113.28,249.817,55.203,32.641 +2020-01-28 06:45:00,117.53,255.15900000000002,55.203,32.641 +2020-01-28 07:00:00,128.45,253.637,69.029,32.641 +2020-01-28 07:15:00,128.39,259.301,69.029,32.641 +2020-01-28 07:30:00,128.01,262.45099999999996,69.029,32.641 +2020-01-28 07:45:00,129.0,264.04200000000003,69.029,32.641 +2020-01-28 08:00:00,131.4,262.563,65.85300000000001,32.641 +2020-01-28 08:15:00,131.16,262.79400000000004,65.85300000000001,32.641 +2020-01-28 08:30:00,129.91,259.853,65.85300000000001,32.641 +2020-01-28 08:45:00,128.94,256.934,65.85300000000001,32.641 +2020-01-28 09:00:00,130.01,250.21099999999998,61.566,32.641 +2020-01-28 09:15:00,130.06,247.43,61.566,32.641 +2020-01-28 09:30:00,128.11,245.77700000000002,61.566,32.641 +2020-01-28 09:45:00,126.64,242.335,61.566,32.641 +2020-01-28 10:00:00,125.99,236.479,61.244,32.641 +2020-01-28 10:15:00,125.95,232.801,61.244,32.641 +2020-01-28 10:30:00,127.09,228.778,61.244,32.641 +2020-01-28 10:45:00,125.87,226.981,61.244,32.641 +2020-01-28 11:00:00,127.54,223.747,61.16,32.641 +2020-01-28 11:15:00,126.56,222.465,61.16,32.641 +2020-01-28 11:30:00,125.17,220.58599999999998,61.16,32.641 +2020-01-28 11:45:00,127.37,220.002,61.16,32.641 +2020-01-28 12:00:00,124.87,216.142,59.09,32.641 +2020-01-28 12:15:00,123.4,215.963,59.09,32.641 +2020-01-28 12:30:00,122.31,215.505,59.09,32.641 +2020-01-28 12:45:00,119.99,216.01,59.09,32.641 +2020-01-28 13:00:00,117.55,213.6,60.21,32.641 +2020-01-28 13:15:00,119.52,212.706,60.21,32.641 +2020-01-28 13:30:00,119.94,211.708,60.21,32.641 +2020-01-28 13:45:00,117.83,211.977,60.21,32.641 +2020-01-28 14:00:00,115.55,212.02200000000002,60.673,32.641 +2020-01-28 14:15:00,117.18,212.426,60.673,32.641 +2020-01-28 14:30:00,118.0,213.271,60.673,32.641 +2020-01-28 14:45:00,119.26,214.18,60.673,32.641 +2020-01-28 15:00:00,119.6,215.243,62.232,32.641 +2020-01-28 15:15:00,119.97,215.834,62.232,32.641 +2020-01-28 15:30:00,119.48,217.37400000000002,62.232,32.641 +2020-01-28 15:45:00,120.64,218.15,62.232,32.641 +2020-01-28 16:00:00,121.9,219.89,63.611999999999995,32.641 +2020-01-28 16:15:00,122.55,221.785,63.611999999999995,32.641 +2020-01-28 16:30:00,127.29,224.62599999999998,63.611999999999995,32.641 +2020-01-28 16:45:00,127.69,225.864,63.611999999999995,32.641 +2020-01-28 17:00:00,132.53,228.227,70.658,32.641 +2020-01-28 17:15:00,136.01,229.27,70.658,32.641 +2020-01-28 17:30:00,140.13,230.585,70.658,32.641 +2020-01-28 17:45:00,139.21,230.69,70.658,32.641 +2020-01-28 18:00:00,139.49,232.614,68.361,32.641 +2020-01-28 18:15:00,137.64,229.528,68.361,32.641 +2020-01-28 18:30:00,137.81,228.544,68.361,32.641 +2020-01-28 18:45:00,137.16,229.122,68.361,32.641 +2020-01-28 19:00:00,134.44,227.023,62.922,32.641 +2020-01-28 19:15:00,132.06,222.94400000000002,62.922,32.641 +2020-01-28 19:30:00,137.09,220.002,62.922,32.641 +2020-01-28 19:45:00,138.01,217.142,62.922,32.641 +2020-01-28 20:00:00,129.62,212.735,63.251999999999995,32.641 +2020-01-28 20:15:00,119.68,205.938,63.251999999999995,32.641 +2020-01-28 20:30:00,118.14,201.671,63.251999999999995,32.641 +2020-01-28 20:45:00,115.08,200.576,63.251999999999995,32.641 +2020-01-28 21:00:00,112.27,196.71099999999998,54.47,32.641 +2020-01-28 21:15:00,114.93,193.66400000000002,54.47,32.641 +2020-01-28 21:30:00,113.2,191.614,54.47,32.641 +2020-01-28 21:45:00,108.39,190.24,54.47,32.641 +2020-01-28 22:00:00,99.19,182.795,51.12,32.641 +2020-01-28 22:15:00,103.31,177.084,51.12,32.641 +2020-01-28 22:30:00,102.8,163.095,51.12,32.641 +2020-01-28 22:45:00,99.67,154.697,51.12,32.641 +2020-01-28 23:00:00,91.53,147.145,42.156000000000006,32.641 +2020-01-28 23:15:00,92.74,145.862,42.156000000000006,32.641 +2020-01-28 23:30:00,92.86,146.243,42.156000000000006,32.641 +2020-01-28 23:45:00,91.28,145.873,42.156000000000006,32.641 +2020-01-29 00:00:00,85.71,139.624,37.192,32.641 +2020-01-29 00:15:00,85.1,139.17700000000002,37.192,32.641 +2020-01-29 00:30:00,86.49,140.494,37.192,32.641 +2020-01-29 00:45:00,83.22,142.166,37.192,32.641 +2020-01-29 01:00:00,79.05,145.10299999999998,32.24,32.641 +2020-01-29 01:15:00,81.99,144.944,32.24,32.641 +2020-01-29 01:30:00,83.1,145.232,32.24,32.641 +2020-01-29 01:45:00,82.85,145.632,32.24,32.641 +2020-01-29 02:00:00,76.67,148.023,30.34,32.641 +2020-01-29 02:15:00,79.99,150.127,30.34,32.641 +2020-01-29 02:30:00,80.46,151.332,30.34,32.641 +2020-01-29 02:45:00,82.45,153.511,30.34,32.641 +2020-01-29 03:00:00,78.21,156.477,29.129,32.641 +2020-01-29 03:15:00,81.98,157.697,29.129,32.641 +2020-01-29 03:30:00,83.78,159.504,29.129,32.641 +2020-01-29 03:45:00,84.27,161.56799999999998,29.129,32.641 +2020-01-29 04:00:00,79.14,172.926,30.075,32.641 +2020-01-29 04:15:00,81.89,184.338,30.075,32.641 +2020-01-29 04:30:00,86.55,188.173,30.075,32.641 +2020-01-29 04:45:00,86.15,191.208,30.075,32.641 +2020-01-29 05:00:00,85.56,225.864,35.684,32.641 +2020-01-29 05:15:00,86.72,253.618,35.684,32.641 +2020-01-29 05:30:00,89.23,249.269,35.684,32.641 +2020-01-29 05:45:00,94.99,242.946,35.684,32.641 +2020-01-29 06:00:00,105.54,240.192,51.49,32.641 +2020-01-29 06:15:00,109.68,246.484,51.49,32.641 +2020-01-29 06:30:00,113.38,249.673,51.49,32.641 +2020-01-29 06:45:00,116.12,255.03099999999998,51.49,32.641 +2020-01-29 07:00:00,121.07,253.53,68.242,32.641 +2020-01-29 07:15:00,126.34,259.173,68.242,32.641 +2020-01-29 07:30:00,126.89,262.293,68.242,32.641 +2020-01-29 07:45:00,128.53,263.848,68.242,32.641 +2020-01-29 08:00:00,129.79,262.355,63.619,32.641 +2020-01-29 08:15:00,129.69,262.562,63.619,32.641 +2020-01-29 08:30:00,128.46,259.569,63.619,32.641 +2020-01-29 08:45:00,129.7,256.64,63.619,32.641 +2020-01-29 09:00:00,130.75,249.91400000000002,61.333,32.641 +2020-01-29 09:15:00,133.1,247.137,61.333,32.641 +2020-01-29 09:30:00,134.98,245.503,61.333,32.641 +2020-01-29 09:45:00,134.4,242.05900000000003,61.333,32.641 +2020-01-29 10:00:00,132.46,236.209,59.663000000000004,32.641 +2020-01-29 10:15:00,133.39,232.55200000000002,59.663000000000004,32.641 +2020-01-29 10:30:00,133.22,228.53099999999998,59.663000000000004,32.641 +2020-01-29 10:45:00,132.39,226.74400000000003,59.663000000000004,32.641 +2020-01-29 11:00:00,132.31,223.495,59.771,32.641 +2020-01-29 11:15:00,133.12,222.22,59.771,32.641 +2020-01-29 11:30:00,134.86,220.34599999999998,59.771,32.641 +2020-01-29 11:45:00,133.3,219.771,59.771,32.641 +2020-01-29 12:00:00,132.7,215.925,58.723,32.641 +2020-01-29 12:15:00,132.51,215.765,58.723,32.641 +2020-01-29 12:30:00,131.59,215.287,58.723,32.641 +2020-01-29 12:45:00,130.75,215.78900000000002,58.723,32.641 +2020-01-29 13:00:00,129.37,213.39,58.727,32.641 +2020-01-29 13:15:00,129.56,212.47400000000002,58.727,32.641 +2020-01-29 13:30:00,127.85,211.46099999999998,58.727,32.641 +2020-01-29 13:45:00,128.01,211.72799999999998,58.727,32.641 +2020-01-29 14:00:00,127.61,211.81799999999998,59.803999999999995,32.641 +2020-01-29 14:15:00,127.81,212.204,59.803999999999995,32.641 +2020-01-29 14:30:00,127.7,213.043,59.803999999999995,32.641 +2020-01-29 14:45:00,127.4,213.96900000000002,59.803999999999995,32.641 +2020-01-29 15:00:00,128.03,215.045,61.05,32.641 +2020-01-29 15:15:00,127.39,215.608,61.05,32.641 +2020-01-29 15:30:00,128.43,217.12,61.05,32.641 +2020-01-29 15:45:00,124.88,217.88,61.05,32.641 +2020-01-29 16:00:00,126.8,219.623,64.012,32.641 +2020-01-29 16:15:00,126.43,221.516,64.012,32.641 +2020-01-29 16:30:00,130.89,224.363,64.012,32.641 +2020-01-29 16:45:00,131.2,225.593,64.012,32.641 +2020-01-29 17:00:00,135.48,227.94799999999998,66.751,32.641 +2020-01-29 17:15:00,135.19,229.023,66.751,32.641 +2020-01-29 17:30:00,136.26,230.373,66.751,32.641 +2020-01-29 17:45:00,136.97,230.505,66.751,32.641 +2020-01-29 18:00:00,136.56,232.447,65.91199999999999,32.641 +2020-01-29 18:15:00,134.46,229.4,65.91199999999999,32.641 +2020-01-29 18:30:00,136.21,228.42,65.91199999999999,32.641 +2020-01-29 18:45:00,134.51,229.021,65.91199999999999,32.641 +2020-01-29 19:00:00,130.95,226.882,63.324,32.641 +2020-01-29 19:15:00,130.8,222.81099999999998,63.324,32.641 +2020-01-29 19:30:00,128.57,219.885,63.324,32.641 +2020-01-29 19:45:00,133.5,217.047,63.324,32.641 +2020-01-29 20:00:00,127.01,212.614,63.573,32.641 +2020-01-29 20:15:00,117.14,205.825,63.573,32.641 +2020-01-29 20:30:00,114.01,201.562,63.573,32.641 +2020-01-29 20:45:00,109.09,200.477,63.573,32.641 +2020-01-29 21:00:00,107.15,196.59400000000002,55.073,32.641 +2020-01-29 21:15:00,111.75,193.52900000000002,55.073,32.641 +2020-01-29 21:30:00,109.55,191.48,55.073,32.641 +2020-01-29 21:45:00,104.52,190.122,55.073,32.641 +2020-01-29 22:00:00,95.88,182.666,51.321999999999996,32.641 +2020-01-29 22:15:00,96.63,176.979,51.321999999999996,32.641 +2020-01-29 22:30:00,97.76,162.968,51.321999999999996,32.641 +2020-01-29 22:45:00,95.81,154.578,51.321999999999996,32.641 +2020-01-29 23:00:00,89.08,147.014,42.09,32.641 +2020-01-29 23:15:00,83.66,145.744,42.09,32.641 +2020-01-29 23:30:00,86.56,146.143,42.09,32.641 +2020-01-29 23:45:00,88.77,145.786,42.09,32.641 +2020-01-30 00:00:00,82.65,139.567,38.399,32.641 +2020-01-30 00:15:00,78.77,139.108,38.399,32.641 +2020-01-30 00:30:00,76.46,140.406,38.399,32.641 +2020-01-30 00:45:00,81.36,142.06799999999998,38.399,32.641 +2020-01-30 01:00:00,78.58,144.985,36.94,32.641 +2020-01-30 01:15:00,78.21,144.812,36.94,32.641 +2020-01-30 01:30:00,76.11,145.093,36.94,32.641 +2020-01-30 01:45:00,79.6,145.488,36.94,32.641 +2020-01-30 02:00:00,78.45,147.884,35.275,32.641 +2020-01-30 02:15:00,77.62,149.989,35.275,32.641 +2020-01-30 02:30:00,71.61,151.20600000000002,35.275,32.641 +2020-01-30 02:45:00,72.74,153.385,35.275,32.641 +2020-01-30 03:00:00,71.23,156.349,35.329,32.641 +2020-01-30 03:15:00,71.37,157.578,35.329,32.641 +2020-01-30 03:30:00,74.11,159.38299999999998,35.329,32.641 +2020-01-30 03:45:00,80.9,161.459,35.329,32.641 +2020-01-30 04:00:00,82.14,172.805,36.275,32.641 +2020-01-30 04:15:00,83.94,184.206,36.275,32.641 +2020-01-30 04:30:00,79.65,188.05,36.275,32.641 +2020-01-30 04:45:00,79.86,191.074,36.275,32.641 +2020-01-30 05:00:00,84.84,225.699,42.193999999999996,32.641 +2020-01-30 05:15:00,87.97,253.44799999999998,42.193999999999996,32.641 +2020-01-30 05:30:00,91.94,249.08700000000002,42.193999999999996,32.641 +2020-01-30 05:45:00,96.69,242.77599999999998,42.193999999999996,32.641 +2020-01-30 06:00:00,105.79,240.03900000000002,56.422,32.641 +2020-01-30 06:15:00,111.14,246.34400000000002,56.422,32.641 +2020-01-30 06:30:00,115.88,249.519,56.422,32.641 +2020-01-30 06:45:00,119.21,254.891,56.422,32.641 +2020-01-30 07:00:00,127.68,253.41299999999998,72.569,32.641 +2020-01-30 07:15:00,130.37,259.034,72.569,32.641 +2020-01-30 07:30:00,129.83,262.124,72.569,32.641 +2020-01-30 07:45:00,131.25,263.643,72.569,32.641 +2020-01-30 08:00:00,133.61,262.135,67.704,32.641 +2020-01-30 08:15:00,133.19,262.318,67.704,32.641 +2020-01-30 08:30:00,132.87,259.27099999999996,67.704,32.641 +2020-01-30 08:45:00,132.5,256.33299999999997,67.704,32.641 +2020-01-30 09:00:00,132.2,249.60299999999998,63.434,32.641 +2020-01-30 09:15:00,133.4,246.83,63.434,32.641 +2020-01-30 09:30:00,133.88,245.217,63.434,32.641 +2020-01-30 09:45:00,134.13,241.77200000000002,63.434,32.641 +2020-01-30 10:00:00,134.86,235.928,61.88399999999999,32.641 +2020-01-30 10:15:00,134.96,232.292,61.88399999999999,32.641 +2020-01-30 10:30:00,133.39,228.273,61.88399999999999,32.641 +2020-01-30 10:45:00,134.4,226.497,61.88399999999999,32.641 +2020-01-30 11:00:00,133.27,223.232,61.481,32.641 +2020-01-30 11:15:00,134.3,221.965,61.481,32.641 +2020-01-30 11:30:00,136.88,220.09599999999998,61.481,32.641 +2020-01-30 11:45:00,137.03,219.52900000000002,61.481,32.641 +2020-01-30 12:00:00,133.83,215.699,59.527,32.641 +2020-01-30 12:15:00,133.63,215.558,59.527,32.641 +2020-01-30 12:30:00,132.45,215.06,59.527,32.641 +2020-01-30 12:45:00,132.76,215.56,59.527,32.641 +2020-01-30 13:00:00,132.63,213.172,58.794,32.641 +2020-01-30 13:15:00,131.93,212.234,58.794,32.641 +2020-01-30 13:30:00,131.32,211.206,58.794,32.641 +2020-01-30 13:45:00,131.62,211.47099999999998,58.794,32.641 +2020-01-30 14:00:00,130.86,211.605,60.32,32.641 +2020-01-30 14:15:00,130.65,211.976,60.32,32.641 +2020-01-30 14:30:00,129.29,212.80599999999998,60.32,32.641 +2020-01-30 14:45:00,129.11,213.748,60.32,32.641 +2020-01-30 15:00:00,128.71,214.838,62.52,32.641 +2020-01-30 15:15:00,127.59,215.372,62.52,32.641 +2020-01-30 15:30:00,126.25,216.857,62.52,32.641 +2020-01-30 15:45:00,127.28,217.6,62.52,32.641 +2020-01-30 16:00:00,127.19,219.34400000000002,64.199,32.641 +2020-01-30 16:15:00,126.69,221.235,64.199,32.641 +2020-01-30 16:30:00,127.31,224.08700000000002,64.199,32.641 +2020-01-30 16:45:00,129.45,225.31,64.199,32.641 +2020-01-30 17:00:00,133.14,227.657,68.19800000000001,32.641 +2020-01-30 17:15:00,136.81,228.763,68.19800000000001,32.641 +2020-01-30 17:30:00,138.93,230.148,68.19800000000001,32.641 +2020-01-30 17:45:00,139.45,230.30700000000002,68.19800000000001,32.641 +2020-01-30 18:00:00,139.02,232.267,67.899,32.641 +2020-01-30 18:15:00,140.21,229.262,67.899,32.641 +2020-01-30 18:30:00,137.12,228.28400000000002,67.899,32.641 +2020-01-30 18:45:00,137.17,228.91099999999997,67.899,32.641 +2020-01-30 19:00:00,134.6,226.73,64.72399999999999,32.641 +2020-01-30 19:15:00,131.53,222.669,64.72399999999999,32.641 +2020-01-30 19:30:00,136.23,219.75900000000001,64.72399999999999,32.641 +2020-01-30 19:45:00,136.85,216.945,64.72399999999999,32.641 +2020-01-30 20:00:00,129.72,212.484,64.062,32.641 +2020-01-30 20:15:00,119.43,205.704,64.062,32.641 +2020-01-30 20:30:00,117.24,201.445,64.062,32.641 +2020-01-30 20:45:00,114.49,200.37099999999998,64.062,32.641 +2020-01-30 21:00:00,110.45,196.468,57.971000000000004,32.641 +2020-01-30 21:15:00,114.4,193.387,57.971000000000004,32.641 +2020-01-30 21:30:00,113.1,191.33700000000002,57.971000000000004,32.641 +2020-01-30 21:45:00,108.52,189.99599999999998,57.971000000000004,32.641 +2020-01-30 22:00:00,99.82,182.52700000000002,53.715,32.641 +2020-01-30 22:15:00,95.43,176.864,53.715,32.641 +2020-01-30 22:30:00,95.06,162.83100000000002,53.715,32.641 +2020-01-30 22:45:00,97.76,154.44899999999998,53.715,32.641 +2020-01-30 23:00:00,95.28,146.873,47.8,32.641 +2020-01-30 23:15:00,93.83,145.61700000000002,47.8,32.641 +2020-01-30 23:30:00,85.99,146.034,47.8,32.641 +2020-01-30 23:45:00,83.25,145.692,47.8,32.641 +2020-01-31 00:00:00,83.54,138.534,43.656000000000006,32.641 +2020-01-31 00:15:00,86.38,138.262,43.656000000000006,32.641 +2020-01-31 00:30:00,85.86,139.339,43.656000000000006,32.641 +2020-01-31 00:45:00,81.71,141.053,43.656000000000006,32.641 +2020-01-31 01:00:00,75.55,143.645,41.263000000000005,32.641 +2020-01-31 01:15:00,81.11,144.639,41.263000000000005,32.641 +2020-01-31 01:30:00,82.73,144.529,41.263000000000005,32.641 +2020-01-31 01:45:00,83.26,145.086,41.263000000000005,32.641 +2020-01-31 02:00:00,78.12,147.43200000000002,40.799,32.641 +2020-01-31 02:15:00,77.01,149.412,40.799,32.641 +2020-01-31 02:30:00,81.31,151.12,40.799,32.641 +2020-01-31 02:45:00,81.77,153.464,40.799,32.641 +2020-01-31 03:00:00,80.53,155.114,41.398,32.641 +2020-01-31 03:15:00,78.43,157.67700000000002,41.398,32.641 +2020-01-31 03:30:00,83.05,159.502,41.398,32.641 +2020-01-31 03:45:00,85.88,161.82299999999998,41.398,32.641 +2020-01-31 04:00:00,81.83,173.408,42.38,32.641 +2020-01-31 04:15:00,79.91,184.787,42.38,32.641 +2020-01-31 04:30:00,83.03,188.74200000000002,42.38,32.641 +2020-01-31 04:45:00,88.4,190.513,42.38,32.641 +2020-01-31 05:00:00,92.99,223.68099999999998,46.181000000000004,32.641 +2020-01-31 05:15:00,86.2,253.05200000000002,46.181000000000004,32.641 +2020-01-31 05:30:00,87.66,249.93200000000002,46.181000000000004,32.641 +2020-01-31 05:45:00,92.16,243.638,46.181000000000004,32.641 +2020-01-31 06:00:00,100.18,241.388,59.33,32.641 +2020-01-31 06:15:00,103.88,245.922,59.33,32.641 +2020-01-31 06:30:00,107.37,248.06099999999998,59.33,32.641 +2020-01-31 06:45:00,111.97,255.41,59.33,32.641 +2020-01-31 07:00:00,118.81,252.83900000000003,72.454,32.641 +2020-01-31 07:15:00,121.43,259.492,72.454,32.641 +2020-01-31 07:30:00,120.61,262.686,72.454,32.641 +2020-01-31 07:45:00,123.16,263.152,72.454,32.641 +2020-01-31 08:00:00,125.9,260.182,67.175,32.641 +2020-01-31 08:15:00,125.96,259.769,67.175,32.641 +2020-01-31 08:30:00,124.1,257.837,67.175,32.641 +2020-01-31 08:45:00,122.45,253.051,67.175,32.641 +2020-01-31 09:00:00,121.29,247.245,65.365,32.641 +2020-01-31 09:15:00,120.99,244.838,65.365,32.641 +2020-01-31 09:30:00,122.63,242.851,65.365,32.641 +2020-01-31 09:45:00,125.07,239.21400000000003,65.365,32.641 +2020-01-31 10:00:00,125.19,232.065,63.95,32.641 +2020-01-31 10:15:00,123.48,229.30700000000002,63.95,32.641 +2020-01-31 10:30:00,118.43,225.118,63.95,32.641 +2020-01-31 10:45:00,116.42,222.847,63.95,32.641 +2020-01-31 11:00:00,112.88,219.519,63.92100000000001,32.641 +2020-01-31 11:15:00,114.05,217.38099999999997,63.92100000000001,32.641 +2020-01-31 11:30:00,112.59,217.62,63.92100000000001,32.641 +2020-01-31 11:45:00,113.51,217.265,63.92100000000001,32.641 +2020-01-31 12:00:00,110.81,214.657,60.79600000000001,32.641 +2020-01-31 12:15:00,108.51,212.18099999999998,60.79600000000001,32.641 +2020-01-31 12:30:00,110.61,211.828,60.79600000000001,32.641 +2020-01-31 12:45:00,107.4,213.051,60.79600000000001,32.641 +2020-01-31 13:00:00,109.64,211.68400000000003,59.393,32.641 +2020-01-31 13:15:00,114.24,211.65400000000002,59.393,32.641 +2020-01-31 13:30:00,114.13,210.50400000000002,59.393,32.641 +2020-01-31 13:45:00,116.3,210.64700000000002,59.393,32.641 +2020-01-31 14:00:00,114.74,209.613,57.943999999999996,32.641 +2020-01-31 14:15:00,117.45,209.69099999999997,57.943999999999996,32.641 +2020-01-31 14:30:00,113.57,210.88299999999998,57.943999999999996,32.641 +2020-01-31 14:45:00,113.98,212.3,57.943999999999996,32.641 +2020-01-31 15:00:00,114.17,212.85,60.153999999999996,32.641 +2020-01-31 15:15:00,115.31,212.88099999999997,60.153999999999996,32.641 +2020-01-31 15:30:00,115.47,212.65400000000002,60.153999999999996,32.641 +2020-01-31 15:45:00,118.13,213.44,60.153999999999996,32.641 +2020-01-31 16:00:00,117.86,213.96900000000002,62.933,32.641 +2020-01-31 16:15:00,117.65,216.122,62.933,32.641 +2020-01-31 16:30:00,119.13,219.115,62.933,32.641 +2020-01-31 16:45:00,120.48,220.305,62.933,32.641 +2020-01-31 17:00:00,125.21,222.662,68.657,32.641 +2020-01-31 17:15:00,125.41,223.35,68.657,32.641 +2020-01-31 17:30:00,129.61,224.38299999999998,68.657,32.641 +2020-01-31 17:45:00,127.16,224.322,68.657,32.641 +2020-01-31 18:00:00,127.56,227.113,67.111,32.641 +2020-01-31 18:15:00,125.29,223.821,67.111,32.641 +2020-01-31 18:30:00,125.6,223.31,67.111,32.641 +2020-01-31 18:45:00,124.45,223.90200000000002,67.111,32.641 +2020-01-31 19:00:00,121.9,222.61900000000003,62.434,32.641 +2020-01-31 19:15:00,121.13,220.045,62.434,32.641 +2020-01-31 19:30:00,123.82,216.67700000000002,62.434,32.641 +2020-01-31 19:45:00,118.48,213.486,62.434,32.641 +2020-01-31 20:00:00,112.24,209.06799999999998,61.763000000000005,32.641 +2020-01-31 20:15:00,106.36,202.207,61.763000000000005,32.641 +2020-01-31 20:30:00,103.04,197.96599999999998,61.763000000000005,32.641 +2020-01-31 20:45:00,102.41,197.637,61.763000000000005,32.641 +2020-01-31 21:00:00,97.4,194.132,56.785,32.641 +2020-01-31 21:15:00,102.69,191.324,56.785,32.641 +2020-01-31 21:30:00,101.42,189.345,56.785,32.641 +2020-01-31 21:45:00,97.23,188.61700000000002,56.785,32.641 +2020-01-31 22:00:00,90.73,182.27200000000002,52.693000000000005,32.641 +2020-01-31 22:15:00,85.64,176.511,52.693000000000005,32.641 +2020-01-31 22:30:00,87.1,169.132,52.693000000000005,32.641 +2020-01-31 22:45:00,89.23,164.673,52.693000000000005,32.641 +2020-01-31 23:00:00,85.55,156.369,45.443999999999996,32.641 +2020-01-31 23:15:00,83.18,153.1,45.443999999999996,32.641 +2020-01-31 23:30:00,79.02,152.094,45.443999999999996,32.641 +2020-01-31 23:45:00,83.75,151.02700000000002,45.443999999999996,32.641 +2020-02-01 00:00:00,76.71,129.81,42.033,32.431999999999995 +2020-02-01 00:15:00,73.23,124.75399999999999,42.033,32.431999999999995 +2020-02-01 00:30:00,70.83,127.20700000000001,42.033,32.431999999999995 +2020-02-01 00:45:00,74.17,129.618,42.033,32.431999999999995 +2020-02-01 01:00:00,71.69,132.734,38.255,32.431999999999995 +2020-02-01 01:15:00,71.85,132.577,38.255,32.431999999999995 +2020-02-01 01:30:00,67.13,132.036,38.255,32.431999999999995 +2020-02-01 01:45:00,71.17,132.134,38.255,32.431999999999995 +2020-02-01 02:00:00,69.97,135.287,36.404,32.431999999999995 +2020-02-01 02:15:00,69.51,136.813,36.404,32.431999999999995 +2020-02-01 02:30:00,67.27,137.26,36.404,32.431999999999995 +2020-02-01 02:45:00,73.49,139.49,36.404,32.431999999999995 +2020-02-01 03:00:00,69.19,141.94,36.083,32.431999999999995 +2020-02-01 03:15:00,67.7,143.183,36.083,32.431999999999995 +2020-02-01 03:30:00,62.05,143.143,36.083,32.431999999999995 +2020-02-01 03:45:00,63.49,145.23,36.083,32.431999999999995 +2020-02-01 04:00:00,67.97,152.216,36.102,32.431999999999995 +2020-02-01 04:15:00,68.64,160.666,36.102,32.431999999999995 +2020-02-01 04:30:00,65.01,162.115,36.102,32.431999999999995 +2020-02-01 04:45:00,63.87,163.159,36.102,32.431999999999995 +2020-02-01 05:00:00,64.66,178.486,35.284,32.431999999999995 +2020-02-01 05:15:00,65.5,187.328,35.284,32.431999999999995 +2020-02-01 05:30:00,65.69,184.41,35.284,32.431999999999995 +2020-02-01 05:45:00,67.86,183.62,35.284,32.431999999999995 +2020-02-01 06:00:00,69.24,201.104,36.265,32.431999999999995 +2020-02-01 06:15:00,71.37,222.93900000000002,36.265,32.431999999999995 +2020-02-01 06:30:00,71.93,219.313,36.265,32.431999999999995 +2020-02-01 06:45:00,74.95,216.52599999999998,36.265,32.431999999999995 +2020-02-01 07:00:00,78.52,210.303,40.714,32.431999999999995 +2020-02-01 07:15:00,80.13,215.368,40.714,32.431999999999995 +2020-02-01 07:30:00,82.2,221.09,40.714,32.431999999999995 +2020-02-01 07:45:00,85.69,225.72099999999998,40.714,32.431999999999995 +2020-02-01 08:00:00,91.13,227.44099999999997,46.692,32.431999999999995 +2020-02-01 08:15:00,91.12,231.018,46.692,32.431999999999995 +2020-02-01 08:30:00,92.79,230.90099999999998,46.692,32.431999999999995 +2020-02-01 08:45:00,94.88,229.52900000000002,46.692,32.431999999999995 +2020-02-01 09:00:00,97.7,225.294,48.925,32.431999999999995 +2020-02-01 09:15:00,97.76,223.685,48.925,32.431999999999995 +2020-02-01 09:30:00,98.69,222.65200000000002,48.925,32.431999999999995 +2020-02-01 09:45:00,101.05,219.40099999999998,48.925,32.431999999999995 +2020-02-01 10:00:00,101.31,212.862,47.799,32.431999999999995 +2020-02-01 10:15:00,101.73,210.19,47.799,32.431999999999995 +2020-02-01 10:30:00,101.39,206.543,47.799,32.431999999999995 +2020-02-01 10:45:00,106.31,205.921,47.799,32.431999999999995 +2020-02-01 11:00:00,103.02,203.19299999999998,44.309,32.431999999999995 +2020-02-01 11:15:00,103.3,200.252,44.309,32.431999999999995 +2020-02-01 11:30:00,108.4,199.29,44.309,32.431999999999995 +2020-02-01 11:45:00,112.35,197.61900000000003,44.309,32.431999999999995 +2020-02-01 12:00:00,105.64,193.695,42.367,32.431999999999995 +2020-02-01 12:15:00,104.87,191.93599999999998,42.367,32.431999999999995 +2020-02-01 12:30:00,102.75,191.995,42.367,32.431999999999995 +2020-02-01 12:45:00,101.19,192.31900000000002,42.367,32.431999999999995 +2020-02-01 13:00:00,98.77,190.88400000000001,39.036,32.431999999999995 +2020-02-01 13:15:00,102.03,188.609,39.036,32.431999999999995 +2020-02-01 13:30:00,97.87,187.08700000000002,39.036,32.431999999999995 +2020-02-01 13:45:00,97.89,187.785,39.036,32.431999999999995 +2020-02-01 14:00:00,95.54,188.062,37.995,32.431999999999995 +2020-02-01 14:15:00,95.43,187.68599999999998,37.995,32.431999999999995 +2020-02-01 14:30:00,94.24,186.92700000000002,37.995,32.431999999999995 +2020-02-01 14:45:00,95.58,188.394,37.995,32.431999999999995 +2020-02-01 15:00:00,95.01,189.855,40.71,32.431999999999995 +2020-02-01 15:15:00,94.99,190.50799999999998,40.71,32.431999999999995 +2020-02-01 15:30:00,93.2,192.142,40.71,32.431999999999995 +2020-02-01 15:45:00,93.06,193.253,40.71,32.431999999999995 +2020-02-01 16:00:00,95.23,191.983,46.998000000000005,32.431999999999995 +2020-02-01 16:15:00,95.22,195.12099999999998,46.998000000000005,32.431999999999995 +2020-02-01 16:30:00,97.78,198.024,46.998000000000005,32.431999999999995 +2020-02-01 16:45:00,99.14,200.262,46.998000000000005,32.431999999999995 +2020-02-01 17:00:00,105.82,202.077,55.431000000000004,32.431999999999995 +2020-02-01 17:15:00,106.49,204.93599999999998,55.431000000000004,32.431999999999995 +2020-02-01 17:30:00,107.67,205.97799999999998,55.431000000000004,32.431999999999995 +2020-02-01 17:45:00,109.78,205.5,55.431000000000004,32.431999999999995 +2020-02-01 18:00:00,112.26,207.61599999999999,55.989,32.431999999999995 +2020-02-01 18:15:00,110.96,206.43400000000003,55.989,32.431999999999995 +2020-02-01 18:30:00,109.62,207.187,55.989,32.431999999999995 +2020-02-01 18:45:00,108.33,204.48,55.989,32.431999999999995 +2020-02-01 19:00:00,106.53,204.679,50.882,32.431999999999995 +2020-02-01 19:15:00,105.46,201.778,50.882,32.431999999999995 +2020-02-01 19:30:00,104.21,199.399,50.882,32.431999999999995 +2020-02-01 19:45:00,102.98,195.791,50.882,32.431999999999995 +2020-02-01 20:00:00,97.91,193.72299999999998,43.172,32.431999999999995 +2020-02-01 20:15:00,94.51,189.55700000000002,43.172,32.431999999999995 +2020-02-01 20:30:00,91.8,185.125,43.172,32.431999999999995 +2020-02-01 20:45:00,90.04,184.016,43.172,32.431999999999995 +2020-02-01 21:00:00,84.99,183.33900000000003,37.599000000000004,32.431999999999995 +2020-02-01 21:15:00,84.93,181.149,37.599000000000004,32.431999999999995 +2020-02-01 21:30:00,83.67,180.52,37.599000000000004,32.431999999999995 +2020-02-01 21:45:00,82.32,179.476,37.599000000000004,32.431999999999995 +2020-02-01 22:00:00,78.77,174.79,39.047,32.431999999999995 +2020-02-01 22:15:00,79.21,171.956,39.047,32.431999999999995 +2020-02-01 22:30:00,75.78,171.52200000000002,39.047,32.431999999999995 +2020-02-01 22:45:00,74.58,169.205,39.047,32.431999999999995 +2020-02-01 23:00:00,71.15,163.97299999999998,32.339,32.431999999999995 +2020-02-01 23:15:00,70.35,158.893,32.339,32.431999999999995 +2020-02-01 23:30:00,67.66,155.857,32.339,32.431999999999995 +2020-02-01 23:45:00,67.11,152.153,32.339,32.431999999999995 +2020-02-02 00:00:00,63.53,130.029,29.988000000000003,32.431999999999995 +2020-02-02 00:15:00,63.8,124.745,29.988000000000003,32.431999999999995 +2020-02-02 00:30:00,62.55,126.78,29.988000000000003,32.431999999999995 +2020-02-02 00:45:00,61.0,129.969,29.988000000000003,32.431999999999995 +2020-02-02 01:00:00,57.82,132.872,28.531999999999996,32.431999999999995 +2020-02-02 01:15:00,58.55,133.874,28.531999999999996,32.431999999999995 +2020-02-02 01:30:00,58.11,133.923,28.531999999999996,32.431999999999995 +2020-02-02 01:45:00,57.64,133.714,28.531999999999996,32.431999999999995 +2020-02-02 02:00:00,55.93,136.03,27.805999999999997,32.431999999999995 +2020-02-02 02:15:00,56.4,136.515,27.805999999999997,32.431999999999995 +2020-02-02 02:30:00,55.63,137.906,27.805999999999997,32.431999999999995 +2020-02-02 02:45:00,55.85,140.687,27.805999999999997,32.431999999999995 +2020-02-02 03:00:00,54.61,143.411,26.193,32.431999999999995 +2020-02-02 03:15:00,55.42,144.041,26.193,32.431999999999995 +2020-02-02 03:30:00,55.76,145.66,26.193,32.431999999999995 +2020-02-02 03:45:00,55.94,147.775,26.193,32.431999999999995 +2020-02-02 04:00:00,55.92,154.489,27.19,32.431999999999995 +2020-02-02 04:15:00,57.67,161.849,27.19,32.431999999999995 +2020-02-02 04:30:00,57.96,163.20600000000002,27.19,32.431999999999995 +2020-02-02 04:45:00,58.08,164.608,27.19,32.431999999999995 +2020-02-02 05:00:00,58.81,175.93599999999998,28.166999999999998,32.431999999999995 +2020-02-02 05:15:00,60.25,182.16099999999997,28.166999999999998,32.431999999999995 +2020-02-02 05:30:00,60.89,179.095,28.166999999999998,32.431999999999995 +2020-02-02 05:45:00,61.68,178.62599999999998,28.166999999999998,32.431999999999995 +2020-02-02 06:00:00,62.19,196.321,27.16,32.431999999999995 +2020-02-02 06:15:00,62.48,216.12400000000002,27.16,32.431999999999995 +2020-02-02 06:30:00,63.17,211.331,27.16,32.431999999999995 +2020-02-02 06:45:00,64.8,207.49599999999998,27.16,32.431999999999995 +2020-02-02 07:00:00,68.38,204.016,29.578000000000003,32.431999999999995 +2020-02-02 07:15:00,68.86,208.31099999999998,29.578000000000003,32.431999999999995 +2020-02-02 07:30:00,69.25,212.454,29.578000000000003,32.431999999999995 +2020-02-02 07:45:00,71.7,216.199,29.578000000000003,32.431999999999995 +2020-02-02 08:00:00,76.25,219.887,34.650999999999996,32.431999999999995 +2020-02-02 08:15:00,77.74,223.195,34.650999999999996,32.431999999999995 +2020-02-02 08:30:00,79.71,224.77,34.650999999999996,32.431999999999995 +2020-02-02 08:45:00,79.84,225.657,34.650999999999996,32.431999999999995 +2020-02-02 09:00:00,84.28,220.99099999999999,38.080999999999996,32.431999999999995 +2020-02-02 09:15:00,85.13,220.058,38.080999999999996,32.431999999999995 +2020-02-02 09:30:00,86.37,218.822,38.080999999999996,32.431999999999995 +2020-02-02 09:45:00,88.68,215.322,38.080999999999996,32.431999999999995 +2020-02-02 10:00:00,88.73,211.52200000000002,39.934,32.431999999999995 +2020-02-02 10:15:00,89.78,209.44799999999998,39.934,32.431999999999995 +2020-02-02 10:30:00,92.29,206.45,39.934,32.431999999999995 +2020-02-02 10:45:00,94.48,203.609,39.934,32.431999999999995 +2020-02-02 11:00:00,96.74,201.93599999999998,43.74100000000001,32.431999999999995 +2020-02-02 11:15:00,100.32,199.2,43.74100000000001,32.431999999999995 +2020-02-02 11:30:00,102.7,197.222,43.74100000000001,32.431999999999995 +2020-02-02 11:45:00,103.15,196.201,43.74100000000001,32.431999999999995 +2020-02-02 12:00:00,99.58,191.542,40.001999999999995,32.431999999999995 +2020-02-02 12:15:00,98.64,191.982,40.001999999999995,32.431999999999995 +2020-02-02 12:30:00,95.5,190.382,40.001999999999995,32.431999999999995 +2020-02-02 12:45:00,93.0,189.704,40.001999999999995,32.431999999999995 +2020-02-02 13:00:00,90.71,187.521,37.855,32.431999999999995 +2020-02-02 13:15:00,89.28,188.63099999999997,37.855,32.431999999999995 +2020-02-02 13:30:00,87.1,186.988,37.855,32.431999999999995 +2020-02-02 13:45:00,86.01,186.828,37.855,32.431999999999995 +2020-02-02 14:00:00,84.14,187.291,35.946999999999996,32.431999999999995 +2020-02-02 14:15:00,84.33,188.176,35.946999999999996,32.431999999999995 +2020-02-02 14:30:00,83.61,188.908,35.946999999999996,32.431999999999995 +2020-02-02 14:45:00,84.39,190.054,35.946999999999996,32.431999999999995 +2020-02-02 15:00:00,84.93,189.815,35.138000000000005,32.431999999999995 +2020-02-02 15:15:00,84.41,191.359,35.138000000000005,32.431999999999995 +2020-02-02 15:30:00,83.67,193.63299999999998,35.138000000000005,32.431999999999995 +2020-02-02 15:45:00,83.84,195.46,35.138000000000005,32.431999999999995 +2020-02-02 16:00:00,85.14,196.38099999999997,38.672,32.431999999999995 +2020-02-02 16:15:00,85.03,198.49599999999998,38.672,32.431999999999995 +2020-02-02 16:30:00,86.66,201.58599999999998,38.672,32.431999999999995 +2020-02-02 16:45:00,89.42,203.954,38.672,32.431999999999995 +2020-02-02 17:00:00,94.56,205.68900000000002,48.684,32.431999999999995 +2020-02-02 17:15:00,97.47,208.07299999999998,48.684,32.431999999999995 +2020-02-02 17:30:00,99.16,209.385,48.684,32.431999999999995 +2020-02-02 17:45:00,100.99,211.365,48.684,32.431999999999995 +2020-02-02 18:00:00,105.14,212.826,51.568999999999996,32.431999999999995 +2020-02-02 18:15:00,103.39,213.162,51.568999999999996,32.431999999999995 +2020-02-02 18:30:00,106.37,211.679,51.568999999999996,32.431999999999995 +2020-02-02 18:45:00,102.89,210.99400000000003,51.568999999999996,32.431999999999995 +2020-02-02 19:00:00,101.38,210.583,48.608000000000004,32.431999999999995 +2020-02-02 19:15:00,99.21,208.429,48.608000000000004,32.431999999999995 +2020-02-02 19:30:00,98.13,205.91099999999997,48.608000000000004,32.431999999999995 +2020-02-02 19:45:00,98.79,203.928,48.608000000000004,32.431999999999995 +2020-02-02 20:00:00,93.72,201.81799999999998,43.733999999999995,32.431999999999995 +2020-02-02 20:15:00,91.94,198.74400000000003,43.733999999999995,32.431999999999995 +2020-02-02 20:30:00,90.61,195.58700000000002,43.733999999999995,32.431999999999995 +2020-02-02 20:45:00,89.18,193.338,43.733999999999995,32.431999999999995 +2020-02-02 21:00:00,86.51,189.835,39.283,32.431999999999995 +2020-02-02 21:15:00,86.22,186.979,39.283,32.431999999999995 +2020-02-02 21:30:00,86.04,186.727,39.283,32.431999999999995 +2020-02-02 21:45:00,87.9,185.815,39.283,32.431999999999995 +2020-02-02 22:00:00,84.32,179.638,40.111,32.431999999999995 +2020-02-02 22:15:00,85.45,176.153,40.111,32.431999999999995 +2020-02-02 22:30:00,83.23,172.30900000000003,40.111,32.431999999999995 +2020-02-02 22:45:00,82.39,169.19,40.111,32.431999999999995 +2020-02-02 23:00:00,78.06,160.942,35.791,32.431999999999995 +2020-02-02 23:15:00,78.99,157.774,35.791,32.431999999999995 +2020-02-02 23:30:00,76.63,155.68200000000002,35.791,32.431999999999995 +2020-02-02 23:45:00,77.09,152.945,35.791,32.431999999999995 +2020-02-03 00:00:00,72.55,134.424,34.311,32.613 +2020-02-03 00:15:00,73.3,132.341,34.311,32.613 +2020-02-03 00:30:00,72.72,134.543,34.311,32.613 +2020-02-03 00:45:00,71.94,137.173,34.311,32.613 +2020-02-03 01:00:00,69.66,140.04,34.585,32.613 +2020-02-03 01:15:00,70.92,140.44,34.585,32.613 +2020-02-03 01:30:00,70.34,140.496,34.585,32.613 +2020-02-03 01:45:00,70.86,140.424,34.585,32.613 +2020-02-03 02:00:00,69.94,142.671,34.111,32.613 +2020-02-03 02:15:00,71.57,144.918,34.111,32.613 +2020-02-03 02:30:00,70.86,146.672,34.111,32.613 +2020-02-03 02:45:00,72.17,148.778,34.111,32.613 +2020-02-03 03:00:00,71.64,152.88299999999998,32.435,32.613 +2020-02-03 03:15:00,71.9,155.274,32.435,32.613 +2020-02-03 03:30:00,73.09,156.496,32.435,32.613 +2020-02-03 03:45:00,73.72,158.063,32.435,32.613 +2020-02-03 04:00:00,74.52,169.278,33.04,32.613 +2020-02-03 04:15:00,75.25,180.90200000000002,33.04,32.613 +2020-02-03 04:30:00,76.93,184.803,33.04,32.613 +2020-02-03 04:45:00,80.1,186.326,33.04,32.613 +2020-02-03 05:00:00,83.89,214.47799999999998,40.399,32.613 +2020-02-03 05:15:00,86.25,242.049,40.399,32.613 +2020-02-03 05:30:00,91.33,239.489,40.399,32.613 +2020-02-03 05:45:00,95.99,233.071,40.399,32.613 +2020-02-03 06:00:00,104.74,231.924,60.226000000000006,32.613 +2020-02-03 06:15:00,110.75,236.253,60.226000000000006,32.613 +2020-02-03 06:30:00,114.63,239.93900000000002,60.226000000000006,32.613 +2020-02-03 06:45:00,119.48,245.31099999999998,60.226000000000006,32.613 +2020-02-03 07:00:00,129.14,244.451,73.578,32.613 +2020-02-03 07:15:00,130.05,249.81900000000002,73.578,32.613 +2020-02-03 07:30:00,129.17,253.173,73.578,32.613 +2020-02-03 07:45:00,130.1,253.949,73.578,32.613 +2020-02-03 08:00:00,135.43,252.31099999999998,66.58,32.613 +2020-02-03 08:15:00,135.0,253.35,66.58,32.613 +2020-02-03 08:30:00,134.72,250.42,66.58,32.613 +2020-02-03 08:45:00,136.03,247.53799999999998,66.58,32.613 +2020-02-03 09:00:00,138.01,241.83900000000003,62.0,32.613 +2020-02-03 09:15:00,139.79,237.245,62.0,32.613 +2020-02-03 09:30:00,141.36,235.02900000000002,62.0,32.613 +2020-02-03 09:45:00,139.59,232.144,62.0,32.613 +2020-02-03 10:00:00,138.43,227.13400000000001,59.099,32.613 +2020-02-03 10:15:00,138.67,224.736,59.099,32.613 +2020-02-03 10:30:00,137.73,220.82299999999998,59.099,32.613 +2020-02-03 10:45:00,137.82,219.03,59.099,32.613 +2020-02-03 11:00:00,138.07,214.34599999999998,57.729,32.613 +2020-02-03 11:15:00,139.42,213.59,57.729,32.613 +2020-02-03 11:30:00,139.86,213.06599999999997,57.729,32.613 +2020-02-03 11:45:00,140.03,211.54,57.729,32.613 +2020-02-03 12:00:00,139.07,209.048,55.615,32.613 +2020-02-03 12:15:00,138.54,209.49200000000002,55.615,32.613 +2020-02-03 12:30:00,136.96,208.28099999999998,55.615,32.613 +2020-02-03 12:45:00,137.41,209.297,55.615,32.613 +2020-02-03 13:00:00,134.33,207.68,56.515,32.613 +2020-02-03 13:15:00,132.51,207.333,56.515,32.613 +2020-02-03 13:30:00,130.0,205.06400000000002,56.515,32.613 +2020-02-03 13:45:00,128.89,204.83,56.515,32.613 +2020-02-03 14:00:00,128.86,204.725,58.1,32.613 +2020-02-03 14:15:00,127.42,204.808,58.1,32.613 +2020-02-03 14:30:00,127.1,204.94799999999998,58.1,32.613 +2020-02-03 14:45:00,128.03,205.826,58.1,32.613 +2020-02-03 15:00:00,129.69,207.56599999999997,59.801,32.613 +2020-02-03 15:15:00,135.62,207.56099999999998,59.801,32.613 +2020-02-03 15:30:00,130.78,208.795,59.801,32.613 +2020-02-03 15:45:00,132.56,210.171,59.801,32.613 +2020-02-03 16:00:00,130.0,211.095,62.901,32.613 +2020-02-03 16:15:00,128.75,212.358,62.901,32.613 +2020-02-03 16:30:00,126.25,214.46200000000002,62.901,32.613 +2020-02-03 16:45:00,126.93,215.51,62.901,32.613 +2020-02-03 17:00:00,132.31,217.107,70.418,32.613 +2020-02-03 17:15:00,134.91,218.438,70.418,32.613 +2020-02-03 17:30:00,139.16,219.222,70.418,32.613 +2020-02-03 17:45:00,137.81,219.636,70.418,32.613 +2020-02-03 18:00:00,140.89,221.644,71.726,32.613 +2020-02-03 18:15:00,138.81,219.799,71.726,32.613 +2020-02-03 18:30:00,142.27,219.105,71.726,32.613 +2020-02-03 18:45:00,138.78,218.90900000000002,71.726,32.613 +2020-02-03 19:00:00,137.55,216.71,65.997,32.613 +2020-02-03 19:15:00,133.6,213.172,65.997,32.613 +2020-02-03 19:30:00,132.16,211.24599999999998,65.997,32.613 +2020-02-03 19:45:00,130.1,208.445,65.997,32.613 +2020-02-03 20:00:00,123.56,203.873,68.09100000000001,32.613 +2020-02-03 20:15:00,121.51,197.967,68.09100000000001,32.613 +2020-02-03 20:30:00,117.5,192.733,68.09100000000001,32.613 +2020-02-03 20:45:00,116.06,192.24400000000003,68.09100000000001,32.613 +2020-02-03 21:00:00,112.08,189.36900000000003,59.617,32.613 +2020-02-03 21:15:00,119.36,185.173,59.617,32.613 +2020-02-03 21:30:00,115.51,183.97799999999998,59.617,32.613 +2020-02-03 21:45:00,112.17,182.554,59.617,32.613 +2020-02-03 22:00:00,102.03,173.428,54.938,32.613 +2020-02-03 22:15:00,101.13,168.33,54.938,32.613 +2020-02-03 22:30:00,103.23,154.47899999999998,54.938,32.613 +2020-02-03 22:45:00,102.28,146.101,54.938,32.613 +2020-02-03 23:00:00,97.78,138.697,47.43,32.613 +2020-02-03 23:15:00,93.27,138.597,47.43,32.613 +2020-02-03 23:30:00,87.74,139.533,47.43,32.613 +2020-02-03 23:45:00,93.42,139.745,47.43,32.613 +2020-02-04 00:00:00,90.86,133.845,48.354,32.613 +2020-02-04 00:15:00,91.14,133.235,48.354,32.613 +2020-02-04 00:30:00,85.35,134.276,48.354,32.613 +2020-02-04 00:45:00,87.43,135.703,48.354,32.613 +2020-02-04 01:00:00,85.05,138.391,45.68600000000001,32.613 +2020-02-04 01:15:00,86.31,138.264,45.68600000000001,32.613 +2020-02-04 01:30:00,82.43,138.523,45.68600000000001,32.613 +2020-02-04 01:45:00,87.86,138.858,45.68600000000001,32.613 +2020-02-04 02:00:00,86.41,141.167,44.269,32.613 +2020-02-04 02:15:00,87.36,143.13,44.269,32.613 +2020-02-04 02:30:00,82.98,144.28,44.269,32.613 +2020-02-04 02:45:00,87.34,146.35399999999998,44.269,32.613 +2020-02-04 03:00:00,85.37,149.16899999999998,44.187,32.613 +2020-02-04 03:15:00,87.11,150.472,44.187,32.613 +2020-02-04 03:30:00,84.96,152.24200000000002,44.187,32.613 +2020-02-04 03:45:00,83.22,154.197,44.187,32.613 +2020-02-04 04:00:00,82.45,165.356,46.126999999999995,32.613 +2020-02-04 04:15:00,83.66,176.574,46.126999999999995,32.613 +2020-02-04 04:30:00,85.46,180.15099999999998,46.126999999999995,32.613 +2020-02-04 04:45:00,87.52,182.96599999999998,46.126999999999995,32.613 +2020-02-04 05:00:00,92.46,216.549,49.666000000000004,32.613 +2020-02-04 05:15:00,94.39,243.795,49.666000000000004,32.613 +2020-02-04 05:30:00,96.94,239.44099999999997,49.666000000000004,32.613 +2020-02-04 05:45:00,101.07,233.153,49.666000000000004,32.613 +2020-02-04 06:00:00,109.2,230.50799999999998,61.077,32.613 +2020-02-04 06:15:00,111.14,236.672,61.077,32.613 +2020-02-04 06:30:00,117.58,239.65099999999998,61.077,32.613 +2020-02-04 06:45:00,123.45,244.737,61.077,32.613 +2020-02-04 07:00:00,133.3,243.669,74.717,32.613 +2020-02-04 07:15:00,131.29,248.86900000000003,74.717,32.613 +2020-02-04 07:30:00,133.43,251.56799999999998,74.717,32.613 +2020-02-04 07:45:00,133.13,252.69799999999998,74.717,32.613 +2020-02-04 08:00:00,136.35,251.172,69.033,32.613 +2020-02-04 08:15:00,136.11,251.097,69.033,32.613 +2020-02-04 08:30:00,134.66,247.888,69.033,32.613 +2020-02-04 08:45:00,134.24,244.832,69.033,32.613 +2020-02-04 09:00:00,133.77,238.14700000000002,63.113,32.613 +2020-02-04 09:15:00,135.41,235.375,63.113,32.613 +2020-02-04 09:30:00,136.68,233.847,63.113,32.613 +2020-02-04 09:45:00,135.45,230.52900000000002,63.113,32.613 +2020-02-04 10:00:00,134.9,225.05700000000002,61.461999999999996,32.613 +2020-02-04 10:15:00,134.83,221.503,61.461999999999996,32.613 +2020-02-04 10:30:00,135.76,217.795,61.461999999999996,32.613 +2020-02-04 10:45:00,134.18,216.19,61.461999999999996,32.613 +2020-02-04 11:00:00,131.66,213.17,59.614,32.613 +2020-02-04 11:15:00,132.42,211.995,59.614,32.613 +2020-02-04 11:30:00,131.61,210.327,59.614,32.613 +2020-02-04 11:45:00,129.45,209.65900000000002,59.614,32.613 +2020-02-04 12:00:00,126.71,205.668,57.415,32.613 +2020-02-04 12:15:00,125.47,205.61599999999999,57.415,32.613 +2020-02-04 12:30:00,128.08,205.076,57.415,32.613 +2020-02-04 12:45:00,125.59,205.66400000000002,57.415,32.613 +2020-02-04 13:00:00,123.19,203.65400000000002,58.534,32.613 +2020-02-04 13:15:00,122.62,202.643,58.534,32.613 +2020-02-04 13:30:00,119.8,201.68,58.534,32.613 +2020-02-04 13:45:00,120.47,201.791,58.534,32.613 +2020-02-04 14:00:00,121.58,201.998,59.415,32.613 +2020-02-04 14:15:00,123.66,202.265,59.415,32.613 +2020-02-04 14:30:00,122.74,203.05599999999998,59.415,32.613 +2020-02-04 14:45:00,123.25,203.967,59.415,32.613 +2020-02-04 15:00:00,126.72,205.26,62.071999999999996,32.613 +2020-02-04 15:15:00,128.05,205.477,62.071999999999996,32.613 +2020-02-04 15:30:00,126.41,206.958,62.071999999999996,32.613 +2020-02-04 15:45:00,125.03,207.822,62.071999999999996,32.613 +2020-02-04 16:00:00,125.33,209.22799999999998,64.99,32.613 +2020-02-04 16:15:00,124.64,211.00400000000002,64.99,32.613 +2020-02-04 16:30:00,127.25,213.877,64.99,32.613 +2020-02-04 16:45:00,128.17,215.144,64.99,32.613 +2020-02-04 17:00:00,133.12,217.313,72.658,32.613 +2020-02-04 17:15:00,136.25,218.657,72.658,32.613 +2020-02-04 17:30:00,140.8,220.261,72.658,32.613 +2020-02-04 17:45:00,141.04,220.595,72.658,32.613 +2020-02-04 18:00:00,142.74,222.60299999999998,73.645,32.613 +2020-02-04 18:15:00,143.03,220.092,73.645,32.613 +2020-02-04 18:30:00,140.63,219.092,73.645,32.613 +2020-02-04 18:45:00,138.97,219.81400000000002,73.645,32.613 +2020-02-04 19:00:00,135.76,217.78,67.085,32.613 +2020-02-04 19:15:00,134.8,213.929,67.085,32.613 +2020-02-04 19:30:00,140.27,211.31400000000002,67.085,32.613 +2020-02-04 19:45:00,133.39,208.546,67.085,32.613 +2020-02-04 20:00:00,125.17,204.082,66.138,32.613 +2020-02-04 20:15:00,128.55,197.642,66.138,32.613 +2020-02-04 20:30:00,119.49,193.523,66.138,32.613 +2020-02-04 20:45:00,118.62,192.36700000000002,66.138,32.613 +2020-02-04 21:00:00,111.08,188.612,57.512,32.613 +2020-02-04 21:15:00,115.78,185.55599999999998,57.512,32.613 +2020-02-04 21:30:00,115.31,183.541,57.512,32.613 +2020-02-04 21:45:00,112.98,182.362,57.512,32.613 +2020-02-04 22:00:00,101.24,175.079,54.545,32.613 +2020-02-04 22:15:00,101.11,169.757,54.545,32.613 +2020-02-04 22:30:00,101.29,155.946,54.545,32.613 +2020-02-04 22:45:00,102.32,147.877,54.545,32.613 +2020-02-04 23:00:00,97.65,140.57399999999998,48.605,32.613 +2020-02-04 23:15:00,94.45,139.328,48.605,32.613 +2020-02-04 23:30:00,87.36,139.886,48.605,32.613 +2020-02-04 23:45:00,92.55,139.616,48.605,32.613 +2020-02-05 00:00:00,89.46,133.739,45.675,32.613 +2020-02-05 00:15:00,90.37,133.12,45.675,32.613 +2020-02-05 00:30:00,85.45,134.141,45.675,32.613 +2020-02-05 00:45:00,84.27,135.56,45.675,32.613 +2020-02-05 01:00:00,85.32,138.22299999999998,43.015,32.613 +2020-02-05 01:15:00,85.92,138.083,43.015,32.613 +2020-02-05 01:30:00,85.0,138.333,43.015,32.613 +2020-02-05 01:45:00,83.16,138.666,43.015,32.613 +2020-02-05 02:00:00,83.15,140.976,41.0,32.613 +2020-02-05 02:15:00,77.78,142.938,41.0,32.613 +2020-02-05 02:30:00,81.84,144.102,41.0,32.613 +2020-02-05 02:45:00,86.05,146.175,41.0,32.613 +2020-02-05 03:00:00,81.25,148.99200000000002,41.318000000000005,32.613 +2020-02-05 03:15:00,88.66,150.3,41.318000000000005,32.613 +2020-02-05 03:30:00,89.0,152.067,41.318000000000005,32.613 +2020-02-05 03:45:00,89.87,154.033,41.318000000000005,32.613 +2020-02-05 04:00:00,85.75,165.185,42.544,32.613 +2020-02-05 04:15:00,82.09,176.393,42.544,32.613 +2020-02-05 04:30:00,83.82,179.982,42.544,32.613 +2020-02-05 04:45:00,88.66,182.785,42.544,32.613 +2020-02-05 05:00:00,90.45,216.34400000000002,45.161,32.613 +2020-02-05 05:15:00,92.16,243.595,45.161,32.613 +2020-02-05 05:30:00,94.66,239.22400000000002,45.161,32.613 +2020-02-05 05:45:00,100.11,232.94299999999998,45.161,32.613 +2020-02-05 06:00:00,110.31,230.312,61.86600000000001,32.613 +2020-02-05 06:15:00,112.52,236.489,61.86600000000001,32.613 +2020-02-05 06:30:00,118.94,239.447,61.86600000000001,32.613 +2020-02-05 06:45:00,122.35,244.541,61.86600000000001,32.613 +2020-02-05 07:00:00,129.73,243.495,77.814,32.613 +2020-02-05 07:15:00,130.12,248.67,77.814,32.613 +2020-02-05 07:30:00,132.0,251.338,77.814,32.613 +2020-02-05 07:45:00,130.03,252.429,77.814,32.613 +2020-02-05 08:00:00,133.79,250.885,70.251,32.613 +2020-02-05 08:15:00,132.59,250.78599999999997,70.251,32.613 +2020-02-05 08:30:00,134.41,247.52200000000002,70.251,32.613 +2020-02-05 08:45:00,131.14,244.46200000000002,70.251,32.613 +2020-02-05 09:00:00,131.14,237.77700000000002,66.965,32.613 +2020-02-05 09:15:00,131.67,235.00900000000001,66.965,32.613 +2020-02-05 09:30:00,133.35,233.50099999999998,66.965,32.613 +2020-02-05 09:45:00,132.54,230.18200000000002,66.965,32.613 +2020-02-05 10:00:00,133.0,224.718,63.628,32.613 +2020-02-05 10:15:00,130.23,221.18900000000002,63.628,32.613 +2020-02-05 10:30:00,128.04,217.488,63.628,32.613 +2020-02-05 10:45:00,128.01,215.894,63.628,32.613 +2020-02-05 11:00:00,127.57,212.861,62.516999999999996,32.613 +2020-02-05 11:15:00,126.97,211.695,62.516999999999996,32.613 +2020-02-05 11:30:00,127.26,210.033,62.516999999999996,32.613 +2020-02-05 11:45:00,128.78,209.375,62.516999999999996,32.613 +2020-02-05 12:00:00,125.28,205.40099999999998,60.888999999999996,32.613 +2020-02-05 12:15:00,123.86,205.365,60.888999999999996,32.613 +2020-02-05 12:30:00,125.88,204.801,60.888999999999996,32.613 +2020-02-05 12:45:00,122.32,205.387,60.888999999999996,32.613 +2020-02-05 13:00:00,120.11,203.394,61.57899999999999,32.613 +2020-02-05 13:15:00,120.74,202.361,61.57899999999999,32.613 +2020-02-05 13:30:00,121.03,201.382,61.57899999999999,32.613 +2020-02-05 13:45:00,121.19,201.493,61.57899999999999,32.613 +2020-02-05 14:00:00,119.29,201.74900000000002,62.602,32.613 +2020-02-05 14:15:00,121.95,201.998,62.602,32.613 +2020-02-05 14:30:00,121.58,202.77700000000002,62.602,32.613 +2020-02-05 14:45:00,122.34,203.703,62.602,32.613 +2020-02-05 15:00:00,125.21,205.007,64.259,32.613 +2020-02-05 15:15:00,123.42,205.195,64.259,32.613 +2020-02-05 15:30:00,126.69,206.644,64.259,32.613 +2020-02-05 15:45:00,122.86,207.49099999999999,64.259,32.613 +2020-02-05 16:00:00,126.84,208.898,67.632,32.613 +2020-02-05 16:15:00,124.67,210.669,67.632,32.613 +2020-02-05 16:30:00,126.69,213.547,67.632,32.613 +2020-02-05 16:45:00,128.11,214.801,67.632,32.613 +2020-02-05 17:00:00,131.41,216.96400000000003,72.583,32.613 +2020-02-05 17:15:00,135.42,218.33599999999998,72.583,32.613 +2020-02-05 17:30:00,141.15,219.976,72.583,32.613 +2020-02-05 17:45:00,142.13,220.33599999999998,72.583,32.613 +2020-02-05 18:00:00,142.56,222.359,72.744,32.613 +2020-02-05 18:15:00,140.69,219.895,72.744,32.613 +2020-02-05 18:30:00,140.23,218.899,72.744,32.613 +2020-02-05 18:45:00,138.2,219.645,72.744,32.613 +2020-02-05 19:00:00,133.78,217.56900000000002,69.684,32.613 +2020-02-05 19:15:00,135.47,213.73,69.684,32.613 +2020-02-05 19:30:00,139.35,211.13400000000001,69.684,32.613 +2020-02-05 19:45:00,137.95,208.394,69.684,32.613 +2020-02-05 20:00:00,129.71,203.90200000000002,70.036,32.613 +2020-02-05 20:15:00,121.3,197.472,70.036,32.613 +2020-02-05 20:30:00,116.66,193.362,70.036,32.613 +2020-02-05 20:45:00,118.71,192.213,70.036,32.613 +2020-02-05 21:00:00,109.48,188.44,60.431999999999995,32.613 +2020-02-05 21:15:00,115.23,185.36900000000003,60.431999999999995,32.613 +2020-02-05 21:30:00,114.27,183.355,60.431999999999995,32.613 +2020-02-05 21:45:00,111.11,182.192,60.431999999999995,32.613 +2020-02-05 22:00:00,102.57,174.896,56.2,32.613 +2020-02-05 22:15:00,98.72,169.59799999999998,56.2,32.613 +2020-02-05 22:30:00,95.62,155.756,56.2,32.613 +2020-02-05 22:45:00,97.92,147.694,56.2,32.613 +2020-02-05 23:00:00,93.72,140.384,47.927,32.613 +2020-02-05 23:15:00,95.58,139.151,47.927,32.613 +2020-02-05 23:30:00,93.57,139.726,47.927,32.613 +2020-02-05 23:45:00,93.42,139.472,47.927,32.613 +2020-02-06 00:00:00,88.03,133.625,43.794,32.613 +2020-02-06 00:15:00,88.69,132.998,43.794,32.613 +2020-02-06 00:30:00,88.37,134.0,43.794,32.613 +2020-02-06 00:45:00,83.31,135.411,43.794,32.613 +2020-02-06 01:00:00,85.13,138.046,42.397,32.613 +2020-02-06 01:15:00,86.78,137.894,42.397,32.613 +2020-02-06 01:30:00,84.13,138.134,42.397,32.613 +2020-02-06 01:45:00,81.34,138.464,42.397,32.613 +2020-02-06 02:00:00,83.95,140.776,40.010999999999996,32.613 +2020-02-06 02:15:00,85.6,142.738,40.010999999999996,32.613 +2020-02-06 02:30:00,84.94,143.915,40.010999999999996,32.613 +2020-02-06 02:45:00,83.06,145.987,40.010999999999996,32.613 +2020-02-06 03:00:00,85.59,148.806,39.181,32.613 +2020-02-06 03:15:00,88.0,150.118,39.181,32.613 +2020-02-06 03:30:00,87.64,151.882,39.181,32.613 +2020-02-06 03:45:00,86.23,153.86,39.181,32.613 +2020-02-06 04:00:00,87.6,165.005,40.39,32.613 +2020-02-06 04:15:00,83.48,176.203,40.39,32.613 +2020-02-06 04:30:00,84.04,179.805,40.39,32.613 +2020-02-06 04:45:00,86.36,182.597,40.39,32.613 +2020-02-06 05:00:00,90.39,216.13299999999998,45.504,32.613 +2020-02-06 05:15:00,92.47,243.39,45.504,32.613 +2020-02-06 05:30:00,94.94,238.99900000000002,45.504,32.613 +2020-02-06 05:45:00,99.52,232.726,45.504,32.613 +2020-02-06 06:00:00,109.99,230.108,57.748000000000005,32.613 +2020-02-06 06:15:00,113.45,236.298,57.748000000000005,32.613 +2020-02-06 06:30:00,117.48,239.236,57.748000000000005,32.613 +2020-02-06 06:45:00,122.09,244.335,57.748000000000005,32.613 +2020-02-06 07:00:00,130.35,243.31099999999998,72.138,32.613 +2020-02-06 07:15:00,131.22,248.46200000000002,72.138,32.613 +2020-02-06 07:30:00,134.4,251.09599999999998,72.138,32.613 +2020-02-06 07:45:00,135.28,252.148,72.138,32.613 +2020-02-06 08:00:00,138.42,250.588,65.542,32.613 +2020-02-06 08:15:00,139.12,250.46400000000003,65.542,32.613 +2020-02-06 08:30:00,135.68,247.144,65.542,32.613 +2020-02-06 08:45:00,135.18,244.079,65.542,32.613 +2020-02-06 09:00:00,136.0,237.396,60.523,32.613 +2020-02-06 09:15:00,137.89,234.63099999999997,60.523,32.613 +2020-02-06 09:30:00,140.17,233.143,60.523,32.613 +2020-02-06 09:45:00,143.52,229.824,60.523,32.613 +2020-02-06 10:00:00,140.22,224.36900000000003,57.449,32.613 +2020-02-06 10:15:00,141.82,220.864,57.449,32.613 +2020-02-06 10:30:00,141.15,217.17,57.449,32.613 +2020-02-06 10:45:00,141.82,215.588,57.449,32.613 +2020-02-06 11:00:00,140.12,212.542,54.505,32.613 +2020-02-06 11:15:00,141.56,211.387,54.505,32.613 +2020-02-06 11:30:00,142.0,209.731,54.505,32.613 +2020-02-06 11:45:00,141.74,209.082,54.505,32.613 +2020-02-06 12:00:00,141.83,205.12400000000002,51.50899999999999,32.613 +2020-02-06 12:15:00,140.81,205.106,51.50899999999999,32.613 +2020-02-06 12:30:00,138.96,204.518,51.50899999999999,32.613 +2020-02-06 12:45:00,136.26,205.1,51.50899999999999,32.613 +2020-02-06 13:00:00,133.75,203.12599999999998,51.303999999999995,32.613 +2020-02-06 13:15:00,132.36,202.07,51.303999999999995,32.613 +2020-02-06 13:30:00,131.1,201.075,51.303999999999995,32.613 +2020-02-06 13:45:00,131.74,201.18599999999998,51.303999999999995,32.613 +2020-02-06 14:00:00,131.95,201.49400000000003,52.785,32.613 +2020-02-06 14:15:00,131.78,201.72299999999998,52.785,32.613 +2020-02-06 14:30:00,129.87,202.489,52.785,32.613 +2020-02-06 14:45:00,131.48,203.43099999999998,52.785,32.613 +2020-02-06 15:00:00,130.36,204.74599999999998,56.458999999999996,32.613 +2020-02-06 15:15:00,130.85,204.90400000000002,56.458999999999996,32.613 +2020-02-06 15:30:00,132.16,206.32,56.458999999999996,32.613 +2020-02-06 15:45:00,129.54,207.15200000000002,56.458999999999996,32.613 +2020-02-06 16:00:00,131.64,208.558,59.388000000000005,32.613 +2020-02-06 16:15:00,130.67,210.32299999999998,59.388000000000005,32.613 +2020-02-06 16:30:00,131.07,213.205,59.388000000000005,32.613 +2020-02-06 16:45:00,131.83,214.446,59.388000000000005,32.613 +2020-02-06 17:00:00,136.93,216.605,64.462,32.613 +2020-02-06 17:15:00,139.65,218.00400000000002,64.462,32.613 +2020-02-06 17:30:00,141.81,219.679,64.462,32.613 +2020-02-06 17:45:00,143.36,220.06400000000002,64.462,32.613 +2020-02-06 18:00:00,143.08,222.104,65.128,32.613 +2020-02-06 18:15:00,141.76,219.69,65.128,32.613 +2020-02-06 18:30:00,142.88,218.695,65.128,32.613 +2020-02-06 18:45:00,140.14,219.465,65.128,32.613 +2020-02-06 19:00:00,137.45,217.34799999999998,61.316,32.613 +2020-02-06 19:15:00,136.1,213.52,61.316,32.613 +2020-02-06 19:30:00,140.99,210.94400000000002,61.316,32.613 +2020-02-06 19:45:00,140.88,208.235,61.316,32.613 +2020-02-06 20:00:00,130.65,203.713,59.845,32.613 +2020-02-06 20:15:00,123.56,197.295,59.845,32.613 +2020-02-06 20:30:00,119.45,193.192,59.845,32.613 +2020-02-06 20:45:00,118.13,192.05,59.845,32.613 +2020-02-06 21:00:00,110.84,188.26,54.83,32.613 +2020-02-06 21:15:00,117.96,185.175,54.83,32.613 +2020-02-06 21:30:00,117.04,183.16,54.83,32.613 +2020-02-06 21:45:00,112.67,182.015,54.83,32.613 +2020-02-06 22:00:00,101.49,174.704,50.933,32.613 +2020-02-06 22:15:00,101.52,169.43,50.933,32.613 +2020-02-06 22:30:00,97.68,155.55700000000002,50.933,32.613 +2020-02-06 22:45:00,100.63,147.503,50.933,32.613 +2020-02-06 23:00:00,98.2,140.183,45.32899999999999,32.613 +2020-02-06 23:15:00,98.49,138.966,45.32899999999999,32.613 +2020-02-06 23:30:00,93.7,139.555,45.32899999999999,32.613 +2020-02-06 23:45:00,88.78,139.321,45.32899999999999,32.613 +2020-02-07 00:00:00,88.69,132.535,43.74,32.613 +2020-02-07 00:15:00,91.22,132.095,43.74,32.613 +2020-02-07 00:30:00,91.01,132.892,43.74,32.613 +2020-02-07 00:45:00,85.92,134.365,43.74,32.613 +2020-02-07 01:00:00,86.38,136.671,42.555,32.613 +2020-02-07 01:15:00,86.96,137.619,42.555,32.613 +2020-02-07 01:30:00,85.7,137.502,42.555,32.613 +2020-02-07 01:45:00,78.77,137.983,42.555,32.613 +2020-02-07 02:00:00,77.21,140.266,41.68600000000001,32.613 +2020-02-07 02:15:00,78.6,142.107,41.68600000000001,32.613 +2020-02-07 02:30:00,78.82,143.773,41.68600000000001,32.613 +2020-02-07 02:45:00,79.14,145.988,41.68600000000001,32.613 +2020-02-07 03:00:00,80.57,147.57399999999998,42.278999999999996,32.613 +2020-02-07 03:15:00,87.01,150.124,42.278999999999996,32.613 +2020-02-07 03:30:00,88.53,151.899,42.278999999999996,32.613 +2020-02-07 03:45:00,91.75,154.13299999999998,42.278999999999996,32.613 +2020-02-07 04:00:00,81.9,165.515,43.742,32.613 +2020-02-07 04:15:00,81.41,176.655,43.742,32.613 +2020-02-07 04:30:00,83.77,180.39,43.742,32.613 +2020-02-07 04:45:00,85.67,181.968,43.742,32.613 +2020-02-07 05:00:00,89.49,214.108,46.973,32.613 +2020-02-07 05:15:00,90.53,242.95,46.973,32.613 +2020-02-07 05:30:00,95.28,239.739,46.973,32.613 +2020-02-07 05:45:00,99.96,233.467,46.973,32.613 +2020-02-07 06:00:00,110.48,231.317,59.63399999999999,32.613 +2020-02-07 06:15:00,113.13,235.822,59.63399999999999,32.613 +2020-02-07 06:30:00,118.49,237.761,59.63399999999999,32.613 +2020-02-07 06:45:00,122.09,244.732,59.63399999999999,32.613 +2020-02-07 07:00:00,129.43,242.68599999999998,71.631,32.613 +2020-02-07 07:15:00,129.96,248.84,71.631,32.613 +2020-02-07 07:30:00,130.46,251.521,71.631,32.613 +2020-02-07 07:45:00,133.64,251.55700000000002,71.631,32.613 +2020-02-07 08:00:00,137.27,248.618,66.181,32.613 +2020-02-07 08:15:00,133.27,247.94099999999997,66.181,32.613 +2020-02-07 08:30:00,132.95,245.679,66.181,32.613 +2020-02-07 08:45:00,131.08,240.852,66.181,32.613 +2020-02-07 09:00:00,132.18,234.997,63.086000000000006,32.613 +2020-02-07 09:15:00,132.77,232.62599999999998,63.086000000000006,32.613 +2020-02-07 09:30:00,132.22,230.766,63.086000000000006,32.613 +2020-02-07 09:45:00,132.21,227.275,63.086000000000006,32.613 +2020-02-07 10:00:00,132.16,220.571,60.886,32.613 +2020-02-07 10:15:00,134.75,217.90599999999998,60.886,32.613 +2020-02-07 10:30:00,132.78,214.06599999999997,60.886,32.613 +2020-02-07 10:45:00,131.45,212.013,60.886,32.613 +2020-02-07 11:00:00,127.82,208.912,59.391000000000005,32.613 +2020-02-07 11:15:00,128.72,206.90200000000002,59.391000000000005,32.613 +2020-02-07 11:30:00,126.6,207.252,59.391000000000005,32.613 +2020-02-07 11:45:00,125.26,206.785,59.391000000000005,32.613 +2020-02-07 12:00:00,123.4,204.003,56.172,32.613 +2020-02-07 12:15:00,122.69,201.739,56.172,32.613 +2020-02-07 12:30:00,121.83,201.28900000000002,56.172,32.613 +2020-02-07 12:45:00,121.23,202.543,56.172,32.613 +2020-02-07 13:00:00,119.63,201.565,54.406000000000006,32.613 +2020-02-07 13:15:00,119.78,201.37599999999998,54.406000000000006,32.613 +2020-02-07 13:30:00,118.9,200.287,54.406000000000006,32.613 +2020-02-07 13:45:00,118.56,200.28799999999998,54.406000000000006,32.613 +2020-02-07 14:00:00,115.99,199.468,53.578,32.613 +2020-02-07 14:15:00,117.39,199.42700000000002,53.578,32.613 +2020-02-07 14:30:00,118.46,200.57,53.578,32.613 +2020-02-07 14:45:00,121.39,201.94799999999998,53.578,32.613 +2020-02-07 15:00:00,122.71,202.743,56.568999999999996,32.613 +2020-02-07 15:15:00,118.45,202.41299999999998,56.568999999999996,32.613 +2020-02-07 15:30:00,118.49,202.174,56.568999999999996,32.613 +2020-02-07 15:45:00,118.79,203.062,56.568999999999996,32.613 +2020-02-07 16:00:00,120.97,203.285,60.169,32.613 +2020-02-07 16:15:00,123.05,205.308,60.169,32.613 +2020-02-07 16:30:00,121.89,208.31900000000002,60.169,32.613 +2020-02-07 16:45:00,123.97,209.50400000000002,60.169,32.613 +2020-02-07 17:00:00,128.63,211.71099999999998,65.497,32.613 +2020-02-07 17:15:00,131.36,212.703,65.497,32.613 +2020-02-07 17:30:00,135.74,214.044,65.497,32.613 +2020-02-07 17:45:00,136.59,214.21599999999998,65.497,32.613 +2020-02-07 18:00:00,137.38,217.047,65.082,32.613 +2020-02-07 18:15:00,136.53,214.35,65.082,32.613 +2020-02-07 18:30:00,135.49,213.798,65.082,32.613 +2020-02-07 18:45:00,134.69,214.545,65.082,32.613 +2020-02-07 19:00:00,131.86,213.304,60.968,32.613 +2020-02-07 19:15:00,131.03,210.918,60.968,32.613 +2020-02-07 19:30:00,134.97,207.908,60.968,32.613 +2020-02-07 19:45:00,136.65,204.822,60.968,32.613 +2020-02-07 20:00:00,128.29,200.338,61.123000000000005,32.613 +2020-02-07 20:15:00,120.3,193.86,61.123000000000005,32.613 +2020-02-07 20:30:00,116.56,189.77,61.123000000000005,32.613 +2020-02-07 20:45:00,113.11,189.325,61.123000000000005,32.613 +2020-02-07 21:00:00,107.82,185.94299999999998,55.416000000000004,32.613 +2020-02-07 21:15:00,111.91,183.15400000000002,55.416000000000004,32.613 +2020-02-07 21:30:00,109.99,181.205,55.416000000000004,32.613 +2020-02-07 21:45:00,109.63,180.65099999999998,55.416000000000004,32.613 +2020-02-07 22:00:00,99.9,174.41299999999998,51.631,32.613 +2020-02-07 22:15:00,94.38,169.043,51.631,32.613 +2020-02-07 22:30:00,91.58,161.64,51.631,32.613 +2020-02-07 22:45:00,96.88,157.365,51.631,32.613 +2020-02-07 23:00:00,93.8,149.38299999999998,44.898,32.613 +2020-02-07 23:15:00,96.38,146.203,44.898,32.613 +2020-02-07 23:30:00,86.3,145.392,44.898,32.613 +2020-02-07 23:45:00,82.22,144.46200000000002,44.898,32.613 +2020-02-08 00:00:00,76.07,129.066,42.033,32.431999999999995 +2020-02-08 00:15:00,81.2,123.95200000000001,42.033,32.431999999999995 +2020-02-08 00:30:00,83.22,126.271,42.033,32.431999999999995 +2020-02-08 00:45:00,85.24,128.624,42.033,32.431999999999995 +2020-02-08 01:00:00,74.78,131.55700000000002,38.255,32.431999999999995 +2020-02-08 01:15:00,73.72,131.313,38.255,32.431999999999995 +2020-02-08 01:30:00,71.69,130.709,38.255,32.431999999999995 +2020-02-08 01:45:00,80.83,130.78799999999998,38.255,32.431999999999995 +2020-02-08 02:00:00,81.35,133.951,36.404,32.431999999999995 +2020-02-08 02:15:00,79.32,135.47799999999998,36.404,32.431999999999995 +2020-02-08 02:30:00,73.69,136.015,36.404,32.431999999999995 +2020-02-08 02:45:00,72.18,138.24,36.404,32.431999999999995 +2020-02-08 03:00:00,76.73,140.701,36.083,32.431999999999995 +2020-02-08 03:15:00,78.04,141.977,36.083,32.431999999999995 +2020-02-08 03:30:00,75.79,141.916,36.083,32.431999999999995 +2020-02-08 03:45:00,72.95,144.086,36.083,32.431999999999995 +2020-02-08 04:00:00,69.34,151.02,36.102,32.431999999999995 +2020-02-08 04:15:00,69.82,159.40200000000002,36.102,32.431999999999995 +2020-02-08 04:30:00,69.75,160.933,36.102,32.431999999999995 +2020-02-08 04:45:00,73.37,161.901,36.102,32.431999999999995 +2020-02-08 05:00:00,71.6,177.055,35.284,32.431999999999995 +2020-02-08 05:15:00,72.11,185.93599999999998,35.284,32.431999999999995 +2020-02-08 05:30:00,71.73,182.887,35.284,32.431999999999995 +2020-02-08 05:45:00,75.3,182.155,35.284,32.431999999999995 +2020-02-08 06:00:00,74.8,199.732,36.265,32.431999999999995 +2020-02-08 06:15:00,74.36,221.66,36.265,32.431999999999995 +2020-02-08 06:30:00,75.45,217.892,36.265,32.431999999999995 +2020-02-08 06:45:00,77.69,215.15,36.265,32.431999999999995 +2020-02-08 07:00:00,83.57,209.08700000000002,40.714,32.431999999999995 +2020-02-08 07:15:00,87.81,213.976,40.714,32.431999999999995 +2020-02-08 07:30:00,87.5,219.47400000000002,40.714,32.431999999999995 +2020-02-08 07:45:00,88.9,223.83700000000002,40.714,32.431999999999995 +2020-02-08 08:00:00,93.52,225.43400000000003,46.692,32.431999999999995 +2020-02-08 08:15:00,95.5,228.845,46.692,32.431999999999995 +2020-02-08 08:30:00,98.42,228.342,46.692,32.431999999999995 +2020-02-08 08:45:00,100.91,226.93599999999998,46.692,32.431999999999995 +2020-02-08 09:00:00,103.5,222.706,48.925,32.431999999999995 +2020-02-08 09:15:00,103.89,221.12,48.925,32.431999999999995 +2020-02-08 09:30:00,104.75,220.226,48.925,32.431999999999995 +2020-02-08 09:45:00,107.02,216.97299999999998,48.925,32.431999999999995 +2020-02-08 10:00:00,107.36,210.49,47.799,32.431999999999995 +2020-02-08 10:15:00,107.78,207.989,47.799,32.431999999999995 +2020-02-08 10:30:00,108.48,204.386,47.799,32.431999999999995 +2020-02-08 10:45:00,109.57,203.84799999999998,47.799,32.431999999999995 +2020-02-08 11:00:00,108.73,201.024,44.309,32.431999999999995 +2020-02-08 11:15:00,110.59,198.15400000000002,44.309,32.431999999999995 +2020-02-08 11:30:00,111.33,197.233,44.309,32.431999999999995 +2020-02-08 11:45:00,110.89,195.63099999999997,44.309,32.431999999999995 +2020-02-08 12:00:00,108.2,191.81599999999997,42.367,32.431999999999995 +2020-02-08 12:15:00,106.08,190.18,42.367,32.431999999999995 +2020-02-08 12:30:00,103.17,190.074,42.367,32.431999999999995 +2020-02-08 12:45:00,102.67,190.37599999999998,42.367,32.431999999999995 +2020-02-08 13:00:00,98.62,189.06799999999998,39.036,32.431999999999995 +2020-02-08 13:15:00,98.03,186.632,39.036,32.431999999999995 +2020-02-08 13:30:00,95.95,185.00099999999998,39.036,32.431999999999995 +2020-02-08 13:45:00,94.83,185.69799999999998,39.036,32.431999999999995 +2020-02-08 14:00:00,96.32,186.321,37.995,32.431999999999995 +2020-02-08 14:15:00,93.19,185.817,37.995,32.431999999999995 +2020-02-08 14:30:00,93.21,184.97400000000002,37.995,32.431999999999995 +2020-02-08 14:45:00,93.07,186.553,37.995,32.431999999999995 +2020-02-08 15:00:00,92.13,188.085,40.71,32.431999999999995 +2020-02-08 15:15:00,92.1,188.537,40.71,32.431999999999995 +2020-02-08 15:30:00,93.08,189.947,40.71,32.431999999999995 +2020-02-08 15:45:00,94.41,190.942,40.71,32.431999999999995 +2020-02-08 16:00:00,93.8,189.675,46.998000000000005,32.431999999999995 +2020-02-08 16:15:00,94.73,192.775,46.998000000000005,32.431999999999995 +2020-02-08 16:30:00,96.17,195.707,46.998000000000005,32.431999999999995 +2020-02-08 16:45:00,97.61,197.858,46.998000000000005,32.431999999999995 +2020-02-08 17:00:00,102.44,199.639,55.431000000000004,32.431999999999995 +2020-02-08 17:15:00,103.98,202.69299999999998,55.431000000000004,32.431999999999995 +2020-02-08 17:30:00,110.41,203.977,55.431000000000004,32.431999999999995 +2020-02-08 17:45:00,109.38,203.68,55.431000000000004,32.431999999999995 +2020-02-08 18:00:00,111.7,205.91099999999997,55.989,32.431999999999995 +2020-02-08 18:15:00,111.45,205.063,55.989,32.431999999999995 +2020-02-08 18:30:00,109.88,205.833,55.989,32.431999999999995 +2020-02-08 18:45:00,108.49,203.294,55.989,32.431999999999995 +2020-02-08 19:00:00,107.67,203.201,50.882,32.431999999999995 +2020-02-08 19:15:00,106.93,200.38299999999998,50.882,32.431999999999995 +2020-02-08 19:30:00,104.88,198.138,50.882,32.431999999999995 +2020-02-08 19:45:00,102.39,194.731,50.882,32.431999999999995 +2020-02-08 20:00:00,98.25,192.463,43.172,32.431999999999995 +2020-02-08 20:15:00,94.38,188.37400000000002,43.172,32.431999999999995 +2020-02-08 20:30:00,91.97,183.99599999999998,43.172,32.431999999999995 +2020-02-08 20:45:00,89.93,182.93900000000002,43.172,32.431999999999995 +2020-02-08 21:00:00,85.08,182.136,37.599000000000004,32.431999999999995 +2020-02-08 21:15:00,83.97,179.845,37.599000000000004,32.431999999999995 +2020-02-08 21:30:00,82.71,179.21599999999998,37.599000000000004,32.431999999999995 +2020-02-08 21:45:00,82.15,178.28900000000002,37.599000000000004,32.431999999999995 +2020-02-08 22:00:00,77.71,173.505,39.047,32.431999999999995 +2020-02-08 22:15:00,77.18,170.838,39.047,32.431999999999995 +2020-02-08 22:30:00,73.7,170.201,39.047,32.431999999999995 +2020-02-08 22:45:00,72.2,167.93,39.047,32.431999999999995 +2020-02-08 23:00:00,69.4,162.638,32.339,32.431999999999995 +2020-02-08 23:15:00,68.66,157.657,32.339,32.431999999999995 +2020-02-08 23:30:00,65.61,154.733,32.339,32.431999999999995 +2020-02-08 23:45:00,64.73,151.15200000000002,32.339,32.431999999999995 +2020-02-09 00:00:00,61.27,129.226,29.988000000000003,32.431999999999995 +2020-02-09 00:15:00,60.71,123.889,29.988000000000003,32.431999999999995 +2020-02-09 00:30:00,59.99,125.789,29.988000000000003,32.431999999999995 +2020-02-09 00:45:00,59.14,128.922,29.988000000000003,32.431999999999995 +2020-02-09 01:00:00,57.53,131.635,28.531999999999996,32.431999999999995 +2020-02-09 01:15:00,56.93,132.55,28.531999999999996,32.431999999999995 +2020-02-09 01:30:00,57.32,132.533,28.531999999999996,32.431999999999995 +2020-02-09 01:45:00,57.14,132.30700000000002,28.531999999999996,32.431999999999995 +2020-02-09 02:00:00,56.24,134.63,27.805999999999997,32.431999999999995 +2020-02-09 02:15:00,57.09,135.11700000000002,27.805999999999997,32.431999999999995 +2020-02-09 02:30:00,56.53,136.59799999999998,27.805999999999997,32.431999999999995 +2020-02-09 02:45:00,54.95,139.373,27.805999999999997,32.431999999999995 +2020-02-09 03:00:00,54.55,142.111,26.193,32.431999999999995 +2020-02-09 03:15:00,55.28,142.769,26.193,32.431999999999995 +2020-02-09 03:30:00,55.8,144.366,26.193,32.431999999999995 +2020-02-09 03:45:00,56.3,146.563,26.193,32.431999999999995 +2020-02-09 04:00:00,56.5,153.233,27.19,32.431999999999995 +2020-02-09 04:15:00,57.24,160.52700000000002,27.19,32.431999999999995 +2020-02-09 04:30:00,57.91,161.968,27.19,32.431999999999995 +2020-02-09 04:45:00,57.79,163.293,27.19,32.431999999999995 +2020-02-09 05:00:00,58.77,174.454,28.166999999999998,32.431999999999995 +2020-02-09 05:15:00,58.46,180.73,28.166999999999998,32.431999999999995 +2020-02-09 05:30:00,60.97,177.52599999999998,28.166999999999998,32.431999999999995 +2020-02-09 05:45:00,62.39,177.112,28.166999999999998,32.431999999999995 +2020-02-09 06:00:00,60.88,194.896,27.16,32.431999999999995 +2020-02-09 06:15:00,63.92,214.793,27.16,32.431999999999995 +2020-02-09 06:30:00,65.05,209.84799999999998,27.16,32.431999999999995 +2020-02-09 06:45:00,63.69,206.051,27.16,32.431999999999995 +2020-02-09 07:00:00,68.95,202.731,29.578000000000003,32.431999999999995 +2020-02-09 07:15:00,68.37,206.84900000000002,29.578000000000003,32.431999999999995 +2020-02-09 07:30:00,70.21,210.766,29.578000000000003,32.431999999999995 +2020-02-09 07:45:00,72.49,214.237,29.578000000000003,32.431999999999995 +2020-02-09 08:00:00,76.04,217.799,34.650999999999996,32.431999999999995 +2020-02-09 08:15:00,76.96,220.94,34.650999999999996,32.431999999999995 +2020-02-09 08:30:00,77.09,222.12400000000002,34.650999999999996,32.431999999999995 +2020-02-09 08:45:00,77.53,222.98,34.650999999999996,32.431999999999995 +2020-02-09 09:00:00,79.01,218.326,38.080999999999996,32.431999999999995 +2020-02-09 09:15:00,78.77,217.41299999999998,38.080999999999996,32.431999999999995 +2020-02-09 09:30:00,75.58,216.317,38.080999999999996,32.431999999999995 +2020-02-09 09:45:00,79.56,212.81599999999997,38.080999999999996,32.431999999999995 +2020-02-09 10:00:00,81.3,209.075,39.934,32.431999999999995 +2020-02-09 10:15:00,76.81,207.17700000000002,39.934,32.431999999999995 +2020-02-09 10:30:00,79.12,204.226,39.934,32.431999999999995 +2020-02-09 10:45:00,81.4,201.47099999999998,39.934,32.431999999999995 +2020-02-09 11:00:00,84.98,199.705,43.74100000000001,32.431999999999995 +2020-02-09 11:15:00,91.65,197.043,43.74100000000001,32.431999999999995 +2020-02-09 11:30:00,93.87,195.105,43.74100000000001,32.431999999999995 +2020-02-09 11:45:00,94.14,194.15599999999998,43.74100000000001,32.431999999999995 +2020-02-09 12:00:00,89.58,189.606,40.001999999999995,32.431999999999995 +2020-02-09 12:15:00,87.22,190.168,40.001999999999995,32.431999999999995 +2020-02-09 12:30:00,81.41,188.4,40.001999999999995,32.431999999999995 +2020-02-09 12:45:00,83.91,187.699,40.001999999999995,32.431999999999995 +2020-02-09 13:00:00,83.42,185.649,37.855,32.431999999999995 +2020-02-09 13:15:00,83.34,186.597,37.855,32.431999999999995 +2020-02-09 13:30:00,83.38,184.843,37.855,32.431999999999995 +2020-02-09 13:45:00,83.48,184.68400000000003,37.855,32.431999999999995 +2020-02-09 14:00:00,82.03,185.49900000000002,35.946999999999996,32.431999999999995 +2020-02-09 14:15:00,81.11,186.253,35.946999999999996,32.431999999999995 +2020-02-09 14:30:00,80.88,186.899,35.946999999999996,32.431999999999995 +2020-02-09 14:45:00,80.8,188.155,35.946999999999996,32.431999999999995 +2020-02-09 15:00:00,80.61,187.985,35.138000000000005,32.431999999999995 +2020-02-09 15:15:00,81.01,189.326,35.138000000000005,32.431999999999995 +2020-02-09 15:30:00,81.24,191.368,35.138000000000005,32.431999999999995 +2020-02-09 15:45:00,82.18,193.079,35.138000000000005,32.431999999999995 +2020-02-09 16:00:00,84.12,194.005,38.672,32.431999999999995 +2020-02-09 16:15:00,82.75,196.077,38.672,32.431999999999995 +2020-02-09 16:30:00,83.87,199.19400000000002,38.672,32.431999999999995 +2020-02-09 16:45:00,85.94,201.47099999999998,38.672,32.431999999999995 +2020-02-09 17:00:00,91.05,203.174,48.684,32.431999999999995 +2020-02-09 17:15:00,91.88,205.752,48.684,32.431999999999995 +2020-02-09 17:30:00,98.58,207.305,48.684,32.431999999999995 +2020-02-09 17:45:00,98.25,209.468,48.684,32.431999999999995 +2020-02-09 18:00:00,103.28,211.042,51.568999999999996,32.431999999999995 +2020-02-09 18:15:00,100.42,211.72099999999998,51.568999999999996,32.431999999999995 +2020-02-09 18:30:00,104.15,210.25400000000002,51.568999999999996,32.431999999999995 +2020-02-09 18:45:00,100.37,209.738,51.568999999999996,32.431999999999995 +2020-02-09 19:00:00,98.81,209.03599999999997,48.608000000000004,32.431999999999995 +2020-02-09 19:15:00,97.23,206.96599999999998,48.608000000000004,32.431999999999995 +2020-02-09 19:30:00,95.94,204.58599999999998,48.608000000000004,32.431999999999995 +2020-02-09 19:45:00,94.87,202.80900000000003,48.608000000000004,32.431999999999995 +2020-02-09 20:00:00,91.94,200.498,43.733999999999995,32.431999999999995 +2020-02-09 20:15:00,91.17,197.502,43.733999999999995,32.431999999999995 +2020-02-09 20:30:00,89.34,194.40599999999998,43.733999999999995,32.431999999999995 +2020-02-09 20:45:00,90.36,192.203,43.733999999999995,32.431999999999995 +2020-02-09 21:00:00,83.75,188.577,39.283,32.431999999999995 +2020-02-09 21:15:00,84.63,185.62,39.283,32.431999999999995 +2020-02-09 21:30:00,84.54,185.36900000000003,39.283,32.431999999999995 +2020-02-09 21:45:00,87.87,184.575,39.283,32.431999999999995 +2020-02-09 22:00:00,86.37,178.296,40.111,32.431999999999995 +2020-02-09 22:15:00,84.1,174.982,40.111,32.431999999999995 +2020-02-09 22:30:00,80.59,170.921,40.111,32.431999999999995 +2020-02-09 22:45:00,80.15,167.84900000000002,40.111,32.431999999999995 +2020-02-09 23:00:00,76.59,159.545,35.791,32.431999999999995 +2020-02-09 23:15:00,78.67,156.477,35.791,32.431999999999995 +2020-02-09 23:30:00,75.47,154.495,35.791,32.431999999999995 +2020-02-09 23:45:00,74.68,151.886,35.791,32.431999999999995 +2020-02-10 00:00:00,69.25,133.565,34.311,32.613 +2020-02-10 00:15:00,70.25,131.43200000000002,34.311,32.613 +2020-02-10 00:30:00,70.75,133.498,34.311,32.613 +2020-02-10 00:45:00,70.63,136.07399999999998,34.311,32.613 +2020-02-10 01:00:00,69.11,138.743,34.585,32.613 +2020-02-10 01:15:00,68.52,139.054,34.585,32.613 +2020-02-10 01:30:00,68.82,139.04399999999998,34.585,32.613 +2020-02-10 01:45:00,69.91,138.957,34.585,32.613 +2020-02-10 02:00:00,68.03,141.208,34.111,32.613 +2020-02-10 02:15:00,68.74,143.455,34.111,32.613 +2020-02-10 02:30:00,68.09,145.303,34.111,32.613 +2020-02-10 02:45:00,68.85,147.40200000000002,34.111,32.613 +2020-02-10 03:00:00,67.44,151.52200000000002,32.435,32.613 +2020-02-10 03:15:00,68.32,153.937,32.435,32.613 +2020-02-10 03:30:00,69.58,155.138,32.435,32.613 +2020-02-10 03:45:00,70.89,156.787,32.435,32.613 +2020-02-10 04:00:00,71.36,167.96099999999998,33.04,32.613 +2020-02-10 04:15:00,73.21,179.521,33.04,32.613 +2020-02-10 04:30:00,75.25,183.50900000000001,33.04,32.613 +2020-02-10 04:45:00,77.06,184.954,33.04,32.613 +2020-02-10 05:00:00,81.16,212.946,40.399,32.613 +2020-02-10 05:15:00,83.71,240.581,40.399,32.613 +2020-02-10 05:30:00,88.12,237.87599999999998,40.399,32.613 +2020-02-10 05:45:00,91.96,231.50799999999998,40.399,32.613 +2020-02-10 06:00:00,100.85,230.446,60.226000000000006,32.613 +2020-02-10 06:15:00,104.58,234.87099999999998,60.226000000000006,32.613 +2020-02-10 06:30:00,109.61,238.394,60.226000000000006,32.613 +2020-02-10 06:45:00,114.01,243.798,60.226000000000006,32.613 +2020-02-10 07:00:00,123.45,243.1,73.578,32.613 +2020-02-10 07:15:00,122.16,248.285,73.578,32.613 +2020-02-10 07:30:00,126.33,251.41,73.578,32.613 +2020-02-10 07:45:00,124.92,251.91099999999997,73.578,32.613 +2020-02-10 08:00:00,128.37,250.143,66.58,32.613 +2020-02-10 08:15:00,127.45,251.013,66.58,32.613 +2020-02-10 08:30:00,127.24,247.687,66.58,32.613 +2020-02-10 08:45:00,126.73,244.77900000000002,66.58,32.613 +2020-02-10 09:00:00,128.93,239.095,62.0,32.613 +2020-02-10 09:15:00,132.12,234.521,62.0,32.613 +2020-02-10 09:30:00,129.88,232.447,62.0,32.613 +2020-02-10 09:45:00,132.17,229.562,62.0,32.613 +2020-02-10 10:00:00,131.38,224.611,59.099,32.613 +2020-02-10 10:15:00,127.86,222.393,59.099,32.613 +2020-02-10 10:30:00,126.07,218.533,59.099,32.613 +2020-02-10 10:45:00,129.64,216.829,59.099,32.613 +2020-02-10 11:00:00,130.68,212.051,57.729,32.613 +2020-02-10 11:15:00,131.96,211.373,57.729,32.613 +2020-02-10 11:30:00,132.31,210.891,57.729,32.613 +2020-02-10 11:45:00,136.23,209.438,57.729,32.613 +2020-02-10 12:00:00,131.68,207.05599999999998,55.615,32.613 +2020-02-10 12:15:00,132.92,207.62,55.615,32.613 +2020-02-10 12:30:00,134.19,206.238,55.615,32.613 +2020-02-10 12:45:00,129.29,207.23,55.615,32.613 +2020-02-10 13:00:00,125.98,205.753,56.515,32.613 +2020-02-10 13:15:00,124.66,205.24,56.515,32.613 +2020-02-10 13:30:00,124.24,202.86,56.515,32.613 +2020-02-10 13:45:00,128.1,202.62900000000002,56.515,32.613 +2020-02-10 14:00:00,124.97,202.88400000000001,58.1,32.613 +2020-02-10 14:15:00,125.5,202.832,58.1,32.613 +2020-02-10 14:30:00,126.78,202.88299999999998,58.1,32.613 +2020-02-10 14:45:00,128.81,203.86900000000003,58.1,32.613 +2020-02-10 15:00:00,129.17,205.675,59.801,32.613 +2020-02-10 15:15:00,129.48,205.46599999999998,59.801,32.613 +2020-02-10 15:30:00,124.6,206.46400000000003,59.801,32.613 +2020-02-10 15:45:00,123.59,207.72099999999998,59.801,32.613 +2020-02-10 16:00:00,123.44,208.65,62.901,32.613 +2020-02-10 16:15:00,123.28,209.86700000000002,62.901,32.613 +2020-02-10 16:30:00,122.68,211.997,62.901,32.613 +2020-02-10 16:45:00,123.82,212.947,62.901,32.613 +2020-02-10 17:00:00,128.52,214.516,70.418,32.613 +2020-02-10 17:15:00,127.46,216.03799999999998,70.418,32.613 +2020-02-10 17:30:00,135.21,217.065,70.418,32.613 +2020-02-10 17:45:00,135.18,217.66099999999997,70.418,32.613 +2020-02-10 18:00:00,137.86,219.78099999999998,71.726,32.613 +2020-02-10 18:15:00,135.1,218.28900000000002,71.726,32.613 +2020-02-10 18:30:00,134.0,217.61,71.726,32.613 +2020-02-10 18:45:00,133.93,217.583,71.726,32.613 +2020-02-10 19:00:00,132.08,215.093,65.997,32.613 +2020-02-10 19:15:00,130.65,211.641,65.997,32.613 +2020-02-10 19:30:00,130.87,209.857,65.997,32.613 +2020-02-10 19:45:00,134.33,207.269,65.997,32.613 +2020-02-10 20:00:00,125.69,202.49400000000003,68.09100000000001,32.613 +2020-02-10 20:15:00,123.18,196.667,68.09100000000001,32.613 +2020-02-10 20:30:00,113.96,191.49900000000002,68.09100000000001,32.613 +2020-02-10 20:45:00,114.18,191.053,68.09100000000001,32.613 +2020-02-10 21:00:00,108.98,188.05599999999998,59.617,32.613 +2020-02-10 21:15:00,111.78,183.76,59.617,32.613 +2020-02-10 21:30:00,113.19,182.56599999999997,59.617,32.613 +2020-02-10 21:45:00,110.69,181.261,59.617,32.613 +2020-02-10 22:00:00,98.01,172.03099999999998,54.938,32.613 +2020-02-10 22:15:00,97.21,167.104,54.938,32.613 +2020-02-10 22:30:00,96.16,153.02700000000002,54.938,32.613 +2020-02-10 22:45:00,99.73,144.695,54.938,32.613 +2020-02-10 23:00:00,94.89,137.238,47.43,32.613 +2020-02-10 23:15:00,95.68,137.238,47.43,32.613 +2020-02-10 23:30:00,87.21,138.285,47.43,32.613 +2020-02-10 23:45:00,87.28,138.628,47.43,32.613 +2020-02-11 00:00:00,86.73,132.929,48.354,32.613 +2020-02-11 00:15:00,86.67,132.27200000000002,48.354,32.613 +2020-02-11 00:30:00,84.13,133.178,48.354,32.613 +2020-02-11 00:45:00,83.69,134.55200000000002,48.354,32.613 +2020-02-11 01:00:00,83.21,137.034,45.68600000000001,32.613 +2020-02-11 01:15:00,84.29,136.819,45.68600000000001,32.613 +2020-02-11 01:30:00,79.62,137.00799999999998,45.68600000000001,32.613 +2020-02-11 01:45:00,77.48,137.332,45.68600000000001,32.613 +2020-02-11 02:00:00,82.18,139.641,44.269,32.613 +2020-02-11 02:15:00,83.97,141.60399999999998,44.269,32.613 +2020-02-11 02:30:00,82.96,142.84799999999998,44.269,32.613 +2020-02-11 02:45:00,77.71,144.915,44.269,32.613 +2020-02-11 03:00:00,76.55,147.748,44.187,32.613 +2020-02-11 03:15:00,81.43,149.071,44.187,32.613 +2020-02-11 03:30:00,85.46,150.81799999999998,44.187,32.613 +2020-02-11 03:45:00,86.18,152.856,44.187,32.613 +2020-02-11 04:00:00,84.27,163.98,46.126999999999995,32.613 +2020-02-11 04:15:00,84.94,175.13400000000001,46.126999999999995,32.613 +2020-02-11 04:30:00,88.91,178.803,46.126999999999995,32.613 +2020-02-11 04:45:00,90.77,181.537,46.126999999999995,32.613 +2020-02-11 05:00:00,89.12,214.968,49.666000000000004,32.613 +2020-02-11 05:15:00,89.51,242.28799999999998,49.666000000000004,32.613 +2020-02-11 05:30:00,91.15,237.78400000000002,49.666000000000004,32.613 +2020-02-11 05:45:00,95.27,231.54,49.666000000000004,32.613 +2020-02-11 06:00:00,104.24,228.979,61.077,32.613 +2020-02-11 06:15:00,107.46,235.239,61.077,32.613 +2020-02-11 06:30:00,112.26,238.045,61.077,32.613 +2020-02-11 06:45:00,115.29,243.15599999999998,61.077,32.613 +2020-02-11 07:00:00,122.79,242.25,74.717,32.613 +2020-02-11 07:15:00,123.23,247.265,74.717,32.613 +2020-02-11 07:30:00,126.38,249.733,74.717,32.613 +2020-02-11 07:45:00,126.0,250.583,74.717,32.613 +2020-02-11 08:00:00,128.06,248.925,69.033,32.613 +2020-02-11 08:15:00,126.93,248.678,69.033,32.613 +2020-02-11 08:30:00,130.01,245.06900000000002,69.033,32.613 +2020-02-11 08:45:00,126.16,241.99200000000002,69.033,32.613 +2020-02-11 09:00:00,127.51,235.327,63.113,32.613 +2020-02-11 09:15:00,129.21,232.574,63.113,32.613 +2020-02-11 09:30:00,129.61,231.18599999999998,63.113,32.613 +2020-02-11 09:45:00,128.56,227.87099999999998,63.113,32.613 +2020-02-11 10:00:00,125.66,222.46099999999998,61.461999999999996,32.613 +2020-02-11 10:15:00,124.7,219.09099999999998,61.461999999999996,32.613 +2020-02-11 10:30:00,123.35,215.44,61.461999999999996,32.613 +2020-02-11 10:45:00,125.93,213.925,61.461999999999996,32.613 +2020-02-11 11:00:00,119.21,210.815,59.614,32.613 +2020-02-11 11:15:00,119.93,209.72,59.614,32.613 +2020-02-11 11:30:00,119.99,208.093,59.614,32.613 +2020-02-11 11:45:00,121.04,207.5,59.614,32.613 +2020-02-11 12:00:00,115.58,203.62099999999998,57.415,32.613 +2020-02-11 12:15:00,116.2,203.688,57.415,32.613 +2020-02-11 12:30:00,121.86,202.972,57.415,32.613 +2020-02-11 12:45:00,119.33,203.535,57.415,32.613 +2020-02-11 13:00:00,115.97,201.671,58.534,32.613 +2020-02-11 13:15:00,115.16,200.493,58.534,32.613 +2020-02-11 13:30:00,116.39,199.42,58.534,32.613 +2020-02-11 13:45:00,118.38,199.53400000000002,58.534,32.613 +2020-02-11 14:00:00,114.61,200.108,59.415,32.613 +2020-02-11 14:15:00,116.91,200.239,59.415,32.613 +2020-02-11 14:30:00,117.94,200.933,59.415,32.613 +2020-02-11 14:45:00,120.5,201.953,59.415,32.613 +2020-02-11 15:00:00,122.94,203.31099999999998,62.071999999999996,32.613 +2020-02-11 15:15:00,119.07,203.321,62.071999999999996,32.613 +2020-02-11 15:30:00,121.84,204.55900000000003,62.071999999999996,32.613 +2020-02-11 15:45:00,120.06,205.305,62.071999999999996,32.613 +2020-02-11 16:00:00,122.44,206.71400000000003,64.99,32.613 +2020-02-11 16:15:00,120.88,208.44099999999997,64.99,32.613 +2020-02-11 16:30:00,120.1,211.33900000000003,64.99,32.613 +2020-02-11 16:45:00,121.9,212.503,64.99,32.613 +2020-02-11 17:00:00,126.56,214.648,72.658,32.613 +2020-02-11 17:15:00,130.27,216.179,72.658,32.613 +2020-02-11 17:30:00,133.97,218.02700000000002,72.658,32.613 +2020-02-11 17:45:00,133.32,218.545,72.658,32.613 +2020-02-11 18:00:00,135.96,220.662,73.645,32.613 +2020-02-11 18:15:00,134.35,218.513,73.645,32.613 +2020-02-11 18:30:00,137.41,217.52700000000002,73.645,32.613 +2020-02-11 18:45:00,134.9,218.418,73.645,32.613 +2020-02-11 19:00:00,133.28,216.093,67.085,32.613 +2020-02-11 19:15:00,131.07,212.33,67.085,32.613 +2020-02-11 19:30:00,128.99,209.861,67.085,32.613 +2020-02-11 19:45:00,130.79,207.312,67.085,32.613 +2020-02-11 20:00:00,123.69,202.644,66.138,32.613 +2020-02-11 20:15:00,117.11,196.28599999999997,66.138,32.613 +2020-02-11 20:30:00,115.68,192.236,66.138,32.613 +2020-02-11 20:45:00,116.21,191.12,66.138,32.613 +2020-02-11 21:00:00,108.07,187.245,57.512,32.613 +2020-02-11 21:15:00,113.35,184.08900000000003,57.512,32.613 +2020-02-11 21:30:00,113.57,182.076,57.512,32.613 +2020-02-11 21:45:00,109.47,181.015,57.512,32.613 +2020-02-11 22:00:00,100.17,173.62599999999998,54.545,32.613 +2020-02-11 22:15:00,99.34,168.476,54.545,32.613 +2020-02-11 22:30:00,95.56,154.42700000000002,54.545,32.613 +2020-02-11 22:45:00,96.63,146.406,54.545,32.613 +2020-02-11 23:00:00,97.75,139.053,48.605,32.613 +2020-02-11 23:15:00,94.85,137.909,48.605,32.613 +2020-02-11 23:30:00,89.6,138.576,48.605,32.613 +2020-02-11 23:45:00,87.08,138.44,48.605,32.613 +2020-02-12 00:00:00,83.69,132.765,45.675,32.613 +2020-02-12 00:15:00,81.35,132.10399999999998,45.675,32.613 +2020-02-12 00:30:00,81.62,132.99,45.675,32.613 +2020-02-12 00:45:00,87.28,134.359,45.675,32.613 +2020-02-12 01:00:00,85.43,136.80700000000002,43.015,32.613 +2020-02-12 01:15:00,86.31,136.579,43.015,32.613 +2020-02-12 01:30:00,81.81,136.75799999999998,43.015,32.613 +2020-02-12 01:45:00,85.94,137.08,43.015,32.613 +2020-02-12 02:00:00,84.66,139.388,41.0,32.613 +2020-02-12 02:15:00,84.18,141.351,41.0,32.613 +2020-02-12 02:30:00,80.13,142.608,41.0,32.613 +2020-02-12 02:45:00,89.1,144.674,41.0,32.613 +2020-02-12 03:00:00,85.93,147.511,41.318000000000005,32.613 +2020-02-12 03:15:00,86.61,148.835,41.318000000000005,32.613 +2020-02-12 03:30:00,84.88,150.577,41.318000000000005,32.613 +2020-02-12 03:45:00,88.19,152.627,41.318000000000005,32.613 +2020-02-12 04:00:00,87.93,163.749,42.544,32.613 +2020-02-12 04:15:00,84.22,174.896,42.544,32.613 +2020-02-12 04:30:00,85.75,178.579,42.544,32.613 +2020-02-12 04:45:00,92.54,181.30200000000002,42.544,32.613 +2020-02-12 05:00:00,97.5,214.71400000000003,45.161,32.613 +2020-02-12 05:15:00,96.39,242.053,45.161,32.613 +2020-02-12 05:30:00,93.69,237.52200000000002,45.161,32.613 +2020-02-12 05:45:00,97.9,231.282,45.161,32.613 +2020-02-12 06:00:00,105.8,228.73,61.86600000000001,32.613 +2020-02-12 06:15:00,112.45,235.005,61.86600000000001,32.613 +2020-02-12 06:30:00,115.07,237.782,61.86600000000001,32.613 +2020-02-12 06:45:00,119.42,242.891,61.86600000000001,32.613 +2020-02-12 07:00:00,125.77,242.01,77.814,32.613 +2020-02-12 07:15:00,125.17,246.997,77.814,32.613 +2020-02-12 07:30:00,125.81,249.429,77.814,32.613 +2020-02-12 07:45:00,127.26,250.237,77.814,32.613 +2020-02-12 08:00:00,129.7,248.55900000000003,70.251,32.613 +2020-02-12 08:15:00,127.18,248.287,70.251,32.613 +2020-02-12 08:30:00,128.56,244.618,70.251,32.613 +2020-02-12 08:45:00,125.84,241.541,70.251,32.613 +2020-02-12 09:00:00,125.04,234.88099999999997,66.965,32.613 +2020-02-12 09:15:00,127.84,232.13,66.965,32.613 +2020-02-12 09:30:00,126.14,230.763,66.965,32.613 +2020-02-12 09:45:00,126.04,227.449,66.965,32.613 +2020-02-12 10:00:00,123.44,222.047,63.628,32.613 +2020-02-12 10:15:00,124.2,218.708,63.628,32.613 +2020-02-12 10:30:00,123.23,215.067,63.628,32.613 +2020-02-12 10:45:00,121.64,213.56599999999997,63.628,32.613 +2020-02-12 11:00:00,120.34,210.44400000000002,62.516999999999996,32.613 +2020-02-12 11:15:00,122.82,209.362,62.516999999999996,32.613 +2020-02-12 11:30:00,118.97,207.74099999999999,62.516999999999996,32.613 +2020-02-12 11:45:00,119.71,207.16,62.516999999999996,32.613 +2020-02-12 12:00:00,116.2,203.298,60.888999999999996,32.613 +2020-02-12 12:15:00,117.0,203.38,60.888999999999996,32.613 +2020-02-12 12:30:00,116.14,202.638,60.888999999999996,32.613 +2020-02-12 12:45:00,117.32,203.196,60.888999999999996,32.613 +2020-02-12 13:00:00,114.38,201.357,61.57899999999999,32.613 +2020-02-12 13:15:00,115.3,200.15400000000002,61.57899999999999,32.613 +2020-02-12 13:30:00,113.12,199.065,61.57899999999999,32.613 +2020-02-12 13:45:00,117.39,199.18,61.57899999999999,32.613 +2020-02-12 14:00:00,117.12,199.81,62.602,32.613 +2020-02-12 14:15:00,116.5,199.92,62.602,32.613 +2020-02-12 14:30:00,117.79,200.59799999999998,62.602,32.613 +2020-02-12 14:45:00,117.74,201.63299999999998,62.602,32.613 +2020-02-12 15:00:00,118.94,202.998,64.259,32.613 +2020-02-12 15:15:00,120.51,202.979,64.259,32.613 +2020-02-12 15:30:00,117.33,204.179,64.259,32.613 +2020-02-12 15:45:00,117.84,204.908,64.259,32.613 +2020-02-12 16:00:00,118.53,206.317,67.632,32.613 +2020-02-12 16:15:00,121.72,208.03400000000002,67.632,32.613 +2020-02-12 16:30:00,120.12,210.935,67.632,32.613 +2020-02-12 16:45:00,121.93,212.081,67.632,32.613 +2020-02-12 17:00:00,125.32,214.22400000000002,72.583,32.613 +2020-02-12 17:15:00,126.29,215.782,72.583,32.613 +2020-02-12 17:30:00,131.21,217.66400000000002,72.583,32.613 +2020-02-12 17:45:00,134.28,218.209,72.583,32.613 +2020-02-12 18:00:00,137.59,220.34099999999998,72.744,32.613 +2020-02-12 18:15:00,135.78,218.24900000000002,72.744,32.613 +2020-02-12 18:30:00,137.74,217.264,72.744,32.613 +2020-02-12 18:45:00,138.12,218.18099999999998,72.744,32.613 +2020-02-12 19:00:00,134.84,215.812,69.684,32.613 +2020-02-12 19:15:00,131.69,212.063,69.684,32.613 +2020-02-12 19:30:00,130.22,209.61700000000002,69.684,32.613 +2020-02-12 19:45:00,132.79,207.10299999999998,69.684,32.613 +2020-02-12 20:00:00,122.05,202.40400000000002,70.036,32.613 +2020-02-12 20:15:00,117.75,196.06,70.036,32.613 +2020-02-12 20:30:00,118.3,192.02200000000002,70.036,32.613 +2020-02-12 20:45:00,115.96,190.91099999999997,70.036,32.613 +2020-02-12 21:00:00,112.05,187.018,60.431999999999995,32.613 +2020-02-12 21:15:00,114.02,183.84900000000002,60.431999999999995,32.613 +2020-02-12 21:30:00,113.2,181.83599999999998,60.431999999999995,32.613 +2020-02-12 21:45:00,108.74,180.792,60.431999999999995,32.613 +2020-02-12 22:00:00,99.68,173.387,56.2,32.613 +2020-02-12 22:15:00,100.84,168.261,56.2,32.613 +2020-02-12 22:30:00,105.11,154.175,56.2,32.613 +2020-02-12 22:45:00,104.72,146.158,56.2,32.613 +2020-02-12 23:00:00,97.34,138.80100000000002,47.927,32.613 +2020-02-12 23:15:00,89.87,137.672,47.927,32.613 +2020-02-12 23:30:00,93.4,138.35399999999998,47.927,32.613 +2020-02-12 23:45:00,93.96,138.24,47.927,32.613 +2020-02-13 00:00:00,89.19,132.594,43.794,32.613 +2020-02-13 00:15:00,88.82,131.93,43.794,32.613 +2020-02-13 00:30:00,89.62,132.795,43.794,32.613 +2020-02-13 00:45:00,88.21,134.158,43.794,32.613 +2020-02-13 01:00:00,82.63,136.571,42.397,32.613 +2020-02-13 01:15:00,82.36,136.33,42.397,32.613 +2020-02-13 01:30:00,84.87,136.498,42.397,32.613 +2020-02-13 01:45:00,85.58,136.821,42.397,32.613 +2020-02-13 02:00:00,81.21,139.126,40.010999999999996,32.613 +2020-02-13 02:15:00,80.58,141.09,40.010999999999996,32.613 +2020-02-13 02:30:00,84.33,142.36,40.010999999999996,32.613 +2020-02-13 02:45:00,85.66,144.425,40.010999999999996,32.613 +2020-02-13 03:00:00,84.48,147.266,39.181,32.613 +2020-02-13 03:15:00,84.82,148.589,39.181,32.613 +2020-02-13 03:30:00,87.48,150.329,39.181,32.613 +2020-02-13 03:45:00,87.5,152.391,39.181,32.613 +2020-02-13 04:00:00,84.29,163.512,40.39,32.613 +2020-02-13 04:15:00,82.01,174.649,40.39,32.613 +2020-02-13 04:30:00,82.76,178.34900000000002,40.39,32.613 +2020-02-13 04:45:00,84.79,181.05900000000003,40.39,32.613 +2020-02-13 05:00:00,88.04,214.454,45.504,32.613 +2020-02-13 05:15:00,91.08,241.812,45.504,32.613 +2020-02-13 05:30:00,93.75,237.25400000000002,45.504,32.613 +2020-02-13 05:45:00,99.4,231.018,45.504,32.613 +2020-02-13 06:00:00,105.36,228.475,57.748000000000005,32.613 +2020-02-13 06:15:00,108.99,234.764,57.748000000000005,32.613 +2020-02-13 06:30:00,113.77,237.51,57.748000000000005,32.613 +2020-02-13 06:45:00,118.6,242.61700000000002,57.748000000000005,32.613 +2020-02-13 07:00:00,125.32,241.76,72.138,32.613 +2020-02-13 07:15:00,124.14,246.71900000000002,72.138,32.613 +2020-02-13 07:30:00,127.06,249.11700000000002,72.138,32.613 +2020-02-13 07:45:00,125.97,249.882,72.138,32.613 +2020-02-13 08:00:00,128.01,248.18200000000002,65.542,32.613 +2020-02-13 08:15:00,126.31,247.885,65.542,32.613 +2020-02-13 08:30:00,125.65,244.155,65.542,32.613 +2020-02-13 08:45:00,126.67,241.079,65.542,32.613 +2020-02-13 09:00:00,125.47,234.424,60.523,32.613 +2020-02-13 09:15:00,126.89,231.675,60.523,32.613 +2020-02-13 09:30:00,129.73,230.329,60.523,32.613 +2020-02-13 09:45:00,129.88,227.015,60.523,32.613 +2020-02-13 10:00:00,131.24,221.625,57.449,32.613 +2020-02-13 10:15:00,135.05,218.315,57.449,32.613 +2020-02-13 10:30:00,132.88,214.68400000000003,57.449,32.613 +2020-02-13 10:45:00,133.42,213.19799999999998,57.449,32.613 +2020-02-13 11:00:00,134.39,210.065,54.505,32.613 +2020-02-13 11:15:00,130.99,208.99599999999998,54.505,32.613 +2020-02-13 11:30:00,130.46,207.382,54.505,32.613 +2020-02-13 11:45:00,131.92,206.813,54.505,32.613 +2020-02-13 12:00:00,132.04,202.96599999999998,51.50899999999999,32.613 +2020-02-13 12:15:00,132.58,203.065,51.50899999999999,32.613 +2020-02-13 12:30:00,130.4,202.295,51.50899999999999,32.613 +2020-02-13 12:45:00,130.32,202.84900000000002,51.50899999999999,32.613 +2020-02-13 13:00:00,128.38,201.03599999999997,51.303999999999995,32.613 +2020-02-13 13:15:00,128.1,199.808,51.303999999999995,32.613 +2020-02-13 13:30:00,127.22,198.702,51.303999999999995,32.613 +2020-02-13 13:45:00,127.79,198.81900000000002,51.303999999999995,32.613 +2020-02-13 14:00:00,128.62,199.505,52.785,32.613 +2020-02-13 14:15:00,126.84,199.595,52.785,32.613 +2020-02-13 14:30:00,126.35,200.25599999999997,52.785,32.613 +2020-02-13 14:45:00,128.53,201.30599999999998,52.785,32.613 +2020-02-13 15:00:00,129.93,202.678,56.458999999999996,32.613 +2020-02-13 15:15:00,128.24,202.62900000000002,56.458999999999996,32.613 +2020-02-13 15:30:00,126.87,203.79,56.458999999999996,32.613 +2020-02-13 15:45:00,125.33,204.50099999999998,56.458999999999996,32.613 +2020-02-13 16:00:00,127.19,205.91099999999997,59.388000000000005,32.613 +2020-02-13 16:15:00,124.76,207.618,59.388000000000005,32.613 +2020-02-13 16:30:00,125.2,210.52200000000002,59.388000000000005,32.613 +2020-02-13 16:45:00,126.24,211.65,59.388000000000005,32.613 +2020-02-13 17:00:00,130.87,213.791,64.462,32.613 +2020-02-13 17:15:00,132.84,215.37400000000002,64.462,32.613 +2020-02-13 17:30:00,137.45,217.291,64.462,32.613 +2020-02-13 17:45:00,138.46,217.863,64.462,32.613 +2020-02-13 18:00:00,138.81,220.00900000000001,65.128,32.613 +2020-02-13 18:15:00,139.01,217.975,65.128,32.613 +2020-02-13 18:30:00,137.4,216.99200000000002,65.128,32.613 +2020-02-13 18:45:00,137.2,217.93200000000002,65.128,32.613 +2020-02-13 19:00:00,135.8,215.521,61.316,32.613 +2020-02-13 19:15:00,131.56,211.78799999999998,61.316,32.613 +2020-02-13 19:30:00,134.76,209.36599999999999,61.316,32.613 +2020-02-13 19:45:00,138.85,206.887,61.316,32.613 +2020-02-13 20:00:00,128.5,202.157,59.845,32.613 +2020-02-13 20:15:00,118.6,195.827,59.845,32.613 +2020-02-13 20:30:00,113.73,191.801,59.845,32.613 +2020-02-13 20:45:00,114.63,190.69299999999998,59.845,32.613 +2020-02-13 21:00:00,108.1,186.78400000000002,54.83,32.613 +2020-02-13 21:15:00,111.9,183.602,54.83,32.613 +2020-02-13 21:30:00,111.83,181.59,54.83,32.613 +2020-02-13 21:45:00,111.1,180.563,54.83,32.613 +2020-02-13 22:00:00,98.11,173.14,50.933,32.613 +2020-02-13 22:15:00,97.59,168.041,50.933,32.613 +2020-02-13 22:30:00,96.24,153.912,50.933,32.613 +2020-02-13 22:45:00,91.94,145.90200000000002,50.933,32.613 +2020-02-13 23:00:00,90.76,138.541,45.32899999999999,32.613 +2020-02-13 23:15:00,94.89,137.42700000000002,45.32899999999999,32.613 +2020-02-13 23:30:00,92.24,138.123,45.32899999999999,32.613 +2020-02-13 23:45:00,90.49,138.031,45.32899999999999,32.613 +2020-02-14 00:00:00,80.41,131.44799999999998,43.74,32.613 +2020-02-14 00:15:00,85.51,130.975,43.74,32.613 +2020-02-14 00:30:00,87.17,131.634,43.74,32.613 +2020-02-14 00:45:00,86.07,133.062,43.74,32.613 +2020-02-14 01:00:00,76.17,135.139,42.555,32.613 +2020-02-14 01:15:00,80.38,135.996,42.555,32.613 +2020-02-14 01:30:00,83.38,135.805,42.555,32.613 +2020-02-14 01:45:00,82.94,136.282,42.555,32.613 +2020-02-14 02:00:00,77.22,138.555,41.68600000000001,32.613 +2020-02-14 02:15:00,79.25,140.39600000000002,41.68600000000001,32.613 +2020-02-14 02:30:00,82.1,142.158,41.68600000000001,32.613 +2020-02-14 02:45:00,84.42,144.364,41.68600000000001,32.613 +2020-02-14 03:00:00,82.15,145.974,42.278999999999996,32.613 +2020-02-14 03:15:00,82.27,148.532,42.278999999999996,32.613 +2020-02-14 03:30:00,85.54,150.282,42.278999999999996,32.613 +2020-02-14 03:45:00,84.15,152.6,42.278999999999996,32.613 +2020-02-14 04:00:00,80.78,163.96400000000003,43.742,32.613 +2020-02-14 04:15:00,81.02,175.043,43.742,32.613 +2020-02-14 04:30:00,81.93,178.87900000000002,43.742,32.613 +2020-02-14 04:45:00,84.27,180.37400000000002,43.742,32.613 +2020-02-14 05:00:00,87.26,212.38099999999997,46.973,32.613 +2020-02-14 05:15:00,88.2,241.33599999999998,46.973,32.613 +2020-02-14 05:30:00,91.52,237.951,46.973,32.613 +2020-02-14 05:45:00,95.37,231.71099999999998,46.973,32.613 +2020-02-14 06:00:00,102.89,229.63400000000001,59.63399999999999,32.613 +2020-02-14 06:15:00,106.98,234.238,59.63399999999999,32.613 +2020-02-14 06:30:00,110.84,235.976,59.63399999999999,32.613 +2020-02-14 06:45:00,116.04,242.94799999999998,59.63399999999999,32.613 +2020-02-14 07:00:00,122.39,241.06900000000002,71.631,32.613 +2020-02-14 07:15:00,121.91,247.02900000000002,71.631,32.613 +2020-02-14 07:30:00,125.24,249.47099999999998,71.631,32.613 +2020-02-14 07:45:00,123.6,249.21599999999998,71.631,32.613 +2020-02-14 08:00:00,125.27,246.135,66.181,32.613 +2020-02-14 08:15:00,123.65,245.28400000000002,66.181,32.613 +2020-02-14 08:30:00,123.84,242.607,66.181,32.613 +2020-02-14 08:45:00,123.1,237.773,66.181,32.613 +2020-02-14 09:00:00,121.88,231.95,63.086000000000006,32.613 +2020-02-14 09:15:00,127.03,229.597,63.086000000000006,32.613 +2020-02-14 09:30:00,125.21,227.877,63.086000000000006,32.613 +2020-02-14 09:45:00,125.55,224.393,63.086000000000006,32.613 +2020-02-14 10:00:00,120.82,217.755,60.886,32.613 +2020-02-14 10:15:00,120.02,215.28900000000002,60.886,32.613 +2020-02-14 10:30:00,120.71,211.518,60.886,32.613 +2020-02-14 10:45:00,119.38,209.56,60.886,32.613 +2020-02-14 11:00:00,118.72,206.375,59.391000000000005,32.613 +2020-02-14 11:15:00,118.29,204.453,59.391000000000005,32.613 +2020-02-14 11:30:00,117.37,204.847,59.391000000000005,32.613 +2020-02-14 11:45:00,114.65,204.46,59.391000000000005,32.613 +2020-02-14 12:00:00,113.88,201.791,56.172,32.613 +2020-02-14 12:15:00,113.37,199.643,56.172,32.613 +2020-02-14 12:30:00,111.06,199.00900000000001,56.172,32.613 +2020-02-14 12:45:00,114.09,200.232,56.172,32.613 +2020-02-14 13:00:00,109.71,199.421,54.406000000000006,32.613 +2020-02-14 13:15:00,109.48,199.05900000000003,54.406000000000006,32.613 +2020-02-14 13:30:00,108.28,197.857,54.406000000000006,32.613 +2020-02-14 13:45:00,110.11,197.86599999999999,54.406000000000006,32.613 +2020-02-14 14:00:00,112.37,197.43200000000002,53.578,32.613 +2020-02-14 14:15:00,116.73,197.24900000000002,53.578,32.613 +2020-02-14 14:30:00,116.58,198.28400000000002,53.578,32.613 +2020-02-14 14:45:00,115.29,199.769,53.578,32.613 +2020-02-14 15:00:00,113.44,200.61700000000002,56.568999999999996,32.613 +2020-02-14 15:15:00,112.25,200.078,56.568999999999996,32.613 +2020-02-14 15:30:00,111.0,199.579,56.568999999999996,32.613 +2020-02-14 15:45:00,113.13,200.34599999999998,56.568999999999996,32.613 +2020-02-14 16:00:00,114.03,200.571,60.169,32.613 +2020-02-14 16:15:00,115.56,202.53400000000002,60.169,32.613 +2020-02-14 16:30:00,117.3,205.56599999999997,60.169,32.613 +2020-02-14 16:45:00,117.51,206.63299999999998,60.169,32.613 +2020-02-14 17:00:00,123.02,208.825,65.497,32.613 +2020-02-14 17:15:00,123.09,209.998,65.497,32.613 +2020-02-14 17:30:00,125.89,211.582,65.497,32.613 +2020-02-14 17:45:00,129.33,211.94,65.497,32.613 +2020-02-14 18:00:00,131.38,214.87599999999998,65.082,32.613 +2020-02-14 18:15:00,129.52,212.56799999999998,65.082,32.613 +2020-02-14 18:30:00,129.37,212.02700000000002,65.082,32.613 +2020-02-14 18:45:00,130.15,212.945,65.082,32.613 +2020-02-14 19:00:00,127.94,211.408,60.968,32.613 +2020-02-14 19:15:00,128.26,209.11900000000003,60.968,32.613 +2020-02-14 19:30:00,129.49,206.266,60.968,32.613 +2020-02-14 19:45:00,134.25,203.418,60.968,32.613 +2020-02-14 20:00:00,123.84,198.725,61.123000000000005,32.613 +2020-02-14 20:15:00,113.6,192.33599999999998,61.123000000000005,32.613 +2020-02-14 20:30:00,111.96,188.327,61.123000000000005,32.613 +2020-02-14 20:45:00,109.54,187.91299999999998,61.123000000000005,32.613 +2020-02-14 21:00:00,103.24,184.41400000000002,55.416000000000004,32.613 +2020-02-14 21:15:00,107.66,181.52900000000002,55.416000000000004,32.613 +2020-02-14 21:30:00,106.5,179.583,55.416000000000004,32.613 +2020-02-14 21:45:00,100.0,179.14700000000002,55.416000000000004,32.613 +2020-02-14 22:00:00,92.22,172.795,51.631,32.613 +2020-02-14 22:15:00,89.89,167.59900000000002,51.631,32.613 +2020-02-14 22:30:00,87.77,159.931,51.631,32.613 +2020-02-14 22:45:00,89.65,155.7,51.631,32.613 +2020-02-14 23:00:00,81.69,147.679,44.898,32.613 +2020-02-14 23:15:00,87.18,144.605,44.898,32.613 +2020-02-14 23:30:00,86.71,143.9,44.898,32.613 +2020-02-14 23:45:00,86.41,143.115,44.898,32.613 +2020-02-15 00:00:00,79.08,116.521,42.033,32.431999999999995 +2020-02-15 00:15:00,72.83,111.295,42.033,32.431999999999995 +2020-02-15 00:30:00,73.01,112.603,42.033,32.431999999999995 +2020-02-15 00:45:00,78.83,113.965,42.033,32.431999999999995 +2020-02-15 01:00:00,77.6,116.42,38.255,32.431999999999995 +2020-02-15 01:15:00,76.74,116.64299999999999,38.255,32.431999999999995 +2020-02-15 01:30:00,71.1,116.18299999999999,38.255,32.431999999999995 +2020-02-15 01:45:00,68.6,116.26,38.255,32.431999999999995 +2020-02-15 02:00:00,68.99,119.05,36.404,32.431999999999995 +2020-02-15 02:15:00,77.1,119.95700000000001,36.404,32.431999999999995 +2020-02-15 02:30:00,76.22,120.164,36.404,32.431999999999995 +2020-02-15 02:45:00,74.46,122.105,36.404,32.431999999999995 +2020-02-15 03:00:00,68.01,124.14200000000001,36.083,32.431999999999995 +2020-02-15 03:15:00,70.9,125.476,36.083,32.431999999999995 +2020-02-15 03:30:00,76.79,125.72399999999999,36.083,32.431999999999995 +2020-02-15 03:45:00,72.61,127.546,36.083,32.431999999999995 +2020-02-15 04:00:00,70.23,135.398,36.102,32.431999999999995 +2020-02-15 04:15:00,69.29,144.376,36.102,32.431999999999995 +2020-02-15 04:30:00,69.13,144.93200000000002,36.102,32.431999999999995 +2020-02-15 04:45:00,70.59,145.541,36.102,32.431999999999995 +2020-02-15 05:00:00,72.01,161.107,35.284,32.431999999999995 +2020-02-15 05:15:00,73.57,171.671,35.284,32.431999999999995 +2020-02-15 05:30:00,71.26,168.453,35.284,32.431999999999995 +2020-02-15 05:45:00,73.74,166.71900000000002,35.284,32.431999999999995 +2020-02-15 06:00:00,74.58,183.00599999999997,36.265,32.431999999999995 +2020-02-15 06:15:00,75.42,203.584,36.265,32.431999999999995 +2020-02-15 06:30:00,76.43,199.912,36.265,32.431999999999995 +2020-02-15 06:45:00,78.71,196.593,36.265,32.431999999999995 +2020-02-15 07:00:00,80.29,192.623,40.714,32.431999999999995 +2020-02-15 07:15:00,81.02,196.359,40.714,32.431999999999995 +2020-02-15 07:30:00,83.66,200.294,40.714,32.431999999999995 +2020-02-15 07:45:00,87.0,203.146,40.714,32.431999999999995 +2020-02-15 08:00:00,90.02,204.477,46.692,32.431999999999995 +2020-02-15 08:15:00,90.93,206.66299999999998,46.692,32.431999999999995 +2020-02-15 08:30:00,92.08,205.672,46.692,32.431999999999995 +2020-02-15 08:45:00,93.93,203.412,46.692,32.431999999999995 +2020-02-15 09:00:00,96.51,198.43,48.925,32.431999999999995 +2020-02-15 09:15:00,95.91,196.627,48.925,32.431999999999995 +2020-02-15 09:30:00,96.0,195.517,48.925,32.431999999999995 +2020-02-15 09:45:00,96.39,192.588,48.925,32.431999999999995 +2020-02-15 10:00:00,96.58,187.47799999999998,47.799,32.431999999999995 +2020-02-15 10:15:00,96.18,184.49900000000002,47.799,32.431999999999995 +2020-02-15 10:30:00,95.93,182.03599999999997,47.799,32.431999999999995 +2020-02-15 10:45:00,96.38,181.78799999999998,47.799,32.431999999999995 +2020-02-15 11:00:00,98.86,180.104,44.309,32.431999999999995 +2020-02-15 11:15:00,100.22,177.608,44.309,32.431999999999995 +2020-02-15 11:30:00,99.83,177.332,44.309,32.431999999999995 +2020-02-15 11:45:00,98.79,175.109,44.309,32.431999999999995 +2020-02-15 12:00:00,99.97,170.31400000000002,42.367,32.431999999999995 +2020-02-15 12:15:00,98.06,168.968,42.367,32.431999999999995 +2020-02-15 12:30:00,94.95,168.99599999999998,42.367,32.431999999999995 +2020-02-15 12:45:00,93.22,169.708,42.367,32.431999999999995 +2020-02-15 13:00:00,91.16,169.28,39.036,32.431999999999995 +2020-02-15 13:15:00,90.52,166.933,39.036,32.431999999999995 +2020-02-15 13:30:00,89.43,165.641,39.036,32.431999999999995 +2020-02-15 13:45:00,90.29,165.61900000000003,39.036,32.431999999999995 +2020-02-15 14:00:00,88.86,166.04,37.995,32.431999999999995 +2020-02-15 14:15:00,88.98,165.236,37.995,32.431999999999995 +2020-02-15 14:30:00,89.39,164.611,37.995,32.431999999999995 +2020-02-15 14:45:00,92.56,165.851,37.995,32.431999999999995 +2020-02-15 15:00:00,90.72,167.85299999999998,40.71,32.431999999999995 +2020-02-15 15:15:00,93.83,167.59599999999998,40.71,32.431999999999995 +2020-02-15 15:30:00,90.34,169.00400000000002,40.71,32.431999999999995 +2020-02-15 15:45:00,89.96,170.202,40.71,32.431999999999995 +2020-02-15 16:00:00,91.14,169.48,46.998000000000005,32.431999999999995 +2020-02-15 16:15:00,90.96,172.19400000000002,46.998000000000005,32.431999999999995 +2020-02-15 16:30:00,91.81,175.07299999999998,46.998000000000005,32.431999999999995 +2020-02-15 16:45:00,93.73,177.238,46.998000000000005,32.431999999999995 +2020-02-15 17:00:00,98.33,178.832,55.431000000000004,32.431999999999995 +2020-02-15 17:15:00,99.99,182.15599999999998,55.431000000000004,32.431999999999995 +2020-02-15 17:30:00,103.94,184.06900000000002,55.431000000000004,32.431999999999995 +2020-02-15 17:45:00,107.33,184.25099999999998,55.431000000000004,32.431999999999995 +2020-02-15 18:00:00,110.74,186.635,55.989,32.431999999999995 +2020-02-15 18:15:00,108.62,187.128,55.989,32.431999999999995 +2020-02-15 18:30:00,109.33,187.54,55.989,32.431999999999995 +2020-02-15 18:45:00,107.71,185.11,55.989,32.431999999999995 +2020-02-15 19:00:00,106.51,186.041,50.882,32.431999999999995 +2020-02-15 19:15:00,104.82,183.521,50.882,32.431999999999995 +2020-02-15 19:30:00,103.08,182.215,50.882,32.431999999999995 +2020-02-15 19:45:00,102.01,178.392,50.882,32.431999999999995 +2020-02-15 20:00:00,96.44,176.037,43.172,32.431999999999995 +2020-02-15 20:15:00,92.99,172.542,43.172,32.431999999999995 +2020-02-15 20:30:00,90.07,168.233,43.172,32.431999999999995 +2020-02-15 20:45:00,89.39,166.56,43.172,32.431999999999995 +2020-02-15 21:00:00,84.61,166.14,37.599000000000004,32.431999999999995 +2020-02-15 21:15:00,83.31,164.019,37.599000000000004,32.431999999999995 +2020-02-15 21:30:00,82.48,163.214,37.599000000000004,32.431999999999995 +2020-02-15 21:45:00,81.66,162.619,37.599000000000004,32.431999999999995 +2020-02-15 22:00:00,77.87,157.94799999999998,39.047,32.431999999999995 +2020-02-15 22:15:00,76.64,155.707,39.047,32.431999999999995 +2020-02-15 22:30:00,74.14,154.209,39.047,32.431999999999995 +2020-02-15 22:45:00,73.06,152.016,39.047,32.431999999999995 +2020-02-15 23:00:00,69.29,147.38299999999998,32.339,32.431999999999995 +2020-02-15 23:15:00,69.86,142.295,32.339,32.431999999999995 +2020-02-15 23:30:00,65.81,140.22,32.339,32.431999999999995 +2020-02-15 23:45:00,64.83,137.039,32.339,32.431999999999995 +2020-02-16 00:00:00,59.92,116.76700000000001,29.988000000000003,32.431999999999995 +2020-02-16 00:15:00,60.99,111.206,29.988000000000003,32.431999999999995 +2020-02-16 00:30:00,60.45,112.125,29.988000000000003,32.431999999999995 +2020-02-16 00:45:00,59.8,114.161,29.988000000000003,32.431999999999995 +2020-02-16 01:00:00,56.82,116.45,28.531999999999996,32.431999999999995 +2020-02-16 01:15:00,58.25,117.669,28.531999999999996,32.431999999999995 +2020-02-16 01:30:00,57.58,117.70200000000001,28.531999999999996,32.431999999999995 +2020-02-16 01:45:00,56.97,117.46600000000001,28.531999999999996,32.431999999999995 +2020-02-16 02:00:00,55.01,119.524,27.805999999999997,32.431999999999995 +2020-02-16 02:15:00,55.92,119.61399999999999,27.805999999999997,32.431999999999995 +2020-02-16 02:30:00,55.77,120.67200000000001,27.805999999999997,32.431999999999995 +2020-02-16 02:45:00,55.31,123.05799999999999,27.805999999999997,32.431999999999995 +2020-02-16 03:00:00,54.28,125.42200000000001,26.193,32.431999999999995 +2020-02-16 03:15:00,56.05,126.24799999999999,26.193,32.431999999999995 +2020-02-16 03:30:00,55.3,127.836,26.193,32.431999999999995 +2020-02-16 03:45:00,55.85,129.576,26.193,32.431999999999995 +2020-02-16 04:00:00,55.44,137.189,27.19,32.431999999999995 +2020-02-16 04:15:00,56.58,145.164,27.19,32.431999999999995 +2020-02-16 04:30:00,56.86,145.828,27.19,32.431999999999995 +2020-02-16 04:45:00,58.08,146.68,27.19,32.431999999999995 +2020-02-16 05:00:00,58.38,158.819,28.166999999999998,32.431999999999995 +2020-02-16 05:15:00,58.85,167.02900000000002,28.166999999999998,32.431999999999995 +2020-02-16 05:30:00,59.76,163.615,28.166999999999998,32.431999999999995 +2020-02-16 05:45:00,60.47,162.11700000000002,28.166999999999998,32.431999999999995 +2020-02-16 06:00:00,61.37,178.252,27.16,32.431999999999995 +2020-02-16 06:15:00,64.85,197.18900000000002,27.16,32.431999999999995 +2020-02-16 06:30:00,63.55,192.395,27.16,32.431999999999995 +2020-02-16 06:45:00,65.16,188.02,27.16,32.431999999999995 +2020-02-16 07:00:00,68.28,186.46,29.578000000000003,32.431999999999995 +2020-02-16 07:15:00,67.41,189.298,29.578000000000003,32.431999999999995 +2020-02-16 07:30:00,69.82,192.037,29.578000000000003,32.431999999999995 +2020-02-16 07:45:00,72.92,194.109,29.578000000000003,32.431999999999995 +2020-02-16 08:00:00,74.96,197.226,34.650999999999996,32.431999999999995 +2020-02-16 08:15:00,75.92,199.338,34.650999999999996,32.431999999999995 +2020-02-16 08:30:00,77.55,199.925,34.650999999999996,32.431999999999995 +2020-02-16 08:45:00,79.55,199.627,34.650999999999996,32.431999999999995 +2020-02-16 09:00:00,81.5,194.25599999999997,38.080999999999996,32.431999999999995 +2020-02-16 09:15:00,82.17,192.979,38.080999999999996,32.431999999999995 +2020-02-16 09:30:00,83.33,191.74900000000002,38.080999999999996,32.431999999999995 +2020-02-16 09:45:00,84.69,188.739,38.080999999999996,32.431999999999995 +2020-02-16 10:00:00,85.76,186.095,39.934,32.431999999999995 +2020-02-16 10:15:00,85.84,183.66400000000002,39.934,32.431999999999995 +2020-02-16 10:30:00,89.06,181.803,39.934,32.431999999999995 +2020-02-16 10:45:00,89.12,179.768,39.934,32.431999999999995 +2020-02-16 11:00:00,92.11,178.949,43.74100000000001,32.431999999999995 +2020-02-16 11:15:00,95.2,176.582,43.74100000000001,32.431999999999995 +2020-02-16 11:30:00,96.92,175.487,43.74100000000001,32.431999999999995 +2020-02-16 11:45:00,95.96,173.87099999999998,43.74100000000001,32.431999999999995 +2020-02-16 12:00:00,93.67,168.581,40.001999999999995,32.431999999999995 +2020-02-16 12:15:00,92.58,169.065,40.001999999999995,32.431999999999995 +2020-02-16 12:30:00,89.3,167.671,40.001999999999995,32.431999999999995 +2020-02-16 12:45:00,90.13,167.426,40.001999999999995,32.431999999999995 +2020-02-16 13:00:00,84.56,166.325,37.855,32.431999999999995 +2020-02-16 13:15:00,84.84,166.843,37.855,32.431999999999995 +2020-02-16 13:30:00,82.46,165.296,37.855,32.431999999999995 +2020-02-16 13:45:00,82.47,164.67700000000002,37.855,32.431999999999995 +2020-02-16 14:00:00,80.52,165.43,35.946999999999996,32.431999999999995 +2020-02-16 14:15:00,80.46,165.77599999999998,35.946999999999996,32.431999999999995 +2020-02-16 14:30:00,80.76,166.28,35.946999999999996,32.431999999999995 +2020-02-16 14:45:00,80.96,167.1,35.946999999999996,32.431999999999995 +2020-02-16 15:00:00,81.11,167.64,35.138000000000005,32.431999999999995 +2020-02-16 15:15:00,79.85,168.058,35.138000000000005,32.431999999999995 +2020-02-16 15:30:00,79.76,169.99,35.138000000000005,32.431999999999995 +2020-02-16 15:45:00,80.59,171.847,35.138000000000005,32.431999999999995 +2020-02-16 16:00:00,82.28,172.89,38.672,32.431999999999995 +2020-02-16 16:15:00,81.81,174.72099999999998,38.672,32.431999999999995 +2020-02-16 16:30:00,81.95,177.882,38.672,32.431999999999995 +2020-02-16 16:45:00,84.44,180.15599999999998,38.672,32.431999999999995 +2020-02-16 17:00:00,88.25,181.767,48.684,32.431999999999995 +2020-02-16 17:15:00,89.74,184.832,48.684,32.431999999999995 +2020-02-16 17:30:00,92.59,187.075,48.684,32.431999999999995 +2020-02-16 17:45:00,97.75,189.489,48.684,32.431999999999995 +2020-02-16 18:00:00,103.5,191.37400000000002,51.568999999999996,32.431999999999995 +2020-02-16 18:15:00,103.77,193.201,51.568999999999996,32.431999999999995 +2020-02-16 18:30:00,103.86,191.58,51.568999999999996,32.431999999999995 +2020-02-16 18:45:00,101.12,190.97400000000002,51.568999999999996,32.431999999999995 +2020-02-16 19:00:00,100.36,191.597,48.608000000000004,32.431999999999995 +2020-02-16 19:15:00,97.84,189.642,48.608000000000004,32.431999999999995 +2020-02-16 19:30:00,100.63,188.19299999999998,48.608000000000004,32.431999999999995 +2020-02-16 19:45:00,103.68,185.798,48.608000000000004,32.431999999999995 +2020-02-16 20:00:00,99.8,183.389,43.733999999999995,32.431999999999995 +2020-02-16 20:15:00,93.26,180.862,43.733999999999995,32.431999999999995 +2020-02-16 20:30:00,92.18,177.79,43.733999999999995,32.431999999999995 +2020-02-16 20:45:00,90.05,174.91400000000002,43.733999999999995,32.431999999999995 +2020-02-16 21:00:00,87.92,171.957,39.283,32.431999999999995 +2020-02-16 21:15:00,88.88,169.209,39.283,32.431999999999995 +2020-02-16 21:30:00,93.52,168.675,39.283,32.431999999999995 +2020-02-16 21:45:00,95.15,168.239,39.283,32.431999999999995 +2020-02-16 22:00:00,90.26,162.445,40.111,32.431999999999995 +2020-02-16 22:15:00,89.37,159.447,40.111,32.431999999999995 +2020-02-16 22:30:00,90.03,154.82,40.111,32.431999999999995 +2020-02-16 22:45:00,90.58,151.776,40.111,32.431999999999995 +2020-02-16 23:00:00,86.82,144.405,35.791,32.431999999999995 +2020-02-16 23:15:00,85.0,141.168,35.791,32.431999999999995 +2020-02-16 23:30:00,86.25,139.881,35.791,32.431999999999995 +2020-02-16 23:45:00,84.99,137.593,35.791,32.431999999999995 +2020-02-17 00:00:00,81.05,120.693,34.311,32.613 +2020-02-17 00:15:00,80.07,118.045,34.311,32.613 +2020-02-17 00:30:00,81.59,119.06200000000001,34.311,32.613 +2020-02-17 00:45:00,81.45,120.56,34.311,32.613 +2020-02-17 01:00:00,76.96,122.838,34.585,32.613 +2020-02-17 01:15:00,76.08,123.53,34.585,32.613 +2020-02-17 01:30:00,79.32,123.61200000000001,34.585,32.613 +2020-02-17 01:45:00,79.06,123.48700000000001,34.585,32.613 +2020-02-17 02:00:00,75.2,125.52799999999999,34.111,32.613 +2020-02-17 02:15:00,77.83,127.016,34.111,32.613 +2020-02-17 02:30:00,78.92,128.418,34.111,32.613 +2020-02-17 02:45:00,80.45,130.196,34.111,32.613 +2020-02-17 03:00:00,74.01,133.811,32.435,32.613 +2020-02-17 03:15:00,76.74,136.253,32.435,32.613 +2020-02-17 03:30:00,81.58,137.588,32.435,32.613 +2020-02-17 03:45:00,81.79,138.791,32.435,32.613 +2020-02-17 04:00:00,80.2,150.708,33.04,32.613 +2020-02-17 04:15:00,77.79,162.775,33.04,32.613 +2020-02-17 04:30:00,85.23,165.579,33.04,32.613 +2020-02-17 04:45:00,86.55,166.584,33.04,32.613 +2020-02-17 05:00:00,89.34,194.2,40.399,32.613 +2020-02-17 05:15:00,84.87,222.275,40.399,32.613 +2020-02-17 05:30:00,88.55,219.07,40.399,32.613 +2020-02-17 05:45:00,93.5,212.00799999999998,40.399,32.613 +2020-02-17 06:00:00,101.21,210.477,60.226000000000006,32.613 +2020-02-17 06:15:00,106.95,214.78900000000002,60.226000000000006,32.613 +2020-02-17 06:30:00,112.4,217.851,60.226000000000006,32.613 +2020-02-17 06:45:00,116.6,222.146,60.226000000000006,32.613 +2020-02-17 07:00:00,122.33,222.924,73.578,32.613 +2020-02-17 07:15:00,122.39,226.99400000000003,73.578,32.613 +2020-02-17 07:30:00,122.25,228.925,73.578,32.613 +2020-02-17 07:45:00,124.77,228.46599999999998,73.578,32.613 +2020-02-17 08:00:00,129.11,226.765,66.58,32.613 +2020-02-17 08:15:00,127.63,226.747,66.58,32.613 +2020-02-17 08:30:00,123.89,223.28599999999997,66.58,32.613 +2020-02-17 08:45:00,122.13,219.752,66.58,32.613 +2020-02-17 09:00:00,124.23,213.396,62.0,32.613 +2020-02-17 09:15:00,125.02,208.692,62.0,32.613 +2020-02-17 09:30:00,125.32,206.49,62.0,32.613 +2020-02-17 09:45:00,122.32,203.72299999999998,62.0,32.613 +2020-02-17 10:00:00,121.29,200.037,59.099,32.613 +2020-02-17 10:15:00,125.81,197.31599999999997,59.099,32.613 +2020-02-17 10:30:00,124.92,194.606,59.099,32.613 +2020-02-17 10:45:00,127.08,193.273,59.099,32.613 +2020-02-17 11:00:00,122.8,189.91400000000002,57.729,32.613 +2020-02-17 11:15:00,123.95,189.315,57.729,32.613 +2020-02-17 11:30:00,125.08,189.584,57.729,32.613 +2020-02-17 11:45:00,121.96,187.597,57.729,32.613 +2020-02-17 12:00:00,120.65,183.946,55.615,32.613 +2020-02-17 12:15:00,122.1,184.45,55.615,32.613 +2020-02-17 12:30:00,120.86,183.24200000000002,55.615,32.613 +2020-02-17 12:45:00,120.22,184.47099999999998,55.615,32.613 +2020-02-17 13:00:00,118.27,183.98,56.515,32.613 +2020-02-17 13:15:00,119.03,183.113,56.515,32.613 +2020-02-17 13:30:00,118.32,181.043,56.515,32.613 +2020-02-17 13:45:00,117.07,180.481,56.515,32.613 +2020-02-17 14:00:00,115.45,180.667,58.1,32.613 +2020-02-17 14:15:00,118.18,180.386,58.1,32.613 +2020-02-17 14:30:00,118.2,180.34400000000002,58.1,32.613 +2020-02-17 14:45:00,115.44,181.199,58.1,32.613 +2020-02-17 15:00:00,117.71,183.47299999999998,59.801,32.613 +2020-02-17 15:15:00,119.93,182.456,59.801,32.613 +2020-02-17 15:30:00,119.86,183.56599999999997,59.801,32.613 +2020-02-17 15:45:00,115.36,184.96400000000003,59.801,32.613 +2020-02-17 16:00:00,116.04,186.22400000000002,62.901,32.613 +2020-02-17 16:15:00,117.83,187.304,62.901,32.613 +2020-02-17 16:30:00,118.12,189.513,62.901,32.613 +2020-02-17 16:45:00,120.01,190.609,62.901,32.613 +2020-02-17 17:00:00,123.07,192.03900000000002,70.418,32.613 +2020-02-17 17:15:00,128.21,194.203,70.418,32.613 +2020-02-17 17:30:00,127.74,195.93200000000002,70.418,32.613 +2020-02-17 17:45:00,131.41,196.90400000000002,70.418,32.613 +2020-02-17 18:00:00,133.59,199.207,71.726,32.613 +2020-02-17 18:15:00,132.22,198.90099999999998,71.726,32.613 +2020-02-17 18:30:00,134.7,197.90400000000002,71.726,32.613 +2020-02-17 18:45:00,132.33,198.072,71.726,32.613 +2020-02-17 19:00:00,129.8,197.084,65.997,32.613 +2020-02-17 19:15:00,128.73,194.02,65.997,32.613 +2020-02-17 19:30:00,129.34,193.07299999999998,65.997,32.613 +2020-02-17 19:45:00,129.08,189.891,65.997,32.613 +2020-02-17 20:00:00,119.74,185.122,68.09100000000001,32.613 +2020-02-17 20:15:00,116.95,180.209,68.09100000000001,32.613 +2020-02-17 20:30:00,113.25,175.343,68.09100000000001,32.613 +2020-02-17 20:45:00,111.85,174.063,68.09100000000001,32.613 +2020-02-17 21:00:00,106.25,171.602,59.617,32.613 +2020-02-17 21:15:00,111.38,167.703,59.617,32.613 +2020-02-17 21:30:00,111.19,166.368,59.617,32.613 +2020-02-17 21:45:00,107.47,165.453,59.617,32.613 +2020-02-17 22:00:00,100.26,156.791,54.938,32.613 +2020-02-17 22:15:00,96.86,152.559,54.938,32.613 +2020-02-17 22:30:00,93.99,138.485,54.938,32.613 +2020-02-17 22:45:00,92.23,130.718,54.938,32.613 +2020-02-17 23:00:00,85.74,124.1,47.43,32.613 +2020-02-17 23:15:00,87.58,123.447,47.43,32.613 +2020-02-17 23:30:00,86.64,124.87200000000001,47.43,32.613 +2020-02-17 23:45:00,94.27,125.181,47.43,32.613 +2020-02-18 00:00:00,90.43,119.78299999999999,48.354,32.613 +2020-02-18 00:15:00,87.49,118.541,48.354,32.613 +2020-02-18 00:30:00,81.76,118.62799999999999,48.354,32.613 +2020-02-18 00:45:00,80.22,119.175,48.354,32.613 +2020-02-18 01:00:00,84.53,121.22,45.68600000000001,32.613 +2020-02-18 01:15:00,84.82,121.465,45.68600000000001,32.613 +2020-02-18 01:30:00,82.61,121.7,45.68600000000001,32.613 +2020-02-18 01:45:00,78.21,121.87,45.68600000000001,32.613 +2020-02-18 02:00:00,82.26,123.896,44.269,32.613 +2020-02-18 02:15:00,84.76,125.286,44.269,32.613 +2020-02-18 02:30:00,83.23,126.115,44.269,32.613 +2020-02-18 02:45:00,78.58,127.90799999999999,44.269,32.613 +2020-02-18 03:00:00,79.52,130.341,44.187,32.613 +2020-02-18 03:15:00,84.9,131.954,44.187,32.613 +2020-02-18 03:30:00,86.95,133.75799999999998,44.187,32.613 +2020-02-18 03:45:00,85.47,135.149,44.187,32.613 +2020-02-18 04:00:00,80.95,146.857,46.126999999999995,32.613 +2020-02-18 04:15:00,86.03,158.57399999999998,46.126999999999995,32.613 +2020-02-18 04:30:00,90.64,161.08700000000002,46.126999999999995,32.613 +2020-02-18 04:45:00,93.17,163.274,46.126999999999995,32.613 +2020-02-18 05:00:00,95.59,195.803,49.666000000000004,32.613 +2020-02-18 05:15:00,96.2,223.696,49.666000000000004,32.613 +2020-02-18 05:30:00,101.55,218.959,49.666000000000004,32.613 +2020-02-18 05:45:00,105.28,211.90200000000002,49.666000000000004,32.613 +2020-02-18 06:00:00,111.44,209.226,61.077,32.613 +2020-02-18 06:15:00,110.26,215.14700000000002,61.077,32.613 +2020-02-18 06:30:00,113.34,217.54,61.077,32.613 +2020-02-18 06:45:00,118.15,221.449,61.077,32.613 +2020-02-18 07:00:00,124.13,222.07,74.717,32.613 +2020-02-18 07:15:00,123.32,225.953,74.717,32.613 +2020-02-18 07:30:00,123.67,227.312,74.717,32.613 +2020-02-18 07:45:00,124.55,227.015,74.717,32.613 +2020-02-18 08:00:00,129.02,225.399,69.033,32.613 +2020-02-18 08:15:00,128.96,224.354,69.033,32.613 +2020-02-18 08:30:00,129.77,220.667,69.033,32.613 +2020-02-18 08:45:00,127.77,216.868,69.033,32.613 +2020-02-18 09:00:00,129.25,209.703,63.113,32.613 +2020-02-18 09:15:00,133.26,206.56900000000002,63.113,32.613 +2020-02-18 09:30:00,129.51,205.06,63.113,32.613 +2020-02-18 09:45:00,127.42,202.097,63.113,32.613 +2020-02-18 10:00:00,123.91,197.831,61.461999999999996,32.613 +2020-02-18 10:15:00,124.56,194.101,61.461999999999996,32.613 +2020-02-18 10:30:00,124.88,191.581,61.461999999999996,32.613 +2020-02-18 10:45:00,127.18,190.551,61.461999999999996,32.613 +2020-02-18 11:00:00,126.45,188.638,59.614,32.613 +2020-02-18 11:15:00,127.78,187.733,59.614,32.613 +2020-02-18 11:30:00,124.02,186.854,59.614,32.613 +2020-02-18 11:45:00,124.8,185.56099999999998,59.614,32.613 +2020-02-18 12:00:00,119.33,180.585,57.415,32.613 +2020-02-18 12:15:00,122.01,180.7,57.415,32.613 +2020-02-18 12:30:00,121.83,180.183,57.415,32.613 +2020-02-18 12:45:00,118.63,181.13099999999997,57.415,32.613 +2020-02-18 13:00:00,114.34,180.268,58.534,32.613 +2020-02-18 13:15:00,116.38,179.06400000000002,58.534,32.613 +2020-02-18 13:30:00,116.49,178.125,58.534,32.613 +2020-02-18 13:45:00,118.33,177.733,58.534,32.613 +2020-02-18 14:00:00,120.0,178.25900000000001,59.415,32.613 +2020-02-18 14:15:00,120.28,178.108,59.415,32.613 +2020-02-18 14:30:00,120.23,178.66400000000002,59.415,32.613 +2020-02-18 14:45:00,123.79,179.44299999999998,59.415,32.613 +2020-02-18 15:00:00,121.51,181.301,62.071999999999996,32.613 +2020-02-18 15:15:00,120.26,180.593,62.071999999999996,32.613 +2020-02-18 15:30:00,122.85,181.87900000000002,62.071999999999996,32.613 +2020-02-18 15:45:00,121.26,182.873,62.071999999999996,32.613 +2020-02-18 16:00:00,122.32,184.453,64.99,32.613 +2020-02-18 16:15:00,121.34,185.988,64.99,32.613 +2020-02-18 16:30:00,122.1,188.827,64.99,32.613 +2020-02-18 16:45:00,124.58,190.19,64.99,32.613 +2020-02-18 17:00:00,130.96,192.155,72.658,32.613 +2020-02-18 17:15:00,129.03,194.37,72.658,32.613 +2020-02-18 17:30:00,131.39,196.77700000000002,72.658,32.613 +2020-02-18 17:45:00,134.3,197.639,72.658,32.613 +2020-02-18 18:00:00,137.03,199.84099999999998,73.645,32.613 +2020-02-18 18:15:00,135.62,199.113,73.645,32.613 +2020-02-18 18:30:00,134.52,197.81,73.645,32.613 +2020-02-18 18:45:00,134.65,198.78099999999998,73.645,32.613 +2020-02-18 19:00:00,130.82,197.82299999999998,67.085,32.613 +2020-02-18 19:15:00,130.82,194.50099999999998,67.085,32.613 +2020-02-18 19:30:00,138.13,192.917,67.085,32.613 +2020-02-18 19:45:00,138.56,189.81,67.085,32.613 +2020-02-18 20:00:00,126.03,185.172,66.138,32.613 +2020-02-18 20:15:00,119.06,179.63099999999997,66.138,32.613 +2020-02-18 20:30:00,114.73,175.77599999999998,66.138,32.613 +2020-02-18 20:45:00,113.6,173.94299999999998,66.138,32.613 +2020-02-18 21:00:00,107.89,170.787,57.512,32.613 +2020-02-18 21:15:00,114.35,167.757,57.512,32.613 +2020-02-18 21:30:00,113.36,165.707,57.512,32.613 +2020-02-18 21:45:00,111.8,165.03599999999997,57.512,32.613 +2020-02-18 22:00:00,100.85,158.043,54.545,32.613 +2020-02-18 22:15:00,99.95,153.578,54.545,32.613 +2020-02-18 22:30:00,100.92,139.555,54.545,32.613 +2020-02-18 22:45:00,102.09,132.065,54.545,32.613 +2020-02-18 23:00:00,97.23,125.461,48.605,32.613 +2020-02-18 23:15:00,94.48,123.948,48.605,32.613 +2020-02-18 23:30:00,89.92,125.03,48.605,32.613 +2020-02-18 23:45:00,91.19,124.929,48.605,32.613 +2020-02-19 00:00:00,90.06,119.571,45.675,32.613 +2020-02-19 00:15:00,88.18,118.331,45.675,32.613 +2020-02-19 00:30:00,88.53,118.399,45.675,32.613 +2020-02-19 00:45:00,85.32,118.94200000000001,45.675,32.613 +2020-02-19 01:00:00,85.6,120.95299999999999,43.015,32.613 +2020-02-19 01:15:00,87.17,121.185,43.015,32.613 +2020-02-19 01:30:00,86.58,121.40700000000001,43.015,32.613 +2020-02-19 01:45:00,82.82,121.579,43.015,32.613 +2020-02-19 02:00:00,84.77,123.601,41.0,32.613 +2020-02-19 02:15:00,86.47,124.98700000000001,41.0,32.613 +2020-02-19 02:30:00,83.73,125.83200000000001,41.0,32.613 +2020-02-19 02:45:00,83.36,127.624,41.0,32.613 +2020-02-19 03:00:00,86.31,130.064,41.318000000000005,32.613 +2020-02-19 03:15:00,87.22,131.672,41.318000000000005,32.613 +2020-02-19 03:30:00,83.52,133.47,41.318000000000005,32.613 +2020-02-19 03:45:00,86.28,134.872,41.318000000000005,32.613 +2020-02-19 04:00:00,81.82,146.582,42.544,32.613 +2020-02-19 04:15:00,86.0,158.291,42.544,32.613 +2020-02-19 04:30:00,91.07,160.819,42.544,32.613 +2020-02-19 04:45:00,93.83,162.994,42.544,32.613 +2020-02-19 05:00:00,95.03,195.505,45.161,32.613 +2020-02-19 05:15:00,92.61,223.41400000000002,45.161,32.613 +2020-02-19 05:30:00,95.38,218.649,45.161,32.613 +2020-02-19 05:45:00,101.64,211.597,45.161,32.613 +2020-02-19 06:00:00,106.45,208.93200000000002,61.86600000000001,32.613 +2020-02-19 06:15:00,110.83,214.862,61.86600000000001,32.613 +2020-02-19 06:30:00,114.29,217.22099999999998,61.86600000000001,32.613 +2020-02-19 06:45:00,118.12,221.122,61.86600000000001,32.613 +2020-02-19 07:00:00,124.11,221.766,77.814,32.613 +2020-02-19 07:15:00,126.22,225.61900000000003,77.814,32.613 +2020-02-19 07:30:00,128.75,226.94299999999998,77.814,32.613 +2020-02-19 07:45:00,127.24,226.606,77.814,32.613 +2020-02-19 08:00:00,130.4,224.96900000000002,70.251,32.613 +2020-02-19 08:15:00,128.37,223.905,70.251,32.613 +2020-02-19 08:30:00,132.94,220.16299999999998,70.251,32.613 +2020-02-19 08:45:00,130.84,216.36900000000003,70.251,32.613 +2020-02-19 09:00:00,133.42,209.21200000000002,66.965,32.613 +2020-02-19 09:15:00,135.01,206.08,66.965,32.613 +2020-02-19 09:30:00,136.59,204.59099999999998,66.965,32.613 +2020-02-19 09:45:00,138.7,201.63400000000001,66.965,32.613 +2020-02-19 10:00:00,134.17,197.37900000000002,63.628,32.613 +2020-02-19 10:15:00,135.78,193.68099999999998,63.628,32.613 +2020-02-19 10:30:00,135.61,191.174,63.628,32.613 +2020-02-19 10:45:00,136.33,190.16,63.628,32.613 +2020-02-19 11:00:00,134.83,188.235,62.516999999999996,32.613 +2020-02-19 11:15:00,132.88,187.34599999999998,62.516999999999996,32.613 +2020-02-19 11:30:00,130.69,186.47299999999998,62.516999999999996,32.613 +2020-02-19 11:45:00,126.99,185.19299999999998,62.516999999999996,32.613 +2020-02-19 12:00:00,126.05,180.234,60.888999999999996,32.613 +2020-02-19 12:15:00,122.3,180.364,60.888999999999996,32.613 +2020-02-19 12:30:00,119.64,179.81799999999998,60.888999999999996,32.613 +2020-02-19 12:45:00,118.58,180.763,60.888999999999996,32.613 +2020-02-19 13:00:00,115.52,179.93,61.57899999999999,32.613 +2020-02-19 13:15:00,116.57,178.705,61.57899999999999,32.613 +2020-02-19 13:30:00,113.94,177.752,61.57899999999999,32.613 +2020-02-19 13:45:00,115.97,177.36,61.57899999999999,32.613 +2020-02-19 14:00:00,114.21,177.94400000000002,62.602,32.613 +2020-02-19 14:15:00,115.49,177.77200000000002,62.602,32.613 +2020-02-19 14:30:00,115.54,178.308,62.602,32.613 +2020-02-19 14:45:00,118.46,179.09900000000002,62.602,32.613 +2020-02-19 15:00:00,119.72,180.96599999999998,64.259,32.613 +2020-02-19 15:15:00,122.91,180.229,64.259,32.613 +2020-02-19 15:30:00,120.97,181.476,64.259,32.613 +2020-02-19 15:45:00,121.61,182.453,64.259,32.613 +2020-02-19 16:00:00,124.31,184.03400000000002,67.632,32.613 +2020-02-19 16:15:00,123.96,185.55599999999998,67.632,32.613 +2020-02-19 16:30:00,123.8,188.396,67.632,32.613 +2020-02-19 16:45:00,124.67,189.734,67.632,32.613 +2020-02-19 17:00:00,129.4,191.702,72.583,32.613 +2020-02-19 17:15:00,130.26,193.933,72.583,32.613 +2020-02-19 17:30:00,133.14,196.37,72.583,32.613 +2020-02-19 17:45:00,133.67,197.252,72.583,32.613 +2020-02-19 18:00:00,138.63,199.46599999999998,72.744,32.613 +2020-02-19 18:15:00,135.29,198.795,72.744,32.613 +2020-02-19 18:30:00,137.36,197.49099999999999,72.744,32.613 +2020-02-19 18:45:00,134.04,198.484,72.744,32.613 +2020-02-19 19:00:00,131.09,197.487,69.684,32.613 +2020-02-19 19:15:00,130.73,194.18,69.684,32.613 +2020-02-19 19:30:00,136.9,192.62099999999998,69.684,32.613 +2020-02-19 19:45:00,137.66,189.549,69.684,32.613 +2020-02-19 20:00:00,126.33,184.882,70.036,32.613 +2020-02-19 20:15:00,117.21,179.354,70.036,32.613 +2020-02-19 20:30:00,116.21,175.517,70.036,32.613 +2020-02-19 20:45:00,114.1,173.687,70.036,32.613 +2020-02-19 21:00:00,108.92,170.518,60.431999999999995,32.613 +2020-02-19 21:15:00,114.41,167.477,60.431999999999995,32.613 +2020-02-19 21:30:00,113.68,165.428,60.431999999999995,32.613 +2020-02-19 21:45:00,113.09,164.775,60.431999999999995,32.613 +2020-02-19 22:00:00,101.66,157.768,56.2,32.613 +2020-02-19 22:15:00,97.67,153.327,56.2,32.613 +2020-02-19 22:30:00,95.3,139.265,56.2,32.613 +2020-02-19 22:45:00,95.0,131.78,56.2,32.613 +2020-02-19 23:00:00,94.34,125.169,47.927,32.613 +2020-02-19 23:15:00,97.09,123.671,47.927,32.613 +2020-02-19 23:30:00,95.13,124.764,47.927,32.613 +2020-02-19 23:45:00,91.62,124.68700000000001,47.927,32.613 +2020-02-20 00:00:00,84.69,119.354,43.794,32.613 +2020-02-20 00:15:00,87.96,118.113,43.794,32.613 +2020-02-20 00:30:00,88.54,118.163,43.794,32.613 +2020-02-20 00:45:00,87.1,118.70299999999999,43.794,32.613 +2020-02-20 01:00:00,80.45,120.679,42.397,32.613 +2020-02-20 01:15:00,79.74,120.897,42.397,32.613 +2020-02-20 01:30:00,79.29,121.10799999999999,42.397,32.613 +2020-02-20 01:45:00,84.73,121.281,42.397,32.613 +2020-02-20 02:00:00,85.65,123.29799999999999,40.010999999999996,32.613 +2020-02-20 02:15:00,86.23,124.682,40.010999999999996,32.613 +2020-02-20 02:30:00,82.4,125.541,40.010999999999996,32.613 +2020-02-20 02:45:00,80.94,127.333,40.010999999999996,32.613 +2020-02-20 03:00:00,86.23,129.78,39.181,32.613 +2020-02-20 03:15:00,87.78,131.382,39.181,32.613 +2020-02-20 03:30:00,86.6,133.175,39.181,32.613 +2020-02-20 03:45:00,82.73,134.589,39.181,32.613 +2020-02-20 04:00:00,84.08,146.3,40.39,32.613 +2020-02-20 04:15:00,82.86,158.0,40.39,32.613 +2020-02-20 04:30:00,83.77,160.543,40.39,32.613 +2020-02-20 04:45:00,86.58,162.708,40.39,32.613 +2020-02-20 05:00:00,97.42,195.2,45.504,32.613 +2020-02-20 05:15:00,99.75,223.127,45.504,32.613 +2020-02-20 05:30:00,97.65,218.333,45.504,32.613 +2020-02-20 05:45:00,102.13,211.28599999999997,45.504,32.613 +2020-02-20 06:00:00,106.74,208.63,57.748000000000005,32.613 +2020-02-20 06:15:00,110.76,214.57,57.748000000000005,32.613 +2020-02-20 06:30:00,114.34,216.894,57.748000000000005,32.613 +2020-02-20 06:45:00,118.21,220.787,57.748000000000005,32.613 +2020-02-20 07:00:00,123.28,221.452,72.138,32.613 +2020-02-20 07:15:00,123.1,225.27700000000002,72.138,32.613 +2020-02-20 07:30:00,125.66,226.56400000000002,72.138,32.613 +2020-02-20 07:45:00,125.06,226.188,72.138,32.613 +2020-02-20 08:00:00,127.31,224.528,65.542,32.613 +2020-02-20 08:15:00,125.33,223.446,65.542,32.613 +2020-02-20 08:30:00,126.93,219.65,65.542,32.613 +2020-02-20 08:45:00,122.41,215.863,65.542,32.613 +2020-02-20 09:00:00,122.95,208.71200000000002,60.523,32.613 +2020-02-20 09:15:00,123.48,205.582,60.523,32.613 +2020-02-20 09:30:00,123.66,204.113,60.523,32.613 +2020-02-20 09:45:00,123.38,201.16299999999998,60.523,32.613 +2020-02-20 10:00:00,123.79,196.918,57.449,32.613 +2020-02-20 10:15:00,122.44,193.252,57.449,32.613 +2020-02-20 10:30:00,120.99,190.75900000000001,57.449,32.613 +2020-02-20 10:45:00,121.04,189.761,57.449,32.613 +2020-02-20 11:00:00,118.26,187.826,54.505,32.613 +2020-02-20 11:15:00,118.28,186.951,54.505,32.613 +2020-02-20 11:30:00,118.21,186.08599999999998,54.505,32.613 +2020-02-20 11:45:00,117.86,184.82,54.505,32.613 +2020-02-20 12:00:00,116.74,179.87599999999998,51.50899999999999,32.613 +2020-02-20 12:15:00,118.18,180.021,51.50899999999999,32.613 +2020-02-20 12:30:00,118.55,179.447,51.50899999999999,32.613 +2020-02-20 12:45:00,118.61,180.388,51.50899999999999,32.613 +2020-02-20 13:00:00,114.9,179.585,51.303999999999995,32.613 +2020-02-20 13:15:00,115.25,178.33700000000002,51.303999999999995,32.613 +2020-02-20 13:30:00,112.8,177.372,51.303999999999995,32.613 +2020-02-20 13:45:00,116.14,176.981,51.303999999999995,32.613 +2020-02-20 14:00:00,111.39,177.622,52.785,32.613 +2020-02-20 14:15:00,115.64,177.429,52.785,32.613 +2020-02-20 14:30:00,114.88,177.945,52.785,32.613 +2020-02-20 14:45:00,116.2,178.748,52.785,32.613 +2020-02-20 15:00:00,118.06,180.623,56.458999999999996,32.613 +2020-02-20 15:15:00,115.92,179.859,56.458999999999996,32.613 +2020-02-20 15:30:00,115.99,181.065,56.458999999999996,32.613 +2020-02-20 15:45:00,117.51,182.02599999999998,56.458999999999996,32.613 +2020-02-20 16:00:00,117.85,183.607,59.388000000000005,32.613 +2020-02-20 16:15:00,118.3,185.11700000000002,59.388000000000005,32.613 +2020-02-20 16:30:00,122.42,187.958,59.388000000000005,32.613 +2020-02-20 16:45:00,122.43,189.268,59.388000000000005,32.613 +2020-02-20 17:00:00,124.49,191.24099999999999,64.462,32.613 +2020-02-20 17:15:00,125.36,193.489,64.462,32.613 +2020-02-20 17:30:00,131.6,195.954,64.462,32.613 +2020-02-20 17:45:00,131.89,196.857,64.462,32.613 +2020-02-20 18:00:00,138.28,199.081,65.128,32.613 +2020-02-20 18:15:00,136.37,198.468,65.128,32.613 +2020-02-20 18:30:00,138.13,197.16400000000002,65.128,32.613 +2020-02-20 18:45:00,136.19,198.179,65.128,32.613 +2020-02-20 19:00:00,132.24,197.143,61.316,32.613 +2020-02-20 19:15:00,136.06,193.85,61.316,32.613 +2020-02-20 19:30:00,140.35,192.31599999999997,61.316,32.613 +2020-02-20 19:45:00,136.18,189.28099999999998,61.316,32.613 +2020-02-20 20:00:00,124.63,184.584,59.845,32.613 +2020-02-20 20:15:00,117.47,179.071,59.845,32.613 +2020-02-20 20:30:00,115.88,175.25,59.845,32.613 +2020-02-20 20:45:00,113.56,173.425,59.845,32.613 +2020-02-20 21:00:00,109.82,170.24099999999999,54.83,32.613 +2020-02-20 21:15:00,107.49,167.19099999999997,54.83,32.613 +2020-02-20 21:30:00,105.41,165.142,54.83,32.613 +2020-02-20 21:45:00,107.31,164.50900000000001,54.83,32.613 +2020-02-20 22:00:00,98.64,157.486,50.933,32.613 +2020-02-20 22:15:00,102.93,153.07,50.933,32.613 +2020-02-20 22:30:00,103.82,138.967,50.933,32.613 +2020-02-20 22:45:00,105.37,131.486,50.933,32.613 +2020-02-20 23:00:00,94.47,124.87,45.32899999999999,32.613 +2020-02-20 23:15:00,90.46,123.387,45.32899999999999,32.613 +2020-02-20 23:30:00,88.59,124.492,45.32899999999999,32.613 +2020-02-20 23:45:00,88.87,124.436,45.32899999999999,32.613 +2020-02-21 00:00:00,84.07,118.053,43.74,32.613 +2020-02-21 00:15:00,90.99,117.012,43.74,32.613 +2020-02-21 00:30:00,90.98,116.915,43.74,32.613 +2020-02-21 00:45:00,87.5,117.566,43.74,32.613 +2020-02-21 01:00:00,80.73,119.194,42.555,32.613 +2020-02-21 01:15:00,82.69,120.294,42.555,32.613 +2020-02-21 01:30:00,78.88,120.279,42.555,32.613 +2020-02-21 01:45:00,79.85,120.557,42.555,32.613 +2020-02-21 02:00:00,79.02,122.663,41.68600000000001,32.613 +2020-02-21 02:15:00,86.13,123.93,41.68600000000001,32.613 +2020-02-21 02:30:00,87.4,125.331,41.68600000000001,32.613 +2020-02-21 02:45:00,87.01,127.15700000000001,41.68600000000001,32.613 +2020-02-21 03:00:00,80.72,128.641,42.278999999999996,32.613 +2020-02-21 03:15:00,80.62,131.14600000000002,42.278999999999996,32.613 +2020-02-21 03:30:00,82.45,132.915,42.278999999999996,32.613 +2020-02-21 03:45:00,82.52,134.672,42.278999999999996,32.613 +2020-02-21 04:00:00,83.5,146.612,43.742,32.613 +2020-02-21 04:15:00,88.63,158.04399999999998,43.742,32.613 +2020-02-21 04:30:00,94.41,160.84,43.742,32.613 +2020-02-21 04:45:00,95.37,161.859,43.742,32.613 +2020-02-21 05:00:00,94.16,193.079,46.973,32.613 +2020-02-21 05:15:00,92.16,222.521,46.973,32.613 +2020-02-21 05:30:00,94.26,218.762,46.973,32.613 +2020-02-21 05:45:00,99.52,211.65099999999998,46.973,32.613 +2020-02-21 06:00:00,108.6,209.43200000000002,59.63399999999999,32.613 +2020-02-21 06:15:00,111.42,213.935,59.63399999999999,32.613 +2020-02-21 06:30:00,115.46,215.37400000000002,59.63399999999999,32.613 +2020-02-21 06:45:00,117.72,220.855,59.63399999999999,32.613 +2020-02-21 07:00:00,122.43,220.72799999999998,71.631,32.613 +2020-02-21 07:15:00,123.45,225.54,71.631,32.613 +2020-02-21 07:30:00,123.07,226.58900000000003,71.631,32.613 +2020-02-21 07:45:00,124.6,225.299,71.631,32.613 +2020-02-21 08:00:00,127.22,222.543,66.181,32.613 +2020-02-21 08:15:00,125.37,221.072,66.181,32.613 +2020-02-21 08:30:00,126.6,218.17700000000002,66.181,32.613 +2020-02-21 08:45:00,123.25,212.84599999999998,66.181,32.613 +2020-02-21 09:00:00,125.59,206.114,63.086000000000006,32.613 +2020-02-21 09:15:00,125.25,203.579,63.086000000000006,32.613 +2020-02-21 09:30:00,126.21,201.69799999999998,63.086000000000006,32.613 +2020-02-21 09:45:00,128.09,198.653,63.086000000000006,32.613 +2020-02-21 10:00:00,122.61,193.30700000000002,60.886,32.613 +2020-02-21 10:15:00,124.67,190.35299999999998,60.886,32.613 +2020-02-21 10:30:00,125.15,187.81599999999997,60.886,32.613 +2020-02-21 10:45:00,124.81,186.39700000000002,60.886,32.613 +2020-02-21 11:00:00,120.18,184.442,59.391000000000005,32.613 +2020-02-21 11:15:00,120.77,182.68200000000002,59.391000000000005,32.613 +2020-02-21 11:30:00,120.33,183.52599999999998,59.391000000000005,32.613 +2020-02-21 11:45:00,120.04,182.298,59.391000000000005,32.613 +2020-02-21 12:00:00,118.73,178.43900000000002,56.172,32.613 +2020-02-21 12:15:00,118.32,176.53799999999998,56.172,32.613 +2020-02-21 12:30:00,117.18,176.092,56.172,32.613 +2020-02-21 12:45:00,119.34,177.515,56.172,32.613 +2020-02-21 13:00:00,112.77,177.671,54.406000000000006,32.613 +2020-02-21 13:15:00,112.5,177.203,54.406000000000006,32.613 +2020-02-21 13:30:00,111.04,176.26,54.406000000000006,32.613 +2020-02-21 13:45:00,111.47,175.815,54.406000000000006,32.613 +2020-02-21 14:00:00,109.79,175.38299999999998,53.578,32.613 +2020-02-21 14:15:00,110.7,175.00599999999997,53.578,32.613 +2020-02-21 14:30:00,110.76,176.04,53.578,32.613 +2020-02-21 14:45:00,110.95,177.13099999999997,53.578,32.613 +2020-02-21 15:00:00,117.42,178.542,56.568999999999996,32.613 +2020-02-21 15:15:00,120.19,177.31599999999997,56.568999999999996,32.613 +2020-02-21 15:30:00,119.17,176.987,56.568999999999996,32.613 +2020-02-21 15:45:00,115.35,178.092,56.568999999999996,32.613 +2020-02-21 16:00:00,117.52,178.511,60.169,32.613 +2020-02-21 16:15:00,115.01,180.301,60.169,32.613 +2020-02-21 16:30:00,116.7,183.232,60.169,32.613 +2020-02-21 16:45:00,119.87,184.386,60.169,32.613 +2020-02-21 17:00:00,123.86,186.592,65.497,32.613 +2020-02-21 17:15:00,126.26,188.45,65.497,32.613 +2020-02-21 17:30:00,126.38,190.63400000000001,65.497,32.613 +2020-02-21 17:45:00,128.7,191.324,65.497,32.613 +2020-02-21 18:00:00,133.63,194.234,65.082,32.613 +2020-02-21 18:15:00,132.24,193.282,65.082,32.613 +2020-02-21 18:30:00,135.09,192.361,65.082,32.613 +2020-02-21 18:45:00,134.34,193.4,65.082,32.613 +2020-02-21 19:00:00,132.35,193.229,60.968,32.613 +2020-02-21 19:15:00,136.76,191.30200000000002,60.968,32.613 +2020-02-21 19:30:00,136.33,189.387,60.968,32.613 +2020-02-21 19:45:00,136.31,185.921,60.968,32.613 +2020-02-21 20:00:00,121.81,181.247,61.123000000000005,32.613 +2020-02-21 20:15:00,119.79,175.767,61.123000000000005,32.613 +2020-02-21 20:30:00,114.65,171.91400000000002,61.123000000000005,32.613 +2020-02-21 20:45:00,115.4,170.627,61.123000000000005,32.613 +2020-02-21 21:00:00,106.52,167.949,55.416000000000004,32.613 +2020-02-21 21:15:00,104.0,165.347,55.416000000000004,32.613 +2020-02-21 21:30:00,102.97,163.342,55.416000000000004,32.613 +2020-02-21 21:45:00,103.72,163.263,55.416000000000004,32.613 +2020-02-21 22:00:00,97.94,157.187,51.631,32.613 +2020-02-21 22:15:00,94.54,152.662,51.631,32.613 +2020-02-21 22:30:00,92.36,144.856,51.631,32.613 +2020-02-21 22:45:00,96.36,140.861,51.631,32.613 +2020-02-21 23:00:00,96.29,133.83,44.898,32.613 +2020-02-21 23:15:00,93.93,130.423,44.898,32.613 +2020-02-21 23:30:00,87.56,130.07399999999998,44.898,32.613 +2020-02-21 23:45:00,86.86,129.387,44.898,32.613 +2020-02-22 00:00:00,80.69,115.046,42.033,32.431999999999995 +2020-02-22 00:15:00,85.92,109.822,42.033,32.431999999999995 +2020-02-22 00:30:00,86.31,111.001,42.033,32.431999999999995 +2020-02-22 00:45:00,83.99,112.337,42.033,32.431999999999995 +2020-02-22 01:00:00,75.49,114.553,38.255,32.431999999999995 +2020-02-22 01:15:00,77.66,114.685,38.255,32.431999999999995 +2020-02-22 01:30:00,74.25,114.135,38.255,32.431999999999995 +2020-02-22 01:45:00,78.27,114.225,38.255,32.431999999999995 +2020-02-22 02:00:00,80.13,116.98200000000001,36.404,32.431999999999995 +2020-02-22 02:15:00,81.83,117.868,36.404,32.431999999999995 +2020-02-22 02:30:00,77.76,118.184,36.404,32.431999999999995 +2020-02-22 02:45:00,77.53,120.12299999999999,36.404,32.431999999999995 +2020-02-22 03:00:00,76.75,122.20700000000001,36.083,32.431999999999995 +2020-02-22 03:15:00,80.16,123.5,36.083,32.431999999999995 +2020-02-22 03:30:00,80.23,123.714,36.083,32.431999999999995 +2020-02-22 03:45:00,78.0,125.616,36.083,32.431999999999995 +2020-02-22 04:00:00,72.92,133.475,36.102,32.431999999999995 +2020-02-22 04:15:00,73.17,142.395,36.102,32.431999999999995 +2020-02-22 04:30:00,73.24,143.054,36.102,32.431999999999995 +2020-02-22 04:45:00,74.17,143.582,36.102,32.431999999999995 +2020-02-22 05:00:00,74.51,159.02200000000002,35.284,32.431999999999995 +2020-02-22 05:15:00,75.89,169.7,35.284,32.431999999999995 +2020-02-22 05:30:00,75.3,166.28599999999997,35.284,32.431999999999995 +2020-02-22 05:45:00,76.79,164.58599999999998,35.284,32.431999999999995 +2020-02-22 06:00:00,78.47,180.94,36.265,32.431999999999995 +2020-02-22 06:15:00,79.02,201.58900000000003,36.265,32.431999999999995 +2020-02-22 06:30:00,79.89,197.68200000000002,36.265,32.431999999999995 +2020-02-22 06:45:00,80.02,194.308,36.265,32.431999999999995 +2020-02-22 07:00:00,81.54,190.49099999999999,40.714,32.431999999999995 +2020-02-22 07:15:00,85.98,194.023,40.714,32.431999999999995 +2020-02-22 07:30:00,86.18,197.71,40.714,32.431999999999995 +2020-02-22 07:45:00,87.86,200.28400000000002,40.714,32.431999999999995 +2020-02-22 08:00:00,89.81,201.467,46.692,32.431999999999995 +2020-02-22 08:15:00,91.73,203.52200000000002,46.692,32.431999999999995 +2020-02-22 08:30:00,98.01,202.149,46.692,32.431999999999995 +2020-02-22 08:45:00,99.24,199.93,46.692,32.431999999999995 +2020-02-22 09:00:00,102.43,194.99599999999998,48.925,32.431999999999995 +2020-02-22 09:15:00,102.12,193.207,48.925,32.431999999999995 +2020-02-22 09:30:00,102.49,192.239,48.925,32.431999999999995 +2020-02-22 09:45:00,99.78,189.347,48.925,32.431999999999995 +2020-02-22 10:00:00,95.56,184.31099999999998,47.799,32.431999999999995 +2020-02-22 10:15:00,99.82,181.56099999999998,47.799,32.431999999999995 +2020-02-22 10:30:00,100.4,179.19,47.799,32.431999999999995 +2020-02-22 10:45:00,99.55,179.048,47.799,32.431999999999995 +2020-02-22 11:00:00,95.77,177.29,44.309,32.431999999999995 +2020-02-22 11:15:00,95.07,174.896,44.309,32.431999999999995 +2020-02-22 11:30:00,94.71,174.668,44.309,32.431999999999995 +2020-02-22 11:45:00,97.76,172.54,44.309,32.431999999999995 +2020-02-22 12:00:00,92.83,167.857,42.367,32.431999999999995 +2020-02-22 12:15:00,92.41,166.61599999999999,42.367,32.431999999999995 +2020-02-22 12:30:00,88.04,166.44400000000002,42.367,32.431999999999995 +2020-02-22 12:45:00,86.69,167.13400000000001,42.367,32.431999999999995 +2020-02-22 13:00:00,82.02,166.915,39.036,32.431999999999995 +2020-02-22 13:15:00,85.74,164.417,39.036,32.431999999999995 +2020-02-22 13:30:00,79.5,163.02700000000002,39.036,32.431999999999995 +2020-02-22 13:45:00,80.82,163.011,39.036,32.431999999999995 +2020-02-22 14:00:00,78.34,163.83100000000002,37.995,32.431999999999995 +2020-02-22 14:15:00,78.85,162.882,37.995,32.431999999999995 +2020-02-22 14:30:00,80.14,162.118,37.995,32.431999999999995 +2020-02-22 14:45:00,81.7,163.444,37.995,32.431999999999995 +2020-02-22 15:00:00,81.24,165.505,40.71,32.431999999999995 +2020-02-22 15:15:00,82.52,165.051,40.71,32.431999999999995 +2020-02-22 15:30:00,83.61,166.18200000000002,40.71,32.431999999999995 +2020-02-22 15:45:00,85.83,167.262,40.71,32.431999999999995 +2020-02-22 16:00:00,87.34,166.548,46.998000000000005,32.431999999999995 +2020-02-22 16:15:00,91.33,169.175,46.998000000000005,32.431999999999995 +2020-02-22 16:30:00,93.53,172.06,46.998000000000005,32.431999999999995 +2020-02-22 16:45:00,92.11,174.048,46.998000000000005,32.431999999999995 +2020-02-22 17:00:00,99.16,175.66,55.431000000000004,32.431999999999995 +2020-02-22 17:15:00,98.14,179.105,55.431000000000004,32.431999999999995 +2020-02-22 17:30:00,103.35,181.222,55.431000000000004,32.431999999999995 +2020-02-22 17:45:00,104.88,181.547,55.431000000000004,32.431999999999995 +2020-02-22 18:00:00,113.78,184.007,55.989,32.431999999999995 +2020-02-22 18:15:00,112.69,184.90400000000002,55.989,32.431999999999995 +2020-02-22 18:30:00,112.95,185.31,55.989,32.431999999999995 +2020-02-22 18:45:00,112.81,183.035,55.989,32.431999999999995 +2020-02-22 19:00:00,111.89,183.692,50.882,32.431999999999995 +2020-02-22 19:15:00,112.4,181.27599999999998,50.882,32.431999999999995 +2020-02-22 19:30:00,107.62,180.136,50.882,32.431999999999995 +2020-02-22 19:45:00,106.47,176.567,50.882,32.431999999999995 +2020-02-22 20:00:00,101.54,174.00799999999998,43.172,32.431999999999995 +2020-02-22 20:15:00,99.49,170.608,43.172,32.431999999999995 +2020-02-22 20:30:00,96.48,166.41400000000002,43.172,32.431999999999995 +2020-02-22 20:45:00,98.36,164.771,43.172,32.431999999999995 +2020-02-22 21:00:00,91.47,164.25099999999998,37.599000000000004,32.431999999999995 +2020-02-22 21:15:00,90.24,162.063,37.599000000000004,32.431999999999995 +2020-02-22 21:30:00,88.52,161.263,37.599000000000004,32.431999999999995 +2020-02-22 21:45:00,88.73,160.796,37.599000000000004,32.431999999999995 +2020-02-22 22:00:00,85.26,156.02100000000002,39.047,32.431999999999995 +2020-02-22 22:15:00,84.4,153.955,39.047,32.431999999999995 +2020-02-22 22:30:00,81.79,152.175,39.047,32.431999999999995 +2020-02-22 22:45:00,81.3,150.017,39.047,32.431999999999995 +2020-02-22 23:00:00,77.58,145.344,32.339,32.431999999999995 +2020-02-22 23:15:00,77.79,140.363,32.339,32.431999999999995 +2020-02-22 23:30:00,74.96,138.365,32.339,32.431999999999995 +2020-02-22 23:45:00,73.63,135.339,32.339,32.431999999999995 +2020-02-23 00:00:00,70.14,115.244,29.988000000000003,32.431999999999995 +2020-02-23 00:15:00,69.61,109.686,29.988000000000003,32.431999999999995 +2020-02-23 00:30:00,69.59,110.479,29.988000000000003,32.431999999999995 +2020-02-23 00:45:00,69.38,112.488,29.988000000000003,32.431999999999995 +2020-02-23 01:00:00,65.46,114.53399999999999,28.531999999999996,32.431999999999995 +2020-02-23 01:15:00,66.66,115.661,28.531999999999996,32.431999999999995 +2020-02-23 01:30:00,65.52,115.603,28.531999999999996,32.431999999999995 +2020-02-23 01:45:00,65.26,115.382,28.531999999999996,32.431999999999995 +2020-02-23 02:00:00,63.83,117.405,27.805999999999997,32.431999999999995 +2020-02-23 02:15:00,64.64,117.473,27.805999999999997,32.431999999999995 +2020-02-23 02:30:00,63.77,118.64200000000001,27.805999999999997,32.431999999999995 +2020-02-23 02:45:00,64.98,121.022,27.805999999999997,32.431999999999995 +2020-02-23 03:00:00,63.94,123.436,26.193,32.431999999999995 +2020-02-23 03:15:00,64.7,124.21799999999999,26.193,32.431999999999995 +2020-02-23 03:30:00,64.1,125.774,26.193,32.431999999999995 +2020-02-23 03:45:00,64.57,127.594,26.193,32.431999999999995 +2020-02-23 04:00:00,64.91,135.216,27.19,32.431999999999995 +2020-02-23 04:15:00,64.97,143.135,27.19,32.431999999999995 +2020-02-23 04:30:00,65.65,143.904,27.19,32.431999999999995 +2020-02-23 04:45:00,66.48,144.673,27.19,32.431999999999995 +2020-02-23 05:00:00,67.26,156.69,28.166999999999998,32.431999999999995 +2020-02-23 05:15:00,67.76,165.021,28.166999999999998,32.431999999999995 +2020-02-23 05:30:00,68.15,161.405,28.166999999999998,32.431999999999995 +2020-02-23 05:45:00,68.8,159.939,28.166999999999998,32.431999999999995 +2020-02-23 06:00:00,69.41,176.139,27.16,32.431999999999995 +2020-02-23 06:15:00,69.86,195.146,27.16,32.431999999999995 +2020-02-23 06:30:00,70.71,190.112,27.16,32.431999999999995 +2020-02-23 06:45:00,69.95,185.675,27.16,32.431999999999995 +2020-02-23 07:00:00,72.62,184.268,29.578000000000003,32.431999999999995 +2020-02-23 07:15:00,73.12,186.9,29.578000000000003,32.431999999999995 +2020-02-23 07:30:00,74.51,189.389,29.578000000000003,32.431999999999995 +2020-02-23 07:45:00,76.29,191.18200000000002,29.578000000000003,32.431999999999995 +2020-02-23 08:00:00,78.45,194.149,34.650999999999996,32.431999999999995 +2020-02-23 08:15:00,78.33,196.13,34.650999999999996,32.431999999999995 +2020-02-23 08:30:00,78.73,196.333,34.650999999999996,32.431999999999995 +2020-02-23 08:45:00,79.07,196.078,34.650999999999996,32.431999999999995 +2020-02-23 09:00:00,78.69,190.75900000000001,38.080999999999996,32.431999999999995 +2020-02-23 09:15:00,78.31,189.495,38.080999999999996,32.431999999999995 +2020-02-23 09:30:00,78.05,188.407,38.080999999999996,32.431999999999995 +2020-02-23 09:45:00,77.75,185.43599999999998,38.080999999999996,32.431999999999995 +2020-02-23 10:00:00,76.21,182.868,39.934,32.431999999999995 +2020-02-23 10:15:00,75.97,180.67,39.934,32.431999999999995 +2020-02-23 10:30:00,75.82,178.90400000000002,39.934,32.431999999999995 +2020-02-23 10:45:00,76.78,176.976,39.934,32.431999999999995 +2020-02-23 11:00:00,77.47,176.084,43.74100000000001,32.431999999999995 +2020-02-23 11:15:00,80.97,173.822,43.74100000000001,32.431999999999995 +2020-02-23 11:30:00,81.83,172.77700000000002,43.74100000000001,32.431999999999995 +2020-02-23 11:45:00,84.37,171.255,43.74100000000001,32.431999999999995 +2020-02-23 12:00:00,80.33,166.08,40.001999999999995,32.431999999999995 +2020-02-23 12:15:00,75.8,166.668,40.001999999999995,32.431999999999995 +2020-02-23 12:30:00,75.85,165.06900000000002,40.001999999999995,32.431999999999995 +2020-02-23 12:45:00,72.42,164.801,40.001999999999995,32.431999999999995 +2020-02-23 13:00:00,71.1,163.916,37.855,32.431999999999995 +2020-02-23 13:15:00,69.74,164.28099999999998,37.855,32.431999999999995 +2020-02-23 13:30:00,66.49,162.636,37.855,32.431999999999995 +2020-02-23 13:45:00,65.1,162.02700000000002,37.855,32.431999999999995 +2020-02-23 14:00:00,63.85,163.18200000000002,35.946999999999996,32.431999999999995 +2020-02-23 14:15:00,67.63,163.381,35.946999999999996,32.431999999999995 +2020-02-23 14:30:00,65.18,163.741,35.946999999999996,32.431999999999995 +2020-02-23 14:45:00,66.34,164.646,35.946999999999996,32.431999999999995 +2020-02-23 15:00:00,67.38,165.24599999999998,35.138000000000005,32.431999999999995 +2020-02-23 15:15:00,71.0,165.46400000000003,35.138000000000005,32.431999999999995 +2020-02-23 15:30:00,70.06,167.115,35.138000000000005,32.431999999999995 +2020-02-23 15:45:00,71.4,168.856,35.138000000000005,32.431999999999995 +2020-02-23 16:00:00,77.36,169.905,38.672,32.431999999999995 +2020-02-23 16:15:00,75.53,171.645,38.672,32.431999999999995 +2020-02-23 16:30:00,77.71,174.812,38.672,32.431999999999995 +2020-02-23 16:45:00,81.19,176.903,38.672,32.431999999999995 +2020-02-23 17:00:00,88.54,178.53599999999997,48.684,32.431999999999995 +2020-02-23 17:15:00,86.32,181.71900000000002,48.684,32.431999999999995 +2020-02-23 17:30:00,90.78,184.165,48.684,32.431999999999995 +2020-02-23 17:45:00,96.64,186.72099999999998,48.684,32.431999999999995 +2020-02-23 18:00:00,104.83,188.683,51.568999999999996,32.431999999999995 +2020-02-23 18:15:00,107.06,190.919,51.568999999999996,32.431999999999995 +2020-02-23 18:30:00,107.23,189.291,51.568999999999996,32.431999999999995 +2020-02-23 18:45:00,103.85,188.84,51.568999999999996,32.431999999999995 +2020-02-23 19:00:00,101.61,189.18900000000002,48.608000000000004,32.431999999999995 +2020-02-23 19:15:00,102.96,187.34,48.608000000000004,32.431999999999995 +2020-02-23 19:30:00,99.45,186.058,48.608000000000004,32.431999999999995 +2020-02-23 19:45:00,100.59,183.923,48.608000000000004,32.431999999999995 +2020-02-23 20:00:00,105.69,181.30900000000003,43.733999999999995,32.431999999999995 +2020-02-23 20:15:00,97.93,178.87599999999998,43.733999999999995,32.431999999999995 +2020-02-23 20:30:00,93.18,175.926,43.733999999999995,32.431999999999995 +2020-02-23 20:45:00,98.42,173.078,43.733999999999995,32.431999999999995 +2020-02-23 21:00:00,92.64,170.021,39.283,32.431999999999995 +2020-02-23 21:15:00,88.96,167.207,39.283,32.431999999999995 +2020-02-23 21:30:00,90.04,166.68,39.283,32.431999999999995 +2020-02-23 21:45:00,93.24,166.37099999999998,39.283,32.431999999999995 +2020-02-23 22:00:00,98.28,160.47,40.111,32.431999999999995 +2020-02-23 22:15:00,92.33,157.649,40.111,32.431999999999995 +2020-02-23 22:30:00,94.51,152.731,40.111,32.431999999999995 +2020-02-23 22:45:00,91.38,149.721,40.111,32.431999999999995 +2020-02-23 23:00:00,86.18,142.312,35.791,32.431999999999995 +2020-02-23 23:15:00,83.82,139.184,35.791,32.431999999999995 +2020-02-23 23:30:00,84.12,137.974,35.791,32.431999999999995 +2020-02-23 23:45:00,89.48,135.843,35.791,32.431999999999995 +2020-02-24 00:00:00,87.4,119.12100000000001,34.311,32.613 +2020-02-24 00:15:00,85.71,116.48200000000001,34.311,32.613 +2020-02-24 00:30:00,79.25,117.37,34.311,32.613 +2020-02-24 00:45:00,80.11,118.845,34.311,32.613 +2020-02-24 01:00:00,75.53,120.87299999999999,34.585,32.613 +2020-02-24 01:15:00,82.25,121.47399999999999,34.585,32.613 +2020-02-24 01:30:00,82.39,121.463,34.585,32.613 +2020-02-24 01:45:00,83.21,121.355,34.585,32.613 +2020-02-24 02:00:00,78.47,123.35799999999999,34.111,32.613 +2020-02-24 02:15:00,81.62,124.824,34.111,32.613 +2020-02-24 02:30:00,83.49,126.336,34.111,32.613 +2020-02-24 02:45:00,85.86,128.109,34.111,32.613 +2020-02-24 03:00:00,77.68,131.778,32.435,32.613 +2020-02-24 03:15:00,84.84,134.17,32.435,32.613 +2020-02-24 03:30:00,85.17,135.471,32.435,32.613 +2020-02-24 03:45:00,85.91,136.755,32.435,32.613 +2020-02-24 04:00:00,83.63,148.685,33.04,32.613 +2020-02-24 04:15:00,83.89,160.695,33.04,32.613 +2020-02-24 04:30:00,89.11,163.607,33.04,32.613 +2020-02-24 04:45:00,86.29,164.52900000000002,33.04,32.613 +2020-02-24 05:00:00,90.81,192.02599999999998,40.399,32.613 +2020-02-24 05:15:00,91.34,220.23,40.399,32.613 +2020-02-24 05:30:00,95.51,216.81900000000002,40.399,32.613 +2020-02-24 05:45:00,100.68,209.787,40.399,32.613 +2020-02-24 06:00:00,112.19,208.31799999999998,60.226000000000006,32.613 +2020-02-24 06:15:00,114.53,212.699,60.226000000000006,32.613 +2020-02-24 06:30:00,121.55,215.514,60.226000000000006,32.613 +2020-02-24 06:45:00,120.58,219.74099999999999,60.226000000000006,32.613 +2020-02-24 07:00:00,127.09,220.673,73.578,32.613 +2020-02-24 07:15:00,128.48,224.535,73.578,32.613 +2020-02-24 07:30:00,128.83,226.213,73.578,32.613 +2020-02-24 07:45:00,128.43,225.47400000000002,73.578,32.613 +2020-02-24 08:00:00,132.65,223.62099999999998,66.58,32.613 +2020-02-24 08:15:00,130.35,223.472,66.58,32.613 +2020-02-24 08:30:00,132.82,219.62400000000002,66.58,32.613 +2020-02-24 08:45:00,128.9,216.138,66.58,32.613 +2020-02-24 09:00:00,130.95,209.83700000000002,62.0,32.613 +2020-02-24 09:15:00,129.17,205.146,62.0,32.613 +2020-02-24 09:30:00,128.34,203.085,62.0,32.613 +2020-02-24 09:45:00,127.77,200.359,62.0,32.613 +2020-02-24 10:00:00,125.53,196.74900000000002,59.099,32.613 +2020-02-24 10:15:00,127.37,194.266,59.099,32.613 +2020-02-24 10:30:00,123.36,191.65599999999998,59.099,32.613 +2020-02-24 10:45:00,127.17,190.43200000000002,59.099,32.613 +2020-02-24 11:00:00,122.95,187.0,57.729,32.613 +2020-02-24 11:15:00,126.73,186.50900000000001,57.729,32.613 +2020-02-24 11:30:00,127.56,186.827,57.729,32.613 +2020-02-24 11:45:00,123.32,184.93599999999998,57.729,32.613 +2020-02-24 12:00:00,122.5,181.40099999999998,55.615,32.613 +2020-02-24 12:15:00,126.78,182.00599999999997,55.615,32.613 +2020-02-24 12:30:00,127.67,180.593,55.615,32.613 +2020-02-24 12:45:00,124.86,181.798,55.615,32.613 +2020-02-24 13:00:00,122.88,181.528,56.515,32.613 +2020-02-24 13:15:00,123.82,180.50599999999997,56.515,32.613 +2020-02-24 13:30:00,125.65,178.34,56.515,32.613 +2020-02-24 13:45:00,128.04,177.78599999999997,56.515,32.613 +2020-02-24 14:00:00,130.27,178.38099999999997,58.1,32.613 +2020-02-24 14:15:00,128.28,177.951,58.1,32.613 +2020-02-24 14:30:00,131.05,177.762,58.1,32.613 +2020-02-24 14:45:00,130.12,178.701,58.1,32.613 +2020-02-24 15:00:00,130.46,181.032,59.801,32.613 +2020-02-24 15:15:00,131.88,179.815,59.801,32.613 +2020-02-24 15:30:00,129.22,180.639,59.801,32.613 +2020-02-24 15:45:00,129.42,181.919,59.801,32.613 +2020-02-24 16:00:00,129.55,183.18599999999998,62.901,32.613 +2020-02-24 16:15:00,129.72,184.172,62.901,32.613 +2020-02-24 16:30:00,127.89,186.385,62.901,32.613 +2020-02-24 16:45:00,129.96,187.296,62.901,32.613 +2020-02-24 17:00:00,133.58,188.75,70.418,32.613 +2020-02-24 17:15:00,136.65,191.028,70.418,32.613 +2020-02-24 17:30:00,139.14,192.96,70.418,32.613 +2020-02-24 17:45:00,138.19,194.074,70.418,32.613 +2020-02-24 18:00:00,144.78,196.45,71.726,32.613 +2020-02-24 18:15:00,140.22,196.56,71.726,32.613 +2020-02-24 18:30:00,140.06,195.55599999999998,71.726,32.613 +2020-02-24 18:45:00,141.43,195.878,71.726,32.613 +2020-02-24 19:00:00,139.29,194.61599999999999,65.997,32.613 +2020-02-24 19:15:00,138.24,191.66,65.997,32.613 +2020-02-24 19:30:00,144.32,190.885,65.997,32.613 +2020-02-24 19:45:00,141.12,187.96599999999998,65.997,32.613 +2020-02-24 20:00:00,133.01,182.99099999999999,68.09100000000001,32.613 +2020-02-24 20:15:00,125.91,178.175,68.09100000000001,32.613 +2020-02-24 20:30:00,120.23,173.433,68.09100000000001,32.613 +2020-02-24 20:45:00,121.43,172.179,68.09100000000001,32.613 +2020-02-24 21:00:00,114.0,169.61900000000003,59.617,32.613 +2020-02-24 21:15:00,117.58,165.65599999999998,59.617,32.613 +2020-02-24 21:30:00,117.76,164.327,59.617,32.613 +2020-02-24 21:45:00,115.93,163.54,59.617,32.613 +2020-02-24 22:00:00,105.83,154.77,54.938,32.613 +2020-02-24 22:15:00,101.05,150.715,54.938,32.613 +2020-02-24 22:30:00,99.67,136.342,54.938,32.613 +2020-02-24 22:45:00,98.93,128.609,54.938,32.613 +2020-02-24 23:00:00,95.34,121.956,47.43,32.613 +2020-02-24 23:15:00,94.94,121.412,47.43,32.613 +2020-02-24 23:30:00,89.82,122.914,47.43,32.613 +2020-02-24 23:45:00,89.72,123.382,47.43,32.613 +2020-02-25 00:00:00,92.74,118.161,48.354,32.613 +2020-02-25 00:15:00,93.6,116.93299999999999,48.354,32.613 +2020-02-25 00:30:00,94.11,116.89200000000001,48.354,32.613 +2020-02-25 00:45:00,89.59,117.417,48.354,32.613 +2020-02-25 01:00:00,84.84,119.20700000000001,45.68600000000001,32.613 +2020-02-25 01:15:00,86.12,119.359,45.68600000000001,32.613 +2020-02-25 01:30:00,82.8,119.501,45.68600000000001,32.613 +2020-02-25 01:45:00,83.08,119.69,45.68600000000001,32.613 +2020-02-25 02:00:00,84.51,121.675,44.269,32.613 +2020-02-25 02:15:00,87.23,123.042,44.269,32.613 +2020-02-25 02:30:00,89.0,123.98200000000001,44.269,32.613 +2020-02-25 02:45:00,93.75,125.771,44.269,32.613 +2020-02-25 03:00:00,88.41,128.25799999999998,44.187,32.613 +2020-02-25 03:15:00,89.23,129.819,44.187,32.613 +2020-02-25 03:30:00,85.06,131.588,44.187,32.613 +2020-02-25 03:45:00,89.04,133.059,44.187,32.613 +2020-02-25 04:00:00,86.27,144.786,46.126999999999995,32.613 +2020-02-25 04:15:00,86.74,156.446,46.126999999999995,32.613 +2020-02-25 04:30:00,88.33,159.06799999999998,46.126999999999995,32.613 +2020-02-25 04:45:00,91.62,161.172,46.126999999999995,32.613 +2020-02-25 05:00:00,97.3,193.585,49.666000000000004,32.613 +2020-02-25 05:15:00,96.94,221.614,49.666000000000004,32.613 +2020-02-25 05:30:00,101.12,216.666,49.666000000000004,32.613 +2020-02-25 05:45:00,105.3,209.638,49.666000000000004,32.613 +2020-02-25 06:00:00,116.13,207.021,61.077,32.613 +2020-02-25 06:15:00,118.32,213.012,61.077,32.613 +2020-02-25 06:30:00,121.9,215.149,61.077,32.613 +2020-02-25 06:45:00,123.41,218.986,61.077,32.613 +2020-02-25 07:00:00,130.25,219.761,74.717,32.613 +2020-02-25 07:15:00,129.83,223.43400000000003,74.717,32.613 +2020-02-25 07:30:00,131.97,224.53900000000002,74.717,32.613 +2020-02-25 07:45:00,129.74,223.96,74.717,32.613 +2020-02-25 08:00:00,132.69,222.18900000000002,69.033,32.613 +2020-02-25 08:15:00,133.52,221.015,69.033,32.613 +2020-02-25 08:30:00,129.26,216.93599999999998,69.033,32.613 +2020-02-25 08:45:00,128.0,213.18900000000002,69.033,32.613 +2020-02-25 09:00:00,134.63,206.084,63.113,32.613 +2020-02-25 09:15:00,134.74,202.96200000000002,63.113,32.613 +2020-02-25 09:30:00,137.02,201.59400000000002,63.113,32.613 +2020-02-25 09:45:00,135.12,198.675,63.113,32.613 +2020-02-25 10:00:00,127.82,194.487,61.461999999999996,32.613 +2020-02-25 10:15:00,127.61,190.99599999999998,61.461999999999996,32.613 +2020-02-25 10:30:00,124.56,188.578,61.461999999999996,32.613 +2020-02-25 10:45:00,124.47,187.65900000000002,61.461999999999996,32.613 +2020-02-25 11:00:00,120.98,185.67700000000002,59.614,32.613 +2020-02-25 11:15:00,121.56,184.88099999999997,59.614,32.613 +2020-02-25 11:30:00,122.41,184.05200000000002,59.614,32.613 +2020-02-25 11:45:00,121.67,182.856,59.614,32.613 +2020-02-25 12:00:00,121.07,177.997,57.415,32.613 +2020-02-25 12:15:00,120.61,178.21200000000002,57.415,32.613 +2020-02-25 12:30:00,117.95,177.486,57.415,32.613 +2020-02-25 12:45:00,118.6,178.41099999999997,57.415,32.613 +2020-02-25 13:00:00,116.09,177.77200000000002,58.534,32.613 +2020-02-25 13:15:00,116.32,176.41299999999998,58.534,32.613 +2020-02-25 13:30:00,118.93,175.377,58.534,32.613 +2020-02-25 13:45:00,116.93,174.995,58.534,32.613 +2020-02-25 14:00:00,119.27,175.935,59.415,32.613 +2020-02-25 14:15:00,117.11,175.63400000000001,59.415,32.613 +2020-02-25 14:30:00,118.09,176.03900000000002,59.415,32.613 +2020-02-25 14:45:00,119.19,176.899,59.415,32.613 +2020-02-25 15:00:00,121.2,178.81400000000002,62.071999999999996,32.613 +2020-02-25 15:15:00,119.3,177.905,62.071999999999996,32.613 +2020-02-25 15:30:00,119.76,178.90200000000002,62.071999999999996,32.613 +2020-02-25 15:45:00,121.39,179.77700000000002,62.071999999999996,32.613 +2020-02-25 16:00:00,123.07,181.363,64.99,32.613 +2020-02-25 16:15:00,125.94,182.801,64.99,32.613 +2020-02-25 16:30:00,123.48,185.644,64.99,32.613 +2020-02-25 16:45:00,125.86,186.815,64.99,32.613 +2020-02-25 17:00:00,133.26,188.80900000000003,72.658,32.613 +2020-02-25 17:15:00,133.66,191.135,72.658,32.613 +2020-02-25 17:30:00,136.58,193.74400000000003,72.658,32.613 +2020-02-25 17:45:00,134.98,194.747,72.658,32.613 +2020-02-25 18:00:00,142.9,197.021,73.645,32.613 +2020-02-25 18:15:00,140.35,196.71400000000003,73.645,32.613 +2020-02-25 18:30:00,140.6,195.40400000000002,73.645,32.613 +2020-02-25 18:45:00,140.89,196.53,73.645,32.613 +2020-02-25 19:00:00,138.89,195.298,67.085,32.613 +2020-02-25 19:15:00,137.3,192.084,67.085,32.613 +2020-02-25 19:30:00,143.66,190.675,67.085,32.613 +2020-02-25 19:45:00,146.82,187.835,67.085,32.613 +2020-02-25 20:00:00,137.15,182.99099999999999,66.138,32.613 +2020-02-25 20:15:00,124.2,177.548,66.138,32.613 +2020-02-25 20:30:00,125.13,173.822,66.138,32.613 +2020-02-25 20:45:00,122.61,172.012,66.138,32.613 +2020-02-25 21:00:00,114.79,168.75900000000001,57.512,32.613 +2020-02-25 21:15:00,117.9,165.666,57.512,32.613 +2020-02-25 21:30:00,118.78,163.622,57.512,32.613 +2020-02-25 21:45:00,115.59,163.079,57.512,32.613 +2020-02-25 22:00:00,105.29,155.977,54.545,32.613 +2020-02-25 22:15:00,105.73,151.689,54.545,32.613 +2020-02-25 22:30:00,100.32,137.36,54.545,32.613 +2020-02-25 22:45:00,103.78,129.903,54.545,32.613 +2020-02-25 23:00:00,103.37,123.266,48.605,32.613 +2020-02-25 23:15:00,102.21,121.863,48.605,32.613 +2020-02-25 23:30:00,94.38,123.02,48.605,32.613 +2020-02-25 23:45:00,93.16,123.08200000000001,48.605,32.613 +2020-02-26 00:00:00,84.21,117.904,45.675,32.613 +2020-02-26 00:15:00,85.32,116.678,45.675,32.613 +2020-02-26 00:30:00,85.53,116.619,45.675,32.613 +2020-02-26 00:45:00,85.09,117.14299999999999,45.675,32.613 +2020-02-26 01:00:00,86.21,118.89200000000001,43.015,32.613 +2020-02-26 01:15:00,91.24,119.031,43.015,32.613 +2020-02-26 01:30:00,91.43,119.15899999999999,43.015,32.613 +2020-02-26 01:45:00,91.86,119.352,43.015,32.613 +2020-02-26 02:00:00,88.93,121.329,41.0,32.613 +2020-02-26 02:15:00,90.95,122.693,41.0,32.613 +2020-02-26 02:30:00,91.59,123.649,41.0,32.613 +2020-02-26 02:45:00,90.28,125.43700000000001,41.0,32.613 +2020-02-26 03:00:00,90.74,127.93299999999999,41.318000000000005,32.613 +2020-02-26 03:15:00,95.62,129.485,41.318000000000005,32.613 +2020-02-26 03:30:00,92.99,131.248,41.318000000000005,32.613 +2020-02-26 03:45:00,89.88,132.731,41.318000000000005,32.613 +2020-02-26 04:00:00,92.31,144.463,42.544,32.613 +2020-02-26 04:15:00,89.03,156.114,42.544,32.613 +2020-02-26 04:30:00,88.74,158.754,42.544,32.613 +2020-02-26 04:45:00,89.87,160.845,42.544,32.613 +2020-02-26 05:00:00,94.12,193.24400000000003,45.161,32.613 +2020-02-26 05:15:00,95.66,221.297,45.161,32.613 +2020-02-26 05:30:00,99.71,216.31599999999997,45.161,32.613 +2020-02-26 05:45:00,104.02,209.291,45.161,32.613 +2020-02-26 06:00:00,116.54,206.68099999999998,61.86600000000001,32.613 +2020-02-26 06:15:00,119.33,212.68099999999998,61.86600000000001,32.613 +2020-02-26 06:30:00,124.91,214.77900000000002,61.86600000000001,32.613 +2020-02-26 06:45:00,124.25,218.601,61.86600000000001,32.613 +2020-02-26 07:00:00,132.05,219.398,77.814,32.613 +2020-02-26 07:15:00,131.93,223.041,77.814,32.613 +2020-02-26 07:30:00,130.23,224.108,77.814,32.613 +2020-02-26 07:45:00,131.9,223.488,77.814,32.613 +2020-02-26 08:00:00,135.76,221.69299999999998,70.251,32.613 +2020-02-26 08:15:00,134.68,220.50099999999998,70.251,32.613 +2020-02-26 08:30:00,134.89,216.365,70.251,32.613 +2020-02-26 08:45:00,132.39,212.62900000000002,70.251,32.613 +2020-02-26 09:00:00,132.02,205.533,66.965,32.613 +2020-02-26 09:15:00,137.94,202.412,66.965,32.613 +2020-02-26 09:30:00,138.13,201.06400000000002,66.965,32.613 +2020-02-26 09:45:00,140.89,198.15200000000002,66.965,32.613 +2020-02-26 10:00:00,135.54,193.976,63.628,32.613 +2020-02-26 10:15:00,133.05,190.52200000000002,63.628,32.613 +2020-02-26 10:30:00,129.5,188.12099999999998,63.628,32.613 +2020-02-26 10:45:00,130.84,187.217,63.628,32.613 +2020-02-26 11:00:00,122.72,185.227,62.516999999999996,32.613 +2020-02-26 11:15:00,122.07,184.449,62.516999999999996,32.613 +2020-02-26 11:30:00,120.82,183.62599999999998,62.516999999999996,32.613 +2020-02-26 11:45:00,120.99,182.445,62.516999999999996,32.613 +2020-02-26 12:00:00,121.64,177.602,60.888999999999996,32.613 +2020-02-26 12:15:00,119.41,177.832,60.888999999999996,32.613 +2020-02-26 12:30:00,119.83,177.074,60.888999999999996,32.613 +2020-02-26 12:45:00,121.37,177.99599999999998,60.888999999999996,32.613 +2020-02-26 13:00:00,118.3,177.392,61.57899999999999,32.613 +2020-02-26 13:15:00,121.25,176.01,61.57899999999999,32.613 +2020-02-26 13:30:00,119.3,174.96,61.57899999999999,32.613 +2020-02-26 13:45:00,117.57,174.58,61.57899999999999,32.613 +2020-02-26 14:00:00,120.42,175.582,62.602,32.613 +2020-02-26 14:15:00,120.08,175.25900000000001,62.602,32.613 +2020-02-26 14:30:00,127.13,175.64,62.602,32.613 +2020-02-26 14:45:00,123.18,176.512,62.602,32.613 +2020-02-26 15:00:00,124.28,178.433,64.259,32.613 +2020-02-26 15:15:00,121.09,177.495,64.259,32.613 +2020-02-26 15:30:00,121.21,178.44799999999998,64.259,32.613 +2020-02-26 15:45:00,124.4,179.30599999999998,64.259,32.613 +2020-02-26 16:00:00,123.44,180.894,67.632,32.613 +2020-02-26 16:15:00,128.2,182.315,67.632,32.613 +2020-02-26 16:30:00,126.24,185.15900000000002,67.632,32.613 +2020-02-26 16:45:00,129.82,186.3,67.632,32.613 +2020-02-26 17:00:00,132.44,188.3,72.583,32.613 +2020-02-26 17:15:00,132.18,190.64,72.583,32.613 +2020-02-26 17:30:00,138.18,193.27700000000002,72.583,32.613 +2020-02-26 17:45:00,137.44,194.299,72.583,32.613 +2020-02-26 18:00:00,144.08,196.584,72.744,32.613 +2020-02-26 18:15:00,142.07,196.34,72.744,32.613 +2020-02-26 18:30:00,143.05,195.028,72.744,32.613 +2020-02-26 18:45:00,143.25,196.175,72.744,32.613 +2020-02-26 19:00:00,142.61,194.905,69.684,32.613 +2020-02-26 19:15:00,143.22,191.708,69.684,32.613 +2020-02-26 19:30:00,148.53,190.325,69.684,32.613 +2020-02-26 19:45:00,145.96,187.525,69.684,32.613 +2020-02-26 20:00:00,134.53,182.65200000000002,70.036,32.613 +2020-02-26 20:15:00,128.04,177.22299999999998,70.036,32.613 +2020-02-26 20:30:00,123.73,173.518,70.036,32.613 +2020-02-26 20:45:00,122.17,171.71,70.036,32.613 +2020-02-26 21:00:00,116.79,168.44400000000002,60.431999999999995,32.613 +2020-02-26 21:15:00,121.15,165.343,60.431999999999995,32.613 +2020-02-26 21:30:00,120.23,163.3,60.431999999999995,32.613 +2020-02-26 21:45:00,113.68,162.774,60.431999999999995,32.613 +2020-02-26 22:00:00,107.74,155.657,56.2,32.613 +2020-02-26 22:15:00,104.57,151.393,56.2,32.613 +2020-02-26 22:30:00,101.62,137.017,56.2,32.613 +2020-02-26 22:45:00,102.65,129.564,56.2,32.613 +2020-02-26 23:00:00,96.31,122.925,47.927,32.613 +2020-02-26 23:15:00,102.68,121.538,47.927,32.613 +2020-02-26 23:30:00,99.27,122.704,47.927,32.613 +2020-02-26 23:45:00,95.89,122.791,47.927,32.613 +2020-02-27 00:00:00,88.18,117.63799999999999,43.794,32.613 +2020-02-27 00:15:00,88.97,116.417,43.794,32.613 +2020-02-27 00:30:00,90.67,116.34,43.794,32.613 +2020-02-27 00:45:00,87.97,116.863,43.794,32.613 +2020-02-27 01:00:00,93.08,118.571,42.397,32.613 +2020-02-27 01:15:00,91.83,118.697,42.397,32.613 +2020-02-27 01:30:00,90.3,118.811,42.397,32.613 +2020-02-27 01:45:00,86.08,119.008,42.397,32.613 +2020-02-27 02:00:00,85.82,120.977,40.010999999999996,32.613 +2020-02-27 02:15:00,93.94,122.337,40.010999999999996,32.613 +2020-02-27 02:30:00,91.53,123.309,40.010999999999996,32.613 +2020-02-27 02:45:00,92.99,125.09700000000001,40.010999999999996,32.613 +2020-02-27 03:00:00,90.02,127.602,39.181,32.613 +2020-02-27 03:15:00,93.1,129.143,39.181,32.613 +2020-02-27 03:30:00,93.75,130.901,39.181,32.613 +2020-02-27 03:45:00,90.45,132.39600000000002,39.181,32.613 +2020-02-27 04:00:00,93.08,144.13299999999998,40.39,32.613 +2020-02-27 04:15:00,98.7,155.77700000000002,40.39,32.613 +2020-02-27 04:30:00,96.19,158.435,40.39,32.613 +2020-02-27 04:45:00,92.23,160.512,40.39,32.613 +2020-02-27 05:00:00,95.1,192.89700000000002,45.504,32.613 +2020-02-27 05:15:00,97.21,220.97400000000002,45.504,32.613 +2020-02-27 05:30:00,102.16,215.96099999999998,45.504,32.613 +2020-02-27 05:45:00,104.54,208.938,45.504,32.613 +2020-02-27 06:00:00,114.7,206.334,57.748000000000005,32.613 +2020-02-27 06:15:00,118.52,212.34400000000002,57.748000000000005,32.613 +2020-02-27 06:30:00,121.44,214.40099999999998,57.748000000000005,32.613 +2020-02-27 06:45:00,123.95,218.209,57.748000000000005,32.613 +2020-02-27 07:00:00,127.56,219.028,72.138,32.613 +2020-02-27 07:15:00,129.28,222.639,72.138,32.613 +2020-02-27 07:30:00,129.58,223.67,72.138,32.613 +2020-02-27 07:45:00,132.15,223.00799999999998,72.138,32.613 +2020-02-27 08:00:00,134.97,221.19,65.542,32.613 +2020-02-27 08:15:00,134.2,219.979,65.542,32.613 +2020-02-27 08:30:00,131.35,215.787,65.542,32.613 +2020-02-27 08:45:00,126.9,212.06,65.542,32.613 +2020-02-27 09:00:00,124.52,204.975,60.523,32.613 +2020-02-27 09:15:00,124.46,201.856,60.523,32.613 +2020-02-27 09:30:00,126.69,200.528,60.523,32.613 +2020-02-27 09:45:00,127.92,197.623,60.523,32.613 +2020-02-27 10:00:00,125.86,193.459,57.449,32.613 +2020-02-27 10:15:00,123.44,190.041,57.449,32.613 +2020-02-27 10:30:00,125.61,187.658,57.449,32.613 +2020-02-27 10:45:00,126.43,186.771,57.449,32.613 +2020-02-27 11:00:00,122.4,184.77200000000002,54.505,32.613 +2020-02-27 11:15:00,127.71,184.00900000000001,54.505,32.613 +2020-02-27 11:30:00,120.16,183.195,54.505,32.613 +2020-02-27 11:45:00,118.8,182.02900000000002,54.505,32.613 +2020-02-27 12:00:00,117.29,177.203,51.50899999999999,32.613 +2020-02-27 12:15:00,116.79,177.446,51.50899999999999,32.613 +2020-02-27 12:30:00,112.3,176.657,51.50899999999999,32.613 +2020-02-27 12:45:00,115.74,177.57299999999998,51.50899999999999,32.613 +2020-02-27 13:00:00,112.3,177.00599999999997,51.303999999999995,32.613 +2020-02-27 13:15:00,119.69,175.601,51.303999999999995,32.613 +2020-02-27 13:30:00,117.02,174.53799999999998,51.303999999999995,32.613 +2020-02-27 13:45:00,114.97,174.16,51.303999999999995,32.613 +2020-02-27 14:00:00,115.47,175.22400000000002,52.785,32.613 +2020-02-27 14:15:00,115.4,174.87900000000002,52.785,32.613 +2020-02-27 14:30:00,119.74,175.236,52.785,32.613 +2020-02-27 14:45:00,121.19,176.11900000000003,52.785,32.613 +2020-02-27 15:00:00,123.14,178.046,56.458999999999996,32.613 +2020-02-27 15:15:00,116.9,177.079,56.458999999999996,32.613 +2020-02-27 15:30:00,124.45,177.988,56.458999999999996,32.613 +2020-02-27 15:45:00,126.19,178.828,56.458999999999996,32.613 +2020-02-27 16:00:00,128.02,180.417,59.388000000000005,32.613 +2020-02-27 16:15:00,126.16,181.822,59.388000000000005,32.613 +2020-02-27 16:30:00,128.55,184.666,59.388000000000005,32.613 +2020-02-27 16:45:00,130.21,185.77599999999998,59.388000000000005,32.613 +2020-02-27 17:00:00,134.52,187.782,64.462,32.613 +2020-02-27 17:15:00,134.4,190.137,64.462,32.613 +2020-02-27 17:30:00,136.14,192.80200000000002,64.462,32.613 +2020-02-27 17:45:00,140.64,193.84400000000002,64.462,32.613 +2020-02-27 18:00:00,148.99,196.137,65.128,32.613 +2020-02-27 18:15:00,149.31,195.958,65.128,32.613 +2020-02-27 18:30:00,144.63,194.645,65.128,32.613 +2020-02-27 18:45:00,147.88,195.813,65.128,32.613 +2020-02-27 19:00:00,146.09,194.50400000000002,61.316,32.613 +2020-02-27 19:15:00,145.64,191.324,61.316,32.613 +2020-02-27 19:30:00,135.54,189.967,61.316,32.613 +2020-02-27 19:45:00,136.53,187.209,61.316,32.613 +2020-02-27 20:00:00,133.14,182.305,59.845,32.613 +2020-02-27 20:15:00,130.96,176.892,59.845,32.613 +2020-02-27 20:30:00,127.67,173.207,59.845,32.613 +2020-02-27 20:45:00,123.1,171.40099999999998,59.845,32.613 +2020-02-27 21:00:00,118.51,168.123,54.83,32.613 +2020-02-27 21:15:00,116.64,165.014,54.83,32.613 +2020-02-27 21:30:00,116.78,162.971,54.83,32.613 +2020-02-27 21:45:00,116.76,162.464,54.83,32.613 +2020-02-27 22:00:00,106.08,155.33,50.933,32.613 +2020-02-27 22:15:00,104.2,151.092,50.933,32.613 +2020-02-27 22:30:00,106.52,136.667,50.933,32.613 +2020-02-27 22:45:00,108.8,129.218,50.933,32.613 +2020-02-27 23:00:00,101.23,122.57600000000001,45.32899999999999,32.613 +2020-02-27 23:15:00,100.86,121.205,45.32899999999999,32.613 +2020-02-27 23:30:00,97.66,122.382,45.32899999999999,32.613 +2020-02-27 23:45:00,99.21,122.494,45.32899999999999,32.613 +2020-02-28 00:00:00,95.24,116.29,43.74,32.613 +2020-02-28 00:15:00,90.0,115.273,43.74,32.613 +2020-02-28 00:30:00,93.54,115.04799999999999,43.74,32.613 +2020-02-28 00:45:00,93.7,115.684,43.74,32.613 +2020-02-28 01:00:00,86.92,117.04,42.555,32.613 +2020-02-28 01:15:00,89.35,118.04700000000001,42.555,32.613 +2020-02-28 01:30:00,90.93,117.935,42.555,32.613 +2020-02-28 01:45:00,91.4,118.236,42.555,32.613 +2020-02-28 02:00:00,88.43,120.294,41.68600000000001,32.613 +2020-02-28 02:15:00,90.06,121.537,41.68600000000001,32.613 +2020-02-28 02:30:00,89.98,123.05,41.68600000000001,32.613 +2020-02-28 02:45:00,91.98,124.87200000000001,41.68600000000001,32.613 +2020-02-28 03:00:00,85.75,126.415,42.278999999999996,32.613 +2020-02-28 03:15:00,90.33,128.856,42.278999999999996,32.613 +2020-02-28 03:30:00,91.82,130.59,42.278999999999996,32.613 +2020-02-28 03:45:00,92.28,132.428,42.278999999999996,32.613 +2020-02-28 04:00:00,93.53,144.399,43.742,32.613 +2020-02-28 04:15:00,94.58,155.774,43.742,32.613 +2020-02-28 04:30:00,96.07,158.686,43.742,32.613 +2020-02-28 04:45:00,88.54,159.619,43.742,32.613 +2020-02-28 05:00:00,92.18,190.735,46.973,32.613 +2020-02-28 05:15:00,94.1,220.334,46.973,32.613 +2020-02-28 05:30:00,99.17,216.352,46.973,32.613 +2020-02-28 05:45:00,101.77,209.26,46.973,32.613 +2020-02-28 06:00:00,114.52,207.092,59.63399999999999,32.613 +2020-02-28 06:15:00,116.78,211.665,59.63399999999999,32.613 +2020-02-28 06:30:00,121.82,212.828,59.63399999999999,32.613 +2020-02-28 06:45:00,121.41,218.22,59.63399999999999,32.613 +2020-02-28 07:00:00,126.51,218.248,71.631,32.613 +2020-02-28 07:15:00,130.43,222.845,71.631,32.613 +2020-02-28 07:30:00,132.73,223.635,71.631,32.613 +2020-02-28 07:45:00,131.7,222.05599999999998,71.631,32.613 +2020-02-28 08:00:00,137.02,219.141,66.181,32.613 +2020-02-28 08:15:00,131.96,217.541,66.181,32.613 +2020-02-28 08:30:00,136.24,214.248,66.181,32.613 +2020-02-28 08:45:00,130.35,208.982,66.181,32.613 +2020-02-28 09:00:00,130.85,202.32,63.086000000000006,32.613 +2020-02-28 09:15:00,128.0,199.795,63.086000000000006,32.613 +2020-02-28 09:30:00,131.49,198.054,63.086000000000006,32.613 +2020-02-28 09:45:00,128.77,195.05700000000002,63.086000000000006,32.613 +2020-02-28 10:00:00,128.05,189.793,60.886,32.613 +2020-02-28 10:15:00,129.03,187.09,60.886,32.613 +2020-02-28 10:30:00,125.57,184.666,60.886,32.613 +2020-02-28 10:45:00,132.11,183.361,60.886,32.613 +2020-02-28 11:00:00,130.54,181.342,59.391000000000005,32.613 +2020-02-28 11:15:00,127.61,179.697,59.391000000000005,32.613 +2020-02-28 11:30:00,127.14,180.593,59.391000000000005,32.613 +2020-02-28 11:45:00,124.9,179.465,59.391000000000005,32.613 +2020-02-28 12:00:00,122.01,175.725,56.172,32.613 +2020-02-28 12:15:00,126.66,173.922,56.172,32.613 +2020-02-28 12:30:00,120.31,173.257,56.172,32.613 +2020-02-28 12:45:00,122.17,174.65400000000002,56.172,32.613 +2020-02-28 13:00:00,118.8,175.051,54.406000000000006,32.613 +2020-02-28 13:15:00,122.05,174.425,54.406000000000006,32.613 +2020-02-28 13:30:00,121.88,173.385,54.406000000000006,32.613 +2020-02-28 13:45:00,125.01,172.955,54.406000000000006,32.613 +2020-02-28 14:00:00,121.64,172.949,53.578,32.613 +2020-02-28 14:15:00,117.04,172.418,53.578,32.613 +2020-02-28 14:30:00,115.28,173.28900000000002,53.578,32.613 +2020-02-28 14:45:00,116.76,174.46099999999998,53.578,32.613 +2020-02-28 15:00:00,112.41,175.921,56.568999999999996,32.613 +2020-02-28 15:15:00,116.08,174.49099999999999,56.568999999999996,32.613 +2020-02-28 15:30:00,121.56,173.861,56.568999999999996,32.613 +2020-02-28 15:45:00,123.51,174.84599999999998,56.568999999999996,32.613 +2020-02-28 16:00:00,121.03,175.273,60.169,32.613 +2020-02-28 16:15:00,119.77,176.956,60.169,32.613 +2020-02-28 16:30:00,124.79,179.888,60.169,32.613 +2020-02-28 16:45:00,130.53,180.83700000000002,60.169,32.613 +2020-02-28 17:00:00,134.83,183.079,65.497,32.613 +2020-02-28 17:15:00,137.27,185.04,65.497,32.613 +2020-02-28 17:30:00,137.09,187.424,65.497,32.613 +2020-02-28 17:45:00,141.86,188.253,65.497,32.613 +2020-02-28 18:00:00,148.36,191.23,65.082,32.613 +2020-02-28 18:15:00,143.47,190.71599999999998,65.082,32.613 +2020-02-28 18:30:00,142.41,189.78599999999997,65.082,32.613 +2020-02-28 18:45:00,144.94,190.97799999999998,65.082,32.613 +2020-02-28 19:00:00,139.83,190.535,60.968,32.613 +2020-02-28 19:15:00,136.32,188.72099999999998,60.968,32.613 +2020-02-28 19:30:00,138.61,186.987,60.968,32.613 +2020-02-28 19:45:00,138.84,183.80200000000002,60.968,32.613 +2020-02-28 20:00:00,130.09,178.919,61.123000000000005,32.613 +2020-02-28 20:15:00,127.16,173.543,61.123000000000005,32.613 +2020-02-28 20:30:00,122.02,169.83,61.123000000000005,32.613 +2020-02-28 20:45:00,118.31,168.558,61.123000000000005,32.613 +2020-02-28 21:00:00,117.05,165.787,55.416000000000004,32.613 +2020-02-28 21:15:00,114.42,163.128,55.416000000000004,32.613 +2020-02-28 21:30:00,108.61,161.128,55.416000000000004,32.613 +2020-02-28 21:45:00,103.76,161.17600000000002,55.416000000000004,32.613 +2020-02-28 22:00:00,96.68,154.987,51.631,32.613 +2020-02-28 22:15:00,95.08,150.64,51.631,32.613 +2020-02-28 22:30:00,100.47,142.507,51.631,32.613 +2020-02-28 22:45:00,100.52,138.542,51.631,32.613 +2020-02-28 23:00:00,93.73,131.487,44.898,32.613 +2020-02-28 23:15:00,87.45,128.192,44.898,32.613 +2020-02-28 23:30:00,82.28,127.915,44.898,32.613 +2020-02-28 23:45:00,85.88,127.398,44.898,32.613 +2020-02-29 00:00:00,82.24,113.23700000000001,42.033,32.431999999999995 +2020-02-29 00:15:00,85.01,108.041,42.033,32.431999999999995 +2020-02-29 00:30:00,80.52,109.09299999999999,42.033,32.431999999999995 +2020-02-29 00:45:00,80.2,110.416,42.033,32.431999999999995 +2020-02-29 01:00:00,76.23,112.354,38.255,32.431999999999995 +2020-02-29 01:15:00,75.84,112.39200000000001,38.255,32.431999999999995 +2020-02-29 01:30:00,74.66,111.743,38.255,32.431999999999995 +2020-02-29 01:45:00,74.31,111.86,38.255,32.431999999999995 +2020-02-29 02:00:00,77.97,114.565,36.404,32.431999999999995 +2020-02-29 02:15:00,79.13,115.428,36.404,32.431999999999995 +2020-02-29 02:30:00,76.43,115.85600000000001,36.404,32.431999999999995 +2020-02-29 02:45:00,74.31,117.789,36.404,32.431999999999995 +2020-02-29 03:00:00,72.93,119.935,36.083,32.431999999999995 +2020-02-29 03:15:00,71.88,121.161,36.083,32.431999999999995 +2020-02-29 03:30:00,71.31,121.338,36.083,32.431999999999995 +2020-02-29 03:45:00,71.98,123.323,36.083,32.431999999999995 +2020-02-29 04:00:00,72.01,131.215,36.102,32.431999999999995 +2020-02-29 04:15:00,72.24,140.079,36.102,32.431999999999995 +2020-02-29 04:30:00,72.51,140.856,36.102,32.431999999999995 +2020-02-29 04:45:00,73.77,141.296,36.102,32.431999999999995 +2020-02-29 05:00:00,73.63,156.637,35.284,32.431999999999995 +2020-02-29 05:15:00,74.75,167.479,35.284,32.431999999999995 +2020-02-29 05:30:00,75.74,163.838,35.284,32.431999999999995 +2020-02-29 05:45:00,77.0,162.155,35.284,32.431999999999995 +2020-02-29 06:00:00,77.1,178.55700000000002,36.265,32.431999999999995 +2020-02-29 06:15:00,78.45,199.275,36.265,32.431999999999995 +2020-02-29 06:30:00,77.71,195.088,36.265,32.431999999999995 +2020-02-29 06:45:00,79.47,191.61700000000002,36.265,32.431999999999995 +2020-02-29 07:00:00,82.82,187.955,40.714,32.431999999999995 +2020-02-29 07:15:00,85.08,191.27,40.714,32.431999999999995 +2020-02-29 07:30:00,87.37,194.697,40.714,32.431999999999995 +2020-02-29 07:45:00,90.24,196.983,40.714,32.431999999999995 +2020-02-29 08:00:00,94.26,198.00400000000002,46.692,32.431999999999995 +2020-02-29 08:15:00,93.79,199.93099999999998,46.692,32.431999999999995 +2020-02-29 08:30:00,95.57,198.158,46.692,32.431999999999995 +2020-02-29 08:45:00,97.68,196.007,46.692,32.431999999999995 +2020-02-29 09:00:00,99.64,191.146,48.925,32.431999999999995 +2020-02-29 09:15:00,101.64,189.36700000000002,48.925,32.431999999999995 +2020-02-29 09:30:00,101.61,188.537,48.925,32.431999999999995 +2020-02-29 09:45:00,103.34,185.695,48.925,32.431999999999995 +2020-02-29 10:00:00,103.19,180.743,47.799,32.431999999999995 +2020-02-29 10:15:00,103.4,178.24599999999998,47.799,32.431999999999995 +2020-02-29 10:30:00,100.05,175.993,47.799,32.431999999999995 +2020-02-29 10:45:00,101.34,175.965,47.799,32.431999999999995 +2020-02-29 11:00:00,102.61,174.145,44.309,32.431999999999995 +2020-02-29 11:15:00,104.3,171.86900000000003,44.309,32.431999999999995 +2020-02-29 11:30:00,107.59,171.69299999999998,44.309,32.431999999999995 +2020-02-29 11:45:00,110.39,169.666,44.309,32.431999999999995 +2020-02-29 12:00:00,107.28,165.10299999999998,42.367,32.431999999999995 +2020-02-29 12:15:00,108.46,163.957,42.367,32.431999999999995 +2020-02-29 12:30:00,105.82,163.566,42.367,32.431999999999995 +2020-02-29 12:45:00,103.19,164.22799999999998,42.367,32.431999999999995 +2020-02-29 13:00:00,97.8,164.255,39.036,32.431999999999995 +2020-02-29 13:15:00,97.25,161.597,39.036,32.431999999999995 +2020-02-29 13:30:00,95.99,160.111,39.036,32.431999999999995 +2020-02-29 13:45:00,96.33,160.111,39.036,32.431999999999995 +2020-02-29 14:00:00,93.08,161.362,37.995,32.431999999999995 +2020-02-29 14:15:00,93.55,160.257,37.995,32.431999999999995 +2020-02-29 14:30:00,93.15,159.326,37.995,32.431999999999995 +2020-02-29 14:45:00,93.28,160.732,37.995,32.431999999999995 +2020-02-29 15:00:00,92.46,162.841,40.71,32.431999999999995 +2020-02-29 15:15:00,91.85,162.183,40.71,32.431999999999995 +2020-02-29 15:30:00,89.88,163.00799999999998,40.71,32.431999999999995 +2020-02-29 15:45:00,88.72,163.968,40.71,32.431999999999995 +2020-02-29 16:00:00,88.8,163.262,46.998000000000005,32.431999999999995 +2020-02-29 16:15:00,87.64,165.77700000000002,46.998000000000005,32.431999999999995 +2020-02-29 16:30:00,89.92,168.66400000000002,46.998000000000005,32.431999999999995 +2020-02-29 16:45:00,92.75,170.44299999999998,46.998000000000005,32.431999999999995 +2020-02-29 17:00:00,98.43,172.093,55.431000000000004,32.431999999999995 +2020-02-29 17:15:00,99.1,175.64,55.431000000000004,32.431999999999995 +2020-02-29 17:30:00,102.07,177.953,55.431000000000004,32.431999999999995 +2020-02-29 17:45:00,105.39,178.418,55.431000000000004,32.431999999999995 +2020-02-29 18:00:00,110.77,180.94299999999998,55.989,32.431999999999995 +2020-02-29 18:15:00,113.04,182.285,55.989,32.431999999999995 +2020-02-29 18:30:00,114.76,182.68,55.989,32.431999999999995 +2020-02-29 18:45:00,114.16,180.55700000000002,55.989,32.431999999999995 +2020-02-29 19:00:00,112.1,180.942,50.882,32.431999999999995 +2020-02-29 19:15:00,110.76,178.642,50.882,32.431999999999995 +2020-02-29 19:30:00,109.34,177.685,50.882,32.431999999999995 +2020-02-29 19:45:00,110.83,174.40099999999998,50.882,32.431999999999995 +2020-02-29 20:00:00,103.05,171.63400000000001,43.172,32.431999999999995 +2020-02-29 20:15:00,100.21,168.33700000000002,43.172,32.431999999999995 +2020-02-29 20:30:00,97.84,164.28599999999997,43.172,32.431999999999995 +2020-02-29 20:45:00,96.65,162.657,43.172,32.431999999999995 +2020-02-29 21:00:00,90.85,162.047,37.599000000000004,32.431999999999995 +2020-02-29 21:15:00,91.68,159.80200000000002,37.599000000000004,32.431999999999995 +2020-02-29 21:30:00,89.56,159.00799999999998,37.599000000000004,32.431999999999995 +2020-02-29 21:45:00,89.3,158.668,37.599000000000004,32.431999999999995 +2020-02-29 22:00:00,85.48,153.77700000000002,39.047,32.431999999999995 +2020-02-29 22:15:00,84.96,151.89,39.047,32.431999999999995 +2020-02-29 22:30:00,79.47,149.774,39.047,32.431999999999995 +2020-02-29 22:45:00,81.08,147.64600000000002,39.047,32.431999999999995 +2020-02-29 23:00:00,77.22,142.952,32.339,32.431999999999995 +2020-02-29 23:15:00,77.22,138.085,32.339,32.431999999999995 +2020-02-29 23:30:00,75.06,136.158,32.339,32.431999999999995 +2020-02-29 23:45:00,73.54,133.304,32.339,32.431999999999995 +2020-03-01 00:00:00,68.61,114.87200000000001,20.007,31.988000000000003 +2020-03-01 00:15:00,70.53,108.667,20.007,31.988000000000003 +2020-03-01 00:30:00,66.5,108.73299999999999,20.007,31.988000000000003 +2020-03-01 00:45:00,67.58,110.04899999999999,20.007,31.988000000000003 +2020-03-01 01:00:00,65.55,112.008,17.378,31.988000000000003 +2020-03-01 01:15:00,65.75,113.44200000000001,17.378,31.988000000000003 +2020-03-01 01:30:00,65.35,113.366,17.378,31.988000000000003 +2020-03-01 01:45:00,65.61,113.147,17.378,31.988000000000003 +2020-03-01 02:00:00,63.96,115.15,16.145,31.988000000000003 +2020-03-01 02:15:00,64.55,114.846,16.145,31.988000000000003 +2020-03-01 02:30:00,62.76,115.854,16.145,31.988000000000003 +2020-03-01 02:45:00,63.36,118.155,16.145,31.988000000000003 +2020-03-01 03:00:00,62.76,120.616,15.427999999999999,31.988000000000003 +2020-03-01 03:15:00,63.56,121.51,15.427999999999999,31.988000000000003 +2020-03-01 03:30:00,63.25,123.181,15.427999999999999,31.988000000000003 +2020-03-01 03:45:00,63.21,124.824,15.427999999999999,31.988000000000003 +2020-03-01 04:00:00,62.91,133.92700000000002,16.663,31.988000000000003 +2020-03-01 04:15:00,63.35,143.066,16.663,31.988000000000003 +2020-03-01 04:30:00,63.9,143.304,16.663,31.988000000000003 +2020-03-01 04:45:00,63.94,143.76,16.663,31.988000000000003 +2020-03-01 05:00:00,66.16,157.566,17.271,31.988000000000003 +2020-03-01 05:15:00,66.79,168.065,17.271,31.988000000000003 +2020-03-01 05:30:00,66.13,163.91,17.271,31.988000000000003 +2020-03-01 05:45:00,66.68,161.535,17.271,31.988000000000003 +2020-03-01 06:00:00,67.98,178.033,17.612000000000002,31.988000000000003 +2020-03-01 06:15:00,67.63,197.967,17.612000000000002,31.988000000000003 +2020-03-01 06:30:00,66.96,192.521,17.612000000000002,31.988000000000003 +2020-03-01 06:45:00,67.33,187.15599999999998,17.612000000000002,31.988000000000003 +2020-03-01 07:00:00,69.6,187.088,20.88,31.988000000000003 +2020-03-01 07:15:00,70.32,188.924,20.88,31.988000000000003 +2020-03-01 07:30:00,72.12,190.649,20.88,31.988000000000003 +2020-03-01 07:45:00,75.07,191.512,20.88,31.988000000000003 +2020-03-01 08:00:00,79.08,194.41400000000002,25.861,31.988000000000003 +2020-03-01 08:15:00,78.76,195.868,25.861,31.988000000000003 +2020-03-01 08:30:00,77.97,195.63400000000001,25.861,31.988000000000003 +2020-03-01 08:45:00,78.47,194.578,25.861,31.988000000000003 +2020-03-01 09:00:00,77.62,188.77599999999998,27.921999999999997,31.988000000000003 +2020-03-01 09:15:00,77.96,187.282,27.921999999999997,31.988000000000003 +2020-03-01 09:30:00,80.81,186.24200000000002,27.921999999999997,31.988000000000003 +2020-03-01 09:45:00,87.16,183.475,27.921999999999997,31.988000000000003 +2020-03-01 10:00:00,87.22,182.304,29.048000000000002,31.988000000000003 +2020-03-01 10:15:00,85.78,180.335,29.048000000000002,31.988000000000003 +2020-03-01 10:30:00,84.44,178.554,29.048000000000002,31.988000000000003 +2020-03-01 10:45:00,83.32,176.795,29.048000000000002,31.988000000000003 +2020-03-01 11:00:00,83.84,174.87900000000002,32.02,31.988000000000003 +2020-03-01 11:15:00,81.49,172.76,32.02,31.988000000000003 +2020-03-01 11:30:00,83.37,172.18400000000003,32.02,31.988000000000003 +2020-03-01 11:45:00,82.81,171.415,32.02,31.988000000000003 +2020-03-01 12:00:00,79.55,165.91400000000002,28.55,31.988000000000003 +2020-03-01 12:15:00,76.4,166.451,28.55,31.988000000000003 +2020-03-01 12:30:00,72.82,165.06900000000002,28.55,31.988000000000003 +2020-03-01 12:45:00,71.89,164.695,28.55,31.988000000000003 +2020-03-01 13:00:00,68.82,163.696,25.601999999999997,31.988000000000003 +2020-03-01 13:15:00,67.4,163.767,25.601999999999997,31.988000000000003 +2020-03-01 13:30:00,66.06,161.689,25.601999999999997,31.988000000000003 +2020-03-01 13:45:00,65.28,160.939,25.601999999999997,31.988000000000003 +2020-03-01 14:00:00,62.31,162.632,23.916999999999998,31.988000000000003 +2020-03-01 14:15:00,63.97,162.403,23.916999999999998,31.988000000000003 +2020-03-01 14:30:00,64.32,162.54399999999998,23.916999999999998,31.988000000000003 +2020-03-01 14:45:00,66.22,163.375,23.916999999999998,31.988000000000003 +2020-03-01 15:00:00,66.27,163.856,24.064,31.988000000000003 +2020-03-01 15:15:00,67.29,163.786,24.064,31.988000000000003 +2020-03-01 15:30:00,68.58,164.68200000000002,24.064,31.988000000000003 +2020-03-01 15:45:00,70.54,165.767,24.064,31.988000000000003 +2020-03-01 16:00:00,73.29,168.68,28.189,31.988000000000003 +2020-03-01 16:15:00,73.42,170.81400000000002,28.189,31.988000000000003 +2020-03-01 16:30:00,75.75,173.27599999999998,28.189,31.988000000000003 +2020-03-01 16:45:00,78.52,174.707,28.189,31.988000000000003 +2020-03-01 17:00:00,83.11,177.141,37.576,31.988000000000003 +2020-03-01 17:15:00,85.27,180.53900000000002,37.576,31.988000000000003 +2020-03-01 17:30:00,89.46,183.257,37.576,31.988000000000003 +2020-03-01 17:45:00,92.14,185.89,37.576,31.988000000000003 +2020-03-01 18:00:00,100.96,188.957,42.669,31.988000000000003 +2020-03-01 18:15:00,103.27,191.891,42.669,31.988000000000003 +2020-03-01 18:30:00,103.76,190.107,42.669,31.988000000000003 +2020-03-01 18:45:00,102.03,190.166,42.669,31.988000000000003 +2020-03-01 19:00:00,101.46,190.812,43.538999999999994,31.988000000000003 +2020-03-01 19:15:00,100.17,188.94400000000002,43.538999999999994,31.988000000000003 +2020-03-01 19:30:00,98.2,188.227,43.538999999999994,31.988000000000003 +2020-03-01 19:45:00,96.97,185.736,43.538999999999994,31.988000000000003 +2020-03-01 20:00:00,93.91,182.748,37.330999999999996,31.988000000000003 +2020-03-01 20:15:00,93.83,180.495,37.330999999999996,31.988000000000003 +2020-03-01 20:30:00,95.91,177.46200000000002,37.330999999999996,31.988000000000003 +2020-03-01 20:45:00,97.34,173.905,37.330999999999996,31.988000000000003 +2020-03-01 21:00:00,94.5,170.889,33.856,31.988000000000003 +2020-03-01 21:15:00,89.25,168.035,33.856,31.988000000000003 +2020-03-01 21:30:00,88.14,167.192,33.856,31.988000000000003 +2020-03-01 21:45:00,89.58,167.22400000000002,33.856,31.988000000000003 +2020-03-01 22:00:00,93.6,161.214,34.711999999999996,31.988000000000003 +2020-03-01 22:15:00,95.33,158.43200000000002,34.711999999999996,31.988000000000003 +2020-03-01 22:30:00,91.18,152.474,34.711999999999996,31.988000000000003 +2020-03-01 22:45:00,83.29,149.13299999999998,34.711999999999996,31.988000000000003 +2020-03-01 23:00:00,79.65,141.811,29.698,31.988000000000003 +2020-03-01 23:15:00,80.74,138.316,29.698,31.988000000000003 +2020-03-01 23:30:00,78.27,137.503,29.698,31.988000000000003 +2020-03-01 23:45:00,78.56,135.459,29.698,31.988000000000003 +2020-03-02 00:00:00,73.51,118.774,29.983,32.166 +2020-03-02 00:15:00,74.57,115.516,29.983,32.166 +2020-03-02 00:30:00,75.5,115.632,29.983,32.166 +2020-03-02 00:45:00,73.67,116.381,29.983,32.166 +2020-03-02 01:00:00,70.05,118.355,29.122,32.166 +2020-03-02 01:15:00,71.82,119.274,29.122,32.166 +2020-03-02 01:30:00,71.29,119.28299999999999,29.122,32.166 +2020-03-02 01:45:00,72.24,119.164,29.122,32.166 +2020-03-02 02:00:00,70.05,121.193,28.676,32.166 +2020-03-02 02:15:00,71.47,122.126,28.676,32.166 +2020-03-02 02:30:00,70.78,123.491,28.676,32.166 +2020-03-02 02:45:00,71.48,125.18299999999999,28.676,32.166 +2020-03-02 03:00:00,72.12,128.914,26.552,32.166 +2020-03-02 03:15:00,72.82,131.45600000000002,26.552,32.166 +2020-03-02 03:30:00,79.44,132.96200000000002,26.552,32.166 +2020-03-02 03:45:00,81.71,134.03,26.552,32.166 +2020-03-02 04:00:00,82.58,147.67700000000002,27.44,32.166 +2020-03-02 04:15:00,77.82,161.142,27.44,32.166 +2020-03-02 04:30:00,78.23,163.398,27.44,32.166 +2020-03-02 04:45:00,79.38,164.046,27.44,32.166 +2020-03-02 05:00:00,83.72,193.69400000000002,36.825,32.166 +2020-03-02 05:15:00,86.57,224.705,36.825,32.166 +2020-03-02 05:30:00,91.53,220.55200000000002,36.825,32.166 +2020-03-02 05:45:00,96.44,212.40900000000002,36.825,32.166 +2020-03-02 06:00:00,105.65,210.571,56.589,32.166 +2020-03-02 06:15:00,110.56,215.197,56.589,32.166 +2020-03-02 06:30:00,114.34,217.84599999999998,56.589,32.166 +2020-03-02 06:45:00,116.99,221.528,56.589,32.166 +2020-03-02 07:00:00,121.98,223.831,67.49,32.166 +2020-03-02 07:15:00,122.83,227.138,67.49,32.166 +2020-03-02 07:30:00,126.26,227.96599999999998,67.49,32.166 +2020-03-02 07:45:00,128.48,226.40400000000002,67.49,32.166 +2020-03-02 08:00:00,132.44,224.445,60.028,32.166 +2020-03-02 08:15:00,131.31,223.685,60.028,32.166 +2020-03-02 08:30:00,130.5,219.38,60.028,32.166 +2020-03-02 08:45:00,131.43,215.199,60.028,32.166 +2020-03-02 09:00:00,130.52,208.362,55.018,32.166 +2020-03-02 09:15:00,130.5,203.305,55.018,32.166 +2020-03-02 09:30:00,131.0,201.21099999999998,55.018,32.166 +2020-03-02 09:45:00,131.43,198.43,55.018,32.166 +2020-03-02 10:00:00,129.42,196.28799999999998,51.183,32.166 +2020-03-02 10:15:00,130.46,194.028,51.183,32.166 +2020-03-02 10:30:00,130.39,191.372,51.183,32.166 +2020-03-02 10:45:00,130.17,190.128,51.183,32.166 +2020-03-02 11:00:00,128.8,185.782,50.065,32.166 +2020-03-02 11:15:00,130.97,185.44099999999997,50.065,32.166 +2020-03-02 11:30:00,128.5,186.283,50.065,32.166 +2020-03-02 11:45:00,124.59,185.206,50.065,32.166 +2020-03-02 12:00:00,122.07,181.143,48.141999999999996,32.166 +2020-03-02 12:15:00,122.75,181.709,48.141999999999996,32.166 +2020-03-02 12:30:00,118.19,180.388,48.141999999999996,32.166 +2020-03-02 12:45:00,115.54,181.465,48.141999999999996,32.166 +2020-03-02 13:00:00,116.29,181.18,47.887,32.166 +2020-03-02 13:15:00,111.2,179.791,47.887,32.166 +2020-03-02 13:30:00,110.44,177.218,47.887,32.166 +2020-03-02 13:45:00,110.06,176.62,47.887,32.166 +2020-03-02 14:00:00,109.1,177.69,48.571000000000005,32.166 +2020-03-02 14:15:00,110.15,176.90200000000002,48.571000000000005,32.166 +2020-03-02 14:30:00,109.82,176.476,48.571000000000005,32.166 +2020-03-02 14:45:00,112.38,177.563,48.571000000000005,32.166 +2020-03-02 15:00:00,111.64,179.74599999999998,49.937,32.166 +2020-03-02 15:15:00,113.26,178.19299999999998,49.937,32.166 +2020-03-02 15:30:00,112.72,178.34599999999998,49.937,32.166 +2020-03-02 15:45:00,112.81,178.925,49.937,32.166 +2020-03-02 16:00:00,116.61,182.183,52.963,32.166 +2020-03-02 16:15:00,115.99,183.57299999999998,52.963,32.166 +2020-03-02 16:30:00,117.48,185.021,52.963,32.166 +2020-03-02 16:45:00,119.0,185.275,52.963,32.166 +2020-03-02 17:00:00,121.05,187.44,61.163999999999994,32.166 +2020-03-02 17:15:00,122.51,189.97299999999998,61.163999999999994,32.166 +2020-03-02 17:30:00,125.34,192.14,61.163999999999994,32.166 +2020-03-02 17:45:00,127.45,193.295,61.163999999999994,32.166 +2020-03-02 18:00:00,135.41,196.68599999999998,63.788999999999994,32.166 +2020-03-02 18:15:00,136.15,197.328,63.788999999999994,32.166 +2020-03-02 18:30:00,135.28,196.08900000000003,63.788999999999994,32.166 +2020-03-02 18:45:00,134.36,197.226,63.788999999999994,32.166 +2020-03-02 19:00:00,134.74,196.25,63.913000000000004,32.166 +2020-03-02 19:15:00,130.7,193.407,63.913000000000004,32.166 +2020-03-02 19:30:00,131.09,193.16099999999997,63.913000000000004,32.166 +2020-03-02 19:45:00,129.93,189.833,63.913000000000004,32.166 +2020-03-02 20:00:00,119.75,184.363,65.44,32.166 +2020-03-02 20:15:00,118.41,179.88299999999998,65.44,32.166 +2020-03-02 20:30:00,115.33,175.128,65.44,32.166 +2020-03-02 20:45:00,114.14,173.17700000000002,65.44,32.166 +2020-03-02 21:00:00,109.07,170.59400000000002,59.117,32.166 +2020-03-02 21:15:00,111.01,166.644,59.117,32.166 +2020-03-02 21:30:00,109.45,165.047,59.117,32.166 +2020-03-02 21:45:00,107.16,164.585,59.117,32.166 +2020-03-02 22:00:00,100.92,155.52,52.301,32.166 +2020-03-02 22:15:00,97.41,151.71,52.301,32.166 +2020-03-02 22:30:00,99.9,135.923,52.301,32.166 +2020-03-02 22:45:00,100.06,127.88600000000001,52.301,32.166 +2020-03-02 23:00:00,94.69,121.306,44.373000000000005,32.166 +2020-03-02 23:15:00,88.97,120.22200000000001,44.373000000000005,32.166 +2020-03-02 23:30:00,89.49,122.09700000000001,44.373000000000005,32.166 +2020-03-02 23:45:00,90.65,122.587,44.373000000000005,32.166 +2020-03-03 00:00:00,87.69,117.507,44.647,32.166 +2020-03-03 00:15:00,84.07,115.73299999999999,44.647,32.166 +2020-03-03 00:30:00,82.45,115.021,44.647,32.166 +2020-03-03 00:45:00,85.5,114.936,44.647,32.166 +2020-03-03 01:00:00,80.31,116.613,41.433,32.166 +2020-03-03 01:15:00,82.7,117.10600000000001,41.433,32.166 +2020-03-03 01:30:00,78.1,117.24600000000001,41.433,32.166 +2020-03-03 01:45:00,76.78,117.35700000000001,41.433,32.166 +2020-03-03 02:00:00,76.01,119.30799999999999,39.909,32.166 +2020-03-03 02:15:00,76.06,120.281,39.909,32.166 +2020-03-03 02:30:00,76.62,121.045,39.909,32.166 +2020-03-03 02:45:00,81.97,122.792,39.909,32.166 +2020-03-03 03:00:00,84.87,125.315,39.14,32.166 +2020-03-03 03:15:00,86.31,127.15700000000001,39.14,32.166 +2020-03-03 03:30:00,84.73,129.112,39.14,32.166 +2020-03-03 03:45:00,85.5,130.22899999999998,39.14,32.166 +2020-03-03 04:00:00,87.66,143.525,40.015,32.166 +2020-03-03 04:15:00,81.89,156.649,40.015,32.166 +2020-03-03 04:30:00,83.96,158.612,40.015,32.166 +2020-03-03 04:45:00,89.14,160.464,40.015,32.166 +2020-03-03 05:00:00,95.89,195.05900000000003,44.93600000000001,32.166 +2020-03-03 05:15:00,98.73,225.97400000000002,44.93600000000001,32.166 +2020-03-03 05:30:00,97.84,220.359,44.93600000000001,32.166 +2020-03-03 05:45:00,100.51,212.12900000000002,44.93600000000001,32.166 +2020-03-03 06:00:00,109.28,209.315,57.271,32.166 +2020-03-03 06:15:00,112.89,215.521,57.271,32.166 +2020-03-03 06:30:00,115.24,217.46900000000002,57.271,32.166 +2020-03-03 06:45:00,116.92,220.65599999999998,57.271,32.166 +2020-03-03 07:00:00,122.15,222.825,68.352,32.166 +2020-03-03 07:15:00,122.86,225.91299999999998,68.352,32.166 +2020-03-03 07:30:00,126.57,226.18099999999998,68.352,32.166 +2020-03-03 07:45:00,127.79,224.65400000000002,68.352,32.166 +2020-03-03 08:00:00,131.05,222.767,60.717,32.166 +2020-03-03 08:15:00,131.16,220.958,60.717,32.166 +2020-03-03 08:30:00,135.43,216.447,60.717,32.166 +2020-03-03 08:45:00,132.45,211.908,60.717,32.166 +2020-03-03 09:00:00,132.71,204.32299999999998,54.603,32.166 +2020-03-03 09:15:00,132.5,200.79,54.603,32.166 +2020-03-03 09:30:00,134.11,199.454,54.603,32.166 +2020-03-03 09:45:00,133.41,196.642,54.603,32.166 +2020-03-03 10:00:00,133.09,193.778,52.308,32.166 +2020-03-03 10:15:00,132.43,190.52200000000002,52.308,32.166 +2020-03-03 10:30:00,133.18,188.062,52.308,32.166 +2020-03-03 10:45:00,133.76,187.234,52.308,32.166 +2020-03-03 11:00:00,132.89,184.308,51.838,32.166 +2020-03-03 11:15:00,134.16,183.71599999999998,51.838,32.166 +2020-03-03 11:30:00,133.77,183.303,51.838,32.166 +2020-03-03 11:45:00,134.04,182.864,51.838,32.166 +2020-03-03 12:00:00,134.11,177.479,50.375,32.166 +2020-03-03 12:15:00,134.12,177.7,50.375,32.166 +2020-03-03 12:30:00,134.26,177.143,50.375,32.166 +2020-03-03 12:45:00,136.24,178.02,50.375,32.166 +2020-03-03 13:00:00,132.14,177.34099999999998,50.735,32.166 +2020-03-03 13:15:00,140.28,175.817,50.735,32.166 +2020-03-03 13:30:00,140.27,174.355,50.735,32.166 +2020-03-03 13:45:00,138.43,173.81900000000002,50.735,32.166 +2020-03-03 14:00:00,134.59,175.283,50.946000000000005,32.166 +2020-03-03 14:15:00,133.48,174.597,50.946000000000005,32.166 +2020-03-03 14:30:00,134.65,174.787,50.946000000000005,32.166 +2020-03-03 14:45:00,139.12,175.71,50.946000000000005,32.166 +2020-03-03 15:00:00,138.89,177.46400000000003,53.18,32.166 +2020-03-03 15:15:00,136.59,176.313,53.18,32.166 +2020-03-03 15:30:00,133.42,176.609,53.18,32.166 +2020-03-03 15:45:00,137.07,176.822,53.18,32.166 +2020-03-03 16:00:00,138.31,180.324,54.928999999999995,32.166 +2020-03-03 16:15:00,137.5,182.165,54.928999999999995,32.166 +2020-03-03 16:30:00,135.91,184.196,54.928999999999995,32.166 +2020-03-03 16:45:00,136.81,184.774,54.928999999999995,32.166 +2020-03-03 17:00:00,138.14,187.50099999999998,60.913000000000004,32.166 +2020-03-03 17:15:00,138.47,190.11700000000002,60.913000000000004,32.166 +2020-03-03 17:30:00,137.24,192.908,60.913000000000004,32.166 +2020-03-03 17:45:00,134.06,193.915,60.913000000000004,32.166 +2020-03-03 18:00:00,143.82,197.111,62.214,32.166 +2020-03-03 18:15:00,146.3,197.49,62.214,32.166 +2020-03-03 18:30:00,146.08,195.918,62.214,32.166 +2020-03-03 18:45:00,142.83,197.834,62.214,32.166 +2020-03-03 19:00:00,139.19,196.77599999999998,62.38,32.166 +2020-03-03 19:15:00,140.37,193.695,62.38,32.166 +2020-03-03 19:30:00,138.01,192.799,62.38,32.166 +2020-03-03 19:45:00,138.66,189.58599999999998,62.38,32.166 +2020-03-03 20:00:00,130.23,184.28,65.018,32.166 +2020-03-03 20:15:00,123.06,179.03400000000002,65.018,32.166 +2020-03-03 20:30:00,123.81,175.297,65.018,32.166 +2020-03-03 20:45:00,121.62,172.838,65.018,32.166 +2020-03-03 21:00:00,117.63,169.65599999999998,56.416000000000004,32.166 +2020-03-03 21:15:00,108.75,166.425,56.416000000000004,32.166 +2020-03-03 21:30:00,108.4,164.136,56.416000000000004,32.166 +2020-03-03 21:45:00,110.03,163.938,56.416000000000004,32.166 +2020-03-03 22:00:00,107.4,156.55,52.846000000000004,32.166 +2020-03-03 22:15:00,105.11,152.477,52.846000000000004,32.166 +2020-03-03 22:30:00,98.01,136.759,52.846000000000004,32.166 +2020-03-03 22:45:00,100.15,128.994,52.846000000000004,32.166 +2020-03-03 23:00:00,96.08,122.352,44.435,32.166 +2020-03-03 23:15:00,96.98,120.572,44.435,32.166 +2020-03-03 23:30:00,89.43,122.104,44.435,32.166 +2020-03-03 23:45:00,87.12,122.21,44.435,32.166 +2020-03-04 00:00:00,83.92,117.189,42.527,32.166 +2020-03-04 00:15:00,81.31,115.42299999999999,42.527,32.166 +2020-03-04 00:30:00,85.2,114.69200000000001,42.527,32.166 +2020-03-04 00:45:00,87.61,114.60600000000001,42.527,32.166 +2020-03-04 01:00:00,83.3,116.243,38.655,32.166 +2020-03-04 01:15:00,83.96,116.72,38.655,32.166 +2020-03-04 01:30:00,76.44,116.84299999999999,38.655,32.166 +2020-03-04 01:45:00,77.77,116.96,38.655,32.166 +2020-03-04 02:00:00,77.27,118.90100000000001,36.912,32.166 +2020-03-04 02:15:00,79.12,119.867,36.912,32.166 +2020-03-04 02:30:00,84.05,120.65,36.912,32.166 +2020-03-04 02:45:00,84.7,122.397,36.912,32.166 +2020-03-04 03:00:00,84.41,124.934,36.98,32.166 +2020-03-04 03:15:00,86.26,126.76,36.98,32.166 +2020-03-04 03:30:00,87.42,128.709,36.98,32.166 +2020-03-04 03:45:00,87.57,129.838,36.98,32.166 +2020-03-04 04:00:00,83.7,143.138,38.052,32.166 +2020-03-04 04:15:00,88.51,156.252,38.052,32.166 +2020-03-04 04:30:00,91.89,158.232,38.052,32.166 +2020-03-04 04:45:00,93.1,160.07,38.052,32.166 +2020-03-04 05:00:00,91.47,194.645,42.455,32.166 +2020-03-04 05:15:00,91.38,225.574,42.455,32.166 +2020-03-04 05:30:00,94.97,219.925,42.455,32.166 +2020-03-04 05:45:00,95.93,211.703,42.455,32.166 +2020-03-04 06:00:00,106.42,208.898,57.986000000000004,32.166 +2020-03-04 06:15:00,112.03,215.11,57.986000000000004,32.166 +2020-03-04 06:30:00,114.0,217.014,57.986000000000004,32.166 +2020-03-04 06:45:00,116.7,220.18200000000002,57.986000000000004,32.166 +2020-03-04 07:00:00,122.52,222.373,71.868,32.166 +2020-03-04 07:15:00,122.72,225.428,71.868,32.166 +2020-03-04 07:30:00,125.92,225.655,71.868,32.166 +2020-03-04 07:45:00,128.12,224.088,71.868,32.166 +2020-03-04 08:00:00,130.83,222.176,62.225,32.166 +2020-03-04 08:15:00,130.93,220.354,62.225,32.166 +2020-03-04 08:30:00,131.84,215.78599999999997,62.225,32.166 +2020-03-04 08:45:00,131.99,211.262,62.225,32.166 +2020-03-04 09:00:00,133.12,203.688,58.802,32.166 +2020-03-04 09:15:00,134.1,200.15599999999998,58.802,32.166 +2020-03-04 09:30:00,135.91,198.843,58.802,32.166 +2020-03-04 09:45:00,136.01,196.042,58.802,32.166 +2020-03-04 10:00:00,135.9,193.19299999999998,54.122,32.166 +2020-03-04 10:15:00,137.47,189.97799999999998,54.122,32.166 +2020-03-04 10:30:00,136.84,187.53799999999998,54.122,32.166 +2020-03-04 10:45:00,137.85,186.72799999999998,54.122,32.166 +2020-03-04 11:00:00,137.84,183.793,54.368,32.166 +2020-03-04 11:15:00,140.09,183.22099999999998,54.368,32.166 +2020-03-04 11:30:00,140.75,182.81599999999997,54.368,32.166 +2020-03-04 11:45:00,141.22,182.395,54.368,32.166 +2020-03-04 12:00:00,138.17,177.02900000000002,52.74,32.166 +2020-03-04 12:15:00,140.05,177.263,52.74,32.166 +2020-03-04 12:30:00,136.13,176.671,52.74,32.166 +2020-03-04 12:45:00,137.21,177.545,52.74,32.166 +2020-03-04 13:00:00,141.05,176.90599999999998,52.544,32.166 +2020-03-04 13:15:00,139.87,175.361,52.544,32.166 +2020-03-04 13:30:00,125.18,173.886,52.544,32.166 +2020-03-04 13:45:00,130.05,173.352,52.544,32.166 +2020-03-04 14:00:00,125.22,174.88400000000001,53.602,32.166 +2020-03-04 14:15:00,123.48,174.174,53.602,32.166 +2020-03-04 14:30:00,126.37,174.335,53.602,32.166 +2020-03-04 14:45:00,123.47,175.268,53.602,32.166 +2020-03-04 15:00:00,121.69,177.032,55.59,32.166 +2020-03-04 15:15:00,118.75,175.852,55.59,32.166 +2020-03-04 15:30:00,116.12,176.09900000000002,55.59,32.166 +2020-03-04 15:45:00,120.18,176.293,55.59,32.166 +2020-03-04 16:00:00,123.92,179.798,57.586999999999996,32.166 +2020-03-04 16:15:00,124.85,181.62099999999998,57.586999999999996,32.166 +2020-03-04 16:30:00,120.88,183.65099999999998,57.586999999999996,32.166 +2020-03-04 16:45:00,124.56,184.18900000000002,57.586999999999996,32.166 +2020-03-04 17:00:00,128.84,186.928,62.111999999999995,32.166 +2020-03-04 17:15:00,130.98,189.553,62.111999999999995,32.166 +2020-03-04 17:30:00,131.7,192.36900000000003,62.111999999999995,32.166 +2020-03-04 17:45:00,131.94,193.391,62.111999999999995,32.166 +2020-03-04 18:00:00,138.98,196.595,64.605,32.166 +2020-03-04 18:15:00,143.1,197.04,64.605,32.166 +2020-03-04 18:30:00,145.58,195.46400000000003,64.605,32.166 +2020-03-04 18:45:00,139.57,197.40099999999998,64.605,32.166 +2020-03-04 19:00:00,139.2,196.305,65.55199999999999,32.166 +2020-03-04 19:15:00,137.12,193.24200000000002,65.55199999999999,32.166 +2020-03-04 19:30:00,134.82,192.373,65.55199999999999,32.166 +2020-03-04 19:45:00,128.94,189.204,65.55199999999999,32.166 +2020-03-04 20:00:00,125.39,183.868,66.778,32.166 +2020-03-04 20:15:00,122.78,178.637,66.778,32.166 +2020-03-04 20:30:00,118.75,174.926,66.778,32.166 +2020-03-04 20:45:00,115.74,172.472,66.778,32.166 +2020-03-04 21:00:00,113.84,169.278,56.103,32.166 +2020-03-04 21:15:00,111.66,166.042,56.103,32.166 +2020-03-04 21:30:00,107.31,163.753,56.103,32.166 +2020-03-04 21:45:00,108.44,163.576,56.103,32.166 +2020-03-04 22:00:00,103.17,156.173,51.371,32.166 +2020-03-04 22:15:00,102.87,152.128,51.371,32.166 +2020-03-04 22:30:00,93.83,136.359,51.371,32.166 +2020-03-04 22:45:00,94.61,128.59799999999998,51.371,32.166 +2020-03-04 23:00:00,93.52,121.95,42.798,32.166 +2020-03-04 23:15:00,93.24,120.189,42.798,32.166 +2020-03-04 23:30:00,86.27,121.729,42.798,32.166 +2020-03-04 23:45:00,85.18,121.86,42.798,32.166 +2020-03-05 00:00:00,79.97,116.865,39.069,32.166 +2020-03-05 00:15:00,84.04,115.10700000000001,39.069,32.166 +2020-03-05 00:30:00,86.08,114.35799999999999,39.069,32.166 +2020-03-05 00:45:00,83.85,114.272,39.069,32.166 +2020-03-05 01:00:00,75.83,115.867,37.043,32.166 +2020-03-05 01:15:00,75.46,116.329,37.043,32.166 +2020-03-05 01:30:00,75.14,116.435,37.043,32.166 +2020-03-05 01:45:00,77.65,116.557,37.043,32.166 +2020-03-05 02:00:00,79.69,118.488,34.625,32.166 +2020-03-05 02:15:00,80.69,119.448,34.625,32.166 +2020-03-05 02:30:00,78.05,120.249,34.625,32.166 +2020-03-05 02:45:00,76.94,121.99700000000001,34.625,32.166 +2020-03-05 03:00:00,77.22,124.545,33.812,32.166 +2020-03-05 03:15:00,82.54,126.35600000000001,33.812,32.166 +2020-03-05 03:30:00,83.02,128.299,33.812,32.166 +2020-03-05 03:45:00,80.83,129.442,33.812,32.166 +2020-03-05 04:00:00,78.0,142.746,35.236999999999995,32.166 +2020-03-05 04:15:00,80.68,155.84799999999998,35.236999999999995,32.166 +2020-03-05 04:30:00,86.04,157.845,35.236999999999995,32.166 +2020-03-05 04:45:00,87.84,159.67,35.236999999999995,32.166 +2020-03-05 05:00:00,88.83,194.22299999999998,40.375,32.166 +2020-03-05 05:15:00,87.34,225.169,40.375,32.166 +2020-03-05 05:30:00,90.9,219.488,40.375,32.166 +2020-03-05 05:45:00,95.15,211.271,40.375,32.166 +2020-03-05 06:00:00,105.43,208.47400000000002,52.316,32.166 +2020-03-05 06:15:00,108.76,214.69299999999998,52.316,32.166 +2020-03-05 06:30:00,115.44,216.55200000000002,52.316,32.166 +2020-03-05 06:45:00,115.49,219.701,52.316,32.166 +2020-03-05 07:00:00,122.69,221.91299999999998,64.115,32.166 +2020-03-05 07:15:00,123.41,224.935,64.115,32.166 +2020-03-05 07:30:00,125.69,225.12099999999998,64.115,32.166 +2020-03-05 07:45:00,128.28,223.512,64.115,32.166 +2020-03-05 08:00:00,131.57,221.575,55.033,32.166 +2020-03-05 08:15:00,127.69,219.74099999999999,55.033,32.166 +2020-03-05 08:30:00,127.62,215.11599999999999,55.033,32.166 +2020-03-05 08:45:00,124.84,210.609,55.033,32.166 +2020-03-05 09:00:00,122.67,203.046,49.411,32.166 +2020-03-05 09:15:00,118.75,199.516,49.411,32.166 +2020-03-05 09:30:00,118.32,198.22400000000002,49.411,32.166 +2020-03-05 09:45:00,118.04,195.43599999999998,49.411,32.166 +2020-03-05 10:00:00,117.58,192.6,45.82899999999999,32.166 +2020-03-05 10:15:00,119.95,189.429,45.82899999999999,32.166 +2020-03-05 10:30:00,123.89,187.00799999999998,45.82899999999999,32.166 +2020-03-05 10:45:00,127.92,186.218,45.82899999999999,32.166 +2020-03-05 11:00:00,124.66,183.275,44.333,32.166 +2020-03-05 11:15:00,129.25,182.722,44.333,32.166 +2020-03-05 11:30:00,117.7,182.325,44.333,32.166 +2020-03-05 11:45:00,115.76,181.92,44.333,32.166 +2020-03-05 12:00:00,113.86,176.574,42.95,32.166 +2020-03-05 12:15:00,115.98,176.822,42.95,32.166 +2020-03-05 12:30:00,123.05,176.19299999999998,42.95,32.166 +2020-03-05 12:45:00,119.31,177.06400000000002,42.95,32.166 +2020-03-05 13:00:00,116.22,176.468,42.489,32.166 +2020-03-05 13:15:00,117.03,174.90099999999998,42.489,32.166 +2020-03-05 13:30:00,114.34,173.41299999999998,42.489,32.166 +2020-03-05 13:45:00,112.98,172.882,42.489,32.166 +2020-03-05 14:00:00,120.48,174.481,43.448,32.166 +2020-03-05 14:15:00,125.43,173.747,43.448,32.166 +2020-03-05 14:30:00,124.36,173.87900000000002,43.448,32.166 +2020-03-05 14:45:00,124.56,174.821,43.448,32.166 +2020-03-05 15:00:00,120.75,176.59599999999998,45.994,32.166 +2020-03-05 15:15:00,117.53,175.38400000000001,45.994,32.166 +2020-03-05 15:30:00,125.28,175.582,45.994,32.166 +2020-03-05 15:45:00,123.67,175.75900000000001,45.994,32.166 +2020-03-05 16:00:00,124.31,179.269,48.167,32.166 +2020-03-05 16:15:00,121.46,181.071,48.167,32.166 +2020-03-05 16:30:00,123.02,183.101,48.167,32.166 +2020-03-05 16:45:00,120.81,183.59900000000002,48.167,32.166 +2020-03-05 17:00:00,126.49,186.34900000000002,52.637,32.166 +2020-03-05 17:15:00,122.58,188.981,52.637,32.166 +2020-03-05 17:30:00,129.9,191.82299999999998,52.637,32.166 +2020-03-05 17:45:00,131.12,192.859,52.637,32.166 +2020-03-05 18:00:00,136.47,196.07,55.739,32.166 +2020-03-05 18:15:00,136.58,196.582,55.739,32.166 +2020-03-05 18:30:00,142.41,195.00400000000002,55.739,32.166 +2020-03-05 18:45:00,143.42,196.96200000000002,55.739,32.166 +2020-03-05 19:00:00,139.76,195.826,56.36600000000001,32.166 +2020-03-05 19:15:00,135.79,192.78099999999998,56.36600000000001,32.166 +2020-03-05 19:30:00,135.89,191.94099999999997,56.36600000000001,32.166 +2020-03-05 19:45:00,135.03,188.817,56.36600000000001,32.166 +2020-03-05 20:00:00,127.56,183.449,56.338,32.166 +2020-03-05 20:15:00,124.1,178.235,56.338,32.166 +2020-03-05 20:30:00,121.73,174.549,56.338,32.166 +2020-03-05 20:45:00,117.92,172.1,56.338,32.166 +2020-03-05 21:00:00,109.2,168.894,49.894,32.166 +2020-03-05 21:15:00,112.76,165.65400000000002,49.894,32.166 +2020-03-05 21:30:00,110.85,163.363,49.894,32.166 +2020-03-05 21:45:00,109.7,163.209,49.894,32.166 +2020-03-05 22:00:00,101.13,155.79,46.687,32.166 +2020-03-05 22:15:00,103.31,151.773,46.687,32.166 +2020-03-05 22:30:00,97.08,135.952,46.687,32.166 +2020-03-05 22:45:00,93.57,128.194,46.687,32.166 +2020-03-05 23:00:00,85.09,121.542,39.211,32.166 +2020-03-05 23:15:00,84.64,119.8,39.211,32.166 +2020-03-05 23:30:00,87.76,121.34700000000001,39.211,32.166 +2020-03-05 23:45:00,89.95,121.505,39.211,32.166 +2020-03-06 00:00:00,84.63,115.272,36.616,32.166 +2020-03-06 00:15:00,80.02,113.741,36.616,32.166 +2020-03-06 00:30:00,78.58,112.88,36.616,32.166 +2020-03-06 00:45:00,79.97,112.955,36.616,32.166 +2020-03-06 01:00:00,81.81,114.15899999999999,33.799,32.166 +2020-03-06 01:15:00,83.15,115.40899999999999,33.799,32.166 +2020-03-06 01:30:00,81.42,115.375,33.799,32.166 +2020-03-06 01:45:00,77.29,115.573,33.799,32.166 +2020-03-06 02:00:00,77.39,117.697,32.968,32.166 +2020-03-06 02:15:00,82.75,118.53200000000001,32.968,32.166 +2020-03-06 02:30:00,81.3,119.965,32.968,32.166 +2020-03-06 02:45:00,79.68,121.664,32.968,32.166 +2020-03-06 03:00:00,75.05,123.374,33.533,32.166 +2020-03-06 03:15:00,80.07,125.906,33.533,32.166 +2020-03-06 03:30:00,83.67,127.795,33.533,32.166 +2020-03-06 03:45:00,79.45,129.38,33.533,32.166 +2020-03-06 04:00:00,79.38,142.92700000000002,36.102,32.166 +2020-03-06 04:15:00,84.68,155.572,36.102,32.166 +2020-03-06 04:30:00,86.79,157.934,36.102,32.166 +2020-03-06 04:45:00,89.75,158.566,36.102,32.166 +2020-03-06 05:00:00,92.61,191.81799999999998,42.423,32.166 +2020-03-06 05:15:00,88.55,224.35,42.423,32.166 +2020-03-06 05:30:00,92.57,219.692,42.423,32.166 +2020-03-06 05:45:00,95.18,211.36,42.423,32.166 +2020-03-06 06:00:00,105.52,209.019,55.38,32.166 +2020-03-06 06:15:00,109.46,213.859,55.38,32.166 +2020-03-06 06:30:00,110.06,214.83900000000003,55.38,32.166 +2020-03-06 06:45:00,112.44,219.50599999999997,55.38,32.166 +2020-03-06 07:00:00,119.09,221.028,65.929,32.166 +2020-03-06 07:15:00,120.34,225.11900000000003,65.929,32.166 +2020-03-06 07:30:00,122.16,224.828,65.929,32.166 +2020-03-06 07:45:00,120.89,222.30200000000002,65.929,32.166 +2020-03-06 08:00:00,121.85,219.385,57.336999999999996,32.166 +2020-03-06 08:15:00,119.68,217.25400000000002,57.336999999999996,32.166 +2020-03-06 08:30:00,118.65,213.497,57.336999999999996,32.166 +2020-03-06 08:45:00,117.07,207.47099999999998,57.336999999999996,32.166 +2020-03-06 09:00:00,115.36,200.05700000000002,54.226000000000006,32.166 +2020-03-06 09:15:00,114.99,197.328,54.226000000000006,32.166 +2020-03-06 09:30:00,115.37,195.553,54.226000000000006,32.166 +2020-03-06 09:45:00,113.84,192.722,54.226000000000006,32.166 +2020-03-06 10:00:00,112.66,188.787,51.298,32.166 +2020-03-06 10:15:00,112.59,186.301,51.298,32.166 +2020-03-06 10:30:00,112.69,183.907,51.298,32.166 +2020-03-06 10:45:00,112.62,182.697,51.298,32.166 +2020-03-06 11:00:00,113.85,179.75400000000002,50.839,32.166 +2020-03-06 11:15:00,114.18,178.215,50.839,32.166 +2020-03-06 11:30:00,118.13,179.46599999999998,50.839,32.166 +2020-03-06 11:45:00,115.78,178.99900000000002,50.839,32.166 +2020-03-06 12:00:00,110.8,174.774,47.976000000000006,32.166 +2020-03-06 12:15:00,112.96,172.933,47.976000000000006,32.166 +2020-03-06 12:30:00,107.04,172.435,47.976000000000006,32.166 +2020-03-06 12:45:00,113.31,173.696,47.976000000000006,32.166 +2020-03-06 13:00:00,113.56,174.122,46.299,32.166 +2020-03-06 13:15:00,112.83,173.34400000000002,46.299,32.166 +2020-03-06 13:30:00,105.26,171.96599999999998,46.299,32.166 +2020-03-06 13:45:00,104.96,171.417,46.299,32.166 +2020-03-06 14:00:00,107.2,171.88099999999997,44.971000000000004,32.166 +2020-03-06 14:15:00,110.28,171.00900000000001,44.971000000000004,32.166 +2020-03-06 14:30:00,113.01,171.803,44.971000000000004,32.166 +2020-03-06 14:45:00,113.67,172.955,44.971000000000004,32.166 +2020-03-06 15:00:00,114.93,174.264,47.48,32.166 +2020-03-06 15:15:00,115.31,172.56799999999998,47.48,32.166 +2020-03-06 15:30:00,123.49,171.176,47.48,32.166 +2020-03-06 15:45:00,122.8,171.57299999999998,47.48,32.166 +2020-03-06 16:00:00,120.52,173.84599999999998,50.648,32.166 +2020-03-06 16:15:00,118.33,175.97,50.648,32.166 +2020-03-06 16:30:00,123.41,178.06799999999998,50.648,32.166 +2020-03-06 16:45:00,122.27,178.315,50.648,32.166 +2020-03-06 17:00:00,123.95,181.48,56.251000000000005,32.166 +2020-03-06 17:15:00,125.14,183.697,56.251000000000005,32.166 +2020-03-06 17:30:00,127.64,186.27200000000002,56.251000000000005,32.166 +2020-03-06 17:45:00,129.37,187.075,56.251000000000005,32.166 +2020-03-06 18:00:00,132.2,190.96,58.982,32.166 +2020-03-06 18:15:00,134.17,191.049,58.982,32.166 +2020-03-06 18:30:00,139.97,189.84,58.982,32.166 +2020-03-06 18:45:00,137.26,191.862,58.982,32.166 +2020-03-06 19:00:00,138.5,191.671,57.293,32.166 +2020-03-06 19:15:00,136.65,190.049,57.293,32.166 +2020-03-06 19:30:00,132.19,188.83900000000003,57.293,32.166 +2020-03-06 19:45:00,128.54,185.19400000000002,57.293,32.166 +2020-03-06 20:00:00,118.1,179.83599999999998,59.433,32.166 +2020-03-06 20:15:00,120.22,174.738,59.433,32.166 +2020-03-06 20:30:00,116.22,170.981,59.433,32.166 +2020-03-06 20:45:00,116.32,168.983,59.433,32.166 +2020-03-06 21:00:00,108.24,166.416,52.153999999999996,32.166 +2020-03-06 21:15:00,107.96,163.793,52.153999999999996,32.166 +2020-03-06 21:30:00,103.98,161.531,52.153999999999996,32.166 +2020-03-06 21:45:00,100.92,161.953,52.153999999999996,32.166 +2020-03-06 22:00:00,101.26,155.461,47.125,32.166 +2020-03-06 22:15:00,98.2,151.312,47.125,32.166 +2020-03-06 22:30:00,91.48,142.222,47.125,32.166 +2020-03-06 22:45:00,86.47,138.014,47.125,32.166 +2020-03-06 23:00:00,80.32,131.118,41.236000000000004,32.166 +2020-03-06 23:15:00,86.56,127.30799999999999,41.236000000000004,32.166 +2020-03-06 23:30:00,86.05,127.225,41.236000000000004,32.166 +2020-03-06 23:45:00,84.17,126.74600000000001,41.236000000000004,32.166 +2020-03-07 00:00:00,73.36,112.361,36.484,31.988000000000003 +2020-03-07 00:15:00,72.69,106.662,36.484,31.988000000000003 +2020-03-07 00:30:00,73.63,106.99600000000001,36.484,31.988000000000003 +2020-03-07 00:45:00,79.66,107.662,36.484,31.988000000000003 +2020-03-07 01:00:00,75.3,109.476,32.391999999999996,31.988000000000003 +2020-03-07 01:15:00,77.06,109.846,32.391999999999996,31.988000000000003 +2020-03-07 01:30:00,71.97,109.189,32.391999999999996,31.988000000000003 +2020-03-07 01:45:00,68.58,109.354,32.391999999999996,31.988000000000003 +2020-03-07 02:00:00,73.65,112.00200000000001,30.194000000000003,31.988000000000003 +2020-03-07 02:15:00,74.65,112.369,30.194000000000003,31.988000000000003 +2020-03-07 02:30:00,69.35,112.65100000000001,30.194000000000003,31.988000000000003 +2020-03-07 02:45:00,68.86,114.555,30.194000000000003,31.988000000000003 +2020-03-07 03:00:00,66.7,116.7,29.677,31.988000000000003 +2020-03-07 03:15:00,65.75,117.959,29.677,31.988000000000003 +2020-03-07 03:30:00,70.1,118.37299999999999,29.677,31.988000000000003 +2020-03-07 03:45:00,72.68,120.28200000000001,29.677,31.988000000000003 +2020-03-07 04:00:00,68.24,129.653,29.616,31.988000000000003 +2020-03-07 04:15:00,67.2,139.745,29.616,31.988000000000003 +2020-03-07 04:30:00,67.39,139.829,29.616,31.988000000000003 +2020-03-07 04:45:00,67.69,140.015,29.616,31.988000000000003 +2020-03-07 05:00:00,67.99,156.97299999999998,29.625,31.988000000000003 +2020-03-07 05:15:00,67.86,169.953,29.625,31.988000000000003 +2020-03-07 05:30:00,67.93,165.817,29.625,31.988000000000003 +2020-03-07 05:45:00,70.0,163.291,29.625,31.988000000000003 +2020-03-07 06:00:00,71.25,180.298,30.551,31.988000000000003 +2020-03-07 06:15:00,72.04,201.768,30.551,31.988000000000003 +2020-03-07 06:30:00,72.04,197.2,30.551,31.988000000000003 +2020-03-07 06:45:00,72.96,192.865,30.551,31.988000000000003 +2020-03-07 07:00:00,76.51,190.575,34.865,31.988000000000003 +2020-03-07 07:15:00,78.94,193.25799999999998,34.865,31.988000000000003 +2020-03-07 07:30:00,81.32,195.71099999999998,34.865,31.988000000000003 +2020-03-07 07:45:00,83.9,197.054,34.865,31.988000000000003 +2020-03-07 08:00:00,88.57,197.97299999999998,41.456,31.988000000000003 +2020-03-07 08:15:00,89.24,199.271,41.456,31.988000000000003 +2020-03-07 08:30:00,90.68,196.99599999999998,41.456,31.988000000000003 +2020-03-07 08:45:00,94.24,194.13099999999997,41.456,31.988000000000003 +2020-03-07 09:00:00,96.14,188.805,43.001999999999995,31.988000000000003 +2020-03-07 09:15:00,98.61,186.863,43.001999999999995,31.988000000000003 +2020-03-07 09:30:00,102.12,186.041,43.001999999999995,31.988000000000003 +2020-03-07 09:45:00,98.59,183.317,43.001999999999995,31.988000000000003 +2020-03-07 10:00:00,95.01,179.757,42.047,31.988000000000003 +2020-03-07 10:15:00,98.58,177.519,42.047,31.988000000000003 +2020-03-07 10:30:00,95.97,175.25900000000001,42.047,31.988000000000003 +2020-03-07 10:45:00,94.89,175.265,42.047,31.988000000000003 +2020-03-07 11:00:00,95.66,172.479,39.894,31.988000000000003 +2020-03-07 11:15:00,96.05,170.417,39.894,31.988000000000003 +2020-03-07 11:30:00,95.32,170.642,39.894,31.988000000000003 +2020-03-07 11:45:00,95.01,169.364,39.894,31.988000000000003 +2020-03-07 12:00:00,95.78,164.368,38.122,31.988000000000003 +2020-03-07 12:15:00,92.41,163.267,38.122,31.988000000000003 +2020-03-07 12:30:00,86.18,163.02,38.122,31.988000000000003 +2020-03-07 12:45:00,85.12,163.636,38.122,31.988000000000003 +2020-03-07 13:00:00,87.26,163.61,34.645,31.988000000000003 +2020-03-07 13:15:00,86.32,160.77,34.645,31.988000000000003 +2020-03-07 13:30:00,84.06,158.97299999999998,34.645,31.988000000000003 +2020-03-07 13:45:00,83.33,158.702,34.645,31.988000000000003 +2020-03-07 14:00:00,77.3,160.404,33.739000000000004,31.988000000000003 +2020-03-07 14:15:00,74.26,158.83,33.739000000000004,31.988000000000003 +2020-03-07 14:30:00,74.73,157.79399999999998,33.739000000000004,31.988000000000003 +2020-03-07 14:45:00,79.24,159.227,33.739000000000004,31.988000000000003 +2020-03-07 15:00:00,79.87,161.2,35.908,31.988000000000003 +2020-03-07 15:15:00,82.29,160.34,35.908,31.988000000000003 +2020-03-07 15:30:00,82.88,160.406,35.908,31.988000000000003 +2020-03-07 15:45:00,83.16,160.681,35.908,31.988000000000003 +2020-03-07 16:00:00,82.92,162.039,39.249,31.988000000000003 +2020-03-07 16:15:00,82.39,164.894,39.249,31.988000000000003 +2020-03-07 16:30:00,84.42,166.97,39.249,31.988000000000003 +2020-03-07 16:45:00,86.48,168.02700000000002,39.249,31.988000000000003 +2020-03-07 17:00:00,89.54,170.46900000000002,46.045,31.988000000000003 +2020-03-07 17:15:00,90.25,174.037,46.045,31.988000000000003 +2020-03-07 17:30:00,93.17,176.52700000000002,46.045,31.988000000000003 +2020-03-07 17:45:00,96.07,177.01,46.045,31.988000000000003 +2020-03-07 18:00:00,104.2,180.562,48.238,31.988000000000003 +2020-03-07 18:15:00,106.48,182.665,48.238,31.988000000000003 +2020-03-07 18:30:00,104.75,182.90599999999998,48.238,31.988000000000003 +2020-03-07 18:45:00,105.22,181.287,48.238,31.988000000000003 +2020-03-07 19:00:00,104.77,181.752,46.785,31.988000000000003 +2020-03-07 19:15:00,101.74,179.548,46.785,31.988000000000003 +2020-03-07 19:30:00,103.23,179.188,46.785,31.988000000000003 +2020-03-07 19:45:00,99.14,175.61599999999999,46.785,31.988000000000003 +2020-03-07 20:00:00,94.25,172.472,39.830999999999996,31.988000000000003 +2020-03-07 20:15:00,90.48,169.38,39.830999999999996,31.988000000000003 +2020-03-07 20:30:00,88.69,165.206,39.830999999999996,31.988000000000003 +2020-03-07 20:45:00,89.27,163.041,39.830999999999996,31.988000000000003 +2020-03-07 21:00:00,82.31,162.475,34.063,31.988000000000003 +2020-03-07 21:15:00,82.13,160.234,34.063,31.988000000000003 +2020-03-07 21:30:00,81.1,159.189,34.063,31.988000000000003 +2020-03-07 21:45:00,79.97,159.173,34.063,31.988000000000003 +2020-03-07 22:00:00,79.99,153.97299999999998,34.455999999999996,31.988000000000003 +2020-03-07 22:15:00,77.88,152.298,34.455999999999996,31.988000000000003 +2020-03-07 22:30:00,73.56,149.155,34.455999999999996,31.988000000000003 +2020-03-07 22:45:00,72.67,146.81,34.455999999999996,31.988000000000003 +2020-03-07 23:00:00,68.96,142.2,27.840999999999998,31.988000000000003 +2020-03-07 23:15:00,69.09,136.869,27.840999999999998,31.988000000000003 +2020-03-07 23:30:00,65.76,135.38299999999998,27.840999999999998,31.988000000000003 +2020-03-07 23:45:00,64.86,132.614,27.840999999999998,31.988000000000003 +2020-03-08 00:00:00,61.0,112.605,20.007,31.988000000000003 +2020-03-08 00:15:00,61.21,106.455,20.007,31.988000000000003 +2020-03-08 00:30:00,60.15,106.39200000000001,20.007,31.988000000000003 +2020-03-08 00:45:00,60.07,107.709,20.007,31.988000000000003 +2020-03-08 01:00:00,56.47,109.37899999999999,17.378,31.988000000000003 +2020-03-08 01:15:00,57.99,110.706,17.378,31.988000000000003 +2020-03-08 01:30:00,57.0,110.509,17.378,31.988000000000003 +2020-03-08 01:45:00,57.02,110.329,17.378,31.988000000000003 +2020-03-08 02:00:00,55.64,112.262,16.145,31.988000000000003 +2020-03-08 02:15:00,56.91,111.91,16.145,31.988000000000003 +2020-03-08 02:30:00,56.07,113.05,16.145,31.988000000000003 +2020-03-08 02:45:00,56.9,115.353,16.145,31.988000000000003 +2020-03-08 03:00:00,55.26,117.9,15.427999999999999,31.988000000000003 +2020-03-08 03:15:00,55.5,118.68299999999999,15.427999999999999,31.988000000000003 +2020-03-08 03:30:00,53.73,120.31200000000001,15.427999999999999,31.988000000000003 +2020-03-08 03:45:00,55.64,122.04700000000001,15.427999999999999,31.988000000000003 +2020-03-08 04:00:00,55.63,131.18200000000002,16.663,31.988000000000003 +2020-03-08 04:15:00,54.6,140.243,16.663,31.988000000000003 +2020-03-08 04:30:00,57.94,140.602,16.663,31.988000000000003 +2020-03-08 04:45:00,58.3,140.964,16.663,31.988000000000003 +2020-03-08 05:00:00,57.65,154.621,17.271,31.988000000000003 +2020-03-08 05:15:00,59.1,165.231,17.271,31.988000000000003 +2020-03-08 05:30:00,56.61,160.846,17.271,31.988000000000003 +2020-03-08 05:45:00,59.93,158.515,17.271,31.988000000000003 +2020-03-08 06:00:00,60.8,175.07299999999998,17.612000000000002,31.988000000000003 +2020-03-08 06:15:00,60.98,195.047,17.612000000000002,31.988000000000003 +2020-03-08 06:30:00,59.58,189.28799999999998,17.612000000000002,31.988000000000003 +2020-03-08 06:45:00,60.73,183.79,17.612000000000002,31.988000000000003 +2020-03-08 07:00:00,64.07,183.87099999999998,20.88,31.988000000000003 +2020-03-08 07:15:00,64.46,185.475,20.88,31.988000000000003 +2020-03-08 07:30:00,67.14,186.912,20.88,31.988000000000003 +2020-03-08 07:45:00,68.83,187.49099999999999,20.88,31.988000000000003 +2020-03-08 08:00:00,70.55,190.217,25.861,31.988000000000003 +2020-03-08 08:15:00,68.92,191.58599999999998,25.861,31.988000000000003 +2020-03-08 08:30:00,68.33,190.949,25.861,31.988000000000003 +2020-03-08 08:45:00,68.61,190.00599999999997,25.861,31.988000000000003 +2020-03-08 09:00:00,68.42,184.287,27.921999999999997,31.988000000000003 +2020-03-08 09:15:00,68.88,182.803,27.921999999999997,31.988000000000003 +2020-03-08 09:30:00,66.98,181.915,27.921999999999997,31.988000000000003 +2020-03-08 09:45:00,66.66,179.234,27.921999999999997,31.988000000000003 +2020-03-08 10:00:00,63.08,178.157,29.048000000000002,31.988000000000003 +2020-03-08 10:15:00,65.17,176.486,29.048000000000002,31.988000000000003 +2020-03-08 10:30:00,66.69,174.85,29.048000000000002,31.988000000000003 +2020-03-08 10:45:00,68.39,173.22299999999998,29.048000000000002,31.988000000000003 +2020-03-08 11:00:00,71.32,171.245,32.02,31.988000000000003 +2020-03-08 11:15:00,68.73,169.267,32.02,31.988000000000003 +2020-03-08 11:30:00,77.87,168.745,32.02,31.988000000000003 +2020-03-08 11:45:00,71.63,168.09599999999998,32.02,31.988000000000003 +2020-03-08 12:00:00,69.65,162.733,28.55,31.988000000000003 +2020-03-08 12:15:00,64.14,163.364,28.55,31.988000000000003 +2020-03-08 12:30:00,62.02,161.72799999999998,28.55,31.988000000000003 +2020-03-08 12:45:00,62.53,161.333,28.55,31.988000000000003 +2020-03-08 13:00:00,59.46,160.626,25.601999999999997,31.988000000000003 +2020-03-08 13:15:00,59.09,160.545,25.601999999999997,31.988000000000003 +2020-03-08 13:30:00,59.19,158.379,25.601999999999997,31.988000000000003 +2020-03-08 13:45:00,58.93,157.644,25.601999999999997,31.988000000000003 +2020-03-08 14:00:00,57.84,159.814,23.916999999999998,31.988000000000003 +2020-03-08 14:15:00,58.46,159.417,23.916999999999998,31.988000000000003 +2020-03-08 14:30:00,62.93,159.34799999999998,23.916999999999998,31.988000000000003 +2020-03-08 14:45:00,64.38,160.249,23.916999999999998,31.988000000000003 +2020-03-08 15:00:00,64.9,160.799,24.064,31.988000000000003 +2020-03-08 15:15:00,65.65,160.517,24.064,31.988000000000003 +2020-03-08 15:30:00,65.05,161.069,24.064,31.988000000000003 +2020-03-08 15:45:00,64.57,162.025,24.064,31.988000000000003 +2020-03-08 16:00:00,67.33,164.97099999999998,28.189,31.988000000000003 +2020-03-08 16:15:00,68.58,166.96400000000003,28.189,31.988000000000003 +2020-03-08 16:30:00,68.93,169.421,28.189,31.988000000000003 +2020-03-08 16:45:00,71.27,170.572,28.189,31.988000000000003 +2020-03-08 17:00:00,75.95,173.09099999999998,37.576,31.988000000000003 +2020-03-08 17:15:00,76.2,176.542,37.576,31.988000000000003 +2020-03-08 17:30:00,80.05,179.435,37.576,31.988000000000003 +2020-03-08 17:45:00,83.25,182.171,37.576,31.988000000000003 +2020-03-08 18:00:00,91.18,185.28900000000002,42.669,31.988000000000003 +2020-03-08 18:15:00,96.39,188.69400000000002,42.669,31.988000000000003 +2020-03-08 18:30:00,101.38,186.88299999999998,42.669,31.988000000000003 +2020-03-08 18:45:00,99.75,187.09,42.669,31.988000000000003 +2020-03-08 19:00:00,97.87,187.46400000000003,43.538999999999994,31.988000000000003 +2020-03-08 19:15:00,93.75,185.72400000000002,43.538999999999994,31.988000000000003 +2020-03-08 19:30:00,94.16,185.204,43.538999999999994,31.988000000000003 +2020-03-08 19:45:00,89.68,183.025,43.538999999999994,31.988000000000003 +2020-03-08 20:00:00,86.68,179.81799999999998,37.330999999999996,31.988000000000003 +2020-03-08 20:15:00,86.27,177.678,37.330999999999996,31.988000000000003 +2020-03-08 20:30:00,83.87,174.827,37.330999999999996,31.988000000000003 +2020-03-08 20:45:00,83.17,171.297,37.330999999999996,31.988000000000003 +2020-03-08 21:00:00,79.53,168.202,33.856,31.988000000000003 +2020-03-08 21:15:00,80.3,165.315,33.856,31.988000000000003 +2020-03-08 21:30:00,80.55,164.47400000000002,33.856,31.988000000000003 +2020-03-08 21:45:00,85.28,164.655,33.856,31.988000000000003 +2020-03-08 22:00:00,87.48,158.54,34.711999999999996,31.988000000000003 +2020-03-08 22:15:00,88.51,155.951,34.711999999999996,31.988000000000003 +2020-03-08 22:30:00,83.88,149.629,34.711999999999996,31.988000000000003 +2020-03-08 22:45:00,79.79,146.309,34.711999999999996,31.988000000000003 +2020-03-08 23:00:00,77.62,138.95,29.698,31.988000000000003 +2020-03-08 23:15:00,82.04,135.591,29.698,31.988000000000003 +2020-03-08 23:30:00,82.56,134.83100000000002,29.698,31.988000000000003 +2020-03-08 23:45:00,79.6,132.972,29.698,31.988000000000003 +2020-03-09 00:00:00,69.92,116.464,29.983,32.166 +2020-03-09 00:15:00,69.51,113.265,29.983,32.166 +2020-03-09 00:30:00,71.32,113.25399999999999,29.983,32.166 +2020-03-09 00:45:00,76.73,114.006,29.983,32.166 +2020-03-09 01:00:00,74.19,115.686,29.122,32.166 +2020-03-09 01:15:00,73.87,116.49700000000001,29.122,32.166 +2020-03-09 01:30:00,67.47,116.385,29.122,32.166 +2020-03-09 01:45:00,74.37,116.307,29.122,32.166 +2020-03-09 02:00:00,73.55,118.26100000000001,28.676,32.166 +2020-03-09 02:15:00,73.53,119.147,28.676,32.166 +2020-03-09 02:30:00,68.13,120.645,28.676,32.166 +2020-03-09 02:45:00,75.19,122.339,28.676,32.166 +2020-03-09 03:00:00,75.27,126.15799999999999,26.552,32.166 +2020-03-09 03:15:00,74.56,128.586,26.552,32.166 +2020-03-09 03:30:00,73.02,130.048,26.552,32.166 +2020-03-09 03:45:00,77.49,131.209,26.552,32.166 +2020-03-09 04:00:00,77.93,144.89,27.44,32.166 +2020-03-09 04:15:00,76.54,158.27700000000002,27.44,32.166 +2020-03-09 04:30:00,74.57,160.655,27.44,32.166 +2020-03-09 04:45:00,74.33,161.21,27.44,32.166 +2020-03-09 05:00:00,77.36,190.71,36.825,32.166 +2020-03-09 05:15:00,83.26,221.83599999999998,36.825,32.166 +2020-03-09 05:30:00,86.0,217.45,36.825,32.166 +2020-03-09 05:45:00,90.09,209.351,36.825,32.166 +2020-03-09 06:00:00,98.46,207.56799999999998,56.589,32.166 +2020-03-09 06:15:00,105.15,212.236,56.589,32.166 +2020-03-09 06:30:00,108.02,214.56799999999998,56.589,32.166 +2020-03-09 06:45:00,110.16,218.112,56.589,32.166 +2020-03-09 07:00:00,117.31,220.562,67.49,32.166 +2020-03-09 07:15:00,118.79,223.637,67.49,32.166 +2020-03-09 07:30:00,122.33,224.175,67.49,32.166 +2020-03-09 07:45:00,125.44,222.329,67.49,32.166 +2020-03-09 08:00:00,129.3,220.19400000000002,60.028,32.166 +2020-03-09 08:15:00,131.05,219.35,60.028,32.166 +2020-03-09 08:30:00,131.67,214.641,60.028,32.166 +2020-03-09 08:45:00,131.5,210.576,60.028,32.166 +2020-03-09 09:00:00,130.44,203.824,55.018,32.166 +2020-03-09 09:15:00,134.15,198.77900000000002,55.018,32.166 +2020-03-09 09:30:00,133.09,196.834,55.018,32.166 +2020-03-09 09:45:00,134.3,194.14,55.018,32.166 +2020-03-09 10:00:00,134.13,192.09400000000002,51.183,32.166 +2020-03-09 10:15:00,134.12,190.137,51.183,32.166 +2020-03-09 10:30:00,134.67,187.627,51.183,32.166 +2020-03-09 10:45:00,135.0,186.516,51.183,32.166 +2020-03-09 11:00:00,136.7,182.109,50.065,32.166 +2020-03-09 11:15:00,135.2,181.91099999999997,50.065,32.166 +2020-03-09 11:30:00,135.31,182.80700000000002,50.065,32.166 +2020-03-09 11:45:00,135.27,181.85299999999998,50.065,32.166 +2020-03-09 12:00:00,136.04,177.92700000000002,48.141999999999996,32.166 +2020-03-09 12:15:00,132.64,178.58700000000002,48.141999999999996,32.166 +2020-03-09 12:30:00,129.11,177.00799999999998,48.141999999999996,32.166 +2020-03-09 12:45:00,129.79,178.063,48.141999999999996,32.166 +2020-03-09 13:00:00,129.66,178.076,47.887,32.166 +2020-03-09 13:15:00,129.83,176.532,47.887,32.166 +2020-03-09 13:30:00,129.35,173.873,47.887,32.166 +2020-03-09 13:45:00,131.32,173.292,47.887,32.166 +2020-03-09 14:00:00,126.29,174.843,48.571000000000005,32.166 +2020-03-09 14:15:00,126.44,173.885,48.571000000000005,32.166 +2020-03-09 14:30:00,128.22,173.24599999999998,48.571000000000005,32.166 +2020-03-09 14:45:00,126.5,174.40099999999998,48.571000000000005,32.166 +2020-03-09 15:00:00,128.64,176.653,49.937,32.166 +2020-03-09 15:15:00,125.18,174.889,49.937,32.166 +2020-03-09 15:30:00,124.09,174.695,49.937,32.166 +2020-03-09 15:45:00,123.67,175.143,49.937,32.166 +2020-03-09 16:00:00,123.86,178.435,52.963,32.166 +2020-03-09 16:15:00,124.07,179.68099999999998,52.963,32.166 +2020-03-09 16:30:00,120.57,181.12400000000002,52.963,32.166 +2020-03-09 16:45:00,120.89,181.093,52.963,32.166 +2020-03-09 17:00:00,124.22,183.347,61.163999999999994,32.166 +2020-03-09 17:15:00,125.08,185.929,61.163999999999994,32.166 +2020-03-09 17:30:00,129.2,188.269,61.163999999999994,32.166 +2020-03-09 17:45:00,128.13,189.52599999999998,61.163999999999994,32.166 +2020-03-09 18:00:00,133.54,192.96599999999998,63.788999999999994,32.166 +2020-03-09 18:15:00,133.96,194.083,63.788999999999994,32.166 +2020-03-09 18:30:00,130.46,192.81599999999997,63.788999999999994,32.166 +2020-03-09 18:45:00,129.43,194.101,63.788999999999994,32.166 +2020-03-09 19:00:00,129.65,192.855,63.913000000000004,32.166 +2020-03-09 19:15:00,125.51,190.139,63.913000000000004,32.166 +2020-03-09 19:30:00,133.89,190.092,63.913000000000004,32.166 +2020-03-09 19:45:00,132.07,187.08,63.913000000000004,32.166 +2020-03-09 20:00:00,119.33,181.389,65.44,32.166 +2020-03-09 20:15:00,115.81,177.02200000000002,65.44,32.166 +2020-03-09 20:30:00,115.88,172.455,65.44,32.166 +2020-03-09 20:45:00,115.72,170.53,65.44,32.166 +2020-03-09 21:00:00,105.11,167.868,59.117,32.166 +2020-03-09 21:15:00,99.97,163.887,59.117,32.166 +2020-03-09 21:30:00,98.25,162.292,59.117,32.166 +2020-03-09 21:45:00,97.32,161.97799999999998,59.117,32.166 +2020-03-09 22:00:00,92.52,152.808,52.301,32.166 +2020-03-09 22:15:00,93.86,149.191,52.301,32.166 +2020-03-09 22:30:00,94.64,133.034,52.301,32.166 +2020-03-09 22:45:00,93.49,125.016,52.301,32.166 +2020-03-09 23:00:00,89.93,118.40299999999999,44.373000000000005,32.166 +2020-03-09 23:15:00,83.28,117.455,44.373000000000005,32.166 +2020-03-09 23:30:00,82.01,119.383,44.373000000000005,32.166 +2020-03-09 23:45:00,79.61,120.059,44.373000000000005,32.166 +2020-03-10 00:00:00,79.63,115.156,44.647,32.166 +2020-03-10 00:15:00,81.95,113.444,44.647,32.166 +2020-03-10 00:30:00,80.93,112.60600000000001,44.647,32.166 +2020-03-10 00:45:00,77.16,112.525,44.647,32.166 +2020-03-10 01:00:00,70.56,113.905,41.433,32.166 +2020-03-10 01:15:00,76.76,114.289,41.433,32.166 +2020-03-10 01:30:00,76.51,114.307,41.433,32.166 +2020-03-10 01:45:00,79.3,114.46,41.433,32.166 +2020-03-10 02:00:00,73.31,116.336,39.909,32.166 +2020-03-10 02:15:00,73.05,117.262,39.909,32.166 +2020-03-10 02:30:00,69.81,118.15700000000001,39.909,32.166 +2020-03-10 02:45:00,71.57,119.90700000000001,39.909,32.166 +2020-03-10 03:00:00,71.1,122.52,39.14,32.166 +2020-03-10 03:15:00,78.8,124.244,39.14,32.166 +2020-03-10 03:30:00,81.43,126.156,39.14,32.166 +2020-03-10 03:45:00,78.81,127.365,39.14,32.166 +2020-03-10 04:00:00,75.34,140.69799999999998,40.015,32.166 +2020-03-10 04:15:00,74.56,153.744,40.015,32.166 +2020-03-10 04:30:00,80.72,155.83,40.015,32.166 +2020-03-10 04:45:00,85.85,157.588,40.015,32.166 +2020-03-10 05:00:00,89.69,192.037,44.93600000000001,32.166 +2020-03-10 05:15:00,87.41,223.071,44.93600000000001,32.166 +2020-03-10 05:30:00,94.15,217.21900000000002,44.93600000000001,32.166 +2020-03-10 05:45:00,99.31,209.03400000000002,44.93600000000001,32.166 +2020-03-10 06:00:00,108.19,206.273,57.271,32.166 +2020-03-10 06:15:00,106.67,212.52,57.271,32.166 +2020-03-10 06:30:00,105.56,214.145,57.271,32.166 +2020-03-10 06:45:00,110.72,217.18900000000002,57.271,32.166 +2020-03-10 07:00:00,114.85,219.50599999999997,68.352,32.166 +2020-03-10 07:15:00,122.4,222.36,68.352,32.166 +2020-03-10 07:30:00,126.34,222.338,68.352,32.166 +2020-03-10 07:45:00,126.87,220.52700000000002,68.352,32.166 +2020-03-10 08:00:00,121.17,218.46200000000002,60.717,32.166 +2020-03-10 08:15:00,121.73,216.57,60.717,32.166 +2020-03-10 08:30:00,123.57,211.655,60.717,32.166 +2020-03-10 08:45:00,119.88,207.236,60.717,32.166 +2020-03-10 09:00:00,120.24,199.739,54.603,32.166 +2020-03-10 09:15:00,125.53,196.215,54.603,32.166 +2020-03-10 09:30:00,125.18,195.02900000000002,54.603,32.166 +2020-03-10 09:45:00,117.63,192.30599999999998,54.603,32.166 +2020-03-10 10:00:00,122.65,189.53900000000002,52.308,32.166 +2020-03-10 10:15:00,122.53,186.588,52.308,32.166 +2020-03-10 10:30:00,122.83,184.278,52.308,32.166 +2020-03-10 10:45:00,123.85,183.583,52.308,32.166 +2020-03-10 11:00:00,124.85,180.6,51.838,32.166 +2020-03-10 11:15:00,124.63,180.15099999999998,51.838,32.166 +2020-03-10 11:30:00,120.0,179.793,51.838,32.166 +2020-03-10 11:45:00,112.97,179.477,51.838,32.166 +2020-03-10 12:00:00,116.62,174.23,50.375,32.166 +2020-03-10 12:15:00,124.04,174.543,50.375,32.166 +2020-03-10 12:30:00,134.05,173.727,50.375,32.166 +2020-03-10 12:45:00,127.79,174.581,50.375,32.166 +2020-03-10 13:00:00,126.21,174.203,50.735,32.166 +2020-03-10 13:15:00,131.91,172.525,50.735,32.166 +2020-03-10 13:30:00,125.28,170.977,50.735,32.166 +2020-03-10 13:45:00,123.2,170.459,50.735,32.166 +2020-03-10 14:00:00,122.54,172.40599999999998,50.946000000000005,32.166 +2020-03-10 14:15:00,121.23,171.549,50.946000000000005,32.166 +2020-03-10 14:30:00,124.05,171.524,50.946000000000005,32.166 +2020-03-10 14:45:00,124.51,172.513,50.946000000000005,32.166 +2020-03-10 15:00:00,131.26,174.335,53.18,32.166 +2020-03-10 15:15:00,123.68,172.97299999999998,53.18,32.166 +2020-03-10 15:30:00,117.32,172.918,53.18,32.166 +2020-03-10 15:45:00,118.3,173.00099999999998,53.18,32.166 +2020-03-10 16:00:00,122.14,176.535,54.928999999999995,32.166 +2020-03-10 16:15:00,120.8,178.231,54.928999999999995,32.166 +2020-03-10 16:30:00,123.17,180.257,54.928999999999995,32.166 +2020-03-10 16:45:00,120.87,180.545,54.928999999999995,32.166 +2020-03-10 17:00:00,119.71,183.364,60.913000000000004,32.166 +2020-03-10 17:15:00,128.27,186.02700000000002,60.913000000000004,32.166 +2020-03-10 17:30:00,128.23,188.989,60.913000000000004,32.166 +2020-03-10 17:45:00,132.22,190.09599999999998,60.913000000000004,32.166 +2020-03-10 18:00:00,133.86,193.33900000000003,62.214,32.166 +2020-03-10 18:15:00,138.61,194.197,62.214,32.166 +2020-03-10 18:30:00,141.3,192.59599999999998,62.214,32.166 +2020-03-10 18:45:00,137.8,194.65900000000002,62.214,32.166 +2020-03-10 19:00:00,133.97,193.333,62.38,32.166 +2020-03-10 19:15:00,133.09,190.37900000000002,62.38,32.166 +2020-03-10 19:30:00,134.88,189.68400000000003,62.38,32.166 +2020-03-10 19:45:00,130.24,186.79,62.38,32.166 +2020-03-10 20:00:00,124.19,181.265,65.018,32.166 +2020-03-10 20:15:00,120.85,176.13299999999998,65.018,32.166 +2020-03-10 20:30:00,119.04,172.58599999999998,65.018,32.166 +2020-03-10 20:45:00,115.01,170.15200000000002,65.018,32.166 +2020-03-10 21:00:00,110.97,166.892,56.416000000000004,32.166 +2020-03-10 21:15:00,109.67,163.63299999999998,56.416000000000004,32.166 +2020-03-10 21:30:00,108.54,161.345,56.416000000000004,32.166 +2020-03-10 21:45:00,105.86,161.29399999999998,56.416000000000004,32.166 +2020-03-10 22:00:00,98.25,153.8,52.846000000000004,32.166 +2020-03-10 22:15:00,100.43,149.92,52.846000000000004,32.166 +2020-03-10 22:30:00,97.18,133.826,52.846000000000004,32.166 +2020-03-10 22:45:00,91.4,126.08,52.846000000000004,32.166 +2020-03-10 23:00:00,89.03,119.40799999999999,44.435,32.166 +2020-03-10 23:15:00,91.06,117.764,44.435,32.166 +2020-03-10 23:30:00,88.04,119.34700000000001,44.435,32.166 +2020-03-10 23:45:00,81.48,119.641,44.435,32.166 +2020-03-11 00:00:00,75.53,114.79700000000001,42.527,32.166 +2020-03-11 00:15:00,80.59,113.096,42.527,32.166 +2020-03-11 00:30:00,83.26,112.241,42.527,32.166 +2020-03-11 00:45:00,83.5,112.162,42.527,32.166 +2020-03-11 01:00:00,75.29,113.49700000000001,38.655,32.166 +2020-03-11 01:15:00,74.08,113.866,38.655,32.166 +2020-03-11 01:30:00,75.7,113.865,38.655,32.166 +2020-03-11 01:45:00,74.92,114.025,38.655,32.166 +2020-03-11 02:00:00,74.13,115.889,36.912,32.166 +2020-03-11 02:15:00,81.05,116.80799999999999,36.912,32.166 +2020-03-11 02:30:00,80.33,117.723,36.912,32.166 +2020-03-11 02:45:00,79.51,119.47200000000001,36.912,32.166 +2020-03-11 03:00:00,75.06,122.09899999999999,36.98,32.166 +2020-03-11 03:15:00,75.42,123.804,36.98,32.166 +2020-03-11 03:30:00,76.42,125.709,36.98,32.166 +2020-03-11 03:45:00,80.52,126.93299999999999,36.98,32.166 +2020-03-11 04:00:00,84.62,140.27200000000002,38.052,32.166 +2020-03-11 04:15:00,85.72,153.308,38.052,32.166 +2020-03-11 04:30:00,81.45,155.411,38.052,32.166 +2020-03-11 04:45:00,84.39,157.156,38.052,32.166 +2020-03-11 05:00:00,91.07,191.585,42.455,32.166 +2020-03-11 05:15:00,95.13,222.638,42.455,32.166 +2020-03-11 05:30:00,98.48,216.752,42.455,32.166 +2020-03-11 05:45:00,96.19,208.571,42.455,32.166 +2020-03-11 06:00:00,104.58,205.817,57.986000000000004,32.166 +2020-03-11 06:15:00,107.88,212.06900000000002,57.986000000000004,32.166 +2020-03-11 06:30:00,110.59,213.645,57.986000000000004,32.166 +2020-03-11 06:45:00,113.78,216.666,57.986000000000004,32.166 +2020-03-11 07:00:00,118.72,219.00400000000002,71.868,32.166 +2020-03-11 07:15:00,127.45,221.824,71.868,32.166 +2020-03-11 07:30:00,131.59,221.76,71.868,32.166 +2020-03-11 07:45:00,132.28,219.90900000000002,71.868,32.166 +2020-03-11 08:00:00,129.65,217.81900000000002,62.225,32.166 +2020-03-11 08:15:00,131.66,215.915,62.225,32.166 +2020-03-11 08:30:00,130.64,210.942,62.225,32.166 +2020-03-11 08:45:00,131.27,206.542,62.225,32.166 +2020-03-11 09:00:00,134.97,199.05900000000003,58.802,32.166 +2020-03-11 09:15:00,139.13,195.53599999999997,58.802,32.166 +2020-03-11 09:30:00,141.53,194.37099999999998,58.802,32.166 +2020-03-11 09:45:00,138.32,191.662,58.802,32.166 +2020-03-11 10:00:00,134.68,188.90900000000002,54.122,32.166 +2020-03-11 10:15:00,135.2,186.00400000000002,54.122,32.166 +2020-03-11 10:30:00,134.26,183.71599999999998,54.122,32.166 +2020-03-11 10:45:00,133.84,183.042,54.122,32.166 +2020-03-11 11:00:00,133.84,180.051,54.368,32.166 +2020-03-11 11:15:00,136.34,179.623,54.368,32.166 +2020-03-11 11:30:00,134.04,179.273,54.368,32.166 +2020-03-11 11:45:00,134.3,178.97400000000002,54.368,32.166 +2020-03-11 12:00:00,136.13,173.747,52.74,32.166 +2020-03-11 12:15:00,139.12,174.074,52.74,32.166 +2020-03-11 12:30:00,137.79,173.21900000000002,52.74,32.166 +2020-03-11 12:45:00,132.99,174.07,52.74,32.166 +2020-03-11 13:00:00,129.17,173.736,52.544,32.166 +2020-03-11 13:15:00,130.87,172.03599999999997,52.544,32.166 +2020-03-11 13:30:00,132.75,170.476,52.544,32.166 +2020-03-11 13:45:00,134.79,169.96099999999998,52.544,32.166 +2020-03-11 14:00:00,129.47,171.979,53.602,32.166 +2020-03-11 14:15:00,126.72,171.09799999999998,53.602,32.166 +2020-03-11 14:30:00,128.13,171.03900000000002,53.602,32.166 +2020-03-11 14:45:00,124.89,172.03799999999998,53.602,32.166 +2020-03-11 15:00:00,129.33,173.86900000000003,55.59,32.166 +2020-03-11 15:15:00,130.78,172.476,55.59,32.166 +2020-03-11 15:30:00,132.29,172.37,55.59,32.166 +2020-03-11 15:45:00,130.2,172.433,55.59,32.166 +2020-03-11 16:00:00,126.12,175.97400000000002,57.586999999999996,32.166 +2020-03-11 16:15:00,122.31,177.64700000000002,57.586999999999996,32.166 +2020-03-11 16:30:00,122.71,179.671,57.586999999999996,32.166 +2020-03-11 16:45:00,123.16,179.915,57.586999999999996,32.166 +2020-03-11 17:00:00,125.12,182.75099999999998,62.111999999999995,32.166 +2020-03-11 17:15:00,128.58,185.418,62.111999999999995,32.166 +2020-03-11 17:30:00,127.81,188.40200000000002,62.111999999999995,32.166 +2020-03-11 17:45:00,130.41,189.523,62.111999999999995,32.166 +2020-03-11 18:00:00,142.61,192.77200000000002,64.605,32.166 +2020-03-11 18:15:00,140.13,193.701,64.605,32.166 +2020-03-11 18:30:00,143.86,192.095,64.605,32.166 +2020-03-11 18:45:00,134.22,194.179,64.605,32.166 +2020-03-11 19:00:00,135.54,192.81400000000002,65.55199999999999,32.166 +2020-03-11 19:15:00,132.58,189.88,65.55199999999999,32.166 +2020-03-11 19:30:00,135.41,189.21400000000003,65.55199999999999,32.166 +2020-03-11 19:45:00,134.32,186.36900000000003,65.55199999999999,32.166 +2020-03-11 20:00:00,127.68,180.81099999999998,66.778,32.166 +2020-03-11 20:15:00,121.46,175.696,66.778,32.166 +2020-03-11 20:30:00,114.11,172.178,66.778,32.166 +2020-03-11 20:45:00,112.76,169.74599999999998,66.778,32.166 +2020-03-11 21:00:00,103.63,166.476,56.103,32.166 +2020-03-11 21:15:00,103.76,163.215,56.103,32.166 +2020-03-11 21:30:00,107.96,160.925,56.103,32.166 +2020-03-11 21:45:00,109.11,160.89700000000002,56.103,32.166 +2020-03-11 22:00:00,106.52,153.386,51.371,32.166 +2020-03-11 22:15:00,101.13,149.535,51.371,32.166 +2020-03-11 22:30:00,100.38,133.38299999999998,51.371,32.166 +2020-03-11 22:45:00,98.36,125.639,51.371,32.166 +2020-03-11 23:00:00,94.04,118.964,42.798,32.166 +2020-03-11 23:15:00,90.56,117.34100000000001,42.798,32.166 +2020-03-11 23:30:00,90.47,118.929,42.798,32.166 +2020-03-11 23:45:00,92.85,119.251,42.798,32.166 +2020-03-12 00:00:00,90.26,114.434,39.069,32.166 +2020-03-12 00:15:00,83.72,112.744,39.069,32.166 +2020-03-12 00:30:00,85.51,111.87,39.069,32.166 +2020-03-12 00:45:00,85.74,111.79299999999999,39.069,32.166 +2020-03-12 01:00:00,82.15,113.084,37.043,32.166 +2020-03-12 01:15:00,81.61,113.43700000000001,37.043,32.166 +2020-03-12 01:30:00,83.56,113.417,37.043,32.166 +2020-03-12 01:45:00,83.38,113.586,37.043,32.166 +2020-03-12 02:00:00,79.78,115.43700000000001,34.625,32.166 +2020-03-12 02:15:00,79.93,116.348,34.625,32.166 +2020-03-12 02:30:00,80.09,117.28200000000001,34.625,32.166 +2020-03-12 02:45:00,83.47,119.03299999999999,34.625,32.166 +2020-03-12 03:00:00,79.75,121.67200000000001,33.812,32.166 +2020-03-12 03:15:00,83.53,123.359,33.812,32.166 +2020-03-12 03:30:00,85.78,125.258,33.812,32.166 +2020-03-12 03:45:00,86.38,126.495,33.812,32.166 +2020-03-12 04:00:00,83.32,139.841,35.236999999999995,32.166 +2020-03-12 04:15:00,85.25,152.864,35.236999999999995,32.166 +2020-03-12 04:30:00,89.13,154.987,35.236999999999995,32.166 +2020-03-12 04:45:00,87.85,156.718,35.236999999999995,32.166 +2020-03-12 05:00:00,87.66,191.12900000000002,40.375,32.166 +2020-03-12 05:15:00,89.16,222.2,40.375,32.166 +2020-03-12 05:30:00,92.45,216.27900000000002,40.375,32.166 +2020-03-12 05:45:00,94.7,208.10299999999998,40.375,32.166 +2020-03-12 06:00:00,106.37,205.355,52.316,32.166 +2020-03-12 06:15:00,109.43,211.613,52.316,32.166 +2020-03-12 06:30:00,110.52,213.139,52.316,32.166 +2020-03-12 06:45:00,112.03,216.136,52.316,32.166 +2020-03-12 07:00:00,115.95,218.495,64.115,32.166 +2020-03-12 07:15:00,118.25,221.282,64.115,32.166 +2020-03-12 07:30:00,121.19,221.176,64.115,32.166 +2020-03-12 07:45:00,121.25,219.28400000000002,64.115,32.166 +2020-03-12 08:00:00,121.28,217.168,55.033,32.166 +2020-03-12 08:15:00,120.35,215.253,55.033,32.166 +2020-03-12 08:30:00,121.03,210.222,55.033,32.166 +2020-03-12 08:45:00,122.01,205.84099999999998,55.033,32.166 +2020-03-12 09:00:00,121.47,198.373,49.411,32.166 +2020-03-12 09:15:00,118.49,194.851,49.411,32.166 +2020-03-12 09:30:00,119.35,193.707,49.411,32.166 +2020-03-12 09:45:00,117.69,191.012,49.411,32.166 +2020-03-12 10:00:00,118.5,188.274,45.82899999999999,32.166 +2020-03-12 10:15:00,119.49,185.41400000000002,45.82899999999999,32.166 +2020-03-12 10:30:00,118.65,183.149,45.82899999999999,32.166 +2020-03-12 10:45:00,120.54,182.495,45.82899999999999,32.166 +2020-03-12 11:00:00,120.4,179.497,44.333,32.166 +2020-03-12 11:15:00,120.2,179.092,44.333,32.166 +2020-03-12 11:30:00,120.88,178.748,44.333,32.166 +2020-03-12 11:45:00,118.34,178.46900000000002,44.333,32.166 +2020-03-12 12:00:00,117.08,173.262,42.95,32.166 +2020-03-12 12:15:00,116.25,173.6,42.95,32.166 +2020-03-12 12:30:00,115.75,172.706,42.95,32.166 +2020-03-12 12:45:00,109.47,173.554,42.95,32.166 +2020-03-12 13:00:00,108.09,173.266,42.489,32.166 +2020-03-12 13:15:00,115.15,171.544,42.489,32.166 +2020-03-12 13:30:00,115.05,169.97099999999998,42.489,32.166 +2020-03-12 13:45:00,111.73,169.46099999999998,42.489,32.166 +2020-03-12 14:00:00,112.25,171.55,43.448,32.166 +2020-03-12 14:15:00,113.98,170.643,43.448,32.166 +2020-03-12 14:30:00,117.08,170.551,43.448,32.166 +2020-03-12 14:45:00,119.53,171.55900000000003,43.448,32.166 +2020-03-12 15:00:00,118.97,173.39700000000002,45.994,32.166 +2020-03-12 15:15:00,118.68,171.975,45.994,32.166 +2020-03-12 15:30:00,117.13,171.81599999999997,45.994,32.166 +2020-03-12 15:45:00,119.59,171.862,45.994,32.166 +2020-03-12 16:00:00,119.06,175.407,48.167,32.166 +2020-03-12 16:15:00,118.71,177.05700000000002,48.167,32.166 +2020-03-12 16:30:00,116.06,179.081,48.167,32.166 +2020-03-12 16:45:00,116.15,179.28099999999998,48.167,32.166 +2020-03-12 17:00:00,117.1,182.13099999999997,52.637,32.166 +2020-03-12 17:15:00,117.47,184.803,52.637,32.166 +2020-03-12 17:30:00,124.27,187.81,52.637,32.166 +2020-03-12 17:45:00,130.48,188.94400000000002,52.637,32.166 +2020-03-12 18:00:00,135.41,192.19799999999998,55.739,32.166 +2020-03-12 18:15:00,134.81,193.199,55.739,32.166 +2020-03-12 18:30:00,135.02,191.58900000000003,55.739,32.166 +2020-03-12 18:45:00,141.44,193.692,55.739,32.166 +2020-03-12 19:00:00,139.92,192.29,56.36600000000001,32.166 +2020-03-12 19:15:00,135.83,189.375,56.36600000000001,32.166 +2020-03-12 19:30:00,133.89,188.739,56.36600000000001,32.166 +2020-03-12 19:45:00,136.57,185.94099999999997,56.36600000000001,32.166 +2020-03-12 20:00:00,127.47,180.352,56.338,32.166 +2020-03-12 20:15:00,117.38,175.25400000000002,56.338,32.166 +2020-03-12 20:30:00,114.25,171.765,56.338,32.166 +2020-03-12 20:45:00,118.49,169.33599999999998,56.338,32.166 +2020-03-12 21:00:00,110.15,166.05599999999998,49.894,32.166 +2020-03-12 21:15:00,109.53,162.791,49.894,32.166 +2020-03-12 21:30:00,99.6,160.502,49.894,32.166 +2020-03-12 21:45:00,103.89,160.494,49.894,32.166 +2020-03-12 22:00:00,96.79,152.968,46.687,32.166 +2020-03-12 22:15:00,101.17,149.144,46.687,32.166 +2020-03-12 22:30:00,99.91,132.936,46.687,32.166 +2020-03-12 22:45:00,97.37,125.19200000000001,46.687,32.166 +2020-03-12 23:00:00,90.19,118.515,39.211,32.166 +2020-03-12 23:15:00,91.27,116.912,39.211,32.166 +2020-03-12 23:30:00,89.72,118.506,39.211,32.166 +2020-03-12 23:45:00,90.08,118.85600000000001,39.211,32.166 +2020-03-13 00:00:00,79.34,112.801,36.616,32.166 +2020-03-13 00:15:00,81.17,111.344,36.616,32.166 +2020-03-13 00:30:00,84.9,110.35700000000001,36.616,32.166 +2020-03-13 00:45:00,84.62,110.445,36.616,32.166 +2020-03-13 01:00:00,76.48,111.34,33.799,32.166 +2020-03-13 01:15:00,75.14,112.479,33.799,32.166 +2020-03-13 01:30:00,75.63,112.32,33.799,32.166 +2020-03-13 01:45:00,78.52,112.565,33.799,32.166 +2020-03-13 02:00:00,80.3,114.60600000000001,32.968,32.166 +2020-03-13 02:15:00,78.45,115.39399999999999,32.968,32.166 +2020-03-13 02:30:00,75.39,116.959,32.968,32.166 +2020-03-13 02:45:00,74.75,118.661,32.968,32.166 +2020-03-13 03:00:00,80.8,120.464,33.533,32.166 +2020-03-13 03:15:00,81.43,122.869,33.533,32.166 +2020-03-13 03:30:00,80.63,124.714,33.533,32.166 +2020-03-13 03:45:00,76.96,126.39299999999999,33.533,32.166 +2020-03-13 04:00:00,77.78,139.984,36.102,32.166 +2020-03-13 04:15:00,76.01,152.549,36.102,32.166 +2020-03-13 04:30:00,78.16,155.04,36.102,32.166 +2020-03-13 04:45:00,80.35,155.576,36.102,32.166 +2020-03-13 05:00:00,83.6,188.688,42.423,32.166 +2020-03-13 05:15:00,86.08,221.351,42.423,32.166 +2020-03-13 05:30:00,89.34,216.452,42.423,32.166 +2020-03-13 05:45:00,93.73,208.157,42.423,32.166 +2020-03-13 06:00:00,103.64,205.862,55.38,32.166 +2020-03-13 06:15:00,107.57,210.74,55.38,32.166 +2020-03-13 06:30:00,110.64,211.385,55.38,32.166 +2020-03-13 06:45:00,114.48,215.893,55.38,32.166 +2020-03-13 07:00:00,119.66,217.563,65.929,32.166 +2020-03-13 07:15:00,122.04,221.418,65.929,32.166 +2020-03-13 07:30:00,125.84,220.833,65.929,32.166 +2020-03-13 07:45:00,127.92,218.025,65.929,32.166 +2020-03-13 08:00:00,130.98,214.92700000000002,57.336999999999996,32.166 +2020-03-13 08:15:00,131.41,212.718,57.336999999999996,32.166 +2020-03-13 08:30:00,133.25,208.553,57.336999999999996,32.166 +2020-03-13 08:45:00,132.73,202.65900000000002,57.336999999999996,32.166 +2020-03-13 09:00:00,131.91,195.34099999999998,54.226000000000006,32.166 +2020-03-13 09:15:00,133.92,192.61900000000003,54.226000000000006,32.166 +2020-03-13 09:30:00,134.68,190.99200000000002,54.226000000000006,32.166 +2020-03-13 09:45:00,135.73,188.257,54.226000000000006,32.166 +2020-03-13 10:00:00,134.92,184.42,51.298,32.166 +2020-03-13 10:15:00,135.4,182.248,51.298,32.166 +2020-03-13 10:30:00,134.47,180.012,51.298,32.166 +2020-03-13 10:45:00,135.19,178.93900000000002,51.298,32.166 +2020-03-13 11:00:00,135.89,175.94299999999998,50.839,32.166 +2020-03-13 11:15:00,138.7,174.553,50.839,32.166 +2020-03-13 11:30:00,137.93,175.858,50.839,32.166 +2020-03-13 11:45:00,136.68,175.517,50.839,32.166 +2020-03-13 12:00:00,135.2,171.43099999999998,47.976000000000006,32.166 +2020-03-13 12:15:00,133.97,169.679,47.976000000000006,32.166 +2020-03-13 12:30:00,132.57,168.91400000000002,47.976000000000006,32.166 +2020-03-13 12:45:00,133.27,170.15200000000002,47.976000000000006,32.166 +2020-03-13 13:00:00,131.19,170.889,46.299,32.166 +2020-03-13 13:15:00,131.49,169.957,46.299,32.166 +2020-03-13 13:30:00,130.77,168.495,46.299,32.166 +2020-03-13 13:45:00,129.96,167.967,46.299,32.166 +2020-03-13 14:00:00,127.92,168.924,44.971000000000004,32.166 +2020-03-13 14:15:00,127.13,167.878,44.971000000000004,32.166 +2020-03-13 14:30:00,128.48,168.445,44.971000000000004,32.166 +2020-03-13 14:45:00,133.54,169.662,44.971000000000004,32.166 +2020-03-13 15:00:00,132.39,171.033,47.48,32.166 +2020-03-13 15:15:00,129.23,169.125,47.48,32.166 +2020-03-13 15:30:00,123.71,167.375,47.48,32.166 +2020-03-13 15:45:00,127.43,167.642,47.48,32.166 +2020-03-13 16:00:00,125.11,169.949,50.648,32.166 +2020-03-13 16:15:00,125.31,171.919,50.648,32.166 +2020-03-13 16:30:00,131.08,174.01,50.648,32.166 +2020-03-13 16:45:00,131.36,173.954,50.648,32.166 +2020-03-13 17:00:00,133.17,177.222,56.251000000000005,32.166 +2020-03-13 17:15:00,127.14,179.476,56.251000000000005,32.166 +2020-03-13 17:30:00,131.27,182.215,56.251000000000005,32.166 +2020-03-13 17:45:00,129.12,183.114,56.251000000000005,32.166 +2020-03-13 18:00:00,131.24,187.041,58.982,32.166 +2020-03-13 18:15:00,135.17,187.62099999999998,58.982,32.166 +2020-03-13 18:30:00,139.57,186.37900000000002,58.982,32.166 +2020-03-13 18:45:00,139.51,188.546,58.982,32.166 +2020-03-13 19:00:00,133.36,188.09,57.293,32.166 +2020-03-13 19:15:00,128.85,186.59799999999998,57.293,32.166 +2020-03-13 19:30:00,132.49,185.59400000000002,57.293,32.166 +2020-03-13 19:45:00,126.88,182.27900000000002,57.293,32.166 +2020-03-13 20:00:00,119.57,176.699,59.433,32.166 +2020-03-13 20:15:00,115.51,171.71900000000002,59.433,32.166 +2020-03-13 20:30:00,114.29,168.16099999999997,59.433,32.166 +2020-03-13 20:45:00,118.37,166.18200000000002,59.433,32.166 +2020-03-13 21:00:00,112.96,163.543,52.153999999999996,32.166 +2020-03-13 21:15:00,110.25,160.898,52.153999999999996,32.166 +2020-03-13 21:30:00,97.71,158.635,52.153999999999996,32.166 +2020-03-13 21:45:00,100.27,159.203,52.153999999999996,32.166 +2020-03-13 22:00:00,97.93,152.60299999999998,47.125,32.166 +2020-03-13 22:15:00,92.71,148.64700000000002,47.125,32.166 +2020-03-13 22:30:00,96.04,139.16299999999998,47.125,32.166 +2020-03-13 22:45:00,96.36,134.97,47.125,32.166 +2020-03-13 23:00:00,92.48,128.05200000000002,41.236000000000004,32.166 +2020-03-13 23:15:00,86.81,124.382,41.236000000000004,32.166 +2020-03-13 23:30:00,83.75,124.344,41.236000000000004,32.166 +2020-03-13 23:45:00,80.73,124.059,41.236000000000004,32.166 +2020-03-14 00:00:00,77.88,109.852,36.484,31.988000000000003 +2020-03-14 00:15:00,83.78,104.229,36.484,31.988000000000003 +2020-03-14 00:30:00,82.86,104.44,36.484,31.988000000000003 +2020-03-14 00:45:00,81.65,105.12,36.484,31.988000000000003 +2020-03-14 01:00:00,70.41,106.62100000000001,32.391999999999996,31.988000000000003 +2020-03-14 01:15:00,73.58,106.88,32.391999999999996,31.988000000000003 +2020-03-14 01:30:00,71.51,106.098,32.391999999999996,31.988000000000003 +2020-03-14 01:45:00,71.4,106.311,32.391999999999996,31.988000000000003 +2020-03-14 02:00:00,70.84,108.875,30.194000000000003,31.988000000000003 +2020-03-14 02:15:00,70.05,109.193,30.194000000000003,31.988000000000003 +2020-03-14 02:30:00,68.78,109.60700000000001,30.194000000000003,31.988000000000003 +2020-03-14 02:45:00,70.66,111.51299999999999,30.194000000000003,31.988000000000003 +2020-03-14 03:00:00,74.8,113.75299999999999,29.677,31.988000000000003 +2020-03-14 03:15:00,77.05,114.881,29.677,31.988000000000003 +2020-03-14 03:30:00,75.79,115.251,29.677,31.988000000000003 +2020-03-14 03:45:00,74.51,117.256,29.677,31.988000000000003 +2020-03-14 04:00:00,73.14,126.67299999999999,29.616,31.988000000000003 +2020-03-14 04:15:00,76.99,136.685,29.616,31.988000000000003 +2020-03-14 04:30:00,79.29,136.898,29.616,31.988000000000003 +2020-03-14 04:45:00,79.22,136.989,29.616,31.988000000000003 +2020-03-14 05:00:00,73.08,153.809,29.625,31.988000000000003 +2020-03-14 05:15:00,73.44,166.923,29.625,31.988000000000003 +2020-03-14 05:30:00,78.54,162.54399999999998,29.625,31.988000000000003 +2020-03-14 05:45:00,82.22,160.054,29.625,31.988000000000003 +2020-03-14 06:00:00,85.25,177.106,30.551,31.988000000000003 +2020-03-14 06:15:00,78.08,198.613,30.551,31.988000000000003 +2020-03-14 06:30:00,81.79,193.703,30.551,31.988000000000003 +2020-03-14 06:45:00,83.22,189.206,30.551,31.988000000000003 +2020-03-14 07:00:00,89.51,187.062,34.865,31.988000000000003 +2020-03-14 07:15:00,91.04,189.51,34.865,31.988000000000003 +2020-03-14 07:30:00,91.15,191.669,34.865,31.988000000000003 +2020-03-14 07:45:00,90.05,192.73,34.865,31.988000000000003 +2020-03-14 08:00:00,95.18,193.46599999999998,41.456,31.988000000000003 +2020-03-14 08:15:00,100.55,194.687,41.456,31.988000000000003 +2020-03-14 08:30:00,104.99,192.00400000000002,41.456,31.988000000000003 +2020-03-14 08:45:00,103.55,189.274,41.456,31.988000000000003 +2020-03-14 09:00:00,103.12,184.047,43.001999999999995,31.988000000000003 +2020-03-14 09:15:00,109.55,182.112,43.001999999999995,31.988000000000003 +2020-03-14 09:30:00,113.29,181.43599999999998,43.001999999999995,31.988000000000003 +2020-03-14 09:45:00,114.02,178.81,43.001999999999995,31.988000000000003 +2020-03-14 10:00:00,110.04,175.35,42.047,31.988000000000003 +2020-03-14 10:15:00,109.56,173.428,42.047,31.988000000000003 +2020-03-14 10:30:00,115.95,171.328,42.047,31.988000000000003 +2020-03-14 10:45:00,117.43,171.47400000000002,42.047,31.988000000000003 +2020-03-14 11:00:00,116.25,168.637,39.894,31.988000000000003 +2020-03-14 11:15:00,113.56,166.72400000000002,39.894,31.988000000000003 +2020-03-14 11:30:00,120.33,167.00400000000002,39.894,31.988000000000003 +2020-03-14 11:45:00,122.42,165.852,39.894,31.988000000000003 +2020-03-14 12:00:00,112.56,160.997,38.122,31.988000000000003 +2020-03-14 12:15:00,110.8,159.982,38.122,31.988000000000003 +2020-03-14 12:30:00,105.88,159.467,38.122,31.988000000000003 +2020-03-14 12:45:00,104.72,160.058,38.122,31.988000000000003 +2020-03-14 13:00:00,102.48,160.347,34.645,31.988000000000003 +2020-03-14 13:15:00,101.32,157.35299999999998,34.645,31.988000000000003 +2020-03-14 13:30:00,99.94,155.47299999999998,34.645,31.988000000000003 +2020-03-14 13:45:00,98.58,155.225,34.645,31.988000000000003 +2020-03-14 14:00:00,96.86,157.421,33.739000000000004,31.988000000000003 +2020-03-14 14:15:00,96.28,155.673,33.739000000000004,31.988000000000003 +2020-03-14 14:30:00,96.6,154.406,33.739000000000004,31.988000000000003 +2020-03-14 14:45:00,97.2,155.903,33.739000000000004,31.988000000000003 +2020-03-14 15:00:00,96.67,157.937,35.908,31.988000000000003 +2020-03-14 15:15:00,95.72,156.866,35.908,31.988000000000003 +2020-03-14 15:30:00,95.92,156.571,35.908,31.988000000000003 +2020-03-14 15:45:00,95.09,156.715,35.908,31.988000000000003 +2020-03-14 16:00:00,95.88,158.109,39.249,31.988000000000003 +2020-03-14 16:15:00,95.55,160.806,39.249,31.988000000000003 +2020-03-14 16:30:00,95.24,162.874,39.249,31.988000000000003 +2020-03-14 16:45:00,95.42,163.626,39.249,31.988000000000003 +2020-03-14 17:00:00,99.69,166.172,46.045,31.988000000000003 +2020-03-14 17:15:00,99.32,169.774,46.045,31.988000000000003 +2020-03-14 17:30:00,101.07,172.42700000000002,46.045,31.988000000000003 +2020-03-14 17:45:00,102.78,173.00599999999997,46.045,31.988000000000003 +2020-03-14 18:00:00,107.78,176.59599999999998,48.238,31.988000000000003 +2020-03-14 18:15:00,108.04,179.19299999999998,48.238,31.988000000000003 +2020-03-14 18:30:00,111.45,179.40099999999998,48.238,31.988000000000003 +2020-03-14 18:45:00,111.41,177.924,48.238,31.988000000000003 +2020-03-14 19:00:00,111.15,178.12599999999998,46.785,31.988000000000003 +2020-03-14 19:15:00,109.51,176.055,46.785,31.988000000000003 +2020-03-14 19:30:00,108.23,175.903,46.785,31.988000000000003 +2020-03-14 19:45:00,106.14,172.66299999999998,46.785,31.988000000000003 +2020-03-14 20:00:00,101.01,169.296,39.830999999999996,31.988000000000003 +2020-03-14 20:15:00,97.68,166.32299999999998,39.830999999999996,31.988000000000003 +2020-03-14 20:30:00,93.02,162.352,39.830999999999996,31.988000000000003 +2020-03-14 20:45:00,93.19,160.203,39.830999999999996,31.988000000000003 +2020-03-14 21:00:00,89.17,159.56799999999998,34.063,31.988000000000003 +2020-03-14 21:15:00,86.26,157.30700000000002,34.063,31.988000000000003 +2020-03-14 21:30:00,89.1,156.261,34.063,31.988000000000003 +2020-03-14 21:45:00,87.56,156.39,34.063,31.988000000000003 +2020-03-14 22:00:00,82.87,151.08100000000002,34.455999999999996,31.988000000000003 +2020-03-14 22:15:00,83.61,149.59799999999998,34.455999999999996,31.988000000000003 +2020-03-14 22:30:00,77.72,146.056,34.455999999999996,31.988000000000003 +2020-03-14 22:45:00,79.25,143.726,34.455999999999996,31.988000000000003 +2020-03-14 23:00:00,75.28,139.096,27.840999999999998,31.988000000000003 +2020-03-14 23:15:00,72.99,133.905,27.840999999999998,31.988000000000003 +2020-03-14 23:30:00,72.93,132.463,27.840999999999998,31.988000000000003 +2020-03-14 23:45:00,71.64,129.89,27.840999999999998,31.988000000000003 +2020-03-15 00:00:00,66.91,110.05799999999999,20.007,31.988000000000003 +2020-03-15 00:15:00,67.0,103.98700000000001,20.007,31.988000000000003 +2020-03-15 00:30:00,66.54,103.802,20.007,31.988000000000003 +2020-03-15 00:45:00,65.35,105.135,20.007,31.988000000000003 +2020-03-15 01:00:00,62.97,106.49,17.378,31.988000000000003 +2020-03-15 01:15:00,63.28,107.705,17.378,31.988000000000003 +2020-03-15 01:30:00,60.74,107.381,17.378,31.988000000000003 +2020-03-15 01:45:00,63.48,107.251,17.378,31.988000000000003 +2020-03-15 02:00:00,61.21,109.098,16.145,31.988000000000003 +2020-03-15 02:15:00,61.18,108.698,16.145,31.988000000000003 +2020-03-15 02:30:00,60.17,109.969,16.145,31.988000000000003 +2020-03-15 02:45:00,61.4,112.275,16.145,31.988000000000003 +2020-03-15 03:00:00,60.81,114.917,15.427999999999999,31.988000000000003 +2020-03-15 03:15:00,61.75,115.56700000000001,15.427999999999999,31.988000000000003 +2020-03-15 03:30:00,61.32,117.15100000000001,15.427999999999999,31.988000000000003 +2020-03-15 03:45:00,62.75,118.98200000000001,15.427999999999999,31.988000000000003 +2020-03-15 04:00:00,62.97,128.166,16.663,31.988000000000003 +2020-03-15 04:15:00,63.98,137.14700000000002,16.663,31.988000000000003 +2020-03-15 04:30:00,63.98,137.636,16.663,31.988000000000003 +2020-03-15 04:45:00,63.9,137.90200000000002,16.663,31.988000000000003 +2020-03-15 05:00:00,63.92,151.424,17.271,31.988000000000003 +2020-03-15 05:15:00,62.64,162.171,17.271,31.988000000000003 +2020-03-15 05:30:00,63.97,157.541,17.271,31.988000000000003 +2020-03-15 05:45:00,64.06,155.245,17.271,31.988000000000003 +2020-03-15 06:00:00,63.06,171.845,17.612000000000002,31.988000000000003 +2020-03-15 06:15:00,61.28,191.857,17.612000000000002,31.988000000000003 +2020-03-15 06:30:00,58.8,185.75099999999998,17.612000000000002,31.988000000000003 +2020-03-15 06:45:00,61.36,180.08599999999998,17.612000000000002,31.988000000000003 +2020-03-15 07:00:00,63.14,180.313,20.88,31.988000000000003 +2020-03-15 07:15:00,62.27,181.68,20.88,31.988000000000003 +2020-03-15 07:30:00,66.04,182.82299999999998,20.88,31.988000000000003 +2020-03-15 07:45:00,68.94,183.12,20.88,31.988000000000003 +2020-03-15 08:00:00,71.34,185.66299999999998,25.861,31.988000000000003 +2020-03-15 08:15:00,73.91,186.957,25.861,31.988000000000003 +2020-03-15 08:30:00,74.04,185.912,25.861,31.988000000000003 +2020-03-15 08:45:00,74.07,185.106,25.861,31.988000000000003 +2020-03-15 09:00:00,74.58,179.49,27.921999999999997,31.988000000000003 +2020-03-15 09:15:00,73.36,178.012,27.921999999999997,31.988000000000003 +2020-03-15 09:30:00,75.48,177.27,27.921999999999997,31.988000000000003 +2020-03-15 09:45:00,75.64,174.68599999999998,27.921999999999997,31.988000000000003 +2020-03-15 10:00:00,75.27,173.71099999999998,29.048000000000002,31.988000000000003 +2020-03-15 10:15:00,74.22,172.36,29.048000000000002,31.988000000000003 +2020-03-15 10:30:00,73.91,170.886,29.048000000000002,31.988000000000003 +2020-03-15 10:45:00,74.56,169.398,29.048000000000002,31.988000000000003 +2020-03-15 11:00:00,77.3,167.37099999999998,32.02,31.988000000000003 +2020-03-15 11:15:00,80.08,165.544,32.02,31.988000000000003 +2020-03-15 11:30:00,82.09,165.077,32.02,31.988000000000003 +2020-03-15 11:45:00,79.85,164.555,32.02,31.988000000000003 +2020-03-15 12:00:00,78.18,159.333,28.55,31.988000000000003 +2020-03-15 12:15:00,79.4,160.05,28.55,31.988000000000003 +2020-03-15 12:30:00,76.27,158.143,28.55,31.988000000000003 +2020-03-15 12:45:00,75.58,157.72299999999998,28.55,31.988000000000003 +2020-03-15 13:00:00,71.35,157.334,25.601999999999997,31.988000000000003 +2020-03-15 13:15:00,71.38,157.099,25.601999999999997,31.988000000000003 +2020-03-15 13:30:00,70.92,154.85,25.601999999999997,31.988000000000003 +2020-03-15 13:45:00,72.05,154.139,25.601999999999997,31.988000000000003 +2020-03-15 14:00:00,72.64,156.80700000000002,23.916999999999998,31.988000000000003 +2020-03-15 14:15:00,71.93,156.235,23.916999999999998,31.988000000000003 +2020-03-15 14:30:00,71.59,155.931,23.916999999999998,31.988000000000003 +2020-03-15 14:45:00,71.9,156.895,23.916999999999998,31.988000000000003 +2020-03-15 15:00:00,73.94,157.505,24.064,31.988000000000003 +2020-03-15 15:15:00,72.87,157.013,24.064,31.988000000000003 +2020-03-15 15:30:00,73.03,157.2,24.064,31.988000000000003 +2020-03-15 15:45:00,74.33,158.025,24.064,31.988000000000003 +2020-03-15 16:00:00,76.14,161.00799999999998,28.189,31.988000000000003 +2020-03-15 16:15:00,77.61,162.841,28.189,31.988000000000003 +2020-03-15 16:30:00,78.45,165.28900000000002,28.189,31.988000000000003 +2020-03-15 16:45:00,83.21,166.13099999999997,28.189,31.988000000000003 +2020-03-15 17:00:00,85.3,168.75799999999998,37.576,31.988000000000003 +2020-03-15 17:15:00,86.88,172.24,37.576,31.988000000000003 +2020-03-15 17:30:00,88.48,175.292,37.576,31.988000000000003 +2020-03-15 17:45:00,89.58,178.122,37.576,31.988000000000003 +2020-03-15 18:00:00,97.68,181.27700000000002,42.669,31.988000000000003 +2020-03-15 18:15:00,98.02,185.18,42.669,31.988000000000003 +2020-03-15 18:30:00,103.29,183.335,42.669,31.988000000000003 +2020-03-15 18:45:00,103.7,183.683,42.669,31.988000000000003 +2020-03-15 19:00:00,105.01,183.797,43.538999999999994,31.988000000000003 +2020-03-15 19:15:00,102.92,182.188,43.538999999999994,31.988000000000003 +2020-03-15 19:30:00,101.41,181.878,43.538999999999994,31.988000000000003 +2020-03-15 19:45:00,99.98,180.03400000000002,43.538999999999994,31.988000000000003 +2020-03-15 20:00:00,98.07,176.605,37.330999999999996,31.988000000000003 +2020-03-15 20:15:00,98.05,174.584,37.330999999999996,31.988000000000003 +2020-03-15 20:30:00,102.06,171.94,37.330999999999996,31.988000000000003 +2020-03-15 20:45:00,101.51,168.424,37.330999999999996,31.988000000000003 +2020-03-15 21:00:00,92.01,165.261,33.856,31.988000000000003 +2020-03-15 21:15:00,96.09,162.356,33.856,31.988000000000003 +2020-03-15 21:30:00,95.45,161.513,33.856,31.988000000000003 +2020-03-15 21:45:00,98.39,161.839,33.856,31.988000000000003 +2020-03-15 22:00:00,96.08,155.615,34.711999999999996,31.988000000000003 +2020-03-15 22:15:00,93.54,153.219,34.711999999999996,31.988000000000003 +2020-03-15 22:30:00,88.54,146.49200000000002,34.711999999999996,31.988000000000003 +2020-03-15 22:45:00,88.54,143.184,34.711999999999996,31.988000000000003 +2020-03-15 23:00:00,90.01,135.808,29.698,31.988000000000003 +2020-03-15 23:15:00,90.99,132.591,29.698,31.988000000000003 +2020-03-15 23:30:00,88.54,131.872,29.698,31.988000000000003 +2020-03-15 23:45:00,84.36,130.211,29.698,31.988000000000003 +2020-03-16 00:00:00,75.75,98.40299999999999,29.983,32.166 +2020-03-16 00:15:00,83.47,96.28299999999999,29.983,32.166 +2020-03-16 00:30:00,84.42,95.56,29.983,32.166 +2020-03-16 00:45:00,82.25,95.21799999999999,29.983,32.166 +2020-03-16 01:00:00,77.57,96.861,29.122,32.166 +2020-03-16 01:15:00,80.67,97.205,29.122,32.166 +2020-03-16 01:30:00,81.83,96.76299999999999,29.122,32.166 +2020-03-16 01:45:00,81.92,96.56,29.122,32.166 +2020-03-16 02:00:00,76.07,98.52600000000001,28.676,32.166 +2020-03-16 02:15:00,79.7,98.59200000000001,28.676,32.166 +2020-03-16 02:30:00,80.65,100.493,28.676,32.166 +2020-03-16 02:45:00,81.61,101.565,28.676,32.166 +2020-03-16 03:00:00,77.46,105.42399999999999,26.552,32.166 +2020-03-16 03:15:00,79.91,107.682,26.552,32.166 +2020-03-16 03:30:00,82.47,108.404,26.552,32.166 +2020-03-16 03:45:00,82.23,109.36399999999999,26.552,32.166 +2020-03-16 04:00:00,80.26,122.883,27.44,32.166 +2020-03-16 04:15:00,78.08,136.07299999999998,27.44,32.166 +2020-03-16 04:30:00,78.99,137.483,27.44,32.166 +2020-03-16 04:45:00,83.48,138.162,27.44,32.166 +2020-03-16 05:00:00,85.58,168.11,36.825,32.166 +2020-03-16 05:15:00,89.84,199.683,36.825,32.166 +2020-03-16 05:30:00,94.03,193.502,36.825,32.166 +2020-03-16 05:45:00,98.78,184.55700000000002,36.825,32.166 +2020-03-16 06:00:00,104.82,183.747,56.589,32.166 +2020-03-16 06:15:00,108.78,188.298,56.589,32.166 +2020-03-16 06:30:00,111.69,189.327,56.589,32.166 +2020-03-16 06:45:00,114.28,191.799,56.589,32.166 +2020-03-16 07:00:00,119.75,194.726,67.49,32.166 +2020-03-16 07:15:00,118.99,196.967,67.49,32.166 +2020-03-16 07:30:00,121.18,196.90599999999998,67.49,32.166 +2020-03-16 07:45:00,121.94,194.672,67.49,32.166 +2020-03-16 08:00:00,124.7,193.282,60.028,32.166 +2020-03-16 08:15:00,122.52,192.105,60.028,32.166 +2020-03-16 08:30:00,121.4,187.63,60.028,32.166 +2020-03-16 08:45:00,119.86,184.179,60.028,32.166 +2020-03-16 09:00:00,117.82,178.373,55.018,32.166 +2020-03-16 09:15:00,116.63,174.179,55.018,32.166 +2020-03-16 09:30:00,117.73,173.43099999999998,55.018,32.166 +2020-03-16 09:45:00,115.81,171.18599999999998,55.018,32.166 +2020-03-16 10:00:00,113.41,168.915,51.183,32.166 +2020-03-16 10:15:00,116.0,167.81400000000002,51.183,32.166 +2020-03-16 10:30:00,115.46,165.19799999999998,51.183,32.166 +2020-03-16 10:45:00,117.05,164.111,51.183,32.166 +2020-03-16 11:00:00,113.1,158.343,50.065,32.166 +2020-03-16 11:15:00,113.17,158.403,50.065,32.166 +2020-03-16 11:30:00,113.75,160.048,50.065,32.166 +2020-03-16 11:45:00,114.16,160.379,50.065,32.166 +2020-03-16 12:00:00,111.33,156.921,48.141999999999996,32.166 +2020-03-16 12:15:00,113.06,157.437,48.141999999999996,32.166 +2020-03-16 12:30:00,110.37,156.293,48.141999999999996,32.166 +2020-03-16 12:45:00,110.4,157.15200000000002,48.141999999999996,32.166 +2020-03-16 13:00:00,108.44,157.352,47.887,32.166 +2020-03-16 13:15:00,107.98,155.593,47.887,32.166 +2020-03-16 13:30:00,107.97,152.868,47.887,32.166 +2020-03-16 13:45:00,108.56,152.096,47.887,32.166 +2020-03-16 14:00:00,111.41,153.593,48.571000000000005,32.166 +2020-03-16 14:15:00,108.91,152.257,48.571000000000005,32.166 +2020-03-16 14:30:00,108.43,151.621,48.571000000000005,32.166 +2020-03-16 14:45:00,109.18,152.847,48.571000000000005,32.166 +2020-03-16 15:00:00,110.53,154.037,49.937,32.166 +2020-03-16 15:15:00,110.99,152.216,49.937,32.166 +2020-03-16 15:30:00,110.57,151.31,49.937,32.166 +2020-03-16 15:45:00,112.88,150.865,49.937,32.166 +2020-03-16 16:00:00,113.73,154.678,52.963,32.166 +2020-03-16 16:15:00,114.16,156.461,52.963,32.166 +2020-03-16 16:30:00,116.67,156.502,52.963,32.166 +2020-03-16 16:45:00,119.49,155.469,52.963,32.166 +2020-03-16 17:00:00,120.61,156.90200000000002,61.163999999999994,32.166 +2020-03-16 17:15:00,121.52,159.283,61.163999999999994,32.166 +2020-03-16 17:30:00,124.46,161.386,61.163999999999994,32.166 +2020-03-16 17:45:00,126.0,162.251,61.163999999999994,32.166 +2020-03-16 18:00:00,128.25,166.34799999999998,63.788999999999994,32.166 +2020-03-16 18:15:00,128.87,167.03099999999998,63.788999999999994,32.166 +2020-03-16 18:30:00,132.59,165.72,63.788999999999994,32.166 +2020-03-16 18:45:00,135.2,169.049,63.788999999999994,32.166 +2020-03-16 19:00:00,135.47,167.59,63.913000000000004,32.166 +2020-03-16 19:15:00,135.93,165.708,63.913000000000004,32.166 +2020-03-16 19:30:00,136.42,165.69400000000002,63.913000000000004,32.166 +2020-03-16 19:45:00,139.91,163.987,63.913000000000004,32.166 +2020-03-16 20:00:00,131.55,157.97,65.44,32.166 +2020-03-16 20:15:00,123.08,154.694,65.44,32.166 +2020-03-16 20:30:00,115.5,151.886,65.44,32.166 +2020-03-16 20:45:00,115.34,150.49200000000002,65.44,32.166 +2020-03-16 21:00:00,111.28,145.696,59.117,32.166 +2020-03-16 21:15:00,109.77,142.936,59.117,32.166 +2020-03-16 21:30:00,108.18,142.46200000000002,59.117,32.166 +2020-03-16 21:45:00,108.66,141.463,59.117,32.166 +2020-03-16 22:00:00,100.71,133.118,52.301,32.166 +2020-03-16 22:15:00,101.8,129.96,52.301,32.166 +2020-03-16 22:30:00,98.84,115.694,52.301,32.166 +2020-03-16 22:45:00,97.68,108.209,52.301,32.166 +2020-03-16 23:00:00,98.26,101.245,44.373000000000005,32.166 +2020-03-16 23:15:00,99.14,100.038,44.373000000000005,32.166 +2020-03-16 23:30:00,95.57,100.943,44.373000000000005,32.166 +2020-03-16 23:45:00,90.74,101.705,44.373000000000005,32.166 +2020-03-17 00:00:00,83.29,96.788,44.647,32.166 +2020-03-17 00:15:00,85.18,96.06,44.647,32.166 +2020-03-17 00:30:00,88.73,94.814,44.647,32.166 +2020-03-17 00:45:00,90.01,93.965,44.647,32.166 +2020-03-17 01:00:00,86.46,95.25399999999999,41.433,32.166 +2020-03-17 01:15:00,85.81,95.272,41.433,32.166 +2020-03-17 01:30:00,81.39,94.90299999999999,41.433,32.166 +2020-03-17 01:45:00,87.26,94.779,41.433,32.166 +2020-03-17 02:00:00,87.84,96.57700000000001,39.909,32.166 +2020-03-17 02:15:00,88.12,96.916,39.909,32.166 +2020-03-17 02:30:00,80.08,98.25200000000001,39.909,32.166 +2020-03-17 02:45:00,81.87,99.441,39.909,32.166 +2020-03-17 03:00:00,88.39,102.213,39.14,32.166 +2020-03-17 03:15:00,89.75,104.1,39.14,32.166 +2020-03-17 03:30:00,82.45,105.178,39.14,32.166 +2020-03-17 03:45:00,84.57,105.939,39.14,32.166 +2020-03-17 04:00:00,86.69,118.898,40.015,32.166 +2020-03-17 04:15:00,91.6,131.80700000000002,40.015,32.166 +2020-03-17 04:30:00,94.85,132.954,40.015,32.166 +2020-03-17 04:45:00,92.46,134.70600000000002,40.015,32.166 +2020-03-17 05:00:00,92.95,168.956,44.93600000000001,32.166 +2020-03-17 05:15:00,94.92,200.56099999999998,44.93600000000001,32.166 +2020-03-17 05:30:00,98.34,193.25,44.93600000000001,32.166 +2020-03-17 05:45:00,99.36,184.088,44.93600000000001,32.166 +2020-03-17 06:00:00,109.4,182.72,57.271,32.166 +2020-03-17 06:15:00,112.51,188.58599999999998,57.271,32.166 +2020-03-17 06:30:00,114.99,188.97299999999998,57.271,32.166 +2020-03-17 06:45:00,116.99,190.85,57.271,32.166 +2020-03-17 07:00:00,118.61,193.696,68.352,32.166 +2020-03-17 07:15:00,121.29,195.703,68.352,32.166 +2020-03-17 07:30:00,121.0,195.18,68.352,32.166 +2020-03-17 07:45:00,121.45,192.78,68.352,32.166 +2020-03-17 08:00:00,121.87,191.438,60.717,32.166 +2020-03-17 08:15:00,119.41,189.315,60.717,32.166 +2020-03-17 08:30:00,117.8,184.7,60.717,32.166 +2020-03-17 08:45:00,117.14,180.785,60.717,32.166 +2020-03-17 09:00:00,114.19,174.417,54.603,32.166 +2020-03-17 09:15:00,117.59,171.476,54.603,32.166 +2020-03-17 09:30:00,123.71,171.49200000000002,54.603,32.166 +2020-03-17 09:45:00,123.51,169.47299999999998,54.603,32.166 +2020-03-17 10:00:00,123.65,166.34900000000002,52.308,32.166 +2020-03-17 10:15:00,126.87,164.386,52.308,32.166 +2020-03-17 10:30:00,129.22,161.95,52.308,32.166 +2020-03-17 10:45:00,132.76,161.387,52.308,32.166 +2020-03-17 11:00:00,132.25,156.829,51.838,32.166 +2020-03-17 11:15:00,132.0,156.743,51.838,32.166 +2020-03-17 11:30:00,132.65,157.129,51.838,32.166 +2020-03-17 11:45:00,134.64,157.938,51.838,32.166 +2020-03-17 12:00:00,132.83,153.315,50.375,32.166 +2020-03-17 12:15:00,132.99,153.584,50.375,32.166 +2020-03-17 12:30:00,132.12,153.22299999999998,50.375,32.166 +2020-03-17 12:45:00,133.93,154.017,50.375,32.166 +2020-03-17 13:00:00,132.01,153.819,50.735,32.166 +2020-03-17 13:15:00,130.45,152.21200000000002,50.735,32.166 +2020-03-17 13:30:00,124.25,150.447,50.735,32.166 +2020-03-17 13:45:00,128.13,149.58700000000002,50.735,32.166 +2020-03-17 14:00:00,127.73,151.502,50.946000000000005,32.166 +2020-03-17 14:15:00,128.74,150.222,50.946000000000005,32.166 +2020-03-17 14:30:00,128.92,150.161,50.946000000000005,32.166 +2020-03-17 14:45:00,130.63,151.126,50.946000000000005,32.166 +2020-03-17 15:00:00,132.16,151.93,53.18,32.166 +2020-03-17 15:15:00,132.92,150.592,53.18,32.166 +2020-03-17 15:30:00,132.15,149.77,53.18,32.166 +2020-03-17 15:45:00,132.5,149.059,53.18,32.166 +2020-03-17 16:00:00,131.86,153.01,54.928999999999995,32.166 +2020-03-17 16:15:00,131.47,155.187,54.928999999999995,32.166 +2020-03-17 16:30:00,129.12,155.662,54.928999999999995,32.166 +2020-03-17 16:45:00,130.42,155.007,54.928999999999995,32.166 +2020-03-17 17:00:00,131.53,156.974,60.913000000000004,32.166 +2020-03-17 17:15:00,135.08,159.489,60.913000000000004,32.166 +2020-03-17 17:30:00,143.36,162.031,60.913000000000004,32.166 +2020-03-17 17:45:00,144.28,162.69899999999998,60.913000000000004,32.166 +2020-03-17 18:00:00,137.97,166.455,62.214,32.166 +2020-03-17 18:15:00,138.35,167.197,62.214,32.166 +2020-03-17 18:30:00,140.04,165.55599999999998,62.214,32.166 +2020-03-17 18:45:00,139.14,169.495,62.214,32.166 +2020-03-17 19:00:00,138.8,167.745,62.38,32.166 +2020-03-17 19:15:00,134.96,165.702,62.38,32.166 +2020-03-17 19:30:00,135.63,165.108,62.38,32.166 +2020-03-17 19:45:00,132.59,163.565,62.38,32.166 +2020-03-17 20:00:00,123.95,157.756,65.018,32.166 +2020-03-17 20:15:00,121.08,153.566,65.018,32.166 +2020-03-17 20:30:00,114.15,151.616,65.018,32.166 +2020-03-17 20:45:00,114.92,149.891,65.018,32.166 +2020-03-17 21:00:00,114.69,144.774,56.416000000000004,32.166 +2020-03-17 21:15:00,109.67,142.322,56.416000000000004,32.166 +2020-03-17 21:30:00,107.76,141.31,56.416000000000004,32.166 +2020-03-17 21:45:00,105.9,140.57399999999998,56.416000000000004,32.166 +2020-03-17 22:00:00,108.3,133.65,52.846000000000004,32.166 +2020-03-17 22:15:00,108.41,130.213,52.846000000000004,32.166 +2020-03-17 22:30:00,101.96,116.061,52.846000000000004,32.166 +2020-03-17 22:45:00,96.25,108.795,52.846000000000004,32.166 +2020-03-17 23:00:00,91.66,101.624,44.435,32.166 +2020-03-17 23:15:00,90.02,100.15700000000001,44.435,32.166 +2020-03-17 23:30:00,86.99,100.775,44.435,32.166 +2020-03-17 23:45:00,89.71,101.256,44.435,32.166 +2020-03-18 00:00:00,83.73,96.42299999999999,42.527,32.166 +2020-03-18 00:15:00,89.73,95.705,42.527,32.166 +2020-03-18 00:30:00,86.8,94.446,42.527,32.166 +2020-03-18 00:45:00,85.84,93.59899999999999,42.527,32.166 +2020-03-18 01:00:00,80.74,94.855,38.655,32.166 +2020-03-18 01:15:00,80.02,94.855,38.655,32.166 +2020-03-18 01:30:00,85.25,94.46799999999999,38.655,32.166 +2020-03-18 01:45:00,87.28,94.34899999999999,38.655,32.166 +2020-03-18 02:00:00,86.8,96.135,36.912,32.166 +2020-03-18 02:15:00,84.18,96.462,36.912,32.166 +2020-03-18 02:30:00,79.64,97.82,36.912,32.166 +2020-03-18 02:45:00,86.07,99.01100000000001,36.912,32.166 +2020-03-18 03:00:00,88.69,101.79799999999999,36.98,32.166 +2020-03-18 03:15:00,89.95,103.663,36.98,32.166 +2020-03-18 03:30:00,85.36,104.735,36.98,32.166 +2020-03-18 03:45:00,81.81,105.51,36.98,32.166 +2020-03-18 04:00:00,86.79,118.46600000000001,38.052,32.166 +2020-03-18 04:15:00,90.82,131.358,38.052,32.166 +2020-03-18 04:30:00,92.75,132.518,38.052,32.166 +2020-03-18 04:45:00,88.52,134.257,38.052,32.166 +2020-03-18 05:00:00,90.89,168.46599999999998,42.455,32.166 +2020-03-18 05:15:00,92.19,200.058,42.455,32.166 +2020-03-18 05:30:00,96.66,192.72400000000002,42.455,32.166 +2020-03-18 05:45:00,100.78,183.581,42.455,32.166 +2020-03-18 06:00:00,108.49,182.225,57.986000000000004,32.166 +2020-03-18 06:15:00,111.7,188.088,57.986000000000004,32.166 +2020-03-18 06:30:00,114.37,188.43599999999998,57.986000000000004,32.166 +2020-03-18 06:45:00,118.53,190.293,57.986000000000004,32.166 +2020-03-18 07:00:00,124.03,193.15400000000002,71.868,32.166 +2020-03-18 07:15:00,124.83,195.13,71.868,32.166 +2020-03-18 07:30:00,127.19,194.567,71.868,32.166 +2020-03-18 07:45:00,130.33,192.137,71.868,32.166 +2020-03-18 08:00:00,133.17,190.771,62.225,32.166 +2020-03-18 08:15:00,131.53,188.65099999999998,62.225,32.166 +2020-03-18 08:30:00,133.54,183.987,62.225,32.166 +2020-03-18 08:45:00,132.96,180.095,62.225,32.166 +2020-03-18 09:00:00,129.67,173.738,58.802,32.166 +2020-03-18 09:15:00,131.06,170.799,58.802,32.166 +2020-03-18 09:30:00,130.32,170.835,58.802,32.166 +2020-03-18 09:45:00,125.59,168.834,58.802,32.166 +2020-03-18 10:00:00,123.25,165.722,54.122,32.166 +2020-03-18 10:15:00,118.73,163.805,54.122,32.166 +2020-03-18 10:30:00,115.43,161.391,54.122,32.166 +2020-03-18 10:45:00,117.34,160.849,54.122,32.166 +2020-03-18 11:00:00,117.69,156.282,54.368,32.166 +2020-03-18 11:15:00,118.38,156.218,54.368,32.166 +2020-03-18 11:30:00,118.73,156.61,54.368,32.166 +2020-03-18 11:45:00,118.1,157.438,54.368,32.166 +2020-03-18 12:00:00,117.22,152.83700000000002,52.74,32.166 +2020-03-18 12:15:00,117.72,153.116,52.74,32.166 +2020-03-18 12:30:00,117.55,152.716,52.74,32.166 +2020-03-18 12:45:00,115.41,153.509,52.74,32.166 +2020-03-18 13:00:00,116.23,153.35399999999998,52.544,32.166 +2020-03-18 13:15:00,118.56,151.731,52.544,32.166 +2020-03-18 13:30:00,118.92,149.958,52.544,32.166 +2020-03-18 13:45:00,118.2,149.1,52.544,32.166 +2020-03-18 14:00:00,119.88,151.083,53.602,32.166 +2020-03-18 14:15:00,117.8,149.78,53.602,32.166 +2020-03-18 14:30:00,116.98,149.68200000000002,53.602,32.166 +2020-03-18 14:45:00,119.05,150.655,53.602,32.166 +2020-03-18 15:00:00,115.92,151.475,55.59,32.166 +2020-03-18 15:15:00,111.49,150.108,55.59,32.166 +2020-03-18 15:30:00,111.91,149.237,55.59,32.166 +2020-03-18 15:45:00,121.29,148.507,55.59,32.166 +2020-03-18 16:00:00,119.54,152.477,57.586999999999996,32.166 +2020-03-18 16:15:00,120.09,154.63,57.586999999999996,32.166 +2020-03-18 16:30:00,114.99,155.105,57.586999999999996,32.166 +2020-03-18 16:45:00,117.72,154.4,57.586999999999996,32.166 +2020-03-18 17:00:00,122.47,156.395,62.111999999999995,32.166 +2020-03-18 17:15:00,129.68,158.906,62.111999999999995,32.166 +2020-03-18 17:30:00,131.77,161.46200000000002,62.111999999999995,32.166 +2020-03-18 17:45:00,132.41,162.132,62.111999999999995,32.166 +2020-03-18 18:00:00,131.64,165.895,64.605,32.166 +2020-03-18 18:15:00,126.8,166.695,64.605,32.166 +2020-03-18 18:30:00,135.48,165.045,64.605,32.166 +2020-03-18 18:45:00,144.49,169.00099999999998,64.605,32.166 +2020-03-18 19:00:00,144.79,167.22,65.55199999999999,32.166 +2020-03-18 19:15:00,137.82,165.19299999999998,65.55199999999999,32.166 +2020-03-18 19:30:00,133.32,164.62400000000002,65.55199999999999,32.166 +2020-03-18 19:45:00,131.22,163.12,65.55199999999999,32.166 +2020-03-18 20:00:00,129.19,157.282,66.778,32.166 +2020-03-18 20:15:00,125.46,153.107,66.778,32.166 +2020-03-18 20:30:00,120.34,151.188,66.778,32.166 +2020-03-18 20:45:00,117.63,149.47299999999998,66.778,32.166 +2020-03-18 21:00:00,114.44,144.349,56.103,32.166 +2020-03-18 21:15:00,114.71,141.899,56.103,32.166 +2020-03-18 21:30:00,109.71,140.885,56.103,32.166 +2020-03-18 21:45:00,108.39,140.174,56.103,32.166 +2020-03-18 22:00:00,107.26,133.243,51.371,32.166 +2020-03-18 22:15:00,105.38,129.833,51.371,32.166 +2020-03-18 22:30:00,98.14,115.63600000000001,51.371,32.166 +2020-03-18 22:45:00,94.88,108.369,51.371,32.166 +2020-03-18 23:00:00,88.42,101.18700000000001,42.798,32.166 +2020-03-18 23:15:00,89.09,99.745,42.798,32.166 +2020-03-18 23:30:00,91.31,100.363,42.798,32.166 +2020-03-18 23:45:00,92.36,100.868,42.798,32.166 +2020-03-19 00:00:00,87.88,96.053,39.069,32.166 +2020-03-19 00:15:00,85.89,95.34700000000001,39.069,32.166 +2020-03-19 00:30:00,87.68,94.073,39.069,32.166 +2020-03-19 00:45:00,87.98,93.228,39.069,32.166 +2020-03-19 01:00:00,84.61,94.45200000000001,37.043,32.166 +2020-03-19 01:15:00,79.55,94.434,37.043,32.166 +2020-03-19 01:30:00,77.18,94.027,37.043,32.166 +2020-03-19 01:45:00,78.42,93.915,37.043,32.166 +2020-03-19 02:00:00,79.65,95.69,34.625,32.166 +2020-03-19 02:15:00,86.0,96.006,34.625,32.166 +2020-03-19 02:30:00,83.94,97.384,34.625,32.166 +2020-03-19 02:45:00,86.89,98.575,34.625,32.166 +2020-03-19 03:00:00,82.57,101.37899999999999,33.812,32.166 +2020-03-19 03:15:00,86.09,103.221,33.812,32.166 +2020-03-19 03:30:00,88.78,104.287,33.812,32.166 +2020-03-19 03:45:00,86.84,105.07799999999999,33.812,32.166 +2020-03-19 04:00:00,86.04,118.03,35.236999999999995,32.166 +2020-03-19 04:15:00,87.46,130.905,35.236999999999995,32.166 +2020-03-19 04:30:00,90.13,132.077,35.236999999999995,32.166 +2020-03-19 04:45:00,89.22,133.804,35.236999999999995,32.166 +2020-03-19 05:00:00,87.18,167.97099999999998,40.375,32.166 +2020-03-19 05:15:00,92.65,199.551,40.375,32.166 +2020-03-19 05:30:00,91.79,192.196,40.375,32.166 +2020-03-19 05:45:00,99.25,183.07,40.375,32.166 +2020-03-19 06:00:00,101.84,181.726,52.316,32.166 +2020-03-19 06:15:00,108.97,187.58700000000002,52.316,32.166 +2020-03-19 06:30:00,112.26,187.893,52.316,32.166 +2020-03-19 06:45:00,114.99,189.731,52.316,32.166 +2020-03-19 07:00:00,119.6,192.607,64.115,32.166 +2020-03-19 07:15:00,119.74,194.554,64.115,32.166 +2020-03-19 07:30:00,122.99,193.949,64.115,32.166 +2020-03-19 07:45:00,125.11,191.489,64.115,32.166 +2020-03-19 08:00:00,132.62,190.1,55.033,32.166 +2020-03-19 08:15:00,133.14,187.981,55.033,32.166 +2020-03-19 08:30:00,132.06,183.27,55.033,32.166 +2020-03-19 08:45:00,130.67,179.4,55.033,32.166 +2020-03-19 09:00:00,131.44,173.053,49.411,32.166 +2020-03-19 09:15:00,128.19,170.11599999999999,49.411,32.166 +2020-03-19 09:30:00,130.89,170.173,49.411,32.166 +2020-03-19 09:45:00,133.55,168.19299999999998,49.411,32.166 +2020-03-19 10:00:00,135.03,165.09099999999998,45.82899999999999,32.166 +2020-03-19 10:15:00,135.81,163.22,45.82899999999999,32.166 +2020-03-19 10:30:00,135.42,160.829,45.82899999999999,32.166 +2020-03-19 10:45:00,133.48,160.30700000000002,45.82899999999999,32.166 +2020-03-19 11:00:00,134.38,155.732,44.333,32.166 +2020-03-19 11:15:00,136.87,155.691,44.333,32.166 +2020-03-19 11:30:00,136.53,156.089,44.333,32.166 +2020-03-19 11:45:00,135.15,156.935,44.333,32.166 +2020-03-19 12:00:00,134.45,152.355,42.95,32.166 +2020-03-19 12:15:00,132.99,152.64600000000002,42.95,32.166 +2020-03-19 12:30:00,130.93,152.20600000000002,42.95,32.166 +2020-03-19 12:45:00,131.7,152.998,42.95,32.166 +2020-03-19 13:00:00,131.55,152.886,42.489,32.166 +2020-03-19 13:15:00,136.58,151.246,42.489,32.166 +2020-03-19 13:30:00,137.49,149.465,42.489,32.166 +2020-03-19 13:45:00,130.19,148.61,42.489,32.166 +2020-03-19 14:00:00,126.15,150.662,43.448,32.166 +2020-03-19 14:15:00,126.13,149.335,43.448,32.166 +2020-03-19 14:30:00,128.46,149.201,43.448,32.166 +2020-03-19 14:45:00,126.37,150.18,43.448,32.166 +2020-03-19 15:00:00,127.54,151.016,45.994,32.166 +2020-03-19 15:15:00,133.12,149.622,45.994,32.166 +2020-03-19 15:30:00,132.92,148.701,45.994,32.166 +2020-03-19 15:45:00,133.74,147.954,45.994,32.166 +2020-03-19 16:00:00,129.51,151.94,48.167,32.166 +2020-03-19 16:15:00,133.23,154.07,48.167,32.166 +2020-03-19 16:30:00,134.2,154.545,48.167,32.166 +2020-03-19 16:45:00,133.59,153.78799999999998,48.167,32.166 +2020-03-19 17:00:00,130.78,155.813,52.637,32.166 +2020-03-19 17:15:00,129.72,158.317,52.637,32.166 +2020-03-19 17:30:00,130.87,160.888,52.637,32.166 +2020-03-19 17:45:00,140.13,161.56,52.637,32.166 +2020-03-19 18:00:00,142.78,165.331,55.739,32.166 +2020-03-19 18:15:00,139.96,166.188,55.739,32.166 +2020-03-19 18:30:00,137.06,164.53099999999998,55.739,32.166 +2020-03-19 18:45:00,135.97,168.502,55.739,32.166 +2020-03-19 19:00:00,140.2,166.69099999999997,56.36600000000001,32.166 +2020-03-19 19:15:00,139.3,164.68,56.36600000000001,32.166 +2020-03-19 19:30:00,137.47,164.135,56.36600000000001,32.166 +2020-03-19 19:45:00,134.57,162.673,56.36600000000001,32.166 +2020-03-19 20:00:00,124.92,156.805,56.338,32.166 +2020-03-19 20:15:00,122.58,152.644,56.338,32.166 +2020-03-19 20:30:00,124.08,150.756,56.338,32.166 +2020-03-19 20:45:00,121.71,149.05200000000002,56.338,32.166 +2020-03-19 21:00:00,111.74,143.921,49.894,32.166 +2020-03-19 21:15:00,110.78,141.474,49.894,32.166 +2020-03-19 21:30:00,112.46,140.45600000000002,49.894,32.166 +2020-03-19 21:45:00,110.73,139.77100000000002,49.894,32.166 +2020-03-19 22:00:00,106.07,132.833,46.687,32.166 +2020-03-19 22:15:00,102.14,129.44899999999998,46.687,32.166 +2020-03-19 22:30:00,100.56,115.20700000000001,46.687,32.166 +2020-03-19 22:45:00,100.02,107.939,46.687,32.166 +2020-03-19 23:00:00,93.88,100.74600000000001,39.211,32.166 +2020-03-19 23:15:00,92.98,99.32700000000001,39.211,32.166 +2020-03-19 23:30:00,90.87,99.948,39.211,32.166 +2020-03-19 23:45:00,92.97,100.475,39.211,32.166 +2020-03-20 00:00:00,81.31,94.274,36.616,32.166 +2020-03-20 00:15:00,83.1,93.804,36.616,32.166 +2020-03-20 00:30:00,80.74,92.495,36.616,32.166 +2020-03-20 00:45:00,87.41,91.87299999999999,36.616,32.166 +2020-03-20 01:00:00,85.22,92.698,33.799,32.166 +2020-03-20 01:15:00,85.31,93.18799999999999,33.799,32.166 +2020-03-20 01:30:00,79.72,92.814,33.799,32.166 +2020-03-20 01:45:00,79.16,92.711,33.799,32.166 +2020-03-20 02:00:00,75.56,94.837,32.968,32.166 +2020-03-20 02:15:00,78.31,95.037,32.968,32.166 +2020-03-20 02:30:00,84.24,97.10799999999999,32.968,32.166 +2020-03-20 02:45:00,84.88,98.116,32.968,32.166 +2020-03-20 03:00:00,84.31,100.412,33.533,32.166 +2020-03-20 03:15:00,80.11,102.57,33.533,32.166 +2020-03-20 03:30:00,86.07,103.539,33.533,32.166 +2020-03-20 03:45:00,86.71,104.882,33.533,32.166 +2020-03-20 04:00:00,89.04,118.052,36.102,32.166 +2020-03-20 04:15:00,86.87,130.207,36.102,32.166 +2020-03-20 04:30:00,82.36,131.881,36.102,32.166 +2020-03-20 04:45:00,84.69,132.501,36.102,32.166 +2020-03-20 05:00:00,89.4,165.488,42.423,32.166 +2020-03-20 05:15:00,90.87,198.532,42.423,32.166 +2020-03-20 05:30:00,93.45,192.05200000000002,42.423,32.166 +2020-03-20 05:45:00,99.81,182.74599999999998,42.423,32.166 +2020-03-20 06:00:00,105.17,181.84099999999998,55.38,32.166 +2020-03-20 06:15:00,110.26,186.604,55.38,32.166 +2020-03-20 06:30:00,112.63,186.18099999999998,55.38,32.166 +2020-03-20 06:45:00,116.56,189.22299999999998,55.38,32.166 +2020-03-20 07:00:00,123.53,191.669,65.929,32.166 +2020-03-20 07:15:00,124.74,194.675,65.929,32.166 +2020-03-20 07:30:00,128.22,193.28099999999998,65.929,32.166 +2020-03-20 07:45:00,131.67,190.02700000000002,65.929,32.166 +2020-03-20 08:00:00,135.19,187.975,57.336999999999996,32.166 +2020-03-20 08:15:00,135.75,185.748,57.336999999999996,32.166 +2020-03-20 08:30:00,136.9,181.74200000000002,57.336999999999996,32.166 +2020-03-20 08:45:00,138.65,176.588,57.336999999999996,32.166 +2020-03-20 09:00:00,138.65,169.93400000000003,54.226000000000006,32.166 +2020-03-20 09:15:00,141.78,168.017,54.226000000000006,32.166 +2020-03-20 09:30:00,141.41,167.548,54.226000000000006,32.166 +2020-03-20 09:45:00,140.33,165.61,54.226000000000006,32.166 +2020-03-20 10:00:00,137.44,161.526,51.298,32.166 +2020-03-20 10:15:00,138.85,160.215,51.298,32.166 +2020-03-20 10:30:00,139.49,157.942,51.298,32.166 +2020-03-20 10:45:00,140.16,157.05,51.298,32.166 +2020-03-20 11:00:00,135.6,152.503,50.839,32.166 +2020-03-20 11:15:00,134.52,151.446,50.839,32.166 +2020-03-20 11:30:00,136.04,153.204,50.839,32.166 +2020-03-20 11:45:00,132.53,153.84799999999998,50.839,32.166 +2020-03-20 12:00:00,128.08,150.317,47.976000000000006,32.166 +2020-03-20 12:15:00,131.48,148.69899999999998,47.976000000000006,32.166 +2020-03-20 12:30:00,131.48,148.384,47.976000000000006,32.166 +2020-03-20 12:45:00,128.91,149.39600000000002,47.976000000000006,32.166 +2020-03-20 13:00:00,125.64,150.276,46.299,32.166 +2020-03-20 13:15:00,127.45,149.352,46.299,32.166 +2020-03-20 13:30:00,121.83,147.784,46.299,32.166 +2020-03-20 13:45:00,121.45,146.958,46.299,32.166 +2020-03-20 14:00:00,122.13,147.91299999999998,44.971000000000004,32.166 +2020-03-20 14:15:00,122.95,146.52700000000002,44.971000000000004,32.166 +2020-03-20 14:30:00,119.93,147.18,44.971000000000004,32.166 +2020-03-20 14:45:00,118.5,148.24,44.971000000000004,32.166 +2020-03-20 15:00:00,118.31,148.666,47.48,32.166 +2020-03-20 15:15:00,118.3,146.813,47.48,32.166 +2020-03-20 15:30:00,117.43,144.417,47.48,32.166 +2020-03-20 15:45:00,118.9,143.97299999999998,47.48,32.166 +2020-03-20 16:00:00,119.03,146.799,50.648,32.166 +2020-03-20 16:15:00,117.4,149.28,50.648,32.166 +2020-03-20 16:30:00,117.62,149.784,50.648,32.166 +2020-03-20 16:45:00,116.65,148.667,50.648,32.166 +2020-03-20 17:00:00,118.31,151.38299999999998,56.251000000000005,32.166 +2020-03-20 17:15:00,119.69,153.495,56.251000000000005,32.166 +2020-03-20 17:30:00,121.15,155.864,56.251000000000005,32.166 +2020-03-20 17:45:00,124.53,156.298,56.251000000000005,32.166 +2020-03-20 18:00:00,130.12,160.666,58.982,32.166 +2020-03-20 18:15:00,129.06,160.994,58.982,32.166 +2020-03-20 18:30:00,130.46,159.622,58.982,32.166 +2020-03-20 18:45:00,132.02,163.724,58.982,32.166 +2020-03-20 19:00:00,134.39,162.879,57.293,32.166 +2020-03-20 19:15:00,130.14,162.16299999999998,57.293,32.166 +2020-03-20 19:30:00,128.96,161.32399999999998,57.293,32.166 +2020-03-20 19:45:00,127.91,159.238,57.293,32.166 +2020-03-20 20:00:00,121.85,153.34799999999998,59.433,32.166 +2020-03-20 20:15:00,124.01,149.438,59.433,32.166 +2020-03-20 20:30:00,121.62,147.405,59.433,32.166 +2020-03-20 20:45:00,115.58,145.928,59.433,32.166 +2020-03-20 21:00:00,107.59,141.58700000000002,52.153999999999996,32.166 +2020-03-20 21:15:00,107.1,139.983,52.153999999999996,32.166 +2020-03-20 21:30:00,102.66,138.957,52.153999999999996,32.166 +2020-03-20 21:45:00,102.64,138.795,52.153999999999996,32.166 +2020-03-20 22:00:00,95.47,132.6,47.125,32.166 +2020-03-20 22:15:00,92.65,129.063,47.125,32.166 +2020-03-20 22:30:00,94.55,121.314,47.125,32.166 +2020-03-20 22:45:00,96.74,117.149,47.125,32.166 +2020-03-20 23:00:00,91.54,110.07799999999999,41.236000000000004,32.166 +2020-03-20 23:15:00,84.93,106.649,41.236000000000004,32.166 +2020-03-20 23:30:00,83.48,105.56200000000001,41.236000000000004,32.166 +2020-03-20 23:45:00,79.79,105.538,41.236000000000004,32.166 +2020-03-21 00:00:00,76.15,92.06,36.484,31.988000000000003 +2020-03-21 00:15:00,76.52,88.061,36.484,31.988000000000003 +2020-03-21 00:30:00,75.95,87.62700000000001,36.484,31.988000000000003 +2020-03-21 00:45:00,78.77,87.333,36.484,31.988000000000003 +2020-03-21 01:00:00,78.62,88.72,32.391999999999996,31.988000000000003 +2020-03-21 01:15:00,79.74,88.615,32.391999999999996,31.988000000000003 +2020-03-21 01:30:00,75.68,87.559,32.391999999999996,31.988000000000003 +2020-03-21 01:45:00,75.56,87.695,32.391999999999996,31.988000000000003 +2020-03-21 02:00:00,77.03,90.04299999999999,30.194000000000003,31.988000000000003 +2020-03-21 02:15:00,75.94,89.686,30.194000000000003,31.988000000000003 +2020-03-21 02:30:00,75.96,90.661,30.194000000000003,31.988000000000003 +2020-03-21 02:45:00,70.86,92.00399999999999,30.194000000000003,31.988000000000003 +2020-03-21 03:00:00,76.24,94.37,29.677,31.988000000000003 +2020-03-21 03:15:00,77.68,95.339,29.677,31.988000000000003 +2020-03-21 03:30:00,77.67,95.18299999999999,29.677,31.988000000000003 +2020-03-21 03:45:00,73.57,97.11200000000001,29.677,31.988000000000003 +2020-03-21 04:00:00,69.82,106.507,29.616,31.988000000000003 +2020-03-21 04:15:00,70.66,116.397,29.616,31.988000000000003 +2020-03-21 04:30:00,72.22,115.867,29.616,31.988000000000003 +2020-03-21 04:45:00,72.34,116.185,29.616,31.988000000000003 +2020-03-21 05:00:00,73.28,134.24200000000002,29.625,31.988000000000003 +2020-03-21 05:15:00,70.66,149.116,29.625,31.988000000000003 +2020-03-21 05:30:00,72.31,143.395,29.625,31.988000000000003 +2020-03-21 05:45:00,71.97,139.756,29.625,31.988000000000003 +2020-03-21 06:00:00,72.01,156.734,30.551,31.988000000000003 +2020-03-21 06:15:00,74.11,176.644,30.551,31.988000000000003 +2020-03-21 06:30:00,70.85,171.11,30.551,31.988000000000003 +2020-03-21 06:45:00,75.14,166.157,30.551,31.988000000000003 +2020-03-21 07:00:00,77.83,164.997,34.865,31.988000000000003 +2020-03-21 07:15:00,78.22,166.58599999999998,34.865,31.988000000000003 +2020-03-21 07:30:00,79.47,167.74200000000002,34.865,31.988000000000003 +2020-03-21 07:45:00,80.88,167.858,34.865,31.988000000000003 +2020-03-21 08:00:00,82.4,168.946,41.456,31.988000000000003 +2020-03-21 08:15:00,82.02,169.545,41.456,31.988000000000003 +2020-03-21 08:30:00,82.13,166.77,41.456,31.988000000000003 +2020-03-21 08:45:00,83.0,164.435,41.456,31.988000000000003 +2020-03-21 09:00:00,84.22,160.035,43.001999999999995,31.988000000000003 +2020-03-21 09:15:00,84.04,158.866,43.001999999999995,31.988000000000003 +2020-03-21 09:30:00,83.23,159.287,43.001999999999995,31.988000000000003 +2020-03-21 09:45:00,82.37,157.369,43.001999999999995,31.988000000000003 +2020-03-21 10:00:00,81.8,153.608,42.047,31.988000000000003 +2020-03-21 10:15:00,80.91,152.577,42.047,31.988000000000003 +2020-03-21 10:30:00,79.58,150.36700000000002,42.047,31.988000000000003 +2020-03-21 10:45:00,80.21,150.458,42.047,31.988000000000003 +2020-03-21 11:00:00,81.27,145.994,39.894,31.988000000000003 +2020-03-21 11:15:00,82.37,144.628,39.894,31.988000000000003 +2020-03-21 11:30:00,82.93,145.55,39.894,31.988000000000003 +2020-03-21 11:45:00,86.04,145.608,39.894,31.988000000000003 +2020-03-21 12:00:00,81.5,141.393,38.122,31.988000000000003 +2020-03-21 12:15:00,78.76,140.543,38.122,31.988000000000003 +2020-03-21 12:30:00,77.88,140.417,38.122,31.988000000000003 +2020-03-21 12:45:00,78.92,140.98,38.122,31.988000000000003 +2020-03-21 13:00:00,73.16,141.30200000000002,34.645,31.988000000000003 +2020-03-21 13:15:00,75.26,138.506,34.645,31.988000000000003 +2020-03-21 13:30:00,72.49,136.60399999999998,34.645,31.988000000000003 +2020-03-21 13:45:00,72.8,135.825,34.645,31.988000000000003 +2020-03-21 14:00:00,71.82,137.874,33.739000000000004,31.988000000000003 +2020-03-21 14:15:00,68.31,135.69299999999998,33.739000000000004,31.988000000000003 +2020-03-21 14:30:00,69.19,134.681,33.739000000000004,31.988000000000003 +2020-03-21 14:45:00,72.21,136.053,33.739000000000004,31.988000000000003 +2020-03-21 15:00:00,73.63,137.141,35.908,31.988000000000003 +2020-03-21 15:15:00,74.28,136.116,35.908,31.988000000000003 +2020-03-21 15:30:00,72.5,135.011,35.908,31.988000000000003 +2020-03-21 15:45:00,76.03,134.32299999999998,35.908,31.988000000000003 +2020-03-21 16:00:00,78.29,136.875,39.249,31.988000000000003 +2020-03-21 16:15:00,79.04,139.828,39.249,31.988000000000003 +2020-03-21 16:30:00,80.66,140.35,39.249,31.988000000000003 +2020-03-21 16:45:00,82.94,139.906,39.249,31.988000000000003 +2020-03-21 17:00:00,86.08,141.953,46.045,31.988000000000003 +2020-03-21 17:15:00,88.1,144.812,46.045,31.988000000000003 +2020-03-21 17:30:00,90.2,147.079,46.045,31.988000000000003 +2020-03-21 17:45:00,92.79,147.321,46.045,31.988000000000003 +2020-03-21 18:00:00,97.5,151.714,48.238,31.988000000000003 +2020-03-21 18:15:00,97.87,154.032,48.238,31.988000000000003 +2020-03-21 18:30:00,100.71,154.118,48.238,31.988000000000003 +2020-03-21 18:45:00,101.17,154.534,48.238,31.988000000000003 +2020-03-21 19:00:00,107.14,153.971,46.785,31.988000000000003 +2020-03-21 19:15:00,105.31,152.583,46.785,31.988000000000003 +2020-03-21 19:30:00,103.63,152.597,46.785,31.988000000000003 +2020-03-21 19:45:00,103.35,150.908,46.785,31.988000000000003 +2020-03-21 20:00:00,97.14,147.029,39.830999999999996,31.988000000000003 +2020-03-21 20:15:00,94.26,144.661,39.830999999999996,31.988000000000003 +2020-03-21 20:30:00,91.73,142.124,39.830999999999996,31.988000000000003 +2020-03-21 20:45:00,90.18,140.87,39.830999999999996,31.988000000000003 +2020-03-21 21:00:00,84.4,137.915,34.063,31.988000000000003 +2020-03-21 21:15:00,85.72,136.566,34.063,31.988000000000003 +2020-03-21 21:30:00,84.05,136.578,34.063,31.988000000000003 +2020-03-21 21:45:00,83.62,135.95600000000002,34.063,31.988000000000003 +2020-03-21 22:00:00,79.7,130.829,34.455999999999996,31.988000000000003 +2020-03-21 22:15:00,78.84,129.39,34.455999999999996,31.988000000000003 +2020-03-21 22:30:00,73.84,126.485,34.455999999999996,31.988000000000003 +2020-03-21 22:45:00,75.89,123.93,34.455999999999996,31.988000000000003 +2020-03-21 23:00:00,70.82,118.62,27.840999999999998,31.988000000000003 +2020-03-21 23:15:00,70.94,114.022,27.840999999999998,31.988000000000003 +2020-03-21 23:30:00,69.79,112.245,27.840999999999998,31.988000000000003 +2020-03-21 23:45:00,68.39,110.39399999999999,27.840999999999998,31.988000000000003 +2020-03-22 00:00:00,60.08,92.443,20.007,31.988000000000003 +2020-03-22 00:15:00,58.06,87.84200000000001,20.007,31.988000000000003 +2020-03-22 00:30:00,55.51,87.055,20.007,31.988000000000003 +2020-03-22 00:45:00,56.45,87.271,20.007,31.988000000000003 +2020-03-22 01:00:00,54.53,88.59700000000001,17.378,31.988000000000003 +2020-03-22 01:15:00,52.88,89.23700000000001,17.378,31.988000000000003 +2020-03-22 01:30:00,52.07,88.521,17.378,31.988000000000003 +2020-03-22 01:45:00,53.98,88.29799999999999,17.378,31.988000000000003 +2020-03-22 02:00:00,48.89,90.07600000000001,16.145,31.988000000000003 +2020-03-22 02:15:00,53.69,89.28200000000001,16.145,31.988000000000003 +2020-03-22 02:30:00,51.28,90.99600000000001,16.145,31.988000000000003 +2020-03-22 02:45:00,53.95,92.603,16.145,31.988000000000003 +2020-03-22 03:00:00,50.72,95.43799999999999,15.427999999999999,31.988000000000003 +2020-03-22 03:15:00,54.24,96.069,15.427999999999999,31.988000000000003 +2020-03-22 03:30:00,51.64,96.738,15.427999999999999,31.988000000000003 +2020-03-22 03:45:00,51.01,98.361,15.427999999999999,31.988000000000003 +2020-03-22 04:00:00,50.48,107.544,16.663,31.988000000000003 +2020-03-22 04:15:00,51.91,116.501,16.663,31.988000000000003 +2020-03-22 04:30:00,51.98,116.48,16.663,31.988000000000003 +2020-03-22 04:45:00,52.83,116.838,16.663,31.988000000000003 +2020-03-22 05:00:00,51.89,132.247,17.271,31.988000000000003 +2020-03-22 05:15:00,52.74,145.017,17.271,31.988000000000003 +2020-03-22 05:30:00,52.54,139.015,17.271,31.988000000000003 +2020-03-22 05:45:00,54.38,135.485,17.271,31.988000000000003 +2020-03-22 06:00:00,55.46,151.58700000000002,17.612000000000002,31.988000000000003 +2020-03-22 06:15:00,53.48,170.449,17.612000000000002,31.988000000000003 +2020-03-22 06:30:00,55.9,163.799,17.612000000000002,31.988000000000003 +2020-03-22 06:45:00,53.71,157.689,17.612000000000002,31.988000000000003 +2020-03-22 07:00:00,55.53,158.501,20.88,31.988000000000003 +2020-03-22 07:15:00,59.67,158.875,20.88,31.988000000000003 +2020-03-22 07:30:00,62.15,159.44299999999998,20.88,31.988000000000003 +2020-03-22 07:45:00,61.32,158.92700000000002,20.88,31.988000000000003 +2020-03-22 08:00:00,67.05,161.621,25.861,31.988000000000003 +2020-03-22 08:15:00,67.97,162.512,25.861,31.988000000000003 +2020-03-22 08:30:00,69.68,161.264,25.861,31.988000000000003 +2020-03-22 08:45:00,69.61,160.52,25.861,31.988000000000003 +2020-03-22 09:00:00,74.12,155.761,27.921999999999997,31.988000000000003 +2020-03-22 09:15:00,75.03,154.89,27.921999999999997,31.988000000000003 +2020-03-22 09:30:00,74.98,155.334,27.921999999999997,31.988000000000003 +2020-03-22 09:45:00,73.91,153.639,27.921999999999997,31.988000000000003 +2020-03-22 10:00:00,76.48,152.053,29.048000000000002,31.988000000000003 +2020-03-22 10:15:00,73.74,151.536,29.048000000000002,31.988000000000003 +2020-03-22 10:30:00,75.72,149.9,29.048000000000002,31.988000000000003 +2020-03-22 10:45:00,74.12,148.77100000000002,29.048000000000002,31.988000000000003 +2020-03-22 11:00:00,72.15,144.928,32.02,31.988000000000003 +2020-03-22 11:15:00,77.67,143.569,32.02,31.988000000000003 +2020-03-22 11:30:00,77.66,143.93200000000002,32.02,31.988000000000003 +2020-03-22 11:45:00,82.07,144.57399999999998,32.02,31.988000000000003 +2020-03-22 12:00:00,72.99,140.191,28.55,31.988000000000003 +2020-03-22 12:15:00,75.01,140.744,28.55,31.988000000000003 +2020-03-22 12:30:00,71.52,139.441,28.55,31.988000000000003 +2020-03-22 12:45:00,69.58,139.036,28.55,31.988000000000003 +2020-03-22 13:00:00,68.21,138.718,25.601999999999997,31.988000000000003 +2020-03-22 13:15:00,67.52,138.237,25.601999999999997,31.988000000000003 +2020-03-22 13:30:00,66.81,135.855,25.601999999999997,31.988000000000003 +2020-03-22 13:45:00,67.38,134.836,25.601999999999997,31.988000000000003 +2020-03-22 14:00:00,60.59,137.475,23.916999999999998,31.988000000000003 +2020-03-22 14:15:00,59.39,136.376,23.916999999999998,31.988000000000003 +2020-03-22 14:30:00,58.18,136.016,23.916999999999998,31.988000000000003 +2020-03-22 14:45:00,60.55,136.768,23.916999999999998,31.988000000000003 +2020-03-22 15:00:00,59.16,136.64700000000002,24.064,31.988000000000003 +2020-03-22 15:15:00,60.01,136.006,24.064,31.988000000000003 +2020-03-22 15:30:00,56.92,135.28799999999998,24.064,31.988000000000003 +2020-03-22 15:45:00,58.07,135.231,24.064,31.988000000000003 +2020-03-22 16:00:00,61.44,138.836,28.189,31.988000000000003 +2020-03-22 16:15:00,62.16,141.086,28.189,31.988000000000003 +2020-03-22 16:30:00,62.09,142.095,28.189,31.988000000000003 +2020-03-22 16:45:00,63.69,141.726,28.189,31.988000000000003 +2020-03-22 17:00:00,63.44,143.861,37.576,31.988000000000003 +2020-03-22 17:15:00,65.92,146.877,37.576,31.988000000000003 +2020-03-22 17:30:00,69.07,149.62,37.576,31.988000000000003 +2020-03-22 17:45:00,69.42,151.817,37.576,31.988000000000003 +2020-03-22 18:00:00,77.2,155.935,42.669,31.988000000000003 +2020-03-22 18:15:00,75.08,159.263,42.669,31.988000000000003 +2020-03-22 18:30:00,76.8,157.589,42.669,31.988000000000003 +2020-03-22 18:45:00,76.23,159.547,42.669,31.988000000000003 +2020-03-22 19:00:00,79.58,159.303,43.538999999999994,31.988000000000003 +2020-03-22 19:15:00,77.87,158.088,43.538999999999994,31.988000000000003 +2020-03-22 19:30:00,78.53,157.924,43.538999999999994,31.988000000000003 +2020-03-22 19:45:00,80.41,157.30700000000002,43.538999999999994,31.988000000000003 +2020-03-22 20:00:00,79.37,153.393,37.330999999999996,31.988000000000003 +2020-03-22 20:15:00,79.52,151.787,37.330999999999996,31.988000000000003 +2020-03-22 20:30:00,78.47,150.509,37.330999999999996,31.988000000000003 +2020-03-22 20:45:00,77.25,147.811,37.330999999999996,31.988000000000003 +2020-03-22 21:00:00,74.47,142.727,33.856,31.988000000000003 +2020-03-22 21:15:00,73.54,140.791,33.856,31.988000000000003 +2020-03-22 21:30:00,73.03,140.843,33.856,31.988000000000003 +2020-03-22 21:45:00,72.54,140.459,33.856,31.988000000000003 +2020-03-22 22:00:00,69.45,134.984,34.711999999999996,31.988000000000003 +2020-03-22 22:15:00,69.2,132.468,34.711999999999996,31.988000000000003 +2020-03-22 22:30:00,64.59,126.84,34.711999999999996,31.988000000000003 +2020-03-22 22:45:00,67.2,123.23,34.711999999999996,31.988000000000003 +2020-03-22 23:00:00,62.87,115.586,29.698,31.988000000000003 +2020-03-22 23:15:00,62.97,112.869,29.698,31.988000000000003 +2020-03-22 23:30:00,58.12,111.583,29.698,31.988000000000003 +2020-03-22 23:45:00,60.08,110.514,29.698,31.988000000000003 +2020-03-23 00:00:00,82.34,95.79,29.983,32.166 +2020-03-23 00:15:00,83.55,93.75399999999999,29.983,32.166 +2020-03-23 00:30:00,79.21,92.929,29.983,32.166 +2020-03-23 00:45:00,78.16,92.605,29.983,32.166 +2020-03-23 01:00:00,79.25,94.01700000000001,29.122,32.166 +2020-03-23 01:15:00,80.78,94.23700000000001,29.122,32.166 +2020-03-23 01:30:00,76.93,93.65899999999999,29.122,32.166 +2020-03-23 01:45:00,77.19,93.501,29.122,32.166 +2020-03-23 02:00:00,77.95,95.383,28.676,32.166 +2020-03-23 02:15:00,78.39,95.369,28.676,32.166 +2020-03-23 02:30:00,81.25,97.412,28.676,32.166 +2020-03-23 02:45:00,76.31,98.49799999999999,28.676,32.166 +2020-03-23 03:00:00,80.56,102.464,26.552,32.166 +2020-03-23 03:15:00,79.77,104.568,26.552,32.166 +2020-03-23 03:30:00,79.74,105.24799999999999,26.552,32.166 +2020-03-23 03:45:00,77.39,106.31200000000001,26.552,32.166 +2020-03-23 04:00:00,81.0,119.811,27.44,32.166 +2020-03-23 04:15:00,84.58,132.873,27.44,32.166 +2020-03-23 04:30:00,82.05,134.373,27.44,32.166 +2020-03-23 04:45:00,82.35,134.965,27.44,32.166 +2020-03-23 05:00:00,81.8,164.62599999999998,36.825,32.166 +2020-03-23 05:15:00,86.07,196.108,36.825,32.166 +2020-03-23 05:30:00,94.52,189.77700000000002,36.825,32.166 +2020-03-23 05:45:00,100.38,180.954,36.825,32.166 +2020-03-23 06:00:00,108.8,180.22799999999998,56.589,32.166 +2020-03-23 06:15:00,114.59,184.75799999999998,56.589,32.166 +2020-03-23 06:30:00,111.33,185.50099999999998,56.589,32.166 +2020-03-23 06:45:00,111.22,187.83,56.589,32.166 +2020-03-23 07:00:00,120.07,190.862,67.49,32.166 +2020-03-23 07:15:00,121.71,192.895,67.49,32.166 +2020-03-23 07:30:00,125.53,192.547,67.49,32.166 +2020-03-23 07:45:00,126.83,190.105,67.49,32.166 +2020-03-23 08:00:00,132.73,188.553,60.028,32.166 +2020-03-23 08:15:00,130.98,187.391,60.028,32.166 +2020-03-23 08:30:00,131.58,182.579,60.028,32.166 +2020-03-23 08:45:00,133.7,179.28900000000002,60.028,32.166 +2020-03-23 09:00:00,132.55,173.55599999999998,55.018,32.166 +2020-03-23 09:15:00,133.07,169.37900000000002,55.018,32.166 +2020-03-23 09:30:00,135.0,168.774,55.018,32.166 +2020-03-23 09:45:00,134.08,166.667,55.018,32.166 +2020-03-23 10:00:00,129.98,164.472,51.183,32.166 +2020-03-23 10:15:00,130.53,163.696,51.183,32.166 +2020-03-23 10:30:00,128.68,161.246,51.183,32.166 +2020-03-23 10:45:00,130.19,160.298,51.183,32.166 +2020-03-23 11:00:00,126.38,154.475,50.065,32.166 +2020-03-23 11:15:00,128.38,154.691,50.065,32.166 +2020-03-23 11:30:00,129.3,156.38,50.065,32.166 +2020-03-23 11:45:00,130.58,156.843,50.065,32.166 +2020-03-23 12:00:00,126.27,153.531,48.141999999999996,32.166 +2020-03-23 12:15:00,128.79,154.124,48.141999999999996,32.166 +2020-03-23 12:30:00,127.53,152.702,48.141999999999996,32.166 +2020-03-23 12:45:00,127.44,153.553,48.141999999999996,32.166 +2020-03-23 13:00:00,125.66,154.064,47.887,32.166 +2020-03-23 13:15:00,135.19,152.185,47.887,32.166 +2020-03-23 13:30:00,135.03,149.401,47.887,32.166 +2020-03-23 13:45:00,132.18,148.64700000000002,47.887,32.166 +2020-03-23 14:00:00,128.57,150.628,48.571000000000005,32.166 +2020-03-23 14:15:00,127.42,149.127,48.571000000000005,32.166 +2020-03-23 14:30:00,130.25,148.237,48.571000000000005,32.166 +2020-03-23 14:45:00,133.63,149.51,48.571000000000005,32.166 +2020-03-23 15:00:00,134.03,150.812,49.937,32.166 +2020-03-23 15:15:00,132.5,148.795,49.937,32.166 +2020-03-23 15:30:00,125.71,147.537,49.937,32.166 +2020-03-23 15:45:00,123.67,146.966,49.937,32.166 +2020-03-23 16:00:00,123.22,150.901,52.963,32.166 +2020-03-23 16:15:00,127.85,152.52,52.963,32.166 +2020-03-23 16:30:00,129.07,152.556,52.963,32.166 +2020-03-23 16:45:00,128.76,151.165,52.963,32.166 +2020-03-23 17:00:00,126.16,152.803,61.163999999999994,32.166 +2020-03-23 17:15:00,121.94,155.144,61.163999999999994,32.166 +2020-03-23 17:30:00,125.11,157.346,61.163999999999994,32.166 +2020-03-23 17:45:00,128.06,158.218,61.163999999999994,32.166 +2020-03-23 18:00:00,127.94,162.369,63.788999999999994,32.166 +2020-03-23 18:15:00,126.59,163.451,63.788999999999994,32.166 +2020-03-23 18:30:00,126.68,162.086,63.788999999999994,32.166 +2020-03-23 18:45:00,129.76,165.52599999999998,63.788999999999994,32.166 +2020-03-23 19:00:00,131.55,163.859,63.913000000000004,32.166 +2020-03-23 19:15:00,124.43,162.089,63.913000000000004,32.166 +2020-03-23 19:30:00,127.1,162.246,63.913000000000004,32.166 +2020-03-23 19:45:00,128.86,160.82399999999998,63.913000000000004,32.166 +2020-03-23 20:00:00,128.96,154.60299999999998,65.44,32.166 +2020-03-23 20:15:00,120.05,151.42600000000002,65.44,32.166 +2020-03-23 20:30:00,119.49,148.838,65.44,32.166 +2020-03-23 20:45:00,118.33,147.517,65.44,32.166 +2020-03-23 21:00:00,111.59,142.67700000000002,59.117,32.166 +2020-03-23 21:15:00,108.56,139.935,59.117,32.166 +2020-03-23 21:30:00,108.54,139.444,59.117,32.166 +2020-03-23 21:45:00,110.95,138.616,59.117,32.166 +2020-03-23 22:00:00,104.38,130.222,52.301,32.166 +2020-03-23 22:15:00,99.93,127.251,52.301,32.166 +2020-03-23 22:30:00,99.37,112.665,52.301,32.166 +2020-03-23 22:45:00,98.94,105.17399999999999,52.301,32.166 +2020-03-23 23:00:00,93.28,98.132,44.373000000000005,32.166 +2020-03-23 23:15:00,87.35,97.09299999999999,44.373000000000005,32.166 +2020-03-23 23:30:00,90.84,98.01299999999999,44.373000000000005,32.166 +2020-03-23 23:45:00,92.8,98.931,44.373000000000005,32.166 +2020-03-24 00:00:00,87.21,94.149,44.647,32.166 +2020-03-24 00:15:00,81.24,93.507,44.647,32.166 +2020-03-24 00:30:00,77.75,92.161,44.647,32.166 +2020-03-24 00:45:00,80.38,91.33200000000001,44.647,32.166 +2020-03-24 01:00:00,79.49,92.387,41.433,32.166 +2020-03-24 01:15:00,81.5,92.28,41.433,32.166 +2020-03-24 01:30:00,81.05,91.77600000000001,41.433,32.166 +2020-03-24 01:45:00,82.59,91.697,41.433,32.166 +2020-03-24 02:00:00,74.4,93.40899999999999,39.909,32.166 +2020-03-24 02:15:00,83.06,93.669,39.909,32.166 +2020-03-24 02:30:00,82.28,95.147,39.909,32.166 +2020-03-24 02:45:00,84.99,96.35,39.909,32.166 +2020-03-24 03:00:00,80.16,99.23100000000001,39.14,32.166 +2020-03-24 03:15:00,82.68,100.959,39.14,32.166 +2020-03-24 03:30:00,86.24,101.995,39.14,32.166 +2020-03-24 03:45:00,85.57,102.86200000000001,39.14,32.166 +2020-03-24 04:00:00,85.36,115.8,40.015,32.166 +2020-03-24 04:15:00,84.21,128.582,40.015,32.166 +2020-03-24 04:30:00,88.34,129.81799999999998,40.015,32.166 +2020-03-24 04:45:00,83.04,131.484,40.015,32.166 +2020-03-24 05:00:00,83.64,165.446,44.93600000000001,32.166 +2020-03-24 05:15:00,88.14,196.958,44.93600000000001,32.166 +2020-03-24 05:30:00,90.73,189.49900000000002,44.93600000000001,32.166 +2020-03-24 05:45:00,92.93,180.458,44.93600000000001,32.166 +2020-03-24 06:00:00,104.65,179.174,57.271,32.166 +2020-03-24 06:15:00,109.33,185.017,57.271,32.166 +2020-03-24 06:30:00,113.7,185.11599999999999,57.271,32.166 +2020-03-24 06:45:00,113.97,186.847,57.271,32.166 +2020-03-24 07:00:00,123.2,189.797,68.352,32.166 +2020-03-24 07:15:00,124.58,191.597,68.352,32.166 +2020-03-24 07:30:00,126.5,190.78599999999997,68.352,32.166 +2020-03-24 07:45:00,129.13,188.18099999999998,68.352,32.166 +2020-03-24 08:00:00,133.3,186.676,60.717,32.166 +2020-03-24 08:15:00,134.37,184.57,60.717,32.166 +2020-03-24 08:30:00,134.29,179.618,60.717,32.166 +2020-03-24 08:45:00,134.4,175.868,60.717,32.166 +2020-03-24 09:00:00,132.5,169.576,54.603,32.166 +2020-03-24 09:15:00,131.97,166.65,54.603,32.166 +2020-03-24 09:30:00,130.44,166.808,54.603,32.166 +2020-03-24 09:45:00,126.35,164.928,54.603,32.166 +2020-03-24 10:00:00,123.85,161.881,52.308,32.166 +2020-03-24 10:15:00,123.58,160.246,52.308,32.166 +2020-03-24 10:30:00,121.34,157.974,52.308,32.166 +2020-03-24 10:45:00,117.88,157.553,52.308,32.166 +2020-03-24 11:00:00,119.32,152.941,51.838,32.166 +2020-03-24 11:15:00,119.66,153.012,51.838,32.166 +2020-03-24 11:30:00,122.6,153.441,51.838,32.166 +2020-03-24 11:45:00,119.94,154.38299999999998,51.838,32.166 +2020-03-24 12:00:00,116.79,149.907,50.375,32.166 +2020-03-24 12:15:00,116.39,150.251,50.375,32.166 +2020-03-24 12:30:00,117.13,149.612,50.375,32.166 +2020-03-24 12:45:00,113.27,150.39600000000002,50.375,32.166 +2020-03-24 13:00:00,113.92,150.511,50.735,32.166 +2020-03-24 13:15:00,118.49,148.784,50.735,32.166 +2020-03-24 13:30:00,117.98,146.963,50.735,32.166 +2020-03-24 13:45:00,116.71,146.122,50.735,32.166 +2020-03-24 14:00:00,115.13,148.52100000000002,50.946000000000005,32.166 +2020-03-24 14:15:00,113.45,147.077,50.946000000000005,32.166 +2020-03-24 14:30:00,110.95,146.75799999999998,50.946000000000005,32.166 +2020-03-24 14:45:00,110.16,147.769,50.946000000000005,32.166 +2020-03-24 15:00:00,114.6,148.683,53.18,32.166 +2020-03-24 15:15:00,116.6,147.15,53.18,32.166 +2020-03-24 15:30:00,109.45,145.975,53.18,32.166 +2020-03-24 15:45:00,117.18,145.137,53.18,32.166 +2020-03-24 16:00:00,117.12,149.214,54.928999999999995,32.166 +2020-03-24 16:15:00,118.32,151.22299999999998,54.928999999999995,32.166 +2020-03-24 16:30:00,119.29,151.694,54.928999999999995,32.166 +2020-03-24 16:45:00,117.54,150.678,54.928999999999995,32.166 +2020-03-24 17:00:00,119.96,152.85299999999998,60.913000000000004,32.166 +2020-03-24 17:15:00,118.56,155.32399999999998,60.913000000000004,32.166 +2020-03-24 17:30:00,119.39,157.963,60.913000000000004,32.166 +2020-03-24 17:45:00,119.91,158.639,60.913000000000004,32.166 +2020-03-24 18:00:00,123.8,162.444,62.214,32.166 +2020-03-24 18:15:00,123.93,163.588,62.214,32.166 +2020-03-24 18:30:00,123.06,161.892,62.214,32.166 +2020-03-24 18:45:00,120.31,165.94099999999997,62.214,32.166 +2020-03-24 19:00:00,128.27,163.985,62.38,32.166 +2020-03-24 19:15:00,128.22,162.053,62.38,32.166 +2020-03-24 19:30:00,127.86,161.632,62.38,32.166 +2020-03-24 19:45:00,126.09,160.375,62.38,32.166 +2020-03-24 20:00:00,128.41,154.361,65.018,32.166 +2020-03-24 20:15:00,124.95,150.27200000000002,65.018,32.166 +2020-03-24 20:30:00,121.53,148.543,65.018,32.166 +2020-03-24 20:45:00,118.91,146.891,65.018,32.166 +2020-03-24 21:00:00,112.76,141.732,56.416000000000004,32.166 +2020-03-24 21:15:00,116.6,139.299,56.416000000000004,32.166 +2020-03-24 21:30:00,112.66,138.269,56.416000000000004,32.166 +2020-03-24 21:45:00,107.6,137.704,56.416000000000004,32.166 +2020-03-24 22:00:00,104.55,130.731,52.846000000000004,32.166 +2020-03-24 22:15:00,103.78,127.48100000000001,52.846000000000004,32.166 +2020-03-24 22:30:00,102.03,113.005,52.846000000000004,32.166 +2020-03-24 22:45:00,97.36,105.73200000000001,52.846000000000004,32.166 +2020-03-24 23:00:00,93.61,98.484,44.435,32.166 +2020-03-24 23:15:00,97.37,97.18700000000001,44.435,32.166 +2020-03-24 23:30:00,91.61,97.818,44.435,32.166 +2020-03-24 23:45:00,86.1,98.45700000000001,44.435,32.166 +2020-03-25 00:00:00,86.32,93.757,42.527,32.166 +2020-03-25 00:15:00,85.72,93.12899999999999,42.527,32.166 +2020-03-25 00:30:00,83.35,91.76899999999999,42.527,32.166 +2020-03-25 00:45:00,80.67,90.944,42.527,32.166 +2020-03-25 01:00:00,76.45,91.96600000000001,38.655,32.166 +2020-03-25 01:15:00,74.51,91.84,38.655,32.166 +2020-03-25 01:30:00,78.1,91.316,38.655,32.166 +2020-03-25 01:45:00,84.0,91.244,38.655,32.166 +2020-03-25 02:00:00,80.41,92.943,36.912,32.166 +2020-03-25 02:15:00,80.95,93.19200000000001,36.912,32.166 +2020-03-25 02:30:00,75.56,94.69,36.912,32.166 +2020-03-25 02:45:00,79.65,95.89399999999999,36.912,32.166 +2020-03-25 03:00:00,81.67,98.792,36.98,32.166 +2020-03-25 03:15:00,81.92,100.49799999999999,36.98,32.166 +2020-03-25 03:30:00,82.53,101.52600000000001,36.98,32.166 +2020-03-25 03:45:00,81.28,102.40899999999999,36.98,32.166 +2020-03-25 04:00:00,76.87,115.345,38.052,32.166 +2020-03-25 04:15:00,82.91,128.107,38.052,32.166 +2020-03-25 04:30:00,87.12,129.356,38.052,32.166 +2020-03-25 04:45:00,89.8,131.01,38.052,32.166 +2020-03-25 05:00:00,84.78,164.93,42.455,32.166 +2020-03-25 05:15:00,86.31,196.429,42.455,32.166 +2020-03-25 05:30:00,91.91,188.94799999999998,42.455,32.166 +2020-03-25 05:45:00,94.23,179.926,42.455,32.166 +2020-03-25 06:00:00,103.12,178.65200000000002,57.986000000000004,32.166 +2020-03-25 06:15:00,110.12,184.49099999999999,57.986000000000004,32.166 +2020-03-25 06:30:00,113.23,184.548,57.986000000000004,32.166 +2020-03-25 06:45:00,114.61,186.25900000000001,57.986000000000004,32.166 +2020-03-25 07:00:00,121.68,189.222,71.868,32.166 +2020-03-25 07:15:00,122.82,190.99200000000002,71.868,32.166 +2020-03-25 07:30:00,123.06,190.141,71.868,32.166 +2020-03-25 07:45:00,125.56,187.507,71.868,32.166 +2020-03-25 08:00:00,131.42,185.979,62.225,32.166 +2020-03-25 08:15:00,131.94,183.877,62.225,32.166 +2020-03-25 08:30:00,129.5,178.877,62.225,32.166 +2020-03-25 08:45:00,130.38,175.15099999999998,62.225,32.166 +2020-03-25 09:00:00,128.7,168.872,58.802,32.166 +2020-03-25 09:15:00,130.92,165.947,58.802,32.166 +2020-03-25 09:30:00,130.3,166.12400000000002,58.802,32.166 +2020-03-25 09:45:00,129.49,164.265,58.802,32.166 +2020-03-25 10:00:00,129.92,161.23,54.122,32.166 +2020-03-25 10:15:00,129.34,159.642,54.122,32.166 +2020-03-25 10:30:00,131.48,157.39600000000002,54.122,32.166 +2020-03-25 10:45:00,131.23,156.995,54.122,32.166 +2020-03-25 11:00:00,133.57,152.376,54.368,32.166 +2020-03-25 11:15:00,133.01,152.469,54.368,32.166 +2020-03-25 11:30:00,135.91,152.905,54.368,32.166 +2020-03-25 11:45:00,134.24,153.866,54.368,32.166 +2020-03-25 12:00:00,134.65,149.411,52.74,32.166 +2020-03-25 12:15:00,134.3,149.766,52.74,32.166 +2020-03-25 12:30:00,131.7,149.085,52.74,32.166 +2020-03-25 12:45:00,131.3,149.868,52.74,32.166 +2020-03-25 13:00:00,128.24,150.028,52.544,32.166 +2020-03-25 13:15:00,135.9,148.285,52.544,32.166 +2020-03-25 13:30:00,134.63,146.457,52.544,32.166 +2020-03-25 13:45:00,130.07,145.619,52.544,32.166 +2020-03-25 14:00:00,125.5,148.08700000000002,53.602,32.166 +2020-03-25 14:15:00,124.21,146.62,53.602,32.166 +2020-03-25 14:30:00,121.48,146.262,53.602,32.166 +2020-03-25 14:45:00,124.82,147.278,53.602,32.166 +2020-03-25 15:00:00,128.87,148.209,55.59,32.166 +2020-03-25 15:15:00,132.49,146.649,55.59,32.166 +2020-03-25 15:30:00,130.48,145.423,55.59,32.166 +2020-03-25 15:45:00,125.19,144.566,55.59,32.166 +2020-03-25 16:00:00,124.83,148.662,57.586999999999996,32.166 +2020-03-25 16:15:00,120.76,150.645,57.586999999999996,32.166 +2020-03-25 16:30:00,126.07,151.116,57.586999999999996,32.166 +2020-03-25 16:45:00,129.45,150.047,57.586999999999996,32.166 +2020-03-25 17:00:00,129.43,152.254,62.111999999999995,32.166 +2020-03-25 17:15:00,125.36,154.717,62.111999999999995,32.166 +2020-03-25 17:30:00,128.18,157.368,62.111999999999995,32.166 +2020-03-25 17:45:00,127.2,158.043,62.111999999999995,32.166 +2020-03-25 18:00:00,131.15,161.856,64.605,32.166 +2020-03-25 18:15:00,124.35,163.05700000000002,64.605,32.166 +2020-03-25 18:30:00,128.29,161.352,64.605,32.166 +2020-03-25 18:45:00,127.92,165.417,64.605,32.166 +2020-03-25 19:00:00,132.44,163.43200000000002,65.55199999999999,32.166 +2020-03-25 19:15:00,123.83,161.515,65.55199999999999,32.166 +2020-03-25 19:30:00,129.2,161.12,65.55199999999999,32.166 +2020-03-25 19:45:00,131.36,159.905,65.55199999999999,32.166 +2020-03-25 20:00:00,129.62,153.862,66.778,32.166 +2020-03-25 20:15:00,119.8,149.78799999999998,66.778,32.166 +2020-03-25 20:30:00,115.17,148.091,66.778,32.166 +2020-03-25 20:45:00,115.5,146.44899999999998,66.778,32.166 +2020-03-25 21:00:00,109.54,141.285,56.103,32.166 +2020-03-25 21:15:00,115.91,138.856,56.103,32.166 +2020-03-25 21:30:00,110.45,137.822,56.103,32.166 +2020-03-25 21:45:00,111.02,137.282,56.103,32.166 +2020-03-25 22:00:00,101.35,130.30200000000002,51.371,32.166 +2020-03-25 22:15:00,99.56,127.07799999999999,51.371,32.166 +2020-03-25 22:30:00,100.15,112.553,51.371,32.166 +2020-03-25 22:45:00,100.47,105.279,51.371,32.166 +2020-03-25 23:00:00,91.64,98.023,42.798,32.166 +2020-03-25 23:15:00,90.67,96.74799999999999,42.798,32.166 +2020-03-25 23:30:00,91.31,97.381,42.798,32.166 +2020-03-25 23:45:00,89.57,98.04299999999999,42.798,32.166 +2020-03-26 00:00:00,81.94,93.36200000000001,39.069,32.166 +2020-03-26 00:15:00,81.2,92.749,39.069,32.166 +2020-03-26 00:30:00,84.64,91.374,39.069,32.166 +2020-03-26 00:45:00,84.28,90.554,39.069,32.166 +2020-03-26 01:00:00,81.91,91.542,37.043,32.166 +2020-03-26 01:15:00,73.61,91.398,37.043,32.166 +2020-03-26 01:30:00,80.41,90.853,37.043,32.166 +2020-03-26 01:45:00,81.37,90.79,37.043,32.166 +2020-03-26 02:00:00,77.06,92.475,34.625,32.166 +2020-03-26 02:15:00,76.31,92.712,34.625,32.166 +2020-03-26 02:30:00,71.77,94.23,34.625,32.166 +2020-03-26 02:45:00,75.66,95.43700000000001,34.625,32.166 +2020-03-26 03:00:00,77.66,98.35,33.812,32.166 +2020-03-26 03:15:00,81.48,100.03200000000001,33.812,32.166 +2020-03-26 03:30:00,74.62,101.055,33.812,32.166 +2020-03-26 03:45:00,78.13,101.95299999999999,33.812,32.166 +2020-03-26 04:00:00,75.81,114.887,35.236999999999995,32.166 +2020-03-26 04:15:00,75.77,127.63,35.236999999999995,32.166 +2020-03-26 04:30:00,74.86,128.892,35.236999999999995,32.166 +2020-03-26 04:45:00,78.41,130.533,35.236999999999995,32.166 +2020-03-26 05:00:00,84.12,164.412,40.375,32.166 +2020-03-26 05:15:00,86.05,195.898,40.375,32.166 +2020-03-26 05:30:00,87.24,188.39700000000002,40.375,32.166 +2020-03-26 05:45:00,92.96,179.391,40.375,32.166 +2020-03-26 06:00:00,98.47,178.128,52.316,32.166 +2020-03-26 06:15:00,106.48,183.963,52.316,32.166 +2020-03-26 06:30:00,108.98,183.97799999999998,52.316,32.166 +2020-03-26 06:45:00,110.31,185.66400000000002,52.316,32.166 +2020-03-26 07:00:00,119.63,188.642,64.115,32.166 +2020-03-26 07:15:00,121.23,190.38400000000001,64.115,32.166 +2020-03-26 07:30:00,122.47,189.49099999999999,64.115,32.166 +2020-03-26 07:45:00,125.54,186.829,64.115,32.166 +2020-03-26 08:00:00,126.35,185.27700000000002,55.033,32.166 +2020-03-26 08:15:00,127.67,183.18099999999998,55.033,32.166 +2020-03-26 08:30:00,128.36,178.132,55.033,32.166 +2020-03-26 08:45:00,129.96,174.43099999999998,55.033,32.166 +2020-03-26 09:00:00,128.86,168.16400000000002,49.411,32.166 +2020-03-26 09:15:00,126.99,165.24200000000002,49.411,32.166 +2020-03-26 09:30:00,128.41,165.438,49.411,32.166 +2020-03-26 09:45:00,128.5,163.6,49.411,32.166 +2020-03-26 10:00:00,125.96,160.575,45.82899999999999,32.166 +2020-03-26 10:15:00,126.19,159.036,45.82899999999999,32.166 +2020-03-26 10:30:00,122.45,156.815,45.82899999999999,32.166 +2020-03-26 10:45:00,122.6,156.434,45.82899999999999,32.166 +2020-03-26 11:00:00,124.89,151.809,44.333,32.166 +2020-03-26 11:15:00,127.98,151.92600000000002,44.333,32.166 +2020-03-26 11:30:00,126.17,152.36700000000002,44.333,32.166 +2020-03-26 11:45:00,127.4,153.346,44.333,32.166 +2020-03-26 12:00:00,122.97,148.91299999999998,42.95,32.166 +2020-03-26 12:15:00,124.07,149.278,42.95,32.166 +2020-03-26 12:30:00,129.47,148.55700000000002,42.95,32.166 +2020-03-26 12:45:00,132.19,149.338,42.95,32.166 +2020-03-26 13:00:00,126.6,149.54399999999998,42.489,32.166 +2020-03-26 13:15:00,121.2,147.784,42.489,32.166 +2020-03-26 13:30:00,122.77,145.94799999999998,42.489,32.166 +2020-03-26 13:45:00,122.03,145.113,42.489,32.166 +2020-03-26 14:00:00,118.12,147.65200000000002,43.448,32.166 +2020-03-26 14:15:00,120.48,146.16,43.448,32.166 +2020-03-26 14:30:00,124.87,145.765,43.448,32.166 +2020-03-26 14:45:00,124.56,146.787,43.448,32.166 +2020-03-26 15:00:00,122.41,147.733,45.994,32.166 +2020-03-26 15:15:00,117.85,146.145,45.994,32.166 +2020-03-26 15:30:00,118.99,144.86700000000002,45.994,32.166 +2020-03-26 15:45:00,120.98,143.993,45.994,32.166 +2020-03-26 16:00:00,116.33,148.107,48.167,32.166 +2020-03-26 16:15:00,113.12,150.065,48.167,32.166 +2020-03-26 16:30:00,110.23,150.535,48.167,32.166 +2020-03-26 16:45:00,103.8,149.41299999999998,48.167,32.166 +2020-03-26 17:00:00,103.8,151.651,52.637,32.166 +2020-03-26 17:15:00,109.33,154.106,52.637,32.166 +2020-03-26 17:30:00,115.38,156.77100000000002,52.637,32.166 +2020-03-26 17:45:00,118.72,157.446,52.637,32.166 +2020-03-26 18:00:00,121.33,161.264,55.739,32.166 +2020-03-26 18:15:00,118.59,162.52200000000002,55.739,32.166 +2020-03-26 18:30:00,113.07,160.809,55.739,32.166 +2020-03-26 18:45:00,113.68,164.889,55.739,32.166 +2020-03-26 19:00:00,113.24,162.876,56.36600000000001,32.166 +2020-03-26 19:15:00,115.41,160.976,56.36600000000001,32.166 +2020-03-26 19:30:00,121.08,160.605,56.36600000000001,32.166 +2020-03-26 19:45:00,117.92,159.431,56.36600000000001,32.166 +2020-03-26 20:00:00,115.8,153.36,56.338,32.166 +2020-03-26 20:15:00,112.12,149.299,56.338,32.166 +2020-03-26 20:30:00,109.36,147.637,56.338,32.166 +2020-03-26 20:45:00,108.08,146.005,56.338,32.166 +2020-03-26 21:00:00,100.24,140.836,49.894,32.166 +2020-03-26 21:15:00,98.69,138.41,49.894,32.166 +2020-03-26 21:30:00,92.64,137.374,49.894,32.166 +2020-03-26 21:45:00,91.85,136.857,49.894,32.166 +2020-03-26 22:00:00,85.8,129.87,46.687,32.166 +2020-03-26 22:15:00,85.45,126.67299999999999,46.687,32.166 +2020-03-26 22:30:00,78.83,112.09899999999999,46.687,32.166 +2020-03-26 22:45:00,80.03,104.823,46.687,32.166 +2020-03-26 23:00:00,73.44,97.556,39.211,32.166 +2020-03-26 23:15:00,74.45,96.307,39.211,32.166 +2020-03-26 23:30:00,71.79,96.94200000000001,39.211,32.166 +2020-03-26 23:45:00,69.13,97.62700000000001,39.211,32.166 +2020-03-27 00:00:00,81.29,91.55799999999999,36.616,32.166 +2020-03-27 00:15:00,83.4,91.184,36.616,32.166 +2020-03-27 00:30:00,84.17,89.77600000000001,36.616,32.166 +2020-03-27 00:45:00,83.96,89.18,36.616,32.166 +2020-03-27 01:00:00,77.52,89.76700000000001,33.799,32.166 +2020-03-27 01:15:00,76.7,90.131,33.799,32.166 +2020-03-27 01:30:00,76.43,89.62,33.799,32.166 +2020-03-27 01:45:00,77.15,89.56700000000001,33.799,32.166 +2020-03-27 02:00:00,76.99,91.602,32.968,32.166 +2020-03-27 02:15:00,77.28,91.721,32.968,32.166 +2020-03-27 02:30:00,77.43,93.932,32.968,32.166 +2020-03-27 02:45:00,79.33,94.956,32.968,32.166 +2020-03-27 03:00:00,79.5,97.36200000000001,33.533,32.166 +2020-03-27 03:15:00,77.66,99.35600000000001,33.533,32.166 +2020-03-27 03:30:00,81.27,100.28299999999999,33.533,32.166 +2020-03-27 03:45:00,79.74,101.734,33.533,32.166 +2020-03-27 04:00:00,79.68,114.887,36.102,32.166 +2020-03-27 04:15:00,79.42,126.90899999999999,36.102,32.166 +2020-03-27 04:30:00,77.92,128.674,36.102,32.166 +2020-03-27 04:45:00,80.87,129.208,36.102,32.166 +2020-03-27 05:00:00,85.69,161.907,42.423,32.166 +2020-03-27 05:15:00,87.55,194.855,42.423,32.166 +2020-03-27 05:30:00,89.94,188.229,42.423,32.166 +2020-03-27 05:45:00,94.07,179.046,42.423,32.166 +2020-03-27 06:00:00,101.92,178.21599999999998,55.38,32.166 +2020-03-27 06:15:00,105.46,182.955,55.38,32.166 +2020-03-27 06:30:00,108.23,182.237,55.38,32.166 +2020-03-27 06:45:00,111.45,185.12599999999998,55.38,32.166 +2020-03-27 07:00:00,118.3,187.672,65.929,32.166 +2020-03-27 07:15:00,118.8,190.475,65.929,32.166 +2020-03-27 07:30:00,122.1,188.794,65.929,32.166 +2020-03-27 07:45:00,124.3,185.33900000000003,65.929,32.166 +2020-03-27 08:00:00,127.28,183.12400000000002,57.336999999999996,32.166 +2020-03-27 08:15:00,127.22,180.92,57.336999999999996,32.166 +2020-03-27 08:30:00,127.94,176.578,57.336999999999996,32.166 +2020-03-27 08:45:00,128.1,171.59599999999998,57.336999999999996,32.166 +2020-03-27 09:00:00,127.49,165.024,54.226000000000006,32.166 +2020-03-27 09:15:00,128.67,163.12,54.226000000000006,32.166 +2020-03-27 09:30:00,129.09,162.789,54.226000000000006,32.166 +2020-03-27 09:45:00,128.83,160.995,54.226000000000006,32.166 +2020-03-27 10:00:00,127.42,156.991,51.298,32.166 +2020-03-27 10:15:00,127.88,156.01,51.298,32.166 +2020-03-27 10:30:00,126.44,153.909,51.298,32.166 +2020-03-27 10:45:00,127.39,153.159,51.298,32.166 +2020-03-27 11:00:00,126.52,148.564,50.839,32.166 +2020-03-27 11:15:00,127.61,147.666,50.839,32.166 +2020-03-27 11:30:00,125.8,149.467,50.839,32.166 +2020-03-27 11:45:00,125.03,150.244,50.839,32.166 +2020-03-27 12:00:00,122.0,146.861,47.976000000000006,32.166 +2020-03-27 12:15:00,122.03,145.314,47.976000000000006,32.166 +2020-03-27 12:30:00,121.02,144.716,47.976000000000006,32.166 +2020-03-27 12:45:00,120.19,145.718,47.976000000000006,32.166 +2020-03-27 13:00:00,119.24,146.917,46.299,32.166 +2020-03-27 13:15:00,120.7,145.874,46.299,32.166 +2020-03-27 13:30:00,117.46,144.253,46.299,32.166 +2020-03-27 13:45:00,119.26,143.44799999999998,46.299,32.166 +2020-03-27 14:00:00,117.14,144.891,44.971000000000004,32.166 +2020-03-27 14:15:00,113.78,143.339,44.971000000000004,32.166 +2020-03-27 14:30:00,110.02,143.72899999999998,44.971000000000004,32.166 +2020-03-27 14:45:00,113.2,144.83100000000002,44.971000000000004,32.166 +2020-03-27 15:00:00,114.23,145.365,47.48,32.166 +2020-03-27 15:15:00,112.17,143.32,47.48,32.166 +2020-03-27 15:30:00,106.47,140.567,47.48,32.166 +2020-03-27 15:45:00,107.87,139.994,47.48,32.166 +2020-03-27 16:00:00,110.03,142.95,50.648,32.166 +2020-03-27 16:15:00,110.03,145.256,50.648,32.166 +2020-03-27 16:30:00,112.39,145.755,50.648,32.166 +2020-03-27 16:45:00,112.32,144.27,50.648,32.166 +2020-03-27 17:00:00,115.94,147.203,56.251000000000005,32.166 +2020-03-27 17:15:00,116.05,149.262,56.251000000000005,32.166 +2020-03-27 17:30:00,117.7,151.722,56.251000000000005,32.166 +2020-03-27 17:45:00,118.13,152.159,56.251000000000005,32.166 +2020-03-27 18:00:00,121.7,156.57299999999998,58.982,32.166 +2020-03-27 18:15:00,122.86,157.303,58.982,32.166 +2020-03-27 18:30:00,122.89,155.875,58.982,32.166 +2020-03-27 18:45:00,124.43,160.083,58.982,32.166 +2020-03-27 19:00:00,125.7,159.037,57.293,32.166 +2020-03-27 19:15:00,121.2,158.434,57.293,32.166 +2020-03-27 19:30:00,121.4,157.769,57.293,32.166 +2020-03-27 19:45:00,119.95,155.972,57.293,32.166 +2020-03-27 20:00:00,116.48,149.879,59.433,32.166 +2020-03-27 20:15:00,114.79,146.07,59.433,32.166 +2020-03-27 20:30:00,113.96,144.265,59.433,32.166 +2020-03-27 20:45:00,113.11,142.858,59.433,32.166 +2020-03-27 21:00:00,108.02,138.48,52.153999999999996,32.166 +2020-03-27 21:15:00,105.58,136.90200000000002,52.153999999999996,32.166 +2020-03-27 21:30:00,99.98,135.85399999999998,52.153999999999996,32.166 +2020-03-27 21:45:00,100.41,135.86,52.153999999999996,32.166 +2020-03-27 22:00:00,93.64,129.616,47.125,32.166 +2020-03-27 22:15:00,92.04,126.265,47.125,32.166 +2020-03-27 22:30:00,91.62,118.181,47.125,32.166 +2020-03-27 22:45:00,93.54,114.007,47.125,32.166 +2020-03-27 23:00:00,88.63,106.86399999999999,41.236000000000004,32.166 +2020-03-27 23:15:00,85.58,103.60600000000001,41.236000000000004,32.166 +2020-03-27 23:30:00,83.35,102.531,41.236000000000004,32.166 +2020-03-27 23:45:00,81.51,102.666,41.236000000000004,32.166 +2020-03-28 00:00:00,57.71,89.321,36.484,31.988000000000003 +2020-03-28 00:15:00,54.17,85.42,36.484,31.988000000000003 +2020-03-28 00:30:00,56.34,84.88799999999999,36.484,31.988000000000003 +2020-03-28 00:45:00,54.63,84.62299999999999,36.484,31.988000000000003 +2020-03-28 01:00:00,49.06,85.77,32.391999999999996,31.988000000000003 +2020-03-28 01:15:00,52.94,85.538,32.391999999999996,31.988000000000003 +2020-03-28 01:30:00,49.74,84.344,32.391999999999996,31.988000000000003 +2020-03-28 01:45:00,52.99,84.53,32.391999999999996,31.988000000000003 +2020-03-28 02:00:00,51.54,86.786,30.194000000000003,31.988000000000003 +2020-03-28 02:15:00,52.29,86.351,30.194000000000003,31.988000000000003 +2020-03-28 02:30:00,52.34,87.463,30.194000000000003,31.988000000000003 +2020-03-28 02:45:00,50.12,88.823,30.194000000000003,31.988000000000003 +2020-03-28 03:00:00,50.63,91.3,29.677,31.988000000000003 +2020-03-28 03:15:00,47.78,92.103,29.677,31.988000000000003 +2020-03-28 03:30:00,48.43,91.905,29.677,31.988000000000003 +2020-03-28 03:45:00,51.9,93.94200000000001,29.677,31.988000000000003 +2020-03-28 04:00:00,49.36,103.32,29.616,31.988000000000003 +2020-03-28 04:15:00,52.2,113.07700000000001,29.616,31.988000000000003 +2020-03-28 04:30:00,53.04,112.63799999999999,29.616,31.988000000000003 +2020-03-28 04:45:00,51.37,112.87,29.616,31.988000000000003 +2020-03-28 05:00:00,55.2,130.638,29.625,31.988000000000003 +2020-03-28 05:15:00,53.94,145.415,29.625,31.988000000000003 +2020-03-28 05:30:00,57.62,139.55100000000002,29.625,31.988000000000003 +2020-03-28 05:45:00,59.67,136.032,29.625,31.988000000000003 +2020-03-28 06:00:00,60.42,153.085,30.551,31.988000000000003 +2020-03-28 06:15:00,63.03,172.97099999999998,30.551,31.988000000000003 +2020-03-28 06:30:00,60.51,167.139,30.551,31.988000000000003 +2020-03-28 06:45:00,61.22,162.032,30.551,31.988000000000003 +2020-03-28 07:00:00,66.83,160.97,34.865,31.988000000000003 +2020-03-28 07:15:00,68.4,162.357,34.865,31.988000000000003 +2020-03-28 07:30:00,67.37,163.225,34.865,31.988000000000003 +2020-03-28 07:45:00,72.4,163.143,34.865,31.988000000000003 +2020-03-28 08:00:00,72.28,164.06799999999998,41.456,31.988000000000003 +2020-03-28 08:15:00,77.29,164.692,41.456,31.988000000000003 +2020-03-28 08:30:00,80.4,161.583,41.456,31.988000000000003 +2020-03-28 08:45:00,80.83,159.421,41.456,31.988000000000003 +2020-03-28 09:00:00,86.02,155.105,43.001999999999995,31.988000000000003 +2020-03-28 09:15:00,86.55,153.95,43.001999999999995,31.988000000000003 +2020-03-28 09:30:00,87.98,154.50799999999998,43.001999999999995,31.988000000000003 +2020-03-28 09:45:00,89.07,152.734,43.001999999999995,31.988000000000003 +2020-03-28 10:00:00,88.02,149.053,42.047,31.988000000000003 +2020-03-28 10:15:00,91.44,148.35399999999998,42.047,31.988000000000003 +2020-03-28 10:30:00,89.49,146.31799999999998,42.047,31.988000000000003 +2020-03-28 10:45:00,95.01,146.55200000000002,42.047,31.988000000000003 +2020-03-28 11:00:00,94.88,142.039,39.894,31.988000000000003 +2020-03-28 11:15:00,97.49,140.834,39.894,31.988000000000003 +2020-03-28 11:30:00,97.87,141.798,39.894,31.988000000000003 +2020-03-28 11:45:00,97.43,141.989,39.894,31.988000000000003 +2020-03-28 12:00:00,92.61,137.923,38.122,31.988000000000003 +2020-03-28 12:15:00,94.95,137.143,38.122,31.988000000000003 +2020-03-28 12:30:00,90.34,136.732,38.122,31.988000000000003 +2020-03-28 12:45:00,86.38,137.285,38.122,31.988000000000003 +2020-03-28 13:00:00,82.75,137.92700000000002,34.645,31.988000000000003 +2020-03-28 13:15:00,84.34,135.013,34.645,31.988000000000003 +2020-03-28 13:30:00,84.37,133.059,34.645,31.988000000000003 +2020-03-28 13:45:00,84.04,132.303,34.645,31.988000000000003 +2020-03-28 14:00:00,79.67,134.838,33.739000000000004,31.988000000000003 +2020-03-28 14:15:00,81.26,132.49200000000002,33.739000000000004,31.988000000000003 +2020-03-28 14:30:00,79.9,131.214,33.739000000000004,31.988000000000003 +2020-03-28 14:45:00,79.54,132.628,33.739000000000004,31.988000000000003 +2020-03-28 15:00:00,78.41,133.822,35.908,31.988000000000003 +2020-03-28 15:15:00,80.8,132.606,35.908,31.988000000000003 +2020-03-28 15:30:00,80.27,131.143,35.908,31.988000000000003 +2020-03-28 15:45:00,77.77,130.329,35.908,31.988000000000003 +2020-03-28 16:00:00,80.98,133.009,39.249,31.988000000000003 +2020-03-28 16:15:00,80.76,135.78799999999998,39.249,31.988000000000003 +2020-03-28 16:30:00,81.37,136.305,39.249,31.988000000000003 +2020-03-28 16:45:00,79.09,135.488,39.249,31.988000000000003 +2020-03-28 17:00:00,83.53,137.756,46.045,31.988000000000003 +2020-03-28 17:15:00,82.0,140.559,46.045,31.988000000000003 +2020-03-28 17:30:00,86.27,142.915,46.045,31.988000000000003 +2020-03-28 17:45:00,87.5,143.157,46.045,31.988000000000003 +2020-03-28 18:00:00,89.89,147.595,48.238,31.988000000000003 +2020-03-28 18:15:00,89.13,150.316,48.238,31.988000000000003 +2020-03-28 18:30:00,88.57,150.346,48.238,31.988000000000003 +2020-03-28 18:45:00,85.87,150.866,48.238,31.988000000000003 +2020-03-28 19:00:00,91.17,150.105,46.785,31.988000000000003 +2020-03-28 19:15:00,86.77,148.829,46.785,31.988000000000003 +2020-03-28 19:30:00,89.79,149.017,46.785,31.988000000000003 +2020-03-28 19:45:00,91.35,147.619,46.785,31.988000000000003 +2020-03-28 20:00:00,86.33,143.537,39.830999999999996,31.988000000000003 +2020-03-28 20:15:00,86.08,141.27,39.830999999999996,31.988000000000003 +2020-03-28 20:30:00,84.34,138.96200000000002,39.830999999999996,31.988000000000003 +2020-03-28 20:45:00,82.32,137.77700000000002,39.830999999999996,31.988000000000003 +2020-03-28 21:00:00,78.48,134.78799999999998,34.063,31.988000000000003 +2020-03-28 21:15:00,78.09,133.465,34.063,31.988000000000003 +2020-03-28 21:30:00,73.54,133.45600000000002,34.063,31.988000000000003 +2020-03-28 21:45:00,76.09,133.001,34.063,31.988000000000003 +2020-03-28 22:00:00,73.18,127.82700000000001,34.455999999999996,31.988000000000003 +2020-03-28 22:15:00,71.44,126.572,34.455999999999996,31.988000000000003 +2020-03-28 22:30:00,69.55,123.32799999999999,34.455999999999996,31.988000000000003 +2020-03-28 22:45:00,69.05,120.762,34.455999999999996,31.988000000000003 +2020-03-28 23:00:00,66.49,115.384,27.840999999999998,31.988000000000003 +2020-03-28 23:15:00,65.31,110.95700000000001,27.840999999999998,31.988000000000003 +2020-03-28 23:30:00,60.24,109.191,27.840999999999998,31.988000000000003 +2020-03-28 23:45:00,61.54,107.5,27.840999999999998,31.988000000000003 +2020-03-29 00:00:00,63.42,89.681,20.007,31.988000000000003 +2020-03-29 00:15:00,63.84,85.18,20.007,31.988000000000003 +2020-03-29 00:30:00,63.41,84.29700000000001,20.007,31.988000000000003 +2020-03-29 00:45:00,63.81,84.542,20.007,31.988000000000003 +2020-03-29 01:00:00,61.41,85.62700000000001,17.378,31.988000000000003 +2020-03-29 01:15:00,61.91,86.14200000000001,17.378,31.988000000000003 +2020-03-29 01:30:00,61.2,85.287,17.378,31.988000000000003 +2020-03-29 01:45:00,61.64,85.115,17.378,31.988000000000003 +2020-03-29 03:00:00,60.55,92.348,15.427999999999999,31.988000000000003 +2020-03-29 03:15:00,61.96,92.81200000000001,15.427999999999999,31.988000000000003 +2020-03-29 03:30:00,61.93,93.43799999999999,15.427999999999999,31.988000000000003 +2020-03-29 03:45:00,61.8,95.17,15.427999999999999,31.988000000000003 +2020-03-29 04:00:00,62.52,104.337,16.663,31.988000000000003 +2020-03-29 04:15:00,62.17,113.15899999999999,16.663,31.988000000000003 +2020-03-29 04:30:00,61.27,113.23,16.663,31.988000000000003 +2020-03-29 04:45:00,61.62,113.501,16.663,31.988000000000003 +2020-03-29 05:00:00,59.26,128.622,17.271,31.988000000000003 +2020-03-29 05:15:00,61.48,141.29399999999998,17.271,31.988000000000003 +2020-03-29 05:30:00,61.34,135.15,17.271,31.988000000000003 +2020-03-29 05:45:00,61.02,131.74200000000002,17.271,31.988000000000003 +2020-03-29 06:00:00,61.85,147.918,17.612000000000002,31.988000000000003 +2020-03-29 06:15:00,63.29,166.752,17.612000000000002,31.988000000000003 +2020-03-29 06:30:00,64.04,159.804,17.612000000000002,31.988000000000003 +2020-03-29 06:45:00,65.71,153.536,17.612000000000002,31.988000000000003 +2020-03-29 07:00:00,65.1,154.444,20.88,31.988000000000003 +2020-03-29 07:15:00,67.09,154.61700000000002,20.88,31.988000000000003 +2020-03-29 07:30:00,68.29,154.899,20.88,31.988000000000003 +2020-03-29 07:45:00,66.11,154.187,20.88,31.988000000000003 +2020-03-29 08:00:00,70.13,156.718,25.861,31.988000000000003 +2020-03-29 08:15:00,69.2,157.636,25.861,31.988000000000003 +2020-03-29 08:30:00,68.25,156.053,25.861,31.988000000000003 +2020-03-29 08:45:00,67.57,155.486,25.861,31.988000000000003 +2020-03-29 09:00:00,66.99,150.813,27.921999999999997,31.988000000000003 +2020-03-29 09:15:00,65.9,149.954,27.921999999999997,31.988000000000003 +2020-03-29 09:30:00,65.28,150.535,27.921999999999997,31.988000000000003 +2020-03-29 09:45:00,65.68,148.985,27.921999999999997,31.988000000000003 +2020-03-29 10:00:00,66.04,147.47799999999998,29.048000000000002,31.988000000000003 +2020-03-29 10:15:00,67.95,147.296,29.048000000000002,31.988000000000003 +2020-03-29 10:30:00,68.96,145.834,29.048000000000002,31.988000000000003 +2020-03-29 10:45:00,69.67,144.84799999999998,29.048000000000002,31.988000000000003 +2020-03-29 11:00:00,66.38,140.961,32.02,31.988000000000003 +2020-03-29 11:15:00,66.11,139.761,32.02,31.988000000000003 +2020-03-29 11:30:00,60.65,140.167,32.02,31.988000000000003 +2020-03-29 11:45:00,61.22,140.942,32.02,31.988000000000003 +2020-03-29 12:00:00,57.94,136.707,28.55,31.988000000000003 +2020-03-29 12:15:00,57.98,137.329,28.55,31.988000000000003 +2020-03-29 12:30:00,57.03,135.741,28.55,31.988000000000003 +2020-03-29 12:45:00,58.14,135.325,28.55,31.988000000000003 +2020-03-29 13:00:00,56.4,135.33,25.601999999999997,31.988000000000003 +2020-03-29 13:15:00,53.99,134.73,25.601999999999997,31.988000000000003 +2020-03-29 13:30:00,54.04,132.297,25.601999999999997,31.988000000000003 +2020-03-29 13:45:00,55.14,131.30200000000002,25.601999999999997,31.988000000000003 +2020-03-29 14:00:00,58.23,134.429,23.916999999999998,31.988000000000003 +2020-03-29 14:15:00,57.08,133.165,23.916999999999998,31.988000000000003 +2020-03-29 14:30:00,54.64,132.536,23.916999999999998,31.988000000000003 +2020-03-29 14:45:00,59.63,133.328,23.916999999999998,31.988000000000003 +2020-03-29 15:00:00,60.63,133.313,24.064,31.988000000000003 +2020-03-29 15:15:00,61.57,132.48,24.064,31.988000000000003 +2020-03-29 15:30:00,63.18,131.404,24.064,31.988000000000003 +2020-03-29 15:45:00,66.09,131.221,24.064,31.988000000000003 +2020-03-29 16:00:00,70.77,134.955,28.189,31.988000000000003 +2020-03-29 16:15:00,72.49,137.029,28.189,31.988000000000003 +2020-03-29 16:30:00,74.01,138.034,28.189,31.988000000000003 +2020-03-29 16:45:00,75.04,137.291,28.189,31.988000000000003 +2020-03-29 17:00:00,82.83,139.64700000000002,37.576,31.988000000000003 +2020-03-29 17:15:00,84.34,142.606,37.576,31.988000000000003 +2020-03-29 17:30:00,85.73,145.435,37.576,31.988000000000003 +2020-03-29 17:45:00,88.1,147.631,37.576,31.988000000000003 +2020-03-29 18:00:00,92.01,151.79,42.669,31.988000000000003 +2020-03-29 18:15:00,90.43,155.52200000000002,42.669,31.988000000000003 +2020-03-29 18:30:00,93.35,153.791,42.669,31.988000000000003 +2020-03-29 18:45:00,100.38,155.852,42.669,31.988000000000003 +2020-03-29 19:00:00,104.31,155.41299999999998,43.538999999999994,31.988000000000003 +2020-03-29 19:15:00,103.88,154.311,43.538999999999994,31.988000000000003 +2020-03-29 19:30:00,94.49,154.321,43.538999999999994,31.988000000000003 +2020-03-29 19:45:00,95.0,153.996,43.538999999999994,31.988000000000003 +2020-03-29 20:00:00,89.36,149.881,37.330999999999996,31.988000000000003 +2020-03-29 20:15:00,94.08,148.375,37.330999999999996,31.988000000000003 +2020-03-29 20:30:00,95.79,147.328,37.330999999999996,31.988000000000003 +2020-03-29 20:45:00,98.77,144.69799999999998,37.330999999999996,31.988000000000003 +2020-03-29 21:00:00,92.39,139.58100000000002,33.856,31.988000000000003 +2020-03-29 21:15:00,90.85,137.675,33.856,31.988000000000003 +2020-03-29 21:30:00,88.72,137.703,33.856,31.988000000000003 +2020-03-29 21:45:00,86.64,137.485,33.856,31.988000000000003 +2020-03-29 22:00:00,86.5,131.963,34.711999999999996,31.988000000000003 +2020-03-29 22:15:00,89.79,129.631,34.711999999999996,31.988000000000003 +2020-03-29 22:30:00,88.27,123.661,34.711999999999996,31.988000000000003 +2020-03-29 22:45:00,85.25,120.038,34.711999999999996,31.988000000000003 +2020-03-29 23:00:00,76.97,112.32799999999999,29.698,31.988000000000003 +2020-03-29 23:15:00,78.47,109.78299999999999,29.698,31.988000000000003 +2020-03-29 23:30:00,76.39,108.507,29.698,31.988000000000003 +2020-03-29 23:45:00,76.76,107.598,29.698,31.988000000000003 +2020-03-30 00:00:00,74.91,93.007,29.983,32.166 +2020-03-30 00:15:00,74.62,91.073,29.983,32.166 +2020-03-30 00:30:00,74.93,90.152,29.983,32.166 +2020-03-30 00:45:00,74.48,89.861,29.983,32.166 +2020-03-30 01:00:00,72.07,91.03,29.122,32.166 +2020-03-30 01:15:00,73.81,91.124,29.122,32.166 +2020-03-30 01:30:00,74.52,90.40700000000001,29.122,32.166 +2020-03-30 01:45:00,73.97,90.3,29.122,32.166 +2020-03-30 02:00:00,73.49,92.089,28.676,32.166 +2020-03-30 02:15:00,75.66,91.99600000000001,28.676,32.166 +2020-03-30 02:30:00,77.39,94.175,28.676,32.166 +2020-03-30 02:45:00,76.44,95.27600000000001,28.676,32.166 +2020-03-30 03:00:00,75.13,99.355,26.552,32.166 +2020-03-30 03:15:00,76.32,101.29,26.552,32.166 +2020-03-30 03:30:00,77.54,101.928,26.552,32.166 +2020-03-30 03:45:00,79.94,103.101,26.552,32.166 +2020-03-30 04:00:00,82.14,116.584,27.44,32.166 +2020-03-30 04:15:00,84.58,129.511,27.44,32.166 +2020-03-30 04:30:00,89.34,131.10299999999998,27.44,32.166 +2020-03-30 04:45:00,93.84,131.608,27.44,32.166 +2020-03-30 05:00:00,101.97,160.982,36.825,32.166 +2020-03-30 05:15:00,106.72,192.364,36.825,32.166 +2020-03-30 05:30:00,107.82,185.893,36.825,32.166 +2020-03-30 05:45:00,108.72,177.19099999999997,36.825,32.166 +2020-03-30 06:00:00,116.23,176.537,56.589,32.166 +2020-03-30 06:15:00,116.59,181.03900000000002,56.589,32.166 +2020-03-30 06:30:00,119.93,181.482,56.589,32.166 +2020-03-30 06:45:00,121.38,183.65099999999998,56.589,32.166 +2020-03-30 07:00:00,124.55,186.778,67.49,32.166 +2020-03-30 07:15:00,124.25,188.61,67.49,32.166 +2020-03-30 07:30:00,123.4,187.977,67.49,32.166 +2020-03-30 07:45:00,123.32,185.34099999999998,67.49,32.166 +2020-03-30 08:00:00,122.13,183.62599999999998,60.028,32.166 +2020-03-30 08:15:00,121.81,182.493,60.028,32.166 +2020-03-30 08:30:00,123.07,177.347,60.028,32.166 +2020-03-30 08:45:00,122.24,174.237,60.028,32.166 +2020-03-30 09:00:00,119.81,168.592,55.018,32.166 +2020-03-30 09:15:00,120.83,164.426,55.018,32.166 +2020-03-30 09:30:00,120.02,163.955,55.018,32.166 +2020-03-30 09:45:00,120.56,161.996,55.018,32.166 +2020-03-30 10:00:00,117.61,159.881,51.183,32.166 +2020-03-30 10:15:00,118.77,159.44,51.183,32.166 +2020-03-30 10:30:00,118.87,157.166,51.183,32.166 +2020-03-30 10:45:00,120.03,156.362,51.183,32.166 +2020-03-30 11:00:00,116.72,150.494,50.065,32.166 +2020-03-30 11:15:00,118.45,150.871,50.065,32.166 +2020-03-30 11:30:00,116.15,152.602,50.065,32.166 +2020-03-30 11:45:00,116.99,153.19799999999998,50.065,32.166 +2020-03-30 12:00:00,115.58,150.036,48.141999999999996,32.166 +2020-03-30 12:15:00,118.73,150.696,48.141999999999996,32.166 +2020-03-30 12:30:00,119.08,148.989,48.141999999999996,32.166 +2020-03-30 12:45:00,116.73,149.827,48.141999999999996,32.166 +2020-03-30 13:00:00,116.77,150.662,47.887,32.166 +2020-03-30 13:15:00,116.21,148.665,47.887,32.166 +2020-03-30 13:30:00,116.04,145.832,47.887,32.166 +2020-03-30 13:45:00,119.23,145.102,47.887,32.166 +2020-03-30 14:00:00,119.84,147.571,48.571000000000005,32.166 +2020-03-30 14:15:00,118.19,145.906,48.571000000000005,32.166 +2020-03-30 14:30:00,115.09,144.745,48.571000000000005,32.166 +2020-03-30 14:45:00,114.72,146.056,48.571000000000005,32.166 +2020-03-30 15:00:00,113.97,147.463,49.937,32.166 +2020-03-30 15:15:00,115.44,145.256,49.937,32.166 +2020-03-30 15:30:00,117.05,143.638,49.937,32.166 +2020-03-30 15:45:00,117.4,142.941,49.937,32.166 +2020-03-30 16:00:00,119.4,147.00799999999998,52.963,32.166 +2020-03-30 16:15:00,117.12,148.44799999999998,52.963,32.166 +2020-03-30 16:30:00,120.03,148.47899999999998,52.963,32.166 +2020-03-30 16:45:00,120.1,146.713,52.963,32.166 +2020-03-30 17:00:00,122.72,148.575,61.163999999999994,32.166 +2020-03-30 17:15:00,121.92,150.855,61.163999999999994,32.166 +2020-03-30 17:30:00,123.43,153.141,61.163999999999994,32.166 +2020-03-30 17:45:00,123.83,154.01,61.163999999999994,32.166 +2020-03-30 18:00:00,125.34,158.202,63.788999999999994,32.166 +2020-03-30 18:15:00,122.68,159.688,63.788999999999994,32.166 +2020-03-30 18:30:00,124.68,158.264,63.788999999999994,32.166 +2020-03-30 18:45:00,125.28,161.806,63.788999999999994,32.166 +2020-03-30 19:00:00,122.28,159.947,63.913000000000004,32.166 +2020-03-30 19:15:00,118.3,158.28799999999998,63.913000000000004,32.166 +2020-03-30 19:30:00,114.81,158.621,63.913000000000004,32.166 +2020-03-30 19:45:00,112.8,157.49200000000002,63.913000000000004,32.166 +2020-03-30 20:00:00,106.59,151.07,65.44,32.166 +2020-03-30 20:15:00,106.52,147.994,65.44,32.166 +2020-03-30 20:30:00,106.51,145.638,65.44,32.166 +2020-03-30 20:45:00,105.14,144.384,65.44,32.166 +2020-03-30 21:00:00,100.49,139.514,59.117,32.166 +2020-03-30 21:15:00,99.35,136.80100000000002,59.117,32.166 +2020-03-30 21:30:00,95.33,136.287,59.117,32.166 +2020-03-30 21:45:00,94.73,135.624,59.117,32.166 +2020-03-30 22:00:00,91.38,127.18299999999999,52.301,32.166 +2020-03-30 22:15:00,91.17,124.395,52.301,32.166 +2020-03-30 22:30:00,89.5,109.465,52.301,32.166 +2020-03-30 22:45:00,88.98,101.96,52.301,32.166 +2020-03-30 23:00:00,66.74,94.854,44.373000000000005,32.166 +2020-03-30 23:15:00,64.17,93.986,44.373000000000005,32.166 +2020-03-30 23:30:00,64.34,94.916,44.373000000000005,32.166 +2020-03-30 23:45:00,63.43,95.994,44.373000000000005,32.166 +2020-03-31 00:00:00,62.68,91.345,44.647,32.166 +2020-03-31 00:15:00,60.68,90.807,44.647,32.166 +2020-03-31 00:30:00,59.93,89.366,44.647,32.166 +2020-03-31 00:45:00,61.17,88.571,44.647,32.166 +2020-03-31 01:00:00,59.63,89.383,41.433,32.166 +2020-03-31 01:15:00,60.51,89.15,41.433,32.166 +2020-03-31 01:30:00,59.6,88.506,41.433,32.166 +2020-03-31 01:45:00,65.34,88.48,41.433,32.166 +2020-03-31 02:00:00,67.35,90.09700000000001,39.909,32.166 +2020-03-31 02:15:00,70.17,90.27799999999999,39.909,32.166 +2020-03-31 02:30:00,68.14,91.891,39.909,32.166 +2020-03-31 02:45:00,65.63,93.11,39.909,32.166 +2020-03-31 03:00:00,72.05,96.104,39.14,32.166 +2020-03-31 03:15:00,71.12,97.663,39.14,32.166 +2020-03-31 03:30:00,68.78,98.656,39.14,32.166 +2020-03-31 03:45:00,71.02,99.632,39.14,32.166 +2020-03-31 04:00:00,72.64,112.556,40.015,32.166 +2020-03-31 04:15:00,77.31,125.20100000000001,40.015,32.166 +2020-03-31 04:30:00,78.77,126.529,40.015,32.166 +2020-03-31 04:45:00,82.3,128.109,40.015,32.166 +2020-03-31 05:00:00,94.54,161.782,44.93600000000001,32.166 +2020-03-31 05:15:00,98.42,193.196,44.93600000000001,32.166 +2020-03-31 05:30:00,103.0,185.597,44.93600000000001,32.166 +2020-03-31 05:45:00,102.84,176.67700000000002,44.93600000000001,32.166 +2020-03-31 06:00:00,112.12,175.46099999999998,57.271,32.166 +2020-03-31 06:15:00,114.11,181.27599999999998,57.271,32.166 +2020-03-31 06:30:00,120.03,181.074,57.271,32.166 +2020-03-31 06:45:00,120.49,182.643,57.271,32.166 +2020-03-31 07:00:00,123.04,185.68599999999998,68.352,32.166 +2020-03-31 07:15:00,121.38,187.287,68.352,32.166 +2020-03-31 07:30:00,118.56,186.19099999999997,68.352,32.166 +2020-03-31 07:45:00,119.78,183.394,68.352,32.166 +2020-03-31 08:00:00,118.34,181.725,60.717,32.166 +2020-03-31 08:15:00,116.42,179.653,60.717,32.166 +2020-03-31 08:30:00,119.31,174.368,60.717,32.166 +2020-03-31 08:45:00,115.45,170.799,60.717,32.166 +2020-03-31 09:00:00,110.19,164.595,54.603,32.166 +2020-03-31 09:15:00,107.11,161.68,54.603,32.166 +2020-03-31 09:30:00,108.81,161.972,54.603,32.166 +2020-03-31 09:45:00,111.86,160.24,54.603,32.166 +2020-03-31 10:00:00,107.21,157.274,52.308,32.166 +2020-03-31 10:15:00,106.49,155.975,52.308,32.166 +2020-03-31 10:30:00,102.82,153.881,52.308,32.166 +2020-03-31 10:45:00,103.11,153.60299999999998,52.308,32.166 +2020-03-31 11:00:00,103.17,148.94899999999998,51.838,32.166 +2020-03-31 11:15:00,105.15,149.18200000000002,51.838,32.166 +2020-03-31 11:30:00,107.81,149.653,51.838,32.166 +2020-03-31 11:45:00,107.37,150.726,51.838,32.166 +2020-03-31 12:00:00,103.49,146.401,50.375,32.166 +2020-03-31 12:15:00,105.66,146.811,50.375,32.166 +2020-03-31 12:30:00,104.92,145.886,50.375,32.166 +2020-03-31 12:45:00,104.34,146.658,50.375,32.166 +2020-03-31 13:00:00,95.18,147.097,50.735,32.166 +2020-03-31 13:15:00,98.54,145.253,50.735,32.166 +2020-03-31 13:30:00,96.68,143.38299999999998,50.735,32.166 +2020-03-31 13:45:00,93.25,142.56799999999998,50.735,32.166 +2020-03-31 14:00:00,100.54,145.45600000000002,50.946000000000005,32.166 +2020-03-31 14:15:00,105.54,143.846,50.946000000000005,32.166 +2020-03-31 14:30:00,101.06,143.255,50.946000000000005,32.166 +2020-03-31 14:45:00,100.21,144.303,50.946000000000005,32.166 +2020-03-31 15:00:00,103.43,145.321,53.18,32.166 +2020-03-31 15:15:00,106.7,143.6,53.18,32.166 +2020-03-31 15:30:00,103.14,142.063,53.18,32.166 +2020-03-31 15:45:00,104.16,141.099,53.18,32.166 +2020-03-31 16:00:00,108.84,145.308,54.928999999999995,32.166 +2020-03-31 16:15:00,109.82,147.137,54.928999999999995,32.166 +2020-03-31 16:30:00,114.81,147.60299999999998,54.928999999999995,32.166 +2020-03-31 16:45:00,110.58,146.209,54.928999999999995,32.166 +2020-03-31 17:00:00,116.98,148.611,60.913000000000004,32.166 +2020-03-31 17:15:00,117.4,151.02,60.913000000000004,32.166 +2020-03-31 17:30:00,117.54,153.74,60.913000000000004,32.166 +2020-03-31 17:45:00,112.39,154.411,60.913000000000004,32.166 +2020-03-31 18:00:00,119.14,158.256,62.214,32.166 +2020-03-31 18:15:00,118.51,159.803,62.214,32.166 +2020-03-31 18:30:00,117.89,158.049,62.214,32.166 +2020-03-31 18:45:00,116.94,162.19799999999998,62.214,32.166 +2020-03-31 19:00:00,116.89,160.05200000000002,62.38,32.166 +2020-03-31 19:15:00,117.46,158.23,62.38,32.166 +2020-03-31 19:30:00,116.74,157.986,62.38,32.166 +2020-03-31 19:45:00,114.19,157.02200000000002,62.38,32.166 +2020-03-31 20:00:00,107.41,150.808,65.018,32.166 +2020-03-31 20:15:00,106.35,146.821,65.018,32.166 +2020-03-31 20:30:00,107.79,145.326,65.018,32.166 +2020-03-31 20:45:00,101.84,143.74,65.018,32.166 +2020-03-31 21:00:00,91.56,138.55100000000002,56.416000000000004,32.166 +2020-03-31 21:15:00,99.16,136.15,56.416000000000004,32.166 +2020-03-31 21:30:00,94.28,135.096,56.416000000000004,32.166 +2020-03-31 21:45:00,95.13,134.695,56.416000000000004,32.166 +2020-03-31 22:00:00,83.39,127.675,52.846000000000004,32.166 +2020-03-31 22:15:00,86.65,124.60799999999999,52.846000000000004,32.166 +2020-03-31 22:30:00,87.84,109.78200000000001,52.846000000000004,32.166 +2020-03-31 22:45:00,84.34,102.49700000000001,52.846000000000004,32.166 +2020-03-31 23:00:00,75.86,95.185,44.435,32.166 +2020-03-31 23:15:00,71.86,94.06,44.435,32.166 +2020-03-31 23:30:00,74.23,94.70100000000001,44.435,32.166 +2020-03-31 23:45:00,72.45,95.499,44.435,32.166 +2020-04-01 00:00:00,69.69,79.456,39.061,30.736 +2020-04-01 00:15:00,75.94,79.935,39.061,30.736 +2020-04-01 00:30:00,78.31,78.125,39.061,30.736 +2020-04-01 00:45:00,79.1,76.366,39.061,30.736 +2020-04-01 01:00:00,70.26,77.627,35.795,30.736 +2020-04-01 01:15:00,71.06,76.926,35.795,30.736 +2020-04-01 01:30:00,68.96,75.87100000000001,35.795,30.736 +2020-04-01 01:45:00,67.64,75.392,35.795,30.736 +2020-04-01 02:00:00,67.11,77.184,33.316,30.736 +2020-04-01 02:15:00,71.63,76.73899999999999,33.316,30.736 +2020-04-01 02:30:00,68.87,79.04899999999999,33.316,30.736 +2020-04-01 02:45:00,69.72,79.654,33.316,30.736 +2020-04-01 03:00:00,70.41,83.104,32.803000000000004,30.736 +2020-04-01 03:15:00,70.75,85.094,32.803000000000004,30.736 +2020-04-01 03:30:00,72.02,84.946,32.803000000000004,30.736 +2020-04-01 03:45:00,74.47,85.49700000000001,32.803000000000004,30.736 +2020-04-01 04:00:00,78.97,98.23200000000001,34.235,30.736 +2020-04-01 04:15:00,81.76,111.244,34.235,30.736 +2020-04-01 04:30:00,84.98,111.566,34.235,30.736 +2020-04-01 04:45:00,89.2,113.62700000000001,34.235,30.736 +2020-04-01 05:00:00,96.35,149.659,38.65,30.736 +2020-04-01 05:15:00,99.7,183.483,38.65,30.736 +2020-04-01 05:30:00,102.53,173.278,38.65,30.736 +2020-04-01 05:45:00,104.9,162.628,38.65,30.736 +2020-04-01 06:00:00,111.67,163.61700000000002,54.951,30.736 +2020-04-01 06:15:00,109.26,169.40099999999998,54.951,30.736 +2020-04-01 06:30:00,111.94,167.69799999999998,54.951,30.736 +2020-04-01 06:45:00,115.16,168.525,54.951,30.736 +2020-04-01 07:00:00,117.0,171.296,67.328,30.736 +2020-04-01 07:15:00,115.4,172.46400000000003,67.328,30.736 +2020-04-01 07:30:00,113.66,171.299,67.328,30.736 +2020-04-01 07:45:00,114.86,168.247,67.328,30.736 +2020-04-01 08:00:00,114.85,167.385,60.23,30.736 +2020-04-01 08:15:00,112.81,165.22400000000002,60.23,30.736 +2020-04-01 08:30:00,115.93,160.542,60.23,30.736 +2020-04-01 08:45:00,116.76,157.317,60.23,30.736 +2020-04-01 09:00:00,116.8,151.364,56.845,30.736 +2020-04-01 09:15:00,118.8,148.974,56.845,30.736 +2020-04-01 09:30:00,116.67,150.576,56.845,30.736 +2020-04-01 09:45:00,112.16,149.53,56.845,30.736 +2020-04-01 10:00:00,107.33,145.305,53.832,30.736 +2020-04-01 10:15:00,106.59,144.53,53.832,30.736 +2020-04-01 10:30:00,110.06,142.187,53.832,30.736 +2020-04-01 10:45:00,112.49,141.908,53.832,30.736 +2020-04-01 11:00:00,111.37,136.292,53.225,30.736 +2020-04-01 11:15:00,112.05,136.708,53.225,30.736 +2020-04-01 11:30:00,108.51,137.773,53.225,30.736 +2020-04-01 11:45:00,105.82,139.032,53.225,30.736 +2020-04-01 12:00:00,99.92,134.388,50.676,30.736 +2020-04-01 12:15:00,105.63,134.911,50.676,30.736 +2020-04-01 12:30:00,106.93,134.461,50.676,30.736 +2020-04-01 12:45:00,103.6,135.236,50.676,30.736 +2020-04-01 13:00:00,103.07,135.707,50.646,30.736 +2020-04-01 13:15:00,102.9,134.39600000000002,50.646,30.736 +2020-04-01 13:30:00,102.03,132.364,50.646,30.736 +2020-04-01 13:45:00,103.27,131.08700000000002,50.646,30.736 +2020-04-01 14:00:00,105.31,132.503,50.786,30.736 +2020-04-01 14:15:00,102.66,131.495,50.786,30.736 +2020-04-01 14:30:00,103.31,131.606,50.786,30.736 +2020-04-01 14:45:00,96.63,132.4,50.786,30.736 +2020-04-01 15:00:00,93.87,132.792,51.535,30.736 +2020-04-01 15:15:00,93.7,131.388,51.535,30.736 +2020-04-01 15:30:00,99.59,130.69299999999998,51.535,30.736 +2020-04-01 15:45:00,101.6,130.118,51.535,30.736 +2020-04-01 16:00:00,98.99,131.167,53.157,30.736 +2020-04-01 16:15:00,101.34,132.373,53.157,30.736 +2020-04-01 16:30:00,101.19,132.377,53.157,30.736 +2020-04-01 16:45:00,103.38,131.024,53.157,30.736 +2020-04-01 17:00:00,106.1,130.7,57.793,30.736 +2020-04-01 17:15:00,106.86,133.321,57.793,30.736 +2020-04-01 17:30:00,107.36,135.382,57.793,30.736 +2020-04-01 17:45:00,109.06,136.537,57.793,30.736 +2020-04-01 18:00:00,110.87,139.363,59.872,30.736 +2020-04-01 18:15:00,109.26,140.57,59.872,30.736 +2020-04-01 18:30:00,110.29,139.069,59.872,30.736 +2020-04-01 18:45:00,111.93,144.532,59.872,30.736 +2020-04-01 19:00:00,113.65,143.02200000000002,60.17100000000001,30.736 +2020-04-01 19:15:00,109.64,141.8,60.17100000000001,30.736 +2020-04-01 19:30:00,110.52,141.476,60.17100000000001,30.736 +2020-04-01 19:45:00,105.99,141.282,60.17100000000001,30.736 +2020-04-01 20:00:00,102.05,136.425,65.015,30.736 +2020-04-01 20:15:00,107.29,133.08100000000002,65.015,30.736 +2020-04-01 20:30:00,106.45,132.442,65.015,30.736 +2020-04-01 20:45:00,102.76,131.428,65.015,30.736 +2020-04-01 21:00:00,92.13,125.118,57.805,30.736 +2020-04-01 21:15:00,93.99,123.633,57.805,30.736 +2020-04-01 21:30:00,88.08,123.904,57.805,30.736 +2020-04-01 21:45:00,86.39,122.70100000000001,57.805,30.736 +2020-04-01 22:00:00,81.08,116.471,52.115,30.736 +2020-04-01 22:15:00,86.83,113.48200000000001,52.115,30.736 +2020-04-01 22:30:00,86.9,100.95,52.115,30.736 +2020-04-01 22:45:00,86.76,93.775,52.115,30.736 +2020-04-01 23:00:00,76.32,84.93299999999999,42.871,30.736 +2020-04-01 23:15:00,73.01,84.014,42.871,30.736 +2020-04-01 23:30:00,73.23,83.148,42.871,30.736 +2020-04-01 23:45:00,75.85,83.984,42.871,30.736 +2020-04-02 00:00:00,71.82,79.054,39.203,30.736 +2020-04-02 00:15:00,78.53,79.546,39.203,30.736 +2020-04-02 00:30:00,80.03,77.726,39.203,30.736 +2020-04-02 00:45:00,79.26,75.968,39.203,30.736 +2020-04-02 01:00:00,71.4,77.212,37.118,30.736 +2020-04-02 01:15:00,75.84,76.488,37.118,30.736 +2020-04-02 01:30:00,76.57,75.41199999999999,37.118,30.736 +2020-04-02 01:45:00,78.33,74.938,37.118,30.736 +2020-04-02 02:00:00,73.97,76.719,35.647,30.736 +2020-04-02 02:15:00,75.9,76.257,35.647,30.736 +2020-04-02 02:30:00,78.09,78.58800000000001,35.647,30.736 +2020-04-02 02:45:00,78.5,79.19800000000001,35.647,30.736 +2020-04-02 03:00:00,76.84,82.665,34.585,30.736 +2020-04-02 03:15:00,79.44,84.63,34.585,30.736 +2020-04-02 03:30:00,81.12,84.477,34.585,30.736 +2020-04-02 03:45:00,81.18,85.04700000000001,34.585,30.736 +2020-04-02 04:00:00,79.89,97.76100000000001,36.184,30.736 +2020-04-02 04:15:00,81.67,110.743,36.184,30.736 +2020-04-02 04:30:00,83.77,111.07,36.184,30.736 +2020-04-02 04:45:00,87.27,113.12,36.184,30.736 +2020-04-02 05:00:00,96.26,149.07299999999998,41.019,30.736 +2020-04-02 05:15:00,98.79,182.831,41.019,30.736 +2020-04-02 05:30:00,101.22,172.627,41.019,30.736 +2020-04-02 05:45:00,102.52,162.015,41.019,30.736 +2020-04-02 06:00:00,111.5,163.025,53.963,30.736 +2020-04-02 06:15:00,109.85,168.795,53.963,30.736 +2020-04-02 06:30:00,113.43,167.063,53.963,30.736 +2020-04-02 06:45:00,114.03,167.875,53.963,30.736 +2020-04-02 07:00:00,119.23,170.65400000000002,66.512,30.736 +2020-04-02 07:15:00,120.65,171.798,66.512,30.736 +2020-04-02 07:30:00,116.77,170.592,66.512,30.736 +2020-04-02 07:45:00,112.67,167.525,66.512,30.736 +2020-04-02 08:00:00,115.24,166.643,58.86,30.736 +2020-04-02 08:15:00,117.46,164.503,58.86,30.736 +2020-04-02 08:30:00,115.91,159.782,58.86,30.736 +2020-04-02 08:45:00,115.21,156.585,58.86,30.736 +2020-04-02 09:00:00,115.87,150.637,52.156000000000006,30.736 +2020-04-02 09:15:00,114.66,148.252,52.156000000000006,30.736 +2020-04-02 09:30:00,119.53,149.874,52.156000000000006,30.736 +2020-04-02 09:45:00,123.85,148.858,52.156000000000006,30.736 +2020-04-02 10:00:00,118.77,144.639,49.034,30.736 +2020-04-02 10:15:00,126.97,143.914,49.034,30.736 +2020-04-02 10:30:00,125.46,141.596,49.034,30.736 +2020-04-02 10:45:00,123.88,141.338,49.034,30.736 +2020-04-02 11:00:00,122.49,135.713,46.53,30.736 +2020-04-02 11:15:00,116.42,136.153,46.53,30.736 +2020-04-02 11:30:00,110.72,137.222,46.53,30.736 +2020-04-02 11:45:00,120.11,138.501,46.53,30.736 +2020-04-02 12:00:00,118.87,133.88299999999998,43.318000000000005,30.736 +2020-04-02 12:15:00,120.48,134.416,43.318000000000005,30.736 +2020-04-02 12:30:00,117.06,133.922,43.318000000000005,30.736 +2020-04-02 12:45:00,111.35,134.69899999999998,43.318000000000005,30.736 +2020-04-02 13:00:00,111.13,135.213,41.608000000000004,30.736 +2020-04-02 13:15:00,108.37,133.891,41.608000000000004,30.736 +2020-04-02 13:30:00,110.04,131.856,41.608000000000004,30.736 +2020-04-02 13:45:00,110.04,130.58100000000002,41.608000000000004,30.736 +2020-04-02 14:00:00,105.79,132.067,41.786,30.736 +2020-04-02 14:15:00,97.68,131.036,41.786,30.736 +2020-04-02 14:30:00,108.28,131.105,41.786,30.736 +2020-04-02 14:45:00,118.85,131.90200000000002,41.786,30.736 +2020-04-02 15:00:00,124.11,132.321,44.181999999999995,30.736 +2020-04-02 15:15:00,123.02,130.893,44.181999999999995,30.736 +2020-04-02 15:30:00,118.52,130.14700000000002,44.181999999999995,30.736 +2020-04-02 15:45:00,111.31,129.553,44.181999999999995,30.736 +2020-04-02 16:00:00,113.66,130.637,45.956,30.736 +2020-04-02 16:15:00,119.09,131.816,45.956,30.736 +2020-04-02 16:30:00,118.53,131.822,45.956,30.736 +2020-04-02 16:45:00,121.92,130.407,45.956,30.736 +2020-04-02 17:00:00,124.5,130.131,50.702,30.736 +2020-04-02 17:15:00,120.31,132.734,50.702,30.736 +2020-04-02 17:30:00,120.7,134.798,50.702,30.736 +2020-04-02 17:45:00,121.11,135.939,50.702,30.736 +2020-04-02 18:00:00,123.23,138.77700000000002,53.595,30.736 +2020-04-02 18:15:00,120.26,140.024,53.595,30.736 +2020-04-02 18:30:00,115.9,138.511,53.595,30.736 +2020-04-02 18:45:00,114.56,143.986,53.595,30.736 +2020-04-02 19:00:00,121.11,142.455,54.207,30.736 +2020-04-02 19:15:00,119.24,141.243,54.207,30.736 +2020-04-02 19:30:00,117.39,140.938,54.207,30.736 +2020-04-02 19:45:00,107.85,140.776,54.207,30.736 +2020-04-02 20:00:00,108.15,135.892,56.948,30.736 +2020-04-02 20:15:00,111.42,132.55700000000002,56.948,30.736 +2020-04-02 20:30:00,106.44,131.955,56.948,30.736 +2020-04-02 20:45:00,101.23,130.964,56.948,30.736 +2020-04-02 21:00:00,100.35,124.652,52.157,30.736 +2020-04-02 21:15:00,101.03,123.176,52.157,30.736 +2020-04-02 21:30:00,107.53,123.44,52.157,30.736 +2020-04-02 21:45:00,103.46,122.266,52.157,30.736 +2020-04-02 22:00:00,96.94,116.041,47.483000000000004,30.736 +2020-04-02 22:15:00,99.13,113.08,47.483000000000004,30.736 +2020-04-02 22:30:00,96.8,100.515,47.483000000000004,30.736 +2020-04-02 22:45:00,92.58,93.336,47.483000000000004,30.736 +2020-04-02 23:00:00,83.94,84.47,41.978,30.736 +2020-04-02 23:15:00,83.81,83.583,41.978,30.736 +2020-04-02 23:30:00,88.75,82.71600000000001,41.978,30.736 +2020-04-02 23:45:00,87.76,83.568,41.978,30.736 +2020-04-03 00:00:00,76.87,76.975,39.301,30.736 +2020-04-03 00:15:00,73.29,77.727,39.301,30.736 +2020-04-03 00:30:00,70.69,75.961,39.301,30.736 +2020-04-03 00:45:00,72.11,74.514,39.301,30.736 +2020-04-03 01:00:00,75.99,75.328,37.976,30.736 +2020-04-03 01:15:00,77.44,74.82,37.976,30.736 +2020-04-03 01:30:00,73.59,73.983,37.976,30.736 +2020-04-03 01:45:00,79.01,73.439,37.976,30.736 +2020-04-03 02:00:00,73.89,75.78399999999999,37.041,30.736 +2020-04-03 02:15:00,73.49,75.205,37.041,30.736 +2020-04-03 02:30:00,79.15,78.352,37.041,30.736 +2020-04-03 02:45:00,75.85,78.607,37.041,30.736 +2020-04-03 03:00:00,74.11,81.914,37.575,30.736 +2020-04-03 03:15:00,79.18,83.74799999999999,37.575,30.736 +2020-04-03 03:30:00,76.15,83.445,37.575,30.736 +2020-04-03 03:45:00,85.07,84.735,37.575,30.736 +2020-04-03 04:00:00,91.35,97.646,39.058,30.736 +2020-04-03 04:15:00,92.78,109.551,39.058,30.736 +2020-04-03 04:30:00,92.59,110.568,39.058,30.736 +2020-04-03 04:45:00,93.75,111.53399999999999,39.058,30.736 +2020-04-03 05:00:00,102.17,146.35299999999998,43.256,30.736 +2020-04-03 05:15:00,102.67,181.516,43.256,30.736 +2020-04-03 05:30:00,104.0,172.08599999999998,43.256,30.736 +2020-04-03 05:45:00,106.32,161.215,43.256,30.736 +2020-04-03 06:00:00,114.66,162.671,56.093999999999994,30.736 +2020-04-03 06:15:00,113.85,167.59900000000002,56.093999999999994,30.736 +2020-04-03 06:30:00,117.45,165.268,56.093999999999994,30.736 +2020-04-03 06:45:00,116.33,167.011,56.093999999999994,30.736 +2020-04-03 07:00:00,117.74,169.637,66.92699999999999,30.736 +2020-04-03 07:15:00,116.74,171.90900000000002,66.92699999999999,30.736 +2020-04-03 07:30:00,116.53,169.487,66.92699999999999,30.736 +2020-04-03 07:45:00,115.69,165.718,66.92699999999999,30.736 +2020-04-03 08:00:00,114.23,164.50900000000001,60.332,30.736 +2020-04-03 08:15:00,113.28,162.47799999999998,60.332,30.736 +2020-04-03 08:30:00,113.5,158.321,60.332,30.736 +2020-04-03 08:45:00,111.4,154.023,60.332,30.736 +2020-04-03 09:00:00,109.66,147.19899999999998,56.085,30.736 +2020-04-03 09:15:00,109.0,146.171,56.085,30.736 +2020-04-03 09:30:00,107.02,147.174,56.085,30.736 +2020-04-03 09:45:00,107.16,146.306,56.085,30.736 +2020-04-03 10:00:00,105.85,141.186,52.91,30.736 +2020-04-03 10:15:00,107.46,140.898,52.91,30.736 +2020-04-03 10:30:00,105.47,138.81799999999998,52.91,30.736 +2020-04-03 10:45:00,107.79,138.22299999999998,52.91,30.736 +2020-04-03 11:00:00,102.99,132.667,52.278999999999996,30.736 +2020-04-03 11:15:00,101.82,131.98,52.278999999999996,30.736 +2020-04-03 11:30:00,101.04,134.144,52.278999999999996,30.736 +2020-04-03 11:45:00,100.88,135.03,52.278999999999996,30.736 +2020-04-03 12:00:00,96.79,131.445,49.023999999999994,30.736 +2020-04-03 12:15:00,100.16,130.16,49.023999999999994,30.736 +2020-04-03 12:30:00,94.47,129.792,49.023999999999994,30.736 +2020-04-03 12:45:00,100.82,130.594,49.023999999999994,30.736 +2020-04-03 13:00:00,98.58,132.121,46.82,30.736 +2020-04-03 13:15:00,96.92,131.47299999999998,46.82,30.736 +2020-04-03 13:30:00,95.45,129.8,46.82,30.736 +2020-04-03 13:45:00,97.9,128.616,46.82,30.736 +2020-04-03 14:00:00,100.06,128.971,45.756,30.736 +2020-04-03 14:15:00,94.08,127.977,45.756,30.736 +2020-04-03 14:30:00,89.77,129.05200000000002,45.756,30.736 +2020-04-03 14:45:00,89.15,129.773,45.756,30.736 +2020-04-03 15:00:00,99.64,129.828,47.56,30.736 +2020-04-03 15:15:00,97.56,127.941,47.56,30.736 +2020-04-03 15:30:00,100.39,125.76799999999999,47.56,30.736 +2020-04-03 15:45:00,96.0,125.601,47.56,30.736 +2020-04-03 16:00:00,102.98,125.535,49.581,30.736 +2020-04-03 16:15:00,107.36,127.12799999999999,49.581,30.736 +2020-04-03 16:30:00,107.41,127.117,49.581,30.736 +2020-04-03 16:45:00,105.69,125.18799999999999,49.581,30.736 +2020-04-03 17:00:00,111.53,125.978,53.918,30.736 +2020-04-03 17:15:00,114.78,128.192,53.918,30.736 +2020-04-03 17:30:00,110.72,130.119,53.918,30.736 +2020-04-03 17:45:00,114.51,130.999,53.918,30.736 +2020-04-03 18:00:00,121.12,134.387,54.266000000000005,30.736 +2020-04-03 18:15:00,115.78,134.931,54.266000000000005,30.736 +2020-04-03 18:30:00,115.51,133.621,54.266000000000005,30.736 +2020-04-03 18:45:00,117.31,139.316,54.266000000000005,30.736 +2020-04-03 19:00:00,117.9,138.845,54.092,30.736 +2020-04-03 19:15:00,113.83,138.86700000000002,54.092,30.736 +2020-04-03 19:30:00,113.99,138.334,54.092,30.736 +2020-04-03 19:45:00,112.93,137.374,54.092,30.736 +2020-04-03 20:00:00,109.53,132.425,59.038999999999994,30.736 +2020-04-03 20:15:00,98.4,129.518,59.038999999999994,30.736 +2020-04-03 20:30:00,94.66,128.66899999999998,59.038999999999994,30.736 +2020-04-03 20:45:00,99.46,127.654,59.038999999999994,30.736 +2020-04-03 21:00:00,89.12,122.366,53.346000000000004,30.736 +2020-04-03 21:15:00,94.16,122.066,53.346000000000004,30.736 +2020-04-03 21:30:00,91.73,122.272,53.346000000000004,30.736 +2020-04-03 21:45:00,92.13,121.59700000000001,53.346000000000004,30.736 +2020-04-03 22:00:00,84.0,115.95,47.938,30.736 +2020-04-03 22:15:00,85.47,112.79700000000001,47.938,30.736 +2020-04-03 22:30:00,85.97,106.915,47.938,30.736 +2020-04-03 22:45:00,86.43,102.51899999999999,47.938,30.736 +2020-04-03 23:00:00,79.39,94.219,40.266,30.736 +2020-04-03 23:15:00,73.78,91.25299999999999,40.266,30.736 +2020-04-03 23:30:00,75.79,88.464,40.266,30.736 +2020-04-03 23:45:00,76.89,88.825,40.266,30.736 +2020-04-04 00:00:00,74.66,75.469,39.184,30.618000000000002 +2020-04-04 00:15:00,73.9,73.183,39.184,30.618000000000002 +2020-04-04 00:30:00,65.93,71.98100000000001,39.184,30.618000000000002 +2020-04-04 00:45:00,68.89,70.57600000000001,39.184,30.618000000000002 +2020-04-04 01:00:00,71.51,71.94,34.692,30.618000000000002 +2020-04-04 01:15:00,76.86,71.125,34.692,30.618000000000002 +2020-04-04 01:30:00,71.53,69.49,34.692,30.618000000000002 +2020-04-04 01:45:00,68.36,69.517,34.692,30.618000000000002 +2020-04-04 02:00:00,66.58,71.745,32.919000000000004,30.618000000000002 +2020-04-04 02:15:00,62.82,70.472,32.919000000000004,30.618000000000002 +2020-04-04 02:30:00,63.39,72.505,32.919000000000004,30.618000000000002 +2020-04-04 02:45:00,64.96,73.27199999999999,32.919000000000004,30.618000000000002 +2020-04-04 03:00:00,65.71,76.22399999999999,32.024,30.618000000000002 +2020-04-04 03:15:00,63.91,76.88600000000001,32.024,30.618000000000002 +2020-04-04 03:30:00,63.74,75.78699999999999,32.024,30.618000000000002 +2020-04-04 03:45:00,64.35,78.009,32.024,30.618000000000002 +2020-04-04 04:00:00,65.42,87.32600000000001,31.958000000000002,30.618000000000002 +2020-04-04 04:15:00,68.5,97.13600000000001,31.958000000000002,30.618000000000002 +2020-04-04 04:30:00,69.35,95.876,31.958000000000002,30.618000000000002 +2020-04-04 04:45:00,69.44,96.686,31.958000000000002,30.618000000000002 +2020-04-04 05:00:00,72.92,117.115,32.75,30.618000000000002 +2020-04-04 05:15:00,68.32,134.414,32.75,30.618000000000002 +2020-04-04 05:30:00,70.28,126.084,32.75,30.618000000000002 +2020-04-04 05:45:00,69.35,121.125,32.75,30.618000000000002 +2020-04-04 06:00:00,75.32,140.066,34.461999999999996,30.618000000000002 +2020-04-04 06:15:00,76.84,159.474,34.461999999999996,30.618000000000002 +2020-04-04 06:30:00,77.92,152.194,34.461999999999996,30.618000000000002 +2020-04-04 06:45:00,79.19,146.56799999999998,34.461999999999996,30.618000000000002 +2020-04-04 07:00:00,82.21,145.606,37.736,30.618000000000002 +2020-04-04 07:15:00,81.98,146.351,37.736,30.618000000000002 +2020-04-04 07:30:00,81.71,146.433,37.736,30.618000000000002 +2020-04-04 07:45:00,82.74,145.686,37.736,30.618000000000002 +2020-04-04 08:00:00,81.59,147.055,42.34,30.618000000000002 +2020-04-04 08:15:00,81.68,147.328,42.34,30.618000000000002 +2020-04-04 08:30:00,81.19,144.188,42.34,30.618000000000002 +2020-04-04 08:45:00,81.27,142.493,42.34,30.618000000000002 +2020-04-04 09:00:00,78.35,138.297,43.571999999999996,30.618000000000002 +2020-04-04 09:15:00,76.95,138.02700000000002,43.571999999999996,30.618000000000002 +2020-04-04 09:30:00,77.5,139.909,43.571999999999996,30.618000000000002 +2020-04-04 09:45:00,76.01,138.953,43.571999999999996,30.618000000000002 +2020-04-04 10:00:00,74.59,134.138,40.514,30.618000000000002 +2020-04-04 10:15:00,75.0,134.186,40.514,30.618000000000002 +2020-04-04 10:30:00,75.58,132.084,40.514,30.618000000000002 +2020-04-04 10:45:00,74.33,132.252,40.514,30.618000000000002 +2020-04-04 11:00:00,72.45,126.696,36.388000000000005,30.618000000000002 +2020-04-04 11:15:00,71.39,125.946,36.388000000000005,30.618000000000002 +2020-04-04 11:30:00,68.52,127.45,36.388000000000005,30.618000000000002 +2020-04-04 11:45:00,68.28,127.98700000000001,36.388000000000005,30.618000000000002 +2020-04-04 12:00:00,64.47,123.79799999999999,35.217,30.618000000000002 +2020-04-04 12:15:00,63.5,123.37200000000001,35.217,30.618000000000002 +2020-04-04 12:30:00,60.6,123.133,35.217,30.618000000000002 +2020-04-04 12:45:00,61.67,123.70200000000001,35.217,30.618000000000002 +2020-04-04 13:00:00,59.52,124.51700000000001,32.001999999999995,30.618000000000002 +2020-04-04 13:15:00,58.68,122.109,32.001999999999995,30.618000000000002 +2020-04-04 13:30:00,59.1,120.18700000000001,32.001999999999995,30.618000000000002 +2020-04-04 13:45:00,60.24,118.756,32.001999999999995,30.618000000000002 +2020-04-04 14:00:00,58.61,120.09200000000001,31.304000000000002,30.618000000000002 +2020-04-04 14:15:00,58.6,118.131,31.304000000000002,30.618000000000002 +2020-04-04 14:30:00,59.62,117.635,31.304000000000002,30.618000000000002 +2020-04-04 14:45:00,60.05,118.73200000000001,31.304000000000002,30.618000000000002 +2020-04-04 15:00:00,61.39,119.492,34.731,30.618000000000002 +2020-04-04 15:15:00,61.93,118.48299999999999,34.731,30.618000000000002 +2020-04-04 15:30:00,62.76,117.48299999999999,34.731,30.618000000000002 +2020-04-04 15:45:00,65.09,116.90299999999999,34.731,30.618000000000002 +2020-04-04 16:00:00,68.45,117.296,38.769,30.618000000000002 +2020-04-04 16:15:00,69.5,119.079,38.769,30.618000000000002 +2020-04-04 16:30:00,75.03,119.14,38.769,30.618000000000002 +2020-04-04 16:45:00,75.19,117.76299999999999,38.769,30.618000000000002 +2020-04-04 17:00:00,79.59,117.855,44.928000000000004,30.618000000000002 +2020-04-04 17:15:00,80.67,120.13799999999999,44.928000000000004,30.618000000000002 +2020-04-04 17:30:00,82.2,121.93700000000001,44.928000000000004,30.618000000000002 +2020-04-04 17:45:00,83.92,122.765,44.928000000000004,30.618000000000002 +2020-04-04 18:00:00,85.73,126.61399999999999,47.786,30.618000000000002 +2020-04-04 18:15:00,83.59,129.254,47.786,30.618000000000002 +2020-04-04 18:30:00,83.84,129.518,47.786,30.618000000000002 +2020-04-04 18:45:00,85.08,131.211,47.786,30.618000000000002 +2020-04-04 19:00:00,87.47,130.589,47.463,30.618000000000002 +2020-04-04 19:15:00,90.07,129.77700000000002,47.463,30.618000000000002 +2020-04-04 19:30:00,88.24,130.158,47.463,30.618000000000002 +2020-04-04 19:45:00,84.51,130.01,47.463,30.618000000000002 +2020-04-04 20:00:00,81.76,126.954,43.735,30.618000000000002 +2020-04-04 20:15:00,80.12,125.14200000000001,43.735,30.618000000000002 +2020-04-04 20:30:00,76.99,123.64399999999999,43.735,30.618000000000002 +2020-04-04 20:45:00,76.56,123.34,43.735,30.618000000000002 +2020-04-04 21:00:00,70.25,118.79799999999999,40.346,30.618000000000002 +2020-04-04 21:15:00,69.31,118.615,40.346,30.618000000000002 +2020-04-04 21:30:00,66.75,119.72200000000001,40.346,30.618000000000002 +2020-04-04 21:45:00,66.4,118.525,40.346,30.618000000000002 +2020-04-04 22:00:00,62.9,113.759,39.323,30.618000000000002 +2020-04-04 22:15:00,62.01,112.402,39.323,30.618000000000002 +2020-04-04 22:30:00,59.88,110.391,39.323,30.618000000000002 +2020-04-04 22:45:00,58.88,107.417,39.323,30.618000000000002 +2020-04-04 23:00:00,54.7,100.368,33.716,30.618000000000002 +2020-04-04 23:15:00,54.75,96.571,33.716,30.618000000000002 +2020-04-04 23:30:00,52.99,93.90100000000001,33.716,30.618000000000002 +2020-04-04 23:45:00,53.7,92.859,33.716,30.618000000000002 +2020-04-05 00:00:00,49.93,76.092,28.703000000000003,30.618000000000002 +2020-04-05 00:15:00,50.91,72.97800000000001,28.703000000000003,30.618000000000002 +2020-04-05 00:30:00,49.61,71.447,28.703000000000003,30.618000000000002 +2020-04-05 00:45:00,49.78,70.42,28.703000000000003,30.618000000000002 +2020-04-05 01:00:00,48.79,71.824,26.171,30.618000000000002 +2020-04-05 01:15:00,49.86,71.562,26.171,30.618000000000002 +2020-04-05 01:30:00,49.35,70.15,26.171,30.618000000000002 +2020-04-05 01:45:00,49.74,69.775,26.171,30.618000000000002 +2020-04-05 02:00:00,49.25,71.568,25.326999999999998,30.618000000000002 +2020-04-05 02:15:00,49.88,70.153,25.326999999999998,30.618000000000002 +2020-04-05 02:30:00,49.37,72.839,25.326999999999998,30.618000000000002 +2020-04-05 02:45:00,49.41,73.73100000000001,25.326999999999998,30.618000000000002 +2020-04-05 03:00:00,48.8,77.26,24.311999999999998,30.618000000000002 +2020-04-05 03:15:00,49.81,77.726,24.311999999999998,30.618000000000002 +2020-04-05 03:30:00,50.35,77.057,24.311999999999998,30.618000000000002 +2020-04-05 03:45:00,51.13,78.79899999999999,24.311999999999998,30.618000000000002 +2020-04-05 04:00:00,51.43,87.915,25.33,30.618000000000002 +2020-04-05 04:15:00,52.55,96.831,25.33,30.618000000000002 +2020-04-05 04:30:00,54.44,96.38600000000001,25.33,30.618000000000002 +2020-04-05 04:45:00,58.04,97.081,25.33,30.618000000000002 +2020-04-05 05:00:00,57.28,115.42299999999999,25.309,30.618000000000002 +2020-04-05 05:15:00,58.24,130.764,25.309,30.618000000000002 +2020-04-05 05:30:00,55.96,122.105,25.309,30.618000000000002 +2020-04-05 05:45:00,57.4,117.171,25.309,30.618000000000002 +2020-04-05 06:00:00,58.75,134.681,25.945999999999998,30.618000000000002 +2020-04-05 06:15:00,58.9,153.49,25.945999999999998,30.618000000000002 +2020-04-05 06:30:00,59.93,145.111,25.945999999999998,30.618000000000002 +2020-04-05 06:45:00,63.35,138.262,25.945999999999998,30.618000000000002 +2020-04-05 07:00:00,64.84,138.942,27.87,30.618000000000002 +2020-04-05 07:15:00,67.39,138.22799999999998,27.87,30.618000000000002 +2020-04-05 07:30:00,67.1,138.191,27.87,30.618000000000002 +2020-04-05 07:45:00,64.74,136.928,27.87,30.618000000000002 +2020-04-05 08:00:00,65.25,139.78799999999998,32.114000000000004,30.618000000000002 +2020-04-05 08:15:00,65.1,140.642,32.114000000000004,30.618000000000002 +2020-04-05 08:30:00,64.21,139.002,32.114000000000004,30.618000000000002 +2020-04-05 08:45:00,63.33,138.611,32.114000000000004,30.618000000000002 +2020-04-05 09:00:00,64.31,134.07,34.222,30.618000000000002 +2020-04-05 09:15:00,61.83,133.924,34.222,30.618000000000002 +2020-04-05 09:30:00,60.69,135.941,34.222,30.618000000000002 +2020-04-05 09:45:00,60.76,135.444,34.222,30.618000000000002 +2020-04-05 10:00:00,61.17,132.583,34.544000000000004,30.618000000000002 +2020-04-05 10:15:00,63.73,133.114,34.544000000000004,30.618000000000002 +2020-04-05 10:30:00,64.51,131.56,34.544000000000004,30.618000000000002 +2020-04-05 10:45:00,63.78,130.931,34.544000000000004,30.618000000000002 +2020-04-05 11:00:00,60.97,125.81200000000001,36.368,30.618000000000002 +2020-04-05 11:15:00,58.9,124.97,36.368,30.618000000000002 +2020-04-05 11:30:00,56.67,126.111,36.368,30.618000000000002 +2020-04-05 11:45:00,56.1,127.214,36.368,30.618000000000002 +2020-04-05 12:00:00,52.5,123.1,32.433,30.618000000000002 +2020-04-05 12:15:00,54.28,123.76299999999999,32.433,30.618000000000002 +2020-04-05 12:30:00,51.11,122.53399999999999,32.433,30.618000000000002 +2020-04-05 12:45:00,51.46,122.119,32.433,30.618000000000002 +2020-04-05 13:00:00,50.91,122.304,28.971999999999998,30.618000000000002 +2020-04-05 13:15:00,50.33,121.807,28.971999999999998,30.618000000000002 +2020-04-05 13:30:00,49.91,119.228,28.971999999999998,30.618000000000002 +2020-04-05 13:45:00,50.1,117.82600000000001,28.971999999999998,30.618000000000002 +2020-04-05 14:00:00,50.18,119.947,25.531999999999996,30.618000000000002 +2020-04-05 14:15:00,50.22,119.027,25.531999999999996,30.618000000000002 +2020-04-05 14:30:00,50.84,118.82700000000001,25.531999999999996,30.618000000000002 +2020-04-05 14:45:00,51.63,119.15,25.531999999999996,30.618000000000002 +2020-04-05 15:00:00,52.57,118.89200000000001,25.766,30.618000000000002 +2020-04-05 15:15:00,53.17,118.04299999999999,25.766,30.618000000000002 +2020-04-05 15:30:00,54.25,117.337,25.766,30.618000000000002 +2020-04-05 15:45:00,57.67,117.369,25.766,30.618000000000002 +2020-04-05 16:00:00,62.85,118.23899999999999,29.232,30.618000000000002 +2020-04-05 16:15:00,63.15,119.47,29.232,30.618000000000002 +2020-04-05 16:30:00,67.45,120.189,29.232,30.618000000000002 +2020-04-05 16:45:00,71.68,118.87299999999999,29.232,30.618000000000002 +2020-04-05 17:00:00,75.8,119.089,37.431,30.618000000000002 +2020-04-05 17:15:00,78.13,121.87700000000001,37.431,30.618000000000002 +2020-04-05 17:30:00,80.23,124.274,37.431,30.618000000000002 +2020-04-05 17:45:00,80.84,126.838,37.431,30.618000000000002 +2020-04-05 18:00:00,83.42,130.58700000000002,41.251999999999995,30.618000000000002 +2020-04-05 18:15:00,82.93,133.947,41.251999999999995,30.618000000000002 +2020-04-05 18:30:00,85.4,132.678,41.251999999999995,30.618000000000002 +2020-04-05 18:45:00,86.36,135.68,41.251999999999995,30.618000000000002 +2020-04-05 19:00:00,89.34,135.89700000000002,41.784,30.618000000000002 +2020-04-05 19:15:00,96.35,134.91899999999998,41.784,30.618000000000002 +2020-04-05 19:30:00,97.94,135.082,41.784,30.618000000000002 +2020-04-05 19:45:00,90.74,135.688,41.784,30.618000000000002 +2020-04-05 20:00:00,83.74,132.64,40.804,30.618000000000002 +2020-04-05 20:15:00,84.54,131.41299999999998,40.804,30.618000000000002 +2020-04-05 20:30:00,88.61,131.183,40.804,30.618000000000002 +2020-04-05 20:45:00,93.47,129.24200000000002,40.804,30.618000000000002 +2020-04-05 21:00:00,90.23,122.90100000000001,38.379,30.618000000000002 +2020-04-05 21:15:00,85.48,122.161,38.379,30.618000000000002 +2020-04-05 21:30:00,81.64,123.109,38.379,30.618000000000002 +2020-04-05 21:45:00,78.03,122.22,38.379,30.618000000000002 +2020-04-05 22:00:00,72.89,117.758,37.87,30.618000000000002 +2020-04-05 22:15:00,76.43,115.053,37.87,30.618000000000002 +2020-04-05 22:30:00,78.18,110.678,37.87,30.618000000000002 +2020-04-05 22:45:00,78.53,106.478,37.87,30.618000000000002 +2020-04-05 23:00:00,72.6,97.42299999999999,33.332,30.618000000000002 +2020-04-05 23:15:00,69.86,95.53299999999999,33.332,30.618000000000002 +2020-04-05 23:30:00,67.11,93.111,33.332,30.618000000000002 +2020-04-05 23:45:00,71.83,92.75,33.332,30.618000000000002 +2020-04-06 00:00:00,72.06,79.122,34.698,30.736 +2020-04-06 00:15:00,73.34,78.301,34.698,30.736 +2020-04-06 00:30:00,69.38,76.625,34.698,30.736 +2020-04-06 00:45:00,66.41,75.048,34.698,30.736 +2020-04-06 01:00:00,65.32,76.639,32.889,30.736 +2020-04-06 01:15:00,71.76,76.038,32.889,30.736 +2020-04-06 01:30:00,72.41,74.84100000000001,32.889,30.736 +2020-04-06 01:45:00,73.18,74.486,32.889,30.736 +2020-04-06 02:00:00,68.6,76.492,32.06,30.736 +2020-04-06 02:15:00,70.89,75.381,32.06,30.736 +2020-04-06 02:30:00,74.64,78.384,32.06,30.736 +2020-04-06 02:45:00,75.36,78.82,32.06,30.736 +2020-04-06 03:00:00,73.31,83.4,30.515,30.736 +2020-04-06 03:15:00,75.92,85.24799999999999,30.515,30.736 +2020-04-06 03:30:00,80.31,84.79899999999999,30.515,30.736 +2020-04-06 03:45:00,82.56,85.961,30.515,30.736 +2020-04-06 04:00:00,85.18,99.426,31.436,30.736 +2020-04-06 04:15:00,84.27,112.485,31.436,30.736 +2020-04-06 04:30:00,87.01,113.088,31.436,30.736 +2020-04-06 04:45:00,88.33,114.081,31.436,30.736 +2020-04-06 05:00:00,97.35,145.80200000000002,38.997,30.736 +2020-04-06 05:15:00,102.88,179.06,38.997,30.736 +2020-04-06 05:30:00,104.6,169.676,38.997,30.736 +2020-04-06 05:45:00,103.3,159.631,38.997,30.736 +2020-04-06 06:00:00,111.48,160.849,54.97,30.736 +2020-04-06 06:15:00,112.43,165.453,54.97,30.736 +2020-04-06 06:30:00,112.7,164.183,54.97,30.736 +2020-04-06 06:45:00,114.94,165.671,54.97,30.736 +2020-04-06 07:00:00,116.47,168.513,66.032,30.736 +2020-04-06 07:15:00,114.52,169.805,66.032,30.736 +2020-04-06 07:30:00,112.82,168.767,66.032,30.736 +2020-04-06 07:45:00,111.27,166.037,66.032,30.736 +2020-04-06 08:00:00,109.22,165.03599999999997,59.941,30.736 +2020-04-06 08:15:00,108.67,163.886,59.941,30.736 +2020-04-06 08:30:00,107.94,159.04,59.941,30.736 +2020-04-06 08:45:00,106.68,156.61,59.941,30.736 +2020-04-06 09:00:00,105.31,151.07299999999998,54.016000000000005,30.736 +2020-04-06 09:15:00,105.6,147.696,54.016000000000005,30.736 +2020-04-06 09:30:00,104.71,148.596,54.016000000000005,30.736 +2020-04-06 09:45:00,103.31,147.168,54.016000000000005,30.736 +2020-04-06 10:00:00,101.9,144.064,50.63,30.736 +2020-04-06 10:15:00,104.54,144.34799999999998,50.63,30.736 +2020-04-06 10:30:00,103.43,141.99200000000002,50.63,30.736 +2020-04-06 10:45:00,105.54,141.142,50.63,30.736 +2020-04-06 11:00:00,100.25,134.44,49.951,30.736 +2020-04-06 11:15:00,103.21,135.025,49.951,30.736 +2020-04-06 11:30:00,98.48,137.47,49.951,30.736 +2020-04-06 11:45:00,100.55,138.54,49.951,30.736 +2020-04-06 12:00:00,103.01,135.093,46.913000000000004,30.736 +2020-04-06 12:15:00,108.32,135.816,46.913000000000004,30.736 +2020-04-06 12:30:00,107.08,134.238,46.913000000000004,30.736 +2020-04-06 12:45:00,101.4,134.923,46.913000000000004,30.736 +2020-04-06 13:00:00,102.39,136.09,47.093999999999994,30.736 +2020-04-06 13:15:00,101.73,134.179,47.093999999999994,30.736 +2020-04-06 13:30:00,102.31,131.29,47.093999999999994,30.736 +2020-04-06 13:45:00,109.82,130.314,47.093999999999994,30.736 +2020-04-06 14:00:00,111.7,131.673,46.678000000000004,30.736 +2020-04-06 14:15:00,107.6,130.526,46.678000000000004,30.736 +2020-04-06 14:30:00,101.94,129.817,46.678000000000004,30.736 +2020-04-06 14:45:00,103.94,131.03799999999998,46.678000000000004,30.736 +2020-04-06 15:00:00,103.09,131.95600000000002,47.715,30.736 +2020-04-06 15:15:00,104.75,129.77100000000002,47.715,30.736 +2020-04-06 15:30:00,107.02,128.752,47.715,30.736 +2020-04-06 15:45:00,106.7,128.225,47.715,30.736 +2020-04-06 16:00:00,111.87,129.474,49.81100000000001,30.736 +2020-04-06 16:15:00,109.46,130.173,49.81100000000001,30.736 +2020-04-06 16:30:00,112.43,129.903,49.81100000000001,30.736 +2020-04-06 16:45:00,110.12,127.697,49.81100000000001,30.736 +2020-04-06 17:00:00,116.7,127.12899999999999,55.591,30.736 +2020-04-06 17:15:00,115.27,129.43200000000002,55.591,30.736 +2020-04-06 17:30:00,116.49,131.267,55.591,30.736 +2020-04-06 17:45:00,114.97,132.599,55.591,30.736 +2020-04-06 18:00:00,118.43,136.04,56.523,30.736 +2020-04-06 18:15:00,116.2,137.036,56.523,30.736 +2020-04-06 18:30:00,115.3,135.815,56.523,30.736 +2020-04-06 18:45:00,112.64,140.894,56.523,30.736 +2020-04-06 19:00:00,111.96,139.827,56.044,30.736 +2020-04-06 19:15:00,117.29,138.756,56.044,30.736 +2020-04-06 19:30:00,115.88,139.08,56.044,30.736 +2020-04-06 19:45:00,110.57,138.85299999999998,56.044,30.736 +2020-04-06 20:00:00,102.39,133.55,61.715,30.736 +2020-04-06 20:15:00,99.43,131.42600000000002,61.715,30.736 +2020-04-06 20:30:00,100.45,130.275,61.715,30.736 +2020-04-06 20:45:00,98.33,129.54,61.715,30.736 +2020-04-06 21:00:00,101.37,123.23200000000001,56.24,30.736 +2020-04-06 21:15:00,100.59,121.96799999999999,56.24,30.736 +2020-04-06 21:30:00,94.39,122.585,56.24,30.736 +2020-04-06 21:45:00,88.76,121.28200000000001,56.24,30.736 +2020-04-06 22:00:00,84.27,113.863,50.437,30.736 +2020-04-06 22:15:00,87.71,111.35700000000001,50.437,30.736 +2020-04-06 22:30:00,87.15,98.445,50.437,30.736 +2020-04-06 22:45:00,85.4,91.066,50.437,30.736 +2020-04-06 23:00:00,73.0,82.488,42.756,30.736 +2020-04-06 23:15:00,73.24,81.514,42.756,30.736 +2020-04-06 23:30:00,69.53,80.881,42.756,30.736 +2020-04-06 23:45:00,72.17,81.986,42.756,30.736 +2020-04-07 00:00:00,69.6,77.01899999999999,39.857,30.736 +2020-04-07 00:15:00,70.65,77.579,39.857,30.736 +2020-04-07 00:30:00,69.86,75.704,39.857,30.736 +2020-04-07 00:45:00,70.53,73.96600000000001,39.857,30.736 +2020-04-07 01:00:00,69.2,75.119,37.233000000000004,30.736 +2020-04-07 01:15:00,73.23,74.282,37.233000000000004,30.736 +2020-04-07 01:30:00,72.34,73.097,37.233000000000004,30.736 +2020-04-07 01:45:00,73.13,72.648,37.233000000000004,30.736 +2020-04-07 02:00:00,72.15,74.37,35.856,30.736 +2020-04-07 02:15:00,72.53,73.82600000000001,35.856,30.736 +2020-04-07 02:30:00,68.0,76.265,35.856,30.736 +2020-04-07 02:45:00,74.06,76.898,35.856,30.736 +2020-04-07 03:00:00,72.61,80.45100000000001,34.766999999999996,30.736 +2020-04-07 03:15:00,72.94,82.287,34.766999999999996,30.736 +2020-04-07 03:30:00,74.44,82.10799999999999,34.766999999999996,30.736 +2020-04-07 03:45:00,76.22,82.777,34.766999999999996,30.736 +2020-04-07 04:00:00,80.49,95.38600000000001,35.468,30.736 +2020-04-07 04:15:00,82.48,108.21,35.468,30.736 +2020-04-07 04:30:00,85.73,108.561,35.468,30.736 +2020-04-07 04:45:00,90.58,110.554,35.468,30.736 +2020-04-07 05:00:00,100.18,146.112,40.399,30.736 +2020-04-07 05:15:00,102.56,179.535,40.399,30.736 +2020-04-07 05:30:00,104.29,169.34400000000002,40.399,30.736 +2020-04-07 05:45:00,106.96,158.921,40.399,30.736 +2020-04-07 06:00:00,110.4,160.033,54.105,30.736 +2020-04-07 06:15:00,110.63,165.732,54.105,30.736 +2020-04-07 06:30:00,112.93,163.855,54.105,30.736 +2020-04-07 06:45:00,116.12,164.59900000000002,54.105,30.736 +2020-04-07 07:00:00,116.23,167.41099999999997,63.083,30.736 +2020-04-07 07:15:00,115.06,168.44,63.083,30.736 +2020-04-07 07:30:00,115.2,167.024,63.083,30.736 +2020-04-07 07:45:00,113.5,163.887,63.083,30.736 +2020-04-07 08:00:00,110.51,162.91,57.254,30.736 +2020-04-07 08:15:00,109.08,160.877,57.254,30.736 +2020-04-07 08:30:00,109.23,155.965,57.254,30.736 +2020-04-07 08:45:00,109.56,152.909,57.254,30.736 +2020-04-07 09:00:00,107.69,146.993,51.395,30.736 +2020-04-07 09:15:00,108.09,144.628,51.395,30.736 +2020-04-07 09:30:00,104.87,146.349,51.395,30.736 +2020-04-07 09:45:00,106.35,145.477,51.395,30.736 +2020-04-07 10:00:00,103.01,141.299,48.201,30.736 +2020-04-07 10:15:00,102.97,140.826,48.201,30.736 +2020-04-07 10:30:00,100.59,138.631,48.201,30.736 +2020-04-07 10:45:00,102.15,138.48,48.201,30.736 +2020-04-07 11:00:00,99.8,132.81,46.133,30.736 +2020-04-07 11:15:00,99.29,133.372,46.133,30.736 +2020-04-07 11:30:00,97.42,134.46,46.133,30.736 +2020-04-07 11:45:00,99.27,135.839,46.133,30.736 +2020-04-07 12:00:00,102.69,131.346,44.243,30.736 +2020-04-07 12:15:00,103.58,131.928,44.243,30.736 +2020-04-07 12:30:00,101.04,131.215,44.243,30.736 +2020-04-07 12:45:00,96.93,131.998,44.243,30.736 +2020-04-07 13:00:00,95.94,132.735,45.042,30.736 +2020-04-07 13:15:00,97.03,131.35399999999998,45.042,30.736 +2020-04-07 13:30:00,100.96,129.305,45.042,30.736 +2020-04-07 13:45:00,103.24,128.042,45.042,30.736 +2020-04-07 14:00:00,101.28,129.875,44.062,30.736 +2020-04-07 14:15:00,102.93,128.732,44.062,30.736 +2020-04-07 14:30:00,106.26,128.583,44.062,30.736 +2020-04-07 14:45:00,104.54,129.403,44.062,30.736 +2020-04-07 15:00:00,100.32,129.96200000000002,46.461999999999996,30.736 +2020-04-07 15:15:00,100.71,128.40200000000002,46.461999999999996,30.736 +2020-04-07 15:30:00,104.76,127.404,46.461999999999996,30.736 +2020-04-07 15:45:00,106.51,126.71600000000001,46.461999999999996,30.736 +2020-04-07 16:00:00,105.14,127.979,48.802,30.736 +2020-04-07 16:15:00,108.47,129.028,48.802,30.736 +2020-04-07 16:30:00,110.54,129.042,48.802,30.736 +2020-04-07 16:45:00,111.8,127.31,48.802,30.736 +2020-04-07 17:00:00,111.67,127.27799999999999,55.672,30.736 +2020-04-07 17:15:00,118.31,129.785,55.672,30.736 +2020-04-07 17:30:00,117.23,131.864,55.672,30.736 +2020-04-07 17:45:00,116.51,132.929,55.672,30.736 +2020-04-07 18:00:00,113.28,135.825,57.006,30.736 +2020-04-07 18:15:00,116.51,137.267,57.006,30.736 +2020-04-07 18:30:00,115.8,135.695,57.006,30.736 +2020-04-07 18:45:00,116.19,141.224,57.006,30.736 +2020-04-07 19:00:00,111.29,139.591,57.148,30.736 +2020-04-07 19:15:00,116.99,138.437,57.148,30.736 +2020-04-07 19:30:00,116.1,138.225,57.148,30.736 +2020-04-07 19:45:00,113.98,138.225,57.148,30.736 +2020-04-07 20:00:00,104.78,133.197,61.895,30.736 +2020-04-07 20:15:00,109.17,129.917,61.895,30.736 +2020-04-07 20:30:00,105.18,129.49200000000002,61.895,30.736 +2020-04-07 20:45:00,104.44,128.619,61.895,30.736 +2020-04-07 21:00:00,94.22,122.29899999999999,54.78,30.736 +2020-04-07 21:15:00,98.11,120.87200000000001,54.78,30.736 +2020-04-07 21:30:00,95.36,121.095,54.78,30.736 +2020-04-07 21:45:00,95.09,120.073,54.78,30.736 +2020-04-07 22:00:00,82.41,113.87100000000001,50.76,30.736 +2020-04-07 22:15:00,81.69,111.046,50.76,30.736 +2020-04-07 22:30:00,86.54,98.31200000000001,50.76,30.736 +2020-04-07 22:45:00,86.26,91.10600000000001,50.76,30.736 +2020-04-07 23:00:00,79.29,82.12700000000001,44.162,30.736 +2020-04-07 23:15:00,77.16,81.404,44.162,30.736 +2020-04-07 23:30:00,74.84,80.531,44.162,30.736 +2020-04-07 23:45:00,80.07,81.457,44.162,30.736 +2020-04-08 00:00:00,77.66,76.607,39.061,30.736 +2020-04-08 00:15:00,75.81,77.182,39.061,30.736 +2020-04-08 00:30:00,72.88,75.296,39.061,30.736 +2020-04-08 00:45:00,69.77,73.563,39.061,30.736 +2020-04-08 01:00:00,73.18,74.697,35.795,30.736 +2020-04-08 01:15:00,76.79,73.837,35.795,30.736 +2020-04-08 01:30:00,75.5,72.631,35.795,30.736 +2020-04-08 01:45:00,70.46,72.187,35.795,30.736 +2020-04-08 02:00:00,71.43,73.89699999999999,33.316,30.736 +2020-04-08 02:15:00,70.31,73.337,33.316,30.736 +2020-04-08 02:30:00,70.6,75.797,33.316,30.736 +2020-04-08 02:45:00,78.12,76.435,33.316,30.736 +2020-04-08 03:00:00,79.43,80.005,32.803000000000004,30.736 +2020-04-08 03:15:00,79.12,81.814,32.803000000000004,30.736 +2020-04-08 03:30:00,75.54,81.631,32.803000000000004,30.736 +2020-04-08 03:45:00,77.28,82.321,32.803000000000004,30.736 +2020-04-08 04:00:00,80.55,94.90899999999999,34.235,30.736 +2020-04-08 04:15:00,81.61,107.698,34.235,30.736 +2020-04-08 04:30:00,85.38,108.055,34.235,30.736 +2020-04-08 04:45:00,90.05,110.037,34.235,30.736 +2020-04-08 05:00:00,97.97,145.516,38.65,30.736 +2020-04-08 05:15:00,99.52,178.87,38.65,30.736 +2020-04-08 05:30:00,102.97,168.683,38.65,30.736 +2020-04-08 05:45:00,107.04,158.297,38.65,30.736 +2020-04-08 06:00:00,111.56,159.431,54.951,30.736 +2020-04-08 06:15:00,113.89,165.113,54.951,30.736 +2020-04-08 06:30:00,116.9,163.208,54.951,30.736 +2020-04-08 06:45:00,118.83,163.938,54.951,30.736 +2020-04-08 07:00:00,121.7,166.757,67.328,30.736 +2020-04-08 07:15:00,121.52,167.764,67.328,30.736 +2020-04-08 07:30:00,120.53,166.30700000000002,67.328,30.736 +2020-04-08 07:45:00,119.16,163.157,67.328,30.736 +2020-04-08 08:00:00,115.39,162.161,60.23,30.736 +2020-04-08 08:15:00,117.51,160.15,60.23,30.736 +2020-04-08 08:30:00,118.44,155.19899999999998,60.23,30.736 +2020-04-08 08:45:00,119.61,152.173,60.23,30.736 +2020-04-08 09:00:00,120.78,146.264,56.845,30.736 +2020-04-08 09:15:00,122.55,143.903,56.845,30.736 +2020-04-08 09:30:00,122.95,145.643,56.845,30.736 +2020-04-08 09:45:00,122.4,144.80100000000002,56.845,30.736 +2020-04-08 10:00:00,122.17,140.63,53.832,30.736 +2020-04-08 10:15:00,121.08,140.207,53.832,30.736 +2020-04-08 10:30:00,124.05,138.03799999999998,53.832,30.736 +2020-04-08 10:45:00,121.53,137.908,53.832,30.736 +2020-04-08 11:00:00,113.64,132.23,53.225,30.736 +2020-04-08 11:15:00,107.84,132.815,53.225,30.736 +2020-04-08 11:30:00,106.89,133.906,53.225,30.736 +2020-04-08 11:45:00,109.26,135.306,53.225,30.736 +2020-04-08 12:00:00,115.23,130.839,50.676,30.736 +2020-04-08 12:15:00,114.32,131.429,50.676,30.736 +2020-04-08 12:30:00,105.49,130.672,50.676,30.736 +2020-04-08 12:45:00,103.19,131.458,50.676,30.736 +2020-04-08 13:00:00,101.47,132.237,50.646,30.736 +2020-04-08 13:15:00,107.86,130.846,50.646,30.736 +2020-04-08 13:30:00,106.14,128.796,50.646,30.736 +2020-04-08 13:45:00,114.59,127.53399999999999,50.646,30.736 +2020-04-08 14:00:00,122.83,129.437,50.786,30.736 +2020-04-08 14:15:00,123.75,128.27200000000002,50.786,30.736 +2020-04-08 14:30:00,119.06,128.078,50.786,30.736 +2020-04-08 14:45:00,114.36,128.901,50.786,30.736 +2020-04-08 15:00:00,108.95,129.488,51.535,30.736 +2020-04-08 15:15:00,110.42,127.902,51.535,30.736 +2020-04-08 15:30:00,107.51,126.854,51.535,30.736 +2020-04-08 15:45:00,110.73,126.148,51.535,30.736 +2020-04-08 16:00:00,116.16,127.445,53.157,30.736 +2020-04-08 16:15:00,113.87,128.469,53.157,30.736 +2020-04-08 16:30:00,113.4,128.485,53.157,30.736 +2020-04-08 16:45:00,118.97,126.69,53.157,30.736 +2020-04-08 17:00:00,120.55,126.706,57.793,30.736 +2020-04-08 17:15:00,115.72,129.194,57.793,30.736 +2020-04-08 17:30:00,117.9,131.276,57.793,30.736 +2020-04-08 17:45:00,119.34,132.32399999999998,57.793,30.736 +2020-04-08 18:00:00,118.83,135.232,59.872,30.736 +2020-04-08 18:15:00,114.99,136.711,59.872,30.736 +2020-04-08 18:30:00,117.03,135.128,59.872,30.736 +2020-04-08 18:45:00,117.74,140.667,59.872,30.736 +2020-04-08 19:00:00,117.3,139.016,60.17100000000001,30.736 +2020-04-08 19:15:00,117.96,137.872,60.17100000000001,30.736 +2020-04-08 19:30:00,117.33,137.67700000000002,60.17100000000001,30.736 +2020-04-08 19:45:00,114.3,137.71,60.17100000000001,30.736 +2020-04-08 20:00:00,102.99,132.656,65.015,30.736 +2020-04-08 20:15:00,103.02,129.386,65.015,30.736 +2020-04-08 20:30:00,107.09,128.996,65.015,30.736 +2020-04-08 20:45:00,106.0,128.14600000000002,65.015,30.736 +2020-04-08 21:00:00,97.85,121.82600000000001,57.805,30.736 +2020-04-08 21:15:00,90.97,120.40899999999999,57.805,30.736 +2020-04-08 21:30:00,89.79,120.62299999999999,57.805,30.736 +2020-04-08 21:45:00,86.79,119.63,57.805,30.736 +2020-04-08 22:00:00,81.91,113.434,52.115,30.736 +2020-04-08 22:15:00,83.05,110.63600000000001,52.115,30.736 +2020-04-08 22:30:00,86.93,97.867,52.115,30.736 +2020-04-08 22:45:00,87.46,90.656,52.115,30.736 +2020-04-08 23:00:00,83.24,81.655,42.871,30.736 +2020-04-08 23:15:00,77.77,80.96300000000001,42.871,30.736 +2020-04-08 23:30:00,78.21,80.09,42.871,30.736 +2020-04-08 23:45:00,81.71,81.031,42.871,30.736 +2020-04-09 00:00:00,75.02,76.195,39.203,30.736 +2020-04-09 00:15:00,76.58,76.783,39.203,30.736 +2020-04-09 00:30:00,70.26,74.887,39.203,30.736 +2020-04-09 00:45:00,71.93,73.15899999999999,39.203,30.736 +2020-04-09 01:00:00,73.86,74.275,37.118,30.736 +2020-04-09 01:15:00,78.67,73.393,37.118,30.736 +2020-04-09 01:30:00,78.31,72.165,37.118,30.736 +2020-04-09 01:45:00,73.67,71.726,37.118,30.736 +2020-04-09 02:00:00,71.77,73.42399999999999,35.647,30.736 +2020-04-09 02:15:00,74.57,72.848,35.647,30.736 +2020-04-09 02:30:00,78.65,75.328,35.647,30.736 +2020-04-09 02:45:00,79.41,75.971,35.647,30.736 +2020-04-09 03:00:00,73.94,79.559,34.585,30.736 +2020-04-09 03:15:00,75.69,81.342,34.585,30.736 +2020-04-09 03:30:00,76.28,81.153,34.585,30.736 +2020-04-09 03:45:00,77.96,81.862,34.585,30.736 +2020-04-09 04:00:00,84.48,94.429,36.184,30.736 +2020-04-09 04:15:00,90.28,107.18700000000001,36.184,30.736 +2020-04-09 04:30:00,90.56,107.54799999999999,36.184,30.736 +2020-04-09 04:45:00,91.01,109.51899999999999,36.184,30.736 +2020-04-09 05:00:00,97.64,144.918,41.019,30.736 +2020-04-09 05:15:00,101.38,178.203,41.019,30.736 +2020-04-09 05:30:00,106.13,168.021,41.019,30.736 +2020-04-09 05:45:00,108.39,157.672,41.019,30.736 +2020-04-09 06:00:00,111.79,158.826,53.963,30.736 +2020-04-09 06:15:00,114.39,164.495,53.963,30.736 +2020-04-09 06:30:00,116.98,162.559,53.963,30.736 +2020-04-09 06:45:00,121.2,163.276,53.963,30.736 +2020-04-09 07:00:00,121.1,166.09900000000002,66.512,30.736 +2020-04-09 07:15:00,120.6,167.085,66.512,30.736 +2020-04-09 07:30:00,119.91,165.58700000000002,66.512,30.736 +2020-04-09 07:45:00,118.39,162.42600000000002,66.512,30.736 +2020-04-09 08:00:00,117.51,161.411,58.86,30.736 +2020-04-09 08:15:00,117.76,159.423,58.86,30.736 +2020-04-09 08:30:00,121.48,154.435,58.86,30.736 +2020-04-09 08:45:00,121.83,151.436,58.86,30.736 +2020-04-09 09:00:00,120.69,145.533,52.156000000000006,30.736 +2020-04-09 09:15:00,115.59,143.17700000000002,52.156000000000006,30.736 +2020-04-09 09:30:00,110.08,144.937,52.156000000000006,30.736 +2020-04-09 09:45:00,113.84,144.123,52.156000000000006,30.736 +2020-04-09 10:00:00,105.34,139.96200000000002,49.034,30.736 +2020-04-09 10:15:00,105.65,139.588,49.034,30.736 +2020-04-09 10:30:00,104.77,137.445,49.034,30.736 +2020-04-09 10:45:00,105.43,137.336,49.034,30.736 +2020-04-09 11:00:00,100.95,131.65,46.53,30.736 +2020-04-09 11:15:00,102.14,132.259,46.53,30.736 +2020-04-09 11:30:00,101.09,133.35299999999998,46.53,30.736 +2020-04-09 11:45:00,101.76,134.774,46.53,30.736 +2020-04-09 12:00:00,100.18,130.33100000000002,43.318000000000005,30.736 +2020-04-09 12:15:00,105.05,130.931,43.318000000000005,30.736 +2020-04-09 12:30:00,101.1,130.13,43.318000000000005,30.736 +2020-04-09 12:45:00,98.59,130.916,43.318000000000005,30.736 +2020-04-09 13:00:00,98.99,131.74,41.608000000000004,30.736 +2020-04-09 13:15:00,100.83,130.338,41.608000000000004,30.736 +2020-04-09 13:30:00,100.23,128.286,41.608000000000004,30.736 +2020-04-09 13:45:00,97.6,127.027,41.608000000000004,30.736 +2020-04-09 14:00:00,107.53,128.997,41.786,30.736 +2020-04-09 14:15:00,114.19,127.811,41.786,30.736 +2020-04-09 14:30:00,114.7,127.574,41.786,30.736 +2020-04-09 14:45:00,112.0,128.401,41.786,30.736 +2020-04-09 15:00:00,111.51,129.015,44.181999999999995,30.736 +2020-04-09 15:15:00,112.57,127.40299999999999,44.181999999999995,30.736 +2020-04-09 15:30:00,107.02,126.306,44.181999999999995,30.736 +2020-04-09 15:45:00,110.02,125.58,44.181999999999995,30.736 +2020-04-09 16:00:00,117.62,126.913,45.956,30.736 +2020-04-09 16:15:00,115.32,127.911,45.956,30.736 +2020-04-09 16:30:00,113.91,127.928,45.956,30.736 +2020-04-09 16:45:00,115.9,126.07,45.956,30.736 +2020-04-09 17:00:00,120.99,126.135,50.702,30.736 +2020-04-09 17:15:00,120.94,128.60299999999998,50.702,30.736 +2020-04-09 17:30:00,122.61,130.686,50.702,30.736 +2020-04-09 17:45:00,117.98,131.719,50.702,30.736 +2020-04-09 18:00:00,116.45,134.638,53.595,30.736 +2020-04-09 18:15:00,119.67,136.156,53.595,30.736 +2020-04-09 18:30:00,119.28,134.56,53.595,30.736 +2020-04-09 18:45:00,118.54,140.11,53.595,30.736 +2020-04-09 19:00:00,113.55,138.439,54.207,30.736 +2020-04-09 19:15:00,111.16,137.30700000000002,54.207,30.736 +2020-04-09 19:30:00,116.65,137.13,54.207,30.736 +2020-04-09 19:45:00,115.97,137.195,54.207,30.736 +2020-04-09 20:00:00,108.98,132.112,56.948,30.736 +2020-04-09 20:15:00,107.57,128.85299999999998,56.948,30.736 +2020-04-09 20:30:00,101.4,128.499,56.948,30.736 +2020-04-09 20:45:00,106.29,127.67299999999999,56.948,30.736 +2020-04-09 21:00:00,102.08,121.351,52.157,30.736 +2020-04-09 21:15:00,96.68,119.946,52.157,30.736 +2020-04-09 21:30:00,90.04,120.15100000000001,52.157,30.736 +2020-04-09 21:45:00,89.87,119.18700000000001,52.157,30.736 +2020-04-09 22:00:00,90.47,112.99700000000001,47.483000000000004,30.736 +2020-04-09 22:15:00,88.93,110.225,47.483000000000004,30.736 +2020-04-09 22:30:00,83.44,97.421,47.483000000000004,30.736 +2020-04-09 22:45:00,80.92,90.204,47.483000000000004,30.736 +2020-04-09 23:00:00,64.51,81.181,41.978,30.736 +2020-04-09 23:15:00,64.07,80.52199999999999,41.978,30.736 +2020-04-09 23:30:00,61.31,79.64699999999999,41.978,30.736 +2020-04-09 23:45:00,61.8,80.60300000000001,41.978,30.736 +2020-04-10 00:00:00,59.64,74.03399999999999,30.72,30.618000000000002 +2020-04-10 00:15:00,60.38,70.993,30.72,30.618000000000002 +2020-04-10 00:30:00,56.32,69.40899999999999,30.72,30.618000000000002 +2020-04-10 00:45:00,59.16,68.405,30.72,30.618000000000002 +2020-04-10 01:00:00,57.38,69.718,26.553,30.618000000000002 +2020-04-10 01:15:00,57.59,69.342,26.553,30.618000000000002 +2020-04-10 01:30:00,57.08,67.82300000000001,26.553,30.618000000000002 +2020-04-10 01:45:00,58.03,67.472,26.553,30.618000000000002 +2020-04-10 02:00:00,53.88,69.205,22.712,30.618000000000002 +2020-04-10 02:15:00,57.6,67.71,22.712,30.618000000000002 +2020-04-10 02:30:00,54.83,70.5,22.712,30.618000000000002 +2020-04-10 02:45:00,57.8,71.417,22.712,30.618000000000002 +2020-04-10 03:00:00,58.43,75.032,20.511999999999997,30.618000000000002 +2020-04-10 03:15:00,56.11,75.366,20.511999999999997,30.618000000000002 +2020-04-10 03:30:00,59.21,74.673,20.511999999999997,30.618000000000002 +2020-04-10 03:45:00,59.68,76.515,20.511999999999997,30.618000000000002 +2020-04-10 04:00:00,61.34,85.525,19.98,30.618000000000002 +2020-04-10 04:15:00,58.56,94.279,19.98,30.618000000000002 +2020-04-10 04:30:00,61.08,93.85799999999999,19.98,30.618000000000002 +2020-04-10 04:45:00,61.82,94.49600000000001,19.98,30.618000000000002 +2020-04-10 05:00:00,61.78,112.44,22.715,30.618000000000002 +2020-04-10 05:15:00,63.19,127.439,22.715,30.618000000000002 +2020-04-10 05:30:00,63.03,118.79899999999999,22.715,30.618000000000002 +2020-04-10 05:45:00,62.57,114.056,22.715,30.618000000000002 +2020-04-10 06:00:00,63.5,131.666,22.576999999999998,30.618000000000002 +2020-04-10 06:15:00,64.36,150.401,22.576999999999998,30.618000000000002 +2020-04-10 06:30:00,66.27,141.877,22.576999999999998,30.618000000000002 +2020-04-10 06:45:00,66.56,134.958,22.576999999999998,30.618000000000002 +2020-04-10 07:00:00,72.0,135.668,23.541999999999998,30.618000000000002 +2020-04-10 07:15:00,72.94,134.844,23.541999999999998,30.618000000000002 +2020-04-10 07:30:00,72.56,134.6,23.541999999999998,30.618000000000002 +2020-04-10 07:45:00,72.68,133.276,23.541999999999998,30.618000000000002 +2020-04-10 08:00:00,73.27,136.041,23.895,30.618000000000002 +2020-04-10 08:15:00,73.33,137.007,23.895,30.618000000000002 +2020-04-10 08:30:00,72.0,135.178,23.895,30.618000000000002 +2020-04-10 08:45:00,71.59,134.93200000000002,23.895,30.618000000000002 +2020-04-10 09:00:00,63.96,130.424,24.239,30.618000000000002 +2020-04-10 09:15:00,70.6,130.297,24.239,30.618000000000002 +2020-04-10 09:30:00,70.79,132.411,24.239,30.618000000000002 +2020-04-10 09:45:00,73.79,132.059,24.239,30.618000000000002 +2020-04-10 10:00:00,71.11,129.239,21.985,30.618000000000002 +2020-04-10 10:15:00,71.88,130.02,21.985,30.618000000000002 +2020-04-10 10:30:00,72.52,128.593,21.985,30.618000000000002 +2020-04-10 10:45:00,68.48,128.07,21.985,30.618000000000002 +2020-04-10 11:00:00,65.66,122.911,22.093000000000004,30.618000000000002 +2020-04-10 11:15:00,62.4,122.189,22.093000000000004,30.618000000000002 +2020-04-10 11:30:00,60.95,123.34700000000001,22.093000000000004,30.618000000000002 +2020-04-10 11:45:00,61.95,124.551,22.093000000000004,30.618000000000002 +2020-04-10 12:00:00,55.24,120.56299999999999,19.041,30.618000000000002 +2020-04-10 12:15:00,54.22,121.271,19.041,30.618000000000002 +2020-04-10 12:30:00,54.57,119.821,19.041,30.618000000000002 +2020-04-10 12:45:00,58.91,119.413,19.041,30.618000000000002 +2020-04-10 13:00:00,55.07,119.819,12.672,30.618000000000002 +2020-04-10 13:15:00,56.02,119.266,12.672,30.618000000000002 +2020-04-10 13:30:00,52.03,116.676,12.672,30.618000000000002 +2020-04-10 13:45:00,51.25,115.288,12.672,30.618000000000002 +2020-04-10 14:00:00,51.01,117.75200000000001,10.321,30.618000000000002 +2020-04-10 14:15:00,54.91,116.723,10.321,30.618000000000002 +2020-04-10 14:30:00,54.25,116.304,10.321,30.618000000000002 +2020-04-10 14:45:00,50.76,116.64299999999999,10.321,30.618000000000002 +2020-04-10 15:00:00,52.9,116.525,13.478,30.618000000000002 +2020-04-10 15:15:00,55.88,115.546,13.478,30.618000000000002 +2020-04-10 15:30:00,52.88,114.59100000000001,13.478,30.618000000000002 +2020-04-10 15:45:00,57.97,114.529,13.478,30.618000000000002 +2020-04-10 16:00:00,62.6,115.57799999999999,17.623,30.618000000000002 +2020-04-10 16:15:00,64.61,116.676,17.623,30.618000000000002 +2020-04-10 16:30:00,67.2,117.40700000000001,17.623,30.618000000000002 +2020-04-10 16:45:00,69.59,115.771,17.623,30.618000000000002 +2020-04-10 17:00:00,76.4,116.23299999999999,22.64,30.618000000000002 +2020-04-10 17:15:00,75.58,118.92200000000001,22.64,30.618000000000002 +2020-04-10 17:30:00,77.36,121.331,22.64,30.618000000000002 +2020-04-10 17:45:00,75.89,123.815,22.64,30.618000000000002 +2020-04-10 18:00:00,81.02,127.62,29.147,30.618000000000002 +2020-04-10 18:15:00,79.22,131.172,29.147,30.618000000000002 +2020-04-10 18:30:00,80.59,129.843,29.147,30.618000000000002 +2020-04-10 18:45:00,80.5,132.89600000000002,29.147,30.618000000000002 +2020-04-10 19:00:00,84.47,133.018,34.491,30.618000000000002 +2020-04-10 19:15:00,83.92,132.095,34.491,30.618000000000002 +2020-04-10 19:30:00,81.85,132.35,34.491,30.618000000000002 +2020-04-10 19:45:00,81.31,133.11700000000002,34.491,30.618000000000002 +2020-04-10 20:00:00,76.54,129.928,41.368,30.618000000000002 +2020-04-10 20:15:00,76.86,128.756,41.368,30.618000000000002 +2020-04-10 20:30:00,73.31,128.704,41.368,30.618000000000002 +2020-04-10 20:45:00,75.27,126.87899999999999,41.368,30.618000000000002 +2020-04-10 21:00:00,70.24,120.53399999999999,37.605,30.618000000000002 +2020-04-10 21:15:00,71.86,119.846,37.605,30.618000000000002 +2020-04-10 21:30:00,65.09,120.75200000000001,37.605,30.618000000000002 +2020-04-10 21:45:00,68.77,120.009,37.605,30.618000000000002 +2020-04-10 22:00:00,65.32,115.573,36.472,30.618000000000002 +2020-04-10 22:15:00,64.47,113.001,36.472,30.618000000000002 +2020-04-10 22:30:00,62.29,108.45200000000001,36.472,30.618000000000002 +2020-04-10 22:45:00,61.86,104.22399999999999,36.472,30.618000000000002 +2020-04-10 23:00:00,78.85,95.059,31.816,30.618000000000002 +2020-04-10 23:15:00,77.86,93.334,31.816,30.618000000000002 +2020-04-10 23:30:00,74.26,90.906,31.816,30.618000000000002 +2020-04-10 23:45:00,72.92,90.618,31.816,30.618000000000002 +2020-04-11 00:00:00,73.14,72.59,39.184,30.618000000000002 +2020-04-11 00:15:00,74.22,70.405,39.184,30.618000000000002 +2020-04-11 00:30:00,72.87,69.128,39.184,30.618000000000002 +2020-04-11 00:45:00,67.49,67.756,39.184,30.618000000000002 +2020-04-11 01:00:00,70.97,68.992,34.692,30.618000000000002 +2020-04-11 01:15:00,73.16,68.01899999999999,34.692,30.618000000000002 +2020-04-11 01:30:00,71.68,66.232,34.692,30.618000000000002 +2020-04-11 01:45:00,67.54,66.294,34.692,30.618000000000002 +2020-04-11 02:00:00,71.64,68.439,32.919000000000004,30.618000000000002 +2020-04-11 02:15:00,73.62,67.054,32.919000000000004,30.618000000000002 +2020-04-11 02:30:00,69.77,69.232,32.919000000000004,30.618000000000002 +2020-04-11 02:45:00,69.74,70.032,32.919000000000004,30.618000000000002 +2020-04-11 03:00:00,65.86,73.10600000000001,32.024,30.618000000000002 +2020-04-11 03:15:00,64.59,73.584,32.024,30.618000000000002 +2020-04-11 03:30:00,65.11,72.45,32.024,30.618000000000002 +2020-04-11 03:45:00,64.5,74.812,32.024,30.618000000000002 +2020-04-11 04:00:00,65.68,83.98200000000001,31.958000000000002,30.618000000000002 +2020-04-11 04:15:00,66.71,93.564,31.958000000000002,30.618000000000002 +2020-04-11 04:30:00,65.87,92.339,31.958000000000002,30.618000000000002 +2020-04-11 04:45:00,66.99,93.068,31.958000000000002,30.618000000000002 +2020-04-11 05:00:00,67.85,112.939,32.75,30.618000000000002 +2020-04-11 05:15:00,67.86,129.761,32.75,30.618000000000002 +2020-04-11 05:30:00,69.08,121.459,32.75,30.618000000000002 +2020-04-11 05:45:00,71.14,116.765,32.75,30.618000000000002 +2020-04-11 06:00:00,72.48,135.846,34.461999999999996,30.618000000000002 +2020-04-11 06:15:00,75.1,155.151,34.461999999999996,30.618000000000002 +2020-04-11 06:30:00,77.55,147.668,34.461999999999996,30.618000000000002 +2020-04-11 06:45:00,80.71,141.945,34.461999999999996,30.618000000000002 +2020-04-11 07:00:00,82.56,141.024,37.736,30.618000000000002 +2020-04-11 07:15:00,83.76,141.615,37.736,30.618000000000002 +2020-04-11 07:30:00,85.57,141.409,37.736,30.618000000000002 +2020-04-11 07:45:00,88.05,140.57399999999998,37.736,30.618000000000002 +2020-04-11 08:00:00,89.53,141.81,42.34,30.618000000000002 +2020-04-11 08:15:00,90.05,142.239,42.34,30.618000000000002 +2020-04-11 08:30:00,88.35,138.835,42.34,30.618000000000002 +2020-04-11 08:45:00,88.99,137.342,42.34,30.618000000000002 +2020-04-11 09:00:00,80.42,133.192,43.571999999999996,30.618000000000002 +2020-04-11 09:15:00,83.26,132.951,43.571999999999996,30.618000000000002 +2020-04-11 09:30:00,81.39,134.967,43.571999999999996,30.618000000000002 +2020-04-11 09:45:00,76.24,134.215,43.571999999999996,30.618000000000002 +2020-04-11 10:00:00,74.6,129.458,40.514,30.618000000000002 +2020-04-11 10:15:00,75.54,129.858,40.514,30.618000000000002 +2020-04-11 10:30:00,76.68,127.932,40.514,30.618000000000002 +2020-04-11 10:45:00,76.58,128.249,40.514,30.618000000000002 +2020-04-11 11:00:00,73.64,122.635,36.388000000000005,30.618000000000002 +2020-04-11 11:15:00,73.67,122.054,36.388000000000005,30.618000000000002 +2020-04-11 11:30:00,76.9,123.58200000000001,36.388000000000005,30.618000000000002 +2020-04-11 11:45:00,74.29,124.258,36.388000000000005,30.618000000000002 +2020-04-11 12:00:00,69.92,120.24700000000001,35.217,30.618000000000002 +2020-04-11 12:15:00,66.27,119.885,35.217,30.618000000000002 +2020-04-11 12:30:00,67.34,119.337,35.217,30.618000000000002 +2020-04-11 12:45:00,67.73,119.915,35.217,30.618000000000002 +2020-04-11 13:00:00,63.46,121.039,32.001999999999995,30.618000000000002 +2020-04-11 13:15:00,65.05,118.553,32.001999999999995,30.618000000000002 +2020-04-11 13:30:00,61.84,116.615,32.001999999999995,30.618000000000002 +2020-04-11 13:45:00,62.81,115.204,32.001999999999995,30.618000000000002 +2020-04-11 14:00:00,62.8,117.021,31.304000000000002,30.618000000000002 +2020-04-11 14:15:00,63.27,114.906,31.304000000000002,30.618000000000002 +2020-04-11 14:30:00,63.28,114.103,31.304000000000002,30.618000000000002 +2020-04-11 14:45:00,63.54,115.226,31.304000000000002,30.618000000000002 +2020-04-11 15:00:00,63.73,116.178,34.731,30.618000000000002 +2020-04-11 15:15:00,64.31,114.98899999999999,34.731,30.618000000000002 +2020-04-11 15:30:00,66.0,113.639,34.731,30.618000000000002 +2020-04-11 15:45:00,68.73,112.928,34.731,30.618000000000002 +2020-04-11 16:00:00,71.32,113.571,38.769,30.618000000000002 +2020-04-11 16:15:00,71.76,115.17,38.769,30.618000000000002 +2020-04-11 16:30:00,74.69,115.245,38.769,30.618000000000002 +2020-04-11 16:45:00,79.72,113.42200000000001,38.769,30.618000000000002 +2020-04-11 17:00:00,82.56,113.86,44.928000000000004,30.618000000000002 +2020-04-11 17:15:00,80.55,116.00200000000001,44.928000000000004,30.618000000000002 +2020-04-11 17:30:00,82.25,117.81700000000001,44.928000000000004,30.618000000000002 +2020-04-11 17:45:00,84.04,118.53399999999999,44.928000000000004,30.618000000000002 +2020-04-11 18:00:00,86.25,122.461,47.786,30.618000000000002 +2020-04-11 18:15:00,84.53,125.369,47.786,30.618000000000002 +2020-04-11 18:30:00,84.88,125.551,47.786,30.618000000000002 +2020-04-11 18:45:00,85.38,127.315,47.786,30.618000000000002 +2020-04-11 19:00:00,86.2,126.56,47.463,30.618000000000002 +2020-04-11 19:15:00,85.71,125.825,47.463,30.618000000000002 +2020-04-11 19:30:00,86.11,126.334,47.463,30.618000000000002 +2020-04-11 19:45:00,84.4,126.412,47.463,30.618000000000002 +2020-04-11 20:00:00,81.75,123.16,43.735,30.618000000000002 +2020-04-11 20:15:00,80.33,121.42200000000001,43.735,30.618000000000002 +2020-04-11 20:30:00,77.95,120.17399999999999,43.735,30.618000000000002 +2020-04-11 20:45:00,76.55,120.03200000000001,43.735,30.618000000000002 +2020-04-11 21:00:00,71.3,115.48700000000001,40.346,30.618000000000002 +2020-04-11 21:15:00,70.85,115.375,40.346,30.618000000000002 +2020-04-11 21:30:00,68.16,116.42200000000001,40.346,30.618000000000002 +2020-04-11 21:45:00,67.66,115.43,40.346,30.618000000000002 +2020-04-11 22:00:00,63.94,110.7,39.323,30.618000000000002 +2020-04-11 22:15:00,64.14,109.53200000000001,39.323,30.618000000000002 +2020-04-11 22:30:00,61.85,107.27600000000001,39.323,30.618000000000002 +2020-04-11 22:45:00,61.25,104.26299999999999,39.323,30.618000000000002 +2020-04-11 23:00:00,60.08,97.06,33.716,30.618000000000002 +2020-04-11 23:15:00,58.06,93.493,33.716,30.618000000000002 +2020-04-11 23:30:00,55.51,90.814,33.716,30.618000000000002 +2020-04-11 23:45:00,56.45,89.876,33.716,30.618000000000002 +2020-04-12 00:00:00,54.53,73.203,30.72,30.618000000000002 +2020-04-12 00:15:00,52.88,70.192,30.72,30.618000000000002 +2020-04-12 00:30:00,52.07,68.59,30.72,30.618000000000002 +2020-04-12 00:45:00,53.98,67.596,30.72,30.618000000000002 +2020-04-12 01:00:00,48.89,68.872,26.553,30.618000000000002 +2020-04-12 01:15:00,53.69,68.45100000000001,26.553,30.618000000000002 +2020-04-12 01:30:00,51.28,66.889,26.553,30.618000000000002 +2020-04-12 01:45:00,53.95,66.54899999999999,26.553,30.618000000000002 +2020-04-12 02:00:00,50.72,68.258,22.712,30.618000000000002 +2020-04-12 02:15:00,54.24,66.73,22.712,30.618000000000002 +2020-04-12 02:30:00,51.64,69.561,22.712,30.618000000000002 +2020-04-12 02:45:00,51.01,70.486,22.712,30.618000000000002 +2020-04-12 03:00:00,50.48,74.13600000000001,20.511999999999997,30.618000000000002 +2020-04-12 03:15:00,51.91,74.418,20.511999999999997,30.618000000000002 +2020-04-12 03:30:00,51.98,73.71600000000001,20.511999999999997,30.618000000000002 +2020-04-12 03:45:00,52.83,75.598,20.511999999999997,30.618000000000002 +2020-04-12 04:00:00,51.89,84.565,19.98,30.618000000000002 +2020-04-12 04:15:00,52.74,93.25200000000001,19.98,30.618000000000002 +2020-04-12 04:30:00,52.54,92.84100000000001,19.98,30.618000000000002 +2020-04-12 04:45:00,54.38,93.456,19.98,30.618000000000002 +2020-04-12 05:00:00,55.46,111.241,22.715,30.618000000000002 +2020-04-12 05:15:00,53.48,126.101,22.715,30.618000000000002 +2020-04-12 05:30:00,55.9,117.47200000000001,22.715,30.618000000000002 +2020-04-12 05:45:00,53.71,112.805,22.715,30.618000000000002 +2020-04-12 06:00:00,55.53,130.452,22.576999999999998,30.618000000000002 +2020-04-12 06:15:00,59.67,149.158,22.576999999999998,30.618000000000002 +2020-04-12 06:30:00,62.15,140.576,22.576999999999998,30.618000000000002 +2020-04-12 06:45:00,61.32,133.628,22.576999999999998,30.618000000000002 +2020-04-12 07:00:00,67.05,134.34799999999998,23.541999999999998,30.618000000000002 +2020-04-12 07:15:00,67.97,133.483,23.541999999999998,30.618000000000002 +2020-04-12 07:30:00,69.68,133.157,23.541999999999998,30.618000000000002 +2020-04-12 07:45:00,69.61,131.813,23.541999999999998,30.618000000000002 +2020-04-12 08:00:00,74.12,134.54,27.568,30.618000000000002 +2020-04-12 08:15:00,75.03,135.55200000000002,27.568,30.618000000000002 +2020-04-12 08:30:00,74.98,133.65,27.568,30.618000000000002 +2020-04-12 08:45:00,73.91,133.46200000000002,27.568,30.618000000000002 +2020-04-12 09:00:00,76.48,128.969,27.965,30.618000000000002 +2020-04-12 09:15:00,73.74,128.851,27.965,30.618000000000002 +2020-04-12 09:30:00,75.72,130.999,27.965,30.618000000000002 +2020-04-12 09:45:00,74.12,130.707,27.965,30.618000000000002 +2020-04-12 10:00:00,72.15,127.902,25.365,30.618000000000002 +2020-04-12 10:15:00,77.67,128.786,25.365,30.618000000000002 +2020-04-12 10:30:00,77.66,127.40899999999999,25.365,30.618000000000002 +2020-04-12 10:45:00,82.07,126.928,25.365,30.618000000000002 +2020-04-12 11:00:00,72.99,121.75399999999999,25.489,30.618000000000002 +2020-04-12 11:15:00,75.01,121.08,25.489,30.618000000000002 +2020-04-12 11:30:00,71.52,122.244,25.489,30.618000000000002 +2020-04-12 11:45:00,69.58,123.48700000000001,25.489,30.618000000000002 +2020-04-12 12:00:00,68.21,119.55,21.968000000000004,30.618000000000002 +2020-04-12 12:15:00,67.52,120.275,21.968000000000004,30.618000000000002 +2020-04-12 12:30:00,66.81,118.738,21.968000000000004,30.618000000000002 +2020-04-12 12:45:00,67.38,118.333,21.968000000000004,30.618000000000002 +2020-04-12 13:00:00,60.59,118.82600000000001,14.62,30.618000000000002 +2020-04-12 13:15:00,59.39,118.251,14.62,30.618000000000002 +2020-04-12 13:30:00,58.18,115.65799999999999,14.62,30.618000000000002 +2020-04-12 13:45:00,60.55,114.27600000000001,14.62,30.618000000000002 +2020-04-12 14:00:00,59.16,116.87700000000001,11.908,30.618000000000002 +2020-04-12 14:15:00,60.01,115.804,11.908,30.618000000000002 +2020-04-12 14:30:00,56.92,115.295,11.908,30.618000000000002 +2020-04-12 14:45:00,58.07,115.64200000000001,11.908,30.618000000000002 +2020-04-12 15:00:00,61.44,115.57700000000001,15.55,30.618000000000002 +2020-04-12 15:15:00,62.16,114.54899999999999,15.55,30.618000000000002 +2020-04-12 15:30:00,62.09,113.494,15.55,30.618000000000002 +2020-04-12 15:45:00,63.69,113.395,15.55,30.618000000000002 +2020-04-12 16:00:00,63.44,114.516,20.332,30.618000000000002 +2020-04-12 16:15:00,65.92,115.56,20.332,30.618000000000002 +2020-04-12 16:30:00,69.07,116.295,20.332,30.618000000000002 +2020-04-12 16:45:00,69.42,114.53200000000001,20.332,30.618000000000002 +2020-04-12 17:00:00,77.2,115.094,26.121,30.618000000000002 +2020-04-12 17:15:00,75.08,117.742,26.121,30.618000000000002 +2020-04-12 17:30:00,76.8,120.152,26.121,30.618000000000002 +2020-04-12 17:45:00,76.23,122.604,26.121,30.618000000000002 +2020-04-12 18:00:00,79.58,126.429,33.626999999999995,30.618000000000002 +2020-04-12 18:15:00,77.87,130.05700000000002,33.626999999999995,30.618000000000002 +2020-04-12 18:30:00,78.53,128.704,33.626999999999995,30.618000000000002 +2020-04-12 18:45:00,80.41,131.775,33.626999999999995,30.618000000000002 +2020-04-12 19:00:00,79.37,131.862,39.793,30.618000000000002 +2020-04-12 19:15:00,79.52,130.96200000000002,39.793,30.618000000000002 +2020-04-12 19:30:00,78.47,131.251,39.793,30.618000000000002 +2020-04-12 19:45:00,77.25,132.084,39.793,30.618000000000002 +2020-04-12 20:00:00,74.47,128.839,41.368,30.618000000000002 +2020-04-12 20:15:00,73.54,127.68799999999999,41.368,30.618000000000002 +2020-04-12 20:30:00,73.03,127.709,41.368,30.618000000000002 +2020-04-12 20:45:00,72.54,125.928,41.368,30.618000000000002 +2020-04-12 21:00:00,69.45,119.583,37.605,30.618000000000002 +2020-04-12 21:15:00,69.2,118.919,37.605,30.618000000000002 +2020-04-12 21:30:00,64.59,119.805,37.605,30.618000000000002 +2020-04-12 21:45:00,67.2,119.119,37.605,30.618000000000002 +2020-04-12 22:00:00,62.87,114.694,36.472,30.618000000000002 +2020-04-12 22:15:00,62.97,112.175,36.472,30.618000000000002 +2020-04-12 22:30:00,58.12,107.554,36.472,30.618000000000002 +2020-04-12 22:45:00,60.08,103.31299999999999,36.472,30.618000000000002 +2020-04-12 23:00:00,57.22,94.10700000000001,31.816,30.618000000000002 +2020-04-12 23:15:00,57.26,92.449,31.816,30.618000000000002 +2020-04-12 23:30:00,53.07,90.01700000000001,31.816,30.618000000000002 +2020-04-12 23:45:00,55.28,89.759,31.816,30.618000000000002 +2020-04-13 00:00:00,49.89,72.78699999999999,30.72,30.618000000000002 +2020-04-13 00:15:00,53.6,69.791,30.72,30.618000000000002 +2020-04-13 00:30:00,50.52,68.178,30.72,30.618000000000002 +2020-04-13 00:45:00,53.11,67.191,30.72,30.618000000000002 +2020-04-13 01:00:00,47.65,68.449,26.553,30.618000000000002 +2020-04-13 01:15:00,51.91,68.006,26.553,30.618000000000002 +2020-04-13 01:30:00,48.7,66.422,26.553,30.618000000000002 +2020-04-13 01:45:00,51.4,66.087,26.553,30.618000000000002 +2020-04-13 02:00:00,49.72,67.783,22.712,30.618000000000002 +2020-04-13 02:15:00,48.31,66.24,22.712,30.618000000000002 +2020-04-13 02:30:00,50.79,69.09100000000001,22.712,30.618000000000002 +2020-04-13 02:45:00,48.57,70.021,22.712,30.618000000000002 +2020-04-13 03:00:00,51.31,73.689,20.511999999999997,30.618000000000002 +2020-04-13 03:15:00,52.15,73.943,20.511999999999997,30.618000000000002 +2020-04-13 03:30:00,51.38,73.236,20.511999999999997,30.618000000000002 +2020-04-13 03:45:00,54.02,75.139,20.511999999999997,30.618000000000002 +2020-04-13 04:00:00,54.39,84.085,19.98,30.618000000000002 +2020-04-13 04:15:00,55.93,92.73899999999999,19.98,30.618000000000002 +2020-04-13 04:30:00,56.7,92.33200000000001,19.98,30.618000000000002 +2020-04-13 04:45:00,57.44,92.93700000000001,19.98,30.618000000000002 +2020-04-13 05:00:00,58.82,110.641,22.715,30.618000000000002 +2020-04-13 05:15:00,59.57,125.43,22.715,30.618000000000002 +2020-04-13 05:30:00,57.7,116.80799999999999,22.715,30.618000000000002 +2020-04-13 05:45:00,55.37,112.177,22.715,30.618000000000002 +2020-04-13 06:00:00,58.84,129.845,22.576999999999998,30.618000000000002 +2020-04-13 06:15:00,57.13,148.534,22.576999999999998,30.618000000000002 +2020-04-13 06:30:00,60.7,139.924,22.576999999999998,30.618000000000002 +2020-04-13 06:45:00,62.85,132.963,22.576999999999998,30.618000000000002 +2020-04-13 07:00:00,68.7,133.687,23.541999999999998,30.618000000000002 +2020-04-13 07:15:00,68.91,132.80200000000002,23.541999999999998,30.618000000000002 +2020-04-13 07:30:00,66.28,132.436,23.541999999999998,30.618000000000002 +2020-04-13 07:45:00,71.72,131.08,23.541999999999998,30.618000000000002 +2020-04-13 08:00:00,70.13,133.79,23.895,30.618000000000002 +2020-04-13 08:15:00,72.83,134.825,23.895,30.618000000000002 +2020-04-13 08:30:00,69.75,132.886,23.895,30.618000000000002 +2020-04-13 08:45:00,71.31,132.72899999999998,23.895,30.618000000000002 +2020-04-13 09:00:00,67.16,128.24200000000002,24.239,30.618000000000002 +2020-04-13 09:15:00,66.12,128.127,24.239,30.618000000000002 +2020-04-13 09:30:00,64.1,130.295,24.239,30.618000000000002 +2020-04-13 09:45:00,66.42,130.032,24.239,30.618000000000002 +2020-04-13 10:00:00,67.4,127.23700000000001,21.985,30.618000000000002 +2020-04-13 10:15:00,66.6,128.17,21.985,30.618000000000002 +2020-04-13 10:30:00,70.58,126.81700000000001,21.985,30.618000000000002 +2020-04-13 10:45:00,69.07,126.359,21.985,30.618000000000002 +2020-04-13 11:00:00,63.2,121.177,22.093000000000004,30.618000000000002 +2020-04-13 11:15:00,62.5,120.52799999999999,22.093000000000004,30.618000000000002 +2020-04-13 11:30:00,63.4,121.693,22.093000000000004,30.618000000000002 +2020-04-13 11:45:00,71.72,122.95700000000001,22.093000000000004,30.618000000000002 +2020-04-13 12:00:00,63.81,119.045,19.041,30.618000000000002 +2020-04-13 12:15:00,54.06,119.77799999999999,19.041,30.618000000000002 +2020-04-13 12:30:00,54.23,118.197,19.041,30.618000000000002 +2020-04-13 12:45:00,56.47,117.792,19.041,30.618000000000002 +2020-04-13 13:00:00,56.38,118.329,12.672,30.618000000000002 +2020-04-13 13:15:00,51.32,117.744,12.672,30.618000000000002 +2020-04-13 13:30:00,50.09,115.15,12.672,30.618000000000002 +2020-04-13 13:45:00,52.76,113.772,12.672,30.618000000000002 +2020-04-13 14:00:00,50.0,116.441,10.321,30.618000000000002 +2020-04-13 14:15:00,47.98,115.345,10.321,30.618000000000002 +2020-04-13 14:30:00,46.88,114.791,10.321,30.618000000000002 +2020-04-13 14:45:00,51.58,115.14200000000001,10.321,30.618000000000002 +2020-04-13 15:00:00,52.85,115.103,13.478,30.618000000000002 +2020-04-13 15:15:00,53.15,114.051,13.478,30.618000000000002 +2020-04-13 15:30:00,51.27,112.945,13.478,30.618000000000002 +2020-04-13 15:45:00,52.98,112.829,13.478,30.618000000000002 +2020-04-13 16:00:00,57.21,113.985,17.623,30.618000000000002 +2020-04-13 16:15:00,61.89,115.00399999999999,17.623,30.618000000000002 +2020-04-13 16:30:00,63.48,115.741,17.623,30.618000000000002 +2020-04-13 16:45:00,65.1,113.913,17.623,30.618000000000002 +2020-04-13 17:00:00,72.1,114.525,22.64,30.618000000000002 +2020-04-13 17:15:00,73.56,117.152,22.64,30.618000000000002 +2020-04-13 17:30:00,76.1,119.56299999999999,22.64,30.618000000000002 +2020-04-13 17:45:00,77.85,121.99799999999999,22.64,30.618000000000002 +2020-04-13 18:00:00,78.34,125.833,29.147,30.618000000000002 +2020-04-13 18:15:00,78.86,129.499,29.147,30.618000000000002 +2020-04-13 18:30:00,81.38,128.134,29.147,30.618000000000002 +2020-04-13 18:45:00,82.16,131.215,29.147,30.618000000000002 +2020-04-13 19:00:00,86.48,131.284,34.491,30.618000000000002 +2020-04-13 19:15:00,87.39,130.394,34.491,30.618000000000002 +2020-04-13 19:30:00,85.34,130.702,34.491,30.618000000000002 +2020-04-13 19:45:00,84.46,131.567,34.491,30.618000000000002 +2020-04-13 20:00:00,81.57,128.29399999999998,41.368,30.618000000000002 +2020-04-13 20:15:00,80.87,127.15299999999999,41.368,30.618000000000002 +2020-04-13 20:30:00,77.82,127.21,41.368,30.618000000000002 +2020-04-13 20:45:00,80.82,125.45200000000001,41.368,30.618000000000002 +2020-04-13 21:00:00,80.42,119.10700000000001,37.605,30.618000000000002 +2020-04-13 21:15:00,79.52,118.454,37.605,30.618000000000002 +2020-04-13 21:30:00,74.22,119.331,37.605,30.618000000000002 +2020-04-13 21:45:00,76.26,118.67399999999999,37.605,30.618000000000002 +2020-04-13 22:00:00,68.73,114.255,36.472,30.618000000000002 +2020-04-13 22:15:00,70.32,111.76100000000001,36.472,30.618000000000002 +2020-04-13 22:30:00,68.58,107.104,36.472,30.618000000000002 +2020-04-13 22:45:00,70.36,102.85700000000001,36.472,30.618000000000002 +2020-04-13 23:00:00,81.66,93.63,31.816,30.618000000000002 +2020-04-13 23:15:00,81.85,92.005,31.816,30.618000000000002 +2020-04-13 23:30:00,75.45,89.572,31.816,30.618000000000002 +2020-04-13 23:45:00,76.28,89.32799999999999,31.816,30.618000000000002 +2020-04-14 00:00:00,77.57,74.116,39.857,30.736 +2020-04-14 00:15:00,78.7,74.781,39.857,30.736 +2020-04-14 00:30:00,77.84,72.835,39.857,30.736 +2020-04-14 00:45:00,74.5,71.13600000000001,39.857,30.736 +2020-04-14 01:00:00,78.14,72.16,37.233000000000004,30.736 +2020-04-14 01:15:00,79.78,71.166,37.233000000000004,30.736 +2020-04-14 01:30:00,79.36,69.831,37.233000000000004,30.736 +2020-04-14 01:45:00,76.71,69.417,37.233000000000004,30.736 +2020-04-14 02:00:00,79.63,71.053,35.856,30.736 +2020-04-14 02:15:00,80.53,70.399,35.856,30.736 +2020-04-14 02:30:00,77.64,72.979,35.856,30.736 +2020-04-14 02:45:00,77.36,73.646,35.856,30.736 +2020-04-14 03:00:00,80.42,77.32,34.766999999999996,30.736 +2020-04-14 03:15:00,77.51,78.97,34.766999999999996,30.736 +2020-04-14 03:30:00,78.75,78.759,34.766999999999996,30.736 +2020-04-14 03:45:00,85.63,79.569,34.766999999999996,30.736 +2020-04-14 04:00:00,90.63,92.03,35.468,30.736 +2020-04-14 04:15:00,89.42,104.62,35.468,30.736 +2020-04-14 04:30:00,89.05,105.006,35.468,30.736 +2020-04-14 04:45:00,91.92,106.92,35.468,30.736 +2020-04-14 05:00:00,100.81,141.918,40.399,30.736 +2020-04-14 05:15:00,102.84,174.856,40.399,30.736 +2020-04-14 05:30:00,105.47,164.701,40.399,30.736 +2020-04-14 05:45:00,107.4,154.543,40.399,30.736 +2020-04-14 06:00:00,112.59,155.791,54.105,30.736 +2020-04-14 06:15:00,113.37,161.384,54.105,30.736 +2020-04-14 06:30:00,116.15,159.305,54.105,30.736 +2020-04-14 06:45:00,117.75,159.95,54.105,30.736 +2020-04-14 07:00:00,122.9,162.8,63.083,30.736 +2020-04-14 07:15:00,119.48,163.681,63.083,30.736 +2020-04-14 07:30:00,122.96,161.981,63.083,30.736 +2020-04-14 07:45:00,119.45,158.767,63.083,30.736 +2020-04-14 08:00:00,120.93,157.66,57.254,30.736 +2020-04-14 08:15:00,123.65,155.789,57.254,30.736 +2020-04-14 08:30:00,125.42,150.616,57.254,30.736 +2020-04-14 08:45:00,126.12,147.766,57.254,30.736 +2020-04-14 09:00:00,121.6,141.899,51.395,30.736 +2020-04-14 09:15:00,123.45,139.561,51.395,30.736 +2020-04-14 09:30:00,123.95,141.412,51.395,30.736 +2020-04-14 09:45:00,124.96,140.746,51.395,30.736 +2020-04-14 10:00:00,122.16,136.626,48.201,30.736 +2020-04-14 10:15:00,123.54,136.503,48.201,30.736 +2020-04-14 10:30:00,121.52,134.487,48.201,30.736 +2020-04-14 10:45:00,120.31,134.483,48.201,30.736 +2020-04-14 11:00:00,116.44,128.762,46.133,30.736 +2020-04-14 11:15:00,111.12,129.491,46.133,30.736 +2020-04-14 11:30:00,108.43,130.6,46.133,30.736 +2020-04-14 11:45:00,109.42,132.118,46.133,30.736 +2020-04-14 12:00:00,104.93,127.803,44.243,30.736 +2020-04-14 12:15:00,109.73,128.44299999999998,44.243,30.736 +2020-04-14 12:30:00,108.63,127.42299999999999,44.243,30.736 +2020-04-14 12:45:00,105.87,128.214,44.243,30.736 +2020-04-14 13:00:00,99.12,129.259,45.042,30.736 +2020-04-14 13:15:00,98.79,127.802,45.042,30.736 +2020-04-14 13:30:00,104.88,125.742,45.042,30.736 +2020-04-14 13:45:00,105.06,124.499,45.042,30.736 +2020-04-14 14:00:00,104.5,126.81,44.062,30.736 +2020-04-14 14:15:00,101.57,125.516,44.062,30.736 +2020-04-14 14:30:00,96.19,125.055,44.062,30.736 +2020-04-14 14:45:00,96.11,125.898,44.062,30.736 +2020-04-14 15:00:00,107.14,126.646,46.461999999999996,30.736 +2020-04-14 15:15:00,106.37,124.911,46.461999999999996,30.736 +2020-04-14 15:30:00,104.16,123.564,46.461999999999996,30.736 +2020-04-14 15:45:00,104.55,122.74700000000001,46.461999999999996,30.736 +2020-04-14 16:00:00,107.58,124.26,48.802,30.736 +2020-04-14 16:15:00,113.1,125.12299999999999,48.802,30.736 +2020-04-14 16:30:00,111.12,125.152,48.802,30.736 +2020-04-14 16:45:00,110.6,122.97399999999999,48.802,30.736 +2020-04-14 17:00:00,112.54,123.29,55.672,30.736 +2020-04-14 17:15:00,115.47,125.654,55.672,30.736 +2020-04-14 17:30:00,119.06,127.742,55.672,30.736 +2020-04-14 17:45:00,115.7,128.691,55.672,30.736 +2020-04-14 18:00:00,112.52,131.662,57.006,30.736 +2020-04-14 18:15:00,117.42,133.36700000000002,57.006,30.736 +2020-04-14 18:30:00,119.58,131.71200000000002,57.006,30.736 +2020-04-14 18:45:00,117.04,137.30700000000002,57.006,30.736 +2020-04-14 19:00:00,110.56,135.55100000000002,57.148,30.736 +2020-04-14 19:15:00,107.17,134.471,57.148,30.736 +2020-04-14 19:30:00,115.88,134.385,57.148,30.736 +2020-04-14 19:45:00,113.92,134.611,57.148,30.736 +2020-04-14 20:00:00,110.45,129.387,61.895,30.736 +2020-04-14 20:15:00,109.71,126.18299999999999,61.895,30.736 +2020-04-14 20:30:00,100.85,126.009,61.895,30.736 +2020-04-14 20:45:00,108.91,125.295,61.895,30.736 +2020-04-14 21:00:00,103.46,118.976,54.78,30.736 +2020-04-14 21:15:00,102.44,117.626,54.78,30.736 +2020-04-14 21:30:00,93.5,117.785,54.78,30.736 +2020-04-14 21:45:00,87.39,116.962,54.78,30.736 +2020-04-14 22:00:00,87.83,110.8,50.76,30.736 +2020-04-14 22:15:00,90.13,108.15799999999999,50.76,30.736 +2020-04-14 22:30:00,87.92,95.17399999999999,50.76,30.736 +2020-04-14 22:45:00,84.46,87.926,50.76,30.736 +2020-04-14 23:00:00,74.36,78.79899999999999,44.162,30.736 +2020-04-14 23:15:00,76.0,78.307,44.162,30.736 +2020-04-14 23:30:00,74.94,77.425,44.162,30.736 +2020-04-14 23:45:00,73.87,78.453,44.162,30.736 +2020-04-15 00:00:00,70.64,73.699,39.061,30.736 +2020-04-15 00:15:00,72.45,74.37899999999999,39.061,30.736 +2020-04-15 00:30:00,71.26,72.425,39.061,30.736 +2020-04-15 00:45:00,73.44,70.73100000000001,39.061,30.736 +2020-04-15 01:00:00,71.82,71.737,35.795,30.736 +2020-04-15 01:15:00,71.14,70.721,35.795,30.736 +2020-04-15 01:30:00,71.08,69.36399999999999,35.795,30.736 +2020-04-15 01:45:00,72.01,68.956,35.795,30.736 +2020-04-15 02:00:00,70.74,70.58,33.316,30.736 +2020-04-15 02:15:00,70.1,69.90899999999999,33.316,30.736 +2020-04-15 02:30:00,72.27,72.509,33.316,30.736 +2020-04-15 02:45:00,78.4,73.181,33.316,30.736 +2020-04-15 03:00:00,80.61,76.872,32.803000000000004,30.736 +2020-04-15 03:15:00,83.75,78.49600000000001,32.803000000000004,30.736 +2020-04-15 03:30:00,82.05,78.28,32.803000000000004,30.736 +2020-04-15 03:45:00,86.07,79.111,32.803000000000004,30.736 +2020-04-15 04:00:00,88.31,91.54899999999999,34.235,30.736 +2020-04-15 04:15:00,89.49,104.10700000000001,34.235,30.736 +2020-04-15 04:30:00,87.96,104.49600000000001,34.235,30.736 +2020-04-15 04:45:00,93.45,106.40100000000001,34.235,30.736 +2020-04-15 05:00:00,98.18,141.31799999999998,38.65,30.736 +2020-04-15 05:15:00,101.94,174.18400000000003,38.65,30.736 +2020-04-15 05:30:00,105.86,164.03599999999997,38.65,30.736 +2020-04-15 05:45:00,109.69,153.917,38.65,30.736 +2020-04-15 06:00:00,114.65,155.183,54.951,30.736 +2020-04-15 06:15:00,114.16,160.76,54.951,30.736 +2020-04-15 06:30:00,116.39,158.65200000000002,54.951,30.736 +2020-04-15 06:45:00,116.53,159.284,54.951,30.736 +2020-04-15 07:00:00,117.39,162.137,67.328,30.736 +2020-04-15 07:15:00,117.4,162.999,67.328,30.736 +2020-04-15 07:30:00,115.55,161.259,67.328,30.736 +2020-04-15 07:45:00,112.28,158.037,67.328,30.736 +2020-04-15 08:00:00,111.52,156.912,60.23,30.736 +2020-04-15 08:15:00,111.85,155.064,60.23,30.736 +2020-04-15 08:30:00,112.04,149.856,60.23,30.736 +2020-04-15 08:45:00,110.65,147.036,60.23,30.736 +2020-04-15 09:00:00,106.92,141.17600000000002,56.845,30.736 +2020-04-15 09:15:00,107.34,138.842,56.845,30.736 +2020-04-15 09:30:00,106.27,140.71,56.845,30.736 +2020-04-15 09:45:00,106.1,140.07299999999998,56.845,30.736 +2020-04-15 10:00:00,105.75,135.961,53.832,30.736 +2020-04-15 10:15:00,106.27,135.888,53.832,30.736 +2020-04-15 10:30:00,106.67,133.898,53.832,30.736 +2020-04-15 10:45:00,105.02,133.914,53.832,30.736 +2020-04-15 11:00:00,103.38,128.187,53.225,30.736 +2020-04-15 11:15:00,103.27,128.941,53.225,30.736 +2020-04-15 11:30:00,104.28,130.05100000000002,53.225,30.736 +2020-04-15 11:45:00,102.15,131.589,53.225,30.736 +2020-04-15 12:00:00,100.29,127.3,50.676,30.736 +2020-04-15 12:15:00,98.13,127.947,50.676,30.736 +2020-04-15 12:30:00,104.5,126.884,50.676,30.736 +2020-04-15 12:45:00,105.18,127.676,50.676,30.736 +2020-04-15 13:00:00,101.11,128.764,50.646,30.736 +2020-04-15 13:15:00,100.65,127.29700000000001,50.646,30.736 +2020-04-15 13:30:00,102.83,125.236,50.646,30.736 +2020-04-15 13:45:00,103.75,123.99700000000001,50.646,30.736 +2020-04-15 14:00:00,104.03,126.375,50.786,30.736 +2020-04-15 14:15:00,97.55,125.059,50.786,30.736 +2020-04-15 14:30:00,97.21,124.553,50.786,30.736 +2020-04-15 14:45:00,102.82,125.4,50.786,30.736 +2020-04-15 15:00:00,103.26,126.17299999999999,51.535,30.736 +2020-04-15 15:15:00,100.4,124.414,51.535,30.736 +2020-04-15 15:30:00,99.53,123.01799999999999,51.535,30.736 +2020-04-15 15:45:00,104.12,122.182,51.535,30.736 +2020-04-15 16:00:00,104.01,123.73200000000001,53.157,30.736 +2020-04-15 16:15:00,108.96,124.56700000000001,53.157,30.736 +2020-04-15 16:30:00,107.57,124.6,53.157,30.736 +2020-04-15 16:45:00,115.32,122.35799999999999,53.157,30.736 +2020-04-15 17:00:00,116.1,122.72399999999999,57.793,30.736 +2020-04-15 17:15:00,113.72,125.06700000000001,57.793,30.736 +2020-04-15 17:30:00,111.49,127.154,57.793,30.736 +2020-04-15 17:45:00,115.74,128.088,57.793,30.736 +2020-04-15 18:00:00,119.64,131.067,59.872,30.736 +2020-04-15 18:15:00,116.25,132.809,59.872,30.736 +2020-04-15 18:30:00,111.49,131.142,59.872,30.736 +2020-04-15 18:45:00,111.02,136.745,59.872,30.736 +2020-04-15 19:00:00,115.27,134.97299999999998,60.17100000000001,30.736 +2020-04-15 19:15:00,112.47,133.905,60.17100000000001,30.736 +2020-04-15 19:30:00,112.99,133.835,60.17100000000001,30.736 +2020-04-15 19:45:00,112.24,134.093,60.17100000000001,30.736 +2020-04-15 20:00:00,110.1,128.842,65.015,30.736 +2020-04-15 20:15:00,111.51,125.648,65.015,30.736 +2020-04-15 20:30:00,106.08,125.51100000000001,65.015,30.736 +2020-04-15 20:45:00,109.31,124.819,65.015,30.736 +2020-04-15 21:00:00,104.34,118.501,57.805,30.736 +2020-04-15 21:15:00,101.95,117.163,57.805,30.736 +2020-04-15 21:30:00,93.9,117.31200000000001,57.805,30.736 +2020-04-15 21:45:00,86.0,116.51700000000001,57.805,30.736 +2020-04-15 22:00:00,78.84,110.359,52.115,30.736 +2020-04-15 22:15:00,87.14,107.744,52.115,30.736 +2020-04-15 22:30:00,85.74,94.72200000000001,52.115,30.736 +2020-04-15 22:45:00,86.73,87.46799999999999,52.115,30.736 +2020-04-15 23:00:00,77.18,78.321,42.871,30.736 +2020-04-15 23:15:00,76.65,77.863,42.871,30.736 +2020-04-15 23:30:00,76.51,76.979,42.871,30.736 +2020-04-15 23:45:00,79.0,78.02199999999999,42.871,30.736 +2020-04-16 00:00:00,76.59,66.062,39.203,30.736 +2020-04-16 00:15:00,76.53,66.53699999999999,39.203,30.736 +2020-04-16 00:30:00,74.45,64.943,39.203,30.736 +2020-04-16 00:45:00,72.38,63.372,39.203,30.736 +2020-04-16 01:00:00,73.63,63.707,37.118,30.736 +2020-04-16 01:15:00,77.98,63.019,37.118,30.736 +2020-04-16 01:30:00,75.57,61.729,37.118,30.736 +2020-04-16 01:45:00,78.14,61.175,37.118,30.736 +2020-04-16 02:00:00,69.37,62.033,35.647,30.736 +2020-04-16 02:15:00,73.86,61.263000000000005,35.647,30.736 +2020-04-16 02:30:00,69.08,63.718999999999994,35.647,30.736 +2020-04-16 02:45:00,73.63,64.268,35.647,30.736 +2020-04-16 03:00:00,78.41,67.297,34.585,30.736 +2020-04-16 03:15:00,81.88,68.52,34.585,30.736 +2020-04-16 03:30:00,81.3,67.967,34.585,30.736 +2020-04-16 03:45:00,77.39,68.23100000000001,34.585,30.736 +2020-04-16 04:00:00,81.22,80.163,36.184,30.736 +2020-04-16 04:15:00,82.62,92.54,36.184,30.736 +2020-04-16 04:30:00,86.64,92.275,36.184,30.736 +2020-04-16 04:45:00,90.81,94.12799999999999,36.184,30.736 +2020-04-16 05:00:00,97.5,127.575,41.019,30.736 +2020-04-16 05:15:00,101.04,159.359,41.019,30.736 +2020-04-16 05:30:00,104.32,148.222,41.019,30.736 +2020-04-16 05:45:00,104.7,137.95,41.019,30.736 +2020-04-16 06:00:00,111.42,139.596,53.963,30.736 +2020-04-16 06:15:00,111.14,144.635,53.963,30.736 +2020-04-16 06:30:00,112.46,141.951,53.963,30.736 +2020-04-16 06:45:00,111.75,142.252,53.963,30.736 +2020-04-16 07:00:00,113.92,144.352,66.512,30.736 +2020-04-16 07:15:00,113.61,144.842,66.512,30.736 +2020-04-16 07:30:00,113.32,143.05100000000002,66.512,30.736 +2020-04-16 07:45:00,111.01,139.931,66.512,30.736 +2020-04-16 08:00:00,109.42,137.489,58.86,30.736 +2020-04-16 08:15:00,108.68,135.996,58.86,30.736 +2020-04-16 08:30:00,109.43,131.921,58.86,30.736 +2020-04-16 08:45:00,108.55,130.05200000000002,58.86,30.736 +2020-04-16 09:00:00,107.62,123.76799999999999,52.156000000000006,30.736 +2020-04-16 09:15:00,105.64,121.553,52.156000000000006,30.736 +2020-04-16 09:30:00,105.44,123.573,52.156000000000006,30.736 +2020-04-16 09:45:00,105.36,123.007,52.156000000000006,30.736 +2020-04-16 10:00:00,104.88,118.589,49.034,30.736 +2020-04-16 10:15:00,105.52,118.499,49.034,30.736 +2020-04-16 10:30:00,104.66,116.969,49.034,30.736 +2020-04-16 10:45:00,103.7,117.022,49.034,30.736 +2020-04-16 11:00:00,101.79,111.735,46.53,30.736 +2020-04-16 11:15:00,100.62,112.494,46.53,30.736 +2020-04-16 11:30:00,102.26,113.68,46.53,30.736 +2020-04-16 11:45:00,100.98,114.271,46.53,30.736 +2020-04-16 12:00:00,101.74,109.26899999999999,43.318000000000005,30.736 +2020-04-16 12:15:00,100.04,110.089,43.318000000000005,30.736 +2020-04-16 12:30:00,100.71,109.241,43.318000000000005,30.736 +2020-04-16 12:45:00,98.75,109.965,43.318000000000005,30.736 +2020-04-16 13:00:00,96.55,110.53399999999999,41.608000000000004,30.736 +2020-04-16 13:15:00,101.33,109.822,41.608000000000004,30.736 +2020-04-16 13:30:00,105.43,107.68799999999999,41.608000000000004,30.736 +2020-04-16 13:45:00,99.87,106.266,41.608000000000004,30.736 +2020-04-16 14:00:00,104.65,107.289,41.786,30.736 +2020-04-16 14:15:00,103.74,106.805,41.786,30.736 +2020-04-16 14:30:00,99.2,106.686,41.786,30.736 +2020-04-16 14:45:00,96.98,107.146,41.786,30.736 +2020-04-16 15:00:00,97.32,108.044,44.181999999999995,30.736 +2020-04-16 15:15:00,97.34,106.54299999999999,44.181999999999995,30.736 +2020-04-16 15:30:00,96.99,106.125,44.181999999999995,30.736 +2020-04-16 15:45:00,105.87,105.76,44.181999999999995,30.736 +2020-04-16 16:00:00,108.15,105.584,45.956,30.736 +2020-04-16 16:15:00,109.03,105.436,45.956,30.736 +2020-04-16 16:30:00,102.46,105.414,45.956,30.736 +2020-04-16 16:45:00,107.51,103.3,45.956,30.736 +2020-04-16 17:00:00,105.77,102.85,50.702,30.736 +2020-04-16 17:15:00,109.81,105.25399999999999,50.702,30.736 +2020-04-16 17:30:00,116.22,106.64299999999999,50.702,30.736 +2020-04-16 17:45:00,115.39,108.116,50.702,30.736 +2020-04-16 18:00:00,117.78,108.929,53.595,30.736 +2020-04-16 18:15:00,112.22,110.46799999999999,53.595,30.736 +2020-04-16 18:30:00,111.55,109.307,53.595,30.736 +2020-04-16 18:45:00,116.31,114.927,53.595,30.736 +2020-04-16 19:00:00,115.59,113.14,54.207,30.736 +2020-04-16 19:15:00,112.76,112.333,54.207,30.736 +2020-04-16 19:30:00,111.41,112.33,54.207,30.736 +2020-04-16 19:45:00,109.87,112.74,54.207,30.736 +2020-04-16 20:00:00,110.54,110.89399999999999,56.948,30.736 +2020-04-16 20:15:00,108.79,108.249,56.948,30.736 +2020-04-16 20:30:00,106.12,107.178,56.948,30.736 +2020-04-16 20:45:00,100.7,106.319,56.948,30.736 +2020-04-16 21:00:00,100.63,102.01,52.157,30.736 +2020-04-16 21:15:00,99.93,101.508,52.157,30.736 +2020-04-16 21:30:00,96.78,101.87899999999999,52.157,30.736 +2020-04-16 21:45:00,91.01,101.338,52.157,30.736 +2020-04-16 22:00:00,88.24,96.85600000000001,47.483000000000004,30.736 +2020-04-16 22:15:00,90.64,94.991,47.483000000000004,30.736 +2020-04-16 22:30:00,87.26,83.77600000000001,47.483000000000004,30.736 +2020-04-16 22:45:00,83.96,78.098,47.483000000000004,30.736 +2020-04-16 23:00:00,79.47,70.275,41.978,30.736 +2020-04-16 23:15:00,82.32,68.947,41.978,30.736 +2020-04-16 23:30:00,82.51,68.09100000000001,41.978,30.736 +2020-04-16 23:45:00,79.47,68.702,41.978,30.736 +2020-04-17 00:00:00,75.69,63.902,39.301,30.736 +2020-04-17 00:15:00,80.52,64.638,39.301,30.736 +2020-04-17 00:30:00,79.11,63.152,39.301,30.736 +2020-04-17 00:45:00,74.97,61.931000000000004,39.301,30.736 +2020-04-17 01:00:00,76.44,61.839,37.976,30.736 +2020-04-17 01:15:00,78.63,61.181999999999995,37.976,30.736 +2020-04-17 01:30:00,78.54,60.246,37.976,30.736 +2020-04-17 01:45:00,75.92,59.578,37.976,30.736 +2020-04-17 02:00:00,76.67,61.106,37.041,30.736 +2020-04-17 02:15:00,78.96,60.225,37.041,30.736 +2020-04-17 02:30:00,73.71,63.532,37.041,30.736 +2020-04-17 02:45:00,72.47,63.641000000000005,37.041,30.736 +2020-04-17 03:00:00,72.74,66.71300000000001,37.575,30.736 +2020-04-17 03:15:00,77.67,67.558,37.575,30.736 +2020-04-17 03:30:00,81.93,66.829,37.575,30.736 +2020-04-17 03:45:00,82.98,67.883,37.575,30.736 +2020-04-17 04:00:00,85.63,79.98899999999999,39.058,30.736 +2020-04-17 04:15:00,81.63,91.12700000000001,39.058,30.736 +2020-04-17 04:30:00,87.46,91.634,39.058,30.736 +2020-04-17 04:45:00,88.26,92.454,39.058,30.736 +2020-04-17 05:00:00,94.18,124.83,43.256,30.736 +2020-04-17 05:15:00,97.98,157.922,43.256,30.736 +2020-04-17 05:30:00,101.19,147.47299999999998,43.256,30.736 +2020-04-17 05:45:00,104.3,136.907,43.256,30.736 +2020-04-17 06:00:00,109.57,138.972,56.093999999999994,30.736 +2020-04-17 06:15:00,110.1,143.366,56.093999999999994,30.736 +2020-04-17 06:30:00,111.6,140.194,56.093999999999994,30.736 +2020-04-17 06:45:00,112.04,141.218,56.093999999999994,30.736 +2020-04-17 07:00:00,113.63,143.342,66.92699999999999,30.736 +2020-04-17 07:15:00,113.48,144.96,66.92699999999999,30.736 +2020-04-17 07:30:00,114.3,141.72799999999998,66.92699999999999,30.736 +2020-04-17 07:45:00,111.17,138.004,66.92699999999999,30.736 +2020-04-17 08:00:00,109.06,135.482,60.332,30.736 +2020-04-17 08:15:00,108.75,134.251,60.332,30.736 +2020-04-17 08:30:00,108.96,130.61,60.332,30.736 +2020-04-17 08:45:00,111.11,127.825,60.332,30.736 +2020-04-17 09:00:00,107.04,120.291,56.085,30.736 +2020-04-17 09:15:00,107.43,119.624,56.085,30.736 +2020-04-17 09:30:00,106.4,120.985,56.085,30.736 +2020-04-17 09:45:00,106.21,120.639,56.085,30.736 +2020-04-17 10:00:00,104.57,115.45100000000001,52.91,30.736 +2020-04-17 10:15:00,105.96,115.67200000000001,52.91,30.736 +2020-04-17 10:30:00,107.27,114.46600000000001,52.91,30.736 +2020-04-17 10:45:00,103.6,114.225,52.91,30.736 +2020-04-17 11:00:00,104.04,109.054,52.278999999999996,30.736 +2020-04-17 11:15:00,101.25,108.65100000000001,52.278999999999996,30.736 +2020-04-17 11:30:00,100.24,110.637,52.278999999999996,30.736 +2020-04-17 11:45:00,100.36,110.69200000000001,52.278999999999996,30.736 +2020-04-17 12:00:00,98.31,106.641,49.023999999999994,30.736 +2020-04-17 12:15:00,98.3,105.836,49.023999999999994,30.736 +2020-04-17 12:30:00,98.9,105.109,49.023999999999994,30.736 +2020-04-17 12:45:00,96.92,105.68,49.023999999999994,30.736 +2020-04-17 13:00:00,95.35,107.199,46.82,30.736 +2020-04-17 13:15:00,94.15,107.081,46.82,30.736 +2020-04-17 13:30:00,95.45,105.429,46.82,30.736 +2020-04-17 13:45:00,95.7,104.15100000000001,46.82,30.736 +2020-04-17 14:00:00,96.68,104.079,45.756,30.736 +2020-04-17 14:15:00,94.93,103.727,45.756,30.736 +2020-04-17 14:30:00,95.22,104.76,45.756,30.736 +2020-04-17 14:45:00,96.43,104.99799999999999,45.756,30.736 +2020-04-17 15:00:00,98.56,105.598,47.56,30.736 +2020-04-17 15:15:00,96.36,103.66799999999999,47.56,30.736 +2020-04-17 15:30:00,98.86,101.962,47.56,30.736 +2020-04-17 15:45:00,99.49,102.11399999999999,47.56,30.736 +2020-04-17 16:00:00,100.98,100.829,49.581,30.736 +2020-04-17 16:15:00,102.21,101.12700000000001,49.581,30.736 +2020-04-17 16:30:00,103.54,101.053,49.581,30.736 +2020-04-17 16:45:00,105.31,98.324,49.581,30.736 +2020-04-17 17:00:00,109.87,99.141,53.918,30.736 +2020-04-17 17:15:00,105.24,101.18,53.918,30.736 +2020-04-17 17:30:00,108.79,102.488,53.918,30.736 +2020-04-17 17:45:00,108.09,103.699,53.918,30.736 +2020-04-17 18:00:00,109.81,104.992,54.266000000000005,30.736 +2020-04-17 18:15:00,106.31,105.728,54.266000000000005,30.736 +2020-04-17 18:30:00,105.75,104.70200000000001,54.266000000000005,30.736 +2020-04-17 18:45:00,105.56,110.596,54.266000000000005,30.736 +2020-04-17 19:00:00,104.01,109.889,54.092,30.736 +2020-04-17 19:15:00,101.85,110.20700000000001,54.092,30.736 +2020-04-17 19:30:00,103.45,110.035,54.092,30.736 +2020-04-17 19:45:00,104.61,109.553,54.092,30.736 +2020-04-17 20:00:00,101.81,107.604,59.038999999999994,30.736 +2020-04-17 20:15:00,104.72,105.49,59.038999999999994,30.736 +2020-04-17 20:30:00,104.95,104.11200000000001,59.038999999999994,30.736 +2020-04-17 20:45:00,104.9,103.052,59.038999999999994,30.736 +2020-04-17 21:00:00,92.54,99.882,53.346000000000004,30.736 +2020-04-17 21:15:00,89.07,100.73,53.346000000000004,30.736 +2020-04-17 21:30:00,86.36,101.012,53.346000000000004,30.736 +2020-04-17 21:45:00,91.79,100.928,53.346000000000004,30.736 +2020-04-17 22:00:00,87.88,96.88600000000001,47.938,30.736 +2020-04-17 22:15:00,87.19,94.811,47.938,30.736 +2020-04-17 22:30:00,79.25,90.098,47.938,30.736 +2020-04-17 22:45:00,78.07,86.851,47.938,30.736 +2020-04-17 23:00:00,78.74,79.878,40.266,30.736 +2020-04-17 23:15:00,79.25,76.51899999999999,40.266,30.736 +2020-04-17 23:30:00,79.44,73.683,40.266,30.736 +2020-04-17 23:45:00,72.93,73.869,40.266,30.736 +2020-04-18 00:00:00,72.24,62.902,39.184,30.618000000000002 +2020-04-18 00:15:00,74.92,61.023999999999994,39.184,30.618000000000002 +2020-04-18 00:30:00,73.65,59.887,39.184,30.618000000000002 +2020-04-18 00:45:00,71.07,58.537,39.184,30.618000000000002 +2020-04-18 01:00:00,64.33,58.955,34.692,30.618000000000002 +2020-04-18 01:15:00,65.67,58.178000000000004,34.692,30.618000000000002 +2020-04-18 01:30:00,64.17,56.406000000000006,34.692,30.618000000000002 +2020-04-18 01:45:00,68.34,56.482,34.692,30.618000000000002 +2020-04-18 02:00:00,71.5,57.685,32.919000000000004,30.618000000000002 +2020-04-18 02:15:00,72.43,56.059,32.919000000000004,30.618000000000002 +2020-04-18 02:30:00,69.43,58.281000000000006,32.919000000000004,30.618000000000002 +2020-04-18 02:45:00,69.21,58.986000000000004,32.919000000000004,30.618000000000002 +2020-04-18 03:00:00,72.51,61.458999999999996,32.024,30.618000000000002 +2020-04-18 03:15:00,68.08,61.188,32.024,30.618000000000002 +2020-04-18 03:30:00,66.16,59.88399999999999,32.024,30.618000000000002 +2020-04-18 03:45:00,64.76,62.033,32.024,30.618000000000002 +2020-04-18 04:00:00,66.57,70.758,31.958000000000002,30.618000000000002 +2020-04-18 04:15:00,67.33,79.971,31.958000000000002,30.618000000000002 +2020-04-18 04:30:00,66.23,78.243,31.958000000000002,30.618000000000002 +2020-04-18 04:45:00,64.3,78.995,31.958000000000002,30.618000000000002 +2020-04-18 05:00:00,64.93,97.959,32.75,30.618000000000002 +2020-04-18 05:15:00,66.66,114.07,32.75,30.618000000000002 +2020-04-18 05:30:00,67.03,104.89,32.75,30.618000000000002 +2020-04-18 05:45:00,70.54,100.15,32.75,30.618000000000002 +2020-04-18 06:00:00,71.71,118.912,34.461999999999996,30.618000000000002 +2020-04-18 06:15:00,73.46,136.754,34.461999999999996,30.618000000000002 +2020-04-18 06:30:00,75.72,128.946,34.461999999999996,30.618000000000002 +2020-04-18 06:45:00,76.77,123.31,34.461999999999996,30.618000000000002 +2020-04-18 07:00:00,78.94,122.14299999999999,37.736,30.618000000000002 +2020-04-18 07:15:00,78.58,122.23200000000001,37.736,30.618000000000002 +2020-04-18 07:30:00,79.45,121.365,37.736,30.618000000000002 +2020-04-18 07:45:00,80.73,120.3,37.736,30.618000000000002 +2020-04-18 08:00:00,80.41,120.022,42.34,30.618000000000002 +2020-04-18 08:15:00,80.62,120.61,42.34,30.618000000000002 +2020-04-18 08:30:00,79.58,117.786,42.34,30.618000000000002 +2020-04-18 08:45:00,79.67,117.322,42.34,30.618000000000002 +2020-04-18 09:00:00,77.81,112.633,43.571999999999996,30.618000000000002 +2020-04-18 09:15:00,76.73,112.691,43.571999999999996,30.618000000000002 +2020-04-18 09:30:00,75.68,114.87700000000001,43.571999999999996,30.618000000000002 +2020-04-18 09:45:00,75.15,114.365,43.571999999999996,30.618000000000002 +2020-04-18 10:00:00,74.84,109.54700000000001,40.514,30.618000000000002 +2020-04-18 10:15:00,76.04,110.12799999999999,40.514,30.618000000000002 +2020-04-18 10:30:00,76.37,108.82600000000001,40.514,30.618000000000002 +2020-04-18 10:45:00,75.02,109.126,40.514,30.618000000000002 +2020-04-18 11:00:00,73.78,103.92299999999999,36.388000000000005,30.618000000000002 +2020-04-18 11:15:00,72.94,103.676,36.388000000000005,30.618000000000002 +2020-04-18 11:30:00,70.92,105.191,36.388000000000005,30.618000000000002 +2020-04-18 11:45:00,69.83,105.12100000000001,36.388000000000005,30.618000000000002 +2020-04-18 12:00:00,67.19,100.64200000000001,35.217,30.618000000000002 +2020-04-18 12:15:00,67.12,100.725,35.217,30.618000000000002 +2020-04-18 12:30:00,65.58,100.066,35.217,30.618000000000002 +2020-04-18 12:45:00,65.28,100.61399999999999,35.217,30.618000000000002 +2020-04-18 13:00:00,61.25,101.42399999999999,32.001999999999995,30.618000000000002 +2020-04-18 13:15:00,62.76,99.764,32.001999999999995,30.618000000000002 +2020-04-18 13:30:00,62.74,97.959,32.001999999999995,30.618000000000002 +2020-04-18 13:45:00,62.56,96.169,32.001999999999995,30.618000000000002 +2020-04-18 14:00:00,63.31,96.9,31.304000000000002,30.618000000000002 +2020-04-18 14:15:00,63.14,95.477,31.304000000000002,30.618000000000002 +2020-04-18 14:30:00,63.14,95.135,31.304000000000002,30.618000000000002 +2020-04-18 14:45:00,64.8,95.787,31.304000000000002,30.618000000000002 +2020-04-18 15:00:00,65.13,97.071,34.731,30.618000000000002 +2020-04-18 15:15:00,65.05,96.014,34.731,30.618000000000002 +2020-04-18 15:30:00,66.87,95.29700000000001,34.731,30.618000000000002 +2020-04-18 15:45:00,69.39,94.9,34.731,30.618000000000002 +2020-04-18 16:00:00,71.14,94.525,38.769,30.618000000000002 +2020-04-18 16:15:00,72.55,94.76299999999999,38.769,30.618000000000002 +2020-04-18 16:30:00,74.77,94.803,38.769,30.618000000000002 +2020-04-18 16:45:00,76.84,92.49600000000001,38.769,30.618000000000002 +2020-04-18 17:00:00,79.92,92.44,44.928000000000004,30.618000000000002 +2020-04-18 17:15:00,79.83,94.03399999999999,44.928000000000004,30.618000000000002 +2020-04-18 17:30:00,81.4,95.20200000000001,44.928000000000004,30.618000000000002 +2020-04-18 17:45:00,81.53,96.46799999999999,44.928000000000004,30.618000000000002 +2020-04-18 18:00:00,85.84,98.512,47.786,30.618000000000002 +2020-04-18 18:15:00,83.87,101.316,47.786,30.618000000000002 +2020-04-18 18:30:00,81.87,101.869,47.786,30.618000000000002 +2020-04-18 18:45:00,81.6,103.727,47.786,30.618000000000002 +2020-04-18 19:00:00,82.11,102.561,47.463,30.618000000000002 +2020-04-18 19:15:00,80.38,101.963,47.463,30.618000000000002 +2020-04-18 19:30:00,82.61,102.70299999999999,47.463,30.618000000000002 +2020-04-18 19:45:00,81.66,103.302,47.463,30.618000000000002 +2020-04-18 20:00:00,78.44,103.012,43.735,30.618000000000002 +2020-04-18 20:15:00,77.86,101.625,43.735,30.618000000000002 +2020-04-18 20:30:00,76.06,99.522,43.735,30.618000000000002 +2020-04-18 20:45:00,75.04,99.48700000000001,43.735,30.618000000000002 +2020-04-18 21:00:00,69.71,96.571,40.346,30.618000000000002 +2020-04-18 21:15:00,69.85,97.436,40.346,30.618000000000002 +2020-04-18 21:30:00,66.1,98.477,40.346,30.618000000000002 +2020-04-18 21:45:00,66.61,97.851,40.346,30.618000000000002 +2020-04-18 22:00:00,63.91,94.52,39.323,30.618000000000002 +2020-04-18 22:15:00,63.83,93.944,39.323,30.618000000000002 +2020-04-18 22:30:00,61.62,92.242,39.323,30.618000000000002 +2020-04-18 22:45:00,61.98,90.219,39.323,30.618000000000002 +2020-04-18 23:00:00,56.73,84.055,33.716,30.618000000000002 +2020-04-18 23:15:00,56.39,80.14699999999999,33.716,30.618000000000002 +2020-04-18 23:30:00,56.57,78.0,33.716,30.618000000000002 +2020-04-18 23:45:00,57.7,77.148,33.716,30.618000000000002 +2020-04-19 00:00:00,47.91,63.659,28.703000000000003,30.618000000000002 +2020-04-19 00:15:00,49.58,60.85,28.703000000000003,30.618000000000002 +2020-04-19 00:30:00,48.9,59.416000000000004,28.703000000000003,30.618000000000002 +2020-04-19 00:45:00,51.89,58.347,28.703000000000003,30.618000000000002 +2020-04-19 01:00:00,48.88,58.867,26.171,30.618000000000002 +2020-04-19 01:15:00,49.69,58.504,26.171,30.618000000000002 +2020-04-19 01:30:00,47.4,56.879,26.171,30.618000000000002 +2020-04-19 01:45:00,50.6,56.542,26.171,30.618000000000002 +2020-04-19 02:00:00,48.49,57.409,25.326999999999998,30.618000000000002 +2020-04-19 02:15:00,49.54,55.821000000000005,25.326999999999998,30.618000000000002 +2020-04-19 02:30:00,49.42,58.62,25.326999999999998,30.618000000000002 +2020-04-19 02:45:00,49.28,59.363,25.326999999999998,30.618000000000002 +2020-04-19 03:00:00,49.27,62.45399999999999,24.311999999999998,30.618000000000002 +2020-04-19 03:15:00,50.12,62.077,24.311999999999998,30.618000000000002 +2020-04-19 03:30:00,50.56,60.961999999999996,24.311999999999998,30.618000000000002 +2020-04-19 03:45:00,51.01,62.55,24.311999999999998,30.618000000000002 +2020-04-19 04:00:00,52.33,71.09100000000001,25.33,30.618000000000002 +2020-04-19 04:15:00,53.42,79.464,25.33,30.618000000000002 +2020-04-19 04:30:00,54.02,78.689,25.33,30.618000000000002 +2020-04-19 04:45:00,52.87,79.245,25.33,30.618000000000002 +2020-04-19 05:00:00,53.79,96.527,25.309,30.618000000000002 +2020-04-19 05:15:00,55.01,110.838,25.309,30.618000000000002 +2020-04-19 05:30:00,54.77,101.319,25.309,30.618000000000002 +2020-04-19 05:45:00,56.87,96.553,25.309,30.618000000000002 +2020-04-19 06:00:00,58.42,113.615,25.945999999999998,30.618000000000002 +2020-04-19 06:15:00,58.63,131.168,25.945999999999998,30.618000000000002 +2020-04-19 06:30:00,59.65,122.322,25.945999999999998,30.618000000000002 +2020-04-19 06:45:00,61.3,115.475,25.945999999999998,30.618000000000002 +2020-04-19 07:00:00,62.91,115.682,27.87,30.618000000000002 +2020-04-19 07:15:00,63.9,114.219,27.87,30.618000000000002 +2020-04-19 07:30:00,64.4,113.54899999999999,27.87,30.618000000000002 +2020-04-19 07:45:00,63.96,112.075,27.87,30.618000000000002 +2020-04-19 08:00:00,62.93,113.177,32.114000000000004,30.618000000000002 +2020-04-19 08:15:00,62.87,114.525,32.114000000000004,30.618000000000002 +2020-04-19 08:30:00,62.0,113.11399999999999,32.114000000000004,30.618000000000002 +2020-04-19 08:45:00,61.46,113.685,32.114000000000004,30.618000000000002 +2020-04-19 09:00:00,60.12,108.691,34.222,30.618000000000002 +2020-04-19 09:15:00,60.81,108.734,34.222,30.618000000000002 +2020-04-19 09:30:00,59.84,111.131,34.222,30.618000000000002 +2020-04-19 09:45:00,60.52,111.23299999999999,34.222,30.618000000000002 +2020-04-19 10:00:00,60.88,108.11,34.544000000000004,30.618000000000002 +2020-04-19 10:15:00,63.47,109.119,34.544000000000004,30.618000000000002 +2020-04-19 10:30:00,64.28,108.31299999999999,34.544000000000004,30.618000000000002 +2020-04-19 10:45:00,64.13,108.21,34.544000000000004,30.618000000000002 +2020-04-19 11:00:00,60.96,103.27799999999999,36.368,30.618000000000002 +2020-04-19 11:15:00,60.33,102.855,36.368,30.618000000000002 +2020-04-19 11:30:00,57.6,104.197,36.368,30.618000000000002 +2020-04-19 11:45:00,56.64,104.645,36.368,30.618000000000002 +2020-04-19 12:00:00,54.74,100.47,32.433,30.618000000000002 +2020-04-19 12:15:00,53.47,101.291,32.433,30.618000000000002 +2020-04-19 12:30:00,53.55,99.87299999999999,32.433,30.618000000000002 +2020-04-19 12:45:00,53.72,99.48299999999999,32.433,30.618000000000002 +2020-04-19 13:00:00,52.34,99.73100000000001,28.971999999999998,30.618000000000002 +2020-04-19 13:15:00,52.36,99.473,28.971999999999998,30.618000000000002 +2020-04-19 13:30:00,52.22,96.885,28.971999999999998,30.618000000000002 +2020-04-19 13:45:00,53.3,95.381,28.971999999999998,30.618000000000002 +2020-04-19 14:00:00,52.13,97.029,25.531999999999996,30.618000000000002 +2020-04-19 14:15:00,52.55,96.542,25.531999999999996,30.618000000000002 +2020-04-19 14:30:00,52.58,96.13799999999999,25.531999999999996,30.618000000000002 +2020-04-19 14:45:00,53.01,95.916,25.531999999999996,30.618000000000002 +2020-04-19 15:00:00,55.7,96.435,25.766,30.618000000000002 +2020-04-19 15:15:00,59.0,95.31299999999999,25.766,30.618000000000002 +2020-04-19 15:30:00,60.62,94.788,25.766,30.618000000000002 +2020-04-19 15:45:00,65.31,94.945,25.766,30.618000000000002 +2020-04-19 16:00:00,66.6,94.571,29.232,30.618000000000002 +2020-04-19 16:15:00,66.2,94.41799999999999,29.232,30.618000000000002 +2020-04-19 16:30:00,70.78,95.229,29.232,30.618000000000002 +2020-04-19 16:45:00,71.98,92.97,29.232,30.618000000000002 +2020-04-19 17:00:00,74.6,93.13,37.431,30.618000000000002 +2020-04-19 17:15:00,75.63,95.47,37.431,30.618000000000002 +2020-04-19 17:30:00,77.0,97.29899999999999,37.431,30.618000000000002 +2020-04-19 17:45:00,78.99,100.04899999999999,37.431,30.618000000000002 +2020-04-19 18:00:00,82.25,102.135,41.251999999999995,30.618000000000002 +2020-04-19 18:15:00,79.87,105.406,41.251999999999995,30.618000000000002 +2020-04-19 18:30:00,79.78,104.67200000000001,41.251999999999995,30.618000000000002 +2020-04-19 18:45:00,79.99,107.59899999999999,41.251999999999995,30.618000000000002 +2020-04-19 19:00:00,81.9,107.616,41.784,30.618000000000002 +2020-04-19 19:15:00,82.8,106.60600000000001,41.784,30.618000000000002 +2020-04-19 19:30:00,87.07,107.10799999999999,41.784,30.618000000000002 +2020-04-19 19:45:00,85.32,108.184,41.784,30.618000000000002 +2020-04-19 20:00:00,87.59,107.975,40.804,30.618000000000002 +2020-04-19 20:15:00,90.05,107.021,40.804,30.618000000000002 +2020-04-19 20:30:00,90.49,106.132,40.804,30.618000000000002 +2020-04-19 20:45:00,89.62,104.4,40.804,30.618000000000002 +2020-04-19 21:00:00,81.38,100.014,38.379,30.618000000000002 +2020-04-19 21:15:00,84.66,100.369,38.379,30.618000000000002 +2020-04-19 21:30:00,77.57,101.12200000000001,38.379,30.618000000000002 +2020-04-19 21:45:00,78.05,100.834,38.379,30.618000000000002 +2020-04-19 22:00:00,77.86,98.241,37.87,30.618000000000002 +2020-04-19 22:15:00,82.24,96.18799999999999,37.87,30.618000000000002 +2020-04-19 22:30:00,80.13,92.486,37.87,30.618000000000002 +2020-04-19 22:45:00,76.14,89.17200000000001,37.87,30.618000000000002 +2020-04-19 23:00:00,67.84,81.33,33.332,30.618000000000002 +2020-04-19 23:15:00,72.28,79.26100000000001,33.332,30.618000000000002 +2020-04-19 23:30:00,75.58,77.17699999999999,33.332,30.618000000000002 +2020-04-19 23:45:00,73.37,76.899,33.332,30.618000000000002 +2020-04-20 00:00:00,69.74,66.392,34.698,30.736 +2020-04-20 00:15:00,72.46,65.615,34.698,30.736 +2020-04-20 00:30:00,70.41,63.977,34.698,30.736 +2020-04-20 00:45:00,69.96,62.376000000000005,34.698,30.736 +2020-04-20 01:00:00,66.93,63.148,32.889,30.736 +2020-04-20 01:15:00,72.98,62.507,32.889,30.736 +2020-04-20 01:30:00,73.29,61.136,32.889,30.736 +2020-04-20 01:45:00,71.28,60.791000000000004,32.889,30.736 +2020-04-20 02:00:00,68.66,61.93899999999999,32.06,30.736 +2020-04-20 02:15:00,73.7,60.363,32.06,30.736 +2020-04-20 02:30:00,73.14,63.458999999999996,32.06,30.736 +2020-04-20 02:45:00,73.33,63.801,32.06,30.736 +2020-04-20 03:00:00,67.47,67.866,30.515,30.736 +2020-04-20 03:15:00,68.31,68.77,30.515,30.736 +2020-04-20 03:30:00,68.12,67.986,30.515,30.736 +2020-04-20 03:45:00,69.11,69.008,30.515,30.736 +2020-04-20 04:00:00,74.41,81.767,31.436,30.736 +2020-04-20 04:15:00,77.05,94.147,31.436,30.736 +2020-04-20 04:30:00,80.21,94.115,31.436,30.736 +2020-04-20 04:45:00,83.85,94.995,31.436,30.736 +2020-04-20 05:00:00,92.02,124.523,38.997,30.736 +2020-04-20 05:15:00,94.95,155.561,38.997,30.736 +2020-04-20 05:30:00,97.01,145.115,38.997,30.736 +2020-04-20 05:45:00,100.5,135.55100000000002,38.997,30.736 +2020-04-20 06:00:00,105.42,137.136,54.97,30.736 +2020-04-20 06:15:00,107.37,141.161,54.97,30.736 +2020-04-20 06:30:00,110.05,138.94799999999998,54.97,30.736 +2020-04-20 06:45:00,111.02,140.03,54.97,30.736 +2020-04-20 07:00:00,115.16,142.121,66.032,30.736 +2020-04-20 07:15:00,112.2,142.808,66.032,30.736 +2020-04-20 07:30:00,115.55,141.128,66.032,30.736 +2020-04-20 07:45:00,114.85,138.559,66.032,30.736 +2020-04-20 08:00:00,113.82,136.049,59.941,30.736 +2020-04-20 08:15:00,117.57,135.525,59.941,30.736 +2020-04-20 08:30:00,120.95,131.31799999999998,59.941,30.736 +2020-04-20 08:45:00,120.25,130.308,59.941,30.736 +2020-04-20 09:00:00,121.46,124.26299999999999,54.016000000000005,30.736 +2020-04-20 09:15:00,120.31,121.29700000000001,54.016000000000005,30.736 +2020-04-20 09:30:00,119.35,122.581,54.016000000000005,30.736 +2020-04-20 09:45:00,116.25,121.405,54.016000000000005,30.736 +2020-04-20 10:00:00,114.59,118.214,50.63,30.736 +2020-04-20 10:15:00,118.18,118.99600000000001,50.63,30.736 +2020-04-20 10:30:00,111.25,117.441,50.63,30.736 +2020-04-20 10:45:00,112.04,116.792,50.63,30.736 +2020-04-20 11:00:00,118.8,110.635,49.951,30.736 +2020-04-20 11:15:00,119.3,111.428,49.951,30.736 +2020-04-20 11:30:00,123.96,113.979,49.951,30.736 +2020-04-20 11:45:00,126.42,114.51899999999999,49.951,30.736 +2020-04-20 12:00:00,124.57,110.541,46.913000000000004,30.736 +2020-04-20 12:15:00,122.52,111.43799999999999,46.913000000000004,30.736 +2020-04-20 12:30:00,115.69,109.486,46.913000000000004,30.736 +2020-04-20 12:45:00,118.2,109.985,46.913000000000004,30.736 +2020-04-20 13:00:00,113.18,111.163,47.093999999999994,30.736 +2020-04-20 13:15:00,112.18,109.56700000000001,47.093999999999994,30.736 +2020-04-20 13:30:00,112.03,106.774,47.093999999999994,30.736 +2020-04-20 13:45:00,106.19,105.821,47.093999999999994,30.736 +2020-04-20 14:00:00,99.72,106.662,46.678000000000004,30.736 +2020-04-20 14:15:00,107.85,106.13,46.678000000000004,30.736 +2020-04-20 14:30:00,105.99,105.26899999999999,46.678000000000004,30.736 +2020-04-20 14:45:00,95.87,106.25399999999999,46.678000000000004,30.736 +2020-04-20 15:00:00,100.13,107.649,47.715,30.736 +2020-04-20 15:15:00,102.74,105.315,47.715,30.736 +2020-04-20 15:30:00,106.75,104.714,47.715,30.736 +2020-04-20 15:45:00,109.06,104.306,47.715,30.736 +2020-04-20 16:00:00,111.0,104.484,49.81100000000001,30.736 +2020-04-20 16:15:00,110.49,103.917,49.81100000000001,30.736 +2020-04-20 16:30:00,109.82,103.781,49.81100000000001,30.736 +2020-04-20 16:45:00,113.31,100.8,49.81100000000001,30.736 +2020-04-20 17:00:00,114.39,100.116,55.591,30.736 +2020-04-20 17:15:00,113.67,102.141,55.591,30.736 +2020-04-20 17:30:00,113.09,103.419,55.591,30.736 +2020-04-20 17:45:00,114.97,105.07,55.591,30.736 +2020-04-20 18:00:00,116.55,106.62,56.523,30.736 +2020-04-20 18:15:00,113.53,107.551,56.523,30.736 +2020-04-20 18:30:00,111.92,106.67,56.523,30.736 +2020-04-20 18:45:00,111.5,112.01299999999999,56.523,30.736 +2020-04-20 19:00:00,109.89,110.913,56.044,30.736 +2020-04-20 19:15:00,115.06,110.16,56.044,30.736 +2020-04-20 19:30:00,113.63,110.69200000000001,56.044,30.736 +2020-04-20 19:45:00,112.78,110.954,56.044,30.736 +2020-04-20 20:00:00,101.75,108.655,61.715,30.736 +2020-04-20 20:15:00,100.7,107.319,61.715,30.736 +2020-04-20 20:30:00,100.68,105.82600000000001,61.715,30.736 +2020-04-20 20:45:00,100.06,105.12200000000001,61.715,30.736 +2020-04-20 21:00:00,99.18,100.616,56.24,30.736 +2020-04-20 21:15:00,98.71,100.664,56.24,30.736 +2020-04-20 21:30:00,94.69,101.24700000000001,56.24,30.736 +2020-04-20 21:45:00,91.68,100.58200000000001,56.24,30.736 +2020-04-20 22:00:00,88.84,95.15299999999999,50.437,30.736 +2020-04-20 22:15:00,88.96,93.74600000000001,50.437,30.736 +2020-04-20 22:30:00,84.92,82.199,50.437,30.736 +2020-04-20 22:45:00,85.41,76.347,50.437,30.736 +2020-04-20 23:00:00,81.6,68.904,42.756,30.736 +2020-04-20 23:15:00,81.0,67.168,42.756,30.736 +2020-04-20 23:30:00,80.1,66.495,42.756,30.736 +2020-04-20 23:45:00,81.46,67.245,42.756,30.736 +2020-04-21 00:00:00,77.51,64.109,39.857,30.736 +2020-04-21 00:15:00,75.11,64.649,39.857,30.736 +2020-04-21 00:30:00,73.12,63.016999999999996,39.857,30.736 +2020-04-21 00:45:00,78.89,61.471000000000004,39.857,30.736 +2020-04-21 01:00:00,78.67,61.772,37.233000000000004,30.736 +2020-04-21 01:15:00,77.8,60.961000000000006,37.233000000000004,30.736 +2020-04-21 01:30:00,72.87,59.566,37.233000000000004,30.736 +2020-04-21 01:45:00,74.1,59.026,37.233000000000004,30.736 +2020-04-21 02:00:00,78.95,59.831,35.856,30.736 +2020-04-21 02:15:00,78.47,58.976000000000006,35.856,30.736 +2020-04-21 02:30:00,72.53,61.526,35.856,30.736 +2020-04-21 02:45:00,71.94,62.106,35.856,30.736 +2020-04-21 03:00:00,74.86,65.212,34.766999999999996,30.736 +2020-04-21 03:15:00,80.59,66.313,34.766999999999996,30.736 +2020-04-21 03:30:00,83.48,65.743,34.766999999999996,30.736 +2020-04-21 03:45:00,83.58,66.119,34.766999999999996,30.736 +2020-04-21 04:00:00,79.94,77.88,35.468,30.736 +2020-04-21 04:15:00,82.73,90.056,35.468,30.736 +2020-04-21 04:30:00,86.26,89.789,35.468,30.736 +2020-04-21 04:45:00,87.7,91.59100000000001,35.468,30.736 +2020-04-21 05:00:00,95.23,124.53200000000001,40.399,30.736 +2020-04-21 05:15:00,98.1,155.803,40.399,30.736 +2020-04-21 05:30:00,100.01,144.778,40.399,30.736 +2020-04-21 05:45:00,102.09,134.755,40.399,30.736 +2020-04-21 06:00:00,107.66,136.52100000000002,54.105,30.736 +2020-04-21 06:15:00,108.52,141.458,54.105,30.736 +2020-04-21 06:30:00,110.71,138.69,54.105,30.736 +2020-04-21 06:45:00,111.29,138.968,54.105,30.736 +2020-04-21 07:00:00,110.64,141.067,63.083,30.736 +2020-04-21 07:15:00,112.61,141.486,63.083,30.736 +2020-04-21 07:30:00,117.23,139.503,63.083,30.736 +2020-04-21 07:45:00,111.61,136.388,63.083,30.736 +2020-04-21 08:00:00,109.22,133.88299999999998,57.254,30.736 +2020-04-21 08:15:00,106.0,132.562,57.254,30.736 +2020-04-21 08:30:00,108.93,128.343,57.254,30.736 +2020-04-21 08:45:00,111.02,126.616,57.254,30.736 +2020-04-21 09:00:00,108.14,120.348,51.395,30.736 +2020-04-21 09:15:00,111.76,118.16,51.395,30.736 +2020-04-21 09:30:00,112.64,120.26700000000001,51.395,30.736 +2020-04-21 09:45:00,108.02,119.867,51.395,30.736 +2020-04-21 10:00:00,106.08,115.491,48.201,30.736 +2020-04-21 10:15:00,104.55,115.639,48.201,30.736 +2020-04-21 10:30:00,104.69,114.223,48.201,30.736 +2020-04-21 10:45:00,104.67,114.375,48.201,30.736 +2020-04-21 11:00:00,103.29,109.04799999999999,46.133,30.736 +2020-04-21 11:15:00,102.35,109.921,46.133,30.736 +2020-04-21 11:30:00,98.0,111.10700000000001,46.133,30.736 +2020-04-21 11:45:00,100.05,111.792,46.133,30.736 +2020-04-21 12:00:00,95.93,106.93700000000001,44.243,30.736 +2020-04-21 12:15:00,104.04,107.79700000000001,44.243,30.736 +2020-04-21 12:30:00,105.24,106.734,44.243,30.736 +2020-04-21 12:45:00,107.04,107.476,44.243,30.736 +2020-04-21 13:00:00,100.69,108.23700000000001,45.042,30.736 +2020-04-21 13:15:00,99.99,107.499,45.042,30.736 +2020-04-21 13:30:00,104.95,105.37799999999999,45.042,30.736 +2020-04-21 13:45:00,111.42,103.964,45.042,30.736 +2020-04-21 14:00:00,114.25,105.295,44.062,30.736 +2020-04-21 14:15:00,112.57,104.719,44.062,30.736 +2020-04-21 14:30:00,109.07,104.374,44.062,30.736 +2020-04-21 14:45:00,106.59,104.85,44.062,30.736 +2020-04-21 15:00:00,113.59,105.935,46.461999999999996,30.736 +2020-04-21 15:15:00,116.36,104.32,46.461999999999996,30.736 +2020-04-21 15:30:00,115.99,103.68299999999999,46.461999999999996,30.736 +2020-04-21 15:45:00,113.69,103.225,46.461999999999996,30.736 +2020-04-21 16:00:00,109.01,103.26299999999999,48.802,30.736 +2020-04-21 16:15:00,111.19,102.992,48.802,30.736 +2020-04-21 16:30:00,117.84,102.99799999999999,48.802,30.736 +2020-04-21 16:45:00,116.61,100.555,48.802,30.736 +2020-04-21 17:00:00,119.91,100.361,55.672,30.736 +2020-04-21 17:15:00,117.41,102.63799999999999,55.672,30.736 +2020-04-21 17:30:00,119.66,104.00299999999999,55.672,30.736 +2020-04-21 17:45:00,120.44,105.34700000000001,55.672,30.736 +2020-04-21 18:00:00,118.14,106.23299999999999,57.006,30.736 +2020-04-21 18:15:00,114.94,107.87,57.006,30.736 +2020-04-21 18:30:00,117.32,106.64200000000001,57.006,30.736 +2020-04-21 18:45:00,117.13,112.29299999999999,57.006,30.736 +2020-04-21 19:00:00,110.96,110.45100000000001,57.148,30.736 +2020-04-21 19:15:00,108.42,109.675,57.148,30.736 +2020-04-21 19:30:00,113.5,109.726,57.148,30.736 +2020-04-21 19:45:00,116.0,110.249,57.148,30.736 +2020-04-21 20:00:00,107.84,108.26299999999999,61.895,30.736 +2020-04-21 20:15:00,108.53,105.655,61.895,30.736 +2020-04-21 20:30:00,108.46,104.756,61.895,30.736 +2020-04-21 20:45:00,106.57,104.056,61.895,30.736 +2020-04-21 21:00:00,95.96,99.756,54.78,30.736 +2020-04-21 21:15:00,98.48,99.322,54.78,30.736 +2020-04-21 21:30:00,95.81,99.62700000000001,54.78,30.736 +2020-04-21 21:45:00,93.29,99.24,54.78,30.736 +2020-04-21 22:00:00,84.54,94.82799999999999,50.76,30.736 +2020-04-21 22:15:00,88.53,93.089,50.76,30.736 +2020-04-21 22:30:00,86.77,81.759,50.76,30.736 +2020-04-21 22:45:00,86.04,76.04,50.76,30.736 +2020-04-21 23:00:00,75.63,68.074,44.162,30.736 +2020-04-21 23:15:00,72.93,66.936,44.162,30.736 +2020-04-21 23:30:00,77.07,66.072,44.162,30.736 +2020-04-21 23:45:00,80.79,66.721,44.162,30.736 +2020-04-22 00:00:00,77.42,63.72,39.061,30.736 +2020-04-22 00:15:00,75.61,64.27199999999999,39.061,30.736 +2020-04-22 00:30:00,73.58,62.63399999999999,39.061,30.736 +2020-04-22 00:45:00,72.45,61.093,39.061,30.736 +2020-04-22 01:00:00,69.55,61.388000000000005,35.795,30.736 +2020-04-22 01:15:00,76.2,60.552,35.795,30.736 +2020-04-22 01:30:00,70.64,59.136,35.795,30.736 +2020-04-22 01:45:00,71.82,58.599,35.795,30.736 +2020-04-22 02:00:00,71.59,59.394,33.316,30.736 +2020-04-22 02:15:00,78.92,58.523,33.316,30.736 +2020-04-22 02:30:00,78.64,61.091,33.316,30.736 +2020-04-22 02:45:00,78.89,61.676,33.316,30.736 +2020-04-22 03:00:00,80.34,64.79899999999999,32.803000000000004,30.736 +2020-04-22 03:15:00,75.97,65.874,32.803000000000004,30.736 +2020-04-22 03:30:00,72.33,65.30199999999999,32.803000000000004,30.736 +2020-04-22 03:45:00,75.91,65.7,32.803000000000004,30.736 +2020-04-22 04:00:00,78.96,77.42699999999999,34.235,30.736 +2020-04-22 04:15:00,78.93,89.56200000000001,34.235,30.736 +2020-04-22 04:30:00,83.97,89.294,34.235,30.736 +2020-04-22 04:45:00,84.72,91.086,34.235,30.736 +2020-04-22 05:00:00,92.29,123.926,38.65,30.736 +2020-04-22 05:15:00,96.41,155.092,38.65,30.736 +2020-04-22 05:30:00,96.57,144.094,38.65,30.736 +2020-04-22 05:45:00,103.85,134.12,38.65,30.736 +2020-04-22 06:00:00,107.64,135.908,54.951,30.736 +2020-04-22 06:15:00,108.62,140.82299999999998,54.951,30.736 +2020-04-22 06:30:00,111.0,138.041,54.951,30.736 +2020-04-22 06:45:00,110.88,138.314,54.951,30.736 +2020-04-22 07:00:00,112.83,140.412,67.328,30.736 +2020-04-22 07:15:00,112.0,140.819,67.328,30.736 +2020-04-22 07:30:00,110.25,138.798,67.328,30.736 +2020-04-22 07:45:00,108.56,135.684,67.328,30.736 +2020-04-22 08:00:00,105.32,133.16899999999998,60.23,30.736 +2020-04-22 08:15:00,105.49,131.882,60.23,30.736 +2020-04-22 08:30:00,110.55,127.635,60.23,30.736 +2020-04-22 08:45:00,113.42,125.93700000000001,60.23,30.736 +2020-04-22 09:00:00,107.8,119.671,56.845,30.736 +2020-04-22 09:15:00,107.23,117.49,56.845,30.736 +2020-04-22 09:30:00,106.86,119.613,56.845,30.736 +2020-04-22 09:45:00,108.88,119.24600000000001,56.845,30.736 +2020-04-22 10:00:00,117.52,114.87799999999999,53.832,30.736 +2020-04-22 10:15:00,121.69,115.073,53.832,30.736 +2020-04-22 10:30:00,118.62,113.68,53.832,30.736 +2020-04-22 10:45:00,118.16,113.852,53.832,30.736 +2020-04-22 11:00:00,108.04,108.51700000000001,53.225,30.736 +2020-04-22 11:15:00,106.35,109.412,53.225,30.736 +2020-04-22 11:30:00,113.65,110.598,53.225,30.736 +2020-04-22 11:45:00,119.82,111.302,53.225,30.736 +2020-04-22 12:00:00,111.54,106.476,50.676,30.736 +2020-04-22 12:15:00,112.94,107.34299999999999,50.676,30.736 +2020-04-22 12:30:00,106.75,106.238,50.676,30.736 +2020-04-22 12:45:00,107.24,106.98299999999999,50.676,30.736 +2020-04-22 13:00:00,106.99,107.78200000000001,50.646,30.736 +2020-04-22 13:15:00,103.94,107.039,50.646,30.736 +2020-04-22 13:30:00,111.47,104.92,50.646,30.736 +2020-04-22 13:45:00,121.34,103.508,50.646,30.736 +2020-04-22 14:00:00,117.59,104.902,50.786,30.736 +2020-04-22 14:15:00,115.99,104.306,50.786,30.736 +2020-04-22 14:30:00,118.6,103.917,50.786,30.736 +2020-04-22 14:45:00,123.1,104.39399999999999,50.786,30.736 +2020-04-22 15:00:00,122.69,105.51799999999999,51.535,30.736 +2020-04-22 15:15:00,113.87,103.881,51.535,30.736 +2020-04-22 15:30:00,107.0,103.198,51.535,30.736 +2020-04-22 15:45:00,105.01,102.72399999999999,51.535,30.736 +2020-04-22 16:00:00,115.45,102.803,53.157,30.736 +2020-04-22 16:15:00,119.01,102.509,53.157,30.736 +2020-04-22 16:30:00,121.41,102.52,53.157,30.736 +2020-04-22 16:45:00,118.86,100.01100000000001,53.157,30.736 +2020-04-22 17:00:00,126.78,99.868,57.793,30.736 +2020-04-22 17:15:00,124.76,102.12,57.793,30.736 +2020-04-22 17:30:00,124.73,103.48,57.793,30.736 +2020-04-22 17:45:00,117.96,104.79799999999999,57.793,30.736 +2020-04-22 18:00:00,119.79,105.698,59.872,30.736 +2020-04-22 18:15:00,119.56,107.354,59.872,30.736 +2020-04-22 18:30:00,118.02,106.11200000000001,59.872,30.736 +2020-04-22 18:45:00,114.89,111.771,59.872,30.736 +2020-04-22 19:00:00,117.2,109.917,60.17100000000001,30.736 +2020-04-22 19:15:00,115.46,109.146,60.17100000000001,30.736 +2020-04-22 19:30:00,111.46,109.208,60.17100000000001,30.736 +2020-04-22 19:45:00,114.24,109.75299999999999,60.17100000000001,30.736 +2020-04-22 20:00:00,110.05,107.742,65.015,30.736 +2020-04-22 20:15:00,112.02,105.141,65.015,30.736 +2020-04-22 20:30:00,103.59,104.27600000000001,65.015,30.736 +2020-04-22 20:45:00,101.23,103.60600000000001,65.015,30.736 +2020-04-22 21:00:00,101.65,99.30799999999999,57.805,30.736 +2020-04-22 21:15:00,100.48,98.88799999999999,57.805,30.736 +2020-04-22 21:30:00,91.45,99.18,57.805,30.736 +2020-04-22 21:45:00,90.16,98.823,57.805,30.736 +2020-04-22 22:00:00,83.23,94.425,52.115,30.736 +2020-04-22 22:15:00,86.17,92.711,52.115,30.736 +2020-04-22 22:30:00,88.07,81.357,52.115,30.736 +2020-04-22 22:45:00,85.72,75.62899999999999,52.115,30.736 +2020-04-22 23:00:00,77.3,67.635,42.871,30.736 +2020-04-22 23:15:00,76.06,66.535,42.871,30.736 +2020-04-22 23:30:00,81.31,65.67,42.871,30.736 +2020-04-22 23:45:00,81.61,66.325,42.871,30.736 +2020-04-23 00:00:00,77.46,63.331,39.203,30.736 +2020-04-23 00:15:00,73.8,63.897,39.203,30.736 +2020-04-23 00:30:00,72.13,62.251000000000005,39.203,30.736 +2020-04-23 00:45:00,78.28,60.717,39.203,30.736 +2020-04-23 01:00:00,77.24,61.004,37.118,30.736 +2020-04-23 01:15:00,76.9,60.145,37.118,30.736 +2020-04-23 01:30:00,73.05,58.708,37.118,30.736 +2020-04-23 01:45:00,74.2,58.173,37.118,30.736 +2020-04-23 02:00:00,77.66,58.958,35.647,30.736 +2020-04-23 02:15:00,74.79,58.07,35.647,30.736 +2020-04-23 02:30:00,73.24,60.656000000000006,35.647,30.736 +2020-04-23 02:45:00,74.37,61.248000000000005,35.647,30.736 +2020-04-23 03:00:00,74.42,64.38600000000001,34.585,30.736 +2020-04-23 03:15:00,83.44,65.436,34.585,30.736 +2020-04-23 03:30:00,82.78,64.861,34.585,30.736 +2020-04-23 03:45:00,85.04,65.282,34.585,30.736 +2020-04-23 04:00:00,81.36,76.975,36.184,30.736 +2020-04-23 04:15:00,83.4,89.069,36.184,30.736 +2020-04-23 04:30:00,86.65,88.79899999999999,36.184,30.736 +2020-04-23 04:45:00,90.42,90.58200000000001,36.184,30.736 +2020-04-23 05:00:00,96.73,123.321,41.019,30.736 +2020-04-23 05:15:00,99.72,154.384,41.019,30.736 +2020-04-23 05:30:00,104.3,143.409,41.019,30.736 +2020-04-23 05:45:00,108.09,133.485,41.019,30.736 +2020-04-23 06:00:00,115.69,135.297,53.963,30.736 +2020-04-23 06:15:00,112.99,140.192,53.963,30.736 +2020-04-23 06:30:00,117.65,137.392,53.963,30.736 +2020-04-23 06:45:00,114.98,137.661,53.963,30.736 +2020-04-23 07:00:00,122.88,139.759,66.512,30.736 +2020-04-23 07:15:00,119.72,140.15200000000002,66.512,30.736 +2020-04-23 07:30:00,116.23,138.096,66.512,30.736 +2020-04-23 07:45:00,110.91,134.984,66.512,30.736 +2020-04-23 08:00:00,109.73,132.45600000000002,58.86,30.736 +2020-04-23 08:15:00,111.4,131.204,58.86,30.736 +2020-04-23 08:30:00,115.66,126.929,58.86,30.736 +2020-04-23 08:45:00,116.9,125.26,58.86,30.736 +2020-04-23 09:00:00,117.14,118.99799999999999,52.156000000000006,30.736 +2020-04-23 09:15:00,117.82,116.822,52.156000000000006,30.736 +2020-04-23 09:30:00,125.16,118.961,52.156000000000006,30.736 +2020-04-23 09:45:00,126.64,118.62700000000001,52.156000000000006,30.736 +2020-04-23 10:00:00,115.39,114.26899999999999,49.034,30.736 +2020-04-23 10:15:00,107.22,114.51,49.034,30.736 +2020-04-23 10:30:00,110.97,113.139,49.034,30.736 +2020-04-23 10:45:00,105.98,113.331,49.034,30.736 +2020-04-23 11:00:00,110.48,107.988,46.53,30.736 +2020-04-23 11:15:00,109.12,108.90700000000001,46.53,30.736 +2020-04-23 11:30:00,104.52,110.09200000000001,46.53,30.736 +2020-04-23 11:45:00,118.1,110.81299999999999,46.53,30.736 +2020-04-23 12:00:00,100.86,106.01799999999999,43.318000000000005,30.736 +2020-04-23 12:15:00,107.42,106.89200000000001,43.318000000000005,30.736 +2020-04-23 12:30:00,119.08,105.743,43.318000000000005,30.736 +2020-04-23 12:45:00,113.59,106.492,43.318000000000005,30.736 +2020-04-23 13:00:00,105.64,107.32799999999999,41.608000000000004,30.736 +2020-04-23 13:15:00,109.66,106.58,41.608000000000004,30.736 +2020-04-23 13:30:00,107.3,104.464,41.608000000000004,30.736 +2020-04-23 13:45:00,109.96,103.055,41.608000000000004,30.736 +2020-04-23 14:00:00,108.12,104.509,41.786,30.736 +2020-04-23 14:15:00,98.99,103.896,41.786,30.736 +2020-04-23 14:30:00,100.33,103.461,41.786,30.736 +2020-04-23 14:45:00,105.57,103.94200000000001,41.786,30.736 +2020-04-23 15:00:00,105.07,105.101,44.181999999999995,30.736 +2020-04-23 15:15:00,107.35,103.44200000000001,44.181999999999995,30.736 +2020-04-23 15:30:00,99.98,102.71700000000001,44.181999999999995,30.736 +2020-04-23 15:45:00,100.25,102.22399999999999,44.181999999999995,30.736 +2020-04-23 16:00:00,98.86,102.346,45.956,30.736 +2020-04-23 16:15:00,107.63,102.027,45.956,30.736 +2020-04-23 16:30:00,113.08,102.045,45.956,30.736 +2020-04-23 16:45:00,113.6,99.471,45.956,30.736 +2020-04-23 17:00:00,112.41,99.37799999999999,50.702,30.736 +2020-04-23 17:15:00,112.21,101.604,50.702,30.736 +2020-04-23 17:30:00,118.08,102.958,50.702,30.736 +2020-04-23 17:45:00,120.54,104.251,50.702,30.736 +2020-04-23 18:00:00,119.87,105.165,53.595,30.736 +2020-04-23 18:15:00,109.3,106.839,53.595,30.736 +2020-04-23 18:30:00,109.67,105.585,53.595,30.736 +2020-04-23 18:45:00,108.25,111.24799999999999,53.595,30.736 +2020-04-23 19:00:00,107.8,109.385,54.207,30.736 +2020-04-23 19:15:00,105.82,108.62,54.207,30.736 +2020-04-23 19:30:00,107.51,108.69200000000001,54.207,30.736 +2020-04-23 19:45:00,112.01,109.26,54.207,30.736 +2020-04-23 20:00:00,110.19,107.22,56.948,30.736 +2020-04-23 20:15:00,109.28,104.626,56.948,30.736 +2020-04-23 20:30:00,106.75,103.795,56.948,30.736 +2020-04-23 20:45:00,105.41,103.15899999999999,56.948,30.736 +2020-04-23 21:00:00,103.61,98.861,52.157,30.736 +2020-04-23 21:15:00,98.19,98.456,52.157,30.736 +2020-04-23 21:30:00,87.51,98.734,52.157,30.736 +2020-04-23 21:45:00,93.02,98.40700000000001,52.157,30.736 +2020-04-23 22:00:00,90.12,94.023,47.483000000000004,30.736 +2020-04-23 22:15:00,89.7,92.33200000000001,47.483000000000004,30.736 +2020-04-23 22:30:00,83.45,80.955,47.483000000000004,30.736 +2020-04-23 22:45:00,79.56,75.218,47.483000000000004,30.736 +2020-04-23 23:00:00,78.7,67.197,41.978,30.736 +2020-04-23 23:15:00,82.28,66.135,41.978,30.736 +2020-04-23 23:30:00,81.32,65.26899999999999,41.978,30.736 +2020-04-23 23:45:00,80.34,65.931,41.978,30.736 +2020-04-24 00:00:00,72.2,61.173,39.301,30.736 +2020-04-24 00:15:00,76.53,62.001999999999995,39.301,30.736 +2020-04-24 00:30:00,78.65,60.465,39.301,30.736 +2020-04-24 00:45:00,78.84,59.282,39.301,30.736 +2020-04-24 01:00:00,72.93,59.141999999999996,37.976,30.736 +2020-04-24 01:15:00,76.27,58.315,37.976,30.736 +2020-04-24 01:30:00,79.2,57.232,37.976,30.736 +2020-04-24 01:45:00,79.29,56.583999999999996,37.976,30.736 +2020-04-24 02:00:00,75.93,58.038999999999994,37.041,30.736 +2020-04-24 02:15:00,76.43,57.04,37.041,30.736 +2020-04-24 02:30:00,79.9,60.477,37.041,30.736 +2020-04-24 02:45:00,79.92,60.628,37.041,30.736 +2020-04-24 03:00:00,78.47,63.809,37.575,30.736 +2020-04-24 03:15:00,78.54,64.48100000000001,37.575,30.736 +2020-04-24 03:30:00,83.02,63.73,37.575,30.736 +2020-04-24 03:45:00,86.25,64.94,37.575,30.736 +2020-04-24 04:00:00,84.35,76.809,39.058,30.736 +2020-04-24 04:15:00,84.36,87.662,39.058,30.736 +2020-04-24 04:30:00,84.53,88.164,39.058,30.736 +2020-04-24 04:45:00,87.97,88.916,39.058,30.736 +2020-04-24 05:00:00,94.74,120.58200000000001,43.256,30.736 +2020-04-24 05:15:00,98.06,152.952,43.256,30.736 +2020-04-24 05:30:00,101.99,142.67,43.256,30.736 +2020-04-24 05:45:00,104.32,132.45,43.256,30.736 +2020-04-24 06:00:00,110.14,134.679,56.093999999999994,30.736 +2020-04-24 06:15:00,110.92,138.92700000000002,56.093999999999994,30.736 +2020-04-24 06:30:00,115.01,135.642,56.093999999999994,30.736 +2020-04-24 06:45:00,113.33,136.634,56.093999999999994,30.736 +2020-04-24 07:00:00,114.79,138.756,66.92699999999999,30.736 +2020-04-24 07:15:00,114.94,140.278,66.92699999999999,30.736 +2020-04-24 07:30:00,111.87,136.784,66.92699999999999,30.736 +2020-04-24 07:45:00,113.65,133.072,66.92699999999999,30.736 +2020-04-24 08:00:00,111.88,130.465,60.332,30.736 +2020-04-24 08:15:00,111.97,129.475,60.332,30.736 +2020-04-24 08:30:00,116.04,125.63600000000001,60.332,30.736 +2020-04-24 08:45:00,116.96,123.051,60.332,30.736 +2020-04-24 09:00:00,113.47,115.54,56.085,30.736 +2020-04-24 09:15:00,109.61,114.911,56.085,30.736 +2020-04-24 09:30:00,104.73,116.389,56.085,30.736 +2020-04-24 09:45:00,106.39,116.274,56.085,30.736 +2020-04-24 10:00:00,104.8,111.147,52.91,30.736 +2020-04-24 10:15:00,110.96,111.698,52.91,30.736 +2020-04-24 10:30:00,108.8,110.649,52.91,30.736 +2020-04-24 10:45:00,112.42,110.54799999999999,52.91,30.736 +2020-04-24 11:00:00,105.01,105.321,52.278999999999996,30.736 +2020-04-24 11:15:00,106.04,105.079,52.278999999999996,30.736 +2020-04-24 11:30:00,106.23,107.06299999999999,52.278999999999996,30.736 +2020-04-24 11:45:00,105.8,107.24700000000001,52.278999999999996,30.736 +2020-04-24 12:00:00,101.15,103.402,49.023999999999994,30.736 +2020-04-24 12:15:00,100.58,102.65100000000001,49.023999999999994,30.736 +2020-04-24 12:30:00,96.07,101.62299999999999,49.023999999999994,30.736 +2020-04-24 12:45:00,90.58,102.21799999999999,49.023999999999994,30.736 +2020-04-24 13:00:00,91.3,104.00399999999999,46.82,30.736 +2020-04-24 13:15:00,91.72,103.84899999999999,46.82,30.736 +2020-04-24 13:30:00,89.96,102.21700000000001,46.82,30.736 +2020-04-24 13:45:00,90.43,100.95299999999999,46.82,30.736 +2020-04-24 14:00:00,91.03,101.309,45.756,30.736 +2020-04-24 14:15:00,90.38,100.82799999999999,45.756,30.736 +2020-04-24 14:30:00,90.13,101.54700000000001,45.756,30.736 +2020-04-24 14:45:00,87.82,101.804,45.756,30.736 +2020-04-24 15:00:00,88.57,102.664,47.56,30.736 +2020-04-24 15:15:00,89.54,100.579,47.56,30.736 +2020-04-24 15:30:00,90.6,98.565,47.56,30.736 +2020-04-24 15:45:00,93.53,98.59200000000001,47.56,30.736 +2020-04-24 16:00:00,96.08,97.604,49.581,30.736 +2020-04-24 16:15:00,98.58,97.73100000000001,49.581,30.736 +2020-04-24 16:30:00,103.04,97.697,49.581,30.736 +2020-04-24 16:45:00,102.74,94.509,49.581,30.736 +2020-04-24 17:00:00,106.18,95.684,53.918,30.736 +2020-04-24 17:15:00,105.16,97.54299999999999,53.918,30.736 +2020-04-24 17:30:00,105.91,98.814,53.918,30.736 +2020-04-24 17:45:00,107.32,99.845,53.918,30.736 +2020-04-24 18:00:00,107.44,101.23700000000001,54.266000000000005,30.736 +2020-04-24 18:15:00,104.49,102.10700000000001,54.266000000000005,30.736 +2020-04-24 18:30:00,104.57,100.98899999999999,54.266000000000005,30.736 +2020-04-24 18:45:00,103.28,106.92399999999999,54.266000000000005,30.736 +2020-04-24 19:00:00,105.31,106.14399999999999,54.092,30.736 +2020-04-24 19:15:00,99.77,106.50299999999999,54.092,30.736 +2020-04-24 19:30:00,104.65,106.406,54.092,30.736 +2020-04-24 19:45:00,102.74,106.08,54.092,30.736 +2020-04-24 20:00:00,99.02,103.93700000000001,59.038999999999994,30.736 +2020-04-24 20:15:00,105.31,101.875,59.038999999999994,30.736 +2020-04-24 20:30:00,106.75,100.73700000000001,59.038999999999994,30.736 +2020-04-24 20:45:00,103.82,99.896,59.038999999999994,30.736 +2020-04-24 21:00:00,92.72,96.742,53.346000000000004,30.736 +2020-04-24 21:15:00,89.77,97.68700000000001,53.346000000000004,30.736 +2020-04-24 21:30:00,92.29,97.876,53.346000000000004,30.736 +2020-04-24 21:45:00,91.53,98.0,53.346000000000004,30.736 +2020-04-24 22:00:00,86.77,94.05799999999999,47.938,30.736 +2020-04-24 22:15:00,82.96,92.156,47.938,30.736 +2020-04-24 22:30:00,85.15,87.279,47.938,30.736 +2020-04-24 22:45:00,83.5,83.97200000000001,47.938,30.736 +2020-04-24 23:00:00,78.39,76.804,40.266,30.736 +2020-04-24 23:15:00,76.03,73.711,40.266,30.736 +2020-04-24 23:30:00,79.22,70.86399999999999,40.266,30.736 +2020-04-24 23:45:00,77.46,71.1,40.266,30.736 +2020-04-25 00:00:00,72.92,60.177,39.184,30.618000000000002 +2020-04-25 00:15:00,67.06,58.391000000000005,39.184,30.618000000000002 +2020-04-25 00:30:00,65.85,57.206,39.184,30.618000000000002 +2020-04-25 00:45:00,69.65,55.895,39.184,30.618000000000002 +2020-04-25 01:00:00,71.38,56.265,34.692,30.618000000000002 +2020-04-25 01:15:00,72.76,55.318000000000005,34.692,30.618000000000002 +2020-04-25 01:30:00,69.42,53.401,34.692,30.618000000000002 +2020-04-25 01:45:00,71.88,53.498000000000005,34.692,30.618000000000002 +2020-04-25 02:00:00,71.15,54.626000000000005,32.919000000000004,30.618000000000002 +2020-04-25 02:15:00,71.09,52.883,32.919000000000004,30.618000000000002 +2020-04-25 02:30:00,65.69,55.233999999999995,32.919000000000004,30.618000000000002 +2020-04-25 02:45:00,61.4,55.98,32.919000000000004,30.618000000000002 +2020-04-25 03:00:00,64.48,58.56100000000001,32.024,30.618000000000002 +2020-04-25 03:15:00,63.49,58.118,32.024,30.618000000000002 +2020-04-25 03:30:00,63.7,56.793,32.024,30.618000000000002 +2020-04-25 03:45:00,64.2,59.101000000000006,32.024,30.618000000000002 +2020-04-25 04:00:00,64.64,67.586,31.958000000000002,30.618000000000002 +2020-04-25 04:15:00,64.45,76.515,31.958000000000002,30.618000000000002 +2020-04-25 04:30:00,62.58,74.78,31.958000000000002,30.618000000000002 +2020-04-25 04:45:00,61.79,75.464,31.958000000000002,30.618000000000002 +2020-04-25 05:00:00,62.67,93.719,32.75,30.618000000000002 +2020-04-25 05:15:00,63.83,109.10600000000001,32.75,30.618000000000002 +2020-04-25 05:30:00,66.59,100.096,32.75,30.618000000000002 +2020-04-25 05:45:00,66.17,95.70100000000001,32.75,30.618000000000002 +2020-04-25 06:00:00,69.06,114.625,34.461999999999996,30.618000000000002 +2020-04-25 06:15:00,71.02,132.321,34.461999999999996,30.618000000000002 +2020-04-25 06:30:00,71.93,124.402,34.461999999999996,30.618000000000002 +2020-04-25 06:45:00,73.64,118.735,34.461999999999996,30.618000000000002 +2020-04-25 07:00:00,76.52,117.56200000000001,37.736,30.618000000000002 +2020-04-25 07:15:00,76.86,117.56,37.736,30.618000000000002 +2020-04-25 07:30:00,77.45,116.432,37.736,30.618000000000002 +2020-04-25 07:45:00,75.16,115.383,37.736,30.618000000000002 +2020-04-25 08:00:00,75.87,115.021,42.34,30.618000000000002 +2020-04-25 08:15:00,75.01,115.853,42.34,30.618000000000002 +2020-04-25 08:30:00,76.19,112.833,42.34,30.618000000000002 +2020-04-25 08:45:00,75.0,112.568,42.34,30.618000000000002 +2020-04-25 09:00:00,77.22,107.902,43.571999999999996,30.618000000000002 +2020-04-25 09:15:00,74.1,107.99700000000001,43.571999999999996,30.618000000000002 +2020-04-25 09:30:00,74.58,110.29799999999999,43.571999999999996,30.618000000000002 +2020-04-25 09:45:00,72.53,110.01700000000001,43.571999999999996,30.618000000000002 +2020-04-25 10:00:00,70.38,105.26,40.514,30.618000000000002 +2020-04-25 10:15:00,71.83,106.17,40.514,30.618000000000002 +2020-04-25 10:30:00,72.86,105.02600000000001,40.514,30.618000000000002 +2020-04-25 10:45:00,76.31,105.464,40.514,30.618000000000002 +2020-04-25 11:00:00,75.64,100.208,36.388000000000005,30.618000000000002 +2020-04-25 11:15:00,72.22,100.119,36.388000000000005,30.618000000000002 +2020-04-25 11:30:00,70.44,101.631,36.388000000000005,30.618000000000002 +2020-04-25 11:45:00,66.44,101.69,36.388000000000005,30.618000000000002 +2020-04-25 12:00:00,62.4,97.416,35.217,30.618000000000002 +2020-04-25 12:15:00,63.37,97.552,35.217,30.618000000000002 +2020-04-25 12:30:00,62.6,96.59299999999999,35.217,30.618000000000002 +2020-04-25 12:45:00,62.97,97.165,35.217,30.618000000000002 +2020-04-25 13:00:00,66.68,98.23899999999999,32.001999999999995,30.618000000000002 +2020-04-25 13:15:00,66.46,96.544,32.001999999999995,30.618000000000002 +2020-04-25 13:30:00,68.38,94.76,32.001999999999995,30.618000000000002 +2020-04-25 13:45:00,63.86,92.984,32.001999999999995,30.618000000000002 +2020-04-25 14:00:00,65.79,94.14200000000001,31.304000000000002,30.618000000000002 +2020-04-25 14:15:00,65.45,92.59,31.304000000000002,30.618000000000002 +2020-04-25 14:30:00,67.98,91.934,31.304000000000002,30.618000000000002 +2020-04-25 14:45:00,70.14,92.603,31.304000000000002,30.618000000000002 +2020-04-25 15:00:00,73.95,94.147,34.731,30.618000000000002 +2020-04-25 15:15:00,68.45,92.935,34.731,30.618000000000002 +2020-04-25 15:30:00,69.17,91.913,34.731,30.618000000000002 +2020-04-25 15:45:00,71.96,91.391,34.731,30.618000000000002 +2020-04-25 16:00:00,74.03,91.31200000000001,38.769,30.618000000000002 +2020-04-25 16:15:00,73.54,91.38,38.769,30.618000000000002 +2020-04-25 16:30:00,77.94,91.461,38.769,30.618000000000002 +2020-04-25 16:45:00,77.97,88.696,38.769,30.618000000000002 +2020-04-25 17:00:00,79.95,88.99700000000001,44.928000000000004,30.618000000000002 +2020-04-25 17:15:00,79.81,90.412,44.928000000000004,30.618000000000002 +2020-04-25 17:30:00,81.88,91.541,44.928000000000004,30.618000000000002 +2020-04-25 17:45:00,79.84,92.62799999999999,44.928000000000004,30.618000000000002 +2020-04-25 18:00:00,81.95,94.76899999999999,47.786,30.618000000000002 +2020-04-25 18:15:00,80.8,97.704,47.786,30.618000000000002 +2020-04-25 18:30:00,80.62,98.166,47.786,30.618000000000002 +2020-04-25 18:45:00,81.58,100.06200000000001,47.786,30.618000000000002 +2020-04-25 19:00:00,82.25,98.82600000000001,47.463,30.618000000000002 +2020-04-25 19:15:00,81.6,98.26799999999999,47.463,30.618000000000002 +2020-04-25 19:30:00,78.79,99.083,47.463,30.618000000000002 +2020-04-25 19:45:00,81.11,99.837,47.463,30.618000000000002 +2020-04-25 20:00:00,79.47,99.35600000000001,43.735,30.618000000000002 +2020-04-25 20:15:00,80.74,98.01899999999999,43.735,30.618000000000002 +2020-04-25 20:30:00,77.33,96.155,43.735,30.618000000000002 +2020-04-25 20:45:00,77.88,96.339,43.735,30.618000000000002 +2020-04-25 21:00:00,76.72,93.439,40.346,30.618000000000002 +2020-04-25 21:15:00,73.54,94.40100000000001,40.346,30.618000000000002 +2020-04-25 21:30:00,70.17,95.348,40.346,30.618000000000002 +2020-04-25 21:45:00,69.94,94.93,40.346,30.618000000000002 +2020-04-25 22:00:00,66.61,91.697,39.323,30.618000000000002 +2020-04-25 22:15:00,65.99,91.294,39.323,30.618000000000002 +2020-04-25 22:30:00,63.67,89.425,39.323,30.618000000000002 +2020-04-25 22:45:00,62.7,87.34100000000001,39.323,30.618000000000002 +2020-04-25 23:00:00,58.03,80.985,33.716,30.618000000000002 +2020-04-25 23:15:00,58.89,77.345,33.716,30.618000000000002 +2020-04-25 23:30:00,57.34,75.185,33.716,30.618000000000002 +2020-04-25 23:45:00,57.63,74.384,33.716,30.618000000000002 +2020-04-26 00:00:00,53.94,60.93899999999999,28.703000000000003,30.618000000000002 +2020-04-26 00:15:00,54.77,58.223,28.703000000000003,30.618000000000002 +2020-04-26 00:30:00,54.11,56.742,28.703000000000003,30.618000000000002 +2020-04-26 00:45:00,54.48,55.713,28.703000000000003,30.618000000000002 +2020-04-26 01:00:00,53.07,56.185,26.171,30.618000000000002 +2020-04-26 01:15:00,53.77,55.653,26.171,30.618000000000002 +2020-04-26 01:30:00,53.29,53.88399999999999,26.171,30.618000000000002 +2020-04-26 01:45:00,53.5,53.567,26.171,30.618000000000002 +2020-04-26 02:00:00,52.56,54.358999999999995,25.326999999999998,30.618000000000002 +2020-04-26 02:15:00,53.08,52.657,25.326999999999998,30.618000000000002 +2020-04-26 02:30:00,53.0,55.581,25.326999999999998,30.618000000000002 +2020-04-26 02:45:00,53.59,56.367,25.326999999999998,30.618000000000002 +2020-04-26 03:00:00,53.57,59.565,24.311999999999998,30.618000000000002 +2020-04-26 03:15:00,54.44,59.016000000000005,24.311999999999998,30.618000000000002 +2020-04-26 03:30:00,54.58,57.88,24.311999999999998,30.618000000000002 +2020-04-26 03:45:00,54.59,59.626999999999995,24.311999999999998,30.618000000000002 +2020-04-26 04:00:00,55.25,67.928,25.33,30.618000000000002 +2020-04-26 04:15:00,56.11,76.015,25.33,30.618000000000002 +2020-04-26 04:30:00,54.4,75.235,25.33,30.618000000000002 +2020-04-26 04:45:00,54.42,75.722,25.33,30.618000000000002 +2020-04-26 05:00:00,53.23,92.29700000000001,25.309,30.618000000000002 +2020-04-26 05:15:00,54.13,105.882,25.309,30.618000000000002 +2020-04-26 05:30:00,53.73,96.536,25.309,30.618000000000002 +2020-04-26 05:45:00,55.13,92.115,25.309,30.618000000000002 +2020-04-26 06:00:00,56.83,109.336,25.945999999999998,30.618000000000002 +2020-04-26 06:15:00,59.16,126.744,25.945999999999998,30.618000000000002 +2020-04-26 06:30:00,59.67,117.788,25.945999999999998,30.618000000000002 +2020-04-26 06:45:00,60.03,110.90899999999999,25.945999999999998,30.618000000000002 +2020-04-26 07:00:00,62.06,111.11,27.87,30.618000000000002 +2020-04-26 07:15:00,63.4,109.559,27.87,30.618000000000002 +2020-04-26 07:30:00,62.84,108.62899999999999,27.87,30.618000000000002 +2020-04-26 07:45:00,61.39,107.17399999999999,27.87,30.618000000000002 +2020-04-26 08:00:00,61.91,108.194,32.114000000000004,30.618000000000002 +2020-04-26 08:15:00,60.92,109.787,32.114000000000004,30.618000000000002 +2020-04-26 08:30:00,59.14,108.181,32.114000000000004,30.618000000000002 +2020-04-26 08:45:00,58.58,108.95200000000001,32.114000000000004,30.618000000000002 +2020-04-26 09:00:00,57.18,103.98299999999999,34.222,30.618000000000002 +2020-04-26 09:15:00,58.03,104.06200000000001,34.222,30.618000000000002 +2020-04-26 09:30:00,58.37,106.571,34.222,30.618000000000002 +2020-04-26 09:45:00,60.08,106.904,34.222,30.618000000000002 +2020-04-26 10:00:00,59.48,103.84200000000001,34.544000000000004,30.618000000000002 +2020-04-26 10:15:00,60.83,105.178,34.544000000000004,30.618000000000002 +2020-04-26 10:30:00,61.23,104.53,34.544000000000004,30.618000000000002 +2020-04-26 10:45:00,61.1,104.56299999999999,34.544000000000004,30.618000000000002 +2020-04-26 11:00:00,62.69,99.581,36.368,30.618000000000002 +2020-04-26 11:15:00,58.41,99.315,36.368,30.618000000000002 +2020-04-26 11:30:00,56.55,100.654,36.368,30.618000000000002 +2020-04-26 11:45:00,57.98,101.229,36.368,30.618000000000002 +2020-04-26 12:00:00,52.84,97.259,32.433,30.618000000000002 +2020-04-26 12:15:00,51.83,98.132,32.433,30.618000000000002 +2020-04-26 12:30:00,49.53,96.414,32.433,30.618000000000002 +2020-04-26 12:45:00,50.89,96.04799999999999,32.433,30.618000000000002 +2020-04-26 13:00:00,45.96,96.559,28.971999999999998,30.618000000000002 +2020-04-26 13:15:00,46.59,96.266,28.971999999999998,30.618000000000002 +2020-04-26 13:30:00,46.54,93.7,28.971999999999998,30.618000000000002 +2020-04-26 13:45:00,49.1,92.21,28.971999999999998,30.618000000000002 +2020-04-26 14:00:00,49.36,94.28200000000001,25.531999999999996,30.618000000000002 +2020-04-26 14:15:00,47.82,93.669,25.531999999999996,30.618000000000002 +2020-04-26 14:30:00,46.91,92.95,25.531999999999996,30.618000000000002 +2020-04-26 14:45:00,50.24,92.745,25.531999999999996,30.618000000000002 +2020-04-26 15:00:00,48.5,93.521,25.766,30.618000000000002 +2020-04-26 15:15:00,49.38,92.24700000000001,25.766,30.618000000000002 +2020-04-26 15:30:00,52.0,91.41799999999999,25.766,30.618000000000002 +2020-04-26 15:45:00,56.72,91.45100000000001,25.766,30.618000000000002 +2020-04-26 16:00:00,58.03,91.37200000000001,29.232,30.618000000000002 +2020-04-26 16:15:00,58.12,91.04899999999999,29.232,30.618000000000002 +2020-04-26 16:30:00,61.98,91.90100000000001,29.232,30.618000000000002 +2020-04-26 16:45:00,66.32,89.185,29.232,30.618000000000002 +2020-04-26 17:00:00,69.92,89.704,37.431,30.618000000000002 +2020-04-26 17:15:00,72.25,91.865,37.431,30.618000000000002 +2020-04-26 17:30:00,71.68,93.652,37.431,30.618000000000002 +2020-04-26 17:45:00,72.65,96.22200000000001,37.431,30.618000000000002 +2020-04-26 18:00:00,74.1,98.405,41.251999999999995,30.618000000000002 +2020-04-26 18:15:00,73.58,101.804,41.251999999999995,30.618000000000002 +2020-04-26 18:30:00,79.83,100.978,41.251999999999995,30.618000000000002 +2020-04-26 18:45:00,81.73,103.944,41.251999999999995,30.618000000000002 +2020-04-26 19:00:00,82.6,103.89399999999999,41.784,30.618000000000002 +2020-04-26 19:15:00,74.77,102.92299999999999,41.784,30.618000000000002 +2020-04-26 19:30:00,78.18,103.49700000000001,41.784,30.618000000000002 +2020-04-26 19:45:00,82.92,104.728,41.784,30.618000000000002 +2020-04-26 20:00:00,80.12,104.32799999999999,40.804,30.618000000000002 +2020-04-26 20:15:00,79.67,103.42299999999999,40.804,30.618000000000002 +2020-04-26 20:30:00,83.74,102.774,40.804,30.618000000000002 +2020-04-26 20:45:00,87.56,101.26,40.804,30.618000000000002 +2020-04-26 21:00:00,86.73,96.89200000000001,38.379,30.618000000000002 +2020-04-26 21:15:00,77.37,97.345,38.379,30.618000000000002 +2020-04-26 21:30:00,78.96,98.001,38.379,30.618000000000002 +2020-04-26 21:45:00,72.49,97.919,38.379,30.618000000000002 +2020-04-26 22:00:00,71.02,95.426,37.87,30.618000000000002 +2020-04-26 22:15:00,74.6,93.542,37.87,30.618000000000002 +2020-04-26 22:30:00,74.46,89.67299999999999,37.87,30.618000000000002 +2020-04-26 22:45:00,77.49,86.29799999999999,37.87,30.618000000000002 +2020-04-26 23:00:00,67.34,78.266,33.332,30.618000000000002 +2020-04-26 23:15:00,67.41,76.46300000000001,33.332,30.618000000000002 +2020-04-26 23:30:00,65.25,74.368,33.332,30.618000000000002 +2020-04-26 23:45:00,71.84,74.14,33.332,30.618000000000002 +2020-04-27 00:00:00,65.34,63.676,34.698,30.736 +2020-04-27 00:15:00,68.11,62.993,34.698,30.736 +2020-04-27 00:30:00,62.17,61.31,34.698,30.736 +2020-04-27 00:45:00,65.5,59.75,34.698,30.736 +2020-04-27 01:00:00,67.94,60.476000000000006,32.889,30.736 +2020-04-27 01:15:00,68.09,59.667,32.889,30.736 +2020-04-27 01:30:00,64.87,58.151,32.889,30.736 +2020-04-27 01:45:00,62.39,57.826,32.889,30.736 +2020-04-27 02:00:00,60.51,58.898999999999994,32.06,30.736 +2020-04-27 02:15:00,61.52,57.211000000000006,32.06,30.736 +2020-04-27 02:30:00,63.11,60.428999999999995,32.06,30.736 +2020-04-27 02:45:00,65.0,60.816,32.06,30.736 +2020-04-27 03:00:00,70.94,64.986,30.515,30.736 +2020-04-27 03:15:00,71.15,65.718,30.515,30.736 +2020-04-27 03:30:00,66.39,64.915,30.515,30.736 +2020-04-27 03:45:00,68.04,66.095,30.515,30.736 +2020-04-27 04:00:00,69.68,78.61399999999999,31.436,30.736 +2020-04-27 04:15:00,70.3,90.708,31.436,30.736 +2020-04-27 04:30:00,70.44,90.67,31.436,30.736 +2020-04-27 04:45:00,74.91,91.48200000000001,31.436,30.736 +2020-04-27 05:00:00,81.04,120.303,38.997,30.736 +2020-04-27 05:15:00,85.59,150.614,38.997,30.736 +2020-04-27 05:30:00,88.32,140.344,38.997,30.736 +2020-04-27 05:45:00,87.62,131.126,38.997,30.736 +2020-04-27 06:00:00,93.16,132.86700000000002,54.97,30.736 +2020-04-27 06:15:00,90.44,136.747,54.97,30.736 +2020-04-27 06:30:00,94.92,134.424,54.97,30.736 +2020-04-27 06:45:00,93.91,135.476,54.97,30.736 +2020-04-27 07:00:00,97.72,137.559,66.032,30.736 +2020-04-27 07:15:00,94.82,138.159,66.032,30.736 +2020-04-27 07:30:00,94.52,136.225,66.032,30.736 +2020-04-27 07:45:00,94.9,133.67700000000002,66.032,30.736 +2020-04-27 08:00:00,92.49,131.08700000000002,59.941,30.736 +2020-04-27 08:15:00,91.85,130.80700000000002,59.941,30.736 +2020-04-27 08:30:00,93.31,126.40799999999999,59.941,30.736 +2020-04-27 08:45:00,92.94,125.59700000000001,59.941,30.736 +2020-04-27 09:00:00,92.84,119.57600000000001,54.016000000000005,30.736 +2020-04-27 09:15:00,91.53,116.648,54.016000000000005,30.736 +2020-04-27 09:30:00,89.3,118.041,54.016000000000005,30.736 +2020-04-27 09:45:00,89.57,117.095,54.016000000000005,30.736 +2020-04-27 10:00:00,89.14,113.96600000000001,50.63,30.736 +2020-04-27 10:15:00,98.83,115.074,50.63,30.736 +2020-04-27 10:30:00,93.52,113.675,50.63,30.736 +2020-04-27 10:45:00,89.12,113.164,50.63,30.736 +2020-04-27 11:00:00,89.55,106.956,49.951,30.736 +2020-04-27 11:15:00,91.6,107.90700000000001,49.951,30.736 +2020-04-27 11:30:00,86.24,110.45299999999999,49.951,30.736 +2020-04-27 11:45:00,87.15,111.12,49.951,30.736 +2020-04-27 12:00:00,88.45,107.34700000000001,46.913000000000004,30.736 +2020-04-27 12:15:00,99.89,108.29299999999999,46.913000000000004,30.736 +2020-04-27 12:30:00,92.92,106.042,46.913000000000004,30.736 +2020-04-27 12:45:00,89.09,106.566,46.913000000000004,30.736 +2020-04-27 13:00:00,96.03,108.00299999999999,47.093999999999994,30.736 +2020-04-27 13:15:00,84.25,106.37299999999999,47.093999999999994,30.736 +2020-04-27 13:30:00,77.31,103.604,47.093999999999994,30.736 +2020-04-27 13:45:00,88.26,102.666,47.093999999999994,30.736 +2020-04-27 14:00:00,85.89,103.928,46.678000000000004,30.736 +2020-04-27 14:15:00,80.66,103.271,46.678000000000004,30.736 +2020-04-27 14:30:00,80.46,102.094,46.678000000000004,30.736 +2020-04-27 14:45:00,81.74,103.096,46.678000000000004,30.736 +2020-04-27 15:00:00,82.97,104.74700000000001,47.715,30.736 +2020-04-27 15:15:00,81.62,102.262,47.715,30.736 +2020-04-27 15:30:00,82.55,101.359,47.715,30.736 +2020-04-27 15:45:00,85.7,100.82799999999999,47.715,30.736 +2020-04-27 16:00:00,85.53,101.301,49.81100000000001,30.736 +2020-04-27 16:15:00,85.86,100.56299999999999,49.81100000000001,30.736 +2020-04-27 16:30:00,88.12,100.47,49.81100000000001,30.736 +2020-04-27 16:45:00,89.52,97.03399999999999,49.81100000000001,30.736 +2020-04-27 17:00:00,93.73,96.706,55.591,30.736 +2020-04-27 17:15:00,92.69,98.552,55.591,30.736 +2020-04-27 17:30:00,95.38,99.787,55.591,30.736 +2020-04-27 17:45:00,94.15,101.258,55.591,30.736 +2020-04-27 18:00:00,95.38,102.904,56.523,30.736 +2020-04-27 18:15:00,92.93,103.962,56.523,30.736 +2020-04-27 18:30:00,92.0,102.98899999999999,56.523,30.736 +2020-04-27 18:45:00,90.2,108.369,56.523,30.736 +2020-04-27 19:00:00,90.92,107.20299999999999,56.044,30.736 +2020-04-27 19:15:00,86.2,106.48899999999999,56.044,30.736 +2020-04-27 19:30:00,85.27,107.094,56.044,30.736 +2020-04-27 19:45:00,86.87,107.507,56.044,30.736 +2020-04-27 20:00:00,84.36,105.01899999999999,61.715,30.736 +2020-04-27 20:15:00,83.51,103.73299999999999,61.715,30.736 +2020-04-27 20:30:00,80.49,102.479,61.715,30.736 +2020-04-27 20:45:00,78.45,101.991,61.715,30.736 +2020-04-27 21:00:00,72.63,97.50399999999999,56.24,30.736 +2020-04-27 21:15:00,73.27,97.652,56.24,30.736 +2020-04-27 21:30:00,68.74,98.13600000000001,56.24,30.736 +2020-04-27 21:45:00,67.7,97.67399999999999,56.24,30.736 +2020-04-27 22:00:00,63.52,92.344,50.437,30.736 +2020-04-27 22:15:00,63.05,91.10600000000001,50.437,30.736 +2020-04-27 22:30:00,60.88,79.39,50.437,30.736 +2020-04-27 22:45:00,60.19,73.476,50.437,30.736 +2020-04-27 23:00:00,81.41,65.845,42.756,30.736 +2020-04-27 23:15:00,80.65,64.377,42.756,30.736 +2020-04-27 23:30:00,78.41,63.692,42.756,30.736 +2020-04-27 23:45:00,78.52,64.492,42.756,30.736 +2020-04-28 00:00:00,77.46,61.4,39.857,30.736 +2020-04-28 00:15:00,76.93,62.035,39.857,30.736 +2020-04-28 00:30:00,76.12,60.358000000000004,39.857,30.736 +2020-04-28 00:45:00,76.92,58.856,39.857,30.736 +2020-04-28 01:00:00,78.02,59.108999999999995,37.233000000000004,30.736 +2020-04-28 01:15:00,78.26,58.13,37.233000000000004,30.736 +2020-04-28 01:30:00,75.49,56.593,37.233000000000004,30.736 +2020-04-28 01:45:00,75.85,56.073,37.233000000000004,30.736 +2020-04-28 02:00:00,78.67,56.803000000000004,35.856,30.736 +2020-04-28 02:15:00,78.88,55.836999999999996,35.856,30.736 +2020-04-28 02:30:00,73.42,58.508,35.856,30.736 +2020-04-28 02:45:00,73.56,59.13,35.856,30.736 +2020-04-28 03:00:00,74.82,62.342,34.766999999999996,30.736 +2020-04-28 03:15:00,76.05,63.271,34.766999999999996,30.736 +2020-04-28 03:30:00,79.51,62.683,34.766999999999996,30.736 +2020-04-28 03:45:00,83.12,63.217,34.766999999999996,30.736 +2020-04-28 04:00:00,84.91,74.73899999999999,35.468,30.736 +2020-04-28 04:15:00,84.94,86.62700000000001,35.468,30.736 +2020-04-28 04:30:00,87.14,86.353,35.468,30.736 +2020-04-28 04:45:00,88.32,88.088,35.468,30.736 +2020-04-28 05:00:00,95.45,120.323,40.399,30.736 +2020-04-28 05:15:00,98.01,150.866,40.399,30.736 +2020-04-28 05:30:00,100.42,140.02200000000002,40.399,30.736 +2020-04-28 05:45:00,102.69,130.343,40.399,30.736 +2020-04-28 06:00:00,107.5,132.263,54.105,30.736 +2020-04-28 06:15:00,108.18,137.054,54.105,30.736 +2020-04-28 06:30:00,110.19,134.178,54.105,30.736 +2020-04-28 06:45:00,110.95,134.42600000000002,54.105,30.736 +2020-04-28 07:00:00,112.62,136.516,63.083,30.736 +2020-04-28 07:15:00,112.53,136.851,63.083,30.736 +2020-04-28 07:30:00,113.87,134.61700000000002,63.083,30.736 +2020-04-28 07:45:00,112.32,131.526,63.083,30.736 +2020-04-28 08:00:00,110.21,128.942,57.254,30.736 +2020-04-28 08:15:00,108.73,127.866,57.254,30.736 +2020-04-28 08:30:00,109.99,123.456,57.254,30.736 +2020-04-28 08:45:00,111.24,121.929,57.254,30.736 +2020-04-28 09:00:00,110.59,115.686,51.395,30.736 +2020-04-28 09:15:00,110.8,113.535,51.395,30.736 +2020-04-28 09:30:00,110.96,115.749,51.395,30.736 +2020-04-28 09:45:00,110.3,115.579,51.395,30.736 +2020-04-28 10:00:00,108.23,111.265,48.201,30.736 +2020-04-28 10:15:00,110.0,111.735,48.201,30.736 +2020-04-28 10:30:00,108.55,110.477,48.201,30.736 +2020-04-28 10:45:00,108.8,110.765,48.201,30.736 +2020-04-28 11:00:00,106.28,105.389,46.133,30.736 +2020-04-28 11:15:00,105.09,106.42,46.133,30.736 +2020-04-28 11:30:00,103.75,107.59899999999999,46.133,30.736 +2020-04-28 11:45:00,106.98,108.41,46.133,30.736 +2020-04-28 12:00:00,106.37,103.759,44.243,30.736 +2020-04-28 12:15:00,109.77,104.667,44.243,30.736 +2020-04-28 12:30:00,108.48,103.306,44.243,30.736 +2020-04-28 12:45:00,108.19,104.073,44.243,30.736 +2020-04-28 13:00:00,104.13,105.09,45.042,30.736 +2020-04-28 13:15:00,103.69,104.32,45.042,30.736 +2020-04-28 13:30:00,104.39,102.223,45.042,30.736 +2020-04-28 13:45:00,106.18,100.82600000000001,45.042,30.736 +2020-04-28 14:00:00,111.13,102.575,44.062,30.736 +2020-04-28 14:15:00,109.93,101.874,44.062,30.736 +2020-04-28 14:30:00,107.35,101.215,44.062,30.736 +2020-04-28 14:45:00,104.4,101.706,44.062,30.736 +2020-04-28 15:00:00,109.65,103.045,46.461999999999996,30.736 +2020-04-28 15:15:00,109.95,101.281,46.461999999999996,30.736 +2020-04-28 15:30:00,109.44,100.34299999999999,46.461999999999996,30.736 +2020-04-28 15:45:00,108.73,99.764,46.461999999999996,30.736 +2020-04-28 16:00:00,108.15,100.094,48.802,30.736 +2020-04-28 16:15:00,109.08,99.656,48.802,30.736 +2020-04-28 16:30:00,111.59,99.704,48.802,30.736 +2020-04-28 16:45:00,112.11,96.806,48.802,30.736 +2020-04-28 17:00:00,114.23,96.969,55.672,30.736 +2020-04-28 17:15:00,114.12,99.066,55.672,30.736 +2020-04-28 17:30:00,116.32,100.38799999999999,55.672,30.736 +2020-04-28 17:45:00,115.06,101.552,55.672,30.736 +2020-04-28 18:00:00,116.52,102.531,57.006,30.736 +2020-04-28 18:15:00,116.37,104.29299999999999,57.006,30.736 +2020-04-28 18:30:00,116.22,102.975,57.006,30.736 +2020-04-28 18:45:00,113.05,108.661,57.006,30.736 +2020-04-28 19:00:00,109.57,106.756,57.148,30.736 +2020-04-28 19:15:00,107.41,106.01799999999999,57.148,30.736 +2020-04-28 19:30:00,111.56,106.139,57.148,30.736 +2020-04-28 19:45:00,110.69,106.81299999999999,57.148,30.736 +2020-04-28 20:00:00,106.84,104.641,61.895,30.736 +2020-04-28 20:15:00,104.83,102.081,61.895,30.736 +2020-04-28 20:30:00,102.09,101.42,61.895,30.736 +2020-04-28 20:45:00,105.29,100.936,61.895,30.736 +2020-04-28 21:00:00,99.4,96.655,54.78,30.736 +2020-04-28 21:15:00,99.25,96.321,54.78,30.736 +2020-04-28 21:30:00,93.24,96.527,54.78,30.736 +2020-04-28 21:45:00,89.26,96.34100000000001,54.78,30.736 +2020-04-28 22:00:00,87.28,92.029,50.76,30.736 +2020-04-28 22:15:00,89.13,90.45700000000001,50.76,30.736 +2020-04-28 22:30:00,86.27,78.956,50.76,30.736 +2020-04-28 22:45:00,83.49,73.17399999999999,50.76,30.736 +2020-04-28 23:00:00,71.4,65.023,44.162,30.736 +2020-04-28 23:15:00,70.29,64.152,44.162,30.736 +2020-04-28 23:30:00,75.32,63.277,44.162,30.736 +2020-04-28 23:45:00,78.15,63.973,44.162,30.736 +2020-04-29 00:00:00,73.93,61.016999999999996,39.061,30.736 +2020-04-29 00:15:00,68.61,61.667,39.061,30.736 +2020-04-29 00:30:00,69.83,59.983999999999995,39.061,30.736 +2020-04-29 00:45:00,74.41,58.489,39.061,30.736 +2020-04-29 01:00:00,73.44,58.735,35.795,30.736 +2020-04-29 01:15:00,71.44,57.733000000000004,35.795,30.736 +2020-04-29 01:30:00,73.21,56.176,35.795,30.736 +2020-04-29 01:45:00,74.77,55.658,35.795,30.736 +2020-04-29 02:00:00,72.45,56.379,33.316,30.736 +2020-04-29 02:15:00,65.54,55.397,33.316,30.736 +2020-04-29 02:30:00,66.56,58.082,33.316,30.736 +2020-04-29 02:45:00,70.87,58.713,33.316,30.736 +2020-04-29 03:00:00,70.07,61.93899999999999,32.803000000000004,30.736 +2020-04-29 03:15:00,76.58,62.843,32.803000000000004,30.736 +2020-04-29 03:30:00,78.46,62.253,32.803000000000004,30.736 +2020-04-29 03:45:00,77.53,62.809,32.803000000000004,30.736 +2020-04-29 04:00:00,79.53,74.297,34.235,30.736 +2020-04-29 04:15:00,80.34,86.14399999999999,34.235,30.736 +2020-04-29 04:30:00,82.42,85.869,34.235,30.736 +2020-04-29 04:45:00,86.48,87.594,34.235,30.736 +2020-04-29 05:00:00,93.77,119.73,38.65,30.736 +2020-04-29 05:15:00,97.73,150.167,38.65,30.736 +2020-04-29 05:30:00,101.06,139.352,38.65,30.736 +2020-04-29 05:45:00,102.22,129.722,38.65,30.736 +2020-04-29 06:00:00,110.99,131.662,54.951,30.736 +2020-04-29 06:15:00,110.65,136.43200000000002,54.951,30.736 +2020-04-29 06:30:00,113.7,133.542,54.951,30.736 +2020-04-29 06:45:00,116.43,133.786,54.951,30.736 +2020-04-29 07:00:00,121.5,135.873,67.328,30.736 +2020-04-29 07:15:00,121.34,136.19899999999998,67.328,30.736 +2020-04-29 07:30:00,121.78,133.929,67.328,30.736 +2020-04-29 07:45:00,122.25,130.845,67.328,30.736 +2020-04-29 08:00:00,122.93,128.249,60.23,30.736 +2020-04-29 08:15:00,125.82,127.21,60.23,30.736 +2020-04-29 08:30:00,127.65,122.774,60.23,30.736 +2020-04-29 08:45:00,128.25,121.274,60.23,30.736 +2020-04-29 09:00:00,129.04,115.035,56.845,30.736 +2020-04-29 09:15:00,129.51,112.889,56.845,30.736 +2020-04-29 09:30:00,126.37,115.118,56.845,30.736 +2020-04-29 09:45:00,124.86,114.979,56.845,30.736 +2020-04-29 10:00:00,121.63,110.675,53.832,30.736 +2020-04-29 10:15:00,121.03,111.19,53.832,30.736 +2020-04-29 10:30:00,122.36,109.954,53.832,30.736 +2020-04-29 10:45:00,116.08,110.26100000000001,53.832,30.736 +2020-04-29 11:00:00,109.95,104.87799999999999,53.225,30.736 +2020-04-29 11:15:00,114.24,105.931,53.225,30.736 +2020-04-29 11:30:00,109.62,107.11,53.225,30.736 +2020-04-29 11:45:00,101.59,107.93799999999999,53.225,30.736 +2020-04-29 12:00:00,99.09,103.316,50.676,30.736 +2020-04-29 12:15:00,97.93,104.23,50.676,30.736 +2020-04-29 12:30:00,97.46,102.82700000000001,50.676,30.736 +2020-04-29 12:45:00,105.95,103.596,50.676,30.736 +2020-04-29 13:00:00,105.96,104.65100000000001,50.646,30.736 +2020-04-29 13:15:00,108.82,103.876,50.646,30.736 +2020-04-29 13:30:00,106.36,101.78200000000001,50.646,30.736 +2020-04-29 13:45:00,107.35,100.38799999999999,50.646,30.736 +2020-04-29 14:00:00,108.22,102.194,50.786,30.736 +2020-04-29 14:15:00,105.88,101.476,50.786,30.736 +2020-04-29 14:30:00,100.87,100.773,50.786,30.736 +2020-04-29 14:45:00,97.81,101.265,50.786,30.736 +2020-04-29 15:00:00,99.63,102.64,51.535,30.736 +2020-04-29 15:15:00,95.3,100.855,51.535,30.736 +2020-04-29 15:30:00,104.15,99.876,51.535,30.736 +2020-04-29 15:45:00,108.0,99.28,51.535,30.736 +2020-04-29 16:00:00,107.58,99.65299999999999,53.157,30.736 +2020-04-29 16:15:00,105.42,99.19,53.157,30.736 +2020-04-29 16:30:00,106.46,99.243,53.157,30.736 +2020-04-29 16:45:00,108.12,96.28399999999999,53.157,30.736 +2020-04-29 17:00:00,109.38,96.49600000000001,57.793,30.736 +2020-04-29 17:15:00,106.94,98.56700000000001,57.793,30.736 +2020-04-29 17:30:00,107.97,99.882,57.793,30.736 +2020-04-29 17:45:00,107.1,101.01899999999999,57.793,30.736 +2020-04-29 18:00:00,108.18,102.01299999999999,59.872,30.736 +2020-04-29 18:15:00,106.95,103.79,59.872,30.736 +2020-04-29 18:30:00,114.64,102.459,59.872,30.736 +2020-04-29 18:45:00,114.76,108.149,59.872,30.736 +2020-04-29 19:00:00,112.55,106.23700000000001,60.17100000000001,30.736 +2020-04-29 19:15:00,104.09,105.50299999999999,60.17100000000001,30.736 +2020-04-29 19:30:00,105.95,105.635,60.17100000000001,30.736 +2020-04-29 19:45:00,108.76,106.331,60.17100000000001,30.736 +2020-04-29 20:00:00,102.11,104.131,65.015,30.736 +2020-04-29 20:15:00,102.79,101.57799999999999,65.015,30.736 +2020-04-29 20:30:00,101.29,100.95100000000001,65.015,30.736 +2020-04-29 20:45:00,104.92,100.49700000000001,65.015,30.736 +2020-04-29 21:00:00,99.96,96.219,57.805,30.736 +2020-04-29 21:15:00,99.95,95.9,57.805,30.736 +2020-04-29 21:30:00,91.66,96.09100000000001,57.805,30.736 +2020-04-29 21:45:00,92.16,95.932,57.805,30.736 +2020-04-29 22:00:00,89.14,91.635,52.115,30.736 +2020-04-29 22:15:00,89.74,90.085,52.115,30.736 +2020-04-29 22:30:00,84.06,78.56,52.115,30.736 +2020-04-29 22:45:00,79.26,72.768,52.115,30.736 +2020-04-29 23:00:00,72.0,64.59100000000001,42.871,30.736 +2020-04-29 23:15:00,77.84,63.75899999999999,42.871,30.736 +2020-04-29 23:30:00,81.54,62.883,42.871,30.736 +2020-04-29 23:45:00,81.39,63.585,42.871,30.736 +2020-04-30 00:00:00,75.37,60.636,39.203,30.736 +2020-04-30 00:15:00,69.88,61.299,39.203,30.736 +2020-04-30 00:30:00,76.11,59.611999999999995,39.203,30.736 +2020-04-30 00:45:00,78.46,58.123000000000005,39.203,30.736 +2020-04-30 01:00:00,76.07,58.363,37.118,30.736 +2020-04-30 01:15:00,74.99,57.338,37.118,30.736 +2020-04-30 01:30:00,69.98,55.761,37.118,30.736 +2020-04-30 01:45:00,72.88,55.246,37.118,30.736 +2020-04-30 02:00:00,77.43,55.956,35.647,30.736 +2020-04-30 02:15:00,77.87,54.958999999999996,35.647,30.736 +2020-04-30 02:30:00,70.93,57.66,35.647,30.736 +2020-04-30 02:45:00,72.1,58.297,35.647,30.736 +2020-04-30 03:00:00,71.36,61.538000000000004,34.585,30.736 +2020-04-30 03:15:00,72.29,62.419,34.585,30.736 +2020-04-30 03:30:00,75.56,61.825,34.585,30.736 +2020-04-30 03:45:00,75.33,62.405,34.585,30.736 +2020-04-30 04:00:00,79.58,73.857,36.184,30.736 +2020-04-30 04:15:00,81.32,85.664,36.184,30.736 +2020-04-30 04:30:00,83.16,85.387,36.184,30.736 +2020-04-30 04:45:00,86.22,87.103,36.184,30.736 +2020-04-30 05:00:00,94.0,119.139,41.019,30.736 +2020-04-30 05:15:00,94.93,149.472,41.019,30.736 +2020-04-30 05:30:00,98.47,138.685,41.019,30.736 +2020-04-30 05:45:00,103.17,129.10299999999998,41.019,30.736 +2020-04-30 06:00:00,110.4,131.064,53.963,30.736 +2020-04-30 06:15:00,111.54,135.813,53.963,30.736 +2020-04-30 06:30:00,114.87,132.909,53.963,30.736 +2020-04-30 06:45:00,116.61,133.149,53.963,30.736 +2020-04-30 07:00:00,121.28,135.233,66.512,30.736 +2020-04-30 07:15:00,121.03,135.549,66.512,30.736 +2020-04-30 07:30:00,122.18,133.245,66.512,30.736 +2020-04-30 07:45:00,121.34,130.166,66.512,30.736 +2020-04-30 08:00:00,120.47,127.561,58.86,30.736 +2020-04-30 08:15:00,121.39,126.557,58.86,30.736 +2020-04-30 08:30:00,122.76,122.095,58.86,30.736 +2020-04-30 08:45:00,121.08,120.624,58.86,30.736 +2020-04-30 09:00:00,121.88,114.389,52.156000000000006,30.736 +2020-04-30 09:15:00,123.85,112.24799999999999,52.156000000000006,30.736 +2020-04-30 09:30:00,124.67,114.49,52.156000000000006,30.736 +2020-04-30 09:45:00,124.74,114.385,52.156000000000006,30.736 +2020-04-30 10:00:00,125.1,110.089,49.034,30.736 +2020-04-30 10:15:00,125.31,110.649,49.034,30.736 +2020-04-30 10:30:00,126.21,109.434,49.034,30.736 +2020-04-30 10:45:00,124.71,109.76,49.034,30.736 +2020-04-30 11:00:00,121.6,104.37200000000001,46.53,30.736 +2020-04-30 11:15:00,123.88,105.447,46.53,30.736 +2020-04-30 11:30:00,120.2,106.624,46.53,30.736 +2020-04-30 11:45:00,119.8,107.46799999999999,46.53,30.736 +2020-04-30 12:00:00,110.03,102.876,43.318000000000005,30.736 +2020-04-30 12:15:00,106.44,103.795,43.318000000000005,30.736 +2020-04-30 12:30:00,100.78,102.351,43.318000000000005,30.736 +2020-04-30 12:45:00,99.22,103.12299999999999,43.318000000000005,30.736 +2020-04-30 13:00:00,106.85,104.212,41.608000000000004,30.736 +2020-04-30 13:15:00,112.1,103.434,41.608000000000004,30.736 +2020-04-30 13:30:00,101.38,101.345,41.608000000000004,30.736 +2020-04-30 13:45:00,100.96,99.954,41.608000000000004,30.736 +2020-04-30 14:00:00,101.38,101.816,41.786,30.736 +2020-04-30 14:15:00,104.57,101.083,41.786,30.736 +2020-04-30 14:30:00,102.6,100.335,41.786,30.736 +2020-04-30 14:45:00,105.7,100.82799999999999,41.786,30.736 +2020-04-30 15:00:00,101.95,102.238,44.181999999999995,30.736 +2020-04-30 15:15:00,104.28,100.432,44.181999999999995,30.736 +2020-04-30 15:30:00,101.87,99.412,44.181999999999995,30.736 +2020-04-30 15:45:00,103.04,98.79899999999999,44.181999999999995,30.736 +2020-04-30 16:00:00,109.28,99.213,45.956,30.736 +2020-04-30 16:15:00,109.2,98.727,45.956,30.736 +2020-04-30 16:30:00,110.26,98.787,45.956,30.736 +2020-04-30 16:45:00,111.89,95.76299999999999,45.956,30.736 +2020-04-30 17:00:00,113.27,96.027,50.702,30.736 +2020-04-30 17:15:00,113.08,98.072,50.702,30.736 +2020-04-30 17:30:00,112.36,99.37899999999999,50.702,30.736 +2020-04-30 17:45:00,111.24,100.491,50.702,30.736 +2020-04-30 18:00:00,111.37,101.49600000000001,53.595,30.736 +2020-04-30 18:15:00,110.86,103.29,53.595,30.736 +2020-04-30 18:30:00,118.75,101.945,53.595,30.736 +2020-04-30 18:45:00,118.09,107.64,53.595,30.736 +2020-04-30 19:00:00,113.08,105.721,54.207,30.736 +2020-04-30 19:15:00,103.48,104.993,54.207,30.736 +2020-04-30 19:30:00,104.39,105.134,54.207,30.736 +2020-04-30 19:45:00,104.48,105.85,54.207,30.736 +2020-04-30 20:00:00,102.44,103.625,56.948,30.736 +2020-04-30 20:15:00,99.4,101.079,56.948,30.736 +2020-04-30 20:30:00,98.53,100.484,56.948,30.736 +2020-04-30 20:45:00,100.91,100.059,56.948,30.736 +2020-04-30 21:00:00,92.46,95.787,52.157,30.736 +2020-04-30 21:15:00,95.11,95.48200000000001,52.157,30.736 +2020-04-30 21:30:00,94.9,95.65799999999999,52.157,30.736 +2020-04-30 21:45:00,93.18,95.525,52.157,30.736 +2020-04-30 22:00:00,90.2,91.242,47.483000000000004,30.736 +2020-04-30 22:15:00,83.38,89.715,47.483000000000004,30.736 +2020-04-30 22:30:00,79.85,78.164,47.483000000000004,30.736 +2020-04-30 22:45:00,83.09,72.363,47.483000000000004,30.736 +2020-04-30 23:00:00,58.59,64.16199999999999,41.978,30.736 +2020-04-30 23:15:00,56.65,63.367,41.978,30.736 +2020-04-30 23:30:00,56.38,62.49100000000001,41.978,30.736 +2020-04-30 23:45:00,55.11,63.199,41.978,30.736 +2020-05-01 00:00:00,50.49,50.163000000000004,18.527,29.662 +2020-05-01 00:15:00,53.64,47.663999999999994,18.527,29.662 +2020-05-01 00:30:00,52.6,46.467,18.527,29.662 +2020-05-01 00:45:00,52.95,45.35,18.527,29.662 +2020-05-01 01:00:00,48.0,45.506,16.348,29.662 +2020-05-01 01:15:00,51.98,45.135,16.348,29.662 +2020-05-01 01:30:00,48.94,43.453,16.348,29.662 +2020-05-01 01:45:00,52.32,43.106,16.348,29.662 +2020-05-01 02:00:00,51.36,43.566,12.581,29.662 +2020-05-01 02:15:00,51.11,41.968,12.581,29.662 +2020-05-01 02:30:00,48.05,44.556999999999995,12.581,29.662 +2020-05-01 02:45:00,47.72,45.224,12.581,29.662 +2020-05-01 03:00:00,48.95,48.13399999999999,10.712,29.662 +2020-05-01 03:15:00,50.28,46.619,10.712,29.662 +2020-05-01 03:30:00,50.66,45.36600000000001,10.712,29.662 +2020-05-01 03:45:00,51.81,46.526,10.712,29.662 +2020-05-01 04:00:00,53.03,54.876000000000005,9.084,29.662 +2020-05-01 04:15:00,54.27,62.676,9.084,29.662 +2020-05-01 04:30:00,51.73,61.516999999999996,9.084,29.662 +2020-05-01 04:45:00,51.01,61.666000000000004,9.084,29.662 +2020-05-01 05:00:00,49.69,76.642,9.388,29.662 +2020-05-01 05:15:00,52.41,87.50299999999999,9.388,29.662 +2020-05-01 05:30:00,52.41,77.738,9.388,29.662 +2020-05-01 05:45:00,50.98,74.282,9.388,29.662 +2020-05-01 06:00:00,55.06,89.3,11.109000000000002,29.662 +2020-05-01 06:15:00,53.68,104.529,11.109000000000002,29.662 +2020-05-01 06:30:00,58.38,96.056,11.109000000000002,29.662 +2020-05-01 06:45:00,60.23,89.587,11.109000000000002,29.662 +2020-05-01 07:00:00,61.8,89.125,13.77,29.662 +2020-05-01 07:15:00,63.86,86.988,13.77,29.662 +2020-05-01 07:30:00,65.25,85.859,13.77,29.662 +2020-05-01 07:45:00,66.17,84.554,13.77,29.662 +2020-05-01 08:00:00,67.11,83.42200000000001,12.868,29.662 +2020-05-01 08:15:00,66.63,85.50299999999999,12.868,29.662 +2020-05-01 08:30:00,67.33,84.87100000000001,12.868,29.662 +2020-05-01 08:45:00,66.74,86.586,12.868,29.662 +2020-05-01 09:00:00,67.45,81.148,12.804,29.662 +2020-05-01 09:15:00,68.21,81.42699999999999,12.804,29.662 +2020-05-01 09:30:00,67.2,84.065,12.804,29.662 +2020-05-01 09:45:00,68.2,84.78399999999999,12.804,29.662 +2020-05-01 10:00:00,69.04,81.47,11.029000000000002,29.662 +2020-05-01 10:15:00,69.41,82.72200000000001,11.029000000000002,29.662 +2020-05-01 10:30:00,70.12,82.583,11.029000000000002,29.662 +2020-05-01 10:45:00,66.61,82.947,11.029000000000002,29.662 +2020-05-01 11:00:00,57.19,79.26,11.681,29.662 +2020-05-01 11:15:00,57.35,79.181,11.681,29.662 +2020-05-01 11:30:00,54.68,80.666,11.681,29.662 +2020-05-01 11:45:00,54.35,80.57,11.681,29.662 +2020-05-01 12:00:00,52.36,78.047,8.915,29.662 +2020-05-01 12:15:00,56.13,78.738,8.915,29.662 +2020-05-01 12:30:00,51.09,77.492,8.915,29.662 +2020-05-01 12:45:00,50.81,77.333,8.915,29.662 +2020-05-01 13:00:00,49.83,78.273,5.4639999999999995,29.662 +2020-05-01 13:15:00,45.08,78.259,5.4639999999999995,29.662 +2020-05-01 13:30:00,46.19,75.832,5.4639999999999995,29.662 +2020-05-01 13:45:00,47.46,74.252,5.4639999999999995,29.662 +2020-05-01 14:00:00,49.01,76.367,3.2939999999999996,29.662 +2020-05-01 14:15:00,48.35,75.575,3.2939999999999996,29.662 +2020-05-01 14:30:00,45.56,75.003,3.2939999999999996,29.662 +2020-05-01 14:45:00,48.43,74.546,3.2939999999999996,29.662 +2020-05-01 15:00:00,49.06,75.21,4.689,29.662 +2020-05-01 15:15:00,49.67,73.595,4.689,29.662 +2020-05-01 15:30:00,51.56,72.592,4.689,29.662 +2020-05-01 15:45:00,53.04,71.998,4.689,29.662 +2020-05-01 16:00:00,55.11,72.08,7.732,29.662 +2020-05-01 16:15:00,59.46,71.52,7.732,29.662 +2020-05-01 16:30:00,62.16,72.118,7.732,29.662 +2020-05-01 16:45:00,62.3,69.263,7.732,29.662 +2020-05-01 17:00:00,67.83,70.095,17.558,29.662 +2020-05-01 17:15:00,72.29,71.555,17.558,29.662 +2020-05-01 17:30:00,72.68,72.794,17.558,29.662 +2020-05-01 17:45:00,73.94,74.653,17.558,29.662 +2020-05-01 18:00:00,75.38,75.821,24.763,29.662 +2020-05-01 18:15:00,74.58,78.516,24.763,29.662 +2020-05-01 18:30:00,75.18,77.906,24.763,29.662 +2020-05-01 18:45:00,75.67,80.471,24.763,29.662 +2020-05-01 19:00:00,79.11,79.535,29.633000000000003,29.662 +2020-05-01 19:15:00,84.51,78.283,29.633000000000003,29.662 +2020-05-01 19:30:00,85.92,78.82,29.633000000000003,29.662 +2020-05-01 19:45:00,82.48,79.994,29.633000000000003,29.662 +2020-05-01 20:00:00,83.91,80.059,38.826,29.662 +2020-05-01 20:15:00,81.16,79.312,38.826,29.662 +2020-05-01 20:30:00,79.77,77.578,38.826,29.662 +2020-05-01 20:45:00,88.48,76.283,38.826,29.662 +2020-05-01 21:00:00,88.98,74.173,37.751,29.662 +2020-05-01 21:15:00,88.97,75.696,37.751,29.662 +2020-05-01 21:30:00,78.96,76.152,37.751,29.662 +2020-05-01 21:45:00,81.17,76.479,37.751,29.662 +2020-05-01 22:00:00,73.75,76.163,39.799,29.662 +2020-05-01 22:15:00,80.27,74.783,39.799,29.662 +2020-05-01 22:30:00,81.01,72.372,39.799,29.662 +2020-05-01 22:45:00,80.12,70.10600000000001,39.799,29.662 +2020-05-01 23:00:00,80.75,64.072,33.686,29.662 +2020-05-01 23:15:00,77.21,61.755,33.686,29.662 +2020-05-01 23:30:00,72.86,60.244,33.686,29.662 +2020-05-01 23:45:00,74.22,59.806999999999995,33.686,29.662 +2020-05-02 00:00:00,67.4,48.595,42.833999999999996,29.662 +2020-05-02 00:15:00,71.63,47.126000000000005,42.833999999999996,29.662 +2020-05-02 00:30:00,75.66,46.176,42.833999999999996,29.662 +2020-05-02 00:45:00,76.11,44.898999999999994,42.833999999999996,29.662 +2020-05-02 01:00:00,72.31,44.903999999999996,37.859,29.662 +2020-05-02 01:15:00,70.22,44.23,37.859,29.662 +2020-05-02 01:30:00,73.08,42.45,37.859,29.662 +2020-05-02 01:45:00,73.7,42.513999999999996,37.859,29.662 +2020-05-02 02:00:00,66.85,43.174,35.327,29.662 +2020-05-02 02:15:00,66.49,41.31100000000001,35.327,29.662 +2020-05-02 02:30:00,68.26,43.461000000000006,35.327,29.662 +2020-05-02 02:45:00,66.14,44.196999999999996,35.327,29.662 +2020-05-02 03:00:00,65.71,46.493,34.908,29.662 +2020-05-02 03:15:00,66.15,44.941,34.908,29.662 +2020-05-02 03:30:00,67.61,43.762,34.908,29.662 +2020-05-02 03:45:00,68.7,45.599,34.908,29.662 +2020-05-02 04:00:00,69.79,54.015,34.84,29.662 +2020-05-02 04:15:00,67.03,62.473,34.84,29.662 +2020-05-02 04:30:00,66.17,60.231,34.84,29.662 +2020-05-02 04:45:00,67.69,60.641999999999996,34.84,29.662 +2020-05-02 05:00:00,69.51,76.563,34.222,29.662 +2020-05-02 05:15:00,70.18,88.72,34.222,29.662 +2020-05-02 05:30:00,69.35,79.377,34.222,29.662 +2020-05-02 05:45:00,71.72,76.12100000000001,34.222,29.662 +2020-05-02 06:00:00,73.67,93.12,35.515,29.662 +2020-05-02 06:15:00,73.73,108.242,35.515,29.662 +2020-05-02 06:30:00,75.39,100.70299999999999,35.515,29.662 +2020-05-02 06:45:00,76.42,95.399,35.515,29.662 +2020-05-02 07:00:00,79.28,93.88799999999999,39.687,29.662 +2020-05-02 07:15:00,77.96,93.337,39.687,29.662 +2020-05-02 07:30:00,78.31,91.61399999999999,39.687,29.662 +2020-05-02 07:45:00,79.86,90.62100000000001,39.687,29.662 +2020-05-02 08:00:00,80.34,88.26799999999999,44.9,29.662 +2020-05-02 08:15:00,78.9,89.515,44.9,29.662 +2020-05-02 08:30:00,78.61,87.58,44.9,29.662 +2020-05-02 08:45:00,79.87,88.637,44.9,29.662 +2020-05-02 09:00:00,76.21,83.447,45.724,29.662 +2020-05-02 09:15:00,75.76,83.902,45.724,29.662 +2020-05-02 09:30:00,75.2,86.28399999999999,45.724,29.662 +2020-05-02 09:45:00,75.61,86.314,45.724,29.662 +2020-05-02 10:00:00,75.42,81.66199999999999,43.123999999999995,29.662 +2020-05-02 10:15:00,76.95,82.645,43.123999999999995,29.662 +2020-05-02 10:30:00,77.79,82.12200000000001,43.123999999999995,29.662 +2020-05-02 10:45:00,76.44,82.48899999999999,43.123999999999995,29.662 +2020-05-02 11:00:00,72.26,78.703,40.255,29.662 +2020-05-02 11:15:00,71.56,78.921,40.255,29.662 +2020-05-02 11:30:00,69.64,80.362,40.255,29.662 +2020-05-02 11:45:00,69.91,79.851,40.255,29.662 +2020-05-02 12:00:00,65.59,76.855,38.582,29.662 +2020-05-02 12:15:00,66.22,77.22399999999999,38.582,29.662 +2020-05-02 12:30:00,64.72,76.383,38.582,29.662 +2020-05-02 12:45:00,64.95,77.09,38.582,29.662 +2020-05-02 13:00:00,62.78,78.554,36.043,29.662 +2020-05-02 13:15:00,62.25,77.737,36.043,29.662 +2020-05-02 13:30:00,62.59,76.217,36.043,29.662 +2020-05-02 13:45:00,62.56,74.087,36.043,29.662 +2020-05-02 14:00:00,61.85,75.279,35.216,29.662 +2020-05-02 14:15:00,62.08,73.669,35.216,29.662 +2020-05-02 14:30:00,62.24,73.452,35.216,29.662 +2020-05-02 14:45:00,62.4,73.947,35.216,29.662 +2020-05-02 15:00:00,62.97,75.17399999999999,36.759,29.662 +2020-05-02 15:15:00,63.02,73.821,36.759,29.662 +2020-05-02 15:30:00,63.12,72.676,36.759,29.662 +2020-05-02 15:45:00,64.76,71.574,36.759,29.662 +2020-05-02 16:00:00,68.0,72.26,40.086,29.662 +2020-05-02 16:15:00,68.86,71.86399999999999,40.086,29.662 +2020-05-02 16:30:00,71.22,71.62,40.086,29.662 +2020-05-02 16:45:00,74.4,68.60300000000001,40.086,29.662 +2020-05-02 17:00:00,79.85,69.22399999999999,44.876999999999995,29.662 +2020-05-02 17:15:00,79.81,69.649,44.876999999999995,29.662 +2020-05-02 17:30:00,80.79,70.16199999999999,44.876999999999995,29.662 +2020-05-02 17:45:00,81.12,70.793,44.876999999999995,29.662 +2020-05-02 18:00:00,83.13,71.803,47.056000000000004,29.662 +2020-05-02 18:15:00,81.4,74.316,47.056000000000004,29.662 +2020-05-02 18:30:00,81.48,74.656,47.056000000000004,29.662 +2020-05-02 18:45:00,82.35,76.45,47.056000000000004,29.662 +2020-05-02 19:00:00,81.48,74.001,45.57,29.662 +2020-05-02 19:15:00,79.11,73.41,45.57,29.662 +2020-05-02 19:30:00,78.9,74.202,45.57,29.662 +2020-05-02 19:45:00,79.86,75.236,45.57,29.662 +2020-05-02 20:00:00,81.06,75.092,41.685,29.662 +2020-05-02 20:15:00,80.73,74.097,41.685,29.662 +2020-05-02 20:30:00,80.18,71.314,41.685,29.662 +2020-05-02 20:45:00,80.23,71.797,41.685,29.662 +2020-05-02 21:00:00,75.92,70.756,39.576,29.662 +2020-05-02 21:15:00,76.58,72.755,39.576,29.662 +2020-05-02 21:30:00,73.98,73.593,39.576,29.662 +2020-05-02 21:45:00,73.06,73.62,39.576,29.662 +2020-05-02 22:00:00,69.29,72.173,39.068000000000005,29.662 +2020-05-02 22:15:00,69.76,72.399,39.068000000000005,29.662 +2020-05-02 22:30:00,67.03,71.508,39.068000000000005,29.662 +2020-05-02 22:45:00,66.09,70.529,39.068000000000005,29.662 +2020-05-02 23:00:00,64.59,65.696,32.06,29.662 +2020-05-02 23:15:00,62.92,61.768,32.06,29.662 +2020-05-02 23:30:00,62.13,60.387,32.06,29.662 +2020-05-02 23:45:00,61.12,59.508,32.06,29.662 +2020-05-03 00:00:00,56.25,49.497,28.825,29.662 +2020-05-03 00:15:00,59.42,47.018,28.825,29.662 +2020-05-03 00:30:00,55.48,45.813,28.825,29.662 +2020-05-03 00:45:00,57.9,44.705,28.825,29.662 +2020-05-03 01:00:00,55.98,44.872,25.995,29.662 +2020-05-03 01:15:00,56.5,44.451,25.995,29.662 +2020-05-03 01:30:00,53.94,42.732,25.995,29.662 +2020-05-03 01:45:00,55.87,42.38399999999999,25.995,29.662 +2020-05-03 02:00:00,55.81,42.83,24.394000000000002,29.662 +2020-05-03 02:15:00,56.25,41.198,24.394000000000002,29.662 +2020-05-03 02:30:00,55.72,43.818000000000005,24.394000000000002,29.662 +2020-05-03 02:45:00,55.33,44.498999999999995,24.394000000000002,29.662 +2020-05-03 03:00:00,51.94,47.433,22.916999999999998,29.662 +2020-05-03 03:15:00,55.79,45.878,22.916999999999998,29.662 +2020-05-03 03:30:00,56.34,44.623999999999995,22.916999999999998,29.662 +2020-05-03 03:45:00,56.91,45.833,22.916999999999998,29.662 +2020-05-03 04:00:00,57.32,54.089,23.576999999999998,29.662 +2020-05-03 04:15:00,55.6,61.795,23.576999999999998,29.662 +2020-05-03 04:30:00,55.41,60.622,23.576999999999998,29.662 +2020-05-03 04:45:00,55.04,60.756,23.576999999999998,29.662 +2020-05-03 05:00:00,56.02,75.493,22.730999999999998,29.662 +2020-05-03 05:15:00,56.44,86.085,22.730999999999998,29.662 +2020-05-03 05:30:00,56.1,76.407,22.730999999999998,29.662 +2020-05-03 05:45:00,56.79,73.071,22.730999999999998,29.662 +2020-05-03 06:00:00,57.69,88.141,22.34,29.662 +2020-05-03 06:15:00,57.57,103.32,22.34,29.662 +2020-05-03 06:30:00,58.58,94.846,22.34,29.662 +2020-05-03 06:45:00,59.95,88.39299999999999,22.34,29.662 +2020-05-03 07:00:00,61.08,87.919,24.691999999999997,29.662 +2020-05-03 07:15:00,61.55,85.772,24.691999999999997,29.662 +2020-05-03 07:30:00,61.04,84.58,24.691999999999997,29.662 +2020-05-03 07:45:00,61.04,83.305,24.691999999999997,29.662 +2020-05-03 08:00:00,61.08,82.164,29.340999999999998,29.662 +2020-05-03 08:15:00,62.23,84.333,29.340999999999998,29.662 +2020-05-03 08:30:00,61.79,83.662,29.340999999999998,29.662 +2020-05-03 08:45:00,60.97,85.425,29.340999999999998,29.662 +2020-05-03 09:00:00,59.6,79.984,30.788,29.662 +2020-05-03 09:15:00,59.67,80.277,30.788,29.662 +2020-05-03 09:30:00,59.03,82.943,30.788,29.662 +2020-05-03 09:45:00,59.85,83.73,30.788,29.662 +2020-05-03 10:00:00,60.2,80.434,30.158,29.662 +2020-05-03 10:15:00,61.9,81.768,30.158,29.662 +2020-05-03 10:30:00,63.99,81.663,30.158,29.662 +2020-05-03 10:45:00,64.7,82.06200000000001,30.158,29.662 +2020-05-03 11:00:00,62.24,78.36,32.056,29.662 +2020-05-03 11:15:00,59.36,78.319,32.056,29.662 +2020-05-03 11:30:00,54.19,79.796,32.056,29.662 +2020-05-03 11:45:00,55.34,79.73100000000001,32.056,29.662 +2020-05-03 12:00:00,52.93,77.274,28.671999999999997,29.662 +2020-05-03 12:15:00,52.8,77.979,28.671999999999997,29.662 +2020-05-03 12:30:00,49.69,76.653,28.671999999999997,29.662 +2020-05-03 12:45:00,47.47,76.506,28.671999999999997,29.662 +2020-05-03 13:00:00,45.23,77.5,23.171,29.662 +2020-05-03 13:15:00,43.21,77.488,23.171,29.662 +2020-05-03 13:30:00,45.06,75.075,23.171,29.662 +2020-05-03 13:45:00,46.0,73.49600000000001,23.171,29.662 +2020-05-03 14:00:00,46.8,75.712,19.11,29.662 +2020-05-03 14:15:00,46.46,74.891,19.11,29.662 +2020-05-03 14:30:00,45.78,74.235,19.11,29.662 +2020-05-03 14:45:00,46.36,73.783,19.11,29.662 +2020-05-03 15:00:00,48.14,74.539,19.689,29.662 +2020-05-03 15:15:00,48.74,72.88600000000001,19.689,29.662 +2020-05-03 15:30:00,49.3,71.814,19.689,29.662 +2020-05-03 15:45:00,51.69,71.186,19.689,29.662 +2020-05-03 16:00:00,55.94,71.361,22.875,29.662 +2020-05-03 16:15:00,57.49,70.76100000000001,22.875,29.662 +2020-05-03 16:30:00,59.74,71.37899999999999,22.875,29.662 +2020-05-03 16:45:00,64.35,68.4,22.875,29.662 +2020-05-03 17:00:00,68.62,69.329,33.884,29.662 +2020-05-03 17:15:00,69.55,70.735,33.884,29.662 +2020-05-03 17:30:00,71.62,71.954,33.884,29.662 +2020-05-03 17:45:00,72.87,73.748,33.884,29.662 +2020-05-03 18:00:00,76.29,74.952,38.453,29.662 +2020-05-03 18:15:00,74.34,77.643,38.453,29.662 +2020-05-03 18:30:00,74.52,77.007,38.453,29.662 +2020-05-03 18:45:00,74.5,79.578,38.453,29.662 +2020-05-03 19:00:00,80.75,78.635,39.221,29.662 +2020-05-03 19:15:00,82.66,77.384,39.221,29.662 +2020-05-03 19:30:00,83.29,77.925,39.221,29.662 +2020-05-03 19:45:00,75.88,79.12100000000001,39.221,29.662 +2020-05-03 20:00:00,79.39,79.132,37.871,29.662 +2020-05-03 20:15:00,79.33,78.392,37.871,29.662 +2020-05-03 20:30:00,79.68,76.717,37.871,29.662 +2020-05-03 20:45:00,81.36,75.5,37.871,29.662 +2020-05-03 21:00:00,87.04,73.398,36.465,29.662 +2020-05-03 21:15:00,90.9,74.95,36.465,29.662 +2020-05-03 21:30:00,84.7,75.37,36.465,29.662 +2020-05-03 21:45:00,81.23,75.756,36.465,29.662 +2020-05-03 22:00:00,78.55,75.484,36.092,29.662 +2020-05-03 22:15:00,82.23,74.148,36.092,29.662 +2020-05-03 22:30:00,81.02,71.717,36.092,29.662 +2020-05-03 22:45:00,76.17,69.429,36.092,29.662 +2020-05-03 23:00:00,72.51,63.32899999999999,31.013,29.662 +2020-05-03 23:15:00,74.86,61.097,31.013,29.662 +2020-05-03 23:30:00,75.21,59.583999999999996,31.013,29.662 +2020-05-03 23:45:00,74.48,59.146,31.013,29.662 +2020-05-04 00:00:00,69.6,51.836999999999996,31.174,29.775 +2020-05-04 00:15:00,72.1,51.037,31.174,29.775 +2020-05-04 00:30:00,73.55,49.571999999999996,31.174,29.775 +2020-05-04 00:45:00,72.62,47.968999999999994,31.174,29.775 +2020-05-04 01:00:00,67.74,48.449,29.663,29.775 +2020-05-04 01:15:00,66.22,47.828,29.663,29.775 +2020-05-04 01:30:00,70.56,46.394,29.663,29.775 +2020-05-04 01:45:00,73.57,46.01,29.663,29.775 +2020-05-04 02:00:00,73.5,46.79600000000001,28.793000000000003,29.775 +2020-05-04 02:15:00,69.89,44.87,28.793000000000003,29.775 +2020-05-04 02:30:00,68.85,47.751000000000005,28.793000000000003,29.775 +2020-05-04 02:45:00,73.7,48.104,28.793000000000003,29.775 +2020-05-04 03:00:00,73.78,51.895,27.728,29.775 +2020-05-04 03:15:00,70.78,51.468,27.728,29.775 +2020-05-04 03:30:00,74.92,50.652,27.728,29.775 +2020-05-04 03:45:00,71.42,51.328,27.728,29.775 +2020-05-04 04:00:00,75.17,63.512,29.266,29.775 +2020-05-04 04:15:00,76.56,74.935,29.266,29.775 +2020-05-04 04:30:00,80.37,74.154,29.266,29.775 +2020-05-04 04:45:00,84.87,74.626,29.266,29.775 +2020-05-04 05:00:00,91.28,99.954,37.889,29.775 +2020-05-04 05:15:00,95.44,125.398,37.889,29.775 +2020-05-04 05:30:00,98.76,114.611,37.889,29.775 +2020-05-04 05:45:00,100.67,106.984,37.889,29.775 +2020-05-04 06:00:00,103.98,108.009,55.485,29.775 +2020-05-04 06:15:00,105.43,110.87700000000001,55.485,29.775 +2020-05-04 06:30:00,106.87,108.288,55.485,29.775 +2020-05-04 06:45:00,108.09,109.04,55.485,29.775 +2020-05-04 07:00:00,108.57,110.086,65.765,29.775 +2020-05-04 07:15:00,107.31,110.166,65.765,29.775 +2020-05-04 07:30:00,106.72,107.994,65.765,29.775 +2020-05-04 07:45:00,105.4,106.05799999999999,65.765,29.775 +2020-05-04 08:00:00,104.99,101.705,56.745,29.775 +2020-05-04 08:15:00,103.93,102.205,56.745,29.775 +2020-05-04 08:30:00,104.33,99.277,56.745,29.775 +2020-05-04 08:45:00,103.33,100.006,56.745,29.775 +2020-05-04 09:00:00,102.16,93.492,53.321999999999996,29.775 +2020-05-04 09:15:00,101.67,91.12200000000001,53.321999999999996,29.775 +2020-05-04 09:30:00,101.56,92.71799999999999,53.321999999999996,29.775 +2020-05-04 09:45:00,101.91,91.898,53.321999999999996,29.775 +2020-05-04 10:00:00,101.39,88.725,51.309,29.775 +2020-05-04 10:15:00,101.65,89.86200000000001,51.309,29.775 +2020-05-04 10:30:00,101.66,89.089,51.309,29.775 +2020-05-04 10:45:00,101.33,88.617,51.309,29.775 +2020-05-04 11:00:00,101.22,84.113,50.415,29.775 +2020-05-04 11:15:00,98.86,85.01899999999999,50.415,29.775 +2020-05-04 11:30:00,98.41,87.557,50.415,29.775 +2020-05-04 11:45:00,97.54,87.715,50.415,29.775 +2020-05-04 12:00:00,97.03,84.945,48.273,29.775 +2020-05-04 12:15:00,95.22,85.738,48.273,29.775 +2020-05-04 12:30:00,95.04,83.7,48.273,29.775 +2020-05-04 12:45:00,94.65,84.18700000000001,48.273,29.775 +2020-05-04 13:00:00,93.91,86.016,48.452,29.775 +2020-05-04 13:15:00,94.72,84.79299999999999,48.452,29.775 +2020-05-04 13:30:00,93.9,82.296,48.452,29.775 +2020-05-04 13:45:00,94.22,81.383,48.452,29.775 +2020-05-04 14:00:00,96.6,82.76899999999999,48.35,29.775 +2020-05-04 14:15:00,94.21,82.103,48.35,29.775 +2020-05-04 14:30:00,94.03,81.062,48.35,29.775 +2020-05-04 14:45:00,93.5,82.105,48.35,29.775 +2020-05-04 15:00:00,92.87,83.38600000000001,48.838,29.775 +2020-05-04 15:15:00,93.09,80.695,48.838,29.775 +2020-05-04 15:30:00,94.12,79.807,48.838,29.775 +2020-05-04 15:45:00,96.9,78.626,48.838,29.775 +2020-05-04 16:00:00,97.74,79.515,50.873000000000005,29.775 +2020-05-04 16:15:00,97.79,78.64699999999999,50.873000000000005,29.775 +2020-05-04 16:30:00,102.74,78.398,50.873000000000005,29.775 +2020-05-04 16:45:00,102.67,74.907,50.873000000000005,29.775 +2020-05-04 17:00:00,105.47,74.95100000000001,56.637,29.775 +2020-05-04 17:15:00,105.44,76.24,56.637,29.775 +2020-05-04 17:30:00,106.23,76.945,56.637,29.775 +2020-05-04 17:45:00,107.54,77.82600000000001,56.637,29.775 +2020-05-04 18:00:00,108.37,78.27,56.35,29.775 +2020-05-04 18:15:00,107.47,78.734,56.35,29.775 +2020-05-04 18:30:00,113.16,77.749,56.35,29.775 +2020-05-04 18:45:00,114.56,83.012,56.35,29.775 +2020-05-04 19:00:00,111.01,81.166,56.023,29.775 +2020-05-04 19:15:00,98.89,80.538,56.023,29.775 +2020-05-04 19:30:00,101.05,80.969,56.023,29.775 +2020-05-04 19:45:00,98.85,81.399,56.023,29.775 +2020-05-04 20:00:00,97.88,79.572,62.372,29.775 +2020-05-04 20:15:00,103.11,79.016,62.372,29.775 +2020-05-04 20:30:00,108.62,77.098,62.372,29.775 +2020-05-04 20:45:00,107.25,76.679,62.372,29.775 +2020-05-04 21:00:00,93.21,74.293,57.516999999999996,29.775 +2020-05-04 21:15:00,90.29,75.783,57.516999999999996,29.775 +2020-05-04 21:30:00,87.29,76.21,57.516999999999996,29.775 +2020-05-04 21:45:00,93.2,76.271,57.516999999999996,29.775 +2020-05-04 22:00:00,88.85,73.393,51.823,29.775 +2020-05-04 22:15:00,88.88,73.15899999999999,51.823,29.775 +2020-05-04 22:30:00,83.03,63.908,51.823,29.775 +2020-05-04 22:45:00,81.21,59.86,51.823,29.775 +2020-05-04 23:00:00,80.18,54.06100000000001,43.832,29.775 +2020-05-04 23:15:00,81.63,51.527,43.832,29.775 +2020-05-04 23:30:00,78.05,50.97,43.832,29.775 +2020-05-04 23:45:00,75.22,51.051,43.832,29.775 +2020-05-05 00:00:00,76.0,49.474,42.371,29.775 +2020-05-05 00:15:00,79.2,49.869,42.371,29.775 +2020-05-05 00:30:00,77.68,48.63,42.371,29.775 +2020-05-05 00:45:00,75.02,47.31100000000001,42.371,29.775 +2020-05-05 01:00:00,77.48,47.3,39.597,29.775 +2020-05-05 01:15:00,78.55,46.585,39.597,29.775 +2020-05-05 01:30:00,78.09,45.091,39.597,29.775 +2020-05-05 01:45:00,74.26,44.409,39.597,29.775 +2020-05-05 02:00:00,77.89,44.806999999999995,38.298,29.775 +2020-05-05 02:15:00,78.97,43.738,38.298,29.775 +2020-05-05 02:30:00,73.18,46.115,38.298,29.775 +2020-05-05 02:45:00,74.65,46.74,38.298,29.775 +2020-05-05 03:00:00,72.05,49.676,37.884,29.775 +2020-05-05 03:15:00,72.97,49.666000000000004,37.884,29.775 +2020-05-05 03:30:00,81.18,48.996,37.884,29.775 +2020-05-05 03:45:00,86.82,48.886,37.884,29.775 +2020-05-05 04:00:00,86.35,59.956,39.442,29.775 +2020-05-05 04:15:00,80.15,71.218,39.442,29.775 +2020-05-05 04:30:00,82.91,70.225,39.442,29.775 +2020-05-05 04:45:00,86.29,71.503,39.442,29.775 +2020-05-05 05:00:00,93.14,99.67200000000001,43.608000000000004,29.775 +2020-05-05 05:15:00,96.1,125.415,43.608000000000004,29.775 +2020-05-05 05:30:00,99.08,114.31700000000001,43.608000000000004,29.775 +2020-05-05 05:45:00,102.21,106.15799999999999,43.608000000000004,29.775 +2020-05-05 06:00:00,105.73,107.665,54.99100000000001,29.775 +2020-05-05 06:15:00,105.97,111.215,54.99100000000001,29.775 +2020-05-05 06:30:00,106.51,108.149,54.99100000000001,29.775 +2020-05-05 06:45:00,106.79,108.066,54.99100000000001,29.775 +2020-05-05 07:00:00,108.05,109.16,66.217,29.775 +2020-05-05 07:15:00,107.67,108.979,66.217,29.775 +2020-05-05 07:30:00,106.25,106.595,66.217,29.775 +2020-05-05 07:45:00,104.55,103.98200000000001,66.217,29.775 +2020-05-05 08:00:00,102.47,99.61399999999999,60.151,29.775 +2020-05-05 08:15:00,101.53,99.43299999999999,60.151,29.775 +2020-05-05 08:30:00,101.82,96.552,60.151,29.775 +2020-05-05 08:45:00,102.85,96.48899999999999,60.151,29.775 +2020-05-05 09:00:00,103.32,89.926,53.873000000000005,29.775 +2020-05-05 09:15:00,101.69,88.057,53.873000000000005,29.775 +2020-05-05 09:30:00,99.95,90.448,53.873000000000005,29.775 +2020-05-05 09:45:00,102.43,90.609,53.873000000000005,29.775 +2020-05-05 10:00:00,101.34,86.17399999999999,51.417,29.775 +2020-05-05 10:15:00,102.24,86.82799999999999,51.417,29.775 +2020-05-05 10:30:00,102.25,86.163,51.417,29.775 +2020-05-05 10:45:00,102.01,86.572,51.417,29.775 +2020-05-05 11:00:00,98.68,82.649,50.43600000000001,29.775 +2020-05-05 11:15:00,99.53,83.743,50.43600000000001,29.775 +2020-05-05 11:30:00,98.28,84.958,50.43600000000001,29.775 +2020-05-05 11:45:00,97.89,85.07700000000001,50.43600000000001,29.775 +2020-05-05 12:00:00,97.44,81.642,47.468,29.775 +2020-05-05 12:15:00,100.02,82.514,47.468,29.775 +2020-05-05 12:30:00,104.48,81.36,47.468,29.775 +2020-05-05 12:45:00,103.25,82.234,47.468,29.775 +2020-05-05 13:00:00,98.15,83.67200000000001,48.453,29.775 +2020-05-05 13:15:00,97.89,83.62799999999999,48.453,29.775 +2020-05-05 13:30:00,98.63,81.594,48.453,29.775 +2020-05-05 13:45:00,96.77,80.05199999999999,48.453,29.775 +2020-05-05 14:00:00,101.19,81.928,48.435,29.775 +2020-05-05 14:15:00,102.3,81.167,48.435,29.775 +2020-05-05 14:30:00,106.02,80.582,48.435,29.775 +2020-05-05 14:45:00,96.9,81.017,48.435,29.775 +2020-05-05 15:00:00,96.46,82.051,49.966,29.775 +2020-05-05 15:15:00,96.66,80.153,49.966,29.775 +2020-05-05 15:30:00,101.42,79.169,49.966,29.775 +2020-05-05 15:45:00,102.58,78.059,49.966,29.775 +2020-05-05 16:00:00,102.74,78.64699999999999,51.184,29.775 +2020-05-05 16:15:00,101.99,78.009,51.184,29.775 +2020-05-05 16:30:00,104.08,77.745,51.184,29.775 +2020-05-05 16:45:00,110.91,74.844,51.184,29.775 +2020-05-05 17:00:00,111.11,75.308,56.138999999999996,29.775 +2020-05-05 17:15:00,110.09,76.896,56.138999999999996,29.775 +2020-05-05 17:30:00,109.87,77.51899999999999,56.138999999999996,29.775 +2020-05-05 17:45:00,109.83,78.067,56.138999999999996,29.775 +2020-05-05 18:00:00,116.18,77.743,57.038000000000004,29.775 +2020-05-05 18:15:00,114.49,79.166,57.038000000000004,29.775 +2020-05-05 18:30:00,112.35,77.851,57.038000000000004,29.775 +2020-05-05 18:45:00,106.65,83.26299999999999,57.038000000000004,29.775 +2020-05-05 19:00:00,104.1,80.51100000000001,56.492,29.775 +2020-05-05 19:15:00,101.34,79.92399999999999,56.492,29.775 +2020-05-05 19:30:00,108.09,79.952,56.492,29.775 +2020-05-05 19:45:00,108.89,80.669,56.492,29.775 +2020-05-05 20:00:00,106.25,79.186,62.534,29.775 +2020-05-05 20:15:00,101.8,77.277,62.534,29.775 +2020-05-05 20:30:00,98.86,75.78699999999999,62.534,29.775 +2020-05-05 20:45:00,99.69,75.52600000000001,62.534,29.775 +2020-05-05 21:00:00,99.28,73.567,55.506,29.775 +2020-05-05 21:15:00,98.12,74.248,55.506,29.775 +2020-05-05 21:30:00,92.99,74.533,55.506,29.775 +2020-05-05 21:45:00,88.48,74.859,55.506,29.775 +2020-05-05 22:00:00,87.66,72.74600000000001,51.472,29.775 +2020-05-05 22:15:00,89.2,72.176,51.472,29.775 +2020-05-05 22:30:00,85.37,63.177,51.472,29.775 +2020-05-05 22:45:00,84.36,59.214,51.472,29.775 +2020-05-05 23:00:00,81.88,52.781000000000006,44.593,29.775 +2020-05-05 23:15:00,80.29,51.196000000000005,44.593,29.775 +2020-05-05 23:30:00,81.46,50.50899999999999,44.593,29.775 +2020-05-05 23:45:00,77.25,50.575,44.593,29.775 +2020-05-06 00:00:00,71.85,49.147,41.978,29.775 +2020-05-06 00:15:00,78.55,49.553000000000004,41.978,29.775 +2020-05-06 00:30:00,77.81,48.31,41.978,29.775 +2020-05-06 00:45:00,77.29,46.997,41.978,29.775 +2020-05-06 01:00:00,73.94,46.992,38.59,29.775 +2020-05-06 01:15:00,78.49,46.251999999999995,38.59,29.775 +2020-05-06 01:30:00,78.56,44.74100000000001,38.59,29.775 +2020-05-06 01:45:00,76.62,44.058,38.59,29.775 +2020-05-06 02:00:00,73.33,44.446999999999996,36.23,29.775 +2020-05-06 02:15:00,78.33,43.364,36.23,29.775 +2020-05-06 02:30:00,77.82,45.753,36.23,29.775 +2020-05-06 02:45:00,72.36,46.386,36.23,29.775 +2020-05-06 03:00:00,70.81,49.335,35.867,29.775 +2020-05-06 03:15:00,72.19,49.305,35.867,29.775 +2020-05-06 03:30:00,73.52,48.63399999999999,35.867,29.775 +2020-05-06 03:45:00,75.1,48.548,35.867,29.775 +2020-05-06 04:00:00,78.39,59.571999999999996,36.75,29.775 +2020-05-06 04:15:00,78.83,70.78699999999999,36.75,29.775 +2020-05-06 04:30:00,81.3,69.78699999999999,36.75,29.775 +2020-05-06 04:45:00,84.96,71.058,36.75,29.775 +2020-05-06 05:00:00,93.15,99.10799999999999,40.461,29.775 +2020-05-06 05:15:00,95.53,124.71799999999999,40.461,29.775 +2020-05-06 05:30:00,97.79,113.664,40.461,29.775 +2020-05-06 05:45:00,101.14,105.565,40.461,29.775 +2020-05-06 06:00:00,106.4,107.095,55.481,29.775 +2020-05-06 06:15:00,106.6,110.62100000000001,55.481,29.775 +2020-05-06 06:30:00,108.89,107.556,55.481,29.775 +2020-05-06 06:45:00,109.94,107.48100000000001,55.481,29.775 +2020-05-06 07:00:00,114.39,108.568,68.45,29.775 +2020-05-06 07:15:00,107.66,108.384,68.45,29.775 +2020-05-06 07:30:00,105.69,105.969,68.45,29.775 +2020-05-06 07:45:00,106.74,103.374,68.45,29.775 +2020-05-06 08:00:00,103.47,99.00200000000001,60.885,29.775 +2020-05-06 08:15:00,101.98,98.86399999999999,60.885,29.775 +2020-05-06 08:30:00,104.01,95.965,60.885,29.775 +2020-05-06 08:45:00,102.48,95.925,60.885,29.775 +2020-05-06 09:00:00,101.52,89.361,56.887,29.775 +2020-05-06 09:15:00,101.65,87.49799999999999,56.887,29.775 +2020-05-06 09:30:00,101.44,89.904,56.887,29.775 +2020-05-06 09:45:00,104.08,90.09700000000001,56.887,29.775 +2020-05-06 10:00:00,109.12,85.67200000000001,54.401,29.775 +2020-05-06 10:15:00,110.04,86.36399999999999,54.401,29.775 +2020-05-06 10:30:00,109.63,85.71600000000001,54.401,29.775 +2020-05-06 10:45:00,105.76,86.14200000000001,54.401,29.775 +2020-05-06 11:00:00,105.77,82.212,53.678000000000004,29.775 +2020-05-06 11:15:00,105.27,83.325,53.678000000000004,29.775 +2020-05-06 11:30:00,108.22,84.535,53.678000000000004,29.775 +2020-05-06 11:45:00,101.7,84.671,53.678000000000004,29.775 +2020-05-06 12:00:00,98.23,81.267,51.68,29.775 +2020-05-06 12:15:00,98.72,82.146,51.68,29.775 +2020-05-06 12:30:00,99.41,80.953,51.68,29.775 +2020-05-06 12:45:00,104.66,81.832,51.68,29.775 +2020-05-06 13:00:00,100.13,83.296,51.263000000000005,29.775 +2020-05-06 13:15:00,102.95,83.25200000000001,51.263000000000005,29.775 +2020-05-06 13:30:00,97.84,81.226,51.263000000000005,29.775 +2020-05-06 13:45:00,96.57,79.685,51.263000000000005,29.775 +2020-05-06 14:00:00,96.82,81.609,51.107,29.775 +2020-05-06 14:15:00,95.84,80.835,51.107,29.775 +2020-05-06 14:30:00,96.81,80.208,51.107,29.775 +2020-05-06 14:45:00,93.98,80.645,51.107,29.775 +2020-05-06 15:00:00,94.2,81.723,51.498000000000005,29.775 +2020-05-06 15:15:00,93.38,79.808,51.498000000000005,29.775 +2020-05-06 15:30:00,95.59,78.79,51.498000000000005,29.775 +2020-05-06 15:45:00,98.49,77.665,51.498000000000005,29.775 +2020-05-06 16:00:00,101.69,78.296,53.376999999999995,29.775 +2020-05-06 16:15:00,101.79,77.641,53.376999999999995,29.775 +2020-05-06 16:30:00,102.02,77.387,53.376999999999995,29.775 +2020-05-06 16:45:00,100.4,74.425,53.376999999999995,29.775 +2020-05-06 17:00:00,102.39,74.937,56.965,29.775 +2020-05-06 17:15:00,101.21,76.498,56.965,29.775 +2020-05-06 17:30:00,100.01,77.111,56.965,29.775 +2020-05-06 17:45:00,102.1,77.626,56.965,29.775 +2020-05-06 18:00:00,104.0,77.32,58.231,29.775 +2020-05-06 18:15:00,104.34,78.742,58.231,29.775 +2020-05-06 18:30:00,105.4,77.414,58.231,29.775 +2020-05-06 18:45:00,103.72,82.82700000000001,58.231,29.775 +2020-05-06 19:00:00,102.23,80.072,58.865,29.775 +2020-05-06 19:15:00,97.93,79.486,58.865,29.775 +2020-05-06 19:30:00,95.7,79.515,58.865,29.775 +2020-05-06 19:45:00,96.66,80.242,58.865,29.775 +2020-05-06 20:00:00,95.25,78.734,65.605,29.775 +2020-05-06 20:15:00,98.04,76.829,65.605,29.775 +2020-05-06 20:30:00,91.55,75.367,65.605,29.775 +2020-05-06 20:45:00,91.56,75.143,65.605,29.775 +2020-05-06 21:00:00,82.99,73.189,58.083999999999996,29.775 +2020-05-06 21:15:00,81.12,73.88600000000001,58.083999999999996,29.775 +2020-05-06 21:30:00,78.05,74.15100000000001,58.083999999999996,29.775 +2020-05-06 21:45:00,75.34,74.505,58.083999999999996,29.775 +2020-05-06 22:00:00,70.55,72.414,53.243,29.775 +2020-05-06 22:15:00,70.4,71.865,53.243,29.775 +2020-05-06 22:30:00,68.32,62.855,53.243,29.775 +2020-05-06 22:45:00,66.71,58.88,53.243,29.775 +2020-05-06 23:00:00,76.44,52.417,44.283,29.775 +2020-05-06 23:15:00,77.93,50.872,44.283,29.775 +2020-05-06 23:30:00,80.4,50.187,44.283,29.775 +2020-05-06 23:45:00,79.81,50.251999999999995,44.283,29.775 +2020-05-07 00:00:00,73.78,48.823,40.219,29.775 +2020-05-07 00:15:00,72.71,49.239,40.219,29.775 +2020-05-07 00:30:00,73.87,47.994,40.219,29.775 +2020-05-07 00:45:00,77.01,46.685,40.219,29.775 +2020-05-07 01:00:00,74.22,46.685,37.959,29.775 +2020-05-07 01:15:00,73.3,45.92100000000001,37.959,29.775 +2020-05-07 01:30:00,72.97,44.391999999999996,37.959,29.775 +2020-05-07 01:45:00,74.22,43.708999999999996,37.959,29.775 +2020-05-07 02:00:00,75.92,44.092,36.113,29.775 +2020-05-07 02:15:00,74.19,42.993,36.113,29.775 +2020-05-07 02:30:00,72.07,45.396,36.113,29.775 +2020-05-07 02:45:00,74.15,46.036,36.113,29.775 +2020-05-07 03:00:00,74.4,48.995,35.546,29.775 +2020-05-07 03:15:00,77.16,48.946000000000005,35.546,29.775 +2020-05-07 03:30:00,76.61,48.275,35.546,29.775 +2020-05-07 03:45:00,78.26,48.214,35.546,29.775 +2020-05-07 04:00:00,79.9,59.19,37.169000000000004,29.775 +2020-05-07 04:15:00,80.7,70.359,37.169000000000004,29.775 +2020-05-07 04:30:00,83.75,69.352,37.169000000000004,29.775 +2020-05-07 04:45:00,87.24,70.615,37.169000000000004,29.775 +2020-05-07 05:00:00,95.0,98.54899999999999,41.233000000000004,29.775 +2020-05-07 05:15:00,97.6,124.024,41.233000000000004,29.775 +2020-05-07 05:30:00,100.93,113.01700000000001,41.233000000000004,29.775 +2020-05-07 05:45:00,104.36,104.975,41.233000000000004,29.775 +2020-05-07 06:00:00,111.07,106.531,52.57,29.775 +2020-05-07 06:15:00,111.79,110.03200000000001,52.57,29.775 +2020-05-07 06:30:00,115.27,106.96700000000001,52.57,29.775 +2020-05-07 06:45:00,116.41,106.9,52.57,29.775 +2020-05-07 07:00:00,121.35,107.98,64.53,29.775 +2020-05-07 07:15:00,120.69,107.794,64.53,29.775 +2020-05-07 07:30:00,120.64,105.34899999999999,64.53,29.775 +2020-05-07 07:45:00,119.58,102.771,64.53,29.775 +2020-05-07 08:00:00,118.88,98.395,55.911,29.775 +2020-05-07 08:15:00,119.04,98.3,55.911,29.775 +2020-05-07 08:30:00,120.65,95.383,55.911,29.775 +2020-05-07 08:45:00,121.31,95.367,55.911,29.775 +2020-05-07 09:00:00,120.75,88.802,50.949,29.775 +2020-05-07 09:15:00,121.94,86.946,50.949,29.775 +2020-05-07 09:30:00,124.2,89.363,50.949,29.775 +2020-05-07 09:45:00,125.86,89.59,50.949,29.775 +2020-05-07 10:00:00,123.35,85.17299999999999,48.136,29.775 +2020-05-07 10:15:00,121.67,85.905,48.136,29.775 +2020-05-07 10:30:00,122.7,85.274,48.136,29.775 +2020-05-07 10:45:00,121.37,85.71700000000001,48.136,29.775 +2020-05-07 11:00:00,120.01,81.78,46.643,29.775 +2020-05-07 11:15:00,119.7,82.912,46.643,29.775 +2020-05-07 11:30:00,116.44,84.117,46.643,29.775 +2020-05-07 11:45:00,120.55,84.266,46.643,29.775 +2020-05-07 12:00:00,114.26,80.895,44.098,29.775 +2020-05-07 12:15:00,115.33,81.78,44.098,29.775 +2020-05-07 12:30:00,115.25,80.548,44.098,29.775 +2020-05-07 12:45:00,111.95,81.433,44.098,29.775 +2020-05-07 13:00:00,111.66,82.92299999999999,43.717,29.775 +2020-05-07 13:15:00,115.79,82.87899999999999,43.717,29.775 +2020-05-07 13:30:00,112.84,80.861,43.717,29.775 +2020-05-07 13:45:00,113.94,79.32,43.717,29.775 +2020-05-07 14:00:00,112.73,81.293,44.218999999999994,29.775 +2020-05-07 14:15:00,110.34,80.506,44.218999999999994,29.775 +2020-05-07 14:30:00,109.63,79.837,44.218999999999994,29.775 +2020-05-07 14:45:00,110.46,80.277,44.218999999999994,29.775 +2020-05-07 15:00:00,109.7,81.399,46.159,29.775 +2020-05-07 15:15:00,111.05,79.46600000000001,46.159,29.775 +2020-05-07 15:30:00,108.64,78.415,46.159,29.775 +2020-05-07 15:45:00,109.14,77.273,46.159,29.775 +2020-05-07 16:00:00,109.78,77.95100000000001,47.115,29.775 +2020-05-07 16:15:00,111.93,77.275,47.115,29.775 +2020-05-07 16:30:00,114.03,77.032,47.115,29.775 +2020-05-07 16:45:00,114.16,74.009,47.115,29.775 +2020-05-07 17:00:00,114.59,74.568,50.827,29.775 +2020-05-07 17:15:00,113.02,76.104,50.827,29.775 +2020-05-07 17:30:00,114.7,76.706,50.827,29.775 +2020-05-07 17:45:00,114.23,77.19,50.827,29.775 +2020-05-07 18:00:00,114.07,76.90100000000001,52.586000000000006,29.775 +2020-05-07 18:15:00,110.01,78.32,52.586000000000006,29.775 +2020-05-07 18:30:00,114.3,76.979,52.586000000000006,29.775 +2020-05-07 18:45:00,113.58,82.39299999999999,52.586000000000006,29.775 +2020-05-07 19:00:00,112.14,79.639,51.886,29.775 +2020-05-07 19:15:00,106.35,79.051,51.886,29.775 +2020-05-07 19:30:00,105.41,79.083,51.886,29.775 +2020-05-07 19:45:00,106.24,79.819,51.886,29.775 +2020-05-07 20:00:00,105.1,78.285,56.162,29.775 +2020-05-07 20:15:00,103.7,76.383,56.162,29.775 +2020-05-07 20:30:00,103.6,74.949,56.162,29.775 +2020-05-07 20:45:00,103.67,74.764,56.162,29.775 +2020-05-07 21:00:00,98.44,72.814,53.023,29.775 +2020-05-07 21:15:00,98.45,73.525,53.023,29.775 +2020-05-07 21:30:00,93.13,73.773,53.023,29.775 +2020-05-07 21:45:00,94.44,74.152,53.023,29.775 +2020-05-07 22:00:00,88.82,72.084,49.303999999999995,29.775 +2020-05-07 22:15:00,86.68,71.556,49.303999999999995,29.775 +2020-05-07 22:30:00,83.38,62.534,49.303999999999995,29.775 +2020-05-07 22:45:00,84.4,58.549,49.303999999999995,29.775 +2020-05-07 23:00:00,58.37,52.053999999999995,43.409,29.775 +2020-05-07 23:15:00,57.74,50.552,43.409,29.775 +2020-05-07 23:30:00,55.82,49.86600000000001,43.409,29.775 +2020-05-07 23:45:00,54.77,49.93,43.409,29.775 +2020-05-08 00:00:00,53.24,46.693000000000005,39.884,29.775 +2020-05-08 00:15:00,54.19,47.361999999999995,39.884,29.775 +2020-05-08 00:30:00,54.32,46.278999999999996,39.884,29.775 +2020-05-08 00:45:00,55.58,45.352,39.884,29.775 +2020-05-08 01:00:00,52.76,44.941,37.658,29.775 +2020-05-08 01:15:00,53.95,44.012,37.658,29.775 +2020-05-08 01:30:00,54.34,42.949,37.658,29.775 +2020-05-08 01:45:00,53.53,42.105,37.658,29.775 +2020-05-08 02:00:00,52.96,43.25,36.707,29.775 +2020-05-08 02:15:00,54.39,42.055,36.707,29.775 +2020-05-08 02:30:00,54.97,45.31399999999999,36.707,29.775 +2020-05-08 02:45:00,54.2,45.437,36.707,29.775 +2020-05-08 03:00:00,55.31,48.655,37.025,29.775 +2020-05-08 03:15:00,56.15,47.978,37.025,29.775 +2020-05-08 03:30:00,57.39,47.113,37.025,29.775 +2020-05-08 03:45:00,59.16,47.886,37.025,29.775 +2020-05-08 04:00:00,61.87,59.01,38.349000000000004,29.775 +2020-05-08 04:15:00,63.45,68.811,38.349000000000004,29.775 +2020-05-08 04:30:00,66.08,68.631,38.349000000000004,29.775 +2020-05-08 04:45:00,68.24,68.958,38.349000000000004,29.775 +2020-05-08 05:00:00,72.24,95.93,41.565,29.775 +2020-05-08 05:15:00,73.47,122.565,41.565,29.775 +2020-05-08 05:30:00,76.45,112.135,41.565,29.775 +2020-05-08 05:45:00,79.96,103.76799999999999,41.565,29.775 +2020-05-08 06:00:00,84.65,105.696,53.861000000000004,29.775 +2020-05-08 06:15:00,86.18,108.787,53.861000000000004,29.775 +2020-05-08 06:30:00,88.73,105.376,53.861000000000004,29.775 +2020-05-08 06:45:00,89.53,105.786,53.861000000000004,29.775 +2020-05-08 07:00:00,93.58,107.083,63.497,29.775 +2020-05-08 07:15:00,92.57,107.986,63.497,29.775 +2020-05-08 07:30:00,92.28,103.915,63.497,29.775 +2020-05-08 07:45:00,92.57,100.859,63.497,29.775 +2020-05-08 08:00:00,92.12,96.67299999999999,55.43899999999999,29.775 +2020-05-08 08:15:00,93.47,96.991,55.43899999999999,29.775 +2020-05-08 08:30:00,95.97,94.354,55.43899999999999,29.775 +2020-05-08 08:45:00,98.27,93.648,55.43899999999999,29.775 +2020-05-08 09:00:00,97.16,85.48,52.132,29.775 +2020-05-08 09:15:00,97.44,85.31700000000001,52.132,29.775 +2020-05-08 09:30:00,96.52,87.052,52.132,29.775 +2020-05-08 09:45:00,93.66,87.565,52.132,29.775 +2020-05-08 10:00:00,90.57,82.54700000000001,49.881,29.775 +2020-05-08 10:15:00,90.43,83.43799999999999,49.881,29.775 +2020-05-08 10:30:00,92.89,83.205,49.881,29.775 +2020-05-08 10:45:00,92.64,83.412,49.881,29.775 +2020-05-08 11:00:00,87.14,79.635,49.396,29.775 +2020-05-08 11:15:00,84.45,79.61,49.396,29.775 +2020-05-08 11:30:00,82.3,81.271,49.396,29.775 +2020-05-08 11:45:00,84.56,80.74600000000001,49.396,29.775 +2020-05-08 12:00:00,85.34,78.204,46.7,29.775 +2020-05-08 12:15:00,87.59,77.72800000000001,46.7,29.775 +2020-05-08 12:30:00,82.79,76.611,46.7,29.775 +2020-05-08 12:45:00,84.03,77.154,46.7,29.775 +2020-05-08 13:00:00,86.69,79.491,44.05,29.775 +2020-05-08 13:15:00,90.34,79.928,44.05,29.775 +2020-05-08 13:30:00,92.17,78.503,44.05,29.775 +2020-05-08 13:45:00,91.91,77.16,44.05,29.775 +2020-05-08 14:00:00,91.0,78.111,42.805,29.775 +2020-05-08 14:15:00,87.13,77.55199999999999,42.805,29.775 +2020-05-08 14:30:00,85.29,78.155,42.805,29.775 +2020-05-08 14:45:00,85.31,78.226,42.805,29.775 +2020-05-08 15:00:00,81.82,79.128,44.36600000000001,29.775 +2020-05-08 15:15:00,80.11,76.816,44.36600000000001,29.775 +2020-05-08 15:30:00,79.41,74.672,44.36600000000001,29.775 +2020-05-08 15:45:00,79.63,74.131,44.36600000000001,29.775 +2020-05-08 16:00:00,80.31,73.779,46.928999999999995,29.775 +2020-05-08 16:15:00,83.58,73.572,46.928999999999995,29.775 +2020-05-08 16:30:00,85.81,73.242,46.928999999999995,29.775 +2020-05-08 16:45:00,88.21,69.52600000000001,46.928999999999995,29.775 +2020-05-08 17:00:00,90.9,71.516,51.468,29.775 +2020-05-08 17:15:00,90.29,72.729,51.468,29.775 +2020-05-08 17:30:00,91.83,73.315,51.468,29.775 +2020-05-08 17:45:00,93.13,73.54899999999999,51.468,29.775 +2020-05-08 18:00:00,95.38,73.646,52.58,29.775 +2020-05-08 18:15:00,93.36,74.184,52.58,29.775 +2020-05-08 18:30:00,94.59,72.903,52.58,29.775 +2020-05-08 18:45:00,91.5,78.64,52.58,29.775 +2020-05-08 19:00:00,88.14,76.949,52.183,29.775 +2020-05-08 19:15:00,85.24,77.32600000000001,52.183,29.775 +2020-05-08 19:30:00,84.68,77.257,52.183,29.775 +2020-05-08 19:45:00,85.55,77.033,52.183,29.775 +2020-05-08 20:00:00,86.18,75.357,58.497,29.775 +2020-05-08 20:15:00,86.8,74.078,58.497,29.775 +2020-05-08 20:30:00,85.42,72.279,58.497,29.775 +2020-05-08 20:45:00,84.39,71.708,58.497,29.775 +2020-05-08 21:00:00,79.14,70.98,54.731,29.775 +2020-05-08 21:15:00,78.08,73.17699999999999,54.731,29.775 +2020-05-08 21:30:00,75.44,73.305,54.731,29.775 +2020-05-08 21:45:00,73.8,74.07300000000001,54.731,29.775 +2020-05-08 22:00:00,69.96,72.279,51.386,29.775 +2020-05-08 22:15:00,70.53,71.528,51.386,29.775 +2020-05-08 22:30:00,67.66,68.579,51.386,29.775 +2020-05-08 22:45:00,66.61,66.559,51.386,29.775 +2020-05-08 23:00:00,62.1,61.192,44.463,29.775 +2020-05-08 23:15:00,61.96,57.783,44.463,29.775 +2020-05-08 23:30:00,61.24,55.128,44.463,29.775 +2020-05-08 23:45:00,61.9,54.85,44.463,29.775 +2020-05-09 00:00:00,59.19,46.31399999999999,42.833999999999996,29.662 +2020-05-09 00:15:00,59.48,44.915,42.833999999999996,29.662 +2020-05-09 00:30:00,58.27,43.941,42.833999999999996,29.662 +2020-05-09 00:45:00,58.41,42.702,42.833999999999996,29.662 +2020-05-09 01:00:00,56.44,42.744,37.859,29.662 +2020-05-09 01:15:00,57.02,41.898999999999994,37.859,29.662 +2020-05-09 01:30:00,54.39,39.994,37.859,29.662 +2020-05-09 01:45:00,57.37,40.054,37.859,29.662 +2020-05-09 02:00:00,56.57,40.662,35.327,29.662 +2020-05-09 02:15:00,57.32,38.695,35.327,29.662 +2020-05-09 02:30:00,56.09,40.938,35.327,29.662 +2020-05-09 02:45:00,56.5,41.725,35.327,29.662 +2020-05-09 03:00:00,56.74,44.101000000000006,34.908,29.662 +2020-05-09 03:15:00,57.04,42.413999999999994,34.908,29.662 +2020-05-09 03:30:00,57.73,41.23,34.908,29.662 +2020-05-09 03:45:00,58.65,43.236000000000004,34.908,29.662 +2020-05-09 04:00:00,56.1,51.325,34.84,29.662 +2020-05-09 04:15:00,58.39,59.455,34.84,29.662 +2020-05-09 04:30:00,58.62,57.167,34.84,29.662 +2020-05-09 04:45:00,58.93,57.523,34.84,29.662 +2020-05-09 05:00:00,59.13,72.622,34.222,29.662 +2020-05-09 05:15:00,58.29,83.84100000000001,34.222,29.662 +2020-05-09 05:30:00,61.65,74.814,34.222,29.662 +2020-05-09 05:45:00,64.49,71.96600000000001,34.222,29.662 +2020-05-09 06:00:00,67.16,89.139,35.515,29.662 +2020-05-09 06:15:00,68.9,104.09200000000001,35.515,29.662 +2020-05-09 06:30:00,67.17,96.552,35.515,29.662 +2020-05-09 06:45:00,72.7,91.306,35.515,29.662 +2020-05-09 07:00:00,71.59,89.74700000000001,39.687,29.662 +2020-05-09 07:15:00,75.04,89.178,39.687,29.662 +2020-05-09 07:30:00,77.56,87.242,39.687,29.662 +2020-05-09 07:45:00,81.67,86.36399999999999,39.687,29.662 +2020-05-09 08:00:00,83.62,83.985,44.9,29.662 +2020-05-09 08:15:00,84.5,85.53399999999999,44.9,29.662 +2020-05-09 08:30:00,84.0,83.471,44.9,29.662 +2020-05-09 08:45:00,81.6,84.695,44.9,29.662 +2020-05-09 09:00:00,79.98,79.499,45.724,29.662 +2020-05-09 09:15:00,82.69,79.999,45.724,29.662 +2020-05-09 09:30:00,76.88,82.469,45.724,29.662 +2020-05-09 09:45:00,80.32,82.73200000000001,45.724,29.662 +2020-05-09 10:00:00,74.68,78.143,43.123999999999995,29.662 +2020-05-09 10:15:00,75.87,79.402,43.123999999999995,29.662 +2020-05-09 10:30:00,75.83,78.999,43.123999999999995,29.662 +2020-05-09 10:45:00,76.14,79.484,43.123999999999995,29.662 +2020-05-09 11:00:00,72.08,75.645,40.255,29.662 +2020-05-09 11:15:00,70.62,75.997,40.255,29.662 +2020-05-09 11:30:00,73.05,77.406,40.255,29.662 +2020-05-09 11:45:00,75.89,76.999,40.255,29.662 +2020-05-09 12:00:00,76.78,74.229,38.582,29.662 +2020-05-09 12:15:00,71.51,74.643,38.582,29.662 +2020-05-09 12:30:00,73.06,73.529,38.582,29.662 +2020-05-09 12:45:00,72.57,74.273,38.582,29.662 +2020-05-09 13:00:00,70.0,75.922,36.043,29.662 +2020-05-09 13:15:00,68.54,75.10600000000001,36.043,29.662 +2020-05-09 13:30:00,62.42,73.637,36.043,29.662 +2020-05-09 13:45:00,66.38,71.518,36.043,29.662 +2020-05-09 14:00:00,70.51,73.04899999999999,35.216,29.662 +2020-05-09 14:15:00,77.12,71.347,35.216,29.662 +2020-05-09 14:30:00,70.75,70.837,35.216,29.662 +2020-05-09 14:45:00,67.87,71.348,35.216,29.662 +2020-05-09 15:00:00,69.68,72.88600000000001,36.759,29.662 +2020-05-09 15:15:00,71.0,71.407,36.759,29.662 +2020-05-09 15:30:00,71.64,70.027,36.759,29.662 +2020-05-09 15:45:00,74.81,68.809,36.759,29.662 +2020-05-09 16:00:00,75.54,69.814,40.086,29.662 +2020-05-09 16:15:00,71.72,69.282,40.086,29.662 +2020-05-09 16:30:00,75.34,69.111,40.086,29.662 +2020-05-09 16:45:00,76.17,65.67,40.086,29.662 +2020-05-09 17:00:00,77.72,66.625,44.876999999999995,29.662 +2020-05-09 17:15:00,79.16,66.865,44.876999999999995,29.662 +2020-05-09 17:30:00,79.96,67.304,44.876999999999995,29.662 +2020-05-09 17:45:00,80.99,67.714,44.876999999999995,29.662 +2020-05-09 18:00:00,80.0,68.844,47.056000000000004,29.662 +2020-05-09 18:15:00,82.8,71.342,47.056000000000004,29.662 +2020-05-09 18:30:00,81.66,71.592,47.056000000000004,29.662 +2020-05-09 18:45:00,82.41,73.399,47.056000000000004,29.662 +2020-05-09 19:00:00,80.39,70.936,45.57,29.662 +2020-05-09 19:15:00,77.08,70.343,45.57,29.662 +2020-05-09 19:30:00,74.71,71.15100000000001,45.57,29.662 +2020-05-09 19:45:00,78.23,72.253,45.57,29.662 +2020-05-09 20:00:00,78.77,71.929,41.685,29.662 +2020-05-09 20:15:00,79.72,70.954,41.685,29.662 +2020-05-09 20:30:00,78.87,68.374,41.685,29.662 +2020-05-09 20:45:00,78.23,69.122,41.685,29.662 +2020-05-09 21:00:00,76.02,68.109,39.576,29.662 +2020-05-09 21:15:00,74.79,70.21300000000001,39.576,29.662 +2020-05-09 21:30:00,70.34,70.922,39.576,29.662 +2020-05-09 21:45:00,72.16,71.14399999999999,39.576,29.662 +2020-05-09 22:00:00,69.44,69.848,39.068000000000005,29.662 +2020-05-09 22:15:00,66.81,70.223,39.068000000000005,29.662 +2020-05-09 22:30:00,65.84,69.253,39.068000000000005,29.662 +2020-05-09 22:45:00,65.64,68.2,39.068000000000005,29.662 +2020-05-09 23:00:00,58.1,63.147,32.06,29.662 +2020-05-09 23:15:00,61.56,59.51,32.06,29.662 +2020-05-09 23:30:00,59.59,58.13,32.06,29.662 +2020-05-09 23:45:00,61.11,57.242,32.06,29.662 +2020-05-10 00:00:00,56.26,47.231,28.825,29.662 +2020-05-10 00:15:00,56.19,44.821000000000005,28.825,29.662 +2020-05-10 00:30:00,56.2,43.593999999999994,28.825,29.662 +2020-05-10 00:45:00,56.93,42.525,28.825,29.662 +2020-05-10 01:00:00,54.42,42.729,25.995,29.662 +2020-05-10 01:15:00,55.57,42.137,25.995,29.662 +2020-05-10 01:30:00,54.8,40.296,25.995,29.662 +2020-05-10 01:45:00,54.72,39.944,25.995,29.662 +2020-05-10 02:00:00,53.8,40.336,24.394000000000002,29.662 +2020-05-10 02:15:00,53.93,38.604,24.394000000000002,29.662 +2020-05-10 02:30:00,54.27,41.316,24.394000000000002,29.662 +2020-05-10 02:45:00,53.34,42.047,24.394000000000002,29.662 +2020-05-10 03:00:00,53.35,45.059,22.916999999999998,29.662 +2020-05-10 03:15:00,53.77,43.37,22.916999999999998,29.662 +2020-05-10 03:30:00,55.05,42.111999999999995,22.916999999999998,29.662 +2020-05-10 03:45:00,55.75,43.489,22.916999999999998,29.662 +2020-05-10 04:00:00,53.43,51.42,23.576999999999998,29.662 +2020-05-10 04:15:00,53.2,58.799,23.576999999999998,29.662 +2020-05-10 04:30:00,52.95,57.58,23.576999999999998,29.662 +2020-05-10 04:45:00,53.86,57.659,23.576999999999998,29.662 +2020-05-10 05:00:00,53.48,71.578,22.730999999999998,29.662 +2020-05-10 05:15:00,54.32,81.235,22.730999999999998,29.662 +2020-05-10 05:30:00,53.89,71.875,22.730999999999998,29.662 +2020-05-10 05:45:00,54.74,68.944,22.730999999999998,29.662 +2020-05-10 06:00:00,56.84,84.185,22.34,29.662 +2020-05-10 06:15:00,56.42,99.195,22.34,29.662 +2020-05-10 06:30:00,57.71,90.72200000000001,22.34,29.662 +2020-05-10 06:45:00,58.61,84.32700000000001,22.34,29.662 +2020-05-10 07:00:00,59.27,83.804,24.691999999999997,29.662 +2020-05-10 07:15:00,59.67,81.642,24.691999999999997,29.662 +2020-05-10 07:30:00,59.68,80.24,24.691999999999997,29.662 +2020-05-10 07:45:00,60.06,79.082,24.691999999999997,29.662 +2020-05-10 08:00:00,59.38,77.918,29.340999999999998,29.662 +2020-05-10 08:15:00,59.29,80.388,29.340999999999998,29.662 +2020-05-10 08:30:00,59.52,79.59,29.340999999999998,29.662 +2020-05-10 08:45:00,58.7,81.51899999999999,29.340999999999998,29.662 +2020-05-10 09:00:00,56.6,76.072,30.788,29.662 +2020-05-10 09:15:00,57.08,76.41,30.788,29.662 +2020-05-10 09:30:00,57.77,79.16199999999999,30.788,29.662 +2020-05-10 09:45:00,60.07,80.18,30.788,29.662 +2020-05-10 10:00:00,59.67,76.94800000000001,30.158,29.662 +2020-05-10 10:15:00,60.32,78.554,30.158,29.662 +2020-05-10 10:30:00,60.97,78.569,30.158,29.662 +2020-05-10 10:45:00,61.22,79.084,30.158,29.662 +2020-05-10 11:00:00,58.46,75.331,32.056,29.662 +2020-05-10 11:15:00,58.18,75.423,32.056,29.662 +2020-05-10 11:30:00,55.24,76.867,32.056,29.662 +2020-05-10 11:45:00,55.54,76.905,32.056,29.662 +2020-05-10 12:00:00,53.33,74.672,28.671999999999997,29.662 +2020-05-10 12:15:00,54.51,75.421,28.671999999999997,29.662 +2020-05-10 12:30:00,50.53,73.825,28.671999999999997,29.662 +2020-05-10 12:45:00,50.54,73.71300000000001,28.671999999999997,29.662 +2020-05-10 13:00:00,47.4,74.889,23.171,29.662 +2020-05-10 13:15:00,45.4,74.87899999999999,23.171,29.662 +2020-05-10 13:30:00,46.65,72.51899999999999,23.171,29.662 +2020-05-10 13:45:00,48.47,70.95,23.171,29.662 +2020-05-10 14:00:00,49.27,73.501,19.11,29.662 +2020-05-10 14:15:00,48.56,72.59100000000001,19.11,29.662 +2020-05-10 14:30:00,47.46,71.642,19.11,29.662 +2020-05-10 14:45:00,47.02,71.206,19.11,29.662 +2020-05-10 15:00:00,46.47,72.26899999999999,19.689,29.662 +2020-05-10 15:15:00,45.86,70.492,19.689,29.662 +2020-05-10 15:30:00,47.51,69.187,19.689,29.662 +2020-05-10 15:45:00,49.12,68.445,19.689,29.662 +2020-05-10 16:00:00,54.26,68.936,22.875,29.662 +2020-05-10 16:15:00,59.43,68.202,22.875,29.662 +2020-05-10 16:30:00,61.12,68.893,22.875,29.662 +2020-05-10 16:45:00,64.01,65.495,22.875,29.662 +2020-05-10 17:00:00,66.15,66.755,33.884,29.662 +2020-05-10 17:15:00,65.55,67.977,33.884,29.662 +2020-05-10 17:30:00,68.01,69.122,33.884,29.662 +2020-05-10 17:45:00,69.14,70.696,33.884,29.662 +2020-05-10 18:00:00,71.22,72.01899999999999,38.453,29.662 +2020-05-10 18:15:00,71.38,74.692,38.453,29.662 +2020-05-10 18:30:00,71.35,73.967,38.453,29.662 +2020-05-10 18:45:00,72.02,76.55,38.453,29.662 +2020-05-10 19:00:00,71.05,75.596,39.221,29.662 +2020-05-10 19:15:00,71.32,74.34100000000001,39.221,29.662 +2020-05-10 19:30:00,76.1,74.89699999999999,39.221,29.662 +2020-05-10 19:45:00,79.65,76.161,39.221,29.662 +2020-05-10 20:00:00,78.39,75.993,37.871,29.662 +2020-05-10 20:15:00,77.6,75.27199999999999,37.871,29.662 +2020-05-10 20:30:00,79.29,73.798,37.871,29.662 +2020-05-10 20:45:00,79.19,72.845,37.871,29.662 +2020-05-10 21:00:00,79.14,70.771,36.465,29.662 +2020-05-10 21:15:00,79.16,72.43,36.465,29.662 +2020-05-10 21:30:00,76.86,72.72,36.465,29.662 +2020-05-10 21:45:00,77.71,73.296,36.465,29.662 +2020-05-10 22:00:00,72.23,73.175,36.092,29.662 +2020-05-10 22:15:00,73.28,71.986,36.092,29.662 +2020-05-10 22:30:00,71.29,69.47399999999999,36.092,29.662 +2020-05-10 22:45:00,72.19,67.111,36.092,29.662 +2020-05-10 23:00:00,66.3,60.795,31.013,29.662 +2020-05-10 23:15:00,67.7,58.853,31.013,29.662 +2020-05-10 23:30:00,66.26,57.341,31.013,29.662 +2020-05-10 23:45:00,66.87,56.893,31.013,29.662 +2020-05-11 00:00:00,68.36,49.586999999999996,31.174,29.775 +2020-05-11 00:15:00,71.71,48.857,31.174,29.775 +2020-05-11 00:30:00,71.93,47.37,31.174,29.775 +2020-05-11 00:45:00,68.14,45.806999999999995,31.174,29.775 +2020-05-11 01:00:00,62.71,46.325,29.663,29.775 +2020-05-11 01:15:00,63.59,45.534,29.663,29.775 +2020-05-11 01:30:00,63.09,43.979,29.663,29.775 +2020-05-11 01:45:00,66.91,43.59,29.663,29.775 +2020-05-11 02:00:00,71.05,44.325,28.793000000000003,29.775 +2020-05-11 02:15:00,71.22,42.299,28.793000000000003,29.775 +2020-05-11 02:30:00,67.29,45.268,28.793000000000003,29.775 +2020-05-11 02:45:00,64.47,45.673,28.793000000000003,29.775 +2020-05-11 03:00:00,65.55,49.538999999999994,27.728,29.775 +2020-05-11 03:15:00,66.79,48.979,27.728,29.775 +2020-05-11 03:30:00,67.95,48.162,27.728,29.775 +2020-05-11 03:45:00,74.12,49.006,27.728,29.775 +2020-05-11 04:00:00,80.04,60.865,29.266,29.775 +2020-05-11 04:15:00,81.67,71.962,29.266,29.775 +2020-05-11 04:30:00,80.33,71.131,29.266,29.775 +2020-05-11 04:45:00,81.18,71.55199999999999,29.266,29.775 +2020-05-11 05:00:00,89.03,96.06299999999999,37.889,29.775 +2020-05-11 05:15:00,93.69,120.575,37.889,29.775 +2020-05-11 05:30:00,95.84,110.10700000000001,37.889,29.775 +2020-05-11 05:45:00,103.12,102.885,37.889,29.775 +2020-05-11 06:00:00,109.88,104.07799999999999,55.485,29.775 +2020-05-11 06:15:00,110.72,106.779,55.485,29.775 +2020-05-11 06:30:00,107.23,104.19200000000001,55.485,29.775 +2020-05-11 06:45:00,105.05,105.00299999999999,55.485,29.775 +2020-05-11 07:00:00,107.77,106.0,65.765,29.775 +2020-05-11 07:15:00,109.31,106.066,65.765,29.775 +2020-05-11 07:30:00,106.64,103.68700000000001,65.765,29.775 +2020-05-11 07:45:00,108.08,101.87200000000001,65.765,29.775 +2020-05-11 08:00:00,112.88,97.49700000000001,56.745,29.775 +2020-05-11 08:15:00,110.39,98.29700000000001,56.745,29.775 +2020-05-11 08:30:00,108.45,95.244,56.745,29.775 +2020-05-11 08:45:00,105.26,96.137,56.745,29.775 +2020-05-11 09:00:00,105.56,89.617,53.321999999999996,29.775 +2020-05-11 09:15:00,103.65,87.292,53.321999999999996,29.775 +2020-05-11 09:30:00,103.56,88.97200000000001,53.321999999999996,29.775 +2020-05-11 09:45:00,106.22,88.381,53.321999999999996,29.775 +2020-05-11 10:00:00,102.45,85.273,51.309,29.775 +2020-05-11 10:15:00,103.67,86.678,51.309,29.775 +2020-05-11 10:30:00,103.32,86.024,51.309,29.775 +2020-05-11 10:45:00,101.26,85.666,51.309,29.775 +2020-05-11 11:00:00,99.43,81.113,50.415,29.775 +2020-05-11 11:15:00,98.15,82.15100000000001,50.415,29.775 +2020-05-11 11:30:00,98.64,84.656,50.415,29.775 +2020-05-11 11:45:00,96.79,84.916,50.415,29.775 +2020-05-11 12:00:00,96.52,82.369,48.273,29.775 +2020-05-11 12:15:00,96.68,83.20299999999999,48.273,29.775 +2020-05-11 12:30:00,95.66,80.89699999999999,48.273,29.775 +2020-05-11 12:45:00,95.11,81.42,48.273,29.775 +2020-05-11 13:00:00,94.53,83.427,48.452,29.775 +2020-05-11 13:15:00,101.79,82.205,48.452,29.775 +2020-05-11 13:30:00,101.81,79.762,48.452,29.775 +2020-05-11 13:45:00,100.1,78.861,48.452,29.775 +2020-05-11 14:00:00,93.14,80.579,48.35,29.775 +2020-05-11 14:15:00,93.8,79.82300000000001,48.35,29.775 +2020-05-11 14:30:00,92.72,78.492,48.35,29.775 +2020-05-11 14:45:00,95.44,79.55,48.35,29.775 +2020-05-11 15:00:00,93.18,81.13600000000001,48.838,29.775 +2020-05-11 15:15:00,91.76,78.321,48.838,29.775 +2020-05-11 15:30:00,93.36,77.204,48.838,29.775 +2020-05-11 15:45:00,94.72,75.90899999999999,48.838,29.775 +2020-05-11 16:00:00,95.88,77.11399999999999,50.873000000000005,29.775 +2020-05-11 16:15:00,96.96,76.11,50.873000000000005,29.775 +2020-05-11 16:30:00,99.03,75.937,50.873000000000005,29.775 +2020-05-11 16:45:00,101.19,72.03,50.873000000000005,29.775 +2020-05-11 17:00:00,103.98,72.402,56.637,29.775 +2020-05-11 17:15:00,104.09,73.51,56.637,29.775 +2020-05-11 17:30:00,105.88,74.139,56.637,29.775 +2020-05-11 17:45:00,105.66,74.80199999999999,56.637,29.775 +2020-05-11 18:00:00,106.65,75.36399999999999,56.35,29.775 +2020-05-11 18:15:00,105.32,75.806,56.35,29.775 +2020-05-11 18:30:00,112.4,74.733,56.35,29.775 +2020-05-11 18:45:00,113.9,80.008,56.35,29.775 +2020-05-11 19:00:00,103.24,78.153,56.023,29.775 +2020-05-11 19:15:00,99.4,77.52,56.023,29.775 +2020-05-11 19:30:00,100.03,77.965,56.023,29.775 +2020-05-11 19:45:00,99.47,78.46300000000001,56.023,29.775 +2020-05-11 20:00:00,100.76,76.457,62.372,29.775 +2020-05-11 20:15:00,98.44,75.921,62.372,29.775 +2020-05-11 20:30:00,97.74,74.20100000000001,62.372,29.775 +2020-05-11 20:45:00,96.87,74.044,62.372,29.775 +2020-05-11 21:00:00,95.03,71.687,57.516999999999996,29.775 +2020-05-11 21:15:00,93.92,73.28399999999999,57.516999999999996,29.775 +2020-05-11 21:30:00,95.99,73.58,57.516999999999996,29.775 +2020-05-11 21:45:00,94.93,73.828,57.516999999999996,29.775 +2020-05-11 22:00:00,89.61,71.101,51.823,29.775 +2020-05-11 22:15:00,82.35,71.01100000000001,51.823,29.775 +2020-05-11 22:30:00,85.45,61.678999999999995,51.823,29.775 +2020-05-11 22:45:00,85.91,57.555,51.823,29.775 +2020-05-11 23:00:00,82.2,51.543,43.832,29.775 +2020-05-11 23:15:00,78.11,49.298,43.832,29.775 +2020-05-11 23:30:00,80.04,48.743,43.832,29.775 +2020-05-11 23:45:00,81.09,48.815,43.832,29.775 +2020-05-12 00:00:00,78.75,47.239,42.371,29.775 +2020-05-12 00:15:00,74.73,47.705,42.371,29.775 +2020-05-12 00:30:00,75.86,46.446000000000005,42.371,29.775 +2020-05-12 00:45:00,79.25,45.168,42.371,29.775 +2020-05-12 01:00:00,77.81,45.195,39.597,29.775 +2020-05-12 01:15:00,76.91,44.31100000000001,39.597,29.775 +2020-05-12 01:30:00,71.67,42.696999999999996,39.597,29.775 +2020-05-12 01:45:00,78.35,42.011,39.597,29.775 +2020-05-12 02:00:00,78.21,42.357,38.298,29.775 +2020-05-12 02:15:00,77.57,41.191,38.298,29.775 +2020-05-12 02:30:00,70.31,43.653,38.298,29.775 +2020-05-12 02:45:00,70.29,44.328,38.298,29.775 +2020-05-12 03:00:00,71.96,47.341,37.884,29.775 +2020-05-12 03:15:00,73.12,47.2,37.884,29.775 +2020-05-12 03:30:00,74.65,46.527,37.884,29.775 +2020-05-12 03:45:00,76.43,46.586000000000006,37.884,29.775 +2020-05-12 04:00:00,82.71,57.331,39.442,29.775 +2020-05-12 04:15:00,87.45,68.267,39.442,29.775 +2020-05-12 04:30:00,90.35,67.226,39.442,29.775 +2020-05-12 04:45:00,87.36,68.453,39.442,29.775 +2020-05-12 05:00:00,92.61,95.80799999999999,43.608000000000004,29.775 +2020-05-12 05:15:00,95.01,120.62100000000001,43.608000000000004,29.775 +2020-05-12 05:30:00,97.1,109.846,43.608000000000004,29.775 +2020-05-12 05:45:00,99.06,102.089,43.608000000000004,29.775 +2020-05-12 06:00:00,104.0,103.76,54.99100000000001,29.775 +2020-05-12 06:15:00,104.83,107.14399999999999,54.99100000000001,29.775 +2020-05-12 06:30:00,107.38,104.08200000000001,54.99100000000001,29.775 +2020-05-12 06:45:00,107.97,104.059,54.99100000000001,29.775 +2020-05-12 07:00:00,108.87,105.101,66.217,29.775 +2020-05-12 07:15:00,110.38,104.91,66.217,29.775 +2020-05-12 07:30:00,109.05,102.322,66.217,29.775 +2020-05-12 07:45:00,113.65,99.833,66.217,29.775 +2020-05-12 08:00:00,108.2,95.444,60.151,29.775 +2020-05-12 08:15:00,103.72,95.56299999999999,60.151,29.775 +2020-05-12 08:30:00,103.77,92.559,60.151,29.775 +2020-05-12 08:45:00,104.97,92.65899999999999,60.151,29.775 +2020-05-12 09:00:00,109.3,86.09100000000001,53.873000000000005,29.775 +2020-05-12 09:15:00,106.31,84.264,53.873000000000005,29.775 +2020-05-12 09:30:00,107.3,86.738,53.873000000000005,29.775 +2020-05-12 09:45:00,105.8,87.12700000000001,53.873000000000005,29.775 +2020-05-12 10:00:00,103.34,82.756,51.417,29.775 +2020-05-12 10:15:00,109.67,83.676,51.417,29.775 +2020-05-12 10:30:00,105.75,83.12700000000001,51.417,29.775 +2020-05-12 10:45:00,108.2,83.65,51.417,29.775 +2020-05-12 11:00:00,106.71,79.682,50.43600000000001,29.775 +2020-05-12 11:15:00,107.35,80.905,50.43600000000001,29.775 +2020-05-12 11:30:00,110.29,82.086,50.43600000000001,29.775 +2020-05-12 11:45:00,120.56,82.305,50.43600000000001,29.775 +2020-05-12 12:00:00,127.19,79.093,47.468,29.775 +2020-05-12 12:15:00,125.28,80.00399999999999,47.468,29.775 +2020-05-12 12:30:00,115.59,78.583,47.468,29.775 +2020-05-12 12:45:00,116.61,79.493,47.468,29.775 +2020-05-12 13:00:00,118.57,81.10600000000001,48.453,29.775 +2020-05-12 13:15:00,118.1,81.064,48.453,29.775 +2020-05-12 13:30:00,109.12,79.085,48.453,29.775 +2020-05-12 13:45:00,111.48,77.554,48.453,29.775 +2020-05-12 14:00:00,124.56,79.757,48.435,29.775 +2020-05-12 14:15:00,123.29,78.90899999999999,48.435,29.775 +2020-05-12 14:30:00,118.12,78.03399999999999,48.435,29.775 +2020-05-12 14:45:00,118.02,78.485,48.435,29.775 +2020-05-12 15:00:00,121.42,79.819,49.966,29.775 +2020-05-12 15:15:00,120.43,77.8,49.966,29.775 +2020-05-12 15:30:00,116.21,76.589,49.966,29.775 +2020-05-12 15:45:00,114.12,75.368,49.966,29.775 +2020-05-12 16:00:00,113.27,76.267,51.184,29.775 +2020-05-12 16:15:00,110.03,75.498,51.184,29.775 +2020-05-12 16:30:00,113.31,75.309,51.184,29.775 +2020-05-12 16:45:00,110.3,71.995,51.184,29.775 +2020-05-12 17:00:00,113.37,72.785,56.138999999999996,29.775 +2020-05-12 17:15:00,113.49,74.193,56.138999999999996,29.775 +2020-05-12 17:30:00,116.69,74.74,56.138999999999996,29.775 +2020-05-12 17:45:00,114.28,75.071,56.138999999999996,29.775 +2020-05-12 18:00:00,119.23,74.863,57.038000000000004,29.775 +2020-05-12 18:15:00,118.62,76.265,57.038000000000004,29.775 +2020-05-12 18:30:00,115.63,74.863,57.038000000000004,29.775 +2020-05-12 18:45:00,111.37,80.283,57.038000000000004,29.775 +2020-05-12 19:00:00,112.68,77.525,56.492,29.775 +2020-05-12 19:15:00,112.7,76.933,56.492,29.775 +2020-05-12 19:30:00,108.78,76.973,56.492,29.775 +2020-05-12 19:45:00,103.96,77.756,56.492,29.775 +2020-05-12 20:00:00,102.83,76.097,62.534,29.775 +2020-05-12 20:15:00,99.27,74.208,62.534,29.775 +2020-05-12 20:30:00,100.18,72.914,62.534,29.775 +2020-05-12 20:45:00,106.01,72.91199999999999,62.534,29.775 +2020-05-12 21:00:00,98.91,70.986,55.506,29.775 +2020-05-12 21:15:00,97.97,71.77199999999999,55.506,29.775 +2020-05-12 21:30:00,90.65,71.925,55.506,29.775 +2020-05-12 21:45:00,87.9,72.434,55.506,29.775 +2020-05-12 22:00:00,89.08,70.472,51.472,29.775 +2020-05-12 22:15:00,88.65,70.043,51.472,29.775 +2020-05-12 22:30:00,85.02,60.961000000000006,51.472,29.775 +2020-05-12 22:45:00,79.11,56.92100000000001,51.472,29.775 +2020-05-12 23:00:00,74.3,50.28,44.593,29.775 +2020-05-12 23:15:00,77.56,48.982,44.593,29.775 +2020-05-12 23:30:00,74.7,48.299,44.593,29.775 +2020-05-12 23:45:00,74.02,48.355,44.593,29.775 +2020-05-13 00:00:00,77.65,46.93,41.978,29.775 +2020-05-13 00:15:00,79.78,47.406000000000006,41.978,29.775 +2020-05-13 00:30:00,79.02,46.145,41.978,29.775 +2020-05-13 00:45:00,74.67,44.873000000000005,41.978,29.775 +2020-05-13 01:00:00,70.45,44.906000000000006,38.59,29.775 +2020-05-13 01:15:00,75.07,43.998999999999995,38.59,29.775 +2020-05-13 01:30:00,76.59,42.369,38.59,29.775 +2020-05-13 01:45:00,78.3,41.681000000000004,38.59,29.775 +2020-05-13 02:00:00,73.41,42.021,36.23,29.775 +2020-05-13 02:15:00,70.67,40.841,36.23,29.775 +2020-05-13 02:30:00,69.26,43.31399999999999,36.23,29.775 +2020-05-13 02:45:00,70.7,43.997,36.23,29.775 +2020-05-13 03:00:00,72.64,47.019,35.867,29.775 +2020-05-13 03:15:00,72.13,46.858999999999995,35.867,29.775 +2020-05-13 03:30:00,73.68,46.188,35.867,29.775 +2020-05-13 03:45:00,77.49,46.271,35.867,29.775 +2020-05-13 04:00:00,80.21,56.97,36.75,29.775 +2020-05-13 04:15:00,87.8,67.86,36.75,29.775 +2020-05-13 04:30:00,91.75,66.811,36.75,29.775 +2020-05-13 04:45:00,96.22,68.03,36.75,29.775 +2020-05-13 05:00:00,96.43,95.272,40.461,29.775 +2020-05-13 05:15:00,98.98,119.954,40.461,29.775 +2020-05-13 05:30:00,102.79,109.227,40.461,29.775 +2020-05-13 05:45:00,105.22,101.527,40.461,29.775 +2020-05-13 06:00:00,111.07,103.219,55.481,29.775 +2020-05-13 06:15:00,111.37,106.58,55.481,29.775 +2020-05-13 06:30:00,115.97,103.52,55.481,29.775 +2020-05-13 06:45:00,119.83,103.505,55.481,29.775 +2020-05-13 07:00:00,123.06,104.54,68.45,29.775 +2020-05-13 07:15:00,124.82,104.348,68.45,29.775 +2020-05-13 07:30:00,124.87,101.734,68.45,29.775 +2020-05-13 07:45:00,126.18,99.264,68.45,29.775 +2020-05-13 08:00:00,124.84,94.87200000000001,60.885,29.775 +2020-05-13 08:15:00,125.61,95.03299999999999,60.885,29.775 +2020-05-13 08:30:00,127.05,92.01299999999999,60.885,29.775 +2020-05-13 08:45:00,130.24,92.135,60.885,29.775 +2020-05-13 09:00:00,129.22,85.56700000000001,56.887,29.775 +2020-05-13 09:15:00,132.69,83.745,56.887,29.775 +2020-05-13 09:30:00,132.0,86.23,56.887,29.775 +2020-05-13 09:45:00,133.26,86.65,56.887,29.775 +2020-05-13 10:00:00,132.15,82.288,54.401,29.775 +2020-05-13 10:15:00,133.61,83.245,54.401,29.775 +2020-05-13 10:30:00,132.7,82.713,54.401,29.775 +2020-05-13 10:45:00,129.86,83.25200000000001,54.401,29.775 +2020-05-13 11:00:00,125.77,79.27600000000001,53.678000000000004,29.775 +2020-05-13 11:15:00,124.22,80.518,53.678000000000004,29.775 +2020-05-13 11:30:00,132.15,81.693,53.678000000000004,29.775 +2020-05-13 11:45:00,134.23,81.925,53.678000000000004,29.775 +2020-05-13 12:00:00,133.92,78.745,51.68,29.775 +2020-05-13 12:15:00,126.49,79.66,51.68,29.775 +2020-05-13 12:30:00,126.47,78.203,51.68,29.775 +2020-05-13 12:45:00,126.1,79.117,51.68,29.775 +2020-05-13 13:00:00,122.35,80.75399999999999,51.263000000000005,29.775 +2020-05-13 13:15:00,121.6,80.712,51.263000000000005,29.775 +2020-05-13 13:30:00,115.7,78.74,51.263000000000005,29.775 +2020-05-13 13:45:00,116.2,77.212,51.263000000000005,29.775 +2020-05-13 14:00:00,122.46,79.46,51.107,29.775 +2020-05-13 14:15:00,118.59,78.6,51.107,29.775 +2020-05-13 14:30:00,107.28,77.684,51.107,29.775 +2020-05-13 14:45:00,98.18,78.138,51.107,29.775 +2020-05-13 15:00:00,97.87,79.513,51.498000000000005,29.775 +2020-05-13 15:15:00,105.19,77.479,51.498000000000005,29.775 +2020-05-13 15:30:00,108.25,76.236,51.498000000000005,29.775 +2020-05-13 15:45:00,112.75,74.999,51.498000000000005,29.775 +2020-05-13 16:00:00,115.28,75.942,53.376999999999995,29.775 +2020-05-13 16:15:00,121.08,75.155,53.376999999999995,29.775 +2020-05-13 16:30:00,127.19,74.976,53.376999999999995,29.775 +2020-05-13 16:45:00,124.65,71.605,53.376999999999995,29.775 +2020-05-13 17:00:00,116.0,72.441,56.965,29.775 +2020-05-13 17:15:00,118.64,73.824,56.965,29.775 +2020-05-13 17:30:00,119.77,74.36,56.965,29.775 +2020-05-13 17:45:00,121.03,74.661,56.965,29.775 +2020-05-13 18:00:00,116.2,74.469,58.231,29.775 +2020-05-13 18:15:00,110.61,75.866,58.231,29.775 +2020-05-13 18:30:00,111.68,74.452,58.231,29.775 +2020-05-13 18:45:00,115.74,79.872,58.231,29.775 +2020-05-13 19:00:00,115.72,77.115,58.865,29.775 +2020-05-13 19:15:00,108.85,76.52199999999999,58.865,29.775 +2020-05-13 19:30:00,105.3,76.563,58.865,29.775 +2020-05-13 19:45:00,108.65,77.355,58.865,29.775 +2020-05-13 20:00:00,108.52,75.672,65.605,29.775 +2020-05-13 20:15:00,107.64,73.78399999999999,65.605,29.775 +2020-05-13 20:30:00,103.12,72.518,65.605,29.775 +2020-05-13 20:45:00,101.58,72.55199999999999,65.605,29.775 +2020-05-13 21:00:00,99.72,70.63,58.083999999999996,29.775 +2020-05-13 21:15:00,98.92,71.432,58.083999999999996,29.775 +2020-05-13 21:30:00,96.57,71.565,58.083999999999996,29.775 +2020-05-13 21:45:00,89.44,72.09899999999999,58.083999999999996,29.775 +2020-05-13 22:00:00,85.38,70.156,53.243,29.775 +2020-05-13 22:15:00,89.96,69.749,53.243,29.775 +2020-05-13 22:30:00,86.78,60.653,53.243,29.775 +2020-05-13 22:45:00,85.75,56.601000000000006,53.243,29.775 +2020-05-13 23:00:00,75.39,49.934,44.283,29.775 +2020-05-13 23:15:00,74.31,48.676,44.283,29.775 +2020-05-13 23:30:00,74.53,47.994,44.283,29.775 +2020-05-13 23:45:00,72.86,48.048,44.283,29.775 +2020-05-14 00:00:00,69.32,46.623000000000005,40.219,29.775 +2020-05-14 00:15:00,70.84,47.11,40.219,29.775 +2020-05-14 00:30:00,69.79,45.847,40.219,29.775 +2020-05-14 00:45:00,70.68,44.582,40.219,29.775 +2020-05-14 01:00:00,70.16,44.619,37.959,29.775 +2020-05-14 01:15:00,69.63,43.68899999999999,37.959,29.775 +2020-05-14 01:30:00,70.15,42.043,37.959,29.775 +2020-05-14 01:45:00,70.69,41.355,37.959,29.775 +2020-05-14 02:00:00,69.83,41.687,36.113,29.775 +2020-05-14 02:15:00,70.46,40.495,36.113,29.775 +2020-05-14 02:30:00,71.03,42.979,36.113,29.775 +2020-05-14 02:45:00,70.72,43.668,36.113,29.775 +2020-05-14 03:00:00,71.24,46.7,35.546,29.775 +2020-05-14 03:15:00,72.5,46.523999999999994,35.546,29.775 +2020-05-14 03:30:00,73.71,45.853,35.546,29.775 +2020-05-14 03:45:00,75.65,45.958999999999996,35.546,29.775 +2020-05-14 04:00:00,79.12,56.613,37.169000000000004,29.775 +2020-05-14 04:15:00,79.46,67.456,37.169000000000004,29.775 +2020-05-14 04:30:00,82.89,66.40100000000001,37.169000000000004,29.775 +2020-05-14 04:45:00,85.82,67.613,37.169000000000004,29.775 +2020-05-14 05:00:00,95.94,94.743,41.233000000000004,29.775 +2020-05-14 05:15:00,98.55,119.294,41.233000000000004,29.775 +2020-05-14 05:30:00,99.72,108.61399999999999,41.233000000000004,29.775 +2020-05-14 05:45:00,102.92,100.969,41.233000000000004,29.775 +2020-05-14 06:00:00,110.05,102.68299999999999,52.57,29.775 +2020-05-14 06:15:00,112.59,106.021,52.57,29.775 +2020-05-14 06:30:00,116.11,102.962,52.57,29.775 +2020-05-14 06:45:00,118.57,102.956,52.57,29.775 +2020-05-14 07:00:00,121.86,103.98200000000001,64.53,29.775 +2020-05-14 07:15:00,122.71,103.792,64.53,29.775 +2020-05-14 07:30:00,123.27,101.15100000000001,64.53,29.775 +2020-05-14 07:45:00,125.08,98.7,64.53,29.775 +2020-05-14 08:00:00,124.66,94.306,55.911,29.775 +2020-05-14 08:15:00,124.04,94.51,55.911,29.775 +2020-05-14 08:30:00,126.3,91.473,55.911,29.775 +2020-05-14 08:45:00,127.75,91.618,55.911,29.775 +2020-05-14 09:00:00,125.81,85.04799999999999,50.949,29.775 +2020-05-14 09:15:00,127.78,83.234,50.949,29.775 +2020-05-14 09:30:00,127.51,85.727,50.949,29.775 +2020-05-14 09:45:00,129.58,86.179,50.949,29.775 +2020-05-14 10:00:00,129.11,81.82600000000001,48.136,29.775 +2020-05-14 10:15:00,129.29,82.819,48.136,29.775 +2020-05-14 10:30:00,129.5,82.303,48.136,29.775 +2020-05-14 10:45:00,130.85,82.85600000000001,48.136,29.775 +2020-05-14 11:00:00,127.65,78.875,46.643,29.775 +2020-05-14 11:15:00,126.84,80.135,46.643,29.775 +2020-05-14 11:30:00,124.7,81.304,46.643,29.775 +2020-05-14 11:45:00,126.76,81.54899999999999,46.643,29.775 +2020-05-14 12:00:00,124.85,78.4,44.098,29.775 +2020-05-14 12:15:00,122.87,79.321,44.098,29.775 +2020-05-14 12:30:00,122.4,77.82600000000001,44.098,29.775 +2020-05-14 12:45:00,118.93,78.745,44.098,29.775 +2020-05-14 13:00:00,117.21,80.405,43.717,29.775 +2020-05-14 13:15:00,120.11,80.363,43.717,29.775 +2020-05-14 13:30:00,119.68,78.4,43.717,29.775 +2020-05-14 13:45:00,123.45,76.874,43.717,29.775 +2020-05-14 14:00:00,125.18,79.166,44.218999999999994,29.775 +2020-05-14 14:15:00,124.9,78.294,44.218999999999994,29.775 +2020-05-14 14:30:00,118.8,77.33800000000001,44.218999999999994,29.775 +2020-05-14 14:45:00,111.7,77.794,44.218999999999994,29.775 +2020-05-14 15:00:00,111.77,79.21,46.159,29.775 +2020-05-14 15:15:00,115.84,77.15899999999999,46.159,29.775 +2020-05-14 15:30:00,116.18,75.887,46.159,29.775 +2020-05-14 15:45:00,115.11,74.634,46.159,29.775 +2020-05-14 16:00:00,111.0,75.619,47.115,29.775 +2020-05-14 16:15:00,111.54,74.814,47.115,29.775 +2020-05-14 16:30:00,116.27,74.64699999999999,47.115,29.775 +2020-05-14 16:45:00,116.72,71.22,47.115,29.775 +2020-05-14 17:00:00,114.96,72.101,50.827,29.775 +2020-05-14 17:15:00,108.11,73.459,50.827,29.775 +2020-05-14 17:30:00,108.02,73.984,50.827,29.775 +2020-05-14 17:45:00,108.53,74.25399999999999,50.827,29.775 +2020-05-14 18:00:00,109.63,74.078,52.586000000000006,29.775 +2020-05-14 18:15:00,108.24,75.471,52.586000000000006,29.775 +2020-05-14 18:30:00,113.52,74.045,52.586000000000006,29.775 +2020-05-14 18:45:00,109.72,79.467,52.586000000000006,29.775 +2020-05-14 19:00:00,112.3,76.709,51.886,29.775 +2020-05-14 19:15:00,106.96,76.115,51.886,29.775 +2020-05-14 19:30:00,106.72,76.158,51.886,29.775 +2020-05-14 19:45:00,107.78,76.959,51.886,29.775 +2020-05-14 20:00:00,105.58,75.251,56.162,29.775 +2020-05-14 20:15:00,99.35,73.365,56.162,29.775 +2020-05-14 20:30:00,102.93,72.126,56.162,29.775 +2020-05-14 20:45:00,105.28,72.195,56.162,29.775 +2020-05-14 21:00:00,103.12,70.278,53.023,29.775 +2020-05-14 21:15:00,99.97,71.095,53.023,29.775 +2020-05-14 21:30:00,93.22,71.209,53.023,29.775 +2020-05-14 21:45:00,93.97,71.766,53.023,29.775 +2020-05-14 22:00:00,90.52,69.846,49.303999999999995,29.775 +2020-05-14 22:15:00,90.9,69.455,49.303999999999995,29.775 +2020-05-14 22:30:00,84.63,60.348,49.303999999999995,29.775 +2020-05-14 22:45:00,88.26,56.285,49.303999999999995,29.775 +2020-05-14 23:00:00,82.53,49.589,43.409,29.775 +2020-05-14 23:15:00,81.52,48.373000000000005,43.409,29.775 +2020-05-14 23:30:00,75.59,47.691,43.409,29.775 +2020-05-14 23:45:00,81.48,47.743,43.409,29.775 +2020-05-15 00:00:00,78.17,44.512,39.884,29.775 +2020-05-15 00:15:00,78.44,45.251999999999995,39.884,29.775 +2020-05-15 00:30:00,73.49,44.153,39.884,29.775 +2020-05-15 00:45:00,78.17,43.27,39.884,29.775 +2020-05-15 01:00:00,78.18,42.896,37.658,29.775 +2020-05-15 01:15:00,78.15,41.803000000000004,37.658,29.775 +2020-05-15 01:30:00,71.38,40.624,37.658,29.775 +2020-05-15 01:45:00,77.87,39.775999999999996,37.658,29.775 +2020-05-15 02:00:00,77.41,40.869,36.707,29.775 +2020-05-15 02:15:00,78.35,39.583,36.707,29.775 +2020-05-15 02:30:00,73.66,42.92,36.707,29.775 +2020-05-15 02:45:00,70.89,43.092,36.707,29.775 +2020-05-15 03:00:00,72.47,46.383,37.025,29.775 +2020-05-15 03:15:00,72.41,45.57899999999999,37.025,29.775 +2020-05-15 03:30:00,73.92,44.714,37.025,29.775 +2020-05-15 03:45:00,77.06,45.655,37.025,29.775 +2020-05-15 04:00:00,81.51,56.457,38.349000000000004,29.775 +2020-05-15 04:15:00,79.37,65.932,38.349000000000004,29.775 +2020-05-15 04:30:00,82.59,65.705,38.349000000000004,29.775 +2020-05-15 04:45:00,85.66,65.982,38.349000000000004,29.775 +2020-05-15 05:00:00,95.76,92.154,41.565,29.775 +2020-05-15 05:15:00,97.85,117.869,41.565,29.775 +2020-05-15 05:30:00,101.38,107.76899999999999,41.565,29.775 +2020-05-15 05:45:00,103.44,99.796,41.565,29.775 +2020-05-15 06:00:00,108.63,101.87700000000001,53.861000000000004,29.775 +2020-05-15 06:15:00,111.05,104.807,53.861000000000004,29.775 +2020-05-15 06:30:00,114.06,101.404,53.861000000000004,29.775 +2020-05-15 06:45:00,116.5,101.876,53.861000000000004,29.775 +2020-05-15 07:00:00,118.58,103.118,63.497,29.775 +2020-05-15 07:15:00,119.62,104.01899999999999,63.497,29.775 +2020-05-15 07:30:00,123.22,99.75399999999999,63.497,29.775 +2020-05-15 07:45:00,121.51,96.83,63.497,29.775 +2020-05-15 08:00:00,120.07,92.62799999999999,55.43899999999999,29.775 +2020-05-15 08:15:00,120.52,93.241,55.43899999999999,29.775 +2020-05-15 08:30:00,121.85,90.48700000000001,55.43899999999999,29.775 +2020-05-15 08:45:00,123.22,89.939,55.43899999999999,29.775 +2020-05-15 09:00:00,120.17,81.768,52.132,29.775 +2020-05-15 09:15:00,120.79,81.646,52.132,29.775 +2020-05-15 09:30:00,120.3,83.456,52.132,29.775 +2020-05-15 09:45:00,121.98,84.19,52.132,29.775 +2020-05-15 10:00:00,122.01,79.236,49.881,29.775 +2020-05-15 10:15:00,122.16,80.387,49.881,29.775 +2020-05-15 10:30:00,125.08,80.267,49.881,29.775 +2020-05-15 10:45:00,121.4,80.583,49.881,29.775 +2020-05-15 11:00:00,118.56,76.764,49.396,29.775 +2020-05-15 11:15:00,117.53,76.866,49.396,29.775 +2020-05-15 11:30:00,115.03,78.49,49.396,29.775 +2020-05-15 11:45:00,117.11,78.059,49.396,29.775 +2020-05-15 12:00:00,113.62,75.738,46.7,29.775 +2020-05-15 12:15:00,112.54,75.296,46.7,29.775 +2020-05-15 12:30:00,109.67,73.919,46.7,29.775 +2020-05-15 12:45:00,110.33,74.495,46.7,29.775 +2020-05-15 13:00:00,105.34,76.998,44.05,29.775 +2020-05-15 13:15:00,104.26,77.437,44.05,29.775 +2020-05-15 13:30:00,101.89,76.069,44.05,29.775 +2020-05-15 13:45:00,103.1,74.741,44.05,29.775 +2020-05-15 14:00:00,99.61,76.006,42.805,29.775 +2020-05-15 14:15:00,96.97,75.365,42.805,29.775 +2020-05-15 14:30:00,94.75,75.683,42.805,29.775 +2020-05-15 14:45:00,93.73,75.768,42.805,29.775 +2020-05-15 15:00:00,96.11,76.961,44.36600000000001,29.775 +2020-05-15 15:15:00,94.15,74.533,44.36600000000001,29.775 +2020-05-15 15:30:00,94.02,72.171,44.36600000000001,29.775 +2020-05-15 15:45:00,94.72,71.51899999999999,44.36600000000001,29.775 +2020-05-15 16:00:00,96.97,71.47399999999999,46.928999999999995,29.775 +2020-05-15 16:15:00,96.61,71.137,46.928999999999995,29.775 +2020-05-15 16:30:00,99.35,70.885,46.928999999999995,29.775 +2020-05-15 16:45:00,100.89,66.767,46.928999999999995,29.775 +2020-05-15 17:00:00,107.94,69.078,51.468,29.775 +2020-05-15 17:15:00,103.98,70.115,51.468,29.775 +2020-05-15 17:30:00,103.89,70.623,51.468,29.775 +2020-05-15 17:45:00,105.25,70.645,51.468,29.775 +2020-05-15 18:00:00,106.7,70.852,52.58,29.775 +2020-05-15 18:15:00,104.83,71.363,52.58,29.775 +2020-05-15 18:30:00,111.99,69.997,52.58,29.775 +2020-05-15 18:45:00,112.9,75.74,52.58,29.775 +2020-05-15 19:00:00,111.75,74.04899999999999,52.183,29.775 +2020-05-15 19:15:00,103.98,74.419,52.183,29.775 +2020-05-15 19:30:00,101.92,74.359,52.183,29.775 +2020-05-15 19:45:00,98.71,74.2,52.183,29.775 +2020-05-15 20:00:00,97.62,72.35,58.497,29.775 +2020-05-15 20:15:00,103.82,71.09,58.497,29.775 +2020-05-15 20:30:00,102.75,69.482,58.497,29.775 +2020-05-15 20:45:00,100.04,69.16199999999999,58.497,29.775 +2020-05-15 21:00:00,88.18,68.469,54.731,29.775 +2020-05-15 21:15:00,88.72,70.771,54.731,29.775 +2020-05-15 21:30:00,85.61,70.765,54.731,29.775 +2020-05-15 21:45:00,83.9,71.708,54.731,29.775 +2020-05-15 22:00:00,82.78,70.062,51.386,29.775 +2020-05-15 22:15:00,84.75,69.445,51.386,29.775 +2020-05-15 22:30:00,82.82,66.408,51.386,29.775 +2020-05-15 22:45:00,80.16,64.311,51.386,29.775 +2020-05-15 23:00:00,71.33,58.746,44.463,29.775 +2020-05-15 23:15:00,70.6,55.622,44.463,29.775 +2020-05-15 23:30:00,69.36,52.972,44.463,29.775 +2020-05-15 23:45:00,70.97,52.681000000000004,44.463,29.775 +2020-05-16 00:00:00,74.15,36.794000000000004,42.833999999999996,29.662 +2020-05-16 00:15:00,73.98,35.758,42.833999999999996,29.662 +2020-05-16 00:30:00,71.46,34.895,42.833999999999996,29.662 +2020-05-16 00:45:00,69.22,33.669000000000004,42.833999999999996,29.662 +2020-05-16 01:00:00,72.75,33.125,37.859,29.662 +2020-05-16 01:15:00,75.46,32.548,37.859,29.662 +2020-05-16 01:30:00,69.76,30.769000000000002,37.859,29.662 +2020-05-16 01:45:00,65.29,30.721,37.859,29.662 +2020-05-16 02:00:00,67.91,30.855,35.327,29.662 +2020-05-16 02:15:00,70.65,28.666,35.327,29.662 +2020-05-16 02:30:00,70.38,30.735,35.327,29.662 +2020-05-16 02:45:00,64.65,31.566,35.327,29.662 +2020-05-16 03:00:00,63.24,33.628,34.908,29.662 +2020-05-16 03:15:00,64.63,30.669,34.908,29.662 +2020-05-16 03:30:00,65.46,29.659000000000002,34.908,29.662 +2020-05-16 03:45:00,65.23,31.218000000000004,34.908,29.662 +2020-05-16 04:00:00,62.15,39.24,34.84,29.662 +2020-05-16 04:15:00,61.52,47.104,34.84,29.662 +2020-05-16 04:30:00,61.86,43.983999999999995,34.84,29.662 +2020-05-16 04:45:00,62.99,44.071999999999996,34.84,29.662 +2020-05-16 05:00:00,64.2,57.34,34.222,29.662 +2020-05-16 05:15:00,64.4,64.827,34.222,29.662 +2020-05-16 05:30:00,63.92,55.413999999999994,34.222,29.662 +2020-05-16 05:45:00,66.28,53.871,34.222,29.662 +2020-05-16 06:00:00,69.14,69.734,35.515,29.662 +2020-05-16 06:15:00,69.45,82.39299999999999,35.515,29.662 +2020-05-16 06:30:00,71.81,75.32300000000001,35.515,29.662 +2020-05-16 06:45:00,72.47,70.584,35.515,29.662 +2020-05-16 07:00:00,73.5,68.16199999999999,39.687,29.662 +2020-05-16 07:15:00,74.27,67.108,39.687,29.662 +2020-05-16 07:30:00,75.61,64.484,39.687,29.662 +2020-05-16 07:45:00,78.65,63.838,39.687,29.662 +2020-05-16 08:00:00,77.98,59.902,44.9,29.662 +2020-05-16 08:15:00,77.32,62.077,44.9,29.662 +2020-05-16 08:30:00,76.53,61.327,44.9,29.662 +2020-05-16 08:45:00,77.34,63.946000000000005,44.9,29.662 +2020-05-16 09:00:00,78.57,61.705,45.724,29.662 +2020-05-16 09:15:00,78.98,62.68899999999999,45.724,29.662 +2020-05-16 09:30:00,74.87,65.62899999999999,45.724,29.662 +2020-05-16 09:45:00,76.27,66.71600000000001,45.724,29.662 +2020-05-16 10:00:00,78.95,63.416000000000004,43.123999999999995,29.662 +2020-05-16 10:15:00,81.25,64.873,43.123999999999995,29.662 +2020-05-16 10:30:00,84.8,64.884,43.123999999999995,29.662 +2020-05-16 10:45:00,82.3,65.693,43.123999999999995,29.662 +2020-05-16 11:00:00,82.79,61.979,40.255,29.662 +2020-05-16 11:15:00,83.53,62.608000000000004,40.255,29.662 +2020-05-16 11:30:00,79.48,64.295,40.255,29.662 +2020-05-16 11:45:00,74.97,64.98,40.255,29.662 +2020-05-16 12:00:00,68.37,60.761,38.582,29.662 +2020-05-16 12:15:00,68.21,60.949,38.582,29.662 +2020-05-16 12:30:00,66.19,60.332,38.582,29.662 +2020-05-16 12:45:00,65.99,61.598,38.582,29.662 +2020-05-16 13:00:00,65.29,62.047,36.043,29.662 +2020-05-16 13:15:00,66.66,62.114,36.043,29.662 +2020-05-16 13:30:00,65.2,60.93,36.043,29.662 +2020-05-16 13:45:00,65.57,58.292,36.043,29.662 +2020-05-16 14:00:00,65.77,58.663999999999994,35.216,29.662 +2020-05-16 14:15:00,65.43,56.394,35.216,29.662 +2020-05-16 14:30:00,67.17,56.395,35.216,29.662 +2020-05-16 14:45:00,69.52,56.943000000000005,35.216,29.662 +2020-05-16 15:00:00,68.76,57.736000000000004,36.759,29.662 +2020-05-16 15:15:00,70.34,55.881,36.759,29.662 +2020-05-16 15:30:00,68.44,53.98,36.759,29.662 +2020-05-16 15:45:00,69.99,51.562,36.759,29.662 +2020-05-16 16:00:00,72.09,54.672,40.086,29.662 +2020-05-16 16:15:00,70.51,54.178000000000004,40.086,29.662 +2020-05-16 16:30:00,72.95,53.538000000000004,40.086,29.662 +2020-05-16 16:45:00,72.75,49.865,40.086,29.662 +2020-05-16 17:00:00,75.24,52.239,44.876999999999995,29.662 +2020-05-16 17:15:00,76.28,51.114,44.876999999999995,29.662 +2020-05-16 17:30:00,78.78,50.931000000000004,44.876999999999995,29.662 +2020-05-16 17:45:00,80.15,50.091,44.876999999999995,29.662 +2020-05-16 18:00:00,84.39,54.299,47.056000000000004,29.662 +2020-05-16 18:15:00,81.11,56.321000000000005,47.056000000000004,29.662 +2020-05-16 18:30:00,81.76,55.763000000000005,47.056000000000004,29.662 +2020-05-16 18:45:00,84.97,57.4,47.056000000000004,29.662 +2020-05-16 19:00:00,81.21,57.782,45.57,29.662 +2020-05-16 19:15:00,78.11,56.823,45.57,29.662 +2020-05-16 19:30:00,77.61,57.25899999999999,45.57,29.662 +2020-05-16 19:45:00,76.99,58.424,45.57,29.662 +2020-05-16 20:00:00,76.78,57.797,41.685,29.662 +2020-05-16 20:15:00,77.96,57.528,41.685,29.662 +2020-05-16 20:30:00,77.7,55.493,41.685,29.662 +2020-05-16 20:45:00,79.72,56.248000000000005,41.685,29.662 +2020-05-16 21:00:00,74.4,54.181999999999995,39.576,29.662 +2020-05-16 21:15:00,73.65,57.01,39.576,29.662 +2020-05-16 21:30:00,70.78,58.263000000000005,39.576,29.662 +2020-05-16 21:45:00,71.05,58.636,39.576,29.662 +2020-05-16 22:00:00,66.23,56.073,39.068000000000005,29.662 +2020-05-16 22:15:00,66.59,57.24100000000001,39.068000000000005,29.662 +2020-05-16 22:30:00,64.33,56.806000000000004,39.068000000000005,29.662 +2020-05-16 22:45:00,63.23,55.815,39.068000000000005,29.662 +2020-05-16 23:00:00,59.53,51.784,32.06,29.662 +2020-05-16 23:15:00,59.33,47.72,32.06,29.662 +2020-05-16 23:30:00,58.61,46.766000000000005,32.06,29.662 +2020-05-16 23:45:00,58.55,45.958,32.06,29.662 +2020-05-17 00:00:00,55.99,37.921,28.825,29.662 +2020-05-17 00:15:00,56.4,35.736999999999995,28.825,29.662 +2020-05-17 00:30:00,55.85,34.657,28.825,29.662 +2020-05-17 00:45:00,55.56,33.474000000000004,28.825,29.662 +2020-05-17 01:00:00,53.93,33.169000000000004,25.995,29.662 +2020-05-17 01:15:00,54.71,32.666,25.995,29.662 +2020-05-17 01:30:00,54.49,30.851,25.995,29.662 +2020-05-17 01:45:00,53.85,30.375,25.995,29.662 +2020-05-17 02:00:00,53.53,30.426,24.394000000000002,29.662 +2020-05-17 02:15:00,54.42,28.711,24.394000000000002,29.662 +2020-05-17 02:30:00,54.27,31.146,24.394000000000002,29.662 +2020-05-17 02:45:00,53.9,31.805999999999997,24.394000000000002,29.662 +2020-05-17 03:00:00,54.33,34.554,22.916999999999998,29.662 +2020-05-17 03:15:00,54.73,31.721,22.916999999999998,29.662 +2020-05-17 03:30:00,56.27,30.311,22.916999999999998,29.662 +2020-05-17 03:45:00,54.77,31.133000000000003,22.916999999999998,29.662 +2020-05-17 04:00:00,51.22,39.014,23.576999999999998,29.662 +2020-05-17 04:15:00,51.74,46.203,23.576999999999998,29.662 +2020-05-17 04:30:00,50.38,44.345,23.576999999999998,29.662 +2020-05-17 04:45:00,50.63,44.044,23.576999999999998,29.662 +2020-05-17 05:00:00,49.16,56.647,22.730999999999998,29.662 +2020-05-17 05:15:00,49.68,62.77,22.730999999999998,29.662 +2020-05-17 05:30:00,49.0,53.008,22.730999999999998,29.662 +2020-05-17 05:45:00,49.52,51.318999999999996,22.730999999999998,29.662 +2020-05-17 06:00:00,50.22,64.92699999999999,22.34,29.662 +2020-05-17 06:15:00,50.56,78.017,22.34,29.662 +2020-05-17 06:30:00,50.65,70.086,22.34,29.662 +2020-05-17 06:45:00,51.26,64.217,22.34,29.662 +2020-05-17 07:00:00,51.72,62.525,24.691999999999997,29.662 +2020-05-17 07:15:00,52.05,59.75899999999999,24.691999999999997,29.662 +2020-05-17 07:30:00,52.11,58.062,24.691999999999997,29.662 +2020-05-17 07:45:00,51.69,57.25899999999999,24.691999999999997,29.662 +2020-05-17 08:00:00,52.0,54.4,29.340999999999998,29.662 +2020-05-17 08:15:00,51.74,57.711000000000006,29.340999999999998,29.662 +2020-05-17 08:30:00,50.65,58.111000000000004,29.340999999999998,29.662 +2020-05-17 08:45:00,50.24,61.091,29.340999999999998,29.662 +2020-05-17 09:00:00,49.9,58.648999999999994,30.788,29.662 +2020-05-17 09:15:00,50.08,59.297,30.788,29.662 +2020-05-17 09:30:00,49.35,62.619,30.788,29.662 +2020-05-17 09:45:00,49.73,64.649,30.788,29.662 +2020-05-17 10:00:00,51.82,62.388000000000005,30.158,29.662 +2020-05-17 10:15:00,54.08,64.119,30.158,29.662 +2020-05-17 10:30:00,54.15,64.479,30.158,29.662 +2020-05-17 10:45:00,54.12,65.82300000000001,30.158,29.662 +2020-05-17 11:00:00,50.69,61.978,32.056,29.662 +2020-05-17 11:15:00,49.03,62.24,32.056,29.662 +2020-05-17 11:30:00,44.51,64.207,32.056,29.662 +2020-05-17 11:45:00,46.44,65.27199999999999,32.056,29.662 +2020-05-17 12:00:00,43.04,61.916000000000004,28.671999999999997,29.662 +2020-05-17 12:15:00,42.65,61.971000000000004,28.671999999999997,29.662 +2020-05-17 12:30:00,41.87,61.183,28.671999999999997,29.662 +2020-05-17 12:45:00,41.91,61.653999999999996,28.671999999999997,29.662 +2020-05-17 13:00:00,41.33,61.688,23.171,29.662 +2020-05-17 13:15:00,41.6,61.905,23.171,29.662 +2020-05-17 13:30:00,42.2,59.663000000000004,23.171,29.662 +2020-05-17 13:45:00,41.31,57.903999999999996,23.171,29.662 +2020-05-17 14:00:00,42.39,59.47,19.11,29.662 +2020-05-17 14:15:00,42.99,57.854,19.11,29.662 +2020-05-17 14:30:00,43.09,56.96,19.11,29.662 +2020-05-17 14:45:00,43.63,56.438,19.11,29.662 +2020-05-17 15:00:00,44.32,57.077,19.689,29.662 +2020-05-17 15:15:00,44.44,54.631,19.689,29.662 +2020-05-17 15:30:00,44.86,52.67100000000001,19.689,29.662 +2020-05-17 15:45:00,47.06,50.652,19.689,29.662 +2020-05-17 16:00:00,49.45,52.661,22.875,29.662 +2020-05-17 16:15:00,50.16,52.172,22.875,29.662 +2020-05-17 16:30:00,52.1,52.534,22.875,29.662 +2020-05-17 16:45:00,55.36,48.893,22.875,29.662 +2020-05-17 17:00:00,60.0,51.653,33.884,29.662 +2020-05-17 17:15:00,60.6,51.835,33.884,29.662 +2020-05-17 17:30:00,65.61,52.448,33.884,29.662 +2020-05-17 17:45:00,64.53,52.451,33.884,29.662 +2020-05-17 18:00:00,68.54,57.073,38.453,29.662 +2020-05-17 18:15:00,66.66,58.964,38.453,29.662 +2020-05-17 18:30:00,69.58,57.735,38.453,29.662 +2020-05-17 18:45:00,66.98,59.857,38.453,29.662 +2020-05-17 19:00:00,68.34,62.187,39.221,29.662 +2020-05-17 19:15:00,67.78,60.285,39.221,29.662 +2020-05-17 19:30:00,66.98,60.448,39.221,29.662 +2020-05-17 19:45:00,67.29,61.458999999999996,39.221,29.662 +2020-05-17 20:00:00,68.66,61.008,37.871,29.662 +2020-05-17 20:15:00,68.42,60.806000000000004,37.871,29.662 +2020-05-17 20:30:00,71.45,59.808,37.871,29.662 +2020-05-17 20:45:00,70.32,58.785,37.871,29.662 +2020-05-17 21:00:00,68.35,56.067,36.465,29.662 +2020-05-17 21:15:00,67.62,58.501999999999995,36.465,29.662 +2020-05-17 21:30:00,65.42,59.177,36.465,29.662 +2020-05-17 21:45:00,64.94,59.942,36.465,29.662 +2020-05-17 22:00:00,61.02,59.086000000000006,36.092,29.662 +2020-05-17 22:15:00,61.21,58.535,36.092,29.662 +2020-05-17 22:30:00,59.51,56.996,36.092,29.662 +2020-05-17 22:45:00,58.72,54.622,36.092,29.662 +2020-05-17 23:00:00,70.52,49.718,31.013,29.662 +2020-05-17 23:15:00,70.67,47.265,31.013,29.662 +2020-05-17 23:30:00,71.07,45.965,31.013,29.662 +2020-05-17 23:45:00,71.64,45.471000000000004,31.013,29.662 +2020-05-18 00:00:00,69.55,39.926,31.174,29.775 +2020-05-18 00:15:00,70.53,39.082,31.174,29.775 +2020-05-18 00:30:00,69.8,37.664,31.174,29.775 +2020-05-18 00:45:00,70.41,36.007,31.174,29.775 +2020-05-18 01:00:00,68.21,36.088,29.663,29.775 +2020-05-18 01:15:00,68.54,35.468,29.663,29.775 +2020-05-18 01:30:00,70.13,33.989000000000004,29.663,29.775 +2020-05-18 01:45:00,71.47,33.439,29.663,29.775 +2020-05-18 02:00:00,71.14,33.912,28.793000000000003,29.775 +2020-05-18 02:15:00,70.93,31.521,28.793000000000003,29.775 +2020-05-18 02:30:00,70.27,34.186,28.793000000000003,29.775 +2020-05-18 02:45:00,71.97,34.594,28.793000000000003,29.775 +2020-05-18 03:00:00,71.3,38.082,27.728,29.775 +2020-05-18 03:15:00,70.0,36.243,27.728,29.775 +2020-05-18 03:30:00,70.97,35.424,27.728,29.775 +2020-05-18 03:45:00,73.44,35.727,27.728,29.775 +2020-05-18 04:00:00,77.39,47.346000000000004,29.266,29.775 +2020-05-18 04:15:00,78.56,58.068999999999996,29.266,29.775 +2020-05-18 04:30:00,79.66,56.18600000000001,29.266,29.775 +2020-05-18 04:45:00,83.67,56.26,29.266,29.775 +2020-05-18 05:00:00,91.78,78.305,37.889,29.775 +2020-05-18 05:15:00,95.73,97.86399999999999,37.889,29.775 +2020-05-18 05:30:00,97.75,86.74700000000001,37.889,29.775 +2020-05-18 05:45:00,99.8,81.139,37.889,29.775 +2020-05-18 06:00:00,105.24,81.69,55.485,29.775 +2020-05-18 06:15:00,107.97,83.287,55.485,29.775 +2020-05-18 06:30:00,108.48,80.672,55.485,29.775 +2020-05-18 06:45:00,108.17,81.521,55.485,29.775 +2020-05-18 07:00:00,108.28,80.867,65.765,29.775 +2020-05-18 07:15:00,108.08,80.506,65.765,29.775 +2020-05-18 07:30:00,108.87,77.82,65.765,29.775 +2020-05-18 07:45:00,109.39,76.815,65.765,29.775 +2020-05-18 08:00:00,109.39,71.055,56.745,29.775 +2020-05-18 08:15:00,107.51,72.851,56.745,29.775 +2020-05-18 08:30:00,106.6,71.499,56.745,29.775 +2020-05-18 08:45:00,105.11,74.01100000000001,56.745,29.775 +2020-05-18 09:00:00,104.7,70.43,53.321999999999996,29.775 +2020-05-18 09:15:00,104.4,68.692,53.321999999999996,29.775 +2020-05-18 09:30:00,104.55,70.94800000000001,53.321999999999996,29.775 +2020-05-18 09:45:00,105.86,70.934,53.321999999999996,29.775 +2020-05-18 10:00:00,104.56,68.959,51.309,29.775 +2020-05-18 10:15:00,105.63,70.516,51.309,29.775 +2020-05-18 10:30:00,106.46,70.271,51.309,29.775 +2020-05-18 10:45:00,105.73,70.328,51.309,29.775 +2020-05-18 11:00:00,103.62,66.137,50.415,29.775 +2020-05-18 11:15:00,102.43,67.071,50.415,29.775 +2020-05-18 11:30:00,102.2,69.97800000000001,50.415,29.775 +2020-05-18 11:45:00,100.97,71.42699999999999,50.415,29.775 +2020-05-18 12:00:00,99.99,67.039,48.273,29.775 +2020-05-18 12:15:00,99.53,67.203,48.273,29.775 +2020-05-18 12:30:00,97.76,65.457,48.273,29.775 +2020-05-18 12:45:00,98.7,66.28,48.273,29.775 +2020-05-18 13:00:00,96.87,67.222,48.452,29.775 +2020-05-18 13:15:00,99.09,66.321,48.452,29.775 +2020-05-18 13:30:00,97.98,64.128,48.452,29.775 +2020-05-18 13:45:00,98.34,63.196999999999996,48.452,29.775 +2020-05-18 14:00:00,96.84,63.848,48.35,29.775 +2020-05-18 14:15:00,97.88,62.621,48.35,29.775 +2020-05-18 14:30:00,97.61,61.413999999999994,48.35,29.775 +2020-05-18 14:45:00,97.53,62.787,48.35,29.775 +2020-05-18 15:00:00,95.98,63.553000000000004,48.838,29.775 +2020-05-18 15:15:00,94.89,60.226000000000006,48.838,29.775 +2020-05-18 15:30:00,96.64,58.75899999999999,48.838,29.775 +2020-05-18 15:45:00,97.73,56.178000000000004,48.838,29.775 +2020-05-18 16:00:00,99.47,59.169,50.873000000000005,29.775 +2020-05-18 16:15:00,100.16,58.563,50.873000000000005,29.775 +2020-05-18 16:30:00,103.58,58.114,50.873000000000005,29.775 +2020-05-18 16:45:00,103.44,54.181999999999995,50.873000000000005,29.775 +2020-05-18 17:00:00,105.33,55.906000000000006,56.637,29.775 +2020-05-18 17:15:00,106.15,56.206,56.637,29.775 +2020-05-18 17:30:00,106.58,56.32899999999999,56.637,29.775 +2020-05-18 17:45:00,107.9,55.603,56.637,29.775 +2020-05-18 18:00:00,107.24,59.253,56.35,29.775 +2020-05-18 18:15:00,106.38,58.951,56.35,29.775 +2020-05-18 18:30:00,111.23,57.136,56.35,29.775 +2020-05-18 18:45:00,112.74,62.372,56.35,29.775 +2020-05-18 19:00:00,107.03,64.04899999999999,56.023,29.775 +2020-05-18 19:15:00,99.04,63.17100000000001,56.023,29.775 +2020-05-18 19:30:00,100.51,63.076,56.023,29.775 +2020-05-18 19:45:00,98.68,63.343999999999994,56.023,29.775 +2020-05-18 20:00:00,97.75,61.221000000000004,62.372,29.775 +2020-05-18 20:15:00,100.39,61.84,62.372,29.775 +2020-05-18 20:30:00,101.96,60.983000000000004,62.372,29.775 +2020-05-18 20:45:00,101.2,60.535,62.372,29.775 +2020-05-18 21:00:00,94.45,57.338,57.516999999999996,29.775 +2020-05-18 21:15:00,93.17,59.972,57.516999999999996,29.775 +2020-05-18 21:30:00,92.14,60.852,57.516999999999996,29.775 +2020-05-18 21:45:00,93.04,61.336000000000006,57.516999999999996,29.775 +2020-05-18 22:00:00,87.62,58.005,51.823,29.775 +2020-05-18 22:15:00,84.34,59.095,51.823,29.775 +2020-05-18 22:30:00,84.14,51.568999999999996,51.823,29.775 +2020-05-18 22:45:00,84.14,48.211000000000006,51.823,29.775 +2020-05-18 23:00:00,65.96,43.48,43.832,29.775 +2020-05-18 23:15:00,68.87,40.029,43.832,29.775 +2020-05-18 23:30:00,68.41,39.236,43.832,29.775 +2020-05-18 23:45:00,64.7,38.741,43.832,29.775 +2020-05-19 00:00:00,62.8,37.38,42.371,29.775 +2020-05-19 00:15:00,66.9,37.645,42.371,29.775 +2020-05-19 00:30:00,66.25,36.715,42.371,29.775 +2020-05-19 00:45:00,63.82,35.619,42.371,29.775 +2020-05-19 01:00:00,61.03,35.165,39.597,29.775 +2020-05-19 01:15:00,66.87,34.537,39.597,29.775 +2020-05-19 01:30:00,67.1,32.953,39.597,29.775 +2020-05-19 01:45:00,66.73,31.971999999999998,39.597,29.775 +2020-05-19 02:00:00,63.92,31.985,38.298,29.775 +2020-05-19 02:15:00,67.6,30.656,38.298,29.775 +2020-05-19 02:30:00,66.14,32.842,38.298,29.775 +2020-05-19 02:45:00,65.87,33.571999999999996,38.298,29.775 +2020-05-19 03:00:00,63.27,36.298,37.884,29.775 +2020-05-19 03:15:00,69.81,35.164,37.884,29.775 +2020-05-19 03:30:00,72.41,34.414,37.884,29.775 +2020-05-19 03:45:00,66.55,33.728,37.884,29.775 +2020-05-19 04:00:00,67.08,44.04600000000001,39.442,29.775 +2020-05-19 04:15:00,68.59,54.656000000000006,39.442,29.775 +2020-05-19 04:30:00,73.0,52.583,39.442,29.775 +2020-05-19 04:45:00,76.72,53.357,39.442,29.775 +2020-05-19 05:00:00,85.35,77.723,43.608000000000004,29.775 +2020-05-19 05:15:00,88.34,97.675,43.608000000000004,29.775 +2020-05-19 05:30:00,91.29,86.52,43.608000000000004,29.775 +2020-05-19 05:45:00,94.03,80.278,43.608000000000004,29.775 +2020-05-19 06:00:00,99.55,81.65100000000001,54.99100000000001,29.775 +2020-05-19 06:15:00,99.82,83.715,54.99100000000001,29.775 +2020-05-19 06:30:00,100.65,80.687,54.99100000000001,29.775 +2020-05-19 06:45:00,100.88,80.633,54.99100000000001,29.775 +2020-05-19 07:00:00,103.61,80.07600000000001,66.217,29.775 +2020-05-19 07:15:00,103.66,79.44800000000001,66.217,29.775 +2020-05-19 07:30:00,103.58,76.64399999999999,66.217,29.775 +2020-05-19 07:45:00,101.83,74.78399999999999,66.217,29.775 +2020-05-19 08:00:00,102.87,68.985,60.151,29.775 +2020-05-19 08:15:00,101.22,70.19800000000001,60.151,29.775 +2020-05-19 08:30:00,101.29,68.957,60.151,29.775 +2020-05-19 08:45:00,100.42,70.559,60.151,29.775 +2020-05-19 09:00:00,103.99,67.12100000000001,53.873000000000005,29.775 +2020-05-19 09:15:00,100.42,65.592,53.873000000000005,29.775 +2020-05-19 09:30:00,100.05,68.646,53.873000000000005,29.775 +2020-05-19 09:45:00,101.34,69.88,53.873000000000005,29.775 +2020-05-19 10:00:00,100.74,66.505,51.417,29.775 +2020-05-19 10:15:00,102.9,67.732,51.417,29.775 +2020-05-19 10:30:00,103.08,67.561,51.417,29.775 +2020-05-19 10:45:00,102.7,68.632,51.417,29.775 +2020-05-19 11:00:00,103.54,64.76,50.43600000000001,29.775 +2020-05-19 11:15:00,99.89,66.014,50.43600000000001,29.775 +2020-05-19 11:30:00,99.47,67.587,50.43600000000001,29.775 +2020-05-19 11:45:00,100.32,68.785,50.43600000000001,29.775 +2020-05-19 12:00:00,97.16,63.95399999999999,47.468,29.775 +2020-05-19 12:15:00,97.84,64.34,47.468,29.775 +2020-05-19 12:30:00,97.67,63.512,47.468,29.775 +2020-05-19 12:45:00,98.46,64.913,47.468,29.775 +2020-05-19 13:00:00,96.91,65.453,48.453,29.775 +2020-05-19 13:15:00,99.44,66.146,48.453,29.775 +2020-05-19 13:30:00,97.7,64.196,48.453,29.775 +2020-05-19 13:45:00,98.33,62.413000000000004,48.453,29.775 +2020-05-19 14:00:00,97.24,63.581,48.435,29.775 +2020-05-19 14:15:00,95.83,62.196000000000005,48.435,29.775 +2020-05-19 14:30:00,96.03,61.394,48.435,29.775 +2020-05-19 14:45:00,95.48,62.022,48.435,29.775 +2020-05-19 15:00:00,94.77,62.601000000000006,49.966,29.775 +2020-05-19 15:15:00,94.41,60.18600000000001,49.966,29.775 +2020-05-19 15:30:00,96.22,58.551,49.966,29.775 +2020-05-19 15:45:00,94.47,56.185,49.966,29.775 +2020-05-19 16:00:00,94.85,58.66,51.184,29.775 +2020-05-19 16:15:00,96.76,58.223,51.184,29.775 +2020-05-19 16:30:00,99.86,57.58,51.184,29.775 +2020-05-19 16:45:00,104.67,54.323,51.184,29.775 +2020-05-19 17:00:00,103.7,56.407,56.138999999999996,29.775 +2020-05-19 17:15:00,102.17,57.08,56.138999999999996,29.775 +2020-05-19 17:30:00,106.22,56.923,56.138999999999996,29.775 +2020-05-19 17:45:00,104.6,55.821999999999996,56.138999999999996,29.775 +2020-05-19 18:00:00,106.74,58.563,57.038000000000004,29.775 +2020-05-19 18:15:00,105.62,59.54600000000001,57.038000000000004,29.775 +2020-05-19 18:30:00,113.35,57.408,57.038000000000004,29.775 +2020-05-19 18:45:00,112.87,62.621,57.038000000000004,29.775 +2020-05-19 19:00:00,108.17,63.18899999999999,56.492,29.775 +2020-05-19 19:15:00,98.13,62.423,56.492,29.775 +2020-05-19 19:30:00,97.16,61.989,56.492,29.775 +2020-05-19 19:45:00,103.23,62.583999999999996,56.492,29.775 +2020-05-19 20:00:00,95.97,60.846000000000004,62.534,29.775 +2020-05-19 20:15:00,100.72,59.967,62.534,29.775 +2020-05-19 20:30:00,104.92,59.372,62.534,29.775 +2020-05-19 20:45:00,104.57,59.255,62.534,29.775 +2020-05-19 21:00:00,94.39,56.748999999999995,55.506,29.775 +2020-05-19 21:15:00,97.34,58.178999999999995,55.506,29.775 +2020-05-19 21:30:00,94.26,59.06,55.506,29.775 +2020-05-19 21:45:00,93.63,59.801,55.506,29.775 +2020-05-19 22:00:00,84.19,56.99,51.472,29.775 +2020-05-19 22:15:00,82.79,57.728,51.472,29.775 +2020-05-19 22:30:00,80.26,50.5,51.472,29.775 +2020-05-19 22:45:00,86.14,47.177,51.472,29.775 +2020-05-19 23:00:00,84.68,41.677,44.593,29.775 +2020-05-19 23:15:00,80.74,39.588,44.593,29.775 +2020-05-19 23:30:00,76.55,38.729,44.593,29.775 +2020-05-19 23:45:00,76.25,38.312,44.593,29.775 +2020-05-20 00:00:00,78.51,37.128,41.978,29.775 +2020-05-20 00:15:00,80.48,37.399,41.978,29.775 +2020-05-20 00:30:00,78.67,36.468,41.978,29.775 +2020-05-20 00:45:00,72.51,35.376,41.978,29.775 +2020-05-20 01:00:00,71.79,34.94,38.59,29.775 +2020-05-20 01:15:00,77.65,34.286,38.59,29.775 +2020-05-20 01:30:00,78.37,32.689,38.59,29.775 +2020-05-20 01:45:00,79.71,31.704,38.59,29.775 +2020-05-20 02:00:00,73.24,31.713,36.23,29.775 +2020-05-20 02:15:00,78.37,30.373,36.23,29.775 +2020-05-20 02:30:00,79.12,32.566,36.23,29.775 +2020-05-20 02:45:00,77.08,33.304,36.23,29.775 +2020-05-20 03:00:00,75.21,36.037,35.867,29.775 +2020-05-20 03:15:00,78.88,34.891999999999996,35.867,29.775 +2020-05-20 03:30:00,81.38,34.144,35.867,29.775 +2020-05-20 03:45:00,80.54,33.484,35.867,29.775 +2020-05-20 04:00:00,75.73,43.74100000000001,36.75,29.775 +2020-05-20 04:15:00,76.72,54.297,36.75,29.775 +2020-05-20 04:30:00,80.9,52.211000000000006,36.75,29.775 +2020-05-20 04:45:00,86.16,52.978,36.75,29.775 +2020-05-20 05:00:00,93.21,77.21,40.461,29.775 +2020-05-20 05:15:00,95.42,96.999,40.461,29.775 +2020-05-20 05:30:00,99.77,85.91,40.461,29.775 +2020-05-20 05:45:00,101.34,79.735,40.461,29.775 +2020-05-20 06:00:00,101.76,81.133,55.481,29.775 +2020-05-20 06:15:00,101.28,83.171,55.481,29.775 +2020-05-20 06:30:00,104.28,80.161,55.481,29.775 +2020-05-20 06:45:00,105.05,80.12899999999999,55.481,29.775 +2020-05-20 07:00:00,108.2,79.56,68.45,29.775 +2020-05-20 07:15:00,107.22,78.939,68.45,29.775 +2020-05-20 07:30:00,107.19,76.111,68.45,29.775 +2020-05-20 07:45:00,106.77,74.281,68.45,29.775 +2020-05-20 08:00:00,105.12,68.487,60.885,29.775 +2020-05-20 08:15:00,105.19,69.749,60.885,29.775 +2020-05-20 08:30:00,106.79,68.49600000000001,60.885,29.775 +2020-05-20 08:45:00,108.41,70.115,60.885,29.775 +2020-05-20 09:00:00,104.53,66.671,56.887,29.775 +2020-05-20 09:15:00,106.56,65.149,56.887,29.775 +2020-05-20 09:30:00,106.98,68.21300000000001,56.887,29.775 +2020-05-20 09:45:00,110.81,69.48,56.887,29.775 +2020-05-20 10:00:00,109.87,66.115,54.401,29.775 +2020-05-20 10:15:00,111.92,67.375,54.401,29.775 +2020-05-20 10:30:00,110.2,67.215,54.401,29.775 +2020-05-20 10:45:00,106.45,68.29899999999999,54.401,29.775 +2020-05-20 11:00:00,111.94,64.417,53.678000000000004,29.775 +2020-05-20 11:15:00,116.47,65.687,53.678000000000004,29.775 +2020-05-20 11:30:00,120.29,67.253,53.678000000000004,29.775 +2020-05-20 11:45:00,127.5,68.46,53.678000000000004,29.775 +2020-05-20 12:00:00,130.45,63.667,51.68,29.775 +2020-05-20 12:15:00,131.65,64.06,51.68,29.775 +2020-05-20 12:30:00,128.54,63.196000000000005,51.68,29.775 +2020-05-20 12:45:00,128.89,64.604,51.68,29.775 +2020-05-20 13:00:00,124.02,65.156,51.263000000000005,29.775 +2020-05-20 13:15:00,125.19,65.852,51.263000000000005,29.775 +2020-05-20 13:30:00,125.37,63.912,51.263000000000005,29.775 +2020-05-20 13:45:00,129.7,62.131,51.263000000000005,29.775 +2020-05-20 14:00:00,127.75,63.336000000000006,51.107,29.775 +2020-05-20 14:15:00,134.43,61.942,51.107,29.775 +2020-05-20 14:30:00,131.56,61.101000000000006,51.107,29.775 +2020-05-20 14:45:00,129.26,61.735,51.107,29.775 +2020-05-20 15:00:00,125.84,62.364,51.498000000000005,29.775 +2020-05-20 15:15:00,123.48,59.934,51.498000000000005,29.775 +2020-05-20 15:30:00,119.44,58.276,51.498000000000005,29.775 +2020-05-20 15:45:00,121.38,55.893,51.498000000000005,29.775 +2020-05-20 16:00:00,119.62,58.416000000000004,53.376999999999995,29.775 +2020-05-20 16:15:00,114.06,57.964,53.376999999999995,29.775 +2020-05-20 16:30:00,110.26,57.338,53.376999999999995,29.775 +2020-05-20 16:45:00,113.71,54.028,53.376999999999995,29.775 +2020-05-20 17:00:00,121.74,56.156000000000006,56.965,29.775 +2020-05-20 17:15:00,122.25,56.806999999999995,56.965,29.775 +2020-05-20 17:30:00,123.94,56.637,56.965,29.775 +2020-05-20 17:45:00,119.22,55.5,56.965,29.775 +2020-05-20 18:00:00,119.16,58.25899999999999,58.231,29.775 +2020-05-20 18:15:00,116.54,59.221000000000004,58.231,29.775 +2020-05-20 18:30:00,114.14,57.071000000000005,58.231,29.775 +2020-05-20 18:45:00,115.86,62.284,58.231,29.775 +2020-05-20 19:00:00,116.25,62.854,58.865,29.775 +2020-05-20 19:15:00,109.47,62.081,58.865,29.775 +2020-05-20 19:30:00,102.19,61.643,58.865,29.775 +2020-05-20 19:45:00,100.8,62.236000000000004,58.865,29.775 +2020-05-20 20:00:00,104.57,60.473,65.605,29.775 +2020-05-20 20:15:00,101.8,59.593999999999994,65.605,29.775 +2020-05-20 20:30:00,104.21,59.021,65.605,29.775 +2020-05-20 20:45:00,107.81,58.949,65.605,29.775 +2020-05-20 21:00:00,100.78,56.446999999999996,58.083999999999996,29.775 +2020-05-20 21:15:00,99.92,57.891999999999996,58.083999999999996,29.775 +2020-05-20 21:30:00,96.61,58.748000000000005,58.083999999999996,29.775 +2020-05-20 21:45:00,94.94,59.515,58.083999999999996,29.775 +2020-05-20 22:00:00,89.31,56.731,53.243,29.775 +2020-05-20 22:15:00,87.09,57.488,53.243,29.775 +2020-05-20 22:30:00,87.82,50.25899999999999,53.243,29.775 +2020-05-20 22:45:00,88.26,46.924,53.243,29.775 +2020-05-20 23:00:00,63.05,41.388999999999996,44.283,29.775 +2020-05-20 23:15:00,62.09,39.345,44.283,29.775 +2020-05-20 23:30:00,60.51,38.489000000000004,44.283,29.775 +2020-05-20 23:45:00,60.15,38.064,44.283,29.775 +2020-05-21 00:00:00,58.01,36.91,18.527,29.662 +2020-05-21 00:15:00,57.28,34.745,18.527,29.662 +2020-05-21 00:30:00,56.86,33.663000000000004,18.527,29.662 +2020-05-21 00:45:00,56.38,32.498000000000005,18.527,29.662 +2020-05-21 01:00:00,54.83,32.263000000000005,16.348,29.662 +2020-05-21 01:15:00,54.78,31.66,16.348,29.662 +2020-05-21 01:30:00,54.4,29.784000000000002,16.348,29.662 +2020-05-21 01:45:00,54.27,29.289,16.348,29.662 +2020-05-21 02:00:00,53.37,29.329,12.581,29.662 +2020-05-21 02:15:00,53.99,27.565,12.581,29.662 +2020-05-21 02:30:00,52.98,30.034000000000002,12.581,29.662 +2020-05-21 02:45:00,55.47,30.726999999999997,12.581,29.662 +2020-05-21 03:00:00,53.56,33.503,10.712,29.662 +2020-05-21 03:15:00,54.49,30.621,10.712,29.662 +2020-05-21 03:30:00,57.11,29.22,10.712,29.662 +2020-05-21 03:45:00,55.48,30.15,10.712,29.662 +2020-05-21 04:00:00,58.06,37.786,9.084,29.662 +2020-05-21 04:15:00,53.75,44.758,9.084,29.662 +2020-05-21 04:30:00,52.67,42.848,9.084,29.662 +2020-05-21 04:45:00,55.12,42.523,9.084,29.662 +2020-05-21 05:00:00,51.91,54.581,9.388,29.662 +2020-05-21 05:15:00,52.73,60.053000000000004,9.388,29.662 +2020-05-21 05:30:00,55.46,50.553999999999995,9.388,29.662 +2020-05-21 05:45:00,54.71,49.13399999999999,9.388,29.662 +2020-05-21 06:00:00,55.97,62.847,11.109000000000002,29.662 +2020-05-21 06:15:00,58.32,75.834,11.109000000000002,29.662 +2020-05-21 06:30:00,57.09,67.965,11.109000000000002,29.662 +2020-05-21 06:45:00,58.87,62.187,11.109000000000002,29.662 +2020-05-21 07:00:00,60.26,60.449,13.77,29.662 +2020-05-21 07:15:00,63.31,57.713,13.77,29.662 +2020-05-21 07:30:00,59.48,55.919,13.77,29.662 +2020-05-21 07:45:00,59.47,55.233000000000004,13.77,29.662 +2020-05-21 08:00:00,59.73,52.391999999999996,12.868,29.662 +2020-05-21 08:15:00,58.67,55.902,12.868,29.662 +2020-05-21 08:30:00,63.81,56.254,12.868,29.662 +2020-05-21 08:45:00,58.75,59.305,12.868,29.662 +2020-05-21 09:00:00,58.3,56.836000000000006,12.804,29.662 +2020-05-21 09:15:00,58.24,57.516999999999996,12.804,29.662 +2020-05-21 09:30:00,57.56,60.875,12.804,29.662 +2020-05-21 09:45:00,61.45,63.036,12.804,29.662 +2020-05-21 10:00:00,57.46,60.818000000000005,11.029000000000002,29.662 +2020-05-21 10:15:00,57.46,62.676,11.029000000000002,29.662 +2020-05-21 10:30:00,60.42,63.08,11.029000000000002,29.662 +2020-05-21 10:45:00,58.0,64.479,11.029000000000002,29.662 +2020-05-21 11:00:00,58.01,60.6,11.681,29.662 +2020-05-21 11:15:00,54.19,60.924,11.681,29.662 +2020-05-21 11:30:00,54.54,62.856,11.681,29.662 +2020-05-21 11:45:00,55.05,63.966,11.681,29.662 +2020-05-21 12:00:00,55.16,60.76,8.915,29.662 +2020-05-21 12:15:00,53.37,60.842,8.915,29.662 +2020-05-21 12:30:00,51.32,59.909,8.915,29.662 +2020-05-21 12:45:00,50.61,60.408,8.915,29.662 +2020-05-21 13:00:00,50.98,60.489,5.4639999999999995,29.662 +2020-05-21 13:15:00,52.85,60.721000000000004,5.4639999999999995,29.662 +2020-05-21 13:30:00,48.58,58.523,5.4639999999999995,29.662 +2020-05-21 13:45:00,49.76,56.766000000000005,5.4639999999999995,29.662 +2020-05-21 14:00:00,51.52,58.485,3.2939999999999996,29.662 +2020-05-21 14:15:00,57.07,56.833999999999996,3.2939999999999996,29.662 +2020-05-21 14:30:00,64.25,55.785,3.2939999999999996,29.662 +2020-05-21 14:45:00,65.85,55.278999999999996,3.2939999999999996,29.662 +2020-05-21 15:00:00,66.73,56.126999999999995,4.689,29.662 +2020-05-21 15:15:00,60.8,53.618,4.689,29.662 +2020-05-21 15:30:00,61.9,51.562,4.689,29.662 +2020-05-21 15:45:00,64.07,49.477,4.689,29.662 +2020-05-21 16:00:00,65.13,51.675,7.732,29.662 +2020-05-21 16:15:00,66.81,51.131,7.732,29.662 +2020-05-21 16:30:00,64.74,51.558,7.732,29.662 +2020-05-21 16:45:00,64.19,47.702,7.732,29.662 +2020-05-21 17:00:00,65.06,50.641999999999996,17.558,29.662 +2020-05-21 17:15:00,66.43,50.731,17.558,29.662 +2020-05-21 17:30:00,69.39,51.294,17.558,29.662 +2020-05-21 17:45:00,71.08,51.156000000000006,17.558,29.662 +2020-05-21 18:00:00,71.37,55.849,24.763,29.662 +2020-05-21 18:15:00,70.43,57.653999999999996,24.763,29.662 +2020-05-21 18:30:00,73.17,56.379,24.763,29.662 +2020-05-21 18:45:00,70.48,58.501000000000005,24.763,29.662 +2020-05-21 19:00:00,69.9,60.832,29.633000000000003,29.662 +2020-05-21 19:15:00,68.76,58.906000000000006,29.633000000000003,29.662 +2020-05-21 19:30:00,69.06,59.05,29.633000000000003,29.662 +2020-05-21 19:45:00,71.41,60.059,29.633000000000003,29.662 +2020-05-21 20:00:00,69.15,59.508,38.826,29.662 +2020-05-21 20:15:00,71.39,59.303999999999995,38.826,29.662 +2020-05-21 20:30:00,74.95,58.396,38.826,29.662 +2020-05-21 20:45:00,71.98,57.552,38.826,29.662 +2020-05-21 21:00:00,71.11,54.851000000000006,37.751,29.662 +2020-05-21 21:15:00,67.66,57.343999999999994,37.751,29.662 +2020-05-21 21:30:00,65.62,57.926,37.751,29.662 +2020-05-21 21:45:00,65.41,58.792,37.751,29.662 +2020-05-21 22:00:00,62.1,58.045,39.799,29.662 +2020-05-21 22:15:00,64.81,57.568999999999996,39.799,29.662 +2020-05-21 22:30:00,60.51,56.025,39.799,29.662 +2020-05-21 22:45:00,60.78,53.603,39.799,29.662 +2020-05-21 23:00:00,83.53,48.56,33.686,29.662 +2020-05-21 23:15:00,79.71,46.288999999999994,33.686,29.662 +2020-05-21 23:30:00,76.64,45.003,33.686,29.662 +2020-05-21 23:45:00,81.28,44.475,33.686,29.662 +2020-05-22 00:00:00,78.78,34.703,39.884,29.662 +2020-05-22 00:15:00,78.38,35.232,39.884,29.662 +2020-05-22 00:30:00,75.8,34.53,39.884,29.662 +2020-05-22 00:45:00,78.88,33.875,39.884,29.662 +2020-05-22 01:00:00,78.43,33.044000000000004,37.658,29.662 +2020-05-22 01:15:00,76.96,31.963,37.658,29.662 +2020-05-22 01:30:00,72.09,30.969,37.658,29.662 +2020-05-22 01:45:00,77.86,29.756999999999998,37.658,29.662 +2020-05-22 02:00:00,78.9,30.668000000000003,36.707,29.662 +2020-05-22 02:15:00,79.75,29.229,36.707,29.662 +2020-05-22 02:30:00,75.51,32.335,36.707,29.662 +2020-05-22 02:45:00,79.05,32.449,36.707,29.662 +2020-05-22 03:00:00,80.35,35.719,37.025,29.662 +2020-05-22 03:15:00,80.81,33.605,37.025,29.662 +2020-05-22 03:30:00,76.91,32.632,37.025,29.662 +2020-05-22 03:45:00,75.74,32.927,37.025,29.662 +2020-05-22 04:00:00,76.89,43.244,38.349000000000004,29.662 +2020-05-22 04:15:00,77.58,52.16,38.349000000000004,29.662 +2020-05-22 04:30:00,80.22,51.0,38.349000000000004,29.662 +2020-05-22 04:45:00,84.77,50.898,38.349000000000004,29.662 +2020-05-22 05:00:00,93.82,74.133,41.565,29.662 +2020-05-22 05:15:00,95.9,94.82,41.565,29.662 +2020-05-22 05:30:00,97.7,84.27,41.565,29.662 +2020-05-22 05:45:00,97.74,77.786,41.565,29.662 +2020-05-22 06:00:00,104.46,79.546,53.861000000000004,29.662 +2020-05-22 06:15:00,105.11,81.39,53.861000000000004,29.662 +2020-05-22 06:30:00,106.02,78.183,53.861000000000004,29.662 +2020-05-22 06:45:00,108.42,78.407,53.861000000000004,29.662 +2020-05-22 07:00:00,109.92,78.263,63.497,29.662 +2020-05-22 07:15:00,110.85,78.738,63.497,29.662 +2020-05-22 07:30:00,113.49,73.98100000000001,63.497,29.662 +2020-05-22 07:45:00,111.86,71.819,63.497,29.662 +2020-05-22 08:00:00,107.87,66.528,55.43899999999999,29.662 +2020-05-22 08:15:00,106.09,68.429,55.43899999999999,29.662 +2020-05-22 08:30:00,104.97,67.28,55.43899999999999,29.662 +2020-05-22 08:45:00,109.12,68.445,55.43899999999999,29.662 +2020-05-22 09:00:00,104.41,62.928000000000004,52.132,29.662 +2020-05-22 09:15:00,102.77,63.343,52.132,29.662 +2020-05-22 09:30:00,103.2,65.684,52.132,29.662 +2020-05-22 09:45:00,105.1,67.34899999999999,52.132,29.662 +2020-05-22 10:00:00,108.85,63.57,49.881,29.662 +2020-05-22 10:15:00,114.42,64.851,49.881,29.662 +2020-05-22 10:30:00,116.06,65.205,49.881,29.662 +2020-05-22 10:45:00,112.9,66.123,49.881,29.662 +2020-05-22 11:00:00,116.37,62.451,49.396,29.662 +2020-05-22 11:15:00,116.42,62.535,49.396,29.662 +2020-05-22 11:30:00,115.65,64.168,49.396,29.662 +2020-05-22 11:45:00,114.92,64.525,49.396,29.662 +2020-05-22 12:00:00,111.62,60.479,46.7,29.662 +2020-05-22 12:15:00,106.84,59.778,46.7,29.662 +2020-05-22 12:30:00,102.56,58.99100000000001,46.7,29.662 +2020-05-22 12:45:00,98.4,59.821000000000005,46.7,29.662 +2020-05-22 13:00:00,94.96,61.16,44.05,29.662 +2020-05-22 13:15:00,96.61,62.23,44.05,29.662 +2020-05-22 13:30:00,93.79,61.045,44.05,29.662 +2020-05-22 13:45:00,92.84,59.531000000000006,44.05,29.662 +2020-05-22 14:00:00,91.24,59.794,42.805,29.662 +2020-05-22 14:15:00,93.29,58.739,42.805,29.662 +2020-05-22 14:30:00,91.88,59.32899999999999,42.805,29.662 +2020-05-22 14:45:00,93.09,59.412,42.805,29.662 +2020-05-22 15:00:00,93.45,59.95399999999999,44.36600000000001,29.662 +2020-05-22 15:15:00,95.82,57.167,44.36600000000001,29.662 +2020-05-22 15:30:00,97.12,54.571000000000005,44.36600000000001,29.662 +2020-05-22 15:45:00,97.8,52.891000000000005,44.36600000000001,29.662 +2020-05-22 16:00:00,96.53,54.466,46.928999999999995,29.662 +2020-05-22 16:15:00,103.21,54.513999999999996,46.928999999999995,29.662 +2020-05-22 16:30:00,104.3,53.775,46.928999999999995,29.662 +2020-05-22 16:45:00,104.51,49.604,46.928999999999995,29.662 +2020-05-22 17:00:00,103.54,53.474,51.468,29.662 +2020-05-22 17:15:00,101.79,53.823,51.468,29.662 +2020-05-22 17:30:00,102.58,53.708,51.468,29.662 +2020-05-22 17:45:00,104.14,52.295,51.468,29.662 +2020-05-22 18:00:00,106.35,55.326,52.58,29.662 +2020-05-22 18:15:00,103.1,55.269,52.58,29.662 +2020-05-22 18:30:00,102.56,53.086000000000006,52.58,29.662 +2020-05-22 18:45:00,105.35,58.692,52.58,29.662 +2020-05-22 19:00:00,107.38,60.31399999999999,52.183,29.662 +2020-05-22 19:15:00,103.6,60.376000000000005,52.183,29.662 +2020-05-22 19:30:00,98.21,59.898999999999994,52.183,29.662 +2020-05-22 19:45:00,98.34,59.427,52.183,29.662 +2020-05-22 20:00:00,94.72,57.468999999999994,58.497,29.662 +2020-05-22 20:15:00,95.77,57.34,58.497,29.662 +2020-05-22 20:30:00,98.16,56.342,58.497,29.662 +2020-05-22 20:45:00,96.69,55.707,58.497,29.662 +2020-05-22 21:00:00,97.63,54.568000000000005,54.731,29.662 +2020-05-22 21:15:00,97.68,57.72,54.731,29.662 +2020-05-22 21:30:00,90.83,58.396,54.731,29.662 +2020-05-22 21:45:00,86.47,59.52,54.731,29.662 +2020-05-22 22:00:00,80.55,56.864,51.386,29.662 +2020-05-22 22:15:00,84.38,57.391000000000005,51.386,29.662 +2020-05-22 22:30:00,86.27,56.013000000000005,51.386,29.662 +2020-05-22 22:45:00,84.62,54.208999999999996,51.386,29.662 +2020-05-22 23:00:00,74.96,50.117,44.463,29.662 +2020-05-22 23:15:00,74.35,46.266000000000005,44.463,29.662 +2020-05-22 23:30:00,71.21,43.38,44.463,29.662 +2020-05-22 23:45:00,76.73,42.683,44.463,29.662 +2020-05-23 00:00:00,75.7,35.035,42.833999999999996,29.662 +2020-05-23 00:15:00,76.89,34.034,42.833999999999996,29.662 +2020-05-23 00:30:00,71.1,33.168,42.833999999999996,29.662 +2020-05-23 00:45:00,75.4,31.974,42.833999999999996,29.662 +2020-05-23 01:00:00,73.52,31.552,37.859,29.662 +2020-05-23 01:15:00,74.62,30.799,37.859,29.662 +2020-05-23 01:30:00,68.8,28.918000000000003,37.859,29.662 +2020-05-23 01:45:00,73.9,28.836,37.859,29.662 +2020-05-23 02:00:00,72.13,28.951,35.327,29.662 +2020-05-23 02:15:00,73.48,26.677,35.327,29.662 +2020-05-23 02:30:00,66.65,28.805,35.327,29.662 +2020-05-23 02:45:00,65.13,29.693,35.327,29.662 +2020-05-23 03:00:00,66.59,31.802,34.908,29.662 +2020-05-23 03:15:00,64.97,28.761,34.908,29.662 +2020-05-23 03:30:00,65.86,27.765,34.908,29.662 +2020-05-23 03:45:00,65.45,29.511999999999997,34.908,29.662 +2020-05-23 04:00:00,63.35,37.108000000000004,34.84,29.662 +2020-05-23 04:15:00,63.76,44.592,34.84,29.662 +2020-05-23 04:30:00,62.96,41.382,34.84,29.662 +2020-05-23 04:45:00,64.33,41.43,34.84,29.662 +2020-05-23 05:00:00,67.52,53.748000000000005,34.222,29.662 +2020-05-23 05:15:00,67.85,60.097,34.222,29.662 +2020-05-23 05:30:00,68.35,51.146,34.222,29.662 +2020-05-23 05:45:00,68.82,50.068999999999996,34.222,29.662 +2020-05-23 06:00:00,72.79,66.115,35.515,29.662 +2020-05-23 06:15:00,71.8,78.594,35.515,29.662 +2020-05-23 06:30:00,73.05,71.634,35.515,29.662 +2020-05-23 06:45:00,75.09,67.053,35.515,29.662 +2020-05-23 07:00:00,76.2,64.55199999999999,39.687,29.662 +2020-05-23 07:15:00,76.2,63.553000000000004,39.687,29.662 +2020-05-23 07:30:00,77.52,60.75899999999999,39.687,29.662 +2020-05-23 07:45:00,79.58,60.32,39.687,29.662 +2020-05-23 08:00:00,78.67,56.416000000000004,44.9,29.662 +2020-05-23 08:15:00,78.07,58.93600000000001,44.9,29.662 +2020-05-23 08:30:00,78.21,58.104,44.9,29.662 +2020-05-23 08:45:00,78.09,60.843,44.9,29.662 +2020-05-23 09:00:00,76.63,58.558,45.724,29.662 +2020-05-23 09:15:00,76.47,59.595,45.724,29.662 +2020-05-23 09:30:00,75.4,62.603,45.724,29.662 +2020-05-23 09:45:00,75.15,63.916000000000004,45.724,29.662 +2020-05-23 10:00:00,75.82,60.69,43.123999999999995,29.662 +2020-05-23 10:15:00,76.15,62.368,43.123999999999995,29.662 +2020-05-23 10:30:00,76.53,62.45399999999999,43.123999999999995,29.662 +2020-05-23 10:45:00,75.37,63.36,43.123999999999995,29.662 +2020-05-23 11:00:00,75.31,59.586999999999996,40.255,29.662 +2020-05-23 11:15:00,77.04,60.324,40.255,29.662 +2020-05-23 11:30:00,77.44,61.949,40.255,29.662 +2020-05-23 11:45:00,74.8,62.708999999999996,40.255,29.662 +2020-05-23 12:00:00,69.86,58.754,38.582,29.662 +2020-05-23 12:15:00,69.32,58.985,38.582,29.662 +2020-05-23 12:30:00,71.91,58.121,38.582,29.662 +2020-05-23 12:45:00,70.49,59.434,38.582,29.662 +2020-05-23 13:00:00,69.63,59.963,36.043,29.662 +2020-05-23 13:15:00,73.6,60.055,36.043,29.662 +2020-05-23 13:30:00,72.67,58.946999999999996,36.043,29.662 +2020-05-23 13:45:00,69.34,56.317,36.043,29.662 +2020-05-23 14:00:00,67.9,56.952,35.216,29.662 +2020-05-23 14:15:00,68.87,54.621,35.216,29.662 +2020-05-23 14:30:00,69.82,54.353,35.216,29.662 +2020-05-23 14:45:00,66.18,54.928999999999995,35.216,29.662 +2020-05-23 15:00:00,71.76,56.085,36.759,29.662 +2020-05-23 15:15:00,72.55,54.121,36.759,29.662 +2020-05-23 15:30:00,72.31,52.053000000000004,36.759,29.662 +2020-05-23 15:45:00,71.95,49.521,36.759,29.662 +2020-05-23 16:00:00,71.37,52.958,40.086,29.662 +2020-05-23 16:15:00,71.22,52.37,40.086,29.662 +2020-05-23 16:30:00,71.77,51.843999999999994,40.086,29.662 +2020-05-23 16:45:00,72.93,47.797,40.086,29.662 +2020-05-23 17:00:00,75.81,50.486000000000004,44.876999999999995,29.662 +2020-05-23 17:15:00,77.15,49.199,44.876999999999995,29.662 +2020-05-23 17:30:00,81.73,48.926,44.876999999999995,29.662 +2020-05-23 17:45:00,83.0,47.845,44.876999999999995,29.662 +2020-05-23 18:00:00,82.3,52.175,47.056000000000004,29.662 +2020-05-23 18:15:00,80.93,54.047,47.056000000000004,29.662 +2020-05-23 18:30:00,81.66,53.409,47.056000000000004,29.662 +2020-05-23 18:45:00,81.46,55.044,47.056000000000004,29.662 +2020-05-23 19:00:00,79.51,55.43,45.57,29.662 +2020-05-23 19:15:00,76.63,54.428999999999995,45.57,29.662 +2020-05-23 19:30:00,75.8,54.83,45.57,29.662 +2020-05-23 19:45:00,75.75,55.99,45.57,29.662 +2020-05-23 20:00:00,75.77,55.193000000000005,41.685,29.662 +2020-05-23 20:15:00,75.2,54.916000000000004,41.685,29.662 +2020-05-23 20:30:00,78.48,53.038999999999994,41.685,29.662 +2020-05-23 20:45:00,77.87,54.102,41.685,29.662 +2020-05-23 21:00:00,75.6,52.068999999999996,39.576,29.662 +2020-05-23 21:15:00,75.41,54.998999999999995,39.576,29.662 +2020-05-23 21:30:00,72.51,56.086999999999996,39.576,29.662 +2020-05-23 21:45:00,71.96,56.635,39.576,29.662 +2020-05-23 22:00:00,68.93,54.263000000000005,39.068000000000005,29.662 +2020-05-23 22:15:00,68.33,55.56,39.068000000000005,29.662 +2020-05-23 22:30:00,65.26,55.118,39.068000000000005,29.662 +2020-05-23 22:45:00,65.06,54.041000000000004,39.068000000000005,29.662 +2020-05-23 23:00:00,61.85,49.77,32.06,29.662 +2020-05-23 23:15:00,61.41,46.022,32.06,29.662 +2020-05-23 23:30:00,61.34,45.095,32.06,29.662 +2020-05-23 23:45:00,59.43,44.225,32.06,29.662 +2020-05-24 00:00:00,57.13,36.184,28.825,29.662 +2020-05-24 00:15:00,57.97,34.035,28.825,29.662 +2020-05-24 00:30:00,57.26,32.953,28.825,29.662 +2020-05-24 00:45:00,57.6,31.803,28.825,29.662 +2020-05-24 01:00:00,55.3,31.62,25.995,29.662 +2020-05-24 01:15:00,56.86,30.941999999999997,25.995,29.662 +2020-05-24 01:30:00,56.68,29.026999999999997,25.995,29.662 +2020-05-24 01:45:00,56.48,28.518,25.995,29.662 +2020-05-24 02:00:00,54.98,28.549,24.394000000000002,29.662 +2020-05-24 02:15:00,55.67,26.753,24.394000000000002,29.662 +2020-05-24 02:30:00,55.71,29.241999999999997,24.394000000000002,29.662 +2020-05-24 02:45:00,54.51,29.96,24.394000000000002,29.662 +2020-05-24 03:00:00,54.2,32.754,22.916999999999998,29.662 +2020-05-24 03:15:00,54.33,29.838,22.916999999999998,29.662 +2020-05-24 03:30:00,55.39,28.445999999999998,22.916999999999998,29.662 +2020-05-24 03:45:00,53.97,29.454,22.916999999999998,29.662 +2020-05-24 04:00:00,51.59,36.912,23.576999999999998,29.662 +2020-05-24 04:15:00,52.43,43.722,23.576999999999998,29.662 +2020-05-24 04:30:00,52.07,41.773999999999994,23.576999999999998,29.662 +2020-05-24 04:45:00,52.18,41.433,23.576999999999998,29.662 +2020-05-24 05:00:00,52.38,53.093999999999994,22.730999999999998,29.662 +2020-05-24 05:15:00,51.01,58.086999999999996,22.730999999999998,29.662 +2020-05-24 05:30:00,51.44,48.788000000000004,22.730999999999998,29.662 +2020-05-24 05:45:00,50.26,47.56100000000001,22.730999999999998,29.662 +2020-05-24 06:00:00,55.4,61.348,22.34,29.662 +2020-05-24 06:15:00,56.11,74.26,22.34,29.662 +2020-05-24 06:30:00,56.5,66.44,22.34,29.662 +2020-05-24 06:45:00,58.7,60.73,22.34,29.662 +2020-05-24 07:00:00,60.0,58.957,24.691999999999997,29.662 +2020-05-24 07:15:00,62.28,56.248999999999995,24.691999999999997,29.662 +2020-05-24 07:30:00,63.86,54.387,24.691999999999997,29.662 +2020-05-24 07:45:00,66.28,53.792,24.691999999999997,29.662 +2020-05-24 08:00:00,68.02,50.965,29.340999999999998,29.662 +2020-05-24 08:15:00,69.85,54.619,29.340999999999998,29.662 +2020-05-24 08:30:00,70.34,54.93600000000001,29.340999999999998,29.662 +2020-05-24 08:45:00,69.19,58.036,29.340999999999998,29.662 +2020-05-24 09:00:00,64.38,55.551,30.788,29.662 +2020-05-24 09:15:00,64.98,56.251999999999995,30.788,29.662 +2020-05-24 09:30:00,61.61,59.635,30.788,29.662 +2020-05-24 09:45:00,58.24,61.888999999999996,30.788,29.662 +2020-05-24 10:00:00,59.37,59.70399999999999,30.158,29.662 +2020-05-24 10:15:00,60.65,61.652,30.158,29.662 +2020-05-24 10:30:00,62.09,62.085,30.158,29.662 +2020-05-24 10:45:00,62.05,63.525,30.158,29.662 +2020-05-24 11:00:00,62.17,59.622,32.056,29.662 +2020-05-24 11:15:00,59.81,59.99100000000001,32.056,29.662 +2020-05-24 11:30:00,55.31,61.896,32.056,29.662 +2020-05-24 11:45:00,55.01,63.036,32.056,29.662 +2020-05-24 12:00:00,56.12,59.94,28.671999999999997,29.662 +2020-05-24 12:15:00,58.46,60.038000000000004,28.671999999999997,29.662 +2020-05-24 12:30:00,58.98,59.003,28.671999999999997,29.662 +2020-05-24 12:45:00,61.98,59.52,28.671999999999997,29.662 +2020-05-24 13:00:00,60.62,59.632,23.171,29.662 +2020-05-24 13:15:00,61.12,59.873000000000005,23.171,29.662 +2020-05-24 13:30:00,56.46,57.708999999999996,23.171,29.662 +2020-05-24 13:45:00,56.65,55.956,23.171,29.662 +2020-05-24 14:00:00,56.1,57.781000000000006,19.11,29.662 +2020-05-24 14:15:00,58.99,56.107,19.11,29.662 +2020-05-24 14:30:00,64.84,54.945,19.11,29.662 +2020-05-24 14:45:00,70.09,54.452,19.11,29.662 +2020-05-24 15:00:00,72.58,55.449,19.689,29.662 +2020-05-24 15:15:00,73.17,52.895,19.689,29.662 +2020-05-24 15:30:00,74.8,50.771,19.689,29.662 +2020-05-24 15:45:00,76.05,48.638999999999996,19.689,29.662 +2020-05-24 16:00:00,84.67,50.972,22.875,29.662 +2020-05-24 16:15:00,87.35,50.39,22.875,29.662 +2020-05-24 16:30:00,87.69,50.867,22.875,29.662 +2020-05-24 16:45:00,83.03,46.857,22.875,29.662 +2020-05-24 17:00:00,83.2,49.927,33.884,29.662 +2020-05-24 17:15:00,80.98,49.951,33.884,29.662 +2020-05-24 17:30:00,82.32,50.475,33.884,29.662 +2020-05-24 17:45:00,86.46,50.239,33.884,29.662 +2020-05-24 18:00:00,88.05,54.983000000000004,38.453,29.662 +2020-05-24 18:15:00,84.96,56.722,38.453,29.662 +2020-05-24 18:30:00,81.33,55.413999999999994,38.453,29.662 +2020-05-24 18:45:00,80.49,57.535,38.453,29.662 +2020-05-24 19:00:00,83.33,59.87,39.221,29.662 +2020-05-24 19:15:00,81.36,57.926,39.221,29.662 +2020-05-24 19:30:00,79.76,58.052,39.221,29.662 +2020-05-24 19:45:00,80.78,59.06,39.221,29.662 +2020-05-24 20:00:00,82.2,58.438,37.871,29.662 +2020-05-24 20:15:00,82.3,58.229,37.871,29.662 +2020-05-24 20:30:00,83.51,57.387,37.871,29.662 +2020-05-24 20:45:00,83.49,56.669,37.871,29.662 +2020-05-24 21:00:00,81.41,53.983999999999995,36.465,29.662 +2020-05-24 21:15:00,82.26,56.519,36.465,29.662 +2020-05-24 21:30:00,75.32,57.03,36.465,29.662 +2020-05-24 21:45:00,76.43,57.966,36.465,29.662 +2020-05-24 22:00:00,74.5,57.299,36.092,29.662 +2020-05-24 22:15:00,78.91,56.873999999999995,36.092,29.662 +2020-05-24 22:30:00,80.16,55.325,36.092,29.662 +2020-05-24 22:45:00,79.85,52.865,36.092,29.662 +2020-05-24 23:00:00,71.49,47.725,31.013,29.662 +2020-05-24 23:15:00,69.19,45.586999999999996,31.013,29.662 +2020-05-24 23:30:00,73.1,44.315,31.013,29.662 +2020-05-24 23:45:00,75.16,43.761,31.013,29.662 +2020-05-25 00:00:00,73.04,38.211999999999996,31.174,29.775 +2020-05-25 00:15:00,71.45,37.404,31.174,29.775 +2020-05-25 00:30:00,65.82,35.985,31.174,29.775 +2020-05-25 00:45:00,65.94,34.361,31.174,29.775 +2020-05-25 01:00:00,68.21,34.563,29.663,29.775 +2020-05-25 01:15:00,73.16,33.77,29.663,29.775 +2020-05-25 01:30:00,73.49,32.193000000000005,29.663,29.775 +2020-05-25 01:45:00,70.92,31.61,29.663,29.775 +2020-05-25 02:00:00,67.31,32.065,28.793000000000003,29.775 +2020-05-25 02:15:00,72.78,29.594,28.793000000000003,29.775 +2020-05-25 02:30:00,74.32,32.31,28.793000000000003,29.775 +2020-05-25 02:45:00,74.35,32.775999999999996,28.793000000000003,29.775 +2020-05-25 03:00:00,69.78,36.306999999999995,27.728,29.775 +2020-05-25 03:15:00,68.47,34.389,27.728,29.775 +2020-05-25 03:30:00,69.86,33.588,27.728,29.775 +2020-05-25 03:45:00,70.1,34.076,27.728,29.775 +2020-05-25 04:00:00,73.53,45.273999999999994,29.266,29.775 +2020-05-25 04:15:00,73.54,55.622,29.266,29.775 +2020-05-25 04:30:00,77.68,53.648,29.266,29.775 +2020-05-25 04:45:00,81.3,53.683,29.266,29.775 +2020-05-25 05:00:00,89.03,74.794,37.889,29.775 +2020-05-25 05:15:00,93.31,93.23,37.889,29.775 +2020-05-25 05:30:00,95.72,82.57700000000001,37.889,29.775 +2020-05-25 05:45:00,98.49,77.42699999999999,37.889,29.775 +2020-05-25 06:00:00,99.46,78.153,55.485,29.775 +2020-05-25 06:15:00,104.22,79.572,55.485,29.775 +2020-05-25 06:30:00,105.6,77.07,55.485,29.775 +2020-05-25 06:45:00,106.6,78.078,55.485,29.775 +2020-05-25 07:00:00,108.35,77.343,65.765,29.775 +2020-05-25 07:15:00,107.73,77.042,65.765,29.775 +2020-05-25 07:30:00,107.92,74.193,65.765,29.775 +2020-05-25 07:45:00,108.55,73.4,65.765,29.775 +2020-05-25 08:00:00,106.16,67.673,56.745,29.775 +2020-05-25 08:15:00,106.57,69.807,56.745,29.775 +2020-05-25 08:30:00,107.31,68.376,56.745,29.775 +2020-05-25 08:45:00,106.93,71.00399999999999,56.745,29.775 +2020-05-25 09:00:00,106.44,67.383,53.321999999999996,29.775 +2020-05-25 09:15:00,106.7,65.695,53.321999999999996,29.775 +2020-05-25 09:30:00,105.96,68.01100000000001,53.321999999999996,29.775 +2020-05-25 09:45:00,105.74,68.217,53.321999999999996,29.775 +2020-05-25 10:00:00,106.53,66.318,51.309,29.775 +2020-05-25 10:15:00,109.6,68.087,51.309,29.775 +2020-05-25 10:30:00,108.78,67.915,51.309,29.775 +2020-05-25 10:45:00,107.84,68.066,51.309,29.775 +2020-05-25 11:00:00,105.05,63.818000000000005,50.415,29.775 +2020-05-25 11:15:00,103.48,64.858,50.415,29.775 +2020-05-25 11:30:00,101.34,67.70100000000001,50.415,29.775 +2020-05-25 11:45:00,101.35,69.223,50.415,29.775 +2020-05-25 12:00:00,100.54,65.095,48.273,29.775 +2020-05-25 12:15:00,101.38,65.3,48.273,29.775 +2020-05-25 12:30:00,99.35,63.31,48.273,29.775 +2020-05-25 12:45:00,101.48,64.178,48.273,29.775 +2020-05-25 13:00:00,100.53,65.195,48.452,29.775 +2020-05-25 13:15:00,101.37,64.317,48.452,29.775 +2020-05-25 13:30:00,99.07,62.199,48.452,29.775 +2020-05-25 13:45:00,100.17,61.278999999999996,48.452,29.775 +2020-05-25 14:00:00,99.77,62.183,48.35,29.775 +2020-05-25 14:15:00,102.24,60.898999999999994,48.35,29.775 +2020-05-25 14:30:00,103.48,59.428000000000004,48.35,29.775 +2020-05-25 14:45:00,101.43,60.82899999999999,48.35,29.775 +2020-05-25 15:00:00,101.0,61.948,48.838,29.775 +2020-05-25 15:15:00,97.03,58.513000000000005,48.838,29.775 +2020-05-25 15:30:00,98.64,56.886,48.838,29.775 +2020-05-25 15:45:00,101.34,54.193999999999996,48.838,29.775 +2020-05-25 16:00:00,101.37,57.505,50.873000000000005,29.775 +2020-05-25 16:15:00,103.25,56.806999999999995,50.873000000000005,29.775 +2020-05-25 16:30:00,106.69,56.475,50.873000000000005,29.775 +2020-05-25 16:45:00,106.5,52.178000000000004,50.873000000000005,29.775 +2020-05-25 17:00:00,107.1,54.208999999999996,56.637,29.775 +2020-05-25 17:15:00,111.13,54.353,56.637,29.775 +2020-05-25 17:30:00,110.99,54.388000000000005,56.637,29.775 +2020-05-25 17:45:00,111.14,53.427,56.637,29.775 +2020-05-25 18:00:00,108.96,57.196999999999996,56.35,29.775 +2020-05-25 18:15:00,105.76,56.744,56.35,29.775 +2020-05-25 18:30:00,113.14,54.85,56.35,29.775 +2020-05-25 18:45:00,116.77,60.083,56.35,29.775 +2020-05-25 19:00:00,108.64,61.768,56.023,29.775 +2020-05-25 19:15:00,98.73,60.848,56.023,29.775 +2020-05-25 19:30:00,103.11,60.716,56.023,29.775 +2020-05-25 19:45:00,99.07,60.978,56.023,29.775 +2020-05-25 20:00:00,96.44,58.687,62.372,29.775 +2020-05-25 20:15:00,103.82,59.299,62.372,29.775 +2020-05-25 20:30:00,106.27,58.593999999999994,62.372,29.775 +2020-05-25 20:45:00,103.64,58.45,62.372,29.775 +2020-05-25 21:00:00,96.06,55.285,57.516999999999996,29.775 +2020-05-25 21:15:00,96.57,58.018,57.516999999999996,29.775 +2020-05-25 21:30:00,96.34,58.732,57.516999999999996,29.775 +2020-05-25 21:45:00,96.09,59.38399999999999,57.516999999999996,29.775 +2020-05-25 22:00:00,89.25,56.24,51.823,29.775 +2020-05-25 22:15:00,83.2,57.45399999999999,51.823,29.775 +2020-05-25 22:30:00,87.35,49.917,51.823,29.775 +2020-05-25 22:45:00,89.63,46.472,51.823,29.775 +2020-05-25 23:00:00,82.93,41.511,43.832,29.775 +2020-05-25 23:15:00,75.55,38.372,43.832,29.775 +2020-05-25 23:30:00,79.33,37.609,43.832,29.775 +2020-05-25 23:45:00,82.15,37.052,43.832,29.775 +2020-05-26 00:00:00,77.88,35.689,42.371,29.775 +2020-05-26 00:15:00,75.1,35.991,42.371,29.775 +2020-05-26 00:30:00,78.04,35.06,42.371,29.775 +2020-05-26 00:45:00,78.89,33.999,42.371,29.775 +2020-05-26 01:00:00,77.82,33.665,39.597,29.775 +2020-05-26 01:15:00,76.09,32.865,39.597,29.775 +2020-05-26 01:30:00,78.26,31.186,39.597,29.775 +2020-05-26 01:45:00,79.73,30.171999999999997,39.597,29.775 +2020-05-26 02:00:00,79.78,30.166999999999998,38.298,29.775 +2020-05-26 02:15:00,76.12,28.761999999999997,38.298,29.775 +2020-05-26 02:30:00,79.45,30.995,38.298,29.775 +2020-05-26 02:45:00,81.63,31.781999999999996,38.298,29.775 +2020-05-26 03:00:00,78.81,34.551,37.884,29.775 +2020-05-26 03:15:00,75.75,33.34,37.884,29.775 +2020-05-26 03:30:00,80.4,32.609,37.884,29.775 +2020-05-26 03:45:00,82.09,32.106,37.884,29.775 +2020-05-26 04:00:00,81.49,42.005,39.442,29.775 +2020-05-26 04:15:00,78.42,52.242,39.442,29.775 +2020-05-26 04:30:00,81.46,50.07899999999999,39.442,29.775 +2020-05-26 04:45:00,86.44,50.81399999999999,39.442,29.775 +2020-05-26 05:00:00,92.93,74.25399999999999,43.608000000000004,29.775 +2020-05-26 05:15:00,95.98,93.09100000000001,43.608000000000004,29.775 +2020-05-26 05:30:00,97.87,82.4,43.608000000000004,29.775 +2020-05-26 05:45:00,100.97,76.611,43.608000000000004,29.775 +2020-05-26 06:00:00,104.23,78.155,54.99100000000001,29.775 +2020-05-26 06:15:00,106.17,80.044,54.99100000000001,29.775 +2020-05-26 06:30:00,107.29,77.12899999999999,54.99100000000001,29.775 +2020-05-26 06:45:00,108.62,77.236,54.99100000000001,29.775 +2020-05-26 07:00:00,111.63,76.596,66.217,29.775 +2020-05-26 07:15:00,109.37,76.031,66.217,29.775 +2020-05-26 07:30:00,108.15,73.069,66.217,29.775 +2020-05-26 07:45:00,107.28,71.422,66.217,29.775 +2020-05-26 08:00:00,105.84,65.657,60.151,29.775 +2020-05-26 08:15:00,107.1,67.205,60.151,29.775 +2020-05-26 08:30:00,107.63,65.885,60.151,29.775 +2020-05-26 08:45:00,105.85,67.601,60.151,29.775 +2020-05-26 09:00:00,106.2,64.122,53.873000000000005,29.775 +2020-05-26 09:15:00,106.76,62.643,53.873000000000005,29.775 +2020-05-26 09:30:00,106.29,65.755,53.873000000000005,29.775 +2020-05-26 09:45:00,107.29,67.207,53.873000000000005,29.775 +2020-05-26 10:00:00,107.68,63.907,51.417,29.775 +2020-05-26 10:15:00,108.29,65.344,51.417,29.775 +2020-05-26 10:30:00,107.02,65.244,51.417,29.775 +2020-05-26 10:45:00,106.79,66.407,51.417,29.775 +2020-05-26 11:00:00,103.4,62.48,50.43600000000001,29.775 +2020-05-26 11:15:00,103.94,63.838,50.43600000000001,29.775 +2020-05-26 11:30:00,107.61,65.347,50.43600000000001,29.775 +2020-05-26 11:45:00,110.51,66.61399999999999,50.43600000000001,29.775 +2020-05-26 12:00:00,108.4,62.042,47.468,29.775 +2020-05-26 12:15:00,103.09,62.467,47.468,29.775 +2020-05-26 12:30:00,104.04,61.398,47.468,29.775 +2020-05-26 12:45:00,101.49,62.843,47.468,29.775 +2020-05-26 13:00:00,102.78,63.45399999999999,48.453,29.775 +2020-05-26 13:15:00,107.92,64.171,48.453,29.775 +2020-05-26 13:30:00,112.42,62.297,48.453,29.775 +2020-05-26 13:45:00,112.14,60.523999999999994,48.453,29.775 +2020-05-26 14:00:00,110.26,61.941,48.435,29.775 +2020-05-26 14:15:00,105.71,60.5,48.435,29.775 +2020-05-26 14:30:00,111.81,59.43600000000001,48.435,29.775 +2020-05-26 14:45:00,112.96,60.092,48.435,29.775 +2020-05-26 15:00:00,112.99,61.018,49.966,29.775 +2020-05-26 15:15:00,110.34,58.498000000000005,49.966,29.775 +2020-05-26 15:30:00,113.39,56.705,49.966,29.775 +2020-05-26 15:45:00,116.68,54.229,49.966,29.775 +2020-05-26 16:00:00,118.92,57.022,51.184,29.775 +2020-05-26 16:15:00,115.17,56.494,51.184,29.775 +2020-05-26 16:30:00,114.9,55.968,51.184,29.775 +2020-05-26 16:45:00,120.29,52.354,51.184,29.775 +2020-05-26 17:00:00,120.37,54.74,56.138999999999996,29.775 +2020-05-26 17:15:00,116.21,55.26,56.138999999999996,29.775 +2020-05-26 17:30:00,111.59,55.015,56.138999999999996,29.775 +2020-05-26 17:45:00,120.21,53.681000000000004,56.138999999999996,29.775 +2020-05-26 18:00:00,122.33,56.542,57.038000000000004,29.775 +2020-05-26 18:15:00,115.55,57.373000000000005,57.038000000000004,29.775 +2020-05-26 18:30:00,110.43,55.159,57.038000000000004,29.775 +2020-05-26 18:45:00,113.6,60.367,57.038000000000004,29.775 +2020-05-26 19:00:00,113.25,60.945,56.492,29.775 +2020-05-26 19:15:00,105.86,60.135,56.492,29.775 +2020-05-26 19:30:00,105.79,59.665,56.492,29.775 +2020-05-26 19:45:00,100.27,60.254,56.492,29.775 +2020-05-26 20:00:00,101.3,58.349,62.534,29.775 +2020-05-26 20:15:00,105.86,57.463,62.534,29.775 +2020-05-26 20:30:00,107.47,57.018,62.534,29.775 +2020-05-26 20:45:00,104.42,57.199,62.534,29.775 +2020-05-26 21:00:00,97.23,54.727,55.506,29.775 +2020-05-26 21:15:00,102.65,56.256,55.506,29.775 +2020-05-26 21:30:00,99.18,56.97,55.506,29.775 +2020-05-26 21:45:00,96.09,57.873999999999995,55.506,29.775 +2020-05-26 22:00:00,85.21,55.248000000000005,51.472,29.775 +2020-05-26 22:15:00,90.23,56.108999999999995,51.472,29.775 +2020-05-26 22:30:00,91.74,48.865,51.472,29.775 +2020-05-26 22:45:00,88.76,45.456,51.472,29.775 +2020-05-26 23:00:00,79.55,39.731,44.593,29.775 +2020-05-26 23:15:00,79.85,37.953,44.593,29.775 +2020-05-26 23:30:00,86.2,37.123000000000005,44.593,29.775 +2020-05-26 23:45:00,87.91,36.645,44.593,29.775 +2020-05-27 00:00:00,78.67,35.461999999999996,41.978,29.775 +2020-05-27 00:15:00,76.21,35.769,41.978,29.775 +2020-05-27 00:30:00,74.83,34.839,41.978,29.775 +2020-05-27 00:45:00,82.21,33.784,41.978,29.775 +2020-05-27 01:00:00,79.98,33.466,38.59,29.775 +2020-05-27 01:15:00,79.27,32.641999999999996,38.59,29.775 +2020-05-27 01:30:00,75.92,30.951,38.59,29.775 +2020-05-27 01:45:00,75.33,29.930999999999997,38.59,29.775 +2020-05-27 02:00:00,79.93,29.924,36.23,29.775 +2020-05-27 02:15:00,81.78,28.51,36.23,29.775 +2020-05-27 02:30:00,78.49,30.748,36.23,29.775 +2020-05-27 02:45:00,76.39,31.541999999999998,36.23,29.775 +2020-05-27 03:00:00,74.78,34.317,35.867,29.775 +2020-05-27 03:15:00,81.81,33.095,35.867,29.775 +2020-05-27 03:30:00,83.72,32.368,35.867,29.775 +2020-05-27 03:45:00,84.2,31.891,35.867,29.775 +2020-05-27 04:00:00,80.31,41.731,36.75,29.775 +2020-05-27 04:15:00,79.3,51.916000000000004,36.75,29.775 +2020-05-27 04:30:00,80.69,49.742,36.75,29.775 +2020-05-27 04:45:00,85.87,50.47,36.75,29.775 +2020-05-27 05:00:00,92.63,73.78399999999999,40.461,29.775 +2020-05-27 05:15:00,97.73,92.46600000000001,40.461,29.775 +2020-05-27 05:30:00,101.04,81.842,40.461,29.775 +2020-05-27 05:45:00,102.83,76.115,40.461,29.775 +2020-05-27 06:00:00,108.55,77.681,55.481,29.775 +2020-05-27 06:15:00,111.03,79.547,55.481,29.775 +2020-05-27 06:30:00,114.05,76.648,55.481,29.775 +2020-05-27 06:45:00,116.14,76.777,55.481,29.775 +2020-05-27 07:00:00,118.55,76.126,68.45,29.775 +2020-05-27 07:15:00,117.45,75.571,68.45,29.775 +2020-05-27 07:30:00,116.41,72.589,68.45,29.775 +2020-05-27 07:45:00,115.84,70.972,68.45,29.775 +2020-05-27 08:00:00,115.67,65.21300000000001,60.885,29.775 +2020-05-27 08:15:00,117.03,66.808,60.885,29.775 +2020-05-27 08:30:00,117.65,65.476,60.885,29.775 +2020-05-27 08:45:00,116.56,67.208,60.885,29.775 +2020-05-27 09:00:00,113.89,63.724,56.887,29.775 +2020-05-27 09:15:00,113.51,62.251000000000005,56.887,29.775 +2020-05-27 09:30:00,110.46,65.37,56.887,29.775 +2020-05-27 09:45:00,113.38,66.851,56.887,29.775 +2020-05-27 10:00:00,111.51,63.56100000000001,54.401,29.775 +2020-05-27 10:15:00,109.58,65.025,54.401,29.775 +2020-05-27 10:30:00,110.08,64.936,54.401,29.775 +2020-05-27 10:45:00,111.45,66.111,54.401,29.775 +2020-05-27 11:00:00,116.34,62.177,53.678000000000004,29.775 +2020-05-27 11:15:00,116.28,63.548,53.678000000000004,29.775 +2020-05-27 11:30:00,120.85,65.04899999999999,53.678000000000004,29.775 +2020-05-27 11:45:00,119.2,66.324,53.678000000000004,29.775 +2020-05-27 12:00:00,113.31,61.788000000000004,51.68,29.775 +2020-05-27 12:15:00,114.74,62.218,51.68,29.775 +2020-05-27 12:30:00,110.65,61.11600000000001,51.68,29.775 +2020-05-27 12:45:00,108.47,62.567,51.68,29.775 +2020-05-27 13:00:00,109.49,63.18600000000001,51.263000000000005,29.775 +2020-05-27 13:15:00,113.95,63.903999999999996,51.263000000000005,29.775 +2020-05-27 13:30:00,118.69,62.041000000000004,51.263000000000005,29.775 +2020-05-27 13:45:00,116.61,60.272,51.263000000000005,29.775 +2020-05-27 14:00:00,106.92,61.72,51.107,29.775 +2020-05-27 14:15:00,103.29,60.273,51.107,29.775 +2020-05-27 14:30:00,103.58,59.173,51.107,29.775 +2020-05-27 14:45:00,101.55,59.833999999999996,51.107,29.775 +2020-05-27 15:00:00,101.02,60.805,51.498000000000005,29.775 +2020-05-27 15:15:00,106.26,58.272,51.498000000000005,29.775 +2020-05-27 15:30:00,108.1,56.458,51.498000000000005,29.775 +2020-05-27 15:45:00,106.97,53.967,51.498000000000005,29.775 +2020-05-27 16:00:00,104.58,56.803000000000004,53.376999999999995,29.775 +2020-05-27 16:15:00,112.71,56.263000000000005,53.376999999999995,29.775 +2020-05-27 16:30:00,115.07,55.753,53.376999999999995,29.775 +2020-05-27 16:45:00,117.58,52.091,53.376999999999995,29.775 +2020-05-27 17:00:00,113.92,54.519,56.965,29.775 +2020-05-27 17:15:00,109.55,55.019,56.965,29.775 +2020-05-27 17:30:00,110.79,54.761,56.965,29.775 +2020-05-27 17:45:00,117.21,53.397,56.965,29.775 +2020-05-27 18:00:00,119.07,56.273,58.231,29.775 +2020-05-27 18:15:00,116.37,57.083,58.231,29.775 +2020-05-27 18:30:00,110.78,54.858000000000004,58.231,29.775 +2020-05-27 18:45:00,111.25,60.065,58.231,29.775 +2020-05-27 19:00:00,112.51,60.646,58.865,29.775 +2020-05-27 19:15:00,110.24,59.83,58.865,29.775 +2020-05-27 19:30:00,106.93,59.354,58.865,29.775 +2020-05-27 19:45:00,104.44,59.942,58.865,29.775 +2020-05-27 20:00:00,106.22,58.015,65.605,29.775 +2020-05-27 20:15:00,109.79,57.128,65.605,29.775 +2020-05-27 20:30:00,105.72,56.702,65.605,29.775 +2020-05-27 20:45:00,106.65,56.923,65.605,29.775 +2020-05-27 21:00:00,103.35,54.456,58.083999999999996,29.775 +2020-05-27 21:15:00,102.42,55.998999999999995,58.083999999999996,29.775 +2020-05-27 21:30:00,96.73,56.688,58.083999999999996,29.775 +2020-05-27 21:45:00,90.01,57.614,58.083999999999996,29.775 +2020-05-27 22:00:00,84.55,55.013999999999996,53.243,29.775 +2020-05-27 22:15:00,92.14,55.89,53.243,29.775 +2020-05-27 22:30:00,90.68,48.641999999999996,53.243,29.775 +2020-05-27 22:45:00,91.0,45.221000000000004,53.243,29.775 +2020-05-27 23:00:00,84.6,39.466,44.283,29.775 +2020-05-27 23:15:00,85.5,37.732,44.283,29.775 +2020-05-27 23:30:00,85.54,36.907,44.283,29.775 +2020-05-27 23:45:00,79.34,36.42,44.283,29.775 +2020-05-28 00:00:00,77.91,35.239000000000004,40.219,29.775 +2020-05-28 00:15:00,80.31,35.55,40.219,29.775 +2020-05-28 00:30:00,82.35,34.621,40.219,29.775 +2020-05-28 00:45:00,82.17,33.571,40.219,29.775 +2020-05-28 01:00:00,73.42,33.269,37.959,29.775 +2020-05-28 01:15:00,73.91,32.423,37.959,29.775 +2020-05-28 01:30:00,80.25,30.72,37.959,29.775 +2020-05-28 01:45:00,80.75,29.695,37.959,29.775 +2020-05-28 02:00:00,78.89,29.686,36.113,29.775 +2020-05-28 02:15:00,76.48,28.261999999999997,36.113,29.775 +2020-05-28 02:30:00,73.17,30.505,36.113,29.775 +2020-05-28 02:45:00,71.76,31.307,36.113,29.775 +2020-05-28 03:00:00,82.48,34.086,35.546,29.775 +2020-05-28 03:15:00,81.95,32.857,35.546,29.775 +2020-05-28 03:30:00,83.28,32.132,35.546,29.775 +2020-05-28 03:45:00,79.02,31.68,35.546,29.775 +2020-05-28 04:00:00,76.6,41.464,37.169000000000004,29.775 +2020-05-28 04:15:00,77.69,51.595,37.169000000000004,29.775 +2020-05-28 04:30:00,81.45,49.409,37.169000000000004,29.775 +2020-05-28 04:45:00,85.59,50.132,37.169000000000004,29.775 +2020-05-28 05:00:00,91.9,73.321,41.233000000000004,29.775 +2020-05-28 05:15:00,94.79,91.84899999999999,41.233000000000004,29.775 +2020-05-28 05:30:00,97.76,81.291,41.233000000000004,29.775 +2020-05-28 05:45:00,101.42,75.626,41.233000000000004,29.775 +2020-05-28 06:00:00,106.7,77.21300000000001,52.57,29.775 +2020-05-28 06:15:00,107.83,79.056,52.57,29.775 +2020-05-28 06:30:00,107.22,76.17399999999999,52.57,29.775 +2020-05-28 06:45:00,108.72,76.32600000000001,52.57,29.775 +2020-05-28 07:00:00,112.97,75.663,64.53,29.775 +2020-05-28 07:15:00,113.42,75.118,64.53,29.775 +2020-05-28 07:30:00,111.88,72.117,64.53,29.775 +2020-05-28 07:45:00,110.47,70.532,64.53,29.775 +2020-05-28 08:00:00,108.25,64.778,55.911,29.775 +2020-05-28 08:15:00,109.2,66.418,55.911,29.775 +2020-05-28 08:30:00,108.89,65.07600000000001,55.911,29.775 +2020-05-28 08:45:00,108.36,66.82300000000001,55.911,29.775 +2020-05-28 09:00:00,108.84,63.333999999999996,50.949,29.775 +2020-05-28 09:15:00,109.6,61.867,50.949,29.775 +2020-05-28 09:30:00,109.68,64.991,50.949,29.775 +2020-05-28 09:45:00,109.89,66.501,50.949,29.775 +2020-05-28 10:00:00,109.06,63.222,48.136,29.775 +2020-05-28 10:15:00,109.02,64.71300000000001,48.136,29.775 +2020-05-28 10:30:00,108.21,64.633,48.136,29.775 +2020-05-28 10:45:00,110.76,65.82,48.136,29.775 +2020-05-28 11:00:00,110.74,61.88,46.643,29.775 +2020-05-28 11:15:00,114.54,63.265,46.643,29.775 +2020-05-28 11:30:00,116.21,64.755,46.643,29.775 +2020-05-28 11:45:00,112.65,66.04,46.643,29.775 +2020-05-28 12:00:00,108.35,61.538999999999994,44.098,29.775 +2020-05-28 12:15:00,107.22,61.972,44.098,29.775 +2020-05-28 12:30:00,101.85,60.839,44.098,29.775 +2020-05-28 12:45:00,104.76,62.294,44.098,29.775 +2020-05-28 13:00:00,104.61,62.92100000000001,43.717,29.775 +2020-05-28 13:15:00,106.42,63.643,43.717,29.775 +2020-05-28 13:30:00,104.67,61.792,43.717,29.775 +2020-05-28 13:45:00,106.86,60.025,43.717,29.775 +2020-05-28 14:00:00,109.05,61.505,44.218999999999994,29.775 +2020-05-28 14:15:00,109.96,60.051,44.218999999999994,29.775 +2020-05-28 14:30:00,111.04,58.915,44.218999999999994,29.775 +2020-05-28 14:45:00,111.45,59.58,44.218999999999994,29.775 +2020-05-28 15:00:00,114.19,60.596000000000004,46.159,29.775 +2020-05-28 15:15:00,116.88,58.05,46.159,29.775 +2020-05-28 15:30:00,117.36,56.215,46.159,29.775 +2020-05-28 15:45:00,118.28,53.708999999999996,46.159,29.775 +2020-05-28 16:00:00,120.09,56.588,47.115,29.775 +2020-05-28 16:15:00,117.02,56.037,47.115,29.775 +2020-05-28 16:30:00,117.12,55.543,47.115,29.775 +2020-05-28 16:45:00,115.03,51.835,47.115,29.775 +2020-05-28 17:00:00,115.21,54.302,50.827,29.775 +2020-05-28 17:15:00,114.26,54.784,50.827,29.775 +2020-05-28 17:30:00,113.32,54.513999999999996,50.827,29.775 +2020-05-28 17:45:00,113.31,53.118,50.827,29.775 +2020-05-28 18:00:00,114.29,56.00899999999999,52.586000000000006,29.775 +2020-05-28 18:15:00,111.81,56.798,52.586000000000006,29.775 +2020-05-28 18:30:00,113.58,54.565,52.586000000000006,29.775 +2020-05-28 18:45:00,111.62,59.769,52.586000000000006,29.775 +2020-05-28 19:00:00,107.16,60.352,51.886,29.775 +2020-05-28 19:15:00,104.15,59.531000000000006,51.886,29.775 +2020-05-28 19:30:00,103.5,59.049,51.886,29.775 +2020-05-28 19:45:00,102.04,59.635,51.886,29.775 +2020-05-28 20:00:00,100.18,57.68600000000001,56.162,29.775 +2020-05-28 20:15:00,101.47,56.798,56.162,29.775 +2020-05-28 20:30:00,103.36,56.391000000000005,56.162,29.775 +2020-05-28 20:45:00,99.21,56.652,56.162,29.775 +2020-05-28 21:00:00,100.34,54.191,53.023,29.775 +2020-05-28 21:15:00,99.7,55.746,53.023,29.775 +2020-05-28 21:30:00,97.4,56.413000000000004,53.023,29.775 +2020-05-28 21:45:00,98.85,57.358000000000004,53.023,29.775 +2020-05-28 22:00:00,89.45,54.783,49.303999999999995,29.775 +2020-05-28 22:15:00,90.82,55.674,49.303999999999995,29.775 +2020-05-28 22:30:00,91.85,48.423,49.303999999999995,29.775 +2020-05-28 22:45:00,92.06,44.99,49.303999999999995,29.775 +2020-05-28 23:00:00,82.98,39.205999999999996,43.409,29.775 +2020-05-28 23:15:00,78.65,37.515,43.409,29.775 +2020-05-28 23:30:00,84.66,36.694,43.409,29.775 +2020-05-28 23:45:00,83.7,36.199,43.409,29.775 +2020-05-29 00:00:00,82.83,33.088,39.884,29.775 +2020-05-29 00:15:00,79.0,33.650999999999996,39.884,29.775 +2020-05-29 00:30:00,76.64,32.953,39.884,29.775 +2020-05-29 00:45:00,75.71,32.335,39.884,29.775 +2020-05-29 01:00:00,75.93,31.62,37.658,29.775 +2020-05-29 01:15:00,75.57,30.374000000000002,37.658,29.775 +2020-05-29 01:30:00,74.04,29.29,37.658,29.775 +2020-05-29 01:45:00,79.8,28.044,37.658,29.775 +2020-05-29 02:00:00,80.93,28.939,36.707,29.775 +2020-05-29 02:15:00,81.07,27.433000000000003,36.707,29.775 +2020-05-29 02:30:00,74.85,30.576,36.707,29.775 +2020-05-29 02:45:00,77.58,30.746,36.707,29.775 +2020-05-29 03:00:00,83.0,34.054,37.025,29.775 +2020-05-29 03:15:00,79.28,31.87,37.025,29.775 +2020-05-29 03:30:00,78.44,30.916999999999998,37.025,29.775 +2020-05-29 03:45:00,80.42,31.393,37.025,29.775 +2020-05-29 04:00:00,86.4,41.3,38.349000000000004,29.775 +2020-05-29 04:15:00,88.81,49.849,38.349000000000004,29.775 +2020-05-29 04:30:00,85.87,48.602,38.349000000000004,29.775 +2020-05-29 04:45:00,86.7,48.461000000000006,38.349000000000004,29.775 +2020-05-29 05:00:00,92.38,70.797,41.565,29.775 +2020-05-29 05:15:00,96.58,90.395,41.565,29.775 +2020-05-29 05:30:00,99.6,80.309,41.565,29.775 +2020-05-29 05:45:00,103.24,74.263,41.565,29.775 +2020-05-29 06:00:00,109.02,76.183,53.861000000000004,29.775 +2020-05-29 06:15:00,107.63,77.858,53.861000000000004,29.775 +2020-05-29 06:30:00,108.54,74.766,53.861000000000004,29.775 +2020-05-29 06:45:00,109.06,75.15100000000001,53.861000000000004,29.775 +2020-05-29 07:00:00,110.54,74.923,63.497,29.775 +2020-05-29 07:15:00,108.76,75.47,63.497,29.775 +2020-05-29 07:30:00,108.91,70.567,63.497,29.775 +2020-05-29 07:45:00,107.71,68.62,63.497,29.775 +2020-05-29 08:00:00,107.34,63.365,55.43899999999999,29.775 +2020-05-29 08:15:00,109.81,65.593,55.43899999999999,29.775 +2020-05-29 08:30:00,113.11,64.368,55.43899999999999,29.775 +2020-05-29 08:45:00,111.74,65.642,55.43899999999999,29.775 +2020-05-29 09:00:00,109.3,60.086999999999996,52.132,29.775 +2020-05-29 09:15:00,107.72,60.547,52.132,29.775 +2020-05-29 09:30:00,107.65,62.937,52.132,29.775 +2020-05-29 09:45:00,106.88,64.81,52.132,29.775 +2020-05-29 10:00:00,108.37,61.106,49.881,29.775 +2020-05-29 10:15:00,108.93,62.585,49.881,29.775 +2020-05-29 10:30:00,106.62,63.006,49.881,29.775 +2020-05-29 10:45:00,106.59,64.012,49.881,29.775 +2020-05-29 11:00:00,105.28,60.29,49.396,29.775 +2020-05-29 11:15:00,105.09,60.472,49.396,29.775 +2020-05-29 11:30:00,105.21,62.038999999999994,49.396,29.775 +2020-05-29 11:45:00,107.75,62.458999999999996,49.396,29.775 +2020-05-29 12:00:00,107.33,58.667,46.7,29.775 +2020-05-29 12:15:00,107.68,57.998999999999995,46.7,29.775 +2020-05-29 12:30:00,104.99,56.981,46.7,29.775 +2020-05-29 12:45:00,108.45,57.851000000000006,46.7,29.775 +2020-05-29 13:00:00,103.8,59.25,44.05,29.775 +2020-05-29 13:15:00,104.04,60.341,44.05,29.775 +2020-05-29 13:30:00,98.47,59.233000000000004,44.05,29.775 +2020-05-29 13:45:00,98.7,57.733999999999995,44.05,29.775 +2020-05-29 14:00:00,101.02,58.231,42.805,29.775 +2020-05-29 14:15:00,107.75,57.125,42.805,29.775 +2020-05-29 14:30:00,112.27,57.461000000000006,42.805,29.775 +2020-05-29 14:45:00,112.96,57.571000000000005,42.805,29.775 +2020-05-29 15:00:00,112.92,58.443000000000005,44.36600000000001,29.775 +2020-05-29 15:15:00,113.34,55.558,44.36600000000001,29.775 +2020-05-29 15:30:00,113.29,52.812,44.36600000000001,29.775 +2020-05-29 15:45:00,113.64,51.027,44.36600000000001,29.775 +2020-05-29 16:00:00,113.52,52.907,46.928999999999995,29.775 +2020-05-29 16:15:00,114.14,52.87,46.928999999999995,29.775 +2020-05-29 16:30:00,113.45,52.247,46.928999999999995,29.775 +2020-05-29 16:45:00,114.36,47.736000000000004,46.928999999999995,29.775 +2020-05-29 17:00:00,114.25,51.898,51.468,29.775 +2020-05-29 17:15:00,112.86,52.104,51.468,29.775 +2020-05-29 17:30:00,111.56,51.902,51.468,29.775 +2020-05-29 17:45:00,111.54,50.268,51.468,29.775 +2020-05-29 18:00:00,112.16,53.412,52.58,29.775 +2020-05-29 18:15:00,107.98,53.203,52.58,29.775 +2020-05-29 18:30:00,107.39,50.948,52.58,29.775 +2020-05-29 18:45:00,106.01,56.54600000000001,52.58,29.775 +2020-05-29 19:00:00,102.16,58.183,52.183,29.775 +2020-05-29 19:15:00,99.93,58.201,52.183,29.775 +2020-05-29 19:30:00,97.8,57.68600000000001,52.183,29.775 +2020-05-29 19:45:00,97.65,57.207,52.183,29.775 +2020-05-29 20:00:00,95.98,55.088,58.497,29.775 +2020-05-29 20:15:00,94.76,54.951,58.497,29.775 +2020-05-29 20:30:00,96.05,54.095,58.497,29.775 +2020-05-29 20:45:00,94.03,53.744,58.497,29.775 +2020-05-29 21:00:00,89.57,52.641999999999996,54.731,29.775 +2020-05-29 21:15:00,90.47,55.89,54.731,29.775 +2020-05-29 21:30:00,93.37,56.398999999999994,54.731,29.775 +2020-05-29 21:45:00,92.72,57.673,54.731,29.775 +2020-05-29 22:00:00,88.91,55.195,51.386,29.775 +2020-05-29 22:15:00,88.07,55.836999999999996,51.386,29.775 +2020-05-29 22:30:00,87.69,54.43600000000001,51.386,29.775 +2020-05-29 22:45:00,85.16,52.544,51.386,29.775 +2020-05-29 23:00:00,77.14,48.243,44.463,29.775 +2020-05-29 23:15:00,77.25,44.698,44.463,29.775 +2020-05-29 23:30:00,80.76,41.843999999999994,44.463,29.775 +2020-05-29 23:45:00,79.05,41.086000000000006,44.463,29.775 +2020-05-30 00:00:00,76.42,33.446,42.833999999999996,29.662 +2020-05-30 00:15:00,70.78,32.48,42.833999999999996,29.662 +2020-05-30 00:30:00,68.05,31.618000000000002,42.833999999999996,29.662 +2020-05-30 00:45:00,68.33,30.462,42.833999999999996,29.662 +2020-05-30 01:00:00,68.06,30.156,37.859,29.662 +2020-05-30 01:15:00,67.11,29.238000000000003,37.859,29.662 +2020-05-30 01:30:00,65.83,27.27,37.859,29.662 +2020-05-30 01:45:00,66.87,27.154,37.859,29.662 +2020-05-30 02:00:00,69.9,27.255,35.327,29.662 +2020-05-30 02:15:00,73.43,24.915,35.327,29.662 +2020-05-30 02:30:00,73.36,27.076999999999998,35.327,29.662 +2020-05-30 02:45:00,69.26,28.019000000000002,35.327,29.662 +2020-05-30 03:00:00,65.65,30.165,34.908,29.662 +2020-05-30 03:15:00,65.73,27.055,34.908,29.662 +2020-05-30 03:30:00,66.22,26.081999999999997,34.908,29.662 +2020-05-30 03:45:00,64.96,28.008000000000003,34.908,29.662 +2020-05-30 04:00:00,63.16,35.196999999999996,34.84,29.662 +2020-05-30 04:15:00,63.06,42.316,34.84,29.662 +2020-05-30 04:30:00,60.91,39.02,34.84,29.662 +2020-05-30 04:45:00,65.06,39.03,34.84,29.662 +2020-05-30 05:00:00,64.16,50.458,34.222,29.662 +2020-05-30 05:15:00,65.68,55.727,34.222,29.662 +2020-05-30 05:30:00,66.81,47.24,34.222,29.662 +2020-05-30 05:45:00,68.98,46.597,34.222,29.662 +2020-05-30 06:00:00,71.04,62.797,35.515,29.662 +2020-05-30 06:15:00,72.65,75.111,35.515,29.662 +2020-05-30 06:30:00,72.85,68.265,35.515,29.662 +2020-05-30 06:45:00,75.19,63.847,35.515,29.662 +2020-05-30 07:00:00,78.59,61.262,39.687,29.662 +2020-05-30 07:15:00,80.62,60.335,39.687,29.662 +2020-05-30 07:30:00,84.57,57.398999999999994,39.687,29.662 +2020-05-30 07:45:00,86.6,57.178000000000004,39.687,29.662 +2020-05-30 08:00:00,86.67,53.312,44.9,29.662 +2020-05-30 08:15:00,86.66,56.153999999999996,44.9,29.662 +2020-05-30 08:30:00,87.74,55.246,44.9,29.662 +2020-05-30 08:45:00,88.34,58.092,44.9,29.662 +2020-05-30 09:00:00,88.31,55.77,45.724,29.662 +2020-05-30 09:15:00,89.73,56.852,45.724,29.662 +2020-05-30 09:30:00,90.73,59.906000000000006,45.724,29.662 +2020-05-30 09:45:00,90.92,61.423,45.724,29.662 +2020-05-30 10:00:00,90.8,58.273999999999994,43.123999999999995,29.662 +2020-05-30 10:15:00,91.47,60.143,43.123999999999995,29.662 +2020-05-30 10:30:00,90.99,60.29600000000001,43.123999999999995,29.662 +2020-05-30 10:45:00,93.34,61.288000000000004,43.123999999999995,29.662 +2020-05-30 11:00:00,92.41,57.467,40.255,29.662 +2020-05-30 11:15:00,90.73,58.301,40.255,29.662 +2020-05-30 11:30:00,88.51,59.858999999999995,40.255,29.662 +2020-05-30 11:45:00,87.44,60.681000000000004,40.255,29.662 +2020-05-30 12:00:00,84.12,56.977,38.582,29.662 +2020-05-30 12:15:00,84.24,57.239,38.582,29.662 +2020-05-30 12:30:00,83.18,56.147,38.582,29.662 +2020-05-30 12:45:00,81.0,57.498999999999995,38.582,29.662 +2020-05-30 13:00:00,79.85,58.083999999999996,36.043,29.662 +2020-05-30 13:15:00,84.69,58.198,36.043,29.662 +2020-05-30 13:30:00,83.22,57.165,36.043,29.662 +2020-05-30 13:45:00,79.43,54.551,36.043,29.662 +2020-05-30 14:00:00,77.05,55.413999999999994,35.216,29.662 +2020-05-30 14:15:00,77.16,53.035,35.216,29.662 +2020-05-30 14:30:00,76.32,52.515,35.216,29.662 +2020-05-30 14:45:00,76.2,53.118,35.216,29.662 +2020-05-30 15:00:00,77.38,54.601000000000006,36.759,29.662 +2020-05-30 15:15:00,76.91,52.538000000000004,36.759,29.662 +2020-05-30 15:30:00,75.07,50.324,36.759,29.662 +2020-05-30 15:45:00,72.67,47.688,36.759,29.662 +2020-05-30 16:00:00,73.75,51.426,40.086,29.662 +2020-05-30 16:15:00,77.63,50.755,40.086,29.662 +2020-05-30 16:30:00,77.61,50.346000000000004,40.086,29.662 +2020-05-30 16:45:00,79.19,45.966,40.086,29.662 +2020-05-30 17:00:00,80.06,48.941,44.876999999999995,29.662 +2020-05-30 17:15:00,80.99,47.513999999999996,44.876999999999995,29.662 +2020-05-30 17:30:00,81.71,47.153999999999996,44.876999999999995,29.662 +2020-05-30 17:45:00,82.2,45.856,44.876999999999995,29.662 +2020-05-30 18:00:00,84.63,50.298,47.056000000000004,29.662 +2020-05-30 18:15:00,85.41,52.016999999999996,47.056000000000004,29.662 +2020-05-30 18:30:00,82.68,51.31,47.056000000000004,29.662 +2020-05-30 18:45:00,82.12,52.935,47.056000000000004,29.662 +2020-05-30 19:00:00,81.55,53.339,45.57,29.662 +2020-05-30 19:15:00,78.78,52.294,45.57,29.662 +2020-05-30 19:30:00,77.84,52.655,45.57,29.662 +2020-05-30 19:45:00,77.76,53.809,45.57,29.662 +2020-05-30 20:00:00,73.8,52.851000000000006,41.685,29.662 +2020-05-30 20:15:00,77.24,52.566,41.685,29.662 +2020-05-30 20:30:00,78.75,50.82899999999999,41.685,29.662 +2020-05-30 20:45:00,79.95,52.173,41.685,29.662 +2020-05-30 21:00:00,76.15,50.175,39.576,29.662 +2020-05-30 21:15:00,75.58,53.202,39.576,29.662 +2020-05-30 21:30:00,73.83,54.123000000000005,39.576,29.662 +2020-05-30 21:45:00,73.25,54.816,39.576,29.662 +2020-05-30 22:00:00,69.37,52.62,39.068000000000005,29.662 +2020-05-30 22:15:00,70.64,54.028999999999996,39.068000000000005,29.662 +2020-05-30 22:30:00,66.88,53.56100000000001,39.068000000000005,29.662 +2020-05-30 22:45:00,65.37,52.398999999999994,39.068000000000005,29.662 +2020-05-30 23:00:00,61.82,47.92100000000001,32.06,29.662 +2020-05-30 23:15:00,61.6,44.477,32.06,29.662 +2020-05-30 23:30:00,60.72,43.583999999999996,32.06,29.662 +2020-05-30 23:45:00,59.52,42.652,32.06,29.662 +2020-05-31 00:00:00,58.29,34.622,28.825,29.662 +2020-05-31 00:15:00,58.97,32.507,28.825,29.662 +2020-05-31 00:30:00,58.75,31.43,28.825,29.662 +2020-05-31 00:45:00,58.69,30.32,28.825,29.662 +2020-05-31 01:00:00,56.94,30.25,25.995,29.662 +2020-05-31 01:15:00,57.91,29.410999999999998,25.995,29.662 +2020-05-31 01:30:00,54.43,27.41,25.995,29.662 +2020-05-31 01:45:00,57.82,26.866,25.995,29.662 +2020-05-31 02:00:00,57.05,26.883000000000003,24.394000000000002,29.662 +2020-05-31 02:15:00,57.16,25.024,24.394000000000002,29.662 +2020-05-31 02:30:00,57.04,27.545,24.394000000000002,29.662 +2020-05-31 02:45:00,57.15,28.318,24.394000000000002,29.662 +2020-05-31 03:00:00,56.31,31.146,22.916999999999998,29.662 +2020-05-31 03:15:00,56.76,28.165,22.916999999999998,29.662 +2020-05-31 03:30:00,57.63,26.795,22.916999999999998,29.662 +2020-05-31 03:45:00,57.02,27.980999999999998,22.916999999999998,29.662 +2020-05-31 04:00:00,54.07,35.034,23.576999999999998,29.662 +2020-05-31 04:15:00,53.44,41.483000000000004,23.576999999999998,29.662 +2020-05-31 04:30:00,52.41,39.448,23.576999999999998,29.662 +2020-05-31 04:45:00,52.3,39.071999999999996,23.576999999999998,29.662 +2020-05-31 05:00:00,51.54,49.851000000000006,22.730999999999998,29.662 +2020-05-31 05:15:00,50.75,53.773999999999994,22.730999999999998,29.662 +2020-05-31 05:30:00,50.89,44.93899999999999,22.730999999999998,29.662 +2020-05-31 05:45:00,52.44,44.14,22.730999999999998,29.662 +2020-05-31 06:00:00,54.87,58.076,22.34,29.662 +2020-05-31 06:15:00,54.95,70.82600000000001,22.34,29.662 +2020-05-31 06:30:00,56.67,63.121,22.34,29.662 +2020-05-31 06:45:00,59.11,57.573,22.34,29.662 +2020-05-31 07:00:00,60.32,55.717,24.691999999999997,29.662 +2020-05-31 07:15:00,62.84,53.083,24.691999999999997,29.662 +2020-05-31 07:30:00,62.65,51.083999999999996,24.691999999999997,29.662 +2020-05-31 07:45:00,62.23,50.706,24.691999999999997,29.662 +2020-05-31 08:00:00,63.06,47.919,29.340999999999998,29.662 +2020-05-31 08:15:00,62.23,51.89,29.340999999999998,29.662 +2020-05-31 08:30:00,64.21,52.13399999999999,29.340999999999998,29.662 +2020-05-31 08:45:00,65.4,55.338,29.340999999999998,29.662 +2020-05-31 09:00:00,62.96,52.817,30.788,29.662 +2020-05-31 09:15:00,61.99,53.562,30.788,29.662 +2020-05-31 09:30:00,59.44,56.989,30.788,29.662 +2020-05-31 09:45:00,62.19,59.445,30.788,29.662 +2020-05-31 10:00:00,62.46,57.333999999999996,30.158,29.662 +2020-05-31 10:15:00,62.53,59.471000000000004,30.158,29.662 +2020-05-31 10:30:00,63.05,59.968999999999994,30.158,29.662 +2020-05-31 10:45:00,65.84,61.492,30.158,29.662 +2020-05-31 11:00:00,61.04,57.544,32.056,29.662 +2020-05-31 11:15:00,61.5,58.008,32.056,29.662 +2020-05-31 11:30:00,63.18,59.845,32.056,29.662 +2020-05-31 11:45:00,61.81,61.044,32.056,29.662 +2020-05-31 12:00:00,59.18,58.198,28.671999999999997,29.662 +2020-05-31 12:15:00,59.97,58.324,28.671999999999997,29.662 +2020-05-31 12:30:00,55.08,57.066,28.671999999999997,29.662 +2020-05-31 12:45:00,55.72,57.618,28.671999999999997,29.662 +2020-05-31 13:00:00,57.1,57.784,23.171,29.662 +2020-05-31 13:15:00,57.89,58.04600000000001,23.171,29.662 +2020-05-31 13:30:00,60.84,55.958,23.171,29.662 +2020-05-31 13:45:00,58.45,54.222,23.171,29.662 +2020-05-31 14:00:00,55.33,56.27,19.11,29.662 +2020-05-31 14:15:00,57.48,54.55,19.11,29.662 +2020-05-31 14:30:00,55.43,53.14,19.11,29.662 +2020-05-31 14:45:00,56.24,52.672,19.11,29.662 +2020-05-31 15:00:00,56.35,53.988,19.689,29.662 +2020-05-31 15:15:00,59.22,51.339,19.689,29.662 +2020-05-31 15:30:00,57.36,49.071999999999996,19.689,29.662 +2020-05-31 15:45:00,59.35,46.838,19.689,29.662 +2020-05-31 16:00:00,63.4,49.468,22.875,29.662 +2020-05-31 16:15:00,64.4,48.803999999999995,22.875,29.662 +2020-05-31 16:30:00,66.23,49.398999999999994,22.875,29.662 +2020-05-31 16:45:00,67.91,45.062,22.875,29.662 +2020-05-31 17:00:00,72.6,48.413999999999994,33.884,29.662 +2020-05-31 17:15:00,71.61,48.3,33.884,29.662 +2020-05-31 17:30:00,75.13,48.739,33.884,29.662 +2020-05-31 17:45:00,76.52,48.29,33.884,29.662 +2020-05-31 18:00:00,79.29,53.143,38.453,29.662 +2020-05-31 18:15:00,79.24,54.731,38.453,29.662 +2020-05-31 18:30:00,79.41,53.353,38.453,29.662 +2020-05-31 18:45:00,83.37,55.465,38.453,29.662 +2020-05-31 19:00:00,80.11,57.818000000000005,39.221,29.662 +2020-05-31 19:15:00,85.23,55.82899999999999,39.221,29.662 +2020-05-31 19:30:00,84.73,55.917,39.221,29.662 +2020-05-31 19:45:00,86.55,56.916000000000004,39.221,29.662 +2020-05-31 20:00:00,86.5,56.137,37.871,29.662 +2020-05-31 20:15:00,86.56,55.92,37.871,29.662 +2020-05-31 20:30:00,87.79,55.214,37.871,29.662 +2020-05-31 20:45:00,86.92,54.773999999999994,37.871,29.662 +2020-05-31 21:00:00,81.05,52.123999999999995,36.465,29.662 +2020-05-31 21:15:00,84.64,54.754,36.465,29.662 +2020-05-31 21:30:00,78.67,55.098,36.465,29.662 +2020-05-31 21:45:00,78.8,56.174,36.465,29.662 +2020-05-31 22:00:00,77.15,55.681999999999995,36.092,29.662 +2020-05-31 22:15:00,82.27,55.36600000000001,36.092,29.662 +2020-05-31 22:30:00,81.25,53.788000000000004,36.092,29.662 +2020-05-31 22:45:00,78.71,51.242,36.092,29.662 +2020-05-31 23:00:00,54.1,45.903999999999996,31.013,29.662 +2020-05-31 23:15:00,55.38,44.067,31.013,29.662 +2020-05-31 23:30:00,54.63,42.828,31.013,29.662 +2020-05-31 23:45:00,53.74,42.213,31.013,29.662 +2020-06-01 00:00:00,52.35,28.079,19.295,29.17 +2020-06-01 00:15:00,52.65,26.704,19.295,29.17 +2020-06-01 00:30:00,52.03,25.386999999999997,19.295,29.17 +2020-06-01 00:45:00,52.09,24.485,19.295,29.17 +2020-06-01 01:00:00,50.91,24.236,15.365,29.17 +2020-06-01 01:15:00,52.12,23.596,15.365,29.17 +2020-06-01 01:30:00,50.95,21.804000000000002,15.365,29.17 +2020-06-01 01:45:00,51.01,21.910999999999998,15.365,29.17 +2020-06-01 02:00:00,50.17,21.433000000000003,13.03,29.17 +2020-06-01 02:15:00,50.81,19.726,13.03,29.17 +2020-06-01 02:30:00,50.97,22.149,13.03,29.17 +2020-06-01 02:45:00,51.9,22.715999999999998,13.03,29.17 +2020-06-01 03:00:00,51.98,24.44,13.46,29.17 +2020-06-01 03:15:00,52.07,21.471,13.46,29.17 +2020-06-01 03:30:00,52.7,20.089000000000002,13.46,29.17 +2020-06-01 03:45:00,49.25,20.781,13.46,29.17 +2020-06-01 04:00:00,51.66,26.259,13.305,29.17 +2020-06-01 04:15:00,51.17,32.0,13.305,29.17 +2020-06-01 04:30:00,48.48,29.869,13.305,29.17 +2020-06-01 04:45:00,50.55,29.331999999999997,13.305,29.17 +2020-06-01 05:00:00,48.78,38.022,13.482000000000001,29.17 +2020-06-01 05:15:00,47.36,39.586,13.482000000000001,29.17 +2020-06-01 05:30:00,50.13,31.343000000000004,13.482000000000001,29.17 +2020-06-01 05:45:00,50.0,31.281,13.482000000000001,29.17 +2020-06-01 06:00:00,51.06,43.007,14.677999999999999,29.17 +2020-06-01 06:15:00,50.92,53.478,14.677999999999999,29.17 +2020-06-01 06:30:00,51.78,46.945,14.677999999999999,29.17 +2020-06-01 06:45:00,52.65,42.336999999999996,14.677999999999999,29.17 +2020-06-01 07:00:00,54.68,42.636,18.473,29.17 +2020-06-01 07:15:00,53.98,40.101,18.473,29.17 +2020-06-01 07:30:00,53.67,38.333,18.473,29.17 +2020-06-01 07:45:00,54.26,38.079,18.473,29.17 +2020-06-01 08:00:00,54.22,38.016,18.142,29.17 +2020-06-01 08:15:00,53.58,41.693999999999996,18.142,29.17 +2020-06-01 08:30:00,52.87,42.685,18.142,29.17 +2020-06-01 08:45:00,53.28,45.778999999999996,18.142,29.17 +2020-06-01 09:00:00,50.94,42.258,19.148,29.17 +2020-06-01 09:15:00,51.9,43.126999999999995,19.148,29.17 +2020-06-01 09:30:00,47.84,46.728,19.148,29.17 +2020-06-01 09:45:00,51.24,49.751999999999995,19.148,29.17 +2020-06-01 10:00:00,53.22,45.975,17.139,29.17 +2020-06-01 10:15:00,55.99,47.87,17.139,29.17 +2020-06-01 10:30:00,57.89,48.418,17.139,29.17 +2020-06-01 10:45:00,57.81,50.29600000000001,17.139,29.17 +2020-06-01 11:00:00,53.25,45.78,18.037,29.17 +2020-06-01 11:15:00,51.93,46.23,18.037,29.17 +2020-06-01 11:30:00,49.85,48.203,18.037,29.17 +2020-06-01 11:45:00,49.1,49.861000000000004,18.037,29.17 +2020-06-01 12:00:00,46.61,47.415,16.559,29.17 +2020-06-01 12:15:00,46.04,47.076,16.559,29.17 +2020-06-01 12:30:00,44.13,46.448,16.559,29.17 +2020-06-01 12:45:00,44.13,47.167,16.559,29.17 +2020-06-01 13:00:00,43.0,47.331,13.697000000000001,29.17 +2020-06-01 13:15:00,42.54,47.643,13.697000000000001,29.17 +2020-06-01 13:30:00,42.78,45.68899999999999,13.697000000000001,29.17 +2020-06-01 13:45:00,44.17,44.308,13.697000000000001,29.17 +2020-06-01 14:00:00,44.11,46.63399999999999,12.578,29.17 +2020-06-01 14:15:00,43.14,44.994,12.578,29.17 +2020-06-01 14:30:00,44.66,43.99,12.578,29.17 +2020-06-01 14:45:00,44.56,43.341,12.578,29.17 +2020-06-01 15:00:00,47.23,44.93899999999999,14.425999999999998,29.17 +2020-06-01 15:15:00,46.69,42.405,14.425999999999998,29.17 +2020-06-01 15:30:00,50.96,40.277,14.425999999999998,29.17 +2020-06-01 15:45:00,50.68,38.414,14.425999999999998,29.17 +2020-06-01 16:00:00,52.39,40.319,18.287,29.17 +2020-06-01 16:15:00,55.05,39.81,18.287,29.17 +2020-06-01 16:30:00,57.45,39.958,18.287,29.17 +2020-06-01 16:45:00,61.38,36.124,18.287,29.17 +2020-06-01 17:00:00,65.43,39.303000000000004,24.461,29.17 +2020-06-01 17:15:00,66.16,38.798,24.461,29.17 +2020-06-01 17:30:00,68.01,38.955999999999996,24.461,29.17 +2020-06-01 17:45:00,69.86,38.388000000000005,24.461,29.17 +2020-06-01 18:00:00,71.38,42.556000000000004,31.44,29.17 +2020-06-01 18:15:00,71.03,43.601000000000006,31.44,29.17 +2020-06-01 18:30:00,73.56,42.254,31.44,29.17 +2020-06-01 18:45:00,79.8,43.708999999999996,31.44,29.17 +2020-06-01 19:00:00,80.73,46.256,34.859,29.17 +2020-06-01 19:15:00,73.32,44.074,34.859,29.17 +2020-06-01 19:30:00,73.38,43.883,34.859,29.17 +2020-06-01 19:45:00,70.97,44.55,34.859,29.17 +2020-06-01 20:00:00,73.37,44.213,42.937,29.17 +2020-06-01 20:15:00,81.13,44.318999999999996,42.937,29.17 +2020-06-01 20:30:00,83.24,43.968,42.937,29.17 +2020-06-01 20:45:00,81.93,43.483999999999995,42.937,29.17 +2020-06-01 21:00:00,74.91,41.38399999999999,39.795,29.17 +2020-06-01 21:15:00,74.14,44.074,39.795,29.17 +2020-06-01 21:30:00,74.04,44.778999999999996,39.795,29.17 +2020-06-01 21:45:00,70.96,45.748999999999995,39.795,29.17 +2020-06-01 22:00:00,72.4,44.92100000000001,41.108000000000004,29.17 +2020-06-01 22:15:00,76.24,45.008,41.108000000000004,29.17 +2020-06-01 22:30:00,72.97,43.971000000000004,41.108000000000004,29.17 +2020-06-01 22:45:00,68.25,41.247,41.108000000000004,29.17 +2020-06-01 23:00:00,75.51,37.607,33.82,29.17 +2020-06-01 23:15:00,76.08,35.554,33.82,29.17 +2020-06-01 23:30:00,75.77,34.259,33.82,29.17 +2020-06-01 23:45:00,75.39,33.885,33.82,29.17 +2020-06-02 00:00:00,72.23,27.486,44.625,29.28 +2020-06-02 00:15:00,72.85,28.073,44.625,29.28 +2020-06-02 00:30:00,72.81,27.004,44.625,29.28 +2020-06-02 00:45:00,72.9,26.359,44.625,29.28 +2020-06-02 01:00:00,72.66,25.963,41.733000000000004,29.28 +2020-06-02 01:15:00,73.01,25.336,41.733000000000004,29.28 +2020-06-02 01:30:00,71.48,23.773000000000003,41.733000000000004,29.28 +2020-06-02 01:45:00,71.71,23.325,41.733000000000004,29.28 +2020-06-02 02:00:00,71.14,22.816,39.872,29.28 +2020-06-02 02:15:00,72.89,21.364,39.872,29.28 +2020-06-02 02:30:00,72.11,23.55,39.872,29.28 +2020-06-02 02:45:00,72.88,24.247,39.872,29.28 +2020-06-02 03:00:00,72.62,25.929000000000002,38.711,29.28 +2020-06-02 03:15:00,73.57,24.605999999999998,38.711,29.28 +2020-06-02 03:30:00,74.97,23.865,38.711,29.28 +2020-06-02 03:45:00,74.58,23.045,38.711,29.28 +2020-06-02 04:00:00,76.34,30.565,39.823,29.28 +2020-06-02 04:15:00,80.5,39.384,39.823,29.28 +2020-06-02 04:30:00,80.66,36.830999999999996,39.823,29.28 +2020-06-02 04:45:00,85.79,37.228,39.823,29.28 +2020-06-02 05:00:00,99.7,55.825,43.228,29.28 +2020-06-02 05:15:00,105.56,69.34,43.228,29.28 +2020-06-02 05:30:00,108.46,59.775,43.228,29.28 +2020-06-02 05:45:00,108.08,55.667,43.228,29.28 +2020-06-02 06:00:00,115.07,57.045,54.316,29.28 +2020-06-02 06:15:00,119.43,57.893,54.316,29.28 +2020-06-02 06:30:00,122.79,55.489,54.316,29.28 +2020-06-02 06:45:00,121.32,55.802,54.316,29.28 +2020-06-02 07:00:00,119.92,56.843,65.758,29.28 +2020-06-02 07:15:00,127.93,56.339,65.758,29.28 +2020-06-02 07:30:00,129.28,53.638999999999996,65.758,29.28 +2020-06-02 07:45:00,129.07,52.556999999999995,65.758,29.28 +2020-06-02 08:00:00,127.22,49.989,57.983000000000004,29.28 +2020-06-02 08:15:00,131.77,51.858999999999995,57.983000000000004,29.28 +2020-06-02 08:30:00,134.36,51.706,57.983000000000004,29.28 +2020-06-02 08:45:00,133.94,53.798,57.983000000000004,29.28 +2020-06-02 09:00:00,128.31,49.465,52.653,29.28 +2020-06-02 09:15:00,126.41,48.339,52.653,29.28 +2020-06-02 09:30:00,129.84,51.693999999999996,52.653,29.28 +2020-06-02 09:45:00,130.52,53.861000000000004,52.653,29.28 +2020-06-02 10:00:00,129.02,49.056999999999995,51.408,29.28 +2020-06-02 10:15:00,128.01,50.575,51.408,29.28 +2020-06-02 10:30:00,124.54,50.652,51.408,29.28 +2020-06-02 10:45:00,130.5,52.098,51.408,29.28 +2020-06-02 11:00:00,130.58,47.683,51.913000000000004,29.28 +2020-06-02 11:15:00,129.85,48.934,51.913000000000004,29.28 +2020-06-02 11:30:00,124.12,50.481,51.913000000000004,29.28 +2020-06-02 11:45:00,123.31,52.216,51.913000000000004,29.28 +2020-06-02 12:00:00,125.14,48.06100000000001,49.508,29.28 +2020-06-02 12:15:00,122.36,48.11600000000001,49.508,29.28 +2020-06-02 12:30:00,118.37,47.354,49.508,29.28 +2020-06-02 12:45:00,115.76,48.853,49.508,29.28 +2020-06-02 13:00:00,120.83,49.547,50.007,29.28 +2020-06-02 13:15:00,121.9,50.588,50.007,29.28 +2020-06-02 13:30:00,116.48,48.832,50.007,29.28 +2020-06-02 13:45:00,110.09,47.396,50.007,29.28 +2020-06-02 14:00:00,109.79,49.284,49.778999999999996,29.28 +2020-06-02 14:15:00,111.56,47.966,49.778999999999996,29.28 +2020-06-02 14:30:00,112.09,47.093999999999994,49.778999999999996,29.28 +2020-06-02 14:45:00,114.29,47.645,49.778999999999996,29.28 +2020-06-02 15:00:00,110.48,48.917,51.559,29.28 +2020-06-02 15:15:00,103.02,46.593999999999994,51.559,29.28 +2020-06-02 15:30:00,100.93,44.923,51.559,29.28 +2020-06-02 15:45:00,105.33,42.842,51.559,29.28 +2020-06-02 16:00:00,105.0,45.167,53.531000000000006,29.28 +2020-06-02 16:15:00,105.3,44.769,53.531000000000006,29.28 +2020-06-02 16:30:00,106.76,43.912,53.531000000000006,29.28 +2020-06-02 16:45:00,111.6,40.665,53.531000000000006,29.28 +2020-06-02 17:00:00,113.3,43.038999999999994,59.497,29.28 +2020-06-02 17:15:00,109.25,43.193000000000005,59.497,29.28 +2020-06-02 17:30:00,106.26,42.56,59.497,29.28 +2020-06-02 17:45:00,108.2,41.12,59.497,29.28 +2020-06-02 18:00:00,110.8,43.38,59.861999999999995,29.28 +2020-06-02 18:15:00,114.43,43.85,59.861999999999995,29.28 +2020-06-02 18:30:00,111.78,41.553999999999995,59.861999999999995,29.28 +2020-06-02 18:45:00,107.85,45.961000000000006,59.861999999999995,29.28 +2020-06-02 19:00:00,102.8,46.961999999999996,60.989,29.28 +2020-06-02 19:15:00,105.39,46.086000000000006,60.989,29.28 +2020-06-02 19:30:00,98.65,45.317,60.989,29.28 +2020-06-02 19:45:00,98.03,45.648,60.989,29.28 +2020-06-02 20:00:00,95.94,44.276,68.35600000000001,29.28 +2020-06-02 20:15:00,101.74,44.063,68.35600000000001,29.28 +2020-06-02 20:30:00,101.49,44.187,68.35600000000001,29.28 +2020-06-02 20:45:00,102.03,44.443000000000005,68.35600000000001,29.28 +2020-06-02 21:00:00,92.62,42.57,59.251000000000005,29.28 +2020-06-02 21:15:00,92.31,44.255,59.251000000000005,29.28 +2020-06-02 21:30:00,87.58,45.371,59.251000000000005,29.28 +2020-06-02 21:45:00,86.58,46.31399999999999,59.251000000000005,29.28 +2020-06-02 22:00:00,81.88,43.613,54.736999999999995,29.28 +2020-06-02 22:15:00,80.55,45.158,54.736999999999995,29.28 +2020-06-02 22:30:00,78.5,39.465,54.736999999999995,29.28 +2020-06-02 22:45:00,77.43,36.318000000000005,54.736999999999995,29.28 +2020-06-02 23:00:00,74.39,32.014,46.806999999999995,29.28 +2020-06-02 23:15:00,74.81,30.081,46.806999999999995,29.28 +2020-06-02 23:30:00,74.8,28.956999999999997,46.806999999999995,29.28 +2020-06-02 23:45:00,73.41,28.416999999999998,46.806999999999995,29.28 +2020-06-03 00:00:00,70.04,27.325,43.824,29.28 +2020-06-03 00:15:00,71.56,27.914,43.824,29.28 +2020-06-03 00:30:00,68.67,26.846,43.824,29.28 +2020-06-03 00:45:00,70.95,26.205,43.824,29.28 +2020-06-03 01:00:00,71.49,25.829,39.86,29.28 +2020-06-03 01:15:00,71.44,25.180999999999997,39.86,29.28 +2020-06-03 01:30:00,70.05,23.608,39.86,29.28 +2020-06-03 01:45:00,70.5,23.154,39.86,29.28 +2020-06-03 02:00:00,70.07,22.645,37.931999999999995,29.28 +2020-06-03 02:15:00,71.93,21.186999999999998,37.931999999999995,29.28 +2020-06-03 02:30:00,70.28,23.375,37.931999999999995,29.28 +2020-06-03 02:45:00,70.45,24.079,37.931999999999995,29.28 +2020-06-03 03:00:00,71.64,25.763,37.579,29.28 +2020-06-03 03:15:00,72.7,24.436999999999998,37.579,29.28 +2020-06-03 03:30:00,72.09,23.701,37.579,29.28 +2020-06-03 03:45:00,72.31,22.904,37.579,29.28 +2020-06-03 04:00:00,76.85,30.365,37.931999999999995,29.28 +2020-06-03 04:15:00,80.4,39.134,37.931999999999995,29.28 +2020-06-03 04:30:00,88.31,36.568000000000005,37.931999999999995,29.28 +2020-06-03 04:45:00,93.4,36.96,37.931999999999995,29.28 +2020-06-03 05:00:00,94.29,55.43600000000001,40.942,29.28 +2020-06-03 05:15:00,98.54,68.80199999999999,40.942,29.28 +2020-06-03 05:30:00,101.92,59.305,40.942,29.28 +2020-06-03 05:45:00,106.14,55.255,40.942,29.28 +2020-06-03 06:00:00,113.26,56.653,56.516999999999996,29.28 +2020-06-03 06:15:00,112.2,57.481,56.516999999999996,29.28 +2020-06-03 06:30:00,106.99,55.099,56.516999999999996,29.28 +2020-06-03 06:45:00,110.19,55.44,56.516999999999996,29.28 +2020-06-03 07:00:00,113.12,56.468999999999994,71.707,29.28 +2020-06-03 07:15:00,116.27,55.978,71.707,29.28 +2020-06-03 07:30:00,115.88,53.263000000000005,71.707,29.28 +2020-06-03 07:45:00,113.15,52.215,71.707,29.28 +2020-06-03 08:00:00,113.28,49.655,61.17,29.28 +2020-06-03 08:15:00,115.37,51.568000000000005,61.17,29.28 +2020-06-03 08:30:00,116.39,51.406000000000006,61.17,29.28 +2020-06-03 08:45:00,112.02,53.507,61.17,29.28 +2020-06-03 09:00:00,110.21,49.167,57.282,29.28 +2020-06-03 09:15:00,113.3,48.047,57.282,29.28 +2020-06-03 09:30:00,110.77,51.406000000000006,57.282,29.28 +2020-06-03 09:45:00,112.52,53.597,57.282,29.28 +2020-06-03 10:00:00,114.37,48.805,54.026,29.28 +2020-06-03 10:15:00,114.82,50.343,54.026,29.28 +2020-06-03 10:30:00,116.42,50.426,54.026,29.28 +2020-06-03 10:45:00,116.13,51.88,54.026,29.28 +2020-06-03 11:00:00,109.87,47.458,54.277,29.28 +2020-06-03 11:15:00,104.76,48.72,54.277,29.28 +2020-06-03 11:30:00,108.82,50.256,54.277,29.28 +2020-06-03 11:45:00,108.87,51.997,54.277,29.28 +2020-06-03 12:00:00,106.62,47.876000000000005,52.552,29.28 +2020-06-03 12:15:00,102.67,47.937,52.552,29.28 +2020-06-03 12:30:00,98.68,47.147,52.552,29.28 +2020-06-03 12:45:00,105.85,48.65,52.552,29.28 +2020-06-03 13:00:00,107.79,49.343999999999994,52.111999999999995,29.28 +2020-06-03 13:15:00,108.61,50.388000000000005,52.111999999999995,29.28 +2020-06-03 13:30:00,102.26,48.643,52.111999999999995,29.28 +2020-06-03 13:45:00,104.72,47.208,52.111999999999995,29.28 +2020-06-03 14:00:00,111.64,49.123000000000005,52.066,29.28 +2020-06-03 14:15:00,108.26,47.799,52.066,29.28 +2020-06-03 14:30:00,108.8,46.897,52.066,29.28 +2020-06-03 14:45:00,103.39,47.453,52.066,29.28 +2020-06-03 15:00:00,103.63,48.77,52.523999999999994,29.28 +2020-06-03 15:15:00,105.44,46.435,52.523999999999994,29.28 +2020-06-03 15:30:00,104.72,44.748999999999995,52.523999999999994,29.28 +2020-06-03 15:45:00,100.41,42.653999999999996,52.523999999999994,29.28 +2020-06-03 16:00:00,100.42,45.018,54.101000000000006,29.28 +2020-06-03 16:15:00,110.51,44.611000000000004,54.101000000000006,29.28 +2020-06-03 16:30:00,111.07,43.773999999999994,54.101000000000006,29.28 +2020-06-03 16:45:00,109.26,40.488,54.101000000000006,29.28 +2020-06-03 17:00:00,104.98,42.896,58.155,29.28 +2020-06-03 17:15:00,111.12,43.037,58.155,29.28 +2020-06-03 17:30:00,114.79,42.395,58.155,29.28 +2020-06-03 17:45:00,115.69,40.927,58.155,29.28 +2020-06-03 18:00:00,108.75,43.202,60.205,29.28 +2020-06-03 18:15:00,112.12,43.644,60.205,29.28 +2020-06-03 18:30:00,114.87,41.341,60.205,29.28 +2020-06-03 18:45:00,113.03,45.747,60.205,29.28 +2020-06-03 19:00:00,104.74,46.748999999999995,61.568999999999996,29.28 +2020-06-03 19:15:00,99.58,45.865,61.568999999999996,29.28 +2020-06-03 19:30:00,104.58,45.088,61.568999999999996,29.28 +2020-06-03 19:45:00,104.49,45.413999999999994,61.568999999999996,29.28 +2020-06-03 20:00:00,102.46,44.022,68.145,29.28 +2020-06-03 20:15:00,95.88,43.805,68.145,29.28 +2020-06-03 20:30:00,95.33,43.943999999999996,68.145,29.28 +2020-06-03 20:45:00,95.16,44.238,68.145,29.28 +2020-06-03 21:00:00,92.82,42.37,59.696000000000005,29.28 +2020-06-03 21:15:00,92.05,44.066,59.696000000000005,29.28 +2020-06-03 21:30:00,89.64,45.158,59.696000000000005,29.28 +2020-06-03 21:45:00,88.21,46.118,59.696000000000005,29.28 +2020-06-03 22:00:00,83.59,43.44,54.861999999999995,29.28 +2020-06-03 22:15:00,82.97,44.998000000000005,54.861999999999995,29.28 +2020-06-03 22:30:00,81.26,39.305,54.861999999999995,29.28 +2020-06-03 22:45:00,80.33,36.147,54.861999999999995,29.28 +2020-06-03 23:00:00,75.58,31.816999999999997,45.568000000000005,29.28 +2020-06-03 23:15:00,75.33,29.924,45.568000000000005,29.28 +2020-06-03 23:30:00,74.44,28.808000000000003,45.568000000000005,29.28 +2020-06-03 23:45:00,73.46,28.256999999999998,45.568000000000005,29.28 +2020-06-04 00:00:00,73.36,27.168000000000003,40.181,29.28 +2020-06-04 00:15:00,73.36,27.758000000000003,40.181,29.28 +2020-06-04 00:30:00,72.26,26.691999999999997,40.181,29.28 +2020-06-04 00:45:00,74.73,26.054000000000002,40.181,29.28 +2020-06-04 01:00:00,71.88,25.698,38.296,29.28 +2020-06-04 01:15:00,72.5,25.03,38.296,29.28 +2020-06-04 01:30:00,71.31,23.448,38.296,29.28 +2020-06-04 01:45:00,71.5,22.986,38.296,29.28 +2020-06-04 02:00:00,71.83,22.479,36.575,29.28 +2020-06-04 02:15:00,71.45,21.015,36.575,29.28 +2020-06-04 02:30:00,71.1,23.204,36.575,29.28 +2020-06-04 02:45:00,71.42,23.915,36.575,29.28 +2020-06-04 03:00:00,73.16,25.601,36.394,29.28 +2020-06-04 03:15:00,72.99,24.271,36.394,29.28 +2020-06-04 03:30:00,71.02,23.54,36.394,29.28 +2020-06-04 03:45:00,73.01,22.767,36.394,29.28 +2020-06-04 04:00:00,77.6,30.171,37.207,29.28 +2020-06-04 04:15:00,77.75,38.889,37.207,29.28 +2020-06-04 04:30:00,88.08,36.309,37.207,29.28 +2020-06-04 04:45:00,93.23,36.698,37.207,29.28 +2020-06-04 05:00:00,94.94,55.053999999999995,40.713,29.28 +2020-06-04 05:15:00,96.67,68.271,40.713,29.28 +2020-06-04 05:30:00,104.59,58.843999999999994,40.713,29.28 +2020-06-04 05:45:00,107.94,54.852,40.713,29.28 +2020-06-04 06:00:00,112.46,56.268,50.952,29.28 +2020-06-04 06:15:00,109.5,57.075,50.952,29.28 +2020-06-04 06:30:00,108.67,54.715,50.952,29.28 +2020-06-04 06:45:00,109.33,55.085,50.952,29.28 +2020-06-04 07:00:00,110.44,56.102,64.88,29.28 +2020-06-04 07:15:00,109.83,55.625,64.88,29.28 +2020-06-04 07:30:00,113.96,52.897,64.88,29.28 +2020-06-04 07:45:00,116.37,51.881,64.88,29.28 +2020-06-04 08:00:00,114.22,49.33,55.133,29.28 +2020-06-04 08:15:00,107.51,51.283,55.133,29.28 +2020-06-04 08:30:00,112.36,51.113,55.133,29.28 +2020-06-04 08:45:00,109.8,53.223,55.133,29.28 +2020-06-04 09:00:00,114.08,48.876000000000005,48.912,29.28 +2020-06-04 09:15:00,119.39,47.761,48.912,29.28 +2020-06-04 09:30:00,116.25,51.125,48.912,29.28 +2020-06-04 09:45:00,110.92,53.34,48.912,29.28 +2020-06-04 10:00:00,107.25,48.559,45.968999999999994,29.28 +2020-06-04 10:15:00,107.81,50.117,45.968999999999994,29.28 +2020-06-04 10:30:00,113.72,50.20399999999999,45.968999999999994,29.28 +2020-06-04 10:45:00,116.8,51.668,45.968999999999994,29.28 +2020-06-04 11:00:00,117.17,47.239,44.067,29.28 +2020-06-04 11:15:00,109.05,48.511,44.067,29.28 +2020-06-04 11:30:00,110.66,50.037,44.067,29.28 +2020-06-04 11:45:00,105.85,51.783,44.067,29.28 +2020-06-04 12:00:00,111.23,47.696999999999996,41.501000000000005,29.28 +2020-06-04 12:15:00,113.61,47.762,41.501000000000005,29.28 +2020-06-04 12:30:00,110.92,46.945,41.501000000000005,29.28 +2020-06-04 12:45:00,102.91,48.453,41.501000000000005,29.28 +2020-06-04 13:00:00,102.85,49.145,41.117,29.28 +2020-06-04 13:15:00,107.65,50.192,41.117,29.28 +2020-06-04 13:30:00,107.87,48.458,41.117,29.28 +2020-06-04 13:45:00,112.9,47.026,41.117,29.28 +2020-06-04 14:00:00,99.93,48.964,41.492,29.28 +2020-06-04 14:15:00,105.4,47.635,41.492,29.28 +2020-06-04 14:30:00,103.85,46.70399999999999,41.492,29.28 +2020-06-04 14:45:00,104.92,47.266000000000005,41.492,29.28 +2020-06-04 15:00:00,109.26,48.626000000000005,43.711999999999996,29.28 +2020-06-04 15:15:00,110.94,46.278,43.711999999999996,29.28 +2020-06-04 15:30:00,109.79,44.57899999999999,43.711999999999996,29.28 +2020-06-04 15:45:00,112.69,42.47,43.711999999999996,29.28 +2020-06-04 16:00:00,114.07,44.871,45.446000000000005,29.28 +2020-06-04 16:15:00,109.19,44.458,45.446000000000005,29.28 +2020-06-04 16:30:00,114.59,43.638999999999996,45.446000000000005,29.28 +2020-06-04 16:45:00,116.07,40.316,45.446000000000005,29.28 +2020-06-04 17:00:00,115.34,42.75899999999999,48.803000000000004,29.28 +2020-06-04 17:15:00,110.62,42.886,48.803000000000004,29.28 +2020-06-04 17:30:00,108.75,42.233999999999995,48.803000000000004,29.28 +2020-06-04 17:45:00,116.09,40.74,48.803000000000004,29.28 +2020-06-04 18:00:00,113.07,43.028,51.167,29.28 +2020-06-04 18:15:00,111.11,43.445,51.167,29.28 +2020-06-04 18:30:00,108.92,41.133,51.167,29.28 +2020-06-04 18:45:00,115.29,45.537,51.167,29.28 +2020-06-04 19:00:00,113.15,46.541000000000004,52.486000000000004,29.28 +2020-06-04 19:15:00,104.97,45.648999999999994,52.486000000000004,29.28 +2020-06-04 19:30:00,105.27,44.864,52.486000000000004,29.28 +2020-06-04 19:45:00,105.77,45.185,52.486000000000004,29.28 +2020-06-04 20:00:00,104.54,43.773999999999994,59.635,29.28 +2020-06-04 20:15:00,99.31,43.553999999999995,59.635,29.28 +2020-06-04 20:30:00,96.07,43.706,59.635,29.28 +2020-06-04 20:45:00,96.72,44.038000000000004,59.635,29.28 +2020-06-04 21:00:00,93.51,42.174,54.353,29.28 +2020-06-04 21:15:00,91.29,43.881,54.353,29.28 +2020-06-04 21:30:00,89.16,44.949,54.353,29.28 +2020-06-04 21:45:00,87.45,45.925,54.353,29.28 +2020-06-04 22:00:00,83.88,43.271,49.431999999999995,29.28 +2020-06-04 22:15:00,83.67,44.842,49.431999999999995,29.28 +2020-06-04 22:30:00,80.79,39.148,49.431999999999995,29.28 +2020-06-04 22:45:00,82.95,35.979,49.431999999999995,29.28 +2020-06-04 23:00:00,77.09,31.623,42.872,29.28 +2020-06-04 23:15:00,76.4,29.77,42.872,29.28 +2020-06-04 23:30:00,76.03,28.66,42.872,29.28 +2020-06-04 23:45:00,75.13,28.1,42.872,29.28 +2020-06-05 00:00:00,72.68,25.186999999999998,39.819,29.28 +2020-06-05 00:15:00,74.39,26.006,39.819,29.28 +2020-06-05 00:30:00,73.39,25.189,39.819,29.28 +2020-06-05 00:45:00,72.85,24.973000000000003,39.819,29.28 +2020-06-05 01:00:00,73.16,24.238000000000003,37.797,29.28 +2020-06-05 01:15:00,73.41,23.063000000000002,37.797,29.28 +2020-06-05 01:30:00,71.51,22.136999999999997,37.797,29.28 +2020-06-05 01:45:00,72.07,21.439,37.797,29.28 +2020-06-05 02:00:00,71.82,21.836,36.905,29.28 +2020-06-05 02:15:00,72.76,20.305,36.905,29.28 +2020-06-05 02:30:00,72.71,23.339000000000002,36.905,29.28 +2020-06-05 02:45:00,73.05,23.405,36.905,29.28 +2020-06-05 03:00:00,73.18,25.739,37.1,29.28 +2020-06-05 03:15:00,74.33,23.340999999999998,37.1,29.28 +2020-06-05 03:30:00,73.58,22.383000000000003,37.1,29.28 +2020-06-05 03:45:00,74.46,22.509,37.1,29.28 +2020-06-05 04:00:00,78.21,30.009,37.882,29.28 +2020-06-05 04:15:00,84.39,37.158,37.882,29.28 +2020-06-05 04:30:00,85.9,35.499,37.882,29.28 +2020-06-05 04:45:00,92.12,35.150999999999996,37.882,29.28 +2020-06-05 05:00:00,94.15,52.801,40.777,29.28 +2020-06-05 05:15:00,96.53,66.937,40.777,29.28 +2020-06-05 05:30:00,98.78,57.873000000000005,40.777,29.28 +2020-06-05 05:45:00,105.4,53.503,40.777,29.28 +2020-06-05 06:00:00,110.45,55.2,55.528,29.28 +2020-06-05 06:15:00,115.64,55.998999999999995,55.528,29.28 +2020-06-05 06:30:00,113.37,53.525,55.528,29.28 +2020-06-05 06:45:00,113.78,53.961000000000006,55.528,29.28 +2020-06-05 07:00:00,117.75,55.501999999999995,67.749,29.28 +2020-06-05 07:15:00,116.94,56.013000000000005,67.749,29.28 +2020-06-05 07:30:00,113.93,51.391999999999996,67.749,29.28 +2020-06-05 07:45:00,110.35,50.114,67.749,29.28 +2020-06-05 08:00:00,109.2,48.199,57.55,29.28 +2020-06-05 08:15:00,110.52,50.788000000000004,57.55,29.28 +2020-06-05 08:30:00,122.31,50.622,57.55,29.28 +2020-06-05 08:45:00,120.12,52.435,57.55,29.28 +2020-06-05 09:00:00,121.23,45.938,52.588,29.28 +2020-06-05 09:15:00,117.3,46.71,52.588,29.28 +2020-06-05 09:30:00,116.81,49.378,52.588,29.28 +2020-06-05 09:45:00,121.98,51.972,52.588,29.28 +2020-06-05 10:00:00,119.87,46.911,49.772,29.28 +2020-06-05 10:15:00,116.15,48.356,49.772,29.28 +2020-06-05 10:30:00,115.85,48.961999999999996,49.772,29.28 +2020-06-05 10:45:00,117.93,50.29600000000001,49.772,29.28 +2020-06-05 11:00:00,114.69,46.1,49.226000000000006,29.28 +2020-06-05 11:15:00,113.17,46.253,49.226000000000006,29.28 +2020-06-05 11:30:00,105.0,47.621,49.226000000000006,29.28 +2020-06-05 11:45:00,103.88,48.471000000000004,49.226000000000006,29.28 +2020-06-05 12:00:00,100.79,44.951,45.705,29.28 +2020-06-05 12:15:00,102.92,44.177,45.705,29.28 +2020-06-05 12:30:00,99.51,43.463,45.705,29.28 +2020-06-05 12:45:00,100.37,44.29,45.705,29.28 +2020-06-05 13:00:00,99.89,45.646,43.133,29.28 +2020-06-05 13:15:00,99.79,46.958,43.133,29.28 +2020-06-05 13:30:00,100.88,45.986999999999995,43.133,29.28 +2020-06-05 13:45:00,99.87,44.84,43.133,29.28 +2020-06-05 14:00:00,99.86,45.912,41.989,29.28 +2020-06-05 14:15:00,97.26,44.972,41.989,29.28 +2020-06-05 14:30:00,96.34,45.495,41.989,29.28 +2020-06-05 14:45:00,94.99,45.443999999999996,41.989,29.28 +2020-06-05 15:00:00,95.46,46.718,43.728,29.28 +2020-06-05 15:15:00,98.1,44.083999999999996,43.728,29.28 +2020-06-05 15:30:00,95.79,41.667,43.728,29.28 +2020-06-05 15:45:00,95.92,40.283,43.728,29.28 +2020-06-05 16:00:00,97.42,41.795,45.93899999999999,29.28 +2020-06-05 16:15:00,100.06,41.875,45.93899999999999,29.28 +2020-06-05 16:30:00,99.13,40.913000000000004,45.93899999999999,29.28 +2020-06-05 16:45:00,101.84,36.795,45.93899999999999,29.28 +2020-06-05 17:00:00,103.81,40.939,50.488,29.28 +2020-06-05 17:15:00,103.47,40.846,50.488,29.28 +2020-06-05 17:30:00,103.83,40.308,50.488,29.28 +2020-06-05 17:45:00,104.89,38.606,50.488,29.28 +2020-06-05 18:00:00,103.73,41.031000000000006,52.408,29.28 +2020-06-05 18:15:00,103.19,40.478,52.408,29.28 +2020-06-05 18:30:00,102.75,38.1,52.408,29.28 +2020-06-05 18:45:00,103.29,42.903999999999996,52.408,29.28 +2020-06-05 19:00:00,98.83,44.846000000000004,52.736000000000004,29.28 +2020-06-05 19:15:00,95.61,44.645,52.736000000000004,29.28 +2020-06-05 19:30:00,93.94,43.871,52.736000000000004,29.28 +2020-06-05 19:45:00,93.79,43.167,52.736000000000004,29.28 +2020-06-05 20:00:00,92.15,41.589,59.68,29.28 +2020-06-05 20:15:00,92.31,42.13,59.68,29.28 +2020-06-05 20:30:00,91.82,41.824,59.68,29.28 +2020-06-05 20:45:00,92.41,41.468999999999994,59.68,29.28 +2020-06-05 21:00:00,89.29,40.918,54.343999999999994,29.28 +2020-06-05 21:15:00,87.29,44.281000000000006,54.343999999999994,29.28 +2020-06-05 21:30:00,84.11,45.185,54.343999999999994,29.28 +2020-06-05 21:45:00,83.05,46.424,54.343999999999994,29.28 +2020-06-05 22:00:00,79.44,43.754,49.672,29.28 +2020-06-05 22:15:00,78.6,45.085,49.672,29.28 +2020-06-05 22:30:00,77.06,44.566,49.672,29.28 +2020-06-05 22:45:00,75.8,42.548,49.672,29.28 +2020-06-05 23:00:00,72.07,39.743,42.065,29.28 +2020-06-05 23:15:00,72.18,36.24,42.065,29.28 +2020-06-05 23:30:00,71.31,33.249,42.065,29.28 +2020-06-05 23:45:00,70.36,32.497,42.065,29.28 +2020-06-06 00:00:00,67.51,25.941999999999997,38.829,29.17 +2020-06-06 00:15:00,67.86,25.7,38.829,29.17 +2020-06-06 00:30:00,67.41,24.565,38.829,29.17 +2020-06-06 00:45:00,67.72,23.725,38.829,29.17 +2020-06-06 01:00:00,66.82,23.335,34.63,29.17 +2020-06-06 01:15:00,67.25,22.605999999999998,34.63,29.17 +2020-06-06 01:30:00,65.85,20.85,34.63,29.17 +2020-06-06 01:45:00,66.09,21.315,34.63,29.17 +2020-06-06 02:00:00,64.68,20.839000000000002,32.465,29.17 +2020-06-06 02:15:00,64.57,18.511,32.465,29.17 +2020-06-06 02:30:00,64.68,20.68,32.465,29.17 +2020-06-06 02:45:00,64.25,21.514,32.465,29.17 +2020-06-06 03:00:00,64.14,22.599,31.925,29.17 +2020-06-06 03:15:00,65.0,19.414,31.925,29.17 +2020-06-06 03:30:00,65.09,18.62,31.925,29.17 +2020-06-06 03:45:00,67.6,20.195,31.925,29.17 +2020-06-06 04:00:00,70.16,25.434,31.309,29.17 +2020-06-06 04:15:00,70.62,31.435,31.309,29.17 +2020-06-06 04:30:00,63.11,27.958000000000002,31.309,29.17 +2020-06-06 04:45:00,65.22,27.819000000000003,31.309,29.17 +2020-06-06 05:00:00,67.91,36.066,30.323,29.17 +2020-06-06 05:15:00,67.72,37.842,30.323,29.17 +2020-06-06 05:30:00,69.31,30.338,30.323,29.17 +2020-06-06 05:45:00,71.04,30.802,30.323,29.17 +2020-06-06 06:00:00,75.65,44.887,31.438000000000002,29.17 +2020-06-06 06:15:00,80.98,54.621,31.438000000000002,29.17 +2020-06-06 06:30:00,84.8,48.958999999999996,31.438000000000002,29.17 +2020-06-06 06:45:00,83.47,45.533,31.438000000000002,29.17 +2020-06-06 07:00:00,78.85,45.275,34.891999999999996,29.17 +2020-06-06 07:15:00,78.38,44.446999999999996,34.891999999999996,29.17 +2020-06-06 07:30:00,79.02,41.507,34.891999999999996,29.17 +2020-06-06 07:45:00,80.34,41.519,34.891999999999996,29.17 +2020-06-06 08:00:00,81.61,40.611999999999995,39.608000000000004,29.17 +2020-06-06 08:15:00,82.54,43.39,39.608000000000004,29.17 +2020-06-06 08:30:00,81.42,43.358000000000004,39.608000000000004,29.17 +2020-06-06 08:45:00,81.09,46.385,39.608000000000004,29.17 +2020-06-06 09:00:00,79.87,42.973,40.894,29.17 +2020-06-06 09:15:00,87.56,44.285,40.894,29.17 +2020-06-06 09:30:00,87.68,47.515,40.894,29.17 +2020-06-06 09:45:00,83.66,49.722,40.894,29.17 +2020-06-06 10:00:00,80.88,45.253,39.525,29.17 +2020-06-06 10:15:00,87.29,47.07,39.525,29.17 +2020-06-06 10:30:00,83.73,47.373999999999995,39.525,29.17 +2020-06-06 10:45:00,83.72,48.521,39.525,29.17 +2020-06-06 11:00:00,84.12,44.211000000000006,36.718,29.17 +2020-06-06 11:15:00,90.83,45.122,36.718,29.17 +2020-06-06 11:30:00,94.67,46.63,36.718,29.17 +2020-06-06 11:45:00,94.56,48.016999999999996,36.718,29.17 +2020-06-06 12:00:00,91.99,44.79600000000001,35.688,29.17 +2020-06-06 12:15:00,90.46,44.895,35.688,29.17 +2020-06-06 12:30:00,88.8,44.06399999999999,35.688,29.17 +2020-06-06 12:45:00,87.54,45.498999999999995,35.688,29.17 +2020-06-06 13:00:00,85.74,46.00899999999999,32.858000000000004,29.17 +2020-06-06 13:15:00,87.07,46.6,32.858000000000004,29.17 +2020-06-06 13:30:00,87.33,45.77,32.858000000000004,29.17 +2020-06-06 13:45:00,86.83,43.407,32.858000000000004,29.17 +2020-06-06 14:00:00,86.15,44.693999999999996,31.738000000000003,29.17 +2020-06-06 14:15:00,84.56,42.516000000000005,31.738000000000003,29.17 +2020-06-06 14:30:00,81.7,42.419,31.738000000000003,29.17 +2020-06-06 14:45:00,80.31,42.846000000000004,31.738000000000003,29.17 +2020-06-06 15:00:00,80.41,44.662,34.35,29.17 +2020-06-06 15:15:00,79.49,42.766000000000005,34.35,29.17 +2020-06-06 15:30:00,79.47,40.687,34.35,29.17 +2020-06-06 15:45:00,79.76,38.425,34.35,29.17 +2020-06-06 16:00:00,80.07,41.907,37.522,29.17 +2020-06-06 16:15:00,79.03,41.225,37.522,29.17 +2020-06-06 16:30:00,80.08,40.488,37.522,29.17 +2020-06-06 16:45:00,81.23,36.4,37.522,29.17 +2020-06-06 17:00:00,82.36,39.404,42.498000000000005,29.17 +2020-06-06 17:15:00,81.67,37.425,42.498000000000005,29.17 +2020-06-06 17:30:00,81.82,36.743,42.498000000000005,29.17 +2020-06-06 17:45:00,84.04,35.443000000000005,42.498000000000005,29.17 +2020-06-06 18:00:00,84.14,39.175,44.701,29.17 +2020-06-06 18:15:00,83.83,40.357,44.701,29.17 +2020-06-06 18:30:00,84.61,39.39,44.701,29.17 +2020-06-06 18:45:00,84.06,40.567,44.701,29.17 +2020-06-06 19:00:00,81.29,41.091,45.727,29.17 +2020-06-06 19:15:00,78.62,39.875,45.727,29.17 +2020-06-06 19:30:00,76.78,39.896,45.727,29.17 +2020-06-06 19:45:00,75.72,40.85,45.727,29.17 +2020-06-06 20:00:00,74.89,40.242,43.391000000000005,29.17 +2020-06-06 20:15:00,74.17,40.381,43.391000000000005,29.17 +2020-06-06 20:30:00,74.7,39.22,43.391000000000005,29.17 +2020-06-06 20:45:00,74.33,40.624,43.391000000000005,29.17 +2020-06-06 21:00:00,71.59,38.898,41.231,29.17 +2020-06-06 21:15:00,69.93,41.982,41.231,29.17 +2020-06-06 21:30:00,68.91,43.161,41.231,29.17 +2020-06-06 21:45:00,68.03,43.858999999999995,41.231,29.17 +2020-06-06 22:00:00,64.54,41.316,40.798,29.17 +2020-06-06 22:15:00,64.33,43.133,40.798,29.17 +2020-06-06 22:30:00,62.24,42.849,40.798,29.17 +2020-06-06 22:45:00,62.62,41.352,40.798,29.17 +2020-06-06 23:00:00,58.66,38.109,34.402,29.17 +2020-06-06 23:15:00,58.11,34.889,34.402,29.17 +2020-06-06 23:30:00,56.97,34.075,34.402,29.17 +2020-06-06 23:45:00,56.18,33.421,34.402,29.17 +2020-06-07 00:00:00,54.78,27.147,30.171,29.17 +2020-06-07 00:15:00,55.23,25.78,30.171,29.17 +2020-06-07 00:30:00,54.07,24.471999999999998,30.171,29.17 +2020-06-07 00:45:00,54.14,23.596999999999998,30.171,29.17 +2020-06-07 01:00:00,52.67,23.464000000000002,27.15,29.17 +2020-06-07 01:15:00,52.85,22.699,27.15,29.17 +2020-06-07 01:30:00,52.59,20.854,27.15,29.17 +2020-06-07 01:45:00,52.21,20.92,27.15,29.17 +2020-06-07 02:00:00,50.98,20.448,25.403000000000002,29.17 +2020-06-07 02:15:00,51.38,18.707,25.403000000000002,29.17 +2020-06-07 02:30:00,50.88,21.136,25.403000000000002,29.17 +2020-06-07 02:45:00,51.13,21.745,25.403000000000002,29.17 +2020-06-07 03:00:00,50.84,23.48,23.386999999999997,29.17 +2020-06-07 03:15:00,51.71,20.49,23.386999999999997,29.17 +2020-06-07 03:30:00,52.75,19.136,23.386999999999997,29.17 +2020-06-07 03:45:00,50.92,19.973,23.386999999999997,29.17 +2020-06-07 04:00:00,50.55,25.101,23.941999999999997,29.17 +2020-06-07 04:15:00,51.55,30.546,23.941999999999997,29.17 +2020-06-07 04:30:00,51.6,28.333000000000002,23.941999999999997,29.17 +2020-06-07 04:45:00,52.09,27.774,23.941999999999997,29.17 +2020-06-07 05:00:00,53.72,35.75,23.026,29.17 +2020-06-07 05:15:00,54.12,36.433,23.026,29.17 +2020-06-07 05:30:00,55.59,28.6,23.026,29.17 +2020-06-07 05:45:00,57.85,28.881,23.026,29.17 +2020-06-07 06:00:00,59.26,40.721,23.223000000000003,29.17 +2020-06-07 06:15:00,60.63,51.071999999999996,23.223000000000003,29.17 +2020-06-07 06:30:00,62.83,44.669,23.223000000000003,29.17 +2020-06-07 06:45:00,64.38,40.23,23.223000000000003,29.17 +2020-06-07 07:00:00,65.6,40.458,24.968000000000004,29.17 +2020-06-07 07:15:00,66.44,38.01,24.968000000000004,29.17 +2020-06-07 07:30:00,68.26,36.156,24.968000000000004,29.17 +2020-06-07 07:45:00,68.77,36.098,24.968000000000004,29.17 +2020-06-07 08:00:00,69.95,36.086,29.131,29.17 +2020-06-07 08:15:00,69.53,40.012,29.131,29.17 +2020-06-07 08:30:00,69.54,40.953,29.131,29.17 +2020-06-07 08:45:00,69.78,44.098,29.131,29.17 +2020-06-07 09:00:00,70.2,40.535,29.904,29.17 +2020-06-07 09:15:00,72.11,41.438,29.904,29.17 +2020-06-07 09:30:00,74.06,45.06399999999999,29.904,29.17 +2020-06-07 09:45:00,74.87,48.231,29.904,29.17 +2020-06-07 10:00:00,77.19,44.516000000000005,28.943,29.17 +2020-06-07 10:15:00,78.39,46.53,28.943,29.17 +2020-06-07 10:30:00,77.62,47.106,28.943,29.17 +2020-06-07 10:45:00,75.28,49.04,28.943,29.17 +2020-06-07 11:00:00,71.31,44.483999999999995,31.682,29.17 +2020-06-07 11:15:00,70.31,44.994,31.682,29.17 +2020-06-07 11:30:00,70.0,46.903,31.682,29.17 +2020-06-07 11:45:00,67.85,48.593,31.682,29.17 +2020-06-07 12:00:00,64.35,46.353,27.315,29.17 +2020-06-07 12:15:00,61.2,46.038000000000004,27.315,29.17 +2020-06-07 12:30:00,57.59,45.248999999999995,27.315,29.17 +2020-06-07 12:45:00,59.18,45.997,27.315,29.17 +2020-06-07 13:00:00,59.01,46.153,23.894000000000002,29.17 +2020-06-07 13:15:00,55.2,46.481,23.894000000000002,29.17 +2020-06-07 13:30:00,57.81,44.589,23.894000000000002,29.17 +2020-06-07 13:45:00,60.26,43.223,23.894000000000002,29.17 +2020-06-07 14:00:00,57.88,45.69,21.148000000000003,29.17 +2020-06-07 14:15:00,64.49,44.026,21.148000000000003,29.17 +2020-06-07 14:30:00,66.8,42.846000000000004,21.148000000000003,29.17 +2020-06-07 14:45:00,67.65,42.228,21.148000000000003,29.17 +2020-06-07 15:00:00,67.87,44.083,21.229,29.17 +2020-06-07 15:15:00,65.72,41.477,21.229,29.17 +2020-06-07 15:30:00,59.62,39.268,21.229,29.17 +2020-06-07 15:45:00,60.76,37.325,21.229,29.17 +2020-06-07 16:00:00,62.45,39.453,25.037,29.17 +2020-06-07 16:15:00,63.37,38.900999999999996,25.037,29.17 +2020-06-07 16:30:00,66.59,39.158,25.037,29.17 +2020-06-07 16:45:00,70.11,35.108000000000004,25.037,29.17 +2020-06-07 17:00:00,72.96,38.486,37.11,29.17 +2020-06-07 17:15:00,76.34,37.903,37.11,29.17 +2020-06-07 17:30:00,79.24,38.003,37.11,29.17 +2020-06-07 17:45:00,81.88,37.28,37.11,29.17 +2020-06-07 18:00:00,83.72,41.528999999999996,42.215,29.17 +2020-06-07 18:15:00,79.56,42.415,42.215,29.17 +2020-06-07 18:30:00,76.0,41.022,42.215,29.17 +2020-06-07 18:45:00,78.65,42.47,42.215,29.17 +2020-06-07 19:00:00,79.38,45.026,44.383,29.17 +2020-06-07 19:15:00,79.12,42.795,44.383,29.17 +2020-06-07 19:30:00,79.25,42.556999999999995,44.383,29.17 +2020-06-07 19:45:00,78.61,43.195,44.383,29.17 +2020-06-07 20:00:00,79.46,42.74,43.426,29.17 +2020-06-07 20:15:00,79.03,42.831,43.426,29.17 +2020-06-07 20:30:00,79.41,42.56100000000001,43.426,29.17 +2020-06-07 20:45:00,81.82,42.298,43.426,29.17 +2020-06-07 21:00:00,81.38,40.224000000000004,42.265,29.17 +2020-06-07 21:15:00,80.43,42.977,42.265,29.17 +2020-06-07 21:30:00,78.77,43.54,42.265,29.17 +2020-06-07 21:45:00,77.29,44.605,42.265,29.17 +2020-06-07 22:00:00,73.8,43.917,42.26,29.17 +2020-06-07 22:15:00,72.46,44.08,42.26,29.17 +2020-06-07 22:30:00,70.72,43.038999999999994,42.26,29.17 +2020-06-07 22:45:00,70.73,40.247,42.26,29.17 +2020-06-07 23:00:00,68.68,36.454,36.609,29.17 +2020-06-07 23:15:00,69.16,34.64,36.609,29.17 +2020-06-07 23:30:00,67.02,33.389,36.609,29.17 +2020-06-07 23:45:00,67.56,32.952,36.609,29.17 +2020-06-08 00:00:00,65.18,28.81,34.611,29.28 +2020-06-08 00:15:00,65.79,28.454,34.611,29.28 +2020-06-08 00:30:00,66.1,26.794,34.611,29.28 +2020-06-08 00:45:00,66.13,25.502,34.611,29.28 +2020-06-08 01:00:00,64.79,25.755,33.552,29.28 +2020-06-08 01:15:00,65.06,24.938000000000002,33.552,29.28 +2020-06-08 01:30:00,65.24,23.433000000000003,33.552,29.28 +2020-06-08 01:45:00,65.35,23.410999999999998,33.552,29.28 +2020-06-08 02:00:00,65.58,23.37,32.351,29.28 +2020-06-08 02:15:00,67.8,20.785999999999998,32.351,29.28 +2020-06-08 02:30:00,73.54,23.4,32.351,29.28 +2020-06-08 02:45:00,74.8,23.823,32.351,29.28 +2020-06-08 03:00:00,71.75,26.151999999999997,30.793000000000003,29.28 +2020-06-08 03:15:00,68.21,23.986,30.793000000000003,29.28 +2020-06-08 03:30:00,68.19,23.261999999999997,30.793000000000003,29.28 +2020-06-08 03:45:00,75.1,23.634,30.793000000000003,29.28 +2020-06-08 04:00:00,76.21,32.04,31.274,29.28 +2020-06-08 04:15:00,81.8,40.582,31.274,29.28 +2020-06-08 04:30:00,81.13,38.092,31.274,29.28 +2020-06-08 04:45:00,84.34,37.895,31.274,29.28 +2020-06-08 05:00:00,91.59,53.857,37.75,29.28 +2020-06-08 05:15:00,94.96,65.925,37.75,29.28 +2020-06-08 05:30:00,96.35,56.716,37.75,29.28 +2020-06-08 05:45:00,103.22,53.645,37.75,29.28 +2020-06-08 06:00:00,115.44,54.214,55.36,29.28 +2020-06-08 06:15:00,117.66,54.621,55.36,29.28 +2020-06-08 06:30:00,119.09,52.708999999999996,55.36,29.28 +2020-06-08 06:45:00,118.2,54.078,55.36,29.28 +2020-06-08 07:00:00,122.59,54.916000000000004,65.87,29.28 +2020-06-08 07:15:00,126.53,54.75899999999999,65.87,29.28 +2020-06-08 07:30:00,126.58,52.008,65.87,29.28 +2020-06-08 07:45:00,117.45,52.041000000000004,65.87,29.28 +2020-06-08 08:00:00,116.82,49.583,55.695,29.28 +2020-06-08 08:15:00,124.15,52.214,55.695,29.28 +2020-06-08 08:30:00,127.09,51.86600000000001,55.695,29.28 +2020-06-08 08:45:00,129.96,54.918,55.695,29.28 +2020-06-08 09:00:00,127.9,50.291000000000004,50.881,29.28 +2020-06-08 09:15:00,127.73,49.188,50.881,29.28 +2020-06-08 09:30:00,129.88,51.846000000000004,50.881,29.28 +2020-06-08 09:45:00,128.32,52.891000000000005,50.881,29.28 +2020-06-08 10:00:00,125.16,49.513999999999996,49.138000000000005,29.28 +2020-06-08 10:15:00,119.84,51.376000000000005,49.138000000000005,29.28 +2020-06-08 10:30:00,116.84,51.43899999999999,49.138000000000005,29.28 +2020-06-08 10:45:00,124.11,51.953,49.138000000000005,29.28 +2020-06-08 11:00:00,126.44,47.358000000000004,49.178000000000004,29.28 +2020-06-08 11:15:00,121.8,48.31100000000001,49.178000000000004,29.28 +2020-06-08 11:30:00,117.6,50.997,49.178000000000004,29.28 +2020-06-08 11:45:00,113.89,53.128,49.178000000000004,29.28 +2020-06-08 12:00:00,106.1,49.479,47.698,29.28 +2020-06-08 12:15:00,110.89,49.273999999999994,47.698,29.28 +2020-06-08 12:30:00,105.06,47.468999999999994,47.698,29.28 +2020-06-08 12:45:00,103.24,48.36,47.698,29.28 +2020-06-08 13:00:00,104.28,49.425,48.104,29.28 +2020-06-08 13:15:00,105.35,48.79,48.104,29.28 +2020-06-08 13:30:00,102.99,47.021,48.104,29.28 +2020-06-08 13:45:00,106.17,46.507,48.104,29.28 +2020-06-08 14:00:00,104.77,48.076,48.53,29.28 +2020-06-08 14:15:00,99.17,46.91,48.53,29.28 +2020-06-08 14:30:00,100.05,45.497,48.53,29.28 +2020-06-08 14:45:00,101.1,46.841,48.53,29.28 +2020-06-08 15:00:00,100.5,48.551,49.351000000000006,29.28 +2020-06-08 15:15:00,97.86,45.243,49.351000000000006,29.28 +2020-06-08 15:30:00,98.11,43.674,49.351000000000006,29.28 +2020-06-08 15:45:00,102.17,41.217,49.351000000000006,29.28 +2020-06-08 16:00:00,104.3,44.405,51.44,29.28 +2020-06-08 16:15:00,106.98,43.84,51.44,29.28 +2020-06-08 16:30:00,105.78,43.396,51.44,29.28 +2020-06-08 16:45:00,107.49,39.226,51.44,29.28 +2020-06-08 17:00:00,108.2,41.544,56.868,29.28 +2020-06-08 17:15:00,108.56,41.22,56.868,29.28 +2020-06-08 17:30:00,108.83,40.895,56.868,29.28 +2020-06-08 17:45:00,109.18,39.635,56.868,29.28 +2020-06-08 18:00:00,108.41,42.896,57.229,29.28 +2020-06-08 18:15:00,107.9,41.818999999999996,57.229,29.28 +2020-06-08 18:30:00,107.3,39.758,57.229,29.28 +2020-06-08 18:45:00,105.82,44.278,57.229,29.28 +2020-06-08 19:00:00,103.74,46.408,57.744,29.28 +2020-06-08 19:15:00,100.22,45.333999999999996,57.744,29.28 +2020-06-08 19:30:00,98.33,44.78,57.744,29.28 +2020-06-08 19:45:00,97.62,44.755,57.744,29.28 +2020-06-08 20:00:00,95.68,42.876000000000005,66.05199999999999,29.28 +2020-06-08 20:15:00,95.32,44.086999999999996,66.05199999999999,29.28 +2020-06-08 20:30:00,96.73,44.168,66.05199999999999,29.28 +2020-06-08 20:45:00,96.53,44.285,66.05199999999999,29.28 +2020-06-08 21:00:00,91.82,41.66,59.396,29.28 +2020-06-08 21:15:00,91.66,44.745,59.396,29.28 +2020-06-08 21:30:00,89.92,45.61,59.396,29.28 +2020-06-08 21:45:00,88.61,46.43600000000001,59.396,29.28 +2020-06-08 22:00:00,83.94,43.571000000000005,53.06,29.28 +2020-06-08 22:15:00,83.74,45.533,53.06,29.28 +2020-06-08 22:30:00,81.28,39.543,53.06,29.28 +2020-06-08 22:45:00,80.11,36.312,53.06,29.28 +2020-06-08 23:00:00,74.81,32.603,46.148,29.28 +2020-06-08 23:15:00,76.9,29.473000000000003,46.148,29.28 +2020-06-08 23:30:00,73.41,28.424,46.148,29.28 +2020-06-08 23:45:00,75.06,27.683000000000003,46.148,29.28 +2020-06-09 00:00:00,71.41,26.438000000000002,44.625,29.28 +2020-06-09 00:15:00,73.02,27.034000000000002,44.625,29.28 +2020-06-09 00:30:00,70.5,25.976999999999997,44.625,29.28 +2020-06-09 00:45:00,73.38,25.364,44.625,29.28 +2020-06-09 01:00:00,72.8,25.099,41.733000000000004,29.28 +2020-06-09 01:15:00,73.38,24.331999999999997,41.733000000000004,29.28 +2020-06-09 01:30:00,72.4,22.71,41.733000000000004,29.28 +2020-06-09 01:45:00,72.79,22.215,41.733000000000004,29.28 +2020-06-09 02:00:00,70.58,21.713,39.872,29.28 +2020-06-09 02:15:00,72.27,20.226,39.872,29.28 +2020-06-09 02:30:00,80.49,22.413,39.872,29.28 +2020-06-09 02:45:00,80.75,23.159000000000002,39.872,29.28 +2020-06-09 03:00:00,78.25,24.851,38.711,29.28 +2020-06-09 03:15:00,73.94,23.508000000000003,38.711,29.28 +2020-06-09 03:30:00,77.01,22.8,38.711,29.28 +2020-06-09 03:45:00,74.49,22.145,38.711,29.28 +2020-06-09 04:00:00,77.74,29.266,39.823,29.28 +2020-06-09 04:15:00,79.76,37.743,39.823,29.28 +2020-06-09 04:30:00,82.3,35.097,39.823,29.28 +2020-06-09 04:45:00,87.15,35.468,39.823,29.28 +2020-06-09 05:00:00,96.24,53.251000000000005,43.228,29.28 +2020-06-09 05:15:00,100.0,65.757,43.228,29.28 +2020-06-09 05:30:00,103.94,56.667,43.228,29.28 +2020-06-09 05:45:00,105.25,52.95,43.228,29.28 +2020-06-09 06:00:00,114.5,54.452,54.316,29.28 +2020-06-09 06:15:00,116.55,55.166000000000004,54.316,29.28 +2020-06-09 06:30:00,110.99,52.913000000000004,54.316,29.28 +2020-06-09 06:45:00,112.3,53.423,54.316,29.28 +2020-06-09 07:00:00,121.18,54.379,65.758,29.28 +2020-06-09 07:15:00,120.41,53.98,65.758,29.28 +2020-06-09 07:30:00,115.9,51.185,65.758,29.28 +2020-06-09 07:45:00,116.85,50.333999999999996,65.758,29.28 +2020-06-09 08:00:00,118.19,47.826,57.983000000000004,29.28 +2020-06-09 08:15:00,119.82,49.976000000000006,57.983000000000004,29.28 +2020-06-09 08:30:00,115.93,49.765,57.983000000000004,29.28 +2020-06-09 08:45:00,114.08,51.917,57.983000000000004,29.28 +2020-06-09 09:00:00,112.79,47.534,52.653,29.28 +2020-06-09 09:15:00,113.07,46.446999999999996,52.653,29.28 +2020-06-09 09:30:00,113.8,49.825,52.653,29.28 +2020-06-09 09:45:00,113.95,52.155,52.653,29.28 +2020-06-09 10:00:00,112.65,47.425,51.408,29.28 +2020-06-09 10:15:00,115.45,49.073,51.408,29.28 +2020-06-09 10:30:00,121.75,49.181999999999995,51.408,29.28 +2020-06-09 10:45:00,124.45,50.68899999999999,51.408,29.28 +2020-06-09 11:00:00,118.76,46.229,51.913000000000004,29.28 +2020-06-09 11:15:00,118.38,47.549,51.913000000000004,29.28 +2020-06-09 11:30:00,118.73,49.02,51.913000000000004,29.28 +2020-06-09 11:45:00,120.94,50.788999999999994,51.913000000000004,29.28 +2020-06-09 12:00:00,115.6,46.87,49.508,29.28 +2020-06-09 12:15:00,114.07,46.952,49.508,29.28 +2020-06-09 12:30:00,115.92,46.006,49.508,29.28 +2020-06-09 12:45:00,116.36,47.536,49.508,29.28 +2020-06-09 13:00:00,114.4,48.217,50.007,29.28 +2020-06-09 13:15:00,114.3,49.275,50.007,29.28 +2020-06-09 13:30:00,114.18,47.592,50.007,29.28 +2020-06-09 13:45:00,114.22,46.174,50.007,29.28 +2020-06-09 14:00:00,109.09,48.222,49.778999999999996,29.28 +2020-06-09 14:15:00,106.54,46.875,49.778999999999996,29.28 +2020-06-09 14:30:00,113.2,45.803999999999995,49.778999999999996,29.28 +2020-06-09 14:45:00,112.19,46.391000000000005,49.778999999999996,29.28 +2020-06-09 15:00:00,108.09,47.952,51.559,29.28 +2020-06-09 15:15:00,101.97,45.549,51.559,29.28 +2020-06-09 15:30:00,104.39,43.786,51.559,29.28 +2020-06-09 15:45:00,107.82,41.613,51.559,29.28 +2020-06-09 16:00:00,106.35,44.193000000000005,53.531000000000006,29.28 +2020-06-09 16:15:00,101.43,43.746,53.531000000000006,29.28 +2020-06-09 16:30:00,105.03,43.018,53.531000000000006,29.28 +2020-06-09 16:45:00,106.03,39.527,53.531000000000006,29.28 +2020-06-09 17:00:00,112.45,42.126999999999995,59.497,29.28 +2020-06-09 17:15:00,115.83,42.196000000000005,59.497,29.28 +2020-06-09 17:30:00,116.3,41.498000000000005,59.497,29.28 +2020-06-09 17:45:00,110.26,39.883,59.497,29.28 +2020-06-09 18:00:00,106.4,42.235,59.861999999999995,29.28 +2020-06-09 18:15:00,108.44,42.52,59.861999999999995,29.28 +2020-06-09 18:30:00,112.57,40.175,59.861999999999995,29.28 +2020-06-09 18:45:00,113.13,44.573,59.861999999999995,29.28 +2020-06-09 19:00:00,106.86,45.585,60.989,29.28 +2020-06-09 19:15:00,103.42,44.653,60.989,29.28 +2020-06-09 19:30:00,98.81,43.82899999999999,60.989,29.28 +2020-06-09 19:45:00,99.4,44.125,60.989,29.28 +2020-06-09 20:00:00,95.84,42.619,68.35600000000001,29.28 +2020-06-09 20:15:00,94.92,42.388000000000005,68.35600000000001,29.28 +2020-06-09 20:30:00,94.19,42.602,68.35600000000001,29.28 +2020-06-09 20:45:00,94.35,43.11,68.35600000000001,29.28 +2020-06-09 21:00:00,90.98,41.266999999999996,59.251000000000005,29.28 +2020-06-09 21:15:00,89.1,43.023999999999994,59.251000000000005,29.28 +2020-06-09 21:30:00,86.25,43.975,59.251000000000005,29.28 +2020-06-09 21:45:00,86.55,45.022,59.251000000000005,29.28 +2020-06-09 22:00:00,82.28,42.479,54.736999999999995,29.28 +2020-06-09 22:15:00,80.43,44.108999999999995,54.736999999999995,29.28 +2020-06-09 22:30:00,78.68,38.405,54.736999999999995,29.28 +2020-06-09 22:45:00,78.71,35.179,54.736999999999995,29.28 +2020-06-09 23:00:00,74.03,30.709,46.806999999999995,29.28 +2020-06-09 23:15:00,74.01,29.048000000000002,46.806999999999995,29.28 +2020-06-09 23:30:00,74.25,27.978,46.806999999999995,29.28 +2020-06-09 23:45:00,73.65,27.365,46.806999999999995,29.28 +2020-06-10 00:00:00,69.28,26.302,43.824,29.28 +2020-06-10 00:15:00,71.2,26.901,43.824,29.28 +2020-06-10 00:30:00,66.99,25.846,43.824,29.28 +2020-06-10 00:45:00,71.61,25.239,43.824,29.28 +2020-06-10 01:00:00,69.8,24.991,39.86,29.28 +2020-06-10 01:15:00,71.05,24.204,39.86,29.28 +2020-06-10 01:30:00,70.29,22.575,39.86,29.28 +2020-06-10 01:45:00,71.08,22.073,39.86,29.28 +2020-06-10 02:00:00,69.28,21.573,37.931999999999995,29.28 +2020-06-10 02:15:00,70.6,20.082,37.931999999999995,29.28 +2020-06-10 02:30:00,77.94,22.269000000000002,37.931999999999995,29.28 +2020-06-10 02:45:00,78.48,23.021,37.931999999999995,29.28 +2020-06-10 03:00:00,76.46,24.714000000000002,37.579,29.28 +2020-06-10 03:15:00,74.28,23.369,37.579,29.28 +2020-06-10 03:30:00,73.07,22.666,37.579,29.28 +2020-06-10 03:45:00,74.4,22.034000000000002,37.579,29.28 +2020-06-10 04:00:00,78.61,29.1,37.931999999999995,29.28 +2020-06-10 04:15:00,78.12,37.53,37.931999999999995,29.28 +2020-06-10 04:30:00,82.15,34.872,37.931999999999995,29.28 +2020-06-10 04:45:00,86.72,35.239000000000004,37.931999999999995,29.28 +2020-06-10 05:00:00,95.11,52.913000000000004,40.942,29.28 +2020-06-10 05:15:00,101.05,65.282,40.942,29.28 +2020-06-10 05:30:00,107.89,56.26,40.942,29.28 +2020-06-10 05:45:00,112.89,52.593,40.942,29.28 +2020-06-10 06:00:00,115.77,54.111999999999995,56.516999999999996,29.28 +2020-06-10 06:15:00,116.7,54.808,56.516999999999996,29.28 +2020-06-10 06:30:00,120.98,52.577,56.516999999999996,29.28 +2020-06-10 06:45:00,121.83,53.114,56.516999999999996,29.28 +2020-06-10 07:00:00,121.07,54.059,71.707,29.28 +2020-06-10 07:15:00,116.29,53.674,71.707,29.28 +2020-06-10 07:30:00,121.13,50.87,71.707,29.28 +2020-06-10 07:45:00,119.6,50.05,71.707,29.28 +2020-06-10 08:00:00,120.93,47.552,61.17,29.28 +2020-06-10 08:15:00,117.32,49.738,61.17,29.28 +2020-06-10 08:30:00,118.76,49.519,61.17,29.28 +2020-06-10 08:45:00,116.95,51.678000000000004,61.17,29.28 +2020-06-10 09:00:00,124.86,47.288999999999994,57.282,29.28 +2020-06-10 09:15:00,121.0,46.206,57.282,29.28 +2020-06-10 09:30:00,126.65,49.588,57.282,29.28 +2020-06-10 09:45:00,128.01,51.937,57.282,29.28 +2020-06-10 10:00:00,127.44,47.218,54.026,29.28 +2020-06-10 10:15:00,119.72,48.883,54.026,29.28 +2020-06-10 10:30:00,121.47,48.994,54.026,29.28 +2020-06-10 10:45:00,128.3,50.51,54.026,29.28 +2020-06-10 11:00:00,132.15,46.044,54.277,29.28 +2020-06-10 11:15:00,127.14,47.372,54.277,29.28 +2020-06-10 11:30:00,117.1,48.833999999999996,54.277,29.28 +2020-06-10 11:45:00,114.36,50.605,54.277,29.28 +2020-06-10 12:00:00,120.41,46.718999999999994,52.552,29.28 +2020-06-10 12:15:00,123.17,46.803999999999995,52.552,29.28 +2020-06-10 12:30:00,122.08,45.833999999999996,52.552,29.28 +2020-06-10 12:45:00,120.6,47.368,52.552,29.28 +2020-06-10 13:00:00,118.16,48.044,52.111999999999995,29.28 +2020-06-10 13:15:00,119.23,49.104,52.111999999999995,29.28 +2020-06-10 13:30:00,117.97,47.431000000000004,52.111999999999995,29.28 +2020-06-10 13:45:00,113.38,46.016999999999996,52.111999999999995,29.28 +2020-06-10 14:00:00,112.33,48.085,52.066,29.28 +2020-06-10 14:15:00,108.78,46.735,52.066,29.28 +2020-06-10 14:30:00,112.6,45.636,52.066,29.28 +2020-06-10 14:45:00,111.92,46.229,52.066,29.28 +2020-06-10 15:00:00,110.0,47.827,52.523999999999994,29.28 +2020-06-10 15:15:00,106.57,45.413999999999994,52.523999999999994,29.28 +2020-06-10 15:30:00,108.24,43.638999999999996,52.523999999999994,29.28 +2020-06-10 15:45:00,110.6,41.455,52.523999999999994,29.28 +2020-06-10 16:00:00,112.33,44.068000000000005,54.101000000000006,29.28 +2020-06-10 16:15:00,110.81,43.615,54.101000000000006,29.28 +2020-06-10 16:30:00,111.57,42.906000000000006,54.101000000000006,29.28 +2020-06-10 16:45:00,118.23,39.384,54.101000000000006,29.28 +2020-06-10 17:00:00,119.5,42.012,58.155,29.28 +2020-06-10 17:15:00,116.37,42.07,58.155,29.28 +2020-06-10 17:30:00,117.51,41.364,58.155,29.28 +2020-06-10 17:45:00,114.99,39.728,58.155,29.28 +2020-06-10 18:00:00,121.59,42.092,60.205,29.28 +2020-06-10 18:15:00,119.08,42.352,60.205,29.28 +2020-06-10 18:30:00,115.11,40.0,60.205,29.28 +2020-06-10 18:45:00,110.65,44.397,60.205,29.28 +2020-06-10 19:00:00,108.16,45.411,61.568999999999996,29.28 +2020-06-10 19:15:00,108.06,44.47,61.568999999999996,29.28 +2020-06-10 19:30:00,106.76,43.638999999999996,61.568999999999996,29.28 +2020-06-10 19:45:00,103.49,43.93,61.568999999999996,29.28 +2020-06-10 20:00:00,96.17,42.407,68.145,29.28 +2020-06-10 20:15:00,96.64,42.173,68.145,29.28 +2020-06-10 20:30:00,95.52,42.398999999999994,68.145,29.28 +2020-06-10 20:45:00,94.92,42.938,68.145,29.28 +2020-06-10 21:00:00,90.77,41.101000000000006,59.696000000000005,29.28 +2020-06-10 21:15:00,90.39,42.868,59.696000000000005,29.28 +2020-06-10 21:30:00,86.63,43.795,59.696000000000005,29.28 +2020-06-10 21:45:00,85.83,44.854,59.696000000000005,29.28 +2020-06-10 22:00:00,81.02,42.332,54.861999999999995,29.28 +2020-06-10 22:15:00,79.98,43.971000000000004,54.861999999999995,29.28 +2020-06-10 22:30:00,77.22,38.265,54.861999999999995,29.28 +2020-06-10 22:45:00,76.83,35.029,54.861999999999995,29.28 +2020-06-10 23:00:00,73.12,30.535999999999998,45.568000000000005,29.28 +2020-06-10 23:15:00,75.38,28.914,45.568000000000005,29.28 +2020-06-10 23:30:00,74.71,27.851999999999997,45.568000000000005,29.28 +2020-06-10 23:45:00,75.2,27.229,45.568000000000005,29.28 +2020-06-11 00:00:00,70.41,26.171999999999997,40.181,29.28 +2020-06-11 00:15:00,70.82,26.772,40.181,29.28 +2020-06-11 00:30:00,70.59,25.719,40.181,29.28 +2020-06-11 00:45:00,70.45,25.116999999999997,40.181,29.28 +2020-06-11 01:00:00,69.57,24.886,38.296,29.28 +2020-06-11 01:15:00,70.14,24.081,38.296,29.28 +2020-06-11 01:30:00,68.96,22.445,38.296,29.28 +2020-06-11 01:45:00,69.74,21.936999999999998,38.296,29.28 +2020-06-11 02:00:00,69.81,21.438000000000002,36.575,29.28 +2020-06-11 02:15:00,69.88,19.944000000000003,36.575,29.28 +2020-06-11 02:30:00,70.09,22.129,36.575,29.28 +2020-06-11 02:45:00,76.04,22.886999999999997,36.575,29.28 +2020-06-11 03:00:00,79.84,24.581,36.394,29.28 +2020-06-11 03:15:00,79.7,23.234,36.394,29.28 +2020-06-11 03:30:00,74.17,22.535,36.394,29.28 +2020-06-11 03:45:00,76.38,21.927,36.394,29.28 +2020-06-11 04:00:00,75.88,28.939,37.207,29.28 +2020-06-11 04:15:00,78.09,37.323,37.207,29.28 +2020-06-11 04:30:00,82.26,34.653,37.207,29.28 +2020-06-11 04:45:00,86.97,35.016,37.207,29.28 +2020-06-11 05:00:00,94.99,52.582,40.713,29.28 +2020-06-11 05:15:00,100.23,64.817,40.713,29.28 +2020-06-11 05:30:00,106.76,55.861000000000004,40.713,29.28 +2020-06-11 05:45:00,111.82,52.246,40.713,29.28 +2020-06-11 06:00:00,116.83,53.778999999999996,50.952,29.28 +2020-06-11 06:15:00,112.43,54.458,50.952,29.28 +2020-06-11 06:30:00,113.1,52.248000000000005,50.952,29.28 +2020-06-11 06:45:00,118.36,52.81399999999999,50.952,29.28 +2020-06-11 07:00:00,124.46,53.745,64.88,29.28 +2020-06-11 07:15:00,126.91,53.378,64.88,29.28 +2020-06-11 07:30:00,121.71,50.56100000000001,64.88,29.28 +2020-06-11 07:45:00,118.85,49.775,64.88,29.28 +2020-06-11 08:00:00,123.62,47.285,55.133,29.28 +2020-06-11 08:15:00,123.22,49.50899999999999,55.133,29.28 +2020-06-11 08:30:00,122.66,49.281000000000006,55.133,29.28 +2020-06-11 08:45:00,116.59,51.448,55.133,29.28 +2020-06-11 09:00:00,119.41,47.053000000000004,48.912,29.28 +2020-06-11 09:15:00,123.06,45.975,48.912,29.28 +2020-06-11 09:30:00,124.81,49.357,48.912,29.28 +2020-06-11 09:45:00,122.25,51.726000000000006,48.912,29.28 +2020-06-11 10:00:00,123.12,47.016999999999996,45.968999999999994,29.28 +2020-06-11 10:15:00,122.31,48.696999999999996,45.968999999999994,29.28 +2020-06-11 10:30:00,124.76,48.813,45.968999999999994,29.28 +2020-06-11 10:45:00,121.0,50.336999999999996,45.968999999999994,29.28 +2020-06-11 11:00:00,114.05,45.86600000000001,44.067,29.28 +2020-06-11 11:15:00,112.2,47.202,44.067,29.28 +2020-06-11 11:30:00,108.33,48.652,44.067,29.28 +2020-06-11 11:45:00,108.51,50.427,44.067,29.28 +2020-06-11 12:00:00,109.52,46.573,41.501000000000005,29.28 +2020-06-11 12:15:00,106.82,46.66,41.501000000000005,29.28 +2020-06-11 12:30:00,106.48,45.667,41.501000000000005,29.28 +2020-06-11 12:45:00,100.88,47.20399999999999,41.501000000000005,29.28 +2020-06-11 13:00:00,95.77,47.875,41.117,29.28 +2020-06-11 13:15:00,95.82,48.938,41.117,29.28 +2020-06-11 13:30:00,100.99,47.275,41.117,29.28 +2020-06-11 13:45:00,104.58,45.864,41.117,29.28 +2020-06-11 14:00:00,103.94,47.951,41.492,29.28 +2020-06-11 14:15:00,100.72,46.599,41.492,29.28 +2020-06-11 14:30:00,97.94,45.475,41.492,29.28 +2020-06-11 14:45:00,99.64,46.071000000000005,41.492,29.28 +2020-06-11 15:00:00,99.53,47.706,43.711999999999996,29.28 +2020-06-11 15:15:00,97.45,45.283,43.711999999999996,29.28 +2020-06-11 15:30:00,94.25,43.497,43.711999999999996,29.28 +2020-06-11 15:45:00,100.3,41.3,43.711999999999996,29.28 +2020-06-11 16:00:00,103.4,43.946000000000005,45.446000000000005,29.28 +2020-06-11 16:15:00,100.4,43.488,45.446000000000005,29.28 +2020-06-11 16:30:00,100.04,42.797,45.446000000000005,29.28 +2020-06-11 16:45:00,101.12,39.244,45.446000000000005,29.28 +2020-06-11 17:00:00,110.22,41.901,48.803000000000004,29.28 +2020-06-11 17:15:00,110.07,41.951,48.803000000000004,29.28 +2020-06-11 17:30:00,113.51,41.235,48.803000000000004,29.28 +2020-06-11 17:45:00,106.59,39.578,48.803000000000004,29.28 +2020-06-11 18:00:00,104.74,41.95399999999999,51.167,29.28 +2020-06-11 18:15:00,106.72,42.18899999999999,51.167,29.28 +2020-06-11 18:30:00,107.72,39.83,51.167,29.28 +2020-06-11 18:45:00,102.99,44.225,51.167,29.28 +2020-06-11 19:00:00,99.51,45.243,52.486000000000004,29.28 +2020-06-11 19:15:00,103.25,44.294,52.486000000000004,29.28 +2020-06-11 19:30:00,105.06,43.455,52.486000000000004,29.28 +2020-06-11 19:45:00,97.36,43.74100000000001,52.486000000000004,29.28 +2020-06-11 20:00:00,101.57,42.2,59.635,29.28 +2020-06-11 20:15:00,101.54,41.964,59.635,29.28 +2020-06-11 20:30:00,100.96,42.202,59.635,29.28 +2020-06-11 20:45:00,96.25,42.772,59.635,29.28 +2020-06-11 21:00:00,92.74,40.939,54.353,29.28 +2020-06-11 21:15:00,89.61,42.716,54.353,29.28 +2020-06-11 21:30:00,87.7,43.619,54.353,29.28 +2020-06-11 21:45:00,94.37,44.69,54.353,29.28 +2020-06-11 22:00:00,89.15,42.188,49.431999999999995,29.28 +2020-06-11 22:15:00,88.41,43.838,49.431999999999995,29.28 +2020-06-11 22:30:00,80.58,38.126999999999995,49.431999999999995,29.28 +2020-06-11 22:45:00,80.98,34.881,49.431999999999995,29.28 +2020-06-11 23:00:00,82.25,30.368000000000002,42.872,29.28 +2020-06-11 23:15:00,83.85,28.783,42.872,29.28 +2020-06-11 23:30:00,79.67,27.729,42.872,29.28 +2020-06-11 23:45:00,74.85,27.095,42.872,29.28 +2020-06-12 00:00:00,71.36,24.219,39.819,29.28 +2020-06-12 00:15:00,72.66,25.046999999999997,39.819,29.28 +2020-06-12 00:30:00,77.04,24.245,39.819,29.28 +2020-06-12 00:45:00,78.71,24.064,39.819,29.28 +2020-06-12 01:00:00,74.65,23.453000000000003,37.797,29.28 +2020-06-12 01:15:00,72.83,22.143,37.797,29.28 +2020-06-12 01:30:00,71.34,21.164,37.797,29.28 +2020-06-12 01:45:00,78.24,20.419,37.797,29.28 +2020-06-12 02:00:00,77.22,20.825,36.905,29.28 +2020-06-12 02:15:00,75.15,19.269000000000002,36.905,29.28 +2020-06-12 02:30:00,76.1,22.295,36.905,29.28 +2020-06-12 02:45:00,72.39,22.408,36.905,29.28 +2020-06-12 03:00:00,77.81,24.747,37.1,29.28 +2020-06-12 03:15:00,79.33,22.335,37.1,29.28 +2020-06-12 03:30:00,78.33,21.410999999999998,37.1,29.28 +2020-06-12 03:45:00,74.64,21.698,37.1,29.28 +2020-06-12 04:00:00,77.34,28.813000000000002,37.882,29.28 +2020-06-12 04:15:00,84.37,35.63,37.882,29.28 +2020-06-12 04:30:00,86.71,33.882,37.882,29.28 +2020-06-12 04:45:00,88.82,33.51,37.882,29.28 +2020-06-12 05:00:00,89.4,50.383,40.777,29.28 +2020-06-12 05:15:00,93.53,63.548,40.777,29.28 +2020-06-12 05:30:00,98.42,54.953,40.777,29.28 +2020-06-12 05:45:00,98.91,50.955,40.777,29.28 +2020-06-12 06:00:00,102.82,52.763000000000005,55.528,29.28 +2020-06-12 06:15:00,102.95,53.437,55.528,29.28 +2020-06-12 06:30:00,110.8,51.111999999999995,55.528,29.28 +2020-06-12 06:45:00,112.79,51.744,55.528,29.28 +2020-06-12 07:00:00,116.91,53.201,67.749,29.28 +2020-06-12 07:15:00,107.11,53.821000000000005,67.749,29.28 +2020-06-12 07:30:00,104.05,49.117,67.749,29.28 +2020-06-12 07:45:00,103.9,48.068999999999996,67.749,29.28 +2020-06-12 08:00:00,106.28,46.215,57.55,29.28 +2020-06-12 08:15:00,102.7,49.07,57.55,29.28 +2020-06-12 08:30:00,109.84,48.847,57.55,29.28 +2020-06-12 08:45:00,110.15,50.713,57.55,29.28 +2020-06-12 09:00:00,108.4,44.169,52.588,29.28 +2020-06-12 09:15:00,103.04,44.977,52.588,29.28 +2020-06-12 09:30:00,109.97,47.66,52.588,29.28 +2020-06-12 09:45:00,108.51,50.403999999999996,52.588,29.28 +2020-06-12 10:00:00,107.23,45.416000000000004,49.772,29.28 +2020-06-12 10:15:00,105.95,46.98,49.772,29.28 +2020-06-12 10:30:00,114.2,47.611999999999995,49.772,29.28 +2020-06-12 10:45:00,115.09,49.004,49.772,29.28 +2020-06-12 11:00:00,112.1,44.768,49.226000000000006,29.28 +2020-06-12 11:15:00,107.96,44.983999999999995,49.226000000000006,29.28 +2020-06-12 11:30:00,105.26,46.276,49.226000000000006,29.28 +2020-06-12 11:45:00,103.17,47.152,49.226000000000006,29.28 +2020-06-12 12:00:00,101.7,43.861000000000004,45.705,29.28 +2020-06-12 12:15:00,103.75,43.108999999999995,45.705,29.28 +2020-06-12 12:30:00,102.31,42.221000000000004,45.705,29.28 +2020-06-12 12:45:00,102.28,43.075,45.705,29.28 +2020-06-12 13:00:00,98.03,44.407,43.133,29.28 +2020-06-12 13:15:00,98.2,45.735,43.133,29.28 +2020-06-12 13:30:00,97.7,44.833999999999996,43.133,29.28 +2020-06-12 13:45:00,99.86,43.71,43.133,29.28 +2020-06-12 14:00:00,100.43,44.925,41.989,29.28 +2020-06-12 14:15:00,103.53,43.963,41.989,29.28 +2020-06-12 14:30:00,105.57,44.29600000000001,41.989,29.28 +2020-06-12 14:45:00,108.6,44.278999999999996,41.989,29.28 +2020-06-12 15:00:00,107.88,45.821999999999996,43.728,29.28 +2020-06-12 15:15:00,108.67,43.114,43.728,29.28 +2020-06-12 15:30:00,108.56,40.613,43.728,29.28 +2020-06-12 15:45:00,108.51,39.143,43.728,29.28 +2020-06-12 16:00:00,107.02,40.894,45.93899999999999,29.28 +2020-06-12 16:15:00,107.22,40.931,45.93899999999999,29.28 +2020-06-12 16:30:00,106.33,40.096,45.93899999999999,29.28 +2020-06-12 16:45:00,105.53,35.755,45.93899999999999,29.28 +2020-06-12 17:00:00,105.78,40.11,50.488,29.28 +2020-06-12 17:15:00,104.46,39.942,50.488,29.28 +2020-06-12 17:30:00,104.33,39.343,50.488,29.28 +2020-06-12 17:45:00,106.02,37.482,50.488,29.28 +2020-06-12 18:00:00,104.89,39.993,52.408,29.28 +2020-06-12 18:15:00,103.14,39.260999999999996,52.408,29.28 +2020-06-12 18:30:00,102.14,36.836999999999996,52.408,29.28 +2020-06-12 18:45:00,101.78,41.63,52.408,29.28 +2020-06-12 19:00:00,98.46,43.586999999999996,52.736000000000004,29.28 +2020-06-12 19:15:00,94.32,43.331,52.736000000000004,29.28 +2020-06-12 19:30:00,94.61,42.503,52.736000000000004,29.28 +2020-06-12 19:45:00,91.52,41.763999999999996,52.736000000000004,29.28 +2020-06-12 20:00:00,89.27,40.058,59.68,29.28 +2020-06-12 20:15:00,91.99,40.582,59.68,29.28 +2020-06-12 20:30:00,88.8,40.359,59.68,29.28 +2020-06-12 20:45:00,88.94,40.238,59.68,29.28 +2020-06-12 21:00:00,86.4,39.718,54.343999999999994,29.28 +2020-06-12 21:15:00,85.82,43.148,54.343999999999994,29.28 +2020-06-12 21:30:00,83.13,43.888999999999996,54.343999999999994,29.28 +2020-06-12 21:45:00,84.05,45.218,54.343999999999994,29.28 +2020-06-12 22:00:00,77.72,42.698,49.672,29.28 +2020-06-12 22:15:00,77.94,44.105,49.672,29.28 +2020-06-12 22:30:00,74.99,43.566,49.672,29.28 +2020-06-12 22:45:00,76.11,41.47,49.672,29.28 +2020-06-12 23:00:00,70.24,38.516,42.065,29.28 +2020-06-12 23:15:00,69.98,35.275999999999996,42.065,29.28 +2020-06-12 23:30:00,69.72,32.342,42.065,29.28 +2020-06-12 23:45:00,69.07,31.518,42.065,29.28 +2020-06-13 00:00:00,64.84,25.0,38.829,29.17 +2020-06-13 00:15:00,66.33,24.767,38.829,29.17 +2020-06-13 00:30:00,65.93,23.649,38.829,29.17 +2020-06-13 00:45:00,64.97,22.844,38.829,29.17 +2020-06-13 01:00:00,64.06,22.574,34.63,29.17 +2020-06-13 01:15:00,64.21,21.715,34.63,29.17 +2020-06-13 01:30:00,62.98,19.908,34.63,29.17 +2020-06-13 01:45:00,63.23,20.326,34.63,29.17 +2020-06-13 02:00:00,62.58,19.86,32.465,29.17 +2020-06-13 02:15:00,62.35,17.509,32.465,29.17 +2020-06-13 02:30:00,62.98,19.667,32.465,29.17 +2020-06-13 02:45:00,62.07,20.549,32.465,29.17 +2020-06-13 03:00:00,63.84,21.636999999999997,31.925,29.17 +2020-06-13 03:15:00,70.97,18.439,31.925,29.17 +2020-06-13 03:30:00,65.1,17.679000000000002,31.925,29.17 +2020-06-13 03:45:00,63.54,19.415,31.925,29.17 +2020-06-13 04:00:00,61.45,24.273000000000003,31.309,29.17 +2020-06-13 04:15:00,69.01,29.947,31.309,29.17 +2020-06-13 04:30:00,69.89,26.381,31.309,29.17 +2020-06-13 04:45:00,70.47,26.219,31.309,29.17 +2020-06-13 05:00:00,64.5,33.701,30.323,29.17 +2020-06-13 05:15:00,66.69,34.52,30.323,29.17 +2020-06-13 05:30:00,66.77,27.484,30.323,29.17 +2020-06-13 05:45:00,69.87,28.311999999999998,30.323,29.17 +2020-06-13 06:00:00,75.11,42.504,31.438000000000002,29.17 +2020-06-13 06:15:00,75.75,52.11600000000001,31.438000000000002,29.17 +2020-06-13 06:30:00,84.06,46.602,31.438000000000002,29.17 +2020-06-13 06:45:00,87.29,43.371,31.438000000000002,29.17 +2020-06-13 07:00:00,86.33,43.028,34.891999999999996,29.17 +2020-06-13 07:15:00,87.6,42.312,34.891999999999996,29.17 +2020-06-13 07:30:00,90.91,39.294000000000004,34.891999999999996,29.17 +2020-06-13 07:45:00,93.57,39.534,34.891999999999996,29.17 +2020-06-13 08:00:00,94.56,38.69,39.608000000000004,29.17 +2020-06-13 08:15:00,92.12,41.727,39.608000000000004,29.17 +2020-06-13 08:30:00,96.32,41.638999999999996,39.608000000000004,29.17 +2020-06-13 08:45:00,97.62,44.715,39.608000000000004,29.17 +2020-06-13 09:00:00,91.24,41.26,40.894,29.17 +2020-06-13 09:15:00,87.74,42.605,40.894,29.17 +2020-06-13 09:30:00,87.68,45.849,40.894,29.17 +2020-06-13 09:45:00,84.6,48.202,40.894,29.17 +2020-06-13 10:00:00,79.46,43.803999999999995,39.525,29.17 +2020-06-13 10:15:00,84.16,45.736000000000004,39.525,29.17 +2020-06-13 10:30:00,89.39,46.066,39.525,29.17 +2020-06-13 10:45:00,89.77,47.268,39.525,29.17 +2020-06-13 11:00:00,91.12,42.92,36.718,29.17 +2020-06-13 11:15:00,93.16,43.891999999999996,36.718,29.17 +2020-06-13 11:30:00,86.51,45.324,36.718,29.17 +2020-06-13 11:45:00,84.34,46.735,36.718,29.17 +2020-06-13 12:00:00,79.65,43.74,35.688,29.17 +2020-06-13 12:15:00,73.18,43.858000000000004,35.688,29.17 +2020-06-13 12:30:00,70.87,42.857,35.688,29.17 +2020-06-13 12:45:00,71.71,44.318999999999996,35.688,29.17 +2020-06-13 13:00:00,72.51,44.803000000000004,32.858000000000004,29.17 +2020-06-13 13:15:00,73.91,45.407,32.858000000000004,29.17 +2020-06-13 13:30:00,70.81,44.647,32.858000000000004,29.17 +2020-06-13 13:45:00,71.46,42.306000000000004,32.858000000000004,29.17 +2020-06-13 14:00:00,65.58,43.733000000000004,31.738000000000003,29.17 +2020-06-13 14:15:00,67.67,41.534,31.738000000000003,29.17 +2020-06-13 14:30:00,66.02,41.251000000000005,31.738000000000003,29.17 +2020-06-13 14:45:00,69.44,41.713,31.738000000000003,29.17 +2020-06-13 15:00:00,69.59,43.79,34.35,29.17 +2020-06-13 15:15:00,68.57,41.821000000000005,34.35,29.17 +2020-06-13 15:30:00,70.63,39.661,34.35,29.17 +2020-06-13 15:45:00,70.47,37.313,34.35,29.17 +2020-06-13 16:00:00,70.88,41.03,37.522,29.17 +2020-06-13 16:15:00,69.13,40.306999999999995,37.522,29.17 +2020-06-13 16:30:00,70.38,39.698,37.522,29.17 +2020-06-13 16:45:00,72.18,35.393,37.522,29.17 +2020-06-13 17:00:00,75.53,38.603,42.498000000000005,29.17 +2020-06-13 17:15:00,75.43,36.553000000000004,42.498000000000005,29.17 +2020-06-13 17:30:00,76.48,35.812,42.498000000000005,29.17 +2020-06-13 17:45:00,78.84,34.356,42.498000000000005,29.17 +2020-06-13 18:00:00,80.38,38.174,44.701,29.17 +2020-06-13 18:15:00,80.49,39.178000000000004,44.701,29.17 +2020-06-13 18:30:00,83.97,38.166,44.701,29.17 +2020-06-13 18:45:00,81.48,39.333,44.701,29.17 +2020-06-13 19:00:00,81.9,39.872,45.727,29.17 +2020-06-13 19:15:00,76.64,38.601,45.727,29.17 +2020-06-13 19:30:00,75.82,38.568000000000005,45.727,29.17 +2020-06-13 19:45:00,78.53,39.488,45.727,29.17 +2020-06-13 20:00:00,74.72,38.754,43.391000000000005,29.17 +2020-06-13 20:15:00,76.21,38.875,43.391000000000005,29.17 +2020-06-13 20:30:00,72.61,37.795,43.391000000000005,29.17 +2020-06-13 20:45:00,72.49,39.428000000000004,43.391000000000005,29.17 +2020-06-13 21:00:00,71.31,37.734,41.231,29.17 +2020-06-13 21:15:00,69.62,40.884,41.231,29.17 +2020-06-13 21:30:00,68.99,41.898999999999994,41.231,29.17 +2020-06-13 21:45:00,65.8,42.681000000000004,41.231,29.17 +2020-06-13 22:00:00,61.94,40.286,40.798,29.17 +2020-06-13 22:15:00,62.44,42.176,40.798,29.17 +2020-06-13 22:30:00,60.41,41.869,40.798,29.17 +2020-06-13 22:45:00,59.47,40.296,40.798,29.17 +2020-06-13 23:00:00,56.53,36.909,34.402,29.17 +2020-06-13 23:15:00,56.79,33.949,34.402,29.17 +2020-06-13 23:30:00,56.94,33.191,34.402,29.17 +2020-06-13 23:45:00,56.32,32.468,34.402,29.17 +2020-06-14 00:00:00,52.28,26.233,30.171,29.17 +2020-06-14 00:15:00,53.6,24.875,30.171,29.17 +2020-06-14 00:30:00,49.61,23.584,30.171,29.17 +2020-06-14 00:45:00,51.87,22.745,30.171,29.17 +2020-06-14 01:00:00,50.13,22.730999999999998,27.15,29.17 +2020-06-14 01:15:00,51.9,21.837,27.15,29.17 +2020-06-14 01:30:00,50.98,19.942999999999998,27.15,29.17 +2020-06-14 01:45:00,51.2,19.963,27.15,29.17 +2020-06-14 02:00:00,49.19,19.500999999999998,25.403000000000002,29.17 +2020-06-14 02:15:00,49.45,17.74,25.403000000000002,29.17 +2020-06-14 02:30:00,46.81,20.155,25.403000000000002,29.17 +2020-06-14 02:45:00,49.86,20.811,25.403000000000002,29.17 +2020-06-14 03:00:00,48.72,22.546999999999997,23.386999999999997,29.17 +2020-06-14 03:15:00,49.41,19.547,23.386999999999997,29.17 +2020-06-14 03:30:00,49.29,18.229,23.386999999999997,29.17 +2020-06-14 03:45:00,48.21,19.223,23.386999999999997,29.17 +2020-06-14 04:00:00,46.3,23.975,23.941999999999997,29.17 +2020-06-14 04:15:00,49.49,29.096999999999998,23.941999999999997,29.17 +2020-06-14 04:30:00,49.02,26.796999999999997,23.941999999999997,29.17 +2020-06-14 04:45:00,51.15,26.214000000000002,23.941999999999997,29.17 +2020-06-14 05:00:00,52.15,33.439,23.026,29.17 +2020-06-14 05:15:00,52.37,33.179,23.026,29.17 +2020-06-14 05:30:00,51.73,25.811,23.026,29.17 +2020-06-14 05:45:00,51.3,26.45,23.026,29.17 +2020-06-14 06:00:00,55.97,38.391,23.223000000000003,29.17 +2020-06-14 06:15:00,56.52,48.623999999999995,23.223000000000003,29.17 +2020-06-14 06:30:00,57.52,42.369,23.223000000000003,29.17 +2020-06-14 06:45:00,56.72,38.123000000000005,23.223000000000003,29.17 +2020-06-14 07:00:00,58.51,38.266999999999996,24.968000000000004,29.17 +2020-06-14 07:15:00,64.2,35.931999999999995,24.968000000000004,29.17 +2020-06-14 07:30:00,64.55,34.004,24.968000000000004,29.17 +2020-06-14 07:45:00,59.52,34.175,24.968000000000004,29.17 +2020-06-14 08:00:00,60.05,34.224000000000004,29.131,29.17 +2020-06-14 08:15:00,60.54,38.406,29.131,29.17 +2020-06-14 08:30:00,59.86,39.29,29.131,29.17 +2020-06-14 08:45:00,59.75,42.483999999999995,29.131,29.17 +2020-06-14 09:00:00,55.4,38.879,29.904,29.17 +2020-06-14 09:15:00,59.64,39.813,29.904,29.17 +2020-06-14 09:30:00,58.18,43.45,29.904,29.17 +2020-06-14 09:45:00,60.74,46.76,29.904,29.17 +2020-06-14 10:00:00,65.21,43.11600000000001,28.943,29.17 +2020-06-14 10:15:00,69.62,45.239,28.943,29.17 +2020-06-14 10:30:00,76.14,45.839,28.943,29.17 +2020-06-14 10:45:00,76.23,47.826,28.943,29.17 +2020-06-14 11:00:00,72.62,43.235,31.682,29.17 +2020-06-14 11:15:00,66.42,43.803999999999995,31.682,29.17 +2020-06-14 11:30:00,63.36,45.636,31.682,29.17 +2020-06-14 11:45:00,64.54,47.348,31.682,29.17 +2020-06-14 12:00:00,58.01,45.331,27.315,29.17 +2020-06-14 12:15:00,62.22,45.035,27.315,29.17 +2020-06-14 12:30:00,67.96,44.08,27.315,29.17 +2020-06-14 12:45:00,72.55,44.851000000000006,27.315,29.17 +2020-06-14 13:00:00,70.76,44.978,23.894000000000002,29.17 +2020-06-14 13:15:00,65.86,45.318000000000005,23.894000000000002,29.17 +2020-06-14 13:30:00,52.16,43.497,23.894000000000002,29.17 +2020-06-14 13:45:00,56.73,42.155,23.894000000000002,29.17 +2020-06-14 14:00:00,56.02,44.756,21.148000000000003,29.17 +2020-06-14 14:15:00,58.16,43.071999999999996,21.148000000000003,29.17 +2020-06-14 14:30:00,59.65,41.708999999999996,21.148000000000003,29.17 +2020-06-14 14:45:00,66.92,41.126999999999995,21.148000000000003,29.17 +2020-06-14 15:00:00,72.76,43.235,21.229,29.17 +2020-06-14 15:15:00,75.07,40.558,21.229,29.17 +2020-06-14 15:30:00,73.16,38.271,21.229,29.17 +2020-06-14 15:45:00,75.74,36.243,21.229,29.17 +2020-06-14 16:00:00,74.37,38.602,25.037,29.17 +2020-06-14 16:15:00,75.85,38.010999999999996,25.037,29.17 +2020-06-14 16:30:00,76.68,38.395,25.037,29.17 +2020-06-14 16:45:00,79.86,34.135,25.037,29.17 +2020-06-14 17:00:00,80.06,37.714,37.11,29.17 +2020-06-14 17:15:00,75.98,37.064,37.11,29.17 +2020-06-14 17:30:00,76.02,37.105,37.11,29.17 +2020-06-14 17:45:00,75.92,36.233000000000004,37.11,29.17 +2020-06-14 18:00:00,79.59,40.564,42.215,29.17 +2020-06-14 18:15:00,78.09,41.273999999999994,42.215,29.17 +2020-06-14 18:30:00,79.69,39.839,42.215,29.17 +2020-06-14 18:45:00,80.19,41.276,42.215,29.17 +2020-06-14 19:00:00,81.77,43.848,44.383,29.17 +2020-06-14 19:15:00,78.54,41.562,44.383,29.17 +2020-06-14 19:30:00,77.06,41.271,44.383,29.17 +2020-06-14 19:45:00,76.9,41.873999999999995,44.383,29.17 +2020-06-14 20:00:00,77.81,41.295,43.426,29.17 +2020-06-14 20:15:00,77.47,41.37,43.426,29.17 +2020-06-14 20:30:00,78.68,41.177,43.426,29.17 +2020-06-14 20:45:00,80.51,41.136,43.426,29.17 +2020-06-14 21:00:00,78.65,39.095,42.265,29.17 +2020-06-14 21:15:00,80.2,41.913999999999994,42.265,29.17 +2020-06-14 21:30:00,77.65,42.313,42.265,29.17 +2020-06-14 21:45:00,77.23,43.458,42.265,29.17 +2020-06-14 22:00:00,72.55,42.913999999999994,42.26,29.17 +2020-06-14 22:15:00,73.29,43.146,42.26,29.17 +2020-06-14 22:30:00,72.39,42.07899999999999,42.26,29.17 +2020-06-14 22:45:00,72.99,39.211,42.26,29.17 +2020-06-14 23:00:00,66.65,35.281,36.609,29.17 +2020-06-14 23:15:00,66.5,33.724000000000004,36.609,29.17 +2020-06-14 23:30:00,66.47,32.531,36.609,29.17 +2020-06-14 23:45:00,66.58,32.023,36.609,29.17 +2020-06-15 00:00:00,63.68,27.921999999999997,34.611,29.28 +2020-06-15 00:15:00,66.57,27.576,34.611,29.28 +2020-06-15 00:30:00,66.22,25.934,34.611,29.28 +2020-06-15 00:45:00,67.82,24.68,34.611,29.28 +2020-06-15 01:00:00,66.38,25.049,33.552,29.28 +2020-06-15 01:15:00,63.83,24.105,33.552,29.28 +2020-06-15 01:30:00,65.85,22.554000000000002,33.552,29.28 +2020-06-15 01:45:00,63.4,22.485,33.552,29.28 +2020-06-15 02:00:00,64.21,22.454,32.351,29.28 +2020-06-15 02:15:00,65.04,19.855,32.351,29.28 +2020-06-15 02:30:00,65.8,22.451,32.351,29.28 +2020-06-15 02:45:00,71.81,22.92,32.351,29.28 +2020-06-15 03:00:00,71.74,25.247,30.793000000000003,29.28 +2020-06-15 03:15:00,73.07,23.074,30.793000000000003,29.28 +2020-06-15 03:30:00,67.77,22.386999999999997,30.793000000000003,29.28 +2020-06-15 03:45:00,68.94,22.915,30.793000000000003,29.28 +2020-06-15 04:00:00,70.83,30.949,31.274,29.28 +2020-06-15 04:15:00,72.74,39.173,31.274,29.28 +2020-06-15 04:30:00,76.76,36.596,31.274,29.28 +2020-06-15 04:45:00,82.08,36.376999999999995,31.274,29.28 +2020-06-15 05:00:00,88.23,51.6,37.75,29.28 +2020-06-15 05:15:00,92.27,62.74100000000001,37.75,29.28 +2020-06-15 05:30:00,94.48,53.993,37.75,29.28 +2020-06-15 05:45:00,97.09,51.272,37.75,29.28 +2020-06-15 06:00:00,102.24,51.938,55.36,29.28 +2020-06-15 06:15:00,111.18,52.23,55.36,29.28 +2020-06-15 06:30:00,115.52,50.466,55.36,29.28 +2020-06-15 06:45:00,117.34,52.028,55.36,29.28 +2020-06-15 07:00:00,115.83,52.782,65.87,29.28 +2020-06-15 07:15:00,114.27,52.74100000000001,65.87,29.28 +2020-06-15 07:30:00,122.7,49.918,65.87,29.28 +2020-06-15 07:45:00,120.92,50.178999999999995,65.87,29.28 +2020-06-15 08:00:00,119.84,47.783,55.695,29.28 +2020-06-15 08:15:00,111.36,50.665,55.695,29.28 +2020-06-15 08:30:00,110.75,50.261,55.695,29.28 +2020-06-15 08:45:00,113.47,53.36,55.695,29.28 +2020-06-15 09:00:00,117.2,48.68899999999999,50.881,29.28 +2020-06-15 09:15:00,116.73,47.618,50.881,29.28 +2020-06-15 09:30:00,110.25,50.285,50.881,29.28 +2020-06-15 09:45:00,118.14,51.467,50.881,29.28 +2020-06-15 10:00:00,117.56,48.161,49.138000000000005,29.28 +2020-06-15 10:15:00,114.82,50.128,49.138000000000005,29.28 +2020-06-15 10:30:00,113.15,50.214,49.138000000000005,29.28 +2020-06-15 10:45:00,110.62,50.78,49.138000000000005,29.28 +2020-06-15 11:00:00,110.55,46.151,49.178000000000004,29.28 +2020-06-15 11:15:00,113.2,47.161,49.178000000000004,29.28 +2020-06-15 11:30:00,114.7,49.771,49.178000000000004,29.28 +2020-06-15 11:45:00,107.65,51.922,49.178000000000004,29.28 +2020-06-15 12:00:00,103.58,48.492,47.698,29.28 +2020-06-15 12:15:00,100.76,48.303000000000004,47.698,29.28 +2020-06-15 12:30:00,98.31,46.336000000000006,47.698,29.28 +2020-06-15 12:45:00,100.35,47.248000000000005,47.698,29.28 +2020-06-15 13:00:00,98.72,48.282,48.104,29.28 +2020-06-15 13:15:00,97.1,47.658,48.104,29.28 +2020-06-15 13:30:00,98.57,45.958,48.104,29.28 +2020-06-15 13:45:00,100.45,45.47,48.104,29.28 +2020-06-15 14:00:00,109.23,47.168,48.53,29.28 +2020-06-15 14:15:00,107.1,45.983999999999995,48.53,29.28 +2020-06-15 14:30:00,105.45,44.391000000000005,48.53,29.28 +2020-06-15 14:45:00,107.37,45.77,48.53,29.28 +2020-06-15 15:00:00,105.82,47.726000000000006,49.351000000000006,29.28 +2020-06-15 15:15:00,101.42,44.35,49.351000000000006,29.28 +2020-06-15 15:30:00,93.89,42.706,49.351000000000006,29.28 +2020-06-15 15:45:00,102.39,40.167,49.351000000000006,29.28 +2020-06-15 16:00:00,103.23,43.58,51.44,29.28 +2020-06-15 16:15:00,103.03,42.976000000000006,51.44,29.28 +2020-06-15 16:30:00,103.38,42.66,51.44,29.28 +2020-06-15 16:45:00,105.31,38.287,51.44,29.28 +2020-06-15 17:00:00,110.64,40.8,56.868,29.28 +2020-06-15 17:15:00,108.12,40.414,56.868,29.28 +2020-06-15 17:30:00,107.45,40.03,56.868,29.28 +2020-06-15 17:45:00,108.12,38.626,56.868,29.28 +2020-06-15 18:00:00,107.0,41.968,57.229,29.28 +2020-06-15 18:15:00,105.19,40.717,57.229,29.28 +2020-06-15 18:30:00,103.36,38.615,57.229,29.28 +2020-06-15 18:45:00,103.93,43.123999999999995,57.229,29.28 +2020-06-15 19:00:00,101.36,45.273,57.744,29.28 +2020-06-15 19:15:00,97.27,44.143,57.744,29.28 +2020-06-15 19:30:00,94.43,43.535,57.744,29.28 +2020-06-15 19:45:00,94.45,43.476000000000006,57.744,29.28 +2020-06-15 20:00:00,93.01,41.475,66.05199999999999,29.28 +2020-06-15 20:15:00,93.07,42.67,66.05199999999999,29.28 +2020-06-15 20:30:00,92.72,42.825,66.05199999999999,29.28 +2020-06-15 20:45:00,92.0,43.159,66.05199999999999,29.28 +2020-06-15 21:00:00,90.53,40.566,59.396,29.28 +2020-06-15 21:15:00,89.26,43.715,59.396,29.28 +2020-06-15 21:30:00,86.15,44.418,59.396,29.28 +2020-06-15 21:45:00,83.94,45.318999999999996,59.396,29.28 +2020-06-15 22:00:00,79.53,42.593999999999994,53.06,29.28 +2020-06-15 22:15:00,79.12,44.623999999999995,53.06,29.28 +2020-06-15 22:30:00,76.82,38.604,53.06,29.28 +2020-06-15 22:45:00,77.6,35.296,53.06,29.28 +2020-06-15 23:00:00,71.23,31.458000000000002,46.148,29.28 +2020-06-15 23:15:00,71.79,28.58,46.148,29.28 +2020-06-15 23:30:00,73.2,27.590999999999998,46.148,29.28 +2020-06-15 23:45:00,71.26,26.781,46.148,29.28 +2020-06-16 00:00:00,68.96,24.359,44.625,29.28 +2020-06-16 00:15:00,69.29,25.055999999999997,44.625,29.28 +2020-06-16 00:30:00,68.44,23.929000000000002,44.625,29.28 +2020-06-16 00:45:00,68.79,23.45,44.625,29.28 +2020-06-16 01:00:00,66.84,23.256,41.733000000000004,29.28 +2020-06-16 01:15:00,68.54,22.443,41.733000000000004,29.28 +2020-06-16 01:30:00,67.64,20.79,41.733000000000004,29.28 +2020-06-16 01:45:00,68.65,20.428,41.733000000000004,29.28 +2020-06-16 02:00:00,68.14,19.965999999999998,39.872,29.28 +2020-06-16 02:15:00,67.92,18.408,39.872,29.28 +2020-06-16 02:30:00,67.69,20.671999999999997,39.872,29.28 +2020-06-16 02:45:00,68.5,21.430999999999997,39.872,29.28 +2020-06-16 03:00:00,68.84,23.045,38.711,29.28 +2020-06-16 03:15:00,70.43,21.794,38.711,29.28 +2020-06-16 03:30:00,71.46,21.061999999999998,38.711,29.28 +2020-06-16 03:45:00,71.24,20.377,38.711,29.28 +2020-06-16 04:00:00,78.78,26.963,39.823,29.28 +2020-06-16 04:15:00,83.37,34.927,39.823,29.28 +2020-06-16 04:30:00,86.33,32.236999999999995,39.823,29.28 +2020-06-16 04:45:00,84.83,32.514,39.823,29.28 +2020-06-16 05:00:00,89.13,48.638999999999996,43.228,29.28 +2020-06-16 05:15:00,95.5,59.591,43.228,29.28 +2020-06-16 05:30:00,102.96,51.088,43.228,29.28 +2020-06-16 05:45:00,106.21,47.806999999999995,43.228,29.28 +2020-06-16 06:00:00,110.04,49.923,54.316,29.28 +2020-06-16 06:15:00,106.31,50.313,54.316,29.28 +2020-06-16 06:30:00,112.22,48.407,54.316,29.28 +2020-06-16 06:45:00,116.7,49.299,54.316,29.28 +2020-06-16 07:00:00,118.4,50.067,65.758,29.28 +2020-06-16 07:15:00,114.45,49.853,65.758,29.28 +2020-06-16 07:30:00,120.01,47.098,65.758,29.28 +2020-06-16 07:45:00,121.47,46.503,65.758,29.28 +2020-06-16 08:00:00,122.52,43.562,57.983000000000004,29.28 +2020-06-16 08:15:00,120.87,46.042,57.983000000000004,29.28 +2020-06-16 08:30:00,119.62,46.19,57.983000000000004,29.28 +2020-06-16 08:45:00,120.59,48.538000000000004,57.983000000000004,29.28 +2020-06-16 09:00:00,119.48,43.669,52.653,29.28 +2020-06-16 09:15:00,115.43,42.684,52.653,29.28 +2020-06-16 09:30:00,115.25,46.261,52.653,29.28 +2020-06-16 09:45:00,119.15,48.981,52.653,29.28 +2020-06-16 10:00:00,124.46,43.623000000000005,51.408,29.28 +2020-06-16 10:15:00,124.81,45.39,51.408,29.28 +2020-06-16 10:30:00,116.98,45.55,51.408,29.28 +2020-06-16 10:45:00,119.25,47.229,51.408,29.28 +2020-06-16 11:00:00,119.52,43.428000000000004,51.913000000000004,29.28 +2020-06-16 11:15:00,106.77,44.788000000000004,51.913000000000004,29.28 +2020-06-16 11:30:00,105.75,46.257,51.913000000000004,29.28 +2020-06-16 11:45:00,111.19,48.073,51.913000000000004,29.28 +2020-06-16 12:00:00,110.92,44.413000000000004,49.508,29.28 +2020-06-16 12:15:00,110.35,44.346000000000004,49.508,29.28 +2020-06-16 12:30:00,105.59,43.29,49.508,29.28 +2020-06-16 12:45:00,115.07,44.753,49.508,29.28 +2020-06-16 13:00:00,112.54,45.398,50.007,29.28 +2020-06-16 13:15:00,111.53,46.568999999999996,50.007,29.28 +2020-06-16 13:30:00,107.86,44.854,50.007,29.28 +2020-06-16 13:45:00,113.1,43.668,50.007,29.28 +2020-06-16 14:00:00,111.48,45.714,49.778999999999996,29.28 +2020-06-16 14:15:00,108.96,44.407,49.778999999999996,29.28 +2020-06-16 14:30:00,105.74,43.292,49.778999999999996,29.28 +2020-06-16 14:45:00,106.26,43.869,49.778999999999996,29.28 +2020-06-16 15:00:00,102.33,45.617,51.559,29.28 +2020-06-16 15:15:00,103.42,43.174,51.559,29.28 +2020-06-16 15:30:00,111.85,41.39,51.559,29.28 +2020-06-16 15:45:00,116.04,39.3,51.559,29.28 +2020-06-16 16:00:00,111.53,42.037,53.531000000000006,29.28 +2020-06-16 16:15:00,109.87,41.598,53.531000000000006,29.28 +2020-06-16 16:30:00,107.38,40.802,53.531000000000006,29.28 +2020-06-16 16:45:00,111.98,37.24,53.531000000000006,29.28 +2020-06-16 17:00:00,113.93,40.24,59.497,29.28 +2020-06-16 17:15:00,112.65,40.22,59.497,29.28 +2020-06-16 17:30:00,108.82,39.359,59.497,29.28 +2020-06-16 17:45:00,114.35,37.766,59.497,29.28 +2020-06-16 18:00:00,115.88,40.343,59.861999999999995,29.28 +2020-06-16 18:15:00,116.48,40.538000000000004,59.861999999999995,29.28 +2020-06-16 18:30:00,107.47,38.215,59.861999999999995,29.28 +2020-06-16 18:45:00,107.61,42.318000000000005,59.861999999999995,29.28 +2020-06-16 19:00:00,111.62,42.979,60.989,29.28 +2020-06-16 19:15:00,106.07,42.001000000000005,60.989,29.28 +2020-06-16 19:30:00,102.41,41.065,60.989,29.28 +2020-06-16 19:45:00,95.32,41.147,60.989,29.28 +2020-06-16 20:00:00,92.13,38.826,68.35600000000001,29.28 +2020-06-16 20:15:00,94.97,38.798,68.35600000000001,29.28 +2020-06-16 20:30:00,91.06,39.229,68.35600000000001,29.28 +2020-06-16 20:45:00,94.21,39.921,68.35600000000001,29.28 +2020-06-16 21:00:00,89.7,38.23,59.251000000000005,29.28 +2020-06-16 21:15:00,89.12,40.126999999999995,59.251000000000005,29.28 +2020-06-16 21:30:00,86.59,41.136,59.251000000000005,29.28 +2020-06-16 21:45:00,85.8,42.263999999999996,59.251000000000005,29.28 +2020-06-16 22:00:00,81.71,39.696,54.736999999999995,29.28 +2020-06-16 22:15:00,82.26,41.648,54.736999999999995,29.28 +2020-06-16 22:30:00,79.77,36.277,54.736999999999995,29.28 +2020-06-16 22:45:00,81.67,32.983000000000004,54.736999999999995,29.28 +2020-06-16 23:00:00,74.51,28.76,46.806999999999995,29.28 +2020-06-16 23:15:00,74.79,27.148000000000003,46.806999999999995,29.28 +2020-06-16 23:30:00,82.15,25.996,46.806999999999995,29.28 +2020-06-16 23:45:00,74.96,25.283,46.806999999999995,29.28 +2020-06-17 00:00:00,70.38,24.259,43.824,29.28 +2020-06-17 00:15:00,71.92,24.956,43.824,29.28 +2020-06-17 00:30:00,70.72,23.831999999999997,43.824,29.28 +2020-06-17 00:45:00,71.52,23.358,43.824,29.28 +2020-06-17 01:00:00,69.75,23.180999999999997,39.86,29.28 +2020-06-17 01:15:00,70.66,22.351999999999997,39.86,29.28 +2020-06-17 01:30:00,70.25,20.694000000000003,39.86,29.28 +2020-06-17 01:45:00,70.36,20.323,39.86,29.28 +2020-06-17 02:00:00,69.85,19.865,37.931999999999995,29.28 +2020-06-17 02:15:00,69.97,18.305999999999997,37.931999999999995,29.28 +2020-06-17 02:30:00,77.19,20.566,37.931999999999995,29.28 +2020-06-17 02:45:00,79.52,21.33,37.931999999999995,29.28 +2020-06-17 03:00:00,75.47,22.943,37.579,29.28 +2020-06-17 03:15:00,71.44,21.693,37.579,29.28 +2020-06-17 03:30:00,73.65,20.968000000000004,37.579,29.28 +2020-06-17 03:45:00,74.61,20.305,37.579,29.28 +2020-06-17 04:00:00,75.19,26.839000000000002,37.931999999999995,29.28 +2020-06-17 04:15:00,77.28,34.758,37.931999999999995,29.28 +2020-06-17 04:30:00,80.23,32.056,37.931999999999995,29.28 +2020-06-17 04:45:00,86.39,32.33,37.931999999999995,29.28 +2020-06-17 05:00:00,92.75,48.354,40.942,29.28 +2020-06-17 05:15:00,94.78,59.178000000000004,40.942,29.28 +2020-06-17 05:30:00,102.45,50.743,40.942,29.28 +2020-06-17 05:45:00,103.25,47.508,40.942,29.28 +2020-06-17 06:00:00,109.39,49.636,56.516999999999996,29.28 +2020-06-17 06:15:00,108.76,50.011,56.516999999999996,29.28 +2020-06-17 06:30:00,106.58,48.129,56.516999999999996,29.28 +2020-06-17 06:45:00,115.87,49.049,56.516999999999996,29.28 +2020-06-17 07:00:00,120.05,49.805,71.707,29.28 +2020-06-17 07:15:00,118.84,49.608999999999995,71.707,29.28 +2020-06-17 07:30:00,112.36,46.847,71.707,29.28 +2020-06-17 07:45:00,116.82,46.285,71.707,29.28 +2020-06-17 08:00:00,118.62,43.354,61.17,29.28 +2020-06-17 08:15:00,120.54,45.867,61.17,29.28 +2020-06-17 08:30:00,112.25,46.007,61.17,29.28 +2020-06-17 08:45:00,110.41,48.36,61.17,29.28 +2020-06-17 09:00:00,112.65,43.483999999999995,57.282,29.28 +2020-06-17 09:15:00,116.01,42.503,57.282,29.28 +2020-06-17 09:30:00,115.28,46.07899999999999,57.282,29.28 +2020-06-17 09:45:00,117.39,48.817,57.282,29.28 +2020-06-17 10:00:00,119.51,43.468,54.026,29.28 +2020-06-17 10:15:00,118.12,45.247,54.026,29.28 +2020-06-17 10:30:00,115.99,45.409,54.026,29.28 +2020-06-17 10:45:00,112.31,47.093999999999994,54.026,29.28 +2020-06-17 11:00:00,117.86,43.288999999999994,54.277,29.28 +2020-06-17 11:15:00,118.81,44.655,54.277,29.28 +2020-06-17 11:30:00,112.48,46.113,54.277,29.28 +2020-06-17 11:45:00,106.68,47.93,54.277,29.28 +2020-06-17 12:00:00,107.8,44.3,52.552,29.28 +2020-06-17 12:15:00,109.75,44.235,52.552,29.28 +2020-06-17 12:30:00,109.97,43.158,52.552,29.28 +2020-06-17 12:45:00,106.0,44.623000000000005,52.552,29.28 +2020-06-17 13:00:00,99.76,45.25899999999999,52.111999999999995,29.28 +2020-06-17 13:15:00,101.66,46.431999999999995,52.111999999999995,29.28 +2020-06-17 13:30:00,106.4,44.727,52.111999999999995,29.28 +2020-06-17 13:45:00,109.32,43.545,52.111999999999995,29.28 +2020-06-17 14:00:00,108.0,45.607,52.066,29.28 +2020-06-17 14:15:00,102.64,44.297,52.066,29.28 +2020-06-17 14:30:00,105.38,43.159,52.066,29.28 +2020-06-17 14:45:00,107.65,43.742,52.066,29.28 +2020-06-17 15:00:00,106.16,45.522,52.523999999999994,29.28 +2020-06-17 15:15:00,98.78,43.071000000000005,52.523999999999994,29.28 +2020-06-17 15:30:00,97.44,41.277,52.523999999999994,29.28 +2020-06-17 15:45:00,102.09,39.176,52.523999999999994,29.28 +2020-06-17 16:00:00,102.74,41.942,54.101000000000006,29.28 +2020-06-17 16:15:00,111.61,41.501000000000005,54.101000000000006,29.28 +2020-06-17 16:30:00,112.07,40.723,54.101000000000006,29.28 +2020-06-17 16:45:00,111.13,37.138000000000005,54.101000000000006,29.28 +2020-06-17 17:00:00,107.3,40.161,58.155,29.28 +2020-06-17 17:15:00,106.07,40.135,58.155,29.28 +2020-06-17 17:30:00,109.67,39.269,58.155,29.28 +2020-06-17 17:45:00,109.22,37.658,58.155,29.28 +2020-06-17 18:00:00,115.64,40.247,60.205,29.28 +2020-06-17 18:15:00,115.65,40.415,60.205,29.28 +2020-06-17 18:30:00,112.11,38.086999999999996,60.205,29.28 +2020-06-17 18:45:00,108.54,42.19,60.205,29.28 +2020-06-17 19:00:00,111.91,42.853,61.568999999999996,29.28 +2020-06-17 19:15:00,107.65,41.867,61.568999999999996,29.28 +2020-06-17 19:30:00,104.81,40.923,61.568999999999996,29.28 +2020-06-17 19:45:00,100.79,41.0,61.568999999999996,29.28 +2020-06-17 20:00:00,95.94,38.662,68.145,29.28 +2020-06-17 20:15:00,95.76,38.63,68.145,29.28 +2020-06-17 20:30:00,95.13,39.07,68.145,29.28 +2020-06-17 20:45:00,95.74,39.789,68.145,29.28 +2020-06-17 21:00:00,93.35,38.104,59.696000000000005,29.28 +2020-06-17 21:15:00,92.86,40.008,59.696000000000005,29.28 +2020-06-17 21:30:00,90.82,40.994,59.696000000000005,29.28 +2020-06-17 21:45:00,88.68,42.13,59.696000000000005,29.28 +2020-06-17 22:00:00,84.2,39.58,54.861999999999995,29.28 +2020-06-17 22:15:00,84.82,41.541000000000004,54.861999999999995,29.28 +2020-06-17 22:30:00,81.78,36.164,54.861999999999995,29.28 +2020-06-17 22:45:00,80.67,32.86,54.861999999999995,29.28 +2020-06-17 23:00:00,74.94,28.62,45.568000000000005,29.28 +2020-06-17 23:15:00,76.51,27.043000000000003,45.568000000000005,29.28 +2020-06-17 23:30:00,75.84,25.903000000000002,45.568000000000005,29.28 +2020-06-17 23:45:00,75.6,25.178,45.568000000000005,29.28 +2020-06-18 00:00:00,73.4,24.163,40.181,29.28 +2020-06-18 00:15:00,72.87,24.86,40.181,29.28 +2020-06-18 00:30:00,72.34,23.739,40.181,29.28 +2020-06-18 00:45:00,73.84,23.271,40.181,29.28 +2020-06-18 01:00:00,71.31,23.109,38.296,29.28 +2020-06-18 01:15:00,72.98,22.264,38.296,29.28 +2020-06-18 01:30:00,73.12,20.601999999999997,38.296,29.28 +2020-06-18 01:45:00,72.86,20.224,38.296,29.28 +2020-06-18 02:00:00,72.69,19.769000000000002,36.575,29.28 +2020-06-18 02:15:00,73.69,18.209,36.575,29.28 +2020-06-18 02:30:00,80.7,20.464000000000002,36.575,29.28 +2020-06-18 02:45:00,81.54,21.235,36.575,29.28 +2020-06-18 03:00:00,76.54,22.845,36.394,29.28 +2020-06-18 03:15:00,74.22,21.598000000000003,36.394,29.28 +2020-06-18 03:30:00,74.27,20.877,36.394,29.28 +2020-06-18 03:45:00,75.37,20.236,36.394,29.28 +2020-06-18 04:00:00,78.58,26.719,37.207,29.28 +2020-06-18 04:15:00,77.79,34.595,37.207,29.28 +2020-06-18 04:30:00,81.29,31.881,37.207,29.28 +2020-06-18 04:45:00,87.9,32.150999999999996,37.207,29.28 +2020-06-18 05:00:00,91.79,48.077,40.713,29.28 +2020-06-18 05:15:00,94.18,58.776,40.713,29.28 +2020-06-18 05:30:00,102.83,50.407,40.713,29.28 +2020-06-18 05:45:00,105.92,47.218999999999994,40.713,29.28 +2020-06-18 06:00:00,112.75,49.357,50.952,29.28 +2020-06-18 06:15:00,110.24,49.718999999999994,50.952,29.28 +2020-06-18 06:30:00,106.85,47.858000000000004,50.952,29.28 +2020-06-18 06:45:00,108.84,48.808,50.952,29.28 +2020-06-18 07:00:00,116.96,49.552,64.88,29.28 +2020-06-18 07:15:00,117.84,49.375,64.88,29.28 +2020-06-18 07:30:00,118.78,46.605,64.88,29.28 +2020-06-18 07:45:00,110.21,46.077,64.88,29.28 +2020-06-18 08:00:00,112.37,43.156000000000006,55.133,29.28 +2020-06-18 08:15:00,120.59,45.7,55.133,29.28 +2020-06-18 08:30:00,116.62,45.832,55.133,29.28 +2020-06-18 08:45:00,113.88,48.188,55.133,29.28 +2020-06-18 09:00:00,111.01,43.306000000000004,48.912,29.28 +2020-06-18 09:15:00,116.08,42.32899999999999,48.912,29.28 +2020-06-18 09:30:00,116.1,45.903999999999996,48.912,29.28 +2020-06-18 09:45:00,110.88,48.659,48.912,29.28 +2020-06-18 10:00:00,111.72,43.321000000000005,45.968999999999994,29.28 +2020-06-18 10:15:00,116.87,45.11,45.968999999999994,29.28 +2020-06-18 10:30:00,119.29,45.273999999999994,45.968999999999994,29.28 +2020-06-18 10:45:00,117.36,46.965,45.968999999999994,29.28 +2020-06-18 11:00:00,115.58,43.156000000000006,44.067,29.28 +2020-06-18 11:15:00,108.97,44.528,44.067,29.28 +2020-06-18 11:30:00,104.55,45.975,44.067,29.28 +2020-06-18 11:45:00,104.96,47.792,44.067,29.28 +2020-06-18 12:00:00,117.07,44.193000000000005,41.501000000000005,29.28 +2020-06-18 12:15:00,118.98,44.129,41.501000000000005,29.28 +2020-06-18 12:30:00,111.45,43.032,41.501000000000005,29.28 +2020-06-18 12:45:00,105.65,44.498999999999995,41.501000000000005,29.28 +2020-06-18 13:00:00,107.61,45.126000000000005,41.117,29.28 +2020-06-18 13:15:00,111.84,46.298,41.117,29.28 +2020-06-18 13:30:00,112.94,44.604,41.117,29.28 +2020-06-18 13:45:00,114.13,43.426,41.117,29.28 +2020-06-18 14:00:00,110.3,45.501999999999995,41.492,29.28 +2020-06-18 14:15:00,112.76,44.191,41.492,29.28 +2020-06-18 14:30:00,115.17,43.031000000000006,41.492,29.28 +2020-06-18 14:45:00,117.92,43.619,41.492,29.28 +2020-06-18 15:00:00,115.32,45.431000000000004,43.711999999999996,29.28 +2020-06-18 15:15:00,123.12,42.968999999999994,43.711999999999996,29.28 +2020-06-18 15:30:00,122.03,41.169,43.711999999999996,29.28 +2020-06-18 15:45:00,118.04,39.056999999999995,43.711999999999996,29.28 +2020-06-18 16:00:00,118.78,41.851000000000006,45.446000000000005,29.28 +2020-06-18 16:15:00,112.38,41.406000000000006,45.446000000000005,29.28 +2020-06-18 16:30:00,118.25,40.647,45.446000000000005,29.28 +2020-06-18 16:45:00,122.87,37.039,45.446000000000005,29.28 +2020-06-18 17:00:00,115.45,40.086999999999996,48.803000000000004,29.28 +2020-06-18 17:15:00,111.22,40.056,48.803000000000004,29.28 +2020-06-18 17:30:00,107.59,39.183,48.803000000000004,29.28 +2020-06-18 17:45:00,109.13,37.556999999999995,48.803000000000004,29.28 +2020-06-18 18:00:00,114.28,40.154,51.167,29.28 +2020-06-18 18:15:00,108.94,40.298,51.167,29.28 +2020-06-18 18:30:00,104.66,37.966,51.167,29.28 +2020-06-18 18:45:00,108.51,42.067,51.167,29.28 +2020-06-18 19:00:00,107.19,42.733000000000004,52.486000000000004,29.28 +2020-06-18 19:15:00,97.5,41.736999999999995,52.486000000000004,29.28 +2020-06-18 19:30:00,95.56,40.787,52.486000000000004,29.28 +2020-06-18 19:45:00,94.72,40.857,52.486000000000004,29.28 +2020-06-18 20:00:00,92.52,38.503,59.635,29.28 +2020-06-18 20:15:00,92.24,38.47,59.635,29.28 +2020-06-18 20:30:00,92.21,38.916,59.635,29.28 +2020-06-18 20:45:00,92.12,39.663000000000004,59.635,29.28 +2020-06-18 21:00:00,90.57,37.983000000000004,54.353,29.28 +2020-06-18 21:15:00,89.89,39.894,54.353,29.28 +2020-06-18 21:30:00,85.01,40.857,54.353,29.28 +2020-06-18 21:45:00,85.71,42.0,54.353,29.28 +2020-06-18 22:00:00,80.59,39.469,49.431999999999995,29.28 +2020-06-18 22:15:00,80.6,41.437,49.431999999999995,29.28 +2020-06-18 22:30:00,78.43,36.054,49.431999999999995,29.28 +2020-06-18 22:45:00,78.92,32.741,49.431999999999995,29.28 +2020-06-18 23:00:00,74.17,28.486,42.872,29.28 +2020-06-18 23:15:00,75.08,26.943,42.872,29.28 +2020-06-18 23:30:00,71.55,25.811,42.872,29.28 +2020-06-18 23:45:00,74.47,25.078000000000003,42.872,29.28 +2020-06-19 00:00:00,71.17,22.217,39.819,29.28 +2020-06-19 00:15:00,70.01,23.143,39.819,29.28 +2020-06-19 00:30:00,67.45,22.285999999999998,39.819,29.28 +2020-06-19 00:45:00,69.33,22.254,39.819,29.28 +2020-06-19 01:00:00,66.99,21.705,37.797,29.28 +2020-06-19 01:15:00,71.13,20.305,37.797,29.28 +2020-06-19 01:30:00,76.08,19.335,37.797,29.28 +2020-06-19 01:45:00,76.62,18.707,37.797,29.28 +2020-06-19 02:00:00,72.72,19.19,36.905,29.28 +2020-06-19 02:15:00,76.55,17.572,36.905,29.28 +2020-06-19 02:30:00,75.24,20.678,36.905,29.28 +2020-06-19 02:45:00,72.74,20.775,36.905,29.28 +2020-06-19 03:00:00,70.27,23.093000000000004,37.1,29.28 +2020-06-19 03:15:00,72.1,20.705,37.1,29.28 +2020-06-19 03:30:00,71.55,19.752,37.1,29.28 +2020-06-19 03:45:00,73.4,20.027,37.1,29.28 +2020-06-19 04:00:00,75.24,26.611,37.882,29.28 +2020-06-19 04:15:00,77.24,32.875,37.882,29.28 +2020-06-19 04:30:00,79.93,31.11,37.882,29.28 +2020-06-19 04:45:00,81.23,30.662,37.882,29.28 +2020-06-19 05:00:00,90.71,45.931000000000004,40.777,29.28 +2020-06-19 05:15:00,98.92,57.556000000000004,40.777,29.28 +2020-06-19 05:30:00,104.38,49.518,40.777,29.28 +2020-06-19 05:45:00,106.32,45.928999999999995,40.777,29.28 +2020-06-19 06:00:00,107.95,48.331,55.528,29.28 +2020-06-19 06:15:00,110.49,48.743,55.528,29.28 +2020-06-19 06:30:00,109.88,46.794,55.528,29.28 +2020-06-19 06:45:00,113.13,47.756,55.528,29.28 +2020-06-19 07:00:00,124.83,49.074,67.749,29.28 +2020-06-19 07:15:00,124.36,49.885,67.749,29.28 +2020-06-19 07:30:00,123.58,45.167,67.749,29.28 +2020-06-19 07:45:00,117.62,44.401,67.749,29.28 +2020-06-19 08:00:00,115.83,42.183,57.55,29.28 +2020-06-19 08:15:00,118.6,45.393,57.55,29.28 +2020-06-19 08:30:00,123.82,45.49100000000001,57.55,29.28 +2020-06-19 08:45:00,128.81,47.595,57.55,29.28 +2020-06-19 09:00:00,126.71,40.461999999999996,52.588,29.28 +2020-06-19 09:15:00,114.96,41.42100000000001,52.588,29.28 +2020-06-19 09:30:00,122.02,44.286,52.588,29.28 +2020-06-19 09:45:00,127.81,47.428999999999995,52.588,29.28 +2020-06-19 10:00:00,122.67,41.851000000000006,49.772,29.28 +2020-06-19 10:15:00,122.54,43.483000000000004,49.772,29.28 +2020-06-19 10:30:00,122.79,44.187,49.772,29.28 +2020-06-19 10:45:00,121.18,45.756,49.772,29.28 +2020-06-19 11:00:00,122.52,42.196999999999996,49.226000000000006,29.28 +2020-06-19 11:15:00,124.31,42.438,49.226000000000006,29.28 +2020-06-19 11:30:00,125.71,43.64,49.226000000000006,29.28 +2020-06-19 11:45:00,123.32,44.515,49.226000000000006,29.28 +2020-06-19 12:00:00,114.63,41.446999999999996,45.705,29.28 +2020-06-19 12:15:00,122.04,40.604,45.705,29.28 +2020-06-19 12:30:00,117.45,39.614000000000004,45.705,29.28 +2020-06-19 12:45:00,117.38,40.341,45.705,29.28 +2020-06-19 13:00:00,106.5,41.613,43.133,29.28 +2020-06-19 13:15:00,107.87,43.023,43.133,29.28 +2020-06-19 13:30:00,118.64,42.123999999999995,43.133,29.28 +2020-06-19 13:45:00,120.24,41.25,43.133,29.28 +2020-06-19 14:00:00,114.0,42.461999999999996,41.989,29.28 +2020-06-19 14:15:00,101.38,41.568000000000005,41.989,29.28 +2020-06-19 14:30:00,104.98,41.913000000000004,41.989,29.28 +2020-06-19 14:45:00,100.54,41.845,41.989,29.28 +2020-06-19 15:00:00,102.98,43.575,43.728,29.28 +2020-06-19 15:15:00,95.22,40.839,43.728,29.28 +2020-06-19 15:30:00,100.15,38.366,43.728,29.28 +2020-06-19 15:45:00,103.04,37.007,43.728,29.28 +2020-06-19 16:00:00,102.24,38.91,45.93899999999999,29.28 +2020-06-19 16:15:00,97.18,38.972,45.93899999999999,29.28 +2020-06-19 16:30:00,99.11,38.058,45.93899999999999,29.28 +2020-06-19 16:45:00,103.2,33.641999999999996,45.93899999999999,29.28 +2020-06-19 17:00:00,108.34,38.445,50.488,29.28 +2020-06-19 17:15:00,102.87,38.21,50.488,29.28 +2020-06-19 17:30:00,103.68,37.475,50.488,29.28 +2020-06-19 17:45:00,106.28,35.653,50.488,29.28 +2020-06-19 18:00:00,106.78,38.35,52.408,29.28 +2020-06-19 18:15:00,100.21,37.504,52.408,29.28 +2020-06-19 18:30:00,105.97,35.091,52.408,29.28 +2020-06-19 18:45:00,105.56,39.607,52.408,29.28 +2020-06-19 19:00:00,102.94,41.207,52.736000000000004,29.28 +2020-06-19 19:15:00,96.06,40.876999999999995,52.736000000000004,29.28 +2020-06-19 19:30:00,98.92,39.952,52.736000000000004,29.28 +2020-06-19 19:45:00,97.55,38.978,52.736000000000004,29.28 +2020-06-19 20:00:00,95.78,36.455999999999996,59.68,29.28 +2020-06-19 20:15:00,97.37,37.21,59.68,29.28 +2020-06-19 20:30:00,92.83,37.177,59.68,29.28 +2020-06-19 20:45:00,91.07,37.179,59.68,29.28 +2020-06-19 21:00:00,94.6,36.84,54.343999999999994,29.28 +2020-06-19 21:15:00,92.39,40.45,54.343999999999994,29.28 +2020-06-19 21:30:00,85.76,41.243,54.343999999999994,29.28 +2020-06-19 21:45:00,83.91,42.629,54.343999999999994,29.28 +2020-06-19 22:00:00,82.56,40.035,49.672,29.28 +2020-06-19 22:15:00,84.43,41.754,49.672,29.28 +2020-06-19 22:30:00,80.48,41.489,49.672,29.28 +2020-06-19 22:45:00,74.58,39.236,49.672,29.28 +2020-06-19 23:00:00,68.57,36.621,42.065,29.28 +2020-06-19 23:15:00,75.75,33.433,42.065,29.28 +2020-06-19 23:30:00,75.04,30.408,42.065,29.28 +2020-06-19 23:45:00,71.81,29.502,42.065,29.28 +2020-06-20 00:00:00,66.09,23.177,38.829,29.17 +2020-06-20 00:15:00,61.95,23.165,38.829,29.17 +2020-06-20 00:30:00,69.46,21.93,38.829,29.17 +2020-06-20 00:45:00,70.54,21.221,38.829,29.17 +2020-06-20 01:00:00,69.8,21.002,34.63,29.17 +2020-06-20 01:15:00,64.85,20.109,34.63,29.17 +2020-06-20 01:30:00,62.76,18.302,34.63,29.17 +2020-06-20 01:45:00,68.84,18.891,34.63,29.17 +2020-06-20 02:00:00,66.79,18.442,32.465,29.17 +2020-06-20 02:15:00,62.57,16.019000000000002,32.465,29.17 +2020-06-20 02:30:00,62.89,18.262,32.465,29.17 +2020-06-20 02:45:00,62.21,19.153,32.465,29.17 +2020-06-20 03:00:00,63.7,20.147000000000002,31.925,29.17 +2020-06-20 03:15:00,68.4,16.994,31.925,29.17 +2020-06-20 03:30:00,69.28,16.274,31.925,29.17 +2020-06-20 03:45:00,66.18,18.046,31.925,29.17 +2020-06-20 04:00:00,64.28,22.455,31.309,29.17 +2020-06-20 04:15:00,67.18,27.634,31.309,29.17 +2020-06-20 04:30:00,67.69,24.068,31.309,29.17 +2020-06-20 04:45:00,65.96,23.858,31.309,29.17 +2020-06-20 05:00:00,64.27,29.932,30.323,29.17 +2020-06-20 05:15:00,63.38,29.461,30.323,29.17 +2020-06-20 05:30:00,66.59,23.021,30.323,29.17 +2020-06-20 05:45:00,73.18,24.226,30.323,29.17 +2020-06-20 06:00:00,77.68,38.798,31.438000000000002,29.17 +2020-06-20 06:15:00,75.5,47.883,31.438000000000002,29.17 +2020-06-20 06:30:00,73.46,42.826,31.438000000000002,29.17 +2020-06-20 06:45:00,70.8,40.108000000000004,31.438000000000002,29.17 +2020-06-20 07:00:00,78.74,39.754,34.891999999999996,29.17 +2020-06-20 07:15:00,82.01,39.227,34.891999999999996,29.17 +2020-06-20 07:30:00,82.18,36.159,34.891999999999996,29.17 +2020-06-20 07:45:00,82.18,36.577,34.891999999999996,29.17 +2020-06-20 08:00:00,80.59,35.275,39.608000000000004,29.17 +2020-06-20 08:15:00,79.69,38.524,39.608000000000004,29.17 +2020-06-20 08:30:00,85.25,38.698,39.608000000000004,29.17 +2020-06-20 08:45:00,92.26,41.931000000000004,39.608000000000004,29.17 +2020-06-20 09:00:00,94.83,37.946,40.894,29.17 +2020-06-20 09:15:00,91.28,39.433,40.894,29.17 +2020-06-20 09:30:00,90.28,42.839,40.894,29.17 +2020-06-20 09:45:00,90.39,45.566,40.894,29.17 +2020-06-20 10:00:00,96.46,40.611,39.525,29.17 +2020-06-20 10:15:00,102.27,42.614,39.525,29.17 +2020-06-20 10:30:00,103.51,42.993,39.525,29.17 +2020-06-20 10:45:00,99.42,44.306000000000004,39.525,29.17 +2020-06-20 11:00:00,94.12,40.628,36.718,29.17 +2020-06-20 11:15:00,91.99,41.68600000000001,36.718,29.17 +2020-06-20 11:30:00,90.43,43.083999999999996,36.718,29.17 +2020-06-20 11:45:00,91.18,44.558,36.718,29.17 +2020-06-20 12:00:00,88.9,41.86600000000001,35.688,29.17 +2020-06-20 12:15:00,87.4,41.9,35.688,29.17 +2020-06-20 12:30:00,86.31,40.782,35.688,29.17 +2020-06-20 12:45:00,85.13,42.178999999999995,35.688,29.17 +2020-06-20 13:00:00,84.77,42.562,32.858000000000004,29.17 +2020-06-20 13:15:00,85.22,43.309,32.858000000000004,29.17 +2020-06-20 13:30:00,84.09,42.575,32.858000000000004,29.17 +2020-06-20 13:45:00,82.67,40.41,32.858000000000004,29.17 +2020-06-20 14:00:00,81.43,41.786,31.738000000000003,29.17 +2020-06-20 14:15:00,81.85,39.626,31.738000000000003,29.17 +2020-06-20 14:30:00,81.06,39.414,31.738000000000003,29.17 +2020-06-20 14:45:00,82.67,39.839,31.738000000000003,29.17 +2020-06-20 15:00:00,82.96,42.092,34.35,29.17 +2020-06-20 15:15:00,80.52,40.094,34.35,29.17 +2020-06-20 15:30:00,81.22,37.909,34.35,29.17 +2020-06-20 15:45:00,82.66,35.633,34.35,29.17 +2020-06-20 16:00:00,85.43,39.605,37.522,29.17 +2020-06-20 16:15:00,84.06,38.836999999999996,37.522,29.17 +2020-06-20 16:30:00,83.74,38.161,37.522,29.17 +2020-06-20 16:45:00,84.53,33.751999999999995,37.522,29.17 +2020-06-20 17:00:00,86.97,37.381,42.498000000000005,29.17 +2020-06-20 17:15:00,87.28,35.118,42.498000000000005,29.17 +2020-06-20 17:30:00,88.27,34.24,42.498000000000005,29.17 +2020-06-20 17:45:00,87.85,32.866,42.498000000000005,29.17 +2020-06-20 18:00:00,88.24,36.915,44.701,29.17 +2020-06-20 18:15:00,86.6,37.805,44.701,29.17 +2020-06-20 18:30:00,88.94,36.808,44.701,29.17 +2020-06-20 18:45:00,84.9,37.689,44.701,29.17 +2020-06-20 19:00:00,81.63,37.759,45.727,29.17 +2020-06-20 19:15:00,76.92,36.394,45.727,29.17 +2020-06-20 19:30:00,78.37,36.266999999999996,45.727,29.17 +2020-06-20 19:45:00,75.8,37.018,45.727,29.17 +2020-06-20 20:00:00,74.14,35.438,43.391000000000005,29.17 +2020-06-20 20:15:00,71.76,35.691,43.391000000000005,29.17 +2020-06-20 20:30:00,72.4,34.777,43.391000000000005,29.17 +2020-06-20 20:45:00,71.24,36.609,43.391000000000005,29.17 +2020-06-20 21:00:00,70.11,34.961,41.231,29.17 +2020-06-20 21:15:00,68.91,38.260999999999996,41.231,29.17 +2020-06-20 21:30:00,66.55,39.293,41.231,29.17 +2020-06-20 21:45:00,64.82,40.121,41.231,29.17 +2020-06-20 22:00:00,64.11,37.602,40.798,29.17 +2020-06-20 22:15:00,62.69,39.724000000000004,40.798,29.17 +2020-06-20 22:30:00,62.16,39.457,40.798,29.17 +2020-06-20 22:45:00,58.77,37.674,40.798,29.17 +2020-06-20 23:00:00,54.05,34.529,34.402,29.17 +2020-06-20 23:15:00,54.66,31.689,34.402,29.17 +2020-06-20 23:30:00,53.59,30.991,34.402,29.17 +2020-06-20 23:45:00,53.51,30.281999999999996,34.402,29.17 +2020-06-21 00:00:00,51.37,24.479,30.171,29.17 +2020-06-21 00:15:00,51.6,23.31,30.171,29.17 +2020-06-21 00:30:00,51.28,21.910999999999998,30.171,29.17 +2020-06-21 00:45:00,50.78,21.142,30.171,29.17 +2020-06-21 01:00:00,47.84,21.191,27.15,29.17 +2020-06-21 01:15:00,50.1,20.225,27.15,29.17 +2020-06-21 01:30:00,49.76,18.312,27.15,29.17 +2020-06-21 01:45:00,49.33,18.498,27.15,29.17 +2020-06-21 02:00:00,48.37,18.084,25.403000000000002,29.17 +2020-06-21 02:15:00,48.73,16.308,25.403000000000002,29.17 +2020-06-21 02:30:00,48.89,18.782,25.403000000000002,29.17 +2020-06-21 02:45:00,48.92,19.419,25.403000000000002,29.17 +2020-06-21 03:00:00,48.71,21.072,23.386999999999997,29.17 +2020-06-21 03:15:00,49.4,18.149,23.386999999999997,29.17 +2020-06-21 03:30:00,50.71,16.795,23.386999999999997,29.17 +2020-06-21 03:45:00,50.44,17.798,23.386999999999997,29.17 +2020-06-21 04:00:00,49.72,22.112,23.941999999999997,29.17 +2020-06-21 04:15:00,50.86,26.76,23.941999999999997,29.17 +2020-06-21 04:30:00,51.2,24.508000000000003,23.941999999999997,29.17 +2020-06-21 04:45:00,52.17,23.851999999999997,23.941999999999997,29.17 +2020-06-21 05:00:00,52.67,29.799,23.026,29.17 +2020-06-21 05:15:00,53.27,28.311,23.026,29.17 +2020-06-21 05:30:00,54.64,21.53,23.026,29.17 +2020-06-21 05:45:00,56.86,22.523000000000003,23.026,29.17 +2020-06-21 06:00:00,58.47,34.766999999999996,23.223000000000003,29.17 +2020-06-21 06:15:00,59.86,44.56100000000001,23.223000000000003,29.17 +2020-06-21 06:30:00,61.63,38.775,23.223000000000003,29.17 +2020-06-21 06:45:00,63.3,35.046,23.223000000000003,29.17 +2020-06-21 07:00:00,66.04,35.114000000000004,24.968000000000004,29.17 +2020-06-21 07:15:00,66.82,32.942,24.968000000000004,29.17 +2020-06-21 07:30:00,67.37,31.054000000000002,24.968000000000004,29.17 +2020-06-21 07:45:00,67.58,31.428,24.968000000000004,29.17 +2020-06-21 08:00:00,66.37,30.988000000000003,29.131,29.17 +2020-06-21 08:15:00,68.59,35.423,29.131,29.17 +2020-06-21 08:30:00,65.93,36.542,29.131,29.17 +2020-06-21 08:45:00,64.07,39.815,29.131,29.17 +2020-06-21 09:00:00,66.26,35.691,29.904,29.17 +2020-06-21 09:15:00,65.59,36.727,29.904,29.17 +2020-06-21 09:30:00,63.46,40.547,29.904,29.17 +2020-06-21 09:45:00,64.99,44.266000000000005,29.904,29.17 +2020-06-21 10:00:00,69.22,39.995,28.943,29.17 +2020-06-21 10:15:00,70.76,42.17,28.943,29.17 +2020-06-21 10:30:00,72.73,42.803000000000004,28.943,29.17 +2020-06-21 10:45:00,73.48,45.013999999999996,28.943,29.17 +2020-06-21 11:00:00,70.43,41.044,31.682,29.17 +2020-06-21 11:15:00,67.81,41.674,31.682,29.17 +2020-06-21 11:30:00,68.94,43.528999999999996,31.682,29.17 +2020-06-21 11:45:00,70.2,45.287,31.682,29.17 +2020-06-21 12:00:00,72.15,43.645,27.315,29.17 +2020-06-21 12:15:00,71.91,43.158,27.315,29.17 +2020-06-21 12:30:00,70.77,42.159,27.315,29.17 +2020-06-21 12:45:00,71.96,42.878,27.315,29.17 +2020-06-21 13:00:00,67.38,42.913000000000004,23.894000000000002,29.17 +2020-06-21 13:15:00,68.77,43.246,23.894000000000002,29.17 +2020-06-21 13:30:00,67.21,41.412,23.894000000000002,29.17 +2020-06-21 13:45:00,64.71,40.32,23.894000000000002,29.17 +2020-06-21 14:00:00,59.95,42.907,21.148000000000003,29.17 +2020-06-21 14:15:00,61.83,41.231,21.148000000000003,29.17 +2020-06-21 14:30:00,64.05,39.84,21.148000000000003,29.17 +2020-06-21 14:45:00,66.51,39.191,21.148000000000003,29.17 +2020-06-21 15:00:00,65.17,41.54,21.229,29.17 +2020-06-21 15:15:00,65.5,38.769,21.229,29.17 +2020-06-21 15:30:00,67.43,36.427,21.229,29.17 +2020-06-21 15:45:00,68.84,34.457,21.229,29.17 +2020-06-21 16:00:00,69.3,36.935,25.037,29.17 +2020-06-21 16:15:00,70.82,36.345,25.037,29.17 +2020-06-21 16:30:00,76.2,36.694,25.037,29.17 +2020-06-21 16:45:00,75.99,32.335,25.037,29.17 +2020-06-21 17:00:00,79.94,36.341,37.11,29.17 +2020-06-21 17:15:00,79.54,35.556,37.11,29.17 +2020-06-21 17:30:00,79.33,35.486,37.11,29.17 +2020-06-21 17:45:00,79.29,34.625,37.11,29.17 +2020-06-21 18:00:00,82.67,39.243,42.215,29.17 +2020-06-21 18:15:00,79.05,39.774,42.215,29.17 +2020-06-21 18:30:00,78.46,38.423,42.215,29.17 +2020-06-21 18:45:00,78.31,39.51,42.215,29.17 +2020-06-21 19:00:00,79.89,41.713,44.383,29.17 +2020-06-21 19:15:00,78.41,39.275,44.383,29.17 +2020-06-21 19:30:00,76.51,38.885999999999996,44.383,29.17 +2020-06-21 19:45:00,76.7,39.255,44.383,29.17 +2020-06-21 20:00:00,77.36,37.827,43.426,29.17 +2020-06-21 20:15:00,76.94,37.992,43.426,29.17 +2020-06-21 20:30:00,78.36,37.949,43.426,29.17 +2020-06-21 20:45:00,78.33,38.084,43.426,29.17 +2020-06-21 21:00:00,77.45,36.18,42.265,29.17 +2020-06-21 21:15:00,76.37,39.157,42.265,29.17 +2020-06-21 21:30:00,74.61,39.539,42.265,29.17 +2020-06-21 21:45:00,74.02,40.735,42.265,29.17 +2020-06-21 22:00:00,72.94,40.181,42.26,29.17 +2020-06-21 22:15:00,71.13,40.609,42.26,29.17 +2020-06-21 22:30:00,69.5,39.675,42.26,29.17 +2020-06-21 22:45:00,73.05,36.58,42.26,29.17 +2020-06-21 23:00:00,65.27,32.983000000000004,36.609,29.17 +2020-06-21 23:15:00,66.55,31.525,36.609,29.17 +2020-06-21 23:30:00,65.95,30.346,36.609,29.17 +2020-06-21 23:45:00,64.51,29.828000000000003,36.609,29.17 +2020-06-22 00:00:00,62.06,26.113000000000003,34.611,29.28 +2020-06-22 00:15:00,63.43,25.878,34.611,29.28 +2020-06-22 00:30:00,63.24,24.111,34.611,29.28 +2020-06-22 00:45:00,62.85,22.93,34.611,29.28 +2020-06-22 01:00:00,61.06,23.374000000000002,33.552,29.28 +2020-06-22 01:15:00,62.06,22.379,33.552,29.28 +2020-06-22 01:30:00,62.07,20.820999999999998,33.552,29.28 +2020-06-22 01:45:00,62.34,20.910999999999998,33.552,29.28 +2020-06-22 02:00:00,61.38,20.945,32.351,29.28 +2020-06-22 02:15:00,62.36,18.246,32.351,29.28 +2020-06-22 02:30:00,62.99,20.894000000000002,32.351,29.28 +2020-06-22 02:45:00,69.91,21.36,32.351,29.28 +2020-06-22 03:00:00,71.83,23.574,30.793000000000003,29.28 +2020-06-22 03:15:00,74.38,21.448,30.793000000000003,29.28 +2020-06-22 03:30:00,68.44,20.76,30.793000000000003,29.28 +2020-06-22 03:45:00,72.17,21.3,30.793000000000003,29.28 +2020-06-22 04:00:00,73.67,28.855999999999998,31.274,29.28 +2020-06-22 04:15:00,81.48,36.568000000000005,31.274,29.28 +2020-06-22 04:30:00,85.31,33.944,31.274,29.28 +2020-06-22 04:45:00,87.89,33.66,31.274,29.28 +2020-06-22 05:00:00,90.15,47.393,37.75,29.28 +2020-06-22 05:15:00,92.28,57.013000000000005,37.75,29.28 +2020-06-22 05:30:00,98.06,48.794,37.75,29.28 +2020-06-22 05:45:00,104.24,46.501000000000005,37.75,29.28 +2020-06-22 06:00:00,114.07,47.684,55.36,29.28 +2020-06-22 06:15:00,113.59,47.718999999999994,55.36,29.28 +2020-06-22 06:30:00,120.64,46.295,55.36,29.28 +2020-06-22 06:45:00,117.35,48.265,55.36,29.28 +2020-06-22 07:00:00,125.32,48.82,65.87,29.28 +2020-06-22 07:15:00,124.38,48.982,65.87,29.28 +2020-06-22 07:30:00,123.8,46.201,65.87,29.28 +2020-06-22 07:45:00,120.42,46.763999999999996,65.87,29.28 +2020-06-22 08:00:00,127.99,43.946999999999996,55.695,29.28 +2020-06-22 08:15:00,124.81,47.11,55.695,29.28 +2020-06-22 08:30:00,122.97,47.053000000000004,55.695,29.28 +2020-06-22 08:45:00,126.89,50.353,55.695,29.28 +2020-06-22 09:00:00,125.76,45.153999999999996,50.881,29.28 +2020-06-22 09:15:00,128.65,44.243,50.881,29.28 +2020-06-22 09:30:00,129.68,47.093,50.881,29.28 +2020-06-22 09:45:00,120.54,48.583999999999996,50.881,29.28 +2020-06-22 10:00:00,114.39,44.678999999999995,49.138000000000005,29.28 +2020-06-22 10:15:00,122.56,46.699,49.138000000000005,29.28 +2020-06-22 10:30:00,122.96,46.832,49.138000000000005,29.28 +2020-06-22 10:45:00,125.29,47.528,49.138000000000005,29.28 +2020-06-22 11:00:00,120.65,43.623000000000005,49.178000000000004,29.28 +2020-06-22 11:15:00,114.96,44.631,49.178000000000004,29.28 +2020-06-22 11:30:00,112.14,47.236000000000004,49.178000000000004,29.28 +2020-06-22 11:45:00,104.36,49.468999999999994,49.178000000000004,29.28 +2020-06-22 12:00:00,104.38,46.236000000000004,47.698,29.28 +2020-06-22 12:15:00,103.64,45.858000000000004,47.698,29.28 +2020-06-22 12:30:00,103.64,43.793,47.698,29.28 +2020-06-22 12:45:00,102.45,44.586999999999996,47.698,29.28 +2020-06-22 13:00:00,96.63,45.56100000000001,48.104,29.28 +2020-06-22 13:15:00,99.66,44.949,48.104,29.28 +2020-06-22 13:30:00,103.52,43.265,48.104,29.28 +2020-06-22 13:45:00,107.28,43.06399999999999,48.104,29.28 +2020-06-22 14:00:00,103.1,44.723,48.53,29.28 +2020-06-22 14:15:00,98.24,43.601000000000006,48.53,29.28 +2020-06-22 14:30:00,97.96,41.998999999999995,48.53,29.28 +2020-06-22 14:45:00,98.66,43.403,48.53,29.28 +2020-06-22 15:00:00,96.96,45.501000000000005,49.351000000000006,29.28 +2020-06-22 15:15:00,95.96,42.066,49.351000000000006,29.28 +2020-06-22 15:30:00,103.76,40.439,49.351000000000006,29.28 +2020-06-22 15:45:00,101.27,37.955999999999996,49.351000000000006,29.28 +2020-06-22 16:00:00,100.46,41.551,51.44,29.28 +2020-06-22 16:15:00,102.13,40.983999999999995,51.44,29.28 +2020-06-22 16:30:00,101.46,40.643,51.44,29.28 +2020-06-22 16:45:00,101.72,36.227,51.44,29.28 +2020-06-22 17:00:00,104.77,39.12,56.868,29.28 +2020-06-22 17:15:00,101.5,38.658,56.868,29.28 +2020-06-22 17:30:00,104.47,38.171,56.868,29.28 +2020-06-22 17:45:00,106.24,36.828,56.868,29.28 +2020-06-22 18:00:00,107.49,40.416,57.229,29.28 +2020-06-22 18:15:00,104.01,38.998000000000005,57.229,29.28 +2020-06-22 18:30:00,102.52,36.928000000000004,57.229,29.28 +2020-06-22 18:45:00,102.38,41.181000000000004,57.229,29.28 +2020-06-22 19:00:00,100.87,43.022,57.744,29.28 +2020-06-22 19:15:00,100.34,41.828,57.744,29.28 +2020-06-22 19:30:00,96.04,41.091,57.744,29.28 +2020-06-22 19:45:00,97.08,40.805,57.744,29.28 +2020-06-22 20:00:00,93.07,37.99,66.05199999999999,29.28 +2020-06-22 20:15:00,93.95,39.416,66.05199999999999,29.28 +2020-06-22 20:30:00,95.77,39.804,66.05199999999999,29.28 +2020-06-22 20:45:00,99.45,40.259,66.05199999999999,29.28 +2020-06-22 21:00:00,94.07,37.760999999999996,59.396,29.28 +2020-06-22 21:15:00,93.7,41.123999999999995,59.396,29.28 +2020-06-22 21:30:00,90.26,41.853,59.396,29.28 +2020-06-22 21:45:00,88.36,42.81,59.396,29.28 +2020-06-22 22:00:00,84.6,40.099000000000004,53.06,29.28 +2020-06-22 22:15:00,85.31,42.441,53.06,29.28 +2020-06-22 22:30:00,82.83,36.732,53.06,29.28 +2020-06-22 22:45:00,81.6,33.37,53.06,29.28 +2020-06-22 23:00:00,76.8,29.838,46.148,29.28 +2020-06-22 23:15:00,77.46,26.904,46.148,29.28 +2020-06-22 23:30:00,78.9,25.831999999999997,46.148,29.28 +2020-06-22 23:45:00,78.36,24.899,46.148,29.28 +2020-06-23 00:00:00,73.53,23.746,44.625,29.28 +2020-06-23 00:15:00,75.33,24.444000000000003,44.625,29.28 +2020-06-23 00:30:00,74.92,23.34,44.625,29.28 +2020-06-23 00:45:00,74.6,22.901,44.625,29.28 +2020-06-23 01:00:00,73.27,22.811999999999998,41.733000000000004,29.28 +2020-06-23 01:15:00,75.07,21.89,41.733000000000004,29.28 +2020-06-23 01:30:00,73.73,20.209,41.733000000000004,29.28 +2020-06-23 01:45:00,74.21,19.797,41.733000000000004,29.28 +2020-06-23 02:00:00,73.89,19.355999999999998,39.872,29.28 +2020-06-23 02:15:00,73.43,17.804000000000002,39.872,29.28 +2020-06-23 02:30:00,80.85,20.026,39.872,29.28 +2020-06-23 02:45:00,82.26,20.824,39.872,29.28 +2020-06-23 03:00:00,78.93,22.423000000000002,38.711,29.28 +2020-06-23 03:15:00,75.46,21.189,38.711,29.28 +2020-06-23 03:30:00,78.81,20.5,38.711,29.28 +2020-06-23 03:45:00,76.4,19.961,38.711,29.28 +2020-06-23 04:00:00,84.82,26.201,39.823,29.28 +2020-06-23 04:15:00,90.66,33.869,39.823,29.28 +2020-06-23 04:30:00,92.85,31.098000000000003,39.823,29.28 +2020-06-23 04:45:00,92.53,31.357,39.823,29.28 +2020-06-23 05:00:00,96.07,46.823,43.228,29.28 +2020-06-23 05:15:00,98.92,56.934,43.228,29.28 +2020-06-23 05:30:00,106.98,48.886,43.228,29.28 +2020-06-23 05:45:00,113.0,45.909,43.228,29.28 +2020-06-23 06:00:00,118.57,48.091,54.316,29.28 +2020-06-23 06:15:00,120.37,48.391999999999996,54.316,29.28 +2020-06-23 06:30:00,119.5,46.638999999999996,54.316,29.28 +2020-06-23 06:45:00,123.82,47.731,54.316,29.28 +2020-06-23 07:00:00,124.88,48.418,65.758,29.28 +2020-06-23 07:15:00,122.94,48.336000000000006,65.758,29.28 +2020-06-23 07:30:00,120.91,45.54,65.758,29.28 +2020-06-23 07:45:00,125.1,45.174,65.758,29.28 +2020-06-23 08:00:00,122.4,42.302,57.983000000000004,29.28 +2020-06-23 08:15:00,118.74,44.994,57.983000000000004,29.28 +2020-06-23 08:30:00,118.28,45.086000000000006,57.983000000000004,29.28 +2020-06-23 08:45:00,122.7,47.455,57.983000000000004,29.28 +2020-06-23 09:00:00,122.76,42.547,52.653,29.28 +2020-06-23 09:15:00,122.01,41.583999999999996,52.653,29.28 +2020-06-23 09:30:00,120.53,45.15,52.653,29.28 +2020-06-23 09:45:00,118.95,47.977,52.653,29.28 +2020-06-23 10:00:00,130.1,42.687,51.408,29.28 +2020-06-23 10:15:00,123.77,44.523,51.408,29.28 +2020-06-23 10:30:00,123.38,44.691,51.408,29.28 +2020-06-23 10:45:00,116.88,46.407,51.408,29.28 +2020-06-23 11:00:00,116.32,42.581,51.913000000000004,29.28 +2020-06-23 11:15:00,111.31,43.983000000000004,51.913000000000004,29.28 +2020-06-23 11:30:00,115.49,45.371,51.913000000000004,29.28 +2020-06-23 11:45:00,107.36,47.188,51.913000000000004,29.28 +2020-06-23 12:00:00,107.78,43.729,49.508,29.28 +2020-06-23 12:15:00,112.97,43.67,49.508,29.28 +2020-06-23 12:30:00,115.46,42.479,49.508,29.28 +2020-06-23 12:45:00,111.06,43.953,49.508,29.28 +2020-06-23 13:00:00,110.37,44.533,50.007,29.28 +2020-06-23 13:15:00,106.59,45.705,50.007,29.28 +2020-06-23 13:30:00,108.56,44.053999999999995,50.007,29.28 +2020-06-23 13:45:00,117.29,42.903,50.007,29.28 +2020-06-23 14:00:00,107.17,45.04,49.778999999999996,29.28 +2020-06-23 14:15:00,109.31,43.724,49.778999999999996,29.28 +2020-06-23 14:30:00,107.16,42.458999999999996,49.778999999999996,29.28 +2020-06-23 14:45:00,97.6,43.075,49.778999999999996,29.28 +2020-06-23 15:00:00,108.88,45.023,51.559,29.28 +2020-06-23 15:15:00,109.4,42.522,51.559,29.28 +2020-06-23 15:30:00,106.06,40.688,51.559,29.28 +2020-06-23 15:45:00,108.71,38.524,51.559,29.28 +2020-06-23 16:00:00,110.94,41.452,53.531000000000006,29.28 +2020-06-23 16:15:00,110.74,40.99100000000001,53.531000000000006,29.28 +2020-06-23 16:30:00,113.66,40.326,53.531000000000006,29.28 +2020-06-23 16:45:00,109.97,36.621,53.531000000000006,29.28 +2020-06-23 17:00:00,116.03,39.773,59.497,29.28 +2020-06-23 17:15:00,115.14,39.727,59.497,29.28 +2020-06-23 17:30:00,117.22,38.826,59.497,29.28 +2020-06-23 17:45:00,112.66,37.13,59.497,29.28 +2020-06-23 18:00:00,114.14,39.775,59.861999999999995,29.28 +2020-06-23 18:15:00,114.54,39.8,59.861999999999995,29.28 +2020-06-23 18:30:00,116.69,37.45,59.861999999999995,29.28 +2020-06-23 18:45:00,112.08,41.543,59.861999999999995,29.28 +2020-06-23 19:00:00,104.05,42.224,60.989,29.28 +2020-06-23 19:15:00,100.72,41.187,60.989,29.28 +2020-06-23 19:30:00,98.01,40.196999999999996,60.989,29.28 +2020-06-23 19:45:00,97.06,40.241,60.989,29.28 +2020-06-23 20:00:00,96.0,37.812,68.35600000000001,29.28 +2020-06-23 20:15:00,96.1,37.766,68.35600000000001,29.28 +2020-06-23 20:30:00,95.0,38.244,68.35600000000001,29.28 +2020-06-23 20:45:00,98.48,39.116,68.35600000000001,29.28 +2020-06-23 21:00:00,95.1,37.458,59.251000000000005,29.28 +2020-06-23 21:15:00,93.78,39.403,59.251000000000005,29.28 +2020-06-23 21:30:00,89.52,40.253,59.251000000000005,29.28 +2020-06-23 21:45:00,88.7,41.42,59.251000000000005,29.28 +2020-06-23 22:00:00,83.01,38.971,54.736999999999995,29.28 +2020-06-23 22:15:00,84.33,40.971000000000004,54.736999999999995,29.28 +2020-06-23 22:30:00,82.74,35.55,54.736999999999995,29.28 +2020-06-23 22:45:00,81.15,32.186,54.736999999999995,29.28 +2020-06-23 23:00:00,78.29,27.874000000000002,46.806999999999995,29.28 +2020-06-23 23:15:00,77.68,26.494,46.806999999999995,29.28 +2020-06-23 23:30:00,76.77,25.414,46.806999999999995,29.28 +2020-06-23 23:45:00,75.36,24.633000000000003,46.806999999999995,29.28 +2020-06-24 00:00:00,72.75,23.675,43.824,29.28 +2020-06-24 00:15:00,73.59,24.374000000000002,43.824,29.28 +2020-06-24 00:30:00,73.6,23.273000000000003,43.824,29.28 +2020-06-24 00:45:00,73.92,22.840999999999998,43.824,29.28 +2020-06-24 01:00:00,72.73,22.764,39.86,29.28 +2020-06-24 01:15:00,73.28,21.828000000000003,39.86,29.28 +2020-06-24 01:30:00,72.51,20.145,39.86,29.28 +2020-06-24 01:45:00,73.33,19.726,39.86,29.28 +2020-06-24 02:00:00,77.46,19.287,37.931999999999995,29.28 +2020-06-24 02:15:00,82.15,17.739,37.931999999999995,29.28 +2020-06-24 02:30:00,81.13,19.952,37.931999999999995,29.28 +2020-06-24 02:45:00,74.48,20.756999999999998,37.931999999999995,29.28 +2020-06-24 03:00:00,77.13,22.351999999999997,37.579,29.28 +2020-06-24 03:15:00,76.09,21.122,37.579,29.28 +2020-06-24 03:30:00,76.54,20.439,37.579,29.28 +2020-06-24 03:45:00,76.21,19.92,37.579,29.28 +2020-06-24 04:00:00,84.58,26.114,37.931999999999995,29.28 +2020-06-24 04:15:00,88.31,33.742,37.931999999999995,29.28 +2020-06-24 04:30:00,92.93,30.961,37.931999999999995,29.28 +2020-06-24 04:45:00,91.92,31.218000000000004,37.931999999999995,29.28 +2020-06-24 05:00:00,95.34,46.599,40.942,29.28 +2020-06-24 05:15:00,98.24,56.599,40.942,29.28 +2020-06-24 05:30:00,101.16,48.613,40.942,29.28 +2020-06-24 05:45:00,103.36,45.676,40.942,29.28 +2020-06-24 06:00:00,111.68,47.864,56.516999999999996,29.28 +2020-06-24 06:15:00,116.87,48.153999999999996,56.516999999999996,29.28 +2020-06-24 06:30:00,116.87,46.42100000000001,56.516999999999996,29.28 +2020-06-24 06:45:00,117.03,47.542,56.516999999999996,29.28 +2020-06-24 07:00:00,116.69,48.217,71.707,29.28 +2020-06-24 07:15:00,116.48,48.155,71.707,29.28 +2020-06-24 07:30:00,116.84,45.356,71.707,29.28 +2020-06-24 07:45:00,112.07,45.023,71.707,29.28 +2020-06-24 08:00:00,109.37,42.161,61.17,29.28 +2020-06-24 08:15:00,108.4,44.878,61.17,29.28 +2020-06-24 08:30:00,114.26,44.963,61.17,29.28 +2020-06-24 08:45:00,114.72,47.333,61.17,29.28 +2020-06-24 09:00:00,112.86,42.42,57.282,29.28 +2020-06-24 09:15:00,108.55,41.458999999999996,57.282,29.28 +2020-06-24 09:30:00,118.09,45.023,57.282,29.28 +2020-06-24 09:45:00,123.97,47.863,57.282,29.28 +2020-06-24 10:00:00,122.12,42.582,54.026,29.28 +2020-06-24 10:15:00,117.01,44.424,54.026,29.28 +2020-06-24 10:30:00,123.16,44.593,54.026,29.28 +2020-06-24 10:45:00,121.18,46.31399999999999,54.026,29.28 +2020-06-24 11:00:00,123.52,42.483999999999995,54.277,29.28 +2020-06-24 11:15:00,117.69,43.891000000000005,54.277,29.28 +2020-06-24 11:30:00,115.43,45.269,54.277,29.28 +2020-06-24 11:45:00,119.14,47.083999999999996,54.277,29.28 +2020-06-24 12:00:00,124.99,43.653,52.552,29.28 +2020-06-24 12:15:00,127.32,43.593,52.552,29.28 +2020-06-24 12:30:00,126.8,42.385,52.552,29.28 +2020-06-24 12:45:00,117.73,43.86,52.552,29.28 +2020-06-24 13:00:00,101.49,44.428999999999995,52.111999999999995,29.28 +2020-06-24 13:15:00,105.29,45.601000000000006,52.111999999999995,29.28 +2020-06-24 13:30:00,116.19,43.958,52.111999999999995,29.28 +2020-06-24 13:45:00,110.54,42.81100000000001,52.111999999999995,29.28 +2020-06-24 14:00:00,99.9,44.958999999999996,52.066,29.28 +2020-06-24 14:15:00,102.05,43.643,52.066,29.28 +2020-06-24 14:30:00,110.45,42.358000000000004,52.066,29.28 +2020-06-24 14:45:00,109.15,42.979,52.066,29.28 +2020-06-24 15:00:00,108.94,44.952,52.523999999999994,29.28 +2020-06-24 15:15:00,101.7,42.445,52.523999999999994,29.28 +2020-06-24 15:30:00,107.34,40.605,52.523999999999994,29.28 +2020-06-24 15:45:00,106.1,38.431999999999995,52.523999999999994,29.28 +2020-06-24 16:00:00,104.67,41.382,54.101000000000006,29.28 +2020-06-24 16:15:00,105.58,40.921,54.101000000000006,29.28 +2020-06-24 16:30:00,112.42,40.273,54.101000000000006,29.28 +2020-06-24 16:45:00,110.04,36.552,54.101000000000006,29.28 +2020-06-24 17:00:00,111.19,39.722,58.155,29.28 +2020-06-24 17:15:00,108.5,39.675,58.155,29.28 +2020-06-24 17:30:00,112.41,38.77,58.155,29.28 +2020-06-24 17:45:00,117.25,37.063,58.155,29.28 +2020-06-24 18:00:00,116.0,39.716,60.205,29.28 +2020-06-24 18:15:00,111.3,39.717,60.205,29.28 +2020-06-24 18:30:00,108.34,37.365,60.205,29.28 +2020-06-24 18:45:00,107.8,41.457,60.205,29.28 +2020-06-24 19:00:00,103.84,42.141000000000005,61.568999999999996,29.28 +2020-06-24 19:15:00,101.09,41.096000000000004,61.568999999999996,29.28 +2020-06-24 19:30:00,102.04,40.098,61.568999999999996,29.28 +2020-06-24 19:45:00,97.68,40.138000000000005,61.568999999999996,29.28 +2020-06-24 20:00:00,95.61,37.694,68.145,29.28 +2020-06-24 20:15:00,94.76,37.645,68.145,29.28 +2020-06-24 20:30:00,94.42,38.129,68.145,29.28 +2020-06-24 20:45:00,94.83,39.023,68.145,29.28 +2020-06-24 21:00:00,96.89,37.37,59.696000000000005,29.28 +2020-06-24 21:15:00,94.06,39.32,59.696000000000005,29.28 +2020-06-24 21:30:00,90.13,40.148,59.696000000000005,29.28 +2020-06-24 21:45:00,88.79,41.318000000000005,59.696000000000005,29.28 +2020-06-24 22:00:00,84.68,38.884,54.861999999999995,29.28 +2020-06-24 22:15:00,87.11,40.889,54.861999999999995,29.28 +2020-06-24 22:30:00,84.09,35.459,54.861999999999995,29.28 +2020-06-24 22:45:00,84.16,32.086,54.861999999999995,29.28 +2020-06-24 23:00:00,79.54,27.765,45.568000000000005,29.28 +2020-06-24 23:15:00,76.73,26.414,45.568000000000005,29.28 +2020-06-24 23:30:00,75.86,25.346,45.568000000000005,29.28 +2020-06-24 23:45:00,78.52,24.555999999999997,45.568000000000005,29.28 +2020-06-25 00:00:00,75.55,23.608,40.181,29.28 +2020-06-25 00:15:00,75.77,24.307,40.181,29.28 +2020-06-25 00:30:00,72.44,23.21,40.181,29.28 +2020-06-25 00:45:00,75.39,22.784000000000002,40.181,29.28 +2020-06-25 01:00:00,72.29,22.72,38.296,29.28 +2020-06-25 01:15:00,74.01,21.771,38.296,29.28 +2020-06-25 01:30:00,72.67,20.085,38.296,29.28 +2020-06-25 01:45:00,72.74,19.66,38.296,29.28 +2020-06-25 02:00:00,73.25,19.224,36.575,29.28 +2020-06-25 02:15:00,74.48,17.678,36.575,29.28 +2020-06-25 02:30:00,74.61,19.884,36.575,29.28 +2020-06-25 02:45:00,74.51,20.693,36.575,29.28 +2020-06-25 03:00:00,75.05,22.285,36.394,29.28 +2020-06-25 03:15:00,76.2,21.059,36.394,29.28 +2020-06-25 03:30:00,83.01,20.384,36.394,29.28 +2020-06-25 03:45:00,85.84,19.883,36.394,29.28 +2020-06-25 04:00:00,80.84,26.031999999999996,37.207,29.28 +2020-06-25 04:15:00,81.65,33.622,37.207,29.28 +2020-06-25 04:30:00,84.58,30.829,37.207,29.28 +2020-06-25 04:45:00,88.67,31.086,37.207,29.28 +2020-06-25 05:00:00,97.63,46.383,40.713,29.28 +2020-06-25 05:15:00,98.52,56.276,40.713,29.28 +2020-06-25 05:30:00,107.02,48.352,40.713,29.28 +2020-06-25 05:45:00,107.47,45.452,40.713,29.28 +2020-06-25 06:00:00,115.01,47.646,50.952,29.28 +2020-06-25 06:15:00,111.77,47.926,50.952,29.28 +2020-06-25 06:30:00,117.15,46.214,50.952,29.28 +2020-06-25 06:45:00,118.68,47.363,50.952,29.28 +2020-06-25 07:00:00,115.38,48.026,64.88,29.28 +2020-06-25 07:15:00,114.08,47.983000000000004,64.88,29.28 +2020-06-25 07:30:00,113.71,45.181999999999995,64.88,29.28 +2020-06-25 07:45:00,114.99,44.88,64.88,29.28 +2020-06-25 08:00:00,113.05,42.027,55.133,29.28 +2020-06-25 08:15:00,112.54,44.771,55.133,29.28 +2020-06-25 08:30:00,110.54,44.848,55.133,29.28 +2020-06-25 08:45:00,116.65,47.22,55.133,29.28 +2020-06-25 09:00:00,118.67,42.302,48.912,29.28 +2020-06-25 09:15:00,119.01,41.343,48.912,29.28 +2020-06-25 09:30:00,116.77,44.903999999999996,48.912,29.28 +2020-06-25 09:45:00,122.25,47.756,48.912,29.28 +2020-06-25 10:00:00,123.53,42.483000000000004,45.968999999999994,29.28 +2020-06-25 10:15:00,129.33,44.333,45.968999999999994,29.28 +2020-06-25 10:30:00,122.87,44.501000000000005,45.968999999999994,29.28 +2020-06-25 10:45:00,119.98,46.227,45.968999999999994,29.28 +2020-06-25 11:00:00,120.33,42.395,44.067,29.28 +2020-06-25 11:15:00,118.71,43.806000000000004,44.067,29.28 +2020-06-25 11:30:00,112.17,45.172,44.067,29.28 +2020-06-25 11:45:00,113.06,46.986999999999995,44.067,29.28 +2020-06-25 12:00:00,123.41,43.581,41.501000000000005,29.28 +2020-06-25 12:15:00,127.98,43.521,41.501000000000005,29.28 +2020-06-25 12:30:00,129.51,42.29600000000001,41.501000000000005,29.28 +2020-06-25 12:45:00,125.72,43.772,41.501000000000005,29.28 +2020-06-25 13:00:00,119.23,44.33,41.117,29.28 +2020-06-25 13:15:00,120.12,45.5,41.117,29.28 +2020-06-25 13:30:00,125.64,43.867,41.117,29.28 +2020-06-25 13:45:00,128.35,42.726000000000006,41.117,29.28 +2020-06-25 14:00:00,128.81,44.882,41.492,29.28 +2020-06-25 14:15:00,125.95,43.567,41.492,29.28 +2020-06-25 14:30:00,120.48,42.263999999999996,41.492,29.28 +2020-06-25 14:45:00,120.88,42.89,41.492,29.28 +2020-06-25 15:00:00,123.63,44.88399999999999,43.711999999999996,29.28 +2020-06-25 15:15:00,121.05,42.37,43.711999999999996,29.28 +2020-06-25 15:30:00,122.08,40.525999999999996,43.711999999999996,29.28 +2020-06-25 15:45:00,120.83,38.344,43.711999999999996,29.28 +2020-06-25 16:00:00,127.83,41.317,45.446000000000005,29.28 +2020-06-25 16:15:00,122.67,40.853,45.446000000000005,29.28 +2020-06-25 16:30:00,122.29,40.224000000000004,45.446000000000005,29.28 +2020-06-25 16:45:00,126.06,36.488,45.446000000000005,29.28 +2020-06-25 17:00:00,123.94,39.675,48.803000000000004,29.28 +2020-06-25 17:15:00,123.31,39.628,48.803000000000004,29.28 +2020-06-25 17:30:00,120.34,38.719,48.803000000000004,29.28 +2020-06-25 17:45:00,114.21,37.001,48.803000000000004,29.28 +2020-06-25 18:00:00,118.99,39.661,51.167,29.28 +2020-06-25 18:15:00,118.56,39.641,51.167,29.28 +2020-06-25 18:30:00,119.87,37.286,51.167,29.28 +2020-06-25 18:45:00,112.47,41.376000000000005,51.167,29.28 +2020-06-25 19:00:00,107.94,42.06399999999999,52.486000000000004,29.28 +2020-06-25 19:15:00,104.55,41.011,52.486000000000004,29.28 +2020-06-25 19:30:00,102.42,40.006,52.486000000000004,29.28 +2020-06-25 19:45:00,103.35,40.04,52.486000000000004,29.28 +2020-06-25 20:00:00,97.55,37.582,59.635,29.28 +2020-06-25 20:15:00,97.58,37.531,59.635,29.28 +2020-06-25 20:30:00,98.76,38.021,59.635,29.28 +2020-06-25 20:45:00,98.12,38.935,59.635,29.28 +2020-06-25 21:00:00,96.92,37.287,54.353,29.28 +2020-06-25 21:15:00,92.89,39.243,54.353,29.28 +2020-06-25 21:30:00,90.17,40.049,54.353,29.28 +2020-06-25 21:45:00,88.89,41.221000000000004,54.353,29.28 +2020-06-25 22:00:00,85.17,38.8,49.431999999999995,29.28 +2020-06-25 22:15:00,84.56,40.81,49.431999999999995,29.28 +2020-06-25 22:30:00,81.54,35.371,49.431999999999995,29.28 +2020-06-25 22:45:00,80.23,31.987,49.431999999999995,29.28 +2020-06-25 23:00:00,78.24,27.659000000000002,42.872,29.28 +2020-06-25 23:15:00,77.2,26.339000000000002,42.872,29.28 +2020-06-25 23:30:00,76.09,25.281999999999996,42.872,29.28 +2020-06-25 23:45:00,74.95,24.483,42.872,29.28 +2020-06-26 00:00:00,73.91,21.691,39.819,29.28 +2020-06-26 00:15:00,73.66,22.618000000000002,39.819,29.28 +2020-06-26 00:30:00,73.5,21.787,39.819,29.28 +2020-06-26 00:45:00,73.41,21.796999999999997,39.819,29.28 +2020-06-26 01:00:00,72.61,21.344,37.797,29.28 +2020-06-26 01:15:00,73.69,19.842,37.797,29.28 +2020-06-26 01:30:00,73.2,18.852,37.797,29.28 +2020-06-26 01:45:00,73.02,18.176,37.797,29.28 +2020-06-26 02:00:00,77.1,18.678,36.905,29.28 +2020-06-26 02:15:00,81.05,17.077,36.905,29.28 +2020-06-26 02:30:00,79.47,20.13,36.905,29.28 +2020-06-26 02:45:00,73.65,20.267,36.905,29.28 +2020-06-26 03:00:00,76.43,22.564,37.1,29.28 +2020-06-26 03:15:00,75.3,20.2,37.1,29.28 +2020-06-26 03:30:00,76.44,19.292,37.1,29.28 +2020-06-26 03:45:00,77.02,19.705,37.1,29.28 +2020-06-26 04:00:00,77.12,25.962,37.882,29.28 +2020-06-26 04:15:00,84.19,31.944000000000003,37.882,29.28 +2020-06-26 04:30:00,91.07,30.104,37.882,29.28 +2020-06-26 04:45:00,88.15,29.641,37.882,29.28 +2020-06-26 05:00:00,93.96,44.298,40.777,29.28 +2020-06-26 05:15:00,96.44,55.133,40.777,29.28 +2020-06-26 05:30:00,99.55,47.538000000000004,40.777,29.28 +2020-06-26 05:45:00,102.21,44.23,40.777,29.28 +2020-06-26 06:00:00,105.49,46.681999999999995,55.528,29.28 +2020-06-26 06:15:00,106.56,47.016000000000005,55.528,29.28 +2020-06-26 06:30:00,111.38,45.214,55.528,29.28 +2020-06-26 06:45:00,113.94,46.373000000000005,55.528,29.28 +2020-06-26 07:00:00,119.24,47.61,67.749,29.28 +2020-06-26 07:15:00,120.18,48.556999999999995,67.749,29.28 +2020-06-26 07:30:00,118.22,43.81100000000001,67.749,29.28 +2020-06-26 07:45:00,111.2,43.27,67.749,29.28 +2020-06-26 08:00:00,111.58,41.121,57.55,29.28 +2020-06-26 08:15:00,114.37,44.523999999999994,57.55,29.28 +2020-06-26 08:30:00,114.99,44.568000000000005,57.55,29.28 +2020-06-26 08:45:00,113.74,46.685,57.55,29.28 +2020-06-26 09:00:00,109.16,39.516999999999996,52.588,29.28 +2020-06-26 09:15:00,113.26,40.493,52.588,29.28 +2020-06-26 09:30:00,112.71,43.34,52.588,29.28 +2020-06-26 09:45:00,110.41,46.576,52.588,29.28 +2020-06-26 10:00:00,105.72,41.063,49.772,29.28 +2020-06-26 10:15:00,112.85,42.751000000000005,49.772,29.28 +2020-06-26 10:30:00,112.6,43.458,49.772,29.28 +2020-06-26 10:45:00,111.95,45.062,49.772,29.28 +2020-06-26 11:00:00,106.15,41.48,49.226000000000006,29.28 +2020-06-26 11:15:00,102.95,41.75899999999999,49.226000000000006,29.28 +2020-06-26 11:30:00,108.99,42.881,49.226000000000006,29.28 +2020-06-26 11:45:00,107.29,43.751000000000005,49.226000000000006,29.28 +2020-06-26 12:00:00,102.93,40.873000000000005,45.705,29.28 +2020-06-26 12:15:00,97.81,40.031,45.705,29.28 +2020-06-26 12:30:00,99.61,38.917,45.705,29.28 +2020-06-26 12:45:00,101.18,39.65,45.705,29.28 +2020-06-26 13:00:00,99.27,40.851,43.133,29.28 +2020-06-26 13:15:00,96.97,42.258,43.133,29.28 +2020-06-26 13:30:00,91.8,41.42,43.133,29.28 +2020-06-26 13:45:00,90.58,40.583,43.133,29.28 +2020-06-26 14:00:00,89.92,41.871,41.989,29.28 +2020-06-26 14:15:00,91.04,40.973,41.989,29.28 +2020-06-26 14:30:00,96.86,41.178999999999995,41.989,29.28 +2020-06-26 14:45:00,96.54,41.148999999999994,41.989,29.28 +2020-06-26 15:00:00,96.12,43.053999999999995,43.728,29.28 +2020-06-26 15:15:00,92.58,40.266,43.728,29.28 +2020-06-26 15:30:00,92.76,37.751999999999995,43.728,29.28 +2020-06-26 15:45:00,99.14,36.326,43.728,29.28 +2020-06-26 16:00:00,102.55,38.402,45.93899999999999,29.28 +2020-06-26 16:15:00,102.02,38.446,45.93899999999999,29.28 +2020-06-26 16:30:00,97.83,37.661,45.93899999999999,29.28 +2020-06-26 16:45:00,104.14,33.126,45.93899999999999,29.28 +2020-06-26 17:00:00,107.5,38.062,50.488,29.28 +2020-06-26 17:15:00,108.79,37.815,50.488,29.28 +2020-06-26 17:30:00,102.7,37.045,50.488,29.28 +2020-06-26 17:45:00,108.93,35.137,50.488,29.28 +2020-06-26 18:00:00,110.72,37.895,52.408,29.28 +2020-06-26 18:15:00,106.92,36.888000000000005,52.408,29.28 +2020-06-26 18:30:00,100.88,34.454,52.408,29.28 +2020-06-26 18:45:00,99.23,38.957,52.408,29.28 +2020-06-26 19:00:00,92.97,40.580999999999996,52.736000000000004,29.28 +2020-06-26 19:15:00,93.69,40.194,52.736000000000004,29.28 +2020-06-26 19:30:00,98.71,39.216,52.736000000000004,29.28 +2020-06-26 19:45:00,97.1,38.205,52.736000000000004,29.28 +2020-06-26 20:00:00,93.03,35.582,59.68,29.28 +2020-06-26 20:15:00,87.14,36.32,59.68,29.28 +2020-06-26 20:30:00,89.87,36.326,59.68,29.28 +2020-06-26 20:45:00,87.72,36.488,59.68,29.28 +2020-06-26 21:00:00,86.85,36.183,54.343999999999994,29.28 +2020-06-26 21:15:00,86.7,39.836,54.343999999999994,29.28 +2020-06-26 21:30:00,87.8,40.472,54.343999999999994,29.28 +2020-06-26 21:45:00,87.75,41.882,54.343999999999994,29.28 +2020-06-26 22:00:00,84.36,39.395,49.672,29.28 +2020-06-26 22:15:00,81.7,41.153,49.672,29.28 +2020-06-26 22:30:00,76.68,40.829,49.672,29.28 +2020-06-26 22:45:00,74.71,38.507,49.672,29.28 +2020-06-26 23:00:00,71.08,35.825,42.065,29.28 +2020-06-26 23:15:00,71.04,32.855,42.065,29.28 +2020-06-26 23:30:00,66.37,29.904,42.065,29.28 +2020-06-26 23:45:00,69.55,28.935,42.065,29.28 +2020-06-27 00:00:00,65.06,22.680999999999997,38.829,29.17 +2020-06-27 00:15:00,66.26,22.67,38.829,29.17 +2020-06-27 00:30:00,67.1,21.462,38.829,29.17 +2020-06-27 00:45:00,66.18,20.795,38.829,29.17 +2020-06-27 01:00:00,63.32,20.668000000000003,34.63,29.17 +2020-06-27 01:15:00,71.54,19.678,34.63,29.17 +2020-06-27 01:30:00,69.12,17.852,34.63,29.17 +2020-06-27 01:45:00,70.08,18.394000000000002,34.63,29.17 +2020-06-27 02:00:00,62.8,17.964000000000002,32.465,29.17 +2020-06-27 02:15:00,62.11,15.561,32.465,29.17 +2020-06-27 02:30:00,65.37,17.749000000000002,32.465,29.17 +2020-06-27 02:45:00,68.43,18.676,32.465,29.17 +2020-06-27 03:00:00,68.92,19.649,31.925,29.17 +2020-06-27 03:15:00,68.83,16.522000000000002,31.925,29.17 +2020-06-27 03:30:00,65.01,15.847999999999999,31.925,29.17 +2020-06-27 03:45:00,60.82,17.756,31.925,29.17 +2020-06-27 04:00:00,61.0,21.845,31.309,29.17 +2020-06-27 04:15:00,68.68,26.747,31.309,29.17 +2020-06-27 04:30:00,68.89,23.107,31.309,29.17 +2020-06-27 04:45:00,67.89,22.884,31.309,29.17 +2020-06-27 05:00:00,63.25,28.361,30.323,29.17 +2020-06-27 05:15:00,62.79,27.12,30.323,29.17 +2020-06-27 05:30:00,67.71,21.116,30.323,29.17 +2020-06-27 05:45:00,71.77,22.594,30.323,29.17 +2020-06-27 06:00:00,75.97,37.208,31.438000000000002,29.17 +2020-06-27 06:15:00,71.83,46.221000000000004,31.438000000000002,29.17 +2020-06-27 06:30:00,70.47,41.309,31.438000000000002,29.17 +2020-06-27 06:45:00,72.75,38.786,31.438000000000002,29.17 +2020-06-27 07:00:00,76.92,38.353,34.891999999999996,29.17 +2020-06-27 07:15:00,77.53,37.963,34.891999999999996,29.17 +2020-06-27 07:30:00,75.96,34.871,34.891999999999996,29.17 +2020-06-27 07:45:00,74.03,35.513000000000005,34.891999999999996,29.17 +2020-06-27 08:00:00,75.68,34.279,39.608000000000004,29.17 +2020-06-27 08:15:00,76.83,37.715,39.608000000000004,29.17 +2020-06-27 08:30:00,81.14,37.835,39.608000000000004,29.17 +2020-06-27 08:45:00,76.55,41.08,39.608000000000004,29.17 +2020-06-27 09:00:00,72.19,37.061,40.894,29.17 +2020-06-27 09:15:00,77.15,38.563,40.894,29.17 +2020-06-27 09:30:00,77.14,41.949,40.894,29.17 +2020-06-27 09:45:00,75.67,44.763999999999996,40.894,29.17 +2020-06-27 10:00:00,75.84,39.874,39.525,29.17 +2020-06-27 10:15:00,73.35,41.927,39.525,29.17 +2020-06-27 10:30:00,70.92,42.309,39.525,29.17 +2020-06-27 10:45:00,72.03,43.653999999999996,39.525,29.17 +2020-06-27 11:00:00,67.93,39.957,36.718,29.17 +2020-06-27 11:15:00,67.39,41.049,36.718,29.17 +2020-06-27 11:30:00,68.26,42.368,36.718,29.17 +2020-06-27 11:45:00,65.04,43.833,36.718,29.17 +2020-06-27 12:00:00,61.59,41.327,35.688,29.17 +2020-06-27 12:15:00,61.46,41.361999999999995,35.688,29.17 +2020-06-27 12:30:00,59.79,40.125,35.688,29.17 +2020-06-27 12:45:00,61.07,41.525,35.688,29.17 +2020-06-27 13:00:00,56.5,41.835,32.858000000000004,29.17 +2020-06-27 13:15:00,57.59,42.577,32.858000000000004,29.17 +2020-06-27 13:30:00,57.29,41.902,32.858000000000004,29.17 +2020-06-27 13:45:00,57.27,39.775999999999996,32.858000000000004,29.17 +2020-06-27 14:00:00,56.82,41.222,31.738000000000003,29.17 +2020-06-27 14:15:00,57.12,39.06,31.738000000000003,29.17 +2020-06-27 14:30:00,57.83,38.715,31.738000000000003,29.17 +2020-06-27 14:45:00,60.56,39.176,31.738000000000003,29.17 +2020-06-27 15:00:00,59.67,41.595,34.35,29.17 +2020-06-27 15:15:00,59.31,39.548,34.35,29.17 +2020-06-27 15:30:00,60.2,37.325,34.35,29.17 +2020-06-27 15:45:00,61.46,34.984,34.35,29.17 +2020-06-27 16:00:00,62.44,39.122,37.522,29.17 +2020-06-27 16:15:00,63.64,38.339,37.522,29.17 +2020-06-27 16:30:00,66.31,37.792,37.522,29.17 +2020-06-27 16:45:00,67.86,33.27,37.522,29.17 +2020-06-27 17:00:00,70.65,37.027,42.498000000000005,29.17 +2020-06-27 17:15:00,71.24,34.755,42.498000000000005,29.17 +2020-06-27 17:30:00,73.98,33.844,42.498000000000005,29.17 +2020-06-27 17:45:00,75.04,32.39,42.498000000000005,29.17 +2020-06-27 18:00:00,77.44,36.498000000000005,44.701,29.17 +2020-06-27 18:15:00,76.21,37.229,44.701,29.17 +2020-06-27 18:30:00,76.3,36.213,44.701,29.17 +2020-06-27 18:45:00,76.3,37.082,44.701,29.17 +2020-06-27 19:00:00,76.28,37.175,45.727,29.17 +2020-06-27 19:15:00,73.26,35.755,45.727,29.17 +2020-06-27 19:30:00,72.4,35.575,45.727,29.17 +2020-06-27 19:45:00,70.94,36.29,45.727,29.17 +2020-06-27 20:00:00,71.82,34.611,43.391000000000005,29.17 +2020-06-27 20:15:00,71.23,34.848,43.391000000000005,29.17 +2020-06-27 20:30:00,71.26,33.97,43.391000000000005,29.17 +2020-06-27 20:45:00,72.04,35.955999999999996,43.391000000000005,29.17 +2020-06-27 21:00:00,74.06,34.341,41.231,29.17 +2020-06-27 21:15:00,74.57,37.683,41.231,29.17 +2020-06-27 21:30:00,70.48,38.56,41.231,29.17 +2020-06-27 21:45:00,70.41,39.407,41.231,29.17 +2020-06-27 22:00:00,62.13,36.991,40.798,29.17 +2020-06-27 22:15:00,64.93,39.149,40.798,29.17 +2020-06-27 22:30:00,60.41,38.819,40.798,29.17 +2020-06-27 22:45:00,62.8,36.968,40.798,29.17 +2020-06-27 23:00:00,59.35,33.760999999999996,34.402,29.17 +2020-06-27 23:15:00,59.05,31.136999999999997,34.402,29.17 +2020-06-27 23:30:00,58.0,30.514,34.402,29.17 +2020-06-27 23:45:00,57.81,29.741999999999997,34.402,29.17 +2020-06-28 00:00:00,52.34,24.013,30.171,29.17 +2020-06-28 00:15:00,55.36,22.846,30.171,29.17 +2020-06-28 00:30:00,54.45,21.474,30.171,29.17 +2020-06-28 00:45:00,54.19,20.747,30.171,29.17 +2020-06-28 01:00:00,53.18,20.885,27.15,29.17 +2020-06-28 01:15:00,50.44,19.824,27.15,29.17 +2020-06-28 01:30:00,52.82,17.895,27.15,29.17 +2020-06-28 01:45:00,53.02,18.034000000000002,27.15,29.17 +2020-06-28 02:00:00,52.68,17.64,25.403000000000002,29.17 +2020-06-28 02:15:00,52.39,15.887,25.403000000000002,29.17 +2020-06-28 02:30:00,52.08,18.303,25.403000000000002,29.17 +2020-06-28 02:45:00,52.53,18.977,25.403000000000002,29.17 +2020-06-28 03:00:00,52.73,20.605999999999998,23.386999999999997,29.17 +2020-06-28 03:15:00,53.01,17.711,23.386999999999997,29.17 +2020-06-28 03:30:00,52.27,16.403,23.386999999999997,29.17 +2020-06-28 03:45:00,52.48,17.54,23.386999999999997,29.17 +2020-06-28 04:00:00,51.85,21.539,23.941999999999997,29.17 +2020-06-28 04:15:00,51.64,25.916999999999998,23.941999999999997,29.17 +2020-06-28 04:30:00,52.18,23.593000000000004,23.941999999999997,29.17 +2020-06-28 04:45:00,52.88,22.923000000000002,23.941999999999997,29.17 +2020-06-28 05:00:00,52.47,28.29,23.026,29.17 +2020-06-28 05:15:00,52.43,26.049,23.026,29.17 +2020-06-28 05:30:00,49.33,19.701,23.026,29.17 +2020-06-28 05:45:00,52.47,20.958000000000002,23.026,29.17 +2020-06-28 06:00:00,53.34,33.241,23.223000000000003,29.17 +2020-06-28 06:15:00,53.67,42.965,23.223000000000003,29.17 +2020-06-28 06:30:00,54.36,37.324,23.223000000000003,29.17 +2020-06-28 06:45:00,54.97,33.788000000000004,23.223000000000003,29.17 +2020-06-28 07:00:00,54.97,33.775,24.968000000000004,29.17 +2020-06-28 07:15:00,54.23,31.741999999999997,24.968000000000004,29.17 +2020-06-28 07:30:00,53.95,29.835,24.968000000000004,29.17 +2020-06-28 07:45:00,53.99,30.430999999999997,24.968000000000004,29.17 +2020-06-28 08:00:00,54.31,30.06,29.131,29.17 +2020-06-28 08:15:00,53.99,34.674,29.131,29.17 +2020-06-28 08:30:00,54.29,35.741,29.131,29.17 +2020-06-28 08:45:00,55.38,39.021,29.131,29.17 +2020-06-28 09:00:00,53.19,34.866,29.904,29.17 +2020-06-28 09:15:00,54.67,35.915,29.904,29.17 +2020-06-28 09:30:00,54.92,39.713,29.904,29.17 +2020-06-28 09:45:00,55.03,43.516000000000005,29.904,29.17 +2020-06-28 10:00:00,56.5,39.309,28.943,29.17 +2020-06-28 10:15:00,57.14,41.528999999999996,28.943,29.17 +2020-06-28 10:30:00,58.29,42.163000000000004,28.943,29.17 +2020-06-28 10:45:00,58.46,44.405,28.943,29.17 +2020-06-28 11:00:00,57.77,40.417,31.682,29.17 +2020-06-28 11:15:00,55.17,41.08,31.682,29.17 +2020-06-28 11:30:00,54.11,42.855,31.682,29.17 +2020-06-28 11:45:00,54.05,44.603,31.682,29.17 +2020-06-28 12:00:00,52.24,43.143,27.315,29.17 +2020-06-28 12:15:00,54.17,42.653999999999996,27.315,29.17 +2020-06-28 12:30:00,51.79,41.54,27.315,29.17 +2020-06-28 12:45:00,52.87,42.261,27.315,29.17 +2020-06-28 13:00:00,48.04,42.218999999999994,23.894000000000002,29.17 +2020-06-28 13:15:00,48.71,42.547,23.894000000000002,29.17 +2020-06-28 13:30:00,47.9,40.772,23.894000000000002,29.17 +2020-06-28 13:45:00,51.51,39.719,23.894000000000002,29.17 +2020-06-28 14:00:00,51.52,42.372,21.148000000000003,29.17 +2020-06-28 14:15:00,49.54,40.695,21.148000000000003,29.17 +2020-06-28 14:30:00,49.02,39.174,21.148000000000003,29.17 +2020-06-28 14:45:00,52.42,38.562,21.148000000000003,29.17 +2020-06-28 15:00:00,52.14,41.068000000000005,21.229,29.17 +2020-06-28 15:15:00,52.45,38.25,21.229,29.17 +2020-06-28 15:30:00,53.38,35.873000000000005,21.229,29.17 +2020-06-28 15:45:00,50.9,33.839,21.229,29.17 +2020-06-28 16:00:00,52.91,36.479,25.037,29.17 +2020-06-28 16:15:00,55.27,35.875,25.037,29.17 +2020-06-28 16:30:00,56.98,36.352,25.037,29.17 +2020-06-28 16:45:00,61.07,31.888,25.037,29.17 +2020-06-28 17:00:00,63.55,36.014,37.11,29.17 +2020-06-28 17:15:00,65.49,35.226,37.11,29.17 +2020-06-28 17:30:00,66.42,35.125,37.11,29.17 +2020-06-28 17:45:00,67.75,34.19,37.11,29.17 +2020-06-28 18:00:00,72.02,38.864000000000004,42.215,29.17 +2020-06-28 18:15:00,71.79,39.239000000000004,42.215,29.17 +2020-06-28 18:30:00,71.81,37.871,42.215,29.17 +2020-06-28 18:45:00,73.23,38.946,42.215,29.17 +2020-06-28 19:00:00,75.51,41.174,44.383,29.17 +2020-06-28 19:15:00,72.91,38.681,44.383,29.17 +2020-06-28 19:30:00,72.05,38.239000000000004,44.383,29.17 +2020-06-28 19:45:00,71.62,38.571999999999996,44.383,29.17 +2020-06-28 20:00:00,72.23,37.048,43.426,29.17 +2020-06-28 20:15:00,72.93,37.198,43.426,29.17 +2020-06-28 20:30:00,75.71,37.187,43.426,29.17 +2020-06-28 20:45:00,76.24,37.47,43.426,29.17 +2020-06-28 21:00:00,79.17,35.599000000000004,42.265,29.17 +2020-06-28 21:15:00,79.66,38.616,42.265,29.17 +2020-06-28 21:30:00,77.53,38.846,42.265,29.17 +2020-06-28 21:45:00,76.34,40.054,42.265,29.17 +2020-06-28 22:00:00,71.7,39.599000000000004,42.26,29.17 +2020-06-28 22:15:00,73.09,40.06,42.26,29.17 +2020-06-28 22:30:00,70.56,39.058,42.26,29.17 +2020-06-28 22:45:00,71.0,35.898,42.26,29.17 +2020-06-28 23:00:00,64.44,32.248000000000005,36.609,29.17 +2020-06-28 23:15:00,64.62,30.998,36.609,29.17 +2020-06-28 23:30:00,61.55,29.895,36.609,29.17 +2020-06-28 23:45:00,63.88,29.316,36.609,29.17 +2020-06-29 00:00:00,66.83,25.676,34.611,29.28 +2020-06-29 00:15:00,67.56,25.444000000000003,34.611,29.28 +2020-06-29 00:30:00,66.99,23.704,34.611,29.28 +2020-06-29 00:45:00,65.2,22.566,34.611,29.28 +2020-06-29 01:00:00,61.33,23.096,33.552,29.28 +2020-06-29 01:15:00,61.25,22.009,33.552,29.28 +2020-06-29 01:30:00,68.73,20.438,33.552,29.28 +2020-06-29 01:45:00,69.26,20.480999999999998,33.552,29.28 +2020-06-29 02:00:00,67.73,20.535999999999998,32.351,29.28 +2020-06-29 02:15:00,64.1,17.863,32.351,29.28 +2020-06-29 02:30:00,65.25,20.449,32.351,29.28 +2020-06-29 02:45:00,62.46,20.95,32.351,29.28 +2020-06-29 03:00:00,63.93,23.139,30.793000000000003,29.28 +2020-06-29 03:15:00,64.66,21.044,30.793000000000003,29.28 +2020-06-29 03:30:00,64.42,20.402,30.793000000000003,29.28 +2020-06-29 03:45:00,65.17,21.073,30.793000000000003,29.28 +2020-06-29 04:00:00,69.29,28.322,31.274,29.28 +2020-06-29 04:15:00,69.39,35.769,31.274,29.28 +2020-06-29 04:30:00,73.21,33.074,31.274,29.28 +2020-06-29 04:45:00,76.37,32.777,31.274,29.28 +2020-06-29 05:00:00,84.05,45.946999999999996,37.75,29.28 +2020-06-29 05:15:00,89.26,54.832,37.75,29.28 +2020-06-29 05:30:00,91.77,47.041000000000004,37.75,29.28 +2020-06-29 05:45:00,100.7,45.004,37.75,29.28 +2020-06-29 06:00:00,106.18,46.221000000000004,55.36,29.28 +2020-06-29 06:15:00,104.78,46.18899999999999,55.36,29.28 +2020-06-29 06:30:00,101.12,44.909,55.36,29.28 +2020-06-29 06:45:00,106.27,47.068999999999996,55.36,29.28 +2020-06-29 07:00:00,110.35,47.54600000000001,65.87,29.28 +2020-06-29 07:15:00,106.69,47.847,65.87,29.28 +2020-06-29 07:30:00,102.74,45.05,65.87,29.28 +2020-06-29 07:45:00,98.31,45.833999999999996,65.87,29.28 +2020-06-29 08:00:00,101.86,43.085,55.695,29.28 +2020-06-29 08:15:00,104.63,46.422,55.695,29.28 +2020-06-29 08:30:00,103.4,46.312,55.695,29.28 +2020-06-29 08:45:00,98.22,49.619,55.695,29.28 +2020-06-29 09:00:00,97.83,44.388000000000005,50.881,29.28 +2020-06-29 09:15:00,98.5,43.49100000000001,50.881,29.28 +2020-06-29 09:30:00,97.26,46.31399999999999,50.881,29.28 +2020-06-29 09:45:00,99.53,47.886,50.881,29.28 +2020-06-29 10:00:00,105.48,44.043,49.138000000000005,29.28 +2020-06-29 10:15:00,107.62,46.105,49.138000000000005,29.28 +2020-06-29 10:30:00,106.69,46.236999999999995,49.138000000000005,29.28 +2020-06-29 10:45:00,101.86,46.96,49.138000000000005,29.28 +2020-06-29 11:00:00,108.46,43.041000000000004,49.178000000000004,29.28 +2020-06-29 11:15:00,104.0,44.08,49.178000000000004,29.28 +2020-06-29 11:30:00,102.52,46.606,49.178000000000004,29.28 +2020-06-29 11:45:00,97.39,48.825,49.178000000000004,29.28 +2020-06-29 12:00:00,95.56,45.77,47.698,29.28 +2020-06-29 12:15:00,98.26,45.39,47.698,29.28 +2020-06-29 12:30:00,92.89,43.213,47.698,29.28 +2020-06-29 12:45:00,91.17,44.008,47.698,29.28 +2020-06-29 13:00:00,90.56,44.903,48.104,29.28 +2020-06-29 13:15:00,91.19,44.283,48.104,29.28 +2020-06-29 13:30:00,93.09,42.658,48.104,29.28 +2020-06-29 13:45:00,90.72,42.498000000000005,48.104,29.28 +2020-06-29 14:00:00,92.67,44.217,48.53,29.28 +2020-06-29 14:15:00,90.61,43.095,48.53,29.28 +2020-06-29 14:30:00,89.74,41.367,48.53,29.28 +2020-06-29 14:45:00,89.22,42.806999999999995,48.53,29.28 +2020-06-29 15:00:00,92.79,45.053999999999995,49.351000000000006,29.28 +2020-06-29 15:15:00,89.48,41.575,49.351000000000006,29.28 +2020-06-29 15:30:00,89.59,39.916,49.351000000000006,29.28 +2020-06-29 15:45:00,91.17,37.37,49.351000000000006,29.28 +2020-06-29 16:00:00,92.45,41.121,51.44,29.28 +2020-06-29 16:15:00,93.63,40.543,51.44,29.28 +2020-06-29 16:30:00,98.96,40.327,51.44,29.28 +2020-06-29 16:45:00,97.02,35.815,51.44,29.28 +2020-06-29 17:00:00,99.29,38.823,56.868,29.28 +2020-06-29 17:15:00,99.23,38.361,56.868,29.28 +2020-06-29 17:30:00,99.19,37.845,56.868,29.28 +2020-06-29 17:45:00,102.14,36.434,56.868,29.28 +2020-06-29 18:00:00,102.78,40.075,57.229,29.28 +2020-06-29 18:15:00,101.5,38.505,57.229,29.28 +2020-06-29 18:30:00,103.73,36.42,57.229,29.28 +2020-06-29 18:45:00,102.37,40.659,57.229,29.28 +2020-06-29 19:00:00,99.56,42.528,57.744,29.28 +2020-06-29 19:15:00,95.89,41.278,57.744,29.28 +2020-06-29 19:30:00,96.63,40.488,57.744,29.28 +2020-06-29 19:45:00,92.86,40.168,57.744,29.28 +2020-06-29 20:00:00,91.91,37.258,66.05199999999999,29.28 +2020-06-29 20:15:00,90.4,38.671,66.05199999999999,29.28 +2020-06-29 20:30:00,93.06,39.088,66.05199999999999,29.28 +2020-06-29 20:45:00,90.78,39.683,66.05199999999999,29.28 +2020-06-29 21:00:00,92.65,37.217,59.396,29.28 +2020-06-29 21:15:00,88.69,40.618,59.396,29.28 +2020-06-29 21:30:00,86.02,41.196000000000005,59.396,29.28 +2020-06-29 21:45:00,84.23,42.163000000000004,59.396,29.28 +2020-06-29 22:00:00,80.64,39.546,53.06,29.28 +2020-06-29 22:15:00,79.82,41.918,53.06,29.28 +2020-06-29 22:30:00,78.02,36.138000000000005,53.06,29.28 +2020-06-29 22:45:00,80.24,32.711,53.06,29.28 +2020-06-29 23:00:00,73.65,29.131,46.148,29.28 +2020-06-29 23:15:00,74.02,26.403000000000002,46.148,29.28 +2020-06-29 23:30:00,72.94,25.406999999999996,46.148,29.28 +2020-06-29 23:45:00,71.27,24.415,46.148,29.28 +2020-06-30 00:00:00,72.03,23.34,44.625,29.28 +2020-06-30 00:15:00,69.91,24.04,44.625,29.28 +2020-06-30 00:30:00,68.65,22.963,44.625,29.28 +2020-06-30 00:45:00,71.12,22.569000000000003,44.625,29.28 +2020-06-30 01:00:00,68.89,22.561,41.733000000000004,29.28 +2020-06-30 01:15:00,68.91,21.552,41.733000000000004,29.28 +2020-06-30 01:30:00,69.21,19.86,41.733000000000004,29.28 +2020-06-30 01:45:00,72.69,19.401,41.733000000000004,29.28 +2020-06-30 02:00:00,69.55,18.98,39.872,29.28 +2020-06-30 02:15:00,70.67,17.457,39.872,29.28 +2020-06-30 02:30:00,77.04,19.615,39.872,29.28 +2020-06-30 02:45:00,77.77,20.448,39.872,29.28 +2020-06-30 03:00:00,73.56,22.02,38.711,29.28 +2020-06-30 03:15:00,71.83,20.819000000000003,38.711,29.28 +2020-06-30 03:30:00,74.59,20.177,38.711,29.28 +2020-06-30 03:45:00,72.48,19.767,38.711,29.28 +2020-06-30 04:00:00,73.67,25.706,39.823,29.28 +2020-06-30 04:15:00,76.58,33.113,39.823,29.28 +2020-06-30 04:30:00,78.87,30.273000000000003,39.823,29.28 +2020-06-30 04:45:00,84.64,30.521,39.823,29.28 +2020-06-30 05:00:00,90.01,45.441,43.228,29.28 +2020-06-30 05:15:00,94.67,54.836000000000006,43.228,29.28 +2020-06-30 05:30:00,100.35,47.21,43.228,29.28 +2020-06-30 05:45:00,104.12,44.481,43.228,29.28 +2020-06-30 06:00:00,104.34,46.69,54.316,29.28 +2020-06-30 06:15:00,106.09,46.928999999999995,54.316,29.28 +2020-06-30 06:30:00,104.25,45.317,54.316,29.28 +2020-06-30 06:45:00,111.24,46.6,54.316,29.28 +2020-06-30 07:00:00,112.68,47.207,65.758,29.28 +2020-06-30 07:15:00,112.93,47.266000000000005,65.758,29.28 +2020-06-30 07:30:00,106.91,44.458999999999996,65.758,29.28 +2020-06-30 07:45:00,109.59,44.313,65.758,29.28 +2020-06-30 08:00:00,108.14,41.508,57.983000000000004,29.28 +2020-06-30 08:15:00,105.14,44.367,57.983000000000004,29.28 +2020-06-30 08:30:00,102.56,44.407,57.983000000000004,29.28 +2020-06-30 08:45:00,109.58,46.778999999999996,57.983000000000004,29.28 +2020-06-30 09:00:00,108.83,41.842,52.653,29.28 +2020-06-30 09:15:00,107.0,40.89,52.653,29.28 +2020-06-30 09:30:00,102.96,44.428000000000004,52.653,29.28 +2020-06-30 09:45:00,100.07,47.33,52.653,29.28 +2020-06-30 10:00:00,106.46,42.102,51.408,29.28 +2020-06-30 10:15:00,108.26,43.974,51.408,29.28 +2020-06-30 10:30:00,109.08,44.14,51.408,29.28 +2020-06-30 10:45:00,106.82,45.883,51.408,29.28 +2020-06-30 11:00:00,100.17,42.043,51.913000000000004,29.28 +2020-06-30 11:15:00,103.91,43.474,51.913000000000004,29.28 +2020-06-30 11:30:00,106.58,44.784,51.913000000000004,29.28 +2020-06-30 11:45:00,101.37,46.586000000000006,51.913000000000004,29.28 +2020-06-30 12:00:00,98.37,43.301,49.508,29.28 +2020-06-30 12:15:00,96.43,43.236999999999995,49.508,29.28 +2020-06-30 12:30:00,100.54,41.938,49.508,29.28 +2020-06-30 12:45:00,100.76,43.412,49.508,29.28 +2020-06-30 13:00:00,99.12,43.91,50.007,29.28 +2020-06-30 13:15:00,93.41,45.073,50.007,29.28 +2020-06-30 13:30:00,100.0,43.479,50.007,29.28 +2020-06-30 13:45:00,100.4,42.37,50.007,29.28 +2020-06-30 14:00:00,101.59,44.562,49.778999999999996,29.28 +2020-06-30 14:15:00,93.92,43.248000000000005,49.778999999999996,29.28 +2020-06-30 14:30:00,98.79,41.861000000000004,49.778999999999996,29.28 +2020-06-30 14:45:00,99.07,42.513000000000005,49.778999999999996,29.28 +2020-06-30 15:00:00,98.76,44.603,51.559,29.28 +2020-06-30 15:15:00,96.36,42.058,51.559,29.28 +2020-06-30 15:30:00,97.72,40.195,51.559,29.28 +2020-06-30 15:45:00,100.74,37.971,51.559,29.28 +2020-06-30 16:00:00,101.72,41.047,53.531000000000006,29.28 +2020-06-30 16:15:00,97.36,40.577,53.531000000000006,29.28 +2020-06-30 16:30:00,100.31,40.037,53.531000000000006,29.28 +2020-06-30 16:45:00,100.27,36.243,53.531000000000006,29.28 +2020-06-30 17:00:00,108.34,39.504,59.497,29.28 +2020-06-30 17:15:00,109.28,39.463,59.497,29.28 +2020-06-30 17:30:00,107.79,38.535,59.497,29.28 +2020-06-30 17:45:00,105.96,36.778,59.497,29.28 +2020-06-30 18:00:00,107.94,39.473,59.861999999999995,29.28 +2020-06-30 18:15:00,109.77,39.347,59.861999999999995,29.28 +2020-06-30 18:30:00,112.0,36.985,59.861999999999995,29.28 +2020-06-30 18:45:00,107.95,41.065,59.861999999999995,29.28 +2020-06-30 19:00:00,102.45,41.773,60.989,29.28 +2020-06-30 19:15:00,102.48,40.681999999999995,60.989,29.28 +2020-06-30 19:30:00,105.3,39.64,60.989,29.28 +2020-06-30 19:45:00,101.39,39.65,60.989,29.28 +2020-06-30 20:00:00,93.96,37.129,68.35600000000001,29.28 +2020-06-30 20:15:00,92.47,37.068000000000005,68.35600000000001,29.28 +2020-06-30 20:30:00,92.77,37.573,68.35600000000001,29.28 +2020-06-30 20:45:00,95.45,38.578,68.35600000000001,29.28 +2020-06-30 21:00:00,91.73,36.955,59.251000000000005,29.28 +2020-06-30 21:15:00,91.24,38.936,59.251000000000005,29.28 +2020-06-30 21:30:00,89.56,39.635,59.251000000000005,29.28 +2020-06-30 21:45:00,87.06,40.806,59.251000000000005,29.28 +2020-06-30 22:00:00,82.92,38.446999999999996,54.736999999999995,29.28 +2020-06-30 22:15:00,83.13,40.474000000000004,54.736999999999995,29.28 +2020-06-30 22:30:00,79.39,34.979,54.736999999999995,29.28 +2020-06-30 22:45:00,78.5,31.551,54.736999999999995,29.28 +2020-06-30 23:00:00,75.71,27.199,46.806999999999995,29.28 +2020-06-30 23:15:00,75.56,26.018,46.806999999999995,29.28 +2020-06-30 23:30:00,74.43,25.016,46.806999999999995,29.28 +2020-06-30 23:45:00,73.31,24.177,46.806999999999995,29.28 +2020-07-01 00:00:00,70.43,18.788,42.195,29.509 +2020-07-01 00:15:00,71.7,19.662,42.195,29.509 +2020-07-01 00:30:00,70.95,18.385,42.195,29.509 +2020-07-01 00:45:00,71.27,18.276,42.195,29.509 +2020-07-01 01:00:00,69.68,18.217,38.82,29.509 +2020-07-01 01:15:00,70.95,17.452,38.82,29.509 +2020-07-01 01:30:00,71.25,15.89,38.82,29.509 +2020-07-01 01:45:00,71.45,15.93,38.82,29.509 +2020-07-01 02:00:00,70.66,15.644,37.023,29.509 +2020-07-01 02:15:00,70.4,14.513,37.023,29.509 +2020-07-01 02:30:00,71.19,16.352999999999998,37.023,29.509 +2020-07-01 02:45:00,71.81,17.062,37.023,29.509 +2020-07-01 03:00:00,72.87,18.293,36.818000000000005,29.509 +2020-07-01 03:15:00,71.24,17.483,36.818000000000005,29.509 +2020-07-01 03:30:00,72.47,16.722,36.818000000000005,29.509 +2020-07-01 03:45:00,73.77,15.866,36.818000000000005,29.509 +2020-07-01 04:00:00,79.37,20.849,37.495,29.509 +2020-07-01 04:15:00,83.66,27.185,37.495,29.509 +2020-07-01 04:30:00,85.87,24.576,37.495,29.509 +2020-07-01 04:45:00,82.08,24.599,37.495,29.509 +2020-07-01 05:00:00,87.68,37.652,39.858000000000004,29.509 +2020-07-01 05:15:00,91.0,45.316,39.858000000000004,29.509 +2020-07-01 05:30:00,94.48,39.075,39.858000000000004,29.509 +2020-07-01 05:45:00,96.89,36.391999999999996,39.858000000000004,29.509 +2020-07-01 06:00:00,102.46,37.272,52.867,29.509 +2020-07-01 06:15:00,102.72,36.881,52.867,29.509 +2020-07-01 06:30:00,102.82,36.024,52.867,29.509 +2020-07-01 06:45:00,103.56,37.891999999999996,52.867,29.509 +2020-07-01 07:00:00,106.17,38.054,66.061,29.509 +2020-07-01 07:15:00,107.32,38.36,66.061,29.509 +2020-07-01 07:30:00,103.25,36.042,66.061,29.509 +2020-07-01 07:45:00,105.44,36.064,66.061,29.509 +2020-07-01 08:00:00,102.77,31.921,58.532,29.509 +2020-07-01 08:15:00,106.3,34.949,58.532,29.509 +2020-07-01 08:30:00,109.35,36.171,58.532,29.509 +2020-07-01 08:45:00,110.17,38.823,58.532,29.509 +2020-07-01 09:00:00,107.3,32.952,56.047,29.509 +2020-07-01 09:15:00,102.83,32.253,56.047,29.509 +2020-07-01 09:30:00,105.13,36.139,56.047,29.509 +2020-07-01 09:45:00,108.99,39.617,56.047,29.509 +2020-07-01 10:00:00,108.91,36.763000000000005,53.823,29.509 +2020-07-01 10:15:00,111.42,38.422,53.823,29.509 +2020-07-01 10:30:00,107.94,38.482,53.823,29.509 +2020-07-01 10:45:00,106.69,39.787,53.823,29.509 +2020-07-01 11:00:00,108.14,37.555,54.184,29.509 +2020-07-01 11:15:00,107.34,38.873000000000005,54.184,29.509 +2020-07-01 11:30:00,103.63,40.246,54.184,29.509 +2020-07-01 11:45:00,99.79,41.605,54.184,29.509 +2020-07-01 12:00:00,101.32,36.647,52.628,29.509 +2020-07-01 12:15:00,105.42,36.146,52.628,29.509 +2020-07-01 12:30:00,103.81,35.018,52.628,29.509 +2020-07-01 12:45:00,100.08,36.165,52.628,29.509 +2020-07-01 13:00:00,97.35,36.506,52.31,29.509 +2020-07-01 13:15:00,99.57,37.897,52.31,29.509 +2020-07-01 13:30:00,102.34,36.091,52.31,29.509 +2020-07-01 13:45:00,101.61,35.67,52.31,29.509 +2020-07-01 14:00:00,97.36,37.439,52.278999999999996,29.509 +2020-07-01 14:15:00,95.86,36.361,52.278999999999996,29.509 +2020-07-01 14:30:00,94.71,35.399,52.278999999999996,29.509 +2020-07-01 14:45:00,102.25,35.926,52.278999999999996,29.509 +2020-07-01 15:00:00,101.58,37.861999999999995,53.306999999999995,29.509 +2020-07-01 15:15:00,106.6,35.504,53.306999999999995,29.509 +2020-07-01 15:30:00,109.06,33.898,53.306999999999995,29.509 +2020-07-01 15:45:00,110.7,32.225,53.306999999999995,29.509 +2020-07-01 16:00:00,104.2,34.999,55.358999999999995,29.509 +2020-07-01 16:15:00,100.24,34.702,55.358999999999995,29.509 +2020-07-01 16:30:00,107.63,33.689,55.358999999999995,29.509 +2020-07-01 16:45:00,110.17,30.451999999999998,55.358999999999995,29.509 +2020-07-01 17:00:00,110.35,34.147,59.211999999999996,29.509 +2020-07-01 17:15:00,104.03,34.079,59.211999999999996,29.509 +2020-07-01 17:30:00,107.88,32.936,59.211999999999996,29.509 +2020-07-01 17:45:00,108.5,31.721,59.211999999999996,29.509 +2020-07-01 18:00:00,113.12,34.732,60.403999999999996,29.509 +2020-07-01 18:15:00,112.56,34.763000000000005,60.403999999999996,29.509 +2020-07-01 18:30:00,109.0,32.714,60.403999999999996,29.509 +2020-07-01 18:45:00,106.69,35.778,60.403999999999996,29.509 +2020-07-01 19:00:00,103.85,36.946999999999996,60.993,29.509 +2020-07-01 19:15:00,104.5,36.014,60.993,29.509 +2020-07-01 19:30:00,99.49,34.96,60.993,29.509 +2020-07-01 19:45:00,102.64,34.247,60.993,29.509 +2020-07-01 20:00:00,93.35,31.489,66.6,29.509 +2020-07-01 20:15:00,92.62,31.057,66.6,29.509 +2020-07-01 20:30:00,94.73,31.609,66.6,29.509 +2020-07-01 20:45:00,92.77,32.135999999999996,66.6,29.509 +2020-07-01 21:00:00,93.61,30.487,59.855,29.509 +2020-07-01 21:15:00,91.43,32.09,59.855,29.509 +2020-07-01 21:30:00,88.22,33.063,59.855,29.509 +2020-07-01 21:45:00,86.19,34.155,59.855,29.509 +2020-07-01 22:00:00,82.68,31.27,54.942,29.509 +2020-07-01 22:15:00,82.47,33.906,54.942,29.509 +2020-07-01 22:30:00,79.33,29.747,54.942,29.509 +2020-07-01 22:45:00,79.58,26.566999999999997,54.942,29.509 +2020-07-01 23:00:00,76.55,23.329,46.056000000000004,29.509 +2020-07-01 23:15:00,75.74,21.73,46.056000000000004,29.509 +2020-07-01 23:30:00,76.51,20.483,46.056000000000004,29.509 +2020-07-01 23:45:00,75.37,19.579,46.056000000000004,29.509 +2020-07-02 00:00:00,71.27,18.766,40.859,29.509 +2020-07-02 00:15:00,75.23,19.639,40.859,29.509 +2020-07-02 00:30:00,72.87,18.366,40.859,29.509 +2020-07-02 00:45:00,72.99,18.262,40.859,29.509 +2020-07-02 01:00:00,73.16,18.214000000000002,39.06,29.509 +2020-07-02 01:15:00,71.58,17.437,39.06,29.509 +2020-07-02 01:30:00,70.03,15.875,39.06,29.509 +2020-07-02 01:45:00,71.6,15.907,39.06,29.509 +2020-07-02 02:00:00,71.98,15.627,37.592,29.509 +2020-07-02 02:15:00,72.45,14.513,37.592,29.509 +2020-07-02 02:30:00,71.95,16.332,37.592,29.509 +2020-07-02 02:45:00,73.87,17.044,37.592,29.509 +2020-07-02 03:00:00,73.17,18.271,37.416,29.509 +2020-07-02 03:15:00,73.55,17.469,37.416,29.509 +2020-07-02 03:30:00,74.45,16.714000000000002,37.416,29.509 +2020-07-02 03:45:00,76.07,15.876,37.416,29.509 +2020-07-02 04:00:00,85.78,20.818,38.176,29.509 +2020-07-02 04:15:00,84.85,27.118000000000002,38.176,29.509 +2020-07-02 04:30:00,81.2,24.5,38.176,29.509 +2020-07-02 04:45:00,82.99,24.521,38.176,29.509 +2020-07-02 05:00:00,91.83,37.501999999999995,41.203,29.509 +2020-07-02 05:15:00,92.7,45.07,41.203,29.509 +2020-07-02 05:30:00,93.71,38.889,41.203,29.509 +2020-07-02 05:45:00,100.65,36.238,41.203,29.509 +2020-07-02 06:00:00,111.15,37.12,51.09,29.509 +2020-07-02 06:15:00,112.6,36.724000000000004,51.09,29.509 +2020-07-02 06:30:00,107.72,35.889,51.09,29.509 +2020-07-02 06:45:00,109.61,37.786,51.09,29.509 +2020-07-02 07:00:00,106.92,37.939,63.541000000000004,29.509 +2020-07-02 07:15:00,111.87,38.266,63.541000000000004,29.509 +2020-07-02 07:30:00,113.84,35.949,63.541000000000004,29.509 +2020-07-02 07:45:00,113.44,36.0,63.541000000000004,29.509 +2020-07-02 08:00:00,107.91,31.868000000000002,55.65,29.509 +2020-07-02 08:15:00,107.55,34.914,55.65,29.509 +2020-07-02 08:30:00,114.18,36.126,55.65,29.509 +2020-07-02 08:45:00,115.94,38.775,55.65,29.509 +2020-07-02 09:00:00,110.22,32.898,51.833999999999996,29.509 +2020-07-02 09:15:00,109.29,32.201,51.833999999999996,29.509 +2020-07-02 09:30:00,107.38,36.082,51.833999999999996,29.509 +2020-07-02 09:45:00,111.58,39.568000000000005,51.833999999999996,29.509 +2020-07-02 10:00:00,112.97,36.721,49.70399999999999,29.509 +2020-07-02 10:15:00,113.92,38.382,49.70399999999999,29.509 +2020-07-02 10:30:00,108.83,38.439,49.70399999999999,29.509 +2020-07-02 10:45:00,113.08,39.747,49.70399999999999,29.509 +2020-07-02 11:00:00,112.65,37.512,48.593999999999994,29.509 +2020-07-02 11:15:00,108.39,38.832,48.593999999999994,29.509 +2020-07-02 11:30:00,105.46,40.194,48.593999999999994,29.509 +2020-07-02 11:45:00,103.74,41.549,48.593999999999994,29.509 +2020-07-02 12:00:00,102.28,36.616,46.275,29.509 +2020-07-02 12:15:00,112.02,36.114000000000004,46.275,29.509 +2020-07-02 12:30:00,110.12,34.974000000000004,46.275,29.509 +2020-07-02 12:45:00,111.95,36.118,46.275,29.509 +2020-07-02 13:00:00,108.93,36.444,45.803000000000004,29.509 +2020-07-02 13:15:00,104.36,37.830999999999996,45.803000000000004,29.509 +2020-07-02 13:30:00,111.63,36.033,45.803000000000004,29.509 +2020-07-02 13:45:00,108.98,35.619,45.803000000000004,29.509 +2020-07-02 14:00:00,114.1,37.393,46.251999999999995,29.509 +2020-07-02 14:15:00,102.95,36.316,46.251999999999995,29.509 +2020-07-02 14:30:00,101.66,35.339,46.251999999999995,29.509 +2020-07-02 14:45:00,108.11,35.873000000000005,46.251999999999995,29.509 +2020-07-02 15:00:00,117.54,37.826,48.309,29.509 +2020-07-02 15:15:00,115.68,35.461,48.309,29.509 +2020-07-02 15:30:00,115.1,33.853,48.309,29.509 +2020-07-02 15:45:00,108.35,32.172,48.309,29.509 +2020-07-02 16:00:00,106.86,34.964,49.681999999999995,29.509 +2020-07-02 16:15:00,116.47,34.668,49.681999999999995,29.509 +2020-07-02 16:30:00,115.12,33.673,49.681999999999995,29.509 +2020-07-02 16:45:00,115.58,30.432,49.681999999999995,29.509 +2020-07-02 17:00:00,113.83,34.135999999999996,53.086000000000006,29.509 +2020-07-02 17:15:00,108.49,34.075,53.086000000000006,29.509 +2020-07-02 17:30:00,108.27,32.933,53.086000000000006,29.509 +2020-07-02 17:45:00,110.25,31.714000000000002,53.086000000000006,29.509 +2020-07-02 18:00:00,117.44,34.733000000000004,54.038999999999994,29.509 +2020-07-02 18:15:00,113.72,34.743,54.038999999999994,29.509 +2020-07-02 18:30:00,110.69,32.695,54.038999999999994,29.509 +2020-07-02 18:45:00,103.51,35.757,54.038999999999994,29.509 +2020-07-02 19:00:00,99.92,36.928000000000004,53.408,29.509 +2020-07-02 19:15:00,95.01,35.986999999999995,53.408,29.509 +2020-07-02 19:30:00,95.4,34.926,53.408,29.509 +2020-07-02 19:45:00,91.38,34.207,53.408,29.509 +2020-07-02 20:00:00,91.86,31.435,55.309,29.509 +2020-07-02 20:15:00,89.76,31.002,55.309,29.509 +2020-07-02 20:30:00,90.27,31.554000000000002,55.309,29.509 +2020-07-02 20:45:00,93.88,32.098,55.309,29.509 +2020-07-02 21:00:00,88.63,30.451999999999998,51.585,29.509 +2020-07-02 21:15:00,87.77,32.058,51.585,29.509 +2020-07-02 21:30:00,87.98,33.01,51.585,29.509 +2020-07-02 21:45:00,85.55,34.099000000000004,51.585,29.509 +2020-07-02 22:00:00,81.55,31.225,48.006,29.509 +2020-07-02 22:15:00,79.87,33.864000000000004,48.006,29.509 +2020-07-02 22:30:00,78.85,29.693,48.006,29.509 +2020-07-02 22:45:00,80.14,26.506,48.006,29.509 +2020-07-02 23:00:00,74.23,23.264,42.309,29.509 +2020-07-02 23:15:00,74.85,21.691999999999997,42.309,29.509 +2020-07-02 23:30:00,74.97,20.459,42.309,29.509 +2020-07-02 23:45:00,73.89,19.547,42.309,29.509 +2020-07-03 00:00:00,70.93,16.918,39.649,29.509 +2020-07-03 00:15:00,74.24,18.007,39.649,29.509 +2020-07-03 00:30:00,70.69,17.027,39.649,29.509 +2020-07-03 00:45:00,71.66,17.366,39.649,29.509 +2020-07-03 01:00:00,70.83,16.942,37.744,29.509 +2020-07-03 01:15:00,71.09,15.489,37.744,29.509 +2020-07-03 01:30:00,73.13,14.683,37.744,29.509 +2020-07-03 01:45:00,70.01,14.513,37.744,29.509 +2020-07-03 02:00:00,69.99,15.138,36.965,29.509 +2020-07-03 02:15:00,72.95,14.513,36.965,29.509 +2020-07-03 02:30:00,70.26,16.631,36.965,29.509 +2020-07-03 02:45:00,72.71,16.631,36.965,29.509 +2020-07-03 03:00:00,71.77,18.701,37.678000000000004,29.509 +2020-07-03 03:15:00,71.51,16.611,37.678000000000004,29.509 +2020-07-03 03:30:00,76.38,15.615,37.678000000000004,29.509 +2020-07-03 03:45:00,80.75,15.698,37.678000000000004,29.509 +2020-07-03 04:00:00,85.8,20.732,38.591,29.509 +2020-07-03 04:15:00,79.97,25.381,38.591,29.509 +2020-07-03 04:30:00,79.88,23.738000000000003,38.591,29.509 +2020-07-03 04:45:00,87.51,23.125999999999998,38.591,29.509 +2020-07-03 05:00:00,88.68,35.584,40.666,29.509 +2020-07-03 05:15:00,99.92,44.004,40.666,29.509 +2020-07-03 05:30:00,102.65,38.053000000000004,40.666,29.509 +2020-07-03 05:45:00,105.28,34.976,40.666,29.509 +2020-07-03 06:00:00,112.07,36.077,51.784,29.509 +2020-07-03 06:15:00,109.3,35.889,51.784,29.509 +2020-07-03 06:30:00,114.85,35.047,51.784,29.509 +2020-07-03 06:45:00,117.65,36.794000000000004,51.784,29.509 +2020-07-03 07:00:00,118.78,37.632,61.383,29.509 +2020-07-03 07:15:00,114.49,38.888000000000005,61.383,29.509 +2020-07-03 07:30:00,122.05,34.546,61.383,29.509 +2020-07-03 07:45:00,121.0,34.44,61.383,29.509 +2020-07-03 08:00:00,120.65,31.176,55.272,29.509 +2020-07-03 08:15:00,115.76,34.957,55.272,29.509 +2020-07-03 08:30:00,118.63,36.018,55.272,29.509 +2020-07-03 08:45:00,124.55,38.578,55.272,29.509 +2020-07-03 09:00:00,124.35,30.26,53.506,29.509 +2020-07-03 09:15:00,123.23,31.552,53.506,29.509 +2020-07-03 09:30:00,117.79,34.724000000000004,53.506,29.509 +2020-07-03 09:45:00,124.21,38.628,53.506,29.509 +2020-07-03 10:00:00,124.18,35.675,51.363,29.509 +2020-07-03 10:15:00,125.33,37.067,51.363,29.509 +2020-07-03 10:30:00,113.6,37.708,51.363,29.509 +2020-07-03 10:45:00,112.47,38.94,51.363,29.509 +2020-07-03 11:00:00,117.84,36.973,51.043,29.509 +2020-07-03 11:15:00,119.54,37.192,51.043,29.509 +2020-07-03 11:30:00,114.56,38.071,51.043,29.509 +2020-07-03 11:45:00,111.63,38.407,51.043,29.509 +2020-07-03 12:00:00,106.04,33.891999999999996,47.52,29.509 +2020-07-03 12:15:00,110.61,32.827,47.52,29.509 +2020-07-03 12:30:00,107.32,31.789,47.52,29.509 +2020-07-03 12:45:00,103.64,32.069,47.52,29.509 +2020-07-03 13:00:00,101.08,32.958,45.494,29.509 +2020-07-03 13:15:00,99.8,34.49,45.494,29.509 +2020-07-03 13:30:00,95.66,33.538000000000004,45.494,29.509 +2020-07-03 13:45:00,101.57,33.458,45.494,29.509 +2020-07-03 14:00:00,98.0,34.446999999999996,43.883,29.509 +2020-07-03 14:15:00,99.34,33.841,43.883,29.509 +2020-07-03 14:30:00,102.34,34.419000000000004,43.883,29.509 +2020-07-03 14:45:00,107.73,34.214,43.883,29.509 +2020-07-03 15:00:00,109.4,36.125,45.714,29.509 +2020-07-03 15:15:00,106.77,33.525,45.714,29.509 +2020-07-03 15:30:00,104.22,31.396,45.714,29.509 +2020-07-03 15:45:00,105.17,30.504,45.714,29.509 +2020-07-03 16:00:00,101.31,32.468,48.222,29.509 +2020-07-03 16:15:00,98.01,32.68,48.222,29.509 +2020-07-03 16:30:00,102.98,31.509,48.222,29.509 +2020-07-03 16:45:00,107.56,27.44,48.222,29.509 +2020-07-03 17:00:00,110.19,32.964,52.619,29.509 +2020-07-03 17:15:00,103.65,32.747,52.619,29.509 +2020-07-03 17:30:00,108.81,31.796,52.619,29.509 +2020-07-03 17:45:00,110.01,30.412,52.619,29.509 +2020-07-03 18:00:00,107.36,33.431,52.99,29.509 +2020-07-03 18:15:00,103.91,32.449,52.99,29.509 +2020-07-03 18:30:00,105.74,30.276,52.99,29.509 +2020-07-03 18:45:00,106.53,33.777,52.99,29.509 +2020-07-03 19:00:00,100.94,35.813,51.923,29.509 +2020-07-03 19:15:00,91.66,35.426,51.923,29.509 +2020-07-03 19:30:00,95.59,34.438,51.923,29.509 +2020-07-03 19:45:00,96.63,32.675,51.923,29.509 +2020-07-03 20:00:00,95.47,29.728,56.238,29.509 +2020-07-03 20:15:00,91.08,30.12,56.238,29.509 +2020-07-03 20:30:00,87.72,30.166,56.238,29.509 +2020-07-03 20:45:00,93.25,29.855,56.238,29.509 +2020-07-03 21:00:00,93.94,29.558000000000003,52.426,29.509 +2020-07-03 21:15:00,91.58,32.889,52.426,29.509 +2020-07-03 21:30:00,84.87,33.661,52.426,29.509 +2020-07-03 21:45:00,81.0,34.939,52.426,29.509 +2020-07-03 22:00:00,81.22,31.889,48.196000000000005,29.509 +2020-07-03 22:15:00,83.92,34.275999999999996,48.196000000000005,29.509 +2020-07-03 22:30:00,81.87,34.796,48.196000000000005,29.509 +2020-07-03 22:45:00,78.52,32.36,48.196000000000005,29.509 +2020-07-03 23:00:00,69.31,30.891,41.71,29.509 +2020-07-03 23:15:00,70.51,27.789,41.71,29.509 +2020-07-03 23:30:00,75.91,24.73,41.71,29.509 +2020-07-03 23:45:00,74.86,23.709,41.71,29.509 +2020-07-04 00:00:00,71.28,18.308,41.105,29.398000000000003 +2020-07-04 00:15:00,66.58,18.862000000000002,41.105,29.398000000000003 +2020-07-04 00:30:00,68.4,17.344,41.105,29.398000000000003 +2020-07-04 00:45:00,72.06,16.899,41.105,29.398000000000003 +2020-07-04 01:00:00,70.88,16.746,36.934,29.398000000000003 +2020-07-04 01:15:00,68.8,15.936,36.934,29.398000000000003 +2020-07-04 01:30:00,62.1,14.513,36.934,29.398000000000003 +2020-07-04 01:45:00,61.89,15.368,36.934,29.398000000000003 +2020-07-04 02:00:00,61.38,15.015999999999998,34.782,29.398000000000003 +2020-07-04 02:15:00,61.18,14.513,34.782,29.398000000000003 +2020-07-04 02:30:00,67.97,14.918,34.782,29.398000000000003 +2020-07-04 02:45:00,68.39,15.735999999999999,34.782,29.398000000000003 +2020-07-04 03:00:00,67.7,16.349,34.489000000000004,29.398000000000003 +2020-07-04 03:15:00,65.75,14.513,34.489000000000004,29.398000000000003 +2020-07-04 03:30:00,69.36,14.513,34.489000000000004,29.398000000000003 +2020-07-04 03:45:00,67.53,14.645,34.489000000000004,29.398000000000003 +2020-07-04 04:00:00,64.42,17.846,34.111,29.398000000000003 +2020-07-04 04:15:00,60.03,21.635,34.111,29.398000000000003 +2020-07-04 04:30:00,62.56,18.337,34.111,29.398000000000003 +2020-07-04 04:45:00,68.6,18.028,34.111,29.398000000000003 +2020-07-04 05:00:00,69.39,22.414,33.283,29.398000000000003 +2020-07-04 05:15:00,67.18,20.141,33.283,29.398000000000003 +2020-07-04 05:30:00,62.58,15.806,33.283,29.398000000000003 +2020-07-04 05:45:00,66.37,17.168,33.283,29.398000000000003 +2020-07-04 06:00:00,67.09,29.13,33.653,29.398000000000003 +2020-07-04 06:15:00,70.36,36.363,33.653,29.398000000000003 +2020-07-04 06:30:00,75.13,32.815,33.653,29.398000000000003 +2020-07-04 06:45:00,76.55,31.62,33.653,29.398000000000003 +2020-07-04 07:00:00,74.19,31.340999999999998,36.732,29.398000000000003 +2020-07-04 07:15:00,69.0,31.328000000000003,36.732,29.398000000000003 +2020-07-04 07:30:00,75.39,28.43,36.732,29.398000000000003 +2020-07-04 07:45:00,78.46,29.125999999999998,36.732,29.398000000000003 +2020-07-04 08:00:00,78.82,26.471,41.318999999999996,29.398000000000003 +2020-07-04 08:15:00,75.62,29.854,41.318999999999996,29.398000000000003 +2020-07-04 08:30:00,72.05,30.805,41.318999999999996,29.398000000000003 +2020-07-04 08:45:00,72.92,34.181999999999995,41.318999999999996,29.398000000000003 +2020-07-04 09:00:00,75.51,29.045,43.195,29.398000000000003 +2020-07-04 09:15:00,75.93,30.805,43.195,29.398000000000003 +2020-07-04 09:30:00,76.19,34.44,43.195,29.398000000000003 +2020-07-04 09:45:00,69.16,37.868,43.195,29.398000000000003 +2020-07-04 10:00:00,68.23,35.586999999999996,41.843999999999994,29.398000000000003 +2020-07-04 10:15:00,68.67,37.345,41.843999999999994,29.398000000000003 +2020-07-04 10:30:00,72.15,37.611999999999995,41.843999999999994,29.398000000000003 +2020-07-04 10:45:00,69.35,38.399,41.843999999999994,29.398000000000003 +2020-07-04 11:00:00,67.52,36.268,39.035,29.398000000000003 +2020-07-04 11:15:00,67.46,37.444,39.035,29.398000000000003 +2020-07-04 11:30:00,65.74,38.674,39.035,29.398000000000003 +2020-07-04 11:45:00,64.54,39.766999999999996,39.035,29.398000000000003 +2020-07-04 12:00:00,62.25,35.786,38.001,29.398000000000003 +2020-07-04 12:15:00,60.77,35.577,38.001,29.398000000000003 +2020-07-04 12:30:00,60.3,34.369,38.001,29.398000000000003 +2020-07-04 12:45:00,59.23,35.464,38.001,29.398000000000003 +2020-07-04 13:00:00,57.7,35.444,34.747,29.398000000000003 +2020-07-04 13:15:00,57.19,36.525999999999996,34.747,29.398000000000003 +2020-07-04 13:30:00,57.53,35.806,34.747,29.398000000000003 +2020-07-04 13:45:00,57.64,34.283,34.747,29.398000000000003 +2020-07-04 14:00:00,57.47,35.258,33.434,29.398000000000003 +2020-07-04 14:15:00,57.5,33.368,33.434,29.398000000000003 +2020-07-04 14:30:00,57.46,33.594,33.434,29.398000000000003 +2020-07-04 14:45:00,58.13,33.887,33.434,29.398000000000003 +2020-07-04 15:00:00,59.46,36.232,35.921,29.398000000000003 +2020-07-04 15:15:00,59.91,34.321,35.921,29.398000000000003 +2020-07-04 15:30:00,60.67,32.314,35.921,29.398000000000003 +2020-07-04 15:45:00,61.72,30.444000000000003,35.921,29.398000000000003 +2020-07-04 16:00:00,63.63,34.691,39.427,29.398000000000003 +2020-07-04 16:15:00,64.21,33.919000000000004,39.427,29.398000000000003 +2020-07-04 16:30:00,69.13,33.007,39.427,29.398000000000003 +2020-07-04 16:45:00,69.2,28.857,39.427,29.398000000000003 +2020-07-04 17:00:00,69.91,33.164,44.096000000000004,29.398000000000003 +2020-07-04 17:15:00,72.05,30.614,44.096000000000004,29.398000000000003 +2020-07-04 17:30:00,73.58,29.529,44.096000000000004,29.398000000000003 +2020-07-04 17:45:00,75.0,28.683000000000003,44.096000000000004,29.398000000000003 +2020-07-04 18:00:00,76.84,33.131,43.931000000000004,29.398000000000003 +2020-07-04 18:15:00,76.16,33.784,43.931000000000004,29.398000000000003 +2020-07-04 18:30:00,76.26,32.959,43.931000000000004,29.398000000000003 +2020-07-04 18:45:00,77.12,33.004,43.931000000000004,29.398000000000003 +2020-07-04 19:00:00,75.57,33.28,42.187,29.398000000000003 +2020-07-04 19:15:00,72.31,31.864,42.187,29.398000000000003 +2020-07-04 19:30:00,71.21,31.631999999999998,42.187,29.398000000000003 +2020-07-04 19:45:00,70.88,31.693,42.187,29.398000000000003 +2020-07-04 20:00:00,70.25,29.52,38.315,29.398000000000003 +2020-07-04 20:15:00,69.83,29.163,38.315,29.398000000000003 +2020-07-04 20:30:00,69.99,28.314,38.315,29.398000000000003 +2020-07-04 20:45:00,70.95,29.953000000000003,38.315,29.398000000000003 +2020-07-04 21:00:00,71.53,28.039,36.843,29.398000000000003 +2020-07-04 21:15:00,71.29,30.995,36.843,29.398000000000003 +2020-07-04 21:30:00,70.14,31.895,36.843,29.398000000000003 +2020-07-04 21:45:00,68.61,32.623000000000005,36.843,29.398000000000003 +2020-07-04 22:00:00,66.07,29.506999999999998,37.260999999999996,29.398000000000003 +2020-07-04 22:15:00,66.0,32.06,37.260999999999996,29.398000000000003 +2020-07-04 22:30:00,62.91,31.948,37.260999999999996,29.398000000000003 +2020-07-04 22:45:00,62.98,29.82,37.260999999999996,29.398000000000003 +2020-07-04 23:00:00,59.32,27.59,32.148,29.398000000000003 +2020-07-04 23:15:00,58.95,25.004,32.148,29.398000000000003 +2020-07-04 23:30:00,58.24,24.543000000000003,32.148,29.398000000000003 +2020-07-04 23:45:00,57.81,23.965999999999998,32.148,29.398000000000003 +2020-07-05 00:00:00,55.67,19.707,28.905,29.398000000000003 +2020-07-05 00:15:00,55.37,19.081,28.905,29.398000000000003 +2020-07-05 00:30:00,54.85,17.43,28.905,29.398000000000003 +2020-07-05 00:45:00,54.72,16.848,28.905,29.398000000000003 +2020-07-05 01:00:00,53.6,16.983,26.906999999999996,29.398000000000003 +2020-07-05 01:15:00,53.84,15.994000000000002,26.906999999999996,29.398000000000003 +2020-07-05 01:30:00,54.02,14.513,26.906999999999996,29.398000000000003 +2020-07-05 01:45:00,54.04,14.877,26.906999999999996,29.398000000000003 +2020-07-05 02:00:00,53.25,14.642000000000001,25.938000000000002,29.398000000000003 +2020-07-05 02:15:00,53.53,14.513,25.938000000000002,29.398000000000003 +2020-07-05 02:30:00,53.44,15.464,25.938000000000002,29.398000000000003 +2020-07-05 02:45:00,52.89,15.967,25.938000000000002,29.398000000000003 +2020-07-05 03:00:00,52.29,17.232,24.693,29.398000000000003 +2020-07-05 03:15:00,53.06,14.776,24.693,29.398000000000003 +2020-07-05 03:30:00,52.75,14.513,24.693,29.398000000000003 +2020-07-05 03:45:00,51.46,14.513,24.693,29.398000000000003 +2020-07-05 04:00:00,51.22,17.359,25.683000000000003,29.398000000000003 +2020-07-05 04:15:00,51.09,20.709,25.683000000000003,29.398000000000003 +2020-07-05 04:30:00,50.47,18.782,25.683000000000003,29.398000000000003 +2020-07-05 04:45:00,51.05,17.98,25.683000000000003,29.398000000000003 +2020-07-05 05:00:00,50.86,22.613000000000003,26.023000000000003,29.398000000000003 +2020-07-05 05:15:00,50.47,19.552,26.023000000000003,29.398000000000003 +2020-07-05 05:30:00,48.1,14.87,26.023000000000003,29.398000000000003 +2020-07-05 05:45:00,51.33,15.972000000000001,26.023000000000003,29.398000000000003 +2020-07-05 06:00:00,51.68,25.519000000000002,25.834,29.398000000000003 +2020-07-05 06:15:00,51.55,33.667,25.834,29.398000000000003 +2020-07-05 06:30:00,52.79,29.468000000000004,25.834,29.398000000000003 +2020-07-05 06:45:00,53.9,27.323,25.834,29.398000000000003 +2020-07-05 07:00:00,53.77,27.261999999999997,27.765,29.398000000000003 +2020-07-05 07:15:00,53.67,25.614,27.765,29.398000000000003 +2020-07-05 07:30:00,52.11,24.101,27.765,29.398000000000003 +2020-07-05 07:45:00,54.99,24.828000000000003,27.765,29.398000000000003 +2020-07-05 08:00:00,55.61,22.898000000000003,31.357,29.398000000000003 +2020-07-05 08:15:00,55.39,27.529,31.357,29.398000000000003 +2020-07-05 08:30:00,56.12,29.285999999999998,31.357,29.398000000000003 +2020-07-05 08:45:00,56.36,32.464,31.357,29.398000000000003 +2020-07-05 09:00:00,57.17,27.238000000000003,33.238,29.398000000000003 +2020-07-05 09:15:00,57.85,28.448,33.238,29.398000000000003 +2020-07-05 09:30:00,60.75,32.537,33.238,29.398000000000003 +2020-07-05 09:45:00,58.0,37.027,33.238,29.398000000000003 +2020-07-05 10:00:00,61.54,35.167,34.22,29.398000000000003 +2020-07-05 10:15:00,62.53,37.032,34.22,29.398000000000003 +2020-07-05 10:30:00,60.64,37.488,34.22,29.398000000000003 +2020-07-05 10:45:00,64.41,39.474000000000004,34.22,29.398000000000003 +2020-07-05 11:00:00,63.97,36.913000000000004,36.298,29.398000000000003 +2020-07-05 11:15:00,62.55,37.61,36.298,29.398000000000003 +2020-07-05 11:30:00,61.59,39.439,36.298,29.398000000000003 +2020-07-05 11:45:00,61.53,40.755,36.298,29.398000000000003 +2020-07-05 12:00:00,58.57,37.967,33.52,29.398000000000003 +2020-07-05 12:15:00,59.34,36.952,33.52,29.398000000000003 +2020-07-05 12:30:00,56.89,36.068000000000005,33.52,29.398000000000003 +2020-07-05 12:45:00,56.14,36.559,33.52,29.398000000000003 +2020-07-05 13:00:00,56.09,36.25,30.12,29.398000000000003 +2020-07-05 13:15:00,58.54,36.499,30.12,29.398000000000003 +2020-07-05 13:30:00,53.91,34.625,30.12,29.398000000000003 +2020-07-05 13:45:00,54.48,34.336999999999996,30.12,29.398000000000003 +2020-07-05 14:00:00,53.1,36.564,27.233,29.398000000000003 +2020-07-05 14:15:00,52.56,35.042,27.233,29.398000000000003 +2020-07-05 14:30:00,53.24,33.861999999999995,27.233,29.398000000000003 +2020-07-05 14:45:00,53.16,33.063,27.233,29.398000000000003 +2020-07-05 15:00:00,52.17,35.689,27.468000000000004,29.398000000000003 +2020-07-05 15:15:00,53.08,32.861999999999995,27.468000000000004,29.398000000000003 +2020-07-05 15:30:00,52.72,30.621,27.468000000000004,29.398000000000003 +2020-07-05 15:45:00,52.87,28.991999999999997,27.468000000000004,29.398000000000003 +2020-07-05 16:00:00,58.7,31.435,30.8,29.398000000000003 +2020-07-05 16:15:00,59.41,30.967,30.8,29.398000000000003 +2020-07-05 16:30:00,58.69,31.108,30.8,29.398000000000003 +2020-07-05 16:45:00,63.11,27.015,30.8,29.398000000000003 +2020-07-05 17:00:00,67.05,31.717,37.806,29.398000000000003 +2020-07-05 17:15:00,64.23,30.783,37.806,29.398000000000003 +2020-07-05 17:30:00,65.25,30.528000000000002,37.806,29.398000000000003 +2020-07-05 17:45:00,68.4,29.974,37.806,29.398000000000003 +2020-07-05 18:00:00,72.02,35.104,40.766,29.398000000000003 +2020-07-05 18:15:00,71.05,35.239000000000004,40.766,29.398000000000003 +2020-07-05 18:30:00,71.03,34.275999999999996,40.766,29.398000000000003 +2020-07-05 18:45:00,71.01,34.335,40.766,29.398000000000003 +2020-07-05 19:00:00,71.74,36.906,41.163000000000004,29.398000000000003 +2020-07-05 19:15:00,70.17,34.306999999999995,41.163000000000004,29.398000000000003 +2020-07-05 19:30:00,70.19,33.819,41.163000000000004,29.398000000000003 +2020-07-05 19:45:00,70.61,33.332,41.163000000000004,29.398000000000003 +2020-07-05 20:00:00,70.45,31.316,39.885999999999996,29.398000000000003 +2020-07-05 20:15:00,71.14,30.765,39.885999999999996,29.398000000000003 +2020-07-05 20:30:00,75.63,30.69,39.885999999999996,29.398000000000003 +2020-07-05 20:45:00,75.49,30.666,39.885999999999996,29.398000000000003 +2020-07-05 21:00:00,75.9,28.771,38.900999999999996,29.398000000000003 +2020-07-05 21:15:00,76.48,31.448,38.900999999999996,29.398000000000003 +2020-07-05 21:30:00,74.17,31.642,38.900999999999996,29.398000000000003 +2020-07-05 21:45:00,73.12,32.73,38.900999999999996,29.398000000000003 +2020-07-05 22:00:00,70.17,31.783,39.806999999999995,29.398000000000003 +2020-07-05 22:15:00,70.35,32.641999999999996,39.806999999999995,29.398000000000003 +2020-07-05 22:30:00,68.44,32.145,39.806999999999995,29.398000000000003 +2020-07-05 22:45:00,68.89,28.736,39.806999999999995,29.398000000000003 +2020-07-05 23:00:00,66.69,26.328000000000003,35.564,29.398000000000003 +2020-07-05 23:15:00,65.66,24.987,35.564,29.398000000000003 +2020-07-05 23:30:00,64.82,23.948,35.564,29.398000000000003 +2020-07-05 23:45:00,63.72,23.485,35.564,29.398000000000003 +2020-07-06 00:00:00,62.93,21.053,36.578,29.509 +2020-07-06 00:15:00,62.05,21.09,36.578,29.509 +2020-07-06 00:30:00,61.97,19.039,36.578,29.509 +2020-07-06 00:45:00,61.79,18.081,36.578,29.509 +2020-07-06 01:00:00,61.64,18.625,35.292,29.509 +2020-07-06 01:15:00,60.91,17.667,35.292,29.509 +2020-07-06 01:30:00,60.67,16.254,35.292,29.509 +2020-07-06 01:45:00,59.95,16.808,35.292,29.509 +2020-07-06 02:00:00,61.3,17.049,34.319,29.509 +2020-07-06 02:15:00,61.83,14.513,34.319,29.509 +2020-07-06 02:30:00,62.07,16.898,34.319,29.509 +2020-07-06 02:45:00,62.17,17.285,34.319,29.509 +2020-07-06 03:00:00,65.43,19.002,33.13,29.509 +2020-07-06 03:15:00,64.03,17.215,33.13,29.509 +2020-07-06 03:30:00,65.09,16.549,33.13,29.509 +2020-07-06 03:45:00,65.84,16.95,33.13,29.509 +2020-07-06 04:00:00,67.57,23.045,33.851,29.509 +2020-07-06 04:15:00,68.87,29.180999999999997,33.851,29.509 +2020-07-06 04:30:00,71.48,26.636999999999997,33.851,29.509 +2020-07-06 04:45:00,75.03,26.21,33.851,29.509 +2020-07-06 05:00:00,80.19,37.529,38.718,29.509 +2020-07-06 05:15:00,84.78,44.1,38.718,29.509 +2020-07-06 05:30:00,88.76,37.885,38.718,29.509 +2020-07-06 05:45:00,91.27,36.103,38.718,29.509 +2020-07-06 06:00:00,94.97,35.835,51.648999999999994,29.509 +2020-07-06 06:15:00,100.81,35.295,51.648999999999994,29.509 +2020-07-06 06:30:00,107.42,34.845,51.648999999999994,29.509 +2020-07-06 06:45:00,109.2,37.764,51.648999999999994,29.509 +2020-07-06 07:00:00,100.07,37.709,60.159,29.509 +2020-07-06 07:15:00,99.42,38.38,60.159,29.509 +2020-07-06 07:30:00,109.47,36.021,60.159,29.509 +2020-07-06 07:45:00,108.74,37.223,60.159,29.509 +2020-07-06 08:00:00,103.48,33.216,53.8,29.509 +2020-07-06 08:15:00,101.9,36.735,53.8,29.509 +2020-07-06 08:30:00,102.67,37.72,53.8,29.509 +2020-07-06 08:45:00,102.51,41.309,53.8,29.509 +2020-07-06 09:00:00,108.05,34.997,50.583,29.509 +2020-07-06 09:15:00,108.16,34.559,50.583,29.509 +2020-07-06 09:30:00,103.79,37.734,50.583,29.509 +2020-07-06 09:45:00,100.1,39.804,50.583,29.509 +2020-07-06 10:00:00,100.26,38.406,49.11600000000001,29.509 +2020-07-06 10:15:00,98.97,40.135999999999996,49.11600000000001,29.509 +2020-07-06 10:30:00,101.45,40.166,49.11600000000001,29.509 +2020-07-06 10:45:00,105.24,40.429,49.11600000000001,29.509 +2020-07-06 11:00:00,115.14,38.292,49.056000000000004,29.509 +2020-07-06 11:15:00,114.12,39.161,49.056000000000004,29.509 +2020-07-06 11:30:00,109.51,41.618,49.056000000000004,29.509 +2020-07-06 11:45:00,107.16,43.486999999999995,49.056000000000004,29.509 +2020-07-06 12:00:00,98.95,38.736,47.227,29.509 +2020-07-06 12:15:00,97.34,37.839,47.227,29.509 +2020-07-06 12:30:00,94.65,35.788000000000004,47.227,29.509 +2020-07-06 12:45:00,94.35,36.159,47.227,29.509 +2020-07-06 13:00:00,95.56,36.779,47.006,29.509 +2020-07-06 13:15:00,94.55,36.189,47.006,29.509 +2020-07-06 13:30:00,93.02,34.541,47.006,29.509 +2020-07-06 13:45:00,98.7,35.205999999999996,47.006,29.509 +2020-07-06 14:00:00,95.14,36.535,47.19,29.509 +2020-07-06 14:15:00,91.16,35.689,47.19,29.509 +2020-07-06 14:30:00,89.41,34.361999999999995,47.19,29.509 +2020-07-06 14:45:00,88.76,35.762,47.19,29.509 +2020-07-06 15:00:00,87.58,37.905,47.846000000000004,29.509 +2020-07-06 15:15:00,88.49,34.553000000000004,47.846000000000004,29.509 +2020-07-06 15:30:00,88.04,33.184,47.846000000000004,29.509 +2020-07-06 15:45:00,88.3,31.066999999999997,47.846000000000004,29.509 +2020-07-06 16:00:00,88.8,34.711999999999996,49.641000000000005,29.509 +2020-07-06 16:15:00,90.56,34.364000000000004,49.641000000000005,29.509 +2020-07-06 16:30:00,93.03,33.887,49.641000000000005,29.509 +2020-07-06 16:45:00,95.66,29.892,49.641000000000005,29.509 +2020-07-06 17:00:00,98.75,33.438,54.133,29.509 +2020-07-06 17:15:00,99.58,32.971,54.133,29.509 +2020-07-06 17:30:00,99.52,32.346,54.133,29.509 +2020-07-06 17:45:00,99.38,31.47,54.133,29.509 +2020-07-06 18:00:00,99.13,35.501999999999995,53.761,29.509 +2020-07-06 18:15:00,101.41,33.836999999999996,53.761,29.509 +2020-07-06 18:30:00,101.33,32.049,53.761,29.509 +2020-07-06 18:45:00,99.47,35.366,53.761,29.509 +2020-07-06 19:00:00,96.1,37.764,53.923,29.509 +2020-07-06 19:15:00,92.71,36.589,53.923,29.509 +2020-07-06 19:30:00,92.02,35.689,53.923,29.509 +2020-07-06 19:45:00,92.82,34.603,53.923,29.509 +2020-07-06 20:00:00,91.58,31.381,58.786,29.509 +2020-07-06 20:15:00,89.83,32.415,58.786,29.509 +2020-07-06 20:30:00,90.8,32.982,58.786,29.509 +2020-07-06 20:45:00,90.9,33.107,58.786,29.509 +2020-07-06 21:00:00,88.74,30.525,54.591,29.509 +2020-07-06 21:15:00,87.51,33.721,54.591,29.509 +2020-07-06 21:30:00,85.2,34.366,54.591,29.509 +2020-07-06 21:45:00,83.29,35.247,54.591,29.509 +2020-07-06 22:00:00,80.54,32.321999999999996,51.551,29.509 +2020-07-06 22:15:00,79.21,35.305,51.551,29.509 +2020-07-06 22:30:00,78.44,30.779,51.551,29.509 +2020-07-06 22:45:00,76.79,27.581999999999997,51.551,29.509 +2020-07-06 23:00:00,75.39,25.141,44.716,29.509 +2020-07-06 23:15:00,74.53,21.991,44.716,29.509 +2020-07-06 23:30:00,74.57,20.794,44.716,29.509 +2020-07-06 23:45:00,72.99,19.643,44.716,29.509 +2020-07-07 00:00:00,71.53,18.721,43.01,29.509 +2020-07-07 00:15:00,70.84,19.582,43.01,29.509 +2020-07-07 00:30:00,71.39,18.327,43.01,29.509 +2020-07-07 00:45:00,70.56,18.253,43.01,29.509 +2020-07-07 01:00:00,71.2,18.25,40.687,29.509 +2020-07-07 01:15:00,70.9,17.422,40.687,29.509 +2020-07-07 01:30:00,70.33,15.867,40.687,29.509 +2020-07-07 01:45:00,70.39,15.862,40.687,29.509 +2020-07-07 02:00:00,70.97,15.605,39.554,29.509 +2020-07-07 02:15:00,70.84,14.513,39.554,29.509 +2020-07-07 02:30:00,77.92,16.289,39.554,29.509 +2020-07-07 02:45:00,78.65,17.02,39.554,29.509 +2020-07-07 03:00:00,75.67,18.214000000000002,38.958,29.509 +2020-07-07 03:15:00,70.77,17.46,38.958,29.509 +2020-07-07 03:30:00,74.81,16.742,38.958,29.509 +2020-07-07 03:45:00,74.03,15.989,38.958,29.509 +2020-07-07 04:00:00,77.7,20.735,39.783,29.509 +2020-07-07 04:15:00,82.93,26.872,39.783,29.509 +2020-07-07 04:30:00,86.8,24.212,39.783,29.509 +2020-07-07 04:45:00,87.2,24.226999999999997,39.783,29.509 +2020-07-07 05:00:00,87.99,36.885999999999996,42.281000000000006,29.509 +2020-07-07 05:15:00,95.27,44.023999999999994,42.281000000000006,29.509 +2020-07-07 05:30:00,96.03,38.126999999999995,42.281000000000006,29.509 +2020-07-07 05:45:00,100.96,35.614000000000004,42.281000000000006,29.509 +2020-07-07 06:00:00,111.3,36.497,50.801,29.509 +2020-07-07 06:15:00,114.63,36.082,50.801,29.509 +2020-07-07 06:30:00,116.56,35.349000000000004,50.801,29.509 +2020-07-07 06:45:00,116.13,37.391,50.801,29.509 +2020-07-07 07:00:00,116.59,37.498000000000005,60.202,29.509 +2020-07-07 07:15:00,121.92,37.93,60.202,29.509 +2020-07-07 07:30:00,124.36,35.623000000000005,60.202,29.509 +2020-07-07 07:45:00,123.71,35.817,60.202,29.509 +2020-07-07 08:00:00,119.38,31.735,54.461000000000006,29.509 +2020-07-07 08:15:00,124.37,34.859,54.461000000000006,29.509 +2020-07-07 08:30:00,128.67,36.022,54.461000000000006,29.509 +2020-07-07 08:45:00,130.1,38.647,54.461000000000006,29.509 +2020-07-07 09:00:00,129.85,32.748000000000005,50.753,29.509 +2020-07-07 09:15:00,128.0,32.054,50.753,29.509 +2020-07-07 09:30:00,130.61,35.906,50.753,29.509 +2020-07-07 09:45:00,129.99,39.417,50.753,29.509 +2020-07-07 10:00:00,122.45,36.611999999999995,49.703,29.509 +2020-07-07 10:15:00,120.63,38.272,49.703,29.509 +2020-07-07 10:30:00,122.78,38.313,49.703,29.509 +2020-07-07 10:45:00,123.45,39.629,49.703,29.509 +2020-07-07 11:00:00,128.68,37.385,49.42100000000001,29.509 +2020-07-07 11:15:00,125.65,38.714,49.42100000000001,29.509 +2020-07-07 11:30:00,122.76,40.018,49.42100000000001,29.509 +2020-07-07 11:45:00,121.95,41.35,49.42100000000001,29.509 +2020-07-07 12:00:00,118.98,36.525999999999996,47.155,29.509 +2020-07-07 12:15:00,125.09,36.022,47.155,29.509 +2020-07-07 12:30:00,122.87,34.824,47.155,29.509 +2020-07-07 12:45:00,119.92,35.959,47.155,29.509 +2020-07-07 13:00:00,121.51,36.201,47.515,29.509 +2020-07-07 13:15:00,124.02,37.568000000000005,47.515,29.509 +2020-07-07 13:30:00,121.3,35.8,47.515,29.509 +2020-07-07 13:45:00,120.45,35.42,47.515,29.509 +2020-07-07 14:00:00,111.95,37.211,47.575,29.509 +2020-07-07 14:15:00,112.15,36.14,47.575,29.509 +2020-07-07 14:30:00,118.73,35.104,47.575,29.509 +2020-07-07 14:45:00,117.88,35.671,47.575,29.509 +2020-07-07 15:00:00,112.52,37.69,48.903,29.509 +2020-07-07 15:15:00,106.84,35.296,48.903,29.509 +2020-07-07 15:30:00,107.4,33.681999999999995,48.903,29.509 +2020-07-07 15:45:00,99.77,31.961,48.903,29.509 +2020-07-07 16:00:00,96.93,34.83,50.218999999999994,29.509 +2020-07-07 16:15:00,106.09,34.543,50.218999999999994,29.509 +2020-07-07 16:30:00,106.41,33.641,50.218999999999994,29.509 +2020-07-07 16:45:00,104.59,30.386999999999997,50.218999999999994,29.509 +2020-07-07 17:00:00,99.96,34.125,55.396,29.509 +2020-07-07 17:15:00,103.8,34.11,55.396,29.509 +2020-07-07 17:30:00,100.99,32.974000000000004,55.396,29.509 +2020-07-07 17:45:00,102.8,31.754,55.396,29.509 +2020-07-07 18:00:00,108.74,34.804,55.583999999999996,29.509 +2020-07-07 18:15:00,108.95,34.722,55.583999999999996,29.509 +2020-07-07 18:30:00,107.59,32.679,55.583999999999996,29.509 +2020-07-07 18:45:00,103.26,35.739000000000004,55.583999999999996,29.509 +2020-07-07 19:00:00,99.07,36.917,56.071000000000005,29.509 +2020-07-07 19:15:00,94.57,35.938,56.071000000000005,29.509 +2020-07-07 19:30:00,92.99,34.843,56.071000000000005,29.509 +2020-07-07 19:45:00,92.6,34.101,56.071000000000005,29.509 +2020-07-07 20:00:00,91.59,31.27,61.55,29.509 +2020-07-07 20:15:00,90.41,30.826999999999998,61.55,29.509 +2020-07-07 20:30:00,90.06,31.373,61.55,29.509 +2020-07-07 20:45:00,90.65,31.985,61.55,29.509 +2020-07-07 21:00:00,90.29,30.355999999999998,55.94,29.509 +2020-07-07 21:15:00,88.11,31.968000000000004,55.94,29.509 +2020-07-07 21:30:00,85.72,32.819,55.94,29.509 +2020-07-07 21:45:00,84.05,33.887,55.94,29.509 +2020-07-07 22:00:00,82.01,31.055,52.857,29.509 +2020-07-07 22:15:00,79.35,33.702,52.857,29.509 +2020-07-07 22:30:00,77.27,29.464000000000002,52.857,29.509 +2020-07-07 22:45:00,75.9,26.239,52.857,29.509 +2020-07-07 23:00:00,73.88,23.0,46.04,29.509 +2020-07-07 23:15:00,72.06,21.55,46.04,29.509 +2020-07-07 23:30:00,71.73,20.384,46.04,29.509 +2020-07-07 23:45:00,70.42,19.437,46.04,29.509 +2020-07-08 00:00:00,68.87,18.723,42.195,29.509 +2020-07-08 00:15:00,68.28,19.582,42.195,29.509 +2020-07-08 00:30:00,67.17,18.331,42.195,29.509 +2020-07-08 00:45:00,67.5,18.262,42.195,29.509 +2020-07-08 01:00:00,68.65,18.267,38.82,29.509 +2020-07-08 01:15:00,68.23,17.430999999999997,38.82,29.509 +2020-07-08 01:30:00,67.33,15.877,38.82,29.509 +2020-07-08 01:45:00,67.32,15.865,38.82,29.509 +2020-07-08 02:00:00,67.95,15.613,37.023,29.509 +2020-07-08 02:15:00,67.93,14.513,37.023,29.509 +2020-07-08 02:30:00,75.12,16.292,37.023,29.509 +2020-07-08 02:45:00,73.61,17.028,37.023,29.509 +2020-07-08 03:00:00,77.07,18.215,36.818000000000005,29.509 +2020-07-08 03:15:00,72.12,17.471,36.818000000000005,29.509 +2020-07-08 03:30:00,73.91,16.761,36.818000000000005,29.509 +2020-07-08 03:45:00,71.79,16.022000000000002,36.818000000000005,29.509 +2020-07-08 04:00:00,77.39,20.733,37.495,29.509 +2020-07-08 04:15:00,82.0,26.840999999999998,37.495,29.509 +2020-07-08 04:30:00,84.87,24.173000000000002,37.495,29.509 +2020-07-08 04:45:00,83.56,24.188000000000002,37.495,29.509 +2020-07-08 05:00:00,86.92,36.789,39.858000000000004,29.509 +2020-07-08 05:15:00,93.46,43.852,39.858000000000004,29.509 +2020-07-08 05:30:00,93.59,38.008,39.858000000000004,29.509 +2020-07-08 05:45:00,96.52,35.519,39.858000000000004,29.509 +2020-07-08 06:00:00,99.68,36.399,52.867,29.509 +2020-07-08 06:15:00,102.4,35.982,52.867,29.509 +2020-07-08 06:30:00,108.54,35.269,52.867,29.509 +2020-07-08 06:45:00,113.47,37.338,52.867,29.509 +2020-07-08 07:00:00,108.0,37.436,66.061,29.509 +2020-07-08 07:15:00,102.54,37.89,66.061,29.509 +2020-07-08 07:30:00,100.01,35.586,66.061,29.509 +2020-07-08 07:45:00,101.07,35.809,66.061,29.509 +2020-07-08 08:00:00,108.39,31.736,58.532,29.509 +2020-07-08 08:15:00,116.1,34.872,58.532,29.509 +2020-07-08 08:30:00,117.34,36.025,58.532,29.509 +2020-07-08 08:45:00,115.72,38.645,58.532,29.509 +2020-07-08 09:00:00,111.76,32.743,56.047,29.509 +2020-07-08 09:15:00,108.9,32.047,56.047,29.509 +2020-07-08 09:30:00,108.08,35.893,56.047,29.509 +2020-07-08 09:45:00,116.56,39.406,56.047,29.509 +2020-07-08 10:00:00,120.29,36.609,53.823,29.509 +2020-07-08 10:15:00,118.95,38.266999999999996,53.823,29.509 +2020-07-08 10:30:00,112.11,38.305,53.823,29.509 +2020-07-08 10:45:00,109.74,39.623000000000005,53.823,29.509 +2020-07-08 11:00:00,109.06,37.376999999999995,54.184,29.509 +2020-07-08 11:15:00,108.4,38.705999999999996,54.184,29.509 +2020-07-08 11:30:00,105.6,40.0,54.184,29.509 +2020-07-08 11:45:00,107.2,41.326,54.184,29.509 +2020-07-08 12:00:00,114.53,36.522,52.628,29.509 +2020-07-08 12:15:00,112.75,36.016,52.628,29.509 +2020-07-08 12:30:00,101.99,34.808,52.628,29.509 +2020-07-08 12:45:00,99.23,35.94,52.628,29.509 +2020-07-08 13:00:00,100.2,36.167,52.31,29.509 +2020-07-08 13:15:00,102.73,37.527,52.31,29.509 +2020-07-08 13:30:00,102.61,35.765,52.31,29.509 +2020-07-08 13:45:00,102.47,35.391999999999996,52.31,29.509 +2020-07-08 14:00:00,116.5,37.185,52.278999999999996,29.509 +2020-07-08 14:15:00,112.73,36.115,52.278999999999996,29.509 +2020-07-08 14:30:00,108.85,35.069,52.278999999999996,29.509 +2020-07-08 14:45:00,109.45,35.644,52.278999999999996,29.509 +2020-07-08 15:00:00,101.37,37.671,53.306999999999995,29.509 +2020-07-08 15:15:00,100.94,35.272,53.306999999999995,29.509 +2020-07-08 15:30:00,99.62,33.659,53.306999999999995,29.509 +2020-07-08 15:45:00,97.35,31.93,53.306999999999995,29.509 +2020-07-08 16:00:00,93.45,34.812,55.358999999999995,29.509 +2020-07-08 16:15:00,94.89,34.528,55.358999999999995,29.509 +2020-07-08 16:30:00,98.99,33.643,55.358999999999995,29.509 +2020-07-08 16:45:00,99.62,30.389,55.358999999999995,29.509 +2020-07-08 17:00:00,98.93,34.132,59.211999999999996,29.509 +2020-07-08 17:15:00,98.18,34.126999999999995,59.211999999999996,29.509 +2020-07-08 17:30:00,98.72,32.994,59.211999999999996,29.509 +2020-07-08 17:45:00,100.38,31.776999999999997,59.211999999999996,29.509 +2020-07-08 18:00:00,99.58,34.832,60.403999999999996,29.509 +2020-07-08 18:15:00,99.0,34.734,60.403999999999996,29.509 +2020-07-08 18:30:00,100.18,32.691,60.403999999999996,29.509 +2020-07-08 18:45:00,98.99,35.751999999999995,60.403999999999996,29.509 +2020-07-08 19:00:00,96.37,36.931999999999995,60.993,29.509 +2020-07-08 19:15:00,92.43,35.945,60.993,29.509 +2020-07-08 19:30:00,91.2,34.844,60.993,29.509 +2020-07-08 19:45:00,90.88,34.098,60.993,29.509 +2020-07-08 20:00:00,90.29,31.255,66.6,29.509 +2020-07-08 20:15:00,88.95,30.811,66.6,29.509 +2020-07-08 20:30:00,89.13,31.355999999999998,66.6,29.509 +2020-07-08 20:45:00,91.03,31.976999999999997,66.6,29.509 +2020-07-08 21:00:00,89.33,30.351999999999997,59.855,29.509 +2020-07-08 21:15:00,87.63,31.965999999999998,59.855,29.509 +2020-07-08 21:30:00,85.13,32.796,59.855,29.509 +2020-07-08 21:45:00,83.76,33.858000000000004,59.855,29.509 +2020-07-08 22:00:00,81.36,31.031999999999996,54.942,29.509 +2020-07-08 22:15:00,79.43,33.68,54.942,29.509 +2020-07-08 22:30:00,76.01,29.427,54.942,29.509 +2020-07-08 22:45:00,74.2,26.193,54.942,29.509 +2020-07-08 23:00:00,74.17,22.959,46.056000000000004,29.509 +2020-07-08 23:15:00,72.84,21.531999999999996,46.056000000000004,29.509 +2020-07-08 23:30:00,72.63,20.379,46.056000000000004,29.509 +2020-07-08 23:45:00,72.27,19.425,46.056000000000004,29.509 +2020-07-09 00:00:00,70.77,18.729,40.859,29.509 +2020-07-09 00:15:00,69.68,19.587,40.859,29.509 +2020-07-09 00:30:00,68.62,18.339000000000002,40.859,29.509 +2020-07-09 00:45:00,68.26,18.276,40.859,29.509 +2020-07-09 01:00:00,68.62,18.287,39.06,29.509 +2020-07-09 01:15:00,67.79,17.444000000000003,39.06,29.509 +2020-07-09 01:30:00,66.52,15.892000000000001,39.06,29.509 +2020-07-09 01:45:00,67.76,15.872,39.06,29.509 +2020-07-09 02:00:00,68.72,15.626,37.592,29.509 +2020-07-09 02:15:00,67.64,14.513,37.592,29.509 +2020-07-09 02:30:00,67.33,16.301,37.592,29.509 +2020-07-09 02:45:00,67.89,17.039,37.592,29.509 +2020-07-09 03:00:00,69.13,18.219,37.416,29.509 +2020-07-09 03:15:00,70.24,17.485,37.416,29.509 +2020-07-09 03:30:00,71.26,16.783,37.416,29.509 +2020-07-09 03:45:00,72.08,16.06,37.416,29.509 +2020-07-09 04:00:00,76.24,20.737,38.176,29.509 +2020-07-09 04:15:00,75.02,26.815,38.176,29.509 +2020-07-09 04:30:00,77.62,24.14,38.176,29.509 +2020-07-09 04:45:00,80.21,24.154,38.176,29.509 +2020-07-09 05:00:00,85.53,36.703,41.203,29.509 +2020-07-09 05:15:00,89.78,43.691,41.203,29.509 +2020-07-09 05:30:00,93.38,37.9,41.203,29.509 +2020-07-09 05:45:00,101.99,35.434,41.203,29.509 +2020-07-09 06:00:00,106.23,36.31,51.09,29.509 +2020-07-09 06:15:00,109.0,35.891999999999996,51.09,29.509 +2020-07-09 06:30:00,106.78,35.196999999999996,51.09,29.509 +2020-07-09 06:45:00,104.81,37.294000000000004,51.09,29.509 +2020-07-09 07:00:00,106.36,37.384,63.541000000000004,29.509 +2020-07-09 07:15:00,110.69,37.859,63.541000000000004,29.509 +2020-07-09 07:30:00,112.06,35.56,63.541000000000004,29.509 +2020-07-09 07:45:00,118.5,35.809,63.541000000000004,29.509 +2020-07-09 08:00:00,121.66,31.746,55.65,29.509 +2020-07-09 08:15:00,122.22,34.893,55.65,29.509 +2020-07-09 08:30:00,118.81,36.037,55.65,29.509 +2020-07-09 08:45:00,116.9,38.649,55.65,29.509 +2020-07-09 09:00:00,117.8,32.743,51.833999999999996,29.509 +2020-07-09 09:15:00,120.31,32.048,51.833999999999996,29.509 +2020-07-09 09:30:00,120.69,35.887,51.833999999999996,29.509 +2020-07-09 09:45:00,124.35,39.402,51.833999999999996,29.509 +2020-07-09 10:00:00,130.54,36.613,49.70399999999999,29.509 +2020-07-09 10:15:00,131.59,38.268,49.70399999999999,29.509 +2020-07-09 10:30:00,127.98,38.302,49.70399999999999,29.509 +2020-07-09 10:45:00,122.86,39.621,49.70399999999999,29.509 +2020-07-09 11:00:00,122.53,37.374,48.593999999999994,29.509 +2020-07-09 11:15:00,121.63,38.705,48.593999999999994,29.509 +2020-07-09 11:30:00,121.85,39.986,48.593999999999994,29.509 +2020-07-09 11:45:00,120.39,41.306999999999995,48.593999999999994,29.509 +2020-07-09 12:00:00,120.74,36.523,46.275,29.509 +2020-07-09 12:15:00,120.27,36.015,46.275,29.509 +2020-07-09 12:30:00,117.63,34.798,46.275,29.509 +2020-07-09 12:45:00,114.26,35.927,46.275,29.509 +2020-07-09 13:00:00,109.73,36.135999999999996,45.803000000000004,29.509 +2020-07-09 13:15:00,108.7,37.491,45.803000000000004,29.509 +2020-07-09 13:30:00,113.78,35.734,45.803000000000004,29.509 +2020-07-09 13:45:00,117.0,35.368,45.803000000000004,29.509 +2020-07-09 14:00:00,110.72,37.163000000000004,46.251999999999995,29.509 +2020-07-09 14:15:00,111.27,36.095,46.251999999999995,29.509 +2020-07-09 14:30:00,112.08,35.039,46.251999999999995,29.509 +2020-07-09 14:45:00,109.67,35.62,46.251999999999995,29.509 +2020-07-09 15:00:00,109.08,37.655,48.309,29.509 +2020-07-09 15:15:00,104.89,35.251,48.309,29.509 +2020-07-09 15:30:00,100.7,33.638000000000005,48.309,29.509 +2020-07-09 15:45:00,104.1,31.903000000000002,48.309,29.509 +2020-07-09 16:00:00,103.6,34.797,49.681999999999995,29.509 +2020-07-09 16:15:00,105.47,34.515,49.681999999999995,29.509 +2020-07-09 16:30:00,100.69,33.648,49.681999999999995,29.509 +2020-07-09 16:45:00,101.97,30.396,49.681999999999995,29.509 +2020-07-09 17:00:00,103.65,34.143,53.086000000000006,29.509 +2020-07-09 17:15:00,103.74,34.149,53.086000000000006,29.509 +2020-07-09 17:30:00,105.78,33.018,53.086000000000006,29.509 +2020-07-09 17:45:00,108.82,31.804000000000002,53.086000000000006,29.509 +2020-07-09 18:00:00,108.39,34.865,54.038999999999994,29.509 +2020-07-09 18:15:00,108.03,34.75,54.038999999999994,29.509 +2020-07-09 18:30:00,106.59,32.711,54.038999999999994,29.509 +2020-07-09 18:45:00,103.0,35.77,54.038999999999994,29.509 +2020-07-09 19:00:00,100.88,36.952,53.408,29.509 +2020-07-09 19:15:00,97.96,35.959,53.408,29.509 +2020-07-09 19:30:00,94.18,34.851,53.408,29.509 +2020-07-09 19:45:00,92.67,34.101,53.408,29.509 +2020-07-09 20:00:00,90.99,31.249000000000002,55.309,29.509 +2020-07-09 20:15:00,90.23,30.802,55.309,29.509 +2020-07-09 20:30:00,90.8,31.345,55.309,29.509 +2020-07-09 20:45:00,92.1,31.975,55.309,29.509 +2020-07-09 21:00:00,89.34,30.354,51.585,29.509 +2020-07-09 21:15:00,87.34,31.968000000000004,51.585,29.509 +2020-07-09 21:30:00,84.56,32.778,51.585,29.509 +2020-07-09 21:45:00,83.79,33.833,51.585,29.509 +2020-07-09 22:00:00,80.79,31.013,48.006,29.509 +2020-07-09 22:15:00,78.88,33.661,48.006,29.509 +2020-07-09 22:30:00,76.76,29.392,48.006,29.509 +2020-07-09 22:45:00,75.3,26.151999999999997,48.006,29.509 +2020-07-09 23:00:00,73.05,22.921,42.309,29.509 +2020-07-09 23:15:00,72.92,21.516,42.309,29.509 +2020-07-09 23:30:00,71.81,20.377,42.309,29.509 +2020-07-09 23:45:00,70.78,19.418,42.309,29.509 +2020-07-10 00:00:00,75.87,16.908,39.649,29.509 +2020-07-10 00:15:00,78.17,17.980999999999998,39.649,29.509 +2020-07-10 00:30:00,75.85,17.027,39.649,29.509 +2020-07-10 00:45:00,71.62,17.408,39.649,29.509 +2020-07-10 01:00:00,69.81,17.038,37.744,29.509 +2020-07-10 01:15:00,68.97,15.522,37.744,29.509 +2020-07-10 01:30:00,67.92,14.729000000000001,37.744,29.509 +2020-07-10 01:45:00,69.22,14.513,37.744,29.509 +2020-07-10 02:00:00,68.97,15.165999999999999,36.965,29.509 +2020-07-10 02:15:00,71.25,14.513,36.965,29.509 +2020-07-10 02:30:00,75.07,16.63,36.965,29.509 +2020-07-10 02:45:00,74.85,16.655,36.965,29.509 +2020-07-10 03:00:00,75.76,18.677,37.678000000000004,29.509 +2020-07-10 03:15:00,71.23,16.657,37.678000000000004,29.509 +2020-07-10 03:30:00,77.96,15.714,37.678000000000004,29.509 +2020-07-10 03:45:00,73.99,15.908,37.678000000000004,29.509 +2020-07-10 04:00:00,73.54,20.688000000000002,38.591,29.509 +2020-07-10 04:15:00,74.61,25.119,38.591,29.509 +2020-07-10 04:30:00,77.28,23.421,38.591,29.509 +2020-07-10 04:45:00,79.7,22.803,38.591,29.509 +2020-07-10 05:00:00,86.23,34.849000000000004,40.666,29.509 +2020-07-10 05:15:00,91.4,42.71,40.666,29.509 +2020-07-10 05:30:00,95.84,37.144,40.666,29.509 +2020-07-10 05:45:00,103.23,34.24,40.666,29.509 +2020-07-10 06:00:00,106.94,35.33,51.784,29.509 +2020-07-10 06:15:00,106.98,35.125,51.784,29.509 +2020-07-10 06:30:00,107.3,34.42,51.784,29.509 +2020-07-10 06:45:00,110.82,36.363,51.784,29.509 +2020-07-10 07:00:00,103.93,37.14,61.383,29.509 +2020-07-10 07:15:00,103.93,38.543,61.383,29.509 +2020-07-10 07:30:00,100.62,34.223,61.383,29.509 +2020-07-10 07:45:00,107.12,34.312,61.383,29.509 +2020-07-10 08:00:00,114.53,31.116999999999997,55.272,29.509 +2020-07-10 08:15:00,110.12,34.992,55.272,29.509 +2020-07-10 08:30:00,106.93,35.983000000000004,55.272,29.509 +2020-07-10 08:45:00,109.04,38.505,55.272,29.509 +2020-07-10 09:00:00,114.76,30.16,53.506,29.509 +2020-07-10 09:15:00,113.51,31.453000000000003,53.506,29.509 +2020-07-10 09:30:00,110.89,34.580999999999996,53.506,29.509 +2020-07-10 09:45:00,107.02,38.509,53.506,29.509 +2020-07-10 10:00:00,113.21,35.611,51.363,29.509 +2020-07-10 10:15:00,111.84,36.994,51.363,29.509 +2020-07-10 10:30:00,112.42,37.61,51.363,29.509 +2020-07-10 10:45:00,110.71,38.852,51.363,29.509 +2020-07-10 11:00:00,112.67,36.874,51.043,29.509 +2020-07-10 11:15:00,113.11,37.103,51.043,29.509 +2020-07-10 11:30:00,107.88,37.903,51.043,29.509 +2020-07-10 11:45:00,109.91,38.202,51.043,29.509 +2020-07-10 12:00:00,111.54,33.830999999999996,47.52,29.509 +2020-07-10 12:15:00,112.77,32.756,47.52,29.509 +2020-07-10 12:30:00,109.24,31.646,47.52,29.509 +2020-07-10 12:45:00,105.69,31.91,47.52,29.509 +2020-07-10 13:00:00,106.61,32.681,45.494,29.509 +2020-07-10 13:15:00,112.86,34.179,45.494,29.509 +2020-07-10 13:30:00,115.59,33.268,45.494,29.509 +2020-07-10 13:45:00,109.93,33.236999999999995,45.494,29.509 +2020-07-10 14:00:00,105.89,34.242,43.883,29.509 +2020-07-10 14:15:00,101.49,33.646,43.883,29.509 +2020-07-10 14:30:00,95.86,34.148,43.883,29.509 +2020-07-10 14:45:00,97.16,33.991,43.883,29.509 +2020-07-10 15:00:00,94.55,35.974000000000004,45.714,29.509 +2020-07-10 15:15:00,89.18,33.336999999999996,45.714,29.509 +2020-07-10 15:30:00,91.05,31.205,45.714,29.509 +2020-07-10 15:45:00,100.25,30.261999999999997,45.714,29.509 +2020-07-10 16:00:00,97.71,32.321999999999996,48.222,29.509 +2020-07-10 16:15:00,101.07,32.548,48.222,29.509 +2020-07-10 16:30:00,98.92,31.503,48.222,29.509 +2020-07-10 16:45:00,98.38,27.433000000000003,48.222,29.509 +2020-07-10 17:00:00,103.96,32.991,52.619,29.509 +2020-07-10 17:15:00,105.3,32.848,52.619,29.509 +2020-07-10 17:30:00,104.89,31.909000000000002,52.619,29.509 +2020-07-10 17:45:00,101.36,30.537,52.619,29.509 +2020-07-10 18:00:00,102.68,33.596,52.99,29.509 +2020-07-10 18:15:00,105.55,32.494,52.99,29.509 +2020-07-10 18:30:00,106.3,30.329,52.99,29.509 +2020-07-10 18:45:00,103.31,33.828,52.99,29.509 +2020-07-10 19:00:00,97.76,35.876,51.923,29.509 +2020-07-10 19:15:00,92.42,35.438,51.923,29.509 +2020-07-10 19:30:00,92.95,34.405,51.923,29.509 +2020-07-10 19:45:00,97.4,32.611,51.923,29.509 +2020-07-10 20:00:00,95.69,29.585,56.238,29.509 +2020-07-10 20:15:00,92.52,29.965999999999998,56.238,29.509 +2020-07-10 20:30:00,86.88,30.0,56.238,29.509 +2020-07-10 20:45:00,87.29,29.768,56.238,29.509 +2020-07-10 21:00:00,83.47,29.494,52.426,29.509 +2020-07-10 21:15:00,83.93,32.832,52.426,29.509 +2020-07-10 21:30:00,80.36,33.465,52.426,29.509 +2020-07-10 21:45:00,84.98,34.704,52.426,29.509 +2020-07-10 22:00:00,82.87,31.701999999999998,48.196000000000005,29.509 +2020-07-10 22:15:00,79.75,34.096,48.196000000000005,29.509 +2020-07-10 22:30:00,73.88,34.513000000000005,48.196000000000005,29.509 +2020-07-10 22:45:00,70.67,32.027,48.196000000000005,29.509 +2020-07-10 23:00:00,71.98,30.574,41.71,29.509 +2020-07-10 23:15:00,75.31,27.635,41.71,29.509 +2020-07-10 23:30:00,73.88,24.671999999999997,41.71,29.509 +2020-07-10 23:45:00,69.5,23.603,41.71,29.509 +2020-07-11 00:00:00,66.78,18.323,41.105,29.398000000000003 +2020-07-11 00:15:00,65.61,18.863,41.105,29.398000000000003 +2020-07-11 00:30:00,68.91,17.372,41.105,29.398000000000003 +2020-07-11 00:45:00,71.3,16.97,41.105,29.398000000000003 +2020-07-11 01:00:00,71.31,16.866,36.934,29.398000000000003 +2020-07-11 01:15:00,66.13,15.995999999999999,36.934,29.398000000000003 +2020-07-11 01:30:00,62.39,14.513,36.934,29.398000000000003 +2020-07-11 01:45:00,65.34,15.394,36.934,29.398000000000003 +2020-07-11 02:00:00,69.2,15.074000000000002,34.782,29.398000000000003 +2020-07-11 02:15:00,69.08,14.513,34.782,29.398000000000003 +2020-07-11 02:30:00,66.46,14.948,34.782,29.398000000000003 +2020-07-11 02:45:00,61.07,15.79,34.782,29.398000000000003 +2020-07-11 03:00:00,61.65,16.352,34.489000000000004,29.398000000000003 +2020-07-11 03:15:00,67.74,14.513,34.489000000000004,29.398000000000003 +2020-07-11 03:30:00,68.92,14.513,34.489000000000004,29.398000000000003 +2020-07-11 03:45:00,67.2,14.882,34.489000000000004,29.398000000000003 +2020-07-11 04:00:00,61.39,17.837,34.111,29.398000000000003 +2020-07-11 04:15:00,62.51,21.416,34.111,29.398000000000003 +2020-07-11 04:30:00,67.0,18.064,34.111,29.398000000000003 +2020-07-11 04:45:00,68.68,17.75,34.111,29.398000000000003 +2020-07-11 05:00:00,67.76,21.743000000000002,33.283,29.398000000000003 +2020-07-11 05:15:00,65.79,18.933,33.283,29.398000000000003 +2020-07-11 05:30:00,71.55,14.975999999999999,33.283,29.398000000000003 +2020-07-11 05:45:00,73.19,16.502,33.283,29.398000000000003 +2020-07-11 06:00:00,72.98,28.447,33.653,29.398000000000003 +2020-07-11 06:15:00,70.87,35.666,33.653,29.398000000000003 +2020-07-11 06:30:00,69.27,32.253,33.653,29.398000000000003 +2020-07-11 06:45:00,72.03,31.250999999999998,33.653,29.398000000000003 +2020-07-11 07:00:00,78.34,30.91,36.732,29.398000000000003 +2020-07-11 07:15:00,79.62,31.045,36.732,29.398000000000003 +2020-07-11 07:30:00,79.34,28.175,36.732,29.398000000000003 +2020-07-11 07:45:00,76.51,29.061,36.732,29.398000000000003 +2020-07-11 08:00:00,81.35,26.475,41.318999999999996,29.398000000000003 +2020-07-11 08:15:00,81.1,29.944000000000003,41.318999999999996,29.398000000000003 +2020-07-11 08:30:00,79.87,30.825,41.318999999999996,29.398000000000003 +2020-07-11 08:45:00,74.11,34.164,41.318999999999996,29.398000000000003 +2020-07-11 09:00:00,76.56,29.0,43.195,29.398000000000003 +2020-07-11 09:15:00,73.1,30.758000000000003,43.195,29.398000000000003 +2020-07-11 09:30:00,79.0,34.347,43.195,29.398000000000003 +2020-07-11 09:45:00,80.51,37.795,43.195,29.398000000000003 +2020-07-11 10:00:00,81.2,35.568000000000005,41.843999999999994,29.398000000000003 +2020-07-11 10:15:00,76.6,37.312,41.843999999999994,29.398000000000003 +2020-07-11 10:30:00,80.08,37.554,41.843999999999994,29.398000000000003 +2020-07-11 10:45:00,79.57,38.348,41.843999999999994,29.398000000000003 +2020-07-11 11:00:00,72.28,36.21,39.035,29.398000000000003 +2020-07-11 11:15:00,72.4,37.393,39.035,29.398000000000003 +2020-07-11 11:30:00,66.56,38.545,39.035,29.398000000000003 +2020-07-11 11:45:00,64.38,39.599000000000004,39.035,29.398000000000003 +2020-07-11 12:00:00,63.06,35.757,38.001,29.398000000000003 +2020-07-11 12:15:00,61.1,35.538000000000004,38.001,29.398000000000003 +2020-07-11 12:30:00,60.22,34.263000000000005,38.001,29.398000000000003 +2020-07-11 12:45:00,59.31,35.336999999999996,38.001,29.398000000000003 +2020-07-11 13:00:00,58.37,35.196999999999996,34.747,29.398000000000003 +2020-07-11 13:15:00,58.07,36.245,34.747,29.398000000000003 +2020-07-11 13:30:00,57.74,35.563,34.747,29.398000000000003 +2020-07-11 13:45:00,57.83,34.092,34.747,29.398000000000003 +2020-07-11 14:00:00,58.61,35.078,33.434,29.398000000000003 +2020-07-11 14:15:00,58.61,33.199,33.434,29.398000000000003 +2020-07-11 14:30:00,59.59,33.353,33.434,29.398000000000003 +2020-07-11 14:45:00,60.3,33.694,33.434,29.398000000000003 +2020-07-11 15:00:00,61.37,36.101,35.921,29.398000000000003 +2020-07-11 15:15:00,61.08,34.154,35.921,29.398000000000003 +2020-07-11 15:30:00,61.78,32.147,35.921,29.398000000000003 +2020-07-11 15:45:00,62.97,30.228,35.921,29.398000000000003 +2020-07-11 16:00:00,64.97,34.564,39.427,29.398000000000003 +2020-07-11 16:15:00,65.55,33.809,39.427,29.398000000000003 +2020-07-11 16:30:00,67.43,33.021,39.427,29.398000000000003 +2020-07-11 16:45:00,69.59,28.877,39.427,29.398000000000003 +2020-07-11 17:00:00,71.58,33.213,44.096000000000004,29.398000000000003 +2020-07-11 17:15:00,72.95,30.74,44.096000000000004,29.398000000000003 +2020-07-11 17:30:00,74.4,29.671,44.096000000000004,29.398000000000003 +2020-07-11 17:45:00,76.23,28.840999999999998,44.096000000000004,29.398000000000003 +2020-07-11 18:00:00,76.65,33.328,43.931000000000004,29.398000000000003 +2020-07-11 18:15:00,76.76,33.864000000000004,43.931000000000004,29.398000000000003 +2020-07-11 18:30:00,77.06,33.051,43.931000000000004,29.398000000000003 +2020-07-11 18:45:00,77.28,33.094,43.931000000000004,29.398000000000003 +2020-07-11 19:00:00,75.61,33.381,42.187,29.398000000000003 +2020-07-11 19:15:00,73.06,31.915,42.187,29.398000000000003 +2020-07-11 19:30:00,72.09,31.64,42.187,29.398000000000003 +2020-07-11 19:45:00,72.06,31.671,42.187,29.398000000000003 +2020-07-11 20:00:00,71.02,29.423000000000002,38.315,29.398000000000003 +2020-07-11 20:15:00,70.88,29.055,38.315,29.398000000000003 +2020-07-11 20:30:00,71.67,28.191999999999997,38.315,29.398000000000003 +2020-07-11 20:45:00,73.05,29.901999999999997,38.315,29.398000000000003 +2020-07-11 21:00:00,72.16,28.011999999999997,36.843,29.398000000000003 +2020-07-11 21:15:00,70.77,30.971999999999998,36.843,29.398000000000003 +2020-07-11 21:30:00,69.19,31.735,36.843,29.398000000000003 +2020-07-11 21:45:00,68.12,32.418,36.843,29.398000000000003 +2020-07-11 22:00:00,65.88,29.346,37.260999999999996,29.398000000000003 +2020-07-11 22:15:00,64.83,31.903000000000002,37.260999999999996,29.398000000000003 +2020-07-11 22:30:00,63.5,31.685,37.260999999999996,29.398000000000003 +2020-07-11 22:45:00,61.86,29.505,37.260999999999996,29.398000000000003 +2020-07-11 23:00:00,60.37,27.302,32.148,29.398000000000003 +2020-07-11 23:15:00,59.02,24.873,32.148,29.398000000000003 +2020-07-11 23:30:00,58.04,24.506999999999998,32.148,29.398000000000003 +2020-07-11 23:45:00,57.38,23.886,32.148,29.398000000000003 +2020-07-12 00:00:00,56.58,19.749000000000002,28.905,29.398000000000003 +2020-07-12 00:15:00,55.15,19.109,28.905,29.398000000000003 +2020-07-12 00:30:00,55.08,17.485,28.905,29.398000000000003 +2020-07-12 00:45:00,55.02,16.945,28.905,29.398000000000003 +2020-07-12 01:00:00,54.63,17.125999999999998,26.906999999999996,29.398000000000003 +2020-07-12 01:15:00,54.08,16.081,26.906999999999996,29.398000000000003 +2020-07-12 01:30:00,54.07,14.513,26.906999999999996,29.398000000000003 +2020-07-12 01:45:00,53.22,14.932,26.906999999999996,29.398000000000003 +2020-07-12 02:00:00,53.3,14.73,25.938000000000002,29.398000000000003 +2020-07-12 02:15:00,52.94,14.513,25.938000000000002,29.398000000000003 +2020-07-12 02:30:00,53.05,15.523,25.938000000000002,29.398000000000003 +2020-07-12 02:45:00,52.79,16.05,25.938000000000002,29.398000000000003 +2020-07-12 03:00:00,53.08,17.264,24.693,29.398000000000003 +2020-07-12 03:15:00,52.25,14.882,24.693,29.398000000000003 +2020-07-12 03:30:00,52.6,14.513,24.693,29.398000000000003 +2020-07-12 03:45:00,52.24,14.513,24.693,29.398000000000003 +2020-07-12 04:00:00,51.42,17.385,25.683000000000003,29.398000000000003 +2020-07-12 04:15:00,51.0,20.531,25.683000000000003,29.398000000000003 +2020-07-12 04:30:00,50.5,18.554000000000002,25.683000000000003,29.398000000000003 +2020-07-12 04:45:00,50.97,17.746,25.683000000000003,29.398000000000003 +2020-07-12 05:00:00,51.24,22.005,26.023000000000003,29.398000000000003 +2020-07-12 05:15:00,51.43,18.430999999999997,26.023000000000003,29.398000000000003 +2020-07-12 05:30:00,51.69,14.513,26.023000000000003,29.398000000000003 +2020-07-12 05:45:00,52.25,15.375,26.023000000000003,29.398000000000003 +2020-07-12 06:00:00,52.24,24.9,25.834,29.398000000000003 +2020-07-12 06:15:00,52.9,33.04,25.834,29.398000000000003 +2020-07-12 06:30:00,53.7,28.971,25.834,29.398000000000003 +2020-07-12 06:45:00,55.15,27.016,25.834,29.398000000000003 +2020-07-12 07:00:00,54.82,26.895,27.765,29.398000000000003 +2020-07-12 07:15:00,55.23,25.395,27.765,29.398000000000003 +2020-07-12 07:30:00,56.51,23.913,27.765,29.398000000000003 +2020-07-12 07:45:00,57.17,24.826999999999998,27.765,29.398000000000003 +2020-07-12 08:00:00,55.26,22.965,31.357,29.398000000000003 +2020-07-12 08:15:00,55.02,27.676,31.357,29.398000000000003 +2020-07-12 08:30:00,54.22,29.362,31.357,29.398000000000003 +2020-07-12 08:45:00,55.5,32.498000000000005,31.357,29.398000000000003 +2020-07-12 09:00:00,56.55,27.249000000000002,33.238,29.398000000000003 +2020-07-12 09:15:00,54.44,28.454,33.238,29.398000000000003 +2020-07-12 09:30:00,53.99,32.496,33.238,29.398000000000003 +2020-07-12 09:45:00,54.89,37.0,33.238,29.398000000000003 +2020-07-12 10:00:00,55.54,35.193000000000005,34.22,29.398000000000003 +2020-07-12 10:15:00,57.68,37.039,34.22,29.398000000000003 +2020-07-12 10:30:00,59.4,37.47,34.22,29.398000000000003 +2020-07-12 10:45:00,58.87,39.461,34.22,29.398000000000003 +2020-07-12 11:00:00,56.65,36.895,36.298,29.398000000000003 +2020-07-12 11:15:00,55.61,37.598,36.298,29.398000000000003 +2020-07-12 11:30:00,53.69,39.349000000000004,36.298,29.398000000000003 +2020-07-12 11:45:00,51.82,40.623000000000005,36.298,29.398000000000003 +2020-07-12 12:00:00,49.79,37.971,33.52,29.398000000000003 +2020-07-12 12:15:00,48.34,36.944,33.52,29.398000000000003 +2020-07-12 12:30:00,47.06,35.995,33.52,29.398000000000003 +2020-07-12 12:45:00,46.84,36.466,33.52,29.398000000000003 +2020-07-12 13:00:00,45.47,36.035,30.12,29.398000000000003 +2020-07-12 13:15:00,44.9,36.248000000000005,30.12,29.398000000000003 +2020-07-12 13:30:00,46.01,34.412,30.12,29.398000000000003 +2020-07-12 13:45:00,48.63,34.175,30.12,29.398000000000003 +2020-07-12 14:00:00,47.77,36.409,27.233,29.398000000000003 +2020-07-12 14:15:00,50.67,34.899,27.233,29.398000000000003 +2020-07-12 14:30:00,49.51,33.652,27.233,29.398000000000003 +2020-07-12 14:45:00,50.09,32.899,27.233,29.398000000000003 +2020-07-12 15:00:00,49.11,35.577,27.468000000000004,29.398000000000003 +2020-07-12 15:15:00,51.61,32.717,27.468000000000004,29.398000000000003 +2020-07-12 15:30:00,52.28,30.478,27.468000000000004,29.398000000000003 +2020-07-12 15:45:00,54.69,28.804000000000002,27.468000000000004,29.398000000000003 +2020-07-12 16:00:00,56.69,31.328000000000003,30.8,29.398000000000003 +2020-07-12 16:15:00,57.79,30.877,30.8,29.398000000000003 +2020-07-12 16:30:00,60.36,31.142,30.8,29.398000000000003 +2020-07-12 16:45:00,62.51,27.061999999999998,30.8,29.398000000000003 +2020-07-12 17:00:00,64.81,31.787,37.806,29.398000000000003 +2020-07-12 17:15:00,65.45,30.935,37.806,29.398000000000003 +2020-07-12 17:30:00,67.71,30.695999999999998,37.806,29.398000000000003 +2020-07-12 17:45:00,68.61,30.166999999999998,37.806,29.398000000000003 +2020-07-12 18:00:00,71.24,35.333,40.766,29.398000000000003 +2020-07-12 18:15:00,71.8,35.356,40.766,29.398000000000003 +2020-07-12 18:30:00,72.57,34.407,40.766,29.398000000000003 +2020-07-12 18:45:00,72.89,34.463,40.766,29.398000000000003 +2020-07-12 19:00:00,72.83,37.047,41.163000000000004,29.398000000000003 +2020-07-12 19:15:00,71.76,34.399,41.163000000000004,29.398000000000003 +2020-07-12 19:30:00,70.93,33.869,41.163000000000004,29.398000000000003 +2020-07-12 19:45:00,71.3,33.354,41.163000000000004,29.398000000000003 +2020-07-12 20:00:00,68.86,31.265,39.885999999999996,29.398000000000003 +2020-07-12 20:15:00,69.89,30.703000000000003,39.885999999999996,29.398000000000003 +2020-07-12 20:30:00,71.35,30.611,39.885999999999996,29.398000000000003 +2020-07-12 20:45:00,74.04,30.651,39.885999999999996,29.398000000000003 +2020-07-12 21:00:00,76.76,28.779,38.900999999999996,29.398000000000003 +2020-07-12 21:15:00,76.45,31.459,38.900999999999996,29.398000000000003 +2020-07-12 21:30:00,75.21,31.52,38.900999999999996,29.398000000000003 +2020-07-12 21:45:00,73.92,32.556,38.900999999999996,29.398000000000003 +2020-07-12 22:00:00,72.32,31.649,39.806999999999995,29.398000000000003 +2020-07-12 22:15:00,71.26,32.507,39.806999999999995,29.398000000000003 +2020-07-12 22:30:00,70.12,31.899,39.806999999999995,29.398000000000003 +2020-07-12 22:45:00,69.17,28.441999999999997,39.806999999999995,29.398000000000003 +2020-07-12 23:00:00,66.25,26.066999999999997,35.564,29.398000000000003 +2020-07-12 23:15:00,66.98,24.875999999999998,35.564,29.398000000000003 +2020-07-12 23:30:00,67.33,23.935,35.564,29.398000000000003 +2020-07-12 23:45:00,65.65,23.429000000000002,35.564,29.398000000000003 +2020-07-13 00:00:00,63.25,21.121,36.578,29.509 +2020-07-13 00:15:00,63.73,21.145,36.578,29.509 +2020-07-13 00:30:00,63.24,19.121,36.578,29.509 +2020-07-13 00:45:00,64.24,18.207,36.578,29.509 +2020-07-13 01:00:00,62.98,18.792,35.292,29.509 +2020-07-13 01:15:00,64.06,17.78,35.292,29.509 +2020-07-13 01:30:00,62.35,16.386,35.292,29.509 +2020-07-13 01:45:00,62.1,16.892,35.292,29.509 +2020-07-13 02:00:00,62.13,17.167,34.319,29.509 +2020-07-13 02:15:00,63.93,14.513,34.319,29.509 +2020-07-13 02:30:00,62.7,16.987000000000002,34.319,29.509 +2020-07-13 02:45:00,68.66,17.396,34.319,29.509 +2020-07-13 03:00:00,71.71,19.061,33.13,29.509 +2020-07-13 03:15:00,70.72,17.35,33.13,29.509 +2020-07-13 03:30:00,68.96,16.737000000000002,33.13,29.509 +2020-07-13 03:45:00,68.2,17.241,33.13,29.509 +2020-07-13 04:00:00,71.65,23.105999999999998,33.851,29.509 +2020-07-13 04:15:00,75.48,29.046,33.851,29.509 +2020-07-13 04:30:00,81.43,26.454,33.851,29.509 +2020-07-13 04:45:00,88.04,26.021,33.851,29.509 +2020-07-13 05:00:00,87.82,36.985,38.718,29.509 +2020-07-13 05:15:00,92.38,43.06399999999999,38.718,29.509 +2020-07-13 05:30:00,92.03,37.214,38.718,29.509 +2020-07-13 05:45:00,94.27,35.575,38.718,29.509 +2020-07-13 06:00:00,105.23,35.279,51.648999999999994,29.509 +2020-07-13 06:15:00,111.06,34.735,51.648999999999994,29.509 +2020-07-13 06:30:00,115.27,34.412,51.648999999999994,29.509 +2020-07-13 06:45:00,111.43,37.519,51.648999999999994,29.509 +2020-07-13 07:00:00,115.92,37.404,60.159,29.509 +2020-07-13 07:15:00,114.27,38.225,60.159,29.509 +2020-07-13 07:30:00,121.23,35.899,60.159,29.509 +2020-07-13 07:45:00,121.9,37.286,60.159,29.509 +2020-07-13 08:00:00,121.83,33.344,53.8,29.509 +2020-07-13 08:15:00,117.34,36.936,53.8,29.509 +2020-07-13 08:30:00,119.03,37.851,53.8,29.509 +2020-07-13 08:45:00,125.58,41.396,53.8,29.509 +2020-07-13 09:00:00,127.01,35.063,50.583,29.509 +2020-07-13 09:15:00,124.63,34.619,50.583,29.509 +2020-07-13 09:30:00,120.25,37.743,50.583,29.509 +2020-07-13 09:45:00,127.67,39.821999999999996,50.583,29.509 +2020-07-13 10:00:00,129.63,38.476,49.11600000000001,29.509 +2020-07-13 10:15:00,127.89,40.185,49.11600000000001,29.509 +2020-07-13 10:30:00,123.11,40.189,49.11600000000001,29.509 +2020-07-13 10:45:00,122.39,40.455,49.11600000000001,29.509 +2020-07-13 11:00:00,118.84,38.315,49.056000000000004,29.509 +2020-07-13 11:15:00,121.61,39.187,49.056000000000004,29.509 +2020-07-13 11:30:00,114.5,41.566,49.056000000000004,29.509 +2020-07-13 11:45:00,110.96,43.393,49.056000000000004,29.509 +2020-07-13 12:00:00,107.31,38.772,47.227,29.509 +2020-07-13 12:15:00,105.66,37.86,47.227,29.509 +2020-07-13 12:30:00,102.31,35.75,47.227,29.509 +2020-07-13 12:45:00,93.18,36.098,47.227,29.509 +2020-07-13 13:00:00,92.65,36.595,47.006,29.509 +2020-07-13 13:15:00,92.56,35.967,47.006,29.509 +2020-07-13 13:30:00,91.16,34.355,47.006,29.509 +2020-07-13 13:45:00,91.85,35.071999999999996,47.006,29.509 +2020-07-13 14:00:00,90.82,36.405,47.19,29.509 +2020-07-13 14:15:00,91.72,35.571999999999996,47.19,29.509 +2020-07-13 14:30:00,90.69,34.181999999999995,47.19,29.509 +2020-07-13 14:45:00,90.03,35.629,47.19,29.509 +2020-07-13 15:00:00,93.94,37.814,47.846000000000004,29.509 +2020-07-13 15:15:00,99.23,34.43,47.846000000000004,29.509 +2020-07-13 15:30:00,95.18,33.066,47.846000000000004,29.509 +2020-07-13 15:45:00,96.44,30.903000000000002,47.846000000000004,29.509 +2020-07-13 16:00:00,96.49,34.625,49.641000000000005,29.509 +2020-07-13 16:15:00,95.72,34.295,49.641000000000005,29.509 +2020-07-13 16:30:00,96.85,33.941,49.641000000000005,29.509 +2020-07-13 16:45:00,94.4,29.968000000000004,49.641000000000005,29.509 +2020-07-13 17:00:00,98.43,33.529,54.133,29.509 +2020-07-13 17:15:00,98.75,33.147,54.133,29.509 +2020-07-13 17:30:00,99.74,32.542,54.133,29.509 +2020-07-13 17:45:00,98.7,31.698,54.133,29.509 +2020-07-13 18:00:00,102.1,35.762,53.761,29.509 +2020-07-13 18:15:00,99.07,33.991,53.761,29.509 +2020-07-13 18:30:00,100.45,32.218,53.761,29.509 +2020-07-13 18:45:00,100.43,35.531,53.761,29.509 +2020-07-13 19:00:00,97.31,37.945,53.923,29.509 +2020-07-13 19:15:00,93.81,36.722,53.923,29.509 +2020-07-13 19:30:00,93.3,35.78,53.923,29.509 +2020-07-13 19:45:00,91.72,34.668,53.923,29.509 +2020-07-13 20:00:00,90.68,31.375,58.786,29.509 +2020-07-13 20:15:00,90.77,32.399,58.786,29.509 +2020-07-13 20:30:00,90.87,32.946,58.786,29.509 +2020-07-13 20:45:00,91.51,33.126999999999995,58.786,29.509 +2020-07-13 21:00:00,89.62,30.568,54.591,29.509 +2020-07-13 21:15:00,88.41,33.764,54.591,29.509 +2020-07-13 21:30:00,85.66,34.279,54.591,29.509 +2020-07-13 21:45:00,85.22,35.104,54.591,29.509 +2020-07-13 22:00:00,79.21,32.213,51.551,29.509 +2020-07-13 22:15:00,80.28,35.194,51.551,29.509 +2020-07-13 22:30:00,79.16,30.554000000000002,51.551,29.509 +2020-07-13 22:45:00,78.47,27.307,51.551,29.509 +2020-07-13 23:00:00,72.96,24.906999999999996,44.716,29.509 +2020-07-13 23:15:00,73.07,21.904,44.716,29.509 +2020-07-13 23:30:00,69.81,20.805,44.716,29.509 +2020-07-13 23:45:00,72.76,19.611,44.716,29.509 +2020-07-14 00:00:00,67.54,18.816,43.01,29.509 +2020-07-14 00:15:00,69.07,19.663,43.01,29.509 +2020-07-14 00:30:00,66.45,18.436,43.01,29.509 +2020-07-14 00:45:00,70.02,18.406,43.01,29.509 +2020-07-14 01:00:00,69.42,18.439,40.687,29.509 +2020-07-14 01:15:00,69.69,17.563,40.687,29.509 +2020-07-14 01:30:00,67.81,16.028,40.687,29.509 +2020-07-14 01:45:00,69.25,15.975999999999999,40.687,29.509 +2020-07-14 02:00:00,68.49,15.753,39.554,29.509 +2020-07-14 02:15:00,69.32,14.513,39.554,29.509 +2020-07-14 02:30:00,69.78,16.408,39.554,29.509 +2020-07-14 02:45:00,69.55,17.16,39.554,29.509 +2020-07-14 03:00:00,70.53,18.301,38.958,29.509 +2020-07-14 03:15:00,71.95,17.624000000000002,38.958,29.509 +2020-07-14 03:30:00,71.88,16.959,38.958,29.509 +2020-07-14 03:45:00,75.76,16.305,38.958,29.509 +2020-07-14 04:00:00,74.52,20.831999999999997,39.783,29.509 +2020-07-14 04:15:00,75.95,26.779,39.783,29.509 +2020-07-14 04:30:00,79.03,24.072,39.783,29.509 +2020-07-14 04:45:00,81.77,24.083000000000002,39.783,29.509 +2020-07-14 05:00:00,88.06,36.406,42.281000000000006,29.509 +2020-07-14 05:15:00,90.34,43.077,42.281000000000006,29.509 +2020-07-14 05:30:00,92.69,37.535,42.281000000000006,29.509 +2020-07-14 05:45:00,98.51,35.157,42.281000000000006,29.509 +2020-07-14 06:00:00,109.3,36.005,50.801,29.509 +2020-07-14 06:15:00,108.62,35.589,50.801,29.509 +2020-07-14 06:30:00,105.84,34.981,50.801,29.509 +2020-07-14 06:45:00,105.08,37.208,50.801,29.509 +2020-07-14 07:00:00,116.07,37.256,60.202,29.509 +2020-07-14 07:15:00,115.56,37.838,60.202,29.509 +2020-07-14 07:30:00,111.32,35.568000000000005,60.202,29.509 +2020-07-14 07:45:00,103.14,35.945,60.202,29.509 +2020-07-14 08:00:00,106.06,31.928,54.461000000000006,29.509 +2020-07-14 08:15:00,103.57,35.115,54.461000000000006,29.509 +2020-07-14 08:30:00,103.42,36.209,54.461000000000006,29.509 +2020-07-14 08:45:00,106.65,38.788000000000004,54.461000000000006,29.509 +2020-07-14 09:00:00,110.63,32.869,50.753,29.509 +2020-07-14 09:15:00,110.97,32.167,50.753,29.509 +2020-07-14 09:30:00,104.6,35.965,50.753,29.509 +2020-07-14 09:45:00,103.21,39.481,50.753,29.509 +2020-07-14 10:00:00,108.94,36.727,49.703,29.509 +2020-07-14 10:15:00,110.62,38.361,49.703,29.509 +2020-07-14 10:30:00,109.15,38.375,49.703,29.509 +2020-07-14 10:45:00,102.77,39.693000000000005,49.703,29.509 +2020-07-14 11:00:00,100.68,37.448,49.42100000000001,29.509 +2020-07-14 11:15:00,104.47,38.778,49.42100000000001,29.509 +2020-07-14 11:30:00,103.85,40.007,49.42100000000001,29.509 +2020-07-14 11:45:00,104.68,41.293,49.42100000000001,29.509 +2020-07-14 12:00:00,101.13,36.594,47.155,29.509 +2020-07-14 12:15:00,100.12,36.073,47.155,29.509 +2020-07-14 12:30:00,97.38,34.82,47.155,29.509 +2020-07-14 12:45:00,103.21,35.93,47.155,29.509 +2020-07-14 13:00:00,104.0,36.05,47.515,29.509 +2020-07-14 13:15:00,102.08,37.375,47.515,29.509 +2020-07-14 13:30:00,96.92,35.641,47.515,29.509 +2020-07-14 13:45:00,97.9,35.316,47.515,29.509 +2020-07-14 14:00:00,96.33,37.105,47.575,29.509 +2020-07-14 14:15:00,101.9,36.049,47.575,29.509 +2020-07-14 14:30:00,99.58,34.953,47.575,29.509 +2020-07-14 14:45:00,98.18,35.567,47.575,29.509 +2020-07-14 15:00:00,93.03,37.619,48.903,29.509 +2020-07-14 15:15:00,90.27,35.195,48.903,29.509 +2020-07-14 15:30:00,93.61,33.589,48.903,29.509 +2020-07-14 15:45:00,99.29,31.824,48.903,29.509 +2020-07-14 16:00:00,102.48,34.764,50.218999999999994,29.509 +2020-07-14 16:15:00,101.61,34.497,50.218999999999994,29.509 +2020-07-14 16:30:00,97.93,33.715,50.218999999999994,29.509 +2020-07-14 16:45:00,99.0,30.489,50.218999999999994,29.509 +2020-07-14 17:00:00,100.95,34.236999999999995,55.396,29.509 +2020-07-14 17:15:00,101.47,34.311,55.396,29.509 +2020-07-14 17:30:00,101.51,33.196999999999996,55.396,29.509 +2020-07-14 17:45:00,104.76,32.015,55.396,29.509 +2020-07-14 18:00:00,107.85,35.096,55.583999999999996,29.509 +2020-07-14 18:15:00,108.01,34.913000000000004,55.583999999999996,29.509 +2020-07-14 18:30:00,105.94,32.885999999999996,55.583999999999996,29.509 +2020-07-14 18:45:00,101.24,35.943000000000005,55.583999999999996,29.509 +2020-07-14 19:00:00,102.79,37.137,56.071000000000005,29.509 +2020-07-14 19:15:00,102.8,36.111,56.071000000000005,29.509 +2020-07-14 19:30:00,101.74,34.975,56.071000000000005,29.509 +2020-07-14 19:45:00,95.47,34.209,56.071000000000005,29.509 +2020-07-14 20:00:00,91.86,31.309,61.55,29.509 +2020-07-14 20:15:00,91.07,30.857,61.55,29.509 +2020-07-14 20:30:00,91.99,31.381,61.55,29.509 +2020-07-14 20:45:00,93.25,32.04,61.55,29.509 +2020-07-14 21:00:00,90.18,30.436,55.94,29.509 +2020-07-14 21:15:00,89.36,32.047,55.94,29.509 +2020-07-14 21:30:00,86.61,32.768,55.94,29.509 +2020-07-14 21:45:00,85.12,33.775,55.94,29.509 +2020-07-14 22:00:00,80.32,30.973000000000003,52.857,29.509 +2020-07-14 22:15:00,79.79,33.615,52.857,29.509 +2020-07-14 22:30:00,78.37,29.256999999999998,52.857,29.509 +2020-07-14 22:45:00,77.97,25.984,52.857,29.509 +2020-07-14 23:00:00,72.49,22.794,46.04,29.509 +2020-07-14 23:15:00,73.85,21.485,46.04,29.509 +2020-07-14 23:30:00,70.65,20.417,46.04,29.509 +2020-07-14 23:45:00,72.46,19.43,46.04,29.509 +2020-07-15 00:00:00,70.34,18.844,42.195,29.509 +2020-07-15 00:15:00,71.25,19.69,42.195,29.509 +2020-07-15 00:30:00,70.44,18.468,42.195,29.509 +2020-07-15 00:45:00,70.41,18.444000000000003,42.195,29.509 +2020-07-15 01:00:00,67.46,18.48,38.82,29.509 +2020-07-15 01:15:00,68.86,17.599,38.82,29.509 +2020-07-15 01:30:00,67.2,16.067999999999998,38.82,29.509 +2020-07-15 01:45:00,67.83,16.009,38.82,29.509 +2020-07-15 02:00:00,66.75,15.790999999999999,37.023,29.509 +2020-07-15 02:15:00,67.72,14.513,37.023,29.509 +2020-07-15 02:30:00,67.76,16.441,37.023,29.509 +2020-07-15 02:45:00,67.84,17.197,37.023,29.509 +2020-07-15 03:00:00,66.87,18.329,36.818000000000005,29.509 +2020-07-15 03:15:00,71.03,17.664,36.818000000000005,29.509 +2020-07-15 03:30:00,71.12,17.007,36.818000000000005,29.509 +2020-07-15 03:45:00,72.97,16.365,36.818000000000005,29.509 +2020-07-15 04:00:00,73.68,20.866,37.495,29.509 +2020-07-15 04:15:00,76.29,26.789,37.495,29.509 +2020-07-15 04:30:00,80.11,24.078000000000003,37.495,29.509 +2020-07-15 04:45:00,84.96,24.088,37.495,29.509 +2020-07-15 05:00:00,87.78,36.374,39.858000000000004,29.509 +2020-07-15 05:15:00,87.33,42.99100000000001,39.858000000000004,29.509 +2020-07-15 05:30:00,94.23,37.495,39.858000000000004,29.509 +2020-07-15 05:45:00,94.63,35.131,39.858000000000004,29.509 +2020-07-15 06:00:00,100.78,35.971,52.867,29.509 +2020-07-15 06:15:00,108.9,35.558,52.867,29.509 +2020-07-15 06:30:00,110.63,34.966,52.867,29.509 +2020-07-15 06:45:00,111.18,37.216,52.867,29.509 +2020-07-15 07:00:00,105.52,37.258,66.061,29.509 +2020-07-15 07:15:00,103.46,37.86,66.061,29.509 +2020-07-15 07:30:00,103.47,35.599000000000004,66.061,29.509 +2020-07-15 07:45:00,102.77,35.999,66.061,29.509 +2020-07-15 08:00:00,103.23,31.991,58.532,29.509 +2020-07-15 08:15:00,108.72,35.184,58.532,29.509 +2020-07-15 08:30:00,110.2,36.266999999999996,58.532,29.509 +2020-07-15 08:45:00,110.41,38.838,58.532,29.509 +2020-07-15 09:00:00,110.93,32.918,56.047,29.509 +2020-07-15 09:15:00,114.49,32.213,56.047,29.509 +2020-07-15 09:30:00,113.05,36.003,56.047,29.509 +2020-07-15 09:45:00,111.8,39.516999999999996,56.047,29.509 +2020-07-15 10:00:00,110.53,36.769,53.823,29.509 +2020-07-15 10:15:00,113.38,38.397,53.823,29.509 +2020-07-15 10:30:00,118.08,38.407,53.823,29.509 +2020-07-15 10:45:00,119.14,39.724000000000004,53.823,29.509 +2020-07-15 11:00:00,114.85,37.48,54.184,29.509 +2020-07-15 11:15:00,105.35,38.809,54.184,29.509 +2020-07-15 11:30:00,107.75,40.027,54.184,29.509 +2020-07-15 11:45:00,104.65,41.305,54.184,29.509 +2020-07-15 12:00:00,101.57,36.622,52.628,29.509 +2020-07-15 12:15:00,102.98,36.098,52.628,29.509 +2020-07-15 12:30:00,101.37,34.839,52.628,29.509 +2020-07-15 12:45:00,100.1,35.945,52.628,29.509 +2020-07-15 13:00:00,95.13,36.046,52.31,29.509 +2020-07-15 13:15:00,97.79,37.365,52.31,29.509 +2020-07-15 13:30:00,92.09,35.635,52.31,29.509 +2020-07-15 13:45:00,93.99,35.318000000000005,52.31,29.509 +2020-07-15 14:00:00,91.96,37.105,52.278999999999996,29.509 +2020-07-15 14:15:00,93.66,36.051,52.278999999999996,29.509 +2020-07-15 14:30:00,93.43,34.949,52.278999999999996,29.509 +2020-07-15 14:45:00,94.43,35.57,52.278999999999996,29.509 +2020-07-15 15:00:00,90.17,37.62,53.306999999999995,29.509 +2020-07-15 15:15:00,89.69,35.193000000000005,53.306999999999995,29.509 +2020-07-15 15:30:00,94.34,33.59,53.306999999999995,29.509 +2020-07-15 15:45:00,93.63,31.82,53.306999999999995,29.509 +2020-07-15 16:00:00,94.82,34.765,55.358999999999995,29.509 +2020-07-15 16:15:00,96.19,34.501999999999995,55.358999999999995,29.509 +2020-07-15 16:30:00,95.74,33.736999999999995,55.358999999999995,29.509 +2020-07-15 16:45:00,96.06,30.52,55.358999999999995,29.509 +2020-07-15 17:00:00,98.6,34.265,59.211999999999996,29.509 +2020-07-15 17:15:00,98.46,34.354,59.211999999999996,29.509 +2020-07-15 17:30:00,98.19,33.245,59.211999999999996,29.509 +2020-07-15 17:45:00,101.1,32.071999999999996,59.211999999999996,29.509 +2020-07-15 18:00:00,101.97,35.156,60.403999999999996,29.509 +2020-07-15 18:15:00,100.51,34.96,60.403999999999996,29.509 +2020-07-15 18:30:00,99.62,32.937,60.403999999999996,29.509 +2020-07-15 18:45:00,99.61,35.994,60.403999999999996,29.509 +2020-07-15 19:00:00,96.67,37.191,60.993,29.509 +2020-07-15 19:15:00,92.57,36.158,60.993,29.509 +2020-07-15 19:30:00,91.19,35.016999999999996,60.993,29.509 +2020-07-15 19:45:00,90.05,34.249,60.993,29.509 +2020-07-15 20:00:00,89.39,31.340999999999998,66.6,29.509 +2020-07-15 20:15:00,89.79,30.886999999999997,66.6,29.509 +2020-07-15 20:30:00,90.48,31.406999999999996,66.6,29.509 +2020-07-15 20:45:00,94.33,32.068000000000005,66.6,29.509 +2020-07-15 21:00:00,89.0,30.467,59.855,29.509 +2020-07-15 21:15:00,90.24,32.078,59.855,29.509 +2020-07-15 21:30:00,86.46,32.781,59.855,29.509 +2020-07-15 21:45:00,84.69,33.777,59.855,29.509 +2020-07-15 22:00:00,80.58,30.976999999999997,54.942,29.509 +2020-07-15 22:15:00,79.78,33.615,54.942,29.509 +2020-07-15 22:30:00,77.4,29.239,54.942,29.509 +2020-07-15 22:45:00,76.55,25.959,54.942,29.509 +2020-07-15 23:00:00,70.55,22.781,46.056000000000004,29.509 +2020-07-15 23:15:00,73.75,21.489,46.056000000000004,29.509 +2020-07-15 23:30:00,72.68,20.434,46.056000000000004,29.509 +2020-07-15 23:45:00,71.83,19.444000000000003,46.056000000000004,29.509 +2020-07-16 00:00:00,68.4,17.695999999999998,40.859,29.509 +2020-07-16 00:15:00,69.33,18.609,40.859,29.509 +2020-07-16 00:30:00,69.21,17.279,40.859,29.509 +2020-07-16 00:45:00,69.57,17.358,40.859,29.509 +2020-07-16 01:00:00,66.81,17.385,39.06,29.509 +2020-07-16 01:15:00,69.01,16.565,39.06,29.509 +2020-07-16 01:30:00,67.69,15.054,39.06,29.509 +2020-07-16 01:45:00,68.28,15.171,39.06,29.509 +2020-07-16 02:00:00,68.0,15.002,37.592,29.509 +2020-07-16 02:15:00,67.65,14.513,37.592,29.509 +2020-07-16 02:30:00,67.91,15.694,37.592,29.509 +2020-07-16 02:45:00,69.21,16.421,37.592,29.509 +2020-07-16 03:00:00,68.8,17.433,37.416,29.509 +2020-07-16 03:15:00,69.64,16.917,37.416,29.509 +2020-07-16 03:30:00,71.41,16.209,37.416,29.509 +2020-07-16 03:45:00,74.92,15.402999999999999,37.416,29.509 +2020-07-16 04:00:00,75.15,19.62,38.176,29.509 +2020-07-16 04:15:00,76.88,25.261999999999997,38.176,29.509 +2020-07-16 04:30:00,76.66,22.57,38.176,29.509 +2020-07-16 04:45:00,80.18,22.496,38.176,29.509 +2020-07-16 05:00:00,87.87,34.303000000000004,41.203,29.509 +2020-07-16 05:15:00,89.57,40.372,41.203,29.509 +2020-07-16 05:30:00,96.26,35.325,41.203,29.509 +2020-07-16 05:45:00,101.5,32.945,41.203,29.509 +2020-07-16 06:00:00,107.07,33.306,51.09,29.509 +2020-07-16 06:15:00,102.95,32.66,51.09,29.509 +2020-07-16 06:30:00,101.95,32.357,51.09,29.509 +2020-07-16 06:45:00,107.07,34.909,51.09,29.509 +2020-07-16 07:00:00,109.85,34.71,63.541000000000004,29.509 +2020-07-16 07:15:00,107.55,35.439,63.541000000000004,29.509 +2020-07-16 07:30:00,105.46,33.343,63.541000000000004,29.509 +2020-07-16 07:45:00,99.9,33.835,63.541000000000004,29.509 +2020-07-16 08:00:00,103.71,29.159000000000002,55.65,29.509 +2020-07-16 08:15:00,106.39,32.507,55.65,29.509 +2020-07-16 08:30:00,107.35,34.055,55.65,29.509 +2020-07-16 08:45:00,103.5,36.774,55.65,29.509 +2020-07-16 09:00:00,99.32,32.482,51.833999999999996,29.509 +2020-07-16 09:15:00,101.09,32.001999999999995,51.833999999999996,29.509 +2020-07-16 09:30:00,106.4,35.815,51.833999999999996,29.509 +2020-07-16 09:45:00,112.1,39.141,51.833999999999996,29.509 +2020-07-16 10:00:00,113.34,36.639,49.70399999999999,29.509 +2020-07-16 10:15:00,103.53,38.18,49.70399999999999,29.509 +2020-07-16 10:30:00,108.61,38.09,49.70399999999999,29.509 +2020-07-16 10:45:00,110.26,39.092,49.70399999999999,29.509 +2020-07-16 11:00:00,107.49,36.751999999999995,48.593999999999994,29.509 +2020-07-16 11:15:00,101.65,38.056999999999995,48.593999999999994,29.509 +2020-07-16 11:30:00,99.95,39.297,48.593999999999994,29.509 +2020-07-16 11:45:00,105.9,40.42,48.593999999999994,29.509 +2020-07-16 12:00:00,104.35,34.999,46.275,29.509 +2020-07-16 12:15:00,106.27,34.306999999999995,46.275,29.509 +2020-07-16 12:30:00,97.52,33.074,46.275,29.509 +2020-07-16 12:45:00,98.65,34.076,46.275,29.509 +2020-07-16 13:00:00,102.47,34.053000000000004,45.803000000000004,29.509 +2020-07-16 13:15:00,104.02,35.477,45.803000000000004,29.509 +2020-07-16 13:30:00,100.98,33.608000000000004,45.803000000000004,29.509 +2020-07-16 13:45:00,94.53,33.578,45.803000000000004,29.509 +2020-07-16 14:00:00,96.85,35.39,46.251999999999995,29.509 +2020-07-16 14:15:00,96.71,34.388000000000005,46.251999999999995,29.509 +2020-07-16 14:30:00,99.91,33.429,46.251999999999995,29.509 +2020-07-16 14:45:00,99.62,34.041,46.251999999999995,29.509 +2020-07-16 15:00:00,96.59,36.379,48.309,29.509 +2020-07-16 15:15:00,93.27,34.016999999999996,48.309,29.509 +2020-07-16 15:30:00,95.88,32.552,48.309,29.509 +2020-07-16 15:45:00,100.36,30.926,48.309,29.509 +2020-07-16 16:00:00,100.07,33.31,49.681999999999995,29.509 +2020-07-16 16:15:00,95.77,33.111,49.681999999999995,29.509 +2020-07-16 16:30:00,98.62,32.172,49.681999999999995,29.509 +2020-07-16 16:45:00,100.86,29.104,49.681999999999995,29.509 +2020-07-16 17:00:00,105.52,33.069,53.086000000000006,29.509 +2020-07-16 17:15:00,107.45,33.188,53.086000000000006,29.509 +2020-07-16 17:30:00,104.16,32.003,53.086000000000006,29.509 +2020-07-16 17:45:00,103.07,31.022,53.086000000000006,29.509 +2020-07-16 18:00:00,101.84,34.315,54.038999999999994,29.509 +2020-07-16 18:15:00,100.31,34.167,54.038999999999994,29.509 +2020-07-16 18:30:00,100.5,32.226,54.038999999999994,29.509 +2020-07-16 18:45:00,106.08,34.979,54.038999999999994,29.509 +2020-07-16 19:00:00,105.9,36.38,53.408,29.509 +2020-07-16 19:15:00,102.77,35.374,53.408,29.509 +2020-07-16 19:30:00,96.1,34.2,53.408,29.509 +2020-07-16 19:45:00,95.11,33.135,53.408,29.509 +2020-07-16 20:00:00,91.57,30.674,55.309,29.509 +2020-07-16 20:15:00,94.42,29.885,55.309,29.509 +2020-07-16 20:30:00,93.02,30.358,55.309,29.509 +2020-07-16 20:45:00,93.42,30.788,55.309,29.509 +2020-07-16 21:00:00,91.2,29.548000000000002,51.585,29.509 +2020-07-16 21:15:00,90.29,30.741999999999997,51.585,29.509 +2020-07-16 21:30:00,87.58,31.38,51.585,29.509 +2020-07-16 21:45:00,86.37,32.32,51.585,29.509 +2020-07-16 22:00:00,81.95,29.609,48.006,29.509 +2020-07-16 22:15:00,81.66,32.455,48.006,29.509 +2020-07-16 22:30:00,79.46,28.267,48.006,29.509 +2020-07-16 22:45:00,80.13,25.146,48.006,29.509 +2020-07-16 23:00:00,75.25,21.895,42.309,29.509 +2020-07-16 23:15:00,75.85,20.484,42.309,29.509 +2020-07-16 23:30:00,74.85,19.387,42.309,29.509 +2020-07-16 23:45:00,73.27,18.362000000000002,42.309,29.509 +2020-07-17 00:00:00,69.67,15.872,39.649,29.509 +2020-07-17 00:15:00,72.11,17.002,39.649,29.509 +2020-07-17 00:30:00,72.27,15.982999999999999,39.649,29.509 +2020-07-17 00:45:00,72.1,16.518,39.649,29.509 +2020-07-17 01:00:00,69.69,16.156,37.744,29.509 +2020-07-17 01:15:00,70.44,14.607000000000001,37.744,29.509 +2020-07-17 01:30:00,70.83,14.513,37.744,29.509 +2020-07-17 01:45:00,73.34,14.513,37.744,29.509 +2020-07-17 02:00:00,70.32,14.565999999999999,36.965,29.509 +2020-07-17 02:15:00,70.93,14.513,36.965,29.509 +2020-07-17 02:30:00,70.09,16.064,36.965,29.509 +2020-07-17 02:45:00,69.59,16.048,36.965,29.509 +2020-07-17 03:00:00,70.93,17.969,37.678000000000004,29.509 +2020-07-17 03:15:00,72.45,16.085,37.678000000000004,29.509 +2020-07-17 03:30:00,74.49,15.127,37.678000000000004,29.509 +2020-07-17 03:45:00,74.49,15.26,37.678000000000004,29.509 +2020-07-17 04:00:00,76.78,19.583,38.591,29.509 +2020-07-17 04:15:00,83.05,23.531999999999996,38.591,29.509 +2020-07-17 04:30:00,83.79,21.848000000000003,38.591,29.509 +2020-07-17 04:45:00,82.32,21.16,38.591,29.509 +2020-07-17 05:00:00,92.53,32.51,40.666,29.509 +2020-07-17 05:15:00,93.78,39.453,40.666,29.509 +2020-07-17 05:30:00,99.45,34.591,40.666,29.509 +2020-07-17 05:45:00,102.94,31.750999999999998,40.666,29.509 +2020-07-17 06:00:00,108.29,32.311,51.784,29.509 +2020-07-17 06:15:00,101.27,31.945999999999998,51.784,29.509 +2020-07-17 06:30:00,101.61,31.660999999999998,51.784,29.509 +2020-07-17 06:45:00,108.21,33.99,51.784,29.509 +2020-07-17 07:00:00,112.7,34.54,61.383,29.509 +2020-07-17 07:15:00,110.87,36.194,61.383,29.509 +2020-07-17 07:30:00,107.13,31.997,61.383,29.509 +2020-07-17 07:45:00,105.72,32.355,61.383,29.509 +2020-07-17 08:00:00,109.76,28.636,55.272,29.509 +2020-07-17 08:15:00,109.09,32.758,55.272,29.509 +2020-07-17 08:30:00,106.95,34.098,55.272,29.509 +2020-07-17 08:45:00,103.14,36.793,55.272,29.509 +2020-07-17 09:00:00,105.78,29.919,53.506,29.509 +2020-07-17 09:15:00,106.52,31.503,53.506,29.509 +2020-07-17 09:30:00,107.91,34.588,53.506,29.509 +2020-07-17 09:45:00,105.7,38.348,53.506,29.509 +2020-07-17 10:00:00,103.69,35.79,51.363,29.509 +2020-07-17 10:15:00,108.86,37.004,51.363,29.509 +2020-07-17 10:30:00,108.56,37.53,51.363,29.509 +2020-07-17 10:45:00,105.22,38.471,51.363,29.509 +2020-07-17 11:00:00,99.4,36.409,51.043,29.509 +2020-07-17 11:15:00,104.69,36.599000000000004,51.043,29.509 +2020-07-17 11:30:00,105.83,37.249,51.043,29.509 +2020-07-17 11:45:00,105.9,37.297,51.043,29.509 +2020-07-17 12:00:00,97.56,32.258,47.52,29.509 +2020-07-17 12:15:00,96.68,31.066,47.52,29.509 +2020-07-17 12:30:00,91.93,29.941999999999997,47.52,29.509 +2020-07-17 12:45:00,92.95,30.011999999999997,47.52,29.509 +2020-07-17 13:00:00,96.42,30.528000000000002,45.494,29.509 +2020-07-17 13:15:00,97.53,32.061,45.494,29.509 +2020-07-17 13:30:00,94.43,31.075,45.494,29.509 +2020-07-17 13:45:00,91.76,31.401999999999997,45.494,29.509 +2020-07-17 14:00:00,92.13,32.44,43.883,29.509 +2020-07-17 14:15:00,90.45,31.94,43.883,29.509 +2020-07-17 14:30:00,91.4,32.594,43.883,29.509 +2020-07-17 14:45:00,93.5,32.418,43.883,29.509 +2020-07-17 15:00:00,94.71,34.715,45.714,29.509 +2020-07-17 15:15:00,95.27,32.129,45.714,29.509 +2020-07-17 15:30:00,88.29,30.189,45.714,29.509 +2020-07-17 15:45:00,92.07,29.386,45.714,29.509 +2020-07-17 16:00:00,91.83,30.943,48.222,29.509 +2020-07-17 16:15:00,100.32,31.264,48.222,29.509 +2020-07-17 16:30:00,98.96,30.133000000000003,48.222,29.509 +2020-07-17 16:45:00,102.8,26.223000000000003,48.222,29.509 +2020-07-17 17:00:00,96.41,32.058,52.619,29.509 +2020-07-17 17:15:00,97.39,32.042,52.619,29.509 +2020-07-17 17:30:00,96.03,31.074,52.619,29.509 +2020-07-17 17:45:00,101.63,29.944000000000003,52.619,29.509 +2020-07-17 18:00:00,104.59,33.204,52.99,29.509 +2020-07-17 18:15:00,104.12,32.05,52.99,29.509 +2020-07-17 18:30:00,99.36,29.965999999999998,52.99,29.509 +2020-07-17 18:45:00,95.42,33.177,52.99,29.509 +2020-07-17 19:00:00,99.76,35.439,51.923,29.509 +2020-07-17 19:15:00,99.09,34.96,51.923,29.509 +2020-07-17 19:30:00,93.72,33.879,51.923,29.509 +2020-07-17 19:45:00,89.16,31.75,51.923,29.509 +2020-07-17 20:00:00,86.65,29.105999999999998,56.238,29.509 +2020-07-17 20:15:00,91.65,29.175,56.238,29.509 +2020-07-17 20:30:00,91.6,29.116999999999997,56.238,29.509 +2020-07-17 20:45:00,89.51,28.627,56.238,29.509 +2020-07-17 21:00:00,81.75,28.764,52.426,29.509 +2020-07-17 21:15:00,84.05,31.724,52.426,29.509 +2020-07-17 21:30:00,85.59,32.179,52.426,29.509 +2020-07-17 21:45:00,85.67,33.286,52.426,29.509 +2020-07-17 22:00:00,79.08,30.348000000000003,48.196000000000005,29.509 +2020-07-17 22:15:00,73.88,32.935,48.196000000000005,29.509 +2020-07-17 22:30:00,71.96,33.379,48.196000000000005,29.509 +2020-07-17 22:45:00,70.94,30.925,48.196000000000005,29.509 +2020-07-17 23:00:00,69.13,29.535999999999998,41.71,29.509 +2020-07-17 23:15:00,72.96,26.595,41.71,29.509 +2020-07-17 23:30:00,74.34,23.662,41.71,29.509 +2020-07-17 23:45:00,73.55,22.548000000000002,41.71,29.509 +2020-07-18 00:00:00,64.97,17.482,41.105,29.398000000000003 +2020-07-18 00:15:00,64.83,18.22,41.105,29.398000000000003 +2020-07-18 00:30:00,69.52,16.590999999999998,41.105,29.398000000000003 +2020-07-18 00:45:00,69.35,16.285,41.105,29.398000000000003 +2020-07-18 01:00:00,67.98,16.169,36.934,29.398000000000003 +2020-07-18 01:15:00,63.54,15.332,36.934,29.398000000000003 +2020-07-18 01:30:00,65.47,14.513,36.934,29.398000000000003 +2020-07-18 01:45:00,67.32,14.98,36.934,29.398000000000003 +2020-07-18 02:00:00,66.36,14.703,34.782,29.398000000000003 +2020-07-18 02:15:00,62.18,14.513,34.782,29.398000000000003 +2020-07-18 02:30:00,60.75,14.605,34.782,29.398000000000003 +2020-07-18 02:45:00,66.16,15.431,34.782,29.398000000000003 +2020-07-18 03:00:00,65.84,15.811,34.489000000000004,29.398000000000003 +2020-07-18 03:15:00,67.15,14.513,34.489000000000004,29.398000000000003 +2020-07-18 03:30:00,60.47,14.513,34.489000000000004,29.398000000000003 +2020-07-18 03:45:00,66.51,14.547,34.489000000000004,29.398000000000003 +2020-07-18 04:00:00,65.6,17.13,34.111,29.398000000000003 +2020-07-18 04:15:00,60.82,20.293,34.111,29.398000000000003 +2020-07-18 04:30:00,65.33,16.974,34.111,29.398000000000003 +2020-07-18 04:45:00,61.82,16.62,34.111,29.398000000000003 +2020-07-18 05:00:00,61.13,20.213,33.283,29.398000000000003 +2020-07-18 05:15:00,60.73,16.794,33.283,29.398000000000003 +2020-07-18 05:30:00,62.16,14.513,33.283,29.398000000000003 +2020-07-18 05:45:00,66.61,15.129000000000001,33.283,29.398000000000003 +2020-07-18 06:00:00,72.23,26.281,33.653,29.398000000000003 +2020-07-18 06:15:00,71.83,33.03,33.653,29.398000000000003 +2020-07-18 06:30:00,69.3,30.125,33.653,29.398000000000003 +2020-07-18 06:45:00,68.38,29.725,33.653,29.398000000000003 +2020-07-18 07:00:00,73.72,29.413,36.732,29.398000000000003 +2020-07-18 07:15:00,77.89,29.795,36.732,29.398000000000003 +2020-07-18 07:30:00,79.76,26.998,36.732,29.398000000000003 +2020-07-18 07:45:00,78.08,28.014,36.732,29.398000000000003 +2020-07-18 08:00:00,79.55,24.814,41.318999999999996,29.398000000000003 +2020-07-18 08:15:00,85.3,28.331999999999997,41.318999999999996,29.398000000000003 +2020-07-18 08:30:00,84.34,29.479,41.318999999999996,29.398000000000003 +2020-07-18 08:45:00,82.38,32.879,41.318999999999996,29.398000000000003 +2020-07-18 09:00:00,79.5,29.301,43.195,29.398000000000003 +2020-07-18 09:15:00,79.99,31.334,43.195,29.398000000000003 +2020-07-18 09:30:00,88.35,34.856,43.195,29.398000000000003 +2020-07-18 09:45:00,91.53,38.098,43.195,29.398000000000003 +2020-07-18 10:00:00,87.81,36.235,41.843999999999994,29.398000000000003 +2020-07-18 10:15:00,83.2,37.816,41.843999999999994,29.398000000000003 +2020-07-18 10:30:00,84.65,37.939,41.843999999999994,29.398000000000003 +2020-07-18 10:45:00,92.01,38.341,41.843999999999994,29.398000000000003 +2020-07-18 11:00:00,88.51,36.077,39.035,29.398000000000003 +2020-07-18 11:15:00,83.11,37.3,39.035,29.398000000000003 +2020-07-18 11:30:00,78.1,38.375,39.035,29.398000000000003 +2020-07-18 11:45:00,76.27,39.258,39.035,29.398000000000003 +2020-07-18 12:00:00,74.23,34.78,38.001,29.398000000000003 +2020-07-18 12:15:00,71.26,34.455,38.001,29.398000000000003 +2020-07-18 12:30:00,65.03,33.147,38.001,29.398000000000003 +2020-07-18 12:45:00,61.3,34.099000000000004,38.001,29.398000000000003 +2020-07-18 13:00:00,60.24,33.71,34.747,29.398000000000003 +2020-07-18 13:15:00,62.03,34.865,34.747,29.398000000000003 +2020-07-18 13:30:00,65.18,34.141,34.747,29.398000000000003 +2020-07-18 13:45:00,70.0,32.931999999999995,34.747,29.398000000000003 +2020-07-18 14:00:00,72.75,33.854,33.434,29.398000000000003 +2020-07-18 14:15:00,74.53,32.037,33.434,29.398000000000003 +2020-07-18 14:30:00,77.56,32.413000000000004,33.434,29.398000000000003 +2020-07-18 14:45:00,79.82,32.749,33.434,29.398000000000003 +2020-07-18 15:00:00,79.73,35.416,35.921,29.398000000000003 +2020-07-18 15:15:00,78.71,33.518,35.921,29.398000000000003 +2020-07-18 15:30:00,78.58,31.645,35.921,29.398000000000003 +2020-07-18 15:45:00,78.43,29.824,35.921,29.398000000000003 +2020-07-18 16:00:00,77.97,33.81,39.427,29.398000000000003 +2020-07-18 16:15:00,75.31,33.069,39.427,29.398000000000003 +2020-07-18 16:30:00,75.91,32.207,39.427,29.398000000000003 +2020-07-18 16:45:00,77.36,28.189,39.427,29.398000000000003 +2020-07-18 17:00:00,78.24,32.734,44.096000000000004,29.398000000000003 +2020-07-18 17:15:00,78.03,30.230999999999998,44.096000000000004,29.398000000000003 +2020-07-18 17:30:00,78.44,29.135,44.096000000000004,29.398000000000003 +2020-07-18 17:45:00,80.13,28.595,44.096000000000004,29.398000000000003 +2020-07-18 18:00:00,79.23,33.352,43.931000000000004,29.398000000000003 +2020-07-18 18:15:00,78.26,33.839,43.931000000000004,29.398000000000003 +2020-07-18 18:30:00,77.24,33.111,43.931000000000004,29.398000000000003 +2020-07-18 18:45:00,76.69,32.857,43.931000000000004,29.398000000000003 +2020-07-18 19:00:00,73.66,33.239000000000004,42.187,29.398000000000003 +2020-07-18 19:15:00,71.27,31.711,42.187,29.398000000000003 +2020-07-18 19:30:00,70.94,31.392,42.187,29.398000000000003 +2020-07-18 19:45:00,71.36,31.168000000000003,42.187,29.398000000000003 +2020-07-18 20:00:00,71.01,29.235,38.315,29.398000000000003 +2020-07-18 20:15:00,72.39,28.453000000000003,38.315,29.398000000000003 +2020-07-18 20:30:00,72.9,27.475,38.315,29.398000000000003 +2020-07-18 20:45:00,72.58,29.005,38.315,29.398000000000003 +2020-07-18 21:00:00,71.06,27.381,36.843,29.398000000000003 +2020-07-18 21:15:00,70.83,29.934,36.843,29.398000000000003 +2020-07-18 21:30:00,68.87,30.485,36.843,29.398000000000003 +2020-07-18 21:45:00,68.49,31.026,36.843,29.398000000000003 +2020-07-18 22:00:00,65.43,27.968000000000004,37.260999999999996,29.398000000000003 +2020-07-18 22:15:00,65.56,30.64,37.260999999999996,29.398000000000003 +2020-07-18 22:30:00,63.24,30.226,37.260999999999996,29.398000000000003 +2020-07-18 22:45:00,62.82,28.031999999999996,37.260999999999996,29.398000000000003 +2020-07-18 23:00:00,57.26,25.816999999999997,32.148,29.398000000000003 +2020-07-18 23:15:00,59.35,23.447,32.148,29.398000000000003 +2020-07-18 23:30:00,58.36,23.248,32.148,29.398000000000003 +2020-07-18 23:45:00,57.61,22.671,32.148,29.398000000000003 +2020-07-19 00:00:00,55.61,18.977,28.905,29.398000000000003 +2020-07-19 00:15:00,55.79,18.499000000000002,28.905,29.398000000000003 +2020-07-19 00:30:00,54.68,16.748,28.905,29.398000000000003 +2020-07-19 00:45:00,54.38,16.272000000000002,28.905,29.398000000000003 +2020-07-19 01:00:00,51.87,16.452,26.906999999999996,29.398000000000003 +2020-07-19 01:15:00,53.39,15.395999999999999,26.906999999999996,29.398000000000003 +2020-07-19 01:30:00,53.69,14.513,26.906999999999996,29.398000000000003 +2020-07-19 01:45:00,53.75,14.513,26.906999999999996,29.398000000000003 +2020-07-19 02:00:00,52.09,14.513,25.938000000000002,29.398000000000003 +2020-07-19 02:15:00,52.77,14.513,25.938000000000002,29.398000000000003 +2020-07-19 02:30:00,52.15,15.205,25.938000000000002,29.398000000000003 +2020-07-19 02:45:00,52.3,15.685,25.938000000000002,29.398000000000003 +2020-07-19 03:00:00,51.95,16.727999999999998,24.693,29.398000000000003 +2020-07-19 03:15:00,52.28,14.535,24.693,29.398000000000003 +2020-07-19 03:30:00,53.32,14.513,24.693,29.398000000000003 +2020-07-19 03:45:00,53.05,14.513,24.693,29.398000000000003 +2020-07-19 04:00:00,52.24,16.62,25.683000000000003,29.398000000000003 +2020-07-19 04:15:00,51.65,19.378,25.683000000000003,29.398000000000003 +2020-07-19 04:30:00,51.6,17.485,25.683000000000003,29.398000000000003 +2020-07-19 04:45:00,52.23,16.61,25.683000000000003,29.398000000000003 +2020-07-19 05:00:00,51.43,20.625,26.023000000000003,29.398000000000003 +2020-07-19 05:15:00,51.17,16.522000000000002,26.023000000000003,29.398000000000003 +2020-07-19 05:30:00,51.75,14.513,26.023000000000003,29.398000000000003 +2020-07-19 05:45:00,52.6,14.513,26.023000000000003,29.398000000000003 +2020-07-19 06:00:00,54.27,22.829,25.834,29.398000000000003 +2020-07-19 06:15:00,54.5,30.6,25.834,29.398000000000003 +2020-07-19 06:30:00,55.07,27.053,25.834,29.398000000000003 +2020-07-19 06:45:00,57.47,25.699,25.834,29.398000000000003 +2020-07-19 07:00:00,59.13,25.54,27.765,29.398000000000003 +2020-07-19 07:15:00,59.65,24.248,27.765,29.398000000000003 +2020-07-19 07:30:00,60.2,22.959,27.765,29.398000000000003 +2020-07-19 07:45:00,60.84,24.031999999999996,27.765,29.398000000000003 +2020-07-19 08:00:00,61.14,21.519000000000002,31.357,29.398000000000003 +2020-07-19 08:15:00,58.21,26.334,31.357,29.398000000000003 +2020-07-19 08:30:00,57.28,28.247,31.357,29.398000000000003 +2020-07-19 08:45:00,58.82,31.338,31.357,29.398000000000003 +2020-07-19 09:00:00,59.11,27.697,33.238,29.398000000000003 +2020-07-19 09:15:00,60.1,29.121,33.238,29.398000000000003 +2020-07-19 09:30:00,59.77,33.126,33.238,29.398000000000003 +2020-07-19 09:45:00,62.12,37.48,33.238,29.398000000000003 +2020-07-19 10:00:00,62.62,35.931,34.22,29.398000000000003 +2020-07-19 10:15:00,63.28,37.588,34.22,29.398000000000003 +2020-07-19 10:30:00,63.94,37.878,34.22,29.398000000000003 +2020-07-19 10:45:00,63.69,39.635,34.22,29.398000000000003 +2020-07-19 11:00:00,63.64,36.87,36.298,29.398000000000003 +2020-07-19 11:15:00,65.93,37.582,36.298,29.398000000000003 +2020-07-19 11:30:00,62.62,39.328,36.298,29.398000000000003 +2020-07-19 11:45:00,61.61,40.412,36.298,29.398000000000003 +2020-07-19 12:00:00,60.44,37.195,33.52,29.398000000000003 +2020-07-19 12:15:00,58.9,35.936,33.52,29.398000000000003 +2020-07-19 12:30:00,58.57,35.041,33.52,29.398000000000003 +2020-07-19 12:45:00,56.89,35.4,33.52,29.398000000000003 +2020-07-19 13:00:00,54.98,34.747,30.12,29.398000000000003 +2020-07-19 13:15:00,53.24,34.882,30.12,29.398000000000003 +2020-07-19 13:30:00,51.7,32.954,30.12,29.398000000000003 +2020-07-19 13:45:00,54.52,33.073,30.12,29.398000000000003 +2020-07-19 14:00:00,54.96,35.285,27.233,29.398000000000003 +2020-07-19 14:15:00,52.39,33.801,27.233,29.398000000000003 +2020-07-19 14:30:00,50.08,32.659,27.233,29.398000000000003 +2020-07-19 14:45:00,49.54,31.871,27.233,29.398000000000003 +2020-07-19 15:00:00,51.32,34.882,27.468000000000004,29.398000000000003 +2020-07-19 15:15:00,51.02,32.001,27.468000000000004,29.398000000000003 +2020-07-19 15:30:00,52.26,29.862,27.468000000000004,29.398000000000003 +2020-07-19 15:45:00,52.6,28.27,27.468000000000004,29.398000000000003 +2020-07-19 16:00:00,56.74,30.274,30.8,29.398000000000003 +2020-07-19 16:15:00,57.44,29.891,30.8,29.398000000000003 +2020-07-19 16:30:00,60.95,30.116999999999997,30.8,29.398000000000003 +2020-07-19 16:45:00,61.94,26.168000000000003,30.8,29.398000000000003 +2020-07-19 17:00:00,67.22,31.125999999999998,37.806,29.398000000000003 +2020-07-19 17:15:00,71.64,30.329,37.806,29.398000000000003 +2020-07-19 17:30:00,73.51,30.09,37.806,29.398000000000003 +2020-07-19 17:45:00,74.82,29.78,37.806,29.398000000000003 +2020-07-19 18:00:00,75.5,35.266,40.766,29.398000000000003 +2020-07-19 18:15:00,73.22,35.173,40.766,29.398000000000003 +2020-07-19 18:30:00,72.29,34.387,40.766,29.398000000000003 +2020-07-19 18:45:00,72.76,34.076,40.766,29.398000000000003 +2020-07-19 19:00:00,74.69,36.863,41.163000000000004,29.398000000000003 +2020-07-19 19:15:00,73.14,34.086,41.163000000000004,29.398000000000003 +2020-07-19 19:30:00,71.41,33.510999999999996,41.163000000000004,29.398000000000003 +2020-07-19 19:45:00,72.67,32.666,41.163000000000004,29.398000000000003 +2020-07-19 20:00:00,73.53,30.916,39.885999999999996,29.398000000000003 +2020-07-19 20:15:00,75.2,29.899,39.885999999999996,29.398000000000003 +2020-07-19 20:30:00,76.95,29.673000000000002,39.885999999999996,29.398000000000003 +2020-07-19 20:45:00,76.76,29.506,39.885999999999996,29.398000000000003 +2020-07-19 21:00:00,74.32,28.004,38.900999999999996,29.398000000000003 +2020-07-19 21:15:00,74.99,30.284000000000002,38.900999999999996,29.398000000000003 +2020-07-19 21:30:00,73.19,30.101999999999997,38.900999999999996,29.398000000000003 +2020-07-19 21:45:00,71.17,30.999000000000002,38.900999999999996,29.398000000000003 +2020-07-19 22:00:00,66.49,30.218000000000004,39.806999999999995,29.398000000000003 +2020-07-19 22:15:00,68.96,31.16,39.806999999999995,29.398000000000003 +2020-07-19 22:30:00,67.63,30.438000000000002,39.806999999999995,29.398000000000003 +2020-07-19 22:45:00,67.17,26.953000000000003,39.806999999999995,29.398000000000003 +2020-07-19 23:00:00,62.05,24.655,35.564,29.398000000000003 +2020-07-19 23:15:00,63.9,23.502,35.564,29.398000000000003 +2020-07-19 23:30:00,62.49,22.683000000000003,35.564,29.398000000000003 +2020-07-19 23:45:00,62.2,22.201,35.564,29.398000000000003 +2020-07-20 00:00:00,60.09,20.273,36.578,29.509 +2020-07-20 00:15:00,60.71,20.372,36.578,29.509 +2020-07-20 00:30:00,59.91,18.199,36.578,29.509 +2020-07-20 00:45:00,60.28,17.354,36.578,29.509 +2020-07-20 01:00:00,58.64,17.95,35.292,29.509 +2020-07-20 01:15:00,58.78,16.954,35.292,29.509 +2020-07-20 01:30:00,59.4,15.62,35.292,29.509 +2020-07-20 01:45:00,58.95,16.297,35.292,29.509 +2020-07-20 02:00:00,58.17,16.67,34.319,29.509 +2020-07-20 02:15:00,59.43,14.513,34.319,29.509 +2020-07-20 02:30:00,65.62,16.453,34.319,29.509 +2020-07-20 02:45:00,68.61,16.833,34.319,29.509 +2020-07-20 03:00:00,68.25,18.299,33.13,29.509 +2020-07-20 03:15:00,63.19,16.746,33.13,29.509 +2020-07-20 03:30:00,64.3,16.148,33.13,29.509 +2020-07-20 03:45:00,71.14,16.613,33.13,29.509 +2020-07-20 04:00:00,74.7,22.09,33.851,29.509 +2020-07-20 04:15:00,76.07,27.603,33.851,29.509 +2020-07-20 04:30:00,72.95,24.997,33.851,29.509 +2020-07-20 04:45:00,78.22,24.505,33.851,29.509 +2020-07-20 05:00:00,83.67,34.926,38.718,29.509 +2020-07-20 05:15:00,86.56,40.138000000000005,38.718,29.509 +2020-07-20 05:30:00,86.91,34.939,38.718,29.509 +2020-07-20 05:45:00,96.34,33.38,38.718,29.509 +2020-07-20 06:00:00,101.08,32.46,51.648999999999994,29.509 +2020-07-20 06:15:00,103.96,31.761999999999997,51.648999999999994,29.509 +2020-07-20 06:30:00,100.97,31.808000000000003,51.648999999999994,29.509 +2020-07-20 06:45:00,101.61,35.384,51.648999999999994,29.509 +2020-07-20 07:00:00,101.25,34.973,60.159,29.509 +2020-07-20 07:15:00,101.47,36.051,60.159,29.509 +2020-07-20 07:30:00,105.36,33.915,60.159,29.509 +2020-07-20 07:45:00,106.42,35.589,60.159,29.509 +2020-07-20 08:00:00,107.88,31.041,53.8,29.509 +2020-07-20 08:15:00,105.6,34.771,53.8,29.509 +2020-07-20 08:30:00,101.05,36.065,53.8,29.509 +2020-07-20 08:45:00,107.78,39.739000000000004,53.8,29.509 +2020-07-20 09:00:00,106.48,34.959,50.583,29.509 +2020-07-20 09:15:00,103.76,34.821,50.583,29.509 +2020-07-20 09:30:00,100.88,37.909,50.583,29.509 +2020-07-20 09:45:00,97.4,39.689,50.583,29.509 +2020-07-20 10:00:00,103.26,38.675,49.11600000000001,29.509 +2020-07-20 10:15:00,107.19,40.2,49.11600000000001,29.509 +2020-07-20 10:30:00,105.63,40.084,49.11600000000001,29.509 +2020-07-20 10:45:00,97.62,39.985,49.11600000000001,29.509 +2020-07-20 11:00:00,97.57,37.84,49.056000000000004,29.509 +2020-07-20 11:15:00,103.01,38.643,49.056000000000004,29.509 +2020-07-20 11:30:00,102.55,40.983000000000004,49.056000000000004,29.509 +2020-07-20 11:45:00,105.27,42.663999999999994,49.056000000000004,29.509 +2020-07-20 12:00:00,98.29,37.333,47.227,29.509 +2020-07-20 12:15:00,99.49,36.194,47.227,29.509 +2020-07-20 12:30:00,97.27,34.074,47.227,29.509 +2020-07-20 12:45:00,96.77,34.235,47.227,29.509 +2020-07-20 13:00:00,95.98,34.484,47.006,29.509 +2020-07-20 13:15:00,91.51,33.8,47.006,29.509 +2020-07-20 13:30:00,91.54,32.129,47.006,29.509 +2020-07-20 13:45:00,91.45,33.25,47.006,29.509 +2020-07-20 14:00:00,91.7,34.586,47.19,29.509 +2020-07-20 14:15:00,91.78,33.838,47.19,29.509 +2020-07-20 14:30:00,88.92,32.577,47.19,29.509 +2020-07-20 14:45:00,90.77,34.091,47.19,29.509 +2020-07-20 15:00:00,87.71,36.536,47.846000000000004,29.509 +2020-07-20 15:15:00,88.74,33.169000000000004,47.846000000000004,29.509 +2020-07-20 15:30:00,89.17,31.98,47.846000000000004,29.509 +2020-07-20 15:45:00,88.84,29.899,47.846000000000004,29.509 +2020-07-20 16:00:00,89.26,33.132,49.641000000000005,29.509 +2020-07-20 16:15:00,89.55,32.911,49.641000000000005,29.509 +2020-07-20 16:30:00,91.94,32.528,49.641000000000005,29.509 +2020-07-20 16:45:00,94.53,28.75,49.641000000000005,29.509 +2020-07-20 17:00:00,95.39,32.519,54.133,29.509 +2020-07-20 17:15:00,94.5,32.255,54.133,29.509 +2020-07-20 17:30:00,96.79,31.66,54.133,29.509 +2020-07-20 17:45:00,98.25,31.088,54.133,29.509 +2020-07-20 18:00:00,98.64,35.414,53.761,29.509 +2020-07-20 18:15:00,97.32,33.545,53.761,29.509 +2020-07-20 18:30:00,97.45,31.878,53.761,29.509 +2020-07-20 18:45:00,96.82,34.93,53.761,29.509 +2020-07-20 19:00:00,93.5,37.611,53.923,29.509 +2020-07-20 19:15:00,90.15,36.361,53.923,29.509 +2020-07-20 19:30:00,89.51,35.339,53.923,29.509 +2020-07-20 19:45:00,89.22,33.908,53.923,29.509 +2020-07-20 20:00:00,86.74,31.003,58.786,29.509 +2020-07-20 20:15:00,87.84,31.72,58.786,29.509 +2020-07-20 20:30:00,88.62,32.219,58.786,29.509 +2020-07-20 20:45:00,88.28,32.134,58.786,29.509 +2020-07-20 21:00:00,86.92,29.896,54.591,29.509 +2020-07-20 21:15:00,85.22,32.747,54.591,29.509 +2020-07-20 21:30:00,80.97,33.064,54.591,29.509 +2020-07-20 21:45:00,80.99,33.756,54.591,29.509 +2020-07-20 22:00:00,74.55,31.004,51.551,29.509 +2020-07-20 22:15:00,75.32,34.176,51.551,29.509 +2020-07-20 22:30:00,74.29,29.589000000000002,51.551,29.509 +2020-07-20 22:45:00,72.88,26.478,51.551,29.509 +2020-07-20 23:00:00,68.65,24.109,44.716,29.509 +2020-07-20 23:15:00,69.81,20.999000000000002,44.716,29.509 +2020-07-20 23:30:00,68.67,19.935,44.716,29.509 +2020-07-20 23:45:00,67.93,18.665,44.716,29.509 +2020-07-21 00:00:00,63.82,17.929000000000002,43.01,29.509 +2020-07-21 00:15:00,65.39,18.83,43.01,29.509 +2020-07-21 00:30:00,64.02,17.522000000000002,43.01,29.509 +2020-07-21 00:45:00,64.2,17.633,43.01,29.509 +2020-07-21 01:00:00,64.16,17.659000000000002,40.687,29.509 +2020-07-21 01:15:00,65.18,16.822,40.687,29.509 +2020-07-21 01:30:00,64.43,15.337,40.687,29.509 +2020-07-21 01:45:00,65.07,15.425,40.687,29.509 +2020-07-21 02:00:00,63.66,15.28,39.554,29.509 +2020-07-21 02:15:00,65.32,14.513,39.554,29.509 +2020-07-21 02:30:00,72.4,15.956,39.554,29.509 +2020-07-21 02:45:00,73.53,16.692,39.554,29.509 +2020-07-21 03:00:00,68.84,17.657,38.958,29.509 +2020-07-21 03:15:00,66.73,17.21,38.958,29.509 +2020-07-21 03:30:00,70.8,16.541,38.958,29.509 +2020-07-21 03:45:00,75.24,15.792,38.958,29.509 +2020-07-21 04:00:00,77.83,19.896,39.783,29.509 +2020-07-21 04:15:00,79.77,25.436,39.783,29.509 +2020-07-21 04:30:00,76.2,22.725,39.783,29.509 +2020-07-21 04:45:00,78.11,22.65,39.783,29.509 +2020-07-21 05:00:00,86.55,34.32,42.281000000000006,29.509 +2020-07-21 05:15:00,88.52,40.176,42.281000000000006,29.509 +2020-07-21 05:30:00,90.48,35.345,42.281000000000006,29.509 +2020-07-21 05:45:00,99.68,33.008,42.281000000000006,29.509 +2020-07-21 06:00:00,105.81,33.314,50.801,29.509 +2020-07-21 06:15:00,105.55,32.696,50.801,29.509 +2020-07-21 06:30:00,99.73,32.465,50.801,29.509 +2020-07-21 06:45:00,103.62,35.137,50.801,29.509 +2020-07-21 07:00:00,102.74,34.907,60.202,29.509 +2020-07-21 07:15:00,100.9,35.742,60.202,29.509 +2020-07-21 07:30:00,99.85,33.693000000000005,60.202,29.509 +2020-07-21 07:45:00,99.38,34.294000000000004,60.202,29.509 +2020-07-21 08:00:00,99.42,29.659000000000002,54.461000000000006,29.509 +2020-07-21 08:15:00,99.3,33.006,54.461000000000006,29.509 +2020-07-21 08:30:00,105.13,34.493,54.461000000000006,29.509 +2020-07-21 08:45:00,106.32,37.162,54.461000000000006,29.509 +2020-07-21 09:00:00,105.96,32.863,50.753,29.509 +2020-07-21 09:15:00,101.04,32.369,50.753,29.509 +2020-07-21 09:30:00,99.71,36.133,50.753,29.509 +2020-07-21 09:45:00,98.06,39.431999999999995,50.753,29.509 +2020-07-21 10:00:00,97.74,36.961999999999996,49.703,29.509 +2020-07-21 10:15:00,100.93,38.46,49.703,29.509 +2020-07-21 10:30:00,104.39,38.344,49.703,29.509 +2020-07-21 10:45:00,104.51,39.336999999999996,49.703,29.509 +2020-07-21 11:00:00,102.28,37.006,49.42100000000001,29.509 +2020-07-21 11:15:00,99.28,38.304,49.42100000000001,29.509 +2020-07-21 11:30:00,95.02,39.493,49.42100000000001,29.509 +2020-07-21 11:45:00,101.89,40.571,49.42100000000001,29.509 +2020-07-21 12:00:00,99.86,35.217,47.155,29.509 +2020-07-21 12:15:00,95.66,34.506,47.155,29.509 +2020-07-21 12:30:00,93.76,33.255,47.155,29.509 +2020-07-21 12:45:00,93.59,34.227,47.155,29.509 +2020-07-21 13:00:00,92.33,34.104,47.515,29.509 +2020-07-21 13:15:00,91.18,35.485,47.515,29.509 +2020-07-21 13:30:00,89.86,33.629,47.515,29.509 +2020-07-21 13:45:00,90.0,33.645,47.515,29.509 +2020-07-21 14:00:00,90.09,35.437,47.575,29.509 +2020-07-21 14:15:00,92.68,34.449,47.575,29.509 +2020-07-21 14:30:00,90.15,33.472,47.575,29.509 +2020-07-21 14:45:00,88.28,34.118,47.575,29.509 +2020-07-21 15:00:00,88.62,36.428000000000004,48.903,29.509 +2020-07-21 15:15:00,88.99,34.052,48.903,29.509 +2020-07-21 15:30:00,89.61,32.603,48.903,29.509 +2020-07-21 15:45:00,91.36,30.955,48.903,29.509 +2020-07-21 16:00:00,93.5,33.351,50.218999999999994,29.509 +2020-07-21 16:15:00,91.9,33.176,50.218999999999994,29.509 +2020-07-21 16:30:00,94.2,32.317,50.218999999999994,29.509 +2020-07-21 16:45:00,95.32,29.316,50.218999999999994,29.509 +2020-07-21 17:00:00,95.98,33.25,55.396,29.509 +2020-07-21 17:15:00,97.01,33.464,55.396,29.509 +2020-07-21 17:30:00,98.47,32.316,55.396,29.509 +2020-07-21 17:45:00,99.18,31.405,55.396,29.509 +2020-07-21 18:00:00,98.99,34.709,55.583999999999996,29.509 +2020-07-21 18:15:00,99.15,34.52,55.583999999999996,29.509 +2020-07-21 18:30:00,101.85,32.603,55.583999999999996,29.509 +2020-07-21 18:45:00,98.64,35.358000000000004,55.583999999999996,29.509 +2020-07-21 19:00:00,97.79,36.768,56.071000000000005,29.509 +2020-07-21 19:15:00,91.91,35.734,56.071000000000005,29.509 +2020-07-21 19:30:00,90.5,34.538000000000004,56.071000000000005,29.509 +2020-07-21 19:45:00,89.8,33.467,56.071000000000005,29.509 +2020-07-21 20:00:00,87.79,30.967,61.55,29.509 +2020-07-21 20:15:00,91.21,30.176,61.55,29.509 +2020-07-21 20:30:00,90.1,30.614,61.55,29.509 +2020-07-21 20:45:00,90.93,31.037,61.55,29.509 +2020-07-21 21:00:00,86.27,29.811999999999998,55.94,29.509 +2020-07-21 21:15:00,86.42,30.99,55.94,29.509 +2020-07-21 21:30:00,83.72,31.548000000000002,55.94,29.509 +2020-07-21 21:45:00,82.77,32.415,55.94,29.509 +2020-07-21 22:00:00,76.63,29.698,52.857,29.509 +2020-07-21 22:15:00,77.71,32.525999999999996,52.857,29.509 +2020-07-21 22:30:00,75.72,28.224,52.857,29.509 +2020-07-21 22:45:00,76.32,25.078000000000003,52.857,29.509 +2020-07-21 23:00:00,71.02,21.905,46.04,29.509 +2020-07-21 23:15:00,71.7,20.565,46.04,29.509 +2020-07-21 23:30:00,70.36,19.544,46.04,29.509 +2020-07-21 23:45:00,70.53,18.503,46.04,29.509 +2020-07-22 00:00:00,67.59,17.987000000000002,42.195,29.509 +2020-07-22 00:15:00,68.74,18.885,42.195,29.509 +2020-07-22 00:30:00,66.81,17.581,42.195,29.509 +2020-07-22 00:45:00,67.64,17.7,42.195,29.509 +2020-07-22 01:00:00,66.47,17.722,38.82,29.509 +2020-07-22 01:15:00,67.91,16.884,38.82,29.509 +2020-07-22 01:30:00,65.56,15.405999999999999,38.82,29.509 +2020-07-22 01:45:00,66.27,15.485999999999999,38.82,29.509 +2020-07-22 02:00:00,64.97,15.347999999999999,37.023,29.509 +2020-07-22 02:15:00,72.4,14.513,37.023,29.509 +2020-07-22 02:30:00,74.12,16.021,37.023,29.509 +2020-07-22 02:45:00,73.22,16.758,37.023,29.509 +2020-07-22 03:00:00,66.46,17.713,36.818000000000005,29.509 +2020-07-22 03:15:00,75.15,17.28,36.818000000000005,29.509 +2020-07-22 03:30:00,76.71,16.618,36.818000000000005,29.509 +2020-07-22 03:45:00,78.64,15.88,36.818000000000005,29.509 +2020-07-22 04:00:00,77.87,19.965,37.495,29.509 +2020-07-22 04:15:00,72.77,25.488000000000003,37.495,29.509 +2020-07-22 04:30:00,76.27,22.774,37.495,29.509 +2020-07-22 04:45:00,81.51,22.7,37.495,29.509 +2020-07-22 05:00:00,85.93,34.351,39.858000000000004,29.509 +2020-07-22 05:15:00,89.79,40.176,39.858000000000004,29.509 +2020-07-22 05:30:00,98.41,35.385,39.858000000000004,29.509 +2020-07-22 05:45:00,102.38,33.051,39.858000000000004,29.509 +2020-07-22 06:00:00,107.43,33.343,52.867,29.509 +2020-07-22 06:15:00,101.45,32.733000000000004,52.867,29.509 +2020-07-22 06:30:00,105.73,32.514,52.867,29.509 +2020-07-22 06:45:00,102.48,35.209,52.867,29.509 +2020-07-22 07:00:00,105.43,34.973,66.061,29.509 +2020-07-22 07:15:00,109.7,35.829,66.061,29.509 +2020-07-22 07:30:00,109.95,33.79,66.061,29.509 +2020-07-22 07:45:00,108.67,34.412,66.061,29.509 +2020-07-22 08:00:00,103.21,29.785,58.532,29.509 +2020-07-22 08:15:00,103.06,33.128,58.532,29.509 +2020-07-22 08:30:00,103.46,34.603,58.532,29.509 +2020-07-22 08:45:00,102.16,37.262,58.532,29.509 +2020-07-22 09:00:00,103.55,32.961999999999996,56.047,29.509 +2020-07-22 09:15:00,112.12,32.464,56.047,29.509 +2020-07-22 09:30:00,113.73,36.217,56.047,29.509 +2020-07-22 09:45:00,114.83,39.509,56.047,29.509 +2020-07-22 10:00:00,106.75,37.044000000000004,53.823,29.509 +2020-07-22 10:15:00,110.2,38.532,53.823,29.509 +2020-07-22 10:30:00,111.64,38.41,53.823,29.509 +2020-07-22 10:45:00,111.39,39.402,53.823,29.509 +2020-07-22 11:00:00,107.68,37.074,54.184,29.509 +2020-07-22 11:15:00,106.1,38.369,54.184,29.509 +2020-07-22 11:30:00,109.13,39.548,54.184,29.509 +2020-07-22 11:45:00,107.03,40.616,54.184,29.509 +2020-07-22 12:00:00,105.01,35.274,52.628,29.509 +2020-07-22 12:15:00,102.07,34.558,52.628,29.509 +2020-07-22 12:30:00,96.47,33.306,52.628,29.509 +2020-07-22 12:45:00,97.11,34.27,52.628,29.509 +2020-07-22 13:00:00,97.9,34.126999999999995,52.31,29.509 +2020-07-22 13:15:00,101.75,35.499,52.31,29.509 +2020-07-22 13:30:00,97.86,33.644,52.31,29.509 +2020-07-22 13:45:00,95.02,33.67,52.31,29.509 +2020-07-22 14:00:00,94.83,35.455999999999996,52.278999999999996,29.509 +2020-07-22 14:15:00,98.49,34.472,52.278999999999996,29.509 +2020-07-22 14:30:00,95.78,33.493,52.278999999999996,29.509 +2020-07-22 14:45:00,94.64,34.146,52.278999999999996,29.509 +2020-07-22 15:00:00,92.12,36.445,53.306999999999995,29.509 +2020-07-22 15:15:00,93.44,34.067,53.306999999999995,29.509 +2020-07-22 15:30:00,92.66,32.624,53.306999999999995,29.509 +2020-07-22 15:45:00,95.61,30.971999999999998,53.306999999999995,29.509 +2020-07-22 16:00:00,95.06,33.367,55.358999999999995,29.509 +2020-07-22 16:15:00,96.05,33.196,55.358999999999995,29.509 +2020-07-22 16:30:00,96.79,32.354,55.358999999999995,29.509 +2020-07-22 16:45:00,97.88,29.37,55.358999999999995,29.509 +2020-07-22 17:00:00,99.85,33.293,59.211999999999996,29.509 +2020-07-22 17:15:00,99.72,33.529,59.211999999999996,29.509 +2020-07-22 17:30:00,100.86,32.388000000000005,59.211999999999996,29.509 +2020-07-22 17:45:00,101.07,31.495,59.211999999999996,29.509 +2020-07-22 18:00:00,101.72,34.8,60.403999999999996,29.509 +2020-07-22 18:15:00,102.59,34.605,60.403999999999996,29.509 +2020-07-22 18:30:00,100.5,32.693000000000005,60.403999999999996,29.509 +2020-07-22 18:45:00,100.9,35.45,60.403999999999996,29.509 +2020-07-22 19:00:00,97.19,36.861,60.993,29.509 +2020-07-22 19:15:00,97.17,35.821999999999996,60.993,29.509 +2020-07-22 19:30:00,92.14,34.623000000000005,60.993,29.509 +2020-07-22 19:45:00,91.77,33.551,60.993,29.509 +2020-07-22 20:00:00,90.35,31.044,66.6,29.509 +2020-07-22 20:15:00,91.78,30.253,66.6,29.509 +2020-07-22 20:30:00,95.13,30.684,66.6,29.509 +2020-07-22 20:45:00,92.78,31.101999999999997,66.6,29.509 +2020-07-22 21:00:00,90.14,29.88,59.855,29.509 +2020-07-22 21:15:00,88.39,31.054000000000002,59.855,29.509 +2020-07-22 21:30:00,85.36,31.596,59.855,29.509 +2020-07-22 21:45:00,84.32,32.446,59.855,29.509 +2020-07-22 22:00:00,79.02,29.726,54.942,29.509 +2020-07-22 22:15:00,78.99,32.55,54.942,29.509 +2020-07-22 22:30:00,76.96,28.224,54.942,29.509 +2020-07-22 22:45:00,79.38,25.072,54.942,29.509 +2020-07-22 23:00:00,71.81,21.918000000000003,46.056000000000004,29.509 +2020-07-22 23:15:00,73.46,20.590999999999998,46.056000000000004,29.509 +2020-07-22 23:30:00,72.52,19.585,46.056000000000004,29.509 +2020-07-22 23:45:00,71.69,18.542,46.056000000000004,29.509 +2020-07-23 00:00:00,68.3,18.047,40.859,29.509 +2020-07-23 00:15:00,70.02,18.944000000000003,40.859,29.509 +2020-07-23 00:30:00,68.78,17.644000000000002,40.859,29.509 +2020-07-23 00:45:00,69.1,17.771,40.859,29.509 +2020-07-23 01:00:00,68.25,17.789,39.06,29.509 +2020-07-23 01:15:00,68.94,16.949,39.06,29.509 +2020-07-23 01:30:00,67.5,15.479000000000001,39.06,29.509 +2020-07-23 01:45:00,67.58,15.552999999999999,39.06,29.509 +2020-07-23 02:00:00,66.85,15.42,37.592,29.509 +2020-07-23 02:15:00,71.03,14.513,37.592,29.509 +2020-07-23 02:30:00,76.38,16.089000000000002,37.592,29.509 +2020-07-23 02:45:00,76.3,16.827,37.592,29.509 +2020-07-23 03:00:00,71.84,17.772000000000002,37.416,29.509 +2020-07-23 03:15:00,72.28,17.354,37.416,29.509 +2020-07-23 03:30:00,79.96,16.701,37.416,29.509 +2020-07-23 03:45:00,81.25,15.972000000000001,37.416,29.509 +2020-07-23 04:00:00,80.97,20.04,38.176,29.509 +2020-07-23 04:15:00,74.82,25.546,38.176,29.509 +2020-07-23 04:30:00,77.22,22.83,38.176,29.509 +2020-07-23 04:45:00,84.81,22.756999999999998,38.176,29.509 +2020-07-23 05:00:00,87.82,34.391,41.203,29.509 +2020-07-23 05:15:00,95.47,40.189,41.203,29.509 +2020-07-23 05:30:00,100.21,35.435,41.203,29.509 +2020-07-23 05:45:00,104.43,33.103,41.203,29.509 +2020-07-23 06:00:00,105.17,33.383,51.09,29.509 +2020-07-23 06:15:00,102.39,32.779,51.09,29.509 +2020-07-23 06:30:00,107.01,32.573,51.09,29.509 +2020-07-23 06:45:00,109.32,35.289,51.09,29.509 +2020-07-23 07:00:00,113.51,35.048,63.541000000000004,29.509 +2020-07-23 07:15:00,107.64,35.925,63.541000000000004,29.509 +2020-07-23 07:30:00,106.21,33.898,63.541000000000004,29.509 +2020-07-23 07:45:00,107.98,34.539,63.541000000000004,29.509 +2020-07-23 08:00:00,109.58,29.919,55.65,29.509 +2020-07-23 08:15:00,109.46,33.257,55.65,29.509 +2020-07-23 08:30:00,109.2,34.72,55.65,29.509 +2020-07-23 08:45:00,111.06,37.368,55.65,29.509 +2020-07-23 09:00:00,111.53,33.069,51.833999999999996,29.509 +2020-07-23 09:15:00,109.8,32.566,51.833999999999996,29.509 +2020-07-23 09:30:00,107.6,36.308,51.833999999999996,29.509 +2020-07-23 09:45:00,106.21,39.592,51.833999999999996,29.509 +2020-07-23 10:00:00,111.27,37.133,49.70399999999999,29.509 +2020-07-23 10:15:00,114.48,38.61,49.70399999999999,29.509 +2020-07-23 10:30:00,116.26,38.482,49.70399999999999,29.509 +2020-07-23 10:45:00,112.02,39.472,49.70399999999999,29.509 +2020-07-23 11:00:00,112.26,37.147,48.593999999999994,29.509 +2020-07-23 11:15:00,110.11,38.439,48.593999999999994,29.509 +2020-07-23 11:30:00,107.88,39.608000000000004,48.593999999999994,29.509 +2020-07-23 11:45:00,103.41,40.667,48.593999999999994,29.509 +2020-07-23 12:00:00,100.19,35.335,46.275,29.509 +2020-07-23 12:15:00,101.53,34.614000000000004,46.275,29.509 +2020-07-23 12:30:00,98.55,33.36,46.275,29.509 +2020-07-23 12:45:00,97.18,34.318000000000005,46.275,29.509 +2020-07-23 13:00:00,95.62,34.154,45.803000000000004,29.509 +2020-07-23 13:15:00,94.73,35.516,45.803000000000004,29.509 +2020-07-23 13:30:00,96.45,33.664,45.803000000000004,29.509 +2020-07-23 13:45:00,95.72,33.7,45.803000000000004,29.509 +2020-07-23 14:00:00,95.01,35.48,46.251999999999995,29.509 +2020-07-23 14:15:00,93.53,34.498000000000005,46.251999999999995,29.509 +2020-07-23 14:30:00,92.34,33.518,46.251999999999995,29.509 +2020-07-23 14:45:00,91.59,34.177,46.251999999999995,29.509 +2020-07-23 15:00:00,90.99,36.466,48.309,29.509 +2020-07-23 15:15:00,91.91,34.086,48.309,29.509 +2020-07-23 15:30:00,92.29,32.647,48.309,29.509 +2020-07-23 15:45:00,98.7,30.991999999999997,48.309,29.509 +2020-07-23 16:00:00,94.54,33.385,49.681999999999995,29.509 +2020-07-23 16:15:00,95.86,33.22,49.681999999999995,29.509 +2020-07-23 16:30:00,97.35,32.391999999999996,49.681999999999995,29.509 +2020-07-23 16:45:00,99.16,29.426,49.681999999999995,29.509 +2020-07-23 17:00:00,99.71,33.339,53.086000000000006,29.509 +2020-07-23 17:15:00,100.72,33.598,53.086000000000006,29.509 +2020-07-23 17:30:00,104.23,32.466,53.086000000000006,29.509 +2020-07-23 17:45:00,102.39,31.589000000000002,53.086000000000006,29.509 +2020-07-23 18:00:00,103.82,34.895,54.038999999999994,29.509 +2020-07-23 18:15:00,102.29,34.695,54.038999999999994,29.509 +2020-07-23 18:30:00,102.15,32.79,54.038999999999994,29.509 +2020-07-23 18:45:00,101.8,35.545,54.038999999999994,29.509 +2020-07-23 19:00:00,98.33,36.96,53.408,29.509 +2020-07-23 19:15:00,94.82,35.916,53.408,29.509 +2020-07-23 19:30:00,93.96,34.714,53.408,29.509 +2020-07-23 19:45:00,92.17,33.641,53.408,29.509 +2020-07-23 20:00:00,91.92,31.128,55.309,29.509 +2020-07-23 20:15:00,92.98,30.337,55.309,29.509 +2020-07-23 20:30:00,94.08,30.759,55.309,29.509 +2020-07-23 20:45:00,96.13,31.17,55.309,29.509 +2020-07-23 21:00:00,89.56,29.951999999999998,51.585,29.509 +2020-07-23 21:15:00,88.93,31.123,51.585,29.509 +2020-07-23 21:30:00,84.47,31.65,51.585,29.509 +2020-07-23 21:45:00,83.82,32.483000000000004,51.585,29.509 +2020-07-23 22:00:00,79.54,29.758000000000003,48.006,29.509 +2020-07-23 22:15:00,79.02,32.576,48.006,29.509 +2020-07-23 22:30:00,77.0,28.225,48.006,29.509 +2020-07-23 22:45:00,76.14,25.069000000000003,48.006,29.509 +2020-07-23 23:00:00,71.58,21.935,42.309,29.509 +2020-07-23 23:15:00,73.24,20.619,42.309,29.509 +2020-07-23 23:30:00,70.94,19.628,42.309,29.509 +2020-07-23 23:45:00,70.55,18.584,42.309,29.509 +2020-07-24 00:00:00,68.03,16.248,39.649,29.509 +2020-07-24 00:15:00,69.75,17.363,39.649,29.509 +2020-07-24 00:30:00,68.38,16.374000000000002,39.649,29.509 +2020-07-24 00:45:00,68.41,16.957,39.649,29.509 +2020-07-24 01:00:00,66.25,16.582,37.744,29.509 +2020-07-24 01:15:00,67.66,15.015,37.744,29.509 +2020-07-24 01:30:00,66.78,14.513,37.744,29.509 +2020-07-24 01:45:00,67.15,14.513,37.744,29.509 +2020-07-24 02:00:00,66.84,15.012,36.965,29.509 +2020-07-24 02:15:00,67.65,14.513,36.965,29.509 +2020-07-24 02:30:00,73.02,16.486,36.965,29.509 +2020-07-24 02:45:00,75.8,16.48,36.965,29.509 +2020-07-24 03:00:00,71.52,18.334,37.678000000000004,29.509 +2020-07-24 03:15:00,70.7,16.549,37.678000000000004,29.509 +2020-07-24 03:30:00,77.5,15.645999999999999,37.678000000000004,29.509 +2020-07-24 03:45:00,79.57,15.852,37.678000000000004,29.509 +2020-07-24 04:00:00,80.0,20.035999999999998,38.591,29.509 +2020-07-24 04:15:00,75.24,23.857,38.591,29.509 +2020-07-24 04:30:00,76.43,22.151999999999997,38.591,29.509 +2020-07-24 04:45:00,79.48,21.465,38.591,29.509 +2020-07-24 05:00:00,87.6,32.664,40.666,29.509 +2020-07-24 05:15:00,90.22,39.36,40.666,29.509 +2020-07-24 05:30:00,93.14,34.783,40.666,29.509 +2020-07-24 05:45:00,91.92,31.98,40.666,29.509 +2020-07-24 06:00:00,106.2,32.453,51.784,29.509 +2020-07-24 06:15:00,107.22,32.135,51.784,29.509 +2020-07-24 06:30:00,110.0,31.943,51.784,29.509 +2020-07-24 06:45:00,107.88,34.431999999999995,51.784,29.509 +2020-07-24 07:00:00,114.31,34.941,61.383,29.509 +2020-07-24 07:15:00,115.72,36.741,61.383,29.509 +2020-07-24 07:30:00,112.69,32.617,61.383,29.509 +2020-07-24 07:45:00,107.73,33.121,61.383,29.509 +2020-07-24 08:00:00,109.58,29.456,55.272,29.509 +2020-07-24 08:15:00,115.15,33.56,55.272,29.509 +2020-07-24 08:30:00,122.28,34.815,55.272,29.509 +2020-07-24 08:45:00,123.43,37.437,55.272,29.509 +2020-07-24 09:00:00,124.99,30.558000000000003,53.506,29.509 +2020-07-24 09:15:00,124.06,32.117,53.506,29.509 +2020-07-24 09:30:00,113.17,35.128,53.506,29.509 +2020-07-24 09:45:00,102.68,38.842,53.506,29.509 +2020-07-24 10:00:00,108.12,36.326,51.363,29.509 +2020-07-24 10:15:00,109.83,37.472,51.363,29.509 +2020-07-24 10:30:00,109.0,37.959,51.363,29.509 +2020-07-24 10:45:00,104.36,38.887,51.363,29.509 +2020-07-24 11:00:00,106.38,36.842,51.043,29.509 +2020-07-24 11:15:00,106.17,37.016999999999996,51.043,29.509 +2020-07-24 11:30:00,105.41,37.599000000000004,51.043,29.509 +2020-07-24 11:45:00,102.94,37.579,51.043,29.509 +2020-07-24 12:00:00,101.2,32.623000000000005,47.52,29.509 +2020-07-24 12:15:00,101.62,31.401999999999997,47.52,29.509 +2020-07-24 12:30:00,104.8,30.261,47.52,29.509 +2020-07-24 12:45:00,107.68,30.285,47.52,29.509 +2020-07-24 13:00:00,109.01,30.66,45.494,29.509 +2020-07-24 13:15:00,111.33,32.129,45.494,29.509 +2020-07-24 13:30:00,109.44,31.159000000000002,45.494,29.509 +2020-07-24 13:45:00,103.01,31.551,45.494,29.509 +2020-07-24 14:00:00,93.71,32.553000000000004,43.883,29.509 +2020-07-24 14:15:00,95.41,32.075,43.883,29.509 +2020-07-24 14:30:00,104.39,32.713,43.883,29.509 +2020-07-24 14:45:00,101.16,32.585,43.883,29.509 +2020-07-24 15:00:00,96.2,34.821,45.714,29.509 +2020-07-24 15:15:00,94.94,32.219,45.714,29.509 +2020-07-24 15:30:00,91.29,30.305999999999997,45.714,29.509 +2020-07-24 15:45:00,89.73,29.476,45.714,29.509 +2020-07-24 16:00:00,90.1,31.035999999999998,48.222,29.509 +2020-07-24 16:15:00,94.51,31.392,48.222,29.509 +2020-07-24 16:30:00,101.04,30.37,48.222,29.509 +2020-07-24 16:45:00,100.07,26.570999999999998,48.222,29.509 +2020-07-24 17:00:00,103.96,32.347,52.619,29.509 +2020-07-24 17:15:00,101.89,32.475,52.619,29.509 +2020-07-24 17:30:00,100.32,31.561,52.619,29.509 +2020-07-24 17:45:00,99.63,30.541999999999998,52.619,29.509 +2020-07-24 18:00:00,100.58,33.814,52.99,29.509 +2020-07-24 18:15:00,97.61,32.611999999999995,52.99,29.509 +2020-07-24 18:30:00,96.16,30.566,52.99,29.509 +2020-07-24 18:45:00,95.07,33.78,52.99,29.509 +2020-07-24 19:00:00,91.05,36.055,51.923,29.509 +2020-07-24 19:15:00,89.09,35.54,51.923,29.509 +2020-07-24 19:30:00,87.63,34.434,51.923,29.509 +2020-07-24 19:45:00,86.2,32.297,51.923,29.509 +2020-07-24 20:00:00,85.56,29.603,56.238,29.509 +2020-07-24 20:15:00,85.67,29.671,56.238,29.509 +2020-07-24 20:30:00,86.67,29.561,56.238,29.509 +2020-07-24 20:45:00,85.85,29.044,56.238,29.509 +2020-07-24 21:00:00,80.66,29.201999999999998,52.426,29.509 +2020-07-24 21:15:00,79.89,32.137,52.426,29.509 +2020-07-24 21:30:00,76.38,32.484,52.426,29.509 +2020-07-24 21:45:00,77.28,33.479,52.426,29.509 +2020-07-24 22:00:00,72.52,30.523000000000003,48.196000000000005,29.509 +2020-07-24 22:15:00,73.32,33.078,48.196000000000005,29.509 +2020-07-24 22:30:00,71.32,33.355,48.196000000000005,29.509 +2020-07-24 22:45:00,70.83,30.868000000000002,48.196000000000005,29.509 +2020-07-24 23:00:00,64.51,29.603,41.71,29.509 +2020-07-24 23:15:00,66.35,26.752,41.71,29.509 +2020-07-24 23:30:00,63.12,23.924,41.71,29.509 +2020-07-24 23:45:00,64.62,22.791,41.71,29.509 +2020-07-25 00:00:00,60.52,17.883,41.105,29.398000000000003 +2020-07-25 00:15:00,63.43,18.605999999999998,41.105,29.398000000000003 +2020-07-25 00:30:00,61.73,17.007,41.105,29.398000000000003 +2020-07-25 00:45:00,60.92,16.749000000000002,41.105,29.398000000000003 +2020-07-25 01:00:00,58.7,16.616,36.934,29.398000000000003 +2020-07-25 01:15:00,60.36,15.764000000000001,36.934,29.398000000000003 +2020-07-25 01:30:00,59.34,14.513,36.934,29.398000000000003 +2020-07-25 01:45:00,59.81,15.419,36.934,29.398000000000003 +2020-07-25 02:00:00,63.49,15.175999999999998,34.782,29.398000000000003 +2020-07-25 02:15:00,67.28,14.513,34.782,29.398000000000003 +2020-07-25 02:30:00,64.51,15.055,34.782,29.398000000000003 +2020-07-25 02:45:00,58.8,15.890999999999998,34.782,29.398000000000003 +2020-07-25 03:00:00,58.48,16.201,34.489000000000004,29.398000000000003 +2020-07-25 03:15:00,60.49,14.513,34.489000000000004,29.398000000000003 +2020-07-25 03:30:00,60.97,14.513,34.489000000000004,29.398000000000003 +2020-07-25 03:45:00,67.24,15.163,34.489000000000004,29.398000000000003 +2020-07-25 04:00:00,67.49,17.617,34.111,29.398000000000003 +2020-07-25 04:15:00,64.17,20.659000000000002,34.111,29.398000000000003 +2020-07-25 04:30:00,56.16,17.320999999999998,34.111,29.398000000000003 +2020-07-25 04:45:00,60.64,16.969,34.111,29.398000000000003 +2020-07-25 05:00:00,64.22,20.432000000000002,33.283,29.398000000000003 +2020-07-25 05:15:00,62.07,16.791,33.283,29.398000000000003 +2020-07-25 05:30:00,66.1,14.513,33.283,29.398000000000003 +2020-07-25 05:45:00,71.54,15.429,33.283,29.398000000000003 +2020-07-25 06:00:00,72.53,26.487,33.653,29.398000000000003 +2020-07-25 06:15:00,71.91,33.288000000000004,33.653,29.398000000000003 +2020-07-25 06:30:00,69.01,30.473000000000003,33.653,29.398000000000003 +2020-07-25 06:45:00,69.56,30.228,33.653,29.398000000000003 +2020-07-25 07:00:00,73.11,29.875999999999998,36.732,29.398000000000003 +2020-07-25 07:15:00,76.36,30.405,36.732,29.398000000000003 +2020-07-25 07:30:00,78.06,27.684,36.732,29.398000000000003 +2020-07-25 07:45:00,80.04,28.840999999999998,36.732,29.398000000000003 +2020-07-25 08:00:00,77.09,25.694000000000003,41.318999999999996,29.398000000000003 +2020-07-25 08:15:00,79.78,29.186,41.318999999999996,29.398000000000003 +2020-07-25 08:30:00,80.24,30.247,41.318999999999996,29.398000000000003 +2020-07-25 08:45:00,80.25,33.574,41.318999999999996,29.398000000000003 +2020-07-25 09:00:00,76.95,29.991999999999997,43.195,29.398000000000003 +2020-07-25 09:15:00,80.1,31.998,43.195,29.398000000000003 +2020-07-25 09:30:00,78.95,35.445,43.195,29.398000000000003 +2020-07-25 09:45:00,77.75,38.635999999999996,43.195,29.398000000000003 +2020-07-25 10:00:00,76.41,36.813,41.843999999999994,29.398000000000003 +2020-07-25 10:15:00,81.4,38.321999999999996,41.843999999999994,29.398000000000003 +2020-07-25 10:30:00,76.14,38.407,41.843999999999994,29.398000000000003 +2020-07-25 10:45:00,74.84,38.793,41.843999999999994,29.398000000000003 +2020-07-25 11:00:00,74.35,36.548,39.035,29.398000000000003 +2020-07-25 11:15:00,74.37,37.754,39.035,29.398000000000003 +2020-07-25 11:30:00,82.18,38.760999999999996,39.035,29.398000000000003 +2020-07-25 11:45:00,79.36,39.576,39.035,29.398000000000003 +2020-07-25 12:00:00,73.47,35.176,38.001,29.398000000000003 +2020-07-25 12:15:00,71.39,34.818000000000005,38.001,29.398000000000003 +2020-07-25 12:30:00,73.92,33.497,38.001,29.398000000000003 +2020-07-25 12:45:00,73.51,34.403,38.001,29.398000000000003 +2020-07-25 13:00:00,62.75,33.873000000000005,34.747,29.398000000000003 +2020-07-25 13:15:00,60.52,34.961,34.747,29.398000000000003 +2020-07-25 13:30:00,64.66,34.251,34.747,29.398000000000003 +2020-07-25 13:45:00,71.77,33.11,34.747,29.398000000000003 +2020-07-25 14:00:00,74.39,33.992,33.434,29.398000000000003 +2020-07-25 14:15:00,80.98,32.196,33.434,29.398000000000003 +2020-07-25 14:30:00,77.88,32.56,33.434,29.398000000000003 +2020-07-25 14:45:00,74.01,32.943000000000005,33.434,29.398000000000003 +2020-07-25 15:00:00,67.72,35.54,35.921,29.398000000000003 +2020-07-25 15:15:00,63.96,33.629,35.921,29.398000000000003 +2020-07-25 15:30:00,60.29,31.785,35.921,29.398000000000003 +2020-07-25 15:45:00,70.07,29.939,35.921,29.398000000000003 +2020-07-25 16:00:00,80.46,33.92,39.427,29.398000000000003 +2020-07-25 16:15:00,81.09,33.215,39.427,29.398000000000003 +2020-07-25 16:30:00,81.48,32.46,39.427,29.398000000000003 +2020-07-25 16:45:00,81.44,28.561,39.427,29.398000000000003 +2020-07-25 17:00:00,83.25,33.041,44.096000000000004,29.398000000000003 +2020-07-25 17:15:00,81.38,30.686,44.096000000000004,29.398000000000003 +2020-07-25 17:30:00,81.62,29.645,44.096000000000004,29.398000000000003 +2020-07-25 17:45:00,80.18,29.224,44.096000000000004,29.398000000000003 +2020-07-25 18:00:00,82.87,33.99,43.931000000000004,29.398000000000003 +2020-07-25 18:15:00,82.76,34.434,43.931000000000004,29.398000000000003 +2020-07-25 18:30:00,83.27,33.746,43.931000000000004,29.398000000000003 +2020-07-25 18:45:00,81.38,33.495,43.931000000000004,29.398000000000003 +2020-07-25 19:00:00,79.1,33.891999999999996,42.187,29.398000000000003 +2020-07-25 19:15:00,73.98,32.328,42.187,29.398000000000003 +2020-07-25 19:30:00,74.69,31.985,42.187,29.398000000000003 +2020-07-25 19:45:00,73.47,31.756999999999998,42.187,29.398000000000003 +2020-07-25 20:00:00,74.38,29.776,38.315,29.398000000000003 +2020-07-25 20:15:00,74.87,28.994,38.315,29.398000000000003 +2020-07-25 20:30:00,74.53,27.961,38.315,29.398000000000003 +2020-07-25 20:45:00,73.21,29.456,38.315,29.398000000000003 +2020-07-25 21:00:00,69.25,27.854,36.843,29.398000000000003 +2020-07-25 21:15:00,69.72,30.38,36.843,29.398000000000003 +2020-07-25 21:30:00,66.85,30.825,36.843,29.398000000000003 +2020-07-25 21:45:00,66.91,31.249000000000002,36.843,29.398000000000003 +2020-07-25 22:00:00,63.87,28.166999999999998,37.260999999999996,29.398000000000003 +2020-07-25 22:15:00,65.21,30.805999999999997,37.260999999999996,29.398000000000003 +2020-07-25 22:30:00,60.75,30.22,37.260999999999996,29.398000000000003 +2020-07-25 22:45:00,62.66,27.993000000000002,37.260999999999996,29.398000000000003 +2020-07-25 23:00:00,57.37,25.910999999999998,32.148,29.398000000000003 +2020-07-25 23:15:00,58.97,23.625,32.148,29.398000000000003 +2020-07-25 23:30:00,57.29,23.531,32.148,29.398000000000003 +2020-07-25 23:45:00,57.1,22.938000000000002,32.148,29.398000000000003 +2020-07-26 00:00:00,52.63,19.403,28.905,29.398000000000003 +2020-07-26 00:15:00,55.31,18.910999999999998,28.905,29.398000000000003 +2020-07-26 00:30:00,54.02,17.189,28.905,29.398000000000003 +2020-07-26 00:45:00,54.11,16.762999999999998,28.905,29.398000000000003 +2020-07-26 01:00:00,51.5,16.92,26.906999999999996,29.398000000000003 +2020-07-26 01:15:00,52.88,15.854000000000001,26.906999999999996,29.398000000000003 +2020-07-26 01:30:00,53.34,14.513,26.906999999999996,29.398000000000003 +2020-07-26 01:45:00,53.47,14.937999999999999,26.906999999999996,29.398000000000003 +2020-07-26 02:00:00,55.77,14.847000000000001,25.938000000000002,29.398000000000003 +2020-07-26 02:15:00,52.74,14.513,25.938000000000002,29.398000000000003 +2020-07-26 02:30:00,51.94,15.683,25.938000000000002,29.398000000000003 +2020-07-26 02:45:00,52.62,16.17,25.938000000000002,29.398000000000003 +2020-07-26 03:00:00,52.69,17.144000000000002,24.693,29.398000000000003 +2020-07-26 03:15:00,52.83,15.054,24.693,29.398000000000003 +2020-07-26 03:30:00,53.82,14.513,24.693,29.398000000000003 +2020-07-26 03:45:00,53.8,14.713,24.693,29.398000000000003 +2020-07-26 04:00:00,53.27,17.141,25.683000000000003,29.398000000000003 +2020-07-26 04:15:00,52.43,19.784000000000002,25.683000000000003,29.398000000000003 +2020-07-26 04:30:00,51.66,17.875999999999998,25.683000000000003,29.398000000000003 +2020-07-26 04:45:00,52.2,17.003,25.683000000000003,29.398000000000003 +2020-07-26 05:00:00,51.2,20.91,26.023000000000003,29.398000000000003 +2020-07-26 05:15:00,51.8,16.61,26.023000000000003,29.398000000000003 +2020-07-26 05:30:00,52.03,14.513,26.023000000000003,29.398000000000003 +2020-07-26 05:45:00,52.83,14.56,26.023000000000003,29.398000000000003 +2020-07-26 06:00:00,54.33,23.101,25.834,29.398000000000003 +2020-07-26 06:15:00,54.38,30.927,25.834,29.398000000000003 +2020-07-26 06:30:00,54.62,27.465999999999998,25.834,29.398000000000003 +2020-07-26 06:45:00,55.22,26.263,25.834,29.398000000000003 +2020-07-26 07:00:00,57.6,26.066,27.765,29.398000000000003 +2020-07-26 07:15:00,56.19,24.92,27.765,29.398000000000003 +2020-07-26 07:30:00,57.26,23.711,27.765,29.398000000000003 +2020-07-26 07:45:00,57.26,24.921,27.765,29.398000000000003 +2020-07-26 08:00:00,55.54,22.46,31.357,29.398000000000003 +2020-07-26 08:15:00,55.35,27.239,31.357,29.398000000000003 +2020-07-26 08:30:00,54.77,29.068,31.357,29.398000000000003 +2020-07-26 08:45:00,57.54,32.083,31.357,29.398000000000003 +2020-07-26 09:00:00,55.42,28.44,33.238,29.398000000000003 +2020-07-26 09:15:00,54.51,29.836,33.238,29.398000000000003 +2020-07-26 09:30:00,54.13,33.762,33.238,29.398000000000003 +2020-07-26 09:45:00,56.02,38.06,33.238,29.398000000000003 +2020-07-26 10:00:00,57.58,36.551,34.22,29.398000000000003 +2020-07-26 10:15:00,58.34,38.132,34.22,29.398000000000003 +2020-07-26 10:30:00,58.17,38.382,34.22,29.398000000000003 +2020-07-26 10:45:00,57.3,40.123000000000005,34.22,29.398000000000003 +2020-07-26 11:00:00,52.42,37.379,36.298,29.398000000000003 +2020-07-26 11:15:00,55.26,38.073,36.298,29.398000000000003 +2020-07-26 11:30:00,54.51,39.75,36.298,29.398000000000003 +2020-07-26 11:45:00,58.89,40.765,36.298,29.398000000000003 +2020-07-26 12:00:00,51.91,37.62,33.52,29.398000000000003 +2020-07-26 12:15:00,53.07,36.328,33.52,29.398000000000003 +2020-07-26 12:30:00,55.05,35.425,33.52,29.398000000000003 +2020-07-26 12:45:00,57.13,35.735,33.52,29.398000000000003 +2020-07-26 13:00:00,54.79,34.94,30.12,29.398000000000003 +2020-07-26 13:15:00,57.11,35.007,30.12,29.398000000000003 +2020-07-26 13:30:00,57.12,33.091,30.12,29.398000000000003 +2020-07-26 13:45:00,53.35,33.279,30.12,29.398000000000003 +2020-07-26 14:00:00,53.46,35.445,27.233,29.398000000000003 +2020-07-26 14:15:00,50.5,33.985,27.233,29.398000000000003 +2020-07-26 14:30:00,48.85,32.835,27.233,29.398000000000003 +2020-07-26 14:45:00,48.31,32.092,27.233,29.398000000000003 +2020-07-26 15:00:00,50.34,35.024,27.468000000000004,29.398000000000003 +2020-07-26 15:15:00,51.44,32.132,27.468000000000004,29.398000000000003 +2020-07-26 15:30:00,51.5,30.025,27.468000000000004,29.398000000000003 +2020-07-26 15:45:00,56.42,28.409000000000002,27.468000000000004,29.398000000000003 +2020-07-26 16:00:00,57.3,30.403000000000002,30.8,29.398000000000003 +2020-07-26 16:15:00,58.79,30.057,30.8,29.398000000000003 +2020-07-26 16:30:00,61.59,30.386999999999997,30.8,29.398000000000003 +2020-07-26 16:45:00,60.88,26.564,30.8,29.398000000000003 +2020-07-26 17:00:00,65.37,31.451,37.806,29.398000000000003 +2020-07-26 17:15:00,66.63,30.805999999999997,37.806,29.398000000000003 +2020-07-26 17:30:00,68.97,30.625999999999998,37.806,29.398000000000003 +2020-07-26 17:45:00,71.33,30.439,37.806,29.398000000000003 +2020-07-26 18:00:00,72.0,35.931999999999995,40.766,29.398000000000003 +2020-07-26 18:15:00,74.61,35.803000000000004,40.766,29.398000000000003 +2020-07-26 18:30:00,73.46,35.058,40.766,29.398000000000003 +2020-07-26 18:45:00,72.82,34.749,40.766,29.398000000000003 +2020-07-26 19:00:00,75.82,37.553000000000004,41.163000000000004,29.398000000000003 +2020-07-26 19:15:00,72.02,34.741,41.163000000000004,29.398000000000003 +2020-07-26 19:30:00,72.59,34.144,41.163000000000004,29.398000000000003 +2020-07-26 19:45:00,73.1,33.296,41.163000000000004,29.398000000000003 +2020-07-26 20:00:00,77.43,31.500999999999998,39.885999999999996,29.398000000000003 +2020-07-26 20:15:00,75.87,30.485,39.885999999999996,29.398000000000003 +2020-07-26 20:30:00,76.9,30.201999999999998,39.885999999999996,29.398000000000003 +2020-07-26 20:45:00,76.0,29.991,39.885999999999996,29.398000000000003 +2020-07-26 21:00:00,74.18,28.511,38.900999999999996,29.398000000000003 +2020-07-26 21:15:00,74.35,30.761999999999997,38.900999999999996,29.398000000000003 +2020-07-26 21:30:00,73.05,30.476999999999997,38.900999999999996,29.398000000000003 +2020-07-26 21:45:00,74.93,31.252,38.900999999999996,29.398000000000003 +2020-07-26 22:00:00,69.15,30.444000000000003,39.806999999999995,29.398000000000003 +2020-07-26 22:15:00,68.91,31.346999999999998,39.806999999999995,29.398000000000003 +2020-07-26 22:30:00,67.21,30.45,39.806999999999995,29.398000000000003 +2020-07-26 22:45:00,66.92,26.933000000000003,39.806999999999995,29.398000000000003 +2020-07-26 23:00:00,62.44,24.776,35.564,29.398000000000003 +2020-07-26 23:15:00,64.2,23.701,35.564,29.398000000000003 +2020-07-26 23:30:00,63.75,22.988000000000003,35.564,29.398000000000003 +2020-07-26 23:45:00,63.65,22.491,35.564,29.398000000000003 +2020-07-27 00:00:00,58.08,20.721999999999998,36.578,29.509 +2020-07-27 00:15:00,59.82,20.807,36.578,29.509 +2020-07-27 00:30:00,59.44,18.666,36.578,29.509 +2020-07-27 00:45:00,59.65,17.87,36.578,29.509 +2020-07-27 01:00:00,58.51,18.438,35.292,29.509 +2020-07-27 01:15:00,59.94,17.435,35.292,29.509 +2020-07-27 01:30:00,58.16,16.151,35.292,29.509 +2020-07-27 01:45:00,59.68,16.791,35.292,29.509 +2020-07-27 02:00:00,59.36,17.198,34.319,29.509 +2020-07-27 02:15:00,60.33,14.605,34.319,29.509 +2020-07-27 02:30:00,60.15,16.959,34.319,29.509 +2020-07-27 02:45:00,68.55,17.345,34.319,29.509 +2020-07-27 03:00:00,69.23,18.741,33.13,29.509 +2020-07-27 03:15:00,68.09,17.291,33.13,29.509 +2020-07-27 03:30:00,64.78,16.747,33.13,29.509 +2020-07-27 03:45:00,67.17,17.275,33.13,29.509 +2020-07-27 04:00:00,69.51,22.643,33.851,29.509 +2020-07-27 04:15:00,71.44,28.05,33.851,29.509 +2020-07-27 04:30:00,75.51,25.430999999999997,33.851,29.509 +2020-07-27 04:45:00,75.8,24.941,33.851,29.509 +2020-07-27 05:00:00,83.51,35.275999999999996,38.718,29.509 +2020-07-27 05:15:00,93.98,40.316,38.718,29.509 +2020-07-27 05:30:00,96.18,35.374,38.718,29.509 +2020-07-27 05:45:00,97.7,33.821,38.718,29.509 +2020-07-27 06:00:00,98.3,32.796,51.648999999999994,29.509 +2020-07-27 06:15:00,101.88,32.159,51.648999999999994,29.509 +2020-07-27 06:30:00,101.45,32.286,51.648999999999994,29.509 +2020-07-27 06:45:00,102.71,36.009,51.648999999999994,29.509 +2020-07-27 07:00:00,109.88,35.56,60.159,29.509 +2020-07-27 07:15:00,110.12,36.783,60.159,29.509 +2020-07-27 07:30:00,108.64,34.733000000000004,60.159,29.509 +2020-07-27 07:45:00,103.69,36.539,60.159,29.509 +2020-07-27 08:00:00,102.39,32.04,53.8,29.509 +2020-07-27 08:15:00,101.23,35.729,53.8,29.509 +2020-07-27 08:30:00,102.32,36.937,53.8,29.509 +2020-07-27 08:45:00,101.63,40.533,53.8,29.509 +2020-07-27 09:00:00,100.95,35.754,50.583,29.509 +2020-07-27 09:15:00,100.48,35.586,50.583,29.509 +2020-07-27 09:30:00,99.92,38.594,50.583,29.509 +2020-07-27 09:45:00,102.25,40.314,50.583,29.509 +2020-07-27 10:00:00,100.85,39.336,49.11600000000001,29.509 +2020-07-27 10:15:00,105.36,40.782,49.11600000000001,29.509 +2020-07-27 10:30:00,110.58,40.625,49.11600000000001,29.509 +2020-07-27 10:45:00,104.99,40.507,49.11600000000001,29.509 +2020-07-27 11:00:00,104.14,38.387,49.056000000000004,29.509 +2020-07-27 11:15:00,99.79,39.169000000000004,49.056000000000004,29.509 +2020-07-27 11:30:00,97.15,41.443999999999996,49.056000000000004,29.509 +2020-07-27 11:45:00,97.01,43.053000000000004,49.056000000000004,29.509 +2020-07-27 12:00:00,94.59,37.788000000000004,47.227,29.509 +2020-07-27 12:15:00,96.64,36.614000000000004,47.227,29.509 +2020-07-27 12:30:00,94.46,34.49,47.227,29.509 +2020-07-27 12:45:00,96.43,34.6,47.227,29.509 +2020-07-27 13:00:00,94.41,34.707,47.006,29.509 +2020-07-27 13:15:00,98.63,33.953,47.006,29.509 +2020-07-27 13:30:00,92.15,32.293,47.006,29.509 +2020-07-27 13:45:00,92.79,33.483000000000004,47.006,29.509 +2020-07-27 14:00:00,96.24,34.769,47.19,29.509 +2020-07-27 14:15:00,94.96,34.046,47.19,29.509 +2020-07-27 14:30:00,97.53,32.781,47.19,29.509 +2020-07-27 14:45:00,94.66,34.342,47.19,29.509 +2020-07-27 15:00:00,96.06,36.696,47.846000000000004,29.509 +2020-07-27 15:15:00,96.69,33.32,47.846000000000004,29.509 +2020-07-27 15:30:00,94.08,32.165,47.846000000000004,29.509 +2020-07-27 15:45:00,95.96,30.063000000000002,47.846000000000004,29.509 +2020-07-27 16:00:00,97.15,33.278,49.641000000000005,29.509 +2020-07-27 16:15:00,97.99,33.095,49.641000000000005,29.509 +2020-07-27 16:30:00,102.46,32.815,49.641000000000005,29.509 +2020-07-27 16:45:00,101.93,29.17,49.641000000000005,29.509 +2020-07-27 17:00:00,100.45,32.861999999999995,54.133,29.509 +2020-07-27 17:15:00,98.37,32.753,54.133,29.509 +2020-07-27 17:30:00,98.03,32.219,54.133,29.509 +2020-07-27 17:45:00,97.96,31.778000000000002,54.133,29.509 +2020-07-27 18:00:00,100.96,36.108000000000004,53.761,29.509 +2020-07-27 18:15:00,102.23,34.207,53.761,29.509 +2020-07-27 18:30:00,99.94,32.584,53.761,29.509 +2020-07-27 18:45:00,102.7,35.639,53.761,29.509 +2020-07-27 19:00:00,97.52,38.338,53.923,29.509 +2020-07-27 19:15:00,93.44,37.054,53.923,29.509 +2020-07-27 19:30:00,92.1,36.010999999999996,53.923,29.509 +2020-07-27 19:45:00,92.41,34.579,53.923,29.509 +2020-07-27 20:00:00,90.77,31.631999999999998,58.786,29.509 +2020-07-27 20:15:00,91.86,32.35,58.786,29.509 +2020-07-27 20:30:00,92.73,32.789,58.786,29.509 +2020-07-27 20:45:00,92.17,32.653,58.786,29.509 +2020-07-27 21:00:00,88.4,30.436,54.591,29.509 +2020-07-27 21:15:00,86.32,33.257,54.591,29.509 +2020-07-27 21:30:00,81.87,33.473,54.591,29.509 +2020-07-27 21:45:00,81.44,34.038000000000004,54.591,29.509 +2020-07-27 22:00:00,75.29,31.253,51.551,29.509 +2020-07-27 22:15:00,76.35,34.385999999999996,51.551,29.509 +2020-07-27 22:30:00,74.74,29.62,51.551,29.509 +2020-07-27 22:45:00,78.59,26.478,51.551,29.509 +2020-07-27 23:00:00,69.51,24.256,44.716,29.509 +2020-07-27 23:15:00,70.81,21.219,44.716,29.509 +2020-07-27 23:30:00,70.12,20.26,44.716,29.509 +2020-07-27 23:45:00,69.6,18.979,44.716,29.509 +2020-07-28 00:00:00,66.06,18.403,43.01,29.509 +2020-07-28 00:15:00,67.08,19.291,43.01,29.509 +2020-07-28 00:30:00,66.41,18.014,43.01,29.509 +2020-07-28 00:45:00,67.0,18.176,43.01,29.509 +2020-07-28 01:00:00,64.96,18.167,40.687,29.509 +2020-07-28 01:15:00,66.69,17.328,40.687,29.509 +2020-07-28 01:30:00,66.57,15.895,40.687,29.509 +2020-07-28 01:45:00,67.11,15.945,40.687,29.509 +2020-07-28 02:00:00,65.29,15.835,39.554,29.509 +2020-07-28 02:15:00,66.22,14.524000000000001,39.554,29.509 +2020-07-28 02:30:00,67.12,16.489,39.554,29.509 +2020-07-28 02:45:00,74.51,17.230999999999998,39.554,29.509 +2020-07-28 03:00:00,75.51,18.124000000000002,38.958,29.509 +2020-07-28 03:15:00,71.6,17.781,38.958,29.509 +2020-07-28 03:30:00,69.58,17.167,38.958,29.509 +2020-07-28 03:45:00,71.85,16.477999999999998,38.958,29.509 +2020-07-28 04:00:00,75.9,20.482,39.783,29.509 +2020-07-28 04:15:00,74.67,25.921999999999997,39.783,29.509 +2020-07-28 04:30:00,75.33,23.201999999999998,39.783,29.509 +2020-07-28 04:45:00,79.19,23.13,39.783,29.509 +2020-07-28 05:00:00,91.86,34.734,42.281000000000006,29.509 +2020-07-28 05:15:00,95.84,40.444,42.281000000000006,29.509 +2020-07-28 05:30:00,96.24,35.861,42.281000000000006,29.509 +2020-07-28 05:45:00,94.7,33.519,42.281000000000006,29.509 +2020-07-28 06:00:00,104.47,33.715,50.801,29.509 +2020-07-28 06:15:00,108.31,33.161,50.801,29.509 +2020-07-28 06:30:00,109.99,33.008,50.801,29.509 +2020-07-28 06:45:00,108.51,35.823,50.801,29.509 +2020-07-28 07:00:00,107.45,35.556,60.202,29.509 +2020-07-28 07:15:00,105.46,36.536,60.202,29.509 +2020-07-28 07:30:00,107.66,34.574,60.202,29.509 +2020-07-28 07:45:00,111.91,35.306,60.202,29.509 +2020-07-28 08:00:00,111.65,30.717,54.461000000000006,29.509 +2020-07-28 08:15:00,111.03,34.015,54.461000000000006,29.509 +2020-07-28 08:30:00,104.82,35.417,54.461000000000006,29.509 +2020-07-28 08:45:00,105.65,38.006,54.461000000000006,29.509 +2020-07-28 09:00:00,105.34,33.71,50.753,29.509 +2020-07-28 09:15:00,104.97,33.184,50.753,29.509 +2020-07-28 09:30:00,111.63,36.865,50.753,29.509 +2020-07-28 09:45:00,111.1,40.098,50.753,29.509 +2020-07-28 10:00:00,103.92,37.664,49.703,29.509 +2020-07-28 10:15:00,108.58,39.079,49.703,29.509 +2020-07-28 10:30:00,104.47,38.921,49.703,29.509 +2020-07-28 10:45:00,102.44,39.895,49.703,29.509 +2020-07-28 11:00:00,99.91,37.591,49.42100000000001,29.509 +2020-07-28 11:15:00,101.99,38.866,49.42100000000001,29.509 +2020-07-28 11:30:00,102.94,39.99,49.42100000000001,29.509 +2020-07-28 11:45:00,104.11,40.995,49.42100000000001,29.509 +2020-07-28 12:00:00,103.58,35.702,47.155,29.509 +2020-07-28 12:15:00,108.58,34.954,47.155,29.509 +2020-07-28 12:30:00,112.24,33.703,47.155,29.509 +2020-07-28 12:45:00,113.51,34.622,47.155,29.509 +2020-07-28 13:00:00,110.53,34.356,47.515,29.509 +2020-07-28 13:15:00,113.4,35.666,47.515,29.509 +2020-07-28 13:30:00,111.67,33.818000000000005,47.515,29.509 +2020-07-28 13:45:00,108.55,33.906,47.515,29.509 +2020-07-28 14:00:00,98.05,35.644,47.575,29.509 +2020-07-28 14:15:00,94.38,34.681,47.575,29.509 +2020-07-28 14:30:00,93.99,33.705,47.575,29.509 +2020-07-28 14:45:00,93.91,34.396,47.575,29.509 +2020-07-28 15:00:00,99.21,36.606,48.903,29.509 +2020-07-28 15:15:00,101.07,34.223,48.903,29.509 +2020-07-28 15:30:00,100.87,32.81,48.903,29.509 +2020-07-28 15:45:00,100.92,31.144000000000002,48.903,29.509 +2020-07-28 16:00:00,97.59,33.515,50.218999999999994,29.509 +2020-07-28 16:15:00,98.84,33.376999999999995,50.218999999999994,29.509 +2020-07-28 16:30:00,99.43,32.621,50.218999999999994,29.509 +2020-07-28 16:45:00,99.6,29.761,50.218999999999994,29.509 +2020-07-28 17:00:00,102.62,33.61,55.396,29.509 +2020-07-28 17:15:00,101.28,33.985,55.396,29.509 +2020-07-28 17:30:00,100.85,32.898,55.396,29.509 +2020-07-28 17:45:00,102.53,32.125,55.396,29.509 +2020-07-28 18:00:00,102.54,35.43,55.583999999999996,29.509 +2020-07-28 18:15:00,100.74,35.217,55.583999999999996,29.509 +2020-07-28 18:30:00,102.02,33.344,55.583999999999996,29.509 +2020-07-28 18:45:00,102.2,36.103,55.583999999999996,29.509 +2020-07-28 19:00:00,97.76,37.53,56.071000000000005,29.509 +2020-07-28 19:15:00,95.49,36.464,56.071000000000005,29.509 +2020-07-28 19:30:00,94.9,35.249,56.071000000000005,29.509 +2020-07-28 19:45:00,93.33,34.178000000000004,56.071000000000005,29.509 +2020-07-28 20:00:00,91.91,31.64,61.55,29.509 +2020-07-28 20:15:00,93.19,30.85,61.55,29.509 +2020-07-28 20:30:00,94.82,31.225,61.55,29.509 +2020-07-28 20:45:00,93.22,31.589000000000002,61.55,29.509 +2020-07-28 21:00:00,89.1,30.386999999999997,55.94,29.509 +2020-07-28 21:15:00,87.92,31.531999999999996,55.94,29.509 +2020-07-28 21:30:00,84.69,31.991999999999997,55.94,29.509 +2020-07-28 21:45:00,84.68,32.727,55.94,29.509 +2020-07-28 22:00:00,78.94,29.971,52.857,29.509 +2020-07-28 22:15:00,85.09,32.757,52.857,29.509 +2020-07-28 22:30:00,81.12,28.272,52.857,29.509 +2020-07-28 22:45:00,81.25,25.096999999999998,52.857,29.509 +2020-07-28 23:00:00,77.11,22.079,46.04,29.509 +2020-07-28 23:15:00,78.13,20.805999999999997,46.04,29.509 +2020-07-28 23:30:00,77.41,19.891,46.04,29.509 +2020-07-28 23:45:00,77.45,18.84,46.04,29.509 +2020-07-29 00:00:00,72.28,18.484,42.195,29.509 +2020-07-29 00:15:00,73.33,19.371,42.195,29.509 +2020-07-29 00:30:00,73.27,18.098,42.195,29.509 +2020-07-29 00:45:00,72.85,18.268,42.195,29.509 +2020-07-29 01:00:00,71.33,18.252,38.82,29.509 +2020-07-29 01:15:00,72.86,17.414,38.82,29.509 +2020-07-29 01:30:00,73.45,15.99,38.82,29.509 +2020-07-29 01:45:00,73.58,16.035,38.82,29.509 +2020-07-29 02:00:00,71.67,15.93,37.023,29.509 +2020-07-29 02:15:00,72.3,14.637,37.023,29.509 +2020-07-29 02:30:00,69.76,16.581,37.023,29.509 +2020-07-29 02:45:00,79.06,17.323,37.023,29.509 +2020-07-29 03:00:00,79.64,18.204,36.818000000000005,29.509 +2020-07-29 03:15:00,77.13,17.878,36.818000000000005,29.509 +2020-07-29 03:30:00,73.32,17.271,36.818000000000005,29.509 +2020-07-29 03:45:00,77.35,16.589000000000002,36.818000000000005,29.509 +2020-07-29 04:00:00,79.17,20.584,37.495,29.509 +2020-07-29 04:15:00,78.45,26.015,37.495,29.509 +2020-07-29 04:30:00,78.67,23.294,37.495,29.509 +2020-07-29 04:45:00,81.9,23.224,37.495,29.509 +2020-07-29 05:00:00,97.1,34.83,39.858000000000004,29.509 +2020-07-29 05:15:00,99.54,40.534,39.858000000000004,29.509 +2020-07-29 05:30:00,103.49,35.981,39.858000000000004,29.509 +2020-07-29 05:45:00,98.13,33.632,39.858000000000004,29.509 +2020-07-29 06:00:00,105.66,33.81,52.867,29.509 +2020-07-29 06:15:00,110.73,33.266999999999996,52.867,29.509 +2020-07-29 06:30:00,111.3,33.122,52.867,29.509 +2020-07-29 06:45:00,112.66,35.955,52.867,29.509 +2020-07-29 07:00:00,108.85,35.685,66.061,29.509 +2020-07-29 07:15:00,105.98,36.684,66.061,29.509 +2020-07-29 07:30:00,105.83,34.736999999999995,66.061,29.509 +2020-07-29 07:45:00,104.0,35.484,66.061,29.509 +2020-07-29 08:00:00,106.11,30.903000000000002,58.532,29.509 +2020-07-29 08:15:00,104.13,34.188,58.532,29.509 +2020-07-29 08:30:00,106.01,35.577,58.532,29.509 +2020-07-29 08:45:00,106.01,38.155,58.532,29.509 +2020-07-29 09:00:00,106.42,33.86,56.047,29.509 +2020-07-29 09:15:00,105.79,33.329,56.047,29.509 +2020-07-29 09:30:00,105.45,36.997,56.047,29.509 +2020-07-29 09:45:00,103.21,40.218,56.047,29.509 +2020-07-29 10:00:00,102.4,37.788000000000004,53.823,29.509 +2020-07-29 10:15:00,106.01,39.188,53.823,29.509 +2020-07-29 10:30:00,104.96,39.025,53.823,29.509 +2020-07-29 10:45:00,104.1,39.994,53.823,29.509 +2020-07-29 11:00:00,108.14,37.696,54.184,29.509 +2020-07-29 11:15:00,108.27,38.968,54.184,29.509 +2020-07-29 11:30:00,108.1,40.082,54.184,29.509 +2020-07-29 11:45:00,102.46,41.075,54.184,29.509 +2020-07-29 12:00:00,102.99,35.788000000000004,52.628,29.509 +2020-07-29 12:15:00,100.87,35.034,52.628,29.509 +2020-07-29 12:30:00,100.15,33.786,52.628,29.509 +2020-07-29 12:45:00,98.46,34.696,52.628,29.509 +2020-07-29 13:00:00,98.77,34.41,52.31,29.509 +2020-07-29 13:15:00,97.85,35.708,52.31,29.509 +2020-07-29 13:30:00,97.02,33.861,52.31,29.509 +2020-07-29 13:45:00,97.78,33.959,52.31,29.509 +2020-07-29 14:00:00,96.14,35.687,52.278999999999996,29.509 +2020-07-29 14:15:00,95.9,34.727,52.278999999999996,29.509 +2020-07-29 14:30:00,96.19,33.756,52.278999999999996,29.509 +2020-07-29 14:45:00,96.56,34.452,52.278999999999996,29.509 +2020-07-29 15:00:00,95.81,36.641999999999996,53.306999999999995,29.509 +2020-07-29 15:15:00,92.1,34.259,53.306999999999995,29.509 +2020-07-29 15:30:00,94.45,32.853,53.306999999999995,29.509 +2020-07-29 15:45:00,96.56,31.185,53.306999999999995,29.509 +2020-07-29 16:00:00,97.51,33.548,55.358999999999995,29.509 +2020-07-29 16:15:00,98.55,33.417,55.358999999999995,29.509 +2020-07-29 16:30:00,98.44,32.673,55.358999999999995,29.509 +2020-07-29 16:45:00,98.51,29.837,55.358999999999995,29.509 +2020-07-29 17:00:00,100.98,33.671,59.211999999999996,29.509 +2020-07-29 17:15:00,101.93,34.071,59.211999999999996,29.509 +2020-07-29 17:30:00,102.63,32.995,59.211999999999996,29.509 +2020-07-29 17:45:00,103.93,32.245,59.211999999999996,29.509 +2020-07-29 18:00:00,104.7,35.549,60.403999999999996,29.509 +2020-07-29 18:15:00,103.25,35.335,60.403999999999996,29.509 +2020-07-29 18:30:00,103.93,33.47,60.403999999999996,29.509 +2020-07-29 18:45:00,103.57,36.229,60.403999999999996,29.509 +2020-07-29 19:00:00,99.94,37.659,60.993,29.509 +2020-07-29 19:15:00,95.58,36.588,60.993,29.509 +2020-07-29 19:30:00,95.02,35.373000000000005,60.993,29.509 +2020-07-29 19:45:00,95.21,34.303000000000004,60.993,29.509 +2020-07-29 20:00:00,93.06,31.76,66.6,29.509 +2020-07-29 20:15:00,95.46,30.971999999999998,66.6,29.509 +2020-07-29 20:30:00,95.47,31.337,66.6,29.509 +2020-07-29 20:45:00,94.87,31.686999999999998,66.6,29.509 +2020-07-29 21:00:00,91.12,30.487,59.855,29.509 +2020-07-29 21:15:00,90.81,31.628,59.855,29.509 +2020-07-29 21:30:00,87.11,32.074,59.855,29.509 +2020-07-29 21:45:00,85.59,32.789,59.855,29.509 +2020-07-29 22:00:00,80.94,30.025,54.942,29.509 +2020-07-29 22:15:00,79.82,32.803000000000004,54.942,29.509 +2020-07-29 22:30:00,78.83,28.29,54.942,29.509 +2020-07-29 22:45:00,78.28,25.111,54.942,29.509 +2020-07-29 23:00:00,73.71,22.119,46.056000000000004,29.509 +2020-07-29 23:15:00,75.13,20.851999999999997,46.056000000000004,29.509 +2020-07-29 23:30:00,74.23,19.952,46.056000000000004,29.509 +2020-07-29 23:45:00,72.75,18.901,46.056000000000004,29.509 +2020-07-30 00:00:00,66.91,18.569000000000003,40.859,29.509 +2020-07-30 00:15:00,70.85,19.454,40.859,29.509 +2020-07-30 00:30:00,68.48,18.186,40.859,29.509 +2020-07-30 00:45:00,71.01,18.364,40.859,29.509 +2020-07-30 01:00:00,68.53,18.339000000000002,39.06,29.509 +2020-07-30 01:15:00,69.68,17.503,39.06,29.509 +2020-07-30 01:30:00,69.53,16.087,39.06,29.509 +2020-07-30 01:45:00,69.27,16.129,39.06,29.509 +2020-07-30 02:00:00,69.25,16.028,37.592,29.509 +2020-07-30 02:15:00,69.73,14.753,37.592,29.509 +2020-07-30 02:30:00,69.73,16.677,37.592,29.509 +2020-07-30 02:45:00,70.05,17.419,37.592,29.509 +2020-07-30 03:00:00,70.14,18.289,37.416,29.509 +2020-07-30 03:15:00,67.95,17.977999999999998,37.416,29.509 +2020-07-30 03:30:00,71.64,17.379,37.416,29.509 +2020-07-30 03:45:00,74.13,16.704,37.416,29.509 +2020-07-30 04:00:00,78.12,20.691,38.176,29.509 +2020-07-30 04:15:00,77.94,26.114,38.176,29.509 +2020-07-30 04:30:00,82.51,23.393,38.176,29.509 +2020-07-30 04:45:00,88.96,23.324,38.176,29.509 +2020-07-30 05:00:00,95.56,34.935,41.203,29.509 +2020-07-30 05:15:00,96.07,40.637,41.203,29.509 +2020-07-30 05:30:00,94.6,36.111999999999995,41.203,29.509 +2020-07-30 05:45:00,97.84,33.755,41.203,29.509 +2020-07-30 06:00:00,103.74,33.912,51.09,29.509 +2020-07-30 06:15:00,106.27,33.382,51.09,29.509 +2020-07-30 06:30:00,107.84,33.246,51.09,29.509 +2020-07-30 06:45:00,108.65,36.096,51.09,29.509 +2020-07-30 07:00:00,115.44,35.821,63.541000000000004,29.509 +2020-07-30 07:15:00,115.82,36.841,63.541000000000004,29.509 +2020-07-30 07:30:00,119.1,34.909,63.541000000000004,29.509 +2020-07-30 07:45:00,116.84,35.671,63.541000000000004,29.509 +2020-07-30 08:00:00,114.55,31.096,55.65,29.509 +2020-07-30 08:15:00,110.16,34.368,55.65,29.509 +2020-07-30 08:30:00,112.03,35.745,55.65,29.509 +2020-07-30 08:45:00,108.34,38.311,55.65,29.509 +2020-07-30 09:00:00,110.22,34.018,51.833999999999996,29.509 +2020-07-30 09:15:00,117.14,33.48,51.833999999999996,29.509 +2020-07-30 09:30:00,114.83,37.135,51.833999999999996,29.509 +2020-07-30 09:45:00,112.31,40.343,51.833999999999996,29.509 +2020-07-30 10:00:00,105.58,37.918,49.70399999999999,29.509 +2020-07-30 10:15:00,106.54,39.303000000000004,49.70399999999999,29.509 +2020-07-30 10:30:00,105.24,39.134,49.70399999999999,29.509 +2020-07-30 10:45:00,108.78,40.099000000000004,49.70399999999999,29.509 +2020-07-30 11:00:00,108.28,37.806,48.593999999999994,29.509 +2020-07-30 11:15:00,108.61,39.073,48.593999999999994,29.509 +2020-07-30 11:30:00,108.06,40.179,48.593999999999994,29.509 +2020-07-30 11:45:00,105.5,41.161,48.593999999999994,29.509 +2020-07-30 12:00:00,101.8,35.878,46.275,29.509 +2020-07-30 12:15:00,100.72,35.118,46.275,29.509 +2020-07-30 12:30:00,98.24,33.873000000000005,46.275,29.509 +2020-07-30 12:45:00,98.08,34.773,46.275,29.509 +2020-07-30 13:00:00,96.54,34.467,45.803000000000004,29.509 +2020-07-30 13:15:00,96.92,35.754,45.803000000000004,29.509 +2020-07-30 13:30:00,96.23,33.906,45.803000000000004,29.509 +2020-07-30 13:45:00,97.99,34.016,45.803000000000004,29.509 +2020-07-30 14:00:00,96.53,35.733000000000004,46.251999999999995,29.509 +2020-07-30 14:15:00,96.4,34.777,46.251999999999995,29.509 +2020-07-30 14:30:00,96.01,33.809,46.251999999999995,29.509 +2020-07-30 14:45:00,97.0,34.510999999999996,46.251999999999995,29.509 +2020-07-30 15:00:00,95.45,36.68,48.309,29.509 +2020-07-30 15:15:00,95.82,34.297,48.309,29.509 +2020-07-30 15:30:00,95.51,32.898,48.309,29.509 +2020-07-30 15:45:00,96.03,31.229,48.309,29.509 +2020-07-30 16:00:00,97.23,33.583,49.681999999999995,29.509 +2020-07-30 16:15:00,98.16,33.459,49.681999999999995,29.509 +2020-07-30 16:30:00,98.84,32.728,49.681999999999995,29.509 +2020-07-30 16:45:00,101.61,29.916999999999998,49.681999999999995,29.509 +2020-07-30 17:00:00,101.98,33.736,53.086000000000006,29.509 +2020-07-30 17:15:00,101.92,34.16,53.086000000000006,29.509 +2020-07-30 17:30:00,102.4,33.095,53.086000000000006,29.509 +2020-07-30 17:45:00,104.67,32.369,53.086000000000006,29.509 +2020-07-30 18:00:00,104.01,35.671,54.038999999999994,29.509 +2020-07-30 18:15:00,102.55,35.457,54.038999999999994,29.509 +2020-07-30 18:30:00,101.69,33.601,54.038999999999994,29.509 +2020-07-30 18:45:00,101.62,36.359,54.038999999999994,29.509 +2020-07-30 19:00:00,97.79,37.793,53.408,29.509 +2020-07-30 19:15:00,95.25,36.719,53.408,29.509 +2020-07-30 19:30:00,94.88,35.501999999999995,53.408,29.509 +2020-07-30 19:45:00,94.26,34.434,53.408,29.509 +2020-07-30 20:00:00,94.38,31.886,55.309,29.509 +2020-07-30 20:15:00,95.53,31.099,55.309,29.509 +2020-07-30 20:30:00,95.23,31.453000000000003,55.309,29.509 +2020-07-30 20:45:00,94.49,31.789,55.309,29.509 +2020-07-30 21:00:00,90.29,30.593000000000004,51.585,29.509 +2020-07-30 21:15:00,90.19,31.728,51.585,29.509 +2020-07-30 21:30:00,85.83,32.162,51.585,29.509 +2020-07-30 21:45:00,84.2,32.855,51.585,29.509 +2020-07-30 22:00:00,79.37,30.081999999999997,48.006,29.509 +2020-07-30 22:15:00,79.95,32.852,48.006,29.509 +2020-07-30 22:30:00,77.7,28.309,48.006,29.509 +2020-07-30 22:45:00,78.53,25.127,48.006,29.509 +2020-07-30 23:00:00,73.03,22.162,42.309,29.509 +2020-07-30 23:15:00,73.29,20.901999999999997,42.309,29.509 +2020-07-30 23:30:00,72.79,20.016,42.309,29.509 +2020-07-30 23:45:00,72.36,18.965999999999998,42.309,29.509 +2020-07-31 00:00:00,67.66,16.794,39.649,29.509 +2020-07-31 00:15:00,67.46,17.897000000000002,39.649,29.509 +2020-07-31 00:30:00,69.3,16.939,39.649,29.509 +2020-07-31 00:45:00,69.18,17.575,39.649,29.509 +2020-07-31 01:00:00,68.54,17.151,37.744,29.509 +2020-07-31 01:15:00,69.64,15.592,37.744,29.509 +2020-07-31 01:30:00,68.26,14.98,37.744,29.509 +2020-07-31 01:45:00,68.52,14.735999999999999,37.744,29.509 +2020-07-31 02:00:00,66.7,15.647,36.965,29.509 +2020-07-31 02:15:00,69.12,14.513,36.965,29.509 +2020-07-31 02:30:00,68.98,17.101,36.965,29.509 +2020-07-31 02:45:00,68.73,17.097,36.965,29.509 +2020-07-31 03:00:00,69.73,18.875999999999998,37.678000000000004,29.509 +2020-07-31 03:15:00,70.06,17.2,37.678000000000004,29.509 +2020-07-31 03:30:00,71.02,16.35,37.678000000000004,29.509 +2020-07-31 03:45:00,73.59,16.607,37.678000000000004,29.509 +2020-07-31 04:00:00,77.51,20.721,38.591,29.509 +2020-07-31 04:15:00,84.16,24.464000000000002,38.591,29.509 +2020-07-31 04:30:00,85.18,22.758000000000003,38.591,29.509 +2020-07-31 04:45:00,83.87,22.075,38.591,29.509 +2020-07-31 05:00:00,88.03,33.272,40.666,29.509 +2020-07-31 05:15:00,93.14,39.897,40.666,29.509 +2020-07-31 05:30:00,96.11,35.541,40.666,29.509 +2020-07-31 05:45:00,103.78,32.701,40.666,29.509 +2020-07-31 06:00:00,109.56,33.047,51.784,29.509 +2020-07-31 06:15:00,108.31,32.806,51.784,29.509 +2020-07-31 06:30:00,104.13,32.679,51.784,29.509 +2020-07-31 06:45:00,103.29,35.299,51.784,29.509 +2020-07-31 07:00:00,105.64,35.775,61.383,29.509 +2020-07-31 07:15:00,107.2,37.719,61.383,29.509 +2020-07-31 07:30:00,106.76,33.693000000000005,61.383,29.509 +2020-07-31 07:45:00,108.81,34.313,61.383,29.509 +2020-07-31 08:00:00,110.94,30.691,55.272,29.509 +2020-07-31 08:15:00,111.98,34.72,55.272,29.509 +2020-07-31 08:30:00,111.09,35.89,55.272,29.509 +2020-07-31 08:45:00,105.5,38.428000000000004,55.272,29.509 +2020-07-31 09:00:00,112.21,31.557,53.506,29.509 +2020-07-31 09:15:00,112.71,33.08,53.506,29.509 +2020-07-31 09:30:00,117.26,36.003,53.506,29.509 +2020-07-31 09:45:00,117.46,39.635,53.506,29.509 +2020-07-31 10:00:00,118.86,37.153,51.363,29.509 +2020-07-31 10:15:00,109.46,38.202,51.363,29.509 +2020-07-31 10:30:00,106.07,38.647,51.363,29.509 +2020-07-31 10:45:00,104.7,39.549,51.363,29.509 +2020-07-31 11:00:00,102.28,37.538000000000004,51.043,29.509 +2020-07-31 11:15:00,99.07,37.687,51.043,29.509 +2020-07-31 11:30:00,98.78,38.205999999999996,51.043,29.509 +2020-07-31 11:45:00,102.16,38.109,51.043,29.509 +2020-07-31 12:00:00,98.03,33.196,47.52,29.509 +2020-07-31 12:15:00,98.7,31.933000000000003,47.52,29.509 +2020-07-31 12:30:00,97.7,30.805999999999997,47.52,29.509 +2020-07-31 12:45:00,101.31,30.771,47.52,29.509 +2020-07-31 13:00:00,97.21,31.002,45.494,29.509 +2020-07-31 13:15:00,94.94,32.394,45.494,29.509 +2020-07-31 13:30:00,95.09,31.428,45.494,29.509 +2020-07-31 13:45:00,99.62,31.894000000000002,45.494,29.509 +2020-07-31 14:00:00,99.4,32.83,43.883,29.509 +2020-07-31 14:15:00,102.69,32.379,43.883,29.509 +2020-07-31 14:30:00,104.82,33.031,43.883,29.509 +2020-07-31 14:45:00,108.76,32.946,43.883,29.509 +2020-07-31 15:00:00,107.19,35.053000000000004,45.714,29.509 +2020-07-31 15:15:00,107.16,32.45,45.714,29.509 +2020-07-31 15:30:00,102.02,30.58,45.714,29.509 +2020-07-31 15:45:00,98.73,29.737,45.714,29.509 +2020-07-31 16:00:00,97.24,31.250999999999998,48.222,29.509 +2020-07-31 16:15:00,96.65,31.649,48.222,29.509 +2020-07-31 16:30:00,95.0,30.721999999999998,48.222,29.509 +2020-07-31 16:45:00,98.3,27.085,48.222,29.509 +2020-07-31 17:00:00,100.32,32.76,52.619,29.509 +2020-07-31 17:15:00,100.27,33.058,52.619,29.509 +2020-07-31 17:30:00,99.43,32.214,52.619,29.509 +2020-07-31 17:45:00,99.98,31.351999999999997,52.619,29.509 +2020-07-31 18:00:00,100.2,34.617,52.99,29.509 +2020-07-31 18:15:00,101.05,33.407,52.99,29.509 +2020-07-31 18:30:00,100.24,31.410999999999998,52.99,29.509 +2020-07-31 18:45:00,97.28,34.628,52.99,29.509 +2020-07-31 19:00:00,94.41,36.923,51.923,29.509 +2020-07-31 19:15:00,93.63,36.38,51.923,29.509 +2020-07-31 19:30:00,90.61,35.259,51.923,29.509 +2020-07-31 19:45:00,91.18,33.129,51.923,29.509 +2020-07-31 20:00:00,91.95,30.405,56.238,29.509 +2020-07-31 20:15:00,91.38,30.476999999999997,56.238,29.509 +2020-07-31 20:30:00,90.78,30.296999999999997,56.238,29.509 +2020-07-31 20:45:00,90.26,29.695,56.238,29.509 +2020-07-31 21:00:00,85.04,29.877,52.426,29.509 +2020-07-31 21:15:00,83.4,32.773,52.426,29.509 +2020-07-31 21:30:00,80.13,33.031,52.426,29.509 +2020-07-31 21:45:00,79.5,33.88,52.426,29.509 +2020-07-31 22:00:00,75.94,30.871,48.196000000000005,29.509 +2020-07-31 22:15:00,75.61,33.374,48.196000000000005,29.509 +2020-07-31 22:30:00,74.04,33.457,48.196000000000005,29.509 +2020-07-31 22:45:00,74.51,30.945,48.196000000000005,29.509 +2020-07-31 23:00:00,69.29,29.855999999999998,41.71,29.509 +2020-07-31 23:15:00,67.98,27.054000000000002,41.71,29.509 +2020-07-31 23:30:00,67.78,24.331999999999997,41.71,29.509 +2020-07-31 23:45:00,68.07,23.197,41.71,29.509 +2020-08-01 00:00:00,64.84,16.840999999999998,40.227,29.423000000000002 +2020-08-01 00:15:00,65.7,17.429000000000002,40.227,29.423000000000002 +2020-08-01 00:30:00,61.29,16.066,40.227,29.423000000000002 +2020-08-01 00:45:00,63.86,15.87,40.227,29.423000000000002 +2020-08-01 01:00:00,61.89,15.71,36.303000000000004,29.423000000000002 +2020-08-01 01:15:00,63.24,14.94,36.303000000000004,29.423000000000002 +2020-08-01 01:30:00,61.9,13.640999999999998,36.303000000000004,29.423000000000002 +2020-08-01 01:45:00,62.53,14.588,36.303000000000004,29.423000000000002 +2020-08-01 02:00:00,60.34,14.395999999999999,33.849000000000004,29.423000000000002 +2020-08-01 02:15:00,60.96,13.040999999999999,33.849000000000004,29.423000000000002 +2020-08-01 02:30:00,66.99,14.253,33.849000000000004,29.423000000000002 +2020-08-01 02:45:00,68.32,15.014000000000001,33.849000000000004,29.423000000000002 +2020-08-01 03:00:00,65.65,15.258,33.149,29.423000000000002 +2020-08-01 03:15:00,61.57,13.152999999999999,33.149,29.423000000000002 +2020-08-01 03:30:00,61.95,13.040999999999999,33.149,29.423000000000002 +2020-08-01 03:45:00,64.13,14.512,33.149,29.423000000000002 +2020-08-01 04:00:00,63.58,16.715,32.501,29.423000000000002 +2020-08-01 04:15:00,62.36,19.454,32.501,29.423000000000002 +2020-08-01 04:30:00,64.17,16.437,32.501,29.423000000000002 +2020-08-01 04:45:00,69.28,16.136,32.501,29.423000000000002 +2020-08-01 05:00:00,70.79,19.305,31.648000000000003,29.423000000000002 +2020-08-01 05:15:00,66.78,16.058,31.648000000000003,29.423000000000002 +2020-08-01 05:30:00,64.36,13.495999999999999,31.648000000000003,29.423000000000002 +2020-08-01 05:45:00,65.34,14.887,31.648000000000003,29.423000000000002 +2020-08-01 06:00:00,68.08,24.894000000000002,32.552,29.423000000000002 +2020-08-01 06:15:00,71.47,31.201,32.552,29.423000000000002 +2020-08-01 06:30:00,70.7,28.622,32.552,29.423000000000002 +2020-08-01 06:45:00,73.6,28.4,32.552,29.423000000000002 +2020-08-01 07:00:00,80.26,28.034000000000002,35.181999999999995,29.423000000000002 +2020-08-01 07:15:00,81.15,28.61,35.181999999999995,29.423000000000002 +2020-08-01 07:30:00,83.99,26.254,35.181999999999995,29.423000000000002 +2020-08-01 07:45:00,78.02,27.406,35.181999999999995,29.423000000000002 +2020-08-01 08:00:00,78.36,24.79,40.35,29.423000000000002 +2020-08-01 08:15:00,78.58,27.854,40.35,29.423000000000002 +2020-08-01 08:30:00,84.13,28.653000000000002,40.35,29.423000000000002 +2020-08-01 08:45:00,88.25,31.538,40.35,29.423000000000002 +2020-08-01 09:00:00,83.81,27.921,42.292,29.423000000000002 +2020-08-01 09:15:00,77.51,29.616,42.292,29.423000000000002 +2020-08-01 09:30:00,77.4,32.631,42.292,29.423000000000002 +2020-08-01 09:45:00,81.46,35.472,42.292,29.423000000000002 +2020-08-01 10:00:00,88.03,33.78,40.084,29.423000000000002 +2020-08-01 10:15:00,87.71,35.077,40.084,29.423000000000002 +2020-08-01 10:30:00,88.54,35.137,40.084,29.423000000000002 +2020-08-01 10:45:00,81.12,35.558,40.084,29.423000000000002 +2020-08-01 11:00:00,75.49,33.609,36.966,29.423000000000002 +2020-08-01 11:15:00,77.04,34.655,36.966,29.423000000000002 +2020-08-01 11:30:00,73.65,35.507,36.966,29.423000000000002 +2020-08-01 11:45:00,73.41,36.196999999999996,36.966,29.423000000000002 +2020-08-01 12:00:00,72.34,32.466,35.19,29.423000000000002 +2020-08-01 12:15:00,72.7,32.124,35.19,29.423000000000002 +2020-08-01 12:30:00,68.3,30.954,35.19,29.423000000000002 +2020-08-01 12:45:00,72.12,31.738000000000003,35.19,29.423000000000002 +2020-08-01 13:00:00,71.85,31.165,32.277,29.423000000000002 +2020-08-01 13:15:00,73.17,32.038000000000004,32.277,29.423000000000002 +2020-08-01 13:30:00,75.14,31.421,32.277,29.423000000000002 +2020-08-01 13:45:00,78.23,30.410999999999998,32.277,29.423000000000002 +2020-08-01 14:00:00,75.78,31.076999999999998,31.436999999999998,29.423000000000002 +2020-08-01 14:15:00,76.7,29.488000000000003,31.436999999999998,29.423000000000002 +2020-08-01 14:30:00,76.4,29.755,31.436999999999998,29.423000000000002 +2020-08-01 14:45:00,71.54,30.14,31.436999999999998,29.423000000000002 +2020-08-01 15:00:00,72.15,32.229,33.493,29.423000000000002 +2020-08-01 15:15:00,79.53,30.479,33.493,29.423000000000002 +2020-08-01 15:30:00,82.37,28.81,33.493,29.423000000000002 +2020-08-01 15:45:00,82.54,27.113000000000003,33.493,29.423000000000002 +2020-08-01 16:00:00,80.64,30.905,36.593,29.423000000000002 +2020-08-01 16:15:00,78.96,30.311,36.593,29.423000000000002 +2020-08-01 16:30:00,81.42,29.761,36.593,29.423000000000002 +2020-08-01 16:45:00,77.27,26.39,36.593,29.423000000000002 +2020-08-01 17:00:00,78.24,30.271,42.049,29.423000000000002 +2020-08-01 17:15:00,80.69,28.34,42.049,29.423000000000002 +2020-08-01 17:30:00,83.53,27.478,42.049,29.423000000000002 +2020-08-01 17:45:00,81.6,27.182,42.049,29.423000000000002 +2020-08-01 18:00:00,83.2,31.39,43.755,29.423000000000002 +2020-08-01 18:15:00,82.56,31.776,43.755,29.423000000000002 +2020-08-01 18:30:00,82.19,31.188000000000002,43.755,29.423000000000002 +2020-08-01 18:45:00,81.88,31.037,43.755,29.423000000000002 +2020-08-01 19:00:00,79.58,31.397,44.492,29.423000000000002 +2020-08-01 19:15:00,76.37,29.973000000000003,44.492,29.423000000000002 +2020-08-01 19:30:00,75.74,29.653000000000002,44.492,29.423000000000002 +2020-08-01 19:45:00,76.03,29.514,44.492,29.423000000000002 +2020-08-01 20:00:00,78.67,27.618000000000002,40.896,29.423000000000002 +2020-08-01 20:15:00,78.26,27.023000000000003,40.896,29.423000000000002 +2020-08-01 20:30:00,77.78,26.051,40.896,29.423000000000002 +2020-08-01 20:45:00,79.77,27.362,40.896,29.423000000000002 +2020-08-01 21:00:00,74.49,25.883000000000003,39.056,29.423000000000002 +2020-08-01 21:15:00,73.64,28.221,39.056,29.423000000000002 +2020-08-01 21:30:00,71.31,28.568,39.056,29.423000000000002 +2020-08-01 21:45:00,70.86,28.83,39.056,29.423000000000002 +2020-08-01 22:00:00,68.57,26.011,38.478,29.423000000000002 +2020-08-01 22:15:00,67.73,28.296999999999997,38.478,29.423000000000002 +2020-08-01 22:30:00,65.12,27.611,38.478,29.423000000000002 +2020-08-01 22:45:00,65.56,25.581999999999997,38.478,29.423000000000002 +2020-08-01 23:00:00,61.72,23.868000000000002,32.953,29.423000000000002 +2020-08-01 23:15:00,60.35,21.84,32.953,29.423000000000002 +2020-08-01 23:30:00,58.32,21.82,32.953,29.423000000000002 +2020-08-01 23:45:00,57.81,21.265,32.953,29.423000000000002 +2020-08-02 00:00:00,56.36,18.218,28.584,29.423000000000002 +2020-08-02 00:15:00,57.66,17.723,28.584,29.423000000000002 +2020-08-02 00:30:00,57.1,16.248,28.584,29.423000000000002 +2020-08-02 00:45:00,57.12,15.909,28.584,29.423000000000002 +2020-08-02 01:00:00,55.38,16.003,26.419,29.423000000000002 +2020-08-02 01:15:00,55.55,15.054,26.419,29.423000000000002 +2020-08-02 01:30:00,55.72,13.6,26.419,29.423000000000002 +2020-08-02 01:45:00,55.6,14.199000000000002,26.419,29.423000000000002 +2020-08-02 02:00:00,55.11,14.134,25.335,29.423000000000002 +2020-08-02 02:15:00,55.47,13.040999999999999,25.335,29.423000000000002 +2020-08-02 02:30:00,54.77,14.844000000000001,25.335,29.423000000000002 +2020-08-02 02:45:00,55.36,15.297,25.335,29.423000000000002 +2020-08-02 03:00:00,54.8,16.134,24.805,29.423000000000002 +2020-08-02 03:15:00,55.54,14.325,24.805,29.423000000000002 +2020-08-02 03:30:00,55.72,13.217,24.805,29.423000000000002 +2020-08-02 03:45:00,55.21,14.154000000000002,24.805,29.423000000000002 +2020-08-02 04:00:00,58.15,16.34,25.772,29.423000000000002 +2020-08-02 04:15:00,56.39,18.723,25.772,29.423000000000002 +2020-08-02 04:30:00,54.95,16.98,25.772,29.423000000000002 +2020-08-02 04:45:00,55.36,16.218,25.772,29.423000000000002 +2020-08-02 05:00:00,55.17,19.77,25.971999999999998,29.423000000000002 +2020-08-02 05:15:00,54.51,15.937000000000001,25.971999999999998,29.423000000000002 +2020-08-02 05:30:00,54.25,13.040999999999999,25.971999999999998,29.423000000000002 +2020-08-02 05:45:00,55.07,14.138,25.971999999999998,29.423000000000002 +2020-08-02 06:00:00,55.41,21.9,26.026,29.423000000000002 +2020-08-02 06:15:00,55.73,29.107,26.026,29.423000000000002 +2020-08-02 06:30:00,57.85,25.941,26.026,29.423000000000002 +2020-08-02 06:45:00,59.03,24.854,26.026,29.423000000000002 +2020-08-02 07:00:00,59.2,24.644000000000002,27.396,29.423000000000002 +2020-08-02 07:15:00,60.74,23.725,27.396,29.423000000000002 +2020-08-02 07:30:00,63.57,22.7,27.396,29.423000000000002 +2020-08-02 07:45:00,64.87,23.888,27.396,29.423000000000002 +2020-08-02 08:00:00,63.57,21.895,30.791999999999998,29.423000000000002 +2020-08-02 08:15:00,66.35,26.094,30.791999999999998,29.423000000000002 +2020-08-02 08:30:00,66.99,27.593000000000004,30.791999999999998,29.423000000000002 +2020-08-02 08:45:00,68.25,30.224,30.791999999999998,29.423000000000002 +2020-08-02 09:00:00,68.6,26.549,32.482,29.423000000000002 +2020-08-02 09:15:00,67.03,27.708000000000002,32.482,29.423000000000002 +2020-08-02 09:30:00,66.69,31.144000000000002,32.482,29.423000000000002 +2020-08-02 09:45:00,67.87,34.96,32.482,29.423000000000002 +2020-08-02 10:00:00,69.2,33.574,31.951,29.423000000000002 +2020-08-02 10:15:00,67.97,34.938,31.951,29.423000000000002 +2020-08-02 10:30:00,69.5,35.152,31.951,29.423000000000002 +2020-08-02 10:45:00,73.44,36.748000000000005,31.951,29.423000000000002 +2020-08-02 11:00:00,71.52,34.372,33.619,29.423000000000002 +2020-08-02 11:15:00,64.82,34.964,33.619,29.423000000000002 +2020-08-02 11:30:00,60.86,36.402,33.619,29.423000000000002 +2020-08-02 11:45:00,66.18,37.275,33.619,29.423000000000002 +2020-08-02 12:00:00,63.81,34.646,30.975,29.423000000000002 +2020-08-02 12:15:00,60.26,33.494,30.975,29.423000000000002 +2020-08-02 12:30:00,56.84,32.681,30.975,29.423000000000002 +2020-08-02 12:45:00,55.4,32.926,30.975,29.423000000000002 +2020-08-02 13:00:00,53.87,32.108000000000004,27.956999999999997,29.423000000000002 +2020-08-02 13:15:00,58.19,32.109,27.956999999999997,29.423000000000002 +2020-08-02 13:30:00,55.86,30.419,27.956999999999997,29.423000000000002 +2020-08-02 13:45:00,57.45,30.579,27.956999999999997,29.423000000000002 +2020-08-02 14:00:00,56.54,32.385999999999996,25.555999999999997,29.423000000000002 +2020-08-02 14:15:00,55.69,31.107,25.555999999999997,29.423000000000002 +2020-08-02 14:30:00,56.82,30.05,25.555999999999997,29.423000000000002 +2020-08-02 14:45:00,56.34,29.430999999999997,25.555999999999997,29.423000000000002 +2020-08-02 15:00:00,56.78,31.79,26.271,29.423000000000002 +2020-08-02 15:15:00,54.95,29.179000000000002,26.271,29.423000000000002 +2020-08-02 15:30:00,54.84,27.285,26.271,29.423000000000002 +2020-08-02 15:45:00,56.41,25.801,26.271,29.423000000000002 +2020-08-02 16:00:00,59.72,27.843000000000004,30.369,29.423000000000002 +2020-08-02 16:15:00,59.6,27.56,30.369,29.423000000000002 +2020-08-02 16:30:00,61.23,27.973000000000003,30.369,29.423000000000002 +2020-08-02 16:45:00,63.53,24.678,30.369,29.423000000000002 +2020-08-02 17:00:00,67.12,28.910999999999998,38.787,29.423000000000002 +2020-08-02 17:15:00,68.98,28.498,38.787,29.423000000000002 +2020-08-02 17:30:00,70.71,28.406,38.787,29.423000000000002 +2020-08-02 17:45:00,74.99,28.346999999999998,38.787,29.423000000000002 +2020-08-02 18:00:00,74.67,33.192,41.886,29.423000000000002 +2020-08-02 18:15:00,73.6,33.085,41.886,29.423000000000002 +2020-08-02 18:30:00,76.34,32.428000000000004,41.886,29.423000000000002 +2020-08-02 18:45:00,74.12,32.243,41.886,29.423000000000002 +2020-08-02 19:00:00,75.09,34.739000000000004,42.91,29.423000000000002 +2020-08-02 19:15:00,72.97,32.211999999999996,42.91,29.423000000000002 +2020-08-02 19:30:00,73.09,31.666,42.91,29.423000000000002 +2020-08-02 19:45:00,73.57,30.991,42.91,29.423000000000002 +2020-08-02 20:00:00,78.39,29.259,42.148999999999994,29.423000000000002 +2020-08-02 20:15:00,77.61,28.465,42.148999999999994,29.423000000000002 +2020-08-02 20:30:00,77.86,28.169,42.148999999999994,29.423000000000002 +2020-08-02 20:45:00,80.93,27.945999999999998,42.148999999999994,29.423000000000002 +2020-08-02 21:00:00,76.12,26.549,40.955999999999996,29.423000000000002 +2020-08-02 21:15:00,75.92,28.636999999999997,40.955999999999996,29.423000000000002 +2020-08-02 21:30:00,72.29,28.338,40.955999999999996,29.423000000000002 +2020-08-02 21:45:00,71.28,28.91,40.955999999999996,29.423000000000002 +2020-08-02 22:00:00,67.02,28.098000000000003,39.873000000000005,29.423000000000002 +2020-08-02 22:15:00,68.04,28.831,39.873000000000005,29.423000000000002 +2020-08-02 22:30:00,65.78,27.840999999999998,39.873000000000005,29.423000000000002 +2020-08-02 22:45:00,65.72,24.656999999999996,39.873000000000005,29.423000000000002 +2020-08-02 23:00:00,60.72,22.861,35.510999999999996,29.423000000000002 +2020-08-02 23:15:00,63.05,21.92,35.510999999999996,29.423000000000002 +2020-08-02 23:30:00,62.6,21.354,35.510999999999996,29.423000000000002 +2020-08-02 23:45:00,62.86,20.894000000000002,35.510999999999996,29.423000000000002 +2020-08-03 00:00:00,64.33,19.451,33.475,29.535 +2020-08-03 00:15:00,67.22,19.498,33.475,29.535 +2020-08-03 00:30:00,66.41,17.651,33.475,29.535 +2020-08-03 00:45:00,63.17,16.98,33.475,29.535 +2020-08-03 01:00:00,63.68,17.434,33.111,29.535 +2020-08-03 01:15:00,60.61,16.54,33.111,29.535 +2020-08-03 01:30:00,60.33,15.436,33.111,29.535 +2020-08-03 01:45:00,61.13,15.931,33.111,29.535 +2020-08-03 02:00:00,59.92,16.308,32.358000000000004,29.535 +2020-08-03 02:15:00,61.4,14.12,32.358000000000004,29.535 +2020-08-03 02:30:00,61.07,16.077,32.358000000000004,29.535 +2020-08-03 02:45:00,61.46,16.435,32.358000000000004,29.535 +2020-08-03 03:00:00,60.32,17.656,30.779,29.535 +2020-08-03 03:15:00,63.48,16.432000000000002,30.779,29.535 +2020-08-03 03:30:00,63.96,16.000999999999998,30.779,29.535 +2020-08-03 03:45:00,66.5,16.54,30.779,29.535 +2020-08-03 04:00:00,74.97,21.386999999999997,31.416,29.535 +2020-08-03 04:15:00,76.04,26.27,31.416,29.535 +2020-08-03 04:30:00,74.98,23.919,31.416,29.535 +2020-08-03 04:45:00,83.07,23.5,31.416,29.535 +2020-08-03 05:00:00,90.67,32.928000000000004,37.221,29.535 +2020-08-03 05:15:00,89.8,37.603,37.221,29.535 +2020-08-03 05:30:00,91.37,33.244,37.221,29.535 +2020-08-03 05:45:00,93.43,31.784000000000002,37.221,29.535 +2020-08-03 06:00:00,105.8,30.877,51.891000000000005,29.535 +2020-08-03 06:15:00,107.93,30.43,51.891000000000005,29.535 +2020-08-03 06:30:00,104.29,30.522,51.891000000000005,29.535 +2020-08-03 06:45:00,105.53,33.891,51.891000000000005,29.535 +2020-08-03 07:00:00,111.0,33.523,62.282,29.535 +2020-08-03 07:15:00,108.3,34.719,62.282,29.535 +2020-08-03 07:30:00,108.02,32.942,62.282,29.535 +2020-08-03 07:45:00,103.2,34.629,62.282,29.535 +2020-08-03 08:00:00,104.16,30.793000000000003,54.102,29.535 +2020-08-03 08:15:00,105.8,33.993,54.102,29.535 +2020-08-03 08:30:00,107.65,34.896,54.102,29.535 +2020-08-03 08:45:00,105.3,38.001999999999995,54.102,29.535 +2020-08-03 09:00:00,103.61,33.321999999999996,50.917,29.535 +2020-08-03 09:15:00,107.08,33.051,50.917,29.535 +2020-08-03 09:30:00,106.27,35.66,50.917,29.535 +2020-08-03 09:45:00,106.17,37.192,50.917,29.535 +2020-08-03 10:00:00,102.23,36.264,49.718999999999994,29.535 +2020-08-03 10:15:00,106.93,37.5,49.718999999999994,29.535 +2020-08-03 10:30:00,107.96,37.342,49.718999999999994,29.535 +2020-08-03 10:45:00,107.77,37.3,49.718999999999994,29.535 +2020-08-03 11:00:00,100.64,35.437,49.833999999999996,29.535 +2020-08-03 11:15:00,97.07,36.126,49.833999999999996,29.535 +2020-08-03 11:30:00,95.48,38.11,49.833999999999996,29.535 +2020-08-03 11:45:00,95.33,39.504,49.833999999999996,29.535 +2020-08-03 12:00:00,98.79,35.001,47.832,29.535 +2020-08-03 12:15:00,96.36,33.953,47.832,29.535 +2020-08-03 12:30:00,97.53,32.065,47.832,29.535 +2020-08-03 12:45:00,95.9,32.147,47.832,29.535 +2020-08-03 13:00:00,100.77,32.144,48.03,29.535 +2020-08-03 13:15:00,99.22,31.401,48.03,29.535 +2020-08-03 13:30:00,102.37,29.932,48.03,29.535 +2020-08-03 13:45:00,97.29,30.98,48.03,29.535 +2020-08-03 14:00:00,94.86,31.984,48.157,29.535 +2020-08-03 14:15:00,93.05,31.354,48.157,29.535 +2020-08-03 14:30:00,92.91,30.191999999999997,48.157,29.535 +2020-08-03 14:45:00,92.65,31.615,48.157,29.535 +2020-08-03 15:00:00,92.04,33.464,48.897,29.535 +2020-08-03 15:15:00,91.52,30.410999999999998,48.897,29.535 +2020-08-03 15:30:00,90.89,29.355999999999998,48.897,29.535 +2020-08-03 15:45:00,92.82,27.436999999999998,48.897,29.535 +2020-08-03 16:00:00,94.14,30.561,51.446000000000005,29.535 +2020-08-03 16:15:00,94.64,30.415,51.446000000000005,29.535 +2020-08-03 16:30:00,96.56,30.275,51.446000000000005,29.535 +2020-08-03 16:45:00,98.01,27.131999999999998,51.446000000000005,29.535 +2020-08-03 17:00:00,98.65,30.291,57.507,29.535 +2020-08-03 17:15:00,100.17,30.348000000000003,57.507,29.535 +2020-08-03 17:30:00,101.49,29.936999999999998,57.507,29.535 +2020-08-03 17:45:00,102.04,29.642,57.507,29.535 +2020-08-03 18:00:00,103.4,33.454,57.896,29.535 +2020-08-03 18:15:00,101.83,31.758000000000003,57.896,29.535 +2020-08-03 18:30:00,102.94,30.328000000000003,57.896,29.535 +2020-08-03 18:45:00,102.54,33.139,57.896,29.535 +2020-08-03 19:00:00,99.56,35.525,57.891999999999996,29.535 +2020-08-03 19:15:00,96.15,34.346,57.891999999999996,29.535 +2020-08-03 19:30:00,94.72,33.41,57.891999999999996,29.535 +2020-08-03 19:45:00,96.09,32.209,57.891999999999996,29.535 +2020-08-03 20:00:00,96.55,29.432,64.57300000000001,29.535 +2020-08-03 20:15:00,103.85,30.159000000000002,64.57300000000001,29.535 +2020-08-03 20:30:00,102.7,30.487,64.57300000000001,29.535 +2020-08-03 20:45:00,96.84,30.337,64.57300000000001,29.535 +2020-08-03 21:00:00,90.83,28.291999999999998,59.431999999999995,29.535 +2020-08-03 21:15:00,90.21,30.875999999999998,59.431999999999995,29.535 +2020-08-03 21:30:00,85.03,31.019000000000002,59.431999999999995,29.535 +2020-08-03 21:45:00,84.99,31.394000000000002,59.431999999999995,29.535 +2020-08-03 22:00:00,79.53,28.799,51.519,29.535 +2020-08-03 22:15:00,80.49,31.503,51.519,29.535 +2020-08-03 22:30:00,79.92,26.988000000000003,51.519,29.535 +2020-08-03 22:45:00,78.23,24.1,51.519,29.535 +2020-08-03 23:00:00,73.74,22.268,44.501000000000005,29.535 +2020-08-03 23:15:00,72.71,19.594,44.501000000000005,29.535 +2020-08-03 23:30:00,74.86,18.83,44.501000000000005,29.535 +2020-08-03 23:45:00,80.0,17.692,44.501000000000005,29.535 +2020-08-04 00:00:00,77.12,17.405,44.522,29.535 +2020-08-04 00:15:00,74.58,18.18,44.522,29.535 +2020-08-04 00:30:00,69.85,17.092,44.522,29.535 +2020-08-04 00:45:00,70.87,17.262999999999998,44.522,29.535 +2020-08-04 01:00:00,70.62,17.199,41.441,29.535 +2020-08-04 01:15:00,70.7,16.449,41.441,29.535 +2020-08-04 01:30:00,73.19,15.217,41.441,29.535 +2020-08-04 01:45:00,78.29,15.190999999999999,41.441,29.535 +2020-08-04 02:00:00,77.5,15.107999999999999,40.203,29.535 +2020-08-04 02:15:00,71.54,14.061,40.203,29.535 +2020-08-04 02:30:00,71.46,15.665999999999999,40.203,29.535 +2020-08-04 02:45:00,76.65,16.338,40.203,29.535 +2020-08-04 03:00:00,78.03,17.101,39.536,29.535 +2020-08-04 03:15:00,78.15,16.855,39.536,29.535 +2020-08-04 03:30:00,71.82,16.365,39.536,29.535 +2020-08-04 03:45:00,79.75,15.821,39.536,29.535 +2020-08-04 04:00:00,84.05,19.462,40.759,29.535 +2020-08-04 04:15:00,86.03,24.38,40.759,29.535 +2020-08-04 04:30:00,85.13,21.936,40.759,29.535 +2020-08-04 04:45:00,89.94,21.901999999999997,40.759,29.535 +2020-08-04 05:00:00,95.49,32.521,43.623999999999995,29.535 +2020-08-04 05:15:00,96.01,37.814,43.623999999999995,29.535 +2020-08-04 05:30:00,98.12,33.751,43.623999999999995,29.535 +2020-08-04 05:45:00,99.46,31.581,43.623999999999995,29.535 +2020-08-04 06:00:00,100.86,31.744,52.684,29.535 +2020-08-04 06:15:00,104.1,31.389,52.684,29.535 +2020-08-04 06:30:00,107.85,31.221999999999998,52.684,29.535 +2020-08-04 06:45:00,112.25,33.775999999999996,52.684,29.535 +2020-08-04 07:00:00,112.11,33.569,62.676,29.535 +2020-08-04 07:15:00,113.72,34.546,62.676,29.535 +2020-08-04 07:30:00,109.77,32.847,62.676,29.535 +2020-08-04 07:45:00,107.83,33.577,62.676,29.535 +2020-08-04 08:00:00,112.52,29.664,56.161,29.535 +2020-08-04 08:15:00,112.46,32.497,56.161,29.535 +2020-08-04 08:30:00,112.9,33.57,56.161,29.535 +2020-08-04 08:45:00,111.06,35.781,56.161,29.535 +2020-08-04 09:00:00,115.35,31.52,52.132,29.535 +2020-08-04 09:15:00,114.68,30.95,52.132,29.535 +2020-08-04 09:30:00,112.07,34.161,52.132,29.535 +2020-08-04 09:45:00,109.95,37.027,52.132,29.535 +2020-08-04 10:00:00,113.26,34.8,51.032,29.535 +2020-08-04 10:15:00,111.01,35.992,51.032,29.535 +2020-08-04 10:30:00,108.81,35.836,51.032,29.535 +2020-08-04 10:45:00,112.99,36.762,51.032,29.535 +2020-08-04 11:00:00,106.02,34.756,51.085,29.535 +2020-08-04 11:15:00,107.07,35.876999999999995,51.085,29.535 +2020-08-04 11:30:00,108.91,36.829,51.085,29.535 +2020-08-04 11:45:00,110.11,37.693000000000005,51.085,29.535 +2020-08-04 12:00:00,105.38,33.145,49.049,29.535 +2020-08-04 12:15:00,101.69,32.468,49.049,29.535 +2020-08-04 12:30:00,100.66,31.366999999999997,49.049,29.535 +2020-08-04 12:45:00,99.77,32.162,49.049,29.535 +2020-08-04 13:00:00,98.37,31.823,49.722,29.535 +2020-08-04 13:15:00,98.25,32.902,49.722,29.535 +2020-08-04 13:30:00,97.22,31.279,49.722,29.535 +2020-08-04 13:45:00,97.65,31.354,49.722,29.535 +2020-08-04 14:00:00,98.37,32.76,49.565,29.535 +2020-08-04 14:15:00,100.11,31.919,49.565,29.535 +2020-08-04 14:30:00,100.18,31.025,49.565,29.535 +2020-08-04 14:45:00,99.55,31.674,49.565,29.535 +2020-08-04 15:00:00,100.99,33.382,51.108999999999995,29.535 +2020-08-04 15:15:00,98.14,31.217,51.108999999999995,29.535 +2020-08-04 15:30:00,97.4,29.936999999999998,51.108999999999995,29.535 +2020-08-04 15:45:00,98.43,28.403000000000002,51.108999999999995,29.535 +2020-08-04 16:00:00,102.05,30.775,52.725,29.535 +2020-08-04 16:15:00,104.62,30.676,52.725,29.535 +2020-08-04 16:30:00,102.53,30.116999999999997,52.725,29.535 +2020-08-04 16:45:00,103.42,27.68,52.725,29.535 +2020-08-04 17:00:00,104.82,30.979,58.031000000000006,29.535 +2020-08-04 17:15:00,106.88,31.469,58.031000000000006,29.535 +2020-08-04 17:30:00,106.52,30.576,58.031000000000006,29.535 +2020-08-04 17:45:00,106.89,29.989,58.031000000000006,29.535 +2020-08-04 18:00:00,107.37,32.888000000000005,58.338,29.535 +2020-08-04 18:15:00,105.89,32.689,58.338,29.535 +2020-08-04 18:30:00,105.26,31.037,58.338,29.535 +2020-08-04 18:45:00,104.69,33.591,58.338,29.535 +2020-08-04 19:00:00,102.74,34.849000000000004,58.464,29.535 +2020-08-04 19:15:00,99.6,33.861,58.464,29.535 +2020-08-04 19:30:00,98.69,32.769,58.464,29.535 +2020-08-04 19:45:00,99.54,31.89,58.464,29.535 +2020-08-04 20:00:00,98.28,29.48,63.708,29.535 +2020-08-04 20:15:00,104.76,28.862,63.708,29.535 +2020-08-04 20:30:00,104.72,29.14,63.708,29.535 +2020-08-04 20:45:00,102.08,29.421999999999997,63.708,29.535 +2020-08-04 21:00:00,93.21,28.273000000000003,57.06399999999999,29.535 +2020-08-04 21:15:00,91.37,29.372,57.06399999999999,29.535 +2020-08-04 21:30:00,88.94,29.728,57.06399999999999,29.535 +2020-08-04 21:45:00,87.22,30.253,57.06399999999999,29.535 +2020-08-04 22:00:00,81.39,27.693,52.831,29.535 +2020-08-04 22:15:00,90.48,30.084,52.831,29.535 +2020-08-04 22:30:00,86.19,25.816999999999997,52.831,29.535 +2020-08-04 22:45:00,84.07,22.903000000000002,52.831,29.535 +2020-08-04 23:00:00,76.58,20.366,44.717,29.535 +2020-08-04 23:15:00,82.38,19.252,44.717,29.535 +2020-08-04 23:30:00,81.93,18.522000000000002,44.717,29.535 +2020-08-04 23:45:00,82.08,17.589000000000002,44.717,29.535 +2020-08-05 00:00:00,74.43,17.499000000000002,41.263000000000005,29.535 +2020-08-05 00:15:00,72.81,18.273,41.263000000000005,29.535 +2020-08-05 00:30:00,73.78,17.189,41.263000000000005,29.535 +2020-08-05 00:45:00,79.92,17.368,41.263000000000005,29.535 +2020-08-05 01:00:00,79.4,17.293,38.448,29.535 +2020-08-05 01:15:00,79.23,16.547,38.448,29.535 +2020-08-05 01:30:00,73.16,15.324000000000002,38.448,29.535 +2020-08-05 01:45:00,74.56,15.296,38.448,29.535 +2020-08-05 02:00:00,71.2,15.217,36.471,29.535 +2020-08-05 02:15:00,72.53,14.187999999999999,36.471,29.535 +2020-08-05 02:30:00,76.7,15.772,36.471,29.535 +2020-08-05 02:45:00,78.9,16.442999999999998,36.471,29.535 +2020-08-05 03:00:00,76.32,17.195,36.042,29.535 +2020-08-05 03:15:00,71.19,16.965,36.042,29.535 +2020-08-05 03:30:00,73.06,16.482,36.042,29.535 +2020-08-05 03:45:00,74.45,15.94,36.042,29.535 +2020-08-05 04:00:00,76.22,19.583,36.705,29.535 +2020-08-05 04:15:00,79.11,24.499000000000002,36.705,29.535 +2020-08-05 04:30:00,80.91,22.057,36.705,29.535 +2020-08-05 04:45:00,83.59,22.023000000000003,36.705,29.535 +2020-08-05 05:00:00,89.3,32.665,39.716,29.535 +2020-08-05 05:15:00,93.87,37.975,39.716,29.535 +2020-08-05 05:30:00,98.13,33.93,39.716,29.535 +2020-08-05 05:45:00,102.24,31.743000000000002,39.716,29.535 +2020-08-05 06:00:00,110.74,31.885,52.756,29.535 +2020-08-05 06:15:00,113.42,31.543000000000003,52.756,29.535 +2020-08-05 06:30:00,114.26,31.381999999999998,52.756,29.535 +2020-08-05 06:45:00,111.89,33.946999999999996,52.756,29.535 +2020-08-05 07:00:00,112.05,33.738,65.977,29.535 +2020-08-05 07:15:00,111.49,34.732,65.977,29.535 +2020-08-05 07:30:00,110.79,33.048,65.977,29.535 +2020-08-05 07:45:00,115.87,33.79,65.977,29.535 +2020-08-05 08:00:00,116.83,29.88,57.927,29.535 +2020-08-05 08:15:00,115.09,32.696999999999996,57.927,29.535 +2020-08-05 08:30:00,115.44,33.76,57.927,29.535 +2020-08-05 08:45:00,116.68,35.958,57.927,29.535 +2020-08-05 09:00:00,119.16,31.701,54.86,29.535 +2020-08-05 09:15:00,115.56,31.124000000000002,54.86,29.535 +2020-08-05 09:30:00,112.66,34.321999999999996,54.86,29.535 +2020-08-05 09:45:00,127.72,37.173,54.86,29.535 +2020-08-05 10:00:00,125.46,34.949,52.818000000000005,29.535 +2020-08-05 10:15:00,122.4,36.124,52.818000000000005,29.535 +2020-08-05 10:30:00,115.0,35.961999999999996,52.818000000000005,29.535 +2020-08-05 10:45:00,114.33,36.883,52.818000000000005,29.535 +2020-08-05 11:00:00,111.89,34.884,52.937,29.535 +2020-08-05 11:15:00,111.04,36.0,52.937,29.535 +2020-08-05 11:30:00,107.75,36.945,52.937,29.535 +2020-08-05 11:45:00,105.24,37.798,52.937,29.535 +2020-08-05 12:00:00,104.45,33.249,50.826,29.535 +2020-08-05 12:15:00,103.52,32.566,50.826,29.535 +2020-08-05 12:30:00,103.67,31.47,50.826,29.535 +2020-08-05 12:45:00,102.21,32.255,50.826,29.535 +2020-08-05 13:00:00,100.82,31.898000000000003,50.556000000000004,29.535 +2020-08-05 13:15:00,100.96,32.968,50.556000000000004,29.535 +2020-08-05 13:30:00,105.71,31.343000000000004,50.556000000000004,29.535 +2020-08-05 13:45:00,102.53,31.427,50.556000000000004,29.535 +2020-08-05 14:00:00,104.32,32.821,51.188,29.535 +2020-08-05 14:15:00,112.39,31.984,51.188,29.535 +2020-08-05 14:30:00,116.08,31.096999999999998,51.188,29.535 +2020-08-05 14:45:00,116.4,31.75,51.188,29.535 +2020-08-05 15:00:00,114.44,33.433,52.976000000000006,29.535 +2020-08-05 15:15:00,114.65,31.269000000000002,52.976000000000006,29.535 +2020-08-05 15:30:00,115.6,29.997,52.976000000000006,29.535 +2020-08-05 15:45:00,115.27,28.464000000000002,52.976000000000006,29.535 +2020-08-05 16:00:00,113.47,30.823,55.463,29.535 +2020-08-05 16:15:00,110.57,30.73,55.463,29.535 +2020-08-05 16:30:00,109.52,30.18,55.463,29.535 +2020-08-05 16:45:00,111.03,27.772,55.463,29.535 +2020-08-05 17:00:00,109.65,31.051,59.435,29.535 +2020-08-05 17:15:00,106.85,31.566,59.435,29.535 +2020-08-05 17:30:00,106.49,30.684,59.435,29.535 +2020-08-05 17:45:00,109.02,30.124000000000002,59.435,29.535 +2020-08-05 18:00:00,109.21,33.019,61.387,29.535 +2020-08-05 18:15:00,107.26,32.824,61.387,29.535 +2020-08-05 18:30:00,107.93,31.18,61.387,29.535 +2020-08-05 18:45:00,105.44,33.734,61.387,29.535 +2020-08-05 19:00:00,102.48,34.996,63.323,29.535 +2020-08-05 19:15:00,99.33,34.006,63.323,29.535 +2020-08-05 19:30:00,98.06,32.913000000000004,63.323,29.535 +2020-08-05 19:45:00,97.58,32.037,63.323,29.535 +2020-08-05 20:00:00,98.82,29.625999999999998,69.083,29.535 +2020-08-05 20:15:00,99.21,29.009,69.083,29.535 +2020-08-05 20:30:00,100.79,29.276,69.083,29.535 +2020-08-05 20:45:00,97.25,29.539,69.083,29.535 +2020-08-05 21:00:00,94.59,28.393,59.957,29.535 +2020-08-05 21:15:00,93.52,29.486,59.957,29.535 +2020-08-05 21:30:00,89.59,29.833000000000002,59.957,29.535 +2020-08-05 21:45:00,86.86,30.335,59.957,29.535 +2020-08-05 22:00:00,81.98,27.763,53.821000000000005,29.535 +2020-08-05 22:15:00,82.56,30.144000000000002,53.821000000000005,29.535 +2020-08-05 22:30:00,80.52,25.849,53.821000000000005,29.535 +2020-08-05 22:45:00,83.39,22.934,53.821000000000005,29.535 +2020-08-05 23:00:00,76.4,20.426,45.458,29.535 +2020-08-05 23:15:00,76.83,19.312,45.458,29.535 +2020-08-05 23:30:00,76.33,18.596,45.458,29.535 +2020-08-05 23:45:00,75.85,17.664,45.458,29.535 +2020-08-06 00:00:00,71.94,17.595,40.36,29.535 +2020-08-06 00:15:00,72.79,18.368,40.36,29.535 +2020-08-06 00:30:00,71.6,17.289,40.36,29.535 +2020-08-06 00:45:00,72.0,17.474,40.36,29.535 +2020-08-06 01:00:00,70.14,17.387999999999998,38.552,29.535 +2020-08-06 01:15:00,71.15,16.648,38.552,29.535 +2020-08-06 01:30:00,70.1,15.435,38.552,29.535 +2020-08-06 01:45:00,69.93,15.404000000000002,38.552,29.535 +2020-08-06 02:00:00,76.01,15.328,36.895,29.535 +2020-08-06 02:15:00,77.91,14.318,36.895,29.535 +2020-08-06 02:30:00,75.22,15.882,36.895,29.535 +2020-08-06 02:45:00,72.63,16.552,36.895,29.535 +2020-08-06 03:00:00,72.82,17.294,36.565,29.535 +2020-08-06 03:15:00,72.3,17.077,36.565,29.535 +2020-08-06 03:30:00,74.58,16.601,36.565,29.535 +2020-08-06 03:45:00,75.09,16.062,36.565,29.535 +2020-08-06 04:00:00,79.6,19.707,37.263000000000005,29.535 +2020-08-06 04:15:00,86.77,24.622,37.263000000000005,29.535 +2020-08-06 04:30:00,88.17,22.183000000000003,37.263000000000005,29.535 +2020-08-06 04:45:00,90.68,22.151,37.263000000000005,29.535 +2020-08-06 05:00:00,90.92,32.816,40.412,29.535 +2020-08-06 05:15:00,94.47,38.146,40.412,29.535 +2020-08-06 05:30:00,102.91,34.119,40.412,29.535 +2020-08-06 05:45:00,107.32,31.916,40.412,29.535 +2020-08-06 06:00:00,110.3,32.035,49.825,29.535 +2020-08-06 06:15:00,109.16,31.708000000000002,49.825,29.535 +2020-08-06 06:30:00,113.88,31.549,49.825,29.535 +2020-08-06 06:45:00,115.25,34.126,49.825,29.535 +2020-08-06 07:00:00,118.34,33.914,61.082,29.535 +2020-08-06 07:15:00,113.87,34.925,61.082,29.535 +2020-08-06 07:30:00,116.05,33.259,61.082,29.535 +2020-08-06 07:45:00,116.6,34.01,61.082,29.535 +2020-08-06 08:00:00,115.41,30.105,53.961999999999996,29.535 +2020-08-06 08:15:00,108.89,32.903,53.961999999999996,29.535 +2020-08-06 08:30:00,114.46,33.955,53.961999999999996,29.535 +2020-08-06 08:45:00,116.44,36.141999999999996,53.961999999999996,29.535 +2020-08-06 09:00:00,117.7,31.888,50.06100000000001,29.535 +2020-08-06 09:15:00,113.69,31.304000000000002,50.06100000000001,29.535 +2020-08-06 09:30:00,109.27,34.489000000000004,50.06100000000001,29.535 +2020-08-06 09:45:00,108.39,37.323,50.06100000000001,29.535 +2020-08-06 10:00:00,109.59,35.102,47.68,29.535 +2020-08-06 10:15:00,109.22,36.260999999999996,47.68,29.535 +2020-08-06 10:30:00,115.31,36.093,47.68,29.535 +2020-08-06 10:45:00,123.63,37.009,47.68,29.535 +2020-08-06 11:00:00,120.81,35.016999999999996,45.93899999999999,29.535 +2020-08-06 11:15:00,120.53,36.128,45.93899999999999,29.535 +2020-08-06 11:30:00,115.4,37.066,45.93899999999999,29.535 +2020-08-06 11:45:00,114.1,37.907,45.93899999999999,29.535 +2020-08-06 12:00:00,108.48,33.357,43.648999999999994,29.535 +2020-08-06 12:15:00,115.79,32.666,43.648999999999994,29.535 +2020-08-06 12:30:00,114.88,31.578000000000003,43.648999999999994,29.535 +2020-08-06 12:45:00,109.1,32.354,43.648999999999994,29.535 +2020-08-06 13:00:00,105.16,31.976999999999997,42.801,29.535 +2020-08-06 13:15:00,109.12,33.035,42.801,29.535 +2020-08-06 13:30:00,109.71,31.41,42.801,29.535 +2020-08-06 13:45:00,108.45,31.504,42.801,29.535 +2020-08-06 14:00:00,105.05,32.884,43.24,29.535 +2020-08-06 14:15:00,106.1,32.052,43.24,29.535 +2020-08-06 14:30:00,113.49,31.171,43.24,29.535 +2020-08-06 14:45:00,112.04,31.83,43.24,29.535 +2020-08-06 15:00:00,106.97,33.484,45.04600000000001,29.535 +2020-08-06 15:15:00,105.8,31.323,45.04600000000001,29.535 +2020-08-06 15:30:00,104.44,30.059,45.04600000000001,29.535 +2020-08-06 15:45:00,107.22,28.526999999999997,45.04600000000001,29.535 +2020-08-06 16:00:00,109.97,30.872,46.568000000000005,29.535 +2020-08-06 16:15:00,108.43,30.785999999999998,46.568000000000005,29.535 +2020-08-06 16:30:00,109.72,30.246,46.568000000000005,29.535 +2020-08-06 16:45:00,114.1,27.866999999999997,46.568000000000005,29.535 +2020-08-06 17:00:00,114.18,31.127,50.618,29.535 +2020-08-06 17:15:00,107.45,31.666,50.618,29.535 +2020-08-06 17:30:00,110.68,30.795,50.618,29.535 +2020-08-06 17:45:00,113.82,30.261,50.618,29.535 +2020-08-06 18:00:00,115.22,33.153,52.806999999999995,29.535 +2020-08-06 18:15:00,116.03,32.963,52.806999999999995,29.535 +2020-08-06 18:30:00,115.79,31.326999999999998,52.806999999999995,29.535 +2020-08-06 18:45:00,111.98,33.882,52.806999999999995,29.535 +2020-08-06 19:00:00,104.96,35.147,53.464,29.535 +2020-08-06 19:15:00,99.09,34.155,53.464,29.535 +2020-08-06 19:30:00,97.96,33.061,53.464,29.535 +2020-08-06 19:45:00,99.34,32.188,53.464,29.535 +2020-08-06 20:00:00,102.66,29.776999999999997,56.753,29.535 +2020-08-06 20:15:00,99.01,29.160999999999998,56.753,29.535 +2020-08-06 20:30:00,98.36,29.416999999999998,56.753,29.535 +2020-08-06 20:45:00,96.53,29.66,56.753,29.535 +2020-08-06 21:00:00,92.34,28.517,52.506,29.535 +2020-08-06 21:15:00,91.64,29.603,52.506,29.535 +2020-08-06 21:30:00,87.55,29.941999999999997,52.506,29.535 +2020-08-06 21:45:00,86.87,30.42,52.506,29.535 +2020-08-06 22:00:00,82.17,27.836,48.163000000000004,29.535 +2020-08-06 22:15:00,83.32,30.206999999999997,48.163000000000004,29.535 +2020-08-06 22:30:00,79.81,25.884,48.163000000000004,29.535 +2020-08-06 22:45:00,79.96,22.967,48.163000000000004,29.535 +2020-08-06 23:00:00,73.69,20.49,42.379,29.535 +2020-08-06 23:15:00,75.01,19.374000000000002,42.379,29.535 +2020-08-06 23:30:00,75.72,18.672,42.379,29.535 +2020-08-06 23:45:00,76.69,17.742,42.379,29.535 +2020-08-07 00:00:00,71.41,16.029,38.505,29.535 +2020-08-07 00:15:00,70.97,16.997,38.505,29.535 +2020-08-07 00:30:00,66.19,16.194000000000003,38.505,29.535 +2020-08-07 00:45:00,68.12,16.788,38.505,29.535 +2020-08-07 01:00:00,69.02,16.339000000000002,37.004,29.535 +2020-08-07 01:15:00,70.25,14.969000000000001,37.004,29.535 +2020-08-07 01:30:00,70.41,14.469000000000001,37.004,29.535 +2020-08-07 01:45:00,70.59,14.186,37.004,29.535 +2020-08-07 02:00:00,76.79,15.011,36.098,29.535 +2020-08-07 02:15:00,79.54,13.975999999999999,36.098,29.535 +2020-08-07 02:30:00,74.87,16.284000000000002,36.098,29.535 +2020-08-07 02:45:00,70.69,16.291,36.098,29.535 +2020-08-07 03:00:00,70.91,17.83,36.561,29.535 +2020-08-07 03:15:00,70.99,16.409000000000002,36.561,29.535 +2020-08-07 03:30:00,73.56,15.71,36.561,29.535 +2020-08-07 03:45:00,75.0,15.999,36.561,29.535 +2020-08-07 04:00:00,77.55,19.767,37.355,29.535 +2020-08-07 04:15:00,85.83,23.194000000000003,37.355,29.535 +2020-08-07 04:30:00,87.24,21.662,37.355,29.535 +2020-08-07 04:45:00,88.67,21.076,37.355,29.535 +2020-08-07 05:00:00,92.85,31.379,40.285,29.535 +2020-08-07 05:15:00,98.87,37.567,40.285,29.535 +2020-08-07 05:30:00,96.26,33.688,40.285,29.535 +2020-08-07 05:45:00,100.51,31.046999999999997,40.285,29.535 +2020-08-07 06:00:00,106.1,31.334,52.378,29.535 +2020-08-07 06:15:00,108.88,31.254,52.378,29.535 +2020-08-07 06:30:00,111.1,31.092,52.378,29.535 +2020-08-07 06:45:00,113.24,33.475,52.378,29.535 +2020-08-07 07:00:00,114.37,33.924,60.891999999999996,29.535 +2020-08-07 07:15:00,113.51,35.764,60.891999999999996,29.535 +2020-08-07 07:30:00,113.78,32.241,60.891999999999996,29.535 +2020-08-07 07:45:00,114.69,32.854,60.891999999999996,29.535 +2020-08-07 08:00:00,115.11,29.781,53.652,29.535 +2020-08-07 08:15:00,119.6,33.239000000000004,53.652,29.535 +2020-08-07 08:30:00,121.84,34.12,53.652,29.535 +2020-08-07 08:45:00,120.12,36.262,53.652,29.535 +2020-08-07 09:00:00,124.38,29.730999999999998,51.456,29.535 +2020-08-07 09:15:00,127.6,30.976999999999997,51.456,29.535 +2020-08-07 09:30:00,130.39,33.506,51.456,29.535 +2020-08-07 09:45:00,126.25,36.711,51.456,29.535 +2020-08-07 10:00:00,120.83,34.423,49.4,29.535 +2020-08-07 10:15:00,127.56,35.29,49.4,29.535 +2020-08-07 10:30:00,127.41,35.665,49.4,29.535 +2020-08-07 10:45:00,126.26,36.516999999999996,49.4,29.535 +2020-08-07 11:00:00,118.94,34.779,48.773,29.535 +2020-08-07 11:15:00,114.75,34.885,48.773,29.535 +2020-08-07 11:30:00,117.42,35.325,48.773,29.535 +2020-08-07 11:45:00,115.42,35.209,48.773,29.535 +2020-08-07 12:00:00,110.13,30.995,46.033,29.535 +2020-08-07 12:15:00,110.2,29.831999999999997,46.033,29.535 +2020-08-07 12:30:00,109.71,28.854,46.033,29.535 +2020-08-07 12:45:00,110.98,28.804000000000002,46.033,29.535 +2020-08-07 13:00:00,108.18,28.916999999999998,44.38399999999999,29.535 +2020-08-07 13:15:00,108.46,30.076999999999998,44.38399999999999,29.535 +2020-08-07 13:30:00,109.25,29.230999999999998,44.38399999999999,29.535 +2020-08-07 13:45:00,111.29,29.642,44.38399999999999,29.535 +2020-08-07 14:00:00,99.65,30.31,43.162,29.535 +2020-08-07 14:15:00,103.35,29.925,43.162,29.535 +2020-08-07 14:30:00,100.75,30.491,43.162,29.535 +2020-08-07 14:45:00,103.99,30.454,43.162,29.535 +2020-08-07 15:00:00,92.04,32.04,44.91,29.535 +2020-08-07 15:15:00,93.49,29.68,44.91,29.535 +2020-08-07 15:30:00,91.17,27.985,44.91,29.535 +2020-08-07 15:45:00,93.1,27.19,44.91,29.535 +2020-08-07 16:00:00,99.66,28.771,47.489,29.535 +2020-08-07 16:15:00,101.19,29.153000000000002,47.489,29.535 +2020-08-07 16:30:00,102.13,28.436999999999998,47.489,29.535 +2020-08-07 16:45:00,100.1,25.329,47.489,29.535 +2020-08-07 17:00:00,107.59,30.236,52.047,29.535 +2020-08-07 17:15:00,107.4,30.662,52.047,29.535 +2020-08-07 17:30:00,106.27,29.985,52.047,29.535 +2020-08-07 17:45:00,100.88,29.333000000000002,52.047,29.535 +2020-08-07 18:00:00,106.56,32.196,53.306000000000004,29.535 +2020-08-07 18:15:00,103.38,31.123,53.306000000000004,29.535 +2020-08-07 18:30:00,105.2,29.368000000000002,53.306000000000004,29.535 +2020-08-07 18:45:00,105.43,32.33,53.306000000000004,29.535 +2020-08-07 19:00:00,100.82,34.372,53.516000000000005,29.535 +2020-08-07 19:15:00,99.6,33.865,53.516000000000005,29.535 +2020-08-07 19:30:00,101.51,32.855,53.516000000000005,29.535 +2020-08-07 19:45:00,97.37,31.035,53.516000000000005,29.535 +2020-08-07 20:00:00,96.5,28.47,57.88,29.535 +2020-08-07 20:15:00,97.87,28.62,57.88,29.535 +2020-08-07 20:30:00,97.05,28.397,57.88,29.535 +2020-08-07 20:45:00,91.55,27.805,57.88,29.535 +2020-08-07 21:00:00,87.14,27.892,53.32,29.535 +2020-08-07 21:15:00,88.39,30.548000000000002,53.32,29.535 +2020-08-07 21:30:00,86.76,30.733,53.32,29.535 +2020-08-07 21:45:00,86.09,31.351,53.32,29.535 +2020-08-07 22:00:00,78.09,28.561,48.074,29.535 +2020-08-07 22:15:00,76.09,30.691,48.074,29.535 +2020-08-07 22:30:00,77.97,30.533,48.074,29.535 +2020-08-07 22:45:00,79.31,28.241999999999997,48.074,29.535 +2020-08-07 23:00:00,74.19,27.438000000000002,41.306999999999995,29.535 +2020-08-07 23:15:00,68.5,24.93,41.306999999999995,29.535 +2020-08-07 23:30:00,66.0,22.58,41.306999999999995,29.535 +2020-08-07 23:45:00,65.38,21.570999999999998,41.306999999999995,29.535 +2020-08-08 00:00:00,66.74,17.496,40.227,29.423000000000002 +2020-08-08 00:15:00,69.29,18.078,40.227,29.423000000000002 +2020-08-08 00:30:00,68.8,16.746,40.227,29.423000000000002 +2020-08-08 00:45:00,63.95,16.6,40.227,29.423000000000002 +2020-08-08 01:00:00,66.72,16.363,36.303000000000004,29.423000000000002 +2020-08-08 01:15:00,67.67,15.626,36.303000000000004,29.423000000000002 +2020-08-08 01:30:00,68.13,14.392999999999999,36.303000000000004,29.423000000000002 +2020-08-08 01:45:00,65.03,15.319,36.303000000000004,29.423000000000002 +2020-08-08 02:00:00,64.36,15.154000000000002,33.849000000000004,29.423000000000002 +2020-08-08 02:15:00,66.91,13.402000000000001,33.849000000000004,29.423000000000002 +2020-08-08 02:30:00,66.76,14.997,33.849000000000004,29.423000000000002 +2020-08-08 02:45:00,63.89,15.75,33.849000000000004,29.423000000000002 +2020-08-08 03:00:00,58.71,15.923,33.149,29.423000000000002 +2020-08-08 03:15:00,64.73,13.921,33.149,29.423000000000002 +2020-08-08 03:30:00,67.22,13.64,33.149,29.423000000000002 +2020-08-08 03:45:00,67.03,15.345,33.149,29.423000000000002 +2020-08-08 04:00:00,65.26,17.557000000000002,32.501,29.423000000000002 +2020-08-08 04:15:00,60.78,20.284000000000002,32.501,29.423000000000002 +2020-08-08 04:30:00,63.06,17.284000000000002,32.501,29.423000000000002 +2020-08-08 04:45:00,63.74,16.992,32.501,29.423000000000002 +2020-08-08 05:00:00,68.08,20.31,31.648000000000003,29.423000000000002 +2020-08-08 05:15:00,70.17,17.18,31.648000000000003,29.423000000000002 +2020-08-08 05:30:00,69.63,14.745999999999999,31.648000000000003,29.423000000000002 +2020-08-08 05:45:00,69.72,16.029,31.648000000000003,29.423000000000002 +2020-08-08 06:00:00,74.01,25.884,32.552,29.423000000000002 +2020-08-08 06:15:00,76.69,32.289,32.552,29.423000000000002 +2020-08-08 06:30:00,74.92,29.739,32.552,29.423000000000002 +2020-08-08 06:45:00,75.68,29.601,32.552,29.423000000000002 +2020-08-08 07:00:00,78.07,29.214000000000002,35.181999999999995,29.423000000000002 +2020-08-08 07:15:00,77.53,29.912,35.181999999999995,29.423000000000002 +2020-08-08 07:30:00,79.7,27.668000000000003,35.181999999999995,29.423000000000002 +2020-08-08 07:45:00,85.61,28.895,35.181999999999995,29.423000000000002 +2020-08-08 08:00:00,89.39,26.308000000000003,40.35,29.423000000000002 +2020-08-08 08:15:00,89.11,29.252,40.35,29.423000000000002 +2020-08-08 08:30:00,80.69,29.98,40.35,29.423000000000002 +2020-08-08 08:45:00,79.94,32.78,40.35,29.423000000000002 +2020-08-08 09:00:00,81.67,29.186,42.292,29.423000000000002 +2020-08-08 09:15:00,85.15,30.836,42.292,29.423000000000002 +2020-08-08 09:30:00,86.84,33.758,42.292,29.423000000000002 +2020-08-08 09:45:00,85.72,36.493,42.292,29.423000000000002 +2020-08-08 10:00:00,78.03,34.819,40.084,29.423000000000002 +2020-08-08 10:15:00,80.44,36.003,40.084,29.423000000000002 +2020-08-08 10:30:00,87.73,36.022,40.084,29.423000000000002 +2020-08-08 10:45:00,76.01,36.408,40.084,29.423000000000002 +2020-08-08 11:00:00,75.02,34.505,36.966,29.423000000000002 +2020-08-08 11:15:00,71.3,35.515,36.966,29.423000000000002 +2020-08-08 11:30:00,71.44,36.321,36.966,29.423000000000002 +2020-08-08 11:45:00,83.52,36.929,36.966,29.423000000000002 +2020-08-08 12:00:00,78.82,33.196,35.19,29.423000000000002 +2020-08-08 12:15:00,77.58,32.803000000000004,35.19,29.423000000000002 +2020-08-08 12:30:00,68.36,31.677,35.19,29.423000000000002 +2020-08-08 12:45:00,71.92,32.397,35.19,29.423000000000002 +2020-08-08 13:00:00,65.67,31.694000000000003,32.277,29.423000000000002 +2020-08-08 13:15:00,71.96,32.49,32.277,29.423000000000002 +2020-08-08 13:30:00,63.1,31.866,32.277,29.423000000000002 +2020-08-08 13:45:00,64.32,30.926,32.277,29.423000000000002 +2020-08-08 14:00:00,72.95,31.502,31.436999999999998,29.423000000000002 +2020-08-08 14:15:00,65.23,29.944000000000003,31.436999999999998,29.423000000000002 +2020-08-08 14:30:00,63.29,30.256,31.436999999999998,29.423000000000002 +2020-08-08 14:45:00,69.43,30.671999999999997,31.436999999999998,29.423000000000002 +2020-08-08 15:00:00,64.98,32.576,33.493,29.423000000000002 +2020-08-08 15:15:00,62.48,30.843000000000004,33.493,29.423000000000002 +2020-08-08 15:30:00,61.84,29.228,33.493,29.423000000000002 +2020-08-08 15:45:00,64.18,27.535999999999998,33.493,29.423000000000002 +2020-08-08 16:00:00,64.57,31.239,36.593,29.423000000000002 +2020-08-08 16:15:00,66.55,30.691,36.593,29.423000000000002 +2020-08-08 16:30:00,72.26,30.206999999999997,36.593,29.423000000000002 +2020-08-08 16:45:00,72.92,27.033,36.593,29.423000000000002 +2020-08-08 17:00:00,74.06,30.78,42.049,29.423000000000002 +2020-08-08 17:15:00,73.2,29.022,42.049,29.423000000000002 +2020-08-08 17:30:00,75.04,28.235,42.049,29.423000000000002 +2020-08-08 17:45:00,77.03,28.121,42.049,29.423000000000002 +2020-08-08 18:00:00,78.39,32.303000000000004,43.755,29.423000000000002 +2020-08-08 18:15:00,76.69,32.72,43.755,29.423000000000002 +2020-08-08 18:30:00,76.27,32.189,43.755,29.423000000000002 +2020-08-08 18:45:00,76.64,32.038000000000004,43.755,29.423000000000002 +2020-08-08 19:00:00,75.57,32.424,44.492,29.423000000000002 +2020-08-08 19:15:00,72.67,30.984,44.492,29.423000000000002 +2020-08-08 19:30:00,72.39,30.662,44.492,29.423000000000002 +2020-08-08 19:45:00,73.31,30.538,44.492,29.423000000000002 +2020-08-08 20:00:00,75.0,28.638,40.896,29.423000000000002 +2020-08-08 20:15:00,74.88,28.051,40.896,29.423000000000002 +2020-08-08 20:30:00,74.76,27.0,40.896,29.423000000000002 +2020-08-08 20:45:00,74.69,28.18,40.896,29.423000000000002 +2020-08-08 21:00:00,71.99,26.721999999999998,39.056,29.423000000000002 +2020-08-08 21:15:00,71.04,29.015,39.056,29.423000000000002 +2020-08-08 21:30:00,67.87,29.302,39.056,29.423000000000002 +2020-08-08 21:45:00,67.58,29.401,39.056,29.423000000000002 +2020-08-08 22:00:00,63.3,26.502,38.478,29.423000000000002 +2020-08-08 22:15:00,64.97,28.721,38.478,29.423000000000002 +2020-08-08 22:30:00,62.91,27.836,38.478,29.423000000000002 +2020-08-08 22:45:00,62.35,25.795,38.478,29.423000000000002 +2020-08-08 23:00:00,57.64,24.291,32.953,29.423000000000002 +2020-08-08 23:15:00,57.7,22.261999999999997,32.953,29.423000000000002 +2020-08-08 23:30:00,58.28,22.333000000000002,32.953,29.423000000000002 +2020-08-08 23:45:00,58.51,21.79,32.953,29.423000000000002 +2020-08-09 00:00:00,54.08,18.893,28.584,29.423000000000002 +2020-08-09 00:15:00,54.76,18.393,28.584,29.423000000000002 +2020-08-09 00:30:00,54.22,16.949,28.584,29.423000000000002 +2020-08-09 00:45:00,55.07,16.660999999999998,28.584,29.423000000000002 +2020-08-09 01:00:00,51.21,16.673,26.419,29.423000000000002 +2020-08-09 01:15:00,53.23,15.76,26.419,29.423000000000002 +2020-08-09 01:30:00,50.21,14.374,26.419,29.423000000000002 +2020-08-09 01:45:00,52.88,14.954,26.419,29.423000000000002 +2020-08-09 02:00:00,51.33,14.915,25.335,29.423000000000002 +2020-08-09 02:15:00,52.26,13.91,25.335,29.423000000000002 +2020-08-09 02:30:00,51.66,15.61,25.335,29.423000000000002 +2020-08-09 02:45:00,51.22,16.055999999999997,25.335,29.423000000000002 +2020-08-09 03:00:00,51.33,16.819000000000003,24.805,29.423000000000002 +2020-08-09 03:15:00,52.88,15.114,24.805,29.423000000000002 +2020-08-09 03:30:00,52.66,14.052,24.805,29.423000000000002 +2020-08-09 03:45:00,53.13,15.005999999999998,24.805,29.423000000000002 +2020-08-09 04:00:00,53.45,17.209,25.772,29.423000000000002 +2020-08-09 04:15:00,54.38,19.586,25.772,29.423000000000002 +2020-08-09 04:30:00,52.71,17.863,25.772,29.423000000000002 +2020-08-09 04:45:00,52.22,17.111,25.772,29.423000000000002 +2020-08-09 05:00:00,51.09,20.83,25.971999999999998,29.423000000000002 +2020-08-09 05:15:00,51.86,17.137999999999998,25.971999999999998,29.423000000000002 +2020-08-09 05:30:00,51.9,14.347999999999999,25.971999999999998,29.423000000000002 +2020-08-09 05:45:00,52.64,15.338,25.971999999999998,29.423000000000002 +2020-08-09 06:00:00,52.67,22.945,26.026,29.423000000000002 +2020-08-09 06:15:00,53.6,30.255,26.026,29.423000000000002 +2020-08-09 06:30:00,54.27,27.113000000000003,26.026,29.423000000000002 +2020-08-09 06:45:00,55.49,26.105999999999998,26.026,29.423000000000002 +2020-08-09 07:00:00,57.23,25.877,27.396,29.423000000000002 +2020-08-09 07:15:00,57.15,25.078000000000003,27.396,29.423000000000002 +2020-08-09 07:30:00,56.53,24.169,27.396,29.423000000000002 +2020-08-09 07:45:00,56.43,25.428,27.396,29.423000000000002 +2020-08-09 08:00:00,56.12,23.463,30.791999999999998,29.423000000000002 +2020-08-09 08:15:00,57.24,27.535,30.791999999999998,29.423000000000002 +2020-08-09 08:30:00,56.64,28.962,30.791999999999998,29.423000000000002 +2020-08-09 08:45:00,57.8,31.506999999999998,30.791999999999998,29.423000000000002 +2020-08-09 09:00:00,57.9,27.857,32.482,29.423000000000002 +2020-08-09 09:15:00,57.48,28.969,32.482,29.423000000000002 +2020-08-09 09:30:00,53.71,32.31,32.482,29.423000000000002 +2020-08-09 09:45:00,57.49,36.016999999999996,32.482,29.423000000000002 +2020-08-09 10:00:00,58.06,34.648,31.951,29.423000000000002 +2020-08-09 10:15:00,59.54,35.896,31.951,29.423000000000002 +2020-08-09 10:30:00,61.42,36.068000000000005,31.951,29.423000000000002 +2020-08-09 10:45:00,60.84,37.628,31.951,29.423000000000002 +2020-08-09 11:00:00,60.37,35.299,33.619,29.423000000000002 +2020-08-09 11:15:00,61.49,35.853,33.619,29.423000000000002 +2020-08-09 11:30:00,57.13,37.247,33.619,29.423000000000002 +2020-08-09 11:45:00,57.85,38.035,33.619,29.423000000000002 +2020-08-09 12:00:00,53.65,35.400999999999996,30.975,29.423000000000002 +2020-08-09 12:15:00,52.86,34.196999999999996,30.975,29.423000000000002 +2020-08-09 12:30:00,50.62,33.433,30.975,29.423000000000002 +2020-08-09 12:45:00,50.63,33.611999999999995,30.975,29.423000000000002 +2020-08-09 13:00:00,49.19,32.663000000000004,27.956999999999997,29.423000000000002 +2020-08-09 13:15:00,48.43,32.585,27.956999999999997,29.423000000000002 +2020-08-09 13:30:00,48.61,30.886999999999997,27.956999999999997,29.423000000000002 +2020-08-09 13:45:00,49.71,31.119,27.956999999999997,29.423000000000002 +2020-08-09 14:00:00,49.4,32.830999999999996,25.555999999999997,29.423000000000002 +2020-08-09 14:15:00,50.46,31.583000000000002,25.555999999999997,29.423000000000002 +2020-08-09 14:30:00,49.99,30.576,25.555999999999997,29.423000000000002 +2020-08-09 14:45:00,51.47,29.987,25.555999999999997,29.423000000000002 +2020-08-09 15:00:00,52.05,32.154,26.271,29.423000000000002 +2020-08-09 15:15:00,51.23,29.56,26.271,29.423000000000002 +2020-08-09 15:30:00,52.35,27.721999999999998,26.271,29.423000000000002 +2020-08-09 15:45:00,54.8,26.245,26.271,29.423000000000002 +2020-08-09 16:00:00,58.39,28.193,30.369,29.423000000000002 +2020-08-09 16:15:00,59.37,27.954,30.369,29.423000000000002 +2020-08-09 16:30:00,61.94,28.434,30.369,29.423000000000002 +2020-08-09 16:45:00,64.25,25.340999999999998,30.369,29.423000000000002 +2020-08-09 17:00:00,67.55,29.435,38.787,29.423000000000002 +2020-08-09 17:15:00,69.77,29.199,38.787,29.423000000000002 +2020-08-09 17:30:00,74.47,29.183000000000003,38.787,29.423000000000002 +2020-08-09 17:45:00,73.06,29.31,38.787,29.423000000000002 +2020-08-09 18:00:00,76.18,34.126999999999995,41.886,29.423000000000002 +2020-08-09 18:15:00,75.37,34.056,41.886,29.423000000000002 +2020-08-09 18:30:00,78.04,33.457,41.886,29.423000000000002 +2020-08-09 18:45:00,75.37,33.274,41.886,29.423000000000002 +2020-08-09 19:00:00,77.91,35.796,42.91,29.423000000000002 +2020-08-09 19:15:00,76.23,33.253,42.91,29.423000000000002 +2020-08-09 19:30:00,76.04,32.707,42.91,29.423000000000002 +2020-08-09 19:45:00,78.63,32.049,42.91,29.423000000000002 +2020-08-09 20:00:00,79.32,30.315,42.148999999999994,29.423000000000002 +2020-08-09 20:15:00,78.88,29.53,42.148999999999994,29.423000000000002 +2020-08-09 20:30:00,78.51,29.153000000000002,42.148999999999994,29.423000000000002 +2020-08-09 20:45:00,77.97,28.791,42.148999999999994,29.423000000000002 +2020-08-09 21:00:00,76.29,27.416,40.955999999999996,29.423000000000002 +2020-08-09 21:15:00,76.26,29.456999999999997,40.955999999999996,29.423000000000002 +2020-08-09 21:30:00,74.21,29.101,40.955999999999996,29.423000000000002 +2020-08-09 21:45:00,72.91,29.506,40.955999999999996,29.423000000000002 +2020-08-09 22:00:00,68.37,28.61,39.873000000000005,29.423000000000002 +2020-08-09 22:15:00,70.97,29.274,39.873000000000005,29.423000000000002 +2020-08-09 22:30:00,68.0,28.081,39.873000000000005,29.423000000000002 +2020-08-09 22:45:00,68.68,24.886999999999997,39.873000000000005,29.423000000000002 +2020-08-09 23:00:00,64.15,23.305999999999997,35.510999999999996,29.423000000000002 +2020-08-09 23:15:00,65.15,22.361,35.510999999999996,29.423000000000002 +2020-08-09 23:30:00,65.42,21.884,35.510999999999996,29.423000000000002 +2020-08-09 23:45:00,64.78,21.439,35.510999999999996,29.423000000000002 +2020-08-10 00:00:00,64.59,20.146,33.475,29.535 +2020-08-10 00:15:00,67.7,20.188,33.475,29.535 +2020-08-10 00:30:00,67.92,18.373,33.475,29.535 +2020-08-10 00:45:00,66.94,17.753,33.475,29.535 +2020-08-10 01:00:00,60.75,18.12,33.111,29.535 +2020-08-10 01:15:00,62.05,17.266,33.111,29.535 +2020-08-10 01:30:00,61.91,16.230999999999998,33.111,29.535 +2020-08-10 01:45:00,61.67,16.708,33.111,29.535 +2020-08-10 02:00:00,62.03,17.11,32.358000000000004,29.535 +2020-08-10 02:15:00,66.27,15.054,32.358000000000004,29.535 +2020-08-10 02:30:00,69.88,16.866,32.358000000000004,29.535 +2020-08-10 02:45:00,70.55,17.215,32.358000000000004,29.535 +2020-08-10 03:00:00,67.73,18.362000000000002,30.779,29.535 +2020-08-10 03:15:00,64.33,17.243,30.779,29.535 +2020-08-10 03:30:00,68.6,16.858,30.779,29.535 +2020-08-10 03:45:00,68.36,17.41,30.779,29.535 +2020-08-10 04:00:00,72.46,22.284000000000002,31.416,29.535 +2020-08-10 04:15:00,75.4,27.168000000000003,31.416,29.535 +2020-08-10 04:30:00,77.15,24.838,31.416,29.535 +2020-08-10 04:45:00,82.65,24.43,31.416,29.535 +2020-08-10 05:00:00,87.89,34.043,37.221,29.535 +2020-08-10 05:15:00,93.43,38.881,37.221,29.535 +2020-08-10 05:30:00,98.76,34.632,37.221,29.535 +2020-08-10 05:45:00,106.75,33.045,37.221,29.535 +2020-08-10 06:00:00,109.61,31.978,51.891000000000005,29.535 +2020-08-10 06:15:00,110.17,31.636,51.891000000000005,29.535 +2020-08-10 06:30:00,106.16,31.749000000000002,51.891000000000005,29.535 +2020-08-10 06:45:00,105.44,35.194,51.891000000000005,29.535 +2020-08-10 07:00:00,112.69,34.809,62.282,29.535 +2020-08-10 07:15:00,114.13,36.123000000000005,62.282,29.535 +2020-08-10 07:30:00,114.44,34.466,62.282,29.535 +2020-08-10 07:45:00,111.46,36.219,62.282,29.535 +2020-08-10 08:00:00,109.2,32.409,54.102,29.535 +2020-08-10 08:15:00,114.03,35.476,54.102,29.535 +2020-08-10 08:30:00,115.94,36.306999999999995,54.102,29.535 +2020-08-10 08:45:00,113.48,39.326,54.102,29.535 +2020-08-10 09:00:00,109.67,34.673,50.917,29.535 +2020-08-10 09:15:00,113.16,34.354,50.917,29.535 +2020-08-10 09:30:00,114.97,36.866,50.917,29.535 +2020-08-10 09:45:00,114.23,38.284,50.917,29.535 +2020-08-10 10:00:00,110.17,37.372,49.718999999999994,29.535 +2020-08-10 10:15:00,112.96,38.489000000000004,49.718999999999994,29.535 +2020-08-10 10:30:00,113.44,38.289,49.718999999999994,29.535 +2020-08-10 10:45:00,112.68,38.208,49.718999999999994,29.535 +2020-08-10 11:00:00,106.81,36.395,49.833999999999996,29.535 +2020-08-10 11:15:00,101.75,37.046,49.833999999999996,29.535 +2020-08-10 11:30:00,108.76,38.986,49.833999999999996,29.535 +2020-08-10 11:45:00,115.96,40.294000000000004,49.833999999999996,29.535 +2020-08-10 12:00:00,116.04,35.781,47.832,29.535 +2020-08-10 12:15:00,108.93,34.679,47.832,29.535 +2020-08-10 12:30:00,105.17,32.842,47.832,29.535 +2020-08-10 12:45:00,109.13,32.858000000000004,47.832,29.535 +2020-08-10 13:00:00,105.35,32.725,48.03,29.535 +2020-08-10 13:15:00,106.96,31.901999999999997,48.03,29.535 +2020-08-10 13:30:00,112.55,30.421999999999997,48.03,29.535 +2020-08-10 13:45:00,110.89,31.543000000000003,48.03,29.535 +2020-08-10 14:00:00,108.24,32.449,48.157,29.535 +2020-08-10 14:15:00,116.67,31.851,48.157,29.535 +2020-08-10 14:30:00,109.93,30.743000000000002,48.157,29.535 +2020-08-10 14:45:00,100.75,32.195,48.157,29.535 +2020-08-10 15:00:00,102.13,33.843,48.897,29.535 +2020-08-10 15:15:00,101.58,30.81,48.897,29.535 +2020-08-10 15:30:00,103.82,29.811999999999998,48.897,29.535 +2020-08-10 15:45:00,112.32,27.903000000000002,48.897,29.535 +2020-08-10 16:00:00,107.56,30.926,51.446000000000005,29.535 +2020-08-10 16:15:00,110.02,30.826,51.446000000000005,29.535 +2020-08-10 16:30:00,111.09,30.749000000000002,51.446000000000005,29.535 +2020-08-10 16:45:00,111.76,27.815,51.446000000000005,29.535 +2020-08-10 17:00:00,116.67,30.83,57.507,29.535 +2020-08-10 17:15:00,115.62,31.066999999999997,57.507,29.535 +2020-08-10 17:30:00,119.59,30.733,57.507,29.535 +2020-08-10 17:45:00,117.62,30.63,57.507,29.535 +2020-08-10 18:00:00,112.36,34.413000000000004,57.896,29.535 +2020-08-10 18:15:00,111.68,32.756,57.896,29.535 +2020-08-10 18:30:00,111.01,31.386,57.896,29.535 +2020-08-10 18:45:00,118.2,34.199,57.896,29.535 +2020-08-10 19:00:00,115.05,36.61,57.891999999999996,29.535 +2020-08-10 19:15:00,108.14,35.417,57.891999999999996,29.535 +2020-08-10 19:30:00,100.95,34.482,57.891999999999996,29.535 +2020-08-10 19:45:00,107.15,33.301,57.891999999999996,29.535 +2020-08-10 20:00:00,107.38,30.524,64.57300000000001,29.535 +2020-08-10 20:15:00,106.34,31.261,64.57300000000001,29.535 +2020-08-10 20:30:00,101.37,31.505,64.57300000000001,29.535 +2020-08-10 20:45:00,96.58,31.21,64.57300000000001,29.535 +2020-08-10 21:00:00,92.54,29.186,59.431999999999995,29.535 +2020-08-10 21:15:00,98.18,31.721999999999998,59.431999999999995,29.535 +2020-08-10 21:30:00,94.61,31.811,59.431999999999995,29.535 +2020-08-10 21:45:00,89.3,32.016,59.431999999999995,29.535 +2020-08-10 22:00:00,85.65,29.331999999999997,51.519,29.535 +2020-08-10 22:15:00,83.02,31.964000000000002,51.519,29.535 +2020-08-10 22:30:00,84.02,27.245,51.519,29.535 +2020-08-10 22:45:00,86.92,24.348000000000003,51.519,29.535 +2020-08-10 23:00:00,83.27,22.737,44.501000000000005,29.535 +2020-08-10 23:15:00,79.13,20.052,44.501000000000005,29.535 +2020-08-10 23:30:00,77.81,19.378,44.501000000000005,29.535 +2020-08-10 23:45:00,76.11,18.256,44.501000000000005,29.535 +2020-08-11 00:00:00,80.82,18.12,44.522,29.535 +2020-08-11 00:15:00,81.42,18.89,44.522,29.535 +2020-08-11 00:30:00,79.26,17.834,44.522,29.535 +2020-08-11 00:45:00,77.23,18.055999999999997,44.522,29.535 +2020-08-11 01:00:00,77.88,17.902,41.441,29.535 +2020-08-11 01:15:00,81.19,17.195,41.441,29.535 +2020-08-11 01:30:00,80.87,16.033,41.441,29.535 +2020-08-11 01:45:00,77.52,15.99,41.441,29.535 +2020-08-11 02:00:00,76.14,15.933,40.203,29.535 +2020-08-11 02:15:00,80.33,15.019,40.203,29.535 +2020-08-11 02:30:00,80.22,16.476,40.203,29.535 +2020-08-11 02:45:00,76.52,17.139,40.203,29.535 +2020-08-11 03:00:00,76.12,17.828,39.536,29.535 +2020-08-11 03:15:00,81.42,17.687,39.536,29.535 +2020-08-11 03:30:00,82.68,17.243,39.536,29.535 +2020-08-11 03:45:00,81.38,16.709,39.536,29.535 +2020-08-11 04:00:00,84.21,20.386,40.759,29.535 +2020-08-11 04:15:00,83.25,25.311,40.759,29.535 +2020-08-11 04:30:00,87.0,22.891,40.759,29.535 +2020-08-11 04:45:00,96.1,22.866999999999997,40.759,29.535 +2020-08-11 05:00:00,103.35,33.692,43.623999999999995,29.535 +2020-08-11 05:15:00,104.96,39.169000000000004,43.623999999999995,29.535 +2020-08-11 05:30:00,101.93,35.208,43.623999999999995,29.535 +2020-08-11 05:45:00,103.66,32.900999999999996,43.623999999999995,29.535 +2020-08-11 06:00:00,107.81,32.9,52.684,29.535 +2020-08-11 06:15:00,115.41,32.653,52.684,29.535 +2020-08-11 06:30:00,119.45,32.504,52.684,29.535 +2020-08-11 06:45:00,121.63,35.13,52.684,29.535 +2020-08-11 07:00:00,117.33,34.907,62.676,29.535 +2020-08-11 07:15:00,117.63,36.001,62.676,29.535 +2020-08-11 07:30:00,120.47,34.424,62.676,29.535 +2020-08-11 07:45:00,121.97,35.217,62.676,29.535 +2020-08-11 08:00:00,118.67,31.328000000000003,56.161,29.535 +2020-08-11 08:15:00,115.82,34.021,56.161,29.535 +2020-08-11 08:30:00,109.61,35.024,56.161,29.535 +2020-08-11 08:45:00,108.39,37.145,56.161,29.535 +2020-08-11 09:00:00,118.63,32.913000000000004,52.132,29.535 +2020-08-11 09:15:00,124.0,32.294000000000004,52.132,29.535 +2020-08-11 09:30:00,120.89,35.407,52.132,29.535 +2020-08-11 09:45:00,113.74,38.154,52.132,29.535 +2020-08-11 10:00:00,115.04,35.943000000000005,51.032,29.535 +2020-08-11 10:15:00,111.74,37.010999999999996,51.032,29.535 +2020-08-11 10:30:00,117.81,36.813,51.032,29.535 +2020-08-11 10:45:00,120.03,37.7,51.032,29.535 +2020-08-11 11:00:00,116.21,35.745,51.085,29.535 +2020-08-11 11:15:00,109.85,36.827,51.085,29.535 +2020-08-11 11:30:00,112.41,37.735,51.085,29.535 +2020-08-11 11:45:00,117.13,38.514,51.085,29.535 +2020-08-11 12:00:00,121.63,33.95,49.049,29.535 +2020-08-11 12:15:00,122.61,33.218,49.049,29.535 +2020-08-11 12:30:00,116.41,32.172,49.049,29.535 +2020-08-11 12:45:00,106.88,32.898,49.049,29.535 +2020-08-11 13:00:00,103.94,32.428000000000004,49.722,29.535 +2020-08-11 13:15:00,106.12,33.428000000000004,49.722,29.535 +2020-08-11 13:30:00,107.33,31.793000000000003,49.722,29.535 +2020-08-11 13:45:00,105.9,31.94,49.722,29.535 +2020-08-11 14:00:00,104.55,33.245,49.565,29.535 +2020-08-11 14:15:00,111.12,32.436,49.565,29.535 +2020-08-11 14:30:00,106.63,31.599,49.565,29.535 +2020-08-11 14:45:00,112.83,32.278,49.565,29.535 +2020-08-11 15:00:00,106.44,33.777,51.108999999999995,29.535 +2020-08-11 15:15:00,107.53,31.633000000000003,51.108999999999995,29.535 +2020-08-11 15:30:00,111.68,30.412,51.108999999999995,29.535 +2020-08-11 15:45:00,112.97,28.89,51.108999999999995,29.535 +2020-08-11 16:00:00,110.35,31.155,52.725,29.535 +2020-08-11 16:15:00,106.75,31.103,52.725,29.535 +2020-08-11 16:30:00,107.28,30.604,52.725,29.535 +2020-08-11 16:45:00,113.29,28.383000000000003,52.725,29.535 +2020-08-11 17:00:00,120.26,31.531999999999996,58.031000000000006,29.535 +2020-08-11 17:15:00,116.61,32.205,58.031000000000006,29.535 +2020-08-11 17:30:00,113.1,31.391,58.031000000000006,29.535 +2020-08-11 17:45:00,115.09,31.000999999999998,58.031000000000006,29.535 +2020-08-11 18:00:00,111.56,33.868,58.338,29.535 +2020-08-11 18:15:00,114.45,33.714,58.338,29.535 +2020-08-11 18:30:00,115.07,32.123000000000005,58.338,29.535 +2020-08-11 18:45:00,115.29,34.679,58.338,29.535 +2020-08-11 19:00:00,107.15,35.964,58.464,29.535 +2020-08-11 19:15:00,108.56,34.964,58.464,29.535 +2020-08-11 19:30:00,109.56,33.873000000000005,58.464,29.535 +2020-08-11 19:45:00,111.07,33.014,58.464,29.535 +2020-08-11 20:00:00,104.95,30.608,63.708,29.535 +2020-08-11 20:15:00,102.1,30.0,63.708,29.535 +2020-08-11 20:30:00,97.33,30.191999999999997,63.708,29.535 +2020-08-11 20:45:00,97.81,30.324,63.708,29.535 +2020-08-11 21:00:00,95.02,29.195,57.06399999999999,29.535 +2020-08-11 21:15:00,98.79,30.244,57.06399999999999,29.535 +2020-08-11 21:30:00,96.16,30.55,57.06399999999999,29.535 +2020-08-11 21:45:00,94.75,30.9,57.06399999999999,29.535 +2020-08-11 22:00:00,85.73,28.247,52.831,29.535 +2020-08-11 22:15:00,90.12,30.564,52.831,29.535 +2020-08-11 22:30:00,87.77,26.09,52.831,29.535 +2020-08-11 22:45:00,86.92,23.166999999999998,52.831,29.535 +2020-08-11 23:00:00,83.18,20.858,44.717,29.535 +2020-08-11 23:15:00,84.73,19.727,44.717,29.535 +2020-08-11 23:30:00,83.67,19.087,44.717,29.535 +2020-08-11 23:45:00,76.9,18.173,44.717,29.535 +2020-08-12 00:00:00,72.5,18.233,41.263000000000005,29.535 +2020-08-12 00:15:00,78.82,19.003,41.263000000000005,29.535 +2020-08-12 00:30:00,80.76,17.951,41.263000000000005,29.535 +2020-08-12 00:45:00,81.39,18.18,41.263000000000005,29.535 +2020-08-12 01:00:00,78.93,18.011,38.448,29.535 +2020-08-12 01:15:00,81.15,17.312,38.448,29.535 +2020-08-12 01:30:00,80.17,16.162,38.448,29.535 +2020-08-12 01:45:00,77.33,16.117,38.448,29.535 +2020-08-12 02:00:00,73.77,16.063,36.471,29.535 +2020-08-12 02:15:00,73.65,15.169,36.471,29.535 +2020-08-12 02:30:00,80.17,16.605,36.471,29.535 +2020-08-12 02:45:00,80.73,17.266,36.471,29.535 +2020-08-12 03:00:00,81.84,17.944000000000003,36.042,29.535 +2020-08-12 03:15:00,75.9,17.819000000000003,36.042,29.535 +2020-08-12 03:30:00,82.63,17.381,36.042,29.535 +2020-08-12 03:45:00,85.64,16.847,36.042,29.535 +2020-08-12 04:00:00,87.96,20.533,36.705,29.535 +2020-08-12 04:15:00,83.3,25.464000000000002,36.705,29.535 +2020-08-12 04:30:00,94.27,23.048000000000002,36.705,29.535 +2020-08-12 04:45:00,96.6,23.026,36.705,29.535 +2020-08-12 05:00:00,101.67,33.89,39.716,29.535 +2020-08-12 05:15:00,102.94,39.406,39.716,29.535 +2020-08-12 05:30:00,106.43,35.455,39.716,29.535 +2020-08-12 05:45:00,111.7,33.123000000000005,39.716,29.535 +2020-08-12 06:00:00,115.63,33.095,52.756,29.535 +2020-08-12 06:15:00,114.21,32.867,52.756,29.535 +2020-08-12 06:30:00,115.31,32.718,52.756,29.535 +2020-08-12 06:45:00,119.39,35.351,52.756,29.535 +2020-08-12 07:00:00,120.77,35.128,65.977,29.535 +2020-08-12 07:15:00,115.6,36.236999999999995,65.977,29.535 +2020-08-12 07:30:00,114.05,34.679,65.977,29.535 +2020-08-12 07:45:00,117.2,35.479,65.977,29.535 +2020-08-12 08:00:00,118.01,31.593000000000004,57.927,29.535 +2020-08-12 08:15:00,113.31,34.262,57.927,29.535 +2020-08-12 08:30:00,111.75,35.255,57.927,29.535 +2020-08-12 08:45:00,110.43,37.361999999999995,57.927,29.535 +2020-08-12 09:00:00,116.17,33.135,54.86,29.535 +2020-08-12 09:15:00,116.01,32.508,54.86,29.535 +2020-08-12 09:30:00,116.37,35.607,54.86,29.535 +2020-08-12 09:45:00,112.57,38.335,54.86,29.535 +2020-08-12 10:00:00,113.53,36.125,52.818000000000005,29.535 +2020-08-12 10:15:00,118.87,37.175,52.818000000000005,29.535 +2020-08-12 10:30:00,114.46,36.969,52.818000000000005,29.535 +2020-08-12 10:45:00,110.49,37.849000000000004,52.818000000000005,29.535 +2020-08-12 11:00:00,116.41,35.904,52.937,29.535 +2020-08-12 11:15:00,112.99,36.979,52.937,29.535 +2020-08-12 11:30:00,106.78,37.882,52.937,29.535 +2020-08-12 11:45:00,114.37,38.647,52.937,29.535 +2020-08-12 12:00:00,116.44,34.079,50.826,29.535 +2020-08-12 12:15:00,114.58,33.338,50.826,29.535 +2020-08-12 12:30:00,119.4,32.302,50.826,29.535 +2020-08-12 12:45:00,114.81,33.016999999999996,50.826,29.535 +2020-08-12 13:00:00,111.49,32.529,50.556000000000004,29.535 +2020-08-12 13:15:00,104.65,33.516999999999996,50.556000000000004,29.535 +2020-08-12 13:30:00,100.7,31.879,50.556000000000004,29.535 +2020-08-12 13:45:00,102.39,32.037,50.556000000000004,29.535 +2020-08-12 14:00:00,103.75,33.325,51.188,29.535 +2020-08-12 14:15:00,103.76,32.522,51.188,29.535 +2020-08-12 14:30:00,107.42,31.695999999999998,51.188,29.535 +2020-08-12 14:45:00,105.01,32.378,51.188,29.535 +2020-08-12 15:00:00,100.4,33.842,52.976000000000006,29.535 +2020-08-12 15:15:00,103.59,31.701999999999998,52.976000000000006,29.535 +2020-08-12 15:30:00,101.61,30.491,52.976000000000006,29.535 +2020-08-12 15:45:00,103.92,28.971999999999998,52.976000000000006,29.535 +2020-08-12 16:00:00,102.77,31.217,55.463,29.535 +2020-08-12 16:15:00,107.41,31.173000000000002,55.463,29.535 +2020-08-12 16:30:00,108.87,30.682,55.463,29.535 +2020-08-12 16:45:00,109.71,28.494,55.463,29.535 +2020-08-12 17:00:00,108.65,31.62,59.435,29.535 +2020-08-12 17:15:00,108.44,32.32,59.435,29.535 +2020-08-12 17:30:00,108.62,31.518,59.435,29.535 +2020-08-12 17:45:00,109.31,31.159000000000002,59.435,29.535 +2020-08-12 18:00:00,109.78,34.02,61.387,29.535 +2020-08-12 18:15:00,107.43,33.876,61.387,29.535 +2020-08-12 18:30:00,107.41,32.295,61.387,29.535 +2020-08-12 18:45:00,106.93,34.85,61.387,29.535 +2020-08-12 19:00:00,105.24,36.139,63.323,29.535 +2020-08-12 19:15:00,101.29,35.138000000000005,63.323,29.535 +2020-08-12 19:30:00,101.31,34.048,63.323,29.535 +2020-08-12 19:45:00,105.17,33.194,63.323,29.535 +2020-08-12 20:00:00,102.58,30.789,69.083,29.535 +2020-08-12 20:15:00,100.04,30.183000000000003,69.083,29.535 +2020-08-12 20:30:00,99.46,30.362,69.083,29.535 +2020-08-12 20:45:00,97.18,30.468000000000004,69.083,29.535 +2020-08-12 21:00:00,93.2,29.343000000000004,59.957,29.535 +2020-08-12 21:15:00,90.83,30.383000000000003,59.957,29.535 +2020-08-12 21:30:00,86.78,30.683000000000003,59.957,29.535 +2020-08-12 21:45:00,87.17,31.006,59.957,29.535 +2020-08-12 22:00:00,81.89,28.338,53.821000000000005,29.535 +2020-08-12 22:15:00,83.57,30.643,53.821000000000005,29.535 +2020-08-12 22:30:00,80.19,26.136999999999997,53.821000000000005,29.535 +2020-08-12 22:45:00,79.2,23.215,53.821000000000005,29.535 +2020-08-12 23:00:00,76.03,20.941,45.458,29.535 +2020-08-12 23:15:00,75.7,19.805,45.458,29.535 +2020-08-12 23:30:00,76.14,19.178,45.458,29.535 +2020-08-12 23:45:00,76.61,18.267,45.458,29.535 +2020-08-13 00:00:00,72.42,18.349,40.36,29.535 +2020-08-13 00:15:00,73.49,19.119,40.36,29.535 +2020-08-13 00:30:00,73.4,18.072,40.36,29.535 +2020-08-13 00:45:00,73.58,18.308,40.36,29.535 +2020-08-13 01:00:00,73.1,18.123,38.552,29.535 +2020-08-13 01:15:00,73.24,17.432000000000002,38.552,29.535 +2020-08-13 01:30:00,72.48,16.293,38.552,29.535 +2020-08-13 01:45:00,73.22,16.247,38.552,29.535 +2020-08-13 02:00:00,71.87,16.195999999999998,36.895,29.535 +2020-08-13 02:15:00,75.03,15.322000000000001,36.895,29.535 +2020-08-13 02:30:00,78.27,16.736,36.895,29.535 +2020-08-13 02:45:00,80.01,17.394000000000002,36.895,29.535 +2020-08-13 03:00:00,77.38,18.062,36.565,29.535 +2020-08-13 03:15:00,72.6,17.952,36.565,29.535 +2020-08-13 03:30:00,74.33,17.521,36.565,29.535 +2020-08-13 03:45:00,79.52,16.986,36.565,29.535 +2020-08-13 04:00:00,85.0,20.684,37.263000000000005,29.535 +2020-08-13 04:15:00,91.08,25.62,37.263000000000005,29.535 +2020-08-13 04:30:00,94.95,23.21,37.263000000000005,29.535 +2020-08-13 04:45:00,93.59,23.19,37.263000000000005,29.535 +2020-08-13 05:00:00,98.02,34.096,40.412,29.535 +2020-08-13 05:15:00,100.02,39.655,40.412,29.535 +2020-08-13 05:30:00,106.59,35.711,40.412,29.535 +2020-08-13 05:45:00,112.6,33.354,40.412,29.535 +2020-08-13 06:00:00,116.48,33.299,49.825,29.535 +2020-08-13 06:15:00,113.4,33.088,49.825,29.535 +2020-08-13 06:30:00,111.91,32.939,49.825,29.535 +2020-08-13 06:45:00,111.96,35.58,49.825,29.535 +2020-08-13 07:00:00,114.86,35.355,61.082,29.535 +2020-08-13 07:15:00,116.59,36.48,61.082,29.535 +2020-08-13 07:30:00,119.63,34.942,61.082,29.535 +2020-08-13 07:45:00,118.33,35.748000000000005,61.082,29.535 +2020-08-13 08:00:00,111.91,31.864,53.961999999999996,29.535 +2020-08-13 08:15:00,110.29,34.508,53.961999999999996,29.535 +2020-08-13 08:30:00,109.63,35.491,53.961999999999996,29.535 +2020-08-13 08:45:00,114.21,37.586,53.961999999999996,29.535 +2020-08-13 09:00:00,116.43,33.364000000000004,50.06100000000001,29.535 +2020-08-13 09:15:00,117.8,32.728,50.06100000000001,29.535 +2020-08-13 09:30:00,114.9,35.812,50.06100000000001,29.535 +2020-08-13 09:45:00,115.89,38.521,50.06100000000001,29.535 +2020-08-13 10:00:00,115.79,36.312,47.68,29.535 +2020-08-13 10:15:00,109.66,37.342,47.68,29.535 +2020-08-13 10:30:00,115.53,37.13,47.68,29.535 +2020-08-13 10:45:00,114.31,38.004,47.68,29.535 +2020-08-13 11:00:00,112.62,36.067,45.93899999999999,29.535 +2020-08-13 11:15:00,112.44,37.135999999999996,45.93899999999999,29.535 +2020-08-13 11:30:00,112.77,38.033,45.93899999999999,29.535 +2020-08-13 11:45:00,109.6,38.785,45.93899999999999,29.535 +2020-08-13 12:00:00,110.46,34.21,43.648999999999994,29.535 +2020-08-13 12:15:00,110.53,33.461,43.648999999999994,29.535 +2020-08-13 12:30:00,110.63,32.435,43.648999999999994,29.535 +2020-08-13 12:45:00,107.32,33.141,43.648999999999994,29.535 +2020-08-13 13:00:00,111.0,32.635,42.801,29.535 +2020-08-13 13:15:00,110.62,33.609,42.801,29.535 +2020-08-13 13:30:00,110.8,31.969,42.801,29.535 +2020-08-13 13:45:00,106.12,32.137,42.801,29.535 +2020-08-13 14:00:00,106.62,33.408,43.24,29.535 +2020-08-13 14:15:00,110.75,32.61,43.24,29.535 +2020-08-13 14:30:00,108.44,31.795,43.24,29.535 +2020-08-13 14:45:00,108.43,32.482,43.24,29.535 +2020-08-13 15:00:00,100.47,33.91,45.04600000000001,29.535 +2020-08-13 15:15:00,100.61,31.774,45.04600000000001,29.535 +2020-08-13 15:30:00,104.49,30.573,45.04600000000001,29.535 +2020-08-13 15:45:00,105.82,29.055999999999997,45.04600000000001,29.535 +2020-08-13 16:00:00,111.96,31.281999999999996,46.568000000000005,29.535 +2020-08-13 16:15:00,112.43,31.245,46.568000000000005,29.535 +2020-08-13 16:30:00,113.32,30.761,46.568000000000005,29.535 +2020-08-13 16:45:00,111.17,28.608,46.568000000000005,29.535 +2020-08-13 17:00:00,120.16,31.709,50.618,29.535 +2020-08-13 17:15:00,119.4,32.437,50.618,29.535 +2020-08-13 17:30:00,121.21,31.648000000000003,50.618,29.535 +2020-08-13 17:45:00,113.4,31.320999999999998,50.618,29.535 +2020-08-13 18:00:00,115.18,34.176,52.806999999999995,29.535 +2020-08-13 18:15:00,116.91,34.041,52.806999999999995,29.535 +2020-08-13 18:30:00,119.58,32.469,52.806999999999995,29.535 +2020-08-13 18:45:00,114.31,35.025,52.806999999999995,29.535 +2020-08-13 19:00:00,107.96,36.318000000000005,53.464,29.535 +2020-08-13 19:15:00,104.43,35.316,53.464,29.535 +2020-08-13 19:30:00,104.41,34.227,53.464,29.535 +2020-08-13 19:45:00,110.75,33.378,53.464,29.535 +2020-08-13 20:00:00,110.35,30.975,56.753,29.535 +2020-08-13 20:15:00,104.81,30.371,56.753,29.535 +2020-08-13 20:30:00,100.88,30.537,56.753,29.535 +2020-08-13 20:45:00,98.91,30.616,56.753,29.535 +2020-08-13 21:00:00,94.78,29.493000000000002,52.506,29.535 +2020-08-13 21:15:00,93.57,30.526,52.506,29.535 +2020-08-13 21:30:00,89.55,30.82,52.506,29.535 +2020-08-13 21:45:00,89.48,31.116999999999997,52.506,29.535 +2020-08-13 22:00:00,84.47,28.432,48.163000000000004,29.535 +2020-08-13 22:15:00,85.66,30.724,48.163000000000004,29.535 +2020-08-13 22:30:00,83.52,26.188000000000002,48.163000000000004,29.535 +2020-08-13 22:45:00,82.68,23.265,48.163000000000004,29.535 +2020-08-13 23:00:00,78.63,21.028000000000002,42.379,29.535 +2020-08-13 23:15:00,79.67,19.885,42.379,29.535 +2020-08-13 23:30:00,78.45,19.271,42.379,29.535 +2020-08-13 23:45:00,79.55,18.363,42.379,29.535 +2020-08-14 00:00:00,77.38,16.802,38.505,29.535 +2020-08-14 00:15:00,77.2,17.767,38.505,29.535 +2020-08-14 00:30:00,76.42,16.995,38.505,29.535 +2020-08-14 00:45:00,77.01,17.641,38.505,29.535 +2020-08-14 01:00:00,75.04,17.09,37.004,29.535 +2020-08-14 01:15:00,76.59,15.772,37.004,29.535 +2020-08-14 01:30:00,76.72,15.347000000000001,37.004,29.535 +2020-08-14 01:45:00,76.98,15.050999999999998,37.004,29.535 +2020-08-14 02:00:00,75.7,15.899000000000001,36.098,29.535 +2020-08-14 02:15:00,76.98,15.003,36.098,29.535 +2020-08-14 02:30:00,76.36,17.160999999999998,36.098,29.535 +2020-08-14 02:45:00,77.05,17.155,36.098,29.535 +2020-08-14 03:00:00,76.85,18.619,36.561,29.535 +2020-08-14 03:15:00,76.89,17.305,36.561,29.535 +2020-08-14 03:30:00,77.81,16.651,36.561,29.535 +2020-08-14 03:45:00,81.89,16.94,36.561,29.535 +2020-08-14 04:00:00,84.07,20.77,37.355,29.535 +2020-08-14 04:15:00,91.01,24.226,37.355,29.535 +2020-08-14 04:30:00,96.53,22.723000000000003,37.355,29.535 +2020-08-14 04:45:00,98.47,22.149,37.355,29.535 +2020-08-14 05:00:00,97.75,32.713,40.285,29.535 +2020-08-14 05:15:00,106.42,39.152,40.285,29.535 +2020-08-14 05:30:00,111.47,35.349000000000004,40.285,29.535 +2020-08-14 05:45:00,114.34,32.543,40.285,29.535 +2020-08-14 06:00:00,117.13,32.652,52.378,29.535 +2020-08-14 06:15:00,115.21,32.692,52.378,29.535 +2020-08-14 06:30:00,119.17,32.536,52.378,29.535 +2020-08-14 06:45:00,121.31,34.979,52.378,29.535 +2020-08-14 07:00:00,122.16,35.415,60.891999999999996,29.535 +2020-08-14 07:15:00,114.97,37.369,60.891999999999996,29.535 +2020-08-14 07:30:00,119.05,33.976,60.891999999999996,29.535 +2020-08-14 07:45:00,119.76,34.639,60.891999999999996,29.535 +2020-08-14 08:00:00,121.92,31.587,53.652,29.535 +2020-08-14 08:15:00,117.57,34.885,53.652,29.535 +2020-08-14 08:30:00,116.45,35.696,53.652,29.535 +2020-08-14 08:45:00,119.87,37.746,53.652,29.535 +2020-08-14 09:00:00,122.22,31.248,51.456,29.535 +2020-08-14 09:15:00,121.31,32.442,51.456,29.535 +2020-08-14 09:30:00,117.74,34.867,51.456,29.535 +2020-08-14 09:45:00,114.72,37.943000000000005,51.456,29.535 +2020-08-14 10:00:00,116.88,35.665,49.4,29.535 +2020-08-14 10:15:00,122.22,36.4,49.4,29.535 +2020-08-14 10:30:00,123.37,36.731,49.4,29.535 +2020-08-14 10:45:00,118.57,37.541,49.4,29.535 +2020-08-14 11:00:00,116.16,35.859,48.773,29.535 +2020-08-14 11:15:00,114.64,35.923,48.773,29.535 +2020-08-14 11:30:00,114.36,36.323,48.773,29.535 +2020-08-14 11:45:00,109.72,36.116,48.773,29.535 +2020-08-14 12:00:00,109.46,31.871,46.033,29.535 +2020-08-14 12:15:00,106.86,30.65,46.033,29.535 +2020-08-14 12:30:00,104.86,29.738000000000003,46.033,29.535 +2020-08-14 12:45:00,104.8,29.616,46.033,29.535 +2020-08-14 13:00:00,101.33,29.599,44.38399999999999,29.535 +2020-08-14 13:15:00,101.51,30.674,44.38399999999999,29.535 +2020-08-14 13:30:00,101.84,29.811999999999998,44.38399999999999,29.535 +2020-08-14 13:45:00,104.46,30.298000000000002,44.38399999999999,29.535 +2020-08-14 14:00:00,98.79,30.854,43.162,29.535 +2020-08-14 14:15:00,100.52,30.504,43.162,29.535 +2020-08-14 14:30:00,104.56,31.139,43.162,29.535 +2020-08-14 14:45:00,107.41,31.128,43.162,29.535 +2020-08-14 15:00:00,107.75,32.481,44.91,29.535 +2020-08-14 15:15:00,107.7,30.148000000000003,44.91,29.535 +2020-08-14 15:30:00,103.14,28.518,44.91,29.535 +2020-08-14 15:45:00,104.08,27.74,44.91,29.535 +2020-08-14 16:00:00,108.58,29.195,47.489,29.535 +2020-08-14 16:15:00,109.61,29.625999999999998,47.489,29.535 +2020-08-14 16:30:00,105.67,28.965999999999998,47.489,29.535 +2020-08-14 16:45:00,106.48,26.09,47.489,29.535 +2020-08-14 17:00:00,106.27,30.831999999999997,52.047,29.535 +2020-08-14 17:15:00,112.92,31.45,52.047,29.535 +2020-08-14 17:30:00,117.17,30.857,52.047,29.535 +2020-08-14 17:45:00,114.87,30.416999999999998,52.047,29.535 +2020-08-14 18:00:00,113.07,33.241,53.306000000000004,29.535 +2020-08-14 18:15:00,108.05,32.227,53.306000000000004,29.535 +2020-08-14 18:30:00,110.51,30.538,53.306000000000004,29.535 +2020-08-14 18:45:00,114.14,33.5,53.306000000000004,29.535 +2020-08-14 19:00:00,111.52,35.571,53.516000000000005,29.535 +2020-08-14 19:15:00,103.77,35.055,53.516000000000005,29.535 +2020-08-14 19:30:00,100.59,34.052,53.516000000000005,29.535 +2020-08-14 19:45:00,102.62,32.256,53.516000000000005,29.535 +2020-08-14 20:00:00,101.7,29.701999999999998,57.88,29.535 +2020-08-14 20:15:00,100.14,29.865,57.88,29.535 +2020-08-14 20:30:00,104.22,29.55,57.88,29.535 +2020-08-14 20:45:00,102.33,28.785999999999998,57.88,29.535 +2020-08-14 21:00:00,93.47,28.896,53.32,29.535 +2020-08-14 21:15:00,89.73,31.496,53.32,29.535 +2020-08-14 21:30:00,84.2,31.639,53.32,29.535 +2020-08-14 21:45:00,90.61,32.071999999999996,53.32,29.535 +2020-08-14 22:00:00,87.69,29.177,48.074,29.535 +2020-08-14 22:15:00,85.9,31.226999999999997,48.074,29.535 +2020-08-14 22:30:00,80.19,30.853,48.074,29.535 +2020-08-14 22:45:00,79.07,28.558000000000003,48.074,29.535 +2020-08-14 23:00:00,72.75,27.998,41.306999999999995,29.535 +2020-08-14 23:15:00,73.35,25.458000000000002,41.306999999999995,29.535 +2020-08-14 23:30:00,77.1,23.195,41.306999999999995,29.535 +2020-08-14 23:45:00,80.37,22.21,41.306999999999995,29.535 +2020-08-15 00:00:00,78.66,18.289,40.227,29.423000000000002 +2020-08-15 00:15:00,78.01,18.867,40.227,29.423000000000002 +2020-08-15 00:30:00,77.59,17.567,40.227,29.423000000000002 +2020-08-15 00:45:00,78.28,17.474,40.227,29.423000000000002 +2020-08-15 01:00:00,75.62,17.129,36.303000000000004,29.423000000000002 +2020-08-15 01:15:00,73.05,16.448,36.303000000000004,29.423000000000002 +2020-08-15 01:30:00,72.17,15.292,36.303000000000004,29.423000000000002 +2020-08-15 01:45:00,76.06,16.205,36.303000000000004,29.423000000000002 +2020-08-15 02:00:00,74.7,16.063,33.849000000000004,29.423000000000002 +2020-08-15 02:15:00,74.19,14.452,33.849000000000004,29.423000000000002 +2020-08-15 02:30:00,72.6,15.894,33.849000000000004,29.423000000000002 +2020-08-15 02:45:00,74.73,16.635,33.849000000000004,29.423000000000002 +2020-08-15 03:00:00,74.32,16.73,33.149,29.423000000000002 +2020-08-15 03:15:00,71.83,14.837,33.149,29.423000000000002 +2020-08-15 03:30:00,66.95,14.600999999999999,33.149,29.423000000000002 +2020-08-15 03:45:00,67.51,16.303,33.149,29.423000000000002 +2020-08-15 04:00:00,68.28,18.586,32.501,29.423000000000002 +2020-08-15 04:15:00,68.18,21.346999999999998,32.501,29.423000000000002 +2020-08-15 04:30:00,67.99,18.38,32.501,29.423000000000002 +2020-08-15 04:45:00,69.92,18.101,32.501,29.423000000000002 +2020-08-15 05:00:00,68.61,21.697,31.648000000000003,29.423000000000002 +2020-08-15 05:15:00,71.82,18.840999999999998,31.648000000000003,29.423000000000002 +2020-08-15 05:30:00,69.58,16.473,31.648000000000003,29.423000000000002 +2020-08-15 05:45:00,70.22,17.582,31.648000000000003,29.423000000000002 +2020-08-15 06:00:00,73.17,27.255,32.552,29.423000000000002 +2020-08-15 06:15:00,75.29,33.784,32.552,29.423000000000002 +2020-08-15 06:30:00,78.84,31.236,32.552,29.423000000000002 +2020-08-15 06:45:00,86.96,31.153000000000002,32.552,29.423000000000002 +2020-08-15 07:00:00,88.59,30.756,35.181999999999995,29.423000000000002 +2020-08-15 07:15:00,88.61,31.564,35.181999999999995,29.423000000000002 +2020-08-15 07:30:00,88.1,29.455,35.181999999999995,29.423000000000002 +2020-08-15 07:45:00,94.77,30.729,35.181999999999995,29.423000000000002 +2020-08-15 08:00:00,96.75,28.160999999999998,40.35,29.423000000000002 +2020-08-15 08:15:00,93.18,30.936999999999998,40.35,29.423000000000002 +2020-08-15 08:30:00,93.98,31.596,40.35,29.423000000000002 +2020-08-15 08:45:00,95.48,34.302,40.35,29.423000000000002 +2020-08-15 09:00:00,93.38,30.744,42.292,29.423000000000002 +2020-08-15 09:15:00,91.59,32.339,42.292,29.423000000000002 +2020-08-15 09:30:00,92.73,35.157,42.292,29.423000000000002 +2020-08-15 09:45:00,96.95,37.759,42.292,29.423000000000002 +2020-08-15 10:00:00,100.16,36.094,40.084,29.423000000000002 +2020-08-15 10:15:00,101.04,37.143,40.084,29.423000000000002 +2020-08-15 10:30:00,99.67,37.117,40.084,29.423000000000002 +2020-08-15 10:45:00,99.38,37.459,40.084,29.423000000000002 +2020-08-15 11:00:00,95.9,35.616,36.966,29.423000000000002 +2020-08-15 11:15:00,93.51,36.58,36.966,29.423000000000002 +2020-08-15 11:30:00,89.93,37.348,36.966,29.423000000000002 +2020-08-15 11:45:00,86.8,37.865,36.966,29.423000000000002 +2020-08-15 12:00:00,82.81,34.096,35.19,29.423000000000002 +2020-08-15 12:15:00,81.87,33.643,35.19,29.423000000000002 +2020-08-15 12:30:00,80.39,32.586999999999996,35.19,29.423000000000002 +2020-08-15 12:45:00,72.88,33.234,35.19,29.423000000000002 +2020-08-15 13:00:00,68.23,32.400999999999996,32.277,29.423000000000002 +2020-08-15 13:15:00,68.49,33.111999999999995,32.277,29.423000000000002 +2020-08-15 13:30:00,72.94,32.469,32.277,29.423000000000002 +2020-08-15 13:45:00,73.41,31.604,32.277,29.423000000000002 +2020-08-15 14:00:00,67.3,32.065,31.436999999999998,29.423000000000002 +2020-08-15 14:15:00,71.1,30.541999999999998,31.436999999999998,29.423000000000002 +2020-08-15 14:30:00,67.23,30.926,31.436999999999998,29.423000000000002 +2020-08-15 14:45:00,66.15,31.37,31.436999999999998,29.423000000000002 +2020-08-15 15:00:00,64.93,33.033,33.493,29.423000000000002 +2020-08-15 15:15:00,67.48,31.328000000000003,33.493,29.423000000000002 +2020-08-15 15:30:00,66.52,29.779,33.493,29.423000000000002 +2020-08-15 15:45:00,68.11,28.105999999999998,33.493,29.423000000000002 +2020-08-15 16:00:00,71.24,31.679000000000002,36.593,29.423000000000002 +2020-08-15 16:15:00,71.7,31.18,36.593,29.423000000000002 +2020-08-15 16:30:00,73.28,30.749000000000002,36.593,29.423000000000002 +2020-08-15 16:45:00,76.09,27.814,36.593,29.423000000000002 +2020-08-15 17:00:00,77.79,31.39,42.049,29.423000000000002 +2020-08-15 17:15:00,78.28,29.826,42.049,29.423000000000002 +2020-08-15 17:30:00,80.48,29.125999999999998,42.049,29.423000000000002 +2020-08-15 17:45:00,81.56,29.226,42.049,29.423000000000002 +2020-08-15 18:00:00,82.67,33.369,43.755,29.423000000000002 +2020-08-15 18:15:00,81.18,33.85,43.755,29.423000000000002 +2020-08-15 18:30:00,80.1,33.385,43.755,29.423000000000002 +2020-08-15 18:45:00,80.79,33.236,43.755,29.423000000000002 +2020-08-15 19:00:00,78.86,33.652,44.492,29.423000000000002 +2020-08-15 19:15:00,76.55,32.202,44.492,29.423000000000002 +2020-08-15 19:30:00,77.59,31.889,44.492,29.423000000000002 +2020-08-15 19:45:00,80.08,31.791,44.492,29.423000000000002 +2020-08-15 20:00:00,79.74,29.905,40.896,29.423000000000002 +2020-08-15 20:15:00,80.99,29.331,40.896,29.423000000000002 +2020-08-15 20:30:00,76.08,28.186999999999998,40.896,29.423000000000002 +2020-08-15 20:45:00,75.31,29.188000000000002,40.896,29.423000000000002 +2020-08-15 21:00:00,72.65,27.752,39.056,29.423000000000002 +2020-08-15 21:15:00,72.67,29.988000000000003,39.056,29.423000000000002 +2020-08-15 21:30:00,70.17,30.236,39.056,29.423000000000002 +2020-08-15 21:45:00,69.86,30.145,39.056,29.423000000000002 +2020-08-15 22:00:00,66.08,27.138,38.478,29.423000000000002 +2020-08-15 22:15:00,67.02,29.274,38.478,29.423000000000002 +2020-08-15 22:30:00,64.4,28.171999999999997,38.478,29.423000000000002 +2020-08-15 22:45:00,64.41,26.127,38.478,29.423000000000002 +2020-08-15 23:00:00,59.88,24.873,32.953,29.423000000000002 +2020-08-15 23:15:00,61.24,22.807,32.953,29.423000000000002 +2020-08-15 23:30:00,60.61,22.965,32.953,29.423000000000002 +2020-08-15 23:45:00,59.27,22.448,32.953,29.423000000000002 +2020-08-16 00:00:00,56.4,20.963,28.584,29.423000000000002 +2020-08-16 00:15:00,57.15,20.25,28.584,29.423000000000002 +2020-08-16 00:30:00,56.4,19.059,28.584,29.423000000000002 +2020-08-16 00:45:00,56.75,18.803,28.584,29.423000000000002 +2020-08-16 01:00:00,54.08,18.761,26.419,29.423000000000002 +2020-08-16 01:15:00,55.73,17.887999999999998,26.419,29.423000000000002 +2020-08-16 01:30:00,54.82,16.56,26.419,29.423000000000002 +2020-08-16 01:45:00,55.19,16.852,26.419,29.423000000000002 +2020-08-16 02:00:00,53.79,16.772000000000002,25.335,29.423000000000002 +2020-08-16 02:15:00,54.64,15.895999999999999,25.335,29.423000000000002 +2020-08-16 02:30:00,54.21,17.325,25.335,29.423000000000002 +2020-08-16 02:45:00,53.98,17.839000000000002,25.335,29.423000000000002 +2020-08-16 03:00:00,53.46,18.679000000000002,24.805,29.423000000000002 +2020-08-16 03:15:00,54.06,16.922,24.805,29.423000000000002 +2020-08-16 03:30:00,54.48,15.995999999999999,24.805,29.423000000000002 +2020-08-16 03:45:00,55.19,17.086,24.805,29.423000000000002 +2020-08-16 04:00:00,55.99,19.613,25.772,29.423000000000002 +2020-08-16 04:15:00,57.26,22.346,25.772,29.423000000000002 +2020-08-16 04:30:00,54.82,20.494,25.772,29.423000000000002 +2020-08-16 04:45:00,56.06,19.851,25.772,29.423000000000002 +2020-08-16 05:00:00,55.21,23.886,25.971999999999998,29.423000000000002 +2020-08-16 05:15:00,55.74,20.704,25.971999999999998,29.423000000000002 +2020-08-16 05:30:00,55.42,17.433,25.971999999999998,29.423000000000002 +2020-08-16 05:45:00,56.8,18.361,25.971999999999998,29.423000000000002 +2020-08-16 06:00:00,55.15,26.846999999999998,26.026,29.423000000000002 +2020-08-16 06:15:00,58.89,34.753,26.026,29.423000000000002 +2020-08-16 06:30:00,59.5,31.089000000000002,26.026,29.423000000000002 +2020-08-16 06:45:00,60.53,29.549,26.026,29.423000000000002 +2020-08-16 07:00:00,61.9,29.266,27.396,29.423000000000002 +2020-08-16 07:15:00,61.93,28.503,27.396,29.423000000000002 +2020-08-16 07:30:00,61.72,27.575,27.396,29.423000000000002 +2020-08-16 07:45:00,59.55,28.915,27.396,29.423000000000002 +2020-08-16 08:00:00,62.74,27.895,30.791999999999998,29.423000000000002 +2020-08-16 08:15:00,61.33,31.752,30.791999999999998,29.423000000000002 +2020-08-16 08:30:00,60.91,32.825,30.791999999999998,29.423000000000002 +2020-08-16 08:45:00,60.73,35.29,30.791999999999998,29.423000000000002 +2020-08-16 09:00:00,60.52,30.888,32.482,29.423000000000002 +2020-08-16 09:15:00,61.23,31.75,32.482,29.423000000000002 +2020-08-16 09:30:00,61.34,34.828,32.482,29.423000000000002 +2020-08-16 09:45:00,61.67,38.303000000000004,32.482,29.423000000000002 +2020-08-16 10:00:00,59.03,35.681,31.951,29.423000000000002 +2020-08-16 10:15:00,63.77,36.964,31.951,29.423000000000002 +2020-08-16 10:30:00,64.3,37.236,31.951,29.423000000000002 +2020-08-16 10:45:00,64.55,39.031,31.951,29.423000000000002 +2020-08-16 11:00:00,62.19,37.029,33.619,29.423000000000002 +2020-08-16 11:15:00,61.81,37.51,33.619,29.423000000000002 +2020-08-16 11:30:00,60.1,38.786,33.619,29.423000000000002 +2020-08-16 11:45:00,59.33,39.66,33.619,29.423000000000002 +2020-08-16 12:00:00,56.59,37.803000000000004,30.975,29.423000000000002 +2020-08-16 12:15:00,56.52,36.787,30.975,29.423000000000002 +2020-08-16 12:30:00,56.09,36.008,30.975,29.423000000000002 +2020-08-16 12:45:00,54.79,36.227,30.975,29.423000000000002 +2020-08-16 13:00:00,51.28,35.288000000000004,27.956999999999997,29.423000000000002 +2020-08-16 13:15:00,55.09,35.214,27.956999999999997,29.423000000000002 +2020-08-16 13:30:00,52.21,33.672,27.956999999999997,29.423000000000002 +2020-08-16 13:45:00,54.84,33.584,27.956999999999997,29.423000000000002 +2020-08-16 14:00:00,55.31,35.247,25.555999999999997,29.423000000000002 +2020-08-16 14:15:00,56.62,34.028,25.555999999999997,29.423000000000002 +2020-08-16 14:30:00,54.58,32.937,25.555999999999997,29.423000000000002 +2020-08-16 14:45:00,56.86,32.461,25.555999999999997,29.423000000000002 +2020-08-16 15:00:00,58.29,34.168,26.271,29.423000000000002 +2020-08-16 15:15:00,58.87,31.699,26.271,29.423000000000002 +2020-08-16 15:30:00,54.96,29.926,26.271,29.423000000000002 +2020-08-16 15:45:00,59.3,28.335,26.271,29.423000000000002 +2020-08-16 16:00:00,63.28,30.392,30.369,29.423000000000002 +2020-08-16 16:15:00,64.29,30.148000000000003,30.369,29.423000000000002 +2020-08-16 16:30:00,66.51,30.853,30.369,29.423000000000002 +2020-08-16 16:45:00,70.7,27.897,30.369,29.423000000000002 +2020-08-16 17:00:00,74.15,31.488000000000003,38.787,29.423000000000002 +2020-08-16 17:15:00,75.05,31.449,38.787,29.423000000000002 +2020-08-16 17:30:00,76.55,31.503,38.787,29.423000000000002 +2020-08-16 17:45:00,77.73,31.592,38.787,29.423000000000002 +2020-08-16 18:00:00,80.1,35.961,41.886,29.423000000000002 +2020-08-16 18:15:00,79.15,35.951,41.886,29.423000000000002 +2020-08-16 18:30:00,78.57,35.23,41.886,29.423000000000002 +2020-08-16 18:45:00,78.39,35.485,41.886,29.423000000000002 +2020-08-16 19:00:00,77.11,37.847,42.91,29.423000000000002 +2020-08-16 19:15:00,78.29,35.399,42.91,29.423000000000002 +2020-08-16 19:30:00,80.1,34.857,42.91,29.423000000000002 +2020-08-16 19:45:00,82.78,34.594,42.91,29.423000000000002 +2020-08-16 20:00:00,82.35,32.407,42.148999999999994,29.423000000000002 +2020-08-16 20:15:00,81.74,32.199,42.148999999999994,29.423000000000002 +2020-08-16 20:30:00,81.4,31.858,42.148999999999994,29.423000000000002 +2020-08-16 20:45:00,81.03,31.601,42.148999999999994,29.423000000000002 +2020-08-16 21:00:00,79.1,29.82,40.955999999999996,29.423000000000002 +2020-08-16 21:15:00,80.28,32.325,40.955999999999996,29.423000000000002 +2020-08-16 21:30:00,78.42,32.105,40.955999999999996,29.423000000000002 +2020-08-16 21:45:00,77.85,32.394,40.955999999999996,29.423000000000002 +2020-08-16 22:00:00,73.71,31.552,39.873000000000005,29.423000000000002 +2020-08-16 22:15:00,74.13,31.956,39.873000000000005,29.423000000000002 +2020-08-16 22:30:00,70.68,30.46,39.873000000000005,29.423000000000002 +2020-08-16 22:45:00,73.48,27.304000000000002,39.873000000000005,29.423000000000002 +2020-08-16 23:00:00,66.63,25.803,35.510999999999996,29.423000000000002 +2020-08-16 23:15:00,68.8,24.945999999999998,35.510999999999996,29.423000000000002 +2020-08-16 23:30:00,67.36,24.513,35.510999999999996,29.423000000000002 +2020-08-16 23:45:00,68.31,24.014,35.510999999999996,29.423000000000002 +2020-08-17 00:00:00,67.08,22.375,33.475,29.535 +2020-08-17 00:15:00,66.94,22.315,33.475,29.535 +2020-08-17 00:30:00,66.43,20.779,33.475,29.535 +2020-08-17 00:45:00,66.96,20.184,33.475,29.535 +2020-08-17 01:00:00,64.35,20.474,33.111,29.535 +2020-08-17 01:15:00,65.92,19.635,33.111,29.535 +2020-08-17 01:30:00,65.15,18.643,33.111,29.535 +2020-08-17 01:45:00,64.74,18.845,33.111,29.535 +2020-08-17 02:00:00,65.98,19.177,32.358000000000004,29.535 +2020-08-17 02:15:00,72.77,17.374000000000002,32.358000000000004,29.535 +2020-08-17 02:30:00,73.53,18.923,32.358000000000004,29.535 +2020-08-17 02:45:00,69.28,19.314,32.358000000000004,29.535 +2020-08-17 03:00:00,66.89,20.573,30.779,29.535 +2020-08-17 03:15:00,67.8,19.445999999999998,30.779,29.535 +2020-08-17 03:30:00,71.43,19.149,30.779,29.535 +2020-08-17 03:45:00,74.75,19.83,30.779,29.535 +2020-08-17 04:00:00,80.78,25.086,31.416,29.535 +2020-08-17 04:15:00,87.43,30.389,31.416,29.535 +2020-08-17 04:30:00,90.67,28.061,31.416,29.535 +2020-08-17 04:45:00,90.09,27.750999999999998,31.416,29.535 +2020-08-17 05:00:00,93.62,38.103,37.221,29.535 +2020-08-17 05:15:00,96.83,43.934,37.221,29.535 +2020-08-17 05:30:00,105.37,39.284,37.221,29.535 +2020-08-17 05:45:00,108.9,37.507,37.221,29.535 +2020-08-17 06:00:00,113.83,36.976,51.891000000000005,29.535 +2020-08-17 06:15:00,111.93,36.961,51.891000000000005,29.535 +2020-08-17 06:30:00,116.33,36.743,51.891000000000005,29.535 +2020-08-17 06:45:00,116.15,39.816,51.891000000000005,29.535 +2020-08-17 07:00:00,119.7,39.711999999999996,62.282,29.535 +2020-08-17 07:15:00,114.94,40.998000000000005,62.282,29.535 +2020-08-17 07:30:00,113.97,39.330999999999996,62.282,29.535 +2020-08-17 07:45:00,116.01,40.996,62.282,29.535 +2020-08-17 08:00:00,116.0,38.075,54.102,29.535 +2020-08-17 08:15:00,113.19,40.864000000000004,54.102,29.535 +2020-08-17 08:30:00,108.1,41.148,54.102,29.535 +2020-08-17 08:45:00,111.95,43.861000000000004,54.102,29.535 +2020-08-17 09:00:00,116.11,38.529,50.917,29.535 +2020-08-17 09:15:00,113.89,37.845,50.917,29.535 +2020-08-17 09:30:00,111.12,40.088,50.917,29.535 +2020-08-17 09:45:00,106.99,41.456,50.917,29.535 +2020-08-17 10:00:00,109.37,39.195,49.718999999999994,29.535 +2020-08-17 10:15:00,108.32,40.332,49.718999999999994,29.535 +2020-08-17 10:30:00,115.64,40.201,49.718999999999994,29.535 +2020-08-17 10:45:00,113.32,40.523,49.718999999999994,29.535 +2020-08-17 11:00:00,109.23,38.788000000000004,49.833999999999996,29.535 +2020-08-17 11:15:00,106.15,39.465,49.833999999999996,29.535 +2020-08-17 11:30:00,106.16,41.331,49.833999999999996,29.535 +2020-08-17 11:45:00,112.52,42.663999999999994,49.833999999999996,29.535 +2020-08-17 12:00:00,113.63,39.105,47.832,29.535 +2020-08-17 12:15:00,106.33,38.181,47.832,29.535 +2020-08-17 12:30:00,107.11,36.42,47.832,29.535 +2020-08-17 12:45:00,107.04,36.57,47.832,29.535 +2020-08-17 13:00:00,103.37,36.479,48.03,29.535 +2020-08-17 13:15:00,102.48,35.629,48.03,29.535 +2020-08-17 13:30:00,104.21,34.259,48.03,29.535 +2020-08-17 13:45:00,102.22,35.001,48.03,29.535 +2020-08-17 14:00:00,103.51,35.816,48.157,29.535 +2020-08-17 14:15:00,99.38,35.17,48.157,29.535 +2020-08-17 14:30:00,99.26,33.96,48.157,29.535 +2020-08-17 14:45:00,99.3,35.389,48.157,29.535 +2020-08-17 15:00:00,102.77,36.656,48.897,29.535 +2020-08-17 15:15:00,101.97,33.701,48.897,29.535 +2020-08-17 15:30:00,103.47,32.678000000000004,48.897,29.535 +2020-08-17 15:45:00,104.76,30.66,48.897,29.535 +2020-08-17 16:00:00,106.74,33.74,51.446000000000005,29.535 +2020-08-17 16:15:00,109.18,33.586,51.446000000000005,29.535 +2020-08-17 16:30:00,111.89,33.715,51.446000000000005,29.535 +2020-08-17 16:45:00,110.39,30.851,51.446000000000005,29.535 +2020-08-17 17:00:00,109.89,33.382,57.507,29.535 +2020-08-17 17:15:00,108.72,33.742,57.507,29.535 +2020-08-17 17:30:00,109.15,33.471,57.507,29.535 +2020-08-17 17:45:00,108.54,33.272,57.507,29.535 +2020-08-17 18:00:00,108.29,36.677,57.896,29.535 +2020-08-17 18:15:00,107.32,35.067,57.896,29.535 +2020-08-17 18:30:00,107.79,33.650999999999996,57.896,29.535 +2020-08-17 18:45:00,106.79,36.765,57.896,29.535 +2020-08-17 19:00:00,113.59,38.936,57.891999999999996,29.535 +2020-08-17 19:15:00,107.67,37.709,57.891999999999996,29.535 +2020-08-17 19:30:00,110.58,36.824,57.891999999999996,29.535 +2020-08-17 19:45:00,109.62,36.025999999999996,57.891999999999996,29.535 +2020-08-17 20:00:00,102.56,32.74,64.57300000000001,29.535 +2020-08-17 20:15:00,100.21,33.866,64.57300000000001,29.535 +2020-08-17 20:30:00,98.27,34.029,64.57300000000001,29.535 +2020-08-17 20:45:00,99.3,33.9,64.57300000000001,29.535 +2020-08-17 21:00:00,95.28,31.535,59.431999999999995,29.535 +2020-08-17 21:15:00,96.5,34.459,59.431999999999995,29.535 +2020-08-17 21:30:00,93.09,34.631,59.431999999999995,29.535 +2020-08-17 21:45:00,92.08,34.704,59.431999999999995,29.535 +2020-08-17 22:00:00,89.32,32.049,51.519,29.535 +2020-08-17 22:15:00,87.02,34.274,51.519,29.535 +2020-08-17 22:30:00,82.96,29.033,51.519,29.535 +2020-08-17 22:45:00,87.26,25.968000000000004,51.519,29.535 +2020-08-17 23:00:00,83.17,24.506999999999998,44.501000000000005,29.535 +2020-08-17 23:15:00,84.93,22.086,44.501000000000005,29.535 +2020-08-17 23:30:00,79.26,21.566999999999997,44.501000000000005,29.535 +2020-08-17 23:45:00,75.42,20.524,44.501000000000005,29.535 +2020-08-18 00:00:00,80.51,20.454,44.522,29.535 +2020-08-18 00:15:00,81.43,21.15,44.522,29.535 +2020-08-18 00:30:00,80.49,20.289,44.522,29.535 +2020-08-18 00:45:00,75.85,20.444000000000003,44.522,29.535 +2020-08-18 01:00:00,72.88,20.229,41.441,29.535 +2020-08-18 01:15:00,73.3,19.511,41.441,29.535 +2020-08-18 01:30:00,75.87,18.41,41.441,29.535 +2020-08-18 01:45:00,80.87,18.136,41.441,29.535 +2020-08-18 02:00:00,81.98,18.032,40.203,29.535 +2020-08-18 02:15:00,79.19,17.307000000000002,40.203,29.535 +2020-08-18 02:30:00,76.72,18.492,40.203,29.535 +2020-08-18 02:45:00,82.03,19.179000000000002,40.203,29.535 +2020-08-18 03:00:00,82.91,19.949,39.536,29.535 +2020-08-18 03:15:00,82.37,19.711,39.536,29.535 +2020-08-18 03:30:00,78.01,19.379,39.536,29.535 +2020-08-18 03:45:00,83.18,19.037,39.536,29.535 +2020-08-18 04:00:00,90.43,23.16,40.759,29.535 +2020-08-18 04:15:00,92.1,28.493000000000002,40.759,29.535 +2020-08-18 04:30:00,89.48,26.066999999999997,40.759,29.535 +2020-08-18 04:45:00,92.67,26.177,40.759,29.535 +2020-08-18 05:00:00,97.83,37.921,43.623999999999995,29.535 +2020-08-18 05:15:00,105.65,44.367,43.623999999999995,29.535 +2020-08-18 05:30:00,111.86,39.913000000000004,43.623999999999995,29.535 +2020-08-18 05:45:00,115.65,37.446,43.623999999999995,29.535 +2020-08-18 06:00:00,116.29,37.865,52.684,29.535 +2020-08-18 06:15:00,117.43,38.018,52.684,29.535 +2020-08-18 06:30:00,123.82,37.52,52.684,29.535 +2020-08-18 06:45:00,129.27,39.798,52.684,29.535 +2020-08-18 07:00:00,135.91,39.836,62.676,29.535 +2020-08-18 07:15:00,126.32,40.907,62.676,29.535 +2020-08-18 07:30:00,127.7,39.289,62.676,29.535 +2020-08-18 07:45:00,125.98,40.064,62.676,29.535 +2020-08-18 08:00:00,127.08,37.076,56.161,29.535 +2020-08-18 08:15:00,131.88,39.455,56.161,29.535 +2020-08-18 08:30:00,134.91,39.891,56.161,29.535 +2020-08-18 08:45:00,131.78,41.751000000000005,56.161,29.535 +2020-08-18 09:00:00,132.31,36.759,52.132,29.535 +2020-08-18 09:15:00,134.14,35.897,52.132,29.535 +2020-08-18 09:30:00,140.01,38.738,52.132,29.535 +2020-08-18 09:45:00,140.34,41.32,52.132,29.535 +2020-08-18 10:00:00,137.81,37.816,51.032,29.535 +2020-08-18 10:15:00,133.5,38.835,51.032,29.535 +2020-08-18 10:30:00,134.66,38.716,51.032,29.535 +2020-08-18 10:45:00,131.74,39.949,51.032,29.535 +2020-08-18 11:00:00,125.16,38.181,51.085,29.535 +2020-08-18 11:15:00,124.46,39.236,51.085,29.535 +2020-08-18 11:30:00,124.13,40.074,51.085,29.535 +2020-08-18 11:45:00,115.02,40.953,51.085,29.535 +2020-08-18 12:00:00,109.2,37.262,49.049,29.535 +2020-08-18 12:15:00,108.88,36.66,49.049,29.535 +2020-08-18 12:30:00,112.98,35.681999999999995,49.049,29.535 +2020-08-18 12:45:00,115.32,36.479,49.049,29.535 +2020-08-18 13:00:00,108.39,36.041,49.722,29.535 +2020-08-18 13:15:00,111.75,36.867,49.722,29.535 +2020-08-18 13:30:00,111.55,35.42,49.722,29.535 +2020-08-18 13:45:00,107.18,35.269,49.722,29.535 +2020-08-18 14:00:00,114.92,36.473,49.565,29.535 +2020-08-18 14:15:00,117.53,35.641999999999996,49.565,29.535 +2020-08-18 14:30:00,117.41,34.726,49.565,29.535 +2020-08-18 14:45:00,113.97,35.425,49.565,29.535 +2020-08-18 15:00:00,104.47,36.525,51.108999999999995,29.535 +2020-08-18 15:15:00,107.86,34.426,51.108999999999995,29.535 +2020-08-18 15:30:00,116.75,33.21,51.108999999999995,29.535 +2020-08-18 15:45:00,114.65,31.535999999999998,51.108999999999995,29.535 +2020-08-18 16:00:00,112.4,33.915,52.725,29.535 +2020-08-18 16:15:00,114.87,33.833,52.725,29.535 +2020-08-18 16:30:00,114.02,33.6,52.725,29.535 +2020-08-18 16:45:00,116.97,31.426,52.725,29.535 +2020-08-18 17:00:00,113.73,34.107,58.031000000000006,29.535 +2020-08-18 17:15:00,110.05,34.88,58.031000000000006,29.535 +2020-08-18 17:30:00,118.5,34.19,58.031000000000006,29.535 +2020-08-18 17:45:00,119.33,33.718,58.031000000000006,29.535 +2020-08-18 18:00:00,122.4,36.251999999999995,58.338,29.535 +2020-08-18 18:15:00,114.26,36.035,58.338,29.535 +2020-08-18 18:30:00,115.94,34.396,58.338,29.535 +2020-08-18 18:45:00,118.08,37.306999999999995,58.338,29.535 +2020-08-18 19:00:00,107.79,38.418,58.464,29.535 +2020-08-18 19:15:00,104.16,37.361,58.464,29.535 +2020-08-18 19:30:00,111.36,36.297,58.464,29.535 +2020-08-18 19:45:00,113.13,35.806,58.464,29.535 +2020-08-18 20:00:00,106.38,32.879,63.708,29.535 +2020-08-18 20:15:00,103.17,32.701,63.708,29.535 +2020-08-18 20:30:00,98.53,32.858000000000004,63.708,29.535 +2020-08-18 20:45:00,97.08,33.101,63.708,29.535 +2020-08-18 21:00:00,89.74,31.555,57.06399999999999,29.535 +2020-08-18 21:15:00,91.57,33.103,57.06399999999999,29.535 +2020-08-18 21:30:00,86.58,33.452,57.06399999999999,29.535 +2020-08-18 21:45:00,86.69,33.669000000000004,57.06399999999999,29.535 +2020-08-18 22:00:00,80.92,31.107,52.831,29.535 +2020-08-18 22:15:00,81.51,33.018,52.831,29.535 +2020-08-18 22:30:00,80.13,28.011999999999997,52.831,29.535 +2020-08-18 22:45:00,79.13,24.936999999999998,52.831,29.535 +2020-08-18 23:00:00,72.87,22.809,44.717,29.535 +2020-08-18 23:15:00,76.22,21.831999999999997,44.717,29.535 +2020-08-18 23:30:00,77.08,21.331,44.717,29.535 +2020-08-18 23:45:00,78.95,20.47,44.717,29.535 +2020-08-19 00:00:00,75.55,20.588,41.263000000000005,29.535 +2020-08-19 00:15:00,74.25,21.284000000000002,41.263000000000005,29.535 +2020-08-19 00:30:00,73.14,20.428,41.263000000000005,29.535 +2020-08-19 00:45:00,73.85,20.59,41.263000000000005,29.535 +2020-08-19 01:00:00,72.97,20.358,38.448,29.535 +2020-08-19 01:15:00,75.56,19.652,38.448,29.535 +2020-08-19 01:30:00,70.9,18.563,38.448,29.535 +2020-08-19 01:45:00,73.95,18.288,38.448,29.535 +2020-08-19 02:00:00,73.83,18.187,36.471,29.535 +2020-08-19 02:15:00,75.11,17.482,36.471,29.535 +2020-08-19 02:30:00,74.21,18.646,36.471,29.535 +2020-08-19 02:45:00,74.47,19.331,36.471,29.535 +2020-08-19 03:00:00,76.06,20.089000000000002,36.042,29.535 +2020-08-19 03:15:00,77.07,19.866,36.042,29.535 +2020-08-19 03:30:00,74.69,19.54,36.042,29.535 +2020-08-19 03:45:00,84.15,19.194000000000003,36.042,29.535 +2020-08-19 04:00:00,90.54,23.335,36.705,29.535 +2020-08-19 04:15:00,91.52,28.679000000000002,36.705,29.535 +2020-08-19 04:30:00,88.4,26.26,36.705,29.535 +2020-08-19 04:45:00,91.16,26.372,36.705,29.535 +2020-08-19 05:00:00,95.33,38.171,39.716,29.535 +2020-08-19 05:15:00,104.66,44.675,39.716,29.535 +2020-08-19 05:30:00,106.17,40.223,39.716,29.535 +2020-08-19 05:45:00,113.8,37.723,39.716,29.535 +2020-08-19 06:00:00,118.61,38.113,52.756,29.535 +2020-08-19 06:15:00,117.99,38.286,52.756,29.535 +2020-08-19 06:30:00,114.51,37.786,52.756,29.535 +2020-08-19 06:45:00,111.03,40.067,52.756,29.535 +2020-08-19 07:00:00,120.0,40.104,65.977,29.535 +2020-08-19 07:15:00,121.82,41.191,65.977,29.535 +2020-08-19 07:30:00,118.1,39.595,65.977,29.535 +2020-08-19 07:45:00,117.26,40.374,65.977,29.535 +2020-08-19 08:00:00,114.13,37.389,57.927,29.535 +2020-08-19 08:15:00,117.26,39.741,57.927,29.535 +2020-08-19 08:30:00,120.28,40.171,57.927,29.535 +2020-08-19 08:45:00,118.51,42.016999999999996,57.927,29.535 +2020-08-19 09:00:00,117.01,37.033,54.86,29.535 +2020-08-19 09:15:00,114.75,36.162,54.86,29.535 +2020-08-19 09:30:00,117.13,38.986999999999995,54.86,29.535 +2020-08-19 09:45:00,112.52,41.547,54.86,29.535 +2020-08-19 10:00:00,114.95,38.043,52.818000000000005,29.535 +2020-08-19 10:15:00,116.13,39.04,52.818000000000005,29.535 +2020-08-19 10:30:00,116.65,38.913000000000004,52.818000000000005,29.535 +2020-08-19 10:45:00,111.6,40.138000000000005,52.818000000000005,29.535 +2020-08-19 11:00:00,110.58,38.38,52.937,29.535 +2020-08-19 11:15:00,112.5,39.426,52.937,29.535 +2020-08-19 11:30:00,112.13,40.26,52.937,29.535 +2020-08-19 11:45:00,111.52,41.125,52.937,29.535 +2020-08-19 12:00:00,106.13,37.423,50.826,29.535 +2020-08-19 12:15:00,106.08,36.811,50.826,29.535 +2020-08-19 12:30:00,106.22,35.847,50.826,29.535 +2020-08-19 12:45:00,115.53,36.633,50.826,29.535 +2020-08-19 13:00:00,113.75,36.177,50.556000000000004,29.535 +2020-08-19 13:15:00,112.48,36.993,50.556000000000004,29.535 +2020-08-19 13:30:00,105.73,35.543,50.556000000000004,29.535 +2020-08-19 13:45:00,105.62,35.400999999999996,50.556000000000004,29.535 +2020-08-19 14:00:00,109.57,36.584,51.188,29.535 +2020-08-19 14:15:00,112.24,35.759,51.188,29.535 +2020-08-19 14:30:00,108.23,34.856,51.188,29.535 +2020-08-19 14:45:00,107.5,35.559,51.188,29.535 +2020-08-19 15:00:00,109.94,36.618,52.976000000000006,29.535 +2020-08-19 15:15:00,110.19,34.525999999999996,52.976000000000006,29.535 +2020-08-19 15:30:00,105.86,33.321999999999996,52.976000000000006,29.535 +2020-08-19 15:45:00,109.9,31.653000000000002,52.976000000000006,29.535 +2020-08-19 16:00:00,110.95,34.01,55.463,29.535 +2020-08-19 16:15:00,112.44,33.936,55.463,29.535 +2020-08-19 16:30:00,109.15,33.707,55.463,29.535 +2020-08-19 16:45:00,107.57,31.572,55.463,29.535 +2020-08-19 17:00:00,115.26,34.226,59.435,29.535 +2020-08-19 17:15:00,118.06,35.025,59.435,29.535 +2020-08-19 17:30:00,118.86,34.346,59.435,29.535 +2020-08-19 17:45:00,116.17,33.907,59.435,29.535 +2020-08-19 18:00:00,112.79,36.433,61.387,29.535 +2020-08-19 18:15:00,115.1,36.225,61.387,29.535 +2020-08-19 18:30:00,118.21,34.598,61.387,29.535 +2020-08-19 18:45:00,118.6,37.507,61.387,29.535 +2020-08-19 19:00:00,113.59,38.624,63.323,29.535 +2020-08-19 19:15:00,106.04,37.567,63.323,29.535 +2020-08-19 19:30:00,112.3,36.503,63.323,29.535 +2020-08-19 19:45:00,112.18,36.016,63.323,29.535 +2020-08-19 20:00:00,107.02,33.094,69.083,29.535 +2020-08-19 20:15:00,107.02,32.918,69.083,29.535 +2020-08-19 20:30:00,100.41,33.061,69.083,29.535 +2020-08-19 20:45:00,98.9,33.274,69.083,29.535 +2020-08-19 21:00:00,94.63,31.729,59.957,29.535 +2020-08-19 21:15:00,94.3,33.27,59.957,29.535 +2020-08-19 21:30:00,91.09,33.616,59.957,29.535 +2020-08-19 21:45:00,88.92,33.804,59.957,29.535 +2020-08-19 22:00:00,81.63,31.223000000000003,53.821000000000005,29.535 +2020-08-19 22:15:00,84.95,33.12,53.821000000000005,29.535 +2020-08-19 22:30:00,83.56,28.084,53.821000000000005,29.535 +2020-08-19 22:45:00,83.98,25.009,53.821000000000005,29.535 +2020-08-19 23:00:00,74.72,22.921,45.458,29.535 +2020-08-19 23:15:00,78.64,21.933000000000003,45.458,29.535 +2020-08-19 23:30:00,74.14,21.443,45.458,29.535 +2020-08-19 23:45:00,79.63,20.586,45.458,29.535 +2020-08-20 00:00:00,73.64,20.724,40.36,29.535 +2020-08-20 00:15:00,75.13,21.421,40.36,29.535 +2020-08-20 00:30:00,70.59,20.57,40.36,29.535 +2020-08-20 00:45:00,74.45,20.739,40.36,29.535 +2020-08-20 01:00:00,72.22,20.49,38.552,29.535 +2020-08-20 01:15:00,74.16,19.795,38.552,29.535 +2020-08-20 01:30:00,75.17,18.719,38.552,29.535 +2020-08-20 01:45:00,81.58,18.444000000000003,38.552,29.535 +2020-08-20 02:00:00,80.48,18.346,36.895,29.535 +2020-08-20 02:15:00,77.16,17.660999999999998,36.895,29.535 +2020-08-20 02:30:00,74.84,18.802,36.895,29.535 +2020-08-20 02:45:00,76.18,19.485,36.895,29.535 +2020-08-20 03:00:00,77.16,20.230999999999998,36.565,29.535 +2020-08-20 03:15:00,76.8,20.024,36.565,29.535 +2020-08-20 03:30:00,77.13,19.705,36.565,29.535 +2020-08-20 03:45:00,81.08,19.352999999999998,36.565,29.535 +2020-08-20 04:00:00,82.53,23.513,37.263000000000005,29.535 +2020-08-20 04:15:00,85.3,28.87,37.263000000000005,29.535 +2020-08-20 04:30:00,89.78,26.456,37.263000000000005,29.535 +2020-08-20 04:45:00,92.89,26.572,37.263000000000005,29.535 +2020-08-20 05:00:00,100.42,38.428000000000004,40.412,29.535 +2020-08-20 05:15:00,97.56,44.992,40.412,29.535 +2020-08-20 05:30:00,104.41,40.54,40.412,29.535 +2020-08-20 05:45:00,114.19,38.008,40.412,29.535 +2020-08-20 06:00:00,118.96,38.368,49.825,29.535 +2020-08-20 06:15:00,118.57,38.561,49.825,29.535 +2020-08-20 06:30:00,115.56,38.058,49.825,29.535 +2020-08-20 06:45:00,116.37,40.343,49.825,29.535 +2020-08-20 07:00:00,117.94,40.379,61.082,29.535 +2020-08-20 07:15:00,121.74,41.48,61.082,29.535 +2020-08-20 07:30:00,117.74,39.907,61.082,29.535 +2020-08-20 07:45:00,117.37,40.691,61.082,29.535 +2020-08-20 08:00:00,119.5,37.708,53.961999999999996,29.535 +2020-08-20 08:15:00,119.23,40.032,53.961999999999996,29.535 +2020-08-20 08:30:00,117.61,40.457,53.961999999999996,29.535 +2020-08-20 08:45:00,119.25,42.288999999999994,53.961999999999996,29.535 +2020-08-20 09:00:00,121.78,37.311,50.06100000000001,29.535 +2020-08-20 09:15:00,121.69,36.431999999999995,50.06100000000001,29.535 +2020-08-20 09:30:00,113.46,39.24,50.06100000000001,29.535 +2020-08-20 09:45:00,113.9,41.778,50.06100000000001,29.535 +2020-08-20 10:00:00,119.89,38.274,47.68,29.535 +2020-08-20 10:15:00,119.66,39.247,47.68,29.535 +2020-08-20 10:30:00,118.06,39.114000000000004,47.68,29.535 +2020-08-20 10:45:00,111.47,40.330999999999996,47.68,29.535 +2020-08-20 11:00:00,110.59,38.582,45.93899999999999,29.535 +2020-08-20 11:15:00,113.71,39.62,45.93899999999999,29.535 +2020-08-20 11:30:00,113.77,40.45,45.93899999999999,29.535 +2020-08-20 11:45:00,113.94,41.301,45.93899999999999,29.535 +2020-08-20 12:00:00,110.52,37.588,43.648999999999994,29.535 +2020-08-20 12:15:00,113.88,36.965,43.648999999999994,29.535 +2020-08-20 12:30:00,114.93,36.016,43.648999999999994,29.535 +2020-08-20 12:45:00,114.05,36.791,43.648999999999994,29.535 +2020-08-20 13:00:00,113.61,36.317,42.801,29.535 +2020-08-20 13:15:00,115.95,37.122,42.801,29.535 +2020-08-20 13:30:00,127.37,35.669000000000004,42.801,29.535 +2020-08-20 13:45:00,125.7,35.538000000000004,42.801,29.535 +2020-08-20 14:00:00,120.24,36.696999999999996,43.24,29.535 +2020-08-20 14:15:00,116.22,35.878,43.24,29.535 +2020-08-20 14:30:00,117.36,34.991,43.24,29.535 +2020-08-20 14:45:00,120.32,35.696,43.24,29.535 +2020-08-20 15:00:00,118.94,36.713,45.04600000000001,29.535 +2020-08-20 15:15:00,113.51,34.626999999999995,45.04600000000001,29.535 +2020-08-20 15:30:00,112.59,33.437,45.04600000000001,29.535 +2020-08-20 15:45:00,117.13,31.774,45.04600000000001,29.535 +2020-08-20 16:00:00,112.87,34.106,46.568000000000005,29.535 +2020-08-20 16:15:00,107.6,34.041,46.568000000000005,29.535 +2020-08-20 16:30:00,113.61,33.817,46.568000000000005,29.535 +2020-08-20 16:45:00,113.75,31.721,46.568000000000005,29.535 +2020-08-20 17:00:00,116.5,34.345,50.618,29.535 +2020-08-20 17:15:00,117.63,35.172,50.618,29.535 +2020-08-20 17:30:00,118.34,34.506,50.618,29.535 +2020-08-20 17:45:00,121.4,34.099000000000004,50.618,29.535 +2020-08-20 18:00:00,121.03,36.616,52.806999999999995,29.535 +2020-08-20 18:15:00,118.78,36.42,52.806999999999995,29.535 +2020-08-20 18:30:00,118.33,34.802,52.806999999999995,29.535 +2020-08-20 18:45:00,113.93,37.711,52.806999999999995,29.535 +2020-08-20 19:00:00,110.67,38.834,53.464,29.535 +2020-08-20 19:15:00,108.18,37.775999999999996,53.464,29.535 +2020-08-20 19:30:00,108.15,36.714,53.464,29.535 +2020-08-20 19:45:00,108.46,36.23,53.464,29.535 +2020-08-20 20:00:00,104.07,33.314,56.753,29.535 +2020-08-20 20:15:00,102.81,33.14,56.753,29.535 +2020-08-20 20:30:00,101.22,33.266999999999996,56.753,29.535 +2020-08-20 20:45:00,100.86,33.449,56.753,29.535 +2020-08-20 21:00:00,93.78,31.909000000000002,52.506,29.535 +2020-08-20 21:15:00,95.92,33.44,52.506,29.535 +2020-08-20 21:30:00,88.69,33.784,52.506,29.535 +2020-08-20 21:45:00,90.87,33.941,52.506,29.535 +2020-08-20 22:00:00,83.08,31.343000000000004,48.163000000000004,29.535 +2020-08-20 22:15:00,86.43,33.224000000000004,48.163000000000004,29.535 +2020-08-20 22:30:00,83.99,28.158,48.163000000000004,29.535 +2020-08-20 22:45:00,83.77,25.084,48.163000000000004,29.535 +2020-08-20 23:00:00,75.0,23.035,42.379,29.535 +2020-08-20 23:15:00,77.96,22.035,42.379,29.535 +2020-08-20 23:30:00,75.78,21.557,42.379,29.535 +2020-08-20 23:45:00,78.91,20.704,42.379,29.535 +2020-08-21 00:00:00,75.42,19.239,38.505,29.535 +2020-08-21 00:15:00,75.15,20.131,38.505,29.535 +2020-08-21 00:30:00,73.55,19.534000000000002,38.505,29.535 +2020-08-21 00:45:00,74.33,20.096,38.505,29.535 +2020-08-21 01:00:00,73.08,19.484,37.004,29.535 +2020-08-21 01:15:00,73.78,18.242,37.004,29.535 +2020-08-21 01:30:00,73.77,17.834,37.004,29.535 +2020-08-21 01:45:00,80.71,17.328,37.004,29.535 +2020-08-21 02:00:00,81.21,18.083,36.098,29.535 +2020-08-21 02:15:00,77.73,17.374000000000002,36.098,29.535 +2020-08-21 02:30:00,74.39,19.24,36.098,29.535 +2020-08-21 02:45:00,75.1,19.294,36.098,29.535 +2020-08-21 03:00:00,77.31,20.749000000000002,36.561,29.535 +2020-08-21 03:15:00,77.03,19.445,36.561,29.535 +2020-08-21 03:30:00,76.96,18.914,36.561,29.535 +2020-08-21 03:45:00,83.38,19.355,36.561,29.535 +2020-08-21 04:00:00,88.24,23.659000000000002,37.355,29.535 +2020-08-21 04:15:00,91.71,27.607,37.355,29.535 +2020-08-21 04:30:00,90.26,26.066,37.355,29.535 +2020-08-21 04:45:00,91.87,25.607,37.355,29.535 +2020-08-21 05:00:00,99.2,37.098,40.285,29.535 +2020-08-21 05:15:00,102.18,44.583,40.285,29.535 +2020-08-21 05:30:00,106.7,40.306,40.285,29.535 +2020-08-21 05:45:00,115.67,37.338,40.285,29.535 +2020-08-21 06:00:00,123.11,37.869,52.378,29.535 +2020-08-21 06:15:00,120.91,38.234,52.378,29.535 +2020-08-21 06:30:00,123.41,37.684,52.378,29.535 +2020-08-21 06:45:00,127.16,39.852,52.378,29.535 +2020-08-21 07:00:00,129.29,40.475,60.891999999999996,29.535 +2020-08-21 07:15:00,124.88,42.409,60.891999999999996,29.535 +2020-08-21 07:30:00,117.88,39.092,60.891999999999996,29.535 +2020-08-21 07:45:00,116.37,39.691,60.891999999999996,29.535 +2020-08-21 08:00:00,118.09,37.425,53.652,29.535 +2020-08-21 08:15:00,119.83,40.330999999999996,53.652,29.535 +2020-08-21 08:30:00,122.73,40.656,53.652,29.535 +2020-08-21 08:45:00,121.5,42.353,53.652,29.535 +2020-08-21 09:00:00,118.6,35.285,51.456,29.535 +2020-08-21 09:15:00,120.12,36.135999999999996,51.456,29.535 +2020-08-21 09:30:00,126.02,38.305,51.456,29.535 +2020-08-21 09:45:00,122.6,41.172,51.456,29.535 +2020-08-21 10:00:00,120.93,37.529,49.4,29.535 +2020-08-21 10:15:00,121.21,38.27,49.4,29.535 +2020-08-21 10:30:00,124.91,38.634,49.4,29.535 +2020-08-21 10:45:00,126.82,39.759,49.4,29.535 +2020-08-21 11:00:00,123.43,38.259,48.773,29.535 +2020-08-21 11:15:00,121.31,38.306,48.773,29.535 +2020-08-21 11:30:00,114.97,38.778,48.773,29.535 +2020-08-21 11:45:00,117.42,38.736,48.773,29.535 +2020-08-21 12:00:00,122.33,35.384,46.033,29.535 +2020-08-21 12:15:00,112.57,34.199,46.033,29.535 +2020-08-21 12:30:00,103.79,33.37,46.033,29.535 +2020-08-21 12:45:00,111.23,33.400999999999996,46.033,29.535 +2020-08-21 13:00:00,109.6,33.441,44.38399999999999,29.535 +2020-08-21 13:15:00,108.67,34.389,44.38399999999999,29.535 +2020-08-21 13:30:00,105.43,33.662,44.38399999999999,29.535 +2020-08-21 13:45:00,105.72,33.823,44.38399999999999,29.535 +2020-08-21 14:00:00,109.3,34.238,43.162,29.535 +2020-08-21 14:15:00,119.63,33.829,43.162,29.535 +2020-08-21 14:30:00,116.81,34.333,43.162,29.535 +2020-08-21 14:45:00,115.99,34.4,43.162,29.535 +2020-08-21 15:00:00,108.46,35.311,44.91,29.535 +2020-08-21 15:15:00,102.35,33.022,44.91,29.535 +2020-08-21 15:30:00,102.98,31.35,44.91,29.535 +2020-08-21 15:45:00,109.7,30.392,44.91,29.535 +2020-08-21 16:00:00,115.52,31.933000000000003,47.489,29.535 +2020-08-21 16:15:00,117.53,32.324,47.489,29.535 +2020-08-21 16:30:00,114.19,31.936,47.489,29.535 +2020-08-21 16:45:00,114.63,29.162,47.489,29.535 +2020-08-21 17:00:00,114.87,33.342,52.047,29.535 +2020-08-21 17:15:00,112.6,34.046,52.047,29.535 +2020-08-21 17:30:00,111.56,33.547,52.047,29.535 +2020-08-21 17:45:00,119.81,33.025999999999996,52.047,29.535 +2020-08-21 18:00:00,118.61,35.549,53.306000000000004,29.535 +2020-08-21 18:15:00,111.71,34.506,53.306000000000004,29.535 +2020-08-21 18:30:00,109.02,32.798,53.306000000000004,29.535 +2020-08-21 18:45:00,109.32,36.089,53.306000000000004,29.535 +2020-08-21 19:00:00,104.75,37.998000000000005,53.516000000000005,29.535 +2020-08-21 19:15:00,103.22,37.463,53.516000000000005,29.535 +2020-08-21 19:30:00,105.19,36.463,53.516000000000005,29.535 +2020-08-21 19:45:00,109.91,35.064,53.516000000000005,29.535 +2020-08-21 20:00:00,108.19,32.015,57.88,29.535 +2020-08-21 20:15:00,105.08,32.567,57.88,29.535 +2020-08-21 20:30:00,99.01,32.235,57.88,29.535 +2020-08-21 20:45:00,95.41,31.636,57.88,29.535 +2020-08-21 21:00:00,90.21,31.29,53.32,29.535 +2020-08-21 21:15:00,89.55,34.330999999999996,53.32,29.535 +2020-08-21 21:30:00,92.66,34.534,53.32,29.535 +2020-08-21 21:45:00,91.64,34.841,53.32,29.535 +2020-08-21 22:00:00,87.68,32.082,48.074,29.535 +2020-08-21 22:15:00,84.08,33.724000000000004,48.074,29.535 +2020-08-21 22:30:00,86.02,32.883,48.074,29.535 +2020-08-21 22:45:00,85.3,30.55,48.074,29.535 +2020-08-21 23:00:00,77.83,30.086,41.306999999999995,29.535 +2020-08-21 23:15:00,71.95,27.67,41.306999999999995,29.535 +2020-08-21 23:30:00,73.74,25.559,41.306999999999995,29.535 +2020-08-21 23:45:00,71.29,24.608,41.306999999999995,29.535 +2020-08-22 00:00:00,69.81,20.534000000000002,40.227,29.423000000000002 +2020-08-22 00:15:00,69.02,20.857,40.227,29.423000000000002 +2020-08-22 00:30:00,67.89,19.826,40.227,29.423000000000002 +2020-08-22 00:45:00,72.34,19.723,40.227,29.423000000000002 +2020-08-22 01:00:00,75.79,19.339000000000002,36.303000000000004,29.423000000000002 +2020-08-22 01:15:00,76.02,18.655,36.303000000000004,29.423000000000002 +2020-08-22 01:30:00,75.61,17.533,36.303000000000004,29.423000000000002 +2020-08-22 01:45:00,73.35,18.16,36.303000000000004,29.423000000000002 +2020-08-22 02:00:00,74.97,18.018,33.849000000000004,29.423000000000002 +2020-08-22 02:15:00,75.4,16.616,33.849000000000004,29.423000000000002 +2020-08-22 02:30:00,69.28,17.753,33.849000000000004,29.423000000000002 +2020-08-22 02:45:00,66.05,18.517,33.849000000000004,29.423000000000002 +2020-08-22 03:00:00,72.84,18.707,33.149,29.423000000000002 +2020-08-22 03:15:00,73.03,16.8,33.149,29.423000000000002 +2020-08-22 03:30:00,67.68,16.592,33.149,29.423000000000002 +2020-08-22 03:45:00,66.88,18.373,33.149,29.423000000000002 +2020-08-22 04:00:00,69.12,21.037,32.501,29.423000000000002 +2020-08-22 04:15:00,67.9,24.219,32.501,29.423000000000002 +2020-08-22 04:30:00,68.89,21.195999999999998,32.501,29.423000000000002 +2020-08-22 04:45:00,75.53,20.994,32.501,29.423000000000002 +2020-08-22 05:00:00,76.39,25.175,31.648000000000003,29.423000000000002 +2020-08-22 05:15:00,77.34,23.013,31.648000000000003,29.423000000000002 +2020-08-22 05:30:00,70.85,20.104,31.648000000000003,29.423000000000002 +2020-08-22 05:45:00,75.88,21.084,31.648000000000003,29.423000000000002 +2020-08-22 06:00:00,79.9,31.51,32.552,29.423000000000002 +2020-08-22 06:15:00,81.93,38.769,32.552,29.423000000000002 +2020-08-22 06:30:00,78.71,35.705999999999996,32.552,29.423000000000002 +2020-08-22 06:45:00,78.42,35.068000000000005,32.552,29.423000000000002 +2020-08-22 07:00:00,81.49,34.53,35.181999999999995,29.423000000000002 +2020-08-22 07:15:00,83.52,35.321999999999996,35.181999999999995,29.423000000000002 +2020-08-22 07:30:00,88.71,33.359,35.181999999999995,29.423000000000002 +2020-08-22 07:45:00,83.28,34.743,35.181999999999995,29.423000000000002 +2020-08-22 08:00:00,87.36,33.074,40.35,29.423000000000002 +2020-08-22 08:15:00,83.56,35.699,40.35,29.423000000000002 +2020-08-22 08:30:00,83.76,35.982,40.35,29.423000000000002 +2020-08-22 08:45:00,86.8,38.474000000000004,40.35,29.423000000000002 +2020-08-22 09:00:00,91.83,34.203,42.292,29.423000000000002 +2020-08-22 09:15:00,94.77,35.471,42.292,29.423000000000002 +2020-08-22 09:30:00,93.76,38.058,42.292,29.423000000000002 +2020-08-22 09:45:00,86.84,40.493,42.292,29.423000000000002 +2020-08-22 10:00:00,92.6,37.429,40.084,29.423000000000002 +2020-08-22 10:15:00,82.43,38.467,40.084,29.423000000000002 +2020-08-22 10:30:00,84.17,38.509,40.084,29.423000000000002 +2020-08-22 10:45:00,83.75,39.282,40.084,29.423000000000002 +2020-08-22 11:00:00,82.45,37.674,36.966,29.423000000000002 +2020-08-22 11:15:00,78.36,38.518,36.966,29.423000000000002 +2020-08-22 11:30:00,80.55,39.266,36.966,29.423000000000002 +2020-08-22 11:45:00,84.46,39.841,36.966,29.423000000000002 +2020-08-22 12:00:00,80.83,36.912,35.19,29.423000000000002 +2020-08-22 12:15:00,81.73,36.481,35.19,29.423000000000002 +2020-08-22 12:30:00,83.66,35.539,35.19,29.423000000000002 +2020-08-22 12:45:00,83.6,36.244,35.19,29.423000000000002 +2020-08-22 13:00:00,80.22,35.459,32.277,29.423000000000002 +2020-08-22 13:15:00,81.83,35.946,32.277,29.423000000000002 +2020-08-22 13:30:00,79.68,35.398,32.277,29.423000000000002 +2020-08-22 13:45:00,76.05,34.327,32.277,29.423000000000002 +2020-08-22 14:00:00,76.01,34.762,31.436999999999998,29.423000000000002 +2020-08-22 14:15:00,76.05,33.229,31.436999999999998,29.423000000000002 +2020-08-22 14:30:00,73.17,33.400999999999996,31.436999999999998,29.423000000000002 +2020-08-22 14:45:00,73.18,33.904,31.436999999999998,29.423000000000002 +2020-08-22 15:00:00,73.62,35.177,33.493,29.423000000000002 +2020-08-22 15:15:00,75.64,33.522,33.493,29.423000000000002 +2020-08-22 15:30:00,75.93,32.013000000000005,33.493,29.423000000000002 +2020-08-22 15:45:00,75.66,30.217,33.493,29.423000000000002 +2020-08-22 16:00:00,74.81,33.667,36.593,29.423000000000002 +2020-08-22 16:15:00,75.25,33.234,36.593,29.423000000000002 +2020-08-22 16:30:00,75.96,33.058,36.593,29.423000000000002 +2020-08-22 16:45:00,78.59,30.281,36.593,29.423000000000002 +2020-08-22 17:00:00,80.01,33.37,42.049,29.423000000000002 +2020-08-22 17:15:00,81.17,32.102,42.049,29.423000000000002 +2020-08-22 17:30:00,80.84,31.496,42.049,29.423000000000002 +2020-08-22 17:45:00,79.99,31.468000000000004,42.049,29.423000000000002 +2020-08-22 18:00:00,83.24,35.213,43.755,29.423000000000002 +2020-08-22 18:15:00,78.95,35.67,43.755,29.423000000000002 +2020-08-22 18:30:00,78.74,35.183,43.755,29.423000000000002 +2020-08-22 18:45:00,79.68,35.374,43.755,29.423000000000002 +2020-08-22 19:00:00,79.36,35.782,44.492,29.423000000000002 +2020-08-22 19:15:00,78.0,34.344,44.492,29.423000000000002 +2020-08-22 19:30:00,78.76,34.03,44.492,29.423000000000002 +2020-08-22 19:45:00,78.12,34.227,44.492,29.423000000000002 +2020-08-22 20:00:00,75.97,31.935,40.896,29.423000000000002 +2020-08-22 20:15:00,75.27,31.884,40.896,29.423000000000002 +2020-08-22 20:30:00,77.26,30.746,40.896,29.423000000000002 +2020-08-22 20:45:00,74.04,31.799,40.896,29.423000000000002 +2020-08-22 21:00:00,72.99,30.090999999999998,39.056,29.423000000000002 +2020-08-22 21:15:00,69.37,32.803000000000004,39.056,29.423000000000002 +2020-08-22 21:30:00,65.99,33.161,39.056,29.423000000000002 +2020-08-22 21:45:00,65.64,32.949,39.056,29.423000000000002 +2020-08-22 22:00:00,62.66,30.133000000000003,38.478,29.423000000000002 +2020-08-22 22:15:00,62.96,31.954,38.478,29.423000000000002 +2020-08-22 22:30:00,60.65,30.666999999999998,38.478,29.423000000000002 +2020-08-22 22:45:00,60.45,28.648000000000003,38.478,29.423000000000002 +2020-08-22 23:00:00,57.7,27.596,32.953,29.423000000000002 +2020-08-22 23:15:00,56.86,25.565,32.953,29.423000000000002 +2020-08-22 23:30:00,56.41,25.7,32.953,29.423000000000002 +2020-08-22 23:45:00,56.12,25.105,32.953,29.423000000000002 +2020-08-23 00:00:00,54.05,21.918000000000003,28.584,29.423000000000002 +2020-08-23 00:15:00,54.14,21.205,28.584,29.423000000000002 +2020-08-23 00:30:00,51.82,20.051,28.584,29.423000000000002 +2020-08-23 00:45:00,52.75,19.846,28.584,29.423000000000002 +2020-08-23 01:00:00,50.79,19.685,26.419,29.423000000000002 +2020-08-23 01:15:00,51.91,18.892,26.419,29.423000000000002 +2020-08-23 01:30:00,52.0,17.651,26.419,29.423000000000002 +2020-08-23 01:45:00,51.21,17.939,26.419,29.423000000000002 +2020-08-23 02:00:00,49.98,17.88,25.335,29.423000000000002 +2020-08-23 02:15:00,49.28,17.148,25.335,29.423000000000002 +2020-08-23 02:30:00,49.62,18.421,25.335,29.423000000000002 +2020-08-23 02:45:00,50.01,18.914,25.335,29.423000000000002 +2020-08-23 03:00:00,49.63,19.677,24.805,29.423000000000002 +2020-08-23 03:15:00,50.61,18.027,24.805,29.423000000000002 +2020-08-23 03:30:00,51.05,17.143,24.805,29.423000000000002 +2020-08-23 03:45:00,51.04,18.202,24.805,29.423000000000002 +2020-08-23 04:00:00,52.83,20.862,25.772,29.423000000000002 +2020-08-23 04:15:00,53.3,23.68,25.772,29.423000000000002 +2020-08-23 04:30:00,53.83,21.873,25.772,29.423000000000002 +2020-08-23 04:45:00,55.23,21.249000000000002,25.772,29.423000000000002 +2020-08-23 05:00:00,53.76,25.686999999999998,25.971999999999998,29.423000000000002 +2020-08-23 05:15:00,53.31,22.923000000000002,25.971999999999998,29.423000000000002 +2020-08-23 05:30:00,52.98,19.655,25.971999999999998,29.423000000000002 +2020-08-23 05:45:00,54.2,20.352,25.971999999999998,29.423000000000002 +2020-08-23 06:00:00,55.52,28.631999999999998,26.026,29.423000000000002 +2020-08-23 06:15:00,56.17,36.676,26.026,29.423000000000002 +2020-08-23 06:30:00,57.6,32.996,26.026,29.423000000000002 +2020-08-23 06:45:00,58.37,31.48,26.026,29.423000000000002 +2020-08-23 07:00:00,60.01,31.19,27.396,29.423000000000002 +2020-08-23 07:15:00,59.93,30.531,27.396,29.423000000000002 +2020-08-23 07:30:00,61.88,29.76,27.396,29.423000000000002 +2020-08-23 07:45:00,62.61,31.128,27.396,29.423000000000002 +2020-08-23 08:00:00,63.28,30.125999999999998,30.791999999999998,29.423000000000002 +2020-08-23 08:15:00,61.72,33.788000000000004,30.791999999999998,29.423000000000002 +2020-08-23 08:30:00,60.05,34.823,30.791999999999998,29.423000000000002 +2020-08-23 08:45:00,60.37,37.19,30.791999999999998,29.423000000000002 +2020-08-23 09:00:00,57.88,32.838,32.482,29.423000000000002 +2020-08-23 09:15:00,58.54,33.64,32.482,29.423000000000002 +2020-08-23 09:30:00,61.36,36.603,32.482,29.423000000000002 +2020-08-23 09:45:00,64.5,39.924,32.482,29.423000000000002 +2020-08-23 10:00:00,65.45,37.297,31.951,29.423000000000002 +2020-08-23 10:15:00,63.8,38.42,31.951,29.423000000000002 +2020-08-23 10:30:00,69.36,38.641,31.951,29.423000000000002 +2020-08-23 10:45:00,69.5,40.378,31.951,29.423000000000002 +2020-08-23 11:00:00,68.42,38.444,33.619,29.423000000000002 +2020-08-23 11:15:00,68.97,38.867,33.619,29.423000000000002 +2020-08-23 11:30:00,65.45,40.113,33.619,29.423000000000002 +2020-08-23 11:45:00,65.73,40.89,33.619,29.423000000000002 +2020-08-23 12:00:00,62.07,38.953,30.975,29.423000000000002 +2020-08-23 12:15:00,60.77,37.865,30.975,29.423000000000002 +2020-08-23 12:30:00,57.24,37.19,30.975,29.423000000000002 +2020-08-23 12:45:00,59.1,37.333,30.975,29.423000000000002 +2020-08-23 13:00:00,54.41,36.266999999999996,27.956999999999997,29.423000000000002 +2020-08-23 13:15:00,56.3,36.116,27.956999999999997,29.423000000000002 +2020-08-23 13:30:00,54.14,34.552,27.956999999999997,29.423000000000002 +2020-08-23 13:45:00,57.34,34.536,27.956999999999997,29.423000000000002 +2020-08-23 14:00:00,54.95,36.039,25.555999999999997,29.423000000000002 +2020-08-23 14:15:00,55.93,34.866,25.555999999999997,29.423000000000002 +2020-08-23 14:30:00,53.34,33.878,25.555999999999997,29.423000000000002 +2020-08-23 14:45:00,52.83,33.415,25.555999999999997,29.423000000000002 +2020-08-23 15:00:00,52.56,34.832,26.271,29.423000000000002 +2020-08-23 15:15:00,52.26,32.414,26.271,29.423000000000002 +2020-08-23 15:30:00,53.99,30.730999999999998,26.271,29.423000000000002 +2020-08-23 15:45:00,55.72,29.178,26.271,29.423000000000002 +2020-08-23 16:00:00,60.31,31.070999999999998,30.369,29.423000000000002 +2020-08-23 16:15:00,60.83,30.884,30.369,29.423000000000002 +2020-08-23 16:30:00,63.75,31.62,30.369,29.423000000000002 +2020-08-23 16:45:00,67.7,28.939,30.369,29.423000000000002 +2020-08-23 17:00:00,71.4,32.325,38.787,29.423000000000002 +2020-08-23 17:15:00,73.13,32.479,38.787,29.423000000000002 +2020-08-23 17:30:00,74.84,32.615,38.787,29.423000000000002 +2020-08-23 17:45:00,76.19,32.934,38.787,29.423000000000002 +2020-08-23 18:00:00,78.8,37.244,41.886,29.423000000000002 +2020-08-23 18:15:00,78.18,37.311,41.886,29.423000000000002 +2020-08-23 18:30:00,76.17,36.661,41.886,29.423000000000002 +2020-08-23 18:45:00,77.31,36.91,41.886,29.423000000000002 +2020-08-23 19:00:00,79.28,39.316,42.91,29.423000000000002 +2020-08-23 19:15:00,79.78,36.865,42.91,29.423000000000002 +2020-08-23 19:30:00,81.08,36.332,42.91,29.423000000000002 +2020-08-23 19:45:00,80.3,36.093,42.91,29.423000000000002 +2020-08-23 20:00:00,79.94,33.946999999999996,42.148999999999994,29.423000000000002 +2020-08-23 20:15:00,79.46,33.751,42.148999999999994,29.423000000000002 +2020-08-23 20:30:00,78.65,33.303000000000004,42.148999999999994,29.423000000000002 +2020-08-23 20:45:00,78.43,32.828,42.148999999999994,29.423000000000002 +2020-08-23 21:00:00,77.51,31.070999999999998,40.955999999999996,29.423000000000002 +2020-08-23 21:15:00,78.17,33.514,40.955999999999996,29.423000000000002 +2020-08-23 21:30:00,74.81,33.278,40.955999999999996,29.423000000000002 +2020-08-23 21:45:00,74.82,33.356,40.955999999999996,29.423000000000002 +2020-08-23 22:00:00,70.62,32.385999999999996,39.873000000000005,29.423000000000002 +2020-08-23 22:15:00,71.54,32.687,39.873000000000005,29.423000000000002 +2020-08-23 22:30:00,70.08,30.976999999999997,39.873000000000005,29.423000000000002 +2020-08-23 22:45:00,70.5,27.828000000000003,39.873000000000005,29.423000000000002 +2020-08-23 23:00:00,65.77,26.604,35.510999999999996,29.423000000000002 +2020-08-23 23:15:00,68.03,25.668000000000003,35.510999999999996,29.423000000000002 +2020-08-23 23:30:00,68.09,25.31,35.510999999999996,29.423000000000002 +2020-08-23 23:45:00,67.17,24.844,35.510999999999996,29.423000000000002 +2020-08-24 00:00:00,61.92,23.346999999999998,33.475,29.535 +2020-08-24 00:15:00,61.51,23.287,33.475,29.535 +2020-08-24 00:30:00,62.23,21.79,33.475,29.535 +2020-08-24 00:45:00,64.91,21.246,33.475,29.535 +2020-08-24 01:00:00,63.88,21.413,33.111,29.535 +2020-08-24 01:15:00,64.28,20.656,33.111,29.535 +2020-08-24 01:30:00,63.48,19.754,33.111,29.535 +2020-08-24 01:45:00,64.2,19.951,33.111,29.535 +2020-08-24 02:00:00,63.59,20.304000000000002,32.358000000000004,29.535 +2020-08-24 02:15:00,65.17,18.646,32.358000000000004,29.535 +2020-08-24 02:30:00,66.78,20.039,32.358000000000004,29.535 +2020-08-24 02:45:00,69.86,20.409000000000002,32.358000000000004,29.535 +2020-08-24 03:00:00,74.42,21.589000000000002,30.779,29.535 +2020-08-24 03:15:00,75.12,20.570999999999998,30.779,29.535 +2020-08-24 03:30:00,69.75,20.315,30.779,29.535 +2020-08-24 03:45:00,71.44,20.962,30.779,29.535 +2020-08-24 04:00:00,77.38,26.358,31.416,29.535 +2020-08-24 04:15:00,84.0,31.753,31.416,29.535 +2020-08-24 04:30:00,88.97,29.471,31.416,29.535 +2020-08-24 04:45:00,90.63,29.182,31.416,29.535 +2020-08-24 05:00:00,92.59,39.953,37.221,29.535 +2020-08-24 05:15:00,98.51,46.222,37.221,29.535 +2020-08-24 05:30:00,97.7,41.567,37.221,29.535 +2020-08-24 05:45:00,106.99,39.55,37.221,29.535 +2020-08-24 06:00:00,114.75,38.809,51.891000000000005,29.535 +2020-08-24 06:15:00,115.62,38.935,51.891000000000005,29.535 +2020-08-24 06:30:00,114.57,38.699,51.891000000000005,29.535 +2020-08-24 06:45:00,111.02,41.791000000000004,51.891000000000005,29.535 +2020-08-24 07:00:00,116.17,41.681999999999995,62.282,29.535 +2020-08-24 07:15:00,117.57,43.071000000000005,62.282,29.535 +2020-08-24 07:30:00,116.26,41.563,62.282,29.535 +2020-08-24 07:45:00,111.86,43.251999999999995,62.282,29.535 +2020-08-24 08:00:00,112.24,40.347,54.102,29.535 +2020-08-24 08:15:00,110.91,42.93600000000001,54.102,29.535 +2020-08-24 08:30:00,107.9,43.183,54.102,29.535 +2020-08-24 08:45:00,106.7,45.798,54.102,29.535 +2020-08-24 09:00:00,103.97,40.516999999999996,50.917,29.535 +2020-08-24 09:15:00,112.8,39.77,50.917,29.535 +2020-08-24 09:30:00,109.89,41.897,50.917,29.535 +2020-08-24 09:45:00,115.07,43.108000000000004,50.917,29.535 +2020-08-24 10:00:00,107.02,40.842,49.718999999999994,29.535 +2020-08-24 10:15:00,116.52,41.815,49.718999999999994,29.535 +2020-08-24 10:30:00,119.9,41.635,49.718999999999994,29.535 +2020-08-24 10:45:00,119.31,41.896,49.718999999999994,29.535 +2020-08-24 11:00:00,116.15,40.231,49.833999999999996,29.535 +2020-08-24 11:15:00,121.04,40.848,49.833999999999996,29.535 +2020-08-24 11:30:00,121.33,42.687,49.833999999999996,29.535 +2020-08-24 11:45:00,120.83,43.92,49.833999999999996,29.535 +2020-08-24 12:00:00,115.38,40.277,47.832,29.535 +2020-08-24 12:15:00,118.08,39.281,47.832,29.535 +2020-08-24 12:30:00,110.62,37.626999999999995,47.832,29.535 +2020-08-24 12:45:00,109.16,37.7,47.832,29.535 +2020-08-24 13:00:00,106.38,37.482,48.03,29.535 +2020-08-24 13:15:00,100.21,36.554,48.03,29.535 +2020-08-24 13:30:00,103.29,35.159,48.03,29.535 +2020-08-24 13:45:00,105.95,35.975,48.03,29.535 +2020-08-24 14:00:00,103.3,36.626999999999995,48.157,29.535 +2020-08-24 14:15:00,99.66,36.027,48.157,29.535 +2020-08-24 14:30:00,103.17,34.923,48.157,29.535 +2020-08-24 14:45:00,110.25,36.366,48.157,29.535 +2020-08-24 15:00:00,115.96,37.335,48.897,29.535 +2020-08-24 15:15:00,112.33,34.433,48.897,29.535 +2020-08-24 15:30:00,104.1,33.501,48.897,29.535 +2020-08-24 15:45:00,99.37,31.523000000000003,48.897,29.535 +2020-08-24 16:00:00,108.59,34.434,51.446000000000005,29.535 +2020-08-24 16:15:00,110.63,34.338,51.446000000000005,29.535 +2020-08-24 16:30:00,113.83,34.496,51.446000000000005,29.535 +2020-08-24 16:45:00,113.22,31.912,51.446000000000005,29.535 +2020-08-24 17:00:00,108.34,34.234,57.507,29.535 +2020-08-24 17:15:00,106.39,34.789,57.507,29.535 +2020-08-24 17:30:00,107.38,34.6,57.507,29.535 +2020-08-24 17:45:00,112.93,34.635999999999996,57.507,29.535 +2020-08-24 18:00:00,113.44,37.981,57.896,29.535 +2020-08-24 18:15:00,110.9,36.451,57.896,29.535 +2020-08-24 18:30:00,108.43,35.108000000000004,57.896,29.535 +2020-08-24 18:45:00,106.18,38.216,57.896,29.535 +2020-08-24 19:00:00,105.21,40.431,57.891999999999996,29.535 +2020-08-24 19:15:00,109.56,39.202,57.891999999999996,29.535 +2020-08-24 19:30:00,106.94,38.328,57.891999999999996,29.535 +2020-08-24 19:45:00,101.03,37.554,57.891999999999996,29.535 +2020-08-24 20:00:00,100.29,34.311,64.57300000000001,29.535 +2020-08-24 20:15:00,98.55,35.45,64.57300000000001,29.535 +2020-08-24 20:30:00,97.25,35.505,64.57300000000001,29.535 +2020-08-24 20:45:00,101.29,35.153,64.57300000000001,29.535 +2020-08-24 21:00:00,99.14,32.809,59.431999999999995,29.535 +2020-08-24 21:15:00,93.54,35.671,59.431999999999995,29.535 +2020-08-24 21:30:00,84.27,35.83,59.431999999999995,29.535 +2020-08-24 21:45:00,83.92,35.689,59.431999999999995,29.535 +2020-08-24 22:00:00,75.13,32.900999999999996,51.519,29.535 +2020-08-24 22:15:00,79.97,35.023,51.519,29.535 +2020-08-24 22:30:00,74.93,29.565,51.519,29.535 +2020-08-24 22:45:00,75.97,26.509,51.519,29.535 +2020-08-24 23:00:00,72.01,25.331,44.501000000000005,29.535 +2020-08-24 23:15:00,73.98,22.824,44.501000000000005,29.535 +2020-08-24 23:30:00,73.76,22.38,44.501000000000005,29.535 +2020-08-24 23:45:00,72.64,21.371,44.501000000000005,29.535 +2020-08-25 00:00:00,71.72,21.445,44.522,29.535 +2020-08-25 00:15:00,71.62,22.142,44.522,29.535 +2020-08-25 00:30:00,70.03,21.316999999999997,44.522,29.535 +2020-08-25 00:45:00,71.88,21.523000000000003,44.522,29.535 +2020-08-25 01:00:00,71.19,21.183000000000003,41.441,29.535 +2020-08-25 01:15:00,72.36,20.55,41.441,29.535 +2020-08-25 01:30:00,70.95,19.538,41.441,29.535 +2020-08-25 01:45:00,71.76,19.262,41.441,29.535 +2020-08-25 02:00:00,70.7,19.179000000000002,40.203,29.535 +2020-08-25 02:15:00,69.84,18.6,40.203,29.535 +2020-08-25 02:30:00,70.82,19.628,40.203,29.535 +2020-08-25 02:45:00,70.92,20.293,40.203,29.535 +2020-08-25 03:00:00,72.33,20.983,39.536,29.535 +2020-08-25 03:15:00,72.62,20.854,39.536,29.535 +2020-08-25 03:30:00,75.29,20.564,39.536,29.535 +2020-08-25 03:45:00,77.05,20.184,39.536,29.535 +2020-08-25 04:00:00,84.41,24.456,40.759,29.535 +2020-08-25 04:15:00,89.97,29.886,40.759,29.535 +2020-08-25 04:30:00,92.14,27.509,40.759,29.535 +2020-08-25 04:45:00,92.03,27.641,40.759,29.535 +2020-08-25 05:00:00,99.0,39.819,43.623999999999995,29.535 +2020-08-25 05:15:00,107.55,46.723,43.623999999999995,29.535 +2020-08-25 05:30:00,109.53,42.256,43.623999999999995,29.535 +2020-08-25 05:45:00,114.17,39.54,43.623999999999995,29.535 +2020-08-25 06:00:00,113.12,39.746,52.684,29.535 +2020-08-25 06:15:00,114.58,40.044000000000004,52.684,29.535 +2020-08-25 06:30:00,116.2,39.524,52.684,29.535 +2020-08-25 06:45:00,116.67,41.817,52.684,29.535 +2020-08-25 07:00:00,123.26,41.85,62.676,29.535 +2020-08-25 07:15:00,127.76,43.023,62.676,29.535 +2020-08-25 07:30:00,128.42,41.568000000000005,62.676,29.535 +2020-08-25 07:45:00,121.39,42.361999999999995,62.676,29.535 +2020-08-25 08:00:00,117.12,39.391,56.161,29.535 +2020-08-25 08:15:00,112.74,41.563,56.161,29.535 +2020-08-25 08:30:00,113.5,41.963,56.161,29.535 +2020-08-25 08:45:00,116.16,43.723,56.161,29.535 +2020-08-25 09:00:00,118.01,38.784,52.132,29.535 +2020-08-25 09:15:00,126.14,37.858000000000004,52.132,29.535 +2020-08-25 09:30:00,124.08,40.582,52.132,29.535 +2020-08-25 09:45:00,118.84,43.004,52.132,29.535 +2020-08-25 10:00:00,117.53,39.493,51.032,29.535 +2020-08-25 10:15:00,121.68,40.346,51.032,29.535 +2020-08-25 10:30:00,120.94,40.177,51.032,29.535 +2020-08-25 10:45:00,113.26,41.349,51.032,29.535 +2020-08-25 11:00:00,109.61,39.652,51.085,29.535 +2020-08-25 11:15:00,117.1,40.645,51.085,29.535 +2020-08-25 11:30:00,122.66,41.457,51.085,29.535 +2020-08-25 11:45:00,124.89,42.236000000000004,51.085,29.535 +2020-08-25 12:00:00,115.6,38.455,49.049,29.535 +2020-08-25 12:15:00,113.66,37.78,49.049,29.535 +2020-08-25 12:30:00,120.05,36.912,49.049,29.535 +2020-08-25 12:45:00,118.16,37.631,49.049,29.535 +2020-08-25 13:00:00,117.18,37.067,49.722,29.535 +2020-08-25 13:15:00,108.95,37.815,49.722,29.535 +2020-08-25 13:30:00,104.14,36.343,49.722,29.535 +2020-08-25 13:45:00,104.85,36.264,49.722,29.535 +2020-08-25 14:00:00,109.46,37.303000000000004,49.565,29.535 +2020-08-25 14:15:00,108.21,36.518,49.565,29.535 +2020-08-25 14:30:00,106.98,35.711,49.565,29.535 +2020-08-25 14:45:00,101.38,36.424,49.565,29.535 +2020-08-25 15:00:00,98.49,37.22,51.108999999999995,29.535 +2020-08-25 15:15:00,103.6,35.174,51.108999999999995,29.535 +2020-08-25 15:30:00,106.69,34.051,51.108999999999995,29.535 +2020-08-25 15:45:00,107.37,32.419000000000004,51.108999999999995,29.535 +2020-08-25 16:00:00,106.02,34.625,52.725,29.535 +2020-08-25 16:15:00,105.04,34.601,52.725,29.535 +2020-08-25 16:30:00,111.46,34.395,52.725,29.535 +2020-08-25 16:45:00,113.59,32.505,52.725,29.535 +2020-08-25 17:00:00,115.04,34.974000000000004,58.031000000000006,29.535 +2020-08-25 17:15:00,109.32,35.943000000000005,58.031000000000006,29.535 +2020-08-25 17:30:00,115.28,35.338,58.031000000000006,29.535 +2020-08-25 17:45:00,115.65,35.104,58.031000000000006,29.535 +2020-08-25 18:00:00,116.18,37.575,58.338,29.535 +2020-08-25 18:15:00,111.28,37.442,58.338,29.535 +2020-08-25 18:30:00,110.08,35.876999999999995,58.338,29.535 +2020-08-25 18:45:00,108.95,38.782,58.338,29.535 +2020-08-25 19:00:00,112.98,39.938,58.464,29.535 +2020-08-25 19:15:00,115.45,38.88,58.464,29.535 +2020-08-25 19:30:00,112.47,37.827,58.464,29.535 +2020-08-25 19:45:00,107.22,37.361999999999995,58.464,29.535 +2020-08-25 20:00:00,101.92,34.48,63.708,29.535 +2020-08-25 20:15:00,107.45,34.316,63.708,29.535 +2020-08-25 20:30:00,106.27,34.363,63.708,29.535 +2020-08-25 20:45:00,103.99,34.378,63.708,29.535 +2020-08-25 21:00:00,94.4,32.854,57.06399999999999,29.535 +2020-08-25 21:15:00,95.17,34.338,57.06399999999999,29.535 +2020-08-25 21:30:00,89.29,34.676,57.06399999999999,29.535 +2020-08-25 21:45:00,87.19,34.677,57.06399999999999,29.535 +2020-08-25 22:00:00,78.39,31.979,52.831,29.535 +2020-08-25 22:15:00,81.0,33.783,52.831,29.535 +2020-08-25 22:30:00,76.91,28.561,52.831,29.535 +2020-08-25 22:45:00,80.86,25.493000000000002,52.831,29.535 +2020-08-25 23:00:00,74.5,23.654,44.717,29.535 +2020-08-25 23:15:00,72.88,22.587,44.717,29.535 +2020-08-25 23:30:00,72.05,22.16,44.717,29.535 +2020-08-25 23:45:00,72.32,21.335,44.717,29.535 +2020-08-26 00:00:00,71.27,21.596,41.263000000000005,29.535 +2020-08-26 00:15:00,72.44,22.294,41.263000000000005,29.535 +2020-08-26 00:30:00,72.68,21.475,41.263000000000005,29.535 +2020-08-26 00:45:00,73.43,21.688000000000002,41.263000000000005,29.535 +2020-08-26 01:00:00,69.7,21.326,38.448,29.535 +2020-08-26 01:15:00,71.13,20.708000000000002,38.448,29.535 +2020-08-26 01:30:00,67.88,19.711,38.448,29.535 +2020-08-26 01:45:00,71.52,19.434,38.448,29.535 +2020-08-26 02:00:00,69.26,19.352999999999998,36.471,29.535 +2020-08-26 02:15:00,69.62,18.797,36.471,29.535 +2020-08-26 02:30:00,70.17,19.801,36.471,29.535 +2020-08-26 02:45:00,71.2,20.463,36.471,29.535 +2020-08-26 03:00:00,72.48,21.142,36.042,29.535 +2020-08-26 03:15:00,72.93,21.028000000000002,36.042,29.535 +2020-08-26 03:30:00,74.6,20.743000000000002,36.042,29.535 +2020-08-26 03:45:00,77.43,20.357,36.042,29.535 +2020-08-26 04:00:00,82.27,24.653000000000002,36.705,29.535 +2020-08-26 04:15:00,88.85,30.103,36.705,29.535 +2020-08-26 04:30:00,91.76,27.734,36.705,29.535 +2020-08-26 04:45:00,93.35,27.868000000000002,36.705,29.535 +2020-08-26 05:00:00,101.37,40.118,39.716,29.535 +2020-08-26 05:15:00,108.33,47.097,39.716,29.535 +2020-08-26 05:30:00,110.12,42.623000000000005,39.716,29.535 +2020-08-26 05:45:00,111.53,39.868,39.716,29.535 +2020-08-26 06:00:00,112.68,40.042,52.756,29.535 +2020-08-26 06:15:00,116.55,40.361999999999995,52.756,29.535 +2020-08-26 06:30:00,120.01,39.836999999999996,52.756,29.535 +2020-08-26 06:45:00,121.27,42.13,52.756,29.535 +2020-08-26 07:00:00,119.57,42.163000000000004,65.977,29.535 +2020-08-26 07:15:00,114.72,43.35,65.977,29.535 +2020-08-26 07:30:00,121.57,41.919,65.977,29.535 +2020-08-26 07:45:00,118.18,42.715,65.977,29.535 +2020-08-26 08:00:00,118.81,39.744,57.927,29.535 +2020-08-26 08:15:00,116.2,41.883,57.927,29.535 +2020-08-26 08:30:00,118.47,42.278,57.927,29.535 +2020-08-26 08:45:00,117.16,44.023999999999994,57.927,29.535 +2020-08-26 09:00:00,113.77,39.093,54.86,29.535 +2020-08-26 09:15:00,109.86,38.158,54.86,29.535 +2020-08-26 09:30:00,114.74,40.864000000000004,54.86,29.535 +2020-08-26 09:45:00,115.54,43.261,54.86,29.535 +2020-08-26 10:00:00,115.19,39.749,52.818000000000005,29.535 +2020-08-26 10:15:00,109.01,40.578,52.818000000000005,29.535 +2020-08-26 10:30:00,111.28,40.4,52.818000000000005,29.535 +2020-08-26 10:45:00,114.2,41.563,52.818000000000005,29.535 +2020-08-26 11:00:00,112.86,39.876,52.937,29.535 +2020-08-26 11:15:00,109.67,40.861,52.937,29.535 +2020-08-26 11:30:00,106.25,41.67,52.937,29.535 +2020-08-26 11:45:00,111.83,42.434,52.937,29.535 +2020-08-26 12:00:00,109.68,38.638000000000005,50.826,29.535 +2020-08-26 12:15:00,109.43,37.951,50.826,29.535 +2020-08-26 12:30:00,102.87,37.102,50.826,29.535 +2020-08-26 12:45:00,108.4,37.808,50.826,29.535 +2020-08-26 13:00:00,105.9,37.226,50.556000000000004,29.535 +2020-08-26 13:15:00,105.95,37.961999999999996,50.556000000000004,29.535 +2020-08-26 13:30:00,101.59,36.486999999999995,50.556000000000004,29.535 +2020-08-26 13:45:00,99.54,36.418,50.556000000000004,29.535 +2020-08-26 14:00:00,100.94,37.431999999999995,51.188,29.535 +2020-08-26 14:15:00,105.78,36.654,51.188,29.535 +2020-08-26 14:30:00,108.59,35.864000000000004,51.188,29.535 +2020-08-26 14:45:00,109.21,36.579,51.188,29.535 +2020-08-26 15:00:00,104.05,37.327,52.976000000000006,29.535 +2020-08-26 15:15:00,107.09,35.291,52.976000000000006,29.535 +2020-08-26 15:30:00,108.02,34.181,52.976000000000006,29.535 +2020-08-26 15:45:00,108.26,32.556,52.976000000000006,29.535 +2020-08-26 16:00:00,107.58,34.734,55.463,29.535 +2020-08-26 16:15:00,106.64,34.72,55.463,29.535 +2020-08-26 16:30:00,106.57,34.516,55.463,29.535 +2020-08-26 16:45:00,108.55,32.669000000000004,55.463,29.535 +2020-08-26 17:00:00,113.88,35.105,59.435,29.535 +2020-08-26 17:15:00,118.13,36.104,59.435,29.535 +2020-08-26 17:30:00,118.87,35.510999999999996,59.435,29.535 +2020-08-26 17:45:00,113.11,35.313,59.435,29.535 +2020-08-26 18:00:00,113.43,37.775,61.387,29.535 +2020-08-26 18:15:00,112.92,37.656,61.387,29.535 +2020-08-26 18:30:00,117.05,36.104,61.387,29.535 +2020-08-26 18:45:00,113.66,39.007,61.387,29.535 +2020-08-26 19:00:00,116.87,40.169000000000004,63.323,29.535 +2020-08-26 19:15:00,114.49,39.111,63.323,29.535 +2020-08-26 19:30:00,115.09,38.061,63.323,29.535 +2020-08-26 19:45:00,114.82,37.6,63.323,29.535 +2020-08-26 20:00:00,107.23,34.726,69.083,29.535 +2020-08-26 20:15:00,102.55,34.564,69.083,29.535 +2020-08-26 20:30:00,99.74,34.594,69.083,29.535 +2020-08-26 20:45:00,99.86,34.573,69.083,29.535 +2020-08-26 21:00:00,92.47,33.052,59.957,29.535 +2020-08-26 21:15:00,89.95,34.527,59.957,29.535 +2020-08-26 21:30:00,86.5,34.865,59.957,29.535 +2020-08-26 21:45:00,86.61,34.833,59.957,29.535 +2020-08-26 22:00:00,81.97,32.114000000000004,53.821000000000005,29.535 +2020-08-26 22:15:00,83.28,33.902,53.821000000000005,29.535 +2020-08-26 22:30:00,79.53,28.649,53.821000000000005,29.535 +2020-08-26 22:45:00,84.8,25.583000000000002,53.821000000000005,29.535 +2020-08-26 23:00:00,81.23,23.787,45.458,29.535 +2020-08-26 23:15:00,79.13,22.703000000000003,45.458,29.535 +2020-08-26 23:30:00,78.99,22.287,45.458,29.535 +2020-08-26 23:45:00,78.9,21.468000000000004,45.458,29.535 +2020-08-27 00:00:00,71.33,21.749000000000002,40.36,29.535 +2020-08-27 00:15:00,73.36,22.448,40.36,29.535 +2020-08-27 00:30:00,73.38,21.634,40.36,29.535 +2020-08-27 00:45:00,73.37,21.854,40.36,29.535 +2020-08-27 01:00:00,70.34,21.473000000000003,38.552,29.535 +2020-08-27 01:15:00,71.82,20.868000000000002,38.552,29.535 +2020-08-27 01:30:00,71.13,19.885,38.552,29.535 +2020-08-27 01:45:00,71.82,19.609,38.552,29.535 +2020-08-27 02:00:00,69.67,19.53,36.895,29.535 +2020-08-27 02:15:00,71.68,18.995,36.895,29.535 +2020-08-27 02:30:00,71.41,19.977,36.895,29.535 +2020-08-27 02:45:00,72.88,20.635,36.895,29.535 +2020-08-27 03:00:00,74.73,21.303,36.565,29.535 +2020-08-27 03:15:00,73.5,21.204,36.565,29.535 +2020-08-27 03:30:00,74.7,20.924,36.565,29.535 +2020-08-27 03:45:00,77.44,20.531,36.565,29.535 +2020-08-27 04:00:00,80.24,24.855,37.263000000000005,29.535 +2020-08-27 04:15:00,84.26,30.322,37.263000000000005,29.535 +2020-08-27 04:30:00,88.93,27.961,37.263000000000005,29.535 +2020-08-27 04:45:00,92.35,28.099,37.263000000000005,29.535 +2020-08-27 05:00:00,100.04,40.422,40.412,29.535 +2020-08-27 05:15:00,101.99,47.481,40.412,29.535 +2020-08-27 05:30:00,103.56,43.0,40.412,29.535 +2020-08-27 05:45:00,108.21,40.204,40.412,29.535 +2020-08-27 06:00:00,111.6,40.345,49.825,29.535 +2020-08-27 06:15:00,115.35,40.686,49.825,29.535 +2020-08-27 06:30:00,124.04,40.156,49.825,29.535 +2020-08-27 06:45:00,126.88,42.449,49.825,29.535 +2020-08-27 07:00:00,129.89,42.482,61.082,29.535 +2020-08-27 07:15:00,127.93,43.683,61.082,29.535 +2020-08-27 07:30:00,132.59,42.276,61.082,29.535 +2020-08-27 07:45:00,134.35,43.071999999999996,61.082,29.535 +2020-08-27 08:00:00,130.7,40.103,53.961999999999996,29.535 +2020-08-27 08:15:00,129.47,42.208999999999996,53.961999999999996,29.535 +2020-08-27 08:30:00,135.31,42.599,53.961999999999996,29.535 +2020-08-27 08:45:00,130.08,44.33,53.961999999999996,29.535 +2020-08-27 09:00:00,132.01,39.407,50.06100000000001,29.535 +2020-08-27 09:15:00,137.15,38.461999999999996,50.06100000000001,29.535 +2020-08-27 09:30:00,136.56,41.151,50.06100000000001,29.535 +2020-08-27 09:45:00,138.15,43.523999999999994,50.06100000000001,29.535 +2020-08-27 10:00:00,140.11,40.01,47.68,29.535 +2020-08-27 10:15:00,137.51,40.811,47.68,29.535 +2020-08-27 10:30:00,134.24,40.628,47.68,29.535 +2020-08-27 10:45:00,138.17,41.781000000000006,47.68,29.535 +2020-08-27 11:00:00,132.89,40.105,45.93899999999999,29.535 +2020-08-27 11:15:00,126.1,41.08,45.93899999999999,29.535 +2020-08-27 11:30:00,131.52,41.886,45.93899999999999,29.535 +2020-08-27 11:45:00,134.76,42.635,45.93899999999999,29.535 +2020-08-27 12:00:00,133.16,38.823,43.648999999999994,29.535 +2020-08-27 12:15:00,130.96,38.125,43.648999999999994,29.535 +2020-08-27 12:30:00,125.67,37.294000000000004,43.648999999999994,29.535 +2020-08-27 12:45:00,134.33,37.988,43.648999999999994,29.535 +2020-08-27 13:00:00,127.85,37.389,42.801,29.535 +2020-08-27 13:15:00,124.91,38.113,42.801,29.535 +2020-08-27 13:30:00,127.6,36.634,42.801,29.535 +2020-08-27 13:45:00,132.81,36.576,42.801,29.535 +2020-08-27 14:00:00,129.82,37.563,43.24,29.535 +2020-08-27 14:15:00,126.07,36.792,43.24,29.535 +2020-08-27 14:30:00,121.33,36.021,43.24,29.535 +2020-08-27 14:45:00,126.96,36.736999999999995,43.24,29.535 +2020-08-27 15:00:00,125.31,37.437,45.04600000000001,29.535 +2020-08-27 15:15:00,123.64,35.409,45.04600000000001,29.535 +2020-08-27 15:30:00,117.09,34.314,45.04600000000001,29.535 +2020-08-27 15:45:00,113.24,32.696,45.04600000000001,29.535 +2020-08-27 16:00:00,115.1,34.846,46.568000000000005,29.535 +2020-08-27 16:15:00,117.47,34.841,46.568000000000005,29.535 +2020-08-27 16:30:00,113.23,34.639,46.568000000000005,29.535 +2020-08-27 16:45:00,113.72,32.836999999999996,46.568000000000005,29.535 +2020-08-27 17:00:00,116.96,35.239000000000004,50.618,29.535 +2020-08-27 17:15:00,113.6,36.266999999999996,50.618,29.535 +2020-08-27 17:30:00,111.58,35.686,50.618,29.535 +2020-08-27 17:45:00,112.19,35.525999999999996,50.618,29.535 +2020-08-27 18:00:00,111.35,37.977,52.806999999999995,29.535 +2020-08-27 18:15:00,110.44,37.874,52.806999999999995,29.535 +2020-08-27 18:30:00,108.59,36.330999999999996,52.806999999999995,29.535 +2020-08-27 18:45:00,108.94,39.234,52.806999999999995,29.535 +2020-08-27 19:00:00,111.09,40.403,53.464,29.535 +2020-08-27 19:15:00,108.1,39.346,53.464,29.535 +2020-08-27 19:30:00,108.89,38.298,53.464,29.535 +2020-08-27 19:45:00,104.8,37.842,53.464,29.535 +2020-08-27 20:00:00,99.49,34.976,56.753,29.535 +2020-08-27 20:15:00,100.21,34.816,56.753,29.535 +2020-08-27 20:30:00,96.74,34.83,56.753,29.535 +2020-08-27 20:45:00,94.89,34.772,56.753,29.535 +2020-08-27 21:00:00,91.41,33.254,52.506,29.535 +2020-08-27 21:15:00,90.08,34.719,52.506,29.535 +2020-08-27 21:30:00,85.55,35.056999999999995,52.506,29.535 +2020-08-27 21:45:00,85.17,34.993,52.506,29.535 +2020-08-27 22:00:00,80.57,32.251999999999995,48.163000000000004,29.535 +2020-08-27 22:15:00,82.4,34.023,48.163000000000004,29.535 +2020-08-27 22:30:00,79.2,28.738000000000003,48.163000000000004,29.535 +2020-08-27 22:45:00,79.7,25.674,48.163000000000004,29.535 +2020-08-27 23:00:00,75.57,23.923000000000002,42.379,29.535 +2020-08-27 23:15:00,76.68,22.822,42.379,29.535 +2020-08-27 23:30:00,75.21,22.416,42.379,29.535 +2020-08-27 23:45:00,74.58,21.603,42.379,29.535 +2020-08-28 00:00:00,72.43,20.281,38.505,29.535 +2020-08-28 00:15:00,71.75,21.175,38.505,29.535 +2020-08-28 00:30:00,72.78,20.615,38.505,29.535 +2020-08-28 00:45:00,72.99,21.226999999999997,38.505,29.535 +2020-08-28 01:00:00,70.23,20.479,37.004,29.535 +2020-08-28 01:15:00,72.14,19.332,37.004,29.535 +2020-08-28 01:30:00,71.17,19.017,37.004,29.535 +2020-08-28 01:45:00,72.62,18.511,37.004,29.535 +2020-08-28 02:00:00,71.55,19.285,36.098,29.535 +2020-08-28 02:15:00,72.3,18.726,36.098,29.535 +2020-08-28 02:30:00,72.07,20.434,36.098,29.535 +2020-08-28 02:45:00,72.86,20.463,36.098,29.535 +2020-08-28 03:00:00,73.05,21.838,36.561,29.535 +2020-08-28 03:15:00,75.07,20.644000000000002,36.561,29.535 +2020-08-28 03:30:00,74.45,20.151,36.561,29.535 +2020-08-28 03:45:00,79.36,20.548000000000002,36.561,29.535 +2020-08-28 04:00:00,87.62,25.022,37.355,29.535 +2020-08-28 04:15:00,89.23,29.087,37.355,29.535 +2020-08-28 04:30:00,93.88,27.601,37.355,29.535 +2020-08-28 04:45:00,94.42,27.164,37.355,29.535 +2020-08-28 05:00:00,99.29,39.138000000000005,40.285,29.535 +2020-08-28 05:15:00,100.91,47.137,40.285,29.535 +2020-08-28 05:30:00,109.34,42.821999999999996,40.285,29.535 +2020-08-28 05:45:00,112.16,39.583,40.285,29.535 +2020-08-28 06:00:00,116.49,39.891999999999996,52.378,29.535 +2020-08-28 06:15:00,113.8,40.41,52.378,29.535 +2020-08-28 06:30:00,110.25,39.828,52.378,29.535 +2020-08-28 06:45:00,110.75,42.0,52.378,29.535 +2020-08-28 07:00:00,116.02,42.622,60.891999999999996,29.535 +2020-08-28 07:15:00,113.76,44.653,60.891999999999996,29.535 +2020-08-28 07:30:00,113.32,41.505,60.891999999999996,29.535 +2020-08-28 07:45:00,116.11,42.113,60.891999999999996,29.535 +2020-08-28 08:00:00,110.53,39.859,53.652,29.535 +2020-08-28 08:15:00,110.47,42.542,53.652,29.535 +2020-08-28 08:30:00,118.74,42.833,53.652,29.535 +2020-08-28 08:45:00,118.78,44.428000000000004,53.652,29.535 +2020-08-28 09:00:00,114.6,37.416,51.456,29.535 +2020-08-28 09:15:00,109.6,38.2,51.456,29.535 +2020-08-28 09:30:00,106.67,40.25,51.456,29.535 +2020-08-28 09:45:00,107.3,42.948,51.456,29.535 +2020-08-28 10:00:00,110.6,39.293,49.4,29.535 +2020-08-28 10:15:00,108.84,39.86,49.4,29.535 +2020-08-28 10:30:00,109.67,40.172,49.4,29.535 +2020-08-28 10:45:00,104.36,41.235,49.4,29.535 +2020-08-28 11:00:00,105.31,39.806999999999995,48.773,29.535 +2020-08-28 11:15:00,104.1,39.791,48.773,29.535 +2020-08-28 11:30:00,102.59,40.239000000000004,48.773,29.535 +2020-08-28 11:45:00,100.47,40.095,48.773,29.535 +2020-08-28 12:00:00,97.94,36.64,46.033,29.535 +2020-08-28 12:15:00,100.85,35.379,46.033,29.535 +2020-08-28 12:30:00,98.11,34.671,46.033,29.535 +2020-08-28 12:45:00,98.83,34.62,46.033,29.535 +2020-08-28 13:00:00,96.79,34.536,44.38399999999999,29.535 +2020-08-28 13:15:00,98.89,35.403,44.38399999999999,29.535 +2020-08-28 13:30:00,109.7,34.647,44.38399999999999,29.535 +2020-08-28 13:45:00,107.01,34.881,44.38399999999999,29.535 +2020-08-28 14:00:00,107.86,35.121,43.162,29.535 +2020-08-28 14:15:00,106.56,34.760999999999996,43.162,29.535 +2020-08-28 14:30:00,110.38,35.385,43.162,29.535 +2020-08-28 14:45:00,113.12,35.463,43.162,29.535 +2020-08-28 15:00:00,113.59,36.05,44.91,29.535 +2020-08-28 15:15:00,109.31,33.82,44.91,29.535 +2020-08-28 15:30:00,109.09,32.246,44.91,29.535 +2020-08-28 15:45:00,113.5,31.334,44.91,29.535 +2020-08-28 16:00:00,117.73,32.688,47.489,29.535 +2020-08-28 16:15:00,114.05,33.139,47.489,29.535 +2020-08-28 16:30:00,111.88,32.772,47.489,29.535 +2020-08-28 16:45:00,110.29,30.296,47.489,29.535 +2020-08-28 17:00:00,116.92,34.25,52.047,29.535 +2020-08-28 17:15:00,116.73,35.156,52.047,29.535 +2020-08-28 17:30:00,116.42,34.746,52.047,29.535 +2020-08-28 17:45:00,110.79,34.473,52.047,29.535 +2020-08-28 18:00:00,112.92,36.928000000000004,53.306000000000004,29.535 +2020-08-28 18:15:00,115.02,35.982,53.306000000000004,29.535 +2020-08-28 18:30:00,113.25,34.352,53.306000000000004,29.535 +2020-08-28 18:45:00,112.51,37.635,53.306000000000004,29.535 +2020-08-28 19:00:00,107.58,39.59,53.516000000000005,29.535 +2020-08-28 19:15:00,106.68,39.058,53.516000000000005,29.535 +2020-08-28 19:30:00,106.55,38.073,53.516000000000005,29.535 +2020-08-28 19:45:00,110.05,36.702,53.516000000000005,29.535 +2020-08-28 20:00:00,105.55,33.705999999999996,57.88,29.535 +2020-08-28 20:15:00,103.72,34.273,57.88,29.535 +2020-08-28 20:30:00,95.59,33.826,57.88,29.535 +2020-08-28 20:45:00,90.62,32.982,57.88,29.535 +2020-08-28 21:00:00,84.68,32.658,53.32,29.535 +2020-08-28 21:15:00,83.65,35.631,53.32,29.535 +2020-08-28 21:30:00,80.52,35.832,53.32,29.535 +2020-08-28 21:45:00,82.9,35.915,53.32,29.535 +2020-08-28 22:00:00,83.63,33.01,48.074,29.535 +2020-08-28 22:15:00,83.65,34.539,48.074,29.535 +2020-08-28 22:30:00,78.8,33.479,48.074,29.535 +2020-08-28 22:45:00,76.63,31.156999999999996,48.074,29.535 +2020-08-28 23:00:00,75.72,30.994,41.306999999999995,29.535 +2020-08-28 23:15:00,75.8,28.473000000000003,41.306999999999995,29.535 +2020-08-28 23:30:00,73.3,26.433000000000003,41.306999999999995,29.535 +2020-08-28 23:45:00,66.16,25.524,41.306999999999995,29.535 +2020-08-29 00:00:00,64.44,21.593000000000004,40.227,29.423000000000002 +2020-08-29 00:15:00,71.06,21.918000000000003,40.227,29.423000000000002 +2020-08-29 00:30:00,70.31,20.924,40.227,29.423000000000002 +2020-08-29 00:45:00,70.07,20.871,40.227,29.423000000000002 +2020-08-29 01:00:00,63.31,20.348,36.303000000000004,29.423000000000002 +2020-08-29 01:15:00,64.56,19.761,36.303000000000004,29.423000000000002 +2020-08-29 01:30:00,62.06,18.733,36.303000000000004,29.423000000000002 +2020-08-29 01:45:00,61.97,19.362000000000002,36.303000000000004,29.423000000000002 +2020-08-29 02:00:00,61.2,19.239,33.849000000000004,29.423000000000002 +2020-08-29 02:15:00,65.22,17.988,33.849000000000004,29.423000000000002 +2020-08-29 02:30:00,67.93,18.965,33.849000000000004,29.423000000000002 +2020-08-29 02:45:00,67.63,19.703,33.849000000000004,29.423000000000002 +2020-08-29 03:00:00,62.88,19.814,33.149,29.423000000000002 +2020-08-29 03:15:00,60.75,18.016,33.149,29.423000000000002 +2020-08-29 03:30:00,64.25,17.847,33.149,29.423000000000002 +2020-08-29 03:45:00,69.62,19.58,33.149,29.423000000000002 +2020-08-29 04:00:00,70.43,22.421999999999997,32.501,29.423000000000002 +2020-08-29 04:15:00,67.71,25.729,32.501,29.423000000000002 +2020-08-29 04:30:00,64.26,22.76,32.501,29.423000000000002 +2020-08-29 04:45:00,66.86,22.581,32.501,29.423000000000002 +2020-08-29 05:00:00,72.03,27.261,31.648000000000003,29.423000000000002 +2020-08-29 05:15:00,74.87,25.631,31.648000000000003,29.423000000000002 +2020-08-29 05:30:00,74.46,22.678,31.648000000000003,29.423000000000002 +2020-08-29 05:45:00,72.84,23.379,31.648000000000003,29.423000000000002 +2020-08-29 06:00:00,73.13,33.579,32.552,29.423000000000002 +2020-08-29 06:15:00,77.89,40.993,32.552,29.423000000000002 +2020-08-29 06:30:00,78.78,37.895,32.552,29.423000000000002 +2020-08-29 06:45:00,77.89,37.258,32.552,29.423000000000002 +2020-08-29 07:00:00,77.07,36.72,35.181999999999995,29.423000000000002 +2020-08-29 07:15:00,78.08,37.609,35.181999999999995,29.423000000000002 +2020-08-29 07:30:00,79.75,35.816,35.181999999999995,29.423000000000002 +2020-08-29 07:45:00,82.27,37.204,35.181999999999995,29.423000000000002 +2020-08-29 08:00:00,84.1,35.546,40.35,29.423000000000002 +2020-08-29 08:15:00,84.31,37.943000000000005,40.35,29.423000000000002 +2020-08-29 08:30:00,85.25,38.193000000000005,40.35,29.423000000000002 +2020-08-29 08:45:00,85.89,40.580999999999996,40.35,29.423000000000002 +2020-08-29 09:00:00,89.71,36.368,42.292,29.423000000000002 +2020-08-29 09:15:00,89.66,37.569,42.292,29.423000000000002 +2020-08-29 09:30:00,85.99,40.035,42.292,29.423000000000002 +2020-08-29 09:45:00,80.14,42.298,42.292,29.423000000000002 +2020-08-29 10:00:00,80.79,39.221,40.084,29.423000000000002 +2020-08-29 10:15:00,88.58,40.082,40.084,29.423000000000002 +2020-08-29 10:30:00,90.14,40.073,40.084,29.423000000000002 +2020-08-29 10:45:00,90.37,40.782,40.084,29.423000000000002 +2020-08-29 11:00:00,89.97,39.248000000000005,36.966,29.423000000000002 +2020-08-29 11:15:00,91.84,40.025999999999996,36.966,29.423000000000002 +2020-08-29 11:30:00,89.82,40.753,36.966,29.423000000000002 +2020-08-29 11:45:00,88.72,41.224,36.966,29.423000000000002 +2020-08-29 12:00:00,86.32,38.188,35.19,29.423000000000002 +2020-08-29 12:15:00,84.51,37.68,35.19,29.423000000000002 +2020-08-29 12:30:00,82.92,36.863,35.19,29.423000000000002 +2020-08-29 12:45:00,85.5,37.486,35.19,29.423000000000002 +2020-08-29 13:00:00,81.37,36.576,32.277,29.423000000000002 +2020-08-29 13:15:00,82.13,36.982,32.277,29.423000000000002 +2020-08-29 13:30:00,81.68,36.403,32.277,29.423000000000002 +2020-08-29 13:45:00,81.28,35.406,32.277,29.423000000000002 +2020-08-29 14:00:00,79.6,35.664,31.436999999999998,29.423000000000002 +2020-08-29 14:15:00,79.02,34.179,31.436999999999998,29.423000000000002 +2020-08-29 14:30:00,76.91,34.474000000000004,31.436999999999998,29.423000000000002 +2020-08-29 14:45:00,76.5,34.986999999999995,31.436999999999998,29.423000000000002 +2020-08-29 15:00:00,76.4,35.93,33.493,29.423000000000002 +2020-08-29 15:15:00,77.73,34.336,33.493,29.423000000000002 +2020-08-29 15:30:00,78.14,32.926,33.493,29.423000000000002 +2020-08-29 15:45:00,77.73,31.178,33.493,29.423000000000002 +2020-08-29 16:00:00,78.32,34.436,36.593,29.423000000000002 +2020-08-29 16:15:00,79.7,34.065,36.593,29.423000000000002 +2020-08-29 16:30:00,80.81,33.906,36.593,29.423000000000002 +2020-08-29 16:45:00,82.06,31.432,36.593,29.423000000000002 +2020-08-29 17:00:00,84.59,34.291,42.049,29.423000000000002 +2020-08-29 17:15:00,86.73,33.227,42.049,29.423000000000002 +2020-08-29 17:30:00,85.87,32.711,42.049,29.423000000000002 +2020-08-29 17:45:00,83.17,32.935,42.049,29.423000000000002 +2020-08-29 18:00:00,84.18,36.61,43.755,29.423000000000002 +2020-08-29 18:15:00,83.2,37.168,43.755,29.423000000000002 +2020-08-29 18:30:00,83.27,36.759,43.755,29.423000000000002 +2020-08-29 18:45:00,84.31,36.943000000000005,43.755,29.423000000000002 +2020-08-29 19:00:00,89.05,37.398,44.492,29.423000000000002 +2020-08-29 19:15:00,84.66,35.963,44.492,29.423000000000002 +2020-08-29 19:30:00,86.21,35.665,44.492,29.423000000000002 +2020-08-29 19:45:00,81.85,35.891999999999996,44.492,29.423000000000002 +2020-08-29 20:00:00,77.43,33.655,40.896,29.423000000000002 +2020-08-29 20:15:00,77.44,33.62,40.896,29.423000000000002 +2020-08-29 20:30:00,75.73,32.365,40.896,29.423000000000002 +2020-08-29 20:45:00,76.44,33.168,40.896,29.423000000000002 +2020-08-29 21:00:00,73.3,31.482,39.056,29.423000000000002 +2020-08-29 21:15:00,72.72,34.125,39.056,29.423000000000002 +2020-08-29 21:30:00,70.23,34.483000000000004,39.056,29.423000000000002 +2020-08-29 21:45:00,69.8,34.045,39.056,29.423000000000002 +2020-08-29 22:00:00,66.22,31.079,38.478,29.423000000000002 +2020-08-29 22:15:00,65.01,32.786,38.478,29.423000000000002 +2020-08-29 22:30:00,63.02,31.276999999999997,38.478,29.423000000000002 +2020-08-29 22:45:00,61.99,29.272,38.478,29.423000000000002 +2020-08-29 23:00:00,58.73,28.525,32.953,29.423000000000002 +2020-08-29 23:15:00,57.9,26.384,32.953,29.423000000000002 +2020-08-29 23:30:00,57.04,26.589000000000002,32.953,29.423000000000002 +2020-08-29 23:45:00,56.91,26.037,32.953,29.423000000000002 +2020-08-30 00:00:00,54.27,22.994,28.584,29.423000000000002 +2020-08-30 00:15:00,55.13,22.281999999999996,28.584,29.423000000000002 +2020-08-30 00:30:00,50.94,21.165,28.584,29.423000000000002 +2020-08-30 00:45:00,53.35,21.011,28.584,29.423000000000002 +2020-08-30 01:00:00,51.34,20.706999999999997,26.419,29.423000000000002 +2020-08-30 01:15:00,52.9,20.012999999999998,26.419,29.423000000000002 +2020-08-30 01:30:00,52.18,18.868,26.419,29.423000000000002 +2020-08-30 01:45:00,51.93,19.159000000000002,26.419,29.423000000000002 +2020-08-30 02:00:00,52.18,19.118,25.335,29.423000000000002 +2020-08-30 02:15:00,51.62,18.54,25.335,29.423000000000002 +2020-08-30 02:30:00,51.01,19.651,25.335,29.423000000000002 +2020-08-30 02:45:00,50.76,20.117,25.335,29.423000000000002 +2020-08-30 03:00:00,51.01,20.8,24.805,29.423000000000002 +2020-08-30 03:15:00,51.32,19.261,24.805,29.423000000000002 +2020-08-30 03:30:00,52.27,18.414,24.805,29.423000000000002 +2020-08-30 03:45:00,53.2,19.423,24.805,29.423000000000002 +2020-08-30 04:00:00,53.28,22.268,25.772,29.423000000000002 +2020-08-30 04:15:00,53.79,25.218000000000004,25.772,29.423000000000002 +2020-08-30 04:30:00,54.34,23.467,25.772,29.423000000000002 +2020-08-30 04:45:00,55.42,22.866,25.772,29.423000000000002 +2020-08-30 05:00:00,56.16,27.819000000000003,25.971999999999998,29.423000000000002 +2020-08-30 05:15:00,56.99,25.605999999999998,25.971999999999998,29.423000000000002 +2020-08-30 05:30:00,56.61,22.284000000000002,25.971999999999998,29.423000000000002 +2020-08-30 05:45:00,57.04,22.694000000000003,25.971999999999998,29.423000000000002 +2020-08-30 06:00:00,58.54,30.746,26.026,29.423000000000002 +2020-08-30 06:15:00,59.88,38.948,26.026,29.423000000000002 +2020-08-30 06:30:00,61.66,35.231,26.026,29.423000000000002 +2020-08-30 06:45:00,63.97,33.71,26.026,29.423000000000002 +2020-08-30 07:00:00,66.86,33.422,27.396,29.423000000000002 +2020-08-30 07:15:00,68.13,32.858000000000004,27.396,29.423000000000002 +2020-08-30 07:30:00,70.07,32.260999999999996,27.396,29.423000000000002 +2020-08-30 07:45:00,71.87,33.628,27.396,29.423000000000002 +2020-08-30 08:00:00,74.54,32.635,30.791999999999998,29.423000000000002 +2020-08-30 08:15:00,76.67,36.064,30.791999999999998,29.423000000000002 +2020-08-30 08:30:00,77.37,37.067,30.791999999999998,29.423000000000002 +2020-08-30 08:45:00,78.29,39.330999999999996,30.791999999999998,29.423000000000002 +2020-08-30 09:00:00,78.71,35.037,32.482,29.423000000000002 +2020-08-30 09:15:00,79.95,35.771,32.482,29.423000000000002 +2020-08-30 09:30:00,80.79,38.611999999999995,32.482,29.423000000000002 +2020-08-30 09:45:00,83.14,41.757,32.482,29.423000000000002 +2020-08-30 10:00:00,84.85,39.117,31.951,29.423000000000002 +2020-08-30 10:15:00,87.31,40.061,31.951,29.423000000000002 +2020-08-30 10:30:00,88.3,40.231,31.951,29.423000000000002 +2020-08-30 10:45:00,87.52,41.902,31.951,29.423000000000002 +2020-08-30 11:00:00,84.25,40.043,33.619,29.423000000000002 +2020-08-30 11:15:00,83.28,40.399,33.619,29.423000000000002 +2020-08-30 11:30:00,81.94,41.626000000000005,33.619,29.423000000000002 +2020-08-30 11:45:00,80.98,42.297,33.619,29.423000000000002 +2020-08-30 12:00:00,78.49,40.249,30.975,29.423000000000002 +2020-08-30 12:15:00,77.41,39.084,30.975,29.423000000000002 +2020-08-30 12:30:00,75.82,38.535,30.975,29.423000000000002 +2020-08-30 12:45:00,78.16,38.595,30.975,29.423000000000002 +2020-08-30 13:00:00,72.08,37.405,27.956999999999997,29.423000000000002 +2020-08-30 13:15:00,72.23,37.171,27.956999999999997,29.423000000000002 +2020-08-30 13:30:00,71.97,35.578,27.956999999999997,29.423000000000002 +2020-08-30 13:45:00,72.4,35.635,27.956999999999997,29.423000000000002 +2020-08-30 14:00:00,70.92,36.957,25.555999999999997,29.423000000000002 +2020-08-30 14:15:00,70.88,35.834,25.555999999999997,29.423000000000002 +2020-08-30 14:30:00,70.54,34.972,25.555999999999997,29.423000000000002 +2020-08-30 14:45:00,70.65,34.519,25.555999999999997,29.423000000000002 +2020-08-30 15:00:00,71.32,35.598,26.271,29.423000000000002 +2020-08-30 15:15:00,71.25,33.243,26.271,29.423000000000002 +2020-08-30 15:30:00,71.19,31.660999999999998,26.271,29.423000000000002 +2020-08-30 15:45:00,71.97,30.158,26.271,29.423000000000002 +2020-08-30 16:00:00,73.4,31.854,30.369,29.423000000000002 +2020-08-30 16:15:00,74.06,31.729,30.369,29.423000000000002 +2020-08-30 16:30:00,75.86,32.481,30.369,29.423000000000002 +2020-08-30 16:45:00,78.3,30.105999999999998,30.369,29.423000000000002 +2020-08-30 17:00:00,79.85,33.259,38.787,29.423000000000002 +2020-08-30 17:15:00,80.1,33.619,38.787,29.423000000000002 +2020-08-30 17:30:00,81.58,33.845,38.787,29.423000000000002 +2020-08-30 17:45:00,80.78,34.421,38.787,29.423000000000002 +2020-08-30 18:00:00,83.14,38.659,41.886,29.423000000000002 +2020-08-30 18:15:00,80.9,38.830999999999996,41.886,29.423000000000002 +2020-08-30 18:30:00,80.98,38.26,41.886,29.423000000000002 +2020-08-30 18:45:00,80.43,38.503,41.886,29.423000000000002 +2020-08-30 19:00:00,86.05,40.954,42.91,29.423000000000002 +2020-08-30 19:15:00,82.33,38.507,42.91,29.423000000000002 +2020-08-30 19:30:00,83.91,37.993,42.91,29.423000000000002 +2020-08-30 19:45:00,80.57,37.784,42.91,29.423000000000002 +2020-08-30 20:00:00,78.83,35.695,42.148999999999994,29.423000000000002 +2020-08-30 20:15:00,79.05,35.516,42.148999999999994,29.423000000000002 +2020-08-30 20:30:00,78.41,34.949,42.148999999999994,29.423000000000002 +2020-08-30 20:45:00,79.19,34.219,42.148999999999994,29.423000000000002 +2020-08-30 21:00:00,77.65,32.483000000000004,40.955999999999996,29.423000000000002 +2020-08-30 21:15:00,78.79,34.856,40.955999999999996,29.423000000000002 +2020-08-30 21:30:00,75.78,34.623000000000005,40.955999999999996,29.423000000000002 +2020-08-30 21:45:00,75.79,34.472,40.955999999999996,29.423000000000002 +2020-08-30 22:00:00,72.02,33.349000000000004,39.873000000000005,29.423000000000002 +2020-08-30 22:15:00,72.93,33.534,39.873000000000005,29.423000000000002 +2020-08-30 22:30:00,71.7,31.603,39.873000000000005,29.423000000000002 +2020-08-30 22:45:00,70.88,28.467,39.873000000000005,29.423000000000002 +2020-08-30 23:00:00,66.13,27.553,35.510999999999996,29.423000000000002 +2020-08-30 23:15:00,67.65,26.502,35.510999999999996,29.423000000000002 +2020-08-30 23:30:00,68.19,26.213,35.510999999999996,29.423000000000002 +2020-08-30 23:45:00,66.64,25.791,35.510999999999996,29.423000000000002 +2020-08-31 00:00:00,63.09,24.439,33.475,29.535 +2020-08-31 00:15:00,65.41,24.381,33.475,29.535 +2020-08-31 00:30:00,65.2,22.921,33.475,29.535 +2020-08-31 00:45:00,65.56,22.427,33.475,29.535 +2020-08-31 01:00:00,62.92,22.448,33.111,29.535 +2020-08-31 01:15:00,64.41,21.793000000000003,33.111,29.535 +2020-08-31 01:30:00,64.04,20.988000000000003,33.111,29.535 +2020-08-31 01:45:00,65.01,21.19,33.111,29.535 +2020-08-31 02:00:00,62.9,21.559,32.358000000000004,29.535 +2020-08-31 02:15:00,64.34,20.055999999999997,32.358000000000004,29.535 +2020-08-31 02:30:00,64.2,21.287,32.358000000000004,29.535 +2020-08-31 02:45:00,64.04,21.629,32.358000000000004,29.535 +2020-08-31 03:00:00,65.64,22.729,30.779,29.535 +2020-08-31 03:15:00,66.35,21.822,30.779,29.535 +2020-08-31 03:30:00,72.4,21.601999999999997,30.779,29.535 +2020-08-31 03:45:00,75.77,22.195,30.779,29.535 +2020-08-31 04:00:00,80.59,27.787,31.416,29.535 +2020-08-31 04:15:00,79.36,33.317,31.416,29.535 +2020-08-31 04:30:00,88.89,31.094,31.416,29.535 +2020-08-31 04:45:00,94.27,30.829,31.416,29.535 +2020-08-31 05:00:00,100.31,42.129,37.221,29.535 +2020-08-31 05:15:00,101.92,48.968,37.221,29.535 +2020-08-31 05:30:00,101.22,44.25,37.221,29.535 +2020-08-31 05:45:00,104.81,41.94,37.221,29.535 +2020-08-31 06:00:00,108.02,40.968,51.891000000000005,29.535 +2020-08-31 06:15:00,111.44,41.255,51.891000000000005,29.535 +2020-08-31 06:30:00,112.93,40.977,51.891000000000005,29.535 +2020-08-31 06:45:00,115.62,44.062,51.891000000000005,29.535 +2020-08-31 07:00:00,119.0,43.956,62.282,29.535 +2020-08-31 07:15:00,119.27,45.438,62.282,29.535 +2020-08-31 07:30:00,120.08,44.106,62.282,29.535 +2020-08-31 07:45:00,119.77,45.791000000000004,62.282,29.535 +2020-08-31 08:00:00,121.76,42.894,54.102,29.535 +2020-08-31 08:15:00,123.35,45.245,54.102,29.535 +2020-08-31 08:30:00,124.12,45.458999999999996,54.102,29.535 +2020-08-31 08:45:00,125.71,47.97,54.102,29.535 +2020-08-31 09:00:00,129.33,42.748999999999995,50.917,29.535 +2020-08-31 09:15:00,126.56,41.933,50.917,29.535 +2020-08-31 09:30:00,126.46,43.938,50.917,29.535 +2020-08-31 09:45:00,127.36,44.971000000000004,50.917,29.535 +2020-08-31 10:00:00,128.24,42.68899999999999,49.718999999999994,29.535 +2020-08-31 10:15:00,131.55,43.481,49.718999999999994,29.535 +2020-08-31 10:30:00,130.34,43.248000000000005,49.718999999999994,29.535 +2020-08-31 10:45:00,123.89,43.443000000000005,49.718999999999994,29.535 +2020-08-31 11:00:00,124.43,41.855,49.833999999999996,29.535 +2020-08-31 11:15:00,124.83,42.403,49.833999999999996,29.535 +2020-08-31 11:30:00,129.95,44.224,49.833999999999996,29.535 +2020-08-31 11:45:00,125.36,45.352,49.833999999999996,29.535 +2020-08-31 12:00:00,121.07,41.592,47.832,29.535 +2020-08-31 12:15:00,122.98,40.518,47.832,29.535 +2020-08-31 12:30:00,117.12,38.993,47.832,29.535 +2020-08-31 12:45:00,116.46,38.983000000000004,47.832,29.535 +2020-08-31 13:00:00,120.35,38.641,48.03,29.535 +2020-08-31 13:15:00,120.22,37.631,48.03,29.535 +2020-08-31 13:30:00,118.44,36.205,48.03,29.535 +2020-08-31 13:45:00,111.87,37.094,48.03,29.535 +2020-08-31 14:00:00,111.57,37.562,48.157,29.535 +2020-08-31 14:15:00,114.04,37.012,48.157,29.535 +2020-08-31 14:30:00,113.71,36.038000000000004,48.157,29.535 +2020-08-31 14:45:00,116.13,37.49,48.157,29.535 +2020-08-31 15:00:00,113.44,38.115,48.897,29.535 +2020-08-31 15:15:00,111.84,35.278,48.897,29.535 +2020-08-31 15:30:00,110.24,34.448,48.897,29.535 +2020-08-31 15:45:00,108.51,32.522,48.897,29.535 +2020-08-31 16:00:00,104.99,35.231,51.446000000000005,29.535 +2020-08-31 16:15:00,104.55,35.198,51.446000000000005,29.535 +2020-08-31 16:30:00,108.33,35.369,51.446000000000005,29.535 +2020-08-31 16:45:00,108.42,33.097,51.446000000000005,29.535 +2020-08-31 17:00:00,109.57,35.18,57.507,29.535 +2020-08-31 17:15:00,108.31,35.944,57.507,29.535 +2020-08-31 17:30:00,111.74,35.846,57.507,29.535 +2020-08-31 17:45:00,110.77,36.141999999999996,57.507,29.535 +2020-08-31 18:00:00,113.05,39.413000000000004,57.896,29.535 +2020-08-31 18:15:00,109.66,37.992,57.896,29.535 +2020-08-31 18:30:00,110.56,36.729,57.896,29.535 +2020-08-31 18:45:00,108.24,39.83,57.896,29.535 +2020-08-31 19:00:00,109.87,42.091,57.891999999999996,29.535 +2020-08-31 19:15:00,105.78,40.868,57.891999999999996,29.535 +2020-08-31 19:30:00,103.3,40.012,57.891999999999996,29.535 +2020-08-31 19:45:00,105.3,39.27,57.891999999999996,29.535 +2020-08-31 20:00:00,98.65,36.086999999999996,64.57300000000001,29.535 +2020-08-31 20:15:00,99.73,37.242,64.57300000000001,29.535 +2020-08-31 20:30:00,98.19,37.177,64.57300000000001,29.535 +2020-08-31 20:45:00,98.28,36.565,64.57300000000001,29.535 +2020-08-31 21:00:00,93.39,34.244,59.431999999999995,29.535 +2020-08-31 21:15:00,93.97,37.034,59.431999999999995,29.535 +2020-08-31 21:30:00,89.46,37.196999999999996,59.431999999999995,29.535 +2020-08-31 21:45:00,86.99,36.826,59.431999999999995,29.535 +2020-08-31 22:00:00,86.09,33.882,51.519,29.535 +2020-08-31 22:15:00,89.99,35.885999999999996,51.519,29.535 +2020-08-31 22:30:00,88.27,30.206,51.519,29.535 +2020-08-31 22:45:00,83.31,27.164,51.519,29.535 +2020-08-31 23:00:00,75.5,26.299,44.501000000000005,29.535 +2020-08-31 23:15:00,75.51,23.673000000000002,44.501000000000005,29.535 +2020-08-31 23:30:00,75.27,23.296999999999997,44.501000000000005,29.535 +2020-08-31 23:45:00,75.09,22.335,44.501000000000005,29.535 +2020-09-01 00:00:00,73.47,29.916999999999998,44.438,29.93 +2020-09-01 00:15:00,81.26,30.489,44.438,29.93 +2020-09-01 00:30:00,72.65,30.061,44.438,29.93 +2020-09-01 00:45:00,74.24,30.002,44.438,29.93 +2020-09-01 01:00:00,75.98,29.759,41.468999999999994,29.93 +2020-09-01 01:15:00,79.49,28.94,41.468999999999994,29.93 +2020-09-01 01:30:00,80.77,27.840999999999998,41.468999999999994,29.93 +2020-09-01 01:45:00,80.77,26.919,41.468999999999994,29.93 +2020-09-01 02:00:00,76.57,26.625,39.708,29.93 +2020-09-01 02:15:00,74.41,26.226,39.708,29.93 +2020-09-01 02:30:00,73.57,26.895,39.708,29.93 +2020-09-01 02:45:00,75.06,27.743000000000002,39.708,29.93 +2020-09-01 03:00:00,75.97,28.845,38.919000000000004,29.93 +2020-09-01 03:15:00,76.67,28.206,38.919000000000004,29.93 +2020-09-01 03:30:00,77.0,28.11,38.919000000000004,29.93 +2020-09-01 03:45:00,80.42,28.19,38.919000000000004,29.93 +2020-09-01 04:00:00,83.6,34.74,40.092,29.93 +2020-09-01 04:15:00,85.52,41.981,40.092,29.93 +2020-09-01 04:30:00,93.85,39.315,40.092,29.93 +2020-09-01 04:45:00,102.83,39.861999999999995,40.092,29.93 +2020-09-01 05:00:00,110.21,57.162,43.713,29.93 +2020-09-01 05:15:00,108.06,68.223,43.713,29.93 +2020-09-01 05:30:00,106.25,62.226000000000006,43.713,29.93 +2020-09-01 05:45:00,108.65,58.465,43.713,29.93 +2020-09-01 06:00:00,117.46,58.838,56.033,29.93 +2020-09-01 06:15:00,114.57,60.111000000000004,56.033,29.93 +2020-09-01 06:30:00,114.76,58.723,56.033,29.93 +2020-09-01 06:45:00,117.5,60.365,56.033,29.93 +2020-09-01 07:00:00,120.97,58.69,66.003,29.93 +2020-09-01 07:15:00,121.58,59.825,66.003,29.93 +2020-09-01 07:30:00,122.12,58.056000000000004,66.003,29.93 +2020-09-01 07:45:00,122.42,58.925,66.003,29.93 +2020-09-01 08:00:00,123.81,57.608999999999995,57.474,29.93 +2020-09-01 08:15:00,119.75,59.684,57.474,29.93 +2020-09-01 08:30:00,121.34,58.928999999999995,57.474,29.93 +2020-09-01 08:45:00,119.7,60.385,57.474,29.93 +2020-09-01 09:00:00,119.63,56.747,51.928000000000004,29.93 +2020-09-01 09:15:00,114.95,55.32899999999999,51.928000000000004,29.93 +2020-09-01 09:30:00,111.22,57.449,51.928000000000004,29.93 +2020-09-01 09:45:00,112.13,59.055,51.928000000000004,29.93 +2020-09-01 10:00:00,110.13,55.532,49.46,29.93 +2020-09-01 10:15:00,111.52,56.278999999999996,49.46,29.93 +2020-09-01 10:30:00,112.58,56.043,49.46,29.93 +2020-09-01 10:45:00,113.39,57.13399999999999,49.46,29.93 +2020-09-01 11:00:00,106.2,53.576,48.206,29.93 +2020-09-01 11:15:00,104.73,54.661,48.206,29.93 +2020-09-01 11:30:00,106.63,55.326,48.206,29.93 +2020-09-01 11:45:00,108.22,56.138000000000005,48.206,29.93 +2020-09-01 12:00:00,105.4,52.413999999999994,46.285,29.93 +2020-09-01 12:15:00,110.05,52.071000000000005,46.285,29.93 +2020-09-01 12:30:00,108.08,51.176,46.285,29.93 +2020-09-01 12:45:00,105.77,52.277,46.285,29.93 +2020-09-01 13:00:00,110.03,51.369,46.861999999999995,29.93 +2020-09-01 13:15:00,106.07,51.993,46.861999999999995,29.93 +2020-09-01 13:30:00,107.01,50.818000000000005,46.861999999999995,29.93 +2020-09-01 13:45:00,108.05,49.903999999999996,46.861999999999995,29.93 +2020-09-01 14:00:00,104.78,51.108000000000004,46.488,29.93 +2020-09-01 14:15:00,107.92,50.152,46.488,29.93 +2020-09-01 14:30:00,103.2,48.902,46.488,29.93 +2020-09-01 14:45:00,103.58,49.669,46.488,29.93 +2020-09-01 15:00:00,101.89,50.393,48.442,29.93 +2020-09-01 15:15:00,103.13,48.303000000000004,48.442,29.93 +2020-09-01 15:30:00,103.38,47.102,48.442,29.93 +2020-09-01 15:45:00,102.77,44.833999999999996,48.442,29.93 +2020-09-01 16:00:00,102.66,47.282,50.397,29.93 +2020-09-01 16:15:00,105.53,47.086000000000006,50.397,29.93 +2020-09-01 16:30:00,106.51,47.457,50.397,29.93 +2020-09-01 16:45:00,108.89,45.013999999999996,50.397,29.93 +2020-09-01 17:00:00,109.84,46.915,56.668,29.93 +2020-09-01 17:15:00,109.18,47.858000000000004,56.668,29.93 +2020-09-01 17:30:00,109.98,47.298,56.668,29.93 +2020-09-01 17:45:00,112.11,46.278999999999996,56.668,29.93 +2020-09-01 18:00:00,113.7,48.562,57.957,29.93 +2020-09-01 18:15:00,109.54,48.016999999999996,57.957,29.93 +2020-09-01 18:30:00,111.45,46.04,57.957,29.93 +2020-09-01 18:45:00,109.99,50.2,57.957,29.93 +2020-09-01 19:00:00,111.38,52.652,57.056000000000004,29.93 +2020-09-01 19:15:00,107.84,51.383,57.056000000000004,29.93 +2020-09-01 19:30:00,107.01,50.34,57.056000000000004,29.93 +2020-09-01 19:45:00,106.18,50.461999999999996,57.056000000000004,29.93 +2020-09-01 20:00:00,100.69,48.705,64.156,29.93 +2020-09-01 20:15:00,100.76,48.184,64.156,29.93 +2020-09-01 20:30:00,98.36,47.782,64.156,29.93 +2020-09-01 20:45:00,97.15,47.833,64.156,29.93 +2020-09-01 21:00:00,91.14,46.413000000000004,56.507,29.93 +2020-09-01 21:15:00,91.05,48.013000000000005,56.507,29.93 +2020-09-01 21:30:00,87.36,47.721000000000004,56.507,29.93 +2020-09-01 21:45:00,85.97,47.453,56.507,29.93 +2020-09-01 22:00:00,81.23,45.082,50.728,29.93 +2020-09-01 22:15:00,80.37,45.82899999999999,50.728,29.93 +2020-09-01 22:30:00,79.33,38.537,50.728,29.93 +2020-09-01 22:45:00,78.66,35.022,50.728,29.93 +2020-09-01 23:00:00,74.04,31.743000000000002,43.556999999999995,29.93 +2020-09-01 23:15:00,74.63,31.009,43.556999999999995,29.93 +2020-09-01 23:30:00,74.95,30.879,43.556999999999995,29.93 +2020-09-01 23:45:00,73.76,30.048000000000002,43.556999999999995,29.93 +2020-09-02 00:00:00,70.09,30.121,41.151,29.93 +2020-09-02 00:15:00,71.22,30.691,41.151,29.93 +2020-09-02 00:30:00,70.52,30.272,41.151,29.93 +2020-09-02 00:45:00,70.04,30.22,41.151,29.93 +2020-09-02 01:00:00,70.4,29.961,37.763000000000005,29.93 +2020-09-02 01:15:00,71.55,29.159000000000002,37.763000000000005,29.93 +2020-09-02 01:30:00,70.54,28.076999999999998,37.763000000000005,29.93 +2020-09-02 01:45:00,71.31,27.156,37.763000000000005,29.93 +2020-09-02 02:00:00,70.05,26.865,35.615,29.93 +2020-09-02 02:15:00,70.33,26.489,35.615,29.93 +2020-09-02 02:30:00,70.42,27.131999999999998,35.615,29.93 +2020-09-02 02:45:00,71.01,27.975,35.615,29.93 +2020-09-02 03:00:00,71.66,29.066,35.153,29.93 +2020-09-02 03:15:00,73.29,28.445,35.153,29.93 +2020-09-02 03:30:00,74.2,28.353,35.153,29.93 +2020-09-02 03:45:00,76.12,28.423000000000002,35.153,29.93 +2020-09-02 04:00:00,79.71,35.003,36.203,29.93 +2020-09-02 04:15:00,83.25,42.268,36.203,29.93 +2020-09-02 04:30:00,93.94,39.609,36.203,29.93 +2020-09-02 04:45:00,98.95,40.161,36.203,29.93 +2020-09-02 05:00:00,102.22,57.54600000000001,39.922,29.93 +2020-09-02 05:15:00,99.73,68.696,39.922,29.93 +2020-09-02 05:30:00,104.42,62.69,39.922,29.93 +2020-09-02 05:45:00,106.12,58.883,39.922,29.93 +2020-09-02 06:00:00,110.69,59.221000000000004,56.443999999999996,29.93 +2020-09-02 06:15:00,110.55,60.515,56.443999999999996,29.93 +2020-09-02 06:30:00,112.02,59.126999999999995,56.443999999999996,29.93 +2020-09-02 06:45:00,111.67,60.766000000000005,56.443999999999996,29.93 +2020-09-02 07:00:00,114.69,59.091,68.683,29.93 +2020-09-02 07:15:00,114.16,60.243,68.683,29.93 +2020-09-02 07:30:00,112.53,58.504,68.683,29.93 +2020-09-02 07:45:00,112.99,59.375,68.683,29.93 +2020-09-02 08:00:00,112.66,58.06399999999999,59.003,29.93 +2020-09-02 08:15:00,112.55,60.106,59.003,29.93 +2020-09-02 08:30:00,111.76,59.354,59.003,29.93 +2020-09-02 08:45:00,108.42,60.795,59.003,29.93 +2020-09-02 09:00:00,107.65,57.165,56.21,29.93 +2020-09-02 09:15:00,106.69,55.736000000000004,56.21,29.93 +2020-09-02 09:30:00,106.79,57.836000000000006,56.21,29.93 +2020-09-02 09:45:00,106.64,59.413999999999994,56.21,29.93 +2020-09-02 10:00:00,106.81,55.888999999999996,52.358999999999995,29.93 +2020-09-02 10:15:00,107.49,56.603,52.358999999999995,29.93 +2020-09-02 10:30:00,104.67,56.357,52.358999999999995,29.93 +2020-09-02 10:45:00,104.68,57.435,52.358999999999995,29.93 +2020-09-02 11:00:00,102.67,53.89,51.161,29.93 +2020-09-02 11:15:00,101.54,54.961000000000006,51.161,29.93 +2020-09-02 11:30:00,105.83,55.623999999999995,51.161,29.93 +2020-09-02 11:45:00,103.98,56.419,51.161,29.93 +2020-09-02 12:00:00,99.78,52.676,49.119,29.93 +2020-09-02 12:15:00,101.12,52.318999999999996,49.119,29.93 +2020-09-02 12:30:00,101.87,51.45,49.119,29.93 +2020-09-02 12:45:00,104.35,52.54,49.119,29.93 +2020-09-02 13:00:00,100.22,51.608000000000004,49.187,29.93 +2020-09-02 13:15:00,101.85,52.223,49.187,29.93 +2020-09-02 13:30:00,99.0,51.04600000000001,49.187,29.93 +2020-09-02 13:45:00,102.96,50.141999999999996,49.187,29.93 +2020-09-02 14:00:00,102.01,51.306999999999995,49.787,29.93 +2020-09-02 14:15:00,102.18,50.363,49.787,29.93 +2020-09-02 14:30:00,100.6,49.137,49.787,29.93 +2020-09-02 14:45:00,103.38,49.903,49.787,29.93 +2020-09-02 15:00:00,106.74,50.576,51.458999999999996,29.93 +2020-09-02 15:15:00,101.18,48.5,51.458999999999996,29.93 +2020-09-02 15:30:00,102.11,47.323,51.458999999999996,29.93 +2020-09-02 15:45:00,100.59,45.065,51.458999999999996,29.93 +2020-09-02 16:00:00,102.98,47.48,53.663000000000004,29.93 +2020-09-02 16:15:00,103.91,47.295,53.663000000000004,29.93 +2020-09-02 16:30:00,108.38,47.666000000000004,53.663000000000004,29.93 +2020-09-02 16:45:00,107.21,45.275,53.663000000000004,29.93 +2020-09-02 17:00:00,108.81,47.14,58.183,29.93 +2020-09-02 17:15:00,109.2,48.111000000000004,58.183,29.93 +2020-09-02 17:30:00,109.84,47.558,58.183,29.93 +2020-09-02 17:45:00,111.27,46.574,58.183,29.93 +2020-09-02 18:00:00,113.28,48.843,60.141000000000005,29.93 +2020-09-02 18:15:00,110.36,48.303999999999995,60.141000000000005,29.93 +2020-09-02 18:30:00,108.48,46.339,60.141000000000005,29.93 +2020-09-02 18:45:00,112.25,50.495,60.141000000000005,29.93 +2020-09-02 19:00:00,116.4,52.958999999999996,60.582,29.93 +2020-09-02 19:15:00,114.79,51.688,60.582,29.93 +2020-09-02 19:30:00,113.75,50.644,60.582,29.93 +2020-09-02 19:45:00,114.25,50.766000000000005,60.582,29.93 +2020-09-02 20:00:00,110.94,49.023,66.61,29.93 +2020-09-02 20:15:00,106.94,48.503,66.61,29.93 +2020-09-02 20:30:00,102.41,48.07899999999999,66.61,29.93 +2020-09-02 20:45:00,98.12,48.092,66.61,29.93 +2020-09-02 21:00:00,91.53,46.675,57.658,29.93 +2020-09-02 21:15:00,91.25,48.266000000000005,57.658,29.93 +2020-09-02 21:30:00,86.71,47.976000000000006,57.658,29.93 +2020-09-02 21:45:00,86.21,47.672,57.658,29.93 +2020-09-02 22:00:00,80.75,45.28,51.81,29.93 +2020-09-02 22:15:00,81.19,46.005,51.81,29.93 +2020-09-02 22:30:00,80.24,38.691,51.81,29.93 +2020-09-02 22:45:00,83.69,35.178000000000004,51.81,29.93 +2020-09-02 23:00:00,82.31,31.945,42.93600000000001,29.93 +2020-09-02 23:15:00,84.53,31.188000000000002,42.93600000000001,29.93 +2020-09-02 23:30:00,80.72,31.066,42.93600000000001,29.93 +2020-09-02 23:45:00,74.93,30.238000000000003,42.93600000000001,29.93 +2020-09-03 00:00:00,78.78,30.328000000000003,39.211,29.93 +2020-09-03 00:15:00,80.39,30.897,39.211,29.93 +2020-09-03 00:30:00,80.32,30.485,39.211,29.93 +2020-09-03 00:45:00,73.95,30.44,39.211,29.93 +2020-09-03 01:00:00,73.78,30.165,37.607,29.93 +2020-09-03 01:15:00,79.05,29.381,37.607,29.93 +2020-09-03 01:30:00,79.16,28.316,37.607,29.93 +2020-09-03 01:45:00,76.51,27.395,37.607,29.93 +2020-09-03 02:00:00,74.11,27.108,36.44,29.93 +2020-09-03 02:15:00,80.14,26.754,36.44,29.93 +2020-09-03 02:30:00,79.59,27.372,36.44,29.93 +2020-09-03 02:45:00,78.57,28.211,36.44,29.93 +2020-09-03 03:00:00,73.82,29.289,36.116,29.93 +2020-09-03 03:15:00,72.93,28.685,36.116,29.93 +2020-09-03 03:30:00,74.34,28.599,36.116,29.93 +2020-09-03 03:45:00,77.3,28.656999999999996,36.116,29.93 +2020-09-03 04:00:00,81.78,35.27,37.398,29.93 +2020-09-03 04:15:00,82.8,42.558,37.398,29.93 +2020-09-03 04:30:00,91.14,39.907,37.398,29.93 +2020-09-03 04:45:00,98.67,40.465,37.398,29.93 +2020-09-03 05:00:00,108.57,57.937,41.776,29.93 +2020-09-03 05:15:00,103.7,69.179,41.776,29.93 +2020-09-03 05:30:00,104.33,63.162,41.776,29.93 +2020-09-03 05:45:00,105.67,59.306999999999995,41.776,29.93 +2020-09-03 06:00:00,110.59,59.608999999999995,55.61,29.93 +2020-09-03 06:15:00,111.02,60.928000000000004,55.61,29.93 +2020-09-03 06:30:00,112.63,59.537,55.61,29.93 +2020-09-03 06:45:00,113.01,61.175,55.61,29.93 +2020-09-03 07:00:00,115.07,59.497,67.13600000000001,29.93 +2020-09-03 07:15:00,114.55,60.665,67.13600000000001,29.93 +2020-09-03 07:30:00,112.42,58.958,67.13600000000001,29.93 +2020-09-03 07:45:00,112.27,59.832,67.13600000000001,29.93 +2020-09-03 08:00:00,109.14,58.523999999999994,57.55,29.93 +2020-09-03 08:15:00,107.8,60.532,57.55,29.93 +2020-09-03 08:30:00,111.96,59.786,57.55,29.93 +2020-09-03 08:45:00,108.99,61.208,57.55,29.93 +2020-09-03 09:00:00,108.66,57.586000000000006,52.931999999999995,29.93 +2020-09-03 09:15:00,108.25,56.148999999999994,52.931999999999995,29.93 +2020-09-03 09:30:00,107.97,58.228,52.931999999999995,29.93 +2020-09-03 09:45:00,106.96,59.778,52.931999999999995,29.93 +2020-09-03 10:00:00,107.82,56.248999999999995,50.36600000000001,29.93 +2020-09-03 10:15:00,109.16,56.93,50.36600000000001,29.93 +2020-09-03 10:30:00,108.59,56.674,50.36600000000001,29.93 +2020-09-03 10:45:00,107.7,57.74,50.36600000000001,29.93 +2020-09-03 11:00:00,107.03,54.208,47.893,29.93 +2020-09-03 11:15:00,107.11,55.266000000000005,47.893,29.93 +2020-09-03 11:30:00,109.86,55.926,47.893,29.93 +2020-09-03 11:45:00,106.11,56.703,47.893,29.93 +2020-09-03 12:00:00,104.22,52.94,45.271,29.93 +2020-09-03 12:15:00,106.28,52.57,45.271,29.93 +2020-09-03 12:30:00,105.6,51.727,45.271,29.93 +2020-09-03 12:45:00,104.71,52.805,45.271,29.93 +2020-09-03 13:00:00,106.15,51.85,44.351000000000006,29.93 +2020-09-03 13:15:00,107.6,52.457,44.351000000000006,29.93 +2020-09-03 13:30:00,113.46,51.277,44.351000000000006,29.93 +2020-09-03 13:45:00,112.08,50.383,44.351000000000006,29.93 +2020-09-03 14:00:00,113.14,51.511,44.99,29.93 +2020-09-03 14:15:00,108.1,50.577,44.99,29.93 +2020-09-03 14:30:00,104.21,49.376000000000005,44.99,29.93 +2020-09-03 14:45:00,103.23,50.141000000000005,44.99,29.93 +2020-09-03 15:00:00,107.19,50.763000000000005,46.869,29.93 +2020-09-03 15:15:00,106.83,48.701,46.869,29.93 +2020-09-03 15:30:00,105.79,47.54600000000001,46.869,29.93 +2020-09-03 15:45:00,104.72,45.301,46.869,29.93 +2020-09-03 16:00:00,105.33,47.681000000000004,48.902,29.93 +2020-09-03 16:15:00,105.72,47.508,48.902,29.93 +2020-09-03 16:30:00,107.33,47.879,48.902,29.93 +2020-09-03 16:45:00,110.5,45.538999999999994,48.902,29.93 +2020-09-03 17:00:00,112.54,47.36600000000001,53.244,29.93 +2020-09-03 17:15:00,111.29,48.365,53.244,29.93 +2020-09-03 17:30:00,110.8,47.821999999999996,53.244,29.93 +2020-09-03 17:45:00,111.93,46.872,53.244,29.93 +2020-09-03 18:00:00,112.15,49.126999999999995,54.343999999999994,29.93 +2020-09-03 18:15:00,110.62,48.596000000000004,54.343999999999994,29.93 +2020-09-03 18:30:00,112.94,46.643,54.343999999999994,29.93 +2020-09-03 18:45:00,111.47,50.794,54.343999999999994,29.93 +2020-09-03 19:00:00,112.83,53.269,54.332,29.93 +2020-09-03 19:15:00,109.18,51.998000000000005,54.332,29.93 +2020-09-03 19:30:00,109.17,50.953,54.332,29.93 +2020-09-03 19:45:00,112.48,51.073,54.332,29.93 +2020-09-03 20:00:00,107.89,49.346000000000004,58.06,29.93 +2020-09-03 20:15:00,107.07,48.826,58.06,29.93 +2020-09-03 20:30:00,101.06,48.38,58.06,29.93 +2020-09-03 20:45:00,99.03,48.354,58.06,29.93 +2020-09-03 21:00:00,91.24,46.941,52.411,29.93 +2020-09-03 21:15:00,98.02,48.522,52.411,29.93 +2020-09-03 21:30:00,96.57,48.235,52.411,29.93 +2020-09-03 21:45:00,96.41,47.895,52.411,29.93 +2020-09-03 22:00:00,88.52,45.481,47.148999999999994,29.93 +2020-09-03 22:15:00,84.04,46.18600000000001,47.148999999999994,29.93 +2020-09-03 22:30:00,85.59,38.847,47.148999999999994,29.93 +2020-09-03 22:45:00,88.8,35.336999999999996,47.148999999999994,29.93 +2020-09-03 23:00:00,84.03,32.149,40.814,29.93 +2020-09-03 23:15:00,81.23,31.37,40.814,29.93 +2020-09-03 23:30:00,78.97,31.255,40.814,29.93 +2020-09-03 23:45:00,84.43,30.430999999999997,40.814,29.93 +2020-09-04 00:00:00,79.88,28.809,39.153,29.93 +2020-09-04 00:15:00,81.93,29.595,39.153,29.93 +2020-09-04 00:30:00,76.83,29.414,39.153,29.93 +2020-09-04 00:45:00,81.19,29.766,39.153,29.93 +2020-09-04 01:00:00,78.59,29.094,37.228,29.93 +2020-09-04 01:15:00,75.78,27.914,37.228,29.93 +2020-09-04 01:30:00,74.53,27.471,37.228,29.93 +2020-09-04 01:45:00,76.53,26.340999999999998,37.228,29.93 +2020-09-04 02:00:00,79.34,26.897,35.851,29.93 +2020-09-04 02:15:00,80.68,26.505,35.851,29.93 +2020-09-04 02:30:00,79.68,27.898000000000003,35.851,29.93 +2020-09-04 02:45:00,72.95,28.131,35.851,29.93 +2020-09-04 03:00:00,71.7,29.756,36.54,29.93 +2020-09-04 03:15:00,72.28,28.221,36.54,29.93 +2020-09-04 03:30:00,76.49,27.927,36.54,29.93 +2020-09-04 03:45:00,83.91,28.791999999999998,36.54,29.93 +2020-09-04 04:00:00,87.81,35.59,37.578,29.93 +2020-09-04 04:15:00,87.55,41.49100000000001,37.578,29.93 +2020-09-04 04:30:00,92.55,39.719,37.578,29.93 +2020-09-04 04:45:00,98.47,39.561,37.578,29.93 +2020-09-04 05:00:00,103.34,56.532,40.387,29.93 +2020-09-04 05:15:00,107.52,68.905,40.387,29.93 +2020-09-04 05:30:00,105.74,63.184,40.387,29.93 +2020-09-04 05:45:00,109.74,58.878,40.387,29.93 +2020-09-04 06:00:00,115.98,59.403999999999996,54.668,29.93 +2020-09-04 06:15:00,115.47,60.705,54.668,29.93 +2020-09-04 06:30:00,117.38,59.156000000000006,54.668,29.93 +2020-09-04 06:45:00,119.84,60.883,54.668,29.93 +2020-09-04 07:00:00,121.76,59.673,63.971000000000004,29.93 +2020-09-04 07:15:00,124.37,61.793,63.971000000000004,29.93 +2020-09-04 07:30:00,124.71,58.375,63.971000000000004,29.93 +2020-09-04 07:45:00,124.2,58.946000000000005,63.971000000000004,29.93 +2020-09-04 08:00:00,125.37,58.18,56.042,29.93 +2020-09-04 08:15:00,124.6,60.688,56.042,29.93 +2020-09-04 08:30:00,128.49,59.994,56.042,29.93 +2020-09-04 08:45:00,128.74,61.058,56.042,29.93 +2020-09-04 09:00:00,130.79,55.483000000000004,52.832,29.93 +2020-09-04 09:15:00,128.22,55.792,52.832,29.93 +2020-09-04 09:30:00,125.85,57.18899999999999,52.832,29.93 +2020-09-04 09:45:00,122.8,59.033,52.832,29.93 +2020-09-04 10:00:00,123.79,55.185,50.044,29.93 +2020-09-04 10:15:00,126.55,55.742,50.044,29.93 +2020-09-04 10:30:00,128.28,55.945,50.044,29.93 +2020-09-04 10:45:00,124.38,56.851000000000006,50.044,29.93 +2020-09-04 11:00:00,124.72,53.549,49.06100000000001,29.93 +2020-09-04 11:15:00,126.1,53.519,49.06100000000001,29.93 +2020-09-04 11:30:00,129.93,54.115,49.06100000000001,29.93 +2020-09-04 11:45:00,120.43,54.05,49.06100000000001,29.93 +2020-09-04 12:00:00,116.34,50.803999999999995,45.595,29.93 +2020-09-04 12:15:00,122.71,49.553999999999995,45.595,29.93 +2020-09-04 12:30:00,109.57,48.861999999999995,45.595,29.93 +2020-09-04 12:45:00,103.66,49.325,45.595,29.93 +2020-09-04 13:00:00,100.92,48.995,43.218,29.93 +2020-09-04 13:15:00,101.16,49.873000000000005,43.218,29.93 +2020-09-04 13:30:00,100.93,49.382,43.218,29.93 +2020-09-04 13:45:00,100.61,48.751999999999995,43.218,29.93 +2020-09-04 14:00:00,99.88,48.977,41.926,29.93 +2020-09-04 14:15:00,100.39,48.406000000000006,41.926,29.93 +2020-09-04 14:30:00,103.67,48.613,41.926,29.93 +2020-09-04 14:45:00,105.03,48.823,41.926,29.93 +2020-09-04 15:00:00,106.17,49.254,43.79,29.93 +2020-09-04 15:15:00,101.95,46.93600000000001,43.79,29.93 +2020-09-04 15:30:00,95.93,45.091,43.79,29.93 +2020-09-04 15:45:00,97.23,43.538999999999994,43.79,29.93 +2020-09-04 16:00:00,99.92,44.988,45.895,29.93 +2020-09-04 16:15:00,100.88,45.3,45.895,29.93 +2020-09-04 16:30:00,104.55,45.525,45.895,29.93 +2020-09-04 16:45:00,106.79,42.534,45.895,29.93 +2020-09-04 17:00:00,106.67,45.867,51.36,29.93 +2020-09-04 17:15:00,105.13,46.685,51.36,29.93 +2020-09-04 17:30:00,106.1,46.251999999999995,51.36,29.93 +2020-09-04 17:45:00,108.53,45.159,51.36,29.93 +2020-09-04 18:00:00,108.52,47.55,52.985,29.93 +2020-09-04 18:15:00,105.88,46.141999999999996,52.985,29.93 +2020-09-04 18:30:00,108.88,44.162,52.985,29.93 +2020-09-04 18:45:00,110.64,48.68,52.985,29.93 +2020-09-04 19:00:00,111.33,52.07,52.602,29.93 +2020-09-04 19:15:00,112.44,51.495,52.602,29.93 +2020-09-04 19:30:00,112.3,50.453,52.602,29.93 +2020-09-04 19:45:00,106.77,49.611999999999995,52.602,29.93 +2020-09-04 20:00:00,98.61,47.763000000000005,58.063,29.93 +2020-09-04 20:15:00,99.3,47.946999999999996,58.063,29.93 +2020-09-04 20:30:00,95.69,47.044,58.063,29.93 +2020-09-04 20:45:00,93.25,46.33,58.063,29.93 +2020-09-04 21:00:00,88.17,46.15,50.135,29.93 +2020-09-04 21:15:00,88.23,49.258,50.135,29.93 +2020-09-04 21:30:00,85.09,48.846000000000004,50.135,29.93 +2020-09-04 21:45:00,85.78,48.718999999999994,50.135,29.93 +2020-09-04 22:00:00,82.3,46.283,45.165,29.93 +2020-09-04 22:15:00,81.47,46.729,45.165,29.93 +2020-09-04 22:30:00,77.2,44.387,45.165,29.93 +2020-09-04 22:45:00,79.3,42.086000000000006,45.165,29.93 +2020-09-04 23:00:00,74.88,40.385,39.121,29.93 +2020-09-04 23:15:00,70.48,37.946999999999996,39.121,29.93 +2020-09-04 23:30:00,72.77,36.041,39.121,29.93 +2020-09-04 23:45:00,73.16,35.028,39.121,29.93 +2020-09-05 00:00:00,69.6,29.750999999999998,38.49,29.816 +2020-09-05 00:15:00,65.9,29.398000000000003,38.49,29.816 +2020-09-05 00:30:00,61.59,28.978,38.49,29.816 +2020-09-05 00:45:00,64.02,28.78,38.49,29.816 +2020-09-05 01:00:00,62.98,28.416999999999998,34.5,29.816 +2020-09-05 01:15:00,69.08,27.651,34.5,29.816 +2020-09-05 01:30:00,69.9,26.445999999999998,34.5,29.816 +2020-09-05 01:45:00,65.62,26.391,34.5,29.816 +2020-09-05 02:00:00,59.22,26.165,32.236,29.816 +2020-09-05 02:15:00,61.62,25.051,32.236,29.816 +2020-09-05 02:30:00,60.74,25.576999999999998,32.236,29.816 +2020-09-05 02:45:00,61.81,26.514,32.236,29.816 +2020-09-05 03:00:00,59.94,26.993000000000002,32.067,29.816 +2020-09-05 03:15:00,61.24,24.705,32.067,29.816 +2020-09-05 03:30:00,61.65,24.504,32.067,29.816 +2020-09-05 03:45:00,61.59,26.679000000000002,32.067,29.816 +2020-09-05 04:00:00,61.57,31.316999999999997,33.071,29.816 +2020-09-05 04:15:00,59.69,36.134,33.071,29.816 +2020-09-05 04:30:00,62.02,32.619,33.071,29.816 +2020-09-05 04:45:00,63.77,32.647,33.071,29.816 +2020-09-05 05:00:00,64.24,40.606,33.014,29.816 +2020-09-05 05:15:00,65.87,41.105,33.014,29.816 +2020-09-05 05:30:00,65.26,36.77,33.014,29.816 +2020-09-05 05:45:00,64.15,37.029,33.014,29.816 +2020-09-05 06:00:00,67.9,49.608999999999995,34.628,29.816 +2020-09-05 06:15:00,70.61,59.795,34.628,29.816 +2020-09-05 06:30:00,71.88,55.083,34.628,29.816 +2020-09-05 06:45:00,72.73,52.895,34.628,29.816 +2020-09-05 07:00:00,75.06,49.843999999999994,38.871,29.816 +2020-09-05 07:15:00,73.03,50.681999999999995,38.871,29.816 +2020-09-05 07:30:00,77.02,48.961999999999996,38.871,29.816 +2020-09-05 07:45:00,79.22,50.842,38.871,29.816 +2020-09-05 08:00:00,77.29,51.131,43.293,29.816 +2020-09-05 08:15:00,78.24,53.879,43.293,29.816 +2020-09-05 08:30:00,76.23,53.385,43.293,29.816 +2020-09-05 08:45:00,75.72,55.662,43.293,29.816 +2020-09-05 09:00:00,74.97,53.013000000000005,44.559,29.816 +2020-09-05 09:15:00,72.64,53.833,44.559,29.816 +2020-09-05 09:30:00,73.09,55.76,44.559,29.816 +2020-09-05 09:45:00,75.25,57.203,44.559,29.816 +2020-09-05 10:00:00,74.29,53.878,42.091,29.816 +2020-09-05 10:15:00,73.61,54.733000000000004,42.091,29.816 +2020-09-05 10:30:00,73.64,54.651,42.091,29.816 +2020-09-05 10:45:00,71.59,55.418,42.091,29.816 +2020-09-05 11:00:00,70.86,52.035,38.505,29.816 +2020-09-05 11:15:00,67.38,52.648,38.505,29.816 +2020-09-05 11:30:00,67.78,53.333999999999996,38.505,29.816 +2020-09-05 11:45:00,67.79,53.699,38.505,29.816 +2020-09-05 12:00:00,64.86,50.608999999999995,35.388000000000005,29.816 +2020-09-05 12:15:00,64.27,50.17,35.388000000000005,29.816 +2020-09-05 12:30:00,61.73,49.437,35.388000000000005,29.816 +2020-09-05 12:45:00,61.93,50.407,35.388000000000005,29.816 +2020-09-05 13:00:00,57.25,49.278999999999996,31.355999999999998,29.816 +2020-09-05 13:15:00,60.01,49.393,31.355999999999998,29.816 +2020-09-05 13:30:00,60.1,48.998000000000005,31.355999999999998,29.816 +2020-09-05 13:45:00,58.58,47.281000000000006,31.355999999999998,29.816 +2020-09-05 14:00:00,62.4,47.692,30.522,29.816 +2020-09-05 14:15:00,62.32,45.977,30.522,29.816 +2020-09-05 14:30:00,69.06,45.586000000000006,30.522,29.816 +2020-09-05 14:45:00,67.31,46.24,30.522,29.816 +2020-09-05 15:00:00,64.95,47.103,34.36,29.816 +2020-09-05 15:15:00,68.02,45.523,34.36,29.816 +2020-09-05 15:30:00,73.3,44.092,34.36,29.816 +2020-09-05 15:45:00,66.23,41.758,34.36,29.816 +2020-09-05 16:00:00,70.0,44.927,39.507,29.816 +2020-09-05 16:15:00,70.69,44.593999999999994,39.507,29.816 +2020-09-05 16:30:00,72.97,45.01,39.507,29.816 +2020-09-05 16:45:00,75.43,42.166000000000004,39.507,29.816 +2020-09-05 17:00:00,78.45,44.356,47.151,29.816 +2020-09-05 17:15:00,79.25,43.55,47.151,29.816 +2020-09-05 17:30:00,80.34,42.998000000000005,47.151,29.816 +2020-09-05 17:45:00,81.44,42.316,47.151,29.816 +2020-09-05 18:00:00,84.42,45.887,50.303999999999995,29.816 +2020-09-05 18:15:00,82.25,46.185,50.303999999999995,29.816 +2020-09-05 18:30:00,82.55,45.576,50.303999999999995,29.816 +2020-09-05 18:45:00,85.27,46.61600000000001,50.303999999999995,29.816 +2020-09-05 19:00:00,83.52,48.75899999999999,50.622,29.816 +2020-09-05 19:15:00,83.04,47.233000000000004,50.622,29.816 +2020-09-05 19:30:00,81.56,46.961999999999996,50.622,29.816 +2020-09-05 19:45:00,80.53,47.653,50.622,29.816 +2020-09-05 20:00:00,77.83,46.8,45.391000000000005,29.816 +2020-09-05 20:15:00,76.29,46.696000000000005,45.391000000000005,29.816 +2020-09-05 20:30:00,75.74,44.957,45.391000000000005,29.816 +2020-09-05 20:45:00,75.02,45.777,45.391000000000005,29.816 +2020-09-05 21:00:00,71.28,44.6,39.98,29.816 +2020-09-05 21:15:00,70.5,47.446000000000005,39.98,29.816 +2020-09-05 21:30:00,67.18,47.358999999999995,39.98,29.816 +2020-09-05 21:45:00,67.05,46.666000000000004,39.98,29.816 +2020-09-05 22:00:00,64.24,44.354,37.53,29.816 +2020-09-05 22:15:00,62.85,45.309,37.53,29.816 +2020-09-05 22:30:00,60.44,43.391000000000005,37.53,29.816 +2020-09-05 22:45:00,60.44,41.656000000000006,37.53,29.816 +2020-09-05 23:00:00,55.7,39.704,30.97,29.816 +2020-09-05 23:15:00,55.88,37.408,30.97,29.816 +2020-09-05 23:30:00,55.87,37.45,30.97,29.816 +2020-09-05 23:45:00,54.85,36.455,30.97,29.816 +2020-09-06 00:00:00,51.94,31.230999999999998,27.24,29.816 +2020-09-06 00:15:00,52.98,29.822,27.24,29.816 +2020-09-06 00:30:00,51.93,29.235,27.24,29.816 +2020-09-06 00:45:00,52.47,29.033,27.24,29.816 +2020-09-06 01:00:00,50.9,28.868000000000002,25.662,29.816 +2020-09-06 01:15:00,51.69,28.144000000000002,25.662,29.816 +2020-09-06 01:30:00,51.06,26.9,25.662,29.816 +2020-09-06 01:45:00,51.75,26.471999999999998,25.662,29.816 +2020-09-06 02:00:00,50.22,26.226,25.67,29.816 +2020-09-06 02:15:00,50.83,25.649,25.67,29.816 +2020-09-06 02:30:00,50.93,26.423000000000002,25.67,29.816 +2020-09-06 02:45:00,51.01,27.158,25.67,29.816 +2020-09-06 03:00:00,50.72,28.236,24.258000000000003,29.816 +2020-09-06 03:15:00,51.86,26.129,24.258000000000003,29.816 +2020-09-06 03:30:00,51.93,25.463,24.258000000000003,29.816 +2020-09-06 03:45:00,52.05,26.918000000000003,24.258000000000003,29.816 +2020-09-06 04:00:00,52.55,31.531,25.051,29.816 +2020-09-06 04:15:00,52.95,35.867,25.051,29.816 +2020-09-06 04:30:00,53.59,33.545,25.051,29.816 +2020-09-06 04:45:00,54.33,33.202,25.051,29.816 +2020-09-06 05:00:00,56.46,40.988,25.145,29.816 +2020-09-06 05:15:00,55.34,40.624,25.145,29.816 +2020-09-06 05:30:00,53.86,35.900999999999996,25.145,29.816 +2020-09-06 05:45:00,55.65,35.897,25.145,29.816 +2020-09-06 06:00:00,56.53,46.333999999999996,26.371,29.816 +2020-09-06 06:15:00,57.41,57.077,26.371,29.816 +2020-09-06 06:30:00,58.46,51.619,26.371,29.816 +2020-09-06 06:45:00,60.16,48.426,26.371,29.816 +2020-09-06 07:00:00,60.1,45.915,28.756999999999998,29.816 +2020-09-06 07:15:00,60.26,45.225,28.756999999999998,29.816 +2020-09-06 07:30:00,65.45,44.508,28.756999999999998,29.816 +2020-09-06 07:45:00,66.55,46.265,28.756999999999998,29.816 +2020-09-06 08:00:00,66.48,47.442,32.82,29.816 +2020-09-06 08:15:00,65.64,51.174,32.82,29.816 +2020-09-06 08:30:00,58.67,51.653,32.82,29.816 +2020-09-06 08:45:00,61.25,54.093,32.82,29.816 +2020-09-06 09:00:00,63.13,51.301,35.534,29.816 +2020-09-06 09:15:00,57.61,51.75,35.534,29.816 +2020-09-06 09:30:00,58.16,54.012,35.534,29.816 +2020-09-06 09:45:00,60.38,56.286,35.534,29.816 +2020-09-06 10:00:00,61.07,53.742,35.925,29.816 +2020-09-06 10:15:00,60.78,54.75,35.925,29.816 +2020-09-06 10:30:00,63.8,54.928000000000004,35.925,29.816 +2020-09-06 10:45:00,61.65,56.318000000000005,35.925,29.816 +2020-09-06 11:00:00,61.14,52.763999999999996,37.056,29.816 +2020-09-06 11:15:00,57.92,52.993,37.056,29.816 +2020-09-06 11:30:00,60.04,54.018,37.056,29.816 +2020-09-06 11:45:00,59.7,54.666000000000004,37.056,29.816 +2020-09-06 12:00:00,59.47,52.391999999999996,33.124,29.816 +2020-09-06 12:15:00,59.46,51.644,33.124,29.816 +2020-09-06 12:30:00,57.73,50.933,33.124,29.816 +2020-09-06 12:45:00,56.0,51.214,33.124,29.816 +2020-09-06 13:00:00,53.2,49.711000000000006,29.874000000000002,29.816 +2020-09-06 13:15:00,54.82,49.708,29.874000000000002,29.816 +2020-09-06 13:30:00,54.67,48.316,29.874000000000002,29.816 +2020-09-06 13:45:00,53.78,47.49100000000001,29.874000000000002,29.816 +2020-09-06 14:00:00,52.66,48.938,27.302,29.816 +2020-09-06 14:15:00,55.42,47.761,27.302,29.816 +2020-09-06 14:30:00,57.66,46.483000000000004,27.302,29.816 +2020-09-06 14:45:00,57.04,46.153,27.302,29.816 +2020-09-06 15:00:00,57.93,46.895,27.642,29.816 +2020-09-06 15:15:00,56.35,44.723,27.642,29.816 +2020-09-06 15:30:00,55.04,43.233000000000004,27.642,29.816 +2020-09-06 15:45:00,55.71,41.247,27.642,29.816 +2020-09-06 16:00:00,59.68,43.17,31.945999999999998,29.816 +2020-09-06 16:15:00,60.8,42.937,31.945999999999998,29.816 +2020-09-06 16:30:00,66.16,44.26,31.945999999999998,29.816 +2020-09-06 16:45:00,71.81,41.54,31.945999999999998,29.816 +2020-09-06 17:00:00,76.25,44.00899999999999,40.387,29.816 +2020-09-06 17:15:00,77.49,44.511,40.387,29.816 +2020-09-06 17:30:00,78.34,44.708,40.387,29.816 +2020-09-06 17:45:00,74.93,44.711000000000006,40.387,29.816 +2020-09-06 18:00:00,79.12,48.699,44.575,29.816 +2020-09-06 18:15:00,78.19,48.806999999999995,44.575,29.816 +2020-09-06 18:30:00,80.41,47.744,44.575,29.816 +2020-09-06 18:45:00,80.48,49.102,44.575,29.816 +2020-09-06 19:00:00,92.24,53.111000000000004,45.623999999999995,29.816 +2020-09-06 19:15:00,91.93,50.673,45.623999999999995,29.816 +2020-09-06 19:30:00,89.23,50.163999999999994,45.623999999999995,29.816 +2020-09-06 19:45:00,85.71,50.617,45.623999999999995,29.816 +2020-09-06 20:00:00,81.21,49.949,44.583999999999996,29.816 +2020-09-06 20:15:00,87.39,49.841,44.583999999999996,29.816 +2020-09-06 20:30:00,86.81,48.938,44.583999999999996,29.816 +2020-09-06 20:45:00,86.6,48.099,44.583999999999996,29.816 +2020-09-06 21:00:00,78.52,46.498999999999995,39.732,29.816 +2020-09-06 21:15:00,79.0,48.997,39.732,29.816 +2020-09-06 21:30:00,80.14,48.376999999999995,39.732,29.816 +2020-09-06 21:45:00,77.29,47.978,39.732,29.816 +2020-09-06 22:00:00,73.53,47.293,38.571,29.816 +2020-09-06 22:15:00,73.0,46.657,38.571,29.816 +2020-09-06 22:30:00,71.65,43.901,38.571,29.816 +2020-09-06 22:45:00,71.15,40.95,38.571,29.816 +2020-09-06 23:00:00,65.54,38.457,33.121,29.816 +2020-09-06 23:15:00,66.93,37.468,33.121,29.816 +2020-09-06 23:30:00,66.52,37.135999999999996,33.121,29.816 +2020-09-06 23:45:00,66.55,36.387,33.121,29.816 +2020-09-07 00:00:00,64.09,33.271,32.506,29.93 +2020-09-07 00:15:00,66.1,32.906,32.506,29.93 +2020-09-07 00:30:00,65.16,32.003,32.506,29.93 +2020-09-07 00:45:00,72.59,31.397,32.506,29.93 +2020-09-07 01:00:00,71.19,31.555999999999997,31.121,29.93 +2020-09-07 01:15:00,72.55,30.802,31.121,29.93 +2020-09-07 01:30:00,70.95,29.899,31.121,29.93 +2020-09-07 01:45:00,71.59,29.4,31.121,29.93 +2020-09-07 02:00:00,71.83,29.554000000000002,29.605999999999998,29.93 +2020-09-07 02:15:00,71.29,28.28,29.605999999999998,29.93 +2020-09-07 02:30:00,65.32,29.213,29.605999999999998,29.93 +2020-09-07 02:45:00,68.55,29.741,29.605999999999998,29.93 +2020-09-07 03:00:00,73.65,31.401,28.124000000000002,29.93 +2020-09-07 03:15:00,75.4,30.138,28.124000000000002,29.93 +2020-09-07 03:30:00,77.07,30.045,28.124000000000002,29.93 +2020-09-07 03:45:00,72.74,31.016,28.124000000000002,29.93 +2020-09-07 04:00:00,78.77,38.909,29.743000000000002,29.93 +2020-09-07 04:15:00,78.27,46.333999999999996,29.743000000000002,29.93 +2020-09-07 04:30:00,81.92,43.856,29.743000000000002,29.93 +2020-09-07 04:45:00,87.93,43.864,29.743000000000002,29.93 +2020-09-07 05:00:00,95.33,59.738,36.191,29.93 +2020-09-07 05:15:00,99.43,70.842,36.191,29.93 +2020-09-07 05:30:00,100.39,64.77199999999999,36.191,29.93 +2020-09-07 05:45:00,103.16,61.369,36.191,29.93 +2020-09-07 06:00:00,107.79,60.716,55.277,29.93 +2020-09-07 06:15:00,107.35,61.775,55.277,29.93 +2020-09-07 06:30:00,111.41,60.729,55.277,29.93 +2020-09-07 06:45:00,110.88,63.191,55.277,29.93 +2020-09-07 07:00:00,111.5,61.396,65.697,29.93 +2020-09-07 07:15:00,111.03,62.872,65.697,29.93 +2020-09-07 07:30:00,108.26,61.34,65.697,29.93 +2020-09-07 07:45:00,107.84,63.056999999999995,65.697,29.93 +2020-09-07 08:00:00,106.73,61.815,57.028,29.93 +2020-09-07 08:15:00,106.0,64.19800000000001,57.028,29.93 +2020-09-07 08:30:00,105.72,63.345,57.028,29.93 +2020-09-07 08:45:00,105.41,65.547,57.028,29.93 +2020-09-07 09:00:00,105.06,61.758,52.633,29.93 +2020-09-07 09:15:00,103.6,60.206,52.633,29.93 +2020-09-07 09:30:00,104.14,61.515,52.633,29.93 +2020-09-07 09:45:00,108.19,61.792,52.633,29.93 +2020-09-07 10:00:00,105.88,59.531000000000006,50.647,29.93 +2020-09-07 10:15:00,107.1,60.333999999999996,50.647,29.93 +2020-09-07 10:30:00,106.5,59.99,50.647,29.93 +2020-09-07 10:45:00,110.53,60.085,50.647,29.93 +2020-09-07 11:00:00,112.98,56.413000000000004,50.245,29.93 +2020-09-07 11:15:00,107.64,57.105,50.245,29.93 +2020-09-07 11:30:00,101.36,58.91,50.245,29.93 +2020-09-07 11:45:00,103.67,59.924,50.245,29.93 +2020-09-07 12:00:00,108.34,56.413000000000004,46.956,29.93 +2020-09-07 12:15:00,106.87,55.748000000000005,46.956,29.93 +2020-09-07 12:30:00,97.46,54.169,46.956,29.93 +2020-09-07 12:45:00,99.84,54.635,46.956,29.93 +2020-09-07 13:00:00,97.74,53.949,47.383,29.93 +2020-09-07 13:15:00,100.52,52.99100000000001,47.383,29.93 +2020-09-07 13:30:00,97.51,51.676,47.383,29.93 +2020-09-07 13:45:00,96.25,51.64,47.383,29.93 +2020-09-07 14:00:00,98.27,52.184,47.1,29.93 +2020-09-07 14:15:00,99.93,51.449,47.1,29.93 +2020-09-07 14:30:00,104.1,49.985,47.1,29.93 +2020-09-07 14:45:00,107.04,51.446999999999996,47.1,29.93 +2020-09-07 15:00:00,106.36,52.048,49.355,29.93 +2020-09-07 15:15:00,104.11,49.193999999999996,49.355,29.93 +2020-09-07 15:30:00,105.03,48.29,49.355,29.93 +2020-09-07 15:45:00,108.61,45.835,49.355,29.93 +2020-09-07 16:00:00,109.53,48.643,52.14,29.93 +2020-09-07 16:15:00,109.89,48.38399999999999,52.14,29.93 +2020-09-07 16:30:00,113.1,49.004,52.14,29.93 +2020-09-07 16:45:00,113.41,46.208,52.14,29.93 +2020-09-07 17:00:00,113.76,47.629,58.705,29.93 +2020-09-07 17:15:00,113.0,48.368,58.705,29.93 +2020-09-07 17:30:00,115.63,48.17,58.705,29.93 +2020-09-07 17:45:00,114.27,47.673,58.705,29.93 +2020-09-07 18:00:00,115.1,50.723,59.153,29.93 +2020-09-07 18:15:00,112.76,48.966,59.153,29.93 +2020-09-07 18:30:00,113.45,47.331,59.153,29.93 +2020-09-07 18:45:00,118.05,51.548,59.153,29.93 +2020-09-07 19:00:00,114.98,55.098,61.483000000000004,29.93 +2020-09-07 19:15:00,112.15,53.696999999999996,61.483000000000004,29.93 +2020-09-07 19:30:00,113.66,52.918,61.483000000000004,29.93 +2020-09-07 19:45:00,113.35,52.733000000000004,61.483000000000004,29.93 +2020-09-07 20:00:00,109.69,50.699,67.55,29.93 +2020-09-07 20:15:00,107.25,51.534,67.55,29.93 +2020-09-07 20:30:00,106.23,50.851000000000006,67.55,29.93 +2020-09-07 20:45:00,103.57,50.35,67.55,29.93 +2020-09-07 21:00:00,96.79,48.262,60.026,29.93 +2020-09-07 21:15:00,92.0,51.003,60.026,29.93 +2020-09-07 21:30:00,87.06,50.658,60.026,29.93 +2020-09-07 21:45:00,91.31,49.968999999999994,60.026,29.93 +2020-09-07 22:00:00,88.7,47.123000000000005,52.736999999999995,29.93 +2020-09-07 22:15:00,87.47,48.068999999999996,52.736999999999995,29.93 +2020-09-07 22:30:00,82.02,40.363,52.736999999999995,29.93 +2020-09-07 22:45:00,84.25,36.84,52.736999999999995,29.93 +2020-09-07 23:00:00,81.67,34.525999999999996,44.408,29.93 +2020-09-07 23:15:00,82.18,32.357,44.408,29.93 +2020-09-07 23:30:00,79.67,32.315,44.408,29.93 +2020-09-07 23:45:00,80.93,31.396,44.408,29.93 +2020-09-08 00:00:00,78.69,31.4,44.438,29.93 +2020-09-08 00:15:00,78.24,31.965,44.438,29.93 +2020-09-08 00:30:00,74.74,31.59,44.438,29.93 +2020-09-08 00:45:00,78.91,31.578000000000003,44.438,29.93 +2020-09-08 01:00:00,78.69,31.218000000000004,41.468999999999994,29.93 +2020-09-08 01:15:00,79.73,30.53,41.468999999999994,29.93 +2020-09-08 01:30:00,74.2,29.549,41.468999999999994,29.93 +2020-09-08 01:45:00,79.22,28.631999999999998,41.468999999999994,29.93 +2020-09-08 02:00:00,78.78,28.364,39.708,29.93 +2020-09-08 02:15:00,79.67,28.124000000000002,39.708,29.93 +2020-09-08 02:30:00,78.51,28.616999999999997,39.708,29.93 +2020-09-08 02:45:00,78.4,29.432,39.708,29.93 +2020-09-08 03:00:00,81.3,30.445999999999998,38.919000000000004,29.93 +2020-09-08 03:15:00,79.9,29.929000000000002,38.919000000000004,29.93 +2020-09-08 03:30:00,77.62,29.871,38.919000000000004,29.93 +2020-09-08 03:45:00,81.23,29.865,38.919000000000004,29.93 +2020-09-08 04:00:00,88.53,36.65,40.092,29.93 +2020-09-08 04:15:00,91.69,44.075,40.092,29.93 +2020-09-08 04:30:00,94.06,41.461999999999996,40.092,29.93 +2020-09-08 04:45:00,99.19,42.047,40.092,29.93 +2020-09-08 05:00:00,107.52,59.983000000000004,43.713,29.93 +2020-09-08 05:15:00,111.02,71.717,43.713,29.93 +2020-09-08 05:30:00,106.44,65.628,43.713,29.93 +2020-09-08 05:45:00,110.66,61.523,43.713,29.93 +2020-09-08 06:00:00,113.9,61.646,56.033,29.93 +2020-09-08 06:15:00,116.71,63.085,56.033,29.93 +2020-09-08 06:30:00,119.58,61.678000000000004,56.033,29.93 +2020-09-08 06:45:00,119.15,63.302,56.033,29.93 +2020-09-08 07:00:00,121.33,61.621,66.003,29.93 +2020-09-08 07:15:00,120.99,62.865,66.003,29.93 +2020-09-08 07:30:00,121.51,61.317,66.003,29.93 +2020-09-08 07:45:00,119.76,62.193999999999996,66.003,29.93 +2020-09-08 08:00:00,113.06,60.905,57.474,29.93 +2020-09-08 08:15:00,112.42,62.732,57.474,29.93 +2020-09-08 08:30:00,112.09,62.012,57.474,29.93 +2020-09-08 08:45:00,114.08,63.347,57.474,29.93 +2020-09-08 09:00:00,111.56,59.766999999999996,51.928000000000004,29.93 +2020-09-08 09:15:00,113.34,58.276,51.928000000000004,29.93 +2020-09-08 09:30:00,112.99,60.253,51.928000000000004,29.93 +2020-09-08 09:45:00,116.22,61.656000000000006,51.928000000000004,29.93 +2020-09-08 10:00:00,117.84,58.111000000000004,49.46,29.93 +2020-09-08 10:15:00,109.46,58.625,49.46,29.93 +2020-09-08 10:30:00,109.25,58.316,49.46,29.93 +2020-09-08 10:45:00,109.93,59.315,49.46,29.93 +2020-09-08 11:00:00,105.93,55.848,48.206,29.93 +2020-09-08 11:15:00,109.06,56.84,48.206,29.93 +2020-09-08 11:30:00,107.95,57.488,48.206,29.93 +2020-09-08 11:45:00,108.6,58.178000000000004,48.206,29.93 +2020-09-08 12:00:00,108.71,54.305,46.285,29.93 +2020-09-08 12:15:00,108.48,53.868,46.285,29.93 +2020-09-08 12:30:00,108.83,53.159,46.285,29.93 +2020-09-08 12:45:00,105.3,54.181999999999995,46.285,29.93 +2020-09-08 13:00:00,104.99,53.108000000000004,46.861999999999995,29.93 +2020-09-08 13:15:00,107.88,53.677,46.861999999999995,29.93 +2020-09-08 13:30:00,110.14,52.479,46.861999999999995,29.93 +2020-09-08 13:45:00,107.86,51.63,46.861999999999995,29.93 +2020-09-08 14:00:00,108.5,52.566,46.488,29.93 +2020-09-08 14:15:00,109.29,51.687,46.488,29.93 +2020-09-08 14:30:00,103.98,50.617,46.488,29.93 +2020-09-08 14:45:00,103.36,51.375,46.488,29.93 +2020-09-08 15:00:00,104.44,51.73,48.442,29.93 +2020-09-08 15:15:00,102.88,49.745,48.442,29.93 +2020-09-08 15:30:00,100.95,48.707,48.442,29.93 +2020-09-08 15:45:00,101.93,46.519,48.442,29.93 +2020-09-08 16:00:00,106.74,48.718999999999994,50.397,29.93 +2020-09-08 16:15:00,109.62,48.608000000000004,50.397,29.93 +2020-09-08 16:30:00,111.41,48.976000000000006,50.397,29.93 +2020-09-08 16:45:00,111.89,46.9,50.397,29.93 +2020-09-08 17:00:00,114.63,48.536,56.668,29.93 +2020-09-08 17:15:00,112.41,49.675,56.668,29.93 +2020-09-08 17:30:00,113.49,49.18,56.668,29.93 +2020-09-08 17:45:00,115.51,48.411,56.668,29.93 +2020-09-08 18:00:00,115.96,50.586999999999996,57.957,29.93 +2020-09-08 18:15:00,112.95,50.101000000000006,57.957,29.93 +2020-09-08 18:30:00,113.99,48.21,57.957,29.93 +2020-09-08 18:45:00,116.35,52.343,57.957,29.93 +2020-09-08 19:00:00,120.65,54.869,57.056000000000004,29.93 +2020-09-08 19:15:00,118.5,53.596000000000004,57.056000000000004,29.93 +2020-09-08 19:30:00,120.53,52.551,57.056000000000004,29.93 +2020-09-08 19:45:00,111.59,52.668,57.056000000000004,29.93 +2020-09-08 20:00:00,103.31,51.019,64.156,29.93 +2020-09-08 20:15:00,102.93,50.501000000000005,64.156,29.93 +2020-09-08 20:30:00,100.46,49.946999999999996,64.156,29.93 +2020-09-08 20:45:00,102.55,49.711000000000006,64.156,29.93 +2020-09-08 21:00:00,101.45,48.318000000000005,56.507,29.93 +2020-09-08 21:15:00,102.7,49.847,56.507,29.93 +2020-09-08 21:30:00,97.23,49.58,56.507,29.93 +2020-09-08 21:45:00,96.21,49.055,56.507,29.93 +2020-09-08 22:00:00,91.16,46.526,50.728,29.93 +2020-09-08 22:15:00,91.57,47.123000000000005,50.728,29.93 +2020-09-08 22:30:00,84.85,39.665,50.728,29.93 +2020-09-08 22:45:00,86.91,36.171,50.728,29.93 +2020-09-08 23:00:00,84.69,33.22,43.556999999999995,29.93 +2020-09-08 23:15:00,85.78,32.317,43.556999999999995,29.93 +2020-09-08 23:30:00,82.52,32.239000000000004,43.556999999999995,29.93 +2020-09-08 23:45:00,85.59,31.436999999999998,43.556999999999995,29.93 +2020-09-09 00:00:00,82.31,31.622,41.151,29.93 +2020-09-09 00:15:00,83.13,32.187,41.151,29.93 +2020-09-09 00:30:00,79.51,31.819000000000003,41.151,29.93 +2020-09-09 00:45:00,77.56,31.813000000000002,41.151,29.93 +2020-09-09 01:00:00,79.6,31.435,37.763000000000005,29.93 +2020-09-09 01:15:00,81.91,30.766,37.763000000000005,29.93 +2020-09-09 01:30:00,82.15,29.803,37.763000000000005,29.93 +2020-09-09 01:45:00,79.99,28.886999999999997,37.763000000000005,29.93 +2020-09-09 02:00:00,74.52,28.623,35.615,29.93 +2020-09-09 02:15:00,74.8,28.405,35.615,29.93 +2020-09-09 02:30:00,81.69,28.874000000000002,35.615,29.93 +2020-09-09 02:45:00,82.63,29.683000000000003,35.615,29.93 +2020-09-09 03:00:00,83.44,30.685,35.153,29.93 +2020-09-09 03:15:00,78.79,30.186,35.153,29.93 +2020-09-09 03:30:00,84.28,30.133000000000003,35.153,29.93 +2020-09-09 03:45:00,86.29,30.113000000000003,35.153,29.93 +2020-09-09 04:00:00,89.48,36.935,36.203,29.93 +2020-09-09 04:15:00,89.42,44.39,36.203,29.93 +2020-09-09 04:30:00,94.57,41.785,36.203,29.93 +2020-09-09 04:45:00,102.54,42.376000000000005,36.203,29.93 +2020-09-09 05:00:00,108.61,60.409,39.922,29.93 +2020-09-09 05:15:00,107.95,72.247,39.922,29.93 +2020-09-09 05:30:00,111.12,66.142,39.922,29.93 +2020-09-09 05:45:00,111.45,61.985,39.922,29.93 +2020-09-09 06:00:00,113.78,62.07,56.443999999999996,29.93 +2020-09-09 06:15:00,116.37,63.535,56.443999999999996,29.93 +2020-09-09 06:30:00,119.03,62.123000000000005,56.443999999999996,29.93 +2020-09-09 06:45:00,118.81,63.743,56.443999999999996,29.93 +2020-09-09 07:00:00,118.73,62.062,68.683,29.93 +2020-09-09 07:15:00,118.39,63.321000000000005,68.683,29.93 +2020-09-09 07:30:00,116.09,61.806000000000004,68.683,29.93 +2020-09-09 07:45:00,114.53,62.681999999999995,68.683,29.93 +2020-09-09 08:00:00,113.91,61.396,59.003,29.93 +2020-09-09 08:15:00,112.48,63.184,59.003,29.93 +2020-09-09 08:30:00,113.23,62.471000000000004,59.003,29.93 +2020-09-09 08:45:00,114.44,63.788000000000004,59.003,29.93 +2020-09-09 09:00:00,111.57,60.216,56.21,29.93 +2020-09-09 09:15:00,109.34,58.714,56.21,29.93 +2020-09-09 09:30:00,109.66,60.67100000000001,56.21,29.93 +2020-09-09 09:45:00,109.6,62.044,56.21,29.93 +2020-09-09 10:00:00,108.97,58.495,52.358999999999995,29.93 +2020-09-09 10:15:00,108.59,58.974,52.358999999999995,29.93 +2020-09-09 10:30:00,107.81,58.653,52.358999999999995,29.93 +2020-09-09 10:45:00,107.09,59.638999999999996,52.358999999999995,29.93 +2020-09-09 11:00:00,105.96,56.187,51.161,29.93 +2020-09-09 11:15:00,104.7,57.163999999999994,51.161,29.93 +2020-09-09 11:30:00,105.82,57.81,51.161,29.93 +2020-09-09 11:45:00,102.87,58.482,51.161,29.93 +2020-09-09 12:00:00,103.56,54.586000000000006,49.119,29.93 +2020-09-09 12:15:00,100.18,54.136,49.119,29.93 +2020-09-09 12:30:00,98.72,53.456,49.119,29.93 +2020-09-09 12:45:00,100.89,54.467,49.119,29.93 +2020-09-09 13:00:00,101.22,53.369,49.187,29.93 +2020-09-09 13:15:00,101.0,53.93,49.187,29.93 +2020-09-09 13:30:00,103.88,52.728,49.187,29.93 +2020-09-09 13:45:00,102.94,51.888000000000005,49.187,29.93 +2020-09-09 14:00:00,103.48,52.784,49.787,29.93 +2020-09-09 14:15:00,104.49,51.917,49.787,29.93 +2020-09-09 14:30:00,101.7,50.873999999999995,49.787,29.93 +2020-09-09 14:45:00,101.43,51.63,49.787,29.93 +2020-09-09 15:00:00,101.46,51.931000000000004,51.458999999999996,29.93 +2020-09-09 15:15:00,101.99,49.961000000000006,51.458999999999996,29.93 +2020-09-09 15:30:00,102.58,48.946999999999996,51.458999999999996,29.93 +2020-09-09 15:45:00,104.51,46.772,51.458999999999996,29.93 +2020-09-09 16:00:00,104.61,48.935,53.663000000000004,29.93 +2020-09-09 16:15:00,109.4,48.835,53.663000000000004,29.93 +2020-09-09 16:30:00,108.58,49.201,53.663000000000004,29.93 +2020-09-09 16:45:00,109.96,47.181000000000004,53.663000000000004,29.93 +2020-09-09 17:00:00,112.76,48.776,58.183,29.93 +2020-09-09 17:15:00,112.11,49.945,58.183,29.93 +2020-09-09 17:30:00,115.42,49.458999999999996,58.183,29.93 +2020-09-09 17:45:00,113.9,48.728,58.183,29.93 +2020-09-09 18:00:00,112.53,50.887,60.141000000000005,29.93 +2020-09-09 18:15:00,111.03,50.411,60.141000000000005,29.93 +2020-09-09 18:30:00,112.22,48.534,60.141000000000005,29.93 +2020-09-09 18:45:00,115.87,52.662,60.141000000000005,29.93 +2020-09-09 19:00:00,114.45,55.199,60.582,29.93 +2020-09-09 19:15:00,114.54,53.925,60.582,29.93 +2020-09-09 19:30:00,114.12,52.881,60.582,29.93 +2020-09-09 19:45:00,114.17,52.997,60.582,29.93 +2020-09-09 20:00:00,107.24,51.36600000000001,66.61,29.93 +2020-09-09 20:15:00,100.92,50.848,66.61,29.93 +2020-09-09 20:30:00,105.27,50.271,66.61,29.93 +2020-09-09 20:45:00,105.34,49.992,66.61,29.93 +2020-09-09 21:00:00,100.98,48.603,57.658,29.93 +2020-09-09 21:15:00,97.14,50.12,57.658,29.93 +2020-09-09 21:30:00,95.75,49.858999999999995,57.658,29.93 +2020-09-09 21:45:00,95.25,49.29600000000001,57.658,29.93 +2020-09-09 22:00:00,92.5,46.744,51.81,29.93 +2020-09-09 22:15:00,86.93,47.318000000000005,51.81,29.93 +2020-09-09 22:30:00,87.86,39.836,51.81,29.93 +2020-09-09 22:45:00,86.09,36.346,51.81,29.93 +2020-09-09 23:00:00,84.21,33.443000000000005,42.93600000000001,29.93 +2020-09-09 23:15:00,82.99,32.514,42.93600000000001,29.93 +2020-09-09 23:30:00,83.34,32.443000000000005,42.93600000000001,29.93 +2020-09-09 23:45:00,84.51,31.645,42.93600000000001,29.93 +2020-09-10 00:00:00,80.33,31.846,39.211,29.93 +2020-09-10 00:15:00,77.06,32.409,39.211,29.93 +2020-09-10 00:30:00,80.13,32.05,39.211,29.93 +2020-09-10 00:45:00,81.28,32.049,39.211,29.93 +2020-09-10 01:00:00,81.02,31.653000000000002,37.607,29.93 +2020-09-10 01:15:00,75.14,31.005,37.607,29.93 +2020-09-10 01:30:00,73.91,30.059,37.607,29.93 +2020-09-10 01:45:00,77.05,29.145,37.607,29.93 +2020-09-10 02:00:00,80.23,28.884,36.44,29.93 +2020-09-10 02:15:00,81.87,28.69,36.44,29.93 +2020-09-10 02:30:00,73.49,29.133000000000003,36.44,29.93 +2020-09-10 02:45:00,74.47,29.936999999999998,36.44,29.93 +2020-09-10 03:00:00,77.1,30.927,36.116,29.93 +2020-09-10 03:15:00,80.63,30.445,36.116,29.93 +2020-09-10 03:30:00,83.84,30.397,36.116,29.93 +2020-09-10 03:45:00,83.29,30.363000000000003,36.116,29.93 +2020-09-10 04:00:00,83.53,37.223,37.398,29.93 +2020-09-10 04:15:00,89.58,44.708,37.398,29.93 +2020-09-10 04:30:00,94.75,42.11,37.398,29.93 +2020-09-10 04:45:00,100.81,42.708999999999996,37.398,29.93 +2020-09-10 05:00:00,101.58,60.841,41.776,29.93 +2020-09-10 05:15:00,104.3,72.78699999999999,41.776,29.93 +2020-09-10 05:30:00,107.36,66.663,41.776,29.93 +2020-09-10 05:45:00,107.9,62.453,41.776,29.93 +2020-09-10 06:00:00,114.18,62.5,55.61,29.93 +2020-09-10 06:15:00,116.19,63.99,55.61,29.93 +2020-09-10 06:30:00,117.06,62.575,55.61,29.93 +2020-09-10 06:45:00,118.62,64.19,55.61,29.93 +2020-09-10 07:00:00,119.66,62.50899999999999,67.13600000000001,29.93 +2020-09-10 07:15:00,118.33,63.782,67.13600000000001,29.93 +2020-09-10 07:30:00,118.98,62.299,67.13600000000001,29.93 +2020-09-10 07:45:00,118.75,63.175,67.13600000000001,29.93 +2020-09-10 08:00:00,117.68,61.891999999999996,57.55,29.93 +2020-09-10 08:15:00,118.15,63.641000000000005,57.55,29.93 +2020-09-10 08:30:00,118.58,62.933,57.55,29.93 +2020-09-10 08:45:00,119.2,64.232,57.55,29.93 +2020-09-10 09:00:00,121.14,60.669,52.931999999999995,29.93 +2020-09-10 09:15:00,118.91,59.157,52.931999999999995,29.93 +2020-09-10 09:30:00,120.24,61.093,52.931999999999995,29.93 +2020-09-10 09:45:00,124.85,62.435,52.931999999999995,29.93 +2020-09-10 10:00:00,125.58,58.882,50.36600000000001,29.93 +2020-09-10 10:15:00,124.15,59.327,50.36600000000001,29.93 +2020-09-10 10:30:00,125.44,58.995,50.36600000000001,29.93 +2020-09-10 10:45:00,121.44,59.967,50.36600000000001,29.93 +2020-09-10 11:00:00,119.2,56.528,47.893,29.93 +2020-09-10 11:15:00,121.84,57.49100000000001,47.893,29.93 +2020-09-10 11:30:00,126.86,58.136,47.893,29.93 +2020-09-10 11:45:00,126.23,58.79,47.893,29.93 +2020-09-10 12:00:00,125.52,54.871,45.271,29.93 +2020-09-10 12:15:00,125.28,54.406000000000006,45.271,29.93 +2020-09-10 12:30:00,120.23,53.754,45.271,29.93 +2020-09-10 12:45:00,121.25,54.754,45.271,29.93 +2020-09-10 13:00:00,119.37,53.633,44.351000000000006,29.93 +2020-09-10 13:15:00,120.66,54.18600000000001,44.351000000000006,29.93 +2020-09-10 13:30:00,119.3,52.98,44.351000000000006,29.93 +2020-09-10 13:45:00,117.09,52.148,44.351000000000006,29.93 +2020-09-10 14:00:00,117.58,53.005,44.99,29.93 +2020-09-10 14:15:00,117.93,52.148999999999994,44.99,29.93 +2020-09-10 14:30:00,115.71,51.13399999999999,44.99,29.93 +2020-09-10 14:45:00,114.86,51.888000000000005,44.99,29.93 +2020-09-10 15:00:00,113.58,52.13399999999999,46.869,29.93 +2020-09-10 15:15:00,112.65,50.178999999999995,46.869,29.93 +2020-09-10 15:30:00,113.09,49.191,46.869,29.93 +2020-09-10 15:45:00,113.8,47.027,46.869,29.93 +2020-09-10 16:00:00,114.36,49.151,48.902,29.93 +2020-09-10 16:15:00,116.87,49.066,48.902,29.93 +2020-09-10 16:30:00,117.85,49.43,48.902,29.93 +2020-09-10 16:45:00,119.64,47.464,48.902,29.93 +2020-09-10 17:00:00,120.81,49.018,53.244,29.93 +2020-09-10 17:15:00,118.06,50.216,53.244,29.93 +2020-09-10 17:30:00,118.85,49.74100000000001,53.244,29.93 +2020-09-10 17:45:00,118.18,49.047,53.244,29.93 +2020-09-10 18:00:00,119.17,51.18899999999999,54.343999999999994,29.93 +2020-09-10 18:15:00,118.52,50.725,54.343999999999994,29.93 +2020-09-10 18:30:00,117.98,48.86,54.343999999999994,29.93 +2020-09-10 18:45:00,117.71,52.985,54.343999999999994,29.93 +2020-09-10 19:00:00,118.08,55.531000000000006,54.332,29.93 +2020-09-10 19:15:00,117.58,54.257,54.332,29.93 +2020-09-10 19:30:00,115.86,53.214,54.332,29.93 +2020-09-10 19:45:00,111.56,53.33,54.332,29.93 +2020-09-10 20:00:00,103.85,51.715,58.06,29.93 +2020-09-10 20:15:00,104.58,51.196999999999996,58.06,29.93 +2020-09-10 20:30:00,99.86,50.598,58.06,29.93 +2020-09-10 20:45:00,103.55,50.276,58.06,29.93 +2020-09-10 21:00:00,99.44,48.89,52.411,29.93 +2020-09-10 21:15:00,100.62,50.397,52.411,29.93 +2020-09-10 21:30:00,92.72,50.141000000000005,52.411,29.93 +2020-09-10 21:45:00,88.73,49.54,52.411,29.93 +2020-09-10 22:00:00,87.31,46.963,47.148999999999994,29.93 +2020-09-10 22:15:00,88.58,47.515,47.148999999999994,29.93 +2020-09-10 22:30:00,84.17,40.009,47.148999999999994,29.93 +2020-09-10 22:45:00,83.22,36.524,47.148999999999994,29.93 +2020-09-10 23:00:00,80.89,33.669000000000004,40.814,29.93 +2020-09-10 23:15:00,82.71,32.713,40.814,29.93 +2020-09-10 23:30:00,78.26,32.649,40.814,29.93 +2020-09-10 23:45:00,82.96,31.855999999999998,40.814,29.93 +2020-09-11 00:00:00,75.6,30.344,39.153,29.93 +2020-09-11 00:15:00,80.38,31.125,39.153,29.93 +2020-09-11 00:30:00,80.19,30.996,39.153,29.93 +2020-09-11 00:45:00,79.37,31.391,39.153,29.93 +2020-09-11 01:00:00,72.6,30.596999999999998,37.228,29.93 +2020-09-11 01:15:00,79.38,29.555,37.228,29.93 +2020-09-11 01:30:00,77.58,29.232,37.228,29.93 +2020-09-11 01:45:00,80.0,28.109,37.228,29.93 +2020-09-11 02:00:00,76.16,28.691,35.851,29.93 +2020-09-11 02:15:00,80.66,28.458000000000002,35.851,29.93 +2020-09-11 02:30:00,81.2,29.677,35.851,29.93 +2020-09-11 02:45:00,80.38,29.875999999999998,35.851,29.93 +2020-09-11 03:00:00,76.4,31.41,36.54,29.93 +2020-09-11 03:15:00,80.73,29.999000000000002,36.54,29.93 +2020-09-11 03:30:00,84.41,29.741999999999997,36.54,29.93 +2020-09-11 03:45:00,81.82,30.514,36.54,29.93 +2020-09-11 04:00:00,85.33,37.564,37.578,29.93 +2020-09-11 04:15:00,91.54,43.666000000000004,37.578,29.93 +2020-09-11 04:30:00,94.67,41.95,37.578,29.93 +2020-09-11 04:45:00,96.14,41.833,37.578,29.93 +2020-09-11 05:00:00,101.04,59.476000000000006,40.387,29.93 +2020-09-11 05:15:00,102.73,72.569,40.387,29.93 +2020-09-11 05:30:00,108.87,66.734,40.387,29.93 +2020-09-11 05:45:00,111.53,62.067,40.387,29.93 +2020-09-11 06:00:00,116.13,62.336000000000006,54.668,29.93 +2020-09-11 06:15:00,118.68,63.81100000000001,54.668,29.93 +2020-09-11 06:30:00,120.17,62.233999999999995,54.668,29.93 +2020-09-11 06:45:00,123.12,63.937,54.668,29.93 +2020-09-11 07:00:00,125.11,62.724,63.971000000000004,29.93 +2020-09-11 07:15:00,125.24,64.94800000000001,63.971000000000004,29.93 +2020-09-11 07:30:00,125.46,61.755,63.971000000000004,29.93 +2020-09-11 07:45:00,125.76,62.323,63.971000000000004,29.93 +2020-09-11 08:00:00,124.69,61.582,56.042,29.93 +2020-09-11 08:15:00,125.79,63.826,56.042,29.93 +2020-09-11 08:30:00,127.99,63.174,56.042,29.93 +2020-09-11 08:45:00,128.03,64.111,56.042,29.93 +2020-09-11 09:00:00,127.4,58.596000000000004,52.832,29.93 +2020-09-11 09:15:00,127.15,58.831,52.832,29.93 +2020-09-11 09:30:00,126.24,60.083999999999996,52.832,29.93 +2020-09-11 09:45:00,126.73,61.718,52.832,29.93 +2020-09-11 10:00:00,123.95,57.843999999999994,50.044,29.93 +2020-09-11 10:15:00,126.91,58.161,50.044,29.93 +2020-09-11 10:30:00,126.92,58.288000000000004,50.044,29.93 +2020-09-11 10:45:00,126.94,59.099,50.044,29.93 +2020-09-11 11:00:00,124.25,55.891999999999996,49.06100000000001,29.93 +2020-09-11 11:15:00,122.43,55.766000000000005,49.06100000000001,29.93 +2020-09-11 11:30:00,123.76,56.348,49.06100000000001,29.93 +2020-09-11 11:45:00,121.22,56.158,49.06100000000001,29.93 +2020-09-11 12:00:00,117.99,52.754,45.595,29.93 +2020-09-11 12:15:00,118.71,51.409,45.595,29.93 +2020-09-11 12:30:00,115.33,50.912,45.595,29.93 +2020-09-11 12:45:00,114.1,51.29600000000001,45.595,29.93 +2020-09-11 13:00:00,113.23,50.799,43.218,29.93 +2020-09-11 13:15:00,112.25,51.623000000000005,43.218,29.93 +2020-09-11 13:30:00,110.79,51.105,43.218,29.93 +2020-09-11 13:45:00,111.31,50.538000000000004,43.218,29.93 +2020-09-11 14:00:00,110.24,50.486999999999995,41.926,29.93 +2020-09-11 14:15:00,112.95,49.996,41.926,29.93 +2020-09-11 14:30:00,108.86,50.391999999999996,41.926,29.93 +2020-09-11 14:45:00,112.25,50.591,41.926,29.93 +2020-09-11 15:00:00,111.4,50.64,43.79,29.93 +2020-09-11 15:15:00,111.65,48.431000000000004,43.79,29.93 +2020-09-11 15:30:00,110.79,46.753,43.79,29.93 +2020-09-11 15:45:00,107.67,45.287,43.79,29.93 +2020-09-11 16:00:00,107.07,46.474,45.895,29.93 +2020-09-11 16:15:00,108.07,46.873999999999995,45.895,29.93 +2020-09-11 16:30:00,106.95,47.09,45.895,29.93 +2020-09-11 16:45:00,105.52,44.477,45.895,29.93 +2020-09-11 17:00:00,109.06,47.535,51.36,29.93 +2020-09-11 17:15:00,109.74,48.553000000000004,51.36,29.93 +2020-09-11 17:30:00,110.3,48.187,51.36,29.93 +2020-09-11 17:45:00,111.89,47.353,51.36,29.93 +2020-09-11 18:00:00,111.62,49.632,52.985,29.93 +2020-09-11 18:15:00,109.54,48.294,52.985,29.93 +2020-09-11 18:30:00,111.46,46.401,52.985,29.93 +2020-09-11 18:45:00,114.19,50.891999999999996,52.985,29.93 +2020-09-11 19:00:00,112.04,54.356,52.602,29.93 +2020-09-11 19:15:00,108.7,53.778,52.602,29.93 +2020-09-11 19:30:00,105.58,52.738,52.602,29.93 +2020-09-11 19:45:00,103.63,51.891999999999996,52.602,29.93 +2020-09-11 20:00:00,98.96,50.158,58.063,29.93 +2020-09-11 20:15:00,100.7,50.345,58.063,29.93 +2020-09-11 20:30:00,97.79,49.286,58.063,29.93 +2020-09-11 20:45:00,98.82,48.273999999999994,58.063,29.93 +2020-09-11 21:00:00,92.16,48.119,50.135,29.93 +2020-09-11 21:15:00,87.15,51.151,50.135,29.93 +2020-09-11 21:30:00,82.01,50.773999999999994,50.135,29.93 +2020-09-11 21:45:00,83.79,50.385,50.135,29.93 +2020-09-11 22:00:00,84.45,47.785,45.165,29.93 +2020-09-11 22:15:00,82.84,48.075,45.165,29.93 +2020-09-11 22:30:00,74.05,45.567,45.165,29.93 +2020-09-11 22:45:00,75.88,43.292,45.165,29.93 +2020-09-11 23:00:00,67.88,41.926,39.121,29.93 +2020-09-11 23:15:00,66.06,39.306999999999995,39.121,29.93 +2020-09-11 23:30:00,65.9,37.449,39.121,29.93 +2020-09-11 23:45:00,73.95,36.47,39.121,29.93 +2020-09-12 00:00:00,72.49,31.303,38.49,29.816 +2020-09-12 00:15:00,70.54,30.943,38.49,29.816 +2020-09-12 00:30:00,63.74,30.576,38.49,29.816 +2020-09-12 00:45:00,64.17,30.421999999999997,38.49,29.816 +2020-09-12 01:00:00,63.03,29.933000000000003,34.5,29.816 +2020-09-12 01:15:00,61.92,29.307,34.5,29.816 +2020-09-12 01:30:00,61.64,28.223000000000003,34.5,29.816 +2020-09-12 01:45:00,61.96,28.176,34.5,29.816 +2020-09-12 02:00:00,62.24,27.976,32.236,29.816 +2020-09-12 02:15:00,67.5,27.022,32.236,29.816 +2020-09-12 02:30:00,67.15,27.374000000000002,32.236,29.816 +2020-09-12 02:45:00,66.48,28.275,32.236,29.816 +2020-09-12 03:00:00,65.68,28.665,32.067,29.816 +2020-09-12 03:15:00,61.69,26.500999999999998,32.067,29.816 +2020-09-12 03:30:00,61.21,26.336,32.067,29.816 +2020-09-12 03:45:00,61.06,28.415,32.067,29.816 +2020-09-12 04:00:00,62.25,33.311,33.071,29.816 +2020-09-12 04:15:00,61.64,38.334,33.071,29.816 +2020-09-12 04:30:00,61.43,34.878,33.071,29.816 +2020-09-12 04:45:00,64.14,34.945,33.071,29.816 +2020-09-12 05:00:00,66.46,43.589,33.014,29.816 +2020-09-12 05:15:00,68.23,44.825,33.014,29.816 +2020-09-12 05:30:00,66.76,40.367,33.014,29.816 +2020-09-12 05:45:00,68.02,40.258,33.014,29.816 +2020-09-12 06:00:00,70.3,52.581,34.628,29.816 +2020-09-12 06:15:00,72.97,62.941,34.628,29.816 +2020-09-12 06:30:00,74.15,58.201,34.628,29.816 +2020-09-12 06:45:00,76.25,55.983999999999995,34.628,29.816 +2020-09-12 07:00:00,77.74,52.931999999999995,38.871,29.816 +2020-09-12 07:15:00,80.76,53.872,38.871,29.816 +2020-09-12 07:30:00,82.04,52.38,38.871,29.816 +2020-09-12 07:45:00,79.99,54.251999999999995,38.871,29.816 +2020-09-12 08:00:00,80.65,54.566,43.293,29.816 +2020-09-12 08:15:00,84.33,57.047,43.293,29.816 +2020-09-12 08:30:00,80.92,56.593,43.293,29.816 +2020-09-12 08:45:00,82.18,58.745,43.293,29.816 +2020-09-12 09:00:00,73.56,56.155,44.559,29.816 +2020-09-12 09:15:00,72.42,56.9,44.559,29.816 +2020-09-12 09:30:00,72.7,58.683,44.559,29.816 +2020-09-12 09:45:00,77.44,59.915,44.559,29.816 +2020-09-12 10:00:00,76.92,56.563,42.091,29.816 +2020-09-12 10:15:00,82.93,57.177,42.091,29.816 +2020-09-12 10:30:00,75.13,57.018,42.091,29.816 +2020-09-12 10:45:00,78.26,57.68899999999999,42.091,29.816 +2020-09-12 11:00:00,75.56,54.4,38.505,29.816 +2020-09-12 11:15:00,72.28,54.917,38.505,29.816 +2020-09-12 11:30:00,70.41,55.589,38.505,29.816 +2020-09-12 11:45:00,69.55,55.831,38.505,29.816 +2020-09-12 12:00:00,69.43,52.578,35.388000000000005,29.816 +2020-09-12 12:15:00,67.6,52.043,35.388000000000005,29.816 +2020-09-12 12:30:00,66.35,51.507,35.388000000000005,29.816 +2020-09-12 12:45:00,66.07,52.397,35.388000000000005,29.816 +2020-09-12 13:00:00,63.61,51.104,31.355999999999998,29.816 +2020-09-12 13:15:00,68.82,51.163999999999994,31.355999999999998,29.816 +2020-09-12 13:30:00,66.78,50.739,31.355999999999998,29.816 +2020-09-12 13:45:00,66.36,49.083999999999996,31.355999999999998,29.816 +2020-09-12 14:00:00,68.36,49.22,30.522,29.816 +2020-09-12 14:15:00,69.67,47.583999999999996,30.522,29.816 +2020-09-12 14:30:00,66.25,47.385,30.522,29.816 +2020-09-12 14:45:00,67.5,48.027,30.522,29.816 +2020-09-12 15:00:00,68.44,48.504,34.36,29.816 +2020-09-12 15:15:00,69.12,47.034,34.36,29.816 +2020-09-12 15:30:00,69.44,45.773,34.36,29.816 +2020-09-12 15:45:00,70.61,43.525,34.36,29.816 +2020-09-12 16:00:00,73.84,46.43,39.507,29.816 +2020-09-12 16:15:00,75.23,46.185,39.507,29.816 +2020-09-12 16:30:00,75.45,46.589,39.507,29.816 +2020-09-12 16:45:00,77.86,44.126999999999995,39.507,29.816 +2020-09-12 17:00:00,81.34,46.038000000000004,47.151,29.816 +2020-09-12 17:15:00,84.36,45.433,47.151,29.816 +2020-09-12 17:30:00,82.72,44.951,47.151,29.816 +2020-09-12 17:45:00,83.58,44.531000000000006,47.151,29.816 +2020-09-12 18:00:00,85.61,47.988,50.303999999999995,29.816 +2020-09-12 18:15:00,84.62,48.356,50.303999999999995,29.816 +2020-09-12 18:30:00,89.46,47.836999999999996,50.303999999999995,29.816 +2020-09-12 18:45:00,89.86,48.85,50.303999999999995,29.816 +2020-09-12 19:00:00,88.15,51.066,50.622,29.816 +2020-09-12 19:15:00,83.92,49.538000000000004,50.622,29.816 +2020-09-12 19:30:00,83.24,49.269,50.622,29.816 +2020-09-12 19:45:00,82.65,49.957,50.622,29.816 +2020-09-12 20:00:00,80.31,49.22,45.391000000000005,29.816 +2020-09-12 20:15:00,77.48,49.12,45.391000000000005,29.816 +2020-09-12 20:30:00,76.12,47.223,45.391000000000005,29.816 +2020-09-12 20:45:00,75.87,47.74100000000001,45.391000000000005,29.816 +2020-09-12 21:00:00,71.68,46.589,39.98,29.816 +2020-09-12 21:15:00,70.68,49.358999999999995,39.98,29.816 +2020-09-12 21:30:00,68.86,49.309,39.98,29.816 +2020-09-12 21:45:00,68.45,48.352,39.98,29.816 +2020-09-12 22:00:00,64.74,45.873999999999995,37.53,29.816 +2020-09-12 22:15:00,65.48,46.672,37.53,29.816 +2020-09-12 22:30:00,62.36,44.59,37.53,29.816 +2020-09-12 22:45:00,62.8,42.88,37.53,29.816 +2020-09-12 23:00:00,57.88,41.266999999999996,30.97,29.816 +2020-09-12 23:15:00,56.65,38.785,30.97,29.816 +2020-09-12 23:30:00,58.13,38.875,30.97,29.816 +2020-09-12 23:45:00,57.91,37.913000000000004,30.97,29.816 +2020-09-13 00:00:00,53.28,32.799,27.24,29.816 +2020-09-13 00:15:00,53.54,31.384,27.24,29.816 +2020-09-13 00:30:00,53.48,30.849,27.24,29.816 +2020-09-13 00:45:00,53.88,30.689,27.24,29.816 +2020-09-13 01:00:00,51.24,30.398000000000003,25.662,29.816 +2020-09-13 01:15:00,52.62,29.816,25.662,29.816 +2020-09-13 01:30:00,52.7,28.694000000000003,25.662,29.816 +2020-09-13 01:45:00,52.44,28.274,25.662,29.816 +2020-09-13 02:00:00,53.25,28.054000000000002,25.67,29.816 +2020-09-13 02:15:00,52.44,27.636999999999997,25.67,29.816 +2020-09-13 02:30:00,51.82,28.238000000000003,25.67,29.816 +2020-09-13 02:45:00,51.81,28.935,25.67,29.816 +2020-09-13 03:00:00,50.96,29.924,24.258000000000003,29.816 +2020-09-13 03:15:00,52.6,27.941,24.258000000000003,29.816 +2020-09-13 03:30:00,52.63,27.311,24.258000000000003,29.816 +2020-09-13 03:45:00,52.9,28.666999999999998,24.258000000000003,29.816 +2020-09-13 04:00:00,53.7,33.546,25.051,29.816 +2020-09-13 04:15:00,54.59,38.092,25.051,29.816 +2020-09-13 04:30:00,54.4,35.829,25.051,29.816 +2020-09-13 04:45:00,55.85,35.528,25.051,29.816 +2020-09-13 05:00:00,57.42,44.011,25.145,29.816 +2020-09-13 05:15:00,58.05,44.397,25.145,29.816 +2020-09-13 05:30:00,55.99,39.544000000000004,25.145,29.816 +2020-09-13 05:45:00,56.86,39.168,25.145,29.816 +2020-09-13 06:00:00,57.81,49.345,26.371,29.816 +2020-09-13 06:15:00,58.65,60.265,26.371,29.816 +2020-09-13 06:30:00,58.81,54.775,26.371,29.816 +2020-09-13 06:45:00,60.74,51.551,26.371,29.816 +2020-09-13 07:00:00,61.74,49.041000000000004,28.756999999999998,29.816 +2020-09-13 07:15:00,61.61,48.45,28.756999999999998,29.816 +2020-09-13 07:30:00,61.01,47.961999999999996,28.756999999999998,29.816 +2020-09-13 07:45:00,61.51,49.708999999999996,28.756999999999998,29.816 +2020-09-13 08:00:00,61.6,50.91,32.82,29.816 +2020-09-13 08:15:00,63.67,54.369,32.82,29.816 +2020-09-13 08:30:00,61.43,54.89,32.82,29.816 +2020-09-13 08:45:00,60.33,57.203,32.82,29.816 +2020-09-13 09:00:00,58.83,54.472,35.534,29.816 +2020-09-13 09:15:00,60.35,54.845,35.534,29.816 +2020-09-13 09:30:00,60.42,56.964,35.534,29.816 +2020-09-13 09:45:00,60.83,59.023999999999994,35.534,29.816 +2020-09-13 10:00:00,61.52,56.451,35.925,29.816 +2020-09-13 10:15:00,63.03,57.215,35.925,29.816 +2020-09-13 10:30:00,64.13,57.315,35.925,29.816 +2020-09-13 10:45:00,64.9,58.61,35.925,29.816 +2020-09-13 11:00:00,61.16,55.151,37.056,29.816 +2020-09-13 11:15:00,60.3,55.281000000000006,37.056,29.816 +2020-09-13 11:30:00,56.6,56.294,37.056,29.816 +2020-09-13 11:45:00,58.49,56.818000000000005,37.056,29.816 +2020-09-13 12:00:00,52.37,54.376999999999995,33.124,29.816 +2020-09-13 12:15:00,54.75,53.535,33.124,29.816 +2020-09-13 12:30:00,50.06,53.025,33.124,29.816 +2020-09-13 12:45:00,50.17,53.224,33.124,29.816 +2020-09-13 13:00:00,49.32,51.556999999999995,29.874000000000002,29.816 +2020-09-13 13:15:00,49.67,51.497,29.874000000000002,29.816 +2020-09-13 13:30:00,48.44,50.077,29.874000000000002,29.816 +2020-09-13 13:45:00,49.63,49.31399999999999,29.874000000000002,29.816 +2020-09-13 14:00:00,49.39,50.481,27.302,29.816 +2020-09-13 14:15:00,50.02,49.385,27.302,29.816 +2020-09-13 14:30:00,50.96,48.302,27.302,29.816 +2020-09-13 14:45:00,52.31,47.96,27.302,29.816 +2020-09-13 15:00:00,52.47,48.31100000000001,27.642,29.816 +2020-09-13 15:15:00,56.04,46.251000000000005,27.642,29.816 +2020-09-13 15:30:00,55.91,44.931000000000004,27.642,29.816 +2020-09-13 15:45:00,58.62,43.033,27.642,29.816 +2020-09-13 16:00:00,63.63,44.687,31.945999999999998,29.816 +2020-09-13 16:15:00,63.63,44.543,31.945999999999998,29.816 +2020-09-13 16:30:00,67.25,45.853,31.945999999999998,29.816 +2020-09-13 16:45:00,69.54,43.519,31.945999999999998,29.816 +2020-09-13 17:00:00,73.29,45.705,40.387,29.816 +2020-09-13 17:15:00,75.48,46.41,40.387,29.816 +2020-09-13 17:30:00,76.49,46.678000000000004,40.387,29.816 +2020-09-13 17:45:00,80.06,46.943999999999996,40.387,29.816 +2020-09-13 18:00:00,80.24,50.817,44.575,29.816 +2020-09-13 18:15:00,80.31,50.998000000000005,44.575,29.816 +2020-09-13 18:30:00,86.23,50.026,44.575,29.816 +2020-09-13 18:45:00,84.95,51.357,44.575,29.816 +2020-09-13 19:00:00,83.91,55.43899999999999,45.623999999999995,29.816 +2020-09-13 19:15:00,82.55,53.001000000000005,45.623999999999995,29.816 +2020-09-13 19:30:00,81.33,52.494,45.623999999999995,29.816 +2020-09-13 19:45:00,82.57,52.943999999999996,45.623999999999995,29.816 +2020-09-13 20:00:00,82.59,52.394,44.583999999999996,29.816 +2020-09-13 20:15:00,85.52,52.29,44.583999999999996,29.816 +2020-09-13 20:30:00,81.65,51.228,44.583999999999996,29.816 +2020-09-13 20:45:00,80.24,50.083999999999996,44.583999999999996,29.816 +2020-09-13 21:00:00,74.96,48.508,39.732,29.816 +2020-09-13 21:15:00,80.14,50.928000000000004,39.732,29.816 +2020-09-13 21:30:00,82.33,50.347,39.732,29.816 +2020-09-13 21:45:00,81.42,49.684,39.732,29.816 +2020-09-13 22:00:00,76.5,48.83,38.571,29.816 +2020-09-13 22:15:00,71.7,48.037,38.571,29.816 +2020-09-13 22:30:00,69.2,45.117,38.571,29.816 +2020-09-13 22:45:00,69.21,42.193000000000005,38.571,29.816 +2020-09-13 23:00:00,66.31,40.039,33.121,29.816 +2020-09-13 23:15:00,68.89,38.861,33.121,29.816 +2020-09-13 23:30:00,67.54,38.577,33.121,29.816 +2020-09-13 23:45:00,72.63,37.861999999999995,33.121,29.816 +2020-09-14 00:00:00,70.49,34.855,32.506,29.93 +2020-09-14 00:15:00,71.68,34.483000000000004,32.506,29.93 +2020-09-14 00:30:00,66.33,33.631,32.506,29.93 +2020-09-14 00:45:00,63.56,33.067,32.506,29.93 +2020-09-14 01:00:00,65.34,33.099000000000004,31.121,29.93 +2020-09-14 01:15:00,71.25,32.488,31.121,29.93 +2020-09-14 01:30:00,71.36,31.708000000000002,31.121,29.93 +2020-09-14 01:45:00,70.39,31.218000000000004,31.121,29.93 +2020-09-14 02:00:00,67.0,31.398000000000003,29.605999999999998,29.93 +2020-09-14 02:15:00,72.09,30.285,29.605999999999998,29.93 +2020-09-14 02:30:00,73.13,31.045,29.605999999999998,29.93 +2020-09-14 02:45:00,72.63,31.535,29.605999999999998,29.93 +2020-09-14 03:00:00,70.81,33.106,28.124000000000002,29.93 +2020-09-14 03:15:00,74.52,31.967,28.124000000000002,29.93 +2020-09-14 03:30:00,76.44,31.909000000000002,28.124000000000002,29.93 +2020-09-14 03:45:00,75.06,32.779,28.124000000000002,29.93 +2020-09-14 04:00:00,77.33,40.942,29.743000000000002,29.93 +2020-09-14 04:15:00,83.88,48.583999999999996,29.743000000000002,29.93 +2020-09-14 04:30:00,89.01,46.166000000000004,29.743000000000002,29.93 +2020-09-14 04:45:00,92.93,46.215,29.743000000000002,29.93 +2020-09-14 05:00:00,94.93,62.798,36.191,29.93 +2020-09-14 05:15:00,104.51,74.667,36.191,29.93 +2020-09-14 05:30:00,102.86,68.46,36.191,29.93 +2020-09-14 05:45:00,103.19,64.678,36.191,29.93 +2020-09-14 06:00:00,108.82,63.763999999999996,55.277,29.93 +2020-09-14 06:15:00,111.8,65.002,55.277,29.93 +2020-09-14 06:30:00,112.82,63.923,55.277,29.93 +2020-09-14 06:45:00,111.4,66.351,55.277,29.93 +2020-09-14 07:00:00,115.98,64.559,65.697,29.93 +2020-09-14 07:15:00,110.78,66.132,65.697,29.93 +2020-09-14 07:30:00,110.77,64.83,65.697,29.93 +2020-09-14 07:45:00,108.81,66.53399999999999,65.697,29.93 +2020-09-14 08:00:00,107.66,65.313,57.028,29.93 +2020-09-14 08:15:00,106.67,67.42,57.028,29.93 +2020-09-14 08:30:00,111.75,66.61,57.028,29.93 +2020-09-14 08:45:00,114.96,68.684,57.028,29.93 +2020-09-14 09:00:00,107.61,64.957,52.633,29.93 +2020-09-14 09:15:00,104.76,63.328,52.633,29.93 +2020-09-14 09:30:00,104.17,64.493,52.633,29.93 +2020-09-14 09:45:00,103.52,64.554,52.633,29.93 +2020-09-14 10:00:00,104.08,62.263000000000005,50.647,29.93 +2020-09-14 10:15:00,103.09,62.821000000000005,50.647,29.93 +2020-09-14 10:30:00,103.8,62.4,50.647,29.93 +2020-09-14 10:45:00,103.53,62.397,50.647,29.93 +2020-09-14 11:00:00,102.76,58.821999999999996,50.245,29.93 +2020-09-14 11:15:00,103.69,59.413000000000004,50.245,29.93 +2020-09-14 11:30:00,102.69,61.208,50.245,29.93 +2020-09-14 11:45:00,105.2,62.097,50.245,29.93 +2020-09-14 12:00:00,101.81,58.416000000000004,46.956,29.93 +2020-09-14 12:15:00,103.71,57.656000000000006,46.956,29.93 +2020-09-14 12:30:00,101.04,56.28,46.956,29.93 +2020-09-14 12:45:00,103.68,56.665,46.956,29.93 +2020-09-14 13:00:00,99.52,55.81399999999999,47.383,29.93 +2020-09-14 13:15:00,98.82,54.8,47.383,29.93 +2020-09-14 13:30:00,100.67,53.45399999999999,47.383,29.93 +2020-09-14 13:45:00,102.23,53.481,47.383,29.93 +2020-09-14 14:00:00,102.81,53.742,47.1,29.93 +2020-09-14 14:15:00,100.22,53.089,47.1,29.93 +2020-09-14 14:30:00,100.3,51.821999999999996,47.1,29.93 +2020-09-14 14:45:00,98.73,53.272,47.1,29.93 +2020-09-14 15:00:00,99.67,53.479,49.355,29.93 +2020-09-14 15:15:00,101.22,50.736999999999995,49.355,29.93 +2020-09-14 15:30:00,103.35,50.004,49.355,29.93 +2020-09-14 15:45:00,102.28,47.64,49.355,29.93 +2020-09-14 16:00:00,103.7,50.174,52.14,29.93 +2020-09-14 16:15:00,104.69,50.006,52.14,29.93 +2020-09-14 16:30:00,107.28,50.61,52.14,29.93 +2020-09-14 16:45:00,109.54,48.20399999999999,52.14,29.93 +2020-09-14 17:00:00,111.52,49.339,58.705,29.93 +2020-09-14 17:15:00,109.57,50.282,58.705,29.93 +2020-09-14 17:30:00,112.39,50.155,58.705,29.93 +2020-09-14 17:45:00,111.73,49.925,58.705,29.93 +2020-09-14 18:00:00,113.13,52.857,59.153,29.93 +2020-09-14 18:15:00,111.59,51.177,59.153,29.93 +2020-09-14 18:30:00,117.13,49.633,59.153,29.93 +2020-09-14 18:45:00,115.67,53.824,59.153,29.93 +2020-09-14 19:00:00,112.01,57.446000000000005,61.483000000000004,29.93 +2020-09-14 19:15:00,107.89,56.045,61.483000000000004,29.93 +2020-09-14 19:30:00,105.2,55.27,61.483000000000004,29.93 +2020-09-14 19:45:00,108.61,55.082,61.483000000000004,29.93 +2020-09-14 20:00:00,105.34,53.168,67.55,29.93 +2020-09-14 20:15:00,106.11,54.007,67.55,29.93 +2020-09-14 20:30:00,103.61,53.163000000000004,67.55,29.93 +2020-09-14 20:45:00,94.43,52.355,67.55,29.93 +2020-09-14 21:00:00,92.43,50.291000000000004,60.026,29.93 +2020-09-14 21:15:00,89.42,52.953,60.026,29.93 +2020-09-14 21:30:00,85.77,52.648999999999994,60.026,29.93 +2020-09-14 21:45:00,91.92,51.695,60.026,29.93 +2020-09-14 22:00:00,88.36,48.677,52.736999999999995,29.93 +2020-09-14 22:15:00,88.46,49.465,52.736999999999995,29.93 +2020-09-14 22:30:00,82.45,41.596000000000004,52.736999999999995,29.93 +2020-09-14 22:45:00,82.32,38.1,52.736999999999995,29.93 +2020-09-14 23:00:00,80.56,36.128,44.408,29.93 +2020-09-14 23:15:00,81.23,33.766,44.408,29.93 +2020-09-14 23:30:00,76.93,33.77,44.408,29.93 +2020-09-14 23:45:00,75.97,32.887,44.408,29.93 +2020-09-15 00:00:00,76.99,32.999,44.438,29.93 +2020-09-15 00:15:00,79.26,33.558,44.438,29.93 +2020-09-15 00:30:00,78.82,33.234,44.438,29.93 +2020-09-15 00:45:00,74.27,33.263000000000005,44.438,29.93 +2020-09-15 01:00:00,70.44,32.773,41.468999999999994,29.93 +2020-09-15 01:15:00,74.86,32.23,41.468999999999994,29.93 +2020-09-15 01:30:00,77.73,31.374000000000002,41.468999999999994,29.93 +2020-09-15 01:45:00,78.5,30.465999999999998,41.468999999999994,29.93 +2020-09-15 02:00:00,77.58,30.223000000000003,39.708,29.93 +2020-09-15 02:15:00,76.77,30.145,39.708,29.93 +2020-09-15 02:30:00,78.83,30.465,39.708,29.93 +2020-09-15 02:45:00,78.93,31.241,39.708,29.93 +2020-09-15 03:00:00,79.89,32.166,38.919000000000004,29.93 +2020-09-15 03:15:00,80.14,31.774,38.919000000000004,29.93 +2020-09-15 03:30:00,82.91,31.75,38.919000000000004,29.93 +2020-09-15 03:45:00,83.91,31.641,38.919000000000004,29.93 +2020-09-15 04:00:00,85.18,38.702,40.092,29.93 +2020-09-15 04:15:00,89.49,46.347,40.092,29.93 +2020-09-15 04:30:00,94.24,43.79600000000001,40.092,29.93 +2020-09-15 04:45:00,96.41,44.424,40.092,29.93 +2020-09-15 05:00:00,99.04,63.07899999999999,43.713,29.93 +2020-09-15 05:15:00,103.98,75.593,43.713,29.93 +2020-09-15 05:30:00,107.4,69.36,43.713,29.93 +2020-09-15 05:45:00,109.39,64.872,43.713,29.93 +2020-09-15 06:00:00,117.19,64.73100000000001,56.033,29.93 +2020-09-15 06:15:00,114.09,66.351,56.033,29.93 +2020-09-15 06:30:00,115.14,64.90899999999999,56.033,29.93 +2020-09-15 06:45:00,116.49,66.49600000000001,56.033,29.93 +2020-09-15 07:00:00,118.41,64.818,66.003,29.93 +2020-09-15 07:15:00,117.26,66.15899999999999,66.003,29.93 +2020-09-15 07:30:00,117.65,64.84100000000001,66.003,29.93 +2020-09-15 07:45:00,116.28,65.70100000000001,66.003,29.93 +2020-09-15 08:00:00,113.85,64.434,57.474,29.93 +2020-09-15 08:15:00,110.41,65.98100000000001,57.474,29.93 +2020-09-15 08:30:00,110.67,65.303,57.474,29.93 +2020-09-15 08:45:00,110.98,66.51,57.474,29.93 +2020-09-15 09:00:00,110.26,62.99100000000001,51.928000000000004,29.93 +2020-09-15 09:15:00,109.79,61.425,51.928000000000004,29.93 +2020-09-15 09:30:00,108.62,63.258,51.928000000000004,29.93 +2020-09-15 09:45:00,108.66,64.442,51.928000000000004,29.93 +2020-09-15 10:00:00,108.71,60.86600000000001,49.46,29.93 +2020-09-15 10:15:00,108.89,61.133,49.46,29.93 +2020-09-15 10:30:00,108.34,60.746,49.46,29.93 +2020-09-15 10:45:00,108.25,61.647,49.46,29.93 +2020-09-15 11:00:00,106.43,58.276,48.206,29.93 +2020-09-15 11:15:00,104.48,59.166000000000004,48.206,29.93 +2020-09-15 11:30:00,106.87,59.805,48.206,29.93 +2020-09-15 11:45:00,104.72,60.371,48.206,29.93 +2020-09-15 12:00:00,102.62,56.325,46.285,29.93 +2020-09-15 12:15:00,101.2,55.792,46.285,29.93 +2020-09-15 12:30:00,102.37,55.288999999999994,46.285,29.93 +2020-09-15 12:45:00,101.53,56.23,46.285,29.93 +2020-09-15 13:00:00,101.21,54.992,46.861999999999995,29.93 +2020-09-15 13:15:00,100.74,55.506,46.861999999999995,29.93 +2020-09-15 13:30:00,100.23,54.276,46.861999999999995,29.93 +2020-09-15 13:45:00,102.07,53.488,46.861999999999995,29.93 +2020-09-15 14:00:00,100.98,54.14,46.488,29.93 +2020-09-15 14:15:00,101.37,53.342,46.488,29.93 +2020-09-15 14:30:00,102.21,52.472,46.488,29.93 +2020-09-15 14:45:00,104.2,53.218,46.488,29.93 +2020-09-15 15:00:00,103.31,53.176,48.442,29.93 +2020-09-15 15:15:00,105.66,51.303999999999995,48.442,29.93 +2020-09-15 15:30:00,104.22,50.43899999999999,48.442,29.93 +2020-09-15 15:45:00,106.45,48.342,48.442,29.93 +2020-09-15 16:00:00,107.2,50.266000000000005,50.397,29.93 +2020-09-15 16:15:00,108.47,50.245,50.397,29.93 +2020-09-15 16:30:00,111.24,50.597,50.397,29.93 +2020-09-15 16:45:00,112.38,48.913000000000004,50.397,29.93 +2020-09-15 17:00:00,118.39,50.25899999999999,56.668,29.93 +2020-09-15 17:15:00,113.96,51.603,56.668,29.93 +2020-09-15 17:30:00,113.72,51.18,56.668,29.93 +2020-09-15 17:45:00,114.84,50.68,56.668,29.93 +2020-09-15 18:00:00,117.58,52.738,57.957,29.93 +2020-09-15 18:15:00,116.18,52.331,57.957,29.93 +2020-09-15 18:30:00,120.29,50.532,57.957,29.93 +2020-09-15 18:45:00,119.15,54.638999999999996,57.957,29.93 +2020-09-15 19:00:00,120.04,57.236000000000004,57.056000000000004,29.93 +2020-09-15 19:15:00,118.63,55.964,57.056000000000004,29.93 +2020-09-15 19:30:00,113.73,54.924,57.056000000000004,29.93 +2020-09-15 19:45:00,113.54,55.038000000000004,57.056000000000004,29.93 +2020-09-15 20:00:00,105.26,53.512,64.156,29.93 +2020-09-15 20:15:00,101.39,52.998000000000005,64.156,29.93 +2020-09-15 20:30:00,98.17,52.282,64.156,29.93 +2020-09-15 20:45:00,97.7,51.735,64.156,29.93 +2020-09-15 21:00:00,98.43,50.36600000000001,56.507,29.93 +2020-09-15 21:15:00,100.0,51.815,56.507,29.93 +2020-09-15 21:30:00,94.23,51.591,56.507,29.93 +2020-09-15 21:45:00,89.6,50.799,56.507,29.93 +2020-09-15 22:00:00,85.63,48.097,50.728,29.93 +2020-09-15 22:15:00,88.34,48.534,50.728,29.93 +2020-09-15 22:30:00,87.42,40.913000000000004,50.728,29.93 +2020-09-15 22:45:00,85.3,37.449,50.728,29.93 +2020-09-15 23:00:00,78.95,34.841,43.556999999999995,29.93 +2020-09-15 23:15:00,83.3,33.741,43.556999999999995,29.93 +2020-09-15 23:30:00,83.87,33.709,43.556999999999995,29.93 +2020-09-15 23:45:00,78.6,32.943000000000005,43.556999999999995,29.93 +2020-09-16 00:00:00,73.44,39.084,41.151,29.93 +2020-09-16 00:15:00,73.98,39.830999999999996,41.151,29.93 +2020-09-16 00:30:00,78.99,39.635,41.151,29.93 +2020-09-16 00:45:00,80.82,39.476,41.151,29.93 +2020-09-16 01:00:00,76.35,39.616,37.763000000000005,29.93 +2020-09-16 01:15:00,73.59,38.859,37.763000000000005,29.93 +2020-09-16 01:30:00,72.26,37.896,37.763000000000005,29.93 +2020-09-16 01:45:00,78.96,37.031,37.763000000000005,29.93 +2020-09-16 02:00:00,78.44,37.302,35.615,29.93 +2020-09-16 02:15:00,78.12,37.166,35.615,29.93 +2020-09-16 02:30:00,72.51,37.736,35.615,29.93 +2020-09-16 02:45:00,78.41,38.519,35.615,29.93 +2020-09-16 03:00:00,82.31,40.568000000000005,35.153,29.93 +2020-09-16 03:15:00,81.48,40.628,35.153,29.93 +2020-09-16 03:30:00,78.89,40.606,35.153,29.93 +2020-09-16 03:45:00,80.9,41.105,35.153,29.93 +2020-09-16 04:00:00,85.15,49.181000000000004,36.203,29.93 +2020-09-16 04:15:00,89.89,57.271,36.203,29.93 +2020-09-16 04:30:00,92.92,55.732,36.203,29.93 +2020-09-16 04:45:00,92.18,56.861000000000004,36.203,29.93 +2020-09-16 05:00:00,99.03,78.215,39.922,29.93 +2020-09-16 05:15:00,104.76,95.447,39.922,29.93 +2020-09-16 05:30:00,108.0,89.35799999999999,39.922,29.93 +2020-09-16 05:45:00,109.51,83.431,39.922,29.93 +2020-09-16 06:00:00,113.65,83.573,56.443999999999996,29.93 +2020-09-16 06:15:00,114.5,86.17,56.443999999999996,29.93 +2020-09-16 06:30:00,115.4,84.446,56.443999999999996,29.93 +2020-09-16 06:45:00,117.08,85.50399999999999,56.443999999999996,29.93 +2020-09-16 07:00:00,119.77,85.01899999999999,68.683,29.93 +2020-09-16 07:15:00,119.16,86.70100000000001,68.683,29.93 +2020-09-16 07:30:00,124.41,85.609,68.683,29.93 +2020-09-16 07:45:00,120.27,86.053,68.683,29.93 +2020-09-16 08:00:00,119.32,82.478,59.003,29.93 +2020-09-16 08:15:00,118.97,83.669,59.003,29.93 +2020-09-16 08:30:00,117.59,81.854,59.003,29.93 +2020-09-16 08:45:00,117.4,82.26899999999999,59.003,29.93 +2020-09-16 09:00:00,118.31,76.666,56.21,29.93 +2020-09-16 09:15:00,120.11,74.795,56.21,29.93 +2020-09-16 09:30:00,122.52,75.825,56.21,29.93 +2020-09-16 09:45:00,124.42,76.199,56.21,29.93 +2020-09-16 10:00:00,126.1,73.703,52.358999999999995,29.93 +2020-09-16 10:15:00,126.69,73.807,52.358999999999995,29.93 +2020-09-16 10:30:00,121.23,73.306,52.358999999999995,29.93 +2020-09-16 10:45:00,118.65,73.608,52.358999999999995,29.93 +2020-09-16 11:00:00,109.55,71.77600000000001,51.161,29.93 +2020-09-16 11:15:00,111.88,72.722,51.161,29.93 +2020-09-16 11:30:00,117.41,73.042,51.161,29.93 +2020-09-16 11:45:00,116.4,72.767,51.161,29.93 +2020-09-16 12:00:00,107.59,70.317,49.119,29.93 +2020-09-16 12:15:00,105.17,70.104,49.119,29.93 +2020-09-16 12:30:00,102.46,69.05199999999999,49.119,29.93 +2020-09-16 12:45:00,103.14,69.616,49.119,29.93 +2020-09-16 13:00:00,101.75,69.403,49.187,29.93 +2020-09-16 13:15:00,101.95,69.571,49.187,29.93 +2020-09-16 13:30:00,104.52,68.53399999999999,49.187,29.93 +2020-09-16 13:45:00,101.25,67.885,49.187,29.93 +2020-09-16 14:00:00,102.1,68.471,49.787,29.93 +2020-09-16 14:15:00,103.0,68.127,49.787,29.93 +2020-09-16 14:30:00,103.44,66.956,49.787,29.93 +2020-09-16 14:45:00,104.37,67.27600000000001,49.787,29.93 +2020-09-16 15:00:00,102.18,67.498,51.458999999999996,29.93 +2020-09-16 15:15:00,104.63,66.195,51.458999999999996,29.93 +2020-09-16 15:30:00,104.49,65.661,51.458999999999996,29.93 +2020-09-16 15:45:00,106.34,64.396,51.458999999999996,29.93 +2020-09-16 16:00:00,111.51,65.649,53.663000000000004,29.93 +2020-09-16 16:15:00,108.92,65.133,53.663000000000004,29.93 +2020-09-16 16:30:00,110.62,65.763,53.663000000000004,29.93 +2020-09-16 16:45:00,111.54,63.458999999999996,53.663000000000004,29.93 +2020-09-16 17:00:00,116.11,64.80199999999999,58.183,29.93 +2020-09-16 17:15:00,114.55,65.96,58.183,29.93 +2020-09-16 17:30:00,114.66,65.595,58.183,29.93 +2020-09-16 17:45:00,115.07,65.561,58.183,29.93 +2020-09-16 18:00:00,117.2,65.617,60.141000000000005,29.93 +2020-09-16 18:15:00,116.17,65.419,60.141000000000005,29.93 +2020-09-16 18:30:00,119.01,64.142,60.141000000000005,29.93 +2020-09-16 18:45:00,118.27,68.15,60.141000000000005,29.93 +2020-09-16 19:00:00,116.67,67.969,60.582,29.93 +2020-09-16 19:15:00,116.96,66.834,60.582,29.93 +2020-09-16 19:30:00,113.33,66.178,60.582,29.93 +2020-09-16 19:45:00,115.63,66.4,60.582,29.93 +2020-09-16 20:00:00,104.66,64.4,66.61,29.93 +2020-09-16 20:15:00,107.17,62.825,66.61,29.93 +2020-09-16 20:30:00,104.72,61.527,66.61,29.93 +2020-09-16 20:45:00,104.85,61.146,66.61,29.93 +2020-09-16 21:00:00,98.44,59.425,57.658,29.93 +2020-09-16 21:15:00,92.27,60.768,57.658,29.93 +2020-09-16 21:30:00,95.86,59.723,57.658,29.93 +2020-09-16 21:45:00,94.6,58.926,57.658,29.93 +2020-09-16 22:00:00,90.23,57.67,51.81,29.93 +2020-09-16 22:15:00,84.5,56.826,51.81,29.93 +2020-09-16 22:30:00,82.73,48.071000000000005,51.81,29.93 +2020-09-16 22:45:00,80.31,44.38,51.81,29.93 +2020-09-16 23:00:00,82.5,40.444,42.93600000000001,29.93 +2020-09-16 23:15:00,82.69,40.001999999999995,42.93600000000001,29.93 +2020-09-16 23:30:00,82.91,40.023,42.93600000000001,29.93 +2020-09-16 23:45:00,78.13,39.402,42.93600000000001,29.93 +2020-09-17 00:00:00,79.44,39.347,39.211,29.93 +2020-09-17 00:15:00,80.59,40.09,39.211,29.93 +2020-09-17 00:30:00,78.36,39.903,39.211,29.93 +2020-09-17 00:45:00,75.57,39.747,39.211,29.93 +2020-09-17 01:00:00,79.12,39.88,37.607,29.93 +2020-09-17 01:15:00,78.2,39.145,37.607,29.93 +2020-09-17 01:30:00,77.23,38.2,37.607,29.93 +2020-09-17 01:45:00,74.58,37.333,37.607,29.93 +2020-09-17 02:00:00,78.04,37.611,36.44,29.93 +2020-09-17 02:15:00,79.78,37.494,36.44,29.93 +2020-09-17 02:30:00,76.24,38.041,36.44,29.93 +2020-09-17 02:45:00,75.01,38.82,36.44,29.93 +2020-09-17 03:00:00,77.9,40.855,36.116,29.93 +2020-09-17 03:15:00,79.65,40.934,36.116,29.93 +2020-09-17 03:30:00,81.81,40.917,36.116,29.93 +2020-09-17 03:45:00,78.48,41.401,36.116,29.93 +2020-09-17 04:00:00,87.76,49.50899999999999,37.398,29.93 +2020-09-17 04:15:00,91.21,57.629,37.398,29.93 +2020-09-17 04:30:00,95.34,56.093999999999994,37.398,29.93 +2020-09-17 04:45:00,94.05,57.232,37.398,29.93 +2020-09-17 05:00:00,98.21,78.67699999999999,41.776,29.93 +2020-09-17 05:15:00,104.48,96.00299999999999,41.776,29.93 +2020-09-17 05:30:00,108.42,89.899,41.776,29.93 +2020-09-17 05:45:00,108.7,83.925,41.776,29.93 +2020-09-17 06:00:00,114.23,84.03399999999999,55.61,29.93 +2020-09-17 06:15:00,113.86,86.65299999999999,55.61,29.93 +2020-09-17 06:30:00,116.05,84.93299999999999,55.61,29.93 +2020-09-17 06:45:00,116.34,85.98899999999999,55.61,29.93 +2020-09-17 07:00:00,117.73,85.50200000000001,67.13600000000001,29.93 +2020-09-17 07:15:00,117.01,87.2,67.13600000000001,29.93 +2020-09-17 07:30:00,115.49,86.14299999999999,67.13600000000001,29.93 +2020-09-17 07:45:00,114.64,86.59,67.13600000000001,29.93 +2020-09-17 08:00:00,113.27,83.023,57.55,29.93 +2020-09-17 08:15:00,113.0,84.181,57.55,29.93 +2020-09-17 08:30:00,112.72,82.383,57.55,29.93 +2020-09-17 08:45:00,115.23,82.779,57.55,29.93 +2020-09-17 09:00:00,116.21,77.181,52.931999999999995,29.93 +2020-09-17 09:15:00,118.42,75.3,52.931999999999995,29.93 +2020-09-17 09:30:00,115.37,76.311,52.931999999999995,29.93 +2020-09-17 09:45:00,110.86,76.655,52.931999999999995,29.93 +2020-09-17 10:00:00,114.03,74.156,50.36600000000001,29.93 +2020-09-17 10:15:00,117.04,74.223,50.36600000000001,29.93 +2020-09-17 10:30:00,119.88,73.707,50.36600000000001,29.93 +2020-09-17 10:45:00,114.08,73.994,50.36600000000001,29.93 +2020-09-17 11:00:00,112.85,72.17399999999999,47.893,29.93 +2020-09-17 11:15:00,111.3,73.104,47.893,29.93 +2020-09-17 11:30:00,112.41,73.421,47.893,29.93 +2020-09-17 11:45:00,109.95,73.13,47.893,29.93 +2020-09-17 12:00:00,109.44,70.656,45.271,29.93 +2020-09-17 12:15:00,107.34,70.431,45.271,29.93 +2020-09-17 12:30:00,105.72,69.41,45.271,29.93 +2020-09-17 12:45:00,114.51,69.967,45.271,29.93 +2020-09-17 13:00:00,108.61,69.725,44.351000000000006,29.93 +2020-09-17 13:15:00,107.32,69.892,44.351000000000006,29.93 +2020-09-17 13:30:00,104.8,68.85300000000001,44.351000000000006,29.93 +2020-09-17 13:45:00,105.93,68.209,44.351000000000006,29.93 +2020-09-17 14:00:00,105.83,68.747,44.99,29.93 +2020-09-17 14:15:00,108.76,68.418,44.99,29.93 +2020-09-17 14:30:00,104.88,67.279,44.99,29.93 +2020-09-17 14:45:00,103.7,67.595,44.99,29.93 +2020-09-17 15:00:00,105.59,67.77199999999999,46.869,29.93 +2020-09-17 15:15:00,105.16,66.488,46.869,29.93 +2020-09-17 15:30:00,103.15,65.985,46.869,29.93 +2020-09-17 15:45:00,107.04,64.735,46.869,29.93 +2020-09-17 16:00:00,107.68,65.949,48.902,29.93 +2020-09-17 16:15:00,110.33,65.449,48.902,29.93 +2020-09-17 16:30:00,109.86,66.07600000000001,48.902,29.93 +2020-09-17 16:45:00,111.84,63.827,48.902,29.93 +2020-09-17 17:00:00,114.46,65.133,53.244,29.93 +2020-09-17 17:15:00,113.14,66.313,53.244,29.93 +2020-09-17 17:30:00,113.05,65.952,53.244,29.93 +2020-09-17 17:45:00,114.69,65.947,53.244,29.93 +2020-09-17 18:00:00,115.77,65.985,54.343999999999994,29.93 +2020-09-17 18:15:00,118.59,65.783,54.343999999999994,29.93 +2020-09-17 18:30:00,118.36,64.518,54.343999999999994,29.93 +2020-09-17 18:45:00,120.85,68.518,54.343999999999994,29.93 +2020-09-17 19:00:00,115.71,68.35,54.332,29.93 +2020-09-17 19:15:00,116.59,67.211,54.332,29.93 +2020-09-17 19:30:00,109.14,66.551,54.332,29.93 +2020-09-17 19:45:00,110.22,66.765,54.332,29.93 +2020-09-17 20:00:00,101.81,64.786,58.06,29.93 +2020-09-17 20:15:00,102.55,63.208,58.06,29.93 +2020-09-17 20:30:00,100.73,61.886,58.06,29.93 +2020-09-17 20:45:00,100.68,61.467,58.06,29.93 +2020-09-17 21:00:00,95.01,59.748999999999995,52.411,29.93 +2020-09-17 21:15:00,100.41,61.083,52.411,29.93 +2020-09-17 21:30:00,97.41,60.045,52.411,29.93 +2020-09-17 21:45:00,94.7,59.213,52.411,29.93 +2020-09-17 22:00:00,85.44,57.93899999999999,47.148999999999994,29.93 +2020-09-17 22:15:00,89.95,57.071000000000005,47.148999999999994,29.93 +2020-09-17 22:30:00,89.79,48.309,47.148999999999994,29.93 +2020-09-17 22:45:00,89.53,44.622,47.148999999999994,29.93 +2020-09-17 23:00:00,81.01,40.726,40.814,29.93 +2020-09-17 23:15:00,79.55,40.256,40.814,29.93 +2020-09-17 23:30:00,83.76,40.281,40.814,29.93 +2020-09-17 23:45:00,83.95,39.659,40.814,29.93 +2020-09-18 00:00:00,80.29,38.004,39.153,29.93 +2020-09-18 00:15:00,74.72,38.957,39.153,29.93 +2020-09-18 00:30:00,73.6,38.939,39.153,29.93 +2020-09-18 00:45:00,72.77,39.126,39.153,29.93 +2020-09-18 01:00:00,71.06,38.888000000000005,37.228,29.93 +2020-09-18 01:15:00,72.86,37.991,37.228,29.93 +2020-09-18 01:30:00,71.99,37.521,37.228,29.93 +2020-09-18 01:45:00,72.65,36.501999999999995,37.228,29.93 +2020-09-18 02:00:00,72.83,37.49,35.851,29.93 +2020-09-18 02:15:00,78.45,37.321999999999996,35.851,29.93 +2020-09-18 02:30:00,72.91,38.597,35.851,29.93 +2020-09-18 02:45:00,73.98,38.884,35.851,29.93 +2020-09-18 03:00:00,74.75,41.191,36.54,29.93 +2020-09-18 03:15:00,77.42,40.675,36.54,29.93 +2020-09-18 03:30:00,77.21,40.485,36.54,29.93 +2020-09-18 03:45:00,81.51,41.681000000000004,36.54,29.93 +2020-09-18 04:00:00,90.84,49.988,37.578,29.93 +2020-09-18 04:15:00,91.02,56.937,37.578,29.93 +2020-09-18 04:30:00,86.07,56.161,37.578,29.93 +2020-09-18 04:45:00,92.31,56.513999999999996,37.578,29.93 +2020-09-18 05:00:00,108.17,77.343,40.387,29.93 +2020-09-18 05:15:00,112.85,95.874,40.387,29.93 +2020-09-18 05:30:00,108.76,90.18299999999999,40.387,29.93 +2020-09-18 05:45:00,109.88,83.81,40.387,29.93 +2020-09-18 06:00:00,117.15,84.181,54.668,29.93 +2020-09-18 06:15:00,115.51,86.54899999999999,54.668,29.93 +2020-09-18 06:30:00,115.18,84.565,54.668,29.93 +2020-09-18 06:45:00,116.95,85.958,54.668,29.93 +2020-09-18 07:00:00,118.68,85.72399999999999,63.971000000000004,29.93 +2020-09-18 07:15:00,118.95,88.38600000000001,63.971000000000004,29.93 +2020-09-18 07:30:00,116.43,85.897,63.971000000000004,29.93 +2020-09-18 07:45:00,116.62,85.943,63.971000000000004,29.93 +2020-09-18 08:00:00,114.28,82.63,56.042,29.93 +2020-09-18 08:15:00,112.83,84.12299999999999,56.042,29.93 +2020-09-18 08:30:00,114.36,82.556,56.042,29.93 +2020-09-18 08:45:00,115.55,82.37,56.042,29.93 +2020-09-18 09:00:00,114.17,75.26,52.832,29.93 +2020-09-18 09:15:00,111.77,74.903,52.832,29.93 +2020-09-18 09:30:00,111.15,75.277,52.832,29.93 +2020-09-18 09:45:00,111.61,75.834,52.832,29.93 +2020-09-18 10:00:00,107.89,72.842,50.044,29.93 +2020-09-18 10:15:00,109.25,72.935,50.044,29.93 +2020-09-18 10:30:00,109.02,72.765,50.044,29.93 +2020-09-18 10:45:00,107.6,72.832,50.044,29.93 +2020-09-18 11:00:00,107.22,71.185,49.06100000000001,29.93 +2020-09-18 11:15:00,115.43,71.064,49.06100000000001,29.93 +2020-09-18 11:30:00,114.09,71.685,49.06100000000001,29.93 +2020-09-18 11:45:00,112.5,70.729,49.06100000000001,29.93 +2020-09-18 12:00:00,109.27,68.899,45.595,29.93 +2020-09-18 12:15:00,107.26,67.53399999999999,45.595,29.93 +2020-09-18 12:30:00,111.87,66.678,45.595,29.93 +2020-09-18 12:45:00,112.45,66.866,45.595,29.93 +2020-09-18 13:00:00,107.19,67.291,43.218,29.93 +2020-09-18 13:15:00,111.3,67.846,43.218,29.93 +2020-09-18 13:30:00,109.47,67.354,43.218,29.93 +2020-09-18 13:45:00,112.2,66.903,43.218,29.93 +2020-09-18 14:00:00,112.75,66.477,41.926,29.93 +2020-09-18 14:15:00,113.18,66.402,41.926,29.93 +2020-09-18 14:30:00,113.34,66.488,41.926,29.93 +2020-09-18 14:45:00,112.16,66.43,41.926,29.93 +2020-09-18 15:00:00,107.96,66.34899999999999,43.79,29.93 +2020-09-18 15:15:00,108.95,64.778,43.79,29.93 +2020-09-18 15:30:00,111.25,63.426,43.79,29.93 +2020-09-18 15:45:00,111.89,62.758,43.79,29.93 +2020-09-18 16:00:00,114.19,63.008,45.895,29.93 +2020-09-18 16:15:00,112.94,62.958,45.895,29.93 +2020-09-18 16:30:00,113.82,63.486000000000004,45.895,29.93 +2020-09-18 16:45:00,116.38,60.707,45.895,29.93 +2020-09-18 17:00:00,117.22,63.245,51.36,29.93 +2020-09-18 17:15:00,116.42,64.20100000000001,51.36,29.93 +2020-09-18 17:30:00,115.99,63.861000000000004,51.36,29.93 +2020-09-18 17:45:00,115.41,63.695,51.36,29.93 +2020-09-18 18:00:00,117.73,64.012,52.985,29.93 +2020-09-18 18:15:00,116.7,63.016000000000005,52.985,29.93 +2020-09-18 18:30:00,116.11,61.803000000000004,52.985,29.93 +2020-09-18 18:45:00,115.8,66.096,52.985,29.93 +2020-09-18 19:00:00,113.5,66.872,52.602,29.93 +2020-09-18 19:15:00,108.61,66.538,52.602,29.93 +2020-09-18 19:30:00,106.11,65.805,52.602,29.93 +2020-09-18 19:45:00,104.55,65.138,52.602,29.93 +2020-09-18 20:00:00,95.47,63.067,58.063,29.93 +2020-09-18 20:15:00,94.24,62.063,58.063,29.93 +2020-09-18 20:30:00,92.29,60.351000000000006,58.063,29.93 +2020-09-18 20:45:00,95.23,59.466,58.063,29.93 +2020-09-18 21:00:00,87.91,58.849,50.135,29.93 +2020-09-18 21:15:00,85.57,61.511,50.135,29.93 +2020-09-18 21:30:00,78.26,60.382,50.135,29.93 +2020-09-18 21:45:00,84.38,59.817,50.135,29.93 +2020-09-18 22:00:00,79.5,58.698,45.165,29.93 +2020-09-18 22:15:00,81.75,57.588,45.165,29.93 +2020-09-18 22:30:00,74.47,54.06,45.165,29.93 +2020-09-18 22:45:00,77.94,51.985,45.165,29.93 +2020-09-18 23:00:00,76.49,49.233000000000004,39.121,29.93 +2020-09-18 23:15:00,74.79,47.049,39.121,29.93 +2020-09-18 23:30:00,72.85,45.343999999999994,39.121,29.93 +2020-09-18 23:45:00,71.53,44.448,39.121,29.93 +2020-09-19 00:00:00,71.22,38.38,38.49,29.816 +2020-09-19 00:15:00,70.98,37.655,38.49,29.816 +2020-09-19 00:30:00,62.35,37.674,38.49,29.816 +2020-09-19 00:45:00,70.07,37.529,38.49,29.816 +2020-09-19 01:00:00,68.84,37.658,34.5,29.816 +2020-09-19 01:15:00,66.83,36.938,34.5,29.816 +2020-09-19 01:30:00,59.53,35.755,34.5,29.816 +2020-09-19 01:45:00,60.48,35.583,34.5,29.816 +2020-09-19 02:00:00,61.5,36.045,32.236,29.816 +2020-09-19 02:15:00,67.13,35.221,32.236,29.816 +2020-09-19 02:30:00,59.26,35.586999999999996,32.236,29.816 +2020-09-19 02:45:00,59.5,36.468,32.236,29.816 +2020-09-19 03:00:00,58.35,37.939,32.067,29.816 +2020-09-19 03:15:00,60.56,36.596,32.067,29.816 +2020-09-19 03:30:00,58.82,36.205,32.067,29.816 +2020-09-19 03:45:00,60.46,38.491,32.067,29.816 +2020-09-19 04:00:00,61.29,44.302,33.071,29.816 +2020-09-19 04:15:00,60.56,49.925,33.071,29.816 +2020-09-19 04:30:00,57.39,47.338,33.071,29.816 +2020-09-19 04:45:00,61.32,47.757,33.071,29.816 +2020-09-19 05:00:00,63.99,58.761,33.014,29.816 +2020-09-19 05:15:00,64.16,64.37100000000001,33.014,29.816 +2020-09-19 05:30:00,65.42,59.894,33.014,29.816 +2020-09-19 05:45:00,62.16,58.2,33.014,29.816 +2020-09-19 06:00:00,67.73,71.545,34.628,29.816 +2020-09-19 06:15:00,67.75,83.959,34.628,29.816 +2020-09-19 06:30:00,71.21,78.477,34.628,29.816 +2020-09-19 06:45:00,72.05,75.16,34.628,29.816 +2020-09-19 07:00:00,74.14,72.542,38.871,29.816 +2020-09-19 07:15:00,71.97,73.934,38.871,29.816 +2020-09-19 07:30:00,76.89,73.319,38.871,29.816 +2020-09-19 07:45:00,80.01,75.127,38.871,29.816 +2020-09-19 08:00:00,80.53,73.286,43.293,29.816 +2020-09-19 08:15:00,82.15,75.622,43.293,29.816 +2020-09-19 08:30:00,81.09,74.517,43.293,29.816 +2020-09-19 08:45:00,76.52,75.882,43.293,29.816 +2020-09-19 09:00:00,75.61,71.438,44.559,29.816 +2020-09-19 09:15:00,74.54,71.634,44.559,29.816 +2020-09-19 09:30:00,74.81,72.601,44.559,29.816 +2020-09-19 09:45:00,76.44,72.857,44.559,29.816 +2020-09-19 10:00:00,76.74,70.247,42.091,29.816 +2020-09-19 10:15:00,77.89,70.598,42.091,29.816 +2020-09-19 10:30:00,82.26,70.22800000000001,42.091,29.816 +2020-09-19 10:45:00,79.12,70.435,42.091,29.816 +2020-09-19 11:00:00,78.34,68.741,38.505,29.816 +2020-09-19 11:15:00,81.3,68.98899999999999,38.505,29.816 +2020-09-19 11:30:00,77.75,69.456,38.505,29.816 +2020-09-19 11:45:00,76.77,68.646,38.505,29.816 +2020-09-19 12:00:00,74.59,66.622,35.388000000000005,29.816 +2020-09-19 12:15:00,77.13,66.02600000000001,35.388000000000005,29.816 +2020-09-19 12:30:00,69.95,65.217,35.388000000000005,29.816 +2020-09-19 12:45:00,65.4,65.63600000000001,35.388000000000005,29.816 +2020-09-19 13:00:00,66.49,65.413,31.355999999999998,29.816 +2020-09-19 13:15:00,69.13,64.945,31.355999999999998,29.816 +2020-09-19 13:30:00,67.92,64.436,31.355999999999998,29.816 +2020-09-19 13:45:00,71.32,63.224,31.355999999999998,29.816 +2020-09-19 14:00:00,68.7,63.16,30.522,29.816 +2020-09-19 14:15:00,68.61,62.07899999999999,30.522,29.816 +2020-09-19 14:30:00,70.26,61.327,30.522,29.816 +2020-09-19 14:45:00,70.21,61.659,30.522,29.816 +2020-09-19 15:00:00,71.68,62.038999999999994,34.36,29.816 +2020-09-19 15:15:00,71.93,61.226000000000006,34.36,29.816 +2020-09-19 15:30:00,73.29,60.536,34.36,29.816 +2020-09-19 15:45:00,75.49,59.263999999999996,34.36,29.816 +2020-09-19 16:00:00,78.43,60.763999999999996,39.507,29.816 +2020-09-19 16:15:00,79.18,60.381,39.507,29.816 +2020-09-19 16:30:00,79.14,61.047,39.507,29.816 +2020-09-19 16:45:00,81.7,58.574,39.507,29.816 +2020-09-19 17:00:00,83.48,60.071999999999996,47.151,29.816 +2020-09-19 17:15:00,84.27,60.06100000000001,47.151,29.816 +2020-09-19 17:30:00,85.58,59.604,47.151,29.816 +2020-09-19 17:45:00,85.8,59.68600000000001,47.151,29.816 +2020-09-19 18:00:00,89.34,60.965,50.303999999999995,29.816 +2020-09-19 18:15:00,89.84,61.67100000000001,50.303999999999995,29.816 +2020-09-19 18:30:00,90.7,61.82,50.303999999999995,29.816 +2020-09-19 18:45:00,89.05,62.67,50.303999999999995,29.816 +2020-09-19 19:00:00,87.56,62.681000000000004,50.622,29.816 +2020-09-19 19:15:00,84.6,61.475,50.622,29.816 +2020-09-19 19:30:00,82.62,61.503,50.622,29.816 +2020-09-19 19:45:00,81.68,62.065,50.622,29.816 +2020-09-19 20:00:00,77.1,61.138999999999996,45.391000000000005,29.816 +2020-09-19 20:15:00,73.8,60.276,45.391000000000005,29.816 +2020-09-19 20:30:00,72.52,57.81,45.391000000000005,29.816 +2020-09-19 20:45:00,74.56,58.104,45.391000000000005,29.816 +2020-09-19 21:00:00,70.72,57.077,39.98,29.816 +2020-09-19 21:15:00,71.27,59.6,39.98,29.816 +2020-09-19 21:30:00,68.48,58.958,39.98,29.816 +2020-09-19 21:45:00,68.12,57.851000000000006,39.98,29.816 +2020-09-19 22:00:00,63.93,57.068999999999996,37.53,29.816 +2020-09-19 22:15:00,68.03,56.803000000000004,37.53,29.816 +2020-09-19 22:30:00,62.43,54.72,37.53,29.816 +2020-09-19 22:45:00,64.15,53.43600000000001,37.53,29.816 +2020-09-19 23:00:00,60.71,50.88399999999999,30.97,29.816 +2020-09-19 23:15:00,60.35,48.526,30.97,29.816 +2020-09-19 23:30:00,59.11,48.121,30.97,29.816 +2020-09-19 23:45:00,56.8,46.816,30.97,29.816 +2020-09-20 00:00:00,55.45,39.763000000000005,27.24,29.816 +2020-09-20 00:15:00,55.34,38.111999999999995,27.24,29.816 +2020-09-20 00:30:00,53.87,37.93,27.24,29.816 +2020-09-20 00:45:00,54.21,37.898,27.24,29.816 +2020-09-20 01:00:00,52.57,38.174,25.662,29.816 +2020-09-20 01:15:00,53.16,37.672,25.662,29.816 +2020-09-20 01:30:00,53.67,36.552,25.662,29.816 +2020-09-20 01:45:00,54.29,36.016999999999996,25.662,29.816 +2020-09-20 02:00:00,51.96,36.338,25.67,29.816 +2020-09-20 02:15:00,53.19,35.809,25.67,29.816 +2020-09-20 02:30:00,53.0,36.524,25.67,29.816 +2020-09-20 02:45:00,53.05,37.319,25.67,29.816 +2020-09-20 03:00:00,52.73,39.335,24.258000000000003,29.816 +2020-09-20 03:15:00,53.33,38.053000000000004,24.258000000000003,29.816 +2020-09-20 03:30:00,52.77,37.523,24.258000000000003,29.816 +2020-09-20 03:45:00,53.76,39.198,24.258000000000003,29.816 +2020-09-20 04:00:00,54.65,44.95399999999999,25.051,29.816 +2020-09-20 04:15:00,54.55,50.015,25.051,29.816 +2020-09-20 04:30:00,55.06,48.416000000000004,25.051,29.816 +2020-09-20 04:45:00,55.46,48.582,25.051,29.816 +2020-09-20 05:00:00,56.45,58.891999999999996,25.145,29.816 +2020-09-20 05:15:00,57.48,63.419,25.145,29.816 +2020-09-20 05:30:00,58.07,58.583,25.145,29.816 +2020-09-20 05:45:00,58.21,56.698,25.145,29.816 +2020-09-20 06:00:00,59.41,68.219,26.371,29.816 +2020-09-20 06:15:00,60.9,80.828,26.371,29.816 +2020-09-20 06:30:00,62.42,74.55,26.371,29.816 +2020-09-20 06:45:00,64.42,70.22399999999999,26.371,29.816 +2020-09-20 07:00:00,66.46,68.428,28.756999999999998,29.816 +2020-09-20 07:15:00,67.42,68.416,28.756999999999998,29.816 +2020-09-20 07:30:00,69.0,68.42699999999999,28.756999999999998,29.816 +2020-09-20 07:45:00,71.83,70.008,28.756999999999998,29.816 +2020-09-20 08:00:00,72.34,69.197,32.82,29.816 +2020-09-20 08:15:00,74.03,72.319,32.82,29.816 +2020-09-20 08:30:00,74.41,72.318,32.82,29.816 +2020-09-20 08:45:00,74.79,74.168,32.82,29.816 +2020-09-20 09:00:00,76.91,69.527,35.534,29.816 +2020-09-20 09:15:00,76.22,69.52,35.534,29.816 +2020-09-20 09:30:00,78.51,70.729,35.534,29.816 +2020-09-20 09:45:00,78.9,71.635,35.534,29.816 +2020-09-20 10:00:00,78.27,70.111,35.925,29.816 +2020-09-20 10:15:00,84.18,70.676,35.925,29.816 +2020-09-20 10:30:00,87.27,70.625,35.925,29.816 +2020-09-20 10:45:00,86.96,70.958,35.925,29.816 +2020-09-20 11:00:00,84.99,69.303,37.056,29.816 +2020-09-20 11:15:00,85.58,69.262,37.056,29.816 +2020-09-20 11:30:00,85.55,69.827,37.056,29.816 +2020-09-20 11:45:00,82.98,69.359,37.056,29.816 +2020-09-20 12:00:00,78.15,67.827,33.124,29.816 +2020-09-20 12:15:00,79.24,67.391,33.124,29.816 +2020-09-20 12:30:00,78.19,66.303,33.124,29.816 +2020-09-20 12:45:00,78.17,65.979,33.124,29.816 +2020-09-20 13:00:00,77.34,65.316,29.874000000000002,29.816 +2020-09-20 13:15:00,74.05,65.38,29.874000000000002,29.816 +2020-09-20 13:30:00,74.77,64.042,29.874000000000002,29.816 +2020-09-20 13:45:00,77.63,63.398,29.874000000000002,29.816 +2020-09-20 14:00:00,79.84,64.18,27.302,29.816 +2020-09-20 14:15:00,80.59,63.778999999999996,27.302,29.816 +2020-09-20 14:30:00,80.69,62.596000000000004,27.302,29.816 +2020-09-20 14:45:00,79.0,62.066,27.302,29.816 +2020-09-20 15:00:00,76.77,62.028,27.642,29.816 +2020-09-20 15:15:00,78.69,60.915,27.642,29.816 +2020-09-20 15:30:00,79.78,60.31399999999999,27.642,29.816 +2020-09-20 15:45:00,80.99,59.467,27.642,29.816 +2020-09-20 16:00:00,82.95,60.283,31.945999999999998,29.816 +2020-09-20 16:15:00,83.28,59.808,31.945999999999998,29.816 +2020-09-20 16:30:00,84.6,61.248999999999995,31.945999999999998,29.816 +2020-09-20 16:45:00,89.69,58.915,31.945999999999998,29.816 +2020-09-20 17:00:00,94.49,60.631,40.387,29.816 +2020-09-20 17:15:00,95.66,61.602,40.387,29.816 +2020-09-20 17:30:00,95.71,61.797,40.387,29.816 +2020-09-20 17:45:00,97.43,62.872,40.387,29.816 +2020-09-20 18:00:00,98.66,64.33,44.575,29.816 +2020-09-20 18:15:00,95.42,65.113,44.575,29.816 +2020-09-20 18:30:00,96.3,64.515,44.575,29.816 +2020-09-20 18:45:00,92.61,65.957,44.575,29.816 +2020-09-20 19:00:00,91.79,67.405,45.623999999999995,29.816 +2020-09-20 19:15:00,93.46,65.542,45.623999999999995,29.816 +2020-09-20 19:30:00,95.78,65.34,45.623999999999995,29.816 +2020-09-20 19:45:00,94.24,65.939,45.623999999999995,29.816 +2020-09-20 20:00:00,87.59,65.218,44.583999999999996,29.816 +2020-09-20 20:15:00,90.25,64.521,44.583999999999996,29.816 +2020-09-20 20:30:00,92.85,62.952,44.583999999999996,29.816 +2020-09-20 20:45:00,92.59,61.67100000000001,44.583999999999996,29.816 +2020-09-20 21:00:00,85.14,59.825,39.732,29.816 +2020-09-20 21:15:00,82.41,61.951,39.732,29.816 +2020-09-20 21:30:00,79.04,60.927,39.732,29.816 +2020-09-20 21:45:00,80.63,60.081,39.732,29.816 +2020-09-20 22:00:00,78.14,60.426,38.571,29.816 +2020-09-20 22:15:00,79.55,58.713,38.571,29.816 +2020-09-20 22:30:00,75.33,55.406000000000006,38.571,29.816 +2020-09-20 22:45:00,75.99,52.976000000000006,38.571,29.816 +2020-09-20 23:00:00,74.75,49.496,33.121,29.816 +2020-09-20 23:15:00,77.14,48.525,33.121,29.816 +2020-09-20 23:30:00,75.39,47.949,33.121,29.816 +2020-09-20 23:45:00,71.17,46.997,33.121,29.816 +2020-09-21 00:00:00,63.72,42.257,32.506,29.93 +2020-09-21 00:15:00,71.94,41.973,32.506,29.93 +2020-09-21 00:30:00,71.02,41.553999999999995,32.506,29.93 +2020-09-21 00:45:00,73.89,41.096000000000004,32.506,29.93 +2020-09-21 01:00:00,68.98,41.651,31.121,29.93 +2020-09-21 01:15:00,69.62,41.038000000000004,31.121,29.93 +2020-09-21 01:30:00,73.22,40.213,31.121,29.93 +2020-09-21 01:45:00,73.73,39.638000000000005,31.121,29.93 +2020-09-21 02:00:00,72.86,40.29,29.605999999999998,29.93 +2020-09-21 02:15:00,70.41,39.441,29.605999999999998,29.93 +2020-09-21 02:30:00,75.57,40.343,29.605999999999998,29.93 +2020-09-21 02:45:00,75.89,40.859,29.605999999999998,29.93 +2020-09-21 03:00:00,73.84,43.576,28.124000000000002,29.93 +2020-09-21 03:15:00,72.28,43.278,28.124000000000002,29.93 +2020-09-21 03:30:00,78.86,43.173,28.124000000000002,29.93 +2020-09-21 03:45:00,81.91,44.346000000000004,28.124000000000002,29.93 +2020-09-21 04:00:00,83.6,53.56399999999999,29.743000000000002,29.93 +2020-09-21 04:15:00,81.79,61.896,29.743000000000002,29.93 +2020-09-21 04:30:00,85.13,60.549,29.743000000000002,29.93 +2020-09-21 04:45:00,90.52,61.034,29.743000000000002,29.93 +2020-09-21 05:00:00,99.97,80.40899999999999,36.191,29.93 +2020-09-21 05:15:00,106.86,97.719,36.191,29.93 +2020-09-21 05:30:00,113.92,91.788,36.191,29.93 +2020-09-21 05:45:00,121.46,86.161,36.191,29.93 +2020-09-21 06:00:00,123.22,85.652,55.277,29.93 +2020-09-21 06:15:00,121.57,87.79899999999999,55.277,29.93 +2020-09-21 06:30:00,121.97,86.494,55.277,29.93 +2020-09-21 06:45:00,124.18,88.302,55.277,29.93 +2020-09-21 07:00:00,126.05,87.74700000000001,65.697,29.93 +2020-09-21 07:15:00,123.56,89.73700000000001,65.697,29.93 +2020-09-21 07:30:00,119.15,88.95200000000001,65.697,29.93 +2020-09-21 07:45:00,121.25,90.06200000000001,65.697,29.93 +2020-09-21 08:00:00,122.84,86.54299999999999,57.028,29.93 +2020-09-21 08:15:00,123.58,88.176,57.028,29.93 +2020-09-21 08:30:00,124.68,86.37299999999999,57.028,29.93 +2020-09-21 08:45:00,123.86,87.43,57.028,29.93 +2020-09-21 09:00:00,122.73,81.84100000000001,52.633,29.93 +2020-09-21 09:15:00,122.01,79.57300000000001,52.633,29.93 +2020-09-21 09:30:00,121.3,79.82,52.633,29.93 +2020-09-21 09:45:00,123.09,79.15100000000001,52.633,29.93 +2020-09-21 10:00:00,127.26,77.78,50.647,29.93 +2020-09-21 10:15:00,126.28,78.107,50.647,29.93 +2020-09-21 10:30:00,120.76,77.462,50.647,29.93 +2020-09-21 10:45:00,121.89,76.899,50.647,29.93 +2020-09-21 11:00:00,118.55,74.681,50.245,29.93 +2020-09-21 11:15:00,120.27,75.36399999999999,50.245,29.93 +2020-09-21 11:30:00,121.52,76.82600000000001,50.245,29.93 +2020-09-21 11:45:00,118.95,76.563,50.245,29.93 +2020-09-21 12:00:00,109.74,74.55,46.956,29.93 +2020-09-21 12:15:00,113.88,74.17699999999999,46.956,29.93 +2020-09-21 12:30:00,105.64,72.477,46.956,29.93 +2020-09-21 12:45:00,114.96,72.624,46.956,29.93 +2020-09-21 13:00:00,114.37,72.619,47.383,29.93 +2020-09-21 13:15:00,107.54,71.649,47.383,29.93 +2020-09-21 13:30:00,102.4,70.263,47.383,29.93 +2020-09-21 13:45:00,107.14,70.24600000000001,47.383,29.93 +2020-09-21 14:00:00,122.16,70.2,47.1,29.93 +2020-09-21 14:15:00,123.25,70.017,47.1,29.93 +2020-09-21 14:30:00,111.73,68.586,47.1,29.93 +2020-09-21 14:45:00,113.21,69.455,47.1,29.93 +2020-09-21 15:00:00,114.42,69.688,49.355,29.93 +2020-09-21 15:15:00,113.68,67.744,49.355,29.93 +2020-09-21 15:30:00,115.25,67.443,49.355,29.93 +2020-09-21 15:45:00,112.83,66.139,49.355,29.93 +2020-09-21 16:00:00,116.9,67.553,52.14,29.93 +2020-09-21 16:15:00,113.27,66.914,52.14,29.93 +2020-09-21 16:30:00,113.73,67.6,52.14,29.93 +2020-09-21 16:45:00,116.11,64.986,52.14,29.93 +2020-09-21 17:00:00,119.45,65.829,58.705,29.93 +2020-09-21 17:15:00,118.12,66.80199999999999,58.705,29.93 +2020-09-21 17:30:00,117.64,66.57,58.705,29.93 +2020-09-21 17:45:00,115.33,66.952,58.705,29.93 +2020-09-21 18:00:00,117.55,67.64,59.153,29.93 +2020-09-21 18:15:00,117.76,66.494,59.153,29.93 +2020-09-21 18:30:00,118.86,65.55,59.153,29.93 +2020-09-21 18:45:00,118.16,69.444,59.153,29.93 +2020-09-21 19:00:00,113.92,70.17,61.483000000000004,29.93 +2020-09-21 19:15:00,109.99,68.964,61.483000000000004,29.93 +2020-09-21 19:30:00,107.98,68.619,61.483000000000004,29.93 +2020-09-21 19:45:00,105.49,68.542,61.483000000000004,29.93 +2020-09-21 20:00:00,105.98,66.316,67.55,29.93 +2020-09-21 20:15:00,107.86,65.946,67.55,29.93 +2020-09-21 20:30:00,105.11,64.217,67.55,29.93 +2020-09-21 20:45:00,95.78,63.5,67.55,29.93 +2020-09-21 21:00:00,94.14,61.354,60.026,29.93 +2020-09-21 21:15:00,93.33,63.472,60.026,29.93 +2020-09-21 21:30:00,94.41,62.532,60.026,29.93 +2020-09-21 21:45:00,94.3,61.356,60.026,29.93 +2020-09-21 22:00:00,89.01,59.428000000000004,52.736999999999995,29.93 +2020-09-21 22:15:00,84.04,58.778999999999996,52.736999999999995,29.93 +2020-09-21 22:30:00,83.53,49.756,52.736999999999995,29.93 +2020-09-21 22:45:00,85.82,46.015,52.736999999999995,29.93 +2020-09-21 23:00:00,83.32,42.842,44.408,29.93 +2020-09-21 23:15:00,76.73,41.349,44.408,29.93 +2020-09-21 23:30:00,74.23,41.486000000000004,44.408,29.93 +2020-09-21 23:45:00,81.41,40.847,44.408,29.93 +2020-09-22 00:00:00,79.25,40.692,44.438,29.93 +2020-09-22 00:15:00,79.02,41.415,44.438,29.93 +2020-09-22 00:30:00,71.81,41.271,44.438,29.93 +2020-09-22 00:45:00,70.35,41.129,44.438,29.93 +2020-09-22 01:00:00,78.04,41.229,41.468999999999994,29.93 +2020-09-22 01:15:00,79.59,40.599000000000004,41.468999999999994,29.93 +2020-09-22 01:30:00,77.85,39.745,41.468999999999994,29.93 +2020-09-22 01:45:00,74.14,38.878,41.468999999999994,29.93 +2020-09-22 02:00:00,76.1,39.185,39.708,29.93 +2020-09-22 02:15:00,78.9,39.167,39.708,29.93 +2020-09-22 02:30:00,78.9,39.602,39.708,29.93 +2020-09-22 02:45:00,74.95,40.354,39.708,29.93 +2020-09-22 03:00:00,72.1,42.326,38.919000000000004,29.93 +2020-09-22 03:15:00,77.33,42.497,38.919000000000004,29.93 +2020-09-22 03:30:00,75.89,42.504,38.919000000000004,29.93 +2020-09-22 03:45:00,80.08,42.905,38.919000000000004,29.93 +2020-09-22 04:00:00,83.28,51.184,40.092,29.93 +2020-09-22 04:15:00,83.18,59.461999999999996,40.092,29.93 +2020-09-22 04:30:00,88.24,57.948,40.092,29.93 +2020-09-22 04:45:00,92.26,59.123999999999995,40.092,29.93 +2020-09-22 05:00:00,101.36,81.043,43.713,29.93 +2020-09-22 05:15:00,112.59,98.867,43.713,29.93 +2020-09-22 05:30:00,117.29,92.676,43.713,29.93 +2020-09-22 05:45:00,118.66,86.45299999999999,43.713,29.93 +2020-09-22 06:00:00,117.1,86.402,56.033,29.93 +2020-09-22 06:15:00,118.12,89.12799999999999,56.033,29.93 +2020-09-22 06:30:00,117.39,87.425,56.033,29.93 +2020-09-22 06:45:00,118.72,88.47200000000001,56.033,29.93 +2020-09-22 07:00:00,120.28,87.978,66.003,29.93 +2020-09-22 07:15:00,118.56,89.75299999999999,66.003,29.93 +2020-09-22 07:30:00,117.78,88.869,66.003,29.93 +2020-09-22 07:45:00,117.36,89.32600000000001,66.003,29.93 +2020-09-22 08:00:00,115.63,85.794,57.474,29.93 +2020-09-22 08:15:00,115.97,86.789,57.474,29.93 +2020-09-22 08:30:00,116.15,85.074,57.474,29.93 +2020-09-22 08:45:00,116.14,85.37,57.474,29.93 +2020-09-22 09:00:00,113.46,79.796,51.928000000000004,29.93 +2020-09-22 09:15:00,114.86,77.87100000000001,51.928000000000004,29.93 +2020-09-22 09:30:00,116.65,78.78399999999999,51.928000000000004,29.93 +2020-09-22 09:45:00,117.21,78.98100000000001,51.928000000000004,29.93 +2020-09-22 10:00:00,116.31,76.461,49.46,29.93 +2020-09-22 10:15:00,112.61,76.334,49.46,29.93 +2020-09-22 10:30:00,112.62,75.747,49.46,29.93 +2020-09-22 10:45:00,113.46,75.955,49.46,29.93 +2020-09-22 11:00:00,111.98,74.19800000000001,48.206,29.93 +2020-09-22 11:15:00,107.81,75.045,48.206,29.93 +2020-09-22 11:30:00,113.38,75.354,48.206,29.93 +2020-09-22 11:45:00,106.2,74.97399999999999,48.206,29.93 +2020-09-22 12:00:00,105.03,72.382,46.285,29.93 +2020-09-22 12:15:00,104.5,72.095,46.285,29.93 +2020-09-22 12:30:00,103.2,71.24,46.285,29.93 +2020-09-22 12:45:00,99.21,71.758,46.285,29.93 +2020-09-22 13:00:00,102.59,71.366,46.861999999999995,29.93 +2020-09-22 13:15:00,108.02,71.529,46.861999999999995,29.93 +2020-09-22 13:30:00,103.9,70.479,46.861999999999995,29.93 +2020-09-22 13:45:00,102.03,69.86,46.861999999999995,29.93 +2020-09-22 14:00:00,106.58,70.15899999999999,46.488,29.93 +2020-09-22 14:15:00,105.28,69.903,46.488,29.93 +2020-09-22 14:30:00,101.09,68.925,46.488,29.93 +2020-09-22 14:45:00,102.61,69.22399999999999,46.488,29.93 +2020-09-22 15:00:00,105.33,69.167,48.442,29.93 +2020-09-22 15:15:00,104.23,67.98,48.442,29.93 +2020-09-22 15:30:00,103.53,67.63600000000001,48.442,29.93 +2020-09-22 15:45:00,105.75,66.461,48.442,29.93 +2020-09-22 16:00:00,108.11,67.479,50.397,29.93 +2020-09-22 16:15:00,108.64,67.058,50.397,29.93 +2020-09-22 16:30:00,110.45,67.669,50.397,29.93 +2020-09-22 16:45:00,113.26,65.699,50.397,29.93 +2020-09-22 17:00:00,114.67,66.813,56.668,29.93 +2020-09-22 17:15:00,114.65,68.11,56.668,29.93 +2020-09-22 17:30:00,116.47,67.773,56.668,29.93 +2020-09-22 17:45:00,117.39,67.911,56.668,29.93 +2020-09-22 18:00:00,121.5,67.854,57.957,29.93 +2020-09-22 18:15:00,119.7,67.63600000000001,57.957,29.93 +2020-09-22 18:30:00,121.25,66.432,57.957,29.93 +2020-09-22 18:45:00,119.41,70.4,57.957,29.93 +2020-09-22 19:00:00,117.27,70.289,57.056000000000004,29.93 +2020-09-22 19:15:00,114.03,69.138,57.056000000000004,29.93 +2020-09-22 19:30:00,113.68,68.457,57.056000000000004,29.93 +2020-09-22 19:45:00,108.16,68.626,57.056000000000004,29.93 +2020-09-22 20:00:00,106.04,66.758,64.156,29.93 +2020-09-22 20:15:00,110.06,65.166,64.156,29.93 +2020-09-22 20:30:00,108.07,63.717,64.156,29.93 +2020-09-22 20:45:00,100.9,63.108999999999995,64.156,29.93 +2020-09-22 21:00:00,93.55,61.407,56.507,29.93 +2020-09-22 21:15:00,93.78,62.69,56.507,29.93 +2020-09-22 21:30:00,90.09,61.68899999999999,56.507,29.93 +2020-09-22 21:45:00,92.81,60.684,56.507,29.93 +2020-09-22 22:00:00,88.4,59.316,50.728,29.93 +2020-09-22 22:15:00,90.76,58.328,50.728,29.93 +2020-09-22 22:30:00,83.74,49.538000000000004,50.728,29.93 +2020-09-22 22:45:00,79.35,45.873000000000005,50.728,29.93 +2020-09-22 23:00:00,73.73,42.176,43.556999999999995,29.93 +2020-09-22 23:15:00,74.31,41.556999999999995,43.556999999999995,29.93 +2020-09-22 23:30:00,72.56,41.6,43.556999999999995,29.93 +2020-09-22 23:45:00,73.7,40.976000000000006,43.556999999999995,29.93 +2020-09-23 00:00:00,74.14,40.966,41.151,29.93 +2020-09-23 00:15:00,80.43,41.68600000000001,41.151,29.93 +2020-09-23 00:30:00,80.03,41.549,41.151,29.93 +2020-09-23 00:45:00,79.17,41.41,41.151,29.93 +2020-09-23 01:00:00,72.52,41.504,37.763000000000005,29.93 +2020-09-23 01:15:00,76.54,40.896,37.763000000000005,29.93 +2020-09-23 01:30:00,79.93,40.059,37.763000000000005,29.93 +2020-09-23 01:45:00,81.24,39.193000000000005,37.763000000000005,29.93 +2020-09-23 02:00:00,78.02,39.505,35.615,29.93 +2020-09-23 02:15:00,77.07,39.507,35.615,29.93 +2020-09-23 02:30:00,81.33,39.919000000000004,35.615,29.93 +2020-09-23 02:45:00,81.34,40.667,35.615,29.93 +2020-09-23 03:00:00,77.9,42.626000000000005,35.153,29.93 +2020-09-23 03:15:00,76.25,42.815,35.153,29.93 +2020-09-23 03:30:00,84.12,42.827,35.153,29.93 +2020-09-23 03:45:00,87.89,43.21,35.153,29.93 +2020-09-23 04:00:00,91.84,51.525,36.203,29.93 +2020-09-23 04:15:00,87.47,59.836999999999996,36.203,29.93 +2020-09-23 04:30:00,95.22,58.328,36.203,29.93 +2020-09-23 04:45:00,101.67,59.511,36.203,29.93 +2020-09-23 05:00:00,111.18,81.527,39.922,29.93 +2020-09-23 05:15:00,112.35,99.454,39.922,29.93 +2020-09-23 05:30:00,111.69,93.244,39.922,29.93 +2020-09-23 05:45:00,116.38,86.97,39.922,29.93 +2020-09-23 06:00:00,119.88,86.88600000000001,56.443999999999996,29.93 +2020-09-23 06:15:00,119.76,89.635,56.443999999999996,29.93 +2020-09-23 06:30:00,120.0,87.935,56.443999999999996,29.93 +2020-09-23 06:45:00,121.7,88.979,56.443999999999996,29.93 +2020-09-23 07:00:00,124.88,88.484,68.683,29.93 +2020-09-23 07:15:00,120.09,90.274,68.683,29.93 +2020-09-23 07:30:00,121.21,89.425,68.683,29.93 +2020-09-23 07:45:00,123.12,89.883,68.683,29.93 +2020-09-23 08:00:00,116.84,86.35799999999999,59.003,29.93 +2020-09-23 08:15:00,115.12,87.31700000000001,59.003,29.93 +2020-09-23 08:30:00,115.48,85.62,59.003,29.93 +2020-09-23 08:45:00,115.51,85.897,59.003,29.93 +2020-09-23 09:00:00,114.02,80.328,56.21,29.93 +2020-09-23 09:15:00,114.6,78.39399999999999,56.21,29.93 +2020-09-23 09:30:00,110.5,79.286,56.21,29.93 +2020-09-23 09:45:00,108.97,79.453,56.21,29.93 +2020-09-23 10:00:00,108.11,76.928,52.358999999999995,29.93 +2020-09-23 10:15:00,109.82,76.764,52.358999999999995,29.93 +2020-09-23 10:30:00,110.15,76.161,52.358999999999995,29.93 +2020-09-23 10:45:00,105.93,76.354,52.358999999999995,29.93 +2020-09-23 11:00:00,105.59,74.609,51.161,29.93 +2020-09-23 11:15:00,106.75,75.438,51.161,29.93 +2020-09-23 11:30:00,104.38,75.747,51.161,29.93 +2020-09-23 11:45:00,103.61,75.348,51.161,29.93 +2020-09-23 12:00:00,100.04,72.733,49.119,29.93 +2020-09-23 12:15:00,106.82,72.434,49.119,29.93 +2020-09-23 12:30:00,109.78,71.612,49.119,29.93 +2020-09-23 12:45:00,102.31,72.122,49.119,29.93 +2020-09-23 13:00:00,103.09,71.70100000000001,49.187,29.93 +2020-09-23 13:15:00,103.29,71.862,49.187,29.93 +2020-09-23 13:30:00,102.62,70.81,49.187,29.93 +2020-09-23 13:45:00,105.34,70.196,49.187,29.93 +2020-09-23 14:00:00,107.99,70.447,49.787,29.93 +2020-09-23 14:15:00,105.73,70.204,49.787,29.93 +2020-09-23 14:30:00,107.7,69.259,49.787,29.93 +2020-09-23 14:45:00,109.29,69.555,49.787,29.93 +2020-09-23 15:00:00,108.43,69.45100000000001,51.458999999999996,29.93 +2020-09-23 15:15:00,104.72,68.285,51.458999999999996,29.93 +2020-09-23 15:30:00,107.19,67.971,51.458999999999996,29.93 +2020-09-23 15:45:00,107.85,66.812,51.458999999999996,29.93 +2020-09-23 16:00:00,109.45,67.789,53.663000000000004,29.93 +2020-09-23 16:15:00,108.62,67.385,53.663000000000004,29.93 +2020-09-23 16:30:00,110.5,67.992,53.663000000000004,29.93 +2020-09-23 16:45:00,113.22,66.078,53.663000000000004,29.93 +2020-09-23 17:00:00,116.47,67.153,58.183,29.93 +2020-09-23 17:15:00,114.39,68.475,58.183,29.93 +2020-09-23 17:30:00,116.38,68.142,58.183,29.93 +2020-09-23 17:45:00,117.99,68.31,58.183,29.93 +2020-09-23 18:00:00,119.46,68.234,60.141000000000005,29.93 +2020-09-23 18:15:00,119.27,68.014,60.141000000000005,29.93 +2020-09-23 18:30:00,119.58,66.82,60.141000000000005,29.93 +2020-09-23 18:45:00,119.51,70.78399999999999,60.141000000000005,29.93 +2020-09-23 19:00:00,115.57,70.683,60.582,29.93 +2020-09-23 19:15:00,111.65,69.531,60.582,29.93 +2020-09-23 19:30:00,110.06,68.845,60.582,29.93 +2020-09-23 19:45:00,107.79,69.005,60.582,29.93 +2020-09-23 20:00:00,103.36,67.161,66.61,29.93 +2020-09-23 20:15:00,99.89,65.566,66.61,29.93 +2020-09-23 20:30:00,99.32,64.09,66.61,29.93 +2020-09-23 20:45:00,101.02,63.443000000000005,66.61,29.93 +2020-09-23 21:00:00,98.0,61.744,57.658,29.93 +2020-09-23 21:15:00,99.6,63.016999999999996,57.658,29.93 +2020-09-23 21:30:00,86.71,62.025,57.658,29.93 +2020-09-23 21:45:00,90.43,60.985,57.658,29.93 +2020-09-23 22:00:00,84.37,59.597,51.81,29.93 +2020-09-23 22:15:00,91.09,58.586000000000006,51.81,29.93 +2020-09-23 22:30:00,88.12,49.79,51.81,29.93 +2020-09-23 22:45:00,83.92,46.13,51.81,29.93 +2020-09-23 23:00:00,76.12,42.474,42.93600000000001,29.93 +2020-09-23 23:15:00,75.88,41.823,42.93600000000001,29.93 +2020-09-23 23:30:00,79.32,41.87,42.93600000000001,29.93 +2020-09-23 23:45:00,82.59,41.246,42.93600000000001,29.93 +2020-09-24 00:00:00,79.44,41.243,39.211,29.93 +2020-09-24 00:15:00,76.44,41.958999999999996,39.211,29.93 +2020-09-24 00:30:00,75.48,41.83,39.211,29.93 +2020-09-24 00:45:00,79.62,41.693999999999996,39.211,29.93 +2020-09-24 01:00:00,76.77,41.78,37.607,29.93 +2020-09-24 01:15:00,80.51,41.193000000000005,37.607,29.93 +2020-09-24 01:30:00,72.87,40.375,37.607,29.93 +2020-09-24 01:45:00,79.28,39.509,37.607,29.93 +2020-09-24 02:00:00,78.34,39.827,36.44,29.93 +2020-09-24 02:15:00,78.78,39.849000000000004,36.44,29.93 +2020-09-24 02:30:00,74.7,40.239000000000004,36.44,29.93 +2020-09-24 02:45:00,82.77,40.981,36.44,29.93 +2020-09-24 03:00:00,81.53,42.928000000000004,36.116,29.93 +2020-09-24 03:15:00,79.01,43.135,36.116,29.93 +2020-09-24 03:30:00,78.21,43.151,36.116,29.93 +2020-09-24 03:45:00,82.11,43.516999999999996,36.116,29.93 +2020-09-24 04:00:00,87.36,51.869,37.398,29.93 +2020-09-24 04:15:00,93.6,60.213,37.398,29.93 +2020-09-24 04:30:00,97.83,58.708999999999996,37.398,29.93 +2020-09-24 04:45:00,99.6,59.9,37.398,29.93 +2020-09-24 05:00:00,109.7,82.016,41.776,29.93 +2020-09-24 05:15:00,108.24,100.046,41.776,29.93 +2020-09-24 05:30:00,110.1,93.816,41.776,29.93 +2020-09-24 05:45:00,112.5,87.49,41.776,29.93 +2020-09-24 06:00:00,118.92,87.375,55.61,29.93 +2020-09-24 06:15:00,120.61,90.146,55.61,29.93 +2020-09-24 06:30:00,123.08,88.449,55.61,29.93 +2020-09-24 06:45:00,124.17,89.49,55.61,29.93 +2020-09-24 07:00:00,126.37,88.994,67.13600000000001,29.93 +2020-09-24 07:15:00,125.34,90.79899999999999,67.13600000000001,29.93 +2020-09-24 07:30:00,127.26,89.985,67.13600000000001,29.93 +2020-09-24 07:45:00,127.08,90.443,67.13600000000001,29.93 +2020-09-24 08:00:00,125.35,86.925,57.55,29.93 +2020-09-24 08:15:00,126.55,87.84899999999999,57.55,29.93 +2020-09-24 08:30:00,129.1,86.16799999999999,57.55,29.93 +2020-09-24 08:45:00,130.06,86.425,57.55,29.93 +2020-09-24 09:00:00,128.57,80.861,52.931999999999995,29.93 +2020-09-24 09:15:00,126.84,78.918,52.931999999999995,29.93 +2020-09-24 09:30:00,123.67,79.79,52.931999999999995,29.93 +2020-09-24 09:45:00,127.27,79.92699999999999,52.931999999999995,29.93 +2020-09-24 10:00:00,125.65,77.398,50.36600000000001,29.93 +2020-09-24 10:15:00,124.59,77.194,50.36600000000001,29.93 +2020-09-24 10:30:00,120.94,76.577,50.36600000000001,29.93 +2020-09-24 10:45:00,116.89,76.75399999999999,50.36600000000001,29.93 +2020-09-24 11:00:00,110.52,75.02,47.893,29.93 +2020-09-24 11:15:00,109.17,75.833,47.893,29.93 +2020-09-24 11:30:00,110.97,76.141,47.893,29.93 +2020-09-24 11:45:00,106.21,75.725,47.893,29.93 +2020-09-24 12:00:00,102.53,73.084,45.271,29.93 +2020-09-24 12:15:00,102.03,72.77199999999999,45.271,29.93 +2020-09-24 12:30:00,99.75,71.986,45.271,29.93 +2020-09-24 12:45:00,99.48,72.488,45.271,29.93 +2020-09-24 13:00:00,98.27,72.03699999999999,44.351000000000006,29.93 +2020-09-24 13:15:00,99.53,72.196,44.351000000000006,29.93 +2020-09-24 13:30:00,100.61,71.143,44.351000000000006,29.93 +2020-09-24 13:45:00,102.58,70.532,44.351000000000006,29.93 +2020-09-24 14:00:00,103.51,70.735,44.99,29.93 +2020-09-24 14:15:00,103.08,70.508,44.99,29.93 +2020-09-24 14:30:00,101.16,69.596,44.99,29.93 +2020-09-24 14:45:00,101.81,69.888,44.99,29.93 +2020-09-24 15:00:00,101.94,69.737,46.869,29.93 +2020-09-24 15:15:00,103.21,68.59,46.869,29.93 +2020-09-24 15:30:00,103.97,68.308,46.869,29.93 +2020-09-24 15:45:00,104.84,67.164,46.869,29.93 +2020-09-24 16:00:00,108.09,68.101,48.902,29.93 +2020-09-24 16:15:00,109.23,67.714,48.902,29.93 +2020-09-24 16:30:00,110.09,68.317,48.902,29.93 +2020-09-24 16:45:00,113.09,66.46,48.902,29.93 +2020-09-24 17:00:00,116.14,67.495,53.244,29.93 +2020-09-24 17:15:00,116.08,68.84100000000001,53.244,29.93 +2020-09-24 17:30:00,117.28,68.513,53.244,29.93 +2020-09-24 17:45:00,116.1,68.711,53.244,29.93 +2020-09-24 18:00:00,120.4,68.615,54.343999999999994,29.93 +2020-09-24 18:15:00,120.5,68.393,54.343999999999994,29.93 +2020-09-24 18:30:00,123.4,67.212,54.343999999999994,29.93 +2020-09-24 18:45:00,118.83,71.17,54.343999999999994,29.93 +2020-09-24 19:00:00,115.06,71.08,54.332,29.93 +2020-09-24 19:15:00,112.24,69.925,54.332,29.93 +2020-09-24 19:30:00,114.88,69.235,54.332,29.93 +2020-09-24 19:45:00,110.88,69.387,54.332,29.93 +2020-09-24 20:00:00,107.62,67.565,58.06,29.93 +2020-09-24 20:15:00,108.78,65.968,58.06,29.93 +2020-09-24 20:30:00,104.84,64.46600000000001,58.06,29.93 +2020-09-24 20:45:00,96.86,63.781000000000006,58.06,29.93 +2020-09-24 21:00:00,94.23,62.083999999999996,52.411,29.93 +2020-09-24 21:15:00,89.92,63.346000000000004,52.411,29.93 +2020-09-24 21:30:00,87.79,62.361000000000004,52.411,29.93 +2020-09-24 21:45:00,89.98,61.287,52.411,29.93 +2020-09-24 22:00:00,88.04,59.88,47.148999999999994,29.93 +2020-09-24 22:15:00,89.31,58.845,47.148999999999994,29.93 +2020-09-24 22:30:00,80.71,50.044,47.148999999999994,29.93 +2020-09-24 22:45:00,79.84,46.388000000000005,47.148999999999994,29.93 +2020-09-24 23:00:00,71.75,42.773,40.814,29.93 +2020-09-24 23:15:00,77.77,42.091,40.814,29.93 +2020-09-24 23:30:00,81.26,42.141000000000005,40.814,29.93 +2020-09-24 23:45:00,80.31,41.516999999999996,40.814,29.93 +2020-09-25 00:00:00,74.0,39.914,39.153,29.93 +2020-09-25 00:15:00,72.28,40.838,39.153,29.93 +2020-09-25 00:30:00,77.46,40.879,39.153,29.93 +2020-09-25 00:45:00,78.16,41.085,39.153,29.93 +2020-09-25 01:00:00,76.04,40.797,37.228,29.93 +2020-09-25 01:15:00,73.4,40.051,37.228,29.93 +2020-09-25 01:30:00,76.94,39.709,37.228,29.93 +2020-09-25 01:45:00,79.03,38.69,37.228,29.93 +2020-09-25 02:00:00,75.49,39.719,35.851,29.93 +2020-09-25 02:15:00,75.88,39.689,35.851,29.93 +2020-09-25 02:30:00,78.09,40.806,35.851,29.93 +2020-09-25 02:45:00,74.34,41.058,35.851,29.93 +2020-09-25 03:00:00,73.16,43.275,36.54,29.93 +2020-09-25 03:15:00,77.62,42.89,36.54,29.93 +2020-09-25 03:30:00,83.89,42.731,36.54,29.93 +2020-09-25 03:45:00,88.6,43.809,36.54,29.93 +2020-09-25 04:00:00,88.15,52.363,37.578,29.93 +2020-09-25 04:15:00,90.7,59.54,37.578,29.93 +2020-09-25 04:30:00,93.93,58.795,37.578,29.93 +2020-09-25 04:45:00,97.88,59.202,37.578,29.93 +2020-09-25 05:00:00,100.27,80.708,40.387,29.93 +2020-09-25 05:15:00,109.18,99.95299999999999,40.387,29.93 +2020-09-25 05:30:00,109.6,94.13,40.387,29.93 +2020-09-25 05:45:00,110.57,87.402,40.387,29.93 +2020-09-25 06:00:00,115.32,87.54799999999999,54.668,29.93 +2020-09-25 06:15:00,115.66,90.071,54.668,29.93 +2020-09-25 06:30:00,118.26,88.10700000000001,54.668,29.93 +2020-09-25 06:45:00,117.52,89.484,54.668,29.93 +2020-09-25 07:00:00,120.63,89.242,63.971000000000004,29.93 +2020-09-25 07:15:00,121.05,92.009,63.971000000000004,29.93 +2020-09-25 07:30:00,117.32,89.76299999999999,63.971000000000004,29.93 +2020-09-25 07:45:00,117.02,89.816,63.971000000000004,29.93 +2020-09-25 08:00:00,113.23,86.553,56.042,29.93 +2020-09-25 08:15:00,112.83,87.81,56.042,29.93 +2020-09-25 08:30:00,112.79,86.36,56.042,29.93 +2020-09-25 08:45:00,114.62,86.03399999999999,56.042,29.93 +2020-09-25 09:00:00,110.41,78.957,52.832,29.93 +2020-09-25 09:15:00,113.15,78.538,52.832,29.93 +2020-09-25 09:30:00,115.51,78.775,52.832,29.93 +2020-09-25 09:45:00,121.32,79.123,52.832,29.93 +2020-09-25 10:00:00,122.11,76.09899999999999,50.044,29.93 +2020-09-25 10:15:00,119.95,75.921,50.044,29.93 +2020-09-25 10:30:00,119.89,75.65,50.044,29.93 +2020-09-25 10:45:00,123.44,75.60600000000001,50.044,29.93 +2020-09-25 11:00:00,126.14,74.045,49.06100000000001,29.93 +2020-09-25 11:15:00,126.84,73.806,49.06100000000001,29.93 +2020-09-25 11:30:00,127.73,74.418,49.06100000000001,29.93 +2020-09-25 11:45:00,125.85,73.337,49.06100000000001,29.93 +2020-09-25 12:00:00,124.09,71.339,45.595,29.93 +2020-09-25 12:15:00,125.3,69.887,45.595,29.93 +2020-09-25 12:30:00,123.99,69.266,45.595,29.93 +2020-09-25 12:45:00,122.75,69.4,45.595,29.93 +2020-09-25 13:00:00,119.15,69.617,43.218,29.93 +2020-09-25 13:15:00,119.21,70.164,43.218,29.93 +2020-09-25 13:30:00,117.01,69.656,43.218,29.93 +2020-09-25 13:45:00,117.78,69.24,43.218,29.93 +2020-09-25 14:00:00,116.77,68.476,41.926,29.93 +2020-09-25 14:15:00,115.25,68.501,41.926,29.93 +2020-09-25 14:30:00,113.74,68.818,41.926,29.93 +2020-09-25 14:45:00,110.79,68.735,41.926,29.93 +2020-09-25 15:00:00,110.83,68.325,43.79,29.93 +2020-09-25 15:15:00,108.35,66.892,43.79,29.93 +2020-09-25 15:30:00,109.43,65.762,43.79,29.93 +2020-09-25 15:45:00,113.35,65.20100000000001,43.79,29.93 +2020-09-25 16:00:00,112.61,65.172,45.895,29.93 +2020-09-25 16:15:00,111.78,65.234,45.895,29.93 +2020-09-25 16:30:00,114.66,65.737,45.895,29.93 +2020-09-25 16:45:00,114.16,63.353,45.895,29.93 +2020-09-25 17:00:00,116.51,65.618,51.36,29.93 +2020-09-25 17:15:00,117.46,66.74,51.36,29.93 +2020-09-25 17:30:00,116.53,66.434,51.36,29.93 +2020-09-25 17:45:00,119.69,66.47399999999999,51.36,29.93 +2020-09-25 18:00:00,124.62,66.657,52.985,29.93 +2020-09-25 18:15:00,119.12,65.642,52.985,29.93 +2020-09-25 18:30:00,119.12,64.514,52.985,29.93 +2020-09-25 18:45:00,114.05,68.764,52.985,29.93 +2020-09-25 19:00:00,110.75,69.617,52.602,29.93 +2020-09-25 19:15:00,109.43,69.267,52.602,29.93 +2020-09-25 19:30:00,107.76,68.505,52.602,29.93 +2020-09-25 19:45:00,104.87,67.777,52.602,29.93 +2020-09-25 20:00:00,100.97,65.862,58.063,29.93 +2020-09-25 20:15:00,102.74,64.84,58.063,29.93 +2020-09-25 20:30:00,100.5,62.946999999999996,58.063,29.93 +2020-09-25 20:45:00,93.97,61.794,58.063,29.93 +2020-09-25 21:00:00,88.38,61.198,50.135,29.93 +2020-09-25 21:15:00,87.81,63.786,50.135,29.93 +2020-09-25 21:30:00,85.24,62.715,50.135,29.93 +2020-09-25 21:45:00,86.18,61.906000000000006,50.135,29.93 +2020-09-25 22:00:00,83.96,60.652,45.165,29.93 +2020-09-25 22:15:00,79.17,59.376000000000005,45.165,29.93 +2020-09-25 22:30:00,75.1,55.809,45.165,29.93 +2020-09-25 22:45:00,77.09,53.768,45.165,29.93 +2020-09-25 23:00:00,79.16,51.29600000000001,39.121,29.93 +2020-09-25 23:15:00,75.29,48.898,39.121,29.93 +2020-09-25 23:30:00,70.25,47.217,39.121,29.93 +2020-09-25 23:45:00,67.44,46.32,39.121,29.93 +2020-09-26 00:00:00,64.11,40.303000000000004,38.49,29.816 +2020-09-26 00:15:00,64.22,39.549,38.49,29.816 +2020-09-26 00:30:00,71.12,39.624,38.49,29.816 +2020-09-26 00:45:00,71.01,39.498000000000005,38.49,29.816 +2020-09-26 01:00:00,69.95,39.577,34.5,29.816 +2020-09-26 01:15:00,63.99,39.008,34.5,29.816 +2020-09-26 01:30:00,69.4,37.955,34.5,29.816 +2020-09-26 01:45:00,69.93,37.782,34.5,29.816 +2020-09-26 02:00:00,68.96,38.286,32.236,29.816 +2020-09-26 02:15:00,64.21,37.6,32.236,29.816 +2020-09-26 02:30:00,68.74,37.809,32.236,29.816 +2020-09-26 02:45:00,67.86,38.655,32.236,29.816 +2020-09-26 03:00:00,62.1,40.036,32.067,29.816 +2020-09-26 03:15:00,61.48,38.824,32.067,29.816 +2020-09-26 03:30:00,62.24,38.464,32.067,29.816 +2020-09-26 03:45:00,62.77,40.629,32.067,29.816 +2020-09-26 04:00:00,63.44,46.69,33.071,29.816 +2020-09-26 04:15:00,63.08,52.544,33.071,29.816 +2020-09-26 04:30:00,63.34,49.988,33.071,29.816 +2020-09-26 04:45:00,65.2,50.463,33.071,29.816 +2020-09-26 05:00:00,67.69,62.15,33.014,29.816 +2020-09-26 05:15:00,69.25,68.483,33.014,29.816 +2020-09-26 05:30:00,71.19,63.869,33.014,29.816 +2020-09-26 05:45:00,70.98,61.817,33.014,29.816 +2020-09-26 06:00:00,72.82,74.937,34.628,29.816 +2020-09-26 06:15:00,73.93,87.507,34.628,29.816 +2020-09-26 06:30:00,75.0,82.045,34.628,29.816 +2020-09-26 06:45:00,77.15,78.709,34.628,29.816 +2020-09-26 07:00:00,78.72,76.085,38.871,29.816 +2020-09-26 07:15:00,78.7,77.581,38.871,29.816 +2020-09-26 07:30:00,79.43,77.208,38.871,29.816 +2020-09-26 07:45:00,81.14,79.021,38.871,29.816 +2020-09-26 08:00:00,82.04,77.229,43.293,29.816 +2020-09-26 08:15:00,81.29,79.325,43.293,29.816 +2020-09-26 08:30:00,79.31,78.339,43.293,29.816 +2020-09-26 08:45:00,78.64,79.563,43.293,29.816 +2020-09-26 09:00:00,77.73,75.152,44.559,29.816 +2020-09-26 09:15:00,77.3,75.286,44.559,29.816 +2020-09-26 09:30:00,76.01,76.116,44.559,29.816 +2020-09-26 09:45:00,76.69,76.161,44.559,29.816 +2020-09-26 10:00:00,77.09,73.52,42.091,29.816 +2020-09-26 10:15:00,78.02,73.59899999999999,42.091,29.816 +2020-09-26 10:30:00,79.27,73.126,42.091,29.816 +2020-09-26 10:45:00,75.73,73.221,42.091,29.816 +2020-09-26 11:00:00,74.16,71.61399999999999,38.505,29.816 +2020-09-26 11:15:00,73.22,71.741,38.505,29.816 +2020-09-26 11:30:00,74.17,72.20100000000001,38.505,29.816 +2020-09-26 11:45:00,72.12,71.267,38.505,29.816 +2020-09-26 12:00:00,70.46,69.072,35.388000000000005,29.816 +2020-09-26 12:15:00,67.44,68.39,35.388000000000005,29.816 +2020-09-26 12:30:00,71.03,67.819,35.388000000000005,29.816 +2020-09-26 12:45:00,69.48,68.183,35.388000000000005,29.816 +2020-09-26 13:00:00,63.24,67.752,31.355999999999998,29.816 +2020-09-26 13:15:00,64.45,67.275,31.355999999999998,29.816 +2020-09-26 13:30:00,64.55,66.75,31.355999999999998,29.816 +2020-09-26 13:45:00,63.96,65.571,31.355999999999998,29.816 +2020-09-26 14:00:00,64.09,65.168,30.522,29.816 +2020-09-26 14:15:00,67.34,64.189,30.522,29.816 +2020-09-26 14:30:00,65.35,63.668,30.522,29.816 +2020-09-26 14:45:00,66.06,63.978,30.522,29.816 +2020-09-26 15:00:00,67.79,64.027,34.36,29.816 +2020-09-26 15:15:00,66.51,63.351000000000006,34.36,29.816 +2020-09-26 15:30:00,68.16,62.885,34.36,29.816 +2020-09-26 15:45:00,72.59,61.718999999999994,34.36,29.816 +2020-09-26 16:00:00,75.45,62.938,39.507,29.816 +2020-09-26 16:15:00,77.55,62.669,39.507,29.816 +2020-09-26 16:30:00,79.63,63.308,39.507,29.816 +2020-09-26 16:45:00,83.05,61.232,39.507,29.816 +2020-09-26 17:00:00,85.81,62.456,47.151,29.816 +2020-09-26 17:15:00,86.82,62.61,47.151,29.816 +2020-09-26 17:30:00,88.1,62.18899999999999,47.151,29.816 +2020-09-26 17:45:00,90.04,62.478,47.151,29.816 +2020-09-26 18:00:00,95.33,63.623000000000005,50.303999999999995,29.816 +2020-09-26 18:15:00,94.65,64.312,50.303999999999995,29.816 +2020-09-26 18:30:00,97.77,64.545,50.303999999999995,29.816 +2020-09-26 18:45:00,95.72,65.35300000000001,50.303999999999995,29.816 +2020-09-26 19:00:00,90.07,65.441,50.622,29.816 +2020-09-26 19:15:00,86.83,64.219,50.622,29.816 +2020-09-26 19:30:00,85.33,64.218,50.622,29.816 +2020-09-26 19:45:00,84.12,64.718,50.622,29.816 +2020-09-26 20:00:00,79.3,63.951,45.391000000000005,29.816 +2020-09-26 20:15:00,80.25,63.068000000000005,45.391000000000005,29.816 +2020-09-26 20:30:00,78.57,60.422,45.391000000000005,29.816 +2020-09-26 20:45:00,77.53,60.446000000000005,45.391000000000005,29.816 +2020-09-26 21:00:00,73.77,59.438,39.98,29.816 +2020-09-26 21:15:00,75.16,61.888000000000005,39.98,29.816 +2020-09-26 21:30:00,69.54,61.303999999999995,39.98,29.816 +2020-09-26 21:45:00,69.49,59.95399999999999,39.98,29.816 +2020-09-26 22:00:00,66.64,59.036,37.53,29.816 +2020-09-26 22:15:00,67.12,58.604,37.53,29.816 +2020-09-26 22:30:00,63.4,56.483000000000004,37.53,29.816 +2020-09-26 22:45:00,63.85,55.235,37.53,29.816 +2020-09-26 23:00:00,58.44,52.963,30.97,29.816 +2020-09-26 23:15:00,57.4,50.388000000000005,30.97,29.816 +2020-09-26 23:30:00,57.88,50.007,30.97,29.816 +2020-09-26 23:45:00,57.35,48.701,30.97,29.816 +2020-09-27 00:00:00,58.71,41.699,27.24,29.816 +2020-09-27 00:15:00,55.74,40.016999999999996,27.24,29.816 +2020-09-27 00:30:00,54.56,39.891,27.24,29.816 +2020-09-27 00:45:00,54.16,39.876999999999995,27.24,29.816 +2020-09-27 01:00:00,52.05,40.103,25.662,29.816 +2020-09-27 01:15:00,53.71,39.753,25.662,29.816 +2020-09-27 01:30:00,52.61,38.762,25.662,29.816 +2020-09-27 01:45:00,52.16,38.228,25.662,29.816 +2020-09-27 02:00:00,52.19,38.591,25.67,29.816 +2020-09-27 02:15:00,52.28,38.199,25.67,29.816 +2020-09-27 02:30:00,52.04,38.759,25.67,29.816 +2020-09-27 02:45:00,51.99,39.516,25.67,29.816 +2020-09-27 03:00:00,52.66,41.443000000000005,24.258000000000003,29.816 +2020-09-27 03:15:00,52.96,40.293,24.258000000000003,29.816 +2020-09-27 03:30:00,54.26,39.794000000000004,24.258000000000003,29.816 +2020-09-27 03:45:00,54.89,41.345,24.258000000000003,29.816 +2020-09-27 04:00:00,55.49,47.354,25.051,29.816 +2020-09-27 04:15:00,55.87,52.648999999999994,25.051,29.816 +2020-09-27 04:30:00,55.54,51.083999999999996,25.051,29.816 +2020-09-27 04:45:00,56.78,51.303999999999995,25.051,29.816 +2020-09-27 05:00:00,58.11,62.303999999999995,25.145,29.816 +2020-09-27 05:15:00,58.1,67.563,25.145,29.816 +2020-09-27 05:30:00,58.65,62.586000000000006,25.145,29.816 +2020-09-27 05:45:00,58.18,60.34,25.145,29.816 +2020-09-27 06:00:00,59.68,71.635,26.371,29.816 +2020-09-27 06:15:00,60.43,84.40100000000001,26.371,29.816 +2020-09-27 06:30:00,61.96,78.143,26.371,29.816 +2020-09-27 06:45:00,63.71,73.797,26.371,29.816 +2020-09-27 07:00:00,64.31,71.99600000000001,28.756999999999998,29.816 +2020-09-27 07:15:00,65.55,72.084,28.756999999999998,29.816 +2020-09-27 07:30:00,66.0,72.34,28.756999999999998,29.816 +2020-09-27 07:45:00,66.81,73.921,28.756999999999998,29.816 +2020-09-27 08:00:00,66.27,73.15899999999999,32.82,29.816 +2020-09-27 08:15:00,65.26,76.039,32.82,29.816 +2020-09-27 08:30:00,64.16,76.156,32.82,29.816 +2020-09-27 08:45:00,63.97,77.865,32.82,29.816 +2020-09-27 09:00:00,62.41,73.256,35.534,29.816 +2020-09-27 09:15:00,62.07,73.188,35.534,29.816 +2020-09-27 09:30:00,61.44,74.26,35.534,29.816 +2020-09-27 09:45:00,62.57,74.954,35.534,29.816 +2020-09-27 10:00:00,63.81,73.396,35.925,29.816 +2020-09-27 10:15:00,64.87,73.69,35.925,29.816 +2020-09-27 10:30:00,65.54,73.535,35.925,29.816 +2020-09-27 10:45:00,66.79,73.756,35.925,29.816 +2020-09-27 11:00:00,64.87,72.186,37.056,29.816 +2020-09-27 11:15:00,63.25,72.028,37.056,29.816 +2020-09-27 11:30:00,60.2,72.585,37.056,29.816 +2020-09-27 11:45:00,59.31,71.992,37.056,29.816 +2020-09-27 12:00:00,54.52,70.286,33.124,29.816 +2020-09-27 12:15:00,56.11,69.765,33.124,29.816 +2020-09-27 12:30:00,55.91,68.917,33.124,29.816 +2020-09-27 12:45:00,58.63,68.538,33.124,29.816 +2020-09-27 13:00:00,58.5,67.667,29.874000000000002,29.816 +2020-09-27 13:15:00,55.01,67.723,29.874000000000002,29.816 +2020-09-27 13:30:00,55.43,66.368,29.874000000000002,29.816 +2020-09-27 13:45:00,55.98,65.756,29.874000000000002,29.816 +2020-09-27 14:00:00,57.32,66.19800000000001,27.302,29.816 +2020-09-27 14:15:00,57.14,65.899,27.302,29.816 +2020-09-27 14:30:00,57.98,64.95,27.302,29.816 +2020-09-27 14:45:00,60.21,64.396,27.302,29.816 +2020-09-27 15:00:00,61.57,64.02600000000001,27.642,29.816 +2020-09-27 15:15:00,62.18,63.051,27.642,29.816 +2020-09-27 15:30:00,64.05,62.675,27.642,29.816 +2020-09-27 15:45:00,67.12,61.934,27.642,29.816 +2020-09-27 16:00:00,70.42,62.468,31.945999999999998,29.816 +2020-09-27 16:15:00,73.68,62.107,31.945999999999998,29.816 +2020-09-27 16:30:00,76.58,63.519,31.945999999999998,29.816 +2020-09-27 16:45:00,79.25,61.583999999999996,31.945999999999998,29.816 +2020-09-27 17:00:00,82.98,63.023,40.387,29.816 +2020-09-27 17:15:00,82.75,64.161,40.387,29.816 +2020-09-27 17:30:00,87.05,64.39399999999999,40.387,29.816 +2020-09-27 17:45:00,86.61,65.675,40.387,29.816 +2020-09-27 18:00:00,90.18,67.0,44.575,29.816 +2020-09-27 18:15:00,89.39,67.767,44.575,29.816 +2020-09-27 18:30:00,89.29,67.25399999999999,44.575,29.816 +2020-09-27 18:45:00,87.47,68.656,44.575,29.816 +2020-09-27 19:00:00,91.61,70.178,45.623999999999995,29.816 +2020-09-27 19:15:00,93.17,68.29899999999999,45.623999999999995,29.816 +2020-09-27 19:30:00,92.79,68.07,45.623999999999995,29.816 +2020-09-27 19:45:00,86.34,68.607,45.623999999999995,29.816 +2020-09-27 20:00:00,78.55,68.045,44.583999999999996,29.816 +2020-09-27 20:15:00,81.4,67.33,44.583999999999996,29.816 +2020-09-27 20:30:00,83.71,65.579,44.583999999999996,29.816 +2020-09-27 20:45:00,80.58,64.027,44.583999999999996,29.816 +2020-09-27 21:00:00,84.36,62.199,39.732,29.816 +2020-09-27 21:15:00,89.77,64.25,39.732,29.816 +2020-09-27 21:30:00,86.37,63.286,39.732,29.816 +2020-09-27 21:45:00,80.3,62.198,39.732,29.816 +2020-09-27 22:00:00,75.34,62.406000000000006,38.571,29.816 +2020-09-27 22:15:00,77.9,60.526,38.571,29.816 +2020-09-27 22:30:00,78.22,57.183,38.571,29.816 +2020-09-27 22:45:00,80.64,54.788999999999994,38.571,29.816 +2020-09-27 23:00:00,76.15,51.59,33.121,29.816 +2020-09-27 23:15:00,70.67,50.4,33.121,29.816 +2020-09-27 23:30:00,68.9,49.846000000000004,33.121,29.816 +2020-09-27 23:45:00,69.98,48.894,33.121,29.816 +2020-09-28 00:00:00,72.56,44.205,32.506,29.93 +2020-09-28 00:15:00,73.53,43.888999999999996,32.506,29.93 +2020-09-28 00:30:00,72.62,43.526,32.506,29.93 +2020-09-28 00:45:00,68.54,43.083999999999996,32.506,29.93 +2020-09-28 01:00:00,65.59,43.588,31.121,29.93 +2020-09-28 01:15:00,65.31,43.13,31.121,29.93 +2020-09-28 01:30:00,66.33,42.431999999999995,31.121,29.93 +2020-09-28 01:45:00,66.87,41.858999999999995,31.121,29.93 +2020-09-28 02:00:00,64.01,42.553999999999995,29.605999999999998,29.93 +2020-09-28 02:15:00,69.97,41.842,29.605999999999998,29.93 +2020-09-28 02:30:00,74.06,42.589,29.605999999999998,29.93 +2020-09-28 02:45:00,75.8,43.067,29.605999999999998,29.93 +2020-09-28 03:00:00,72.48,45.693999999999996,28.124000000000002,29.93 +2020-09-28 03:15:00,69.89,45.528999999999996,28.124000000000002,29.93 +2020-09-28 03:30:00,74.11,45.453,28.124000000000002,29.93 +2020-09-28 03:45:00,79.39,46.504,28.124000000000002,29.93 +2020-09-28 04:00:00,83.04,55.977,29.743000000000002,29.93 +2020-09-28 04:15:00,81.76,64.546,29.743000000000002,29.93 +2020-09-28 04:30:00,89.53,63.233000000000004,29.743000000000002,29.93 +2020-09-28 04:45:00,96.21,63.772,29.743000000000002,29.93 +2020-09-28 05:00:00,105.6,83.844,36.191,29.93 +2020-09-28 05:15:00,107.36,101.895,36.191,29.93 +2020-09-28 05:30:00,107.75,95.816,36.191,29.93 +2020-09-28 05:45:00,111.23,89.825,36.191,29.93 +2020-09-28 06:00:00,115.68,89.09299999999999,55.277,29.93 +2020-09-28 06:15:00,115.66,91.397,55.277,29.93 +2020-09-28 06:30:00,117.81,90.111,55.277,29.93 +2020-09-28 06:45:00,119.85,91.897,55.277,29.93 +2020-09-28 07:00:00,122.15,91.338,65.697,29.93 +2020-09-28 07:15:00,122.14,93.427,65.697,29.93 +2020-09-28 07:30:00,121.34,92.88600000000001,65.697,29.93 +2020-09-28 07:45:00,116.32,93.995,65.697,29.93 +2020-09-28 08:00:00,117.15,90.522,57.028,29.93 +2020-09-28 08:15:00,115.61,91.912,57.028,29.93 +2020-09-28 08:30:00,119.14,90.226,57.028,29.93 +2020-09-28 08:45:00,116.91,91.141,57.028,29.93 +2020-09-28 09:00:00,116.01,85.585,52.633,29.93 +2020-09-28 09:15:00,119.67,83.25399999999999,52.633,29.93 +2020-09-28 09:30:00,118.68,83.365,52.633,29.93 +2020-09-28 09:45:00,122.1,82.484,52.633,29.93 +2020-09-28 10:00:00,117.58,81.078,50.647,29.93 +2020-09-28 10:15:00,119.15,81.132,50.647,29.93 +2020-09-28 10:30:00,116.21,80.383,50.647,29.93 +2020-09-28 10:45:00,116.73,79.708,50.647,29.93 +2020-09-28 11:00:00,119.42,77.577,50.245,29.93 +2020-09-28 11:15:00,116.62,78.139,50.245,29.93 +2020-09-28 11:30:00,121.85,79.594,50.245,29.93 +2020-09-28 11:45:00,117.42,79.208,50.245,29.93 +2020-09-28 12:00:00,113.11,77.02,46.956,29.93 +2020-09-28 12:15:00,116.0,76.562,46.956,29.93 +2020-09-28 12:30:00,113.7,75.102,46.956,29.93 +2020-09-28 12:45:00,110.78,75.194,46.956,29.93 +2020-09-28 13:00:00,109.56,74.983,47.383,29.93 +2020-09-28 13:15:00,112.19,74.00399999999999,47.383,29.93 +2020-09-28 13:30:00,112.32,72.598,47.383,29.93 +2020-09-28 13:45:00,113.59,72.613,47.383,29.93 +2020-09-28 14:00:00,115.79,72.227,47.1,29.93 +2020-09-28 14:15:00,115.49,72.146,47.1,29.93 +2020-09-28 14:30:00,114.12,70.952,47.1,29.93 +2020-09-28 14:45:00,114.56,71.797,47.1,29.93 +2020-09-28 15:00:00,117.46,71.695,49.355,29.93 +2020-09-28 15:15:00,116.47,69.889,49.355,29.93 +2020-09-28 15:30:00,114.37,69.814,49.355,29.93 +2020-09-28 15:45:00,114.24,68.618,49.355,29.93 +2020-09-28 16:00:00,115.27,69.747,52.14,29.93 +2020-09-28 16:15:00,117.99,69.223,52.14,29.93 +2020-09-28 16:30:00,117.86,69.88,52.14,29.93 +2020-09-28 16:45:00,120.05,67.665,52.14,29.93 +2020-09-28 17:00:00,122.78,68.229,58.705,29.93 +2020-09-28 17:15:00,121.13,69.37100000000001,58.705,29.93 +2020-09-28 17:30:00,122.96,69.17699999999999,58.705,29.93 +2020-09-28 17:45:00,120.89,69.768,58.705,29.93 +2020-09-28 18:00:00,124.3,70.32,59.153,29.93 +2020-09-28 18:15:00,121.62,69.16,59.153,29.93 +2020-09-28 18:30:00,123.48,68.303,59.153,29.93 +2020-09-28 18:45:00,119.09,72.157,59.153,29.93 +2020-09-28 19:00:00,115.15,72.955,61.483000000000004,29.93 +2020-09-28 19:15:00,109.93,71.735,61.483000000000004,29.93 +2020-09-28 19:30:00,107.66,71.362,61.483000000000004,29.93 +2020-09-28 19:45:00,110.06,71.22399999999999,61.483000000000004,29.93 +2020-09-28 20:00:00,99.87,69.15899999999999,67.55,29.93 +2020-09-28 20:15:00,98.53,68.77,67.55,29.93 +2020-09-28 20:30:00,102.19,66.858,67.55,29.93 +2020-09-28 20:45:00,103.9,65.867,67.55,29.93 +2020-09-28 21:00:00,96.81,63.74,60.026,29.93 +2020-09-28 21:15:00,91.9,65.782,60.026,29.93 +2020-09-28 21:30:00,86.66,64.904,60.026,29.93 +2020-09-28 21:45:00,91.92,63.486999999999995,60.026,29.93 +2020-09-28 22:00:00,89.43,61.42,52.736999999999995,29.93 +2020-09-28 22:15:00,89.06,60.604,52.736999999999995,29.93 +2020-09-28 22:30:00,81.86,51.547,52.736999999999995,29.93 +2020-09-28 22:45:00,86.05,47.843999999999994,52.736999999999995,29.93 +2020-09-28 23:00:00,82.52,44.95,44.408,29.93 +2020-09-28 23:15:00,80.8,43.236000000000004,44.408,29.93 +2020-09-28 23:30:00,76.73,43.395,44.408,29.93 +2020-09-28 23:45:00,79.66,42.756,44.408,29.93 +2020-09-29 00:00:00,77.73,42.65,44.438,29.93 +2020-09-29 00:15:00,76.61,43.342,44.438,29.93 +2020-09-29 00:30:00,76.57,43.253,44.438,29.93 +2020-09-29 00:45:00,78.99,43.126999999999995,44.438,29.93 +2020-09-29 01:00:00,77.92,43.176,41.468999999999994,29.93 +2020-09-29 01:15:00,78.18,42.699,41.468999999999994,29.93 +2020-09-29 01:30:00,74.21,41.974,41.468999999999994,29.93 +2020-09-29 01:45:00,77.34,41.108999999999995,41.468999999999994,29.93 +2020-09-29 02:00:00,78.32,41.458,39.708,29.93 +2020-09-29 02:15:00,77.97,41.577,39.708,29.93 +2020-09-29 02:30:00,78.04,41.858000000000004,39.708,29.93 +2020-09-29 02:45:00,80.85,42.573,39.708,29.93 +2020-09-29 03:00:00,79.93,44.456,38.919000000000004,29.93 +2020-09-29 03:15:00,76.51,44.75899999999999,38.919000000000004,29.93 +2020-09-29 03:30:00,79.49,44.794,38.919000000000004,29.93 +2020-09-29 03:45:00,80.02,45.07,38.919000000000004,29.93 +2020-09-29 04:00:00,89.45,53.608999999999995,40.092,29.93 +2020-09-29 04:15:00,93.05,62.126999999999995,40.092,29.93 +2020-09-29 04:30:00,95.48,60.648,40.092,29.93 +2020-09-29 04:45:00,95.39,61.878,40.092,29.93 +2020-09-29 05:00:00,103.88,84.5,43.713,29.93 +2020-09-29 05:15:00,108.14,103.072,43.713,29.93 +2020-09-29 05:30:00,110.41,96.729,43.713,29.93 +2020-09-29 05:45:00,114.39,90.139,43.713,29.93 +2020-09-29 06:00:00,118.77,89.865,56.033,29.93 +2020-09-29 06:15:00,120.88,92.749,56.033,29.93 +2020-09-29 06:30:00,121.79,91.06299999999999,56.033,29.93 +2020-09-29 06:45:00,123.75,92.088,56.033,29.93 +2020-09-29 07:00:00,126.35,91.59100000000001,66.003,29.93 +2020-09-29 07:15:00,126.55,93.463,66.003,29.93 +2020-09-29 07:30:00,125.28,92.823,66.003,29.93 +2020-09-29 07:45:00,123.7,93.275,66.003,29.93 +2020-09-29 08:00:00,120.71,89.791,57.474,29.93 +2020-09-29 08:15:00,117.0,90.537,57.474,29.93 +2020-09-29 08:30:00,116.25,88.941,57.474,29.93 +2020-09-29 08:45:00,114.5,89.095,57.474,29.93 +2020-09-29 09:00:00,115.58,83.553,51.928000000000004,29.93 +2020-09-29 09:15:00,120.06,81.567,51.928000000000004,29.93 +2020-09-29 09:30:00,116.6,82.34299999999999,51.928000000000004,29.93 +2020-09-29 09:45:00,112.53,82.32700000000001,51.928000000000004,29.93 +2020-09-29 10:00:00,117.82,79.771,49.46,29.93 +2020-09-29 10:15:00,117.6,79.37100000000001,49.46,29.93 +2020-09-29 10:30:00,115.38,78.68,49.46,29.93 +2020-09-29 10:45:00,115.03,78.775,49.46,29.93 +2020-09-29 11:00:00,112.32,77.102,48.206,29.93 +2020-09-29 11:15:00,114.51,77.828,48.206,29.93 +2020-09-29 11:30:00,115.85,78.133,48.206,29.93 +2020-09-29 11:45:00,122.55,77.62899999999999,48.206,29.93 +2020-09-29 12:00:00,121.21,74.861,46.285,29.93 +2020-09-29 12:15:00,123.65,74.48899999999999,46.285,29.93 +2020-09-29 12:30:00,119.98,73.876,46.285,29.93 +2020-09-29 12:45:00,119.13,74.34,46.285,29.93 +2020-09-29 13:00:00,117.91,73.741,46.861999999999995,29.93 +2020-09-29 13:15:00,116.67,73.89399999999999,46.861999999999995,29.93 +2020-09-29 13:30:00,117.65,72.825,46.861999999999995,29.93 +2020-09-29 13:45:00,118.25,72.237,46.861999999999995,29.93 +2020-09-29 14:00:00,115.97,72.195,46.488,29.93 +2020-09-29 14:15:00,115.47,72.04,46.488,29.93 +2020-09-29 14:30:00,114.39,71.3,46.488,29.93 +2020-09-29 14:45:00,114.29,71.57600000000001,46.488,29.93 +2020-09-29 15:00:00,110.86,71.184,48.442,29.93 +2020-09-29 15:15:00,112.31,70.13600000000001,48.442,29.93 +2020-09-29 15:30:00,111.91,70.016,48.442,29.93 +2020-09-29 15:45:00,114.05,68.95,48.442,29.93 +2020-09-29 16:00:00,116.1,69.681,50.397,29.93 +2020-09-29 16:15:00,115.08,69.375,50.397,29.93 +2020-09-29 16:30:00,117.86,69.956,50.397,29.93 +2020-09-29 16:45:00,119.27,68.388,50.397,29.93 +2020-09-29 17:00:00,122.25,69.221,56.668,29.93 +2020-09-29 17:15:00,118.99,70.688,56.668,29.93 +2020-09-29 17:30:00,119.0,70.389,56.668,29.93 +2020-09-29 17:45:00,121.21,70.738,56.668,29.93 +2020-09-29 18:00:00,124.72,70.545,57.957,29.93 +2020-09-29 18:15:00,120.93,70.315,57.957,29.93 +2020-09-29 18:30:00,122.34,69.196,57.957,29.93 +2020-09-29 18:45:00,118.19,73.126,57.957,29.93 +2020-09-29 19:00:00,111.46,73.086,57.056000000000004,29.93 +2020-09-29 19:15:00,107.02,71.922,57.056000000000004,29.93 +2020-09-29 19:30:00,105.82,71.21300000000001,57.056000000000004,29.93 +2020-09-29 19:45:00,104.05,71.321,57.056000000000004,29.93 +2020-09-29 20:00:00,99.51,69.616,64.156,29.93 +2020-09-29 20:15:00,95.64,68.005,64.156,29.93 +2020-09-29 20:30:00,91.07,66.372,64.156,29.93 +2020-09-29 20:45:00,88.43,65.49,64.156,29.93 +2020-09-29 21:00:00,82.9,63.803999999999995,56.507,29.93 +2020-09-29 21:15:00,78.99,65.01100000000001,56.507,29.93 +2020-09-29 21:30:00,76.78,64.074,56.507,29.93 +2020-09-29 21:45:00,75.2,62.826,56.507,29.93 +2020-09-29 22:00:00,71.1,61.318999999999996,50.728,29.93 +2020-09-29 22:15:00,70.7,60.165,50.728,29.93 +2020-09-29 22:30:00,67.54,51.342,50.728,29.93 +2020-09-29 22:45:00,65.87,47.714,50.728,29.93 +2020-09-29 23:00:00,77.01,44.299,43.556999999999995,29.93 +2020-09-29 23:15:00,80.01,43.45399999999999,43.556999999999995,29.93 +2020-09-29 23:30:00,80.68,43.519,43.556999999999995,29.93 +2020-09-29 23:45:00,80.11,42.897,43.556999999999995,29.93 +2020-09-30 00:00:00,75.35,42.93600000000001,41.151,29.93 +2020-09-30 00:15:00,78.11,43.623000000000005,41.151,29.93 +2020-09-30 00:30:00,79.8,43.541000000000004,41.151,29.93 +2020-09-30 00:45:00,81.56,43.416000000000004,41.151,29.93 +2020-09-30 01:00:00,76.14,43.457,37.763000000000005,29.93 +2020-09-30 01:15:00,75.09,43.004,37.763000000000005,29.93 +2020-09-30 01:30:00,75.45,42.298,37.763000000000005,29.93 +2020-09-30 01:45:00,80.02,41.433,37.763000000000005,29.93 +2020-09-30 02:00:00,79.63,41.788000000000004,35.615,29.93 +2020-09-30 02:15:00,78.9,41.925,35.615,29.93 +2020-09-30 02:30:00,76.86,42.18600000000001,35.615,29.93 +2020-09-30 02:45:00,78.72,42.895,35.615,29.93 +2020-09-30 03:00:00,81.85,44.765,35.153,29.93 +2020-09-30 03:15:00,81.74,45.086999999999996,35.153,29.93 +2020-09-30 03:30:00,82.99,45.126000000000005,35.153,29.93 +2020-09-30 03:45:00,83.55,45.383,35.153,29.93 +2020-09-30 04:00:00,88.62,53.961000000000006,36.203,29.93 +2020-09-30 04:15:00,91.38,62.516000000000005,36.203,29.93 +2020-09-30 04:30:00,94.94,61.042,36.203,29.93 +2020-09-30 04:45:00,97.42,62.28,36.203,29.93 +2020-09-30 05:00:00,103.8,85.005,39.922,29.93 +2020-09-30 05:15:00,107.95,103.689,39.922,29.93 +2020-09-30 05:30:00,110.12,97.321,39.922,29.93 +2020-09-30 05:45:00,113.38,90.678,39.922,29.93 +2020-09-30 06:00:00,119.1,90.37100000000001,56.443999999999996,29.93 +2020-09-30 06:15:00,117.38,93.279,56.443999999999996,29.93 +2020-09-30 06:30:00,119.12,91.594,56.443999999999996,29.93 +2020-09-30 06:45:00,120.5,92.615,56.443999999999996,29.93 +2020-09-30 07:00:00,123.29,92.118,68.683,29.93 +2020-09-30 07:15:00,121.03,94.00399999999999,68.683,29.93 +2020-09-30 07:30:00,122.13,93.398,68.683,29.93 +2020-09-30 07:45:00,120.93,93.84700000000001,68.683,29.93 +2020-09-30 08:00:00,117.66,90.37,59.003,29.93 +2020-09-30 08:15:00,117.3,91.08,59.003,29.93 +2020-09-30 08:30:00,116.66,89.5,59.003,29.93 +2020-09-30 08:45:00,116.56,89.633,59.003,29.93 +2020-09-30 09:00:00,115.38,84.096,56.21,29.93 +2020-09-30 09:15:00,115.67,82.101,56.21,29.93 +2020-09-30 09:30:00,115.08,82.85799999999999,56.21,29.93 +2020-09-30 09:45:00,114.96,82.81,56.21,29.93 +2020-09-30 10:00:00,115.1,80.25,52.358999999999995,29.93 +2020-09-30 10:15:00,116.5,79.81,52.358999999999995,29.93 +2020-09-30 10:30:00,113.95,79.10300000000001,52.358999999999995,29.93 +2020-09-30 10:45:00,112.23,79.183,52.358999999999995,29.93 +2020-09-30 11:00:00,108.77,77.523,51.161,29.93 +2020-09-30 11:15:00,108.69,78.23100000000001,51.161,29.93 +2020-09-30 11:30:00,109.39,78.536,51.161,29.93 +2020-09-30 11:45:00,108.48,78.013,51.161,29.93 +2020-09-30 12:00:00,104.28,75.219,49.119,29.93 +2020-09-30 12:15:00,106.05,74.836,49.119,29.93 +2020-09-30 12:30:00,104.62,74.258,49.119,29.93 +2020-09-30 12:45:00,103.69,74.71300000000001,49.119,29.93 +2020-09-30 13:00:00,102.95,74.086,49.187,29.93 +2020-09-30 13:15:00,102.97,74.237,49.187,29.93 +2020-09-30 13:30:00,103.28,73.165,49.187,29.93 +2020-09-30 13:45:00,103.85,72.581,49.187,29.93 +2020-09-30 14:00:00,104.84,72.49,49.787,29.93 +2020-09-30 14:15:00,104.58,72.35,49.787,29.93 +2020-09-30 14:30:00,105.13,71.645,49.787,29.93 +2020-09-30 14:45:00,105.67,71.917,49.787,29.93 +2020-09-30 15:00:00,105.44,71.477,51.458999999999996,29.93 +2020-09-30 15:15:00,106.06,70.44800000000001,51.458999999999996,29.93 +2020-09-30 15:30:00,106.29,70.362,51.458999999999996,29.93 +2020-09-30 15:45:00,107.58,69.31,51.458999999999996,29.93 +2020-09-30 16:00:00,110.43,70.0,53.663000000000004,29.93 +2020-09-30 16:15:00,109.91,69.711,53.663000000000004,29.93 +2020-09-30 16:30:00,111.8,70.28699999999999,53.663000000000004,29.93 +2020-09-30 16:45:00,113.95,68.777,53.663000000000004,29.93 +2020-09-30 17:00:00,117.7,69.568,58.183,29.93 +2020-09-30 17:15:00,116.19,71.06,58.183,29.93 +2020-09-30 17:30:00,119.57,70.767,58.183,29.93 +2020-09-30 17:45:00,120.66,71.14699999999999,58.183,29.93 +2020-09-30 18:00:00,122.31,70.935,60.141000000000005,29.93 +2020-09-30 18:15:00,120.45,70.704,60.141000000000005,29.93 +2020-09-30 18:30:00,121.29,69.597,60.141000000000005,29.93 +2020-09-30 18:45:00,119.4,73.52199999999999,60.141000000000005,29.93 +2020-09-30 19:00:00,116.88,73.491,60.582,29.93 +2020-09-30 19:15:00,115.1,72.325,60.582,29.93 +2020-09-30 19:30:00,111.94,71.613,60.582,29.93 +2020-09-30 19:45:00,110.07,71.71300000000001,60.582,29.93 +2020-09-30 20:00:00,104.26,70.03,66.61,29.93 +2020-09-30 20:15:00,105.15,68.417,66.61,29.93 +2020-09-30 20:30:00,101.51,66.757,66.61,29.93 +2020-09-30 20:45:00,102.47,65.836,66.61,29.93 +2020-09-30 21:00:00,96.11,64.15100000000001,57.658,29.93 +2020-09-30 21:15:00,94.9,65.348,57.658,29.93 +2020-09-30 21:30:00,93.22,64.42,57.658,29.93 +2020-09-30 21:45:00,94.05,63.138000000000005,57.658,29.93 +2020-09-30 22:00:00,89.15,61.61,51.81,29.93 +2020-09-30 22:15:00,86.92,60.433,51.81,29.93 +2020-09-30 22:30:00,83.39,51.606,51.81,29.93 +2020-09-30 22:45:00,81.96,47.983999999999995,51.81,29.93 +2020-09-30 23:00:00,72.81,44.608999999999995,42.93600000000001,29.93 +2020-09-30 23:15:00,74.96,43.731,42.93600000000001,29.93 +2020-09-30 23:30:00,74.25,43.799,42.93600000000001,29.93 +2020-09-30 23:45:00,77.92,43.177,42.93600000000001,29.93 +2020-10-01 00:00:00,74.36,48.013000000000005,42.746,31.349 +2020-10-01 00:15:00,73.35,49.218999999999994,42.746,31.349 +2020-10-01 00:30:00,71.89,48.891000000000005,42.746,31.349 +2020-10-01 00:45:00,74.82,48.773,42.746,31.349 +2020-10-01 01:00:00,74.95,49.074,40.025999999999996,31.349 +2020-10-01 01:15:00,73.4,48.443000000000005,40.025999999999996,31.349 +2020-10-01 01:30:00,70.75,47.641000000000005,40.025999999999996,31.349 +2020-10-01 01:45:00,74.21,47.16,40.025999999999996,31.349 +2020-10-01 02:00:00,74.54,47.581,38.154,31.349 +2020-10-01 02:15:00,75.11,47.598,38.154,31.349 +2020-10-01 02:30:00,70.83,48.211000000000006,38.154,31.349 +2020-10-01 02:45:00,74.98,48.898999999999994,38.154,31.349 +2020-10-01 03:00:00,76.99,51.108999999999995,37.575,31.349 +2020-10-01 03:15:00,78.83,51.843,37.575,31.349 +2020-10-01 03:30:00,75.95,51.895,37.575,31.349 +2020-10-01 03:45:00,81.07,52.646,37.575,31.349 +2020-10-01 04:00:00,86.24,61.338,39.154,31.349 +2020-10-01 04:15:00,88.04,69.964,39.154,31.349 +2020-10-01 04:30:00,85.63,69.25,39.154,31.349 +2020-10-01 04:45:00,92.33,70.865,39.154,31.349 +2020-10-01 05:00:00,98.87,96.057,44.085,31.349 +2020-10-01 05:15:00,108.96,118.06,44.085,31.349 +2020-10-01 05:30:00,116.11,112.14200000000001,44.085,31.349 +2020-10-01 05:45:00,112.83,104.488,44.085,31.349 +2020-10-01 06:00:00,118.1,104.522,57.49,31.349 +2020-10-01 06:15:00,117.62,108.09299999999999,57.49,31.349 +2020-10-01 06:30:00,119.15,106.42200000000001,57.49,31.349 +2020-10-01 06:45:00,121.05,107.11200000000001,57.49,31.349 +2020-10-01 07:00:00,122.34,107.375,73.617,31.349 +2020-10-01 07:15:00,120.54,109.579,73.617,31.349 +2020-10-01 07:30:00,118.69,109.189,73.617,31.349 +2020-10-01 07:45:00,117.8,109.36,73.617,31.349 +2020-10-01 08:00:00,115.95,107.73899999999999,69.281,31.349 +2020-10-01 08:15:00,115.95,108.02799999999999,69.281,31.349 +2020-10-01 08:30:00,122.25,105.654,69.281,31.349 +2020-10-01 08:45:00,124.29,104.889,69.281,31.349 +2020-10-01 09:00:00,122.13,100.23700000000001,63.926,31.349 +2020-10-01 09:15:00,121.05,98.084,63.926,31.349 +2020-10-01 09:30:00,116.55,98.544,63.926,31.349 +2020-10-01 09:45:00,115.32,98.477,63.926,31.349 +2020-10-01 10:00:00,116.93,95.274,59.442,31.349 +2020-10-01 10:15:00,119.48,94.848,59.442,31.349 +2020-10-01 10:30:00,116.82,93.89399999999999,59.442,31.349 +2020-10-01 10:45:00,115.21,93.82700000000001,59.442,31.349 +2020-10-01 11:00:00,110.64,90.531,56.771,31.349 +2020-10-01 11:15:00,110.57,91.235,56.771,31.349 +2020-10-01 11:30:00,114.18,91.44200000000001,56.771,31.349 +2020-10-01 11:45:00,110.72,91.22200000000001,56.771,31.349 +2020-10-01 12:00:00,110.27,87.663,53.701,31.349 +2020-10-01 12:15:00,110.53,87.29899999999999,53.701,31.349 +2020-10-01 12:30:00,110.68,86.45700000000001,53.701,31.349 +2020-10-01 12:45:00,110.33,86.771,53.701,31.349 +2020-10-01 13:00:00,109.27,86.516,52.364,31.349 +2020-10-01 13:15:00,108.61,86.375,52.364,31.349 +2020-10-01 13:30:00,106.82,85.52,52.364,31.349 +2020-10-01 13:45:00,108.3,85.07799999999999,52.364,31.349 +2020-10-01 14:00:00,106.34,84.788,53.419,31.349 +2020-10-01 14:15:00,103.21,85.045,53.419,31.349 +2020-10-01 14:30:00,104.24,84.14200000000001,53.419,31.349 +2020-10-01 14:45:00,101.78,84.021,53.419,31.349 +2020-10-01 15:00:00,102.18,84.177,56.744,31.349 +2020-10-01 15:15:00,105.7,83.725,56.744,31.349 +2020-10-01 15:30:00,109.3,83.95100000000001,56.744,31.349 +2020-10-01 15:45:00,112.38,83.89299999999999,56.744,31.349 +2020-10-01 16:00:00,110.92,83.941,60.458,31.349 +2020-10-01 16:15:00,110.81,83.338,60.458,31.349 +2020-10-01 16:30:00,115.07,84.031,60.458,31.349 +2020-10-01 16:45:00,114.72,82.064,60.458,31.349 +2020-10-01 17:00:00,119.03,82.493,66.295,31.349 +2020-10-01 17:15:00,116.61,83.67299999999999,66.295,31.349 +2020-10-01 17:30:00,118.16,83.4,66.295,31.349 +2020-10-01 17:45:00,121.44,84.021,66.295,31.349 +2020-10-01 18:00:00,125.72,83.735,68.468,31.349 +2020-10-01 18:15:00,122.88,83.59700000000001,68.468,31.349 +2020-10-01 18:30:00,124.48,82.62799999999999,68.468,31.349 +2020-10-01 18:45:00,119.47,86.285,68.468,31.349 +2020-10-01 19:00:00,116.32,86.665,66.39399999999999,31.349 +2020-10-01 19:15:00,113.44,85.539,66.39399999999999,31.349 +2020-10-01 19:30:00,108.44,85.01100000000001,66.39399999999999,31.349 +2020-10-01 19:45:00,109.78,85.054,66.39399999999999,31.349 +2020-10-01 20:00:00,106.35,84.295,63.183,31.349 +2020-10-01 20:15:00,108.46,82.07799999999999,63.183,31.349 +2020-10-01 20:30:00,106.1,81.286,63.183,31.349 +2020-10-01 20:45:00,101.39,80.277,63.183,31.349 +2020-10-01 21:00:00,93.83,78.27,55.133,31.349 +2020-10-01 21:15:00,92.88,78.835,55.133,31.349 +2020-10-01 21:30:00,91.65,77.945,55.133,31.349 +2020-10-01 21:45:00,92.82,76.352,55.133,31.349 +2020-10-01 22:00:00,85.0,73.477,50.111999999999995,31.349 +2020-10-01 22:15:00,85.89,71.318,50.111999999999995,31.349 +2020-10-01 22:30:00,82.6,60.872,50.111999999999995,31.349 +2020-10-01 22:45:00,86.0,55.68899999999999,50.111999999999995,31.349 +2020-10-01 23:00:00,78.43,50.915,44.536,31.349 +2020-10-01 23:15:00,77.59,50.651,44.536,31.349 +2020-10-01 23:30:00,77.83,50.118,44.536,31.349 +2020-10-01 23:45:00,79.72,50.093999999999994,44.536,31.349 +2020-10-02 00:00:00,74.84,46.853,42.291000000000004,31.349 +2020-10-02 00:15:00,76.22,48.257,42.291000000000004,31.349 +2020-10-02 00:30:00,70.57,48.04,42.291000000000004,31.349 +2020-10-02 00:45:00,72.88,48.211000000000006,42.291000000000004,31.349 +2020-10-02 01:00:00,69.56,48.17,41.008,31.349 +2020-10-02 01:15:00,70.58,47.573,41.008,31.349 +2020-10-02 01:30:00,77.02,47.115,41.008,31.349 +2020-10-02 01:45:00,78.7,46.528999999999996,41.008,31.349 +2020-10-02 02:00:00,78.05,47.533,39.521,31.349 +2020-10-02 02:15:00,73.35,47.49,39.521,31.349 +2020-10-02 02:30:00,78.63,48.773,39.521,31.349 +2020-10-02 02:45:00,79.6,49.075,39.521,31.349 +2020-10-02 03:00:00,74.8,51.332,39.812,31.349 +2020-10-02 03:15:00,75.56,51.751000000000005,39.812,31.349 +2020-10-02 03:30:00,79.48,51.661,39.812,31.349 +2020-10-02 03:45:00,85.82,53.034,39.812,31.349 +2020-10-02 04:00:00,90.92,61.923,41.22,31.349 +2020-10-02 04:15:00,87.14,69.571,41.22,31.349 +2020-10-02 04:30:00,91.86,69.50399999999999,41.22,31.349 +2020-10-02 04:45:00,98.29,70.303,41.22,31.349 +2020-10-02 05:00:00,108.33,94.79799999999999,45.115,31.349 +2020-10-02 05:15:00,107.36,118.024,45.115,31.349 +2020-10-02 05:30:00,109.91,112.62,45.115,31.349 +2020-10-02 05:45:00,112.72,104.635,45.115,31.349 +2020-10-02 06:00:00,119.78,104.962,59.06100000000001,31.349 +2020-10-02 06:15:00,119.47,108.075,59.06100000000001,31.349 +2020-10-02 06:30:00,123.95,106.051,59.06100000000001,31.349 +2020-10-02 06:45:00,121.8,107.29899999999999,59.06100000000001,31.349 +2020-10-02 07:00:00,122.66,107.613,71.874,31.349 +2020-10-02 07:15:00,121.44,110.766,71.874,31.349 +2020-10-02 07:30:00,120.73,109.22200000000001,71.874,31.349 +2020-10-02 07:45:00,118.08,108.91799999999999,71.874,31.349 +2020-10-02 08:00:00,116.05,107.29700000000001,68.439,31.349 +2020-10-02 08:15:00,115.68,107.771,68.439,31.349 +2020-10-02 08:30:00,115.03,105.775,68.439,31.349 +2020-10-02 08:45:00,114.17,104.251,68.439,31.349 +2020-10-02 09:00:00,111.59,98.507,65.523,31.349 +2020-10-02 09:15:00,110.49,97.64299999999999,65.523,31.349 +2020-10-02 09:30:00,110.15,97.525,65.523,31.349 +2020-10-02 09:45:00,108.89,97.596,65.523,31.349 +2020-10-02 10:00:00,107.9,93.774,62.005,31.349 +2020-10-02 10:15:00,108.37,93.501,62.005,31.349 +2020-10-02 10:30:00,108.22,92.79,62.005,31.349 +2020-10-02 10:45:00,109.74,92.461,62.005,31.349 +2020-10-02 11:00:00,105.01,89.28200000000001,60.351000000000006,31.349 +2020-10-02 11:15:00,103.83,88.994,60.351000000000006,31.349 +2020-10-02 11:30:00,107.03,89.801,60.351000000000006,31.349 +2020-10-02 11:45:00,102.87,89.085,60.351000000000006,31.349 +2020-10-02 12:00:00,100.19,86.24,55.331,31.349 +2020-10-02 12:15:00,99.6,84.566,55.331,31.349 +2020-10-02 12:30:00,97.98,83.89200000000001,55.331,31.349 +2020-10-02 12:45:00,96.69,84.041,55.331,31.349 +2020-10-02 13:00:00,95.31,84.48899999999999,53.361999999999995,31.349 +2020-10-02 13:15:00,96.32,84.825,53.361999999999995,31.349 +2020-10-02 13:30:00,95.77,84.383,53.361999999999995,31.349 +2020-10-02 13:45:00,96.42,84.069,53.361999999999995,31.349 +2020-10-02 14:00:00,97.78,82.788,51.708,31.349 +2020-10-02 14:15:00,97.81,83.20100000000001,51.708,31.349 +2020-10-02 14:30:00,97.63,83.339,51.708,31.349 +2020-10-02 14:45:00,99.23,83.00299999999999,51.708,31.349 +2020-10-02 15:00:00,99.88,82.859,54.571000000000005,31.349 +2020-10-02 15:15:00,98.98,82.102,54.571000000000005,31.349 +2020-10-02 15:30:00,100.68,81.37100000000001,54.571000000000005,31.349 +2020-10-02 15:45:00,102.59,81.78399999999999,54.571000000000005,31.349 +2020-10-02 16:00:00,107.25,80.862,58.662,31.349 +2020-10-02 16:15:00,107.43,80.667,58.662,31.349 +2020-10-02 16:30:00,108.62,81.304,58.662,31.349 +2020-10-02 16:45:00,109.89,78.92399999999999,58.662,31.349 +2020-10-02 17:00:00,114.07,80.343,65.941,31.349 +2020-10-02 17:15:00,111.49,81.27,65.941,31.349 +2020-10-02 17:30:00,115.35,80.94800000000001,65.941,31.349 +2020-10-02 17:45:00,115.86,81.39699999999999,65.941,31.349 +2020-10-02 18:00:00,122.12,81.46300000000001,65.628,31.349 +2020-10-02 18:15:00,118.93,80.623,65.628,31.349 +2020-10-02 18:30:00,117.78,79.774,65.628,31.349 +2020-10-02 18:45:00,115.24,83.65299999999999,65.628,31.349 +2020-10-02 19:00:00,111.24,84.94,63.662,31.349 +2020-10-02 19:15:00,106.67,84.70299999999999,63.662,31.349 +2020-10-02 19:30:00,105.91,84.031,63.662,31.349 +2020-10-02 19:45:00,102.96,83.294,63.662,31.349 +2020-10-02 20:00:00,92.61,82.48899999999999,61.945,31.349 +2020-10-02 20:15:00,99.16,80.71600000000001,61.945,31.349 +2020-10-02 20:30:00,98.55,79.605,61.945,31.349 +2020-10-02 20:45:00,96.27,78.343,61.945,31.349 +2020-10-02 21:00:00,88.3,77.288,53.903,31.349 +2020-10-02 21:15:00,85.55,78.967,53.903,31.349 +2020-10-02 21:30:00,80.59,78.02,53.903,31.349 +2020-10-02 21:45:00,84.21,76.735,53.903,31.349 +2020-10-02 22:00:00,80.6,74.171,48.403999999999996,31.349 +2020-10-02 22:15:00,79.32,71.794,48.403999999999996,31.349 +2020-10-02 22:30:00,71.59,66.65899999999999,48.403999999999996,31.349 +2020-10-02 22:45:00,70.6,63.413000000000004,48.403999999999996,31.349 +2020-10-02 23:00:00,62.12,59.43899999999999,41.07,31.349 +2020-10-02 23:15:00,61.6,57.461999999999996,41.07,31.349 +2020-10-02 23:30:00,61.56,55.302,41.07,31.349 +2020-10-02 23:45:00,59.87,54.931000000000004,41.07,31.349 +2020-10-03 00:00:00,57.65,47.711000000000006,11.117,31.177 +2020-10-03 00:15:00,57.59,46.246,11.117,31.177 +2020-10-03 00:30:00,56.67,46.058,11.117,31.177 +2020-10-03 00:45:00,56.96,46.294,11.117,31.177 +2020-10-03 01:00:00,56.34,46.769,10.685,31.177 +2020-10-03 01:15:00,56.06,46.481,10.685,31.177 +2020-10-03 01:30:00,55.87,45.493,10.685,31.177 +2020-10-03 01:45:00,55.71,45.202,10.685,31.177 +2020-10-03 02:00:00,55.96,45.67,7.925,31.177 +2020-10-03 02:15:00,56.34,45.118,7.925,31.177 +2020-10-03 02:30:00,56.14,45.926,7.925,31.177 +2020-10-03 02:45:00,56.27,46.731,7.925,31.177 +2020-10-03 03:00:00,55.64,48.928000000000004,7.627999999999999,31.177 +2020-10-03 03:15:00,57.67,48.434,7.627999999999999,31.177 +2020-10-03 03:30:00,57.91,48.034,7.627999999999999,31.177 +2020-10-03 03:45:00,58.8,49.805,7.627999999999999,31.177 +2020-10-03 04:00:00,60.47,55.907,7.986000000000001,31.177 +2020-10-03 04:15:00,59.42,61.435,7.986000000000001,31.177 +2020-10-03 04:30:00,59.0,60.357,7.986000000000001,31.177 +2020-10-03 04:45:00,58.38,60.965,7.986000000000001,31.177 +2020-10-03 05:00:00,59.9,73.63,9.039,31.177 +2020-10-03 05:15:00,59.86,81.95299999999999,9.039,31.177 +2020-10-03 05:30:00,60.28,77.28,9.039,31.177 +2020-10-03 05:45:00,61.97,73.884,9.039,31.177 +2020-10-03 06:00:00,62.0,86.214,10.683,31.177 +2020-10-03 06:15:00,62.94,100.009,10.683,31.177 +2020-10-03 06:30:00,64.88,93.45,10.683,31.177 +2020-10-03 06:45:00,68.16,88.402,10.683,31.177 +2020-10-03 07:00:00,74.5,87.154,14.055,31.177 +2020-10-03 07:15:00,72.95,87.805,14.055,31.177 +2020-10-03 07:30:00,76.05,88.484,14.055,31.177 +2020-10-03 07:45:00,75.81,89.97200000000001,14.055,31.177 +2020-10-03 08:00:00,77.66,91.262,17.652,31.177 +2020-10-03 08:15:00,78.36,93.70299999999999,17.652,31.177 +2020-10-03 08:30:00,76.08,93.544,17.652,31.177 +2020-10-03 08:45:00,77.02,94.595,17.652,31.177 +2020-10-03 09:00:00,76.35,90.97,21.353,31.177 +2020-10-03 09:15:00,79.16,90.64,21.353,31.177 +2020-10-03 09:30:00,78.99,91.32799999999999,21.353,31.177 +2020-10-03 09:45:00,81.23,91.696,21.353,31.177 +2020-10-03 10:00:00,83.68,89.508,23.467,31.177 +2020-10-03 10:15:00,86.8,89.756,23.467,31.177 +2020-10-03 10:30:00,87.14,89.29700000000001,23.467,31.177 +2020-10-03 10:45:00,87.03,89.06,23.467,31.177 +2020-10-03 11:00:00,82.41,86.065,24.539,31.177 +2020-10-03 11:15:00,82.83,85.729,24.539,31.177 +2020-10-03 11:30:00,76.93,86.07799999999999,24.539,31.177 +2020-10-03 11:45:00,78.79,85.66799999999999,24.539,31.177 +2020-10-03 12:00:00,74.79,82.721,21.488000000000003,31.177 +2020-10-03 12:15:00,72.19,82.29799999999999,21.488000000000003,31.177 +2020-10-03 12:30:00,59.91,81.195,21.488000000000003,31.177 +2020-10-03 12:45:00,62.91,80.593,21.488000000000003,31.177 +2020-10-03 13:00:00,61.74,79.922,18.776,31.177 +2020-10-03 13:15:00,59.78,80.102,18.776,31.177 +2020-10-03 13:30:00,66.56,78.882,18.776,31.177 +2020-10-03 13:45:00,70.81,78.37899999999999,18.776,31.177 +2020-10-03 14:00:00,68.47,78.347,17.301,31.177 +2020-10-03 14:15:00,68.4,78.655,17.301,31.177 +2020-10-03 14:30:00,67.72,77.69800000000001,17.301,31.177 +2020-10-03 14:45:00,64.03,76.969,17.301,31.177 +2020-10-03 15:00:00,66.02,76.7,21.236,31.177 +2020-10-03 15:15:00,68.96,76.618,21.236,31.177 +2020-10-03 15:30:00,70.76,76.907,21.236,31.177 +2020-10-03 15:45:00,72.25,77.335,21.236,31.177 +2020-10-03 16:00:00,74.29,77.045,28.31,31.177 +2020-10-03 16:15:00,75.52,76.52199999999999,28.31,31.177 +2020-10-03 16:30:00,78.34,77.89699999999999,28.31,31.177 +2020-10-03 16:45:00,82.8,76.039,28.31,31.177 +2020-10-03 17:00:00,87.35,76.783,41.687,31.177 +2020-10-03 17:15:00,87.12,77.97,41.687,31.177 +2020-10-03 17:30:00,88.97,78.093,41.687,31.177 +2020-10-03 17:45:00,90.28,79.843,41.687,31.177 +2020-10-03 18:00:00,94.76,80.597,49.201,31.177 +2020-10-03 18:15:00,92.25,81.734,49.201,31.177 +2020-10-03 18:30:00,94.32,81.203,49.201,31.177 +2020-10-03 18:45:00,91.48,82.59100000000001,49.201,31.177 +2020-10-03 19:00:00,92.73,84.505,51.937,31.177 +2020-10-03 19:15:00,98.28,83.11,51.937,31.177 +2020-10-03 19:30:00,96.89,82.96,51.937,31.177 +2020-10-03 19:45:00,88.92,83.443,51.937,31.177 +2020-10-03 20:00:00,85.03,84.101,52.617,31.177 +2020-10-03 20:15:00,85.02,83.164,52.617,31.177 +2020-10-03 20:30:00,90.9,82.34700000000001,52.617,31.177 +2020-10-03 20:45:00,91.31,80.497,52.617,31.177 +2020-10-03 21:00:00,85.41,78.417,46.238,31.177 +2020-10-03 21:15:00,84.03,79.649,46.238,31.177 +2020-10-03 21:30:00,86.88,79.069,46.238,31.177 +2020-10-03 21:45:00,86.4,77.541,46.238,31.177 +2020-10-03 22:00:00,81.78,76.17,48.275,31.177 +2020-10-03 22:15:00,78.99,73.67,48.275,31.177 +2020-10-03 22:30:00,80.17,69.319,48.275,31.177 +2020-10-03 22:45:00,79.39,65.98899999999999,48.275,31.177 +2020-10-03 23:00:00,55.75,61.363,38.071999999999996,31.177 +2020-10-03 23:15:00,57.09,60.38399999999999,38.071999999999996,31.177 +2020-10-03 23:30:00,53.87,58.913999999999994,38.071999999999996,31.177 +2020-10-03 23:45:00,56.4,58.196000000000005,38.071999999999996,31.177 +2020-10-04 00:00:00,53.94,48.018,28.229,31.177 +2020-10-04 00:15:00,54.47,46.545,28.229,31.177 +2020-10-04 00:30:00,52.62,46.365,28.229,31.177 +2020-10-04 00:45:00,53.51,46.6,28.229,31.177 +2020-10-04 01:00:00,50.25,47.07899999999999,25.669,31.177 +2020-10-04 01:15:00,52.39,46.812,25.669,31.177 +2020-10-04 01:30:00,51.82,45.841,25.669,31.177 +2020-10-04 01:45:00,51.72,45.548,25.669,31.177 +2020-10-04 02:00:00,51.1,46.023999999999994,24.948,31.177 +2020-10-04 02:15:00,52.15,45.488,24.948,31.177 +2020-10-04 02:30:00,51.41,46.276,24.948,31.177 +2020-10-04 02:45:00,50.99,47.077,24.948,31.177 +2020-10-04 03:00:00,52.2,49.261,24.445,31.177 +2020-10-04 03:15:00,51.81,48.788000000000004,24.445,31.177 +2020-10-04 03:30:00,52.33,48.39,24.445,31.177 +2020-10-04 03:45:00,53.15,50.145,24.445,31.177 +2020-10-04 04:00:00,53.33,56.275,25.839000000000002,31.177 +2020-10-04 04:15:00,53.92,61.833,25.839000000000002,31.177 +2020-10-04 04:30:00,54.43,60.755,25.839000000000002,31.177 +2020-10-04 04:45:00,54.79,61.372,25.839000000000002,31.177 +2020-10-04 05:00:00,57.04,74.122,26.803,31.177 +2020-10-04 05:15:00,56.99,82.527,26.803,31.177 +2020-10-04 05:30:00,58.51,77.84100000000001,26.803,31.177 +2020-10-04 05:45:00,59.57,74.402,26.803,31.177 +2020-10-04 06:00:00,60.65,86.709,28.147,31.177 +2020-10-04 06:15:00,59.54,100.521,28.147,31.177 +2020-10-04 06:30:00,61.12,93.97399999999999,28.147,31.177 +2020-10-04 06:45:00,61.43,88.93,28.147,31.177 +2020-10-04 07:00:00,66.21,87.679,31.116,31.177 +2020-10-04 07:15:00,65.25,88.34700000000001,31.116,31.177 +2020-10-04 07:30:00,64.85,89.059,31.116,31.177 +2020-10-04 07:45:00,65.24,90.551,31.116,31.177 +2020-10-04 08:00:00,63.88,91.851,35.739000000000004,31.177 +2020-10-04 08:15:00,66.44,94.265,35.739000000000004,31.177 +2020-10-04 08:30:00,70.93,94.132,35.739000000000004,31.177 +2020-10-04 08:45:00,67.85,95.15899999999999,35.739000000000004,31.177 +2020-10-04 09:00:00,66.06,91.535,39.455999999999996,31.177 +2020-10-04 09:15:00,67.28,91.199,39.455999999999996,31.177 +2020-10-04 09:30:00,70.83,91.869,39.455999999999996,31.177 +2020-10-04 09:45:00,77.44,92.211,39.455999999999996,31.177 +2020-10-04 10:00:00,78.19,90.01700000000001,41.343999999999994,31.177 +2020-10-04 10:15:00,81.74,90.225,41.343999999999994,31.177 +2020-10-04 10:30:00,83.73,89.74799999999999,41.343999999999994,31.177 +2020-10-04 10:45:00,84.45,89.494,41.343999999999994,31.177 +2020-10-04 11:00:00,83.31,86.509,43.645,31.177 +2020-10-04 11:15:00,81.0,86.15299999999999,43.645,31.177 +2020-10-04 11:30:00,79.77,86.50200000000001,43.645,31.177 +2020-10-04 11:45:00,79.1,86.075,43.645,31.177 +2020-10-04 12:00:00,75.39,83.105,39.796,31.177 +2020-10-04 12:15:00,75.42,82.67200000000001,39.796,31.177 +2020-10-04 12:30:00,74.51,81.604,39.796,31.177 +2020-10-04 12:45:00,74.31,80.998,39.796,31.177 +2020-10-04 13:00:00,73.03,80.295,36.343,31.177 +2020-10-04 13:15:00,72.61,80.479,36.343,31.177 +2020-10-04 13:30:00,73.42,79.259,36.343,31.177 +2020-10-04 13:45:00,73.96,78.757,36.343,31.177 +2020-10-04 14:00:00,72.48,78.671,33.162,31.177 +2020-10-04 14:15:00,72.19,78.99600000000001,33.162,31.177 +2020-10-04 14:30:00,73.52,78.075,33.162,31.177 +2020-10-04 14:45:00,73.96,77.342,33.162,31.177 +2020-10-04 15:00:00,74.74,77.039,33.215,31.177 +2020-10-04 15:15:00,74.08,76.976,33.215,31.177 +2020-10-04 15:30:00,74.78,77.30199999999999,33.215,31.177 +2020-10-04 15:45:00,76.21,77.745,33.215,31.177 +2020-10-04 16:00:00,78.09,77.42,37.385999999999996,31.177 +2020-10-04 16:15:00,79.1,76.916,37.385999999999996,31.177 +2020-10-04 16:30:00,80.57,78.28699999999999,37.385999999999996,31.177 +2020-10-04 16:45:00,82.81,76.484,37.385999999999996,31.177 +2020-10-04 17:00:00,87.51,77.188,46.618,31.177 +2020-10-04 17:15:00,86.86,78.395,46.618,31.177 +2020-10-04 17:30:00,92.1,78.51899999999999,46.618,31.177 +2020-10-04 17:45:00,89.55,80.29,46.618,31.177 +2020-10-04 18:00:00,95.29,81.03,50.111000000000004,31.177 +2020-10-04 18:15:00,88.95,82.15100000000001,50.111000000000004,31.177 +2020-10-04 18:30:00,91.01,81.631,50.111000000000004,31.177 +2020-10-04 18:45:00,87.31,83.012,50.111000000000004,31.177 +2020-10-04 19:00:00,86.3,84.939,50.25,31.177 +2020-10-04 19:15:00,84.42,83.538,50.25,31.177 +2020-10-04 19:30:00,83.14,83.38,50.25,31.177 +2020-10-04 19:45:00,81.46,83.845,50.25,31.177 +2020-10-04 20:00:00,77.95,84.52600000000001,44.265,31.177 +2020-10-04 20:15:00,79.6,83.583,44.265,31.177 +2020-10-04 20:30:00,77.92,82.73899999999999,44.265,31.177 +2020-10-04 20:45:00,78.85,80.859,44.265,31.177 +2020-10-04 21:00:00,77.61,78.781,39.717,31.177 +2020-10-04 21:15:00,77.95,80.00399999999999,39.717,31.177 +2020-10-04 21:30:00,75.4,79.433,39.717,31.177 +2020-10-04 21:45:00,75.62,77.875,39.717,31.177 +2020-10-04 22:00:00,72.93,76.492,39.224000000000004,31.177 +2020-10-04 22:15:00,72.3,73.969,39.224000000000004,31.177 +2020-10-04 22:30:00,69.94,69.63,39.224000000000004,31.177 +2020-10-04 22:45:00,69.71,66.307,39.224000000000004,31.177 +2020-10-04 23:00:00,65.21,61.708999999999996,33.518,31.177 +2020-10-04 23:15:00,66.43,60.7,33.518,31.177 +2020-10-04 23:30:00,65.32,59.232,33.518,31.177 +2020-10-04 23:45:00,65.1,58.507,33.518,31.177 +2020-10-05 00:00:00,66.33,50.821999999999996,34.301,31.349 +2020-10-05 00:15:00,64.68,50.945,34.301,31.349 +2020-10-05 00:30:00,63.44,50.6,34.301,31.349 +2020-10-05 00:45:00,65.02,50.398,34.301,31.349 +2020-10-05 01:00:00,64.78,51.103,34.143,31.349 +2020-10-05 01:15:00,64.46,50.661,34.143,31.349 +2020-10-05 01:30:00,64.07,49.93899999999999,34.143,31.349 +2020-10-05 01:45:00,64.5,49.631,34.143,31.349 +2020-10-05 02:00:00,63.51,50.364,33.650999999999996,31.349 +2020-10-05 02:15:00,65.03,49.815,33.650999999999996,31.349 +2020-10-05 02:30:00,65.36,50.809,33.650999999999996,31.349 +2020-10-05 02:45:00,66.21,51.282,33.650999999999996,31.349 +2020-10-05 03:00:00,69.64,54.224,32.599000000000004,31.349 +2020-10-05 03:15:00,74.23,54.818999999999996,32.599000000000004,31.349 +2020-10-05 03:30:00,75.31,54.718,32.599000000000004,31.349 +2020-10-05 03:45:00,73.63,55.974,32.599000000000004,31.349 +2020-10-05 04:00:00,76.43,65.589,33.785,31.349 +2020-10-05 04:15:00,77.8,74.464,33.785,31.349 +2020-10-05 04:30:00,89.37,73.941,33.785,31.349 +2020-10-05 04:45:00,95.43,74.844,33.785,31.349 +2020-10-05 05:00:00,102.6,97.568,41.285,31.349 +2020-10-05 05:15:00,101.26,119.60700000000001,41.285,31.349 +2020-10-05 05:30:00,105.1,114.088,41.285,31.349 +2020-10-05 05:45:00,112.77,106.70100000000001,41.285,31.349 +2020-10-05 06:00:00,116.26,106.459,60.486000000000004,31.349 +2020-10-05 06:15:00,116.2,109.37,60.486000000000004,31.349 +2020-10-05 06:30:00,117.92,108.182,60.486000000000004,31.349 +2020-10-05 06:45:00,120.49,109.552,60.486000000000004,31.349 +2020-10-05 07:00:00,124.95,109.79,74.012,31.349 +2020-10-05 07:15:00,123.49,112.266,74.012,31.349 +2020-10-05 07:30:00,122.31,112.215,74.012,31.349 +2020-10-05 07:45:00,122.8,112.87799999999999,74.012,31.349 +2020-10-05 08:00:00,122.15,111.291,69.569,31.349 +2020-10-05 08:15:00,123.81,112.141,69.569,31.349 +2020-10-05 08:30:00,125.38,109.84100000000001,69.569,31.349 +2020-10-05 08:45:00,126.19,109.616,69.569,31.349 +2020-10-05 09:00:00,123.98,105.119,66.152,31.349 +2020-10-05 09:15:00,123.63,102.352,66.152,31.349 +2020-10-05 09:30:00,123.4,102.083,66.152,31.349 +2020-10-05 09:45:00,125.12,101.262,66.152,31.349 +2020-10-05 10:00:00,124.46,99.031,62.923,31.349 +2020-10-05 10:15:00,127.71,98.98,62.923,31.349 +2020-10-05 10:30:00,127.77,97.86399999999999,62.923,31.349 +2020-10-05 10:45:00,125.66,97.073,62.923,31.349 +2020-10-05 11:00:00,120.69,93.165,61.522,31.349 +2020-10-05 11:15:00,117.37,93.73,61.522,31.349 +2020-10-05 11:30:00,112.63,95.04799999999999,61.522,31.349 +2020-10-05 11:45:00,108.66,94.689,61.522,31.349 +2020-10-05 12:00:00,102.98,91.74600000000001,58.632,31.349 +2020-10-05 12:15:00,103.41,91.36,58.632,31.349 +2020-10-05 12:30:00,100.84,89.89299999999999,58.632,31.349 +2020-10-05 12:45:00,101.8,89.97,58.632,31.349 +2020-10-05 13:00:00,100.62,89.954,59.06,31.349 +2020-10-05 13:15:00,100.28,89.065,59.06,31.349 +2020-10-05 13:30:00,100.19,87.693,59.06,31.349 +2020-10-05 13:45:00,100.01,87.664,59.06,31.349 +2020-10-05 14:00:00,100.8,86.809,59.791000000000004,31.349 +2020-10-05 14:15:00,101.96,87.161,59.791000000000004,31.349 +2020-10-05 14:30:00,103.14,85.947,59.791000000000004,31.349 +2020-10-05 14:45:00,104.3,86.251,59.791000000000004,31.349 +2020-10-05 15:00:00,107.02,86.544,61.148,31.349 +2020-10-05 15:15:00,104.62,85.551,61.148,31.349 +2020-10-05 15:30:00,105.02,85.93,61.148,31.349 +2020-10-05 15:45:00,104.28,85.939,61.148,31.349 +2020-10-05 16:00:00,105.06,86.02600000000001,66.009,31.349 +2020-10-05 16:15:00,106.92,85.244,66.009,31.349 +2020-10-05 16:30:00,109.95,85.84,66.009,31.349 +2020-10-05 16:45:00,111.32,83.586,66.009,31.349 +2020-10-05 17:00:00,116.22,83.50299999999999,73.683,31.349 +2020-10-05 17:15:00,114.24,84.52600000000001,73.683,31.349 +2020-10-05 17:30:00,117.49,84.211,73.683,31.349 +2020-10-05 17:45:00,122.2,85.152,73.683,31.349 +2020-10-05 18:00:00,125.09,85.385,72.848,31.349 +2020-10-05 18:15:00,121.39,84.57,72.848,31.349 +2020-10-05 18:30:00,120.7,83.915,72.848,31.349 +2020-10-05 18:45:00,118.89,87.323,72.848,31.349 +2020-10-05 19:00:00,119.68,88.385,71.139,31.349 +2020-10-05 19:15:00,117.11,87.249,71.139,31.349 +2020-10-05 19:30:00,112.56,87.075,71.139,31.349 +2020-10-05 19:45:00,108.13,86.844,71.139,31.349 +2020-10-05 20:00:00,101.98,85.89200000000001,69.667,31.349 +2020-10-05 20:15:00,101.73,84.719,69.667,31.349 +2020-10-05 20:30:00,97.75,83.385,69.667,31.349 +2020-10-05 20:45:00,97.25,82.257,69.667,31.349 +2020-10-05 21:00:00,93.19,80.05,61.166000000000004,31.349 +2020-10-05 21:15:00,97.6,81.041,61.166000000000004,31.349 +2020-10-05 21:30:00,94.2,80.37899999999999,61.166000000000004,31.349 +2020-10-05 21:45:00,93.44,78.468,61.166000000000004,31.349 +2020-10-05 22:00:00,85.58,74.764,52.772,31.349 +2020-10-05 22:15:00,82.71,72.807,52.772,31.349 +2020-10-05 22:30:00,81.05,62.21,52.772,31.349 +2020-10-05 22:45:00,84.7,56.94,52.772,31.349 +2020-10-05 23:00:00,82.54,52.706,45.136,31.349 +2020-10-05 23:15:00,82.36,51.802,45.136,31.349 +2020-10-05 23:30:00,74.67,51.42,45.136,31.349 +2020-10-05 23:45:00,79.64,51.446999999999996,45.136,31.349 +2020-10-06 00:00:00,78.11,49.55,47.35,31.349 +2020-10-06 00:15:00,81.09,50.716,47.35,31.349 +2020-10-06 00:30:00,75.23,50.428000000000004,47.35,31.349 +2020-10-06 00:45:00,74.03,50.303999999999995,47.35,31.349 +2020-10-06 01:00:00,70.95,50.623999999999995,43.424,31.349 +2020-10-06 01:15:00,79.65,50.096000000000004,43.424,31.349 +2020-10-06 01:30:00,79.59,49.382,43.424,31.349 +2020-10-06 01:45:00,78.21,48.891999999999996,43.424,31.349 +2020-10-06 02:00:00,72.09,49.352,41.778999999999996,31.349 +2020-10-06 02:15:00,78.09,49.449,41.778999999999996,31.349 +2020-10-06 02:30:00,79.6,49.968999999999994,41.778999999999996,31.349 +2020-10-06 02:45:00,79.98,50.633,41.778999999999996,31.349 +2020-10-06 03:00:00,79.25,52.776,40.771,31.349 +2020-10-06 03:15:00,82.62,53.61,40.771,31.349 +2020-10-06 03:30:00,83.6,53.68,40.771,31.349 +2020-10-06 03:45:00,83.88,54.345,40.771,31.349 +2020-10-06 04:00:00,83.35,63.176,41.816,31.349 +2020-10-06 04:15:00,89.08,71.957,41.816,31.349 +2020-10-06 04:30:00,94.83,71.24600000000001,41.816,31.349 +2020-10-06 04:45:00,100.98,72.905,41.816,31.349 +2020-10-06 05:00:00,99.83,98.514,45.842,31.349 +2020-10-06 05:15:00,101.8,120.931,45.842,31.349 +2020-10-06 05:30:00,108.7,114.943,45.842,31.349 +2020-10-06 05:45:00,110.57,107.079,45.842,31.349 +2020-10-06 06:00:00,117.91,106.995,59.12,31.349 +2020-10-06 06:15:00,116.24,110.652,59.12,31.349 +2020-10-06 06:30:00,118.48,109.045,59.12,31.349 +2020-10-06 06:45:00,119.02,109.751,59.12,31.349 +2020-10-06 07:00:00,123.59,110.00200000000001,70.33,31.349 +2020-10-06 07:15:00,121.19,112.28299999999999,70.33,31.349 +2020-10-06 07:30:00,117.92,112.06200000000001,70.33,31.349 +2020-10-06 07:45:00,117.6,112.25200000000001,70.33,31.349 +2020-10-06 08:00:00,114.16,110.685,67.788,31.349 +2020-10-06 08:15:00,114.34,110.84,67.788,31.349 +2020-10-06 08:30:00,118.79,108.587,67.788,31.349 +2020-10-06 08:45:00,121.22,107.713,67.788,31.349 +2020-10-06 09:00:00,113.61,103.059,62.622,31.349 +2020-10-06 09:15:00,114.17,100.876,62.622,31.349 +2020-10-06 09:30:00,117.63,101.25,62.622,31.349 +2020-10-06 09:45:00,116.96,101.04799999999999,62.622,31.349 +2020-10-06 10:00:00,111.74,97.815,60.887,31.349 +2020-10-06 10:15:00,115.0,97.189,60.887,31.349 +2020-10-06 10:30:00,113.09,96.147,60.887,31.349 +2020-10-06 10:45:00,113.9,95.99799999999999,60.887,31.349 +2020-10-06 11:00:00,115.53,92.74600000000001,59.812,31.349 +2020-10-06 11:15:00,116.95,93.359,59.812,31.349 +2020-10-06 11:30:00,115.24,93.559,59.812,31.349 +2020-10-06 11:45:00,109.97,93.256,59.812,31.349 +2020-10-06 12:00:00,105.42,89.58,56.614,31.349 +2020-10-06 12:15:00,102.97,89.16799999999999,56.614,31.349 +2020-10-06 12:30:00,103.1,88.50299999999999,56.614,31.349 +2020-10-06 12:45:00,106.67,88.796,56.614,31.349 +2020-10-06 13:00:00,103.21,88.37899999999999,56.824,31.349 +2020-10-06 13:15:00,101.73,88.26,56.824,31.349 +2020-10-06 13:30:00,102.74,87.40100000000001,56.824,31.349 +2020-10-06 13:45:00,104.36,86.963,56.824,31.349 +2020-10-06 14:00:00,104.47,86.411,57.623999999999995,31.349 +2020-10-06 14:15:00,105.34,86.749,57.623999999999995,31.349 +2020-10-06 14:30:00,104.12,86.022,57.623999999999995,31.349 +2020-10-06 14:45:00,104.25,85.884,57.623999999999995,31.349 +2020-10-06 15:00:00,104.24,85.866,59.724,31.349 +2020-10-06 15:15:00,105.47,85.515,59.724,31.349 +2020-10-06 15:30:00,105.21,85.925,59.724,31.349 +2020-10-06 15:45:00,107.87,85.944,59.724,31.349 +2020-10-06 16:00:00,110.19,85.816,61.64,31.349 +2020-10-06 16:15:00,113.18,85.307,61.64,31.349 +2020-10-06 16:30:00,112.84,85.98299999999999,61.64,31.349 +2020-10-06 16:45:00,115.02,84.288,61.64,31.349 +2020-10-06 17:00:00,119.5,84.516,68.962,31.349 +2020-10-06 17:15:00,116.33,85.795,68.962,31.349 +2020-10-06 17:30:00,121.99,85.531,68.962,31.349 +2020-10-06 17:45:00,123.39,86.258,68.962,31.349 +2020-10-06 18:00:00,127.05,85.90100000000001,69.149,31.349 +2020-10-06 18:15:00,122.1,85.682,69.149,31.349 +2020-10-06 18:30:00,121.85,84.766,69.149,31.349 +2020-10-06 18:45:00,120.37,88.39,69.149,31.349 +2020-10-06 19:00:00,118.45,88.83200000000001,68.832,31.349 +2020-10-06 19:15:00,118.71,87.679,68.832,31.349 +2020-10-06 19:30:00,115.61,87.10600000000001,68.832,31.349 +2020-10-06 19:45:00,112.12,87.06,68.832,31.349 +2020-10-06 20:00:00,100.95,86.41799999999999,66.403,31.349 +2020-10-06 20:15:00,100.94,84.17299999999999,66.403,31.349 +2020-10-06 20:30:00,98.55,83.243,66.403,31.349 +2020-10-06 20:45:00,98.12,82.089,66.403,31.349 +2020-10-06 21:00:00,91.59,80.08800000000001,57.352,31.349 +2020-10-06 21:15:00,90.82,80.605,57.352,31.349 +2020-10-06 21:30:00,87.87,79.758,57.352,31.349 +2020-10-06 21:45:00,90.21,78.021,57.352,31.349 +2020-10-06 22:00:00,90.01,75.087,51.148999999999994,31.349 +2020-10-06 22:15:00,89.32,72.814,51.148999999999994,31.349 +2020-10-06 22:30:00,85.07,62.431999999999995,51.148999999999994,31.349 +2020-10-06 22:45:00,83.99,57.276,51.148999999999994,31.349 +2020-10-06 23:00:00,78.1,52.643,41.8,31.349 +2020-10-06 23:15:00,82.01,52.229,41.8,31.349 +2020-10-06 23:30:00,84.4,51.70399999999999,41.8,31.349 +2020-10-06 23:45:00,87.64,51.651,41.8,31.349 +2020-10-07 00:00:00,80.25,49.86,42.269,31.349 +2020-10-07 00:15:00,80.19,51.019,42.269,31.349 +2020-10-07 00:30:00,81.39,50.736999999999995,42.269,31.349 +2020-10-07 00:45:00,86.62,50.611999999999995,42.269,31.349 +2020-10-07 01:00:00,82.23,50.937,38.527,31.349 +2020-10-07 01:15:00,79.29,50.428999999999995,38.527,31.349 +2020-10-07 01:30:00,81.05,49.733000000000004,38.527,31.349 +2020-10-07 01:45:00,83.23,49.24100000000001,38.527,31.349 +2020-10-07 02:00:00,83.52,49.708,36.393,31.349 +2020-10-07 02:15:00,82.09,49.821000000000005,36.393,31.349 +2020-10-07 02:30:00,79.19,50.321999999999996,36.393,31.349 +2020-10-07 02:45:00,79.81,50.982,36.393,31.349 +2020-10-07 03:00:00,86.92,53.113,36.167,31.349 +2020-10-07 03:15:00,87.2,53.966,36.167,31.349 +2020-10-07 03:30:00,88.26,54.038999999999994,36.167,31.349 +2020-10-07 03:45:00,86.31,54.68600000000001,36.167,31.349 +2020-10-07 04:00:00,94.04,63.54600000000001,38.092,31.349 +2020-10-07 04:15:00,97.41,72.358,38.092,31.349 +2020-10-07 04:30:00,98.81,71.648,38.092,31.349 +2020-10-07 04:45:00,98.37,73.316,38.092,31.349 +2020-10-07 05:00:00,104.2,99.01,42.268,31.349 +2020-10-07 05:15:00,110.49,121.51100000000001,42.268,31.349 +2020-10-07 05:30:00,112.99,115.508,42.268,31.349 +2020-10-07 05:45:00,120.34,107.602,42.268,31.349 +2020-10-07 06:00:00,123.12,107.494,60.158,31.349 +2020-10-07 06:15:00,115.11,111.17,60.158,31.349 +2020-10-07 06:30:00,121.4,109.574,60.158,31.349 +2020-10-07 06:45:00,121.67,110.28299999999999,60.158,31.349 +2020-10-07 07:00:00,123.41,110.53299999999999,74.792,31.349 +2020-10-07 07:15:00,121.71,112.82700000000001,74.792,31.349 +2020-10-07 07:30:00,121.33,112.64,74.792,31.349 +2020-10-07 07:45:00,119.53,112.833,74.792,31.349 +2020-10-07 08:00:00,115.82,111.277,70.499,31.349 +2020-10-07 08:15:00,116.66,111.404,70.499,31.349 +2020-10-07 08:30:00,116.5,109.17399999999999,70.499,31.349 +2020-10-07 08:45:00,116.09,108.279,70.499,31.349 +2020-10-07 09:00:00,113.61,103.625,68.892,31.349 +2020-10-07 09:15:00,113.03,101.435,68.892,31.349 +2020-10-07 09:30:00,112.32,101.794,68.892,31.349 +2020-10-07 09:45:00,113.24,101.564,68.892,31.349 +2020-10-07 10:00:00,113.52,98.324,66.88600000000001,31.349 +2020-10-07 10:15:00,116.01,97.65799999999999,66.88600000000001,31.349 +2020-10-07 10:30:00,112.9,96.59899999999999,66.88600000000001,31.349 +2020-10-07 10:45:00,113.01,96.432,66.88600000000001,31.349 +2020-10-07 11:00:00,110.74,93.19,66.187,31.349 +2020-10-07 11:15:00,107.17,93.78399999999999,66.187,31.349 +2020-10-07 11:30:00,106.12,93.98299999999999,66.187,31.349 +2020-10-07 11:45:00,106.28,93.664,66.187,31.349 +2020-10-07 12:00:00,103.85,89.963,62.18,31.349 +2020-10-07 12:15:00,104.47,89.54299999999999,62.18,31.349 +2020-10-07 12:30:00,103.29,88.913,62.18,31.349 +2020-10-07 12:45:00,104.27,89.20200000000001,62.18,31.349 +2020-10-07 13:00:00,103.01,88.75399999999999,62.23,31.349 +2020-10-07 13:15:00,102.19,88.63799999999999,62.23,31.349 +2020-10-07 13:30:00,102.91,87.77799999999999,62.23,31.349 +2020-10-07 13:45:00,103.83,87.34,62.23,31.349 +2020-10-07 14:00:00,104.04,86.73700000000001,63.721000000000004,31.349 +2020-10-07 14:15:00,103.67,87.09,63.721000000000004,31.349 +2020-10-07 14:30:00,102.78,86.399,63.721000000000004,31.349 +2020-10-07 14:45:00,102.93,86.258,63.721000000000004,31.349 +2020-10-07 15:00:00,104.88,86.205,66.523,31.349 +2020-10-07 15:15:00,108.17,85.874,66.523,31.349 +2020-10-07 15:30:00,106.16,86.321,66.523,31.349 +2020-10-07 15:45:00,106.9,86.355,66.523,31.349 +2020-10-07 16:00:00,110.44,86.19200000000001,69.679,31.349 +2020-10-07 16:15:00,110.4,85.70200000000001,69.679,31.349 +2020-10-07 16:30:00,112.36,86.374,69.679,31.349 +2020-10-07 16:45:00,114.77,84.734,69.679,31.349 +2020-10-07 17:00:00,117.12,84.92200000000001,75.04,31.349 +2020-10-07 17:15:00,116.68,86.22,75.04,31.349 +2020-10-07 17:30:00,121.96,85.959,75.04,31.349 +2020-10-07 17:45:00,123.54,86.70700000000001,75.04,31.349 +2020-10-07 18:00:00,125.9,86.336,75.915,31.349 +2020-10-07 18:15:00,122.45,86.101,75.915,31.349 +2020-10-07 18:30:00,122.89,85.197,75.915,31.349 +2020-10-07 18:45:00,119.42,88.81299999999999,75.915,31.349 +2020-10-07 19:00:00,114.78,89.26799999999999,74.66,31.349 +2020-10-07 19:15:00,112.06,88.11,74.66,31.349 +2020-10-07 19:30:00,107.91,87.527,74.66,31.349 +2020-10-07 19:45:00,108.91,87.464,74.66,31.349 +2020-10-07 20:00:00,101.7,86.845,71.204,31.349 +2020-10-07 20:15:00,104.07,84.595,71.204,31.349 +2020-10-07 20:30:00,101.57,83.637,71.204,31.349 +2020-10-07 20:45:00,99.17,82.454,71.204,31.349 +2020-10-07 21:00:00,94.53,80.454,61.052,31.349 +2020-10-07 21:15:00,94.88,80.96,61.052,31.349 +2020-10-07 21:30:00,96.49,80.123,61.052,31.349 +2020-10-07 21:45:00,95.82,78.357,61.052,31.349 +2020-10-07 22:00:00,91.81,75.411,54.691,31.349 +2020-10-07 22:15:00,85.9,73.115,54.691,31.349 +2020-10-07 22:30:00,83.52,62.748000000000005,54.691,31.349 +2020-10-07 22:45:00,79.93,57.597,54.691,31.349 +2020-10-07 23:00:00,76.11,52.993,45.18,31.349 +2020-10-07 23:15:00,76.08,52.548,45.18,31.349 +2020-10-07 23:30:00,75.38,52.023999999999994,45.18,31.349 +2020-10-07 23:45:00,79.99,51.966,45.18,31.349 +2020-10-08 00:00:00,79.69,50.172,42.746,31.349 +2020-10-08 00:15:00,79.41,51.321000000000005,42.746,31.349 +2020-10-08 00:30:00,77.72,51.048,42.746,31.349 +2020-10-08 00:45:00,77.1,50.92,42.746,31.349 +2020-10-08 01:00:00,77.16,51.248999999999995,40.025999999999996,31.349 +2020-10-08 01:15:00,79.65,50.761,40.025999999999996,31.349 +2020-10-08 01:30:00,78.99,50.083999999999996,40.025999999999996,31.349 +2020-10-08 01:45:00,73.79,49.59,40.025999999999996,31.349 +2020-10-08 02:00:00,79.29,50.065,38.154,31.349 +2020-10-08 02:15:00,80.26,50.193999999999996,38.154,31.349 +2020-10-08 02:30:00,78.95,50.676,38.154,31.349 +2020-10-08 02:45:00,76.08,51.33,38.154,31.349 +2020-10-08 03:00:00,75.33,53.449,37.575,31.349 +2020-10-08 03:15:00,82.42,54.321999999999996,37.575,31.349 +2020-10-08 03:30:00,84.01,54.4,37.575,31.349 +2020-10-08 03:45:00,86.07,55.028,37.575,31.349 +2020-10-08 04:00:00,84.0,63.916000000000004,39.154,31.349 +2020-10-08 04:15:00,90.23,72.76100000000001,39.154,31.349 +2020-10-08 04:30:00,94.69,72.051,39.154,31.349 +2020-10-08 04:45:00,97.5,73.727,39.154,31.349 +2020-10-08 05:00:00,97.92,99.507,44.085,31.349 +2020-10-08 05:15:00,101.71,122.09299999999999,44.085,31.349 +2020-10-08 05:30:00,107.92,116.074,44.085,31.349 +2020-10-08 05:45:00,111.57,108.126,44.085,31.349 +2020-10-08 06:00:00,116.78,107.995,57.49,31.349 +2020-10-08 06:15:00,114.86,111.68700000000001,57.49,31.349 +2020-10-08 06:30:00,114.18,110.105,57.49,31.349 +2020-10-08 06:45:00,118.63,110.81700000000001,57.49,31.349 +2020-10-08 07:00:00,120.92,111.065,73.617,31.349 +2020-10-08 07:15:00,119.12,113.374,73.617,31.349 +2020-10-08 07:30:00,117.33,113.219,73.617,31.349 +2020-10-08 07:45:00,116.58,113.415,73.617,31.349 +2020-10-08 08:00:00,115.2,111.869,69.281,31.349 +2020-10-08 08:15:00,114.49,111.969,69.281,31.349 +2020-10-08 08:30:00,113.74,109.76299999999999,69.281,31.349 +2020-10-08 08:45:00,113.82,108.845,69.281,31.349 +2020-10-08 09:00:00,111.25,104.189,63.926,31.349 +2020-10-08 09:15:00,110.51,101.995,63.926,31.349 +2020-10-08 09:30:00,110.18,102.336,63.926,31.349 +2020-10-08 09:45:00,110.35,102.079,63.926,31.349 +2020-10-08 10:00:00,108.45,98.833,59.442,31.349 +2020-10-08 10:15:00,109.67,98.12799999999999,59.442,31.349 +2020-10-08 10:30:00,108.62,97.05,59.442,31.349 +2020-10-08 10:45:00,107.19,96.867,59.442,31.349 +2020-10-08 11:00:00,105.31,93.634,56.771,31.349 +2020-10-08 11:15:00,106.63,94.209,56.771,31.349 +2020-10-08 11:30:00,106.21,94.40700000000001,56.771,31.349 +2020-10-08 11:45:00,108.79,94.072,56.771,31.349 +2020-10-08 12:00:00,101.41,90.34700000000001,53.701,31.349 +2020-10-08 12:15:00,101.81,89.917,53.701,31.349 +2020-10-08 12:30:00,99.91,89.323,53.701,31.349 +2020-10-08 12:45:00,100.67,89.609,53.701,31.349 +2020-10-08 13:00:00,99.91,89.12799999999999,52.364,31.349 +2020-10-08 13:15:00,100.85,89.016,52.364,31.349 +2020-10-08 13:30:00,99.77,88.156,52.364,31.349 +2020-10-08 13:45:00,102.06,87.71799999999999,52.364,31.349 +2020-10-08 14:00:00,101.66,87.06200000000001,53.419,31.349 +2020-10-08 14:15:00,102.04,87.431,53.419,31.349 +2020-10-08 14:30:00,101.37,86.77600000000001,53.419,31.349 +2020-10-08 14:45:00,102.03,86.632,53.419,31.349 +2020-10-08 15:00:00,102.38,86.545,56.744,31.349 +2020-10-08 15:15:00,106.15,86.235,56.744,31.349 +2020-10-08 15:30:00,105.14,86.71700000000001,56.744,31.349 +2020-10-08 15:45:00,106.58,86.766,56.744,31.349 +2020-10-08 16:00:00,108.41,86.568,60.458,31.349 +2020-10-08 16:15:00,109.23,86.09700000000001,60.458,31.349 +2020-10-08 16:30:00,111.33,86.765,60.458,31.349 +2020-10-08 16:45:00,112.69,85.179,60.458,31.349 +2020-10-08 17:00:00,117.22,85.32600000000001,66.295,31.349 +2020-10-08 17:15:00,114.98,86.646,66.295,31.349 +2020-10-08 17:30:00,121.72,86.387,66.295,31.349 +2020-10-08 17:45:00,122.79,87.156,66.295,31.349 +2020-10-08 18:00:00,124.63,86.772,68.468,31.349 +2020-10-08 18:15:00,124.71,86.521,68.468,31.349 +2020-10-08 18:30:00,121.3,85.62799999999999,68.468,31.349 +2020-10-08 18:45:00,120.86,89.238,68.468,31.349 +2020-10-08 19:00:00,121.84,89.704,66.39399999999999,31.349 +2020-10-08 19:15:00,118.99,88.54,66.39399999999999,31.349 +2020-10-08 19:30:00,110.25,87.949,66.39399999999999,31.349 +2020-10-08 19:45:00,112.82,87.868,66.39399999999999,31.349 +2020-10-08 20:00:00,104.39,87.273,63.183,31.349 +2020-10-08 20:15:00,104.11,85.01700000000001,63.183,31.349 +2020-10-08 20:30:00,96.44,84.031,63.183,31.349 +2020-10-08 20:45:00,97.74,82.82,63.183,31.349 +2020-10-08 21:00:00,95.47,80.82,55.133,31.349 +2020-10-08 21:15:00,94.29,81.316,55.133,31.349 +2020-10-08 21:30:00,88.11,80.48899999999999,55.133,31.349 +2020-10-08 21:45:00,94.31,78.695,55.133,31.349 +2020-10-08 22:00:00,86.54,75.735,50.111999999999995,31.349 +2020-10-08 22:15:00,84.64,73.417,50.111999999999995,31.349 +2020-10-08 22:30:00,84.73,63.065,50.111999999999995,31.349 +2020-10-08 22:45:00,80.12,57.919,50.111999999999995,31.349 +2020-10-08 23:00:00,82.03,53.343,44.536,31.349 +2020-10-08 23:15:00,83.35,52.867,44.536,31.349 +2020-10-08 23:30:00,82.16,52.345,44.536,31.349 +2020-10-08 23:45:00,78.69,52.281000000000006,44.536,31.349 +2020-10-09 00:00:00,78.1,49.02,42.291000000000004,31.349 +2020-10-09 00:15:00,79.85,50.364,42.291000000000004,31.349 +2020-10-09 00:30:00,79.77,50.202,42.291000000000004,31.349 +2020-10-09 00:45:00,76.69,50.361999999999995,42.291000000000004,31.349 +2020-10-09 01:00:00,73.43,50.349,41.008,31.349 +2020-10-09 01:15:00,80.07,49.895,41.008,31.349 +2020-10-09 01:30:00,78.35,49.56100000000001,41.008,31.349 +2020-10-09 01:45:00,75.18,48.961999999999996,41.008,31.349 +2020-10-09 02:00:00,76.68,50.022,39.521,31.349 +2020-10-09 02:15:00,74.18,50.09,39.521,31.349 +2020-10-09 02:30:00,79.65,51.242,39.521,31.349 +2020-10-09 02:45:00,79.83,51.511,39.521,31.349 +2020-10-09 03:00:00,80.63,53.677,39.812,31.349 +2020-10-09 03:15:00,76.92,54.235,39.812,31.349 +2020-10-09 03:30:00,78.86,54.17,39.812,31.349 +2020-10-09 03:45:00,81.09,55.42,39.812,31.349 +2020-10-09 04:00:00,87.69,64.506,41.22,31.349 +2020-10-09 04:15:00,90.33,72.376,41.22,31.349 +2020-10-09 04:30:00,89.24,72.312,41.22,31.349 +2020-10-09 04:45:00,95.09,73.171,41.22,31.349 +2020-10-09 05:00:00,107.98,98.257,45.115,31.349 +2020-10-09 05:15:00,110.72,122.072,45.115,31.349 +2020-10-09 05:30:00,111.56,116.56299999999999,45.115,31.349 +2020-10-09 05:45:00,111.91,108.28200000000001,45.115,31.349 +2020-10-09 06:00:00,119.82,108.445,59.06100000000001,31.349 +2020-10-09 06:15:00,118.17,111.68,59.06100000000001,31.349 +2020-10-09 06:30:00,118.58,109.745,59.06100000000001,31.349 +2020-10-09 06:45:00,120.01,111.014,59.06100000000001,31.349 +2020-10-09 07:00:00,122.14,111.315,71.874,31.349 +2020-10-09 07:15:00,120.54,114.571,71.874,31.349 +2020-10-09 07:30:00,118.81,113.26100000000001,71.874,31.349 +2020-10-09 07:45:00,118.12,112.98,71.874,31.349 +2020-10-09 08:00:00,116.07,111.43299999999999,68.439,31.349 +2020-10-09 08:15:00,113.32,111.71600000000001,68.439,31.349 +2020-10-09 08:30:00,113.35,109.887,68.439,31.349 +2020-10-09 08:45:00,116.44,108.208,68.439,31.349 +2020-10-09 09:00:00,120.09,102.461,65.523,31.349 +2020-10-09 09:15:00,119.56,101.556,65.523,31.349 +2020-10-09 09:30:00,119.2,101.32,65.523,31.349 +2020-10-09 09:45:00,120.05,101.2,65.523,31.349 +2020-10-09 10:00:00,118.05,97.335,62.005,31.349 +2020-10-09 10:15:00,120.4,96.78399999999999,62.005,31.349 +2020-10-09 10:30:00,117.17,95.949,62.005,31.349 +2020-10-09 10:45:00,113.01,95.501,62.005,31.349 +2020-10-09 11:00:00,109.41,92.38600000000001,60.351000000000006,31.349 +2020-10-09 11:15:00,112.78,91.969,60.351000000000006,31.349 +2020-10-09 11:30:00,116.56,92.76799999999999,60.351000000000006,31.349 +2020-10-09 11:45:00,115.51,91.935,60.351000000000006,31.349 +2020-10-09 12:00:00,112.96,88.92399999999999,55.331,31.349 +2020-10-09 12:15:00,112.0,87.18700000000001,55.331,31.349 +2020-10-09 12:30:00,109.13,86.762,55.331,31.349 +2020-10-09 12:45:00,106.59,86.881,55.331,31.349 +2020-10-09 13:00:00,98.34,87.105,53.361999999999995,31.349 +2020-10-09 13:15:00,98.84,87.469,53.361999999999995,31.349 +2020-10-09 13:30:00,99.07,87.02,53.361999999999995,31.349 +2020-10-09 13:45:00,99.93,86.71,53.361999999999995,31.349 +2020-10-09 14:00:00,99.24,85.06299999999999,51.708,31.349 +2020-10-09 14:15:00,99.81,85.588,51.708,31.349 +2020-10-09 14:30:00,100.03,85.976,51.708,31.349 +2020-10-09 14:45:00,101.58,85.617,51.708,31.349 +2020-10-09 15:00:00,101.74,85.23100000000001,54.571000000000005,31.349 +2020-10-09 15:15:00,104.75,84.615,54.571000000000005,31.349 +2020-10-09 15:30:00,101.76,84.139,54.571000000000005,31.349 +2020-10-09 15:45:00,104.43,84.66,54.571000000000005,31.349 +2020-10-09 16:00:00,106.95,83.49,58.662,31.349 +2020-10-09 16:15:00,108.48,83.429,58.662,31.349 +2020-10-09 16:30:00,109.76,84.04,58.662,31.349 +2020-10-09 16:45:00,111.36,82.041,58.662,31.349 +2020-10-09 17:00:00,113.98,83.177,65.941,31.349 +2020-10-09 17:15:00,116.0,84.243,65.941,31.349 +2020-10-09 17:30:00,116.08,83.93700000000001,65.941,31.349 +2020-10-09 17:45:00,120.05,84.536,65.941,31.349 +2020-10-09 18:00:00,122.43,84.505,65.628,31.349 +2020-10-09 18:15:00,118.95,83.552,65.628,31.349 +2020-10-09 18:30:00,120.14,82.779,65.628,31.349 +2020-10-09 18:45:00,116.46,86.61200000000001,65.628,31.349 +2020-10-09 19:00:00,110.08,87.984,63.662,31.349 +2020-10-09 19:15:00,107.3,87.711,63.662,31.349 +2020-10-09 19:30:00,102.67,86.975,63.662,31.349 +2020-10-09 19:45:00,102.52,86.115,63.662,31.349 +2020-10-09 20:00:00,94.57,85.47399999999999,61.945,31.349 +2020-10-09 20:15:00,92.41,83.66,61.945,31.349 +2020-10-09 20:30:00,93.56,82.355,61.945,31.349 +2020-10-09 20:45:00,88.21,80.892,61.945,31.349 +2020-10-09 21:00:00,83.31,79.843,53.903,31.349 +2020-10-09 21:15:00,83.13,81.453,53.903,31.349 +2020-10-09 21:30:00,83.95,80.569,53.903,31.349 +2020-10-09 21:45:00,86.18,79.085,53.903,31.349 +2020-10-09 22:00:00,80.94,76.436,48.403999999999996,31.349 +2020-10-09 22:15:00,79.92,73.9,48.403999999999996,31.349 +2020-10-09 22:30:00,73.84,68.861,48.403999999999996,31.349 +2020-10-09 22:45:00,69.3,65.65100000000001,48.403999999999996,31.349 +2020-10-09 23:00:00,66.03,61.873999999999995,41.07,31.349 +2020-10-09 23:15:00,66.63,59.684,41.07,31.349 +2020-10-09 23:30:00,71.33,57.535,41.07,31.349 +2020-10-09 23:45:00,72.04,57.123999999999995,41.07,31.349 +2020-10-10 00:00:00,68.05,48.924,38.989000000000004,31.177 +2020-10-10 00:15:00,66.03,48.18899999999999,38.989000000000004,31.177 +2020-10-10 00:30:00,63.47,48.287,38.989000000000004,31.177 +2020-10-10 00:45:00,70.67,48.302,38.989000000000004,31.177 +2020-10-10 01:00:00,68.97,48.708,35.275,31.177 +2020-10-10 01:15:00,68.98,48.229,35.275,31.177 +2020-10-10 01:30:00,62.64,47.24100000000001,35.275,31.177 +2020-10-10 01:45:00,63.19,47.28,35.275,31.177 +2020-10-10 02:00:00,60.49,48.051,32.838,31.177 +2020-10-10 02:15:00,67.91,47.53,32.838,31.177 +2020-10-10 02:30:00,68.09,47.768,32.838,31.177 +2020-10-10 02:45:00,61.18,48.528999999999996,32.838,31.177 +2020-10-10 03:00:00,60.57,50.135,32.418,31.177 +2020-10-10 03:15:00,60.96,49.832,32.418,31.177 +2020-10-10 03:30:00,60.75,49.342,32.418,31.177 +2020-10-10 03:45:00,61.33,51.481,32.418,31.177 +2020-10-10 04:00:00,63.26,57.9,32.099000000000004,31.177 +2020-10-10 04:15:00,62.9,64.293,32.099000000000004,31.177 +2020-10-10 04:30:00,62.36,62.413000000000004,32.099000000000004,31.177 +2020-10-10 04:45:00,64.21,63.248000000000005,32.099000000000004,31.177 +2020-10-10 05:00:00,67.64,77.72,32.926,31.177 +2020-10-10 05:15:00,68.34,87.98899999999999,32.926,31.177 +2020-10-10 05:30:00,67.96,83.508,32.926,31.177 +2020-10-10 05:45:00,69.98,79.889,32.926,31.177 +2020-10-10 06:00:00,70.7,93.516,35.069,31.177 +2020-10-10 06:15:00,73.22,107.594,35.069,31.177 +2020-10-10 06:30:00,74.31,101.949,35.069,31.177 +2020-10-10 06:45:00,77.52,97.90799999999999,35.069,31.177 +2020-10-10 07:00:00,79.57,95.572,40.906,31.177 +2020-10-10 07:15:00,80.64,97.601,40.906,31.177 +2020-10-10 07:30:00,80.5,98.264,40.906,31.177 +2020-10-10 07:45:00,81.73,100.09,40.906,31.177 +2020-10-10 08:00:00,80.79,100.335,46.603,31.177 +2020-10-10 08:15:00,80.92,101.959,46.603,31.177 +2020-10-10 08:30:00,80.69,100.809,46.603,31.177 +2020-10-10 08:45:00,79.3,100.93,46.603,31.177 +2020-10-10 09:00:00,80.93,97.544,49.935,31.177 +2020-10-10 09:15:00,78.7,97.215,49.935,31.177 +2020-10-10 09:30:00,77.39,97.61399999999999,49.935,31.177 +2020-10-10 09:45:00,77.01,97.289,49.935,31.177 +2020-10-10 10:00:00,75.49,93.726,47.585,31.177 +2020-10-10 10:15:00,76.27,93.39399999999999,47.585,31.177 +2020-10-10 10:30:00,75.48,92.43299999999999,47.585,31.177 +2020-10-10 10:45:00,75.32,92.353,47.585,31.177 +2020-10-10 11:00:00,73.13,89.22200000000001,43.376999999999995,31.177 +2020-10-10 11:15:00,72.55,88.932,43.376999999999995,31.177 +2020-10-10 11:30:00,69.49,89.382,43.376999999999995,31.177 +2020-10-10 11:45:00,71.73,88.456,43.376999999999995,31.177 +2020-10-10 12:00:00,67.21,85.07799999999999,40.855,31.177 +2020-10-10 12:15:00,65.23,84.057,40.855,31.177 +2020-10-10 12:30:00,64.52,83.74700000000001,40.855,31.177 +2020-10-10 12:45:00,64.43,83.87899999999999,40.855,31.177 +2020-10-10 13:00:00,62.06,83.459,37.251,31.177 +2020-10-10 13:15:00,63.54,82.611,37.251,31.177 +2020-10-10 13:30:00,64.24,82.051,37.251,31.177 +2020-10-10 13:45:00,65.9,81.27199999999999,37.251,31.177 +2020-10-10 14:00:00,65.04,80.149,38.548,31.177 +2020-10-10 14:15:00,65.98,79.81,38.548,31.177 +2020-10-10 14:30:00,67.11,79.18,38.548,31.177 +2020-10-10 14:45:00,68.17,79.163,38.548,31.177 +2020-10-10 15:00:00,68.93,79.266,42.883,31.177 +2020-10-10 15:15:00,68.68,79.398,42.883,31.177 +2020-10-10 15:30:00,70.26,79.771,42.883,31.177 +2020-10-10 15:45:00,73.92,79.848,42.883,31.177 +2020-10-10 16:00:00,76.34,79.433,48.143,31.177 +2020-10-10 16:15:00,80.43,79.315,48.143,31.177 +2020-10-10 16:30:00,82.57,80.018,48.143,31.177 +2020-10-10 16:45:00,81.96,78.45,48.143,31.177 +2020-10-10 17:00:00,87.07,78.757,55.25,31.177 +2020-10-10 17:15:00,85.61,79.41,55.25,31.177 +2020-10-10 17:30:00,93.1,78.993,55.25,31.177 +2020-10-10 17:45:00,93.78,79.703,55.25,31.177 +2020-10-10 18:00:00,94.81,80.311,57.506,31.177 +2020-10-10 18:15:00,93.4,81.016,57.506,31.177 +2020-10-10 18:30:00,95.37,81.557,57.506,31.177 +2020-10-10 18:45:00,92.73,82.074,57.506,31.177 +2020-10-10 19:00:00,86.72,83.03299999999999,55.528999999999996,31.177 +2020-10-10 19:15:00,83.84,81.98700000000001,55.528999999999996,31.177 +2020-10-10 19:30:00,81.98,81.98100000000001,55.528999999999996,31.177 +2020-10-10 19:45:00,80.54,82.01799999999999,55.528999999999996,31.177 +2020-10-10 20:00:00,77.04,82.738,46.166000000000004,31.177 +2020-10-10 20:15:00,76.13,81.439,46.166000000000004,31.177 +2020-10-10 20:30:00,75.51,79.47399999999999,46.166000000000004,31.177 +2020-10-10 20:45:00,73.93,78.848,46.166000000000004,31.177 +2020-10-10 21:00:00,71.35,77.916,40.406,31.177 +2020-10-10 21:15:00,70.87,79.498,40.406,31.177 +2020-10-10 21:30:00,66.41,79.23100000000001,40.406,31.177 +2020-10-10 21:45:00,65.71,77.248,40.406,31.177 +2020-10-10 22:00:00,61.42,75.122,39.616,31.177 +2020-10-10 22:15:00,62.07,73.71600000000001,39.616,31.177 +2020-10-10 22:30:00,59.56,71.013,39.616,31.177 +2020-10-10 22:45:00,58.47,68.78,39.616,31.177 +2020-10-10 23:00:00,54.74,65.638,32.205,31.177 +2020-10-10 23:15:00,54.08,62.986999999999995,32.205,31.177 +2020-10-10 23:30:00,55.19,61.508,32.205,31.177 +2020-10-10 23:45:00,53.38,60.303999999999995,32.205,31.177 +2020-10-11 00:00:00,50.93,50.198,28.229,31.177 +2020-10-11 00:15:00,51.94,48.662,28.229,31.177 +2020-10-11 00:30:00,50.97,48.536,28.229,31.177 +2020-10-11 00:45:00,51.87,48.757,28.229,31.177 +2020-10-11 01:00:00,49.67,49.263999999999996,25.669,31.177 +2020-10-11 01:15:00,51.23,49.141000000000005,25.669,31.177 +2020-10-11 01:30:00,50.66,48.294,25.669,31.177 +2020-10-11 01:45:00,51.05,47.988,25.669,31.177 +2020-10-11 02:00:00,51.26,48.52,24.948,31.177 +2020-10-11 02:15:00,50.24,48.092,24.948,31.177 +2020-10-11 02:30:00,49.75,48.754,24.948,31.177 +2020-10-11 02:45:00,49.74,49.521,24.948,31.177 +2020-10-11 03:00:00,49.25,51.614,24.445,31.177 +2020-10-11 03:15:00,50.7,51.281000000000006,24.445,31.177 +2020-10-11 03:30:00,51.34,50.907,24.445,31.177 +2020-10-11 03:45:00,51.61,52.536,24.445,31.177 +2020-10-11 04:00:00,52.83,58.86600000000001,25.839000000000002,31.177 +2020-10-11 04:15:00,53.93,64.65,25.839000000000002,31.177 +2020-10-11 04:30:00,53.84,63.577,25.839000000000002,31.177 +2020-10-11 04:45:00,54.0,64.253,25.839000000000002,31.177 +2020-10-11 05:00:00,56.26,77.597,26.803,31.177 +2020-10-11 05:15:00,56.25,86.6,26.803,31.177 +2020-10-11 05:30:00,56.59,81.8,26.803,31.177 +2020-10-11 05:45:00,54.83,78.065,26.803,31.177 +2020-10-11 06:00:00,60.32,90.211,28.147,31.177 +2020-10-11 06:15:00,59.82,104.145,28.147,31.177 +2020-10-11 06:30:00,62.29,97.684,28.147,31.177 +2020-10-11 06:45:00,63.34,92.662,28.147,31.177 +2020-10-11 07:00:00,65.28,91.40100000000001,31.116,31.177 +2020-10-11 07:15:00,66.84,92.166,31.116,31.177 +2020-10-11 07:30:00,67.06,93.11200000000001,31.116,31.177 +2020-10-11 07:45:00,67.82,94.62100000000001,31.116,31.177 +2020-10-11 08:00:00,66.65,95.993,35.739000000000004,31.177 +2020-10-11 08:15:00,66.05,98.213,35.739000000000004,31.177 +2020-10-11 08:30:00,64.97,98.24600000000001,35.739000000000004,31.177 +2020-10-11 08:45:00,64.71,99.119,35.739000000000004,31.177 +2020-10-11 09:00:00,62.3,95.48899999999999,39.455999999999996,31.177 +2020-10-11 09:15:00,61.87,95.111,39.455999999999996,31.177 +2020-10-11 09:30:00,61.94,95.667,39.455999999999996,31.177 +2020-10-11 09:45:00,63.26,95.81700000000001,39.455999999999996,31.177 +2020-10-11 10:00:00,63.21,93.579,41.343999999999994,31.177 +2020-10-11 10:15:00,63.71,93.508,41.343999999999994,31.177 +2020-10-11 10:30:00,64.26,92.906,41.343999999999994,31.177 +2020-10-11 10:45:00,63.77,92.535,41.343999999999994,31.177 +2020-10-11 11:00:00,60.7,89.61200000000001,43.645,31.177 +2020-10-11 11:15:00,59.98,89.12700000000001,43.645,31.177 +2020-10-11 11:30:00,57.95,89.469,43.645,31.177 +2020-10-11 11:45:00,60.21,88.926,43.645,31.177 +2020-10-11 12:00:00,56.47,85.79,39.796,31.177 +2020-10-11 12:15:00,53.94,85.294,39.796,31.177 +2020-10-11 12:30:00,52.59,84.475,39.796,31.177 +2020-10-11 12:45:00,54.32,83.84200000000001,39.796,31.177 +2020-10-11 13:00:00,49.62,82.914,36.343,31.177 +2020-10-11 13:15:00,51.08,83.12700000000001,36.343,31.177 +2020-10-11 13:30:00,51.24,81.898,36.343,31.177 +2020-10-11 13:45:00,52.47,81.398,36.343,31.177 +2020-10-11 14:00:00,52.86,80.949,33.162,31.177 +2020-10-11 14:15:00,54.18,81.385,33.162,31.177 +2020-10-11 14:30:00,54.72,80.714,33.162,31.177 +2020-10-11 14:45:00,57.08,79.959,33.162,31.177 +2020-10-11 15:00:00,61.5,79.414,33.215,31.177 +2020-10-11 15:15:00,61.01,79.492,33.215,31.177 +2020-10-11 15:30:00,61.52,80.07300000000001,33.215,31.177 +2020-10-11 15:45:00,64.89,80.623,33.215,31.177 +2020-10-11 16:00:00,69.86,80.05,37.385999999999996,31.177 +2020-10-11 16:15:00,73.5,79.68,37.385999999999996,31.177 +2020-10-11 16:30:00,72.54,81.023,37.385999999999996,31.177 +2020-10-11 16:45:00,77.12,79.60300000000001,37.385999999999996,31.177 +2020-10-11 17:00:00,81.3,80.021,46.618,31.177 +2020-10-11 17:15:00,82.78,81.369,46.618,31.177 +2020-10-11 17:30:00,89.15,81.513,46.618,31.177 +2020-10-11 17:45:00,88.39,83.434,46.618,31.177 +2020-10-11 18:00:00,91.86,84.07700000000001,50.111000000000004,31.177 +2020-10-11 18:15:00,89.54,85.089,50.111000000000004,31.177 +2020-10-11 18:30:00,91.48,84.645,50.111000000000004,31.177 +2020-10-11 18:45:00,86.46,85.98299999999999,50.111000000000004,31.177 +2020-10-11 19:00:00,81.58,87.99,50.25,31.177 +2020-10-11 19:15:00,87.47,86.554,50.25,31.177 +2020-10-11 19:30:00,87.65,86.333,50.25,31.177 +2020-10-11 19:45:00,84.98,86.675,50.25,31.177 +2020-10-11 20:00:00,82.24,87.521,44.265,31.177 +2020-10-11 20:15:00,88.55,86.537,44.265,31.177 +2020-10-11 20:30:00,87.01,85.499,44.265,31.177 +2020-10-11 20:45:00,88.49,83.41799999999999,44.265,31.177 +2020-10-11 21:00:00,79.77,81.342,39.717,31.177 +2020-10-11 21:15:00,77.18,82.494,39.717,31.177 +2020-10-11 21:30:00,75.52,81.98899999999999,39.717,31.177 +2020-10-11 21:45:00,74.79,80.236,39.717,31.177 +2020-10-11 22:00:00,70.87,78.767,39.224000000000004,31.177 +2020-10-11 22:15:00,77.05,76.087,39.224000000000004,31.177 +2020-10-11 22:30:00,76.79,71.846,39.224000000000004,31.177 +2020-10-11 22:45:00,77.47,68.562,39.224000000000004,31.177 +2020-10-11 23:00:00,67.29,64.157,33.518,31.177 +2020-10-11 23:15:00,67.19,62.933,33.518,31.177 +2020-10-11 23:30:00,72.95,61.476000000000006,33.518,31.177 +2020-10-11 23:45:00,73.21,60.711999999999996,33.518,31.177 +2020-10-12 00:00:00,70.94,53.006,34.301,31.349 +2020-10-12 00:15:00,68.42,53.067,34.301,31.349 +2020-10-12 00:30:00,68.11,52.773999999999994,34.301,31.349 +2020-10-12 00:45:00,72.11,52.558,34.301,31.349 +2020-10-12 01:00:00,70.36,53.288999999999994,34.143,31.349 +2020-10-12 01:15:00,68.12,52.99100000000001,34.143,31.349 +2020-10-12 01:30:00,65.37,52.393,34.143,31.349 +2020-10-12 01:45:00,69.82,52.073,34.143,31.349 +2020-10-12 02:00:00,69.15,52.861999999999995,33.650999999999996,31.349 +2020-10-12 02:15:00,70.12,52.422,33.650999999999996,31.349 +2020-10-12 02:30:00,66.23,53.29,33.650999999999996,31.349 +2020-10-12 02:45:00,70.36,53.729,33.650999999999996,31.349 +2020-10-12 03:00:00,72.15,56.57899999999999,32.599000000000004,31.349 +2020-10-12 03:15:00,72.16,57.313,32.599000000000004,31.349 +2020-10-12 03:30:00,69.17,57.236000000000004,32.599000000000004,31.349 +2020-10-12 03:45:00,76.19,58.367,32.599000000000004,31.349 +2020-10-12 04:00:00,82.36,68.183,33.785,31.349 +2020-10-12 04:15:00,85.74,77.285,33.785,31.349 +2020-10-12 04:30:00,84.49,76.767,33.785,31.349 +2020-10-12 04:45:00,89.3,77.73,33.785,31.349 +2020-10-12 05:00:00,101.52,101.051,41.285,31.349 +2020-10-12 05:15:00,106.64,123.691,41.285,31.349 +2020-10-12 05:30:00,106.95,118.056,41.285,31.349 +2020-10-12 05:45:00,106.85,110.37,41.285,31.349 +2020-10-12 06:00:00,115.15,109.96799999999999,60.486000000000004,31.349 +2020-10-12 06:15:00,115.95,113.00200000000001,60.486000000000004,31.349 +2020-10-12 06:30:00,115.89,111.90100000000001,60.486000000000004,31.349 +2020-10-12 06:45:00,118.2,113.29,60.486000000000004,31.349 +2020-10-12 07:00:00,121.53,113.51899999999999,74.012,31.349 +2020-10-12 07:15:00,122.2,116.09200000000001,74.012,31.349 +2020-10-12 07:30:00,118.03,116.274,74.012,31.349 +2020-10-12 07:45:00,116.52,116.95,74.012,31.349 +2020-10-12 08:00:00,114.57,115.436,69.569,31.349 +2020-10-12 08:15:00,114.1,116.088,69.569,31.349 +2020-10-12 08:30:00,114.31,113.955,69.569,31.349 +2020-10-12 08:45:00,116.52,113.573,69.569,31.349 +2020-10-12 09:00:00,111.0,109.07,66.152,31.349 +2020-10-12 09:15:00,110.6,106.264,66.152,31.349 +2020-10-12 09:30:00,110.4,105.881,66.152,31.349 +2020-10-12 09:45:00,110.21,104.867,66.152,31.349 +2020-10-12 10:00:00,109.5,102.59200000000001,62.923,31.349 +2020-10-12 10:15:00,116.29,102.26299999999999,62.923,31.349 +2020-10-12 10:30:00,112.61,101.021,62.923,31.349 +2020-10-12 10:45:00,114.5,100.115,62.923,31.349 +2020-10-12 11:00:00,111.52,96.26700000000001,61.522,31.349 +2020-10-12 11:15:00,115.75,96.70100000000001,61.522,31.349 +2020-10-12 11:30:00,110.56,98.014,61.522,31.349 +2020-10-12 11:45:00,105.29,97.54,61.522,31.349 +2020-10-12 12:00:00,103.06,94.429,58.632,31.349 +2020-10-12 12:15:00,103.27,93.98200000000001,58.632,31.349 +2020-10-12 12:30:00,99.5,92.764,58.632,31.349 +2020-10-12 12:45:00,102.59,92.81299999999999,58.632,31.349 +2020-10-12 13:00:00,103.02,92.575,59.06,31.349 +2020-10-12 13:15:00,104.16,91.713,59.06,31.349 +2020-10-12 13:30:00,101.81,90.331,59.06,31.349 +2020-10-12 13:45:00,102.12,90.304,59.06,31.349 +2020-10-12 14:00:00,104.85,89.085,59.791000000000004,31.349 +2020-10-12 14:15:00,111.72,89.54899999999999,59.791000000000004,31.349 +2020-10-12 14:30:00,108.5,88.586,59.791000000000004,31.349 +2020-10-12 14:45:00,107.1,88.87,59.791000000000004,31.349 +2020-10-12 15:00:00,107.33,88.921,61.148,31.349 +2020-10-12 15:15:00,107.86,88.06700000000001,61.148,31.349 +2020-10-12 15:30:00,108.23,88.7,61.148,31.349 +2020-10-12 15:45:00,110.3,88.81700000000001,61.148,31.349 +2020-10-12 16:00:00,111.96,88.654,66.009,31.349 +2020-10-12 16:15:00,114.55,88.006,66.009,31.349 +2020-10-12 16:30:00,114.39,88.575,66.009,31.349 +2020-10-12 16:45:00,116.45,86.70299999999999,66.009,31.349 +2020-10-12 17:00:00,117.5,86.335,73.683,31.349 +2020-10-12 17:15:00,116.77,87.499,73.683,31.349 +2020-10-12 17:30:00,118.94,87.204,73.683,31.349 +2020-10-12 17:45:00,119.82,88.29700000000001,73.683,31.349 +2020-10-12 18:00:00,121.77,88.435,72.848,31.349 +2020-10-12 18:15:00,121.95,87.51100000000001,72.848,31.349 +2020-10-12 18:30:00,119.5,86.93299999999999,72.848,31.349 +2020-10-12 18:45:00,116.12,90.29799999999999,72.848,31.349 +2020-10-12 19:00:00,110.92,91.43799999999999,71.139,31.349 +2020-10-12 19:15:00,106.85,90.26799999999999,71.139,31.349 +2020-10-12 19:30:00,103.85,90.03200000000001,71.139,31.349 +2020-10-12 19:45:00,107.99,89.679,71.139,31.349 +2020-10-12 20:00:00,105.84,88.89,69.667,31.349 +2020-10-12 20:15:00,105.89,87.679,69.667,31.349 +2020-10-12 20:30:00,98.58,86.147,69.667,31.349 +2020-10-12 20:45:00,96.33,84.819,69.667,31.349 +2020-10-12 21:00:00,91.25,82.61399999999999,61.166000000000004,31.349 +2020-10-12 21:15:00,90.87,83.53299999999999,61.166000000000004,31.349 +2020-10-12 21:30:00,91.81,82.939,61.166000000000004,31.349 +2020-10-12 21:45:00,92.26,80.832,61.166000000000004,31.349 +2020-10-12 22:00:00,88.19,77.041,52.772,31.349 +2020-10-12 22:15:00,87.73,74.929,52.772,31.349 +2020-10-12 22:30:00,87.11,64.431,52.772,31.349 +2020-10-12 22:45:00,87.25,59.202,52.772,31.349 +2020-10-12 23:00:00,79.67,55.16,45.136,31.349 +2020-10-12 23:15:00,78.21,54.04,45.136,31.349 +2020-10-12 23:30:00,80.76,53.67,45.136,31.349 +2020-10-12 23:45:00,81.41,53.657,45.136,31.349 +2020-10-13 00:00:00,73.81,51.739,47.35,31.349 +2020-10-13 00:15:00,72.3,52.842,47.35,31.349 +2020-10-13 00:30:00,74.12,52.604,47.35,31.349 +2020-10-13 00:45:00,77.97,52.465,47.35,31.349 +2020-10-13 01:00:00,75.1,52.812,43.424,31.349 +2020-10-13 01:15:00,74.15,52.428000000000004,43.424,31.349 +2020-10-13 01:30:00,75.85,51.838,43.424,31.349 +2020-10-13 01:45:00,77.72,51.335,43.424,31.349 +2020-10-13 02:00:00,77.58,51.851000000000006,41.778999999999996,31.349 +2020-10-13 02:15:00,75.84,52.056000000000004,41.778999999999996,31.349 +2020-10-13 02:30:00,75.85,52.452,41.778999999999996,31.349 +2020-10-13 02:45:00,75.3,53.08,41.778999999999996,31.349 +2020-10-13 03:00:00,79.74,55.13399999999999,40.771,31.349 +2020-10-13 03:15:00,78.15,56.108000000000004,40.771,31.349 +2020-10-13 03:30:00,75.52,56.201,40.771,31.349 +2020-10-13 03:45:00,75.48,56.739,40.771,31.349 +2020-10-13 04:00:00,77.47,65.77199999999999,41.816,31.349 +2020-10-13 04:15:00,82.06,74.781,41.816,31.349 +2020-10-13 04:30:00,90.51,74.07600000000001,41.816,31.349 +2020-10-13 04:45:00,97.73,75.794,41.816,31.349 +2020-10-13 05:00:00,105.85,102.00200000000001,45.842,31.349 +2020-10-13 05:15:00,102.75,125.023,45.842,31.349 +2020-10-13 05:30:00,106.37,118.916,45.842,31.349 +2020-10-13 05:45:00,110.98,110.75399999999999,45.842,31.349 +2020-10-13 06:00:00,119.85,110.51,59.12,31.349 +2020-10-13 06:15:00,119.21,114.292,59.12,31.349 +2020-10-13 06:30:00,118.8,112.76899999999999,59.12,31.349 +2020-10-13 06:45:00,121.82,113.495,59.12,31.349 +2020-10-13 07:00:00,123.79,113.73899999999999,70.33,31.349 +2020-10-13 07:15:00,123.79,116.11399999999999,70.33,31.349 +2020-10-13 07:30:00,123.33,116.124,70.33,31.349 +2020-10-13 07:45:00,123.65,116.325,70.33,31.349 +2020-10-13 08:00:00,123.2,114.829,67.788,31.349 +2020-10-13 08:15:00,123.26,114.786,67.788,31.349 +2020-10-13 08:30:00,124.04,112.699,67.788,31.349 +2020-10-13 08:45:00,124.33,111.66799999999999,67.788,31.349 +2020-10-13 09:00:00,122.91,107.008,62.622,31.349 +2020-10-13 09:15:00,121.43,104.785,62.622,31.349 +2020-10-13 09:30:00,119.64,105.04799999999999,62.622,31.349 +2020-10-13 09:45:00,119.0,104.652,62.622,31.349 +2020-10-13 10:00:00,118.75,101.374,60.887,31.349 +2020-10-13 10:15:00,118.55,100.47,60.887,31.349 +2020-10-13 10:30:00,116.94,99.303,60.887,31.349 +2020-10-13 10:45:00,119.41,99.036,60.887,31.349 +2020-10-13 11:00:00,113.99,95.845,59.812,31.349 +2020-10-13 11:15:00,112.18,96.32600000000001,59.812,31.349 +2020-10-13 11:30:00,114.67,96.523,59.812,31.349 +2020-10-13 11:45:00,109.79,96.10600000000001,59.812,31.349 +2020-10-13 12:00:00,105.65,92.26,56.614,31.349 +2020-10-13 12:15:00,104.47,91.789,56.614,31.349 +2020-10-13 12:30:00,103.49,91.37299999999999,56.614,31.349 +2020-10-13 12:45:00,102.45,91.63799999999999,56.614,31.349 +2020-10-13 13:00:00,102.46,91.0,56.824,31.349 +2020-10-13 13:15:00,105.71,90.90700000000001,56.824,31.349 +2020-10-13 13:30:00,106.9,90.038,56.824,31.349 +2020-10-13 13:45:00,109.76,89.601,56.824,31.349 +2020-10-13 14:00:00,110.65,88.68700000000001,57.623999999999995,31.349 +2020-10-13 14:15:00,111.37,89.135,57.623999999999995,31.349 +2020-10-13 14:30:00,111.33,88.661,57.623999999999995,31.349 +2020-10-13 14:45:00,111.96,88.50200000000001,57.623999999999995,31.349 +2020-10-13 15:00:00,111.31,88.243,59.724,31.349 +2020-10-13 15:15:00,107.85,88.03,59.724,31.349 +2020-10-13 15:30:00,108.03,88.694,59.724,31.349 +2020-10-13 15:45:00,111.58,88.821,59.724,31.349 +2020-10-13 16:00:00,111.54,88.443,61.64,31.349 +2020-10-13 16:15:00,111.16,88.069,61.64,31.349 +2020-10-13 16:30:00,113.67,88.71700000000001,61.64,31.349 +2020-10-13 16:45:00,116.95,87.404,61.64,31.349 +2020-10-13 17:00:00,118.92,87.344,68.962,31.349 +2020-10-13 17:15:00,119.99,88.766,68.962,31.349 +2020-10-13 17:30:00,122.98,88.524,68.962,31.349 +2020-10-13 17:45:00,123.02,89.40299999999999,68.962,31.349 +2020-10-13 18:00:00,123.95,88.95,69.149,31.349 +2020-10-13 18:15:00,123.41,88.626,69.149,31.349 +2020-10-13 18:30:00,120.16,87.787,69.149,31.349 +2020-10-13 18:45:00,119.43,91.367,69.149,31.349 +2020-10-13 19:00:00,114.42,91.887,68.832,31.349 +2020-10-13 19:15:00,112.4,90.699,68.832,31.349 +2020-10-13 19:30:00,115.3,90.065,68.832,31.349 +2020-10-13 19:45:00,110.17,89.897,68.832,31.349 +2020-10-13 20:00:00,100.53,89.419,66.403,31.349 +2020-10-13 20:15:00,101.98,87.134,66.403,31.349 +2020-10-13 20:30:00,100.14,86.008,66.403,31.349 +2020-10-13 20:45:00,100.17,84.654,66.403,31.349 +2020-10-13 21:00:00,98.31,82.654,57.352,31.349 +2020-10-13 21:15:00,99.67,83.098,57.352,31.349 +2020-10-13 21:30:00,94.62,82.321,57.352,31.349 +2020-10-13 21:45:00,94.12,80.389,57.352,31.349 +2020-10-13 22:00:00,86.01,77.366,51.148999999999994,31.349 +2020-10-13 22:15:00,88.4,74.939,51.148999999999994,31.349 +2020-10-13 22:30:00,85.44,64.658,51.148999999999994,31.349 +2020-10-13 22:45:00,86.13,59.544,51.148999999999994,31.349 +2020-10-13 23:00:00,77.28,55.103,41.8,31.349 +2020-10-13 23:15:00,80.43,54.472,41.8,31.349 +2020-10-13 23:30:00,81.88,53.957,41.8,31.349 +2020-10-13 23:45:00,80.49,53.865,41.8,31.349 +2020-10-14 00:00:00,73.56,52.053000000000004,42.269,31.349 +2020-10-14 00:15:00,77.84,53.146,42.269,31.349 +2020-10-14 00:30:00,78.77,52.915,42.269,31.349 +2020-10-14 00:45:00,79.64,52.773999999999994,42.269,31.349 +2020-10-14 01:00:00,73.44,53.123999999999995,38.527,31.349 +2020-10-14 01:15:00,70.94,52.76,38.527,31.349 +2020-10-14 01:30:00,68.57,52.188,38.527,31.349 +2020-10-14 01:45:00,76.68,51.684,38.527,31.349 +2020-10-14 02:00:00,78.54,52.208,36.393,31.349 +2020-10-14 02:15:00,78.33,52.428000000000004,36.393,31.349 +2020-10-14 02:30:00,74.42,52.806000000000004,36.393,31.349 +2020-10-14 02:45:00,75.3,53.431000000000004,36.393,31.349 +2020-10-14 03:00:00,80.29,55.471000000000004,36.167,31.349 +2020-10-14 03:15:00,82.02,56.465,36.167,31.349 +2020-10-14 03:30:00,80.68,56.562,36.167,31.349 +2020-10-14 03:45:00,79.09,57.08,36.167,31.349 +2020-10-14 04:00:00,83.44,66.143,38.092,31.349 +2020-10-14 04:15:00,90.73,75.185,38.092,31.349 +2020-10-14 04:30:00,92.79,74.482,38.092,31.349 +2020-10-14 04:45:00,97.44,76.208,38.092,31.349 +2020-10-14 05:00:00,100.81,102.50299999999999,42.268,31.349 +2020-10-14 05:15:00,104.2,125.611,42.268,31.349 +2020-10-14 05:30:00,107.8,119.486,42.268,31.349 +2020-10-14 05:45:00,107.29,111.281,42.268,31.349 +2020-10-14 06:00:00,119.76,111.015,60.158,31.349 +2020-10-14 06:15:00,120.14,114.814,60.158,31.349 +2020-10-14 06:30:00,119.69,113.304,60.158,31.349 +2020-10-14 06:45:00,121.56,114.03200000000001,60.158,31.349 +2020-10-14 07:00:00,125.08,114.275,74.792,31.349 +2020-10-14 07:15:00,123.17,116.663,74.792,31.349 +2020-10-14 07:30:00,121.58,116.706,74.792,31.349 +2020-10-14 07:45:00,120.8,116.906,74.792,31.349 +2020-10-14 08:00:00,118.68,115.421,70.499,31.349 +2020-10-14 08:15:00,118.45,115.34899999999999,70.499,31.349 +2020-10-14 08:30:00,117.08,113.28399999999999,70.499,31.349 +2020-10-14 08:45:00,117.25,112.23,70.499,31.349 +2020-10-14 09:00:00,115.59,107.568,68.892,31.349 +2020-10-14 09:15:00,114.95,105.34,68.892,31.349 +2020-10-14 09:30:00,114.97,105.587,68.892,31.349 +2020-10-14 09:45:00,113.19,105.165,68.892,31.349 +2020-10-14 10:00:00,112.69,101.88,66.88600000000001,31.349 +2020-10-14 10:15:00,113.46,100.93700000000001,66.88600000000001,31.349 +2020-10-14 10:30:00,111.52,99.751,66.88600000000001,31.349 +2020-10-14 10:45:00,111.35,99.469,66.88600000000001,31.349 +2020-10-14 11:00:00,109.19,96.285,66.187,31.349 +2020-10-14 11:15:00,108.97,96.74799999999999,66.187,31.349 +2020-10-14 11:30:00,109.66,96.945,66.187,31.349 +2020-10-14 11:45:00,107.61,96.51100000000001,66.187,31.349 +2020-10-14 12:00:00,105.62,92.641,62.18,31.349 +2020-10-14 12:15:00,107.74,92.162,62.18,31.349 +2020-10-14 12:30:00,102.93,91.781,62.18,31.349 +2020-10-14 12:45:00,105.05,92.044,62.18,31.349 +2020-10-14 13:00:00,103.94,91.37299999999999,62.23,31.349 +2020-10-14 13:15:00,104.45,91.285,62.23,31.349 +2020-10-14 13:30:00,103.05,90.414,62.23,31.349 +2020-10-14 13:45:00,104.96,89.976,62.23,31.349 +2020-10-14 14:00:00,105.25,89.01100000000001,63.721000000000004,31.349 +2020-10-14 14:15:00,105.9,89.475,63.721000000000004,31.349 +2020-10-14 14:30:00,106.58,89.037,63.721000000000004,31.349 +2020-10-14 14:45:00,106.09,88.876,63.721000000000004,31.349 +2020-10-14 15:00:00,106.28,88.58200000000001,66.523,31.349 +2020-10-14 15:15:00,106.7,88.389,66.523,31.349 +2020-10-14 15:30:00,107.34,89.088,66.523,31.349 +2020-10-14 15:45:00,109.23,89.23,66.523,31.349 +2020-10-14 16:00:00,110.31,88.816,69.679,31.349 +2020-10-14 16:15:00,111.71,88.462,69.679,31.349 +2020-10-14 16:30:00,113.73,89.105,69.679,31.349 +2020-10-14 16:45:00,116.27,87.84700000000001,69.679,31.349 +2020-10-14 17:00:00,121.1,87.74600000000001,75.04,31.349 +2020-10-14 17:15:00,119.15,89.189,75.04,31.349 +2020-10-14 17:30:00,125.29,88.95,75.04,31.349 +2020-10-14 17:45:00,126.05,89.851,75.04,31.349 +2020-10-14 18:00:00,126.68,89.385,75.915,31.349 +2020-10-14 18:15:00,123.93,89.046,75.915,31.349 +2020-10-14 18:30:00,123.69,88.21799999999999,75.915,31.349 +2020-10-14 18:45:00,120.41,91.794,75.915,31.349 +2020-10-14 19:00:00,120.5,92.323,74.66,31.349 +2020-10-14 19:15:00,119.72,91.131,74.66,31.349 +2020-10-14 19:30:00,116.45,90.488,74.66,31.349 +2020-10-14 19:45:00,107.96,90.303,74.66,31.349 +2020-10-14 20:00:00,107.34,89.848,71.204,31.349 +2020-10-14 20:15:00,109.49,87.55799999999999,71.204,31.349 +2020-10-14 20:30:00,100.41,86.404,71.204,31.349 +2020-10-14 20:45:00,104.83,85.022,71.204,31.349 +2020-10-14 21:00:00,93.46,83.021,61.052,31.349 +2020-10-14 21:15:00,92.85,83.454,61.052,31.349 +2020-10-14 21:30:00,93.83,82.68700000000001,61.052,31.349 +2020-10-14 21:45:00,95.35,80.72800000000001,61.052,31.349 +2020-10-14 22:00:00,90.21,77.693,54.691,31.349 +2020-10-14 22:15:00,86.19,75.244,54.691,31.349 +2020-10-14 22:30:00,79.18,64.979,54.691,31.349 +2020-10-14 22:45:00,83.68,59.871,54.691,31.349 +2020-10-14 23:00:00,81.26,55.456,45.18,31.349 +2020-10-14 23:15:00,82.45,54.794,45.18,31.349 +2020-10-14 23:30:00,76.29,54.28,45.18,31.349 +2020-10-14 23:45:00,79.49,54.183,45.18,31.349 +2020-10-15 00:00:00,78.17,52.367,42.746,31.349 +2020-10-15 00:15:00,79.69,53.452,42.746,31.349 +2020-10-15 00:30:00,76.7,53.227,42.746,31.349 +2020-10-15 00:45:00,80.0,53.082,42.746,31.349 +2020-10-15 01:00:00,77.04,53.437,40.025999999999996,31.349 +2020-10-15 01:15:00,76.62,53.093999999999994,40.025999999999996,31.349 +2020-10-15 01:30:00,72.38,52.538999999999994,40.025999999999996,31.349 +2020-10-15 01:45:00,72.39,52.033,40.025999999999996,31.349 +2020-10-15 02:00:00,75.25,52.565,38.154,31.349 +2020-10-15 02:15:00,79.25,52.799,38.154,31.349 +2020-10-15 02:30:00,80.42,53.161,38.154,31.349 +2020-10-15 02:45:00,76.0,53.781000000000006,38.154,31.349 +2020-10-15 03:00:00,78.42,55.809,37.575,31.349 +2020-10-15 03:15:00,81.16,56.821000000000005,37.575,31.349 +2020-10-15 03:30:00,82.38,56.92100000000001,37.575,31.349 +2020-10-15 03:45:00,80.84,57.42100000000001,37.575,31.349 +2020-10-15 04:00:00,86.29,66.514,39.154,31.349 +2020-10-15 04:15:00,91.49,75.59,39.154,31.349 +2020-10-15 04:30:00,94.3,74.887,39.154,31.349 +2020-10-15 04:45:00,97.08,76.622,39.154,31.349 +2020-10-15 05:00:00,98.74,103.00299999999999,44.085,31.349 +2020-10-15 05:15:00,105.55,126.2,44.085,31.349 +2020-10-15 05:30:00,107.13,120.055,44.085,31.349 +2020-10-15 05:45:00,111.73,111.80799999999999,44.085,31.349 +2020-10-15 06:00:00,119.93,111.52,57.49,31.349 +2020-10-15 06:15:00,118.79,115.337,57.49,31.349 +2020-10-15 06:30:00,120.03,113.837,57.49,31.349 +2020-10-15 06:45:00,122.37,114.57,57.49,31.349 +2020-10-15 07:00:00,124.75,114.81200000000001,73.617,31.349 +2020-10-15 07:15:00,124.91,117.212,73.617,31.349 +2020-10-15 07:30:00,125.12,117.286,73.617,31.349 +2020-10-15 07:45:00,121.11,117.48700000000001,73.617,31.349 +2020-10-15 08:00:00,118.19,116.01,69.281,31.349 +2020-10-15 08:15:00,117.96,115.90899999999999,69.281,31.349 +2020-10-15 08:30:00,122.14,113.867,69.281,31.349 +2020-10-15 08:45:00,122.14,112.792,69.281,31.349 +2020-10-15 09:00:00,120.75,108.12799999999999,63.926,31.349 +2020-10-15 09:15:00,121.83,105.895,63.926,31.349 +2020-10-15 09:30:00,123.0,106.12700000000001,63.926,31.349 +2020-10-15 09:45:00,123.77,105.677,63.926,31.349 +2020-10-15 10:00:00,117.9,102.385,59.442,31.349 +2020-10-15 10:15:00,119.2,101.40299999999999,59.442,31.349 +2020-10-15 10:30:00,115.6,100.199,59.442,31.349 +2020-10-15 10:45:00,114.68,99.9,59.442,31.349 +2020-10-15 11:00:00,111.42,96.72399999999999,56.771,31.349 +2020-10-15 11:15:00,110.71,97.169,56.771,31.349 +2020-10-15 11:30:00,111.45,97.36399999999999,56.771,31.349 +2020-10-15 11:45:00,110.73,96.916,56.771,31.349 +2020-10-15 12:00:00,108.08,93.021,53.701,31.349 +2020-10-15 12:15:00,108.8,92.53399999999999,53.701,31.349 +2020-10-15 12:30:00,106.76,92.189,53.701,31.349 +2020-10-15 12:45:00,106.52,92.447,53.701,31.349 +2020-10-15 13:00:00,106.01,91.74600000000001,52.364,31.349 +2020-10-15 13:15:00,105.75,91.661,52.364,31.349 +2020-10-15 13:30:00,107.45,90.788,52.364,31.349 +2020-10-15 13:45:00,110.96,90.34899999999999,52.364,31.349 +2020-10-15 14:00:00,110.78,89.334,53.419,31.349 +2020-10-15 14:15:00,109.56,89.81299999999999,53.419,31.349 +2020-10-15 14:30:00,111.28,89.411,53.419,31.349 +2020-10-15 14:45:00,109.87,89.24700000000001,53.419,31.349 +2020-10-15 15:00:00,111.22,88.921,56.744,31.349 +2020-10-15 15:15:00,111.54,88.74600000000001,56.744,31.349 +2020-10-15 15:30:00,111.77,89.48200000000001,56.744,31.349 +2020-10-15 15:45:00,113.05,89.63799999999999,56.744,31.349 +2020-10-15 16:00:00,116.56,89.189,60.458,31.349 +2020-10-15 16:15:00,120.2,88.854,60.458,31.349 +2020-10-15 16:30:00,122.91,89.492,60.458,31.349 +2020-10-15 16:45:00,122.73,88.289,60.458,31.349 +2020-10-15 17:00:00,126.75,88.146,66.295,31.349 +2020-10-15 17:15:00,126.46,89.611,66.295,31.349 +2020-10-15 17:30:00,128.48,89.376,66.295,31.349 +2020-10-15 17:45:00,124.9,90.3,66.295,31.349 +2020-10-15 18:00:00,127.14,89.82,68.468,31.349 +2020-10-15 18:15:00,123.0,89.46700000000001,68.468,31.349 +2020-10-15 18:30:00,124.51,88.649,68.468,31.349 +2020-10-15 18:45:00,120.83,92.22,68.468,31.349 +2020-10-15 19:00:00,114.09,92.759,66.39399999999999,31.349 +2020-10-15 19:15:00,112.02,91.56299999999999,66.39399999999999,31.349 +2020-10-15 19:30:00,107.14,90.911,66.39399999999999,31.349 +2020-10-15 19:45:00,113.12,90.709,66.39399999999999,31.349 +2020-10-15 20:00:00,108.37,90.277,63.183,31.349 +2020-10-15 20:15:00,108.7,87.98200000000001,63.183,31.349 +2020-10-15 20:30:00,99.66,86.8,63.183,31.349 +2020-10-15 20:45:00,101.56,85.389,63.183,31.349 +2020-10-15 21:00:00,93.27,83.387,55.133,31.349 +2020-10-15 21:15:00,93.41,83.809,55.133,31.349 +2020-10-15 21:30:00,93.15,83.053,55.133,31.349 +2020-10-15 21:45:00,94.83,81.067,55.133,31.349 +2020-10-15 22:00:00,90.93,78.02,50.111999999999995,31.349 +2020-10-15 22:15:00,84.87,75.54899999999999,50.111999999999995,31.349 +2020-10-15 22:30:00,80.83,65.3,50.111999999999995,31.349 +2020-10-15 22:45:00,85.9,60.198,50.111999999999995,31.349 +2020-10-15 23:00:00,83.4,55.809,44.536,31.349 +2020-10-15 23:15:00,83.04,55.11600000000001,44.536,31.349 +2020-10-15 23:30:00,78.5,54.603,44.536,31.349 +2020-10-15 23:45:00,78.94,54.5,44.536,31.349 +2020-10-16 00:00:00,78.53,56.543,42.291000000000004,31.349 +2020-10-16 00:15:00,80.34,58.228,42.291000000000004,31.349 +2020-10-16 00:30:00,75.11,57.871,42.291000000000004,31.349 +2020-10-16 00:45:00,78.3,57.95399999999999,42.291000000000004,31.349 +2020-10-16 01:00:00,77.43,58.34,41.008,31.349 +2020-10-16 01:15:00,79.28,57.986000000000004,41.008,31.349 +2020-10-16 01:30:00,77.29,57.566,41.008,31.349 +2020-10-16 01:45:00,76.13,57.258,41.008,31.349 +2020-10-16 02:00:00,78.43,58.57,39.521,31.349 +2020-10-16 02:15:00,78.48,58.61600000000001,39.521,31.349 +2020-10-16 02:30:00,76.3,59.912,39.521,31.349 +2020-10-16 02:45:00,72.93,60.25899999999999,39.521,31.349 +2020-10-16 03:00:00,75.99,62.902,39.812,31.349 +2020-10-16 03:15:00,78.35,63.992,39.812,31.349 +2020-10-16 03:30:00,83.32,64.1,39.812,31.349 +2020-10-16 03:45:00,87.67,65.527,39.812,31.349 +2020-10-16 04:00:00,86.71,75.56,41.22,31.349 +2020-10-16 04:15:00,87.83,84.14,41.22,31.349 +2020-10-16 04:30:00,93.37,84.585,41.22,31.349 +2020-10-16 04:45:00,97.81,85.63799999999999,41.22,31.349 +2020-10-16 05:00:00,105.43,113.375,45.115,31.349 +2020-10-16 05:15:00,108.97,139.797,45.115,31.349 +2020-10-16 05:30:00,110.44,134.631,45.115,31.349 +2020-10-16 05:45:00,116.33,125.77,45.115,31.349 +2020-10-16 06:00:00,121.67,125.60799999999999,59.06100000000001,31.349 +2020-10-16 06:15:00,121.67,129.158,59.06100000000001,31.349 +2020-10-16 06:30:00,123.2,127.425,59.06100000000001,31.349 +2020-10-16 06:45:00,125.24,128.667,59.06100000000001,31.349 +2020-10-16 07:00:00,129.04,129.357,71.874,31.349 +2020-10-16 07:15:00,129.43,132.971,71.874,31.349 +2020-10-16 07:30:00,129.86,132.234,71.874,31.349 +2020-10-16 07:45:00,129.09,131.789,71.874,31.349 +2020-10-16 08:00:00,129.38,131.43,68.439,31.349 +2020-10-16 08:15:00,130.59,131.24200000000002,68.439,31.349 +2020-10-16 08:30:00,135.1,129.144,68.439,31.349 +2020-10-16 08:45:00,135.29,126.51,68.439,31.349 +2020-10-16 09:00:00,133.1,121.61399999999999,65.523,31.349 +2020-10-16 09:15:00,134.37,120.37200000000001,65.523,31.349 +2020-10-16 09:30:00,133.1,119.85799999999999,65.523,31.349 +2020-10-16 09:45:00,136.75,119.541,65.523,31.349 +2020-10-16 10:00:00,133.64,115.68799999999999,62.005,31.349 +2020-10-16 10:15:00,134.38,115.005,62.005,31.349 +2020-10-16 10:30:00,133.95,113.708,62.005,31.349 +2020-10-16 10:45:00,134.02,113.04,62.005,31.349 +2020-10-16 11:00:00,132.18,109.76799999999999,60.351000000000006,31.349 +2020-10-16 11:15:00,131.6,109.25299999999999,60.351000000000006,31.349 +2020-10-16 11:30:00,132.82,110.32799999999999,60.351000000000006,31.349 +2020-10-16 11:45:00,132.96,110.382,60.351000000000006,31.349 +2020-10-16 12:00:00,127.23,107.589,55.331,31.349 +2020-10-16 12:15:00,125.71,105.316,55.331,31.349 +2020-10-16 12:30:00,122.7,105.105,55.331,31.349 +2020-10-16 12:45:00,120.88,105.475,55.331,31.349 +2020-10-16 13:00:00,119.13,106.29700000000001,53.361999999999995,31.349 +2020-10-16 13:15:00,118.48,106.384,53.361999999999995,31.349 +2020-10-16 13:30:00,117.76,105.999,53.361999999999995,31.349 +2020-10-16 13:45:00,117.81,105.741,53.361999999999995,31.349 +2020-10-16 14:00:00,116.71,104.645,51.708,31.349 +2020-10-16 14:15:00,115.79,104.56,51.708,31.349 +2020-10-16 14:30:00,116.75,104.74799999999999,51.708,31.349 +2020-10-16 14:45:00,114.09,104.55,51.708,31.349 +2020-10-16 15:00:00,111.63,103.62700000000001,54.571000000000005,31.349 +2020-10-16 15:15:00,111.74,103.186,54.571000000000005,31.349 +2020-10-16 15:30:00,111.8,102.095,54.571000000000005,31.349 +2020-10-16 15:45:00,112.85,102.43700000000001,54.571000000000005,31.349 +2020-10-16 16:00:00,116.73,102.41799999999999,58.662,31.349 +2020-10-16 16:15:00,115.35,103.176,58.662,31.349 +2020-10-16 16:30:00,120.57,103.56,58.662,31.349 +2020-10-16 16:45:00,121.41,101.693,58.662,31.349 +2020-10-16 17:00:00,125.43,103.272,65.941,31.349 +2020-10-16 17:15:00,124.15,103.835,65.941,31.349 +2020-10-16 17:30:00,126.8,103.678,65.941,31.349 +2020-10-16 17:45:00,124.08,103.572,65.941,31.349 +2020-10-16 18:00:00,125.15,105.102,65.628,31.349 +2020-10-16 18:15:00,120.47,104.225,65.628,31.349 +2020-10-16 18:30:00,119.06,103.037,65.628,31.349 +2020-10-16 18:45:00,118.12,106.52799999999999,65.628,31.349 +2020-10-16 19:00:00,111.76,108.321,63.662,31.349 +2020-10-16 19:15:00,108.6,107.913,63.662,31.349 +2020-10-16 19:30:00,107.6,106.984,63.662,31.349 +2020-10-16 19:45:00,105.8,105.885,63.662,31.349 +2020-10-16 20:00:00,105.65,102.874,61.945,31.349 +2020-10-16 20:15:00,104.08,100.425,61.945,31.349 +2020-10-16 20:30:00,97.94,99.93700000000001,61.945,31.349 +2020-10-16 20:45:00,91.95,98.415,61.945,31.349 +2020-10-16 21:00:00,89.11,95.71700000000001,53.903,31.349 +2020-10-16 21:15:00,85.85,96.61399999999999,53.903,31.349 +2020-10-16 21:30:00,82.94,95.831,53.903,31.349 +2020-10-16 21:45:00,80.74,93.97200000000001,53.903,31.349 +2020-10-16 22:00:00,77.43,89.986,48.403999999999996,31.349 +2020-10-16 22:15:00,76.58,86.50399999999999,48.403999999999996,31.349 +2020-10-16 22:30:00,73.82,80.395,48.403999999999996,31.349 +2020-10-16 22:45:00,73.45,76.143,48.403999999999996,31.349 +2020-10-16 23:00:00,75.53,70.91199999999999,41.07,31.349 +2020-10-16 23:15:00,75.35,69.029,41.07,31.349 +2020-10-16 23:30:00,75.46,66.437,41.07,31.349 +2020-10-16 23:45:00,67.11,66.324,41.07,31.349 +2020-10-17 00:00:00,64.47,56.047,38.989000000000004,31.177 +2020-10-17 00:15:00,67.85,55.284,38.989000000000004,31.177 +2020-10-17 00:30:00,69.98,55.373000000000005,38.989000000000004,31.177 +2020-10-17 00:45:00,71.35,55.461000000000006,38.989000000000004,31.177 +2020-10-17 01:00:00,65.24,56.316,35.275,31.177 +2020-10-17 01:15:00,64.34,55.773999999999994,35.275,31.177 +2020-10-17 01:30:00,64.87,54.731,35.275,31.177 +2020-10-17 01:45:00,69.92,54.907,35.275,31.177 +2020-10-17 02:00:00,67.08,56.114,32.838,31.177 +2020-10-17 02:15:00,64.62,55.611999999999995,32.838,31.177 +2020-10-17 02:30:00,62.67,55.968,32.838,31.177 +2020-10-17 02:45:00,62.07,56.736999999999995,32.838,31.177 +2020-10-17 03:00:00,61.8,59.028999999999996,32.418,31.177 +2020-10-17 03:15:00,61.68,59.211999999999996,32.418,31.177 +2020-10-17 03:30:00,62.25,58.705,32.418,31.177 +2020-10-17 03:45:00,62.17,60.88399999999999,32.418,31.177 +2020-10-17 04:00:00,63.27,68.046,32.099000000000004,31.177 +2020-10-17 04:15:00,63.3,74.992,32.099000000000004,31.177 +2020-10-17 04:30:00,63.92,73.575,32.099000000000004,31.177 +2020-10-17 04:45:00,65.08,74.53,32.099000000000004,31.177 +2020-10-17 05:00:00,68.03,90.771,32.926,31.177 +2020-10-17 05:15:00,68.43,102.831,32.926,31.177 +2020-10-17 05:30:00,69.72,98.57600000000001,32.926,31.177 +2020-10-17 05:45:00,72.43,94.47,32.926,31.177 +2020-10-17 06:00:00,75.18,108.464,35.069,31.177 +2020-10-17 06:15:00,75.93,123.736,35.069,31.177 +2020-10-17 06:30:00,77.23,118.042,35.069,31.177 +2020-10-17 06:45:00,80.22,113.37700000000001,35.069,31.177 +2020-10-17 07:00:00,83.39,111.169,40.906,31.177 +2020-10-17 07:15:00,84.09,113.566,40.906,31.177 +2020-10-17 07:30:00,85.78,114.92299999999999,40.906,31.177 +2020-10-17 07:45:00,87.06,116.913,40.906,31.177 +2020-10-17 08:00:00,87.85,118.645,46.603,31.177 +2020-10-17 08:15:00,88.48,120.234,46.603,31.177 +2020-10-17 08:30:00,88.17,118.999,46.603,31.177 +2020-10-17 08:45:00,89.51,118.406,46.603,31.177 +2020-10-17 09:00:00,88.81,115.679,49.935,31.177 +2020-10-17 09:15:00,91.01,115.045,49.935,31.177 +2020-10-17 09:30:00,92.06,115.214,49.935,31.177 +2020-10-17 09:45:00,91.72,114.764,49.935,31.177 +2020-10-17 10:00:00,86.24,111.156,47.585,31.177 +2020-10-17 10:15:00,83.34,110.669,47.585,31.177 +2020-10-17 10:30:00,82.45,109.304,47.585,31.177 +2020-10-17 10:45:00,81.32,109.196,47.585,31.177 +2020-10-17 11:00:00,79.02,105.931,43.376999999999995,31.177 +2020-10-17 11:15:00,79.49,105.35600000000001,43.376999999999995,31.177 +2020-10-17 11:30:00,76.81,105.913,43.376999999999995,31.177 +2020-10-17 11:45:00,81.02,105.679,43.376999999999995,31.177 +2020-10-17 12:00:00,75.69,102.375,40.855,31.177 +2020-10-17 12:15:00,78.67,100.795,40.855,31.177 +2020-10-17 12:30:00,77.84,100.75200000000001,40.855,31.177 +2020-10-17 12:45:00,75.43,100.959,40.855,31.177 +2020-10-17 13:00:00,74.06,101.12700000000001,37.251,31.177 +2020-10-17 13:15:00,75.51,99.822,37.251,31.177 +2020-10-17 13:30:00,76.5,99.24700000000001,37.251,31.177 +2020-10-17 13:45:00,76.49,98.74799999999999,37.251,31.177 +2020-10-17 14:00:00,73.63,98.32,38.548,31.177 +2020-10-17 14:15:00,76.38,97.464,38.548,31.177 +2020-10-17 14:30:00,77.84,96.46700000000001,38.548,31.177 +2020-10-17 14:45:00,80.65,96.57700000000001,38.548,31.177 +2020-10-17 15:00:00,81.45,96.179,42.883,31.177 +2020-10-17 15:15:00,81.41,96.49600000000001,42.883,31.177 +2020-10-17 15:30:00,83.3,96.42,42.883,31.177 +2020-10-17 15:45:00,84.57,96.435,42.883,31.177 +2020-10-17 16:00:00,85.25,96.794,48.143,31.177 +2020-10-17 16:15:00,84.79,97.71600000000001,48.143,31.177 +2020-10-17 16:30:00,86.19,98.15799999999999,48.143,31.177 +2020-10-17 16:45:00,89.26,96.83,48.143,31.177 +2020-10-17 17:00:00,95.62,97.73299999999999,55.25,31.177 +2020-10-17 17:15:00,96.78,98.31700000000001,55.25,31.177 +2020-10-17 17:30:00,101.35,98.053,55.25,31.177 +2020-10-17 17:45:00,99.12,97.947,55.25,31.177 +2020-10-17 18:00:00,101.19,99.876,57.506,31.177 +2020-10-17 18:15:00,98.31,100.652,57.506,31.177 +2020-10-17 18:30:00,99.49,100.771,57.506,31.177 +2020-10-17 18:45:00,95.84,100.971,57.506,31.177 +2020-10-17 19:00:00,89.41,102.62799999999999,55.528999999999996,31.177 +2020-10-17 19:15:00,87.46,101.51,55.528999999999996,31.177 +2020-10-17 19:30:00,85.9,101.3,55.528999999999996,31.177 +2020-10-17 19:45:00,84.72,100.851,55.528999999999996,31.177 +2020-10-17 20:00:00,80.41,99.40299999999999,46.166000000000004,31.177 +2020-10-17 20:15:00,80.37,97.781,46.166000000000004,31.177 +2020-10-17 20:30:00,79.26,96.693,46.166000000000004,31.177 +2020-10-17 20:45:00,77.14,95.757,46.166000000000004,31.177 +2020-10-17 21:00:00,73.18,93.601,40.406,31.177 +2020-10-17 21:15:00,72.29,94.561,40.406,31.177 +2020-10-17 21:30:00,70.67,94.51,40.406,31.177 +2020-10-17 21:45:00,69.52,92.175,40.406,31.177 +2020-10-17 22:00:00,66.37,88.87700000000001,39.616,31.177 +2020-10-17 22:15:00,65.35,86.779,39.616,31.177 +2020-10-17 22:30:00,63.44,83.772,39.616,31.177 +2020-10-17 22:45:00,62.35,80.665,39.616,31.177 +2020-10-17 23:00:00,58.53,76.434,32.205,31.177 +2020-10-17 23:15:00,56.67,73.855,32.205,31.177 +2020-10-17 23:30:00,56.68,71.44,32.205,31.177 +2020-10-17 23:45:00,56.38,70.208,32.205,31.177 +2020-10-18 00:00:00,52.9,57.243,28.229,31.177 +2020-10-18 00:15:00,53.22,55.765,28.229,31.177 +2020-10-18 00:30:00,52.88,55.606,28.229,31.177 +2020-10-18 00:45:00,53.61,55.98,28.229,31.177 +2020-10-18 01:00:00,51.32,56.903999999999996,25.669,31.177 +2020-10-18 01:15:00,52.64,56.831,25.669,31.177 +2020-10-18 01:30:00,51.62,55.996,25.669,31.177 +2020-10-18 01:45:00,51.85,55.832,25.669,31.177 +2020-10-18 02:00:00,51.91,56.717,24.948,31.177 +2020-10-18 02:15:00,52.54,56.151,24.948,31.177 +2020-10-18 02:30:00,52.65,56.998000000000005,24.948,31.177 +2020-10-18 02:45:00,52.06,57.849,24.948,31.177 +2020-10-18 03:00:00,51.53,60.59,24.445,31.177 +2020-10-18 03:15:00,52.25,60.667,24.445,31.177 +2020-10-18 03:30:00,52.4,60.486999999999995,24.445,31.177 +2020-10-18 03:45:00,52.91,62.228,24.445,31.177 +2020-10-18 04:00:00,54.69,69.267,25.839000000000002,31.177 +2020-10-18 04:15:00,55.62,75.545,25.839000000000002,31.177 +2020-10-18 04:30:00,56.31,74.807,25.839000000000002,31.177 +2020-10-18 04:45:00,56.9,75.675,25.839000000000002,31.177 +2020-10-18 05:00:00,58.65,90.402,26.803,31.177 +2020-10-18 05:15:00,58.63,101.00399999999999,26.803,31.177 +2020-10-18 05:30:00,59.14,96.463,26.803,31.177 +2020-10-18 05:45:00,57.88,92.30799999999999,26.803,31.177 +2020-10-18 06:00:00,62.76,105.06200000000001,28.147,31.177 +2020-10-18 06:15:00,63.62,119.914,28.147,31.177 +2020-10-18 06:30:00,63.69,113.374,28.147,31.177 +2020-10-18 06:45:00,65.7,107.73,28.147,31.177 +2020-10-18 07:00:00,70.03,106.82,31.116,31.177 +2020-10-18 07:15:00,71.46,108.045,31.116,31.177 +2020-10-18 07:30:00,71.58,109.411,31.116,31.177 +2020-10-18 07:45:00,72.74,111.01,31.116,31.177 +2020-10-18 08:00:00,71.27,113.97399999999999,35.739000000000004,31.177 +2020-10-18 08:15:00,71.83,116.021,35.739000000000004,31.177 +2020-10-18 08:30:00,71.64,116.059,35.739000000000004,31.177 +2020-10-18 08:45:00,73.24,116.448,35.739000000000004,31.177 +2020-10-18 09:00:00,73.13,113.434,39.455999999999996,31.177 +2020-10-18 09:15:00,75.2,112.875,39.455999999999996,31.177 +2020-10-18 09:30:00,75.81,113.135,39.455999999999996,31.177 +2020-10-18 09:45:00,78.43,113.03299999999999,39.455999999999996,31.177 +2020-10-18 10:00:00,74.81,110.97200000000001,41.343999999999994,31.177 +2020-10-18 10:15:00,75.06,110.79299999999999,41.343999999999994,31.177 +2020-10-18 10:30:00,75.48,109.82700000000001,41.343999999999994,31.177 +2020-10-18 10:45:00,76.1,109.087,41.343999999999994,31.177 +2020-10-18 11:00:00,75.4,106.171,43.645,31.177 +2020-10-18 11:15:00,70.54,105.47,43.645,31.177 +2020-10-18 11:30:00,68.91,105.75200000000001,43.645,31.177 +2020-10-18 11:45:00,69.28,105.943,43.645,31.177 +2020-10-18 12:00:00,69.75,102.689,39.796,31.177 +2020-10-18 12:15:00,67.11,101.93700000000001,39.796,31.177 +2020-10-18 12:30:00,67.36,101.18799999999999,39.796,31.177 +2020-10-18 12:45:00,69.13,100.594,39.796,31.177 +2020-10-18 13:00:00,64.24,100.186,36.343,31.177 +2020-10-18 13:15:00,62.42,100.39,36.343,31.177 +2020-10-18 13:30:00,63.26,99.26299999999999,36.343,31.177 +2020-10-18 13:45:00,63.85,98.81700000000001,36.343,31.177 +2020-10-18 14:00:00,61.41,98.94,33.162,31.177 +2020-10-18 14:15:00,61.03,98.955,33.162,31.177 +2020-10-18 14:30:00,62.69,98.22399999999999,33.162,31.177 +2020-10-18 14:45:00,64.11,97.681,33.162,31.177 +2020-10-18 15:00:00,66.4,96.44,33.215,31.177 +2020-10-18 15:15:00,66.52,96.898,33.215,31.177 +2020-10-18 15:30:00,68.51,97.12799999999999,33.215,31.177 +2020-10-18 15:45:00,71.34,97.664,33.215,31.177 +2020-10-18 16:00:00,75.08,98.287,37.385999999999996,31.177 +2020-10-18 16:15:00,76.81,98.823,37.385999999999996,31.177 +2020-10-18 16:30:00,79.34,99.815,37.385999999999996,31.177 +2020-10-18 16:45:00,83.18,98.639,37.385999999999996,31.177 +2020-10-18 17:00:00,89.98,99.574,46.618,31.177 +2020-10-18 17:15:00,92.24,100.639,46.618,31.177 +2020-10-18 17:30:00,94.57,100.874,46.618,31.177 +2020-10-18 17:45:00,94.81,102.181,46.618,31.177 +2020-10-18 18:00:00,98.0,104.02799999999999,50.111000000000004,31.177 +2020-10-18 18:15:00,94.97,105.304,50.111000000000004,31.177 +2020-10-18 18:30:00,93.21,104.22200000000001,50.111000000000004,31.177 +2020-10-18 18:45:00,91.12,105.446,50.111000000000004,31.177 +2020-10-18 19:00:00,88.83,107.86,50.25,31.177 +2020-10-18 19:15:00,92.4,106.557,50.25,31.177 +2020-10-18 19:30:00,93.45,106.14,50.25,31.177 +2020-10-18 19:45:00,92.2,106.221,50.25,31.177 +2020-10-18 20:00:00,82.79,104.83200000000001,44.265,31.177 +2020-10-18 20:15:00,85.44,103.654,44.265,31.177 +2020-10-18 20:30:00,87.59,103.538,44.265,31.177 +2020-10-18 20:45:00,91.02,101.215,44.265,31.177 +2020-10-18 21:00:00,88.07,97.631,39.717,31.177 +2020-10-18 21:15:00,83.42,98.124,39.717,31.177 +2020-10-18 21:30:00,80.07,97.94200000000001,39.717,31.177 +2020-10-18 21:45:00,83.72,95.816,39.717,31.177 +2020-10-18 22:00:00,81.94,92.81299999999999,39.224000000000004,31.177 +2020-10-18 22:15:00,82.38,89.552,39.224000000000004,31.177 +2020-10-18 22:30:00,77.0,84.719,39.224000000000004,31.177 +2020-10-18 22:45:00,77.52,80.611,39.224000000000004,31.177 +2020-10-18 23:00:00,75.69,74.82,33.518,31.177 +2020-10-18 23:15:00,75.04,73.737,33.518,31.177 +2020-10-18 23:30:00,72.22,71.502,33.518,31.177 +2020-10-18 23:45:00,70.95,70.789,33.518,31.177 +2020-10-19 00:00:00,72.57,60.35,34.301,31.349 +2020-10-19 00:15:00,72.05,60.687,34.301,31.349 +2020-10-19 00:30:00,71.7,60.415,34.301,31.349 +2020-10-19 00:45:00,68.05,60.333999999999996,34.301,31.349 +2020-10-19 01:00:00,63.81,61.445,34.143,31.349 +2020-10-19 01:15:00,70.57,61.14,34.143,31.349 +2020-10-19 01:30:00,73.46,60.521,34.143,31.349 +2020-10-19 01:45:00,73.96,60.363,34.143,31.349 +2020-10-19 02:00:00,69.68,61.452,33.650999999999996,31.349 +2020-10-19 02:15:00,71.16,61.118,33.650999999999996,31.349 +2020-10-19 02:30:00,74.31,62.191,33.650999999999996,31.349 +2020-10-19 02:45:00,75.73,62.668,33.650999999999996,31.349 +2020-10-19 03:00:00,74.19,66.23100000000001,32.599000000000004,31.349 +2020-10-19 03:15:00,76.4,67.46600000000001,32.599000000000004,31.349 +2020-10-19 03:30:00,80.44,67.487,32.599000000000004,31.349 +2020-10-19 03:45:00,82.75,68.72399999999999,32.599000000000004,31.349 +2020-10-19 04:00:00,83.01,79.33800000000001,33.785,31.349 +2020-10-19 04:15:00,86.46,89.04,33.785,31.349 +2020-10-19 04:30:00,93.16,89.11200000000001,33.785,31.349 +2020-10-19 04:45:00,99.61,90.245,33.785,31.349 +2020-10-19 05:00:00,103.97,115.891,41.285,31.349 +2020-10-19 05:15:00,106.86,141.102,41.285,31.349 +2020-10-19 05:30:00,111.68,135.92700000000002,41.285,31.349 +2020-10-19 05:45:00,123.4,127.571,41.285,31.349 +2020-10-19 06:00:00,130.7,127.085,60.486000000000004,31.349 +2020-10-19 06:15:00,123.9,130.441,60.486000000000004,31.349 +2020-10-19 06:30:00,124.3,129.692,60.486000000000004,31.349 +2020-10-19 06:45:00,125.37,130.83,60.486000000000004,31.349 +2020-10-19 07:00:00,128.39,131.643,74.012,31.349 +2020-10-19 07:15:00,126.92,134.558,74.012,31.349 +2020-10-19 07:30:00,124.7,135.17,74.012,31.349 +2020-10-19 07:45:00,123.89,135.63299999999998,74.012,31.349 +2020-10-19 08:00:00,122.1,135.503,69.569,31.349 +2020-10-19 08:15:00,120.91,135.888,69.569,31.349 +2020-10-19 08:30:00,123.72,133.418,69.569,31.349 +2020-10-19 08:45:00,126.6,132.155,69.569,31.349 +2020-10-19 09:00:00,123.28,128.308,66.152,31.349 +2020-10-19 09:15:00,122.26,125.132,66.152,31.349 +2020-10-19 09:30:00,120.9,124.45,66.152,31.349 +2020-10-19 09:45:00,122.7,123.491,66.152,31.349 +2020-10-19 10:00:00,123.26,121.242,62.923,31.349 +2020-10-19 10:15:00,123.92,120.78200000000001,62.923,31.349 +2020-10-19 10:30:00,124.65,119.12799999999999,62.923,31.349 +2020-10-19 10:45:00,121.74,118.13,62.923,31.349 +2020-10-19 11:00:00,111.45,113.977,61.522,31.349 +2020-10-19 11:15:00,111.72,114.376,61.522,31.349 +2020-10-19 11:30:00,111.64,115.708,61.522,31.349 +2020-10-19 11:45:00,114.51,115.85799999999999,61.522,31.349 +2020-10-19 12:00:00,109.18,113.042,58.632,31.349 +2020-10-19 12:15:00,109.27,112.32799999999999,58.632,31.349 +2020-10-19 12:30:00,119.09,111.34,58.632,31.349 +2020-10-19 12:45:00,120.49,111.61399999999999,58.632,31.349 +2020-10-19 13:00:00,121.81,111.93299999999999,59.06,31.349 +2020-10-19 13:15:00,123.36,111.008,59.06,31.349 +2020-10-19 13:30:00,124.18,109.641,59.06,31.349 +2020-10-19 13:45:00,123.73,109.555,59.06,31.349 +2020-10-19 14:00:00,128.78,108.941,59.791000000000004,31.349 +2020-10-19 14:15:00,127.05,108.83,59.791000000000004,31.349 +2020-10-19 14:30:00,122.62,107.762,59.791000000000004,31.349 +2020-10-19 14:45:00,122.32,107.991,59.791000000000004,31.349 +2020-10-19 15:00:00,121.8,107.624,61.148,31.349 +2020-10-19 15:15:00,124.23,107.045,61.148,31.349 +2020-10-19 15:30:00,120.17,107.131,61.148,31.349 +2020-10-19 15:45:00,122.07,107.24,61.148,31.349 +2020-10-19 16:00:00,125.4,108.13600000000001,66.009,31.349 +2020-10-19 16:15:00,122.23,108.296,66.009,31.349 +2020-10-19 16:30:00,123.96,108.48,66.009,31.349 +2020-10-19 16:45:00,124.66,106.705,66.009,31.349 +2020-10-19 17:00:00,132.19,106.90799999999999,73.683,31.349 +2020-10-19 17:15:00,128.75,107.635,73.683,31.349 +2020-10-19 17:30:00,131.94,107.412,73.683,31.349 +2020-10-19 17:45:00,128.27,107.76,73.683,31.349 +2020-10-19 18:00:00,132.72,109.30799999999999,72.848,31.349 +2020-10-19 18:15:00,126.09,108.596,72.848,31.349 +2020-10-19 18:30:00,127.58,107.54700000000001,72.848,31.349 +2020-10-19 18:45:00,123.85,110.50200000000001,72.848,31.349 +2020-10-19 19:00:00,120.43,111.916,71.139,31.349 +2020-10-19 19:15:00,112.13,110.569,71.139,31.349 +2020-10-19 19:30:00,108.57,110.238,71.139,31.349 +2020-10-19 19:45:00,108.74,109.59,71.139,31.349 +2020-10-19 20:00:00,106.01,106.426,69.667,31.349 +2020-10-19 20:15:00,109.28,104.56700000000001,69.667,31.349 +2020-10-19 20:30:00,106.18,103.68299999999999,69.667,31.349 +2020-10-19 20:45:00,100.16,102.28200000000001,69.667,31.349 +2020-10-19 21:00:00,94.21,98.705,61.166000000000004,31.349 +2020-10-19 21:15:00,95.43,98.78,61.166000000000004,31.349 +2020-10-19 21:30:00,96.14,98.366,61.166000000000004,31.349 +2020-10-19 21:45:00,94.59,95.859,61.166000000000004,31.349 +2020-10-19 22:00:00,89.3,90.444,52.772,31.349 +2020-10-19 22:15:00,84.72,87.365,52.772,31.349 +2020-10-19 22:30:00,86.82,75.70100000000001,52.772,31.349 +2020-10-19 22:45:00,86.53,69.094,52.772,31.349 +2020-10-19 23:00:00,81.21,63.718,45.136,31.349 +2020-10-19 23:15:00,79.44,63.248999999999995,45.136,31.349 +2020-10-19 23:30:00,81.27,62.427,45.136,31.349 +2020-10-19 23:45:00,78.67,62.832,45.136,31.349 +2020-10-20 00:00:00,75.94,59.277,47.35,31.349 +2020-10-20 00:15:00,72.26,60.708999999999996,47.35,31.349 +2020-10-20 00:30:00,77.13,60.317,47.35,31.349 +2020-10-20 00:45:00,79.34,60.123999999999995,47.35,31.349 +2020-10-20 01:00:00,76.83,60.9,43.424,31.349 +2020-10-20 01:15:00,74.74,60.449,43.424,31.349 +2020-10-20 01:30:00,77.46,59.869,43.424,31.349 +2020-10-20 01:45:00,79.29,59.611999999999995,43.424,31.349 +2020-10-20 02:00:00,74.21,60.479,41.778999999999996,31.349 +2020-10-20 02:15:00,72.87,60.657,41.778999999999996,31.349 +2020-10-20 02:30:00,80.67,61.238,41.778999999999996,31.349 +2020-10-20 02:45:00,80.69,61.875,41.778999999999996,31.349 +2020-10-20 03:00:00,84.16,64.58,40.771,31.349 +2020-10-20 03:15:00,76.84,65.876,40.771,31.349 +2020-10-20 03:30:00,81.98,66.119,40.771,31.349 +2020-10-20 03:45:00,85.52,66.896,40.771,31.349 +2020-10-20 04:00:00,88.83,76.83,41.816,31.349 +2020-10-20 04:15:00,86.61,86.4,41.816,31.349 +2020-10-20 04:30:00,89.0,86.262,41.816,31.349 +2020-10-20 04:45:00,93.57,88.22,41.816,31.349 +2020-10-20 05:00:00,103.8,117.08200000000001,45.842,31.349 +2020-10-20 05:15:00,114.96,142.561,45.842,31.349 +2020-10-20 05:30:00,119.33,136.741,45.842,31.349 +2020-10-20 05:45:00,118.64,127.999,45.842,31.349 +2020-10-20 06:00:00,120.99,127.436,59.12,31.349 +2020-10-20 06:15:00,123.0,131.688,59.12,31.349 +2020-10-20 06:30:00,124.14,130.493,59.12,31.349 +2020-10-20 06:45:00,126.4,131.034,59.12,31.349 +2020-10-20 07:00:00,128.62,131.822,70.33,31.349 +2020-10-20 07:15:00,126.71,134.554,70.33,31.349 +2020-10-20 07:30:00,126.14,134.931,70.33,31.349 +2020-10-20 07:45:00,125.14,135.055,70.33,31.349 +2020-10-20 08:00:00,124.17,134.97,67.788,31.349 +2020-10-20 08:15:00,128.23,134.60299999999998,67.788,31.349 +2020-10-20 08:30:00,129.26,132.143,67.788,31.349 +2020-10-20 08:45:00,129.38,130.308,67.788,31.349 +2020-10-20 09:00:00,127.86,126.161,62.622,31.349 +2020-10-20 09:15:00,129.76,123.774,62.622,31.349 +2020-10-20 09:30:00,128.95,123.736,62.622,31.349 +2020-10-20 09:45:00,129.62,123.212,62.622,31.349 +2020-10-20 10:00:00,130.0,120.056,60.887,31.349 +2020-10-20 10:15:00,130.9,118.914,60.887,31.349 +2020-10-20 10:30:00,130.71,117.34899999999999,60.887,31.349 +2020-10-20 10:45:00,129.56,116.9,60.887,31.349 +2020-10-20 11:00:00,126.57,113.57799999999999,59.812,31.349 +2020-10-20 11:15:00,127.65,113.931,59.812,31.349 +2020-10-20 11:30:00,129.33,114.149,59.812,31.349 +2020-10-20 11:45:00,128.71,114.49700000000001,59.812,31.349 +2020-10-20 12:00:00,126.55,110.807,56.614,31.349 +2020-10-20 12:15:00,128.41,109.98,56.614,31.349 +2020-10-20 12:30:00,126.5,109.773,56.614,31.349 +2020-10-20 12:45:00,126.79,110.145,56.614,31.349 +2020-10-20 13:00:00,125.27,110.044,56.824,31.349 +2020-10-20 13:15:00,127.5,109.61200000000001,56.824,31.349 +2020-10-20 13:30:00,127.16,108.911,56.824,31.349 +2020-10-20 13:45:00,125.66,108.56200000000001,56.824,31.349 +2020-10-20 14:00:00,126.58,108.228,57.623999999999995,31.349 +2020-10-20 14:15:00,125.58,108.148,57.623999999999995,31.349 +2020-10-20 14:30:00,125.54,107.60600000000001,57.623999999999995,31.349 +2020-10-20 14:45:00,124.61,107.48700000000001,57.623999999999995,31.349 +2020-10-20 15:00:00,124.48,106.787,59.724,31.349 +2020-10-20 15:15:00,125.53,106.771,59.724,31.349 +2020-10-20 15:30:00,122.51,106.945,59.724,31.349 +2020-10-20 15:45:00,123.47,106.96700000000001,59.724,31.349 +2020-10-20 16:00:00,124.86,107.792,61.64,31.349 +2020-10-20 16:15:00,126.88,108.274,61.64,31.349 +2020-10-20 16:30:00,127.79,108.666,61.64,31.349 +2020-10-20 16:45:00,130.62,107.39299999999999,61.64,31.349 +2020-10-20 17:00:00,132.63,107.946,68.962,31.349 +2020-10-20 17:15:00,134.24,108.882,68.962,31.349 +2020-10-20 17:30:00,132.03,108.838,68.962,31.349 +2020-10-20 17:45:00,129.1,108.991,68.962,31.349 +2020-10-20 18:00:00,128.24,110.06,69.149,31.349 +2020-10-20 18:15:00,127.87,109.694,69.149,31.349 +2020-10-20 18:30:00,123.22,108.37899999999999,69.149,31.349 +2020-10-20 18:45:00,124.82,111.66799999999999,69.149,31.349 +2020-10-20 19:00:00,115.2,112.62100000000001,68.832,31.349 +2020-10-20 19:15:00,110.22,111.197,68.832,31.349 +2020-10-20 19:30:00,108.41,110.40899999999999,68.832,31.349 +2020-10-20 19:45:00,104.68,109.90299999999999,68.832,31.349 +2020-10-20 20:00:00,101.56,107.01299999999999,66.403,31.349 +2020-10-20 20:15:00,101.65,104.178,66.403,31.349 +2020-10-20 20:30:00,100.36,103.81,66.403,31.349 +2020-10-20 20:45:00,100.6,102.27600000000001,66.403,31.349 +2020-10-20 21:00:00,91.23,98.71700000000001,57.352,31.349 +2020-10-20 21:15:00,90.92,98.59700000000001,57.352,31.349 +2020-10-20 21:30:00,88.27,97.89299999999999,57.352,31.349 +2020-10-20 21:45:00,86.17,95.569,57.352,31.349 +2020-10-20 22:00:00,81.6,91.111,51.148999999999994,31.349 +2020-10-20 22:15:00,80.06,87.729,51.148999999999994,31.349 +2020-10-20 22:30:00,78.33,76.27,51.148999999999994,31.349 +2020-10-20 22:45:00,77.72,69.81,51.148999999999994,31.349 +2020-10-20 23:00:00,75.14,64.126,41.8,31.349 +2020-10-20 23:15:00,72.31,63.852,41.8,31.349 +2020-10-20 23:30:00,72.2,62.843,41.8,31.349 +2020-10-20 23:45:00,70.69,63.091,41.8,31.349 +2020-10-21 00:00:00,69.76,59.614,42.269,31.349 +2020-10-21 00:15:00,70.51,61.033,42.269,31.349 +2020-10-21 00:30:00,68.91,60.648,42.269,31.349 +2020-10-21 00:45:00,70.39,60.449,42.269,31.349 +2020-10-21 01:00:00,68.96,61.236999999999995,38.527,31.349 +2020-10-21 01:15:00,69.7,60.805,38.527,31.349 +2020-10-21 01:30:00,66.77,60.242,38.527,31.349 +2020-10-21 01:45:00,69.61,59.981,38.527,31.349 +2020-10-21 02:00:00,69.44,60.858000000000004,36.393,31.349 +2020-10-21 02:15:00,70.57,61.048,36.393,31.349 +2020-10-21 02:30:00,70.25,61.61600000000001,36.393,31.349 +2020-10-21 02:45:00,70.99,62.246,36.393,31.349 +2020-10-21 03:00:00,72.78,64.939,36.167,31.349 +2020-10-21 03:15:00,73.7,66.257,36.167,31.349 +2020-10-21 03:30:00,74.4,66.502,36.167,31.349 +2020-10-21 03:45:00,74.06,67.263,36.167,31.349 +2020-10-21 04:00:00,80.73,77.21600000000001,38.092,31.349 +2020-10-21 04:15:00,83.23,86.816,38.092,31.349 +2020-10-21 04:30:00,94.46,86.675,38.092,31.349 +2020-10-21 04:45:00,98.59,88.64200000000001,38.092,31.349 +2020-10-21 05:00:00,105.32,117.571,42.268,31.349 +2020-10-21 05:15:00,106.7,143.113,42.268,31.349 +2020-10-21 05:30:00,113.14,137.283,42.268,31.349 +2020-10-21 05:45:00,116.18,128.509,42.268,31.349 +2020-10-21 06:00:00,122.3,127.931,60.158,31.349 +2020-10-21 06:15:00,125.24,132.197,60.158,31.349 +2020-10-21 06:30:00,128.18,131.024,60.158,31.349 +2020-10-21 06:45:00,129.46,131.575,60.158,31.349 +2020-10-21 07:00:00,132.09,132.361,74.792,31.349 +2020-10-21 07:15:00,133.19,135.106,74.792,31.349 +2020-10-21 07:30:00,135.44,135.514,74.792,31.349 +2020-10-21 07:45:00,133.65,135.642,74.792,31.349 +2020-10-21 08:00:00,132.41,135.57,70.499,31.349 +2020-10-21 08:15:00,134.22,135.18200000000002,70.499,31.349 +2020-10-21 08:30:00,134.75,132.749,70.499,31.349 +2020-10-21 08:45:00,134.77,130.888,70.499,31.349 +2020-10-21 09:00:00,135.22,126.735,68.892,31.349 +2020-10-21 09:15:00,136.3,124.346,68.892,31.349 +2020-10-21 09:30:00,138.43,124.294,68.892,31.349 +2020-10-21 09:45:00,138.85,123.74600000000001,68.892,31.349 +2020-10-21 10:00:00,138.15,120.583,66.88600000000001,31.349 +2020-10-21 10:15:00,138.15,119.40100000000001,66.88600000000001,31.349 +2020-10-21 10:30:00,137.96,117.815,66.88600000000001,31.349 +2020-10-21 10:45:00,135.02,117.351,66.88600000000001,31.349 +2020-10-21 11:00:00,131.3,114.03200000000001,66.187,31.349 +2020-10-21 11:15:00,131.26,114.366,66.187,31.349 +2020-10-21 11:30:00,131.11,114.583,66.187,31.349 +2020-10-21 11:45:00,132.59,114.916,66.187,31.349 +2020-10-21 12:00:00,129.61,111.206,62.18,31.349 +2020-10-21 12:15:00,129.87,110.37299999999999,62.18,31.349 +2020-10-21 12:30:00,128.91,110.20200000000001,62.18,31.349 +2020-10-21 12:45:00,128.14,110.572,62.18,31.349 +2020-10-21 13:00:00,124.86,110.43700000000001,62.23,31.349 +2020-10-21 13:15:00,121.86,110.012,62.23,31.349 +2020-10-21 13:30:00,123.13,109.31,62.23,31.349 +2020-10-21 13:45:00,120.42,108.958,62.23,31.349 +2020-10-21 14:00:00,118.39,108.573,63.721000000000004,31.349 +2020-10-21 14:15:00,120.77,108.508,63.721000000000004,31.349 +2020-10-21 14:30:00,121.85,108.00299999999999,63.721000000000004,31.349 +2020-10-21 14:45:00,121.34,107.882,63.721000000000004,31.349 +2020-10-21 15:00:00,118.71,107.161,66.523,31.349 +2020-10-21 15:15:00,118.61,107.163,66.523,31.349 +2020-10-21 15:30:00,118.37,107.37700000000001,66.523,31.349 +2020-10-21 15:45:00,117.66,107.412,66.523,31.349 +2020-10-21 16:00:00,117.97,108.20700000000001,69.679,31.349 +2020-10-21 16:15:00,119.99,108.711,69.679,31.349 +2020-10-21 16:30:00,122.09,109.101,69.679,31.349 +2020-10-21 16:45:00,123.57,107.87899999999999,69.679,31.349 +2020-10-21 17:00:00,130.15,108.39,75.04,31.349 +2020-10-21 17:15:00,128.12,109.345,75.04,31.349 +2020-10-21 17:30:00,132.36,109.304,75.04,31.349 +2020-10-21 17:45:00,129.08,109.47200000000001,75.04,31.349 +2020-10-21 18:00:00,130.24,110.53299999999999,75.915,31.349 +2020-10-21 18:15:00,127.7,110.141,75.915,31.349 +2020-10-21 18:30:00,127.89,108.836,75.915,31.349 +2020-10-21 18:45:00,123.32,112.119,75.915,31.349 +2020-10-21 19:00:00,118.23,113.083,74.66,31.349 +2020-10-21 19:15:00,113.94,111.652,74.66,31.349 +2020-10-21 19:30:00,110.24,110.85,74.66,31.349 +2020-10-21 19:45:00,109.15,110.32,74.66,31.349 +2020-10-21 20:00:00,103.44,107.45299999999999,71.204,31.349 +2020-10-21 20:15:00,104.11,104.609,71.204,31.349 +2020-10-21 20:30:00,104.78,104.212,71.204,31.349 +2020-10-21 20:45:00,105.75,102.661,71.204,31.349 +2020-10-21 21:00:00,100.72,99.09899999999999,61.052,31.349 +2020-10-21 21:15:00,95.64,98.96700000000001,61.052,31.349 +2020-10-21 21:30:00,91.76,98.273,61.052,31.349 +2020-10-21 21:45:00,95.11,95.929,61.052,31.349 +2020-10-21 22:00:00,91.39,91.464,54.691,31.349 +2020-10-21 22:15:00,87.48,88.06200000000001,54.691,31.349 +2020-10-21 22:30:00,84.33,76.632,54.691,31.349 +2020-10-21 22:45:00,85.24,70.179,54.691,31.349 +2020-10-21 23:00:00,81.86,64.51,45.18,31.349 +2020-10-21 23:15:00,83.34,64.208,45.18,31.349 +2020-10-21 23:30:00,79.08,63.201,45.18,31.349 +2020-10-21 23:45:00,78.07,63.438,45.18,31.349 +2020-10-22 00:00:00,78.96,59.951,42.746,31.349 +2020-10-22 00:15:00,80.34,61.357,42.746,31.349 +2020-10-22 00:30:00,76.75,60.979,42.746,31.349 +2020-10-22 00:45:00,74.47,60.773,42.746,31.349 +2020-10-22 01:00:00,74.08,61.575,40.025999999999996,31.349 +2020-10-22 01:15:00,78.96,61.161,40.025999999999996,31.349 +2020-10-22 01:30:00,78.92,60.614,40.025999999999996,31.349 +2020-10-22 01:45:00,72.66,60.349,40.025999999999996,31.349 +2020-10-22 02:00:00,72.85,61.236000000000004,38.154,31.349 +2020-10-22 02:15:00,72.18,61.438,38.154,31.349 +2020-10-22 02:30:00,80.12,61.99100000000001,38.154,31.349 +2020-10-22 02:45:00,80.24,62.618,38.154,31.349 +2020-10-22 03:00:00,77.98,65.297,37.575,31.349 +2020-10-22 03:15:00,77.21,66.63600000000001,37.575,31.349 +2020-10-22 03:30:00,76.58,66.88600000000001,37.575,31.349 +2020-10-22 03:45:00,81.48,67.62899999999999,37.575,31.349 +2020-10-22 04:00:00,90.01,77.601,39.154,31.349 +2020-10-22 04:15:00,92.44,87.23,39.154,31.349 +2020-10-22 04:30:00,87.34,87.086,39.154,31.349 +2020-10-22 04:45:00,93.79,89.06200000000001,39.154,31.349 +2020-10-22 05:00:00,105.64,118.059,44.085,31.349 +2020-10-22 05:15:00,114.87,143.665,44.085,31.349 +2020-10-22 05:30:00,114.7,137.82399999999998,44.085,31.349 +2020-10-22 05:45:00,113.54,129.018,44.085,31.349 +2020-10-22 06:00:00,122.12,128.42600000000002,57.49,31.349 +2020-10-22 06:15:00,124.14,132.705,57.49,31.349 +2020-10-22 06:30:00,127.55,131.553,57.49,31.349 +2020-10-22 06:45:00,128.59,132.114,57.49,31.349 +2020-10-22 07:00:00,132.08,132.9,73.617,31.349 +2020-10-22 07:15:00,132.26,135.658,73.617,31.349 +2020-10-22 07:30:00,133.23,136.096,73.617,31.349 +2020-10-22 07:45:00,133.61,136.227,73.617,31.349 +2020-10-22 08:00:00,133.59,136.168,69.281,31.349 +2020-10-22 08:15:00,133.94,135.75799999999998,69.281,31.349 +2020-10-22 08:30:00,133.56,133.351,69.281,31.349 +2020-10-22 08:45:00,134.29,131.466,69.281,31.349 +2020-10-22 09:00:00,132.11,127.307,63.926,31.349 +2020-10-22 09:15:00,134.27,124.915,63.926,31.349 +2020-10-22 09:30:00,133.85,124.851,63.926,31.349 +2020-10-22 09:45:00,133.96,124.27799999999999,63.926,31.349 +2020-10-22 10:00:00,131.38,121.10799999999999,59.442,31.349 +2020-10-22 10:15:00,133.21,119.88600000000001,59.442,31.349 +2020-10-22 10:30:00,132.9,118.28,59.442,31.349 +2020-10-22 10:45:00,132.11,117.79899999999999,59.442,31.349 +2020-10-22 11:00:00,127.4,114.485,56.771,31.349 +2020-10-22 11:15:00,129.2,114.801,56.771,31.349 +2020-10-22 11:30:00,127.65,115.016,56.771,31.349 +2020-10-22 11:45:00,125.62,115.334,56.771,31.349 +2020-10-22 12:00:00,123.46,111.602,53.701,31.349 +2020-10-22 12:15:00,124.88,110.765,53.701,31.349 +2020-10-22 12:30:00,119.09,110.62799999999999,53.701,31.349 +2020-10-22 12:45:00,120.14,110.99799999999999,53.701,31.349 +2020-10-22 13:00:00,122.1,110.82799999999999,52.364,31.349 +2020-10-22 13:15:00,123.26,110.411,52.364,31.349 +2020-10-22 13:30:00,124.34,109.709,52.364,31.349 +2020-10-22 13:45:00,124.93,109.353,52.364,31.349 +2020-10-22 14:00:00,128.1,108.916,53.419,31.349 +2020-10-22 14:15:00,124.78,108.868,53.419,31.349 +2020-10-22 14:30:00,123.07,108.398,53.419,31.349 +2020-10-22 14:45:00,119.7,108.27600000000001,53.419,31.349 +2020-10-22 15:00:00,121.74,107.53399999999999,56.744,31.349 +2020-10-22 15:15:00,119.68,107.554,56.744,31.349 +2020-10-22 15:30:00,119.77,107.806,56.744,31.349 +2020-10-22 15:45:00,118.01,107.85600000000001,56.744,31.349 +2020-10-22 16:00:00,123.58,108.62,60.458,31.349 +2020-10-22 16:15:00,123.95,109.146,60.458,31.349 +2020-10-22 16:30:00,124.24,109.53299999999999,60.458,31.349 +2020-10-22 16:45:00,126.61,108.363,60.458,31.349 +2020-10-22 17:00:00,132.29,108.83200000000001,66.295,31.349 +2020-10-22 17:15:00,131.4,109.807,66.295,31.349 +2020-10-22 17:30:00,130.67,109.76799999999999,66.295,31.349 +2020-10-22 17:45:00,130.55,109.95200000000001,66.295,31.349 +2020-10-22 18:00:00,130.07,111.006,68.468,31.349 +2020-10-22 18:15:00,127.24,110.588,68.468,31.349 +2020-10-22 18:30:00,126.24,109.292,68.468,31.349 +2020-10-22 18:45:00,124.94,112.571,68.468,31.349 +2020-10-22 19:00:00,116.69,113.544,66.39399999999999,31.349 +2020-10-22 19:15:00,113.13,112.10600000000001,66.39399999999999,31.349 +2020-10-22 19:30:00,109.94,111.291,66.39399999999999,31.349 +2020-10-22 19:45:00,109.01,110.73700000000001,66.39399999999999,31.349 +2020-10-22 20:00:00,106.55,107.891,63.183,31.349 +2020-10-22 20:15:00,110.78,105.039,63.183,31.349 +2020-10-22 20:30:00,109.45,104.61399999999999,63.183,31.349 +2020-10-22 20:45:00,102.93,103.04299999999999,63.183,31.349 +2020-10-22 21:00:00,95.75,99.479,55.133,31.349 +2020-10-22 21:15:00,94.53,99.337,55.133,31.349 +2020-10-22 21:30:00,91.45,98.652,55.133,31.349 +2020-10-22 21:45:00,96.05,96.286,55.133,31.349 +2020-10-22 22:00:00,89.93,91.816,50.111999999999995,31.349 +2020-10-22 22:15:00,88.13,88.395,50.111999999999995,31.349 +2020-10-22 22:30:00,89.13,76.995,50.111999999999995,31.349 +2020-10-22 22:45:00,89.9,70.547,50.111999999999995,31.349 +2020-10-22 23:00:00,83.75,64.893,44.536,31.349 +2020-10-22 23:15:00,78.07,64.564,44.536,31.349 +2020-10-22 23:30:00,80.56,63.558,44.536,31.349 +2020-10-22 23:45:00,82.63,63.785,44.536,31.349 +2020-10-23 00:00:00,78.83,58.907,42.291000000000004,31.349 +2020-10-23 00:15:00,76.14,60.5,42.291000000000004,31.349 +2020-10-23 00:30:00,70.38,60.188,42.291000000000004,31.349 +2020-10-23 00:45:00,78.33,60.231,42.291000000000004,31.349 +2020-10-23 01:00:00,77.49,60.708999999999996,41.008,31.349 +2020-10-23 01:15:00,78.38,60.483000000000004,41.008,31.349 +2020-10-23 01:30:00,72.93,60.181999999999995,41.008,31.349 +2020-10-23 01:45:00,73.02,59.847,41.008,31.349 +2020-10-23 02:00:00,71.03,61.23,39.521,31.349 +2020-10-23 02:15:00,73.65,61.361000000000004,39.521,31.349 +2020-10-23 02:30:00,79.52,62.555,39.521,31.349 +2020-10-23 02:45:00,80.33,62.872,39.521,31.349 +2020-10-23 03:00:00,79.49,65.42,39.812,31.349 +2020-10-23 03:15:00,77.2,66.66199999999999,39.812,31.349 +2020-10-23 03:30:00,77.66,66.79,39.812,31.349 +2020-10-23 03:45:00,80.08,68.09899999999999,39.812,31.349 +2020-10-23 04:00:00,93.02,78.267,41.22,31.349 +2020-10-23 04:15:00,92.04,87.04899999999999,41.22,31.349 +2020-10-23 04:30:00,95.88,87.476,41.22,31.349 +2020-10-23 04:45:00,103.64,88.589,41.22,31.349 +2020-10-23 05:00:00,110.56,116.802,45.115,31.349 +2020-10-23 05:15:00,109.64,143.666,45.115,31.349 +2020-10-23 05:30:00,112.58,138.431,45.115,31.349 +2020-10-23 05:45:00,115.81,129.345,45.115,31.349 +2020-10-23 06:00:00,127.62,129.082,59.06100000000001,31.349 +2020-10-23 06:15:00,125.7,132.726,59.06100000000001,31.349 +2020-10-23 06:30:00,129.36,131.142,59.06100000000001,31.349 +2020-10-23 06:45:00,130.72,132.45600000000002,59.06100000000001,31.349 +2020-10-23 07:00:00,132.87,133.134,71.874,31.349 +2020-10-23 07:15:00,132.91,136.845,71.874,31.349 +2020-10-23 07:30:00,132.75,136.32399999999998,71.874,31.349 +2020-10-23 07:45:00,132.39,135.908,71.874,31.349 +2020-10-23 08:00:00,135.33,135.643,68.439,31.349 +2020-10-23 08:15:00,133.18,135.3,68.439,31.349 +2020-10-23 08:30:00,131.71,133.393,68.439,31.349 +2020-10-23 08:45:00,132.74,130.588,68.439,31.349 +2020-10-23 09:00:00,130.86,125.649,65.523,31.349 +2020-10-23 09:15:00,129.44,124.387,65.523,31.349 +2020-10-23 09:30:00,130.21,123.78299999999999,65.523,31.349 +2020-10-23 09:45:00,126.99,123.292,65.523,31.349 +2020-10-23 10:00:00,126.29,119.389,62.005,31.349 +2020-10-23 10:15:00,126.97,118.429,62.005,31.349 +2020-10-23 10:30:00,127.62,116.988,62.005,31.349 +2020-10-23 10:45:00,128.81,116.204,62.005,31.349 +2020-10-23 11:00:00,125.12,112.964,60.351000000000006,31.349 +2020-10-23 11:15:00,124.04,112.315,60.351000000000006,31.349 +2020-10-23 11:30:00,124.64,113.381,60.351000000000006,31.349 +2020-10-23 11:45:00,128.31,113.32799999999999,60.351000000000006,31.349 +2020-10-23 12:00:00,121.36,110.387,55.331,31.349 +2020-10-23 12:15:00,123.71,108.07700000000001,55.331,31.349 +2020-10-23 12:30:00,121.4,108.113,55.331,31.349 +2020-10-23 12:45:00,120.71,108.477,55.331,31.349 +2020-10-23 13:00:00,114.05,109.057,53.361999999999995,31.349 +2020-10-23 13:15:00,112.96,109.195,53.361999999999995,31.349 +2020-10-23 13:30:00,114.06,108.807,53.361999999999995,31.349 +2020-10-23 13:45:00,109.64,108.527,53.361999999999995,31.349 +2020-10-23 14:00:00,109.29,107.06299999999999,51.708,31.349 +2020-10-23 14:15:00,107.91,107.09200000000001,51.708,31.349 +2020-10-23 14:30:00,109.64,107.539,51.708,31.349 +2020-10-23 14:45:00,108.55,107.329,51.708,31.349 +2020-10-23 15:00:00,108.97,106.251,54.571000000000005,31.349 +2020-10-23 15:15:00,110.24,105.94,54.571000000000005,31.349 +2020-10-23 15:30:00,110.25,105.12200000000001,54.571000000000005,31.349 +2020-10-23 15:45:00,111.4,105.56299999999999,54.571000000000005,31.349 +2020-10-23 16:00:00,111.51,105.333,58.662,31.349 +2020-10-23 16:15:00,113.23,106.244,58.662,31.349 +2020-10-23 16:30:00,114.42,106.60799999999999,58.662,31.349 +2020-10-23 16:45:00,118.53,105.10600000000001,58.662,31.349 +2020-10-23 17:00:00,122.49,106.39399999999999,65.941,31.349 +2020-10-23 17:15:00,122.56,107.088,65.941,31.349 +2020-10-23 17:30:00,123.72,106.946,65.941,31.349 +2020-10-23 17:45:00,123.59,106.948,65.941,31.349 +2020-10-23 18:00:00,121.84,108.42299999999999,65.628,31.349 +2020-10-23 18:15:00,121.3,107.361,65.628,31.349 +2020-10-23 18:30:00,117.05,106.244,65.628,31.349 +2020-10-23 18:45:00,113.81,109.695,65.628,31.349 +2020-10-23 19:00:00,107.92,111.56299999999999,63.662,31.349 +2020-10-23 19:15:00,105.21,111.102,63.662,31.349 +2020-10-23 19:30:00,103.36,110.083,63.662,31.349 +2020-10-23 19:45:00,100.53,108.814,63.662,31.349 +2020-10-23 20:00:00,96.19,105.954,61.945,31.349 +2020-10-23 20:15:00,96.91,103.45,61.945,31.349 +2020-10-23 20:30:00,98.96,102.758,61.945,31.349 +2020-10-23 20:45:00,98.21,101.104,61.945,31.349 +2020-10-23 21:00:00,88.95,98.39200000000001,53.903,31.349 +2020-10-23 21:15:00,85.03,99.21700000000001,53.903,31.349 +2020-10-23 21:30:00,86.27,98.49700000000001,53.903,31.349 +2020-10-23 21:45:00,86.36,96.485,53.903,31.349 +2020-10-23 22:00:00,82.11,92.458,48.403999999999996,31.349 +2020-10-23 22:15:00,79.76,88.836,48.403999999999996,31.349 +2020-10-23 22:30:00,78.87,82.929,48.403999999999996,31.349 +2020-10-23 22:45:00,77.66,78.721,48.403999999999996,31.349 +2020-10-23 23:00:00,72.45,73.60300000000001,41.07,31.349 +2020-10-23 23:15:00,66.27,71.525,41.07,31.349 +2020-10-23 23:30:00,71.85,68.943,41.07,31.349 +2020-10-23 23:45:00,72.89,68.757,41.07,31.349 +2020-10-24 00:00:00,69.15,58.409,38.989000000000004,31.177 +2020-10-24 00:15:00,66.21,57.553999999999995,38.989000000000004,31.177 +2020-10-24 00:30:00,66.17,57.687,38.989000000000004,31.177 +2020-10-24 00:45:00,68.56,57.732,38.989000000000004,31.177 +2020-10-24 01:00:00,67.79,58.681000000000004,35.275,31.177 +2020-10-24 01:15:00,63.68,58.266000000000005,35.275,31.177 +2020-10-24 01:30:00,65.18,57.342,35.275,31.177 +2020-10-24 01:45:00,67.86,57.486999999999995,35.275,31.177 +2020-10-24 02:00:00,66.58,58.766999999999996,32.838,31.177 +2020-10-24 02:15:00,64.36,58.35,32.838,31.177 +2020-10-24 02:30:00,61.86,58.606,32.838,31.177 +2020-10-24 02:45:00,66.88,59.343999999999994,32.838,31.177 +2020-10-24 03:00:00,65.58,61.542,32.418,31.177 +2020-10-24 03:15:00,60.76,61.876000000000005,32.418,31.177 +2020-10-24 03:30:00,61.45,61.39,32.418,31.177 +2020-10-24 03:45:00,60.33,63.449,32.418,31.177 +2020-10-24 04:00:00,61.63,70.747,32.099000000000004,31.177 +2020-10-24 04:15:00,61.62,77.895,32.099000000000004,31.177 +2020-10-24 04:30:00,61.95,76.46,32.099000000000004,31.177 +2020-10-24 04:45:00,64.96,77.475,32.099000000000004,31.177 +2020-10-24 05:00:00,66.95,94.19200000000001,32.926,31.177 +2020-10-24 05:15:00,68.94,106.695,32.926,31.177 +2020-10-24 05:30:00,70.32,102.368,32.926,31.177 +2020-10-24 05:45:00,72.01,98.037,32.926,31.177 +2020-10-24 06:00:00,75.35,111.932,35.069,31.177 +2020-10-24 06:15:00,77.38,127.29899999999999,35.069,31.177 +2020-10-24 06:30:00,80.27,121.75399999999999,35.069,31.177 +2020-10-24 06:45:00,82.52,117.161,35.069,31.177 +2020-10-24 07:00:00,84.52,114.94,40.906,31.177 +2020-10-24 07:15:00,86.47,117.434,40.906,31.177 +2020-10-24 07:30:00,88.55,119.00399999999999,40.906,31.177 +2020-10-24 07:45:00,90.46,121.02,40.906,31.177 +2020-10-24 08:00:00,93.09,122.844,46.603,31.177 +2020-10-24 08:15:00,92.39,124.279,46.603,31.177 +2020-10-24 08:30:00,92.69,123.23299999999999,46.603,31.177 +2020-10-24 08:45:00,92.18,122.46799999999999,46.603,31.177 +2020-10-24 09:00:00,92.61,119.697,49.935,31.177 +2020-10-24 09:15:00,95.38,119.044,49.935,31.177 +2020-10-24 09:30:00,92.79,119.12299999999999,49.935,31.177 +2020-10-24 09:45:00,98.37,118.501,49.935,31.177 +2020-10-24 10:00:00,95.5,114.84299999999999,47.585,31.177 +2020-10-24 10:15:00,96.58,114.081,47.585,31.177 +2020-10-24 10:30:00,97.94,112.571,47.585,31.177 +2020-10-24 10:45:00,98.26,112.348,47.585,31.177 +2020-10-24 11:00:00,98.27,109.11399999999999,43.376999999999995,31.177 +2020-10-24 11:15:00,98.66,108.404,43.376999999999995,31.177 +2020-10-24 11:30:00,99.06,108.95299999999999,43.376999999999995,31.177 +2020-10-24 11:45:00,96.15,108.61399999999999,43.376999999999995,31.177 +2020-10-24 12:00:00,91.6,105.161,40.855,31.177 +2020-10-24 12:15:00,91.87,103.545,40.855,31.177 +2020-10-24 12:30:00,93.38,103.74799999999999,40.855,31.177 +2020-10-24 12:45:00,90.21,103.949,40.855,31.177 +2020-10-24 13:00:00,86.6,103.87899999999999,37.251,31.177 +2020-10-24 13:15:00,86.51,102.62299999999999,37.251,31.177 +2020-10-24 13:30:00,85.95,102.045,37.251,31.177 +2020-10-24 13:45:00,85.82,101.522,37.251,31.177 +2020-10-24 14:00:00,86.06,100.728,38.548,31.177 +2020-10-24 14:15:00,86.43,99.986,38.548,31.177 +2020-10-24 14:30:00,86.03,99.24700000000001,38.548,31.177 +2020-10-24 14:45:00,85.75,99.345,38.548,31.177 +2020-10-24 15:00:00,84.72,98.795,42.883,31.177 +2020-10-24 15:15:00,85.56,99.241,42.883,31.177 +2020-10-24 15:30:00,85.6,99.435,42.883,31.177 +2020-10-24 15:45:00,86.3,99.54899999999999,42.883,31.177 +2020-10-24 16:00:00,89.01,99.697,48.143,31.177 +2020-10-24 16:15:00,89.51,100.771,48.143,31.177 +2020-10-24 16:30:00,92.55,101.194,48.143,31.177 +2020-10-24 16:45:00,96.25,100.23100000000001,48.143,31.177 +2020-10-24 17:00:00,102.45,100.84299999999999,55.25,31.177 +2020-10-24 17:15:00,102.68,101.559,55.25,31.177 +2020-10-24 17:30:00,105.3,101.311,55.25,31.177 +2020-10-24 17:45:00,102.74,101.31299999999999,55.25,31.177 +2020-10-24 18:00:00,102.08,103.189,57.506,31.177 +2020-10-24 18:15:00,99.7,103.78200000000001,57.506,31.177 +2020-10-24 18:30:00,98.42,103.971,57.506,31.177 +2020-10-24 18:45:00,96.26,104.133,57.506,31.177 +2020-10-24 19:00:00,91.99,105.861,55.528999999999996,31.177 +2020-10-24 19:15:00,88.78,104.691,55.528999999999996,31.177 +2020-10-24 19:30:00,86.53,104.39200000000001,55.528999999999996,31.177 +2020-10-24 19:45:00,85.01,103.775,55.528999999999996,31.177 +2020-10-24 20:00:00,81.07,102.477,46.166000000000004,31.177 +2020-10-24 20:15:00,81.19,100.8,46.166000000000004,31.177 +2020-10-24 20:30:00,79.06,99.508,46.166000000000004,31.177 +2020-10-24 20:45:00,78.71,98.44,46.166000000000004,31.177 +2020-10-24 21:00:00,74.27,96.27,40.406,31.177 +2020-10-24 21:15:00,73.82,97.156,40.406,31.177 +2020-10-24 21:30:00,69.74,97.17,40.406,31.177 +2020-10-24 21:45:00,70.03,94.685,40.406,31.177 +2020-10-24 22:00:00,66.57,91.34200000000001,39.616,31.177 +2020-10-24 22:15:00,66.52,89.10799999999999,39.616,31.177 +2020-10-24 22:30:00,63.76,86.304,39.616,31.177 +2020-10-24 22:45:00,63.35,83.242,39.616,31.177 +2020-10-24 23:00:00,59.13,79.12100000000001,32.205,31.177 +2020-10-24 23:15:00,57.91,76.347,32.205,31.177 +2020-10-24 23:30:00,58.49,73.942,32.205,31.177 +2020-10-24 23:45:00,57.43,72.638,32.205,31.177 +2020-10-25 00:00:00,54.37,59.602,28.229,31.177 +2020-10-25 00:15:00,54.85,58.032,28.229,31.177 +2020-10-25 00:30:00,53.15,57.915,28.229,31.177 +2020-10-25 00:45:00,53.46,58.245,28.229,31.177 +2020-10-25 01:00:00,52.45,59.261,25.669,31.177 +2020-10-25 01:15:00,53.12,59.316,25.669,31.177 +2020-10-25 01:30:00,52.62,58.598,25.669,31.177 +2020-10-25 01:45:00,53.42,58.406000000000006,25.669,31.177 +2020-10-25 02:00:00,53.47,59.361999999999995,24.948,31.177 +2020-10-25 02:15:00,52.07,58.88,24.948,31.177 +2020-10-25 02:30:00,53.29,59.628,24.948,31.177 +2020-10-25 02:45:00,50.45,60.449,24.948,31.177 +2020-10-25 02:00:00,53.47,59.361999999999995,24.948,31.177 +2020-10-25 02:15:00,52.07,58.88,24.948,31.177 +2020-10-25 02:30:00,53.29,59.628,24.948,31.177 +2020-10-25 02:45:00,50.45,60.449,24.948,31.177 +2020-10-25 03:00:00,51.47,63.097,25.839000000000002,31.177 +2020-10-25 03:15:00,53.26,63.324,25.839000000000002,31.177 +2020-10-25 03:30:00,53.34,63.165,25.839000000000002,31.177 +2020-10-25 03:45:00,54.51,64.78699999999999,25.839000000000002,31.177 +2020-10-25 04:00:00,53.29,71.962,26.803,31.177 +2020-10-25 04:15:00,53.34,78.443,26.803,31.177 +2020-10-25 04:30:00,53.8,77.687,26.803,31.177 +2020-10-25 04:45:00,54.16,78.613,26.803,31.177 +2020-10-25 05:00:00,55.1,93.816,28.147,31.177 +2020-10-25 05:15:00,57.45,104.86200000000001,28.147,31.177 +2020-10-25 05:30:00,57.71,100.24700000000001,28.147,31.177 +2020-10-25 05:45:00,58.69,95.866,28.147,31.177 +2020-10-25 06:00:00,62.27,108.524,31.116,31.177 +2020-10-25 06:15:00,63.1,123.47,31.116,31.177 +2020-10-25 06:30:00,62.46,117.07799999999999,31.116,31.177 +2020-10-25 06:45:00,62.7,111.507,31.116,31.177 +2020-10-25 07:00:00,63.83,110.587,35.739000000000004,31.177 +2020-10-25 07:15:00,68.03,111.905,35.739000000000004,31.177 +2020-10-25 07:30:00,70.45,113.48200000000001,35.739000000000004,31.177 +2020-10-25 07:45:00,73.32,115.104,35.739000000000004,31.177 +2020-10-25 08:00:00,74.3,118.15899999999999,39.455999999999996,31.177 +2020-10-25 08:15:00,75.67,120.051,39.455999999999996,31.177 +2020-10-25 08:30:00,77.85,120.27600000000001,39.455999999999996,31.177 +2020-10-25 08:45:00,79.31,120.493,39.455999999999996,31.177 +2020-10-25 09:00:00,79.61,117.434,41.343999999999994,31.177 +2020-10-25 09:15:00,84.58,116.85700000000001,41.343999999999994,31.177 +2020-10-25 09:30:00,85.64,117.029,41.343999999999994,31.177 +2020-10-25 09:45:00,82.94,116.75399999999999,41.343999999999994,31.177 +2020-10-25 10:00:00,85.77,114.64299999999999,43.645,31.177 +2020-10-25 10:15:00,85.99,114.189,43.645,31.177 +2020-10-25 10:30:00,87.54,113.08,43.645,31.177 +2020-10-25 10:45:00,90.19,112.226,43.645,31.177 +2020-10-25 11:00:00,91.19,109.338,39.796,31.177 +2020-10-25 11:15:00,94.15,108.50299999999999,39.796,31.177 +2020-10-25 11:30:00,95.37,108.779,39.796,31.177 +2020-10-25 11:45:00,98.86,108.865,39.796,31.177 +2020-10-25 12:00:00,91.98,105.462,36.343,31.177 +2020-10-25 12:15:00,91.68,104.677,36.343,31.177 +2020-10-25 12:30:00,88.9,104.17200000000001,36.343,31.177 +2020-10-25 12:45:00,90.13,103.572,36.343,31.177 +2020-10-25 13:00:00,82.09,102.927,33.162,31.177 +2020-10-25 13:15:00,81.4,103.18,33.162,31.177 +2020-10-25 13:30:00,81.38,102.04700000000001,33.162,31.177 +2020-10-25 13:45:00,81.84,101.579,33.162,31.177 +2020-10-25 14:00:00,81.77,101.337,33.215,31.177 +2020-10-25 14:15:00,79.73,101.465,33.215,31.177 +2020-10-25 14:30:00,79.1,100.992,33.215,31.177 +2020-10-25 14:45:00,79.01,100.43799999999999,33.215,31.177 +2020-10-25 15:00:00,79.17,99.04700000000001,37.385999999999996,31.177 +2020-10-25 15:15:00,80.91,99.631,37.385999999999996,31.177 +2020-10-25 15:30:00,81.52,100.13,37.385999999999996,31.177 +2020-10-25 15:45:00,82.75,100.765,37.385999999999996,31.177 +2020-10-25 16:00:00,82.73,101.178,46.618,31.177 +2020-10-25 16:15:00,84.13,101.86399999999999,46.618,31.177 +2020-10-25 16:30:00,88.98,102.837,46.618,31.177 +2020-10-25 16:45:00,90.57,102.025,46.618,31.177 +2020-10-25 17:00:00,95.12,102.66799999999999,50.111000000000004,31.177 +2020-10-25 17:15:00,97.69,103.867,50.111000000000004,31.177 +2020-10-25 17:30:00,96.42,104.118,50.111000000000004,31.177 +2020-10-25 17:45:00,98.46,105.537,50.111000000000004,31.177 +2020-10-25 18:00:00,103.58,107.33200000000001,50.25,31.177 +2020-10-25 18:15:00,101.93,108.426,50.25,31.177 +2020-10-25 18:30:00,102.54,107.414,50.25,31.177 +2020-10-25 18:45:00,100.8,108.6,50.25,31.177 +2020-10-25 19:00:00,96.36,111.086,44.265,31.177 +2020-10-25 19:15:00,94.85,109.73100000000001,44.265,31.177 +2020-10-25 19:30:00,95.47,109.225,44.265,31.177 +2020-10-25 19:45:00,92.72,109.13799999999999,44.265,31.177 +2020-10-25 20:00:00,95.91,107.9,39.717,31.177 +2020-10-25 20:15:00,98.51,106.665,39.717,31.177 +2020-10-25 20:30:00,93.22,106.344,39.717,31.177 +2020-10-25 20:45:00,87.29,103.89200000000001,39.717,31.177 +2020-10-25 21:00:00,89.17,100.29299999999999,39.224000000000004,31.177 +2020-10-25 21:15:00,87.17,100.712,39.224000000000004,31.177 +2020-10-25 21:30:00,90.1,100.595,39.224000000000004,31.177 +2020-10-25 21:45:00,88.38,98.32,39.224000000000004,31.177 +2020-10-25 22:00:00,85.43,95.274,33.518,31.177 +2020-10-25 22:15:00,83.95,91.87799999999999,33.518,31.177 +2020-10-25 22:30:00,78.09,87.24700000000001,33.518,31.177 +2020-10-25 22:45:00,78.18,83.186,33.518,31.177 +2020-10-25 23:00:00,78.0,77.503,33.518,31.177 +2020-10-25 23:15:00,83.24,76.22399999999999,33.518,31.177 +2020-10-25 23:30:00,81.26,73.999,33.518,31.177 +2020-10-25 23:45:00,79.19,73.21600000000001,33.518,31.177 +2020-10-26 00:00:00,70.49,62.706,34.301,31.349 +2020-10-26 00:15:00,74.87,62.949,34.301,31.349 +2020-10-26 00:30:00,78.69,62.718999999999994,34.301,31.349 +2020-10-26 00:45:00,81.84,62.592,34.301,31.349 +2020-10-26 01:00:00,75.6,63.79600000000001,34.143,31.349 +2020-10-26 01:15:00,71.72,63.619,34.143,31.349 +2020-10-26 01:30:00,75.5,63.11600000000001,34.143,31.349 +2020-10-26 01:45:00,77.08,62.928999999999995,34.143,31.349 +2020-10-26 02:00:00,76.26,64.08800000000001,33.650999999999996,31.349 +2020-10-26 02:15:00,72.33,63.838,33.650999999999996,31.349 +2020-10-26 02:30:00,72.88,64.815,33.650999999999996,31.349 +2020-10-26 02:45:00,75.86,65.26100000000001,33.650999999999996,31.349 +2020-10-26 03:00:00,73.71,68.73,32.599000000000004,31.349 +2020-10-26 03:15:00,76.24,70.116,32.599000000000004,31.349 +2020-10-26 03:30:00,71.02,70.158,32.599000000000004,31.349 +2020-10-26 03:45:00,78.2,71.275,32.599000000000004,31.349 +2020-10-26 04:00:00,79.63,82.025,33.785,31.349 +2020-10-26 04:15:00,78.32,91.931,33.785,31.349 +2020-10-26 04:30:00,77.2,91.984,33.785,31.349 +2020-10-26 04:45:00,76.44,93.176,33.785,31.349 +2020-10-26 05:00:00,81.4,119.296,41.285,31.349 +2020-10-26 05:15:00,86.72,144.952,41.285,31.349 +2020-10-26 05:30:00,94.41,139.7,41.285,31.349 +2020-10-26 05:45:00,100.77,131.119,41.285,31.349 +2020-10-26 06:00:00,111.1,130.539,60.486000000000004,31.349 +2020-10-26 06:15:00,109.54,133.99,60.486000000000004,31.349 +2020-10-26 06:30:00,116.42,133.387,60.486000000000004,31.349 +2020-10-26 06:45:00,125.54,134.597,60.486000000000004,31.349 +2020-10-26 07:00:00,128.36,135.40200000000002,74.012,31.349 +2020-10-26 07:15:00,127.02,138.408,74.012,31.349 +2020-10-26 07:30:00,129.02,139.22899999999998,74.012,31.349 +2020-10-26 07:45:00,130.69,139.71200000000002,74.012,31.349 +2020-10-26 08:00:00,133.0,139.673,69.569,31.349 +2020-10-26 08:15:00,130.26,139.901,69.569,31.349 +2020-10-26 08:30:00,131.6,137.61700000000002,69.569,31.349 +2020-10-26 08:45:00,131.65,136.181,69.569,31.349 +2020-10-26 09:00:00,132.1,132.289,66.152,31.349 +2020-10-26 09:15:00,133.34,129.095,66.152,31.349 +2020-10-26 09:30:00,133.68,128.327,66.152,31.349 +2020-10-26 09:45:00,134.8,127.196,66.152,31.349 +2020-10-26 10:00:00,135.34,124.896,62.923,31.349 +2020-10-26 10:15:00,134.92,124.164,62.923,31.349 +2020-10-26 10:30:00,135.66,122.366,62.923,31.349 +2020-10-26 10:45:00,136.84,121.25299999999999,62.923,31.349 +2020-10-26 11:00:00,136.57,117.12899999999999,61.522,31.349 +2020-10-26 11:15:00,138.31,117.39399999999999,61.522,31.349 +2020-10-26 11:30:00,137.5,118.719,61.522,31.349 +2020-10-26 11:45:00,136.55,118.765,61.522,31.349 +2020-10-26 12:00:00,132.39,115.801,58.632,31.349 +2020-10-26 12:15:00,133.44,115.056,58.632,31.349 +2020-10-26 12:30:00,136.05,114.31200000000001,58.632,31.349 +2020-10-26 12:45:00,136.12,114.579,58.632,31.349 +2020-10-26 13:00:00,131.73,114.663,59.06,31.349 +2020-10-26 13:15:00,133.41,113.785,59.06,31.349 +2020-10-26 13:30:00,132.28,112.412,59.06,31.349 +2020-10-26 13:45:00,129.34,112.303,59.06,31.349 +2020-10-26 14:00:00,128.88,111.329,59.791000000000004,31.349 +2020-10-26 14:15:00,130.35,111.32799999999999,59.791000000000004,31.349 +2020-10-26 14:30:00,130.63,110.51799999999999,59.791000000000004,31.349 +2020-10-26 14:45:00,129.71,110.73700000000001,59.791000000000004,31.349 +2020-10-26 15:00:00,133.57,110.221,61.148,31.349 +2020-10-26 15:15:00,130.84,109.76799999999999,61.148,31.349 +2020-10-26 15:30:00,129.48,110.12,61.148,31.349 +2020-10-26 15:45:00,127.89,110.32600000000001,61.148,31.349 +2020-10-26 16:00:00,129.63,111.01299999999999,66.009,31.349 +2020-10-26 16:15:00,130.73,111.325,66.009,31.349 +2020-10-26 16:30:00,128.87,111.488,66.009,31.349 +2020-10-26 16:45:00,131.75,110.07600000000001,66.009,31.349 +2020-10-26 17:00:00,135.6,109.988,73.683,31.349 +2020-10-26 17:15:00,135.29,110.84899999999999,73.683,31.349 +2020-10-26 17:30:00,138.5,110.64399999999999,73.683,31.349 +2020-10-26 17:45:00,138.2,111.103,73.683,31.349 +2020-10-26 18:00:00,139.43,112.59899999999999,72.848,31.349 +2020-10-26 18:15:00,137.29,111.71,72.848,31.349 +2020-10-26 18:30:00,135.93,110.73100000000001,72.848,31.349 +2020-10-26 18:45:00,133.94,113.649,72.848,31.349 +2020-10-26 19:00:00,131.63,115.131,71.139,31.349 +2020-10-26 19:15:00,128.86,113.73299999999999,71.139,31.349 +2020-10-26 19:30:00,126.98,113.314,71.139,31.349 +2020-10-26 19:45:00,125.67,112.5,71.139,31.349 +2020-10-26 20:00:00,117.03,109.485,69.667,31.349 +2020-10-26 20:15:00,114.91,107.57,69.667,31.349 +2020-10-26 20:30:00,114.37,106.48299999999999,69.667,31.349 +2020-10-26 20:45:00,109.01,104.954,69.667,31.349 +2020-10-26 21:00:00,112.26,101.359,61.166000000000004,31.349 +2020-10-26 21:15:00,112.02,101.35799999999999,61.166000000000004,31.349 +2020-10-26 21:30:00,107.75,101.01100000000001,61.166000000000004,31.349 +2020-10-26 21:45:00,102.71,98.35700000000001,61.166000000000004,31.349 +2020-10-26 22:00:00,96.59,92.899,52.772,31.349 +2020-10-26 22:15:00,95.82,89.686,52.772,31.349 +2020-10-26 22:30:00,95.29,78.227,52.772,31.349 +2020-10-26 22:45:00,98.09,71.666,52.772,31.349 +2020-10-26 23:00:00,93.54,66.396,45.136,31.349 +2020-10-26 23:15:00,86.6,65.73100000000001,45.136,31.349 +2020-10-26 23:30:00,84.41,64.919,45.136,31.349 +2020-10-26 23:45:00,88.99,65.25399999999999,45.136,31.349 +2020-10-27 00:00:00,81.74,61.629,47.35,31.349 +2020-10-27 00:15:00,82.4,62.966,47.35,31.349 +2020-10-27 00:30:00,77.99,62.614,47.35,31.349 +2020-10-27 00:45:00,77.23,62.376000000000005,47.35,31.349 +2020-10-27 01:00:00,80.95,63.243,43.424,31.349 +2020-10-27 01:15:00,82.6,62.918,43.424,31.349 +2020-10-27 01:30:00,79.87,62.45399999999999,43.424,31.349 +2020-10-27 01:45:00,72.99,62.169,43.424,31.349 +2020-10-27 02:00:00,72.38,63.107,41.778999999999996,31.349 +2020-10-27 02:15:00,71.66,63.367,41.778999999999996,31.349 +2020-10-27 02:30:00,79.74,63.854,41.778999999999996,31.349 +2020-10-27 02:45:00,81.55,64.459,41.778999999999996,31.349 +2020-10-27 03:00:00,82.6,67.071,40.771,31.349 +2020-10-27 03:15:00,75.83,68.518,40.771,31.349 +2020-10-27 03:30:00,74.84,68.78,40.771,31.349 +2020-10-27 03:45:00,75.64,69.439,40.771,31.349 +2020-10-27 04:00:00,78.97,79.508,41.816,31.349 +2020-10-27 04:15:00,82.85,89.28299999999999,41.816,31.349 +2020-10-27 04:30:00,84.37,89.12799999999999,41.816,31.349 +2020-10-27 04:45:00,85.9,91.14399999999999,41.816,31.349 +2020-10-27 05:00:00,85.7,120.477,45.842,31.349 +2020-10-27 05:15:00,86.54,146.40200000000002,45.842,31.349 +2020-10-27 05:30:00,89.88,140.503,45.842,31.349 +2020-10-27 05:45:00,92.99,131.537,45.842,31.349 +2020-10-27 06:00:00,102.93,130.881,59.12,31.349 +2020-10-27 06:15:00,108.45,135.22899999999998,59.12,31.349 +2020-10-27 06:30:00,113.1,134.179,59.12,31.349 +2020-10-27 06:45:00,115.04,134.792,59.12,31.349 +2020-10-27 07:00:00,122.18,135.57399999999998,70.33,31.349 +2020-10-27 07:15:00,119.35,138.393,70.33,31.349 +2020-10-27 07:30:00,121.17,138.977,70.33,31.349 +2020-10-27 07:45:00,122.32,139.118,70.33,31.349 +2020-10-27 08:00:00,123.86,139.123,67.788,31.349 +2020-10-27 08:15:00,121.9,138.599,67.788,31.349 +2020-10-27 08:30:00,120.05,136.322,67.788,31.349 +2020-10-27 08:45:00,119.09,134.313,67.788,31.349 +2020-10-27 09:00:00,117.83,130.122,62.622,31.349 +2020-10-27 09:15:00,117.56,127.71700000000001,62.622,31.349 +2020-10-27 09:30:00,117.58,127.595,62.622,31.349 +2020-10-27 09:45:00,116.75,126.899,62.622,31.349 +2020-10-27 10:00:00,115.99,123.69200000000001,60.887,31.349 +2020-10-27 10:15:00,115.96,122.279,60.887,31.349 +2020-10-27 10:30:00,114.65,120.57,60.887,31.349 +2020-10-27 10:45:00,115.67,120.009,60.887,31.349 +2020-10-27 11:00:00,114.34,116.712,59.812,31.349 +2020-10-27 11:15:00,114.91,116.93299999999999,59.812,31.349 +2020-10-27 11:30:00,116.28,117.145,59.812,31.349 +2020-10-27 11:45:00,115.75,117.391,59.812,31.349 +2020-10-27 12:00:00,113.14,113.553,56.614,31.349 +2020-10-27 12:15:00,114.82,112.695,56.614,31.349 +2020-10-27 12:30:00,122.4,112.73200000000001,56.614,31.349 +2020-10-27 12:45:00,118.13,113.09700000000001,56.614,31.349 +2020-10-27 13:00:00,120.98,112.76,56.824,31.349 +2020-10-27 13:15:00,123.1,112.37700000000001,56.824,31.349 +2020-10-27 13:30:00,120.06,111.669,56.824,31.349 +2020-10-27 13:45:00,116.95,111.295,56.824,31.349 +2020-10-27 14:00:00,111.93,110.604,57.623999999999995,31.349 +2020-10-27 14:15:00,110.15,110.633,57.623999999999995,31.349 +2020-10-27 14:30:00,114.67,110.348,57.623999999999995,31.349 +2020-10-27 14:45:00,116.3,110.22,57.623999999999995,31.349 +2020-10-27 15:00:00,118.01,109.37299999999999,59.724,31.349 +2020-10-27 15:15:00,119.23,109.48100000000001,59.724,31.349 +2020-10-27 15:30:00,118.98,109.921,59.724,31.349 +2020-10-27 15:45:00,123.51,110.039,59.724,31.349 +2020-10-27 16:00:00,124.34,110.655,61.64,31.349 +2020-10-27 16:15:00,123.37,111.288,61.64,31.349 +2020-10-27 16:30:00,126.21,111.66,61.64,31.349 +2020-10-27 16:45:00,127.32,110.74799999999999,61.64,31.349 +2020-10-27 17:00:00,132.95,111.009,68.962,31.349 +2020-10-27 17:15:00,130.54,112.081,68.962,31.349 +2020-10-27 17:30:00,133.6,112.05799999999999,68.962,31.349 +2020-10-27 17:45:00,132.78,112.322,68.962,31.349 +2020-10-27 18:00:00,133.9,113.34,69.149,31.349 +2020-10-27 18:15:00,131.39,112.79799999999999,69.149,31.349 +2020-10-27 18:30:00,130.49,111.552,69.149,31.349 +2020-10-27 18:45:00,129.9,114.807,69.149,31.349 +2020-10-27 19:00:00,126.61,115.82600000000001,68.832,31.349 +2020-10-27 19:15:00,124.87,114.351,68.832,31.349 +2020-10-27 19:30:00,122.2,113.476,68.832,31.349 +2020-10-27 19:45:00,121.07,112.803,68.832,31.349 +2020-10-27 20:00:00,114.52,110.06200000000001,66.403,31.349 +2020-10-27 20:15:00,112.08,107.171,66.403,31.349 +2020-10-27 20:30:00,108.46,106.601,66.403,31.349 +2020-10-27 20:45:00,105.79,104.941,66.403,31.349 +2020-10-27 21:00:00,98.15,101.363,57.352,31.349 +2020-10-27 21:15:00,94.41,101.166,57.352,31.349 +2020-10-27 21:30:00,92.13,100.529,57.352,31.349 +2020-10-27 21:45:00,91.04,98.061,57.352,31.349 +2020-10-27 22:00:00,84.86,93.559,51.148999999999994,31.349 +2020-10-27 22:15:00,81.56,90.044,51.148999999999994,31.349 +2020-10-27 22:30:00,78.97,78.791,51.148999999999994,31.349 +2020-10-27 22:45:00,75.04,72.378,51.148999999999994,31.349 +2020-10-27 23:00:00,69.33,66.797,41.8,31.349 +2020-10-27 23:15:00,72.04,66.328,41.8,31.349 +2020-10-27 23:30:00,67.62,65.33,41.8,31.349 +2020-10-27 23:45:00,70.34,65.508,41.8,31.349 +2020-10-28 00:00:00,66.64,61.961999999999996,42.269,31.349 +2020-10-28 00:15:00,63.47,63.285,42.269,31.349 +2020-10-28 00:30:00,62.17,62.938,42.269,31.349 +2020-10-28 00:45:00,59.22,62.693000000000005,42.269,31.349 +2020-10-28 01:00:00,59.46,63.573,38.527,31.349 +2020-10-28 01:15:00,59.73,63.266000000000005,38.527,31.349 +2020-10-28 01:30:00,57.69,62.818000000000005,38.527,31.349 +2020-10-28 01:45:00,59.75,62.528,38.527,31.349 +2020-10-28 02:00:00,58.9,63.477,36.393,31.349 +2020-10-28 02:15:00,57.99,63.747,36.393,31.349 +2020-10-28 02:30:00,58.66,64.222,36.393,31.349 +2020-10-28 02:45:00,59.76,64.822,36.393,31.349 +2020-10-28 03:00:00,58.64,67.422,36.167,31.349 +2020-10-28 03:15:00,59.18,68.89,36.167,31.349 +2020-10-28 03:30:00,59.1,69.155,36.167,31.349 +2020-10-28 03:45:00,59.8,69.797,36.167,31.349 +2020-10-28 04:00:00,57.35,79.88600000000001,38.092,31.349 +2020-10-28 04:15:00,60.36,89.689,38.092,31.349 +2020-10-28 04:30:00,61.45,89.531,38.092,31.349 +2020-10-28 04:45:00,62.49,91.555,38.092,31.349 +2020-10-28 05:00:00,64.56,120.95700000000001,42.268,31.349 +2020-10-28 05:15:00,64.41,146.945,42.268,31.349 +2020-10-28 05:30:00,63.43,141.032,42.268,31.349 +2020-10-28 05:45:00,60.47,132.036,42.268,31.349 +2020-10-28 06:00:00,64.37,131.36700000000002,60.158,31.349 +2020-10-28 06:15:00,65.72,135.72799999999998,60.158,31.349 +2020-10-28 06:30:00,65.45,134.7,60.158,31.349 +2020-10-28 06:45:00,65.86,135.322,60.158,31.349 +2020-10-28 07:00:00,68.96,136.10399999999998,74.792,31.349 +2020-10-28 07:15:00,70.01,138.934,74.792,31.349 +2020-10-28 07:30:00,71.67,139.547,74.792,31.349 +2020-10-28 07:45:00,73.31,139.689,74.792,31.349 +2020-10-28 08:00:00,76.29,139.70600000000002,70.499,31.349 +2020-10-28 08:15:00,75.92,139.159,70.499,31.349 +2020-10-28 08:30:00,76.39,136.907,70.499,31.349 +2020-10-28 08:45:00,76.44,134.873,70.499,31.349 +2020-10-28 09:00:00,75.16,130.675,68.892,31.349 +2020-10-28 09:15:00,75.44,128.268,68.892,31.349 +2020-10-28 09:30:00,72.53,128.134,68.892,31.349 +2020-10-28 09:45:00,73.66,127.415,68.892,31.349 +2020-10-28 10:00:00,72.88,124.2,66.88600000000001,31.349 +2020-10-28 10:15:00,69.24,122.749,66.88600000000001,31.349 +2020-10-28 10:30:00,71.4,121.02,66.88600000000001,31.349 +2020-10-28 10:45:00,72.71,120.443,66.88600000000001,31.349 +2020-10-28 11:00:00,72.82,117.149,66.187,31.349 +2020-10-28 11:15:00,74.56,117.351,66.187,31.349 +2020-10-28 11:30:00,75.37,117.56200000000001,66.187,31.349 +2020-10-28 11:45:00,76.16,117.794,66.187,31.349 +2020-10-28 12:00:00,73.91,113.936,62.18,31.349 +2020-10-28 12:15:00,72.6,113.075,62.18,31.349 +2020-10-28 12:30:00,71.55,113.145,62.18,31.349 +2020-10-28 12:45:00,70.09,113.51,62.18,31.349 +2020-10-28 13:00:00,70.36,113.141,62.23,31.349 +2020-10-28 13:15:00,67.51,112.76299999999999,62.23,31.349 +2020-10-28 13:30:00,65.99,112.054,62.23,31.349 +2020-10-28 13:45:00,66.82,111.676,62.23,31.349 +2020-10-28 14:00:00,67.12,110.936,63.721000000000004,31.349 +2020-10-28 14:15:00,67.37,110.98,63.721000000000004,31.349 +2020-10-28 14:30:00,69.07,110.73200000000001,63.721000000000004,31.349 +2020-10-28 14:45:00,72.24,110.602,63.721000000000004,31.349 +2020-10-28 15:00:00,74.87,109.736,66.523,31.349 +2020-10-28 15:15:00,75.32,109.86,66.523,31.349 +2020-10-28 15:30:00,79.14,110.337,66.523,31.349 +2020-10-28 15:45:00,80.15,110.46799999999999,66.523,31.349 +2020-10-28 16:00:00,83.7,111.055,69.679,31.349 +2020-10-28 16:15:00,83.65,111.71,69.679,31.349 +2020-10-28 16:30:00,85.79,112.07799999999999,69.679,31.349 +2020-10-28 16:45:00,87.82,111.21600000000001,69.679,31.349 +2020-10-28 17:00:00,95.0,111.43700000000001,75.04,31.349 +2020-10-28 17:15:00,96.91,112.52799999999999,75.04,31.349 +2020-10-28 17:30:00,100.33,112.508,75.04,31.349 +2020-10-28 17:45:00,101.95,112.79,75.04,31.349 +2020-10-28 18:00:00,102.58,113.801,75.915,31.349 +2020-10-28 18:15:00,101.4,113.235,75.915,31.349 +2020-10-28 18:30:00,100.78,111.999,75.915,31.349 +2020-10-28 18:45:00,99.5,115.25,75.915,31.349 +2020-10-28 19:00:00,96.55,116.27600000000001,74.66,31.349 +2020-10-28 19:15:00,96.95,114.795,74.66,31.349 +2020-10-28 19:30:00,94.44,113.90700000000001,74.66,31.349 +2020-10-28 19:45:00,93.81,113.212,74.66,31.349 +2020-10-28 20:00:00,97.85,110.491,71.204,31.349 +2020-10-28 20:15:00,98.96,107.59299999999999,71.204,31.349 +2020-10-28 20:30:00,93.72,106.994,71.204,31.349 +2020-10-28 20:45:00,96.99,105.316,71.204,31.349 +2020-10-28 21:00:00,88.01,101.735,61.052,31.349 +2020-10-28 21:15:00,91.89,101.527,61.052,31.349 +2020-10-28 21:30:00,86.26,100.899,61.052,31.349 +2020-10-28 21:45:00,85.22,98.413,61.052,31.349 +2020-10-28 22:00:00,82.78,93.904,54.691,31.349 +2020-10-28 22:15:00,84.41,90.37200000000001,54.691,31.349 +2020-10-28 22:30:00,80.28,79.148,54.691,31.349 +2020-10-28 22:45:00,82.44,72.742,54.691,31.349 +2020-10-28 23:00:00,82.46,67.175,45.18,31.349 +2020-10-28 23:15:00,86.27,66.678,45.18,31.349 +2020-10-28 23:30:00,83.78,65.682,45.18,31.349 +2020-10-28 23:45:00,82.95,65.851,45.18,31.349 +2020-10-29 00:00:00,77.65,62.294,42.746,31.349 +2020-10-29 00:15:00,79.71,63.602,42.746,31.349 +2020-10-29 00:30:00,79.87,63.261,42.746,31.349 +2020-10-29 00:45:00,70.04,63.008,42.746,31.349 +2020-10-29 01:00:00,74.52,63.9,40.025999999999996,31.349 +2020-10-29 01:15:00,77.14,63.611000000000004,40.025999999999996,31.349 +2020-10-29 01:30:00,78.1,63.178999999999995,40.025999999999996,31.349 +2020-10-29 01:45:00,74.52,62.885,40.025999999999996,31.349 +2020-10-29 02:00:00,74.6,63.843999999999994,38.154,31.349 +2020-10-29 02:15:00,78.3,64.126,38.154,31.349 +2020-10-29 02:30:00,77.12,64.58800000000001,38.154,31.349 +2020-10-29 02:45:00,73.83,65.185,38.154,31.349 +2020-10-29 03:00:00,77.13,67.771,37.575,31.349 +2020-10-29 03:15:00,78.81,69.26100000000001,37.575,31.349 +2020-10-29 03:30:00,79.06,69.528,37.575,31.349 +2020-10-29 03:45:00,73.2,70.152,37.575,31.349 +2020-10-29 04:00:00,76.0,80.26,39.154,31.349 +2020-10-29 04:15:00,81.59,90.094,39.154,31.349 +2020-10-29 04:30:00,84.79,89.934,39.154,31.349 +2020-10-29 04:45:00,83.0,91.965,39.154,31.349 +2020-10-29 05:00:00,83.0,121.43299999999999,44.085,31.349 +2020-10-29 05:15:00,86.43,147.484,44.085,31.349 +2020-10-29 05:30:00,90.66,141.559,44.085,31.349 +2020-10-29 05:45:00,100.19,132.532,44.085,31.349 +2020-10-29 06:00:00,111.83,131.852,57.49,31.349 +2020-10-29 06:15:00,117.74,136.226,57.49,31.349 +2020-10-29 06:30:00,119.59,135.217,57.49,31.349 +2020-10-29 06:45:00,115.34,135.85,57.49,31.349 +2020-10-29 07:00:00,122.6,136.632,73.617,31.349 +2020-10-29 07:15:00,123.1,139.474,73.617,31.349 +2020-10-29 07:30:00,124.09,140.114,73.617,31.349 +2020-10-29 07:45:00,126.48,140.257,73.617,31.349 +2020-10-29 08:00:00,126.46,140.285,69.281,31.349 +2020-10-29 08:15:00,123.29,139.715,69.281,31.349 +2020-10-29 08:30:00,119.21,137.487,69.281,31.349 +2020-10-29 08:45:00,118.47,135.429,69.281,31.349 +2020-10-29 09:00:00,118.0,131.224,63.926,31.349 +2020-10-29 09:15:00,122.76,128.815,63.926,31.349 +2020-10-29 09:30:00,119.63,128.67,63.926,31.349 +2020-10-29 09:45:00,122.87,127.926,63.926,31.349 +2020-10-29 10:00:00,122.23,124.705,59.442,31.349 +2020-10-29 10:15:00,119.89,123.21600000000001,59.442,31.349 +2020-10-29 10:30:00,124.6,121.46700000000001,59.442,31.349 +2020-10-29 10:45:00,131.0,120.874,59.442,31.349 +2020-10-29 11:00:00,127.83,117.583,56.771,31.349 +2020-10-29 11:15:00,128.86,117.766,56.771,31.349 +2020-10-29 11:30:00,130.16,117.978,56.771,31.349 +2020-10-29 11:45:00,131.01,118.196,56.771,31.349 +2020-10-29 12:00:00,130.57,114.31700000000001,53.701,31.349 +2020-10-29 12:15:00,130.91,113.45200000000001,53.701,31.349 +2020-10-29 12:30:00,133.3,113.556,53.701,31.349 +2020-10-29 12:45:00,130.48,113.921,53.701,31.349 +2020-10-29 13:00:00,133.45,113.51899999999999,52.364,31.349 +2020-10-29 13:15:00,130.71,113.147,52.364,31.349 +2020-10-29 13:30:00,128.95,112.436,52.364,31.349 +2020-10-29 13:45:00,129.8,112.055,52.364,31.349 +2020-10-29 14:00:00,130.29,111.265,53.419,31.349 +2020-10-29 14:15:00,129.67,111.324,53.419,31.349 +2020-10-29 14:30:00,128.02,111.11200000000001,53.419,31.349 +2020-10-29 14:45:00,130.31,110.98299999999999,53.419,31.349 +2020-10-29 15:00:00,127.71,110.096,56.744,31.349 +2020-10-29 15:15:00,126.9,110.236,56.744,31.349 +2020-10-29 15:30:00,126.36,110.75,56.744,31.349 +2020-10-29 15:45:00,125.0,110.89399999999999,56.744,31.349 +2020-10-29 16:00:00,125.03,111.45200000000001,60.458,31.349 +2020-10-29 16:15:00,124.56,112.12799999999999,60.458,31.349 +2020-10-29 16:30:00,124.0,112.494,60.458,31.349 +2020-10-29 16:45:00,128.25,111.682,60.458,31.349 +2020-10-29 17:00:00,134.32,111.861,66.295,31.349 +2020-10-29 17:15:00,137.45,112.973,66.295,31.349 +2020-10-29 17:30:00,135.07,112.95700000000001,66.295,31.349 +2020-10-29 17:45:00,135.2,113.255,66.295,31.349 +2020-10-29 18:00:00,134.97,114.259,68.468,31.349 +2020-10-29 18:15:00,133.97,113.67,68.468,31.349 +2020-10-29 18:30:00,134.41,112.444,68.468,31.349 +2020-10-29 18:45:00,130.82,115.691,68.468,31.349 +2020-10-29 19:00:00,127.46,116.72399999999999,66.39399999999999,31.349 +2020-10-29 19:15:00,126.41,115.23700000000001,66.39399999999999,31.349 +2020-10-29 19:30:00,124.92,114.338,66.39399999999999,31.349 +2020-10-29 19:45:00,123.49,113.619,66.39399999999999,31.349 +2020-10-29 20:00:00,123.79,110.91799999999999,63.183,31.349 +2020-10-29 20:15:00,121.13,108.01299999999999,63.183,31.349 +2020-10-29 20:30:00,115.55,107.385,63.183,31.349 +2020-10-29 20:45:00,110.22,105.69,63.183,31.349 +2020-10-29 21:00:00,106.12,102.105,55.133,31.349 +2020-10-29 21:15:00,108.93,101.885,55.133,31.349 +2020-10-29 21:30:00,108.46,101.26799999999999,55.133,31.349 +2020-10-29 21:45:00,104.99,98.762,55.133,31.349 +2020-10-29 22:00:00,95.45,94.24700000000001,50.111999999999995,31.349 +2020-10-29 22:15:00,94.84,90.696,50.111999999999995,31.349 +2020-10-29 22:30:00,95.34,79.503,50.111999999999995,31.349 +2020-10-29 22:45:00,96.16,73.104,50.111999999999995,31.349 +2020-10-29 23:00:00,90.51,67.551,44.536,31.349 +2020-10-29 23:15:00,87.23,67.025,44.536,31.349 +2020-10-29 23:30:00,86.68,66.032,44.536,31.349 +2020-10-29 23:45:00,87.36,66.191,44.536,31.349 +2020-10-30 00:00:00,85.77,61.242,42.291000000000004,31.349 +2020-10-30 00:15:00,82.09,62.739,42.291000000000004,31.349 +2020-10-30 00:30:00,77.43,62.463,42.291000000000004,31.349 +2020-10-30 00:45:00,82.66,62.458,42.291000000000004,31.349 +2020-10-30 01:00:00,80.41,63.026,41.008,31.349 +2020-10-30 01:15:00,82.12,62.924,41.008,31.349 +2020-10-30 01:30:00,75.47,62.736000000000004,41.008,31.349 +2020-10-30 01:45:00,77.24,62.371,41.008,31.349 +2020-10-30 02:00:00,74.28,63.827,39.521,31.349 +2020-10-30 02:15:00,74.78,64.03699999999999,39.521,31.349 +2020-10-30 02:30:00,79.52,65.142,39.521,31.349 +2020-10-30 02:45:00,81.93,65.428,39.521,31.349 +2020-10-30 03:00:00,80.63,67.885,39.812,31.349 +2020-10-30 03:15:00,75.97,69.27600000000001,39.812,31.349 +2020-10-30 03:30:00,78.26,69.422,39.812,31.349 +2020-10-30 03:45:00,82.39,70.611,39.812,31.349 +2020-10-30 04:00:00,82.18,80.916,41.22,31.349 +2020-10-30 04:15:00,78.3,89.902,41.22,31.349 +2020-10-30 04:30:00,84.06,90.31200000000001,41.22,31.349 +2020-10-30 04:45:00,88.56,91.48200000000001,41.22,31.349 +2020-10-30 05:00:00,92.62,120.163,45.115,31.349 +2020-10-30 05:15:00,89.69,147.474,45.115,31.349 +2020-10-30 05:30:00,95.75,142.15200000000002,45.115,31.349 +2020-10-30 05:45:00,102.23,132.844,45.115,31.349 +2020-10-30 06:00:00,113.59,132.496,59.06100000000001,31.349 +2020-10-30 06:15:00,111.04,136.235,59.06100000000001,31.349 +2020-10-30 06:30:00,113.71,134.79399999999998,59.06100000000001,31.349 +2020-10-30 06:45:00,116.04,136.179,59.06100000000001,31.349 +2020-10-30 07:00:00,119.06,136.855,71.874,31.349 +2020-10-30 07:15:00,120.84,140.64700000000002,71.874,31.349 +2020-10-30 07:30:00,122.72,140.326,71.874,31.349 +2020-10-30 07:45:00,122.52,139.918,71.874,31.349 +2020-10-30 08:00:00,125.5,139.739,68.439,31.349 +2020-10-30 08:15:00,124.47,139.237,68.439,31.349 +2020-10-30 08:30:00,127.07,137.506,68.439,31.349 +2020-10-30 08:45:00,127.1,134.52700000000002,68.439,31.349 +2020-10-30 09:00:00,128.83,129.542,65.523,31.349 +2020-10-30 09:15:00,129.4,128.264,65.523,31.349 +2020-10-30 09:30:00,128.5,127.581,65.523,31.349 +2020-10-30 09:45:00,127.31,126.919,65.523,31.349 +2020-10-30 10:00:00,126.26,122.965,62.005,31.349 +2020-10-30 10:15:00,124.62,121.74,62.005,31.349 +2020-10-30 10:30:00,123.49,120.156,62.005,31.349 +2020-10-30 10:45:00,124.03,119.26,62.005,31.349 +2020-10-30 11:00:00,124.17,116.04299999999999,60.351000000000006,31.349 +2020-10-30 11:15:00,126.28,115.26100000000001,60.351000000000006,31.349 +2020-10-30 11:30:00,123.52,116.325,60.351000000000006,31.349 +2020-10-30 11:45:00,123.86,116.17299999999999,60.351000000000006,31.349 +2020-10-30 12:00:00,121.54,113.084,55.331,31.349 +2020-10-30 12:15:00,125.28,110.75,55.331,31.349 +2020-10-30 12:30:00,124.06,111.024,55.331,31.349 +2020-10-30 12:45:00,121.96,111.383,55.331,31.349 +2020-10-30 13:00:00,118.71,111.73299999999999,53.361999999999995,31.349 +2020-10-30 13:15:00,120.56,111.917,53.361999999999995,31.349 +2020-10-30 13:30:00,118.3,111.51799999999999,53.361999999999995,31.349 +2020-10-30 13:45:00,120.01,111.213,53.361999999999995,31.349 +2020-10-30 14:00:00,117.73,109.399,51.708,31.349 +2020-10-30 14:15:00,118.43,109.535,51.708,31.349 +2020-10-30 14:30:00,118.52,110.236,51.708,31.349 +2020-10-30 14:45:00,119.61,110.02,51.708,31.349 +2020-10-30 15:00:00,120.23,108.8,54.571000000000005,31.349 +2020-10-30 15:15:00,118.83,108.60799999999999,54.571000000000005,31.349 +2020-10-30 15:30:00,118.31,108.04899999999999,54.571000000000005,31.349 +2020-10-30 15:45:00,119.66,108.585,54.571000000000005,31.349 +2020-10-30 16:00:00,120.97,108.147,58.662,31.349 +2020-10-30 16:15:00,120.45,109.209,58.662,31.349 +2020-10-30 16:30:00,122.73,109.552,58.662,31.349 +2020-10-30 16:45:00,126.37,108.40799999999999,58.662,31.349 +2020-10-30 17:00:00,132.24,109.404,65.941,31.349 +2020-10-30 17:15:00,131.66,110.236,65.941,31.349 +2020-10-30 17:30:00,135.32,110.119,65.941,31.349 +2020-10-30 17:45:00,131.92,110.234,65.941,31.349 +2020-10-30 18:00:00,131.8,111.662,65.628,31.349 +2020-10-30 18:15:00,129.88,110.432,65.628,31.349 +2020-10-30 18:30:00,128.86,109.383,65.628,31.349 +2020-10-30 18:45:00,128.89,112.803,65.628,31.349 +2020-10-30 19:00:00,127.22,114.73,63.662,31.349 +2020-10-30 19:15:00,123.33,114.22,63.662,31.349 +2020-10-30 19:30:00,121.1,113.117,63.662,31.349 +2020-10-30 19:45:00,121.23,111.685,63.662,31.349 +2020-10-30 20:00:00,115.04,108.969,61.945,31.349 +2020-10-30 20:15:00,110.74,106.413,61.945,31.349 +2020-10-30 20:30:00,109.01,105.51799999999999,61.945,31.349 +2020-10-30 20:45:00,106.5,103.742,61.945,31.349 +2020-10-30 21:00:00,100.01,101.007,53.903,31.349 +2020-10-30 21:15:00,102.44,101.75200000000001,53.903,31.349 +2020-10-30 21:30:00,100.98,101.102,53.903,31.349 +2020-10-30 21:45:00,94.15,98.95299999999999,53.903,31.349 +2020-10-30 22:00:00,87.77,94.88,48.403999999999996,31.349 +2020-10-30 22:15:00,85.26,91.131,48.403999999999996,31.349 +2020-10-30 22:30:00,84.58,85.432,48.403999999999996,31.349 +2020-10-30 22:45:00,83.7,81.27199999999999,48.403999999999996,31.349 +2020-10-30 23:00:00,82.16,76.253,41.07,31.349 +2020-10-30 23:15:00,81.37,73.979,41.07,31.349 +2020-10-30 23:30:00,73.71,71.40899999999999,41.07,31.349 +2020-10-30 23:45:00,71.13,71.156,41.07,31.349 +2020-10-31 00:00:00,72.45,61.597,38.989000000000004,31.177 +2020-10-31 00:15:00,73.37,59.941,38.989000000000004,31.177 +2020-10-31 00:30:00,73.61,59.854,38.989000000000004,31.177 +2020-10-31 00:45:00,71.83,60.141000000000005,38.989000000000004,31.177 +2020-10-31 01:00:00,68.78,61.233000000000004,35.275,31.177 +2020-10-31 01:15:00,71.23,61.393,35.275,31.177 +2020-10-31 01:30:00,69.97,60.773,35.275,31.177 +2020-10-31 01:45:00,65.93,60.555,35.275,31.177 +2020-10-31 02:00:00,64.99,61.573,32.838,31.177 +2020-10-31 02:15:00,69.96,61.156000000000006,32.838,31.177 +2020-10-31 02:30:00,68.68,61.831,32.838,31.177 +2020-10-31 02:45:00,66.64,62.625,32.838,31.177 +2020-10-31 03:00:00,65.79,65.196,32.418,31.177 +2020-10-31 03:15:00,67.38,65.55199999999999,32.418,31.177 +2020-10-31 03:30:00,67.91,65.406,32.418,31.177 +2020-10-31 03:45:00,63.45,66.92699999999999,32.418,31.177 +2020-10-31 04:00:00,60.12,74.21600000000001,32.099000000000004,31.177 +2020-10-31 04:15:00,61.24,80.874,32.099000000000004,31.177 +2020-10-31 04:30:00,62.55,80.10300000000001,32.099000000000004,31.177 +2020-10-31 04:45:00,62.68,81.078,32.099000000000004,31.177 +2020-10-31 05:00:00,63.12,96.679,32.926,31.177 +2020-10-31 05:15:00,62.57,108.109,32.926,31.177 +2020-10-31 05:30:00,63.2,103.416,32.926,31.177 +2020-10-31 05:45:00,65.37,98.848,32.926,31.177 +2020-10-31 06:00:00,68.19,111.434,35.069,31.177 +2020-10-31 06:15:00,69.79,126.461,35.069,31.177 +2020-10-31 06:30:00,71.11,120.191,35.069,31.177 +2020-10-31 06:45:00,73.82,114.679,35.069,31.177 +2020-10-31 07:00:00,75.79,113.76,40.906,31.177 +2020-10-31 07:15:00,77.22,115.145,40.906,31.177 +2020-10-31 07:30:00,78.86,116.89,40.906,31.177 +2020-10-31 07:45:00,81.1,118.516,40.906,31.177 +2020-10-31 08:00:00,83.23,121.64399999999999,46.603,31.177 +2020-10-31 08:15:00,84.49,123.396,46.603,31.177 +2020-10-31 08:30:00,86.25,123.77,46.603,31.177 +2020-10-31 08:45:00,88.76,123.838,46.603,31.177 +2020-10-31 09:00:00,92.45,120.73899999999999,49.935,31.177 +2020-10-31 09:15:00,93.68,120.148,49.935,31.177 +2020-10-31 09:30:00,94.02,120.255,49.935,31.177 +2020-10-31 09:45:00,94.02,119.835,49.935,31.177 +2020-10-31 10:00:00,92.45,117.68,47.585,31.177 +2020-10-31 10:15:00,93.24,117.001,47.585,31.177 +2020-10-31 10:30:00,92.64,115.77,47.585,31.177 +2020-10-31 10:45:00,92.15,114.821,47.585,31.177 +2020-10-31 11:00:00,93.34,111.95100000000001,43.376999999999995,31.177 +2020-10-31 11:15:00,96.08,111.00299999999999,43.376999999999995,31.177 +2020-10-31 11:30:00,95.83,111.277,43.376999999999995,31.177 +2020-10-31 11:45:00,96.13,111.28,43.376999999999995,31.177 +2020-10-31 12:00:00,95.48,107.751,40.855,31.177 +2020-10-31 12:15:00,95.57,106.947,40.855,31.177 +2020-10-31 12:30:00,92.12,106.645,40.855,31.177 +2020-10-31 12:45:00,91.78,106.042,40.855,31.177 +2020-10-31 13:00:00,88.92,105.20100000000001,37.251,31.177 +2020-10-31 13:15:00,89.25,105.492,37.251,31.177 +2020-10-31 13:30:00,88.99,104.34899999999999,37.251,31.177 +2020-10-31 13:45:00,89.08,103.85700000000001,37.251,31.177 +2020-10-31 14:00:00,86.2,103.321,38.548,31.177 +2020-10-31 14:15:00,87.18,103.538,38.548,31.177 +2020-10-31 14:30:00,86.99,103.285,38.548,31.177 +2020-10-31 14:45:00,87.31,102.726,38.548,31.177 +2020-10-31 15:00:00,87.48,101.215,42.883,31.177 +2020-10-31 15:15:00,87.93,101.897,42.883,31.177 +2020-10-31 15:30:00,87.72,102.617,42.883,31.177 +2020-10-31 15:45:00,88.54,103.33200000000001,42.883,31.177 +2020-10-31 16:00:00,90.83,103.56700000000001,48.143,31.177 +2020-10-31 16:15:00,90.48,104.383,48.143,31.177 +2020-10-31 16:30:00,92.56,105.337,48.143,31.177 +2020-10-31 16:45:00,97.53,104.829,48.143,31.177 +2020-10-31 17:00:00,102.04,105.22399999999999,55.25,31.177 +2020-10-31 17:15:00,100.68,106.541,55.25,31.177 +2020-10-31 17:30:00,102.51,106.81700000000001,55.25,31.177 +2020-10-31 17:45:00,102.66,108.331,55.25,31.177 +2020-10-31 18:00:00,104.1,110.088,57.506,31.177 +2020-10-31 18:15:00,107.0,111.04,57.506,31.177 +2020-10-31 18:30:00,103.91,110.087,57.506,31.177 +2020-10-31 18:45:00,104.55,111.25,57.506,31.177 +2020-10-31 19:00:00,100.55,113.781,55.528999999999996,31.177 +2020-10-31 19:15:00,99.24,112.38600000000001,55.528999999999996,31.177 +2020-10-31 19:30:00,97.68,111.81,55.528999999999996,31.177 +2020-10-31 19:45:00,96.75,111.583,55.528999999999996,31.177 +2020-10-31 20:00:00,91.74,110.46700000000001,46.166000000000004,31.177 +2020-10-31 20:15:00,88.35,109.18799999999999,46.166000000000004,31.177 +2020-10-31 20:30:00,86.31,108.696,46.166000000000004,31.177 +2020-10-31 20:45:00,83.91,106.139,46.166000000000004,31.177 +2020-10-31 21:00:00,80.56,102.51899999999999,40.406,31.177 +2020-10-31 21:15:00,80.4,102.87,40.406,31.177 +2020-10-31 21:30:00,79.05,102.81200000000001,40.406,31.177 +2020-10-31 21:45:00,77.96,100.42200000000001,40.406,31.177 +2020-10-31 22:00:00,75.29,97.337,39.616,31.177 +2020-10-31 22:15:00,74.6,93.835,39.616,31.177 +2020-10-31 22:30:00,71.03,89.384,39.616,31.177 +2020-10-31 22:45:00,70.49,85.363,39.616,31.177 +2020-10-31 23:00:00,67.0,79.763,32.205,31.177 +2020-10-31 23:15:00,66.8,78.317,32.205,31.177 +2020-10-31 23:30:00,64.91,76.102,32.205,31.177 +2020-10-31 23:45:00,63.18,75.262,32.205,31.177 +2020-11-01 00:00:00,58.91,73.844,36.376,32.047 +2020-11-01 00:15:00,57.64,71.598,36.376,32.047 +2020-11-01 00:30:00,58.09,71.771,36.376,32.047 +2020-11-01 00:45:00,56.8,72.655,36.376,32.047 +2020-11-01 01:00:00,53.61,74.10300000000001,32.992,32.047 +2020-11-01 01:15:00,55.48,74.691,32.992,32.047 +2020-11-01 01:30:00,54.86,74.157,32.992,32.047 +2020-11-01 01:45:00,55.56,74.169,32.992,32.047 +2020-11-01 02:00:00,53.33,75.35600000000001,32.327,32.047 +2020-11-01 02:15:00,53.19,74.95,32.327,32.047 +2020-11-01 02:30:00,53.38,75.499,32.327,32.047 +2020-11-01 02:45:00,53.54,76.763,32.327,32.047 +2020-11-01 03:00:00,52.29,79.542,31.169,32.047 +2020-11-01 03:15:00,53.27,79.608,31.169,32.047 +2020-11-01 03:30:00,53.38,80.009,31.169,32.047 +2020-11-01 03:45:00,53.67,81.561,31.169,32.047 +2020-11-01 04:00:00,53.04,90.10600000000001,30.796,32.047 +2020-11-01 04:15:00,53.69,97.82600000000001,30.796,32.047 +2020-11-01 04:30:00,54.71,97.304,30.796,32.047 +2020-11-01 04:45:00,54.7,98.21600000000001,30.796,32.047 +2020-11-01 05:00:00,56.28,114.69200000000001,30.848000000000003,32.047 +2020-11-01 05:15:00,56.84,126.919,30.848000000000003,32.047 +2020-11-01 05:30:00,56.12,122.334,30.848000000000003,32.047 +2020-11-01 05:45:00,57.2,117.79299999999999,30.848000000000003,32.047 +2020-11-01 06:00:00,58.93,131.375,31.166,32.047 +2020-11-01 06:15:00,60.16,148.164,31.166,32.047 +2020-11-01 06:30:00,60.09,141.499,31.166,32.047 +2020-11-01 06:45:00,61.33,135.033,31.166,32.047 +2020-11-01 07:00:00,62.62,134.209,33.527,32.047 +2020-11-01 07:15:00,64.53,135.998,33.527,32.047 +2020-11-01 07:30:00,66.19,138.07299999999998,33.527,32.047 +2020-11-01 07:45:00,69.32,140.029,33.527,32.047 +2020-11-01 08:00:00,72.18,143.476,36.616,32.047 +2020-11-01 08:15:00,73.05,145.618,36.616,32.047 +2020-11-01 08:30:00,75.6,146.314,36.616,32.047 +2020-11-01 08:45:00,76.81,146.583,36.616,32.047 +2020-11-01 09:00:00,78.39,143.313,37.857,32.047 +2020-11-01 09:15:00,79.58,142.33100000000002,37.857,32.047 +2020-11-01 09:30:00,79.36,141.849,37.857,32.047 +2020-11-01 09:45:00,80.15,141.034,37.857,32.047 +2020-11-01 10:00:00,78.79,139.36,36.319,32.047 +2020-11-01 10:15:00,81.18,138.553,36.319,32.047 +2020-11-01 10:30:00,82.06,137.161,36.319,32.047 +2020-11-01 10:45:00,84.37,135.914,36.319,32.047 +2020-11-01 11:00:00,87.44,132.654,37.236999999999995,32.047 +2020-11-01 11:15:00,90.16,131.516,37.236999999999995,32.047 +2020-11-01 11:30:00,92.79,131.694,37.236999999999995,32.047 +2020-11-01 11:45:00,94.47,132.332,37.236999999999995,32.047 +2020-11-01 12:00:00,93.72,128.334,34.871,32.047 +2020-11-01 12:15:00,91.05,127.323,34.871,32.047 +2020-11-01 12:30:00,88.73,126.853,34.871,32.047 +2020-11-01 12:45:00,87.98,126.25399999999999,34.871,32.047 +2020-11-01 13:00:00,81.86,125.57600000000001,29.738000000000003,32.047 +2020-11-01 13:15:00,79.48,125.912,29.738000000000003,32.047 +2020-11-01 13:30:00,78.24,124.734,29.738000000000003,32.047 +2020-11-01 13:45:00,77.77,124.292,29.738000000000003,32.047 +2020-11-01 14:00:00,78.82,124.43,27.333000000000002,32.047 +2020-11-01 14:15:00,77.97,124.331,27.333000000000002,32.047 +2020-11-01 14:30:00,78.18,123.743,27.333000000000002,32.047 +2020-11-01 14:45:00,79.25,123.29,27.333000000000002,32.047 +2020-11-01 15:00:00,79.18,121.579,28.232,32.047 +2020-11-01 15:15:00,79.37,122.45100000000001,28.232,32.047 +2020-11-01 15:30:00,79.55,122.805,28.232,32.047 +2020-11-01 15:45:00,81.75,123.443,28.232,32.047 +2020-11-01 16:00:00,83.82,125.545,32.815,32.047 +2020-11-01 16:15:00,83.89,127.041,32.815,32.047 +2020-11-01 16:30:00,84.8,127.854,32.815,32.047 +2020-11-01 16:45:00,89.87,127.169,32.815,32.047 +2020-11-01 17:00:00,96.6,129.106,43.068999999999996,32.047 +2020-11-01 17:15:00,96.91,130.164,43.068999999999996,32.047 +2020-11-01 17:30:00,99.73,130.485,43.068999999999996,32.047 +2020-11-01 17:45:00,101.95,131.416,43.068999999999996,32.047 +2020-11-01 18:00:00,100.66,134.56799999999998,50.498999999999995,32.047 +2020-11-01 18:15:00,100.99,135.72299999999998,50.498999999999995,32.047 +2020-11-01 18:30:00,97.5,134.084,50.498999999999995,32.047 +2020-11-01 18:45:00,96.13,135.263,50.498999999999995,32.047 +2020-11-01 19:00:00,94.03,137.83700000000002,53.481,32.047 +2020-11-01 19:15:00,94.0,136.382,53.481,32.047 +2020-11-01 19:30:00,92.13,135.606,53.481,32.047 +2020-11-01 19:45:00,92.89,135.071,53.481,32.047 +2020-11-01 20:00:00,90.69,132.186,51.687,32.047 +2020-11-01 20:15:00,94.58,130.61700000000002,51.687,32.047 +2020-11-01 20:30:00,92.24,130.408,51.687,32.047 +2020-11-01 20:45:00,86.66,127.39,51.687,32.047 +2020-11-01 21:00:00,83.37,123.02,47.674,32.047 +2020-11-01 21:15:00,84.21,122.93,47.674,32.047 +2020-11-01 21:30:00,88.7,122.751,47.674,32.047 +2020-11-01 21:45:00,90.47,120.36399999999999,47.674,32.047 +2020-11-01 22:00:00,90.97,116.14,48.178000000000004,32.047 +2020-11-01 22:15:00,86.98,112.27799999999999,48.178000000000004,32.047 +2020-11-01 22:30:00,83.62,106.661,48.178000000000004,32.047 +2020-11-01 22:45:00,82.66,102.23,48.178000000000004,32.047 +2020-11-01 23:00:00,81.93,96.52,42.553999999999995,32.047 +2020-11-01 23:15:00,85.7,94.42200000000001,42.553999999999995,32.047 +2020-11-01 23:30:00,82.06,92.15700000000001,42.553999999999995,32.047 +2020-11-01 23:45:00,78.61,90.99799999999999,42.553999999999995,32.047 +2020-11-02 00:00:00,77.71,77.483,37.177,32.225 +2020-11-02 00:15:00,78.04,77.413,37.177,32.225 +2020-11-02 00:30:00,75.0,77.514,37.177,32.225 +2020-11-02 00:45:00,71.86,77.888,37.177,32.225 +2020-11-02 01:00:00,73.26,79.508,35.358000000000004,32.225 +2020-11-02 01:15:00,75.19,79.788,35.358000000000004,32.225 +2020-11-02 01:30:00,74.29,79.453,35.358000000000004,32.225 +2020-11-02 01:45:00,71.36,79.48899999999999,35.358000000000004,32.225 +2020-11-02 02:00:00,74.46,80.84899999999999,35.03,32.225 +2020-11-02 02:15:00,76.19,80.941,35.03,32.225 +2020-11-02 02:30:00,73.58,81.756,35.03,32.225 +2020-11-02 02:45:00,70.49,82.568,35.03,32.225 +2020-11-02 03:00:00,71.48,86.30799999999999,34.394,32.225 +2020-11-02 03:15:00,76.54,87.72,34.394,32.225 +2020-11-02 03:30:00,78.06,88.242,34.394,32.225 +2020-11-02 03:45:00,72.8,89.243,34.394,32.225 +2020-11-02 04:00:00,77.28,101.759,34.421,32.225 +2020-11-02 04:15:00,80.21,113.29299999999999,34.421,32.225 +2020-11-02 04:30:00,81.33,113.90799999999999,34.421,32.225 +2020-11-02 04:45:00,78.06,115.085,34.421,32.225 +2020-11-02 05:00:00,85.36,144.252,39.435,32.225 +2020-11-02 05:15:00,91.98,173.24200000000002,39.435,32.225 +2020-11-02 05:30:00,96.04,168.166,39.435,32.225 +2020-11-02 05:45:00,95.92,158.836,39.435,32.225 +2020-11-02 06:00:00,104.79,157.326,55.685,32.225 +2020-11-02 06:15:00,114.4,161.115,55.685,32.225 +2020-11-02 06:30:00,121.39,161.122,55.685,32.225 +2020-11-02 06:45:00,125.87,162.344,55.685,32.225 +2020-11-02 07:00:00,126.76,163.525,66.837,32.225 +2020-11-02 07:15:00,126.88,167.03400000000002,66.837,32.225 +2020-11-02 07:30:00,127.09,168.299,66.837,32.225 +2020-11-02 07:45:00,125.37,168.74099999999999,66.837,32.225 +2020-11-02 08:00:00,127.99,168.55700000000002,72.217,32.225 +2020-11-02 08:15:00,126.32,168.81599999999997,72.217,32.225 +2020-11-02 08:30:00,126.89,166.49400000000003,72.217,32.225 +2020-11-02 08:45:00,124.03,164.62900000000002,72.217,32.225 +2020-11-02 09:00:00,123.27,160.44799999999998,66.117,32.225 +2020-11-02 09:15:00,123.92,156.477,66.117,32.225 +2020-11-02 09:30:00,125.15,154.977,66.117,32.225 +2020-11-02 09:45:00,128.93,153.493,66.117,32.225 +2020-11-02 10:00:00,124.84,151.434,62.1,32.225 +2020-11-02 10:15:00,123.92,150.312,62.1,32.225 +2020-11-02 10:30:00,124.83,148.13299999999998,62.1,32.225 +2020-11-02 10:45:00,125.47,146.829,62.1,32.225 +2020-11-02 11:00:00,125.02,141.958,60.021,32.225 +2020-11-02 11:15:00,116.55,142.15,60.021,32.225 +2020-11-02 11:30:00,119.3,143.525,60.021,32.225 +2020-11-02 11:45:00,122.68,144.034,60.021,32.225 +2020-11-02 12:00:00,116.26,140.8,56.75899999999999,32.225 +2020-11-02 12:15:00,116.59,139.82399999999998,56.75899999999999,32.225 +2020-11-02 12:30:00,124.27,139.215,56.75899999999999,32.225 +2020-11-02 12:45:00,119.06,139.694,56.75899999999999,32.225 +2020-11-02 13:00:00,118.02,139.783,56.04600000000001,32.225 +2020-11-02 13:15:00,119.4,138.85399999999998,56.04600000000001,32.225 +2020-11-02 13:30:00,122.02,137.35399999999998,56.04600000000001,32.225 +2020-11-02 13:45:00,119.1,137.218,56.04600000000001,32.225 +2020-11-02 14:00:00,122.77,136.59,55.475,32.225 +2020-11-02 14:15:00,120.69,136.239,55.475,32.225 +2020-11-02 14:30:00,120.41,135.251,55.475,32.225 +2020-11-02 14:45:00,124.04,135.44,55.475,32.225 +2020-11-02 15:00:00,120.94,134.887,57.048,32.225 +2020-11-02 15:15:00,120.7,134.558,57.048,32.225 +2020-11-02 15:30:00,120.66,134.607,57.048,32.225 +2020-11-02 15:45:00,121.41,134.784,57.048,32.225 +2020-11-02 16:00:00,122.05,137.142,59.06,32.225 +2020-11-02 16:15:00,124.08,138.155,59.06,32.225 +2020-11-02 16:30:00,124.36,138.067,59.06,32.225 +2020-11-02 16:45:00,129.23,136.614,59.06,32.225 +2020-11-02 17:00:00,134.05,137.868,65.419,32.225 +2020-11-02 17:15:00,132.55,138.431,65.419,32.225 +2020-11-02 17:30:00,137.22,138.244,65.419,32.225 +2020-11-02 17:45:00,135.22,138.03,65.419,32.225 +2020-11-02 18:00:00,135.07,141.039,69.345,32.225 +2020-11-02 18:15:00,132.86,140.007,69.345,32.225 +2020-11-02 18:30:00,131.02,138.54399999999998,69.345,32.225 +2020-11-02 18:45:00,131.86,141.345,69.345,32.225 +2020-11-02 19:00:00,128.07,142.715,73.825,32.225 +2020-11-02 19:15:00,126.32,140.951,73.825,32.225 +2020-11-02 19:30:00,127.01,140.357,73.825,32.225 +2020-11-02 19:45:00,122.74,139.009,73.825,32.225 +2020-11-02 20:00:00,119.02,134.085,64.027,32.225 +2020-11-02 20:15:00,113.3,131.38299999999998,64.027,32.225 +2020-11-02 20:30:00,108.72,130.102,64.027,32.225 +2020-11-02 20:45:00,106.21,128.234,64.027,32.225 +2020-11-02 21:00:00,107.92,123.986,57.952,32.225 +2020-11-02 21:15:00,109.71,123.275,57.952,32.225 +2020-11-02 21:30:00,108.09,122.71700000000001,57.952,32.225 +2020-11-02 21:45:00,100.41,119.898,57.952,32.225 +2020-11-02 22:00:00,93.26,112.988,53.031000000000006,32.225 +2020-11-02 22:15:00,93.34,108.98299999999999,53.031000000000006,32.225 +2020-11-02 22:30:00,98.42,95.46700000000001,53.031000000000006,32.225 +2020-11-02 22:45:00,95.12,87.84,53.031000000000006,32.225 +2020-11-02 23:00:00,89.39,82.635,45.085,32.225 +2020-11-02 23:15:00,85.21,81.652,45.085,32.225 +2020-11-02 23:30:00,86.99,81.206,45.085,32.225 +2020-11-02 23:45:00,87.54,81.581,45.085,32.225 +2020-11-03 00:00:00,84.2,76.499,42.843,32.225 +2020-11-03 00:15:00,78.55,77.664,42.843,32.225 +2020-11-03 00:30:00,76.57,77.46300000000001,42.843,32.225 +2020-11-03 00:45:00,82.07,77.531,42.843,32.225 +2020-11-03 01:00:00,80.46,78.831,41.542,32.225 +2020-11-03 01:15:00,79.44,78.893,41.542,32.225 +2020-11-03 01:30:00,74.87,78.63,41.542,32.225 +2020-11-03 01:45:00,72.5,78.641,41.542,32.225 +2020-11-03 02:00:00,78.57,79.813,40.19,32.225 +2020-11-03 02:15:00,80.48,80.32300000000001,40.19,32.225 +2020-11-03 02:30:00,80.8,80.593,40.19,32.225 +2020-11-03 02:45:00,74.67,81.544,40.19,32.225 +2020-11-03 03:00:00,71.93,84.294,39.626,32.225 +2020-11-03 03:15:00,79.33,85.59299999999999,39.626,32.225 +2020-11-03 03:30:00,82.08,86.404,39.626,32.225 +2020-11-03 03:45:00,82.13,87.045,39.626,32.225 +2020-11-03 04:00:00,77.08,98.926,40.196999999999996,32.225 +2020-11-03 04:15:00,77.12,110.274,40.196999999999996,32.225 +2020-11-03 04:30:00,82.35,110.641,40.196999999999996,32.225 +2020-11-03 04:45:00,87.06,112.781,40.196999999999996,32.225 +2020-11-03 05:00:00,91.11,145.749,43.378,32.225 +2020-11-03 05:15:00,88.11,174.916,43.378,32.225 +2020-11-03 05:30:00,88.09,168.97,43.378,32.225 +2020-11-03 05:45:00,96.86,159.315,43.378,32.225 +2020-11-03 06:00:00,106.9,157.497,55.691,32.225 +2020-11-03 06:15:00,109.83,162.39700000000002,55.691,32.225 +2020-11-03 06:30:00,115.64,161.89700000000002,55.691,32.225 +2020-11-03 06:45:00,121.32,162.537,55.691,32.225 +2020-11-03 07:00:00,121.08,163.66,65.567,32.225 +2020-11-03 07:15:00,122.41,166.97799999999998,65.567,32.225 +2020-11-03 07:30:00,122.07,167.926,65.567,32.225 +2020-11-03 07:45:00,122.18,168.12400000000002,65.567,32.225 +2020-11-03 08:00:00,123.78,168.005,73.001,32.225 +2020-11-03 08:15:00,123.3,167.40099999999998,73.001,32.225 +2020-11-03 08:30:00,121.01,165.054,73.001,32.225 +2020-11-03 08:45:00,120.18,162.636,73.001,32.225 +2020-11-03 09:00:00,119.27,158.009,67.08800000000001,32.225 +2020-11-03 09:15:00,120.13,155.065,67.08800000000001,32.225 +2020-11-03 09:30:00,119.45,154.263,67.08800000000001,32.225 +2020-11-03 09:45:00,119.38,153.096,67.08800000000001,32.225 +2020-11-03 10:00:00,117.17,150.135,62.803000000000004,32.225 +2020-11-03 10:15:00,116.37,148.19,62.803000000000004,32.225 +2020-11-03 10:30:00,114.86,146.122,62.803000000000004,32.225 +2020-11-03 10:45:00,114.46,145.343,62.803000000000004,32.225 +2020-11-03 11:00:00,115.53,141.503,60.155,32.225 +2020-11-03 11:15:00,116.0,141.577,60.155,32.225 +2020-11-03 11:30:00,114.96,141.749,60.155,32.225 +2020-11-03 11:45:00,115.15,142.586,60.155,32.225 +2020-11-03 12:00:00,114.54,138.3,56.845,32.225 +2020-11-03 12:15:00,113.41,137.136,56.845,32.225 +2020-11-03 12:30:00,116.09,137.35399999999998,56.845,32.225 +2020-11-03 12:45:00,113.87,137.849,56.845,32.225 +2020-11-03 13:00:00,112.05,137.47799999999998,56.163000000000004,32.225 +2020-11-03 13:15:00,113.4,136.88,56.163000000000004,32.225 +2020-11-03 13:30:00,111.98,136.209,56.163000000000004,32.225 +2020-11-03 13:45:00,112.78,135.895,56.163000000000004,32.225 +2020-11-03 14:00:00,114.01,135.56,55.934,32.225 +2020-11-03 14:15:00,114.62,135.273,55.934,32.225 +2020-11-03 14:30:00,114.22,134.879,55.934,32.225 +2020-11-03 14:45:00,116.32,134.763,55.934,32.225 +2020-11-03 15:00:00,118.75,133.835,57.43899999999999,32.225 +2020-11-03 15:15:00,121.12,134.054,57.43899999999999,32.225 +2020-11-03 15:30:00,118.99,134.238,57.43899999999999,32.225 +2020-11-03 15:45:00,120.3,134.248,57.43899999999999,32.225 +2020-11-03 16:00:00,121.23,136.631,59.968999999999994,32.225 +2020-11-03 16:15:00,121.12,138.031,59.968999999999994,32.225 +2020-11-03 16:30:00,124.29,138.269,59.968999999999994,32.225 +2020-11-03 16:45:00,128.38,137.31,59.968999999999994,32.225 +2020-11-03 17:00:00,134.36,138.976,67.428,32.225 +2020-11-03 17:15:00,135.38,139.727,67.428,32.225 +2020-11-03 17:30:00,136.98,139.846,67.428,32.225 +2020-11-03 17:45:00,136.7,139.438,67.428,32.225 +2020-11-03 18:00:00,135.03,142.02700000000002,71.533,32.225 +2020-11-03 18:15:00,133.99,141.156,71.533,32.225 +2020-11-03 18:30:00,132.72,139.4,71.533,32.225 +2020-11-03 18:45:00,132.44,142.667,71.533,32.225 +2020-11-03 19:00:00,129.7,143.67,73.32300000000001,32.225 +2020-11-03 19:15:00,127.73,141.774,73.32300000000001,32.225 +2020-11-03 19:30:00,125.23,140.636,73.32300000000001,32.225 +2020-11-03 19:45:00,124.06,139.406,73.32300000000001,32.225 +2020-11-03 20:00:00,116.4,134.749,64.166,32.225 +2020-11-03 20:15:00,113.06,131.075,64.166,32.225 +2020-11-03 20:30:00,111.89,130.452,64.166,32.225 +2020-11-03 20:45:00,108.23,128.349,64.166,32.225 +2020-11-03 21:00:00,111.71,123.95299999999999,57.891999999999996,32.225 +2020-11-03 21:15:00,112.07,123.279,57.891999999999996,32.225 +2020-11-03 21:30:00,109.48,122.31299999999999,57.891999999999996,32.225 +2020-11-03 21:45:00,100.8,119.70200000000001,57.891999999999996,32.225 +2020-11-03 22:00:00,96.06,113.992,53.242,32.225 +2020-11-03 22:15:00,98.84,109.67299999999999,53.242,32.225 +2020-11-03 22:30:00,98.89,96.368,53.242,32.225 +2020-11-03 22:45:00,97.23,88.929,53.242,32.225 +2020-11-03 23:00:00,87.29,83.465,46.665,32.225 +2020-11-03 23:15:00,90.27,82.43700000000001,46.665,32.225 +2020-11-03 23:30:00,89.3,81.75399999999999,46.665,32.225 +2020-11-03 23:45:00,90.61,81.89,46.665,32.225 +2020-11-04 00:00:00,81.4,76.873,43.16,32.225 +2020-11-04 00:15:00,84.07,78.01899999999999,43.16,32.225 +2020-11-04 00:30:00,85.61,77.822,43.16,32.225 +2020-11-04 00:45:00,83.69,77.87899999999999,43.16,32.225 +2020-11-04 01:00:00,75.16,79.202,40.972,32.225 +2020-11-04 01:15:00,82.28,79.28,40.972,32.225 +2020-11-04 01:30:00,82.05,79.033,40.972,32.225 +2020-11-04 01:45:00,82.13,79.03699999999999,40.972,32.225 +2020-11-04 02:00:00,77.14,80.223,39.749,32.225 +2020-11-04 02:15:00,83.18,80.742,39.749,32.225 +2020-11-04 02:30:00,81.85,81.001,39.749,32.225 +2020-11-04 02:45:00,79.64,81.949,39.749,32.225 +2020-11-04 03:00:00,77.05,84.684,39.422,32.225 +2020-11-04 03:15:00,82.52,86.007,39.422,32.225 +2020-11-04 03:30:00,83.16,86.821,39.422,32.225 +2020-11-04 03:45:00,81.55,87.447,39.422,32.225 +2020-11-04 04:00:00,77.98,99.337,40.505,32.225 +2020-11-04 04:15:00,84.43,110.712,40.505,32.225 +2020-11-04 04:30:00,87.82,111.073,40.505,32.225 +2020-11-04 04:45:00,88.51,113.22,40.505,32.225 +2020-11-04 05:00:00,88.07,146.239,43.397,32.225 +2020-11-04 05:15:00,92.6,175.447,43.397,32.225 +2020-11-04 05:30:00,99.67,169.497,43.397,32.225 +2020-11-04 05:45:00,101.58,159.82,43.397,32.225 +2020-11-04 06:00:00,103.46,157.997,55.218,32.225 +2020-11-04 06:15:00,112.23,162.908,55.218,32.225 +2020-11-04 06:30:00,114.12,162.44,55.218,32.225 +2020-11-04 06:45:00,117.78,163.09799999999998,55.218,32.225 +2020-11-04 07:00:00,121.0,164.22,67.39,32.225 +2020-11-04 07:15:00,123.07,167.551,67.39,32.225 +2020-11-04 07:30:00,121.54,168.52599999999998,67.39,32.225 +2020-11-04 07:45:00,123.46,168.72799999999998,67.39,32.225 +2020-11-04 08:00:00,126.53,168.62400000000002,74.345,32.225 +2020-11-04 08:15:00,124.13,168.00099999999998,74.345,32.225 +2020-11-04 08:30:00,124.39,165.68,74.345,32.225 +2020-11-04 08:45:00,125.06,163.233,74.345,32.225 +2020-11-04 09:00:00,120.85,158.594,69.336,32.225 +2020-11-04 09:15:00,122.69,155.651,69.336,32.225 +2020-11-04 09:30:00,127.17,154.84,69.336,32.225 +2020-11-04 09:45:00,119.89,153.649,69.336,32.225 +2020-11-04 10:00:00,117.85,150.679,64.291,32.225 +2020-11-04 10:15:00,119.47,148.695,64.291,32.225 +2020-11-04 10:30:00,118.23,146.60299999999998,64.291,32.225 +2020-11-04 10:45:00,117.75,145.809,64.291,32.225 +2020-11-04 11:00:00,117.87,141.967,62.20399999999999,32.225 +2020-11-04 11:15:00,117.63,142.02100000000002,62.20399999999999,32.225 +2020-11-04 11:30:00,117.7,142.192,62.20399999999999,32.225 +2020-11-04 11:45:00,119.73,143.015,62.20399999999999,32.225 +2020-11-04 12:00:00,117.89,138.71,59.042,32.225 +2020-11-04 12:15:00,116.62,137.545,59.042,32.225 +2020-11-04 12:30:00,119.41,137.797,59.042,32.225 +2020-11-04 12:45:00,114.95,138.29399999999998,59.042,32.225 +2020-11-04 13:00:00,118.41,137.885,57.907,32.225 +2020-11-04 13:15:00,117.16,137.295,57.907,32.225 +2020-11-04 13:30:00,115.27,136.624,57.907,32.225 +2020-11-04 13:45:00,116.77,136.303,57.907,32.225 +2020-11-04 14:00:00,114.41,135.916,58.358000000000004,32.225 +2020-11-04 14:15:00,113.25,135.64700000000002,58.358000000000004,32.225 +2020-11-04 14:30:00,113.2,135.291,58.358000000000004,32.225 +2020-11-04 14:45:00,116.71,135.17600000000002,58.358000000000004,32.225 +2020-11-04 15:00:00,118.53,134.237,59.348,32.225 +2020-11-04 15:15:00,117.22,134.471,59.348,32.225 +2020-11-04 15:30:00,115.9,134.694,59.348,32.225 +2020-11-04 15:45:00,117.62,134.718,59.348,32.225 +2020-11-04 16:00:00,119.43,137.075,61.413999999999994,32.225 +2020-11-04 16:15:00,119.61,138.499,61.413999999999994,32.225 +2020-11-04 16:30:00,124.61,138.737,61.413999999999994,32.225 +2020-11-04 16:45:00,127.54,137.829,61.413999999999994,32.225 +2020-11-04 17:00:00,135.41,139.454,67.107,32.225 +2020-11-04 17:15:00,134.88,140.226,67.107,32.225 +2020-11-04 17:30:00,134.97,140.349,67.107,32.225 +2020-11-04 17:45:00,134.76,139.954,67.107,32.225 +2020-11-04 18:00:00,134.73,142.542,71.92,32.225 +2020-11-04 18:15:00,133.15,141.637,71.92,32.225 +2020-11-04 18:30:00,132.19,139.892,71.92,32.225 +2020-11-04 18:45:00,131.93,143.155,71.92,32.225 +2020-11-04 19:00:00,128.13,144.165,75.09,32.225 +2020-11-04 19:15:00,126.83,142.259,75.09,32.225 +2020-11-04 19:30:00,125.21,141.106,75.09,32.225 +2020-11-04 19:45:00,126.42,139.845,75.09,32.225 +2020-11-04 20:00:00,120.08,135.208,65.977,32.225 +2020-11-04 20:15:00,113.28,131.525,65.977,32.225 +2020-11-04 20:30:00,111.02,130.87,65.977,32.225 +2020-11-04 20:45:00,112.13,128.757,65.977,32.225 +2020-11-04 21:00:00,108.11,124.35700000000001,58.798,32.225 +2020-11-04 21:15:00,107.66,123.671,58.798,32.225 +2020-11-04 21:30:00,109.28,122.713,58.798,32.225 +2020-11-04 21:45:00,103.93,120.087,58.798,32.225 +2020-11-04 22:00:00,97.92,114.374,54.486000000000004,32.225 +2020-11-04 22:15:00,93.55,110.039,54.486000000000004,32.225 +2020-11-04 22:30:00,95.7,96.777,54.486000000000004,32.225 +2020-11-04 22:45:00,98.22,89.345,54.486000000000004,32.225 +2020-11-04 23:00:00,93.29,83.88600000000001,47.783,32.225 +2020-11-04 23:15:00,92.59,82.83200000000001,47.783,32.225 +2020-11-04 23:30:00,89.1,82.152,47.783,32.225 +2020-11-04 23:45:00,89.25,82.274,47.783,32.225 +2020-11-05 00:00:00,85.13,77.245,43.88,32.225 +2020-11-05 00:15:00,81.55,78.372,43.88,32.225 +2020-11-05 00:30:00,84.52,78.18,43.88,32.225 +2020-11-05 00:45:00,83.95,78.225,43.88,32.225 +2020-11-05 01:00:00,78.92,79.571,42.242,32.225 +2020-11-05 01:15:00,78.1,79.663,42.242,32.225 +2020-11-05 01:30:00,81.46,79.433,42.242,32.225 +2020-11-05 01:45:00,82.78,79.43,42.242,32.225 +2020-11-05 02:00:00,79.05,80.63,40.918,32.225 +2020-11-05 02:15:00,78.82,81.15899999999999,40.918,32.225 +2020-11-05 02:30:00,81.15,81.407,40.918,32.225 +2020-11-05 02:45:00,85.55,82.351,40.918,32.225 +2020-11-05 03:00:00,79.23,85.072,40.411,32.225 +2020-11-05 03:15:00,74.81,86.42,40.411,32.225 +2020-11-05 03:30:00,82.28,87.23700000000001,40.411,32.225 +2020-11-05 03:45:00,82.7,87.84700000000001,40.411,32.225 +2020-11-05 04:00:00,84.49,99.74700000000001,41.246,32.225 +2020-11-05 04:15:00,80.91,111.145,41.246,32.225 +2020-11-05 04:30:00,84.02,111.501,41.246,32.225 +2020-11-05 04:45:00,88.63,113.65700000000001,41.246,32.225 +2020-11-05 05:00:00,89.89,146.725,44.533,32.225 +2020-11-05 05:15:00,85.19,175.975,44.533,32.225 +2020-11-05 05:30:00,88.1,170.021,44.533,32.225 +2020-11-05 05:45:00,93.43,160.321,44.533,32.225 +2020-11-05 06:00:00,102.82,158.494,55.005,32.225 +2020-11-05 06:15:00,110.75,163.417,55.005,32.225 +2020-11-05 06:30:00,115.53,162.97799999999998,55.005,32.225 +2020-11-05 06:45:00,126.41,163.656,55.005,32.225 +2020-11-05 07:00:00,128.69,164.77700000000002,64.597,32.225 +2020-11-05 07:15:00,123.86,168.12,64.597,32.225 +2020-11-05 07:30:00,127.33,169.122,64.597,32.225 +2020-11-05 07:45:00,126.84,169.326,64.597,32.225 +2020-11-05 08:00:00,128.0,169.237,71.71600000000001,32.225 +2020-11-05 08:15:00,129.17,168.595,71.71600000000001,32.225 +2020-11-05 08:30:00,128.65,166.301,71.71600000000001,32.225 +2020-11-05 08:45:00,128.37,163.826,71.71600000000001,32.225 +2020-11-05 09:00:00,127.46,159.174,66.51899999999999,32.225 +2020-11-05 09:15:00,128.89,156.231,66.51899999999999,32.225 +2020-11-05 09:30:00,127.36,155.411,66.51899999999999,32.225 +2020-11-05 09:45:00,127.69,154.197,66.51899999999999,32.225 +2020-11-05 10:00:00,124.33,151.218,63.04,32.225 +2020-11-05 10:15:00,121.71,149.195,63.04,32.225 +2020-11-05 10:30:00,118.24,147.08,63.04,32.225 +2020-11-05 10:45:00,119.63,146.269,63.04,32.225 +2020-11-05 11:00:00,119.67,142.42700000000002,60.998000000000005,32.225 +2020-11-05 11:15:00,125.95,142.46,60.998000000000005,32.225 +2020-11-05 11:30:00,128.57,142.63,60.998000000000005,32.225 +2020-11-05 11:45:00,128.14,143.44,60.998000000000005,32.225 +2020-11-05 12:00:00,125.75,139.116,58.27,32.225 +2020-11-05 12:15:00,124.99,137.951,58.27,32.225 +2020-11-05 12:30:00,126.45,138.237,58.27,32.225 +2020-11-05 12:45:00,127.2,138.736,58.27,32.225 +2020-11-05 13:00:00,125.78,138.29,57.196000000000005,32.225 +2020-11-05 13:15:00,126.04,137.708,57.196000000000005,32.225 +2020-11-05 13:30:00,123.81,137.035,57.196000000000005,32.225 +2020-11-05 13:45:00,124.92,136.709,57.196000000000005,32.225 +2020-11-05 14:00:00,127.64,136.27,57.38399999999999,32.225 +2020-11-05 14:15:00,128.62,136.016,57.38399999999999,32.225 +2020-11-05 14:30:00,130.34,135.69899999999998,57.38399999999999,32.225 +2020-11-05 14:45:00,127.29,135.585,57.38399999999999,32.225 +2020-11-05 15:00:00,129.48,134.634,58.647,32.225 +2020-11-05 15:15:00,129.34,134.884,58.647,32.225 +2020-11-05 15:30:00,127.64,135.14600000000002,58.647,32.225 +2020-11-05 15:45:00,127.96,135.18200000000002,58.647,32.225 +2020-11-05 16:00:00,131.92,137.515,60.083999999999996,32.225 +2020-11-05 16:15:00,131.35,138.964,60.083999999999996,32.225 +2020-11-05 16:30:00,131.61,139.2,60.083999999999996,32.225 +2020-11-05 16:45:00,134.19,138.344,60.083999999999996,32.225 +2020-11-05 17:00:00,137.92,139.92700000000002,65.85600000000001,32.225 +2020-11-05 17:15:00,136.96,140.72,65.85600000000001,32.225 +2020-11-05 17:30:00,137.47,140.84799999999998,65.85600000000001,32.225 +2020-11-05 17:45:00,138.29,140.466,65.85600000000001,32.225 +2020-11-05 18:00:00,139.48,143.053,69.855,32.225 +2020-11-05 18:15:00,134.15,142.115,69.855,32.225 +2020-11-05 18:30:00,133.51,140.38,69.855,32.225 +2020-11-05 18:45:00,132.8,143.64,69.855,32.225 +2020-11-05 19:00:00,131.09,144.657,74.015,32.225 +2020-11-05 19:15:00,127.89,142.741,74.015,32.225 +2020-11-05 19:30:00,128.05,141.57299999999998,74.015,32.225 +2020-11-05 19:45:00,123.88,140.283,74.015,32.225 +2020-11-05 20:00:00,116.5,135.664,65.316,32.225 +2020-11-05 20:15:00,113.48,131.972,65.316,32.225 +2020-11-05 20:30:00,112.84,131.285,65.316,32.225 +2020-11-05 20:45:00,114.31,129.164,65.316,32.225 +2020-11-05 21:00:00,103.83,124.757,58.403999999999996,32.225 +2020-11-05 21:15:00,105.69,124.059,58.403999999999996,32.225 +2020-11-05 21:30:00,103.3,123.11,58.403999999999996,32.225 +2020-11-05 21:45:00,102.93,120.469,58.403999999999996,32.225 +2020-11-05 22:00:00,97.89,114.755,54.092,32.225 +2020-11-05 22:15:00,97.8,110.404,54.092,32.225 +2020-11-05 22:30:00,92.88,97.184,54.092,32.225 +2020-11-05 22:45:00,97.46,89.76,54.092,32.225 +2020-11-05 23:00:00,92.45,84.305,48.18600000000001,32.225 +2020-11-05 23:15:00,91.31,83.226,48.18600000000001,32.225 +2020-11-05 23:30:00,83.3,82.54899999999999,48.18600000000001,32.225 +2020-11-05 23:45:00,84.68,82.65700000000001,48.18600000000001,32.225 +2020-11-06 00:00:00,84.7,76.2,45.18899999999999,32.225 +2020-11-06 00:15:00,85.23,77.523,45.18899999999999,32.225 +2020-11-06 00:30:00,83.88,77.358,45.18899999999999,32.225 +2020-11-06 00:45:00,80.5,77.637,45.18899999999999,32.225 +2020-11-06 01:00:00,80.34,78.648,43.256,32.225 +2020-11-06 01:15:00,81.37,79.095,43.256,32.225 +2020-11-06 01:30:00,77.99,79.031,43.256,32.225 +2020-11-06 01:45:00,77.75,78.985,43.256,32.225 +2020-11-06 02:00:00,79.46,80.635,42.312,32.225 +2020-11-06 02:15:00,81.87,81.078,42.312,32.225 +2020-11-06 02:30:00,75.94,81.99,42.312,32.225 +2020-11-06 02:45:00,73.82,82.675,42.312,32.225 +2020-11-06 03:00:00,72.73,85.07600000000001,41.833,32.225 +2020-11-06 03:15:00,81.36,86.537,41.833,32.225 +2020-11-06 03:30:00,82.16,87.24600000000001,41.833,32.225 +2020-11-06 03:45:00,82.46,88.412,41.833,32.225 +2020-11-06 04:00:00,78.14,100.52,42.732,32.225 +2020-11-06 04:15:00,80.31,111.135,42.732,32.225 +2020-11-06 04:30:00,83.97,112.02799999999999,42.732,32.225 +2020-11-06 04:45:00,89.09,113.204,42.732,32.225 +2020-11-06 05:00:00,92.15,145.32399999999998,46.254,32.225 +2020-11-06 05:15:00,86.95,175.96,46.254,32.225 +2020-11-06 05:30:00,90.34,170.75799999999998,46.254,32.225 +2020-11-06 05:45:00,99.94,160.809,46.254,32.225 +2020-11-06 06:00:00,111.88,159.36700000000002,56.76,32.225 +2020-11-06 06:15:00,114.2,163.43200000000002,56.76,32.225 +2020-11-06 06:30:00,122.58,162.452,56.76,32.225 +2020-11-06 06:45:00,120.37,164.13,56.76,32.225 +2020-11-06 07:00:00,125.85,164.983,66.029,32.225 +2020-11-06 07:15:00,126.83,169.363,66.029,32.225 +2020-11-06 07:30:00,126.69,169.503,66.029,32.225 +2020-11-06 07:45:00,128.85,169.049,66.029,32.225 +2020-11-06 08:00:00,132.52,168.55,73.128,32.225 +2020-11-06 08:15:00,131.81,167.88299999999998,73.128,32.225 +2020-11-06 08:30:00,131.12,166.232,73.128,32.225 +2020-11-06 08:45:00,131.33,162.619,73.128,32.225 +2020-11-06 09:00:00,132.45,157.394,68.23100000000001,32.225 +2020-11-06 09:15:00,134.02,155.541,68.23100000000001,32.225 +2020-11-06 09:30:00,135.36,154.16899999999998,68.23100000000001,32.225 +2020-11-06 09:45:00,135.72,152.999,68.23100000000001,32.225 +2020-11-06 10:00:00,135.71,149.14,64.733,32.225 +2020-11-06 10:15:00,136.19,147.489,64.733,32.225 +2020-11-06 10:30:00,134.9,145.493,64.733,32.225 +2020-11-06 10:45:00,134.66,144.32399999999998,64.733,32.225 +2020-11-06 11:00:00,134.81,140.532,62.0,32.225 +2020-11-06 11:15:00,135.06,139.54399999999998,62.0,32.225 +2020-11-06 11:30:00,134.53,140.829,62.0,32.225 +2020-11-06 11:45:00,137.14,141.335,62.0,32.225 +2020-11-06 12:00:00,134.12,137.924,57.876999999999995,32.225 +2020-11-06 12:15:00,133.37,135.04399999999998,57.876999999999995,32.225 +2020-11-06 12:30:00,132.61,135.516,57.876999999999995,32.225 +2020-11-06 12:45:00,131.3,136.129,57.876999999999995,32.225 +2020-11-06 13:00:00,128.69,136.524,55.585,32.225 +2020-11-06 13:15:00,127.97,136.601,55.585,32.225 +2020-11-06 13:30:00,124.2,136.194,55.585,32.225 +2020-11-06 13:45:00,123.92,135.91299999999998,55.585,32.225 +2020-11-06 14:00:00,118.95,134.342,54.5,32.225 +2020-11-06 14:15:00,120.9,134.113,54.5,32.225 +2020-11-06 14:30:00,121.51,134.691,54.5,32.225 +2020-11-06 14:45:00,120.58,134.575,54.5,32.225 +2020-11-06 15:00:00,122.67,133.238,55.131,32.225 +2020-11-06 15:15:00,118.7,133.108,55.131,32.225 +2020-11-06 15:30:00,121.37,132.126,55.131,32.225 +2020-11-06 15:45:00,120.79,132.526,55.131,32.225 +2020-11-06 16:00:00,122.13,133.756,56.8,32.225 +2020-11-06 16:15:00,122.5,135.602,56.8,32.225 +2020-11-06 16:30:00,127.43,135.842,56.8,32.225 +2020-11-06 16:45:00,129.4,134.688,56.8,32.225 +2020-11-06 17:00:00,133.06,137.007,63.428999999999995,32.225 +2020-11-06 17:15:00,134.26,137.476,63.428999999999995,32.225 +2020-11-06 17:30:00,132.78,137.447,63.428999999999995,32.225 +2020-11-06 17:45:00,131.33,136.857,63.428999999999995,32.225 +2020-11-06 18:00:00,134.08,139.959,67.915,32.225 +2020-11-06 18:15:00,130.5,138.378,67.915,32.225 +2020-11-06 18:30:00,129.8,136.88299999999998,67.915,32.225 +2020-11-06 18:45:00,133.03,140.289,67.915,32.225 +2020-11-06 19:00:00,126.8,142.261,69.428,32.225 +2020-11-06 19:15:00,125.27,141.47899999999998,69.428,32.225 +2020-11-06 19:30:00,122.86,140.039,69.428,32.225 +2020-11-06 19:45:00,124.09,138.034,69.428,32.225 +2020-11-06 20:00:00,114.98,133.422,60.56100000000001,32.225 +2020-11-06 20:15:00,113.0,130.02100000000002,60.56100000000001,32.225 +2020-11-06 20:30:00,109.07,129.093,60.56100000000001,32.225 +2020-11-06 20:45:00,109.58,127.03299999999999,60.56100000000001,32.225 +2020-11-06 21:00:00,102.28,123.459,55.18600000000001,32.225 +2020-11-06 21:15:00,106.27,123.67200000000001,55.18600000000001,32.225 +2020-11-06 21:30:00,104.99,122.706,55.18600000000001,32.225 +2020-11-06 21:45:00,96.99,120.491,55.18600000000001,32.225 +2020-11-06 22:00:00,88.19,115.375,51.433,32.225 +2020-11-06 22:15:00,86.68,110.825,51.433,32.225 +2020-11-06 22:30:00,82.9,103.704,51.433,32.225 +2020-11-06 22:45:00,82.11,98.971,51.433,32.225 +2020-11-06 23:00:00,77.89,93.86200000000001,46.201,32.225 +2020-11-06 23:15:00,81.36,90.863,46.201,32.225 +2020-11-06 23:30:00,75.34,88.525,46.201,32.225 +2020-11-06 23:45:00,74.67,88.12899999999999,46.201,32.225 +2020-11-07 00:00:00,72.4,75.25399999999999,42.576,32.047 +2020-11-07 00:15:00,76.3,73.567,42.576,32.047 +2020-11-07 00:30:00,76.56,74.063,42.576,32.047 +2020-11-07 00:45:00,74.82,74.486,42.576,32.047 +2020-11-07 01:00:00,70.47,76.05,39.34,32.047 +2020-11-07 01:15:00,73.1,76.13,39.34,32.047 +2020-11-07 01:30:00,72.65,75.42,39.34,32.047 +2020-11-07 01:45:00,66.71,75.745,39.34,32.047 +2020-11-07 02:00:00,66.31,77.455,37.582,32.047 +2020-11-07 02:15:00,71.52,77.346,37.582,32.047 +2020-11-07 02:30:00,68.13,77.218,37.582,32.047 +2020-11-07 02:45:00,68.12,78.291,37.582,32.047 +2020-11-07 03:00:00,64.49,80.518,36.523,32.047 +2020-11-07 03:15:00,70.44,80.949,36.523,32.047 +2020-11-07 03:30:00,71.27,80.803,36.523,32.047 +2020-11-07 03:45:00,70.23,82.641,36.523,32.047 +2020-11-07 04:00:00,62.21,91.421,36.347,32.047 +2020-11-07 04:15:00,62.81,100.102,36.347,32.047 +2020-11-07 04:30:00,63.23,98.935,36.347,32.047 +2020-11-07 04:45:00,63.56,99.926,36.347,32.047 +2020-11-07 05:00:00,64.36,118.78299999999999,36.407,32.047 +2020-11-07 05:15:00,65.17,133.07,36.407,32.047 +2020-11-07 05:30:00,64.94,128.732,36.407,32.047 +2020-11-07 05:45:00,64.25,124.027,36.407,32.047 +2020-11-07 06:00:00,70.3,138.67,38.228,32.047 +2020-11-07 06:15:00,70.95,156.255,38.228,32.047 +2020-11-07 06:30:00,68.59,150.749,38.228,32.047 +2020-11-07 06:45:00,70.43,145.471,38.228,32.047 +2020-11-07 07:00:00,75.46,143.015,41.905,32.047 +2020-11-07 07:15:00,76.33,146.082,41.905,32.047 +2020-11-07 07:30:00,78.63,148.593,41.905,32.047 +2020-11-07 07:45:00,81.45,151.06799999999998,41.905,32.047 +2020-11-07 08:00:00,83.9,153.178,46.051,32.047 +2020-11-07 08:15:00,83.65,154.811,46.051,32.047 +2020-11-07 08:30:00,84.05,154.248,46.051,32.047 +2020-11-07 08:45:00,85.96,153.055,46.051,32.047 +2020-11-07 09:00:00,87.34,150.039,46.683,32.047 +2020-11-07 09:15:00,87.81,148.872,46.683,32.047 +2020-11-07 09:30:00,86.74,148.283,46.683,32.047 +2020-11-07 09:45:00,86.17,147.033,46.683,32.047 +2020-11-07 10:00:00,86.08,143.433,44.425,32.047 +2020-11-07 10:15:00,85.25,141.98,44.425,32.047 +2020-11-07 10:30:00,85.52,139.95600000000002,44.425,32.047 +2020-11-07 10:45:00,85.84,139.55,44.425,32.047 +2020-11-07 11:00:00,86.36,135.795,42.148999999999994,32.047 +2020-11-07 11:15:00,87.42,134.597,42.148999999999994,32.047 +2020-11-07 11:30:00,87.9,135.195,42.148999999999994,32.047 +2020-11-07 11:45:00,89.42,135.241,42.148999999999994,32.047 +2020-11-07 12:00:00,86.24,131.194,39.683,32.047 +2020-11-07 12:15:00,84.54,129.049,39.683,32.047 +2020-11-07 12:30:00,83.03,129.739,39.683,32.047 +2020-11-07 12:45:00,82.28,130.041,39.683,32.047 +2020-11-07 13:00:00,80.02,129.766,37.154,32.047 +2020-11-07 13:15:00,79.68,128.201,37.154,32.047 +2020-11-07 13:30:00,78.82,127.529,37.154,32.047 +2020-11-07 13:45:00,78.57,127.15,37.154,32.047 +2020-11-07 14:00:00,77.79,126.416,36.457,32.047 +2020-11-07 14:15:00,78.58,125.419,36.457,32.047 +2020-11-07 14:30:00,78.89,124.588,36.457,32.047 +2020-11-07 14:45:00,80.87,124.78399999999999,36.457,32.047 +2020-11-07 15:00:00,80.77,124.04299999999999,38.257,32.047 +2020-11-07 15:15:00,82.29,124.734,38.257,32.047 +2020-11-07 15:30:00,83.92,124.965,38.257,32.047 +2020-11-07 15:45:00,86.04,125.09299999999999,38.257,32.047 +2020-11-07 16:00:00,87.33,126.383,41.181000000000004,32.047 +2020-11-07 16:15:00,88.36,128.577,41.181000000000004,32.047 +2020-11-07 16:30:00,90.62,128.856,41.181000000000004,32.047 +2020-11-07 16:45:00,95.89,128.364,41.181000000000004,32.047 +2020-11-07 17:00:00,101.97,130.005,46.806000000000004,32.047 +2020-11-07 17:15:00,101.76,130.86700000000002,46.806000000000004,32.047 +2020-11-07 17:30:00,104.34,130.73,46.806000000000004,32.047 +2020-11-07 17:45:00,104.83,130.05,46.806000000000004,32.047 +2020-11-07 18:00:00,105.8,133.376,52.073,32.047 +2020-11-07 18:15:00,105.2,133.58700000000002,52.073,32.047 +2020-11-07 18:30:00,103.89,133.498,52.073,32.047 +2020-11-07 18:45:00,101.96,133.372,52.073,32.047 +2020-11-07 19:00:00,100.94,135.424,53.608000000000004,32.047 +2020-11-07 19:15:00,98.9,133.929,53.608000000000004,32.047 +2020-11-07 19:30:00,97.79,133.262,53.608000000000004,32.047 +2020-11-07 19:45:00,96.56,131.755,53.608000000000004,32.047 +2020-11-07 20:00:00,91.1,128.975,50.265,32.047 +2020-11-07 20:15:00,85.93,126.74799999999999,50.265,32.047 +2020-11-07 20:30:00,85.7,125.228,50.265,32.047 +2020-11-07 20:45:00,83.77,123.581,50.265,32.047 +2020-11-07 21:00:00,79.67,120.964,45.766000000000005,32.047 +2020-11-07 21:15:00,79.69,121.325,45.766000000000005,32.047 +2020-11-07 21:30:00,78.88,121.25399999999999,45.766000000000005,32.047 +2020-11-07 21:45:00,77.61,118.55,45.766000000000005,32.047 +2020-11-07 22:00:00,75.18,114.319,45.97,32.047 +2020-11-07 22:15:00,76.21,111.5,45.97,32.047 +2020-11-07 22:30:00,73.1,108.414,45.97,32.047 +2020-11-07 22:45:00,72.11,105.071,45.97,32.047 +2020-11-07 23:00:00,68.65,101.348,40.415,32.047 +2020-11-07 23:15:00,68.16,97.39200000000001,40.415,32.047 +2020-11-07 23:30:00,64.73,94.814,40.415,32.047 +2020-11-07 23:45:00,63.74,92.92399999999999,40.415,32.047 +2020-11-08 00:00:00,56.78,76.44800000000001,36.376,32.047 +2020-11-08 00:15:00,59.59,74.069,36.376,32.047 +2020-11-08 00:30:00,59.04,74.27,36.376,32.047 +2020-11-08 00:45:00,58.74,75.078,36.376,32.047 +2020-11-08 01:00:00,56.0,76.681,32.992,32.047 +2020-11-08 01:15:00,56.15,77.38,32.992,32.047 +2020-11-08 01:30:00,53.94,76.958,32.992,32.047 +2020-11-08 01:45:00,55.96,76.921,32.992,32.047 +2020-11-08 02:00:00,53.18,78.202,32.327,32.047 +2020-11-08 02:15:00,54.85,77.861,32.327,32.047 +2020-11-08 02:30:00,53.77,78.33800000000001,32.327,32.047 +2020-11-08 02:45:00,54.28,79.574,32.327,32.047 +2020-11-08 03:00:00,54.05,82.25399999999999,31.169,32.047 +2020-11-08 03:15:00,54.15,82.494,31.169,32.047 +2020-11-08 03:30:00,54.49,82.912,31.169,32.047 +2020-11-08 03:45:00,52.1,84.354,31.169,32.047 +2020-11-08 04:00:00,54.3,92.965,30.796,32.047 +2020-11-08 04:15:00,54.34,100.863,30.796,32.047 +2020-11-08 04:30:00,55.34,100.29799999999999,30.796,32.047 +2020-11-08 04:45:00,52.7,101.26799999999999,30.796,32.047 +2020-11-08 05:00:00,55.15,118.09200000000001,30.848000000000003,32.047 +2020-11-08 05:15:00,56.75,130.61,30.848000000000003,32.047 +2020-11-08 05:30:00,56.68,125.994,30.848000000000003,32.047 +2020-11-08 05:45:00,57.37,121.301,30.848000000000003,32.047 +2020-11-08 06:00:00,58.77,134.852,31.166,32.047 +2020-11-08 06:15:00,56.79,151.719,31.166,32.047 +2020-11-08 06:30:00,59.58,145.264,31.166,32.047 +2020-11-08 06:45:00,61.46,138.931,31.166,32.047 +2020-11-08 07:00:00,63.74,138.10299999999998,33.527,32.047 +2020-11-08 07:15:00,64.26,139.97799999999998,33.527,32.047 +2020-11-08 07:30:00,66.51,142.24200000000002,33.527,32.047 +2020-11-08 07:45:00,65.6,144.219,33.527,32.047 +2020-11-08 08:00:00,71.16,147.767,36.616,32.047 +2020-11-08 08:15:00,72.66,149.776,36.616,32.047 +2020-11-08 08:30:00,73.14,150.658,36.616,32.047 +2020-11-08 08:45:00,75.05,150.73,36.616,32.047 +2020-11-08 09:00:00,75.61,147.37,37.857,32.047 +2020-11-08 09:15:00,73.91,146.389,37.857,32.047 +2020-11-08 09:30:00,74.91,145.84799999999998,37.857,32.047 +2020-11-08 09:45:00,74.9,144.869,37.857,32.047 +2020-11-08 10:00:00,75.28,143.132,36.319,32.047 +2020-11-08 10:15:00,81.36,142.055,36.319,32.047 +2020-11-08 10:30:00,83.52,140.497,36.319,32.047 +2020-11-08 10:45:00,84.81,139.138,36.319,32.047 +2020-11-08 11:00:00,89.21,135.871,37.236999999999995,32.047 +2020-11-08 11:15:00,90.66,134.592,37.236999999999995,32.047 +2020-11-08 11:30:00,90.88,134.764,37.236999999999995,32.047 +2020-11-08 11:45:00,89.95,135.306,37.236999999999995,32.047 +2020-11-08 12:00:00,87.53,131.175,34.871,32.047 +2020-11-08 12:15:00,85.32,130.161,34.871,32.047 +2020-11-08 12:30:00,83.34,129.931,34.871,32.047 +2020-11-08 12:45:00,78.63,129.341,34.871,32.047 +2020-11-08 13:00:00,69.57,128.405,29.738000000000003,32.047 +2020-11-08 13:15:00,69.16,128.797,29.738000000000003,32.047 +2020-11-08 13:30:00,66.72,127.609,29.738000000000003,32.047 +2020-11-08 13:45:00,67.62,127.12299999999999,29.738000000000003,32.047 +2020-11-08 14:00:00,66.83,126.906,27.333000000000002,32.047 +2020-11-08 14:15:00,66.82,126.916,27.333000000000002,32.047 +2020-11-08 14:30:00,67.73,126.59700000000001,27.333000000000002,32.047 +2020-11-08 14:45:00,69.57,126.15100000000001,27.333000000000002,32.047 +2020-11-08 15:00:00,71.36,124.36,28.232,32.047 +2020-11-08 15:15:00,70.85,125.339,28.232,32.047 +2020-11-08 15:30:00,72.33,125.96799999999999,28.232,32.047 +2020-11-08 15:45:00,74.18,126.693,28.232,32.047 +2020-11-08 16:00:00,77.95,128.625,32.815,32.047 +2020-11-08 16:15:00,78.12,130.292,32.815,32.047 +2020-11-08 16:30:00,81.11,131.095,32.815,32.047 +2020-11-08 16:45:00,86.31,130.768,32.815,32.047 +2020-11-08 17:00:00,91.61,132.41899999999998,43.068999999999996,32.047 +2020-11-08 17:15:00,92.71,133.623,43.068999999999996,32.047 +2020-11-08 17:30:00,94.77,133.976,43.068999999999996,32.047 +2020-11-08 17:45:00,96.36,135.0,43.068999999999996,32.047 +2020-11-08 18:00:00,97.41,138.14700000000002,50.498999999999995,32.047 +2020-11-08 18:15:00,96.11,139.069,50.498999999999995,32.047 +2020-11-08 18:30:00,96.01,137.498,50.498999999999995,32.047 +2020-11-08 18:45:00,94.28,138.656,50.498999999999995,32.047 +2020-11-08 19:00:00,92.22,141.273,53.481,32.047 +2020-11-08 19:15:00,90.52,139.755,53.481,32.047 +2020-11-08 19:30:00,89.88,138.872,53.481,32.047 +2020-11-08 19:45:00,88.47,138.131,53.481,32.047 +2020-11-08 20:00:00,86.19,135.379,51.687,32.047 +2020-11-08 20:15:00,84.49,133.74200000000002,51.687,32.047 +2020-11-08 20:30:00,83.73,133.315,51.687,32.047 +2020-11-08 20:45:00,83.27,130.233,51.687,32.047 +2020-11-08 21:00:00,79.79,125.823,47.674,32.047 +2020-11-08 21:15:00,80.23,125.645,47.674,32.047 +2020-11-08 21:30:00,79.81,125.53,47.674,32.047 +2020-11-08 21:45:00,80.44,123.037,47.674,32.047 +2020-11-08 22:00:00,78.11,118.802,48.178000000000004,32.047 +2020-11-08 22:15:00,78.45,114.829,48.178000000000004,32.047 +2020-11-08 22:30:00,76.78,109.508,48.178000000000004,32.047 +2020-11-08 22:45:00,77.39,105.12899999999999,48.178000000000004,32.047 +2020-11-08 23:00:00,74.59,99.45,42.553999999999995,32.047 +2020-11-08 23:15:00,73.28,97.175,42.553999999999995,32.047 +2020-11-08 23:30:00,72.41,94.932,42.553999999999995,32.047 +2020-11-08 23:45:00,72.06,93.675,42.553999999999995,32.047 +2020-11-09 00:00:00,68.05,80.07300000000001,37.177,32.225 +2020-11-09 00:15:00,68.1,79.87,37.177,32.225 +2020-11-09 00:30:00,68.17,79.997,37.177,32.225 +2020-11-09 00:45:00,67.31,80.294,37.177,32.225 +2020-11-09 01:00:00,65.98,82.068,35.358000000000004,32.225 +2020-11-09 01:15:00,65.25,82.456,35.358000000000004,32.225 +2020-11-09 01:30:00,64.89,82.23299999999999,35.358000000000004,32.225 +2020-11-09 01:45:00,65.37,82.22,35.358000000000004,32.225 +2020-11-09 02:00:00,63.02,83.67399999999999,35.03,32.225 +2020-11-09 02:15:00,64.11,83.83,35.03,32.225 +2020-11-09 02:30:00,64.54,84.574,35.03,32.225 +2020-11-09 02:45:00,64.76,85.359,35.03,32.225 +2020-11-09 03:00:00,64.28,89.001,34.394,32.225 +2020-11-09 03:15:00,66.01,90.586,34.394,32.225 +2020-11-09 03:30:00,66.41,91.126,34.394,32.225 +2020-11-09 03:45:00,67.37,92.016,34.394,32.225 +2020-11-09 04:00:00,69.97,104.598,34.421,32.225 +2020-11-09 04:15:00,71.09,116.309,34.421,32.225 +2020-11-09 04:30:00,71.91,116.883,34.421,32.225 +2020-11-09 04:45:00,79.32,118.117,34.421,32.225 +2020-11-09 05:00:00,82.71,147.628,39.435,32.225 +2020-11-09 05:15:00,87.16,176.90900000000002,39.435,32.225 +2020-11-09 05:30:00,87.32,171.798,39.435,32.225 +2020-11-09 05:45:00,101.72,162.31799999999998,39.435,32.225 +2020-11-09 06:00:00,110.45,160.78,55.685,32.225 +2020-11-09 06:15:00,113.86,164.645,55.685,32.225 +2020-11-09 06:30:00,115.39,164.861,55.685,32.225 +2020-11-09 06:45:00,118.64,166.217,55.685,32.225 +2020-11-09 07:00:00,122.58,167.396,66.837,32.225 +2020-11-09 07:15:00,122.82,170.988,66.837,32.225 +2020-11-09 07:30:00,126.22,172.438,66.837,32.225 +2020-11-09 07:45:00,125.54,172.898,66.837,32.225 +2020-11-09 08:00:00,127.34,172.813,72.217,32.225 +2020-11-09 08:15:00,127.18,172.938,72.217,32.225 +2020-11-09 08:30:00,126.88,170.8,72.217,32.225 +2020-11-09 08:45:00,127.51,168.736,72.217,32.225 +2020-11-09 09:00:00,128.25,164.46599999999998,66.117,32.225 +2020-11-09 09:15:00,130.74,160.498,66.117,32.225 +2020-11-09 09:30:00,135.41,158.94,66.117,32.225 +2020-11-09 09:45:00,128.44,157.295,66.117,32.225 +2020-11-09 10:00:00,121.64,155.171,62.1,32.225 +2020-11-09 10:15:00,122.64,153.78,62.1,32.225 +2020-11-09 10:30:00,121.37,151.439,62.1,32.225 +2020-11-09 10:45:00,120.12,150.023,62.1,32.225 +2020-11-09 11:00:00,119.17,145.142,60.021,32.225 +2020-11-09 11:15:00,118.05,145.196,60.021,32.225 +2020-11-09 11:30:00,120.45,146.566,60.021,32.225 +2020-11-09 11:45:00,127.24,146.98,60.021,32.225 +2020-11-09 12:00:00,123.12,143.612,56.75899999999999,32.225 +2020-11-09 12:15:00,119.35,142.637,56.75899999999999,32.225 +2020-11-09 12:30:00,119.39,142.264,56.75899999999999,32.225 +2020-11-09 12:45:00,119.84,142.754,56.75899999999999,32.225 +2020-11-09 13:00:00,116.9,142.586,56.04600000000001,32.225 +2020-11-09 13:15:00,116.79,141.71200000000002,56.04600000000001,32.225 +2020-11-09 13:30:00,113.73,140.2,56.04600000000001,32.225 +2020-11-09 13:45:00,114.61,140.019,56.04600000000001,32.225 +2020-11-09 14:00:00,115.9,139.042,55.475,32.225 +2020-11-09 14:15:00,116.58,138.798,55.475,32.225 +2020-11-09 14:30:00,120.75,138.077,55.475,32.225 +2020-11-09 14:45:00,120.18,138.275,55.475,32.225 +2020-11-09 15:00:00,123.17,137.644,57.048,32.225 +2020-11-09 15:15:00,122.33,137.42,57.048,32.225 +2020-11-09 15:30:00,124.39,137.74,57.048,32.225 +2020-11-09 15:45:00,125.72,138.004,57.048,32.225 +2020-11-09 16:00:00,126.17,140.192,59.06,32.225 +2020-11-09 16:15:00,125.85,141.375,59.06,32.225 +2020-11-09 16:30:00,127.02,141.27700000000002,59.06,32.225 +2020-11-09 16:45:00,131.92,140.181,59.06,32.225 +2020-11-09 17:00:00,136.09,141.148,65.419,32.225 +2020-11-09 17:15:00,136.48,141.859,65.419,32.225 +2020-11-09 17:30:00,137.83,141.708,65.419,32.225 +2020-11-09 17:45:00,137.56,141.585,65.419,32.225 +2020-11-09 18:00:00,135.48,144.59,69.345,32.225 +2020-11-09 18:15:00,133.71,143.328,69.345,32.225 +2020-11-09 18:30:00,132.59,141.934,69.345,32.225 +2020-11-09 18:45:00,132.3,144.716,69.345,32.225 +2020-11-09 19:00:00,127.93,146.127,73.825,32.225 +2020-11-09 19:15:00,126.34,144.3,73.825,32.225 +2020-11-09 19:30:00,123.57,143.6,73.825,32.225 +2020-11-09 19:45:00,121.6,142.05,73.825,32.225 +2020-11-09 20:00:00,115.11,137.256,64.027,32.225 +2020-11-09 20:15:00,112.04,134.487,64.027,32.225 +2020-11-09 20:30:00,108.08,132.988,64.027,32.225 +2020-11-09 20:45:00,107.81,131.05700000000002,64.027,32.225 +2020-11-09 21:00:00,107.92,126.76799999999999,57.952,32.225 +2020-11-09 21:15:00,109.06,125.969,57.952,32.225 +2020-11-09 21:30:00,106.65,125.475,57.952,32.225 +2020-11-09 21:45:00,100.41,122.554,57.952,32.225 +2020-11-09 22:00:00,92.6,115.631,53.031000000000006,32.225 +2020-11-09 22:15:00,93.12,111.51899999999999,53.031000000000006,32.225 +2020-11-09 22:30:00,95.32,98.29700000000001,53.031000000000006,32.225 +2020-11-09 22:45:00,94.64,90.72200000000001,53.031000000000006,32.225 +2020-11-09 23:00:00,90.53,85.54799999999999,45.085,32.225 +2020-11-09 23:15:00,89.52,84.38799999999999,45.085,32.225 +2020-11-09 23:30:00,88.09,83.963,45.085,32.225 +2020-11-09 23:45:00,87.29,84.242,45.085,32.225 +2020-11-10 00:00:00,79.63,79.074,42.843,32.225 +2020-11-10 00:15:00,76.93,80.104,42.843,32.225 +2020-11-10 00:30:00,82.65,79.929,42.843,32.225 +2020-11-10 00:45:00,81.56,79.919,42.843,32.225 +2020-11-10 01:00:00,80.25,81.372,41.542,32.225 +2020-11-10 01:15:00,76.77,81.541,41.542,32.225 +2020-11-10 01:30:00,79.55,81.388,41.542,32.225 +2020-11-10 01:45:00,79.66,81.351,41.542,32.225 +2020-11-10 02:00:00,78.05,82.617,40.19,32.225 +2020-11-10 02:15:00,73.84,83.189,40.19,32.225 +2020-11-10 02:30:00,77.2,83.39,40.19,32.225 +2020-11-10 02:45:00,79.47,84.315,40.19,32.225 +2020-11-10 03:00:00,79.27,86.96600000000001,39.626,32.225 +2020-11-10 03:15:00,78.58,88.43700000000001,39.626,32.225 +2020-11-10 03:30:00,80.16,89.265,39.626,32.225 +2020-11-10 03:45:00,80.07,89.79799999999999,39.626,32.225 +2020-11-10 04:00:00,78.72,101.744,40.196999999999996,32.225 +2020-11-10 04:15:00,81.21,113.26899999999999,40.196999999999996,32.225 +2020-11-10 04:30:00,83.65,113.595,40.196999999999996,32.225 +2020-11-10 04:45:00,86.52,115.79,40.196999999999996,32.225 +2020-11-10 05:00:00,87.82,149.101,43.378,32.225 +2020-11-10 05:15:00,90.74,178.55599999999998,43.378,32.225 +2020-11-10 05:30:00,99.8,172.574,43.378,32.225 +2020-11-10 05:45:00,101.83,162.769,43.378,32.225 +2020-11-10 06:00:00,103.85,160.92600000000002,55.691,32.225 +2020-11-10 06:15:00,109.52,165.903,55.691,32.225 +2020-11-10 06:30:00,115.89,165.612,55.691,32.225 +2020-11-10 06:45:00,121.67,166.38299999999998,55.691,32.225 +2020-11-10 07:00:00,125.99,167.50599999999997,65.567,32.225 +2020-11-10 07:15:00,125.23,170.90400000000002,65.567,32.225 +2020-11-10 07:30:00,125.48,172.033,65.567,32.225 +2020-11-10 07:45:00,127.15,172.24599999999998,65.567,32.225 +2020-11-10 08:00:00,130.42,172.225,73.001,32.225 +2020-11-10 08:15:00,129.33,171.486,73.001,32.225 +2020-11-10 08:30:00,128.62,169.31900000000002,73.001,32.225 +2020-11-10 08:45:00,130.72,166.703,73.001,32.225 +2020-11-10 09:00:00,130.86,161.987,67.08800000000001,32.225 +2020-11-10 09:15:00,133.46,159.045,67.08800000000001,32.225 +2020-11-10 09:30:00,134.07,158.188,67.08800000000001,32.225 +2020-11-10 09:45:00,134.92,156.86,67.08800000000001,32.225 +2020-11-10 10:00:00,138.69,153.835,62.803000000000004,32.225 +2020-11-10 10:15:00,136.4,151.625,62.803000000000004,32.225 +2020-11-10 10:30:00,138.3,149.394,62.803000000000004,32.225 +2020-11-10 10:45:00,138.45,148.505,62.803000000000004,32.225 +2020-11-10 11:00:00,138.8,144.653,60.155,32.225 +2020-11-10 11:15:00,140.26,144.589,60.155,32.225 +2020-11-10 11:30:00,138.98,144.757,60.155,32.225 +2020-11-10 11:45:00,138.44,145.502,60.155,32.225 +2020-11-10 12:00:00,136.24,141.084,56.845,32.225 +2020-11-10 12:15:00,132.78,139.921,56.845,32.225 +2020-11-10 12:30:00,132.82,140.374,56.845,32.225 +2020-11-10 12:45:00,133.55,140.88,56.845,32.225 +2020-11-10 13:00:00,133.51,140.255,56.163000000000004,32.225 +2020-11-10 13:15:00,133.18,139.71,56.163000000000004,32.225 +2020-11-10 13:30:00,132.48,139.026,56.163000000000004,32.225 +2020-11-10 13:45:00,135.29,138.667,56.163000000000004,32.225 +2020-11-10 14:00:00,132.64,137.986,55.934,32.225 +2020-11-10 14:15:00,133.0,137.806,55.934,32.225 +2020-11-10 14:30:00,130.66,137.678,55.934,32.225 +2020-11-10 14:45:00,129.66,137.57,55.934,32.225 +2020-11-10 15:00:00,129.09,136.569,57.43899999999999,32.225 +2020-11-10 15:15:00,128.92,136.889,57.43899999999999,32.225 +2020-11-10 15:30:00,125.2,137.341,57.43899999999999,32.225 +2020-11-10 15:45:00,127.27,137.435,57.43899999999999,32.225 +2020-11-10 16:00:00,129.73,139.649,59.968999999999994,32.225 +2020-11-10 16:15:00,128.3,141.219,59.968999999999994,32.225 +2020-11-10 16:30:00,129.86,141.44799999999998,59.968999999999994,32.225 +2020-11-10 16:45:00,133.6,140.843,59.968999999999994,32.225 +2020-11-10 17:00:00,138.8,142.224,67.428,32.225 +2020-11-10 17:15:00,136.91,143.123,67.428,32.225 +2020-11-10 17:30:00,136.54,143.278,67.428,32.225 +2020-11-10 17:45:00,137.56,142.963,67.428,32.225 +2020-11-10 18:00:00,136.51,145.55,71.533,32.225 +2020-11-10 18:15:00,134.26,144.453,71.533,32.225 +2020-11-10 18:30:00,133.81,142.766,71.533,32.225 +2020-11-10 18:45:00,133.71,146.015,71.533,32.225 +2020-11-10 19:00:00,131.4,147.056,73.32300000000001,32.225 +2020-11-10 19:15:00,128.61,145.097,73.32300000000001,32.225 +2020-11-10 19:30:00,126.59,143.855,73.32300000000001,32.225 +2020-11-10 19:45:00,125.18,142.424,73.32300000000001,32.225 +2020-11-10 20:00:00,121.52,137.89600000000002,64.166,32.225 +2020-11-10 20:15:00,115.1,134.157,64.166,32.225 +2020-11-10 20:30:00,113.81,133.317,64.166,32.225 +2020-11-10 20:45:00,112.32,131.153,64.166,32.225 +2020-11-10 21:00:00,107.06,126.714,57.891999999999996,32.225 +2020-11-10 21:15:00,105.3,125.95200000000001,57.891999999999996,32.225 +2020-11-10 21:30:00,102.67,125.04799999999999,57.891999999999996,32.225 +2020-11-10 21:45:00,101.94,122.339,57.891999999999996,32.225 +2020-11-10 22:00:00,98.35,116.615,53.242,32.225 +2020-11-10 22:15:00,100.16,112.191,53.242,32.225 +2020-11-10 22:30:00,98.28,99.18,53.242,32.225 +2020-11-10 22:45:00,96.68,91.795,53.242,32.225 +2020-11-10 23:00:00,88.01,86.35799999999999,46.665,32.225 +2020-11-10 23:15:00,84.04,85.154,46.665,32.225 +2020-11-10 23:30:00,84.59,84.492,46.665,32.225 +2020-11-10 23:45:00,81.05,84.535,46.665,32.225 +2020-11-11 00:00:00,83.4,79.433,43.16,32.225 +2020-11-11 00:15:00,83.26,80.444,43.16,32.225 +2020-11-11 00:30:00,84.67,80.27,43.16,32.225 +2020-11-11 00:45:00,81.96,80.248,43.16,32.225 +2020-11-11 01:00:00,78.35,81.723,40.972,32.225 +2020-11-11 01:15:00,81.15,81.906,40.972,32.225 +2020-11-11 01:30:00,81.08,81.768,40.972,32.225 +2020-11-11 01:45:00,81.16,81.72399999999999,40.972,32.225 +2020-11-11 02:00:00,78.61,83.00299999999999,39.749,32.225 +2020-11-11 02:15:00,80.73,83.584,39.749,32.225 +2020-11-11 02:30:00,81.45,83.777,39.749,32.225 +2020-11-11 02:45:00,79.88,84.698,39.749,32.225 +2020-11-11 03:00:00,77.2,87.337,39.422,32.225 +2020-11-11 03:15:00,82.68,88.831,39.422,32.225 +2020-11-11 03:30:00,82.22,89.661,39.422,32.225 +2020-11-11 03:45:00,83.11,90.179,39.422,32.225 +2020-11-11 04:00:00,83.29,102.134,40.505,32.225 +2020-11-11 04:15:00,85.37,113.684,40.505,32.225 +2020-11-11 04:30:00,86.75,114.00299999999999,40.505,32.225 +2020-11-11 04:45:00,86.34,116.20700000000001,40.505,32.225 +2020-11-11 05:00:00,93.0,149.563,43.397,32.225 +2020-11-11 05:15:00,96.5,179.05900000000003,43.397,32.225 +2020-11-11 05:30:00,96.82,173.071,43.397,32.225 +2020-11-11 05:45:00,95.16,163.246,43.397,32.225 +2020-11-11 06:00:00,104.31,161.401,55.218,32.225 +2020-11-11 06:15:00,112.72,166.389,55.218,32.225 +2020-11-11 06:30:00,122.08,166.125,55.218,32.225 +2020-11-11 06:45:00,124.33,166.916,55.218,32.225 +2020-11-11 07:00:00,126.72,168.04,67.39,32.225 +2020-11-11 07:15:00,123.92,171.447,67.39,32.225 +2020-11-11 07:30:00,127.19,172.601,67.39,32.225 +2020-11-11 07:45:00,125.8,172.813,67.39,32.225 +2020-11-11 08:00:00,126.17,172.80700000000002,74.345,32.225 +2020-11-11 08:15:00,124.97,172.047,74.345,32.225 +2020-11-11 08:30:00,126.2,169.903,74.345,32.225 +2020-11-11 08:45:00,125.23,167.25900000000001,74.345,32.225 +2020-11-11 09:00:00,124.4,162.53,69.336,32.225 +2020-11-11 09:15:00,125.54,159.589,69.336,32.225 +2020-11-11 09:30:00,123.59,158.725,69.336,32.225 +2020-11-11 09:45:00,121.93,157.375,69.336,32.225 +2020-11-11 10:00:00,121.82,154.341,64.291,32.225 +2020-11-11 10:15:00,120.73,152.095,64.291,32.225 +2020-11-11 10:30:00,118.92,149.842,64.291,32.225 +2020-11-11 10:45:00,121.37,148.938,64.291,32.225 +2020-11-11 11:00:00,120.95,145.084,62.20399999999999,32.225 +2020-11-11 11:15:00,122.46,145.001,62.20399999999999,32.225 +2020-11-11 11:30:00,121.56,145.16899999999998,62.20399999999999,32.225 +2020-11-11 11:45:00,121.47,145.9,62.20399999999999,32.225 +2020-11-11 12:00:00,120.37,141.465,59.042,32.225 +2020-11-11 12:15:00,119.79,140.303,59.042,32.225 +2020-11-11 12:30:00,119.63,140.78799999999998,59.042,32.225 +2020-11-11 12:45:00,121.96,141.296,59.042,32.225 +2020-11-11 13:00:00,120.59,140.635,57.907,32.225 +2020-11-11 13:15:00,122.95,140.09799999999998,57.907,32.225 +2020-11-11 13:30:00,118.81,139.411,57.907,32.225 +2020-11-11 13:45:00,117.0,139.046,57.907,32.225 +2020-11-11 14:00:00,116.01,138.31799999999998,58.358000000000004,32.225 +2020-11-11 14:15:00,115.36,138.151,58.358000000000004,32.225 +2020-11-11 14:30:00,116.38,138.061,58.358000000000004,32.225 +2020-11-11 14:45:00,119.55,137.955,58.358000000000004,32.225 +2020-11-11 15:00:00,123.96,136.944,59.348,32.225 +2020-11-11 15:15:00,123.92,137.278,59.348,32.225 +2020-11-11 15:30:00,125.52,137.766,59.348,32.225 +2020-11-11 15:45:00,123.32,137.872,59.348,32.225 +2020-11-11 16:00:00,125.75,140.063,61.413999999999994,32.225 +2020-11-11 16:15:00,126.76,141.656,61.413999999999994,32.225 +2020-11-11 16:30:00,128.31,141.88299999999998,61.413999999999994,32.225 +2020-11-11 16:45:00,133.13,141.327,61.413999999999994,32.225 +2020-11-11 17:00:00,138.86,142.668,67.107,32.225 +2020-11-11 17:15:00,136.54,143.589,67.107,32.225 +2020-11-11 17:30:00,137.81,143.75,67.107,32.225 +2020-11-11 17:45:00,137.09,143.44899999999998,67.107,32.225 +2020-11-11 18:00:00,136.75,146.037,71.92,32.225 +2020-11-11 18:15:00,134.87,144.91,71.92,32.225 +2020-11-11 18:30:00,134.79,143.232,71.92,32.225 +2020-11-11 18:45:00,135.01,146.47899999999998,71.92,32.225 +2020-11-11 19:00:00,130.77,147.524,75.09,32.225 +2020-11-11 19:15:00,128.61,145.55700000000002,75.09,32.225 +2020-11-11 19:30:00,126.23,144.30100000000002,75.09,32.225 +2020-11-11 19:45:00,129.72,142.842,75.09,32.225 +2020-11-11 20:00:00,120.35,138.33,65.977,32.225 +2020-11-11 20:15:00,114.91,134.583,65.977,32.225 +2020-11-11 20:30:00,111.86,133.71200000000002,65.977,32.225 +2020-11-11 20:45:00,111.47,131.542,65.977,32.225 +2020-11-11 21:00:00,106.24,127.095,58.798,32.225 +2020-11-11 21:15:00,111.59,126.32,58.798,32.225 +2020-11-11 21:30:00,109.24,125.426,58.798,32.225 +2020-11-11 21:45:00,110.46,122.704,58.798,32.225 +2020-11-11 22:00:00,100.23,116.978,54.486000000000004,32.225 +2020-11-11 22:15:00,95.43,112.539,54.486000000000004,32.225 +2020-11-11 22:30:00,93.35,99.57,54.486000000000004,32.225 +2020-11-11 22:45:00,97.79,92.194,54.486000000000004,32.225 +2020-11-11 23:00:00,94.38,86.758,47.783,32.225 +2020-11-11 23:15:00,93.93,85.53,47.783,32.225 +2020-11-11 23:30:00,85.88,84.87299999999999,47.783,32.225 +2020-11-11 23:45:00,84.95,84.90299999999999,47.783,32.225 +2020-11-12 00:00:00,85.17,79.789,43.88,32.225 +2020-11-12 00:15:00,87.34,80.78,43.88,32.225 +2020-11-12 00:30:00,85.98,80.609,43.88,32.225 +2020-11-12 00:45:00,80.22,80.575,43.88,32.225 +2020-11-12 01:00:00,83.59,82.071,42.242,32.225 +2020-11-12 01:15:00,84.86,82.26899999999999,42.242,32.225 +2020-11-12 01:30:00,83.06,82.146,42.242,32.225 +2020-11-12 01:45:00,81.74,82.094,42.242,32.225 +2020-11-12 02:00:00,83.52,83.387,40.918,32.225 +2020-11-12 02:15:00,83.31,83.976,40.918,32.225 +2020-11-12 02:30:00,78.25,84.161,40.918,32.225 +2020-11-12 02:45:00,78.19,85.07700000000001,40.918,32.225 +2020-11-12 03:00:00,75.8,87.70299999999999,40.411,32.225 +2020-11-12 03:15:00,78.38,89.221,40.411,32.225 +2020-11-12 03:30:00,82.84,90.053,40.411,32.225 +2020-11-12 03:45:00,85.06,90.555,40.411,32.225 +2020-11-12 04:00:00,83.04,102.51899999999999,41.246,32.225 +2020-11-12 04:15:00,79.36,114.094,41.246,32.225 +2020-11-12 04:30:00,80.43,114.40899999999999,41.246,32.225 +2020-11-12 04:45:00,81.3,116.619,41.246,32.225 +2020-11-12 05:00:00,85.8,150.023,44.533,32.225 +2020-11-12 05:15:00,88.26,179.558,44.533,32.225 +2020-11-12 05:30:00,92.04,173.56400000000002,44.533,32.225 +2020-11-12 05:45:00,97.64,163.719,44.533,32.225 +2020-11-12 06:00:00,113.62,161.872,55.005,32.225 +2020-11-12 06:15:00,116.6,166.87,55.005,32.225 +2020-11-12 06:30:00,120.04,166.635,55.005,32.225 +2020-11-12 06:45:00,120.88,167.44400000000002,55.005,32.225 +2020-11-12 07:00:00,127.95,168.56900000000002,64.597,32.225 +2020-11-12 07:15:00,124.59,171.987,64.597,32.225 +2020-11-12 07:30:00,125.29,173.16299999999998,64.597,32.225 +2020-11-12 07:45:00,127.13,173.375,64.597,32.225 +2020-11-12 08:00:00,129.63,173.38099999999997,71.71600000000001,32.225 +2020-11-12 08:15:00,127.3,172.602,71.71600000000001,32.225 +2020-11-12 08:30:00,127.82,170.482,71.71600000000001,32.225 +2020-11-12 08:45:00,127.07,167.80900000000003,71.71600000000001,32.225 +2020-11-12 09:00:00,123.74,163.067,66.51899999999999,32.225 +2020-11-12 09:15:00,124.7,160.127,66.51899999999999,32.225 +2020-11-12 09:30:00,127.83,159.257,66.51899999999999,32.225 +2020-11-12 09:45:00,129.16,157.885,66.51899999999999,32.225 +2020-11-12 10:00:00,129.07,154.842,63.04,32.225 +2020-11-12 10:15:00,128.54,152.56,63.04,32.225 +2020-11-12 10:30:00,127.42,150.284,63.04,32.225 +2020-11-12 10:45:00,124.95,149.365,63.04,32.225 +2020-11-12 11:00:00,124.62,145.50799999999998,60.998000000000005,32.225 +2020-11-12 11:15:00,124.2,145.406,60.998000000000005,32.225 +2020-11-12 11:30:00,123.72,145.57399999999998,60.998000000000005,32.225 +2020-11-12 11:45:00,125.51,146.29399999999998,60.998000000000005,32.225 +2020-11-12 12:00:00,126.91,141.84,58.27,32.225 +2020-11-12 12:15:00,122.96,140.68,58.27,32.225 +2020-11-12 12:30:00,121.82,141.197,58.27,32.225 +2020-11-12 12:45:00,116.29,141.707,58.27,32.225 +2020-11-12 13:00:00,117.07,141.012,57.196000000000005,32.225 +2020-11-12 13:15:00,119.85,140.481,57.196000000000005,32.225 +2020-11-12 13:30:00,119.33,139.791,57.196000000000005,32.225 +2020-11-12 13:45:00,120.26,139.41899999999998,57.196000000000005,32.225 +2020-11-12 14:00:00,118.83,138.64600000000002,57.38399999999999,32.225 +2020-11-12 14:15:00,120.08,138.493,57.38399999999999,32.225 +2020-11-12 14:30:00,121.28,138.439,57.38399999999999,32.225 +2020-11-12 14:45:00,124.13,138.335,57.38399999999999,32.225 +2020-11-12 15:00:00,125.14,137.315,58.647,32.225 +2020-11-12 15:15:00,123.4,137.661,58.647,32.225 +2020-11-12 15:30:00,122.12,138.186,58.647,32.225 +2020-11-12 15:45:00,123.51,138.30200000000002,58.647,32.225 +2020-11-12 16:00:00,126.72,140.471,60.083999999999996,32.225 +2020-11-12 16:15:00,125.87,142.08700000000002,60.083999999999996,32.225 +2020-11-12 16:30:00,129.87,142.313,60.083999999999996,32.225 +2020-11-12 16:45:00,133.87,141.805,60.083999999999996,32.225 +2020-11-12 17:00:00,137.7,143.105,65.85600000000001,32.225 +2020-11-12 17:15:00,137.13,144.049,65.85600000000001,32.225 +2020-11-12 17:30:00,139.61,144.218,65.85600000000001,32.225 +2020-11-12 17:45:00,138.78,143.93,65.85600000000001,32.225 +2020-11-12 18:00:00,137.47,146.519,69.855,32.225 +2020-11-12 18:15:00,136.57,145.361,69.855,32.225 +2020-11-12 18:30:00,135.22,143.692,69.855,32.225 +2020-11-12 18:45:00,135.89,146.94,69.855,32.225 +2020-11-12 19:00:00,131.65,147.986,74.015,32.225 +2020-11-12 19:15:00,131.85,146.012,74.015,32.225 +2020-11-12 19:30:00,129.37,144.74200000000002,74.015,32.225 +2020-11-12 19:45:00,126.94,143.257,74.015,32.225 +2020-11-12 20:00:00,119.74,138.762,65.316,32.225 +2020-11-12 20:15:00,117.13,135.005,65.316,32.225 +2020-11-12 20:30:00,115.09,134.105,65.316,32.225 +2020-11-12 20:45:00,111.94,131.92600000000002,65.316,32.225 +2020-11-12 21:00:00,107.77,127.473,58.403999999999996,32.225 +2020-11-12 21:15:00,113.2,126.684,58.403999999999996,32.225 +2020-11-12 21:30:00,112.61,125.8,58.403999999999996,32.225 +2020-11-12 21:45:00,110.3,123.066,58.403999999999996,32.225 +2020-11-12 22:00:00,100.12,117.338,54.092,32.225 +2020-11-12 22:15:00,96.26,112.885,54.092,32.225 +2020-11-12 22:30:00,99.58,99.958,54.092,32.225 +2020-11-12 22:45:00,99.58,92.59,54.092,32.225 +2020-11-12 23:00:00,93.92,87.156,48.18600000000001,32.225 +2020-11-12 23:15:00,90.66,85.905,48.18600000000001,32.225 +2020-11-12 23:30:00,82.77,85.25,48.18600000000001,32.225 +2020-11-12 23:45:00,93.39,85.26700000000001,48.18600000000001,32.225 +2020-11-13 00:00:00,88.54,78.727,45.18899999999999,32.225 +2020-11-13 00:15:00,87.54,79.914,45.18899999999999,32.225 +2020-11-13 00:30:00,83.28,79.76899999999999,45.18899999999999,32.225 +2020-11-13 00:45:00,75.68,79.967,45.18899999999999,32.225 +2020-11-13 01:00:00,83.0,81.128,43.256,32.225 +2020-11-13 01:15:00,83.41,81.67699999999999,43.256,32.225 +2020-11-13 01:30:00,80.05,81.719,43.256,32.225 +2020-11-13 01:45:00,78.52,81.625,43.256,32.225 +2020-11-13 02:00:00,74.26,83.368,42.312,32.225 +2020-11-13 02:15:00,81.05,83.87,42.312,32.225 +2020-11-13 02:30:00,84.7,84.721,42.312,32.225 +2020-11-13 02:45:00,85.03,85.37799999999999,42.312,32.225 +2020-11-13 03:00:00,79.15,87.684,41.833,32.225 +2020-11-13 03:15:00,80.84,89.316,41.833,32.225 +2020-11-13 03:30:00,82.96,90.04,41.833,32.225 +2020-11-13 03:45:00,87.43,91.09899999999999,41.833,32.225 +2020-11-13 04:00:00,85.9,103.27,42.732,32.225 +2020-11-13 04:15:00,84.02,114.059,42.732,32.225 +2020-11-13 04:30:00,88.32,114.913,42.732,32.225 +2020-11-13 04:45:00,91.69,116.14200000000001,42.732,32.225 +2020-11-13 05:00:00,90.57,148.593,46.254,32.225 +2020-11-13 05:15:00,89.24,179.515,46.254,32.225 +2020-11-13 05:30:00,100.06,174.269,46.254,32.225 +2020-11-13 05:45:00,105.63,164.176,46.254,32.225 +2020-11-13 06:00:00,111.79,162.717,56.76,32.225 +2020-11-13 06:15:00,111.89,166.858,56.76,32.225 +2020-11-13 06:30:00,118.4,166.08,56.76,32.225 +2020-11-13 06:45:00,122.39,167.889,56.76,32.225 +2020-11-13 07:00:00,129.36,168.747,66.029,32.225 +2020-11-13 07:15:00,127.21,173.19799999999998,66.029,32.225 +2020-11-13 07:30:00,127.28,173.50900000000001,66.029,32.225 +2020-11-13 07:45:00,128.94,173.06,66.029,32.225 +2020-11-13 08:00:00,131.25,172.65400000000002,73.128,32.225 +2020-11-13 08:15:00,129.89,171.84900000000002,73.128,32.225 +2020-11-13 08:30:00,128.17,170.36700000000002,73.128,32.225 +2020-11-13 08:45:00,128.29,166.558,73.128,32.225 +2020-11-13 09:00:00,123.97,161.243,68.23100000000001,32.225 +2020-11-13 09:15:00,128.09,159.393,68.23100000000001,32.225 +2020-11-13 09:30:00,127.96,157.974,68.23100000000001,32.225 +2020-11-13 09:45:00,126.9,156.645,68.23100000000001,32.225 +2020-11-13 10:00:00,123.72,152.725,64.733,32.225 +2020-11-13 10:15:00,124.12,150.817,64.733,32.225 +2020-11-13 10:30:00,121.64,148.661,64.733,32.225 +2020-11-13 10:45:00,122.3,147.386,64.733,32.225 +2020-11-13 11:00:00,119.98,143.577,62.0,32.225 +2020-11-13 11:15:00,121.51,142.455,62.0,32.225 +2020-11-13 11:30:00,120.54,143.74,62.0,32.225 +2020-11-13 11:45:00,120.13,144.156,62.0,32.225 +2020-11-13 12:00:00,119.43,140.618,57.876999999999995,32.225 +2020-11-13 12:15:00,119.97,137.744,57.876999999999995,32.225 +2020-11-13 12:30:00,120.64,138.444,57.876999999999995,32.225 +2020-11-13 12:45:00,119.97,139.069,57.876999999999995,32.225 +2020-11-13 13:00:00,117.26,139.217,55.585,32.225 +2020-11-13 13:15:00,116.98,139.343,55.585,32.225 +2020-11-13 13:30:00,114.0,138.91899999999998,55.585,32.225 +2020-11-13 13:45:00,115.51,138.592,55.585,32.225 +2020-11-13 14:00:00,113.7,136.691,54.5,32.225 +2020-11-13 14:15:00,116.76,136.561,54.5,32.225 +2020-11-13 14:30:00,115.06,137.4,54.5,32.225 +2020-11-13 14:45:00,117.33,137.297,54.5,32.225 +2020-11-13 15:00:00,118.57,135.893,55.131,32.225 +2020-11-13 15:15:00,121.83,135.856,55.131,32.225 +2020-11-13 15:30:00,118.63,135.135,55.131,32.225 +2020-11-13 15:45:00,120.04,135.612,55.131,32.225 +2020-11-13 16:00:00,123.01,136.678,56.8,32.225 +2020-11-13 16:15:00,123.05,138.69,56.8,32.225 +2020-11-13 16:30:00,127.02,138.92,56.8,32.225 +2020-11-13 16:45:00,132.91,138.112,56.8,32.225 +2020-11-13 17:00:00,136.31,140.149,63.428999999999995,32.225 +2020-11-13 17:15:00,134.14,140.769,63.428999999999995,32.225 +2020-11-13 17:30:00,135.6,140.783,63.428999999999995,32.225 +2020-11-13 17:45:00,136.85,140.28799999999998,63.428999999999995,32.225 +2020-11-13 18:00:00,132.99,143.393,67.915,32.225 +2020-11-13 18:15:00,132.55,141.596,67.915,32.225 +2020-11-13 18:30:00,131.85,140.16899999999998,67.915,32.225 +2020-11-13 18:45:00,131.77,143.563,67.915,32.225 +2020-11-13 19:00:00,127.72,145.562,69.428,32.225 +2020-11-13 19:15:00,126.33,144.722,69.428,32.225 +2020-11-13 19:30:00,123.37,143.181,69.428,32.225 +2020-11-13 19:45:00,122.12,140.983,69.428,32.225 +2020-11-13 20:00:00,116.08,136.49200000000002,60.56100000000001,32.225 +2020-11-13 20:15:00,112.51,133.029,60.56100000000001,32.225 +2020-11-13 20:30:00,109.7,131.888,60.56100000000001,32.225 +2020-11-13 20:45:00,109.08,129.773,60.56100000000001,32.225 +2020-11-13 21:00:00,102.38,126.15,55.18600000000001,32.225 +2020-11-13 21:15:00,102.35,126.273,55.18600000000001,32.225 +2020-11-13 21:30:00,103.54,125.37200000000001,55.18600000000001,32.225 +2020-11-13 21:45:00,102.4,123.066,55.18600000000001,32.225 +2020-11-13 22:00:00,94.88,117.936,51.433,32.225 +2020-11-13 22:15:00,90.57,113.288,51.433,32.225 +2020-11-13 22:30:00,83.68,106.458,51.433,32.225 +2020-11-13 22:45:00,88.07,101.781,51.433,32.225 +2020-11-13 23:00:00,87.2,96.691,46.201,32.225 +2020-11-13 23:15:00,86.93,93.52,46.201,32.225 +2020-11-13 23:30:00,81.06,91.205,46.201,32.225 +2020-11-13 23:45:00,75.58,90.72,46.201,32.225 +2020-11-14 00:00:00,73.58,77.764,42.576,32.047 +2020-11-14 00:15:00,74.26,75.94,42.576,32.047 +2020-11-14 00:30:00,76.41,76.453,42.576,32.047 +2020-11-14 00:45:00,75.17,76.795,42.576,32.047 +2020-11-14 01:00:00,67.85,78.506,39.34,32.047 +2020-11-14 01:15:00,73.52,78.689,39.34,32.047 +2020-11-14 01:30:00,72.25,78.084,39.34,32.047 +2020-11-14 01:45:00,73.31,78.359,39.34,32.047 +2020-11-14 02:00:00,65.19,80.163,37.582,32.047 +2020-11-14 02:15:00,71.47,80.111,37.582,32.047 +2020-11-14 02:30:00,72.68,79.926,37.582,32.047 +2020-11-14 02:45:00,72.1,80.97,37.582,32.047 +2020-11-14 03:00:00,65.86,83.103,36.523,32.047 +2020-11-14 03:15:00,72.14,83.70299999999999,36.523,32.047 +2020-11-14 03:30:00,71.64,83.572,36.523,32.047 +2020-11-14 03:45:00,67.64,85.304,36.523,32.047 +2020-11-14 04:00:00,64.63,94.146,36.347,32.047 +2020-11-14 04:15:00,66.11,103.001,36.347,32.047 +2020-11-14 04:30:00,66.46,101.794,36.347,32.047 +2020-11-14 04:45:00,67.22,102.839,36.347,32.047 +2020-11-14 05:00:00,66.75,122.023,36.407,32.047 +2020-11-14 05:15:00,66.43,136.593,36.407,32.047 +2020-11-14 05:30:00,66.52,132.209,36.407,32.047 +2020-11-14 05:45:00,69.84,127.36399999999999,36.407,32.047 +2020-11-14 06:00:00,68.77,141.99200000000002,38.228,32.047 +2020-11-14 06:15:00,71.68,159.651,38.228,32.047 +2020-11-14 06:30:00,74.01,154.347,38.228,32.047 +2020-11-14 06:45:00,75.87,149.19799999999998,38.228,32.047 +2020-11-14 07:00:00,77.55,146.75,41.905,32.047 +2020-11-14 07:15:00,78.66,149.886,41.905,32.047 +2020-11-14 07:30:00,84.59,152.562,41.905,32.047 +2020-11-14 07:45:00,85.21,155.03799999999998,41.905,32.047 +2020-11-14 08:00:00,88.67,157.241,46.051,32.047 +2020-11-14 08:15:00,88.43,158.736,46.051,32.047 +2020-11-14 08:30:00,88.2,158.338,46.051,32.047 +2020-11-14 08:45:00,89.95,156.94899999999998,46.051,32.047 +2020-11-14 09:00:00,90.45,153.843,46.683,32.047 +2020-11-14 09:15:00,92.7,152.679,46.683,32.047 +2020-11-14 09:30:00,89.36,152.045,46.683,32.047 +2020-11-14 09:45:00,91.08,150.639,46.683,32.047 +2020-11-14 10:00:00,92.47,146.975,44.425,32.047 +2020-11-14 10:15:00,89.41,145.27,44.425,32.047 +2020-11-14 10:30:00,89.74,143.088,44.425,32.047 +2020-11-14 10:45:00,89.93,142.576,44.425,32.047 +2020-11-14 11:00:00,88.64,138.803,42.148999999999994,32.047 +2020-11-14 11:15:00,89.78,137.474,42.148999999999994,32.047 +2020-11-14 11:30:00,87.93,138.07,42.148999999999994,32.047 +2020-11-14 11:45:00,90.55,138.03,42.148999999999994,32.047 +2020-11-14 12:00:00,88.09,133.856,39.683,32.047 +2020-11-14 12:15:00,84.91,131.719,39.683,32.047 +2020-11-14 12:30:00,85.92,132.635,39.683,32.047 +2020-11-14 12:45:00,81.63,132.94799999999998,39.683,32.047 +2020-11-14 13:00:00,83.08,132.429,37.154,32.047 +2020-11-14 13:15:00,79.91,130.911,37.154,32.047 +2020-11-14 13:30:00,78.46,130.222,37.154,32.047 +2020-11-14 13:45:00,81.0,129.796,37.154,32.047 +2020-11-14 14:00:00,78.11,128.738,36.457,32.047 +2020-11-14 14:15:00,80.05,127.838,36.457,32.047 +2020-11-14 14:30:00,79.25,127.26700000000001,36.457,32.047 +2020-11-14 14:45:00,81.47,127.476,36.457,32.047 +2020-11-14 15:00:00,82.81,126.669,38.257,32.047 +2020-11-14 15:15:00,83.68,127.45100000000001,38.257,32.047 +2020-11-14 15:30:00,86.67,127.93799999999999,38.257,32.047 +2020-11-14 15:45:00,89.46,128.145,38.257,32.047 +2020-11-14 16:00:00,93.0,129.27100000000002,41.181000000000004,32.047 +2020-11-14 16:15:00,94.17,131.631,41.181000000000004,32.047 +2020-11-14 16:30:00,97.49,131.899,41.181000000000004,32.047 +2020-11-14 16:45:00,98.74,131.75,41.181000000000004,32.047 +2020-11-14 17:00:00,103.86,133.11,46.806000000000004,32.047 +2020-11-14 17:15:00,102.08,134.124,46.806000000000004,32.047 +2020-11-14 17:30:00,106.38,134.031,46.806000000000004,32.047 +2020-11-14 17:45:00,104.65,133.447,46.806000000000004,32.047 +2020-11-14 18:00:00,105.57,136.77700000000002,52.073,32.047 +2020-11-14 18:15:00,107.67,136.77700000000002,52.073,32.047 +2020-11-14 18:30:00,107.18,136.754,52.073,32.047 +2020-11-14 18:45:00,104.68,136.619,52.073,32.047 +2020-11-14 19:00:00,106.23,138.694,53.608000000000004,32.047 +2020-11-14 19:15:00,102.11,137.142,53.608000000000004,32.047 +2020-11-14 19:30:00,101.48,136.377,53.608000000000004,32.047 +2020-11-14 19:45:00,102.58,134.679,53.608000000000004,32.047 +2020-11-14 20:00:00,93.26,132.019,50.265,32.047 +2020-11-14 20:15:00,91.95,129.72899999999998,50.265,32.047 +2020-11-14 20:30:00,89.67,127.99700000000001,50.265,32.047 +2020-11-14 20:45:00,87.38,126.29799999999999,50.265,32.047 +2020-11-14 21:00:00,82.46,123.631,45.766000000000005,32.047 +2020-11-14 21:15:00,82.49,123.90100000000001,45.766000000000005,32.047 +2020-11-14 21:30:00,81.16,123.895,45.766000000000005,32.047 +2020-11-14 21:45:00,81.79,121.103,45.766000000000005,32.047 +2020-11-14 22:00:00,76.57,116.85799999999999,45.97,32.047 +2020-11-14 22:15:00,78.47,113.943,45.97,32.047 +2020-11-14 22:30:00,73.69,111.147,45.97,32.047 +2020-11-14 22:45:00,74.23,107.86,45.97,32.047 +2020-11-14 23:00:00,71.23,104.15299999999999,40.415,32.047 +2020-11-14 23:15:00,73.31,100.02799999999999,40.415,32.047 +2020-11-14 23:30:00,67.49,97.47399999999999,40.415,32.047 +2020-11-14 23:45:00,69.47,95.495,40.415,32.047 +2020-11-15 00:00:00,64.33,78.939,36.376,32.047 +2020-11-15 00:15:00,64.14,76.423,36.376,32.047 +2020-11-15 00:30:00,62.75,76.64,36.376,32.047 +2020-11-15 00:45:00,61.39,77.367,36.376,32.047 +2020-11-15 01:00:00,59.62,79.115,32.992,32.047 +2020-11-15 01:15:00,59.48,79.914,32.992,32.047 +2020-11-15 01:30:00,59.29,79.596,32.992,32.047 +2020-11-15 01:45:00,58.73,79.51,32.992,32.047 +2020-11-15 02:00:00,57.48,80.884,32.327,32.047 +2020-11-15 02:15:00,58.06,80.6,32.327,32.047 +2020-11-15 02:30:00,56.99,81.02,32.327,32.047 +2020-11-15 02:45:00,57.41,82.229,32.327,32.047 +2020-11-15 03:00:00,56.71,84.815,31.169,32.047 +2020-11-15 03:15:00,58.22,85.223,31.169,32.047 +2020-11-15 03:30:00,57.82,85.655,31.169,32.047 +2020-11-15 03:45:00,60.95,86.992,31.169,32.047 +2020-11-15 04:00:00,58.41,95.665,30.796,32.047 +2020-11-15 04:15:00,58.67,103.736,30.796,32.047 +2020-11-15 04:30:00,59.85,103.132,30.796,32.047 +2020-11-15 04:45:00,60.47,104.154,30.796,32.047 +2020-11-15 05:00:00,61.1,121.301,30.848000000000003,32.047 +2020-11-15 05:15:00,62.05,134.10299999999998,30.848000000000003,32.047 +2020-11-15 05:30:00,61.78,129.438,30.848000000000003,32.047 +2020-11-15 05:45:00,62.1,124.605,30.848000000000003,32.047 +2020-11-15 06:00:00,63.73,138.144,31.166,32.047 +2020-11-15 06:15:00,64.63,155.086,31.166,32.047 +2020-11-15 06:30:00,63.71,148.829,31.166,32.047 +2020-11-15 06:45:00,66.0,142.626,31.166,32.047 +2020-11-15 07:00:00,69.28,141.808,33.527,32.047 +2020-11-15 07:15:00,69.23,143.747,33.527,32.047 +2020-11-15 07:30:00,70.66,146.174,33.527,32.047 +2020-11-15 07:45:00,72.74,148.149,33.527,32.047 +2020-11-15 08:00:00,76.18,151.78799999999998,36.616,32.047 +2020-11-15 08:15:00,75.92,153.657,36.616,32.047 +2020-11-15 08:30:00,76.26,154.701,36.616,32.047 +2020-11-15 08:45:00,76.69,154.578,36.616,32.047 +2020-11-15 09:00:00,75.86,151.127,37.857,32.047 +2020-11-15 09:15:00,76.01,150.151,37.857,32.047 +2020-11-15 09:30:00,76.34,149.567,37.857,32.047 +2020-11-15 09:45:00,75.23,148.433,37.857,32.047 +2020-11-15 10:00:00,74.72,146.63299999999998,36.319,32.047 +2020-11-15 10:15:00,76.47,145.306,36.319,32.047 +2020-11-15 10:30:00,77.15,143.592,36.319,32.047 +2020-11-15 10:45:00,81.23,142.128,36.319,32.047 +2020-11-15 11:00:00,80.21,138.842,37.236999999999995,32.047 +2020-11-15 11:15:00,83.25,137.43200000000002,37.236999999999995,32.047 +2020-11-15 11:30:00,84.16,137.60299999999998,37.236999999999995,32.047 +2020-11-15 11:45:00,84.67,138.061,37.236999999999995,32.047 +2020-11-15 12:00:00,81.09,133.804,34.871,32.047 +2020-11-15 12:15:00,79.37,132.8,34.871,32.047 +2020-11-15 12:30:00,75.92,132.793,34.871,32.047 +2020-11-15 12:45:00,80.89,132.215,34.871,32.047 +2020-11-15 13:00:00,77.87,131.03799999999998,29.738000000000003,32.047 +2020-11-15 13:15:00,77.45,131.476,29.738000000000003,32.047 +2020-11-15 13:30:00,80.57,130.268,29.738000000000003,32.047 +2020-11-15 13:45:00,80.75,129.736,29.738000000000003,32.047 +2020-11-15 14:00:00,77.13,129.19799999999998,27.333000000000002,32.047 +2020-11-15 14:15:00,77.57,129.305,27.333000000000002,32.047 +2020-11-15 14:30:00,76.34,129.243,27.333000000000002,32.047 +2020-11-15 14:45:00,81.67,128.811,27.333000000000002,32.047 +2020-11-15 15:00:00,80.78,126.95700000000001,28.232,32.047 +2020-11-15 15:15:00,82.26,128.025,28.232,32.047 +2020-11-15 15:30:00,82.54,128.907,28.232,32.047 +2020-11-15 15:45:00,83.8,129.707,28.232,32.047 +2020-11-15 16:00:00,84.97,131.477,32.815,32.047 +2020-11-15 16:15:00,87.01,133.31,32.815,32.047 +2020-11-15 16:30:00,90.92,134.102,32.815,32.047 +2020-11-15 16:45:00,94.28,134.114,32.815,32.047 +2020-11-15 17:00:00,99.69,135.486,43.068999999999996,32.047 +2020-11-15 17:15:00,99.2,136.842,43.068999999999996,32.047 +2020-11-15 17:30:00,101.41,137.244,43.068999999999996,32.047 +2020-11-15 17:45:00,102.52,138.362,43.068999999999996,32.047 +2020-11-15 18:00:00,103.76,141.516,50.498999999999995,32.047 +2020-11-15 18:15:00,103.31,142.23,50.498999999999995,32.047 +2020-11-15 18:30:00,103.12,140.725,50.498999999999995,32.047 +2020-11-15 18:45:00,100.94,141.876,50.498999999999995,32.047 +2020-11-15 19:00:00,98.74,144.512,53.481,32.047 +2020-11-15 19:15:00,97.83,142.938,53.481,32.047 +2020-11-15 19:30:00,95.48,141.958,53.481,32.047 +2020-11-15 19:45:00,95.27,141.03,53.481,32.047 +2020-11-15 20:00:00,97.93,138.394,51.687,32.047 +2020-11-15 20:15:00,98.82,136.695,51.687,32.047 +2020-11-15 20:30:00,95.61,136.059,51.687,32.047 +2020-11-15 20:45:00,88.72,132.92600000000002,51.687,32.047 +2020-11-15 21:00:00,85.57,128.464,47.674,32.047 +2020-11-15 21:15:00,87.2,128.194,47.674,32.047 +2020-11-15 21:30:00,86.41,128.145,47.674,32.047 +2020-11-15 21:45:00,91.7,125.568,47.674,32.047 +2020-11-15 22:00:00,90.91,121.318,48.178000000000004,32.047 +2020-11-15 22:15:00,94.56,117.251,48.178000000000004,32.047 +2020-11-15 22:30:00,86.94,112.219,48.178000000000004,32.047 +2020-11-15 22:45:00,86.52,107.897,48.178000000000004,32.047 +2020-11-15 23:00:00,86.59,102.23200000000001,42.553999999999995,32.047 +2020-11-15 23:15:00,89.06,99.788,42.553999999999995,32.047 +2020-11-15 23:30:00,87.84,97.57,42.553999999999995,32.047 +2020-11-15 23:45:00,83.38,96.226,42.553999999999995,32.047 +2020-11-16 00:00:00,77.1,99.905,37.177,32.225 +2020-11-16 00:15:00,77.33,98.662,37.177,32.225 +2020-11-16 00:30:00,82.22,99.729,37.177,32.225 +2020-11-16 00:45:00,82.47,101.445,37.177,32.225 +2020-11-16 01:00:00,81.36,102.945,35.358000000000004,32.225 +2020-11-16 01:15:00,75.91,104.265,35.358000000000004,32.225 +2020-11-16 01:30:00,77.02,104.413,35.358000000000004,32.225 +2020-11-16 01:45:00,82.03,104.87100000000001,35.358000000000004,32.225 +2020-11-16 02:00:00,81.18,106.01299999999999,35.03,32.225 +2020-11-16 02:15:00,77.98,106.837,35.03,32.225 +2020-11-16 02:30:00,75.5,107.139,35.03,32.225 +2020-11-16 02:45:00,80.51,108.699,35.03,32.225 +2020-11-16 03:00:00,81.9,112.40299999999999,34.394,32.225 +2020-11-16 03:15:00,83.19,113.646,34.394,32.225 +2020-11-16 03:30:00,82.04,115.22200000000001,34.394,32.225 +2020-11-16 03:45:00,82.89,115.84,34.394,32.225 +2020-11-16 04:00:00,84.77,130.126,34.421,32.225 +2020-11-16 04:15:00,83.73,143.125,34.421,32.225 +2020-11-16 04:30:00,79.15,144.792,34.421,32.225 +2020-11-16 04:45:00,87.58,145.659,34.421,32.225 +2020-11-16 05:00:00,93.5,175.607,39.435,32.225 +2020-11-16 05:15:00,96.13,205.88400000000001,39.435,32.225 +2020-11-16 05:30:00,95.49,202.571,39.435,32.225 +2020-11-16 05:45:00,96.86,193.637,39.435,32.225 +2020-11-16 06:00:00,106.96,190.03,55.685,32.225 +2020-11-16 06:15:00,117.57,193.713,55.685,32.225 +2020-11-16 06:30:00,124.3,195.49400000000003,55.685,32.225 +2020-11-16 06:45:00,128.75,196.998,55.685,32.225 +2020-11-16 07:00:00,127.88,198.408,66.837,32.225 +2020-11-16 07:15:00,130.09,202.535,66.837,32.225 +2020-11-16 07:30:00,131.28,204.43900000000002,66.837,32.225 +2020-11-16 07:45:00,132.13,204.878,66.837,32.225 +2020-11-16 08:00:00,134.59,203.71,72.217,32.225 +2020-11-16 08:15:00,135.13,204.238,72.217,32.225 +2020-11-16 08:30:00,134.93,202.28799999999998,72.217,32.225 +2020-11-16 08:45:00,135.48,199.72,72.217,32.225 +2020-11-16 09:00:00,137.04,194.734,66.117,32.225 +2020-11-16 09:15:00,137.72,189.67,66.117,32.225 +2020-11-16 09:30:00,139.23,186.65099999999998,66.117,32.225 +2020-11-16 09:45:00,140.89,184.546,66.117,32.225 +2020-11-16 10:00:00,140.75,182.533,62.1,32.225 +2020-11-16 10:15:00,140.27,180.21200000000002,62.1,32.225 +2020-11-16 10:30:00,139.75,178.195,62.1,32.225 +2020-11-16 10:45:00,141.87,176.75400000000002,62.1,32.225 +2020-11-16 11:00:00,139.36,173.687,60.021,32.225 +2020-11-16 11:15:00,138.76,173.46599999999998,60.021,32.225 +2020-11-16 11:30:00,136.87,174.088,60.021,32.225 +2020-11-16 11:45:00,137.88,173.037,60.021,32.225 +2020-11-16 12:00:00,136.95,168.87,56.75899999999999,32.225 +2020-11-16 12:15:00,136.17,167.93599999999998,56.75899999999999,32.225 +2020-11-16 12:30:00,138.07,167.206,56.75899999999999,32.225 +2020-11-16 12:45:00,139.07,167.998,56.75899999999999,32.225 +2020-11-16 13:00:00,135.65,167.93099999999998,56.04600000000001,32.225 +2020-11-16 13:15:00,138.73,167.377,56.04600000000001,32.225 +2020-11-16 13:30:00,134.79,165.9,56.04600000000001,32.225 +2020-11-16 13:45:00,133.83,165.859,56.04600000000001,32.225 +2020-11-16 14:00:00,130.83,165.37900000000002,55.475,32.225 +2020-11-16 14:15:00,130.65,165.18099999999998,55.475,32.225 +2020-11-16 14:30:00,129.75,164.22,55.475,32.225 +2020-11-16 14:45:00,129.95,164.2,55.475,32.225 +2020-11-16 15:00:00,130.58,164.608,57.048,32.225 +2020-11-16 15:15:00,129.71,164.543,57.048,32.225 +2020-11-16 15:30:00,129.12,165.127,57.048,32.225 +2020-11-16 15:45:00,129.23,166.125,57.048,32.225 +2020-11-16 16:00:00,130.93,169.43,59.06,32.225 +2020-11-16 16:15:00,132.04,170.313,59.06,32.225 +2020-11-16 16:30:00,136.67,171.088,59.06,32.225 +2020-11-16 16:45:00,138.56,170.58900000000003,59.06,32.225 +2020-11-16 17:00:00,139.38,173.562,65.419,32.225 +2020-11-16 17:15:00,139.96,173.8,65.419,32.225 +2020-11-16 17:30:00,140.06,173.597,65.419,32.225 +2020-11-16 17:45:00,139.57,173.247,65.419,32.225 +2020-11-16 18:00:00,137.5,175.109,69.345,32.225 +2020-11-16 18:15:00,136.2,174.597,69.345,32.225 +2020-11-16 18:30:00,135.18,173.084,69.345,32.225 +2020-11-16 18:45:00,135.88,173.261,69.345,32.225 +2020-11-16 19:00:00,133.06,174.851,73.825,32.225 +2020-11-16 19:15:00,129.71,172.138,73.825,32.225 +2020-11-16 19:30:00,128.36,171.513,73.825,32.225 +2020-11-16 19:45:00,126.9,168.456,73.825,32.225 +2020-11-16 20:00:00,121.58,164.43200000000002,64.027,32.225 +2020-11-16 20:15:00,117.62,160.344,64.027,32.225 +2020-11-16 20:30:00,113.7,156.911,64.027,32.225 +2020-11-16 20:45:00,112.48,154.009,64.027,32.225 +2020-11-16 21:00:00,108.97,152.451,57.952,32.225 +2020-11-16 21:15:00,107.75,150.361,57.952,32.225 +2020-11-16 21:30:00,110.85,148.789,57.952,32.225 +2020-11-16 21:45:00,111.19,146.843,57.952,32.225 +2020-11-16 22:00:00,106.86,138.77200000000002,53.031000000000006,32.225 +2020-11-16 22:15:00,98.64,134.02100000000002,53.031000000000006,32.225 +2020-11-16 22:30:00,94.58,118.351,53.031000000000006,32.225 +2020-11-16 22:45:00,92.11,110.148,53.031000000000006,32.225 +2020-11-16 23:00:00,89.04,105.603,45.085,32.225 +2020-11-16 23:15:00,88.11,104.111,45.085,32.225 +2020-11-16 23:30:00,84.69,104.76700000000001,45.085,32.225 +2020-11-16 23:45:00,83.57,104.964,45.085,32.225 +2020-11-17 00:00:00,84.58,99.30799999999999,42.843,32.225 +2020-11-17 00:15:00,87.16,99.41,42.843,32.225 +2020-11-17 00:30:00,88.47,99.79700000000001,42.843,32.225 +2020-11-17 00:45:00,83.12,100.8,42.843,32.225 +2020-11-17 01:00:00,78.7,102.07799999999999,41.542,32.225 +2020-11-17 01:15:00,84.09,103.046,41.542,32.225 +2020-11-17 01:30:00,84.23,103.331,41.542,32.225 +2020-11-17 01:45:00,83.62,103.946,41.542,32.225 +2020-11-17 02:00:00,76.91,105.01700000000001,40.19,32.225 +2020-11-17 02:15:00,80.46,105.959,40.19,32.225 +2020-11-17 02:30:00,83.38,105.678,40.19,32.225 +2020-11-17 02:45:00,84.48,107.306,40.19,32.225 +2020-11-17 03:00:00,82.24,109.87200000000001,39.626,32.225 +2020-11-17 03:15:00,84.6,110.59299999999999,39.626,32.225 +2020-11-17 03:30:00,86.1,112.57600000000001,39.626,32.225 +2020-11-17 03:45:00,86.14,113.147,39.626,32.225 +2020-11-17 04:00:00,82.44,127.023,40.196999999999996,32.225 +2020-11-17 04:15:00,86.13,139.739,40.196999999999996,32.225 +2020-11-17 04:30:00,89.79,141.108,40.196999999999996,32.225 +2020-11-17 04:45:00,90.47,143.09799999999998,40.196999999999996,32.225 +2020-11-17 05:00:00,88.89,177.57299999999998,43.378,32.225 +2020-11-17 05:15:00,91.86,207.77200000000002,43.378,32.225 +2020-11-17 05:30:00,94.62,203.21400000000003,43.378,32.225 +2020-11-17 05:45:00,98.85,194.155,43.378,32.225 +2020-11-17 06:00:00,105.56,189.748,55.691,32.225 +2020-11-17 06:15:00,112.04,194.85299999999998,55.691,32.225 +2020-11-17 06:30:00,118.42,196.07299999999998,55.691,32.225 +2020-11-17 06:45:00,121.99,197.13400000000001,55.691,32.225 +2020-11-17 07:00:00,126.36,198.41299999999998,65.567,32.225 +2020-11-17 07:15:00,128.77,202.37,65.567,32.225 +2020-11-17 07:30:00,129.41,203.832,65.567,32.225 +2020-11-17 07:45:00,131.36,204.28900000000002,65.567,32.225 +2020-11-17 08:00:00,134.03,203.22099999999998,73.001,32.225 +2020-11-17 08:15:00,133.22,202.782,73.001,32.225 +2020-11-17 08:30:00,137.27,200.732,73.001,32.225 +2020-11-17 08:45:00,136.45,197.748,73.001,32.225 +2020-11-17 09:00:00,137.66,192.072,67.08800000000001,32.225 +2020-11-17 09:15:00,138.23,188.37599999999998,67.08800000000001,32.225 +2020-11-17 09:30:00,141.61,186.05599999999998,67.08800000000001,32.225 +2020-11-17 09:45:00,141.87,183.963,67.08800000000001,32.225 +2020-11-17 10:00:00,142.21,181.206,62.803000000000004,32.225 +2020-11-17 10:15:00,140.92,177.903,62.803000000000004,32.225 +2020-11-17 10:30:00,143.35,176.02200000000002,62.803000000000004,32.225 +2020-11-17 10:45:00,142.09,174.975,62.803000000000004,32.225 +2020-11-17 11:00:00,140.39,173.19299999999998,60.155,32.225 +2020-11-17 11:15:00,139.74,172.726,60.155,32.225 +2020-11-17 11:30:00,140.21,172.148,60.155,32.225 +2020-11-17 11:45:00,140.97,171.627,60.155,32.225 +2020-11-17 12:00:00,139.79,166.22099999999998,56.845,32.225 +2020-11-17 12:15:00,138.99,164.982,56.845,32.225 +2020-11-17 12:30:00,143.01,165.047,56.845,32.225 +2020-11-17 12:45:00,146.71,165.69400000000002,56.845,32.225 +2020-11-17 13:00:00,144.04,165.172,56.163000000000004,32.225 +2020-11-17 13:15:00,138.69,164.601,56.163000000000004,32.225 +2020-11-17 13:30:00,137.01,164.142,56.163000000000004,32.225 +2020-11-17 13:45:00,136.33,164.106,56.163000000000004,32.225 +2020-11-17 14:00:00,136.4,163.898,55.934,32.225 +2020-11-17 14:15:00,130.93,163.82,55.934,32.225 +2020-11-17 14:30:00,127.86,163.495,55.934,32.225 +2020-11-17 14:45:00,127.46,163.289,55.934,32.225 +2020-11-17 15:00:00,129.68,163.296,57.43899999999999,32.225 +2020-11-17 15:15:00,129.91,163.672,57.43899999999999,32.225 +2020-11-17 15:30:00,130.07,164.459,57.43899999999999,32.225 +2020-11-17 15:45:00,132.74,165.16299999999998,57.43899999999999,32.225 +2020-11-17 16:00:00,135.56,168.65099999999998,59.968999999999994,32.225 +2020-11-17 16:15:00,137.0,169.99099999999999,59.968999999999994,32.225 +2020-11-17 16:30:00,137.55,171.27900000000002,59.968999999999994,32.225 +2020-11-17 16:45:00,137.39,171.187,59.968999999999994,32.225 +2020-11-17 17:00:00,139.11,174.64,67.428,32.225 +2020-11-17 17:15:00,139.71,174.99,67.428,32.225 +2020-11-17 17:30:00,139.17,175.31,67.428,32.225 +2020-11-17 17:45:00,137.63,174.801,67.428,32.225 +2020-11-17 18:00:00,135.8,176.44,71.533,32.225 +2020-11-17 18:15:00,134.06,175.666,71.533,32.225 +2020-11-17 18:30:00,133.05,173.852,71.533,32.225 +2020-11-17 18:45:00,133.67,174.7,71.533,32.225 +2020-11-17 19:00:00,130.58,176.188,73.32300000000001,32.225 +2020-11-17 19:15:00,128.02,173.24200000000002,73.32300000000001,32.225 +2020-11-17 19:30:00,125.82,171.97799999999998,73.32300000000001,32.225 +2020-11-17 19:45:00,124.09,168.967,73.32300000000001,32.225 +2020-11-17 20:00:00,119.92,165.153,64.166,32.225 +2020-11-17 20:15:00,115.35,160.27200000000002,64.166,32.225 +2020-11-17 20:30:00,112.08,157.69899999999998,64.166,32.225 +2020-11-17 20:45:00,109.65,154.369,64.166,32.225 +2020-11-17 21:00:00,106.35,152.325,57.891999999999996,32.225 +2020-11-17 21:15:00,101.33,150.787,57.891999999999996,32.225 +2020-11-17 21:30:00,97.94,148.607,57.891999999999996,32.225 +2020-11-17 21:45:00,94.87,146.888,57.891999999999996,32.225 +2020-11-17 22:00:00,90.4,140.35,53.242,32.225 +2020-11-17 22:15:00,87.25,135.31,53.242,32.225 +2020-11-17 22:30:00,83.17,119.822,53.242,32.225 +2020-11-17 22:45:00,81.13,111.868,53.242,32.225 +2020-11-17 23:00:00,77.54,107.215,46.665,32.225 +2020-11-17 23:15:00,75.31,105.156,46.665,32.225 +2020-11-17 23:30:00,73.28,105.50200000000001,46.665,32.225 +2020-11-17 23:45:00,71.49,105.323,46.665,32.225 +2020-11-18 00:00:00,68.06,96.84,43.16,32.225 +2020-11-18 00:15:00,66.65,92.867,43.16,32.225 +2020-11-18 00:30:00,66.05,93.897,43.16,32.225 +2020-11-18 00:45:00,65.38,96.11,43.16,32.225 +2020-11-18 01:00:00,63.81,97.618,40.972,32.225 +2020-11-18 01:15:00,65.53,99.4,40.972,32.225 +2020-11-18 01:30:00,61.92,99.46,40.972,32.225 +2020-11-18 01:45:00,62.1,99.815,40.972,32.225 +2020-11-18 02:00:00,61.67,100.939,39.749,32.225 +2020-11-18 02:15:00,62.09,100.72399999999999,39.749,32.225 +2020-11-18 02:30:00,61.4,100.70299999999999,39.749,32.225 +2020-11-18 02:45:00,61.82,102.81200000000001,39.749,32.225 +2020-11-18 03:00:00,61.2,105.334,39.422,32.225 +2020-11-18 03:15:00,61.6,105.109,39.422,32.225 +2020-11-18 03:30:00,61.32,106.791,39.422,32.225 +2020-11-18 03:45:00,62.03,107.945,39.422,32.225 +2020-11-18 04:00:00,62.95,117.961,40.505,32.225 +2020-11-18 04:15:00,62.82,126.931,40.505,32.225 +2020-11-18 04:30:00,63.09,126.81700000000001,40.505,32.225 +2020-11-18 04:45:00,63.58,127.492,40.505,32.225 +2020-11-18 05:00:00,64.61,142.859,43.397,32.225 +2020-11-18 05:15:00,64.56,154.186,43.397,32.225 +2020-11-18 05:30:00,64.42,150.976,43.397,32.225 +2020-11-18 05:45:00,65.18,147.387,43.397,32.225 +2020-11-18 06:00:00,65.99,160.76,55.218,32.225 +2020-11-18 06:15:00,66.23,178.679,55.218,32.225 +2020-11-18 06:30:00,67.7,173.043,55.218,32.225 +2020-11-18 06:45:00,69.63,166.206,55.218,32.225 +2020-11-18 07:00:00,72.15,165.429,67.39,32.225 +2020-11-18 07:15:00,73.16,168.10299999999998,67.39,32.225 +2020-11-18 07:30:00,73.58,170.863,67.39,32.225 +2020-11-18 07:45:00,76.05,173.437,67.39,32.225 +2020-11-18 08:00:00,79.97,176.707,74.345,32.225 +2020-11-18 08:15:00,81.61,179.26,74.345,32.225 +2020-11-18 08:30:00,82.38,181.017,74.345,32.225 +2020-11-18 08:45:00,84.44,181.19799999999998,74.345,32.225 +2020-11-18 09:00:00,86.73,177.138,69.336,32.225 +2020-11-18 09:15:00,88.15,175.387,69.336,32.225 +2020-11-18 09:30:00,88.01,173.38099999999997,69.336,32.225 +2020-11-18 09:45:00,88.77,171.382,69.336,32.225 +2020-11-18 10:00:00,88.6,170.173,64.291,32.225 +2020-11-18 10:15:00,89.69,168.09,64.291,32.225 +2020-11-18 10:30:00,91.83,166.854,64.291,32.225 +2020-11-18 10:45:00,93.65,165.011,64.291,32.225 +2020-11-18 11:00:00,93.82,164.09099999999998,62.20399999999999,32.225 +2020-11-18 11:15:00,96.88,162.225,62.20399999999999,32.225 +2020-11-18 11:30:00,97.62,161.532,62.20399999999999,32.225 +2020-11-18 11:45:00,98.81,160.72899999999998,62.20399999999999,32.225 +2020-11-18 12:00:00,98.43,155.30100000000002,59.042,32.225 +2020-11-18 12:15:00,96.83,154.36,59.042,32.225 +2020-11-18 12:30:00,94.69,153.64600000000002,59.042,32.225 +2020-11-18 12:45:00,92.52,153.125,59.042,32.225 +2020-11-18 13:00:00,91.17,152.308,57.907,32.225 +2020-11-18 13:15:00,90.28,153.11700000000002,57.907,32.225 +2020-11-18 13:30:00,89.49,152.067,57.907,32.225 +2020-11-18 13:45:00,89.41,151.834,57.907,32.225 +2020-11-18 14:00:00,88.17,151.933,58.358000000000004,32.225 +2020-11-18 14:15:00,87.97,152.22299999999998,58.358000000000004,32.225 +2020-11-18 14:30:00,88.15,151.829,58.358000000000004,32.225 +2020-11-18 14:45:00,89.22,151.511,58.358000000000004,32.225 +2020-11-18 15:00:00,91.04,150.407,59.348,32.225 +2020-11-18 15:15:00,90.98,151.707,59.348,32.225 +2020-11-18 15:30:00,90.33,152.963,59.348,32.225 +2020-11-18 15:45:00,91.75,154.442,59.348,32.225 +2020-11-18 16:00:00,93.83,157.414,61.413999999999994,32.225 +2020-11-18 16:15:00,96.36,159.002,61.413999999999994,32.225 +2020-11-18 16:30:00,97.49,160.732,61.413999999999994,32.225 +2020-11-18 16:45:00,98.72,161.352,61.413999999999994,32.225 +2020-11-18 17:00:00,101.46,164.60299999999998,67.107,32.225 +2020-11-18 17:15:00,103.06,165.655,67.107,32.225 +2020-11-18 17:30:00,107.61,166.00599999999997,67.107,32.225 +2020-11-18 17:45:00,105.53,167.03400000000002,67.107,32.225 +2020-11-18 18:00:00,105.8,168.706,71.92,32.225 +2020-11-18 18:15:00,104.59,170.32,71.92,32.225 +2020-11-18 18:30:00,103.28,168.372,71.92,32.225 +2020-11-18 18:45:00,104.04,167.43,71.92,32.225 +2020-11-18 19:00:00,102.53,170.468,75.09,32.225 +2020-11-18 19:15:00,100.99,168.55,75.09,32.225 +2020-11-18 19:30:00,99.54,167.50900000000001,75.09,32.225 +2020-11-18 19:45:00,98.32,165.204,75.09,32.225 +2020-11-18 20:00:00,101.42,163.47799999999998,65.977,32.225 +2020-11-18 20:15:00,101.77,161.30200000000002,65.977,32.225 +2020-11-18 20:30:00,98.91,159.345,65.977,32.225 +2020-11-18 20:45:00,94.0,155.009,65.977,32.225 +2020-11-18 21:00:00,91.32,153.06799999999998,58.798,32.225 +2020-11-18 21:15:00,94.27,151.904,58.798,32.225 +2020-11-18 21:30:00,94.17,150.993,58.798,32.225 +2020-11-18 21:45:00,90.94,149.516,58.798,32.225 +2020-11-18 22:00:00,87.11,144.303,54.486000000000004,32.225 +2020-11-18 22:15:00,86.53,140.368,54.486000000000004,32.225 +2020-11-18 22:30:00,87.31,133.83,54.486000000000004,32.225 +2020-11-18 22:45:00,83.89,129.859,54.486000000000004,32.225 +2020-11-18 23:00:00,79.96,124.615,47.783,32.225 +2020-11-18 23:15:00,80.17,121.041,47.783,32.225 +2020-11-18 23:30:00,78.39,119.30799999999999,47.783,32.225 +2020-11-18 23:45:00,77.89,117.245,47.783,32.225 +2020-11-19 00:00:00,74.83,100.085,43.88,32.225 +2020-11-19 00:15:00,74.27,100.132,43.88,32.225 +2020-11-19 00:30:00,72.66,100.522,43.88,32.225 +2020-11-19 00:45:00,72.09,101.492,43.88,32.225 +2020-11-19 01:00:00,71.02,102.845,42.242,32.225 +2020-11-19 01:15:00,71.4,103.829,42.242,32.225 +2020-11-19 01:30:00,70.62,104.14,42.242,32.225 +2020-11-19 01:45:00,71.65,104.73,42.242,32.225 +2020-11-19 02:00:00,74.0,105.839,40.918,32.225 +2020-11-19 02:15:00,71.51,106.79,40.918,32.225 +2020-11-19 02:30:00,71.25,106.499,40.918,32.225 +2020-11-19 02:45:00,72.0,108.12299999999999,40.918,32.225 +2020-11-19 03:00:00,71.53,110.661,40.411,32.225 +2020-11-19 03:15:00,71.33,111.44,40.411,32.225 +2020-11-19 03:30:00,71.54,113.429,40.411,32.225 +2020-11-19 03:45:00,73.18,113.98200000000001,40.411,32.225 +2020-11-19 04:00:00,74.7,127.82600000000001,41.246,32.225 +2020-11-19 04:15:00,75.98,140.563,41.246,32.225 +2020-11-19 04:30:00,77.0,141.905,41.246,32.225 +2020-11-19 04:45:00,79.12,143.909,41.246,32.225 +2020-11-19 05:00:00,84.14,178.382,44.533,32.225 +2020-11-19 05:15:00,87.47,208.544,44.533,32.225 +2020-11-19 05:30:00,90.75,204.02200000000002,44.533,32.225 +2020-11-19 05:45:00,96.44,194.972,44.533,32.225 +2020-11-19 06:00:00,104.01,190.593,55.005,32.225 +2020-11-19 06:15:00,109.76,195.706,55.005,32.225 +2020-11-19 06:30:00,116.54,197.018,55.005,32.225 +2020-11-19 06:45:00,120.52,198.15200000000002,55.005,32.225 +2020-11-19 07:00:00,124.39,199.429,64.597,32.225 +2020-11-19 07:15:00,126.36,203.41299999999998,64.597,32.225 +2020-11-19 07:30:00,127.26,204.91,64.597,32.225 +2020-11-19 07:45:00,130.79,205.38299999999998,64.597,32.225 +2020-11-19 08:00:00,133.32,204.34599999999998,71.71600000000001,32.225 +2020-11-19 08:15:00,133.27,203.889,71.71600000000001,32.225 +2020-11-19 08:30:00,133.0,201.88400000000001,71.71600000000001,32.225 +2020-11-19 08:45:00,133.02,198.83599999999998,71.71600000000001,32.225 +2020-11-19 09:00:00,137.0,193.114,66.51899999999999,32.225 +2020-11-19 09:15:00,138.81,189.428,66.51899999999999,32.225 +2020-11-19 09:30:00,139.87,187.102,66.51899999999999,32.225 +2020-11-19 09:45:00,136.56,184.975,66.51899999999999,32.225 +2020-11-19 10:00:00,136.84,182.19400000000002,63.04,32.225 +2020-11-19 10:15:00,136.24,178.824,63.04,32.225 +2020-11-19 10:30:00,136.58,176.892,63.04,32.225 +2020-11-19 10:45:00,138.2,175.81799999999998,63.04,32.225 +2020-11-19 11:00:00,143.48,174.016,60.998000000000005,32.225 +2020-11-19 11:15:00,145.08,173.512,60.998000000000005,32.225 +2020-11-19 11:30:00,141.99,172.93099999999998,60.998000000000005,32.225 +2020-11-19 11:45:00,139.15,172.388,60.998000000000005,32.225 +2020-11-19 12:00:00,137.73,166.959,58.27,32.225 +2020-11-19 12:15:00,136.88,165.732,58.27,32.225 +2020-11-19 12:30:00,137.43,165.851,58.27,32.225 +2020-11-19 12:45:00,136.49,166.507,58.27,32.225 +2020-11-19 13:00:00,134.8,165.90599999999998,57.196000000000005,32.225 +2020-11-19 13:15:00,134.79,165.35,57.196000000000005,32.225 +2020-11-19 13:30:00,132.52,164.889,57.196000000000005,32.225 +2020-11-19 13:45:00,131.77,164.835,57.196000000000005,32.225 +2020-11-19 14:00:00,133.0,164.542,57.38399999999999,32.225 +2020-11-19 14:15:00,133.06,164.49,57.38399999999999,32.225 +2020-11-19 14:30:00,131.21,164.235,57.38399999999999,32.225 +2020-11-19 14:45:00,131.57,164.03900000000002,57.38399999999999,32.225 +2020-11-19 15:00:00,132.6,164.05700000000002,58.647,32.225 +2020-11-19 15:15:00,131.83,164.451,58.647,32.225 +2020-11-19 15:30:00,132.4,165.30900000000003,58.647,32.225 +2020-11-19 15:45:00,133.34,166.02900000000002,58.647,32.225 +2020-11-19 16:00:00,136.61,169.5,60.083999999999996,32.225 +2020-11-19 16:15:00,135.17,170.892,60.083999999999996,32.225 +2020-11-19 16:30:00,137.53,172.18599999999998,60.083999999999996,32.225 +2020-11-19 16:45:00,137.22,172.18200000000002,60.083999999999996,32.225 +2020-11-19 17:00:00,138.52,175.574,65.85600000000001,32.225 +2020-11-19 17:15:00,138.89,175.96900000000002,65.85600000000001,32.225 +2020-11-19 17:30:00,140.31,176.305,65.85600000000001,32.225 +2020-11-19 17:45:00,138.58,175.81,65.85600000000001,32.225 +2020-11-19 18:00:00,136.88,177.47400000000002,69.855,32.225 +2020-11-19 18:15:00,135.08,176.61,69.855,32.225 +2020-11-19 18:30:00,134.94,174.812,69.855,32.225 +2020-11-19 18:45:00,135.05,175.66299999999998,69.855,32.225 +2020-11-19 19:00:00,132.41,177.15200000000002,74.015,32.225 +2020-11-19 19:15:00,129.97,174.18200000000002,74.015,32.225 +2020-11-19 19:30:00,128.17,172.87900000000002,74.015,32.225 +2020-11-19 19:45:00,126.77,169.799,74.015,32.225 +2020-11-19 20:00:00,121.56,166.00799999999998,65.316,32.225 +2020-11-19 20:15:00,116.74,161.10299999999998,65.316,32.225 +2020-11-19 20:30:00,113.64,158.468,65.316,32.225 +2020-11-19 20:45:00,117.52,155.156,65.316,32.225 +2020-11-19 21:00:00,113.74,153.093,58.403999999999996,32.225 +2020-11-19 21:15:00,113.82,151.528,58.403999999999996,32.225 +2020-11-19 21:30:00,108.94,149.357,58.403999999999996,32.225 +2020-11-19 21:45:00,105.09,147.631,58.403999999999996,32.225 +2020-11-19 22:00:00,99.66,141.107,54.092,32.225 +2020-11-19 22:15:00,102.85,136.05100000000002,54.092,32.225 +2020-11-19 22:30:00,100.78,120.677,54.092,32.225 +2020-11-19 22:45:00,99.23,112.73700000000001,54.092,32.225 +2020-11-19 23:00:00,91.21,108.055,48.18600000000001,32.225 +2020-11-19 23:15:00,88.51,105.96700000000001,48.18600000000001,32.225 +2020-11-19 23:30:00,92.47,106.329,48.18600000000001,32.225 +2020-11-19 23:45:00,91.07,106.109,48.18600000000001,32.225 +2020-11-20 00:00:00,87.75,99.23,45.18899999999999,32.225 +2020-11-20 00:15:00,81.05,99.459,45.18899999999999,32.225 +2020-11-20 00:30:00,85.97,99.78200000000001,45.18899999999999,32.225 +2020-11-20 00:45:00,85.53,100.905,45.18899999999999,32.225 +2020-11-20 01:00:00,83.27,101.958,43.256,32.225 +2020-11-20 01:15:00,80.61,103.631,43.256,32.225 +2020-11-20 01:30:00,82.75,103.89,43.256,32.225 +2020-11-20 01:45:00,83.03,104.514,43.256,32.225 +2020-11-20 02:00:00,78.63,105.882,42.312,32.225 +2020-11-20 02:15:00,77.14,106.729,42.312,32.225 +2020-11-20 02:30:00,77.65,107.031,42.312,32.225 +2020-11-20 02:45:00,82.12,108.568,42.312,32.225 +2020-11-20 03:00:00,83.0,110.37899999999999,41.833,32.225 +2020-11-20 03:15:00,81.44,111.771,41.833,32.225 +2020-11-20 03:30:00,78.04,113.704,41.833,32.225 +2020-11-20 03:45:00,81.83,114.689,41.833,32.225 +2020-11-20 04:00:00,85.52,128.736,42.732,32.225 +2020-11-20 04:15:00,86.71,140.985,42.732,32.225 +2020-11-20 04:30:00,82.74,142.685,42.732,32.225 +2020-11-20 04:45:00,85.24,143.602,42.732,32.225 +2020-11-20 05:00:00,87.34,176.903,46.254,32.225 +2020-11-20 05:15:00,95.1,208.50799999999998,46.254,32.225 +2020-11-20 05:30:00,100.38,204.958,46.254,32.225 +2020-11-20 05:45:00,104.5,195.774,46.254,32.225 +2020-11-20 06:00:00,105.66,191.827,56.76,32.225 +2020-11-20 06:15:00,110.32,195.717,56.76,32.225 +2020-11-20 06:30:00,114.29,196.326,56.76,32.225 +2020-11-20 06:45:00,119.4,198.864,56.76,32.225 +2020-11-20 07:00:00,126.14,199.542,66.029,32.225 +2020-11-20 07:15:00,127.73,204.579,66.029,32.225 +2020-11-20 07:30:00,127.93,205.59099999999998,66.029,32.225 +2020-11-20 07:45:00,128.79,205.271,66.029,32.225 +2020-11-20 08:00:00,131.62,203.435,73.128,32.225 +2020-11-20 08:15:00,131.62,202.748,73.128,32.225 +2020-11-20 08:30:00,131.81,201.59400000000002,73.128,32.225 +2020-11-20 08:45:00,132.09,197.12599999999998,73.128,32.225 +2020-11-20 09:00:00,132.08,191.378,68.23100000000001,32.225 +2020-11-20 09:15:00,132.7,188.517,68.23100000000001,32.225 +2020-11-20 09:30:00,130.51,185.7,68.23100000000001,32.225 +2020-11-20 09:45:00,130.25,183.52700000000002,68.23100000000001,32.225 +2020-11-20 10:00:00,127.82,179.71400000000003,64.733,32.225 +2020-11-20 10:15:00,126.85,176.882,64.733,32.225 +2020-11-20 10:30:00,125.62,174.955,64.733,32.225 +2020-11-20 10:45:00,125.03,173.46599999999998,64.733,32.225 +2020-11-20 11:00:00,126.8,171.67,62.0,32.225 +2020-11-20 11:15:00,126.91,170.18599999999998,62.0,32.225 +2020-11-20 11:30:00,124.26,171.074,62.0,32.225 +2020-11-20 11:45:00,122.85,170.40400000000002,62.0,32.225 +2020-11-20 12:00:00,121.61,165.986,57.876999999999995,32.225 +2020-11-20 12:15:00,121.21,162.827,57.876999999999995,32.225 +2020-11-20 12:30:00,121.35,163.131,57.876999999999995,32.225 +2020-11-20 12:45:00,122.36,164.113,57.876999999999995,32.225 +2020-11-20 13:00:00,119.72,164.393,55.585,32.225 +2020-11-20 13:15:00,118.77,164.592,55.585,32.225 +2020-11-20 13:30:00,118.89,164.27200000000002,55.585,32.225 +2020-11-20 13:45:00,117.82,164.204,55.585,32.225 +2020-11-20 14:00:00,119.04,162.74,54.5,32.225 +2020-11-20 14:15:00,118.42,162.615,54.5,32.225 +2020-11-20 14:30:00,118.28,163.089,54.5,32.225 +2020-11-20 14:45:00,119.7,163.05,54.5,32.225 +2020-11-20 15:00:00,119.39,162.639,55.131,32.225 +2020-11-20 15:15:00,120.02,162.61700000000002,55.131,32.225 +2020-11-20 15:30:00,120.71,162.084,55.131,32.225 +2020-11-20 15:45:00,122.19,163.062,55.131,32.225 +2020-11-20 16:00:00,124.56,165.365,56.8,32.225 +2020-11-20 16:15:00,127.81,167.11599999999999,56.8,32.225 +2020-11-20 16:30:00,132.47,168.468,56.8,32.225 +2020-11-20 16:45:00,134.58,168.27900000000002,56.8,32.225 +2020-11-20 17:00:00,135.53,172.09799999999998,63.428999999999995,32.225 +2020-11-20 17:15:00,135.41,172.127,63.428999999999995,32.225 +2020-11-20 17:30:00,135.86,172.21599999999998,63.428999999999995,32.225 +2020-11-20 17:45:00,135.03,171.495,63.428999999999995,32.225 +2020-11-20 18:00:00,133.0,173.782,67.915,32.225 +2020-11-20 18:15:00,132.6,172.382,67.915,32.225 +2020-11-20 18:30:00,132.16,170.923,67.915,32.225 +2020-11-20 18:45:00,133.09,171.83599999999998,67.915,32.225 +2020-11-20 19:00:00,129.97,174.26,69.428,32.225 +2020-11-20 19:15:00,127.95,172.574,69.428,32.225 +2020-11-20 19:30:00,125.39,170.898,69.428,32.225 +2020-11-20 19:45:00,122.11,167.21900000000002,69.428,32.225 +2020-11-20 20:00:00,117.37,163.468,60.56100000000001,32.225 +2020-11-20 20:15:00,113.89,158.686,60.56100000000001,32.225 +2020-11-20 20:30:00,110.74,155.905,60.56100000000001,32.225 +2020-11-20 20:45:00,109.52,152.961,60.56100000000001,32.225 +2020-11-20 21:00:00,106.9,151.546,55.18600000000001,32.225 +2020-11-20 21:15:00,105.87,150.619,55.18600000000001,32.225 +2020-11-20 21:30:00,106.31,148.468,55.18600000000001,32.225 +2020-11-20 21:45:00,103.66,147.249,55.18600000000001,32.225 +2020-11-20 22:00:00,92.56,141.57,51.433,32.225 +2020-11-20 22:15:00,90.8,136.34799999999998,51.433,32.225 +2020-11-20 22:30:00,84.72,127.389,51.433,32.225 +2020-11-20 22:45:00,86.32,122.689,51.433,32.225 +2020-11-20 23:00:00,87.62,117.875,46.201,32.225 +2020-11-20 23:15:00,86.93,113.809,46.201,32.225 +2020-11-20 23:30:00,82.7,112.605,46.201,32.225 +2020-11-20 23:45:00,78.82,111.764,46.201,32.225 +2020-11-21 00:00:00,79.22,97.382,42.576,32.047 +2020-11-21 00:15:00,78.96,93.809,42.576,32.047 +2020-11-21 00:30:00,73.94,95.18799999999999,42.576,32.047 +2020-11-21 00:45:00,70.84,96.777,42.576,32.047 +2020-11-21 01:00:00,71.67,98.469,39.34,32.047 +2020-11-21 01:15:00,74.73,99.413,39.34,32.047 +2020-11-21 01:30:00,74.22,99.094,39.34,32.047 +2020-11-21 01:45:00,71.33,99.74799999999999,39.34,32.047 +2020-11-21 02:00:00,71.97,101.554,37.582,32.047 +2020-11-21 02:15:00,73.0,101.944,37.582,32.047 +2020-11-21 02:30:00,71.44,101.148,37.582,32.047 +2020-11-21 02:45:00,69.8,102.914,37.582,32.047 +2020-11-21 03:00:00,70.35,105.005,36.523,32.047 +2020-11-21 03:15:00,71.12,105.26100000000001,36.523,32.047 +2020-11-21 03:30:00,68.69,105.90799999999999,36.523,32.047 +2020-11-21 03:45:00,65.43,107.25299999999999,36.523,32.047 +2020-11-21 04:00:00,63.81,117.439,36.347,32.047 +2020-11-21 04:15:00,64.05,127.375,36.347,32.047 +2020-11-21 04:30:00,65.13,126.90899999999999,36.347,32.047 +2020-11-21 04:45:00,65.92,127.463,36.347,32.047 +2020-11-21 05:00:00,66.64,145.74,36.407,32.047 +2020-11-21 05:15:00,66.61,159.19,36.407,32.047 +2020-11-21 05:30:00,66.7,156.248,36.407,32.047 +2020-11-21 05:45:00,68.33,152.523,36.407,32.047 +2020-11-21 06:00:00,70.41,166.495,38.228,32.047 +2020-11-21 06:15:00,71.52,185.717,38.228,32.047 +2020-11-21 06:30:00,73.49,181.28099999999998,38.228,32.047 +2020-11-21 06:45:00,75.76,175.635,38.228,32.047 +2020-11-21 07:00:00,79.69,172.74200000000002,41.905,32.047 +2020-11-21 07:15:00,81.93,176.484,41.905,32.047 +2020-11-21 07:30:00,82.59,180.097,41.905,32.047 +2020-11-21 07:45:00,86.02,183.34099999999998,41.905,32.047 +2020-11-21 08:00:00,89.63,184.977,46.051,32.047 +2020-11-21 08:15:00,90.83,187.362,46.051,32.047 +2020-11-21 08:30:00,93.03,187.613,46.051,32.047 +2020-11-21 08:45:00,96.12,185.99200000000002,46.051,32.047 +2020-11-21 09:00:00,100.89,182.237,46.683,32.047 +2020-11-21 09:15:00,98.64,180.114,46.683,32.047 +2020-11-21 09:30:00,100.25,178.16,46.683,32.047 +2020-11-21 09:45:00,100.87,176.032,46.683,32.047 +2020-11-21 10:00:00,100.67,172.535,44.425,32.047 +2020-11-21 10:15:00,99.74,169.87400000000002,44.425,32.047 +2020-11-21 10:30:00,99.32,168.007,44.425,32.047 +2020-11-21 10:45:00,99.73,167.572,44.425,32.047 +2020-11-21 11:00:00,100.59,165.90200000000002,42.148999999999994,32.047 +2020-11-21 11:15:00,101.81,163.94400000000002,42.148999999999994,32.047 +2020-11-21 11:30:00,104.19,163.908,42.148999999999994,32.047 +2020-11-21 11:45:00,103.4,162.507,42.148999999999994,32.047 +2020-11-21 12:00:00,103.31,157.35299999999998,39.683,32.047 +2020-11-21 12:15:00,103.72,154.89600000000002,39.683,32.047 +2020-11-21 12:30:00,101.44,155.484,39.683,32.047 +2020-11-21 12:45:00,101.91,155.91899999999998,39.683,32.047 +2020-11-21 13:00:00,97.83,155.659,37.154,32.047 +2020-11-21 13:15:00,97.45,153.987,37.154,32.047 +2020-11-21 13:30:00,97.31,153.30200000000002,37.154,32.047 +2020-11-21 13:45:00,96.77,153.42,37.154,32.047 +2020-11-21 14:00:00,94.23,152.981,36.457,32.047 +2020-11-21 14:15:00,92.82,152.19899999999998,36.457,32.047 +2020-11-21 14:30:00,91.21,151.05100000000002,36.457,32.047 +2020-11-21 14:45:00,90.74,151.286,36.457,32.047 +2020-11-21 15:00:00,89.08,151.498,38.257,32.047 +2020-11-21 15:15:00,90.29,152.30200000000002,38.257,32.047 +2020-11-21 15:30:00,90.87,153.183,38.257,32.047 +2020-11-21 15:45:00,91.79,154.035,38.257,32.047 +2020-11-21 16:00:00,94.22,155.641,41.181000000000004,32.047 +2020-11-21 16:15:00,97.63,158.055,41.181000000000004,32.047 +2020-11-21 16:30:00,100.73,159.40200000000002,41.181000000000004,32.047 +2020-11-21 16:45:00,102.19,160.024,41.181000000000004,32.047 +2020-11-21 17:00:00,104.89,163.127,46.806000000000004,32.047 +2020-11-21 17:15:00,108.34,164.282,46.806000000000004,32.047 +2020-11-21 17:30:00,107.18,164.275,46.806000000000004,32.047 +2020-11-21 17:45:00,107.64,163.287,46.806000000000004,32.047 +2020-11-21 18:00:00,107.72,165.36599999999999,52.073,32.047 +2020-11-21 18:15:00,107.31,165.763,52.073,32.047 +2020-11-21 18:30:00,105.98,165.696,52.073,32.047 +2020-11-21 18:45:00,105.32,163.127,52.073,32.047 +2020-11-21 19:00:00,103.23,166.097,53.608000000000004,32.047 +2020-11-21 19:15:00,101.82,163.806,53.608000000000004,32.047 +2020-11-21 19:30:00,100.14,162.893,53.608000000000004,32.047 +2020-11-21 19:45:00,98.85,159.30200000000002,53.608000000000004,32.047 +2020-11-21 20:00:00,94.8,157.629,50.265,32.047 +2020-11-21 20:15:00,91.04,154.589,50.265,32.047 +2020-11-21 20:30:00,87.39,151.33,50.265,32.047 +2020-11-21 20:45:00,85.88,148.344,50.265,32.047 +2020-11-21 21:00:00,83.83,148.64700000000002,45.766000000000005,32.047 +2020-11-21 21:15:00,82.44,148.033,45.766000000000005,32.047 +2020-11-21 21:30:00,81.16,146.989,45.766000000000005,32.047 +2020-11-21 21:45:00,82.28,145.325,45.766000000000005,32.047 +2020-11-21 22:00:00,77.41,140.82399999999998,45.97,32.047 +2020-11-21 22:15:00,75.61,137.806,45.97,32.047 +2020-11-21 22:30:00,72.97,134.268,45.97,32.047 +2020-11-21 22:45:00,72.35,131.268,45.97,32.047 +2020-11-21 23:00:00,69.85,128.459,40.415,32.047 +2020-11-21 23:15:00,68.87,123.02,40.415,32.047 +2020-11-21 23:30:00,66.45,120.694,40.415,32.047 +2020-11-21 23:45:00,64.91,117.774,40.415,32.047 +2020-11-22 00:00:00,62.39,98.36399999999999,36.376,32.047 +2020-11-22 00:15:00,61.59,94.28299999999999,36.376,32.047 +2020-11-22 00:30:00,60.39,95.315,36.376,32.047 +2020-11-22 00:45:00,59.43,97.46,36.376,32.047 +2020-11-22 01:00:00,60.22,99.115,32.992,32.047 +2020-11-22 01:15:00,57.85,100.927,32.992,32.047 +2020-11-22 01:30:00,56.88,101.039,32.992,32.047 +2020-11-22 01:45:00,56.3,101.34299999999999,32.992,32.047 +2020-11-22 02:00:00,55.45,102.54299999999999,32.327,32.047 +2020-11-22 02:15:00,55.69,102.346,32.327,32.047 +2020-11-22 02:30:00,55.72,102.307,32.327,32.047 +2020-11-22 02:45:00,55.6,104.40899999999999,32.327,32.047 +2020-11-22 03:00:00,55.22,106.87299999999999,31.169,32.047 +2020-11-22 03:15:00,55.28,106.764,31.169,32.047 +2020-11-22 03:30:00,55.5,108.458,31.169,32.047 +2020-11-22 03:45:00,55.87,109.579,31.169,32.047 +2020-11-22 04:00:00,55.68,119.53,30.796,32.047 +2020-11-22 04:15:00,56.25,128.539,30.796,32.047 +2020-11-22 04:30:00,56.7,128.373,30.796,32.047 +2020-11-22 04:45:00,57.4,129.07399999999998,30.796,32.047 +2020-11-22 05:00:00,57.89,144.434,30.848000000000003,32.047 +2020-11-22 05:15:00,58.33,155.689,30.848000000000003,32.047 +2020-11-22 05:30:00,58.4,152.545,30.848000000000003,32.047 +2020-11-22 05:45:00,58.91,148.976,30.848000000000003,32.047 +2020-11-22 06:00:00,60.42,162.407,31.166,32.047 +2020-11-22 06:15:00,61.02,180.342,31.166,32.047 +2020-11-22 06:30:00,61.68,174.887,31.166,32.047 +2020-11-22 06:45:00,63.19,168.197,31.166,32.047 +2020-11-22 07:00:00,66.58,167.417,33.527,32.047 +2020-11-22 07:15:00,67.92,170.138,33.527,32.047 +2020-11-22 07:30:00,68.4,172.968,33.527,32.047 +2020-11-22 07:45:00,70.27,175.56599999999997,33.527,32.047 +2020-11-22 08:00:00,73.27,178.896,36.616,32.047 +2020-11-22 08:15:00,74.74,181.408,36.616,32.047 +2020-11-22 08:30:00,76.33,183.252,36.616,32.047 +2020-11-22 08:45:00,76.44,183.30700000000002,36.616,32.047 +2020-11-22 09:00:00,77.09,179.15400000000002,37.857,32.047 +2020-11-22 09:15:00,78.76,177.422,37.857,32.047 +2020-11-22 09:30:00,79.57,175.412,37.857,32.047 +2020-11-22 09:45:00,79.12,173.343,37.857,32.047 +2020-11-22 10:00:00,78.1,172.08700000000002,36.319,32.047 +2020-11-22 10:15:00,80.56,169.87400000000002,36.319,32.047 +2020-11-22 10:30:00,83.15,168.53900000000002,36.319,32.047 +2020-11-22 10:45:00,84.7,166.643,36.319,32.047 +2020-11-22 11:00:00,85.59,165.68099999999998,37.236999999999995,32.047 +2020-11-22 11:15:00,87.48,163.744,37.236999999999995,32.047 +2020-11-22 11:30:00,89.9,163.045,37.236999999999995,32.047 +2020-11-22 11:45:00,94.79,162.2,37.236999999999995,32.047 +2020-11-22 12:00:00,90.94,156.726,34.871,32.047 +2020-11-22 12:15:00,89.01,155.812,34.871,32.047 +2020-11-22 12:30:00,86.85,155.20600000000002,34.871,32.047 +2020-11-22 12:45:00,85.53,154.701,34.871,32.047 +2020-11-22 13:00:00,81.64,153.731,29.738000000000003,32.047 +2020-11-22 13:15:00,78.73,154.57,29.738000000000003,32.047 +2020-11-22 13:30:00,78.52,153.511,29.738000000000003,32.047 +2020-11-22 13:45:00,76.68,153.241,29.738000000000003,32.047 +2020-11-22 14:00:00,76.68,153.178,27.333000000000002,32.047 +2020-11-22 14:15:00,75.93,153.518,27.333000000000002,32.047 +2020-11-22 14:30:00,75.96,153.26,27.333000000000002,32.047 +2020-11-22 14:45:00,75.98,152.963,27.333000000000002,32.047 +2020-11-22 15:00:00,80.48,151.885,28.232,32.047 +2020-11-22 15:15:00,77.4,153.216,28.232,32.047 +2020-11-22 15:30:00,79.06,154.611,28.232,32.047 +2020-11-22 15:45:00,81.3,156.118,28.232,32.047 +2020-11-22 16:00:00,84.0,159.056,32.815,32.047 +2020-11-22 16:15:00,86.71,160.746,32.815,32.047 +2020-11-22 16:30:00,91.42,162.487,32.815,32.047 +2020-11-22 16:45:00,94.2,163.279,32.815,32.047 +2020-11-22 17:00:00,97.01,166.412,43.068999999999996,32.047 +2020-11-22 17:15:00,99.13,167.55599999999998,43.068999999999996,32.047 +2020-11-22 17:30:00,106.82,167.94099999999997,43.068999999999996,32.047 +2020-11-22 17:45:00,108.43,168.998,43.068999999999996,32.047 +2020-11-22 18:00:00,106.98,170.72,50.498999999999995,32.047 +2020-11-22 18:15:00,102.7,172.16400000000002,50.498999999999995,32.047 +2020-11-22 18:30:00,103.87,170.24900000000002,50.498999999999995,32.047 +2020-11-22 18:45:00,101.15,169.312,50.498999999999995,32.047 +2020-11-22 19:00:00,101.38,172.34799999999998,53.481,32.047 +2020-11-22 19:15:00,97.91,170.38400000000001,53.481,32.047 +2020-11-22 19:30:00,97.08,169.269,53.481,32.047 +2020-11-22 19:45:00,94.87,166.83,53.481,32.047 +2020-11-22 20:00:00,100.26,165.14700000000002,51.687,32.047 +2020-11-22 20:15:00,100.16,162.92600000000002,51.687,32.047 +2020-11-22 20:30:00,97.79,160.846,51.687,32.047 +2020-11-22 20:45:00,89.12,156.549,51.687,32.047 +2020-11-22 21:00:00,89.33,154.567,47.674,32.047 +2020-11-22 21:15:00,90.04,153.346,47.674,32.047 +2020-11-22 21:30:00,87.6,152.453,47.674,32.047 +2020-11-22 21:45:00,87.83,150.965,47.674,32.047 +2020-11-22 22:00:00,86.54,145.78,48.178000000000004,32.047 +2020-11-22 22:15:00,87.22,141.814,48.178000000000004,32.047 +2020-11-22 22:30:00,85.35,135.501,48.178000000000004,32.047 +2020-11-22 22:45:00,86.7,131.559,48.178000000000004,32.047 +2020-11-22 23:00:00,87.2,126.255,42.553999999999995,32.047 +2020-11-22 23:15:00,88.4,122.626,42.553999999999995,32.047 +2020-11-22 23:30:00,84.48,120.92200000000001,42.553999999999995,32.047 +2020-11-22 23:45:00,81.66,118.785,42.553999999999995,32.047 +2020-11-23 00:00:00,81.01,102.585,37.177,32.225 +2020-11-23 00:15:00,80.79,101.152,37.177,32.225 +2020-11-23 00:30:00,79.11,102.22200000000001,37.177,32.225 +2020-11-23 00:45:00,74.45,103.821,37.177,32.225 +2020-11-23 01:00:00,77.84,105.58,35.358000000000004,32.225 +2020-11-23 01:15:00,77.83,106.954,35.358000000000004,32.225 +2020-11-23 01:30:00,77.76,107.191,35.358000000000004,32.225 +2020-11-23 01:45:00,72.18,107.56299999999999,35.358000000000004,32.225 +2020-11-23 02:00:00,77.53,108.836,35.03,32.225 +2020-11-23 02:15:00,77.99,109.693,35.03,32.225 +2020-11-23 02:30:00,77.7,109.962,35.03,32.225 +2020-11-23 02:45:00,71.51,111.509,35.03,32.225 +2020-11-23 03:00:00,77.86,115.11200000000001,34.394,32.225 +2020-11-23 03:15:00,77.98,116.559,34.394,32.225 +2020-11-23 03:30:00,79.9,118.154,34.394,32.225 +2020-11-23 03:45:00,76.4,118.714,34.394,32.225 +2020-11-23 04:00:00,75.65,132.888,34.421,32.225 +2020-11-23 04:15:00,75.21,145.955,34.421,32.225 +2020-11-23 04:30:00,80.58,147.53,34.421,32.225 +2020-11-23 04:45:00,86.22,148.444,34.421,32.225 +2020-11-23 05:00:00,89.91,178.38099999999997,39.435,32.225 +2020-11-23 05:15:00,93.08,208.532,39.435,32.225 +2020-11-23 05:30:00,91.38,205.338,39.435,32.225 +2020-11-23 05:45:00,101.04,196.43599999999998,39.435,32.225 +2020-11-23 06:00:00,111.88,192.929,55.685,32.225 +2020-11-23 06:15:00,119.66,196.643,55.685,32.225 +2020-11-23 06:30:00,117.56,198.74,55.685,32.225 +2020-11-23 06:45:00,126.52,200.50099999999998,55.685,32.225 +2020-11-23 07:00:00,127.0,201.90599999999998,66.837,32.225 +2020-11-23 07:15:00,130.33,206.11599999999999,66.837,32.225 +2020-11-23 07:30:00,133.16,208.145,66.837,32.225 +2020-11-23 07:45:00,134.79,208.628,66.837,32.225 +2020-11-23 08:00:00,135.1,207.56599999999997,72.217,32.225 +2020-11-23 08:15:00,134.95,208.025,72.217,32.225 +2020-11-23 08:30:00,135.26,206.227,72.217,32.225 +2020-11-23 08:45:00,135.33,203.44,72.217,32.225 +2020-11-23 09:00:00,137.24,198.29,66.117,32.225 +2020-11-23 09:15:00,141.14,193.26,66.117,32.225 +2020-11-23 09:30:00,142.69,190.23,66.117,32.225 +2020-11-23 09:45:00,142.86,188.003,66.117,32.225 +2020-11-23 10:00:00,143.04,185.90900000000002,62.1,32.225 +2020-11-23 10:15:00,143.04,183.359,62.1,32.225 +2020-11-23 10:30:00,143.62,181.167,62.1,32.225 +2020-11-23 10:45:00,139.51,179.63299999999998,62.1,32.225 +2020-11-23 11:00:00,144.08,176.495,60.021,32.225 +2020-11-23 11:15:00,159.0,176.148,60.021,32.225 +2020-11-23 11:30:00,160.47,176.75799999999998,60.021,32.225 +2020-11-23 11:45:00,160.25,175.63299999999998,60.021,32.225 +2020-11-23 12:00:00,147.32,171.385,56.75899999999999,32.225 +2020-11-23 12:15:00,145.08,170.497,56.75899999999999,32.225 +2020-11-23 12:30:00,148.09,169.957,56.75899999999999,32.225 +2020-11-23 12:45:00,145.6,170.778,56.75899999999999,32.225 +2020-11-23 13:00:00,143.46,170.442,56.04600000000001,32.225 +2020-11-23 13:15:00,142.41,169.93900000000002,56.04600000000001,32.225 +2020-11-23 13:30:00,140.37,168.447,56.04600000000001,32.225 +2020-11-23 13:45:00,135.08,168.343,56.04600000000001,32.225 +2020-11-23 14:00:00,135.61,167.577,55.475,32.225 +2020-11-23 14:15:00,135.86,167.467,55.475,32.225 +2020-11-23 14:30:00,135.91,166.745,55.475,32.225 +2020-11-23 14:45:00,136.54,166.763,55.475,32.225 +2020-11-23 15:00:00,136.12,167.21400000000003,57.048,32.225 +2020-11-23 15:15:00,135.68,167.205,57.048,32.225 +2020-11-23 15:30:00,135.08,168.033,57.048,32.225 +2020-11-23 15:45:00,135.81,169.081,57.048,32.225 +2020-11-23 16:00:00,136.9,172.327,59.06,32.225 +2020-11-23 16:15:00,137.68,173.388,59.06,32.225 +2020-11-23 16:30:00,139.13,174.18400000000003,59.06,32.225 +2020-11-23 16:45:00,141.15,173.987,59.06,32.225 +2020-11-23 17:00:00,155.46,176.75400000000002,65.419,32.225 +2020-11-23 17:15:00,157.73,177.153,65.419,32.225 +2020-11-23 17:30:00,158.42,177.005,65.419,32.225 +2020-11-23 17:45:00,145.53,176.706,65.419,32.225 +2020-11-23 18:00:00,137.98,178.655,69.345,32.225 +2020-11-23 18:15:00,139.34,177.84,69.345,32.225 +2020-11-23 18:30:00,138.47,176.386,69.345,32.225 +2020-11-23 18:45:00,139.68,176.57299999999998,69.345,32.225 +2020-11-23 19:00:00,137.89,178.16,73.825,32.225 +2020-11-23 19:15:00,134.19,175.365,73.825,32.225 +2020-11-23 19:30:00,131.82,174.611,73.825,32.225 +2020-11-23 19:45:00,129.59,171.31599999999997,73.825,32.225 +2020-11-23 20:00:00,125.6,167.36900000000003,64.027,32.225 +2020-11-23 20:15:00,119.28,163.202,64.027,32.225 +2020-11-23 20:30:00,122.12,159.554,64.027,32.225 +2020-11-23 20:45:00,121.81,156.717,64.027,32.225 +2020-11-23 21:00:00,118.81,155.09,57.952,32.225 +2020-11-23 21:15:00,109.73,152.90200000000002,57.952,32.225 +2020-11-23 21:30:00,108.05,151.36,57.952,32.225 +2020-11-23 21:45:00,106.57,149.395,57.952,32.225 +2020-11-23 22:00:00,105.12,141.372,53.031000000000006,32.225 +2020-11-23 22:15:00,106.96,136.566,53.031000000000006,32.225 +2020-11-23 22:30:00,104.93,121.29,53.031000000000006,32.225 +2020-11-23 22:45:00,101.2,113.13799999999999,53.031000000000006,32.225 +2020-11-23 23:00:00,92.0,108.491,45.085,32.225 +2020-11-23 23:15:00,94.85,106.9,45.085,32.225 +2020-11-23 23:30:00,94.19,107.60799999999999,45.085,32.225 +2020-11-23 23:45:00,91.56,107.67299999999999,45.085,32.225 +2020-11-24 00:00:00,86.66,101.96,42.843,32.225 +2020-11-24 00:15:00,81.99,101.87299999999999,42.843,32.225 +2020-11-24 00:30:00,80.81,102.262,42.843,32.225 +2020-11-24 00:45:00,83.16,103.147,42.843,32.225 +2020-11-24 01:00:00,86.21,104.68,41.542,32.225 +2020-11-24 01:15:00,86.93,105.70100000000001,41.542,32.225 +2020-11-24 01:30:00,83.03,106.073,41.542,32.225 +2020-11-24 01:45:00,80.91,106.603,41.542,32.225 +2020-11-24 02:00:00,80.28,107.804,40.19,32.225 +2020-11-24 02:15:00,85.04,108.777,40.19,32.225 +2020-11-24 02:30:00,80.94,108.46600000000001,40.19,32.225 +2020-11-24 02:45:00,83.11,110.08,40.19,32.225 +2020-11-24 03:00:00,82.33,112.54700000000001,39.626,32.225 +2020-11-24 03:15:00,81.11,113.471,39.626,32.225 +2020-11-24 03:30:00,80.6,115.473,39.626,32.225 +2020-11-24 03:45:00,81.65,115.988,39.626,32.225 +2020-11-24 04:00:00,89.16,129.751,40.196999999999996,32.225 +2020-11-24 04:15:00,89.98,142.534,40.196999999999996,32.225 +2020-11-24 04:30:00,91.48,143.811,40.196999999999996,32.225 +2020-11-24 04:45:00,88.32,145.847,40.196999999999996,32.225 +2020-11-24 05:00:00,95.04,180.31,43.378,32.225 +2020-11-24 05:15:00,100.05,210.38299999999998,43.378,32.225 +2020-11-24 05:30:00,105.49,205.94099999999997,43.378,32.225 +2020-11-24 05:45:00,104.35,196.915,43.378,32.225 +2020-11-24 06:00:00,111.75,192.609,55.691,32.225 +2020-11-24 06:15:00,115.57,197.745,55.691,32.225 +2020-11-24 06:30:00,120.7,199.278,55.691,32.225 +2020-11-24 06:45:00,125.63,200.59400000000002,55.691,32.225 +2020-11-24 07:00:00,131.53,201.87,65.567,32.225 +2020-11-24 07:15:00,134.38,205.908,65.567,32.225 +2020-11-24 07:30:00,135.59,207.489,65.567,32.225 +2020-11-24 07:45:00,136.05,207.987,65.567,32.225 +2020-11-24 08:00:00,133.21,207.02200000000002,73.001,32.225 +2020-11-24 08:15:00,132.47,206.514,73.001,32.225 +2020-11-24 08:30:00,131.85,204.61,73.001,32.225 +2020-11-24 08:45:00,132.0,201.40599999999998,73.001,32.225 +2020-11-24 09:00:00,133.22,195.56900000000002,67.08800000000001,32.225 +2020-11-24 09:15:00,134.13,191.907,67.08800000000001,32.225 +2020-11-24 09:30:00,136.8,189.577,67.08800000000001,32.225 +2020-11-24 09:45:00,138.03,187.364,67.08800000000001,32.225 +2020-11-24 10:00:00,143.53,184.52599999999998,62.803000000000004,32.225 +2020-11-24 10:15:00,143.26,181.0,62.803000000000004,32.225 +2020-11-24 10:30:00,144.11,178.94400000000002,62.803000000000004,32.225 +2020-11-24 10:45:00,142.0,177.80599999999998,62.803000000000004,32.225 +2020-11-24 11:00:00,145.32,175.952,60.155,32.225 +2020-11-24 11:15:00,159.14,175.36,60.155,32.225 +2020-11-24 11:30:00,159.74,174.77200000000002,60.155,32.225 +2020-11-24 11:45:00,158.94,174.179,60.155,32.225 +2020-11-24 12:00:00,144.91,168.69400000000002,56.845,32.225 +2020-11-24 12:15:00,143.79,167.502,56.845,32.225 +2020-11-24 12:30:00,144.9,167.753,56.845,32.225 +2020-11-24 12:45:00,148.31,168.429,56.845,32.225 +2020-11-24 13:00:00,143.29,167.641,56.163000000000004,32.225 +2020-11-24 13:15:00,142.71,167.11900000000003,56.163000000000004,32.225 +2020-11-24 13:30:00,138.34,166.644,56.163000000000004,32.225 +2020-11-24 13:45:00,137.62,166.546,56.163000000000004,32.225 +2020-11-24 14:00:00,137.28,166.058,55.934,32.225 +2020-11-24 14:15:00,136.5,166.065,55.934,32.225 +2020-11-24 14:30:00,136.48,165.97799999999998,55.934,32.225 +2020-11-24 14:45:00,135.39,165.81,55.934,32.225 +2020-11-24 15:00:00,134.87,165.862,57.43899999999999,32.225 +2020-11-24 15:15:00,134.85,166.291,57.43899999999999,32.225 +2020-11-24 15:30:00,135.23,167.31599999999997,57.43899999999999,32.225 +2020-11-24 15:45:00,134.55,168.06900000000002,57.43899999999999,32.225 +2020-11-24 16:00:00,136.4,171.49900000000002,59.968999999999994,32.225 +2020-11-24 16:15:00,141.88,173.015,59.968999999999994,32.225 +2020-11-24 16:30:00,145.09,174.326,59.968999999999994,32.225 +2020-11-24 16:45:00,146.27,174.53099999999998,59.968999999999994,32.225 +2020-11-24 17:00:00,147.1,177.77900000000002,67.428,32.225 +2020-11-24 17:15:00,160.92,178.29,67.428,32.225 +2020-11-24 17:30:00,161.04,178.67,67.428,32.225 +2020-11-24 17:45:00,161.02,178.213,67.428,32.225 +2020-11-24 18:00:00,147.32,179.94099999999997,71.533,32.225 +2020-11-24 18:15:00,138.99,178.87099999999998,71.533,32.225 +2020-11-24 18:30:00,137.9,177.114,71.533,32.225 +2020-11-24 18:45:00,141.41,177.975,71.533,32.225 +2020-11-24 19:00:00,139.01,179.456,73.32300000000001,32.225 +2020-11-24 19:15:00,136.46,176.43,73.32300000000001,32.225 +2020-11-24 19:30:00,133.33,175.03799999999998,73.32300000000001,32.225 +2020-11-24 19:45:00,132.31,171.794,73.32300000000001,32.225 +2020-11-24 20:00:00,128.54,168.054,64.166,32.225 +2020-11-24 20:15:00,122.92,163.094,64.166,32.225 +2020-11-24 20:30:00,122.74,160.308,64.166,32.225 +2020-11-24 20:45:00,124.43,157.046,64.166,32.225 +2020-11-24 21:00:00,120.21,154.929,57.891999999999996,32.225 +2020-11-24 21:15:00,116.54,153.293,57.891999999999996,32.225 +2020-11-24 21:30:00,111.79,151.145,57.891999999999996,32.225 +2020-11-24 21:45:00,110.12,149.408,57.891999999999996,32.225 +2020-11-24 22:00:00,104.81,142.917,53.242,32.225 +2020-11-24 22:15:00,106.93,137.826,53.242,32.225 +2020-11-24 22:30:00,109.19,122.727,53.242,32.225 +2020-11-24 22:45:00,106.58,114.824,53.242,32.225 +2020-11-24 23:00:00,99.59,110.068,46.665,32.225 +2020-11-24 23:15:00,93.79,107.912,46.665,32.225 +2020-11-24 23:30:00,97.04,108.311,46.665,32.225 +2020-11-24 23:45:00,95.69,108.001,46.665,32.225 +2020-11-25 00:00:00,94.25,102.323,43.16,32.225 +2020-11-25 00:15:00,89.05,102.209,43.16,32.225 +2020-11-25 00:30:00,88.28,102.59700000000001,43.16,32.225 +2020-11-25 00:45:00,90.4,103.465,43.16,32.225 +2020-11-25 01:00:00,88.67,105.03299999999999,40.972,32.225 +2020-11-25 01:15:00,83.25,106.059,40.972,32.225 +2020-11-25 01:30:00,82.33,106.444,40.972,32.225 +2020-11-25 01:45:00,79.08,106.96,40.972,32.225 +2020-11-25 02:00:00,81.52,108.182,39.749,32.225 +2020-11-25 02:15:00,83.98,109.15799999999999,39.749,32.225 +2020-11-25 02:30:00,87.31,108.84299999999999,39.749,32.225 +2020-11-25 02:45:00,88.71,110.45700000000001,39.749,32.225 +2020-11-25 03:00:00,85.46,112.91,39.422,32.225 +2020-11-25 03:15:00,84.79,113.861,39.422,32.225 +2020-11-25 03:30:00,89.21,115.866,39.422,32.225 +2020-11-25 03:45:00,90.74,116.37200000000001,39.422,32.225 +2020-11-25 04:00:00,92.44,130.12,40.505,32.225 +2020-11-25 04:15:00,89.59,142.911,40.505,32.225 +2020-11-25 04:30:00,92.56,144.178,40.505,32.225 +2020-11-25 04:45:00,96.12,146.219,40.505,32.225 +2020-11-25 05:00:00,97.39,180.678,43.397,32.225 +2020-11-25 05:15:00,101.36,210.732,43.397,32.225 +2020-11-25 05:30:00,106.35,206.30599999999998,43.397,32.225 +2020-11-25 05:45:00,111.83,197.285,43.397,32.225 +2020-11-25 06:00:00,112.45,192.995,55.218,32.225 +2020-11-25 06:15:00,119.29,198.136,55.218,32.225 +2020-11-25 06:30:00,122.62,199.71200000000002,55.218,32.225 +2020-11-25 06:45:00,127.32,201.063,55.218,32.225 +2020-11-25 07:00:00,129.42,202.33900000000003,67.39,32.225 +2020-11-25 07:15:00,133.53,206.388,67.39,32.225 +2020-11-25 07:30:00,133.46,207.983,67.39,32.225 +2020-11-25 07:45:00,133.22,208.485,67.39,32.225 +2020-11-25 08:00:00,134.72,207.533,74.345,32.225 +2020-11-25 08:15:00,134.93,207.014,74.345,32.225 +2020-11-25 08:30:00,133.18,205.128,74.345,32.225 +2020-11-25 08:45:00,136.06,201.892,74.345,32.225 +2020-11-25 09:00:00,136.12,196.03400000000002,69.336,32.225 +2020-11-25 09:15:00,136.89,192.37599999999998,69.336,32.225 +2020-11-25 09:30:00,137.83,190.047,69.336,32.225 +2020-11-25 09:45:00,136.21,187.817,69.336,32.225 +2020-11-25 10:00:00,134.1,184.968,64.291,32.225 +2020-11-25 10:15:00,134.21,181.41099999999997,64.291,32.225 +2020-11-25 10:30:00,132.81,179.333,64.291,32.225 +2020-11-25 10:45:00,132.27,178.18200000000002,64.291,32.225 +2020-11-25 11:00:00,130.7,176.317,62.20399999999999,32.225 +2020-11-25 11:15:00,130.65,175.709,62.20399999999999,32.225 +2020-11-25 11:30:00,129.63,175.11900000000003,62.20399999999999,32.225 +2020-11-25 11:45:00,128.59,174.517,62.20399999999999,32.225 +2020-11-25 12:00:00,127.98,169.023,59.042,32.225 +2020-11-25 12:15:00,127.46,167.83700000000002,59.042,32.225 +2020-11-25 12:30:00,129.41,168.113,59.042,32.225 +2020-11-25 12:45:00,127.0,168.794,59.042,32.225 +2020-11-25 13:00:00,126.51,167.97,57.907,32.225 +2020-11-25 13:15:00,126.11,167.454,57.907,32.225 +2020-11-25 13:30:00,126.17,166.975,57.907,32.225 +2020-11-25 13:45:00,126.16,166.868,57.907,32.225 +2020-11-25 14:00:00,128.27,166.345,58.358000000000004,32.225 +2020-11-25 14:15:00,129.25,166.362,58.358000000000004,32.225 +2020-11-25 14:30:00,129.64,166.30700000000002,58.358000000000004,32.225 +2020-11-25 14:45:00,130.02,166.146,58.358000000000004,32.225 +2020-11-25 15:00:00,131.43,166.205,59.348,32.225 +2020-11-25 15:15:00,133.51,166.64,59.348,32.225 +2020-11-25 15:30:00,133.44,167.696,59.348,32.225 +2020-11-25 15:45:00,134.78,168.456,59.348,32.225 +2020-11-25 16:00:00,136.6,171.878,61.413999999999994,32.225 +2020-11-25 16:15:00,139.28,173.418,61.413999999999994,32.225 +2020-11-25 16:30:00,143.76,174.731,61.413999999999994,32.225 +2020-11-25 16:45:00,145.53,174.976,61.413999999999994,32.225 +2020-11-25 17:00:00,146.91,178.197,67.107,32.225 +2020-11-25 17:15:00,147.41,178.731,67.107,32.225 +2020-11-25 17:30:00,147.84,179.12099999999998,67.107,32.225 +2020-11-25 17:45:00,147.01,178.673,67.107,32.225 +2020-11-25 18:00:00,144.4,180.41400000000002,71.92,32.225 +2020-11-25 18:15:00,143.14,179.304,71.92,32.225 +2020-11-25 18:30:00,141.88,177.55700000000002,71.92,32.225 +2020-11-25 18:45:00,142.19,178.42,71.92,32.225 +2020-11-25 19:00:00,141.09,179.89700000000002,75.09,32.225 +2020-11-25 19:15:00,137.11,176.861,75.09,32.225 +2020-11-25 19:30:00,134.84,175.452,75.09,32.225 +2020-11-25 19:45:00,133.69,172.17700000000002,75.09,32.225 +2020-11-25 20:00:00,130.33,168.446,65.977,32.225 +2020-11-25 20:15:00,122.19,163.476,65.977,32.225 +2020-11-25 20:30:00,123.02,160.661,65.977,32.225 +2020-11-25 20:45:00,124.89,157.409,65.977,32.225 +2020-11-25 21:00:00,121.48,155.282,58.798,32.225 +2020-11-25 21:15:00,116.62,153.631,58.798,32.225 +2020-11-25 21:30:00,108.88,151.487,58.798,32.225 +2020-11-25 21:45:00,109.31,149.749,58.798,32.225 +2020-11-25 22:00:00,105.77,143.263,54.486000000000004,32.225 +2020-11-25 22:15:00,109.26,138.168,54.486000000000004,32.225 +2020-11-25 22:30:00,106.25,123.12200000000001,54.486000000000004,32.225 +2020-11-25 22:45:00,104.95,115.227,54.486000000000004,32.225 +2020-11-25 23:00:00,95.13,110.454,47.783,32.225 +2020-11-25 23:15:00,98.94,108.286,47.783,32.225 +2020-11-25 23:30:00,97.56,108.693,47.783,32.225 +2020-11-25 23:45:00,94.42,108.366,47.783,32.225 +2020-11-26 00:00:00,86.96,102.682,43.88,32.225 +2020-11-26 00:15:00,83.82,102.54,43.88,32.225 +2020-11-26 00:30:00,85.5,102.928,43.88,32.225 +2020-11-26 00:45:00,88.72,103.77799999999999,43.88,32.225 +2020-11-26 01:00:00,88.89,105.38,42.242,32.225 +2020-11-26 01:15:00,85.06,106.413,42.242,32.225 +2020-11-26 01:30:00,83.57,106.809,42.242,32.225 +2020-11-26 01:45:00,85.67,107.31299999999999,42.242,32.225 +2020-11-26 02:00:00,86.12,108.552,40.918,32.225 +2020-11-26 02:15:00,83.43,109.53299999999999,40.918,32.225 +2020-11-26 02:30:00,83.05,109.21600000000001,40.918,32.225 +2020-11-26 02:45:00,85.0,110.82700000000001,40.918,32.225 +2020-11-26 03:00:00,85.05,113.26700000000001,40.411,32.225 +2020-11-26 03:15:00,83.54,114.24600000000001,40.411,32.225 +2020-11-26 03:30:00,84.59,116.25299999999999,40.411,32.225 +2020-11-26 03:45:00,88.11,116.75299999999999,40.411,32.225 +2020-11-26 04:00:00,89.54,130.483,41.246,32.225 +2020-11-26 04:15:00,86.6,143.284,41.246,32.225 +2020-11-26 04:30:00,88.88,144.537,41.246,32.225 +2020-11-26 04:45:00,93.86,146.585,41.246,32.225 +2020-11-26 05:00:00,97.17,181.04,44.533,32.225 +2020-11-26 05:15:00,98.12,211.076,44.533,32.225 +2020-11-26 05:30:00,99.16,206.66400000000002,44.533,32.225 +2020-11-26 05:45:00,105.0,197.649,44.533,32.225 +2020-11-26 06:00:00,117.69,193.375,55.005,32.225 +2020-11-26 06:15:00,124.77,198.52,55.005,32.225 +2020-11-26 06:30:00,126.33,200.138,55.005,32.225 +2020-11-26 06:45:00,125.77,201.525,55.005,32.225 +2020-11-26 07:00:00,130.95,202.803,64.597,32.225 +2020-11-26 07:15:00,134.46,206.86,64.597,32.225 +2020-11-26 07:30:00,133.78,208.47,64.597,32.225 +2020-11-26 07:45:00,135.55,208.97400000000002,64.597,32.225 +2020-11-26 08:00:00,136.97,208.035,71.71600000000001,32.225 +2020-11-26 08:15:00,136.62,207.50400000000002,71.71600000000001,32.225 +2020-11-26 08:30:00,135.96,205.636,71.71600000000001,32.225 +2020-11-26 08:45:00,134.99,202.37,71.71600000000001,32.225 +2020-11-26 09:00:00,134.73,196.488,66.51899999999999,32.225 +2020-11-26 09:15:00,134.01,192.83599999999998,66.51899999999999,32.225 +2020-11-26 09:30:00,134.51,190.507,66.51899999999999,32.225 +2020-11-26 09:45:00,133.76,188.261,66.51899999999999,32.225 +2020-11-26 10:00:00,133.11,185.40099999999998,63.04,32.225 +2020-11-26 10:15:00,133.63,181.81599999999997,63.04,32.225 +2020-11-26 10:30:00,132.09,179.71400000000003,63.04,32.225 +2020-11-26 10:45:00,131.91,178.551,63.04,32.225 +2020-11-26 11:00:00,130.31,176.675,60.998000000000005,32.225 +2020-11-26 11:15:00,129.82,176.05,60.998000000000005,32.225 +2020-11-26 11:30:00,129.38,175.459,60.998000000000005,32.225 +2020-11-26 11:45:00,129.19,174.84900000000002,60.998000000000005,32.225 +2020-11-26 12:00:00,129.43,169.345,58.27,32.225 +2020-11-26 12:15:00,129.92,168.167,58.27,32.225 +2020-11-26 12:30:00,130.77,168.467,58.27,32.225 +2020-11-26 12:45:00,128.99,169.15200000000002,58.27,32.225 +2020-11-26 13:00:00,127.51,168.292,57.196000000000005,32.225 +2020-11-26 13:15:00,127.43,167.783,57.196000000000005,32.225 +2020-11-26 13:30:00,128.14,167.3,57.196000000000005,32.225 +2020-11-26 13:45:00,128.96,167.18400000000003,57.196000000000005,32.225 +2020-11-26 14:00:00,131.54,166.625,57.38399999999999,32.225 +2020-11-26 14:15:00,132.48,166.65400000000002,57.38399999999999,32.225 +2020-11-26 14:30:00,133.07,166.63,57.38399999999999,32.225 +2020-11-26 14:45:00,134.6,166.475,57.38399999999999,32.225 +2020-11-26 15:00:00,135.57,166.542,58.647,32.225 +2020-11-26 15:15:00,134.79,166.982,58.647,32.225 +2020-11-26 15:30:00,133.95,168.06900000000002,58.647,32.225 +2020-11-26 15:45:00,134.77,168.833,58.647,32.225 +2020-11-26 16:00:00,135.09,172.248,60.083999999999996,32.225 +2020-11-26 16:15:00,139.08,173.813,60.083999999999996,32.225 +2020-11-26 16:30:00,142.52,175.12900000000002,60.083999999999996,32.225 +2020-11-26 16:45:00,144.11,175.41400000000002,60.083999999999996,32.225 +2020-11-26 17:00:00,145.11,178.605,65.85600000000001,32.225 +2020-11-26 17:15:00,145.39,179.16299999999998,65.85600000000001,32.225 +2020-11-26 17:30:00,144.99,179.56400000000002,65.85600000000001,32.225 +2020-11-26 17:45:00,144.73,179.125,65.85600000000001,32.225 +2020-11-26 18:00:00,142.36,180.87900000000002,69.855,32.225 +2020-11-26 18:15:00,139.12,179.732,69.855,32.225 +2020-11-26 18:30:00,138.76,177.99200000000002,69.855,32.225 +2020-11-26 18:45:00,139.25,178.859,69.855,32.225 +2020-11-26 19:00:00,138.59,180.332,74.015,32.225 +2020-11-26 19:15:00,135.27,177.285,74.015,32.225 +2020-11-26 19:30:00,133.83,175.861,74.015,32.225 +2020-11-26 19:45:00,132.18,172.55599999999998,74.015,32.225 +2020-11-26 20:00:00,127.04,168.832,65.316,32.225 +2020-11-26 20:15:00,124.0,163.852,65.316,32.225 +2020-11-26 20:30:00,120.42,161.009,65.316,32.225 +2020-11-26 20:45:00,117.9,157.766,65.316,32.225 +2020-11-26 21:00:00,116.75,155.628,58.403999999999996,32.225 +2020-11-26 21:15:00,114.04,153.963,58.403999999999996,32.225 +2020-11-26 21:30:00,110.09,151.82299999999998,58.403999999999996,32.225 +2020-11-26 21:45:00,106.75,150.084,58.403999999999996,32.225 +2020-11-26 22:00:00,103.22,143.605,54.092,32.225 +2020-11-26 22:15:00,99.89,138.503,54.092,32.225 +2020-11-26 22:30:00,96.72,123.51100000000001,54.092,32.225 +2020-11-26 22:45:00,95.07,115.624,54.092,32.225 +2020-11-26 23:00:00,93.68,110.834,48.18600000000001,32.225 +2020-11-26 23:15:00,90.11,108.655,48.18600000000001,32.225 +2020-11-26 23:30:00,88.8,109.069,48.18600000000001,32.225 +2020-11-26 23:45:00,89.14,108.726,48.18600000000001,32.225 +2020-11-27 00:00:00,84.43,101.79899999999999,45.18899999999999,32.225 +2020-11-27 00:15:00,89.72,101.839,45.18899999999999,32.225 +2020-11-27 00:30:00,89.24,102.156,45.18899999999999,32.225 +2020-11-27 00:45:00,87.86,103.16,45.18899999999999,32.225 +2020-11-27 01:00:00,81.04,104.459,43.256,32.225 +2020-11-27 01:15:00,79.47,106.179,43.256,32.225 +2020-11-27 01:30:00,83.22,106.521,43.256,32.225 +2020-11-27 01:45:00,85.75,107.06,43.256,32.225 +2020-11-27 02:00:00,86.44,108.556,42.312,32.225 +2020-11-27 02:15:00,83.54,109.431,42.312,32.225 +2020-11-27 02:30:00,79.41,109.71,42.312,32.225 +2020-11-27 02:45:00,79.4,111.234,42.312,32.225 +2020-11-27 03:00:00,80.77,112.949,41.833,32.225 +2020-11-27 03:15:00,88.63,114.539,41.833,32.225 +2020-11-27 03:30:00,90.12,116.492,41.833,32.225 +2020-11-27 03:45:00,88.67,117.42200000000001,41.833,32.225 +2020-11-27 04:00:00,85.04,131.356,42.732,32.225 +2020-11-27 04:15:00,84.49,143.668,42.732,32.225 +2020-11-27 04:30:00,91.48,145.282,42.732,32.225 +2020-11-27 04:45:00,95.37,146.24,42.732,32.225 +2020-11-27 05:00:00,98.6,179.519,46.254,32.225 +2020-11-27 05:15:00,98.74,211.0,46.254,32.225 +2020-11-27 05:30:00,104.82,207.55599999999998,46.254,32.225 +2020-11-27 05:45:00,107.0,198.40900000000002,46.254,32.225 +2020-11-27 06:00:00,109.72,194.56900000000002,56.76,32.225 +2020-11-27 06:15:00,114.28,198.49099999999999,56.76,32.225 +2020-11-27 06:30:00,121.1,199.40200000000002,56.76,32.225 +2020-11-27 06:45:00,125.73,202.19099999999997,56.76,32.225 +2020-11-27 07:00:00,132.85,202.873,66.029,32.225 +2020-11-27 07:15:00,137.07,207.98,66.029,32.225 +2020-11-27 07:30:00,138.18,209.1,66.029,32.225 +2020-11-27 07:45:00,139.13,208.80700000000002,66.029,32.225 +2020-11-27 08:00:00,140.02,207.06799999999998,73.128,32.225 +2020-11-27 08:15:00,141.53,206.30599999999998,73.128,32.225 +2020-11-27 08:30:00,141.95,205.28,73.128,32.225 +2020-11-27 08:45:00,141.74,200.595,73.128,32.225 +2020-11-27 09:00:00,143.12,194.68900000000002,68.23100000000001,32.225 +2020-11-27 09:15:00,144.97,191.862,68.23100000000001,32.225 +2020-11-27 09:30:00,146.38,189.045,68.23100000000001,32.225 +2020-11-27 09:45:00,148.07,186.753,68.23100000000001,32.225 +2020-11-27 10:00:00,148.43,182.864,64.733,32.225 +2020-11-27 10:15:00,149.35,179.82,64.733,32.225 +2020-11-27 10:30:00,148.75,177.725,64.733,32.225 +2020-11-27 10:45:00,149.72,176.15099999999998,64.733,32.225 +2020-11-27 11:00:00,148.49,174.278,62.0,32.225 +2020-11-27 11:15:00,148.43,172.675,62.0,32.225 +2020-11-27 11:30:00,148.91,173.55599999999998,62.0,32.225 +2020-11-27 11:45:00,148.96,172.81900000000002,62.0,32.225 +2020-11-27 12:00:00,148.12,168.327,57.876999999999995,32.225 +2020-11-27 12:15:00,148.75,165.21900000000002,57.876999999999995,32.225 +2020-11-27 12:30:00,151.8,165.7,57.876999999999995,32.225 +2020-11-27 12:45:00,148.18,166.71200000000002,57.876999999999995,32.225 +2020-11-27 13:00:00,147.61,166.737,55.585,32.225 +2020-11-27 13:15:00,145.8,166.979,55.585,32.225 +2020-11-27 13:30:00,143.06,166.637,55.585,32.225 +2020-11-27 13:45:00,142.49,166.505,55.585,32.225 +2020-11-27 14:00:00,140.86,164.783,54.5,32.225 +2020-11-27 14:15:00,140.22,164.737,54.5,32.225 +2020-11-27 14:30:00,139.38,165.44,54.5,32.225 +2020-11-27 14:45:00,139.36,165.44400000000002,54.5,32.225 +2020-11-27 15:00:00,138.46,165.082,55.131,32.225 +2020-11-27 15:15:00,138.42,165.104,55.131,32.225 +2020-11-27 15:30:00,138.56,164.794,55.131,32.225 +2020-11-27 15:45:00,138.86,165.815,55.131,32.225 +2020-11-27 16:00:00,140.34,168.062,56.8,32.225 +2020-11-27 16:15:00,142.71,169.984,56.8,32.225 +2020-11-27 16:30:00,143.85,171.358,56.8,32.225 +2020-11-27 16:45:00,144.15,171.454,56.8,32.225 +2020-11-27 17:00:00,144.16,175.07299999999998,63.428999999999995,32.225 +2020-11-27 17:15:00,144.21,175.265,63.428999999999995,32.225 +2020-11-27 17:30:00,143.91,175.423,63.428999999999995,32.225 +2020-11-27 17:45:00,143.48,174.76,63.428999999999995,32.225 +2020-11-27 18:00:00,141.75,177.138,67.915,32.225 +2020-11-27 18:15:00,141.19,175.46099999999998,67.915,32.225 +2020-11-27 18:30:00,140.31,174.05900000000003,67.915,32.225 +2020-11-27 18:45:00,140.84,174.99200000000002,67.915,32.225 +2020-11-27 19:00:00,139.02,177.395,69.428,32.225 +2020-11-27 19:15:00,136.92,175.63299999999998,69.428,32.225 +2020-11-27 19:30:00,133.97,173.84,69.428,32.225 +2020-11-27 19:45:00,132.93,169.93900000000002,69.428,32.225 +2020-11-27 20:00:00,127.08,166.252,60.56100000000001,32.225 +2020-11-27 20:15:00,123.22,161.39700000000002,60.56100000000001,32.225 +2020-11-27 20:30:00,120.97,158.409,60.56100000000001,32.225 +2020-11-27 20:45:00,121.98,155.536,60.56100000000001,32.225 +2020-11-27 21:00:00,120.28,154.04399999999998,55.18600000000001,32.225 +2020-11-27 21:15:00,115.06,153.016,55.18600000000001,32.225 +2020-11-27 21:30:00,106.27,150.89700000000002,55.18600000000001,32.225 +2020-11-27 21:45:00,104.92,149.668,55.18600000000001,32.225 +2020-11-27 22:00:00,98.21,144.031,51.433,32.225 +2020-11-27 22:15:00,95.41,138.768,51.433,32.225 +2020-11-27 22:30:00,92.29,130.185,51.433,32.225 +2020-11-27 22:45:00,89.5,125.54,51.433,32.225 +2020-11-27 23:00:00,88.76,120.616,46.201,32.225 +2020-11-27 23:15:00,90.76,116.462,46.201,32.225 +2020-11-27 23:30:00,88.25,115.31200000000001,46.201,32.225 +2020-11-27 23:45:00,84.02,114.34899999999999,46.201,32.225 +2020-11-28 00:00:00,77.33,99.921,42.576,32.047 +2020-11-28 00:15:00,75.71,96.16,42.576,32.047 +2020-11-28 00:30:00,79.64,97.53200000000001,42.576,32.047 +2020-11-28 00:45:00,79.58,99.0,42.576,32.047 +2020-11-28 01:00:00,78.45,100.935,39.34,32.047 +2020-11-28 01:15:00,69.57,101.92299999999999,39.34,32.047 +2020-11-28 01:30:00,71.73,101.686,39.34,32.047 +2020-11-28 01:45:00,75.49,102.255,39.34,32.047 +2020-11-28 02:00:00,75.22,104.189,37.582,32.047 +2020-11-28 02:15:00,73.99,104.60799999999999,37.582,32.047 +2020-11-28 02:30:00,67.25,103.791,37.582,32.047 +2020-11-28 02:45:00,71.5,105.54299999999999,37.582,32.047 +2020-11-28 03:00:00,74.56,107.538,36.523,32.047 +2020-11-28 03:15:00,74.51,107.991,36.523,32.047 +2020-11-28 03:30:00,72.04,108.655,36.523,32.047 +2020-11-28 03:45:00,70.18,109.949,36.523,32.047 +2020-11-28 04:00:00,74.75,120.022,36.347,32.047 +2020-11-28 04:15:00,70.48,130.019,36.347,32.047 +2020-11-28 04:30:00,69.82,129.468,36.347,32.047 +2020-11-28 04:45:00,68.36,130.064,36.347,32.047 +2020-11-28 05:00:00,68.09,148.316,36.407,32.047 +2020-11-28 05:15:00,67.78,161.641,36.407,32.047 +2020-11-28 05:30:00,68.17,158.80200000000002,36.407,32.047 +2020-11-28 05:45:00,70.47,155.115,36.407,32.047 +2020-11-28 06:00:00,72.18,169.19400000000002,38.228,32.047 +2020-11-28 06:15:00,75.04,188.45,38.228,32.047 +2020-11-28 06:30:00,76.95,184.31099999999998,38.228,32.047 +2020-11-28 06:45:00,78.95,178.916,38.228,32.047 +2020-11-28 07:00:00,81.61,176.02900000000002,41.905,32.047 +2020-11-28 07:15:00,85.49,179.83700000000002,41.905,32.047 +2020-11-28 07:30:00,86.76,183.553,41.905,32.047 +2020-11-28 07:45:00,89.52,186.821,41.905,32.047 +2020-11-28 08:00:00,92.6,188.55,46.051,32.047 +2020-11-28 08:15:00,93.47,190.86,46.051,32.047 +2020-11-28 08:30:00,94.42,191.234,46.051,32.047 +2020-11-28 08:45:00,96.07,189.398,46.051,32.047 +2020-11-28 09:00:00,97.99,185.484,46.683,32.047 +2020-11-28 09:15:00,98.87,183.395,46.683,32.047 +2020-11-28 09:30:00,100.11,181.44400000000002,46.683,32.047 +2020-11-28 09:45:00,100.75,179.19799999999998,46.683,32.047 +2020-11-28 10:00:00,100.87,175.627,44.425,32.047 +2020-11-28 10:15:00,100.95,172.757,44.425,32.047 +2020-11-28 10:30:00,101.61,170.725,44.425,32.047 +2020-11-28 10:45:00,102.02,170.206,44.425,32.047 +2020-11-28 11:00:00,102.71,168.458,42.148999999999994,32.047 +2020-11-28 11:15:00,102.34,166.38299999999998,42.148999999999994,32.047 +2020-11-28 11:30:00,102.7,166.33900000000003,42.148999999999994,32.047 +2020-11-28 11:45:00,102.53,164.873,42.148999999999994,32.047 +2020-11-28 12:00:00,101.89,159.649,39.683,32.047 +2020-11-28 12:15:00,99.63,157.246,39.683,32.047 +2020-11-28 12:30:00,98.83,158.006,39.683,32.047 +2020-11-28 12:45:00,95.47,158.469,39.683,32.047 +2020-11-28 13:00:00,91.62,157.96,37.154,32.047 +2020-11-28 13:15:00,89.78,156.328,37.154,32.047 +2020-11-28 13:30:00,88.89,155.621,37.154,32.047 +2020-11-28 13:45:00,89.56,155.675,37.154,32.047 +2020-11-28 14:00:00,88.24,154.984,36.457,32.047 +2020-11-28 14:15:00,89.2,154.278,36.457,32.047 +2020-11-28 14:30:00,90.17,153.356,36.457,32.047 +2020-11-28 14:45:00,90.93,153.635,36.457,32.047 +2020-11-28 15:00:00,91.29,153.899,38.257,32.047 +2020-11-28 15:15:00,91.79,154.743,38.257,32.047 +2020-11-28 15:30:00,92.89,155.843,38.257,32.047 +2020-11-28 15:45:00,94.51,156.736,38.257,32.047 +2020-11-28 16:00:00,96.87,158.286,41.181000000000004,32.047 +2020-11-28 16:15:00,100.02,160.869,41.181000000000004,32.047 +2020-11-28 16:30:00,104.43,162.238,41.181000000000004,32.047 +2020-11-28 16:45:00,105.58,163.142,41.181000000000004,32.047 +2020-11-28 17:00:00,107.55,166.046,46.806000000000004,32.047 +2020-11-28 17:15:00,108.65,167.36599999999999,46.806000000000004,32.047 +2020-11-28 17:30:00,109.6,167.429,46.806000000000004,32.047 +2020-11-28 17:45:00,109.96,166.5,46.806000000000004,32.047 +2020-11-28 18:00:00,110.46,168.673,52.073,32.047 +2020-11-28 18:15:00,109.84,168.799,52.073,32.047 +2020-11-28 18:30:00,109.58,168.78900000000002,52.073,32.047 +2020-11-28 18:45:00,109.05,166.24200000000002,52.073,32.047 +2020-11-28 19:00:00,106.97,169.187,53.608000000000004,32.047 +2020-11-28 19:15:00,105.88,166.82299999999998,53.608000000000004,32.047 +2020-11-28 19:30:00,104.51,165.793,53.608000000000004,32.047 +2020-11-28 19:45:00,103.77,161.985,53.608000000000004,32.047 +2020-11-28 20:00:00,99.85,160.374,50.265,32.047 +2020-11-28 20:15:00,95.69,157.26,50.265,32.047 +2020-11-28 20:30:00,93.1,153.798,50.265,32.047 +2020-11-28 20:45:00,91.53,150.88299999999998,50.265,32.047 +2020-11-28 21:00:00,89.7,151.108,45.766000000000005,32.047 +2020-11-28 21:15:00,87.06,150.393,45.766000000000005,32.047 +2020-11-28 21:30:00,85.08,149.381,45.766000000000005,32.047 +2020-11-28 21:45:00,84.12,147.709,45.766000000000005,32.047 +2020-11-28 22:00:00,82.85,143.25,45.97,32.047 +2020-11-28 22:15:00,80.31,140.19299999999998,45.97,32.047 +2020-11-28 22:30:00,77.52,137.02700000000002,45.97,32.047 +2020-11-28 22:45:00,75.91,134.08100000000002,45.97,32.047 +2020-11-28 23:00:00,73.58,131.162,40.415,32.047 +2020-11-28 23:15:00,72.14,125.635,40.415,32.047 +2020-11-28 23:30:00,69.33,123.365,40.415,32.047 +2020-11-28 23:45:00,66.87,120.325,40.415,32.047 +2020-11-29 00:00:00,64.94,100.87299999999999,36.376,32.047 +2020-11-29 00:15:00,62.98,96.604,36.376,32.047 +2020-11-29 00:30:00,61.95,97.626,36.376,32.047 +2020-11-29 00:45:00,60.96,99.65100000000001,36.376,32.047 +2020-11-29 01:00:00,59.8,101.545,32.992,32.047 +2020-11-29 01:15:00,59.26,103.4,32.992,32.047 +2020-11-29 01:30:00,58.37,103.59,32.992,32.047 +2020-11-29 01:45:00,57.51,103.811,32.992,32.047 +2020-11-29 02:00:00,57.27,105.13799999999999,32.327,32.047 +2020-11-29 02:15:00,56.22,104.96799999999999,32.327,32.047 +2020-11-29 02:30:00,55.73,104.90899999999999,32.327,32.047 +2020-11-29 02:45:00,55.26,106.99799999999999,32.327,32.047 +2020-11-29 03:00:00,54.75,109.369,31.169,32.047 +2020-11-29 03:15:00,55.27,109.456,31.169,32.047 +2020-11-29 03:30:00,55.26,111.166,31.169,32.047 +2020-11-29 03:45:00,55.47,112.23700000000001,31.169,32.047 +2020-11-29 04:00:00,56.47,122.075,30.796,32.047 +2020-11-29 04:15:00,56.6,131.143,30.796,32.047 +2020-11-29 04:30:00,56.82,130.894,30.796,32.047 +2020-11-29 04:45:00,57.66,131.634,30.796,32.047 +2020-11-29 05:00:00,58.4,146.967,30.848000000000003,32.047 +2020-11-29 05:15:00,59.09,158.09799999999998,30.848000000000003,32.047 +2020-11-29 05:30:00,59.43,155.056,30.848000000000003,32.047 +2020-11-29 05:45:00,60.08,151.525,30.848000000000003,32.047 +2020-11-29 06:00:00,61.05,165.06400000000002,31.166,32.047 +2020-11-29 06:15:00,61.8,183.03400000000002,31.166,32.047 +2020-11-29 06:30:00,62.33,177.872,31.166,32.047 +2020-11-29 06:45:00,64.49,171.43099999999998,31.166,32.047 +2020-11-29 07:00:00,66.6,170.65900000000002,33.527,32.047 +2020-11-29 07:15:00,68.82,173.44400000000002,33.527,32.047 +2020-11-29 07:30:00,70.6,176.37099999999998,33.527,32.047 +2020-11-29 07:45:00,72.8,178.989,33.527,32.047 +2020-11-29 08:00:00,75.93,182.40900000000002,36.616,32.047 +2020-11-29 08:15:00,78.15,184.84400000000002,36.616,32.047 +2020-11-29 08:30:00,79.59,186.805,36.616,32.047 +2020-11-29 08:45:00,81.98,186.64700000000002,36.616,32.047 +2020-11-29 09:00:00,83.56,182.33700000000002,37.857,32.047 +2020-11-29 09:15:00,85.67,180.64,37.857,32.047 +2020-11-29 09:30:00,86.37,178.63299999999998,37.857,32.047 +2020-11-29 09:45:00,87.65,176.449,37.857,32.047 +2020-11-29 10:00:00,88.71,175.118,36.319,32.047 +2020-11-29 10:15:00,89.33,172.702,36.319,32.047 +2020-11-29 10:30:00,89.75,171.203,36.319,32.047 +2020-11-29 10:45:00,91.28,169.226,36.319,32.047 +2020-11-29 11:00:00,94.21,168.18400000000003,37.236999999999995,32.047 +2020-11-29 11:15:00,98.71,166.132,37.236999999999995,32.047 +2020-11-29 11:30:00,100.96,165.42700000000002,37.236999999999995,32.047 +2020-11-29 11:45:00,103.01,164.52,37.236999999999995,32.047 +2020-11-29 12:00:00,100.88,158.976,34.871,32.047 +2020-11-29 12:15:00,98.94,158.11700000000002,34.871,32.047 +2020-11-29 12:30:00,96.38,157.68,34.871,32.047 +2020-11-29 12:45:00,94.52,157.204,34.871,32.047 +2020-11-29 13:00:00,94.12,155.987,29.738000000000003,32.047 +2020-11-29 13:15:00,92.16,156.864,29.738000000000003,32.047 +2020-11-29 13:30:00,90.98,155.782,29.738000000000003,32.047 +2020-11-29 13:45:00,89.74,155.44899999999998,29.738000000000003,32.047 +2020-11-29 14:00:00,89.32,155.141,27.333000000000002,32.047 +2020-11-29 14:15:00,88.36,155.555,27.333000000000002,32.047 +2020-11-29 14:30:00,90.19,155.519,27.333000000000002,32.047 +2020-11-29 14:45:00,90.43,155.267,27.333000000000002,32.047 +2020-11-29 15:00:00,89.89,154.24200000000002,28.232,32.047 +2020-11-29 15:15:00,90.56,155.61,28.232,32.047 +2020-11-29 15:30:00,91.22,157.219,28.232,32.047 +2020-11-29 15:45:00,92.39,158.764,28.232,32.047 +2020-11-29 16:00:00,95.42,161.649,32.815,32.047 +2020-11-29 16:15:00,96.9,163.506,32.815,32.047 +2020-11-29 16:30:00,98.92,165.269,32.815,32.047 +2020-11-29 16:45:00,101.02,166.33900000000003,32.815,32.047 +2020-11-29 17:00:00,102.53,169.273,43.068999999999996,32.047 +2020-11-29 17:15:00,104.58,170.584,43.068999999999996,32.047 +2020-11-29 17:30:00,105.89,171.041,43.068999999999996,32.047 +2020-11-29 17:45:00,107.7,172.16,43.068999999999996,32.047 +2020-11-29 18:00:00,106.71,173.975,50.498999999999995,32.047 +2020-11-29 18:15:00,105.17,175.155,50.498999999999995,32.047 +2020-11-29 18:30:00,103.9,173.296,50.498999999999995,32.047 +2020-11-29 18:45:00,102.8,172.385,50.498999999999995,32.047 +2020-11-29 19:00:00,101.44,175.391,53.481,32.047 +2020-11-29 19:15:00,99.87,173.355,53.481,32.047 +2020-11-29 19:30:00,98.61,172.127,53.481,32.047 +2020-11-29 19:45:00,96.7,169.475,53.481,32.047 +2020-11-29 20:00:00,95.81,167.84900000000002,51.687,32.047 +2020-11-29 20:15:00,93.42,165.55900000000003,51.687,32.047 +2020-11-29 20:30:00,91.99,163.27700000000002,51.687,32.047 +2020-11-29 20:45:00,90.53,159.05200000000002,51.687,32.047 +2020-11-29 21:00:00,89.47,156.99200000000002,47.674,32.047 +2020-11-29 21:15:00,87.86,155.668,47.674,32.047 +2020-11-29 21:30:00,87.32,154.80700000000002,47.674,32.047 +2020-11-29 21:45:00,87.42,153.313,47.674,32.047 +2020-11-29 22:00:00,87.27,148.168,48.178000000000004,32.047 +2020-11-29 22:15:00,87.13,144.168,48.178000000000004,32.047 +2020-11-29 22:30:00,85.14,138.221,48.178000000000004,32.047 +2020-11-29 22:45:00,83.37,134.334,48.178000000000004,32.047 +2020-11-29 23:00:00,80.39,128.91899999999998,42.553999999999995,32.047 +2020-11-29 23:15:00,78.24,125.204,42.553999999999995,32.047 +2020-11-29 23:30:00,76.63,123.557,42.553999999999995,32.047 +2020-11-29 23:45:00,74.94,121.303,42.553999999999995,32.047 +2020-11-30 00:00:00,70.81,105.06200000000001,37.177,32.225 +2020-11-30 00:15:00,71.25,103.441,37.177,32.225 +2020-11-30 00:30:00,71.29,104.5,37.177,32.225 +2020-11-30 00:45:00,72.06,105.978,37.177,32.225 +2020-11-30 01:00:00,67.48,107.973,35.358000000000004,32.225 +2020-11-30 01:15:00,69.08,109.38799999999999,35.358000000000004,32.225 +2020-11-30 01:30:00,68.47,109.70200000000001,35.358000000000004,32.225 +2020-11-30 01:45:00,68.71,109.99,35.358000000000004,32.225 +2020-11-30 02:00:00,66.0,111.391,35.03,32.225 +2020-11-30 02:15:00,67.54,112.273,35.03,32.225 +2020-11-30 02:30:00,67.4,112.52600000000001,35.03,32.225 +2020-11-30 02:45:00,68.06,114.06,35.03,32.225 +2020-11-30 03:00:00,66.68,117.569,34.394,32.225 +2020-11-30 03:15:00,67.69,119.211,34.394,32.225 +2020-11-30 03:30:00,68.14,120.822,34.394,32.225 +2020-11-30 03:45:00,69.36,121.333,34.394,32.225 +2020-11-30 04:00:00,71.04,135.393,34.421,32.225 +2020-11-30 04:15:00,71.36,148.519,34.421,32.225 +2020-11-30 04:30:00,80.36,150.013,34.421,32.225 +2020-11-30 04:45:00,83.58,150.964,34.421,32.225 +2020-11-30 05:00:00,84.07,180.87099999999998,39.435,32.225 +2020-11-30 05:15:00,83.14,210.899,39.435,32.225 +2020-11-30 05:30:00,93.56,207.80200000000002,39.435,32.225 +2020-11-30 05:45:00,101.91,198.94,39.435,32.225 +2020-11-30 06:00:00,110.42,195.543,55.685,32.225 +2020-11-30 06:15:00,109.5,199.292,55.685,32.225 +2020-11-30 06:30:00,112.78,201.679,55.685,32.225 +2020-11-30 06:45:00,120.16,203.687,55.685,32.225 +2020-11-30 07:00:00,123.28,205.102,66.837,32.225 +2020-11-30 07:15:00,128.77,209.372,66.837,32.225 +2020-11-30 07:30:00,128.06,211.49400000000003,66.837,32.225 +2020-11-30 07:45:00,128.21,211.993,66.837,32.225 +2020-11-30 08:00:00,135.05,211.018,72.217,32.225 +2020-11-30 08:15:00,132.17,211.399,72.217,32.225 +2020-11-30 08:30:00,130.69,209.71200000000002,72.217,32.225 +2020-11-30 08:45:00,130.49,206.71200000000002,72.217,32.225 +2020-11-30 09:00:00,131.71,201.407,66.117,32.225 +2020-11-30 09:15:00,132.93,196.41099999999997,66.117,32.225 +2020-11-30 09:30:00,133.88,193.389,66.117,32.225 +2020-11-30 09:45:00,136.35,191.048,66.117,32.225 +2020-11-30 10:00:00,133.09,188.88,62.1,32.225 +2020-11-30 10:15:00,133.18,186.13,62.1,32.225 +2020-11-30 10:30:00,131.07,183.77599999999998,62.1,32.225 +2020-11-30 10:45:00,130.86,182.16299999999998,62.1,32.225 +2020-11-30 11:00:00,129.58,178.945,60.021,32.225 +2020-11-30 11:15:00,127.7,178.484,60.021,32.225 +2020-11-30 11:30:00,128.06,179.08900000000003,60.021,32.225 +2020-11-30 11:45:00,127.63,177.903,60.021,32.225 +2020-11-30 12:00:00,127.01,173.58900000000003,56.75899999999999,32.225 +2020-11-30 12:15:00,129.44,172.75599999999997,56.75899999999999,32.225 +2020-11-30 12:30:00,126.09,172.38299999999998,56.75899999999999,32.225 +2020-11-30 12:45:00,130.5,173.231,56.75899999999999,32.225 +2020-11-30 13:00:00,127.8,172.65200000000002,56.04600000000001,32.225 +2020-11-30 13:15:00,135.15,172.187,56.04600000000001,32.225 +2020-11-30 13:30:00,133.2,170.669,56.04600000000001,32.225 +2020-11-30 13:45:00,134.94,170.502,56.04600000000001,32.225 +2020-11-30 14:00:00,132.77,169.498,55.475,32.225 +2020-11-30 14:15:00,134.22,169.459,55.475,32.225 +2020-11-30 14:30:00,134.93,168.958,55.475,32.225 +2020-11-30 14:45:00,139.22,169.021,55.475,32.225 +2020-11-30 15:00:00,139.45,169.52599999999998,57.048,32.225 +2020-11-30 15:15:00,138.24,169.551,57.048,32.225 +2020-11-30 15:30:00,137.99,170.58900000000003,57.048,32.225 +2020-11-30 15:45:00,136.61,171.674,57.048,32.225 +2020-11-30 16:00:00,139.97,174.865,59.06,32.225 +2020-11-30 16:15:00,141.95,176.092,59.06,32.225 +2020-11-30 16:30:00,140.58,176.91,59.06,32.225 +2020-11-30 16:45:00,140.15,176.986,59.06,32.225 +2020-11-30 17:00:00,142.38,179.55700000000002,65.419,32.225 +2020-11-30 17:15:00,140.03,180.122,65.419,32.225 +2020-11-30 17:30:00,141.11,180.05200000000002,65.419,32.225 +2020-11-30 17:45:00,140.16,179.81400000000002,65.419,32.225 +2020-11-30 18:00:00,138.78,181.859,69.345,32.225 +2020-11-30 18:15:00,137.07,180.787,69.345,32.225 +2020-11-30 18:30:00,136.41,179.389,69.345,32.225 +2020-11-30 18:45:00,137.66,179.602,69.345,32.225 +2020-11-30 19:00:00,134.46,181.15599999999998,73.825,32.225 +2020-11-30 19:15:00,133.63,178.29,73.825,32.225 +2020-11-30 19:30:00,137.08,177.426,73.825,32.225 +2020-11-30 19:45:00,135.8,173.923,73.825,32.225 +2020-11-30 20:00:00,129.29,170.03,64.027,32.225 +2020-11-30 20:15:00,123.1,165.794,64.027,32.225 +2020-11-30 20:30:00,117.76,161.946,64.027,32.225 +2020-11-30 20:45:00,115.12,159.184,64.027,32.225 +2020-11-30 21:00:00,116.06,157.475,57.952,32.225 +2020-11-30 21:15:00,113.82,155.185,57.952,32.225 +2020-11-30 21:30:00,112.58,153.67600000000002,57.952,32.225 +2020-11-30 21:45:00,105.49,151.708,57.952,32.225 +2020-11-30 22:00:00,101.57,143.72299999999998,53.031000000000006,32.225 +2020-11-30 22:15:00,95.52,138.885,53.031000000000006,32.225 +2020-11-30 22:30:00,97.58,123.971,53.031000000000006,32.225 +2020-11-30 22:45:00,97.01,115.875,53.031000000000006,32.225 +2020-11-30 23:00:00,92.05,111.11399999999999,45.085,32.225 +2020-11-30 23:15:00,89.71,109.441,45.085,32.225 +2020-11-30 23:30:00,83.96,110.206,45.085,32.225 +2020-11-30 23:45:00,81.23,110.156,45.085,32.225 +2020-12-01 00:00:00,77.38,114.376,43.537,32.65 +2020-12-01 00:15:00,76.24,114.65799999999999,43.537,32.65 +2020-12-01 00:30:00,76.87,115.84100000000001,43.537,32.65 +2020-12-01 00:45:00,75.04,117.59700000000001,43.537,32.65 +2020-12-01 01:00:00,73.08,119.316,41.854,32.65 +2020-12-01 01:15:00,73.82,120.051,41.854,32.65 +2020-12-01 01:30:00,71.9,120.405,41.854,32.65 +2020-12-01 01:45:00,73.54,121.234,41.854,32.65 +2020-12-01 02:00:00,71.89,122.501,40.321,32.65 +2020-12-01 02:15:00,73.96,123.98899999999999,40.321,32.65 +2020-12-01 02:30:00,71.76,124.215,40.321,32.65 +2020-12-01 02:45:00,72.0,126.095,40.321,32.65 +2020-12-01 03:00:00,71.75,128.914,39.632,32.65 +2020-12-01 03:15:00,75.34,129.286,39.632,32.65 +2020-12-01 03:30:00,74.29,131.14,39.632,32.65 +2020-12-01 03:45:00,75.41,132.171,39.632,32.65 +2020-12-01 04:00:00,76.26,145.259,40.183,32.65 +2020-12-01 04:15:00,77.11,157.393,40.183,32.65 +2020-12-01 04:30:00,79.19,160.136,40.183,32.65 +2020-12-01 04:45:00,80.44,162.73,40.183,32.65 +2020-12-01 05:00:00,84.76,197.88,43.945,32.65 +2020-12-01 05:15:00,88.06,227.541,43.945,32.65 +2020-12-01 05:30:00,90.98,222.683,43.945,32.65 +2020-12-01 05:45:00,99.38,214.511,43.945,32.65 +2020-12-01 06:00:00,107.58,210.252,56.048,32.65 +2020-12-01 06:15:00,111.62,215.68,56.048,32.65 +2020-12-01 06:30:00,119.23,217.26,56.048,32.65 +2020-12-01 06:45:00,121.67,219.46099999999998,56.048,32.65 +2020-12-01 07:00:00,129.63,218.843,65.74,32.65 +2020-12-01 07:15:00,130.84,223.71400000000003,65.74,32.65 +2020-12-01 07:30:00,137.48,226.422,65.74,32.65 +2020-12-01 07:45:00,137.37,227.78900000000002,65.74,32.65 +2020-12-01 08:00:00,140.6,226.493,72.757,32.65 +2020-12-01 08:15:00,141.1,226.658,72.757,32.65 +2020-12-01 08:30:00,140.35,224.92700000000002,72.757,32.65 +2020-12-01 08:45:00,140.13,222.5,72.757,32.65 +2020-12-01 09:00:00,141.71,216.40599999999998,67.692,32.65 +2020-12-01 09:15:00,141.8,212.893,67.692,32.65 +2020-12-01 09:30:00,142.81,210.30700000000002,67.692,32.65 +2020-12-01 09:45:00,145.16,207.66299999999998,67.692,32.65 +2020-12-01 10:00:00,142.57,203.207,63.506,32.65 +2020-12-01 10:15:00,141.13,199.245,63.506,32.65 +2020-12-01 10:30:00,139.34,197.101,63.506,32.65 +2020-12-01 10:45:00,143.54,195.78,63.506,32.65 +2020-12-01 11:00:00,143.81,194.96099999999998,60.758,32.65 +2020-12-01 11:15:00,146.07,194.06400000000002,60.758,32.65 +2020-12-01 11:30:00,145.51,192.882,60.758,32.65 +2020-12-01 11:45:00,143.54,191.393,60.758,32.65 +2020-12-01 12:00:00,141.07,185.77599999999998,57.519,32.65 +2020-12-01 12:15:00,140.56,184.696,57.519,32.65 +2020-12-01 12:30:00,136.23,184.53,57.519,32.65 +2020-12-01 12:45:00,133.91,185.405,57.519,32.65 +2020-12-01 13:00:00,132.2,184.952,56.46,32.65 +2020-12-01 13:15:00,136.5,184.533,56.46,32.65 +2020-12-01 13:30:00,133.44,184.457,56.46,32.65 +2020-12-01 13:45:00,129.4,184.488,56.46,32.65 +2020-12-01 14:00:00,127.85,183.696,56.207,32.65 +2020-12-01 14:15:00,132.64,184.13400000000001,56.207,32.65 +2020-12-01 14:30:00,135.84,184.32299999999998,56.207,32.65 +2020-12-01 14:45:00,135.32,184.10299999999998,56.207,32.65 +2020-12-01 15:00:00,135.69,184.78400000000002,57.391999999999996,32.65 +2020-12-01 15:15:00,135.61,185.36599999999999,57.391999999999996,32.65 +2020-12-01 15:30:00,133.27,187.30200000000002,57.391999999999996,32.65 +2020-12-01 15:45:00,133.91,189.111,57.391999999999996,32.65 +2020-12-01 16:00:00,137.4,190.207,59.955,32.65 +2020-12-01 16:15:00,139.6,191.11599999999999,59.955,32.65 +2020-12-01 16:30:00,142.04,193.775,59.955,32.65 +2020-12-01 16:45:00,141.31,194.75400000000002,59.955,32.65 +2020-12-01 17:00:00,144.35,197.62,67.063,32.65 +2020-12-01 17:15:00,144.05,197.739,67.063,32.65 +2020-12-01 17:30:00,146.6,198.13,67.063,32.65 +2020-12-01 17:45:00,145.09,197.708,67.063,32.65 +2020-12-01 18:00:00,144.03,198.524,71.477,32.65 +2020-12-01 18:15:00,142.72,196.93200000000002,71.477,32.65 +2020-12-01 18:30:00,142.42,195.355,71.477,32.65 +2020-12-01 18:45:00,141.42,195.035,71.477,32.65 +2020-12-01 19:00:00,139.2,196.077,74.32,32.65 +2020-12-01 19:15:00,138.58,192.56,74.32,32.65 +2020-12-01 19:30:00,136.59,190.588,74.32,32.65 +2020-12-01 19:45:00,136.62,187.38299999999998,74.32,32.65 +2020-12-01 20:00:00,127.68,183.91299999999998,66.157,32.65 +2020-12-01 20:15:00,124.64,178.15099999999998,66.157,32.65 +2020-12-01 20:30:00,120.77,174.831,66.157,32.65 +2020-12-01 20:45:00,119.59,171.986,66.157,32.65 +2020-12-01 21:00:00,113.2,170.00900000000001,59.806000000000004,32.65 +2020-12-01 21:15:00,113.24,168.122,59.806000000000004,32.65 +2020-12-01 21:30:00,115.21,166.013,59.806000000000004,32.65 +2020-12-01 21:45:00,113.56,164.245,59.806000000000004,32.65 +2020-12-01 22:00:00,109.11,157.439,54.785,32.65 +2020-12-01 22:15:00,104.46,151.769,54.785,32.65 +2020-12-01 22:30:00,97.9,136.998,54.785,32.65 +2020-12-01 22:45:00,101.22,128.881,54.785,32.65 +2020-12-01 23:00:00,96.47,123.572,47.176,32.65 +2020-12-01 23:15:00,95.75,121.62100000000001,47.176,32.65 +2020-12-01 23:30:00,88.96,121.59299999999999,47.176,32.65 +2020-12-01 23:45:00,85.14,121.363,47.176,32.65 +2020-12-02 00:00:00,84.73,114.73200000000001,43.42,32.65 +2020-12-02 00:15:00,88.25,114.98200000000001,43.42,32.65 +2020-12-02 00:30:00,88.93,116.162,43.42,32.65 +2020-12-02 00:45:00,86.11,117.9,43.42,32.65 +2020-12-02 01:00:00,82.71,119.66,40.869,32.65 +2020-12-02 01:15:00,87.22,120.396,40.869,32.65 +2020-12-02 01:30:00,85.57,120.758,40.869,32.65 +2020-12-02 01:45:00,81.75,121.573,40.869,32.65 +2020-12-02 02:00:00,83.12,122.861,39.541,32.65 +2020-12-02 02:15:00,84.79,124.352,39.541,32.65 +2020-12-02 02:30:00,82.28,124.575,39.541,32.65 +2020-12-02 02:45:00,82.21,126.456,39.541,32.65 +2020-12-02 03:00:00,84.94,129.262,39.052,32.65 +2020-12-02 03:15:00,86.09,129.664,39.052,32.65 +2020-12-02 03:30:00,83.57,131.52100000000002,39.052,32.65 +2020-12-02 03:45:00,84.34,132.55200000000002,39.052,32.65 +2020-12-02 04:00:00,87.42,145.606,40.36,32.65 +2020-12-02 04:15:00,90.24,157.736,40.36,32.65 +2020-12-02 04:30:00,90.6,160.465,40.36,32.65 +2020-12-02 04:45:00,88.51,163.063,40.36,32.65 +2020-12-02 05:00:00,94.36,198.175,43.133,32.65 +2020-12-02 05:15:00,100.52,227.77700000000002,43.133,32.65 +2020-12-02 05:30:00,105.35,222.95,43.133,32.65 +2020-12-02 05:45:00,105.35,214.801,43.133,32.65 +2020-12-02 06:00:00,109.55,210.567,54.953,32.65 +2020-12-02 06:15:00,115.06,215.998,54.953,32.65 +2020-12-02 06:30:00,123.34,217.628,54.953,32.65 +2020-12-02 06:45:00,127.78,219.878,54.953,32.65 +2020-12-02 07:00:00,134.98,219.26,66.566,32.65 +2020-12-02 07:15:00,137.75,224.141,66.566,32.65 +2020-12-02 07:30:00,136.68,226.858,66.566,32.65 +2020-12-02 07:45:00,138.02,228.231,66.566,32.65 +2020-12-02 08:00:00,141.25,226.949,72.902,32.65 +2020-12-02 08:15:00,138.63,227.109,72.902,32.65 +2020-12-02 08:30:00,137.77,225.389,72.902,32.65 +2020-12-02 08:45:00,136.96,222.929,72.902,32.65 +2020-12-02 09:00:00,134.22,216.808,68.465,32.65 +2020-12-02 09:15:00,138.14,213.30200000000002,68.465,32.65 +2020-12-02 09:30:00,138.14,210.72099999999998,68.465,32.65 +2020-12-02 09:45:00,137.02,208.063,68.465,32.65 +2020-12-02 10:00:00,134.5,203.59900000000002,63.625,32.65 +2020-12-02 10:15:00,134.21,199.612,63.625,32.65 +2020-12-02 10:30:00,134.09,197.44299999999998,63.625,32.65 +2020-12-02 10:45:00,133.17,196.112,63.625,32.65 +2020-12-02 11:00:00,128.69,195.27599999999998,61.628,32.65 +2020-12-02 11:15:00,131.51,194.36599999999999,61.628,32.65 +2020-12-02 11:30:00,130.92,193.18099999999998,61.628,32.65 +2020-12-02 11:45:00,130.63,191.68400000000003,61.628,32.65 +2020-12-02 12:00:00,128.7,186.065,58.708999999999996,32.65 +2020-12-02 12:15:00,125.44,184.99599999999998,58.708999999999996,32.65 +2020-12-02 12:30:00,126.63,184.84900000000002,58.708999999999996,32.65 +2020-12-02 12:45:00,128.48,185.729,58.708999999999996,32.65 +2020-12-02 13:00:00,126.35,185.24,57.373000000000005,32.65 +2020-12-02 13:15:00,128.95,184.826,57.373000000000005,32.65 +2020-12-02 13:30:00,127.1,184.747,57.373000000000005,32.65 +2020-12-02 13:45:00,127.31,184.768,57.373000000000005,32.65 +2020-12-02 14:00:00,125.8,183.947,57.684,32.65 +2020-12-02 14:15:00,128.72,184.392,57.684,32.65 +2020-12-02 14:30:00,128.58,184.61,57.684,32.65 +2020-12-02 14:45:00,128.73,184.4,57.684,32.65 +2020-12-02 15:00:00,132.19,185.09799999999998,58.03,32.65 +2020-12-02 15:15:00,132.73,185.68099999999998,58.03,32.65 +2020-12-02 15:30:00,132.49,187.644,58.03,32.65 +2020-12-02 15:45:00,133.74,189.456,58.03,32.65 +2020-12-02 16:00:00,137.37,190.55200000000002,59.97,32.65 +2020-12-02 16:15:00,138.66,191.484,59.97,32.65 +2020-12-02 16:30:00,141.97,194.15,59.97,32.65 +2020-12-02 16:45:00,142.93,195.16400000000002,59.97,32.65 +2020-12-02 17:00:00,147.26,198.00400000000002,65.661,32.65 +2020-12-02 17:15:00,146.69,198.15099999999998,65.661,32.65 +2020-12-02 17:30:00,147.72,198.554,65.661,32.65 +2020-12-02 17:45:00,146.61,198.139,65.661,32.65 +2020-12-02 18:00:00,144.03,198.97299999999998,70.96300000000001,32.65 +2020-12-02 18:15:00,143.55,197.338,70.96300000000001,32.65 +2020-12-02 18:30:00,141.98,195.77,70.96300000000001,32.65 +2020-12-02 18:45:00,139.97,195.455,70.96300000000001,32.65 +2020-12-02 19:00:00,141.8,196.488,74.133,32.65 +2020-12-02 19:15:00,140.75,192.959,74.133,32.65 +2020-12-02 19:30:00,137.4,190.96900000000002,74.133,32.65 +2020-12-02 19:45:00,137.36,187.734,74.133,32.65 +2020-12-02 20:00:00,130.36,184.267,65.613,32.65 +2020-12-02 20:15:00,122.11,178.49400000000003,65.613,32.65 +2020-12-02 20:30:00,123.19,175.14700000000002,65.613,32.65 +2020-12-02 20:45:00,122.73,172.321,65.613,32.65 +2020-12-02 21:00:00,117.07,170.332,58.583,32.65 +2020-12-02 21:15:00,110.96,168.428,58.583,32.65 +2020-12-02 21:30:00,110.94,166.321,58.583,32.65 +2020-12-02 21:45:00,111.13,164.558,58.583,32.65 +2020-12-02 22:00:00,105.41,157.761,54.411,32.65 +2020-12-02 22:15:00,105.31,152.091,54.411,32.65 +2020-12-02 22:30:00,102.13,137.376,54.411,32.65 +2020-12-02 22:45:00,104.64,129.267,54.411,32.65 +2020-12-02 23:00:00,96.98,123.932,47.878,32.65 +2020-12-02 23:15:00,94.32,121.976,47.878,32.65 +2020-12-02 23:30:00,94.93,121.96,47.878,32.65 +2020-12-02 23:45:00,93.72,121.713,47.878,32.65 +2020-12-03 00:00:00,87.84,115.08200000000001,44.513000000000005,32.65 +2020-12-03 00:15:00,83.11,115.303,44.513000000000005,32.65 +2020-12-03 00:30:00,84.51,116.478,44.513000000000005,32.65 +2020-12-03 00:45:00,87.95,118.196,44.513000000000005,32.65 +2020-12-03 01:00:00,84.16,119.99700000000001,43.169,32.65 +2020-12-03 01:15:00,83.09,120.73299999999999,43.169,32.65 +2020-12-03 01:30:00,85.34,121.10600000000001,43.169,32.65 +2020-12-03 01:45:00,86.49,121.904,43.169,32.65 +2020-12-03 02:00:00,82.06,123.214,41.763999999999996,32.65 +2020-12-03 02:15:00,82.6,124.706,41.763999999999996,32.65 +2020-12-03 02:30:00,85.09,124.931,41.763999999999996,32.65 +2020-12-03 02:45:00,86.07,126.811,41.763999999999996,32.65 +2020-12-03 03:00:00,80.57,129.602,41.155,32.65 +2020-12-03 03:15:00,80.92,130.036,41.155,32.65 +2020-12-03 03:30:00,83.75,131.89700000000002,41.155,32.65 +2020-12-03 03:45:00,87.62,132.925,41.155,32.65 +2020-12-03 04:00:00,89.28,145.94799999999998,41.96,32.65 +2020-12-03 04:15:00,85.37,158.075,41.96,32.65 +2020-12-03 04:30:00,83.55,160.786,41.96,32.65 +2020-12-03 04:45:00,85.67,163.389,41.96,32.65 +2020-12-03 05:00:00,90.67,198.46200000000002,45.206,32.65 +2020-12-03 05:15:00,94.78,228.007,45.206,32.65 +2020-12-03 05:30:00,95.98,223.21200000000002,45.206,32.65 +2020-12-03 05:45:00,101.33,215.085,45.206,32.65 +2020-12-03 06:00:00,109.52,210.877,55.398999999999994,32.65 +2020-12-03 06:15:00,116.92,216.308,55.398999999999994,32.65 +2020-12-03 06:30:00,120.32,217.99,55.398999999999994,32.65 +2020-12-03 06:45:00,124.73,220.28599999999997,55.398999999999994,32.65 +2020-12-03 07:00:00,131.84,219.67,64.627,32.65 +2020-12-03 07:15:00,134.4,224.56099999999998,64.627,32.65 +2020-12-03 07:30:00,137.22,227.28400000000002,64.627,32.65 +2020-12-03 07:45:00,137.69,228.66400000000002,64.627,32.65 +2020-12-03 08:00:00,141.29,227.395,70.895,32.65 +2020-12-03 08:15:00,139.35,227.55,70.895,32.65 +2020-12-03 08:30:00,139.65,225.842,70.895,32.65 +2020-12-03 08:45:00,139.71,223.34900000000002,70.895,32.65 +2020-12-03 09:00:00,139.33,217.199,66.382,32.65 +2020-12-03 09:15:00,140.17,213.701,66.382,32.65 +2020-12-03 09:30:00,140.93,211.125,66.382,32.65 +2020-12-03 09:45:00,141.66,208.455,66.382,32.65 +2020-12-03 10:00:00,143.6,203.981,62.739,32.65 +2020-12-03 10:15:00,145.07,199.96900000000002,62.739,32.65 +2020-12-03 10:30:00,144.64,197.77700000000002,62.739,32.65 +2020-12-03 10:45:00,144.41,196.437,62.739,32.65 +2020-12-03 11:00:00,143.34,195.584,60.843,32.65 +2020-12-03 11:15:00,145.71,194.658,60.843,32.65 +2020-12-03 11:30:00,144.57,193.47099999999998,60.843,32.65 +2020-12-03 11:45:00,144.85,191.968,60.843,32.65 +2020-12-03 12:00:00,142.96,186.34599999999998,58.466,32.65 +2020-12-03 12:15:00,140.5,185.28900000000002,58.466,32.65 +2020-12-03 12:30:00,139.07,185.16,58.466,32.65 +2020-12-03 12:45:00,139.98,186.046,58.466,32.65 +2020-12-03 13:00:00,137.73,185.521,56.883,32.65 +2020-12-03 13:15:00,138.78,185.111,56.883,32.65 +2020-12-03 13:30:00,136.57,185.028,56.883,32.65 +2020-12-03 13:45:00,136.43,185.03799999999998,56.883,32.65 +2020-12-03 14:00:00,135.01,184.19099999999997,56.503,32.65 +2020-12-03 14:15:00,135.23,184.644,56.503,32.65 +2020-12-03 14:30:00,135.78,184.89,56.503,32.65 +2020-12-03 14:45:00,136.83,184.68900000000002,56.503,32.65 +2020-12-03 15:00:00,136.93,185.407,57.803999999999995,32.65 +2020-12-03 15:15:00,136.54,185.989,57.803999999999995,32.65 +2020-12-03 15:30:00,135.85,187.979,57.803999999999995,32.65 +2020-12-03 15:45:00,135.94,189.792,57.803999999999995,32.65 +2020-12-03 16:00:00,139.8,190.887,59.379,32.65 +2020-12-03 16:15:00,140.97,191.84400000000002,59.379,32.65 +2020-12-03 16:30:00,142.86,194.516,59.379,32.65 +2020-12-03 16:45:00,142.14,195.565,59.379,32.65 +2020-12-03 17:00:00,143.33,198.38,64.71600000000001,32.65 +2020-12-03 17:15:00,143.46,198.551,64.71600000000001,32.65 +2020-12-03 17:30:00,145.46,198.96900000000002,64.71600000000001,32.65 +2020-12-03 17:45:00,144.91,198.55900000000003,64.71600000000001,32.65 +2020-12-03 18:00:00,141.72,199.41299999999998,68.803,32.65 +2020-12-03 18:15:00,139.73,197.738,68.803,32.65 +2020-12-03 18:30:00,138.04,196.17700000000002,68.803,32.65 +2020-12-03 18:45:00,139.53,195.868,68.803,32.65 +2020-12-03 19:00:00,136.48,196.892,72.934,32.65 +2020-12-03 19:15:00,136.16,193.352,72.934,32.65 +2020-12-03 19:30:00,132.96,191.345,72.934,32.65 +2020-12-03 19:45:00,132.54,188.079,72.934,32.65 +2020-12-03 20:00:00,124.03,184.614,65.175,32.65 +2020-12-03 20:15:00,120.74,178.831,65.175,32.65 +2020-12-03 20:30:00,117.79,175.456,65.175,32.65 +2020-12-03 20:45:00,115.66,172.65099999999998,65.175,32.65 +2020-12-03 21:00:00,113.13,170.648,58.55,32.65 +2020-12-03 21:15:00,111.13,168.72799999999998,58.55,32.65 +2020-12-03 21:30:00,107.15,166.62099999999998,58.55,32.65 +2020-12-03 21:45:00,104.82,164.864,58.55,32.65 +2020-12-03 22:00:00,98.64,158.077,55.041000000000004,32.65 +2020-12-03 22:15:00,100.35,152.408,55.041000000000004,32.65 +2020-12-03 22:30:00,99.69,137.747,55.041000000000004,32.65 +2020-12-03 22:45:00,99.18,129.645,55.041000000000004,32.65 +2020-12-03 23:00:00,96.32,124.286,48.258,32.65 +2020-12-03 23:15:00,90.21,122.32600000000001,48.258,32.65 +2020-12-03 23:30:00,91.13,122.322,48.258,32.65 +2020-12-03 23:45:00,90.28,122.055,48.258,32.65 +2020-12-04 00:00:00,88.17,114.368,45.02,32.65 +2020-12-04 00:15:00,80.56,114.755,45.02,32.65 +2020-12-04 00:30:00,74.99,115.79,45.02,32.65 +2020-12-04 00:45:00,78.87,117.596,45.02,32.65 +2020-12-04 01:00:00,80.8,119.12799999999999,42.695,32.65 +2020-12-04 01:15:00,81.08,120.788,42.695,32.65 +2020-12-04 01:30:00,79.01,120.94,42.695,32.65 +2020-12-04 01:45:00,77.31,121.831,42.695,32.65 +2020-12-04 02:00:00,74.12,123.24,41.511,32.65 +2020-12-04 02:15:00,74.97,124.619,41.511,32.65 +2020-12-04 02:30:00,79.2,125.36200000000001,41.511,32.65 +2020-12-04 02:45:00,82.28,127.291,41.511,32.65 +2020-12-04 03:00:00,79.37,129.062,41.162,32.65 +2020-12-04 03:15:00,79.26,130.482,41.162,32.65 +2020-12-04 03:30:00,82.2,132.33100000000002,41.162,32.65 +2020-12-04 03:45:00,85.02,133.678,41.162,32.65 +2020-12-04 04:00:00,84.4,146.89600000000002,42.226000000000006,32.65 +2020-12-04 04:15:00,78.87,158.79,42.226000000000006,32.65 +2020-12-04 04:30:00,75.73,161.70600000000002,42.226000000000006,32.65 +2020-12-04 04:45:00,80.98,163.172,42.226000000000006,32.65 +2020-12-04 05:00:00,85.06,196.937,45.597,32.65 +2020-12-04 05:15:00,88.99,227.93,45.597,32.65 +2020-12-04 05:30:00,89.73,224.248,45.597,32.65 +2020-12-04 05:45:00,96.09,216.083,45.597,32.65 +2020-12-04 06:00:00,108.01,212.333,56.263999999999996,32.65 +2020-12-04 06:15:00,112.74,216.285,56.263999999999996,32.65 +2020-12-04 06:30:00,117.29,217.148,56.263999999999996,32.65 +2020-12-04 06:45:00,121.66,221.128,56.263999999999996,32.65 +2020-12-04 07:00:00,130.21,219.666,66.888,32.65 +2020-12-04 07:15:00,129.15,225.585,66.888,32.65 +2020-12-04 07:30:00,132.61,228.15400000000002,66.888,32.65 +2020-12-04 07:45:00,134.61,228.649,66.888,32.65 +2020-12-04 08:00:00,137.01,226.28,73.459,32.65 +2020-12-04 08:15:00,136.94,226.03599999999997,73.459,32.65 +2020-12-04 08:30:00,137.62,225.31599999999997,73.459,32.65 +2020-12-04 08:45:00,137.29,221.213,73.459,32.65 +2020-12-04 09:00:00,138.54,215.50599999999997,69.087,32.65 +2020-12-04 09:15:00,139.96,212.579,69.087,32.65 +2020-12-04 09:30:00,139.56,209.581,69.087,32.65 +2020-12-04 09:45:00,138.84,206.78799999999998,69.087,32.65 +2020-12-04 10:00:00,136.1,201.173,65.404,32.65 +2020-12-04 10:15:00,138.43,197.835,65.404,32.65 +2020-12-04 10:30:00,137.86,195.547,65.404,32.65 +2020-12-04 10:45:00,138.39,193.75599999999997,65.404,32.65 +2020-12-04 11:00:00,136.83,192.87,63.0,32.65 +2020-12-04 11:15:00,137.96,191.03599999999997,63.0,32.65 +2020-12-04 11:30:00,138.11,191.58900000000003,63.0,32.65 +2020-12-04 11:45:00,138.08,190.125,63.0,32.65 +2020-12-04 12:00:00,136.37,185.582,59.083,32.65 +2020-12-04 12:15:00,134.96,182.451,59.083,32.65 +2020-12-04 12:30:00,134.55,182.497,59.083,32.65 +2020-12-04 12:45:00,137.65,183.898,59.083,32.65 +2020-12-04 13:00:00,132.64,184.27200000000002,56.611999999999995,32.65 +2020-12-04 13:15:00,133.9,184.679,56.611999999999995,32.65 +2020-12-04 13:30:00,132.84,184.613,56.611999999999995,32.65 +2020-12-04 13:45:00,133.43,184.551,56.611999999999995,32.65 +2020-12-04 14:00:00,130.52,182.54,55.161,32.65 +2020-12-04 14:15:00,129.92,182.826,55.161,32.65 +2020-12-04 14:30:00,127.64,183.615,55.161,32.65 +2020-12-04 14:45:00,127.22,183.72099999999998,55.161,32.65 +2020-12-04 15:00:00,125.79,183.976,55.583,32.65 +2020-12-04 15:15:00,126.08,184.122,55.583,32.65 +2020-12-04 15:30:00,126.65,184.62599999999998,55.583,32.65 +2020-12-04 15:45:00,131.71,186.58900000000003,55.583,32.65 +2020-12-04 16:00:00,133.56,186.52,57.611999999999995,32.65 +2020-12-04 16:15:00,135.6,187.78900000000002,57.611999999999995,32.65 +2020-12-04 16:30:00,138.73,190.56400000000002,57.611999999999995,32.65 +2020-12-04 16:45:00,136.33,191.52700000000002,57.611999999999995,32.65 +2020-12-04 17:00:00,138.83,194.524,64.14,32.65 +2020-12-04 17:15:00,137.41,194.31099999999998,64.14,32.65 +2020-12-04 17:30:00,138.46,194.424,64.14,32.65 +2020-12-04 17:45:00,138.3,193.78900000000002,64.14,32.65 +2020-12-04 18:00:00,135.16,195.34900000000002,68.086,32.65 +2020-12-04 18:15:00,133.86,193.245,68.086,32.65 +2020-12-04 18:30:00,134.09,192.083,68.086,32.65 +2020-12-04 18:45:00,137.38,191.77700000000002,68.086,32.65 +2020-12-04 19:00:00,132.52,193.69400000000002,69.915,32.65 +2020-12-04 19:15:00,129.24,191.503,69.915,32.65 +2020-12-04 19:30:00,128.95,189.06900000000002,69.915,32.65 +2020-12-04 19:45:00,126.18,185.313,69.915,32.65 +2020-12-04 20:00:00,118.12,181.905,61.695,32.65 +2020-12-04 20:15:00,116.18,176.11900000000003,61.695,32.65 +2020-12-04 20:30:00,113.76,172.675,61.695,32.65 +2020-12-04 20:45:00,111.07,170.445,61.695,32.65 +2020-12-04 21:00:00,107.3,168.935,56.041000000000004,32.65 +2020-12-04 21:15:00,103.46,167.43400000000003,56.041000000000004,32.65 +2020-12-04 21:30:00,100.57,165.37400000000002,56.041000000000004,32.65 +2020-12-04 21:45:00,103.14,164.165,56.041000000000004,32.65 +2020-12-04 22:00:00,98.06,158.365,51.888999999999996,32.65 +2020-12-04 22:15:00,96.92,152.564,51.888999999999996,32.65 +2020-12-04 22:30:00,84.82,144.314,51.888999999999996,32.65 +2020-12-04 22:45:00,84.39,139.736,51.888999999999996,32.65 +2020-12-04 23:00:00,84.2,133.908,45.787,32.65 +2020-12-04 23:15:00,84.93,130.002,45.787,32.65 +2020-12-04 23:30:00,82.36,128.553,45.787,32.65 +2020-12-04 23:45:00,78.57,127.60700000000001,45.787,32.65 +2020-12-05 00:00:00,72.37,111.85600000000001,41.815,32.468 +2020-12-05 00:15:00,73.35,107.964,41.815,32.468 +2020-12-05 00:30:00,73.52,110.323,41.815,32.468 +2020-12-05 00:45:00,69.58,112.825,41.815,32.468 +2020-12-05 01:00:00,65.32,115.024,38.645,32.468 +2020-12-05 01:15:00,67.07,115.698,38.645,32.468 +2020-12-05 01:30:00,70.05,115.344,38.645,32.468 +2020-12-05 01:45:00,72.67,116.0,38.645,32.468 +2020-12-05 02:00:00,67.0,118.118,36.696,32.468 +2020-12-05 02:15:00,65.94,119.133,36.696,32.468 +2020-12-05 02:30:00,62.92,118.76799999999999,36.696,32.468 +2020-12-05 02:45:00,67.89,120.795,36.696,32.468 +2020-12-05 03:00:00,68.09,123.185,35.42,32.468 +2020-12-05 03:15:00,69.91,123.42200000000001,35.42,32.468 +2020-12-05 03:30:00,67.12,123.686,35.42,32.468 +2020-12-05 03:45:00,70.54,125.135,35.42,32.468 +2020-12-05 04:00:00,70.06,134.191,35.167,32.468 +2020-12-05 04:15:00,64.46,143.546,35.167,32.468 +2020-12-05 04:30:00,62.74,144.289,35.167,32.468 +2020-12-05 04:45:00,63.37,145.262,35.167,32.468 +2020-12-05 05:00:00,63.4,163.07299999999998,35.311,32.468 +2020-12-05 05:15:00,63.89,175.081,35.311,32.468 +2020-12-05 05:30:00,64.64,171.774,35.311,32.468 +2020-12-05 05:45:00,66.98,169.051,35.311,32.468 +2020-12-05 06:00:00,68.66,184.03099999999998,37.117,32.468 +2020-12-05 06:15:00,70.86,204.283,37.117,32.468 +2020-12-05 06:30:00,72.62,199.84,37.117,32.468 +2020-12-05 06:45:00,75.93,194.908,37.117,32.468 +2020-12-05 07:00:00,78.46,189.69299999999998,40.948,32.468 +2020-12-05 07:15:00,81.06,194.37400000000002,40.948,32.468 +2020-12-05 07:30:00,83.25,199.646,40.948,32.468 +2020-12-05 07:45:00,85.28,204.113,40.948,32.468 +2020-12-05 08:00:00,88.29,205.796,44.903,32.468 +2020-12-05 08:15:00,88.3,209.168,44.903,32.468 +2020-12-05 08:30:00,89.21,210.06599999999997,44.903,32.468 +2020-12-05 08:45:00,91.96,209.077,44.903,32.468 +2020-12-05 09:00:00,93.43,205.105,46.283,32.468 +2020-12-05 09:15:00,93.03,202.935,46.283,32.468 +2020-12-05 09:30:00,95.57,200.84099999999998,46.283,32.468 +2020-12-05 09:45:00,93.63,198.196,46.283,32.468 +2020-12-05 10:00:00,95.54,192.859,44.103,32.468 +2020-12-05 10:15:00,96.38,189.66,44.103,32.468 +2020-12-05 10:30:00,98.31,187.513,44.103,32.468 +2020-12-05 10:45:00,98.64,187.00599999999997,44.103,32.468 +2020-12-05 11:00:00,101.27,186.32299999999998,42.373999999999995,32.468 +2020-12-05 11:15:00,101.54,183.795,42.373999999999995,32.468 +2020-12-05 11:30:00,99.33,183.24,42.373999999999995,32.468 +2020-12-05 11:45:00,97.85,180.82299999999998,42.373999999999995,32.468 +2020-12-05 12:00:00,99.19,175.40900000000002,39.937,32.468 +2020-12-05 12:15:00,96.78,172.928,39.937,32.468 +2020-12-05 12:30:00,95.3,173.30599999999998,39.937,32.468 +2020-12-05 12:45:00,95.84,173.953,39.937,32.468 +2020-12-05 13:00:00,90.76,173.893,37.138000000000005,32.468 +2020-12-05 13:15:00,88.94,172.267,37.138000000000005,32.468 +2020-12-05 13:30:00,88.02,171.75099999999998,37.138000000000005,32.468 +2020-12-05 13:45:00,84.59,172.145,37.138000000000005,32.468 +2020-12-05 14:00:00,86.03,171.328,36.141999999999996,32.468 +2020-12-05 14:15:00,87.73,171.08599999999998,36.141999999999996,32.468 +2020-12-05 14:30:00,88.66,170.1,36.141999999999996,32.468 +2020-12-05 14:45:00,88.57,170.43400000000003,36.141999999999996,32.468 +2020-12-05 15:00:00,90.45,171.362,37.964,32.468 +2020-12-05 15:15:00,92.3,172.308,37.964,32.468 +2020-12-05 15:30:00,95.92,174.362,37.964,32.468 +2020-12-05 15:45:00,95.54,176.33700000000002,37.964,32.468 +2020-12-05 16:00:00,99.12,175.083,40.699,32.468 +2020-12-05 16:15:00,103.2,177.25900000000001,40.699,32.468 +2020-12-05 16:30:00,101.85,179.989,40.699,32.468 +2020-12-05 16:45:00,102.17,181.863,40.699,32.468 +2020-12-05 17:00:00,105.16,184.262,46.216,32.468 +2020-12-05 17:15:00,104.75,185.71099999999998,46.216,32.468 +2020-12-05 17:30:00,109.26,185.743,46.216,32.468 +2020-12-05 17:45:00,106.26,184.71400000000003,46.216,32.468 +2020-12-05 18:00:00,107.15,185.812,51.123999999999995,32.468 +2020-12-05 18:15:00,107.16,185.459,51.123999999999995,32.468 +2020-12-05 18:30:00,106.1,185.628,51.123999999999995,32.468 +2020-12-05 18:45:00,105.22,182.0,51.123999999999995,32.468 +2020-12-05 19:00:00,103.27,184.81900000000002,52.336000000000006,32.468 +2020-12-05 19:15:00,102.54,182.122,52.336000000000006,32.468 +2020-12-05 19:30:00,101.72,180.421,52.336000000000006,32.468 +2020-12-05 19:45:00,99.84,176.465,52.336000000000006,32.468 +2020-12-05 20:00:00,95.78,175.215,48.825,32.468 +2020-12-05 20:15:00,92.88,171.52599999999998,48.825,32.468 +2020-12-05 20:30:00,90.67,167.706,48.825,32.468 +2020-12-05 20:45:00,89.27,165.101,48.825,32.468 +2020-12-05 21:00:00,84.32,165.80599999999998,43.729,32.468 +2020-12-05 21:15:00,84.83,164.725,43.729,32.468 +2020-12-05 21:30:00,83.33,163.88400000000001,43.729,32.468 +2020-12-05 21:45:00,81.56,162.278,43.729,32.468 +2020-12-05 22:00:00,77.6,157.82299999999998,44.126000000000005,32.468 +2020-12-05 22:15:00,76.48,154.488,44.126000000000005,32.468 +2020-12-05 22:30:00,73.47,152.457,44.126000000000005,32.468 +2020-12-05 22:45:00,72.33,149.74200000000002,44.126000000000005,32.468 +2020-12-05 23:00:00,67.46,146.29399999999998,38.169000000000004,32.468 +2020-12-05 23:15:00,67.5,140.766,38.169000000000004,32.468 +2020-12-05 23:30:00,65.09,137.606,38.169000000000004,32.468 +2020-12-05 23:45:00,63.28,134.237,38.169000000000004,32.468 +2020-12-06 00:00:00,59.2,112.60799999999999,35.232,32.468 +2020-12-06 00:15:00,58.21,108.366,35.232,32.468 +2020-12-06 00:30:00,58.38,110.34700000000001,35.232,32.468 +2020-12-06 00:45:00,57.76,113.522,35.232,32.468 +2020-12-06 01:00:00,53.35,115.618,31.403000000000002,32.468 +2020-12-06 01:15:00,54.31,117.323,31.403000000000002,32.468 +2020-12-06 01:30:00,53.96,117.494,31.403000000000002,32.468 +2020-12-06 01:45:00,53.89,117.824,31.403000000000002,32.468 +2020-12-06 02:00:00,52.08,119.219,30.69,32.468 +2020-12-06 02:15:00,53.48,119.389,30.69,32.468 +2020-12-06 02:30:00,52.62,119.875,30.69,32.468 +2020-12-06 02:45:00,52.89,122.36,30.69,32.468 +2020-12-06 03:00:00,50.89,125.052,29.516,32.468 +2020-12-06 03:15:00,52.01,124.8,29.516,32.468 +2020-12-06 03:30:00,49.54,126.455,29.516,32.468 +2020-12-06 03:45:00,51.95,127.82600000000001,29.516,32.468 +2020-12-06 04:00:00,51.32,136.608,29.148000000000003,32.468 +2020-12-06 04:15:00,52.22,144.953,29.148000000000003,32.468 +2020-12-06 04:30:00,53.06,145.747,29.148000000000003,32.468 +2020-12-06 04:45:00,54.12,146.994,29.148000000000003,32.468 +2020-12-06 05:00:00,54.06,161.283,28.706,32.468 +2020-12-06 05:15:00,55.45,170.83599999999998,28.706,32.468 +2020-12-06 05:30:00,55.14,167.392,28.706,32.468 +2020-12-06 05:45:00,55.66,164.93400000000003,28.706,32.468 +2020-12-06 06:00:00,57.08,179.829,28.771,32.468 +2020-12-06 06:15:00,58.52,198.37599999999998,28.771,32.468 +2020-12-06 06:30:00,60.18,192.88299999999998,28.771,32.468 +2020-12-06 06:45:00,62.27,186.947,28.771,32.468 +2020-12-06 07:00:00,64.58,184.169,31.39,32.468 +2020-12-06 07:15:00,66.66,188.005,31.39,32.468 +2020-12-06 07:30:00,70.38,192.06599999999997,31.39,32.468 +2020-12-06 07:45:00,71.0,195.782,31.39,32.468 +2020-12-06 08:00:00,72.79,199.30599999999998,34.972,32.468 +2020-12-06 08:15:00,75.41,202.59,34.972,32.468 +2020-12-06 08:30:00,74.46,205.142,34.972,32.468 +2020-12-06 08:45:00,78.84,206.12,34.972,32.468 +2020-12-06 09:00:00,80.64,201.72299999999998,36.709,32.468 +2020-12-06 09:15:00,82.63,200.10299999999998,36.709,32.468 +2020-12-06 09:30:00,84.01,197.863,36.709,32.468 +2020-12-06 09:45:00,85.27,195.09799999999998,36.709,32.468 +2020-12-06 10:00:00,87.21,192.24400000000003,35.812,32.468 +2020-12-06 10:15:00,88.37,189.549,35.812,32.468 +2020-12-06 10:30:00,89.09,187.975,35.812,32.468 +2020-12-06 10:45:00,91.13,185.59900000000002,35.812,32.468 +2020-12-06 11:00:00,92.13,185.801,36.746,32.468 +2020-12-06 11:15:00,94.24,183.386,36.746,32.468 +2020-12-06 11:30:00,96.69,181.97799999999998,36.746,32.468 +2020-12-06 11:45:00,97.52,180.15400000000002,36.746,32.468 +2020-12-06 12:00:00,95.11,174.195,35.048,32.468 +2020-12-06 12:15:00,93.73,173.592,35.048,32.468 +2020-12-06 12:30:00,90.61,172.55700000000002,35.048,32.468 +2020-12-06 12:45:00,90.11,172.25,35.048,32.468 +2020-12-06 13:00:00,86.89,171.447,29.987,32.468 +2020-12-06 13:15:00,86.08,172.782,29.987,32.468 +2020-12-06 13:30:00,85.02,172.041,29.987,32.468 +2020-12-06 13:45:00,84.86,171.791,29.987,32.468 +2020-12-06 14:00:00,83.89,171.206,27.21,32.468 +2020-12-06 14:15:00,85.55,172.157,27.21,32.468 +2020-12-06 14:30:00,85.84,172.392,27.21,32.468 +2020-12-06 14:45:00,85.27,172.317,27.21,32.468 +2020-12-06 15:00:00,86.37,171.761,27.726999999999997,32.468 +2020-12-06 15:15:00,86.71,173.438,27.726999999999997,32.468 +2020-12-06 15:30:00,87.73,176.092,27.726999999999997,32.468 +2020-12-06 15:45:00,90.08,178.752,27.726999999999997,32.468 +2020-12-06 16:00:00,91.02,179.32,32.23,32.468 +2020-12-06 16:15:00,92.78,180.62599999999998,32.23,32.468 +2020-12-06 16:30:00,94.77,183.63,32.23,32.468 +2020-12-06 16:45:00,95.94,185.667,32.23,32.468 +2020-12-06 17:00:00,99.97,188.047,42.016999999999996,32.468 +2020-12-06 17:15:00,100.43,189.213,42.016999999999996,32.468 +2020-12-06 17:30:00,101.22,189.55,42.016999999999996,32.468 +2020-12-06 17:45:00,103.06,190.769,42.016999999999996,32.468 +2020-12-06 18:00:00,104.02,191.354,49.338,32.468 +2020-12-06 18:15:00,103.43,192.268,49.338,32.468 +2020-12-06 18:30:00,103.55,190.385,49.338,32.468 +2020-12-06 18:45:00,102.55,188.59400000000002,49.338,32.468 +2020-12-06 19:00:00,101.83,191.08900000000003,52.369,32.468 +2020-12-06 19:15:00,100.33,188.959,52.369,32.468 +2020-12-06 19:30:00,99.34,187.078,52.369,32.468 +2020-12-06 19:45:00,96.53,184.515,52.369,32.468 +2020-12-06 20:00:00,95.12,183.24099999999999,50.405,32.468 +2020-12-06 20:15:00,95.13,180.514,50.405,32.468 +2020-12-06 20:30:00,94.12,177.896,50.405,32.468 +2020-12-06 20:45:00,99.95,174.113,50.405,32.468 +2020-12-06 21:00:00,96.19,172.24400000000003,46.235,32.468 +2020-12-06 21:15:00,96.46,170.52599999999998,46.235,32.468 +2020-12-06 21:30:00,87.84,169.972,46.235,32.468 +2020-12-06 21:45:00,89.67,168.50900000000001,46.235,32.468 +2020-12-06 22:00:00,87.62,162.901,46.861000000000004,32.468 +2020-12-06 22:15:00,90.69,158.804,46.861000000000004,32.468 +2020-12-06 22:30:00,89.86,153.694,46.861000000000004,32.468 +2020-12-06 22:45:00,88.9,150.136,46.861000000000004,32.468 +2020-12-06 23:00:00,78.62,143.899,41.302,32.468 +2020-12-06 23:15:00,77.12,140.21,41.302,32.468 +2020-12-06 23:30:00,79.89,137.859,41.302,32.468 +2020-12-06 23:45:00,77.11,135.356,41.302,32.468 +2020-12-07 00:00:00,72.47,117.10600000000001,37.164,32.65 +2020-12-07 00:15:00,69.76,115.786,37.164,32.65 +2020-12-07 00:30:00,69.85,117.88799999999999,37.164,32.65 +2020-12-07 00:45:00,69.38,120.509,37.164,32.65 +2020-12-07 01:00:00,67.99,122.65899999999999,34.994,32.65 +2020-12-07 01:15:00,70.59,123.839,34.994,32.65 +2020-12-07 01:30:00,73.49,124.073,34.994,32.65 +2020-12-07 01:45:00,70.94,124.50200000000001,34.994,32.65 +2020-12-07 02:00:00,66.53,125.896,34.571,32.65 +2020-12-07 02:15:00,67.94,127.52,34.571,32.65 +2020-12-07 02:30:00,66.77,128.33700000000002,34.571,32.65 +2020-12-07 02:45:00,67.8,130.205,34.571,32.65 +2020-12-07 03:00:00,68.56,134.14700000000002,33.934,32.65 +2020-12-07 03:15:00,68.92,135.562,33.934,32.65 +2020-12-07 03:30:00,72.78,136.951,33.934,32.65 +2020-12-07 03:45:00,77.58,137.773,33.934,32.65 +2020-12-07 04:00:00,78.96,150.85,34.107,32.65 +2020-12-07 04:15:00,76.3,163.312,34.107,32.65 +2020-12-07 04:30:00,75.15,166.274,34.107,32.65 +2020-12-07 04:45:00,76.91,167.683,34.107,32.65 +2020-12-07 05:00:00,80.43,197.60299999999998,39.575,32.65 +2020-12-07 05:15:00,83.47,227.14,39.575,32.65 +2020-12-07 05:30:00,88.42,224.0,39.575,32.65 +2020-12-07 05:45:00,93.74,215.947,39.575,32.65 +2020-12-07 06:00:00,104.42,213.045,56.156000000000006,32.65 +2020-12-07 06:15:00,111.84,216.856,56.156000000000006,32.65 +2020-12-07 06:30:00,115.43,219.375,56.156000000000006,32.65 +2020-12-07 06:45:00,121.15,222.22299999999998,56.156000000000006,32.65 +2020-12-07 07:00:00,126.05,221.801,67.926,32.65 +2020-12-07 07:15:00,129.31,226.882,67.926,32.65 +2020-12-07 07:30:00,132.06,230.178,67.926,32.65 +2020-12-07 07:45:00,131.74,231.34599999999998,67.926,32.65 +2020-12-07 08:00:00,134.22,230.016,72.58,32.65 +2020-12-07 08:15:00,134.5,231.165,72.58,32.65 +2020-12-07 08:30:00,134.15,229.669,72.58,32.65 +2020-12-07 08:45:00,132.35,227.305,72.58,32.65 +2020-12-07 09:00:00,135.85,221.885,66.984,32.65 +2020-12-07 09:15:00,133.39,216.815,66.984,32.65 +2020-12-07 09:30:00,137.13,213.58900000000003,66.984,32.65 +2020-12-07 09:45:00,137.46,211.10299999999998,66.984,32.65 +2020-12-07 10:00:00,137.29,207.167,63.158,32.65 +2020-12-07 10:15:00,137.05,204.125,63.158,32.65 +2020-12-07 10:30:00,137.9,201.66,63.158,32.65 +2020-12-07 10:45:00,135.05,200.014,63.158,32.65 +2020-12-07 11:00:00,137.29,197.605,61.141000000000005,32.65 +2020-12-07 11:15:00,137.89,196.96099999999998,61.141000000000005,32.65 +2020-12-07 11:30:00,136.52,196.918,61.141000000000005,32.65 +2020-12-07 11:45:00,136.73,194.68599999999998,61.141000000000005,32.65 +2020-12-07 12:00:00,135.1,190.422,57.961000000000006,32.65 +2020-12-07 12:15:00,132.49,189.833,57.961000000000006,32.65 +2020-12-07 12:30:00,133.75,189.05700000000002,57.961000000000006,32.65 +2020-12-07 12:45:00,132.35,190.261,57.961000000000006,32.65 +2020-12-07 13:00:00,131.69,190.002,56.843,32.65 +2020-12-07 13:15:00,130.03,189.96599999999998,56.843,32.65 +2020-12-07 13:30:00,129.19,188.69799999999998,56.843,32.65 +2020-12-07 13:45:00,129.62,188.476,56.843,32.65 +2020-12-07 14:00:00,131.03,187.24900000000002,55.992,32.65 +2020-12-07 14:15:00,133.01,187.574,55.992,32.65 +2020-12-07 14:30:00,132.23,187.301,55.992,32.65 +2020-12-07 14:45:00,130.84,187.215,55.992,32.65 +2020-12-07 15:00:00,132.97,188.435,57.523,32.65 +2020-12-07 15:15:00,132.34,188.688,57.523,32.65 +2020-12-07 15:30:00,133.31,190.551,57.523,32.65 +2020-12-07 15:45:00,133.8,192.769,57.523,32.65 +2020-12-07 16:00:00,136.26,193.521,59.471000000000004,32.65 +2020-12-07 16:15:00,137.17,194.09599999999998,59.471000000000004,32.65 +2020-12-07 16:30:00,133.45,196.149,59.471000000000004,32.65 +2020-12-07 16:45:00,141.66,197.045,59.471000000000004,32.65 +2020-12-07 17:00:00,155.57,199.225,65.066,32.65 +2020-12-07 17:15:00,156.59,199.476,65.066,32.65 +2020-12-07 17:30:00,158.11,199.28400000000002,65.066,32.65 +2020-12-07 17:45:00,143.66,199.03099999999998,65.066,32.65 +2020-12-07 18:00:00,137.35,200.05900000000003,69.581,32.65 +2020-12-07 18:15:00,140.33,198.747,69.581,32.65 +2020-12-07 18:30:00,138.53,197.518,69.581,32.65 +2020-12-07 18:45:00,138.89,196.44299999999998,69.581,32.65 +2020-12-07 19:00:00,138.22,197.331,73.771,32.65 +2020-12-07 19:15:00,133.86,194.03,73.771,32.65 +2020-12-07 19:30:00,132.32,192.628,73.771,32.65 +2020-12-07 19:45:00,130.15,189.213,73.771,32.65 +2020-12-07 20:00:00,125.89,185.597,65.035,32.65 +2020-12-07 20:15:00,120.27,180.40099999999998,65.035,32.65 +2020-12-07 20:30:00,116.83,175.91099999999997,65.035,32.65 +2020-12-07 20:45:00,115.19,173.76,65.035,32.65 +2020-12-07 21:00:00,113.15,172.405,58.7,32.65 +2020-12-07 21:15:00,109.2,169.50599999999997,58.7,32.65 +2020-12-07 21:30:00,106.77,168.132,58.7,32.65 +2020-12-07 21:45:00,102.99,166.173,58.7,32.65 +2020-12-07 22:00:00,100.77,157.714,53.888000000000005,32.65 +2020-12-07 22:15:00,98.17,152.30700000000002,53.888000000000005,32.65 +2020-12-07 22:30:00,96.13,137.769,53.888000000000005,32.65 +2020-12-07 22:45:00,92.55,129.42,53.888000000000005,32.65 +2020-12-07 23:00:00,91.82,123.929,45.501999999999995,32.65 +2020-12-07 23:15:00,89.24,122.87299999999999,45.501999999999995,32.65 +2020-12-07 23:30:00,86.63,123.27600000000001,45.501999999999995,32.65 +2020-12-07 23:45:00,83.86,123.38,45.501999999999995,32.65 +2020-12-08 00:00:00,81.48,116.75399999999999,43.537,32.65 +2020-12-08 00:15:00,80.94,116.822,43.537,32.65 +2020-12-08 00:30:00,78.77,117.975,43.537,32.65 +2020-12-08 00:45:00,79.27,119.594,43.537,32.65 +2020-12-08 01:00:00,79.97,121.589,41.854,32.65 +2020-12-08 01:15:00,75.56,122.323,41.854,32.65 +2020-12-08 01:30:00,74.99,122.73700000000001,41.854,32.65 +2020-12-08 01:45:00,74.98,123.461,41.854,32.65 +2020-12-08 02:00:00,75.24,124.875,40.321,32.65 +2020-12-08 02:15:00,76.79,126.37799999999999,40.321,32.65 +2020-12-08 02:30:00,77.69,126.604,40.321,32.65 +2020-12-08 02:45:00,75.39,128.481,40.321,32.65 +2020-12-08 03:00:00,75.09,131.209,39.632,32.65 +2020-12-08 03:15:00,77.11,131.793,39.632,32.65 +2020-12-08 03:30:00,77.43,133.668,39.632,32.65 +2020-12-08 03:45:00,78.5,134.694,39.632,32.65 +2020-12-08 04:00:00,80.28,147.555,40.183,32.65 +2020-12-08 04:15:00,79.41,159.66299999999998,40.183,32.65 +2020-12-08 04:30:00,80.97,162.30100000000002,40.183,32.65 +2020-12-08 04:45:00,83.79,164.922,40.183,32.65 +2020-12-08 05:00:00,87.76,199.80200000000002,43.945,32.65 +2020-12-08 05:15:00,91.47,229.07,43.945,32.65 +2020-12-08 05:30:00,96.61,224.416,43.945,32.65 +2020-12-08 05:45:00,100.78,216.40099999999998,43.945,32.65 +2020-12-08 06:00:00,109.95,212.322,56.048,32.65 +2020-12-08 06:15:00,114.88,217.763,56.048,32.65 +2020-12-08 06:30:00,120.13,219.68400000000003,56.048,32.65 +2020-12-08 06:45:00,125.43,222.21400000000003,56.048,32.65 +2020-12-08 07:00:00,131.43,221.608,65.74,32.65 +2020-12-08 07:15:00,135.23,226.53900000000002,65.74,32.65 +2020-12-08 07:30:00,137.34,229.292,65.74,32.65 +2020-12-08 07:45:00,140.61,230.69,65.74,32.65 +2020-12-08 08:00:00,140.68,229.48,72.757,32.65 +2020-12-08 08:15:00,141.83,229.604,72.757,32.65 +2020-12-08 08:30:00,140.68,227.933,72.757,32.65 +2020-12-08 08:45:00,140.89,225.28400000000002,72.757,32.65 +2020-12-08 09:00:00,137.03,218.998,67.692,32.65 +2020-12-08 09:15:00,139.39,215.53799999999998,67.692,32.65 +2020-12-08 09:30:00,139.92,212.989,67.692,32.65 +2020-12-08 09:45:00,139.63,210.25900000000001,67.692,32.65 +2020-12-08 10:00:00,141.01,205.74200000000002,63.506,32.65 +2020-12-08 10:15:00,141.07,201.618,63.506,32.65 +2020-12-08 10:30:00,142.01,199.31,63.506,32.65 +2020-12-08 10:45:00,148.52,197.928,63.506,32.65 +2020-12-08 11:00:00,159.29,196.99099999999999,60.758,32.65 +2020-12-08 11:15:00,158.54,195.997,60.758,32.65 +2020-12-08 11:30:00,159.27,194.8,60.758,32.65 +2020-12-08 11:45:00,146.55,193.266,60.758,32.65 +2020-12-08 12:00:00,143.87,187.637,57.519,32.65 +2020-12-08 12:15:00,142.59,186.643,57.519,32.65 +2020-12-08 12:30:00,142.23,186.595,57.519,32.65 +2020-12-08 12:45:00,140.71,187.507,57.519,32.65 +2020-12-08 13:00:00,136.17,186.812,56.46,32.65 +2020-12-08 13:15:00,135.08,186.417,56.46,32.65 +2020-12-08 13:30:00,133.39,186.313,56.46,32.65 +2020-12-08 13:45:00,132.97,186.27599999999998,56.46,32.65 +2020-12-08 14:00:00,133.26,185.308,56.207,32.65 +2020-12-08 14:15:00,134.5,185.797,56.207,32.65 +2020-12-08 14:30:00,134.65,186.173,56.207,32.65 +2020-12-08 14:45:00,134.64,186.024,56.207,32.65 +2020-12-08 15:00:00,135.74,186.833,57.391999999999996,32.65 +2020-12-08 15:15:00,137.12,187.408,57.391999999999996,32.65 +2020-12-08 15:30:00,138.16,189.517,57.391999999999996,32.65 +2020-12-08 15:45:00,139.22,191.333,57.391999999999996,32.65 +2020-12-08 16:00:00,144.76,192.426,59.955,32.65 +2020-12-08 16:15:00,147.35,193.497,59.955,32.65 +2020-12-08 16:30:00,147.5,196.205,59.955,32.65 +2020-12-08 16:45:00,149.58,197.412,59.955,32.65 +2020-12-08 17:00:00,161.96,200.109,67.063,32.65 +2020-12-08 17:15:00,162.06,200.412,67.063,32.65 +2020-12-08 17:30:00,163.06,200.90400000000002,67.063,32.65 +2020-12-08 17:45:00,150.37,200.532,67.063,32.65 +2020-12-08 18:00:00,142.45,201.481,71.477,32.65 +2020-12-08 18:15:00,144.58,199.622,71.477,32.65 +2020-12-08 18:30:00,143.42,198.09599999999998,71.477,32.65 +2020-12-08 18:45:00,143.59,197.825,71.477,32.65 +2020-12-08 19:00:00,141.4,198.78900000000002,74.32,32.65 +2020-12-08 19:15:00,138.68,195.19799999999998,74.32,32.65 +2020-12-08 19:30:00,136.14,193.11599999999999,74.32,32.65 +2020-12-08 19:45:00,135.11,189.708,74.32,32.65 +2020-12-08 20:00:00,130.38,186.248,66.157,32.65 +2020-12-08 20:15:00,125.0,180.418,66.157,32.65 +2020-12-08 20:30:00,121.61,176.912,66.157,32.65 +2020-12-08 20:45:00,119.36,174.207,66.157,32.65 +2020-12-08 21:00:00,116.99,172.13299999999998,59.806000000000004,32.65 +2020-12-08 21:15:00,116.17,170.135,59.806000000000004,32.65 +2020-12-08 21:30:00,118.31,168.03,59.806000000000004,32.65 +2020-12-08 21:45:00,115.49,166.30599999999998,59.806000000000004,32.65 +2020-12-08 22:00:00,106.63,159.56,54.785,32.65 +2020-12-08 22:15:00,103.62,153.899,54.785,32.65 +2020-12-08 22:30:00,104.21,139.499,54.785,32.65 +2020-12-08 22:45:00,103.55,131.433,54.785,32.65 +2020-12-08 23:00:00,100.51,125.949,47.176,32.65 +2020-12-08 23:15:00,93.05,123.975,47.176,32.65 +2020-12-08 23:30:00,89.98,124.031,47.176,32.65 +2020-12-08 23:45:00,88.91,123.681,47.176,32.65 +2020-12-09 00:00:00,91.27,117.072,43.42,32.65 +2020-12-09 00:15:00,91.07,117.11,43.42,32.65 +2020-12-09 00:30:00,87.44,118.256,43.42,32.65 +2020-12-09 00:45:00,84.26,119.85700000000001,43.42,32.65 +2020-12-09 01:00:00,86.4,121.887,40.869,32.65 +2020-12-09 01:15:00,87.29,122.62,40.869,32.65 +2020-12-09 01:30:00,87.03,123.04299999999999,40.869,32.65 +2020-12-09 01:45:00,82.74,123.751,40.869,32.65 +2020-12-09 02:00:00,80.31,125.185,39.541,32.65 +2020-12-09 02:15:00,81.19,126.69,39.541,32.65 +2020-12-09 02:30:00,86.0,126.91799999999999,39.541,32.65 +2020-12-09 02:45:00,86.44,128.79399999999998,39.541,32.65 +2020-12-09 03:00:00,85.55,131.509,39.052,32.65 +2020-12-09 03:15:00,80.17,132.123,39.052,32.65 +2020-12-09 03:30:00,84.96,134.0,39.052,32.65 +2020-12-09 03:45:00,88.74,135.02700000000002,39.052,32.65 +2020-12-09 04:00:00,90.1,147.856,40.36,32.65 +2020-12-09 04:15:00,86.33,159.96,40.36,32.65 +2020-12-09 04:30:00,85.2,162.585,40.36,32.65 +2020-12-09 04:45:00,86.88,165.207,40.36,32.65 +2020-12-09 05:00:00,93.26,200.05,43.133,32.65 +2020-12-09 05:15:00,99.1,229.263,43.133,32.65 +2020-12-09 05:30:00,104.98,224.636,43.133,32.65 +2020-12-09 05:45:00,108.07,216.644,43.133,32.65 +2020-12-09 06:00:00,110.28,212.59,54.953,32.65 +2020-12-09 06:15:00,116.51,218.035,54.953,32.65 +2020-12-09 06:30:00,119.87,220.00099999999998,54.953,32.65 +2020-12-09 06:45:00,127.56,222.575,54.953,32.65 +2020-12-09 07:00:00,130.61,221.97299999999998,66.566,32.65 +2020-12-09 07:15:00,131.47,226.91099999999997,66.566,32.65 +2020-12-09 07:30:00,133.34,229.667,66.566,32.65 +2020-12-09 07:45:00,136.34,231.06599999999997,66.566,32.65 +2020-12-09 08:00:00,138.72,229.86599999999999,72.902,32.65 +2020-12-09 08:15:00,140.05,229.984,72.902,32.65 +2020-12-09 08:30:00,140.81,228.317,72.902,32.65 +2020-12-09 08:45:00,140.14,225.637,72.902,32.65 +2020-12-09 09:00:00,141.66,219.325,68.465,32.65 +2020-12-09 09:15:00,142.55,215.872,68.465,32.65 +2020-12-09 09:30:00,144.01,213.329,68.465,32.65 +2020-12-09 09:45:00,148.03,210.588,68.465,32.65 +2020-12-09 10:00:00,148.43,206.063,63.625,32.65 +2020-12-09 10:15:00,148.69,201.92,63.625,32.65 +2020-12-09 10:30:00,147.3,199.59,63.625,32.65 +2020-12-09 10:45:00,150.11,198.201,63.625,32.65 +2020-12-09 11:00:00,165.23,197.245,61.628,32.65 +2020-12-09 11:15:00,167.64,196.24,61.628,32.65 +2020-12-09 11:30:00,171.35,195.041,61.628,32.65 +2020-12-09 11:45:00,158.93,193.50099999999998,61.628,32.65 +2020-12-09 12:00:00,150.69,187.872,58.708999999999996,32.65 +2020-12-09 12:15:00,147.05,186.891,58.708999999999996,32.65 +2020-12-09 12:30:00,147.05,186.858,58.708999999999996,32.65 +2020-12-09 12:45:00,144.87,187.774,58.708999999999996,32.65 +2020-12-09 13:00:00,143.99,187.047,57.373000000000005,32.65 +2020-12-09 13:15:00,143.5,186.655,57.373000000000005,32.65 +2020-12-09 13:30:00,140.33,186.545,57.373000000000005,32.65 +2020-12-09 13:45:00,139.48,186.498,57.373000000000005,32.65 +2020-12-09 14:00:00,139.53,185.511,57.684,32.65 +2020-12-09 14:15:00,138.98,186.005,57.684,32.65 +2020-12-09 14:30:00,138.49,186.405,57.684,32.65 +2020-12-09 14:45:00,137.14,186.267,57.684,32.65 +2020-12-09 15:00:00,135.82,187.095,58.03,32.65 +2020-12-09 15:15:00,135.39,187.666,58.03,32.65 +2020-12-09 15:30:00,138.38,189.798,58.03,32.65 +2020-12-09 15:45:00,138.54,191.613,58.03,32.65 +2020-12-09 16:00:00,139.67,192.706,59.97,32.65 +2020-12-09 16:15:00,145.31,193.799,59.97,32.65 +2020-12-09 16:30:00,147.26,196.513,59.97,32.65 +2020-12-09 16:45:00,151.24,197.75099999999998,59.97,32.65 +2020-12-09 17:00:00,169.05,200.424,65.661,32.65 +2020-12-09 17:15:00,169.17,200.75400000000002,65.661,32.65 +2020-12-09 17:30:00,170.79,201.261,65.661,32.65 +2020-12-09 17:45:00,149.97,200.899,65.661,32.65 +2020-12-09 18:00:00,141.53,201.86599999999999,70.96300000000001,32.65 +2020-12-09 18:15:00,143.4,199.97400000000002,70.96300000000001,32.65 +2020-12-09 18:30:00,142.84,198.456,70.96300000000001,32.65 +2020-12-09 18:45:00,143.61,198.19400000000002,70.96300000000001,32.65 +2020-12-09 19:00:00,142.88,199.144,74.133,32.65 +2020-12-09 19:15:00,140.01,195.545,74.133,32.65 +2020-12-09 19:30:00,138.02,193.44799999999998,74.133,32.65 +2020-12-09 19:45:00,135.99,190.014,74.133,32.65 +2020-12-09 20:00:00,129.82,186.553,65.613,32.65 +2020-12-09 20:15:00,124.78,180.71400000000003,65.613,32.65 +2020-12-09 20:30:00,122.6,177.18400000000003,65.613,32.65 +2020-12-09 20:45:00,120.04,174.5,65.613,32.65 +2020-12-09 21:00:00,116.74,172.41,58.583,32.65 +2020-12-09 21:15:00,113.73,170.396,58.583,32.65 +2020-12-09 21:30:00,111.56,168.293,58.583,32.65 +2020-12-09 21:45:00,112.32,166.574,58.583,32.65 +2020-12-09 22:00:00,111.72,159.83700000000002,54.411,32.65 +2020-12-09 22:15:00,110.88,154.179,54.411,32.65 +2020-12-09 22:30:00,104.66,139.827,54.411,32.65 +2020-12-09 22:45:00,99.54,131.77,54.411,32.65 +2020-12-09 23:00:00,93.47,126.26100000000001,47.878,32.65 +2020-12-09 23:15:00,92.23,124.285,47.878,32.65 +2020-12-09 23:30:00,90.56,124.353,47.878,32.65 +2020-12-09 23:45:00,89.81,123.98700000000001,47.878,32.65 +2020-12-10 00:00:00,92.49,117.383,44.513000000000005,32.65 +2020-12-10 00:15:00,87.7,117.391,44.513000000000005,32.65 +2020-12-10 00:30:00,90.23,118.53200000000001,44.513000000000005,32.65 +2020-12-10 00:45:00,85.04,120.113,44.513000000000005,32.65 +2020-12-10 01:00:00,82.13,122.18,43.169,32.65 +2020-12-10 01:15:00,80.96,122.911,43.169,32.65 +2020-12-10 01:30:00,85.8,123.34,43.169,32.65 +2020-12-10 01:45:00,87.1,124.03299999999999,43.169,32.65 +2020-12-10 02:00:00,87.01,125.48899999999999,41.763999999999996,32.65 +2020-12-10 02:15:00,82.06,126.995,41.763999999999996,32.65 +2020-12-10 02:30:00,85.45,127.22399999999999,41.763999999999996,32.65 +2020-12-10 02:45:00,87.43,129.1,41.763999999999996,32.65 +2020-12-10 03:00:00,87.6,131.804,41.155,32.65 +2020-12-10 03:15:00,83.72,132.446,41.155,32.65 +2020-12-10 03:30:00,87.63,134.326,41.155,32.65 +2020-12-10 03:45:00,90.33,135.352,41.155,32.65 +2020-12-10 04:00:00,91.8,148.15,41.96,32.65 +2020-12-10 04:15:00,88.57,160.25,41.96,32.65 +2020-12-10 04:30:00,87.86,162.861,41.96,32.65 +2020-12-10 04:45:00,87.24,165.486,41.96,32.65 +2020-12-10 05:00:00,94.72,200.29,45.206,32.65 +2020-12-10 05:15:00,98.25,229.452,45.206,32.65 +2020-12-10 05:30:00,98.01,224.84900000000002,45.206,32.65 +2020-12-10 05:45:00,102.63,216.87900000000002,45.206,32.65 +2020-12-10 06:00:00,109.41,212.851,55.398999999999994,32.65 +2020-12-10 06:15:00,116.05,218.298,55.398999999999994,32.65 +2020-12-10 06:30:00,120.99,220.30900000000003,55.398999999999994,32.65 +2020-12-10 06:45:00,125.66,222.928,55.398999999999994,32.65 +2020-12-10 07:00:00,129.75,222.33,64.627,32.65 +2020-12-10 07:15:00,134.39,227.273,64.627,32.65 +2020-12-10 07:30:00,136.07,230.03400000000002,64.627,32.65 +2020-12-10 07:45:00,137.41,231.43200000000002,64.627,32.65 +2020-12-10 08:00:00,138.31,230.24200000000002,70.895,32.65 +2020-12-10 08:15:00,138.27,230.352,70.895,32.65 +2020-12-10 08:30:00,139.4,228.68900000000002,70.895,32.65 +2020-12-10 08:45:00,139.75,225.979,70.895,32.65 +2020-12-10 09:00:00,140.06,219.641,66.382,32.65 +2020-12-10 09:15:00,143.71,216.195,66.382,32.65 +2020-12-10 09:30:00,145.38,213.66,66.382,32.65 +2020-12-10 09:45:00,143.55,210.907,66.382,32.65 +2020-12-10 10:00:00,144.96,206.37400000000002,62.739,32.65 +2020-12-10 10:15:00,143.67,202.21099999999998,62.739,32.65 +2020-12-10 10:30:00,142.44,199.86,62.739,32.65 +2020-12-10 10:45:00,142.02,198.46400000000003,62.739,32.65 +2020-12-10 11:00:00,140.23,197.49099999999999,60.843,32.65 +2020-12-10 11:15:00,141.63,196.47299999999998,60.843,32.65 +2020-12-10 11:30:00,140.83,195.273,60.843,32.65 +2020-12-10 11:45:00,140.68,193.729,60.843,32.65 +2020-12-10 12:00:00,140.94,188.09900000000002,58.466,32.65 +2020-12-10 12:15:00,140.46,187.13,58.466,32.65 +2020-12-10 12:30:00,138.97,187.112,58.466,32.65 +2020-12-10 12:45:00,136.01,188.033,58.466,32.65 +2020-12-10 13:00:00,137.69,187.275,56.883,32.65 +2020-12-10 13:15:00,137.48,186.88400000000001,56.883,32.65 +2020-12-10 13:30:00,132.54,186.77,56.883,32.65 +2020-12-10 13:45:00,133.09,186.713,56.883,32.65 +2020-12-10 14:00:00,135.21,185.706,56.503,32.65 +2020-12-10 14:15:00,136.53,186.206,56.503,32.65 +2020-12-10 14:30:00,137.12,186.63,56.503,32.65 +2020-12-10 14:45:00,137.05,186.502,56.503,32.65 +2020-12-10 15:00:00,137.96,187.34900000000002,57.803999999999995,32.65 +2020-12-10 15:15:00,139.18,187.918,57.803999999999995,32.65 +2020-12-10 15:30:00,136.67,190.06900000000002,57.803999999999995,32.65 +2020-12-10 15:45:00,137.64,191.88400000000001,57.803999999999995,32.65 +2020-12-10 16:00:00,138.45,192.976,59.379,32.65 +2020-12-10 16:15:00,142.96,194.09,59.379,32.65 +2020-12-10 16:30:00,146.32,196.812,59.379,32.65 +2020-12-10 16:45:00,153.38,198.078,59.379,32.65 +2020-12-10 17:00:00,168.95,200.729,64.71600000000001,32.65 +2020-12-10 17:15:00,169.75,201.08599999999998,64.71600000000001,32.65 +2020-12-10 17:30:00,169.32,201.61,64.71600000000001,32.65 +2020-12-10 17:45:00,147.93,201.25599999999997,64.71600000000001,32.65 +2020-12-10 18:00:00,141.13,202.24400000000003,68.803,32.65 +2020-12-10 18:15:00,143.26,200.32,68.803,32.65 +2020-12-10 18:30:00,143.39,198.808,68.803,32.65 +2020-12-10 18:45:00,144.16,198.555,68.803,32.65 +2020-12-10 19:00:00,142.57,199.49099999999999,72.934,32.65 +2020-12-10 19:15:00,140.71,195.882,72.934,32.65 +2020-12-10 19:30:00,137.64,193.773,72.934,32.65 +2020-12-10 19:45:00,135.04,190.31400000000002,72.934,32.65 +2020-12-10 20:00:00,132.42,186.852,65.175,32.65 +2020-12-10 20:15:00,126.79,181.005,65.175,32.65 +2020-12-10 20:30:00,122.81,177.45,65.175,32.65 +2020-12-10 20:45:00,120.88,174.785,65.175,32.65 +2020-12-10 21:00:00,117.89,172.68099999999998,58.55,32.65 +2020-12-10 21:15:00,115.42,170.65,58.55,32.65 +2020-12-10 21:30:00,118.82,168.548,58.55,32.65 +2020-12-10 21:45:00,116.05,166.838,58.55,32.65 +2020-12-10 22:00:00,113.48,160.107,55.041000000000004,32.65 +2020-12-10 22:15:00,110.17,154.452,55.041000000000004,32.65 +2020-12-10 22:30:00,99.6,140.149,55.041000000000004,32.65 +2020-12-10 22:45:00,100.62,132.09799999999998,55.041000000000004,32.65 +2020-12-10 23:00:00,94.51,126.565,48.258,32.65 +2020-12-10 23:15:00,91.88,124.586,48.258,32.65 +2020-12-10 23:30:00,89.58,124.66799999999999,48.258,32.65 +2020-12-10 23:45:00,91.27,124.288,48.258,32.65 +2020-12-11 00:00:00,91.85,116.62899999999999,45.02,32.65 +2020-12-11 00:15:00,90.83,116.805,45.02,32.65 +2020-12-11 00:30:00,85.32,117.803,45.02,32.65 +2020-12-11 00:45:00,82.24,119.473,45.02,32.65 +2020-12-11 01:00:00,79.17,121.26299999999999,42.695,32.65 +2020-12-11 01:15:00,81.0,122.91799999999999,42.695,32.65 +2020-12-11 01:30:00,77.89,123.124,42.695,32.65 +2020-12-11 01:45:00,78.18,123.911,42.695,32.65 +2020-12-11 02:00:00,81.93,125.464,41.511,32.65 +2020-12-11 02:15:00,86.0,126.85700000000001,41.511,32.65 +2020-12-11 02:30:00,86.34,127.60600000000001,41.511,32.65 +2020-12-11 02:45:00,81.69,129.531,41.511,32.65 +2020-12-11 03:00:00,81.72,131.216,41.162,32.65 +2020-12-11 03:15:00,86.02,132.842,41.162,32.65 +2020-12-11 03:30:00,88.54,134.709,41.162,32.65 +2020-12-11 03:45:00,89.73,136.055,41.162,32.65 +2020-12-11 04:00:00,84.52,149.05200000000002,42.226000000000006,32.65 +2020-12-11 04:15:00,82.83,160.917,42.226000000000006,32.65 +2020-12-11 04:30:00,84.73,163.737,42.226000000000006,32.65 +2020-12-11 04:45:00,86.99,165.222,42.226000000000006,32.65 +2020-12-11 05:00:00,89.74,198.715,45.597,32.65 +2020-12-11 05:15:00,93.03,229.33,45.597,32.65 +2020-12-11 05:30:00,96.79,225.83599999999998,45.597,32.65 +2020-12-11 05:45:00,101.33,217.828,45.597,32.65 +2020-12-11 06:00:00,110.09,214.25900000000001,56.263999999999996,32.65 +2020-12-11 06:15:00,118.24,218.227,56.263999999999996,32.65 +2020-12-11 06:30:00,122.75,219.41400000000002,56.263999999999996,32.65 +2020-12-11 06:45:00,127.63,223.713,56.263999999999996,32.65 +2020-12-11 07:00:00,132.56,222.27200000000002,66.888,32.65 +2020-12-11 07:15:00,135.59,228.239,66.888,32.65 +2020-12-11 07:30:00,138.93,230.83900000000003,66.888,32.65 +2020-12-11 07:45:00,140.11,231.35,66.888,32.65 +2020-12-11 08:00:00,140.74,229.05599999999998,73.459,32.65 +2020-12-11 08:15:00,138.35,228.766,73.459,32.65 +2020-12-11 08:30:00,138.94,228.084,73.459,32.65 +2020-12-11 08:45:00,138.68,223.765,73.459,32.65 +2020-12-11 09:00:00,138.76,217.87099999999998,69.087,32.65 +2020-12-11 09:15:00,142.43,214.997,69.087,32.65 +2020-12-11 09:30:00,145.27,212.041,69.087,32.65 +2020-12-11 09:45:00,145.82,209.167,69.087,32.65 +2020-12-11 10:00:00,146.67,203.49599999999998,65.404,32.65 +2020-12-11 10:15:00,146.04,200.011,65.404,32.65 +2020-12-11 10:30:00,146.52,197.567,65.404,32.65 +2020-12-11 10:45:00,146.39,195.722,65.404,32.65 +2020-12-11 11:00:00,144.72,194.715,63.0,32.65 +2020-12-11 11:15:00,144.28,192.791,63.0,32.65 +2020-12-11 11:30:00,145.11,193.333,63.0,32.65 +2020-12-11 11:45:00,143.67,191.829,63.0,32.65 +2020-12-11 12:00:00,143.59,187.28,59.083,32.65 +2020-12-11 12:15:00,143.45,184.239,59.083,32.65 +2020-12-11 12:30:00,142.82,184.391,59.083,32.65 +2020-12-11 12:45:00,140.7,185.827,59.083,32.65 +2020-12-11 13:00:00,139.92,185.97299999999998,56.611999999999995,32.65 +2020-12-11 13:15:00,139.16,186.396,56.611999999999995,32.65 +2020-12-11 13:30:00,136.21,186.297,56.611999999999995,32.65 +2020-12-11 13:45:00,136.82,186.169,56.611999999999995,32.65 +2020-12-11 14:00:00,134.19,184.005,55.161,32.65 +2020-12-11 14:15:00,134.68,184.335,55.161,32.65 +2020-12-11 14:30:00,134.81,185.299,55.161,32.65 +2020-12-11 14:45:00,133.75,185.48,55.161,32.65 +2020-12-11 15:00:00,133.3,185.865,55.583,32.65 +2020-12-11 15:15:00,132.89,185.993,55.583,32.65 +2020-12-11 15:30:00,133.49,186.65400000000002,55.583,32.65 +2020-12-11 15:45:00,133.98,188.61599999999999,55.583,32.65 +2020-12-11 16:00:00,135.78,188.544,57.611999999999995,32.65 +2020-12-11 16:15:00,139.22,189.968,57.611999999999995,32.65 +2020-12-11 16:30:00,141.38,192.791,57.611999999999995,32.65 +2020-12-11 16:45:00,141.75,193.968,57.611999999999995,32.65 +2020-12-11 17:00:00,142.8,196.8,64.14,32.65 +2020-12-11 17:15:00,142.37,196.774,64.14,32.65 +2020-12-11 17:30:00,142.8,196.99900000000002,64.14,32.65 +2020-12-11 17:45:00,141.37,196.421,64.14,32.65 +2020-12-11 18:00:00,139.86,198.11599999999999,68.086,32.65 +2020-12-11 18:15:00,137.98,195.77200000000002,68.086,32.65 +2020-12-11 18:30:00,137.12,194.65900000000002,68.086,32.65 +2020-12-11 18:45:00,137.09,194.41099999999997,68.086,32.65 +2020-12-11 19:00:00,135.72,196.236,69.915,32.65 +2020-12-11 19:15:00,133.39,193.97799999999998,69.915,32.65 +2020-12-11 19:30:00,131.85,191.445,69.915,32.65 +2020-12-11 19:45:00,128.74,187.503,69.915,32.65 +2020-12-11 20:00:00,123.92,184.093,61.695,32.65 +2020-12-11 20:15:00,119.13,178.245,61.695,32.65 +2020-12-11 20:30:00,116.21,174.625,61.695,32.65 +2020-12-11 20:45:00,114.15,172.53400000000002,61.695,32.65 +2020-12-11 21:00:00,114.45,170.922,56.041000000000004,32.65 +2020-12-11 21:15:00,112.95,169.31,56.041000000000004,32.65 +2020-12-11 21:30:00,111.79,167.25400000000002,56.041000000000004,32.65 +2020-12-11 21:45:00,111.52,166.093,56.041000000000004,32.65 +2020-12-11 22:00:00,100.51,160.34799999999998,51.888999999999996,32.65 +2020-12-11 22:15:00,97.98,154.564,51.888999999999996,32.65 +2020-12-11 22:30:00,92.39,146.664,51.888999999999996,32.65 +2020-12-11 22:45:00,92.97,142.139,51.888999999999996,32.65 +2020-12-11 23:00:00,94.85,136.137,45.787,32.65 +2020-12-11 23:15:00,93.72,132.213,45.787,32.65 +2020-12-11 23:30:00,88.87,130.852,45.787,32.65 +2020-12-11 23:45:00,84.74,129.795,45.787,32.65 +2020-12-12 00:00:00,84.97,114.07600000000001,41.815,32.468 +2020-12-12 00:15:00,83.87,109.97399999999999,41.815,32.468 +2020-12-12 00:30:00,79.35,112.294,41.815,32.468 +2020-12-12 00:45:00,76.4,114.661,41.815,32.468 +2020-12-12 01:00:00,78.93,117.113,38.645,32.468 +2020-12-12 01:15:00,79.22,117.779,38.645,32.468 +2020-12-12 01:30:00,74.91,117.477,38.645,32.468 +2020-12-12 01:45:00,70.19,118.03,38.645,32.468 +2020-12-12 02:00:00,69.07,120.29,36.696,32.468 +2020-12-12 02:15:00,68.62,121.31700000000001,36.696,32.468 +2020-12-12 02:30:00,76.58,120.963,36.696,32.468 +2020-12-12 02:45:00,71.16,122.986,36.696,32.468 +2020-12-12 03:00:00,72.83,125.289,35.42,32.468 +2020-12-12 03:15:00,75.73,125.73200000000001,35.42,32.468 +2020-12-12 03:30:00,75.68,126.01299999999999,35.42,32.468 +2020-12-12 03:45:00,71.18,127.463,35.42,32.468 +2020-12-12 04:00:00,69.45,136.298,35.167,32.468 +2020-12-12 04:15:00,68.25,145.624,35.167,32.468 +2020-12-12 04:30:00,69.44,146.27200000000002,35.167,32.468 +2020-12-12 04:45:00,69.89,147.263,35.167,32.468 +2020-12-12 05:00:00,71.14,164.803,35.311,32.468 +2020-12-12 05:15:00,71.23,176.438,35.311,32.468 +2020-12-12 05:30:00,71.29,173.31400000000002,35.311,32.468 +2020-12-12 05:45:00,72.91,170.747,35.311,32.468 +2020-12-12 06:00:00,74.72,185.908,37.117,32.468 +2020-12-12 06:15:00,76.39,206.178,37.117,32.468 +2020-12-12 06:30:00,78.26,202.05200000000002,37.117,32.468 +2020-12-12 06:45:00,80.93,197.43599999999998,37.117,32.468 +2020-12-12 07:00:00,84.33,192.245,40.948,32.468 +2020-12-12 07:15:00,86.73,196.97,40.948,32.468 +2020-12-12 07:30:00,90.02,202.268,40.948,32.468 +2020-12-12 07:45:00,93.49,206.74400000000003,40.948,32.468 +2020-12-12 08:00:00,95.85,208.5,44.903,32.468 +2020-12-12 08:15:00,97.55,211.82299999999998,44.903,32.468 +2020-12-12 08:30:00,99.61,212.753,44.903,32.468 +2020-12-12 08:45:00,103.07,211.549,44.903,32.468 +2020-12-12 09:00:00,104.78,207.393,46.283,32.468 +2020-12-12 09:15:00,105.94,205.275,46.283,32.468 +2020-12-12 09:30:00,106.91,203.227,46.283,32.468 +2020-12-12 09:45:00,107.66,200.5,46.283,32.468 +2020-12-12 10:00:00,108.32,195.109,44.103,32.468 +2020-12-12 10:15:00,109.17,191.769,44.103,32.468 +2020-12-12 10:30:00,109.72,189.468,44.103,32.468 +2020-12-12 10:45:00,110.26,188.91,44.103,32.468 +2020-12-12 11:00:00,111.78,188.105,42.373999999999995,32.468 +2020-12-12 11:15:00,113.31,185.49,42.373999999999995,32.468 +2020-12-12 11:30:00,113.65,184.924,42.373999999999995,32.468 +2020-12-12 11:45:00,113.32,182.46900000000002,42.373999999999995,32.468 +2020-12-12 12:00:00,111.9,177.05200000000002,39.937,32.468 +2020-12-12 12:15:00,111.01,174.662,39.937,32.468 +2020-12-12 12:30:00,108.83,175.142,39.937,32.468 +2020-12-12 12:45:00,106.63,175.82299999999998,39.937,32.468 +2020-12-12 13:00:00,103.27,175.53900000000002,37.138000000000005,32.468 +2020-12-12 13:15:00,101.79,173.928,37.138000000000005,32.468 +2020-12-12 13:30:00,101.3,173.377,37.138000000000005,32.468 +2020-12-12 13:45:00,101.1,173.704,37.138000000000005,32.468 +2020-12-12 14:00:00,100.88,172.74400000000003,36.141999999999996,32.468 +2020-12-12 14:15:00,100.41,172.542,36.141999999999996,32.468 +2020-12-12 14:30:00,100.19,171.72799999999998,36.141999999999996,32.468 +2020-12-12 14:45:00,99.5,172.138,36.141999999999996,32.468 +2020-12-12 15:00:00,98.71,173.196,37.964,32.468 +2020-12-12 15:15:00,98.98,174.12,37.964,32.468 +2020-12-12 15:30:00,99.94,176.325,37.964,32.468 +2020-12-12 15:45:00,100.8,178.297,37.964,32.468 +2020-12-12 16:00:00,104.51,177.041,40.699,32.468 +2020-12-12 16:15:00,107.74,179.368,40.699,32.468 +2020-12-12 16:30:00,108.74,182.148,40.699,32.468 +2020-12-12 16:45:00,109.29,184.231,40.699,32.468 +2020-12-12 17:00:00,110.17,186.467,46.216,32.468 +2020-12-12 17:15:00,110.88,188.104,46.216,32.468 +2020-12-12 17:30:00,111.64,188.24900000000002,46.216,32.468 +2020-12-12 17:45:00,112.51,187.28099999999998,46.216,32.468 +2020-12-12 18:00:00,112.37,188.514,51.123999999999995,32.468 +2020-12-12 18:15:00,112.02,187.929,51.123999999999995,32.468 +2020-12-12 18:30:00,111.33,188.148,51.123999999999995,32.468 +2020-12-12 18:45:00,111.02,184.58,51.123999999999995,32.468 +2020-12-12 19:00:00,110.03,187.303,52.336000000000006,32.468 +2020-12-12 19:15:00,108.32,184.541,52.336000000000006,32.468 +2020-12-12 19:30:00,107.24,182.74400000000003,52.336000000000006,32.468 +2020-12-12 19:45:00,106.61,178.607,52.336000000000006,32.468 +2020-12-12 20:00:00,102.62,177.35299999999998,48.825,32.468 +2020-12-12 20:15:00,97.95,173.604,48.825,32.468 +2020-12-12 20:30:00,96.03,169.61,48.825,32.468 +2020-12-12 20:45:00,94.62,167.145,48.825,32.468 +2020-12-12 21:00:00,92.77,167.74599999999998,43.729,32.468 +2020-12-12 21:15:00,90.0,166.553,43.729,32.468 +2020-12-12 21:30:00,87.78,165.718,43.729,32.468 +2020-12-12 21:45:00,87.21,164.16,43.729,32.468 +2020-12-12 22:00:00,86.33,159.75799999999998,44.126000000000005,32.468 +2020-12-12 22:15:00,84.41,156.444,44.126000000000005,32.468 +2020-12-12 22:30:00,82.37,154.756,44.126000000000005,32.468 +2020-12-12 22:45:00,80.33,152.092,44.126000000000005,32.468 +2020-12-12 23:00:00,76.41,148.47299999999998,38.169000000000004,32.468 +2020-12-12 23:15:00,74.99,142.928,38.169000000000004,32.468 +2020-12-12 23:30:00,72.58,139.857,38.169000000000004,32.468 +2020-12-12 23:45:00,70.05,136.382,38.169000000000004,32.468 +2020-12-13 00:00:00,67.83,114.786,35.232,32.468 +2020-12-13 00:15:00,65.77,110.337,35.232,32.468 +2020-12-13 00:30:00,64.76,112.277,35.232,32.468 +2020-12-13 00:45:00,63.83,115.316,35.232,32.468 +2020-12-13 01:00:00,62.51,117.65799999999999,31.403000000000002,32.468 +2020-12-13 01:15:00,61.48,119.354,31.403000000000002,32.468 +2020-12-13 01:30:00,61.2,119.575,31.403000000000002,32.468 +2020-12-13 01:45:00,60.78,119.804,31.403000000000002,32.468 +2020-12-13 02:00:00,59.95,121.339,30.69,32.468 +2020-12-13 02:15:00,59.57,121.521,30.69,32.468 +2020-12-13 02:30:00,59.18,122.01899999999999,30.69,32.468 +2020-12-13 02:45:00,59.27,124.5,30.69,32.468 +2020-12-13 03:00:00,59.3,127.10700000000001,29.516,32.468 +2020-12-13 03:15:00,59.76,127.05799999999999,29.516,32.468 +2020-12-13 03:30:00,60.59,128.73,29.516,32.468 +2020-12-13 03:45:00,60.29,130.10399999999998,29.516,32.468 +2020-12-13 04:00:00,59.87,138.665,29.148000000000003,32.468 +2020-12-13 04:15:00,59.82,146.981,29.148000000000003,32.468 +2020-12-13 04:30:00,60.61,147.683,29.148000000000003,32.468 +2020-12-13 04:45:00,60.83,148.946,29.148000000000003,32.468 +2020-12-13 05:00:00,61.5,162.963,28.706,32.468 +2020-12-13 05:15:00,62.25,172.148,28.706,32.468 +2020-12-13 05:30:00,63.05,168.88299999999998,28.706,32.468 +2020-12-13 05:45:00,63.76,166.579,28.706,32.468 +2020-12-13 06:00:00,64.65,181.65599999999998,28.771,32.468 +2020-12-13 06:15:00,65.56,200.222,28.771,32.468 +2020-12-13 06:30:00,65.84,195.041,28.771,32.468 +2020-12-13 06:45:00,67.8,189.417,28.771,32.468 +2020-12-13 07:00:00,69.8,186.665,31.39,32.468 +2020-12-13 07:15:00,72.1,190.542,31.39,32.468 +2020-12-13 07:30:00,74.71,194.625,31.39,32.468 +2020-12-13 07:45:00,76.75,198.34400000000002,31.39,32.468 +2020-12-13 08:00:00,78.86,201.938,34.972,32.468 +2020-12-13 08:15:00,81.37,205.171,34.972,32.468 +2020-12-13 08:30:00,82.52,207.74599999999998,34.972,32.468 +2020-12-13 08:45:00,84.69,208.512,34.972,32.468 +2020-12-13 09:00:00,85.83,203.933,36.709,32.468 +2020-12-13 09:15:00,87.39,202.365,36.709,32.468 +2020-12-13 09:30:00,88.29,200.173,36.709,32.468 +2020-12-13 09:45:00,89.37,197.327,36.709,32.468 +2020-12-13 10:00:00,91.42,194.422,35.812,32.468 +2020-12-13 10:15:00,92.02,191.58900000000003,35.812,32.468 +2020-12-13 10:30:00,93.21,189.86599999999999,35.812,32.468 +2020-12-13 10:45:00,95.56,187.44,35.812,32.468 +2020-12-13 11:00:00,97.35,187.52,36.746,32.468 +2020-12-13 11:15:00,99.58,185.02,36.746,32.468 +2020-12-13 11:30:00,99.04,183.602,36.746,32.468 +2020-12-13 11:45:00,101.44,181.74200000000002,36.746,32.468 +2020-12-13 12:00:00,102.29,175.783,35.048,32.468 +2020-12-13 12:15:00,100.68,175.27200000000002,35.048,32.468 +2020-12-13 12:30:00,97.69,174.334,35.048,32.468 +2020-12-13 12:45:00,93.5,174.06099999999998,35.048,32.468 +2020-12-13 13:00:00,93.18,173.04,29.987,32.468 +2020-12-13 13:15:00,91.25,174.38400000000001,29.987,32.468 +2020-12-13 13:30:00,90.27,173.607,29.987,32.468 +2020-12-13 13:45:00,90.93,173.293,29.987,32.468 +2020-12-13 14:00:00,91.2,172.57299999999998,27.21,32.468 +2020-12-13 14:15:00,91.53,173.56,27.21,32.468 +2020-12-13 14:30:00,92.77,173.96400000000003,27.21,32.468 +2020-12-13 14:45:00,92.68,173.96599999999998,27.21,32.468 +2020-12-13 15:00:00,92.95,173.54,27.726999999999997,32.468 +2020-12-13 15:15:00,93.32,175.192,27.726999999999997,32.468 +2020-12-13 15:30:00,93.65,177.99,27.726999999999997,32.468 +2020-12-13 15:45:00,94.82,180.645,27.726999999999997,32.468 +2020-12-13 16:00:00,97.84,181.21,32.23,32.468 +2020-12-13 16:15:00,102.34,182.666,32.23,32.468 +2020-12-13 16:30:00,104.88,185.718,32.23,32.468 +2020-12-13 16:45:00,106.38,187.96200000000002,32.23,32.468 +2020-12-13 17:00:00,108.65,190.179,42.016999999999996,32.468 +2020-12-13 17:15:00,110.19,191.53400000000002,42.016999999999996,32.468 +2020-12-13 17:30:00,112.07,191.988,42.016999999999996,32.468 +2020-12-13 17:45:00,113.2,193.269,42.016999999999996,32.468 +2020-12-13 18:00:00,112.55,193.99099999999999,49.338,32.468 +2020-12-13 18:15:00,110.56,194.68200000000002,49.338,32.468 +2020-12-13 18:30:00,110.25,192.84799999999998,49.338,32.468 +2020-12-13 18:45:00,110.1,191.11900000000003,49.338,32.468 +2020-12-13 19:00:00,108.26,193.512,52.369,32.468 +2020-12-13 19:15:00,106.2,191.321,52.369,32.468 +2020-12-13 19:30:00,105.15,189.34799999999998,52.369,32.468 +2020-12-13 19:45:00,104.42,186.611,52.369,32.468 +2020-12-13 20:00:00,108.33,185.328,50.405,32.468 +2020-12-13 20:15:00,108.05,182.543,50.405,32.468 +2020-12-13 20:30:00,103.92,179.755,50.405,32.468 +2020-12-13 20:45:00,96.6,176.112,50.405,32.468 +2020-12-13 21:00:00,98.84,174.137,46.235,32.468 +2020-12-13 21:15:00,101.2,172.30700000000002,46.235,32.468 +2020-12-13 21:30:00,100.09,171.757,46.235,32.468 +2020-12-13 21:45:00,99.54,170.34599999999998,46.235,32.468 +2020-12-13 22:00:00,91.06,164.78900000000002,46.861000000000004,32.468 +2020-12-13 22:15:00,90.94,160.716,46.861000000000004,32.468 +2020-12-13 22:30:00,85.27,155.94,46.861000000000004,32.468 +2020-12-13 22:45:00,86.11,152.435,46.861000000000004,32.468 +2020-12-13 23:00:00,87.91,146.025,41.302,32.468 +2020-12-13 23:15:00,88.21,142.32299999999998,41.302,32.468 +2020-12-13 23:30:00,84.64,140.062,41.302,32.468 +2020-12-13 23:45:00,77.19,137.455,41.302,32.468 +2020-12-14 00:00:00,73.85,119.242,37.164,32.65 +2020-12-14 00:15:00,76.66,117.71700000000001,37.164,32.65 +2020-12-14 00:30:00,78.78,119.77600000000001,37.164,32.65 +2020-12-14 00:45:00,77.4,122.26100000000001,37.164,32.65 +2020-12-14 01:00:00,68.45,124.65100000000001,34.994,32.65 +2020-12-14 01:15:00,73.71,125.82,34.994,32.65 +2020-12-14 01:30:00,75.05,126.103,34.994,32.65 +2020-12-14 01:45:00,74.17,126.43,34.994,32.65 +2020-12-14 02:00:00,71.84,127.964,34.571,32.65 +2020-12-14 02:15:00,74.01,129.599,34.571,32.65 +2020-12-14 02:30:00,74.73,130.429,34.571,32.65 +2020-12-14 02:45:00,74.65,132.29399999999998,34.571,32.65 +2020-12-14 03:00:00,70.83,136.15200000000002,33.934,32.65 +2020-12-14 03:15:00,76.74,137.769,33.934,32.65 +2020-12-14 03:30:00,78.07,139.173,33.934,32.65 +2020-12-14 03:45:00,77.08,139.999,33.934,32.65 +2020-12-14 04:00:00,73.01,152.859,34.107,32.65 +2020-12-14 04:15:00,78.81,165.28900000000002,34.107,32.65 +2020-12-14 04:30:00,80.92,168.162,34.107,32.65 +2020-12-14 04:45:00,82.24,169.58599999999998,34.107,32.65 +2020-12-14 05:00:00,82.67,199.233,39.575,32.65 +2020-12-14 05:15:00,82.48,228.407,39.575,32.65 +2020-12-14 05:30:00,87.89,225.44099999999997,39.575,32.65 +2020-12-14 05:45:00,93.59,217.542,39.575,32.65 +2020-12-14 06:00:00,104.33,214.821,56.156000000000006,32.65 +2020-12-14 06:15:00,110.84,218.653,56.156000000000006,32.65 +2020-12-14 06:30:00,116.28,221.476,56.156000000000006,32.65 +2020-12-14 06:45:00,125.61,224.635,56.156000000000006,32.65 +2020-12-14 07:00:00,128.28,224.24200000000002,67.926,32.65 +2020-12-14 07:15:00,129.58,229.359,67.926,32.65 +2020-12-14 07:30:00,133.95,232.672,67.926,32.65 +2020-12-14 07:45:00,132.49,233.838,67.926,32.65 +2020-12-14 08:00:00,136.16,232.574,72.58,32.65 +2020-12-14 08:15:00,133.51,233.67,72.58,32.65 +2020-12-14 08:30:00,136.78,232.19,72.58,32.65 +2020-12-14 08:45:00,132.47,229.61599999999999,72.58,32.65 +2020-12-14 09:00:00,134.5,224.016,66.984,32.65 +2020-12-14 09:15:00,136.6,218.998,66.984,32.65 +2020-12-14 09:30:00,141.47,215.822,66.984,32.65 +2020-12-14 09:45:00,137.79,213.25799999999998,66.984,32.65 +2020-12-14 10:00:00,135.32,209.27,63.158,32.65 +2020-12-14 10:15:00,131.67,206.097,63.158,32.65 +2020-12-14 10:30:00,131.48,203.485,63.158,32.65 +2020-12-14 10:45:00,133.72,201.791,63.158,32.65 +2020-12-14 11:00:00,129.21,199.26,61.141000000000005,32.65 +2020-12-14 11:15:00,129.73,198.532,61.141000000000005,32.65 +2020-12-14 11:30:00,131.24,198.482,61.141000000000005,32.65 +2020-12-14 11:45:00,132.76,196.21599999999998,61.141000000000005,32.65 +2020-12-14 12:00:00,129.52,191.954,57.961000000000006,32.65 +2020-12-14 12:15:00,133.74,191.457,57.961000000000006,32.65 +2020-12-14 12:30:00,133.03,190.775,57.961000000000006,32.65 +2020-12-14 12:45:00,135.84,192.012,57.961000000000006,32.65 +2020-12-14 13:00:00,131.88,191.54,56.843,32.65 +2020-12-14 13:15:00,130.95,191.511,56.843,32.65 +2020-12-14 13:30:00,128.48,190.205,56.843,32.65 +2020-12-14 13:45:00,128.95,189.919,56.843,32.65 +2020-12-14 14:00:00,123.91,188.565,55.992,32.65 +2020-12-14 14:15:00,124.92,188.925,55.992,32.65 +2020-12-14 14:30:00,124.35,188.81599999999997,55.992,32.65 +2020-12-14 14:45:00,126.22,188.80700000000002,55.992,32.65 +2020-12-14 15:00:00,127.2,190.158,57.523,32.65 +2020-12-14 15:15:00,128.82,190.382,57.523,32.65 +2020-12-14 15:30:00,126.77,192.38400000000001,57.523,32.65 +2020-12-14 15:45:00,128.99,194.595,57.523,32.65 +2020-12-14 16:00:00,133.74,195.34400000000002,59.471000000000004,32.65 +2020-12-14 16:15:00,135.18,196.06599999999997,59.471000000000004,32.65 +2020-12-14 16:30:00,136.84,198.168,59.471000000000004,32.65 +2020-12-14 16:45:00,137.31,199.264,59.471000000000004,32.65 +2020-12-14 17:00:00,139.82,201.285,65.066,32.65 +2020-12-14 17:15:00,139.13,201.725,65.066,32.65 +2020-12-14 17:30:00,139.59,201.65200000000002,65.066,32.65 +2020-12-14 17:45:00,139.83,201.465,65.066,32.65 +2020-12-14 18:00:00,137.48,202.62900000000002,69.581,32.65 +2020-12-14 18:15:00,135.23,201.102,69.581,32.65 +2020-12-14 18:30:00,134.3,199.922,69.581,32.65 +2020-12-14 18:45:00,134.53,198.91299999999998,69.581,32.65 +2020-12-14 19:00:00,132.27,199.696,73.771,32.65 +2020-12-14 19:15:00,137.65,196.335,73.771,32.65 +2020-12-14 19:30:00,137.55,194.843,73.771,32.65 +2020-12-14 19:45:00,136.21,191.26,73.771,32.65 +2020-12-14 20:00:00,121.11,187.632,65.035,32.65 +2020-12-14 20:15:00,117.82,182.38099999999997,65.035,32.65 +2020-12-14 20:30:00,115.14,177.72400000000002,65.035,32.65 +2020-12-14 20:45:00,115.32,175.71200000000002,65.035,32.65 +2020-12-14 21:00:00,109.37,174.25,58.7,32.65 +2020-12-14 21:15:00,108.96,171.238,58.7,32.65 +2020-12-14 21:30:00,104.73,169.86900000000003,58.7,32.65 +2020-12-14 21:45:00,104.04,167.965,58.7,32.65 +2020-12-14 22:00:00,99.84,159.553,53.888000000000005,32.65 +2020-12-14 22:15:00,96.41,154.173,53.888000000000005,32.65 +2020-12-14 22:30:00,96.15,139.96200000000002,53.888000000000005,32.65 +2020-12-14 22:45:00,97.48,131.666,53.888000000000005,32.65 +2020-12-14 23:00:00,92.99,126.00399999999999,45.501999999999995,32.65 +2020-12-14 23:15:00,90.86,124.936,45.501999999999995,32.65 +2020-12-14 23:30:00,86.06,125.43,45.501999999999995,32.65 +2020-12-14 23:45:00,88.58,125.434,45.501999999999995,32.65 +2020-12-15 00:00:00,83.52,115.656,43.537,32.65 +2020-12-15 00:15:00,83.19,115.315,43.537,32.65 +2020-12-15 00:30:00,81.66,116.205,43.537,32.65 +2020-12-15 00:45:00,84.41,117.449,43.537,32.65 +2020-12-15 01:00:00,80.92,119.626,41.854,32.65 +2020-12-15 01:15:00,78.66,120.473,41.854,32.65 +2020-12-15 01:30:00,78.58,120.975,41.854,32.65 +2020-12-15 01:45:00,80.79,121.521,41.854,32.65 +2020-12-15 02:00:00,78.33,123.03200000000001,40.321,32.65 +2020-12-15 02:15:00,79.13,124.414,40.321,32.65 +2020-12-15 02:30:00,79.46,124.492,40.321,32.65 +2020-12-15 02:45:00,81.65,126.29299999999999,40.321,32.65 +2020-12-15 03:00:00,80.09,128.789,39.632,32.65 +2020-12-15 03:15:00,79.26,129.722,39.632,32.65 +2020-12-15 03:30:00,80.46,131.651,39.632,32.65 +2020-12-15 03:45:00,82.17,132.52100000000002,39.632,32.65 +2020-12-15 04:00:00,82.38,145.236,40.183,32.65 +2020-12-15 04:15:00,81.57,157.416,40.183,32.65 +2020-12-15 04:30:00,85.91,159.6,40.183,32.65 +2020-12-15 04:45:00,85.18,162.086,40.183,32.65 +2020-12-15 05:00:00,85.38,196.30599999999998,43.945,32.65 +2020-12-15 05:15:00,84.75,225.167,43.945,32.65 +2020-12-15 05:30:00,94.66,220.77900000000002,43.945,32.65 +2020-12-15 05:45:00,104.6,212.74599999999998,43.945,32.65 +2020-12-15 06:00:00,114.07,208.825,56.048,32.65 +2020-12-15 06:15:00,115.06,214.197,56.048,32.65 +2020-12-15 06:30:00,115.45,216.41,56.048,32.65 +2020-12-15 06:45:00,118.13,219.02599999999998,56.048,32.65 +2020-12-15 07:00:00,127.23,218.861,65.74,32.65 +2020-12-15 07:15:00,130.09,223.575,65.74,32.65 +2020-12-15 07:30:00,130.79,226.06599999999997,65.74,32.65 +2020-12-15 07:45:00,134.25,227.21,65.74,32.65 +2020-12-15 08:00:00,137.81,226.11599999999999,72.757,32.65 +2020-12-15 08:15:00,135.91,225.97099999999998,72.757,32.65 +2020-12-15 08:30:00,136.55,224.303,72.757,32.65 +2020-12-15 08:45:00,137.31,221.18200000000002,72.757,32.65 +2020-12-15 09:00:00,136.97,214.54,67.692,32.65 +2020-12-15 09:15:00,141.95,211.025,67.692,32.65 +2020-12-15 09:30:00,142.11,208.47,67.692,32.65 +2020-12-15 09:45:00,142.8,205.74200000000002,67.692,32.65 +2020-12-15 10:00:00,141.36,201.498,63.506,32.65 +2020-12-15 10:15:00,141.94,197.08599999999998,63.506,32.65 +2020-12-15 10:30:00,141.49,194.91400000000002,63.506,32.65 +2020-12-15 10:45:00,143.1,193.605,63.506,32.65 +2020-12-15 11:00:00,141.22,192.74099999999999,60.758,32.65 +2020-12-15 11:15:00,142.97,191.713,60.758,32.65 +2020-12-15 11:30:00,142.93,190.668,60.758,32.65 +2020-12-15 11:45:00,141.87,189.172,60.758,32.65 +2020-12-15 12:00:00,141.11,183.533,57.519,32.65 +2020-12-15 12:15:00,141.16,182.61700000000002,57.519,32.65 +2020-12-15 12:30:00,140.9,182.8,57.519,32.65 +2020-12-15 12:45:00,138.4,183.675,57.519,32.65 +2020-12-15 13:00:00,133.81,182.6,56.46,32.65 +2020-12-15 13:15:00,131.95,182.195,56.46,32.65 +2020-12-15 13:30:00,129.26,181.915,56.46,32.65 +2020-12-15 13:45:00,129.67,181.796,56.46,32.65 +2020-12-15 14:00:00,123.7,180.81599999999997,56.207,32.65 +2020-12-15 14:15:00,124.77,181.243,56.207,32.65 +2020-12-15 14:30:00,124.87,181.71400000000003,56.207,32.65 +2020-12-15 14:45:00,125.31,181.653,56.207,32.65 +2020-12-15 15:00:00,127.76,182.535,57.391999999999996,32.65 +2020-12-15 15:15:00,126.63,183.02599999999998,57.391999999999996,32.65 +2020-12-15 15:30:00,126.46,185.03799999999998,57.391999999999996,32.65 +2020-12-15 15:45:00,128.96,186.55,57.391999999999996,32.65 +2020-12-15 16:00:00,131.98,188.59799999999998,59.955,32.65 +2020-12-15 16:15:00,135.15,189.975,59.955,32.65 +2020-12-15 16:30:00,137.94,192.37400000000002,59.955,32.65 +2020-12-15 16:45:00,139.15,193.635,59.955,32.65 +2020-12-15 17:00:00,141.27,196.472,67.063,32.65 +2020-12-15 17:15:00,141.7,197.14700000000002,67.063,32.65 +2020-12-15 17:30:00,143.06,197.852,67.063,32.65 +2020-12-15 17:45:00,142.57,197.611,67.063,32.65 +2020-12-15 18:00:00,140.73,198.834,71.477,32.65 +2020-12-15 18:15:00,139.06,197.072,71.477,32.65 +2020-12-15 18:30:00,137.45,195.517,71.477,32.65 +2020-12-15 18:45:00,137.98,195.32299999999998,71.477,32.65 +2020-12-15 19:00:00,135.69,196.59099999999998,74.32,32.65 +2020-12-15 19:15:00,134.51,193.03599999999997,74.32,32.65 +2020-12-15 19:30:00,132.57,191.079,74.32,32.65 +2020-12-15 19:45:00,131.33,187.28799999999998,74.32,32.65 +2020-12-15 20:00:00,123.93,183.83700000000002,66.157,32.65 +2020-12-15 20:15:00,121.36,178.165,66.157,32.65 +2020-12-15 20:30:00,114.46,174.50599999999997,66.157,32.65 +2020-12-15 20:45:00,115.6,171.75400000000002,66.157,32.65 +2020-12-15 21:00:00,115.82,169.782,59.806000000000004,32.65 +2020-12-15 21:15:00,114.44,167.699,59.806000000000004,32.65 +2020-12-15 21:30:00,113.31,165.55900000000003,59.806000000000004,32.65 +2020-12-15 21:45:00,103.61,163.97,59.806000000000004,32.65 +2020-12-15 22:00:00,102.01,157.408,54.785,32.65 +2020-12-15 22:15:00,98.37,151.947,54.785,32.65 +2020-12-15 22:30:00,92.73,137.92,54.785,32.65 +2020-12-15 22:45:00,94.84,130.05100000000002,54.785,32.65 +2020-12-15 23:00:00,93.0,124.61200000000001,47.176,32.65 +2020-12-15 23:15:00,94.63,122.538,47.176,32.65 +2020-12-15 23:30:00,87.81,122.829,47.176,32.65 +2020-12-15 23:45:00,85.28,122.35,47.176,32.65 +2020-12-16 00:00:00,80.62,115.92399999999999,43.42,32.65 +2020-12-16 00:15:00,83.74,115.554,43.42,32.65 +2020-12-16 00:30:00,81.51,116.43799999999999,43.42,32.65 +2020-12-16 00:45:00,80.18,117.663,43.42,32.65 +2020-12-16 01:00:00,69.83,119.867,40.869,32.65 +2020-12-16 01:15:00,80.0,120.713,40.869,32.65 +2020-12-16 01:30:00,80.76,121.22,40.869,32.65 +2020-12-16 01:45:00,80.19,121.75399999999999,40.869,32.65 +2020-12-16 02:00:00,74.32,123.281,39.541,32.65 +2020-12-16 02:15:00,78.32,124.664,39.541,32.65 +2020-12-16 02:30:00,77.27,124.74799999999999,39.541,32.65 +2020-12-16 02:45:00,80.86,126.546,39.541,32.65 +2020-12-16 03:00:00,75.94,129.032,39.052,32.65 +2020-12-16 03:15:00,79.31,129.991,39.052,32.65 +2020-12-16 03:30:00,79.64,131.922,39.052,32.65 +2020-12-16 03:45:00,82.22,132.792,39.052,32.65 +2020-12-16 04:00:00,79.05,145.48,40.36,32.65 +2020-12-16 04:15:00,81.08,157.658,40.36,32.65 +2020-12-16 04:30:00,85.18,159.833,40.36,32.65 +2020-12-16 04:45:00,82.1,162.319,40.36,32.65 +2020-12-16 05:00:00,83.77,196.507,43.133,32.65 +2020-12-16 05:15:00,87.36,225.327,43.133,32.65 +2020-12-16 05:30:00,91.05,220.956,43.133,32.65 +2020-12-16 05:45:00,95.36,212.94299999999998,43.133,32.65 +2020-12-16 06:00:00,105.17,209.046,54.953,32.65 +2020-12-16 06:15:00,108.16,214.421,54.953,32.65 +2020-12-16 06:30:00,114.63,216.671,54.953,32.65 +2020-12-16 06:45:00,117.72,219.325,54.953,32.65 +2020-12-16 07:00:00,127.82,219.166,66.566,32.65 +2020-12-16 07:15:00,130.45,223.882,66.566,32.65 +2020-12-16 07:30:00,131.15,226.372,66.566,32.65 +2020-12-16 07:45:00,132.85,227.511,66.566,32.65 +2020-12-16 08:00:00,137.7,226.423,72.902,32.65 +2020-12-16 08:15:00,135.94,226.269,72.902,32.65 +2020-12-16 08:30:00,136.44,224.597,72.902,32.65 +2020-12-16 08:45:00,137.62,221.44799999999998,72.902,32.65 +2020-12-16 09:00:00,137.95,214.785,68.465,32.65 +2020-12-16 09:15:00,139.43,211.27599999999998,68.465,32.65 +2020-12-16 09:30:00,140.51,208.729,68.465,32.65 +2020-12-16 09:45:00,139.57,205.99,68.465,32.65 +2020-12-16 10:00:00,137.58,201.74,63.625,32.65 +2020-12-16 10:15:00,137.97,197.31400000000002,63.625,32.65 +2020-12-16 10:30:00,140.2,195.123,63.625,32.65 +2020-12-16 10:45:00,138.04,193.81,63.625,32.65 +2020-12-16 11:00:00,138.03,192.929,61.628,32.65 +2020-12-16 11:15:00,136.32,191.89,61.628,32.65 +2020-12-16 11:30:00,136.53,190.845,61.628,32.65 +2020-12-16 11:45:00,136.0,189.34599999999998,61.628,32.65 +2020-12-16 12:00:00,135.94,183.708,58.708999999999996,32.65 +2020-12-16 12:15:00,135.61,182.80599999999998,58.708999999999996,32.65 +2020-12-16 12:30:00,133.9,182.998,58.708999999999996,32.65 +2020-12-16 12:45:00,135.77,183.877,58.708999999999996,32.65 +2020-12-16 13:00:00,133.66,182.77900000000002,57.373000000000005,32.65 +2020-12-16 13:15:00,133.97,182.372,57.373000000000005,32.65 +2020-12-16 13:30:00,134.94,182.085,57.373000000000005,32.65 +2020-12-16 13:45:00,130.4,181.957,57.373000000000005,32.65 +2020-12-16 14:00:00,129.27,180.96599999999998,57.684,32.65 +2020-12-16 14:15:00,131.13,181.395,57.684,32.65 +2020-12-16 14:30:00,131.55,181.887,57.684,32.65 +2020-12-16 14:45:00,131.65,181.83599999999998,57.684,32.65 +2020-12-16 15:00:00,133.62,182.735,58.03,32.65 +2020-12-16 15:15:00,133.02,183.22,58.03,32.65 +2020-12-16 15:30:00,131.8,185.247,58.03,32.65 +2020-12-16 15:45:00,133.43,186.75799999999998,58.03,32.65 +2020-12-16 16:00:00,137.44,188.804,59.97,32.65 +2020-12-16 16:15:00,138.97,190.2,59.97,32.65 +2020-12-16 16:30:00,140.77,192.604,59.97,32.65 +2020-12-16 16:45:00,140.66,193.891,59.97,32.65 +2020-12-16 17:00:00,143.13,196.707,65.661,32.65 +2020-12-16 17:15:00,142.28,197.408,65.661,32.65 +2020-12-16 17:30:00,143.12,198.132,65.661,32.65 +2020-12-16 17:45:00,142.9,197.90200000000002,65.661,32.65 +2020-12-16 18:00:00,139.93,199.144,70.96300000000001,32.65 +2020-12-16 18:15:00,139.4,197.36,70.96300000000001,32.65 +2020-12-16 18:30:00,138.28,195.812,70.96300000000001,32.65 +2020-12-16 18:45:00,137.34,195.62900000000002,70.96300000000001,32.65 +2020-12-16 19:00:00,135.22,196.88,74.133,32.65 +2020-12-16 19:15:00,133.45,193.31799999999998,74.133,32.65 +2020-12-16 19:30:00,130.36,191.351,74.133,32.65 +2020-12-16 19:45:00,130.37,187.542,74.133,32.65 +2020-12-16 20:00:00,125.93,184.088,65.613,32.65 +2020-12-16 20:15:00,119.64,178.40900000000002,65.613,32.65 +2020-12-16 20:30:00,118.92,174.72799999999998,65.613,32.65 +2020-12-16 20:45:00,115.48,171.995,65.613,32.65 +2020-12-16 21:00:00,115.75,170.007,58.583,32.65 +2020-12-16 21:15:00,117.64,167.907,58.583,32.65 +2020-12-16 21:30:00,113.57,165.769,58.583,32.65 +2020-12-16 21:45:00,107.19,164.188,58.583,32.65 +2020-12-16 22:00:00,102.1,157.631,54.411,32.65 +2020-12-16 22:15:00,102.82,152.17600000000002,54.411,32.65 +2020-12-16 22:30:00,100.28,138.189,54.411,32.65 +2020-12-16 22:45:00,101.94,130.327,54.411,32.65 +2020-12-16 23:00:00,95.99,124.867,47.878,32.65 +2020-12-16 23:15:00,90.51,122.79,47.878,32.65 +2020-12-16 23:30:00,87.38,123.094,47.878,32.65 +2020-12-16 23:45:00,89.89,122.604,47.878,32.65 +2020-12-17 00:00:00,84.21,116.184,44.513000000000005,32.65 +2020-12-17 00:15:00,85.18,115.789,44.513000000000005,32.65 +2020-12-17 00:30:00,74.91,116.663,44.513000000000005,32.65 +2020-12-17 00:45:00,77.85,117.87100000000001,44.513000000000005,32.65 +2020-12-17 01:00:00,72.93,120.101,43.169,32.65 +2020-12-17 01:15:00,73.81,120.945,43.169,32.65 +2020-12-17 01:30:00,79.24,121.45700000000001,43.169,32.65 +2020-12-17 01:45:00,81.74,121.978,43.169,32.65 +2020-12-17 02:00:00,79.9,123.524,41.763999999999996,32.65 +2020-12-17 02:15:00,75.28,124.90799999999999,41.763999999999996,32.65 +2020-12-17 02:30:00,79.95,124.994,41.763999999999996,32.65 +2020-12-17 02:45:00,81.25,126.792,41.763999999999996,32.65 +2020-12-17 03:00:00,79.07,129.269,41.155,32.65 +2020-12-17 03:15:00,76.1,130.252,41.155,32.65 +2020-12-17 03:30:00,80.42,132.184,41.155,32.65 +2020-12-17 03:45:00,81.44,133.056,41.155,32.65 +2020-12-17 04:00:00,76.27,145.717,41.96,32.65 +2020-12-17 04:15:00,77.05,157.893,41.96,32.65 +2020-12-17 04:30:00,82.44,160.058,41.96,32.65 +2020-12-17 04:45:00,82.35,162.546,41.96,32.65 +2020-12-17 05:00:00,84.79,196.699,45.206,32.65 +2020-12-17 05:15:00,87.77,225.481,45.206,32.65 +2020-12-17 05:30:00,92.75,221.127,45.206,32.65 +2020-12-17 05:45:00,94.22,213.13099999999997,45.206,32.65 +2020-12-17 06:00:00,101.61,209.25799999999998,55.398999999999994,32.65 +2020-12-17 06:15:00,107.73,214.637,55.398999999999994,32.65 +2020-12-17 06:30:00,111.51,216.922,55.398999999999994,32.65 +2020-12-17 06:45:00,116.77,219.614,55.398999999999994,32.65 +2020-12-17 07:00:00,125.56,219.46200000000002,64.627,32.65 +2020-12-17 07:15:00,127.94,224.178,64.627,32.65 +2020-12-17 07:30:00,129.03,226.668,64.627,32.65 +2020-12-17 07:45:00,130.58,227.801,64.627,32.65 +2020-12-17 08:00:00,131.85,226.71900000000002,70.895,32.65 +2020-12-17 08:15:00,132.24,226.554,70.895,32.65 +2020-12-17 08:30:00,132.44,224.88,70.895,32.65 +2020-12-17 08:45:00,131.91,221.704,70.895,32.65 +2020-12-17 09:00:00,135.0,215.017,66.382,32.65 +2020-12-17 09:15:00,133.95,211.517,66.382,32.65 +2020-12-17 09:30:00,134.58,208.97799999999998,66.382,32.65 +2020-12-17 09:45:00,134.76,206.22799999999998,66.382,32.65 +2020-12-17 10:00:00,134.7,201.97299999999998,62.739,32.65 +2020-12-17 10:15:00,134.75,197.532,62.739,32.65 +2020-12-17 10:30:00,131.89,195.32299999999998,62.739,32.65 +2020-12-17 10:45:00,131.47,194.005,62.739,32.65 +2020-12-17 11:00:00,130.35,193.108,60.843,32.65 +2020-12-17 11:15:00,130.67,192.05900000000003,60.843,32.65 +2020-12-17 11:30:00,128.97,191.014,60.843,32.65 +2020-12-17 11:45:00,127.33,189.512,60.843,32.65 +2020-12-17 12:00:00,121.37,183.87400000000002,58.466,32.65 +2020-12-17 12:15:00,126.04,182.985,58.466,32.65 +2020-12-17 12:30:00,128.2,183.188,58.466,32.65 +2020-12-17 12:45:00,130.0,184.072,58.466,32.65 +2020-12-17 13:00:00,126.85,182.94799999999998,56.883,32.65 +2020-12-17 13:15:00,126.09,182.54,56.883,32.65 +2020-12-17 13:30:00,125.29,182.247,56.883,32.65 +2020-12-17 13:45:00,126.95,182.11,56.883,32.65 +2020-12-17 14:00:00,126.35,181.108,56.503,32.65 +2020-12-17 14:15:00,127.72,181.54,56.503,32.65 +2020-12-17 14:30:00,124.54,182.05200000000002,56.503,32.65 +2020-12-17 14:45:00,123.37,182.012,56.503,32.65 +2020-12-17 15:00:00,124.4,182.928,57.803999999999995,32.65 +2020-12-17 15:15:00,125.58,183.40599999999998,57.803999999999995,32.65 +2020-12-17 15:30:00,125.0,185.44799999999998,57.803999999999995,32.65 +2020-12-17 15:45:00,124.76,186.956,57.803999999999995,32.65 +2020-12-17 16:00:00,128.21,189.00099999999998,59.379,32.65 +2020-12-17 16:15:00,129.34,190.415,59.379,32.65 +2020-12-17 16:30:00,129.97,192.825,59.379,32.65 +2020-12-17 16:45:00,130.22,194.135,59.379,32.65 +2020-12-17 17:00:00,132.3,196.93099999999998,64.71600000000001,32.65 +2020-12-17 17:15:00,132.66,197.66,64.71600000000001,32.65 +2020-12-17 17:30:00,133.9,198.40200000000002,64.71600000000001,32.65 +2020-12-17 17:45:00,133.14,198.18400000000003,64.71600000000001,32.65 +2020-12-17 18:00:00,130.47,199.445,68.803,32.65 +2020-12-17 18:15:00,129.63,197.641,68.803,32.65 +2020-12-17 18:30:00,128.9,196.09799999999998,68.803,32.65 +2020-12-17 18:45:00,128.16,195.926,68.803,32.65 +2020-12-17 19:00:00,126.35,197.15900000000002,72.934,32.65 +2020-12-17 19:15:00,123.77,193.59099999999998,72.934,32.65 +2020-12-17 19:30:00,122.92,191.61599999999999,72.934,32.65 +2020-12-17 19:45:00,121.49,187.78799999999998,72.934,32.65 +2020-12-17 20:00:00,114.67,184.33,65.175,32.65 +2020-12-17 20:15:00,109.95,178.645,65.175,32.65 +2020-12-17 20:30:00,106.37,174.94400000000002,65.175,32.65 +2020-12-17 20:45:00,104.71,172.22799999999998,65.175,32.65 +2020-12-17 21:00:00,100.41,170.226,58.55,32.65 +2020-12-17 21:15:00,99.67,168.11,58.55,32.65 +2020-12-17 21:30:00,97.77,165.972,58.55,32.65 +2020-12-17 21:45:00,96.78,164.40099999999998,58.55,32.65 +2020-12-17 22:00:00,90.43,157.846,55.041000000000004,32.65 +2020-12-17 22:15:00,88.71,152.398,55.041000000000004,32.65 +2020-12-17 22:30:00,84.97,138.44899999999998,55.041000000000004,32.65 +2020-12-17 22:45:00,82.32,130.597,55.041000000000004,32.65 +2020-12-17 23:00:00,77.33,125.113,48.258,32.65 +2020-12-17 23:15:00,78.58,123.036,48.258,32.65 +2020-12-17 23:30:00,74.54,123.352,48.258,32.65 +2020-12-17 23:45:00,74.17,122.852,48.258,32.65 +2020-12-18 00:00:00,69.93,115.34700000000001,45.02,32.65 +2020-12-18 00:15:00,68.18,115.12299999999999,45.02,32.65 +2020-12-18 00:30:00,67.68,115.87,45.02,32.65 +2020-12-18 00:45:00,67.22,117.18,45.02,32.65 +2020-12-18 01:00:00,62.8,119.12299999999999,42.695,32.65 +2020-12-18 01:15:00,62.98,120.82700000000001,42.695,32.65 +2020-12-18 01:30:00,62.72,121.15299999999999,42.695,32.65 +2020-12-18 01:45:00,63.95,121.755,42.695,32.65 +2020-12-18 02:00:00,61.06,123.43,41.511,32.65 +2020-12-18 02:15:00,62.5,124.70299999999999,41.511,32.65 +2020-12-18 02:30:00,62.92,125.325,41.511,32.65 +2020-12-18 02:45:00,63.6,127.141,41.511,32.65 +2020-12-18 03:00:00,61.83,128.67600000000002,41.162,32.65 +2020-12-18 03:15:00,62.77,130.548,41.162,32.65 +2020-12-18 03:30:00,63.63,132.457,41.162,32.65 +2020-12-18 03:45:00,64.24,133.673,41.162,32.65 +2020-12-18 04:00:00,64.94,146.534,42.226000000000006,32.65 +2020-12-18 04:15:00,66.41,158.416,42.226000000000006,32.65 +2020-12-18 04:30:00,68.08,160.825,42.226000000000006,32.65 +2020-12-18 04:45:00,70.29,162.19299999999998,42.226000000000006,32.65 +2020-12-18 05:00:00,72.72,195.077,45.597,32.65 +2020-12-18 05:15:00,74.61,225.30200000000002,45.597,32.65 +2020-12-18 05:30:00,78.87,222.00799999999998,45.597,32.65 +2020-12-18 05:45:00,83.71,213.953,45.597,32.65 +2020-12-18 06:00:00,92.43,210.528,56.263999999999996,32.65 +2020-12-18 06:15:00,96.81,214.503,56.263999999999996,32.65 +2020-12-18 06:30:00,100.32,215.99,56.263999999999996,32.65 +2020-12-18 06:45:00,104.38,220.27700000000002,56.263999999999996,32.65 +2020-12-18 07:00:00,110.53,219.352,66.888,32.65 +2020-12-18 07:15:00,113.01,225.084,66.888,32.65 +2020-12-18 07:30:00,116.17,227.328,66.888,32.65 +2020-12-18 07:45:00,119.09,227.59599999999998,66.888,32.65 +2020-12-18 08:00:00,121.73,225.487,73.459,32.65 +2020-12-18 08:15:00,120.52,224.96400000000003,73.459,32.65 +2020-12-18 08:30:00,121.35,224.218,73.459,32.65 +2020-12-18 08:45:00,122.72,219.497,73.459,32.65 +2020-12-18 09:00:00,124.32,213.14,69.087,32.65 +2020-12-18 09:15:00,125.38,210.268,69.087,32.65 +2020-12-18 09:30:00,126.56,207.3,69.087,32.65 +2020-12-18 09:45:00,126.98,204.45,69.087,32.65 +2020-12-18 10:00:00,127.19,199.09799999999998,65.404,32.65 +2020-12-18 10:15:00,126.67,195.303,65.404,32.65 +2020-12-18 10:30:00,127.1,193.03099999999998,65.404,32.65 +2020-12-18 10:45:00,125.45,191.282,65.404,32.65 +2020-12-18 11:00:00,123.87,190.359,63.0,32.65 +2020-12-18 11:15:00,125.98,188.398,63.0,32.65 +2020-12-18 11:30:00,126.3,189.011,63.0,32.65 +2020-12-18 11:45:00,124.8,187.51,63.0,32.65 +2020-12-18 12:00:00,123.25,182.92700000000002,59.083,32.65 +2020-12-18 12:15:00,123.46,180.024,59.083,32.65 +2020-12-18 12:30:00,122.58,180.393,59.083,32.65 +2020-12-18 12:45:00,124.82,181.736,59.083,32.65 +2020-12-18 13:00:00,120.54,181.50900000000001,56.611999999999995,32.65 +2020-12-18 13:15:00,119.45,181.887,56.611999999999995,32.65 +2020-12-18 13:30:00,117.01,181.64,56.611999999999995,32.65 +2020-12-18 13:45:00,115.3,181.44799999999998,56.611999999999995,32.65 +2020-12-18 14:00:00,111.85,179.312,55.161,32.65 +2020-12-18 14:15:00,111.13,179.59599999999998,55.161,32.65 +2020-12-18 14:30:00,110.97,180.68599999999998,55.161,32.65 +2020-12-18 14:45:00,110.48,180.91400000000002,55.161,32.65 +2020-12-18 15:00:00,111.97,181.382,55.583,32.65 +2020-12-18 15:15:00,109.45,181.424,55.583,32.65 +2020-12-18 15:30:00,108.82,182.00400000000002,55.583,32.65 +2020-12-18 15:45:00,110.05,183.68400000000003,55.583,32.65 +2020-12-18 16:00:00,114.02,184.56900000000002,57.611999999999995,32.65 +2020-12-18 16:15:00,116.24,186.3,57.611999999999995,32.65 +2020-12-18 16:30:00,116.88,188.799,57.611999999999995,32.65 +2020-12-18 16:45:00,117.32,189.99,57.611999999999995,32.65 +2020-12-18 17:00:00,119.59,193.02,64.14,32.65 +2020-12-18 17:15:00,117.88,193.373,64.14,32.65 +2020-12-18 17:30:00,118.76,193.835,64.14,32.65 +2020-12-18 17:45:00,117.85,193.396,64.14,32.65 +2020-12-18 18:00:00,116.42,195.33700000000002,68.086,32.65 +2020-12-18 18:15:00,115.7,193.104,68.086,32.65 +2020-12-18 18:30:00,115.31,191.945,68.086,32.65 +2020-12-18 18:45:00,114.72,191.793,68.086,32.65 +2020-12-18 19:00:00,112.86,193.90900000000002,69.915,32.65 +2020-12-18 19:15:00,110.63,191.672,69.915,32.65 +2020-12-18 19:30:00,107.94,189.292,69.915,32.65 +2020-12-18 19:45:00,106.94,184.96900000000002,69.915,32.65 +2020-12-18 20:00:00,101.64,181.555,61.695,32.65 +2020-12-18 20:15:00,99.78,175.899,61.695,32.65 +2020-12-18 20:30:00,94.34,172.12099999999998,61.695,32.65 +2020-12-18 20:45:00,92.41,169.93200000000002,61.695,32.65 +2020-12-18 21:00:00,88.08,168.44799999999998,56.041000000000004,32.65 +2020-12-18 21:15:00,86.79,166.792,56.041000000000004,32.65 +2020-12-18 21:30:00,85.7,164.696,56.041000000000004,32.65 +2020-12-18 21:45:00,83.61,163.665,56.041000000000004,32.65 +2020-12-18 22:00:00,79.01,158.058,51.888999999999996,32.65 +2020-12-18 22:15:00,77.65,152.47799999999998,51.888999999999996,32.65 +2020-12-18 22:30:00,75.96,144.877,51.888999999999996,32.65 +2020-12-18 22:45:00,74.55,140.467,51.888999999999996,32.65 +2020-12-18 23:00:00,68.49,134.586,45.787,32.65 +2020-12-18 23:15:00,67.5,130.575,45.787,32.65 +2020-12-18 23:30:00,65.03,129.435,45.787,32.65 +2020-12-18 23:45:00,63.68,128.28,45.787,32.65 +2020-12-19 00:00:00,58.59,112.90299999999999,41.815,32.468 +2020-12-19 00:15:00,56.86,108.542,41.815,32.468 +2020-12-19 00:30:00,56.32,110.538,41.815,32.468 +2020-12-19 00:45:00,55.88,112.488,41.815,32.468 +2020-12-19 01:00:00,53.45,115.075,38.645,32.468 +2020-12-19 01:15:00,53.4,115.85600000000001,38.645,32.468 +2020-12-19 01:30:00,53.25,115.65799999999999,38.645,32.468 +2020-12-19 01:45:00,53.53,116.088,38.645,32.468 +2020-12-19 02:00:00,51.2,118.405,36.696,32.468 +2020-12-19 02:15:00,51.19,119.29299999999999,36.696,32.468 +2020-12-19 02:30:00,51.56,118.825,36.696,32.468 +2020-12-19 02:45:00,51.39,120.76700000000001,36.696,32.468 +2020-12-19 03:00:00,49.17,122.844,35.42,32.468 +2020-12-19 03:15:00,49.88,123.54899999999999,35.42,32.468 +2020-12-19 03:30:00,50.39,123.95100000000001,35.42,32.468 +2020-12-19 03:45:00,50.38,125.331,35.42,32.468 +2020-12-19 04:00:00,50.5,134.138,35.167,32.468 +2020-12-19 04:15:00,51.79,143.55200000000002,35.167,32.468 +2020-12-19 04:30:00,52.07,143.812,35.167,32.468 +2020-12-19 04:45:00,52.89,144.718,35.167,32.468 +2020-12-19 05:00:00,55.09,161.997,35.311,32.468 +2020-12-19 05:15:00,59.51,173.59400000000002,35.311,32.468 +2020-12-19 05:30:00,56.55,170.71599999999998,35.311,32.468 +2020-12-19 05:45:00,59.21,168.06099999999998,35.311,32.468 +2020-12-19 06:00:00,62.57,183.03900000000002,37.117,32.468 +2020-12-19 06:15:00,65.09,202.952,37.117,32.468 +2020-12-19 06:30:00,65.7,199.22299999999998,37.117,32.468 +2020-12-19 06:45:00,67.91,194.838,37.117,32.468 +2020-12-19 07:00:00,72.04,190.25900000000001,40.948,32.468 +2020-12-19 07:15:00,73.9,194.74,40.948,32.468 +2020-12-19 07:30:00,76.28,199.62900000000002,40.948,32.468 +2020-12-19 07:45:00,80.12,203.72299999999998,40.948,32.468 +2020-12-19 08:00:00,84.67,205.49,44.903,32.468 +2020-12-19 08:15:00,84.63,208.417,44.903,32.468 +2020-12-19 08:30:00,85.9,209.208,44.903,32.468 +2020-12-19 08:45:00,89.47,207.513,44.903,32.468 +2020-12-19 09:00:00,91.2,202.946,46.283,32.468 +2020-12-19 09:15:00,91.66,200.81900000000002,46.283,32.468 +2020-12-19 09:30:00,92.64,198.743,46.283,32.468 +2020-12-19 09:45:00,94.43,196.018,46.283,32.468 +2020-12-19 10:00:00,95.05,190.972,44.103,32.468 +2020-12-19 10:15:00,97.92,187.33599999999998,44.103,32.468 +2020-12-19 10:30:00,97.13,185.18900000000002,44.103,32.468 +2020-12-19 10:45:00,98.68,184.658,44.103,32.468 +2020-12-19 11:00:00,98.05,183.915,42.373999999999995,32.468 +2020-12-19 11:15:00,97.08,181.329,42.373999999999995,32.468 +2020-12-19 11:30:00,99.87,180.894,42.373999999999995,32.468 +2020-12-19 11:45:00,100.27,178.51,42.373999999999995,32.468 +2020-12-19 12:00:00,97.52,173.118,39.937,32.468 +2020-12-19 12:15:00,97.76,170.877,39.937,32.468 +2020-12-19 12:30:00,98.11,171.55,39.937,32.468 +2020-12-19 12:45:00,100.55,172.199,39.937,32.468 +2020-12-19 13:00:00,99.76,171.535,37.138000000000005,32.468 +2020-12-19 13:15:00,98.38,169.935,37.138000000000005,32.468 +2020-12-19 13:30:00,95.17,169.263,37.138000000000005,32.468 +2020-12-19 13:45:00,95.3,169.452,37.138000000000005,32.468 +2020-12-19 14:00:00,93.76,168.46900000000002,36.141999999999996,32.468 +2020-12-19 14:15:00,94.4,168.18900000000002,36.141999999999996,32.468 +2020-12-19 14:30:00,93.73,167.553,36.141999999999996,32.468 +2020-12-19 14:45:00,95.27,168.02,36.141999999999996,32.468 +2020-12-19 15:00:00,95.53,169.136,37.964,32.468 +2020-12-19 15:15:00,95.12,169.968,37.964,32.468 +2020-12-19 15:30:00,94.85,172.035,37.964,32.468 +2020-12-19 15:45:00,95.87,173.68400000000003,37.964,32.468 +2020-12-19 16:00:00,102.71,173.486,40.699,32.468 +2020-12-19 16:15:00,100.85,176.049,40.699,32.468 +2020-12-19 16:30:00,103.71,178.514,40.699,32.468 +2020-12-19 16:45:00,102.26,180.572,40.699,32.468 +2020-12-19 17:00:00,105.22,182.958,46.216,32.468 +2020-12-19 17:15:00,103.82,184.842,46.216,32.468 +2020-12-19 17:30:00,107.42,185.225,46.216,32.468 +2020-12-19 17:45:00,106.86,184.43,46.216,32.468 +2020-12-19 18:00:00,106.39,185.953,51.123999999999995,32.468 +2020-12-19 18:15:00,105.15,185.488,51.123999999999995,32.468 +2020-12-19 18:30:00,104.09,185.662,51.123999999999995,32.468 +2020-12-19 18:45:00,103.35,182.18599999999998,51.123999999999995,32.468 +2020-12-19 19:00:00,101.94,185.102,52.336000000000006,32.468 +2020-12-19 19:15:00,100.64,182.34900000000002,52.336000000000006,32.468 +2020-12-19 19:30:00,101.69,180.71,52.336000000000006,32.468 +2020-12-19 19:45:00,98.12,176.26,52.336000000000006,32.468 +2020-12-19 20:00:00,92.73,174.96900000000002,48.825,32.468 +2020-12-19 20:15:00,89.48,171.326,48.825,32.468 +2020-12-19 20:30:00,86.99,167.16099999999997,48.825,32.468 +2020-12-19 20:45:00,85.88,164.67,48.825,32.468 +2020-12-19 21:00:00,82.34,165.27700000000002,43.729,32.468 +2020-12-19 21:15:00,85.85,164.014,43.729,32.468 +2020-12-19 21:30:00,81.94,163.107,43.729,32.468 +2020-12-19 21:45:00,81.18,161.67600000000002,43.729,32.468 +2020-12-19 22:00:00,76.01,157.365,44.126000000000005,32.468 +2020-12-19 22:15:00,75.43,154.187,44.126000000000005,32.468 +2020-12-19 22:30:00,71.47,152.58100000000002,44.126000000000005,32.468 +2020-12-19 22:45:00,70.06,149.986,44.126000000000005,32.468 +2020-12-19 23:00:00,66.49,146.393,38.169000000000004,32.468 +2020-12-19 23:15:00,66.77,140.828,38.169000000000004,32.468 +2020-12-19 23:30:00,64.05,138.111,38.169000000000004,32.468 +2020-12-19 23:45:00,61.94,134.629,38.169000000000004,32.468 +2020-12-20 00:00:00,57.44,113.59899999999999,35.232,32.468 +2020-12-20 00:15:00,55.83,108.861,35.232,32.468 +2020-12-20 00:30:00,55.22,110.48299999999999,35.232,32.468 +2020-12-20 00:45:00,53.65,113.07600000000001,35.232,32.468 +2020-12-20 01:00:00,50.86,115.56200000000001,31.403000000000002,32.468 +2020-12-20 01:15:00,51.31,117.324,31.403000000000002,32.468 +2020-12-20 01:30:00,50.82,117.62100000000001,31.403000000000002,32.468 +2020-12-20 01:45:00,50.65,117.726,31.403000000000002,32.468 +2020-12-20 02:00:00,48.06,119.34700000000001,30.69,32.468 +2020-12-20 02:15:00,48.95,119.454,30.69,32.468 +2020-12-20 02:30:00,49.69,119.81200000000001,30.69,32.468 +2020-12-20 02:45:00,49.72,122.182,30.69,32.468 +2020-12-20 03:00:00,52.26,124.57700000000001,29.516,32.468 +2020-12-20 03:15:00,50.67,124.82,29.516,32.468 +2020-12-20 03:30:00,50.57,126.522,29.516,32.468 +2020-12-20 03:45:00,50.51,127.795,29.516,32.468 +2020-12-20 04:00:00,50.06,136.341,29.148000000000003,32.468 +2020-12-20 04:15:00,50.44,144.77100000000002,29.148000000000003,32.468 +2020-12-20 04:30:00,51.53,145.145,29.148000000000003,32.468 +2020-12-20 04:45:00,52.44,146.28799999999998,29.148000000000003,32.468 +2020-12-20 05:00:00,53.76,160.211,28.706,32.468 +2020-12-20 05:15:00,54.72,169.44299999999998,28.706,32.468 +2020-12-20 05:30:00,54.25,166.403,28.706,32.468 +2020-12-20 05:45:00,55.03,163.986,28.706,32.468 +2020-12-20 06:00:00,57.82,178.773,28.771,32.468 +2020-12-20 06:15:00,58.08,197.097,28.771,32.468 +2020-12-20 06:30:00,59.03,192.31900000000002,28.771,32.468 +2020-12-20 06:45:00,60.79,186.922,28.771,32.468 +2020-12-20 07:00:00,62.77,184.69099999999997,31.39,32.468 +2020-12-20 07:15:00,63.39,188.282,31.39,32.468 +2020-12-20 07:30:00,65.38,192.062,31.39,32.468 +2020-12-20 07:45:00,67.0,195.424,31.39,32.468 +2020-12-20 08:00:00,72.03,198.976,34.972,32.468 +2020-12-20 08:15:00,73.09,201.861,34.972,32.468 +2020-12-20 08:30:00,75.12,204.26,34.972,32.468 +2020-12-20 08:45:00,78.34,204.452,34.972,32.468 +2020-12-20 09:00:00,79.75,199.475,36.709,32.468 +2020-12-20 09:15:00,81.63,197.854,36.709,32.468 +2020-12-20 09:30:00,81.76,195.66099999999997,36.709,32.468 +2020-12-20 09:45:00,83.87,192.864,36.709,32.468 +2020-12-20 10:00:00,85.18,190.226,35.812,32.468 +2020-12-20 10:15:00,86.79,187.08700000000002,35.812,32.468 +2020-12-20 10:30:00,87.22,185.505,35.812,32.468 +2020-12-20 10:45:00,88.97,183.232,35.812,32.468 +2020-12-20 11:00:00,87.56,183.31900000000002,36.746,32.468 +2020-12-20 11:15:00,90.47,180.827,36.746,32.468 +2020-12-20 11:30:00,91.82,179.59799999999998,36.746,32.468 +2020-12-20 11:45:00,93.1,177.797,36.746,32.468 +2020-12-20 12:00:00,92.2,171.933,35.048,32.468 +2020-12-20 12:15:00,91.24,171.46599999999998,35.048,32.468 +2020-12-20 12:30:00,91.52,170.787,35.048,32.468 +2020-12-20 12:45:00,87.78,170.493,35.048,32.468 +2020-12-20 13:00:00,84.85,169.11700000000002,29.987,32.468 +2020-12-20 13:15:00,83.06,170.32299999999998,29.987,32.468 +2020-12-20 13:30:00,80.86,169.38299999999998,29.987,32.468 +2020-12-20 13:45:00,80.54,169.005,29.987,32.468 +2020-12-20 14:00:00,80.42,168.31099999999998,27.21,32.468 +2020-12-20 14:15:00,80.53,169.185,27.21,32.468 +2020-12-20 14:30:00,83.4,169.662,27.21,32.468 +2020-12-20 14:45:00,81.31,169.695,27.21,32.468 +2020-12-20 15:00:00,85.33,169.394,27.726999999999997,32.468 +2020-12-20 15:15:00,82.97,170.889,27.726999999999997,32.468 +2020-12-20 15:30:00,84.51,173.511,27.726999999999997,32.468 +2020-12-20 15:45:00,86.95,175.827,27.726999999999997,32.468 +2020-12-20 16:00:00,90.83,177.327,32.23,32.468 +2020-12-20 16:15:00,91.19,179.05700000000002,32.23,32.468 +2020-12-20 16:30:00,92.7,181.822,32.23,32.468 +2020-12-20 16:45:00,93.89,184.03099999999998,32.23,32.468 +2020-12-20 17:00:00,96.36,186.42700000000002,42.016999999999996,32.468 +2020-12-20 17:15:00,96.89,188.093,42.016999999999996,32.468 +2020-12-20 17:30:00,101.65,188.80599999999998,42.016999999999996,32.468 +2020-12-20 17:45:00,99.7,190.2,42.016999999999996,32.468 +2020-12-20 18:00:00,99.58,191.257,49.338,32.468 +2020-12-20 18:15:00,98.86,192.024,49.338,32.468 +2020-12-20 18:30:00,99.68,190.203,49.338,32.468 +2020-12-20 18:45:00,98.16,188.51,49.338,32.468 +2020-12-20 19:00:00,96.38,191.18099999999998,52.369,32.468 +2020-12-20 19:15:00,95.5,188.947,52.369,32.468 +2020-12-20 19:30:00,94.26,187.13400000000001,52.369,32.468 +2020-12-20 19:45:00,93.64,184.032,52.369,32.468 +2020-12-20 20:00:00,92.2,182.707,50.405,32.468 +2020-12-20 20:15:00,91.3,179.99400000000003,50.405,32.468 +2020-12-20 20:30:00,89.41,177.025,50.405,32.468 +2020-12-20 20:45:00,88.72,173.33700000000002,50.405,32.468 +2020-12-20 21:00:00,82.11,171.449,46.235,32.468 +2020-12-20 21:15:00,79.31,169.55900000000003,46.235,32.468 +2020-12-20 21:30:00,78.23,168.908,46.235,32.468 +2020-12-20 21:45:00,80.71,167.632,46.235,32.468 +2020-12-20 22:00:00,76.48,162.267,46.861000000000004,32.468 +2020-12-20 22:15:00,75.69,158.30200000000002,46.861000000000004,32.468 +2020-12-20 22:30:00,72.53,153.685,46.861000000000004,32.468 +2020-12-20 22:45:00,71.4,150.233,46.861000000000004,32.468 +2020-12-20 23:00:00,67.24,143.93200000000002,41.302,32.468 +2020-12-20 23:15:00,67.09,140.191,41.302,32.468 +2020-12-20 23:30:00,65.09,138.24200000000002,41.302,32.468 +2020-12-20 23:45:00,63.54,135.611,41.302,32.468 +2020-12-21 00:00:00,59.61,117.899,37.164,32.65 +2020-12-21 00:15:00,58.76,116.00200000000001,37.164,32.65 +2020-12-21 00:30:00,55.48,117.723,37.164,32.65 +2020-12-21 00:45:00,58.12,119.76799999999999,37.164,32.65 +2020-12-21 01:00:00,54.85,122.302,34.994,32.65 +2020-12-21 01:15:00,52.98,123.559,34.994,32.65 +2020-12-21 01:30:00,50.94,123.928,34.994,32.65 +2020-12-21 01:45:00,50.8,124.125,34.994,32.65 +2020-12-21 02:00:00,52.33,125.757,34.571,32.65 +2020-12-21 02:15:00,52.41,127.214,34.571,32.65 +2020-12-21 02:30:00,52.65,127.90100000000001,34.571,32.65 +2020-12-21 02:45:00,53.11,129.673,34.571,32.65 +2020-12-21 03:00:00,51.52,133.284,33.934,32.65 +2020-12-21 03:15:00,52.71,135.15,33.934,32.65 +2020-12-21 03:30:00,52.07,136.624,33.934,32.65 +2020-12-21 03:45:00,49.61,137.35299999999998,33.934,32.65 +2020-12-21 04:00:00,53.48,150.143,34.107,32.65 +2020-12-21 04:15:00,52.93,162.64,34.107,32.65 +2020-12-21 04:30:00,53.67,165.072,34.107,32.65 +2020-12-21 04:45:00,54.53,166.38400000000001,34.107,32.65 +2020-12-21 05:00:00,56.42,195.558,39.575,32.65 +2020-12-21 05:15:00,57.68,224.365,39.575,32.65 +2020-12-21 05:30:00,58.83,221.533,39.575,32.65 +2020-12-21 05:45:00,60.34,213.628,39.575,32.65 +2020-12-21 06:00:00,62.76,210.952,56.156000000000006,32.65 +2020-12-21 06:15:00,66.38,214.78599999999997,56.156000000000006,32.65 +2020-12-21 06:30:00,67.63,217.83,56.156000000000006,32.65 +2020-12-21 06:45:00,70.42,221.06,56.156000000000006,32.65 +2020-12-21 07:00:00,74.09,221.109,67.926,32.65 +2020-12-21 07:15:00,75.56,225.983,67.926,32.65 +2020-12-21 07:30:00,78.04,228.984,67.926,32.65 +2020-12-21 07:45:00,81.33,229.912,67.926,32.65 +2020-12-21 08:00:00,85.45,228.74900000000002,72.58,32.65 +2020-12-21 08:15:00,86.9,229.53400000000002,72.58,32.65 +2020-12-21 08:30:00,89.21,228.0,72.58,32.65 +2020-12-21 08:45:00,91.43,225.005,72.58,32.65 +2020-12-21 09:00:00,94.89,219.02200000000002,66.984,32.65 +2020-12-21 09:15:00,97.25,214.018,66.984,32.65 +2020-12-21 09:30:00,101.27,210.845,66.984,32.65 +2020-12-21 09:45:00,99.72,208.222,66.984,32.65 +2020-12-21 10:00:00,101.1,204.55200000000002,63.158,32.65 +2020-12-21 10:15:00,100.36,201.084,63.158,32.65 +2020-12-21 10:30:00,101.56,198.63400000000001,63.158,32.65 +2020-12-21 10:45:00,102.86,196.997,63.158,32.65 +2020-12-21 11:00:00,100.92,194.606,61.141000000000005,32.65 +2020-12-21 11:15:00,101.74,193.829,61.141000000000005,32.65 +2020-12-21 11:30:00,103.83,193.94,61.141000000000005,32.65 +2020-12-21 11:45:00,102.91,191.77200000000002,61.141000000000005,32.65 +2020-12-21 12:00:00,101.04,187.456,57.961000000000006,32.65 +2020-12-21 12:15:00,101.4,187.00900000000001,57.961000000000006,32.65 +2020-12-21 12:30:00,98.37,186.525,57.961000000000006,32.65 +2020-12-21 12:45:00,97.4,187.678,57.961000000000006,32.65 +2020-12-21 13:00:00,94.93,186.868,56.843,32.65 +2020-12-21 13:15:00,92.49,186.717,56.843,32.65 +2020-12-21 13:30:00,90.19,185.27599999999998,56.843,32.65 +2020-12-21 13:45:00,89.94,184.963,56.843,32.65 +2020-12-21 14:00:00,86.88,183.642,55.992,32.65 +2020-12-21 14:15:00,84.1,183.937,55.992,32.65 +2020-12-21 14:30:00,81.84,183.912,55.992,32.65 +2020-12-21 14:45:00,83.32,184.023,55.992,32.65 +2020-12-21 15:00:00,83.47,185.426,57.523,32.65 +2020-12-21 15:15:00,81.79,185.524,57.523,32.65 +2020-12-21 15:30:00,81.2,187.407,57.523,32.65 +2020-12-21 15:45:00,82.37,189.275,57.523,32.65 +2020-12-21 16:00:00,85.28,191.02,59.471000000000004,32.65 +2020-12-21 16:15:00,84.75,192.044,59.471000000000004,32.65 +2020-12-21 16:30:00,86.17,193.86700000000002,59.471000000000004,32.65 +2020-12-21 16:45:00,86.8,194.97,59.471000000000004,32.65 +2020-12-21 17:00:00,89.11,197.155,65.066,32.65 +2020-12-21 17:15:00,90.27,197.953,65.066,32.65 +2020-12-21 17:30:00,92.59,198.148,65.066,32.65 +2020-12-21 17:45:00,92.01,198.112,65.066,32.65 +2020-12-21 18:00:00,92.03,199.575,69.581,32.65 +2020-12-21 18:15:00,92.38,198.145,69.581,32.65 +2020-12-21 18:30:00,91.75,196.93200000000002,69.581,32.65 +2020-12-21 18:45:00,91.35,196.043,69.581,32.65 +2020-12-21 19:00:00,87.77,197.149,73.771,32.65 +2020-12-21 19:15:00,86.86,193.826,73.771,32.65 +2020-12-21 19:30:00,85.1,192.47299999999998,73.771,32.65 +2020-12-21 19:45:00,84.71,188.53799999999998,73.771,32.65 +2020-12-21 20:00:00,81.21,184.893,65.035,32.65 +2020-12-21 20:15:00,80.86,179.843,65.035,32.65 +2020-12-21 20:30:00,79.94,175.08700000000002,65.035,32.65 +2020-12-21 20:45:00,78.91,172.983,65.035,32.65 +2020-12-21 21:00:00,76.0,171.56799999999998,58.7,32.65 +2020-12-21 21:15:00,76.31,168.549,58.7,32.65 +2020-12-21 21:30:00,74.92,167.11900000000003,58.7,32.65 +2020-12-21 21:45:00,74.61,165.36,58.7,32.65 +2020-12-21 22:00:00,72.26,157.161,53.888000000000005,32.65 +2020-12-21 22:15:00,72.03,152.0,53.888000000000005,32.65 +2020-12-21 22:30:00,70.9,138.101,53.888000000000005,32.65 +2020-12-21 22:45:00,70.7,130.013,53.888000000000005,32.65 +2020-12-21 23:00:00,67.92,124.43299999999999,45.501999999999995,32.65 +2020-12-21 23:15:00,65.32,123.19,45.501999999999995,32.65 +2020-12-21 23:30:00,63.5,123.906,45.501999999999995,32.65 +2020-12-21 23:45:00,63.09,123.789,45.501999999999995,32.65 +2020-12-22 00:00:00,77.11,117.387,43.537,32.65 +2020-12-22 00:15:00,75.55,116.86,43.537,32.65 +2020-12-22 00:30:00,74.5,117.693,43.537,32.65 +2020-12-22 00:45:00,75.85,118.81,43.537,32.65 +2020-12-22 01:00:00,74.35,121.161,41.854,32.65 +2020-12-22 01:15:00,72.04,121.991,41.854,32.65 +2020-12-22 01:30:00,70.74,122.525,41.854,32.65 +2020-12-22 01:45:00,72.79,122.985,41.854,32.65 +2020-12-22 02:00:00,71.78,124.613,40.321,32.65 +2020-12-22 02:15:00,72.48,126.00299999999999,40.321,32.65 +2020-12-22 02:30:00,72.39,126.111,40.321,32.65 +2020-12-22 02:45:00,73.21,127.904,40.321,32.65 +2020-12-22 03:00:00,72.62,130.333,39.632,32.65 +2020-12-22 03:15:00,73.58,131.439,39.632,32.65 +2020-12-22 03:30:00,75.36,133.376,39.632,32.65 +2020-12-22 03:45:00,75.07,134.254,39.632,32.65 +2020-12-22 04:00:00,74.68,146.79,40.183,32.65 +2020-12-22 04:15:00,74.32,158.94799999999998,40.183,32.65 +2020-12-22 04:30:00,75.7,161.071,40.183,32.65 +2020-12-22 04:45:00,77.44,163.56,40.183,32.65 +2020-12-22 05:00:00,80.88,197.551,43.945,32.65 +2020-12-22 05:15:00,83.17,226.143,43.945,32.65 +2020-12-22 05:30:00,88.89,221.862,43.945,32.65 +2020-12-22 05:45:00,94.9,213.957,43.945,32.65 +2020-12-22 06:00:00,103.72,210.203,56.048,32.65 +2020-12-22 06:15:00,108.11,215.607,56.048,32.65 +2020-12-22 06:30:00,112.38,218.05200000000002,56.048,32.65 +2020-12-22 06:45:00,116.06,220.926,56.048,32.65 +2020-12-22 07:00:00,122.27,220.813,65.74,32.65 +2020-12-22 07:15:00,126.37,225.525,65.74,32.65 +2020-12-22 07:30:00,128.12,227.997,65.74,32.65 +2020-12-22 07:45:00,131.48,229.09,65.74,32.65 +2020-12-22 08:00:00,133.47,228.032,72.757,32.65 +2020-12-22 08:15:00,133.0,227.81099999999998,72.757,32.65 +2020-12-22 08:30:00,133.5,226.104,72.757,32.65 +2020-12-22 08:45:00,134.21,222.799,72.757,32.65 +2020-12-22 09:00:00,133.97,216.007,67.692,32.65 +2020-12-22 09:15:00,136.46,212.53900000000002,67.692,32.65 +2020-12-22 09:30:00,137.68,210.049,67.692,32.65 +2020-12-22 09:45:00,137.1,207.25099999999998,67.692,32.65 +2020-12-22 10:00:00,137.74,202.968,63.506,32.65 +2020-12-22 10:15:00,137.9,198.468,63.506,32.65 +2020-12-22 10:30:00,137.65,196.17700000000002,63.506,32.65 +2020-12-22 10:45:00,140.41,194.83900000000003,63.506,32.65 +2020-12-22 11:00:00,142.25,193.857,60.758,32.65 +2020-12-22 11:15:00,143.49,192.766,60.758,32.65 +2020-12-22 11:30:00,143.27,191.726,60.758,32.65 +2020-12-22 11:45:00,138.82,190.21099999999998,60.758,32.65 +2020-12-22 12:00:00,137.52,184.581,57.519,32.65 +2020-12-22 12:15:00,137.54,183.763,57.519,32.65 +2020-12-22 12:30:00,137.08,184.00599999999997,57.519,32.65 +2020-12-22 12:45:00,135.61,184.908,57.519,32.65 +2020-12-22 13:00:00,132.97,183.676,56.46,32.65 +2020-12-22 13:15:00,132.28,183.25400000000002,56.46,32.65 +2020-12-22 13:30:00,130.5,182.924,56.46,32.65 +2020-12-22 13:45:00,130.46,182.74599999999998,56.46,32.65 +2020-12-22 14:00:00,129.11,181.707,56.207,32.65 +2020-12-22 14:15:00,129.2,182.144,56.207,32.65 +2020-12-22 14:30:00,127.97,182.748,56.207,32.65 +2020-12-22 14:45:00,128.67,182.766,56.207,32.65 +2020-12-22 15:00:00,128.91,183.767,57.391999999999996,32.65 +2020-12-22 15:15:00,128.69,184.206,57.391999999999996,32.65 +2020-12-22 15:30:00,128.66,186.304,57.391999999999996,32.65 +2020-12-22 15:45:00,129.67,187.796,57.391999999999996,32.65 +2020-12-22 16:00:00,133.28,189.83700000000002,59.955,32.65 +2020-12-22 16:15:00,135.66,191.335,59.955,32.65 +2020-12-22 16:30:00,137.94,193.77200000000002,59.955,32.65 +2020-12-22 16:45:00,139.17,195.19400000000002,59.955,32.65 +2020-12-22 17:00:00,143.78,197.887,67.063,32.65 +2020-12-22 17:15:00,144.03,198.753,67.063,32.65 +2020-12-22 17:30:00,145.49,199.59599999999998,67.063,32.65 +2020-12-22 17:45:00,142.58,199.43900000000002,67.063,32.65 +2020-12-22 18:00:00,139.78,200.797,71.477,32.65 +2020-12-22 18:15:00,138.93,198.907,71.477,32.65 +2020-12-22 18:30:00,137.96,197.396,71.477,32.65 +2020-12-22 18:45:00,138.52,197.283,71.477,32.65 +2020-12-22 19:00:00,134.79,198.418,74.32,32.65 +2020-12-22 19:15:00,134.11,194.825,74.32,32.65 +2020-12-22 19:30:00,134.57,192.81400000000002,74.32,32.65 +2020-12-22 19:45:00,134.19,188.90900000000002,74.32,32.65 +2020-12-22 20:00:00,126.29,185.421,66.157,32.65 +2020-12-22 20:15:00,120.72,179.71200000000002,66.157,32.65 +2020-12-22 20:30:00,116.25,175.917,66.157,32.65 +2020-12-22 20:45:00,114.97,173.28900000000002,66.157,32.65 +2020-12-22 21:00:00,111.39,171.205,59.806000000000004,32.65 +2020-12-22 21:15:00,112.49,169.00799999999998,59.806000000000004,32.65 +2020-12-22 21:30:00,112.51,166.87900000000002,59.806000000000004,32.65 +2020-12-22 21:45:00,108.75,165.354,59.806000000000004,32.65 +2020-12-22 22:00:00,100.85,158.816,54.785,32.65 +2020-12-22 22:15:00,98.46,153.403,54.785,32.65 +2020-12-22 22:30:00,93.77,139.632,54.785,32.65 +2020-12-22 22:45:00,94.39,131.819,54.785,32.65 +2020-12-22 23:00:00,92.62,126.223,47.176,32.65 +2020-12-22 23:15:00,91.43,124.146,47.176,32.65 +2020-12-22 23:30:00,86.04,124.527,47.176,32.65 +2020-12-22 23:45:00,83.32,123.98200000000001,47.176,32.65 +2020-12-23 00:00:00,83.37,117.60700000000001,43.42,32.65 +2020-12-23 00:15:00,83.59,117.055,43.42,32.65 +2020-12-23 00:30:00,83.23,117.87899999999999,43.42,32.65 +2020-12-23 00:45:00,80.94,118.979,43.42,32.65 +2020-12-23 01:00:00,76.7,121.35,40.869,32.65 +2020-12-23 01:15:00,80.48,122.177,40.869,32.65 +2020-12-23 01:30:00,81.54,122.714,40.869,32.65 +2020-12-23 01:45:00,80.75,123.163,40.869,32.65 +2020-12-23 02:00:00,77.53,124.807,39.541,32.65 +2020-12-23 02:15:00,76.71,126.197,39.541,32.65 +2020-12-23 02:30:00,79.2,126.31,39.541,32.65 +2020-12-23 02:45:00,81.57,128.10299999999998,39.541,32.65 +2020-12-23 03:00:00,79.82,130.524,39.052,32.65 +2020-12-23 03:15:00,77.81,131.65200000000002,39.052,32.65 +2020-12-23 03:30:00,79.49,133.59,39.052,32.65 +2020-12-23 03:45:00,83.44,134.471,39.052,32.65 +2020-12-23 04:00:00,82.03,146.98,40.36,32.65 +2020-12-23 04:15:00,80.22,159.136,40.36,32.65 +2020-12-23 04:30:00,80.62,161.252,40.36,32.65 +2020-12-23 04:45:00,83.03,163.74,40.36,32.65 +2020-12-23 05:00:00,87.93,197.69799999999998,43.133,32.65 +2020-12-23 05:15:00,91.08,226.25599999999997,43.133,32.65 +2020-12-23 05:30:00,94.96,221.985,43.133,32.65 +2020-12-23 05:45:00,98.6,214.09900000000002,43.133,32.65 +2020-12-23 06:00:00,105.55,210.36900000000003,54.953,32.65 +2020-12-23 06:15:00,110.85,215.77700000000002,54.953,32.65 +2020-12-23 06:30:00,115.87,218.25099999999998,54.953,32.65 +2020-12-23 06:45:00,120.4,221.162,54.953,32.65 +2020-12-23 07:00:00,126.78,221.05700000000002,66.566,32.65 +2020-12-23 07:15:00,128.65,225.765,66.566,32.65 +2020-12-23 07:30:00,130.99,228.233,66.566,32.65 +2020-12-23 07:45:00,133.38,229.315,66.566,32.65 +2020-12-23 08:00:00,135.54,228.261,72.902,32.65 +2020-12-23 08:15:00,135.89,228.02700000000002,72.902,32.65 +2020-12-23 08:30:00,136.41,226.31099999999998,72.902,32.65 +2020-12-23 08:45:00,136.61,222.982,72.902,32.65 +2020-12-23 09:00:00,137.52,216.169,68.465,32.65 +2020-12-23 09:15:00,138.65,212.708,68.465,32.65 +2020-12-23 09:30:00,139.53,210.229,68.465,32.65 +2020-12-23 09:45:00,140.58,207.421,68.465,32.65 +2020-12-23 10:00:00,139.15,203.13299999999998,63.625,32.65 +2020-12-23 10:15:00,139.36,198.62400000000002,63.625,32.65 +2020-12-23 10:30:00,139.02,196.31799999999998,63.625,32.65 +2020-12-23 10:45:00,138.24,194.97799999999998,63.625,32.65 +2020-12-23 11:00:00,141.5,193.97799999999998,61.628,32.65 +2020-12-23 11:15:00,140.02,192.88,61.628,32.65 +2020-12-23 11:30:00,142.39,191.84,61.628,32.65 +2020-12-23 11:45:00,137.8,190.325,61.628,32.65 +2020-12-23 12:00:00,134.47,184.69799999999998,58.708999999999996,32.65 +2020-12-23 12:15:00,132.86,183.894,58.708999999999996,32.65 +2020-12-23 12:30:00,132.25,184.143,58.708999999999996,32.65 +2020-12-23 12:45:00,133.37,185.047,58.708999999999996,32.65 +2020-12-23 13:00:00,133.19,183.796,57.373000000000005,32.65 +2020-12-23 13:15:00,133.97,183.37,57.373000000000005,32.65 +2020-12-23 13:30:00,132.3,183.033,57.373000000000005,32.65 +2020-12-23 13:45:00,130.97,182.84799999999998,57.373000000000005,32.65 +2020-12-23 14:00:00,130.64,181.804,57.684,32.65 +2020-12-23 14:15:00,130.79,182.24099999999999,57.684,32.65 +2020-12-23 14:30:00,130.4,182.863,57.684,32.65 +2020-12-23 14:45:00,130.0,182.893,57.684,32.65 +2020-12-23 15:00:00,130.36,183.90900000000002,58.03,32.65 +2020-12-23 15:15:00,130.22,184.33900000000003,58.03,32.65 +2020-12-23 15:30:00,129.82,186.446,58.03,32.65 +2020-12-23 15:45:00,131.13,187.935,58.03,32.65 +2020-12-23 16:00:00,133.7,189.97299999999998,59.97,32.65 +2020-12-23 16:15:00,136.31,191.487,59.97,32.65 +2020-12-23 16:30:00,139.13,193.93,59.97,32.65 +2020-12-23 16:45:00,141.42,195.372,59.97,32.65 +2020-12-23 17:00:00,147.76,198.045,65.661,32.65 +2020-12-23 17:15:00,147.93,198.938,65.661,32.65 +2020-12-23 17:30:00,148.05,199.803,65.661,32.65 +2020-12-23 17:45:00,143.23,199.658,65.661,32.65 +2020-12-23 18:00:00,140.17,201.03599999999997,70.96300000000001,32.65 +2020-12-23 18:15:00,139.83,199.13299999999998,70.96300000000001,32.65 +2020-12-23 18:30:00,138.54,197.62900000000002,70.96300000000001,32.65 +2020-12-23 18:45:00,138.83,197.52900000000002,70.96300000000001,32.65 +2020-12-23 19:00:00,136.66,198.641,74.133,32.65 +2020-12-23 19:15:00,134.75,195.046,74.133,32.65 +2020-12-23 19:30:00,133.88,193.02900000000002,74.133,32.65 +2020-12-23 19:45:00,133.25,189.11,74.133,32.65 +2020-12-23 20:00:00,126.72,185.614,65.613,32.65 +2020-12-23 20:15:00,120.6,179.90200000000002,65.613,32.65 +2020-12-23 20:30:00,117.16,176.08900000000003,65.613,32.65 +2020-12-23 20:45:00,115.94,173.47799999999998,65.613,32.65 +2020-12-23 21:00:00,113.09,171.37900000000002,58.583,32.65 +2020-12-23 21:15:00,114.76,169.165,58.583,32.65 +2020-12-23 21:30:00,112.8,167.037,58.583,32.65 +2020-12-23 21:45:00,109.3,165.523,58.583,32.65 +2020-12-23 22:00:00,103.75,158.988,54.411,32.65 +2020-12-23 22:15:00,102.73,153.583,54.411,32.65 +2020-12-23 22:30:00,99.23,139.843,54.411,32.65 +2020-12-23 22:45:00,99.37,132.03799999999998,54.411,32.65 +2020-12-23 23:00:00,94.42,126.421,47.878,32.65 +2020-12-23 23:15:00,89.7,124.345,47.878,32.65 +2020-12-23 23:30:00,86.46,124.74,47.878,32.65 +2020-12-23 23:45:00,87.03,124.18799999999999,47.878,32.65 +2020-12-24 00:00:00,57.0,117.822,44.513000000000005,32.468 +2020-12-24 00:15:00,57.34,117.244,44.513000000000005,32.468 +2020-12-24 00:30:00,56.42,118.05799999999999,44.513000000000005,32.468 +2020-12-24 00:45:00,55.53,119.14,44.513000000000005,32.468 +2020-12-24 01:00:00,52.63,121.53200000000001,43.169,32.468 +2020-12-24 01:15:00,53.35,122.355,43.169,32.468 +2020-12-24 01:30:00,52.36,122.896,43.169,32.468 +2020-12-24 01:45:00,52.48,123.33200000000001,43.169,32.468 +2020-12-24 02:00:00,51.25,124.992,41.763999999999996,32.468 +2020-12-24 02:15:00,52.09,126.383,41.763999999999996,32.468 +2020-12-24 02:30:00,51.45,126.501,41.763999999999996,32.468 +2020-12-24 02:45:00,51.82,128.293,41.763999999999996,32.468 +2020-12-24 03:00:00,51.68,130.705,41.155,32.468 +2020-12-24 03:15:00,52.17,131.857,41.155,32.468 +2020-12-24 03:30:00,52.33,133.796,41.155,32.468 +2020-12-24 03:45:00,53.49,134.678,41.155,32.468 +2020-12-24 04:00:00,54.37,147.165,41.96,32.468 +2020-12-24 04:15:00,54.98,159.316,41.96,32.468 +2020-12-24 04:30:00,55.93,161.425,41.96,32.468 +2020-12-24 04:45:00,57.9,163.912,41.96,32.468 +2020-12-24 05:00:00,61.23,197.83700000000002,45.206,32.468 +2020-12-24 05:15:00,61.7,226.36,45.206,32.468 +2020-12-24 05:30:00,63.74,222.101,45.206,32.468 +2020-12-24 05:45:00,65.72,214.232,45.206,32.468 +2020-12-24 06:00:00,71.25,210.52599999999998,55.398999999999994,32.468 +2020-12-24 06:15:00,73.82,215.94,55.398999999999994,32.468 +2020-12-24 06:30:00,75.73,218.44099999999997,55.398999999999994,32.468 +2020-12-24 06:45:00,79.76,221.386,55.398999999999994,32.468 +2020-12-24 07:00:00,84.31,221.29,64.627,32.468 +2020-12-24 07:15:00,85.26,225.99599999999998,64.627,32.468 +2020-12-24 07:30:00,86.83,228.458,64.627,32.468 +2020-12-24 07:45:00,89.99,229.53,64.627,32.468 +2020-12-24 08:00:00,92.92,228.477,70.895,32.468 +2020-12-24 08:15:00,93.1,228.232,70.895,32.468 +2020-12-24 08:30:00,94.95,226.505,70.895,32.468 +2020-12-24 08:45:00,95.57,223.15200000000002,70.895,32.468 +2020-12-24 09:00:00,99.59,216.31900000000002,66.382,32.468 +2020-12-24 09:15:00,100.3,212.865,66.382,32.468 +2020-12-24 09:30:00,100.82,210.396,66.382,32.468 +2020-12-24 09:45:00,100.23,207.579,66.382,32.468 +2020-12-24 10:00:00,101.06,203.28799999999998,62.739,32.468 +2020-12-24 10:15:00,101.79,198.771,62.739,32.468 +2020-12-24 10:30:00,102.05,196.449,62.739,32.468 +2020-12-24 10:45:00,102.54,195.107,62.739,32.468 +2020-12-24 11:00:00,100.69,194.09,60.843,32.468 +2020-12-24 11:15:00,101.48,192.986,60.843,32.468 +2020-12-24 11:30:00,100.88,191.946,60.843,32.468 +2020-12-24 11:45:00,99.0,190.43,60.843,32.468 +2020-12-24 12:00:00,94.85,184.805,58.466,32.468 +2020-12-24 12:15:00,95.31,184.016,58.466,32.468 +2020-12-24 12:30:00,93.98,184.271,58.466,32.468 +2020-12-24 12:45:00,96.03,185.178,58.466,32.468 +2020-12-24 13:00:00,94.18,183.908,56.883,32.468 +2020-12-24 13:15:00,90.44,183.48,56.883,32.468 +2020-12-24 13:30:00,90.13,183.132,56.883,32.468 +2020-12-24 13:45:00,90.54,182.94,56.883,32.468 +2020-12-24 14:00:00,89.78,181.893,56.503,32.468 +2020-12-24 14:15:00,92.43,182.33,56.503,32.468 +2020-12-24 14:30:00,90.39,182.968,56.503,32.468 +2020-12-24 14:45:00,92.14,183.01,56.503,32.468 +2020-12-24 15:00:00,92.57,184.044,57.803999999999995,32.468 +2020-12-24 15:15:00,93.64,184.463,57.803999999999995,32.468 +2020-12-24 15:30:00,94.37,186.578,57.803999999999995,32.468 +2020-12-24 15:45:00,97.44,188.062,57.803999999999995,32.468 +2020-12-24 16:00:00,103.78,190.1,59.379,32.468 +2020-12-24 16:15:00,107.92,191.628,59.379,32.468 +2020-12-24 16:30:00,106.71,194.077,59.379,32.468 +2020-12-24 16:45:00,107.58,195.53900000000002,59.379,32.468 +2020-12-24 17:00:00,111.26,198.19299999999998,64.71600000000001,32.468 +2020-12-24 17:15:00,110.82,199.113,64.71600000000001,32.468 +2020-12-24 17:30:00,112.07,200.0,64.71600000000001,32.468 +2020-12-24 17:45:00,111.35,199.86700000000002,64.71600000000001,32.468 +2020-12-24 18:00:00,110.75,201.264,68.803,32.468 +2020-12-24 18:15:00,110.62,199.34900000000002,68.803,32.468 +2020-12-24 18:30:00,110.52,197.851,68.803,32.468 +2020-12-24 18:45:00,110.48,197.765,68.803,32.468 +2020-12-24 19:00:00,111.49,198.856,72.934,32.468 +2020-12-24 19:15:00,106.52,195.25599999999997,72.934,32.468 +2020-12-24 19:30:00,104.08,193.234,72.934,32.468 +2020-12-24 19:45:00,102.68,189.304,72.934,32.468 +2020-12-24 20:00:00,96.72,185.8,65.175,32.468 +2020-12-24 20:15:00,96.77,180.084,65.175,32.468 +2020-12-24 20:30:00,91.18,176.25400000000002,65.175,32.468 +2020-12-24 20:45:00,89.83,173.66,65.175,32.468 +2020-12-24 21:00:00,86.22,171.545,58.55,32.468 +2020-12-24 21:15:00,85.58,169.31400000000002,58.55,32.468 +2020-12-24 21:30:00,83.25,167.188,58.55,32.468 +2020-12-24 21:45:00,85.54,165.68599999999998,58.55,32.468 +2020-12-24 22:00:00,76.93,159.151,55.041000000000004,32.468 +2020-12-24 22:15:00,77.34,153.755,55.041000000000004,32.468 +2020-12-24 22:30:00,73.53,140.047,55.041000000000004,32.468 +2020-12-24 22:45:00,72.69,132.249,55.041000000000004,32.468 +2020-12-24 23:00:00,68.02,126.61,48.258,32.468 +2020-12-24 23:15:00,68.11,124.535,48.258,32.468 +2020-12-24 23:30:00,64.77,124.944,48.258,32.468 +2020-12-24 23:45:00,67.67,124.384,48.258,32.468 +2020-12-25 00:00:00,57.87,114.70100000000001,32.311,32.468 +2020-12-25 00:15:00,58.21,109.835,32.311,32.468 +2020-12-25 00:30:00,57.43,111.413,32.311,32.468 +2020-12-25 00:45:00,57.71,113.917,32.311,32.468 +2020-12-25 01:00:00,54.98,116.509,25.569000000000003,32.468 +2020-12-25 01:15:00,55.19,118.25200000000001,25.569000000000003,32.468 +2020-12-25 01:30:00,54.78,118.568,25.569000000000003,32.468 +2020-12-25 01:45:00,53.55,118.613,25.569000000000003,32.468 +2020-12-25 02:00:00,50.76,120.314,21.038,32.468 +2020-12-25 02:15:00,52.54,120.425,21.038,32.468 +2020-12-25 02:30:00,49.88,120.809,21.038,32.468 +2020-12-25 02:45:00,52.68,123.17399999999999,21.038,32.468 +2020-12-25 03:00:00,51.32,125.52600000000001,19.865,32.468 +2020-12-25 03:15:00,51.45,125.884,19.865,32.468 +2020-12-25 03:30:00,52.01,127.59,19.865,32.468 +2020-12-25 03:45:00,52.84,128.874,19.865,32.468 +2020-12-25 04:00:00,52.17,137.297,19.076,32.468 +2020-12-25 04:15:00,52.24,145.708,19.076,32.468 +2020-12-25 04:30:00,53.63,146.046,19.076,32.468 +2020-12-25 04:45:00,54.09,147.187,19.076,32.468 +2020-12-25 05:00:00,53.22,160.945,20.174,32.468 +2020-12-25 05:15:00,54.53,169.99900000000002,20.174,32.468 +2020-12-25 05:30:00,53.56,167.02200000000002,20.174,32.468 +2020-12-25 05:45:00,57.2,164.69400000000002,20.174,32.468 +2020-12-25 06:00:00,57.45,179.6,19.854,32.468 +2020-12-25 06:15:00,58.55,197.949,19.854,32.468 +2020-12-25 06:30:00,59.92,193.317,19.854,32.468 +2020-12-25 06:45:00,61.05,188.095,19.854,32.468 +2020-12-25 07:00:00,63.95,185.90599999999998,23.096999999999998,32.468 +2020-12-25 07:15:00,65.26,189.485,23.096999999999998,32.468 +2020-12-25 07:30:00,66.28,193.239,23.096999999999998,32.468 +2020-12-25 07:45:00,69.33,196.55,23.096999999999998,32.468 +2020-12-25 08:00:00,71.62,200.118,30.849,32.468 +2020-12-25 08:15:00,74.42,202.94400000000002,30.849,32.468 +2020-12-25 08:30:00,76.82,205.296,30.849,32.468 +2020-12-25 08:45:00,78.68,205.364,30.849,32.468 +2020-12-25 09:00:00,81.32,200.285,30.03,32.468 +2020-12-25 09:15:00,83.82,198.69799999999998,30.03,32.468 +2020-12-25 09:30:00,85.11,196.558,30.03,32.468 +2020-12-25 09:45:00,87.16,193.717,30.03,32.468 +2020-12-25 10:00:00,87.91,191.055,27.625999999999998,32.468 +2020-12-25 10:15:00,89.23,187.868,27.625999999999998,32.468 +2020-12-25 10:30:00,91.55,186.21,27.625999999999998,32.468 +2020-12-25 10:45:00,93.17,183.923,27.625999999999998,32.468 +2020-12-25 11:00:00,94.65,183.926,29.03,32.468 +2020-12-25 11:15:00,98.58,181.39700000000002,29.03,32.468 +2020-12-25 11:30:00,100.6,180.172,29.03,32.468 +2020-12-25 11:45:00,100.05,178.365,29.03,32.468 +2020-12-25 12:00:00,95.72,172.512,25.93,32.468 +2020-12-25 12:15:00,92.54,172.11900000000003,25.93,32.468 +2020-12-25 12:30:00,88.4,171.47,25.93,32.468 +2020-12-25 12:45:00,84.51,171.19299999999998,25.93,32.468 +2020-12-25 13:00:00,81.0,169.72099999999998,16.363,32.468 +2020-12-25 13:15:00,80.85,170.907,16.363,32.468 +2020-12-25 13:30:00,80.27,169.92700000000002,16.363,32.468 +2020-12-25 13:45:00,80.22,169.51,16.363,32.468 +2020-12-25 14:00:00,77.57,168.797,14.370999999999999,32.468 +2020-12-25 14:15:00,78.09,169.671,14.370999999999999,32.468 +2020-12-25 14:30:00,77.16,170.231,14.370999999999999,32.468 +2020-12-25 14:45:00,76.87,170.32299999999998,14.370999999999999,32.468 +2020-12-25 15:00:00,77.34,170.106,19.031,32.468 +2020-12-25 15:15:00,76.88,171.554,19.031,32.468 +2020-12-25 15:30:00,76.93,174.22,19.031,32.468 +2020-12-25 15:45:00,78.2,176.516,19.031,32.468 +2020-12-25 16:00:00,81.38,178.011,24.998,32.468 +2020-12-25 16:15:00,82.28,179.81900000000002,24.998,32.468 +2020-12-25 16:30:00,86.93,182.61,24.998,32.468 +2020-12-25 16:45:00,86.17,184.921,24.998,32.468 +2020-12-25 17:00:00,89.63,187.218,35.976,32.468 +2020-12-25 17:15:00,87.98,189.021,35.976,32.468 +2020-12-25 17:30:00,89.65,189.84099999999998,35.976,32.468 +2020-12-25 17:45:00,90.91,191.3,35.976,32.468 +2020-12-25 18:00:00,91.63,192.454,41.513000000000005,32.468 +2020-12-25 18:15:00,90.25,193.15400000000002,41.513000000000005,32.468 +2020-12-25 18:30:00,91.06,191.364,41.513000000000005,32.468 +2020-12-25 18:45:00,91.59,189.735,41.513000000000005,32.468 +2020-12-25 19:00:00,90.53,192.299,45.607,32.468 +2020-12-25 19:15:00,89.44,190.046,45.607,32.468 +2020-12-25 19:30:00,88.41,188.205,45.607,32.468 +2020-12-25 19:45:00,87.49,185.03900000000002,45.607,32.468 +2020-12-25 20:00:00,85.33,183.676,43.372,32.468 +2020-12-25 20:15:00,84.64,180.94400000000002,43.372,32.468 +2020-12-25 20:30:00,83.34,177.888,43.372,32.468 +2020-12-25 20:45:00,82.68,174.28599999999997,43.372,32.468 +2020-12-25 21:00:00,80.07,172.31599999999997,39.458,32.468 +2020-12-25 21:15:00,79.28,170.345,39.458,32.468 +2020-12-25 21:30:00,78.47,169.702,39.458,32.468 +2020-12-25 21:45:00,77.69,168.477,39.458,32.468 +2020-12-25 22:00:00,74.04,163.124,40.15,32.468 +2020-12-25 22:15:00,75.14,159.202,40.15,32.468 +2020-12-25 22:30:00,71.33,154.743,40.15,32.468 +2020-12-25 22:45:00,71.18,151.332,40.15,32.468 +2020-12-25 23:00:00,67.24,144.92,33.876999999999995,32.468 +2020-12-25 23:15:00,66.92,141.184,33.876999999999995,32.468 +2020-12-25 23:30:00,63.35,139.3,33.876999999999995,32.468 +2020-12-25 23:45:00,62.32,136.632,33.876999999999995,32.468 +2020-12-26 00:00:00,57.9,114.9,32.311,32.468 +2020-12-26 00:15:00,58.19,110.01,32.311,32.468 +2020-12-26 00:30:00,57.69,111.57700000000001,32.311,32.468 +2020-12-26 00:45:00,58.18,114.064,32.311,32.468 +2020-12-26 01:00:00,54.2,116.675,25.569000000000003,32.468 +2020-12-26 01:15:00,53.83,118.415,25.569000000000003,32.468 +2020-12-26 01:30:00,54.04,118.73299999999999,25.569000000000003,32.468 +2020-12-26 01:45:00,53.35,118.766,25.569000000000003,32.468 +2020-12-26 02:00:00,50.27,120.48200000000001,21.038,32.468 +2020-12-26 02:15:00,50.84,120.59299999999999,21.038,32.468 +2020-12-26 02:30:00,50.18,120.984,21.038,32.468 +2020-12-26 02:45:00,49.92,123.348,21.038,32.468 +2020-12-26 03:00:00,50.44,125.69200000000001,19.865,32.468 +2020-12-26 03:15:00,51.13,126.073,19.865,32.468 +2020-12-26 03:30:00,51.68,127.77799999999999,19.865,32.468 +2020-12-26 03:45:00,52.19,129.064,19.865,32.468 +2020-12-26 04:00:00,52.65,137.464,19.076,32.468 +2020-12-26 04:15:00,51.74,145.872,19.076,32.468 +2020-12-26 04:30:00,52.08,146.203,19.076,32.468 +2020-12-26 04:45:00,52.91,147.342,19.076,32.468 +2020-12-26 05:00:00,52.87,161.06799999999998,20.174,32.468 +2020-12-26 05:15:00,54.46,170.08900000000003,20.174,32.468 +2020-12-26 05:30:00,54.44,167.12099999999998,20.174,32.468 +2020-12-26 05:45:00,56.77,164.81099999999998,20.174,32.468 +2020-12-26 06:00:00,56.75,179.74099999999999,19.854,32.468 +2020-12-26 06:15:00,60.38,198.097,19.854,32.468 +2020-12-26 06:30:00,58.83,193.49,19.854,32.468 +2020-12-26 06:45:00,59.86,188.3,19.854,32.468 +2020-12-26 07:00:00,62.28,186.122,23.096999999999998,32.468 +2020-12-26 07:15:00,63.68,189.696,23.096999999999998,32.468 +2020-12-26 07:30:00,64.23,193.442,23.096999999999998,32.468 +2020-12-26 07:45:00,69.19,196.74200000000002,23.096999999999998,32.468 +2020-12-26 08:00:00,69.13,200.31099999999998,28.963,32.468 +2020-12-26 08:15:00,74.4,203.125,28.963,32.468 +2020-12-26 08:30:00,73.12,205.46400000000003,28.963,32.468 +2020-12-26 08:45:00,75.14,205.50900000000001,28.963,32.468 +2020-12-26 09:00:00,78.03,200.41099999999997,28.194000000000003,32.468 +2020-12-26 09:15:00,80.29,198.832,28.194000000000003,32.468 +2020-12-26 09:30:00,83.3,196.702,28.194000000000003,32.468 +2020-12-26 09:45:00,81.36,193.852,28.194000000000003,32.468 +2020-12-26 10:00:00,86.11,191.188,25.936999999999998,32.468 +2020-12-26 10:15:00,85.18,187.99400000000003,25.936999999999998,32.468 +2020-12-26 10:30:00,89.16,186.321,25.936999999999998,32.468 +2020-12-26 10:45:00,88.48,184.033,25.936999999999998,32.468 +2020-12-26 11:00:00,92.1,184.018,27.256,32.468 +2020-12-26 11:15:00,93.15,181.483,27.256,32.468 +2020-12-26 11:30:00,95.06,180.26,27.256,32.468 +2020-12-26 11:45:00,97.74,178.452,27.256,32.468 +2020-12-26 12:00:00,92.69,172.60299999999998,24.345,32.468 +2020-12-26 12:15:00,92.99,172.22400000000002,24.345,32.468 +2020-12-26 12:30:00,85.85,171.58,24.345,32.468 +2020-12-26 12:45:00,84.15,171.30599999999998,24.345,32.468 +2020-12-26 13:00:00,84.26,169.81599999999997,15.363,32.468 +2020-12-26 13:15:00,80.45,170.998,15.363,32.468 +2020-12-26 13:30:00,78.78,170.00900000000001,15.363,32.468 +2020-12-26 13:45:00,79.31,169.584,15.363,32.468 +2020-12-26 14:00:00,76.77,168.87099999999998,13.492,32.468 +2020-12-26 14:15:00,76.41,169.743,13.492,32.468 +2020-12-26 14:30:00,76.48,170.31900000000002,13.492,32.468 +2020-12-26 14:45:00,77.52,170.423,13.492,32.468 +2020-12-26 15:00:00,78.64,170.22299999999998,17.868,32.468 +2020-12-26 15:15:00,80.39,171.66,17.868,32.468 +2020-12-26 15:30:00,77.92,174.332,17.868,32.468 +2020-12-26 15:45:00,79.11,176.623,17.868,32.468 +2020-12-26 16:00:00,86.11,178.118,23.47,32.468 +2020-12-26 16:15:00,83.6,179.93900000000002,23.47,32.468 +2020-12-26 16:30:00,87.85,182.736,23.47,32.468 +2020-12-26 16:45:00,86.53,185.065,23.47,32.468 +2020-12-26 17:00:00,89.38,187.34400000000002,33.777,32.468 +2020-12-26 17:15:00,90.4,189.174,33.777,32.468 +2020-12-26 17:30:00,94.34,190.016,33.777,32.468 +2020-12-26 17:45:00,92.08,191.488,33.777,32.468 +2020-12-26 18:00:00,93.83,192.66099999999997,38.975,32.468 +2020-12-26 18:15:00,92.14,193.35299999999998,38.975,32.468 +2020-12-26 18:30:00,92.33,191.56799999999998,38.975,32.468 +2020-12-26 18:45:00,93.19,189.954,38.975,32.468 +2020-12-26 19:00:00,91.96,192.495,42.818999999999996,32.468 +2020-12-26 19:15:00,91.71,190.238,42.818999999999996,32.468 +2020-12-26 19:30:00,90.43,188.393,42.818999999999996,32.468 +2020-12-26 19:45:00,90.65,185.217,42.818999999999996,32.468 +2020-12-26 20:00:00,86.8,183.845,43.372,32.468 +2020-12-26 20:15:00,86.59,181.109,43.372,32.468 +2020-12-26 20:30:00,84.5,178.03799999999998,43.372,32.468 +2020-12-26 20:45:00,83.84,174.453,43.372,32.468 +2020-12-26 21:00:00,79.9,172.46599999999998,39.458,32.468 +2020-12-26 21:15:00,79.31,170.479,39.458,32.468 +2020-12-26 21:30:00,79.36,169.83700000000002,39.458,32.468 +2020-12-26 21:45:00,81.72,168.62400000000002,39.458,32.468 +2020-12-26 22:00:00,74.52,163.27200000000002,40.15,32.468 +2020-12-26 22:15:00,74.0,159.359,40.15,32.468 +2020-12-26 22:30:00,72.07,154.929,40.15,32.468 +2020-12-26 22:45:00,70.15,151.52700000000002,40.15,32.468 +2020-12-26 23:00:00,66.32,145.092,33.876999999999995,32.468 +2020-12-26 23:15:00,66.62,141.359,33.876999999999995,32.468 +2020-12-26 23:30:00,63.94,139.489,33.876999999999995,32.468 +2020-12-26 23:45:00,64.95,136.814,33.876999999999995,32.468 +2020-12-27 00:00:00,56.66,115.09299999999999,35.232,32.468 +2020-12-27 00:15:00,57.29,110.179,35.232,32.468 +2020-12-27 00:30:00,56.01,111.735,35.232,32.468 +2020-12-27 00:45:00,54.37,114.205,35.232,32.468 +2020-12-27 01:00:00,52.25,116.833,31.403000000000002,32.468 +2020-12-27 01:15:00,53.32,118.569,31.403000000000002,32.468 +2020-12-27 01:30:00,52.46,118.88799999999999,31.403000000000002,32.468 +2020-12-27 01:45:00,51.48,118.911,31.403000000000002,32.468 +2020-12-27 02:00:00,49.93,120.641,30.69,32.468 +2020-12-27 02:15:00,51.57,120.75299999999999,30.69,32.468 +2020-12-27 02:30:00,50.82,121.15,30.69,32.468 +2020-12-27 02:45:00,50.77,123.514,30.69,32.468 +2020-12-27 03:00:00,49.34,125.84899999999999,29.516,32.468 +2020-12-27 03:15:00,50.62,126.25200000000001,29.516,32.468 +2020-12-27 03:30:00,51.39,127.959,29.516,32.468 +2020-12-27 03:45:00,51.75,129.248,29.516,32.468 +2020-12-27 04:00:00,50.79,137.624,29.148000000000003,32.468 +2020-12-27 04:15:00,51.03,146.02700000000002,29.148000000000003,32.468 +2020-12-27 04:30:00,51.16,146.35299999999998,29.148000000000003,32.468 +2020-12-27 04:45:00,52.11,147.49,29.148000000000003,32.468 +2020-12-27 05:00:00,52.51,161.184,28.706,32.468 +2020-12-27 05:15:00,53.68,170.172,28.706,32.468 +2020-12-27 05:30:00,52.91,167.213,28.706,32.468 +2020-12-27 05:45:00,54.55,164.921,28.706,32.468 +2020-12-27 06:00:00,55.49,179.87400000000002,28.771,32.468 +2020-12-27 06:15:00,55.76,198.235,28.771,32.468 +2020-12-27 06:30:00,56.15,193.65400000000002,28.771,32.468 +2020-12-27 06:45:00,57.92,188.49599999999998,28.771,32.468 +2020-12-27 07:00:00,60.11,186.328,31.39,32.468 +2020-12-27 07:15:00,61.04,189.898,31.39,32.468 +2020-12-27 07:30:00,65.58,193.636,31.39,32.468 +2020-12-27 07:45:00,65.21,196.923,31.39,32.468 +2020-12-27 08:00:00,66.03,200.493,34.972,32.468 +2020-12-27 08:15:00,66.67,203.295,34.972,32.468 +2020-12-27 08:30:00,68.78,205.62,34.972,32.468 +2020-12-27 08:45:00,72.45,205.642,34.972,32.468 +2020-12-27 09:00:00,73.81,200.525,36.709,32.468 +2020-12-27 09:15:00,75.71,198.952,36.709,32.468 +2020-12-27 09:30:00,75.67,196.835,36.709,32.468 +2020-12-27 09:45:00,76.59,193.976,36.709,32.468 +2020-12-27 10:00:00,77.56,191.308,35.812,32.468 +2020-12-27 10:15:00,79.49,188.108,35.812,32.468 +2020-12-27 10:30:00,81.12,186.422,35.812,32.468 +2020-12-27 10:45:00,83.85,184.13299999999998,35.812,32.468 +2020-12-27 11:00:00,84.02,184.101,36.746,32.468 +2020-12-27 11:15:00,86.47,181.56,36.746,32.468 +2020-12-27 11:30:00,87.71,180.338,36.746,32.468 +2020-12-27 11:45:00,88.37,178.52900000000002,36.746,32.468 +2020-12-27 12:00:00,87.6,172.685,35.048,32.468 +2020-12-27 12:15:00,85.66,172.321,35.048,32.468 +2020-12-27 12:30:00,83.94,171.68,35.048,32.468 +2020-12-27 12:45:00,84.23,171.40900000000002,35.048,32.468 +2020-12-27 13:00:00,81.23,169.90400000000002,29.987,32.468 +2020-12-27 13:15:00,79.67,171.08,29.987,32.468 +2020-12-27 13:30:00,78.24,170.083,29.987,32.468 +2020-12-27 13:45:00,77.15,169.65,29.987,32.468 +2020-12-27 14:00:00,73.27,168.938,27.21,32.468 +2020-12-27 14:15:00,75.35,169.808,27.21,32.468 +2020-12-27 14:30:00,74.83,170.398,27.21,32.468 +2020-12-27 14:45:00,74.81,170.515,27.21,32.468 +2020-12-27 15:00:00,75.04,170.331,27.726999999999997,32.468 +2020-12-27 15:15:00,76.57,171.757,27.726999999999997,32.468 +2020-12-27 15:30:00,77.1,174.43400000000003,27.726999999999997,32.468 +2020-12-27 15:45:00,79.95,176.72,27.726999999999997,32.468 +2020-12-27 16:00:00,85.35,178.213,32.23,32.468 +2020-12-27 16:15:00,85.01,180.049,32.23,32.468 +2020-12-27 16:30:00,85.83,182.852,32.23,32.468 +2020-12-27 16:45:00,87.36,185.197,32.23,32.468 +2020-12-27 17:00:00,89.42,187.457,42.016999999999996,32.468 +2020-12-27 17:15:00,89.72,189.315,42.016999999999996,32.468 +2020-12-27 17:30:00,91.66,190.179,42.016999999999996,32.468 +2020-12-27 17:45:00,92.42,191.665,42.016999999999996,32.468 +2020-12-27 18:00:00,93.13,192.858,49.338,32.468 +2020-12-27 18:15:00,92.27,193.541,49.338,32.468 +2020-12-27 18:30:00,93.04,191.763,49.338,32.468 +2020-12-27 18:45:00,92.9,190.16299999999998,49.338,32.468 +2020-12-27 19:00:00,91.41,192.68,52.369,32.468 +2020-12-27 19:15:00,90.74,190.42,52.369,32.468 +2020-12-27 19:30:00,89.58,188.57299999999998,52.369,32.468 +2020-12-27 19:45:00,88.24,185.388,52.369,32.468 +2020-12-27 20:00:00,84.87,184.005,50.405,32.468 +2020-12-27 20:15:00,84.15,181.267,50.405,32.468 +2020-12-27 20:30:00,82.18,178.18099999999998,50.405,32.468 +2020-12-27 20:45:00,80.95,174.613,50.405,32.468 +2020-12-27 21:00:00,77.99,172.61,46.235,32.468 +2020-12-27 21:15:00,79.18,170.606,46.235,32.468 +2020-12-27 21:30:00,78.7,169.965,46.235,32.468 +2020-12-27 21:45:00,80.23,168.764,46.235,32.468 +2020-12-27 22:00:00,76.73,163.412,46.861000000000004,32.468 +2020-12-27 22:15:00,73.06,159.51,46.861000000000004,32.468 +2020-12-27 22:30:00,70.7,155.107,46.861000000000004,32.468 +2020-12-27 22:45:00,70.0,151.71200000000002,46.861000000000004,32.468 +2020-12-27 23:00:00,65.04,145.257,41.302,32.468 +2020-12-27 23:15:00,65.95,141.525,41.302,32.468 +2020-12-27 23:30:00,64.23,139.66899999999998,41.302,32.468 +2020-12-27 23:45:00,63.33,136.99,41.302,32.468 +2020-12-28 00:00:00,57.03,119.344,37.164,32.468 +2020-12-28 00:15:00,57.21,117.274,37.164,32.468 +2020-12-28 00:30:00,56.31,118.925,37.164,32.468 +2020-12-28 00:45:00,56.35,120.848,37.164,32.468 +2020-12-28 01:00:00,52.26,123.51899999999999,34.994,32.468 +2020-12-28 01:15:00,53.0,124.74799999999999,34.994,32.468 +2020-12-28 01:30:00,52.53,125.137,34.994,32.468 +2020-12-28 01:45:00,52.32,125.25399999999999,34.994,32.468 +2020-12-28 02:00:00,51.03,126.993,34.571,32.468 +2020-12-28 02:15:00,52.03,128.454,34.571,32.468 +2020-12-28 02:30:00,50.99,129.18200000000002,34.571,32.468 +2020-12-28 02:45:00,50.55,130.94799999999998,34.571,32.468 +2020-12-28 03:00:00,49.6,134.501,33.934,32.468 +2020-12-28 03:15:00,51.33,136.524,33.934,32.468 +2020-12-28 03:30:00,50.91,138.001,33.934,32.468 +2020-12-28 03:45:00,51.45,138.749,33.934,32.468 +2020-12-28 04:00:00,51.82,151.371,34.107,32.468 +2020-12-28 04:15:00,52.15,163.84,34.107,32.468 +2020-12-28 04:30:00,53.03,166.226,34.107,32.468 +2020-12-28 04:45:00,54.7,167.53099999999998,34.107,32.468 +2020-12-28 05:00:00,57.21,196.475,39.575,32.468 +2020-12-28 05:15:00,58.21,225.044,39.575,32.468 +2020-12-28 05:30:00,58.82,222.28799999999998,39.575,32.468 +2020-12-28 05:45:00,60.69,214.507,39.575,32.468 +2020-12-28 06:00:00,63.74,211.99599999999998,56.156000000000006,32.468 +2020-12-28 06:15:00,64.22,215.868,56.156000000000006,32.468 +2020-12-28 06:30:00,66.48,219.101,56.156000000000006,32.468 +2020-12-28 06:45:00,67.97,222.567,56.156000000000006,32.468 +2020-12-28 07:00:00,71.66,222.68099999999998,67.926,32.468 +2020-12-28 07:15:00,73.71,227.53,67.926,32.468 +2020-12-28 07:30:00,73.74,230.484,67.926,32.468 +2020-12-28 07:45:00,76.15,231.332,67.926,32.468 +2020-12-28 08:00:00,78.92,230.185,72.58,32.468 +2020-12-28 08:15:00,77.89,230.88400000000001,72.58,32.468 +2020-12-28 08:30:00,79.51,229.27,72.58,32.468 +2020-12-28 08:45:00,81.98,226.108,72.58,32.468 +2020-12-28 09:00:00,84.91,219.989,66.984,32.468 +2020-12-28 09:15:00,86.68,215.032,66.984,32.468 +2020-12-28 09:30:00,86.35,211.93599999999998,66.984,32.468 +2020-12-28 09:45:00,87.57,209.25400000000002,66.984,32.468 +2020-12-28 10:00:00,88.75,205.554,63.158,32.468 +2020-12-28 10:15:00,89.35,202.032,63.158,32.468 +2020-12-28 10:30:00,90.53,199.482,63.158,32.468 +2020-12-28 10:45:00,92.46,197.829,63.158,32.468 +2020-12-28 11:00:00,92.51,195.31900000000002,61.141000000000005,32.468 +2020-12-28 11:15:00,93.89,194.49599999999998,61.141000000000005,32.468 +2020-12-28 11:30:00,95.76,194.61599999999999,61.141000000000005,32.468 +2020-12-28 11:45:00,98.55,192.44299999999998,61.141000000000005,32.468 +2020-12-28 12:00:00,96.46,188.148,57.961000000000006,32.468 +2020-12-28 12:15:00,96.89,187.805,57.961000000000006,32.468 +2020-12-28 12:30:00,94.61,187.355,57.961000000000006,32.468 +2020-12-28 12:45:00,93.99,188.53,57.961000000000006,32.468 +2020-12-28 13:00:00,91.48,187.59599999999998,56.843,32.468 +2020-12-28 13:15:00,89.62,187.41299999999998,56.843,32.468 +2020-12-28 13:30:00,87.6,185.91299999999998,56.843,32.468 +2020-12-28 13:45:00,89.12,185.547,56.843,32.468 +2020-12-28 14:00:00,86.23,184.21599999999998,55.992,32.468 +2020-12-28 14:15:00,84.24,184.503,55.992,32.468 +2020-12-28 14:30:00,84.27,184.58700000000002,55.992,32.468 +2020-12-28 14:45:00,84.15,184.78400000000002,55.992,32.468 +2020-12-28 15:00:00,83.99,186.304,57.523,32.468 +2020-12-28 15:15:00,84.98,186.33,57.523,32.468 +2020-12-28 15:30:00,82.61,188.26,57.523,32.468 +2020-12-28 15:45:00,84.31,190.09599999999998,57.523,32.468 +2020-12-28 16:00:00,90.69,191.83599999999998,59.471000000000004,32.468 +2020-12-28 16:15:00,89.04,192.96099999999998,59.471000000000004,32.468 +2020-12-28 16:30:00,91.46,194.821,59.471000000000004,32.468 +2020-12-28 16:45:00,92.1,196.054,59.471000000000004,32.468 +2020-12-28 17:00:00,95.79,198.107,65.066,32.468 +2020-12-28 17:15:00,96.36,199.09599999999998,65.066,32.468 +2020-12-28 17:30:00,94.64,199.445,65.066,32.468 +2020-12-28 17:45:00,94.08,199.503,65.066,32.468 +2020-12-28 18:00:00,92.03,201.101,69.581,32.468 +2020-12-28 18:15:00,92.63,199.59799999999998,69.581,32.468 +2020-12-28 18:30:00,92.68,198.426,69.581,32.468 +2020-12-28 18:45:00,95.26,197.632,69.581,32.468 +2020-12-28 19:00:00,91.26,198.58,73.771,32.468 +2020-12-28 19:15:00,88.66,195.235,73.771,32.468 +2020-12-28 19:30:00,89.79,193.85,73.771,32.468 +2020-12-28 19:45:00,85.86,189.838,73.771,32.468 +2020-12-28 20:00:00,81.83,186.13400000000001,65.035,32.468 +2020-12-28 20:15:00,80.18,181.06099999999998,65.035,32.468 +2020-12-28 20:30:00,78.48,176.19099999999997,65.035,32.468 +2020-12-28 20:45:00,77.08,174.206,65.035,32.468 +2020-12-28 21:00:00,73.88,172.676,58.7,32.468 +2020-12-28 21:15:00,73.68,169.542,58.7,32.468 +2020-12-28 21:30:00,72.87,168.12400000000002,58.7,32.468 +2020-12-28 21:45:00,72.61,166.44099999999997,58.7,32.468 +2020-12-28 22:00:00,69.98,158.253,53.888000000000005,32.468 +2020-12-28 22:15:00,69.76,153.156,53.888000000000005,32.468 +2020-12-28 22:30:00,68.69,139.464,53.888000000000005,32.468 +2020-12-28 22:45:00,68.64,131.43200000000002,53.888000000000005,32.468 +2020-12-28 23:00:00,64.36,125.7,45.501999999999995,32.468 +2020-12-28 23:15:00,66.25,124.46799999999999,45.501999999999995,32.468 +2020-12-28 23:30:00,64.12,125.277,45.501999999999995,32.468 +2020-12-28 23:45:00,64.06,125.116,45.501999999999995,32.468 +2020-12-29 00:00:00,57.18,118.78299999999999,43.537,32.468 +2020-12-29 00:15:00,56.17,118.086,43.537,32.468 +2020-12-29 00:30:00,56.18,118.84700000000001,43.537,32.468 +2020-12-29 00:45:00,56.31,119.844,43.537,32.468 +2020-12-29 01:00:00,53.79,122.324,41.854,32.468 +2020-12-29 01:15:00,53.88,123.124,41.854,32.468 +2020-12-29 01:30:00,50.49,123.676,41.854,32.468 +2020-12-29 01:45:00,53.7,124.057,41.854,32.468 +2020-12-29 02:00:00,51.98,125.79,40.321,32.468 +2020-12-29 02:15:00,53.31,127.184,40.321,32.468 +2020-12-29 02:30:00,53.66,127.334,40.321,32.468 +2020-12-29 02:45:00,53.8,129.121,40.321,32.468 +2020-12-29 03:00:00,53.3,131.494,39.632,32.468 +2020-12-29 03:15:00,58.73,132.754,39.632,32.468 +2020-12-29 03:30:00,61.45,134.694,39.632,32.468 +2020-12-29 03:45:00,56.91,135.592,39.632,32.468 +2020-12-29 04:00:00,58.6,147.961,40.183,32.468 +2020-12-29 04:15:00,58.92,160.092,40.183,32.468 +2020-12-29 04:30:00,58.73,162.172,40.183,32.468 +2020-12-29 04:45:00,60.76,164.65200000000002,40.183,32.468 +2020-12-29 05:00:00,65.7,198.412,43.945,32.468 +2020-12-29 05:15:00,68.23,226.771,43.945,32.468 +2020-12-29 05:30:00,72.46,222.56099999999998,43.945,32.468 +2020-12-29 05:45:00,78.0,214.78,43.945,32.468 +2020-12-29 06:00:00,86.13,211.19,56.048,32.468 +2020-12-29 06:15:00,90.88,216.63400000000001,56.048,32.468 +2020-12-29 06:30:00,98.39,219.25900000000001,56.048,32.468 +2020-12-29 06:45:00,101.09,222.36700000000002,56.048,32.468 +2020-12-29 07:00:00,109.02,222.32,65.74,32.468 +2020-12-29 07:15:00,110.78,227.003,65.74,32.468 +2020-12-29 07:30:00,111.89,229.425,65.74,32.468 +2020-12-29 07:45:00,116.47,230.43099999999998,65.74,32.468 +2020-12-29 08:00:00,119.28,229.387,72.757,32.468 +2020-12-29 08:15:00,119.18,229.077,72.757,32.468 +2020-12-29 08:30:00,121.46,227.28400000000002,72.757,32.468 +2020-12-29 08:45:00,123.85,223.81599999999997,72.757,32.468 +2020-12-29 09:00:00,126.31,216.889,67.692,32.468 +2020-12-29 09:15:00,127.18,213.46900000000002,67.692,32.468 +2020-12-29 09:30:00,129.54,211.058,67.692,32.468 +2020-12-29 09:45:00,127.09,208.2,67.692,32.468 +2020-12-29 10:00:00,130.14,203.892,63.506,32.468 +2020-12-29 10:15:00,131.8,199.34099999999998,63.506,32.468 +2020-12-29 10:30:00,131.24,196.954,63.506,32.468 +2020-12-29 10:45:00,133.38,195.604,63.506,32.468 +2020-12-29 11:00:00,132.11,194.502,60.758,32.468 +2020-12-29 11:15:00,132.81,193.368,60.758,32.468 +2020-12-29 11:30:00,132.74,192.33700000000002,60.758,32.468 +2020-12-29 11:45:00,132.51,190.82,60.758,32.468 +2020-12-29 12:00:00,133.01,185.213,57.519,32.468 +2020-12-29 12:15:00,132.94,184.5,57.519,32.468 +2020-12-29 12:30:00,132.61,184.77200000000002,57.519,32.468 +2020-12-29 12:45:00,131.32,185.69400000000002,57.519,32.468 +2020-12-29 13:00:00,129.11,184.345,56.46,32.468 +2020-12-29 13:15:00,129.11,183.888,56.46,32.468 +2020-12-29 13:30:00,128.11,183.49900000000002,56.46,32.468 +2020-12-29 13:45:00,128.44,183.268,56.46,32.468 +2020-12-29 14:00:00,129.73,182.227,56.207,32.468 +2020-12-29 14:15:00,128.01,182.65400000000002,56.207,32.468 +2020-12-29 14:30:00,127.57,183.363,56.207,32.468 +2020-12-29 14:45:00,128.05,183.468,56.207,32.468 +2020-12-29 15:00:00,125.11,184.584,57.391999999999996,32.468 +2020-12-29 15:15:00,124.05,184.94799999999998,57.391999999999996,32.468 +2020-12-29 15:30:00,122.28,187.088,57.391999999999996,32.468 +2020-12-29 15:45:00,122.29,188.547,57.391999999999996,32.468 +2020-12-29 16:00:00,126.09,190.58,59.955,32.468 +2020-12-29 16:15:00,127.33,192.178,59.955,32.468 +2020-12-29 16:30:00,131.25,194.65200000000002,59.955,32.468 +2020-12-29 16:45:00,132.28,196.199,59.955,32.468 +2020-12-29 17:00:00,135.14,198.761,67.063,32.468 +2020-12-29 17:15:00,134.64,199.817,67.063,32.468 +2020-12-29 17:30:00,135.54,200.817,67.063,32.468 +2020-12-29 17:45:00,135.22,200.755,67.063,32.468 +2020-12-29 18:00:00,135.98,202.24900000000002,71.477,32.468 +2020-12-29 18:15:00,133.12,200.295,71.477,32.468 +2020-12-29 18:30:00,132.74,198.825,71.477,32.468 +2020-12-29 18:45:00,132.76,198.80900000000003,71.477,32.468 +2020-12-29 19:00:00,129.1,199.782,74.32,32.468 +2020-12-29 19:15:00,131.66,196.169,74.32,32.468 +2020-12-29 19:30:00,131.57,194.13,74.32,32.468 +2020-12-29 19:45:00,134.28,190.15400000000002,74.32,32.468 +2020-12-29 20:00:00,124.16,186.60299999999998,66.157,32.468 +2020-12-29 20:15:00,118.77,180.87400000000002,66.157,32.468 +2020-12-29 20:30:00,114.09,176.968,66.157,32.468 +2020-12-29 20:45:00,113.21,174.458,66.157,32.468 +2020-12-29 21:00:00,104.2,172.25900000000001,59.806000000000004,32.468 +2020-12-29 21:15:00,110.79,169.947,59.806000000000004,32.468 +2020-12-29 21:30:00,106.78,167.829,59.806000000000004,32.468 +2020-12-29 21:45:00,106.31,166.38299999999998,59.806000000000004,32.468 +2020-12-29 22:00:00,95.44,159.85399999999998,54.785,32.468 +2020-12-29 22:15:00,92.96,154.50799999999998,54.785,32.468 +2020-12-29 22:30:00,85.11,140.935,54.785,32.468 +2020-12-29 22:45:00,86.87,133.178,54.785,32.468 +2020-12-29 23:00:00,82.77,127.431,47.176,32.468 +2020-12-29 23:15:00,84.86,125.367,47.176,32.468 +2020-12-29 23:30:00,77.52,125.84200000000001,47.176,32.468 +2020-12-29 23:45:00,78.36,125.257,47.176,32.468 +2020-12-30 00:00:00,81.02,118.954,43.42,32.468 +2020-12-30 00:15:00,81.82,118.23299999999999,43.42,32.468 +2020-12-30 00:30:00,81.73,118.985,43.42,32.468 +2020-12-30 00:45:00,76.13,119.964,43.42,32.468 +2020-12-30 01:00:00,70.28,122.458,40.869,32.468 +2020-12-30 01:15:00,74.3,123.25299999999999,40.869,32.468 +2020-12-30 01:30:00,77.22,123.807,40.869,32.468 +2020-12-30 01:45:00,76.85,124.179,40.869,32.468 +2020-12-30 02:00:00,73.1,125.92399999999999,39.541,32.468 +2020-12-30 02:15:00,70.43,127.319,39.541,32.468 +2020-12-30 02:30:00,71.98,127.475,39.541,32.468 +2020-12-30 02:45:00,76.52,129.263,39.541,32.468 +2020-12-30 03:00:00,75.41,131.627,39.052,32.468 +2020-12-30 03:15:00,74.83,132.908,39.052,32.468 +2020-12-30 03:30:00,72.89,134.84799999999998,39.052,32.468 +2020-12-30 03:45:00,79.01,135.749,39.052,32.468 +2020-12-30 04:00:00,76.82,148.096,40.36,32.468 +2020-12-30 04:15:00,72.95,160.222,40.36,32.468 +2020-12-30 04:30:00,72.14,162.297,40.36,32.468 +2020-12-30 04:45:00,80.94,164.775,40.36,32.468 +2020-12-30 05:00:00,88.48,198.503,43.133,32.468 +2020-12-30 05:15:00,91.18,226.833,43.133,32.468 +2020-12-30 05:30:00,90.88,222.62900000000002,43.133,32.468 +2020-12-30 05:45:00,93.94,214.865,43.133,32.468 +2020-12-30 06:00:00,101.26,211.298,54.953,32.468 +2020-12-30 06:15:00,106.96,216.748,54.953,32.468 +2020-12-30 06:30:00,112.28,219.395,54.953,32.468 +2020-12-30 06:45:00,115.14,222.53400000000002,54.953,32.468 +2020-12-30 07:00:00,123.05,222.49900000000002,66.566,32.468 +2020-12-30 07:15:00,123.6,227.174,66.566,32.468 +2020-12-30 07:30:00,126.82,229.58599999999998,66.566,32.468 +2020-12-30 07:45:00,128.84,230.578,66.566,32.468 +2020-12-30 08:00:00,131.65,229.533,72.902,32.468 +2020-12-30 08:15:00,130.78,229.21,72.902,32.468 +2020-12-30 08:30:00,132.01,227.4,72.902,32.468 +2020-12-30 08:45:00,132.85,223.91099999999997,72.902,32.468 +2020-12-30 09:00:00,134.02,216.967,68.465,32.468 +2020-12-30 09:15:00,135.31,213.55200000000002,68.465,32.468 +2020-12-30 09:30:00,136.11,211.15400000000002,68.465,32.468 +2020-12-30 09:45:00,137.25,208.28900000000002,68.465,32.468 +2020-12-30 10:00:00,135.85,203.97799999999998,63.625,32.468 +2020-12-30 10:15:00,138.58,199.425,63.625,32.468 +2020-12-30 10:30:00,138.23,197.024,63.625,32.468 +2020-12-30 10:45:00,136.57,195.674,63.625,32.468 +2020-12-30 11:00:00,133.24,194.555,61.628,32.468 +2020-12-30 11:15:00,126.44,193.417,61.628,32.468 +2020-12-30 11:30:00,130.71,192.387,61.628,32.468 +2020-12-30 11:45:00,129.54,190.87099999999998,61.628,32.468 +2020-12-30 12:00:00,125.24,185.268,58.708999999999996,32.468 +2020-12-30 12:15:00,124.69,184.571,58.708999999999996,32.468 +2020-12-30 12:30:00,123.95,184.84400000000002,58.708999999999996,32.468 +2020-12-30 12:45:00,129.68,185.769,58.708999999999996,32.468 +2020-12-30 13:00:00,133.73,184.407,57.373000000000005,32.468 +2020-12-30 13:15:00,136.53,183.94400000000002,57.373000000000005,32.468 +2020-12-30 13:30:00,133.6,183.545,57.373000000000005,32.468 +2020-12-30 13:45:00,133.51,183.30599999999998,57.373000000000005,32.468 +2020-12-30 14:00:00,132.59,182.271,57.684,32.468 +2020-12-30 14:15:00,131.17,182.69400000000002,57.684,32.468 +2020-12-30 14:30:00,128.89,183.417,57.684,32.468 +2020-12-30 14:45:00,127.23,183.533,57.684,32.468 +2020-12-30 15:00:00,128.01,184.666,58.03,32.468 +2020-12-30 15:15:00,127.89,185.017,58.03,32.468 +2020-12-30 15:30:00,127.38,187.16,58.03,32.468 +2020-12-30 15:45:00,128.01,188.612,58.03,32.468 +2020-12-30 16:00:00,131.71,190.645,59.97,32.468 +2020-12-30 16:15:00,132.63,192.255,59.97,32.468 +2020-12-30 16:30:00,136.55,194.735,59.97,32.468 +2020-12-30 16:45:00,136.97,196.297,59.97,32.468 +2020-12-30 17:00:00,140.13,198.84099999999998,65.661,32.468 +2020-12-30 17:15:00,138.72,199.924,65.661,32.468 +2020-12-30 17:30:00,136.98,200.94799999999998,65.661,32.468 +2020-12-30 17:45:00,138.66,200.9,65.661,32.468 +2020-12-30 18:00:00,138.81,202.41299999999998,70.96300000000001,32.468 +2020-12-30 18:15:00,137.74,200.455,70.96300000000001,32.468 +2020-12-30 18:30:00,135.58,198.99099999999999,70.96300000000001,32.468 +2020-12-30 18:45:00,136.58,198.989,70.96300000000001,32.468 +2020-12-30 19:00:00,133.41,199.937,74.133,32.468 +2020-12-30 19:15:00,132.16,196.324,74.133,32.468 +2020-12-30 19:30:00,129.15,194.283,74.133,32.468 +2020-12-30 19:45:00,128.43,190.3,74.133,32.468 +2020-12-30 20:00:00,120.97,186.738,65.613,32.468 +2020-12-30 20:15:00,116.68,181.007,65.613,32.468 +2020-12-30 20:30:00,112.92,177.08900000000003,65.613,32.468 +2020-12-30 20:45:00,113.77,174.59599999999998,65.613,32.468 +2020-12-30 21:00:00,108.79,172.378,58.583,32.468 +2020-12-30 21:15:00,113.74,170.051,58.583,32.468 +2020-12-30 21:30:00,111.85,167.933,58.583,32.468 +2020-12-30 21:45:00,107.84,166.5,58.583,32.468 +2020-12-30 22:00:00,97.67,159.97,54.411,32.468 +2020-12-30 22:15:00,98.11,154.636,54.411,32.468 +2020-12-30 22:30:00,96.26,141.086,54.411,32.468 +2020-12-30 22:45:00,94.93,133.338,54.411,32.468 +2020-12-30 23:00:00,90.4,127.57,47.878,32.468 +2020-12-30 23:15:00,82.73,125.509,47.878,32.468 +2020-12-30 23:30:00,77.9,125.99799999999999,47.878,32.468 +2020-12-30 23:45:00,78.08,125.40899999999999,47.878,32.468 +2020-12-31 00:00:00,72.56,119.118,44.513000000000005,32.468 +2020-12-31 00:15:00,75.51,118.375,44.513000000000005,32.468 +2020-12-31 00:30:00,76.2,119.11399999999999,44.513000000000005,32.468 +2020-12-31 00:45:00,79.94,120.07600000000001,44.513000000000005,32.468 +2020-12-31 01:00:00,76.22,122.584,43.169,32.468 +2020-12-31 01:15:00,75.52,123.374,43.169,32.468 +2020-12-31 01:30:00,69.71,123.929,43.169,32.468 +2020-12-31 01:45:00,72.15,124.29,43.169,32.468 +2020-12-31 02:00:00,68.46,126.051,41.763999999999996,32.468 +2020-12-31 02:15:00,68.93,127.444,41.763999999999996,32.468 +2020-12-31 02:30:00,69.13,127.60799999999999,41.763999999999996,32.468 +2020-12-31 02:45:00,71.05,129.394,41.763999999999996,32.468 +2020-12-31 03:00:00,77.24,131.754,41.155,32.468 +2020-12-31 03:15:00,78.88,133.054,41.155,32.468 +2020-12-31 03:30:00,79.6,134.993,41.155,32.468 +2020-12-31 03:45:00,73.47,135.89700000000002,41.155,32.468 +2020-12-31 04:00:00,71.66,148.222,41.96,32.468 +2020-12-31 04:15:00,72.6,160.344,41.96,32.468 +2020-12-31 04:30:00,74.43,162.416,41.96,32.468 +2020-12-31 04:45:00,76.31,164.89,41.96,32.468 +2020-12-31 05:00:00,80.9,198.58599999999998,45.206,32.468 +2020-12-31 05:15:00,83.91,226.886,45.206,32.468 +2020-12-31 05:30:00,88.55,222.68900000000002,45.206,32.468 +2020-12-31 05:45:00,92.98,214.942,45.206,32.468 +2020-12-31 06:00:00,101.92,211.398,55.398999999999994,32.468 +2020-12-31 06:15:00,106.02,216.854,55.398999999999994,32.468 +2020-12-31 06:30:00,110.67,219.52200000000002,55.398999999999994,32.468 +2020-12-31 06:45:00,115.36,222.69,55.398999999999994,32.468 +2020-12-31 07:00:00,121.21,222.667,64.627,32.468 +2020-12-31 07:15:00,125.44,227.334,64.627,32.468 +2020-12-31 07:30:00,127.04,229.737,64.627,32.468 +2020-12-31 07:45:00,129.3,230.713,64.627,32.468 +2020-12-31 08:00:00,131.83,229.668,70.895,32.468 +2020-12-31 08:15:00,129.92,229.331,70.895,32.468 +2020-12-31 08:30:00,130.03,227.505,70.895,32.468 +2020-12-31 08:45:00,129.65,223.993,70.895,32.468 +2020-12-31 09:00:00,130.4,217.032,66.382,32.468 +2020-12-31 09:15:00,132.18,213.62400000000002,66.382,32.468 +2020-12-31 09:30:00,133.68,211.239,66.382,32.468 +2020-12-31 09:45:00,133.99,208.36700000000002,66.382,32.468 +2020-12-31 10:00:00,133.44,204.053,62.739,32.468 +2020-12-31 10:15:00,135.64,199.49599999999998,62.739,32.468 +2020-12-31 10:30:00,134.92,197.084,62.739,32.468 +2020-12-31 10:45:00,136.65,195.734,62.739,32.468 +2020-12-31 11:00:00,136.01,194.59799999999998,60.843,32.468 +2020-12-31 11:15:00,135.6,193.455,60.843,32.468 +2020-12-31 11:30:00,135.49,192.428,60.843,32.468 +2020-12-31 11:45:00,136.56,190.91299999999998,60.843,32.468 +2020-12-31 12:00:00,136.24,185.315,58.466,32.468 +2020-12-31 12:15:00,135.89,184.63299999999998,58.466,32.468 +2020-12-31 12:30:00,133.92,184.908,58.466,32.468 +2020-12-31 12:45:00,134.1,185.834,58.466,32.468 +2020-12-31 13:00:00,130.94,184.46,56.883,32.468 +2020-12-31 13:15:00,129.98,183.99,56.883,32.468 +2020-12-31 13:30:00,129.31,183.582,56.883,32.468 +2020-12-31 13:45:00,127.59,183.33599999999998,56.883,32.468 +2020-12-31 14:00:00,126.5,182.30700000000002,56.503,32.468 +2020-12-31 14:15:00,129.81,182.727,56.503,32.468 +2020-12-31 14:30:00,128.98,183.46099999999998,56.503,32.468 +2020-12-31 14:45:00,127.66,183.59,56.503,32.468 +2020-12-31 15:00:00,131.69,184.74,57.803999999999995,32.468 +2020-12-31 15:15:00,134.08,185.078,57.803999999999995,32.468 +2020-12-31 15:30:00,130.49,187.222,57.803999999999995,32.468 +2020-12-31 15:45:00,132.71,188.668,57.803999999999995,32.468 +2020-12-31 16:00:00,134.12,190.7,59.379,32.468 +2020-12-31 16:15:00,134.79,192.322,59.379,32.468 +2020-12-31 16:30:00,137.68,194.80599999999998,59.379,32.468 +2020-12-31 16:45:00,138.3,196.382,59.379,32.468 +2020-12-31 17:00:00,141.23,198.90900000000002,64.71600000000001,32.468 +2020-12-31 17:15:00,140.55,200.021,64.71600000000001,32.468 +2020-12-31 17:30:00,141.63,201.067,64.71600000000001,32.468 +2020-12-31 17:45:00,141.55,201.035,64.71600000000001,32.468 +2020-12-31 18:00:00,140.02,202.56799999999998,68.803,32.468 +2020-12-31 18:15:00,138.28,200.606,68.803,32.468 +2020-12-31 18:30:00,137.19,199.14700000000002,68.803,32.468 +2020-12-31 18:45:00,137.6,199.16,68.803,32.468 +2020-12-31 19:00:00,134.26,200.084,72.934,32.468 +2020-12-31 19:15:00,133.22,196.468,72.934,32.468 +2020-12-31 19:30:00,134.64,194.42700000000002,72.934,32.468 +2020-12-31 19:45:00,136.88,190.43900000000002,72.934,32.468 +2020-12-31 20:00:00,130.24,186.864,65.175,32.468 +2020-12-31 20:15:00,119.33,181.132,65.175,32.468 +2020-12-31 20:30:00,118.31,177.201,65.175,32.468 +2020-12-31 20:45:00,112.7,174.72400000000002,65.175,32.468 +2020-12-31 21:00:00,107.44,172.49,58.55,32.468 +2020-12-31 21:15:00,112.86,170.146,58.55,32.468 +2020-12-31 21:30:00,111.93,168.03,58.55,32.468 +2020-12-31 21:45:00,108.29,166.609,58.55,32.468 +2020-12-31 22:00:00,98.52,160.079,55.041000000000004,32.468 +2020-12-31 22:15:00,94.85,154.757,55.041000000000004,32.468 +2020-12-31 22:30:00,98.1,141.22899999999998,55.041000000000004,32.468 +2020-12-31 22:45:00,96.62,133.488,55.041000000000004,32.468 +2020-12-31 23:00:00,92.22,127.7,48.258,32.468 +2020-12-31 23:15:00,84.12,125.64200000000001,48.258,32.468 +2020-12-31 23:30:00,79.31,126.145,48.258,32.468 +2020-12-31 23:45:00,83.54,125.553,48.258,32.468 diff --git a/examples/05_Two-stage-optimization/two_stage_optimization.py b/examples/05_Two-stage-optimization/two_stage_optimization.py new file mode 100644 index 000000000..5e19d54c1 --- /dev/null +++ b/examples/05_Two-stage-optimization/two_stage_optimization.py @@ -0,0 +1,148 @@ +""" +This script demonstrates how to use the different calcualtion types in the flixOPt framework +to model the same energy system. THe Results will be compared to each other. +""" + +import logging +import pathlib +import timeit + +import pandas as pd +import xarray as xr + +import flixopt as fx + +logger = logging.getLogger('flixopt') + +if __name__ == '__main__': + # Data Import + data_import = pd.read_csv(pathlib.Path('Zeitreihen2020.csv'), index_col=0).sort_index() + filtered_data = data_import[:500] + + filtered_data.index = pd.to_datetime(filtered_data.index) + timesteps = filtered_data.index + + # Access specific columns and convert to 1D-numpy array + electricity_demand = filtered_data['P_Netz/MW'].to_numpy() + heat_demand = filtered_data['Q_Netz/MW'].to_numpy() + electricity_price = filtered_data['Strompr.€/MWh'].to_numpy() + gas_price = filtered_data['Gaspr.€/MWh'].to_numpy() + + flow_system = fx.FlowSystem(timesteps) + flow_system.add_elements( + fx.Bus('Strom'), + fx.Bus('Fernwärme'), + fx.Bus('Gas'), + fx.Bus('Kohle'), + fx.Effect('costs', '€', 'Kosten', is_standard=True, is_objective=True), + fx.Effect('CO2', 'kg', 'CO2_e-Emissionen'), + fx.Effect('PE', 'kWh_PE', 'Primärenergie'), + fx.linear_converters.Boiler( + 'Kessel', + eta=0.85, + Q_th=fx.Flow(label='Q_th', bus='Fernwärme'), + Q_fu=fx.Flow( + label='Q_fu', + bus='Gas', + size=fx.InvestParameters(specific_effects={'costs': 1_000}, minimum_size=10, maximum_size=500), + relative_minimum=0.2, + previous_flow_rate=20, + on_off_parameters=fx.OnOffParameters(effects_per_switch_on=300), + ), + ), + fx.linear_converters.CHP( + 'BHKW2', + eta_th=0.58, + eta_el=0.22, + on_off_parameters=fx.OnOffParameters(effects_per_switch_on=1_000, consecutive_on_hours_min=10, consecutive_off_hours_min=10), + P_el=fx.Flow('P_el', bus='Strom'), + Q_th=fx.Flow('Q_th', bus='Fernwärme'), + Q_fu=fx.Flow('Q_fu', bus='Kohle', + size=fx.InvestParameters(specific_effects={'costs':3_000}, minimum_size=10, maximum_size=500), + relative_minimum=0.3, previous_flow_rate=100), + ), + fx.Storage( + 'Speicher', + capacity_in_flow_hours=684, + initial_charge_state=137, + minimal_final_charge_state=137, + maximal_final_charge_state=158, + eta_charge=1, + eta_discharge=1, + relative_loss_per_hour=0.001, + prevent_simultaneous_charge_and_discharge=True, + charging=fx.Flow('Q_th_load', size=137, bus='Fernwärme'), + discharging=fx.Flow('Q_th_unload', size=158, bus='Fernwärme'), + ), + fx.Sink( + 'Wärmelast', sink=fx.Flow('Q_th_Last', bus='Fernwärme', size=1, fixed_relative_profile=heat_demand) + ), + fx.Source( + 'Gastarif', + source=fx.Flow('Q_Gas', bus='Gas', size=1000, effects_per_flow_hour={'costs': gas_price, 'CO2': 0.3}), + ), + fx.Source( + 'Kohletarif', + source=fx.Flow('Q_Kohle', bus='Kohle', size=1000, effects_per_flow_hour={'costs': 4.6, 'CO2': 0.3}), + ), + fx.Source( + 'Einspeisung', + source=fx.Flow('P_el', bus='Strom', size=1000, effects_per_flow_hour={'costs': electricity_price + 0.5, 'CO2': 0.3}), + ), + fx.Sink( + 'Stromlast', + sink=fx.Flow('P_el_Last', bus='Strom', size=1, fixed_relative_profile=electricity_demand), + ), + fx.Source( + 'Stromtarif', + source=fx.Flow('P_el', bus='Strom', size=1000, effects_per_flow_hour={'costs': electricity_price, 'CO2': 0.3}), + ), + ) + + # Separate optimization of flow sizes and dispatch + start = timeit.default_timer() + calculation_sizing = fx.FullCalculation('Sizing', flow_system.resample('4h')) + calculation_sizing.do_modeling() + calculation_sizing.solve(fx.solvers.HighsSolver(0.1/100, 600)) + timer_sizing = timeit.default_timer() - start + flow_sizes = xr.Dataset({flow.size.name: flow.size for flow in calculation_sizing.results.flows.values()}) + + calculation_dispatch = fx.FullCalculation('Sizing', flow_system) + calculation_dispatch.do_modeling() + for name, da in flow_sizes.data_vars.items(): + if name in calculation_dispatch.model.variables: + con = calculation_dispatch.model.add_constraints( + calculation_dispatch.model[name] == da, + name=f'{name}_fixing', + ) + logger.info(f'Constraint {con.name} added:\n{con}') + + calculation_dispatch.solve(fx.solvers.HighsSolver(0.1 / 100, 600)) + timer_dispatch = timeit.default_timer() - start + + if (calculation_dispatch.results.sizes().round(5) == calculation_sizing.results.sizes().round(5)).all(): + logger.info('Sizes where correctly equalized') + else: + raise RuntimeError('Sizes where not correctly equalized') + + # Optimization of both flow sizes and dispatch together + start = timeit.default_timer() + calculation_combined = fx.FullCalculation('Sizing', flow_system) + calculation_combined.do_modeling() + calculation_combined.solve(fx.solvers.HighsSolver(0.1/100, 600)) + timer_combined = timeit.default_timer() - start + + # Comparison of results + comparison = xr.concat( + [calculation_combined.results.solution, calculation_dispatch.results.solution], dim='mode' + ).assign_coords(mode=['Combined', 'Two-stage']) + comparison['Duration [s]'] = xr.DataArray([timer_combined, timer_sizing + timer_dispatch], dims='mode') + + comparison_main = comparison[['Duration [s]', 'costs|total', 'costs(invest)|total', 'costs(operation)|total', 'BHKW2(Q_fu)|size', 'Kessel(Q_fu)|size']] + comparison_main = xr.concat([ + comparison_main, + ((comparison_main.sel(mode='Two-stage') - comparison_main.sel(mode='Combined')) + / comparison_main.sel(mode='Combined') * 100).assign_coords(mode='Diff [%]') + ], dim='mode') + + print(comparison_main.to_pandas().T.round(2)) From ef0acfc38c9f12868551e6ec14109b576aa95a23 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 16 Jul 2025 10:24:30 +0200 Subject: [PATCH 178/448] Add example that leverages resampling adn fixing of Investments --- .../two_stage_optimization.py | 45 +++++++++---------- flixopt/calculation.py | 23 ++++++++++ 2 files changed, 43 insertions(+), 25 deletions(-) diff --git a/examples/05_Two-stage-optimization/two_stage_optimization.py b/examples/05_Two-stage-optimization/two_stage_optimization.py index 5e19d54c1..7f0a412bf 100644 --- a/examples/05_Two-stage-optimization/two_stage_optimization.py +++ b/examples/05_Two-stage-optimization/two_stage_optimization.py @@ -1,6 +1,10 @@ """ -This script demonstrates how to use the different calcualtion types in the flixOPt framework -to model the same energy system. THe Results will be compared to each other. +This script demonstrates how to use downsampling of a FlowSystem to effectively reduce the size of a model. +This can be very useful when working with large models or during developement state, +as it can drastically reduce the computational time. +This leads to faster results and easier debugging. +A common use case is to do optimize the investments of a model with a downsampled version of the original model, and than fix the computed sizes when calculating th actual dispatch. +While the final optimum might differ from the fglobal optimum, the solving will be much faster. """ import logging @@ -38,17 +42,17 @@ fx.Effect('CO2', 'kg', 'CO2_e-Emissionen'), fx.Effect('PE', 'kWh_PE', 'Primärenergie'), fx.linear_converters.Boiler( - 'Kessel', - eta=0.85, - Q_th=fx.Flow(label='Q_th', bus='Fernwärme'), - Q_fu=fx.Flow( - label='Q_fu', - bus='Gas', - size=fx.InvestParameters(specific_effects={'costs': 1_000}, minimum_size=10, maximum_size=500), - relative_minimum=0.2, - previous_flow_rate=20, - on_off_parameters=fx.OnOffParameters(effects_per_switch_on=300), - ), + 'Kessel', + eta=0.85, + Q_th=fx.Flow(label='Q_th', bus='Fernwärme'), + Q_fu=fx.Flow( + label='Q_fu', + bus='Gas', + size=fx.InvestParameters(specific_effects={'costs': 1_000}, minimum_size=10, maximum_size=500), + relative_minimum=0.2, + previous_flow_rate=20, + on_off_parameters=fx.OnOffParameters(effects_per_switch_on=300), + ), ), fx.linear_converters.CHP( 'BHKW2', @@ -63,10 +67,8 @@ ), fx.Storage( 'Speicher', - capacity_in_flow_hours=684, - initial_charge_state=137, - minimal_final_charge_state=137, - maximal_final_charge_state=158, + capacity_in_flow_hours=fx.InvestParameters(minimum_size=10, maximum_size=1000, specific_effects={'costs': 60}), + initial_charge_state='lastValueOfSim', eta_charge=1, eta_discharge=1, relative_loss_per_hour=0.001, @@ -109,14 +111,7 @@ calculation_dispatch = fx.FullCalculation('Sizing', flow_system) calculation_dispatch.do_modeling() - for name, da in flow_sizes.data_vars.items(): - if name in calculation_dispatch.model.variables: - con = calculation_dispatch.model.add_constraints( - calculation_dispatch.model[name] == da, - name=f'{name}_fixing', - ) - logger.info(f'Constraint {con.name} added:\n{con}') - + calculation_dispatch.fix_sizes(calculation_sizing.results.solution) calculation_dispatch.solve(fx.solvers.HighsSolver(0.1 / 100, 600)) timer_dispatch = timeit.default_timer() - start diff --git a/flixopt/calculation.py b/flixopt/calculation.py index 0fb735bef..88e686681 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -180,6 +180,29 @@ def do_modeling(self) -> SystemModel: self.durations['modeling'] = round(timeit.default_timer() - t_start, 2) return self.model + def fix_sizes(self, ds: xr.Dataset, decimal_rounding: Optional[int] = 5): + """Fix the sizes of the calculations to specified values. + + Args: + ds: The dataset that contains the variable names mapped to their sizes. If None, the dataset is loaded from the results. + decimal_rounding: The number of decimal places to round the sizes to. If no rounding is applied, numerical errors might lead to infeasibility. + """ + if decimal_rounding is not None: + ds = ds.round(decimal_rounding) + + for name, da in ds.data_vars.items(): + if '|size' not in name: + continue + if name not in self.model.variables: + logger.debug(f'Variable {name} not found in calculation model. Skipping.') + continue + + con = self.model.add_constraints( + self.model[name] == da, + name=f'{name}-fixed', + ) + logger.debug(f'Fixed "{name}":\n{con}') + def solve(self, solver: _Solver, log_file: Optional[pathlib.Path] = None, log_main_results: bool = True): t_start = timeit.default_timer() From 706c1ec919e28eba6ca454897365334e4e67a3d7 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 16 Jul 2025 10:36:37 +0200 Subject: [PATCH 179/448] Add flag to Calculation if its modeled --- flixopt/calculation.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/flixopt/calculation.py b/flixopt/calculation.py index 88e686681..e7ea8d053 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -95,6 +95,8 @@ def __init__( f'Folder {self.folder} and its parent do not exist. Please create them first.' ) from e + self._modeled = False + @property def main_results(self) -> Dict[str, Union[Scalar, Dict]]: from flixopt.features import InvestmentModel @@ -164,6 +166,10 @@ def active_timesteps(self) -> pd.DatetimeIndex: ) return self._active_timesteps + @property + def modeled(self) -> bool: + return True if self.model is not None else False + class FullCalculation(Calculation): """ @@ -187,6 +193,8 @@ def fix_sizes(self, ds: xr.Dataset, decimal_rounding: Optional[int] = 5): ds: The dataset that contains the variable names mapped to their sizes. If None, the dataset is loaded from the results. decimal_rounding: The number of decimal places to round the sizes to. If no rounding is applied, numerical errors might lead to infeasibility. """ + if not self.modeled: + raise RuntimeError('Model was not created. Call do_modeling() first.') if decimal_rounding is not None: ds = ds.round(decimal_rounding) From a4cdb433f48cb50ecce5fe2ccab2f7610d961013 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 16 Jul 2025 10:37:03 +0200 Subject: [PATCH 180/448] Make flag for connected_and_transformed FLowSystem public --- flixopt/flow_system.py | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 7001ca9e3..b0dc746bb 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -208,7 +208,7 @@ def to_dataset(self) -> xr.Dataset: Returns: xr.Dataset: Dataset containing all DataArrays with structure in attributes """ - if not self._connected_and_transformed: + if not self.connected_and_transformed: logger.warning('FlowSystem is not connected_and_transformed. Connecting and transforming data now.') self.connect_and_transform() @@ -278,7 +278,7 @@ def to_netcdf(self, path: Union[str, pathlib.Path], compression: int = 0): path: The path to the netCDF file. compression: The compression level to use when saving the file. """ - if not self._connected_and_transformed: + if not self.connected_and_transformed: logger.warning('FlowSystem is not connected. Calling connect_and_transform() now.') self.connect_and_transform() @@ -294,7 +294,7 @@ def get_structure(self, clean: bool = False, stats: bool = False) -> Dict: clean: If True, remove None and empty dicts and lists. stats: If True, replace DataArray references with statistics """ - if not self._connected_and_transformed: + if not self.connected_and_transformed: logger.warning('FlowSystem is not connected. Calling connect_and_transform() now.') self.connect_and_transform() @@ -308,7 +308,7 @@ def to_json(self, path: Union[str, pathlib.Path]): Args: path: The path to the JSON file. """ - if not self._connected_and_transformed: + if not self.connected_and_transformed: logger.warning('FlowSystem needs to be connected and transformed before saving to JSON. Calling connect_and_transform() now.') self.connect_and_transform() @@ -387,7 +387,7 @@ def connect_and_transform(self): logger.warning(f'Scenario weights are not normalized to 1. This is reccomended for a better scaled model. ' f'Sum of weights={self.scenario_weights.sum().item()}') - if not self._connected_and_transformed: + if not self.connected_and_transformed: self._connect_network() for element in list(self.components.values()) + list(self.effects.effects.values()) + list(self.buses.values()): element.transform_data(self) @@ -401,7 +401,7 @@ def add_elements(self, *elements: Element) -> None: *elements: childs of Element like Boiler, HeatPump, Bus,... modeling Elements """ - if self._connected_and_transformed: + if self.connected_and_transformed: warnings.warn( 'You are adding elements to an already connected FlowSystem. This is not recommended (But it works).', stacklevel=2, @@ -420,7 +420,7 @@ def add_elements(self, *elements: Element) -> None: ) def create_model(self) -> SystemModel: - if not self._connected_and_transformed: + if not self.connected_and_transformed: raise RuntimeError('FlowSystem is not connected_and_transformed. Call FlowSystem.connect_and_transform() first.') self.model = SystemModel(self) return self.model @@ -445,7 +445,7 @@ def plot_network( return plotting.plot_network(node_infos, edge_infos, path, controls, show) def network_infos(self) -> Tuple[Dict[str, Dict[str, str]], Dict[str, Dict[str, str]]]: - if not self._connected_and_transformed: + if not self.connected_and_transformed: self.connect_and_transform() nodes = { node.label_full: { @@ -532,7 +532,7 @@ def _connect_network(self): def __repr__(self) -> str: """Compact representation for debugging.""" - status = '✓' if self._connected_and_transformed else '⚠' + status = '✓' if self.connected_and_transformed else '⚠' return ( f'FlowSystem({len(self.timesteps)} timesteps ' f'[{self.timesteps[0].strftime("%Y-%m-%d")} to {self.timesteps[-1].strftime("%Y-%m-%d")}], ' @@ -562,7 +562,7 @@ def format_elements(element_names: list, label: str, alignment: int = 12): format_elements(list(self.components.keys()), 'Components'), format_elements(list(self.buses.keys()), 'Buses'), format_elements(list(self.effects.effects.keys()), 'Effects'), - f'Status: {"Connected & Transformed" if self._connected_and_transformed else "Not connected"}', + f'Status: {"Connected & Transformed" if self.connected_and_transformed else "Not connected"}', ] return '\n'.join(lines) @@ -641,7 +641,7 @@ def sel(self, time: Optional[Union[str, slice, List[str], pd.Timestamp, pd.Datet Returns: FlowSystem: New FlowSystem with selected data """ - if not self._connected_and_transformed: + if not self.connected_and_transformed: self.connect_and_transform() # Build indexers dict from non-None parameters @@ -669,7 +669,7 @@ def isel(self, time: Optional[Union[int, slice, List[int]]] = None, scenario: Op Returns: FlowSystem: New FlowSystem with selected data """ - if not self._connected_and_transformed: + if not self.connected_and_transformed: self.connect_and_transform() # Build indexers dict from non-None parameters @@ -704,7 +704,7 @@ def resample( Returns: FlowSystem: New FlowSystem with resampled data """ - if not self._connected_and_transformed: + if not self.connected_and_transformed: self.connect_and_transform() dataset = self.to_dataset() @@ -737,3 +737,7 @@ def resample( resampled_dataset = resampled_time_data return self.__class__.from_dataset(resampled_dataset) + + @property + def connected_and_transformed(self) -> bool: + return self._connected_and_transformed From 148a8524f45bb2375c16fcd2e0aaebe748c1dae8 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 16 Jul 2025 10:37:27 +0200 Subject: [PATCH 181/448] Make Calcualtion Methods return themselfes to make them chainable --- flixopt/calculation.py | 20 +++++++++++++------- flixopt/flow_system.py | 2 +- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/flixopt/calculation.py b/flixopt/calculation.py index e7ea8d053..764961b78 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -176,7 +176,7 @@ class FullCalculation(Calculation): class for defined way of solving a flow_system optimization """ - def do_modeling(self) -> SystemModel: + def do_modeling(self) -> 'FullCalculation': t_start = timeit.default_timer() self.flow_system.connect_and_transform() @@ -184,9 +184,9 @@ def do_modeling(self) -> SystemModel: self.model.do_modeling() self.durations['modeling'] = round(timeit.default_timer() - t_start, 2) - return self.model + return self - def fix_sizes(self, ds: xr.Dataset, decimal_rounding: Optional[int] = 5): + def fix_sizes(self, ds: xr.Dataset, decimal_rounding: Optional[int] = 5) -> 'FullCalculation': """Fix the sizes of the calculations to specified values. Args: @@ -211,7 +211,9 @@ def fix_sizes(self, ds: xr.Dataset, decimal_rounding: Optional[int] = 5): ) logger.debug(f'Fixed "{name}":\n{con}') - def solve(self, solver: _Solver, log_file: Optional[pathlib.Path] = None, log_main_results: bool = True): + return self + + def solve(self, solver: _Solver, log_file: Optional[pathlib.Path] = None, log_main_results: bool = True) -> 'FullCalculation': t_start = timeit.default_timer() self.model.solve( @@ -248,6 +250,8 @@ def solve(self, solver: _Solver, log_file: Optional[pathlib.Path] = None, log_ma self.results = CalculationResults.from_calculation(self) + return self + class AggregatedCalculation(FullCalculation): """ @@ -288,7 +292,7 @@ def __init__( self.components_to_clusterize = components_to_clusterize self.aggregation = None - def do_modeling(self) -> SystemModel: + def do_modeling(self) -> 'AggregatedCalculation': t_start = timeit.default_timer() self.flow_system.connect_and_transform() self._perform_aggregation() @@ -302,7 +306,7 @@ def do_modeling(self) -> SystemModel: ) self.aggregation.do_modeling() self.durations['modeling'] = round(timeit.default_timer() - t_start, 2) - return self.model + return self def _perform_aggregation(self): from .aggregation import Aggregation @@ -463,7 +467,7 @@ def _create_sub_calculations(self): def do_modeling_and_solve( self, solver: _Solver, log_file: Optional[pathlib.Path] = None, log_main_results: bool = False - ): + ) -> 'SegmentedCalculation': logger.info(f'{"":#^80}') logger.info(f'{" Segmented Solving ":#^80}') self._create_sub_calculations() @@ -505,6 +509,8 @@ def do_modeling_and_solve( self.results = SegmentedCalculationResults.from_calculation(self) + return self + def _transfer_start_values(self, i: int): """ This function gets the last values of the previous solved segment and diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index b0dc746bb..3d43313b3 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -737,7 +737,7 @@ def resample( resampled_dataset = resampled_time_data return self.__class__.from_dataset(resampled_dataset) - + @property def connected_and_transformed(self) -> bool: return self._connected_and_transformed From 61755f9bd68a15f691ba2c2ea3de721adb282d86 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 16 Jul 2025 13:50:24 +0200 Subject: [PATCH 182/448] Improve example --- examples/05_Two-stage-optimization/two_stage_optimization.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/05_Two-stage-optimization/two_stage_optimization.py b/examples/05_Two-stage-optimization/two_stage_optimization.py index 7f0a412bf..3548726b4 100644 --- a/examples/05_Two-stage-optimization/two_stage_optimization.py +++ b/examples/05_Two-stage-optimization/two_stage_optimization.py @@ -4,7 +4,7 @@ as it can drastically reduce the computational time. This leads to faster results and easier debugging. A common use case is to do optimize the investments of a model with a downsampled version of the original model, and than fix the computed sizes when calculating th actual dispatch. -While the final optimum might differ from the fglobal optimum, the solving will be much faster. +While the final optimum might differ from the global optimum, the solving will be much faster. """ import logging @@ -107,7 +107,6 @@ calculation_sizing.do_modeling() calculation_sizing.solve(fx.solvers.HighsSolver(0.1/100, 600)) timer_sizing = timeit.default_timer() - start - flow_sizes = xr.Dataset({flow.size.name: flow.size for flow in calculation_sizing.results.flows.values()}) calculation_dispatch = fx.FullCalculation('Sizing', flow_system) calculation_dispatch.do_modeling() @@ -133,7 +132,7 @@ ).assign_coords(mode=['Combined', 'Two-stage']) comparison['Duration [s]'] = xr.DataArray([timer_combined, timer_sizing + timer_dispatch], dims='mode') - comparison_main = comparison[['Duration [s]', 'costs|total', 'costs(invest)|total', 'costs(operation)|total', 'BHKW2(Q_fu)|size', 'Kessel(Q_fu)|size']] + comparison_main = comparison[['Duration [s]', 'costs|total', 'costs(invest)|total', 'costs(operation)|total', 'BHKW2(Q_fu)|size', 'Kessel(Q_fu)|size', 'Speicher|size']] comparison_main = xr.concat([ comparison_main, ((comparison_main.sel(mode='Two-stage') - comparison_main.sel(mode='Combined')) From 66f6a8675b421131fcb8f8bedf2a26851783639a Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 16 Jul 2025 15:54:36 +0200 Subject: [PATCH 183/448] Improve Unreleased CHANGELOG.md --- CHANGELOG.md | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 860c2e842..f1ee8a916 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [Unreleased - New Model dimensions] ## What's New @@ -35,37 +35,38 @@ This might occur when scenarios represent years or months, while an investment d * Feature 2 - Description -## [Unreleased] +## [Unreleased - Data Management and IO] ### Changed * **BREAKING**: `relative_minimum_charge_state` and `relative_maximum_charge_state` don't have an extra timestep anymore. The final charge state can now be constrained by parameters `relative_minimum_final_charge_state` and `relative_maximum_final_charge_state` instead +* **BREAKING**: Calculation.do_modeling() now returns the Calculation object instead of its linopy.Model * FlowSystems can not be shared across multiple Calculations anymore. A copy of the FlowSystem is created instead, making every Calculation independent * Type system overhaul - added clear separation between temporal and non-temporal data throughout codebase for better clarity * FlowSystem data management simplified - removed `time_series_collection` pattern in favor of direct timestep properties * Enhanced FlowSystem interface with improved `__repr__()` and `__str__()` methods ### Added -* **NEW**: Complete serialization infrastructure through `Interface` base class +* Complete serialization infrastructure through `Interface` base class * IO for all Interfaces and the FlowSystem with round-trip serialization support * Automatic DataArray extraction and restoration * NetCDF export/import capabilities for all Interface objects and FlowSystem * JSON export for documentation purposes * Recursive handling of nested Interface objects -* **NEW**: FlowSystem data manipulation methods +* FlowSystem data manipulation methods * `sel()` and `isel()` methods for temporal data selection * `resample()` method for temporal resampling * `copy()` method to create a copy of a FlowSystem, including all underlying Elements and their data * `__eq__()` method for FlowSystem comparison -* **NEW**: Storage component enhancements +* Storage component enhancements * `relative_minimum_final_charge_state` parameter for final state control * `relative_maximum_final_charge_state` parameter for final state control -* *Internal*: Enhanced data handling methods - * `fit_to_model_coords()` method for data alignment - * `fit_effects_to_model_coords()` method for effect data processing - * `connect_and_transform()` method replacing separate operations -* **NEW**: Core data handling improvements +* Core data handling improvements * `get_dataarray_stats()` function for statistical summaries * Enhanced `DataConverter` class with better TimeSeriesData support +* Internal: Enhanced data handling methods + * `fit_to_model_coords()` method for data alignment + * `fit_effects_to_model_coords()` method for effect data processing + * `connect_and_transform()` method replacing several operations ### Fixed * Enhanced NetCDF I/O with proper attribute preservation for DataArrays @@ -74,7 +75,7 @@ This might occur when scenarios represent years or months, while an investment d ### Know Issues * Plotly >= 6 may raise errors if "nbformat" is not installed. We pinned plotly to <6, but this may be fixed in the future. -* IO for single Interfaces/Elemenets to Datasets might not work properly if the Interface/Element is not part of a fully transformed and connected FlowSystem. This arrises from Numeric Data not being stored as xr.DataArray by the user. TO avoid this, always use the `to_dataset()` on Elements inside a FlowSystem thats connected and transformed. +* IO for single Interfaces/Elemenets to Datasets might not work properly if the Interface/Element is not part of a fully transformed and connected FlowSystem. This arrises from Numeric Data not being stored as xr.DataArray by the user. To avoid this, always use the `to_dataset()` on Elements inside a FlowSystem thats connected and transformed. ### Deprecated * The `agg_group` and `agg_weight` parameters of `TimeSeriesData` are deprecated and will be removed in a future version. Use `aggregation_group` and `aggregation_weight` instead. From a757e7fb42f7e63e2f4f72abc0a688ca1f70c8be Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 8 Jul 2025 09:45:16 +0200 Subject: [PATCH 184/448] Add year coord to FlowSystem --- flixopt/flow_system.py | 74 +++++++++++++++++++++++++++++++++--------- 1 file changed, 59 insertions(+), 15 deletions(-) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 3d43313b3..ffb408369 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -62,29 +62,34 @@ class FlowSystem(Interface): def __init__( self, timesteps: pd.DatetimeIndex, + years: Optional[pd.Index] = None, scenarios: Optional[pd.Index] = None, hours_of_last_timestep: Optional[float] = None, hours_of_previous_timesteps: Optional[Union[int, float, np.ndarray]] = None, - scenario_weights: Optional[NonTemporalDataUser] = None, + weights: Optional[NonTemporalDataUser] = None, ): """ Args: timesteps: The timesteps of the model. + years: The years of the model. scenarios: The scenarios of the model. hours_of_last_timestep: The duration of the last time step. Uses the last time interval if not specified hours_of_previous_timesteps: The duration of previous timesteps. If None, the first time increment of time_series is used. This is needed to calculate previous durations (for example consecutive_on_hours). If you use an array, take care that its long enough to cover all previous values! - scenario_weights: The weights of each scenarios. If None, all scenarios have the same weight (normalized to 1). Its recommended to scale the weights to sum up to 1. + weights: The weights of each year and scenario. If None, all have the same weight (normalized to 1). Its recommended to scale the weights to sum up to 1. """ self.timesteps = self._validate_timesteps(timesteps) self.timesteps_extra = self._create_timesteps_with_extra(timesteps, hours_of_last_timestep) self.hours_of_previous_timesteps = self._calculate_hours_of_previous_timesteps(timesteps, hours_of_previous_timesteps) + self.years = None if years is None else self._validate_years(years) + self.scenarios = None if scenarios is None else self._validate_scenarios(scenarios) - self.scenario_weights = scenario_weights + + self.weights = weights hours_per_timestep = self.calculate_hours_per_timestep(self.timesteps_extra) @@ -130,6 +135,29 @@ def _validate_scenarios(scenarios: pd.Index) -> pd.Index: return scenarios + @staticmethod + def _validate_years(years: pd.Index) -> pd.Index: + """ + Validate and prepare year index. + + Args: + years: The year index to validate + """ + if not isinstance(years, pd.Index) or len(years) == 0: + raise ConversionError('Years must be a non-empty Index') + + if not ( + years.dtype.kind == 'i' # integer dtype + and years.is_monotonic_increasing # rising + and years.is_unique + ): + raise ConversionError('Years must be a monotonically increasing and unique Index') + + if years.name != 'year': + years = years.rename('year') + + return years + @staticmethod def _create_timesteps_with_extra( timesteps: pd.DatetimeIndex, hours_of_last_timestep: Optional[float] @@ -235,10 +263,11 @@ def from_dataset(cls, ds: xr.Dataset) -> 'FlowSystem': # Create FlowSystem instance with constructor parameters flow_system = cls( timesteps=ds.indexes['time'], + years=ds.indexes.get('year'), scenarios=ds.indexes.get('scenario'), - scenario_weights=cls._resolve_dataarray_reference( - reference_structure['scenario_weights'], arrays_dict - ) if 'scenario_weights' in reference_structure else None, + weights=cls._resolve_dataarray_reference(reference_structure['weights'], arrays_dict) + if 'weights' in reference_structure + else None, hours_of_last_timestep=reference_structure.get('hours_of_last_timestep'), hours_of_previous_timesteps=reference_structure.get('hours_of_previous_timesteps'), ) @@ -380,12 +409,12 @@ def fit_effects_to_model_coords( def connect_and_transform(self): """Transform data for all elements using the new simplified approach.""" - self.scenario_weights = self.fit_to_model_coords( - 'scenario_weights', self.scenario_weights, has_time_dim=False + self.weights = self.fit_to_model_coords( + 'weights', self.weights, has_time_dim=False ) - if self.scenario_weights is not None and self.scenario_weights.sum() != 1: + if self.weights is not None and self.weights.sum() != 1: logger.warning(f'Scenario weights are not normalized to 1. This is reccomended for a better scaled model. ' - f'Sum of weights={self.scenario_weights.sum().item()}') + f'Sum of weights={self.weights.sum().item()}') if not self.connected_and_transformed: self._connect_network() @@ -621,6 +650,8 @@ def all_elements(self) -> Dict[str, Element]: @property def coords(self) -> Dict[str, pd.Index]: active_coords = {'time': self.timesteps} + if self.years is not None: + active_coords['year'] = self.years if self.scenarios is not None: active_coords['scenario'] = self.scenarios return active_coords @@ -629,13 +660,18 @@ def coords(self) -> Dict[str, pd.Index]: def used_in_calculation(self) -> bool: return self._used_in_calculation - def sel(self, time: Optional[Union[str, slice, List[str], pd.Timestamp, pd.DatetimeIndex]] = None, - scenario: Optional[Union[str, slice, List[str], pd.Index]] = None) -> 'FlowSystem': + def sel( + self, + time: Optional[Union[str, slice, List[str], pd.Timestamp, pd.DatetimeIndex]] = None, + year: Optional[Union[int, slice, List[int], pd.Index]] = None, + scenario: Optional[Union[str, slice, List[str], pd.Index]] = None, + ) -> 'FlowSystem': """ Select a subset of the flowsystem by the time coordinate. Args: time: Time selection (e.g., slice('2023-01-01', '2023-12-31'), '2023-06-15', or list of times) + year: Year selection (e.g., slice(2023, 2024), or list of years) scenario: Scenario selection (e.g., slice('scenario1', 'scenario2'), or list of scenarios) Returns: @@ -648,7 +684,8 @@ def sel(self, time: Optional[Union[str, slice, List[str], pd.Timestamp, pd.Datet indexers = {} if time is not None: indexers['time'] = time - + if year is not None: + indexers['year'] = year if scenario is not None: indexers['scenario'] = scenario @@ -658,12 +695,18 @@ def sel(self, time: Optional[Union[str, slice, List[str], pd.Timestamp, pd.Datet selected_dataset = self.to_dataset().sel(**indexers) return self.__class__.from_dataset(selected_dataset) - def isel(self, time: Optional[Union[int, slice, List[int]]] = None, scenario: Optional[Union[int, slice, List[int]]] = None) -> 'FlowSystem': + def isel( + self, + time: Optional[Union[int, slice, List[int]]] = None, + year: Optional[Union[int, slice, List[int]]] = None, + scenario: Optional[Union[int, slice, List[int]]] = None + ) -> 'FlowSystem': """ Select a subset of the flowsystem by integer indices. Args: time: Time selection by integer index (e.g., slice(0, 100), 50, or [0, 5, 10]) + year: Year selection by integer index (e.g., slice(0, 100), 50, or [0, 5, 10]) scenario: Scenario selection by integer index (e.g., slice(0, 3), 50, or [0, 5, 10]) Returns: @@ -676,7 +719,8 @@ def isel(self, time: Optional[Union[int, slice, List[int]]] = None, scenario: Op indexers = {} if time is not None: indexers['time'] = time - + if year is not None: + indexers['year'] = year if scenario is not None: indexers['scenario'] = scenario From 941d93f26fb3700b06619d40dbe5285dbca2ad17 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 8 Jul 2025 09:46:09 +0200 Subject: [PATCH 185/448] Improve dimension handling --- examples/04_Scenarios/scenario_example.py | 5 ++- flixopt/core.py | 2 +- flixopt/effects.py | 26 +++++------ flixopt/elements.py | 2 +- flixopt/features.py | 51 +++++++++++---------- flixopt/flow_system.py | 14 +++--- flixopt/structure.py | 28 ++++++------ tests/conftest.py | 2 +- tests/test_functional.py | 54 ----------------------- tests/test_scenarios.py | 33 +++++++------- 10 files changed, 80 insertions(+), 137 deletions(-) diff --git a/examples/04_Scenarios/scenario_example.py b/examples/04_Scenarios/scenario_example.py index ae53cc1ff..d1ab0cedd 100644 --- a/examples/04_Scenarios/scenario_example.py +++ b/examples/04_Scenarios/scenario_example.py @@ -12,14 +12,15 @@ # Create datetime array starting from '2020-01-01' for the given time period timesteps = pd.date_range('2020-01-01', periods=9, freq='h') scenarios = pd.Index(['Base Case', 'High Demand']) + years = pd.Index([2020, 2021, 2022]) # --- Create Time Series Data --- # Heat demand profile (e.g., kW) over time and corresponding power prices heat_demand_per_h = pd.DataFrame({'Base Case':[30, 0, 90, 110, 110, 20, 20, 20, 20], 'High Demand':[30, 0, 100, 118, 125, 20, 20, 20, 20]}, index=timesteps) - power_prices = np.array([0.08, 0.09]) + power_prices = np.array([0.08, 0.09, 0.10]) - flow_system = fx.FlowSystem(timesteps=timesteps, scenarios=scenarios, scenario_weights=np.array([0.5, 0.6])) + flow_system = fx.FlowSystem(timesteps=timesteps, years=years, scenarios=scenarios, weights=np.array([0.5, 0.6])) # --- Define Energy Buses --- # These represent nodes, where the used medias are balanced (electricity, heat, and gas) diff --git a/flixopt/core.py b/flixopt/core.py index ee0ef0540..fb0e4bae0 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -30,7 +30,7 @@ NonTemporalData = Union[Scalar, xr.DataArray] """Internally used datatypes for non-temporal data. Can be a Scalar or an xr.DataArray.""" -FlowSystemDimensions = Literal['time', 'scenario'] +FlowSystemDimensions = Literal['time', 'year', 'scenario'] """Possible dimensions of a FlowSystem.""" diff --git a/flixopt/effects.py b/flixopt/effects.py index 381b5a3de..639ae559b 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -145,8 +145,7 @@ def __init__(self, model: SystemModel, element: Effect): self.invest: ShareAllocationModel = self.add( ShareAllocationModel( model=self._model, - has_time_dim=False, - has_scenario_dim=True, + dims=['year', 'scenario'], label_of_element=self.label_of_element, label='invest', label_full=f'{self.label_full}(invest)', @@ -158,8 +157,7 @@ def __init__(self, model: SystemModel, element: Effect): self.operation: ShareAllocationModel = self.add( ShareAllocationModel( model=self._model, - has_time_dim=True, - has_scenario_dim=True, + dims=['time', 'year', 'scenario'], label_of_element=self.label_of_element, label='operation', label_full=f'{self.label_full}(operation)', @@ -182,7 +180,7 @@ def do_modeling(self): self._model.add_variables( lower=self.element.minimum_total if self.element.minimum_total is not None else -np.inf, upper=self.element.maximum_total if self.element.maximum_total is not None else np.inf, - coords=self._model.get_coords(time_dim=False), + coords=self._model.get_coords(['year', 'scenario']), name=f'{self.label_full}|total', ), 'total', @@ -406,15 +404,13 @@ def add_share_to_effects( self.effects[effect].model.operation.add_share( name, expression, - has_time_dim=True, - has_scenario_dim=True, + dims=['time', 'year', 'scenario'], ) elif target == 'invest': self.effects[effect].model.invest.add_share( name, expression, - has_time_dim=False, - has_scenario_dim=True, + dims=['year', 'scenario'], ) else: raise ValueError(f'Target {target} not supported!') @@ -422,13 +418,13 @@ def add_share_to_effects( def add_share_to_penalty(self, name: str, expression: linopy.LinearExpression) -> None: if expression.ndim != 0: raise TypeError(f'Penalty shares must be scalar expressions! ({expression.ndim=})') - self.penalty.add_share(name, expression, has_time_dim=False, has_scenario_dim=False) + self.penalty.add_share(name, expression, dims=[]) def do_modeling(self): for effect in self.effects: effect.create_model(self._model) self.penalty = self.add( - ShareAllocationModel(self._model, has_time_dim=False, has_scenario_dim=False, label_of_element='Penalty') + ShareAllocationModel(self._model, dims=[], label_of_element='Penalty') ) for model in [effect.model for effect in self.effects] + [self.penalty]: model.do_modeling() @@ -436,7 +432,7 @@ def do_modeling(self): self._add_share_between_effects() self._model.add_objective( - (self.effects.objective_effect.model.total * self._model.scenario_weights).sum() + (self.effects.objective_effect.model.total * self._model.weights).sum() + self.penalty.total.sum() ) @@ -447,16 +443,14 @@ def _add_share_between_effects(self): self.effects[target_effect].model.operation.add_share( origin_effect.model.operation.label_full, origin_effect.model.operation.total_per_timestep * time_series, - has_time_dim=True, - has_scenario_dim=True, + dims=['time', 'year', 'scenario'], ) # 2. invest: -> hier ist es Scalar (share) for target_effect, factor in origin_effect.specific_share_to_other_effects_invest.items(): self.effects[target_effect].model.invest.add_share( origin_effect.model.invest.label_full, origin_effect.model.invest.total * factor, - has_time_dim=False, - has_scenario_dim=True, + dims=['year', 'scenario'], ) diff --git a/flixopt/elements.py b/flixopt/elements.py index 1daadeb55..968b70ca5 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -364,7 +364,7 @@ def do_modeling(self): self._model.add_variables( lower=self.element.flow_hours_total_min if self.element.flow_hours_total_min is not None else 0, upper=self.element.flow_hours_total_max if self.element.flow_hours_total_max is not None else np.inf, - coords=self._model.get_coords(time_dim=False), + coords=self._model.get_coords(['year', 'scenario']), name=f'{self.label_full}|total_flow_hours', ), 'total_flow_hours', diff --git a/flixopt/features.py b/flixopt/features.py index 4e47ace7f..c002a8ff6 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -10,7 +10,7 @@ import numpy as np from .config import CONFIG -from .core import NonTemporalData, Scalar, TemporalData +from .core import NonTemporalData, Scalar, TemporalData, FlowSystemDimensions from .interface import InvestParameters, OnOffParameters, Piecewise from .structure import Model, SystemModel @@ -48,7 +48,7 @@ def do_modeling(self): lower=0 if self.parameters.optional else self.parameters.minimum_or_fixed_size, upper=self.parameters.maximum_or_fixed_size, name=f'{self.label_full}|size', - coords=self._model.get_coords(time_dim=False), + coords=self._model.get_coords(['year', 'scenario']), ), 'size', ) @@ -59,7 +59,7 @@ def do_modeling(self): self._model.add_variables( binary=True, name=f'{self.label_full}|is_invested', - coords=self._model.get_coords(time_dim=False), + coords=self._model.get_coords(['year', 'scenario']), ), 'is_invested', ) @@ -294,7 +294,7 @@ def do_modeling(self): self._model.add_variables( lower=self._on_hours_total_min, upper=self._on_hours_total_max, - coords=self._model.get_coords(time_dim=False), + coords=self._model.get_coords(['year', 'scenario']), name=f'{self.label_full}|on_hours_total', ), 'on_hours_total', @@ -952,8 +952,7 @@ class ShareAllocationModel(Model): def __init__( self, model: SystemModel, - has_time_dim: bool, - has_scenario_dim: bool, + dims: List[FlowSystemDimensions], label_of_element: Optional[str] = None, label: Optional[str] = None, label_full: Optional[str] = None, @@ -963,10 +962,11 @@ def __init__( min_per_hour: Optional[TemporalData] = None, ): super().__init__(model, label_of_element=label_of_element, label=label, label_full=label_full) - if not has_time_dim: # If the condition is True - assert max_per_hour is None and min_per_hour is None, ( - 'Both max_per_hour and min_per_hour cannot be used when has_time_dim is False' - ) + + if 'time' not in dims and max_per_hour is not None or min_per_hour is not None: + raise ValueError('Both max_per_hour and min_per_hour cannot be used when has_time_dim is False') + + self._dims = dims self.total_per_timestep: Optional[linopy.Variable] = None self.total: Optional[linopy.Variable] = None self.shares: Dict[str, linopy.Variable] = {} @@ -976,8 +976,6 @@ def __init__( self._eq_total: Optional[linopy.Constraint] = None # Parameters - self._has_time_dim = has_time_dim - self._has_scenario_dim = has_scenario_dim self._total_max = total_max if total_max is not None else np.inf self._total_min = total_min if total_min is not None else -np.inf self._max_per_hour = max_per_hour if max_per_hour is not None else np.inf @@ -988,7 +986,7 @@ def do_modeling(self): self._model.add_variables( lower=self._total_min, upper=self._total_max, - coords=self._model.get_coords(time_dim=False, scenario_dim=self._has_scenario_dim), + coords=self._model.get_coords([dim for dim in self._dims if dim != 'time']), name=f'{self.label_full}|total', ), 'total', @@ -998,12 +996,12 @@ def do_modeling(self): self._model.add_constraints(self.total == 0, name=f'{self.label_full}|total'), 'total' ) - if self._has_time_dim: + if 'time' in self._dims: self.total_per_timestep = self.add( self._model.add_variables( lower=-np.inf if (self._min_per_hour is None) else self._min_per_hour * self._model.hours_per_step, upper=np.inf if (self._max_per_hour is None) else self._max_per_hour * self._model.hours_per_step, - coords=self._model.get_coords(time_dim=True, scenario_dim=self._has_scenario_dim), + coords=self._model.get_coords(self._dims), name=f'{self.label_full}|total_per_timestep', ), 'total_per_timestep', @@ -1021,8 +1019,7 @@ def add_share( self, name: str, expression: linopy.LinearExpression, - has_time_dim: bool, - has_scenario_dim: bool, + dims: Optional[List[FlowSystemDimensions]] = None, ): """ Add a share to the share allocation model. If the share already exists, the expression is added to the existing share. @@ -1033,18 +1030,24 @@ def add_share( Args: name: The name of the share. expression: The expression of the share. Added to the right hand side of the constraint. + dims: The dimensions of the share. Defaults to all dimensions. Dims are ordered automatically """ - if has_time_dim and not self._has_time_dim: - raise ValueError('Cannot add share with time_dim=True to a model without time_dim') - if has_scenario_dim and not self._has_scenario_dim: - raise ValueError('Cannot add share with scenario_dim=True to a model without scenario_dim') + if dims is None: + dims = self._dims + else: + if 'time' in dims and 'time' not in self._dims: + raise ValueError('Cannot add share with time-dim to a model without time-dim') + if 'year' in dims and 'year' not in self._dims: + raise ValueError('Cannot add share with year-dim to a model without year-dim') + if 'scenario' in dims and 'scenario' not in self._dims: + raise ValueError('Cannot add share with scenario-dim to a model without scenario-dim') if name in self.shares: self.share_constraints[name].lhs -= expression else: self.shares[name] = self.add( self._model.add_variables( - coords=self._model.get_coords(time_dim=has_time_dim, scenario_dim=has_scenario_dim), + coords=self._model.get_coords(dims), name=f'{name}->{self.label_full}', ), name, @@ -1052,7 +1055,7 @@ def add_share( self.share_constraints[name] = self.add( self._model.add_constraints(self.shares[name] == expression, name=f'{name}->{self.label_full}'), name ) - if not has_time_dim: + if 'time' not in dims: self._eq_total.lhs -= self.shares[name] else: self._eq_total_per_timestep.lhs -= self.shares[name] @@ -1083,7 +1086,7 @@ def do_modeling(self): self.shares = { effect: self.add( self._model.add_variables( - coords=self._model.get_coords(time_dim=False), name=f'{self.label_full}|{effect}' + coords=self._model.get_coords(['year', 'scenario']), name=f'{self.label_full}|{effect}' ), f'{effect}', ) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index ffb408369..0a7550fc6 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -376,12 +376,12 @@ def fit_to_model_coords( except ConversionError as e: raise ConversionError( f'Could not convert time series data "{name}" to DataArray:\n{data}\nOriginal Error: {e}') from e - else: - try: - return DataConverter.to_dataarray(data, coords=coords).rename(name) - except ConversionError as e: - raise ConversionError( - f'Could not convert data "{name}" to DataArray:\n{data}\nOriginal Error: {e}') from e + + try: + return DataConverter.to_dataarray(data, coords=coords).rename(name) + except ConversionError as e: + raise ConversionError( + f'Could not convert data "{name}" to DataArray:\n{data}\nOriginal Error: {e}') from e def fit_effects_to_model_coords( self, @@ -413,7 +413,7 @@ def connect_and_transform(self): 'weights', self.weights, has_time_dim=False ) if self.weights is not None and self.weights.sum() != 1: - logger.warning(f'Scenario weights are not normalized to 1. This is reccomended for a better scaled model. ' + logger.warning(f'Scenario weights are not normalized to 1. This is recomended for a better scaled model. ' f'Sum of weights={self.weights.sum().item()}') if not self.connected_and_transformed: diff --git a/flixopt/structure.py b/flixopt/structure.py index 5edee1bd3..3aad34489 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -19,7 +19,7 @@ from . import io as fx_io from .config import CONFIG -from .core import NonTemporalData, Scalar, TemporalDataUser, TimeSeriesData, get_dataarray_stats +from .core import NonTemporalData, Scalar, TemporalDataUser, TimeSeriesData, get_dataarray_stats, FlowSystemDimensions if TYPE_CHECKING: # for type checking and preventing circular imports from .effects import EffectCollectionModel @@ -103,28 +103,28 @@ def hours_of_previous_timesteps(self): return self.flow_system.hours_of_previous_timesteps def get_coords( - self, scenario_dim=True, time_dim=True, extra_timestep=False + self, + dims: Optional[FlowSystemDimensions] = None, + extra_timestep=False, ) -> Optional[Union[Tuple[pd.Index], Tuple[pd.Index, pd.Index]]]: """ Returns the coordinates of the model Args: - scenario_dim: If True, the scenario dimension is included in the coordinates - time_dim: If True, the time dimension is included in the coordinates + dims: The dimensions to include in the coordinates. Defaults to all dimensions. Coords are ordered automatically extra_timestep: If True, the extra timesteps are used instead of the regular timesteps Returns: The coordinates of the model. Might also be None if no scenarios are present and time_dim is False """ - if extra_timestep and not time_dim: - raise ValueError('extra_timestep=True requires time_dim=True') + if dims is not None and extra_timestep and 'time' not in dims: + raise ValueError('extra_timestep=True requires time to be included in dims') - coords = self.flow_system.coords + if dims is None: + coords = self.flow_system.coords + else: + coords = {k: v for k, v in self.flow_system.coords.items() if k in dims} - if not scenario_dim: - coords.pop('scenario', None) - if not time_dim: - coords.pop('time', None) if extra_timestep: coords['time'] = self.flow_system.timesteps_extra @@ -137,12 +137,12 @@ def get_coords( return tuple(coords.values()) @property - def scenario_weights(self) -> Union[int, xr.DataArray]: + def weights(self) -> Union[int, xr.DataArray]: """Returns the scenario weights of the FlowSystem.""" - if self.flow_system.scenario_weights is None: + if self.flow_system.weights is None: return 1 - return self.flow_system.scenario_weights + return self.flow_system.weights class Interface: diff --git a/tests/conftest.py b/tests/conftest.py index 9f247164f..7c1b08a5b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -191,7 +191,7 @@ def simple_flow_system_scenarios() -> fx.FlowSystem: ) # Create flow system - flow_system = fx.FlowSystem(base_timesteps, scenarios=pd.Index(['A', 'B', 'C']), scenario_weights=np.array([0.5, 0.25, 0.25])) + flow_system = fx.FlowSystem(base_timesteps, scenarios=pd.Index(['A', 'B', 'C']), weights=np.array([0.5, 0.25, 0.25])) flow_system.add_elements(fx.Bus('Strom'), fx.Bus('Fernwärme'), fx.Bus('Gas')) flow_system.add_elements(storage, costs, co2, boiler, heat_load, gas_tariff, electricity_feed_in, chp) diff --git a/tests/test_functional.py b/tests/test_functional.py index 5db83f656..9542d656b 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -325,60 +325,6 @@ def test_optional_invest(solver_fixture, time_steps_fixture): err_msg='"Boiler__Q_th__IsInvested" does not have the right value', ) - def test_fixed_relative_profile(self): - self.flow_system = self.create_model(self.datetime_array) - self.flow_system.add_elements( - fx.linear_converters.Boiler( - 'Boiler', - 0.5, - Q_fu=fx.Flow('Q_fu', bus=self.get_element('Gas')), - Q_th=fx.Flow( - 'Q_th', - bus=self.get_element('Fernwärme'), - size=fx.InvestParameters(optional=True, minimum_size=40, fix_effects=10, specific_effects=1), - ), - ), - fx.linear_converters.Boiler( - 'Boiler_optional', - 0.5, - Q_fu=fx.Flow('Q_fu', bus=self.get_element('Gas')), - Q_th=fx.Flow( - 'Q_th', - bus=self.get_element('Fernwärme'), - size=fx.InvestParameters(optional=True, minimum_size=50, fix_effects=10, specific_effects=1), - ), - ), - ) - self.flow_system.add_elements( - fx.Source( - 'Wärmequelle', - source=fx.Flow( - 'Q_th', - bus=self.get_element('Fernwärme'), - fixed_relative_profile=np.linspace(0, 5, len(self.datetime_array)), - size=fx.InvestParameters(optional=False, minimum_size=2, maximum_size=5), - ), - ) - ) - self.get_element('Fernwärme').excess_penalty_per_flow_hour = 1e5 - - self.solve_and_load(self.flow_system) - source = self.get_element('Wärmequelle') - assert_allclose( - source.source.model.flow_rate.result, - np.linspace(0, 5, len(self.datetime_array)) * source.source.model._investment.size.result, - rtol=self.mip_gap, - atol=1e-10, - err_msg='The total costs does not have the right value', - ) - assert_allclose( - source.source.model._investment.size.result, - 2, - rtol=self.mip_gap, - atol=1e-10, - err_msg='The total costs does not have the right value', - ) - def test_on(solver_fixture, time_steps_fixture): """Tests if the On Variable is correctly created and calculated in a Flow""" diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py index cd3de4407..717d5919b 100644 --- a/tests/test_scenarios.py +++ b/tests/test_scenarios.py @@ -24,15 +24,14 @@ def test_system(): # Create two scenarios scenarios = pd.Index(["Scenario A", "Scenario B"], name="scenario") - # Create scenario weights as TimeSeriesData - # Using TimeSeriesData to avoid conversion issues - scenario_weights = TimeSeriesData(np.array([0.7, 0.3])) + # Create scenario weights + weights = np.array([0.7, 0.3]) # Create a flow system with scenarios flow_system = FlowSystem( timesteps=timesteps, scenarios=scenarios, - scenario_weights=scenario_weights # Use TimeSeriesData for weights + weights=weights # Use TimeSeriesData for weights ) # Create demand profiles that differ between scenarios @@ -254,27 +253,27 @@ def flow_system_piecewise_conversion_scenarios(flow_system_complex_scenarios) -> return flow_system -def test_scenario_weights(flow_system_piecewise_conversion_scenarios): +def test_weights(flow_system_piecewise_conversion_scenarios): """Test that scenario weights are correctly used in the model.""" scenarios = flow_system_piecewise_conversion_scenarios.scenarios weights = np.linspace(0.5, 1, len(scenarios)) - flow_system_piecewise_conversion_scenarios.scenario_weights = weights + flow_system_piecewise_conversion_scenarios.weights = weights model = create_linopy_model(flow_system_piecewise_conversion_scenarios) - np.testing.assert_allclose(model.scenario_weights.values, weights) + np.testing.assert_allclose(model.weights.values, weights) assert_linequal(model.objective.expression, (model.variables['costs|total'] * weights).sum() + model.variables['Penalty|total']) - assert np.isclose(model.scenario_weights.sum().item(), 2.25) + assert np.isclose(model.weights.sum().item(), 2.25) -def test_scenario_weights_io(flow_system_piecewise_conversion_scenarios): +def test_weights_io(flow_system_piecewise_conversion_scenarios): """Test that scenario weights are correctly used in the model.""" scenarios = flow_system_piecewise_conversion_scenarios.scenarios weights = np.linspace(0.5, 1, len(scenarios)) / np.sum(np.linspace(0.5, 1, len(scenarios))) - flow_system_piecewise_conversion_scenarios.scenario_weights = weights + flow_system_piecewise_conversion_scenarios.weights = weights model = create_linopy_model(flow_system_piecewise_conversion_scenarios) - np.testing.assert_allclose(model.scenario_weights.values, weights) + np.testing.assert_allclose(model.weights.values, weights) assert_linequal(model.objective.expression, (model.variables['costs|total'] * weights).sum() + model.variables['Penalty|total']) - assert np.isclose(model.scenario_weights.sum().item(), 1.0) + assert np.isclose(model.weights.sum().item(), 1.0) def test_scenario_dimensions_in_variables(flow_system_piecewise_conversion_scenarios): """Test that all time variables are correctly broadcasted to scenario dimensions.""" @@ -286,7 +285,7 @@ def test_full_scenario_optimization(flow_system_piecewise_conversion_scenarios): """Test a full optimization with scenarios and verify results.""" scenarios = flow_system_piecewise_conversion_scenarios.scenarios weights = np.linspace(0.5, 1, len(scenarios)) / np.sum(np.linspace(0.5, 1, len(scenarios))) - flow_system_piecewise_conversion_scenarios.scenario_weights = weights + flow_system_piecewise_conversion_scenarios.weights = weights calc = create_calculation_and_solve(flow_system_piecewise_conversion_scenarios, solver=fx.solvers.GurobiSolver(mip_gap=0.01, time_limit_seconds=60), name='test_full_scenario') @@ -305,7 +304,7 @@ def test_io_persistance(flow_system_piecewise_conversion_scenarios): """Test a full optimization with scenarios and verify results.""" scenarios = flow_system_piecewise_conversion_scenarios.scenarios weights = np.linspace(0.5, 1, len(scenarios)) / np.sum(np.linspace(0.5, 1, len(scenarios))) - flow_system_piecewise_conversion_scenarios.scenario_weights = weights + flow_system_piecewise_conversion_scenarios.weights = weights calc = create_calculation_and_solve(flow_system_piecewise_conversion_scenarios, solver=fx.solvers.HighsSolver(mip_gap=0.001, time_limit_seconds=60), name='test_full_scenario') @@ -326,12 +325,12 @@ def test_scenarios_selection(flow_system_piecewise_conversion_scenarios): flow_system_full = flow_system_piecewise_conversion_scenarios scenarios = flow_system_full.scenarios weights = np.linspace(0.5, 1, len(scenarios)) / np.sum(np.linspace(0.5, 1, len(scenarios))) - flow_system_full.scenario_weights = weights + flow_system_full.weights = weights flow_system = flow_system_full.sel(scenario=scenarios[0:2]) assert flow_system.scenarios.equals(flow_system_full.scenarios[0:2]) - np.testing.assert_allclose(flow_system.scenario_weights.values, flow_system_full.scenario_weights[0:2]) + np.testing.assert_allclose(flow_system.weights.values, flow_system_full.weights[0:2]) calc = fx.FullCalculation(flow_system=flow_system, name='test_full_scenario') @@ -340,6 +339,6 @@ def test_scenarios_selection(flow_system_piecewise_conversion_scenarios): calc.results.to_file() - np.testing.assert_allclose(calc.results.objective, ((calc.results.solution['costs|total'] * flow_system.scenario_weights).sum() + calc.results.solution['Penalty|total']).item()) ## Acount for rounding errors + np.testing.assert_allclose(calc.results.objective, ((calc.results.solution['costs|total'] * flow_system.weights).sum() + calc.results.solution['Penalty|total']).item()) ## Acount for rounding errors assert calc.results.solution.indexes['scenario'].equals(flow_system_full.scenarios[0:2]) From 745e88589888c7566664a628c24acc1cd009f037 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 8 Jul 2025 14:36:35 +0200 Subject: [PATCH 186/448] Change plotting to use an indexer instead --- flixopt/flow_system.py | 7 ++--- flixopt/results.py | 58 ++++++++++++++++++++++++++++++++++++------ 2 files changed, 52 insertions(+), 13 deletions(-) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 0a7550fc6..bbccbfce5 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -12,10 +12,7 @@ import numpy as np import pandas as pd import xarray as xr -from rich.console import Console -from rich.pretty import Pretty -from . import io as fx_io from .core import ( ConversionError, DataConverter, @@ -648,7 +645,7 @@ def all_elements(self) -> Dict[str, Element]: return {**self.components, **self.effects.effects, **self.flows, **self.buses} @property - def coords(self) -> Dict[str, pd.Index]: + def coords(self) -> Dict[FlowSystemDimensions, pd.Index]: active_coords = {'time': self.timesteps} if self.years is not None: active_coords['year'] = self.years @@ -665,7 +662,7 @@ def sel( time: Optional[Union[str, slice, List[str], pd.Timestamp, pd.DatetimeIndex]] = None, year: Optional[Union[int, slice, List[int], pd.Index]] = None, scenario: Optional[Union[str, slice, List[str], pd.Index]] = None, - ) -> 'FlowSystem': + ) -> 'FlowSystem': """ Select a subset of the flowsystem by the time coordinate. diff --git a/flixopt/results.py b/flixopt/results.py index 97dfc136a..96f099228 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -21,6 +21,7 @@ import pyvis from .calculation import Calculation, SegmentedCalculation + from .core import FlowSystemDimensions logger = logging.getLogger('flixopt') @@ -611,7 +612,7 @@ def plot_heatmap( save: Union[bool, pathlib.Path] = False, show: bool = True, engine: plotting.PlottingEngine = 'plotly', - scenario: Optional[Union[str, int]] = None, + indexer: Optional[Dict['FlowSystemDimensions', Any]] = None, ) -> Union[plotly.graph_objs.Figure, Tuple[plt.Figure, plt.Axes]]: """ Plots a heatmap of the solution of a variable. @@ -624,19 +625,60 @@ def plot_heatmap( save: Whether to save the plot or not. If a path is provided, the plot will be saved at that location. show: Whether to show the plot or not. engine: The engine to use for plotting. Can be either 'plotly' or 'matplotlib'. - scenario: The scenario to plot. Defaults to the first scenario. Has no effect without scenarios present + indexer: Optional selection dict, e.g., {'scenario': 'base', 'year': 2024}. + If None, uses first value for each dimension. + + Examples: + Basic usage (uses first scenario, first year, all time): + + >>> results.plot_heatmap('Battery|charge_state') + + Select specific scenario and year: + + >>> results.plot_heatmap('Boiler(Qth)|flow_rate', indexer={'scenario': 'base', 'year': 2024}) + + Time filtering (summer months only): + + >>> results.plot_heatmap('Boiler(Qth)|flow_rate', indexer={ + ... 'scenario': 'base', + ... 'time': results.solution.time[results.solution.time.dt.month.isin([6, 7, 8])] + ... }) + + Save to specific location: + + >>> results.plot_heatmap('Boiler(Qth)|flow_rate', + ... indexer={'scenario': 'base'}, + ... save='path/to/my_heatmap.html') """ dataarray = self.solution[variable_name] - scenario_suffix = '' - if 'scenario' in dataarray.indexes: - chosen_scenario = scenario or self.scenarios[0] - dataarray = dataarray.sel(scenario=chosen_scenario).drop_vars('scenario') - scenario_suffix = f'--{chosen_scenario}' + # Apply indexer or use first values + if indexer is not None: + # User provided custom indexer - apply it + dataarray = dataarray.sel(indexer) + suffix = '--' + '_'.join(f'{k}_{v}' for k, v in indexer.items()) + else: + # No indexer - use first value for each dimension except 'time' + selection = {} + suffix_parts = [] + + for dim in dataarray.dims: + if dim != 'time' and dim in dataarray.coords: + first_value = dataarray.coords[dim].values[0] + selection[dim] = first_value + suffix_parts.append(f'{dim}_{first_value}') + + if selection: + dataarray = dataarray.sel(selection) + + suffix = '--' + '_'.join(suffix_parts) if suffix_parts else '' + + # Create name + name = f'{variable_name}{suffix}' if suffix else variable_name return plot_heatmap( dataarray=dataarray, - name=f'{variable_name}{scenario_suffix}', + name=name, folder=self.folder, heatmap_timeframes=heatmap_timeframes, heatmap_timesteps_per_frame=heatmap_timesteps_per_frame, From c9bae2ae982d3fa844186eb1d87604e2a9b31d21 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 15 Jul 2025 11:07:52 +0200 Subject: [PATCH 187/448] Change plotting to use an indexer instead --- examples/04_Scenarios/scenario_example.py | 2 + flixopt/results.py | 61 ++++++++++++++--------- 2 files changed, 39 insertions(+), 24 deletions(-) diff --git a/examples/04_Scenarios/scenario_example.py b/examples/04_Scenarios/scenario_example.py index d1ab0cedd..5295d2820 100644 --- a/examples/04_Scenarios/scenario_example.py +++ b/examples/04_Scenarios/scenario_example.py @@ -111,6 +111,8 @@ # --- Solve the Calculation and Save Results --- calculation.solve(fx.solvers.HighsSolver(mip_gap=0, time_limit_seconds=30)) + calculation.results.plot_heatmap('CHP(Q_th)|flow_rate') + # --- Analyze Results --- calculation.results['Fernwärme'].plot_node_balance_pie() calculation.results['Fernwärme'].plot_node_balance(style='stacked_bar') diff --git a/flixopt/results.py b/flixopt/results.py index 96f099228..941d0d6dc 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -652,29 +652,11 @@ def plot_heatmap( """ dataarray = self.solution[variable_name] - # Apply indexer or use first values - if indexer is not None: - # User provided custom indexer - apply it - dataarray = dataarray.sel(indexer) - suffix = '--' + '_'.join(f'{k}_{v}' for k, v in indexer.items()) - else: - # No indexer - use first value for each dimension except 'time' - selection = {} - suffix_parts = [] - - for dim in dataarray.dims: - if dim != 'time' and dim in dataarray.coords: - first_value = dataarray.coords[dim].values[0] - selection[dim] = first_value - suffix_parts.append(f'{dim}_{first_value}') - - if selection: - dataarray = dataarray.sel(selection) - - suffix = '--' + '_'.join(suffix_parts) if suffix_parts else '' + dataarray, suffix_parts = _apply_indexer_to_data(dataarray, indexer) # Create name - name = f'{variable_name}{suffix}' if suffix else variable_name + suffix = '--' + '-'.join(suffix_parts) if suffix_parts else '' + name = variable_name if not suffix_parts else f'{variable_name}--{'-'.join(suffix_parts)}' if suffix else variable_name return plot_heatmap( dataarray=dataarray, @@ -854,7 +836,7 @@ def plot_node_balance( show: bool = True, colors: plotting.ColorType = 'viridis', engine: plotting.PlottingEngine = 'plotly', - scenario: Optional[Union[str, int]] = None, + indexer: Optional[Dict['FlowSystemDimensions', Any]] = None, mode: Literal['flow_rate', 'flow_hours'] = 'flow_rate', style: Literal['area', 'stacked_bar', 'line'] = 'stacked_bar', drop_suffix: bool = True, @@ -917,7 +899,7 @@ def plot_node_balance_pie( save: Union[bool, pathlib.Path] = False, show: bool = True, engine: plotting.PlottingEngine = 'plotly', - scenario: Optional[Union[str, int]] = None, + indexer: Optional[Dict['FlowSystemDimensions', Any]] = None, ) -> plotly.graph_objects.Figure: """ Plots a pie chart of the flow hours of the inputs and outputs of buses or components. @@ -1068,7 +1050,7 @@ def plot_charge_state( colors: plotting.ColorType = 'viridis', engine: plotting.PlottingEngine = 'plotly', style: Literal['area', 'stacked_bar', 'line'] = 'stacked_bar', - scenario: Optional[Union[str, int]] = None, + indexer: Optional[Dict['FlowSystemDimensions', Any]] = None, ) -> plotly.graph_objs.Figure: """ Plots the charge state of a Storage. @@ -1625,3 +1607,34 @@ def apply_filter(array, coord_name: str, coord_values: Union[Any, List[Any]]): raise ValueError(f"No edges match criteria: {filters}") return da + + +def _apply_indexer_to_data(data: xr.DataArray, indexer: Optional[Dict[str, Any]] = None): + """ + Apply indexer selection or auto-select first values for non-time dimensions. + + Args: + data: xarray Dataset or DataArray + indexer: Optional selection dict + + Returns: + Tuple of (selected_data, suffix_parts_list) + """ + suffix_parts = [] + + if indexer is not None: + # User provided indexer + data = data.sel(indexer) + suffix_parts.extend(f"{v}[{k}]" for k, v in indexer.items()) + else: + # Auto-select first value for each dimension except 'time' + selection = {} + for dim in data.dims: + if dim != 'time' and dim in data.coords: + first_value = data.coords[dim].values[0] + selection[dim] = first_value + suffix_parts.append(f"{first_value}[{dim}]") + if selection: + data = data.sel(selection) + + return data, suffix_parts From d1b55094f4c01b24de9eea4b8cc7a2ac01192c70 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 16 Jul 2025 16:05:09 +0200 Subject: [PATCH 188/448] Use tuples to set dimensions in Models --- flixopt/components.py | 4 ++-- flixopt/core.py | 4 ++-- flixopt/effects.py | 16 ++++++++-------- flixopt/features.py | 7 ++++--- flixopt/flow_system.py | 2 +- flixopt/structure.py | 2 +- tests/test_dataconverter.py | 14 +++++++------- tests/test_effects_shares_summation.py | 4 ++-- 8 files changed, 27 insertions(+), 26 deletions(-) diff --git a/flixopt/components.py b/flixopt/components.py index 00d0c073f..f34e2945c 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -635,7 +635,7 @@ def relative_charge_state_bounds(self) -> Tuple[xr.DataArray, xr.DataArray]: ).assign_coords(time=final_timestep) else: min_final = xr.DataArray( - [self.element.relative_minimum_final_charge_state], coords=final_coords, dims=['time'] + [self.element.relative_minimum_final_charge_state], coords=final_coords, dims='time' ) # Get final maximum charge state @@ -645,7 +645,7 @@ def relative_charge_state_bounds(self) -> Tuple[xr.DataArray, xr.DataArray]: ).assign_coords(time=final_timestep) else: max_final = xr.DataArray( - [self.element.relative_maximum_final_charge_state], coords=final_coords, dims=['time'] + [self.element.relative_maximum_final_charge_state], coords=final_coords, dims='time' ) # Concatenate with original bounds diff --git a/flixopt/core.py b/flixopt/core.py index fb0e4bae0..509cd8a91 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -191,7 +191,7 @@ def _match_series_to_dimension( # Try to match Series index to coordinates for dim_name in target_dims: if data.index.equals(coords[dim_name]): - return xr.DataArray(data.values.copy(), coords={dim_name: coords[dim_name]}, dims=[dim_name]) + return xr.DataArray(data.values.copy(), coords={dim_name: coords[dim_name]}, dims=dim_name) # If no index matches, raise error raise ConversionError(f'Series index does not match any target dimension coordinates: {target_dims}') @@ -237,7 +237,7 @@ def _match_array_to_dimension( # Match to the single matching dimension match_dim = matching_dims[0] - return xr.DataArray(data.copy(), coords={match_dim: coords[match_dim]}, dims=[match_dim]) + return xr.DataArray(data.copy(), coords={match_dim: coords[match_dim]}, dims=match_dim) @staticmethod def _match_multidim_array_to_dimensions( diff --git a/flixopt/effects.py b/flixopt/effects.py index 639ae559b..0b5c9f5eb 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -145,7 +145,7 @@ def __init__(self, model: SystemModel, element: Effect): self.invest: ShareAllocationModel = self.add( ShareAllocationModel( model=self._model, - dims=['year', 'scenario'], + dims=('year', 'scenario'), label_of_element=self.label_of_element, label='invest', label_full=f'{self.label_full}(invest)', @@ -157,7 +157,7 @@ def __init__(self, model: SystemModel, element: Effect): self.operation: ShareAllocationModel = self.add( ShareAllocationModel( model=self._model, - dims=['time', 'year', 'scenario'], + dims=('time', 'year', 'scenario'), label_of_element=self.label_of_element, label='operation', label_full=f'{self.label_full}(operation)', @@ -404,13 +404,13 @@ def add_share_to_effects( self.effects[effect].model.operation.add_share( name, expression, - dims=['time', 'year', 'scenario'], + dims=('time', 'year', 'scenario'), ) elif target == 'invest': self.effects[effect].model.invest.add_share( name, expression, - dims=['year', 'scenario'], + dims=('year', 'scenario'), ) else: raise ValueError(f'Target {target} not supported!') @@ -418,13 +418,13 @@ def add_share_to_effects( def add_share_to_penalty(self, name: str, expression: linopy.LinearExpression) -> None: if expression.ndim != 0: raise TypeError(f'Penalty shares must be scalar expressions! ({expression.ndim=})') - self.penalty.add_share(name, expression, dims=[]) + self.penalty.add_share(name, expression, dims=()) def do_modeling(self): for effect in self.effects: effect.create_model(self._model) self.penalty = self.add( - ShareAllocationModel(self._model, dims=[], label_of_element='Penalty') + ShareAllocationModel(self._model, dims=(), label_of_element='Penalty') ) for model in [effect.model for effect in self.effects] + [self.penalty]: model.do_modeling() @@ -443,14 +443,14 @@ def _add_share_between_effects(self): self.effects[target_effect].model.operation.add_share( origin_effect.model.operation.label_full, origin_effect.model.operation.total_per_timestep * time_series, - dims=['time', 'year', 'scenario'], + dims=('time', 'year', 'scenario'), ) # 2. invest: -> hier ist es Scalar (share) for target_effect, factor in origin_effect.specific_share_to_other_effects_invest.items(): self.effects[target_effect].model.invest.add_share( origin_effect.model.invest.label_full, origin_effect.model.invest.total * factor, - dims=['year', 'scenario'], + dims=('year', 'scenario'), ) diff --git a/flixopt/features.py b/flixopt/features.py index c002a8ff6..76842fc50 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -823,11 +823,12 @@ def __init__( self._as_time_series = as_time_series def do_modeling(self): + dims =('time', 'year','scenario') if self._as_time_series else ('year','scenario') self.inside_piece = self.add( self._model.add_variables( binary=True, name=f'{self.label_full}|inside_piece', - coords=self._model.get_coords(time_dim=self._as_time_series), + coords=self._model.get_coords(dims=dims), ), 'inside_piece', ) @@ -837,7 +838,7 @@ def do_modeling(self): lower=0, upper=1, name=f'{self.label_full}|lambda0', - coords=self._model.get_coords(time_dim=self._as_time_series), + coords=self._model.get_coords(dims=dims), ), 'lambda0', ) @@ -847,7 +848,7 @@ def do_modeling(self): lower=0, upper=1, name=f'{self.label_full}|lambda1', - coords=self._model.get_coords(time_dim=self._as_time_series), + coords=self._model.get_coords(dims=dims), ), 'lambda1', ) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index bbccbfce5..26ce0c0e2 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -171,7 +171,7 @@ def calculate_hours_per_timestep(timesteps_extra: pd.DatetimeIndex) -> xr.DataAr """Calculate duration of each timestep as a 1D DataArray.""" hours_per_step = np.diff(timesteps_extra) / pd.Timedelta(hours=1) return xr.DataArray( - hours_per_step, coords={'time': timesteps_extra[:-1]}, dims=['time'], name='hours_per_timestep' + hours_per_step, coords={'time': timesteps_extra[:-1]}, dims='time', name='hours_per_timestep' ) @staticmethod diff --git a/flixopt/structure.py b/flixopt/structure.py index 3aad34489..5b95e0be4 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -104,7 +104,7 @@ def hours_of_previous_timesteps(self): def get_coords( self, - dims: Optional[FlowSystemDimensions] = None, + dims: Optional[Tuple[FlowSystemDimensions]] = None, extra_timestep=False, ) -> Optional[Union[Tuple[pd.Index], Tuple[pd.Index, pd.Index]]]: """ diff --git a/tests/test_dataconverter.py b/tests/test_dataconverter.py index 2fbad4a13..08c7d926c 100644 --- a/tests/test_dataconverter.py +++ b/tests/test_dataconverter.py @@ -447,7 +447,7 @@ class TestDataArrayConversion: def test_compatible_dataarray(self, time_coords): """Compatible DataArray should pass through.""" - original = xr.DataArray([10, 20, 30, 40, 50], coords={'time': time_coords}, dims=['time']) + original = xr.DataArray([10, 20, 30, 40, 50], coords={'time': time_coords}, dims='time') result = DataConverter.to_dataarray(original, coords={'time': time_coords}) assert result.shape == (5,) @@ -461,14 +461,14 @@ def test_compatible_dataarray(self, time_coords): def test_incompatible_dataarray_coords(self, time_coords): """DataArray with wrong coordinates should fail.""" wrong_times = pd.date_range('2025-01-01', periods=5, freq='D', name='time') - original = xr.DataArray([10, 20, 30, 40, 50], coords={'time': wrong_times}, dims=['time']) + original = xr.DataArray([10, 20, 30, 40, 50], coords={'time': wrong_times}, dims='time') with pytest.raises(ConversionError): DataConverter.to_dataarray(original, coords={'time': time_coords}) def test_incompatible_dataarray_dims(self, time_coords): """DataArray with wrong dimensions should fail.""" - original = xr.DataArray([10, 20, 30, 40, 50], coords={'wrong_dim': range(5)}, dims=['wrong_dim']) + original = xr.DataArray([10, 20, 30, 40, 50], coords={'wrong_dim': range(5)}, dims='wrong_dim') with pytest.raises(ConversionError): DataConverter.to_dataarray(original, coords={'time': time_coords}) @@ -476,7 +476,7 @@ def test_incompatible_dataarray_dims(self, time_coords): def test_dataarray_broadcast(self, time_coords, scenario_coords): """DataArray should broadcast to additional dimensions.""" # 1D time DataArray to 2D time+scenario - original = xr.DataArray([10, 20, 30, 40, 50], coords={'time': time_coords}, dims=['time']) + original = xr.DataArray([10, 20, 30, 40, 50], coords={'time': time_coords}, dims='time') result = DataConverter.to_dataarray(original, coords={'time': time_coords, 'scenario': scenario_coords}) assert result.shape == (5, 3) @@ -499,7 +499,7 @@ def test_2d_dataarray_broadcast_to_more_dimensions(self, standard_coords): original = xr.DataArray( [[10, 20, 30], [40, 50, 60], [70, 80, 90], [100, 110, 120], [130, 140, 150]], coords={'time': standard_coords['time'], 'scenario': standard_coords['scenario']}, - dims=['time', 'scenario'] + dims=('time', 'scenario') ) # Broadcast to 3D @@ -521,7 +521,7 @@ class TestTimeSeriesDataConversion: def test_timeseries_data_basic(self, time_coords): """TimeSeriesData should work like DataArray.""" - data_array = xr.DataArray([10, 20, 30, 40, 50], coords={'time': time_coords}, dims=['time']) + data_array = xr.DataArray([10, 20, 30, 40, 50], coords={'time': time_coords}, dims='time') ts_data = TimeSeriesData(data_array, aggregation_group='test') result = DataConverter.to_dataarray(ts_data, coords={'time': time_coords}) @@ -532,7 +532,7 @@ def test_timeseries_data_basic(self, time_coords): def test_timeseries_data_broadcast(self, time_coords, scenario_coords): """TimeSeriesData should broadcast to additional dimensions.""" - data_array = xr.DataArray([10, 20, 30, 40, 50], coords={'time': time_coords}, dims=['time']) + data_array = xr.DataArray([10, 20, 30, 40, 50], coords={'time': time_coords}, dims='time') ts_data = TimeSeriesData(data_array) result = DataConverter.to_dataarray(ts_data, coords={'time': time_coords, 'scenario': scenario_coords}) diff --git a/tests/test_effects_shares_summation.py b/tests/test_effects_shares_summation.py index e2dada7e9..b1ff5c3a3 100644 --- a/tests/test_effects_shares_summation.py +++ b/tests/test_effects_shares_summation.py @@ -46,8 +46,8 @@ def test_xarray_conversions(): """Test with xarray DataArrays that have dimensions.""" # Create DataArrays with a time dimension time_points = [1, 2, 3] - a_to_b = xr.DataArray([2.0, 2.1, 2.2], dims=['time'], coords={'time': time_points}) - b_to_c = xr.DataArray([3.0, 3.1, 3.2], dims=['time'], coords={'time': time_points}) + a_to_b = xr.DataArray([2.0, 2.1, 2.2], dims='time', coords={'time': time_points}) + b_to_c = xr.DataArray([3.0, 3.1, 3.2], dims='time', coords={'time': time_points}) conversion_dict = { 'A': {'B': a_to_b}, From c7568dd0ec63bb9941d9cdef7258c4b280a4924a Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 16 Jul 2025 16:18:44 +0200 Subject: [PATCH 189/448] Bugfix in validation logic and test --- flixopt/features.py | 2 +- tests/test_scenarios.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index 76842fc50..f53ebecca 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -964,7 +964,7 @@ def __init__( ): super().__init__(model, label_of_element=label_of_element, label=label, label_full=label_full) - if 'time' not in dims and max_per_hour is not None or min_per_hour is not None: + if 'time' not in dims and (max_per_hour is not None or min_per_hour is not None): raise ValueError('Both max_per_hour and min_per_hour cannot be used when has_time_dim is False') self._dims = dims diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py index 717d5919b..62f206e68 100644 --- a/tests/test_scenarios.py +++ b/tests/test_scenarios.py @@ -142,7 +142,7 @@ def flow_system_complex_scenarios() -> fx.FlowSystem: thermal_load = np.array([30, 0, 90, 110, 110, 20, 20, 20, 20]) electrical_load = np.array([40, 40, 40, 40, 40, 40, 40, 40, 40]) flow_system = fx.FlowSystem(pd.date_range('2020-01-01', periods=9, freq='h', name='time'), - pd.Index(['A', 'B', 'C'], name='scenario')) + scenarios=pd.Index(['A', 'B', 'C'], name='scenario')) # Define the components and flow_system flow_system.add_elements( fx.Effect('costs', '€', 'Kosten', is_standard=True, is_objective=True), From fc62634fdc6b9a000f76ac3ec2cb610d794332aa Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 16 Jul 2025 16:21:08 +0200 Subject: [PATCH 190/448] Improve Errors --- flixopt/core.py | 2 +- flixopt/flow_system.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/flixopt/core.py b/flixopt/core.py index 509cd8a91..c9eeb5c6a 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -185,7 +185,7 @@ def _match_series_to_dimension( """ if len(target_dims) == 0: if len(data) != 1: - raise ConversionError('Cannot convert multi-element Series without target dimensions') + raise ConversionError(f'Cannot convert multi-element Series without target dimensions. Got \n{data}\n and \n{coords}') return xr.DataArray(data.iloc[0]) # Try to match Series index to coordinates diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 26ce0c0e2..7dab1bed0 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -141,14 +141,14 @@ def _validate_years(years: pd.Index) -> pd.Index: years: The year index to validate """ if not isinstance(years, pd.Index) or len(years) == 0: - raise ConversionError('Years must be a non-empty Index') + raise ConversionError(f'Years must be a non-empty Index. Got {years}') if not ( years.dtype.kind == 'i' # integer dtype and years.is_monotonic_increasing # rising and years.is_unique ): - raise ConversionError('Years must be a monotonically increasing and unique Index') + raise ConversionError(f'Years must be a monotonically increasing and unique Index. Got {years}') if years.name != 'year': years = years.rename('year') From e320b9fa6c377bdafcbd02541d41bf11f112ce4f Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 16 Jul 2025 16:38:09 +0200 Subject: [PATCH 191/448] Improve weights handling and rescaling if None --- flixopt/flow_system.py | 11 +++++++---- flixopt/structure.py | 6 ++++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 7dab1bed0..f82568588 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -406,6 +406,10 @@ def fit_effects_to_model_coords( def connect_and_transform(self): """Transform data for all elements using the new simplified approach.""" + if self.connected_and_transformed: + logger.debug('FlowSystem already connected and transformed') + return + self.weights = self.fit_to_model_coords( 'weights', self.weights, has_time_dim=False ) @@ -413,10 +417,9 @@ def connect_and_transform(self): logger.warning(f'Scenario weights are not normalized to 1. This is recomended for a better scaled model. ' f'Sum of weights={self.weights.sum().item()}') - if not self.connected_and_transformed: - self._connect_network() - for element in list(self.components.values()) + list(self.effects.effects.values()) + list(self.buses.values()): - element.transform_data(self) + self._connect_network() + for element in list(self.components.values()) + list(self.effects.effects.values()) + list(self.buses.values()): + element.transform_data(self) self._connected_and_transformed = True def add_elements(self, *elements: Element) -> None: diff --git a/flixopt/structure.py b/flixopt/structure.py index 5b95e0be4..b0005cbd1 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -138,9 +138,11 @@ def get_coords( @property def weights(self) -> Union[int, xr.DataArray]: - """Returns the scenario weights of the FlowSystem.""" + """Returns the scenario weights of the FlowSystem. If None, return weights that are normalized to 1 (one)""" if self.flow_system.weights is None: - return 1 + weights = self.flow_system.fit_to_model_coords('weights', 1, has_time_dim=False) + + return weights / weights.sum() return self.flow_system.weights From 33a22aa56c13e467c171c18009c65baf4e41bb28 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 16 Jul 2025 16:38:17 +0200 Subject: [PATCH 192/448] Fix typehint --- flixopt/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flixopt/core.py b/flixopt/core.py index c9eeb5c6a..99b69b5ed 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -27,7 +27,7 @@ NonTemporalDataUser = Union[int, float, np.integer, np.floating, np.ndarray, pd.Series, pd.DataFrame, xr.DataArray] """User data which has no time dimension. Internally converted to a Scalar or an xr.DataArray without a time dimension.""" -NonTemporalData = Union[Scalar, xr.DataArray] +NonTemporalData = xr.DataArray """Internally used datatypes for non-temporal data. Can be a Scalar or an xr.DataArray.""" FlowSystemDimensions = Literal['time', 'year', 'scenario'] From 904211f7c8ed52fc07ce5b1590ef2800d7309eb2 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 16 Jul 2025 17:16:41 +0200 Subject: [PATCH 193/448] Update Broadcasting in Storage Bounds and improve type hints --- flixopt/components.py | 44 +++++++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/flixopt/components.py b/flixopt/components.py index f34e2945c..03a45e377 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -129,14 +129,14 @@ def __init__( label: str, charging: Flow, discharging: Flow, - capacity_in_flow_hours: Union[Scalar, InvestParameters], + capacity_in_flow_hours: Union[NonTemporalDataUser, InvestParameters], relative_minimum_charge_state: TemporalDataUser = 0, relative_maximum_charge_state: TemporalDataUser = 1, - initial_charge_state: Union[Scalar, Literal['lastValueOfSim']] = 0, - minimal_final_charge_state: Optional[Scalar] = None, - maximal_final_charge_state: Optional[Scalar] = None, - relative_minimum_final_charge_state: Optional[Scalar] = None, - relative_maximum_final_charge_state: Optional[Scalar] = None, + initial_charge_state: Union[NonTemporalDataUser, Literal['lastValueOfSim']] = 0, + minimal_final_charge_state: Optional[NonTemporalDataUser] = None, + maximal_final_charge_state: Optional[NonTemporalDataUser] = None, + relative_minimum_final_charge_state: Optional[NonTemporalDataUser] = None, + relative_maximum_final_charge_state: Optional[NonTemporalDataUser] = None, eta_charge: TemporalDataUser = 1, eta_discharge: TemporalDataUser = 1, relative_loss_per_hour: TemporalDataUser = 0, @@ -187,8 +187,8 @@ def __init__( self.relative_minimum_charge_state: TemporalDataUser = relative_minimum_charge_state self.relative_maximum_charge_state: TemporalDataUser = relative_maximum_charge_state - self.relative_minimum_final_charge_state: Scalar = relative_minimum_final_charge_state - self.relative_maximum_final_charge_state: Scalar = relative_maximum_final_charge_state + self.relative_minimum_final_charge_state = relative_minimum_final_charge_state + self.relative_maximum_final_charge_state = relative_maximum_final_charge_state self.initial_charge_state = initial_charge_state self.minimal_final_charge_state = minimal_final_charge_state @@ -230,6 +230,12 @@ def transform_data(self, flow_system: 'FlowSystem') -> None: self.maximal_final_charge_state = flow_system.fit_to_model_coords( f'{self.label_full}|maximal_final_charge_state', self.maximal_final_charge_state, has_time_dim=False ) + self.relative_minimum_final_charge_state = flow_system.fit_to_model_coords( + f'{self.label_full}|relative_minimum_final_charge_state', self.relative_minimum_final_charge_state, has_time_dim=False + ) + self.relative_maximum_final_charge_state = flow_system.fit_to_model_coords( + f'{self.label_full}|relative_maximum_final_charge_state', self.relative_maximum_final_charge_state, has_time_dim=False + ) if isinstance(self.capacity_in_flow_hours, InvestParameters): self.capacity_in_flow_hours.transform_data(flow_system, f'{self.label_full}|InvestParameters') else: @@ -625,29 +631,21 @@ def relative_charge_state_bounds(self) -> Tuple[xr.DataArray, xr.DataArray]: Returns: Tuple of (minimum_bounds, maximum_bounds) DataArrays extending to final timestep """ - final_timestep = self._model.flow_system.timesteps_extra[-1] - final_coords = {'time': [final_timestep]} + final_coords = {'time': [self._model.flow_system.timesteps_extra[-1]]} # Get final minimum charge state if self.element.relative_minimum_final_charge_state is None: - min_final = self.element.relative_minimum_charge_state.isel( - time=-1, drop=True - ).assign_coords(time=final_timestep) + min_final = self.element.relative_minimum_charge_state.isel(time=-1, drop=True) else: - min_final = xr.DataArray( - [self.element.relative_minimum_final_charge_state], coords=final_coords, dims='time' - ) + min_final = self.element.relative_minimum_final_charge_state + min_final = min_final.expand_dims('time').assign_coords(time=final_coords['time']) # Get final maximum charge state if self.element.relative_maximum_final_charge_state is None: - max_final = self.element.relative_maximum_charge_state.isel( - time=-1, drop=True - ).assign_coords(time=final_timestep) + max_final = self.element.relative_maximum_charge_state.isel(time=-1, drop=True) else: - max_final = xr.DataArray( - [self.element.relative_maximum_final_charge_state], coords=final_coords, dims='time' - ) - + max_final = self.element.relative_maximum_final_charge_state + max_final = max_final.expand_dims('time').assign_coords(time=final_coords['time']) # Concatenate with original bounds min_bounds = xr.concat([self.element.relative_minimum_charge_state, min_final], dim='time') max_bounds = xr.concat([self.element.relative_maximum_charge_state, max_final], dim='time') From 3c6c08bfda16820573e192fb1a92c1d692d99e2c Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 16 Jul 2025 17:17:44 +0200 Subject: [PATCH 194/448] Make .get_model_coords() return an actual xr.Coordinates Object --- flixopt/structure.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/flixopt/structure.py b/flixopt/structure.py index b0005cbd1..9931a951f 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -106,7 +106,7 @@ def get_coords( self, dims: Optional[Tuple[FlowSystemDimensions]] = None, extra_timestep=False, - ) -> Optional[Union[Tuple[pd.Index], Tuple[pd.Index, pd.Index]]]: + ) -> Optional[xr.Coordinates]: """ Returns the coordinates of the model @@ -131,10 +131,7 @@ def get_coords( if not coords: return None - if len(coords) == 1: - return (coords.popitem()[1],) - - return tuple(coords.values()) + return xr.Coordinates(coords) @property def weights(self) -> Union[int, xr.DataArray]: From 22a1cef407ba086813294042c227c27668f80020 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 17 Jul 2025 10:42:53 +0200 Subject: [PATCH 195/448] Improve get_coords() --- flixopt/structure.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/flixopt/structure.py b/flixopt/structure.py index 9931a951f..6d1df20e4 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -8,7 +8,7 @@ import logging import pathlib from io import StringIO -from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Tuple, Union +from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Tuple, Union, Collection import linopy import numpy as np @@ -104,34 +104,34 @@ def hours_of_previous_timesteps(self): def get_coords( self, - dims: Optional[Tuple[FlowSystemDimensions]] = None, - extra_timestep=False, + dims: Optional[Collection[str]] = None, + extra_timestep: bool = False, ) -> Optional[xr.Coordinates]: """ Returns the coordinates of the model Args: - dims: The dimensions to include in the coordinates. Defaults to all dimensions. Coords are ordered automatically - extra_timestep: If True, the extra timesteps are used instead of the regular timesteps + dims: The dimensions to include in the coordinates. If None, includes all dimensions + extra_timestep: If True, uses extra timesteps instead of regular timesteps Returns: - The coordinates of the model. Might also be None if no scenarios are present and time_dim is False + The coordinates of the model, or None if no coordinates are available + + Raises: + ValueError: If extra_timestep=True but 'time' is not in dims """ - if dims is not None and extra_timestep and 'time' not in dims: - raise ValueError('extra_timestep=True requires time to be included in dims') + if extra_timestep and dims is not None and 'time' not in dims: + raise ValueError('extra_timestep=True requires "time" to be included in dims') if dims is None: - coords = self.flow_system.coords + coords = dict(self.flow_system.coords) else: coords = {k: v for k, v in self.flow_system.coords.items() if k in dims} - if extra_timestep: + if extra_timestep and coords: coords['time'] = self.flow_system.timesteps_extra - if not coords: - return None - - return xr.Coordinates(coords) + return xr.Coordinates(coords) if coords else None @property def weights(self) -> Union[int, xr.DataArray]: From 44dbefcce98c736b484b2acbe572435ba13350f4 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 17 Jul 2025 10:45:07 +0200 Subject: [PATCH 196/448] Rename SystemModel to FlowSystemModel --- flixopt/aggregation.py | 4 ++-- flixopt/calculation.py | 12 ++++++------ flixopt/components.py | 12 ++++++------ flixopt/effects.py | 10 +++++----- flixopt/elements.py | 14 +++++++------- flixopt/features.py | 30 +++++++++++++++--------------- flixopt/flow_system.py | 8 ++++---- flixopt/structure.py | 14 +++++++------- tests/conftest.py | 4 ++-- 9 files changed, 54 insertions(+), 54 deletions(-) diff --git a/flixopt/aggregation.py b/flixopt/aggregation.py index d47a42997..47ac1336d 100644 --- a/flixopt/aggregation.py +++ b/flixopt/aggregation.py @@ -28,7 +28,7 @@ from .structure import ( Element, Model, - SystemModel, + FlowSystemModel, ) if TYPE_CHECKING: @@ -292,7 +292,7 @@ class AggregationModel(Model): def __init__( self, - model: SystemModel, + model: FlowSystemModel, aggregation_parameters: AggregationParameters, flow_system: FlowSystem, aggregation_data: Aggregation, diff --git a/flixopt/calculation.py b/flixopt/calculation.py index 764961b78..6bf86bb20 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -1,11 +1,11 @@ """ This module contains the Calculation functionality for the flixopt framework. -It is used to calculate a SystemModel for a given FlowSystem through a solver. +It is used to calculate a FlowSystemModel for a given FlowSystem through a solver. There are three different Calculation types: - 1. FullCalculation: Calculates the SystemModel for the full FlowSystem - 2. AggregatedCalculation: Calculates the SystemModel for the full FlowSystem, but aggregates the TimeSeriesData. + 1. FullCalculation: Calculates the FlowSystemModel for the full FlowSystem + 2. AggregatedCalculation: Calculates the FlowSystemModel for the full FlowSystem, but aggregates the TimeSeriesData. This simplifies the mathematical model and usually speeds up the solving process. - 3. SegmentedCalculation: Solves a SystemModel for each individual Segment of the FlowSystem. + 3. SegmentedCalculation: Solves a FlowSystemModel for each individual Segment of the FlowSystem. """ import logging @@ -32,7 +32,7 @@ from .flow_system import FlowSystem from .results import CalculationResults, SegmentedCalculationResults from .solvers import _Solver -from .structure import SystemModel +from .structure import FlowSystemModel logger = logging.getLogger('flixopt') @@ -81,7 +81,7 @@ def __init__( flow_system._used_in_calculation = True self.flow_system = flow_system - self.model: Optional[SystemModel] = None + self.model: Optional[FlowSystemModel] = None self.durations = {'modeling': 0.0, 'solving': 0.0, 'saving': 0.0} self.folder = pathlib.Path.cwd() / 'results' if folder is None else pathlib.Path(folder) diff --git a/flixopt/components.py b/flixopt/components.py index 03a45e377..685928714 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -14,7 +14,7 @@ from .elements import Component, ComponentModel, Flow from .features import InvestmentModel, OnOffModel, PiecewiseModel from .interface import InvestParameters, OnOffParameters, PiecewiseConversion -from .structure import SystemModel, register_class_for_io +from .structure import FlowSystemModel, register_class_for_io if TYPE_CHECKING: from .flow_system import FlowSystem @@ -58,7 +58,7 @@ def __init__( self.conversion_factors = conversion_factors or [] self.piecewise_conversion = piecewise_conversion - def create_model(self, model: SystemModel) -> 'LinearConverterModel': + def create_model(self, model: FlowSystemModel) -> 'LinearConverterModel': self._plausibility_checks() self.model = LinearConverterModel(model, self) return self.model @@ -200,7 +200,7 @@ def __init__( self.prevent_simultaneous_charge_and_discharge = prevent_simultaneous_charge_and_discharge self.balanced = balanced - def create_model(self, model: SystemModel) -> 'StorageModel': + def create_model(self, model: FlowSystemModel) -> 'StorageModel': self._plausibility_checks() self.model = StorageModel(model, self) return self.model @@ -393,7 +393,7 @@ def transform_data(self, flow_system: 'FlowSystem') -> None: class TransmissionModel(ComponentModel): - def __init__(self, model: SystemModel, element: Transmission): + def __init__(self, model: FlowSystemModel, element: Transmission): super().__init__(model, element) self.element: Transmission = element self.on_off: Optional[OnOffModel] = None @@ -444,7 +444,7 @@ def create_transmission_equation(self, name: str, in_flow: Flow, out_flow: Flow) class LinearConverterModel(ComponentModel): - def __init__(self, model: SystemModel, element: LinearConverter): + def __init__(self, model: FlowSystemModel, element: LinearConverter): super().__init__(model, element) self.element: LinearConverter = element self.on_off: Optional[OnOffModel] = None @@ -494,7 +494,7 @@ def do_modeling(self): class StorageModel(ComponentModel): """Model of Storage""" - def __init__(self, model: SystemModel, element: Storage): + def __init__(self, model: FlowSystemModel, element: Storage): super().__init__(model, element) self.element: Storage = element self.charge_state: Optional[linopy.Variable] = None diff --git a/flixopt/effects.py b/flixopt/effects.py index 0b5c9f5eb..23943d16b 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -15,7 +15,7 @@ from .core import Scalar, TemporalData, TemporalDataUser from .features import ShareAllocationModel -from .structure import Element, ElementModel, Interface, Model, SystemModel, register_class_for_io +from .structure import Element, ElementModel, Interface, Model, FlowSystemModel, register_class_for_io if TYPE_CHECKING: from .flow_system import FlowSystem @@ -127,7 +127,7 @@ def transform_data(self, flow_system: 'FlowSystem'): has_time_dim=False ) - def create_model(self, model: SystemModel) -> 'EffectModel': + def create_model(self, model: FlowSystemModel) -> 'EffectModel': self._plausibility_checks() self.model = EffectModel(model, self) return self.model @@ -138,7 +138,7 @@ def _plausibility_checks(self) -> None: class EffectModel(ElementModel): - def __init__(self, model: SystemModel, element: Effect): + def __init__(self, model: FlowSystemModel, element: Effect): super().__init__(model, element) self.element: Effect = element self.total: Optional[linopy.Variable] = None @@ -222,7 +222,7 @@ def __init__(self, *effects: List[Effect]): self.model: Optional[EffectCollectionModel] = None self.add_effects(*effects) - def create_model(self, model: SystemModel) -> 'EffectCollectionModel': + def create_model(self, model: FlowSystemModel) -> 'EffectCollectionModel': self._plausibility_checks() self.model = EffectCollectionModel(model, self) return self.model @@ -388,7 +388,7 @@ class EffectCollectionModel(Model): Handling all Effects """ - def __init__(self, model: SystemModel, effects: EffectCollection): + def __init__(self, model: FlowSystemModel, effects: EffectCollection): super().__init__(model, label_of_element='Effects') self.effects = effects self.penalty: Optional[ShareAllocationModel] = None diff --git a/flixopt/elements.py b/flixopt/elements.py index 968b70ca5..a546b5e9c 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -14,7 +14,7 @@ from .effects import TemporalEffectsUser from .features import InvestmentModel, OnOffModel, PreventSimultaneousUsageModel from .interface import InvestParameters, OnOffParameters -from .structure import Element, ElementModel, SystemModel, register_class_for_io +from .structure import Element, ElementModel, FlowSystemModel, register_class_for_io if TYPE_CHECKING: from .flow_system import FlowSystem @@ -63,7 +63,7 @@ def __init__( self.flows: Dict[str, Flow] = {flow.label: flow for flow in self.inputs + self.outputs} - def create_model(self, model: SystemModel) -> 'ComponentModel': + def create_model(self, model: FlowSystemModel) -> 'ComponentModel': self._plausibility_checks() self.model = ComponentModel(model, self) return self.model @@ -108,7 +108,7 @@ def __init__( self.inputs: List[Flow] = [] self.outputs: List[Flow] = [] - def create_model(self, model: SystemModel) -> 'BusModel': + def create_model(self, model: FlowSystemModel) -> 'BusModel': self._plausibility_checks() self.model = BusModel(model, self) return self.model @@ -227,7 +227,7 @@ def __init__( self.bus = bus self._bus_object = None - def create_model(self, model: SystemModel) -> 'FlowModel': + def create_model(self, model: FlowSystemModel) -> 'FlowModel': self._plausibility_checks() self.model = FlowModel(model, self) return self.model @@ -308,7 +308,7 @@ def invest_is_optional(self) -> bool: class FlowModel(ElementModel): - def __init__(self, model: SystemModel, element: Flow): + def __init__(self, model: FlowSystemModel, element: Flow): super().__init__(model, element) self.element: Flow = element self.flow_rate: Optional[linopy.Variable] = None @@ -490,7 +490,7 @@ def flow_rate_upper_bound(self) -> TemporalData: class BusModel(ElementModel): - def __init__(self, model: SystemModel, element: Bus): + def __init__(self, model: FlowSystemModel, element: Bus): super().__init__(model, element) self.element: Bus = element self.excess_input: Optional[linopy.Variable] = None @@ -538,7 +538,7 @@ def results_structure(self): class ComponentModel(ElementModel): - def __init__(self, model: SystemModel, element: Component): + def __init__(self, model: FlowSystemModel, element: Component): super().__init__(model, element) self.element: Component = element self.on_off: Optional[OnOffModel] = None diff --git a/flixopt/features.py b/flixopt/features.py index f53ebecca..bc4bfb9b3 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -12,7 +12,7 @@ from .config import CONFIG from .core import NonTemporalData, Scalar, TemporalData, FlowSystemDimensions from .interface import InvestParameters, OnOffParameters, Piecewise -from .structure import Model, SystemModel +from .structure import Model, FlowSystemModel logger = logging.getLogger('flixopt') @@ -22,7 +22,7 @@ class InvestmentModel(Model): def __init__( self, - model: SystemModel, + model: FlowSystemModel, label_of_element: str, parameters: InvestParameters, defining_variable: [linopy.Variable], @@ -240,7 +240,7 @@ class StateModel(Model): def __init__( self, - model: SystemModel, + model: FlowSystemModel, label_of_element: str, defining_variables: List[linopy.Variable], defining_bounds: List[Tuple[TemporalData, TemporalData]], @@ -255,7 +255,7 @@ def __init__( Models binary state variables based on a continous variable. Args: - model: The SystemModel that is used to create the model. + model: The FlowSystemModel that is used to create the model. label_of_element: The label of the parent (Element). Used to construct the full label of the model. defining_variables: List of Variables that are used to define the state defining_bounds: List of Tuples, defining the absolute bounds of each defining variable @@ -404,7 +404,7 @@ class SwitchStateModel(Model): def __init__( self, - model: SystemModel, + model: FlowSystemModel, label_of_element: str, state_variable: linopy.Variable, previous_state=0, @@ -488,7 +488,7 @@ class ConsecutiveStateModel(Model): def __init__( self, - model: SystemModel, + model: FlowSystemModel, label_of_element: str, state_variable: linopy.Variable, minimum_duration: Optional[TemporalData] = None, @@ -500,7 +500,7 @@ def __init__( Model and constraint the consecutive duration of a state variable. Args: - model: The SystemModel that is used to create the model. + model: The FlowSystemModel that is used to create the model. label_of_element: The label of the parent (Element). Used to construct the full label of the model. state_variable: The state variable that is used to model the duration. state = {0, 1} minimum_duration: The minimum duration of the state variable. @@ -665,7 +665,7 @@ class OnOffModel(Model): def __init__( self, - model: SystemModel, + model: FlowSystemModel, on_off_parameters: OnOffParameters, label_of_element: str, defining_variables: List[linopy.Variable], @@ -677,7 +677,7 @@ def __init__( Constructor for OnOffModel Args: - model: Reference to the SystemModel + model: Reference to the FlowSystemModel on_off_parameters: Parameters for the OnOffModel label_of_element: Label of the Parent defining_variables: List of Variables that are used to define the OnOffModel @@ -811,7 +811,7 @@ class PieceModel(Model): def __init__( self, - model: SystemModel, + model: FlowSystemModel, label_of_element: str, label: str, as_time_series: bool = True, @@ -865,7 +865,7 @@ def do_modeling(self): class PiecewiseModel(Model): def __init__( self, - model: SystemModel, + model: FlowSystemModel, label_of_element: str, piecewise_variables: Dict[str, Piecewise], zero_point: Optional[Union[bool, linopy.Variable]], @@ -878,7 +878,7 @@ def __init__( Each Piece is a tuple of (start, end). Args: - model: The SystemModel that is used to create the model. + model: The FlowSystemModel that is used to create the model. label_of_element: The label of the parent (Element). Used to construct the full label of the model. label: The label of the model. Used to construct the full label of the model. piecewise_variables: The variables to which the Pieces are assigned. @@ -952,7 +952,7 @@ def do_modeling(self): class ShareAllocationModel(Model): def __init__( self, - model: SystemModel, + model: FlowSystemModel, dims: List[FlowSystemDimensions], label_of_element: Optional[str] = None, label: Optional[str] = None, @@ -1065,7 +1065,7 @@ def add_share( class PiecewiseEffectsModel(Model): def __init__( self, - model: SystemModel, + model: FlowSystemModel, label_of_element: str, piecewise_origin: Tuple[str, Piecewise], piecewise_shares: Dict[str, Piecewise], @@ -1143,7 +1143,7 @@ class PreventSimultaneousUsageModel(Model): def __init__( self, - model: SystemModel, + model: FlowSystemModel, variables: List[linopy.Variable], label_of_element: str, label: str = 'PreventSimultaneousUsage', diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index f82568588..877db6fdc 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -32,7 +32,7 @@ TemporalEffectsUser, ) from .elements import Bus, Component, Flow -from .structure import Element, Interface, SystemModel +from .structure import Element, Interface, FlowSystemModel if TYPE_CHECKING: import pyvis @@ -98,7 +98,7 @@ def __init__( self.components: Dict[str, Component] = {} self.buses: Dict[str, Bus] = {} self.effects: EffectCollection = EffectCollection() - self.model: Optional[SystemModel] = None + self.model: Optional[FlowSystemModel] = None self._connected_and_transformed = False self._used_in_calculation = False @@ -448,10 +448,10 @@ def add_elements(self, *elements: Element) -> None: f'Tried to add incompatible object to FlowSystem: {type(new_element)=}: {new_element=} ' ) - def create_model(self) -> SystemModel: + def create_model(self) -> FlowSystemModel: if not self.connected_and_transformed: raise RuntimeError('FlowSystem is not connected_and_transformed. Call FlowSystem.connect_and_transform() first.') - self.model = SystemModel(self) + self.model = FlowSystemModel(self) return self.model def plot_network( diff --git a/flixopt/structure.py b/flixopt/structure.py index 6d1df20e4..9566e303f 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -43,9 +43,9 @@ def register_class_for_io(cls): return cls -class SystemModel(linopy.Model): +class FlowSystemModel(linopy.Model): """ - The SystemModel is the linopy Model that is used to create the mathematical model of the flow_system. + The FlowSystemModel is the linopy Model that is used to create the mathematical model of the flow_system. It is used to create and store the variables and constraints for the flow_system. """ @@ -667,7 +667,7 @@ def _plausibility_checks(self) -> None: """This function is used to do some basic plausibility checks for each Element during initialization""" raise NotImplementedError('Every Element needs a _plausibility_checks() method') - def create_model(self, model: SystemModel) -> 'ElementModel': + def create_model(self, model: FlowSystemModel) -> 'ElementModel': raise NotImplementedError('Every Element needs a create_model() method') @property @@ -700,11 +700,11 @@ class Model: """Stores Variables and Constraints.""" def __init__( - self, model: SystemModel, label_of_element: str, label: str = '', label_full: Optional[str] = None + self, model: FlowSystemModel, label_of_element: str, label: str = '', label_full: Optional[str] = None ): """ Args: - model: The SystemModel that is used to create the model. + model: The FlowSystemModel that is used to create the model. label_of_element: The label of the parent (Element). Used to construct the full label of the model. label: The label of the model. Used to construct the full label of the model. label_full: The full label of the model. Can overwrite the full label constructed from the other labels. @@ -834,10 +834,10 @@ def all_sub_models(self) -> List['Model']: class ElementModel(Model): """Stores the mathematical Variables and Constraints for Elements""" - def __init__(self, model: SystemModel, element: Element): + def __init__(self, model: FlowSystemModel, element: Element): """ Args: - model: The SystemModel that is used to create the model. + model: The FlowSystemModel that is used to create the model. element: The element this model is created for. """ super().__init__(model, label_of_element=element.label_full, label=element.label, label_full=element.label_full) diff --git a/tests/conftest.py b/tests/conftest.py index 7c1b08a5b..5d98cdcb5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,7 +13,7 @@ import xarray as xr import flixopt as fx -from flixopt.structure import SystemModel +from flixopt.structure import FlowSystemModel @pytest.fixture() @@ -496,7 +496,7 @@ def create_calculation_and_solve(flow_system: fx.FlowSystem, solver, name: str, return calculation -def create_linopy_model(flow_system: fx.FlowSystem) -> SystemModel: +def create_linopy_model(flow_system: fx.FlowSystem) -> FlowSystemModel: calculation = fx.FullCalculation('GenericName', flow_system) calculation.do_modeling() return calculation.model From f82556aafaafa5dfb2f3e109bcdc705b5d348013 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 18 Jul 2025 10:23:03 +0200 Subject: [PATCH 197/448] First steps --- flixopt/features.py | 905 +++++++++++++++++++++++++++---------------- flixopt/structure.py | 21 + 2 files changed, 598 insertions(+), 328 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index bc4bfb9b3..e495e2973 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -12,225 +12,623 @@ from .config import CONFIG from .core import NonTemporalData, Scalar, TemporalData, FlowSystemDimensions from .interface import InvestParameters, OnOffParameters, Piecewise -from .structure import Model, FlowSystemModel +from .structure import Model, FlowSystemModel, BaseFeatureModel logger = logging.getLogger('flixopt') -class InvestmentModel(Model): - """Class for modeling an investment""" +class ModelingPrimitives: + """Mathematical modeling primitives returning (variables, constraints) tuples""" - def __init__( - self, + @staticmethod + def binary_state_pair( + model: FlowSystemModel, name: str, coords: List[str] = None + ) -> Tuple[Dict[str, linopy.Variable], Dict[str, linopy.Constraint]]: + """ + Creates complementary binary variables with completeness constraint. + + Mathematical formulation: + on[t] + off[t] = 1 ∀t + on[t], off[t] ∈ {0, 1} + + Returns: + variables: {'on': binary_var, 'off': binary_var} + constraints: {'complementary': constraint} + """ + coords = coords or ['time'] + + on = model.add_variables(binary=True, name=f'{name}|on', coords=model.get_coords(coords)) + off = model.add_variables(binary=True, name=f'{name}|off', coords=model.get_coords(coords)) + + # Constraint: on + off = 1 + complementary = model.add_constraints(on + off == 1, name=f'{name}|complementary') + + variables = {'on': on, 'off': off} + constraints = {'complementary': complementary} + + return variables, constraints + + @staticmethod + def proportionally_bounded_variable( model: FlowSystemModel, - label_of_element: str, - parameters: InvestParameters, - defining_variable: [linopy.Variable], - relative_bounds_of_defining_variable: Tuple[TemporalData, TemporalData], - label: Optional[str] = None, - on_variable: Optional[linopy.Variable] = None, - ): - super().__init__(model, label_of_element, label) - self.size: Optional[Union[Scalar, linopy.Variable]] = None - self.is_invested: Optional[linopy.Variable] = None - self.scenario_of_investment: Optional[linopy.Variable] = None + name: str, + controlling_variable, + bounds: Tuple[TemporalData, TemporalData], + coords: List[str] = None, + ) -> Tuple[Dict[str, linopy.Variable], Dict[str, linopy.Constraint]]: + """ + Creates variable with bounds proportional to another variable. - self.piecewise_effects: Optional[PiecewiseEffectsModel] = None + Mathematical formulation: + lower_factor[t] * controller[t] ≤ variable[t] ≤ upper_factor[t] * controller[t] ∀t - self._on_variable = on_variable - self._defining_variable = defining_variable - self._relative_bounds_of_defining_variable = relative_bounds_of_defining_variable - self.parameters = parameters + Returns: + variables: {'variable': bounded_var} + constraints: {'lower_bound': constraint, 'upper_bound': constraint} + """ + coords = coords or ['time'] + variable = model.add_variables(name=f'{name}|bounded', coords=model.get_coords(coords)) - def do_modeling(self): - self.size = self.add( - self._model.add_variables( - lower=0 if self.parameters.optional else self.parameters.minimum_or_fixed_size, - upper=self.parameters.maximum_or_fixed_size, - name=f'{self.label_full}|size', - coords=self._model.get_coords(['year', 'scenario']), - ), - 'size', + lower_factor, upper_factor = bounds + + # Constraints: lower_factor * controller ≤ var ≤ upper_factor * controller + lower_bound = model.add_constraints( + variable >= controlling_variable * lower_factor, name=f'{name}|proportional_lb' + ) + upper_bound = model.add_constraints( + variable <= controlling_variable * upper_factor, name=f'{name}|proportional_ub' ) - # Optional - if self.parameters.optional: - self.is_invested = self.add( - self._model.add_variables( - binary=True, - name=f'{self.label_full}|is_invested', - coords=self._model.get_coords(['year', 'scenario']), - ), - 'is_invested', - ) + variables = {'variable': variable} + constraints = {'lower_bound': lower_bound, 'upper_bound': upper_bound} - self._create_bounds_for_optional_investment() + return variables, constraints - if self._model.flow_system.scenarios is not None: - self._create_bounds_for_scenarios() + @staticmethod + def expression_tracking_variable( + model: FlowSystemModel, + name: str, + tracked_expression, + bounds: Tuple[TemporalData, TemporalData] = None, + coords: List[str] = None, + ) -> Tuple[Dict[str, linopy.Variable], Dict[str, linopy.Constraint]]: + """ + Creates variable that equals a given expression. - # Bounds for defining variable - self._create_bounds_for_defining_variable() + Mathematical formulation: + tracker = expression + lower ≤ tracker ≤ upper (if bounds provided) - self._create_shares() + Returns: + variables: {'tracker': tracker_var} + constraints: {'tracking': constraint} + """ + coords = coords or ['year', 'scenario'] - def _create_shares(self): - # fix_effects: - fix_effects = self.parameters.fix_effects - if fix_effects != {}: - self._model.effects.add_share_to_effects( - name=self.label_of_element, - expressions={ - effect: self.is_invested * factor if self.is_invested is not None else factor - for effect, factor in fix_effects.items() - }, - target='invest', + if bounds: + tracker = model.add_variables( + lower=bounds[0], upper=bounds[1], name=f'{name}|tracker', coords=model.get_coords(coords) ) + else: + tracker = model.add_variables(name=f'{name}|tracker', coords=model.get_coords(coords)) - if self.parameters.divest_effects != {} and self.parameters.optional: - # share: divest_effects - isInvested * divest_effects - self._model.effects.add_share_to_effects( - name=self.label_of_element, - expressions={effect: -self.is_invested * factor + factor for effect, factor in self.parameters.divest_effects.items()}, - target='invest', - ) + # Constraint: tracker = expression + tracking = model.add_constraints(tracker == tracked_expression, name=f'{name}|tracking_eq') + + variables = {'tracker': tracker} + constraints = {'tracking': tracking} + + return variables, constraints + + @staticmethod + def state_transition_variables( + model: FlowSystemModel, name: str, state_variable, previous_state=0 + ) -> Tuple[Dict[str, linopy.Variable], Dict[str, linopy.Constraint]]: + """ + Creates switch-on/off variables with state transition logic. + + Mathematical formulation: + switch_on[t] - switch_off[t] = state[t] - state[t-1] ∀t > 0 + switch_on[0] - switch_off[0] = state[0] - previous_state + switch_on[t] + switch_off[t] ≤ 1 ∀t + switch_on[t], switch_off[t] ∈ {0, 1} + + Returns: + variables: {'switch_on': binary_var, 'switch_off': binary_var} + constraints: {'transition': constraint, 'initial': constraint, 'mutex': constraint} + """ + switch_on = model.add_variables(binary=True, name=f'{name}|switch_on', coords=model.get_coords(['time'])) + switch_off = model.add_variables(binary=True, name=f'{name}|switch_off', coords=model.get_coords(['time'])) + + # State transition constraints for t > 0 + transition = model.add_constraints( + switch_on.isel(time=slice(1, None)) - switch_off.isel(time=slice(1, None)) + == state_variable.isel(time=slice(1, None)) - state_variable.isel(time=slice(None, -1)), + name=f'{name}|state_transition', + ) + + # Initial state transition for t = 0 + initial = model.add_constraints( + switch_on.isel(time=0) - switch_off.isel(time=0) == state_variable.isel(time=0) - previous_state, + name=f'{name}|initial_transition', + ) + + # At most one switch per timestep + mutex = model.add_constraints(switch_on + switch_off <= 1, name=f'{name}|switch_mutex') + + variables = {'switch_on': switch_on, 'switch_off': switch_off} + constraints = {'transition': transition, 'initial': initial, 'mutex': mutex} + + return variables, constraints + + @staticmethod + def big_m_binary_bounds( + model: FlowSystemModel, + name: str, + variable, + binary_control, + size_variable, + relative_bounds: Tuple[TemporalData, TemporalData], + ) -> Tuple[Dict, Dict[str, linopy.Constraint]]: + """ + Creates bounds controlled by both binary and continuous variables. + + Mathematical formulation: + variable[t] ≤ size[t] * upper_factor[t] ∀t + + If binary_control provided: + variable[t] ≥ M * (binary[t] - 1) + size[t] * lower_factor[t] ∀t + where M = max(size) * max(upper_factor) + Else: + variable[t] ≥ size[t] * lower_factor[t] ∀t + + Returns: + variables: {} (no new variables created) + constraints: {'upper_bound': constraint, 'lower_bound': constraint} + """ + rel_lower, rel_upper = relative_bounds + + # Upper bound: variable ≤ size * upper_factor + upper_bound = model.add_constraints(variable <= size_variable * rel_upper, name=f'{name}|size_upper_bound') - if self.parameters.specific_effects != {}: - self._model.effects.add_share_to_effects( - name=self.label_of_element, - expressions={effect: self.size * factor for effect, factor in self.parameters.specific_effects.items()}, - target='invest', + if binary_control is not None: + # Big-M lower bound: variable ≥ M*(binary-1) + size*lower_factor + big_m = size_variable.max() * rel_upper.max() # Conservative big-M + lower_bound = model.add_constraints( + variable >= big_m * (binary_control - 1) + size_variable * rel_lower, + name=f'{name}|binary_controlled_lower_bound', ) + else: + # Simple lower bound: variable ≥ size * lower_factor + lower_bound = model.add_constraints(variable >= size_variable * rel_lower, name=f'{name}|size_lower_bound') - if self.parameters.piecewise_effects: - self.piecewise_effects = self.add( - PiecewiseEffectsModel( - model=self._model, - label_of_element=self.label_of_element, - piecewise_origin=(self.size.name, self.parameters.piecewise_effects.piecewise_origin), - piecewise_shares=self.parameters.piecewise_effects.piecewise_shares, - zero_point=self.is_invested, - ), - 'segments', + variables = {} # No new variables created + constraints = {'upper_bound': upper_bound, 'lower_bound': lower_bound} + + return variables, constraints + + @staticmethod + def consecutive_duration_tracking( + model: FlowSystemModel, + name: str, + state_variable: linopy.Variable, + minimum_duration: Optional[TemporalData] = None, + maximum_duration: Optional[TemporalData] = None, + previous_duration: TemporalData = 0, + ) -> Tuple[Dict[str, linopy.Variable], Dict[str, linopy.Constraint]]: + """ + Creates consecutive duration tracking for a binary state variable. + + Mathematical formulation: + duration[t] ≤ state[t] * M ∀t + duration[t+1] ≤ duration[t] + hours_per_step[t] ∀t + duration[t+1] ≥ duration[t] + hours_per_step[t] + (state[t+1] - 1) * M ∀t + duration[0] = (hours_per_step[0] + previous_duration) * state[0] + + If minimum_duration provided: + duration[t] ≥ (state[t-1] - state[t]) * minimum_duration[t-1] ∀t > 0 + + Args: + state_variable: Binary state variable to track duration for + minimum_duration: Optional minimum consecutive duration + maximum_duration: Optional maximum consecutive duration + previous_duration: Duration from before first timestep + + Returns: + variables: {'duration': duration_var} + constraints: {'upper_bound': constraint, 'forward': constraint, 'backward': constraint, ...} + """ + hours_per_step = model.hours_per_step + mega = hours_per_step.sum('time') + previous_duration # Big-M value + + # Duration variable + duration = model.add_variables( + lower=0, + upper=maximum_duration if maximum_duration is not None else mega, + coords=model.get_coords(['time']), + name=f'{name}|duration', + ) + + constraints = {} + + # Upper bound: duration[t] ≤ state[t] * M + constraints['upper_bound'] = model.add_constraints( + duration <= state_variable * mega, name=f'{name}|duration_upper_bound' + ) + + # Forward constraint: duration[t+1] ≤ duration[t] + hours_per_step[t] + constraints['forward'] = model.add_constraints( + duration.isel(time=slice(1, None)) + <= duration.isel(time=slice(None, -1)) + hours_per_step.isel(time=slice(None, -1)), + name=f'{name}|duration_forward', + ) + + # Backward constraint: duration[t+1] ≥ duration[t] + hours_per_step[t] + (state[t+1] - 1) * M + constraints['backward'] = model.add_constraints( + duration.isel(time=slice(1, None)) + >= duration.isel(time=slice(None, -1)) + + hours_per_step.isel(time=slice(None, -1)) + + (state_variable.isel(time=slice(1, None)) - 1) * mega, + name=f'{name}|duration_backward', + ) + + # Initial condition: duration[0] = (hours_per_step[0] + previous_duration) * state[0] + constraints['initial'] = model.add_constraints( + duration.isel(time=0) + == (hours_per_step.isel(time=0) + previous_duration) * state_variable.isel(time=0), + name=f'{name}|duration_initial', + ) + + # Minimum duration constraint if provided + if minimum_duration is not None: + constraints['minimum'] = model.add_constraints( + duration.isel(time=slice(1, None)) + >= (state_variable.isel(time=slice(None, -1)) - state_variable.isel(time=slice(1, None))) + * minimum_duration.isel(time=slice(None, -1)), + name=f'{name}|duration_minimum', ) - self.piecewise_effects.do_modeling() - def _create_bounds_for_optional_investment(self): - if self.parameters.fixed_size: - # eq: investment_size = isInvested * fixed_size - self.add( - self._model.add_constraints( - self.size == self.is_invested * self.parameters.fixed_size, name=f'{self.label_full}|is_invested' - ), - 'is_invested', + # Handle initial condition for minimum duration + if previous_duration > 0 and previous_duration < minimum_duration.isel(time=0).max(): + constraints['initial_minimum'] = model.add_constraints( + state_variable.isel(time=0) == 1, name=f'{name}|duration_initial_minimum' + ) + + variables = {'duration': duration} + + return variables, constraints + + +class ModelingPatterns: + """High-level patterns that compose primitives and return (variables, constraints) tuples""" + + @staticmethod + def investment_sizing_pattern( + model: FlowSystemModel, + name: str, + size_bounds: Tuple[TemporalData, TemporalData], + controlled_variables: List[linopy.Variable] = None, + control_factors: List[Tuple[TemporalData, TemporalData]] = None, + optional: bool = False, + ) -> Tuple[Dict[str, linopy.Variable], Dict[str, linopy.Constraint]]: + """ + Complete investment sizing pattern with optional binary decision. + + Returns: + variables: {'size': size_var, 'is_invested': binary_var (if optional)} + constraints: {'investment_upper_bound': constraint, 'investment_lower_bound': constraint, ...} + """ + variables = {} + constraints = {} + + # Investment size variable + size_min, size_max = size_bounds + variables['size'] = model.add_variables( + lower=size_min, + upper=size_max, + name=f'{name}|investment_size', + coords=model.get_coords(['year', 'scenario']), + ) + + # Optional binary investment decision + if optional: + variables['is_invested'] = model.add_variables( + binary=True, name=f'{name}|is_invested', coords=model.get_coords(['year', 'scenario']) ) + # Link size to investment decision + if abs(size_min - size_max) < 1e-10: # Fixed size case + constraints['fixed_investment_size'] = model.add_constraints( + variables['size'] == variables['is_invested'] * size_max, name=f'{name}|fixed_investment_size' + ) + else: # Variable size case + constraints['investment_upper_bound'] = model.add_constraints( + variables['size'] <= variables['is_invested'] * size_max, name=f'{name}|investment_upper_bound' + ) + constraints['investment_lower_bound'] = model.add_constraints( + variables['size'] >= variables['is_invested'] * max(CONFIG.modeling.EPSILON, size_min), + name=f'{name}|investment_lower_bound', + ) + + # Control dependent variables + if controlled_variables and control_factors: + for i, (var, factors) in enumerate(zip(controlled_variables, control_factors)): + _, control_constraints = ModelingPrimitives.big_m_binary_bounds( + model, f'{name}|control_{i}', var, variables.get('is_invested'), variables['size'], factors + ) + # Flatten control constraints with indexed names + constraints[f'control_{i}_upper_bound'] = control_constraints['upper_bound'] + constraints[f'control_{i}_lower_bound'] = control_constraints['lower_bound'] + + return variables, constraints + + @staticmethod + def operational_binary_control_pattern( + model: FlowSystemModel, + name: str, + controlled_variables: List[linopy.Variable], + variable_bounds: List[Tuple[TemporalData, TemporalData]], + use_complement: bool = False, + track_total_duration: bool = False, + track_switches: bool = False, + previous_state=0, + duration_bounds: Tuple[TemporalData, TemporalData] = None, + ) -> Tuple[Dict[str, linopy.Variable], Dict[str, linopy.Constraint]]: + """ + Operational binary control with optional features. + + Returns: + variables: {'on': binary_var, 'off': binary_var (optional), 'total_duration': var (optional), ...} + constraints: {'complementary': constraint, 'control_0_lower': constraint, ...} + """ + variables = {} + constraints = {} + + # Main binary state + if use_complement: + state_vars, state_constraints = ModelingPrimitives.binary_state_pair(model, name) + variables.update(state_vars) + constraints.update(state_constraints) else: - # eq1: P_invest <= isInvested * investSize_max - self.add( - self._model.add_constraints( - self.size <= self.is_invested * self.parameters.maximum_size, - name=f'{self.label_full}|is_invested_ub', - ), - 'is_invested_ub', + variables['on'] = model.add_variables(binary=True, name=f'{name}|on', coords=model.get_coords(['time'])) + + # Control variables with binary state + for i, (var, (lower_bound, upper_bound)) in enumerate(zip(controlled_variables, variable_bounds)): + # Lower bound constraint + constraints[f'control_{i}_lower'] = model.add_constraints( + variables['on'] * max(CONFIG.modeling.EPSILON, lower_bound) <= var, name=f'{name}|control_{i}_lower' + ) + # Upper bound constraint + constraints[f'control_{i}_upper'] = model.add_constraints( + var <= variables['on'] * upper_bound, name=f'{name}|control_{i}_upper' ) - # eq2: P_invest >= isInvested * max(epsilon, investSize_min) - self.add( - self._model.add_constraints( - self.size >= self.is_invested * np.maximum(CONFIG.modeling.EPSILON, self.parameters.minimum_or_fixed_size), - name=f'{self.label_full}|is_invested_lb', - ), - 'is_invested_lb', + # Total duration tracking + if track_total_duration: + duration_expr = (variables['on'] * model.hours_per_step).sum('time') + duration_vars, duration_constraints = ModelingPrimitives.expression_tracking_variable( + model, f'{name}|duration', duration_expr, duration_bounds ) + variables['total_duration'] = duration_vars['tracker'] + constraints['duration_tracking'] = duration_constraints['tracking'] - def _create_bounds_for_defining_variable(self): - variable = self._defining_variable - lb_relative, ub_relative = self._relative_bounds_of_defining_variable - if np.all(lb_relative == ub_relative): - self.add( - self._model.add_constraints( - variable == self.size * ub_relative, name=f'{self.label_full}|fix_{variable.name}' - ), - f'fix_{variable.name}', + # Switch tracking + if track_switches: + switch_vars, switch_constraints = ModelingPrimitives.state_transition_variables( + model, f'{name}|switches', variables['on'], previous_state ) - return + variables.update(switch_vars) + # Add switch constraints with prefixed names + for switch_name, switch_constraint in switch_constraints.items(): + constraints[f'switch_{switch_name}'] = switch_constraint - # eq: defining_variable(t) <= size * upper_bound(t) - self.add( - self._model.add_constraints( - variable <= self.size * ub_relative, name=f'{self.label_full}|ub_{variable.name}' - ), - f'ub_{variable.name}', - ) + return variables, constraints - if self._on_variable is None: - # eq: defining_variable(t) >= investment_size * relative_minimum(t) - self.add( - self._model.add_constraints( - variable >= self.size * lb_relative, name=f'{self.label_full}|lb_{variable.name}' - ), - f'lb_{variable.name}', - ) + @staticmethod + def operational_binary_control_pattern( + model: FlowSystemModel, + name: str, + controlled_variables: List[linopy.Variable], + variable_bounds: List[Tuple[TemporalData, TemporalData]], + use_complement: bool = False, + track_total_duration: bool = False, + track_switches: bool = False, + previous_state=0, + duration_bounds: Tuple[TemporalData, TemporalData] = None, + track_consecutive_on: bool = False, + consecutive_on_bounds: Tuple[Optional[TemporalData], Optional[TemporalData]] = (None, None), + previous_on_duration: TemporalData = 0, + track_consecutive_off: bool = False, + consecutive_off_bounds: Tuple[Optional[TemporalData], Optional[TemporalData]] = (None, None), + previous_off_duration: TemporalData = 0, + ) -> Tuple[Dict[str, linopy.Variable], Dict[str, linopy.Constraint]]: + """ + Enhanced operational binary control with consecutive duration tracking. + + New Args: + track_consecutive_on: Whether to track consecutive on duration + consecutive_on_bounds: (min_duration, max_duration) for consecutive on + previous_on_duration: Previous consecutive on duration + track_consecutive_off: Whether to track consecutive off duration + consecutive_off_bounds: (min_duration, max_duration) for consecutive off + previous_off_duration: Previous consecutive off duration + """ + variables = {} + constraints = {} + + # Main binary state (existing logic) + if use_complement: + state_vars, state_constraints = ModelingPrimitives.binary_state_pair(model, name) + variables.update(state_vars) + constraints.update(state_constraints) else: - ## 2. Gleichung: Minimum durch Investmentgröße und On - # eq: defining_variable(t) >= mega * (On(t)-1) + size * relative_minimum(t) - # ... mit mega = relative_maximum * maximum_size - # äquivalent zu:. - # eq: - defining_variable(t) + mega * On(t) + size * relative_minimum(t) <= + mega - mega = self.parameters.maximum_size * lb_relative - on = self._on_variable - self.add( - self._model.add_constraints( - variable >= mega * (on - 1) + self.size * lb_relative, name=f'{self.label_full}|lb_{variable.name}' - ), - f'lb_{variable.name}', + variables['on'] = model.add_variables(binary=True, name=f'{name}|on', coords=model.get_coords(['time'])) + + # Control variables (existing logic) + for i, (var, (lower_bound, upper_bound)) in enumerate(zip(controlled_variables, variable_bounds)): + constraints[f'control_{i}_lower'] = model.add_constraints( + variables['on'] * max(CONFIG.modeling.EPSILON, lower_bound) <= var, name=f'{name}|control_{i}_lower' + ) + constraints[f'control_{i}_upper'] = model.add_constraints( + var <= variables['on'] * upper_bound, name=f'{name}|control_{i}_upper' ) - # anmerkung: Glg bei Spezialfall relative_minimum = 0 redundant zu OnOff ?? - def _create_bounds_for_scenarios(self): - if isinstance(self.parameters.investment_scenarios, str): - if self.parameters.investment_scenarios == 'individual': - return - raise ValueError(f'Invalid value for investment_scenarios: {self.parameters.investment_scenarios}') + # Total duration tracking (existing logic) + if track_total_duration: + duration_expr = (variables['on'] * model.hours_per_step).sum('time') + duration_vars, duration_constraints = ModelingPrimitives.expression_tracking_variable( + model, f'{name}|duration', duration_expr, duration_bounds + ) + variables['total_duration'] = duration_vars['tracker'] + constraints['duration_tracking'] = duration_constraints['tracking'] - if self.parameters.investment_scenarios is None: - self.add( - self._model.add_constraints( - self.size.isel(scenario=slice(None, -1)) == self.size.isel(scenario=slice(1, None)), - name=f'{self.label_full}|equalize_size_per_scenario', - ), - 'equalize_size_per_scenario', + # Switch tracking (existing logic) + if track_switches: + switch_vars, switch_constraints = ModelingPrimitives.state_transition_variables( + model, f'{name}|switches', variables['on'], previous_state ) - return - if not isinstance(self.parameters.investment_scenarios, list): - raise ValueError(f'Invalid value for investment_scenarios: {self.parameters.investment_scenarios}') - if not all(scenario in self._model.time_series_collection.scenarios for scenario in self.parameters.investment_scenarios): - raise ValueError(f'Some scenarios in investment_scenarios are not present in the time_series_collection: ' - f'{self.parameters.investment_scenarios}. This might be due to selecting a subset of ' - f'all scenarios, which is not yet supported.') - - investment_scenarios = self._model.time_series_collection.scenarios.intersection(self.parameters.investment_scenarios) - no_investment_scenarios = self._model.time_series_collection.scenarios.difference(self.parameters.investment_scenarios) - - # eq: size(s) = size(s') for s, s' in investment_scenarios - if len(investment_scenarios) > 1: - self.add( - self._model.add_constraints( - self.size.sel(scenario=investment_scenarios[:-1]) == self.size.sel(scenario=investment_scenarios[1:]), - name=f'{self.label_full}|investment_scenarios', - ), - 'investment_scenarios', + variables.update(switch_vars) + for switch_name, switch_constraint in switch_constraints.items(): + constraints[f'switch_{switch_name}'] = switch_constraint + + # NEW: Consecutive on duration tracking + if track_consecutive_on: + min_on, max_on = consecutive_on_bounds + consecutive_on_vars, consecutive_on_constraints = ModelingPrimitives.consecutive_duration_tracking( + model, + f'{name}|consecutive_on', + variables['on'], + minimum_duration=min_on, + maximum_duration=max_on, + previous_duration=previous_on_duration, ) - - if len(no_investment_scenarios) >= 1: - self.add( - self._model.add_constraints( - self.size.sel(scenario=no_investment_scenarios) == 0, - name=f'{self.label_full}|no_investment_scenarios', - ), - 'no_investment_scenarios', + variables['consecutive_on_duration'] = consecutive_on_vars['duration'] + for cons_name, cons_constraint in consecutive_on_constraints.items(): + constraints[f'consecutive_on_{cons_name}'] = cons_constraint + + # NEW: Consecutive off duration tracking + if track_consecutive_off and 'off' in variables: + min_off, max_off = consecutive_off_bounds + consecutive_off_vars, consecutive_off_constraints = ModelingPrimitives.consecutive_duration_tracking( + model, + f'{name}|consecutive_off', + variables['off'], + minimum_duration=min_off, + maximum_duration=max_off, + previous_duration=previous_off_duration, ) + variables['consecutive_off_duration'] = consecutive_off_vars['duration'] + for cons_name, cons_constraint in consecutive_off_constraints.items(): + constraints[f'consecutive_off_{cons_name}'] = cons_constraint + + return variables, constraints + + +class InvestmentModel(BaseFeatureModel): + def create_variables(self): + # Clean tuple unpacking + variables, constraints = ModelingPatterns.investment_sizing_pattern( + model=self._model, + name=self.label_full, + size_bounds=( + 0 if self.parameters.optional else self.parameters.minimum_or_fixed_size, + self.parameters.maximum_or_fixed_size, + ), + controlled_variables=[self._defining_variable], + control_factors=[self._relative_bounds_of_defining_variable], + optional=self.parameters.optional, + ) + + # Register variables + self.size = self.add(variables['size'], 'size') + if 'is_invested' in variables: + self.is_invested = self.add(variables['is_invested'], 'is_invested') + + # Register all constraints + for constraint_name, constraint in constraints.items(): + self.add(constraint, constraint_name) + + +class OnOffModel(BaseFeatureModel): + """OnOff model using factory patterns""" + + def __init__( + self, + model: FlowSystemModel, + on_off_parameters: OnOffParameters, + label_of_element: str, + defining_variables: List[linopy.Variable], + defining_bounds: List[Tuple[TemporalData, TemporalData]], + previous_values: List[Optional[TemporalData]], + label: Optional[str] = None, + ): + super().__init__(model, label_of_element, on_off_parameters, label) + + self._defining_variables = defining_variables + self._defining_bounds = defining_bounds + self._previous_values = previous_values + + # All variables set by factory + self.on: Optional[linopy.Variable] = None + self.off: Optional[linopy.Variable] = None + self.total_on_hours: Optional[linopy.Variable] = None + self.switch_on: Optional[linopy.Variable] = None + self.switch_off: Optional[linopy.Variable] = None + self.consecutive_on_hours: Optional[linopy.Variable] = None + self.consecutive_off_hours: Optional[linopy.Variable] = None + + def create_variables_and_constraints(self): + # Use enhanced factory pattern + variables, constraints = ModelingPatterns.operational_binary_control_pattern( + model=self._model, + name=self.label_full, + controlled_variables=self._defining_variables, + variable_bounds=self._defining_bounds, + use_complement=self.parameters.use_off, + track_total_duration=True, + track_switches=self.parameters.use_switch_on, + previous_state=self._get_previous_state(), + duration_bounds=(self.parameters.on_hours_total_min, self.parameters.on_hours_total_max), + track_consecutive_on=self.parameters.use_consecutive_on_hours, + consecutive_on_bounds=(self.parameters.consecutive_on_hours_min, self.parameters.consecutive_on_hours_max), + previous_on_duration=self._get_previous_on_duration(), + track_consecutive_off=self.parameters.use_consecutive_off_hours, + consecutive_off_bounds=( + self.parameters.consecutive_off_hours_min, + self.parameters.consecutive_off_hours_max, + ), + previous_off_duration=self._get_previous_off_duration(), + ) + + # Register all variables + self.on = self.add(variables['on'], 'on') + if 'off' in variables: + self.off = self.add(variables['off'], 'off') + if 'total_duration' in variables: + self.total_on_hours = self.add(variables['total_duration'], 'total_duration') + if 'switch_on' in variables: + self.switch_on = self.add(variables['switch_on'], 'switch_on') + self.switch_off = self.add(variables['switch_off'], 'switch_off') + if 'consecutive_on_duration' in variables: + self.consecutive_on_hours = self.add(variables['consecutive_on_duration'], 'consecutive_on_hours') + if 'consecutive_off_duration' in variables: + self.consecutive_off_hours = self.add(variables['consecutive_off_duration'], 'consecutive_off_hours') + + # Register all constraints + for constraint_name, constraint in constraints.items(): + self.add(constraint, constraint_name) + + def _get_previous_on_duration(self): + """Calculate previous consecutive on duration""" + # Implementation based on _previous_values + return 0 # Placeholder + + def _get_previous_off_duration(self): + """Calculate previous consecutive off duration""" + # Implementation based on _previous_values + return 0 # Placeholder + + # Remove the old placeholder methods - no longer needed! class StateModel(Model): @@ -657,155 +1055,6 @@ def compute_consecutive_hours_in_state( return np.sum(binary_values[-nr_of_indexes_with_consecutive_ones:] * hours_per_timestep[-nr_of_indexes_with_consecutive_ones:]) -class OnOffModel(Model): - """ - Class for modeling the on and off state of a variable - Uses component models to create a modular implementation - """ - - def __init__( - self, - model: FlowSystemModel, - on_off_parameters: OnOffParameters, - label_of_element: str, - defining_variables: List[linopy.Variable], - defining_bounds: List[Tuple[TemporalData, TemporalData]], - previous_values: List[Optional[TemporalData]], - label: Optional[str] = None, - ): - """ - Constructor for OnOffModel - - Args: - model: Reference to the FlowSystemModel - on_off_parameters: Parameters for the OnOffModel - label_of_element: Label of the Parent - defining_variables: List of Variables that are used to define the OnOffModel - defining_bounds: List of Tuples, defining the absolute bounds of each defining variable - previous_values: List of previous values of the defining variables - label: Label of the OnOffModel - """ - super().__init__(model, label_of_element, label) - self.parameters = on_off_parameters - self._defining_variables = defining_variables - self._defining_bounds = defining_bounds - self._previous_values = previous_values - - self.state_model = None - self.switch_state_model = None - self.consecutive_on_model = None - self.consecutive_off_model = None - - def do_modeling(self): - """Create all variables and constraints for the OnOffModel""" - - # Create binary state component - self.state_model = StateModel( - model=self._model, - label_of_element=self.label_of_element, - defining_variables=self._defining_variables, - defining_bounds=self._defining_bounds, - previous_values=self._previous_values, - use_off=self.parameters.use_off, - on_hours_total_min=self.parameters.on_hours_total_min, - on_hours_total_max=self.parameters.on_hours_total_max, - effects_per_running_hour=self.parameters.effects_per_running_hour, - ) - self.add(self.state_model) - self.state_model.do_modeling() - - # Create switch component if needed - if self.parameters.use_switch_on: - self.switch_state_model = SwitchStateModel( - model=self._model, - label_of_element=self.label_of_element, - state_variable=self.state_model.on, - previous_state=self.state_model.previous_on_states[-1], - switch_on_max=self.parameters.switch_on_total_max, - ) - self.add(self.switch_state_model) - self.switch_state_model.do_modeling() - - # Create consecutive on hours component if needed - if self.parameters.use_consecutive_on_hours: - self.consecutive_on_model = ConsecutiveStateModel( - model=self._model, - label_of_element=self.label_of_element, - state_variable=self.state_model.on, - minimum_duration=self.parameters.consecutive_on_hours_min, - maximum_duration=self.parameters.consecutive_on_hours_max, - previous_states=self.state_model.previous_on_states, - label='ConsecutiveOn', - ) - self.add(self.consecutive_on_model) - self.consecutive_on_model.do_modeling() - - # Create consecutive off hours component if needed - if self.parameters.use_consecutive_off_hours: - self.consecutive_off_model = ConsecutiveStateModel( - model=self._model, - label_of_element=self.label_of_element, - state_variable=self.state_model.off, - minimum_duration=self.parameters.consecutive_off_hours_min, - maximum_duration=self.parameters.consecutive_off_hours_max, - previous_states=self.state_model.previous_off_states, - label='ConsecutiveOff', - ) - self.add(self.consecutive_off_model) - self.consecutive_off_model.do_modeling() - - self._create_shares() - - def _create_shares(self): - if self.parameters.effects_per_running_hour: - self._model.effects.add_share_to_effects( - name=self.label_of_element, - expressions={ - effect: self.state_model.on * factor * self._model.hours_per_step - for effect, factor in self.parameters.effects_per_running_hour.items() - }, - target='operation', - ) - - if self.parameters.effects_per_switch_on: - self._model.effects.add_share_to_effects( - name=self.label_of_element, - expressions={ - effect: self.switch_state_model.switch_on * factor - for effect, factor in self.parameters.effects_per_switch_on.items() - }, - target='operation', - ) - - @property - def on(self): - return self.state_model.on - - @property - def off(self): - return self.state_model.off - - @property - def switch_on(self): - return self.switch_state_model.switch_on - - @property - def switch_off(self): - return self.switch_state_model.switch_off - - @property - def switch_on_nr(self): - return self.switch_state_model.switch_on_nr - - @property - def consecutive_on_hours(self): - return self.consecutive_on_model.duration - - @property - def consecutive_off_hours(self): - return self.consecutive_off_model.duration - - class PieceModel(Model): """Class for modeling a linear piece of one or more variables in parallel""" diff --git a/flixopt/structure.py b/flixopt/structure.py index 9566e303f..fed5bed94 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -831,6 +831,27 @@ def all_sub_models(self) -> List['Model']: return [model for sub_model in self.sub_models for model in [sub_model] + sub_model.all_sub_models] +class BaseFeatureModel(Model): + """Minimal base class for feature models that use factory patterns""" + + def __init__(self, model: FlowSystemModel, label_of_element: str, parameters, label: Optional[str] = None): + super().__init__(model, label_of_element, label or self.__class__.__name__) + self.parameters = parameters + + def do_modeling(self): + """Template method - creates variables and constraints, then effects""" + self.create_variables_and_constraints() + self.add_effects() + + def create_variables_and_constraints(self): + """Override in subclasses to create variables and constraints""" + raise NotImplementedError('Subclasses must implement create_variables_and_constraints()') + + def add_effects(self): + """Override in subclasses to add effects""" + pass # Default: no effects + + class ElementModel(Model): """Stores the mathematical Variables and Constraints for Elements""" From 33460a0220d3d415113334433c552cbae2c23ab1 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 18 Jul 2025 11:02:50 +0200 Subject: [PATCH 198/448] Improve Feature Patterns --- flixopt/features.py | 452 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 353 insertions(+), 99 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index e495e2973..3986f7b49 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -17,6 +17,140 @@ logger = logging.getLogger('flixopt') +class ModelingUtilities: + """Utility functions for modeling calculations - used across different classes""" + + @staticmethod + def compute_consecutive_hours_in_state( + binary_values: TemporalData, hours_per_timestep: Union[int, float, np.ndarray] + ) -> Scalar: + """ + Computes the final consecutive duration in state 'on' (=1) in hours, from a binary array. + + Args: + binary_values: An int or 1D binary array containing only `0`s and `1`s. + hours_per_timestep: The duration of each timestep in hours. + If a scalar is provided, it is used for all timesteps. + If an array is provided, it must be as long as the last consecutive duration in binary_values. + + Returns: + The duration of the binary variable in hours. + + Raises + ------ + TypeError + If the length of binary_values and dt_in_hours is not equal, but None is a scalar. + """ + if np.isscalar(binary_values) and np.isscalar(hours_per_timestep): + return binary_values * hours_per_timestep + elif np.isscalar(binary_values) and not np.isscalar(hours_per_timestep): + return binary_values * hours_per_timestep[-1] + + if np.isclose(binary_values[-1], 0, atol=CONFIG.modeling.EPSILON): + return 0 + + if np.isscalar(hours_per_timestep): + hours_per_timestep = np.ones(len(binary_values)) * hours_per_timestep + hours_per_timestep: np.ndarray + + indexes_with_zero_values = np.where(np.isclose(binary_values, 0, atol=CONFIG.modeling.EPSILON))[0] + if len(indexes_with_zero_values) == 0: + nr_of_indexes_with_consecutive_ones = len(binary_values) + else: + nr_of_indexes_with_consecutive_ones = len(binary_values) - indexes_with_zero_values[-1] - 1 + + if len(hours_per_timestep) < nr_of_indexes_with_consecutive_ones: + raise ValueError( + f'When trying to calculate the consecutive duration, the length of the last duration ' + f'({nr_of_indexes_with_consecutive_ones}) is longer than the provided hours_per_timestep ({len(hours_per_timestep)}), ' + f'as {binary_values=}' + ) + + return np.sum( + binary_values[-nr_of_indexes_with_consecutive_ones:] + * hours_per_timestep[-nr_of_indexes_with_consecutive_ones:] + ) + + @staticmethod + def compute_previous_states(previous_values: List[TemporalData], epsilon: float = None) -> np.ndarray: + """ + Computes the previous states {0, 1} of defining variables as a binary array from their previous values. + + Args: + previous_values: List of previous values for variables + epsilon: Tolerance for zero detection (uses CONFIG.modeling.EPSILON if None) + + Returns: + Binary array of previous states + """ + if epsilon is None: + epsilon = CONFIG.modeling.EPSILON + + if not previous_values or all(val is None for val in previous_values): + return np.array([0]) + + # Convert to 2D-array and compute binary on/off states + previous_values = np.array([values for values in previous_values if values is not None]) # Filter out None + if previous_values.ndim > 1: + return np.any(~np.isclose(previous_values, 0, atol=epsilon), axis=0).astype(int) + + return (~np.isclose(previous_values, 0, atol=epsilon)).astype(int) + + @staticmethod + def compute_previous_on_duration(previous_values: List[TemporalData], hours_per_step: Union[int, float]) -> Scalar: + """ + Convenience method to compute previous consecutive 'on' duration. + + Args: + previous_values: List of previous values for variables + hours_per_step: Duration of each timestep in hours + + Returns: + Previous consecutive on duration in hours + """ + if not previous_values: + return 0 + + previous_states = ModelingUtilities.compute_previous_states(previous_values) + return ModelingUtilities.compute_consecutive_hours_in_state(previous_states, hours_per_step) + + @staticmethod + def compute_previous_off_duration(previous_values: List[TemporalData], hours_per_step: Union[int, float]) -> Scalar: + """ + Convenience method to compute previous consecutive 'off' duration. + + Args: + previous_values: List of previous values for variables + hours_per_step: Duration of each timestep in hours + + Returns: + Previous consecutive off duration in hours + """ + if not previous_values: + return 0 + + previous_states = ModelingUtilities.compute_previous_states(previous_values) + previous_off_states = 1 - previous_states + return ModelingUtilities.compute_consecutive_hours_in_state(previous_off_states, hours_per_step) + + @staticmethod + def get_most_recent_state(previous_values: List[TemporalData]) -> int: + """ + Get the most recent binary state from previous values. + + Args: + previous_values: List of previous values for variables + + Returns: + Most recent binary state (0 or 1) + """ + if not previous_values: + return 0 + + previous_states = ModelingUtilities.compute_previous_states(previous_values) + return int(previous_states[-1]) + + class ModelingPrimitives: """Mathematical modeling primitives returning (variables, constraints) tuples""" @@ -105,12 +239,15 @@ def expression_tracking_variable( """ coords = coords or ['year', 'scenario'] - if bounds: + if not bounds: + tracker = model.add_variables(name=f'{name}|tracker', coords=model.get_coords(coords)) + else: tracker = model.add_variables( - lower=bounds[0], upper=bounds[1], name=f'{name}|tracker', coords=model.get_coords(coords) + lower=bounds[0] if bounds[0] is not None else -np.inf, + upper=bounds[1] if bounds[1] is not None else np.inf, + name=f'{name}|tracker', + coords=model.get_coords(coords), ) - else: - tracker = model.add_variables(name=f'{name}|tracker', coords=model.get_coords(coords)) # Constraint: tracker = expression tracking = model.add_constraints(tracker == tracked_expression, name=f'{name}|tracking_eq') @@ -298,6 +435,49 @@ def consecutive_duration_tracking( return variables, constraints + @staticmethod + def mutual_exclusivity_constraint( + model: FlowSystemModel, name: str, binary_variables: List[linopy.Variable], tolerance: float = 1.1 + ) -> Tuple[Dict, Dict[str, linopy.Constraint]]: + """ + Creates mutual exclusivity constraint for binary variables. + + Mathematical formulation: + Σ(binary_vars[i]) ≤ tolerance ∀t + + Ensures at most one binary variable can be 1 at any time. + Tolerance > 1.0 accounts for binary variable numerical precision. + + Args: + binary_variables: List of binary variables that should be mutually exclusive + tolerance: Upper bound (typically 1.1 for numerical stability) + + Returns: + variables: {} (no new variables created) + constraints: {'mutual_exclusivity': constraint} + + Raises: + AssertionError: If fewer than 2 variables provided or variables aren't binary + """ + assert len(binary_variables) >= 2, ( + f'Mutual exclusivity requires at least 2 variables, got {len(binary_variables)}' + ) + + for var in binary_variables: + assert var.attrs.get('binary', False), ( + f'Variable {var.name} must be binary for mutual exclusivity constraint' + ) + + # Create mutual exclusivity constraint + mutual_exclusivity = model.add_constraints( + sum(binary_variables) <= tolerance, name=f'{name}|mutual_exclusivity' + ) + + variables = {} # No new variables created + constraints = {'mutual_exclusivity': mutual_exclusivity} + + return variables, constraints + class ModelingPatterns: """High-level patterns that compose primitives and return (variables, constraints) tuples""" @@ -362,68 +542,6 @@ def investment_sizing_pattern( return variables, constraints - @staticmethod - def operational_binary_control_pattern( - model: FlowSystemModel, - name: str, - controlled_variables: List[linopy.Variable], - variable_bounds: List[Tuple[TemporalData, TemporalData]], - use_complement: bool = False, - track_total_duration: bool = False, - track_switches: bool = False, - previous_state=0, - duration_bounds: Tuple[TemporalData, TemporalData] = None, - ) -> Tuple[Dict[str, linopy.Variable], Dict[str, linopy.Constraint]]: - """ - Operational binary control with optional features. - - Returns: - variables: {'on': binary_var, 'off': binary_var (optional), 'total_duration': var (optional), ...} - constraints: {'complementary': constraint, 'control_0_lower': constraint, ...} - """ - variables = {} - constraints = {} - - # Main binary state - if use_complement: - state_vars, state_constraints = ModelingPrimitives.binary_state_pair(model, name) - variables.update(state_vars) - constraints.update(state_constraints) - else: - variables['on'] = model.add_variables(binary=True, name=f'{name}|on', coords=model.get_coords(['time'])) - - # Control variables with binary state - for i, (var, (lower_bound, upper_bound)) in enumerate(zip(controlled_variables, variable_bounds)): - # Lower bound constraint - constraints[f'control_{i}_lower'] = model.add_constraints( - variables['on'] * max(CONFIG.modeling.EPSILON, lower_bound) <= var, name=f'{name}|control_{i}_lower' - ) - # Upper bound constraint - constraints[f'control_{i}_upper'] = model.add_constraints( - var <= variables['on'] * upper_bound, name=f'{name}|control_{i}_upper' - ) - - # Total duration tracking - if track_total_duration: - duration_expr = (variables['on'] * model.hours_per_step).sum('time') - duration_vars, duration_constraints = ModelingPrimitives.expression_tracking_variable( - model, f'{name}|duration', duration_expr, duration_bounds - ) - variables['total_duration'] = duration_vars['tracker'] - constraints['duration_tracking'] = duration_constraints['tracking'] - - # Switch tracking - if track_switches: - switch_vars, switch_constraints = ModelingPrimitives.state_transition_variables( - model, f'{name}|switches', variables['on'], previous_state - ) - variables.update(switch_vars) - # Add switch constraints with prefixed names - for switch_name, switch_constraint in switch_constraints.items(): - constraints[f'switch_{switch_name}'] = switch_constraint - - return variables, constraints - @staticmethod def operational_binary_control_pattern( model: FlowSystemModel, @@ -467,7 +585,7 @@ def operational_binary_control_pattern( # Control variables (existing logic) for i, (var, (lower_bound, upper_bound)) in enumerate(zip(controlled_variables, variable_bounds)): constraints[f'control_{i}_lower'] = model.add_constraints( - variables['on'] * max(CONFIG.modeling.EPSILON, lower_bound) <= var, name=f'{name}|control_{i}_lower' + variables['on'] * np.maximum(lower_bound, CONFIG.modeling.EPSILON) <= var, name=f'{name}|control_{i}_lower' ) constraints[f'control_{i}_upper'] = model.add_constraints( var <= variables['on'] * upper_bound, name=f'{name}|control_{i}_upper' @@ -525,8 +643,30 @@ def operational_binary_control_pattern( class InvestmentModel(BaseFeatureModel): - def create_variables(self): - # Clean tuple unpacking + """Investment model using factory patterns but keeping old interface""" + + def __init__( + self, + model: FlowSystemModel, + label_of_element: str, + parameters: InvestParameters, + defining_variable: linopy.Variable, + relative_bounds_of_defining_variable: Tuple[TemporalData, TemporalData], + label: Optional[str] = None, + on_variable: Optional[linopy.Variable] = None, + ): + super().__init__(model, label_of_element, parameters, label) + + self._defining_variable = defining_variable + self._relative_bounds_of_defining_variable = relative_bounds_of_defining_variable + self._on_variable = on_variable + + # Only keep non-variable attributes + self.scenario_of_investment: Optional[linopy.Variable] = None + self.piecewise_effects: Optional[PiecewiseEffectsModel] = None + + def create_variables_and_constraints(self): + # Use factory patterns variables, constraints = ModelingPatterns.investment_sizing_pattern( model=self._model, name=self.label_full, @@ -539,15 +679,76 @@ def create_variables(self): optional=self.parameters.optional, ) - # Register variables - self.size = self.add(variables['size'], 'size') + # Register variables (stored in Model's variable tracking) + self.add(variables['size'], 'size') if 'is_invested' in variables: - self.is_invested = self.add(variables['is_invested'], 'is_invested') + self.add(variables['is_invested'], 'is_invested') - # Register all constraints + # Register constraints for constraint_name, constraint in constraints.items(): self.add(constraint, constraint_name) + # Handle scenarios and piecewise effects... + if self._model.flow_system.scenarios is not None: + self._create_bounds_for_scenarios() + + if self.parameters.piecewise_effects: + self.piecewise_effects = self.add( + PiecewiseEffectsModel( + model=self._model, + label_of_element=self.label_of_element, + piecewise_origin=(self.size.name, self.parameters.piecewise_effects.piecewise_origin), + piecewise_shares=self.parameters.piecewise_effects.piecewise_shares, + zero_point=self.is_invested, + ), + 'segments', + ) + self.piecewise_effects.do_modeling() + + # Properties access variables from Model's tracking system + @property + def size(self) -> Optional[linopy.Variable]: + """Investment size variable""" + return self.get_variable_by_short_name('size') + + @property + def is_invested(self) -> Optional[linopy.Variable]: + """Binary investment decision variable""" + return self.get_variable_by_short_name('is_invested') + + def add_effects(self): + """Add investment effects""" + if self.parameters.fix_effects: + self._model.effects.add_share_to_effects( + name=self.label_of_element, + expressions={ + effect: self.is_invested * factor if self.is_invested is not None else factor + for effect, factor in self.parameters.fix_effects.items() + }, + target='invest', + ) + + if self.parameters.divest_effects and self.parameters.optional: + self._model.effects.add_share_to_effects( + name=self.label_of_element, + expressions={ + effect: -self.is_invested * factor + factor + for effect, factor in self.parameters.divest_effects.items() + }, + target='invest', + ) + + if self.parameters.specific_effects: + self._model.effects.add_share_to_effects( + name=self.label_of_element, + expressions={effect: self.size * factor for effect, factor in self.parameters.specific_effects.items()}, + target='invest', + ) + + def _create_bounds_for_scenarios(self): + """Keep existing scenario logic""" + pass + class OnOffModel(BaseFeatureModel): """OnOff model using factory patterns""" @@ -555,8 +756,8 @@ class OnOffModel(BaseFeatureModel): def __init__( self, model: FlowSystemModel, - on_off_parameters: OnOffParameters, label_of_element: str, + on_off_parameters: OnOffParameters, defining_variables: List[linopy.Variable], defining_bounds: List[Tuple[TemporalData, TemporalData]], previous_values: List[Optional[TemporalData]], @@ -568,17 +769,8 @@ def __init__( self._defining_bounds = defining_bounds self._previous_values = previous_values - # All variables set by factory - self.on: Optional[linopy.Variable] = None - self.off: Optional[linopy.Variable] = None - self.total_on_hours: Optional[linopy.Variable] = None - self.switch_on: Optional[linopy.Variable] = None - self.switch_off: Optional[linopy.Variable] = None - self.consecutive_on_hours: Optional[linopy.Variable] = None - self.consecutive_off_hours: Optional[linopy.Variable] = None - def create_variables_and_constraints(self): - # Use enhanced factory pattern + # Use factory patterns variables, constraints = ModelingPatterns.operational_binary_control_pattern( model=self._model, name=self.label_full, @@ -600,35 +792,97 @@ def create_variables_and_constraints(self): previous_off_duration=self._get_previous_off_duration(), ) - # Register all variables - self.on = self.add(variables['on'], 'on') + # Register all variables (stored in Model's variable tracking) + self.add(variables['on'], 'on') if 'off' in variables: - self.off = self.add(variables['off'], 'off') + self.add(variables['off'], 'off') if 'total_duration' in variables: - self.total_on_hours = self.add(variables['total_duration'], 'total_duration') + self.add(variables['total_duration'], 'total_duration') if 'switch_on' in variables: - self.switch_on = self.add(variables['switch_on'], 'switch_on') - self.switch_off = self.add(variables['switch_off'], 'switch_off') + self.add(variables['switch_on'], 'switch_on') + self.add(variables['switch_off'], 'switch_off') if 'consecutive_on_duration' in variables: - self.consecutive_on_hours = self.add(variables['consecutive_on_duration'], 'consecutive_on_hours') + self.add(variables['consecutive_on_duration'], 'consecutive_on_hours') if 'consecutive_off_duration' in variables: - self.consecutive_off_hours = self.add(variables['consecutive_off_duration'], 'consecutive_off_hours') + self.add(variables['consecutive_off_duration'], 'consecutive_off_hours') # Register all constraints for constraint_name, constraint in constraints.items(): self.add(constraint, constraint_name) + # Properties access variables from Model's tracking system + @property + def on(self) -> Optional[linopy.Variable]: + """Binary on state variable""" + return self.get_variable_by_short_name('on') + + @property + def off(self) -> Optional[linopy.Variable]: + """Binary off state variable""" + return self.get_variable_by_short_name('off') + + @property + def total_on_hours(self) -> Optional[linopy.Variable]: + """Total on hours variable""" + return self.get_variable_by_short_name('total_duration') + + @property + def switch_on(self) -> Optional[linopy.Variable]: + """Switch on variable""" + return self.get_variable_by_short_name('switch_on') + + @property + def switch_off(self) -> Optional[linopy.Variable]: + """Switch off variable""" + return self.get_variable_by_short_name('switch_off') + + @property + def switch_on_nr(self) -> Optional[linopy.Variable]: + """Number of switch-ons variable""" + # This could be added to factory if needed + return None + + @property + def consecutive_on_hours(self) -> Optional[linopy.Variable]: + """Consecutive on hours variable""" + return self.get_variable_by_short_name('consecutive_on_hours') + + @property + def consecutive_off_hours(self) -> Optional[linopy.Variable]: + """Consecutive off hours variable""" + return self.get_variable_by_short_name('consecutive_off_hours') + + def add_effects(self): + """Add operational effects""" + if self.parameters.effects_per_running_hour: + self._model.effects.add_share_to_effects( + name=self.label_of_element, + expressions={ + effect: self.on * factor * self._model.hours_per_step + for effect, factor in self.parameters.effects_per_running_hour.items() + }, + target='operation', + ) + + if self.parameters.effects_per_switch_on and self.switch_on: + self._model.effects.add_share_to_effects( + name=self.label_of_element, + expressions={ + effect: self.switch_on * factor for effect, factor in self.parameters.effects_per_switch_on.items() + }, + target='operation', + ) + def _get_previous_on_duration(self): - """Calculate previous consecutive on duration""" - # Implementation based on _previous_values - return 0 # Placeholder + hours_per_step = self._model.hours_per_step.isel(time=0).values.flatten()[0] + return ModelingUtilities.compute_previous_on_duration(self._previous_values, hours_per_step) def _get_previous_off_duration(self): - """Calculate previous consecutive off duration""" - # Implementation based on _previous_values - return 0 # Placeholder + hours_per_step = self._model.hours_per_step.isel(time=0).values.flatten()[0] + return ModelingUtilities.compute_previous_off_duration(self._previous_values, hours_per_step) - # Remove the old placeholder methods - no longer needed! + def _get_previous_state(self): + return ModelingUtilities.get_most_recent_state(self._previous_values) class StateModel(Model): From ff70674ac7425f7b3a1ecb6e0ae2cac8599332cf Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 18 Jul 2025 11:18:00 +0200 Subject: [PATCH 199/448] Improve acess to variables via short names --- flixopt/structure.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/flixopt/structure.py b/flixopt/structure.py index fed5bed94..5a4b016ce 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -739,10 +739,10 @@ def add( # TODO: Check uniquenes of short names if isinstance(item, linopy.Variable): self._variables_direct.append(item.name) - self._variables_short[item.name] = short_name or item.name + self._variables_short[short_name] = item.name elif isinstance(item, linopy.Constraint): self._constraints_direct.append(item.name) - self._constraints_short[item.name] = short_name or item.name + self._constraints_short[short_name] = item.name elif isinstance(item, Model): self.sub_models.append(item) self._sub_models_short[item.label_full] = short_name or item.label_full @@ -830,6 +830,18 @@ def constraints(self) -> linopy.Constraints: def all_sub_models(self) -> List['Model']: return [model for sub_model in self.sub_models for model in [sub_model] + sub_model.all_sub_models] + def get_variable_by_short_name(self, short_name: str, default_return = None) -> Optional[linopy.Variable]: + """Get variable by short name""" + if short_name not in self._variables_short: + return default_return + return self._model.variables[self._variables_short.get(short_name)] + + def get_constraint_by_short_name(self, short_name: str, default_return = None) -> Optional[linopy.Constraint]: + """Get variable by short name""" + if short_name not in self._constraints_short: + return default_return + return self._model.constraints[self._constraints_short.get(short_name)] + class BaseFeatureModel(Model): """Minimal base class for feature models that use factory patterns""" From fa5e30a11e11d6a564228ce6a1408b2613d6aed5 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 18 Jul 2025 11:52:35 +0200 Subject: [PATCH 200/448] Improve --- flixopt/effects.py | 6 +- flixopt/elements.py | 96 ++-- flixopt/features.py | 1193 +++--------------------------------------- flixopt/modeling.py | 636 ++++++++++++++++++++++ flixopt/structure.py | 31 +- 5 files changed, 777 insertions(+), 1185 deletions(-) create mode 100644 flixopt/modeling.py diff --git a/flixopt/effects.py b/flixopt/effects.py index 23943d16b..2fc2aae37 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -147,8 +147,7 @@ def __init__(self, model: FlowSystemModel, element: Effect): model=self._model, dims=('year', 'scenario'), label_of_element=self.label_of_element, - label='invest', - label_full=f'{self.label_full}(invest)', + label_of_model=f'{self.label_of_model}(invest)', total_max=self.element.maximum_invest, total_min=self.element.minimum_invest, ) @@ -159,8 +158,7 @@ def __init__(self, model: FlowSystemModel, element: Effect): model=self._model, dims=('time', 'year', 'scenario'), label_of_element=self.label_of_element, - label='operation', - label_full=f'{self.label_full}(operation)', + label_of_model=f'{self.label_of_model}|(operation)', total_max=self.element.maximum_operation, total_min=self.element.minimum_operation, min_per_hour=self.element.minimum_operation_per_hour diff --git a/flixopt/elements.py b/flixopt/elements.py index a546b5e9c..43907b07a 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -12,7 +12,7 @@ from .config import CONFIG from .core import PlausibilityError, Scalar, TemporalData, TemporalDataUser from .effects import TemporalEffectsUser -from .features import InvestmentModel, OnOffModel, PreventSimultaneousUsageModel +from .features import InvestmentModel, OnOffModel, PreventSimultaneousUsageModel, ModelingPatterns, ModelingPrimitives from .interface import InvestParameters, OnOffParameters from .structure import Element, ElementModel, FlowSystemModel, register_class_for_io @@ -311,15 +311,14 @@ class FlowModel(ElementModel): def __init__(self, model: FlowSystemModel, element: Flow): super().__init__(model, element) self.element: Flow = element - self.flow_rate: Optional[linopy.Variable] = None - self.total_flow_hours: Optional[linopy.Variable] = None + # Feature models (set by do_modeling) self.on_off: Optional[OnOffModel] = None self._investment: Optional[InvestmentModel] = None def do_modeling(self): - # eq relative_minimum(t) * size <= flow_rate(t) <= relative_maximum(t) * size - self.flow_rate: linopy.Variable = self.add( + # Main flow rate variable + self.add( self._model.add_variables( lower=self.flow_rate_lower_bound, upper=self.flow_rate_upper_bound, @@ -329,7 +328,7 @@ def do_modeling(self): 'flow_rate', ) - # OnOff + # OnOff feature if self.element.on_off_parameters is not None: self.on_off: OnOffModel = self.add( OnOffModel( @@ -344,46 +343,57 @@ def do_modeling(self): ) self.on_off.do_modeling() - # Investment + # Investment feature if isinstance(self.element.size, InvestParameters): self._investment: InvestmentModel = self.add( InvestmentModel( model=self._model, label_of_element=self.label_of_element, + label_of_model=self.label_of_element, parameters=self.element.size, defining_variable=self.flow_rate, - relative_bounds_of_defining_variable=(self.flow_rate_lower_bound_relative, - self.flow_rate_upper_bound_relative), + relative_bounds_of_defining_variable=( + self.flow_rate_lower_bound_relative, + self.flow_rate_upper_bound_relative, + ), on_variable=self.on_off.on if self.on_off is not None else None, ), 'investment', ) self._investment.do_modeling() - self.total_flow_hours = self.add( - self._model.add_variables( - lower=self.element.flow_hours_total_min if self.element.flow_hours_total_min is not None else 0, - upper=self.element.flow_hours_total_max if self.element.flow_hours_total_max is not None else np.inf, - coords=self._model.get_coords(['year', 'scenario']), - name=f'{self.label_full}|total_flow_hours', + # Total flow hours tracking (could use factory pattern) + variables, constraints = ModelingPrimitives.expression_tracking_variable( + model=self._model, + name=f'{self.label_full}|total_flow_hours', + tracked_expression=(self.flow_rate * self._model.hours_per_step).sum('time'), + bounds=( + self.element.flow_hours_total_min if self.element.flow_hours_total_min is not None else 0, + self.element.flow_hours_total_max if self.element.flow_hours_total_max is not None else None, ), - 'total_flow_hours', + coords=['year', 'scenario'], ) - self.add( - self._model.add_constraints( - self.total_flow_hours == (self.flow_rate * self._model.hours_per_step).sum('time'), - name=f'{self.label_full}|total_flow_hours', - ), - 'total_flow_hours', - ) + self.add(variables['tracker'], 'total_flow_hours') + self.add(constraints['tracking'], 'total_flow_hours_tracking') - # Load factor + # Load factor constraints self._create_bounds_for_load_factor() - # Shares + # Effects self._create_shares() + # Properties for clean access to variables + @property + def flow_rate(self) -> Optional[linopy.Variable]: + """Main flow rate variable""" + return self.get_variable_by_short_name('flow_rate') + + @property + def total_flow_hours(self) -> Optional[linopy.Variable]: + """Total flow hours variable""" + return self.get_variable_by_short_name('total_flow_hours') + def results_structure(self): return { **super().results_structure(), @@ -393,10 +403,10 @@ def results_structure(self): } def _create_shares(self): - # Arbeitskosten: - if self.element.effects_per_flow_hour != {}: + # Effects per flow hour + if self.element.effects_per_flow_hour: self._model.effects.add_share_to_effects( - name=self.label_full, # Use the full label of the element + name=self.label_full, expressions={ effect: self.flow_rate * self._model.hours_per_step * factor for effect, factor in self.element.effects_per_flow_hour.items() @@ -405,39 +415,35 @@ def _create_shares(self): ) def _create_bounds_for_load_factor(self): - # TODO: Add Variable load_factor for better evaluation? + """Create load factor constraints using current approach""" + # Get the size (either from element or investment) + size = self.element.size if self._investment is None else self._investment.size - # eq: var_sumFlowHours <= size * dt_tot * load_factor_max + # Maximum load factor constraint if self.element.load_factor_max is not None: - name_short = 'load_factor_max' flow_hours_per_size_max = self._model.hours_per_step.sum('time') * self.element.load_factor_max - size = self.element.size if self._investment is None else self._investment.size - self.add( self._model.add_constraints( self.total_flow_hours <= size * flow_hours_per_size_max, - name=f'{self.label_full}|{name_short}', + name=f'{self.label_full}|load_factor_max', ), - name_short, + 'load_factor_max', ) - # eq: size * sum(dt)* load_factor_min <= var_sumFlowHours + # Minimum load factor constraint if self.element.load_factor_min is not None: - name_short = 'load_factor_min' flow_hours_per_size_min = self._model.hours_per_step.sum('time') * self.element.load_factor_min - size = self.element.size if self._investment is None else self._investment.size - self.add( self._model.add_constraints( self.total_flow_hours >= size * flow_hours_per_size_min, - name=f'{self.label_full}|{name_short}', + name=f'{self.label_full}|load_factor_min', ), - name_short, + 'load_factor_min', ) @property def flow_rate_bounds_on(self) -> Tuple[TemporalData, TemporalData]: - """Returns absolute flow rate bounds. Important for OnOffModel""" + """Returns absolute flow rate bounds for OnOffModel""" relative_minimum, relative_maximum = self.flow_rate_lower_bound_relative, self.flow_rate_upper_bound_relative size = self.element.size if not isinstance(size, InvestParameters): @@ -458,7 +464,7 @@ def flow_rate_lower_bound_relative(self) -> TemporalData: @property def flow_rate_upper_bound_relative(self) -> TemporalData: - """ Returns the upper bound of the flow_rate relative to its size""" + """Returns the upper bound of the flow_rate relative to its size""" fixed_profile = self.element.fixed_relative_profile if fixed_profile is None: return self.element.relative_maximum @@ -566,8 +572,8 @@ def do_modeling(self): self.on_off = self.add( OnOffModel( self._model, - self.element.on_off_parameters, - self.label_of_element, + on_off_parameters=self.element.on_off_parameters, + label_of_element=self.label_of_element, defining_variables=[flow.model.flow_rate for flow in all_flows], defining_bounds=[flow.model.flow_rate_bounds_on for flow in all_flows], previous_values=[flow.previous_flow_rate for flow in all_flows], diff --git a/flixopt/features.py b/flixopt/features.py index 3986f7b49..e08d94cb1 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -11,637 +11,13 @@ from .config import CONFIG from .core import NonTemporalData, Scalar, TemporalData, FlowSystemDimensions -from .interface import InvestParameters, OnOffParameters, Piecewise +from .interface import InvestParameters, OnOffParameters, Piecewise, PiecewiseEffects from .structure import Model, FlowSystemModel, BaseFeatureModel +from .modeling import ModelingPatterns, ModelingUtilities, ModelingPrimitives logger = logging.getLogger('flixopt') -class ModelingUtilities: - """Utility functions for modeling calculations - used across different classes""" - - @staticmethod - def compute_consecutive_hours_in_state( - binary_values: TemporalData, hours_per_timestep: Union[int, float, np.ndarray] - ) -> Scalar: - """ - Computes the final consecutive duration in state 'on' (=1) in hours, from a binary array. - - Args: - binary_values: An int or 1D binary array containing only `0`s and `1`s. - hours_per_timestep: The duration of each timestep in hours. - If a scalar is provided, it is used for all timesteps. - If an array is provided, it must be as long as the last consecutive duration in binary_values. - - Returns: - The duration of the binary variable in hours. - - Raises - ------ - TypeError - If the length of binary_values and dt_in_hours is not equal, but None is a scalar. - """ - if np.isscalar(binary_values) and np.isscalar(hours_per_timestep): - return binary_values * hours_per_timestep - elif np.isscalar(binary_values) and not np.isscalar(hours_per_timestep): - return binary_values * hours_per_timestep[-1] - - if np.isclose(binary_values[-1], 0, atol=CONFIG.modeling.EPSILON): - return 0 - - if np.isscalar(hours_per_timestep): - hours_per_timestep = np.ones(len(binary_values)) * hours_per_timestep - hours_per_timestep: np.ndarray - - indexes_with_zero_values = np.where(np.isclose(binary_values, 0, atol=CONFIG.modeling.EPSILON))[0] - if len(indexes_with_zero_values) == 0: - nr_of_indexes_with_consecutive_ones = len(binary_values) - else: - nr_of_indexes_with_consecutive_ones = len(binary_values) - indexes_with_zero_values[-1] - 1 - - if len(hours_per_timestep) < nr_of_indexes_with_consecutive_ones: - raise ValueError( - f'When trying to calculate the consecutive duration, the length of the last duration ' - f'({nr_of_indexes_with_consecutive_ones}) is longer than the provided hours_per_timestep ({len(hours_per_timestep)}), ' - f'as {binary_values=}' - ) - - return np.sum( - binary_values[-nr_of_indexes_with_consecutive_ones:] - * hours_per_timestep[-nr_of_indexes_with_consecutive_ones:] - ) - - @staticmethod - def compute_previous_states(previous_values: List[TemporalData], epsilon: float = None) -> np.ndarray: - """ - Computes the previous states {0, 1} of defining variables as a binary array from their previous values. - - Args: - previous_values: List of previous values for variables - epsilon: Tolerance for zero detection (uses CONFIG.modeling.EPSILON if None) - - Returns: - Binary array of previous states - """ - if epsilon is None: - epsilon = CONFIG.modeling.EPSILON - - if not previous_values or all(val is None for val in previous_values): - return np.array([0]) - - # Convert to 2D-array and compute binary on/off states - previous_values = np.array([values for values in previous_values if values is not None]) # Filter out None - if previous_values.ndim > 1: - return np.any(~np.isclose(previous_values, 0, atol=epsilon), axis=0).astype(int) - - return (~np.isclose(previous_values, 0, atol=epsilon)).astype(int) - - @staticmethod - def compute_previous_on_duration(previous_values: List[TemporalData], hours_per_step: Union[int, float]) -> Scalar: - """ - Convenience method to compute previous consecutive 'on' duration. - - Args: - previous_values: List of previous values for variables - hours_per_step: Duration of each timestep in hours - - Returns: - Previous consecutive on duration in hours - """ - if not previous_values: - return 0 - - previous_states = ModelingUtilities.compute_previous_states(previous_values) - return ModelingUtilities.compute_consecutive_hours_in_state(previous_states, hours_per_step) - - @staticmethod - def compute_previous_off_duration(previous_values: List[TemporalData], hours_per_step: Union[int, float]) -> Scalar: - """ - Convenience method to compute previous consecutive 'off' duration. - - Args: - previous_values: List of previous values for variables - hours_per_step: Duration of each timestep in hours - - Returns: - Previous consecutive off duration in hours - """ - if not previous_values: - return 0 - - previous_states = ModelingUtilities.compute_previous_states(previous_values) - previous_off_states = 1 - previous_states - return ModelingUtilities.compute_consecutive_hours_in_state(previous_off_states, hours_per_step) - - @staticmethod - def get_most_recent_state(previous_values: List[TemporalData]) -> int: - """ - Get the most recent binary state from previous values. - - Args: - previous_values: List of previous values for variables - - Returns: - Most recent binary state (0 or 1) - """ - if not previous_values: - return 0 - - previous_states = ModelingUtilities.compute_previous_states(previous_values) - return int(previous_states[-1]) - - -class ModelingPrimitives: - """Mathematical modeling primitives returning (variables, constraints) tuples""" - - @staticmethod - def binary_state_pair( - model: FlowSystemModel, name: str, coords: List[str] = None - ) -> Tuple[Dict[str, linopy.Variable], Dict[str, linopy.Constraint]]: - """ - Creates complementary binary variables with completeness constraint. - - Mathematical formulation: - on[t] + off[t] = 1 ∀t - on[t], off[t] ∈ {0, 1} - - Returns: - variables: {'on': binary_var, 'off': binary_var} - constraints: {'complementary': constraint} - """ - coords = coords or ['time'] - - on = model.add_variables(binary=True, name=f'{name}|on', coords=model.get_coords(coords)) - off = model.add_variables(binary=True, name=f'{name}|off', coords=model.get_coords(coords)) - - # Constraint: on + off = 1 - complementary = model.add_constraints(on + off == 1, name=f'{name}|complementary') - - variables = {'on': on, 'off': off} - constraints = {'complementary': complementary} - - return variables, constraints - - @staticmethod - def proportionally_bounded_variable( - model: FlowSystemModel, - name: str, - controlling_variable, - bounds: Tuple[TemporalData, TemporalData], - coords: List[str] = None, - ) -> Tuple[Dict[str, linopy.Variable], Dict[str, linopy.Constraint]]: - """ - Creates variable with bounds proportional to another variable. - - Mathematical formulation: - lower_factor[t] * controller[t] ≤ variable[t] ≤ upper_factor[t] * controller[t] ∀t - - Returns: - variables: {'variable': bounded_var} - constraints: {'lower_bound': constraint, 'upper_bound': constraint} - """ - coords = coords or ['time'] - variable = model.add_variables(name=f'{name}|bounded', coords=model.get_coords(coords)) - - lower_factor, upper_factor = bounds - - # Constraints: lower_factor * controller ≤ var ≤ upper_factor * controller - lower_bound = model.add_constraints( - variable >= controlling_variable * lower_factor, name=f'{name}|proportional_lb' - ) - upper_bound = model.add_constraints( - variable <= controlling_variable * upper_factor, name=f'{name}|proportional_ub' - ) - - variables = {'variable': variable} - constraints = {'lower_bound': lower_bound, 'upper_bound': upper_bound} - - return variables, constraints - - @staticmethod - def expression_tracking_variable( - model: FlowSystemModel, - name: str, - tracked_expression, - bounds: Tuple[TemporalData, TemporalData] = None, - coords: List[str] = None, - ) -> Tuple[Dict[str, linopy.Variable], Dict[str, linopy.Constraint]]: - """ - Creates variable that equals a given expression. - - Mathematical formulation: - tracker = expression - lower ≤ tracker ≤ upper (if bounds provided) - - Returns: - variables: {'tracker': tracker_var} - constraints: {'tracking': constraint} - """ - coords = coords or ['year', 'scenario'] - - if not bounds: - tracker = model.add_variables(name=f'{name}|tracker', coords=model.get_coords(coords)) - else: - tracker = model.add_variables( - lower=bounds[0] if bounds[0] is not None else -np.inf, - upper=bounds[1] if bounds[1] is not None else np.inf, - name=f'{name}|tracker', - coords=model.get_coords(coords), - ) - - # Constraint: tracker = expression - tracking = model.add_constraints(tracker == tracked_expression, name=f'{name}|tracking_eq') - - variables = {'tracker': tracker} - constraints = {'tracking': tracking} - - return variables, constraints - - @staticmethod - def state_transition_variables( - model: FlowSystemModel, name: str, state_variable, previous_state=0 - ) -> Tuple[Dict[str, linopy.Variable], Dict[str, linopy.Constraint]]: - """ - Creates switch-on/off variables with state transition logic. - - Mathematical formulation: - switch_on[t] - switch_off[t] = state[t] - state[t-1] ∀t > 0 - switch_on[0] - switch_off[0] = state[0] - previous_state - switch_on[t] + switch_off[t] ≤ 1 ∀t - switch_on[t], switch_off[t] ∈ {0, 1} - - Returns: - variables: {'switch_on': binary_var, 'switch_off': binary_var} - constraints: {'transition': constraint, 'initial': constraint, 'mutex': constraint} - """ - switch_on = model.add_variables(binary=True, name=f'{name}|switch_on', coords=model.get_coords(['time'])) - switch_off = model.add_variables(binary=True, name=f'{name}|switch_off', coords=model.get_coords(['time'])) - - # State transition constraints for t > 0 - transition = model.add_constraints( - switch_on.isel(time=slice(1, None)) - switch_off.isel(time=slice(1, None)) - == state_variable.isel(time=slice(1, None)) - state_variable.isel(time=slice(None, -1)), - name=f'{name}|state_transition', - ) - - # Initial state transition for t = 0 - initial = model.add_constraints( - switch_on.isel(time=0) - switch_off.isel(time=0) == state_variable.isel(time=0) - previous_state, - name=f'{name}|initial_transition', - ) - - # At most one switch per timestep - mutex = model.add_constraints(switch_on + switch_off <= 1, name=f'{name}|switch_mutex') - - variables = {'switch_on': switch_on, 'switch_off': switch_off} - constraints = {'transition': transition, 'initial': initial, 'mutex': mutex} - - return variables, constraints - - @staticmethod - def big_m_binary_bounds( - model: FlowSystemModel, - name: str, - variable, - binary_control, - size_variable, - relative_bounds: Tuple[TemporalData, TemporalData], - ) -> Tuple[Dict, Dict[str, linopy.Constraint]]: - """ - Creates bounds controlled by both binary and continuous variables. - - Mathematical formulation: - variable[t] ≤ size[t] * upper_factor[t] ∀t - - If binary_control provided: - variable[t] ≥ M * (binary[t] - 1) + size[t] * lower_factor[t] ∀t - where M = max(size) * max(upper_factor) - Else: - variable[t] ≥ size[t] * lower_factor[t] ∀t - - Returns: - variables: {} (no new variables created) - constraints: {'upper_bound': constraint, 'lower_bound': constraint} - """ - rel_lower, rel_upper = relative_bounds - - # Upper bound: variable ≤ size * upper_factor - upper_bound = model.add_constraints(variable <= size_variable * rel_upper, name=f'{name}|size_upper_bound') - - if binary_control is not None: - # Big-M lower bound: variable ≥ M*(binary-1) + size*lower_factor - big_m = size_variable.max() * rel_upper.max() # Conservative big-M - lower_bound = model.add_constraints( - variable >= big_m * (binary_control - 1) + size_variable * rel_lower, - name=f'{name}|binary_controlled_lower_bound', - ) - else: - # Simple lower bound: variable ≥ size * lower_factor - lower_bound = model.add_constraints(variable >= size_variable * rel_lower, name=f'{name}|size_lower_bound') - - variables = {} # No new variables created - constraints = {'upper_bound': upper_bound, 'lower_bound': lower_bound} - - return variables, constraints - - @staticmethod - def consecutive_duration_tracking( - model: FlowSystemModel, - name: str, - state_variable: linopy.Variable, - minimum_duration: Optional[TemporalData] = None, - maximum_duration: Optional[TemporalData] = None, - previous_duration: TemporalData = 0, - ) -> Tuple[Dict[str, linopy.Variable], Dict[str, linopy.Constraint]]: - """ - Creates consecutive duration tracking for a binary state variable. - - Mathematical formulation: - duration[t] ≤ state[t] * M ∀t - duration[t+1] ≤ duration[t] + hours_per_step[t] ∀t - duration[t+1] ≥ duration[t] + hours_per_step[t] + (state[t+1] - 1) * M ∀t - duration[0] = (hours_per_step[0] + previous_duration) * state[0] - - If minimum_duration provided: - duration[t] ≥ (state[t-1] - state[t]) * minimum_duration[t-1] ∀t > 0 - - Args: - state_variable: Binary state variable to track duration for - minimum_duration: Optional minimum consecutive duration - maximum_duration: Optional maximum consecutive duration - previous_duration: Duration from before first timestep - - Returns: - variables: {'duration': duration_var} - constraints: {'upper_bound': constraint, 'forward': constraint, 'backward': constraint, ...} - """ - hours_per_step = model.hours_per_step - mega = hours_per_step.sum('time') + previous_duration # Big-M value - - # Duration variable - duration = model.add_variables( - lower=0, - upper=maximum_duration if maximum_duration is not None else mega, - coords=model.get_coords(['time']), - name=f'{name}|duration', - ) - - constraints = {} - - # Upper bound: duration[t] ≤ state[t] * M - constraints['upper_bound'] = model.add_constraints( - duration <= state_variable * mega, name=f'{name}|duration_upper_bound' - ) - - # Forward constraint: duration[t+1] ≤ duration[t] + hours_per_step[t] - constraints['forward'] = model.add_constraints( - duration.isel(time=slice(1, None)) - <= duration.isel(time=slice(None, -1)) + hours_per_step.isel(time=slice(None, -1)), - name=f'{name}|duration_forward', - ) - - # Backward constraint: duration[t+1] ≥ duration[t] + hours_per_step[t] + (state[t+1] - 1) * M - constraints['backward'] = model.add_constraints( - duration.isel(time=slice(1, None)) - >= duration.isel(time=slice(None, -1)) - + hours_per_step.isel(time=slice(None, -1)) - + (state_variable.isel(time=slice(1, None)) - 1) * mega, - name=f'{name}|duration_backward', - ) - - # Initial condition: duration[0] = (hours_per_step[0] + previous_duration) * state[0] - constraints['initial'] = model.add_constraints( - duration.isel(time=0) - == (hours_per_step.isel(time=0) + previous_duration) * state_variable.isel(time=0), - name=f'{name}|duration_initial', - ) - - # Minimum duration constraint if provided - if minimum_duration is not None: - constraints['minimum'] = model.add_constraints( - duration.isel(time=slice(1, None)) - >= (state_variable.isel(time=slice(None, -1)) - state_variable.isel(time=slice(1, None))) - * minimum_duration.isel(time=slice(None, -1)), - name=f'{name}|duration_minimum', - ) - - # Handle initial condition for minimum duration - if previous_duration > 0 and previous_duration < minimum_duration.isel(time=0).max(): - constraints['initial_minimum'] = model.add_constraints( - state_variable.isel(time=0) == 1, name=f'{name}|duration_initial_minimum' - ) - - variables = {'duration': duration} - - return variables, constraints - - @staticmethod - def mutual_exclusivity_constraint( - model: FlowSystemModel, name: str, binary_variables: List[linopy.Variable], tolerance: float = 1.1 - ) -> Tuple[Dict, Dict[str, linopy.Constraint]]: - """ - Creates mutual exclusivity constraint for binary variables. - - Mathematical formulation: - Σ(binary_vars[i]) ≤ tolerance ∀t - - Ensures at most one binary variable can be 1 at any time. - Tolerance > 1.0 accounts for binary variable numerical precision. - - Args: - binary_variables: List of binary variables that should be mutually exclusive - tolerance: Upper bound (typically 1.1 for numerical stability) - - Returns: - variables: {} (no new variables created) - constraints: {'mutual_exclusivity': constraint} - - Raises: - AssertionError: If fewer than 2 variables provided or variables aren't binary - """ - assert len(binary_variables) >= 2, ( - f'Mutual exclusivity requires at least 2 variables, got {len(binary_variables)}' - ) - - for var in binary_variables: - assert var.attrs.get('binary', False), ( - f'Variable {var.name} must be binary for mutual exclusivity constraint' - ) - - # Create mutual exclusivity constraint - mutual_exclusivity = model.add_constraints( - sum(binary_variables) <= tolerance, name=f'{name}|mutual_exclusivity' - ) - - variables = {} # No new variables created - constraints = {'mutual_exclusivity': mutual_exclusivity} - - return variables, constraints - - -class ModelingPatterns: - """High-level patterns that compose primitives and return (variables, constraints) tuples""" - - @staticmethod - def investment_sizing_pattern( - model: FlowSystemModel, - name: str, - size_bounds: Tuple[TemporalData, TemporalData], - controlled_variables: List[linopy.Variable] = None, - control_factors: List[Tuple[TemporalData, TemporalData]] = None, - optional: bool = False, - ) -> Tuple[Dict[str, linopy.Variable], Dict[str, linopy.Constraint]]: - """ - Complete investment sizing pattern with optional binary decision. - - Returns: - variables: {'size': size_var, 'is_invested': binary_var (if optional)} - constraints: {'investment_upper_bound': constraint, 'investment_lower_bound': constraint, ...} - """ - variables = {} - constraints = {} - - # Investment size variable - size_min, size_max = size_bounds - variables['size'] = model.add_variables( - lower=size_min, - upper=size_max, - name=f'{name}|investment_size', - coords=model.get_coords(['year', 'scenario']), - ) - - # Optional binary investment decision - if optional: - variables['is_invested'] = model.add_variables( - binary=True, name=f'{name}|is_invested', coords=model.get_coords(['year', 'scenario']) - ) - - # Link size to investment decision - if abs(size_min - size_max) < 1e-10: # Fixed size case - constraints['fixed_investment_size'] = model.add_constraints( - variables['size'] == variables['is_invested'] * size_max, name=f'{name}|fixed_investment_size' - ) - else: # Variable size case - constraints['investment_upper_bound'] = model.add_constraints( - variables['size'] <= variables['is_invested'] * size_max, name=f'{name}|investment_upper_bound' - ) - constraints['investment_lower_bound'] = model.add_constraints( - variables['size'] >= variables['is_invested'] * max(CONFIG.modeling.EPSILON, size_min), - name=f'{name}|investment_lower_bound', - ) - - # Control dependent variables - if controlled_variables and control_factors: - for i, (var, factors) in enumerate(zip(controlled_variables, control_factors)): - _, control_constraints = ModelingPrimitives.big_m_binary_bounds( - model, f'{name}|control_{i}', var, variables.get('is_invested'), variables['size'], factors - ) - # Flatten control constraints with indexed names - constraints[f'control_{i}_upper_bound'] = control_constraints['upper_bound'] - constraints[f'control_{i}_lower_bound'] = control_constraints['lower_bound'] - - return variables, constraints - - @staticmethod - def operational_binary_control_pattern( - model: FlowSystemModel, - name: str, - controlled_variables: List[linopy.Variable], - variable_bounds: List[Tuple[TemporalData, TemporalData]], - use_complement: bool = False, - track_total_duration: bool = False, - track_switches: bool = False, - previous_state=0, - duration_bounds: Tuple[TemporalData, TemporalData] = None, - track_consecutive_on: bool = False, - consecutive_on_bounds: Tuple[Optional[TemporalData], Optional[TemporalData]] = (None, None), - previous_on_duration: TemporalData = 0, - track_consecutive_off: bool = False, - consecutive_off_bounds: Tuple[Optional[TemporalData], Optional[TemporalData]] = (None, None), - previous_off_duration: TemporalData = 0, - ) -> Tuple[Dict[str, linopy.Variable], Dict[str, linopy.Constraint]]: - """ - Enhanced operational binary control with consecutive duration tracking. - - New Args: - track_consecutive_on: Whether to track consecutive on duration - consecutive_on_bounds: (min_duration, max_duration) for consecutive on - previous_on_duration: Previous consecutive on duration - track_consecutive_off: Whether to track consecutive off duration - consecutive_off_bounds: (min_duration, max_duration) for consecutive off - previous_off_duration: Previous consecutive off duration - """ - variables = {} - constraints = {} - - # Main binary state (existing logic) - if use_complement: - state_vars, state_constraints = ModelingPrimitives.binary_state_pair(model, name) - variables.update(state_vars) - constraints.update(state_constraints) - else: - variables['on'] = model.add_variables(binary=True, name=f'{name}|on', coords=model.get_coords(['time'])) - - # Control variables (existing logic) - for i, (var, (lower_bound, upper_bound)) in enumerate(zip(controlled_variables, variable_bounds)): - constraints[f'control_{i}_lower'] = model.add_constraints( - variables['on'] * np.maximum(lower_bound, CONFIG.modeling.EPSILON) <= var, name=f'{name}|control_{i}_lower' - ) - constraints[f'control_{i}_upper'] = model.add_constraints( - var <= variables['on'] * upper_bound, name=f'{name}|control_{i}_upper' - ) - - # Total duration tracking (existing logic) - if track_total_duration: - duration_expr = (variables['on'] * model.hours_per_step).sum('time') - duration_vars, duration_constraints = ModelingPrimitives.expression_tracking_variable( - model, f'{name}|duration', duration_expr, duration_bounds - ) - variables['total_duration'] = duration_vars['tracker'] - constraints['duration_tracking'] = duration_constraints['tracking'] - - # Switch tracking (existing logic) - if track_switches: - switch_vars, switch_constraints = ModelingPrimitives.state_transition_variables( - model, f'{name}|switches', variables['on'], previous_state - ) - variables.update(switch_vars) - for switch_name, switch_constraint in switch_constraints.items(): - constraints[f'switch_{switch_name}'] = switch_constraint - - # NEW: Consecutive on duration tracking - if track_consecutive_on: - min_on, max_on = consecutive_on_bounds - consecutive_on_vars, consecutive_on_constraints = ModelingPrimitives.consecutive_duration_tracking( - model, - f'{name}|consecutive_on', - variables['on'], - minimum_duration=min_on, - maximum_duration=max_on, - previous_duration=previous_on_duration, - ) - variables['consecutive_on_duration'] = consecutive_on_vars['duration'] - for cons_name, cons_constraint in consecutive_on_constraints.items(): - constraints[f'consecutive_on_{cons_name}'] = cons_constraint - - # NEW: Consecutive off duration tracking - if track_consecutive_off and 'off' in variables: - min_off, max_off = consecutive_off_bounds - consecutive_off_vars, consecutive_off_constraints = ModelingPrimitives.consecutive_duration_tracking( - model, - f'{name}|consecutive_off', - variables['off'], - minimum_duration=min_off, - maximum_duration=max_off, - previous_duration=previous_off_duration, - ) - variables['consecutive_off_duration'] = consecutive_off_vars['duration'] - for cons_name, cons_constraint in consecutive_off_constraints.items(): - constraints[f'consecutive_off_{cons_name}'] = cons_constraint - - return variables, constraints - - class InvestmentModel(BaseFeatureModel): """Investment model using factory patterns but keeping old interface""" @@ -652,10 +28,10 @@ def __init__( parameters: InvestParameters, defining_variable: linopy.Variable, relative_bounds_of_defining_variable: Tuple[TemporalData, TemporalData], - label: Optional[str] = None, + label_of_model: Optional[str] = None, on_variable: Optional[linopy.Variable] = None, ): - super().__init__(model, label_of_element, parameters, label) + super().__init__(model, label_of_element=label_of_element, parameters=parameters, label_of_model=label_of_model) self._defining_variable = defining_variable self._relative_bounds_of_defining_variable = relative_bounds_of_defining_variable @@ -885,430 +261,6 @@ def _get_previous_state(self): return ModelingUtilities.get_most_recent_state(self._previous_values) -class StateModel(Model): - """ - Handles basic on/off binary states for defining variables - """ - - def __init__( - self, - model: FlowSystemModel, - label_of_element: str, - defining_variables: List[linopy.Variable], - defining_bounds: List[Tuple[TemporalData, TemporalData]], - previous_values: List[Optional[TemporalData]] = None, - use_off: bool = True, - on_hours_total_min: Optional[TemporalData] = 0, - on_hours_total_max: Optional[TemporalData] = None, - effects_per_running_hour: Dict[str, TemporalData] = None, - label: Optional[str] = None, - ): - """ - Models binary state variables based on a continous variable. - - Args: - model: The FlowSystemModel that is used to create the model. - label_of_element: The label of the parent (Element). Used to construct the full label of the model. - defining_variables: List of Variables that are used to define the state - defining_bounds: List of Tuples, defining the absolute bounds of each defining variable - previous_values: List of previous values of the defining variables - use_off: Whether to use the off state or not - on_hours_total_min: min. overall sum of operating hours. - on_hours_total_max: max. overall sum of operating hours. - effects_per_running_hour: Costs per operating hours - label: Label of the OnOffModel - """ - super().__init__(model, label_of_element, label) - assert len(defining_variables) == len(defining_bounds), 'Every defining Variable needs bounds to Model OnOff' - self._defining_variables = defining_variables - self._defining_bounds = defining_bounds - self._previous_values = previous_values or [] - self._on_hours_total_min = on_hours_total_min if on_hours_total_min is not None else 0 - self._on_hours_total_max = on_hours_total_max if on_hours_total_max is not None else np.inf - self._use_off = use_off - self._effects_per_running_hour = effects_per_running_hour if effects_per_running_hour is not None else {} - - self.on = None - self.total_on_hours: Optional[linopy.Variable] = None - self.off = None - - def do_modeling(self): - self.on = self.add( - self._model.add_variables( - name=f'{self.label_full}|on', - binary=True, - coords=self._model.get_coords(), - ), - 'on', - ) - - self.total_on_hours = self.add( - self._model.add_variables( - lower=self._on_hours_total_min, - upper=self._on_hours_total_max, - coords=self._model.get_coords(['year', 'scenario']), - name=f'{self.label_full}|on_hours_total', - ), - 'on_hours_total', - ) - - self.add( - self._model.add_constraints( - self.total_on_hours == (self.on * self._model.hours_per_step).sum('time'), - name=f'{self.label_full}|on_hours_total', - ), - 'on_hours_total', - ) - - # Add defining constraints for each variable - self._add_defining_constraints() - - if self._use_off: - self.off = self.add( - self._model.add_variables( - name=f'{self.label_full}|off', - binary=True, - coords=self._model.get_coords(), - ), - 'off', - ) - - # Constraint: on + off = 1 - self.add(self._model.add_constraints(self.on + self.off == 1, name=f'{self.label_full}|off'), 'off') - - return self - - def _add_defining_constraints(self): - """Add constraints that link defining variables to the on state""" - nr_of_def_vars = len(self._defining_variables) - - if nr_of_def_vars == 1: - # Case for a single defining variable - def_var = self._defining_variables[0] - lb, ub = self._defining_bounds[0] - - # Constraint: on * lower_bound <= def_var - self.add( - self._model.add_constraints( - self.on * np.maximum(CONFIG.modeling.EPSILON, lb) <= def_var, name=f'{self.label_full}|on_con1' - ), - 'on_con1', - ) - - # Constraint: on * upper_bound >= def_var - self.add( - self._model.add_constraints(self.on * ub >= def_var, name=f'{self.label_full}|on_con2'), 'on_con2' - ) - else: - # Case for multiple defining variables - ub = sum(bound[1] for bound in self._defining_bounds) / nr_of_def_vars - lb = CONFIG.modeling.EPSILON #TODO: Can this be a bigger value? (maybe the smallest bound?) - - # Constraint: on * epsilon <= sum(all_defining_variables) - self.add( - self._model.add_constraints( - self.on * lb <= sum(self._defining_variables), name=f'{self.label_full}|on_con1' - ), - 'on_con1', - ) - - # Constraint to ensure all variables are zero when off. - # Divide by nr_of_def_vars to improve numerical stability (smaller factors) - self.add( - self._model.add_constraints( - self.on * ub >= sum([def_var / nr_of_def_vars for def_var in self._defining_variables]), - name=f'{self.label_full}|on_con2', - ), - 'on_con2', - ) - - @property - def previous_states(self) -> np.ndarray: - """Computes the previous states {0, 1} of defining variables as a binary array from their previous values.""" - return StateModel.compute_previous_states(self._previous_values, epsilon=CONFIG.modeling.EPSILON) - - @property - def previous_on_states(self) -> np.ndarray: - return self.previous_states - - @property - def previous_off_states(self): - return 1 - self.previous_states - - @staticmethod - def compute_previous_states(previous_values: List[TemporalData], epsilon: float = 1e-5) -> np.ndarray: - """Computes the previous states {0, 1} of defining variables as a binary array from their previous values.""" - if not previous_values or all([val is None for val in previous_values]): - return np.array([0]) - - # Convert to 2D-array and compute binary on/off states - previous_values = np.array([values for values in previous_values if values is not None]) # Filter out None - if previous_values.ndim > 1: - return np.any(~np.isclose(previous_values, 0, atol=epsilon), axis=0).astype(int) - - return (~np.isclose(previous_values, 0, atol=epsilon)).astype(int) - - -class SwitchStateModel(Model): - """ - Handles switch on/off transitions - """ - - def __init__( - self, - model: FlowSystemModel, - label_of_element: str, - state_variable: linopy.Variable, - previous_state=0, - switch_on_max: Optional[Scalar] = None, - label: Optional[str] = None, - ): - super().__init__(model, label_of_element, label) - self._state_variable = state_variable - self.previous_state = previous_state - self._switch_on_max = switch_on_max if switch_on_max is not None else np.inf - - self.switch_on = None - self.switch_off = None - self.switch_on_nr = None - - def do_modeling(self): - """Create switch variables and constraints""" - - # Create switch variables - self.switch_on = self.add( - self._model.add_variables(binary=True, name=f'{self.label_full}|switch_on', coords=self._model.get_coords()), - 'switch_on', - ) - - self.switch_off = self.add( - self._model.add_variables(binary=True, name=f'{self.label_full}|switch_off', coords=self._model.get_coords()), - 'switch_off', - ) - - # Create count variable for number of switches - self.switch_on_nr = self.add( - self._model.add_variables( - upper=self._switch_on_max, - lower=0, - name=f'{self.label_full}|switch_on_nr', - ), - 'switch_on_nr', - ) - - # Add switch constraints for all entries after the first timestep - self.add( - self._model.add_constraints( - self.switch_on.isel(time=slice(1, None)) - self.switch_off.isel(time=slice(1, None)) - == self._state_variable.isel(time=slice(1, None)) - self._state_variable.isel(time=slice(None, -1)), - name=f'{self.label_full}|switch_con', - ), - 'switch_con', - ) - - # Initial switch constraint - self.add( - self._model.add_constraints( - self.switch_on.isel(time=0) - self.switch_off.isel(time=0) - == self._state_variable.isel(time=0) - self.previous_state, - name=f'{self.label_full}|initial_switch_con', - ), - 'initial_switch_con', - ) - - # Mutual exclusivity constraint - self.add( - self._model.add_constraints(self.switch_on + self.switch_off <= 1.1, name=f'{self.label_full}|switch_on_or_off'), - 'switch_on_or_off', - ) - - # Total switch-on count constraint - self.add( - self._model.add_constraints( - self.switch_on_nr == self.switch_on.sum('time'), name=f'{self.label_full}|switch_on_nr' - ), - 'switch_on_nr', - ) - - return self - - -class ConsecutiveStateModel(Model): - """ - Handles tracking consecutive durations in a state - """ - - def __init__( - self, - model: FlowSystemModel, - label_of_element: str, - state_variable: linopy.Variable, - minimum_duration: Optional[TemporalData] = None, - maximum_duration: Optional[TemporalData] = None, - previous_states: Optional[TemporalData] = None, - label: Optional[str] = None, - ): - """ - Model and constraint the consecutive duration of a state variable. - - Args: - model: The FlowSystemModel that is used to create the model. - label_of_element: The label of the parent (Element). Used to construct the full label of the model. - state_variable: The state variable that is used to model the duration. state = {0, 1} - minimum_duration: The minimum duration of the state variable. - maximum_duration: The maximum duration of the state variable. - previous_states: The previous states of the state variable. - label: The label of the model. Used to construct the full label of the model. - """ - super().__init__(model, label_of_element, label) - self._state_variable = state_variable - self._previous_states = previous_states - self._minimum_duration = minimum_duration - self._maximum_duration = maximum_duration - - self.duration = None - - def do_modeling(self): - """Create consecutive duration variables and constraints""" - # Get the hours per step - hours_per_step = self._model.hours_per_step - mega = hours_per_step.sum('time') + self.previous_duration - - # Create the duration variable - self.duration = self.add( - self._model.add_variables( - lower=0, - upper=self._maximum_duration if self._maximum_duration is not None else mega, - coords=self._model.get_coords(), - name=f'{self.label_full}|hours', - ), - 'hours', - ) - - # Add constraints - - # Upper bound constraint - self.add( - self._model.add_constraints( - self.duration <= self._state_variable * mega, name=f'{self.label_full}|con1' - ), - 'con1', - ) - - # Forward constraint - self.add( - self._model.add_constraints( - self.duration.isel(time=slice(1, None)) - <= self.duration.isel(time=slice(None, -1)) + hours_per_step.isel(time=slice(None, -1)), - name=f'{self.label_full}|con2a', - ), - 'con2a', - ) - - # Backward constraint - self.add( - self._model.add_constraints( - self.duration.isel(time=slice(1, None)) - >= self.duration.isel(time=slice(None, -1)) - + hours_per_step.isel(time=slice(None, -1)) - + (self._state_variable.isel(time=slice(1, None)) - 1) * mega, - name=f'{self.label_full}|con2b', - ), - 'con2b', - ) - - # Add minimum duration constraints if specified - if self._minimum_duration is not None: - self.add( - self._model.add_constraints( - self.duration - >= ( - self._state_variable.isel(time=slice(None, -1)) - self._state_variable.isel(time=slice(1, None)) - ) - * self._minimum_duration.isel(time=slice(None, -1)), - name=f'{self.label_full}|minimum', - ), - 'minimum', - ) - - # Handle initial condition - if 0 < self.previous_duration < self._minimum_duration.isel(time=0).max(): - self.add( - self._model.add_constraints( - self._state_variable.isel(time=0) == 1, name=f'{self.label_full}|initial_minimum' - ), - 'initial_minimum', - ) - - # Set initial value - self.add( - self._model.add_constraints( - self.duration.isel(time=0) == - (hours_per_step.isel(time=0) + self.previous_duration) * self._state_variable.isel(time=0), - name=f'{self.label_full}|initial', - ), - 'initial', - ) - - return self - - @property - def previous_duration(self) -> Scalar: - """Computes the previous duration of the state variable""" - #TODO: Allow for other/dynamic timestep resolutions - return ConsecutiveStateModel.compute_consecutive_hours_in_state( - self._previous_states, self._model.hours_per_step.isel(time=0).values.flatten()[0] - ) - - @staticmethod - def compute_consecutive_hours_in_state( - binary_values: TemporalData, hours_per_timestep: Union[int, float, np.ndarray] - ) -> Scalar: - """ - Computes the final consecutive duration in state 'on' (=1) in hours, from a binary array. - - Args: - binary_values: An int or 1D binary array containing only `0`s and `1`s. - hours_per_timestep: The duration of each timestep in hours. - If a scalar is provided, it is used for all timesteps. - If an array is provided, it must be as long as the last consecutive duration in binary_values. - - Returns: - The duration of the binary variable in hours. - - Raises - ------ - TypeError - If the length of binary_values and dt_in_hours is not equal, but None is a scalar. - """ - if np.isscalar(binary_values) and np.isscalar(hours_per_timestep): - return binary_values * hours_per_timestep - elif np.isscalar(binary_values) and not np.isscalar(hours_per_timestep): - return binary_values * hours_per_timestep[-1] - - if np.isclose(binary_values[-1], 0, atol=CONFIG.modeling.EPSILON): - return 0 - - if np.isscalar(hours_per_timestep): - hours_per_timestep = np.ones(len(binary_values)) * hours_per_timestep - hours_per_timestep: np.ndarray - - indexes_with_zero_values = np.where(np.isclose(binary_values, 0, atol=CONFIG.modeling.EPSILON))[0] - if len(indexes_with_zero_values) == 0: - nr_of_indexes_with_consecutive_ones = len(binary_values) - else: - nr_of_indexes_with_consecutive_ones = len(binary_values) - indexes_with_zero_values[-1] - 1 - - if len(hours_per_timestep) < nr_of_indexes_with_consecutive_ones: - raise ValueError( - f'When trying to calculate the consecutive duration, the length of the last duration ' - f'({len(nr_of_indexes_with_consecutive_ones)}) is longer than the provided hours_per_timestep ({len(hours_per_timestep)}), ' - f'as {binary_values=}' - ) - - return np.sum(binary_values[-nr_of_indexes_with_consecutive_ones:] * hours_per_timestep[-nr_of_indexes_with_consecutive_ones:]) - - class PieceModel(Model): """Class for modeling a linear piece of one or more variables in parallel""" @@ -1316,10 +268,10 @@ def __init__( self, model: FlowSystemModel, label_of_element: str, - label: str, + label_of_model: str, as_time_series: bool = True, ): - super().__init__(model, label_of_element, label) + super().__init__(model, label_of_element, label_of_model) self.inside_piece: Optional[linopy.Variable] = None self.lambda0: Optional[linopy.Variable] = None self.lambda1: Optional[linopy.Variable] = None @@ -1373,7 +325,7 @@ def __init__( piecewise_variables: Dict[str, Piecewise], zero_point: Optional[Union[bool, linopy.Variable]], as_time_series: bool, - label: str = '', + label_of_model: str = '', ): """ Modeling a Piecewise relation between miultiple variables. @@ -1388,7 +340,7 @@ def __init__( zero_point: A variable that can be used to define a zero point for the Piecewise relation. If None or False, no zero point is defined. as_time_series: Whether the Piecewise relation is defined for a TimeSeries or a single variable. """ - super().__init__(model, label_of_element, label) + super().__init__(model, label_of_element=label_of_element, label_of_model=label_of_model) self._piecewise_variables = piecewise_variables self._zero_point = zero_point self._as_time_series = as_time_series @@ -1402,7 +354,7 @@ def do_modeling(self): PieceModel( model=self._model, label_of_element=self.label_of_element, - label=f'Piece_{i}', + label_of_model=f'{self.label_of_element}|Piece_{i}', as_time_series=self._as_time_series, ) ) @@ -1452,20 +404,80 @@ def do_modeling(self): ) +class PiecewiseEffectsModel(Model): + def __init__( + self, + model: FlowSystemModel, + label_of_element: str, + piecewise_origin: Tuple[str, Piecewise], + piecewise_shares: Dict[str, Piecewise], + zero_point: Optional[Union[bool, linopy.Variable]], + label: str = 'PiecewiseEffects', + ): + super().__init__(model, label_of_element, label) + assert len(piecewise_origin[1]) == len(list(piecewise_shares.values())[0]), ( + 'Piece length of variable_segments and share_segments must be equal' + ) + self._zero_point = zero_point + self._piecewise_origin = piecewise_origin + self._piecewise_shares = piecewise_shares + self.shares: Dict[str, linopy.Variable] = {} + + self.piecewise_model: Optional[PiecewiseModel] = None + + def do_modeling(self): + self.shares = { + effect: self.add( + self._model.add_variables( + coords=self._model.get_coords(['year', 'scenario']), name=f'{self.label_full}|{effect}' + ), + f'{effect}', + ) + for effect in self._piecewise_shares + } + + piecewise_variables = { + self._piecewise_origin[0]: self._piecewise_origin[1], + **{ + self.shares[effect_label].name: self._piecewise_shares[effect_label] + for effect_label in self._piecewise_shares + }, + } + + self.piecewise_model = self.add( + PiecewiseModel( + model=self._model, + label_of_element=self.label_of_element, + piecewise_variables=piecewise_variables, + zero_point=self._zero_point, + as_time_series=False, + label_of_model=f'{self.label_of_element}|PiecewiseEffects', + ) + ) + + self.piecewise_model.do_modeling() + + # Shares + self._model.effects.add_share_to_effects( + name=self.label_of_element, + expressions={effect: variable * 1 for effect, variable in self.shares.items()}, + target='invest', + ) + + class ShareAllocationModel(Model): def __init__( self, model: FlowSystemModel, dims: List[FlowSystemDimensions], label_of_element: Optional[str] = None, - label: Optional[str] = None, - label_full: Optional[str] = None, + label_of_model: Optional[str] = None, total_max: Optional[Scalar] = None, total_min: Optional[Scalar] = None, max_per_hour: Optional[TemporalData] = None, min_per_hour: Optional[TemporalData] = None, ): - super().__init__(model, label_of_element=label_of_element, label=label, label_full=label_full) + super().__init__(model, label_of_element=label_of_element, label_of_model=label_of_model) if 'time' not in dims and (max_per_hour is not None or min_per_hour is not None): raise ValueError('Both max_per_hour and min_per_hour cannot be used when has_time_dim is False') @@ -1565,67 +577,6 @@ def add_share( self._eq_total_per_timestep.lhs -= self.shares[name] -class PiecewiseEffectsModel(Model): - def __init__( - self, - model: FlowSystemModel, - label_of_element: str, - piecewise_origin: Tuple[str, Piecewise], - piecewise_shares: Dict[str, Piecewise], - zero_point: Optional[Union[bool, linopy.Variable]], - label: str = 'PiecewiseEffects', - ): - super().__init__(model, label_of_element, label) - assert len(piecewise_origin[1]) == len(list(piecewise_shares.values())[0]), ( - 'Piece length of variable_segments and share_segments must be equal' - ) - self._zero_point = zero_point - self._piecewise_origin = piecewise_origin - self._piecewise_shares = piecewise_shares - self.shares: Dict[str, linopy.Variable] = {} - - self.piecewise_model: Optional[PiecewiseModel] = None - - def do_modeling(self): - self.shares = { - effect: self.add( - self._model.add_variables( - coords=self._model.get_coords(['year', 'scenario']), name=f'{self.label_full}|{effect}' - ), - f'{effect}', - ) - for effect in self._piecewise_shares - } - - piecewise_variables = { - self._piecewise_origin[0]: self._piecewise_origin[1], - **{ - self.shares[effect_label].name: self._piecewise_shares[effect_label] - for effect_label in self._piecewise_shares - }, - } - - self.piecewise_model = self.add( - PiecewiseModel( - model=self._model, - label_of_element=self.label_of_element, - piecewise_variables=piecewise_variables, - zero_point=self._zero_point, - as_time_series=False, - label='PiecewiseEffects', - ) - ) - - self.piecewise_model.do_modeling() - - # Shares - self._model.effects.add_share_to_effects( - name=self.label_of_element, - expressions={effect: variable * 1 for effect, variable in self.shares.items()}, - target='invest', - ) - - class PreventSimultaneousUsageModel(Model): """ Prevents multiple Multiple Binary variables from being 1 at the same time diff --git a/flixopt/modeling.py b/flixopt/modeling.py new file mode 100644 index 000000000..2b5445a3c --- /dev/null +++ b/flixopt/modeling.py @@ -0,0 +1,636 @@ +import logging +from typing import Dict, List, Optional, Tuple, Union + +import linopy +import numpy as np + +from .config import CONFIG +from .core import NonTemporalData, Scalar, TemporalData, FlowSystemDimensions +from .structure import Model, FlowSystemModel, BaseFeatureModel + +logger = logging.getLogger('flixopt') + + +class ModelingUtilities: + """Utility functions for modeling calculations - used across different classes""" + + @staticmethod + def compute_consecutive_hours_in_state( + binary_values: TemporalData, hours_per_timestep: Union[int, float, np.ndarray] + ) -> Scalar: + """ + Computes the final consecutive duration in state 'on' (=1) in hours, from a binary array. + + Args: + binary_values: An int or 1D binary array containing only `0`s and `1`s. + hours_per_timestep: The duration of each timestep in hours. + If a scalar is provided, it is used for all timesteps. + If an array is provided, it must be as long as the last consecutive duration in binary_values. + + Returns: + The duration of the binary variable in hours. + + Raises + ------ + TypeError + If the length of binary_values and dt_in_hours is not equal, but None is a scalar. + """ + if np.isscalar(binary_values) and np.isscalar(hours_per_timestep): + return binary_values * hours_per_timestep + elif np.isscalar(binary_values) and not np.isscalar(hours_per_timestep): + return binary_values * hours_per_timestep[-1] + + if np.isclose(binary_values[-1], 0, atol=CONFIG.modeling.EPSILON): + return 0 + + if np.isscalar(hours_per_timestep): + hours_per_timestep = np.ones(len(binary_values)) * hours_per_timestep + hours_per_timestep: np.ndarray + + indexes_with_zero_values = np.where(np.isclose(binary_values, 0, atol=CONFIG.modeling.EPSILON))[0] + if len(indexes_with_zero_values) == 0: + nr_of_indexes_with_consecutive_ones = len(binary_values) + else: + nr_of_indexes_with_consecutive_ones = len(binary_values) - indexes_with_zero_values[-1] - 1 + + if len(hours_per_timestep) < nr_of_indexes_with_consecutive_ones: + raise ValueError( + f'When trying to calculate the consecutive duration, the length of the last duration ' + f'({nr_of_indexes_with_consecutive_ones}) is longer than the provided hours_per_timestep ({len(hours_per_timestep)}), ' + f'as {binary_values=}' + ) + + return np.sum( + binary_values[-nr_of_indexes_with_consecutive_ones:] + * hours_per_timestep[-nr_of_indexes_with_consecutive_ones:] + ) + + @staticmethod + def compute_previous_states(previous_values: List[TemporalData], epsilon: float = None) -> np.ndarray: + """ + Computes the previous states {0, 1} of defining variables as a binary array from their previous values. + + Args: + previous_values: List of previous values for variables + epsilon: Tolerance for zero detection (uses CONFIG.modeling.EPSILON if None) + + Returns: + Binary array of previous states + """ + if epsilon is None: + epsilon = CONFIG.modeling.EPSILON + + if not previous_values or all(val is None for val in previous_values): + return np.array([0]) + + # Convert to 2D-array and compute binary on/off states + previous_values = np.array([values for values in previous_values if values is not None]) # Filter out None + if previous_values.ndim > 1: + return np.any(~np.isclose(previous_values, 0, atol=epsilon), axis=0).astype(int) + + return (~np.isclose(previous_values, 0, atol=epsilon)).astype(int) + + @staticmethod + def compute_previous_on_duration(previous_values: List[TemporalData], hours_per_step: Union[int, float]) -> Scalar: + """ + Convenience method to compute previous consecutive 'on' duration. + + Args: + previous_values: List of previous values for variables + hours_per_step: Duration of each timestep in hours + + Returns: + Previous consecutive on duration in hours + """ + if not previous_values: + return 0 + + previous_states = ModelingUtilities.compute_previous_states(previous_values) + return ModelingUtilities.compute_consecutive_hours_in_state(previous_states, hours_per_step) + + @staticmethod + def compute_previous_off_duration(previous_values: List[TemporalData], hours_per_step: Union[int, float]) -> Scalar: + """ + Convenience method to compute previous consecutive 'off' duration. + + Args: + previous_values: List of previous values for variables + hours_per_step: Duration of each timestep in hours + + Returns: + Previous consecutive off duration in hours + """ + if not previous_values: + return 0 + + previous_states = ModelingUtilities.compute_previous_states(previous_values) + previous_off_states = 1 - previous_states + return ModelingUtilities.compute_consecutive_hours_in_state(previous_off_states, hours_per_step) + + @staticmethod + def get_most_recent_state(previous_values: List[TemporalData]) -> int: + """ + Get the most recent binary state from previous values. + + Args: + previous_values: List of previous values for variables + + Returns: + Most recent binary state (0 or 1) + """ + if not previous_values: + return 0 + + previous_states = ModelingUtilities.compute_previous_states(previous_values) + return int(previous_states[-1]) + + +class ModelingPrimitives: + """Mathematical modeling primitives returning (variables, constraints) tuples""" + + @staticmethod + def binary_state_pair( + model: FlowSystemModel, name: str, coords: List[str] = None + ) -> Tuple[Dict[str, linopy.Variable], Dict[str, linopy.Constraint]]: + """ + Creates complementary binary variables with completeness constraint. + + Mathematical formulation: + on[t] + off[t] = 1 ∀t + on[t], off[t] ∈ {0, 1} + + Returns: + variables: {'on': binary_var, 'off': binary_var} + constraints: {'complementary': constraint} + """ + coords = coords or ['time'] + + on = model.add_variables(binary=True, name=f'{name}|on', coords=model.get_coords(coords)) + off = model.add_variables(binary=True, name=f'{name}|off', coords=model.get_coords(coords)) + + # Constraint: on + off = 1 + complementary = model.add_constraints(on + off == 1, name=f'{name}|complementary') + + variables = {'on': on, 'off': off} + constraints = {'complementary': complementary} + + return variables, constraints + + @staticmethod + def proportionally_bounded_variable( + model: FlowSystemModel, + name: str, + controlling_variable, + bounds: Tuple[TemporalData, TemporalData], + coords: List[str] = None, + ) -> Tuple[Dict[str, linopy.Variable], Dict[str, linopy.Constraint]]: + """ + Creates variable with bounds proportional to another variable. + + Mathematical formulation: + lower_factor[t] * controller[t] ≤ variable[t] ≤ upper_factor[t] * controller[t] ∀t + + Returns: + variables: {'variable': bounded_var} + constraints: {'lower_bound': constraint, 'upper_bound': constraint} + """ + coords = coords or ['time'] + variable = model.add_variables(name=f'{name}|bounded', coords=model.get_coords(coords)) + + lower_factor, upper_factor = bounds + + # Constraints: lower_factor * controller ≤ var ≤ upper_factor * controller + lower_bound = model.add_constraints( + variable >= controlling_variable * lower_factor, name=f'{name}|proportional_lb' + ) + upper_bound = model.add_constraints( + variable <= controlling_variable * upper_factor, name=f'{name}|proportional_ub' + ) + + variables = {'variable': variable} + constraints = {'lower_bound': lower_bound, 'upper_bound': upper_bound} + + return variables, constraints + + @staticmethod + def expression_tracking_variable( + model: FlowSystemModel, + name: str, + tracked_expression, + bounds: Tuple[TemporalData, TemporalData] = None, + coords: List[str] = None, + ) -> Tuple[Dict[str, linopy.Variable], Dict[str, linopy.Constraint]]: + """ + Creates variable that equals a given expression. + + Mathematical formulation: + tracker = expression + lower ≤ tracker ≤ upper (if bounds provided) + + Returns: + variables: {'tracker': tracker_var} + constraints: {'tracking': constraint} + """ + coords = coords or ['year', 'scenario'] + + if not bounds: + tracker = model.add_variables(name=f'{name}', coords=model.get_coords(coords)) + else: + tracker = model.add_variables( + lower=bounds[0] if bounds[0] is not None else -np.inf, + upper=bounds[1] if bounds[1] is not None else np.inf, + name=f'{name}', + coords=model.get_coords(coords), + ) + + # Constraint: tracker = expression + tracking = model.add_constraints(tracker == tracked_expression, name=f'{name}') + + variables = {'tracker': tracker} + constraints = {'tracking': tracking} + + return variables, constraints + + @staticmethod + def state_transition_variables( + model: FlowSystemModel, name: str, state_variable, previous_state=0 + ) -> Tuple[Dict[str, linopy.Variable], Dict[str, linopy.Constraint]]: + """ + Creates switch-on/off variables with state transition logic. + + Mathematical formulation: + switch_on[t] - switch_off[t] = state[t] - state[t-1] ∀t > 0 + switch_on[0] - switch_off[0] = state[0] - previous_state + switch_on[t] + switch_off[t] ≤ 1 ∀t + switch_on[t], switch_off[t] ∈ {0, 1} + + Returns: + variables: {'switch_on': binary_var, 'switch_off': binary_var} + constraints: {'transition': constraint, 'initial': constraint, 'mutex': constraint} + """ + switch_on = model.add_variables(binary=True, name=f'{name}|switch_on', coords=model.get_coords(['time'])) + switch_off = model.add_variables(binary=True, name=f'{name}|switch_off', coords=model.get_coords(['time'])) + + # State transition constraints for t > 0 + transition = model.add_constraints( + switch_on.isel(time=slice(1, None)) - switch_off.isel(time=slice(1, None)) + == state_variable.isel(time=slice(1, None)) - state_variable.isel(time=slice(None, -1)), + name=f'{name}|state_transition', + ) + + # Initial state transition for t = 0 + initial = model.add_constraints( + switch_on.isel(time=0) - switch_off.isel(time=0) == state_variable.isel(time=0) - previous_state, + name=f'{name}|initial_transition', + ) + + # At most one switch per timestep + mutex = model.add_constraints(switch_on + switch_off <= 1, name=f'{name}|switch_mutex') + + variables = {'switch_on': switch_on, 'switch_off': switch_off} + constraints = {'transition': transition, 'initial': initial, 'mutex': mutex} + + return variables, constraints + + @staticmethod + def big_m_binary_bounds( + model: FlowSystemModel, + name: str, + variable, + binary_control, + size_variable, + relative_bounds: Tuple[TemporalData, TemporalData], + ) -> Tuple[Dict, Dict[str, linopy.Constraint]]: + """ + Creates bounds controlled by both binary and continuous variables. + + Mathematical formulation: + variable[t] ≤ size[t] * upper_factor[t] ∀t + + If binary_control provided: + variable[t] ≥ M * (binary[t] - 1) + size[t] * lower_factor[t] ∀t + where M = max(size) * max(upper_factor) + Else: + variable[t] ≥ size[t] * lower_factor[t] ∀t + + Returns: + variables: {} (no new variables created) + constraints: {'upper_bound': constraint, 'lower_bound': constraint} + """ + rel_lower, rel_upper = relative_bounds + + # Upper bound: variable ≤ size * upper_factor + upper_bound = model.add_constraints(variable <= size_variable * rel_upper, name=f'{name}|size_upper_bound') + + if binary_control is not None: + # Big-M lower bound: variable ≥ M*(binary-1) + size*lower_factor + big_m = size_variable.max() * rel_upper.max() # Conservative big-M + lower_bound = model.add_constraints( + variable >= big_m * (binary_control - 1) + size_variable * rel_lower, + name=f'{name}|binary_controlled_lower_bound', + ) + else: + # Simple lower bound: variable ≥ size * lower_factor + lower_bound = model.add_constraints(variable >= size_variable * rel_lower, name=f'{name}|size_lower_bound') + + variables = {} # No new variables created + constraints = {'upper_bound': upper_bound, 'lower_bound': lower_bound} + + return variables, constraints + + @staticmethod + def consecutive_duration_tracking( + model: FlowSystemModel, + name: str, + state_variable: linopy.Variable, + minimum_duration: Optional[TemporalData] = None, + maximum_duration: Optional[TemporalData] = None, + previous_duration: TemporalData = 0, + ) -> Tuple[Dict[str, linopy.Variable], Dict[str, linopy.Constraint]]: + """ + Creates consecutive duration tracking for a binary state variable. + + Mathematical formulation: + duration[t] ≤ state[t] * M ∀t + duration[t+1] ≤ duration[t] + hours_per_step[t] ∀t + duration[t+1] ≥ duration[t] + hours_per_step[t] + (state[t+1] - 1) * M ∀t + duration[0] = (hours_per_step[0] + previous_duration) * state[0] + + If minimum_duration provided: + duration[t] ≥ (state[t-1] - state[t]) * minimum_duration[t-1] ∀t > 0 + + Args: + state_variable: Binary state variable to track duration for + minimum_duration: Optional minimum consecutive duration + maximum_duration: Optional maximum consecutive duration + previous_duration: Duration from before first timestep + + Returns: + variables: {'duration': duration_var} + constraints: {'upper_bound': constraint, 'forward': constraint, 'backward': constraint, ...} + """ + hours_per_step = model.hours_per_step + mega = hours_per_step.sum('time') + previous_duration # Big-M value + + # Duration variable + duration = model.add_variables( + lower=0, + upper=maximum_duration if maximum_duration is not None else mega, + coords=model.get_coords(['time']), + name=f'{name}|duration', + ) + + constraints = {} + + # Upper bound: duration[t] ≤ state[t] * M + constraints['upper_bound'] = model.add_constraints( + duration <= state_variable * mega, name=f'{name}|duration_upper_bound' + ) + + # Forward constraint: duration[t+1] ≤ duration[t] + hours_per_step[t] + constraints['forward'] = model.add_constraints( + duration.isel(time=slice(1, None)) + <= duration.isel(time=slice(None, -1)) + hours_per_step.isel(time=slice(None, -1)), + name=f'{name}|duration_forward', + ) + + # Backward constraint: duration[t+1] ≥ duration[t] + hours_per_step[t] + (state[t+1] - 1) * M + constraints['backward'] = model.add_constraints( + duration.isel(time=slice(1, None)) + >= duration.isel(time=slice(None, -1)) + + hours_per_step.isel(time=slice(None, -1)) + + (state_variable.isel(time=slice(1, None)) - 1) * mega, + name=f'{name}|duration_backward', + ) + + # Initial condition: duration[0] = (hours_per_step[0] + previous_duration) * state[0] + constraints['initial'] = model.add_constraints( + duration.isel(time=0) + == (hours_per_step.isel(time=0) + previous_duration) * state_variable.isel(time=0), + name=f'{name}|duration_initial', + ) + + # Minimum duration constraint if provided + if minimum_duration is not None: + constraints['minimum'] = model.add_constraints( + duration.isel(time=slice(1, None)) + >= (state_variable.isel(time=slice(None, -1)) - state_variable.isel(time=slice(1, None))) + * minimum_duration.isel(time=slice(None, -1)), + name=f'{name}|duration_minimum', + ) + + # Handle initial condition for minimum duration + if previous_duration > 0 and previous_duration < minimum_duration.isel(time=0).max(): + constraints['initial_minimum'] = model.add_constraints( + state_variable.isel(time=0) == 1, name=f'{name}|duration_initial_minimum' + ) + + variables = {'duration': duration} + + return variables, constraints + + @staticmethod + def mutual_exclusivity_constraint( + model: FlowSystemModel, name: str, binary_variables: List[linopy.Variable], tolerance: float = 1.1 + ) -> Tuple[Dict, Dict[str, linopy.Constraint]]: + """ + Creates mutual exclusivity constraint for binary variables. + + Mathematical formulation: + Σ(binary_vars[i]) ≤ tolerance ∀t + + Ensures at most one binary variable can be 1 at any time. + Tolerance > 1.0 accounts for binary variable numerical precision. + + Args: + binary_variables: List of binary variables that should be mutually exclusive + tolerance: Upper bound (typically 1.1 for numerical stability) + + Returns: + variables: {} (no new variables created) + constraints: {'mutual_exclusivity': constraint} + + Raises: + AssertionError: If fewer than 2 variables provided or variables aren't binary + """ + assert len(binary_variables) >= 2, ( + f'Mutual exclusivity requires at least 2 variables, got {len(binary_variables)}' + ) + + for var in binary_variables: + assert var.attrs.get('binary', False), ( + f'Variable {var.name} must be binary for mutual exclusivity constraint' + ) + + # Create mutual exclusivity constraint + mutual_exclusivity = model.add_constraints( + sum(binary_variables) <= tolerance, name=f'{name}|mutual_exclusivity' + ) + + variables = {} # No new variables created + constraints = {'mutual_exclusivity': mutual_exclusivity} + + return variables, constraints + + +class ModelingPatterns: + """High-level patterns that compose primitives and return (variables, constraints) tuples""" + + @staticmethod + def investment_sizing_pattern( + model: FlowSystemModel, + name: str, + size_bounds: Tuple[TemporalData, TemporalData], + controlled_variables: List[linopy.Variable] = None, + control_factors: List[Tuple[TemporalData, TemporalData]] = None, + optional: bool = False, + ) -> Tuple[Dict[str, linopy.Variable], Dict[str, linopy.Constraint]]: + """ + Complete investment sizing pattern with optional binary decision. + + Returns: + variables: {'size': size_var, 'is_invested': binary_var (if optional)} + constraints: {'upper_bound': constraint, 'lower_bound': constraint, ...} + """ + variables = {} + constraints = {} + + # Investment size variable + size_min, size_max = size_bounds + variables['size'] = model.add_variables( + lower=size_min, + upper=size_max, + name=f'{name}|size', + coords=model.get_coords(['year', 'scenario']), + ) + + # Optional binary investment decision + if optional: + variables['is_invested'] = model.add_variables( + binary=True, name=f'{name}|is_invested', coords=model.get_coords(['year', 'scenario']) + ) + + # Link size to investment decision + if abs(size_min - size_max) < 1e-10: # Fixed size case + constraints['fixed_size'] = model.add_constraints( + variables['size'] == variables['is_invested'] * size_max, name=f'{name}|fixed_size' + ) + else: # Variable size case + constraints['upper_bound'] = model.add_constraints( + variables['size'] <= variables['is_invested'] * size_max, name=f'{name}|upper_bound' + ) + constraints['lower_bound'] = model.add_constraints( + variables['size'] >= variables['is_invested'] * max(CONFIG.modeling.EPSILON, size_min), + name=f'{name}|lower_bound', + ) + + # Control dependent variables + if controlled_variables and control_factors: + for i, (var, factors) in enumerate(zip(controlled_variables, control_factors)): + _, control_constraints = ModelingPrimitives.big_m_binary_bounds( + model, f'{name}|control_{i}', var, variables.get('is_invested'), variables['size'], factors + ) + # Flatten control constraints with indexed names + constraints[f'control_{i}_upper_bound'] = control_constraints['upper_bound'] + constraints[f'control_{i}_lower_bound'] = control_constraints['lower_bound'] + + return variables, constraints + + @staticmethod + def operational_binary_control_pattern( + model: FlowSystemModel, + name: str, + controlled_variables: List[linopy.Variable], + variable_bounds: List[Tuple[TemporalData, TemporalData]], + use_complement: bool = False, + track_total_duration: bool = False, + track_switches: bool = False, + previous_state=0, + duration_bounds: Tuple[TemporalData, TemporalData] = None, + track_consecutive_on: bool = False, + consecutive_on_bounds: Tuple[Optional[TemporalData], Optional[TemporalData]] = (None, None), + previous_on_duration: TemporalData = 0, + track_consecutive_off: bool = False, + consecutive_off_bounds: Tuple[Optional[TemporalData], Optional[TemporalData]] = (None, None), + previous_off_duration: TemporalData = 0, + ) -> Tuple[Dict[str, linopy.Variable], Dict[str, linopy.Constraint]]: + """ + Enhanced operational binary control with consecutive duration tracking. + + New Args: + track_consecutive_on: Whether to track consecutive on duration + consecutive_on_bounds: (min_duration, max_duration) for consecutive on + previous_on_duration: Previous consecutive on duration + track_consecutive_off: Whether to track consecutive off duration + consecutive_off_bounds: (min_duration, max_duration) for consecutive off + previous_off_duration: Previous consecutive off duration + """ + variables = {} + constraints = {} + + # Main binary state (existing logic) + if use_complement: + state_vars, state_constraints = ModelingPrimitives.binary_state_pair(model, name) + variables.update(state_vars) + constraints.update(state_constraints) + else: + variables['on'] = model.add_variables(binary=True, name=f'{name}|on', coords=model.get_coords(['time'])) + + # Control variables (existing logic) + for i, (var, (lower_bound, upper_bound)) in enumerate(zip(controlled_variables, variable_bounds)): + constraints[f'control_{i}_lower'] = model.add_constraints( + variables['on'] * np.maximum(lower_bound, CONFIG.modeling.EPSILON) <= var, name=f'{name}|control_{i}_lower' + ) + constraints[f'control_{i}_upper'] = model.add_constraints( + var <= variables['on'] * upper_bound, name=f'{name}|control_{i}_upper' + ) + + # Total duration tracking (existing logic) + if track_total_duration: + duration_expr = (variables['on'] * model.hours_per_step).sum('time') + duration_vars, duration_constraints = ModelingPrimitives.expression_tracking_variable( + model, f'{name}|duration', duration_expr, duration_bounds + ) + variables['total_duration'] = duration_vars['tracker'] + constraints['duration_tracking'] = duration_constraints['tracking'] + + # Switch tracking (existing logic) + if track_switches: + switch_vars, switch_constraints = ModelingPrimitives.state_transition_variables( + model, f'{name}|switches', variables['on'], previous_state + ) + variables.update(switch_vars) + for switch_name, switch_constraint in switch_constraints.items(): + constraints[f'switch_{switch_name}'] = switch_constraint + + # NEW: Consecutive on duration tracking + if track_consecutive_on: + min_on, max_on = consecutive_on_bounds + consecutive_on_vars, consecutive_on_constraints = ModelingPrimitives.consecutive_duration_tracking( + model, + f'{name}|consecutive_on', + variables['on'], + minimum_duration=min_on, + maximum_duration=max_on, + previous_duration=previous_on_duration, + ) + variables['consecutive_on_duration'] = consecutive_on_vars['duration'] + for cons_name, cons_constraint in consecutive_on_constraints.items(): + constraints[f'consecutive_on_{cons_name}'] = cons_constraint + + # NEW: Consecutive off duration tracking + if track_consecutive_off and 'off' in variables: + min_off, max_off = consecutive_off_bounds + consecutive_off_vars, consecutive_off_constraints = ModelingPrimitives.consecutive_duration_tracking( + model, + f'{name}|consecutive_off', + variables['off'], + minimum_duration=min_off, + maximum_duration=max_off, + previous_duration=previous_off_duration, + ) + variables['consecutive_off_duration'] = consecutive_off_vars['duration'] + for cons_name, cons_constraint in consecutive_off_constraints.items(): + constraints[f'consecutive_off_{cons_name}'] = cons_constraint + + return variables, constraints diff --git a/flixopt/structure.py b/flixopt/structure.py index 5a4b016ce..ec594ca6e 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -700,19 +700,17 @@ class Model: """Stores Variables and Constraints.""" def __init__( - self, model: FlowSystemModel, label_of_element: str, label: str = '', label_full: Optional[str] = None + self, model: FlowSystemModel, label_of_element: str, label_of_model = None ): """ Args: model: The FlowSystemModel that is used to create the model. label_of_element: The label of the parent (Element). Used to construct the full label of the model. - label: The label of the model. Used to construct the full label of the model. - label_full: The full label of the model. Can overwrite the full label constructed from the other labels. + label_of_model: The label of the model. Used as a prefix in all variables and constraints. """ self._model = model self.label_of_element = label_of_element - self._label = label - self._label_full = label_full + self.label_of_model = label_of_model if label_of_model is not None else self.label_of_element self._variables_direct: List[str] = [] self._constraints_direct: List[str] = [] @@ -777,16 +775,11 @@ def filter_variables( @property def label(self) -> str: - return self._label if self._label else self.label_of_element + return self.label_of_model @property def label_full(self) -> str: - """Used to construct the names of variables and constraints""" - if self._label_full: - return self._label_full - elif self._label: - return f'{self.label_of_element}|{self.label}' - return self.label_of_element + return self.label_of_model @property def variables_direct(self) -> linopy.Variables: @@ -846,8 +839,16 @@ def get_constraint_by_short_name(self, short_name: str, default_return = None) - class BaseFeatureModel(Model): """Minimal base class for feature models that use factory patterns""" - def __init__(self, model: FlowSystemModel, label_of_element: str, parameters, label: Optional[str] = None): - super().__init__(model, label_of_element, label or self.__class__.__name__) + def __init__(self, model: FlowSystemModel, label_of_element: str, parameters, label_of_model: Optional[str] = None): + """Initialize the BaseFeatureModel. + Args: + model: The FlowSystemModel that is used to create the model. + label_of_element: The label of the parent (Element). Used to create shares. + label_of_model: The label of the model. Used as a prefix in all variables and constraints. + Defaults to {label_of_element}|{self.__class__.__name__} + parameters: The parameters of the feature model. + """ + super().__init__(model, label_of_element, label_of_model or f'{label_of_element}|{self.__class__.__name__}') self.parameters = parameters def do_modeling(self): @@ -873,7 +874,7 @@ def __init__(self, model: FlowSystemModel, element: Element): model: The FlowSystemModel that is used to create the model. element: The element this model is created for. """ - super().__init__(model, label_of_element=element.label_full, label=element.label, label_full=element.label_full) + super().__init__(model, label_of_element=element.label_full) self.element = element def results_structure(self): From a3511f91aae37d6d50cf04643f6bb275b4078387 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 18 Jul 2025 11:55:02 +0200 Subject: [PATCH 201/448] Add naming options to big_m_binary_bounds() --- flixopt/modeling.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/flixopt/modeling.py b/flixopt/modeling.py index 2b5445a3c..1651cb12b 100644 --- a/flixopt/modeling.py +++ b/flixopt/modeling.py @@ -295,11 +295,12 @@ def state_transition_variables( @staticmethod def big_m_binary_bounds( model: FlowSystemModel, - name: str, variable, binary_control, size_variable, relative_bounds: Tuple[TemporalData, TemporalData], + upper_bound_name: str, + lower_bound_name: str, ) -> Tuple[Dict, Dict[str, linopy.Constraint]]: """ Creates bounds controlled by both binary and continuous variables. @@ -320,18 +321,16 @@ def big_m_binary_bounds( rel_lower, rel_upper = relative_bounds # Upper bound: variable ≤ size * upper_factor - upper_bound = model.add_constraints(variable <= size_variable * rel_upper, name=f'{name}|size_upper_bound') + upper_bound = model.add_constraints(variable <= size_variable * rel_upper, name=upper_bound_name) if binary_control is not None: # Big-M lower bound: variable ≥ M*(binary-1) + size*lower_factor big_m = size_variable.max() * rel_upper.max() # Conservative big-M lower_bound = model.add_constraints( - variable >= big_m * (binary_control - 1) + size_variable * rel_lower, - name=f'{name}|binary_controlled_lower_bound', + variable >= big_m * (binary_control - 1) + size_variable * rel_lower, name=lower_bound_name ) else: - # Simple lower bound: variable ≥ size * lower_factor - lower_bound = model.add_constraints(variable >= size_variable * rel_lower, name=f'{name}|size_lower_bound') + lower_bound = model.add_constraints(variable >= size_variable * rel_lower, name=lower_bound_name) variables = {} # No new variables created constraints = {'upper_bound': upper_bound, 'lower_bound': lower_bound} From 404dc033fc9de602c955c58cd8a083d08bd515dc Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 18 Jul 2025 12:36:39 +0200 Subject: [PATCH 202/448] Fix and improve FLowModeling with Investment --- flixopt/effects.py | 2 +- flixopt/features.py | 6 ++-- flixopt/modeling.py | 43 ++++++++++++++++++++-------- tests/test_flow.py | 68 ++++++++++++++++++++++----------------------- 4 files changed, 68 insertions(+), 51 deletions(-) diff --git a/flixopt/effects.py b/flixopt/effects.py index 2fc2aae37..0e4236076 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -158,7 +158,7 @@ def __init__(self, model: FlowSystemModel, element: Effect): model=self._model, dims=('time', 'year', 'scenario'), label_of_element=self.label_of_element, - label_of_model=f'{self.label_of_model}|(operation)', + label_of_model=f'{self.label_of_model}(operation)', total_max=self.element.maximum_operation, total_min=self.element.minimum_operation, min_per_hour=self.element.minimum_operation_per_hour diff --git a/flixopt/features.py b/flixopt/features.py index e08d94cb1..49635ad3d 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -46,12 +46,10 @@ def create_variables_and_constraints(self): variables, constraints = ModelingPatterns.investment_sizing_pattern( model=self._model, name=self.label_full, - size_bounds=( - 0 if self.parameters.optional else self.parameters.minimum_or_fixed_size, - self.parameters.maximum_or_fixed_size, - ), + size_bounds=(self.parameters.minimum_or_fixed_size, self.parameters.maximum_or_fixed_size,), controlled_variables=[self._defining_variable], control_factors=[self._relative_bounds_of_defining_variable], + state_variables=[self._on_variable], optional=self.parameters.optional, ) diff --git a/flixopt/modeling.py b/flixopt/modeling.py index 1651cb12b..f7aca8755 100644 --- a/flixopt/modeling.py +++ b/flixopt/modeling.py @@ -323,14 +323,15 @@ def big_m_binary_bounds( # Upper bound: variable ≤ size * upper_factor upper_bound = model.add_constraints(variable <= size_variable * rel_upper, name=upper_bound_name) - if binary_control is not None: + if binary_control is None: + lower_bound = model.add_constraints(variable >= size_variable * rel_lower, name=lower_bound_name) + else: # Big-M lower bound: variable ≥ M*(binary-1) + size*lower_factor - big_m = size_variable.max() * rel_upper.max() # Conservative big-M + big_m = CONFIG.modeling.BIG #size_variable.max() * rel_upper.max() # Conservative big-M lower_bound = model.add_constraints( variable >= big_m * (binary_control - 1) + size_variable * rel_lower, name=lower_bound_name ) - else: - lower_bound = model.add_constraints(variable >= size_variable * rel_lower, name=lower_bound_name) + variables = {} # No new variables created constraints = {'upper_bound': upper_bound, 'lower_bound': lower_bound} @@ -482,11 +483,21 @@ def investment_sizing_pattern( size_bounds: Tuple[TemporalData, TemporalData], controlled_variables: List[linopy.Variable] = None, control_factors: List[Tuple[TemporalData, TemporalData]] = None, + state_variables: List[linopy.Variable] = None, optional: bool = False, ) -> Tuple[Dict[str, linopy.Variable], Dict[str, linopy.Constraint]]: """ Complete investment sizing pattern with optional binary decision. + Args: + model: The model to add the variables to. + name: The name of the investment variable. + size_bounds: The minimum and maximum investment size. + controlled_variables: The variables that are controlled by the investment decision. + control_factors: The control factors for the controlled variables. + state_variables: State variable defining the state of the controlled variables. + optional: Whether the investment decision is optional. + Returns: variables: {'size': size_var, 'is_invested': binary_var (if optional)} constraints: {'upper_bound': constraint, 'lower_bound': constraint, ...} @@ -497,7 +508,7 @@ def investment_sizing_pattern( # Investment size variable size_min, size_max = size_bounds variables['size'] = model.add_variables( - lower=size_min, + lower=0 if optional else size_min, upper=size_max, name=f'{name}|size', coords=model.get_coords(['year', 'scenario']), @@ -516,22 +527,30 @@ def investment_sizing_pattern( ) else: # Variable size case constraints['upper_bound'] = model.add_constraints( - variables['size'] <= variables['is_invested'] * size_max, name=f'{name}|upper_bound' + variables['size'] <= variables['is_invested'] * size_max, name=f'{name}|size|upper_bound' ) constraints['lower_bound'] = model.add_constraints( - variables['size'] >= variables['is_invested'] * max(CONFIG.modeling.EPSILON, size_min), - name=f'{name}|lower_bound', + variables['size'] >= variables['is_invested'] * np.maximum(CONFIG.modeling.EPSILON, size_min), + name=f'{name}|size|lower_bound', ) # Control dependent variables if controlled_variables and control_factors: - for i, (var, factors) in enumerate(zip(controlled_variables, control_factors)): + for i, (var, factors, state_variable) in enumerate(zip(controlled_variables, control_factors, state_variables)): + upper_bound_name = f'{var.name}|upper_bound' + lower_bound_name = f'{var.name}|lower_bound' _, control_constraints = ModelingPrimitives.big_m_binary_bounds( - model, f'{name}|control_{i}', var, variables.get('is_invested'), variables['size'], factors + model=model, + variable=var, + binary_control=state_variable, + size_variable=variables['size'], + relative_bounds=factors, + upper_bound_name=upper_bound_name, + lower_bound_name=lower_bound_name, ) # Flatten control constraints with indexed names - constraints[f'control_{i}_upper_bound'] = control_constraints['upper_bound'] - constraints[f'control_{i}_lower_bound'] = control_constraints['lower_bound'] + constraints[upper_bound_name] = control_constraints['upper_bound'] + constraints[lower_bound_name] = control_constraints['lower_bound'] return variables, constraints diff --git a/tests/test_flow.py b/tests/test_flow.py index cce10b21a..9038af1c7 100644 --- a/tests/test_flow.py +++ b/tests/test_flow.py @@ -143,8 +143,8 @@ def test_flow_invest(self, basic_flow_system_linopy): assert set(flow.model.constraints) == set( [ 'Sink(Wärme)|total_flow_hours', - 'Sink(Wärme)|lb_Sink(Wärme)|flow_rate', - 'Sink(Wärme)|ub_Sink(Wärme)|flow_rate', + 'Sink(Wärme)|flow_rate|upper_bound', + 'Sink(Wärme)|flow_rate|lower_bound', ] ) @@ -161,13 +161,13 @@ def test_flow_invest(self, basic_flow_system_linopy): ), ) assert_conequal( - model.constraints['Sink(Wärme)|lb_Sink(Wärme)|flow_rate'], + model.constraints['Sink(Wärme)|flow_rate|lower_bound'], flow.model.variables['Sink(Wärme)|flow_rate'] >= flow.model.variables['Sink(Wärme)|size'] * xr.DataArray(np.linspace(0.1, 0.5, timesteps.size), coords=(timesteps,)), ) assert_conequal( - model.constraints['Sink(Wärme)|ub_Sink(Wärme)|flow_rate'], + model.constraints['Sink(Wärme)|flow_rate|upper_bound'], flow.model.variables['Sink(Wärme)|flow_rate'] <= flow.model.variables['Sink(Wärme)|size'] * xr.DataArray(np.linspace(0.5, 1, timesteps.size), coords=(timesteps,)), @@ -194,10 +194,10 @@ def test_flow_invest_optional(self, basic_flow_system_linopy): assert set(flow.model.constraints) == set( [ 'Sink(Wärme)|total_flow_hours', - 'Sink(Wärme)|is_invested_ub', - 'Sink(Wärme)|is_invested_lb', - 'Sink(Wärme)|lb_Sink(Wärme)|flow_rate', - 'Sink(Wärme)|ub_Sink(Wärme)|flow_rate', + 'Sink(Wärme)|size|lower_bound', + 'Sink(Wärme)|size|upper_bound', + 'Sink(Wärme)|flow_rate|lower_bound', + 'Sink(Wärme)|flow_rate|upper_bound', ] ) @@ -215,13 +215,13 @@ def test_flow_invest_optional(self, basic_flow_system_linopy): ), ) assert_conequal( - model.constraints['Sink(Wärme)|lb_Sink(Wärme)|flow_rate'], + model.constraints['Sink(Wärme)|flow_rate|lower_bound'], flow.model.variables['Sink(Wärme)|flow_rate'] >= flow.model.variables['Sink(Wärme)|size'] * xr.DataArray(np.linspace(0.1, 0.5, timesteps.size), coords=(timesteps,)), ) assert_conequal( - model.constraints['Sink(Wärme)|ub_Sink(Wärme)|flow_rate'], + model.constraints['Sink(Wärme)|flow_rate|upper_bound'], flow.model.variables['Sink(Wärme)|flow_rate'] <= flow.model.variables['Sink(Wärme)|size'] * xr.DataArray(np.linspace(0.5, 1, timesteps.size), coords=(timesteps,)), @@ -229,11 +229,11 @@ def test_flow_invest_optional(self, basic_flow_system_linopy): # Is invested assert_conequal( - model.constraints['Sink(Wärme)|is_invested_ub'], + model.constraints['Sink(Wärme)|size|upper_bound'], flow.model.variables['Sink(Wärme)|size'] <= flow.model.variables['Sink(Wärme)|is_invested'] * 100, ) assert_conequal( - model.constraints['Sink(Wärme)|is_invested_lb'], + model.constraints['Sink(Wärme)|size|lower_bound'], flow.model.variables['Sink(Wärme)|size'] >= flow.model.variables['Sink(Wärme)|is_invested'] * 20, ) @@ -258,10 +258,10 @@ def test_flow_invest_optional_wo_min_size(self, basic_flow_system_linopy): assert set(flow.model.constraints) == set( [ 'Sink(Wärme)|total_flow_hours', - 'Sink(Wärme)|is_invested_ub', - 'Sink(Wärme)|is_invested_lb', - 'Sink(Wärme)|lb_Sink(Wärme)|flow_rate', - 'Sink(Wärme)|ub_Sink(Wärme)|flow_rate', + 'Sink(Wärme)|size|upper_bound', + 'Sink(Wärme)|size|lower_bound', + 'Sink(Wärme)|flow_rate|lower_bound', + 'Sink(Wärme)|flow_rate|upper_bound', ] ) @@ -279,13 +279,13 @@ def test_flow_invest_optional_wo_min_size(self, basic_flow_system_linopy): ), ) assert_conequal( - model.constraints['Sink(Wärme)|lb_Sink(Wärme)|flow_rate'], + model.constraints['Sink(Wärme)|flow_rate|lower_bound'], flow.model.variables['Sink(Wärme)|flow_rate'] >= flow.model.variables['Sink(Wärme)|size'] * xr.DataArray(np.linspace(0.1, 0.5, timesteps.size), coords=(timesteps,)), ) assert_conequal( - model.constraints['Sink(Wärme)|ub_Sink(Wärme)|flow_rate'], + model.constraints['Sink(Wärme)|flow_rate|upper_bound'], flow.model.variables['Sink(Wärme)|flow_rate'] <= flow.model.variables['Sink(Wärme)|size'] * xr.DataArray(np.linspace(0.5, 1, timesteps.size), coords=(timesteps,)), @@ -293,11 +293,11 @@ def test_flow_invest_optional_wo_min_size(self, basic_flow_system_linopy): # Is invested assert_conequal( - model.constraints['Sink(Wärme)|is_invested_ub'], + model.constraints['Sink(Wärme)|size|upper_bound'], flow.model.variables['Sink(Wärme)|size'] <= flow.model.variables['Sink(Wärme)|is_invested'] * 100, ) assert_conequal( - model.constraints['Sink(Wärme)|is_invested_lb'], + model.constraints['Sink(Wärme)|size|lower_bound'], flow.model.variables['Sink(Wärme)|size'] >= flow.model.variables['Sink(Wärme)|is_invested'] * 1e-5, ) @@ -322,8 +322,8 @@ def test_flow_invest_wo_min_size_non_optional(self, basic_flow_system_linopy): assert set(flow.model.constraints) == set( [ 'Sink(Wärme)|total_flow_hours', - 'Sink(Wärme)|lb_Sink(Wärme)|flow_rate', - 'Sink(Wärme)|ub_Sink(Wärme)|flow_rate', + 'Sink(Wärme)|flow_rate|lower_bound', + 'Sink(Wärme)|flow_rate|upper_bound', ] ) @@ -339,13 +339,13 @@ def test_flow_invest_wo_min_size_non_optional(self, basic_flow_system_linopy): ), ) assert_conequal( - model.constraints['Sink(Wärme)|lb_Sink(Wärme)|flow_rate'], + model.constraints['Sink(Wärme)|flow_rate|lower_bound'], flow.model.variables['Sink(Wärme)|flow_rate'] >= flow.model.variables['Sink(Wärme)|size'] * xr.DataArray(np.linspace(0.1, 0.5, timesteps.size), coords=(timesteps,)), ) assert_conequal( - model.constraints['Sink(Wärme)|ub_Sink(Wärme)|flow_rate'], + model.constraints['Sink(Wärme)|flow_rate|upper_bound'], flow.model.variables['Sink(Wärme)|flow_rate'] <= flow.model.variables['Sink(Wärme)|size'] * xr.DataArray(np.linspace(0.5, 1, timesteps.size), coords=(timesteps,)), @@ -935,10 +935,10 @@ def test_flow_on_invest_optional(self, basic_flow_system_linopy): 'Sink(Wärme)|on_hours_total', 'Sink(Wärme)|on_con1', 'Sink(Wärme)|on_con2', - 'Sink(Wärme)|is_invested_lb', - 'Sink(Wärme)|is_invested_ub', - 'Sink(Wärme)|lb_Sink(Wärme)|flow_rate', - 'Sink(Wärme)|ub_Sink(Wärme)|flow_rate', + 'Sink(Wärme)|size|lower_bound', + 'Sink(Wärme)|size|upper_bound', + 'Sink(Wärme)|flow_rate|lower_bound', + 'Sink(Wärme)|flow_rate|upper_bound', ] ) @@ -980,12 +980,12 @@ def test_flow_on_invest_optional(self, basic_flow_system_linopy): mega = 0.2 * 200 # Relative minimum * maximum size assert_conequal( - model.constraints['Sink(Wärme)|lb_Sink(Wärme)|flow_rate'], + model.constraints['Sink(Wärme)|flow_rate|lower_bound'], flow.model.variables['Sink(Wärme)|flow_rate'] >= flow.model.variables['Sink(Wärme)|on'] * mega + flow.model.variables['Sink(Wärme)|size'] * 0.2 - mega, ) assert_conequal( - model.constraints['Sink(Wärme)|ub_Sink(Wärme)|flow_rate'], + model.constraints['Sink(Wärme)|flow_rate|upper_bound'], flow.model.variables['Sink(Wärme)|flow_rate'] <= flow.model.variables['Sink(Wärme)|size'] * 0.8, ) @@ -1019,8 +1019,8 @@ def test_flow_on_invest_non_optional(self, basic_flow_system_linopy): 'Sink(Wärme)|on_hours_total', 'Sink(Wärme)|on_con1', 'Sink(Wärme)|on_con2', - 'Sink(Wärme)|lb_Sink(Wärme)|flow_rate', - 'Sink(Wärme)|ub_Sink(Wärme)|flow_rate', + 'Sink(Wärme)|flow_rate|lower_bound', + 'Sink(Wärme)|flow_rate|upper_bound', ] ) @@ -1062,12 +1062,12 @@ def test_flow_on_invest_non_optional(self, basic_flow_system_linopy): mega = 0.2 * 200 # Relative minimum * maximum size assert_conequal( - model.constraints['Sink(Wärme)|lb_Sink(Wärme)|flow_rate'], + model.constraints['Sink(Wärme)|flow_rate|lower_bound'], flow.model.variables['Sink(Wärme)|flow_rate'] >= flow.model.variables['Sink(Wärme)|on'] * mega + flow.model.variables['Sink(Wärme)|size'] * 0.2 - mega, ) assert_conequal( - model.constraints['Sink(Wärme)|ub_Sink(Wärme)|flow_rate'], + model.constraints['Sink(Wärme)|flow_rate|upper_bound'], flow.model.variables['Sink(Wärme)|flow_rate'] <= flow.model.variables['Sink(Wärme)|size'] * 0.8, ) From 1ad74ce43b507872bfe16533a3fe3d56f942e2ab Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 18 Jul 2025 13:17:45 +0200 Subject: [PATCH 203/448] Improve --- flixopt/elements.py | 9 +-- flixopt/features.py | 133 ++++++++++++++++++++++++++++---------------- flixopt/modeling.py | 57 ++++++++++--------- 3 files changed, 118 insertions(+), 81 deletions(-) diff --git a/flixopt/elements.py b/flixopt/elements.py index 43907b07a..440ac6de4 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -334,10 +334,11 @@ def do_modeling(self): OnOffModel( model=self._model, label_of_element=self.label_of_element, - on_off_parameters=self.element.on_off_parameters, - defining_variables=[self.flow_rate], - defining_bounds=[self.flow_rate_bounds_on], - previous_values=[self.element.previous_flow_rate], + parameters=self.element.on_off_parameters, + flow_rates=[self.flow_rate], + flow_rate_bounds=[self.flow_rate_bounds_on], + previous_flow_rates=[self.element.previous_flow_rate], + label_of_model=self.label_of_element, ), 'on_off', ) diff --git a/flixopt/features.py b/flixopt/features.py index 49635ad3d..52c1302c2 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -131,58 +131,95 @@ def __init__( self, model: FlowSystemModel, label_of_element: str, - on_off_parameters: OnOffParameters, - defining_variables: List[linopy.Variable], - defining_bounds: List[Tuple[TemporalData, TemporalData]], - previous_values: List[Optional[TemporalData]], - label: Optional[str] = None, + parameters: OnOffParameters, + flow_rates: List[linopy.Variable], + flow_rate_bounds: List[Tuple[TemporalData, TemporalData]], + previous_flow_rates: List[Optional[TemporalData]], + label_of_model: Optional[str] = None, ): - super().__init__(model, label_of_element, on_off_parameters, label) - - self._defining_variables = defining_variables - self._defining_bounds = defining_bounds - self._previous_values = previous_values + super().__init__(model, label_of_element, parameters=parameters, label_of_model=label_of_model) + self._flow_rates = flow_rates + self._flow_rate_bounds = flow_rate_bounds + self._previous_flow_rates = previous_flow_rates def create_variables_and_constraints(self): - # Use factory patterns - variables, constraints = ModelingPatterns.operational_binary_control_pattern( - model=self._model, - name=self.label_full, - controlled_variables=self._defining_variables, - variable_bounds=self._defining_bounds, - use_complement=self.parameters.use_off, - track_total_duration=True, - track_switches=self.parameters.use_switch_on, - previous_state=self._get_previous_state(), - duration_bounds=(self.parameters.on_hours_total_min, self.parameters.on_hours_total_max), - track_consecutive_on=self.parameters.use_consecutive_on_hours, - consecutive_on_bounds=(self.parameters.consecutive_on_hours_min, self.parameters.consecutive_on_hours_max), - previous_on_duration=self._get_previous_on_duration(), - track_consecutive_off=self.parameters.use_consecutive_off_hours, - consecutive_off_bounds=( - self.parameters.consecutive_off_hours_min, - self.parameters.consecutive_off_hours_max, - ), - previous_off_duration=self._get_previous_off_duration(), + variables = {} + constraints = {} + + # 1. Main binary state using existing pattern + state_vars, state_constraints = ModelingPrimitives.binary_state_pair(self._model, self.label_of_model, use_complement=self.parameters.use_off) + variables.update(state_vars) + constraints.update(state_constraints) + + # 2. Control variables - use big_m_binary_bounds pattern for consistency + for i, (flow_rate, (lower_bound, upper_bound)) in enumerate(zip(self._flow_rates, self._flow_rate_bounds)): + suffix = f'_{i}' if len(self._flow_rates) > 1 else '' + # Use the big_m pattern but without binary control (None) + _, control_constraints = ModelingPrimitives.big_m_binary_bounds( + model=self._model, + variable=flow_rate, + binary_control=None, + size_variable=variables['on'], + relative_bounds=(lower_bound, upper_bound), + upper_bound_name=f'{variables['on'].name}|ub{suffix}', + lower_bound_name=f'{variables['on'].name}|lb{suffix}', + ) + constraints[f'ub_{i}'] = control_constraints['upper_bound'] + constraints[f'lb_{i}'] = control_constraints['lower_bound'] + + # 3. Total duration tracking using existing pattern + duration_expr = (variables['on'] * self._model.hours_per_step).sum('time') + duration_vars, duration_constraints = ModelingPrimitives.expression_tracking_variable( + self._model, f'{self.label_of_model}|on_hours_total', duration_expr, + (self.parameters.on_hours_total_min if self.parameters.on_hours_total_min is not None else 0, + self.parameters.on_hours_total_max if self.parameters.on_hours_total_max is not None else np.inf),#TODO: self._model.hours_per_step.sum('time').item() + self._get_previous_on_duration()) ) + variables['on_hours_total'] = duration_vars['tracker'] + constraints['on_hours_total'] = duration_constraints['tracking'] + + # 4. Switch tracking using existing pattern + if self.parameters.use_switch_on: + switch_vars, switch_constraints = ModelingPrimitives.state_transition_variables( + self._model, f'{self.label_of_model}|switches', variables['on'], + previous_state=ModelingUtilities.get_most_recent_state(self._previous_flow_rates) + ) + variables.update(switch_vars) + for switch_name, switch_constraint in switch_constraints.items(): + constraints[f'switch_{switch_name}'] = switch_constraint + + # 5. Consecutive on duration using existing pattern + if self.parameters.use_consecutive_on_hours: + consecutive_on_vars, consecutive_on_constraints = ModelingPrimitives.consecutive_duration_tracking( + self._model, + f'{self.label_of_model}|consecutive_on', + variables['on'], + minimum_duration=self.parameters.consecutive_on_hours_min, + maximum_duration=self.parameters.consecutive_on_hours_max, + previous_duration=ModelingUtilities.compute_previous_on_duration(self._previous_flow_rates, self._model.hours_per_step), + ) + variables['consecutive_on_duration'] = consecutive_on_vars['duration'] + for cons_name, cons_constraint in consecutive_on_constraints.items(): + constraints[f'consecutive_on_{cons_name}'] = cons_constraint + + # 6. Consecutive off duration using existing pattern + if self.parameters.use_consecutive_off_hours: + consecutive_off_vars, consecutive_off_constraints = ModelingPrimitives.consecutive_duration_tracking( + self._model, + f'{self.label_of_model}|consecutive_off', + variables['off'], + minimum_duration=self.parameters.consecutive_off_hours_min, + maximum_duration=self.parameters.consecutive_off_hours_max, + previous_duration=ModelingUtilities.compute_previous_off_duration(self._previous_flow_rates, self._model.hours_per_step), + ) + variables['consecutive_off_duration'] = consecutive_off_vars['duration'] + for cons_name, cons_constraint in consecutive_off_constraints.items(): + constraints[f'consecutive_off_{cons_name}'] = cons_constraint - # Register all variables (stored in Model's variable tracking) - self.add(variables['on'], 'on') - if 'off' in variables: - self.add(variables['off'], 'off') - if 'total_duration' in variables: - self.add(variables['total_duration'], 'total_duration') - if 'switch_on' in variables: - self.add(variables['switch_on'], 'switch_on') - self.add(variables['switch_off'], 'switch_off') - if 'consecutive_on_duration' in variables: - self.add(variables['consecutive_on_duration'], 'consecutive_on_hours') - if 'consecutive_off_duration' in variables: - self.add(variables['consecutive_off_duration'], 'consecutive_off_hours') - - # Register all constraints + # Register all constraints and variables for constraint_name, constraint in constraints.items(): self.add(constraint, constraint_name) + for variable_name, variable in variables.items(): + self.add(variable, variable_name) # Properties access variables from Model's tracking system @property @@ -249,14 +286,14 @@ def add_effects(self): def _get_previous_on_duration(self): hours_per_step = self._model.hours_per_step.isel(time=0).values.flatten()[0] - return ModelingUtilities.compute_previous_on_duration(self._previous_values, hours_per_step) + return ModelingUtilities.compute_previous_on_duration(self._previous_flow_rates, hours_per_step) def _get_previous_off_duration(self): hours_per_step = self._model.hours_per_step.isel(time=0).values.flatten()[0] - return ModelingUtilities.compute_previous_off_duration(self._previous_values, hours_per_step) + return ModelingUtilities.compute_previous_off_duration(self._previous_flow_rates, hours_per_step) def _get_previous_state(self): - return ModelingUtilities.get_most_recent_state(self._previous_values) + return ModelingUtilities.get_most_recent_state(self._previous_flow_rates) class PieceModel(Model): diff --git a/flixopt/modeling.py b/flixopt/modeling.py index f7aca8755..64f0164d6 100644 --- a/flixopt/modeling.py +++ b/flixopt/modeling.py @@ -150,7 +150,7 @@ class ModelingPrimitives: @staticmethod def binary_state_pair( - model: FlowSystemModel, name: str, coords: List[str] = None + model: FlowSystemModel, name: str, coords: List[str] = None, use_complement: bool = True ) -> Tuple[Dict[str, linopy.Variable], Dict[str, linopy.Constraint]]: """ Creates complementary binary variables with completeness constraint. @@ -166,15 +166,16 @@ def binary_state_pair( coords = coords or ['time'] on = model.add_variables(binary=True, name=f'{name}|on', coords=model.get_coords(coords)) - off = model.add_variables(binary=True, name=f'{name}|off', coords=model.get_coords(coords)) - - # Constraint: on + off = 1 - complementary = model.add_constraints(on + off == 1, name=f'{name}|complementary') + if use_complement: + off = model.add_variables(binary=True, name=f'{name}|off', coords=model.get_coords(coords)) - variables = {'on': on, 'off': off} - constraints = {'complementary': complementary} + # Constraint: on + off = 1 + complementary = model.add_constraints(on + off == 1, name=f'{name}|complementary') - return variables, constraints + variables = {'on': on, 'off': off} + constraints = {'complementary': complementary} + return variables, constraints + return {'on': on}, {} @staticmethod def proportionally_bounded_variable( @@ -573,20 +574,12 @@ def operational_binary_control_pattern( previous_off_duration: TemporalData = 0, ) -> Tuple[Dict[str, linopy.Variable], Dict[str, linopy.Constraint]]: """ - Enhanced operational binary control with consecutive duration tracking. - - New Args: - track_consecutive_on: Whether to track consecutive on duration - consecutive_on_bounds: (min_duration, max_duration) for consecutive on - previous_on_duration: Previous consecutive on duration - track_consecutive_off: Whether to track consecutive off duration - consecutive_off_bounds: (min_duration, max_duration) for consecutive off - previous_off_duration: Previous consecutive off duration + Enhanced operational binary control using composable patterns. """ variables = {} constraints = {} - # Main binary state (existing logic) + # 1. Main binary state using existing pattern if use_complement: state_vars, state_constraints = ModelingPrimitives.binary_state_pair(model, name) variables.update(state_vars) @@ -594,25 +587,31 @@ def operational_binary_control_pattern( else: variables['on'] = model.add_variables(binary=True, name=f'{name}|on', coords=model.get_coords(['time'])) - # Control variables (existing logic) + # 2. Control variables - use big_m_binary_bounds pattern for consistency for i, (var, (lower_bound, upper_bound)) in enumerate(zip(controlled_variables, variable_bounds)): - constraints[f'control_{i}_lower'] = model.add_constraints( - variables['on'] * np.maximum(lower_bound, CONFIG.modeling.EPSILON) <= var, name=f'{name}|control_{i}_lower' - ) - constraints[f'control_{i}_upper'] = model.add_constraints( - var <= variables['on'] * upper_bound, name=f'{name}|control_{i}_upper' + # Use the big_m pattern but without binary control (None) + _, control_constraints = ModelingPrimitives.big_m_binary_bounds( + model=model, + variable=var, + binary_control=variables['on'], # The on state controls the variables + size_variable=1, # No size scaling, just on/off + relative_bounds=(lower_bound, upper_bound), + upper_bound_name=f'{name}|control_{i}_upper', + lower_bound_name=f'{name}|control_{i}_lower', ) + constraints[f'control_{i}_upper'] = control_constraints['upper_bound'] + constraints[f'control_{i}_lower'] = control_constraints['lower_bound'] - # Total duration tracking (existing logic) + # 3. Total duration tracking using existing pattern if track_total_duration: duration_expr = (variables['on'] * model.hours_per_step).sum('time') duration_vars, duration_constraints = ModelingPrimitives.expression_tracking_variable( - model, f'{name}|duration', duration_expr, duration_bounds + model, f'{name}|on_hours_total', duration_expr, duration_bounds ) variables['total_duration'] = duration_vars['tracker'] constraints['duration_tracking'] = duration_constraints['tracking'] - # Switch tracking (existing logic) + # 4. Switch tracking using existing pattern if track_switches: switch_vars, switch_constraints = ModelingPrimitives.state_transition_variables( model, f'{name}|switches', variables['on'], previous_state @@ -621,7 +620,7 @@ def operational_binary_control_pattern( for switch_name, switch_constraint in switch_constraints.items(): constraints[f'switch_{switch_name}'] = switch_constraint - # NEW: Consecutive on duration tracking + # 5. Consecutive on duration using existing pattern if track_consecutive_on: min_on, max_on = consecutive_on_bounds consecutive_on_vars, consecutive_on_constraints = ModelingPrimitives.consecutive_duration_tracking( @@ -636,7 +635,7 @@ def operational_binary_control_pattern( for cons_name, cons_constraint in consecutive_on_constraints.items(): constraints[f'consecutive_on_{cons_name}'] = cons_constraint - # NEW: Consecutive off duration tracking + # 6. Consecutive off duration using existing pattern if track_consecutive_off and 'off' in variables: min_off, max_off = consecutive_off_bounds consecutive_off_vars, consecutive_off_constraints = ModelingPrimitives.consecutive_duration_tracking( From d1408a48e323bf06f6412508748bfc55143f873f Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 18 Jul 2025 13:56:58 +0200 Subject: [PATCH 204/448] Tyring to improve the Methods for bounding variables in different scenarios --- flixopt/modeling.py | 395 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 386 insertions(+), 9 deletions(-) diff --git a/flixopt/modeling.py b/flixopt/modeling.py index 64f0164d6..019652a0b 100644 --- a/flixopt/modeling.py +++ b/flixopt/modeling.py @@ -293,15 +293,70 @@ def state_transition_variables( return variables, constraints + @staticmethod + def proportional_bounds_with_binary_control( + model: FlowSystemModel, + bounded_variable, + binary_gate: linopy.Variable, + gate_bounds: Tuple[TemporalData, TemporalData], + upper_bound_name: str, + lower_bound_name: str, + scaling_variable=None, + relative_gate_bounds: Tuple[float, float] = None, + ) -> Tuple[Dict, Dict[str, linopy.Constraint]]: + """ + Creates proportional bounds with optional scaling and binary control. + + Args: + bounded_variable: Variable to apply bounds to + relative_bounds: (min_factor, max_factor) - either absolute bounds or factors for scaling + upper_bound_name: Name for the upper bound constraint + lower_bound_name: Name for the lower bound constraint + scaling_variable: Optional variable to scale bounds by (e.g., investment_size) + binary_gate: Optional binary variable that can disable lower bound when 0 + scaling_bounds: Optional (min_value, max_value) of scaling_variable for tighter big-M + + Returns: + variables: {} (no new variables created) + constraints: {'upper_bound': constraint, 'lower_bound': constraint} + """ + + # Determine base expressions for bounds + if scaling_variable is not None: + upper_expr = scaling_variable * relative_gate_bounds[1] + lower_expr = scaling_variable * relative_gate_bounds[0] + else: + upper_expr = gate_bounds[1] + lower_expr = gate_bounds[0] + + # Upper bound constraint + upper_bound = model.add_constraints(bounded_variable <= upper_expr, name=upper_bound_name) + + # Lower bound constraint + if binary_gate is None: + lower_bound = model.add_constraints(bounded_variable >= lower_expr, name=lower_bound_name) + else: + # Calculate tight big-M using scaling bounds if provided + big_m = np.minimum(absolute_bounds[1], CONFIG.modeling.BIG) if absolute_bounds is not None else CONFIG.modeling.BIG + + lower_bound = model.add_constraints( + bounded_variable >= big_m * (binary_gate - 1) + lower_expr, name=lower_bound_name + ) + + variables = {} + constraints = {'upper_bound': upper_bound, 'lower_bound': lower_bound} + return variables, constraints + @staticmethod def big_m_binary_bounds( model: FlowSystemModel, - variable, - binary_control, - size_variable, + bounded_variable: linopy.Variable, + scaling_variable: linopy.Variable, + binary_gate: linopy.Variable, relative_bounds: Tuple[TemporalData, TemporalData], upper_bound_name: str, lower_bound_name: str, + big_m: float = CONFIG.modeling.BIG, ) -> Tuple[Dict, Dict[str, linopy.Constraint]]: """ Creates bounds controlled by both binary and continuous variables. @@ -322,23 +377,345 @@ def big_m_binary_bounds( rel_lower, rel_upper = relative_bounds # Upper bound: variable ≤ size * upper_factor - upper_bound = model.add_constraints(variable <= size_variable * rel_upper, name=upper_bound_name) + upper_bound = model.add_constraints(bounded_variable <= scaling_variable * rel_upper, name=upper_bound_name) - if binary_control is None: - lower_bound = model.add_constraints(variable >= size_variable * rel_lower, name=lower_bound_name) + if binary_gate is None: + lower_bound = model.add_constraints(bounded_variable >= scaling_variable * rel_lower, name=lower_bound_name) else: # Big-M lower bound: variable ≥ M*(binary-1) + size*lower_factor - big_m = CONFIG.modeling.BIG #size_variable.max() * rel_upper.max() # Conservative big-M lower_bound = model.add_constraints( - variable >= big_m * (binary_control - 1) + size_variable * rel_lower, name=lower_bound_name + bounded_variable >= big_m * (binary_gate - 1) + scaling_variable * rel_lower, name=lower_bound_name ) - variables = {} # No new variables created constraints = {'upper_bound': upper_bound, 'lower_bound': lower_bound} return variables, constraints + @staticmethod + def binary_controlled_bounds( + model: FlowSystemModel, + variable: linopy.Variable, + bounds: Tuple[TemporalData, TemporalData], + binary_control: linopy.Variable, + upper_bound_name: str, + lower_bound_name: str, + ) -> Tuple[Dict, Dict[str, linopy.Constraint]]: + """ + Creates bounds controlled by a binary variable with epsilon handling. + + Mathematical formulation: + binary * max(ε, lower_bound) ≤ variable ≤ binary * upper_bound + + When binary = 1: normal bounds apply + When binary = 0: variable is forced to 0 + + Example use case - Investment bounds: + β_inv * max(ε, V^L) ≤ V ≤ β_inv * V^U + where β_inv is investment decision, V is investment size + + Args: + variable: Variable to be bounded + bounds: (lower_bound, upper_bound) absolute bounds + binary_control: Binary variable controlling the bounds + upper_bound_name: Name for upper bound constraint + lower_bound_name: Name for lower bound constraint + + Returns: + variables: {} (no new variables created) + constraints: {'upper_bound': constraint, 'lower_bound': constraint} + """ + lower_bound, upper_bound = bounds + + # Apply epsilon to lower bound to distinguish 0 from "very small positive" + epsilon_lower = np.maximum(CONFIG.modeling.EPSILON, lower_bound) + + upper_constraint = model.add_constraints(variable <= binary_control * upper_bound, name=upper_bound_name) + lower_constraint = model.add_constraints(variable >= binary_control * epsilon_lower, name=lower_bound_name) + + variables = {} + constraints = {'upper_bound': upper_constraint, 'lower_bound': lower_constraint} + return variables, constraints + + @staticmethod + def binary_scaled_bounds( + model: FlowSystemModel, + variable: linopy.Variable, + scaling_variable: linopy.Variable, + relative_bounds: Tuple[TemporalData, TemporalData], + binary_control: linopy.Variable, + upper_bound_name: str, + lower_bound_name: str, + ) -> Tuple[Dict, Dict[str, linopy.Constraint]]: + """ + Creates scaled bounds controlled by a binary variable. + + Mathematical formulation: + binary * max(ε, scaling * lower_factor) ≤ variable ≤ binary * scaling * upper_factor + + When binary = 1: variable bounded by scaled factors + When binary = 0: variable forced to 0 + + Example use case - Fixed size with on/off control: + β_on(t) * max(ε, P * p_rel^L(t)) ≤ p(t) ≤ β_on(t) * P * p_rel^U(t) + where β_on is on/off state, P is fixed size, p is flow rate + + Args: + variable: Variable to be bounded + scaling_variable: Variable to scale the bounds by + relative_bounds: (lower_factor, upper_factor) relative to scaling variable + binary_control: Binary variable controlling the bounds + upper_bound_name: Name for upper bound constraint + lower_bound_name: Name for lower bound constraint + + Returns: + variables: {} (no new variables created) + constraints: {'upper_bound': constraint, 'lower_bound': constraint} + """ + rel_lower, rel_upper = relative_bounds + + # Calculate scaled expressions + upper_expr = scaling_variable * rel_upper + lower_expr = scaling_variable * rel_lower + + # Apply epsilon to lower expression + epsilon_lower = np.maximum(CONFIG.modeling.EPSILON, lower_expr) + + upper_constraint = model.add_constraints(variable <= binary_control * upper_expr, name=upper_bound_name) + lower_constraint = model.add_constraints(variable >= binary_control * epsilon_lower, name=lower_bound_name) + + variables = {} + constraints = {'upper_bound': upper_constraint, 'lower_bound': lower_constraint} + return variables, constraints + + @staticmethod + def big_m_dual_control_bounds( + model: FlowSystemModel, + variable: linopy.Variable, + scaling_variable: linopy.Variable, + relative_bounds: Tuple[TemporalData, TemporalData], + binary_control: linopy.Variable, + scaling_bounds: Tuple[TemporalData, TemporalData], + constraint_name_prefix: str, + ) -> Tuple[Dict, Dict[str, linopy.Constraint]]: + """ + Creates bounds with both binary and continuous variable control using big-M formulation. + + Mathematical formulation: + # Binary control with big-M bounds: + binary * max(ε, scaling_min * lower_factor) ≤ variable ≤ binary * M + # Continuous scaling bounds: + M * (binary - 1) + scaling * lower_factor ≤ variable ≤ scaling * upper_factor + + Where M = scaling_max * upper_factor + + This maintains linearity when both binary and continuous controls are present. + + Example use case - Variable investment size with on/off control: + β_on(t) * max(ε, P^L * p_rel^L(t)) ≤ p(t) ≤ β_on(t) * M(t) + M(t) * (β_on(t) - 1) + P * p_rel^L(t) ≤ p(t) ≤ P * p_rel^U(t) + where β_on is on/off state, P is variable investment size, p is flow rate + + Args: + variable: Variable to be bounded + scaling_variable: Continuous variable that scales the bounds + relative_bounds: (lower_factor, upper_factor) relative to scaling variable + binary_control: Binary variable for on/off control + scaling_bounds: (scaling_min, scaling_max) bounds of the scaling variable + constraint_name_prefix: Prefix for constraint names + + Returns: + variables: {} (no new variables created) + constraints: { + 'binary_lower': binary-controlled lower bound, + 'binary_upper': binary-controlled upper bound, + 'scaling_lower': scaling-controlled lower bound, + 'scaling_upper': scaling-controlled upper bound + } + """ + rel_lower, rel_upper = relative_bounds + scaling_min, scaling_max = scaling_bounds + + # Calculate big-M as maximum possible value + big_m = rel_upper * scaling_max + + # Binary-controlled lower bound with epsilon + epsilon_lower = np.maximum(CONFIG.modeling.EPSILON, rel_lower * scaling_min) + binary_lower = model.add_constraints( + binary_control * epsilon_lower <= variable, name=f'{constraint_name_prefix}|binary_lower' + ) + + # Binary-controlled upper bound with big-M + binary_upper = model.add_constraints( + variable <= binary_control * big_m, name=f'{constraint_name_prefix}|binary_upper' + ) + + # Scaling-controlled lower bound with big-M relaxation + scaling_lower = model.add_constraints( + big_m * (binary_control - 1) + scaling_variable * rel_lower <= variable, + name=f'{constraint_name_prefix}|scaling_lower', + ) + + # Scaling-controlled upper bound + scaling_upper = model.add_constraints( + variable <= scaling_variable * rel_upper, name=f'{constraint_name_prefix}|scaling_upper' + ) + + variables = {} + constraints = { + 'binary_lower': binary_lower, + 'binary_upper': binary_upper, + 'scaling_lower': scaling_lower, + 'scaling_upper': scaling_upper, + } + return variables, constraints + + @staticmethod + def scaled_bounds( + model: FlowSystemModel, + variable: linopy.Variable, + scaling_variable: linopy.Variable, + relative_bounds: Tuple[TemporalData, TemporalData], + upper_bound_name: str, + lower_bound_name: str, + ) -> Tuple[Dict, Dict[str, linopy.Constraint]]: + """ + Creates simple bounds scaled by another variable. + + Mathematical formulation: + scaling * lower_factor ≤ variable ≤ scaling * upper_factor + + Example use case - Flow rate bounded by size: + P * p_rel^L(t) ≤ p(t) ≤ P * p_rel^U(t) + where P is size, p is flow rate + + Args: + variable: Variable to be bounded + scaling_variable: Variable to scale the bounds by + relative_bounds: (lower_factor, upper_factor) relative to scaling variable + upper_bound_name: Name for upper bound constraint + lower_bound_name: Name for lower bound constraint + + Returns: + variables: {} (no new variables created) + constraints: {'upper_bound': constraint, 'lower_bound': constraint} + """ + rel_lower, rel_upper = relative_bounds + + upper_constraint = model.add_constraints(variable <= scaling_variable * rel_upper, name=upper_bound_name) + lower_constraint = model.add_constraints(variable >= scaling_variable * rel_lower, name=lower_bound_name) + + variables = {} + constraints = {'upper_bound': upper_constraint, 'lower_bound': lower_constraint} + return variables, constraints + + @staticmethod + def auto_bounds( + model: FlowSystemModel, + variable: linopy.Variable, + bounds: Tuple[TemporalData, TemporalData], + upper_bound_name: str, + lower_bound_name: str, + scaling_variable: linopy.Variable = None, + binary_control: linopy.Variable = None, + scaling_bounds: Tuple[TemporalData, TemporalData] = None, + constraint_name_prefix: str = None, + ) -> Tuple[Dict, Dict[str, linopy.Constraint]]: + """ + Automatically selects the appropriate bounds method based on provided parameters. + + Parameter combinations and resulting method calls: + + 1. Only bounds → Simple absolute bounds: + lower_bound ≤ variable ≤ upper_bound + + 2. bounds + scaling_variable → scaled_bounds(): + scaling * lower_factor ≤ variable ≤ scaling * upper_factor + + 3. bounds + binary_control → binary_controlled_bounds(): + binary * max(ε, lower_bound) ≤ variable ≤ binary * upper_bound + + 4. bounds + scaling_variable + binary_control → binary_scaled_bounds(): + binary * max(ε, scaling * lower_factor) ≤ variable ≤ binary * scaling * upper_factor + + 5. All parameters → big_m_dual_control_bounds(): + Complex big-M formulation for binary + variable scaling control + + Args: + variable: Variable to be bounded + bounds: (lower, upper) - absolute bounds or relative factors if scaling + upper_bound_name: Name for upper bound constraint + lower_bound_name: Name for lower bound constraint + scaling_variable: Optional variable to scale bounds by + binary_control: Optional binary variable for on/off control + scaling_bounds: Required if using big-M (case 5), bounds of scaling variable + constraint_name_prefix: Required if using big-M (case 5) + + Returns: + Same as the underlying primitive method + + Raises: + ValueError: If big-M case is detected but required parameters are missing + """ + + # Case 5: Big-M dual control (most complex) + if scaling_variable is not None and binary_control is not None and scaling_bounds is not None: + if constraint_name_prefix is None: + raise ValueError('constraint_name_prefix is required when using big-M dual control') + + return ModelingPrimitives.big_m_dual_control_bounds( + model=model, + variable=variable, + scaling_variable=scaling_variable, + relative_bounds=bounds, + binary_control=binary_control, + scaling_bounds=scaling_bounds, + constraint_name_prefix=constraint_name_prefix, + ) + + # Case 4: Binary + scaling (fixed size with on/off) + elif scaling_variable is not None and binary_control is not None: + return ModelingPrimitives.binary_scaled_bounds( + model=model, + variable=variable, + scaling_variable=scaling_variable, + relative_bounds=bounds, + binary_control=binary_control, + upper_bound_name=upper_bound_name, + lower_bound_name=lower_bound_name, + ) + + # Case 3: Binary only (investment decision) + elif binary_control is not None: + return ModelingPrimitives.binary_controlled_bounds( + model=model, + variable=variable, + bounds=bounds, + binary_control=binary_control, + upper_bound_name=upper_bound_name, + lower_bound_name=lower_bound_name, + ) + + # Case 2: Scaling only (size-dependent bounds) + elif scaling_variable is not None: + return ModelingPrimitives.scaled_bounds( + model=model, + variable=variable, + scaling_variable=scaling_variable, + relative_bounds=bounds, + upper_bound_name=upper_bound_name, + lower_bound_name=lower_bound_name, + ) + + # Case 1: Simple absolute bounds + else: + upper_constraint = model.add_constraints(variable <= bounds[1], name=upper_bound_name) + lower_constraint = model.add_constraints(variable >= bounds[0], name=lower_bound_name) + + variables = {} + constraints = {'upper_bound': upper_constraint, 'lower_bound': lower_constraint} + return variables, constraints + @staticmethod def consecutive_duration_tracking( model: FlowSystemModel, From ab000ca804d31f1aa9ef938db56a4fdd6536aeb5 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 18 Jul 2025 17:33:21 +0200 Subject: [PATCH 205/448] Improve BoundingPatterns --- flixopt/modeling.py | 908 +++++++++++++++++++++++--------------------- 1 file changed, 484 insertions(+), 424 deletions(-) diff --git a/flixopt/modeling.py b/flixopt/modeling.py index 019652a0b..a443c3a74 100644 --- a/flixopt/modeling.py +++ b/flixopt/modeling.py @@ -193,7 +193,7 @@ def proportionally_bounded_variable( Returns: variables: {'variable': bounded_var} - constraints: {'lower_bound': constraint, 'upper_bound': constraint} + constraints: {'lb': constraint, 'ub': constraint} """ coords = coords or ['time'] variable = model.add_variables(name=f'{name}|bounded', coords=model.get_coords(coords)) @@ -209,7 +209,7 @@ def proportionally_bounded_variable( ) variables = {'variable': variable} - constraints = {'lower_bound': lower_bound, 'upper_bound': upper_bound} + constraints = {'lb': lower_bound, 'ub': upper_bound} return variables, constraints @@ -294,561 +294,639 @@ def state_transition_variables( return variables, constraints @staticmethod - def proportional_bounds_with_binary_control( + def consecutive_duration_tracking( model: FlowSystemModel, - bounded_variable, - binary_gate: linopy.Variable, - gate_bounds: Tuple[TemporalData, TemporalData], - upper_bound_name: str, - lower_bound_name: str, - scaling_variable=None, - relative_gate_bounds: Tuple[float, float] = None, - ) -> Tuple[Dict, Dict[str, linopy.Constraint]]: + name: str, + state_variable: linopy.Variable, + minimum_duration: Optional[TemporalData] = None, + maximum_duration: Optional[TemporalData] = None, + previous_duration: TemporalData = 0, + ) -> Tuple[Dict[str, linopy.Variable], Dict[str, linopy.Constraint]]: """ - Creates proportional bounds with optional scaling and binary control. + Creates consecutive duration tracking for a binary state variable. + + Mathematical formulation: + duration[t] ≤ state[t] * M ∀t + duration[t+1] ≤ duration[t] + hours_per_step[t] ∀t + duration[t+1] ≥ duration[t] + hours_per_step[t] + (state[t+1] - 1) * M ∀t + duration[0] = (hours_per_step[0] + previous_duration) * state[0] + + If minimum_duration provided: + duration[t] ≥ (state[t-1] - state[t]) * minimum_duration[t-1] ∀t > 0 Args: - bounded_variable: Variable to apply bounds to - relative_bounds: (min_factor, max_factor) - either absolute bounds or factors for scaling - upper_bound_name: Name for the upper bound constraint - lower_bound_name: Name for the lower bound constraint - scaling_variable: Optional variable to scale bounds by (e.g., investment_size) - binary_gate: Optional binary variable that can disable lower bound when 0 - scaling_bounds: Optional (min_value, max_value) of scaling_variable for tighter big-M + state_variable: Binary state variable to track duration for + minimum_duration: Optional minimum consecutive duration + maximum_duration: Optional maximum consecutive duration + previous_duration: Duration from before first timestep Returns: - variables: {} (no new variables created) - constraints: {'upper_bound': constraint, 'lower_bound': constraint} + variables: {'duration': duration_var} + constraints: {'ub': constraint, 'forward': constraint, 'backward': constraint, ...} """ + hours_per_step = model.hours_per_step + mega = hours_per_step.sum('time') + previous_duration # Big-M value - # Determine base expressions for bounds - if scaling_variable is not None: - upper_expr = scaling_variable * relative_gate_bounds[1] - lower_expr = scaling_variable * relative_gate_bounds[0] - else: - upper_expr = gate_bounds[1] - lower_expr = gate_bounds[0] + # Duration variable + duration = model.add_variables( + lower=0, + upper=maximum_duration if maximum_duration is not None else mega, + coords=model.get_coords(['time']), + name=f'{name}|duration', + ) - # Upper bound constraint - upper_bound = model.add_constraints(bounded_variable <= upper_expr, name=upper_bound_name) + constraints = {} - # Lower bound constraint - if binary_gate is None: - lower_bound = model.add_constraints(bounded_variable >= lower_expr, name=lower_bound_name) - else: - # Calculate tight big-M using scaling bounds if provided - big_m = np.minimum(absolute_bounds[1], CONFIG.modeling.BIG) if absolute_bounds is not None else CONFIG.modeling.BIG + # Upper bound: duration[t] ≤ state[t] * M + constraints['ub'] = model.add_constraints( + duration <= state_variable * mega, name=f'{name}|duration_upper_bound' + ) + + # Forward constraint: duration[t+1] ≤ duration[t] + hours_per_step[t] + constraints['forward'] = model.add_constraints( + duration.isel(time=slice(1, None)) + <= duration.isel(time=slice(None, -1)) + hours_per_step.isel(time=slice(None, -1)), + name=f'{name}|duration_forward', + ) - lower_bound = model.add_constraints( - bounded_variable >= big_m * (binary_gate - 1) + lower_expr, name=lower_bound_name + # Backward constraint: duration[t+1] ≥ duration[t] + hours_per_step[t] + (state[t+1] - 1) * M + constraints['backward'] = model.add_constraints( + duration.isel(time=slice(1, None)) + >= duration.isel(time=slice(None, -1)) + + hours_per_step.isel(time=slice(None, -1)) + + (state_variable.isel(time=slice(1, None)) - 1) * mega, + name=f'{name}|duration_backward', + ) + + # Initial condition: duration[0] = (hours_per_step[0] + previous_duration) * state[0] + constraints['initial'] = model.add_constraints( + duration.isel(time=0) + == (hours_per_step.isel(time=0) + previous_duration) * state_variable.isel(time=0), + name=f'{name}|duration_initial', + ) + + # Minimum duration constraint if provided + if minimum_duration is not None: + constraints['minimum'] = model.add_constraints( + duration.isel(time=slice(1, None)) + >= (state_variable.isel(time=slice(None, -1)) - state_variable.isel(time=slice(1, None))) + * minimum_duration.isel(time=slice(None, -1)), + name=f'{name}|duration_minimum', ) - variables = {} - constraints = {'upper_bound': upper_bound, 'lower_bound': lower_bound} + # Handle initial condition for minimum duration + if previous_duration > 0 and previous_duration < minimum_duration.isel(time=0).max(): + constraints['initial_minimum'] = model.add_constraints( + state_variable.isel(time=0) == 1, name=f'{name}|duration_initial_minimum' + ) + + variables = {'duration': duration} + return variables, constraints @staticmethod - def big_m_binary_bounds( - model: FlowSystemModel, - bounded_variable: linopy.Variable, - scaling_variable: linopy.Variable, - binary_gate: linopy.Variable, - relative_bounds: Tuple[TemporalData, TemporalData], - upper_bound_name: str, - lower_bound_name: str, - big_m: float = CONFIG.modeling.BIG, + def mutual_exclusivity_constraint( + model: FlowSystemModel, name: str, binary_variables: List[linopy.Variable], tolerance: float = 1.1 ) -> Tuple[Dict, Dict[str, linopy.Constraint]]: """ - Creates bounds controlled by both binary and continuous variables. + Creates mutual exclusivity constraint for binary variables. Mathematical formulation: - variable[t] ≤ size[t] * upper_factor[t] ∀t + Σ(binary_vars[i]) ≤ tolerance ∀t - If binary_control provided: - variable[t] ≥ M * (binary[t] - 1) + size[t] * lower_factor[t] ∀t - where M = max(size) * max(upper_factor) - Else: - variable[t] ≥ size[t] * lower_factor[t] ∀t + Ensures at most one binary variable can be 1 at any time. + Tolerance > 1.0 accounts for binary variable numerical precision. + + Args: + binary_variables: List of binary variables that should be mutually exclusive + tolerance: Upper bound (typically 1.1 for numerical stability) Returns: variables: {} (no new variables created) - constraints: {'upper_bound': constraint, 'lower_bound': constraint} - """ - rel_lower, rel_upper = relative_bounds + constraints: {'mutual_exclusivity': constraint} - # Upper bound: variable ≤ size * upper_factor - upper_bound = model.add_constraints(bounded_variable <= scaling_variable * rel_upper, name=upper_bound_name) + Raises: + AssertionError: If fewer than 2 variables provided or variables aren't binary + """ + assert len(binary_variables) >= 2, ( + f'Mutual exclusivity requires at least 2 variables, got {len(binary_variables)}' + ) - if binary_gate is None: - lower_bound = model.add_constraints(bounded_variable >= scaling_variable * rel_lower, name=lower_bound_name) - else: - # Big-M lower bound: variable ≥ M*(binary-1) + size*lower_factor - lower_bound = model.add_constraints( - bounded_variable >= big_m * (binary_gate - 1) + scaling_variable * rel_lower, name=lower_bound_name + for var in binary_variables: + assert var.attrs.get('binary', False), ( + f'Variable {var.name} must be binary for mutual exclusivity constraint' ) + # Create mutual exclusivity constraint + mutual_exclusivity = model.add_constraints( + sum(binary_variables) <= tolerance, name=f'{name}|mutual_exclusivity' + ) + variables = {} # No new variables created - constraints = {'upper_bound': upper_bound, 'lower_bound': lower_bound} + constraints = {'mutual_exclusivity': mutual_exclusivity} return variables, constraints + +class BoundingPatterns: + """High-level patterns that compose primitives and return (variables, constraints) tuples""" + + @staticmethod + def basic_bounds( + model: FlowSystemModel, + variable: linopy.Variable, + bounds: Tuple[TemporalData, TemporalData], + ): + """Create simple bounds. + + Args: + model: The optimization model instance + variable: Variable to be bounded + bounds: Tuple of (lower_bound, upper_bound) absolute bounds + + Returns: + Tuple containing: + - variables (Dict): Empty dict (no new variables created) + - constraints (Dict[str, linopy.Constraint]): Dictionary with keys: + - 'ub': Upper bound constraint + - 'lb': Lower bound constraint + """ + lower_bound, upper_bound = bounds + + upper_constraint = model.add_constraints(variable <= upper_bound, name=f'{variable.name}|ub') + lower_constraint = model.add_constraints(variable >= lower_bound, name=f'{variable.name}|lb') + + return {}, {'ub': upper_constraint, 'lb': lower_constraint} + @staticmethod def binary_controlled_bounds( model: FlowSystemModel, variable: linopy.Variable, bounds: Tuple[TemporalData, TemporalData], binary_control: linopy.Variable, - upper_bound_name: str, - lower_bound_name: str, ) -> Tuple[Dict, Dict[str, linopy.Constraint]]: - """ - Creates bounds controlled by a binary variable with epsilon handling. + """Create bounds controlled by a binary variable with epsilon handling. - Mathematical formulation: + This method implements binary-controlled bounds where a binary variable acts as an on/off + switch for the bounded variable. When the binary is 1, normal bounds apply; when 0, the + variable is forced to zero. + + Mathematical Formulation: binary * max(ε, lower_bound) ≤ variable ≤ binary * upper_bound - When binary = 1: normal bounds apply - When binary = 0: variable is forced to 0 + Where: + - binary ∈ {0, 1}: Control variable + - ε: Small positive constant (CONFIG.modeling.EPSILON) + - When binary = 1: Normal bounds apply + - When binary = 0: Variable is forced to 0 + + Use Cases: + - Investment decisions (invest or don't invest) + - Unit commitment (on/off operational states) + - Feature selection in optimization models - Example use case - Investment bounds: + Example: + Investment bounds where β_inv controls whether investment occurs: β_inv * max(ε, V^L) ≤ V ≤ β_inv * V^U - where β_inv is investment decision, V is investment size Args: + model: The optimization model instance variable: Variable to be bounded - bounds: (lower_bound, upper_bound) absolute bounds + bounds: Tuple of (lower_bound, upper_bound) absolute bounds binary_control: Binary variable controlling the bounds - upper_bound_name: Name for upper bound constraint - lower_bound_name: Name for lower bound constraint Returns: - variables: {} (no new variables created) - constraints: {'upper_bound': constraint, 'lower_bound': constraint} + Tuple containing: + - variables (Dict): Empty dict (no new variables created) + - constraints (Dict[str, linopy.Constraint]): Dictionary with keys: + - 'ub': Upper bound constraint + - 'lb': Lower bound constraint + - 'fix': Fix constraint, if upper bound is equal to lower bound + + Note: + The epsilon value is applied to the lower bound to distinguish between + zero and "very small positive" values, which is important for numerical + stability in optimization solvers. """ lower_bound, upper_bound = bounds + if np.all(lower_bound - upper_bound) < 1e-10: + fix_constraint = model.add_constraints( + variable == binary_control * upper_bound, name=f'{variable.name}|fixed_size' + ) + return {}, {'ub': fix_constraint, 'lb': fix_constraint} + # Apply epsilon to lower bound to distinguish 0 from "very small positive" - epsilon_lower = np.maximum(CONFIG.modeling.EPSILON, lower_bound) + epsilon = np.maximum(CONFIG.modeling.EPSILON, lower_bound) - upper_constraint = model.add_constraints(variable <= binary_control * upper_bound, name=upper_bound_name) - lower_constraint = model.add_constraints(variable >= binary_control * epsilon_lower, name=lower_bound_name) + upper_constraint = model.add_constraints(variable <= binary_control * upper_bound, name=f'{variable.name}|ub') + lower_constraint = model.add_constraints(variable >= binary_control * epsilon, name=f'{variable.name}|lb') - variables = {} - constraints = {'upper_bound': upper_constraint, 'lower_bound': lower_constraint} - return variables, constraints + return {}, {'ub': upper_constraint, 'lb': lower_constraint} @staticmethod - def binary_scaled_bounds( + def scaled_bounds( model: FlowSystemModel, variable: linopy.Variable, scaling_variable: linopy.Variable, relative_bounds: Tuple[TemporalData, TemporalData], - binary_control: linopy.Variable, - upper_bound_name: str, - lower_bound_name: str, ) -> Tuple[Dict, Dict[str, linopy.Constraint]]: - """ - Creates scaled bounds controlled by a binary variable. + """Create simple bounds scaled by another variable. - Mathematical formulation: - binary * max(ε, scaling * lower_factor) ≤ variable ≤ binary * scaling * upper_factor + This method creates proportional bounds where the actual bounds are determined + by multiplying relative factors with a scaling variable. This is useful for + capacity-dependent constraints. - When binary = 1: variable bounded by scaled factors - When binary = 0: variable forced to 0 + Mathematical Formulation: + scaling * lower_factor ≤ variable ≤ scaling * upper_factor + + Where: + - scaling: Continuous scaling variable (e.g., capacity, size) + - lower_factor, upper_factor: Relative bound multipliers + + Use Cases: + - Flow rates bounded by equipment capacity + - Production levels scaled by plant size + - Resource consumption proportional to activity level - Example use case - Fixed size with on/off control: - β_on(t) * max(ε, P * p_rel^L(t)) ≤ p(t) ≤ β_on(t) * P * p_rel^U(t) - where β_on is on/off state, P is fixed size, p is flow rate + Example: + Flow rate bounded by equipment size: + P * p_rel^L(t) ≤ p(t) ≤ P * p_rel^U(t) + where P is equipment size, p is flow rate Args: + model: The optimization model instance variable: Variable to be bounded - scaling_variable: Variable to scale the bounds by - relative_bounds: (lower_factor, upper_factor) relative to scaling variable - binary_control: Binary variable controlling the bounds - upper_bound_name: Name for upper bound constraint - lower_bound_name: Name for lower bound constraint + scaling_variable: Variable that scales the bound factors + relative_bounds: Tuple of (lower_factor, upper_factor) relative to scaling variable Returns: - variables: {} (no new variables created) - constraints: {'upper_bound': constraint, 'lower_bound': constraint} + Tuple containing: + - variables (Dict): Empty dict (no new variables created) + - constraints (Dict[str, linopy.Constraint]): Dictionary with keys: + - 'ub': Upper bound constraint + - 'lb': Lower bound constraint + + Note: + This method assumes the scaling variable is always non-negative. + For negative scaling variables, the inequality directions would need adjustment. """ rel_lower, rel_upper = relative_bounds - # Calculate scaled expressions - upper_expr = scaling_variable * rel_upper - lower_expr = scaling_variable * rel_lower - - # Apply epsilon to lower expression - epsilon_lower = np.maximum(CONFIG.modeling.EPSILON, lower_expr) - - upper_constraint = model.add_constraints(variable <= binary_control * upper_expr, name=upper_bound_name) - lower_constraint = model.add_constraints(variable >= binary_control * epsilon_lower, name=lower_bound_name) + upper_constraint = model.add_constraints(variable <= scaling_variable * rel_upper, name=f'{variable}|ub') + lower_constraint = model.add_constraints(variable >= scaling_variable * rel_lower, name=f'{variable}|lb') variables = {} - constraints = {'upper_bound': upper_constraint, 'lower_bound': lower_constraint} + constraints = {'ub': upper_constraint, 'lb': lower_constraint} return variables, constraints @staticmethod - def big_m_dual_control_bounds( + def binary_scaled_bounds( model: FlowSystemModel, variable: linopy.Variable, scaling_variable: linopy.Variable, relative_bounds: Tuple[TemporalData, TemporalData], binary_control: linopy.Variable, scaling_bounds: Tuple[TemporalData, TemporalData], - constraint_name_prefix: str, ) -> Tuple[Dict, Dict[str, linopy.Constraint]]: - """ - Creates bounds with both binary and continuous variable control using big-M formulation. + """Create scaled bounds controlled by a binary variable using linear big-M formulation. - Mathematical formulation: - # Binary control with big-M bounds: - binary * max(ε, scaling_min * lower_factor) ≤ variable ≤ binary * M - # Continuous scaling bounds: - M * (binary - 1) + scaling * lower_factor ≤ variable ≤ scaling * upper_factor + Desired (Non-Linear) Formulation: + binary * max(ε, scaling * lower_factor) ≤ variable ≤ binary * scaling * upper_factor - Where M = scaling_max * upper_factor + Actual Linear Big-M Formulation: + # When binary = 1: scaling bounds apply + scaling * lower_factor ≤ variable ≤ scaling * upper_factor + + # When binary = 0: variable forced to 0 + variable ≤ binary * M_upper + variable ≥ binary * M_lower + + Where: + M_upper = scaling_max * upper_factor + M_lower = max(ε, scaling_min * lower_factor) - This maintains linearity when both binary and continuous controls are present. + Behavior: + - When binary = 1: Variable bounded by scaling * factors (normal operation) + - When binary = 0: Variable forced to 0 (off state) - Example use case - Variable investment size with on/off control: - β_on(t) * max(ε, P^L * p_rel^L(t)) ≤ p(t) ≤ β_on(t) * M(t) - M(t) * (β_on(t) - 1) + P * p_rel^L(t) ≤ p(t) ≤ P * p_rel^U(t) - where β_on is on/off state, P is variable investment size, p is flow rate + Use Cases: + - Fixed-size units with on/off control + - Capacity-scaled operations with binary states + - Process units with binary operational modes + + Example: + Power plant with capacity P ∈ [P_min, P_max] and on/off control β_on: + Linear formulation replaces: β_on(t) * P * p_rel^L(t) ≤ p(t) ≤ β_on(t) * P * p_rel^U(t) Args: + model: The optimization model instance variable: Variable to be bounded - scaling_variable: Continuous variable that scales the bounds - relative_bounds: (lower_factor, upper_factor) relative to scaling variable + scaling_variable: Variable that scales the bound factors + relative_bounds: Tuple of (lower_factor, upper_factor) relative to scaling variable binary_control: Binary variable for on/off control - scaling_bounds: (scaling_min, scaling_max) bounds of the scaling variable - constraint_name_prefix: Prefix for constraint names + scaling_bounds: Tuple of (scaling_min, scaling_max) bounds of the scaling variable Returns: - variables: {} (no new variables created) - constraints: { - 'binary_lower': binary-controlled lower bound, - 'binary_upper': binary-controlled upper bound, - 'scaling_lower': scaling-controlled lower bound, - 'scaling_upper': scaling-controlled upper bound - } + Tuple containing: + - variables (Dict): Empty dict (no new variables created) + - constraints (Dict[str, linopy.Constraint]): Dictionary with keys: + - 'ub': Upper bound constraint + - 'lb': Lower bound constraint + + Note: + This method now requires scaling_bounds to compute appropriate big-M values. + The big-M formulation maintains linearity while preserving the intended behavior. """ rel_lower, rel_upper = relative_bounds scaling_min, scaling_max = scaling_bounds - # Calculate big-M as maximum possible value - big_m = rel_upper * scaling_max + # Calculate big-M values for upper and lower bounds + big_m_upper = scaling_max * rel_upper + big_m_lower = np.maximum(CONFIG.modeling.EPSILON, scaling_min * rel_lower) - # Binary-controlled lower bound with epsilon - epsilon_lower = np.maximum(CONFIG.modeling.EPSILON, rel_lower * scaling_min) - binary_lower = model.add_constraints( - binary_control * epsilon_lower <= variable, name=f'{constraint_name_prefix}|binary_lower' - ) + # Linear constraints using big-M technique: + # When binary = 1: normal scaling bounds apply + # When binary = 0: variable forced to 0 - # Binary-controlled upper bound with big-M + # Upper bound: variable ≤ min(scaling * rel_upper, binary * big_m_upper) + # Implemented as two constraints: + scaling_upper = model.add_constraints( + variable <= scaling_variable * rel_upper, name=f'{scaling_variable.name}|ub' + ) binary_upper = model.add_constraints( - variable <= binary_control * big_m, name=f'{constraint_name_prefix}|binary_upper' + variable <= binary_control * big_m_upper, name=f'{variable.name}|ub' ) - # Scaling-controlled lower bound with big-M relaxation + # Lower bound: variable ≥ max(scaling * rel_lower, binary * big_m_lower) + # When binary = 0: second constraint gives variable ≥ 0 + # When binary = 1: first constraint is active scaling_lower = model.add_constraints( - big_m * (binary_control - 1) + scaling_variable * rel_lower <= variable, - name=f'{constraint_name_prefix}|scaling_lower', + variable >= scaling_variable * rel_lower, name=f'{scaling_variable.name}|ub' ) - - # Scaling-controlled upper bound - scaling_upper = model.add_constraints( - variable <= scaling_variable * rel_upper, name=f'{constraint_name_prefix}|scaling_upper' + binary_lower = model.add_constraints( + variable >= binary_control * big_m_lower, name=f'{variable.name}|lb' ) variables = {} constraints = { - 'binary_lower': binary_lower, - 'binary_upper': binary_upper, - 'scaling_lower': scaling_lower, - 'scaling_upper': scaling_upper, + 'ub': scaling_upper, # Primary upper bound constraint + 'lb': scaling_lower, # Primary lower bound constraint + 'binary_upper': binary_upper, # Binary control upper bound + 'binary_lower': binary_lower, # Binary control lower bound } return variables, constraints @staticmethod - def scaled_bounds( + def dual_binary_scaled_bounds( model: FlowSystemModel, variable: linopy.Variable, scaling_variable: linopy.Variable, relative_bounds: Tuple[TemporalData, TemporalData], - upper_bound_name: str, - lower_bound_name: str, + scaling_binary: linopy.Variable, + secondary_binary: linopy.Variable, + scaling_bounds: Tuple[TemporalData, TemporalData], ) -> Tuple[Dict, Dict[str, linopy.Constraint]]: - """ - Creates simple bounds scaled by another variable. + """Create bounds with dual binary control over a scaled variable. - Mathematical formulation: - scaling * lower_factor ≤ variable ≤ scaling * upper_factor + This method implements the most complex bounding case where you have two binary variables + controlling a scaled relationship between variables. This is commonly used for investment + and operational control scenarios. - Example use case - Flow rate bounded by size: - P * p_rel^L(t) ≤ p(t) ≤ P * p_rel^U(t) - where P is size, p is flow rate + Hierarchical Control: + 1. scaling_binary: Controls whether the scaling variable can be non-zero + 2. Secondary binary: Controls whether the main variable can be non-zero (given scaling exists) - Args: - variable: Variable to be bounded - scaling_variable: Variable to scale the bounds by - relative_bounds: (lower_factor, upper_factor) relative to scaling variable - upper_bound_name: Name for upper bound constraint - lower_bound_name: Name for lower bound constraint + Mathematical Formulation: - Returns: - variables: {} (no new variables created) - constraints: {'upper_bound': constraint, 'lower_bound': constraint} - """ - rel_lower, rel_upper = relative_bounds + Scaling variable bounds: + scaling_binary * max(ε, scaling_min) ≤ scaling_variable ≤ scaling_binary * scaling_max - upper_constraint = model.add_constraints(variable <= scaling_variable * rel_upper, name=upper_bound_name) - lower_constraint = model.add_constraints(variable >= scaling_variable * rel_lower, name=lower_bound_name) + Main variable bounds with dual control: + secondary_binary * max(ε, rel_lower * scaling_min) ≤ variable ≤ secondary_binary * M + M * (secondary_binary - 1) + scaling_variable * rel_lower ≤ variable ≤ scaling_variable * rel_upper - variables = {} - constraints = {'upper_bound': upper_constraint, 'lower_bound': lower_constraint} - return variables, constraints + Where: M = rel_upper * scaling_max - @staticmethod - def auto_bounds( - model: FlowSystemModel, - variable: linopy.Variable, - bounds: Tuple[TemporalData, TemporalData], - upper_bound_name: str, - lower_bound_name: str, - scaling_variable: linopy.Variable = None, - binary_control: linopy.Variable = None, - scaling_bounds: Tuple[TemporalData, TemporalData] = None, - constraint_name_prefix: str = None, - ) -> Tuple[Dict, Dict[str, linopy.Constraint]]: - """ - Automatically selects the appropriate bounds method based on provided parameters. - - Parameter combinations and resulting method calls: - - 1. Only bounds → Simple absolute bounds: - lower_bound ≤ variable ≤ upper_bound - - 2. bounds + scaling_variable → scaled_bounds(): - scaling * lower_factor ≤ variable ≤ scaling * upper_factor + Logical Behavior: + - scaling_binary = 0: No scaling capacity (scaling_variable = 0), no main variable (variable = 0) + - scaling_binary = 1, secondary_binary = 0: Scaling exists but main variable is off (variable = 0) + - scaling_binary = 1, secondary_binary = 1: Normal scaled operation - 3. bounds + binary_control → binary_controlled_bounds(): - binary * max(ε, lower_bound) ≤ variable ≤ binary * upper_bound + Use Cases: + - Investment + operational control (capacity sizing + on/off dispatch) + - Resource allocation + activation (budget + spending) + - Equipment sizing + utilization (capacity + operation) + - Feature selection + intensity (enable + level) - 4. bounds + scaling_variable + binary_control → binary_scaled_bounds(): - binary * max(ε, scaling * lower_factor) ≤ variable ≤ binary * scaling * upper_factor - - 5. All parameters → big_m_dual_control_bounds(): - Complex big-M formulation for binary + variable scaling control + Examples: + - Power plant: Build capacity? (primary) How big? (scaling) When to run? (secondary) + - Marketing: Enter market? (primary) Budget size? (scaling) Campaign active? (secondary) + - Production: Install line? (primary) Line capacity? (scaling) Line running? (secondary) Args: - variable: Variable to be bounded - bounds: (lower, upper) - absolute bounds or relative factors if scaling - upper_bound_name: Name for upper bound constraint - lower_bound_name: Name for lower bound constraint - scaling_variable: Optional variable to scale bounds by - binary_control: Optional binary variable for on/off control - scaling_bounds: Required if using big-M (case 5), bounds of scaling variable - constraint_name_prefix: Required if using big-M (case 5) + model: The optimization model instance + variable: Main variable to be bounded + scaling_variable: Variable that scales the bounds (e.g., capacity, size, budget) + relative_bounds: Tuple of (rel_lower, rel_upper) relative bound multipliers + scaling_binary: Binary controlling scaling_variable existence (e.g., investment decision) + secondary_binary: Binary controlling variable operation (e.g., operational on/off) + scaling_bounds: Tuple of (scaling_min, scaling_max) bounds for scaling_variable Returns: - Same as the underlying primitive method - - Raises: - ValueError: If big-M case is detected but required parameters are missing + Tuple containing: + - variables (Dict): Empty dict (no new variables created) + - constraints (Dict[str, linopy.Constraint]): Dictionary with keys: + - 'primary_scaling_ub': Primary control upper bound for scaling variable + - 'primary_scaling_lb': Primary control lower bound for scaling variable + - 'secondary_variable_ub': Secondary control upper bound for main variable + - 'secondary_variable_lb': Secondary control lower bound for main variable + - 'scaling_variable_ub': Scaling-dependent upper bound for main variable + - 'scaling_variable_lb': Scaling-dependent lower bound for main variable + + Note: + This implements hierarchical binary control where the primary binary enables the scaling + variable, and the secondary binary controls the main variable's operation within the + scaled bounds. Both binaries must be active for normal operation. """ + rel_lower, rel_upper = relative_bounds + scaling_min, scaling_max = scaling_bounds - # Case 5: Big-M dual control (most complex) - if scaling_variable is not None and binary_control is not None and scaling_bounds is not None: - if constraint_name_prefix is None: - raise ValueError('constraint_name_prefix is required when using big-M dual control') - - return ModelingPrimitives.big_m_dual_control_bounds( - model=model, - variable=variable, - scaling_variable=scaling_variable, - relative_bounds=bounds, - binary_control=binary_control, - scaling_bounds=scaling_bounds, - constraint_name_prefix=constraint_name_prefix, - ) + # Calculate big-M value for secondary control constraints + # M = rel_upper * scaling_max (maximum possible variable value) + big_m = rel_upper * scaling_max - # Case 4: Binary + scaling (fixed size with on/off) - elif scaling_variable is not None and binary_control is not None: - return ModelingPrimitives.binary_scaled_bounds( - model=model, - variable=variable, - scaling_variable=scaling_variable, - relative_bounds=bounds, - binary_control=binary_control, - upper_bound_name=upper_bound_name, - lower_bound_name=lower_bound_name, - ) + # 1. PRIMARY BINARY CONSTRAINTS FOR SCALING VARIABLE + # scaling_binary * max(ε, scaling_min) ≤ scaling_variable ≤ scaling_binary * scaling_max + epsilon_scaling = np.maximum(CONFIG.modeling.EPSILON, scaling_min) - # Case 3: Binary only (investment decision) - elif binary_control is not None: - return ModelingPrimitives.binary_controlled_bounds( - model=model, - variable=variable, - bounds=bounds, - binary_control=binary_control, - upper_bound_name=upper_bound_name, - lower_bound_name=lower_bound_name, - ) + primary_scaling_ub = model.add_constraints( + scaling_variable <= scaling_binary * scaling_max, name=f'{scaling_variable.name}|primary_ub' + ) - # Case 2: Scaling only (size-dependent bounds) - elif scaling_variable is not None: - return ModelingPrimitives.scaled_bounds( - model=model, - variable=variable, - scaling_variable=scaling_variable, - relative_bounds=bounds, - upper_bound_name=upper_bound_name, - lower_bound_name=lower_bound_name, - ) + primary_scaling_lb = model.add_constraints( + scaling_variable >= scaling_binary * epsilon_scaling, name=f'{scaling_variable.name}|primary_lb' + ) - # Case 1: Simple absolute bounds - else: - upper_constraint = model.add_constraints(variable <= bounds[1], name=upper_bound_name) - lower_constraint = model.add_constraints(variable >= bounds[0], name=lower_bound_name) + # 2. SECONDARY BINARY CONSTRAINTS FOR MAIN VARIABLE + # secondary_binary * max(ε, rel_lower * scaling_min) ≤ variable ≤ secondary_binary * M + epsilon_variable = np.maximum(CONFIG.modeling.EPSILON, rel_lower * scaling_min) - variables = {} - constraints = {'upper_bound': upper_constraint, 'lower_bound': lower_constraint} - return variables, constraints + secondary_variable_ub = model.add_constraints( + variable <= secondary_binary * big_m, name=f'{variable.name}|secondary_ub' + ) - @staticmethod - def consecutive_duration_tracking( - model: FlowSystemModel, - name: str, - state_variable: linopy.Variable, - minimum_duration: Optional[TemporalData] = None, - maximum_duration: Optional[TemporalData] = None, - previous_duration: TemporalData = 0, - ) -> Tuple[Dict[str, linopy.Variable], Dict[str, linopy.Constraint]]: - """ - Creates consecutive duration tracking for a binary state variable. + secondary_variable_lb = model.add_constraints( + variable >= secondary_binary * epsilon_variable, name=f'{variable.name}|secondary_lb' + ) - Mathematical formulation: - duration[t] ≤ state[t] * M ∀t - duration[t+1] ≤ duration[t] + hours_per_step[t] ∀t - duration[t+1] ≥ duration[t] + hours_per_step[t] + (state[t+1] - 1) * M ∀t - duration[0] = (hours_per_step[0] + previous_duration) * state[0] + # 3. SCALING-DEPENDENT CONSTRAINTS FOR MAIN VARIABLE + # M * (secondary_binary - 1) + scaling_variable * rel_lower ≤ variable ≤ scaling_variable * rel_upper - If minimum_duration provided: - duration[t] ≥ (state[t-1] - state[t]) * minimum_duration[t-1] ∀t > 0 + scaling_variable_ub = model.add_constraints( + variable <= scaling_variable * rel_upper, name=f'{variable.name}|scaling_ub' + ) - Args: - state_variable: Binary state variable to track duration for - minimum_duration: Optional minimum consecutive duration - maximum_duration: Optional maximum consecutive duration - previous_duration: Duration from before first timestep + scaling_variable_lb = model.add_constraints( + big_m * (secondary_binary - 1) + scaling_variable * rel_lower <= variable, + name=f'{variable.name}|scaling_lb', + ) - Returns: - variables: {'duration': duration_var} - constraints: {'upper_bound': constraint, 'forward': constraint, 'backward': constraint, ...} - """ - hours_per_step = model.hours_per_step - mega = hours_per_step.sum('time') + previous_duration # Big-M value + variables = {} + constraints = { + 'primary_scaling_ub': primary_scaling_ub, + 'primary_scaling_lb': primary_scaling_lb, + 'secondary_variable_ub': secondary_variable_ub, + 'secondary_variable_lb': secondary_variable_lb, + 'scaling_variable_ub': scaling_variable_ub, + 'scaling_variable_lb': scaling_variable_lb, + } - # Duration variable - duration = model.add_variables( - lower=0, - upper=maximum_duration if maximum_duration is not None else mega, - coords=model.get_coords(['time']), - name=f'{name}|duration', - ) + return variables, constraints - constraints = {} + @staticmethod + def auto_bounds( + model: FlowSystemModel, + variable: linopy.Variable, + variable_bounds: Tuple[TemporalData, TemporalData], + scaling_variable: linopy.Variable = None, + scaling_state: linopy.Variable = None, + scaling_bounds: Tuple[TemporalData, TemporalData] = None, + variable_state: linopy.Variable = None, + ) -> Tuple[Dict, Dict[str, linopy.Constraint]]: + """Automatically select the appropriate bounds method based on provided parameters. - # Upper bound: duration[t] ≤ state[t] * M - constraints['upper_bound'] = model.add_constraints( - duration <= state_variable * mega, name=f'{name}|duration_upper_bound' - ) + This intelligent dispatcher analyzes the provided parameters and automatically + selects the most appropriate bounding method. It simplifies the API by providing + a single entry point for all bounding scenarios. - # Forward constraint: duration[t+1] ≤ duration[t] + hours_per_step[t] - constraints['forward'] = model.add_constraints( - duration.isel(time=slice(1, None)) - <= duration.isel(time=slice(None, -1)) + hours_per_step.isel(time=slice(None, -1)), - name=f'{name}|duration_forward', - ) + Parameter Combinations and Method Selection: - # Backward constraint: duration[t+1] ≥ duration[t] + hours_per_step[t] + (state[t+1] - 1) * M - constraints['backward'] = model.add_constraints( - duration.isel(time=slice(1, None)) - >= duration.isel(time=slice(None, -1)) - + hours_per_step.isel(time=slice(None, -1)) - + (state_variable.isel(time=slice(1, None)) - 1) * mega, - name=f'{name}|duration_backward', - ) + 1. **Simple Bounds**: Only `bounds` provided + → Creates: lower_bound ≤ variable ≤ upper_bound - # Initial condition: duration[0] = (hours_per_step[0] + previous_duration) * state[0] - constraints['initial'] = model.add_constraints( - duration.isel(time=0) - == (hours_per_step.isel(time=0) + previous_duration) * state_variable.isel(time=0), - name=f'{name}|duration_initial', - ) + 2. **Scaled Bounds**: `bounds` + `scaling_variable` + → Calls: scaled_bounds() + → Creates: scaling * lower_factor ≤ variable ≤ scaling * upper_factor - # Minimum duration constraint if provided - if minimum_duration is not None: - constraints['minimum'] = model.add_constraints( - duration.isel(time=slice(1, None)) - >= (state_variable.isel(time=slice(None, -1)) - state_variable.isel(time=slice(1, None))) - * minimum_duration.isel(time=slice(None, -1)), - name=f'{name}|duration_minimum', - ) + 3. **Binary Controlled**: `bounds` + `binary_control` + → Calls: binary_controlled_bounds() + → Creates: binary * max(ε, lower_bound) ≤ variable ≤ binary * upper_bound - # Handle initial condition for minimum duration - if previous_duration > 0 and previous_duration < minimum_duration.isel(time=0).max(): - constraints['initial_minimum'] = model.add_constraints( - state_variable.isel(time=0) == 1, name=f'{name}|duration_initial_minimum' - ) + 4. **Binary + Scaling**: `bounds` + `scaling_variable` + `binary_control` + → Calls: binary_scaled_bounds() + → Creates: binary * max(ε, scaling * lower_factor) ≤ variable ≤ binary * scaling * upper_factor - variables = {'duration': duration} + 5. **Big-M Dual Control**: All parameters provided + → Calls: big_m_dual_control_bounds() + → Creates: Complex big-M formulation for binary + variable scaling control - return variables, constraints + Usage Examples: + ```python + # Simple bounds + auto_bounds(model, var, (0, 100), 'upper', 'lower') - @staticmethod - def mutual_exclusivity_constraint( - model: FlowSystemModel, name: str, binary_variables: List[linopy.Variable], tolerance: float = 1.1 - ) -> Tuple[Dict, Dict[str, linopy.Constraint]]: - """ - Creates mutual exclusivity constraint for binary variables. + # Capacity-scaled bounds + auto_bounds(model, flow_var, (0.2, 0.8), 'upper', 'lower', scaling_variable=capacity_var) - Mathematical formulation: - Σ(binary_vars[i]) ≤ tolerance ∀t + # Binary on/off control + auto_bounds(model, var, (10, 100), 'upper', 'lower', binary_control=on_off_var) - Ensures at most one binary variable can be 1 at any time. - Tolerance > 1.0 accounts for binary variable numerical precision. + # Full dual control + auto_bounds( + model, + var, + (0.1, 0.9), + 'upper', + 'lower', + scaling_variable=size_var, + binary_control=on_var, + scaling_bounds=(0, 1000), + constraint_name_prefix='dual', + ) + ``` Args: - binary_variables: List of binary variables that should be mutually exclusive - tolerance: Upper bound (typically 1.1 for numerical stability) + model: The optimization model instance + variable: Variable to be bounded + variable_bounds: Tuple of (lower, upper) - absolute bounds or relative factors if scaling + scaling_variable: Optional variable to scale bounds by + scaling_state: Optional binary variable for the state of the scaling variable + scaling_bounds: Required for big-M case - bounds of scaling variable + variable_state: Optional variable that controls the variable state (e.g., on/off) Returns: - variables: {} (no new variables created) - constraints: {'mutual_exclusivity': constraint} + Tuple containing: + - variables (Dict): Variable dictionary from the selected method + - constraints (Dict[str, linopy.Constraint]): Constraint dictionary from the selected method Raises: - AssertionError: If fewer than 2 variables provided or variables aren't binary + ValueError: If big-M dual control is detected but required parameters are missing + + Note: + The method prioritizes more complex formulations when multiple options are available. + Parameter validation ensures all required arguments are provided for each case. """ - assert len(binary_variables) >= 2, ( - f'Mutual exclusivity requires at least 2 variables, got {len(binary_variables)}' - ) + # Case 1: Scaled bounds with state and a state for the variable + if variable_state is not None and scaling_variable is None and scaling_state is None: + return BoundingPatterns.dual_binary_scaled_bounds( + model=model, + variable=variable, + scaling_variable=variable_state, + relative_bounds=variable_bounds, + scaling_binary=variable_state, + secondary_binary=variable_state, + scaling_bounds=scaling_bounds, + ) - for var in binary_variables: - assert var.attrs.get('binary', False), ( - f'Variable {var.name} must be binary for mutual exclusivity constraint' + # Case 2: Scaled Bounds with state for the scaled variable + if variable_state is not None and scaling_variable is not None: + if scaling_bounds is None: + raise ValueError('scaling_bounds is required when using binary_scaled_bounds to compute big-M values') + + return BoundingPatterns.binary_scaled_bounds( + model=model, + variable=variable, + scaling_variable=scaling_variable, + relative_bounds=variable_bounds, + binary_control=variable_state, + scaling_bounds=scaling_bounds, ) - # Create mutual exclusivity constraint - mutual_exclusivity = model.add_constraints( - sum(binary_variables) <= tolerance, name=f'{name}|mutual_exclusivity' - ) + # Case 3: Binary controlled variable with fixed bounds + if variable_state is not None and scaling_variable is None: + return BoundingPatterns.binary_controlled_bounds( + model=model, + variable=variable, + bounds=variable_bounds, + binary_control=variable_state, + ) - variables = {} # No new variables created - constraints = {'mutual_exclusivity': mutual_exclusivity} + # Case 4: Simple absolute bounds + if scaling_variable is None and variable_state is None: + return BoundingPatterns.basic_bounds(model, variable, variable_bounds) - return variables, constraints + raise ValueError('Invalid combination of arguments') class ModelingPatterns: @@ -859,9 +937,9 @@ def investment_sizing_pattern( model: FlowSystemModel, name: str, size_bounds: Tuple[TemporalData, TemporalData], - controlled_variables: List[linopy.Variable] = None, - control_factors: List[Tuple[TemporalData, TemporalData]] = None, - state_variables: List[linopy.Variable] = None, + controlled_variable: linopy.Variable, + control_factors: Tuple[TemporalData, TemporalData], + state_variable: List[linopy.Variable] = None, optional: bool = False, ) -> Tuple[Dict[str, linopy.Variable], Dict[str, linopy.Constraint]]: """ @@ -878,7 +956,7 @@ def investment_sizing_pattern( Returns: variables: {'size': size_var, 'is_invested': binary_var (if optional)} - constraints: {'upper_bound': constraint, 'lower_bound': constraint, ...} + constraints: {'ub': constraint, 'lb': constraint, ...} """ variables = {} constraints = {} @@ -898,37 +976,19 @@ def investment_sizing_pattern( binary=True, name=f'{name}|is_invested', coords=model.get_coords(['year', 'scenario']) ) - # Link size to investment decision - if abs(size_min - size_max) < 1e-10: # Fixed size case - constraints['fixed_size'] = model.add_constraints( - variables['size'] == variables['is_invested'] * size_max, name=f'{name}|fixed_size' - ) - else: # Variable size case - constraints['upper_bound'] = model.add_constraints( - variables['size'] <= variables['is_invested'] * size_max, name=f'{name}|size|upper_bound' - ) - constraints['lower_bound'] = model.add_constraints( - variables['size'] >= variables['is_invested'] * np.maximum(CONFIG.modeling.EPSILON, size_min), - name=f'{name}|size|lower_bound', - ) + _, new_cons = BoundingPatterns.auto_bounds( + model=model, + variable=controlled_variable, + bounds=control_factors, + upper_bound_name=f'{controlled_variable.name}|ub', + lower_bound_name=f'{controlled_variable.name}|lb', + scaling_variable=variables['size'], + binary_control=variables['is_invested'] if optional else None, + scaling_bounds=(size_min, size_max), + constraint_name_prefix=name, + ) - # Control dependent variables - if controlled_variables and control_factors: - for i, (var, factors, state_variable) in enumerate(zip(controlled_variables, control_factors, state_variables)): - upper_bound_name = f'{var.name}|upper_bound' - lower_bound_name = f'{var.name}|lower_bound' - _, control_constraints = ModelingPrimitives.big_m_binary_bounds( - model=model, - variable=var, - binary_control=state_variable, - size_variable=variables['size'], - relative_bounds=factors, - upper_bound_name=upper_bound_name, - lower_bound_name=lower_bound_name, - ) - # Flatten control constraints with indexed names - constraints[upper_bound_name] = control_constraints['upper_bound'] - constraints[lower_bound_name] = control_constraints['lower_bound'] + constraints.update(new_cons) return variables, constraints @@ -967,7 +1027,7 @@ def operational_binary_control_pattern( # 2. Control variables - use big_m_binary_bounds pattern for consistency for i, (var, (lower_bound, upper_bound)) in enumerate(zip(controlled_variables, variable_bounds)): # Use the big_m pattern but without binary control (None) - _, control_constraints = ModelingPrimitives.big_m_binary_bounds( + _, control_constraints = BoundingPatterns.big_m_binary_bounds( model=model, variable=var, binary_control=variables['on'], # The on state controls the variables @@ -976,8 +1036,8 @@ def operational_binary_control_pattern( upper_bound_name=f'{name}|control_{i}_upper', lower_bound_name=f'{name}|control_{i}_lower', ) - constraints[f'control_{i}_upper'] = control_constraints['upper_bound'] - constraints[f'control_{i}_lower'] = control_constraints['lower_bound'] + constraints[f'control_{i}_upper'] = control_constraints['ub'] + constraints[f'control_{i}_lower'] = control_constraints['lb'] # 3. Total duration tracking using existing pattern if track_total_duration: From 2afc24e15b0b16d30d5e06ec0ffde221b645177a Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 18 Jul 2025 17:35:29 +0200 Subject: [PATCH 206/448] Improve BoundingPatterns --- flixopt/modeling.py | 384 +++++++++++++------------------------------- 1 file changed, 110 insertions(+), 274 deletions(-) diff --git a/flixopt/modeling.py b/flixopt/modeling.py index a443c3a74..359ae24e1 100644 --- a/flixopt/modeling.py +++ b/flixopt/modeling.py @@ -439,6 +439,9 @@ def basic_bounds( ): """Create simple bounds. + Mathematical Formulation: + lower_bound ≤ variable ≤ upper_bound + Args: model: The optimization model instance variable: Variable to be bounded @@ -446,10 +449,8 @@ def basic_bounds( Returns: Tuple containing: - - variables (Dict): Empty dict (no new variables created) - - constraints (Dict[str, linopy.Constraint]): Dictionary with keys: - - 'ub': Upper bound constraint - - 'lb': Lower bound constraint + - variables (Dict): Empty dict + - constraints (Dict[str, linopy.Constraint]): 'ub', 'lb' """ lower_bound, upper_bound = bounds @@ -463,64 +464,40 @@ def binary_controlled_bounds( model: FlowSystemModel, variable: linopy.Variable, bounds: Tuple[TemporalData, TemporalData], - binary_control: linopy.Variable, + variable_state: linopy.Variable, ) -> Tuple[Dict, Dict[str, linopy.Constraint]]: - """Create bounds controlled by a binary variable with epsilon handling. - - This method implements binary-controlled bounds where a binary variable acts as an on/off - switch for the bounded variable. When the binary is 1, normal bounds apply; when 0, the - variable is forced to zero. + """Create bounds controlled by a binary variable. Mathematical Formulation: - binary * max(ε, lower_bound) ≤ variable ≤ binary * upper_bound - - Where: - - binary ∈ {0, 1}: Control variable - - ε: Small positive constant (CONFIG.modeling.EPSILON) - - When binary = 1: Normal bounds apply - - When binary = 0: Variable is forced to 0 + variable_state * max(ε, lower_bound) ≤ variable ≤ variable_state * upper_bound Use Cases: - - Investment decisions (invest or don't invest) - - Unit commitment (on/off operational states) - - Feature selection in optimization models - - Example: - Investment bounds where β_inv controls whether investment occurs: - β_inv * max(ε, V^L) ≤ V ≤ β_inv * V^U + - Investment decisions + - Unit commitment (on/off states) Args: model: The optimization model instance variable: Variable to be bounded bounds: Tuple of (lower_bound, upper_bound) absolute bounds - binary_control: Binary variable controlling the bounds + variable_state: Binary variable controlling the bounds Returns: Tuple containing: - - variables (Dict): Empty dict (no new variables created) - - constraints (Dict[str, linopy.Constraint]): Dictionary with keys: - - 'ub': Upper bound constraint - - 'lb': Lower bound constraint - - 'fix': Fix constraint, if upper bound is equal to lower bound - - Note: - The epsilon value is applied to the lower bound to distinguish between - zero and "very small positive" values, which is important for numerical - stability in optimization solvers. + - variables (Dict): Empty dict + - constraints (Dict[str, linopy.Constraint]): 'ub', 'lb' """ lower_bound, upper_bound = bounds if np.all(lower_bound - upper_bound) < 1e-10: fix_constraint = model.add_constraints( - variable == binary_control * upper_bound, name=f'{variable.name}|fixed_size' + variable == variable_state * upper_bound, name=f'{variable.name}|fixed_size' ) return {}, {'ub': fix_constraint, 'lb': fix_constraint} - # Apply epsilon to lower bound to distinguish 0 from "very small positive" epsilon = np.maximum(CONFIG.modeling.EPSILON, lower_bound) - upper_constraint = model.add_constraints(variable <= binary_control * upper_bound, name=f'{variable.name}|ub') - lower_constraint = model.add_constraints(variable >= binary_control * epsilon, name=f'{variable.name}|lb') + upper_constraint = model.add_constraints(variable <= variable_state * upper_bound, name=f'{variable.name}|ub') + lower_constraint = model.add_constraints(variable >= variable_state * epsilon, name=f'{variable.name}|lb') return {}, {'ub': upper_constraint, 'lb': lower_constraint} @@ -531,28 +508,14 @@ def scaled_bounds( scaling_variable: linopy.Variable, relative_bounds: Tuple[TemporalData, TemporalData], ) -> Tuple[Dict, Dict[str, linopy.Constraint]]: - """Create simple bounds scaled by another variable. - - This method creates proportional bounds where the actual bounds are determined - by multiplying relative factors with a scaling variable. This is useful for - capacity-dependent constraints. + """Create bounds scaled by another variable. Mathematical Formulation: - scaling * lower_factor ≤ variable ≤ scaling * upper_factor - - Where: - - scaling: Continuous scaling variable (e.g., capacity, size) - - lower_factor, upper_factor: Relative bound multipliers + scaling_variable * lower_factor ≤ variable ≤ scaling_variable * upper_factor Use Cases: - Flow rates bounded by equipment capacity - Production levels scaled by plant size - - Resource consumption proportional to activity level - - Example: - Flow rate bounded by equipment size: - P * p_rel^L(t) ≤ p(t) ≤ P * p_rel^U(t) - where P is equipment size, p is flow rate Args: model: The optimization model instance @@ -562,19 +525,13 @@ def scaled_bounds( Returns: Tuple containing: - - variables (Dict): Empty dict (no new variables created) - - constraints (Dict[str, linopy.Constraint]): Dictionary with keys: - - 'ub': Upper bound constraint - - 'lb': Lower bound constraint - - Note: - This method assumes the scaling variable is always non-negative. - For negative scaling variables, the inequality directions would need adjustment. + - variables (Dict): Empty dict + - constraints (Dict[str, linopy.Constraint]): 'ub', 'lb' """ rel_lower, rel_upper = relative_bounds - upper_constraint = model.add_constraints(variable <= scaling_variable * rel_upper, name=f'{variable}|ub') - lower_constraint = model.add_constraints(variable >= scaling_variable * rel_lower, name=f'{variable}|lb') + upper_constraint = model.add_constraints(variable <= scaling_variable * rel_upper, name=f'{variable.name}|ub') + lower_constraint = model.add_constraints(variable >= scaling_variable * rel_lower, name=f'{variable.name}|lb') variables = {} constraints = {'ub': upper_constraint, 'lb': lower_constraint} @@ -586,94 +543,57 @@ def binary_scaled_bounds( variable: linopy.Variable, scaling_variable: linopy.Variable, relative_bounds: Tuple[TemporalData, TemporalData], - binary_control: linopy.Variable, + variable_state: linopy.Variable, scaling_bounds: Tuple[TemporalData, TemporalData], ) -> Tuple[Dict, Dict[str, linopy.Constraint]]: - """Create scaled bounds controlled by a binary variable using linear big-M formulation. - - Desired (Non-Linear) Formulation: - binary * max(ε, scaling * lower_factor) ≤ variable ≤ binary * scaling * upper_factor - - Actual Linear Big-M Formulation: - # When binary = 1: scaling bounds apply - scaling * lower_factor ≤ variable ≤ scaling * upper_factor + """Create scaled bounds controlled by a binary variable. - # When binary = 0: variable forced to 0 - variable ≤ binary * M_upper - variable ≥ binary * M_lower + Mathematical Formulation (Big-M): + scaling_variable * lower_factor ≤ variable ≤ scaling_variable * upper_factor + variable ≤ variable_state * M_upper + variable ≥ variable_state * M_lower - Where: - M_upper = scaling_max * upper_factor - M_lower = max(ε, scaling_min * lower_factor) - - Behavior: - - When binary = 1: Variable bounded by scaling * factors (normal operation) - - When binary = 0: Variable forced to 0 (off state) + Where: M_upper = scaling_max * upper_factor, M_lower = max(ε, scaling_min * lower_factor) Use Cases: - - Fixed-size units with on/off control - - Capacity-scaled operations with binary states - - Process units with binary operational modes - - Example: - Power plant with capacity P ∈ [P_min, P_max] and on/off control β_on: - Linear formulation replaces: β_on(t) * P * p_rel^L(t) ≤ p(t) ≤ β_on(t) * P * p_rel^U(t) + - Equipment with capacity and on/off control + - Variable-size units with operational states Args: model: The optimization model instance variable: Variable to be bounded scaling_variable: Variable that scales the bound factors relative_bounds: Tuple of (lower_factor, upper_factor) relative to scaling variable - binary_control: Binary variable for on/off control + variable_state: Binary variable for on/off control scaling_bounds: Tuple of (scaling_min, scaling_max) bounds of the scaling variable Returns: Tuple containing: - - variables (Dict): Empty dict (no new variables created) - - constraints (Dict[str, linopy.Constraint]): Dictionary with keys: - - 'ub': Upper bound constraint - - 'lb': Lower bound constraint - - Note: - This method now requires scaling_bounds to compute appropriate big-M values. - The big-M formulation maintains linearity while preserving the intended behavior. + - variables (Dict): Empty dict + - constraints (Dict[str, linopy.Constraint]): 'ub', 'lb', 'binary_upper', 'binary_lower' """ rel_lower, rel_upper = relative_bounds scaling_min, scaling_max = scaling_bounds - # Calculate big-M values for upper and lower bounds big_m_upper = scaling_max * rel_upper big_m_lower = np.maximum(CONFIG.modeling.EPSILON, scaling_min * rel_lower) - # Linear constraints using big-M technique: - # When binary = 1: normal scaling bounds apply - # When binary = 0: variable forced to 0 - - # Upper bound: variable ≤ min(scaling * rel_upper, binary * big_m_upper) - # Implemented as two constraints: scaling_upper = model.add_constraints( - variable <= scaling_variable * rel_upper, name=f'{scaling_variable.name}|ub' - ) - binary_upper = model.add_constraints( - variable <= binary_control * big_m_upper, name=f'{variable.name}|ub' + variable <= scaling_variable * rel_upper, name=f'{variable.name}|scaling_ub' ) + binary_upper = model.add_constraints(variable <= variable_state * big_m_upper, name=f'{variable.name}|ub') - # Lower bound: variable ≥ max(scaling * rel_lower, binary * big_m_lower) - # When binary = 0: second constraint gives variable ≥ 0 - # When binary = 1: first constraint is active scaling_lower = model.add_constraints( - variable >= scaling_variable * rel_lower, name=f'{scaling_variable.name}|ub' - ) - binary_lower = model.add_constraints( - variable >= binary_control * big_m_lower, name=f'{variable.name}|lb' + variable >= scaling_variable * rel_lower, name=f'{variable.name}|scaling_lb' ) + binary_lower = model.add_constraints(variable >= variable_state * big_m_lower, name=f'{variable.name}|lb') variables = {} constraints = { - 'ub': scaling_upper, # Primary upper bound constraint - 'lb': scaling_lower, # Primary lower bound constraint - 'binary_upper': binary_upper, # Binary control upper bound - 'binary_lower': binary_lower, # Binary control lower bound + 'ub': scaling_upper, + 'lb': scaling_lower, + 'binary_upper': binary_upper, + 'binary_lower': binary_lower, } return variables, constraints @@ -683,121 +603,76 @@ def dual_binary_scaled_bounds( variable: linopy.Variable, scaling_variable: linopy.Variable, relative_bounds: Tuple[TemporalData, TemporalData], - scaling_binary: linopy.Variable, - secondary_binary: linopy.Variable, + scaling_state: linopy.Variable, + variable_state: linopy.Variable, scaling_bounds: Tuple[TemporalData, TemporalData], ) -> Tuple[Dict, Dict[str, linopy.Constraint]]: """Create bounds with dual binary control over a scaled variable. - This method implements the most complex bounding case where you have two binary variables - controlling a scaled relationship between variables. This is commonly used for investment - and operational control scenarios. - - Hierarchical Control: - 1. scaling_binary: Controls whether the scaling variable can be non-zero - 2. Secondary binary: Controls whether the main variable can be non-zero (given scaling exists) - Mathematical Formulation: - - Scaling variable bounds: - scaling_binary * max(ε, scaling_min) ≤ scaling_variable ≤ scaling_binary * scaling_max - - Main variable bounds with dual control: - secondary_binary * max(ε, rel_lower * scaling_min) ≤ variable ≤ secondary_binary * M - M * (secondary_binary - 1) + scaling_variable * rel_lower ≤ variable ≤ scaling_variable * rel_upper + scaling_state * max(ε, scaling_min) ≤ scaling_variable ≤ scaling_state * scaling_max + variable_state * max(ε, rel_lower * scaling_min) ≤ variable ≤ variable_state * M + M * (variable_state - 1) + scaling_variable * rel_lower ≤ variable ≤ scaling_variable * rel_upper Where: M = rel_upper * scaling_max - Logical Behavior: - - scaling_binary = 0: No scaling capacity (scaling_variable = 0), no main variable (variable = 0) - - scaling_binary = 1, secondary_binary = 0: Scaling exists but main variable is off (variable = 0) - - scaling_binary = 1, secondary_binary = 1: Normal scaled operation - Use Cases: - - Investment + operational control (capacity sizing + on/off dispatch) - - Resource allocation + activation (budget + spending) - - Equipment sizing + utilization (capacity + operation) - - Feature selection + intensity (enable + level) - - Examples: - - Power plant: Build capacity? (primary) How big? (scaling) When to run? (secondary) - - Marketing: Enter market? (primary) Budget size? (scaling) Campaign active? (secondary) - - Production: Install line? (primary) Line capacity? (scaling) Line running? (secondary) + - Investment + operational control (capacity sizing + on/off dispatch) + - Equipment sizing + utilization Args: model: The optimization model instance - variable: Main variable to be bounded - scaling_variable: Variable that scales the bounds (e.g., capacity, size, budget) + variable: Variable to be bounded + scaling_variable: Variable that scales the bounds relative_bounds: Tuple of (rel_lower, rel_upper) relative bound multipliers - scaling_binary: Binary controlling scaling_variable existence (e.g., investment decision) - secondary_binary: Binary controlling variable operation (e.g., operational on/off) + scaling_state: Binary controlling scaling_variable existence + variable_state: Binary controlling variable operation scaling_bounds: Tuple of (scaling_min, scaling_max) bounds for scaling_variable Returns: Tuple containing: - - variables (Dict): Empty dict (no new variables created) - - constraints (Dict[str, linopy.Constraint]): Dictionary with keys: - - 'primary_scaling_ub': Primary control upper bound for scaling variable - - 'primary_scaling_lb': Primary control lower bound for scaling variable - - 'secondary_variable_ub': Secondary control upper bound for main variable - - 'secondary_variable_lb': Secondary control lower bound for main variable - - 'scaling_variable_ub': Scaling-dependent upper bound for main variable - - 'scaling_variable_lb': Scaling-dependent lower bound for main variable - - Note: - This implements hierarchical binary control where the primary binary enables the scaling - variable, and the secondary binary controls the main variable's operation within the - scaled bounds. Both binaries must be active for normal operation. + - variables (Dict): Empty dict + - constraints (Dict[str, linopy.Constraint]): Multiple constraint keys """ rel_lower, rel_upper = relative_bounds scaling_min, scaling_max = scaling_bounds - # Calculate big-M value for secondary control constraints - # M = rel_upper * scaling_max (maximum possible variable value) big_m = rel_upper * scaling_max - # 1. PRIMARY BINARY CONSTRAINTS FOR SCALING VARIABLE - # scaling_binary * max(ε, scaling_min) ≤ scaling_variable ≤ scaling_binary * scaling_max + # 1. SCALING VARIABLE CONSTRAINTS epsilon_scaling = np.maximum(CONFIG.modeling.EPSILON, scaling_min) - primary_scaling_ub = model.add_constraints( - scaling_variable <= scaling_binary * scaling_max, name=f'{scaling_variable.name}|primary_ub' + scaling_ub = model.add_constraints( + scaling_variable <= scaling_state * scaling_max, name=f'{scaling_variable.name}|ub' ) - primary_scaling_lb = model.add_constraints( - scaling_variable >= scaling_binary * epsilon_scaling, name=f'{scaling_variable.name}|primary_lb' + scaling_lb = model.add_constraints( + scaling_variable >= scaling_state * epsilon_scaling, name=f'{scaling_variable.name}|lb' ) - # 2. SECONDARY BINARY CONSTRAINTS FOR MAIN VARIABLE - # secondary_binary * max(ε, rel_lower * scaling_min) ≤ variable ≤ secondary_binary * M + # 2. VARIABLE STATE CONSTRAINTS epsilon_variable = np.maximum(CONFIG.modeling.EPSILON, rel_lower * scaling_min) - secondary_variable_ub = model.add_constraints( - variable <= secondary_binary * big_m, name=f'{variable.name}|secondary_ub' - ) + variable_ub = model.add_constraints(variable <= variable_state * big_m, name=f'{variable.name}|ub') - secondary_variable_lb = model.add_constraints( - variable >= secondary_binary * epsilon_variable, name=f'{variable.name}|secondary_lb' - ) - - # 3. SCALING-DEPENDENT CONSTRAINTS FOR MAIN VARIABLE - # M * (secondary_binary - 1) + scaling_variable * rel_lower ≤ variable ≤ scaling_variable * rel_upper + variable_lb = model.add_constraints(variable >= variable_state * epsilon_variable, name=f'{variable.name}|lb') + # 3. SCALING-DEPENDENT CONSTRAINTS scaling_variable_ub = model.add_constraints( variable <= scaling_variable * rel_upper, name=f'{variable.name}|scaling_ub' ) scaling_variable_lb = model.add_constraints( - big_m * (secondary_binary - 1) + scaling_variable * rel_lower <= variable, + big_m * (variable_state - 1) + scaling_variable * rel_lower <= variable, name=f'{variable.name}|scaling_lb', ) variables = {} constraints = { - 'primary_scaling_ub': primary_scaling_ub, - 'primary_scaling_lb': primary_scaling_lb, - 'secondary_variable_ub': secondary_variable_ub, - 'secondary_variable_lb': secondary_variable_lb, + 'scaling_ub': scaling_ub, + 'scaling_lb': scaling_lb, + 'variable_ub': variable_ub, + 'variable_lb': variable_lb, 'scaling_variable_ub': scaling_variable_ub, 'scaling_variable_lb': scaling_variable_lb, } @@ -808,123 +683,84 @@ def dual_binary_scaled_bounds( def auto_bounds( model: FlowSystemModel, variable: linopy.Variable, - variable_bounds: Tuple[TemporalData, TemporalData], + bounds: Tuple[TemporalData, TemporalData], scaling_variable: linopy.Variable = None, scaling_state: linopy.Variable = None, scaling_bounds: Tuple[TemporalData, TemporalData] = None, variable_state: linopy.Variable = None, ) -> Tuple[Dict, Dict[str, linopy.Constraint]]: - """Automatically select the appropriate bounds method based on provided parameters. - - This intelligent dispatcher analyzes the provided parameters and automatically - selects the most appropriate bounding method. It simplifies the API by providing - a single entry point for all bounding scenarios. - - Parameter Combinations and Method Selection: - - 1. **Simple Bounds**: Only `bounds` provided - → Creates: lower_bound ≤ variable ≤ upper_bound - - 2. **Scaled Bounds**: `bounds` + `scaling_variable` - → Calls: scaled_bounds() - → Creates: scaling * lower_factor ≤ variable ≤ scaling * upper_factor - - 3. **Binary Controlled**: `bounds` + `binary_control` - → Calls: binary_controlled_bounds() - → Creates: binary * max(ε, lower_bound) ≤ variable ≤ binary * upper_bound - - 4. **Binary + Scaling**: `bounds` + `scaling_variable` + `binary_control` - → Calls: binary_scaled_bounds() - → Creates: binary * max(ε, scaling * lower_factor) ≤ variable ≤ binary * scaling * upper_factor - - 5. **Big-M Dual Control**: All parameters provided - → Calls: big_m_dual_control_bounds() - → Creates: Complex big-M formulation for binary + variable scaling control - - Usage Examples: - ```python - # Simple bounds - auto_bounds(model, var, (0, 100), 'upper', 'lower') - - # Capacity-scaled bounds - auto_bounds(model, flow_var, (0.2, 0.8), 'upper', 'lower', scaling_variable=capacity_var) - - # Binary on/off control - auto_bounds(model, var, (10, 100), 'upper', 'lower', binary_control=on_off_var) + """Automatically select the appropriate bounds method. - # Full dual control - auto_bounds( - model, - var, - (0.1, 0.9), - 'upper', - 'lower', - scaling_variable=size_var, - binary_control=on_var, - scaling_bounds=(0, 1000), - constraint_name_prefix='dual', - ) - ``` + Parameter Combinations: + 1. Only bounds → basic_bounds() + 2. bounds + scaling_variable → scaled_bounds() + 3. bounds + variable_state → binary_controlled_bounds() + 4. bounds + scaling_variable + variable_state → binary_scaled_bounds() + 5. bounds + scaling_variable + scaling_state + variable_state → dual_binary_scaled_bounds() Args: model: The optimization model instance variable: Variable to be bounded - variable_bounds: Tuple of (lower, upper) - absolute bounds or relative factors if scaling + bounds: Tuple of (lower, upper) bounds or relative factors scaling_variable: Optional variable to scale bounds by - scaling_state: Optional binary variable for the state of the scaling variable - scaling_bounds: Required for big-M case - bounds of scaling variable - variable_state: Optional variable that controls the variable state (e.g., on/off) + scaling_state: Optional binary variable for scaling_variable state + scaling_bounds: Required for cases 4,5 - bounds of scaling variable + variable_state: Optional binary variable for variable state Returns: - Tuple containing: - - variables (Dict): Variable dictionary from the selected method - - constraints (Dict[str, linopy.Constraint]): Constraint dictionary from the selected method + Tuple from the selected method Raises: - ValueError: If big-M dual control is detected but required parameters are missing - - Note: - The method prioritizes more complex formulations when multiple options are available. - Parameter validation ensures all required arguments are provided for each case. + ValueError: If required parameters are missing """ - # Case 1: Scaled bounds with state and a state for the variable - if variable_state is not None and scaling_variable is None and scaling_state is None: + # Case 5: Dual binary control + if scaling_variable is not None and scaling_state is not None and variable_state is not None: + if scaling_bounds is None: + raise ValueError('scaling_bounds is required for dual binary control') return BoundingPatterns.dual_binary_scaled_bounds( model=model, variable=variable, - scaling_variable=variable_state, - relative_bounds=variable_bounds, - scaling_binary=variable_state, - secondary_binary=variable_state, + scaling_variable=scaling_variable, + relative_bounds=bounds, + scaling_state=scaling_state, + variable_state=variable_state, scaling_bounds=scaling_bounds, ) - # Case 2: Scaled Bounds with state for the scaled variable - if variable_state is not None and scaling_variable is not None: + # Case 4: Binary scaled bounds + if scaling_variable is not None and variable_state is not None: if scaling_bounds is None: - raise ValueError('scaling_bounds is required when using binary_scaled_bounds to compute big-M values') - + raise ValueError('scaling_bounds is required for binary scaled bounds') return BoundingPatterns.binary_scaled_bounds( model=model, variable=variable, scaling_variable=scaling_variable, - relative_bounds=variable_bounds, - binary_control=variable_state, + relative_bounds=bounds, + variable_state=variable_state, scaling_bounds=scaling_bounds, ) - # Case 3: Binary controlled variable with fixed bounds + # Case 3: Binary controlled bounds if variable_state is not None and scaling_variable is None: return BoundingPatterns.binary_controlled_bounds( model=model, variable=variable, - bounds=variable_bounds, - binary_control=variable_state, + bounds=bounds, + variable_state=variable_state, + ) + + # Case 2: Scaled bounds + if scaling_variable is not None and variable_state is None: + return BoundingPatterns.scaled_bounds( + model=model, + variable=variable, + scaling_variable=scaling_variable, + relative_bounds=bounds, ) - # Case 4: Simple absolute bounds + # Case 1: Basic bounds if scaling_variable is None and variable_state is None: - return BoundingPatterns.basic_bounds(model, variable, variable_bounds) + return BoundingPatterns.basic_bounds(model, variable, bounds) raise ValueError('Invalid combination of arguments') From b248f5872b285f32ba9a96a1d38d08eafe36c8d6 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 18 Jul 2025 19:32:07 +0200 Subject: [PATCH 207/448] Improve BoundingPatterns --- flixopt/elements.py | 2 +- flixopt/features.py | 114 +++++++++++++++++++++++--------- flixopt/modeling.py | 158 ++++++++++++-------------------------------- 3 files changed, 125 insertions(+), 149 deletions(-) diff --git a/flixopt/elements.py b/flixopt/elements.py index 440ac6de4..09fd07fe9 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -357,7 +357,7 @@ def do_modeling(self): self.flow_rate_lower_bound_relative, self.flow_rate_upper_bound_relative, ), - on_variable=self.on_off.on if self.on_off is not None else None, + state_variable=self.on_off.on if self.on_off is not None else None, ), 'investment', ) diff --git a/flixopt/features.py b/flixopt/features.py index 52c1302c2..884665e9b 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -13,7 +13,7 @@ from .core import NonTemporalData, Scalar, TemporalData, FlowSystemDimensions from .interface import InvestParameters, OnOffParameters, Piecewise, PiecewiseEffects from .structure import Model, FlowSystemModel, BaseFeatureModel -from .modeling import ModelingPatterns, ModelingUtilities, ModelingPrimitives +from .modeling import ModelingPatterns, ModelingUtilities, ModelingPrimitives, BoundingPatterns logger = logging.getLogger('flixopt') @@ -29,42 +29,82 @@ def __init__( defining_variable: linopy.Variable, relative_bounds_of_defining_variable: Tuple[TemporalData, TemporalData], label_of_model: Optional[str] = None, - on_variable: Optional[linopy.Variable] = None, + state_variable: Optional[linopy.Variable] = None, ): + """ + This feature model is used to model the investment of a variable. + It applies the corresponding bounds to the variable and the on/off state of the variable. + + Args: + model: The optimization model instance + label_of_element: The label of the parent (Element). Used to construct the full label of the model. + parameters: The parameters of the feature model. + defining_variable: The variable to be invested + relative_bounds_of_defining_variable: The bounds of the variable, with respect to the minimum/maximum investment sizes + label_of_model: The label of the model. This is needed to construct the full label of the model. + state_variable: The variable tracking the state of the variable + """ super().__init__(model, label_of_element=label_of_element, parameters=parameters, label_of_model=label_of_model) self._defining_variable = defining_variable self._relative_bounds_of_defining_variable = relative_bounds_of_defining_variable - self._on_variable = on_variable + self._state_variable = state_variable # Only keep non-variable attributes self.scenario_of_investment: Optional[linopy.Variable] = None self.piecewise_effects: Optional[PiecewiseEffectsModel] = None def create_variables_and_constraints(self): - # Use factory patterns - variables, constraints = ModelingPatterns.investment_sizing_pattern( - model=self._model, - name=self.label_full, - size_bounds=(self.parameters.minimum_or_fixed_size, self.parameters.maximum_or_fixed_size,), - controlled_variables=[self._defining_variable], - control_factors=[self._relative_bounds_of_defining_variable], - state_variables=[self._on_variable], - optional=self.parameters.optional, + constraints = [] + size_min, size_max = (self.parameters.minimum_or_fixed_size, self.parameters.maximum_or_fixed_size) + size = self.add( + self._model.add_variables( + lower=0 if self.parameters.optional else size_min, + upper=size_max, + name=f'{self.label_of_model}|size', + coords=self._model.get_coords(['year', 'scenario']), + ), + 'size', ) - # Register variables (stored in Model's variable tracking) - self.add(variables['size'], 'size') - if 'is_invested' in variables: - self.add(variables['is_invested'], 'is_invested') + constraints += BoundingPatterns.scaled_bounds( + self._model, + variable=self._defining_variable, + scaling_variable=size, + relative_bounds=self._relative_bounds_of_defining_variable, + ) - # Register constraints - for constraint_name, constraint in constraints.items(): - self.add(constraint, constraint_name) + # Optional binary investment decision + if self.parameters.optional: + is_invested = self.add( + self._model.add_variables( + binary=True, name=f'{self.label_of_model}|is_invested', coords=self._model.get_coords(['year', 'scenario']) + ), + 'is_invested', + ) - # Handle scenarios and piecewise effects... - if self._model.flow_system.scenarios is not None: - self._create_bounds_for_scenarios() + if self._state_variable is None: + constraints += BoundingPatterns.bounds_with_state( + self._model, + variable=size, + variable_state=is_invested, + bounds=(self.parameters.minimum_or_fixed_size, self.parameters.maximum_or_fixed_size), + ) + + else: + constraints += BoundingPatterns.scaled_bounds_with_state( + self._model, + variable=self._defining_variable, + variable_state=self._state_variable, + scaling_variable=size, + relative_bounds=self._relative_bounds_of_defining_variable, + scaling_bounds=(self.parameters.minimum_or_fixed_size, self.parameters.maximum_or_fixed_size), + scaling_state=is_invested, + ) + + # Register constraints + for constraint in constraints: + self.add(constraint) if self.parameters.piecewise_effects: self.piecewise_effects = self.add( @@ -137,6 +177,19 @@ def __init__( previous_flow_rates: List[Optional[TemporalData]], label_of_model: Optional[str] = None, ): + """ + This feature model is used to model the on/off state of flow_rate(s). It does not matter of the flow_rates are + bounded by a size variable or by a hard bound. THe used bound here is the absolute highest/lowest bound! + + Args: + model: The optimization model instance + label_of_element: The label of the parent (Element). Used to construct the full label of the model. + parameters: The parameters of the feature model. + flow_rates: The flow_rates to be modeled + flow_rate_bounds: The bounds of the flow_rates, with respect to the minimum/maximum investment sizes + previous_flow_rates: The previous flow_rates + label_of_model: The label of the model. This is needed to construct the full label of the model. + """ super().__init__(model, label_of_element, parameters=parameters, label_of_model=label_of_model) self._flow_rates = flow_rates self._flow_rate_bounds = flow_rate_bounds @@ -155,17 +208,14 @@ def create_variables_and_constraints(self): for i, (flow_rate, (lower_bound, upper_bound)) in enumerate(zip(self._flow_rates, self._flow_rate_bounds)): suffix = f'_{i}' if len(self._flow_rates) > 1 else '' # Use the big_m pattern but without binary control (None) - _, control_constraints = ModelingPrimitives.big_m_binary_bounds( + _, control_constraints = BoundingPatterns.binary_controlled_bounds( model=self._model, variable=flow_rate, - binary_control=None, - size_variable=variables['on'], - relative_bounds=(lower_bound, upper_bound), - upper_bound_name=f'{variables['on'].name}|ub{suffix}', - lower_bound_name=f'{variables['on'].name}|lb{suffix}', + bounds=(lower_bound, upper_bound), + variable_state=variables['on'], ) - constraints[f'ub_{i}'] = control_constraints['upper_bound'] - constraints[f'lb_{i}'] = control_constraints['lower_bound'] + constraints[f'ub{suffix}'] = control_constraints['ub'] + constraints[f'lb{suffix}'] = control_constraints['lb'] # 3. Total duration tracking using existing pattern duration_expr = (variables['on'] * self._model.hours_per_step).sum('time') @@ -216,8 +266,8 @@ def create_variables_and_constraints(self): constraints[f'consecutive_off_{cons_name}'] = cons_constraint # Register all constraints and variables - for constraint_name, constraint in constraints.items(): - self.add(constraint, constraint_name) + for constraint in constraints: + self.add(constraint) for variable_name, variable in variables.items(): self.add(variable, variable_name) diff --git a/flixopt/modeling.py b/flixopt/modeling.py index 359ae24e1..7380dcdec 100644 --- a/flixopt/modeling.py +++ b/flixopt/modeling.py @@ -438,6 +438,7 @@ def basic_bounds( bounds: Tuple[TemporalData, TemporalData], ): """Create simple bounds. + variable ∈ [lower_bound, upper_bound] Mathematical Formulation: lower_bound ≤ variable ≤ upper_bound @@ -457,19 +458,20 @@ def basic_bounds( upper_constraint = model.add_constraints(variable <= upper_bound, name=f'{variable.name}|ub') lower_constraint = model.add_constraints(variable >= lower_bound, name=f'{variable.name}|lb') - return {}, {'ub': upper_constraint, 'lb': lower_constraint} + return [lower_constraint, upper_constraint] @staticmethod - def binary_controlled_bounds( + def bounds_with_state( model: FlowSystemModel, variable: linopy.Variable, bounds: Tuple[TemporalData, TemporalData], variable_state: linopy.Variable, - ) -> Tuple[Dict, Dict[str, linopy.Constraint]]: - """Create bounds controlled by a binary variable. + ) -> List[linopy.Constraint]: + """Constraint a variable to bounds, that can be escaped from to 0 by a binary variable. + variable ∈ {0, [max(ε, lower_bound), upper_bound]} Mathematical Formulation: - variable_state * max(ε, lower_bound) ≤ variable ≤ variable_state * upper_bound + - variable_state * max(ε, lower_bound) ≤ variable ≤ variable_state * upper_bound Use Cases: - Investment decisions @@ -490,16 +492,16 @@ def binary_controlled_bounds( if np.all(lower_bound - upper_bound) < 1e-10: fix_constraint = model.add_constraints( - variable == variable_state * upper_bound, name=f'{variable.name}|fixed_size' + variable == variable_state * upper_bound, name=f'{variable.name}|fix' ) - return {}, {'ub': fix_constraint, 'lb': fix_constraint} + return [fix_constraint] epsilon = np.maximum(CONFIG.modeling.EPSILON, lower_bound) upper_constraint = model.add_constraints(variable <= variable_state * upper_bound, name=f'{variable.name}|ub') lower_constraint = model.add_constraints(variable >= variable_state * epsilon, name=f'{variable.name}|lb') - return {}, {'ub': upper_constraint, 'lb': lower_constraint} + return [lower_constraint, upper_constraint] @staticmethod def scaled_bounds( @@ -507,8 +509,9 @@ def scaled_bounds( variable: linopy.Variable, scaling_variable: linopy.Variable, relative_bounds: Tuple[TemporalData, TemporalData], - ) -> Tuple[Dict, Dict[str, linopy.Constraint]]: - """Create bounds scaled by another variable. + ) -> List[linopy.Constraint]: + """Constraint a variable by scaling bounds, dependent on another variable. + variable ∈ [lower_bound * scaling_variable, upper_bound * scaling_variable] Mathematical Formulation: scaling_variable * lower_factor ≤ variable ≤ scaling_variable * upper_factor @@ -533,20 +536,24 @@ def scaled_bounds( upper_constraint = model.add_constraints(variable <= scaling_variable * rel_upper, name=f'{variable.name}|ub') lower_constraint = model.add_constraints(variable >= scaling_variable * rel_lower, name=f'{variable.name}|lb') - variables = {} - constraints = {'ub': upper_constraint, 'lb': lower_constraint} - return variables, constraints + return [lower_constraint, upper_constraint] @staticmethod - def binary_scaled_bounds( + def scaled_bounds_with_state( model: FlowSystemModel, variable: linopy.Variable, scaling_variable: linopy.Variable, relative_bounds: Tuple[TemporalData, TemporalData], - variable_state: linopy.Variable, scaling_bounds: Tuple[TemporalData, TemporalData], - ) -> Tuple[Dict, Dict[str, linopy.Constraint]]: - """Create scaled bounds controlled by a binary variable. + variable_state: linopy.Variable, + scaling_state: linopy.Variable, + ) -> List[linopy.Constraint]: + """Constraint a variable by scaling bounds, dependent on another variable. + The bounds only apply if variable_state is 1. + + variable ∈ {0, + [max(ε, lower_relative_bound) * scaling_variable, upper_relative_bound * scaling_variable] + } Mathematical Formulation (Big-M): scaling_variable * lower_factor ≤ variable ≤ scaling_variable * upper_factor @@ -572,112 +579,31 @@ def binary_scaled_bounds( - variables (Dict): Empty dict - constraints (Dict[str, linopy.Constraint]): 'ub', 'lb', 'binary_upper', 'binary_lower' """ + rel_lower, rel_upper = relative_bounds scaling_min, scaling_max = scaling_bounds big_m_upper = scaling_max * rel_upper big_m_lower = np.maximum(CONFIG.modeling.EPSILON, scaling_min * rel_lower) - scaling_upper = model.add_constraints( - variable <= scaling_variable * rel_upper, name=f'{variable.name}|scaling_ub' - ) - binary_upper = model.add_constraints(variable <= variable_state * big_m_upper, name=f'{variable.name}|ub') - - scaling_lower = model.add_constraints( - variable >= scaling_variable * rel_lower, name=f'{variable.name}|scaling_lb' - ) - binary_lower = model.add_constraints(variable >= variable_state * big_m_lower, name=f'{variable.name}|lb') - - variables = {} - constraints = { - 'ub': scaling_upper, - 'lb': scaling_lower, - 'binary_upper': binary_upper, - 'binary_lower': binary_lower, - } - return variables, constraints - - @staticmethod - def dual_binary_scaled_bounds( - model: FlowSystemModel, - variable: linopy.Variable, - scaling_variable: linopy.Variable, - relative_bounds: Tuple[TemporalData, TemporalData], - scaling_state: linopy.Variable, - variable_state: linopy.Variable, - scaling_bounds: Tuple[TemporalData, TemporalData], - ) -> Tuple[Dict, Dict[str, linopy.Constraint]]: - """Create bounds with dual binary control over a scaled variable. - - Mathematical Formulation: - scaling_state * max(ε, scaling_min) ≤ scaling_variable ≤ scaling_state * scaling_max - variable_state * max(ε, rel_lower * scaling_min) ≤ variable ≤ variable_state * M - M * (variable_state - 1) + scaling_variable * rel_lower ≤ variable ≤ scaling_variable * rel_upper - - Where: M = rel_upper * scaling_max - - Use Cases: - - Investment + operational control (capacity sizing + on/off dispatch) - - Equipment sizing + utilization - - Args: - model: The optimization model instance - variable: Variable to be bounded - scaling_variable: Variable that scales the bounds - relative_bounds: Tuple of (rel_lower, rel_upper) relative bound multipliers - scaling_state: Binary controlling scaling_variable existence - variable_state: Binary controlling variable operation - scaling_bounds: Tuple of (scaling_min, scaling_max) bounds for scaling_variable - - Returns: - Tuple containing: - - variables (Dict): Empty dict - - constraints (Dict[str, linopy.Constraint]): Multiple constraint keys - """ - rel_lower, rel_upper = relative_bounds - scaling_min, scaling_max = scaling_bounds - - big_m = rel_upper * scaling_max - - # 1. SCALING VARIABLE CONSTRAINTS - epsilon_scaling = np.maximum(CONFIG.modeling.EPSILON, scaling_min) - - scaling_ub = model.add_constraints( - scaling_variable <= scaling_state * scaling_max, name=f'{scaling_variable.name}|ub' + _, constraints = BoundingPatterns.bounds_with_state( + model, + variable=scaling_variable, + bounds=scaling_bounds, + variable_state=scaling_state, ) - scaling_lb = model.add_constraints( - scaling_variable >= scaling_state * epsilon_scaling, name=f'{scaling_variable.name}|lb' - ) - - # 2. VARIABLE STATE CONSTRAINTS - epsilon_variable = np.maximum(CONFIG.modeling.EPSILON, rel_lower * scaling_min) - - variable_ub = model.add_constraints(variable <= variable_state * big_m, name=f'{variable.name}|ub') - - variable_lb = model.add_constraints(variable >= variable_state * epsilon_variable, name=f'{variable.name}|lb') - - # 3. SCALING-DEPENDENT CONSTRAINTS - scaling_variable_ub = model.add_constraints( - variable <= scaling_variable * rel_upper, name=f'{variable.name}|scaling_ub' + scaling_upper = model.add_constraints( + variable <= scaling_variable * rel_upper, name=f'{variable.name}|ub' ) + binary_upper = model.add_constraints(variable <= variable_state * big_m_upper, name=f'{variable_state.name}|ub') - scaling_variable_lb = model.add_constraints( - big_m * (variable_state - 1) + scaling_variable * rel_lower <= variable, - name=f'{variable.name}|scaling_lb', + scaling_lower = model.add_constraints( + variable >= scaling_variable * rel_lower, name=f'{variable.name}|lb' ) + binary_lower = model.add_constraints(variable >= variable_state * big_m_lower, name=f'{variable_state.name}|lb') - variables = {} - constraints = { - 'scaling_ub': scaling_ub, - 'scaling_lb': scaling_lb, - 'variable_ub': variable_ub, - 'variable_lb': variable_lb, - 'scaling_variable_ub': scaling_variable_ub, - 'scaling_variable_lb': scaling_variable_lb, - } - - return variables, constraints + return [scaling_lower, scaling_upper, binary_lower, binary_upper] @staticmethod def auto_bounds( @@ -688,15 +614,15 @@ def auto_bounds( scaling_state: linopy.Variable = None, scaling_bounds: Tuple[TemporalData, TemporalData] = None, variable_state: linopy.Variable = None, - ) -> Tuple[Dict, Dict[str, linopy.Constraint]]: + ) -> List[linopy.Constraint]: """Automatically select the appropriate bounds method. Parameter Combinations: 1. Only bounds → basic_bounds() 2. bounds + scaling_variable → scaled_bounds() - 3. bounds + variable_state → binary_controlled_bounds() + 3. bounds + variable_state → bounds_with_state() 4. bounds + scaling_variable + variable_state → binary_scaled_bounds() - 5. bounds + scaling_variable + scaling_state + variable_state → dual_binary_scaled_bounds() + 5. bounds + scaling_variable + scaling_state + variable_state → scaled_bounds_with_state_on_both_scaling_and_variable() Args: model: The optimization model instance @@ -717,7 +643,7 @@ def auto_bounds( if scaling_variable is not None and scaling_state is not None and variable_state is not None: if scaling_bounds is None: raise ValueError('scaling_bounds is required for dual binary control') - return BoundingPatterns.dual_binary_scaled_bounds( + return BoundingPatterns.scaled_bounds_with_state_on_both_scaling_and_variable( model=model, variable=variable, scaling_variable=scaling_variable, @@ -742,7 +668,7 @@ def auto_bounds( # Case 3: Binary controlled bounds if variable_state is not None and scaling_variable is None: - return BoundingPatterns.binary_controlled_bounds( + return BoundingPatterns.bounds_with_state( model=model, variable=variable, bounds=bounds, From d34445cd59b18838d810b677a852bcd5f3bf4b7b Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 18 Jul 2025 20:06:30 +0200 Subject: [PATCH 208/448] Fix duration Modeling --- flixopt/modeling.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/flixopt/modeling.py b/flixopt/modeling.py index 7380dcdec..083ba8c54 100644 --- a/flixopt/modeling.py +++ b/flixopt/modeling.py @@ -315,6 +315,7 @@ def consecutive_duration_tracking( duration[t] ≥ (state[t-1] - state[t]) * minimum_duration[t-1] ∀t > 0 Args: + name: Name of the duration variable state_variable: Binary state variable to track duration for minimum_duration: Optional minimum consecutive duration maximum_duration: Optional maximum consecutive duration @@ -332,21 +333,21 @@ def consecutive_duration_tracking( lower=0, upper=maximum_duration if maximum_duration is not None else mega, coords=model.get_coords(['time']), - name=f'{name}|duration', + name=name, ) constraints = {} # Upper bound: duration[t] ≤ state[t] * M constraints['ub'] = model.add_constraints( - duration <= state_variable * mega, name=f'{name}|duration_upper_bound' + duration <= state_variable * mega, name=f'{duration.name}|ub' ) # Forward constraint: duration[t+1] ≤ duration[t] + hours_per_step[t] constraints['forward'] = model.add_constraints( duration.isel(time=slice(1, None)) <= duration.isel(time=slice(None, -1)) + hours_per_step.isel(time=slice(None, -1)), - name=f'{name}|duration_forward', + name=f'{duration.name}|forward', ) # Backward constraint: duration[t+1] ≥ duration[t] + hours_per_step[t] + (state[t+1] - 1) * M @@ -355,29 +356,29 @@ def consecutive_duration_tracking( >= duration.isel(time=slice(None, -1)) + hours_per_step.isel(time=slice(None, -1)) + (state_variable.isel(time=slice(1, None)) - 1) * mega, - name=f'{name}|duration_backward', + name=f'{duration.name}|backward', ) # Initial condition: duration[0] = (hours_per_step[0] + previous_duration) * state[0] constraints['initial'] = model.add_constraints( duration.isel(time=0) == (hours_per_step.isel(time=0) + previous_duration) * state_variable.isel(time=0), - name=f'{name}|duration_initial', + name=f'{duration.name}|initial', ) # Minimum duration constraint if provided if minimum_duration is not None: - constraints['minimum'] = model.add_constraints( - duration.isel(time=slice(1, None)) + constraints['lb'] = model.add_constraints( + duration >= (state_variable.isel(time=slice(None, -1)) - state_variable.isel(time=slice(1, None))) * minimum_duration.isel(time=slice(None, -1)), - name=f'{name}|duration_minimum', + name=f'{duration.name}|lb', ) # Handle initial condition for minimum duration if previous_duration > 0 and previous_duration < minimum_duration.isel(time=0).max(): - constraints['initial_minimum'] = model.add_constraints( - state_variable.isel(time=0) == 1, name=f'{name}|duration_initial_minimum' + constraints['initial_lb'] = model.add_constraints( + state_variable.isel(time=0) == 1, name=f'{duration.name}|initial_lb' ) variables = {'duration': duration} From bde07b471b44fc401bc3af13da26a5ca2b783b51 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 18 Jul 2025 23:20:19 +0200 Subject: [PATCH 209/448] Fix On + Size --- flixopt/features.py | 83 +++++++++++++++++++++--------------- flixopt/modeling.py | 101 +++++++++++++++++++++++--------------------- 2 files changed, 101 insertions(+), 83 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index 884665e9b..9ec91fec6 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -67,15 +67,25 @@ def create_variables_and_constraints(self): 'size', ) - constraints += BoundingPatterns.scaled_bounds( - self._model, - variable=self._defining_variable, - scaling_variable=size, - relative_bounds=self._relative_bounds_of_defining_variable, - ) + if self._state_variable is None and not self.parameters.optional: + constraints += BoundingPatterns.scaled_bounds( + self._model, + variable=self._defining_variable, + scaling_variable=size, + relative_bounds=self._relative_bounds_of_defining_variable, + name=self._defining_variable.name if self._state_variable is None else f'{self._defining_variable.name}_state', + ) - # Optional binary investment decision - if self.parameters.optional: + elif self._state_variable is not None and not self.parameters.optional: + constraints += BoundingPatterns.bounds_with_state( + self._model, + variable=self._defining_variable, + variable_state=self._state_variable, + bounds=self._relative_bounds_of_defining_variable, + name=self._defining_variable.name if self._state_variable is None else f'{self._defining_variable.name}_state', + ) + + elif self.parameters.optional: is_invested = self.add( self._model.add_variables( binary=True, name=f'{self.label_of_model}|is_invested', coords=self._model.get_coords(['year', 'scenario']) @@ -91,6 +101,13 @@ def create_variables_and_constraints(self): bounds=(self.parameters.minimum_or_fixed_size, self.parameters.maximum_or_fixed_size), ) + constraints += BoundingPatterns.scaled_bounds( + self._model, + variable=self._defining_variable, + scaling_variable=size, + relative_bounds=self._relative_bounds_of_defining_variable, + ) + else: constraints += BoundingPatterns.scaled_bounds_with_state( self._model, @@ -99,7 +116,7 @@ def create_variables_and_constraints(self): scaling_variable=size, relative_bounds=self._relative_bounds_of_defining_variable, scaling_bounds=(self.parameters.minimum_or_fixed_size, self.parameters.maximum_or_fixed_size), - scaling_state=is_invested, + name=f'{self.label_of_model}|size+on', ) # Register constraints @@ -197,25 +214,23 @@ def __init__( def create_variables_and_constraints(self): variables = {} - constraints = {} + constraints = [] # 1. Main binary state using existing pattern state_vars, state_constraints = ModelingPrimitives.binary_state_pair(self._model, self.label_of_model, use_complement=self.parameters.use_off) variables.update(state_vars) - constraints.update(state_constraints) + constraints += list(state_constraints.values()) # 2. Control variables - use big_m_binary_bounds pattern for consistency for i, (flow_rate, (lower_bound, upper_bound)) in enumerate(zip(self._flow_rates, self._flow_rate_bounds)): - suffix = f'_{i}' if len(self._flow_rates) > 1 else '' - # Use the big_m pattern but without binary control (None) - _, control_constraints = BoundingPatterns.binary_controlled_bounds( + # TODO: Add suffix options + constraints += BoundingPatterns.bounds_with_state( model=self._model, variable=flow_rate, bounds=(lower_bound, upper_bound), variable_state=variables['on'], + name=flow_rate.name, ) - constraints[f'ub{suffix}'] = control_constraints['ub'] - constraints[f'lb{suffix}'] = control_constraints['lb'] # 3. Total duration tracking using existing pattern duration_expr = (variables['on'] * self._model.hours_per_step).sum('time') @@ -225,45 +240,43 @@ def create_variables_and_constraints(self): self.parameters.on_hours_total_max if self.parameters.on_hours_total_max is not None else np.inf),#TODO: self._model.hours_per_step.sum('time').item() + self._get_previous_on_duration()) ) variables['on_hours_total'] = duration_vars['tracker'] - constraints['on_hours_total'] = duration_constraints['tracking'] + constraints += [duration_constraints['tracking']] # 4. Switch tracking using existing pattern if self.parameters.use_switch_on: switch_vars, switch_constraints = ModelingPrimitives.state_transition_variables( - self._model, f'{self.label_of_model}|switches', variables['on'], - previous_state=ModelingUtilities.get_most_recent_state(self._previous_flow_rates) + self._model, f'{self.label_of_model}|switch', variables['on'], + previous_state=ModelingUtilities.get_most_recent_state(self._previous_flow_rates), + max_count=self.parameters.switch_on_total_max, ) - variables.update(switch_vars) - for switch_name, switch_constraint in switch_constraints.items(): - constraints[f'switch_{switch_name}'] = switch_constraint + variables.update({'switch|on': switch_vars['on'], 'switch|off': switch_vars['off'], 'switch|count': switch_vars['count']}) + constraints += list(switch_constraints.values()) # 5. Consecutive on duration using existing pattern if self.parameters.use_consecutive_on_hours: consecutive_on_vars, consecutive_on_constraints = ModelingPrimitives.consecutive_duration_tracking( self._model, - f'{self.label_of_model}|consecutive_on', + f'{self.label_of_model}|consecutive_on_hours', #TODO: Change name variables['on'], minimum_duration=self.parameters.consecutive_on_hours_min, maximum_duration=self.parameters.consecutive_on_hours_max, previous_duration=ModelingUtilities.compute_previous_on_duration(self._previous_flow_rates, self._model.hours_per_step), ) - variables['consecutive_on_duration'] = consecutive_on_vars['duration'] - for cons_name, cons_constraint in consecutive_on_constraints.items(): - constraints[f'consecutive_on_{cons_name}'] = cons_constraint + variables['consecutive_on_hours'] = consecutive_on_vars['duration'] + constraints += list(consecutive_on_constraints.values()) # 6. Consecutive off duration using existing pattern if self.parameters.use_consecutive_off_hours: consecutive_off_vars, consecutive_off_constraints = ModelingPrimitives.consecutive_duration_tracking( self._model, - f'{self.label_of_model}|consecutive_off', + f'{self.label_of_model}|consecutive_off_hours', variables['off'], minimum_duration=self.parameters.consecutive_off_hours_min, maximum_duration=self.parameters.consecutive_off_hours_max, previous_duration=ModelingUtilities.compute_previous_off_duration(self._previous_flow_rates, self._model.hours_per_step), ) - variables['consecutive_off_duration'] = consecutive_off_vars['duration'] - for cons_name, cons_constraint in consecutive_off_constraints.items(): - constraints[f'consecutive_off_{cons_name}'] = cons_constraint + variables['consecutive_off_hours'] = consecutive_off_vars['duration'] + constraints += list(consecutive_off_constraints.values()) # Register all constraints and variables for constraint in constraints: @@ -285,23 +298,23 @@ def off(self) -> Optional[linopy.Variable]: @property def total_on_hours(self) -> Optional[linopy.Variable]: """Total on hours variable""" - return self.get_variable_by_short_name('total_duration') + return self.get_variable_by_short_name('total_on_hours') @property def switch_on(self) -> Optional[linopy.Variable]: """Switch on variable""" - return self.get_variable_by_short_name('switch_on') + return self.get_variable_by_short_name('switch|on') @property def switch_off(self) -> Optional[linopy.Variable]: """Switch off variable""" - return self.get_variable_by_short_name('switch_off') + return self.get_variable_by_short_name('switch|off') @property def switch_on_nr(self) -> Optional[linopy.Variable]: """Number of switch-ons variable""" # This could be added to factory if needed - return None + return self.get_variable_by_short_name('switch|count') @property def consecutive_on_hours(self) -> Optional[linopy.Variable]: @@ -325,7 +338,7 @@ def add_effects(self): target='operation', ) - if self.parameters.effects_per_switch_on and self.switch_on: + if self.parameters.effects_per_switch_on: self._model.effects.add_share_to_effects( name=self.label_of_element, expressions={ diff --git a/flixopt/modeling.py b/flixopt/modeling.py index 083ba8c54..b8e00a723 100644 --- a/flixopt/modeling.py +++ b/flixopt/modeling.py @@ -254,7 +254,8 @@ def expression_tracking_variable( @staticmethod def state_transition_variables( - model: FlowSystemModel, name: str, state_variable, previous_state=0 + model: FlowSystemModel, name: str, state_variable, previous_state=0, + max_count: Optional[int] = None, ) -> Tuple[Dict[str, linopy.Variable], Dict[str, linopy.Constraint]]: """ Creates switch-on/off variables with state transition logic. @@ -269,27 +270,36 @@ def state_transition_variables( variables: {'switch_on': binary_var, 'switch_off': binary_var} constraints: {'transition': constraint, 'initial': constraint, 'mutex': constraint} """ - switch_on = model.add_variables(binary=True, name=f'{name}|switch_on', coords=model.get_coords(['time'])) - switch_off = model.add_variables(binary=True, name=f'{name}|switch_off', coords=model.get_coords(['time'])) + switch_on = model.add_variables(binary=True, name=f'{name}|on', coords=model.get_coords(['time'])) + switch_off = model.add_variables(binary=True, name=f'{name}|off', coords=model.get_coords(['time'])) # State transition constraints for t > 0 transition = model.add_constraints( switch_on.isel(time=slice(1, None)) - switch_off.isel(time=slice(1, None)) == state_variable.isel(time=slice(1, None)) - state_variable.isel(time=slice(None, -1)), - name=f'{name}|state_transition', + name=name, ) # Initial state transition for t = 0 initial = model.add_constraints( switch_on.isel(time=0) - switch_off.isel(time=0) == state_variable.isel(time=0) - previous_state, - name=f'{name}|initial_transition', + name=f'{name}|initial', ) # At most one switch per timestep - mutex = model.add_constraints(switch_on + switch_off <= 1, name=f'{name}|switch_mutex') + mutex = model.add_constraints(switch_on + switch_off <= 1, name=f'{name}|mutex') + + count = model.add_variables( + lower=0, + upper=max_count if max_count is not None else np.inf, + coords=model.get_coords(['year', 'scenario']), + name=f'{name}|count', + ) + + count_constraint = model.add_constraints(count == switch_on.sum('time'), name=f'{name}|count') - variables = {'switch_on': switch_on, 'switch_off': switch_off} - constraints = {'transition': transition, 'initial': initial, 'mutex': mutex} + variables = {'on': switch_on, 'off': switch_off, 'count': count} + constraints = {'transition': transition, 'initial': initial, 'mutex': mutex, 'count': count_constraint} return variables, constraints @@ -437,6 +447,7 @@ def basic_bounds( model: FlowSystemModel, variable: linopy.Variable, bounds: Tuple[TemporalData, TemporalData], + name: str = None, ): """Create simple bounds. variable ∈ [lower_bound, upper_bound] @@ -455,9 +466,10 @@ def basic_bounds( - constraints (Dict[str, linopy.Constraint]): 'ub', 'lb' """ lower_bound, upper_bound = bounds + name = name or f'{variable.name}' - upper_constraint = model.add_constraints(variable <= upper_bound, name=f'{variable.name}|ub') - lower_constraint = model.add_constraints(variable >= lower_bound, name=f'{variable.name}|lb') + upper_constraint = model.add_constraints(variable <= upper_bound, name=f'{name}|ub') + lower_constraint = model.add_constraints(variable >= lower_bound, name=f'{name}|lb') return [lower_constraint, upper_constraint] @@ -467,6 +479,7 @@ def bounds_with_state( variable: linopy.Variable, bounds: Tuple[TemporalData, TemporalData], variable_state: linopy.Variable, + name: str = None, ) -> List[linopy.Constraint]: """Constraint a variable to bounds, that can be escaped from to 0 by a binary variable. variable ∈ {0, [max(ε, lower_bound), upper_bound]} @@ -490,17 +503,18 @@ def bounds_with_state( - constraints (Dict[str, linopy.Constraint]): 'ub', 'lb' """ lower_bound, upper_bound = bounds + name = name or f'{variable.name}' if np.all(lower_bound - upper_bound) < 1e-10: fix_constraint = model.add_constraints( - variable == variable_state * upper_bound, name=f'{variable.name}|fix' + variable == variable_state * upper_bound, name=f'{name}|fix' ) return [fix_constraint] epsilon = np.maximum(CONFIG.modeling.EPSILON, lower_bound) - upper_constraint = model.add_constraints(variable <= variable_state * upper_bound, name=f'{variable.name}|ub') - lower_constraint = model.add_constraints(variable >= variable_state * epsilon, name=f'{variable.name}|lb') + upper_constraint = model.add_constraints(variable <= variable_state * upper_bound, name=f'{name}|ub') + lower_constraint = model.add_constraints(variable >= variable_state * epsilon, name=f'{name}|lb') return [lower_constraint, upper_constraint] @@ -510,6 +524,7 @@ def scaled_bounds( variable: linopy.Variable, scaling_variable: linopy.Variable, relative_bounds: Tuple[TemporalData, TemporalData], + name: str = None, ) -> List[linopy.Constraint]: """Constraint a variable by scaling bounds, dependent on another variable. variable ∈ [lower_bound * scaling_variable, upper_bound * scaling_variable] @@ -533,9 +548,10 @@ def scaled_bounds( - constraints (Dict[str, linopy.Constraint]): 'ub', 'lb' """ rel_lower, rel_upper = relative_bounds + name = name or f'{variable.name}' - upper_constraint = model.add_constraints(variable <= scaling_variable * rel_upper, name=f'{variable.name}|ub') - lower_constraint = model.add_constraints(variable >= scaling_variable * rel_lower, name=f'{variable.name}|lb') + upper_constraint = model.add_constraints(variable <= scaling_variable * rel_upper, name=f'{name}|ub') + lower_constraint = model.add_constraints(variable >= scaling_variable * rel_lower, name=f'{name}|lb') return [lower_constraint, upper_constraint] @@ -547,62 +563,51 @@ def scaled_bounds_with_state( relative_bounds: Tuple[TemporalData, TemporalData], scaling_bounds: Tuple[TemporalData, TemporalData], variable_state: linopy.Variable, - scaling_state: linopy.Variable, + name: str = None, ) -> List[linopy.Constraint]: - """Constraint a variable by scaling bounds, dependent on another variable. - The bounds only apply if variable_state is 1. + """Constraint a variable by scaling bounds with binary state control. - variable ∈ {0, - [max(ε, lower_relative_bound) * scaling_variable, upper_relative_bound * scaling_variable] - } + variable ∈ {0, [max(ε, lower_relative_bound) * scaling_variable, upper_relative_bound * scaling_variable]} Mathematical Formulation (Big-M): - scaling_variable * lower_factor ≤ variable ≤ scaling_variable * upper_factor - variable ≤ variable_state * M_upper - variable ≥ variable_state * M_lower - - Where: M_upper = scaling_max * upper_factor, M_lower = max(ε, scaling_min * lower_factor) + (variable_state - 1) * M_misc + scaling_variable * rel_lower ≤ variable ≤ scaling_variable * rel_upper + variable_state * big_m_lower ≤ variable ≤ variable_state * big_m_upper - Use Cases: - - Equipment with capacity and on/off control - - Variable-size units with operational states + Where: + M_misc = scaling_max * rel_lower + big_m_upper = scaling_max * rel_upper + big_m_lower = max(ε, scaling_min * rel_lower) Args: model: The optimization model instance variable: Variable to be bounded scaling_variable: Variable that scales the bound factors relative_bounds: Tuple of (lower_factor, upper_factor) relative to scaling variable - variable_state: Binary variable for on/off control scaling_bounds: Tuple of (scaling_min, scaling_max) bounds of the scaling variable + variable_state: Binary variable for on/off control + name: Optional name prefix for constraints Returns: - Tuple containing: - - variables (Dict): Empty dict - - constraints (Dict[str, linopy.Constraint]): 'ub', 'lb', 'binary_upper', 'binary_lower' + List[linopy.Constraint]: List of constraint objects """ - rel_lower, rel_upper = relative_bounds scaling_min, scaling_max = scaling_bounds + name = name or f'{variable.name}' - big_m_upper = scaling_max * rel_upper - big_m_lower = np.maximum(CONFIG.modeling.EPSILON, scaling_min * rel_lower) + big_m_misc = scaling_max * rel_lower - _, constraints = BoundingPatterns.bounds_with_state( - model, - variable=scaling_variable, - bounds=scaling_bounds, - variable_state=scaling_state, + scaling_lower = model.add_constraints( + variable >= (variable_state - 1) * big_m_misc + scaling_variable * rel_lower, name=f'{name}|lb2' ) - scaling_upper = model.add_constraints( - variable <= scaling_variable * rel_upper, name=f'{variable.name}|ub' + variable <= scaling_variable * rel_upper, name=f'{name}|ub2' ) - binary_upper = model.add_constraints(variable <= variable_state * big_m_upper, name=f'{variable_state.name}|ub') - scaling_lower = model.add_constraints( - variable >= scaling_variable * rel_lower, name=f'{variable.name}|lb' - ) - binary_lower = model.add_constraints(variable >= variable_state * big_m_lower, name=f'{variable_state.name}|lb') + big_m_upper = scaling_max * rel_upper + big_m_lower = np.maximum(CONFIG.modeling.EPSILON, scaling_min * rel_lower) + + binary_upper = model.add_constraints(variable_state * big_m_upper >= variable, name=f'{name}|ub1') + binary_lower = model.add_constraints(variable_state * big_m_lower <= variable, name=f'{name}|lb1') return [scaling_lower, scaling_upper, binary_lower, binary_upper] From 5861b281e11789333ea1a0f77fc28f7cb4ad1475 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 19 Jul 2025 19:14:13 +0200 Subject: [PATCH 210/448] Fix InvestmentModel --- flixopt/features.py | 62 +++++++++++++++------------------------------ 1 file changed, 21 insertions(+), 41 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index 9ec91fec6..5a0ce6cf1 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -67,58 +67,39 @@ def create_variables_and_constraints(self): 'size', ) - if self._state_variable is None and not self.parameters.optional: + if self.parameters.optional: + is_invested = self.add( + self._model.add_variables( + binary=True, name=f'{self.label_of_model}|is_invested', coords=self._model.get_coords(['year', 'scenario']) + ), + 'is_invested', + ) + constraints += BoundingPatterns.bounds_with_state( + self._model, + variable=size, + variable_state=is_invested, + bounds=(self.parameters.minimum_or_fixed_size, self.parameters.maximum_or_fixed_size), + ) + + if self._state_variable is None: constraints += BoundingPatterns.scaled_bounds( self._model, variable=self._defining_variable, scaling_variable=size, relative_bounds=self._relative_bounds_of_defining_variable, - name=self._defining_variable.name if self._state_variable is None else f'{self._defining_variable.name}_state', ) - elif self._state_variable is not None and not self.parameters.optional: - constraints += BoundingPatterns.bounds_with_state( + else: + constraints += BoundingPatterns.scaled_bounds_with_state( self._model, variable=self._defining_variable, variable_state=self._state_variable, - bounds=self._relative_bounds_of_defining_variable, - name=self._defining_variable.name if self._state_variable is None else f'{self._defining_variable.name}_state', - ) - - elif self.parameters.optional: - is_invested = self.add( - self._model.add_variables( - binary=True, name=f'{self.label_of_model}|is_invested', coords=self._model.get_coords(['year', 'scenario']) - ), - 'is_invested', + scaling_variable=size, + relative_bounds=self._relative_bounds_of_defining_variable, + scaling_bounds=(self.parameters.minimum_or_fixed_size, self.parameters.maximum_or_fixed_size), + name=f'{self.label_of_model}|size+on', ) - if self._state_variable is None: - constraints += BoundingPatterns.bounds_with_state( - self._model, - variable=size, - variable_state=is_invested, - bounds=(self.parameters.minimum_or_fixed_size, self.parameters.maximum_or_fixed_size), - ) - - constraints += BoundingPatterns.scaled_bounds( - self._model, - variable=self._defining_variable, - scaling_variable=size, - relative_bounds=self._relative_bounds_of_defining_variable, - ) - - else: - constraints += BoundingPatterns.scaled_bounds_with_state( - self._model, - variable=self._defining_variable, - variable_state=self._state_variable, - scaling_variable=size, - relative_bounds=self._relative_bounds_of_defining_variable, - scaling_bounds=(self.parameters.minimum_or_fixed_size, self.parameters.maximum_or_fixed_size), - name=f'{self.label_of_model}|size+on', - ) - # Register constraints for constraint in constraints: self.add(constraint) @@ -229,7 +210,6 @@ def create_variables_and_constraints(self): variable=flow_rate, bounds=(lower_bound, upper_bound), variable_state=variables['on'], - name=flow_rate.name, ) # 3. Total duration tracking using existing pattern From 7809ee4119f8e607797a45d9e7b1f917c8f58ebd Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 19 Jul 2025 22:00:29 +0200 Subject: [PATCH 211/448] Fix Models --- flixopt/elements.py | 19 ++++++++++++++++++- flixopt/features.py | 41 +++++++++++++++++------------------------ flixopt/modeling.py | 3 +++ 3 files changed, 38 insertions(+), 25 deletions(-) diff --git a/flixopt/elements.py b/flixopt/elements.py index 09fd07fe9..4fcce79a2 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -15,6 +15,7 @@ from .features import InvestmentModel, OnOffModel, PreventSimultaneousUsageModel, ModelingPatterns, ModelingPrimitives from .interface import InvestParameters, OnOffParameters from .structure import Element, ElementModel, FlowSystemModel, register_class_for_io +from .modeling import BoundingPatterns if TYPE_CHECKING: from .flow_system import FlowSystem @@ -328,6 +329,8 @@ def do_modeling(self): 'flow_rate', ) + default_cons = not (self.element.on_off_parameters is not None and isinstance(self.element.size, InvestParameters)) + # OnOff feature if self.element.on_off_parameters is not None: self.on_off: OnOffModel = self.add( @@ -339,6 +342,7 @@ def do_modeling(self): flow_rate_bounds=[self.flow_rate_bounds_on], previous_flow_rates=[self.element.previous_flow_rate], label_of_model=self.label_of_element, + apply_bounds_to_flow_rates=default_cons, ), 'on_off', ) @@ -357,12 +361,25 @@ def do_modeling(self): self.flow_rate_lower_bound_relative, self.flow_rate_upper_bound_relative, ), - state_variable=self.on_off.on if self.on_off is not None else None, + apply_bounds_to_flow_rates=default_cons, ), 'investment', ) self._investment.do_modeling() + if not default_cons: + constraints = BoundingPatterns.scaled_bounds_with_state( + model=self._model, + variable=self.flow_rate, + scaling_variable=self._investment.size, + relative_bounds=(self.flow_rate_lower_bound_relative, self.flow_rate_upper_bound_relative), + scaling_bounds=(self.element.size.minimum_or_fixed_size, self.element.size.maximum_or_fixed_size), + variable_state=self.on_off.on, + ) + + for constraint in constraints: + self.add(constraint) + # Total flow hours tracking (could use factory pattern) variables, constraints = ModelingPrimitives.expression_tracking_variable( model=self._model, diff --git a/flixopt/features.py b/flixopt/features.py index 5a0ce6cf1..916defa4f 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -29,7 +29,7 @@ def __init__( defining_variable: linopy.Variable, relative_bounds_of_defining_variable: Tuple[TemporalData, TemporalData], label_of_model: Optional[str] = None, - state_variable: Optional[linopy.Variable] = None, + apply_bounds_to_defining_variable: bool = True, ): """ This feature model is used to model the investment of a variable. @@ -42,13 +42,13 @@ def __init__( defining_variable: The variable to be invested relative_bounds_of_defining_variable: The bounds of the variable, with respect to the minimum/maximum investment sizes label_of_model: The label of the model. This is needed to construct the full label of the model. - state_variable: The variable tracking the state of the variable + """ super().__init__(model, label_of_element=label_of_element, parameters=parameters, label_of_model=label_of_model) self._defining_variable = defining_variable self._relative_bounds_of_defining_variable = relative_bounds_of_defining_variable - self._state_variable = state_variable + self._apply_bounds_to_defining_variable = apply_bounds_to_defining_variable # Only keep non-variable attributes self.scenario_of_investment: Optional[linopy.Variable] = None @@ -81,23 +81,12 @@ def create_variables_and_constraints(self): bounds=(self.parameters.minimum_or_fixed_size, self.parameters.maximum_or_fixed_size), ) - if self._state_variable is None: + if self._apply_bounds_to_defining_variable: constraints += BoundingPatterns.scaled_bounds( self._model, variable=self._defining_variable, - scaling_variable=size, - relative_bounds=self._relative_bounds_of_defining_variable, - ) - - else: - constraints += BoundingPatterns.scaled_bounds_with_state( - self._model, - variable=self._defining_variable, - variable_state=self._state_variable, - scaling_variable=size, + scaling_variable=self.size, relative_bounds=self._relative_bounds_of_defining_variable, - scaling_bounds=(self.parameters.minimum_or_fixed_size, self.parameters.maximum_or_fixed_size), - name=f'{self.label_of_model}|size+on', ) # Register constraints @@ -174,6 +163,7 @@ def __init__( flow_rate_bounds: List[Tuple[TemporalData, TemporalData]], previous_flow_rates: List[Optional[TemporalData]], label_of_model: Optional[str] = None, + apply_bounds_to_flow_rates: bool = True, ): """ This feature model is used to model the on/off state of flow_rate(s). It does not matter of the flow_rates are @@ -192,6 +182,7 @@ def __init__( self._flow_rates = flow_rates self._flow_rate_bounds = flow_rate_bounds self._previous_flow_rates = previous_flow_rates + self._apply_bounds_to_flow_rates = apply_bounds_to_flow_rates def create_variables_and_constraints(self): variables = {} @@ -203,14 +194,16 @@ def create_variables_and_constraints(self): constraints += list(state_constraints.values()) # 2. Control variables - use big_m_binary_bounds pattern for consistency - for i, (flow_rate, (lower_bound, upper_bound)) in enumerate(zip(self._flow_rates, self._flow_rate_bounds)): - # TODO: Add suffix options - constraints += BoundingPatterns.bounds_with_state( - model=self._model, - variable=flow_rate, - bounds=(lower_bound, upper_bound), - variable_state=variables['on'], - ) + if self._apply_bounds_to_flow_rates: + for i, (flow_rate, flow_rate_bounds) in enumerate( + zip(self._flow_rates, self._flow_rate_bounds, strict=True) + ): + constraints += BoundingPatterns.bounds_with_state( + model=self._model, + variable=flow_rate, + bounds=flow_rate_bounds, + variable_state=variables['on'], + ) # 3. Total duration tracking using existing pattern duration_expr = (variables['on'] * self._model.hours_per_step).sum('time') diff --git a/flixopt/modeling.py b/flixopt/modeling.py index b8e00a723..98fc65756 100644 --- a/flixopt/modeling.py +++ b/flixopt/modeling.py @@ -550,6 +550,9 @@ def scaled_bounds( rel_lower, rel_upper = relative_bounds name = name or f'{variable.name}' + if np.abs(rel_lower - rel_upper).all() < 10e-10: + return [model.add_constraints(variable == scaling_variable * rel_lower, name=f'{name}|fixed')] + upper_constraint = model.add_constraints(variable <= scaling_variable * rel_upper, name=f'{name}|ub') lower_constraint = model.add_constraints(variable >= scaling_variable * rel_lower, name=f'{name}|lb') From 2bbdb4483f2db250d582a158ad5fb25185b70336 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 19 Jul 2025 22:03:29 +0200 Subject: [PATCH 212/448] Update constraint names in test --- tests/test_flow.py | 282 +++++++++++++++++++++++---------------------- 1 file changed, 145 insertions(+), 137 deletions(-) diff --git a/tests/test_flow.py b/tests/test_flow.py index 9038af1c7..5b99a79f2 100644 --- a/tests/test_flow.py +++ b/tests/test_flow.py @@ -143,8 +143,8 @@ def test_flow_invest(self, basic_flow_system_linopy): assert set(flow.model.constraints) == set( [ 'Sink(Wärme)|total_flow_hours', - 'Sink(Wärme)|flow_rate|upper_bound', - 'Sink(Wärme)|flow_rate|lower_bound', + 'Sink(Wärme)|flow_rate|ub', + 'Sink(Wärme)|flow_rate|lb', ] ) @@ -161,13 +161,13 @@ def test_flow_invest(self, basic_flow_system_linopy): ), ) assert_conequal( - model.constraints['Sink(Wärme)|flow_rate|lower_bound'], + model.constraints['Sink(Wärme)|flow_rate|lb'], flow.model.variables['Sink(Wärme)|flow_rate'] >= flow.model.variables['Sink(Wärme)|size'] * xr.DataArray(np.linspace(0.1, 0.5, timesteps.size), coords=(timesteps,)), ) assert_conequal( - model.constraints['Sink(Wärme)|flow_rate|upper_bound'], + model.constraints['Sink(Wärme)|flow_rate|ub'], flow.model.variables['Sink(Wärme)|flow_rate'] <= flow.model.variables['Sink(Wärme)|size'] * xr.DataArray(np.linspace(0.5, 1, timesteps.size), coords=(timesteps,)), @@ -194,10 +194,10 @@ def test_flow_invest_optional(self, basic_flow_system_linopy): assert set(flow.model.constraints) == set( [ 'Sink(Wärme)|total_flow_hours', - 'Sink(Wärme)|size|lower_bound', - 'Sink(Wärme)|size|upper_bound', - 'Sink(Wärme)|flow_rate|lower_bound', - 'Sink(Wärme)|flow_rate|upper_bound', + 'Sink(Wärme)|size|lb', + 'Sink(Wärme)|size|ub', + 'Sink(Wärme)|flow_rate|lb', + 'Sink(Wärme)|flow_rate|ub', ] ) @@ -215,13 +215,13 @@ def test_flow_invest_optional(self, basic_flow_system_linopy): ), ) assert_conequal( - model.constraints['Sink(Wärme)|flow_rate|lower_bound'], + model.constraints['Sink(Wärme)|flow_rate|lb'], flow.model.variables['Sink(Wärme)|flow_rate'] >= flow.model.variables['Sink(Wärme)|size'] * xr.DataArray(np.linspace(0.1, 0.5, timesteps.size), coords=(timesteps,)), ) assert_conequal( - model.constraints['Sink(Wärme)|flow_rate|upper_bound'], + model.constraints['Sink(Wärme)|flow_rate|ub'], flow.model.variables['Sink(Wärme)|flow_rate'] <= flow.model.variables['Sink(Wärme)|size'] * xr.DataArray(np.linspace(0.5, 1, timesteps.size), coords=(timesteps,)), @@ -229,11 +229,11 @@ def test_flow_invest_optional(self, basic_flow_system_linopy): # Is invested assert_conequal( - model.constraints['Sink(Wärme)|size|upper_bound'], + model.constraints['Sink(Wärme)|size|ub'], flow.model.variables['Sink(Wärme)|size'] <= flow.model.variables['Sink(Wärme)|is_invested'] * 100, ) assert_conequal( - model.constraints['Sink(Wärme)|size|lower_bound'], + model.constraints['Sink(Wärme)|size|lb'], flow.model.variables['Sink(Wärme)|size'] >= flow.model.variables['Sink(Wärme)|is_invested'] * 20, ) @@ -258,10 +258,10 @@ def test_flow_invest_optional_wo_min_size(self, basic_flow_system_linopy): assert set(flow.model.constraints) == set( [ 'Sink(Wärme)|total_flow_hours', - 'Sink(Wärme)|size|upper_bound', - 'Sink(Wärme)|size|lower_bound', - 'Sink(Wärme)|flow_rate|lower_bound', - 'Sink(Wärme)|flow_rate|upper_bound', + 'Sink(Wärme)|size|ub', + 'Sink(Wärme)|size|lb', + 'Sink(Wärme)|flow_rate|lb', + 'Sink(Wärme)|flow_rate|ub', ] ) @@ -279,13 +279,13 @@ def test_flow_invest_optional_wo_min_size(self, basic_flow_system_linopy): ), ) assert_conequal( - model.constraints['Sink(Wärme)|flow_rate|lower_bound'], + model.constraints['Sink(Wärme)|flow_rate|lb'], flow.model.variables['Sink(Wärme)|flow_rate'] >= flow.model.variables['Sink(Wärme)|size'] * xr.DataArray(np.linspace(0.1, 0.5, timesteps.size), coords=(timesteps,)), ) assert_conequal( - model.constraints['Sink(Wärme)|flow_rate|upper_bound'], + model.constraints['Sink(Wärme)|flow_rate|ub'], flow.model.variables['Sink(Wärme)|flow_rate'] <= flow.model.variables['Sink(Wärme)|size'] * xr.DataArray(np.linspace(0.5, 1, timesteps.size), coords=(timesteps,)), @@ -293,11 +293,11 @@ def test_flow_invest_optional_wo_min_size(self, basic_flow_system_linopy): # Is invested assert_conequal( - model.constraints['Sink(Wärme)|size|upper_bound'], + model.constraints['Sink(Wärme)|size|ub'], flow.model.variables['Sink(Wärme)|size'] <= flow.model.variables['Sink(Wärme)|is_invested'] * 100, ) assert_conequal( - model.constraints['Sink(Wärme)|size|lower_bound'], + model.constraints['Sink(Wärme)|size|lb'], flow.model.variables['Sink(Wärme)|size'] >= flow.model.variables['Sink(Wärme)|is_invested'] * 1e-5, ) @@ -322,8 +322,8 @@ def test_flow_invest_wo_min_size_non_optional(self, basic_flow_system_linopy): assert set(flow.model.constraints) == set( [ 'Sink(Wärme)|total_flow_hours', - 'Sink(Wärme)|flow_rate|lower_bound', - 'Sink(Wärme)|flow_rate|upper_bound', + 'Sink(Wärme)|flow_rate|lb', + 'Sink(Wärme)|flow_rate|ub', ] ) @@ -339,13 +339,13 @@ def test_flow_invest_wo_min_size_non_optional(self, basic_flow_system_linopy): ), ) assert_conequal( - model.constraints['Sink(Wärme)|flow_rate|lower_bound'], + model.constraints['Sink(Wärme)|flow_rate|lb'], flow.model.variables['Sink(Wärme)|flow_rate'] >= flow.model.variables['Sink(Wärme)|size'] * xr.DataArray(np.linspace(0.1, 0.5, timesteps.size), coords=(timesteps,)), ) assert_conequal( - model.constraints['Sink(Wärme)|flow_rate|upper_bound'], + model.constraints['Sink(Wärme)|flow_rate|ub'], flow.model.variables['Sink(Wärme)|flow_rate'] <= flow.model.variables['Sink(Wärme)|size'] * xr.DataArray(np.linspace(0.5, 1, timesteps.size), coords=(timesteps,)), @@ -466,8 +466,8 @@ def test_flow_on(self, basic_flow_system_linopy): [ 'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|on_hours_total', - 'Sink(Wärme)|on_con1', - 'Sink(Wärme)|on_con2', + 'Sink(Wärme)|flow_rate|lb', + 'Sink(Wärme)|flow_rate|ub', ] ) # flow_rate @@ -490,12 +490,12 @@ def test_flow_on(self, basic_flow_system_linopy): model.add_variables(lower=0), ) assert_conequal( - model.constraints['Sink(Wärme)|on_con1'], - flow.model.variables['Sink(Wärme)|on'] * 0.2 * 100 <= flow.model.variables['Sink(Wärme)|flow_rate'], + model.constraints['Sink(Wärme)|flow_rate|lb'], + flow.model.variables['Sink(Wärme)|flow_rate'] >= flow.model.variables['Sink(Wärme)|on'] * 0.2 * 100, ) assert_conequal( - model.constraints['Sink(Wärme)|on_con2'], - flow.model.variables['Sink(Wärme)|on'] * 0.8 * 100 >= flow.model.variables['Sink(Wärme)|flow_rate'], + model.constraints['Sink(Wärme)|flow_rate|ub'], + flow.model.variables['Sink(Wärme)|flow_rate']<= flow.model.variables['Sink(Wärme)|on'] * 0.8 * 100, ) assert_conequal( @@ -530,8 +530,8 @@ def test_effects_per_running_hour(self, basic_flow_system_linopy): } assert set(flow.model.constraints) == { 'Sink(Wärme)|total_flow_hours', - 'Sink(Wärme)|on_con1', - 'Sink(Wärme)|on_con2', + 'Sink(Wärme)|flow_rate|lb', + 'Sink(Wärme)|flow_rate|ub', 'Sink(Wärme)|on_hours_total', } @@ -568,51 +568,51 @@ def test_consecutive_on_hours(self, basic_flow_system_linopy): flow_system.add_elements( fx.Sink('Sink', sink=flow)) model = create_linopy_model(flow_system) - assert {'Sink(Wärme)|ConsecutiveOn|hours', 'Sink(Wärme)|on'}.issubset(set(flow.model.variables)) + assert {'Sink(Wärme)|consecutive_on_hours', 'Sink(Wärme)|on'}.issubset(set(flow.model.variables)) - assert {'Sink(Wärme)|ConsecutiveOn|con1', - 'Sink(Wärme)|ConsecutiveOn|con2a', - 'Sink(Wärme)|ConsecutiveOn|con2b', - 'Sink(Wärme)|ConsecutiveOn|initial', - 'Sink(Wärme)|ConsecutiveOn|minimum', + assert {'Sink(Wärme)|consecutive_on_hours|ub', + 'Sink(Wärme)|consecutive_on_hours|forward', + 'Sink(Wärme)|consecutive_on_hours|backward', + 'Sink(Wärme)|consecutive_on_hours|initial', + 'Sink(Wärme)|consecutive_on_hours|lb', }.issubset(set(flow.model.constraints)) assert_var_equal( - model.variables['Sink(Wärme)|ConsecutiveOn|hours'], + model.variables['Sink(Wärme)|consecutive_on_hours'], model.add_variables(lower=0, upper=8, coords=(timesteps,)) ) mega = model.hours_per_step.sum('time') assert_conequal( - model.constraints['Sink(Wärme)|ConsecutiveOn|con1'], - model.variables['Sink(Wärme)|ConsecutiveOn|hours'] <= model.variables['Sink(Wärme)|on'] * mega + model.constraints['Sink(Wärme)|consecutive_on_hours|ub'], + model.variables['Sink(Wärme)|consecutive_on_hours'] <= model.variables['Sink(Wärme)|on'] * mega ) assert_conequal( - model.constraints['Sink(Wärme)|ConsecutiveOn|con2a'], - model.variables['Sink(Wärme)|ConsecutiveOn|hours'].isel(time=slice(1, None)) - <= model.variables['Sink(Wärme)|ConsecutiveOn|hours'].isel(time=slice(None, -1)) + model.hours_per_step.isel(time=slice(None, -1)) + model.constraints['Sink(Wärme)|consecutive_on_hours|forward'], + model.variables['Sink(Wärme)|consecutive_on_hours'].isel(time=slice(1, None)) + <= model.variables['Sink(Wärme)|consecutive_on_hours'].isel(time=slice(None, -1)) + model.hours_per_step.isel(time=slice(None, -1)) ) # eq: duration(t) >= duration(t - 1) + dt(t) + (On(t) - 1) * BIG assert_conequal( - model.constraints['Sink(Wärme)|ConsecutiveOn|con2b'], - model.variables['Sink(Wärme)|ConsecutiveOn|hours'].isel(time=slice(1, None)) - >= model.variables['Sink(Wärme)|ConsecutiveOn|hours'].isel(time=slice(None, -1)) + model.constraints['Sink(Wärme)|consecutive_on_hours|backward'], + model.variables['Sink(Wärme)|consecutive_on_hours'].isel(time=slice(1, None)) + >= model.variables['Sink(Wärme)|consecutive_on_hours'].isel(time=slice(None, -1)) + model.hours_per_step.isel(time=slice(None, -1)) + (model.variables['Sink(Wärme)|on'].isel(time=slice(1, None)) - 1) * mega ) assert_conequal( - model.constraints['Sink(Wärme)|ConsecutiveOn|initial'], - model.variables['Sink(Wärme)|ConsecutiveOn|hours'].isel(time=0) + model.constraints['Sink(Wärme)|consecutive_on_hours|initial'], + model.variables['Sink(Wärme)|consecutive_on_hours'].isel(time=0) == model.variables['Sink(Wärme)|on'].isel(time=0) * model.hours_per_step.isel(time=0), ) assert_conequal( - model.constraints['Sink(Wärme)|ConsecutiveOn|minimum'], - model.variables['Sink(Wärme)|ConsecutiveOn|hours'] + model.constraints['Sink(Wärme)|consecutive_on_hours|lb'], + model.variables['Sink(Wärme)|consecutive_on_hours'] >= (model.variables['Sink(Wärme)|on'].isel(time=slice(None, -1)) - model.variables['Sink(Wärme)|on'].isel(time=slice(1, None))) * 2 ) @@ -635,51 +635,51 @@ def test_consecutive_on_hours_previous(self, basic_flow_system_linopy): flow_system.add_elements( fx.Sink('Sink', sink=flow)) model = create_linopy_model(flow_system) - assert {'Sink(Wärme)|ConsecutiveOn|hours', 'Sink(Wärme)|on'}.issubset(set(flow.model.variables)) + assert {'Sink(Wärme)|consecutive_on_hours', 'Sink(Wärme)|on'}.issubset(set(flow.model.variables)) - assert {'Sink(Wärme)|ConsecutiveOn|con1', - 'Sink(Wärme)|ConsecutiveOn|con2a', - 'Sink(Wärme)|ConsecutiveOn|con2b', - 'Sink(Wärme)|ConsecutiveOn|initial', - 'Sink(Wärme)|ConsecutiveOn|minimum', + assert {'Sink(Wärme)|consecutive_on_hours|lb', + 'Sink(Wärme)|consecutive_on_hours|forward', + 'Sink(Wärme)|consecutive_on_hours|backward', + 'Sink(Wärme)|consecutive_on_hours|initial', + 'Sink(Wärme)|consecutive_on_hours|lb', }.issubset(set(flow.model.constraints)) assert_var_equal( - model.variables['Sink(Wärme)|ConsecutiveOn|hours'], + model.variables['Sink(Wärme)|consecutive_on_hours'], model.add_variables(lower=0, upper=8, coords=(timesteps,)) ) mega = model.hours_per_step.sum('time') + model.hours_per_step.isel(time=0) * 3 assert_conequal( - model.constraints['Sink(Wärme)|ConsecutiveOn|con1'], - model.variables['Sink(Wärme)|ConsecutiveOn|hours'] <= model.variables['Sink(Wärme)|on'] * mega + model.constraints['Sink(Wärme)|consecutive_on_hours|ub'], + model.variables['Sink(Wärme)|consecutive_on_hours'] <= model.variables['Sink(Wärme)|on'] * mega ) assert_conequal( - model.constraints['Sink(Wärme)|ConsecutiveOn|con2a'], - model.variables['Sink(Wärme)|ConsecutiveOn|hours'].isel(time=slice(1, None)) - <= model.variables['Sink(Wärme)|ConsecutiveOn|hours'].isel(time=slice(None, -1)) + model.hours_per_step.isel(time=slice(None, -1)) + model.constraints['Sink(Wärme)|consecutive_on_hours|forward'], + model.variables['Sink(Wärme)|consecutive_on_hours'].isel(time=slice(1, None)) + <= model.variables['Sink(Wärme)|consecutive_on_hours'].isel(time=slice(None, -1)) + model.hours_per_step.isel(time=slice(None, -1)) ) # eq: duration(t) >= duration(t - 1) + dt(t) + (On(t) - 1) * BIG assert_conequal( - model.constraints['Sink(Wärme)|ConsecutiveOn|con2b'], - model.variables['Sink(Wärme)|ConsecutiveOn|hours'].isel(time=slice(1, None)) - >= model.variables['Sink(Wärme)|ConsecutiveOn|hours'].isel(time=slice(None, -1)) + model.constraints['Sink(Wärme)|consecutive_on_hours|backward'], + model.variables['Sink(Wärme)|consecutive_on_hours'].isel(time=slice(1, None)) + >= model.variables['Sink(Wärme)|consecutive_on_hours'].isel(time=slice(None, -1)) + model.hours_per_step.isel(time=slice(None, -1)) + (model.variables['Sink(Wärme)|on'].isel(time=slice(1, None)) - 1) * mega ) assert_conequal( - model.constraints['Sink(Wärme)|ConsecutiveOn|initial'], - model.variables['Sink(Wärme)|ConsecutiveOn|hours'].isel(time=0) + model.constraints['Sink(Wärme)|consecutive_on_hours|initial'], + model.variables['Sink(Wärme)|consecutive_on_hours'].isel(time=0) == model.variables['Sink(Wärme)|on'].isel(time=0) * (model.hours_per_step.isel(time=0) * (1 + 3)), ) assert_conequal( - model.constraints['Sink(Wärme)|ConsecutiveOn|minimum'], - model.variables['Sink(Wärme)|ConsecutiveOn|hours'] + model.constraints['Sink(Wärme)|consecutive_on_hours|lb'], + model.variables['Sink(Wärme)|consecutive_on_hours'] >= (model.variables['Sink(Wärme)|on'].isel(time=slice(None, -1)) - model.variables['Sink(Wärme)|on'].isel(time=slice(1, None))) * 2 ) @@ -701,52 +701,52 @@ def test_consecutive_off_hours(self, basic_flow_system_linopy): flow_system.add_elements( fx.Sink('Sink', sink=flow)) model = create_linopy_model(flow_system) - assert {'Sink(Wärme)|ConsecutiveOff|hours', 'Sink(Wärme)|off'}.issubset(set(flow.model.variables)) + assert {'Sink(Wärme)|consecutive_off_hours', 'Sink(Wärme)|off'}.issubset(set(flow.model.variables)) assert { - 'Sink(Wärme)|ConsecutiveOff|con1', - 'Sink(Wärme)|ConsecutiveOff|con2a', - 'Sink(Wärme)|ConsecutiveOff|con2b', - 'Sink(Wärme)|ConsecutiveOff|initial', - 'Sink(Wärme)|ConsecutiveOff|minimum' + 'Sink(Wärme)|consecutive_off_hours|ub', + 'Sink(Wärme)|consecutive_off_hours|forward', + 'Sink(Wärme)|consecutive_off_hours|backward', + 'Sink(Wärme)|consecutive_off_hours|initial', + 'Sink(Wärme)|consecutive_off_hours|lb' }.issubset(set(flow.model.constraints)) assert_var_equal( - model.variables['Sink(Wärme)|ConsecutiveOff|hours'], + model.variables['Sink(Wärme)|consecutive_off_hours'], model.add_variables(lower=0, upper=12, coords=(timesteps,)) ) mega = model.hours_per_step.sum('time') + model.hours_per_step.isel(time=0) * 1 # previously off for 1h assert_conequal( - model.constraints['Sink(Wärme)|ConsecutiveOff|con1'], - model.variables['Sink(Wärme)|ConsecutiveOff|hours'] <= model.variables['Sink(Wärme)|off'] * mega + model.constraints['Sink(Wärme)|consecutive_off_hours|ub'], + model.variables['Sink(Wärme)|consecutive_off_hours'] <= model.variables['Sink(Wärme)|off'] * mega ) assert_conequal( - model.constraints['Sink(Wärme)|ConsecutiveOff|con2a'], - model.variables['Sink(Wärme)|ConsecutiveOff|hours'].isel(time=slice(1, None)) - <= model.variables['Sink(Wärme)|ConsecutiveOff|hours'].isel(time=slice(None, -1)) + model.hours_per_step.isel(time=slice(None, -1)) + model.constraints['Sink(Wärme)|consecutive_off_hours|forward'], + model.variables['Sink(Wärme)|consecutive_off_hours'].isel(time=slice(1, None)) + <= model.variables['Sink(Wärme)|consecutive_off_hours'].isel(time=slice(None, -1)) + model.hours_per_step.isel(time=slice(None, -1)) ) # eq: duration(t) >= duration(t - 1) + dt(t) + (On(t) - 1) * BIG assert_conequal( - model.constraints['Sink(Wärme)|ConsecutiveOff|con2b'], - model.variables['Sink(Wärme)|ConsecutiveOff|hours'].isel(time=slice(1, None)) - >= model.variables['Sink(Wärme)|ConsecutiveOff|hours'].isel(time=slice(None, -1)) + model.constraints['Sink(Wärme)|consecutive_off_hours|backward'], + model.variables['Sink(Wärme)|consecutive_off_hours'].isel(time=slice(1, None)) + >= model.variables['Sink(Wärme)|consecutive_off_hours'].isel(time=slice(None, -1)) + model.hours_per_step.isel(time=slice(None, -1)) + (model.variables['Sink(Wärme)|off'].isel(time=slice(1, None)) - 1) * mega ) assert_conequal( - model.constraints['Sink(Wärme)|ConsecutiveOff|initial'], - model.variables['Sink(Wärme)|ConsecutiveOff|hours'].isel(time=0) + model.constraints['Sink(Wärme)|consecutive_off_hours|initial'], + model.variables['Sink(Wärme)|consecutive_off_hours'].isel(time=0) == model.variables['Sink(Wärme)|off'].isel(time=0) * (model.hours_per_step.isel(time=0) * (1 + 1)), ) assert_conequal( - model.constraints['Sink(Wärme)|ConsecutiveOff|minimum'], - model.variables['Sink(Wärme)|ConsecutiveOff|hours'] + model.constraints['Sink(Wärme)|consecutive_off_hours|lb'], + model.variables['Sink(Wärme)|consecutive_off_hours'] >= (model.variables['Sink(Wärme)|off'].isel(time=slice(None, -1)) - model.variables['Sink(Wärme)|off'].isel(time=slice(1, None))) * 4 ) @@ -769,52 +769,52 @@ def test_consecutive_off_hours_previous(self, basic_flow_system_linopy): flow_system.add_elements( fx.Sink('Sink', sink=flow)) model = create_linopy_model(flow_system) - assert {'Sink(Wärme)|ConsecutiveOff|hours', 'Sink(Wärme)|off'}.issubset(set(flow.model.variables)) + assert {'Sink(Wärme)|consecutive_off_hours', 'Sink(Wärme)|off'}.issubset(set(flow.model.variables)) assert { - 'Sink(Wärme)|ConsecutiveOff|con1', - 'Sink(Wärme)|ConsecutiveOff|con2a', - 'Sink(Wärme)|ConsecutiveOff|con2b', - 'Sink(Wärme)|ConsecutiveOff|initial', - 'Sink(Wärme)|ConsecutiveOff|minimum' + 'Sink(Wärme)|consecutive_off_hours|ub', + 'Sink(Wärme)|consecutive_off_hours|forward', + 'Sink(Wärme)|consecutive_off_hours|backward', + 'Sink(Wärme)|consecutive_off_hours|initial', + 'Sink(Wärme)|consecutive_off_hours|lb' }.issubset(set(flow.model.constraints)) assert_var_equal( - model.variables['Sink(Wärme)|ConsecutiveOff|hours'], + model.variables['Sink(Wärme)|consecutive_off_hours'], model.add_variables(lower=0, upper=12, coords=(timesteps,)) ) mega = model.hours_per_step.sum('time') + model.hours_per_step.isel(time=0) * 2 assert_conequal( - model.constraints['Sink(Wärme)|ConsecutiveOff|con1'], - model.variables['Sink(Wärme)|ConsecutiveOff|hours'] <= model.variables['Sink(Wärme)|off'] * mega + model.constraints['Sink(Wärme)|consecutive_off_hours|ub'], + model.variables['Sink(Wärme)|consecutive_off_hours'] <= model.variables['Sink(Wärme)|off'] * mega ) assert_conequal( - model.constraints['Sink(Wärme)|ConsecutiveOff|con2a'], - model.variables['Sink(Wärme)|ConsecutiveOff|hours'].isel(time=slice(1, None)) - <= model.variables['Sink(Wärme)|ConsecutiveOff|hours'].isel(time=slice(None, -1)) + model.hours_per_step.isel(time=slice(None, -1)) + model.constraints['Sink(Wärme)|consecutive_off_hours|forward'], + model.variables['Sink(Wärme)|consecutive_off_hours'].isel(time=slice(1, None)) + <= model.variables['Sink(Wärme)|consecutive_off_hours'].isel(time=slice(None, -1)) + model.hours_per_step.isel(time=slice(None, -1)) ) # eq: duration(t) >= duration(t - 1) + dt(t) + (On(t) - 1) * BIG assert_conequal( - model.constraints['Sink(Wärme)|ConsecutiveOff|con2b'], - model.variables['Sink(Wärme)|ConsecutiveOff|hours'].isel(time=slice(1, None)) - >= model.variables['Sink(Wärme)|ConsecutiveOff|hours'].isel(time=slice(None, -1)) + model.constraints['Sink(Wärme)|consecutive_off_hours|backward'], + model.variables['Sink(Wärme)|consecutive_off_hours'].isel(time=slice(1, None)) + >= model.variables['Sink(Wärme)|consecutive_off_hours'].isel(time=slice(None, -1)) + model.hours_per_step.isel(time=slice(None, -1)) + (model.variables['Sink(Wärme)|off'].isel(time=slice(1, None)) - 1) * mega ) assert_conequal( - model.constraints['Sink(Wärme)|ConsecutiveOff|initial'], - model.variables['Sink(Wärme)|ConsecutiveOff|hours'].isel(time=0) + model.constraints['Sink(Wärme)|consecutive_off_hours|initial'], + model.variables['Sink(Wärme)|consecutive_off_hours'].isel(time=0) == model.variables['Sink(Wärme)|off'].isel(time=0) * (model.hours_per_step.isel(time=0) * (1+2)), ) assert_conequal( - model.constraints['Sink(Wärme)|ConsecutiveOff|minimum'], - model.variables['Sink(Wärme)|ConsecutiveOff|hours'] + model.constraints['Sink(Wärme)|consecutive_off_hours|lb'], + model.variables['Sink(Wärme)|consecutive_off_hours'] >= (model.variables['Sink(Wärme)|off'].isel(time=slice(None, -1)) - model.variables['Sink(Wärme)|off'].isel(time=slice(1, None))) * 4 ) @@ -836,26 +836,26 @@ def test_switch_on_constraints(self, basic_flow_system_linopy): model = create_linopy_model(flow_system) # Check that variables exist - assert {'Sink(Wärme)|switch_on', 'Sink(Wärme)|switch_off', 'Sink(Wärme)|switch_on_nr'}.issubset( + assert {'Sink(Wärme)|switch|on', 'Sink(Wärme)|switch|off', 'Sink(Wärme)|switch|count'}.issubset( set(flow.model.variables) ) # Check that constraints exist assert { - 'Sink(Wärme)|switch_con', - 'Sink(Wärme)|initial_switch_con', - 'Sink(Wärme)|switch_on_or_off', - 'Sink(Wärme)|switch_on_nr', + 'Sink(Wärme)|switch', + 'Sink(Wärme)|switch|initial', + 'Sink(Wärme)|switch|mutex', + 'Sink(Wärme)|switch|count', }.issubset(set(flow.model.constraints)) # Check switch_on_nr variable bounds - assert_var_equal(flow.model.variables['Sink(Wärme)|switch_on_nr'], model.add_variables(lower=0, upper=5)) + assert_var_equal(flow.model.variables['Sink(Wärme)|switch|count'], model.add_variables(lower=0, upper=5)) # Verify switch_on_nr constraint (limits number of startups) assert_conequal( - model.constraints['Sink(Wärme)|switch_on_nr'], - flow.model.variables['Sink(Wärme)|switch_on_nr'] - == flow.model.variables['Sink(Wärme)|switch_on'].sum('time'), + model.constraints['Sink(Wärme)|switch|count'], + flow.model.variables['Sink(Wärme)|switch|count'] + == flow.model.variables['Sink(Wärme)|switch|on'].sum('time'), ) # Check that startup cost effect constraint exists @@ -864,7 +864,7 @@ def test_switch_on_constraints(self, basic_flow_system_linopy): # Verify the startup cost effect constraint assert_conequal( model.constraints['Sink(Wärme)->Costs(operation)'], - model.variables['Sink(Wärme)->Costs(operation)'] == flow.model.variables['Sink(Wärme)|switch_on'] * 100, + model.variables['Sink(Wärme)->Costs(operation)'] == flow.model.variables['Sink(Wärme)|switch|on'] * 100, ) def test_on_hours_limits(self, basic_flow_system_linopy): @@ -933,12 +933,12 @@ def test_flow_on_invest_optional(self, basic_flow_system_linopy): [ 'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|on_hours_total', - 'Sink(Wärme)|on_con1', - 'Sink(Wärme)|on_con2', - 'Sink(Wärme)|size|lower_bound', - 'Sink(Wärme)|size|upper_bound', - 'Sink(Wärme)|flow_rate|lower_bound', - 'Sink(Wärme)|flow_rate|upper_bound', + 'Sink(Wärme)|flow_rate|lb1', + 'Sink(Wärme)|flow_rate|ub1', + 'Sink(Wärme)|size|lb', + 'Sink(Wärme)|size|ub', + 'Sink(Wärme)|flow_rate|lb2', + 'Sink(Wärme)|flow_rate|ub2', ] ) @@ -962,11 +962,19 @@ def test_flow_on_invest_optional(self, basic_flow_system_linopy): model.add_variables(lower=0), ) assert_conequal( - model.constraints['Sink(Wärme)|on_con1'], + model.constraints['Sink(Wärme)|size|lb'], + flow.model.variables['Sink(Wärme)|size'] >= flow.model.variables['Sink(Wärme)|is_invested'] * 20, + ) + assert_conequal( + model.constraints['Sink(Wärme)|size|ub'], + flow.model.variables['Sink(Wärme)|size']<= flow.model.variables['Sink(Wärme)|is_invested'] * 200, + ) + assert_conequal( + model.constraints['Sink(Wärme)|flow_rate|lb1'], flow.model.variables['Sink(Wärme)|on'] * 0.2 * 20 <= flow.model.variables['Sink(Wärme)|flow_rate'], ) assert_conequal( - model.constraints['Sink(Wärme)|on_con2'], + model.constraints['Sink(Wärme)|flow_rate|ub1'], flow.model.variables['Sink(Wärme)|on'] * 0.8 * 200 >= flow.model.variables['Sink(Wärme)|flow_rate'], ) assert_conequal( @@ -980,12 +988,12 @@ def test_flow_on_invest_optional(self, basic_flow_system_linopy): mega = 0.2 * 200 # Relative minimum * maximum size assert_conequal( - model.constraints['Sink(Wärme)|flow_rate|lower_bound'], + model.constraints['Sink(Wärme)|flow_rate|lb2'], flow.model.variables['Sink(Wärme)|flow_rate'] >= flow.model.variables['Sink(Wärme)|on'] * mega + flow.model.variables['Sink(Wärme)|size'] * 0.2 - mega, ) assert_conequal( - model.constraints['Sink(Wärme)|flow_rate|upper_bound'], + model.constraints['Sink(Wärme)|flow_rate|ub2'], flow.model.variables['Sink(Wärme)|flow_rate'] <= flow.model.variables['Sink(Wärme)|size'] * 0.8, ) @@ -1017,10 +1025,10 @@ def test_flow_on_invest_non_optional(self, basic_flow_system_linopy): [ 'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|on_hours_total', - 'Sink(Wärme)|on_con1', - 'Sink(Wärme)|on_con2', - 'Sink(Wärme)|flow_rate|lower_bound', - 'Sink(Wärme)|flow_rate|upper_bound', + 'Sink(Wärme)|flow_rate|lb1', + 'Sink(Wärme)|flow_rate|ub1', + 'Sink(Wärme)|flow_rate|lb2', + 'Sink(Wärme)|flow_rate|ub2', ] ) @@ -1044,11 +1052,11 @@ def test_flow_on_invest_non_optional(self, basic_flow_system_linopy): model.add_variables(lower=0), ) assert_conequal( - model.constraints['Sink(Wärme)|on_con1'], + model.constraints['Sink(Wärme)|flow_rate|lb1'], flow.model.variables['Sink(Wärme)|on'] * 0.2 * 20 <= flow.model.variables['Sink(Wärme)|flow_rate'], ) assert_conequal( - model.constraints['Sink(Wärme)|on_con2'], + model.constraints['Sink(Wärme)|flow_rate|ub1'], flow.model.variables['Sink(Wärme)|on'] * 0.8 * 200 >= flow.model.variables['Sink(Wärme)|flow_rate'], ) assert_conequal( @@ -1062,12 +1070,12 @@ def test_flow_on_invest_non_optional(self, basic_flow_system_linopy): mega = 0.2 * 200 # Relative minimum * maximum size assert_conequal( - model.constraints['Sink(Wärme)|flow_rate|lower_bound'], + model.constraints['Sink(Wärme)|flow_rate|lb2'], flow.model.variables['Sink(Wärme)|flow_rate'] >= flow.model.variables['Sink(Wärme)|on'] * mega + flow.model.variables['Sink(Wärme)|size'] * 0.2 - mega, ) assert_conequal( - model.constraints['Sink(Wärme)|flow_rate|upper_bound'], + model.constraints['Sink(Wärme)|flow_rate|ub2'], flow.model.variables['Sink(Wärme)|flow_rate'] <= flow.model.variables['Sink(Wärme)|size'] * 0.8, ) @@ -1122,7 +1130,7 @@ def test_fixed_profile_with_investment(self, basic_flow_system_linopy): # The constraint should link flow_rate to size * profile assert_conequal( - model.constraints['Sink(Wärme)|fix_Sink(Wärme)|flow_rate'], + model.constraints['Sink(Wärme)|flow_rate|fixed'], flow.model.variables['Sink(Wärme)|flow_rate'] == flow.model.variables['Sink(Wärme)|size'] * xr.DataArray(profile, coords=(timesteps,)), ) From 2a01abe31cf6f0e2c1eaff9a40620dcb94099c30 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 19 Jul 2025 22:43:13 +0200 Subject: [PATCH 213/448] Fix OnOffModel for multiple Flows --- flixopt/elements.py | 16 ++++---- flixopt/features.py | 97 ++++++++++++++++++++++++++++++--------------- 2 files changed, 75 insertions(+), 38 deletions(-) diff --git a/flixopt/elements.py b/flixopt/elements.py index 4fcce79a2..627a4afd6 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -361,7 +361,7 @@ def do_modeling(self): self.flow_rate_lower_bound_relative, self.flow_rate_upper_bound_relative, ), - apply_bounds_to_flow_rates=default_cons, + apply_bounds_to_defining_variable=default_cons, ), 'investment', ) @@ -589,13 +589,15 @@ def do_modeling(self): if self.element.on_off_parameters: self.on_off = self.add( OnOffModel( - self._model, - on_off_parameters=self.element.on_off_parameters, + model=self._model, label_of_element=self.label_of_element, - defining_variables=[flow.model.flow_rate for flow in all_flows], - defining_bounds=[flow.model.flow_rate_bounds_on for flow in all_flows], - previous_values=[flow.previous_flow_rate for flow in all_flows], - ) + parameters=self.element.on_off_parameters, + flow_rates=[flow.model.flow_rate for flow in all_flows], + flow_rate_bounds=[flow.model.flow_rate_bounds_on for flow in all_flows], + previous_flow_rates=[flow.previous_flow_rate for flow in all_flows], + label_of_model=self.label_of_element, + apply_bounds_to_flow_rates=True, + ), ) self.on_off.do_modeling() diff --git a/flixopt/features.py b/flixopt/features.py index 916defa4f..5a1a91810 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -185,77 +185,112 @@ def __init__( self._apply_bounds_to_flow_rates = apply_bounds_to_flow_rates def create_variables_and_constraints(self): - variables = {} - constraints = [] - # 1. Main binary state using existing pattern state_vars, state_constraints = ModelingPrimitives.binary_state_pair(self._model, self.label_of_model, use_complement=self.parameters.use_off) - variables.update(state_vars) - constraints += list(state_constraints.values()) + for k, v in state_vars.items(): + self.add(v, k) + for k, v in state_constraints.items(): + self.add(v, k) # 2. Control variables - use big_m_binary_bounds pattern for consistency if self._apply_bounds_to_flow_rates: - for i, (flow_rate, flow_rate_bounds) in enumerate( - zip(self._flow_rates, self._flow_rate_bounds, strict=True) - ): - constraints += BoundingPatterns.bounds_with_state( - model=self._model, - variable=flow_rate, - bounds=flow_rate_bounds, - variable_state=variables['on'], - ) + self._add_defining_constraints() # 3. Total duration tracking using existing pattern - duration_expr = (variables['on'] * self._model.hours_per_step).sum('time') + duration_expr = (self.on * self._model.hours_per_step).sum('time') duration_vars, duration_constraints = ModelingPrimitives.expression_tracking_variable( self._model, f'{self.label_of_model}|on_hours_total', duration_expr, (self.parameters.on_hours_total_min if self.parameters.on_hours_total_min is not None else 0, self.parameters.on_hours_total_max if self.parameters.on_hours_total_max is not None else np.inf),#TODO: self._model.hours_per_step.sum('time').item() + self._get_previous_on_duration()) ) - variables['on_hours_total'] = duration_vars['tracker'] - constraints += [duration_constraints['tracking']] + self.add(duration_vars['tracker'], 'on_hours_total') + self.add(duration_constraints['tracking']) # 4. Switch tracking using existing pattern if self.parameters.use_switch_on: switch_vars, switch_constraints = ModelingPrimitives.state_transition_variables( - self._model, f'{self.label_of_model}|switch', variables['on'], + self._model, f'{self.label_of_model}|switch', self.on, previous_state=ModelingUtilities.get_most_recent_state(self._previous_flow_rates), max_count=self.parameters.switch_on_total_max, ) - variables.update({'switch|on': switch_vars['on'], 'switch|off': switch_vars['off'], 'switch|count': switch_vars['count']}) - constraints += list(switch_constraints.values()) + self.add(switch_vars['on'], 'switch|on') + self.add(switch_vars['off'], 'switch|off') + self.add(switch_vars['count'], 'switch|count') + self.add(switch_constraints['transition']) + self.add(switch_constraints['initial']) + self.add(switch_constraints['mutex']) # 5. Consecutive on duration using existing pattern if self.parameters.use_consecutive_on_hours: consecutive_on_vars, consecutive_on_constraints = ModelingPrimitives.consecutive_duration_tracking( self._model, f'{self.label_of_model}|consecutive_on_hours', #TODO: Change name - variables['on'], + self.on, minimum_duration=self.parameters.consecutive_on_hours_min, maximum_duration=self.parameters.consecutive_on_hours_max, previous_duration=ModelingUtilities.compute_previous_on_duration(self._previous_flow_rates, self._model.hours_per_step), ) - variables['consecutive_on_hours'] = consecutive_on_vars['duration'] - constraints += list(consecutive_on_constraints.values()) + self.add(consecutive_on_vars['duration'], 'consecutive_on_hours') + for constraint in consecutive_on_constraints.values(): + self.add(constraint) # 6. Consecutive off duration using existing pattern if self.parameters.use_consecutive_off_hours: consecutive_off_vars, consecutive_off_constraints = ModelingPrimitives.consecutive_duration_tracking( self._model, f'{self.label_of_model}|consecutive_off_hours', - variables['off'], + self.off, minimum_duration=self.parameters.consecutive_off_hours_min, maximum_duration=self.parameters.consecutive_off_hours_max, previous_duration=ModelingUtilities.compute_previous_off_duration(self._previous_flow_rates, self._model.hours_per_step), ) - variables['consecutive_off_hours'] = consecutive_off_vars['duration'] - constraints += list(consecutive_off_constraints.values()) + self.add(consecutive_off_vars['duration'], 'consecutive_off_hours') + for constraint in consecutive_off_constraints.values(): + self.add(constraint) - # Register all constraints and variables - for constraint in constraints: - self.add(constraint) - for variable_name, variable in variables.items(): - self.add(variable, variable_name) + def _add_defining_constraints(self): + """Add constraints that link defining variables to the on state""" + count = len(self._flow_rates) + + if count == 1: + # Case for a single defining variable + flow_rate = self._flow_rates[0] + lb, ub = self._flow_rate_bounds[0] + + # Constraint: on * lower_bound <= def_var + self.add( + self._model.add_constraints( + self.on * np.maximum(CONFIG.modeling.EPSILON, lb) <= flow_rate, name=f'{self.label_full}|on|lb' + ), + 'on|lb', + ) + + # Constraint: on * upper_bound >= def_var + self.add( + self._model.add_constraints(self.on * ub >= flow_rate, name=f'{self.label_full}|on|ub'), 'on|ub' + ) + else: + # Case for multiple defining variables + ub = sum(bound[1] for bound in self._flow_rate_bounds) / count + lb = CONFIG.modeling.EPSILON #TODO: Can this be a bigger value? (maybe the smallest bound?) + + # Constraint: on * epsilon <= sum(all_defining_variables) + self.add( + self._model.add_constraints( + self.on * lb <= sum(self._flow_rates), name=f'{self.label_full}|on|lb' + ), + 'on|lb', + ) + + # Constraint to ensure all variables are zero when off. + # Divide by count to improve numerical stability (smaller factors) + self.add( + self._model.add_constraints( + self.on * ub >= sum([def_var / count for def_var in self._flow_rates]), + name=f'{self.label_full}|on|ub', + ), + 'on|ub', + ) # Properties access variables from Model's tracking system @property From 1f1ebb702a70a1c10a11b1b975af80f16f1fc618 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 19 Jul 2025 22:43:25 +0200 Subject: [PATCH 214/448] Update constraint names in tests --- tests/test_component.py | 56 +++++++++++++++--------------- tests/test_on_hours_computation.py | 8 ++--- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/tests/test_component.py b/tests/test_component.py index 11b5385c2..fbedbd415 100644 --- a/tests/test_component.py +++ b/tests/test_component.py @@ -78,7 +78,7 @@ def test_on_with_multiple_flows(self, basic_flow_system_linopy): flow_system.add_elements(comp) model = create_linopy_model(flow_system) - assert { + assert set(comp.model.variables) == { 'TestComponent(In1)|flow_rate', 'TestComponent(In1)|total_flow_hours', 'TestComponent(In1)|on', @@ -93,38 +93,38 @@ def test_on_with_multiple_flows(self, basic_flow_system_linopy): 'TestComponent(Out2)|on_hours_total', 'TestComponent|on', 'TestComponent|on_hours_total', - } == set(comp.model.variables) + } - assert { + assert set(comp.model.constraints) == { 'TestComponent(In1)|total_flow_hours', - 'TestComponent(In1)|on_con1', - 'TestComponent(In1)|on_con2', + 'TestComponent(In1)|on|lb', + 'TestComponent(In1)|on|ub', 'TestComponent(In1)|on_hours_total', 'TestComponent(Out1)|total_flow_hours', - 'TestComponent(Out1)|on_con1', - 'TestComponent(Out1)|on_con2', + 'TestComponent(Out1)|on|lb', + 'TestComponent(Out1)|on|ub', 'TestComponent(Out1)|on_hours_total', 'TestComponent(Out2)|total_flow_hours', - 'TestComponent(Out2)|on_con1', - 'TestComponent(Out2)|on_con2', + 'TestComponent(Out2)|on|lb', + 'TestComponent(Out2)|on|ub', 'TestComponent(Out2)|on_hours_total', - 'TestComponent|on_con1', - 'TestComponent|on_con2', + 'TestComponent|on|lb', + 'TestComponent|on|ub', 'TestComponent|on_hours_total', - } == set(comp.model.constraints) + } assert_var_equal(model['TestComponent(Out2)|flow_rate'], model.add_variables(lower=0, upper=300 * ub_out2, coords=(timesteps,))) assert_var_equal(model['TestComponent|on'], model.add_variables(binary=True, coords = (timesteps,))) assert_var_equal(model['TestComponent(Out2)|on'], model.add_variables(binary=True, coords=(timesteps,))) - assert_conequal(model.constraints['TestComponent(Out2)|on_con1'], model.variables['TestComponent(Out2)|on'] * 0.3 * 300 <= model.variables['TestComponent(Out2)|flow_rate']) - assert_conequal(model.constraints['TestComponent(Out2)|on_con2'], model.variables['TestComponent(Out2)|on'] * 300 * ub_out2 >= model.variables['TestComponent(Out2)|flow_rate']) + assert_conequal(model.constraints['TestComponent(Out2)|on|lb'], model.variables['TestComponent(Out2)|on'] * 0.3 * 300 <= model.variables['TestComponent(Out2)|flow_rate']) + assert_conequal(model.constraints['TestComponent(Out2)|on|ub'], model.variables['TestComponent(Out2)|on'] * 300 * ub_out2 >= model.variables['TestComponent(Out2)|flow_rate']) - assert_conequal(model.constraints['TestComponent|on_con1'], + assert_conequal(model.constraints['TestComponent|on|lb'], model.variables['TestComponent|on'] * 1e-5 <= model.variables['TestComponent(In1)|flow_rate'] + model.variables['TestComponent(Out1)|flow_rate'] + model.variables['TestComponent(Out2)|flow_rate']) # TODO: Might there be a better way to no use 1e-5? - assert_conequal(model.constraints['TestComponent|on_con2'], + assert_conequal(model.constraints['TestComponent|on|ub'], model.variables['TestComponent|on'] * (100 + 200 + 300 * ub_out2)/3 >= (model.variables['TestComponent(In1)|flow_rate'] + model.variables['TestComponent(Out1)|flow_rate'] @@ -145,24 +145,24 @@ def test_on_with_single_flow(self, basic_flow_system_linopy): flow_system.add_elements(comp) model = create_linopy_model(flow_system) - assert { + assert set(comp.model.variables) == { 'TestComponent(In1)|flow_rate', 'TestComponent(In1)|total_flow_hours', 'TestComponent(In1)|on', 'TestComponent(In1)|on_hours_total', 'TestComponent|on', 'TestComponent|on_hours_total', - } == set(comp.model.variables) + } - assert { + assert set(comp.model.constraints) == { 'TestComponent(In1)|total_flow_hours', - 'TestComponent(In1)|on_con1', - 'TestComponent(In1)|on_con2', + 'TestComponent(In1)|on|lb', + 'TestComponent(In1)|on|ub', 'TestComponent(In1)|on_hours_total', - 'TestComponent|on_con1', - 'TestComponent|on_con2', + 'TestComponent|on|lb', + 'TestComponent|on|ub', 'TestComponent|on_hours_total', - } == set(comp.model.constraints) + } assert_var_equal( model['TestComponent(In1)|flow_rate'], model.add_variables(lower=0, upper=100, coords=(timesteps,)) @@ -171,20 +171,20 @@ def test_on_with_single_flow(self, basic_flow_system_linopy): assert_var_equal(model['TestComponent(In1)|on'], model.add_variables(binary=True, coords=(timesteps,))) assert_conequal( - model.constraints['TestComponent(In1)|on_con1'], + model.constraints['TestComponent(In1)|on|lb'], model.variables['TestComponent(In1)|on'] * 0.1 * 100 <= model.variables['TestComponent(In1)|flow_rate'], ) assert_conequal( - model.constraints['TestComponent(In1)|on_con2'], + model.constraints['TestComponent(In1)|on|ub'], model.variables['TestComponent(In1)|on'] * 100 >= model.variables['TestComponent(In1)|flow_rate'], ) assert_conequal( - model.constraints['TestComponent|on_con1'], + model.constraints['TestComponent|on|lb'], model.variables['TestComponent|on'] * 0.1 * 100 <= model.variables['TestComponent(In1)|flow_rate'], ) assert_conequal( - model.constraints['TestComponent|on_con2'], + model.constraints['TestComponent|on|ub'], model.variables['TestComponent|on'] * 100 >= model.variables['TestComponent(In1)|flow_rate'], ) diff --git a/tests/test_on_hours_computation.py b/tests/test_on_hours_computation.py index a873bbd12..c8fa113aa 100644 --- a/tests/test_on_hours_computation.py +++ b/tests/test_on_hours_computation.py @@ -1,7 +1,7 @@ import numpy as np import pytest -from flixopt.features import ConsecutiveStateModel, StateModel +from flixopt.modeling import ModelingUtilities class TestComputeConsecutiveDuration: @@ -31,7 +31,7 @@ class TestComputeConsecutiveDuration: ]) def test_compute_duration(self, binary_values, hours_per_timestep, expected): """Test compute_consecutive_duration with various inputs.""" - result = ConsecutiveStateModel.compute_consecutive_hours_in_state(binary_values, hours_per_timestep) + result = ModelingUtilities.compute_consecutive_hours_in_state(binary_values, hours_per_timestep) assert np.isclose(result, expected) @pytest.mark.parametrize("binary_values, hours_per_timestep", [ @@ -41,7 +41,7 @@ def test_compute_duration(self, binary_values, hours_per_timestep, expected): def test_compute_duration_raises_error(self, binary_values, hours_per_timestep): """Test error conditions.""" with pytest.raises(TypeError): - ConsecutiveStateModel.compute_consecutive_hours_in_state(binary_values, hours_per_timestep) + ModelingUtilities.compute_consecutive_hours_in_state(binary_values, hours_per_timestep) class TestComputePreviousOnStates: @@ -76,7 +76,7 @@ class TestComputePreviousOnStates: ) def test_compute_previous_on_states(self, previous_values, expected): """Test compute_previous_on_states with various inputs.""" - result = StateModel.compute_previous_states(previous_values) + result = ModelingUtilities.compute_previous_states(previous_values) np.testing.assert_array_equal(result, expected) @pytest.mark.parametrize("previous_values, epsilon, expected", [ From c7b351fbd688ef742c1ca430c7b05a47941b872d Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 20 Jul 2025 00:01:59 +0200 Subject: [PATCH 215/448] Simplify --- flixopt/elements.py | 12 ++-- flixopt/features.py | 130 +++++++++++++++---------------------------- flixopt/modeling.py | 80 ++++++++++++++------------ flixopt/structure.py | 6 ++ 4 files changed, 103 insertions(+), 125 deletions(-) diff --git a/flixopt/elements.py b/flixopt/elements.py index 627a4afd6..15d17ef92 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -338,9 +338,9 @@ def do_modeling(self): model=self._model, label_of_element=self.label_of_element, parameters=self.element.on_off_parameters, - flow_rates=[self.flow_rate], - flow_rate_bounds=[self.flow_rate_bounds_on], - previous_flow_rates=[self.element.previous_flow_rate], + flow_rate=self.flow_rate, + flow_rate_bounds=self.flow_rate_bounds_on, + previous_flow_rate=self.element.previous_flow_rate, label_of_model=self.label_of_element, apply_bounds_to_flow_rates=default_cons, ), @@ -381,7 +381,7 @@ def do_modeling(self): self.add(constraint) # Total flow hours tracking (could use factory pattern) - variables, constraints = ModelingPrimitives.expression_tracking_variable( + variable, constraint = ModelingPrimitives.expression_tracking_variable( model=self._model, name=f'{self.label_full}|total_flow_hours', tracked_expression=(self.flow_rate * self._model.hours_per_step).sum('time'), @@ -392,8 +392,8 @@ def do_modeling(self): coords=['year', 'scenario'], ) - self.add(variables['tracker'], 'total_flow_hours') - self.add(constraints['tracking'], 'total_flow_hours_tracking') + self.add(variable, 'total_flow_hours') + self.add(constraint, 'total_flow_hours_tracking') # Load factor constraints self._create_bounds_for_load_factor() diff --git a/flixopt/features.py b/flixopt/features.py index 5a1a91810..dab25b49b 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -159,9 +159,9 @@ def __init__( model: FlowSystemModel, label_of_element: str, parameters: OnOffParameters, - flow_rates: List[linopy.Variable], - flow_rate_bounds: List[Tuple[TemporalData, TemporalData]], - previous_flow_rates: List[Optional[TemporalData]], + flow_rate: linopy.Variable, + flow_rate_bounds: Tuple[TemporalData, TemporalData], + previous_flow_rate: Optional[TemporalData], label_of_model: Optional[str] = None, apply_bounds_to_flow_rates: bool = True, ): @@ -173,53 +173,59 @@ def __init__( model: The optimization model instance label_of_element: The label of the parent (Element). Used to construct the full label of the model. parameters: The parameters of the feature model. - flow_rates: The flow_rates to be modeled + flow_rate: The flow_rates to be modeled flow_rate_bounds: The bounds of the flow_rates, with respect to the minimum/maximum investment sizes - previous_flow_rates: The previous flow_rates + previous_flow_rate: The previous flow_rates label_of_model: The label of the model. This is needed to construct the full label of the model. """ super().__init__(model, label_of_element, parameters=parameters, label_of_model=label_of_model) - self._flow_rates = flow_rates + self._flow_rate = flow_rate self._flow_rate_bounds = flow_rate_bounds - self._previous_flow_rates = previous_flow_rates + self._previous_flow_rate = previous_flow_rate self._apply_bounds_to_flow_rates = apply_bounds_to_flow_rates def create_variables_and_constraints(self): # 1. Main binary state using existing pattern - state_vars, state_constraints = ModelingPrimitives.binary_state_pair(self._model, self.label_of_model, use_complement=self.parameters.use_off) - for k, v in state_vars.items(): - self.add(v, k) - for k, v in state_constraints.items(): - self.add(v, k) + on = self.add(self._model.add_variables(binary=True, name=f'{self.label_of_model}|on', coords=self._model.get_coords()), 'on') + if self.parameters.use_off: + off = self.add(self._model.add_variables(binary=True, name=f'{self.label_of_model}|off', coords=self._model.get_coords()), 'off') + self.add(self._model.add_constraints(on + off == 1, name=f'{self.label_of_model}|complementary'), 'complementary') - # 2. Control variables - use big_m_binary_bounds pattern for consistency + # 2. Control variables if self._apply_bounds_to_flow_rates: - self._add_defining_constraints() + self.add_batch(*BoundingPatterns.bounds_with_state( + self._model, + variable=self._flow_rate, + bounds=self._flow_rate_bounds, + variable_state=self.on, + )) # 3. Total duration tracking using existing pattern duration_expr = (self.on * self._model.hours_per_step).sum('time') - duration_vars, duration_constraints = ModelingPrimitives.expression_tracking_variable( + var, con = ModelingPrimitives.expression_tracking_variable( self._model, f'{self.label_of_model}|on_hours_total', duration_expr, (self.parameters.on_hours_total_min if self.parameters.on_hours_total_min is not None else 0, self.parameters.on_hours_total_max if self.parameters.on_hours_total_max is not None else np.inf),#TODO: self._model.hours_per_step.sum('time').item() + self._get_previous_on_duration()) ) - self.add(duration_vars['tracker'], 'on_hours_total') - self.add(duration_constraints['tracking']) + self.add(var, 'on_hours_total') + self.add(con) # 4. Switch tracking using existing pattern if self.parameters.use_switch_on: switch_vars, switch_constraints = ModelingPrimitives.state_transition_variables( self._model, f'{self.label_of_model}|switch', self.on, - previous_state=ModelingUtilities.get_most_recent_state(self._previous_flow_rates), - max_count=self.parameters.switch_on_total_max, + previous_state=ModelingUtilities.get_most_recent_state(self._previous_flow_rate), ) self.add(switch_vars['on'], 'switch|on') self.add(switch_vars['off'], 'switch|off') - self.add(switch_vars['count'], 'switch|count') self.add(switch_constraints['transition']) self.add(switch_constraints['initial']) self.add(switch_constraints['mutex']) + if self.parameters.switch_on_total_max is not None: + count = self.add(self._model.add_variables(lower=0, upper=self.parameters.switch_on_total_max, coords=self._model.get_coords(('year', 'scenario')), name=f'{self.label_of_model}|switch|count'), 'switch|count') + self.add(self._model.add_constraints(count == self.switch_on.sum('time'), name=f'{self.label_of_model}|switch|count'), 'switch|count') + # 5. Consecutive on duration using existing pattern if self.parameters.use_consecutive_on_hours: consecutive_on_vars, consecutive_on_constraints = ModelingPrimitives.consecutive_duration_tracking( @@ -228,7 +234,7 @@ def create_variables_and_constraints(self): self.on, minimum_duration=self.parameters.consecutive_on_hours_min, maximum_duration=self.parameters.consecutive_on_hours_max, - previous_duration=ModelingUtilities.compute_previous_on_duration(self._previous_flow_rates, self._model.hours_per_step), + previous_duration=ModelingUtilities.compute_previous_on_duration([self._previous_flow_rate], self._model.hours_per_step), ) self.add(consecutive_on_vars['duration'], 'consecutive_on_hours') for constraint in consecutive_on_constraints.values(): @@ -242,54 +248,31 @@ def create_variables_and_constraints(self): self.off, minimum_duration=self.parameters.consecutive_off_hours_min, maximum_duration=self.parameters.consecutive_off_hours_max, - previous_duration=ModelingUtilities.compute_previous_off_duration(self._previous_flow_rates, self._model.hours_per_step), + previous_duration=ModelingUtilities.compute_previous_off_duration([self._previous_flow_rate], self._model.hours_per_step), ) self.add(consecutive_off_vars['duration'], 'consecutive_off_hours') for constraint in consecutive_off_constraints.values(): self.add(constraint) - def _add_defining_constraints(self): - """Add constraints that link defining variables to the on state""" - count = len(self._flow_rates) - - if count == 1: - # Case for a single defining variable - flow_rate = self._flow_rates[0] - lb, ub = self._flow_rate_bounds[0] - - # Constraint: on * lower_bound <= def_var - self.add( - self._model.add_constraints( - self.on * np.maximum(CONFIG.modeling.EPSILON, lb) <= flow_rate, name=f'{self.label_full}|on|lb' - ), - 'on|lb', - ) - - # Constraint: on * upper_bound >= def_var - self.add( - self._model.add_constraints(self.on * ub >= flow_rate, name=f'{self.label_full}|on|ub'), 'on|ub' - ) - else: - # Case for multiple defining variables - ub = sum(bound[1] for bound in self._flow_rate_bounds) / count - lb = CONFIG.modeling.EPSILON #TODO: Can this be a bigger value? (maybe the smallest bound?) - - # Constraint: on * epsilon <= sum(all_defining_variables) - self.add( - self._model.add_constraints( - self.on * lb <= sum(self._flow_rates), name=f'{self.label_full}|on|lb' - ), - 'on|lb', + def add_effects(self): + """Add operational effects""" + if self.parameters.effects_per_running_hour: + self._model.effects.add_share_to_effects( + name=self.label_of_element, + expressions={ + effect: self.on * factor * self._model.hours_per_step + for effect, factor in self.parameters.effects_per_running_hour.items() + }, + target='operation', ) - # Constraint to ensure all variables are zero when off. - # Divide by count to improve numerical stability (smaller factors) - self.add( - self._model.add_constraints( - self.on * ub >= sum([def_var / count for def_var in self._flow_rates]), - name=f'{self.label_full}|on|ub', - ), - 'on|ub', + if self.parameters.effects_per_switch_on: + self._model.effects.add_share_to_effects( + name=self.label_of_element, + expressions={ + effect: self.switch_on * factor for effect, factor in self.parameters.effects_per_switch_on.items() + }, + target='operation', ) # Properties access variables from Model's tracking system @@ -334,30 +317,9 @@ def consecutive_off_hours(self) -> Optional[linopy.Variable]: """Consecutive off hours variable""" return self.get_variable_by_short_name('consecutive_off_hours') - def add_effects(self): - """Add operational effects""" - if self.parameters.effects_per_running_hour: - self._model.effects.add_share_to_effects( - name=self.label_of_element, - expressions={ - effect: self.on * factor * self._model.hours_per_step - for effect, factor in self.parameters.effects_per_running_hour.items() - }, - target='operation', - ) - - if self.parameters.effects_per_switch_on: - self._model.effects.add_share_to_effects( - name=self.label_of_element, - expressions={ - effect: self.switch_on * factor for effect, factor in self.parameters.effects_per_switch_on.items() - }, - target='operation', - ) - def _get_previous_on_duration(self): hours_per_step = self._model.hours_per_step.isel(time=0).values.flatten()[0] - return ModelingUtilities.compute_previous_on_duration(self._previous_flow_rates, hours_per_step) + return ModelingUtilities.compute_previous_on_duration([self._previous_flow_rate], hours_per_step) def _get_previous_off_duration(self): hours_per_step = self._model.hours_per_step.isel(time=0).values.flatten()[0] diff --git a/flixopt/modeling.py b/flixopt/modeling.py index 98fc65756..8c539caa2 100644 --- a/flixopt/modeling.py +++ b/flixopt/modeling.py @@ -151,31 +151,23 @@ class ModelingPrimitives: @staticmethod def binary_state_pair( model: FlowSystemModel, name: str, coords: List[str] = None, use_complement: bool = True - ) -> Tuple[Dict[str, linopy.Variable], Dict[str, linopy.Constraint]]: + ) -> Tuple[Tuple[linopy.Variable, linopy.Variable], linopy.Constraint]: """ Creates complementary binary variables with completeness constraint. Mathematical formulation: on[t] + off[t] = 1 ∀t on[t], off[t] ∈ {0, 1} - - Returns: - variables: {'on': binary_var, 'off': binary_var} - constraints: {'complementary': constraint} """ coords = coords or ['time'] on = model.add_variables(binary=True, name=f'{name}|on', coords=model.get_coords(coords)) - if use_complement: - off = model.add_variables(binary=True, name=f'{name}|off', coords=model.get_coords(coords)) + off = model.add_variables(binary=True, name=f'{name}|off', coords=model.get_coords(coords)) - # Constraint: on + off = 1 - complementary = model.add_constraints(on + off == 1, name=f'{name}|complementary') + # Constraint: on + off = 1 + complementary = model.add_constraints(on + off == 1, name=f'{name}|complementary') - variables = {'on': on, 'off': off} - constraints = {'complementary': complementary} - return variables, constraints - return {'on': on}, {} + return (on, off), complementary @staticmethod def proportionally_bounded_variable( @@ -220,7 +212,7 @@ def expression_tracking_variable( tracked_expression, bounds: Tuple[TemporalData, TemporalData] = None, coords: List[str] = None, - ) -> Tuple[Dict[str, linopy.Variable], Dict[str, linopy.Constraint]]: + ) -> Tuple[linopy.Variable, linopy.Constraint]: """ Creates variable that equals a given expression. @@ -247,15 +239,14 @@ def expression_tracking_variable( # Constraint: tracker = expression tracking = model.add_constraints(tracker == tracked_expression, name=f'{name}') - variables = {'tracker': tracker} - constraints = {'tracking': tracking} - - return variables, constraints + return tracker, tracking @staticmethod def state_transition_variables( - model: FlowSystemModel, name: str, state_variable, previous_state=0, - max_count: Optional[int] = None, + model: FlowSystemModel, + name: str, + state_variable: linopy.Variable, + previous_state=0, ) -> Tuple[Dict[str, linopy.Variable], Dict[str, linopy.Constraint]]: """ Creates switch-on/off variables with state transition logic. @@ -289,19 +280,41 @@ def state_transition_variables( # At most one switch per timestep mutex = model.add_constraints(switch_on + switch_off <= 1, name=f'{name}|mutex') + return {'on': switch_on, 'off': switch_off}, {'transition': transition, 'initial': initial, 'mutex': mutex} + + @staticmethod + def sum_up_variable( + model: FlowSystemModel, + variable_to_count: linopy.Variable, + name: str, + bounds: Tuple[NonTemporalData, NonTemporalData] = None, + factor: TemporalData = 1, + ) -> Tuple[linopy.Variable, linopy.Constraint]: + """ + SUms up a variable over time, applying a factor to the variable. + + Args: + model: The optimization model instance + variable_to_count: The variable to be summed up + name: The name of the constraint + bounds: The bounds of the constraint + factor: The factor to be applied to the variable + """ + if bounds is None: + bounds = (0, np.inf) + else: + bounds = (bounds[0] if bounds[0] is not None else 0, bounds[1] if bounds[1] is not None else np.inf) + count = model.add_variables( - lower=0, - upper=max_count if max_count is not None else np.inf, + lower=bounds[0], + upper=bounds[1], coords=model.get_coords(['year', 'scenario']), - name=f'{name}|count', + name=name, ) - count_constraint = model.add_constraints(count == switch_on.sum('time'), name=f'{name}|count') + count_constraint = model.add_constraints(count == (variable_to_count * factor).sum('time'), name=name) - variables = {'on': switch_on, 'off': switch_off, 'count': count} - constraints = {'transition': transition, 'initial': initial, 'mutex': mutex, 'count': count_constraint} - - return variables, constraints + return count, count_constraint @staticmethod def consecutive_duration_tracking( @@ -397,8 +410,8 @@ def consecutive_duration_tracking( @staticmethod def mutual_exclusivity_constraint( - model: FlowSystemModel, name: str, binary_variables: List[linopy.Variable], tolerance: float = 1.1 - ) -> Tuple[Dict, Dict[str, linopy.Constraint]]: + model: FlowSystemModel, name: str, binary_variables: List[linopy.Variable], tolerance: float = 1 + ) -> linopy.Constraint: """ Creates mutual exclusivity constraint for binary variables. @@ -410,7 +423,7 @@ def mutual_exclusivity_constraint( Args: binary_variables: List of binary variables that should be mutually exclusive - tolerance: Upper bound (typically 1.1 for numerical stability) + tolerance: Upper bound Returns: variables: {} (no new variables created) @@ -433,10 +446,7 @@ def mutual_exclusivity_constraint( sum(binary_variables) <= tolerance, name=f'{name}|mutual_exclusivity' ) - variables = {} # No new variables created - constraints = {'mutual_exclusivity': mutual_exclusivity} - - return variables, constraints + return mutual_exclusivity class BoundingPatterns: diff --git a/flixopt/structure.py b/flixopt/structure.py index ec594ca6e..0997a8093 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -750,6 +750,12 @@ def add( ) return item + def add_batch(self, *con_or_var: Union[linopy.Constraint, linopy.Variable]) -> None: + """Add constraints to the model""" + con_or_var = list(con_or_var) + for c_o_v in con_or_var: + self.add(c_o_v) + def filter_variables( self, filter_by: Optional[Literal['binary', 'continuous', 'integer']] = None, From 5d9b591498be73e011e8ef24e61397f97821261d Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 20 Jul 2025 00:18:42 +0200 Subject: [PATCH 216/448] Improve handling of vars/cons and models --- flixopt/structure.py | 84 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 73 insertions(+), 11 deletions(-) diff --git a/flixopt/structure.py b/flixopt/structure.py index 0997a8093..49ef38db8 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -721,41 +721,103 @@ def __init__( self._sub_models_short: Dict[str, str] = {} logger.debug(f'Created {self.__class__.__name__} "{self.label_full}"') + def __getitem__(self, key: str) -> linopy.Variable: + if key in self._variables: + return self.variables_direct[key] + if key in self._variables_short: + return self.variables_direct[self._variables_short[key]] + raise KeyError(f'Variable "{key}" not found in model "{self.label_full}"') + def do_modeling(self): raise NotImplementedError('Every Model needs a do_modeling() method') def add( + self, + item: Union[ + linopy.Variable, linopy.Constraint, 'Model', Dict[str, Union[linopy.Variable, linopy.Constraint, 'Model']] + ], + short_name: Optional[str] = None, + ) -> Union[ + linopy.Variable, linopy.Constraint, 'Model', Dict[str, Union[linopy.Variable, linopy.Constraint, 'Model']] + ]: + """ + Add a variable, constraint, sub-model, or batch of items to the model + + Args: + item: The variable, constraint, sub-model, or dictionary of items to add + short_name: The short name for single items. Ignored for dictionary inputs. + + Returns: + The added item(s) - same type as input + + Examples: + # Single item + self.add(my_variable, 'var_name') + + # Batch of items + self.add({ + 'on': on_variable, + 'off': off_variable, + 'duration': duration_constraint + }) + """ + # Handle dictionary input (batch mode) + if isinstance(item, dict): + return self._add_batch(item) + + # Handle single item + return self._add_single(item, short_name) + + def _add_batch( + self, items: Dict[str, Union[linopy.Variable, linopy.Constraint, 'Model']] + ) -> Dict[str, Union[linopy.Variable, linopy.Constraint, 'Model']]: + """ + Add a batch of items using their dictionary keys as short names + + Args: + items: Dictionary with short_name -> item mapping + + Returns: + The same dictionary for chaining + """ + for short_name, item in items.items(): + self._add_single(item, short_name) + return items + + def _add_single( self, item: Union[linopy.Variable, linopy.Constraint, 'Model'], short_name: Optional[str] = None ) -> Union[linopy.Variable, linopy.Constraint, 'Model']: """ - Add a variable, constraint or sub-model to the model + Add a single variable, constraint or sub-model to the model Args: item: The variable, constraint or sub-model to add to the model short_name: The short name of the variable, constraint or sub-model. If not provided, the full name is used. + + Returns: + The added item for chaining """ - # TODO: Check uniquenes of short names + if short_name is not None and (short_name in self._variables_short or short_name in self._constraints_short or short_name in self._sub_models_short): + raise ValueError(f'Short name "{short_name}" already assigned to model') + # TODO: Check uniqueness of short names if isinstance(item, linopy.Variable): self._variables_direct.append(item.name) - self._variables_short[short_name] = item.name + if short_name is not None: + self._variables_short[short_name] = item.name elif isinstance(item, linopy.Constraint): self._constraints_direct.append(item.name) - self._constraints_short[short_name] = item.name + if short_name is not None: + self._constraints_short[short_name] = item.name elif isinstance(item, Model): self.sub_models.append(item) - self._sub_models_short[item.label_full] = short_name or item.label_full + if short_name is not None: + self._sub_models_short[short_name] = item.label_full else: raise ValueError( f'Item must be a linopy.Variable, linopy.Constraint or flixopt.structure.Model, got {type(item)}' ) return item - def add_batch(self, *con_or_var: Union[linopy.Constraint, linopy.Variable]) -> None: - """Add constraints to the model""" - con_or_var = list(con_or_var) - for c_o_v in con_or_var: - self.add(c_o_v) - def filter_variables( self, filter_by: Optional[Literal['binary', 'continuous', 'integer']] = None, From 5c56b63da274295c0433c14d21a9c8f9d568b698 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 20 Jul 2025 10:33:46 +0200 Subject: [PATCH 217/448] Revising the basic structure of a class Model --- flixopt/structure.py | 222 +++++++++++++++---------------------------- 1 file changed, 77 insertions(+), 145 deletions(-) diff --git a/flixopt/structure.py b/flixopt/structure.py index 49ef38db8..59cb62251 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -712,111 +712,59 @@ def __init__( self.label_of_element = label_of_element self.label_of_model = label_of_model if label_of_model is not None else self.label_of_element - self._variables_direct: List[str] = [] - self._constraints_direct: List[str] = [] - self.sub_models: List[Model] = [] + self._variables: Dict[str, linopy.Variable] = {} # Mapping from short name to variable + self._constraints: Dict[str, linopy.Constraint] = {} # Mapping from short name to constraint + self.sub_models: Dict[str, 'Model'] = {} - self._variables_short: Dict[str, str] = {} - self._constraints_short: Dict[str, str] = {} - self._sub_models_short: Dict[str, str] = {} logger.debug(f'Created {self.__class__.__name__} "{self.label_full}"') - def __getitem__(self, key: str) -> linopy.Variable: - if key in self._variables: - return self.variables_direct[key] - if key in self._variables_short: - return self.variables_direct[self._variables_short[key]] - raise KeyError(f'Variable "{key}" not found in model "{self.label_full}"') - - def do_modeling(self): - raise NotImplementedError('Every Model needs a do_modeling() method') - - def add( - self, - item: Union[ - linopy.Variable, linopy.Constraint, 'Model', Dict[str, Union[linopy.Variable, linopy.Constraint, 'Model']] - ], - short_name: Optional[str] = None, - ) -> Union[ - linopy.Variable, linopy.Constraint, 'Model', Dict[str, Union[linopy.Variable, linopy.Constraint, 'Model']] - ]: - """ - Add a variable, constraint, sub-model, or batch of items to the model - - Args: - item: The variable, constraint, sub-model, or dictionary of items to add - short_name: The short name for single items. Ignored for dictionary inputs. - - Returns: - The added item(s) - same type as input - - Examples: - # Single item - self.add(my_variable, 'var_name') - - # Batch of items - self.add({ - 'on': on_variable, - 'off': off_variable, - 'duration': duration_constraint - }) - """ - # Handle dictionary input (batch mode) - if isinstance(item, dict): - return self._add_batch(item) - - # Handle single item - return self._add_single(item, short_name) - - def _add_batch( - self, items: Dict[str, Union[linopy.Variable, linopy.Constraint, 'Model']] - ) -> Dict[str, Union[linopy.Variable, linopy.Constraint, 'Model']]: - """ - Add a batch of items using their dictionary keys as short names - - Args: - items: Dictionary with short_name -> item mapping - - Returns: - The same dictionary for chaining - """ - for short_name, item in items.items(): - self._add_single(item, short_name) - return items - - def _add_single( - self, item: Union[linopy.Variable, linopy.Constraint, 'Model'], short_name: Optional[str] = None - ) -> Union[linopy.Variable, linopy.Constraint, 'Model']: - """ - Add a single variable, constraint or sub-model to the model + def add_variable(self, short_name: str, **kwargs) -> linopy.Variable: + """Create and add a variable in one step""" + if 'name' not in kwargs: + kwargs['name'] = f'{self.label_of_model}|{short_name}' + + variable = self._model.add_variables(**kwargs) + self.register_variable(variable, short_name) + return variable + + def add_constraint(self, short_name: str, expression, **kwargs) -> linopy.Constraint: + """Create and add a constraint in one step""" + if 'name' not in kwargs: + kwargs['name'] = f'{self.label_of_model}|{short_name}' + + constraint = self._model.add_constraints(expression, **kwargs) + self.register_constraint(constraint, short_name) + return constraint + + def register_variable(self, variable: linopy.Variable, short_name: str = None) -> None: + """Register a variable with the model""" + if short_name is None: + short_name = self._extract_short_name(variable) + if short_name in self._variables: + raise ValueError(f'Short name "{short_name}" already assigned to model') + self._variables[short_name] = variable - Args: - item: The variable, constraint or sub-model to add to the model - short_name: The short name of the variable, constraint or sub-model. If not provided, the full name is used. + def register_constraint(self, constraint: linopy.Constraint, short_name: str = None) -> None: + """Register a constraint with the model""" + if short_name is None: + short_name = self._extract_short_name(constraint) + if short_name in self._constraints: + raise ValueError(f'Short name "{short_name}" already assigned to model') + self._constraints[short_name] = constraint - Returns: - The added item for chaining - """ - if short_name is not None and (short_name in self._variables_short or short_name in self._constraints_short or short_name in self._sub_models_short): + def register_sub_model(self, sub_model: 'Model', short_name: str) -> None: + """Register a sub-model with the model""" + if short_name is None: + short_name = sub_model.__class__.__name__ + if short_name in self.sub_models: raise ValueError(f'Short name "{short_name}" already assigned to model') - # TODO: Check uniqueness of short names - if isinstance(item, linopy.Variable): - self._variables_direct.append(item.name) - if short_name is not None: - self._variables_short[short_name] = item.name - elif isinstance(item, linopy.Constraint): - self._constraints_direct.append(item.name) - if short_name is not None: - self._constraints_short[short_name] = item.name - elif isinstance(item, Model): - self.sub_models.append(item) - if short_name is not None: - self._sub_models_short[short_name] = item.label_full - else: - raise ValueError( - f'Item must be a linopy.Variable, linopy.Constraint or flixopt.structure.Model, got {type(item)}' - ) - return item + self.sub_models[short_name] = sub_model + + def __getitem__(self, key: str) -> linopy.Variable: + """Get a variable by its short name""" + if key in self._variables: + return self._variables[key] + raise KeyError(f'Variable "{key}" not found in model "{self.label_full}"') def filter_variables( self, @@ -841,67 +789,51 @@ def filter_variables( return all_variables[[name for name in all_variables if 'time' in all_variables[name].dims]] raise ValueError(f'Invalid length "{length}", must be one of "scalar", "time" or None') - @property - def label(self) -> str: - return self.label_of_model - @property def label_full(self) -> str: return self.label_of_model @property - def variables_direct(self) -> linopy.Variables: - return self._model.variables[self._variables_direct] + def variables(self) -> linopy.Variables: + return self._model.variables[[var.name for var in self._variables.values()]] @property - def constraints_direct(self) -> linopy.Constraints: - return self._model.constraints[self._constraints_direct] + def constraints(self) -> linopy.Constraints: + return self._model.constraints[[con.name for con in self._constraints.values()]] @property - def _variables(self) -> List[str]: - all_variables = self._variables_direct.copy() - for sub_model in self.sub_models: - for variable in sub_model._variables: - if variable in all_variables: - raise KeyError( - f"Duplicate key found: '{variable}' in both {self.label_full} and {sub_model.label_full}!" - ) - all_variables.append(variable) - return all_variables + def all_sub_models(self) -> List['Model']: + return [model for sub_model in self.sub_models.values() for model in [sub_model] + sub_model.all_sub_models] @property - def _constraints(self) -> List[str]: - all_constraints = self._constraints_direct.copy() - for sub_model in self.sub_models: - for constraint in sub_model._constraints: - if constraint in all_constraints: - raise KeyError(f"Duplicate key found: '{constraint}' in both main model and submodel!") - all_constraints.append(constraint) - return all_constraints + def all_constraints(self) -> linopy.Constraints: + names = [constraint_name for constraint_name in self.constraints] + [ + constraint.name + for sub_model in self.all_sub_models + for constraint in sub_model.constraints.values() + ] - @property - def variables(self) -> linopy.Variables: - return self._model.variables[self._variables] + return self._model.constraints[names] @property - def constraints(self) -> linopy.Constraints: - return self._model.constraints[self._constraints] + def all_variables(self) -> linopy.Variables: + names = [variable_name for variable_name in self.variables] + [ + variable.name + for sub_model in self.all_sub_models + for variable in sub_model.constraints.values() + ] - @property - def all_sub_models(self) -> List['Model']: - return [model for sub_model in self.sub_models for model in [sub_model] + sub_model.all_sub_models] - - def get_variable_by_short_name(self, short_name: str, default_return = None) -> Optional[linopy.Variable]: - """Get variable by short name""" - if short_name not in self._variables_short: - return default_return - return self._model.variables[self._variables_short.get(short_name)] - - def get_constraint_by_short_name(self, short_name: str, default_return = None) -> Optional[linopy.Constraint]: - """Get variable by short name""" - if short_name not in self._constraints_short: - return default_return - return self._model.constraints[self._constraints_short.get(short_name)] + return self._model.variables[names] + + @staticmethod + def _extract_short_name(item: Union[linopy.Variable, linopy.Constraint]) -> str: + """Extract short name from variable's full name""" + # Assumes format like "model_prefix|short_name" + name = str(item.name) + if '|' in name: + return name.split('|')[-1] # Take last part after | + else: + return name # Use full name if no | separator class BaseFeatureModel(Model): From 9d242b6aee8ad2f2270f9738234722914e703211 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 20 Jul 2025 12:08:12 +0200 Subject: [PATCH 218/448] Revising the basic structure of a class Model --- flixopt/calculation.py | 4 +- flixopt/components.py | 164 ++++++++++------------ flixopt/effects.py | 35 ++--- flixopt/elements.py | 122 ++++++++--------- flixopt/features.py | 301 ++++++++++++++++------------------------- flixopt/modeling.py | 2 +- flixopt/structure.py | 84 ++++++++---- 7 files changed, 325 insertions(+), 387 deletions(-) diff --git a/flixopt/calculation.py b/flixopt/calculation.py index 6bf86bb20..438fbeea5 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -116,13 +116,13 @@ def main_results(self) -> Dict[str, Union[Scalar, Dict]]: 'Invested': { model.label_of_element: model.size.solution for component in self.flow_system.components.values() - for model in component.model.all_sub_models + for model in component.model.sub_models if isinstance(model, InvestmentModel) and model.size.solution.max() >= CONFIG.modeling.EPSILON }, 'Not invested': { model.label_of_element: model.size.solution for component in self.flow_system.components.values() - for model in component.model.all_sub_models + for model in component.model.sub_models if isinstance(model, InvestmentModel) and model.size.solution.max() < CONFIG.modeling.EPSILON }, }, diff --git a/flixopt/components.py b/flixopt/components.py index 685928714..6631cb214 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -418,23 +418,17 @@ def do_modeling(self): # equate size of both directions if self.element.balanced: # eq: in1.size = in2.size - self.add( - self._model.add_constraints( - self.element.in1.model._investment.size == self.element.in2.model._investment.size, - name=f'{self.label_full}|same_size', - ), - 'same_size', + self.add_constraints( + self.element.in1.model._investment.size == self.element.in2.model._investment.size, + short_name='same_size', ) def create_transmission_equation(self, name: str, in_flow: Flow, out_flow: Flow) -> linopy.Constraint: """Creates an Equation for the Transmission efficiency and adds it to the model""" # eq: out(t) + on(t)*loss_abs(t) = in(t)*(1 - loss_rel(t)) - con_transmission = self.add( - self._model.add_constraints( - out_flow.model.flow_rate == -in_flow.model.flow_rate * (self.element.relative_losses - 1), - name=f'{self.label_full}|{name}', - ), - name, + con_transmission = self.add_constraints( + out_flow.model.flow_rate == -in_flow.model.flow_rate * (self.element.relative_losses - 1), + short_name=name, ) if self.element.absolute_losses is not None: @@ -464,12 +458,10 @@ def do_modeling(self): used_inputs: Set = all_input_flows & used_flows used_outputs: Set = all_output_flows & used_flows - self.add( - self._model.add_constraints( - sum([flow.model.flow_rate * conv_factors[flow.label] for flow in used_inputs]) - == sum([flow.model.flow_rate * conv_factors[flow.label] for flow in used_outputs]), - name=f'{self.label_full}|conversion_{i}', - ) + self.add_constraints( + sum([flow.model.flow_rate * conv_factors[flow.label] for flow in used_inputs]) + == sum([flow.model.flow_rate * conv_factors[flow.label] for flow in used_outputs]), + short_name=f'conversion_{i}', ) else: @@ -479,14 +471,15 @@ def do_modeling(self): for flow, piecewise in self.element.piecewise_conversion.items() } - self.piecewise_conversion = self.add( + self.piecewise_conversion = self.register_sub_model( PiecewiseModel( model=self._model, label_of_element=self.label_of_element, piecewise_variables=piecewise_conversion, zero_point=self.on_off.on if self.on_off is not None else False, as_time_series=True, - ) + ), + short_name='PiecewiseConversion', ) self.piecewise_conversion.do_modeling() @@ -497,36 +490,26 @@ class StorageModel(ComponentModel): def __init__(self, model: FlowSystemModel, element: Storage): super().__init__(model, element) self.element: Storage = element - self.charge_state: Optional[linopy.Variable] = None - self.netto_discharge: Optional[linopy.Variable] = None - self._investment: Optional[InvestmentModel] = None def do_modeling(self): super().do_modeling() lb, ub = self.absolute_charge_state_bounds - self.charge_state = self.add( - self._model.add_variables( - lower=lb, - upper=ub, - coords=self._model.get_coords(extra_timestep=True), - name=f'{self.label_full}|charge_state', - ), - 'charge_state', - ) - self.netto_discharge = self.add( - self._model.add_variables(coords=self._model.get_coords(), name=f'{self.label_full}|netto_discharge'), - 'netto_discharge', + self.add_variables( + lower=lb, + upper=ub, + coords=self._model.get_coords(extra_timestep=True), + short_name='charge_state', ) + + self.add_variables(coords=self._model.get_coords(), short_name='netto_discharge') + # netto_discharge: # eq: nettoFlow(t) - discharging(t) + charging(t) = 0 - self.add( - self._model.add_constraints( - self.netto_discharge - == self.element.discharging.model.flow_rate - self.element.charging.model.flow_rate, - name=f'{self.label_full}|netto_discharge', - ), - 'netto_discharge', + self.add_constraints( + self.netto_discharge + == self.element.discharging.model.flow_rate - self.element.charging.model.flow_rate, + short_name='netto_discharge', ) charge_state = self.charge_state @@ -537,76 +520,57 @@ def do_modeling(self): eff_charge = self.element.eta_charge eff_discharge = self.element.eta_discharge - self.add( - self._model.add_constraints( - charge_state.isel(time=slice(1, None)) - == charge_state.isel(time=slice(None, -1)) * ((1 - rel_loss) ** hours_per_step) - + charge_rate * eff_charge * hours_per_step - - discharge_rate * eff_discharge * hours_per_step, - name=f'{self.label_full}|charge_state', - ), - 'charge_state', + self.add_constraints( + charge_state.isel(time=slice(1, None)) + == charge_state.isel(time=slice(None, -1)) * ((1 - rel_loss) ** hours_per_step) + + charge_rate * eff_charge * hours_per_step + - discharge_rate * eff_discharge * hours_per_step, + short_name='charge_state', ) if isinstance(self.element.capacity_in_flow_hours, InvestParameters): - self._investment = InvestmentModel( - model=self._model, - label_of_element=self.label_of_element, - parameters=self.element.capacity_in_flow_hours, - defining_variable=self.charge_state, - relative_bounds_of_defining_variable=self.relative_charge_state_bounds, + self.register_sub_model( + InvestmentModel( + model=self._model, + label_of_element=self.label_of_element, + parameters=self.element.capacity_in_flow_hours, + defining_variable=self.charge_state, + relative_bounds_of_defining_variable=self.relative_charge_state_bounds, + ), + short_name='investment', ) - self.sub_models.append(self._investment) self._investment.do_modeling() # Initial charge state self._initial_and_final_charge_state() if self.element.balanced: - self.add( - self._model.add_constraints( - self.element.charging.model._investment.size * 1 == self.element.discharging.model._investment.size * 1, - name=f'{self.label_full}|balanced_sizes', - ), - 'balanced_sizes' + self.add_constraints( + self.element.charging.model._investment.size * 1 == self.element.discharging.model._investment.size * 1, + short_name='balanced_sizes', ) def _initial_and_final_charge_state(self): if self.element.initial_charge_state is not None: - name_short = 'initial_charge_state' - name = f'{self.label_full}|{name_short}' - if isinstance(self.element.initial_charge_state, str): - self.add( - self._model.add_constraints( - self.charge_state.isel(time=0) == self.charge_state.isel(time=-1), name=name - ), - name_short, + self.add_constraints( + self.charge_state.isel(time=0) == self.charge_state.isel(time=-1), short_name='initial_charge_state' ) else: - self.add( - self._model.add_constraints( - self.charge_state.isel(time=0) == self.element.initial_charge_state, name=name - ), - name_short, + self.add_constraints( + self.charge_state.isel(time=0) == self.element.initial_charge_state, short_name='initial_charge_state' ) if self.element.maximal_final_charge_state is not None: - self.add( - self._model.add_constraints( - self.charge_state.isel(time=-1) <= self.element.maximal_final_charge_state, - name=f'{self.label_full}|final_charge_max', - ), - 'final_charge_max', + self.add_constraints( + self.charge_state.isel(time=-1) <= self.element.maximal_final_charge_state, + short_name='final_charge_max', ) if self.element.minimal_final_charge_state is not None: - self.add( - self._model.add_constraints( - self.charge_state.isel(time=-1) >= self.element.minimal_final_charge_state, - name=f'{self.label_full}|final_charge_min', - ), - 'final_charge_min', + self.add_constraints( + self.charge_state.isel(time=-1) >= self.element.minimal_final_charge_state, + short_name='final_charge_min', ) @property @@ -652,6 +616,28 @@ def relative_charge_state_bounds(self) -> Tuple[xr.DataArray, xr.DataArray]: return min_bounds, max_bounds + @property + def _investment(self) -> Optional[InvestmentModel]: + """Deprecated alias for investment""" + return self.investment + + @property + def investment(self) -> Optional[InvestmentModel]: + """OnOff feature""" + if 'investment' not in self.sub_models_direct: + return None + return self.sub_models_direct['investment'] + + @property + def charge_state(self) -> linopy.Variable: + """Charge state variable""" + return self['charge_state'] + + @property + def netto_discharge(self) -> linopy.Variable: + """Netto discharge variable""" + return self['netto_discharge'] + @register_class_for_io class SourceAndSink(Component): diff --git a/flixopt/effects.py b/flixopt/effects.py index 0e4236076..13ee524e5 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -142,7 +142,7 @@ def __init__(self, model: FlowSystemModel, element: Effect): super().__init__(model, element) self.element: Effect = element self.total: Optional[linopy.Variable] = None - self.invest: ShareAllocationModel = self.add( + self.invest: ShareAllocationModel = self.register_sub_model( ShareAllocationModel( model=self._model, dims=('year', 'scenario'), @@ -150,10 +150,11 @@ def __init__(self, model: FlowSystemModel, element: Effect): label_of_model=f'{self.label_of_model}(invest)', total_max=self.element.maximum_invest, total_min=self.element.minimum_invest, - ) + ), + short_name='invest', ) - self.operation: ShareAllocationModel = self.add( + self.operation: ShareAllocationModel = self.register_sub_model( ShareAllocationModel( model=self._model, dims=('time', 'year', 'scenario'), @@ -167,29 +168,22 @@ def __init__(self, model: FlowSystemModel, element: Effect): max_per_hour=self.element.maximum_operation_per_hour if self.element.maximum_operation_per_hour is not None else None, - ) + ), + short_name='operation', ) def do_modeling(self): for model in self.sub_models: model.do_modeling() - self.total = self.add( - self._model.add_variables( - lower=self.element.minimum_total if self.element.minimum_total is not None else -np.inf, - upper=self.element.maximum_total if self.element.maximum_total is not None else np.inf, - coords=self._model.get_coords(['year', 'scenario']), - name=f'{self.label_full}|total', - ), - 'total', + self.total = self.add_variables( + lower=self.element.minimum_total if self.element.minimum_total is not None else -np.inf, + upper=self.element.maximum_total if self.element.maximum_total is not None else np.inf, + coords=self._model.get_coords(['year', 'scenario']), + short_name='total', ) - self.add( - self._model.add_constraints( - self.total == self.operation.total + self.invest.total, name=f'{self.label_full}|total' - ), - 'total', - ) + self.add_constraints(self.total == self.operation.total + self.invest.total, short_name='total') TemporalEffectsUser = Union[TemporalDataUser, Dict[str, TemporalDataUser]] # User-specified Shares to Effects @@ -421,8 +415,9 @@ def add_share_to_penalty(self, name: str, expression: linopy.LinearExpression) - def do_modeling(self): for effect in self.effects: effect.create_model(self._model) - self.penalty = self.add( - ShareAllocationModel(self._model, dims=(), label_of_element='Penalty') + self.penalty = self.register_sub_model( + ShareAllocationModel(self._model, dims=(), label_of_element='Penalty'), + short_name='penalty', ) for model in [effect.model for effect in self.effects] + [self.penalty]: model.do_modeling() diff --git a/flixopt/elements.py b/flixopt/elements.py index 15d17ef92..dec7425a7 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -313,27 +313,20 @@ def __init__(self, model: FlowSystemModel, element: Flow): super().__init__(model, element) self.element: Flow = element - # Feature models (set by do_modeling) - self.on_off: Optional[OnOffModel] = None - self._investment: Optional[InvestmentModel] = None - def do_modeling(self): # Main flow rate variable - self.add( - self._model.add_variables( - lower=self.flow_rate_lower_bound, - upper=self.flow_rate_upper_bound, - coords=self._model.get_coords(), - name=f'{self.label_full}|flow_rate', - ), - 'flow_rate', + self.add_variables( + lower=self.flow_rate_lower_bound, + upper=self.flow_rate_upper_bound, + coords=self._model.get_coords(), + short_name='flow_rate', ) default_cons = not (self.element.on_off_parameters is not None and isinstance(self.element.size, InvestParameters)) # OnOff feature if self.element.on_off_parameters is not None: - self.on_off: OnOffModel = self.add( + self.register_sub_model( OnOffModel( model=self._model, label_of_element=self.label_of_element, @@ -344,13 +337,12 @@ def do_modeling(self): label_of_model=self.label_of_element, apply_bounds_to_flow_rates=default_cons, ), - 'on_off', - ) - self.on_off.do_modeling() + short_name='on_off', + ).do_modeling() # Investment feature if isinstance(self.element.size, InvestParameters): - self._investment: InvestmentModel = self.add( + self.register_sub_model( InvestmentModel( model=self._model, label_of_element=self.label_of_element, @@ -364,12 +356,11 @@ def do_modeling(self): apply_bounds_to_defining_variable=default_cons, ), 'investment', - ) - self._investment.do_modeling() + ).do_modeling() if not default_cons: - constraints = BoundingPatterns.scaled_bounds_with_state( - model=self._model, + BoundingPatterns.scaled_bounds_with_state( + model=self, variable=self.flow_rate, scaling_variable=self._investment.size, relative_bounds=(self.flow_rate_lower_bound_relative, self.flow_rate_upper_bound_relative), @@ -377,12 +368,9 @@ def do_modeling(self): variable_state=self.on_off.on, ) - for constraint in constraints: - self.add(constraint) - - # Total flow hours tracking (could use factory pattern) - variable, constraint = ModelingPrimitives.expression_tracking_variable( - model=self._model, + # Total flow hours tracking + ModelingPrimitives.expression_tracking_variable( + model=self, name=f'{self.label_full}|total_flow_hours', tracked_expression=(self.flow_rate * self._model.hours_per_step).sum('time'), bounds=( @@ -392,9 +380,6 @@ def do_modeling(self): coords=['year', 'scenario'], ) - self.add(variable, 'total_flow_hours') - self.add(constraint, 'total_flow_hours_tracking') - # Load factor constraints self._create_bounds_for_load_factor() @@ -403,14 +388,14 @@ def do_modeling(self): # Properties for clean access to variables @property - def flow_rate(self) -> Optional[linopy.Variable]: + def flow_rate(self) -> linopy.Variable: """Main flow rate variable""" - return self.get_variable_by_short_name('flow_rate') + return self['flow_rate'] @property - def total_flow_hours(self) -> Optional[linopy.Variable]: + def total_flow_hours(self) -> linopy.Variable: """Total flow hours variable""" - return self.get_variable_by_short_name('total_flow_hours') + return self['total_flow_hours'] def results_structure(self): return { @@ -440,23 +425,17 @@ def _create_bounds_for_load_factor(self): # Maximum load factor constraint if self.element.load_factor_max is not None: flow_hours_per_size_max = self._model.hours_per_step.sum('time') * self.element.load_factor_max - self.add( - self._model.add_constraints( - self.total_flow_hours <= size * flow_hours_per_size_max, - name=f'{self.label_full}|load_factor_max', - ), - 'load_factor_max', + self.add_constraints( + self.total_flow_hours <= size * flow_hours_per_size_max, + short_name='load_factor_max', ) # Minimum load factor constraint if self.element.load_factor_min is not None: flow_hours_per_size_min = self._model.hours_per_step.sum('time') * self.element.load_factor_min - self.add( - self._model.add_constraints( - self.total_flow_hours >= size * flow_hours_per_size_min, - name=f'{self.label_full}|load_factor_min', - ), - 'load_factor_min', + self.add_constraints( + self.total_flow_hours >= size * flow_hours_per_size_min, + short_name='load_factor_min', ) @property @@ -512,6 +491,25 @@ def flow_rate_upper_bound(self) -> TemporalData: return self.flow_rate_upper_bound_relative * self.element.size.maximum_or_fixed_size return self.flow_rate_upper_bound_relative * self.element.size + @property + def on_off(self) -> Optional[OnOffModel]: + """OnOff feature""" + if 'on_off' not in self.sub_models_direct: + return None + return self.sub_models_direct['on_off'] + + @property + def _investment(self) -> Optional[InvestmentModel]: + """Deprecated alias for investment""" + return self.investment + + @property + def investment(self) -> Optional[InvestmentModel]: + """OnOff feature""" + if 'investment' not in self.sub_models_direct: + return None + return self.sub_models_direct['investment'] + class BusModel(ElementModel): def __init__(self, model: FlowSystemModel, element: Bus): @@ -523,28 +521,19 @@ def __init__(self, model: FlowSystemModel, element: Bus): def do_modeling(self) -> None: # inputs == outputs for flow in self.element.inputs + self.element.outputs: - self.add(flow.model.flow_rate, flow.label_full) + self.register_variable(flow.model.flow_rate, flow.label_full) inputs = sum([flow.model.flow_rate for flow in self.element.inputs]) outputs = sum([flow.model.flow_rate for flow in self.element.outputs]) - eq_bus_balance = self.add(self._model.add_constraints(inputs == outputs, name=f'{self.label_full}|balance')) + eq_bus_balance = self.add_constraints(inputs == outputs, short_name='balance') # Fehlerplus/-minus: if self.element.with_excess: - excess_penalty = np.multiply( - self._model.hours_per_step, self.element.excess_penalty_per_flow_hour - ) - self.excess_input = self.add( - self._model.add_variables( - lower=0, coords=self._model.get_coords(), name=f'{self.label_full}|excess_input' - ), - 'excess_input', - ) - self.excess_output = self.add( - self._model.add_variables( - lower=0, coords=self._model.get_coords(), name=f'{self.label_full}|excess_output' - ), - 'excess_output', - ) + excess_penalty = np.multiply(self._model.hours_per_step, self.element.excess_penalty_per_flow_hour) + + self.excess_input = self.add_variables(lower=0, coords=self._model.get_coords(), short_name='excess_input') + + self.excess_output = self.add_variables(lower=0, coords=self._model.get_coords(), short_name='excess_output') + eq_bus_balance.lhs -= -self.excess_input + self.excess_output self._model.effects.add_share_to_penalty(self.label_of_element, (self.excess_input * excess_penalty).sum()) @@ -581,13 +570,13 @@ def do_modeling(self): flow.on_off_parameters = OnOffParameters() for flow in all_flows: - self.add(flow.create_model(self._model), flow.label) + self.register_sub_model(flow.create_model(self._model), short_name=flow.label) for sub_model in self.sub_models: sub_model.do_modeling() if self.element.on_off_parameters: - self.on_off = self.add( + self.on_off = self.register_sub_model( OnOffModel( model=self._model, label_of_element=self.label_of_element, @@ -598,6 +587,7 @@ def do_modeling(self): label_of_model=self.label_of_element, apply_bounds_to_flow_rates=True, ), + short_name='on_off', ) self.on_off.do_modeling() @@ -605,7 +595,7 @@ def do_modeling(self): if self.element.prevent_simultaneous_flows: # Simultanious Useage --> Only One FLow is On at a time, but needs a Binary for every flow on_variables = [flow.model.on_off.on for flow in self.element.prevent_simultaneous_flows] - simultaneous_use = self.add(PreventSimultaneousUsageModel(self._model, on_variables, self.label_full)) + simultaneous_use = self.register_sub_model(PreventSimultaneousUsageModel(self._model, on_variables, self.label_full), short_name='prevent_simultaneous_use') simultaneous_use.do_modeling() def results_structure(self): diff --git a/flixopt/features.py b/flixopt/features.py index dab25b49b..49c80d1c8 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -51,50 +51,40 @@ def __init__( self._apply_bounds_to_defining_variable = apply_bounds_to_defining_variable # Only keep non-variable attributes - self.scenario_of_investment: Optional[linopy.Variable] = None self.piecewise_effects: Optional[PiecewiseEffectsModel] = None + def create_variables_and_constraints(self): - constraints = [] size_min, size_max = (self.parameters.minimum_or_fixed_size, self.parameters.maximum_or_fixed_size) - size = self.add( - self._model.add_variables( - lower=0 if self.parameters.optional else size_min, - upper=size_max, - name=f'{self.label_of_model}|size', - coords=self._model.get_coords(['year', 'scenario']), - ), - 'size', + self.add_variables( + lower=0 if self.parameters.optional else size_min, + upper=size_max, + name=f'{self.label_of_model}|size', + coords=self._model.get_coords(['year', 'scenario']), ) if self.parameters.optional: - is_invested = self.add( - self._model.add_variables( - binary=True, name=f'{self.label_of_model}|is_invested', coords=self._model.get_coords(['year', 'scenario']) - ), - 'is_invested', + self.add_variables( + binary=True, name=f'{self.label_of_model}|is_invested', coords=self._model.get_coords(['year', 'scenario']) ) - constraints += BoundingPatterns.bounds_with_state( - self._model, - variable=size, - variable_state=is_invested, + + BoundingPatterns.bounds_with_state( + self, + variable=self.size, + variable_state=self.is_invested, bounds=(self.parameters.minimum_or_fixed_size, self.parameters.maximum_or_fixed_size), ) if self._apply_bounds_to_defining_variable: - constraints += BoundingPatterns.scaled_bounds( - self._model, + BoundingPatterns.scaled_bounds( + self, variable=self._defining_variable, scaling_variable=self.size, relative_bounds=self._relative_bounds_of_defining_variable, ) - # Register constraints - for constraint in constraints: - self.add(constraint) - if self.parameters.piecewise_effects: - self.piecewise_effects = self.add( + self.piecewise_effects = self.register_sub_model( PiecewiseEffectsModel( model=self._model, label_of_element=self.label_of_element, @@ -102,21 +92,10 @@ def create_variables_and_constraints(self): piecewise_shares=self.parameters.piecewise_effects.piecewise_shares, zero_point=self.is_invested, ), - 'segments', + short_name='segments', ) self.piecewise_effects.do_modeling() - # Properties access variables from Model's tracking system - @property - def size(self) -> Optional[linopy.Variable]: - """Investment size variable""" - return self.get_variable_by_short_name('size') - - @property - def is_invested(self) -> Optional[linopy.Variable]: - """Binary investment decision variable""" - return self.get_variable_by_short_name('is_invested') - def add_effects(self): """Add investment effects""" if self.parameters.fix_effects: @@ -146,9 +125,17 @@ def add_effects(self): target='invest', ) - def _create_bounds_for_scenarios(self): - """Keep existing scenario logic""" - pass + @property + def size(self) -> linopy.Variable: + """Investment size variable""" + return self._variables['size'] + + @property + def is_invested(self) -> Optional[linopy.Variable]: + """Binary investment decision variable""" + if 'is_invested' not in self._variables: + return None + return self._variables['is_invested'] class OnOffModel(BaseFeatureModel): @@ -186,73 +173,60 @@ def __init__( def create_variables_and_constraints(self): # 1. Main binary state using existing pattern - on = self.add(self._model.add_variables(binary=True, name=f'{self.label_of_model}|on', coords=self._model.get_coords()), 'on') + on = self.add_variables(binary=True, short_name='on', coords=self._model.get_coords()) if self.parameters.use_off: - off = self.add(self._model.add_variables(binary=True, name=f'{self.label_of_model}|off', coords=self._model.get_coords()), 'off') - self.add(self._model.add_constraints(on + off == 1, name=f'{self.label_of_model}|complementary'), 'complementary') + off = self.add_variables(binary=True, short_name='off', coords=self._model.get_coords()) + self.add_constraints(on + off == 1, short_name='complementary') # 2. Control variables if self._apply_bounds_to_flow_rates: - self.add_batch(*BoundingPatterns.bounds_with_state( - self._model, + BoundingPatterns.bounds_with_state( + self, variable=self._flow_rate, bounds=self._flow_rate_bounds, variable_state=self.on, - )) + ) # 3. Total duration tracking using existing pattern duration_expr = (self.on * self._model.hours_per_step).sum('time') - var, con = ModelingPrimitives.expression_tracking_variable( - self._model, f'{self.label_of_model}|on_hours_total', duration_expr, + ModelingPrimitives.expression_tracking_variable( + self, f'{self.label_of_model}|on_hours_total', duration_expr, (self.parameters.on_hours_total_min if self.parameters.on_hours_total_min is not None else 0, self.parameters.on_hours_total_max if self.parameters.on_hours_total_max is not None else np.inf),#TODO: self._model.hours_per_step.sum('time').item() + self._get_previous_on_duration()) ) - self.add(var, 'on_hours_total') - self.add(con) # 4. Switch tracking using existing pattern if self.parameters.use_switch_on: - switch_vars, switch_constraints = ModelingPrimitives.state_transition_variables( - self._model, f'{self.label_of_model}|switch', self.on, + ModelingPrimitives.state_transition_variables( + self, f'{self.label_of_model}|switch', self.on, previous_state=ModelingUtilities.get_most_recent_state(self._previous_flow_rate), ) - self.add(switch_vars['on'], 'switch|on') - self.add(switch_vars['off'], 'switch|off') - self.add(switch_constraints['transition']) - self.add(switch_constraints['initial']) - self.add(switch_constraints['mutex']) if self.parameters.switch_on_total_max is not None: - count = self.add(self._model.add_variables(lower=0, upper=self.parameters.switch_on_total_max, coords=self._model.get_coords(('year', 'scenario')), name=f'{self.label_of_model}|switch|count'), 'switch|count') - self.add(self._model.add_constraints(count == self.switch_on.sum('time'), name=f'{self.label_of_model}|switch|count'), 'switch|count') + count = self.add_variables(lower=0, upper=self.parameters.switch_on_total_max, coords=self._model.get_coords(('year', 'scenario')), short_name='switch|count') + self.add_constraints(count == self.switch_on.sum('time'), short_name='switch|count') # 5. Consecutive on duration using existing pattern if self.parameters.use_consecutive_on_hours: - consecutive_on_vars, consecutive_on_constraints = ModelingPrimitives.consecutive_duration_tracking( - self._model, + ModelingPrimitives.consecutive_duration_tracking( + self, f'{self.label_of_model}|consecutive_on_hours', #TODO: Change name self.on, minimum_duration=self.parameters.consecutive_on_hours_min, maximum_duration=self.parameters.consecutive_on_hours_max, previous_duration=ModelingUtilities.compute_previous_on_duration([self._previous_flow_rate], self._model.hours_per_step), ) - self.add(consecutive_on_vars['duration'], 'consecutive_on_hours') - for constraint in consecutive_on_constraints.values(): - self.add(constraint) # 6. Consecutive off duration using existing pattern if self.parameters.use_consecutive_off_hours: - consecutive_off_vars, consecutive_off_constraints = ModelingPrimitives.consecutive_duration_tracking( - self._model, + ModelingPrimitives.consecutive_duration_tracking( + self, f'{self.label_of_model}|consecutive_off_hours', self.off, minimum_duration=self.parameters.consecutive_off_hours_min, maximum_duration=self.parameters.consecutive_off_hours_max, previous_duration=ModelingUtilities.compute_previous_off_duration([self._previous_flow_rate], self._model.hours_per_step), ) - self.add(consecutive_off_vars['duration'], 'consecutive_off_hours') - for constraint in consecutive_off_constraints.values(): - self.add(constraint) def add_effects(self): """Add operational effects""" @@ -279,43 +253,42 @@ def add_effects(self): @property def on(self) -> Optional[linopy.Variable]: """Binary on state variable""" - return self.get_variable_by_short_name('on') - - @property - def off(self) -> Optional[linopy.Variable]: - """Binary off state variable""" - return self.get_variable_by_short_name('off') + return self['on'] @property def total_on_hours(self) -> Optional[linopy.Variable]: """Total on hours variable""" - return self.get_variable_by_short_name('total_on_hours') + return self['total_on_hours'] + + @property + def off(self) -> Optional[linopy.Variable]: + """Binary off state variable""" + return self.get('off') @property def switch_on(self) -> Optional[linopy.Variable]: """Switch on variable""" - return self.get_variable_by_short_name('switch|on') + return self.get('switch|on') @property def switch_off(self) -> Optional[linopy.Variable]: """Switch off variable""" - return self.get_variable_by_short_name('switch|off') + return self.get('switch|off') @property def switch_on_nr(self) -> Optional[linopy.Variable]: """Number of switch-ons variable""" - # This could be added to factory if needed - return self.get_variable_by_short_name('switch|count') + return self.get('switch|count') @property def consecutive_on_hours(self) -> Optional[linopy.Variable]: """Consecutive on hours variable""" - return self.get_variable_by_short_name('consecutive_on_hours') + return self.get('consecutive_on_hours') @property def consecutive_off_hours(self) -> Optional[linopy.Variable]: """Consecutive off hours variable""" - return self.get_variable_by_short_name('consecutive_off_hours') + return self.get('consecutive_off_hours') def _get_previous_on_duration(self): hours_per_step = self._model.hours_per_step.isel(time=0).values.flatten()[0] @@ -347,42 +320,27 @@ def __init__( def do_modeling(self): dims =('time', 'year','scenario') if self._as_time_series else ('year','scenario') - self.inside_piece = self.add( - self._model.add_variables( - binary=True, - name=f'{self.label_full}|inside_piece', - coords=self._model.get_coords(dims=dims), - ), - 'inside_piece', + self.inside_piece = self.add_variables( + binary=True, + short_name='inside_piece', + coords=self._model.get_coords(dims=dims), ) - - self.lambda0 = self.add( - self._model.add_variables( - lower=0, - upper=1, - name=f'{self.label_full}|lambda0', - coords=self._model.get_coords(dims=dims), - ), - 'lambda0', + self.lambda0 = self.add_variables( + lower=0, + upper=1, + name='lambda0', + coords=self._model.get_coords(dims=dims), ) - self.lambda1 = self.add( - self._model.add_variables( - lower=0, - upper=1, - name=f'{self.label_full}|lambda1', - coords=self._model.get_coords(dims=dims), - ), - 'lambda1', + self.lambda1 = self.add_variables( + lower=0, + upper=1, + short_name='lambda1', + coords=self._model.get_coords(dims=dims), ) # eq: lambda0(t) + lambda1(t) = inside_piece(t) - self.add( - self._model.add_constraints( - self.inside_piece == self.lambda0 + self.lambda1, name=f'{self.label_full}|inside_piece' - ), - 'inside_piece', - ) + self.add_constraints(self.inside_piece == self.lambda0 + self.lambda1, short_name='inside_piece') class PiecewiseModel(Model): @@ -418,34 +376,33 @@ def __init__( def do_modeling(self): for i in range(len(list(self._piecewise_variables.values())[0])): - new_piece = self.add( + new_piece = self.register_sub_model( PieceModel( model=self._model, label_of_element=self.label_of_element, label_of_model=f'{self.label_of_element}|Piece_{i}', as_time_series=self._as_time_series, - ) + ), + short_name=f'Piece_{i}', ) self.pieces.append(new_piece) new_piece.do_modeling() for var_name in self._piecewise_variables: variable = self._model.variables[var_name] - self.add( - self._model.add_constraints( - variable - == sum( - [ - piece_model.lambda0 * piece_bounds.start + piece_model.lambda1 * piece_bounds.end - for piece_model, piece_bounds in zip( - self.pieces, self._piecewise_variables[var_name], strict=False - ) - ] - ), - name=f'{self.label_full}|{var_name}|lambda', + self.add_constraints( + variable + == sum( + [ + piece_model.lambda0 * piece_bounds.start + piece_model.lambda1 * piece_bounds.end + for piece_model, piece_bounds in zip( + self.pieces, self._piecewise_variables[var_name], strict=False + ) + ] ), - f'{var_name}|lambda', - ) + name=f'{self.label_full}|{var_name}|lambda', + short_name=f'{var_name}|lambda', + ) # a) eq: Segment1.onSeg(t) + Segment2.onSeg(t) + ... = 1 Aufenthalt nur in Segmenten erlaubt # b) eq: -On(t) + Segment1.onSeg(t) + Segment2.onSeg(t) + ... = 0 zusätzlich kann alles auch Null sein @@ -453,22 +410,17 @@ def do_modeling(self): self.zero_point = self._zero_point rhs = self.zero_point elif self._zero_point is True: - self.zero_point = self.add( - self._model.add_variables( - coords=self._model.get_coords(), binary=True, name=f'{self.label_full}|zero_point' - ), - 'zero_point', + self.zero_point = self.add_variables( + coords=self._model.get_coords(), binary=True, short_name='zero_point' ) rhs = self.zero_point else: rhs = 1 - self.add( - self._model.add_constraints( - sum([piece.inside_piece for piece in self.pieces]) <= rhs, - name=f'{self.label_full}|{variable.name}|single_segment', - ), - f'{var_name}|single_segment', + self.add_constraints( + sum([piece.inside_piece for piece in self.pieces]) <= rhs, + name=f'{self.label_full}|{variable.name}|single_segment', + short_name=f'{var_name}|single_segment', ) @@ -495,12 +447,7 @@ def __init__( def do_modeling(self): self.shares = { - effect: self.add( - self._model.add_variables( - coords=self._model.get_coords(['year', 'scenario']), name=f'{self.label_full}|{effect}' - ), - f'{effect}', - ) + effect: self.add_variables(coords=self._model.get_coords(['year', 'scenario']), short_name=effect) for effect in self._piecewise_shares } @@ -512,7 +459,7 @@ def do_modeling(self): }, } - self.piecewise_model = self.add( + self.piecewise_model = self.register_sub_model( PiecewiseModel( model=self._model, label_of_element=self.label_of_element, @@ -520,7 +467,8 @@ def do_modeling(self): zero_point=self._zero_point, as_time_series=False, label_of_model=f'{self.label_of_element}|PiecewiseEffects', - ) + ), + short_name='PiecewiseEffects', ) self.piecewise_model.do_modeling() @@ -566,35 +514,24 @@ def __init__( self._min_per_hour = min_per_hour if min_per_hour is not None else -np.inf def do_modeling(self): - self.total = self.add( - self._model.add_variables( - lower=self._total_min, - upper=self._total_max, - coords=self._model.get_coords([dim for dim in self._dims if dim != 'time']), - name=f'{self.label_full}|total', - ), - 'total', + self.total = self.add_variables( + lower=self._total_min, + upper=self._total_max, + coords=self._model.get_coords([dim for dim in self._dims if dim != 'time']), + short_name='total' ) # eq: sum = sum(share_i) # skalar - self._eq_total = self.add( - self._model.add_constraints(self.total == 0, name=f'{self.label_full}|total'), 'total' - ) + self._eq_total = self.add_constraints(self.total == 0, short_name='total') if 'time' in self._dims: - self.total_per_timestep = self.add( - self._model.add_variables( - lower=-np.inf if (self._min_per_hour is None) else self._min_per_hour * self._model.hours_per_step, - upper=np.inf if (self._max_per_hour is None) else self._max_per_hour * self._model.hours_per_step, - coords=self._model.get_coords(self._dims), - name=f'{self.label_full}|total_per_timestep', - ), - 'total_per_timestep', + self.total_per_timestep = self.add_variables( + lower=-np.inf if (self._min_per_hour is None) else self._min_per_hour * self._model.hours_per_step, + upper=np.inf if (self._max_per_hour is None) else self._max_per_hour * self._model.hours_per_step, + coords=self._model.get_coords(self._dims), + short_name='total_per_timestep', ) - self._eq_total_per_timestep = self.add( - self._model.add_constraints(self.total_per_timestep == 0, name=f'{self.label_full}|total_per_timestep'), - 'total_per_timestep', - ) + self._eq_total_per_timestep = self.add_constraints(self.total_per_timestep == 0, short_name='total_per_timestep') # Add it to the total self._eq_total.lhs -= self.total_per_timestep.sum(dim='time') @@ -629,16 +566,15 @@ def add_share( if name in self.shares: self.share_constraints[name].lhs -= expression else: - self.shares[name] = self.add( - self._model.add_variables( - coords=self._model.get_coords(dims), - name=f'{name}->{self.label_full}', - ), - name, + self.shares[name] = self.add_variables( + coords=self._model.get_coords(dims), + short_name=f'{name}->{self.label_full}', ) - self.share_constraints[name] = self.add( - self._model.add_constraints(self.shares[name] == expression, name=f'{name}->{self.label_full}'), name + + self.share_constraints[name] = self.add_constraints( + self.shares[name] == expression, short_name=f'{name}->{self.label_full}' ) + if 'time' not in dims: self._eq_total.lhs -= self.shares[name] else: @@ -680,9 +616,4 @@ def __init__( def do_modeling(self): # eq: sum(flow_i.on(t)) <= 1.1 (1 wird etwas größer gewählt wg. Binärvariablengenauigkeit) - self.add( - self._model.add_constraints( - sum(self._simultanious_use_variables) <= 1.1, name=f'{self.label_full}|prevent_simultaneous_use' - ), - 'prevent_simultaneous_use', - ) + self.add_constraints(sum(self._simultanious_use_variables) <= 1.1, short_name='prevent_simultaneous_use') diff --git a/flixopt/modeling.py b/flixopt/modeling.py index 8c539caa2..41f5b4224 100644 --- a/flixopt/modeling.py +++ b/flixopt/modeling.py @@ -324,7 +324,7 @@ def consecutive_duration_tracking( minimum_duration: Optional[TemporalData] = None, maximum_duration: Optional[TemporalData] = None, previous_duration: TemporalData = 0, - ) -> Tuple[Dict[str, linopy.Variable], Dict[str, linopy.Constraint]]: + ) -> Tuple[linopy.Variable, Tuple[linopy.Constraint, linopy.Constraint, linopy.Constraint]]: """ Creates consecutive duration tracking for a binary state variable. diff --git a/flixopt/structure.py b/flixopt/structure.py index 59cb62251..b6c4572d1 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -714,51 +714,58 @@ def __init__( self._variables: Dict[str, linopy.Variable] = {} # Mapping from short name to variable self._constraints: Dict[str, linopy.Constraint] = {} # Mapping from short name to constraint - self.sub_models: Dict[str, 'Model'] = {} + self._sub_models: Dict[str, 'Model'] = {} logger.debug(f'Created {self.__class__.__name__} "{self.label_full}"') - def add_variable(self, short_name: str, **kwargs) -> linopy.Variable: - """Create and add a variable in one step""" + def add_variables(self, short_name: str = None, **kwargs) -> linopy.Variable: + """Create and register a variable in one step""" if 'name' not in kwargs: + if short_name is None: + raise ValueError('Short name must be provided when no name is given') kwargs['name'] = f'{self.label_of_model}|{short_name}' variable = self._model.add_variables(**kwargs) self.register_variable(variable, short_name) return variable - def add_constraint(self, short_name: str, expression, **kwargs) -> linopy.Constraint: - """Create and add a constraint in one step""" + def add_constraints(self, expression, short_name: str = None, **kwargs) -> linopy.Constraint: + """Create and register a constraint in one step""" if 'name' not in kwargs: + if short_name is None: + raise ValueError('Short name must be provided when no name is given') kwargs['name'] = f'{self.label_of_model}|{short_name}' constraint = self._model.add_constraints(expression, **kwargs) self.register_constraint(constraint, short_name) return constraint - def register_variable(self, variable: linopy.Variable, short_name: str = None) -> None: + def register_variable(self, variable: linopy.Variable, short_name: str = None) -> linopy.Variable: """Register a variable with the model""" if short_name is None: short_name = self._extract_short_name(variable) if short_name in self._variables: raise ValueError(f'Short name "{short_name}" already assigned to model') self._variables[short_name] = variable + return variable - def register_constraint(self, constraint: linopy.Constraint, short_name: str = None) -> None: + def register_constraint(self, constraint: linopy.Constraint, short_name: str = None) -> linopy.Constraint: """Register a constraint with the model""" if short_name is None: short_name = self._extract_short_name(constraint) if short_name in self._constraints: raise ValueError(f'Short name "{short_name}" already assigned to model') self._constraints[short_name] = constraint + return constraint - def register_sub_model(self, sub_model: 'Model', short_name: str) -> None: + def register_sub_model(self, sub_model: 'Model', short_name: str) -> 'Model': """Register a sub-model with the model""" if short_name is None: short_name = sub_model.__class__.__name__ - if short_name in self.sub_models: + if short_name in self._sub_models: raise ValueError(f'Short name "{short_name}" already assigned to model') - self.sub_models[short_name] = sub_model + self._sub_models[short_name] = sub_model + return sub_model def __getitem__(self, key: str) -> linopy.Variable: """Get a variable by its short name""" @@ -766,6 +773,24 @@ def __getitem__(self, key: str) -> linopy.Variable: return self._variables[key] raise KeyError(f'Variable "{key}" not found in model "{self.label_full}"') + def __contains__(self, name: str) -> bool: + """Check if a variable exists in the model""" + return name in self._variables or name in self.variables + + def get(self, name: str, default=None): + """Get variable by short name, returning default if not found""" + try: + return self[name] + except KeyError: + return default + + def get_coords( + self, + dims: Optional[Collection[str]] = None, + extra_timestep: bool = False, + ) -> Optional[xr.Coordinates]: + return self._model.get_coords(dims=dims, extra_timestep=extra_timestep) + def filter_variables( self, filter_by: Optional[Literal['binary', 'continuous', 'integer']] = None, @@ -794,33 +819,44 @@ def label_full(self) -> str: return self.label_of_model @property - def variables(self) -> linopy.Variables: + def variables_direct(self) -> linopy.Variables: + """Variables of the model, excluding those of sub-models""" return self._model.variables[[var.name for var in self._variables.values()]] @property - def constraints(self) -> linopy.Constraints: + def constraints_direct(self) -> linopy.Constraints: + """Costraints of the model, excluding those of sub-models""" return self._model.constraints[[con.name for con in self._constraints.values()]] @property - def all_sub_models(self) -> List['Model']: - return [model for sub_model in self.sub_models.values() for model in [sub_model] + sub_model.all_sub_models] + def sub_models_direct(self) -> Dict[str, 'Model']: + """All sub-models of the model, excluding those of sub-models""" + return self._sub_models @property - def all_constraints(self) -> linopy.Constraints: - names = [constraint_name for constraint_name in self.constraints] + [ - constraint.name - for sub_model in self.all_sub_models - for constraint in sub_model.constraints.values() + def sub_models(self) -> List['Model']: + """All sub-models of the model""" + direct = list(self.sub_models_direct.values()) + return direct + [model for sub_model in direct for model in sub_model.sub_models] + + @property + def constraints(self) -> linopy.Constraints: + """All constraints of the model, including those of sub-models""" + names = list(self.constraints_direct) + [ + constraint_name + for sub_model in self.sub_models + for constraint_name in sub_model.constraints_direct ] return self._model.constraints[names] @property - def all_variables(self) -> linopy.Variables: - names = [variable_name for variable_name in self.variables] + [ - variable.name - for sub_model in self.all_sub_models - for variable in sub_model.constraints.values() + def variables(self) -> linopy.Variables: + """All variables of the model, including those of sub-models""" + names = list(self.variables_direct) + [ + variable_name + for sub_model in self.sub_models + for variable_name in sub_model.variables_direct ] return self._model.variables[names] From 099784384730e140f9b4f8da56125660417ae71f Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 21 Jul 2025 11:05:02 +0200 Subject: [PATCH 219/448] Simplify and focus more on own Model class --- flixopt/elements.py | 3 +- flixopt/features.py | 42 +++++--- flixopt/modeling.py | 252 ++++--------------------------------------- flixopt/structure.py | 44 ++++++-- 4 files changed, 84 insertions(+), 257 deletions(-) diff --git a/flixopt/elements.py b/flixopt/elements.py index dec7425a7..9329a88dd 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -12,7 +12,7 @@ from .config import CONFIG from .core import PlausibilityError, Scalar, TemporalData, TemporalDataUser from .effects import TemporalEffectsUser -from .features import InvestmentModel, OnOffModel, PreventSimultaneousUsageModel, ModelingPatterns, ModelingPrimitives +from .features import InvestmentModel, OnOffModel, PreventSimultaneousUsageModel, ModelingPrimitives from .interface import InvestParameters, OnOffParameters from .structure import Element, ElementModel, FlowSystemModel, register_class_for_io from .modeling import BoundingPatterns @@ -378,6 +378,7 @@ def do_modeling(self): self.element.flow_hours_total_max if self.element.flow_hours_total_max is not None else None, ), coords=['year', 'scenario'], + short_name='total_flow_hours', ) # Load factor constraints diff --git a/flixopt/features.py b/flixopt/features.py index 49c80d1c8..6708a3221 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -13,7 +13,7 @@ from .core import NonTemporalData, Scalar, TemporalData, FlowSystemDimensions from .interface import InvestParameters, OnOffParameters, Piecewise, PiecewiseEffects from .structure import Model, FlowSystemModel, BaseFeatureModel -from .modeling import ModelingPatterns, ModelingUtilities, ModelingPrimitives, BoundingPatterns +from .modeling import ModelingUtilities, ModelingPrimitives, BoundingPatterns logger = logging.getLogger('flixopt') @@ -57,15 +57,15 @@ def __init__( def create_variables_and_constraints(self): size_min, size_max = (self.parameters.minimum_or_fixed_size, self.parameters.maximum_or_fixed_size) self.add_variables( + short_name='size', lower=0 if self.parameters.optional else size_min, upper=size_max, - name=f'{self.label_of_model}|size', coords=self._model.get_coords(['year', 'scenario']), ) if self.parameters.optional: self.add_variables( - binary=True, name=f'{self.label_of_model}|is_invested', coords=self._model.get_coords(['year', 'scenario']) + binary=True, coords=self._model.get_coords(['year', 'scenario']), short_name='is_invested', ) BoundingPatterns.bounds_with_state( @@ -190,15 +190,24 @@ def create_variables_and_constraints(self): # 3. Total duration tracking using existing pattern duration_expr = (self.on * self._model.hours_per_step).sum('time') ModelingPrimitives.expression_tracking_variable( - self, f'{self.label_of_model}|on_hours_total', duration_expr, - (self.parameters.on_hours_total_min if self.parameters.on_hours_total_min is not None else 0, - self.parameters.on_hours_total_max if self.parameters.on_hours_total_max is not None else np.inf),#TODO: self._model.hours_per_step.sum('time').item() + self._get_previous_on_duration()) + self, duration_expr, short_name='on_hours_total', + bounds=( + self.parameters.on_hours_total_min if self.parameters.on_hours_total_min is not None else 0, + self.parameters.on_hours_total_max if self.parameters.on_hours_total_max is not None else np.inf, + ), #TODO: self._model.hours_per_step.sum('time').item() + self._get_previous_on_duration()) ) # 4. Switch tracking using existing pattern if self.parameters.use_switch_on: + self.add_variables(binary=True, short_name='switch|on', coords=self.get_coords()) + self.add_variables(binary=True, short_name='switch|off', coords=self.get_coords()) + ModelingPrimitives.state_transition_variables( - self, f'{self.label_of_model}|switch', self.on, + self, + state_variable=self.on, + switch_on=self.switch_on, + switch_off=self.switch_off, + name=f'{self.label_of_model}|switch', previous_state=ModelingUtilities.get_most_recent_state(self._previous_flow_rate), ) @@ -210,8 +219,8 @@ def create_variables_and_constraints(self): if self.parameters.use_consecutive_on_hours: ModelingPrimitives.consecutive_duration_tracking( self, - f'{self.label_of_model}|consecutive_on_hours', #TODO: Change name - self.on, + state_variable=self.on, + short_name='consecutive_on_hours', minimum_duration=self.parameters.consecutive_on_hours_min, maximum_duration=self.parameters.consecutive_on_hours_max, previous_duration=ModelingUtilities.compute_previous_on_duration([self._previous_flow_rate], self._model.hours_per_step), @@ -221,11 +230,13 @@ def create_variables_and_constraints(self): if self.parameters.use_consecutive_off_hours: ModelingPrimitives.consecutive_duration_tracking( self, - f'{self.label_of_model}|consecutive_off_hours', - self.off, + state_variable=self.off, + short_name='consecutive_off_hours', minimum_duration=self.parameters.consecutive_off_hours_min, maximum_duration=self.parameters.consecutive_off_hours_max, - previous_duration=ModelingUtilities.compute_previous_off_duration([self._previous_flow_rate], self._model.hours_per_step), + previous_duration=ModelingUtilities.compute_previous_off_duration( + [self._previous_flow_rate], self._model.hours_per_step + ), ) def add_effects(self): @@ -328,7 +339,7 @@ def do_modeling(self): self.lambda0 = self.add_variables( lower=0, upper=1, - name='lambda0', + short_name='lambda0', coords=self._model.get_coords(dims=dims), ) @@ -568,11 +579,12 @@ def add_share( else: self.shares[name] = self.add_variables( coords=self._model.get_coords(dims), - short_name=f'{name}->{self.label_full}', + name=f'{name}->{self.label_full}', + short_name=name, ) self.share_constraints[name] = self.add_constraints( - self.shares[name] == expression, short_name=f'{name}->{self.label_full}' + self.shares[name] == expression, name=f'{name}->{self.label_full}' ) if 'time' not in dims: diff --git a/flixopt/modeling.py b/flixopt/modeling.py index 41f5b4224..17f47c939 100644 --- a/flixopt/modeling.py +++ b/flixopt/modeling.py @@ -148,68 +148,12 @@ def get_most_recent_state(previous_values: List[TemporalData]) -> int: class ModelingPrimitives: """Mathematical modeling primitives returning (variables, constraints) tuples""" - @staticmethod - def binary_state_pair( - model: FlowSystemModel, name: str, coords: List[str] = None, use_complement: bool = True - ) -> Tuple[Tuple[linopy.Variable, linopy.Variable], linopy.Constraint]: - """ - Creates complementary binary variables with completeness constraint. - - Mathematical formulation: - on[t] + off[t] = 1 ∀t - on[t], off[t] ∈ {0, 1} - """ - coords = coords or ['time'] - - on = model.add_variables(binary=True, name=f'{name}|on', coords=model.get_coords(coords)) - off = model.add_variables(binary=True, name=f'{name}|off', coords=model.get_coords(coords)) - - # Constraint: on + off = 1 - complementary = model.add_constraints(on + off == 1, name=f'{name}|complementary') - - return (on, off), complementary - - @staticmethod - def proportionally_bounded_variable( - model: FlowSystemModel, - name: str, - controlling_variable, - bounds: Tuple[TemporalData, TemporalData], - coords: List[str] = None, - ) -> Tuple[Dict[str, linopy.Variable], Dict[str, linopy.Constraint]]: - """ - Creates variable with bounds proportional to another variable. - - Mathematical formulation: - lower_factor[t] * controller[t] ≤ variable[t] ≤ upper_factor[t] * controller[t] ∀t - - Returns: - variables: {'variable': bounded_var} - constraints: {'lb': constraint, 'ub': constraint} - """ - coords = coords or ['time'] - variable = model.add_variables(name=f'{name}|bounded', coords=model.get_coords(coords)) - - lower_factor, upper_factor = bounds - - # Constraints: lower_factor * controller ≤ var ≤ upper_factor * controller - lower_bound = model.add_constraints( - variable >= controlling_variable * lower_factor, name=f'{name}|proportional_lb' - ) - upper_bound = model.add_constraints( - variable <= controlling_variable * upper_factor, name=f'{name}|proportional_ub' - ) - - variables = {'variable': variable} - constraints = {'lb': lower_bound, 'ub': upper_bound} - - return variables, constraints - @staticmethod def expression_tracking_variable( - model: FlowSystemModel, - name: str, + model: Model, tracked_expression, + name: str = None, + short_name: str = None, bounds: Tuple[TemporalData, TemporalData] = None, coords: List[str] = None, ) -> Tuple[linopy.Variable, linopy.Constraint]: @@ -227,27 +171,30 @@ def expression_tracking_variable( coords = coords or ['year', 'scenario'] if not bounds: - tracker = model.add_variables(name=f'{name}', coords=model.get_coords(coords)) + tracker = model.add_variables(name=name, coords=model.get_coords(coords), short_name=short_name) else: tracker = model.add_variables( lower=bounds[0] if bounds[0] is not None else -np.inf, upper=bounds[1] if bounds[1] is not None else np.inf, - name=f'{name}', + name=name, coords=model.get_coords(coords), + short_name=short_name, ) # Constraint: tracker = expression - tracking = model.add_constraints(tracker == tracked_expression, name=f'{name}') + tracking = model.add_constraints(tracker == tracked_expression, name=name, short_name=short_name) return tracker, tracking @staticmethod def state_transition_variables( - model: FlowSystemModel, - name: str, + model: Union[FlowSystemModel, Model], state_variable: linopy.Variable, + switch_on: linopy.Variable, + switch_off: linopy.Variable, + name: str, previous_state=0, - ) -> Tuple[Dict[str, linopy.Variable], Dict[str, linopy.Constraint]]: + ) -> Tuple[linopy.Constraint, linopy.Constraint, linopy.Constraint]: """ Creates switch-on/off variables with state transition logic. @@ -261,14 +208,11 @@ def state_transition_variables( variables: {'switch_on': binary_var, 'switch_off': binary_var} constraints: {'transition': constraint, 'initial': constraint, 'mutex': constraint} """ - switch_on = model.add_variables(binary=True, name=f'{name}|on', coords=model.get_coords(['time'])) - switch_off = model.add_variables(binary=True, name=f'{name}|off', coords=model.get_coords(['time'])) - # State transition constraints for t > 0 transition = model.add_constraints( switch_on.isel(time=slice(1, None)) - switch_off.isel(time=slice(1, None)) == state_variable.isel(time=slice(1, None)) - state_variable.isel(time=slice(None, -1)), - name=name, + name=f'{name}|transition', ) # Initial state transition for t = 0 @@ -280,13 +224,14 @@ def state_transition_variables( # At most one switch per timestep mutex = model.add_constraints(switch_on + switch_off <= 1, name=f'{name}|mutex') - return {'on': switch_on, 'off': switch_off}, {'transition': transition, 'initial': initial, 'mutex': mutex} + return transition, initial, mutex @staticmethod def sum_up_variable( model: FlowSystemModel, variable_to_count: linopy.Variable, - name: str, + name: str = None, + short_name: str = None, bounds: Tuple[NonTemporalData, NonTemporalData] = None, factor: TemporalData = 1, ) -> Tuple[linopy.Variable, linopy.Constraint]: @@ -319,8 +264,9 @@ def sum_up_variable( @staticmethod def consecutive_duration_tracking( model: FlowSystemModel, - name: str, state_variable: linopy.Variable, + name: str = None, + short_name: str = None, minimum_duration: Optional[TemporalData] = None, maximum_duration: Optional[TemporalData] = None, previous_duration: TemporalData = 0, @@ -357,6 +303,7 @@ def consecutive_duration_tracking( upper=maximum_duration if maximum_duration is not None else mega, coords=model.get_coords(['time']), name=name, + short_name=short_name, ) constraints = {} @@ -708,164 +655,3 @@ def auto_bounds( return BoundingPatterns.basic_bounds(model, variable, bounds) raise ValueError('Invalid combination of arguments') - - -class ModelingPatterns: - """High-level patterns that compose primitives and return (variables, constraints) tuples""" - - @staticmethod - def investment_sizing_pattern( - model: FlowSystemModel, - name: str, - size_bounds: Tuple[TemporalData, TemporalData], - controlled_variable: linopy.Variable, - control_factors: Tuple[TemporalData, TemporalData], - state_variable: List[linopy.Variable] = None, - optional: bool = False, - ) -> Tuple[Dict[str, linopy.Variable], Dict[str, linopy.Constraint]]: - """ - Complete investment sizing pattern with optional binary decision. - - Args: - model: The model to add the variables to. - name: The name of the investment variable. - size_bounds: The minimum and maximum investment size. - controlled_variables: The variables that are controlled by the investment decision. - control_factors: The control factors for the controlled variables. - state_variables: State variable defining the state of the controlled variables. - optional: Whether the investment decision is optional. - - Returns: - variables: {'size': size_var, 'is_invested': binary_var (if optional)} - constraints: {'ub': constraint, 'lb': constraint, ...} - """ - variables = {} - constraints = {} - - # Investment size variable - size_min, size_max = size_bounds - variables['size'] = model.add_variables( - lower=0 if optional else size_min, - upper=size_max, - name=f'{name}|size', - coords=model.get_coords(['year', 'scenario']), - ) - - # Optional binary investment decision - if optional: - variables['is_invested'] = model.add_variables( - binary=True, name=f'{name}|is_invested', coords=model.get_coords(['year', 'scenario']) - ) - - _, new_cons = BoundingPatterns.auto_bounds( - model=model, - variable=controlled_variable, - bounds=control_factors, - upper_bound_name=f'{controlled_variable.name}|ub', - lower_bound_name=f'{controlled_variable.name}|lb', - scaling_variable=variables['size'], - binary_control=variables['is_invested'] if optional else None, - scaling_bounds=(size_min, size_max), - constraint_name_prefix=name, - ) - - constraints.update(new_cons) - - return variables, constraints - - @staticmethod - def operational_binary_control_pattern( - model: FlowSystemModel, - name: str, - controlled_variables: List[linopy.Variable], - variable_bounds: List[Tuple[TemporalData, TemporalData]], - use_complement: bool = False, - track_total_duration: bool = False, - track_switches: bool = False, - previous_state=0, - duration_bounds: Tuple[TemporalData, TemporalData] = None, - track_consecutive_on: bool = False, - consecutive_on_bounds: Tuple[Optional[TemporalData], Optional[TemporalData]] = (None, None), - previous_on_duration: TemporalData = 0, - track_consecutive_off: bool = False, - consecutive_off_bounds: Tuple[Optional[TemporalData], Optional[TemporalData]] = (None, None), - previous_off_duration: TemporalData = 0, - ) -> Tuple[Dict[str, linopy.Variable], Dict[str, linopy.Constraint]]: - """ - Enhanced operational binary control using composable patterns. - """ - variables = {} - constraints = {} - - # 1. Main binary state using existing pattern - if use_complement: - state_vars, state_constraints = ModelingPrimitives.binary_state_pair(model, name) - variables.update(state_vars) - constraints.update(state_constraints) - else: - variables['on'] = model.add_variables(binary=True, name=f'{name}|on', coords=model.get_coords(['time'])) - - # 2. Control variables - use big_m_binary_bounds pattern for consistency - for i, (var, (lower_bound, upper_bound)) in enumerate(zip(controlled_variables, variable_bounds)): - # Use the big_m pattern but without binary control (None) - _, control_constraints = BoundingPatterns.big_m_binary_bounds( - model=model, - variable=var, - binary_control=variables['on'], # The on state controls the variables - size_variable=1, # No size scaling, just on/off - relative_bounds=(lower_bound, upper_bound), - upper_bound_name=f'{name}|control_{i}_upper', - lower_bound_name=f'{name}|control_{i}_lower', - ) - constraints[f'control_{i}_upper'] = control_constraints['ub'] - constraints[f'control_{i}_lower'] = control_constraints['lb'] - - # 3. Total duration tracking using existing pattern - if track_total_duration: - duration_expr = (variables['on'] * model.hours_per_step).sum('time') - duration_vars, duration_constraints = ModelingPrimitives.expression_tracking_variable( - model, f'{name}|on_hours_total', duration_expr, duration_bounds - ) - variables['total_duration'] = duration_vars['tracker'] - constraints['duration_tracking'] = duration_constraints['tracking'] - - # 4. Switch tracking using existing pattern - if track_switches: - switch_vars, switch_constraints = ModelingPrimitives.state_transition_variables( - model, f'{name}|switches', variables['on'], previous_state - ) - variables.update(switch_vars) - for switch_name, switch_constraint in switch_constraints.items(): - constraints[f'switch_{switch_name}'] = switch_constraint - - # 5. Consecutive on duration using existing pattern - if track_consecutive_on: - min_on, max_on = consecutive_on_bounds - consecutive_on_vars, consecutive_on_constraints = ModelingPrimitives.consecutive_duration_tracking( - model, - f'{name}|consecutive_on', - variables['on'], - minimum_duration=min_on, - maximum_duration=max_on, - previous_duration=previous_on_duration, - ) - variables['consecutive_on_duration'] = consecutive_on_vars['duration'] - for cons_name, cons_constraint in consecutive_on_constraints.items(): - constraints[f'consecutive_on_{cons_name}'] = cons_constraint - - # 6. Consecutive off duration using existing pattern - if track_consecutive_off and 'off' in variables: - min_off, max_off = consecutive_off_bounds - consecutive_off_vars, consecutive_off_constraints = ModelingPrimitives.consecutive_duration_tracking( - model, - f'{name}|consecutive_off', - variables['off'], - minimum_duration=min_off, - maximum_duration=max_off, - previous_duration=previous_off_duration, - ) - variables['consecutive_off_duration'] = consecutive_off_vars['duration'] - for cons_name, cons_constraint in consecutive_off_constraints.items(): - constraints[f'consecutive_off_{cons_name}'] = cons_constraint - - return variables, constraints diff --git a/flixopt/structure.py b/flixopt/structure.py index b6c4572d1..953636f9b 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -720,7 +720,7 @@ def __init__( def add_variables(self, short_name: str = None, **kwargs) -> linopy.Variable: """Create and register a variable in one step""" - if 'name' not in kwargs: + if kwargs.get('name') is None: if short_name is None: raise ValueError('Short name must be provided when no name is given') kwargs['name'] = f'{self.label_of_model}|{short_name}' @@ -731,7 +731,7 @@ def add_variables(self, short_name: str = None, **kwargs) -> linopy.Variable: def add_constraints(self, expression, short_name: str = None, **kwargs) -> linopy.Constraint: """Create and register a constraint in one step""" - if 'name' not in kwargs: + if kwargs.get('name') is None: if short_name is None: raise ValueError('Short name must be provided when no name is given') kwargs['name'] = f'{self.label_of_model}|{short_name}' @@ -743,18 +743,20 @@ def add_constraints(self, expression, short_name: str = None, **kwargs) -> linop def register_variable(self, variable: linopy.Variable, short_name: str = None) -> linopy.Variable: """Register a variable with the model""" if short_name is None: - short_name = self._extract_short_name(variable) - if short_name in self._variables: - raise ValueError(f'Short name "{short_name}" already assigned to model') + short_name = variable.name + elif short_name in self._variables: + raise ValueError(f'Short name "{short_name}" already assigned to model variables') + self._variables[short_name] = variable return variable def register_constraint(self, constraint: linopy.Constraint, short_name: str = None) -> linopy.Constraint: """Register a constraint with the model""" if short_name is None: - short_name = self._extract_short_name(constraint) - if short_name in self._constraints: - raise ValueError(f'Short name "{short_name}" already assigned to model') + short_name = constraint.name + elif short_name in self._constraints: + raise ValueError(f'Short name "{short_name}" already assigned to model constraint') + self._constraints[short_name] = constraint return constraint @@ -871,6 +873,28 @@ def _extract_short_name(item: Union[linopy.Variable, linopy.Constraint]) -> str: else: return name # Use full name if no | separator + def __repr__(self) -> str: + """ + Return a string representation of the linopy model. + """ + var_string = self.variables.__repr__().split("\n", 2)[2] + con_string = self.constraints.__repr__().split("\n", 2)[2] + model_string = f"Linopy {self._model.type} submodel: {self.label_of_model}" + + if len(self.sub_models) == 0: + sub_models_string = ' \n' + else: + sub_models_string = '' + for sub_model in self.sub_models: + sub_models_string += f'\n * {sub_model.label_of_model}' + + return ( + f"{model_string}\n{'=' * len(model_string)}\n\n" + f"Variables:\n----------\n{var_string}\n" + f"Constraints:\n------------\n{con_string}\n" + f"Submodels:\n----------\n{sub_models_string}" + ) + class BaseFeatureModel(Model): """Minimal base class for feature models that use factory patterns""" @@ -900,6 +924,10 @@ def add_effects(self): """Override in subclasses to add effects""" pass # Default: no effects + @property + def hours_per_step(self): + return self._model.hours_per_step + class ElementModel(Model): """Stores the mathematical Variables and Constraints for Elements""" From 1d6ef9745b11c29fa5d7c2c35ac27cb8aab7e5a1 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 21 Jul 2025 11:05:39 +0200 Subject: [PATCH 220/448] Update tests --- tests/test_component.py | 24 ++++++++++++------------ tests/test_flow.py | 6 +++--- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/test_component.py b/tests/test_component.py index fbedbd415..f65c93414 100644 --- a/tests/test_component.py +++ b/tests/test_component.py @@ -97,16 +97,16 @@ def test_on_with_multiple_flows(self, basic_flow_system_linopy): assert set(comp.model.constraints) == { 'TestComponent(In1)|total_flow_hours', - 'TestComponent(In1)|on|lb', - 'TestComponent(In1)|on|ub', + 'TestComponent(In1)|flow_rate|lb', + 'TestComponent(In1)|flow_rate|ub', 'TestComponent(In1)|on_hours_total', 'TestComponent(Out1)|total_flow_hours', - 'TestComponent(Out1)|on|lb', - 'TestComponent(Out1)|on|ub', + 'TestComponent(Out1)|flow_rate|lb', + 'TestComponent(Out1)|flow_rate|ub', 'TestComponent(Out1)|on_hours_total', 'TestComponent(Out2)|total_flow_hours', - 'TestComponent(Out2)|on|lb', - 'TestComponent(Out2)|on|ub', + 'TestComponent(Out2)|flow_rate|lb', + 'TestComponent(Out2)|flow_rate|ub', 'TestComponent(Out2)|on_hours_total', 'TestComponent|on|lb', 'TestComponent|on|ub', @@ -118,8 +118,8 @@ def test_on_with_multiple_flows(self, basic_flow_system_linopy): assert_var_equal(model['TestComponent|on'], model.add_variables(binary=True, coords = (timesteps,))) assert_var_equal(model['TestComponent(Out2)|on'], model.add_variables(binary=True, coords=(timesteps,))) - assert_conequal(model.constraints['TestComponent(Out2)|on|lb'], model.variables['TestComponent(Out2)|on'] * 0.3 * 300 <= model.variables['TestComponent(Out2)|flow_rate']) - assert_conequal(model.constraints['TestComponent(Out2)|on|ub'], model.variables['TestComponent(Out2)|on'] * 300 * ub_out2 >= model.variables['TestComponent(Out2)|flow_rate']) + assert_conequal(model.constraints['TestComponent(Out2)|flow_rate|lb'], model.variables['TestComponent(Out2)|flow_rate'] >= model.variables['TestComponent(Out2)|on'] * 0.3 * 300) + assert_conequal(model.constraints['TestComponent(Out2)|flow_rate|ub'], model.variables['TestComponent(Out2)|flow_rate'] <= model.variables['TestComponent(Out2)|on'] * 300 * ub_out2) assert_conequal(model.constraints['TestComponent|on|lb'], model.variables['TestComponent|on'] * 1e-5 <= model.variables['TestComponent(In1)|flow_rate'] + model.variables['TestComponent(Out1)|flow_rate'] + model.variables['TestComponent(Out2)|flow_rate']) @@ -156,8 +156,8 @@ def test_on_with_single_flow(self, basic_flow_system_linopy): assert set(comp.model.constraints) == { 'TestComponent(In1)|total_flow_hours', - 'TestComponent(In1)|on|lb', - 'TestComponent(In1)|on|ub', + 'TestComponent(In1)|flow_rate|lb', + 'TestComponent(In1)|flow_rate|ub', 'TestComponent(In1)|on_hours_total', 'TestComponent|on|lb', 'TestComponent|on|ub', @@ -171,11 +171,11 @@ def test_on_with_single_flow(self, basic_flow_system_linopy): assert_var_equal(model['TestComponent(In1)|on'], model.add_variables(binary=True, coords=(timesteps,))) assert_conequal( - model.constraints['TestComponent(In1)|on|lb'], + model.constraints['TestComponent(In1)|flow_rate|lb'], model.variables['TestComponent(In1)|on'] * 0.1 * 100 <= model.variables['TestComponent(In1)|flow_rate'], ) assert_conequal( - model.constraints['TestComponent(In1)|on|ub'], + model.constraints['TestComponent(In1)|flow_rate|ub'], model.variables['TestComponent(In1)|on'] * 100 >= model.variables['TestComponent(In1)|flow_rate'], ) diff --git a/tests/test_flow.py b/tests/test_flow.py index 5b99a79f2..50154859d 100644 --- a/tests/test_flow.py +++ b/tests/test_flow.py @@ -491,11 +491,11 @@ def test_flow_on(self, basic_flow_system_linopy): ) assert_conequal( model.constraints['Sink(Wärme)|flow_rate|lb'], - flow.model.variables['Sink(Wärme)|flow_rate'] >= flow.model.variables['Sink(Wärme)|on'] * 0.2 * 100, + flow.model.variables['Sink(Wärme)|flow_rate'] >= flow.model.variables['Sink(Wärme)|on'] * 0.2 * 100, ) assert_conequal( model.constraints['Sink(Wärme)|flow_rate|ub'], - flow.model.variables['Sink(Wärme)|flow_rate']<= flow.model.variables['Sink(Wärme)|on'] * 0.8 * 100, + flow.model.variables['Sink(Wärme)|flow_rate'] <= flow.model.variables['Sink(Wärme)|on'] * 0.8 * 100, ) assert_conequal( @@ -842,7 +842,7 @@ def test_switch_on_constraints(self, basic_flow_system_linopy): # Check that constraints exist assert { - 'Sink(Wärme)|switch', + 'Sink(Wärme)|switch|transition', 'Sink(Wärme)|switch|initial', 'Sink(Wärme)|switch|mutex', 'Sink(Wärme)|switch|count', From 972cb901920b4c8dfc39bc3a497439f7c75bfe1a Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 21 Jul 2025 14:22:22 +0200 Subject: [PATCH 221/448] Improve state computation in ModelingUtilities --- flixopt/elements.py | 207 +++++++++++++++++++++++-------------------- flixopt/features.py | 80 ++++++----------- flixopt/modeling.py | 167 +++++++++++++++++++--------------- flixopt/structure.py | 8 +- 4 files changed, 239 insertions(+), 223 deletions(-) diff --git a/flixopt/elements.py b/flixopt/elements.py index 9329a88dd..796d82864 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -8,6 +8,7 @@ import linopy import numpy as np +import xarray as xr from .config import CONFIG from .core import PlausibilityError, Scalar, TemporalData, TemporalDataUser @@ -15,7 +16,7 @@ from .features import InvestmentModel, OnOffModel, PreventSimultaneousUsageModel, ModelingPrimitives from .interface import InvestParameters, OnOffParameters from .structure import Element, ElementModel, FlowSystemModel, register_class_for_io -from .modeling import BoundingPatterns +from .modeling import BoundingPatterns, ModelingUtilities if TYPE_CHECKING: from .flow_system import FlowSystem @@ -316,57 +317,13 @@ def __init__(self, model: FlowSystemModel, element: Flow): def do_modeling(self): # Main flow rate variable self.add_variables( - lower=self.flow_rate_lower_bound, - upper=self.flow_rate_upper_bound, + lower=self.absolute_flow_rate_bounds[0], + upper=self.absolute_flow_rate_bounds[1], coords=self._model.get_coords(), short_name='flow_rate', ) - default_cons = not (self.element.on_off_parameters is not None and isinstance(self.element.size, InvestParameters)) - - # OnOff feature - if self.element.on_off_parameters is not None: - self.register_sub_model( - OnOffModel( - model=self._model, - label_of_element=self.label_of_element, - parameters=self.element.on_off_parameters, - flow_rate=self.flow_rate, - flow_rate_bounds=self.flow_rate_bounds_on, - previous_flow_rate=self.element.previous_flow_rate, - label_of_model=self.label_of_element, - apply_bounds_to_flow_rates=default_cons, - ), - short_name='on_off', - ).do_modeling() - - # Investment feature - if isinstance(self.element.size, InvestParameters): - self.register_sub_model( - InvestmentModel( - model=self._model, - label_of_element=self.label_of_element, - label_of_model=self.label_of_element, - parameters=self.element.size, - defining_variable=self.flow_rate, - relative_bounds_of_defining_variable=( - self.flow_rate_lower_bound_relative, - self.flow_rate_upper_bound_relative, - ), - apply_bounds_to_defining_variable=default_cons, - ), - 'investment', - ).do_modeling() - - if not default_cons: - BoundingPatterns.scaled_bounds_with_state( - model=self, - variable=self.flow_rate, - scaling_variable=self._investment.size, - relative_bounds=(self.flow_rate_lower_bound_relative, self.flow_rate_upper_bound_relative), - scaling_bounds=(self.element.size.minimum_or_fixed_size, self.element.size.maximum_or_fixed_size), - variable_state=self.on_off.on, - ) + self._constraint_flow_rate() # Total flow hours tracking ModelingPrimitives.expression_tracking_variable( @@ -387,6 +344,81 @@ def do_modeling(self): # Effects self._create_shares() + def _create_on_off_model(self): + on = self.add_variables(binary=True, short_name='on', coords=self._model.get_coords()) + self.register_sub_model( + OnOffModel( + model=self._model, + label_of_element=self.label_of_element, + parameters=self.element.on_off_parameters, + on_variable=on, + previous_states=self.previous_states, + label_of_model=self.label_of_element, + ), + short_name='on_off', + ).do_modeling() + + def _create_investment_model(self): + self.register_sub_model( + InvestmentModel( + model=self._model, + label_of_element=self.label_of_element, + parameters=self.element.size, + label_of_model=self.label_of_element, + ), + 'investment', + ).do_modeling() + + def _constraint_flow_rate(self): + if not self.with_investment and not self.with_on_off: + # Most basic case. Already covered by direct variable bounds + pass + + elif self.with_on_off and not self.with_investment: + # OnOff, but no Investment + self._create_on_off_model() + bounds = self.relative_flow_rate_bounds + BoundingPatterns.bounds_with_state( + self, + variable=self.flow_rate, + bounds=(bounds[0] * self.element.size, bounds[1] * self.element.size), + variable_state=self.on_off.on, + ) + + elif self.with_investment and not self.with_on_off: + # Investment, but no OnOff + self._create_investment_model() + BoundingPatterns.scaled_bounds( + self, + variable=self.flow_rate, + scaling_variable=self.investment.size, + relative_bounds=self.relative_flow_rate_bounds, + ) + + elif self.with_investment and self.with_on_off: + # Investment and OnOff + self._create_investment_model() + self._create_on_off_model() + + BoundingPatterns.scaled_bounds_with_state( + model=self, + variable=self.flow_rate, + scaling_variable=self._investment.size, + relative_bounds=self.relative_flow_rate_bounds, + scaling_bounds=(self.element.size.minimum_or_fixed_size, self.element.size.maximum_or_fixed_size), + variable_state=self.on_off.on, + ) + else: + raise Exception('Not valid') + + @property + def with_on_off(self) -> bool: + return self.element.on_off_parameters is not None + + @property + def with_investment(self) -> bool: + return isinstance(self.element.size, InvestParameters) + # Properties for clean access to variables @property def flow_rate(self) -> linopy.Variable: @@ -421,7 +453,7 @@ def _create_shares(self): def _create_bounds_for_load_factor(self): """Create load factor constraints using current approach""" # Get the size (either from element or investment) - size = self.element.size if self._investment is None else self._investment.size + size = self.investment.size if self.with_investment else self.element.size # Maximum load factor constraint if self.element.load_factor_max is not None: @@ -440,57 +472,34 @@ def _create_bounds_for_load_factor(self): ) @property - def flow_rate_bounds_on(self) -> Tuple[TemporalData, TemporalData]: - """Returns absolute flow rate bounds for OnOffModel""" - relative_minimum, relative_maximum = self.flow_rate_lower_bound_relative, self.flow_rate_upper_bound_relative - size = self.element.size - if not isinstance(size, InvestParameters): - return relative_minimum * size, relative_maximum * size - - if size.fixed_size is not None: - return relative_minimum * size.fixed_size, relative_maximum * size.fixed_size - - return relative_minimum * size.minimum_or_fixed_size, relative_maximum * size.maximum_or_fixed_size - - @property - def flow_rate_lower_bound_relative(self) -> TemporalData: - """Returns the lower bound of the flow_rate relative to its size""" - fixed_profile = self.element.fixed_relative_profile - if fixed_profile is None: - return self.element.relative_minimum - return fixed_profile + def relative_flow_rate_bounds(self) -> Tuple[TemporalData, TemporalData]: + if self.element.fixed_relative_profile is not None: + return self.element.fixed_relative_profile, self.element.fixed_relative_profile + return self.element.relative_minimum, self.element.relative_maximum @property - def flow_rate_upper_bound_relative(self) -> TemporalData: - """Returns the upper bound of the flow_rate relative to its size""" - fixed_profile = self.element.fixed_relative_profile - if fixed_profile is None: - return self.element.relative_maximum - return fixed_profile - - @property - def flow_rate_lower_bound(self) -> TemporalData: + def absolute_flow_rate_bounds(self) -> Tuple[TemporalData, TemporalData]: """ - Returns the minimum bound the flow_rate can reach. - Further constraining might be done in OnOffModel and InvestmentModel + Returns the absolute bounds the flow_rate can reach. + Further constraining might be needed """ - if self.element.on_off_parameters is not None: - return 0 - if isinstance(self.element.size, InvestParameters): - if self.element.size.optional: - return 0 - return self.flow_rate_lower_bound_relative * self.element.size.minimum_or_fixed_size - return self.flow_rate_lower_bound_relative * self.element.size + lb_relative, ub_relative = self.relative_flow_rate_bounds + + lb = 0 + if not self.with_on_off: + if not self.with_investment: + # Basic case without investment and without OnOff + lb = lb_relative * self.element.size + elif not self.element.size.optional: + # With non-optional Investment + lb = lb_relative * self.element.size.minimum_or_fixed_size + + if self.with_investment: + ub = ub_relative * self.element.size.maximum_or_fixed_size + else: + ub = ub_relative * self.element.size - @property - def flow_rate_upper_bound(self) -> TemporalData: - """ - Returns the maximum bound the flow_rate can reach. - Further constraining might be done in OnOffModel and InvestmentModel - """ - if isinstance(self.element.size, InvestParameters): - return self.flow_rate_upper_bound_relative * self.element.size.maximum_or_fixed_size - return self.flow_rate_upper_bound_relative * self.element.size + return lb, ub @property def on_off(self) -> Optional[OnOffModel]: @@ -511,6 +520,14 @@ def investment(self) -> Optional[InvestmentModel]: return None return self.sub_models_direct['investment'] + @property + def previous_states(self) -> Optional[xr.DataArray]: + """Previous states of the flow rate""" + if self.element.previous_flow_rate is None: + return None + + return ModelingUtilities.compute_previous_states(self.element.previous_flow_rate) + class BusModel(ElementModel): def __init__(self, model: FlowSystemModel, element: Bus): diff --git a/flixopt/features.py b/flixopt/features.py index 6708a3221..a9e2dc589 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -26,10 +26,7 @@ def __init__( model: FlowSystemModel, label_of_element: str, parameters: InvestParameters, - defining_variable: linopy.Variable, - relative_bounds_of_defining_variable: Tuple[TemporalData, TemporalData], label_of_model: Optional[str] = None, - apply_bounds_to_defining_variable: bool = True, ): """ This feature model is used to model the investment of a variable. @@ -46,11 +43,6 @@ def __init__( """ super().__init__(model, label_of_element=label_of_element, parameters=parameters, label_of_model=label_of_model) - self._defining_variable = defining_variable - self._relative_bounds_of_defining_variable = relative_bounds_of_defining_variable - self._apply_bounds_to_defining_variable = apply_bounds_to_defining_variable - - # Only keep non-variable attributes self.piecewise_effects: Optional[PiecewiseEffectsModel] = None @@ -75,14 +67,6 @@ def create_variables_and_constraints(self): bounds=(self.parameters.minimum_or_fixed_size, self.parameters.maximum_or_fixed_size), ) - if self._apply_bounds_to_defining_variable: - BoundingPatterns.scaled_bounds( - self, - variable=self._defining_variable, - scaling_variable=self.size, - relative_bounds=self._relative_bounds_of_defining_variable, - ) - if self.parameters.piecewise_effects: self.piecewise_effects = self.register_sub_model( PiecewiseEffectsModel( @@ -146,11 +130,9 @@ def __init__( model: FlowSystemModel, label_of_element: str, parameters: OnOffParameters, - flow_rate: linopy.Variable, - flow_rate_bounds: Tuple[TemporalData, TemporalData], - previous_flow_rate: Optional[TemporalData], + on_variable: linopy.Variable, + previous_states: Optional[TemporalData], label_of_model: Optional[str] = None, - apply_bounds_to_flow_rates: bool = True, ): """ This feature model is used to model the on/off state of flow_rate(s). It does not matter of the flow_rates are @@ -160,32 +142,18 @@ def __init__( model: The optimization model instance label_of_element: The label of the parent (Element). Used to construct the full label of the model. parameters: The parameters of the feature model. - flow_rate: The flow_rates to be modeled - flow_rate_bounds: The bounds of the flow_rates, with respect to the minimum/maximum investment sizes - previous_flow_rate: The previous flow_rates + on_variable: The variable that determines the on state + previous_states: The previous flow_rates label_of_model: The label of the model. This is needed to construct the full label of the model. """ super().__init__(model, label_of_element, parameters=parameters, label_of_model=label_of_model) - self._flow_rate = flow_rate - self._flow_rate_bounds = flow_rate_bounds - self._previous_flow_rate = previous_flow_rate - self._apply_bounds_to_flow_rates = apply_bounds_to_flow_rates + self.on = on_variable + self._previous_states = previous_states def create_variables_and_constraints(self): - # 1. Main binary state using existing pattern - on = self.add_variables(binary=True, short_name='on', coords=self._model.get_coords()) if self.parameters.use_off: off = self.add_variables(binary=True, short_name='off', coords=self._model.get_coords()) - self.add_constraints(on + off == 1, short_name='complementary') - - # 2. Control variables - if self._apply_bounds_to_flow_rates: - BoundingPatterns.bounds_with_state( - self, - variable=self._flow_rate, - bounds=self._flow_rate_bounds, - variable_state=self.on, - ) + self.add_constraints(self.on + off == 1, short_name='complementary') # 3. Total duration tracking using existing pattern duration_expr = (self.on * self._model.hours_per_step).sum('time') @@ -208,7 +176,9 @@ def create_variables_and_constraints(self): switch_on=self.switch_on, switch_off=self.switch_off, name=f'{self.label_of_model}|switch', - previous_state=ModelingUtilities.get_most_recent_state(self._previous_flow_rate), + previous_state=ModelingUtilities.get_most_recent_state( + self._previous_states.isel(time=-1) + ) if self._previous_states is not None else 0, ) if self.parameters.switch_on_total_max is not None: @@ -223,7 +193,7 @@ def create_variables_and_constraints(self): short_name='consecutive_on_hours', minimum_duration=self.parameters.consecutive_on_hours_min, maximum_duration=self.parameters.consecutive_on_hours_max, - previous_duration=ModelingUtilities.compute_previous_on_duration([self._previous_flow_rate], self._model.hours_per_step), + previous_duration=self._get_previous_on_duration(), ) # 6. Consecutive off duration using existing pattern @@ -234,10 +204,9 @@ def create_variables_and_constraints(self): short_name='consecutive_off_hours', minimum_duration=self.parameters.consecutive_off_hours_min, maximum_duration=self.parameters.consecutive_off_hours_max, - previous_duration=ModelingUtilities.compute_previous_off_duration( - [self._previous_flow_rate], self._model.hours_per_step - ), + previous_duration=self._get_previous_off_duration(), ) + #TODO: def add_effects(self): """Add operational effects""" @@ -261,10 +230,6 @@ def add_effects(self): ) # Properties access variables from Model's tracking system - @property - def on(self) -> Optional[linopy.Variable]: - """Binary on state variable""" - return self['on'] @property def total_on_hours(self) -> Optional[linopy.Variable]: @@ -302,15 +267,20 @@ def consecutive_off_hours(self) -> Optional[linopy.Variable]: return self.get('consecutive_off_hours') def _get_previous_on_duration(self): - hours_per_step = self._model.hours_per_step.isel(time=0).values.flatten()[0] - return ModelingUtilities.compute_previous_on_duration([self._previous_flow_rate], hours_per_step) + """Get previous on duration. Previously OFF by default, for one timestep""" + hours_per_step = self._model.hours_per_step.isel(time=0).min().item() + if self._previous_states is None: + return 0 + else: + return ModelingUtilities.compute_consecutive_hours_in_state(self._previous_states, hours_per_step) def _get_previous_off_duration(self): - hours_per_step = self._model.hours_per_step.isel(time=0).values.flatten()[0] - return ModelingUtilities.compute_previous_off_duration(self._previous_flow_rates, hours_per_step) - - def _get_previous_state(self): - return ModelingUtilities.get_most_recent_state(self._previous_flow_rates) + """Get previous off duration. Previously OFF by default, for one timestep""" + hours_per_step = self._model.hours_per_step.isel(time=0).min().item() + if self._previous_states is None: + return hours_per_step + else: + return ModelingUtilities.compute_consecutive_hours_in_state(self._previous_states * -1 + 1, hours_per_step) class PieceModel(Model): diff --git a/flixopt/modeling.py b/flixopt/modeling.py index 17f47c939..0c989d01e 100644 --- a/flixopt/modeling.py +++ b/flixopt/modeling.py @@ -3,6 +3,7 @@ import linopy import numpy as np +import xarray as xr from .config import CONFIG from .core import NonTemporalData, Scalar, TemporalData, FlowSystemDimensions @@ -11,138 +12,166 @@ logger = logging.getLogger('flixopt') -class ModelingUtilities: - """Utility functions for modeling calculations - used across different classes""" +class ModelingUtilitiesAbstract: + """Utility functions for modeling calculations - leveraging xarray for temporal data""" @staticmethod - def compute_consecutive_hours_in_state( - binary_values: TemporalData, hours_per_timestep: Union[int, float, np.ndarray] - ) -> Scalar: + def to_binary( + values: xr.DataArray, + epsilon: Optional[float] = None, + dims: Optional[Union[str, List[str]]] = None, + ) -> xr.DataArray: """ - Computes the final consecutive duration in state 'on' (=1) in hours, from a binary array. + Converts a DataArray to binary {0, 1} values. Args: - binary_values: An int or 1D binary array containing only `0`s and `1`s. - hours_per_timestep: The duration of each timestep in hours. - If a scalar is provided, it is used for all timesteps. - If an array is provided, it must be as long as the last consecutive duration in binary_values. + values: Input DataArray to convert to binary + epsilon: Tolerance for zero detection (uses CONFIG.modeling.EPSILON if None) + dims: Dims to keep. Other dimensions are collapsed using .any() -> If any value is 1, all are 1. Returns: - The duration of the binary variable in hours. - - Raises - ------ - TypeError - If the length of binary_values and dt_in_hours is not equal, but None is a scalar. + Binary DataArray with same shape (or collapsed if collapse_non_time=True) """ - if np.isscalar(binary_values) and np.isscalar(hours_per_timestep): - return binary_values * hours_per_timestep - elif np.isscalar(binary_values) and not np.isscalar(hours_per_timestep): - return binary_values * hours_per_timestep[-1] + if not isinstance(values, xr.DataArray): + values = xr.DataArray(values, dims=['time'], coords={'time': range(len(values))}) - if np.isclose(binary_values[-1], 0, atol=CONFIG.modeling.EPSILON): - return 0 + if epsilon is None: + epsilon = CONFIG.modeling.EPSILON - if np.isscalar(hours_per_timestep): - hours_per_timestep = np.ones(len(binary_values)) * hours_per_timestep - hours_per_timestep: np.ndarray + if values.size == 0: + return xr.DataArray(0) if values.item() < epsilon else xr.DataArray(1) - indexes_with_zero_values = np.where(np.isclose(binary_values, 0, atol=CONFIG.modeling.EPSILON))[0] - if len(indexes_with_zero_values) == 0: - nr_of_indexes_with_consecutive_ones = len(binary_values) - else: - nr_of_indexes_with_consecutive_ones = len(binary_values) - indexes_with_zero_values[-1] - 1 + # Convert to binary states + binary_states = (np.abs(values) >= epsilon) - if len(hours_per_timestep) < nr_of_indexes_with_consecutive_ones: - raise ValueError( - f'When trying to calculate the consecutive duration, the length of the last duration ' - f'({nr_of_indexes_with_consecutive_ones}) is longer than the provided hours_per_timestep ({len(hours_per_timestep)}), ' - f'as {binary_values=}' - ) + # Optionally collapse dimensions using .any() + if dims is not None: + dims = [dims] if isinstance(dims, str) else dims - return np.sum( - binary_values[-nr_of_indexes_with_consecutive_ones:] - * hours_per_timestep[-nr_of_indexes_with_consecutive_ones:] - ) + binary_states = binary_states.any(dim=[d for d in binary_states.dims if d not in dims]) + + return binary_states.astype(int) @staticmethod - def compute_previous_states(previous_values: List[TemporalData], epsilon: float = None) -> np.ndarray: + def count_consecutive_states( + binary_values: xr.DataArray, + epsilon: float = None, + ) -> float: """ - Computes the previous states {0, 1} of defining variables as a binary array from their previous values. + Counts the number of consecutive states in a binary time series. Args: - previous_values: List of previous values for variables + binary_values: Binary DataArray with 'time' dim epsilon: Tolerance for zero detection (uses CONFIG.modeling.EPSILON if None) Returns: - Binary array of previous states + The consecutive number of steps spent in the final state of the timeseries """ if epsilon is None: epsilon = CONFIG.modeling.EPSILON - if not previous_values or all(val is None for val in previous_values): - return np.array([0]) + binary_values = binary_values.any(dim=[d for d in binary_values.dims if d != 'time']) + + # Handle scalar case + if binary_values.ndim == 0: + return float(binary_values.item()) - # Convert to 2D-array and compute binary on/off states - previous_values = np.array([values for values in previous_values if values is not None]) # Filter out None - if previous_values.ndim > 1: - return np.any(~np.isclose(previous_values, 0, atol=epsilon), axis=0).astype(int) + # Check if final state is off + if np.isclose(binary_values.isel(time=-1).item(), 0, atol=epsilon): + return 0.0 + + # Find consecutive 'on' period from the end + is_zero = np.isclose(binary_values, 0, atol=epsilon) + + # Find the last zero, then sum everything after it + zero_indices = np.where(is_zero)[0] + if len(zero_indices) == 0: + # All 'on' - sum everything + start_idx = 0 + else: + # Start after last zero + start_idx = zero_indices[-1] + 1 - return (~np.isclose(previous_values, 0, atol=epsilon)).astype(int) + consecutive_values = binary_values.isel(time=slice(start_idx, None)) + + return float(consecutive_values.sum().item()) + + +class ModelingUtilities: @staticmethod - def compute_previous_on_duration(previous_values: List[TemporalData], hours_per_step: Union[int, float]) -> Scalar: + def compute_consecutive_hours_in_state( + binary_values: Union[xr.DataArray, np.ndarray, int], + hours_per_timestep: Union[int, float], + epsilon: float = None, + ) -> float: """ - Convenience method to compute previous consecutive 'on' duration. + Computes the final consecutive duration in state 'on' (=1) in hours. Args: - previous_values: List of previous values for variables - hours_per_step: Duration of each timestep in hours + binary_values: Binary DataArray with 'time' dim, or scalar/array + hours_per_timestep: Duration of each timestep in hours + epsilon: Tolerance for zero detection (uses CONFIG.modeling.EPSILON if None) Returns: - Previous consecutive on duration in hours + The duration of the final consecutive 'on' period in hours """ - if not previous_values: - return 0 + if not isinstance(hours_per_timestep, (int, float)): + raise TypeError(f'hours_per_timestep must be a scalar, got {type(hours_per_timestep)}') - previous_states = ModelingUtilities.compute_previous_states(previous_values) - return ModelingUtilities.compute_consecutive_hours_in_state(previous_states, hours_per_step) + return ModelingUtilitiesAbstract.count_consecutive_states( + binary_values=binary_values, epsilon=epsilon + ) * hours_per_timestep + + @staticmethod + def compute_previous_states(previous_values: Optional[xr.DataArray], epsilon: Optional[float] = None) -> xr.DataArray: + return ModelingUtilitiesAbstract.to_binary(values=previous_values, epsilon=epsilon, dims='time') @staticmethod - def compute_previous_off_duration(previous_values: List[TemporalData], hours_per_step: Union[int, float]) -> Scalar: + def compute_previous_on_duration( + previous_values: xr.DataArray, hours_per_step: Union[xr.DataArray, float, int] + ) -> float: + return ModelingUtilitiesAbstract.count_consecutive_states( + ModelingUtilitiesAbstract.to_binary(previous_values) + ) * hours_per_step + + @staticmethod + def compute_previous_off_duration( + previous_values: xr.DataArray, hours_per_step: Union[xr.DataArray, float, int] + ) -> float: """ - Convenience method to compute previous consecutive 'off' duration. + Compute previous consecutive 'off' duration. Args: - previous_values: List of previous values for variables + previous_values: DataArray with 'time' dimension hours_per_step: Duration of each timestep in hours Returns: Previous consecutive off duration in hours """ - if not previous_values: - return 0 + if previous_values is None or previous_values.size == 0: + return 0.0 previous_states = ModelingUtilities.compute_previous_states(previous_values) previous_off_states = 1 - previous_states return ModelingUtilities.compute_consecutive_hours_in_state(previous_off_states, hours_per_step) @staticmethod - def get_most_recent_state(previous_values: List[TemporalData]) -> int: + def get_most_recent_state(previous_values: Optional[xr.DataArray]) -> int: """ Get the most recent binary state from previous values. Args: - previous_values: List of previous values for variables + previous_values: DataArray with 'time' dimension Returns: Most recent binary state (0 or 1) """ - if not previous_values: + if previous_values is None or previous_values.size == 0: return 0 previous_states = ModelingUtilities.compute_previous_states(previous_values) - return int(previous_states[-1]) + return int(previous_states.isel(time=-1).item()) class ModelingPrimitives: diff --git a/flixopt/structure.py b/flixopt/structure.py index 953636f9b..8fdce6ad0 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -895,6 +895,10 @@ def __repr__(self) -> str: f"Submodels:\n----------\n{sub_models_string}" ) + @property + def hours_per_step(self): + return self._model.hours_per_step + class BaseFeatureModel(Model): """Minimal base class for feature models that use factory patterns""" @@ -924,10 +928,6 @@ def add_effects(self): """Override in subclasses to add effects""" pass # Default: no effects - @property - def hours_per_step(self): - return self._model.hours_per_step - class ElementModel(Model): """Stores the mathematical Variables and Constraints for Elements""" From 29bec8c97aa2b58a5f353590fa273b4aa6db2c6d Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 21 Jul 2025 16:59:38 +0200 Subject: [PATCH 222/448] Improve handling of previous flowrates --- flixopt/elements.py | 69 ++++++++++++++++++------ flixopt/modeling.py | 13 ++--- tests/test_component.py | 113 ++++++++++++++++++++++++++++++++++------ 3 files changed, 159 insertions(+), 36 deletions(-) diff --git a/flixopt/elements.py b/flixopt/elements.py index 796d82864..02d0bf115 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -16,7 +16,7 @@ from .features import InvestmentModel, OnOffModel, PreventSimultaneousUsageModel, ModelingPrimitives from .interface import InvestParameters, OnOffParameters from .structure import Element, ElementModel, FlowSystemModel, register_class_for_io -from .modeling import BoundingPatterns, ModelingUtilities +from .modeling import BoundingPatterns, ModelingUtilitiesAbstract if TYPE_CHECKING: from .flow_system import FlowSystem @@ -163,7 +163,7 @@ def __init__( flow_hours_total_min: Optional[Scalar] = None, load_factor_min: Optional[Scalar] = None, load_factor_max: Optional[Scalar] = None, - previous_flow_rate: Optional[TemporalDataUser] = None, + previous_flow_rate: Optional[Union[Scalar, List[Scalar]]] = None, meta_data: Optional[Dict] = None, ): r""" @@ -210,9 +210,7 @@ def __init__( self.flow_hours_total_min = flow_hours_total_min self.on_off_parameters = on_off_parameters - self.previous_flow_rate = ( - previous_flow_rate if not isinstance(previous_flow_rate, list) else np.array(previous_flow_rate) - ) + self.previous_flow_rate = previous_flow_rate self.component: str = 'UnknownComponent' self.is_input_in_component: Optional[bool] = None @@ -294,6 +292,11 @@ def _plausibility_checks(self) -> None: f'Consider using on_off_parameters to allow the flow to be switched on and off.' ) + if self.previous_flow_rate is not None: + if not any([isinstance(self.previous_flow_rate, np.ndarray) and self.previous_flow_rate.ndim == 1, + isinstance(self.previous_flow_rate, (int, float, list))]): + raise TypeError(f'previous_flow_rate must be None, a scalar, a list of scalars or a 1D-numpy-array. Got {type(self.previous_flow_rate)}') + @property def label_full(self) -> str: return f'{self.component}({self.label})' @@ -523,10 +526,19 @@ def investment(self) -> Optional[InvestmentModel]: @property def previous_states(self) -> Optional[xr.DataArray]: """Previous states of the flow rate""" - if self.element.previous_flow_rate is None: + #TODO: This would be nicer to handle in the Flow itself, and allow DataArrays as well. + previous_flow_rate = self.element.previous_flow_rate + if previous_flow_rate is None: return None - return ModelingUtilities.compute_previous_states(self.element.previous_flow_rate) + return ModelingUtilitiesAbstract.to_binary( + values=xr.DataArray( + [previous_flow_rate] if np.isscalar(previous_flow_rate) else previous_flow_rate, + dims='time' + ), + epsilon=CONFIG.modeling.EPSILON, + dims='time', + ) class BusModel(ElementModel): @@ -588,22 +600,27 @@ def do_modeling(self): flow.on_off_parameters = OnOffParameters() for flow in all_flows: - self.register_sub_model(flow.create_model(self._model), short_name=flow.label) - - for sub_model in self.sub_models: - sub_model.do_modeling() + flow_model = self.register_sub_model(flow.create_model(self._model), short_name=flow.label) + flow_model.do_modeling() if self.element.on_off_parameters: + on = self.add_variables(binary=True, short_name='on', coords=self._model.get_coords()) + if len(all_flows) == 1: + self.add_constraints(on == all_flows[0].model.on_off.on, short_name='on') + else: + flow_ons = [flow.model.on_off.on for flow in all_flows] + #TODO: Is the EPSILON even necessary? + self.add_constraints(on <= sum(flow_ons) + CONFIG.modeling.EPSILON, short_name='on|ub') + self.add_constraints(on >= sum(flow_ons) / (len(flow_ons) + CONFIG.modeling.EPSILON), short_name='on|lb') + self.on_off = self.register_sub_model( OnOffModel( model=self._model, label_of_element=self.label_of_element, parameters=self.element.on_off_parameters, - flow_rates=[flow.model.flow_rate for flow in all_flows], - flow_rate_bounds=[flow.model.flow_rate_bounds_on for flow in all_flows], - previous_flow_rates=[flow.previous_flow_rate for flow in all_flows], + on_variable=on, label_of_model=self.label_of_element, - apply_bounds_to_flow_rates=True, + previous_states=self.previous_states, ), short_name='on_off', ) @@ -623,3 +640,25 @@ def results_structure(self): 'outputs': [flow.model.flow_rate.name for flow in self.element.outputs], 'flows': [flow.label_full for flow in self.element.inputs + self.element.outputs], } + + @property + def previous_states(self) -> Optional[xr.DataArray]: + """Previous state of the component, derived from its flows""" + if self.element.on_off_parameters is None: + raise ValueError(f'OnOffModel not present in \n{self}\nCant access previous_states') + + previous_states = [flow.model.on_off._previous_states for flow in self.element.inputs + self.element.outputs] + previous_states = [da for da in previous_states if da is not None] + + if not previous_states: # Empty list + return None + + max_len = max(da.sizes['time'] for da in previous_states) + + padded_previous_states = [ + da.assign_coords( + time=range(-da.sizes['time'], 0) + ).reindex(time=range(-max_len, 0), fill_value=0) + for da in previous_states + ] + return xr.concat(padded_previous_states, dim='flow').any(dim='flow').astype(int) diff --git a/flixopt/modeling.py b/flixopt/modeling.py index 0c989d01e..c3839749c 100644 --- a/flixopt/modeling.py +++ b/flixopt/modeling.py @@ -55,13 +55,15 @@ def to_binary( @staticmethod def count_consecutive_states( binary_values: xr.DataArray, + dim: str = 'time', epsilon: float = None, ) -> float: """ Counts the number of consecutive states in a binary time series. Args: - binary_values: Binary DataArray with 'time' dim + binary_values: Binary DataArray + dim: Dimension to count consecutive states over epsilon: Tolerance for zero detection (uses CONFIG.modeling.EPSILON if None) Returns: @@ -70,14 +72,14 @@ def count_consecutive_states( if epsilon is None: epsilon = CONFIG.modeling.EPSILON - binary_values = binary_values.any(dim=[d for d in binary_values.dims if d != 'time']) + binary_values = binary_values.any(dim=[d for d in binary_values.dims if d != dim]) # Handle scalar case if binary_values.ndim == 0: return float(binary_values.item()) # Check if final state is off - if np.isclose(binary_values.isel(time=-1).item(), 0, atol=epsilon): + if np.isclose(binary_values.isel({dim: -1}).item(), 0, atol=epsilon).all(): return 0.0 # Find consecutive 'on' period from the end @@ -92,9 +94,9 @@ def count_consecutive_states( # Start after last zero start_idx = zero_indices[-1] + 1 - consecutive_values = binary_values.isel(time=slice(start_idx, None)) + consecutive_values = binary_values.isel({dim:slice(start_idx, None)}) - return float(consecutive_values.sum().item()) + return float(consecutive_values.sum().item()) #TODO: Som only over one dim? class ModelingUtilities: @@ -260,7 +262,6 @@ def sum_up_variable( model: FlowSystemModel, variable_to_count: linopy.Variable, name: str = None, - short_name: str = None, bounds: Tuple[NonTemporalData, NonTemporalData] = None, factor: TemporalData = 1, ) -> Tuple[linopy.Variable, linopy.Constraint]: diff --git a/tests/test_component.py b/tests/test_component.py index f65c93414..25e496694 100644 --- a/tests/test_component.py +++ b/tests/test_component.py @@ -121,15 +121,28 @@ def test_on_with_multiple_flows(self, basic_flow_system_linopy): assert_conequal(model.constraints['TestComponent(Out2)|flow_rate|lb'], model.variables['TestComponent(Out2)|flow_rate'] >= model.variables['TestComponent(Out2)|on'] * 0.3 * 300) assert_conequal(model.constraints['TestComponent(Out2)|flow_rate|ub'], model.variables['TestComponent(Out2)|flow_rate'] <= model.variables['TestComponent(Out2)|on'] * 300 * ub_out2) - assert_conequal(model.constraints['TestComponent|on|lb'], - model.variables['TestComponent|on'] * 1e-5 <= model.variables['TestComponent(In1)|flow_rate'] + model.variables['TestComponent(Out1)|flow_rate'] + model.variables['TestComponent(Out2)|flow_rate']) - # TODO: Might there be a better way to no use 1e-5? - assert_conequal(model.constraints['TestComponent|on|ub'], - model.variables['TestComponent|on'] * (100 + 200 + 300 * ub_out2)/3 - >= (model.variables['TestComponent(In1)|flow_rate'] - + model.variables['TestComponent(Out1)|flow_rate'] - + model.variables['TestComponent(Out2)|flow_rate']) / 3 - ) + assert_conequal( + model.constraints['TestComponent|on|lb'], + model.variables['TestComponent|on'] + >= ( + model.variables['TestComponent(In1)|on'] + + model.variables['TestComponent(Out1)|on'] + + model.variables['TestComponent(Out2)|on'] + ) + / (3 + 1e-5), + ) + assert_conequal( + model.constraints['TestComponent|on|ub'], + model.variables['TestComponent|on'] + <= ( + model.variables['TestComponent(In1)|on'] + + model.variables['TestComponent(Out1)|on'] + + model.variables['TestComponent(Out2)|on'] + ) + + 1e-5, + ) + + def test_on_with_single_flow(self, basic_flow_system_linopy): """Test that flow model constraints are correctly generated.""" @@ -159,8 +172,7 @@ def test_on_with_single_flow(self, basic_flow_system_linopy): 'TestComponent(In1)|flow_rate|lb', 'TestComponent(In1)|flow_rate|ub', 'TestComponent(In1)|on_hours_total', - 'TestComponent|on|lb', - 'TestComponent|on|ub', + 'TestComponent|on', 'TestComponent|on_hours_total', } @@ -172,20 +184,91 @@ def test_on_with_single_flow(self, basic_flow_system_linopy): assert_conequal( model.constraints['TestComponent(In1)|flow_rate|lb'], - model.variables['TestComponent(In1)|on'] * 0.1 * 100 <= model.variables['TestComponent(In1)|flow_rate'], + model.variables['TestComponent(In1)|flow_rate'] >= model.variables['TestComponent(In1)|on'] * 0.1 * 100, ) assert_conequal( model.constraints['TestComponent(In1)|flow_rate|ub'], - model.variables['TestComponent(In1)|on'] * 100 >= model.variables['TestComponent(In1)|flow_rate'], + model.variables['TestComponent(In1)|flow_rate'] <= model.variables['TestComponent(In1)|on'] * 100, ) + assert_conequal( + model.constraints['TestComponent|on'], + model.variables['TestComponent|on'] == model.variables['TestComponent(In1)|on'], + ) + + def test_previous_states_with_multiple_flows(self, basic_flow_system_linopy): + """Test that flow model constraints are correctly generated.""" + flow_system = basic_flow_system_linopy + timesteps = flow_system.timesteps + ub_out2 = np.linspace(1, 1.5, 10).round(2) + inputs = [ + fx.Flow('In1', 'Fernwärme', relative_minimum=np.ones(10) * 0.1, size=100, previous_flow_rate=np.array([0, 0, 1e-6, 1e-5, 1e-4, 3,4])), + ] + outputs = [ + fx.Flow('Out1', 'Gas', relative_minimum=np.ones(10) * 0.2, size=200, previous_flow_rate=xr.DataArray([3,4,5], dims='time')), + fx.Flow('Out2', 'Gas', relative_minimum=np.ones(10) * 0.3, + relative_maximum = ub_out2, size=300, previous_flow_rate=20), + ] + comp = flixopt.elements.Component('TestComponent', inputs=inputs, outputs=outputs, + on_off_parameters=fx.OnOffParameters()) + flow_system.add_elements(comp) + model = create_linopy_model(flow_system) + + assert set(comp.model.variables) == { + 'TestComponent(In1)|flow_rate', + 'TestComponent(In1)|total_flow_hours', + 'TestComponent(In1)|on', + 'TestComponent(In1)|on_hours_total', + 'TestComponent(Out1)|flow_rate', + 'TestComponent(Out1)|total_flow_hours', + 'TestComponent(Out1)|on', + 'TestComponent(Out1)|on_hours_total', + 'TestComponent(Out2)|flow_rate', + 'TestComponent(Out2)|total_flow_hours', + 'TestComponent(Out2)|on', + 'TestComponent(Out2)|on_hours_total', + 'TestComponent|on', + 'TestComponent|on_hours_total', + } + + assert set(comp.model.constraints) == { + 'TestComponent(In1)|total_flow_hours', + 'TestComponent(In1)|flow_rate|lb', + 'TestComponent(In1)|flow_rate|ub', + 'TestComponent(In1)|on_hours_total', + 'TestComponent(Out1)|total_flow_hours', + 'TestComponent(Out1)|flow_rate|lb', + 'TestComponent(Out1)|flow_rate|ub', + 'TestComponent(Out1)|on_hours_total', + 'TestComponent(Out2)|total_flow_hours', + 'TestComponent(Out2)|flow_rate|lb', + 'TestComponent(Out2)|flow_rate|ub', + 'TestComponent(Out2)|on_hours_total', + 'TestComponent|on|lb', + 'TestComponent|on|ub', + 'TestComponent|on_hours_total', + } + + assert_var_equal(model['TestComponent(Out2)|flow_rate'], + model.add_variables(lower=0, upper=300 * ub_out2, coords=(timesteps,))) + assert_var_equal(model['TestComponent|on'], model.add_variables(binary=True, coords = (timesteps,))) + assert_var_equal(model['TestComponent(Out2)|on'], model.add_variables(binary=True, coords=(timesteps,))) + + assert_conequal(model.constraints['TestComponent(Out2)|flow_rate|lb'], model.variables['TestComponent(Out2)|flow_rate'] >= model.variables['TestComponent(Out2)|on'] * 0.3 * 300) + assert_conequal(model.constraints['TestComponent(Out2)|flow_rate|ub'], model.variables['TestComponent(Out2)|flow_rate'] <= model.variables['TestComponent(Out2)|on'] * 300 * ub_out2) + assert_conequal( model.constraints['TestComponent|on|lb'], - model.variables['TestComponent|on'] * 0.1 * 100 <= model.variables['TestComponent(In1)|flow_rate'], + model.variables['TestComponent|on'] >= (model.variables['TestComponent(In1)|on'] + model.variables['TestComponent(Out1)|on'] + model.variables['TestComponent(Out2)|on']) / (3 + 1e-5), ) assert_conequal( model.constraints['TestComponent|on|ub'], - model.variables['TestComponent|on'] * 100 >= model.variables['TestComponent(In1)|flow_rate'], + model.variables['TestComponent|on'] + <= ( + model.variables['TestComponent(In1)|on'] + + model.variables['TestComponent(Out1)|on'] + + model.variables['TestComponent(Out2)|on'] + ) + 1e-5, ) From 370ac9414788cacf1fd40b55ffac95c9d8b48cef Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 21 Jul 2025 17:00:02 +0200 Subject: [PATCH 223/448] Imropove repr and submodel acess --- flixopt/structure.py | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/flixopt/structure.py b/flixopt/structure.py index 8fdce6ad0..34de27f35 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -838,8 +838,14 @@ def sub_models_direct(self) -> Dict[str, 'Model']: @property def sub_models(self) -> List['Model']: """All sub-models of the model""" - direct = list(self.sub_models_direct.values()) - return direct + [model for sub_model in direct for model in sub_model.sub_models] + direct_submodels = list(self._sub_models.values()) + + # Recursively collect nested sub-models + nested_submodels = [] + for submodel in direct_submodels: + nested_submodels.extend(submodel.sub_models) # This calls the property recursively + + return direct_submodels + nested_submodels @property def constraints(self) -> linopy.Constraints: @@ -863,16 +869,6 @@ def variables(self) -> linopy.Variables: return self._model.variables[names] - @staticmethod - def _extract_short_name(item: Union[linopy.Variable, linopy.Constraint]) -> str: - """Extract short name from variable's full name""" - # Assumes format like "model_prefix|short_name" - name = str(item.name) - if '|' in name: - return name.split('|')[-1] # Take last part after | - else: - return name # Use full name if no | separator - def __repr__(self) -> str: """ Return a string representation of the linopy model. @@ -885,14 +881,14 @@ def __repr__(self) -> str: sub_models_string = ' \n' else: sub_models_string = '' - for sub_model in self.sub_models: - sub_models_string += f'\n * {sub_model.label_of_model}' + for sub_model_name, sub_model in self.sub_models_direct.items(): + sub_models_string += f'\n * {sub_model_name} [{sub_model.__class__.__name__}]' return ( f"{model_string}\n{'=' * len(model_string)}\n\n" f"Variables:\n----------\n{var_string}\n" f"Constraints:\n------------\n{con_string}\n" - f"Submodels:\n----------\n{sub_models_string}" + f"Submodels:\n----------{sub_models_string}" ) @property From 0f89ff07953d9ca114cd0080aa6e2165f351b213 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 21 Jul 2025 17:12:44 +0200 Subject: [PATCH 224/448] Update access pattern in tests --- flixopt/components.py | 1 + flixopt/features.py | 2 +- tests/test_linear_converter.py | 12 ++++++------ 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/flixopt/components.py b/flixopt/components.py index 6631cb214..b733d2d39 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -475,6 +475,7 @@ def do_modeling(self): PiecewiseModel( model=self._model, label_of_element=self.label_of_element, + label_of_model=f'{self.label_of_element}', piecewise_variables=piecewise_conversion, zero_point=self.on_off.on if self.on_off is not None else False, as_time_series=True, diff --git a/flixopt/features.py b/flixopt/features.py index a9e2dc589..dd095a9e2 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -329,10 +329,10 @@ def __init__( self, model: FlowSystemModel, label_of_element: str, + label_of_model: str, piecewise_variables: Dict[str, Piecewise], zero_point: Optional[Union[bool, linopy.Variable]], as_time_series: bool, - label_of_model: str = '', ): """ Modeling a Piecewise relation between miultiple variables. diff --git a/tests/test_linear_converter.py b/tests/test_linear_converter.py index a01c17ef2..11b5b5673 100644 --- a/tests/test_linear_converter.py +++ b/tests/test_linear_converter.py @@ -189,8 +189,8 @@ def test_linear_converter_with_on_off(self, basic_flow_system_linopy): # Check on_hours_total constraint assert_conequal( model.constraints['Converter|on_hours_total'], - converter.model.on_off.variables['Converter|on_hours_total'] == - (converter.model.on_off.variables['Converter|on'] * model.hours_per_step).sum() + model.variables['Converter|on_hours_total'] == + (model.variables['Converter|on'] * model.hours_per_step).sum() ) # Check conversion constraint @@ -204,7 +204,7 @@ def test_linear_converter_with_on_off(self, basic_flow_system_linopy): assert_conequal( model.constraints['Converter->Costs(operation)'], model.variables['Converter->Costs(operation)'] == - converter.model.on_off.variables['Converter|on'] * model.hours_per_step * 5 + model.variables['Converter|on'] * model.hours_per_step * 5 ) def test_linear_converter_multidimensional(self, basic_flow_system_linopy): @@ -539,8 +539,8 @@ def test_piecewise_conversion_with_onoff(self, basic_flow_system_linopy): assert 'Converter|on_hours_total' in model.constraints assert_conequal( model.constraints['Converter|on_hours_total'], - converter.model.on_off.variables['Converter|on_hours_total'] == - (converter.model.on_off.variables['Converter|on'] * model.hours_per_step).sum() + model['Converter|on_hours_total'] == + (model['Converter|on'] * model.hours_per_step).sum() ) # Verify that the costs effect is applied @@ -548,7 +548,7 @@ def test_piecewise_conversion_with_onoff(self, basic_flow_system_linopy): assert_conequal( model.constraints['Converter->Costs(operation)'], model.variables['Converter->Costs(operation)'] == - converter.model.on_off.variables['Converter|on'] * model.hours_per_step * 5 + model.variables['Converter|on'] * model.hours_per_step * 5 ) From 4781cff3d5499bc88a28a9297e8397e40fb756e4 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 21 Jul 2025 20:44:25 +0200 Subject: [PATCH 225/448] Fix PiecewiseEffects and StorageModel --- flixopt/components.py | 9 +++++++-- flixopt/features.py | 11 +++++------ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/flixopt/components.py b/flixopt/components.py index b733d2d39..1377d1f83 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -15,6 +15,7 @@ from .features import InvestmentModel, OnOffModel, PiecewiseModel from .interface import InvestParameters, OnOffParameters, PiecewiseConversion from .structure import FlowSystemModel, register_class_for_io +from.modeling import BoundingPatterns if TYPE_CHECKING: from .flow_system import FlowSystem @@ -535,12 +536,16 @@ def do_modeling(self): model=self._model, label_of_element=self.label_of_element, parameters=self.element.capacity_in_flow_hours, - defining_variable=self.charge_state, - relative_bounds_of_defining_variable=self.relative_charge_state_bounds, ), short_name='investment', ) self._investment.do_modeling() + BoundingPatterns.scaled_bounds( + self, + variable=self.charge_state, + scaling_variable=self.investment.size, + relative_bounds=self.relative_charge_state_bounds, + ) # Initial charge state self._initial_and_final_charge_state() diff --git a/flixopt/features.py b/flixopt/features.py index dd095a9e2..475c0e553 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -72,6 +72,7 @@ def create_variables_and_constraints(self): PiecewiseEffectsModel( model=self._model, label_of_element=self.label_of_element, + label_of_model=f'{self.label_of_element}|PiecewiseEffects', piecewise_origin=(self.size.name, self.parameters.piecewise_effects.piecewise_origin), piecewise_shares=self.parameters.piecewise_effects.piecewise_shares, zero_point=self.is_invested, @@ -176,10 +177,8 @@ def create_variables_and_constraints(self): switch_on=self.switch_on, switch_off=self.switch_off, name=f'{self.label_of_model}|switch', - previous_state=ModelingUtilities.get_most_recent_state( - self._previous_states.isel(time=-1) - ) if self._previous_states is not None else 0, - ) + previous_state=self._previous_states.isel(time=-1) if self._previous_states is not None else 0, + ) if self.parameters.switch_on_total_max is not None: count = self.add_variables(lower=0, upper=self.parameters.switch_on_total_max, coords=self._model.get_coords(('year', 'scenario')), short_name='switch|count') @@ -410,12 +409,12 @@ def __init__( self, model: FlowSystemModel, label_of_element: str, + label_of_model: str, piecewise_origin: Tuple[str, Piecewise], piecewise_shares: Dict[str, Piecewise], zero_point: Optional[Union[bool, linopy.Variable]], - label: str = 'PiecewiseEffects', ): - super().__init__(model, label_of_element, label) + super().__init__(model, label_of_element=label_of_element, label_of_model=label_of_model) assert len(piecewise_origin[1]) == len(list(piecewise_shares.values())[0]), ( 'Piece length of variable_segments and share_segments must be equal' ) From 333ab83bd25c19427584304bc270492e4bba6a48 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 21 Jul 2025 21:00:51 +0200 Subject: [PATCH 226/448] Fix StorageModel and Remove PreventSimultaniousUseModel --- flixopt/components.py | 1 + flixopt/elements.py | 9 ++++++--- flixopt/features.py | 38 -------------------------------------- flixopt/modeling.py | 8 ++++---- 4 files changed, 11 insertions(+), 45 deletions(-) diff --git a/flixopt/components.py b/flixopt/components.py index 1377d1f83..81570f9f3 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -535,6 +535,7 @@ def do_modeling(self): InvestmentModel( model=self._model, label_of_element=self.label_of_element, + label_of_model=self.label_of_element, parameters=self.element.capacity_in_flow_hours, ), short_name='investment', diff --git a/flixopt/elements.py b/flixopt/elements.py index 02d0bf115..663434ad8 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -13,7 +13,7 @@ from .config import CONFIG from .core import PlausibilityError, Scalar, TemporalData, TemporalDataUser from .effects import TemporalEffectsUser -from .features import InvestmentModel, OnOffModel, PreventSimultaneousUsageModel, ModelingPrimitives +from .features import InvestmentModel, OnOffModel, ModelingPrimitives from .interface import InvestParameters, OnOffParameters from .structure import Element, ElementModel, FlowSystemModel, register_class_for_io from .modeling import BoundingPatterns, ModelingUtilitiesAbstract @@ -630,8 +630,11 @@ def do_modeling(self): if self.element.prevent_simultaneous_flows: # Simultanious Useage --> Only One FLow is On at a time, but needs a Binary for every flow on_variables = [flow.model.on_off.on for flow in self.element.prevent_simultaneous_flows] - simultaneous_use = self.register_sub_model(PreventSimultaneousUsageModel(self._model, on_variables, self.label_full), short_name='prevent_simultaneous_use') - simultaneous_use.do_modeling() + ModelingPrimitives.mutual_exclusivity_constraint( + self, + binary_variables=[flow.model.on_off.on for flow in self.element.prevent_simultaneous_flows], + short_name='prevent_simultaneous_use', + ) def results_structure(self): return { diff --git a/flixopt/features.py b/flixopt/features.py index 475c0e553..a31550e63 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -560,41 +560,3 @@ def add_share( self._eq_total.lhs -= self.shares[name] else: self._eq_total_per_timestep.lhs -= self.shares[name] - - -class PreventSimultaneousUsageModel(Model): - """ - Prevents multiple Multiple Binary variables from being 1 at the same time - - Only 'classic type is modeled for now (# "classic" -> alle Flows brauchen Binärvariable:) - In 'new', the binary Variables need to be forced beforehand, which is not that straight forward... --> TODO maybe - - - # "new": - # eq: flow_1.on(t) + flow_2.on(t) + .. + flow_i.val(t)/flow_i.max <= 1 (1 Flow ohne Binärvariable!) - - # Anmerkung: Patrick Schönfeld (oemof, custom/link.py) macht bei 2 Flows ohne Binärvariable dies: - # 1) bin + flow1/flow1_max <= 1 - # 2) bin - flow2/flow2_max >= 0 - # 3) geht nur, wenn alle flow.min >= 0 - # --> könnte man auch umsetzen (statt force_on_variable() für die Flows, aber sollte aufs selbe wie "new" kommen) - """ - - def __init__( - self, - model: FlowSystemModel, - variables: List[linopy.Variable], - label_of_element: str, - label: str = 'PreventSimultaneousUsage', - ): - super().__init__(model, label_of_element, label) - self._simultanious_use_variables = variables - assert len(self._simultanious_use_variables) >= 2, ( - f'Model {self.__class__.__name__} must get at least two variables' - ) - for variable in self._simultanious_use_variables: # classic - assert variable.attrs['binary'], f'Variable {variable} must be binary for use in {self.__class__.__name__}' - - def do_modeling(self): - # eq: sum(flow_i.on(t)) <= 1.1 (1 wird etwas größer gewählt wg. Binärvariablengenauigkeit) - self.add_constraints(sum(self._simultanious_use_variables) <= 1.1, short_name='prevent_simultaneous_use') diff --git a/flixopt/modeling.py b/flixopt/modeling.py index c3839749c..d1b739487 100644 --- a/flixopt/modeling.py +++ b/flixopt/modeling.py @@ -387,7 +387,8 @@ def consecutive_duration_tracking( @staticmethod def mutual_exclusivity_constraint( - model: FlowSystemModel, name: str, binary_variables: List[linopy.Variable], tolerance: float = 1 + model: Model, binary_variables: List[linopy.Variable], tolerance: float = 1, + short_name: str = 'mutual_exclusivity', ) -> linopy.Constraint: """ Creates mutual exclusivity constraint for binary variables. @@ -401,6 +402,7 @@ def mutual_exclusivity_constraint( Args: binary_variables: List of binary variables that should be mutually exclusive tolerance: Upper bound + short_name: Short name of the constraint Returns: variables: {} (no new variables created) @@ -419,9 +421,7 @@ def mutual_exclusivity_constraint( ) # Create mutual exclusivity constraint - mutual_exclusivity = model.add_constraints( - sum(binary_variables) <= tolerance, name=f'{name}|mutual_exclusivity' - ) + mutual_exclusivity = model.add_constraints(sum(binary_variables) <= tolerance, short_name=short_name) return mutual_exclusivity From 9702303c534f31c27244a48126a9a8b9bd72f2e1 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 21 Jul 2025 21:01:25 +0200 Subject: [PATCH 227/448] Fix Aggregation and SegmentedCalculation --- flixopt/aggregation.py | 51 ++++++++++++++---------------------------- flixopt/calculation.py | 2 +- 2 files changed, 18 insertions(+), 35 deletions(-) diff --git a/flixopt/aggregation.py b/flixopt/aggregation.py index 47ac1336d..26fb921c9 100644 --- a/flixopt/aggregation.py +++ b/flixopt/aggregation.py @@ -301,7 +301,7 @@ def __init__( """ Modeling-Element for "index-equating"-equations """ - super().__init__(model, label_of_element='Aggregation', label_full='Aggregation') + super().__init__(model, label_of_element='Aggregation', label_of_model='Aggregation') self.flow_system = flow_system self.aggregation_parameters = aggregation_parameters self.aggregation_data = aggregation_data @@ -343,12 +343,9 @@ def _equate_indices(self, variable: linopy.Variable, indices: Tuple[np.ndarray, # Gleichung: # eq1: x(p1,t) - x(p3,t) = 0 # wobei p1 und p3 im gleichen Cluster sind und t = 0..N_p - con = self.add( - self._model.add_constraints( - variable.isel(time=indices[0]) - variable.isel(time=indices[1]) == 0, - name=f'{self.label_full}|equate_indices|{variable.name}', - ), - f'equate_indices|{variable.name}', + con = self.add_constraints( + variable.isel(time=indices[0]) - variable.isel(time=indices[1]) == 0, + short_name=f'equate_indices|{variable.name}', ) # Korrektur: (bisher nur für Binärvariablen:) @@ -356,22 +353,16 @@ def _equate_indices(self, variable: linopy.Variable, indices: Tuple[np.ndarray, variable.name in self._model.variables.binaries and self.aggregation_parameters.percentage_of_period_freedom > 0 ): - var_k1 = self.add( - self._model.add_variables( - binary=True, - coords={'time': variable.isel(time=indices[0]).indexes['time']}, - name=f'{self.label_full}|correction1|{variable.name}', - ), - f'correction1|{variable.name}', + var_k1 = self.add_variables( + binary=True, + coords={'time': variable.isel(time=indices[0]).indexes['time']}, + short_name=f'correction1|{variable.name}', ) - var_k0 = self.add( - self._model.add_variables( - binary=True, - coords={'time': variable.isel(time=indices[0]).indexes['time']}, - name=f'{self.label_full}|correction0|{variable.name}', - ), - f'correction0|{variable.name}', + var_k0 = self.add_variables( + binary=True, + coords={'time': variable.isel(time=indices[0]).indexes['time']}, + short_name=f'correction0|{variable.name}', ) # equation extends ... @@ -384,20 +375,12 @@ def _equate_indices(self, variable: linopy.Variable, indices: Tuple[np.ndarray, # interlock var_k1 and var_K2: # eq: var_k0(t)+var_k1(t) <= 1.1 - self.add( - self._model.add_constraints( - var_k0 + var_k1 <= 1.1, name=f'{self.label_full}|lock_k0_and_k1|{variable.name}' - ), - f'lock_k0_and_k1|{variable.name}', - ) + self.add_constraints(var_k0 + var_k1 <= 1.1, short_name=f'lock_k0_and_k1|{variable.name}') # Begrenzung der Korrektur-Anzahl: # eq: sum(K) <= n_Corr_max - self.add( - self._model.add_constraints( - sum(var_k0) + sum(var_k1) - <= round(self.aggregation_parameters.percentage_of_period_freedom / 100 * length), - name=f'{self.label_full}|limit_corrections|{variable.name}', - ), - f'limit_corrections|{variable.name}', + self.add_constraints( + sum(var_k0) + sum(var_k1) + <= round(self.aggregation_parameters.percentage_of_period_freedom / 100 * length), + short_name=f'limit_corrections|{variable.name}', ) diff --git a/flixopt/calculation.py b/flixopt/calculation.py index 438fbeea5..61747ffe7 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -488,7 +488,7 @@ def do_modeling_and_solve( invest_elements = [ model.label_full for component in calculation.flow_system.components.values() - for model in component.model.all_sub_models + for model in component.model.sub_models if isinstance(model, InvestmentModel) ] if invest_elements: From 91bd4610df55878615a8993a55680c54bf2909a5 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 21 Jul 2025 21:01:50 +0200 Subject: [PATCH 228/448] Update tests --- tests/test_component.py | 2 +- tests/test_storage.py | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/test_component.py b/tests/test_component.py index 25e496694..3bf1699ec 100644 --- a/tests/test_component.py +++ b/tests/test_component.py @@ -205,7 +205,7 @@ def test_previous_states_with_multiple_flows(self, basic_flow_system_linopy): fx.Flow('In1', 'Fernwärme', relative_minimum=np.ones(10) * 0.1, size=100, previous_flow_rate=np.array([0, 0, 1e-6, 1e-5, 1e-4, 3,4])), ] outputs = [ - fx.Flow('Out1', 'Gas', relative_minimum=np.ones(10) * 0.2, size=200, previous_flow_rate=xr.DataArray([3,4,5], dims='time')), + fx.Flow('Out1', 'Gas', relative_minimum=np.ones(10) * 0.2, size=200, previous_flow_rate=[3,4,5]), fx.Flow('Out2', 'Gas', relative_minimum=np.ones(10) * 0.3, relative_maximum = ub_out2, size=300, previous_flow_rate=20), ] diff --git a/tests/test_storage.py b/tests/test_storage.py index 1b9b3b875..3a6b2a06c 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -291,7 +291,7 @@ def test_storage_with_investment(self, basic_flow_system_linopy): assert var_name in model.variables, f"Missing investment variable: {var_name}" # Check investment constraints exist - for con_name in {'InvestStorage|is_invested_ub', 'InvestStorage|is_invested_lb'}: + for con_name in {'InvestStorage|size|ub', 'InvestStorage|size|lb'}: assert con_name in model.constraints, f"Missing investment constraint: {con_name}" # Check variable properties @@ -303,9 +303,9 @@ def test_storage_with_investment(self, basic_flow_system_linopy): model['InvestStorage|is_invested'], model.add_variables(binary=True) ) - assert_conequal(model.constraints['InvestStorage|is_invested_ub'], + assert_conequal(model.constraints['InvestStorage|size|ub'], model.variables['InvestStorage|size'] <= model.variables['InvestStorage|is_invested'] * 100) - assert_conequal(model.constraints['InvestStorage|is_invested_lb'], + assert_conequal(model.constraints['InvestStorage|size|lb'], model.variables['InvestStorage|size'] >= model.variables['InvestStorage|is_invested'] * 20) def test_storage_with_final_state_constraints(self, basic_flow_system_linopy): @@ -417,17 +417,17 @@ def test_simultaneous_charge_discharge(self, basic_flow_system_linopy, prevent_s assert var_name in model.variables, f'Missing binary variable: {var_name}' # Check for constraints that enforce either charging or discharging - constraint_name = 'SimultaneousStorage|PreventSimultaneousUsage|prevent_simultaneous_use' + constraint_name = 'SimultaneousStorage|prevent_simultaneous_use' assert constraint_name in model.constraints, 'Missing constraint to prevent simultaneous operation' - assert_conequal(model.constraints['SimultaneousStorage|PreventSimultaneousUsage|prevent_simultaneous_use'], - model.variables['SimultaneousStorage(Q_th_in)|on'] + model.variables['SimultaneousStorage(Q_th_out)|on'] <= 1.1) + assert_conequal(model.constraints['SimultaneousStorage|prevent_simultaneous_use'], + model.variables['SimultaneousStorage(Q_th_in)|on'] + model.variables['SimultaneousStorage(Q_th_out)|on'] <= 1) @pytest.mark.parametrize( 'optional,minimum_size,expected_vars,expected_constraints', [ - (True, None, {'InvestStorage|is_invested'}, {'InvestStorage|is_invested_lb'}), - (True, 20, {'InvestStorage|is_invested'}, {'InvestStorage|is_invested_lb'}), + (True, None, {'InvestStorage|is_invested'}, {'InvestStorage|size|lb'}), + (True, 20, {'InvestStorage|is_invested'}, {'InvestStorage|size|lb'}), (False, None, set(), set()), (False, 20, set(), set()), ], From 94314c3866a980d824cf3caa87a616b443c156c2 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 21 Jul 2025 21:03:31 +0200 Subject: [PATCH 229/448] Loosen precision in tests --- tests/conftest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 5d98cdcb5..902e01c12 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -33,7 +33,7 @@ def solver_fixture(request): # Custom assertion function def assert_almost_equal_numeric( - actual, desired, err_msg, relative_error_range_in_percent=0.011, absolute_tolerance=1e-9 + actual, desired, err_msg, relative_error_range_in_percent=0.011, absolute_tolerance=1e-7 ): """ Custom assertion function for comparing numeric values with relative and absolute tolerances @@ -122,6 +122,7 @@ def simple_flow_system() -> fx.FlowSystem: return flow_system + @pytest.fixture def simple_flow_system_scenarios() -> fx.FlowSystem: """ From 50cc2cbbb4e324ac145ddba5a0d7f81282268de8 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 21 Jul 2025 21:16:20 +0200 Subject: [PATCH 230/448] Update test_on_hours_computation.py and some types --- flixopt/elements.py | 2 +- flixopt/modeling.py | 2 +- tests/test_on_hours_computation.py | 138 ++++++++++++++--------------- 3 files changed, 68 insertions(+), 74 deletions(-) diff --git a/flixopt/elements.py b/flixopt/elements.py index 663434ad8..62e723d98 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -524,7 +524,7 @@ def investment(self) -> Optional[InvestmentModel]: return self.sub_models_direct['investment'] @property - def previous_states(self) -> Optional[xr.DataArray]: + def previous_states(self) -> Optional[TemporalData]: """Previous states of the flow rate""" #TODO: This would be nicer to handle in the Flow itself, and allow DataArrays as well. previous_flow_rate = self.element.previous_flow_rate diff --git a/flixopt/modeling.py b/flixopt/modeling.py index d1b739487..b4ce4d5db 100644 --- a/flixopt/modeling.py +++ b/flixopt/modeling.py @@ -103,7 +103,7 @@ class ModelingUtilities: @staticmethod def compute_consecutive_hours_in_state( - binary_values: Union[xr.DataArray, np.ndarray, int], + binary_values: TemporalData, hours_per_timestep: Union[int, float], epsilon: float = None, ) -> float: diff --git a/tests/test_on_hours_computation.py b/tests/test_on_hours_computation.py index c8fa113aa..578fd7792 100644 --- a/tests/test_on_hours_computation.py +++ b/tests/test_on_hours_computation.py @@ -1,43 +1,43 @@ import numpy as np import pytest +import xarray as xr from flixopt.modeling import ModelingUtilities class TestComputeConsecutiveDuration: - """Tests for the compute_consecutive_duration static method.""" + """Tests for the compute_consecutive_hours_in_state static method.""" - @pytest.mark.parametrize("binary_values, hours_per_timestep, expected", [ - # Case 1: Both scalar inputs - (1, 5, 5), - (0, 3, 0), - - # Case 2: Scalar binary, array hours - (1, np.array([1, 2, 3]), 3), - (0, np.array([2, 4, 6]), 0), - - # Case 3: Array binary, scalar hours - (np.array([0, 0, 1, 1, 1, 0]), 2, 0), - (np.array([0, 1, 1, 0, 1, 1]), 1, 2), - (np.array([1, 1, 1]), 2, 6), - - # Case 4: Both array inputs - (np.array([0, 1, 1, 0, 1, 1]), np.array([1, 2, 3, 4, 5, 6]), 11), # 5+6 - (np.array([1, 0, 0, 1, 1, 1]), np.array([2, 2, 2, 3, 4, 5]), 12), # 3+4+5 - - # Case 5: Edge cases - (np.array([1]), np.array([4]), 4), - (np.array([0]), np.array([3]), 0), - ]) + @pytest.mark.parametrize( + 'binary_values, hours_per_timestep, expected', + [ + # Case 1: Single timestep DataArrays + (xr.DataArray([1], dims=['time']), 5, 5), + (xr.DataArray([0], dims=['time']), 3, 0), + # Case 2: Array binary, scalar hours + (xr.DataArray([0, 0, 1, 1, 1, 0], dims=['time']), 2, 0), + (xr.DataArray([0, 1, 1, 0, 1, 1], dims=['time']), 1, 2), + (xr.DataArray([1, 1, 1], dims=['time']), 2, 6), + # Case 3: Edge cases + (xr.DataArray([1], dims=['time']), 4, 4), + (xr.DataArray([0], dims=['time']), 3, 0), + # Case 4: More complex patterns + (xr.DataArray([1, 0, 0, 1, 1, 1], dims=['time']), 2, 6), # 3 consecutive at end * 2 hours + (xr.DataArray([0, 1, 1, 1, 0, 0], dims=['time']), 1, 0), # ends with 0 + ], + ) def test_compute_duration(self, binary_values, hours_per_timestep, expected): - """Test compute_consecutive_duration with various inputs.""" + """Test compute_consecutive_hours_in_state with various inputs.""" result = ModelingUtilities.compute_consecutive_hours_in_state(binary_values, hours_per_timestep) assert np.isclose(result, expected) - @pytest.mark.parametrize("binary_values, hours_per_timestep", [ - # Case: Incompatible array lengths - (np.array([1, 1, 1, 1, 1]), np.array([1, 2])), - ]) + @pytest.mark.parametrize( + 'binary_values, hours_per_timestep', + [ + # Case: hours_per_timestep must be scalar + (xr.DataArray([1, 1, 1, 1, 1], dims=['time']), np.array([1, 2])), + ], + ) def test_compute_duration_raises_error(self, binary_values, hours_per_timestep): """Test error conditions.""" with pytest.raises(TypeError): @@ -45,61 +45,55 @@ def test_compute_duration_raises_error(self, binary_values, hours_per_timestep): class TestComputePreviousOnStates: - """Tests for the compute_previous_on_states static method.""" + """Tests for the compute_previous_states static method.""" @pytest.mark.parametrize( 'previous_values, expected', [ - # Case 1: Empty list - ([], np.array([0])), - - # Case 2: All None values - ([None, None], np.array([0])), - - # Case 3: Single value arrays - ([np.array([0])], np.array([0])), - ([np.array([1])], np.array([1])), - ([np.array([0.001])], np.array([1])), # Using default epsilon - ([np.array([1e-4])], np.array([1])), - ([np.array([1e-8])], np.array([0])), - - # Case 4: Multiple 1D arrays - ([np.array([0, 5, 0]), np.array([0, 0, 1])], np.array([0, 1, 1])), - ([np.array([0.1, 0, 0.3]), None, np.array([0, 0, 0])], np.array([1, 0, 1])), - ([np.array([0, 0, 0]), np.array([0, 1, 0])], np.array([0, 1, 0])), - ([np.array([0.1, 0, 0]), np.array([0, 0, 0.2])], np.array([1, 0, 1])), - - # Case 6: Mix of None, 1D and 2D arrays - ([None, np.array([0, 0, 0]), np.array([0, 1, 0]), np.array([0, 0, 0])], np.array([0, 1, 0])), - ([np.array([0, 0, 0]), None, np.array([0, 0, 0]), np.array([0, 0, 0])], np.array([0, 0, 0])), + # Case 1: Single value DataArrays + (xr.DataArray([0], dims=['time']), xr.DataArray([0], dims=['time'])), + (xr.DataArray([1], dims=['time']), xr.DataArray([1], dims=['time'])), + (xr.DataArray([0.001], dims=['time']), xr.DataArray([1], dims=['time'])), # Using default epsilon + (xr.DataArray([1e-4], dims=['time']), xr.DataArray([1], dims=['time'])), + (xr.DataArray([1e-8], dims=['time']), xr.DataArray([0], dims=['time'])), + # Case 1: Multiple timestep DataArrays + (xr.DataArray([0, 5, 0], dims=['time']), xr.DataArray([0, 1, 0], dims=['time'])), + (xr.DataArray([0.1, 0, 0.3], dims=['time']), xr.DataArray([1, 0, 1], dims=['time'])), + (xr.DataArray([0, 0, 0], dims=['time']), xr.DataArray([0, 0, 0], dims=['time'])), + (xr.DataArray([0.1, 0, 0.2], dims=['time']), xr.DataArray([1, 0, 1], dims=['time'])), ], ) def test_compute_previous_on_states(self, previous_values, expected): - """Test compute_previous_on_states with various inputs.""" + """Test compute_previous_states with various inputs.""" result = ModelingUtilities.compute_previous_states(previous_values) - np.testing.assert_array_equal(result, expected) - - @pytest.mark.parametrize("previous_values, epsilon, expected", [ - # Testing with different epsilon values - ([np.array([1e-6, 1e-4, 1e-2])], 1e-3, np.array([0, 0, 1])), - ([np.array([1e-6, 1e-4, 1e-2])], 1e-5, np.array([0, 1, 1])), - ([np.array([1e-6, 1e-4, 1e-2])], 1e-1, np.array([0, 0, 0])), + xr.testing.assert_equal(result, expected) - # Mixed case with custom epsilon - ([np.array([0.05, 0.005, 0.0005])], 0.01, np.array([1, 0, 0])), - ]) + @pytest.mark.parametrize( + 'previous_values, epsilon, expected', + [ + # Testing with different epsilon values + (xr.DataArray([1e-6, 1e-4, 1e-2], dims=['time']), 1e-3, xr.DataArray([0, 0, 1], dims=['time'])), + (xr.DataArray([1e-6, 1e-4, 1e-2], dims=['time']), 1e-5, xr.DataArray([0, 1, 1], dims=['time'])), + (xr.DataArray([1e-6, 1e-4, 1e-2], dims=['time']), 1e-1, xr.DataArray([0, 0, 0], dims=['time'])), + # Mixed case with custom epsilon + (xr.DataArray([0.05, 0.005, 0.0005], dims=['time']), 0.01, xr.DataArray([1, 0, 0], dims=['time'])), + ], + ) def test_compute_previous_on_states_with_epsilon(self, previous_values, epsilon, expected): - """Test compute_previous_on_states with custom epsilon values.""" - result = StateModel.compute_previous_states(previous_values, epsilon) - np.testing.assert_array_equal(result, expected) + """Test compute_previous_states with custom epsilon values.""" + result = ModelingUtilities.compute_previous_states(previous_values, epsilon) + xr.testing.assert_equal(result, expected) - @pytest.mark.parametrize("previous_values, expected_shape", [ - # Check that output shapes match expected dimensions - ([np.array([0, 1, 0, 1])], (4,)), - ([np.array([0, 1]), np.array([1, 0]), np.array([0, 0])], (2,)), - ([np.array([0, 1]), np.array([1, 0])], (2,)), - ]) + @pytest.mark.parametrize( + 'previous_values, expected_shape', + [ + # Check that output shapes match expected dimensions + (xr.DataArray([0, 1, 0, 1], dims=['time']), (4,)), + (xr.DataArray([0, 1], dims=['time']), (2,)), + (xr.DataArray([1, 0], dims=['time']), (2,)), + ], + ) def test_output_shapes(self, previous_values, expected_shape): """Test that output array has the correct shape.""" - result = StateModel.compute_previous_states(previous_values) + result = ModelingUtilities.compute_previous_states(previous_values) assert result.shape == expected_shape From e52f8002e96071483e4dbeb52bca99f7538c0205 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 21 Jul 2025 21:34:20 +0200 Subject: [PATCH 231/448] Rename class Model to Submodel --- flixopt/aggregation.py | 4 ++-- flixopt/calculation.py | 2 +- flixopt/components.py | 2 +- flixopt/effects.py | 4 ++-- flixopt/elements.py | 2 +- flixopt/features.py | 12 ++++++------ flixopt/modeling.py | 8 ++++---- flixopt/structure.py | 14 +++++++------- 8 files changed, 24 insertions(+), 24 deletions(-) diff --git a/flixopt/aggregation.py b/flixopt/aggregation.py index 26fb921c9..eb44ad707 100644 --- a/flixopt/aggregation.py +++ b/flixopt/aggregation.py @@ -27,7 +27,7 @@ from .flow_system import FlowSystem from .structure import ( Element, - Model, + Submodel, FlowSystemModel, ) @@ -285,7 +285,7 @@ def use_low_peaks(self): return self.time_series_for_low_peaks is not None -class AggregationModel(Model): +class AggregationModel(Submodel): """The AggregationModel holds equations and variables related to the Aggregation of a FLowSystem. It creates Equations that equates indices of variables, and introduces penalties related to binary variables, that escape the equation to their related binaries in other periods""" diff --git a/flixopt/calculation.py b/flixopt/calculation.py index 61747ffe7..141a8ead5 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -300,7 +300,7 @@ def do_modeling(self) -> 'AggregatedCalculation': # Model the System self.model = self.flow_system.create_model() self.model.do_modeling() - # Add Aggregation Model after modeling the rest + # Add Aggregation Submodel after modeling the rest self.aggregation = AggregationModel( self.model, self.aggregation_parameters, self.flow_system, self.aggregation, self.components_to_clusterize ) diff --git a/flixopt/components.py b/flixopt/components.py index 81570f9f3..42f1cfdd5 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -487,7 +487,7 @@ def do_modeling(self): class StorageModel(ComponentModel): - """Model of Storage""" + """Submodel of Storage""" def __init__(self, model: FlowSystemModel, element: Storage): super().__init__(model, element) diff --git a/flixopt/effects.py b/flixopt/effects.py index 13ee524e5..2b1b2ed6e 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -15,7 +15,7 @@ from .core import Scalar, TemporalData, TemporalDataUser from .features import ShareAllocationModel -from .structure import Element, ElementModel, Interface, Model, FlowSystemModel, register_class_for_io +from .structure import Element, ElementModel, Interface, Submodel, FlowSystemModel, register_class_for_io if TYPE_CHECKING: from .flow_system import FlowSystem @@ -375,7 +375,7 @@ def calculate_effect_share_factors(self) -> Tuple[ return shares_operation, shares_invest -class EffectCollectionModel(Model): +class EffectCollectionModel(Submodel): """ Handling all Effects """ diff --git a/flixopt/elements.py b/flixopt/elements.py index 62e723d98..c53f7c84f 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -30,7 +30,7 @@ class Component(Element): A Component contains incoming and outgoing [`Flows`][flixopt.elements.Flow]. It defines how these Flows interact with each other. The On or Off state of the Component is defined by all its Flows. Its on, if any of its FLows is On. It's mathematically advisable to define the On/Off state in a FLow rather than a Component if possible, - as this introduces less binary variables to the Model + as this introduces less binary variables to the Submodel Constraints to the On/Off state are defined by the [`on_off_parameters`][flixopt.interface.OnOffParameters]. """ diff --git a/flixopt/features.py b/flixopt/features.py index a31550e63..99928e410 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -12,7 +12,7 @@ from .config import CONFIG from .core import NonTemporalData, Scalar, TemporalData, FlowSystemDimensions from .interface import InvestParameters, OnOffParameters, Piecewise, PiecewiseEffects -from .structure import Model, FlowSystemModel, BaseFeatureModel +from .structure import Submodel, FlowSystemModel, BaseFeatureModel from .modeling import ModelingUtilities, ModelingPrimitives, BoundingPatterns logger = logging.getLogger('flixopt') @@ -228,7 +228,7 @@ def add_effects(self): target='operation', ) - # Properties access variables from Model's tracking system + # Properties access variables from Submodel's tracking system @property def total_on_hours(self) -> Optional[linopy.Variable]: @@ -282,7 +282,7 @@ def _get_previous_off_duration(self): return ModelingUtilities.compute_consecutive_hours_in_state(self._previous_states * -1 + 1, hours_per_step) -class PieceModel(Model): +class PieceModel(Submodel): """Class for modeling a linear piece of one or more variables in parallel""" def __init__( @@ -323,7 +323,7 @@ def do_modeling(self): self.add_constraints(self.inside_piece == self.lambda0 + self.lambda1, short_name='inside_piece') -class PiecewiseModel(Model): +class PiecewiseModel(Submodel): def __init__( self, model: FlowSystemModel, @@ -404,7 +404,7 @@ def do_modeling(self): ) -class PiecewiseEffectsModel(Model): +class PiecewiseEffectsModel(Submodel): def __init__( self, model: FlowSystemModel, @@ -461,7 +461,7 @@ def do_modeling(self): ) -class ShareAllocationModel(Model): +class ShareAllocationModel(Submodel): def __init__( self, model: FlowSystemModel, diff --git a/flixopt/modeling.py b/flixopt/modeling.py index b4ce4d5db..262b0d17d 100644 --- a/flixopt/modeling.py +++ b/flixopt/modeling.py @@ -7,7 +7,7 @@ from .config import CONFIG from .core import NonTemporalData, Scalar, TemporalData, FlowSystemDimensions -from .structure import Model, FlowSystemModel, BaseFeatureModel +from .structure import Submodel, FlowSystemModel, BaseFeatureModel logger = logging.getLogger('flixopt') @@ -181,7 +181,7 @@ class ModelingPrimitives: @staticmethod def expression_tracking_variable( - model: Model, + model: Submodel, tracked_expression, name: str = None, short_name: str = None, @@ -219,7 +219,7 @@ def expression_tracking_variable( @staticmethod def state_transition_variables( - model: Union[FlowSystemModel, Model], + model: Submodel, state_variable: linopy.Variable, switch_on: linopy.Variable, switch_off: linopy.Variable, @@ -387,7 +387,7 @@ def consecutive_duration_tracking( @staticmethod def mutual_exclusivity_constraint( - model: Model, binary_variables: List[linopy.Variable], tolerance: float = 1, + model: Submodel, binary_variables: List[linopy.Variable], tolerance: float = 1, short_name: str = 'mutual_exclusivity', ) -> linopy.Constraint: """ diff --git a/flixopt/structure.py b/flixopt/structure.py index 34de27f35..e6ed849b3 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -696,7 +696,7 @@ def _valid_label(label: str) -> str: return label -class Model: +class Submodel: """Stores Variables and Constraints.""" def __init__( @@ -714,7 +714,7 @@ def __init__( self._variables: Dict[str, linopy.Variable] = {} # Mapping from short name to variable self._constraints: Dict[str, linopy.Constraint] = {} # Mapping from short name to constraint - self._sub_models: Dict[str, 'Model'] = {} + self._sub_models: Dict[str, 'Submodel'] = {} logger.debug(f'Created {self.__class__.__name__} "{self.label_full}"') @@ -760,7 +760,7 @@ def register_constraint(self, constraint: linopy.Constraint, short_name: str = N self._constraints[short_name] = constraint return constraint - def register_sub_model(self, sub_model: 'Model', short_name: str) -> 'Model': + def register_sub_model(self, sub_model: 'Submodel', short_name: str) -> 'Submodel': """Register a sub-model with the model""" if short_name is None: short_name = sub_model.__class__.__name__ @@ -831,12 +831,12 @@ def constraints_direct(self) -> linopy.Constraints: return self._model.constraints[[con.name for con in self._constraints.values()]] @property - def sub_models_direct(self) -> Dict[str, 'Model']: + def sub_models_direct(self) -> Dict[str, 'Submodel']: """All sub-models of the model, excluding those of sub-models""" return self._sub_models @property - def sub_models(self) -> List['Model']: + def sub_models(self) -> List['Submodel']: """All sub-models of the model""" direct_submodels = list(self._sub_models.values()) @@ -896,7 +896,7 @@ def hours_per_step(self): return self._model.hours_per_step -class BaseFeatureModel(Model): +class BaseFeatureModel(Submodel): """Minimal base class for feature models that use factory patterns""" def __init__(self, model: FlowSystemModel, label_of_element: str, parameters, label_of_model: Optional[str] = None): @@ -925,7 +925,7 @@ def add_effects(self): pass # Default: no effects -class ElementModel(Model): +class ElementModel(Submodel): """Stores the mathematical Variables and Constraints for Elements""" def __init__(self, model: FlowSystemModel, element: Element): From 928125640bc8295ecbdfb8f520a974692a7421ab Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 21 Jul 2025 21:36:23 +0200 Subject: [PATCH 232/448] rename sub_model to submodel everywhere --- flixopt/structure.py | 20 ++++++++++---------- tests/test_linear_converter.py | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/flixopt/structure.py b/flixopt/structure.py index e6ed849b3..7da8ade78 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -760,14 +760,14 @@ def register_constraint(self, constraint: linopy.Constraint, short_name: str = N self._constraints[short_name] = constraint return constraint - def register_sub_model(self, sub_model: 'Submodel', short_name: str) -> 'Submodel': + def register_sub_model(self, submodel: 'Submodel', short_name: str) -> 'Submodel': """Register a sub-model with the model""" if short_name is None: - short_name = sub_model.__class__.__name__ + short_name = submodel.__class__.__name__ if short_name in self._sub_models: raise ValueError(f'Short name "{short_name}" already assigned to model') - self._sub_models[short_name] = sub_model - return sub_model + self._sub_models[short_name] = submodel + return submodel def __getitem__(self, key: str) -> linopy.Variable: """Get a variable by its short name""" @@ -852,8 +852,8 @@ def constraints(self) -> linopy.Constraints: """All constraints of the model, including those of sub-models""" names = list(self.constraints_direct) + [ constraint_name - for sub_model in self.sub_models - for constraint_name in sub_model.constraints_direct + for submodel in self.sub_models + for constraint_name in submodel.constraints_direct ] return self._model.constraints[names] @@ -863,8 +863,8 @@ def variables(self) -> linopy.Variables: """All variables of the model, including those of sub-models""" names = list(self.variables_direct) + [ variable_name - for sub_model in self.sub_models - for variable_name in sub_model.variables_direct + for submodel in self.sub_models + for variable_name in submodel.variables_direct ] return self._model.variables[names] @@ -881,8 +881,8 @@ def __repr__(self) -> str: sub_models_string = ' \n' else: sub_models_string = '' - for sub_model_name, sub_model in self.sub_models_direct.items(): - sub_models_string += f'\n * {sub_model_name} [{sub_model.__class__.__name__}]' + for submodel_name, submodel in self.sub_models_direct.items(): + sub_models_string += f'\n * {submodel_name} [{submodel.__class__.__name__}]' return ( f"{model_string}\n{'=' * len(model_string)}\n\n" diff --git a/tests/test_linear_converter.py b/tests/test_linear_converter.py index 11b5b5673..e15b11c1b 100644 --- a/tests/test_linear_converter.py +++ b/tests/test_linear_converter.py @@ -360,7 +360,7 @@ def test_piecewise_conversion(self, basic_flow_system_linopy): # Create model with the piecewise conversion model = create_linopy_model(flow_system) - # Verify that PiecewiseModel was created and added as a sub_model + # Verify that PiecewiseModel was created and added as a submodel assert converter.model.piecewise_conversion is not None # Get the PiecewiseModel instance @@ -472,7 +472,7 @@ def test_piecewise_conversion_with_onoff(self, basic_flow_system_linopy): # Create model with the piecewise conversion model = create_linopy_model(flow_system) - # Verify that PiecewiseModel was created and added as a sub_model + # Verify that PiecewiseModel was created and added as a submodel assert converter.model.piecewise_conversion is not None # Get the PiecewiseModel instance From 9001c6ab129b9f8c98e7f738c603205679cfcd45 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 21 Jul 2025 21:37:34 +0200 Subject: [PATCH 233/448] rename self.model to self.submodel everywhere --- flixopt/calculation.py | 4 ++-- flixopt/components.py | 6 +++--- flixopt/effects.py | 4 ++-- flixopt/elements.py | 6 +++--- flixopt/flow_system.py | 2 +- flixopt/results.py | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/flixopt/calculation.py b/flixopt/calculation.py index 141a8ead5..53c02beca 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -180,7 +180,7 @@ def do_modeling(self) -> 'FullCalculation': t_start = timeit.default_timer() self.flow_system.connect_and_transform() - self.model = self.flow_system.create_model() + self.submodel = self.flow_system.create_model() self.model.do_modeling() self.durations['modeling'] = round(timeit.default_timer() - t_start, 2) @@ -298,7 +298,7 @@ def do_modeling(self) -> 'AggregatedCalculation': self._perform_aggregation() # Model the System - self.model = self.flow_system.create_model() + self.submodel = self.flow_system.create_model() self.model.do_modeling() # Add Aggregation Submodel after modeling the rest self.aggregation = AggregationModel( diff --git a/flixopt/components.py b/flixopt/components.py index 42f1cfdd5..fe776e02d 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -61,7 +61,7 @@ def __init__( def create_model(self, model: FlowSystemModel) -> 'LinearConverterModel': self._plausibility_checks() - self.model = LinearConverterModel(model, self) + self.submodel = LinearConverterModel(model, self) return self.model def _plausibility_checks(self) -> None: @@ -203,7 +203,7 @@ def __init__( def create_model(self, model: FlowSystemModel) -> 'StorageModel': self._plausibility_checks() - self.model = StorageModel(model, self) + self.submodel = StorageModel(model, self) return self.model def transform_data(self, flow_system: 'FlowSystem') -> None: @@ -380,7 +380,7 @@ def _plausibility_checks(self): def create_model(self, model) -> 'TransmissionModel': self._plausibility_checks() - self.model = TransmissionModel(model, self) + self.submodel = TransmissionModel(model, self) return self.model def transform_data(self, flow_system: 'FlowSystem') -> None: diff --git a/flixopt/effects.py b/flixopt/effects.py index 2b1b2ed6e..9f8e2506f 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -129,7 +129,7 @@ def transform_data(self, flow_system: 'FlowSystem'): def create_model(self, model: FlowSystemModel) -> 'EffectModel': self._plausibility_checks() - self.model = EffectModel(model, self) + self.submodel = EffectModel(model, self) return self.model def _plausibility_checks(self) -> None: @@ -216,7 +216,7 @@ def __init__(self, *effects: List[Effect]): def create_model(self, model: FlowSystemModel) -> 'EffectCollectionModel': self._plausibility_checks() - self.model = EffectCollectionModel(model, self) + self.submodel = EffectCollectionModel(model, self) return self.model def add_effects(self, *effects: Effect) -> None: diff --git a/flixopt/elements.py b/flixopt/elements.py index c53f7c84f..4dbf67aea 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -67,7 +67,7 @@ def __init__( def create_model(self, model: FlowSystemModel) -> 'ComponentModel': self._plausibility_checks() - self.model = ComponentModel(model, self) + self.submodel = ComponentModel(model, self) return self.model def transform_data(self, flow_system: 'FlowSystem') -> None: @@ -112,7 +112,7 @@ def __init__( def create_model(self, model: FlowSystemModel) -> 'BusModel': self._plausibility_checks() - self.model = BusModel(model, self) + self.submodel = BusModel(model, self) return self.model def transform_data(self, flow_system: 'FlowSystem'): @@ -229,7 +229,7 @@ def __init__( def create_model(self, model: FlowSystemModel) -> 'FlowModel': self._plausibility_checks() - self.model = FlowModel(model, self) + self.submodel = FlowModel(model, self) return self.model def transform_data(self, flow_system: 'FlowSystem'): diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 877db6fdc..454a552b3 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -451,7 +451,7 @@ def add_elements(self, *elements: Element) -> None: def create_model(self) -> FlowSystemModel: if not self.connected_and_transformed: raise RuntimeError('FlowSystem is not connected_and_transformed. Call FlowSystem.connect_and_transform() first.') - self.model = FlowSystemModel(self) + self.submodel = FlowSystemModel(self) return self.model def plot_network( diff --git a/flixopt/results.py b/flixopt/results.py index 941d0d6dc..9ede9ab49 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -167,7 +167,7 @@ def __init__( self.flow_system_data = flow_system_data self.summary = summary self.name = name - self.model = model + self.submodel = model self.folder = pathlib.Path(folder) if folder is not None else pathlib.Path.cwd() / 'results' self.components = { label: ComponentResults(self, **infos) for label, infos in self.solution.attrs['Components'].items() From 286a8b7f3455972750c8ce1744d5f17632518ed7 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 21 Jul 2025 21:56:26 +0200 Subject: [PATCH 234/448] Rename .model with .submodel if its only a submodel --- .../example_calculation_types.py | 2 +- flixopt/aggregation.py | 8 +- flixopt/calculation.py | 28 +-- flixopt/components.py | 24 +- flixopt/effects.py | 26 +- flixopt/elements.py | 30 +-- flixopt/flow_system.py | 2 +- flixopt/results.py | 2 +- flixopt/structure.py | 10 +- tests/test_bus.py | 8 +- tests/test_component.py | 50 ++-- tests/test_effect.py | 12 +- tests/test_flow.py | 232 +++++++++--------- tests/test_functional.py | 88 +++---- tests/test_integration.py | 22 +- tests/test_linear_converter.py | 28 +-- 16 files changed, 286 insertions(+), 286 deletions(-) diff --git a/examples/03_Calculation_types/example_calculation_types.py b/examples/03_Calculation_types/example_calculation_types.py index 8bbdf1773..9f5828cec 100644 --- a/examples/03_Calculation_types/example_calculation_types.py +++ b/examples/03_Calculation_types/example_calculation_types.py @@ -188,7 +188,7 @@ def get_solutions(calcs: List, variable: str) -> xr.Dataset: if calc.name == 'Segmented': dataarrays.append(calc.results.solution_without_overlap(variable).rename(calc.name)) else: - dataarrays.append(calc.results.model.variables[variable].solution.rename(calc.name)) + dataarrays.append(calc.results.submodel.variables[variable].solution.rename(calc.name)) return xr.merge(dataarrays) # --- Plotting for comparison --- diff --git a/flixopt/aggregation.py b/flixopt/aggregation.py index eb44ad707..e4f7a598a 100644 --- a/flixopt/aggregation.py +++ b/flixopt/aggregation.py @@ -323,14 +323,14 @@ def do_modeling(self): if isinstance(component, Storage) and not self.aggregation_parameters.fix_storage_flows: continue # Fix Nothing in The Storage - all_variables_of_component = set(component.model.variables) + all_variables_of_component = set(component.submodel.variables) if self.aggregation_parameters.aggregate_data_and_fix_non_binary_vars: - relevant_variables = component.model.variables[all_variables_of_component & time_variables] + relevant_variables = component.submodel.variables[all_variables_of_component & time_variables] else: - relevant_variables = component.model.variables[all_variables_of_component & binary_time_variables] + relevant_variables = component.submodel.variables[all_variables_of_component & binary_time_variables] for variable in relevant_variables: - self._equate_indices(component.model.variables[variable], indices) + self._equate_indices(component.submodel.variables[variable], indices) penalty = self.aggregation_parameters.penalty_of_period_freedom if (self.aggregation_parameters.percentage_of_period_freedom > 0) and penalty != 0: diff --git a/flixopt/calculation.py b/flixopt/calculation.py index 53c02beca..5e505ff0f 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -81,7 +81,7 @@ def __init__( flow_system._used_in_calculation = True self.flow_system = flow_system - self.model: Optional[FlowSystemModel] = None + self.submodel: Optional[FlowSystemModel] = None self.durations = {'modeling': 0.0, 'solving': 0.0, 'saving': 0.0} self.folder = pathlib.Path.cwd() / 'results' if folder is None else pathlib.Path(folder) @@ -106,9 +106,9 @@ def main_results(self) -> Dict[str, Union[Scalar, Dict]]: 'Penalty': self.model.effects.penalty.total.solution.values, 'Effects': { f'{effect.label} [{effect.unit}]': { - 'operation': effect.model.operation.total.solution.values, - 'invest': effect.model.invest.total.solution.values, - 'total': effect.model.total.solution.values, + 'operation': effect.submodel.operation.total.solution.values, + 'invest': effect.submodel.invest.total.solution.values, + 'total': effect.submodel.total.solution.values, } for effect in self.flow_system.effects }, @@ -116,28 +116,28 @@ def main_results(self) -> Dict[str, Union[Scalar, Dict]]: 'Invested': { model.label_of_element: model.size.solution for component in self.flow_system.components.values() - for model in component.model.sub_models + for model in component.submodel.sub_models if isinstance(model, InvestmentModel) and model.size.solution.max() >= CONFIG.modeling.EPSILON }, 'Not invested': { model.label_of_element: model.size.solution for component in self.flow_system.components.values() - for model in component.model.sub_models + for model in component.submodel.sub_models if isinstance(model, InvestmentModel) and model.size.solution.max() < CONFIG.modeling.EPSILON }, }, 'Buses with excess': [ { bus.label_full: { - 'input': bus.model.excess_input.solution.sum('time'), - 'output': bus.model.excess_output.solution.sum('time'), + 'input': bus.submodel.excess_input.solution.sum('time'), + 'output': bus.submodel.excess_output.solution.sum('time'), } } for bus in self.flow_system.buses.values() if bus.with_excess and ( - bus.model.excess_input.solution.sum() > 1e-3 - or bus.model.excess_output.solution.sum() > 1e-3 + bus.submodel.excess_input.solution.sum() > 1e-3 + or bus.submodel.excess_output.solution.sum() > 1e-3 ) ], } @@ -180,7 +180,7 @@ def do_modeling(self) -> 'FullCalculation': t_start = timeit.default_timer() self.flow_system.connect_and_transform() - self.submodel = self.flow_system.create_model() + self.model = self.flow_system.create_model() self.model.do_modeling() self.durations['modeling'] = round(timeit.default_timer() - t_start, 2) @@ -298,7 +298,7 @@ def do_modeling(self) -> 'AggregatedCalculation': self._perform_aggregation() # Model the System - self.submodel = self.flow_system.create_model() + self.model = self.flow_system.create_model() self.model.do_modeling() # Add Aggregation Submodel after modeling the rest self.aggregation = AggregationModel( @@ -488,7 +488,7 @@ def do_modeling_and_solve( invest_elements = [ model.label_full for component in calculation.flow_system.components.values() - for model in component.model.sub_models + for model in component.submodel.sub_models if isinstance(model, InvestmentModel) ] if invest_elements: @@ -532,7 +532,7 @@ def _transfer_start_values(self, i: int): for current_flow in current_flow_system.flows.values(): next_flow = next_flow_system.flows[current_flow.label_full] - next_flow.previous_flow_rate = current_flow.model.flow_rate.solution.sel( + next_flow.previous_flow_rate = current_flow.submodel.flow_rate.solution.sel( time=slice(start_previous_values, end_previous_values) ).values start_values_of_this_segment[current_flow.label_full] = next_flow.previous_flow_rate diff --git a/flixopt/components.py b/flixopt/components.py index fe776e02d..52e676323 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -62,7 +62,7 @@ def __init__( def create_model(self, model: FlowSystemModel) -> 'LinearConverterModel': self._plausibility_checks() self.submodel = LinearConverterModel(model, self) - return self.model + return self.submodel def _plausibility_checks(self) -> None: super()._plausibility_checks() @@ -204,7 +204,7 @@ def __init__( def create_model(self, model: FlowSystemModel) -> 'StorageModel': self._plausibility_checks() self.submodel = StorageModel(model, self) - return self.model + return self.submodel def transform_data(self, flow_system: 'FlowSystem') -> None: super().transform_data(flow_system) @@ -381,7 +381,7 @@ def _plausibility_checks(self): def create_model(self, model) -> 'TransmissionModel': self._plausibility_checks() self.submodel = TransmissionModel(model, self) - return self.model + return self.submodel def transform_data(self, flow_system: 'FlowSystem') -> None: super().transform_data(flow_system) @@ -420,7 +420,7 @@ def do_modeling(self): if self.element.balanced: # eq: in1.size = in2.size self.add_constraints( - self.element.in1.model._investment.size == self.element.in2.model._investment.size, + self.element.in1.submodel._investment.size == self.element.in2.submodel._investment.size, short_name='same_size', ) @@ -428,12 +428,12 @@ def create_transmission_equation(self, name: str, in_flow: Flow, out_flow: Flow) """Creates an Equation for the Transmission efficiency and adds it to the model""" # eq: out(t) + on(t)*loss_abs(t) = in(t)*(1 - loss_rel(t)) con_transmission = self.add_constraints( - out_flow.model.flow_rate == -in_flow.model.flow_rate * (self.element.relative_losses - 1), + out_flow.submodel.flow_rate == -in_flow.submodel.flow_rate * (self.element.relative_losses - 1), short_name=name, ) if self.element.absolute_losses is not None: - con_transmission.lhs += in_flow.model.on_off.on * self.element.absolute_losses + con_transmission.lhs += in_flow.submodel.on_off.on * self.element.absolute_losses return con_transmission @@ -460,15 +460,15 @@ def do_modeling(self): used_outputs: Set = all_output_flows & used_flows self.add_constraints( - sum([flow.model.flow_rate * conv_factors[flow.label] for flow in used_inputs]) - == sum([flow.model.flow_rate * conv_factors[flow.label] for flow in used_outputs]), + sum([flow.submodel.flow_rate * conv_factors[flow.label] for flow in used_inputs]) + == sum([flow.submodel.flow_rate * conv_factors[flow.label] for flow in used_outputs]), short_name=f'conversion_{i}', ) else: # TODO: Improve Inclusion of OnOffParameters. Instead of creating a Binary in every flow, the binary could only be part of the Piece itself piecewise_conversion = { - self.element.flows[flow].model.flow_rate.name: piecewise + self.element.flows[flow].submodel.flow_rate.name: piecewise for flow, piecewise in self.element.piecewise_conversion.items() } @@ -510,15 +510,15 @@ def do_modeling(self): # eq: nettoFlow(t) - discharging(t) + charging(t) = 0 self.add_constraints( self.netto_discharge - == self.element.discharging.model.flow_rate - self.element.charging.model.flow_rate, + == self.element.discharging.submodel.flow_rate - self.element.charging.submodel.flow_rate, short_name='netto_discharge', ) charge_state = self.charge_state rel_loss = self.element.relative_loss_per_hour hours_per_step = self._model.hours_per_step - charge_rate = self.element.charging.model.flow_rate - discharge_rate = self.element.discharging.model.flow_rate + charge_rate = self.element.charging.submodel.flow_rate + discharge_rate = self.element.discharging.submodel.flow_rate eff_charge = self.element.eta_charge eff_discharge = self.element.eta_discharge diff --git a/flixopt/effects.py b/flixopt/effects.py index 9f8e2506f..f9b122b1b 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -130,7 +130,7 @@ def transform_data(self, flow_system: 'FlowSystem'): def create_model(self, model: FlowSystemModel) -> 'EffectModel': self._plausibility_checks() self.submodel = EffectModel(model, self) - return self.model + return self.submodel def _plausibility_checks(self) -> None: # TODO: Check for plausibility @@ -211,13 +211,13 @@ def __init__(self, *effects: List[Effect]): self._standard_effect: Optional[Effect] = None self._objective_effect: Optional[Effect] = None - self.model: Optional[EffectCollectionModel] = None + self.submodel: Optional[EffectCollectionModel] = None self.add_effects(*effects) def create_model(self, model: FlowSystemModel) -> 'EffectCollectionModel': self._plausibility_checks() self.submodel = EffectCollectionModel(model, self) - return self.model + return self.submodel def add_effects(self, *effects: Effect) -> None: for effect in list(effects): @@ -393,13 +393,13 @@ def add_share_to_effects( ) -> None: for effect, expression in expressions.items(): if target == 'operation': - self.effects[effect].model.operation.add_share( + self.effects[effect].submodel.operation.add_share( name, expression, dims=('time', 'year', 'scenario'), ) elif target == 'invest': - self.effects[effect].model.invest.add_share( + self.effects[effect].submodel.invest.add_share( name, expression, dims=('year', 'scenario'), @@ -419,13 +419,13 @@ def do_modeling(self): ShareAllocationModel(self._model, dims=(), label_of_element='Penalty'), short_name='penalty', ) - for model in [effect.model for effect in self.effects] + [self.penalty]: + for model in [effect.submodel for effect in self.effects] + [self.penalty]: model.do_modeling() self._add_share_between_effects() self._model.add_objective( - (self.effects.objective_effect.model.total * self._model.weights).sum() + (self.effects.objective_effect.submodel.total * self._model.weights).sum() + self.penalty.total.sum() ) @@ -433,16 +433,16 @@ def _add_share_between_effects(self): for origin_effect in self.effects: # 1. operation: -> hier sind es Zeitreihen (share_TS) for target_effect, time_series in origin_effect.specific_share_to_other_effects_operation.items(): - self.effects[target_effect].model.operation.add_share( - origin_effect.model.operation.label_full, - origin_effect.model.operation.total_per_timestep * time_series, + self.effects[target_effect].submodel.operation.add_share( + origin_effect.submodel.operation.label_full, + origin_effect.submodel.operation.total_per_timestep * time_series, dims=('time', 'year', 'scenario'), ) # 2. invest: -> hier ist es Scalar (share) for target_effect, factor in origin_effect.specific_share_to_other_effects_invest.items(): - self.effects[target_effect].model.invest.add_share( - origin_effect.model.invest.label_full, - origin_effect.model.invest.total * factor, + self.effects[target_effect].submodel.invest.add_share( + origin_effect.submodel.invest.label_full, + origin_effect.submodel.invest.total * factor, dims=('year', 'scenario'), ) diff --git a/flixopt/elements.py b/flixopt/elements.py index 4dbf67aea..b952093ba 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -68,7 +68,7 @@ def __init__( def create_model(self, model: FlowSystemModel) -> 'ComponentModel': self._plausibility_checks() self.submodel = ComponentModel(model, self) - return self.model + return self.submodel def transform_data(self, flow_system: 'FlowSystem') -> None: if self.on_off_parameters is not None: @@ -113,7 +113,7 @@ def __init__( def create_model(self, model: FlowSystemModel) -> 'BusModel': self._plausibility_checks() self.submodel = BusModel(model, self) - return self.model + return self.submodel def transform_data(self, flow_system: 'FlowSystem'): self.excess_penalty_per_flow_hour = flow_system.fit_to_model_coords( @@ -230,7 +230,7 @@ def __init__( def create_model(self, model: FlowSystemModel) -> 'FlowModel': self._plausibility_checks() self.submodel = FlowModel(model, self) - return self.model + return self.submodel def transform_data(self, flow_system: 'FlowSystem'): self.relative_minimum = flow_system.fit_to_model_coords( @@ -551,9 +551,9 @@ def __init__(self, model: FlowSystemModel, element: Bus): def do_modeling(self) -> None: # inputs == outputs for flow in self.element.inputs + self.element.outputs: - self.register_variable(flow.model.flow_rate, flow.label_full) - inputs = sum([flow.model.flow_rate for flow in self.element.inputs]) - outputs = sum([flow.model.flow_rate for flow in self.element.outputs]) + self.register_variable(flow.submodel.flow_rate, flow.label_full) + inputs = sum([flow.submodel.flow_rate for flow in self.element.inputs]) + outputs = sum([flow.submodel.flow_rate for flow in self.element.outputs]) eq_bus_balance = self.add_constraints(inputs == outputs, short_name='balance') # Fehlerplus/-minus: @@ -570,8 +570,8 @@ def do_modeling(self) -> None: self._model.effects.add_share_to_penalty(self.label_of_element, (self.excess_output * excess_penalty).sum()) def results_structure(self): - inputs = [flow.model.flow_rate.name for flow in self.element.inputs] - outputs = [flow.model.flow_rate.name for flow in self.element.outputs] + inputs = [flow.submodel.flow_rate.name for flow in self.element.inputs] + outputs = [flow.submodel.flow_rate.name for flow in self.element.outputs] if self.excess_input is not None: inputs.append(self.excess_input.name) if self.excess_output is not None: @@ -606,9 +606,9 @@ def do_modeling(self): if self.element.on_off_parameters: on = self.add_variables(binary=True, short_name='on', coords=self._model.get_coords()) if len(all_flows) == 1: - self.add_constraints(on == all_flows[0].model.on_off.on, short_name='on') + self.add_constraints(on == all_flows[0].submodel.on_off.on, short_name='on') else: - flow_ons = [flow.model.on_off.on for flow in all_flows] + flow_ons = [flow.submodel.on_off.on for flow in all_flows] #TODO: Is the EPSILON even necessary? self.add_constraints(on <= sum(flow_ons) + CONFIG.modeling.EPSILON, short_name='on|ub') self.add_constraints(on >= sum(flow_ons) / (len(flow_ons) + CONFIG.modeling.EPSILON), short_name='on|lb') @@ -629,18 +629,18 @@ def do_modeling(self): if self.element.prevent_simultaneous_flows: # Simultanious Useage --> Only One FLow is On at a time, but needs a Binary for every flow - on_variables = [flow.model.on_off.on for flow in self.element.prevent_simultaneous_flows] + on_variables = [flow.submodel.on_off.on for flow in self.element.prevent_simultaneous_flows] ModelingPrimitives.mutual_exclusivity_constraint( self, - binary_variables=[flow.model.on_off.on for flow in self.element.prevent_simultaneous_flows], + binary_variables=[flow.submodel.on_off.on for flow in self.element.prevent_simultaneous_flows], short_name='prevent_simultaneous_use', ) def results_structure(self): return { **super().results_structure(), - 'inputs': [flow.model.flow_rate.name for flow in self.element.inputs], - 'outputs': [flow.model.flow_rate.name for flow in self.element.outputs], + 'inputs': [flow.submodel.flow_rate.name for flow in self.element.inputs], + 'outputs': [flow.submodel.flow_rate.name for flow in self.element.outputs], 'flows': [flow.label_full for flow in self.element.inputs + self.element.outputs], } @@ -650,7 +650,7 @@ def previous_states(self) -> Optional[xr.DataArray]: if self.element.on_off_parameters is None: raise ValueError(f'OnOffModel not present in \n{self}\nCant access previous_states') - previous_states = [flow.model.on_off._previous_states for flow in self.element.inputs + self.element.outputs] + previous_states = [flow.submodel.on_off._previous_states for flow in self.element.inputs + self.element.outputs] previous_states = [da for da in previous_states if da is not None] if not previous_states: # Empty list diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 454a552b3..0a10b3ceb 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -452,7 +452,7 @@ def create_model(self) -> FlowSystemModel: if not self.connected_and_transformed: raise RuntimeError('FlowSystem is not connected_and_transformed. Call FlowSystem.connect_and_transform() first.') self.submodel = FlowSystemModel(self) - return self.model + return self.submodel def plot_network( self, diff --git a/flixopt/results.py b/flixopt/results.py index 9ede9ab49..941d0d6dc 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -167,7 +167,7 @@ def __init__( self.flow_system_data = flow_system_data self.summary = summary self.name = name - self.submodel = model + self.model = model self.folder = pathlib.Path(folder) if folder is not None else pathlib.Path.cwd() / 'results' self.components = { label: ComponentResults(self, **infos) for label, infos in self.solution.attrs['Components'].items() diff --git a/flixopt/structure.py b/flixopt/structure.py index 7da8ade78..61becd11f 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -74,21 +74,21 @@ def solution(self): solution['objective'] = self.objective.value solution.attrs = { 'Components': { - comp.label_full: comp.model.results_structure() + comp.label_full: comp.submodel.results_structure() for comp in sorted( self.flow_system.components.values(), key=lambda component: component.label_full.upper() ) }, 'Buses': { - bus.label_full: bus.model.results_structure() + bus.label_full: bus.submodel.results_structure() for bus in sorted(self.flow_system.buses.values(), key=lambda bus: bus.label_full.upper()) }, 'Effects': { - effect.label_full: effect.model.results_structure() + effect.label_full: effect.submodel.results_structure() for effect in sorted(self.flow_system.effects, key=lambda effect: effect.label_full.upper()) }, 'Flows': { - flow.label_full: flow.model.results_structure() + flow.label_full: flow.submodel.results_structure() for flow in sorted(self.flow_system.flows.values(), key=lambda flow: flow.label_full.upper()) }, } @@ -661,7 +661,7 @@ def __init__(self, label: str, meta_data: Dict = None): """ self.label = Element._valid_label(label) self.meta_data = meta_data if meta_data is not None else {} - self.model: Optional[ElementModel] = None + self.submodel: Optional[ElementModel] = None def _plausibility_checks(self) -> None: """This function is used to do some basic plausibility checks for each Element during initialization""" diff --git a/tests/test_bus.py b/tests/test_bus.py index 136f9d2cc..fb1cfcda3 100644 --- a/tests/test_bus.py +++ b/tests/test_bus.py @@ -20,8 +20,8 @@ def test_bus(self, basic_flow_system_linopy): fx.Source('GastarifTest', source=fx.Flow('Q_Gas', 'TestBus'))) model = create_linopy_model(flow_system) - assert set(bus.model.variables) == {'WärmelastTest(Q_th_Last)|flow_rate', 'GastarifTest(Q_Gas)|flow_rate'} - assert set(bus.model.constraints) == {'TestBus|balance'} + assert set(bus.submodel.variables) == {'WärmelastTest(Q_th_Last)|flow_rate', 'GastarifTest(Q_Gas)|flow_rate'} + assert set(bus.submodel.constraints) == {'TestBus|balance'} assert_conequal( model.constraints['TestBus|balance'], @@ -38,11 +38,11 @@ def test_bus_penalty(self, basic_flow_system_linopy): fx.Source('GastarifTest', source=fx.Flow('Q_Gas', 'TestBus'))) model = create_linopy_model(flow_system) - assert set(bus.model.variables) == {'TestBus|excess_input', + assert set(bus.submodel.variables) == {'TestBus|excess_input', 'TestBus|excess_output', 'WärmelastTest(Q_th_Last)|flow_rate', 'GastarifTest(Q_Gas)|flow_rate'} - assert set(bus.model.constraints) == {'TestBus|balance'} + assert set(bus.submodel.constraints) == {'TestBus|balance'} assert_var_equal(model.variables['TestBus|excess_input'], model.add_variables(lower=0, coords = (timesteps,))) assert_var_equal(model.variables['TestBus|excess_output'], model.add_variables(lower=0, coords=(timesteps,))) diff --git a/tests/test_component.py b/tests/test_component.py index 3bf1699ec..14b1544dd 100644 --- a/tests/test_component.py +++ b/tests/test_component.py @@ -53,12 +53,12 @@ def test_component(self, basic_flow_system_linopy): 'TestComponent(Out1)|flow_rate', 'TestComponent(Out1)|total_flow_hours', 'TestComponent(Out2)|flow_rate', - 'TestComponent(Out2)|total_flow_hours'} == set(comp.model.variables) + 'TestComponent(Out2)|total_flow_hours'} == set(comp.submodel.variables) assert {'TestComponent(In1)|total_flow_hours', 'TestComponent(In2)|total_flow_hours', 'TestComponent(Out1)|total_flow_hours', - 'TestComponent(Out2)|total_flow_hours'} == set(comp.model.constraints) + 'TestComponent(Out2)|total_flow_hours'} == set(comp.submodel.constraints) def test_on_with_multiple_flows(self, basic_flow_system_linopy): """Test that flow model constraints are correctly generated.""" @@ -78,7 +78,7 @@ def test_on_with_multiple_flows(self, basic_flow_system_linopy): flow_system.add_elements(comp) model = create_linopy_model(flow_system) - assert set(comp.model.variables) == { + assert set(comp.submodel.variables) == { 'TestComponent(In1)|flow_rate', 'TestComponent(In1)|total_flow_hours', 'TestComponent(In1)|on', @@ -95,7 +95,7 @@ def test_on_with_multiple_flows(self, basic_flow_system_linopy): 'TestComponent|on_hours_total', } - assert set(comp.model.constraints) == { + assert set(comp.submodel.constraints) == { 'TestComponent(In1)|total_flow_hours', 'TestComponent(In1)|flow_rate|lb', 'TestComponent(In1)|flow_rate|ub', @@ -158,7 +158,7 @@ def test_on_with_single_flow(self, basic_flow_system_linopy): flow_system.add_elements(comp) model = create_linopy_model(flow_system) - assert set(comp.model.variables) == { + assert set(comp.submodel.variables) == { 'TestComponent(In1)|flow_rate', 'TestComponent(In1)|total_flow_hours', 'TestComponent(In1)|on', @@ -167,7 +167,7 @@ def test_on_with_single_flow(self, basic_flow_system_linopy): 'TestComponent|on_hours_total', } - assert set(comp.model.constraints) == { + assert set(comp.submodel.constraints) == { 'TestComponent(In1)|total_flow_hours', 'TestComponent(In1)|flow_rate|lb', 'TestComponent(In1)|flow_rate|ub', @@ -214,7 +214,7 @@ def test_previous_states_with_multiple_flows(self, basic_flow_system_linopy): flow_system.add_elements(comp) model = create_linopy_model(flow_system) - assert set(comp.model.variables) == { + assert set(comp.submodel.variables) == { 'TestComponent(In1)|flow_rate', 'TestComponent(In1)|total_flow_hours', 'TestComponent(In1)|on', @@ -231,7 +231,7 @@ def test_previous_states_with_multiple_flows(self, basic_flow_system_linopy): 'TestComponent|on_hours_total', } - assert set(comp.model.constraints) == { + assert set(comp.submodel.constraints) == { 'TestComponent(In1)|total_flow_hours', 'TestComponent(In1)|flow_rate|lb', 'TestComponent(In1)|flow_rate|ub', @@ -296,14 +296,14 @@ def test_transmission_basic(self, basic_flow_system, highs_solver): # Assertions assert_almost_equal_numeric( - transmission.in1.model.on_off.on.solution.values, + transmission.in1.submodel.on_off.on.solution.values, np.array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1]), 'On does not work properly', ) assert_almost_equal_numeric( - transmission.in1.model.flow_rate.solution.values * 0.8 - 20, - transmission.out1.model.flow_rate.solution.values, + transmission.in1.submodel.flow_rate.solution.values * 0.8 - 20, + transmission.out1.submodel.flow_rate.solution.values, 'Losses are not computed correctly', ) @@ -351,27 +351,27 @@ def test_transmission_balanced(self, basic_flow_system, highs_solver): # Assertions assert_almost_equal_numeric( - transmission.in1.model.on_off.on.solution.values, + transmission.in1.submodel.on_off.on.solution.values, np.array([1, 1, 1, 0, 0, 0, 0, 0, 0, 0]), 'On does not work properly', ) assert_almost_equal_numeric( calculation.results.model.variables['Rohr(Rohr1b)|flow_rate'].solution.values, - transmission.out1.model.flow_rate.solution.values, + transmission.out1.submodel.flow_rate.solution.values, 'Flow rate of Rohr__Rohr1b is not correct', ) assert_almost_equal_numeric( - transmission.in1.model.flow_rate.solution.values * 0.8 - - np.array([20 if val > 0.1 else 0 for val in transmission.in1.model.flow_rate.solution.values]), - transmission.out1.model.flow_rate.solution.values, + transmission.in1.submodel.flow_rate.solution.values * 0.8 + - np.array([20 if val > 0.1 else 0 for val in transmission.in1.submodel.flow_rate.solution.values]), + transmission.out1.submodel.flow_rate.solution.values, 'Losses are not computed correctly', ) assert_almost_equal_numeric( - transmission.in1.model._investment.size.solution.item(), - transmission.in2.model._investment.size.solution.item(), + transmission.in1.submodel._investment.size.solution.item(), + transmission.in2.submodel._investment.size.solution.item(), 'The Investments are not equated correctly', ) @@ -419,28 +419,28 @@ def test_transmission_unbalanced(self, basic_flow_system, highs_solver): # Assertions assert_almost_equal_numeric( - transmission.in1.model.on_off.on.solution.values, + transmission.in1.submodel.on_off.on.solution.values, np.array([1, 1, 1, 0, 0, 0, 0, 0, 0, 0]), 'On does not work properly', ) assert_almost_equal_numeric( calculation.results.model.variables['Rohr(Rohr1b)|flow_rate'].solution.values, - transmission.out1.model.flow_rate.solution.values, + transmission.out1.submodel.flow_rate.solution.values, 'Flow rate of Rohr__Rohr1b is not correct', ) assert_almost_equal_numeric( - transmission.in1.model.flow_rate.solution.values * 0.8 - - np.array([20 if val > 0.1 else 0 for val in transmission.in1.model.flow_rate.solution.values]), - transmission.out1.model.flow_rate.solution.values, + transmission.in1.submodel.flow_rate.solution.values * 0.8 + - np.array([20 if val > 0.1 else 0 for val in transmission.in1.submodel.flow_rate.solution.values]), + transmission.out1.submodel.flow_rate.solution.values, 'Losses are not computed correctly', ) - assert transmission.in1.model._investment.size.solution.item() > 11 + assert transmission.in1.submodel._investment.size.solution.item() > 11 assert_almost_equal_numeric( - transmission.in2.model._investment.size.solution.item(), + transmission.in2.submodel._investment.size.solution.item(), 10, 'Sizing does not work properly', ) diff --git a/tests/test_effect.py b/tests/test_effect.py index 8c75813e7..cce8ac939 100644 --- a/tests/test_effect.py +++ b/tests/test_effect.py @@ -19,11 +19,11 @@ def test_minimal(self, basic_flow_system_linopy): flow_system.add_elements(effect) model = create_linopy_model(flow_system) - assert set(effect.model.variables) == {'Effect1(invest)|total', + assert set(effect.submodel.variables) == {'Effect1(invest)|total', 'Effect1(operation)|total', 'Effect1(operation)|total_per_timestep', 'Effect1|total',} - assert set(effect.model.constraints) == {'Effect1(invest)|total', + assert set(effect.submodel.constraints) == {'Effect1(invest)|total', 'Effect1(operation)|total', 'Effect1(operation)|total_per_timestep', 'Effect1|total',} @@ -58,11 +58,11 @@ def test_bounds(self, basic_flow_system_linopy): flow_system.add_elements(effect) model = create_linopy_model(flow_system) - assert set(effect.model.variables) == {'Effect1(invest)|total', + assert set(effect.submodel.variables) == {'Effect1(invest)|total', 'Effect1(operation)|total', 'Effect1(operation)|total_per_timestep', 'Effect1|total',} - assert set(effect.model.constraints) == {'Effect1(invest)|total', + assert set(effect.submodel.constraints) == {'Effect1(invest)|total', 'Effect1(operation)|total', 'Effect1(operation)|total_per_timestep', 'Effect1|total',} @@ -100,7 +100,7 @@ def test_shares(self, basic_flow_system_linopy): flow_system.add_elements(effect1, effect2, effect3) model = create_linopy_model(flow_system) - assert set(effect2.model.variables) == { + assert set(effect2.submodel.variables) == { 'Effect2(invest)|total', 'Effect2(operation)|total', 'Effect2(operation)|total_per_timestep', @@ -108,7 +108,7 @@ def test_shares(self, basic_flow_system_linopy): 'Effect1(invest)->Effect2(invest)', 'Effect1(operation)->Effect2(operation)', } - assert set(effect2.model.constraints) == { + assert set(effect2.submodel.constraints) == { 'Effect2(invest)|total', 'Effect2(operation)|total', 'Effect2(operation)|total_per_timestep', diff --git a/tests/test_flow.py b/tests/test_flow.py index 50154859d..43ecbe34f 100644 --- a/tests/test_flow.py +++ b/tests/test_flow.py @@ -23,14 +23,14 @@ def test_flow_minimal(self, basic_flow_system_linopy): assert_conequal( model.constraints['Sink(Wärme)|total_flow_hours'], - flow.model.variables['Sink(Wärme)|total_flow_hours'] == (flow.model.variables['Sink(Wärme)|flow_rate'] * model.hours_per_step).sum() + flow.submodel.variables['Sink(Wärme)|total_flow_hours'] == (flow.submodel.variables['Sink(Wärme)|flow_rate'] * model.hours_per_step).sum() ) - assert_var_equal(flow.model.flow_rate, + assert_var_equal(flow.submodel.flow_rate, model.add_variables(lower=0, upper=100, coords=(timesteps,))) - assert_var_equal(flow.model.total_flow_hours, model.add_variables(lower=0)) + assert_var_equal(flow.submodel.total_flow_hours, model.add_variables(lower=0)) - assert set(flow.model.variables) == set(['Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate']) - assert set(flow.model.constraints) == set(['Sink(Wärme)|total_flow_hours']) + assert set(flow.submodel.variables) == set(['Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate']) + assert set(flow.submodel.constraints) == set(['Sink(Wärme)|total_flow_hours']) def test_flow(self, basic_flow_system_linopy): flow_system = basic_flow_system_linopy @@ -53,17 +53,17 @@ def test_flow(self, basic_flow_system_linopy): # total_flow_hours assert_conequal( model.constraints['Sink(Wärme)|total_flow_hours'], - flow.model.variables['Sink(Wärme)|total_flow_hours'] - == (flow.model.variables['Sink(Wärme)|flow_rate'] * model.hours_per_step).sum(), + flow.submodel.variables['Sink(Wärme)|total_flow_hours'] + == (flow.submodel.variables['Sink(Wärme)|flow_rate'] * model.hours_per_step).sum(), ) assert_var_equal( - flow.model.total_flow_hours, + flow.submodel.total_flow_hours, model.add_variables(lower=10, upper=1000) ) assert_var_equal( - flow.model.flow_rate, + flow.submodel.flow_rate, model.add_variables(lower=np.linspace(0, 0.5, timesteps.size) * 100, upper=np.linspace(0.5, 1, timesteps.size) * 100, coords=(timesteps,)) @@ -71,18 +71,18 @@ def test_flow(self, basic_flow_system_linopy): assert_conequal( model.constraints['Sink(Wärme)|load_factor_min'], - flow.model.variables['Sink(Wärme)|total_flow_hours'] + flow.submodel.variables['Sink(Wärme)|total_flow_hours'] >= model.hours_per_step.sum('time') * 0.1 * 100, ) assert_conequal( model.constraints['Sink(Wärme)|load_factor_max'], - flow.model.variables['Sink(Wärme)|total_flow_hours'] + flow.submodel.variables['Sink(Wärme)|total_flow_hours'] <= model.hours_per_step.sum('time') * 0.9 * 100, ) - assert set(flow.model.variables) == set(['Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate']) - assert set(flow.model.constraints) == set(['Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|load_factor_max', 'Sink(Wärme)|load_factor_min']) + assert set(flow.submodel.variables) == set(['Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate']) + assert set(flow.submodel.constraints) == set(['Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|load_factor_max', 'Sink(Wärme)|load_factor_min']) def test_effects_per_flow_hour(self, basic_flow_system_linopy): flow_system = basic_flow_system_linopy @@ -100,19 +100,19 @@ def test_effects_per_flow_hour(self, basic_flow_system_linopy): model = create_linopy_model(flow_system) costs, co2 = flow_system.effects['Costs'], flow_system.effects['CO2'] - assert set(flow.model.variables) == {'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate'} - assert set(flow.model.constraints) == {'Sink(Wärme)|total_flow_hours'} + assert set(flow.submodel.variables) == {'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate'} + assert set(flow.submodel.constraints) == {'Sink(Wärme)|total_flow_hours'} - assert 'Sink(Wärme)->Costs(operation)' in set(costs.model.constraints) - assert 'Sink(Wärme)->CO2(operation)' in set(co2.model.constraints) + assert 'Sink(Wärme)->Costs(operation)' in set(costs.submodel.constraints) + assert 'Sink(Wärme)->CO2(operation)' in set(co2.submodel.constraints) assert_conequal( model.constraints['Sink(Wärme)->Costs(operation)'], - model.variables['Sink(Wärme)->Costs(operation)'] == flow.model.variables['Sink(Wärme)|flow_rate'] * model.hours_per_step * costs_per_flow_hour) + model.variables['Sink(Wärme)->Costs(operation)'] == flow.submodel.variables['Sink(Wärme)|flow_rate'] * model.hours_per_step * costs_per_flow_hour) assert_conequal( model.constraints['Sink(Wärme)->CO2(operation)'], - model.variables['Sink(Wärme)->CO2(operation)'] == flow.model.variables['Sink(Wärme)|flow_rate'] * model.hours_per_step * co2_per_flow_hour) + model.variables['Sink(Wärme)->CO2(operation)'] == flow.submodel.variables['Sink(Wärme)|flow_rate'] * model.hours_per_step * co2_per_flow_hour) class TestFlowInvestModel: @@ -133,14 +133,14 @@ def test_flow_invest(self, basic_flow_system_linopy): flow_system.add_elements(fx.Sink('Sink', sink=flow)) model = create_linopy_model(flow_system) - assert set(flow.model.variables) == set( + assert set(flow.submodel.variables) == set( [ 'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate', 'Sink(Wärme)|size', ] ) - assert set(flow.model.constraints) == set( + assert set(flow.submodel.constraints) == set( [ 'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate|ub', @@ -153,7 +153,7 @@ def test_flow_invest(self, basic_flow_system_linopy): # flow_rate assert_var_equal( - flow.model.flow_rate, + flow.submodel.flow_rate, model.add_variables( lower=np.linspace(0.1, 0.5, timesteps.size) * 20, upper=np.linspace(0.5, 1, timesteps.size) * 100, @@ -162,14 +162,14 @@ def test_flow_invest(self, basic_flow_system_linopy): ) assert_conequal( model.constraints['Sink(Wärme)|flow_rate|lb'], - flow.model.variables['Sink(Wärme)|flow_rate'] - >= flow.model.variables['Sink(Wärme)|size'] + flow.submodel.variables['Sink(Wärme)|flow_rate'] + >= flow.submodel.variables['Sink(Wärme)|size'] * xr.DataArray(np.linspace(0.1, 0.5, timesteps.size), coords=(timesteps,)), ) assert_conequal( model.constraints['Sink(Wärme)|flow_rate|ub'], - flow.model.variables['Sink(Wärme)|flow_rate'] - <= flow.model.variables['Sink(Wärme)|size'] + flow.submodel.variables['Sink(Wärme)|flow_rate'] + <= flow.submodel.variables['Sink(Wärme)|size'] * xr.DataArray(np.linspace(0.5, 1, timesteps.size), coords=(timesteps,)), ) @@ -188,10 +188,10 @@ def test_flow_invest_optional(self, basic_flow_system_linopy): flow_system.add_elements(fx.Sink('Sink', sink=flow)) model = create_linopy_model(flow_system) - assert set(flow.model.variables) == set( + assert set(flow.submodel.variables) == set( ['Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate', 'Sink(Wärme)|size', 'Sink(Wärme)|is_invested'] ) - assert set(flow.model.constraints) == set( + assert set(flow.submodel.constraints) == set( [ 'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|size|lb', @@ -207,7 +207,7 @@ def test_flow_invest_optional(self, basic_flow_system_linopy): # flow_rate assert_var_equal( - flow.model.flow_rate, + flow.submodel.flow_rate, model.add_variables( lower=0, # Optional investment upper=np.linspace(0.5, 1, timesteps.size) * 100, @@ -216,25 +216,25 @@ def test_flow_invest_optional(self, basic_flow_system_linopy): ) assert_conequal( model.constraints['Sink(Wärme)|flow_rate|lb'], - flow.model.variables['Sink(Wärme)|flow_rate'] - >= flow.model.variables['Sink(Wärme)|size'] + flow.submodel.variables['Sink(Wärme)|flow_rate'] + >= flow.submodel.variables['Sink(Wärme)|size'] * xr.DataArray(np.linspace(0.1, 0.5, timesteps.size), coords=(timesteps,)), ) assert_conequal( model.constraints['Sink(Wärme)|flow_rate|ub'], - flow.model.variables['Sink(Wärme)|flow_rate'] - <= flow.model.variables['Sink(Wärme)|size'] + flow.submodel.variables['Sink(Wärme)|flow_rate'] + <= flow.submodel.variables['Sink(Wärme)|size'] * xr.DataArray(np.linspace(0.5, 1, timesteps.size), coords=(timesteps,)), ) # Is invested assert_conequal( model.constraints['Sink(Wärme)|size|ub'], - flow.model.variables['Sink(Wärme)|size'] <= flow.model.variables['Sink(Wärme)|is_invested'] * 100, + flow.submodel.variables['Sink(Wärme)|size'] <= flow.submodel.variables['Sink(Wärme)|is_invested'] * 100, ) assert_conequal( model.constraints['Sink(Wärme)|size|lb'], - flow.model.variables['Sink(Wärme)|size'] >= flow.model.variables['Sink(Wärme)|is_invested'] * 20, + flow.submodel.variables['Sink(Wärme)|size'] >= flow.submodel.variables['Sink(Wärme)|is_invested'] * 20, ) def test_flow_invest_optional_wo_min_size(self, basic_flow_system_linopy): @@ -252,10 +252,10 @@ def test_flow_invest_optional_wo_min_size(self, basic_flow_system_linopy): flow_system.add_elements(fx.Sink('Sink', sink=flow)) model = create_linopy_model(flow_system) - assert set(flow.model.variables) == set( + assert set(flow.submodel.variables) == set( ['Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate', 'Sink(Wärme)|size', 'Sink(Wärme)|is_invested'] ) - assert set(flow.model.constraints) == set( + assert set(flow.submodel.constraints) == set( [ 'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|size|ub', @@ -271,7 +271,7 @@ def test_flow_invest_optional_wo_min_size(self, basic_flow_system_linopy): # flow_rate assert_var_equal( - flow.model.flow_rate, + flow.submodel.flow_rate, model.add_variables( lower=0, # Optional investment upper=np.linspace(0.5, 1, timesteps.size) * 100, @@ -280,25 +280,25 @@ def test_flow_invest_optional_wo_min_size(self, basic_flow_system_linopy): ) assert_conequal( model.constraints['Sink(Wärme)|flow_rate|lb'], - flow.model.variables['Sink(Wärme)|flow_rate'] - >= flow.model.variables['Sink(Wärme)|size'] + flow.submodel.variables['Sink(Wärme)|flow_rate'] + >= flow.submodel.variables['Sink(Wärme)|size'] * xr.DataArray(np.linspace(0.1, 0.5, timesteps.size), coords=(timesteps,)), ) assert_conequal( model.constraints['Sink(Wärme)|flow_rate|ub'], - flow.model.variables['Sink(Wärme)|flow_rate'] - <= flow.model.variables['Sink(Wärme)|size'] + flow.submodel.variables['Sink(Wärme)|flow_rate'] + <= flow.submodel.variables['Sink(Wärme)|size'] * xr.DataArray(np.linspace(0.5, 1, timesteps.size), coords=(timesteps,)), ) # Is invested assert_conequal( model.constraints['Sink(Wärme)|size|ub'], - flow.model.variables['Sink(Wärme)|size'] <= flow.model.variables['Sink(Wärme)|is_invested'] * 100, + flow.submodel.variables['Sink(Wärme)|size'] <= flow.submodel.variables['Sink(Wärme)|is_invested'] * 100, ) assert_conequal( model.constraints['Sink(Wärme)|size|lb'], - flow.model.variables['Sink(Wärme)|size'] >= flow.model.variables['Sink(Wärme)|is_invested'] * 1e-5, + flow.submodel.variables['Sink(Wärme)|size'] >= flow.submodel.variables['Sink(Wärme)|is_invested'] * 1e-5, ) def test_flow_invest_wo_min_size_non_optional(self, basic_flow_system_linopy): @@ -316,10 +316,10 @@ def test_flow_invest_wo_min_size_non_optional(self, basic_flow_system_linopy): flow_system.add_elements(fx.Sink('Sink', sink=flow)) model = create_linopy_model(flow_system) - assert set(flow.model.variables) == set( + assert set(flow.submodel.variables) == set( ['Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate', 'Sink(Wärme)|size'] ) - assert set(flow.model.constraints) == set( + assert set(flow.submodel.constraints) == set( [ 'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate|lb', @@ -331,7 +331,7 @@ def test_flow_invest_wo_min_size_non_optional(self, basic_flow_system_linopy): # flow_rate assert_var_equal( - flow.model.flow_rate, + flow.submodel.flow_rate, model.add_variables( lower=np.linspace(0.1, 0.5, timesteps.size) * 1e-5, upper=np.linspace(0.5, 1, timesteps.size) * 100, @@ -340,14 +340,14 @@ def test_flow_invest_wo_min_size_non_optional(self, basic_flow_system_linopy): ) assert_conequal( model.constraints['Sink(Wärme)|flow_rate|lb'], - flow.model.variables['Sink(Wärme)|flow_rate'] - >= flow.model.variables['Sink(Wärme)|size'] + flow.submodel.variables['Sink(Wärme)|flow_rate'] + >= flow.submodel.variables['Sink(Wärme)|size'] * xr.DataArray(np.linspace(0.1, 0.5, timesteps.size), coords=(timesteps,)), ) assert_conequal( model.constraints['Sink(Wärme)|flow_rate|ub'], - flow.model.variables['Sink(Wärme)|flow_rate'] - <= flow.model.variables['Sink(Wärme)|size'] + flow.submodel.variables['Sink(Wärme)|flow_rate'] + <= flow.submodel.variables['Sink(Wärme)|size'] * xr.DataArray(np.linspace(0.5, 1, timesteps.size), coords=(timesteps,)), ) @@ -367,13 +367,13 @@ def test_flow_invest_fixed_size(self, basic_flow_system_linopy): flow_system.add_elements(fx.Sink('Sink', sink=flow)) model = create_linopy_model(flow_system) - assert set(flow.model.variables) == {'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate', 'Sink(Wärme)|size'} + assert set(flow.submodel.variables) == {'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate', 'Sink(Wärme)|size'} # Check that size is fixed to 75 - assert_var_equal(flow.model.variables['Sink(Wärme)|size'], model.add_variables(lower=75, upper=75)) + assert_var_equal(flow.submodel.variables['Sink(Wärme)|size'], model.add_variables(lower=75, upper=75)) # Check flow rate bounds - assert_var_equal(flow.model.flow_rate, model.add_variables(lower=0.2 * 75, upper=0.9 * 75, coords=(timesteps,))) + assert_var_equal(flow.submodel.flow_rate, model.add_variables(lower=0.2 * 75, upper=0.9 * 75, coords=(timesteps,))) def test_flow_invest_with_effects(self, basic_flow_system_linopy): """Test flow with investment effects.""" @@ -405,13 +405,13 @@ def test_flow_invest_with_effects(self, basic_flow_system_linopy): assert_conequal( model.constraints['Sink(Wärme)->Costs(invest)'], model.variables['Sink(Wärme)->Costs(invest)'] - == flow.model.variables['Sink(Wärme)|is_invested'] * 1000 + flow.model.variables['Sink(Wärme)|size'] * 500, + == flow.submodel.variables['Sink(Wärme)|is_invested'] * 1000 + flow.submodel.variables['Sink(Wärme)|size'] * 500, ) assert_conequal( model.constraints['Sink(Wärme)->CO2(invest)'], model.variables['Sink(Wärme)->CO2(invest)'] - == flow.model.variables['Sink(Wärme)|is_invested'] * 5 + flow.model.variables['Sink(Wärme)|size'] * 0.1, + == flow.submodel.variables['Sink(Wärme)|is_invested'] * 5 + flow.submodel.variables['Sink(Wärme)|size'] * 0.1, ) def test_flow_invest_divest_effects(self, basic_flow_system_linopy): @@ -458,11 +458,11 @@ def test_flow_on(self, basic_flow_system_linopy): flow_system.add_elements(fx.Sink('Sink', sink=flow)) model = create_linopy_model(flow_system) - assert set(flow.model.variables) == set( + assert set(flow.submodel.variables) == set( ['Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate', 'Sink(Wärme)|on', 'Sink(Wärme)|on_hours_total'] ) - assert set(flow.model.constraints) == set( + assert set(flow.submodel.constraints) == set( [ 'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|on_hours_total', @@ -472,7 +472,7 @@ def test_flow_on(self, basic_flow_system_linopy): ) # flow_rate assert_var_equal( - flow.model.flow_rate, + flow.submodel.flow_rate, model.add_variables( lower=0, upper=0.8 * 100, @@ -482,7 +482,7 @@ def test_flow_on(self, basic_flow_system_linopy): # OnOff assert_var_equal( - flow.model.on_off.on, + flow.submodel.on_off.on, model.add_variables(binary=True, coords=(timesteps,)), ) assert_var_equal( @@ -491,17 +491,17 @@ def test_flow_on(self, basic_flow_system_linopy): ) assert_conequal( model.constraints['Sink(Wärme)|flow_rate|lb'], - flow.model.variables['Sink(Wärme)|flow_rate'] >= flow.model.variables['Sink(Wärme)|on'] * 0.2 * 100, + flow.submodel.variables['Sink(Wärme)|flow_rate'] >= flow.submodel.variables['Sink(Wärme)|on'] * 0.2 * 100, ) assert_conequal( model.constraints['Sink(Wärme)|flow_rate|ub'], - flow.model.variables['Sink(Wärme)|flow_rate'] <= flow.model.variables['Sink(Wärme)|on'] * 0.8 * 100, + flow.submodel.variables['Sink(Wärme)|flow_rate'] <= flow.submodel.variables['Sink(Wärme)|on'] * 0.8 * 100, ) assert_conequal( model.constraints['Sink(Wärme)|on_hours_total'], - flow.model.variables['Sink(Wärme)|on_hours_total'] - == (flow.model.variables['Sink(Wärme)|on'] * model.hours_per_step).sum(), + flow.submodel.variables['Sink(Wärme)|on_hours_total'] + == (flow.submodel.variables['Sink(Wärme)|on'] * model.hours_per_step).sum(), ) def test_effects_per_running_hour(self, basic_flow_system_linopy): @@ -522,32 +522,32 @@ def test_effects_per_running_hour(self, basic_flow_system_linopy): model = create_linopy_model(flow_system) costs, co2 = flow_system.effects['Costs'], flow_system.effects['CO2'] - assert set(flow.model.variables) == { + assert set(flow.submodel.variables) == { 'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate', 'Sink(Wärme)|on', 'Sink(Wärme)|on_hours_total', } - assert set(flow.model.constraints) == { + assert set(flow.submodel.constraints) == { 'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate|lb', 'Sink(Wärme)|flow_rate|ub', 'Sink(Wärme)|on_hours_total', } - assert 'Sink(Wärme)->Costs(operation)' in set(costs.model.constraints) - assert 'Sink(Wärme)->CO2(operation)' in set(co2.model.constraints) + assert 'Sink(Wärme)->Costs(operation)' in set(costs.submodel.constraints) + assert 'Sink(Wärme)->CO2(operation)' in set(co2.submodel.constraints) assert_conequal( model.constraints['Sink(Wärme)->Costs(operation)'], model.variables['Sink(Wärme)->Costs(operation)'] - == flow.model.variables['Sink(Wärme)|on'] * model.hours_per_step * costs_per_running_hour, + == flow.submodel.variables['Sink(Wärme)|on'] * model.hours_per_step * costs_per_running_hour, ) assert_conequal( model.constraints['Sink(Wärme)->CO2(operation)'], model.variables['Sink(Wärme)->CO2(operation)'] - == flow.model.variables['Sink(Wärme)|on'] * model.hours_per_step * co2_per_running_hour, + == flow.submodel.variables['Sink(Wärme)|on'] * model.hours_per_step * co2_per_running_hour, ) def test_consecutive_on_hours(self, basic_flow_system_linopy): @@ -568,14 +568,14 @@ def test_consecutive_on_hours(self, basic_flow_system_linopy): flow_system.add_elements( fx.Sink('Sink', sink=flow)) model = create_linopy_model(flow_system) - assert {'Sink(Wärme)|consecutive_on_hours', 'Sink(Wärme)|on'}.issubset(set(flow.model.variables)) + assert {'Sink(Wärme)|consecutive_on_hours', 'Sink(Wärme)|on'}.issubset(set(flow.submodel.variables)) assert {'Sink(Wärme)|consecutive_on_hours|ub', 'Sink(Wärme)|consecutive_on_hours|forward', 'Sink(Wärme)|consecutive_on_hours|backward', 'Sink(Wärme)|consecutive_on_hours|initial', 'Sink(Wärme)|consecutive_on_hours|lb', - }.issubset(set(flow.model.constraints)) + }.issubset(set(flow.submodel.constraints)) assert_var_equal( model.variables['Sink(Wärme)|consecutive_on_hours'], @@ -635,14 +635,14 @@ def test_consecutive_on_hours_previous(self, basic_flow_system_linopy): flow_system.add_elements( fx.Sink('Sink', sink=flow)) model = create_linopy_model(flow_system) - assert {'Sink(Wärme)|consecutive_on_hours', 'Sink(Wärme)|on'}.issubset(set(flow.model.variables)) + assert {'Sink(Wärme)|consecutive_on_hours', 'Sink(Wärme)|on'}.issubset(set(flow.submodel.variables)) assert {'Sink(Wärme)|consecutive_on_hours|lb', 'Sink(Wärme)|consecutive_on_hours|forward', 'Sink(Wärme)|consecutive_on_hours|backward', 'Sink(Wärme)|consecutive_on_hours|initial', 'Sink(Wärme)|consecutive_on_hours|lb', - }.issubset(set(flow.model.constraints)) + }.issubset(set(flow.submodel.constraints)) assert_var_equal( model.variables['Sink(Wärme)|consecutive_on_hours'], @@ -701,7 +701,7 @@ def test_consecutive_off_hours(self, basic_flow_system_linopy): flow_system.add_elements( fx.Sink('Sink', sink=flow)) model = create_linopy_model(flow_system) - assert {'Sink(Wärme)|consecutive_off_hours', 'Sink(Wärme)|off'}.issubset(set(flow.model.variables)) + assert {'Sink(Wärme)|consecutive_off_hours', 'Sink(Wärme)|off'}.issubset(set(flow.submodel.variables)) assert { 'Sink(Wärme)|consecutive_off_hours|ub', @@ -709,7 +709,7 @@ def test_consecutive_off_hours(self, basic_flow_system_linopy): 'Sink(Wärme)|consecutive_off_hours|backward', 'Sink(Wärme)|consecutive_off_hours|initial', 'Sink(Wärme)|consecutive_off_hours|lb' - }.issubset(set(flow.model.constraints)) + }.issubset(set(flow.submodel.constraints)) assert_var_equal( model.variables['Sink(Wärme)|consecutive_off_hours'], @@ -769,7 +769,7 @@ def test_consecutive_off_hours_previous(self, basic_flow_system_linopy): flow_system.add_elements( fx.Sink('Sink', sink=flow)) model = create_linopy_model(flow_system) - assert {'Sink(Wärme)|consecutive_off_hours', 'Sink(Wärme)|off'}.issubset(set(flow.model.variables)) + assert {'Sink(Wärme)|consecutive_off_hours', 'Sink(Wärme)|off'}.issubset(set(flow.submodel.variables)) assert { 'Sink(Wärme)|consecutive_off_hours|ub', @@ -777,7 +777,7 @@ def test_consecutive_off_hours_previous(self, basic_flow_system_linopy): 'Sink(Wärme)|consecutive_off_hours|backward', 'Sink(Wärme)|consecutive_off_hours|initial', 'Sink(Wärme)|consecutive_off_hours|lb' - }.issubset(set(flow.model.constraints)) + }.issubset(set(flow.submodel.constraints)) assert_var_equal( model.variables['Sink(Wärme)|consecutive_off_hours'], @@ -837,7 +837,7 @@ def test_switch_on_constraints(self, basic_flow_system_linopy): # Check that variables exist assert {'Sink(Wärme)|switch|on', 'Sink(Wärme)|switch|off', 'Sink(Wärme)|switch|count'}.issubset( - set(flow.model.variables) + set(flow.submodel.variables) ) # Check that constraints exist @@ -846,16 +846,16 @@ def test_switch_on_constraints(self, basic_flow_system_linopy): 'Sink(Wärme)|switch|initial', 'Sink(Wärme)|switch|mutex', 'Sink(Wärme)|switch|count', - }.issubset(set(flow.model.constraints)) + }.issubset(set(flow.submodel.constraints)) # Check switch_on_nr variable bounds - assert_var_equal(flow.model.variables['Sink(Wärme)|switch|count'], model.add_variables(lower=0, upper=5)) + assert_var_equal(flow.submodel.variables['Sink(Wärme)|switch|count'], model.add_variables(lower=0, upper=5)) # Verify switch_on_nr constraint (limits number of startups) assert_conequal( model.constraints['Sink(Wärme)|switch|count'], - flow.model.variables['Sink(Wärme)|switch|count'] - == flow.model.variables['Sink(Wärme)|switch|on'].sum('time'), + flow.submodel.variables['Sink(Wärme)|switch|count'] + == flow.submodel.variables['Sink(Wärme)|switch|on'].sum('time'), ) # Check that startup cost effect constraint exists @@ -864,7 +864,7 @@ def test_switch_on_constraints(self, basic_flow_system_linopy): # Verify the startup cost effect constraint assert_conequal( model.constraints['Sink(Wärme)->Costs(operation)'], - model.variables['Sink(Wärme)->Costs(operation)'] == flow.model.variables['Sink(Wärme)|switch|on'] * 100, + model.variables['Sink(Wärme)->Costs(operation)'] == flow.submodel.variables['Sink(Wärme)|switch|on'] * 100, ) def test_on_hours_limits(self, basic_flow_system_linopy): @@ -885,19 +885,19 @@ def test_on_hours_limits(self, basic_flow_system_linopy): model = create_linopy_model(flow_system) # Check that variables exist - assert {'Sink(Wärme)|on', 'Sink(Wärme)|on_hours_total'}.issubset(set(flow.model.variables)) + assert {'Sink(Wärme)|on', 'Sink(Wärme)|on_hours_total'}.issubset(set(flow.submodel.variables)) # Check that constraints exist assert 'Sink(Wärme)|on_hours_total' in model.constraints # Check on_hours_total variable bounds - assert_var_equal(flow.model.variables['Sink(Wärme)|on_hours_total'], model.add_variables(lower=20, upper=100)) + assert_var_equal(flow.submodel.variables['Sink(Wärme)|on_hours_total'], model.add_variables(lower=20, upper=100)) # Check on_hours_total constraint assert_conequal( model.constraints['Sink(Wärme)|on_hours_total'], - flow.model.variables['Sink(Wärme)|on_hours_total'] - == (flow.model.variables['Sink(Wärme)|on'] * model.hours_per_step).sum(), + flow.submodel.variables['Sink(Wärme)|on_hours_total'] + == (flow.submodel.variables['Sink(Wärme)|on'] * model.hours_per_step).sum(), ) @@ -918,7 +918,7 @@ def test_flow_on_invest_optional(self, basic_flow_system_linopy): flow_system.add_elements(fx.Sink('Sink', sink=flow)) model = create_linopy_model(flow_system) - assert set(flow.model.variables) == set( + assert set(flow.submodel.variables) == set( [ 'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate', @@ -929,7 +929,7 @@ def test_flow_on_invest_optional(self, basic_flow_system_linopy): ] ) - assert set(flow.model.constraints) == set( + assert set(flow.submodel.constraints) == set( [ 'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|on_hours_total', @@ -944,7 +944,7 @@ def test_flow_on_invest_optional(self, basic_flow_system_linopy): # flow_rate assert_var_equal( - flow.model.flow_rate, + flow.submodel.flow_rate, model.add_variables( lower=0, upper=0.8 * 200, @@ -954,7 +954,7 @@ def test_flow_on_invest_optional(self, basic_flow_system_linopy): # OnOff assert_var_equal( - flow.model.on_off.on, + flow.submodel.on_off.on, model.add_variables(binary=True, coords=(timesteps,)), ) assert_var_equal( @@ -963,24 +963,24 @@ def test_flow_on_invest_optional(self, basic_flow_system_linopy): ) assert_conequal( model.constraints['Sink(Wärme)|size|lb'], - flow.model.variables['Sink(Wärme)|size'] >= flow.model.variables['Sink(Wärme)|is_invested'] * 20, + flow.submodel.variables['Sink(Wärme)|size'] >= flow.submodel.variables['Sink(Wärme)|is_invested'] * 20, ) assert_conequal( model.constraints['Sink(Wärme)|size|ub'], - flow.model.variables['Sink(Wärme)|size']<= flow.model.variables['Sink(Wärme)|is_invested'] * 200, + flow.submodel.variables['Sink(Wärme)|size']<= flow.submodel.variables['Sink(Wärme)|is_invested'] * 200, ) assert_conequal( model.constraints['Sink(Wärme)|flow_rate|lb1'], - flow.model.variables['Sink(Wärme)|on'] * 0.2 * 20 <= flow.model.variables['Sink(Wärme)|flow_rate'], + flow.submodel.variables['Sink(Wärme)|on'] * 0.2 * 20 <= flow.submodel.variables['Sink(Wärme)|flow_rate'], ) assert_conequal( model.constraints['Sink(Wärme)|flow_rate|ub1'], - flow.model.variables['Sink(Wärme)|on'] * 0.8 * 200 >= flow.model.variables['Sink(Wärme)|flow_rate'], + flow.submodel.variables['Sink(Wärme)|on'] * 0.8 * 200 >= flow.submodel.variables['Sink(Wärme)|flow_rate'], ) assert_conequal( model.constraints['Sink(Wärme)|on_hours_total'], - flow.model.variables['Sink(Wärme)|on_hours_total'] - == (flow.model.variables['Sink(Wärme)|on'] * model.hours_per_step).sum(), + flow.submodel.variables['Sink(Wärme)|on_hours_total'] + == (flow.submodel.variables['Sink(Wärme)|on'] * model.hours_per_step).sum(), ) # Investment @@ -989,12 +989,12 @@ def test_flow_on_invest_optional(self, basic_flow_system_linopy): mega = 0.2 * 200 # Relative minimum * maximum size assert_conequal( model.constraints['Sink(Wärme)|flow_rate|lb2'], - flow.model.variables['Sink(Wärme)|flow_rate'] - >= flow.model.variables['Sink(Wärme)|on'] * mega + flow.model.variables['Sink(Wärme)|size'] * 0.2 - mega, + flow.submodel.variables['Sink(Wärme)|flow_rate'] + >= flow.submodel.variables['Sink(Wärme)|on'] * mega + flow.submodel.variables['Sink(Wärme)|size'] * 0.2 - mega, ) assert_conequal( model.constraints['Sink(Wärme)|flow_rate|ub2'], - flow.model.variables['Sink(Wärme)|flow_rate'] <= flow.model.variables['Sink(Wärme)|size'] * 0.8, + flow.submodel.variables['Sink(Wärme)|flow_rate'] <= flow.submodel.variables['Sink(Wärme)|size'] * 0.8, ) def test_flow_on_invest_non_optional(self, basic_flow_system_linopy): @@ -1011,7 +1011,7 @@ def test_flow_on_invest_non_optional(self, basic_flow_system_linopy): flow_system.add_elements(fx.Sink('Sink', sink=flow)) model = create_linopy_model(flow_system) - assert set(flow.model.variables) == set( + assert set(flow.submodel.variables) == set( [ 'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate', @@ -1021,7 +1021,7 @@ def test_flow_on_invest_non_optional(self, basic_flow_system_linopy): ] ) - assert set(flow.model.constraints) == set( + assert set(flow.submodel.constraints) == set( [ 'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|on_hours_total', @@ -1034,7 +1034,7 @@ def test_flow_on_invest_non_optional(self, basic_flow_system_linopy): # flow_rate assert_var_equal( - flow.model.flow_rate, + flow.submodel.flow_rate, model.add_variables( lower=0, upper=0.8 * 200, @@ -1044,7 +1044,7 @@ def test_flow_on_invest_non_optional(self, basic_flow_system_linopy): # OnOff assert_var_equal( - flow.model.on_off.on, + flow.submodel.on_off.on, model.add_variables(binary=True, coords=(timesteps,)), ) assert_var_equal( @@ -1053,16 +1053,16 @@ def test_flow_on_invest_non_optional(self, basic_flow_system_linopy): ) assert_conequal( model.constraints['Sink(Wärme)|flow_rate|lb1'], - flow.model.variables['Sink(Wärme)|on'] * 0.2 * 20 <= flow.model.variables['Sink(Wärme)|flow_rate'], + flow.submodel.variables['Sink(Wärme)|on'] * 0.2 * 20 <= flow.submodel.variables['Sink(Wärme)|flow_rate'], ) assert_conequal( model.constraints['Sink(Wärme)|flow_rate|ub1'], - flow.model.variables['Sink(Wärme)|on'] * 0.8 * 200 >= flow.model.variables['Sink(Wärme)|flow_rate'], + flow.submodel.variables['Sink(Wärme)|on'] * 0.8 * 200 >= flow.submodel.variables['Sink(Wärme)|flow_rate'], ) assert_conequal( model.constraints['Sink(Wärme)|on_hours_total'], - flow.model.variables['Sink(Wärme)|on_hours_total'] - == (flow.model.variables['Sink(Wärme)|on'] * model.hours_per_step).sum(), + flow.submodel.variables['Sink(Wärme)|on_hours_total'] + == (flow.submodel.variables['Sink(Wärme)|on'] * model.hours_per_step).sum(), ) # Investment @@ -1071,12 +1071,12 @@ def test_flow_on_invest_non_optional(self, basic_flow_system_linopy): mega = 0.2 * 200 # Relative minimum * maximum size assert_conequal( model.constraints['Sink(Wärme)|flow_rate|lb2'], - flow.model.variables['Sink(Wärme)|flow_rate'] - >= flow.model.variables['Sink(Wärme)|on'] * mega + flow.model.variables['Sink(Wärme)|size'] * 0.2 - mega, + flow.submodel.variables['Sink(Wärme)|flow_rate'] + >= flow.submodel.variables['Sink(Wärme)|on'] * mega + flow.submodel.variables['Sink(Wärme)|size'] * 0.2 - mega, ) assert_conequal( model.constraints['Sink(Wärme)|flow_rate|ub2'], - flow.model.variables['Sink(Wärme)|flow_rate'] <= flow.model.variables['Sink(Wärme)|size'] * 0.8, + flow.submodel.variables['Sink(Wärme)|flow_rate'] <= flow.submodel.variables['Sink(Wärme)|size'] * 0.8, ) @@ -1098,7 +1098,7 @@ def test_fixed_relative_profile(self, basic_flow_system_linopy): flow_system.add_elements(fx.Sink('Sink', sink=flow)) model = create_linopy_model(flow_system) - assert_var_equal(flow.model.variables['Sink(Wärme)|flow_rate'], + assert_var_equal(flow.submodel.variables['Sink(Wärme)|flow_rate'], model.add_variables(lower=profile * 100, upper=profile * 100, coords=(timesteps,)) @@ -1124,15 +1124,15 @@ def test_fixed_profile_with_investment(self, basic_flow_system_linopy): model = create_linopy_model(flow_system) assert_var_equal( - flow.model.variables['Sink(Wärme)|flow_rate'], + flow.submodel.variables['Sink(Wärme)|flow_rate'], model.add_variables(lower=0, upper=profile * 200, coords=(timesteps,)), ) # The constraint should link flow_rate to size * profile assert_conequal( model.constraints['Sink(Wärme)|flow_rate|fixed'], - flow.model.variables['Sink(Wärme)|flow_rate'] - == flow.model.variables['Sink(Wärme)|size'] * xr.DataArray(profile, coords=(timesteps,)), + flow.submodel.variables['Sink(Wärme)|flow_rate'] + == flow.submodel.variables['Sink(Wärme)|size'] * xr.DataArray(profile, coords=(timesteps,)), ) diff --git a/tests/test_functional.py b/tests/test_functional.py index 9542d656b..2315867f1 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -155,21 +155,21 @@ def test_fixed_size(solver_fixture, time_steps_fixture): boiler = flow_system.all_elements['Boiler'] costs = flow_system.effects['costs'] assert_allclose( - costs.model.total.solution.item(), + costs.submodel.total.solution.item(), 80 + 1000 * 1 + 10, rtol=1e-5, atol=1e-10, err_msg='The total costs does not have the right value', ) assert_allclose( - boiler.Q_th.model._investment.size.solution.item(), + boiler.Q_th.submodel._investment.size.solution.item(), 1000, rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__Investment_size" does not have the right value', ) assert_allclose( - boiler.Q_th.model._investment.is_invested.solution.item(), + boiler.Q_th.submodel._investment.is_invested.solution.item(), 1, rtol=1e-5, atol=1e-10, @@ -196,21 +196,21 @@ def test_optimize_size(solver_fixture, time_steps_fixture): boiler = flow_system.all_elements['Boiler'] costs = flow_system.effects['costs'] assert_allclose( - costs.model.total.solution.item(), + costs.submodel.total.solution.item(), 80 + 20 * 1 + 10, rtol=1e-5, atol=1e-10, err_msg='The total costs does not have the right value', ) assert_allclose( - boiler.Q_th.model._investment.size.solution.item(), + boiler.Q_th.submodel._investment.size.solution.item(), 20, rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__Investment_size" does not have the right value', ) assert_allclose( - boiler.Q_th.model._investment.is_invested.solution.item(), + boiler.Q_th.submodel._investment.is_invested.solution.item(), 1, rtol=1e-5, atol=1e-10, @@ -237,21 +237,21 @@ def test_size_bounds(solver_fixture, time_steps_fixture): boiler = flow_system.all_elements['Boiler'] costs = flow_system.effects['costs'] assert_allclose( - costs.model.total.solution.item(), + costs.submodel.total.solution.item(), 80 + 40 * 1 + 10, rtol=1e-5, atol=1e-10, err_msg='The total costs does not have the right value', ) assert_allclose( - boiler.Q_th.model._investment.size.solution.item(), + boiler.Q_th.submodel._investment.size.solution.item(), 40, rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__Investment_size" does not have the right value', ) assert_allclose( - boiler.Q_th.model._investment.is_invested.solution.item(), + boiler.Q_th.submodel._investment.is_invested.solution.item(), 1, rtol=1e-5, atol=1e-10, @@ -289,21 +289,21 @@ def test_optional_invest(solver_fixture, time_steps_fixture): boiler_optional = flow_system.all_elements['Boiler_optional'] costs = flow_system.effects['costs'] assert_allclose( - costs.model.total.solution.item(), + costs.submodel.total.solution.item(), 80 + 40 * 1 + 10, rtol=1e-5, atol=1e-10, err_msg='The total costs does not have the right value', ) assert_allclose( - boiler.Q_th.model._investment.size.solution.item(), + boiler.Q_th.submodel._investment.size.solution.item(), 40, rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__Investment_size" does not have the right value', ) assert_allclose( - boiler.Q_th.model._investment.is_invested.solution.item(), + boiler.Q_th.submodel._investment.is_invested.solution.item(), 1, rtol=1e-5, atol=1e-10, @@ -311,14 +311,14 @@ def test_optional_invest(solver_fixture, time_steps_fixture): ) assert_allclose( - boiler_optional.Q_th.model._investment.size.solution.item(), + boiler_optional.Q_th.submodel._investment.size.solution.item(), 0, rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__Investment_size" does not have the right value', ) assert_allclose( - boiler_optional.Q_th.model._investment.is_invested.solution.item(), + boiler_optional.Q_th.submodel._investment.is_invested.solution.item(), 0, rtol=1e-5, atol=1e-10, @@ -342,7 +342,7 @@ def test_on(solver_fixture, time_steps_fixture): boiler = flow_system.all_elements['Boiler'] costs = flow_system.effects['costs'] assert_allclose( - costs.model.total.solution.item(), + costs.submodel.total.solution.item(), 80, rtol=1e-5, atol=1e-10, @@ -350,14 +350,14 @@ def test_on(solver_fixture, time_steps_fixture): ) assert_allclose( - boiler.Q_th.model.on_off.on.solution.values, + boiler.Q_th.submodel.on_off.on.solution.values, [0, 1, 1, 0, 1], rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__on" does not have the right value', ) assert_allclose( - boiler.Q_th.model.flow_rate.solution.values, + boiler.Q_th.submodel.flow_rate.solution.values, [0, 10, 20, 0, 10], rtol=1e-5, atol=1e-10, @@ -386,7 +386,7 @@ def test_off(solver_fixture, time_steps_fixture): boiler = flow_system.all_elements['Boiler'] costs = flow_system.effects['costs'] assert_allclose( - costs.model.total.solution.item(), + costs.submodel.total.solution.item(), 80, rtol=1e-5, atol=1e-10, @@ -394,21 +394,21 @@ def test_off(solver_fixture, time_steps_fixture): ) assert_allclose( - boiler.Q_th.model.on_off.on.solution.values, + boiler.Q_th.submodel.on_off.on.solution.values, [0, 1, 1, 0, 1], rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__on" does not have the right value', ) assert_allclose( - boiler.Q_th.model.on_off.off.solution.values, - 1 - boiler.Q_th.model.on_off.on.solution.values, + boiler.Q_th.submodel.on_off.off.solution.values, + 1 - boiler.Q_th.submodel.on_off.on.solution.values, rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__off" does not have the right value', ) assert_allclose( - boiler.Q_th.model.flow_rate.solution.values, + boiler.Q_th.submodel.flow_rate.solution.values, [0, 10, 20, 0, 10], rtol=1e-5, atol=1e-10, @@ -437,7 +437,7 @@ def test_switch_on_off(solver_fixture, time_steps_fixture): boiler = flow_system.all_elements['Boiler'] costs = flow_system.effects['costs'] assert_allclose( - costs.model.total.solution.item(), + costs.submodel.total.solution.item(), 80, rtol=1e-5, atol=1e-10, @@ -445,28 +445,28 @@ def test_switch_on_off(solver_fixture, time_steps_fixture): ) assert_allclose( - boiler.Q_th.model.on_off.on.solution.values, + boiler.Q_th.submodel.on_off.on.solution.values, [0, 1, 1, 0, 1], rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__on" does not have the right value', ) assert_allclose( - boiler.Q_th.model.on_off.switch_on.solution.values, + boiler.Q_th.submodel.on_off.switch_on.solution.values, [0, 1, 0, 0, 1], rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__switch_on" does not have the right value', ) assert_allclose( - boiler.Q_th.model.on_off.switch_off.solution.values, + boiler.Q_th.submodel.on_off.switch_off.solution.values, [0, 0, 0, 1, 0], rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__switch_on" does not have the right value', ) assert_allclose( - boiler.Q_th.model.flow_rate.solution.values, + boiler.Q_th.submodel.flow_rate.solution.values, [0, 10, 20, 0, 10], rtol=1e-5, atol=1e-10, @@ -501,7 +501,7 @@ def test_on_total_max(solver_fixture, time_steps_fixture): boiler = flow_system.all_elements['Boiler'] costs = flow_system.effects['costs'] assert_allclose( - costs.model.total.solution.item(), + costs.submodel.total.solution.item(), 140, rtol=1e-5, atol=1e-10, @@ -509,14 +509,14 @@ def test_on_total_max(solver_fixture, time_steps_fixture): ) assert_allclose( - boiler.Q_th.model.on_off.on.solution.values, + boiler.Q_th.submodel.on_off.on.solution.values, [0, 0, 1, 0, 0], rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__on" does not have the right value', ) assert_allclose( - boiler.Q_th.model.flow_rate.solution.values, + boiler.Q_th.submodel.flow_rate.solution.values, [0, 0, 20, 0, 0], rtol=1e-5, atol=1e-10, @@ -560,7 +560,7 @@ def test_on_total_bounds(solver_fixture, time_steps_fixture): boiler_backup = flow_system.all_elements['Boiler_backup'] costs = flow_system.effects['costs'] assert_allclose( - costs.model.total.solution.item(), + costs.submodel.total.solution.item(), 114, rtol=1e-5, atol=1e-10, @@ -568,14 +568,14 @@ def test_on_total_bounds(solver_fixture, time_steps_fixture): ) assert_allclose( - boiler.Q_th.model.on_off.on.solution.values, + boiler.Q_th.submodel.on_off.on.solution.values, [0, 0, 1, 0, 1], rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__on" does not have the right value', ) assert_allclose( - boiler.Q_th.model.flow_rate.solution.values, + boiler.Q_th.submodel.flow_rate.solution.values, [0, 0, 20, 0, 12 - 1e-5], rtol=1e-5, atol=1e-10, @@ -583,14 +583,14 @@ def test_on_total_bounds(solver_fixture, time_steps_fixture): ) assert_allclose( - sum(boiler_backup.Q_th.model.on_off.on.solution.values), + sum(boiler_backup.Q_th.submodel.on_off.on.solution.values), 3, rtol=1e-5, atol=1e-10, err_msg='"Boiler_backup__Q_th__on" does not have the right value', ) assert_allclose( - boiler_backup.Q_th.model.flow_rate.solution.values, + boiler_backup.Q_th.submodel.flow_rate.solution.values, [0, 10, 1.0e-05, 0, 1.0e-05], rtol=1e-5, atol=1e-10, @@ -628,7 +628,7 @@ def test_consecutive_on_off(solver_fixture, time_steps_fixture): boiler_backup = flow_system.all_elements['Boiler_backup'] costs = flow_system.effects['costs'] assert_allclose( - costs.model.total.solution.item(), + costs.submodel.total.solution.item(), 190, rtol=1e-5, atol=1e-10, @@ -636,14 +636,14 @@ def test_consecutive_on_off(solver_fixture, time_steps_fixture): ) assert_allclose( - boiler.Q_th.model.on_off.on.solution.values, + boiler.Q_th.submodel.on_off.on.solution.values, [1, 1, 0, 1, 1], rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__on" does not have the right value', ) assert_allclose( - boiler.Q_th.model.flow_rate.solution.values, + boiler.Q_th.submodel.flow_rate.solution.values, [5, 10, 0, 18, 12], rtol=1e-5, atol=1e-10, @@ -651,7 +651,7 @@ def test_consecutive_on_off(solver_fixture, time_steps_fixture): ) assert_allclose( - boiler_backup.Q_th.model.flow_rate.solution.values, + boiler_backup.Q_th.submodel.flow_rate.solution.values, [0, 0, 20, 0, 0], rtol=1e-5, atol=1e-10, @@ -691,7 +691,7 @@ def test_consecutive_off(solver_fixture, time_steps_fixture): boiler_backup = flow_system.all_elements['Boiler_backup'] costs = flow_system.effects['costs'] assert_allclose( - costs.model.total.solution.item(), + costs.submodel.total.solution.item(), 110, rtol=1e-5, atol=1e-10, @@ -699,21 +699,21 @@ def test_consecutive_off(solver_fixture, time_steps_fixture): ) assert_allclose( - boiler_backup.Q_th.model.on_off.on.solution.values, + boiler_backup.Q_th.submodel.on_off.on.solution.values, [0, 0, 1, 0, 0], rtol=1e-5, atol=1e-10, err_msg='"Boiler_backup__Q_th__on" does not have the right value', ) assert_allclose( - boiler_backup.Q_th.model.on_off.off.solution.values, + boiler_backup.Q_th.submodel.on_off.off.solution.values, [1, 1, 0, 1, 1], rtol=1e-5, atol=1e-10, err_msg='"Boiler_backup__Q_th__off" does not have the right value', ) assert_allclose( - boiler_backup.Q_th.model.flow_rate.solution.values, + boiler_backup.Q_th.submodel.flow_rate.solution.values, [0, 0, 1e-5, 0, 0], rtol=1e-5, atol=1e-10, @@ -721,7 +721,7 @@ def test_consecutive_off(solver_fixture, time_steps_fixture): ) assert_allclose( - boiler.Q_th.model.flow_rate.solution.values, + boiler.Q_th.submodel.flow_rate.solution.values, [5, 0, 20 - 1e-5, 18, 12], rtol=1e-5, atol=1e-10, diff --git a/tests/test_integration.py b/tests/test_integration.py index e3d44d764..babc7b131 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -23,12 +23,12 @@ def test_simple_flow_system(self, simple_flow_system, highs_solver): # Cost assertions assert_almost_equal_numeric( - effects['costs'].model.total.solution.item(), 81.88394666666667, 'costs doesnt match expected value' + effects['costs'].submodel.total.solution.item(), 81.88394666666667, 'costs doesnt match expected value' ) # CO2 assertions assert_almost_equal_numeric( - effects['CO2'].model.total.solution.item(), 255.09184, 'CO2 doesnt match expected value' + effects['CO2'].submodel.total.solution.item(), 255.09184, 'CO2 doesnt match expected value' ) def test_model_components(self, simple_flow_system, highs_solver): @@ -40,14 +40,14 @@ def test_model_components(self, simple_flow_system, highs_solver): # Boiler assertions assert_almost_equal_numeric( - comps['Boiler'].Q_th.model.flow_rate.solution.values, + comps['Boiler'].Q_th.submodel.flow_rate.solution.values, [0, 0, 0, 28.4864, 35, 0, 0, 0, 0], 'Q_th doesnt match expected value', ) # CHP unit assertions assert_almost_equal_numeric( - comps['CHP_unit'].Q_th.model.flow_rate.solution.values, + comps['CHP_unit'].Q_th.submodel.flow_rate.solution.values, [30.0, 26.66666667, 75.0, 75.0, 75.0, 20.0, 20.0, 20.0, 20.0], 'Q_th doesnt match expected value', ) @@ -217,36 +217,36 @@ def test_piecewise_conversion(self, flow_system_piecewise_conversion, highs_solv # Compare expected values with actual values assert_almost_equal_numeric( - effects['costs'].model.total.solution.item(), -10710.997365760755, 'costs doesnt match expected value' + effects['costs'].submodel.total.solution.item(), -10710.997365760755, 'costs doesnt match expected value' ) assert_almost_equal_numeric( - effects['CO2'].model.total.solution.item(), 1278.7939026086956, 'CO2 doesnt match expected value' + effects['CO2'].submodel.total.solution.item(), 1278.7939026086956, 'CO2 doesnt match expected value' ) assert_almost_equal_numeric( - comps['Kessel'].Q_th.model.flow_rate.solution.values, + comps['Kessel'].Q_th.submodel.flow_rate.solution.values, [0, 0, 0, 45, 0, 0, 0, 0, 0], 'Kessel doesnt match expected value', ) kwk_flows = {flow.label: flow for flow in comps['KWK'].inputs + comps['KWK'].outputs} assert_almost_equal_numeric( - kwk_flows['Q_th'].model.flow_rate.solution.values, + kwk_flows['Q_th'].submodel.flow_rate.solution.values, [45.0, 45.0, 64.5962087, 100.0, 61.3136, 45.0, 45.0, 12.86469565, 0.0], 'KWK Q_th doesnt match expected value', ) assert_almost_equal_numeric( - kwk_flows['P_el'].model.flow_rate.solution.values, + kwk_flows['P_el'].submodel.flow_rate.solution.values, [40.0, 40.0, 47.12589407, 60.0, 45.93221818, 40.0, 40.0, 10.91784108, -0.0], 'KWK P_el doesnt match expected value', ) assert_almost_equal_numeric( - comps['Speicher'].model.netto_discharge.solution.values, + comps['Speicher'].submodel.netto_discharge.solution.values, [-15.0, -45.0, 25.4037913, -35.0, 48.6864, -25.0, -25.0, 7.13530435, 20.0], 'Speicher nettoFlow doesnt match expected value', ) assert_almost_equal_numeric( - comps['Speicher'].model.variables['Speicher|PiecewiseEffects|costs'].solution.values, + comps['Speicher'].submodel.variables['Speicher|PiecewiseEffects|costs'].solution.values, 454.74666666666667, 'Speicher investCosts_segmented_costs doesnt match expected value', ) diff --git a/tests/test_linear_converter.py b/tests/test_linear_converter.py index e15b11c1b..7f65d8fc2 100644 --- a/tests/test_linear_converter.py +++ b/tests/test_linear_converter.py @@ -46,7 +46,7 @@ def test_basic_linear_converter(self, basic_flow_system_linopy): # Check conversion constraint (input * 0.8 == output * 1.0) assert_conequal( model.constraints['Converter|conversion_0'], - input_flow.model.flow_rate * 0.8 == output_flow.model.flow_rate * 1.0 + input_flow.submodel.flow_rate * 0.8 == output_flow.submodel.flow_rate * 1.0 ) def test_linear_converter_time_varying(self, basic_flow_system_linopy): @@ -88,7 +88,7 @@ def test_linear_converter_time_varying(self, basic_flow_system_linopy): # Check conversion constraint (input * efficiency_series == output * 1.0) assert_conequal( model.constraints['Converter|conversion_0'], - input_flow.model.flow_rate * efficiency_series == output_flow.model.flow_rate * 1.0 + input_flow.submodel.flow_rate * efficiency_series == output_flow.submodel.flow_rate * 1.0 ) def test_linear_converter_multiple_factors(self, basic_flow_system_linopy): @@ -133,19 +133,19 @@ def test_linear_converter_multiple_factors(self, basic_flow_system_linopy): # Check conversion constraint 1 (input1 * 0.8 == output1 * 1.0) assert_conequal( model.constraints['Converter|conversion_0'], - input_flow1.model.flow_rate * 0.8 == output_flow1.model.flow_rate * 1.0 + input_flow1.submodel.flow_rate * 0.8 == output_flow1.submodel.flow_rate * 1.0 ) # Check conversion constraint 2 (input2 * 0.5 == output2 * 1.0) assert_conequal( model.constraints['Converter|conversion_1'], - input_flow2.model.flow_rate * 0.5 == output_flow2.model.flow_rate * 1.0 + input_flow2.submodel.flow_rate * 0.5 == output_flow2.submodel.flow_rate * 1.0 ) # Check conversion constraint 3 (input1 * 0.2 == output2 * 0.3) assert_conequal( model.constraints['Converter|conversion_2'], - input_flow1.model.flow_rate * 0.2 == output_flow2.model.flow_rate * 0.3 + input_flow1.submodel.flow_rate * 0.2 == output_flow2.submodel.flow_rate * 0.3 ) def test_linear_converter_with_on_off(self, basic_flow_system_linopy): @@ -196,7 +196,7 @@ def test_linear_converter_with_on_off(self, basic_flow_system_linopy): # Check conversion constraint assert_conequal( model.constraints['Converter|conversion_0'], - input_flow.model.flow_rate * 0.8 == output_flow.model.flow_rate * 1.0 + input_flow.submodel.flow_rate * 0.8 == output_flow.submodel.flow_rate * 1.0 ) # Check on_off effects @@ -252,17 +252,17 @@ def test_linear_converter_multidimensional(self, basic_flow_system_linopy): # Check the conversion equations assert_conequal( model.constraints['MultiConverter|conversion_0'], - input_flow1.model.flow_rate * 0.7 == output_flow1.model.flow_rate * 1.0 + input_flow1.submodel.flow_rate * 0.7 == output_flow1.submodel.flow_rate * 1.0 ) assert_conequal( model.constraints['MultiConverter|conversion_1'], - input_flow2.model.flow_rate * 0.3 == output_flow2.model.flow_rate * 1.0 + input_flow2.submodel.flow_rate * 0.3 == output_flow2.submodel.flow_rate * 1.0 ) assert_conequal( model.constraints['MultiConverter|conversion_2'], - input_flow1.model.flow_rate * 0.1 == output_flow2.model.flow_rate * 0.5 + input_flow1.submodel.flow_rate * 0.1 == output_flow2.submodel.flow_rate * 0.5 ) def test_edge_case_time_varying_conversion(self, basic_flow_system_linopy): @@ -311,7 +311,7 @@ def test_edge_case_time_varying_conversion(self, basic_flow_system_linopy): # Verify the constraint has the time-varying coefficient assert_conequal( model.constraints['VariableConverter|conversion_0'], - input_flow.model.flow_rate * fluctuating_cop == output_flow.model.flow_rate * 1.0 + input_flow.submodel.flow_rate * fluctuating_cop == output_flow.submodel.flow_rate * 1.0 ) def test_piecewise_conversion(self, basic_flow_system_linopy): @@ -361,10 +361,10 @@ def test_piecewise_conversion(self, basic_flow_system_linopy): model = create_linopy_model(flow_system) # Verify that PiecewiseModel was created and added as a submodel - assert converter.model.piecewise_conversion is not None + assert converter.submodel.piecewise_conversion is not None # Get the PiecewiseModel instance - piecewise_model = converter.model.piecewise_conversion + piecewise_model = converter.submodel.piecewise_conversion # Check that we have the expected pieces (2 in this case) assert len(piecewise_model.pieces) == 2 @@ -473,10 +473,10 @@ def test_piecewise_conversion_with_onoff(self, basic_flow_system_linopy): model = create_linopy_model(flow_system) # Verify that PiecewiseModel was created and added as a submodel - assert converter.model.piecewise_conversion is not None + assert converter.submodel.piecewise_conversion is not None # Get the PiecewiseModel instance - piecewise_model = converter.model.piecewise_conversion + piecewise_model = converter.submodel.piecewise_conversion # Check that we have the expected pieces (2 in this case) assert len(piecewise_model.pieces) == 2 From ae1752b41b26d060ee5511824b3844cdb62ed664 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 21 Jul 2025 22:00:25 +0200 Subject: [PATCH 235/448] Rename .sub_models with .submodels --- flixopt/calculation.py | 6 +++--- flixopt/effects.py | 2 +- flixopt/structure.py | 10 +++++----- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/flixopt/calculation.py b/flixopt/calculation.py index 5e505ff0f..d50bde388 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -116,13 +116,13 @@ def main_results(self) -> Dict[str, Union[Scalar, Dict]]: 'Invested': { model.label_of_element: model.size.solution for component in self.flow_system.components.values() - for model in component.submodel.sub_models + for model in component.submodel.submodels if isinstance(model, InvestmentModel) and model.size.solution.max() >= CONFIG.modeling.EPSILON }, 'Not invested': { model.label_of_element: model.size.solution for component in self.flow_system.components.values() - for model in component.submodel.sub_models + for model in component.submodel.submodels if isinstance(model, InvestmentModel) and model.size.solution.max() < CONFIG.modeling.EPSILON }, }, @@ -488,7 +488,7 @@ def do_modeling_and_solve( invest_elements = [ model.label_full for component in calculation.flow_system.components.values() - for model in component.submodel.sub_models + for model in component.submodel.submodels if isinstance(model, InvestmentModel) ] if invest_elements: diff --git a/flixopt/effects.py b/flixopt/effects.py index f9b122b1b..77a48e791 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -173,7 +173,7 @@ def __init__(self, model: FlowSystemModel, element: Effect): ) def do_modeling(self): - for model in self.sub_models: + for model in self.submodels: model.do_modeling() self.total = self.add_variables( diff --git a/flixopt/structure.py b/flixopt/structure.py index 61becd11f..f57a13031 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -836,14 +836,14 @@ def sub_models_direct(self) -> Dict[str, 'Submodel']: return self._sub_models @property - def sub_models(self) -> List['Submodel']: + def submodels(self) -> List['Submodel']: """All sub-models of the model""" direct_submodels = list(self._sub_models.values()) # Recursively collect nested sub-models nested_submodels = [] for submodel in direct_submodels: - nested_submodels.extend(submodel.sub_models) # This calls the property recursively + nested_submodels.extend(submodel.submodels) # This calls the property recursively return direct_submodels + nested_submodels @@ -852,7 +852,7 @@ def constraints(self) -> linopy.Constraints: """All constraints of the model, including those of sub-models""" names = list(self.constraints_direct) + [ constraint_name - for submodel in self.sub_models + for submodel in self.submodels for constraint_name in submodel.constraints_direct ] @@ -863,7 +863,7 @@ def variables(self) -> linopy.Variables: """All variables of the model, including those of sub-models""" names = list(self.variables_direct) + [ variable_name - for submodel in self.sub_models + for submodel in self.submodels for variable_name in submodel.variables_direct ] @@ -877,7 +877,7 @@ def __repr__(self) -> str: con_string = self.constraints.__repr__().split("\n", 2)[2] model_string = f"Linopy {self._model.type} submodel: {self.label_of_model}" - if len(self.sub_models) == 0: + if len(self.submodels) == 0: sub_models_string = ' \n' else: sub_models_string = '' From 1822384f1ffa77e3845fe9a7951eb263acd2b693 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 22 Jul 2025 10:49:03 +0200 Subject: [PATCH 236/448] Improve repr --- flixopt/structure.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/flixopt/structure.py b/flixopt/structure.py index f57a13031..37e20ae4d 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -875,21 +875,21 @@ def __repr__(self) -> str: """ var_string = self.variables.__repr__().split("\n", 2)[2] con_string = self.constraints.__repr__().split("\n", 2)[2] - model_string = f"Linopy {self._model.type} submodel: {self.label_of_model}" + model_string = f"Submodel of Linopy {self._model.type}:" if len(self.submodels) == 0: sub_models_string = ' \n' else: sub_models_string = '' for submodel_name, submodel in self.sub_models_direct.items(): - sub_models_string += f'\n * {submodel_name} [{submodel.__class__.__name__}]' + sub_models_string += f'\n * {submodel.__class__.__name__}: "{submodel_name}" [{len(submodel.variables)} Vars + {len(submodel.constraints)} Cons]' - return ( - f"{model_string}\n{'=' * len(model_string)}\n\n" - f"Variables:\n----------\n{var_string}\n" - f"Constraints:\n------------\n{con_string}\n" - f"Submodels:\n----------{sub_models_string}" - ) + text = {f"Variables: [{len(self.variables)}/{len(self._model.variables)}]": var_string, + f"Constraints: [{len(self.constraints)}/{len(self._model.constraints)}]": con_string, + f"Submodels: [{len(self.submodels)}]": sub_models_string} + comb = '\n'.join(f"{key}\n{'-' * len(key)}\n{value}" for key, value in text.items()) + + return f"{model_string}\n{'=' * len(model_string)}\n\n{comb}" @property def hours_per_step(self): From 2aa9d4b559a2afb6e0d4a64e55e03447cf93669b Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 22 Jul 2025 10:55:11 +0200 Subject: [PATCH 237/448] Improve repr --- flixopt/structure.py | 37 +++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/flixopt/structure.py b/flixopt/structure.py index 37e20ae4d..76cfc2392 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -873,23 +873,40 @@ def __repr__(self) -> str: """ Return a string representation of the linopy model. """ - var_string = self.variables.__repr__().split("\n", 2)[2] - con_string = self.constraints.__repr__().split("\n", 2)[2] - model_string = f"Submodel of Linopy {self._model.type}:" + # Extract content from variables and constraints representations + var_string = self.variables.__repr__().split('\n', 2)[2] + con_string = self.constraints.__repr__().split('\n', 2)[2] + model_string = f'Submodel of Linopy {self._model.type}:' + # Build submodels section if len(self.submodels) == 0: sub_models_string = ' \n' else: - sub_models_string = '' + submodel_lines = [] for submodel_name, submodel in self.sub_models_direct.items(): - sub_models_string += f'\n * {submodel.__class__.__name__}: "{submodel_name}" [{len(submodel.variables)} Vars + {len(submodel.constraints)} Cons]' + class_name = submodel.__class__.__name__ + submodel_lines.append(f' * {class_name}: "{submodel_name}" [{len(submodel.variables)} Vars + {len(submodel.constraints)} Cons]') + sub_models_string = '\n' + '\n'.join(submodel_lines) + + # Create sections with counts and content + sections = { + f'Variables: [{len(self.variables)}/{len(self._model.variables)}]': var_string, + f'Constraints: [{len(self.constraints)}/{len(self._model.constraints)}]': con_string, + f'Submodels: [{len(self.sub_models_direct)}]': sub_models_string, + } + + # Format sections with headers and underlines + formatted_sections = [] + for section_header, section_content in sections.items(): + underline = '-' * len(section_header) + formatted_section = f'{section_header}\n{underline}\n{section_content}' + formatted_sections.append(formatted_section) - text = {f"Variables: [{len(self.variables)}/{len(self._model.variables)}]": var_string, - f"Constraints: [{len(self.constraints)}/{len(self._model.constraints)}]": con_string, - f"Submodels: [{len(self.submodels)}]": sub_models_string} - comb = '\n'.join(f"{key}\n{'-' * len(key)}\n{value}" for key, value in text.items()) + # Combine everything with proper formatting + all_sections = '\n'.join(formatted_sections) + header_separator = '=' * len(model_string) - return f"{model_string}\n{'=' * len(model_string)}\n\n{comb}" + return f'{model_string}\n{header_separator}\n\n{all_sections}' @property def hours_per_step(self): From 5ca9707d75dee0d1f61f4db3652b26df10a25cf5 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 22 Jul 2025 11:24:05 +0200 Subject: [PATCH 238/448] Include def do_modeling() into __init__() of models --- flixopt/components.py | 32 ++++++++++++--------------- flixopt/effects.py | 14 +++++------- flixopt/elements.py | 25 ++++++++++------------ flixopt/features.py | 50 ++++++++++++++++++++++++------------------- flixopt/structure.py | 36 +++++++++++-------------------- 5 files changed, 71 insertions(+), 86 deletions(-) diff --git a/flixopt/components.py b/flixopt/components.py index 52e676323..16e74ade0 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -395,19 +395,18 @@ def transform_data(self, flow_system: 'FlowSystem') -> None: class TransmissionModel(ComponentModel): def __init__(self, model: FlowSystemModel, element: Transmission): - super().__init__(model, element) + if (element.absolute_losses is not None) and np.any(element.absolute_losses != 0): + for flow in element.inputs + element.outputs: + if flow.on_off_parameters is None: + flow.on_off_parameters = OnOffParameters() self.element: Transmission = element self.on_off: Optional[OnOffModel] = None - def do_modeling(self): - """Initiates all FlowModels""" - # Force On Variable if absolute losses are present - if (self.element.absolute_losses is not None) and np.any(self.element.absolute_losses != 0): - for flow in self.element.inputs + self.element.outputs: - if flow.on_off_parameters is None: - flow.on_off_parameters = OnOffParameters() + super().__init__(model, element) - super().do_modeling() + def _do_modeling(self): + """Initiates all FlowModels""" + super()._do_modeling() # first direction self.create_transmission_equation('dir1', self.element.in1, self.element.out1) @@ -440,14 +439,13 @@ def create_transmission_equation(self, name: str, in_flow: Flow, out_flow: Flow) class LinearConverterModel(ComponentModel): def __init__(self, model: FlowSystemModel, element: LinearConverter): - super().__init__(model, element) self.element: LinearConverter = element self.on_off: Optional[OnOffModel] = None self.piecewise_conversion: Optional[PiecewiseConversion] = None + super().__init__(model, element) - def do_modeling(self): - super().do_modeling() - + def _do_modeling(self): + super()._do_modeling() # conversion_factors: if self.element.conversion_factors: all_input_flows = set(self.element.inputs) @@ -483,7 +481,6 @@ def do_modeling(self): ), short_name='PiecewiseConversion', ) - self.piecewise_conversion.do_modeling() class StorageModel(ComponentModel): @@ -491,10 +488,9 @@ class StorageModel(ComponentModel): def __init__(self, model: FlowSystemModel, element: Storage): super().__init__(model, element) - self.element: Storage = element - def do_modeling(self): - super().do_modeling() + def _do_modeling(self): + super()._do_modeling() lb, ub = self.absolute_charge_state_bounds self.add_variables( @@ -540,7 +536,7 @@ def do_modeling(self): ), short_name='investment', ) - self._investment.do_modeling() + BoundingPatterns.scaled_bounds( self, variable=self.charge_state, diff --git a/flixopt/effects.py b/flixopt/effects.py index 77a48e791..b5ea81e3e 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -140,7 +140,8 @@ def _plausibility_checks(self) -> None: class EffectModel(ElementModel): def __init__(self, model: FlowSystemModel, element: Effect): super().__init__(model, element) - self.element: Effect = element + + def _do_modeling(self): self.total: Optional[linopy.Variable] = None self.invest: ShareAllocationModel = self.register_sub_model( ShareAllocationModel( @@ -172,10 +173,6 @@ def __init__(self, model: FlowSystemModel, element: Effect): short_name='operation', ) - def do_modeling(self): - for model in self.submodels: - model.do_modeling() - self.total = self.add_variables( lower=self.element.minimum_total if self.element.minimum_total is not None else -np.inf, upper=self.element.maximum_total if self.element.maximum_total is not None else np.inf, @@ -381,9 +378,9 @@ class EffectCollectionModel(Submodel): """ def __init__(self, model: FlowSystemModel, effects: EffectCollection): - super().__init__(model, label_of_element='Effects') self.effects = effects self.penalty: Optional[ShareAllocationModel] = None + super().__init__(model, label_of_element='Effects') def add_share_to_effects( self, @@ -412,15 +409,14 @@ def add_share_to_penalty(self, name: str, expression: linopy.LinearExpression) - raise TypeError(f'Penalty shares must be scalar expressions! ({expression.ndim=})') self.penalty.add_share(name, expression, dims=()) - def do_modeling(self): + def _do_modeling(self): + super()._do_modeling() for effect in self.effects: effect.create_model(self._model) self.penalty = self.register_sub_model( ShareAllocationModel(self._model, dims=(), label_of_element='Penalty'), short_name='penalty', ) - for model in [effect.submodel for effect in self.effects] + [self.penalty]: - model.do_modeling() self._add_share_between_effects() diff --git a/flixopt/elements.py b/flixopt/elements.py index b952093ba..bd4c27eca 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -315,9 +315,9 @@ def invest_is_optional(self) -> bool: class FlowModel(ElementModel): def __init__(self, model: FlowSystemModel, element: Flow): super().__init__(model, element) - self.element: Flow = element - def do_modeling(self): + def _do_modeling(self): + super()._do_modeling() # Main flow rate variable self.add_variables( lower=self.absolute_flow_rate_bounds[0], @@ -359,7 +359,7 @@ def _create_on_off_model(self): label_of_model=self.label_of_element, ), short_name='on_off', - ).do_modeling() + ) def _create_investment_model(self): self.register_sub_model( @@ -370,7 +370,7 @@ def _create_investment_model(self): label_of_model=self.label_of_element, ), 'investment', - ).do_modeling() + ) def _constraint_flow_rate(self): if not self.with_investment and not self.with_on_off: @@ -543,12 +543,12 @@ def previous_states(self) -> Optional[TemporalData]: class BusModel(ElementModel): def __init__(self, model: FlowSystemModel, element: Bus): - super().__init__(model, element) - self.element: Bus = element self.excess_input: Optional[linopy.Variable] = None self.excess_output: Optional[linopy.Variable] = None + super().__init__(model, element) - def do_modeling(self) -> None: + def _do_modeling(self) -> None: + super()._do_modeling() # inputs == outputs for flow in self.element.inputs + self.element.outputs: self.register_variable(flow.submodel.flow_rate, flow.label_full) @@ -582,12 +582,12 @@ def results_structure(self): class ComponentModel(ElementModel): def __init__(self, model: FlowSystemModel, element: Component): - super().__init__(model, element) - self.element: Component = element self.on_off: Optional[OnOffModel] = None + super().__init__(model, element) - def do_modeling(self): + def _do_modeling(self): """Initiates all FlowModels""" + super()._do_modeling() all_flows = self.element.inputs + self.element.outputs if self.element.on_off_parameters: for flow in all_flows: @@ -600,8 +600,7 @@ def do_modeling(self): flow.on_off_parameters = OnOffParameters() for flow in all_flows: - flow_model = self.register_sub_model(flow.create_model(self._model), short_name=flow.label) - flow_model.do_modeling() + self.register_sub_model(flow.create_model(self._model), short_name=flow.label) if self.element.on_off_parameters: on = self.add_variables(binary=True, short_name='on', coords=self._model.get_coords()) @@ -625,8 +624,6 @@ def do_modeling(self): short_name='on_off', ) - self.on_off.do_modeling() - if self.element.prevent_simultaneous_flows: # Simultanious Useage --> Only One FLow is On at a time, but needs a Binary for every flow on_variables = [flow.submodel.on_off.on for flow in self.element.prevent_simultaneous_flows] diff --git a/flixopt/features.py b/flixopt/features.py index 99928e410..fdf62bf75 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -36,17 +36,18 @@ def __init__( model: The optimization model instance label_of_element: The label of the parent (Element). Used to construct the full label of the model. parameters: The parameters of the feature model. - defining_variable: The variable to be invested - relative_bounds_of_defining_variable: The bounds of the variable, with respect to the minimum/maximum investment sizes label_of_model: The label of the model. This is needed to construct the full label of the model. """ - super().__init__(model, label_of_element=label_of_element, parameters=parameters, label_of_model=label_of_model) - self.piecewise_effects: Optional[PiecewiseEffectsModel] = None + super().__init__(model, label_of_element=label_of_element, parameters=parameters, label_of_model=label_of_model) + def _do_modeling(self): + super()._do_modeling() + self._create_variables_and_constraints() + self._add_effects() - def create_variables_and_constraints(self): + def _create_variables_and_constraints(self): size_min, size_max = (self.parameters.minimum_or_fixed_size, self.parameters.maximum_or_fixed_size) self.add_variables( short_name='size', @@ -79,9 +80,8 @@ def create_variables_and_constraints(self): ), short_name='segments', ) - self.piecewise_effects.do_modeling() - def add_effects(self): + def _add_effects(self): """Add investment effects""" if self.parameters.fix_effects: self._model.effects.add_share_to_effects( @@ -147,11 +147,13 @@ def __init__( previous_states: The previous flow_rates label_of_model: The label of the model. This is needed to construct the full label of the model. """ - super().__init__(model, label_of_element, parameters=parameters, label_of_model=label_of_model) self.on = on_variable self._previous_states = previous_states + super().__init__(model, label_of_element, parameters=parameters, label_of_model=label_of_model) + + def _do_modeling(self): + super()._do_modeling() - def create_variables_and_constraints(self): if self.parameters.use_off: off = self.add_variables(binary=True, short_name='off', coords=self._model.get_coords()) self.add_constraints(self.on + off == 1, short_name='complementary') @@ -207,7 +209,9 @@ def create_variables_and_constraints(self): ) #TODO: - def add_effects(self): + self._add_effects() + + def _add_effects(self): """Add operational effects""" if self.parameters.effects_per_running_hour: self._model.effects.add_share_to_effects( @@ -292,13 +296,15 @@ def __init__( label_of_model: str, as_time_series: bool = True, ): - super().__init__(model, label_of_element, label_of_model) self.inside_piece: Optional[linopy.Variable] = None self.lambda0: Optional[linopy.Variable] = None self.lambda1: Optional[linopy.Variable] = None self._as_time_series = as_time_series - def do_modeling(self): + super().__init__(model, label_of_element, label_of_model) + + def _do_modeling(self): + super()._do_modeling() dims =('time', 'year','scenario') if self._as_time_series else ('year','scenario') self.inside_piece = self.add_variables( binary=True, @@ -346,15 +352,16 @@ def __init__( zero_point: A variable that can be used to define a zero point for the Piecewise relation. If None or False, no zero point is defined. as_time_series: Whether the Piecewise relation is defined for a TimeSeries or a single variable. """ - super().__init__(model, label_of_element=label_of_element, label_of_model=label_of_model) self._piecewise_variables = piecewise_variables self._zero_point = zero_point self._as_time_series = as_time_series self.pieces: List[PieceModel] = [] self.zero_point: Optional[linopy.Variable] = None + super().__init__(model, label_of_element=label_of_element, label_of_model=label_of_model) - def do_modeling(self): + def _do_modeling(self): + super()._do_modeling() for i in range(len(list(self._piecewise_variables.values())[0])): new_piece = self.register_sub_model( PieceModel( @@ -366,7 +373,6 @@ def do_modeling(self): short_name=f'Piece_{i}', ) self.pieces.append(new_piece) - new_piece.do_modeling() for var_name in self._piecewise_variables: variable = self._model.variables[var_name] @@ -414,7 +420,6 @@ def __init__( piecewise_shares: Dict[str, Piecewise], zero_point: Optional[Union[bool, linopy.Variable]], ): - super().__init__(model, label_of_element=label_of_element, label_of_model=label_of_model) assert len(piecewise_origin[1]) == len(list(piecewise_shares.values())[0]), ( 'Piece length of variable_segments and share_segments must be equal' ) @@ -425,7 +430,9 @@ def __init__( self.piecewise_model: Optional[PiecewiseModel] = None - def do_modeling(self): + super().__init__(model, label_of_element=label_of_element, label_of_model=label_of_model) + + def _do_modeling(self): self.shares = { effect: self.add_variables(coords=self._model.get_coords(['year', 'scenario']), short_name=effect) for effect in self._piecewise_shares @@ -451,8 +458,6 @@ def do_modeling(self): short_name='PiecewiseEffects', ) - self.piecewise_model.do_modeling() - # Shares self._model.effects.add_share_to_effects( name=self.label_of_element, @@ -473,8 +478,6 @@ def __init__( max_per_hour: Optional[TemporalData] = None, min_per_hour: Optional[TemporalData] = None, ): - super().__init__(model, label_of_element=label_of_element, label_of_model=label_of_model) - if 'time' not in dims and (max_per_hour is not None or min_per_hour is not None): raise ValueError('Both max_per_hour and min_per_hour cannot be used when has_time_dim is False') @@ -493,7 +496,10 @@ def __init__( self._max_per_hour = max_per_hour if max_per_hour is not None else np.inf self._min_per_hour = min_per_hour if min_per_hour is not None else -np.inf - def do_modeling(self): + super().__init__(model, label_of_element=label_of_element, label_of_model=label_of_model) + + def _do_modeling(self): + super()._do_modeling() self.total = self.add_variables( lower=self._total_min, upper=self._total_max, diff --git a/flixopt/structure.py b/flixopt/structure.py index 76cfc2392..0a0b7d4df 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -60,13 +60,10 @@ def __init__(self, flow_system: 'FlowSystem'): def do_modeling(self): self.effects = self.flow_system.effects.create_model(self) - self.effects.do_modeling() - component_models = [component.create_model(self) for component in self.flow_system.components.values()] - bus_models = [bus.create_model(self) for bus in self.flow_system.buses.values()] - for component_model in component_models: - component_model.do_modeling() - for bus_model in bus_models: # Buses after Components, because FlowModels are created in ComponentModels - bus_model.do_modeling() + for component in self.flow_system.components.values(): + component.create_model(self) + for bus in self.flow_system.buses.values(): + bus.create_model(self) @property def solution(self): @@ -716,7 +713,9 @@ def __init__( self._constraints: Dict[str, linopy.Constraint] = {} # Mapping from short name to constraint self._sub_models: Dict[str, 'Submodel'] = {} - logger.debug(f'Created {self.__class__.__name__} "{self.label_full}"') + + logger.debug(f'Creating {self.__class__.__name__} "{self.label_full}"') + self._do_modeling() def add_variables(self, short_name: str = None, **kwargs) -> linopy.Variable: """Create and register a variable in one step""" @@ -912,6 +911,10 @@ def __repr__(self) -> str: def hours_per_step(self): return self._model.hours_per_step + def _do_modeling(self): + """Template method""" + pass + class BaseFeatureModel(Submodel): """Minimal base class for feature models that use factory patterns""" @@ -925,21 +928,8 @@ def __init__(self, model: FlowSystemModel, label_of_element: str, parameters, la Defaults to {label_of_element}|{self.__class__.__name__} parameters: The parameters of the feature model. """ - super().__init__(model, label_of_element, label_of_model or f'{label_of_element}|{self.__class__.__name__}') self.parameters = parameters - - def do_modeling(self): - """Template method - creates variables and constraints, then effects""" - self.create_variables_and_constraints() - self.add_effects() - - def create_variables_and_constraints(self): - """Override in subclasses to create variables and constraints""" - raise NotImplementedError('Subclasses must implement create_variables_and_constraints()') - - def add_effects(self): - """Override in subclasses to add effects""" - pass # Default: no effects + super().__init__(model, label_of_element, label_of_model or f'{label_of_element}|{self.__class__.__name__}') class ElementModel(Submodel): @@ -951,8 +941,8 @@ def __init__(self, model: FlowSystemModel, element: Element): model: The FlowSystemModel that is used to create the model. element: The element this model is created for. """ - super().__init__(model, label_of_element=element.label_full) self.element = element + super().__init__(model, label_of_element=element.label_full) def results_structure(self): return { From 7e043995610852eeaaa40b11dfec98c8548f25a8 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 22 Jul 2025 11:35:35 +0200 Subject: [PATCH 239/448] Make properties private --- flixopt/components.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/flixopt/components.py b/flixopt/components.py index 16e74ade0..4e69f1bcd 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -492,7 +492,7 @@ def __init__(self, model: FlowSystemModel, element: Storage): def _do_modeling(self): super()._do_modeling() - lb, ub = self.absolute_charge_state_bounds + lb, ub = self._absolute_charge_state_bounds self.add_variables( lower=lb, upper=ub, @@ -541,7 +541,7 @@ def _do_modeling(self): self, variable=self.charge_state, scaling_variable=self.investment.size, - relative_bounds=self.relative_charge_state_bounds, + relative_bounds=self._relative_charge_state_bounds, ) # Initial charge state @@ -577,8 +577,8 @@ def _initial_and_final_charge_state(self): ) @property - def absolute_charge_state_bounds(self) -> Tuple[TemporalData, TemporalData]: - relative_lower_bound, relative_upper_bound = self.relative_charge_state_bounds + def _absolute_charge_state_bounds(self) -> Tuple[TemporalData, TemporalData]: + relative_lower_bound, relative_upper_bound = self._relative_charge_state_bounds if not isinstance(self.element.capacity_in_flow_hours, InvestParameters): return ( relative_lower_bound * self.element.capacity_in_flow_hours, @@ -591,7 +591,7 @@ def absolute_charge_state_bounds(self) -> Tuple[TemporalData, TemporalData]: ) @property - def relative_charge_state_bounds(self) -> Tuple[xr.DataArray, xr.DataArray]: + def _relative_charge_state_bounds(self) -> Tuple[xr.DataArray, xr.DataArray]: """ Get relative charge state bounds with final timestep values. From 4f95ebc9ef69e8bb00d4c79a0e07d375f50f18d1 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 22 Jul 2025 11:42:19 +0200 Subject: [PATCH 240/448] Improve Inheritance of Models --- flixopt/features.py | 12 +++++++----- flixopt/modeling.py | 2 +- flixopt/structure.py | 19 +------------------ 3 files changed, 9 insertions(+), 24 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index fdf62bf75..7115c54a8 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -12,13 +12,13 @@ from .config import CONFIG from .core import NonTemporalData, Scalar, TemporalData, FlowSystemDimensions from .interface import InvestParameters, OnOffParameters, Piecewise, PiecewiseEffects -from .structure import Submodel, FlowSystemModel, BaseFeatureModel +from .structure import Submodel, FlowSystemModel from .modeling import ModelingUtilities, ModelingPrimitives, BoundingPatterns logger = logging.getLogger('flixopt') -class InvestmentModel(BaseFeatureModel): +class InvestmentModel(Submodel): """Investment model using factory patterns but keeping old interface""" def __init__( @@ -40,7 +40,8 @@ def __init__( """ self.piecewise_effects: Optional[PiecewiseEffectsModel] = None - super().__init__(model, label_of_element=label_of_element, parameters=parameters, label_of_model=label_of_model) + self.parameters = parameters + super().__init__(model, label_of_element=label_of_element, label_of_model=label_of_model) def _do_modeling(self): super()._do_modeling() @@ -123,7 +124,7 @@ def is_invested(self) -> Optional[linopy.Variable]: return self._variables['is_invested'] -class OnOffModel(BaseFeatureModel): +class OnOffModel(Submodel): """OnOff model using factory patterns""" def __init__( @@ -149,7 +150,8 @@ def __init__( """ self.on = on_variable self._previous_states = previous_states - super().__init__(model, label_of_element, parameters=parameters, label_of_model=label_of_model) + self.parameters = parameters + super().__init__(model, label_of_element, label_of_model=label_of_model) def _do_modeling(self): super()._do_modeling() diff --git a/flixopt/modeling.py b/flixopt/modeling.py index 262b0d17d..a8a0b6f44 100644 --- a/flixopt/modeling.py +++ b/flixopt/modeling.py @@ -7,7 +7,7 @@ from .config import CONFIG from .core import NonTemporalData, Scalar, TemporalData, FlowSystemDimensions -from .structure import Submodel, FlowSystemModel, BaseFeatureModel +from .structure import Submodel, FlowSystemModel logger = logging.getLogger('flixopt') diff --git a/flixopt/structure.py b/flixopt/structure.py index 0a0b7d4df..f38f04815 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -912,26 +912,9 @@ def hours_per_step(self): return self._model.hours_per_step def _do_modeling(self): - """Template method""" + """Called at the end of initialization. Override in subclasses to create variables and constraints.""" pass - -class BaseFeatureModel(Submodel): - """Minimal base class for feature models that use factory patterns""" - - def __init__(self, model: FlowSystemModel, label_of_element: str, parameters, label_of_model: Optional[str] = None): - """Initialize the BaseFeatureModel. - Args: - model: The FlowSystemModel that is used to create the model. - label_of_element: The label of the parent (Element). Used to create shares. - label_of_model: The label of the model. Used as a prefix in all variables and constraints. - Defaults to {label_of_element}|{self.__class__.__name__} - parameters: The parameters of the feature model. - """ - self.parameters = parameters - super().__init__(model, label_of_element, label_of_model or f'{label_of_element}|{self.__class__.__name__}') - - class ElementModel(Submodel): """Stores the mathematical Variables and Constraints for Elements""" From 2d9c9200d1aa9cd1e4730aec543a2764e90f5f7b Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 22 Jul 2025 16:31:36 +0200 Subject: [PATCH 241/448] V3.0.0/plotting (#285) * Use indexer to reliably plot solutions with and wihtout scenarios/years --- flixopt/results.py | 81 +++++++++++++++++++++++----------------------- 1 file changed, 40 insertions(+), 41 deletions(-) diff --git a/flixopt/results.py b/flixopt/results.py index 941d0d6dc..d042c95f1 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -652,15 +652,9 @@ def plot_heatmap( """ dataarray = self.solution[variable_name] - dataarray, suffix_parts = _apply_indexer_to_data(dataarray, indexer) - - # Create name - suffix = '--' + '-'.join(suffix_parts) if suffix_parts else '' - name = variable_name if not suffix_parts else f'{variable_name}--{'-'.join(suffix_parts)}' if suffix else variable_name - return plot_heatmap( dataarray=dataarray, - name=name, + name=variable_name, folder=self.folder, heatmap_timeframes=heatmap_timeframes, heatmap_timesteps_per_frame=heatmap_timesteps_per_frame, @@ -668,6 +662,7 @@ def plot_heatmap( save=save, show=show, engine=engine, + indexer=indexer, ) def plot_network( @@ -848,7 +843,8 @@ def plot_node_balance( show: Whether to show the plot or not. colors: The colors to use for the plot. See `flixopt.plotting.ColorType` for options. engine: The engine to use for plotting. Can be either 'plotly' or 'matplotlib'. - scenario: The scenario to plot. Defaults to the first scenario. Has no effect without scenarios present + indexer: Optional selection dict, e.g., {'scenario': 'base', 'year': 2024}. + If None, uses first value for each dimension (except time). mode: The mode to use for the dataset. Can be 'flow_rate' or 'flow_hours'. - 'flow_rate': Returns the flow_rates of the Node. - 'flow_hours': Returns the flow_hours of the Node. [flow_hours(t) = flow_rate(t) * dt(t)]. Renames suffixes to |flow_hours. @@ -856,12 +852,10 @@ def plot_node_balance( """ ds = self.node_balance(with_last_timestep=True, mode=mode, drop_suffix=drop_suffix) - title = f'{self.label} (flow rates)' if mode == 'flow_rate' else f'{self.label} (flow hours)' + ds, suffix_parts = _apply_indexer_to_data(ds, indexer) + suffix = '--' + '-'.join(suffix_parts) if suffix_parts else '' - if 'scenario' in ds.indexes: - chosen_scenario = scenario or self._calculation_results.scenarios[0] - ds = ds.sel(scenario=chosen_scenario).drop_vars('scenario') - title = f'{title} - {chosen_scenario}' + title = f'{self.label} (flow rates){suffix}' if mode == 'flow_rate' else f'{self.label} (flow hours){suffix}' if engine == 'plotly': figure_like = plotting.with_plotly( @@ -911,8 +905,8 @@ def plot_node_balance_pie( save: Whether to save the figure. show: Whether to show the figure. engine: Plotting engine to use. Only 'plotly' is implemented atm. - scenario: If scenarios are present: The scenario to plot. If None, the first scenario is used. - drop_suffix: Whether to drop the suffix from the variable names. + indexer: Optional selection dict, e.g., {'scenario': 'base', 'year': 2024}. + If None, uses first value for each dimension. """ inputs = sanitize_dataset( ds=self.solution[self.inputs] * self._calculation_results.hours_per_timestep, @@ -928,16 +922,15 @@ def plot_node_balance_pie( zero_small_values=True, drop_suffix='|', ) - inputs = inputs.sum('time') - outputs = outputs.sum('time') - title = f'{self.label} (total flow hours)' + inputs, suffix_parts = _apply_indexer_to_data(inputs, indexer) + outputs, suffix_parts = _apply_indexer_to_data(outputs, indexer) + suffix = '--' + '-'.join(suffix_parts) if suffix_parts else '' - if 'scenario' in inputs.indexes: - chosen_scenario = scenario or self._calculation_results.scenarios[0] - inputs = inputs.sel(scenario=chosen_scenario).drop_vars('scenario') - outputs = outputs.sel(scenario=chosen_scenario).drop_vars('scenario') - title = f'{title} - {chosen_scenario}' + title = f'{self.label} (total flow hours){suffix}' + + inputs = inputs.sum('time') + outputs = outputs.sum('time') if engine == 'plotly': figure_like = plotting.dual_pie_with_plotly( @@ -1060,7 +1053,8 @@ def plot_charge_state( colors: The c engine: Plotting engine to use. Only 'plotly' is implemented atm. style: The plotting mode for the flow_rate - scenario: The scenario to plot. Defaults to the first scenario. Has no effect without scenarios present + indexer: Optional selection dict, e.g., {'scenario': 'base', 'year': 2024}. + If None, uses first value for each dimension. Raises: ValueError: If the Component is not a Storage. @@ -1071,18 +1065,18 @@ def plot_charge_state( ds = self.node_balance(with_last_timestep=True) charge_state = self.charge_state - scenario_suffix = '' - if 'scenario' in ds.indexes: - chosen_scenario = scenario or self._calculation_results.scenarios[0] - ds = ds.sel(scenario=chosen_scenario).drop_vars('scenario') - charge_state = charge_state.sel(scenario=chosen_scenario).drop_vars('scenario') - scenario_suffix = f'--{chosen_scenario}' + ds, suffix_parts = _apply_indexer_to_data(ds, indexer) + charge_state, suffix_parts = _apply_indexer_to_data(charge_state, indexer) + suffix = '--' + '-'.join(suffix_parts) if suffix_parts else '' + + title=f'Operation Balance of {self.label}{suffix}' + if engine == 'plotly': fig = plotting.with_plotly( ds.to_dataframe(), colors=colors, style=style, - title=f'Operation Balance of {self.label}{scenario_suffix}', + title=title, ) # TODO: Use colors for charge state? @@ -1098,7 +1092,7 @@ def plot_charge_state( ds.to_dataframe(), colors=colors, style=style, - title=f'Operation Balance of {self.label}{scenario_suffix}', + title=title, ) charge_state = charge_state.to_dataframe() @@ -1108,7 +1102,7 @@ def plot_charge_state( return plotting.export_figure( fig, - default_path=self._calculation_results.folder / f'{self.label} (charge state){scenario_suffix}', + default_path=self._calculation_results.folder / title, default_filetype='.html', user_path=None if isinstance(save, bool) else pathlib.Path(save), show=show, @@ -1331,6 +1325,7 @@ def plot_heatmap( save: Union[bool, pathlib.Path] = False, show: bool = True, engine: plotting.PlottingEngine = 'plotly', + indexer: Optional[Dict[str, Any]] = None, ): """ Plots a heatmap of the solution of a variable. @@ -1346,6 +1341,10 @@ def plot_heatmap( show: Whether to show the plot or not. engine: The engine to use for plotting. Can be either 'plotly' or 'matplotlib'. """ + dataarray, suffix_parts = _apply_indexer_to_data(dataarray, indexer, drop=True) + suffix = '--' + '-'.join(suffix_parts) if suffix_parts else '' + name = name if not suffix_parts else name + suffix + heatmap_data = plotting.heat_map_data_from_df( dataarray.to_dataframe(name), heatmap_timeframes, heatmap_timesteps_per_frame, 'ffill' ) @@ -1609,7 +1608,7 @@ def apply_filter(array, coord_name: str, coord_values: Union[Any, List[Any]]): return da -def _apply_indexer_to_data(data: xr.DataArray, indexer: Optional[Dict[str, Any]] = None): +def _apply_indexer_to_data(data: Union[xr.DataArray, xr.Dataset], indexer: Optional[Dict[str, Any]] = None, drop=False): """ Apply indexer selection or auto-select first values for non-time dimensions. @@ -1618,14 +1617,14 @@ def _apply_indexer_to_data(data: xr.DataArray, indexer: Optional[Dict[str, Any]] indexer: Optional selection dict Returns: - Tuple of (selected_data, suffix_parts_list) + Tuple of (selected_data, selection_string) """ - suffix_parts = [] + selection_string = [] if indexer is not None: # User provided indexer - data = data.sel(indexer) - suffix_parts.extend(f"{v}[{k}]" for k, v in indexer.items()) + data = data.sel(indexer, drop=drop) + selection_string.extend(f"{v}[{k}]" for k, v in indexer.items()) else: # Auto-select first value for each dimension except 'time' selection = {} @@ -1633,8 +1632,8 @@ def _apply_indexer_to_data(data: xr.DataArray, indexer: Optional[Dict[str, Any]] if dim != 'time' and dim in data.coords: first_value = data.coords[dim].values[0] selection[dim] = first_value - suffix_parts.append(f"{first_value}[{dim}]") + selection_string.append(f"{first_value}[{dim}]") if selection: - data = data.sel(selection) + data = data.sel(selection, drop=drop) - return data, suffix_parts + return data, selection_string From 5fd05e383f5d731a21ab0e4385649314d6153817 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 22 Jul 2025 16:35:31 +0200 Subject: [PATCH 242/448] ruff check --- flixopt/aggregation.py | 2 +- flixopt/components.py | 2 +- flixopt/effects.py | 2 +- flixopt/elements.py | 5 ++--- flixopt/features.py | 6 +++--- flixopt/flow_system.py | 2 +- flixopt/modeling.py | 4 ++-- flixopt/structure.py | 4 ++-- tests/test_flow.py | 1 - 9 files changed, 13 insertions(+), 15 deletions(-) diff --git a/flixopt/aggregation.py b/flixopt/aggregation.py index e4f7a598a..411c9ede7 100644 --- a/flixopt/aggregation.py +++ b/flixopt/aggregation.py @@ -27,8 +27,8 @@ from .flow_system import FlowSystem from .structure import ( Element, - Submodel, FlowSystemModel, + Submodel, ) if TYPE_CHECKING: diff --git a/flixopt/components.py b/flixopt/components.py index 4e69f1bcd..e14e78daf 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -14,8 +14,8 @@ from .elements import Component, ComponentModel, Flow from .features import InvestmentModel, OnOffModel, PiecewiseModel from .interface import InvestParameters, OnOffParameters, PiecewiseConversion +from .modeling import BoundingPatterns from .structure import FlowSystemModel, register_class_for_io -from.modeling import BoundingPatterns if TYPE_CHECKING: from .flow_system import FlowSystem diff --git a/flixopt/effects.py b/flixopt/effects.py index b5ea81e3e..d363cd9fd 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -15,7 +15,7 @@ from .core import Scalar, TemporalData, TemporalDataUser from .features import ShareAllocationModel -from .structure import Element, ElementModel, Interface, Submodel, FlowSystemModel, register_class_for_io +from .structure import Element, ElementModel, FlowSystemModel, Interface, Submodel, register_class_for_io if TYPE_CHECKING: from .flow_system import FlowSystem diff --git a/flixopt/elements.py b/flixopt/elements.py index bd4c27eca..0d403f78d 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -13,10 +13,10 @@ from .config import CONFIG from .core import PlausibilityError, Scalar, TemporalData, TemporalDataUser from .effects import TemporalEffectsUser -from .features import InvestmentModel, OnOffModel, ModelingPrimitives +from .features import InvestmentModel, ModelingPrimitives, OnOffModel from .interface import InvestParameters, OnOffParameters -from .structure import Element, ElementModel, FlowSystemModel, register_class_for_io from .modeling import BoundingPatterns, ModelingUtilitiesAbstract +from .structure import Element, ElementModel, FlowSystemModel, register_class_for_io if TYPE_CHECKING: from .flow_system import FlowSystem @@ -626,7 +626,6 @@ def _do_modeling(self): if self.element.prevent_simultaneous_flows: # Simultanious Useage --> Only One FLow is On at a time, but needs a Binary for every flow - on_variables = [flow.submodel.on_off.on for flow in self.element.prevent_simultaneous_flows] ModelingPrimitives.mutual_exclusivity_constraint( self, binary_variables=[flow.submodel.on_off.on for flow in self.element.prevent_simultaneous_flows], diff --git a/flixopt/features.py b/flixopt/features.py index 7115c54a8..fe9ff9c1c 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -10,10 +10,10 @@ import numpy as np from .config import CONFIG -from .core import NonTemporalData, Scalar, TemporalData, FlowSystemDimensions +from .core import FlowSystemDimensions, NonTemporalData, Scalar, TemporalData from .interface import InvestParameters, OnOffParameters, Piecewise, PiecewiseEffects -from .structure import Submodel, FlowSystemModel -from .modeling import ModelingUtilities, ModelingPrimitives, BoundingPatterns +from .modeling import BoundingPatterns, ModelingPrimitives, ModelingUtilities +from .structure import FlowSystemModel, Submodel logger = logging.getLogger('flixopt') diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 0a10b3ceb..42faadcf6 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -32,7 +32,7 @@ TemporalEffectsUser, ) from .elements import Bus, Component, Flow -from .structure import Element, Interface, FlowSystemModel +from .structure import Element, FlowSystemModel, Interface if TYPE_CHECKING: import pyvis diff --git a/flixopt/modeling.py b/flixopt/modeling.py index a8a0b6f44..f67fc9d19 100644 --- a/flixopt/modeling.py +++ b/flixopt/modeling.py @@ -6,8 +6,8 @@ import xarray as xr from .config import CONFIG -from .core import NonTemporalData, Scalar, TemporalData, FlowSystemDimensions -from .structure import Submodel, FlowSystemModel +from .core import FlowSystemDimensions, NonTemporalData, Scalar, TemporalData +from .structure import FlowSystemModel, Submodel logger = logging.getLogger('flixopt') diff --git a/flixopt/structure.py b/flixopt/structure.py index f38f04815..16e8ad0dd 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -8,7 +8,7 @@ import logging import pathlib from io import StringIO -from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Tuple, Union, Collection +from typing import TYPE_CHECKING, Any, Collection, Dict, List, Literal, Optional, Tuple, Union import linopy import numpy as np @@ -19,7 +19,7 @@ from . import io as fx_io from .config import CONFIG -from .core import NonTemporalData, Scalar, TemporalDataUser, TimeSeriesData, get_dataarray_stats, FlowSystemDimensions +from .core import FlowSystemDimensions, NonTemporalData, Scalar, TemporalDataUser, TimeSeriesData, get_dataarray_stats if TYPE_CHECKING: # for type checking and preventing circular imports from .effects import EffectCollectionModel diff --git a/tests/test_flow.py b/tests/test_flow.py index 43ecbe34f..d22d81993 100644 --- a/tests/test_flow.py +++ b/tests/test_flow.py @@ -641,7 +641,6 @@ def test_consecutive_on_hours_previous(self, basic_flow_system_linopy): 'Sink(Wärme)|consecutive_on_hours|forward', 'Sink(Wärme)|consecutive_on_hours|backward', 'Sink(Wärme)|consecutive_on_hours|initial', - 'Sink(Wärme)|consecutive_on_hours|lb', }.issubset(set(flow.submodel.constraints)) assert_var_equal( From 20a1964b2f592a00828ac2242165208979f5263c Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 22 Jul 2025 16:39:16 +0200 Subject: [PATCH 243/448] Improve typehints --- flixopt/effects.py | 2 ++ flixopt/elements.py | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/flixopt/effects.py b/flixopt/effects.py index d363cd9fd..9c5b60f36 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -138,6 +138,8 @@ def _plausibility_checks(self) -> None: class EffectModel(ElementModel): + element: Effect # Type hint + def __init__(self, model: FlowSystemModel, element: Effect): super().__init__(model, element) diff --git a/flixopt/elements.py b/flixopt/elements.py index 0d403f78d..2d29a4f2d 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -313,6 +313,8 @@ def invest_is_optional(self) -> bool: class FlowModel(ElementModel): + element: Flow # Type hint + def __init__(self, model: FlowSystemModel, element: Flow): super().__init__(model, element) @@ -542,6 +544,8 @@ def previous_states(self) -> Optional[TemporalData]: class BusModel(ElementModel): + element: Bus # Type hint + def __init__(self, model: FlowSystemModel, element: Bus): self.excess_input: Optional[linopy.Variable] = None self.excess_output: Optional[linopy.Variable] = None @@ -581,6 +585,8 @@ def results_structure(self): class ComponentModel(ElementModel): + element: Component # Type hint + def __init__(self, model: FlowSystemModel, element: Component): self.on_off: Optional[OnOffModel] = None super().__init__(model, element) From 9b05f8fcb51bd3dbde01c2ce33c83eb59c02bb3f Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 22 Jul 2025 17:07:00 +0200 Subject: [PATCH 244/448] Update CHANGELOG.md --- CHANGELOG.md | 75 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 47 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 31d6a526a..3404a142f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,45 +7,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased - New Model dimensions] -## What's New -### Scenarios +### Changed +* **BREAKING**: `relative_minimum_charge_state` and `relative_maximum_charge_state` don't have an extra timestep anymore. The final charge state can now be constrained by parameters `relative_minimum_final_charge_state` and `relative_maximum_final_charge_state` instead +* **BREAKING**: Calculation.do_modeling() now returns the Calculation object instead of its linopy.Model +* **BREAKING**: Renamed class `SystemModel` to `FlowSystemModel` +* **BREAKING**: Renamed class `Model` to `Submodel` +* FlowSystems can not be shared across multiple Calculations anymore. A copy of the FlowSystem is created instead, making every Calculation independent +* Each Subcalculation in `SegmentedCalculation` now has its own distinct `FlowSystem` object +* Type system overhaul - added clear separation between temporal and non-temporal data throughout codebase for better clarity +* Enhanced FlowSystem interface with improved `__repr__()` and `__str__()` methods + +#### Internal: +* **BREAKING**: Calculation.do_modeling() now returns the Calculation object instead of its linopy.Model +* **BREAKING**: Renamed class `SystemModel` to `FlowSystemModel` +* **BREAKING**: Renamed class `Model` to `Submodel` +* FlowSystem data management simplified - removed `time_series_collection` pattern in favor of direct timestep properties +* Change modeling hierarchy to allow for more flexibility in future development. This leads to minimal changes in the access and creation of Submodels and their variables. +* Added new module `.modeling`that contains Modelling primitives and utilities + + +### Added + +#### Scenarios Scenarios are a new feature of flixopt. They can be used to model uncertainties in the flow system, such as: * Different demand profiles * Different price forecasts * Different weather conditions -They might also be used to model an evolving system with multiple investment periods. Each **scenario** might be a new year, a new month, or a new day, with a different set of investment decisions to take. - -The weighted sum of the total objective effect of each scenario is used as the objective of the optimization. -#### Investments and scenarios -Scenarios allow for more flexibility in investment decisions. -You can decide to allow different investment decisions for each scenario, or to allow a single investment decision for a subset of all scenarios, while not allowing for an invest in others. -This enables the following use cases: -* Find the best investment decision for each scenario individually +Common use cases are: * Find the best overall investment decision for possible scenarios (robust decision-making) -* Find the best overall investment decision for a subset of all scenarios - -The last one might be useful if you want to model a system with multiple investment periods, where one investment decision is made for more than one scenario. -This might occur when scenarios represent years or months, while an investment decision influences the system for multiple years or months. - - -### Other new features -* Balanced storage - Storage charging and discharging sizes can now be forced to be equal in when optimizing their size. -* Feature 2 - Description +* Find the best dispatch for the most important assets under uncertain price and weather conditions +The weighted sum of the total objective effect of each scenario is used as the objective of the optimization. -## [Unreleased - Data Management and IO] +#### Years (Investment periods) +A flixopt model might be modeled with a "year" dimension. +This enables to model transformation pathways over multiple years. -### Changed -* **BREAKING**: `relative_minimum_charge_state` and `relative_maximum_charge_state` don't have an extra timestep anymore. The final charge state can now be constrained by parameters `relative_minimum_final_charge_state` and `relative_maximum_final_charge_state` instead -* **BREAKING**: Calculation.do_modeling() now returns the Calculation object instead of its linopy.Model -* FlowSystems can not be shared across multiple Calculations anymore. A copy of the FlowSystem is created instead, making every Calculation independent -* Type system overhaul - added clear separation between temporal and non-temporal data throughout codebase for better clarity -* FlowSystem data management simplified - removed `time_series_collection` pattern in favor of direct timestep properties -* Enhanced FlowSystem interface with improved `__repr__()` and `__str__()` methods +%%%%% TODO: New Interfaces to model sizes changing over time, annuity, etc. -### Added +#### Improved Data handling: IO, resampling and more through xarray * Complete serialization infrastructure through `Interface` base class * IO for all Interfaces and the FlowSystem with round-trip serialization support * Automatic DataArray extraction and restoration @@ -68,21 +70,38 @@ This might occur when scenarios represent years or months, while an investment d * `fit_effects_to_model_coords()` method for effect data processing * `connect_and_transform()` method replacing several operations +#### Internal: Improved Model organisation and access +* Clearer separation between the main Model and "Submodels" +* Improved access to the Submodels and their variables, constraints and submodels +* Added __repr__() for Submodels to easily inspect its content +* + + +#### Other new features +* Balanced storage - Storage charging and discharging sizes can now be forced to be equal in when optimizing their size. + +#### Examples: +* Added Example for 2-stage Investment decisions leveraging the resampling of a FlowSystem + + ### Fixed * Enhanced NetCDF I/O with proper attribute preservation for DataArrays * Improved error handling and validation in serialization processes * Better type consistency across all framework components + ### Know Issues * Plotly >= 6 may raise errors if "nbformat" is not installed. We pinned plotly to <6, but this may be fixed in the future. * IO for single Interfaces/Elemenets to Datasets might not work properly if the Interface/Element is not part of a fully transformed and connected FlowSystem. This arrises from Numeric Data not being stored as xr.DataArray by the user. To avoid this, always use the `to_dataset()` on Elements inside a FlowSystem thats connected and transformed. + ### Deprecated * The `agg_group` and `agg_weight` parameters of `TimeSeriesData` are deprecated and will be removed in a future version. Use `aggregation_group` and `aggregation_weight` instead. * The `active_timesteps` parameter of `Calculation` is deprecated and will be removed in a future version. Use the new `sel(time=...)` method on the FlowSystem instead. * The assignment of Bus Objects to Flow.bus is deprecated and will be removed in a future version. Use the label of the Bus instead. * The usage of Effects objects in Dicts to assign shares to Effects is deprecated and will be removed in a future version. Use the label of the Effect instead. + ## [2.1.5] - 2025-07-08 ### Fixed From 4d7fd29d3557be215474bbeff135cef016e9f6e9 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 22 Jul 2025 17:13:08 +0200 Subject: [PATCH 245/448] Bugfix from renaming to .submodel --- flixopt/calculation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flixopt/calculation.py b/flixopt/calculation.py index d50bde388..874e2eab7 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -81,7 +81,7 @@ def __init__( flow_system._used_in_calculation = True self.flow_system = flow_system - self.submodel: Optional[FlowSystemModel] = None + self.model: Optional[FlowSystemModel] = None self.durations = {'modeling': 0.0, 'solving': 0.0, 'saving': 0.0} self.folder = pathlib.Path.cwd() / 'results' if folder is None else pathlib.Path(folder) @@ -540,7 +540,7 @@ def _transfer_start_values(self, i: int): for current_comp in current_flow_system.components.values(): next_comp = next_flow_system.components[current_comp.label_full] if isinstance(next_comp, Storage): - next_comp.initial_charge_state = current_comp.model.charge_state.solution.sel(time=start).item() + next_comp.initial_charge_state = current_comp.submodel.charge_state.solution.sel(time=start).item() start_values_of_this_segment[current_comp.label_full] = next_comp.initial_charge_state self._transfered_start_values.append(start_values_of_this_segment) From 3626517ad8429c4095b6a99ea8d494c7bd331030 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 23 Jul 2025 08:50:36 +0200 Subject: [PATCH 246/448] Bugfix from renaming to .submodel --- examples/03_Calculation_types/example_calculation_types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/03_Calculation_types/example_calculation_types.py b/examples/03_Calculation_types/example_calculation_types.py index 9f5828cec..8bbdf1773 100644 --- a/examples/03_Calculation_types/example_calculation_types.py +++ b/examples/03_Calculation_types/example_calculation_types.py @@ -188,7 +188,7 @@ def get_solutions(calcs: List, variable: str) -> xr.Dataset: if calc.name == 'Segmented': dataarrays.append(calc.results.solution_without_overlap(variable).rename(calc.name)) else: - dataarrays.append(calc.results.submodel.variables[variable].solution.rename(calc.name)) + dataarrays.append(calc.results.model.variables[variable].solution.rename(calc.name)) return xr.merge(dataarrays) # --- Plotting for comparison --- From 50fbb67d8bc2c92c1f9ae016e71ebca6719258ad Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 23 Jul 2025 08:50:54 +0200 Subject: [PATCH 247/448] Improve indexer in results plotting --- flixopt/results.py | 43 +++++++++++++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/flixopt/results.py b/flixopt/results.py index d042c95f1..361e7cfde 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -626,7 +626,8 @@ def plot_heatmap( show: Whether to show the plot or not. engine: The engine to use for plotting. Can be either 'plotly' or 'matplotlib'. indexer: Optional selection dict, e.g., {'scenario': 'base', 'year': 2024}. - If None, uses first value for each dimension. + If None, uses first value for each dimension. + If empty dict {}, uses all values. Examples: Basic usage (uses first scenario, first year, all time): @@ -844,15 +845,16 @@ def plot_node_balance( colors: The colors to use for the plot. See `flixopt.plotting.ColorType` for options. engine: The engine to use for plotting. Can be either 'plotly' or 'matplotlib'. indexer: Optional selection dict, e.g., {'scenario': 'base', 'year': 2024}. - If None, uses first value for each dimension (except time). + If None, uses first value for each dimension (except time). + If empty dict {}, uses all values. mode: The mode to use for the dataset. Can be 'flow_rate' or 'flow_hours'. - 'flow_rate': Returns the flow_rates of the Node. - 'flow_hours': Returns the flow_hours of the Node. [flow_hours(t) = flow_rate(t) * dt(t)]. Renames suffixes to |flow_hours. drop_suffix: Whether to drop the suffix from the variable names. """ - ds = self.node_balance(with_last_timestep=True, mode=mode, drop_suffix=drop_suffix) + ds = self.node_balance(with_last_timestep=True, mode=mode, drop_suffix=drop_suffix, indexer=indexer) - ds, suffix_parts = _apply_indexer_to_data(ds, indexer) + ds, suffix_parts = _apply_indexer_to_data(ds, indexer, drop=True) suffix = '--' + '-'.join(suffix_parts) if suffix_parts else '' title = f'{self.label} (flow rates){suffix}' if mode == 'flow_rate' else f'{self.label} (flow hours){suffix}' @@ -906,7 +908,8 @@ def plot_node_balance_pie( show: Whether to show the figure. engine: Plotting engine to use. Only 'plotly' is implemented atm. indexer: Optional selection dict, e.g., {'scenario': 'base', 'year': 2024}. - If None, uses first value for each dimension. + If None, uses first value for each dimension. + If empty dict {}, uses all values. """ inputs = sanitize_dataset( ds=self.solution[self.inputs] * self._calculation_results.hours_per_timestep, @@ -923,8 +926,8 @@ def plot_node_balance_pie( drop_suffix='|', ) - inputs, suffix_parts = _apply_indexer_to_data(inputs, indexer) - outputs, suffix_parts = _apply_indexer_to_data(outputs, indexer) + inputs, suffix_parts = _apply_indexer_to_data(inputs, indexer, drop=True) + outputs, suffix_parts = _apply_indexer_to_data(outputs, indexer, drop=True) suffix = '--' + '-'.join(suffix_parts) if suffix_parts else '' title = f'{self.label} (total flow hours){suffix}' @@ -976,6 +979,7 @@ def node_balance( with_last_timestep: bool = False, mode: Literal['flow_rate', 'flow_hours'] = 'flow_rate', drop_suffix: bool = False, + indexer: Optional[Dict['FlowSystemDimensions', Any]] = None, ) -> xr.Dataset: """ Returns a dataset with the node balance of the Component or Bus. @@ -988,6 +992,9 @@ def node_balance( - 'flow_rate': Returns the flow_rates of the Node. - 'flow_hours': Returns the flow_hours of the Node. [flow_hours(t) = flow_rate(t) * dt(t)]. Renames suffixes to |flow_hours. drop_suffix: Whether to drop the suffix from the variable names. + indexer: Optional selection dict, e.g., {'scenario': 'base', 'year': 2024}. + If None, uses first value for each dimension. + If empty dict {}, uses all values. """ ds = self.solution[self.inputs + self.outputs] @@ -1007,6 +1014,8 @@ def node_balance( drop_suffix='|' if drop_suffix else None, ) + ds, _ = _apply_indexer_to_data(ds, indexer, drop=True) + if mode == 'flow_hours': ds = ds * self._calculation_results.hours_per_timestep ds = ds.rename_vars({var: var.replace('flow_rate', 'flow_hours') for var in ds.data_vars}) @@ -1054,7 +1063,8 @@ def plot_charge_state( engine: Plotting engine to use. Only 'plotly' is implemented atm. style: The plotting mode for the flow_rate indexer: Optional selection dict, e.g., {'scenario': 'base', 'year': 2024}. - If None, uses first value for each dimension. + If None, uses first value for each dimension. + If empty dict {}, uses all values. Raises: ValueError: If the Component is not a Storage. @@ -1062,11 +1072,11 @@ def plot_charge_state( if not self.is_storage: raise ValueError(f'Cant plot charge_state. "{self.label}" is not a storage') - ds = self.node_balance(with_last_timestep=True) + ds = self.node_balance(with_last_timestep=True, indexer=indexer) charge_state = self.charge_state - ds, suffix_parts = _apply_indexer_to_data(ds, indexer) - charge_state, suffix_parts = _apply_indexer_to_data(charge_state, indexer) + ds, suffix_parts = _apply_indexer_to_data(ds, indexer, drop=True) + charge_state, suffix_parts = _apply_indexer_to_data(charge_state, indexer, drop=True) suffix = '--' + '-'.join(suffix_parts) if suffix_parts else '' title=f'Operation Balance of {self.label}{suffix}' @@ -1340,6 +1350,9 @@ def plot_heatmap( save: Whether to save the plot or not. If a path is provided, the plot will be saved at that location. show: Whether to show the plot or not. engine: The engine to use for plotting. Can be either 'plotly' or 'matplotlib'. + indexer: Optional selection dict, e.g., {'scenario': 'base', 'year': 2024}. + If None, uses first value for each dimension. + If empty dict {}, uses all values. """ dataarray, suffix_parts = _apply_indexer_to_data(dataarray, indexer, drop=True) suffix = '--' + '-'.join(suffix_parts) if suffix_parts else '' @@ -1608,13 +1621,19 @@ def apply_filter(array, coord_name: str, coord_values: Union[Any, List[Any]]): return da -def _apply_indexer_to_data(data: Union[xr.DataArray, xr.Dataset], indexer: Optional[Dict[str, Any]] = None, drop=False): +def _apply_indexer_to_data( + data: Union[xr.DataArray, xr.Dataset], + indexer: Optional[Dict[str, Any]] = None, + drop=False + ) -> Tuple[Union[xr.DataArray, xr.Dataset], List[str]]: """ Apply indexer selection or auto-select first values for non-time dimensions. Args: data: xarray Dataset or DataArray indexer: Optional selection dict + If None, uses first value for each dimension (except time). + If empty dict {}, uses all values. Returns: Tuple of (selected_data, selection_string) From 9368985bb6f9b07a40a5afb52c2f09d2be31d5b5 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 23 Jul 2025 10:18:48 +0200 Subject: [PATCH 248/448] rename register_submodel() to .add_submodels() adn add SUbmodels collection class --- flixopt/calculation.py | 6 +- flixopt/components.py | 8 +-- flixopt/effects.py | 6 +- flixopt/elements.py | 16 +++--- flixopt/features.py | 6 +- flixopt/structure.py | 123 ++++++++++++++++++++++++++++++----------- 6 files changed, 111 insertions(+), 54 deletions(-) diff --git a/flixopt/calculation.py b/flixopt/calculation.py index 874e2eab7..3137b71ec 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -116,13 +116,13 @@ def main_results(self) -> Dict[str, Union[Scalar, Dict]]: 'Invested': { model.label_of_element: model.size.solution for component in self.flow_system.components.values() - for model in component.submodel.submodels + for model in component.submodel.all_submodels if isinstance(model, InvestmentModel) and model.size.solution.max() >= CONFIG.modeling.EPSILON }, 'Not invested': { model.label_of_element: model.size.solution for component in self.flow_system.components.values() - for model in component.submodel.submodels + for model in component.submodel.all_submodels if isinstance(model, InvestmentModel) and model.size.solution.max() < CONFIG.modeling.EPSILON }, }, @@ -488,7 +488,7 @@ def do_modeling_and_solve( invest_elements = [ model.label_full for component in calculation.flow_system.components.values() - for model in component.submodel.submodels + for model in component.submodel.all_submodels if isinstance(model, InvestmentModel) ] if invest_elements: diff --git a/flixopt/components.py b/flixopt/components.py index e14e78daf..495edb5d7 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -470,7 +470,7 @@ def _do_modeling(self): for flow, piecewise in self.element.piecewise_conversion.items() } - self.piecewise_conversion = self.register_sub_model( + self.piecewise_conversion = self.add_submodels( PiecewiseModel( model=self._model, label_of_element=self.label_of_element, @@ -527,7 +527,7 @@ def _do_modeling(self): ) if isinstance(self.element.capacity_in_flow_hours, InvestParameters): - self.register_sub_model( + self.add_submodels( InvestmentModel( model=self._model, label_of_element=self.label_of_element, @@ -627,9 +627,9 @@ def _investment(self) -> Optional[InvestmentModel]: @property def investment(self) -> Optional[InvestmentModel]: """OnOff feature""" - if 'investment' not in self.sub_models_direct: + if 'investment' not in self.submodels: return None - return self.sub_models_direct['investment'] + return self.submodels['investment'] @property def charge_state(self) -> linopy.Variable: diff --git a/flixopt/effects.py b/flixopt/effects.py index 9c5b60f36..db72ff8aa 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -145,7 +145,7 @@ def __init__(self, model: FlowSystemModel, element: Effect): def _do_modeling(self): self.total: Optional[linopy.Variable] = None - self.invest: ShareAllocationModel = self.register_sub_model( + self.invest: ShareAllocationModel = self.add_submodels( ShareAllocationModel( model=self._model, dims=('year', 'scenario'), @@ -157,7 +157,7 @@ def _do_modeling(self): short_name='invest', ) - self.operation: ShareAllocationModel = self.register_sub_model( + self.operation: ShareAllocationModel = self.add_submodels( ShareAllocationModel( model=self._model, dims=('time', 'year', 'scenario'), @@ -415,7 +415,7 @@ def _do_modeling(self): super()._do_modeling() for effect in self.effects: effect.create_model(self._model) - self.penalty = self.register_sub_model( + self.penalty = self.add_submodels( ShareAllocationModel(self._model, dims=(), label_of_element='Penalty'), short_name='penalty', ) diff --git a/flixopt/elements.py b/flixopt/elements.py index 2d29a4f2d..7acf8b0a9 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -351,7 +351,7 @@ def _do_modeling(self): def _create_on_off_model(self): on = self.add_variables(binary=True, short_name='on', coords=self._model.get_coords()) - self.register_sub_model( + self.add_submodels( OnOffModel( model=self._model, label_of_element=self.label_of_element, @@ -364,7 +364,7 @@ def _create_on_off_model(self): ) def _create_investment_model(self): - self.register_sub_model( + self.add_submodels( InvestmentModel( model=self._model, label_of_element=self.label_of_element, @@ -509,9 +509,9 @@ def absolute_flow_rate_bounds(self) -> Tuple[TemporalData, TemporalData]: @property def on_off(self) -> Optional[OnOffModel]: """OnOff feature""" - if 'on_off' not in self.sub_models_direct: + if 'on_off' not in self.submodels: return None - return self.sub_models_direct['on_off'] + return self.submodels['on_off'] @property def _investment(self) -> Optional[InvestmentModel]: @@ -521,9 +521,9 @@ def _investment(self) -> Optional[InvestmentModel]: @property def investment(self) -> Optional[InvestmentModel]: """OnOff feature""" - if 'investment' not in self.sub_models_direct: + if 'investment' not in self.submodels: return None - return self.sub_models_direct['investment'] + return self.submodels['investment'] @property def previous_states(self) -> Optional[TemporalData]: @@ -606,7 +606,7 @@ def _do_modeling(self): flow.on_off_parameters = OnOffParameters() for flow in all_flows: - self.register_sub_model(flow.create_model(self._model), short_name=flow.label) + self.add_submodels(flow.create_model(self._model), short_name=flow.label) if self.element.on_off_parameters: on = self.add_variables(binary=True, short_name='on', coords=self._model.get_coords()) @@ -618,7 +618,7 @@ def _do_modeling(self): self.add_constraints(on <= sum(flow_ons) + CONFIG.modeling.EPSILON, short_name='on|ub') self.add_constraints(on >= sum(flow_ons) / (len(flow_ons) + CONFIG.modeling.EPSILON), short_name='on|lb') - self.on_off = self.register_sub_model( + self.on_off = self.add_submodels( OnOffModel( model=self._model, label_of_element=self.label_of_element, diff --git a/flixopt/features.py b/flixopt/features.py index fe9ff9c1c..0c7606120 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -70,7 +70,7 @@ def _create_variables_and_constraints(self): ) if self.parameters.piecewise_effects: - self.piecewise_effects = self.register_sub_model( + self.piecewise_effects = self.add_submodels( PiecewiseEffectsModel( model=self._model, label_of_element=self.label_of_element, @@ -365,7 +365,7 @@ def __init__( def _do_modeling(self): super()._do_modeling() for i in range(len(list(self._piecewise_variables.values())[0])): - new_piece = self.register_sub_model( + new_piece = self.add_submodels( PieceModel( model=self._model, label_of_element=self.label_of_element, @@ -448,7 +448,7 @@ def _do_modeling(self): }, } - self.piecewise_model = self.register_sub_model( + self.piecewise_model = self.add_submodels( PiecewiseModel( model=self._model, label_of_element=self.label_of_element, diff --git a/flixopt/structure.py b/flixopt/structure.py index 16e8ad0dd..889b6e7aa 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -7,8 +7,9 @@ import json import logging import pathlib +from dataclasses import dataclass from io import StringIO -from typing import TYPE_CHECKING, Any, Collection, Dict, List, Literal, Optional, Tuple, Union +from typing import TYPE_CHECKING, Any, Collection, Dict, List, Literal, Optional, Tuple, Union, Protocol, runtime_checkable, ItemsView, Iterator import linopy import numpy as np @@ -43,7 +44,34 @@ def register_class_for_io(cls): return cls -class FlowSystemModel(linopy.Model): +class SubmodelsMixin: + """Mixin that provides submodel functionality for both FlowSystemModel and Submodel.""" + + submodels: 'Submodels' + @property + def all_submodels(self) -> List['Submodel']: + """Get all submodels including nested ones recursively.""" + direct_submodels = list(self.submodels.values()) + + # Recursively collect nested sub-models + nested_submodels = [] + for submodel in direct_submodels: + nested_submodels.extend(submodel.all_submodels) + + return direct_submodels + nested_submodels + + def add_submodels(self, submodel: 'Submodel', short_name: str = None) -> 'Submodel': + """Register a sub-model with the model""" + if short_name is None: + short_name = submodel.__class__.__name__ + if short_name in self.submodels: + raise ValueError(f'Short name "{short_name}" already assigned to model') + self.submodels.add(submodel, name=short_name) + + return submodel + + +class FlowSystemModel(linopy.Model, SubmodelsMixin): """ The FlowSystemModel is the linopy Model that is used to create the mathematical model of the flow_system. It is used to create and store the variables and constraints for the flow_system. @@ -57,6 +85,7 @@ def __init__(self, flow_system: 'FlowSystem'): super().__init__(force_dim_names=True) self.flow_system = flow_system self.effects: Optional[EffectCollectionModel] = None + self.submodels: Submodels = Submodels({}) def do_modeling(self): self.effects = self.flow_system.effects.create_model(self) @@ -693,7 +722,7 @@ def _valid_label(label: str) -> str: return label -class Submodel: +class Submodel(SubmodelsMixin): """Stores Variables and Constraints.""" def __init__( @@ -711,8 +740,7 @@ def __init__( self._variables: Dict[str, linopy.Variable] = {} # Mapping from short name to variable self._constraints: Dict[str, linopy.Constraint] = {} # Mapping from short name to constraint - self._sub_models: Dict[str, 'Submodel'] = {} - + self.submodels: Submodels = Submodels({}) logger.debug(f'Creating {self.__class__.__name__} "{self.label_full}"') self._do_modeling() @@ -759,15 +787,6 @@ def register_constraint(self, constraint: linopy.Constraint, short_name: str = N self._constraints[short_name] = constraint return constraint - def register_sub_model(self, submodel: 'Submodel', short_name: str) -> 'Submodel': - """Register a sub-model with the model""" - if short_name is None: - short_name = submodel.__class__.__name__ - if short_name in self._sub_models: - raise ValueError(f'Short name "{short_name}" already assigned to model') - self._sub_models[short_name] = submodel - return submodel - def __getitem__(self, key: str) -> linopy.Variable: """Get a variable by its short name""" if key in self._variables: @@ -829,29 +848,12 @@ def constraints_direct(self) -> linopy.Constraints: """Costraints of the model, excluding those of sub-models""" return self._model.constraints[[con.name for con in self._constraints.values()]] - @property - def sub_models_direct(self) -> Dict[str, 'Submodel']: - """All sub-models of the model, excluding those of sub-models""" - return self._sub_models - - @property - def submodels(self) -> List['Submodel']: - """All sub-models of the model""" - direct_submodels = list(self._sub_models.values()) - - # Recursively collect nested sub-models - nested_submodels = [] - for submodel in direct_submodels: - nested_submodels.extend(submodel.submodels) # This calls the property recursively - - return direct_submodels + nested_submodels - @property def constraints(self) -> linopy.Constraints: """All constraints of the model, including those of sub-models""" names = list(self.constraints_direct) + [ constraint_name - for submodel in self.submodels + for submodel in self.submodels.values() for constraint_name in submodel.constraints_direct ] @@ -862,7 +864,7 @@ def variables(self) -> linopy.Variables: """All variables of the model, including those of sub-models""" names = list(self.variables_direct) + [ variable_name - for submodel in self.submodels + for submodel in self.submodels.values() for variable_name in submodel.variables_direct ] @@ -915,6 +917,61 @@ def _do_modeling(self): """Called at the end of initialization. Override in subclasses to create variables and constraints.""" pass + +@dataclass(repr=False) +class Submodels: + """A simple collection for storing submodels with easy access and representation.""" + + data: Dict[str, 'Submodel'] + + def __getitem__(self, name: str) -> 'Submodel': + """Get a submodel by its name.""" + return self.data[name] + + def __getattr__(self, name: str) -> 'Submodel': + """Get a submodel by attribute access.""" + if name in self.data: + return self.data[name] + raise AttributeError(f"Submodels has no attribute '{name}'") + + def __len__(self) -> int: + return len(self.data) + + def __iter__(self) -> Iterator[str]: + return iter(self.data) + + def __contains__(self, name: str) -> bool: + return name in self.data + + def __repr__(self) -> str: + """Simple representation of the submodels collection.""" + if not self.data: + return 'Submodels:\n----------\n \n' + + sub_models_string = '' + for name, submodel in self.data.items(): + sub_models_string += f'\n * {name} ({submodel.__class__.__name__}) [{len(submodel.variables)} Vars + {len(submodel.constraints)} Cons)' + + return f'Submodels:\n----------{sub_models_string}\n' + + def items(self) -> ItemsView[str, 'Submodel']: + return self.data.items() + + def keys(self): + return self.data.keys() + + def values(self): + return self.data.values() + + def add(self, submodel: 'Submodel', name: str) -> None: + """Add a submodel to the collection.""" + self.data[name] = submodel + + def get(self, name: str, default=None): + """Get submodel by name, returning default if not found.""" + return self.data.get(name, default) + + class ElementModel(Submodel): """Stores the mathematical Variables and Constraints for Elements""" From e4ec4107fe4c01042aee08cdbb667129a0729d06 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 23 Jul 2025 11:47:04 +0200 Subject: [PATCH 249/448] Add nice repr to FlowSystemModel and Submodel --- flixopt/structure.py | 72 +++++++++++++++++++++++++++----------------- 1 file changed, 44 insertions(+), 28 deletions(-) diff --git a/flixopt/structure.py b/flixopt/structure.py index 889b6e7aa..94f5f2bda 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -169,6 +169,32 @@ def weights(self) -> Union[int, xr.DataArray]: return self.flow_system.weights + def __repr__(self) -> str: + """ + Return a string representation of the FlowSystemModel, borrowed from linopy.Model. + """ + # Extract content from existing representations + sections = { + f'Variables: [{len(self.variables)}]': self.variables.__repr__().split('\n', 2)[2], + f'Constraints: [{len(self.constraints)}]': self.constraints.__repr__().split('\n', 2)[2], + f'Submodels: [{len(self.submodels)}]': self.submodels.__repr__().split('\n', 2)[2], + 'Status': self.status, + } + + # Format sections with headers and underlines + formatted_sections = [] + for section_header, section_content in sections.items(): + formatted_sections.append( + (f'{section_header}\n' + f'{"-" * len(section_header)}\n' + f'{section_content}') + ) + + title = f'FlowSystemModel ({self.type})' + return (f'{title}\n' + f'{"=" * len(title)}\n\n' + f'{"\n".join(formatted_sections)}') + class Interface: """ @@ -874,40 +900,27 @@ def __repr__(self) -> str: """ Return a string representation of the linopy model. """ - # Extract content from variables and constraints representations - var_string = self.variables.__repr__().split('\n', 2)[2] - con_string = self.constraints.__repr__().split('\n', 2)[2] - model_string = f'Submodel of Linopy {self._model.type}:' - - # Build submodels section - if len(self.submodels) == 0: - sub_models_string = ' \n' - else: - submodel_lines = [] - for submodel_name, submodel in self.sub_models_direct.items(): - class_name = submodel.__class__.__name__ - submodel_lines.append(f' * {class_name}: "{submodel_name}" [{len(submodel.variables)} Vars + {len(submodel.constraints)} Cons]') - sub_models_string = '\n' + '\n'.join(submodel_lines) - - # Create sections with counts and content + # Extract content from existing representations sections = { - f'Variables: [{len(self.variables)}/{len(self._model.variables)}]': var_string, - f'Constraints: [{len(self.constraints)}/{len(self._model.constraints)}]': con_string, - f'Submodels: [{len(self.sub_models_direct)}]': sub_models_string, + f'Variables: [{len(self.variables)}/{len(self._model.variables)}]': self.variables.__repr__().split('\n', 2)[2], + f'Constraints: [{len(self.constraints)}/{len(self._model.constraints)}]': self.constraints.__repr__().split('\n', 2)[2], + f'Submodels: [{len(self.submodels)}]': self.submodels.__repr__().split('\n', 2)[2], } # Format sections with headers and underlines formatted_sections = [] for section_header, section_content in sections.items(): - underline = '-' * len(section_header) - formatted_section = f'{section_header}\n{underline}\n{section_content}' - formatted_sections.append(formatted_section) + formatted_sections.append( + (f'{section_header}\n' + f'{"-" * len(section_header)}\n' + f'{section_content}') + ) - # Combine everything with proper formatting - all_sections = '\n'.join(formatted_sections) - header_separator = '=' * len(model_string) + model_string = f'Submodel "{self.label_of_model}":' - return f'{model_string}\n{header_separator}\n\n{all_sections}' + return (f'{model_string}\n' + f'{"=" * len(model_string)}\n\n' + f'{"\n".join(formatted_sections)}') @property def hours_per_step(self): @@ -945,14 +958,17 @@ def __contains__(self, name: str) -> bool: def __repr__(self) -> str: """Simple representation of the submodels collection.""" + title = 'flixopt.structure.Submodels:' + underline = '-' * len(title) + if not self.data: - return 'Submodels:\n----------\n \n' + return f'{title}\n{underline}\n \n' sub_models_string = '' for name, submodel in self.data.items(): sub_models_string += f'\n * {name} ({submodel.__class__.__name__}) [{len(submodel.variables)} Vars + {len(submodel.constraints)} Cons)' - return f'Submodels:\n----------{sub_models_string}\n' + return f'{title}\n{underline}{sub_models_string}\n' def items(self) -> ItemsView[str, 'Submodel']: return self.data.items() From 66283cb33e070820cea70fdd014e04c0707990a0 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 23 Jul 2025 14:05:06 +0200 Subject: [PATCH 250/448] Bugfix .variables and .constraints --- flixopt/components.py | 11 ++++++----- flixopt/features.py | 8 +++++--- flixopt/structure.py | 28 +++++++++++++++++++++------- 3 files changed, 32 insertions(+), 15 deletions(-) diff --git a/flixopt/components.py b/flixopt/components.py index 495edb5d7..c0e905c5f 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -394,13 +394,13 @@ def transform_data(self, flow_system: 'FlowSystem') -> None: class TransmissionModel(ComponentModel): + element: Transmission + def __init__(self, model: FlowSystemModel, element: Transmission): if (element.absolute_losses is not None) and np.any(element.absolute_losses != 0): for flow in element.inputs + element.outputs: if flow.on_off_parameters is None: flow.on_off_parameters = OnOffParameters() - self.element: Transmission = element - self.on_off: Optional[OnOffModel] = None super().__init__(model, element) @@ -438,9 +438,9 @@ def create_transmission_equation(self, name: str, in_flow: Flow, out_flow: Flow) class LinearConverterModel(ComponentModel): + element: LinearConverter + def __init__(self, model: FlowSystemModel, element: LinearConverter): - self.element: LinearConverter = element - self.on_off: Optional[OnOffModel] = None self.piecewise_conversion: Optional[PiecewiseConversion] = None super().__init__(model, element) @@ -485,6 +485,7 @@ def _do_modeling(self): class StorageModel(ComponentModel): """Submodel of Storage""" + element: Storage def __init__(self, model: FlowSystemModel, element: Storage): super().__init__(model, element) @@ -549,7 +550,7 @@ def _do_modeling(self): if self.element.balanced: self.add_constraints( - self.element.charging.model._investment.size * 1 == self.element.discharging.model._investment.size * 1, + self.element.charging.submodel._investment.size * 1 == self.element.discharging.submodel._investment.size * 1, short_name='balanced_sizes', ) diff --git a/flixopt/features.py b/flixopt/features.py index 0c7606120..7b862375c 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -161,13 +161,15 @@ def _do_modeling(self): self.add_constraints(self.on + off == 1, short_name='complementary') # 3. Total duration tracking using existing pattern - duration_expr = (self.on * self._model.hours_per_step).sum('time') ModelingPrimitives.expression_tracking_variable( - self, duration_expr, short_name='on_hours_total', + self, + tracked_expression=(self.on * self._model.hours_per_step).sum('time'), bounds=( self.parameters.on_hours_total_min if self.parameters.on_hours_total_min is not None else 0, self.parameters.on_hours_total_max if self.parameters.on_hours_total_max is not None else np.inf, - ), #TODO: self._model.hours_per_step.sum('time').item() + self._get_previous_on_duration()) + ),#TODO: self._model.hours_per_step.sum('time').item() + self._get_previous_on_duration()) + short_name='on_hours_total', + coords=self.get_coords(['year', 'scenario']), ) # 4. Switch tracking using existing pattern diff --git a/flixopt/structure.py b/flixopt/structure.py index 94f5f2bda..344d86c01 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -9,7 +9,21 @@ import pathlib from dataclasses import dataclass from io import StringIO -from typing import TYPE_CHECKING, Any, Collection, Dict, List, Literal, Optional, Tuple, Union, Protocol, runtime_checkable, ItemsView, Iterator +from typing import ( + TYPE_CHECKING, + Any, + Collection, + Dict, + ItemsView, + Iterator, + List, + Literal, + Optional, + Protocol, + Tuple, + Union, + runtime_checkable, +) import linopy import numpy as np @@ -46,8 +60,8 @@ def register_class_for_io(cls): class SubmodelsMixin: """Mixin that provides submodel functionality for both FlowSystemModel and Submodel.""" - submodels: 'Submodels' + @property def all_submodels(self) -> List['Submodel']: """Get all submodels including nested ones recursively.""" @@ -871,27 +885,27 @@ def variables_direct(self) -> linopy.Variables: @property def constraints_direct(self) -> linopy.Constraints: - """Costraints of the model, excluding those of sub-models""" + """Constraints of the model, excluding those of sub-models""" return self._model.constraints[[con.name for con in self._constraints.values()]] @property def constraints(self) -> linopy.Constraints: - """All constraints of the model, including those of sub-models""" + """All constraints of the model, including those of all sub-models""" names = list(self.constraints_direct) + [ constraint_name for submodel in self.submodels.values() - for constraint_name in submodel.constraints_direct + for constraint_name in submodel.constraints ] return self._model.constraints[names] @property def variables(self) -> linopy.Variables: - """All variables of the model, including those of sub-models""" + """All variables of the model, including those of all sub-models""" names = list(self.variables_direct) + [ variable_name for submodel in self.submodels.values() - for variable_name in submodel.variables_direct + for variable_name in submodel.variables ] return self._model.variables[names] From a84dfadf9eb450f81d0603963c92e468006b36d1 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 23 Jul 2025 14:08:34 +0200 Subject: [PATCH 251/448] Add type checks to modeling.py --- flixopt/modeling.py | 124 ++++++++++++-------------------------------- 1 file changed, 33 insertions(+), 91 deletions(-) diff --git a/flixopt/modeling.py b/flixopt/modeling.py index f67fc9d19..8c03da9f4 100644 --- a/flixopt/modeling.py +++ b/flixopt/modeling.py @@ -199,6 +199,9 @@ def expression_tracking_variable( variables: {'tracker': tracker_var} constraints: {'tracking': constraint} """ + if not isinstance(model, Submodel): + raise ValueError('ModelingPrimitives.expression_tracking_variable() can only be used with a Submodel') + coords = coords or ['year', 'scenario'] if not bounds: @@ -239,6 +242,9 @@ def state_transition_variables( variables: {'switch_on': binary_var, 'switch_off': binary_var} constraints: {'transition': constraint, 'initial': constraint, 'mutex': constraint} """ + if not isinstance(model, Submodel): + raise ValueError('ModelingPrimitives.state_transition_variables() can only be used with a Submodel') + # State transition constraints for t > 0 transition = model.add_constraints( switch_on.isel(time=slice(1, None)) - switch_off.isel(time=slice(1, None)) @@ -259,7 +265,7 @@ def state_transition_variables( @staticmethod def sum_up_variable( - model: FlowSystemModel, + model: Submodel, variable_to_count: linopy.Variable, name: str = None, bounds: Tuple[NonTemporalData, NonTemporalData] = None, @@ -275,6 +281,9 @@ def sum_up_variable( bounds: The bounds of the constraint factor: The factor to be applied to the variable """ + if not isinstance(model, Submodel): + raise ValueError('ModelingPrimitives.sum_up_variable() can only be used with a Submodel') + if bounds is None: bounds = (0, np.inf) else: @@ -293,7 +302,7 @@ def sum_up_variable( @staticmethod def consecutive_duration_tracking( - model: FlowSystemModel, + model: Submodel, state_variable: linopy.Variable, name: str = None, short_name: str = None, @@ -324,6 +333,9 @@ def consecutive_duration_tracking( variables: {'duration': duration_var} constraints: {'ub': constraint, 'forward': constraint, 'backward': constraint, ...} """ + if not isinstance(model, Submodel): + raise ValueError('ModelingPrimitives.sum_up_variable() can only be used with a Submodel') + hours_per_step = model.hours_per_step mega = hours_per_step.sum('time') + previous_duration # Big-M value @@ -411,6 +423,9 @@ def mutual_exclusivity_constraint( Raises: AssertionError: If fewer than 2 variables provided or variables aren't binary """ + if not isinstance(model, Submodel): + raise ValueError('ModelingPrimitives.sum_up_variable() can only be used with a Submodel') + assert len(binary_variables) >= 2, ( f'Mutual exclusivity requires at least 2 variables, got {len(binary_variables)}' ) @@ -431,7 +446,7 @@ class BoundingPatterns: @staticmethod def basic_bounds( - model: FlowSystemModel, + model: Submodel, variable: linopy.Variable, bounds: Tuple[TemporalData, TemporalData], name: str = None, @@ -452,6 +467,9 @@ def basic_bounds( - variables (Dict): Empty dict - constraints (Dict[str, linopy.Constraint]): 'ub', 'lb' """ + if not isinstance(model, Submodel): + raise ValueError('BoundingPatterns.basic_bounds() can only be used with a Submodel') + lower_bound, upper_bound = bounds name = name or f'{variable.name}' @@ -462,7 +480,7 @@ def basic_bounds( @staticmethod def bounds_with_state( - model: FlowSystemModel, + model: Submodel, variable: linopy.Variable, bounds: Tuple[TemporalData, TemporalData], variable_state: linopy.Variable, @@ -489,6 +507,9 @@ def bounds_with_state( - variables (Dict): Empty dict - constraints (Dict[str, linopy.Constraint]): 'ub', 'lb' """ + if not isinstance(model, Submodel): + raise ValueError('BoundingPatterns.bounds_with_state() can only be used with a Submodel') + lower_bound, upper_bound = bounds name = name or f'{variable.name}' @@ -507,7 +528,7 @@ def bounds_with_state( @staticmethod def scaled_bounds( - model: FlowSystemModel, + model: Submodel, variable: linopy.Variable, scaling_variable: linopy.Variable, relative_bounds: Tuple[TemporalData, TemporalData], @@ -534,6 +555,9 @@ def scaled_bounds( - variables (Dict): Empty dict - constraints (Dict[str, linopy.Constraint]): 'ub', 'lb' """ + if not isinstance(model, Submodel): + raise ValueError('BoundingPatterns.scaled_bounds() can only be used with a Submodel') + rel_lower, rel_upper = relative_bounds name = name or f'{variable.name}' @@ -547,7 +571,7 @@ def scaled_bounds( @staticmethod def scaled_bounds_with_state( - model: FlowSystemModel, + model: Submodel, variable: linopy.Variable, scaling_variable: linopy.Variable, relative_bounds: Tuple[TemporalData, TemporalData], @@ -580,6 +604,9 @@ def scaled_bounds_with_state( Returns: List[linopy.Constraint]: List of constraint objects """ + if not isinstance(model, Submodel): + raise ValueError('BoundingPatterns.active_bounds_with_state() can only be used with a Submodel') + rel_lower, rel_upper = relative_bounds scaling_min, scaling_max = scaling_bounds name = name or f'{variable.name}' @@ -600,88 +627,3 @@ def scaled_bounds_with_state( binary_lower = model.add_constraints(variable_state * big_m_lower <= variable, name=f'{name}|lb1') return [scaling_lower, scaling_upper, binary_lower, binary_upper] - - @staticmethod - def auto_bounds( - model: FlowSystemModel, - variable: linopy.Variable, - bounds: Tuple[TemporalData, TemporalData], - scaling_variable: linopy.Variable = None, - scaling_state: linopy.Variable = None, - scaling_bounds: Tuple[TemporalData, TemporalData] = None, - variable_state: linopy.Variable = None, - ) -> List[linopy.Constraint]: - """Automatically select the appropriate bounds method. - - Parameter Combinations: - 1. Only bounds → basic_bounds() - 2. bounds + scaling_variable → scaled_bounds() - 3. bounds + variable_state → bounds_with_state() - 4. bounds + scaling_variable + variable_state → binary_scaled_bounds() - 5. bounds + scaling_variable + scaling_state + variable_state → scaled_bounds_with_state_on_both_scaling_and_variable() - - Args: - model: The optimization model instance - variable: Variable to be bounded - bounds: Tuple of (lower, upper) bounds or relative factors - scaling_variable: Optional variable to scale bounds by - scaling_state: Optional binary variable for scaling_variable state - scaling_bounds: Required for cases 4,5 - bounds of scaling variable - variable_state: Optional binary variable for variable state - - Returns: - Tuple from the selected method - - Raises: - ValueError: If required parameters are missing - """ - # Case 5: Dual binary control - if scaling_variable is not None and scaling_state is not None and variable_state is not None: - if scaling_bounds is None: - raise ValueError('scaling_bounds is required for dual binary control') - return BoundingPatterns.scaled_bounds_with_state_on_both_scaling_and_variable( - model=model, - variable=variable, - scaling_variable=scaling_variable, - relative_bounds=bounds, - scaling_state=scaling_state, - variable_state=variable_state, - scaling_bounds=scaling_bounds, - ) - - # Case 4: Binary scaled bounds - if scaling_variable is not None and variable_state is not None: - if scaling_bounds is None: - raise ValueError('scaling_bounds is required for binary scaled bounds') - return BoundingPatterns.binary_scaled_bounds( - model=model, - variable=variable, - scaling_variable=scaling_variable, - relative_bounds=bounds, - variable_state=variable_state, - scaling_bounds=scaling_bounds, - ) - - # Case 3: Binary controlled bounds - if variable_state is not None and scaling_variable is None: - return BoundingPatterns.bounds_with_state( - model=model, - variable=variable, - bounds=bounds, - variable_state=variable_state, - ) - - # Case 2: Scaled bounds - if scaling_variable is not None and variable_state is None: - return BoundingPatterns.scaled_bounds( - model=model, - variable=variable, - scaling_variable=scaling_variable, - relative_bounds=bounds, - ) - - # Case 1: Basic bounds - if scaling_variable is None and variable_state is None: - return BoundingPatterns.basic_bounds(model, variable, bounds) - - raise ValueError('Invalid combination of arguments') From d2182aa23dbf558998e6680f6f98ff7ca95faba9 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 23 Jul 2025 14:10:32 +0200 Subject: [PATCH 252/448] Improve assertion in tests --- tests/conftest.py | 22 ++++++++++++ tests/test_component.py | 75 +++++++++++++++++++++++------------------ 2 files changed, 64 insertions(+), 33 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 902e01c12..d6a5df7fa 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,6 +5,7 @@ """ import os +from typing import Iterable import linopy.testing import numpy as np @@ -575,3 +576,24 @@ def assert_var_equal(actual: linopy.Variable, desired: linopy.Variable): if actual.coord_dims != desired.coord_dims: raise AssertionError(f"{name} coordinate dimensions don't match: {actual.coord_dims} != {desired.coord_dims}") + + +def assert_sets_equal(set1: Iterable, set2: Iterable, msg=""): + """Assert two sets are equal with custom error message.""" + set1, set2 = set(set1), set(set2) + + extra = set1 - set2 + missing = set2 - set1 + + if extra or missing: + parts = [] + if extra: + parts.append(f"Extra: {sorted(extra)}") + if missing: + parts.append(f"Missing: {sorted(missing)}") + + error_msg = ", ".join(parts) + if msg: + error_msg = f"{msg}: {error_msg}" + + raise AssertionError(error_msg) diff --git a/tests/test_component.py b/tests/test_component.py index 14b1544dd..5f1adf727 100644 --- a/tests/test_component.py +++ b/tests/test_component.py @@ -9,6 +9,7 @@ from .conftest import ( assert_almost_equal_numeric, assert_conequal, + assert_sets_equal, assert_var_equal, create_calculation_and_solve, create_linopy_model, @@ -78,40 +79,48 @@ def test_on_with_multiple_flows(self, basic_flow_system_linopy): flow_system.add_elements(comp) model = create_linopy_model(flow_system) - assert set(comp.submodel.variables) == { - 'TestComponent(In1)|flow_rate', - 'TestComponent(In1)|total_flow_hours', - 'TestComponent(In1)|on', - 'TestComponent(In1)|on_hours_total', - 'TestComponent(Out1)|flow_rate', - 'TestComponent(Out1)|total_flow_hours', - 'TestComponent(Out1)|on', - 'TestComponent(Out1)|on_hours_total', - 'TestComponent(Out2)|flow_rate', - 'TestComponent(Out2)|total_flow_hours', - 'TestComponent(Out2)|on', - 'TestComponent(Out2)|on_hours_total', - 'TestComponent|on', - 'TestComponent|on_hours_total', - } + assert_sets_equal( + set(comp.submodel.variables), + { + 'TestComponent(In1)|flow_rate', + 'TestComponent(In1)|total_flow_hours', + 'TestComponent(In1)|on', + 'TestComponent(In1)|on_hours_total', + 'TestComponent(Out1)|flow_rate', + 'TestComponent(Out1)|total_flow_hours', + 'TestComponent(Out1)|on', + 'TestComponent(Out1)|on_hours_total', + 'TestComponent(Out2)|flow_rate', + 'TestComponent(Out2)|total_flow_hours', + 'TestComponent(Out2)|on', + 'TestComponent(Out2)|on_hours_total', + 'TestComponent|on', + 'TestComponent|on_hours_total', + }, + msg='Incorrect variables', + ) - assert set(comp.submodel.constraints) == { - 'TestComponent(In1)|total_flow_hours', - 'TestComponent(In1)|flow_rate|lb', - 'TestComponent(In1)|flow_rate|ub', - 'TestComponent(In1)|on_hours_total', - 'TestComponent(Out1)|total_flow_hours', - 'TestComponent(Out1)|flow_rate|lb', - 'TestComponent(Out1)|flow_rate|ub', - 'TestComponent(Out1)|on_hours_total', - 'TestComponent(Out2)|total_flow_hours', - 'TestComponent(Out2)|flow_rate|lb', - 'TestComponent(Out2)|flow_rate|ub', - 'TestComponent(Out2)|on_hours_total', - 'TestComponent|on|lb', - 'TestComponent|on|ub', - 'TestComponent|on_hours_total', - } + assert_sets_equal( + set(comp.submodel.constraints), + { + 'TestComponent(In1)|total_flow_hours', + 'TestComponent(In1)|flow_rate|lb', + 'TestComponent(In1)|flow_rate|ub', + 'TestComponent(In1)|on_hours_total', + 'TestComponent(Out1)|total_flow_hours', + 'TestComponent(Out1)|flow_rate|lb', + 'TestComponent(Out1)|flow_rate|ub', + 'TestComponent(Out1)|on_hours_total', + 'TestComponent(Out2)|total_flow_hours', + 'TestComponent(Out2)|flow_rate|lb', + 'TestComponent(Out2)|flow_rate|ub', + 'TestComponent(Out2)|on_hours_total', + 'TestComponent|on|lb', + 'TestComponent|on|ub', + 'TestComponent|on_hours_total', + }, + msg='Incorrect constraints', + ) assert_var_equal(model['TestComponent(Out2)|flow_rate'], model.add_variables(lower=0, upper=300 * ub_out2, coords=(timesteps,))) From 75c05ee5a5404bae6427052a407ee4eb635bf367 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 23 Jul 2025 14:18:54 +0200 Subject: [PATCH 253/448] Improve docstrings and register ElementModels directly in FlowSystemModel --- flixopt/structure.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/flixopt/structure.py b/flixopt/structure.py index 344d86c01..21416a6d1 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -763,7 +763,10 @@ def _valid_label(label: str) -> str: class Submodel(SubmodelsMixin): - """Stores Variables and Constraints.""" + """Stores Variables and Constraints. Its a subset of a FlowSystemModel. + Variables and constraints are stored in the main FLowSystemModel, and are referenced here. + Can have other Submodels assigned, and can be a Submodel of another Submodel. + """ def __init__( self, model: FlowSystemModel, label_of_element: str, label_of_model = None @@ -1003,7 +1006,10 @@ def get(self, name: str, default=None): class ElementModel(Submodel): - """Stores the mathematical Variables and Constraints for Elements""" + """ + Stores the mathematical Variables and Constraints for Elements. + ElementModels are directly registered in the main FLowSystemModel + """ def __init__(self, model: FlowSystemModel, element: Element): """ @@ -1012,7 +1018,8 @@ def __init__(self, model: FlowSystemModel, element: Element): element: The element this model is created for. """ self.element = element - super().__init__(model, label_of_element=element.label_full) + super().__init__(model, label_of_element=element.label_full, label_of_model=element.label_full) + self._model.add_submodels(self, short_name=self.label_of_model) def results_structure(self): return { From 66a6ff19cbf925ff434f17088f2c796c2005fedb Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 23 Jul 2025 14:30:30 +0200 Subject: [PATCH 254/448] Improve __repr__() --- flixopt/structure.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/flixopt/structure.py b/flixopt/structure.py index 21416a6d1..0e82fa346 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -975,15 +975,23 @@ def __contains__(self, name: str) -> bool: def __repr__(self) -> str: """Simple representation of the submodels collection.""" - title = 'flixopt.structure.Submodels:' + if not self.data: + return 'flixopt.structure.Submodels:\n----------------------------\n \n' + + total_vars = sum(len(submodel.variables) for submodel in self.data.values()) + total_cons = sum(len(submodel.constraints) for submodel in self.data.values()) + + title = f'flixopt.structure.Submodels ({total_vars} vars, {total_cons} constraints, {len(self.data)} submodels):' underline = '-' * len(title) if not self.data: return f'{title}\n{underline}\n \n' - sub_models_string = '' for name, submodel in self.data.items(): - sub_models_string += f'\n * {name} ({submodel.__class__.__name__}) [{len(submodel.variables)} Vars + {len(submodel.constraints)} Cons)' + type_name = submodel.__class__.__name__ + var_count = len(submodel.variables) + con_count = len(submodel.constraints) + sub_models_string += f'\n * {name} [{type_name}] ({var_count}v/{con_count}c)' return f'{title}\n{underline}{sub_models_string}\n' From e2e1f1318f016db5e80d937f5db5cf61533f6df3 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 23 Jul 2025 14:41:47 +0200 Subject: [PATCH 255/448] ruff check --- tests/test_effect.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/test_effect.py b/tests/test_effect.py index cce8ac939..81a220d12 100644 --- a/tests/test_effect.py +++ b/tests/test_effect.py @@ -5,7 +5,13 @@ import flixopt as fx -from .conftest import assert_conequal, assert_var_equal, create_calculation_and_solve, create_linopy_model +from .conftest import ( + assert_conequal, + assert_sets_equal, + assert_var_equal, + create_calculation_and_solve, + create_linopy_model, +) class TestEffectModel: From 62b18b614a6448a7e5e7035548690309af35f480 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 23 Jul 2025 15:28:01 +0200 Subject: [PATCH 256/448] Use new method to compare sets in tests --- tests/test_component.py | 152 ++++++++++++--------- tests/test_effect.py | 92 ++++++++----- tests/test_flow.py | 286 +++++++++++++++++++++++++++------------- 3 files changed, 343 insertions(+), 187 deletions(-) diff --git a/tests/test_component.py b/tests/test_component.py index 5f1adf727..9b3674cd5 100644 --- a/tests/test_component.py +++ b/tests/test_component.py @@ -47,19 +47,27 @@ def test_component(self, basic_flow_system_linopy): flow_system.add_elements(comp) _ = create_linopy_model(flow_system) - assert {'TestComponent(In1)|flow_rate', - 'TestComponent(In1)|total_flow_hours', - 'TestComponent(In2)|flow_rate', - 'TestComponent(In2)|total_flow_hours', - 'TestComponent(Out1)|flow_rate', - 'TestComponent(Out1)|total_flow_hours', - 'TestComponent(Out2)|flow_rate', - 'TestComponent(Out2)|total_flow_hours'} == set(comp.submodel.variables) - - assert {'TestComponent(In1)|total_flow_hours', - 'TestComponent(In2)|total_flow_hours', - 'TestComponent(Out1)|total_flow_hours', - 'TestComponent(Out2)|total_flow_hours'} == set(comp.submodel.constraints) + assert_sets_equal( + set(comp.submodel.variables), + {'TestComponent(In1)|flow_rate', + 'TestComponent(In1)|total_flow_hours', + 'TestComponent(In2)|flow_rate', + 'TestComponent(In2)|total_flow_hours', + 'TestComponent(Out1)|flow_rate', + 'TestComponent(Out1)|total_flow_hours', + 'TestComponent(Out2)|flow_rate', + 'TestComponent(Out2)|total_flow_hours'}, + msg='Incorrect variables' + ) + + assert_sets_equal( + set(comp.submodel.constraints), + {'TestComponent(In1)|total_flow_hours', + 'TestComponent(In2)|total_flow_hours', + 'TestComponent(Out1)|total_flow_hours', + 'TestComponent(Out2)|total_flow_hours'}, + msg='Incorrect constraints' + ) def test_on_with_multiple_flows(self, basic_flow_system_linopy): """Test that flow model constraints are correctly generated.""" @@ -167,23 +175,31 @@ def test_on_with_single_flow(self, basic_flow_system_linopy): flow_system.add_elements(comp) model = create_linopy_model(flow_system) - assert set(comp.submodel.variables) == { - 'TestComponent(In1)|flow_rate', - 'TestComponent(In1)|total_flow_hours', - 'TestComponent(In1)|on', - 'TestComponent(In1)|on_hours_total', - 'TestComponent|on', - 'TestComponent|on_hours_total', - } - - assert set(comp.submodel.constraints) == { - 'TestComponent(In1)|total_flow_hours', - 'TestComponent(In1)|flow_rate|lb', - 'TestComponent(In1)|flow_rate|ub', - 'TestComponent(In1)|on_hours_total', - 'TestComponent|on', - 'TestComponent|on_hours_total', - } + assert_sets_equal( + set(comp.submodel.variables), + { + 'TestComponent(In1)|flow_rate', + 'TestComponent(In1)|total_flow_hours', + 'TestComponent(In1)|on', + 'TestComponent(In1)|on_hours_total', + 'TestComponent|on', + 'TestComponent|on_hours_total', + }, + msg='Incorrect variables' + ) + + assert_sets_equal( + set(comp.submodel.constraints), + { + 'TestComponent(In1)|total_flow_hours', + 'TestComponent(In1)|flow_rate|lb', + 'TestComponent(In1)|flow_rate|ub', + 'TestComponent(In1)|on_hours_total', + 'TestComponent|on', + 'TestComponent|on_hours_total', + }, + msg='Incorrect constraints' + ) assert_var_equal( model['TestComponent(In1)|flow_rate'], model.add_variables(lower=0, upper=100, coords=(timesteps,)) @@ -223,40 +239,48 @@ def test_previous_states_with_multiple_flows(self, basic_flow_system_linopy): flow_system.add_elements(comp) model = create_linopy_model(flow_system) - assert set(comp.submodel.variables) == { - 'TestComponent(In1)|flow_rate', - 'TestComponent(In1)|total_flow_hours', - 'TestComponent(In1)|on', - 'TestComponent(In1)|on_hours_total', - 'TestComponent(Out1)|flow_rate', - 'TestComponent(Out1)|total_flow_hours', - 'TestComponent(Out1)|on', - 'TestComponent(Out1)|on_hours_total', - 'TestComponent(Out2)|flow_rate', - 'TestComponent(Out2)|total_flow_hours', - 'TestComponent(Out2)|on', - 'TestComponent(Out2)|on_hours_total', - 'TestComponent|on', - 'TestComponent|on_hours_total', - } - - assert set(comp.submodel.constraints) == { - 'TestComponent(In1)|total_flow_hours', - 'TestComponent(In1)|flow_rate|lb', - 'TestComponent(In1)|flow_rate|ub', - 'TestComponent(In1)|on_hours_total', - 'TestComponent(Out1)|total_flow_hours', - 'TestComponent(Out1)|flow_rate|lb', - 'TestComponent(Out1)|flow_rate|ub', - 'TestComponent(Out1)|on_hours_total', - 'TestComponent(Out2)|total_flow_hours', - 'TestComponent(Out2)|flow_rate|lb', - 'TestComponent(Out2)|flow_rate|ub', - 'TestComponent(Out2)|on_hours_total', - 'TestComponent|on|lb', - 'TestComponent|on|ub', - 'TestComponent|on_hours_total', - } + assert_sets_equal( + set(comp.submodel.variables), + { + 'TestComponent(In1)|flow_rate', + 'TestComponent(In1)|total_flow_hours', + 'TestComponent(In1)|on', + 'TestComponent(In1)|on_hours_total', + 'TestComponent(Out1)|flow_rate', + 'TestComponent(Out1)|total_flow_hours', + 'TestComponent(Out1)|on', + 'TestComponent(Out1)|on_hours_total', + 'TestComponent(Out2)|flow_rate', + 'TestComponent(Out2)|total_flow_hours', + 'TestComponent(Out2)|on', + 'TestComponent(Out2)|on_hours_total', + 'TestComponent|on', + 'TestComponent|on_hours_total', + }, + msg='Incorrect variables' + ) + + assert_sets_equal( + set(comp.submodel.constraints), + { + 'TestComponent(In1)|total_flow_hours', + 'TestComponent(In1)|flow_rate|lb', + 'TestComponent(In1)|flow_rate|ub', + 'TestComponent(In1)|on_hours_total', + 'TestComponent(Out1)|total_flow_hours', + 'TestComponent(Out1)|flow_rate|lb', + 'TestComponent(Out1)|flow_rate|ub', + 'TestComponent(Out1)|on_hours_total', + 'TestComponent(Out2)|total_flow_hours', + 'TestComponent(Out2)|flow_rate|lb', + 'TestComponent(Out2)|flow_rate|ub', + 'TestComponent(Out2)|on_hours_total', + 'TestComponent|on|lb', + 'TestComponent|on|ub', + 'TestComponent|on_hours_total', + }, + msg='Incorrect constraints' + ) assert_var_equal(model['TestComponent(Out2)|flow_rate'], model.add_variables(lower=0, upper=300 * ub_out2, coords=(timesteps,))) diff --git a/tests/test_effect.py b/tests/test_effect.py index 81a220d12..dfcc2ea66 100644 --- a/tests/test_effect.py +++ b/tests/test_effect.py @@ -25,14 +25,23 @@ def test_minimal(self, basic_flow_system_linopy): flow_system.add_elements(effect) model = create_linopy_model(flow_system) - assert set(effect.submodel.variables) == {'Effect1(invest)|total', - 'Effect1(operation)|total', - 'Effect1(operation)|total_per_timestep', - 'Effect1|total',} - assert set(effect.submodel.constraints) == {'Effect1(invest)|total', - 'Effect1(operation)|total', - 'Effect1(operation)|total_per_timestep', - 'Effect1|total',} + assert_sets_equal( + set(effect.submodel.variables), + {'Effect1(invest)|total', + 'Effect1(operation)|total', + 'Effect1(operation)|total_per_timestep', + 'Effect1|total'}, + msg='Incorrect variables' + ) + + assert_sets_equal( + set(effect.submodel.constraints), + {'Effect1(invest)|total', + 'Effect1(operation)|total', + 'Effect1(operation)|total_per_timestep', + 'Effect1|total'}, + msg='Incorrect constraints' + ) assert_var_equal(model.variables['Effect1|total'], model.add_variables()) assert_var_equal(model.variables['Effect1(invest)|total'], model.add_variables()) @@ -64,14 +73,23 @@ def test_bounds(self, basic_flow_system_linopy): flow_system.add_elements(effect) model = create_linopy_model(flow_system) - assert set(effect.submodel.variables) == {'Effect1(invest)|total', - 'Effect1(operation)|total', - 'Effect1(operation)|total_per_timestep', - 'Effect1|total',} - assert set(effect.submodel.constraints) == {'Effect1(invest)|total', - 'Effect1(operation)|total', - 'Effect1(operation)|total_per_timestep', - 'Effect1|total',} + assert_sets_equal( + set(effect.submodel.variables), + {'Effect1(invest)|total', + 'Effect1(operation)|total', + 'Effect1(operation)|total_per_timestep', + 'Effect1|total'}, + msg='Incorrect variables' + ) + + assert_sets_equal( + set(effect.submodel.constraints), + {'Effect1(invest)|total', + 'Effect1(operation)|total', + 'Effect1(operation)|total_per_timestep', + 'Effect1|total'}, + msg='Incorrect constraints' + ) assert_var_equal(model.variables['Effect1|total'], model.add_variables(lower=3.0, upper=3.1)) assert_var_equal(model.variables['Effect1(invest)|total'], model.add_variables(lower=2.0, upper=2.1)) @@ -106,22 +124,31 @@ def test_shares(self, basic_flow_system_linopy): flow_system.add_elements(effect1, effect2, effect3) model = create_linopy_model(flow_system) - assert set(effect2.submodel.variables) == { - 'Effect2(invest)|total', - 'Effect2(operation)|total', - 'Effect2(operation)|total_per_timestep', - 'Effect2|total', - 'Effect1(invest)->Effect2(invest)', - 'Effect1(operation)->Effect2(operation)', - } - assert set(effect2.submodel.constraints) == { - 'Effect2(invest)|total', - 'Effect2(operation)|total', - 'Effect2(operation)|total_per_timestep', - 'Effect2|total', - 'Effect1(invest)->Effect2(invest)', - 'Effect1(operation)->Effect2(operation)', - } + assert_sets_equal( + set(effect2.submodel.variables), + { + 'Effect2(invest)|total', + 'Effect2(operation)|total', + 'Effect2(operation)|total_per_timestep', + 'Effect2|total', + 'Effect1(invest)->Effect2(invest)', + 'Effect1(operation)->Effect2(operation)', + }, + msg='Incorrect variables for effect2' + ) + + assert_sets_equal( + set(effect2.submodel.constraints), + { + 'Effect2(invest)|total', + 'Effect2(operation)|total', + 'Effect2(operation)|total_per_timestep', + 'Effect2|total', + 'Effect1(invest)->Effect2(invest)', + 'Effect1(operation)->Effect2(operation)', + }, + msg='Incorrect constraints for effect2' + ) assert_conequal( model.constraints['Effect2(invest)|total'], @@ -227,4 +254,3 @@ def test_shares(self, basic_flow_system_linopy): xr.testing.assert_allclose(results.effects_per_component('total').sum('component')['Effect3'], results.solution['Effect3|total']) - diff --git a/tests/test_flow.py b/tests/test_flow.py index d22d81993..2ceb99e33 100644 --- a/tests/test_flow.py +++ b/tests/test_flow.py @@ -5,7 +5,7 @@ import flixopt as fx -from .conftest import assert_conequal, assert_var_equal, create_linopy_model +from .conftest import assert_conequal, assert_sets_equal, assert_var_equal, create_linopy_model class TestFlowModel: @@ -29,8 +29,16 @@ def test_flow_minimal(self, basic_flow_system_linopy): model.add_variables(lower=0, upper=100, coords=(timesteps,))) assert_var_equal(flow.submodel.total_flow_hours, model.add_variables(lower=0)) - assert set(flow.submodel.variables) == set(['Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate']) - assert set(flow.submodel.constraints) == set(['Sink(Wärme)|total_flow_hours']) + assert_sets_equal( + set(flow.submodel.variables), + {'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate'}, + msg='Incorrect variables' + ) + assert_sets_equal( + set(flow.submodel.constraints), + {'Sink(Wärme)|total_flow_hours'}, + msg='Incorrect constraints' + ) def test_flow(self, basic_flow_system_linopy): flow_system = basic_flow_system_linopy @@ -81,8 +89,16 @@ def test_flow(self, basic_flow_system_linopy): <= model.hours_per_step.sum('time') * 0.9 * 100, ) - assert set(flow.submodel.variables) == set(['Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate']) - assert set(flow.submodel.constraints) == set(['Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|load_factor_max', 'Sink(Wärme)|load_factor_min']) + assert_sets_equal( + set(flow.submodel.variables), + {'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate'}, + msg='Incorrect variables' + ) + assert_sets_equal( + set(flow.submodel.constraints), + {'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|load_factor_max', 'Sink(Wärme)|load_factor_min'}, + msg='Incorrect constraints' + ) def test_effects_per_flow_hour(self, basic_flow_system_linopy): flow_system = basic_flow_system_linopy @@ -100,8 +116,16 @@ def test_effects_per_flow_hour(self, basic_flow_system_linopy): model = create_linopy_model(flow_system) costs, co2 = flow_system.effects['Costs'], flow_system.effects['CO2'] - assert set(flow.submodel.variables) == {'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate'} - assert set(flow.submodel.constraints) == {'Sink(Wärme)|total_flow_hours'} + assert_sets_equal( + set(flow.submodel.variables), + {'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate'}, + msg='Incorrect variables' + ) + assert_sets_equal( + set(flow.submodel.constraints), + {'Sink(Wärme)|total_flow_hours'}, + msg='Incorrect constraints' + ) assert 'Sink(Wärme)->Costs(operation)' in set(costs.submodel.constraints) assert 'Sink(Wärme)->CO2(operation)' in set(co2.submodel.constraints) @@ -133,19 +157,23 @@ def test_flow_invest(self, basic_flow_system_linopy): flow_system.add_elements(fx.Sink('Sink', sink=flow)) model = create_linopy_model(flow_system) - assert set(flow.submodel.variables) == set( - [ + assert_sets_equal( + set(flow.submodel.variables), + { 'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate', 'Sink(Wärme)|size', - ] + }, + msg='Incorrect variables' ) - assert set(flow.submodel.constraints) == set( - [ + assert_sets_equal( + set(flow.submodel.constraints), + { 'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate|ub', 'Sink(Wärme)|flow_rate|lb', - ] + }, + msg='Incorrect constraints' ) # size @@ -188,17 +216,21 @@ def test_flow_invest_optional(self, basic_flow_system_linopy): flow_system.add_elements(fx.Sink('Sink', sink=flow)) model = create_linopy_model(flow_system) - assert set(flow.submodel.variables) == set( - ['Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate', 'Sink(Wärme)|size', 'Sink(Wärme)|is_invested'] + assert_sets_equal( + set(flow.submodel.variables), + {'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate', 'Sink(Wärme)|size', 'Sink(Wärme)|is_invested'}, + msg='Incorrect variables' ) - assert set(flow.submodel.constraints) == set( - [ + assert_sets_equal( + set(flow.submodel.constraints), + { 'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|size|lb', 'Sink(Wärme)|size|ub', 'Sink(Wärme)|flow_rate|lb', 'Sink(Wärme)|flow_rate|ub', - ] + }, + msg='Incorrect constraints' ) assert_var_equal(model['Sink(Wärme)|size'], model.add_variables(lower=0, upper=100)) @@ -252,17 +284,21 @@ def test_flow_invest_optional_wo_min_size(self, basic_flow_system_linopy): flow_system.add_elements(fx.Sink('Sink', sink=flow)) model = create_linopy_model(flow_system) - assert set(flow.submodel.variables) == set( - ['Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate', 'Sink(Wärme)|size', 'Sink(Wärme)|is_invested'] + assert_sets_equal( + set(flow.submodel.variables), + {'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate', 'Sink(Wärme)|size', 'Sink(Wärme)|is_invested'}, + msg='Incorrect variables' ) - assert set(flow.submodel.constraints) == set( - [ + assert_sets_equal( + set(flow.submodel.constraints), + { 'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|size|ub', 'Sink(Wärme)|size|lb', 'Sink(Wärme)|flow_rate|lb', 'Sink(Wärme)|flow_rate|ub', - ] + }, + msg='Incorrect constraints' ) assert_var_equal(model['Sink(Wärme)|size'], model.add_variables(lower=0, upper=100)) @@ -316,15 +352,19 @@ def test_flow_invest_wo_min_size_non_optional(self, basic_flow_system_linopy): flow_system.add_elements(fx.Sink('Sink', sink=flow)) model = create_linopy_model(flow_system) - assert set(flow.submodel.variables) == set( - ['Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate', 'Sink(Wärme)|size'] + assert_sets_equal( + set(flow.submodel.variables), + {'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate', 'Sink(Wärme)|size'}, + msg='Incorrect variables' ) - assert set(flow.submodel.constraints) == set( - [ + assert_sets_equal( + set(flow.submodel.constraints), + { 'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate|lb', 'Sink(Wärme)|flow_rate|ub', - ] + }, + msg='Incorrect constraints' ) assert_var_equal(model['Sink(Wärme)|size'], model.add_variables(lower=1e-5, upper=100)) @@ -367,7 +407,11 @@ def test_flow_invest_fixed_size(self, basic_flow_system_linopy): flow_system.add_elements(fx.Sink('Sink', sink=flow)) model = create_linopy_model(flow_system) - assert set(flow.submodel.variables) == {'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate', 'Sink(Wärme)|size'} + assert_sets_equal( + set(flow.submodel.variables), + {'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate', 'Sink(Wärme)|size'}, + msg='Incorrect variables' + ) # Check that size is fixed to 75 assert_var_equal(flow.submodel.variables['Sink(Wärme)|size'], model.add_variables(lower=75, upper=75)) @@ -458,17 +502,21 @@ def test_flow_on(self, basic_flow_system_linopy): flow_system.add_elements(fx.Sink('Sink', sink=flow)) model = create_linopy_model(flow_system) - assert set(flow.submodel.variables) == set( - ['Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate', 'Sink(Wärme)|on', 'Sink(Wärme)|on_hours_total'] + assert_sets_equal( + set(flow.submodel.variables), + {'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate', 'Sink(Wärme)|on', 'Sink(Wärme)|on_hours_total'}, + msg='Incorrect variables' ) - assert set(flow.submodel.constraints) == set( - [ + assert_sets_equal( + set(flow.submodel.constraints), + { 'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|on_hours_total', 'Sink(Wärme)|flow_rate|lb', 'Sink(Wärme)|flow_rate|ub', - ] + }, + msg='Incorrect constraints' ) # flow_rate assert_var_equal( @@ -522,18 +570,26 @@ def test_effects_per_running_hour(self, basic_flow_system_linopy): model = create_linopy_model(flow_system) costs, co2 = flow_system.effects['Costs'], flow_system.effects['CO2'] - assert set(flow.submodel.variables) == { - 'Sink(Wärme)|total_flow_hours', - 'Sink(Wärme)|flow_rate', - 'Sink(Wärme)|on', - 'Sink(Wärme)|on_hours_total', - } - assert set(flow.submodel.constraints) == { - 'Sink(Wärme)|total_flow_hours', - 'Sink(Wärme)|flow_rate|lb', - 'Sink(Wärme)|flow_rate|ub', - 'Sink(Wärme)|on_hours_total', - } + assert_sets_equal( + set(flow.submodel.variables), + { + 'Sink(Wärme)|total_flow_hours', + 'Sink(Wärme)|flow_rate', + 'Sink(Wärme)|on', + 'Sink(Wärme)|on_hours_total', + }, + msg='Incorrect variables' + ) + assert_sets_equal( + set(flow.submodel.constraints), + { + 'Sink(Wärme)|total_flow_hours', + 'Sink(Wärme)|flow_rate|lb', + 'Sink(Wärme)|flow_rate|ub', + 'Sink(Wärme)|on_hours_total', + }, + msg='Incorrect constraints' + ) assert 'Sink(Wärme)->Costs(operation)' in set(costs.submodel.constraints) assert 'Sink(Wärme)->CO2(operation)' in set(co2.submodel.constraints) @@ -570,12 +626,19 @@ def test_consecutive_on_hours(self, basic_flow_system_linopy): assert {'Sink(Wärme)|consecutive_on_hours', 'Sink(Wärme)|on'}.issubset(set(flow.submodel.variables)) - assert {'Sink(Wärme)|consecutive_on_hours|ub', - 'Sink(Wärme)|consecutive_on_hours|forward', - 'Sink(Wärme)|consecutive_on_hours|backward', - 'Sink(Wärme)|consecutive_on_hours|initial', - 'Sink(Wärme)|consecutive_on_hours|lb', - }.issubset(set(flow.submodel.constraints)) + assert_sets_equal( + {'Sink(Wärme)|consecutive_on_hours|ub', + 'Sink(Wärme)|consecutive_on_hours|forward', + 'Sink(Wärme)|consecutive_on_hours|backward', + 'Sink(Wärme)|consecutive_on_hours|initial', + 'Sink(Wärme)|consecutive_on_hours|lb'} & set(flow.submodel.constraints), + {'Sink(Wärme)|consecutive_on_hours|ub', + 'Sink(Wärme)|consecutive_on_hours|forward', + 'Sink(Wärme)|consecutive_on_hours|backward', + 'Sink(Wärme)|consecutive_on_hours|initial', + 'Sink(Wärme)|consecutive_on_hours|lb'}, + msg='Missing consecutive on hours constraints' + ) assert_var_equal( model.variables['Sink(Wärme)|consecutive_on_hours'], @@ -637,11 +700,17 @@ def test_consecutive_on_hours_previous(self, basic_flow_system_linopy): assert {'Sink(Wärme)|consecutive_on_hours', 'Sink(Wärme)|on'}.issubset(set(flow.submodel.variables)) - assert {'Sink(Wärme)|consecutive_on_hours|lb', - 'Sink(Wärme)|consecutive_on_hours|forward', - 'Sink(Wärme)|consecutive_on_hours|backward', - 'Sink(Wärme)|consecutive_on_hours|initial', - }.issubset(set(flow.submodel.constraints)) + assert_sets_equal( + {'Sink(Wärme)|consecutive_on_hours|lb', + 'Sink(Wärme)|consecutive_on_hours|forward', + 'Sink(Wärme)|consecutive_on_hours|backward', + 'Sink(Wärme)|consecutive_on_hours|initial'} & set(flow.submodel.constraints), + {'Sink(Wärme)|consecutive_on_hours|lb', + 'Sink(Wärme)|consecutive_on_hours|forward', + 'Sink(Wärme)|consecutive_on_hours|backward', + 'Sink(Wärme)|consecutive_on_hours|initial'}, + msg='Missing consecutive on hours constraints for previous states' + ) assert_var_equal( model.variables['Sink(Wärme)|consecutive_on_hours'], @@ -702,13 +771,23 @@ def test_consecutive_off_hours(self, basic_flow_system_linopy): assert {'Sink(Wärme)|consecutive_off_hours', 'Sink(Wärme)|off'}.issubset(set(flow.submodel.variables)) - assert { - 'Sink(Wärme)|consecutive_off_hours|ub', - 'Sink(Wärme)|consecutive_off_hours|forward', - 'Sink(Wärme)|consecutive_off_hours|backward', - 'Sink(Wärme)|consecutive_off_hours|initial', - 'Sink(Wärme)|consecutive_off_hours|lb' - }.issubset(set(flow.submodel.constraints)) + assert_sets_equal( + { + 'Sink(Wärme)|consecutive_off_hours|ub', + 'Sink(Wärme)|consecutive_off_hours|forward', + 'Sink(Wärme)|consecutive_off_hours|backward', + 'Sink(Wärme)|consecutive_off_hours|initial', + 'Sink(Wärme)|consecutive_off_hours|lb' + } & set(flow.submodel.constraints), + { + 'Sink(Wärme)|consecutive_off_hours|ub', + 'Sink(Wärme)|consecutive_off_hours|forward', + 'Sink(Wärme)|consecutive_off_hours|backward', + 'Sink(Wärme)|consecutive_off_hours|initial', + 'Sink(Wärme)|consecutive_off_hours|lb' + }, + msg='Missing consecutive off hours constraints' + ) assert_var_equal( model.variables['Sink(Wärme)|consecutive_off_hours'], @@ -770,13 +849,23 @@ def test_consecutive_off_hours_previous(self, basic_flow_system_linopy): assert {'Sink(Wärme)|consecutive_off_hours', 'Sink(Wärme)|off'}.issubset(set(flow.submodel.variables)) - assert { - 'Sink(Wärme)|consecutive_off_hours|ub', - 'Sink(Wärme)|consecutive_off_hours|forward', - 'Sink(Wärme)|consecutive_off_hours|backward', - 'Sink(Wärme)|consecutive_off_hours|initial', - 'Sink(Wärme)|consecutive_off_hours|lb' - }.issubset(set(flow.submodel.constraints)) + assert_sets_equal( + { + 'Sink(Wärme)|consecutive_off_hours|ub', + 'Sink(Wärme)|consecutive_off_hours|forward', + 'Sink(Wärme)|consecutive_off_hours|backward', + 'Sink(Wärme)|consecutive_off_hours|initial', + 'Sink(Wärme)|consecutive_off_hours|lb' + } & set(flow.submodel.constraints), + { + 'Sink(Wärme)|consecutive_off_hours|ub', + 'Sink(Wärme)|consecutive_off_hours|forward', + 'Sink(Wärme)|consecutive_off_hours|backward', + 'Sink(Wärme)|consecutive_off_hours|initial', + 'Sink(Wärme)|consecutive_off_hours|lb' + }, + msg='Missing consecutive off hours constraints for previous states' + ) assert_var_equal( model.variables['Sink(Wärme)|consecutive_off_hours'], @@ -840,12 +929,21 @@ def test_switch_on_constraints(self, basic_flow_system_linopy): ) # Check that constraints exist - assert { - 'Sink(Wärme)|switch|transition', - 'Sink(Wärme)|switch|initial', - 'Sink(Wärme)|switch|mutex', - 'Sink(Wärme)|switch|count', - }.issubset(set(flow.submodel.constraints)) + assert_sets_equal( + { + 'Sink(Wärme)|switch|transition', + 'Sink(Wärme)|switch|initial', + 'Sink(Wärme)|switch|mutex', + 'Sink(Wärme)|switch|count', + } & set(flow.submodel.constraints), + { + 'Sink(Wärme)|switch|transition', + 'Sink(Wärme)|switch|initial', + 'Sink(Wärme)|switch|mutex', + 'Sink(Wärme)|switch|count', + }, + msg='Missing switch constraints' + ) # Check switch_on_nr variable bounds assert_var_equal(flow.submodel.variables['Sink(Wärme)|switch|count'], model.add_variables(lower=0, upper=5)) @@ -917,19 +1015,22 @@ def test_flow_on_invest_optional(self, basic_flow_system_linopy): flow_system.add_elements(fx.Sink('Sink', sink=flow)) model = create_linopy_model(flow_system) - assert set(flow.submodel.variables) == set( - [ + assert_sets_equal( + set(flow.submodel.variables), + { 'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate', 'Sink(Wärme)|is_invested', 'Sink(Wärme)|size', 'Sink(Wärme)|on', 'Sink(Wärme)|on_hours_total', - ] + }, + msg='Incorrect variables' ) - assert set(flow.submodel.constraints) == set( - [ + assert_sets_equal( + set(flow.submodel.constraints), + { 'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|on_hours_total', 'Sink(Wärme)|flow_rate|lb1', @@ -938,7 +1039,8 @@ def test_flow_on_invest_optional(self, basic_flow_system_linopy): 'Sink(Wärme)|size|ub', 'Sink(Wärme)|flow_rate|lb2', 'Sink(Wärme)|flow_rate|ub2', - ] + }, + msg='Incorrect constraints' ) # flow_rate @@ -1010,25 +1112,29 @@ def test_flow_on_invest_non_optional(self, basic_flow_system_linopy): flow_system.add_elements(fx.Sink('Sink', sink=flow)) model = create_linopy_model(flow_system) - assert set(flow.submodel.variables) == set( - [ + assert_sets_equal( + set(flow.submodel.variables), + { 'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate', 'Sink(Wärme)|size', 'Sink(Wärme)|on', 'Sink(Wärme)|on_hours_total', - ] + }, + msg='Incorrect variables' ) - assert set(flow.submodel.constraints) == set( - [ + assert_sets_equal( + set(flow.submodel.constraints), + { 'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|on_hours_total', 'Sink(Wärme)|flow_rate|lb1', 'Sink(Wärme)|flow_rate|ub1', 'Sink(Wärme)|flow_rate|lb2', 'Sink(Wärme)|flow_rate|ub2', - ] + }, + msg='Incorrect constraints' ) # flow_rate @@ -1136,4 +1242,4 @@ def test_fixed_profile_with_investment(self, basic_flow_system_linopy): if __name__ == '__main__': - pytest.main() + pytest.main() \ No newline at end of file From 5f0b503d6e1f34e704d0356a4db381202d73c9a5 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 23 Jul 2025 15:28:32 +0200 Subject: [PATCH 257/448] ruff check --- tests/test_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_flow.py b/tests/test_flow.py index 2ceb99e33..2cc26e52a 100644 --- a/tests/test_flow.py +++ b/tests/test_flow.py @@ -1242,4 +1242,4 @@ def test_fixed_profile_with_investment(self, basic_flow_system_linopy): if __name__ == '__main__': - pytest.main() \ No newline at end of file + pytest.main() From 15a08e956fdc2c6571d3e02f3babbc236c668ab9 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 23 Jul 2025 15:55:52 +0200 Subject: [PATCH 258/448] Update Contribute.md, some dependencies and add pre-commit --- .pre-commit-config.yaml | 15 +++++++++++++++ README.md | 7 ++++++- docs/SUMMARY.md | 1 + docs/contribute.md | 34 +++++++++++++++++++++++----------- pyproject.toml | 3 ++- 5 files changed, 47 insertions(+), 13 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..c7f512a55 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,15 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.12.4 + hooks: + - id: ruff-check + args: [ --fix ] + - id: ruff-format \ No newline at end of file diff --git a/README.md b/README.md index 1312dace9..615322211 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ The documentation is available at [https://flixopt.github.io/flixopt/latest/](ht --- -## 🛠️ Solver Integration +## 🎯️ Solver Integration By default, FlixOpt uses the open-source solver [HiGHS](https://highs.dev/) which is installed by default. However, it is compatible with additional solvers such as: @@ -78,6 +78,11 @@ For detailed licensing and installation instructions, refer to the respective so --- +## 🛠 Development Setup +Look into our docs for [development setup](https://flixopt.github.io/flixopt/latest/contribute/#development-setup) + +--- + ## 📖 Citation If you use FlixOpt in your research or project, please cite the following: diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index fffb84610..0e86a81c8 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -4,4 +4,5 @@ - [Examples](examples/) - [FAQ](faq/) - [API-Reference](api-reference/) +- [Contribute](contribute.md) - [Release Notes](changelog.md) \ No newline at end of file diff --git a/docs/contribute.md b/docs/contribute.md index 439fefe1d..d23d39d38 100644 --- a/docs/contribute.md +++ b/docs/contribute.md @@ -4,17 +4,29 @@ We warmly welcome contributions from the community! This guide will help you get ## Development Setup 1. Clone the repository `git clone https://github.com/flixOpt/flixopt.git` -2. Install the development dependencies `pip install -editable .[dev, docs]` -3. Run `pytest` and `ruff check .` to ensure your code passes all tests - -## Documentation -FlixOpt uses [mkdocs](https://www.mkdocs.org/) to generate documentation. To preview the documentation locally, run `mkdocs serve` in the root directory. - -## Helpful Commands -- `mkdocs serve` to preview the documentation locally. Navigate to `http://127.0.0.1:8000/` to view the documentation. -- `pytest` to run the test suite (You can also run the provided python script `run_all_test.py`) -- `ruff check .` to run the linter -- `ruff check . --fix` to automatically fix linting issues +2. Install the development dependencies `pip install -e ".[dev]"` +3. Install pre-commit hooks `pre-commit install` (one-time setup) +4. Run `pytest` to ensure your code passes all tests + +## Code Quality +We use [Ruff](https://github.com/astral-sh/ruff) for linting and formatting. After the one-time setup above, **code quality checks run automatically on every commit**. + +To run manually: +- `ruff check --fix .` to check and fix linting issues +- `ruff format .` to format code + +## Documentation (Optional) +FlixOpt uses [mkdocs](https://www.mkdocs.org/) to generate documentation. +To work on documentation: +```bash +pip install -e ".[docs]" +mkdocs serve +``` +Then navigate to http://127.0.0.1:8000/ + +## Testing +- `pytest` to run the test suite +- You can also run the provided python script `run_all_test.py` --- # Best practices diff --git a/pyproject.toml b/pyproject.toml index 8c846dc03..3c7a6e18d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,6 +62,7 @@ dev = [ "tsam >= 2.3.1, < 3.0.0", # Time series aggregation "scipy >= 1.15.1, < 2.0.0", # Used by tsam. Prior versions have conflict with highspy. See https://github.com/scipy/scipy/issues/22257 "gurobipy >= 10.0.0", + "pre-commit >= 4.0.0", ] full = [ @@ -82,7 +83,7 @@ docs = [ "markdown-include >= 0.8.0", "pymdown-extensions >= 10.0.0", "pygments >= 2.14.0", - "mike >= 1.1.0, < 2", + "mike >= 2.0.0", ] [project.urls] From 97de53c8c4e708031e018308414d6784b62bdcd2 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 23 Jul 2025 15:56:23 +0200 Subject: [PATCH 259/448] Pre commit hook --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c7f512a55..bf913fbb4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,4 +12,4 @@ repos: hooks: - id: ruff-check args: [ --fix ] - - id: ruff-format \ No newline at end of file + - id: ruff-format From 25f726e125245337c6757a2789d3f94b711e19cf Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 23 Jul 2025 16:05:05 +0200 Subject: [PATCH 260/448] Run Pre-Commit Hook for the first time --- .github/CONTRIBUTING.md | 4 +- .github/ISSUE_TEMPLATE/bug_report.yml | 12 +- .github/ISSUE_TEMPLATE/config.yml | 2 +- .github/ISSUE_TEMPLATE/feature_request.yml | 2 +- .github/pull_request_template.md | 2 +- .github/workflows/python-app.yaml | 2 +- .pre-commit-config.yaml | 1 + CHANGELOG.md | 5 +- README.md | 18 +- docs/SUMMARY.md | 2 +- docs/contribute.md | 6 +- docs/examples/00-Minimal Example.md | 2 +- docs/examples/01-Basic Example.md | 2 +- docs/examples/02-Complex Example.md | 2 +- docs/examples/index.md | 2 +- docs/faq/contribute.md | 40 ++- docs/faq/index.md | 2 +- docs/images/flixopt-icon.svg | 2 +- docs/javascripts/mathjax.js | 2 +- docs/user-guide/Mathematical Notation/Bus.md | 2 +- .../Effects, Penalty & Objective.md | 26 +- docs/user-guide/Mathematical Notation/Flow.md | 2 +- .../Mathematical Notation/LinearConverter.md | 4 +- .../Mathematical Notation/Piecewise.md | 2 +- .../Mathematical Notation/Storage.md | 2 +- .../user-guide/Mathematical Notation/index.md | 2 +- .../Mathematical Notation/others.md | 2 +- docs/user-guide/index.md | 4 +- .../02_Complex/complex_example_results.py | 1 - .../example_calculation_types.py | 9 +- examples/04_Scenarios/scenario_example.py | 15 +- .../two_stage_optimization.py | 62 ++-- flixopt/calculation.py | 30 +- flixopt/components.py | 42 ++- flixopt/core.py | 32 +- flixopt/effects.py | 32 +- flixopt/elements.py | 52 ++-- flixopt/features.py | 31 +- flixopt/flow_system.py | 40 +-- flixopt/interface.py | 8 +- flixopt/modeling.py | 44 ++- flixopt/plotting.py | 14 +- flixopt/results.py | 126 ++++---- flixopt/structure.py | 51 ++-- mkdocs.yml | 2 +- pics/flixopt-icon.svg | 2 +- scripts/extract_release_notes.py | 10 +- tests/conftest.py | 32 +- tests/test_bus.py | 40 ++- tests/test_component.py | 147 +++++---- tests/test_cycle_detection.py | 188 +++++------- tests/test_dataconverter.py | 160 ++++------ tests/test_effect.py | 248 +++++++++------ tests/test_effects_shares_summation.py | 26 +- tests/test_flow.py | 284 ++++++++++-------- tests/test_io.py | 11 +- tests/test_linear_converter.py | 177 ++++------- tests/test_results_plots.py | 2 + tests/test_scenarios.py | 153 +++++----- tests/test_storage.py | 132 ++++---- tests/todos.txt | 4 +- 61 files changed, 1245 insertions(+), 1118 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 6ea202fd6..2a51618d9 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -58,7 +58,7 @@ def create_storage( ) -> Storage: """ Create a battery storage component. - + Args: label: Unique identifier capacity_kwh: Storage capacity [kWh] @@ -82,4 +82,4 @@ def create_storage( --- -**Every contribution helps advance sustainable energy solutions! 🌱⚡** \ No newline at end of file +**Every contribution helps advance sustainable energy solutions! 🌱⚡** diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index e7facb6a7..1c5054e49 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -36,11 +36,11 @@ body: Please provide a minimal reproducible example. See how to [craft minimal bug reports](https://matthewrocklin.com/minimal-bug-reports). placeholder: > import flixopt as fx - + # Create simple energy system that reproduces the bug timesteps = pd.date_range('2024-01-01', periods=24, freq='h') flow_system = fx.FlowSystem(timesteps) - + # Add your components here... render: python - type: textarea @@ -69,13 +69,13 @@ body: attributes: label: Installed Versions description: > - Please share information on your environment. Paste the output below. + Please share information on your environment. Paste the output below. For conda: `conda env export` and for pip: `pip freeze`. value: >
- + ``` Replace this with your environment info ``` - -
\ No newline at end of file + + diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index d031f8bfe..5ddb107b1 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -5,4 +5,4 @@ contact_links: about: Ask questions and discuss with the community - name: 📖 Documentation url: https://flixopt.github.io/flixopt/latest/ - about: Check our documentation for guides and examples \ No newline at end of file + about: Check our documentation for guides and examples diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 112a8102c..fd63ae163 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -85,4 +85,4 @@ body: attributes: label: Additional context description: > - Add any other context, research papers, or examples about the feature request here. \ No newline at end of file + Add any other context, research papers, or examples about the feature request here. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 7efaeac97..d5e15137c 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -17,4 +17,4 @@ Closes #(issue number) ## Checklist - [ ] My code follows the project style - [ ] I have updated documentation if needed -- [ ] I have added tests for new functionality (if applicable) \ No newline at end of file +- [ ] I have added tests for new functionality (if applicable) diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml index 3e7ae84ba..a60a2959a 100644 --- a/.github/workflows/python-app.yaml +++ b/.github/workflows/python-app.yaml @@ -220,4 +220,4 @@ jobs: VERSION=${GITHUB_REF#refs/tags/v} echo "Deploying docs after successful PyPI publish: $VERSION" mike deploy --push --update-aliases $VERSION latest - mike set-default --push latest \ No newline at end of file + mike set-default --push latest diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bf913fbb4..e39033067 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,6 +5,7 @@ repos: - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml + exclude: ^mkdocs\.yml$ # Skip mkdocs.yml - id: check-added-large-files - repo: https://github.com/astral-sh/ruff-pre-commit diff --git a/CHANGELOG.md b/CHANGELOG.md index 3404a142f..dd94fb7c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,7 +74,6 @@ This enables to model transformation pathways over multiple years. * Clearer separation between the main Model and "Submodels" * Improved access to the Submodels and their variables, constraints and submodels * Added __repr__() for Submodels to easily inspect its content -* #### Other new features @@ -121,7 +120,7 @@ This enables to model transformation pathways over multiple years. ## [2.1.2] - 2025-06-14 ### Fixed -- Storage losses per hour where not calculated correctly, as mentioned by @brokenwings01. This might have lead to issues with modeling large losses and long timesteps. +- Storage losses per hour where not calculated correctly, as mentioned by @brokenwings01. This might have lead to issues with modeling large losses and long timesteps. - Old implementation: $c(\text{t}_{i}) \cdot (1-\dot{\text{c}}_\text{rel,loss}(\text{t}_i)) \cdot \Delta \text{t}_{i}$ - Correct implementation: $c(\text{t}_{i}) \cdot (1-\dot{\text{c}}_\text{rel,loss}(\text{t}_i)) ^{\Delta \text{t}_{i}}$ @@ -190,4 +189,4 @@ This enables to model transformation pathways over multiple years. ### Removed - **BREAKING**: Pyomo dependency (replaced by linopy) -- Period concepts in time management (simplified to timesteps) \ No newline at end of file +- Period concepts in time management (simplified to timesteps) diff --git a/README.md b/README.md index 615322211..68966910b 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ **flixopt** provides a user-friendly interface with options for advanced users. -It was originally developed by [TU Dresden](https://github.com/gewv-tu-dresden) as part of the SMARTBIOGRID project, funded by the German Federal Ministry for Economic Affairs and Energy (FKZ: 03KB159B). Building on the Matlab-based flixOptMat framework (developed in the FAKS project), FlixOpt also incorporates concepts from [oemof/solph](https://github.com/oemof/oemof-solph). +It was originally developed by [TU Dresden](https://github.com/gewv-tu-dresden) as part of the SMARTBIOGRID project, funded by the German Federal Ministry for Economic Affairs and Energy (FKZ: 03KB159B). Building on the Matlab-based flixOptMat framework (developed in the FAKS project), FlixOpt also incorporates concepts from [oemof/solph](https://github.com/oemof/oemof-solph). --- @@ -43,7 +43,7 @@ It was originally developed by [TU Dresden](https://github.com/gewv-tu-dresden) - **Calculation Modes** - **Full** - Solve the model with highest accuracy and computational requirements. - - **Segmented** - Speed up solving by using a rolling horizon. + - **Segmented** - Speed up solving by using a rolling horizon. - **Aggregated** - Speed up solving by identifying typical periods using [TSAM](https://github.com/FZJ-IEK3-VSA/tsam). Suitable for large models. --- @@ -67,14 +67,14 @@ The documentation is available at [https://flixopt.github.io/flixopt/latest/](ht ## 🎯️ Solver Integration -By default, FlixOpt uses the open-source solver [HiGHS](https://highs.dev/) which is installed by default. However, it is compatible with additional solvers such as: +By default, FlixOpt uses the open-source solver [HiGHS](https://highs.dev/) which is installed by default. However, it is compatible with additional solvers such as: -- [Gurobi](https://www.gurobi.com/) -- [CBC](https://github.com/coin-or/Cbc) +- [Gurobi](https://www.gurobi.com/) +- [CBC](https://github.com/coin-or/Cbc) - [GLPK](https://www.gnu.org/software/glpk/) - [CPLEX](https://www.ibm.com/analytics/cplex-optimizer) -For detailed licensing and installation instructions, refer to the respective solver documentation. +For detailed licensing and installation instructions, refer to the respective solver documentation. --- @@ -85,7 +85,7 @@ Look into our docs for [development setup](https://flixopt.github.io/flixopt/lat ## 📖 Citation -If you use FlixOpt in your research or project, please cite the following: +If you use FlixOpt in your research or project, please cite the following: -- **Main Citation:** [DOI:10.18086/eurosun.2022.04.07](https://doi.org/10.18086/eurosun.2022.04.07) -- **Short Overview:** [DOI:10.13140/RG.2.2.14948.24969](https://doi.org/10.13140/RG.2.2.14948.24969) +- **Main Citation:** [DOI:10.18086/eurosun.2022.04.07](https://doi.org/10.18086/eurosun.2022.04.07) +- **Short Overview:** [DOI:10.13140/RG.2.2.14948.24969](https://doi.org/10.13140/RG.2.2.14948.24969) diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 0e86a81c8..0ae413dd8 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -5,4 +5,4 @@ - [FAQ](faq/) - [API-Reference](api-reference/) - [Contribute](contribute.md) -- [Release Notes](changelog.md) \ No newline at end of file +- [Release Notes](changelog.md) diff --git a/docs/contribute.md b/docs/contribute.md index d23d39d38..ff31c9f1f 100644 --- a/docs/contribute.md +++ b/docs/contribute.md @@ -42,8 +42,8 @@ Then navigate to http://127.0.0.1:8000/ ## Branches As we start to think FlixOpt in **Releases**, we decided to introduce multiple **dev**-branches instead of only one: Following the **Semantic Versioning** guidelines, we introduced: -- `next/patch`: This is where all pull requests for the next patch release (1.0.x) go. -- `next/minor`: This is where all pull requests for the next minor release (1.x.0) go. +- `next/patch`: This is where all pull requests for the next patch release (1.0.x) go. +- `next/minor`: This is where all pull requests for the next minor release (1.x.0) go. - `next/major`: This is where all pull requests for the next major release (x.0.0) go. Everything else remains in `feature/...`-branches. @@ -56,6 +56,6 @@ At some point, `next/minor` or `next/major` will get merged into `main` using a ## Releases As stated, we follow **Semantic Versioning**. Right after one of the 3 [release branches](#branches) is merged into main, a **Tag** should be added to the merge commit and pushed to the main branch. The tag has the form `v1.2.3`. -With this tag, a release with **Release Notes** must be created. +With this tag, a release with **Release Notes** must be created. *This is our current best practice* diff --git a/docs/examples/00-Minimal Example.md b/docs/examples/00-Minimal Example.md index c61283951..a568cd9c9 100644 --- a/docs/examples/00-Minimal Example.md +++ b/docs/examples/00-Minimal Example.md @@ -2,4 +2,4 @@ ```python {! ../examples/00_Minmal/minimal_example.py !} -``` \ No newline at end of file +``` diff --git a/docs/examples/01-Basic Example.md b/docs/examples/01-Basic Example.md index 600f2516a..6c6bfbee3 100644 --- a/docs/examples/01-Basic Example.md +++ b/docs/examples/01-Basic Example.md @@ -2,4 +2,4 @@ ```python {! ../examples/01_Simple/simple_example.py !} -``` \ No newline at end of file +``` diff --git a/docs/examples/02-Complex Example.md b/docs/examples/02-Complex Example.md index d5373c083..48868cdb0 100644 --- a/docs/examples/02-Complex Example.md +++ b/docs/examples/02-Complex Example.md @@ -7,4 +7,4 @@ This saves the results of a calculation to file and reloads them to analyze the ## Load the Results from file ```python {! ../examples/02_Complex/complex_example_results.py !} -``` \ No newline at end of file +``` diff --git a/docs/examples/index.md b/docs/examples/index.md index 8d535771f..1df12dc28 100644 --- a/docs/examples/index.md +++ b/docs/examples/index.md @@ -2,4 +2,4 @@ Here you can find a collection of examples that demonstrate how to use FlixOpt. -We work on improving this gallery. If you have something to share, please contact us! \ No newline at end of file +We work on improving this gallery. If you have something to share, please contact us! diff --git a/docs/faq/contribute.md b/docs/faq/contribute.md index 439fefe1d..ff31c9f1f 100644 --- a/docs/faq/contribute.md +++ b/docs/faq/contribute.md @@ -4,17 +4,29 @@ We warmly welcome contributions from the community! This guide will help you get ## Development Setup 1. Clone the repository `git clone https://github.com/flixOpt/flixopt.git` -2. Install the development dependencies `pip install -editable .[dev, docs]` -3. Run `pytest` and `ruff check .` to ensure your code passes all tests - -## Documentation -FlixOpt uses [mkdocs](https://www.mkdocs.org/) to generate documentation. To preview the documentation locally, run `mkdocs serve` in the root directory. - -## Helpful Commands -- `mkdocs serve` to preview the documentation locally. Navigate to `http://127.0.0.1:8000/` to view the documentation. -- `pytest` to run the test suite (You can also run the provided python script `run_all_test.py`) -- `ruff check .` to run the linter -- `ruff check . --fix` to automatically fix linting issues +2. Install the development dependencies `pip install -e ".[dev]"` +3. Install pre-commit hooks `pre-commit install` (one-time setup) +4. Run `pytest` to ensure your code passes all tests + +## Code Quality +We use [Ruff](https://github.com/astral-sh/ruff) for linting and formatting. After the one-time setup above, **code quality checks run automatically on every commit**. + +To run manually: +- `ruff check --fix .` to check and fix linting issues +- `ruff format .` to format code + +## Documentation (Optional) +FlixOpt uses [mkdocs](https://www.mkdocs.org/) to generate documentation. +To work on documentation: +```bash +pip install -e ".[docs]" +mkdocs serve +``` +Then navigate to http://127.0.0.1:8000/ + +## Testing +- `pytest` to run the test suite +- You can also run the provided python script `run_all_test.py` --- # Best practices @@ -30,8 +42,8 @@ FlixOpt uses [mkdocs](https://www.mkdocs.org/) to generate documentation. To pre ## Branches As we start to think FlixOpt in **Releases**, we decided to introduce multiple **dev**-branches instead of only one: Following the **Semantic Versioning** guidelines, we introduced: -- `next/patch`: This is where all pull requests for the next patch release (1.0.x) go. -- `next/minor`: This is where all pull requests for the next minor release (1.x.0) go. +- `next/patch`: This is where all pull requests for the next patch release (1.0.x) go. +- `next/minor`: This is where all pull requests for the next minor release (1.x.0) go. - `next/major`: This is where all pull requests for the next major release (x.0.0) go. Everything else remains in `feature/...`-branches. @@ -44,6 +56,6 @@ At some point, `next/minor` or `next/major` will get merged into `main` using a ## Releases As stated, we follow **Semantic Versioning**. Right after one of the 3 [release branches](#branches) is merged into main, a **Tag** should be added to the merge commit and pushed to the main branch. The tag has the form `v1.2.3`. -With this tag, a release with **Release Notes** must be created. +With this tag, a release with **Release Notes** must be created. *This is our current best practice* diff --git a/docs/faq/index.md b/docs/faq/index.md index 85d44e6af..6a245edd3 100644 --- a/docs/faq/index.md +++ b/docs/faq/index.md @@ -1,3 +1,3 @@ # Frequently Asked Questions -## Work in progress \ No newline at end of file +## Work in progress diff --git a/docs/images/flixopt-icon.svg b/docs/images/flixopt-icon.svg index 04a6a6851..08fe340f9 100644 --- a/docs/images/flixopt-icon.svg +++ b/docs/images/flixopt-icon.svg @@ -1 +1 @@ -flixOpt \ No newline at end of file +flixOpt diff --git a/docs/javascripts/mathjax.js b/docs/javascripts/mathjax.js index bb7094d50..af5180b57 100644 --- a/docs/javascripts/mathjax.js +++ b/docs/javascripts/mathjax.js @@ -15,4 +15,4 @@ document$.subscribe(() => { MathJax.typesetClear() MathJax.texReset() MathJax.typesetPromise() -}) \ No newline at end of file +}) diff --git a/docs/user-guide/Mathematical Notation/Bus.md b/docs/user-guide/Mathematical Notation/Bus.md index 840c90a08..6ba17eede 100644 --- a/docs/user-guide/Mathematical Notation/Bus.md +++ b/docs/user-guide/Mathematical Notation/Bus.md @@ -30,4 +30,4 @@ With: - $\phi_\text{in}(\text{t}_i)$ and $\phi_\text{out}(\text{t}_i)$ being the missing or excess flow-rate at time $\text{t}_i$, respectively - $\text{t}_i$ being the time step - $s_{b \rightarrow \Phi}(\text{t}_i)$ being the penalty term -- $\text a_{b \rightarrow \Phi}(\text{t}_i)$ being the penalty coefficient (`excess_penalty_per_flow_hour`) \ No newline at end of file +- $\text a_{b \rightarrow \Phi}(\text{t}_i)$ being the penalty coefficient (`excess_penalty_per_flow_hour`) diff --git a/docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md b/docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md index 1f2f0abdb..9e306394a 100644 --- a/docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md +++ b/docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md @@ -8,17 +8,17 @@ These arise from so called **Shares**, which originate from **Elements** like [F Assiziated effects could be: - costs - given in [€/kWh]... - ...or emissions - given in [kg/kWh]. -- +- Effects are allocated seperatly for investments and operation. ### Shares to Effects $$ \label{eq:Share_invest} -s_{l \rightarrow e, \text{inv}} = \sum_{v \in \mathcal{V}_{l, \text{inv}}} v \cdot \text a_{v \rightarrow e} +s_{l \rightarrow e, \text{inv}} = \sum_{v \in \mathcal{V}_{l, \text{inv}}} v \cdot \text a_{v \rightarrow e} $$ $$ \label{eq:Share_operation} -s_{l \rightarrow e, \text{op}}(\text{t}_i) = \sum_{v \in \mathcal{V}_{l,\text{op}}} v(\text{t}_i) \cdot \text a_{v \rightarrow e}(\text{t}_i) +s_{l \rightarrow e, \text{op}}(\text{t}_i) = \sum_{v \in \mathcal{V}_{l,\text{op}}} v(\text{t}_i) \cdot \text a_{v \rightarrow e}(\text{t}_i) $$ With: @@ -36,26 +36,26 @@ With: ### Shares between different Effects -Furthermore, the Effect $x$ can contribute a share to another Effect ${e} \in \mathcal{E}\backslash x$. -This share is defined by the factor $\text r_{x \rightarrow e}$. +Furthermore, the Effect $x$ can contribute a share to another Effect ${e} \in \mathcal{E}\backslash x$. +This share is defined by the factor $\text r_{x \rightarrow e}$. -For example, the Effect "CO$_2$ emissions" (unit: kg) -can cause an additional share to Effect "monetary costs" (unit: €). +For example, the Effect "CO$_2$ emissions" (unit: kg) +can cause an additional share to Effect "monetary costs" (unit: €). In this case, the factor $\text a_{x \rightarrow e}$ is the specific CO$_2$ price in €/kg. However, circular references have to be avoided. The overall sum of investment shares of an Effect $e$ is given by $\eqref{Effect_invest}$ $$ \label{eq:Effect_invest} -E_{e, \text{inv}} = -\sum_{l \in \mathcal{L}} s_{l \rightarrow e,\text{inv}} + +E_{e, \text{inv}} = +\sum_{l \in \mathcal{L}} s_{l \rightarrow e,\text{inv}} + \sum_{x \in \mathcal{E}\backslash e} E_{x, \text{inv}} \cdot \text{r}_{x \rightarrow e,\text{inv}} $$ The overall sum of operation shares is given by $\eqref{eq:Effect_Operation}$ $$ \label{eq:Effect_Operation} -E_{e, \text{op}}(\text{t}_{i}) = -\sum_{l \in \mathcal{L}} s_{l \rightarrow e, \text{op}}(\text{t}_i) + +E_{e, \text{op}}(\text{t}_{i}) = +\sum_{l \in \mathcal{L}} s_{l \rightarrow e, \text{op}}(\text{t}_i) + \sum_{x \in \mathcal{E}\backslash e} E_{x, \text{op}}(\text{t}_i) \cdot \text{r}_{x \rightarrow {e},\text{op}}(\text{t}_i) $$ @@ -100,7 +100,7 @@ $$ Additionally to the user defined [Effects](#effects), a Penalty $\Phi$ is part of every FlixOpt Model. Its used to prevent unsolvable problems and simplify troubleshooting. -Shares to the penalty can originate from every Element and are constructed similarly to +Shares to the penalty can originate from every Element and are constructed similarly to $\eqref{Share_invest}$ and $\eqref{Share_operation}$. $$ \label{eq:Penalty} @@ -129,4 +129,4 @@ With: This approach allows for a multi-criteria optimization using both... - ... the **Weigted Sum**Method, as the chosen **Objective Effect** can incorporate other Effects. - - ... the ($\epsilon$-constraint method) by constraining effects. \ No newline at end of file + - ... the ($\epsilon$-constraint method) by constraining effects. diff --git a/docs/user-guide/Mathematical Notation/Flow.md b/docs/user-guide/Mathematical Notation/Flow.md index 4b755d005..142904a1d 100644 --- a/docs/user-guide/Mathematical Notation/Flow.md +++ b/docs/user-guide/Mathematical Notation/Flow.md @@ -23,4 +23,4 @@ $$ This mathematical Formulation can be extended or changed when using [OnOffParameters](#onoffparameters) to define the On/Off state of the Flow, or [InvestParameters](#investments), -which changes the size of the Flow from a constant to an optimization variable. \ No newline at end of file +which changes the size of the Flow from a constant to an optimization variable. diff --git a/docs/user-guide/Mathematical Notation/LinearConverter.md b/docs/user-guide/Mathematical Notation/LinearConverter.md index a8cea843e..124d37c8c 100644 --- a/docs/user-guide/Mathematical Notation/LinearConverter.md +++ b/docs/user-guide/Mathematical Notation/LinearConverter.md @@ -10,7 +10,7 @@ With: - $p_{f_\text{in}}(\text{t}_i)$ and $p_{f_\text{out}}(\text{t}_i)$ being the flow-rate at time $\text{t}_i$ for flow $f_\text{in}$ and $f_\text{out}$, respectively - $\text a_{f_\text{in}}(\text{t}_i)$ and $\text b_{f_\text{out}}(\text{t}_i)$ being the ratio of the flow-rate at time $\text{t}_i$ for flow $f_\text{in}$ and $f_\text{out}$, respectively -With one incoming **Flow** and one outgoing **Flow**, this can be simplified to: +With one incoming **Flow** and one outgoing **Flow**, this can be simplified to: $$ \label{eq:Linear-Transformer-Ratio-simple} \text a(\text{t}_i) \cdot p_{f_\text{in}}(\text{t}_i) = p_{f_\text{out}}(\text{t}_i) @@ -18,4 +18,4 @@ $$ where $\text a$ can be interpreted as the conversion efficiency of the **LinearTransformer**. #### Piecewise Concersion factors -The conversion efficiency can be defined as a piecewise linear approximation. See [Piecewise](Piecewise.md) for more details. \ No newline at end of file +The conversion efficiency can be defined as a piecewise linear approximation. See [Piecewise](Piecewise.md) for more details. diff --git a/docs/user-guide/Mathematical Notation/Piecewise.md b/docs/user-guide/Mathematical Notation/Piecewise.md index 4e73cfece..688ac8cea 100644 --- a/docs/user-guide/Mathematical Notation/Piecewise.md +++ b/docs/user-guide/Mathematical Notation/Piecewise.md @@ -40,7 +40,7 @@ Which can also be described as $v \in \{0\} \cup [\text{v}_{\text{start_k}}, \te ## Combining multiple Piecewises -Piecewise allows representing non-linear relationships. +Piecewise allows representing non-linear relationships. This is a powerful technique in linear optimization to model non-linear behaviors while maintaining the problem's linearity. Therefore, each Piecewise must have the same number of Pieces $k$. diff --git a/docs/user-guide/Mathematical Notation/Storage.md b/docs/user-guide/Mathematical Notation/Storage.md index db78b6ab3..577f12150 100644 --- a/docs/user-guide/Mathematical Notation/Storage.md +++ b/docs/user-guide/Mathematical Notation/Storage.md @@ -41,4 +41,4 @@ Where: - $p_{f_\text{in}}(\text{t}_i)$ is the input flow rate at time $\text{t}_i$ - $\eta_\text{in}(\text{t}_i)$ is the charging efficiency at time $\text{t}_i$ - $p_{f_\text{out}}(\text{t}_i)$ is the output flow rate at time $\text{t}_i$ -- $\eta_\text{out}(\text{t}_i)$ is the discharging efficiency at time $\text{t}_i$ \ No newline at end of file +- $\eta_\text{out}(\text{t}_i)$ is the discharging efficiency at time $\text{t}_i$ diff --git a/docs/user-guide/Mathematical Notation/index.md b/docs/user-guide/Mathematical Notation/index.md index 4dabe2af2..b76a1ba1f 100644 --- a/docs/user-guide/Mathematical Notation/index.md +++ b/docs/user-guide/Mathematical Notation/index.md @@ -14,7 +14,7 @@ FlixOpt uses the following naming conventions: ## Timesteps Time steps are defined as a sequence of discrete time steps $\text{t}_i \in \mathcal{T} \quad \text{for} \quad i \in \{1, 2, \dots, \text{n}\}$ (left-aligned in its timespan). -From this sequence, the corresponding time intervals $\Delta \text{t}_i \in \Delta \mathcal{T}$ are derived as +From this sequence, the corresponding time intervals $\Delta \text{t}_i \in \Delta \mathcal{T}$ are derived as $$\Delta \text{t}_i = \text{t}_{i+1} - \text{t}_i \quad \text{for} \quad i \in \{1, 2, \dots, \text{n}-1\}$$ diff --git a/docs/user-guide/Mathematical Notation/others.md b/docs/user-guide/Mathematical Notation/others.md index 0cd82de94..bdc602308 100644 --- a/docs/user-guide/Mathematical Notation/others.md +++ b/docs/user-guide/Mathematical Notation/others.md @@ -1,3 +1,3 @@ # Work in Progress -This is a work in progress. \ No newline at end of file +This is a work in progress. diff --git a/docs/user-guide/index.md b/docs/user-guide/index.md index 8789779b2..bc1738997 100644 --- a/docs/user-guide/index.md +++ b/docs/user-guide/index.md @@ -6,7 +6,7 @@ FlixOpt is built around a set of core concepts that work together to represent a ### FlowSystem -The [`FlowSystem`][flixopt.flow_system.FlowSystem] is the central organizing unit in FlixOpt. +The [`FlowSystem`][flixopt.flow_system.FlowSystem] is the central organizing unit in FlixOpt. Every FlixOpt model starts with creating a FlowSystem. It: - Defines the timesteps for the optimization @@ -40,7 +40,7 @@ Examples: [`Bus`][flixopt.elements.Bus] objects represent nodes or connection points in a FlowSystem. They: - Balance incoming and outgoing flows -- Can represent physical networks like heat, electricity, or gas +- Can represent physical networks like heat, electricity, or gas - Handle infeasible balances gently by allowing the balance to be closed in return for a big Penalty (optional) ### Components diff --git a/examples/02_Complex/complex_example_results.py b/examples/02_Complex/complex_example_results.py index f4428d5ed..1c2766774 100644 --- a/examples/02_Complex/complex_example_results.py +++ b/examples/02_Complex/complex_example_results.py @@ -35,4 +35,3 @@ # Dataframes from results: fw_bus = results['Fernwärme'].node_balance().to_dataframe() all = results.solution.to_dataframe() - diff --git a/examples/03_Calculation_types/example_calculation_types.py b/examples/03_Calculation_types/example_calculation_types.py index 8bbdf1773..4991075b8 100644 --- a/examples/03_Calculation_types/example_calculation_types.py +++ b/examples/03_Calculation_types/example_calculation_types.py @@ -164,12 +164,12 @@ if full: calculation = fx.FullCalculation('Full', flow_system) calculation.do_modeling() - calculation.solve(fx.solvers.HighsSolver(0.01/100, 60)) + calculation.solve(fx.solvers.HighsSolver(0.01 / 100, 60)) calculations.append(calculation) if segmented: calculation = fx.SegmentedCalculation('Segmented', flow_system, segment_length, overlap_length) - calculation.do_modeling_and_solve(fx.solvers.HighsSolver(0.01/100, 60)) + calculation.do_modeling_and_solve(fx.solvers.HighsSolver(0.01 / 100, 60)) calculations.append(calculation) if aggregated: @@ -178,7 +178,7 @@ aggregation_parameters.time_series_for_low_peaks = [TS_electricity_demand, TS_heat_demand] calculation = fx.AggregatedCalculation('Aggregated', flow_system, aggregation_parameters) calculation.do_modeling() - calculation.solve(fx.solvers.HighsSolver(0.01/100, 60)) + calculation.solve(fx.solvers.HighsSolver(0.01 / 100, 60)) calculations.append(calculation) # Get solutions for plotting for different calculations @@ -221,7 +221,8 @@ def get_solutions(calcs: List, variable: str) -> xr.Dataset: ).update_layout(barmode='group').write_html('results/Total Costs.html') fx.plotting.with_plotly( - pd.DataFrame([calc.durations for calc in calculations], index=[calc.name for calc in calculations]), 'stacked_bar' + pd.DataFrame([calc.durations for calc in calculations], index=[calc.name for calc in calculations]), + 'stacked_bar', ).update_layout(title='Duration Comparison', xaxis_title='Calculation type', yaxis_title='Time (s)').write_html( 'results/Speed Comparison.html' ) diff --git a/examples/04_Scenarios/scenario_example.py b/examples/04_Scenarios/scenario_example.py index 5295d2820..62f3e1c82 100644 --- a/examples/04_Scenarios/scenario_example.py +++ b/examples/04_Scenarios/scenario_example.py @@ -16,8 +16,10 @@ # --- Create Time Series Data --- # Heat demand profile (e.g., kW) over time and corresponding power prices - heat_demand_per_h = pd.DataFrame({'Base Case':[30, 0, 90, 110, 110, 20, 20, 20, 20], - 'High Demand':[30, 0, 100, 118, 125, 20, 20, 20, 20]}, index=timesteps) + heat_demand_per_h = pd.DataFrame( + {'Base Case': [30, 0, 90, 110, 110, 20, 20, 20, 20], 'High Demand': [30, 0, 100, 118, 125, 20, 20, 20, 20]}, + index=timesteps, + ) power_prices = np.array([0.08, 0.09, 0.10]) flow_system = fx.FlowSystem(timesteps=timesteps, years=years, scenarios=scenarios, weights=np.array([0.5, 0.6])) @@ -50,7 +52,14 @@ boiler = fx.linear_converters.Boiler( label='Boiler', eta=0.5, - Q_th=fx.Flow(label='Q_th', bus='Fernwärme', size=50, relative_minimum=0.1, relative_maximum=1, on_off_parameters=fx.OnOffParameters()), + Q_th=fx.Flow( + label='Q_th', + bus='Fernwärme', + size=50, + relative_minimum=0.1, + relative_maximum=1, + on_off_parameters=fx.OnOffParameters(), + ), Q_fu=fx.Flow(label='Q_fu', bus='Gas'), ) diff --git a/examples/05_Two-stage-optimization/two_stage_optimization.py b/examples/05_Two-stage-optimization/two_stage_optimization.py index 3548726b4..eee8dd92d 100644 --- a/examples/05_Two-stage-optimization/two_stage_optimization.py +++ b/examples/05_Two-stage-optimization/two_stage_optimization.py @@ -58,16 +58,24 @@ 'BHKW2', eta_th=0.58, eta_el=0.22, - on_off_parameters=fx.OnOffParameters(effects_per_switch_on=1_000, consecutive_on_hours_min=10, consecutive_off_hours_min=10), + on_off_parameters=fx.OnOffParameters( + effects_per_switch_on=1_000, consecutive_on_hours_min=10, consecutive_off_hours_min=10 + ), P_el=fx.Flow('P_el', bus='Strom'), Q_th=fx.Flow('Q_th', bus='Fernwärme'), - Q_fu=fx.Flow('Q_fu', bus='Kohle', - size=fx.InvestParameters(specific_effects={'costs':3_000}, minimum_size=10, maximum_size=500), - relative_minimum=0.3, previous_flow_rate=100), + Q_fu=fx.Flow( + 'Q_fu', + bus='Kohle', + size=fx.InvestParameters(specific_effects={'costs': 3_000}, minimum_size=10, maximum_size=500), + relative_minimum=0.3, + previous_flow_rate=100, + ), ), fx.Storage( 'Speicher', - capacity_in_flow_hours=fx.InvestParameters(minimum_size=10, maximum_size=1000, specific_effects={'costs': 60}), + capacity_in_flow_hours=fx.InvestParameters( + minimum_size=10, maximum_size=1000, specific_effects={'costs': 60} + ), initial_charge_state='lastValueOfSim', eta_charge=1, eta_discharge=1, @@ -76,9 +84,7 @@ charging=fx.Flow('Q_th_load', size=137, bus='Fernwärme'), discharging=fx.Flow('Q_th_unload', size=158, bus='Fernwärme'), ), - fx.Sink( - 'Wärmelast', sink=fx.Flow('Q_th_Last', bus='Fernwärme', size=1, fixed_relative_profile=heat_demand) - ), + fx.Sink('Wärmelast', sink=fx.Flow('Q_th_Last', bus='Fernwärme', size=1, fixed_relative_profile=heat_demand)), fx.Source( 'Gastarif', source=fx.Flow('Q_Gas', bus='Gas', size=1000, effects_per_flow_hour={'costs': gas_price, 'CO2': 0.3}), @@ -89,7 +95,9 @@ ), fx.Source( 'Einspeisung', - source=fx.Flow('P_el', bus='Strom', size=1000, effects_per_flow_hour={'costs': electricity_price + 0.5, 'CO2': 0.3}), + source=fx.Flow( + 'P_el', bus='Strom', size=1000, effects_per_flow_hour={'costs': electricity_price + 0.5, 'CO2': 0.3} + ), ), fx.Sink( 'Stromlast', @@ -97,7 +105,9 @@ ), fx.Source( 'Stromtarif', - source=fx.Flow('P_el', bus='Strom', size=1000, effects_per_flow_hour={'costs': electricity_price, 'CO2': 0.3}), + source=fx.Flow( + 'P_el', bus='Strom', size=1000, effects_per_flow_hour={'costs': electricity_price, 'CO2': 0.3} + ), ), ) @@ -105,7 +115,7 @@ start = timeit.default_timer() calculation_sizing = fx.FullCalculation('Sizing', flow_system.resample('4h')) calculation_sizing.do_modeling() - calculation_sizing.solve(fx.solvers.HighsSolver(0.1/100, 600)) + calculation_sizing.solve(fx.solvers.HighsSolver(0.1 / 100, 600)) timer_sizing = timeit.default_timer() - start calculation_dispatch = fx.FullCalculation('Sizing', flow_system) @@ -123,7 +133,7 @@ start = timeit.default_timer() calculation_combined = fx.FullCalculation('Sizing', flow_system) calculation_combined.do_modeling() - calculation_combined.solve(fx.solvers.HighsSolver(0.1/100, 600)) + calculation_combined.solve(fx.solvers.HighsSolver(0.1 / 100, 600)) timer_combined = timeit.default_timer() - start # Comparison of results @@ -132,11 +142,27 @@ ).assign_coords(mode=['Combined', 'Two-stage']) comparison['Duration [s]'] = xr.DataArray([timer_combined, timer_sizing + timer_dispatch], dims='mode') - comparison_main = comparison[['Duration [s]', 'costs|total', 'costs(invest)|total', 'costs(operation)|total', 'BHKW2(Q_fu)|size', 'Kessel(Q_fu)|size', 'Speicher|size']] - comparison_main = xr.concat([ - comparison_main, - ((comparison_main.sel(mode='Two-stage') - comparison_main.sel(mode='Combined')) - / comparison_main.sel(mode='Combined') * 100).assign_coords(mode='Diff [%]') - ], dim='mode') + comparison_main = comparison[ + [ + 'Duration [s]', + 'costs|total', + 'costs(invest)|total', + 'costs(operation)|total', + 'BHKW2(Q_fu)|size', + 'Kessel(Q_fu)|size', + 'Speicher|size', + ] + ] + comparison_main = xr.concat( + [ + comparison_main, + ( + (comparison_main.sel(mode='Two-stage') - comparison_main.sel(mode='Combined')) + / comparison_main.sel(mode='Combined') + * 100 + ).assign_coords(mode='Diff [%]'), + ], + dim='mode', + ) print(comparison_main.to_pandas().T.round(2)) diff --git a/flixopt/calculation.py b/flixopt/calculation.py index 3137b71ec..d4d4e306d 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -136,8 +136,7 @@ def main_results(self) -> Dict[str, Union[Scalar, Dict]]: for bus in self.flow_system.buses.values() if bus.with_excess and ( - bus.submodel.excess_input.solution.sum() > 1e-3 - or bus.submodel.excess_output.solution.sum() > 1e-3 + bus.submodel.excess_input.solution.sum() > 1e-3 or bus.submodel.excess_output.solution.sum() > 1e-3 ) ], } @@ -213,7 +212,9 @@ def fix_sizes(self, ds: xr.Dataset, decimal_rounding: Optional[int] = 5) -> 'Ful return self - def solve(self, solver: _Solver, log_file: Optional[pathlib.Path] = None, log_main_results: bool = True) -> 'FullCalculation': + def solve( + self, solver: _Solver, log_file: Optional[pathlib.Path] = None, log_main_results: bool = True + ) -> 'FullCalculation': t_start = timeit.default_timer() self.model.solve( @@ -323,13 +324,9 @@ def _perform_aggregation(self): f'Aggregation failed due to inconsistent time step sizes:' f'delta_t varies from {dt_min} to {dt_max} hours.' ) - steps_per_period = ( - self.aggregation_parameters.hours_per_period - / self.flow_system.hours_per_timestep.max() - ) + steps_per_period = self.aggregation_parameters.hours_per_period / self.flow_system.hours_per_timestep.max() is_integer = ( - self.aggregation_parameters.hours_per_period - % self.flow_system.hours_per_timestep.max() + self.aggregation_parameters.hours_per_period % self.flow_system.hours_per_timestep.max() ).item() == 0 if not (steps_per_period.size == 1 and is_integer): raise ValueError( @@ -360,7 +357,11 @@ def _perform_aggregation(self): if self.aggregation_parameters.aggregate_data_and_fix_non_binary_vars: ds = self.flow_system.to_dataset() for name, series in self.aggregation.aggregated_data.items(): - da = DataConverter.to_dataarray(series, self.flow_system.coords).rename(name).assign_attrs(ds[name].attrs) + da = ( + DataConverter.to_dataarray(series, self.flow_system.coords) + .rename(name) + .assign_attrs(ds[name].attrs) + ) if TimeSeriesData.is_timeseries_data(da): da = TimeSeriesData.from_dataarray(da) @@ -429,7 +430,6 @@ def __init__( self.nr_of_previous_values = nr_of_previous_values self.sub_calculations: List[FullCalculation] = [] - self.segment_names = [ f'Segment_{i + 1}' for i in range(math.ceil(len(self.all_timesteps) / self.timesteps_per_segment)) ] @@ -525,7 +525,7 @@ def _transfer_start_values(self, i: int): logger.debug( f'Start of next segment: {start}. Indices of previous values: {start_previous_values} -> {end_previous_values}' ) - current_flow_system = self.sub_calculations[i -1].flow_system + current_flow_system = self.sub_calculations[i - 1].flow_system next_flow_system = self.sub_calculations[i].flow_system start_values_of_this_segment = {} @@ -560,9 +560,9 @@ def timesteps_per_segment_with_overlap(self): @property def start_values_of_segments(self) -> List[Dict[str, Any]]: """Gives an overview of the start values of all Segments""" - return [ - {name: value for name, value in self._original_start_values.items()} - ] + [start_values for start_values in self._transfered_start_values] + return [{name: value for name, value in self._original_start_values.items()}] + [ + start_values for start_values in self._transfered_start_values + ] @property def all_timesteps(self) -> pd.DatetimeIndex: diff --git a/flixopt/components.py b/flixopt/components.py index c0e905c5f..d483ee28c 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -232,10 +232,14 @@ def transform_data(self, flow_system: 'FlowSystem') -> None: f'{self.label_full}|maximal_final_charge_state', self.maximal_final_charge_state, has_time_dim=False ) self.relative_minimum_final_charge_state = flow_system.fit_to_model_coords( - f'{self.label_full}|relative_minimum_final_charge_state', self.relative_minimum_final_charge_state, has_time_dim=False + f'{self.label_full}|relative_minimum_final_charge_state', + self.relative_minimum_final_charge_state, + has_time_dim=False, ) self.relative_maximum_final_charge_state = flow_system.fit_to_model_coords( - f'{self.label_full}|relative_maximum_final_charge_state', self.relative_maximum_final_charge_state, has_time_dim=False + f'{self.label_full}|relative_maximum_final_charge_state', + self.relative_maximum_final_charge_state, + has_time_dim=False, ) if isinstance(self.capacity_in_flow_hours, InvestParameters): self.capacity_in_flow_hours.transform_data(flow_system, f'{self.label_full}|InvestParameters') @@ -281,16 +285,21 @@ def _plausibility_checks(self) -> None: ) if self.balanced: - if not isinstance(self.charging.size, InvestParameters) or not isinstance(self.discharging.size, InvestParameters): + if not isinstance(self.charging.size, InvestParameters) or not isinstance( + self.discharging.size, InvestParameters + ): raise PlausibilityError( - f'Balancing charging and discharging Flows in {self.label_full} ' - f'is only possible with Investments.') - if (self.charging.size.minimum_size > self.discharging.size.maximum_size or - self.charging.size.maximum_size < self.discharging.size.minimum_size): + f'Balancing charging and discharging Flows in {self.label_full} is only possible with Investments.' + ) + if ( + self.charging.size.minimum_size > self.discharging.size.maximum_size + or self.charging.size.maximum_size < self.discharging.size.minimum_size + ): raise PlausibilityError( f'Balancing charging and discharging Flows in {self.label_full} need compatible minimum and maximum sizes.' f'Got: {self.charging.size.minimum_size=}, {self.charging.size.maximum_size=} and ' - f'{self.charging.size.minimum_size=}, {self.charging.size.maximum_size=}.') + f'{self.charging.size.minimum_size=}, {self.charging.size.maximum_size=}.' + ) @register_class_for_io @@ -369,14 +378,14 @@ def _plausibility_checks(self): raise ValueError('Balanced Transmission needs InvestParameters in both in-Flows') if not isinstance(self.in1.size, InvestParameters) or not isinstance(self.in2.size, InvestParameters): raise ValueError('Balanced Transmission needs InvestParameters in both in-Flows') - if ( - (self.in1.size.minimum_or_fixed_size > self.in2.size.maximum_or_fixed_size).any() or - (self.in1.size.maximum_or_fixed_size < self.in2.size.minimum_or_fixed_size).any() - ): + if (self.in1.size.minimum_or_fixed_size > self.in2.size.maximum_or_fixed_size).any() or ( + self.in1.size.maximum_or_fixed_size < self.in2.size.minimum_or_fixed_size + ).any(): raise ValueError( f'Balanced Transmission needs compatible minimum and maximum sizes.' f'Got: {self.in1.size.minimum_size=}, {self.in1.size.maximum_size=}, {self.in1.size.fixed_size=} and ' - f'{self.in2.size.minimum_size=}, {self.in2.size.maximum_size=}, {self.in2.size.fixed_size=}.') + f'{self.in2.size.minimum_size=}, {self.in2.size.maximum_size=}, {self.in2.size.fixed_size=}.' + ) def create_model(self, model) -> 'TransmissionModel': self._plausibility_checks() @@ -485,6 +494,7 @@ def _do_modeling(self): class StorageModel(ComponentModel): """Submodel of Storage""" + element: Storage def __init__(self, model: FlowSystemModel, element: Storage): @@ -550,7 +560,8 @@ def _do_modeling(self): if self.element.balanced: self.add_constraints( - self.element.charging.submodel._investment.size * 1 == self.element.discharging.submodel._investment.size * 1, + self.element.charging.submodel._investment.size * 1 + == self.element.discharging.submodel._investment.size * 1, short_name='balanced_sizes', ) @@ -562,7 +573,8 @@ def _initial_and_final_charge_state(self): ) else: self.add_constraints( - self.charge_state.isel(time=0) == self.element.initial_charge_state, short_name='initial_charge_state' + self.charge_state.isel(time=0) == self.element.initial_charge_state, + short_name='initial_charge_state', ) if self.element.maximal_final_charge_state is not None: diff --git a/flixopt/core.py b/flixopt/core.py index 99b69b5ed..36d19dea4 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -52,13 +52,13 @@ class TimeSeriesData(xr.DataArray): __slots__ = () # No additional instance attributes - everything goes in attrs def __init__( - self, - *args, - aggregation_group: Optional[str] = None, - aggregation_weight: Optional[float] = None, - agg_group: Optional[str] = None, - agg_weight: Optional[float] = None, - **kwargs + self, + *args, + aggregation_group: Optional[str] = None, + aggregation_weight: Optional[float] = None, + agg_group: Optional[str] = None, + agg_weight: Optional[float] = None, + **kwargs, ): """ Args: @@ -105,7 +105,7 @@ def fit_to_coords( da, aggregation_group=self.aggregation_group, aggregation_weight=self.aggregation_weight, - name=name if name is not None else self.name + name=name if name is not None else self.name, ) @property @@ -117,11 +117,17 @@ def aggregation_weight(self) -> Optional[float]: return self.attrs.get('aggregation_weight') @classmethod - def from_dataarray(cls, da: xr.DataArray, aggregation_group: Optional[str] = None, aggregation_weight: Optional[float] = None): + def from_dataarray( + cls, da: xr.DataArray, aggregation_group: Optional[str] = None, aggregation_weight: Optional[float] = None + ): """Create TimeSeriesData from DataArray, extracting metadata from attrs.""" # Get aggregation metadata from attrs or parameters - final_aggregation_group = aggregation_group if aggregation_group is not None else da.attrs.get('aggregation_group') - final_aggregation_weight = aggregation_weight if aggregation_weight is not None else da.attrs.get('aggregation_weight') + final_aggregation_group = ( + aggregation_group if aggregation_group is not None else da.attrs.get('aggregation_group') + ) + final_aggregation_weight = ( + aggregation_weight if aggregation_weight is not None else da.attrs.get('aggregation_weight') + ) return cls(da, aggregation_group=final_aggregation_group, aggregation_weight=final_aggregation_weight) @@ -185,7 +191,9 @@ def _match_series_to_dimension( """ if len(target_dims) == 0: if len(data) != 1: - raise ConversionError(f'Cannot convert multi-element Series without target dimensions. Got \n{data}\n and \n{coords}') + raise ConversionError( + f'Cannot convert multi-element Series without target dimensions. Got \n{data}\n and \n{coords}' + ) return xr.DataArray(data.iloc[0]) # Try to match Series index to coordinates diff --git a/flixopt/effects.py b/flixopt/effects.py index db72ff8aa..79a44e67a 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -117,14 +117,15 @@ def transform_data(self, flow_system: 'FlowSystem'): f'{self.label_full}|maximum_invest', self.maximum_invest, has_time_dim=False ) self.minimum_total = flow_system.fit_to_model_coords( - f'{self.label_full}|minimum_total', self.minimum_total, has_time_dim=False, + f'{self.label_full}|minimum_total', + self.minimum_total, + has_time_dim=False, ) self.maximum_total = flow_system.fit_to_model_coords( f'{self.label_full}|maximum_total', self.maximum_total, has_time_dim=False ) self.specific_share_to_other_effects_invest = flow_system.fit_effects_to_model_coords( - f'{self.label_full}|invest->', self.specific_share_to_other_effects_invest, 'invest', - has_time_dim=False + f'{self.label_full}|invest->', self.specific_share_to_other_effects_invest, 'invest', has_time_dim=False ) def create_model(self, model: FlowSystemModel) -> 'EffectModel': @@ -230,8 +231,7 @@ def add_effects(self, *effects: Effect) -> None: logger.info(f'Registered new Effect: {effect.label}') def create_effect_values_dict( - self, - effect_values_user: Union[NonTemporalEffectsUser, TemporalEffectsUser] + self, effect_values_user: Union[NonTemporalEffectsUser, TemporalEffectsUser] ) -> Optional[Dict[str, Union[Scalar, TemporalDataUser]]]: """ Converts effect values into a dictionary. If a scalar is provided, it is associated with a default effect type. @@ -277,11 +277,11 @@ def _plausibility_checks(self) -> None: invest_cycles = detect_cycles(tuples_to_adjacency_list([key for key in invest])) if operation_cycles: - cycle_str = "\n".join([" -> ".join(cycle) for cycle in operation_cycles]) + cycle_str = '\n'.join([' -> '.join(cycle) for cycle in operation_cycles]) raise ValueError(f'Error: circular operation-shares detected:\n{cycle_str}') if invest_cycles: - cycle_str = "\n".join([" -> ".join(cycle) for cycle in invest_cycles]) + cycle_str = '\n'.join([' -> '.join(cycle) for cycle in invest_cycles]) raise ValueError(f'Error: circular invest-shares detected:\n{cycle_str}') def __getitem__(self, effect: Union[str, Effect]) -> 'Effect': @@ -349,7 +349,9 @@ def objective_effect(self, value: Effect) -> None: raise ValueError(f'An objective-effect already exists! ({self._objective_effect.label=})') self._objective_effect = value - def calculate_effect_share_factors(self) -> Tuple[ + def calculate_effect_share_factors( + self, + ) -> Tuple[ Dict[Tuple[str, str], xr.DataArray], Dict[Tuple[str, str], xr.DataArray], ]: @@ -357,8 +359,7 @@ def calculate_effect_share_factors(self) -> Tuple[ for name, effect in self.effects.items(): if effect.specific_share_to_other_effects_invest: shares_invest[name] = { - target: data - for target, data in effect.specific_share_to_other_effects_invest.items() + target: data for target, data in effect.specific_share_to_other_effects_invest.items() } shares_invest = calculate_all_conversion_paths(shares_invest) @@ -366,8 +367,7 @@ def calculate_effect_share_factors(self) -> Tuple[ for name, effect in self.effects.items(): if effect.specific_share_to_other_effects_operation: shares_operation[name] = { - target: data - for target, data in effect.specific_share_to_other_effects_operation.items() + target: data for target, data in effect.specific_share_to_other_effects_operation.items() } shares_operation = calculate_all_conversion_paths(shares_operation) @@ -423,8 +423,7 @@ def _do_modeling(self): self._add_share_between_effects() self._model.add_objective( - (self.effects.objective_effect.submodel.total * self._model.weights).sum() - + self.penalty.total.sum() + (self.effects.objective_effect.submodel.total * self._model.weights).sum() + self.penalty.total.sum() ) def _add_share_between_effects(self): @@ -446,7 +445,7 @@ def _add_share_between_effects(self): def calculate_all_conversion_paths( - conversion_dict: Dict[str, Dict[str, xr.DataArray]], + conversion_dict: Dict[str, Dict[str, xr.DataArray]], ) -> Dict[Tuple[str, str], xr.DataArray]: """ Calculates all possible direct and indirect conversion factors between units/domains. @@ -511,8 +510,7 @@ def calculate_all_conversion_paths( queue.append((target, indirect_factor, new_path)) # Convert all values to DataArrays - result = {key: value if isinstance(value, xr.DataArray) else xr.DataArray(value) - for key, value in result.items()} + result = {key: value if isinstance(value, xr.DataArray) else xr.DataArray(value) for key, value in result.items()} return result diff --git a/flixopt/elements.py b/flixopt/elements.py index 7acf8b0a9..499ab66db 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -95,7 +95,10 @@ class Bus(Element): """ def __init__( - self, label: str, excess_penalty_per_flow_hour: Optional[TemporalDataUser] = 1e5, meta_data: Optional[Dict] = None + self, + label: str, + excess_penalty_per_flow_hour: Optional[TemporalDataUser] = 1e5, + meta_data: Optional[Dict] = None, ): """ Args: @@ -122,7 +125,9 @@ def transform_data(self, flow_system: 'FlowSystem'): def _plausibility_checks(self) -> None: if self.excess_penalty_per_flow_hour is not None and (self.excess_penalty_per_flow_hour == 0).all(): - logger.warning(f'In Bus {self.label_full}, the excess_penalty_per_flow_hour is 0. Use "None" or a value > 0.') + logger.warning( + f'In Bus {self.label_full}, the excess_penalty_per_flow_hour is 0. Use "None" or a value > 0.' + ) @property def with_excess(self) -> bool: @@ -271,7 +276,7 @@ def _plausibility_checks(self) -> None: raise PlausibilityError(self.label_full + ': Take care, that relative_minimum <= relative_maximum!') if not isinstance(self.size, InvestParameters) and ( - np.any(self.size == CONFIG.modeling.BIG) and self.fixed_relative_profile is not None + np.any(self.size == CONFIG.modeling.BIG) and self.fixed_relative_profile is not None ): # Default Size --> Most likely by accident logger.warning( f'Flow "{self.label_full}" has no size assigned, but a "fixed_relative_profile". ' @@ -293,9 +298,15 @@ def _plausibility_checks(self) -> None: ) if self.previous_flow_rate is not None: - if not any([isinstance(self.previous_flow_rate, np.ndarray) and self.previous_flow_rate.ndim == 1, - isinstance(self.previous_flow_rate, (int, float, list))]): - raise TypeError(f'previous_flow_rate must be None, a scalar, a list of scalars or a 1D-numpy-array. Got {type(self.previous_flow_rate)}') + if not any( + [ + isinstance(self.previous_flow_rate, np.ndarray) and self.previous_flow_rate.ndim == 1, + isinstance(self.previous_flow_rate, (int, float, list)), + ] + ): + raise TypeError( + f'previous_flow_rate must be None, a scalar, a list of scalars or a 1D-numpy-array. Got {type(self.previous_flow_rate)}' + ) @property def label_full(self) -> str: @@ -458,7 +469,7 @@ def _create_shares(self): def _create_bounds_for_load_factor(self): """Create load factor constraints using current approach""" # Get the size (either from element or investment) - size = self.investment.size if self.with_investment else self.element.size + size = self.investment.size if self.with_investment else self.element.size # Maximum load factor constraint if self.element.load_factor_max is not None: @@ -528,15 +539,14 @@ def investment(self) -> Optional[InvestmentModel]: @property def previous_states(self) -> Optional[TemporalData]: """Previous states of the flow rate""" - #TODO: This would be nicer to handle in the Flow itself, and allow DataArrays as well. + # TODO: This would be nicer to handle in the Flow itself, and allow DataArrays as well. previous_flow_rate = self.element.previous_flow_rate if previous_flow_rate is None: return None return ModelingUtilitiesAbstract.to_binary( values=xr.DataArray( - [previous_flow_rate] if np.isscalar(previous_flow_rate) else previous_flow_rate, - dims='time' + [previous_flow_rate] if np.isscalar(previous_flow_rate) else previous_flow_rate, dims='time' ), epsilon=CONFIG.modeling.EPSILON, dims='time', @@ -566,7 +576,9 @@ def _do_modeling(self) -> None: self.excess_input = self.add_variables(lower=0, coords=self._model.get_coords(), short_name='excess_input') - self.excess_output = self.add_variables(lower=0, coords=self._model.get_coords(), short_name='excess_output') + self.excess_output = self.add_variables( + lower=0, coords=self._model.get_coords(), short_name='excess_output' + ) eq_bus_balance.lhs -= -self.excess_input + self.excess_output @@ -580,8 +592,12 @@ def results_structure(self): inputs.append(self.excess_input.name) if self.excess_output is not None: outputs.append(self.excess_output.name) - return {**super().results_structure(), 'inputs': inputs, 'outputs': outputs, - 'flows': [flow.label_full for flow in self.element.inputs + self.element.outputs]} + return { + **super().results_structure(), + 'inputs': inputs, + 'outputs': outputs, + 'flows': [flow.label_full for flow in self.element.inputs + self.element.outputs], + } class ComponentModel(ElementModel): @@ -614,9 +630,11 @@ def _do_modeling(self): self.add_constraints(on == all_flows[0].submodel.on_off.on, short_name='on') else: flow_ons = [flow.submodel.on_off.on for flow in all_flows] - #TODO: Is the EPSILON even necessary? + # TODO: Is the EPSILON even necessary? self.add_constraints(on <= sum(flow_ons) + CONFIG.modeling.EPSILON, short_name='on|ub') - self.add_constraints(on >= sum(flow_ons) / (len(flow_ons) + CONFIG.modeling.EPSILON), short_name='on|lb') + self.add_constraints( + on >= sum(flow_ons) / (len(flow_ons) + CONFIG.modeling.EPSILON), short_name='on|lb' + ) self.on_off = self.add_submodels( OnOffModel( @@ -661,9 +679,7 @@ def previous_states(self) -> Optional[xr.DataArray]: max_len = max(da.sizes['time'] for da in previous_states) padded_previous_states = [ - da.assign_coords( - time=range(-da.sizes['time'], 0) - ).reindex(time=range(-max_len, 0), fill_value=0) + da.assign_coords(time=range(-da.sizes['time'], 0)).reindex(time=range(-max_len, 0), fill_value=0) for da in previous_states ] return xr.concat(padded_previous_states, dim='flow').any(dim='flow').astype(int) diff --git a/flixopt/features.py b/flixopt/features.py index 7b862375c..fc80f0eb3 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -59,7 +59,9 @@ def _create_variables_and_constraints(self): if self.parameters.optional: self.add_variables( - binary=True, coords=self._model.get_coords(['year', 'scenario']), short_name='is_invested', + binary=True, + coords=self._model.get_coords(['year', 'scenario']), + short_name='is_invested', ) BoundingPatterns.bounds_with_state( @@ -167,7 +169,7 @@ def _do_modeling(self): bounds=( self.parameters.on_hours_total_min if self.parameters.on_hours_total_min is not None else 0, self.parameters.on_hours_total_max if self.parameters.on_hours_total_max is not None else np.inf, - ),#TODO: self._model.hours_per_step.sum('time').item() + self._get_previous_on_duration()) + ), # TODO: self._model.hours_per_step.sum('time').item() + self._get_previous_on_duration()) short_name='on_hours_total', coords=self.get_coords(['year', 'scenario']), ) @@ -184,10 +186,15 @@ def _do_modeling(self): switch_off=self.switch_off, name=f'{self.label_of_model}|switch', previous_state=self._previous_states.isel(time=-1) if self._previous_states is not None else 0, - ) + ) if self.parameters.switch_on_total_max is not None: - count = self.add_variables(lower=0, upper=self.parameters.switch_on_total_max, coords=self._model.get_coords(('year', 'scenario')), short_name='switch|count') + count = self.add_variables( + lower=0, + upper=self.parameters.switch_on_total_max, + coords=self._model.get_coords(('year', 'scenario')), + short_name='switch|count', + ) self.add_constraints(count == self.switch_on.sum('time'), short_name='switch|count') # 5. Consecutive on duration using existing pattern @@ -211,7 +218,7 @@ def _do_modeling(self): maximum_duration=self.parameters.consecutive_off_hours_max, previous_duration=self._get_previous_off_duration(), ) - #TODO: + # TODO: self._add_effects() @@ -287,7 +294,7 @@ def _get_previous_off_duration(self): if self._previous_states is None: return hours_per_step else: - return ModelingUtilities.compute_consecutive_hours_in_state(self._previous_states * -1 + 1, hours_per_step) + return ModelingUtilities.compute_consecutive_hours_in_state(self._previous_states * -1 + 1, hours_per_step) class PieceModel(Submodel): @@ -309,7 +316,7 @@ def __init__( def _do_modeling(self): super()._do_modeling() - dims =('time', 'year','scenario') if self._as_time_series else ('year','scenario') + dims = ('time', 'year', 'scenario') if self._as_time_series else ('year', 'scenario') self.inside_piece = self.add_variables( binary=True, short_name='inside_piece', @@ -392,7 +399,7 @@ def _do_modeling(self): ), name=f'{self.label_full}|{var_name}|lambda', short_name=f'{var_name}|lambda', - ) + ) # a) eq: Segment1.onSeg(t) + Segment2.onSeg(t) + ... = 1 Aufenthalt nur in Segmenten erlaubt # b) eq: -On(t) + Segment1.onSeg(t) + Segment2.onSeg(t) + ... = 0 zusätzlich kann alles auch Null sein @@ -508,10 +515,10 @@ def _do_modeling(self): lower=self._total_min, upper=self._total_max, coords=self._model.get_coords([dim for dim in self._dims if dim != 'time']), - short_name='total' + short_name='total', ) # eq: sum = sum(share_i) # skalar - self._eq_total = self.add_constraints(self.total == 0, short_name='total') + self._eq_total = self.add_constraints(self.total == 0, short_name='total') if 'time' in self._dims: self.total_per_timestep = self.add_variables( @@ -521,7 +528,9 @@ def _do_modeling(self): short_name='total_per_timestep', ) - self._eq_total_per_timestep = self.add_constraints(self.total_per_timestep == 0, short_name='total_per_timestep') + self._eq_total_per_timestep = self.add_constraints( + self.total_per_timestep == 0, short_name='total_per_timestep' + ) # Add it to the total self._eq_total.lhs -= self.total_per_timestep.sum(dim='time') diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 42faadcf6..dd202114e 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -80,7 +80,9 @@ def __init__( self.timesteps = self._validate_timesteps(timesteps) self.timesteps_extra = self._create_timesteps_with_extra(timesteps, hours_of_last_timestep) - self.hours_of_previous_timesteps = self._calculate_hours_of_previous_timesteps(timesteps, hours_of_previous_timesteps) + self.hours_of_previous_timesteps = self._calculate_hours_of_previous_timesteps( + timesteps, hours_of_previous_timesteps + ) self.years = None if years is None else self._validate_years(years) @@ -157,7 +159,7 @@ def _validate_years(years: pd.Index) -> pd.Index: @staticmethod def _create_timesteps_with_extra( - timesteps: pd.DatetimeIndex, hours_of_last_timestep: Optional[float] + timesteps: pd.DatetimeIndex, hours_of_last_timestep: Optional[float] ) -> pd.DatetimeIndex: """Create timesteps with an extra step at the end.""" if hours_of_last_timestep is None: @@ -176,7 +178,7 @@ def calculate_hours_per_timestep(timesteps_extra: pd.DatetimeIndex) -> xr.DataAr @staticmethod def _calculate_hours_of_previous_timesteps( - timesteps: pd.DatetimeIndex, hours_of_previous_timesteps: Optional[Union[float, np.ndarray]] + timesteps: pd.DatetimeIndex, hours_of_previous_timesteps: Optional[Union[float, np.ndarray]] ) -> Union[float, np.ndarray]: """Calculate duration of regular timesteps.""" if hours_of_previous_timesteps is not None: @@ -335,7 +337,9 @@ def to_json(self, path: Union[str, pathlib.Path]): path: The path to the JSON file. """ if not self.connected_and_transformed: - logger.warning('FlowSystem needs to be connected and transformed before saving to JSON. Calling connect_and_transform() now.') + logger.warning( + 'FlowSystem needs to be connected and transformed before saving to JSON. Calling connect_and_transform() now.' + ) self.connect_and_transform() super().to_json(path) @@ -372,13 +376,13 @@ def fit_to_model_coords( return data.fit_to_coords(coords) except ConversionError as e: raise ConversionError( - f'Could not convert time series data "{name}" to DataArray:\n{data}\nOriginal Error: {e}') from e + f'Could not convert time series data "{name}" to DataArray:\n{data}\nOriginal Error: {e}' + ) from e try: return DataConverter.to_dataarray(data, coords=coords).rename(name) except ConversionError as e: - raise ConversionError( - f'Could not convert data "{name}" to DataArray:\n{data}\nOriginal Error: {e}') from e + raise ConversionError(f'Could not convert data "{name}" to DataArray:\n{data}\nOriginal Error: {e}') from e def fit_effects_to_model_coords( self, @@ -397,9 +401,7 @@ def fit_effects_to_model_coords( return { effect: self.fit_to_model_coords( - '|'.join(filter(None, [label_prefix, effect, label_suffix])), - value, - has_time_dim=has_time_dim + '|'.join(filter(None, [label_prefix, effect, label_suffix])), value, has_time_dim=has_time_dim ) for effect, value in effect_values_dict.items() } @@ -410,12 +412,12 @@ def connect_and_transform(self): logger.debug('FlowSystem already connected and transformed') return - self.weights = self.fit_to_model_coords( - 'weights', self.weights, has_time_dim=False - ) + self.weights = self.fit_to_model_coords('weights', self.weights, has_time_dim=False) if self.weights is not None and self.weights.sum() != 1: - logger.warning(f'Scenario weights are not normalized to 1. This is recomended for a better scaled model. ' - f'Sum of weights={self.weights.sum().item()}') + logger.warning( + f'Scenario weights are not normalized to 1. This is recomended for a better scaled model. ' + f'Sum of weights={self.weights.sum().item()}' + ) self._connect_network() for element in list(self.components.values()) + list(self.effects.effects.values()) + list(self.buses.values()): @@ -450,7 +452,9 @@ def add_elements(self, *elements: Element) -> None: def create_model(self) -> FlowSystemModel: if not self.connected_and_transformed: - raise RuntimeError('FlowSystem is not connected_and_transformed. Call FlowSystem.connect_and_transform() first.') + raise RuntimeError( + 'FlowSystem is not connected_and_transformed. Call FlowSystem.connect_and_transform() first.' + ) self.submodel = FlowSystemModel(self) return self.submodel @@ -699,7 +703,7 @@ def isel( self, time: Optional[Union[int, slice, List[int]]] = None, year: Optional[Union[int, slice, List[int]]] = None, - scenario: Optional[Union[int, slice, List[int]]] = None + scenario: Optional[Union[int, slice, List[int]]] = None, ) -> 'FlowSystem': """ Select a subset of the flowsystem by integer indices. @@ -734,7 +738,7 @@ def resample( self, time: str, method: Literal['mean', 'sum', 'max', 'min', 'first', 'last', 'std', 'var', 'median', 'count'] = 'mean', - **kwargs: Any + **kwargs: Any, ) -> 'FlowSystem': """ Create a resampled FlowSystem by resampling data along the time dimension (like xr.Dataset.resample()). diff --git a/flixopt/interface.py b/flixopt/interface.py index 76e74616b..374c3fb44 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -277,8 +277,12 @@ def __init__( switch_on_total_max: max nr of switchOn operations force_switch_on: force creation of switch on variable, even if there is no switch_on_total_max """ - self.effects_per_switch_on: 'TemporalEffectsUser' = effects_per_switch_on if effects_per_switch_on is not None else {} - self.effects_per_running_hour: 'TemporalEffectsUser' = effects_per_running_hour if effects_per_running_hour is not None else {} + self.effects_per_switch_on: 'TemporalEffectsUser' = ( + effects_per_switch_on if effects_per_switch_on is not None else {} + ) + self.effects_per_running_hour: 'TemporalEffectsUser' = ( + effects_per_running_hour if effects_per_running_hour is not None else {} + ) self.on_hours_total_min: Scalar = on_hours_total_min self.on_hours_total_max: Scalar = on_hours_total_max self.consecutive_on_hours_min: TemporalDataUser = consecutive_on_hours_min diff --git a/flixopt/modeling.py b/flixopt/modeling.py index 8c03da9f4..fa13aeea8 100644 --- a/flixopt/modeling.py +++ b/flixopt/modeling.py @@ -42,7 +42,7 @@ def to_binary( return xr.DataArray(0) if values.item() < epsilon else xr.DataArray(1) # Convert to binary states - binary_states = (np.abs(values) >= epsilon) + binary_states = np.abs(values) >= epsilon # Optionally collapse dimensions using .any() if dims is not None: @@ -94,13 +94,12 @@ def count_consecutive_states( # Start after last zero start_idx = zero_indices[-1] + 1 - consecutive_values = binary_values.isel({dim:slice(start_idx, None)}) + consecutive_values = binary_values.isel({dim: slice(start_idx, None)}) - return float(consecutive_values.sum().item()) #TODO: Som only over one dim? + return float(consecutive_values.sum().item()) # TODO: Som only over one dim? class ModelingUtilities: - @staticmethod def compute_consecutive_hours_in_state( binary_values: TemporalData, @@ -121,21 +120,25 @@ def compute_consecutive_hours_in_state( if not isinstance(hours_per_timestep, (int, float)): raise TypeError(f'hours_per_timestep must be a scalar, got {type(hours_per_timestep)}') - return ModelingUtilitiesAbstract.count_consecutive_states( - binary_values=binary_values, epsilon=epsilon - ) * hours_per_timestep + return ( + ModelingUtilitiesAbstract.count_consecutive_states(binary_values=binary_values, epsilon=epsilon) + * hours_per_timestep + ) @staticmethod - def compute_previous_states(previous_values: Optional[xr.DataArray], epsilon: Optional[float] = None) -> xr.DataArray: + def compute_previous_states( + previous_values: Optional[xr.DataArray], epsilon: Optional[float] = None + ) -> xr.DataArray: return ModelingUtilitiesAbstract.to_binary(values=previous_values, epsilon=epsilon, dims='time') @staticmethod def compute_previous_on_duration( previous_values: xr.DataArray, hours_per_step: Union[xr.DataArray, float, int] ) -> float: - return ModelingUtilitiesAbstract.count_consecutive_states( - ModelingUtilitiesAbstract.to_binary(previous_values) - ) * hours_per_step + return ( + ModelingUtilitiesAbstract.count_consecutive_states(ModelingUtilitiesAbstract.to_binary(previous_values)) + * hours_per_step + ) @staticmethod def compute_previous_off_duration( @@ -351,9 +354,7 @@ def consecutive_duration_tracking( constraints = {} # Upper bound: duration[t] ≤ state[t] * M - constraints['ub'] = model.add_constraints( - duration <= state_variable * mega, name=f'{duration.name}|ub' - ) + constraints['ub'] = model.add_constraints(duration <= state_variable * mega, name=f'{duration.name}|ub') # Forward constraint: duration[t+1] ≤ duration[t] + hours_per_step[t] constraints['forward'] = model.add_constraints( @@ -373,8 +374,7 @@ def consecutive_duration_tracking( # Initial condition: duration[0] = (hours_per_step[0] + previous_duration) * state[0] constraints['initial'] = model.add_constraints( - duration.isel(time=0) - == (hours_per_step.isel(time=0) + previous_duration) * state_variable.isel(time=0), + duration.isel(time=0) == (hours_per_step.isel(time=0) + previous_duration) * state_variable.isel(time=0), name=f'{duration.name}|initial', ) @@ -399,7 +399,9 @@ def consecutive_duration_tracking( @staticmethod def mutual_exclusivity_constraint( - model: Submodel, binary_variables: List[linopy.Variable], tolerance: float = 1, + model: Submodel, + binary_variables: List[linopy.Variable], + tolerance: float = 1, short_name: str = 'mutual_exclusivity', ) -> linopy.Constraint: """ @@ -514,9 +516,7 @@ def bounds_with_state( name = name or f'{variable.name}' if np.all(lower_bound - upper_bound) < 1e-10: - fix_constraint = model.add_constraints( - variable == variable_state * upper_bound, name=f'{name}|fix' - ) + fix_constraint = model.add_constraints(variable == variable_state * upper_bound, name=f'{name}|fix') return [fix_constraint] epsilon = np.maximum(CONFIG.modeling.EPSILON, lower_bound) @@ -616,9 +616,7 @@ def scaled_bounds_with_state( scaling_lower = model.add_constraints( variable >= (variable_state - 1) * big_m_misc + scaling_variable * rel_lower, name=f'{name}|lb2' ) - scaling_upper = model.add_constraints( - variable <= scaling_variable * rel_upper, name=f'{name}|ub2' - ) + scaling_upper = model.add_constraints(variable <= scaling_variable * rel_upper, name=f'{name}|ub2') big_m_upper = scaling_max * rel_upper big_m_lower = np.maximum(CONFIG.modeling.EPSILON, scaling_min * rel_lower) diff --git a/flixopt/plotting.py b/flixopt/plotting.py index 91fc5e7e7..9543c2c48 100644 --- a/flixopt/plotting.py +++ b/flixopt/plotting.py @@ -251,8 +251,9 @@ def with_plotly( x=data.index, y=data[column], name=column, - marker=dict(color=processed_colors[i], - line=dict(width=0, color='rgba(0,0,0,0)')), #Transparent line with 0 width + marker=dict( + color=processed_colors[i], line=dict(width=0, color='rgba(0,0,0,0)') + ), # Transparent line with 0 width ) ) @@ -263,14 +264,7 @@ def with_plotly( ) if style == 'grouped_bar': for i, column in enumerate(data.columns): - fig.add_trace( - go.Bar( - x=data.index, - y=data[column], - name=column, - marker=dict(color=processed_colors[i]) - ) - ) + fig.add_trace(go.Bar(x=data.index, y=data[column], name=column, marker=dict(color=processed_colors[i]))) fig.update_layout( barmode='group', diff --git a/flixopt/results.py b/flixopt/results.py index 361e7cfde..512af4ad7 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -29,6 +29,7 @@ class _FlowSystemRestorationError(Exception): """Exception raised when a FlowSystem cannot be restored from dataset.""" + pass @@ -175,15 +176,13 @@ def __init__( self.buses = {label: BusResults(self, **infos) for label, infos in self.solution.attrs['Buses'].items()} - self.effects = { - label: EffectResults(self, **infos) for label, infos in self.solution.attrs['Effects'].items() - } + self.effects = {label: EffectResults(self, **infos) for label, infos in self.solution.attrs['Effects'].items()} if 'Flows' not in self.solution.attrs: warnings.warn( 'No Data about flows found in the results. This data is only included since v2.2.0. Some functionality ' 'is not availlable. We recommend to evaluate your results with a version <2.2.0.', - stacklevel=2, + stacklevel=2, ) self.flows = {} else: @@ -247,24 +246,26 @@ def constraints(self) -> linopy.Constraints: def effect_share_factors(self): if self._effect_share_factors is None: effect_share_factors = self.flow_system.effects.calculate_effect_share_factors() - self._effect_share_factors = {'operation': effect_share_factors[0], - 'invest': effect_share_factors[1]} + self._effect_share_factors = {'operation': effect_share_factors[0], 'invest': effect_share_factors[1]} return self._effect_share_factors @property def flow_system(self) -> 'FlowSystem': - """ The restored flow_system that was used to create the calculation. + """The restored flow_system that was used to create the calculation. Contains all input parameters.""" if self._flow_system is None: try: from . import FlowSystem + current_logger_level = logger.getEffectiveLevel() logger.setLevel(logging.CRITICAL) self._flow_system = FlowSystem.from_dataset(self.flow_system_data) self._flow_system._connect_network() logger.setLevel(current_logger_level) except Exception as e: - logger.critical(f'Not able to restore FlowSystem from dataset. Some functionality is not availlable. {e}') + logger.critical( + f'Not able to restore FlowSystem from dataset. Some functionality is not availlable. {e}' + ) raise _FlowSystemRestorationError(f'Not able to restore FlowSystem from dataset. {e}') from e return self._flow_system @@ -351,8 +352,10 @@ def flow_rates( """ if self._flow_rates is None: self._flow_rates = self._assign_flow_coords( - xr.concat([flow.flow_rate.rename(flow.label) for flow in self.flows.values()], - dim=pd.Index(self.flows.keys(), name='flow')) + xr.concat( + [flow.flow_rate.rename(flow.label) for flow in self.flows.values()], + dim=pd.Index(self.flows.keys(), name='flow'), + ) ).rename('flow_rates') filters = {k: v for k, v in {'start': start, 'end': end, 'component': component}.items() if v is not None} return filter_dataarray_by_coord(self._flow_rates, **filters) @@ -393,7 +396,7 @@ def sizes( self, start: Optional[Union[str, List[str]]] = None, end: Optional[Union[str, List[str]]] = None, - component: Optional[Union[str, List[str]]] = None + component: Optional[Union[str, List[str]]] = None, ) -> xr.DataArray: """Returns a dataset with the sizes of the Flows. Args: @@ -410,19 +413,23 @@ def sizes( """ if self._sizes is None: self._sizes = self._assign_flow_coords( - xr.concat([flow.size.rename(flow.label) for flow in self.flows.values()], - dim=pd.Index(self.flows.keys(), name='flow')) + xr.concat( + [flow.size.rename(flow.label) for flow in self.flows.values()], + dim=pd.Index(self.flows.keys(), name='flow'), + ) ).rename('flow_sizes') filters = {k: v for k, v in {'start': start, 'end': end, 'component': component}.items() if v is not None} return filter_dataarray_by_coord(self._sizes, **filters) def _assign_flow_coords(self, da: xr.DataArray): # Add start and end coordinates - da = da.assign_coords({ - 'start': ('flow', [flow.start for flow in self.flows.values()]), - 'end': ('flow', [flow.end for flow in self.flows.values()]), - 'component': ('flow', [flow.component for flow in self.flows.values()]), - }) + da = da.assign_coords( + { + 'start': ('flow', [flow.start for flow in self.flows.values()]), + 'end': ('flow', [flow.end for flow in self.flows.values()]), + 'component': ('flow', [flow.component for flow in self.flows.values()]), + } + ) # Ensure flow is the last dimension if needed existing_dims = [d for d in da.dims if d != 'flow'] @@ -434,7 +441,7 @@ def get_effect_shares( element: str, effect: str, mode: Optional[Literal['operation', 'invest']] = None, - include_flows: bool = False + include_flows: bool = False, ) -> xr.Dataset: """Retrieves individual effect shares for a specific element and effect. Either for operation, investment, or both modes combined. @@ -457,8 +464,14 @@ def get_effect_shares( raise ValueError(f'Effect {effect} is not available.') if mode is None: - return xr.merge([self.get_effect_shares(element=element, effect=effect, mode='operation', include_flows=include_flows), - self.get_effect_shares(element=element, effect=effect, mode='invest', include_flows=include_flows)]) + return xr.merge( + [ + self.get_effect_shares( + element=element, effect=effect, mode='operation', include_flows=include_flows + ), + self.get_effect_shares(element=element, effect=effect, mode='invest', include_flows=include_flows), + ] + ) if mode not in ['operation', 'invest']: raise ValueError(f'Mode {mode} is not available. Choose between "operation" and "invest".') @@ -467,15 +480,20 @@ def get_effect_shares( label = f'{element}->{effect}({mode})' if label in self.solution: - ds = xr.Dataset({label: self.solution[label]}) + ds = xr.Dataset({label: self.solution[label]}) if include_flows: if element not in self.components: raise ValueError(f'Only use Components when retrieving Effects including flows. Got {element}') - flows = [label.split('|')[0] for label in self.components[element].inputs + self.components[element].outputs] + flows = [ + label.split('|')[0] for label in self.components[element].inputs + self.components[element].outputs + ] return xr.merge( - [ds] + [self.get_effect_shares(element=flow, effect=effect, mode=mode, include_flows=False) - for flow in flows] + [ds] + + [ + self.get_effect_shares(element=flow, effect=effect, mode=mode, include_flows=False) + for flow in flows + ] ) return ds @@ -514,8 +532,12 @@ def _compute_effect_total( raise ValueError(f'Effect {effect} is not available.') if mode == 'total': - operation = self._compute_effect_total(element=element, effect=effect, mode='operation', include_flows=include_flows) - invest = self._compute_effect_total(element=element, effect=effect, mode='invest', include_flows=include_flows) + operation = self._compute_effect_total( + element=element, effect=effect, mode='operation', include_flows=include_flows + ) + invest = self._compute_effect_total( + element=element, effect=effect, mode='invest', include_flows=include_flows + ) if invest.isnull().all() and operation.isnull().all(): return xr.DataArray(np.nan) if operation.isnull().all(): @@ -545,8 +567,9 @@ def _compute_effect_total( if include_flows: if element not in self.components: raise ValueError(f'Only use Components when retrieving Effects including flows. Got {element}') - flows = [label.split('|')[0] for label in - self.components[element].inputs + self.components[element].outputs] + flows = [ + label.split('|')[0] for label in self.components[element].inputs + self.components[element].outputs + ] for flow in flows: label = f'{flow}->{target_effect}({mode})' if label in self.solution: @@ -640,16 +663,19 @@ def plot_heatmap( Time filtering (summer months only): - >>> results.plot_heatmap('Boiler(Qth)|flow_rate', indexer={ - ... 'scenario': 'base', - ... 'time': results.solution.time[results.solution.time.dt.month.isin([6, 7, 8])] - ... }) + >>> results.plot_heatmap( + ... 'Boiler(Qth)|flow_rate', + ... indexer={ + ... 'scenario': 'base', + ... 'time': results.solution.time[results.solution.time.dt.month.isin([6, 7, 8])], + ... }, + ... ) Save to specific location: - >>> results.plot_heatmap('Boiler(Qth)|flow_rate', - ... indexer={'scenario': 'base'}, - ... save='path/to/my_heatmap.html') + >>> results.plot_heatmap( + ... 'Boiler(Qth)|flow_rate', indexer={'scenario': 'base'}, save='path/to/my_heatmap.html' + ... ) """ dataarray = self.solution[variable_name] @@ -1079,7 +1105,7 @@ def plot_charge_state( charge_state, suffix_parts = _apply_indexer_to_data(charge_state, indexer, drop=True) suffix = '--' + '-'.join(suffix_parts) if suffix_parts else '' - title=f'Operation Balance of {self.label}{suffix}' + title = f'Operation Balance of {self.label}{suffix}' if engine == 'plotly': fig = plotting.with_plotly( @@ -1097,7 +1123,7 @@ def plot_charge_state( x=charge_state.index, y=charge_state.values.flatten(), mode='lines', name=self._charge_state ) ) - elif engine=='matplotlib': + elif engine == 'matplotlib': fig, ax = plotting.with_matplotlib( ds.to_dataframe(), colors=colors, @@ -1562,10 +1588,7 @@ def filter_dataset( return filtered_ds -def filter_dataarray_by_coord( - da: xr.DataArray, - **kwargs: Optional[Union[str, List[str]]] -) -> xr.DataArray: +def filter_dataarray_by_coord(da: xr.DataArray, **kwargs: Optional[Union[str, List[str]]]) -> xr.DataArray: """Filter flows by node and component attributes. Filters are applied in the order they are specified. All filters must match for an edge to be included. @@ -1585,6 +1608,7 @@ def filter_dataarray_by_coord( AttributeError: If required coordinates are missing. ValueError: If specified nodes don't exist or no matches found. """ + # Helper function to process filters def apply_filter(array, coord_name: str, coord_values: Union[Any, List[Any]]): # Verify coord exists @@ -1598,12 +1622,12 @@ def apply_filter(array, coord_name: str, coord_values: Union[Any, List[Any]]): available = set(array[coord_name].values) missing = [v for v in val_list if v not in available] if missing: - raise ValueError(f"{coord_name.title()} value(s) not found: {missing}") + raise ValueError(f'{coord_name.title()} value(s) not found: {missing}') # Apply filter return array.where( array[coord_name].isin(val_list) if isinstance(coord_values, list) else array[coord_name] == coord_values, - drop=True + drop=True, ) # Apply filters from kwargs @@ -1612,20 +1636,18 @@ def apply_filter(array, coord_name: str, coord_values: Union[Any, List[Any]]): for coord, values in filters.items(): da = apply_filter(da, coord, values) except ValueError as e: - raise ValueError(f"No edges match criteria: {filters}") from e + raise ValueError(f'No edges match criteria: {filters}') from e # Verify results exist if da.size == 0: - raise ValueError(f"No edges match criteria: {filters}") + raise ValueError(f'No edges match criteria: {filters}') return da def _apply_indexer_to_data( - data: Union[xr.DataArray, xr.Dataset], - indexer: Optional[Dict[str, Any]] = None, - drop=False - ) -> Tuple[Union[xr.DataArray, xr.Dataset], List[str]]: + data: Union[xr.DataArray, xr.Dataset], indexer: Optional[Dict[str, Any]] = None, drop=False +) -> Tuple[Union[xr.DataArray, xr.Dataset], List[str]]: """ Apply indexer selection or auto-select first values for non-time dimensions. @@ -1643,7 +1665,7 @@ def _apply_indexer_to_data( if indexer is not None: # User provided indexer data = data.sel(indexer, drop=drop) - selection_string.extend(f"{v}[{k}]" for k, v in indexer.items()) + selection_string.extend(f'{v}[{k}]' for k, v in indexer.items()) else: # Auto-select first value for each dimension except 'time' selection = {} @@ -1651,7 +1673,7 @@ def _apply_indexer_to_data( if dim != 'time' and dim in data.coords: first_value = data.coords[dim].values[0] selection[dim] = first_value - selection_string.append(f"{first_value}[{dim}]") + selection_string.append(f'{first_value}[{dim}]') if selection: data = data.sel(selection, drop=drop) diff --git a/flixopt/structure.py b/flixopt/structure.py index 0e82fa346..da67f9620 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -60,6 +60,7 @@ def register_class_for_io(cls): class SubmodelsMixin: """Mixin that provides submodel functionality for both FlowSystemModel and Submodel.""" + submodels: 'Submodels' @property @@ -198,16 +199,12 @@ def __repr__(self) -> str: # Format sections with headers and underlines formatted_sections = [] for section_header, section_content in sections.items(): - formatted_sections.append( - (f'{section_header}\n' - f'{"-" * len(section_header)}\n' - f'{section_content}') - ) + formatted_sections.append((f'{section_header}\n{"-" * len(section_header)}\n{section_content}')) title = f'FlowSystemModel ({self.type})' - return (f'{title}\n' - f'{"=" * len(title)}\n\n' - f'{"\n".join(formatted_sections)}') + all_sections = '\n'.join(formatted_sections) + + return f'{title}\n{"=" * len(title)}\n\n{all_sections}' class Interface: @@ -523,7 +520,8 @@ def to_dataset(self) -> xr.Dataset: raise ValueError( f'Failed to convert {self.__class__.__name__} to dataset. Its recommended to only call this method on ' f'a fully connected and transformed FlowSystem, or Interfaces inside such a FlowSystem.' - f'Original Error: {e}') from e + f'Original Error: {e}' + ) from e def to_netcdf(self, path: Union[str, pathlib.Path], compression: int = 0): """ @@ -768,9 +766,7 @@ class Submodel(SubmodelsMixin): Can have other Submodels assigned, and can be a Submodel of another Submodel. """ - def __init__( - self, model: FlowSystemModel, label_of_element: str, label_of_model = None - ): + def __init__(self, model: FlowSystemModel, label_of_element: str, label_of_model=None): """ Args: model: The FlowSystemModel that is used to create the model. @@ -895,9 +891,7 @@ def constraints_direct(self) -> linopy.Constraints: def constraints(self) -> linopy.Constraints: """All constraints of the model, including those of all sub-models""" names = list(self.constraints_direct) + [ - constraint_name - for submodel in self.submodels.values() - for constraint_name in submodel.constraints + constraint_name for submodel in self.submodels.values() for constraint_name in submodel.constraints ] return self._model.constraints[names] @@ -906,9 +900,7 @@ def constraints(self) -> linopy.Constraints: def variables(self) -> linopy.Variables: """All variables of the model, including those of all sub-models""" names = list(self.variables_direct) + [ - variable_name - for submodel in self.submodels.values() - for variable_name in submodel.variables + variable_name for submodel in self.submodels.values() for variable_name in submodel.variables ] return self._model.variables[names] @@ -919,25 +911,24 @@ def __repr__(self) -> str: """ # Extract content from existing representations sections = { - f'Variables: [{len(self.variables)}/{len(self._model.variables)}]': self.variables.__repr__().split('\n', 2)[2], - f'Constraints: [{len(self.constraints)}/{len(self._model.constraints)}]': self.constraints.__repr__().split('\n', 2)[2], + f'Variables: [{len(self.variables)}/{len(self._model.variables)}]': self.variables.__repr__().split( + '\n', 2 + )[2], + f'Constraints: [{len(self.constraints)}/{len(self._model.constraints)}]': self.constraints.__repr__().split( + '\n', 2 + )[2], f'Submodels: [{len(self.submodels)}]': self.submodels.__repr__().split('\n', 2)[2], } # Format sections with headers and underlines formatted_sections = [] for section_header, section_content in sections.items(): - formatted_sections.append( - (f'{section_header}\n' - f'{"-" * len(section_header)}\n' - f'{section_content}') - ) + formatted_sections.append((f'{section_header}\n{"-" * len(section_header)}\n{section_content}')) model_string = f'Submodel "{self.label_of_model}":' + all_sections = '\n'.join(formatted_sections) - return (f'{model_string}\n' - f'{"=" * len(model_string)}\n\n' - f'{"\n".join(formatted_sections)}') + return f'{model_string}\n{"=" * len(model_string)}\n\n{all_sections}' @property def hours_per_step(self): @@ -981,7 +972,9 @@ def __repr__(self) -> str: total_vars = sum(len(submodel.variables) for submodel in self.data.values()) total_cons = sum(len(submodel.constraints) for submodel in self.data.values()) - title = f'flixopt.structure.Submodels ({total_vars} vars, {total_cons} constraints, {len(self.data)} submodels):' + title = ( + f'flixopt.structure.Submodels ({total_vars} vars, {total_cons} constraints, {len(self.data)} submodels):' + ) underline = '-' * len(title) if not self.data: diff --git a/mkdocs.yml b/mkdocs.yml index fb009b1fd..6d1a476ff 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -134,4 +134,4 @@ extra_javascript: - https://polyfill.io/v3/polyfill.min.js?features=es6 #Support for older browsers watch: - - flixopt \ No newline at end of file + - flixopt diff --git a/pics/flixopt-icon.svg b/pics/flixopt-icon.svg index 04a6a6851..08fe340f9 100644 --- a/pics/flixopt-icon.svg +++ b/pics/flixopt-icon.svg @@ -1 +1 @@ -flixOpt \ No newline at end of file +flixOpt diff --git a/scripts/extract_release_notes.py b/scripts/extract_release_notes.py index 61ee16425..3532ff2ba 100644 --- a/scripts/extract_release_notes.py +++ b/scripts/extract_release_notes.py @@ -11,10 +11,10 @@ def extract_release_notes(version: str) -> str: """Extract release notes for a specific version from CHANGELOG.md""" - changelog_path = Path("CHANGELOG.md") + changelog_path = Path('CHANGELOG.md') if not changelog_path.exists(): - print("❌ Error: CHANGELOG.md not found", file=sys.stderr) + print('❌ Error: CHANGELOG.md not found', file=sys.stderr) sys.exit(1) content = changelog_path.read_text(encoding='utf-8') @@ -32,8 +32,8 @@ def extract_release_notes(version: str) -> str: def main(): if len(sys.argv) != 2: - print("Usage: python extract_release_notes.py ") - print("Example: python extract_release_notes.py 2.1.2") + print('Usage: python extract_release_notes.py ') + print('Example: python extract_release_notes.py 2.1.2') sys.exit(1) version = sys.argv[1] @@ -41,5 +41,5 @@ def main(): print(release_notes) -if __name__ == "__main__": +if __name__ == '__main__': main() diff --git a/tests/conftest.py b/tests/conftest.py index d6a5df7fa..d12ca9eca 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -193,7 +193,9 @@ def simple_flow_system_scenarios() -> fx.FlowSystem: ) # Create flow system - flow_system = fx.FlowSystem(base_timesteps, scenarios=pd.Index(['A', 'B', 'C']), weights=np.array([0.5, 0.25, 0.25])) + flow_system = fx.FlowSystem( + base_timesteps, scenarios=pd.Index(['A', 'B', 'C']), weights=np.array([0.5, 0.25, 0.25]) + ) flow_system.add_elements(fx.Bus('Strom'), fx.Bus('Fernwärme'), fx.Bus('Gas')) flow_system.add_elements(storage, costs, co2, boiler, heat_load, gas_tariff, electricity_feed_in, chp) @@ -485,7 +487,9 @@ def flow_system_long(): } -def create_calculation_and_solve(flow_system: fx.FlowSystem, solver, name: str, allow_infeasible: bool=False) -> fx.FullCalculation: +def create_calculation_and_solve( + flow_system: fx.FlowSystem, solver, name: str, allow_infeasible: bool = False +) -> fx.FullCalculation: calculation = fx.FullCalculation(name, flow_system) calculation.do_modeling() try: @@ -503,6 +507,7 @@ def create_linopy_model(flow_system: fx.FlowSystem) -> FlowSystemModel: calculation.do_modeling() return calculation.model + @pytest.fixture(params=['h', '3h']) def timesteps_linopy(request): return pd.date_range('2020-01-01', periods=10, freq=request.param, name='time') @@ -527,6 +532,7 @@ def basic_flow_system_linopy(timesteps_linopy) -> fx.FlowSystem: return flow_system + def assert_conequal(actual: linopy.Constraint, desired: linopy.Constraint): """Assert that two constraints are equal with detailed error messages.""" name = actual.name @@ -553,12 +559,16 @@ def assert_var_equal(actual: linopy.Variable, desired: linopy.Variable): try: xr.testing.assert_equal(actual.lower, desired.lower) except AssertionError as e: - raise AssertionError(f"{name} lower bounds don't match:\nActual: {actual.lower}\nExpected: {desired.lower}") from e + raise AssertionError( + f"{name} lower bounds don't match:\nActual: {actual.lower}\nExpected: {desired.lower}" + ) from e try: xr.testing.assert_equal(actual.upper, desired.upper) except AssertionError as e: - raise AssertionError(f"{name} upper bounds don't match:\nActual: {actual.upper}\nExpected: {desired.upper}") from e + raise AssertionError( + f"{name} upper bounds don't match:\nActual: {actual.upper}\nExpected: {desired.upper}" + ) from e if actual.type != desired.type: raise AssertionError(f"{name} types don't match: {actual.type} != {desired.type}") @@ -572,13 +582,15 @@ def assert_var_equal(actual: linopy.Variable, desired: linopy.Variable): try: xr.testing.assert_equal(actual.coords, desired.coords) except AssertionError as e: - raise AssertionError(f"{name} coordinates don't match:\nActual: {actual.coords}\nExpected: {desired.coords}") from e + raise AssertionError( + f"{name} coordinates don't match:\nActual: {actual.coords}\nExpected: {desired.coords}" + ) from e if actual.coord_dims != desired.coord_dims: raise AssertionError(f"{name} coordinate dimensions don't match: {actual.coord_dims} != {desired.coord_dims}") -def assert_sets_equal(set1: Iterable, set2: Iterable, msg=""): +def assert_sets_equal(set1: Iterable, set2: Iterable, msg=''): """Assert two sets are equal with custom error message.""" set1, set2 = set(set1), set(set2) @@ -588,12 +600,12 @@ def assert_sets_equal(set1: Iterable, set2: Iterable, msg=""): if extra or missing: parts = [] if extra: - parts.append(f"Extra: {sorted(extra)}") + parts.append(f'Extra: {sorted(extra)}') if missing: - parts.append(f"Missing: {sorted(missing)}") + parts.append(f'Missing: {sorted(missing)}') - error_msg = ", ".join(parts) + error_msg = ', '.join(parts) if msg: - error_msg = f"{msg}: {error_msg}" + error_msg = f'{msg}: {error_msg}' raise AssertionError(error_msg) diff --git a/tests/test_bus.py b/tests/test_bus.py index fb1cfcda3..c9bf3956c 100644 --- a/tests/test_bus.py +++ b/tests/test_bus.py @@ -15,9 +15,11 @@ def test_bus(self, basic_flow_system_linopy): """Test that flow model constraints are correctly generated.""" flow_system = basic_flow_system_linopy bus = fx.Bus('TestBus', excess_penalty_per_flow_hour=None) - flow_system.add_elements(bus, - fx.Sink('WärmelastTest', sink=fx.Flow('Q_th_Last', 'TestBus')), - fx.Source('GastarifTest', source=fx.Flow('Q_Gas', 'TestBus'))) + flow_system.add_elements( + bus, + fx.Sink('WärmelastTest', sink=fx.Flow('Q_th_Last', 'TestBus')), + fx.Source('GastarifTest', source=fx.Flow('Q_Gas', 'TestBus')), + ) model = create_linopy_model(flow_system) assert set(bus.submodel.variables) == {'WärmelastTest(Q_th_Last)|flow_rate', 'GastarifTest(Q_Gas)|flow_rate'} @@ -25,7 +27,7 @@ def test_bus(self, basic_flow_system_linopy): assert_conequal( model.constraints['TestBus|balance'], - model.variables['GastarifTest(Q_Gas)|flow_rate'] == model.variables['WärmelastTest(Q_th_Last)|flow_rate'] + model.variables['GastarifTest(Q_Gas)|flow_rate'] == model.variables['WärmelastTest(Q_th_Last)|flow_rate'], ) def test_bus_penalty(self, basic_flow_system_linopy): @@ -33,26 +35,36 @@ def test_bus_penalty(self, basic_flow_system_linopy): flow_system = basic_flow_system_linopy timesteps = flow_system.timesteps bus = fx.Bus('TestBus') - flow_system.add_elements(bus, - fx.Sink('WärmelastTest', sink=fx.Flow('Q_th_Last', 'TestBus')), - fx.Source('GastarifTest', source=fx.Flow('Q_Gas', 'TestBus'))) + flow_system.add_elements( + bus, + fx.Sink('WärmelastTest', sink=fx.Flow('Q_th_Last', 'TestBus')), + fx.Source('GastarifTest', source=fx.Flow('Q_Gas', 'TestBus')), + ) model = create_linopy_model(flow_system) - assert set(bus.submodel.variables) == {'TestBus|excess_input', - 'TestBus|excess_output', - 'WärmelastTest(Q_th_Last)|flow_rate', - 'GastarifTest(Q_Gas)|flow_rate'} + assert set(bus.submodel.variables) == { + 'TestBus|excess_input', + 'TestBus|excess_output', + 'WärmelastTest(Q_th_Last)|flow_rate', + 'GastarifTest(Q_Gas)|flow_rate', + } assert set(bus.submodel.constraints) == {'TestBus|balance'} - assert_var_equal(model.variables['TestBus|excess_input'], model.add_variables(lower=0, coords = (timesteps,))) + assert_var_equal(model.variables['TestBus|excess_input'], model.add_variables(lower=0, coords=(timesteps,))) assert_var_equal(model.variables['TestBus|excess_output'], model.add_variables(lower=0, coords=(timesteps,))) assert_conequal( model.constraints['TestBus|balance'], - model.variables['GastarifTest(Q_Gas)|flow_rate'] - model.variables['WärmelastTest(Q_th_Last)|flow_rate'] + model.variables['TestBus|excess_input'] - model.variables['TestBus|excess_output'] == 0 + model.variables['GastarifTest(Q_Gas)|flow_rate'] + - model.variables['WärmelastTest(Q_th_Last)|flow_rate'] + + model.variables['TestBus|excess_input'] + - model.variables['TestBus|excess_output'] + == 0, ) assert_conequal( model.constraints['TestBus->Penalty'], - model.variables['TestBus->Penalty'] == (model.variables['TestBus|excess_input'] * 1e5 * model.hours_per_step).sum() + (model.variables['TestBus|excess_output'] * 1e5 * model.hours_per_step).sum(), + model.variables['TestBus->Penalty'] + == (model.variables['TestBus|excess_input'] * 1e5 * model.hours_per_step).sum() + + (model.variables['TestBus|excess_output'] * 1e5 * model.hours_per_step).sum(), ) diff --git a/tests/test_component.py b/tests/test_component.py index 9b3674cd5..90388ef26 100644 --- a/tests/test_component.py +++ b/tests/test_component.py @@ -17,17 +17,16 @@ class TestComponentModel: - def test_flow_label_check(self, basic_flow_system_linopy): """Test that flow model constraints are correctly generated.""" _ = basic_flow_system_linopy inputs = [ fx.Flow('Q_th_Last', 'Fernwärme', relative_minimum=np.ones(10) * 0.1), - fx.Flow('Q_Gas', 'Fernwärme', relative_minimum=np.ones(10) * 0.1) + fx.Flow('Q_Gas', 'Fernwärme', relative_minimum=np.ones(10) * 0.1), ] outputs = [ fx.Flow('Q_th_Last', 'Gas', relative_minimum=np.ones(10) * 0.01), - fx.Flow('Q_Gas', 'Gas', relative_minimum=np.ones(10) * 0.01) + fx.Flow('Q_Gas', 'Gas', relative_minimum=np.ones(10) * 0.01), ] with pytest.raises(ValueError, match='Flow names must be unique!'): _ = flixopt.elements.Component('TestComponent', inputs=inputs, outputs=outputs) @@ -37,11 +36,11 @@ def test_component(self, basic_flow_system_linopy): flow_system = basic_flow_system_linopy inputs = [ fx.Flow('In1', 'Fernwärme', relative_minimum=np.ones(10) * 0.1), - fx.Flow('In2', 'Fernwärme', relative_minimum=np.ones(10) * 0.1) + fx.Flow('In2', 'Fernwärme', relative_minimum=np.ones(10) * 0.1), ] outputs = [ fx.Flow('Out1', 'Gas', relative_minimum=np.ones(10) * 0.01), - fx.Flow('Out2', 'Gas', relative_minimum=np.ones(10) * 0.01) + fx.Flow('Out2', 'Gas', relative_minimum=np.ones(10) * 0.01), ] comp = flixopt.elements.Component('TestComponent', inputs=inputs, outputs=outputs) flow_system.add_elements(comp) @@ -49,24 +48,28 @@ def test_component(self, basic_flow_system_linopy): assert_sets_equal( set(comp.submodel.variables), - {'TestComponent(In1)|flow_rate', - 'TestComponent(In1)|total_flow_hours', - 'TestComponent(In2)|flow_rate', - 'TestComponent(In2)|total_flow_hours', - 'TestComponent(Out1)|flow_rate', - 'TestComponent(Out1)|total_flow_hours', - 'TestComponent(Out2)|flow_rate', - 'TestComponent(Out2)|total_flow_hours'}, - msg='Incorrect variables' + { + 'TestComponent(In1)|flow_rate', + 'TestComponent(In1)|total_flow_hours', + 'TestComponent(In2)|flow_rate', + 'TestComponent(In2)|total_flow_hours', + 'TestComponent(Out1)|flow_rate', + 'TestComponent(Out1)|total_flow_hours', + 'TestComponent(Out2)|flow_rate', + 'TestComponent(Out2)|total_flow_hours', + }, + msg='Incorrect variables', ) assert_sets_equal( set(comp.submodel.constraints), - {'TestComponent(In1)|total_flow_hours', - 'TestComponent(In2)|total_flow_hours', - 'TestComponent(Out1)|total_flow_hours', - 'TestComponent(Out2)|total_flow_hours'}, - msg='Incorrect constraints' + { + 'TestComponent(In1)|total_flow_hours', + 'TestComponent(In2)|total_flow_hours', + 'TestComponent(Out1)|total_flow_hours', + 'TestComponent(Out2)|total_flow_hours', + }, + msg='Incorrect constraints', ) def test_on_with_multiple_flows(self, basic_flow_system_linopy): @@ -79,11 +82,11 @@ def test_on_with_multiple_flows(self, basic_flow_system_linopy): ] outputs = [ fx.Flow('Out1', 'Gas', relative_minimum=np.ones(10) * 0.2, size=200), - fx.Flow('Out2', 'Gas', relative_minimum=np.ones(10) * 0.3, - relative_maximum = ub_out2, size=300), + fx.Flow('Out2', 'Gas', relative_minimum=np.ones(10) * 0.3, relative_maximum=ub_out2, size=300), ] - comp = flixopt.elements.Component('TestComponent', inputs=inputs, outputs=outputs, - on_off_parameters=fx.OnOffParameters()) + comp = flixopt.elements.Component( + 'TestComponent', inputs=inputs, outputs=outputs, on_off_parameters=fx.OnOffParameters() + ) flow_system.add_elements(comp) model = create_linopy_model(flow_system) @@ -130,13 +133,22 @@ def test_on_with_multiple_flows(self, basic_flow_system_linopy): msg='Incorrect constraints', ) - assert_var_equal(model['TestComponent(Out2)|flow_rate'], - model.add_variables(lower=0, upper=300 * ub_out2, coords=(timesteps,))) - assert_var_equal(model['TestComponent|on'], model.add_variables(binary=True, coords = (timesteps,))) + assert_var_equal( + model['TestComponent(Out2)|flow_rate'], + model.add_variables(lower=0, upper=300 * ub_out2, coords=(timesteps,)), + ) + assert_var_equal(model['TestComponent|on'], model.add_variables(binary=True, coords=(timesteps,))) assert_var_equal(model['TestComponent(Out2)|on'], model.add_variables(binary=True, coords=(timesteps,))) - assert_conequal(model.constraints['TestComponent(Out2)|flow_rate|lb'], model.variables['TestComponent(Out2)|flow_rate'] >= model.variables['TestComponent(Out2)|on'] * 0.3 * 300) - assert_conequal(model.constraints['TestComponent(Out2)|flow_rate|ub'], model.variables['TestComponent(Out2)|flow_rate'] <= model.variables['TestComponent(Out2)|on'] * 300 * ub_out2) + assert_conequal( + model.constraints['TestComponent(Out2)|flow_rate|lb'], + model.variables['TestComponent(Out2)|flow_rate'] >= model.variables['TestComponent(Out2)|on'] * 0.3 * 300, + ) + assert_conequal( + model.constraints['TestComponent(Out2)|flow_rate|ub'], + model.variables['TestComponent(Out2)|flow_rate'] + <= model.variables['TestComponent(Out2)|on'] * 300 * ub_out2, + ) assert_conequal( model.constraints['TestComponent|on|lb'], @@ -159,8 +171,6 @@ def test_on_with_multiple_flows(self, basic_flow_system_linopy): + 1e-5, ) - - def test_on_with_single_flow(self, basic_flow_system_linopy): """Test that flow model constraints are correctly generated.""" flow_system = basic_flow_system_linopy @@ -185,7 +195,7 @@ def test_on_with_single_flow(self, basic_flow_system_linopy): 'TestComponent|on', 'TestComponent|on_hours_total', }, - msg='Incorrect variables' + msg='Incorrect variables', ) assert_sets_equal( @@ -198,7 +208,7 @@ def test_on_with_single_flow(self, basic_flow_system_linopy): 'TestComponent|on', 'TestComponent|on_hours_total', }, - msg='Incorrect constraints' + msg='Incorrect constraints', ) assert_var_equal( @@ -227,15 +237,28 @@ def test_previous_states_with_multiple_flows(self, basic_flow_system_linopy): timesteps = flow_system.timesteps ub_out2 = np.linspace(1, 1.5, 10).round(2) inputs = [ - fx.Flow('In1', 'Fernwärme', relative_minimum=np.ones(10) * 0.1, size=100, previous_flow_rate=np.array([0, 0, 1e-6, 1e-5, 1e-4, 3,4])), + fx.Flow( + 'In1', + 'Fernwärme', + relative_minimum=np.ones(10) * 0.1, + size=100, + previous_flow_rate=np.array([0, 0, 1e-6, 1e-5, 1e-4, 3, 4]), + ), ] outputs = [ - fx.Flow('Out1', 'Gas', relative_minimum=np.ones(10) * 0.2, size=200, previous_flow_rate=[3,4,5]), - fx.Flow('Out2', 'Gas', relative_minimum=np.ones(10) * 0.3, - relative_maximum = ub_out2, size=300, previous_flow_rate=20), + fx.Flow('Out1', 'Gas', relative_minimum=np.ones(10) * 0.2, size=200, previous_flow_rate=[3, 4, 5]), + fx.Flow( + 'Out2', + 'Gas', + relative_minimum=np.ones(10) * 0.3, + relative_maximum=ub_out2, + size=300, + previous_flow_rate=20, + ), ] - comp = flixopt.elements.Component('TestComponent', inputs=inputs, outputs=outputs, - on_off_parameters=fx.OnOffParameters()) + comp = flixopt.elements.Component( + 'TestComponent', inputs=inputs, outputs=outputs, on_off_parameters=fx.OnOffParameters() + ) flow_system.add_elements(comp) model = create_linopy_model(flow_system) @@ -257,7 +280,7 @@ def test_previous_states_with_multiple_flows(self, basic_flow_system_linopy): 'TestComponent|on', 'TestComponent|on_hours_total', }, - msg='Incorrect variables' + msg='Incorrect variables', ) assert_sets_equal( @@ -279,20 +302,35 @@ def test_previous_states_with_multiple_flows(self, basic_flow_system_linopy): 'TestComponent|on|ub', 'TestComponent|on_hours_total', }, - msg='Incorrect constraints' + msg='Incorrect constraints', ) - assert_var_equal(model['TestComponent(Out2)|flow_rate'], - model.add_variables(lower=0, upper=300 * ub_out2, coords=(timesteps,))) - assert_var_equal(model['TestComponent|on'], model.add_variables(binary=True, coords = (timesteps,))) + assert_var_equal( + model['TestComponent(Out2)|flow_rate'], + model.add_variables(lower=0, upper=300 * ub_out2, coords=(timesteps,)), + ) + assert_var_equal(model['TestComponent|on'], model.add_variables(binary=True, coords=(timesteps,))) assert_var_equal(model['TestComponent(Out2)|on'], model.add_variables(binary=True, coords=(timesteps,))) - assert_conequal(model.constraints['TestComponent(Out2)|flow_rate|lb'], model.variables['TestComponent(Out2)|flow_rate'] >= model.variables['TestComponent(Out2)|on'] * 0.3 * 300) - assert_conequal(model.constraints['TestComponent(Out2)|flow_rate|ub'], model.variables['TestComponent(Out2)|flow_rate'] <= model.variables['TestComponent(Out2)|on'] * 300 * ub_out2) + assert_conequal( + model.constraints['TestComponent(Out2)|flow_rate|lb'], + model.variables['TestComponent(Out2)|flow_rate'] >= model.variables['TestComponent(Out2)|on'] * 0.3 * 300, + ) + assert_conequal( + model.constraints['TestComponent(Out2)|flow_rate|ub'], + model.variables['TestComponent(Out2)|flow_rate'] + <= model.variables['TestComponent(Out2)|on'] * 300 * ub_out2, + ) assert_conequal( model.constraints['TestComponent|on|lb'], - model.variables['TestComponent|on'] >= (model.variables['TestComponent(In1)|on'] + model.variables['TestComponent(Out1)|on'] + model.variables['TestComponent(Out2)|on']) / (3 + 1e-5), + model.variables['TestComponent|on'] + >= ( + model.variables['TestComponent(In1)|on'] + + model.variables['TestComponent(Out1)|on'] + + model.variables['TestComponent(Out2)|on'] + ) + / (3 + 1e-5), ) assert_conequal( model.constraints['TestComponent|on|ub'], @@ -301,7 +339,8 @@ def test_previous_states_with_multiple_flows(self, basic_flow_system_linopy): model.variables['TestComponent(In1)|on'] + model.variables['TestComponent(Out1)|on'] + model.variables['TestComponent(Out2)|on'] - ) + 1e-5, + ) + + 1e-5, ) @@ -338,7 +377,7 @@ def test_transmission_basic(self, basic_flow_system, highs_solver): transmission.in1.submodel.flow_rate.solution.values * 0.8 - 20, transmission.out1.submodel.flow_rate.solution.values, 'Losses are not computed correctly', - ) + ) def test_transmission_balanced(self, basic_flow_system, highs_solver): """Test advanced transmission functionality""" @@ -363,7 +402,7 @@ def test_transmission_balanced(self, basic_flow_system, highs_solver): bus='Wärme lokal', size=1, fixed_relative_profile=flow_system.components['Wärmelast'].sink.fixed_relative_profile - * np.array([0, 0, 0, 0, 0, 1, 1, 1, 1, 1]), + * np.array([0, 0, 0, 0, 0, 1, 1, 1, 1, 1]), ), ) @@ -400,7 +439,7 @@ def test_transmission_balanced(self, basic_flow_system, highs_solver): - np.array([20 if val > 0.1 else 0 for val in transmission.in1.submodel.flow_rate.solution.values]), transmission.out1.submodel.flow_rate.solution.values, 'Losses are not computed correctly', - ) + ) assert_almost_equal_numeric( transmission.in1.submodel._investment.size.solution.item(), @@ -431,7 +470,7 @@ def test_transmission_unbalanced(self, basic_flow_system, highs_solver): bus='Wärme lokal', size=1, fixed_relative_profile=flow_system.components['Wärmelast'].sink.fixed_relative_profile - * np.array([0, 0, 0, 0, 0, 1, 1, 1, 1, 1]), + * np.array([0, 0, 0, 0, 0, 1, 1, 1, 1, 1]), ), ) @@ -441,7 +480,9 @@ def test_transmission_unbalanced(self, basic_flow_system, highs_solver): absolute_losses=20, in1=fx.Flow('Rohr1a', bus='Wärme lokal', size=fx.InvestParameters(specific_effects=50, maximum_size=1000)), out1=fx.Flow('Rohr1b', 'Fernwärme', size=1000), - in2=fx.Flow('Rohr2a', 'Fernwärme', size=fx.InvestParameters(specific_effects=100, minimum_size=10, optional=False)), + in2=fx.Flow( + 'Rohr2a', 'Fernwärme', size=fx.InvestParameters(specific_effects=100, minimum_size=10, optional=False) + ), out2=fx.Flow('Rohr2b', bus='Wärme lokal', size=1000), balanced=False, ) @@ -468,7 +509,7 @@ def test_transmission_unbalanced(self, basic_flow_system, highs_solver): - np.array([20 if val > 0.1 else 0 for val in transmission.in1.submodel.flow_rate.solution.values]), transmission.out1.submodel.flow_rate.solution.values, 'Losses are not computed correctly', - ) + ) assert transmission.in1.submodel._investment.size.solution.item() > 11 diff --git a/tests/test_cycle_detection.py b/tests/test_cycle_detection.py index 71c775b99..753a9a3e5 100644 --- a/tests/test_cycle_detection.py +++ b/tests/test_cycle_detection.py @@ -10,164 +10,147 @@ def test_empty_graph(): def test_single_node(): """Test that a graph with a single node and no edges has no cycles.""" - assert detect_cycles({"A": []}) == [] + assert detect_cycles({'A': []}) == [] def test_self_loop(): """Test that a graph with a self-loop has a cycle.""" - cycles = detect_cycles({"A": ["A"]}) + cycles = detect_cycles({'A': ['A']}) assert len(cycles) == 1 - assert cycles[0] == ["A", "A"] + assert cycles[0] == ['A', 'A'] def test_simple_cycle(): """Test that a simple cycle is detected.""" - graph = { - "A": ["B"], - "B": ["C"], - "C": ["A"] - } + graph = {'A': ['B'], 'B': ['C'], 'C': ['A']} cycles = detect_cycles(graph) assert len(cycles) == 1 - assert cycles[0] == ["A", "B", "C", "A"] or cycles[0] == ["B", "C", "A", "B"] or cycles[0] == ["C", "A", "B", "C"] + assert cycles[0] == ['A', 'B', 'C', 'A'] or cycles[0] == ['B', 'C', 'A', 'B'] or cycles[0] == ['C', 'A', 'B', 'C'] def test_no_cycles(): """Test that a directed acyclic graph has no cycles.""" - graph = { - "A": ["B", "C"], - "B": ["D", "E"], - "C": ["F"], - "D": [], - "E": [], - "F": [] - } + graph = {'A': ['B', 'C'], 'B': ['D', 'E'], 'C': ['F'], 'D': [], 'E': [], 'F': []} assert detect_cycles(graph) == [] def test_multiple_cycles(): """Test that a graph with multiple cycles is detected.""" - graph = { - "A": ["B", "D"], - "B": ["C"], - "C": ["A"], - "D": ["E"], - "E": ["D"] - } + graph = {'A': ['B', 'D'], 'B': ['C'], 'C': ['A'], 'D': ['E'], 'E': ['D']} cycles = detect_cycles(graph) assert len(cycles) == 2 # Check that both cycles are detected (order might vary) - cycle_strings = [",".join(cycle) for cycle in cycles] - assert any("A,B,C,A" in s for s in cycle_strings) or any("B,C,A,B" in s for s in cycle_strings) or any( - "C,A,B,C" in s for s in cycle_strings) - assert any("D,E,D" in s for s in cycle_strings) or any("E,D,E" in s for s in cycle_strings) + cycle_strings = [','.join(cycle) for cycle in cycles] + assert ( + any('A,B,C,A' in s for s in cycle_strings) + or any('B,C,A,B' in s for s in cycle_strings) + or any('C,A,B,C' in s for s in cycle_strings) + ) + assert any('D,E,D' in s for s in cycle_strings) or any('E,D,E' in s for s in cycle_strings) def test_hidden_cycle(): """Test that a cycle hidden deep in the graph is detected.""" graph = { - "A": ["B", "C"], - "B": ["D"], - "C": ["E"], - "D": ["F"], - "E": ["G"], - "F": ["H"], - "G": ["I"], - "H": ["J"], - "I": ["K"], - "J": ["L"], - "K": ["M"], - "L": ["N"], - "M": ["N"], - "N": ["O"], - "O": ["P"], - "P": ["Q"], - "Q": ["O"] # Hidden cycle O->P->Q->O + 'A': ['B', 'C'], + 'B': ['D'], + 'C': ['E'], + 'D': ['F'], + 'E': ['G'], + 'F': ['H'], + 'G': ['I'], + 'H': ['J'], + 'I': ['K'], + 'J': ['L'], + 'K': ['M'], + 'L': ['N'], + 'M': ['N'], + 'N': ['O'], + 'O': ['P'], + 'P': ['Q'], + 'Q': ['O'], # Hidden cycle O->P->Q->O } cycles = detect_cycles(graph) assert len(cycles) == 1 # Check that the O-P-Q cycle is detected cycle = cycles[0] - assert "O" in cycle and "P" in cycle and "Q" in cycle + assert 'O' in cycle and 'P' in cycle and 'Q' in cycle # Check that they appear in the correct order - o_index = cycle.index("O") - p_index = cycle.index("P") - q_index = cycle.index("Q") + o_index = cycle.index('O') + p_index = cycle.index('P') + q_index = cycle.index('Q') # Check the cycle order is correct (allowing for different starting points) cycle_len = len(cycle) - assert (p_index == (o_index + 1) % cycle_len and q_index == (p_index + 1) % cycle_len) or \ - (q_index == (o_index + 1) % cycle_len and p_index == (q_index + 1) % cycle_len) or \ - (o_index == (p_index + 1) % cycle_len and q_index == (o_index + 1) % cycle_len) + assert ( + (p_index == (o_index + 1) % cycle_len and q_index == (p_index + 1) % cycle_len) + or (q_index == (o_index + 1) % cycle_len and p_index == (q_index + 1) % cycle_len) + or (o_index == (p_index + 1) % cycle_len and q_index == (o_index + 1) % cycle_len) + ) def test_disconnected_graph(): """Test with a disconnected graph.""" - graph = { - "A": ["B"], - "B": ["C"], - "C": [], - "D": ["E"], - "E": ["F"], - "F": [] - } + graph = {'A': ['B'], 'B': ['C'], 'C': [], 'D': ['E'], 'E': ['F'], 'F': []} assert detect_cycles(graph) == [] def test_disconnected_graph_with_cycle(): """Test with a disconnected graph containing a cycle in one component.""" graph = { - "A": ["B"], - "B": ["C"], - "C": [], - "D": ["E"], - "E": ["F"], - "F": ["D"] # Cycle in D->E->F->D + 'A': ['B'], + 'B': ['C'], + 'C': [], + 'D': ['E'], + 'E': ['F'], + 'F': ['D'], # Cycle in D->E->F->D } cycles = detect_cycles(graph) assert len(cycles) == 1 # Check that the D-E-F cycle is detected cycle = cycles[0] - assert "D" in cycle and "E" in cycle and "F" in cycle + assert 'D' in cycle and 'E' in cycle and 'F' in cycle # Check if they appear in the correct order - d_index = cycle.index("D") - e_index = cycle.index("E") - f_index = cycle.index("F") + d_index = cycle.index('D') + e_index = cycle.index('E') + f_index = cycle.index('F') # Check the cycle order is correct (allowing for different starting points) cycle_len = len(cycle) - assert (e_index == (d_index + 1) % cycle_len and f_index == (e_index + 1) % cycle_len) or \ - (f_index == (d_index + 1) % cycle_len and e_index == (f_index + 1) % cycle_len) or \ - (d_index == (e_index + 1) % cycle_len and f_index == (d_index + 1) % cycle_len) + assert ( + (e_index == (d_index + 1) % cycle_len and f_index == (e_index + 1) % cycle_len) + or (f_index == (d_index + 1) % cycle_len and e_index == (f_index + 1) % cycle_len) + or (d_index == (e_index + 1) % cycle_len and f_index == (d_index + 1) % cycle_len) + ) def test_complex_dag(): """Test with a complex directed acyclic graph.""" graph = { - "A": ["B", "C", "D"], - "B": ["E", "F"], - "C": ["E", "G"], - "D": ["G", "H"], - "E": ["I", "J"], - "F": ["J", "K"], - "G": ["K", "L"], - "H": ["L", "M"], - "I": ["N"], - "J": ["N", "O"], - "K": ["O", "P"], - "L": ["P", "Q"], - "M": ["Q"], - "N": ["R"], - "O": ["R", "S"], - "P": ["S"], - "Q": ["S"], - "R": [], - "S": [] + 'A': ['B', 'C', 'D'], + 'B': ['E', 'F'], + 'C': ['E', 'G'], + 'D': ['G', 'H'], + 'E': ['I', 'J'], + 'F': ['J', 'K'], + 'G': ['K', 'L'], + 'H': ['L', 'M'], + 'I': ['N'], + 'J': ['N', 'O'], + 'K': ['O', 'P'], + 'L': ['P', 'Q'], + 'M': ['Q'], + 'N': ['R'], + 'O': ['R', 'S'], + 'P': ['S'], + 'Q': ['S'], + 'R': [], + 'S': [], } assert detect_cycles(graph) == [] @@ -175,8 +158,8 @@ def test_complex_dag(): def test_missing_node_in_connections(): """Test behavior when a node referenced in edges doesn't have its own key.""" graph = { - "A": ["B", "C"], - "B": ["D"] + 'A': ['B', 'C'], + 'B': ['D'], # C and D don't have their own entries } assert detect_cycles(graph) == [] @@ -184,19 +167,10 @@ def test_missing_node_in_connections(): def test_non_string_keys(): """Test with non-string keys to ensure the algorithm is generic.""" - graph = { - 1: [2, 3], - 2: [4], - 3: [4], - 4: [] - } + graph = {1: [2, 3], 2: [4], 3: [4], 4: []} assert detect_cycles(graph) == [] - graph_with_cycle = { - 1: [2], - 2: [3], - 3: [1] - } + graph_with_cycle = {1: [2], 2: [3], 3: [1]} cycles = detect_cycles(graph_with_cycle) assert len(cycles) == 1 assert cycles[0] == [1, 2, 3, 1] or cycles[0] == [2, 3, 1, 2] or cycles[0] == [3, 1, 2, 3] @@ -222,5 +196,5 @@ def test_complex_network_with_many_nodes(): assert any_cycle_has_both -if __name__ == "__main__": - pytest.main(["-v"]) +if __name__ == '__main__': + pytest.main(['-v']) diff --git a/tests/test_dataconverter.py b/tests/test_dataconverter.py index 08c7d926c..aab04fd15 100644 --- a/tests/test_dataconverter.py +++ b/tests/test_dataconverter.py @@ -31,7 +31,7 @@ def standard_coords(): return { 'time': pd.date_range('2024-01-01', periods=5, freq='D', name='time'), # length 5 'scenario': pd.Index(['A', 'B', 'C'], name='scenario'), # length 3 - 'region': pd.Index(['north', 'south'], name='region') # length 2 + 'region': pd.Index(['north', 'south'], name='region'), # length 2 } @@ -68,10 +68,7 @@ def test_numpy_scalars(self, time_coords): def test_scalar_many_dimensions(self, standard_coords): """Scalar should broadcast to any number of dimensions.""" - coords = { - **standard_coords, - 'technology': pd.Index(['solar', 'wind'], name='technology') - } + coords = {**standard_coords, 'technology': pd.Index(['solar', 'wind'], name='technology')} result = DataConverter.to_dataarray(42, coords=coords) assert result.shape == (5, 3, 2, 2) @@ -134,11 +131,11 @@ def test_1d_array_ambiguous_length(self): # Both dimensions have length 3 coords_3x3 = { 'time': pd.date_range('2024-01-01', periods=3, freq='D', name='time'), - 'scenario': pd.Index(['A', 'B', 'C'], name='scenario') + 'scenario': pd.Index(['A', 'B', 'C'], name='scenario'), } arr = np.array([1, 2, 3]) - with pytest.raises(ConversionError, match="matches multiple dimensions"): + with pytest.raises(ConversionError, match='matches multiple dimensions'): DataConverter.to_dataarray(arr, coords=coords_3x3) def test_1d_array_broadcast_to_many_dimensions(self, standard_coords): @@ -153,10 +150,7 @@ def test_1d_array_broadcast_to_many_dimensions(self, standard_coords): # Check broadcasting - all scenarios and regions should have same time values for scenario in standard_coords['scenario']: for region in standard_coords['region']: - assert np.array_equal( - result.sel(scenario=scenario, region=region).values, - time_arr - ) + assert np.array_equal(result.sel(scenario=scenario, region=region).values, time_arr) class TestSeriesConversion: @@ -227,10 +221,7 @@ def test_series_broadcast_to_many_dimensions(self, standard_coords): # Check that all non-time dimensions have the same time series values for scenario in standard_coords['scenario']: for region in standard_coords['region']: - assert np.array_equal( - result.sel(scenario=scenario, region=region).values, - time_series.values - ) + assert np.array_equal(result.sel(scenario=scenario, region=region).values, time_series.values) class TestDataFrameConversion: @@ -247,11 +238,10 @@ def test_single_column_dataframe(self, time_coords): def test_multi_column_dataframe_accepted(self, time_coords, scenario_coords): """Multi-column DataFrame should now be accepted and converted via numpy array path.""" - df = pd.DataFrame({ - 'value1': [10, 20, 30, 40, 50], - 'value2': [15, 25, 35, 45, 55], - 'value3': [12, 22, 32, 42, 52] - }, index=time_coords) + df = pd.DataFrame( + {'value1': [10, 20, 30, 40, 50], 'value2': [15, 25, 35, 45, 55], 'value3': [12, 22, 32, 42, 52]}, + index=time_coords, + ) # Should work by converting to numpy array (5x3) and matching to time x scenario result = DataConverter.to_dataarray(df, coords={'time': time_coords, 'scenario': scenario_coords}) @@ -264,7 +254,7 @@ def test_empty_dataframe_rejected(self, time_coords): """Empty DataFrame should be rejected.""" df = pd.DataFrame(index=time_coords) # No columns - with pytest.raises(ConversionError, match="DataFrame must have at least one column"): + with pytest.raises(ConversionError, match='DataFrame must have at least one column'): DataConverter.to_dataarray(df, coords={'time': time_coords}) def test_dataframe_broadcast(self, time_coords, scenario_coords): @@ -284,10 +274,9 @@ def test_2d_array_unique_dimensions(self, standard_coords): """2D array with unique dimension lengths should work.""" # 5x3 array should map to time x scenario data_2d = np.random.rand(5, 3) - result = DataConverter.to_dataarray(data_2d, coords={ - 'time': standard_coords['time'], - 'scenario': standard_coords['scenario'] - }) + result = DataConverter.to_dataarray( + data_2d, coords={'time': standard_coords['time'], 'scenario': standard_coords['scenario']} + ) assert result.shape == (5, 3) assert result.dims == ('time', 'scenario') @@ -295,10 +284,9 @@ def test_2d_array_unique_dimensions(self, standard_coords): # 3x5 array should map to scenario x time data_2d_flipped = np.random.rand(3, 5) - result_flipped = DataConverter.to_dataarray(data_2d_flipped, coords={ - 'time': standard_coords['time'], - 'scenario': standard_coords['scenario'] - }) + result_flipped = DataConverter.to_dataarray( + data_2d_flipped, coords={'time': standard_coords['time'], 'scenario': standard_coords['scenario']} + ) assert result_flipped.shape == (5, 3) assert result_flipped.dims == ('time', 'scenario') @@ -343,14 +331,14 @@ def test_4d_array_unique_dimensions(self): 'time': pd.date_range('2024-01-01', periods=2, freq='D', name='time'), # length 2 'scenario': pd.Index(['A', 'B', 'C'], name='scenario'), # length 3 'region': pd.Index(['north', 'south', 'east', 'west'], name='region'), # length 4 - 'technology': pd.Index(['solar', 'wind', 'gas', 'coal', 'hydro'], name='technology') # length 5 + 'technology': pd.Index(['solar', 'wind', 'gas', 'coal', 'hydro'], name='technology'), # length 5 } # 3x5x2x4 array should map to scenario x technology x time x region data_4d = np.random.rand(3, 5, 2, 4) result = DataConverter.to_dataarray(data_4d, coords=coords) - assert result.shape == (2,3,4,5) + assert result.shape == (2, 3, 4, 5) assert result.dims == ('time', 'scenario', 'region', 'technology') assert np.array_equal(result.transpose('scenario', 'technology', 'time', 'region').values, data_4d) @@ -359,18 +347,18 @@ def test_2d_array_ambiguous_dimensions_error(self): # Both dimensions have length 3 coords_ambiguous = { 'scenario': pd.Index(['A', 'B', 'C'], name='scenario'), # length 3 - 'region': pd.Index(['north', 'south', 'east'], name='region') # length 3 + 'region': pd.Index(['north', 'south', 'east'], name='region'), # length 3 } data_2d = np.random.rand(3, 3) - with pytest.raises(ConversionError, match="matches multiple dimension orders"): + with pytest.raises(ConversionError, match='matches multiple dimension orders'): DataConverter.to_dataarray(data_2d, coords=coords_ambiguous) def test_multid_array_no_coords(self): """Multi-D arrays without coords should fail unless scalar.""" # Multi-element fails data_2d = np.random.rand(2, 3) - with pytest.raises(ConversionError, match="Cannot convert multi-element array without target dimensions"): + with pytest.raises(ConversionError, match='Cannot convert multi-element array without target dimensions'): DataConverter.to_dataarray(data_2d) # Single element succeeds @@ -385,25 +373,22 @@ def test_array_no_matching_dimensions_error(self, standard_coords): data_2d = np.random.rand(7, 8) coords_2d = { 'time': standard_coords['time'], # length 5 - 'scenario': standard_coords['scenario'] # length 3 + 'scenario': standard_coords['scenario'], # length 3 } - with pytest.raises(ConversionError, match="Array dimensions do not match any coordinate lengths"): + with pytest.raises(ConversionError, match='Array dimensions do not match any coordinate lengths'): DataConverter.to_dataarray(data_2d, coords=coords_2d) def test_multid_array_special_values(self, standard_coords): """Multi-D arrays should preserve special values.""" # Create 2D array with special values - data_2d = np.array([[1.0, np.nan, 3.0], - [np.inf, 5.0, -np.inf], - [7.0, 8.0, 9.0], - [10.0, np.nan, 12.0], - [13.0, 14.0, np.inf]]) + data_2d = np.array( + [[1.0, np.nan, 3.0], [np.inf, 5.0, -np.inf], [7.0, 8.0, 9.0], [10.0, np.nan, 12.0], [13.0, 14.0, np.inf]] + ) - result = DataConverter.to_dataarray(data_2d, coords={ - 'time': standard_coords['time'], - 'scenario': standard_coords['scenario'] - }) + result = DataConverter.to_dataarray( + data_2d, coords={'time': standard_coords['time'], 'scenario': standard_coords['scenario']} + ) assert result.shape == (5, 3) assert np.array_equal(np.isnan(result.values), np.isnan(data_2d)) @@ -412,31 +397,23 @@ def test_multid_array_special_values(self, standard_coords): def test_multid_array_dtype_preservation(self, standard_coords): """Multi-D arrays should preserve data types.""" # Integer array - int_data = np.array([[1, 2, 3], - [4, 5, 6], - [7, 8, 9], - [10, 11, 12], - [13, 14, 15]], dtype=np.int32) + int_data = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12], [13, 14, 15]], dtype=np.int32) - result_int = DataConverter.to_dataarray(int_data, coords={ - 'time': standard_coords['time'], - 'scenario': standard_coords['scenario'] - }) + result_int = DataConverter.to_dataarray( + int_data, coords={'time': standard_coords['time'], 'scenario': standard_coords['scenario']} + ) assert result_int.dtype == np.int32 assert np.array_equal(result_int.values, int_data) # Boolean array - bool_data = np.array([[True, False, True], - [False, True, False], - [True, True, False], - [False, False, True], - [True, False, True]]) + bool_data = np.array( + [[True, False, True], [False, True, False], [True, True, False], [False, False, True], [True, False, True]] + ) - result_bool = DataConverter.to_dataarray(bool_data, coords={ - 'time': standard_coords['time'], - 'scenario': standard_coords['scenario'] - }) + result_bool = DataConverter.to_dataarray( + bool_data, coords={'time': standard_coords['time'], 'scenario': standard_coords['scenario']} + ) assert result_bool.dtype == bool assert np.array_equal(result_bool.values, bool_data) @@ -499,7 +476,7 @@ def test_2d_dataarray_broadcast_to_more_dimensions(self, standard_coords): original = xr.DataArray( [[10, 20, 30], [40, 50, 60], [70, 80, 90], [100, 110, 120], [130, 140, 150]], coords={'time': standard_coords['time'], 'scenario': standard_coords['scenario']}, - dims=('time', 'scenario') + dims=('time', 'scenario'), ) # Broadcast to 3D @@ -510,10 +487,7 @@ def test_2d_dataarray_broadcast_to_more_dimensions(self, standard_coords): # Check that all regions have the same time+scenario values for region in standard_coords['region']: - assert np.array_equal( - result.sel(region=region).values, - original.values - ) + assert np.array_equal(result.sel(region=region).values, original.values) class TestTimeSeriesDataConversion: @@ -585,7 +559,7 @@ def test_custom_dimensions_complex(self): coords = { 'product': pd.Index(['A', 'B'], name='product'), 'factory': pd.Index(['F1', 'F2', 'F3'], name='factory'), - 'quarter': pd.Index(['Q1', 'Q2', 'Q3', 'Q4'], name='quarter') + 'quarter': pd.Index(['Q1', 'Q2', 'Q3', 'Q4'], name='quarter'), } # Array matching factory dimension @@ -626,7 +600,7 @@ def test_time_coord_validation(self): """Time coordinates must be DatetimeIndex.""" # Non-datetime index with name 'time' should fail wrong_time = pd.Index([1, 2, 3], name='time') - with pytest.raises(ConversionError, match="time coordinates must be a DatetimeIndex"): + with pytest.raises(ConversionError, match='time coordinates must be a DatetimeIndex'): DataConverter.to_dataarray(42, coords={'time': wrong_time}) def test_coord_naming(self, time_coords): @@ -642,13 +616,7 @@ class TestErrorHandling: def test_unsupported_data_types(self, time_coords): """Unsupported data types should fail with clear messages.""" - unsupported = [ - 'string', - object(), - None, - {'dict': 'value'}, - [1, 2, 3] - ] + unsupported = ['string', object(), None, {'dict': 'value'}, [1, 2, 3]] for data in unsupported: with pytest.raises(ConversionError): @@ -658,14 +626,14 @@ def test_dimension_mismatch_messages(self, time_coords, scenario_coords): """Error messages should be informative.""" # Array with wrong length wrong_arr = np.array([1, 2]) # Length 2, but no dimension has length 2 - with pytest.raises(ConversionError, match="matches none of the target dimensions"): + with pytest.raises(ConversionError, match='matches none of the target dimensions'): DataConverter.to_dataarray(wrong_arr, coords={'time': time_coords, 'scenario': scenario_coords}) def test_multidimensional_array_dimension_count_mismatch(self, standard_coords): """Array with wrong number of dimensions should fail with clear error.""" # 4D array with 3D coordinates data_4d = np.random.rand(5, 3, 2, 4) - with pytest.raises(ConversionError, match="matches multiple dimension orders|Array dimensions do not match"): + with pytest.raises(ConversionError, match='matches multiple dimension orders|Array dimensions do not match'): DataConverter.to_dataarray(data_4d, coords=standard_coords) def test_error_message_quality(self, standard_coords): @@ -674,7 +642,7 @@ def test_error_message_quality(self, standard_coords): data_2d = np.random.rand(7, 8) coords_2d = { 'time': standard_coords['time'], # length 5 - 'scenario': standard_coords['scenario'] # length 3 + 'scenario': standard_coords['scenario'], # length 3 } try: @@ -682,8 +650,8 @@ def test_error_message_quality(self, standard_coords): raise AssertionError('Should have raised ConversionError') except ConversionError as e: error_msg = str(e) - assert "Array shape: (7, 8)" in error_msg - assert "Coordinate lengths:" in error_msg + assert 'Array shape: (7, 8)' in error_msg + assert 'Coordinate lengths:' in error_msg class TestDataIntegrity: @@ -725,10 +693,9 @@ def test_dataframe_copy_independence(self, time_coords): def test_multid_array_copy_independence(self, standard_coords): """Multi-D arrays should be independent copies.""" original_data = np.random.rand(5, 3) - result = DataConverter.to_dataarray(original_data, coords={ - 'time': standard_coords['time'], - 'scenario': standard_coords['scenario'] - }) + result = DataConverter.to_dataarray( + original_data, coords={'time': standard_coords['time'], 'scenario': standard_coords['scenario']} + ) # Modify result result[0, 0] = 999 @@ -810,7 +777,7 @@ def test_complex_multid_scenario(self): coords = { 'time': pd.date_range('2024-01-01', periods=24, freq='H', name='time'), # 24 hours 'technology': pd.Index(['solar', 'wind', 'gas', 'coal'], name='technology'), # 4 technologies - 'region': pd.Index(['north', 'south', 'east'], name='region') # 3 regions + 'region': pd.Index(['north', 'south', 'east'], name='region'), # 3 regions } # Capacity factors: 24 x 4 (will broadcast to 24 x 4 x 3) @@ -832,22 +799,22 @@ def test_ambiguous_length_handling(self): coords_3x3x3 = { 'time': pd.date_range('2024-01-01', periods=3, freq='D', name='time'), 'scenario': pd.Index(['A', 'B', 'C'], name='scenario'), - 'region': pd.Index(['X', 'Y', 'Z'], name='region') + 'region': pd.Index(['X', 'Y', 'Z'], name='region'), } # 1D array - should fail arr_1d = np.array([1, 2, 3]) - with pytest.raises(ConversionError, match="matches multiple dimensions"): + with pytest.raises(ConversionError, match='matches multiple dimensions'): DataConverter.to_dataarray(arr_1d, coords=coords_3x3x3) # 2D array - should fail arr_2d = np.random.rand(3, 3) - with pytest.raises(ConversionError, match="matches multiple dimension orders"): + with pytest.raises(ConversionError, match='matches multiple dimension orders'): DataConverter.to_dataarray(arr_2d, coords=coords_3x3x3) # 3D array - should fail arr_3d = np.random.rand(3, 3, 3) - with pytest.raises(ConversionError, match="matches multiple dimension orders"): + with pytest.raises(ConversionError, match='matches multiple dimension orders'): DataConverter.to_dataarray(arr_3d, coords=coords_3x3x3) def test_mixed_broadcasting_scenarios(self): @@ -856,7 +823,7 @@ def test_mixed_broadcasting_scenarios(self): 'time': pd.date_range('2024-01-01', periods=4, freq='D', name='time'), # length 4 'scenario': pd.Index(['A', 'B'], name='scenario'), # length 2 'region': pd.Index(['north', 'south', 'east'], name='region'), # length 3 - 'product': pd.Index(['X', 'Y', 'Z', 'W', 'V'], name='product') # length 5 + 'product': pd.Index(['X', 'Y', 'Z', 'W', 'V'], name='product'), # length 5 } # Scalar to 4D @@ -873,8 +840,7 @@ def test_mixed_broadcasting_scenarios(self): for region in coords['region']: for product in coords['product']: assert np.array_equal( - arr_result.sel(scenario=scenario, region=region, product=product).values, - arr_1d + arr_result.sel(scenario=scenario, region=region, product=product).values, arr_1d ) # 2D array (4x2, matches time×scenario) to 4D @@ -884,10 +850,8 @@ def test_mixed_broadcasting_scenarios(self): # Verify broadcasting for region in coords['region']: for product in coords['product']: - assert np.array_equal( - arr_2d_result.sel(region=region, product=product).values, - arr_2d - ) + assert np.array_equal(arr_2d_result.sel(region=region, product=product).values, arr_2d) + class TestAmbiguousDimensionLengthHandling: """Test that DataConverter correctly raises errors when multiple dimensions have the same length.""" diff --git a/tests/test_effect.py b/tests/test_effect.py index dfcc2ea66..e2807cc89 100644 --- a/tests/test_effect.py +++ b/tests/test_effect.py @@ -27,98 +27,126 @@ def test_minimal(self, basic_flow_system_linopy): assert_sets_equal( set(effect.submodel.variables), - {'Effect1(invest)|total', - 'Effect1(operation)|total', - 'Effect1(operation)|total_per_timestep', - 'Effect1|total'}, - msg='Incorrect variables' + { + 'Effect1(invest)|total', + 'Effect1(operation)|total', + 'Effect1(operation)|total_per_timestep', + 'Effect1|total', + }, + msg='Incorrect variables', ) assert_sets_equal( set(effect.submodel.constraints), - {'Effect1(invest)|total', - 'Effect1(operation)|total', - 'Effect1(operation)|total_per_timestep', - 'Effect1|total'}, - msg='Incorrect constraints' + { + 'Effect1(invest)|total', + 'Effect1(operation)|total', + 'Effect1(operation)|total_per_timestep', + 'Effect1|total', + }, + msg='Incorrect constraints', ) assert_var_equal(model.variables['Effect1|total'], model.add_variables()) assert_var_equal(model.variables['Effect1(invest)|total'], model.add_variables()) assert_var_equal(model.variables['Effect1(operation)|total'], model.add_variables()) - assert_var_equal(model.variables['Effect1(operation)|total_per_timestep'], model.add_variables(coords=(timesteps,))) + assert_var_equal( + model.variables['Effect1(operation)|total_per_timestep'], model.add_variables(coords=(timesteps,)) + ) - assert_conequal(model.constraints['Effect1|total'], - model.variables['Effect1|total'] == model.variables['Effect1(operation)|total'] + model.variables['Effect1(invest)|total']) + assert_conequal( + model.constraints['Effect1|total'], + model.variables['Effect1|total'] + == model.variables['Effect1(operation)|total'] + model.variables['Effect1(invest)|total'], + ) assert_conequal(model.constraints['Effect1(invest)|total'], model.variables['Effect1(invest)|total'] == 0) - assert_conequal(model.constraints['Effect1(operation)|total'], - model.variables['Effect1(operation)|total'] == model.variables['Effect1(operation)|total_per_timestep'].sum()) - assert_conequal(model.constraints['Effect1(operation)|total_per_timestep'], - model.variables['Effect1(operation)|total_per_timestep'] ==0) + assert_conequal( + model.constraints['Effect1(operation)|total'], + model.variables['Effect1(operation)|total'] + == model.variables['Effect1(operation)|total_per_timestep'].sum(), + ) + assert_conequal( + model.constraints['Effect1(operation)|total_per_timestep'], + model.variables['Effect1(operation)|total_per_timestep'] == 0, + ) def test_bounds(self, basic_flow_system_linopy): flow_system = basic_flow_system_linopy timesteps = flow_system.timesteps - effect = fx.Effect('Effect1', '€', 'Testing Effect', - minimum_operation=1.0, - maximum_operation=1.1, - minimum_invest=2.0, - maximum_invest=2.1, - minimum_total=3.0, - maximum_total=3.1, - minimum_operation_per_hour=4.0, - maximum_operation_per_hour=4.1 - ) + effect = fx.Effect( + 'Effect1', + '€', + 'Testing Effect', + minimum_operation=1.0, + maximum_operation=1.1, + minimum_invest=2.0, + maximum_invest=2.1, + minimum_total=3.0, + maximum_total=3.1, + minimum_operation_per_hour=4.0, + maximum_operation_per_hour=4.1, + ) flow_system.add_elements(effect) model = create_linopy_model(flow_system) assert_sets_equal( set(effect.submodel.variables), - {'Effect1(invest)|total', - 'Effect1(operation)|total', - 'Effect1(operation)|total_per_timestep', - 'Effect1|total'}, - msg='Incorrect variables' + { + 'Effect1(invest)|total', + 'Effect1(operation)|total', + 'Effect1(operation)|total_per_timestep', + 'Effect1|total', + }, + msg='Incorrect variables', ) assert_sets_equal( set(effect.submodel.constraints), - {'Effect1(invest)|total', - 'Effect1(operation)|total', - 'Effect1(operation)|total_per_timestep', - 'Effect1|total'}, - msg='Incorrect constraints' + { + 'Effect1(invest)|total', + 'Effect1(operation)|total', + 'Effect1(operation)|total_per_timestep', + 'Effect1|total', + }, + msg='Incorrect constraints', ) assert_var_equal(model.variables['Effect1|total'], model.add_variables(lower=3.0, upper=3.1)) assert_var_equal(model.variables['Effect1(invest)|total'], model.add_variables(lower=2.0, upper=2.1)) assert_var_equal(model.variables['Effect1(operation)|total'], model.add_variables(lower=1.0, upper=1.1)) assert_var_equal( - model.variables['Effect1(operation)|total_per_timestep'], model.add_variables( - lower=4.0 * model.hours_per_step, upper=4.1* model.hours_per_step, coords=(timesteps,)) + model.variables['Effect1(operation)|total_per_timestep'], + model.add_variables( + lower=4.0 * model.hours_per_step, upper=4.1 * model.hours_per_step, coords=(timesteps,) + ), ) - assert_conequal(model.constraints['Effect1|total'], - model.variables['Effect1|total'] == model.variables['Effect1(operation)|total'] + model.variables['Effect1(invest)|total']) + assert_conequal( + model.constraints['Effect1|total'], + model.variables['Effect1|total'] + == model.variables['Effect1(operation)|total'] + model.variables['Effect1(invest)|total'], + ) assert_conequal(model.constraints['Effect1(invest)|total'], model.variables['Effect1(invest)|total'] == 0) - assert_conequal(model.constraints['Effect1(operation)|total'], - model.variables['Effect1(operation)|total'] == model.variables['Effect1(operation)|total_per_timestep'].sum()) - assert_conequal(model.constraints['Effect1(operation)|total_per_timestep'], - model.variables['Effect1(operation)|total_per_timestep'] ==0) + assert_conequal( + model.constraints['Effect1(operation)|total'], + model.variables['Effect1(operation)|total'] + == model.variables['Effect1(operation)|total_per_timestep'].sum(), + ) + assert_conequal( + model.constraints['Effect1(operation)|total_per_timestep'], + model.variables['Effect1(operation)|total_per_timestep'] == 0, + ) def test_shares(self, basic_flow_system_linopy): flow_system = basic_flow_system_linopy - effect1 = fx.Effect('Effect1', '€', 'Testing Effect', - specific_share_to_other_effects_operation={ - 'Effect2': 1.1, - 'Effect3': 1.2 - }, - specific_share_to_other_effects_invest={ - 'Effect2': 2.1, - 'Effect3': 2.2 - } - ) + effect1 = fx.Effect( + 'Effect1', + '€', + 'Testing Effect', + specific_share_to_other_effects_operation={'Effect2': 1.1, 'Effect3': 1.2}, + specific_share_to_other_effects_invest={'Effect2': 2.1, 'Effect3': 2.2}, + ) effect2 = fx.Effect('Effect2', '€', 'Testing Effect') effect3 = fx.Effect('Effect3', '€', 'Testing Effect') flow_system.add_elements(effect1, effect2, effect3) @@ -134,7 +162,7 @@ def test_shares(self, basic_flow_system_linopy): 'Effect1(invest)->Effect2(invest)', 'Effect1(operation)->Effect2(operation)', }, - msg='Incorrect variables for effect2' + msg='Incorrect variables for effect2', ) assert_sets_equal( @@ -147,7 +175,7 @@ def test_shares(self, basic_flow_system_linopy): 'Effect1(invest)->Effect2(invest)', 'Effect1(operation)->Effect2(operation)', }, - msg='Incorrect constraints for effect2' + msg='Incorrect constraints for effect2', ) assert_conequal( @@ -157,19 +185,19 @@ def test_shares(self, basic_flow_system_linopy): assert_conequal( model.constraints['Effect2(operation)|total_per_timestep'], - model.variables['Effect2(operation)|total_per_timestep'] == model.variables['Effect1(operation)->Effect2(operation)'], + model.variables['Effect2(operation)|total_per_timestep'] + == model.variables['Effect1(operation)->Effect2(operation)'], ) assert_conequal( model.constraints['Effect1(operation)->Effect2(operation)'], model.variables['Effect1(operation)->Effect2(operation)'] - == model.variables['Effect1(operation)|total_per_timestep'] * 1.1 + == model.variables['Effect1(operation)|total_per_timestep'] * 1.1, ) assert_conequal( model.constraints['Effect1(invest)->Effect2(invest)'], - model.variables['Effect1(invest)->Effect2(invest)'] - == model.variables['Effect1(invest)|total'] * 2.1, + model.variables['Effect1(invest)->Effect2(invest)'] == model.variables['Effect1(invest)|total'] * 2.1, ) @@ -178,21 +206,24 @@ def test_shares(self, basic_flow_system_linopy): flow_system = basic_flow_system_linopy flow_system.effects['Costs'].specific_share_to_other_effects_operation['Effect1'] = 0.5 flow_system.add_elements( - fx.Effect('Effect1', '€', 'Testing Effect', - specific_share_to_other_effects_operation={ - 'Effect2': 1.1, - 'Effect3': 1.2 - }, - specific_share_to_other_effects_invest={ - 'Effect2': 2.1, - 'Effect3': 2.2 - } - ), + fx.Effect( + 'Effect1', + '€', + 'Testing Effect', + specific_share_to_other_effects_operation={'Effect2': 1.1, 'Effect3': 1.2}, + specific_share_to_other_effects_invest={'Effect2': 2.1, 'Effect3': 2.2}, + ), fx.Effect('Effect2', '€', 'Testing Effect', specific_share_to_other_effects_operation={'Effect3': 5}), fx.Effect('Effect3', '€', 'Testing Effect'), fx.linear_converters.Boiler( - 'Boiler', eta=0.5, Q_th=fx.Flow('Q_th', bus='Fernwärme', ),Q_fu=fx.Flow('Q_fu', bus='Gas'), - ) + 'Boiler', + eta=0.5, + Q_th=fx.Flow( + 'Q_th', + bus='Fernwärme', + ), + Q_fu=fx.Flow('Q_fu', bus='Gas'), + ), ) results = create_calculation_and_solve(flow_system, fx.solvers.HighsSolver(0.01, 60), 'Sim1').results @@ -201,7 +232,7 @@ def test_shares(self, basic_flow_system_linopy): 'operation': { ('Costs', 'Effect1'): 0.5, ('Costs', 'Effect2'): 0.5 * 1.1, - ('Costs', 'Effect3'): 0.5 * 1.1 * 5 + 0.5 * 1.2, #This is where the issue lies + ('Costs', 'Effect3'): 0.5 * 1.1 * 5 + 0.5 * 1.2, # This is where the issue lies ('Effect1', 'Effect2'): 1.1, ('Effect1', 'Effect3'): 1.2 + 1.1 * 5, ('Effect2', 'Effect3'): 5, @@ -209,7 +240,7 @@ def test_shares(self, basic_flow_system_linopy): 'invest': { ('Effect1', 'Effect2'): 2.1, ('Effect1', 'Effect3'): 2.2, - } + }, } for key, value in effect_share_factors['operation'].items(): np.testing.assert_allclose(results.effect_share_factors['operation'][key].values, value) @@ -217,40 +248,59 @@ def test_shares(self, basic_flow_system_linopy): for key, value in effect_share_factors['invest'].items(): np.testing.assert_allclose(results.effect_share_factors['invest'][key].values, value) - xr.testing.assert_allclose(results.effects_per_component('operation').sum('component')['Costs'], - results.solution['Costs(operation)|total_per_timestep'].fillna(0)) + xr.testing.assert_allclose( + results.effects_per_component('operation').sum('component')['Costs'], + results.solution['Costs(operation)|total_per_timestep'].fillna(0), + ) - xr.testing.assert_allclose(results.effects_per_component('operation').sum('component')['Effect1'], - results.solution['Effect1(operation)|total_per_timestep'].fillna(0)) + xr.testing.assert_allclose( + results.effects_per_component('operation').sum('component')['Effect1'], + results.solution['Effect1(operation)|total_per_timestep'].fillna(0), + ) - xr.testing.assert_allclose(results.effects_per_component('operation').sum('component')['Effect2'], - results.solution['Effect2(operation)|total_per_timestep'].fillna(0)) + xr.testing.assert_allclose( + results.effects_per_component('operation').sum('component')['Effect2'], + results.solution['Effect2(operation)|total_per_timestep'].fillna(0), + ) - xr.testing.assert_allclose(results.effects_per_component('operation').sum('component')['Effect3'], - results.solution['Effect3(operation)|total_per_timestep'].fillna(0)) + xr.testing.assert_allclose( + results.effects_per_component('operation').sum('component')['Effect3'], + results.solution['Effect3(operation)|total_per_timestep'].fillna(0), + ) # Invest mode checks - xr.testing.assert_allclose(results.effects_per_component('invest').sum('component')['Costs'], - results.solution['Costs(invest)|total']) + xr.testing.assert_allclose( + results.effects_per_component('invest').sum('component')['Costs'], results.solution['Costs(invest)|total'] + ) - xr.testing.assert_allclose(results.effects_per_component('invest').sum('component')['Effect1'], - results.solution['Effect1(invest)|total']) + xr.testing.assert_allclose( + results.effects_per_component('invest').sum('component')['Effect1'], + results.solution['Effect1(invest)|total'], + ) - xr.testing.assert_allclose(results.effects_per_component('invest').sum('component')['Effect2'], - results.solution['Effect2(invest)|total']) + xr.testing.assert_allclose( + results.effects_per_component('invest').sum('component')['Effect2'], + results.solution['Effect2(invest)|total'], + ) - xr.testing.assert_allclose(results.effects_per_component('invest').sum('component')['Effect3'], - results.solution['Effect3(invest)|total']) + xr.testing.assert_allclose( + results.effects_per_component('invest').sum('component')['Effect3'], + results.solution['Effect3(invest)|total'], + ) # Total mode checks - xr.testing.assert_allclose(results.effects_per_component('total').sum('component')['Costs'], - results.solution['Costs|total']) + xr.testing.assert_allclose( + results.effects_per_component('total').sum('component')['Costs'], results.solution['Costs|total'] + ) - xr.testing.assert_allclose(results.effects_per_component('total').sum('component')['Effect1'], - results.solution['Effect1|total']) + xr.testing.assert_allclose( + results.effects_per_component('total').sum('component')['Effect1'], results.solution['Effect1|total'] + ) - xr.testing.assert_allclose(results.effects_per_component('total').sum('component')['Effect2'], - results.solution['Effect2|total']) + xr.testing.assert_allclose( + results.effects_per_component('total').sum('component')['Effect2'], results.solution['Effect2|total'] + ) - xr.testing.assert_allclose(results.effects_per_component('total').sum('component')['Effect3'], - results.solution['Effect3|total']) + xr.testing.assert_allclose( + results.effects_per_component('total').sum('component')['Effect3'], results.solution['Effect3|total'] + ) diff --git a/tests/test_effects_shares_summation.py b/tests/test_effects_shares_summation.py index b1ff5c3a3..d4d22d6df 100644 --- a/tests/test_effects_shares_summation.py +++ b/tests/test_effects_shares_summation.py @@ -9,10 +9,7 @@ def test_direct_conversions(): """Test direct conversions with simple scalar values.""" - conversion_dict = { - 'A': {'B': xr.DataArray(2.0)}, - 'B': {'C': xr.DataArray(3.0)} - } + conversion_dict = {'A': {'B': xr.DataArray(2.0)}, 'B': {'C': xr.DataArray(3.0)}} result = calculate_all_conversion_paths(conversion_dict) @@ -32,7 +29,7 @@ def test_multiple_paths(): conversion_dict = { 'A': {'B': xr.DataArray(2.0), 'C': xr.DataArray(3.0)}, 'B': {'D': xr.DataArray(4.0)}, - 'C': {'D': xr.DataArray(5.0)} + 'C': {'D': xr.DataArray(5.0)}, } result = calculate_all_conversion_paths(conversion_dict) @@ -49,10 +46,7 @@ def test_xarray_conversions(): a_to_b = xr.DataArray([2.0, 2.1, 2.2], dims='time', coords={'time': time_points}) b_to_c = xr.DataArray([3.0, 3.1, 3.2], dims='time', coords={'time': time_points}) - conversion_dict = { - 'A': {'B': a_to_b}, - 'B': {'C': b_to_c} - } + conversion_dict = {'A': {'B': a_to_b}, 'B': {'C': b_to_c}} result = calculate_all_conversion_paths(conversion_dict) @@ -72,7 +66,7 @@ def test_long_paths(): 'A': {'B': xr.DataArray(2.0)}, 'B': {'C': xr.DataArray(3.0)}, 'C': {'D': xr.DataArray(4.0)}, - 'D': {'E': xr.DataArray(5.0)} + 'D': {'E': xr.DataArray(5.0)}, } result = calculate_all_conversion_paths(conversion_dict) @@ -89,7 +83,7 @@ def test_diamond_paths(): 'A': {'B': xr.DataArray(2.0), 'C': xr.DataArray(3.0)}, 'B': {'D': xr.DataArray(4.0)}, 'C': {'D': xr.DataArray(5.0)}, - 'D': {'E': xr.DataArray(6.0)} + 'D': {'E': xr.DataArray(6.0)}, } result = calculate_all_conversion_paths(conversion_dict) @@ -107,7 +101,7 @@ def test_effect_shares_example(): conversion_dict = { 'Costs': {'Effect1': xr.DataArray(0.5)}, 'Effect1': {'Effect2': xr.DataArray(1.1), 'Effect3': xr.DataArray(1.2)}, - 'Effect2': {'Effect3': xr.DataArray(5.0)} + 'Effect2': {'Effect3': xr.DataArray(5.0)}, } result = calculate_all_conversion_paths(conversion_dict) @@ -142,10 +136,7 @@ def test_empty_conversion_dict(): def test_no_indirect_paths(): """Test with a dictionary that has no indirect paths.""" - conversion_dict = { - 'A': {'B': xr.DataArray(2.0)}, - 'C': {'D': xr.DataArray(3.0)} - } + conversion_dict = {'A': {'B': xr.DataArray(2.0)}, 'C': {'D': xr.DataArray(3.0)}} result = calculate_all_conversion_paths(conversion_dict) @@ -178,7 +169,7 @@ def test_complex_network(): 'N': {'R': xr.DataArray(1.7)}, 'O': {'R': xr.DataArray(2.9), 'S': xr.DataArray(1.0)}, 'P': {'S': xr.DataArray(2.4)}, - 'Q': {'S': xr.DataArray(1.5)} + 'Q': {'S': xr.DataArray(1.5)}, } result = calculate_all_conversion_paths(conversion_dict) @@ -232,5 +223,6 @@ def test_complex_network(): # Just verify we have a reasonable number assert len(result) > 50 + if __name__ == '__main__': pytest.main() diff --git a/tests/test_flow.py b/tests/test_flow.py index 2cc26e52a..93658bf2e 100644 --- a/tests/test_flow.py +++ b/tests/test_flow.py @@ -23,22 +23,18 @@ def test_flow_minimal(self, basic_flow_system_linopy): assert_conequal( model.constraints['Sink(Wärme)|total_flow_hours'], - flow.submodel.variables['Sink(Wärme)|total_flow_hours'] == (flow.submodel.variables['Sink(Wärme)|flow_rate'] * model.hours_per_step).sum() + flow.submodel.variables['Sink(Wärme)|total_flow_hours'] + == (flow.submodel.variables['Sink(Wärme)|flow_rate'] * model.hours_per_step).sum(), ) - assert_var_equal(flow.submodel.flow_rate, - model.add_variables(lower=0, upper=100, coords=(timesteps,))) + assert_var_equal(flow.submodel.flow_rate, model.add_variables(lower=0, upper=100, coords=(timesteps,))) assert_var_equal(flow.submodel.total_flow_hours, model.add_variables(lower=0)) assert_sets_equal( set(flow.submodel.variables), {'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate'}, - msg='Incorrect variables' - ) - assert_sets_equal( - set(flow.submodel.constraints), - {'Sink(Wärme)|total_flow_hours'}, - msg='Incorrect constraints' + msg='Incorrect variables', ) + assert_sets_equal(set(flow.submodel.constraints), {'Sink(Wärme)|total_flow_hours'}, msg='Incorrect constraints') def test_flow(self, basic_flow_system_linopy): flow_system = basic_flow_system_linopy @@ -65,52 +61,47 @@ def test_flow(self, basic_flow_system_linopy): == (flow.submodel.variables['Sink(Wärme)|flow_rate'] * model.hours_per_step).sum(), ) - assert_var_equal( - flow.submodel.total_flow_hours, - model.add_variables(lower=10, upper=1000) - ) + assert_var_equal(flow.submodel.total_flow_hours, model.add_variables(lower=10, upper=1000)) assert_var_equal( flow.submodel.flow_rate, - model.add_variables(lower=np.linspace(0, 0.5, timesteps.size) * 100, - upper=np.linspace(0.5, 1, timesteps.size) * 100, - coords=(timesteps,)) + model.add_variables( + lower=np.linspace(0, 0.5, timesteps.size) * 100, + upper=np.linspace(0.5, 1, timesteps.size) * 100, + coords=(timesteps,), + ), ) assert_conequal( model.constraints['Sink(Wärme)|load_factor_min'], - flow.submodel.variables['Sink(Wärme)|total_flow_hours'] - >= model.hours_per_step.sum('time') * 0.1 * 100, + flow.submodel.variables['Sink(Wärme)|total_flow_hours'] >= model.hours_per_step.sum('time') * 0.1 * 100, ) assert_conequal( model.constraints['Sink(Wärme)|load_factor_max'], - flow.submodel.variables['Sink(Wärme)|total_flow_hours'] - <= model.hours_per_step.sum('time') * 0.9 * 100, + flow.submodel.variables['Sink(Wärme)|total_flow_hours'] <= model.hours_per_step.sum('time') * 0.9 * 100, ) assert_sets_equal( set(flow.submodel.variables), {'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate'}, - msg='Incorrect variables' + msg='Incorrect variables', ) assert_sets_equal( set(flow.submodel.constraints), {'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|load_factor_max', 'Sink(Wärme)|load_factor_min'}, - msg='Incorrect constraints' + msg='Incorrect constraints', ) def test_effects_per_flow_hour(self, basic_flow_system_linopy): flow_system = basic_flow_system_linopy timesteps = flow_system.timesteps - costs_per_flow_hour = xr.DataArray(np.linspace(1,2,timesteps.size), coords=(timesteps,)) + costs_per_flow_hour = xr.DataArray(np.linspace(1, 2, timesteps.size), coords=(timesteps,)) co2_per_flow_hour = xr.DataArray(np.linspace(4, 5, timesteps.size), coords=(timesteps,)) flow = fx.Flow( - 'Wärme', - bus='Fernwärme', - effects_per_flow_hour={'Costs': costs_per_flow_hour, 'CO2': co2_per_flow_hour} + 'Wärme', bus='Fernwärme', effects_per_flow_hour={'Costs': costs_per_flow_hour, 'CO2': co2_per_flow_hour} ) flow_system.add_elements(fx.Sink('Sink', sink=flow), fx.Effect('CO2', 't', '')) model = create_linopy_model(flow_system) @@ -119,24 +110,24 @@ def test_effects_per_flow_hour(self, basic_flow_system_linopy): assert_sets_equal( set(flow.submodel.variables), {'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate'}, - msg='Incorrect variables' - ) - assert_sets_equal( - set(flow.submodel.constraints), - {'Sink(Wärme)|total_flow_hours'}, - msg='Incorrect constraints' + msg='Incorrect variables', ) + assert_sets_equal(set(flow.submodel.constraints), {'Sink(Wärme)|total_flow_hours'}, msg='Incorrect constraints') assert 'Sink(Wärme)->Costs(operation)' in set(costs.submodel.constraints) assert 'Sink(Wärme)->CO2(operation)' in set(co2.submodel.constraints) assert_conequal( model.constraints['Sink(Wärme)->Costs(operation)'], - model.variables['Sink(Wärme)->Costs(operation)'] == flow.submodel.variables['Sink(Wärme)|flow_rate'] * model.hours_per_step * costs_per_flow_hour) + model.variables['Sink(Wärme)->Costs(operation)'] + == flow.submodel.variables['Sink(Wärme)|flow_rate'] * model.hours_per_step * costs_per_flow_hour, + ) assert_conequal( model.constraints['Sink(Wärme)->CO2(operation)'], - model.variables['Sink(Wärme)->CO2(operation)'] == flow.submodel.variables['Sink(Wärme)|flow_rate'] * model.hours_per_step * co2_per_flow_hour) + model.variables['Sink(Wärme)->CO2(operation)'] + == flow.submodel.variables['Sink(Wärme)|flow_rate'] * model.hours_per_step * co2_per_flow_hour, + ) class TestFlowInvestModel: @@ -164,7 +155,7 @@ def test_flow_invest(self, basic_flow_system_linopy): 'Sink(Wärme)|flow_rate', 'Sink(Wärme)|size', }, - msg='Incorrect variables' + msg='Incorrect variables', ) assert_sets_equal( set(flow.submodel.constraints), @@ -173,7 +164,7 @@ def test_flow_invest(self, basic_flow_system_linopy): 'Sink(Wärme)|flow_rate|ub', 'Sink(Wärme)|flow_rate|lb', }, - msg='Incorrect constraints' + msg='Incorrect constraints', ) # size @@ -219,7 +210,7 @@ def test_flow_invest_optional(self, basic_flow_system_linopy): assert_sets_equal( set(flow.submodel.variables), {'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate', 'Sink(Wärme)|size', 'Sink(Wärme)|is_invested'}, - msg='Incorrect variables' + msg='Incorrect variables', ) assert_sets_equal( set(flow.submodel.constraints), @@ -230,7 +221,7 @@ def test_flow_invest_optional(self, basic_flow_system_linopy): 'Sink(Wärme)|flow_rate|lb', 'Sink(Wärme)|flow_rate|ub', }, - msg='Incorrect constraints' + msg='Incorrect constraints', ) assert_var_equal(model['Sink(Wärme)|size'], model.add_variables(lower=0, upper=100)) @@ -287,7 +278,7 @@ def test_flow_invest_optional_wo_min_size(self, basic_flow_system_linopy): assert_sets_equal( set(flow.submodel.variables), {'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate', 'Sink(Wärme)|size', 'Sink(Wärme)|is_invested'}, - msg='Incorrect variables' + msg='Incorrect variables', ) assert_sets_equal( set(flow.submodel.constraints), @@ -298,7 +289,7 @@ def test_flow_invest_optional_wo_min_size(self, basic_flow_system_linopy): 'Sink(Wärme)|flow_rate|lb', 'Sink(Wärme)|flow_rate|ub', }, - msg='Incorrect constraints' + msg='Incorrect constraints', ) assert_var_equal(model['Sink(Wärme)|size'], model.add_variables(lower=0, upper=100)) @@ -355,7 +346,7 @@ def test_flow_invest_wo_min_size_non_optional(self, basic_flow_system_linopy): assert_sets_equal( set(flow.submodel.variables), {'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate', 'Sink(Wärme)|size'}, - msg='Incorrect variables' + msg='Incorrect variables', ) assert_sets_equal( set(flow.submodel.constraints), @@ -364,7 +355,7 @@ def test_flow_invest_wo_min_size_non_optional(self, basic_flow_system_linopy): 'Sink(Wärme)|flow_rate|lb', 'Sink(Wärme)|flow_rate|ub', }, - msg='Incorrect constraints' + msg='Incorrect constraints', ) assert_var_equal(model['Sink(Wärme)|size'], model.add_variables(lower=1e-5, upper=100)) @@ -410,14 +401,16 @@ def test_flow_invest_fixed_size(self, basic_flow_system_linopy): assert_sets_equal( set(flow.submodel.variables), {'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate', 'Sink(Wärme)|size'}, - msg='Incorrect variables' + msg='Incorrect variables', ) # Check that size is fixed to 75 assert_var_equal(flow.submodel.variables['Sink(Wärme)|size'], model.add_variables(lower=75, upper=75)) # Check flow rate bounds - assert_var_equal(flow.submodel.flow_rate, model.add_variables(lower=0.2 * 75, upper=0.9 * 75, coords=(timesteps,))) + assert_var_equal( + flow.submodel.flow_rate, model.add_variables(lower=0.2 * 75, upper=0.9 * 75, coords=(timesteps,)) + ) def test_flow_invest_with_effects(self, basic_flow_system_linopy): """Test flow with investment effects.""" @@ -438,7 +431,7 @@ def test_flow_invest_with_effects(self, basic_flow_system_linopy): ), ) - flow_system.add_elements( fx.Sink('Sink', sink=flow), co2) + flow_system.add_elements(fx.Sink('Sink', sink=flow), co2) model = create_linopy_model(flow_system) # Check investment effects @@ -449,13 +442,15 @@ def test_flow_invest_with_effects(self, basic_flow_system_linopy): assert_conequal( model.constraints['Sink(Wärme)->Costs(invest)'], model.variables['Sink(Wärme)->Costs(invest)'] - == flow.submodel.variables['Sink(Wärme)|is_invested'] * 1000 + flow.submodel.variables['Sink(Wärme)|size'] * 500, + == flow.submodel.variables['Sink(Wärme)|is_invested'] * 1000 + + flow.submodel.variables['Sink(Wärme)|size'] * 500, ) assert_conequal( model.constraints['Sink(Wärme)->CO2(invest)'], model.variables['Sink(Wärme)->CO2(invest)'] - == flow.submodel.variables['Sink(Wärme)|is_invested'] * 5 + flow.submodel.variables['Sink(Wärme)|size'] * 0.1, + == flow.submodel.variables['Sink(Wärme)|is_invested'] * 5 + + flow.submodel.variables['Sink(Wärme)|size'] * 0.1, ) def test_flow_invest_divest_effects(self, basic_flow_system_linopy): @@ -481,7 +476,7 @@ def test_flow_invest_divest_effects(self, basic_flow_system_linopy): assert_conequal( model.constraints['Sink(Wärme)->Costs(invest)'], - model.variables['Sink(Wärme)->Costs(invest)'] + (model.variables['Sink(Wärme)|is_invested'] -1) * 500 == 0 + model.variables['Sink(Wärme)->Costs(invest)'] + (model.variables['Sink(Wärme)|is_invested'] - 1) * 500 == 0, ) @@ -505,7 +500,7 @@ def test_flow_on(self, basic_flow_system_linopy): assert_sets_equal( set(flow.submodel.variables), {'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate', 'Sink(Wärme)|on', 'Sink(Wärme)|on_hours_total'}, - msg='Incorrect variables' + msg='Incorrect variables', ) assert_sets_equal( @@ -516,7 +511,7 @@ def test_flow_on(self, basic_flow_system_linopy): 'Sink(Wärme)|flow_rate|lb', 'Sink(Wärme)|flow_rate|ub', }, - msg='Incorrect constraints' + msg='Incorrect constraints', ) # flow_rate assert_var_equal( @@ -543,7 +538,7 @@ def test_flow_on(self, basic_flow_system_linopy): ) assert_conequal( model.constraints['Sink(Wärme)|flow_rate|ub'], - flow.submodel.variables['Sink(Wärme)|flow_rate'] <= flow.submodel.variables['Sink(Wärme)|on'] * 0.8 * 100, + flow.submodel.variables['Sink(Wärme)|flow_rate'] <= flow.submodel.variables['Sink(Wärme)|on'] * 0.8 * 100, ) assert_conequal( @@ -578,7 +573,7 @@ def test_effects_per_running_hour(self, basic_flow_system_linopy): 'Sink(Wärme)|on', 'Sink(Wärme)|on_hours_total', }, - msg='Incorrect variables' + msg='Incorrect variables', ) assert_sets_equal( set(flow.submodel.constraints), @@ -588,7 +583,7 @@ def test_effects_per_running_hour(self, basic_flow_system_linopy): 'Sink(Wärme)|flow_rate|ub', 'Sink(Wärme)|on_hours_total', }, - msg='Incorrect constraints' + msg='Incorrect constraints', ) assert 'Sink(Wärme)->Costs(operation)' in set(costs.submodel.constraints) @@ -621,41 +616,47 @@ def test_consecutive_on_hours(self, basic_flow_system_linopy): ), ) - flow_system.add_elements( fx.Sink('Sink', sink=flow)) + flow_system.add_elements(fx.Sink('Sink', sink=flow)) model = create_linopy_model(flow_system) assert {'Sink(Wärme)|consecutive_on_hours', 'Sink(Wärme)|on'}.issubset(set(flow.submodel.variables)) assert_sets_equal( - {'Sink(Wärme)|consecutive_on_hours|ub', - 'Sink(Wärme)|consecutive_on_hours|forward', - 'Sink(Wärme)|consecutive_on_hours|backward', - 'Sink(Wärme)|consecutive_on_hours|initial', - 'Sink(Wärme)|consecutive_on_hours|lb'} & set(flow.submodel.constraints), - {'Sink(Wärme)|consecutive_on_hours|ub', - 'Sink(Wärme)|consecutive_on_hours|forward', - 'Sink(Wärme)|consecutive_on_hours|backward', - 'Sink(Wärme)|consecutive_on_hours|initial', - 'Sink(Wärme)|consecutive_on_hours|lb'}, - msg='Missing consecutive on hours constraints' + { + 'Sink(Wärme)|consecutive_on_hours|ub', + 'Sink(Wärme)|consecutive_on_hours|forward', + 'Sink(Wärme)|consecutive_on_hours|backward', + 'Sink(Wärme)|consecutive_on_hours|initial', + 'Sink(Wärme)|consecutive_on_hours|lb', + } + & set(flow.submodel.constraints), + { + 'Sink(Wärme)|consecutive_on_hours|ub', + 'Sink(Wärme)|consecutive_on_hours|forward', + 'Sink(Wärme)|consecutive_on_hours|backward', + 'Sink(Wärme)|consecutive_on_hours|initial', + 'Sink(Wärme)|consecutive_on_hours|lb', + }, + msg='Missing consecutive on hours constraints', ) assert_var_equal( model.variables['Sink(Wärme)|consecutive_on_hours'], - model.add_variables(lower=0, upper=8, coords=(timesteps,)) + model.add_variables(lower=0, upper=8, coords=(timesteps,)), ) mega = model.hours_per_step.sum('time') assert_conequal( model.constraints['Sink(Wärme)|consecutive_on_hours|ub'], - model.variables['Sink(Wärme)|consecutive_on_hours'] <= model.variables['Sink(Wärme)|on'] * mega + model.variables['Sink(Wärme)|consecutive_on_hours'] <= model.variables['Sink(Wärme)|on'] * mega, ) assert_conequal( model.constraints['Sink(Wärme)|consecutive_on_hours|forward'], model.variables['Sink(Wärme)|consecutive_on_hours'].isel(time=slice(1, None)) - <= model.variables['Sink(Wärme)|consecutive_on_hours'].isel(time=slice(None, -1)) + model.hours_per_step.isel(time=slice(None, -1)) + <= model.variables['Sink(Wärme)|consecutive_on_hours'].isel(time=slice(None, -1)) + + model.hours_per_step.isel(time=slice(None, -1)), ) # eq: duration(t) >= duration(t - 1) + dt(t) + (On(t) - 1) * BIG @@ -664,7 +665,7 @@ def test_consecutive_on_hours(self, basic_flow_system_linopy): model.variables['Sink(Wärme)|consecutive_on_hours'].isel(time=slice(1, None)) >= model.variables['Sink(Wärme)|consecutive_on_hours'].isel(time=slice(None, -1)) + model.hours_per_step.isel(time=slice(None, -1)) - + (model.variables['Sink(Wärme)|on'].isel(time=slice(1, None)) - 1) * mega + + (model.variables['Sink(Wärme)|on'].isel(time=slice(1, None)) - 1) * mega, ) assert_conequal( @@ -676,7 +677,11 @@ def test_consecutive_on_hours(self, basic_flow_system_linopy): assert_conequal( model.constraints['Sink(Wärme)|consecutive_on_hours|lb'], model.variables['Sink(Wärme)|consecutive_on_hours'] - >= (model.variables['Sink(Wärme)|on'].isel(time=slice(None, -1)) - model.variables['Sink(Wärme)|on'].isel(time=slice(1, None))) * 2 + >= ( + model.variables['Sink(Wärme)|on'].isel(time=slice(None, -1)) + - model.variables['Sink(Wärme)|on'].isel(time=slice(1, None)) + ) + * 2, ) def test_consecutive_on_hours_previous(self, basic_flow_system_linopy): @@ -692,42 +697,48 @@ def test_consecutive_on_hours_previous(self, basic_flow_system_linopy): consecutive_on_hours_min=2, # Must run for at least 2 hours when turned on consecutive_on_hours_max=8, # Can't run more than 8 consecutive hours ), - previous_flow_rate=np.array([10, 20, 30, 0, 20, 20, 30]) # Previously on for 3 steps + previous_flow_rate=np.array([10, 20, 30, 0, 20, 20, 30]), # Previously on for 3 steps ) - flow_system.add_elements( fx.Sink('Sink', sink=flow)) + flow_system.add_elements(fx.Sink('Sink', sink=flow)) model = create_linopy_model(flow_system) assert {'Sink(Wärme)|consecutive_on_hours', 'Sink(Wärme)|on'}.issubset(set(flow.submodel.variables)) assert_sets_equal( - {'Sink(Wärme)|consecutive_on_hours|lb', - 'Sink(Wärme)|consecutive_on_hours|forward', - 'Sink(Wärme)|consecutive_on_hours|backward', - 'Sink(Wärme)|consecutive_on_hours|initial'} & set(flow.submodel.constraints), - {'Sink(Wärme)|consecutive_on_hours|lb', - 'Sink(Wärme)|consecutive_on_hours|forward', - 'Sink(Wärme)|consecutive_on_hours|backward', - 'Sink(Wärme)|consecutive_on_hours|initial'}, - msg='Missing consecutive on hours constraints for previous states' + { + 'Sink(Wärme)|consecutive_on_hours|lb', + 'Sink(Wärme)|consecutive_on_hours|forward', + 'Sink(Wärme)|consecutive_on_hours|backward', + 'Sink(Wärme)|consecutive_on_hours|initial', + } + & set(flow.submodel.constraints), + { + 'Sink(Wärme)|consecutive_on_hours|lb', + 'Sink(Wärme)|consecutive_on_hours|forward', + 'Sink(Wärme)|consecutive_on_hours|backward', + 'Sink(Wärme)|consecutive_on_hours|initial', + }, + msg='Missing consecutive on hours constraints for previous states', ) assert_var_equal( model.variables['Sink(Wärme)|consecutive_on_hours'], - model.add_variables(lower=0, upper=8, coords=(timesteps,)) + model.add_variables(lower=0, upper=8, coords=(timesteps,)), ) mega = model.hours_per_step.sum('time') + model.hours_per_step.isel(time=0) * 3 assert_conequal( model.constraints['Sink(Wärme)|consecutive_on_hours|ub'], - model.variables['Sink(Wärme)|consecutive_on_hours'] <= model.variables['Sink(Wärme)|on'] * mega + model.variables['Sink(Wärme)|consecutive_on_hours'] <= model.variables['Sink(Wärme)|on'] * mega, ) assert_conequal( model.constraints['Sink(Wärme)|consecutive_on_hours|forward'], model.variables['Sink(Wärme)|consecutive_on_hours'].isel(time=slice(1, None)) - <= model.variables['Sink(Wärme)|consecutive_on_hours'].isel(time=slice(None, -1)) + model.hours_per_step.isel(time=slice(None, -1)) + <= model.variables['Sink(Wärme)|consecutive_on_hours'].isel(time=slice(None, -1)) + + model.hours_per_step.isel(time=slice(None, -1)), ) # eq: duration(t) >= duration(t - 1) + dt(t) + (On(t) - 1) * BIG @@ -736,7 +747,7 @@ def test_consecutive_on_hours_previous(self, basic_flow_system_linopy): model.variables['Sink(Wärme)|consecutive_on_hours'].isel(time=slice(1, None)) >= model.variables['Sink(Wärme)|consecutive_on_hours'].isel(time=slice(None, -1)) + model.hours_per_step.isel(time=slice(None, -1)) - + (model.variables['Sink(Wärme)|on'].isel(time=slice(1, None)) - 1) * mega + + (model.variables['Sink(Wärme)|on'].isel(time=slice(1, None)) - 1) * mega, ) assert_conequal( @@ -748,7 +759,11 @@ def test_consecutive_on_hours_previous(self, basic_flow_system_linopy): assert_conequal( model.constraints['Sink(Wärme)|consecutive_on_hours|lb'], model.variables['Sink(Wärme)|consecutive_on_hours'] - >= (model.variables['Sink(Wärme)|on'].isel(time=slice(None, -1)) - model.variables['Sink(Wärme)|on'].isel(time=slice(1, None))) * 2 + >= ( + model.variables['Sink(Wärme)|on'].isel(time=slice(None, -1)) + - model.variables['Sink(Wärme)|on'].isel(time=slice(1, None)) + ) + * 2, ) def test_consecutive_off_hours(self, basic_flow_system_linopy): @@ -766,7 +781,7 @@ def test_consecutive_off_hours(self, basic_flow_system_linopy): ), ) - flow_system.add_elements( fx.Sink('Sink', sink=flow)) + flow_system.add_elements(fx.Sink('Sink', sink=flow)) model = create_linopy_model(flow_system) assert {'Sink(Wärme)|consecutive_off_hours', 'Sink(Wärme)|off'}.issubset(set(flow.submodel.variables)) @@ -777,34 +792,36 @@ def test_consecutive_off_hours(self, basic_flow_system_linopy): 'Sink(Wärme)|consecutive_off_hours|forward', 'Sink(Wärme)|consecutive_off_hours|backward', 'Sink(Wärme)|consecutive_off_hours|initial', - 'Sink(Wärme)|consecutive_off_hours|lb' - } & set(flow.submodel.constraints), + 'Sink(Wärme)|consecutive_off_hours|lb', + } + & set(flow.submodel.constraints), { 'Sink(Wärme)|consecutive_off_hours|ub', 'Sink(Wärme)|consecutive_off_hours|forward', 'Sink(Wärme)|consecutive_off_hours|backward', 'Sink(Wärme)|consecutive_off_hours|initial', - 'Sink(Wärme)|consecutive_off_hours|lb' + 'Sink(Wärme)|consecutive_off_hours|lb', }, - msg='Missing consecutive off hours constraints' + msg='Missing consecutive off hours constraints', ) assert_var_equal( model.variables['Sink(Wärme)|consecutive_off_hours'], - model.add_variables(lower=0, upper=12, coords=(timesteps,)) + model.add_variables(lower=0, upper=12, coords=(timesteps,)), ) mega = model.hours_per_step.sum('time') + model.hours_per_step.isel(time=0) * 1 # previously off for 1h assert_conequal( model.constraints['Sink(Wärme)|consecutive_off_hours|ub'], - model.variables['Sink(Wärme)|consecutive_off_hours'] <= model.variables['Sink(Wärme)|off'] * mega + model.variables['Sink(Wärme)|consecutive_off_hours'] <= model.variables['Sink(Wärme)|off'] * mega, ) assert_conequal( model.constraints['Sink(Wärme)|consecutive_off_hours|forward'], model.variables['Sink(Wärme)|consecutive_off_hours'].isel(time=slice(1, None)) - <= model.variables['Sink(Wärme)|consecutive_off_hours'].isel(time=slice(None, -1)) + model.hours_per_step.isel(time=slice(None, -1)) + <= model.variables['Sink(Wärme)|consecutive_off_hours'].isel(time=slice(None, -1)) + + model.hours_per_step.isel(time=slice(None, -1)), ) # eq: duration(t) >= duration(t - 1) + dt(t) + (On(t) - 1) * BIG @@ -813,7 +830,7 @@ def test_consecutive_off_hours(self, basic_flow_system_linopy): model.variables['Sink(Wärme)|consecutive_off_hours'].isel(time=slice(1, None)) >= model.variables['Sink(Wärme)|consecutive_off_hours'].isel(time=slice(None, -1)) + model.hours_per_step.isel(time=slice(None, -1)) - + (model.variables['Sink(Wärme)|off'].isel(time=slice(1, None)) - 1) * mega + + (model.variables['Sink(Wärme)|off'].isel(time=slice(1, None)) - 1) * mega, ) assert_conequal( @@ -825,7 +842,11 @@ def test_consecutive_off_hours(self, basic_flow_system_linopy): assert_conequal( model.constraints['Sink(Wärme)|consecutive_off_hours|lb'], model.variables['Sink(Wärme)|consecutive_off_hours'] - >= (model.variables['Sink(Wärme)|off'].isel(time=slice(None, -1)) - model.variables['Sink(Wärme)|off'].isel(time=slice(1, None))) * 4 + >= ( + model.variables['Sink(Wärme)|off'].isel(time=slice(None, -1)) + - model.variables['Sink(Wärme)|off'].isel(time=slice(1, None)) + ) + * 4, ) def test_consecutive_off_hours_previous(self, basic_flow_system_linopy): @@ -841,10 +862,10 @@ def test_consecutive_off_hours_previous(self, basic_flow_system_linopy): consecutive_off_hours_min=4, # Must stay off for at least 4 hours when shut down consecutive_off_hours_max=12, # Can't be off for more than 12 consecutive hours ), - previous_flow_rate=np.array([10, 20, 30, 0, 20, 0, 0]) # Previously off for 2 steps + previous_flow_rate=np.array([10, 20, 30, 0, 20, 0, 0]), # Previously off for 2 steps ) - flow_system.add_elements( fx.Sink('Sink', sink=flow)) + flow_system.add_elements(fx.Sink('Sink', sink=flow)) model = create_linopy_model(flow_system) assert {'Sink(Wärme)|consecutive_off_hours', 'Sink(Wärme)|off'}.issubset(set(flow.submodel.variables)) @@ -855,34 +876,36 @@ def test_consecutive_off_hours_previous(self, basic_flow_system_linopy): 'Sink(Wärme)|consecutive_off_hours|forward', 'Sink(Wärme)|consecutive_off_hours|backward', 'Sink(Wärme)|consecutive_off_hours|initial', - 'Sink(Wärme)|consecutive_off_hours|lb' - } & set(flow.submodel.constraints), + 'Sink(Wärme)|consecutive_off_hours|lb', + } + & set(flow.submodel.constraints), { 'Sink(Wärme)|consecutive_off_hours|ub', 'Sink(Wärme)|consecutive_off_hours|forward', 'Sink(Wärme)|consecutive_off_hours|backward', 'Sink(Wärme)|consecutive_off_hours|initial', - 'Sink(Wärme)|consecutive_off_hours|lb' + 'Sink(Wärme)|consecutive_off_hours|lb', }, - msg='Missing consecutive off hours constraints for previous states' + msg='Missing consecutive off hours constraints for previous states', ) assert_var_equal( model.variables['Sink(Wärme)|consecutive_off_hours'], - model.add_variables(lower=0, upper=12, coords=(timesteps,)) + model.add_variables(lower=0, upper=12, coords=(timesteps,)), ) mega = model.hours_per_step.sum('time') + model.hours_per_step.isel(time=0) * 2 assert_conequal( model.constraints['Sink(Wärme)|consecutive_off_hours|ub'], - model.variables['Sink(Wärme)|consecutive_off_hours'] <= model.variables['Sink(Wärme)|off'] * mega + model.variables['Sink(Wärme)|consecutive_off_hours'] <= model.variables['Sink(Wärme)|off'] * mega, ) assert_conequal( model.constraints['Sink(Wärme)|consecutive_off_hours|forward'], model.variables['Sink(Wärme)|consecutive_off_hours'].isel(time=slice(1, None)) - <= model.variables['Sink(Wärme)|consecutive_off_hours'].isel(time=slice(None, -1)) + model.hours_per_step.isel(time=slice(None, -1)) + <= model.variables['Sink(Wärme)|consecutive_off_hours'].isel(time=slice(None, -1)) + + model.hours_per_step.isel(time=slice(None, -1)), ) # eq: duration(t) >= duration(t - 1) + dt(t) + (On(t) - 1) * BIG @@ -891,19 +914,23 @@ def test_consecutive_off_hours_previous(self, basic_flow_system_linopy): model.variables['Sink(Wärme)|consecutive_off_hours'].isel(time=slice(1, None)) >= model.variables['Sink(Wärme)|consecutive_off_hours'].isel(time=slice(None, -1)) + model.hours_per_step.isel(time=slice(None, -1)) - + (model.variables['Sink(Wärme)|off'].isel(time=slice(1, None)) - 1) * mega + + (model.variables['Sink(Wärme)|off'].isel(time=slice(1, None)) - 1) * mega, ) assert_conequal( model.constraints['Sink(Wärme)|consecutive_off_hours|initial'], model.variables['Sink(Wärme)|consecutive_off_hours'].isel(time=0) - == model.variables['Sink(Wärme)|off'].isel(time=0) * (model.hours_per_step.isel(time=0) * (1+2)), + == model.variables['Sink(Wärme)|off'].isel(time=0) * (model.hours_per_step.isel(time=0) * (1 + 2)), ) assert_conequal( model.constraints['Sink(Wärme)|consecutive_off_hours|lb'], model.variables['Sink(Wärme)|consecutive_off_hours'] - >= (model.variables['Sink(Wärme)|off'].isel(time=slice(None, -1)) - model.variables['Sink(Wärme)|off'].isel(time=slice(1, None))) * 4 + >= ( + model.variables['Sink(Wärme)|off'].isel(time=slice(None, -1)) + - model.variables['Sink(Wärme)|off'].isel(time=slice(1, None)) + ) + * 4, ) def test_switch_on_constraints(self, basic_flow_system_linopy): @@ -935,14 +962,15 @@ def test_switch_on_constraints(self, basic_flow_system_linopy): 'Sink(Wärme)|switch|initial', 'Sink(Wärme)|switch|mutex', 'Sink(Wärme)|switch|count', - } & set(flow.submodel.constraints), + } + & set(flow.submodel.constraints), { 'Sink(Wärme)|switch|transition', 'Sink(Wärme)|switch|initial', 'Sink(Wärme)|switch|mutex', 'Sink(Wärme)|switch|count', }, - msg='Missing switch constraints' + msg='Missing switch constraints', ) # Check switch_on_nr variable bounds @@ -988,7 +1016,9 @@ def test_on_hours_limits(self, basic_flow_system_linopy): assert 'Sink(Wärme)|on_hours_total' in model.constraints # Check on_hours_total variable bounds - assert_var_equal(flow.submodel.variables['Sink(Wärme)|on_hours_total'], model.add_variables(lower=20, upper=100)) + assert_var_equal( + flow.submodel.variables['Sink(Wärme)|on_hours_total'], model.add_variables(lower=20, upper=100) + ) # Check on_hours_total constraint assert_conequal( @@ -1025,7 +1055,7 @@ def test_flow_on_invest_optional(self, basic_flow_system_linopy): 'Sink(Wärme)|on', 'Sink(Wärme)|on_hours_total', }, - msg='Incorrect variables' + msg='Incorrect variables', ) assert_sets_equal( @@ -1040,7 +1070,7 @@ def test_flow_on_invest_optional(self, basic_flow_system_linopy): 'Sink(Wärme)|flow_rate|lb2', 'Sink(Wärme)|flow_rate|ub2', }, - msg='Incorrect constraints' + msg='Incorrect constraints', ) # flow_rate @@ -1064,11 +1094,11 @@ def test_flow_on_invest_optional(self, basic_flow_system_linopy): ) assert_conequal( model.constraints['Sink(Wärme)|size|lb'], - flow.submodel.variables['Sink(Wärme)|size'] >= flow.submodel.variables['Sink(Wärme)|is_invested'] * 20, + flow.submodel.variables['Sink(Wärme)|size'] >= flow.submodel.variables['Sink(Wärme)|is_invested'] * 20, ) assert_conequal( model.constraints['Sink(Wärme)|size|ub'], - flow.submodel.variables['Sink(Wärme)|size']<= flow.submodel.variables['Sink(Wärme)|is_invested'] * 200, + flow.submodel.variables['Sink(Wärme)|size'] <= flow.submodel.variables['Sink(Wärme)|is_invested'] * 200, ) assert_conequal( model.constraints['Sink(Wärme)|flow_rate|lb1'], @@ -1091,7 +1121,9 @@ def test_flow_on_invest_optional(self, basic_flow_system_linopy): assert_conequal( model.constraints['Sink(Wärme)|flow_rate|lb2'], flow.submodel.variables['Sink(Wärme)|flow_rate'] - >= flow.submodel.variables['Sink(Wärme)|on'] * mega + flow.submodel.variables['Sink(Wärme)|size'] * 0.2 - mega, + >= flow.submodel.variables['Sink(Wärme)|on'] * mega + + flow.submodel.variables['Sink(Wärme)|size'] * 0.2 + - mega, ) assert_conequal( model.constraints['Sink(Wärme)|flow_rate|ub2'], @@ -1121,7 +1153,7 @@ def test_flow_on_invest_non_optional(self, basic_flow_system_linopy): 'Sink(Wärme)|on', 'Sink(Wärme)|on_hours_total', }, - msg='Incorrect variables' + msg='Incorrect variables', ) assert_sets_equal( @@ -1134,7 +1166,7 @@ def test_flow_on_invest_non_optional(self, basic_flow_system_linopy): 'Sink(Wärme)|flow_rate|lb2', 'Sink(Wärme)|flow_rate|ub2', }, - msg='Incorrect constraints' + msg='Incorrect constraints', ) # flow_rate @@ -1177,7 +1209,9 @@ def test_flow_on_invest_non_optional(self, basic_flow_system_linopy): assert_conequal( model.constraints['Sink(Wärme)|flow_rate|lb2'], flow.submodel.variables['Sink(Wärme)|flow_rate'] - >= flow.submodel.variables['Sink(Wärme)|on'] * mega + flow.submodel.variables['Sink(Wärme)|size'] * 0.2 - mega, + >= flow.submodel.variables['Sink(Wärme)|on'] * mega + + flow.submodel.variables['Sink(Wärme)|size'] * 0.2 + - mega, ) assert_conequal( model.constraints['Sink(Wärme)|flow_rate|ub2'], @@ -1203,12 +1237,10 @@ def test_fixed_relative_profile(self, basic_flow_system_linopy): flow_system.add_elements(fx.Sink('Sink', sink=flow)) model = create_linopy_model(flow_system) - assert_var_equal(flow.submodel.variables['Sink(Wärme)|flow_rate'], - model.add_variables(lower=profile * 100, - upper=profile * 100, - coords=(timesteps,)) - ) - + assert_var_equal( + flow.submodel.variables['Sink(Wärme)|flow_rate'], + model.add_variables(lower=profile * 100, upper=profile * 100, coords=(timesteps,)), + ) def test_fixed_profile_with_investment(self, basic_flow_system_linopy): """Test flow with fixed profile and investment.""" @@ -1225,7 +1257,7 @@ def test_fixed_profile_with_investment(self, basic_flow_system_linopy): fixed_relative_profile=xr.DataArray(profile, coords=(timesteps,)), ) - flow_system.add_elements( fx.Sink('Sink', sink=flow)) + flow_system.add_elements(fx.Sink('Sink', sink=flow)) model = create_linopy_model(flow_system) assert_var_equal( diff --git a/tests/test_io.py b/tests/test_io.py index 8e56f36eb..6e18ef3d3 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -15,7 +15,15 @@ ) -@pytest.fixture(params=[flow_system_base, simple_flow_system_scenarios, flow_system_segments_of_flows_2, simple_flow_system, flow_system_long]) +@pytest.fixture( + params=[ + flow_system_base, + simple_flow_system_scenarios, + flow_system_segments_of_flows_2, + simple_flow_system, + flow_system_long, + ] +) def flow_system(request): fs = request.getfixturevalue(request.param.__name__) if isinstance(fs, fx.FlowSystem): @@ -23,6 +31,7 @@ def flow_system(request): else: return fs[0] + @pytest.mark.slow def test_flow_system_file_io(flow_system, highs_solver): calculation_0 = fx.FullCalculation('IO', flow_system=flow_system) diff --git a/tests/test_linear_converter.py b/tests/test_linear_converter.py index 7f65d8fc2..42dc80077 100644 --- a/tests/test_linear_converter.py +++ b/tests/test_linear_converter.py @@ -25,15 +25,11 @@ def test_basic_linear_converter(self, basic_flow_system_linopy): label='Converter', inputs=[input_flow], outputs=[output_flow], - conversion_factors=[{input_flow.label: 0.8, output_flow.label: 1.0}] + conversion_factors=[{input_flow.label: 0.8, output_flow.label: 1.0}], ) # Add to flow system - flow_system.add_elements( - fx.Bus('input_bus'), - fx.Bus('output_bus'), - converter - ) + flow_system.add_elements(fx.Bus('input_bus'), fx.Bus('output_bus'), converter) # Create model model = create_linopy_model(flow_system) @@ -46,7 +42,7 @@ def test_basic_linear_converter(self, basic_flow_system_linopy): # Check conversion constraint (input * 0.8 == output * 1.0) assert_conequal( model.constraints['Converter|conversion_0'], - input_flow.submodel.flow_rate * 0.8 == output_flow.submodel.flow_rate * 1.0 + input_flow.submodel.flow_rate * 0.8 == output_flow.submodel.flow_rate * 1.0, ) def test_linear_converter_time_varying(self, basic_flow_system_linopy): @@ -67,15 +63,11 @@ def test_linear_converter_time_varying(self, basic_flow_system_linopy): label='Converter', inputs=[input_flow], outputs=[output_flow], - conversion_factors=[{input_flow.label: efficiency_series, output_flow.label: 1.0}] + conversion_factors=[{input_flow.label: efficiency_series, output_flow.label: 1.0}], ) # Add to flow system - flow_system.add_elements( - fx.Bus('input_bus'), - fx.Bus('output_bus'), - converter - ) + flow_system.add_elements(fx.Bus('input_bus'), fx.Bus('output_bus'), converter) # Create model model = create_linopy_model(flow_system) @@ -88,7 +80,7 @@ def test_linear_converter_time_varying(self, basic_flow_system_linopy): # Check conversion constraint (input * efficiency_series == output * 1.0) assert_conequal( model.constraints['Converter|conversion_0'], - input_flow.submodel.flow_rate * efficiency_series == output_flow.submodel.flow_rate * 1.0 + input_flow.submodel.flow_rate * efficiency_series == output_flow.submodel.flow_rate * 1.0, ) def test_linear_converter_multiple_factors(self, basic_flow_system_linopy): @@ -109,17 +101,13 @@ def test_linear_converter_multiple_factors(self, basic_flow_system_linopy): conversion_factors=[ {input_flow1.label: 0.8, output_flow1.label: 1.0}, # input1 -> output1 {input_flow2.label: 0.5, output_flow2.label: 1.0}, # input2 -> output2 - {input_flow1.label: 0.2, output_flow2.label: 0.3} # input1 contributes to output2 - ] + {input_flow1.label: 0.2, output_flow2.label: 0.3}, # input1 contributes to output2 + ], ) # Add to flow system flow_system.add_elements( - fx.Bus('input_bus1'), - fx.Bus('input_bus2'), - fx.Bus('output_bus1'), - fx.Bus('output_bus2'), - converter + fx.Bus('input_bus1'), fx.Bus('input_bus2'), fx.Bus('output_bus1'), fx.Bus('output_bus2'), converter ) # Create model @@ -133,19 +121,19 @@ def test_linear_converter_multiple_factors(self, basic_flow_system_linopy): # Check conversion constraint 1 (input1 * 0.8 == output1 * 1.0) assert_conequal( model.constraints['Converter|conversion_0'], - input_flow1.submodel.flow_rate * 0.8 == output_flow1.submodel.flow_rate * 1.0 + input_flow1.submodel.flow_rate * 0.8 == output_flow1.submodel.flow_rate * 1.0, ) # Check conversion constraint 2 (input2 * 0.5 == output2 * 1.0) assert_conequal( model.constraints['Converter|conversion_1'], - input_flow2.submodel.flow_rate * 0.5 == output_flow2.submodel.flow_rate * 1.0 + input_flow2.submodel.flow_rate * 0.5 == output_flow2.submodel.flow_rate * 1.0, ) # Check conversion constraint 3 (input1 * 0.2 == output2 * 0.3) assert_conequal( model.constraints['Converter|conversion_2'], - input_flow1.submodel.flow_rate * 0.2 == output_flow2.submodel.flow_rate * 0.3 + input_flow1.submodel.flow_rate * 0.2 == output_flow2.submodel.flow_rate * 0.3, ) def test_linear_converter_with_on_off(self, basic_flow_system_linopy): @@ -158,9 +146,7 @@ def test_linear_converter_with_on_off(self, basic_flow_system_linopy): # Create OnOffParameters on_off_params = fx.OnOffParameters( - on_hours_total_min=10, - on_hours_total_max=40, - effects_per_running_hour={'Costs': 5} + on_hours_total_min=10, on_hours_total_max=40, effects_per_running_hour={'Costs': 5} ) # Create a linear converter with OnOffParameters @@ -169,7 +155,7 @@ def test_linear_converter_with_on_off(self, basic_flow_system_linopy): inputs=[input_flow], outputs=[output_flow], conversion_factors=[{input_flow.label: 0.8, output_flow.label: 1.0}], - on_off_parameters=on_off_params + on_off_parameters=on_off_params, ) # Add to flow system @@ -189,22 +175,22 @@ def test_linear_converter_with_on_off(self, basic_flow_system_linopy): # Check on_hours_total constraint assert_conequal( model.constraints['Converter|on_hours_total'], - model.variables['Converter|on_hours_total'] == - (model.variables['Converter|on'] * model.hours_per_step).sum() + model.variables['Converter|on_hours_total'] + == (model.variables['Converter|on'] * model.hours_per_step).sum(), ) # Check conversion constraint assert_conequal( model.constraints['Converter|conversion_0'], - input_flow.submodel.flow_rate * 0.8 == output_flow.submodel.flow_rate * 1.0 + input_flow.submodel.flow_rate * 0.8 == output_flow.submodel.flow_rate * 1.0, ) # Check on_off effects assert 'Converter->Costs(operation)' in model.constraints assert_conequal( model.constraints['Converter->Costs(operation)'], - model.variables['Converter->Costs(operation)'] == - model.variables['Converter|on'] * model.hours_per_step * 5 + model.variables['Converter->Costs(operation)'] + == model.variables['Converter|on'] * model.hours_per_step * 5, ) def test_linear_converter_multidimensional(self, basic_flow_system_linopy): @@ -228,17 +214,13 @@ def test_linear_converter_multidimensional(self, basic_flow_system_linopy): # Electricity to cooling {input_flow2.label: 0.3, output_flow2.label: 1.0}, # Fuel also contributes to cooling - {input_flow1.label: 0.1, output_flow2.label: 0.5} - ] + {input_flow1.label: 0.1, output_flow2.label: 0.5}, + ], ) # Add to flow system flow_system.add_elements( - fx.Bus('fuel_bus'), - fx.Bus('electricity_bus'), - fx.Bus('heat_bus'), - fx.Bus('cooling_bus'), - converter + fx.Bus('fuel_bus'), fx.Bus('electricity_bus'), fx.Bus('heat_bus'), fx.Bus('cooling_bus'), converter ) # Create model @@ -252,17 +234,17 @@ def test_linear_converter_multidimensional(self, basic_flow_system_linopy): # Check the conversion equations assert_conequal( model.constraints['MultiConverter|conversion_0'], - input_flow1.submodel.flow_rate * 0.7 == output_flow1.submodel.flow_rate * 1.0 + input_flow1.submodel.flow_rate * 0.7 == output_flow1.submodel.flow_rate * 1.0, ) assert_conequal( model.constraints['MultiConverter|conversion_1'], - input_flow2.submodel.flow_rate * 0.3 == output_flow2.submodel.flow_rate * 1.0 + input_flow2.submodel.flow_rate * 0.3 == output_flow2.submodel.flow_rate * 1.0, ) assert_conequal( model.constraints['MultiConverter|conversion_2'], - input_flow1.submodel.flow_rate * 0.1 == output_flow2.submodel.flow_rate * 0.5 + input_flow1.submodel.flow_rate * 0.1 == output_flow2.submodel.flow_rate * 0.5, ) def test_edge_case_time_varying_conversion(self, basic_flow_system_linopy): @@ -272,35 +254,27 @@ def test_edge_case_time_varying_conversion(self, basic_flow_system_linopy): # Create fluctuating conversion efficiency (e.g., for a heat pump) # Values range from very low (0.1) to very high (5.0) - fluctuating_cop = np.concatenate([ - np.linspace(0.1, 1.0, len(timesteps)//3), - np.linspace(1.0, 5.0, len(timesteps)//3), - np.linspace(5.0, 0.1, len(timesteps)//3 + len(timesteps)%3) - ]) + fluctuating_cop = np.concatenate( + [ + np.linspace(0.1, 1.0, len(timesteps) // 3), + np.linspace(1.0, 5.0, len(timesteps) // 3), + np.linspace(5.0, 0.1, len(timesteps) // 3 + len(timesteps) % 3), + ] + ) # Create input and output flows input_flow = fx.Flow('electricity', bus='electricity_bus', size=100) output_flow = fx.Flow('heat', bus='heat_bus', size=500) # Higher maximum to allow for COP of 5 - conversion_factors = [{ - input_flow.label: fluctuating_cop, - output_flow.label: np.ones(len(timesteps)) - }] + conversion_factors = [{input_flow.label: fluctuating_cop, output_flow.label: np.ones(len(timesteps))}] # Create the converter converter = fx.LinearConverter( - label='VariableConverter', - inputs=[input_flow], - outputs=[output_flow], - conversion_factors=conversion_factors + label='VariableConverter', inputs=[input_flow], outputs=[output_flow], conversion_factors=conversion_factors ) # Add to flow system - flow_system.add_elements( - fx.Bus('electricity_bus'), - fx.Bus('heat_bus'), - converter - ) + flow_system.add_elements(fx.Bus('electricity_bus'), fx.Bus('heat_bus'), converter) # Create model model = create_linopy_model(flow_system) @@ -311,7 +285,7 @@ def test_edge_case_time_varying_conversion(self, basic_flow_system_linopy): # Verify the constraint has the time-varying coefficient assert_conequal( model.constraints['VariableConverter|conversion_0'], - input_flow.submodel.flow_rate * fluctuating_cop == output_flow.submodel.flow_rate * 1.0 + input_flow.submodel.flow_rate * fluctuating_cop == output_flow.submodel.flow_rate * 1.0, ) def test_piecewise_conversion(self, basic_flow_system_linopy): @@ -325,37 +299,23 @@ def test_piecewise_conversion(self, basic_flow_system_linopy): # Create pieces for piecewise conversion # For input flow: two pieces from 0-50 and 50-100 - input_pieces = [ - fx.Piece(start=0, end=50), - fx.Piece(start=50, end=100) - ] + input_pieces = [fx.Piece(start=0, end=50), fx.Piece(start=50, end=100)] # For output flow: two pieces from 0-30 and 30-90 - output_pieces = [ - fx.Piece(start=0, end=30), - fx.Piece(start=30, end=90) - ] + output_pieces = [fx.Piece(start=0, end=30), fx.Piece(start=30, end=90)] # Create piecewise conversion - piecewise_conversion = fx.PiecewiseConversion({ - input_flow.label: fx.Piecewise(input_pieces), - output_flow.label: fx.Piecewise(output_pieces) - }) + piecewise_conversion = fx.PiecewiseConversion( + {input_flow.label: fx.Piecewise(input_pieces), output_flow.label: fx.Piecewise(output_pieces)} + ) # Create a linear converter with piecewise conversion converter = fx.LinearConverter( - label='Converter', - inputs=[input_flow], - outputs=[output_flow], - piecewise_conversion=piecewise_conversion + label='Converter', inputs=[input_flow], outputs=[output_flow], piecewise_conversion=piecewise_conversion ) # Add to flow system - flow_system.add_elements( - fx.Bus('input_bus'), - fx.Bus('output_bus'), - converter - ) + flow_system.add_elements(fx.Bus('input_bus'), fx.Bus('output_bus'), converter) # Create model with the piecewise conversion model = create_linopy_model(flow_system) @@ -391,8 +351,7 @@ def test_piecewise_conversion(self, basic_flow_system_linopy): assert_conequal( model.constraints['Converter|Converter(input)|flow_rate|lambda'], model.variables['Converter(input)|flow_rate'] - == - model.variables['Converter|Piece_0|lambda0'] * 0 + == model.variables['Converter|Piece_0|lambda0'] * 0 + model.variables['Converter|Piece_0|lambda1'] * 50 + model.variables['Converter|Piece_1|lambda0'] * 50 + model.variables['Converter|Piece_1|lambda1'] * 100, @@ -401,8 +360,7 @@ def test_piecewise_conversion(self, basic_flow_system_linopy): assert_conequal( model.constraints['Converter|Converter(output)|flow_rate|lambda'], model.variables['Converter(output)|flow_rate'] - == - model.variables['Converter|Piece_0|lambda0'] * 0 + == model.variables['Converter|Piece_0|lambda0'] * 0 + model.variables['Converter|Piece_0|lambda1'] * 30 + model.variables['Converter|Piece_1|lambda0'] * 30 + model.variables['Converter|Piece_1|lambda1'] * 90, @@ -415,11 +373,10 @@ def test_piecewise_conversion(self, basic_flow_system_linopy): # If there's no on_off parameter, the right-hand side should be 1 assert_conequal( model.constraints['Converter|Converter(input)|flow_rate|single_segment'], - sum([model.variables[f'Converter|Piece_{i}|inside_piece'] - for i in range(len(piecewise_model.pieces))]) <= 1 + sum([model.variables[f'Converter|Piece_{i}|inside_piece'] for i in range(len(piecewise_model.pieces))]) + <= 1, ) - def test_piecewise_conversion_with_onoff(self, basic_flow_system_linopy): """Test a LinearConverter with PiecewiseConversion and OnOffParameters.""" flow_system = basic_flow_system_linopy @@ -430,27 +387,18 @@ def test_piecewise_conversion_with_onoff(self, basic_flow_system_linopy): output_flow = fx.Flow('output', bus='output_bus', size=100) # Create pieces for piecewise conversion - input_pieces = [ - fx.Piece(start=0, end=50), - fx.Piece(start=50, end=100) - ] + input_pieces = [fx.Piece(start=0, end=50), fx.Piece(start=50, end=100)] - output_pieces = [ - fx.Piece(start=0, end=30), - fx.Piece(start=30, end=90) - ] + output_pieces = [fx.Piece(start=0, end=30), fx.Piece(start=30, end=90)] # Create piecewise conversion - piecewise_conversion = fx.PiecewiseConversion({ - input_flow.label: fx.Piecewise(input_pieces), - output_flow.label: fx.Piecewise(output_pieces) - }) + piecewise_conversion = fx.PiecewiseConversion( + {input_flow.label: fx.Piecewise(input_pieces), output_flow.label: fx.Piecewise(output_pieces)} + ) # Create OnOffParameters on_off_params = fx.OnOffParameters( - on_hours_total_min=10, - on_hours_total_max=40, - effects_per_running_hour={'Costs': 5} + on_hours_total_min=10, on_hours_total_max=40, effects_per_running_hour={'Costs': 5} ) # Create a linear converter with piecewise conversion and on/off parameters @@ -459,7 +407,7 @@ def test_piecewise_conversion_with_onoff(self, basic_flow_system_linopy): inputs=[input_flow], outputs=[output_flow], piecewise_conversion=piecewise_conversion, - on_off_parameters=on_off_params + on_off_parameters=on_off_params, ) # Add to flow system @@ -508,8 +456,7 @@ def test_piecewise_conversion_with_onoff(self, basic_flow_system_linopy): assert_conequal( model.constraints['Converter|Converter(input)|flow_rate|lambda'], model.variables['Converter(input)|flow_rate'] - == - model.variables['Converter|Piece_0|lambda0'] * 0 + == model.variables['Converter|Piece_0|lambda0'] * 0 + model.variables['Converter|Piece_0|lambda1'] * 50 + model.variables['Converter|Piece_1|lambda0'] * 50 + model.variables['Converter|Piece_1|lambda1'] * 100, @@ -518,8 +465,7 @@ def test_piecewise_conversion_with_onoff(self, basic_flow_system_linopy): assert_conequal( model.constraints['Converter|Converter(output)|flow_rate|lambda'], model.variables['Converter(output)|flow_rate'] - == - model.variables['Converter|Piece_0|lambda0'] * 0 + == model.variables['Converter|Piece_0|lambda0'] * 0 + model.variables['Converter|Piece_0|lambda1'] * 30 + model.variables['Converter|Piece_1|lambda0'] * 30 + model.variables['Converter|Piece_1|lambda1'] * 90, @@ -531,24 +477,23 @@ def test_piecewise_conversion_with_onoff(self, basic_flow_system_linopy): # The constraint should enforce that the sum of inside_piece variables is limited assert_conequal( model.constraints['Converter|Converter(input)|flow_rate|single_segment'], - sum([model.variables[f'Converter|Piece_{i}|inside_piece'] - for i in range(len(piecewise_model.pieces))]) <= model.variables['Converter|on'] + sum([model.variables[f'Converter|Piece_{i}|inside_piece'] for i in range(len(piecewise_model.pieces))]) + <= model.variables['Converter|on'], ) # Also check that the OnOff model is working correctly assert 'Converter|on_hours_total' in model.constraints assert_conequal( model.constraints['Converter|on_hours_total'], - model['Converter|on_hours_total'] == - (model['Converter|on'] * model.hours_per_step).sum() + model['Converter|on_hours_total'] == (model['Converter|on'] * model.hours_per_step).sum(), ) # Verify that the costs effect is applied assert 'Converter->Costs(operation)' in model.constraints assert_conequal( model.constraints['Converter->Costs(operation)'], - model.variables['Converter->Costs(operation)'] == - model.variables['Converter|on'] * model.hours_per_step * 5 + model.variables['Converter->Costs(operation)'] + == model.variables['Converter|on'] * model.hours_per_step * 5, ) diff --git a/tests/test_results_plots.py b/tests/test_results_plots.py index fe8d27c3b..35a219e31 100644 --- a/tests/test_results_plots.py +++ b/tests/test_results_plots.py @@ -40,6 +40,7 @@ def plotting_engine(request): def color_spec(request): return request.param + @pytest.mark.slow def test_results_plots(flow_system, plotting_engine, show, save, color_spec): calculation = create_calculation_and_solve(flow_system, fx.solvers.HighsSolver(0.01, 30), 'test_results_plots') @@ -62,6 +63,7 @@ def test_results_plots(flow_system, plotting_engine, show, save, color_spec): plt.close('all') + @pytest.mark.slow def test_color_handling_edge_cases(flow_system, plotting_engine, show, save): """Test edge cases for color handling""" diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py index 62f206e68..0ccc1a5dd 100644 --- a/tests/test_scenarios.py +++ b/tests/test_scenarios.py @@ -17,12 +17,10 @@ def test_system(): """Create a basic test system with scenarios.""" # Create a two-day time index with hourly resolution - timesteps = pd.date_range( - "2023-01-01", periods=48, freq="h", name="time" - ) + timesteps = pd.date_range('2023-01-01', periods=48, freq='h', name='time') # Create two scenarios - scenarios = pd.Index(["Scenario A", "Scenario B"], name="scenario") + scenarios = pd.Index(['Scenario A', 'Scenario B'], name='scenario') # Create scenario weights weights = np.array([0.7, 0.3]) @@ -31,109 +29,90 @@ def test_system(): flow_system = FlowSystem( timesteps=timesteps, scenarios=scenarios, - weights=weights # Use TimeSeriesData for weights + weights=weights, # Use TimeSeriesData for weights ) # Create demand profiles that differ between scenarios # Scenario A: Higher demand in first day, lower in second day # Scenario B: Lower demand in first day, higher in second day - demand_profile_a = np.concatenate([ - np.sin(np.linspace(0, 2*np.pi, 24)) * 5 + 10, # Day 1, max ~15 - np.sin(np.linspace(0, 2*np.pi, 24)) * 2 + 5 # Day 2, max ~7 - ]) + demand_profile_a = np.concatenate( + [ + np.sin(np.linspace(0, 2 * np.pi, 24)) * 5 + 10, # Day 1, max ~15 + np.sin(np.linspace(0, 2 * np.pi, 24)) * 2 + 5, # Day 2, max ~7 + ] + ) - demand_profile_b = np.concatenate([ - np.sin(np.linspace(0, 2*np.pi, 24)) * 2 + 5, # Day 1, max ~7 - np.sin(np.linspace(0, 2*np.pi, 24)) * 5 + 10 # Day 2, max ~15 - ]) + demand_profile_b = np.concatenate( + [ + np.sin(np.linspace(0, 2 * np.pi, 24)) * 2 + 5, # Day 1, max ~7 + np.sin(np.linspace(0, 2 * np.pi, 24)) * 5 + 10, # Day 2, max ~15 + ] + ) # Stack the profiles into a 2D array (time, scenario) demand_profiles = np.column_stack([demand_profile_a, demand_profile_b]) # Create the necessary model elements # Create buses - electricity_bus = Bus("Electricity") + electricity_bus = Bus('Electricity') # Create a demand sink with scenario-dependent profiles - demand = Flow( - label="Demand", - bus=electricity_bus.label_full, - fixed_relative_profile=demand_profiles - ) - demand_sink = Sink("Demand", sink=demand) + demand = Flow(label='Demand', bus=electricity_bus.label_full, fixed_relative_profile=demand_profiles) + demand_sink = Sink('Demand', sink=demand) # Create a power source with investment option power_gen = Flow( - label="Generation", + label='Generation', bus=electricity_bus.label_full, size=InvestParameters( minimum_size=0, maximum_size=20, - specific_effects={"Costs": 100} # €/kW + specific_effects={'Costs': 100}, # €/kW ), - effects_per_flow_hour={"Costs": 20} # €/MWh + effects_per_flow_hour={'Costs': 20}, # €/MWh ) - generator = Source("Generator", source=power_gen) + generator = Source('Generator', source=power_gen) # Create a storage for electricity - storage_charge = Flow( - label="Charge", - bus=electricity_bus.label_full, - size=10 - ) - storage_discharge = Flow( - label="Discharge", - bus=electricity_bus.label_full, - size=10 - ) + storage_charge = Flow(label='Charge', bus=electricity_bus.label_full, size=10) + storage_discharge = Flow(label='Discharge', bus=electricity_bus.label_full, size=10) storage = Storage( - label="Battery", + label='Battery', charging=storage_charge, discharging=storage_discharge, capacity_in_flow_hours=InvestParameters( minimum_size=0, maximum_size=50, - specific_effects={"Costs": 50} # €/kWh + specific_effects={'Costs': 50}, # €/kWh ), eta_charge=0.95, eta_discharge=0.95, - initial_charge_state="lastValueOfSim" + initial_charge_state='lastValueOfSim', ) # Create effects and objective - cost_effect = Effect( - label="Costs", - unit="€", - description="Total costs", - is_standard=True, - is_objective=True - ) + cost_effect = Effect(label='Costs', unit='€', description='Total costs', is_standard=True, is_objective=True) # Add all elements to the flow system - flow_system.add_elements( - electricity_bus, - generator, - demand_sink, - storage, - cost_effect - ) + flow_system.add_elements(electricity_bus, generator, demand_sink, storage, cost_effect) # Return the created system and its components return { - "flow_system": flow_system, - "timesteps": timesteps, - "scenarios": scenarios, - "electricity_bus": electricity_bus, - "demand": demand, - "demand_sink": demand_sink, - "generator": generator, - "power_gen": power_gen, - "storage": storage, - "storage_charge": storage_charge, - "storage_discharge": storage_discharge, - "cost_effect": cost_effect + 'flow_system': flow_system, + 'timesteps': timesteps, + 'scenarios': scenarios, + 'electricity_bus': electricity_bus, + 'demand': demand, + 'demand_sink': demand_sink, + 'generator': generator, + 'power_gen': power_gen, + 'storage': storage, + 'storage_charge': storage_charge, + 'storage_discharge': storage_discharge, + 'cost_effect': cost_effect, } + @pytest.fixture def flow_system_complex_scenarios() -> fx.FlowSystem: """ @@ -141,8 +120,10 @@ def flow_system_complex_scenarios() -> fx.FlowSystem: """ thermal_load = np.array([30, 0, 90, 110, 110, 20, 20, 20, 20]) electrical_load = np.array([40, 40, 40, 40, 40, 40, 40, 40, 40]) - flow_system = fx.FlowSystem(pd.date_range('2020-01-01', periods=9, freq='h', name='time'), - scenarios=pd.Index(['A', 'B', 'C'], name='scenario')) + flow_system = fx.FlowSystem( + pd.date_range('2020-01-01', periods=9, freq='h', name='time'), + scenarios=pd.Index(['A', 'B', 'C'], name='scenario'), + ) # Define the components and flow_system flow_system.add_elements( fx.Effect('costs', '€', 'Kosten', is_standard=True, is_objective=True), @@ -260,10 +241,12 @@ def test_weights(flow_system_piecewise_conversion_scenarios): flow_system_piecewise_conversion_scenarios.weights = weights model = create_linopy_model(flow_system_piecewise_conversion_scenarios) np.testing.assert_allclose(model.weights.values, weights) - assert_linequal(model.objective.expression, - (model.variables['costs|total'] * weights).sum() + model.variables['Penalty|total']) + assert_linequal( + model.objective.expression, (model.variables['costs|total'] * weights).sum() + model.variables['Penalty|total'] + ) assert np.isclose(model.weights.sum().item(), 2.25) + def test_weights_io(flow_system_piecewise_conversion_scenarios): """Test that scenario weights are correctly used in the model.""" scenarios = flow_system_piecewise_conversion_scenarios.scenarios @@ -271,24 +254,29 @@ def test_weights_io(flow_system_piecewise_conversion_scenarios): flow_system_piecewise_conversion_scenarios.weights = weights model = create_linopy_model(flow_system_piecewise_conversion_scenarios) np.testing.assert_allclose(model.weights.values, weights) - assert_linequal(model.objective.expression, - (model.variables['costs|total'] * weights).sum() + model.variables['Penalty|total']) + assert_linequal( + model.objective.expression, (model.variables['costs|total'] * weights).sum() + model.variables['Penalty|total'] + ) assert np.isclose(model.weights.sum().item(), 1.0) + def test_scenario_dimensions_in_variables(flow_system_piecewise_conversion_scenarios): """Test that all time variables are correctly broadcasted to scenario dimensions.""" model = create_linopy_model(flow_system_piecewise_conversion_scenarios) for var in model.variables: - assert model.variables[var].dims in [('time', 'scenario'), ('scenario',), ()] + assert model.variables[var].dims in [('time', 'scenario'), ('scenario',), ()] + def test_full_scenario_optimization(flow_system_piecewise_conversion_scenarios): """Test a full optimization with scenarios and verify results.""" scenarios = flow_system_piecewise_conversion_scenarios.scenarios weights = np.linspace(0.5, 1, len(scenarios)) / np.sum(np.linspace(0.5, 1, len(scenarios))) flow_system_piecewise_conversion_scenarios.weights = weights - calc = create_calculation_and_solve(flow_system_piecewise_conversion_scenarios, - solver=fx.solvers.GurobiSolver(mip_gap=0.01, time_limit_seconds=60), - name='test_full_scenario') + calc = create_calculation_and_solve( + flow_system_piecewise_conversion_scenarios, + solver=fx.solvers.GurobiSolver(mip_gap=0.01, time_limit_seconds=60), + name='test_full_scenario', + ) calc.results.to_file() res = fx.results.CalculationResults.from_file('results', 'test_full_scenario') @@ -299,15 +287,18 @@ def test_full_scenario_optimization(flow_system_piecewise_conversion_scenarios): name='test_full_scenario', ) -@pytest.mark.skip(reason="This test is taking too long with highs and is too big for gurobipy free") + +@pytest.mark.skip(reason='This test is taking too long with highs and is too big for gurobipy free') def test_io_persistance(flow_system_piecewise_conversion_scenarios): """Test a full optimization with scenarios and verify results.""" scenarios = flow_system_piecewise_conversion_scenarios.scenarios weights = np.linspace(0.5, 1, len(scenarios)) / np.sum(np.linspace(0.5, 1, len(scenarios))) flow_system_piecewise_conversion_scenarios.weights = weights - calc = create_calculation_and_solve(flow_system_piecewise_conversion_scenarios, - solver=fx.solvers.HighsSolver(mip_gap=0.001, time_limit_seconds=60), - name='test_full_scenario') + calc = create_calculation_and_solve( + flow_system_piecewise_conversion_scenarios, + solver=fx.solvers.HighsSolver(mip_gap=0.001, time_limit_seconds=60), + name='test_full_scenario', + ) calc.results.to_file() res = fx.results.CalculationResults.from_file('results', 'test_full_scenario') @@ -332,13 +323,17 @@ def test_scenarios_selection(flow_system_piecewise_conversion_scenarios): np.testing.assert_allclose(flow_system.weights.values, flow_system_full.weights[0:2]) - calc = fx.FullCalculation(flow_system=flow_system, name='test_full_scenario') calc.do_modeling() calc.solve(fx.solvers.GurobiSolver(mip_gap=0.01, time_limit_seconds=60)) calc.results.to_file() - np.testing.assert_allclose(calc.results.objective, ((calc.results.solution['costs|total'] * flow_system.weights).sum() + calc.results.solution['Penalty|total']).item()) ## Acount for rounding errors + np.testing.assert_allclose( + calc.results.objective, + ( + (calc.results.solution['costs|total'] * flow_system.weights).sum() + calc.results.solution['Penalty|total'] + ).item(), + ) ## Acount for rounding errors assert calc.results.solution.indexes['scenario'].equals(flow_system_full.scenarios[0:2]) diff --git a/tests/test_storage.py b/tests/test_storage.py index 3a6b2a06c..f6b6f2079 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -40,7 +40,7 @@ def test_basic_storage(self, basic_flow_system_linopy): 'TestStorage|netto_discharge', } for var_name in expected_variables: - assert var_name in model.variables, f"Missing variable: {var_name}" + assert var_name in model.variables, f'Missing variable: {var_name}' # Check that all expected constraints exist - linopy model constraints are accessed by indexing expected_constraints = { @@ -51,27 +51,24 @@ def test_basic_storage(self, basic_flow_system_linopy): 'TestStorage|initial_charge_state', } for con_name in expected_constraints: - assert con_name in model.constraints, f"Missing constraint: {con_name}" + assert con_name in model.constraints, f'Missing constraint: {con_name}' # Check variable properties assert_var_equal( - model['TestStorage(Q_th_in)|flow_rate'], - model.add_variables(lower=0, upper=20, coords=(timesteps,)) + model['TestStorage(Q_th_in)|flow_rate'], model.add_variables(lower=0, upper=20, coords=(timesteps,)) ) assert_var_equal( - model['TestStorage(Q_th_out)|flow_rate'], - model.add_variables(lower=0, upper=20, coords=(timesteps,)) + model['TestStorage(Q_th_out)|flow_rate'], model.add_variables(lower=0, upper=20, coords=(timesteps,)) ) assert_var_equal( - model['TestStorage|charge_state'], - model.add_variables(lower=0, upper=30, coords=(timesteps_extra,)) + model['TestStorage|charge_state'], model.add_variables(lower=0, upper=30, coords=(timesteps_extra,)) ) # Check constraint formulations assert_conequal( model.constraints['TestStorage|netto_discharge'], - model.variables['TestStorage|netto_discharge'] == - model.variables['TestStorage(Q_th_out)|flow_rate'] - model.variables['TestStorage(Q_th_in)|flow_rate'] + model.variables['TestStorage|netto_discharge'] + == model.variables['TestStorage(Q_th_out)|flow_rate'] - model.variables['TestStorage(Q_th_in)|flow_rate'], ) charge_state = model.variables['TestStorage|charge_state'] @@ -80,12 +77,12 @@ def test_basic_storage(self, basic_flow_system_linopy): charge_state.isel(time=slice(1, None)) == charge_state.isel(time=slice(None, -1)) + model.variables['TestStorage(Q_th_in)|flow_rate'] * model.hours_per_step - - model.variables['TestStorage(Q_th_out)|flow_rate'] * model.hours_per_step, + - model.variables['TestStorage(Q_th_out)|flow_rate'] * model.hours_per_step, ) # Check initial charge state constraint assert_conequal( model.constraints['TestStorage|initial_charge_state'], - model.variables['TestStorage|charge_state'].isel(time=0) == 0 + model.variables['TestStorage|charge_state'].isel(time=0) == 0, ) def test_lossy_storage(self, basic_flow_system_linopy): @@ -120,7 +117,7 @@ def test_lossy_storage(self, basic_flow_system_linopy): 'TestStorage|netto_discharge', } for var_name in expected_variables: - assert var_name in model.variables, f"Missing variable: {var_name}" + assert var_name in model.variables, f'Missing variable: {var_name}' # Check that all expected constraints exist - linopy model constraints are accessed by indexing expected_constraints = { @@ -131,27 +128,24 @@ def test_lossy_storage(self, basic_flow_system_linopy): 'TestStorage|initial_charge_state', } for con_name in expected_constraints: - assert con_name in model.constraints, f"Missing constraint: {con_name}" + assert con_name in model.constraints, f'Missing constraint: {con_name}' # Check variable properties assert_var_equal( - model['TestStorage(Q_th_in)|flow_rate'], - model.add_variables(lower=0, upper=20, coords=(timesteps,)) + model['TestStorage(Q_th_in)|flow_rate'], model.add_variables(lower=0, upper=20, coords=(timesteps,)) ) assert_var_equal( - model['TestStorage(Q_th_out)|flow_rate'], - model.add_variables(lower=0, upper=20, coords=(timesteps,)) + model['TestStorage(Q_th_out)|flow_rate'], model.add_variables(lower=0, upper=20, coords=(timesteps,)) ) assert_var_equal( - model['TestStorage|charge_state'], - model.add_variables(lower=0, upper=30, coords=(timesteps_extra,)) + model['TestStorage|charge_state'], model.add_variables(lower=0, upper=30, coords=(timesteps_extra,)) ) # Check constraint formulations assert_conequal( model.constraints['TestStorage|netto_discharge'], - model.variables['TestStorage|netto_discharge'] == - model.variables['TestStorage(Q_th_out)|flow_rate'] - model.variables['TestStorage(Q_th_in)|flow_rate'] + model.variables['TestStorage|netto_discharge'] + == model.variables['TestStorage(Q_th_out)|flow_rate'] - model.variables['TestStorage(Q_th_in)|flow_rate'], ) charge_state = model.variables['TestStorage|charge_state'] @@ -166,13 +160,14 @@ def test_lossy_storage(self, basic_flow_system_linopy): model.constraints['TestStorage|charge_state'], charge_state.isel(time=slice(1, None)) == charge_state.isel(time=slice(None, -1)) * (1 - rel_loss) ** hours_per_step - + charge_rate * eff_charge * hours_per_step - discharge_rate * eff_discharge * hours_per_step, + + charge_rate * eff_charge * hours_per_step + - discharge_rate * eff_discharge * hours_per_step, ) # Check initial charge state constraint assert_conequal( model.constraints['TestStorage|initial_charge_state'], - model.variables['TestStorage|charge_state'].isel(time=0) == 0 + model.variables['TestStorage|charge_state'].isel(time=0) == 0, ) def test_charge_state_bounds(self, basic_flow_system_linopy): @@ -189,7 +184,7 @@ def test_charge_state_bounds(self, basic_flow_system_linopy): capacity_in_flow_hours=30, # 30 kWh storage capacity initial_charge_state=3, prevent_simultaneous_charge_and_discharge=True, - relative_maximum_charge_state=np.array([0.14, 0.22, 0.3 , 0.38, 0.46, 0.54, 0.62, 0.7 , 0.78, 0.86]), + relative_maximum_charge_state=np.array([0.14, 0.22, 0.3, 0.38, 0.46, 0.54, 0.62, 0.7, 0.78, 0.86]), relative_minimum_charge_state=np.array([0.07, 0.11, 0.15, 0.19, 0.23, 0.27, 0.31, 0.35, 0.39, 0.43]), ) @@ -206,7 +201,7 @@ def test_charge_state_bounds(self, basic_flow_system_linopy): 'TestStorage|netto_discharge', } for var_name in expected_variables: - assert var_name in model.variables, f"Missing variable: {var_name}" + assert var_name in model.variables, f'Missing variable: {var_name}' # Check that all expected constraints exist - linopy model constraints are accessed by indexing expected_constraints = { @@ -217,29 +212,29 @@ def test_charge_state_bounds(self, basic_flow_system_linopy): 'TestStorage|initial_charge_state', } for con_name in expected_constraints: - assert con_name in model.constraints, f"Missing constraint: {con_name}" + assert con_name in model.constraints, f'Missing constraint: {con_name}' # Check variable properties assert_var_equal( - model['TestStorage(Q_th_in)|flow_rate'], - model.add_variables(lower=0, upper=20, coords=(timesteps,)) + model['TestStorage(Q_th_in)|flow_rate'], model.add_variables(lower=0, upper=20, coords=(timesteps,)) ) assert_var_equal( - model['TestStorage(Q_th_out)|flow_rate'], - model.add_variables(lower=0, upper=20, coords=(timesteps,)) + model['TestStorage(Q_th_out)|flow_rate'], model.add_variables(lower=0, upper=20, coords=(timesteps,)) ) assert_var_equal( model['TestStorage|charge_state'], - model.add_variables(lower=np.array([0.07, 0.11, 0.15, 0.19, 0.23, 0.27, 0.31, 0.35, 0.39, 0.43, 0.43]) * 30, - upper=np.array([0.14, 0.22, 0.3 , 0.38, 0.46, 0.54, 0.62, 0.7 , 0.78, 0.86, 0.86]) * 30, - coords=(timesteps_extra,)) + model.add_variables( + lower=np.array([0.07, 0.11, 0.15, 0.19, 0.23, 0.27, 0.31, 0.35, 0.39, 0.43, 0.43]) * 30, + upper=np.array([0.14, 0.22, 0.3, 0.38, 0.46, 0.54, 0.62, 0.7, 0.78, 0.86, 0.86]) * 30, + coords=(timesteps_extra,), + ), ) # Check constraint formulations assert_conequal( model.constraints['TestStorage|netto_discharge'], - model.variables['TestStorage|netto_discharge'] == - model.variables['TestStorage(Q_th_out)|flow_rate'] - model.variables['TestStorage(Q_th_in)|flow_rate'] + model.variables['TestStorage|netto_discharge'] + == model.variables['TestStorage(Q_th_out)|flow_rate'] - model.variables['TestStorage(Q_th_in)|flow_rate'], ) charge_state = model.variables['TestStorage|charge_state'] @@ -248,12 +243,12 @@ def test_charge_state_bounds(self, basic_flow_system_linopy): charge_state.isel(time=slice(1, None)) == charge_state.isel(time=slice(None, -1)) + model.variables['TestStorage(Q_th_in)|flow_rate'] * model.hours_per_step - - model.variables['TestStorage(Q_th_out)|flow_rate'] * model.hours_per_step, + - model.variables['TestStorage(Q_th_out)|flow_rate'] * model.hours_per_step, ) # Check initial charge state constraint assert_conequal( model.constraints['TestStorage|initial_charge_state'], - model.variables['TestStorage|charge_state'].isel(time=0) == 3 + model.variables['TestStorage|charge_state'].isel(time=0) == 3, ) def test_storage_with_investment(self, basic_flow_system_linopy): @@ -266,11 +261,7 @@ def test_storage_with_investment(self, basic_flow_system_linopy): charging=fx.Flow('Q_th_in', bus='Fernwärme', size=20), discharging=fx.Flow('Q_th_out', bus='Fernwärme', size=20), capacity_in_flow_hours=fx.InvestParameters( - fix_effects=100, - specific_effects=10, - minimum_size=20, - maximum_size=100, - optional=True + fix_effects=100, specific_effects=10, minimum_size=20, maximum_size=100, optional=True ), initial_charge_state=0, eta_charge=0.9, @@ -288,25 +279,23 @@ def test_storage_with_investment(self, basic_flow_system_linopy): 'InvestStorage|size', 'InvestStorage|is_invested', }: - assert var_name in model.variables, f"Missing investment variable: {var_name}" + assert var_name in model.variables, f'Missing investment variable: {var_name}' # Check investment constraints exist for con_name in {'InvestStorage|size|ub', 'InvestStorage|size|lb'}: - assert con_name in model.constraints, f"Missing investment constraint: {con_name}" + assert con_name in model.constraints, f'Missing investment constraint: {con_name}' # Check variable properties - assert_var_equal( - model['InvestStorage|size'], - model.add_variables(lower=0, upper=100) + assert_var_equal(model['InvestStorage|size'], model.add_variables(lower=0, upper=100)) + assert_var_equal(model['InvestStorage|is_invested'], model.add_variables(binary=True)) + assert_conequal( + model.constraints['InvestStorage|size|ub'], + model.variables['InvestStorage|size'] <= model.variables['InvestStorage|is_invested'] * 100, ) - assert_var_equal( - model['InvestStorage|is_invested'], - model.add_variables(binary=True) + assert_conequal( + model.constraints['InvestStorage|size|lb'], + model.variables['InvestStorage|size'] >= model.variables['InvestStorage|is_invested'] * 20, ) - assert_conequal(model.constraints['InvestStorage|size|ub'], - model.variables['InvestStorage|size'] <= model.variables['InvestStorage|is_invested'] * 100) - assert_conequal(model.constraints['InvestStorage|size|lb'], - model.variables['InvestStorage|size'] >= model.variables['InvestStorage|is_invested'] * 20) def test_storage_with_final_state_constraints(self, basic_flow_system_linopy): """Test storage with final state constraints.""" @@ -336,7 +325,7 @@ def test_storage_with_final_state_constraints(self, basic_flow_system_linopy): } for con_name in expected_constraints: - assert con_name in model.constraints, f"Missing final state constraint: {con_name}" + assert con_name in model.constraints, f'Missing final state constraint: {con_name}' assert_conequal( model.constraints['FinalStateStorage|initial_charge_state'], @@ -346,11 +335,11 @@ def test_storage_with_final_state_constraints(self, basic_flow_system_linopy): # Check final state constraint formulations assert_conequal( model.constraints['FinalStateStorage|final_charge_min'], - model.variables['FinalStateStorage|charge_state'].isel(time=-1) >= 15 + model.variables['FinalStateStorage|charge_state'].isel(time=-1) >= 15, ) assert_conequal( model.constraints['FinalStateStorage|final_charge_max'], - model.variables['FinalStateStorage|charge_state'].isel(time=-1) <= 25 + model.variables['FinalStateStorage|charge_state'].isel(time=-1) <= 25, ) def test_storage_cyclic_initialization(self, basic_flow_system_linopy): @@ -373,14 +362,13 @@ def test_storage_cyclic_initialization(self, basic_flow_system_linopy): model = create_linopy_model(flow_system) # Check cyclic constraint exists - assert 'CyclicStorage|initial_charge_state' in model.constraints, \ - "Missing cyclic initialization constraint" + assert 'CyclicStorage|initial_charge_state' in model.constraints, 'Missing cyclic initialization constraint' # Check cyclic constraint formulation assert_conequal( model.constraints['CyclicStorage|initial_charge_state'], - model.variables['CyclicStorage|charge_state'].isel(time=0) == - model.variables['CyclicStorage|charge_state'].isel(time=-1) + model.variables['CyclicStorage|charge_state'].isel(time=0) + == model.variables['CyclicStorage|charge_state'].isel(time=-1), ) @pytest.mark.parametrize( @@ -420,8 +408,11 @@ def test_simultaneous_charge_discharge(self, basic_flow_system_linopy, prevent_s constraint_name = 'SimultaneousStorage|prevent_simultaneous_use' assert constraint_name in model.constraints, 'Missing constraint to prevent simultaneous operation' - assert_conequal(model.constraints['SimultaneousStorage|prevent_simultaneous_use'], - model.variables['SimultaneousStorage(Q_th_in)|on'] + model.variables['SimultaneousStorage(Q_th_out)|on'] <= 1) + assert_conequal( + model.constraints['SimultaneousStorage|prevent_simultaneous_use'], + model.variables['SimultaneousStorage(Q_th_in)|on'] + model.variables['SimultaneousStorage(Q_th_out)|on'] + <= 1, + ) @pytest.mark.parametrize( 'optional,minimum_size,expected_vars,expected_constraints', @@ -432,7 +423,9 @@ def test_simultaneous_charge_discharge(self, basic_flow_system_linopy, prevent_s (False, 20, set(), set()), ], ) - def test_investment_parameters(self, basic_flow_system_linopy, optional, minimum_size, expected_vars, expected_constraints): + def test_investment_parameters( + self, basic_flow_system_linopy, optional, minimum_size, expected_vars, expected_constraints + ): """Test different investment parameter combinations.""" flow_system = basic_flow_system_linopy @@ -463,12 +456,12 @@ def test_investment_parameters(self, basic_flow_system_linopy, optional, minimum # Check that expected variables exist for var_name in expected_vars: if optional: - assert var_name in model.variables, f"Expected variable {var_name} not found" + assert var_name in model.variables, f'Expected variable {var_name} not found' # Check that expected constraints exist for constraint_name in expected_constraints: if optional: - assert constraint_name in model.constraints, f"Expected constraint {constraint_name} not found" + assert constraint_name in model.constraints, f'Expected constraint {constraint_name} not found' # If optional is False, is_invested should be fixed to 1 if not optional: @@ -476,5 +469,6 @@ def test_investment_parameters(self, basic_flow_system_linopy, optional, minimum if 'InvestStorage|is_invested' in model.variables: var = model.variables['InvestStorage|is_invested'] # Check if the lower and upper bounds are both 1 - assert var.upper == 1 and var.lower == 1, \ - "is_invested variable should be fixed to 1 when optional=False" + assert var.upper == 1 and var.lower == 1, ( + 'is_invested variable should be fixed to 1 when optional=False' + ) diff --git a/tests/todos.txt b/tests/todos.txt index b82e77f34..d4628c259 100644 --- a/tests/todos.txt +++ b/tests/todos.txt @@ -1,5 +1,5 @@ # testing of # abschnittsweise linear testen - # Komponenten mit offenen Flows + # Komponenten mit offenen Flows # Binärvariablen ohne max-Wert-Vorgabe des Flows (Binärungenauigkeitsproblem) - # Medien-zulässigkeit \ No newline at end of file + # Medien-zulässigkeit From 1716607c88323445fe5c56053a3377d28288caf4 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 23 Jul 2025 16:08:24 +0200 Subject: [PATCH 261/448] Fix link in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 68966910b..8406f971f 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ For detailed licensing and installation instructions, refer to the respective so --- ## 🛠 Development Setup -Look into our docs for [development setup](https://flixopt.github.io/flixopt/latest/contribute/#development-setup) +Look into our docs for [development setup](https://flixopt.github.io/flixopt/latest/faq/contribute/) --- From b4a92361ca5777e126c638b1d6c17e752e1a3889 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 23 Jul 2025 16:48:13 +0200 Subject: [PATCH 262/448] Update Effect name in tests to be 'costs' instead of 'Costs' Everywhere Simplify testing by creating a Element Library --- tests/conftest.py | 742 +++++++++++++++---------- tests/test_effect.py | 16 +- tests/test_effects_shares_summation.py | 16 +- tests/test_flow.py | 46 +- tests/test_integration.py | 6 +- tests/test_linear_converter.py | 16 +- tests/test_scenarios.py | 8 +- 7 files changed, 487 insertions(+), 363 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index d12ca9eca..3d6623eb7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,7 +5,7 @@ """ import os -from typing import Iterable +from typing import Dict, Iterable import linopy.testing import numpy as np @@ -16,6 +16,10 @@ import flixopt as fx from flixopt.structure import FlowSystemModel +# ============================================================================ +# SOLVER FIXTURES +# ============================================================================ + @pytest.fixture() def highs_solver(): @@ -32,20 +36,320 @@ def solver_fixture(request): return request.getfixturevalue(request.param.__name__) -# Custom assertion function -def assert_almost_equal_numeric( - actual, desired, err_msg, relative_error_range_in_percent=0.011, absolute_tolerance=1e-7 -): - """ - Custom assertion function for comparing numeric values with relative and absolute tolerances - """ - relative_tol = relative_error_range_in_percent / 100 +# ============================================================================ +# HIERARCHICAL ELEMENT LIBRARY +# ============================================================================ - if isinstance(desired, (int, float)): - delta = abs(relative_tol * desired) if desired != 0 else absolute_tolerance - assert np.isclose(actual, desired, atol=delta), err_msg - else: - np.testing.assert_allclose(actual, desired, rtol=relative_tol, atol=absolute_tolerance, err_msg=err_msg) + +class Buses: + """Standard buses used across flow systems""" + + @staticmethod + def electricity(): + return fx.Bus('Strom') + + @staticmethod + def heat(): + return fx.Bus('Fernwärme') + + @staticmethod + def gas(): + return fx.Bus('Gas') + + @staticmethod + def coal(): + return fx.Bus('Kohle') + + @staticmethod + def defaults(): + """Get all standard buses at once""" + return [Buses.electricity(), Buses.heat(), Buses.gas()] + + +class Effects: + """Standard effects used across flow systems""" + + @staticmethod + def costs(): + return fx.Effect('costs', '€', 'Kosten', is_standard=True, is_objective=True) + + @staticmethod + def co2(): + return fx.Effect('CO2', 'kg', 'CO2_e-Emissionen') + + @staticmethod + def co2_with_costs_share(): + return fx.Effect( + 'CO2', + 'kg', + 'CO2_e-Emissionen', + specific_share_to_other_effects_operation={'costs': 0.2}, + ) + + @staticmethod + def primary_energy(): + return fx.Effect('PE', 'kWh_PE', 'Primärenergie') + + +class Converters: + """Energy conversion components""" + + class Boilers: + @staticmethod + def simple(): + """Simple boiler from simple_flow_system""" + return fx.linear_converters.Boiler( + 'Boiler', + eta=0.5, + Q_th=fx.Flow( + 'Q_th', + bus='Fernwärme', + size=50, + relative_minimum=5 / 50, + relative_maximum=1, + on_off_parameters=fx.OnOffParameters(), + ), + Q_fu=fx.Flow('Q_fu', bus='Gas'), + ) + + @staticmethod + def complex(): + """Complex boiler with investment parameters from flow_system_complex""" + return fx.linear_converters.Boiler( + 'Kessel', + eta=0.5, + on_off_parameters=fx.OnOffParameters(effects_per_running_hour={'costs': 0, 'CO2': 1000}), + Q_th=fx.Flow( + 'Q_th', + bus='Fernwärme', + load_factor_max=1.0, + load_factor_min=0.1, + relative_minimum=5 / 50, + relative_maximum=1, + previous_flow_rate=50, + size=fx.InvestParameters( + fix_effects=1000, fixed_size=50, optional=False, specific_effects={'costs': 10, 'PE': 2} + ), + on_off_parameters=fx.OnOffParameters( + on_hours_total_min=0, + on_hours_total_max=1000, + consecutive_on_hours_max=10, + consecutive_on_hours_min=1, + consecutive_off_hours_max=10, + effects_per_switch_on=0.01, + switch_on_total_max=1000, + ), + flow_hours_total_max=1e6, + ), + Q_fu=fx.Flow('Q_fu', bus='Gas', size=200, relative_minimum=0, relative_maximum=1), + ) + + class CHPs: + @staticmethod + def simple(): + """Simple CHP from simple_flow_system""" + return fx.linear_converters.CHP( + 'CHP_unit', + eta_th=0.5, + eta_el=0.4, + P_el=fx.Flow( + 'P_el', bus='Strom', size=60, relative_minimum=5 / 60, on_off_parameters=fx.OnOffParameters() + ), + Q_th=fx.Flow('Q_th', bus='Fernwärme'), + Q_fu=fx.Flow('Q_fu', bus='Gas'), + ) + + @staticmethod + def base(): + """CHP from flow_system_base""" + return fx.linear_converters.CHP( + 'KWK', + eta_th=0.5, + eta_el=0.4, + on_off_parameters=fx.OnOffParameters(effects_per_switch_on=0.01), + P_el=fx.Flow('P_el', bus='Strom', size=60, relative_minimum=5 / 60, previous_flow_rate=10), + Q_th=fx.Flow('Q_th', bus='Fernwärme', size=1e3), + Q_fu=fx.Flow('Q_fu', bus='Gas', size=1e3), + ) + + class LinearConverters: + @staticmethod + def piecewise(): + """Piecewise converter from flow_system_piecewise_conversion""" + return fx.LinearConverter( + 'KWK', + inputs=[fx.Flow('Q_fu', bus='Gas')], + outputs=[ + fx.Flow('P_el', bus='Strom', size=60, relative_maximum=55, previous_flow_rate=10), + fx.Flow('Q_th', bus='Fernwärme'), + ], + piecewise_conversion=fx.PiecewiseConversion( + { + 'P_el': fx.Piecewise([fx.Piece(5, 30), fx.Piece(40, 60)]), + 'Q_th': fx.Piecewise([fx.Piece(6, 35), fx.Piece(45, 100)]), + 'Q_fu': fx.Piecewise([fx.Piece(12, 70), fx.Piece(90, 200)]), + } + ), + on_off_parameters=fx.OnOffParameters(effects_per_switch_on=0.01), + ) + + @staticmethod + def segments(timesteps_length): + """Segments converter with time-varying piecewise conversion""" + return fx.LinearConverter( + 'KWK', + inputs=[fx.Flow('Q_fu', bus='Gas')], + outputs=[ + fx.Flow('P_el', bus='Strom', size=60, relative_maximum=55, previous_flow_rate=10), + fx.Flow('Q_th', bus='Fernwärme'), + ], + piecewise_conversion=fx.PiecewiseConversion( + { + 'P_el': fx.Piecewise( + [ + fx.Piece(np.linspace(5, 6, timesteps_length), 30), + fx.Piece(40, np.linspace(60, 70, timesteps_length)), + ] + ), + 'Q_th': fx.Piecewise([fx.Piece(6, 35), fx.Piece(45, 100)]), + 'Q_fu': fx.Piecewise([fx.Piece(12, 70), fx.Piece(90, 200)]), + } + ), + on_off_parameters=fx.OnOffParameters(effects_per_switch_on=0.01), + ) + + +class Storage: + """Energy storage components""" + + @staticmethod + def simple(): + """Simple storage from simple_flow_system""" + return fx.Storage( + 'Speicher', + charging=fx.Flow('Q_th_load', bus='Fernwärme', size=1e4), + discharging=fx.Flow('Q_th_unload', bus='Fernwärme', size=1e4), + capacity_in_flow_hours=fx.InvestParameters(fix_effects=20, fixed_size=30, optional=False), + initial_charge_state=0, + relative_maximum_charge_state=1 / 100 * np.array([80.0, 70.0, 80.0, 80, 80, 80, 80, 80, 80]), + relative_maximum_final_charge_state=0.8, + eta_charge=0.9, + eta_discharge=1, + relative_loss_per_hour=0.08, + prevent_simultaneous_charge_and_discharge=True, + ) + + @staticmethod + def complex(): + """Complex storage with piecewise investment from flow_system_complex""" + invest_speicher = fx.InvestParameters( + fix_effects=0, + piecewise_effects=fx.PiecewiseEffects( + piecewise_origin=fx.Piecewise([fx.Piece(5, 25), fx.Piece(25, 100)]), + piecewise_shares={ + 'costs': fx.Piecewise([fx.Piece(50, 250), fx.Piece(250, 800)]), + 'PE': fx.Piecewise([fx.Piece(5, 25), fx.Piece(25, 100)]), + }, + ), + optional=False, + specific_effects={'costs': 0.01, 'CO2': 0.01}, + minimum_size=0, + maximum_size=1000, + ) + return fx.Storage( + 'Speicher', + charging=fx.Flow('Q_th_load', bus='Fernwärme', size=1e4), + discharging=fx.Flow('Q_th_unload', bus='Fernwärme', size=1e4), + capacity_in_flow_hours=invest_speicher, + initial_charge_state=0, + maximal_final_charge_state=10, + eta_charge=0.9, + eta_discharge=1, + relative_loss_per_hour=0.08, + prevent_simultaneous_charge_and_discharge=True, + ) + + +class LoadProfiles: + """Standard load and price profiles""" + + @staticmethod + def thermal_simple(): + return np.array([30.0, 0.0, 90.0, 110, 110, 20, 20, 20, 20]) + + @staticmethod + def thermal_complex(): + return np.array([30, 0, 90, 110, 110, 20, 20, 20, 20]) + + @staticmethod + def electrical_simple(): + return 1 / 1000 * np.array([80.0, 80.0, 80.0, 80, 80, 80, 80, 80, 80]) + + @staticmethod + def electrical_scenario(): + return np.array([0.08, 0.1, 0.15]) + + @staticmethod + def electrical_complex(): + return np.array([40, 40, 40, 40, 40, 40, 40, 40, 40]) + + @staticmethod + def random_thermal(length=10, seed=42): + np.random.seed(seed) + return np.array([np.random.random() for _ in range(length)]) * 180 + + @staticmethod + def random_electrical(length=10, seed=42): + np.random.seed(seed) + return (np.array([np.random.random() for _ in range(length)]) + 0.5) / 1.5 * 50 + + +class Sinks: + """Energy sinks (loads)""" + + @staticmethod + def heat_load(thermal_profile): + """Create thermal heat load sink""" + return fx.Sink( + 'Wärmelast', sink=fx.Flow('Q_th_Last', bus='Fernwärme', size=1, fixed_relative_profile=thermal_profile) + ) + + @staticmethod + def electricity_feed_in(electrical_price_profile): + """Create electricity feed-in sink""" + return fx.Sink( + 'Einspeisung', sink=fx.Flow('P_el', bus='Strom', effects_per_flow_hour=-1 * electrical_price_profile) + ) + + @staticmethod + def electricity_load(electrical_profile): + """Create electrical load sink (for flow_system_long)""" + return fx.Sink( + 'Stromlast', sink=fx.Flow('P_el_Last', bus='Strom', size=1, fixed_relative_profile=electrical_profile) + ) + + +class Sources: + """Energy sources""" + + @staticmethod + def gas_with_costs_and_co2(): + """Standard gas tariff with CO2 emissions""" + source = Sources.gas_with_costs() + source.source.effects_per_flow_hour = {'costs': 0.04, 'CO2': 0.3} + return source + + @staticmethod + def gas_with_costs(): + """Simple gas tariff without CO2""" + return fx.Source( + 'Gastarif', source=fx.Flow(label='Q_Gas', bus='Gas', size=1000, effects_per_flow_hour={'costs': 0.04}) + ) + + +# ============================================================================ +# RECREATED FIXTURES USING HIERARCHICAL LIBRARY +# ============================================================================ @pytest.fixture @@ -53,72 +357,26 @@ def simple_flow_system() -> fx.FlowSystem: """ Create a simple energy system for testing """ - base_thermal_load = np.array([30.0, 0.0, 90.0, 110, 110, 20, 20, 20, 20]) - base_electrical_price = 1 / 1000 * np.array([80.0, 80.0, 80.0, 80, 80, 80, 80, 80, 80]) + base_thermal_load = LoadProfiles.thermal_simple() + base_electrical_price = LoadProfiles.electrical_simple() base_timesteps = pd.date_range('2020-01-01', periods=9, freq='h', name='time') + # Define effects - costs = fx.Effect('costs', '€', 'Kosten', is_standard=True, is_objective=True) - co2 = fx.Effect( - 'CO2', - 'kg', - 'CO2_e-Emissionen', - specific_share_to_other_effects_operation={costs.label: 0.2}, - maximum_operation_per_hour=1000, - ) + costs = Effects.costs() + co2 = Effects.co2_with_costs_share() + co2.maximum_operation_per_hour = 1000 # Create components - boiler = fx.linear_converters.Boiler( - 'Boiler', - eta=0.5, - Q_th=fx.Flow( - 'Q_th', - bus='Fernwärme', - size=50, - relative_minimum=5 / 50, - relative_maximum=1, - on_off_parameters=fx.OnOffParameters(), - ), - Q_fu=fx.Flow('Q_fu', bus='Gas'), - ) - - chp = fx.linear_converters.CHP( - 'CHP_unit', - eta_th=0.5, - eta_el=0.4, - P_el=fx.Flow('P_el', bus='Strom', size=60, relative_minimum=5 / 60, on_off_parameters=fx.OnOffParameters()), - Q_th=fx.Flow('Q_th', bus='Fernwärme'), - Q_fu=fx.Flow('Q_fu', bus='Gas'), - ) - - storage = fx.Storage( - 'Speicher', - charging=fx.Flow('Q_th_load', bus='Fernwärme', size=1e4), - discharging=fx.Flow('Q_th_unload', bus='Fernwärme', size=1e4), - capacity_in_flow_hours=fx.InvestParameters(fix_effects=20, fixed_size=30, optional=False), - initial_charge_state=0, - relative_maximum_charge_state=1 / 100 * np.array([80.0, 70.0, 80.0, 80, 80, 80, 80, 80, 80]), - relative_maximum_final_charge_state=0.8, - eta_charge=0.9, - eta_discharge=1, - relative_loss_per_hour=0.08, - prevent_simultaneous_charge_and_discharge=True, - ) - - heat_load = fx.Sink( - 'Wärmelast', sink=fx.Flow('Q_th_Last', bus='Fernwärme', size=1, fixed_relative_profile=base_thermal_load) - ) - - gas_tariff = fx.Source( - 'Gastarif', source=fx.Flow('Q_Gas', bus='Gas', size=1000, effects_per_flow_hour={'costs': 0.04, 'CO2': 0.3}) - ) - - electricity_feed_in = fx.Sink( - 'Einspeisung', sink=fx.Flow('P_el', bus='Strom', effects_per_flow_hour=-1 * base_electrical_price) - ) + boiler = Converters.Boilers.simple() + chp = Converters.CHPs.simple() + storage = Storage.simple() + heat_load = Sinks.heat_load(base_thermal_load) + gas_tariff = Sources.gas_with_costs_and_co2() + electricity_feed_in = Sinks.electricity_feed_in(base_electrical_price) # Create flow system flow_system = fx.FlowSystem(base_timesteps) - flow_system.add_elements(fx.Bus('Strom'), fx.Bus('Fernwärme'), fx.Bus('Gas')) + flow_system.add_elements(*Buses.defaults()) flow_system.add_elements(storage, costs, co2, boiler, heat_load, gas_tariff, electricity_feed_in, chp) return flow_system @@ -129,74 +387,28 @@ def simple_flow_system_scenarios() -> fx.FlowSystem: """ Create a simple energy system for testing """ - base_thermal_load = np.array([30.0, 0.0, 90.0, 110, 110, 20, 20, 20, 20]) - base_electrical_price = np.array([0.08, 0.1, 0.15]) + base_thermal_load = LoadProfiles.thermal_simple() + base_electrical_price = LoadProfiles.electrical_scenario() base_timesteps = pd.date_range('2020-01-01', periods=9, freq='h', name='time') + # Define effects - costs = fx.Effect('costs', '€', 'Kosten', is_standard=True, is_objective=True) - co2 = fx.Effect( - 'CO2', - 'kg', - 'CO2_e-Emissionen', - specific_share_to_other_effects_operation={costs.label: 0.2}, - maximum_operation_per_hour=1000, - ) + costs = Effects.costs() + co2 = Effects.co2_with_costs_share() + co2.maximum_operation_per_hour = 1000 # Create components - boiler = fx.linear_converters.Boiler( - 'Boiler', - eta=0.5, - Q_th=fx.Flow( - 'Q_th', - bus='Fernwärme', - size=50, - relative_minimum=5 / 50, - relative_maximum=1, - on_off_parameters=fx.OnOffParameters(), - ), - Q_fu=fx.Flow('Q_fu', bus='Gas'), - ) - - chp = fx.linear_converters.CHP( - 'CHP_unit', - eta_th=0.5, - eta_el=0.4, - P_el=fx.Flow('P_el', bus='Strom', size=60, relative_minimum=5 / 60, on_off_parameters=fx.OnOffParameters()), - Q_th=fx.Flow('Q_th', bus='Fernwärme'), - Q_fu=fx.Flow('Q_fu', bus='Gas'), - ) - - storage = fx.Storage( - 'Speicher', - charging=fx.Flow('Q_th_load', bus='Fernwärme', size=1e4), - discharging=fx.Flow('Q_th_unload', bus='Fernwärme', size=1e4), - capacity_in_flow_hours=fx.InvestParameters(fix_effects=20, fixed_size=30, optional=False), - initial_charge_state=0, - relative_maximum_charge_state=1 / 100 * np.array([80.0, 70.0, 80.0, 80, 80, 80, 80, 80, 80]), - relative_maximum_final_charge_state=0.8, - eta_charge=0.9, - eta_discharge=1, - relative_loss_per_hour=0.08, - prevent_simultaneous_charge_and_discharge=True, - ) - - heat_load = fx.Sink( - 'Wärmelast', sink=fx.Flow('Q_th_Last', bus='Fernwärme', size=1, fixed_relative_profile=base_thermal_load) - ) - - gas_tariff = fx.Source( - 'Gastarif', source=fx.Flow('Q_Gas', bus='Gas', size=1000, effects_per_flow_hour={'costs': 0.04, 'CO2': 0.3}) - ) - - electricity_feed_in = fx.Sink( - 'Einspeisung', sink=fx.Flow('P_el', bus='Strom', effects_per_flow_hour=-1 * base_electrical_price) - ) + boiler = Converters.Boilers.simple() + chp = Converters.CHPs.simple() + storage = Storage.simple() + heat_load = Sinks.heat_load(base_thermal_load) + gas_tariff = Sources.gas_with_costs_and_co2() + electricity_feed_in = Sinks.electricity_feed_in(base_electrical_price) # Create flow system flow_system = fx.FlowSystem( base_timesteps, scenarios=pd.Index(['A', 'B', 'C']), weights=np.array([0.5, 0.25, 0.25]) ) - flow_system.add_elements(fx.Bus('Strom'), fx.Bus('Fernwärme'), fx.Bus('Gas')) + flow_system.add_elements(*Buses.defaults()) flow_system.add_elements(storage, costs, co2, boiler, heat_load, gas_tariff, electricity_feed_in, chp) return flow_system @@ -206,18 +418,17 @@ def simple_flow_system_scenarios() -> fx.FlowSystem: def basic_flow_system() -> fx.FlowSystem: """Create basic elements for component testing""" flow_system = fx.FlowSystem(pd.date_range('2020-01-01', periods=10, freq='h', name='time')) - thermal_load = np.array([np.random.random() for _ in range(10)]) * 180 - p_el = (np.array([np.random.random() for _ in range(10)]) + 0.5) / 1.5 * 50 - flow_system.add_elements( - fx.Bus('Strom'), - fx.Bus('Fernwärme'), - fx.Bus('Gas'), - fx.Effect('Costs', '€', 'Kosten', is_standard=True, is_objective=True), - fx.Sink('Wärmelast', sink=fx.Flow('Q_th_Last', 'Fernwärme', size=1, fixed_relative_profile=thermal_load)), - fx.Source('Gastarif', source=fx.Flow('Q_Gas', 'Gas', size=1000, effects_per_flow_hour=0.04)), - fx.Sink('Einspeisung', sink=fx.Flow('P_el', 'Strom', effects_per_flow_hour=-1 * p_el)), - ) + thermal_load = LoadProfiles.random_thermal(10) + p_el = LoadProfiles.random_electrical(10) + + costs = Effects.costs() + heat_load = Sinks.heat_load(thermal_load) + gas_source = Sources.gas_with_costs() + electricity_sink = Sinks.electricity_feed_in(p_el) + + flow_system.add_elements(*Buses.defaults()) + flow_system.add_elements(costs, heat_load, gas_source, electricity_sink) return flow_system @@ -227,79 +438,26 @@ def flow_system_complex() -> fx.FlowSystem: """ Helper method to create a base model with configurable parameters """ - thermal_load = np.array([30, 0, 90, 110, 110, 20, 20, 20, 20]) - electrical_load = np.array([40, 40, 40, 40, 40, 40, 40, 40, 40]) + thermal_load = LoadProfiles.thermal_complex() + electrical_load = LoadProfiles.electrical_complex() flow_system = fx.FlowSystem(pd.date_range('2020-01-01', periods=9, freq='h', name='time')) + # Define the components and flow_system - flow_system.add_elements( - fx.Effect('costs', '€', 'Kosten', is_standard=True, is_objective=True), - fx.Effect('CO2', 'kg', 'CO2_e-Emissionen', specific_share_to_other_effects_operation={'costs': 0.2}), - fx.Effect('PE', 'kWh_PE', 'Primärenergie', maximum_total=3.5e3), - fx.Bus('Strom'), - fx.Bus('Fernwärme'), - fx.Bus('Gas'), - fx.Sink('Wärmelast', sink=fx.Flow('Q_th_Last', 'Fernwärme', size=1, fixed_relative_profile=thermal_load)), - fx.Source( - 'Gastarif', source=fx.Flow('Q_Gas', 'Gas', size=1000, effects_per_flow_hour={'costs': 0.04, 'CO2': 0.3}) - ), - fx.Sink('Einspeisung', sink=fx.Flow('P_el', 'Strom', effects_per_flow_hour=-1 * electrical_load)), - ) + costs = Effects.costs() + co2 = Effects.co2() + co2.specific_share_to_other_effects_operation = {'costs': 0.2} + pe = Effects.primary_energy() + pe.maximum_total = 3.5e3 - boiler = fx.linear_converters.Boiler( - 'Kessel', - eta=0.5, - on_off_parameters=fx.OnOffParameters(effects_per_running_hour={'costs': 0, 'CO2': 1000}), - Q_th=fx.Flow( - 'Q_th', - bus='Fernwärme', - load_factor_max=1.0, - load_factor_min=0.1, - relative_minimum=5 / 50, - relative_maximum=1, - previous_flow_rate=50, - size=fx.InvestParameters( - fix_effects=1000, fixed_size=50, optional=False, specific_effects={'costs': 10, 'PE': 2} - ), - on_off_parameters=fx.OnOffParameters( - on_hours_total_min=0, - on_hours_total_max=1000, - consecutive_on_hours_max=10, - consecutive_on_hours_min=1, - consecutive_off_hours_max=10, - effects_per_switch_on=0.01, - switch_on_total_max=1000, - ), - flow_hours_total_max=1e6, - ), - Q_fu=fx.Flow('Q_fu', bus='Gas', size=200, relative_minimum=0, relative_maximum=1), - ) + heat_load = Sinks.heat_load(thermal_load) + gas_tariff = Sources.gas_with_costs_and_co2() + electricity_feed_in = Sinks.electricity_feed_in(electrical_load) - invest_speicher = fx.InvestParameters( - fix_effects=0, - piecewise_effects=fx.PiecewiseEffects( - piecewise_origin=fx.Piecewise([fx.Piece(5, 25), fx.Piece(25, 100)]), - piecewise_shares={ - 'costs': fx.Piecewise([fx.Piece(50, 250), fx.Piece(250, 800)]), - 'PE': fx.Piecewise([fx.Piece(5, 25), fx.Piece(25, 100)]), - }, - ), - optional=False, - specific_effects={'costs': 0.01, 'CO2': 0.01}, - minimum_size=0, - maximum_size=1000, - ) - speicher = fx.Storage( - 'Speicher', - charging=fx.Flow('Q_th_load', bus='Fernwärme', size=1e4), - discharging=fx.Flow('Q_th_unload', bus='Fernwärme', size=1e4), - capacity_in_flow_hours=invest_speicher, - initial_charge_state=0, - maximal_final_charge_state=10, - eta_charge=0.9, - eta_discharge=1, - relative_loss_per_hour=0.08, - prevent_simultaneous_charge_and_discharge=True, - ) + flow_system.add_elements(*Buses.defaults()) + flow_system.add_elements(costs, co2, pe, heat_load, gas_tariff, electricity_feed_in) + + boiler = Converters.Boilers.complex() + speicher = Storage.complex() flow_system.add_elements(boiler, speicher) @@ -312,45 +470,16 @@ def flow_system_base(flow_system_complex) -> fx.FlowSystem: Helper method to create a base model with configurable parameters """ flow_system = flow_system_complex - - flow_system.add_elements( - fx.linear_converters.CHP( - 'KWK', - eta_th=0.5, - eta_el=0.4, - on_off_parameters=fx.OnOffParameters(effects_per_switch_on=0.01), - P_el=fx.Flow('P_el', bus='Strom', size=60, relative_minimum=5 / 60, previous_flow_rate=10), - Q_th=fx.Flow('Q_th', bus='Fernwärme', size=1e3), - Q_fu=fx.Flow('Q_fu', bus='Gas', size=1e3), - ) - ) - + chp = Converters.CHPs.base() + flow_system.add_elements(chp) return flow_system @pytest.fixture def flow_system_piecewise_conversion(flow_system_complex) -> fx.FlowSystem: flow_system = flow_system_complex - - flow_system.add_elements( - fx.LinearConverter( - 'KWK', - inputs=[fx.Flow('Q_fu', bus='Gas')], - outputs=[ - fx.Flow('P_el', bus='Strom', size=60, relative_maximum=55, previous_flow_rate=10), - fx.Flow('Q_th', bus='Fernwärme'), - ], - piecewise_conversion=fx.PiecewiseConversion( - { - 'P_el': fx.Piecewise([fx.Piece(5, 30), fx.Piece(40, 60)]), - 'Q_th': fx.Piecewise([fx.Piece(6, 35), fx.Piece(45, 100)]), - 'Q_fu': fx.Piecewise([fx.Piece(12, 70), fx.Piece(90, 200)]), - } - ), - on_off_parameters=fx.OnOffParameters(effects_per_switch_on=0.01), - ) - ) - + converter = Converters.LinearConverters.piecewise() + flow_system.add_elements(converter) return flow_system @@ -360,38 +489,16 @@ def flow_system_segments_of_flows_2(flow_system_complex) -> fx.FlowSystem: Use segments/Piecewise with numeric data """ flow_system = flow_system_complex - - flow_system.add_elements( - fx.LinearConverter( - 'KWK', - inputs=[fx.Flow('Q_fu', bus='Gas')], - outputs=[ - fx.Flow('P_el', bus='Strom', size=60, relative_maximum=55, previous_flow_rate=10), - fx.Flow('Q_th', bus='Fernwärme'), - ], - piecewise_conversion=fx.PiecewiseConversion( - { - 'P_el': fx.Piecewise( - [ - fx.Piece(np.linspace(5, 6, len(flow_system.timesteps)), 30), - fx.Piece(40, np.linspace(60, 70, len(flow_system.timesteps))), - ] - ), - 'Q_th': fx.Piecewise([fx.Piece(6, 35), fx.Piece(45, 100)]), - 'Q_fu': fx.Piecewise([fx.Piece(12, 70), fx.Piece(90, 200)]), - } - ), - on_off_parameters=fx.OnOffParameters(effects_per_switch_on=0.01), - ) - ) - + converter = Converters.LinearConverters.segments(len(flow_system.timesteps)) + flow_system.add_elements(converter) return flow_system @pytest.fixture def flow_system_long(): """ - Fixture to create and return the flow system with loaded data + Special fixture with CSV data loading - kept separate for backward compatibility + Uses library components where possible, but has special elements inline """ # Load data filename = os.path.join(os.path.dirname(__file__), 'ressources', 'Zeitreihen2020.csv') @@ -404,25 +511,22 @@ def flow_system_long(): p_el = data['Strompr.€/MWh'].values gas_price = data['Gaspr.€/MWh'].values - flow_system = fx.FlowSystem(pd.DatetimeIndex(data.index)) - thermal_load_ts, electrical_load_ts = ( - fx.TimeSeriesData(thermal_load, coords={'time': flow_system.timesteps}), - fx.TimeSeriesData(electrical_load, aggregation_weight=0.7, coords={'time': flow_system.timesteps}), + fx.TimeSeriesData(thermal_load), + fx.TimeSeriesData(electrical_load, aggregation_weight=0.7), ) p_feed_in, p_sell = ( - fx.TimeSeriesData(-(p_el - 0.5), aggregation_group='p_el', coords={'time': flow_system.timesteps}), - fx.TimeSeriesData(p_el + 0.5, aggregation_group='p_el', coords={'time': flow_system.timesteps}), + fx.TimeSeriesData(-(p_el - 0.5), aggregation_group='p_el'), + fx.TimeSeriesData(p_el + 0.5, aggregation_group='p_el'), ) + flow_system = fx.FlowSystem(pd.DatetimeIndex(data.index)) flow_system.add_elements( - fx.Bus('Strom'), - fx.Bus('Fernwärme'), - fx.Bus('Gas'), - fx.Bus('Kohle'), - fx.Effect('costs', '€', 'Kosten', is_standard=True, is_objective=True), - fx.Effect('CO2', 'kg', 'CO2_e-Emissionen'), - fx.Effect('PE', 'kWh_PE', 'Primärenergie'), + *Buses.defaults(), + Buses.coal(), + Effects.costs(), + Effects.co2(), + Effects.primary_energy(), fx.Sink( 'Wärmelast', sink=fx.Flow('Q_th_Last', bus='Fernwärme', size=1, fixed_relative_profile=thermal_load_ts) ), @@ -487,6 +591,51 @@ def flow_system_long(): } +@pytest.fixture(params=['h', '3h']) +def timesteps_linopy(request): + return pd.date_range('2020-01-01', periods=10, freq=request.param, name='time') + + +@pytest.fixture +def basic_flow_system_linopy(timesteps_linopy) -> fx.FlowSystem: + """Create basic elements for component testing""" + flow_system = fx.FlowSystem(timesteps_linopy) + + thermal_load = LoadProfiles.random_thermal(10) + p_el = LoadProfiles.random_electrical(10) + + costs = Effects.costs() + heat_load = Sinks.heat_load(thermal_load) + gas_source = Sources.gas_with_costs() + electricity_sink = Sinks.electricity_feed_in(p_el) + + flow_system.add_elements(*Buses.defaults()) + flow_system.add_elements(costs, heat_load, gas_source, electricity_sink) + + return flow_system + + +# ============================================================================ +# UTILITY FUNCTIONS (kept for backward compatibility) +# ============================================================================ + + +# Custom assertion function +def assert_almost_equal_numeric( + actual, desired, err_msg, relative_error_range_in_percent=0.011, absolute_tolerance=1e-7 +): + """ + Custom assertion function for comparing numeric values with relative and absolute tolerances + """ + relative_tol = relative_error_range_in_percent / 100 + + if isinstance(desired, (int, float)): + delta = abs(relative_tol * desired) if desired != 0 else absolute_tolerance + assert np.isclose(actual, desired, atol=delta), err_msg + else: + np.testing.assert_allclose(actual, desired, rtol=relative_tol, atol=absolute_tolerance, err_msg=err_msg) + + def create_calculation_and_solve( flow_system: fx.FlowSystem, solver, name: str, allow_infeasible: bool = False ) -> fx.FullCalculation: @@ -508,31 +657,6 @@ def create_linopy_model(flow_system: fx.FlowSystem) -> FlowSystemModel: return calculation.model -@pytest.fixture(params=['h', '3h']) -def timesteps_linopy(request): - return pd.date_range('2020-01-01', periods=10, freq=request.param, name='time') - - -@pytest.fixture -def basic_flow_system_linopy(timesteps_linopy) -> fx.FlowSystem: - """Create basic elements for component testing""" - flow_system = fx.FlowSystem(timesteps_linopy) - thermal_load = np.array([np.random.random() for _ in range(10)]) * 180 - p_el = (np.array([np.random.random() for _ in range(10)]) + 0.5) / 1.5 * 50 - - flow_system.add_elements( - fx.Bus('Strom'), - fx.Bus('Fernwärme'), - fx.Bus('Gas'), - fx.Effect('Costs', '€', 'Kosten', is_standard=True, is_objective=True), - fx.Sink('Wärmelast', sink=fx.Flow('Q_th_Last', 'Fernwärme', size=1, fixed_relative_profile=thermal_load)), - fx.Source('Gastarif', source=fx.Flow('Q_Gas', 'Gas', size=1000, effects_per_flow_hour=0.04)), - fx.Sink('Einspeisung', sink=fx.Flow('P_el', 'Strom', effects_per_flow_hour=-1 * p_el)), - ) - - return flow_system - - def assert_conequal(actual: linopy.Constraint, desired: linopy.Constraint): """Assert that two constraints are equal with detailed error messages.""" name = actual.name diff --git a/tests/test_effect.py b/tests/test_effect.py index e2807cc89..13e878041 100644 --- a/tests/test_effect.py +++ b/tests/test_effect.py @@ -204,7 +204,7 @@ def test_shares(self, basic_flow_system_linopy): class TestEffectResults: def test_shares(self, basic_flow_system_linopy): flow_system = basic_flow_system_linopy - flow_system.effects['Costs'].specific_share_to_other_effects_operation['Effect1'] = 0.5 + flow_system.effects['costs'].specific_share_to_other_effects_operation['Effect1'] = 0.5 flow_system.add_elements( fx.Effect( 'Effect1', @@ -230,9 +230,9 @@ def test_shares(self, basic_flow_system_linopy): effect_share_factors = { 'operation': { - ('Costs', 'Effect1'): 0.5, - ('Costs', 'Effect2'): 0.5 * 1.1, - ('Costs', 'Effect3'): 0.5 * 1.1 * 5 + 0.5 * 1.2, # This is where the issue lies + ('costs', 'Effect1'): 0.5, + ('costs', 'Effect2'): 0.5 * 1.1, + ('costs', 'Effect3'): 0.5 * 1.1 * 5 + 0.5 * 1.2, # This is where the issue lies ('Effect1', 'Effect2'): 1.1, ('Effect1', 'Effect3'): 1.2 + 1.1 * 5, ('Effect2', 'Effect3'): 5, @@ -249,8 +249,8 @@ def test_shares(self, basic_flow_system_linopy): np.testing.assert_allclose(results.effect_share_factors['invest'][key].values, value) xr.testing.assert_allclose( - results.effects_per_component('operation').sum('component')['Costs'], - results.solution['Costs(operation)|total_per_timestep'].fillna(0), + results.effects_per_component('operation').sum('component')['costs'], + results.solution['costs(operation)|total_per_timestep'].fillna(0), ) xr.testing.assert_allclose( @@ -270,7 +270,7 @@ def test_shares(self, basic_flow_system_linopy): # Invest mode checks xr.testing.assert_allclose( - results.effects_per_component('invest').sum('component')['Costs'], results.solution['Costs(invest)|total'] + results.effects_per_component('invest').sum('component')['costs'], results.solution['costs(invest)|total'] ) xr.testing.assert_allclose( @@ -290,7 +290,7 @@ def test_shares(self, basic_flow_system_linopy): # Total mode checks xr.testing.assert_allclose( - results.effects_per_component('total').sum('component')['Costs'], results.solution['Costs|total'] + results.effects_per_component('total').sum('component')['costs'], results.solution['costs|total'] ) xr.testing.assert_allclose( diff --git a/tests/test_effects_shares_summation.py b/tests/test_effects_shares_summation.py index d4d22d6df..15de93481 100644 --- a/tests/test_effects_shares_summation.py +++ b/tests/test_effects_shares_summation.py @@ -99,7 +99,7 @@ def test_effect_shares_example(): """Test the specific example from the effects share factors test.""" # Create the conversion dictionary based on test example conversion_dict = { - 'Costs': {'Effect1': xr.DataArray(0.5)}, + 'costs': {'Effect1': xr.DataArray(0.5)}, 'Effect1': {'Effect2': xr.DataArray(1.1), 'Effect3': xr.DataArray(1.2)}, 'Effect2': {'Effect3': xr.DataArray(5.0)}, } @@ -107,19 +107,19 @@ def test_effect_shares_example(): result = calculate_all_conversion_paths(conversion_dict) # Test direct paths - assert result[('Costs', 'Effect1')].item() == 0.5 + assert result[('costs', 'Effect1')].item() == 0.5 assert result[('Effect1', 'Effect2')].item() == 1.1 assert result[('Effect2', 'Effect3')].item() == 5.0 # Test indirect paths - # Costs -> Effect2 = Costs -> Effect1 -> Effect2 = 0.5 * 1.1 - assert result[('Costs', 'Effect2')].item() == 0.5 * 1.1 + # costs -> Effect2 = costs -> Effect1 -> Effect2 = 0.5 * 1.1 + assert result[('costs', 'Effect2')].item() == 0.5 * 1.1 - # Costs -> Effect3 has two paths: - # 1. Costs -> Effect1 -> Effect3 = 0.5 * 1.2 = 0.6 - # 2. Costs -> Effect1 -> Effect2 -> Effect3 = 0.5 * 1.1 * 5 = 2.75 + # costs -> Effect3 has two paths: + # 1. costs -> Effect1 -> Effect3 = 0.5 * 1.2 = 0.6 + # 2. costs -> Effect1 -> Effect2 -> Effect3 = 0.5 * 1.1 * 5 = 2.75 # Total = 0.6 + 2.75 = 3.35 - assert result[('Costs', 'Effect3')].item() == 0.5 * 1.2 + 0.5 * 1.1 * 5 + assert result[('costs', 'Effect3')].item() == 0.5 * 1.2 + 0.5 * 1.1 * 5 # Effect1 -> Effect3 has two paths: # 1. Effect1 -> Effect2 -> Effect3 = 1.1 * 5.0 = 5.5 diff --git a/tests/test_flow.py b/tests/test_flow.py index 93658bf2e..2ee609f68 100644 --- a/tests/test_flow.py +++ b/tests/test_flow.py @@ -101,11 +101,11 @@ def test_effects_per_flow_hour(self, basic_flow_system_linopy): co2_per_flow_hour = xr.DataArray(np.linspace(4, 5, timesteps.size), coords=(timesteps,)) flow = fx.Flow( - 'Wärme', bus='Fernwärme', effects_per_flow_hour={'Costs': costs_per_flow_hour, 'CO2': co2_per_flow_hour} + 'Wärme', bus='Fernwärme', effects_per_flow_hour={'costs': costs_per_flow_hour, 'CO2': co2_per_flow_hour} ) flow_system.add_elements(fx.Sink('Sink', sink=flow), fx.Effect('CO2', 't', '')) model = create_linopy_model(flow_system) - costs, co2 = flow_system.effects['Costs'], flow_system.effects['CO2'] + costs, co2 = flow_system.effects['costs'], flow_system.effects['CO2'] assert_sets_equal( set(flow.submodel.variables), @@ -114,12 +114,12 @@ def test_effects_per_flow_hour(self, basic_flow_system_linopy): ) assert_sets_equal(set(flow.submodel.constraints), {'Sink(Wärme)|total_flow_hours'}, msg='Incorrect constraints') - assert 'Sink(Wärme)->Costs(operation)' in set(costs.submodel.constraints) + assert 'Sink(Wärme)->costs(operation)' in set(costs.submodel.constraints) assert 'Sink(Wärme)->CO2(operation)' in set(co2.submodel.constraints) assert_conequal( - model.constraints['Sink(Wärme)->Costs(operation)'], - model.variables['Sink(Wärme)->Costs(operation)'] + model.constraints['Sink(Wärme)->costs(operation)'], + model.variables['Sink(Wärme)->costs(operation)'] == flow.submodel.variables['Sink(Wärme)|flow_rate'] * model.hours_per_step * costs_per_flow_hour, ) @@ -426,8 +426,8 @@ def test_flow_invest_with_effects(self, basic_flow_system_linopy): minimum_size=20, maximum_size=100, optional=True, - fix_effects={'Costs': 1000, 'CO2': 5}, # Fixed investment effects - specific_effects={'Costs': 500, 'CO2': 0.1}, # Specific investment effects + fix_effects={'costs': 1000, 'CO2': 5}, # Fixed investment effects + specific_effects={'costs': 500, 'CO2': 0.1}, # Specific investment effects ), ) @@ -435,13 +435,13 @@ def test_flow_invest_with_effects(self, basic_flow_system_linopy): model = create_linopy_model(flow_system) # Check investment effects - assert 'Sink(Wärme)->Costs(invest)' in model.variables + assert 'Sink(Wärme)->costs(invest)' in model.variables assert 'Sink(Wärme)->CO2(invest)' in model.variables # Check fix effects (applied only when is_invested=1) assert_conequal( - model.constraints['Sink(Wärme)->Costs(invest)'], - model.variables['Sink(Wärme)->Costs(invest)'] + model.constraints['Sink(Wärme)->costs(invest)'], + model.variables['Sink(Wärme)->costs(invest)'] == flow.submodel.variables['Sink(Wärme)|is_invested'] * 1000 + flow.submodel.variables['Sink(Wärme)|size'] * 500, ) @@ -464,7 +464,7 @@ def test_flow_invest_divest_effects(self, basic_flow_system_linopy): minimum_size=20, maximum_size=100, optional=True, - divest_effects={'Costs': 500}, # Cost incurred when NOT investing + divest_effects={'costs': 500}, # Cost incurred when NOT investing ), ) @@ -472,11 +472,11 @@ def test_flow_invest_divest_effects(self, basic_flow_system_linopy): model = create_linopy_model(flow_system) # Check divestment effects - assert 'Sink(Wärme)->Costs(invest)' in model.constraints + assert 'Sink(Wärme)->costs(invest)' in model.constraints assert_conequal( - model.constraints['Sink(Wärme)->Costs(invest)'], - model.variables['Sink(Wärme)->Costs(invest)'] + (model.variables['Sink(Wärme)|is_invested'] - 1) * 500 == 0, + model.constraints['Sink(Wärme)->costs(invest)'], + model.variables['Sink(Wärme)->costs(invest)'] + (model.variables['Sink(Wärme)|is_invested'] - 1) * 500 == 0, ) @@ -558,12 +558,12 @@ def test_effects_per_running_hour(self, basic_flow_system_linopy): 'Wärme', bus='Fernwärme', on_off_parameters=fx.OnOffParameters( - effects_per_running_hour={'Costs': costs_per_running_hour, 'CO2': co2_per_running_hour} + effects_per_running_hour={'costs': costs_per_running_hour, 'CO2': co2_per_running_hour} ), ) flow_system.add_elements(fx.Sink('Sink', sink=flow), fx.Effect('CO2', 't', '')) model = create_linopy_model(flow_system) - costs, co2 = flow_system.effects['Costs'], flow_system.effects['CO2'] + costs, co2 = flow_system.effects['costs'], flow_system.effects['CO2'] assert_sets_equal( set(flow.submodel.variables), @@ -586,12 +586,12 @@ def test_effects_per_running_hour(self, basic_flow_system_linopy): msg='Incorrect constraints', ) - assert 'Sink(Wärme)->Costs(operation)' in set(costs.submodel.constraints) + assert 'Sink(Wärme)->costs(operation)' in set(costs.submodel.constraints) assert 'Sink(Wärme)->CO2(operation)' in set(co2.submodel.constraints) assert_conequal( - model.constraints['Sink(Wärme)->Costs(operation)'], - model.variables['Sink(Wärme)->Costs(operation)'] + model.constraints['Sink(Wärme)->costs(operation)'], + model.variables['Sink(Wärme)->costs(operation)'] == flow.submodel.variables['Sink(Wärme)|on'] * model.hours_per_step * costs_per_running_hour, ) @@ -943,7 +943,7 @@ def test_switch_on_constraints(self, basic_flow_system_linopy): size=100, on_off_parameters=fx.OnOffParameters( switch_on_total_max=5, # Maximum 5 startups - effects_per_switch_on={'Costs': 100}, # 100 EUR startup cost + effects_per_switch_on={'costs': 100}, # 100 EUR startup cost ), ) @@ -984,12 +984,12 @@ def test_switch_on_constraints(self, basic_flow_system_linopy): ) # Check that startup cost effect constraint exists - assert 'Sink(Wärme)->Costs(operation)' in model.constraints + assert 'Sink(Wärme)->costs(operation)' in model.constraints # Verify the startup cost effect constraint assert_conequal( - model.constraints['Sink(Wärme)->Costs(operation)'], - model.variables['Sink(Wärme)->Costs(operation)'] == flow.submodel.variables['Sink(Wärme)|switch|on'] * 100, + model.constraints['Sink(Wärme)->costs(operation)'], + model.variables['Sink(Wärme)->costs(operation)'] == flow.submodel.variables['Sink(Wärme)|switch|on'] * 100, ) def test_on_hours_limits(self, basic_flow_system_linopy): diff --git a/tests/test_integration.py b/tests/test_integration.py index babc7b131..97876c251 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -248,7 +248,7 @@ def test_piecewise_conversion(self, flow_system_piecewise_conversion, highs_solv assert_almost_equal_numeric( comps['Speicher'].submodel.variables['Speicher|PiecewiseEffects|costs'].solution.values, 454.74666666666667, - 'Speicher investCosts_segmented_costs doesnt match expected value', + 'Speicher investcosts_segmented_costs doesnt match expected value', ) @@ -309,13 +309,13 @@ def test_modeling_types_costs(self, modeling_calculation): assert_almost_equal_numeric( calc.results.model['costs|total'].solution.item(), expected_costs[modeling_type], - f'Costs do not match for {modeling_type} modeling type', + f'costs do not match for {modeling_type} modeling type', ) else: assert_almost_equal_numeric( calc.results.solution_without_overlap('costs(operation)|total_per_timestep').sum(), expected_costs[modeling_type], - f'Costs do not match for {modeling_type} modeling type', + f'costs do not match for {modeling_type} modeling type', ) def test_segmented_io(self, modeling_calculation): diff --git a/tests/test_linear_converter.py b/tests/test_linear_converter.py index 42dc80077..322a5f6f0 100644 --- a/tests/test_linear_converter.py +++ b/tests/test_linear_converter.py @@ -146,7 +146,7 @@ def test_linear_converter_with_on_off(self, basic_flow_system_linopy): # Create OnOffParameters on_off_params = fx.OnOffParameters( - on_hours_total_min=10, on_hours_total_max=40, effects_per_running_hour={'Costs': 5} + on_hours_total_min=10, on_hours_total_max=40, effects_per_running_hour={'costs': 5} ) # Create a linear converter with OnOffParameters @@ -186,10 +186,10 @@ def test_linear_converter_with_on_off(self, basic_flow_system_linopy): ) # Check on_off effects - assert 'Converter->Costs(operation)' in model.constraints + assert 'Converter->costs(operation)' in model.constraints assert_conequal( - model.constraints['Converter->Costs(operation)'], - model.variables['Converter->Costs(operation)'] + model.constraints['Converter->costs(operation)'], + model.variables['Converter->costs(operation)'] == model.variables['Converter|on'] * model.hours_per_step * 5, ) @@ -398,7 +398,7 @@ def test_piecewise_conversion_with_onoff(self, basic_flow_system_linopy): # Create OnOffParameters on_off_params = fx.OnOffParameters( - on_hours_total_min=10, on_hours_total_max=40, effects_per_running_hour={'Costs': 5} + on_hours_total_min=10, on_hours_total_max=40, effects_per_running_hour={'costs': 5} ) # Create a linear converter with piecewise conversion and on/off parameters @@ -489,10 +489,10 @@ def test_piecewise_conversion_with_onoff(self, basic_flow_system_linopy): ) # Verify that the costs effect is applied - assert 'Converter->Costs(operation)' in model.constraints + assert 'Converter->costs(operation)' in model.constraints assert_conequal( - model.constraints['Converter->Costs(operation)'], - model.variables['Converter->Costs(operation)'] + model.constraints['Converter->costs(operation)'], + model.variables['Converter->costs(operation)'] == model.variables['Converter|on'] * model.hours_per_step * 5, ) diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py index 0ccc1a5dd..897122242 100644 --- a/tests/test_scenarios.py +++ b/tests/test_scenarios.py @@ -67,9 +67,9 @@ def test_system(): size=InvestParameters( minimum_size=0, maximum_size=20, - specific_effects={'Costs': 100}, # €/kW + specific_effects={'costs': 100}, # €/kW ), - effects_per_flow_hour={'Costs': 20}, # €/MWh + effects_per_flow_hour={'costs': 20}, # €/MWh ) generator = Source('Generator', source=power_gen) @@ -83,7 +83,7 @@ def test_system(): capacity_in_flow_hours=InvestParameters( minimum_size=0, maximum_size=50, - specific_effects={'Costs': 50}, # €/kWh + specific_effects={'costs': 50}, # €/kWh ), eta_charge=0.95, eta_discharge=0.95, @@ -91,7 +91,7 @@ def test_system(): ) # Create effects and objective - cost_effect = Effect(label='Costs', unit='€', description='Total costs', is_standard=True, is_objective=True) + cost_effect = Effect(label='costs', unit='€', description='Total costs', is_standard=True, is_objective=True) # Add all elements to the flow system flow_system.add_elements(electricity_bus, generator, demand_sink, storage, cost_effect) From b7734f8f2241b661d73d6b12cc1b42e106460788 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 23 Jul 2025 18:08:58 +0200 Subject: [PATCH 263/448] Improve some of the modeling and coord handling --- flixopt/features.py | 9 ++- flixopt/modeling.py | 131 ++++++++++++++++---------------------------- 2 files changed, 53 insertions(+), 87 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index fc80f0eb3..d07844c8d 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -171,7 +171,7 @@ def _do_modeling(self): self.parameters.on_hours_total_max if self.parameters.on_hours_total_max is not None else np.inf, ), # TODO: self._model.hours_per_step.sum('time').item() + self._get_previous_on_duration()) short_name='on_hours_total', - coords=self.get_coords(['year', 'scenario']), + coords=['year', 'scenario'], ) # 4. Switch tracking using existing pattern @@ -179,13 +179,14 @@ def _do_modeling(self): self.add_variables(binary=True, short_name='switch|on', coords=self.get_coords()) self.add_variables(binary=True, short_name='switch|off', coords=self.get_coords()) - ModelingPrimitives.state_transition_variables( + BoundingPatterns.state_transition_bounds( self, state_variable=self.on, switch_on=self.switch_on, switch_off=self.switch_off, name=f'{self.label_of_model}|switch', previous_state=self._previous_states.isel(time=-1) if self._previous_states is not None else 0, + coord='time', ) if self.parameters.switch_on_total_max is not None: @@ -408,7 +409,9 @@ def _do_modeling(self): rhs = self.zero_point elif self._zero_point is True: self.zero_point = self.add_variables( - coords=self._model.get_coords(), binary=True, short_name='zero_point' + coords=self._model.get_coords(('year', 'scenario') if self._as_time_series else None), + binary=True, + short_name='zero_point', ) rhs = self.zero_point else: diff --git a/flixopt/modeling.py b/flixopt/modeling.py index fa13aeea8..14f8c45f3 100644 --- a/flixopt/modeling.py +++ b/flixopt/modeling.py @@ -189,7 +189,7 @@ def expression_tracking_variable( name: str = None, short_name: str = None, bounds: Tuple[TemporalData, TemporalData] = None, - coords: List[str] = None, + coords: Optional[Union[str, List[str]]] = None, ) -> Tuple[linopy.Variable, linopy.Constraint]: """ Creates variable that equals a given expression. @@ -205,8 +205,6 @@ def expression_tracking_variable( if not isinstance(model, Submodel): raise ValueError('ModelingPrimitives.expression_tracking_variable() can only be used with a Submodel') - coords = coords or ['year', 'scenario'] - if not bounds: tracker = model.add_variables(name=name, coords=model.get_coords(coords), short_name=short_name) else: @@ -223,86 +221,6 @@ def expression_tracking_variable( return tracker, tracking - @staticmethod - def state_transition_variables( - model: Submodel, - state_variable: linopy.Variable, - switch_on: linopy.Variable, - switch_off: linopy.Variable, - name: str, - previous_state=0, - ) -> Tuple[linopy.Constraint, linopy.Constraint, linopy.Constraint]: - """ - Creates switch-on/off variables with state transition logic. - - Mathematical formulation: - switch_on[t] - switch_off[t] = state[t] - state[t-1] ∀t > 0 - switch_on[0] - switch_off[0] = state[0] - previous_state - switch_on[t] + switch_off[t] ≤ 1 ∀t - switch_on[t], switch_off[t] ∈ {0, 1} - - Returns: - variables: {'switch_on': binary_var, 'switch_off': binary_var} - constraints: {'transition': constraint, 'initial': constraint, 'mutex': constraint} - """ - if not isinstance(model, Submodel): - raise ValueError('ModelingPrimitives.state_transition_variables() can only be used with a Submodel') - - # State transition constraints for t > 0 - transition = model.add_constraints( - switch_on.isel(time=slice(1, None)) - switch_off.isel(time=slice(1, None)) - == state_variable.isel(time=slice(1, None)) - state_variable.isel(time=slice(None, -1)), - name=f'{name}|transition', - ) - - # Initial state transition for t = 0 - initial = model.add_constraints( - switch_on.isel(time=0) - switch_off.isel(time=0) == state_variable.isel(time=0) - previous_state, - name=f'{name}|initial', - ) - - # At most one switch per timestep - mutex = model.add_constraints(switch_on + switch_off <= 1, name=f'{name}|mutex') - - return transition, initial, mutex - - @staticmethod - def sum_up_variable( - model: Submodel, - variable_to_count: linopy.Variable, - name: str = None, - bounds: Tuple[NonTemporalData, NonTemporalData] = None, - factor: TemporalData = 1, - ) -> Tuple[linopy.Variable, linopy.Constraint]: - """ - SUms up a variable over time, applying a factor to the variable. - - Args: - model: The optimization model instance - variable_to_count: The variable to be summed up - name: The name of the constraint - bounds: The bounds of the constraint - factor: The factor to be applied to the variable - """ - if not isinstance(model, Submodel): - raise ValueError('ModelingPrimitives.sum_up_variable() can only be used with a Submodel') - - if bounds is None: - bounds = (0, np.inf) - else: - bounds = (bounds[0] if bounds[0] is not None else 0, bounds[1] if bounds[1] is not None else np.inf) - - count = model.add_variables( - lower=bounds[0], - upper=bounds[1], - coords=model.get_coords(['year', 'scenario']), - name=name, - ) - - count_constraint = model.add_constraints(count == (variable_to_count * factor).sum('time'), name=name) - - return count, count_constraint - @staticmethod def consecutive_duration_tracking( model: Submodel, @@ -346,7 +264,7 @@ def consecutive_duration_tracking( duration = model.add_variables( lower=0, upper=maximum_duration if maximum_duration is not None else mega, - coords=model.get_coords(['time']), + coords=model.get_coords(), name=name, short_name=short_name, ) @@ -625,3 +543,48 @@ def scaled_bounds_with_state( binary_lower = model.add_constraints(variable_state * big_m_lower <= variable, name=f'{name}|lb1') return [scaling_lower, scaling_upper, binary_lower, binary_upper] + + @staticmethod + def state_transition_bounds( + model: Submodel, + state_variable: linopy.Variable, + switch_on: linopy.Variable, + switch_off: linopy.Variable, + name: str, + previous_state=0, + coord: str = 'time', + ) -> Tuple[linopy.Constraint, linopy.Constraint, linopy.Constraint]: + """ + Creates switch-on/off variables with state transition logic. + + Mathematical formulation: + switch_on[t] - switch_off[t] = state[t] - state[t-1] ∀t > 0 + switch_on[0] - switch_off[0] = state[0] - previous_state + switch_on[t] + switch_off[t] ≤ 1 ∀t + switch_on[t], switch_off[t] ∈ {0, 1} + + Returns: + variables: {'switch_on': binary_var, 'switch_off': binary_var} + constraints: {'transition': constraint, 'initial': constraint, 'mutex': constraint} + """ + if not isinstance(model, Submodel): + raise ValueError('ModelingPrimitives.state_transition_variables() can only be used with a Submodel') + + # State transition constraints for t > 0 + transition = model.add_constraints( + switch_on.isel({coord: slice(1, None)}) - switch_off.isel({coord: slice(1, None)}) + == state_variable.isel({coord: slice(1, None)}) - state_variable.isel({coord: slice(None, -1)}), + name=f'{name}|transition', + ) + + # Initial state transition for t = 0 + initial = model.add_constraints( + switch_on.isel({coord: 0}) - switch_off.isel({coord: 0}) + == state_variable.isel({coord: 0}) - previous_state, + name=f'{name}|initial', + ) + + # At most one switch per timestep + mutex = model.add_constraints(switch_on + switch_off <= 1, name=f'{name}|mutex') + + return transition, initial, mutex From e06692b473f1c2cfd298efa2e4e335a7780e1c07 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 23 Jul 2025 21:28:23 +0200 Subject: [PATCH 264/448] Add tests with years and scenarios --- tests/conftest.py | 45 +++++++++++++++++++++++++++++++++++++++++++++ tests/test_bus.py | 43 ++++++++++++++++++++++++++++++++++++------- 2 files changed, 81 insertions(+), 7 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 3d6623eb7..b581233fb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -36,6 +36,32 @@ def solver_fixture(request): return request.getfixturevalue(request.param.__name__) +# ================================= +# COORDINATE CONFIGURATION FIXTURES +# ================================= + + +@pytest.fixture( + params=[ + {'timesteps': pd.date_range('2020-01-01', periods=10, freq='h', name='time'), 'years': None, 'scenarios': None}, + { + 'timesteps': pd.date_range('2020-01-01', periods=10, freq='h', name='time'), + 'years': pd.Index([2020, 2030, 2040], name='year'), + 'scenarios': None, + }, + { + 'timesteps': pd.date_range('2020-01-01', periods=10, freq='h', name='time'), + 'years': pd.Index([2020, 2030, 2040], name='year'), + 'scenarios': pd.Index(['A', 'B'], name='scenario'), + }, + ], + ids=['time_only', 'time+years', 'time+years+scenarios'], +) +def coords_config(request): + """Coordinate configurations for parametrized testing.""" + return request.param + + # ============================================================================ # HIERARCHICAL ELEMENT LIBRARY # ============================================================================ @@ -615,6 +641,25 @@ def basic_flow_system_linopy(timesteps_linopy) -> fx.FlowSystem: return flow_system +@pytest.fixture +def basic_flow_system_linopy_coords(coords_config) -> fx.FlowSystem: + """Create basic elements for component testing with coordinate parametrization.""" + flow_system = fx.FlowSystem(**coords_config) + + thermal_load = LoadProfiles.random_thermal(10) + p_el = LoadProfiles.random_electrical(10) + + costs = Effects.costs() + heat_load = Sinks.heat_load(thermal_load) + gas_source = Sources.gas_with_costs() + electricity_sink = Sinks.electricity_feed_in(p_el) + + flow_system.add_elements(*Buses.defaults()) + flow_system.add_elements(costs, heat_load, gas_source, electricity_sink) + + return flow_system + + # ============================================================================ # UTILITY FUNCTIONS (kept for backward compatibility) # ============================================================================ diff --git a/tests/test_bus.py b/tests/test_bus.py index c9bf3956c..e4e0de6fd 100644 --- a/tests/test_bus.py +++ b/tests/test_bus.py @@ -11,9 +11,9 @@ class TestBusModel: """Test the FlowModel class.""" - def test_bus(self, basic_flow_system_linopy): + def test_bus(self, basic_flow_system_linopy_coords, coords_config): """Test that flow model constraints are correctly generated.""" - flow_system = basic_flow_system_linopy + flow_system = basic_flow_system_linopy_coords bus = fx.Bus('TestBus', excess_penalty_per_flow_hour=None) flow_system.add_elements( bus, @@ -30,10 +30,9 @@ def test_bus(self, basic_flow_system_linopy): model.variables['GastarifTest(Q_Gas)|flow_rate'] == model.variables['WärmelastTest(Q_th_Last)|flow_rate'], ) - def test_bus_penalty(self, basic_flow_system_linopy): + def test_bus_penalty(self, basic_flow_system_linopy_coords, coords_config): """Test that flow model constraints are correctly generated.""" - flow_system = basic_flow_system_linopy - timesteps = flow_system.timesteps + flow_system = basic_flow_system_linopy_coords bus = fx.Bus('TestBus') flow_system.add_elements( bus, @@ -50,8 +49,12 @@ def test_bus_penalty(self, basic_flow_system_linopy): } assert set(bus.submodel.constraints) == {'TestBus|balance'} - assert_var_equal(model.variables['TestBus|excess_input'], model.add_variables(lower=0, coords=(timesteps,))) - assert_var_equal(model.variables['TestBus|excess_output'], model.add_variables(lower=0, coords=(timesteps,))) + assert_var_equal( + model.variables['TestBus|excess_input'], model.add_variables(lower=0, coords=model.get_coords()) + ) + assert_var_equal( + model.variables['TestBus|excess_output'], model.add_variables(lower=0, coords=model.get_coords()) + ) assert_conequal( model.constraints['TestBus|balance'], @@ -68,3 +71,29 @@ def test_bus_penalty(self, basic_flow_system_linopy): == (model.variables['TestBus|excess_input'] * 1e5 * model.hours_per_step).sum() + (model.variables['TestBus|excess_output'] * 1e5 * model.hours_per_step).sum(), ) + + def test_bus_with_coords(self, basic_flow_system_linopy_coords, coords_config): + """Test bus behavior across different coordinate configurations.""" + flow_system = basic_flow_system_linopy_coords + bus = fx.Bus('TestBus', excess_penalty_per_flow_hour=None) + flow_system.add_elements( + bus, + fx.Sink('WärmelastTest', sink=fx.Flow('Q_th_Last', 'TestBus')), + fx.Source('GastarifTest', source=fx.Flow('Q_Gas', 'TestBus')), + ) + model = create_linopy_model(flow_system) + + # Same core assertions as your existing test + assert set(bus.submodel.variables) == {'WärmelastTest(Q_th_Last)|flow_rate', 'GastarifTest(Q_Gas)|flow_rate'} + assert set(bus.submodel.constraints) == {'TestBus|balance'} + + assert_conequal( + model.constraints['TestBus|balance'], + model.variables['GastarifTest(Q_Gas)|flow_rate'] == model.variables['WärmelastTest(Q_th_Last)|flow_rate'], + ) + + # Just verify coordinate dimensions are correct + gas_var = model.variables['GastarifTest(Q_Gas)|flow_rate'] + if flow_system.scenarios is not None: + assert 'scenario' in gas_var.dims + assert 'time' in gas_var.dims From cf0186cec45d98de1629338bbadd55154f3df2e6 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 23 Jul 2025 22:13:52 +0200 Subject: [PATCH 265/448] Update tests to run with multiple coords --- tests/test_bus.py | 6 +-- tests/test_component.py | 19 ++++---- tests/test_effect.py | 57 ++++++++++++++-------- tests/test_flow.py | 88 +++++++++++++++++----------------- tests/test_linear_converter.py | 32 ++++++------- tests/test_storage.py | 38 ++++++++------- 6 files changed, 131 insertions(+), 109 deletions(-) diff --git a/tests/test_bus.py b/tests/test_bus.py index e4e0de6fd..58e00a1dc 100644 --- a/tests/test_bus.py +++ b/tests/test_bus.py @@ -13,7 +13,7 @@ class TestBusModel: def test_bus(self, basic_flow_system_linopy_coords, coords_config): """Test that flow model constraints are correctly generated.""" - flow_system = basic_flow_system_linopy_coords + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config bus = fx.Bus('TestBus', excess_penalty_per_flow_hour=None) flow_system.add_elements( bus, @@ -32,7 +32,7 @@ def test_bus(self, basic_flow_system_linopy_coords, coords_config): def test_bus_penalty(self, basic_flow_system_linopy_coords, coords_config): """Test that flow model constraints are correctly generated.""" - flow_system = basic_flow_system_linopy_coords + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config bus = fx.Bus('TestBus') flow_system.add_elements( bus, @@ -74,7 +74,7 @@ def test_bus_penalty(self, basic_flow_system_linopy_coords, coords_config): def test_bus_with_coords(self, basic_flow_system_linopy_coords, coords_config): """Test bus behavior across different coordinate configurations.""" - flow_system = basic_flow_system_linopy_coords + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config bus = fx.Bus('TestBus', excess_penalty_per_flow_hour=None) flow_system.add_elements( bus, diff --git a/tests/test_component.py b/tests/test_component.py index 90388ef26..2ed9bea3c 100644 --- a/tests/test_component.py +++ b/tests/test_component.py @@ -17,9 +17,8 @@ class TestComponentModel: - def test_flow_label_check(self, basic_flow_system_linopy): + def test_flow_label_check(self): """Test that flow model constraints are correctly generated.""" - _ = basic_flow_system_linopy inputs = [ fx.Flow('Q_th_Last', 'Fernwärme', relative_minimum=np.ones(10) * 0.1), fx.Flow('Q_Gas', 'Fernwärme', relative_minimum=np.ones(10) * 0.1), @@ -31,9 +30,9 @@ def test_flow_label_check(self, basic_flow_system_linopy): with pytest.raises(ValueError, match='Flow names must be unique!'): _ = flixopt.elements.Component('TestComponent', inputs=inputs, outputs=outputs) - def test_component(self, basic_flow_system_linopy): + def test_component(self, basic_flow_system_linopy_coords, coords_config): """Test that flow model constraints are correctly generated.""" - flow_system = basic_flow_system_linopy + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config inputs = [ fx.Flow('In1', 'Fernwärme', relative_minimum=np.ones(10) * 0.1), fx.Flow('In2', 'Fernwärme', relative_minimum=np.ones(10) * 0.1), @@ -72,9 +71,9 @@ def test_component(self, basic_flow_system_linopy): msg='Incorrect constraints', ) - def test_on_with_multiple_flows(self, basic_flow_system_linopy): + def test_on_with_multiple_flows(self, basic_flow_system_linopy_coords, coords_config): """Test that flow model constraints are correctly generated.""" - flow_system = basic_flow_system_linopy + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config timesteps = flow_system.timesteps ub_out2 = np.linspace(1, 1.5, 10).round(2) inputs = [ @@ -171,9 +170,9 @@ def test_on_with_multiple_flows(self, basic_flow_system_linopy): + 1e-5, ) - def test_on_with_single_flow(self, basic_flow_system_linopy): + def test_on_with_single_flow(self, basic_flow_system_linopy_coords, coords_config): """Test that flow model constraints are correctly generated.""" - flow_system = basic_flow_system_linopy + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config timesteps = flow_system.timesteps inputs = [ fx.Flow('In1', 'Fernwärme', relative_minimum=np.ones(10) * 0.1, size=100), @@ -231,9 +230,9 @@ def test_on_with_single_flow(self, basic_flow_system_linopy): model.variables['TestComponent|on'] == model.variables['TestComponent(In1)|on'], ) - def test_previous_states_with_multiple_flows(self, basic_flow_system_linopy): + def test_previous_states_with_multiple_flows(self, basic_flow_system_linopy_coords, coords_config): """Test that flow model constraints are correctly generated.""" - flow_system = basic_flow_system_linopy + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config timesteps = flow_system.timesteps ub_out2 = np.linspace(1, 1.5, 10).round(2) inputs = [ diff --git a/tests/test_effect.py b/tests/test_effect.py index 13e878041..a0a6fecfe 100644 --- a/tests/test_effect.py +++ b/tests/test_effect.py @@ -17,9 +17,8 @@ class TestEffectModel: """Test the FlowModel class.""" - def test_minimal(self, basic_flow_system_linopy): - flow_system = basic_flow_system_linopy - timesteps = flow_system.timesteps + def test_minimal(self, basic_flow_system_linopy_coords, coords_config): + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config effect = fx.Effect('Effect1', '€', 'Testing Effect') flow_system.add_elements(effect) @@ -47,11 +46,18 @@ def test_minimal(self, basic_flow_system_linopy): msg='Incorrect constraints', ) - assert_var_equal(model.variables['Effect1|total'], model.add_variables()) - assert_var_equal(model.variables['Effect1(invest)|total'], model.add_variables()) - assert_var_equal(model.variables['Effect1(operation)|total'], model.add_variables()) assert_var_equal( - model.variables['Effect1(operation)|total_per_timestep'], model.add_variables(coords=(timesteps,)) + model.variables['Effect1|total'], model.add_variables(coords=model.get_coords(['year', 'scenario'])) + ) + assert_var_equal( + model.variables['Effect1(invest)|total'], model.add_variables(coords=model.get_coords(['year', 'scenario'])) + ) + assert_var_equal( + model.variables['Effect1(operation)|total'], + model.add_variables(coords=model.get_coords(['year', 'scenario'])), + ) + assert_var_equal( + model.variables['Effect1(operation)|total_per_timestep'], model.add_variables(coords=model.get_coords()) ) assert_conequal( @@ -63,16 +69,15 @@ def test_minimal(self, basic_flow_system_linopy): assert_conequal( model.constraints['Effect1(operation)|total'], model.variables['Effect1(operation)|total'] - == model.variables['Effect1(operation)|total_per_timestep'].sum(), + == model.variables['Effect1(operation)|total_per_timestep'].sum('time'), ) assert_conequal( model.constraints['Effect1(operation)|total_per_timestep'], model.variables['Effect1(operation)|total_per_timestep'] == 0, ) - def test_bounds(self, basic_flow_system_linopy): - flow_system = basic_flow_system_linopy - timesteps = flow_system.timesteps + def test_bounds(self, basic_flow_system_linopy_coords, coords_config): + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config effect = fx.Effect( 'Effect1', '€', @@ -112,13 +117,24 @@ def test_bounds(self, basic_flow_system_linopy): msg='Incorrect constraints', ) - assert_var_equal(model.variables['Effect1|total'], model.add_variables(lower=3.0, upper=3.1)) - assert_var_equal(model.variables['Effect1(invest)|total'], model.add_variables(lower=2.0, upper=2.1)) - assert_var_equal(model.variables['Effect1(operation)|total'], model.add_variables(lower=1.0, upper=1.1)) + assert_var_equal( + model.variables['Effect1|total'], + model.add_variables(lower=3.0, upper=3.1, coords=model.get_coords(['year', 'scenario'])), + ) + assert_var_equal( + model.variables['Effect1(invest)|total'], + model.add_variables(lower=2.0, upper=2.1, coords=model.get_coords(['year', 'scenario'])), + ) + assert_var_equal( + model.variables['Effect1(operation)|total'], + model.add_variables(lower=1.0, upper=1.1, coords=model.get_coords(['year', 'scenario'])), + ) assert_var_equal( model.variables['Effect1(operation)|total_per_timestep'], model.add_variables( - lower=4.0 * model.hours_per_step, upper=4.1 * model.hours_per_step, coords=(timesteps,) + lower=4.0 * model.hours_per_step, + upper=4.1 * model.hours_per_step, + coords=model.get_coords(['time', 'year', 'scenario']), ), ) @@ -131,15 +147,15 @@ def test_bounds(self, basic_flow_system_linopy): assert_conequal( model.constraints['Effect1(operation)|total'], model.variables['Effect1(operation)|total'] - == model.variables['Effect1(operation)|total_per_timestep'].sum(), + == model.variables['Effect1(operation)|total_per_timestep'].sum('time'), ) assert_conequal( model.constraints['Effect1(operation)|total_per_timestep'], model.variables['Effect1(operation)|total_per_timestep'] == 0, ) - def test_shares(self, basic_flow_system_linopy): - flow_system = basic_flow_system_linopy + def test_shares(self, basic_flow_system_linopy_coords, coords_config): + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config effect1 = fx.Effect( 'Effect1', '€', @@ -202,8 +218,8 @@ def test_shares(self, basic_flow_system_linopy): class TestEffectResults: - def test_shares(self, basic_flow_system_linopy): - flow_system = basic_flow_system_linopy + def test_shares(self, basic_flow_system_linopy_coords, coords_config): + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config flow_system.effects['costs'].specific_share_to_other_effects_operation['Effect1'] = 0.5 flow_system.add_elements( fx.Effect( @@ -221,6 +237,7 @@ def test_shares(self, basic_flow_system_linopy): Q_th=fx.Flow( 'Q_th', bus='Fernwärme', + size=fx.InvestParameters(specific_effects=10, minimum_size=20, optional=False), ), Q_fu=fx.Flow('Q_fu', bus='Gas'), ), diff --git a/tests/test_flow.py b/tests/test_flow.py index 2ee609f68..357b0a352 100644 --- a/tests/test_flow.py +++ b/tests/test_flow.py @@ -11,9 +11,9 @@ class TestFlowModel: """Test the FlowModel class.""" - def test_flow_minimal(self, basic_flow_system_linopy): + def test_flow_minimal(self, basic_flow_system_linopy_coords, coords_config): """Test that flow model constraints are correctly generated.""" - flow_system = basic_flow_system_linopy + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config timesteps = flow_system.timesteps flow = fx.Flow('Wärme', bus='Fernwärme', size=100) @@ -36,8 +36,8 @@ def test_flow_minimal(self, basic_flow_system_linopy): ) assert_sets_equal(set(flow.submodel.constraints), {'Sink(Wärme)|total_flow_hours'}, msg='Incorrect constraints') - def test_flow(self, basic_flow_system_linopy): - flow_system = basic_flow_system_linopy + def test_flow(self, basic_flow_system_linopy_coords, coords_config): + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config timesteps = flow_system.timesteps flow = fx.Flow( 'Wärme', @@ -93,8 +93,8 @@ def test_flow(self, basic_flow_system_linopy): msg='Incorrect constraints', ) - def test_effects_per_flow_hour(self, basic_flow_system_linopy): - flow_system = basic_flow_system_linopy + def test_effects_per_flow_hour(self, basic_flow_system_linopy_coords, coords_config): + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config timesteps = flow_system.timesteps costs_per_flow_hour = xr.DataArray(np.linspace(1, 2, timesteps.size), coords=(timesteps,)) @@ -133,8 +133,8 @@ def test_effects_per_flow_hour(self, basic_flow_system_linopy): class TestFlowInvestModel: """Test the FlowModel class.""" - def test_flow_invest(self, basic_flow_system_linopy): - flow_system = basic_flow_system_linopy + def test_flow_invest(self, basic_flow_system_linopy_coords, coords_config): + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config timesteps = flow_system.timesteps flow = fx.Flow( @@ -192,8 +192,8 @@ def test_flow_invest(self, basic_flow_system_linopy): * xr.DataArray(np.linspace(0.5, 1, timesteps.size), coords=(timesteps,)), ) - def test_flow_invest_optional(self, basic_flow_system_linopy): - flow_system = basic_flow_system_linopy + def test_flow_invest_optional(self, basic_flow_system_linopy_coords, coords_config): + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config timesteps = flow_system.timesteps flow = fx.Flow( @@ -260,8 +260,8 @@ def test_flow_invest_optional(self, basic_flow_system_linopy): flow.submodel.variables['Sink(Wärme)|size'] >= flow.submodel.variables['Sink(Wärme)|is_invested'] * 20, ) - def test_flow_invest_optional_wo_min_size(self, basic_flow_system_linopy): - flow_system = basic_flow_system_linopy + def test_flow_invest_optional_wo_min_size(self, basic_flow_system_linopy_coords, coords_config): + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config timesteps = flow_system.timesteps flow = fx.Flow( @@ -328,8 +328,8 @@ def test_flow_invest_optional_wo_min_size(self, basic_flow_system_linopy): flow.submodel.variables['Sink(Wärme)|size'] >= flow.submodel.variables['Sink(Wärme)|is_invested'] * 1e-5, ) - def test_flow_invest_wo_min_size_non_optional(self, basic_flow_system_linopy): - flow_system = basic_flow_system_linopy + def test_flow_invest_wo_min_size_non_optional(self, basic_flow_system_linopy_coords, coords_config): + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config timesteps = flow_system.timesteps flow = fx.Flow( @@ -382,9 +382,9 @@ def test_flow_invest_wo_min_size_non_optional(self, basic_flow_system_linopy): * xr.DataArray(np.linspace(0.5, 1, timesteps.size), coords=(timesteps,)), ) - def test_flow_invest_fixed_size(self, basic_flow_system_linopy): + def test_flow_invest_fixed_size(self, basic_flow_system_linopy_coords, coords_config): """Test flow with fixed size investment.""" - flow_system = basic_flow_system_linopy + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config timesteps = flow_system.timesteps flow = fx.Flow( @@ -412,9 +412,9 @@ def test_flow_invest_fixed_size(self, basic_flow_system_linopy): flow.submodel.flow_rate, model.add_variables(lower=0.2 * 75, upper=0.9 * 75, coords=(timesteps,)) ) - def test_flow_invest_with_effects(self, basic_flow_system_linopy): + def test_flow_invest_with_effects(self, basic_flow_system_linopy_coords, coords_config): """Test flow with investment effects.""" - flow_system = basic_flow_system_linopy + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config # Create effects co2 = fx.Effect(label='CO2', unit='ton', description='CO2 emissions') @@ -453,9 +453,9 @@ def test_flow_invest_with_effects(self, basic_flow_system_linopy): + flow.submodel.variables['Sink(Wärme)|size'] * 0.1, ) - def test_flow_invest_divest_effects(self, basic_flow_system_linopy): + def test_flow_invest_divest_effects(self, basic_flow_system_linopy_coords, coords_config): """Test flow with divestment effects.""" - flow_system = basic_flow_system_linopy + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config flow = fx.Flow( 'Wärme', @@ -483,8 +483,8 @@ def test_flow_invest_divest_effects(self, basic_flow_system_linopy): class TestFlowOnModel: """Test the FlowModel class.""" - def test_flow_on(self, basic_flow_system_linopy): - flow_system = basic_flow_system_linopy + def test_flow_on(self, basic_flow_system_linopy_coords, coords_config): + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config timesteps = flow_system.timesteps flow = fx.Flow( 'Wärme', @@ -547,8 +547,8 @@ def test_flow_on(self, basic_flow_system_linopy): == (flow.submodel.variables['Sink(Wärme)|on'] * model.hours_per_step).sum(), ) - def test_effects_per_running_hour(self, basic_flow_system_linopy): - flow_system = basic_flow_system_linopy + def test_effects_per_running_hour(self, basic_flow_system_linopy_coords, coords_config): + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config timesteps = flow_system.timesteps costs_per_running_hour = xr.DataArray(np.linspace(1, 2, timesteps.size), coords=(timesteps,)) @@ -601,9 +601,9 @@ def test_effects_per_running_hour(self, basic_flow_system_linopy): == flow.submodel.variables['Sink(Wärme)|on'] * model.hours_per_step * co2_per_running_hour, ) - def test_consecutive_on_hours(self, basic_flow_system_linopy): + def test_consecutive_on_hours(self, basic_flow_system_linopy_coords, coords_config): """Test flow with minimum and maximum consecutive on hours.""" - flow_system = basic_flow_system_linopy + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config timesteps = flow_system.timesteps flow = fx.Flow( @@ -684,9 +684,9 @@ def test_consecutive_on_hours(self, basic_flow_system_linopy): * 2, ) - def test_consecutive_on_hours_previous(self, basic_flow_system_linopy): + def test_consecutive_on_hours_previous(self, basic_flow_system_linopy_coords, coords_config): """Test flow with minimum and maximum consecutive on hours.""" - flow_system = basic_flow_system_linopy + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config timesteps = flow_system.timesteps flow = fx.Flow( @@ -766,9 +766,9 @@ def test_consecutive_on_hours_previous(self, basic_flow_system_linopy): * 2, ) - def test_consecutive_off_hours(self, basic_flow_system_linopy): + def test_consecutive_off_hours(self, basic_flow_system_linopy_coords, coords_config): """Test flow with minimum and maximum consecutive off hours.""" - flow_system = basic_flow_system_linopy + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config timesteps = flow_system.timesteps flow = fx.Flow( @@ -849,9 +849,9 @@ def test_consecutive_off_hours(self, basic_flow_system_linopy): * 4, ) - def test_consecutive_off_hours_previous(self, basic_flow_system_linopy): + def test_consecutive_off_hours_previous(self, basic_flow_system_linopy_coords, coords_config): """Test flow with minimum and maximum consecutive off hours.""" - flow_system = basic_flow_system_linopy + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config timesteps = flow_system.timesteps flow = fx.Flow( @@ -933,9 +933,9 @@ def test_consecutive_off_hours_previous(self, basic_flow_system_linopy): * 4, ) - def test_switch_on_constraints(self, basic_flow_system_linopy): + def test_switch_on_constraints(self, basic_flow_system_linopy_coords, coords_config): """Test flow with constraints on the number of startups.""" - flow_system = basic_flow_system_linopy + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config flow = fx.Flow( 'Wärme', @@ -992,9 +992,9 @@ def test_switch_on_constraints(self, basic_flow_system_linopy): model.variables['Sink(Wärme)->costs(operation)'] == flow.submodel.variables['Sink(Wärme)|switch|on'] * 100, ) - def test_on_hours_limits(self, basic_flow_system_linopy): + def test_on_hours_limits(self, basic_flow_system_linopy_coords, coords_config): """Test flow with limits on total on hours.""" - flow_system = basic_flow_system_linopy + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config flow = fx.Flow( 'Wärme', @@ -1031,8 +1031,8 @@ def test_on_hours_limits(self, basic_flow_system_linopy): class TestFlowOnInvestModel: """Test the FlowModel class.""" - def test_flow_on_invest_optional(self, basic_flow_system_linopy): - flow_system = basic_flow_system_linopy + def test_flow_on_invest_optional(self, basic_flow_system_linopy_coords, coords_config): + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config timesteps = flow_system.timesteps flow = fx.Flow( 'Wärme', @@ -1130,8 +1130,8 @@ def test_flow_on_invest_optional(self, basic_flow_system_linopy): flow.submodel.variables['Sink(Wärme)|flow_rate'] <= flow.submodel.variables['Sink(Wärme)|size'] * 0.8, ) - def test_flow_on_invest_non_optional(self, basic_flow_system_linopy): - flow_system = basic_flow_system_linopy + def test_flow_on_invest_non_optional(self, basic_flow_system_linopy_coords, coords_config): + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config timesteps = flow_system.timesteps flow = fx.Flow( 'Wärme', @@ -1222,9 +1222,9 @@ def test_flow_on_invest_non_optional(self, basic_flow_system_linopy): class TestFlowWithFixedProfile: """Test Flow with fixed relative profile.""" - def test_fixed_relative_profile(self, basic_flow_system_linopy): + def test_fixed_relative_profile(self, basic_flow_system_linopy_coords, coords_config): """Test flow with a fixed relative profile.""" - flow_system = basic_flow_system_linopy + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config timesteps = flow_system.timesteps # Create a time-varying profile (e.g., for a load or renewable generation) @@ -1242,9 +1242,9 @@ def test_fixed_relative_profile(self, basic_flow_system_linopy): model.add_variables(lower=profile * 100, upper=profile * 100, coords=(timesteps,)), ) - def test_fixed_profile_with_investment(self, basic_flow_system_linopy): + def test_fixed_profile_with_investment(self, basic_flow_system_linopy_coords, coords_config): """Test flow with fixed profile and investment.""" - flow_system = basic_flow_system_linopy + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config timesteps = flow_system.timesteps # Create a fixed profile diff --git a/tests/test_linear_converter.py b/tests/test_linear_converter.py index 322a5f6f0..e90d52f40 100644 --- a/tests/test_linear_converter.py +++ b/tests/test_linear_converter.py @@ -12,9 +12,9 @@ class TestLinearConverterModel: """Test the LinearConverterModel class.""" - def test_basic_linear_converter(self, basic_flow_system_linopy): + def test_basic_linear_converter(self, basic_flow_system_linopy_coords, coords_config): """Test basic initialization and modeling of a LinearConverter.""" - flow_system = basic_flow_system_linopy + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config # Create input and output flows input_flow = fx.Flow('input', bus='input_bus', size=100) @@ -45,9 +45,9 @@ def test_basic_linear_converter(self, basic_flow_system_linopy): input_flow.submodel.flow_rate * 0.8 == output_flow.submodel.flow_rate * 1.0, ) - def test_linear_converter_time_varying(self, basic_flow_system_linopy): + def test_linear_converter_time_varying(self, basic_flow_system_linopy_coords, coords_config): """Test a LinearConverter with time-varying conversion factors.""" - flow_system = basic_flow_system_linopy + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config timesteps = flow_system.timesteps # Create time-varying efficiency (e.g., temperature-dependent) @@ -83,9 +83,9 @@ def test_linear_converter_time_varying(self, basic_flow_system_linopy): input_flow.submodel.flow_rate * efficiency_series == output_flow.submodel.flow_rate * 1.0, ) - def test_linear_converter_multiple_factors(self, basic_flow_system_linopy): + def test_linear_converter_multiple_factors(self, basic_flow_system_linopy_coords, coords_config): """Test a LinearConverter with multiple conversion factors.""" - flow_system = basic_flow_system_linopy + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config # Create flows input_flow1 = fx.Flow('input1', bus='input_bus1', size=100) @@ -136,9 +136,9 @@ def test_linear_converter_multiple_factors(self, basic_flow_system_linopy): input_flow1.submodel.flow_rate * 0.2 == output_flow2.submodel.flow_rate * 0.3, ) - def test_linear_converter_with_on_off(self, basic_flow_system_linopy): + def test_linear_converter_with_on_off(self, basic_flow_system_linopy_coords, coords_config): """Test a LinearConverter with OnOffParameters.""" - flow_system = basic_flow_system_linopy + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config # Create input and output flows input_flow = fx.Flow('input', bus='input_bus', size=100) @@ -193,9 +193,9 @@ def test_linear_converter_with_on_off(self, basic_flow_system_linopy): == model.variables['Converter|on'] * model.hours_per_step * 5, ) - def test_linear_converter_multidimensional(self, basic_flow_system_linopy): + def test_linear_converter_multidimensional(self, basic_flow_system_linopy_coords, coords_config): """Test LinearConverter with multiple inputs, outputs, and connections between them.""" - flow_system = basic_flow_system_linopy + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config # Create a more complex setup with multiple flows input_flow1 = fx.Flow('fuel', bus='fuel_bus', size=100) @@ -247,9 +247,9 @@ def test_linear_converter_multidimensional(self, basic_flow_system_linopy): input_flow1.submodel.flow_rate * 0.1 == output_flow2.submodel.flow_rate * 0.5, ) - def test_edge_case_time_varying_conversion(self, basic_flow_system_linopy): + def test_edge_case_time_varying_conversion(self, basic_flow_system_linopy_coords, coords_config): """Test edge case with extreme time-varying conversion factors.""" - flow_system = basic_flow_system_linopy + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config timesteps = flow_system.timesteps # Create fluctuating conversion efficiency (e.g., for a heat pump) @@ -288,9 +288,9 @@ def test_edge_case_time_varying_conversion(self, basic_flow_system_linopy): input_flow.submodel.flow_rate * fluctuating_cop == output_flow.submodel.flow_rate * 1.0, ) - def test_piecewise_conversion(self, basic_flow_system_linopy): + def test_piecewise_conversion(self, basic_flow_system_linopy_coords, coords_config): """Test a LinearConverter with PiecewiseConversion.""" - flow_system = basic_flow_system_linopy + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config timesteps = flow_system.timesteps # Create input and output flows @@ -377,9 +377,9 @@ def test_piecewise_conversion(self, basic_flow_system_linopy): <= 1, ) - def test_piecewise_conversion_with_onoff(self, basic_flow_system_linopy): + def test_piecewise_conversion_with_onoff(self, basic_flow_system_linopy_coords, coords_config): """Test a LinearConverter with PiecewiseConversion and OnOffParameters.""" - flow_system = basic_flow_system_linopy + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config timesteps = flow_system.timesteps # Create input and output flows diff --git a/tests/test_storage.py b/tests/test_storage.py index f6b6f2079..479f66a87 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -11,9 +11,9 @@ class TestStorageModel: """Test that storage model variables and constraints are correctly generated.""" - def test_basic_storage(self, basic_flow_system_linopy): + def test_basic_storage(self, basic_flow_system_linopy_coords, coords_config): """Test that basic storage model variables and constraints are correctly generated.""" - flow_system = basic_flow_system_linopy + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config timesteps = flow_system.timesteps timesteps_extra = flow_system.timesteps_extra @@ -85,9 +85,9 @@ def test_basic_storage(self, basic_flow_system_linopy): model.variables['TestStorage|charge_state'].isel(time=0) == 0, ) - def test_lossy_storage(self, basic_flow_system_linopy): + def test_lossy_storage(self, basic_flow_system_linopy_coords, coords_config): """Test that basic storage model variables and constraints are correctly generated.""" - flow_system = basic_flow_system_linopy + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config timesteps = flow_system.timesteps timesteps_extra = flow_system.timesteps_extra @@ -170,9 +170,9 @@ def test_lossy_storage(self, basic_flow_system_linopy): model.variables['TestStorage|charge_state'].isel(time=0) == 0, ) - def test_charge_state_bounds(self, basic_flow_system_linopy): + def test_charge_state_bounds(self, basic_flow_system_linopy_coords, coords_config): """Test that basic storage model variables and constraints are correctly generated.""" - flow_system = basic_flow_system_linopy + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config timesteps = flow_system.timesteps timesteps_extra = flow_system.timesteps_extra @@ -251,9 +251,9 @@ def test_charge_state_bounds(self, basic_flow_system_linopy): model.variables['TestStorage|charge_state'].isel(time=0) == 3, ) - def test_storage_with_investment(self, basic_flow_system_linopy): + def test_storage_with_investment(self, basic_flow_system_linopy_coords, coords_config): """Test storage with investment parameters.""" - flow_system = basic_flow_system_linopy + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config # Create storage with investment parameters storage = fx.Storage( @@ -297,9 +297,9 @@ def test_storage_with_investment(self, basic_flow_system_linopy): model.variables['InvestStorage|size'] >= model.variables['InvestStorage|is_invested'] * 20, ) - def test_storage_with_final_state_constraints(self, basic_flow_system_linopy): + def test_storage_with_final_state_constraints(self, basic_flow_system_linopy_coords, coords_config): """Test storage with final state constraints.""" - flow_system = basic_flow_system_linopy + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config # Create storage with final state constraints storage = fx.Storage( @@ -342,9 +342,9 @@ def test_storage_with_final_state_constraints(self, basic_flow_system_linopy): model.variables['FinalStateStorage|charge_state'].isel(time=-1) <= 25, ) - def test_storage_cyclic_initialization(self, basic_flow_system_linopy): + def test_storage_cyclic_initialization(self, basic_flow_system_linopy_coords, coords_config): """Test storage with cyclic initialization.""" - flow_system = basic_flow_system_linopy + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config # Create storage with cyclic initialization storage = fx.Storage( @@ -375,9 +375,9 @@ def test_storage_cyclic_initialization(self, basic_flow_system_linopy): 'prevent_simultaneous', [True, False], ) - def test_simultaneous_charge_discharge(self, basic_flow_system_linopy, prevent_simultaneous): + def test_simultaneous_charge_discharge(self, basic_flow_system_linopy_coords, coords_config, prevent_simultaneous): """Test prevent_simultaneous_charge_and_discharge parameter.""" - flow_system = basic_flow_system_linopy + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config # Create storage with or without simultaneous charge/discharge prevention storage = fx.Storage( @@ -424,10 +424,16 @@ def test_simultaneous_charge_discharge(self, basic_flow_system_linopy, prevent_s ], ) def test_investment_parameters( - self, basic_flow_system_linopy, optional, minimum_size, expected_vars, expected_constraints + self, + basic_flow_system_linopy_coords, + coords_config, + optional, + minimum_size, + expected_vars, + expected_constraints, ): """Test different investment parameter combinations.""" - flow_system = basic_flow_system_linopy + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config # Create investment parameters invest_params = { From 5510297e0ba5f63887f2b531384c39ce923ba1ab Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 23 Jul 2025 22:15:03 +0200 Subject: [PATCH 266/448] Fix Effects dataset computation in case of empty effects --- flixopt/interface.py | 20 ++++++------ flixopt/results.py | 76 +++++++++++++++++++++++++++++--------------- tests/test_effect.py | 29 ++++++++++------- 3 files changed, 76 insertions(+), 49 deletions(-) diff --git a/flixopt/interface.py b/flixopt/interface.py index 374c3fb44..8bcab1de0 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -339,15 +339,13 @@ def use_consecutive_off_hours(self) -> bool: @property def use_switch_on(self) -> bool: """Determines wether a Variable for SWITCH-ON is needed or not""" - return ( - any( - param not in (None, {}) - for param in [ - self.effects_per_switch_on, - self.switch_on_total_max, - self.on_hours_total_min, - self.on_hours_total_max, - ] - ) - or self.force_switch_on + if self.force_switch_on: + return True + + return any( + param is not None or param != {} + for param in [ + self.effects_per_switch_on, + self.switch_on_total_max, + ] ) diff --git a/flixopt/results.py b/flixopt/results.py index 512af4ad7..dfe6a759b 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -200,7 +200,7 @@ def __init__( self._flow_rates = None self._flow_hours = None self._sizes = None - self._effects_per_component = {'operation': None, 'invest': None, 'total': None} + self._effects_per_component = None def __getitem__(self, key: str) -> Union['ComponentResults', 'BusResults', 'EffectResults', 'FlowResults']: if key in self.components: @@ -312,20 +312,24 @@ def filter_solution( startswith=startswith, ) - def effects_per_component(self, mode: Literal['operation', 'invest', 'total'] = 'total') -> xr.Dataset: - """Returns a dataset containing effect totals for each components (including their flows). - - Args: - mode: Which effects to contain. (operation, invest, total) + @property + def effects_per_component(self) -> xr.Dataset: + """Returns a dataset containing effect results for each mode, aggregated by Component Returns: An xarray Dataset with an additional component dimension and effects as variables. """ - if mode not in ['operation', 'invest', 'total']: - raise ValueError(f'Invalid mode {mode}') - if self._effects_per_component[mode] is None: - self._effects_per_component[mode] = self._create_effects_dataset(mode) - return self._effects_per_component[mode] + if self._effects_per_component is None: + self._effects_per_component = xr.Dataset( + { + mode: self._create_effects_dataset(mode).to_dataarray('effect', name=mode) + for mode in ['operation', 'invest', 'total'] + } + ) + dim_order = ['time', 'year', 'scenario', 'component', 'effect'] + self._effects_per_component = self._effects_per_component.transpose(*dim_order, missing_dims='ignore') + + return self._effects_per_component def flow_rates( self, @@ -580,7 +584,7 @@ def _compute_effect_total( total = xr.DataArray(np.nan) return total.rename(f'{element}->{effect}({mode})') - def _create_effects_dataset(self, mode: Literal['operation', 'invest', 'total'] = 'total') -> xr.Dataset: + def _create_effects_dataset(self, mode: Literal['operation', 'invest', 'total']) -> xr.Dataset: """Creates a dataset containing effect totals for all components (including their flows). The dataset does contain the direct as well as the indirect effects of each component. @@ -590,24 +594,44 @@ def _create_effects_dataset(self, mode: Literal['operation', 'invest', 'total'] Returns: An xarray Dataset with components as dimension and effects as variables. """ - # Create an empty dataset ds = xr.Dataset() + all_arrays = {} + template = None # Template is needed to determine the dimensions of the arrays. This handles the case of no shares for an effect + + components_list = list(self.components) - # Add each effect as a variable to the dataset + # First pass: collect arrays and find template for effect in self.effects: - # Create a list of DataArrays, one for each component - component_arrays = [ - self._compute_effect_total(element=component, effect=effect, mode=mode, include_flows=True).expand_dims( - component=[component] - ) # Add component dimension to each array - for component in list(self.components) - ] + effect_arrays = [] + for component in components_list: + da = self._compute_effect_total(element=component, effect=effect, mode=mode, include_flows=True) + effect_arrays.append(da) + + if template is None and (da.dims or not da.isnull().all()): + template = da + + all_arrays[effect] = effect_arrays + + # Ensure we have a template + if template is None: + raise ValueError( + f"No template with proper dimensions found for mode '{mode}'. " + f'All computed arrays are scalars, which indicates a data issue.' + ) + + # Second pass: process all effects (guaranteed to include all) + for effect in self.effects: + dataarrays = all_arrays[effect] + component_arrays = [] + + for component, arr in zip(components_list, dataarrays, strict=False): + # Expand scalar NaN arrays to match template dimensions + if not arr.dims and np.isnan(arr.item()): + arr = xr.full_like(template, np.nan, dtype=float).rename(arr.name) + + component_arrays.append(arr.expand_dims(component=[component])) - # Combine all components into one DataArray for this effect - if component_arrays: - effect_array = xr.concat(component_arrays, dim='component', coords='minimal') - # Add this effect as a variable to the dataset - ds[effect] = effect_array + ds[effect] = xr.concat(component_arrays, dim='component', coords='minimal') # For now include a test to ensure correctness suffix = { diff --git a/tests/test_effect.py b/tests/test_effect.py index a0a6fecfe..cc4841900 100644 --- a/tests/test_effect.py +++ b/tests/test_effect.py @@ -266,58 +266,63 @@ def test_shares(self, basic_flow_system_linopy_coords, coords_config): np.testing.assert_allclose(results.effect_share_factors['invest'][key].values, value) xr.testing.assert_allclose( - results.effects_per_component('operation').sum('component')['costs'], + results.effects_per_component['operation'].sum('component').sel(effect='costs', drop=True), results.solution['costs(operation)|total_per_timestep'].fillna(0), ) xr.testing.assert_allclose( - results.effects_per_component('operation').sum('component')['Effect1'], + results.effects_per_component['operation'].sum('component').sel(effect='Effect1', drop=True), results.solution['Effect1(operation)|total_per_timestep'].fillna(0), ) xr.testing.assert_allclose( - results.effects_per_component('operation').sum('component')['Effect2'], + results.effects_per_component['operation'].sum('component').sel(effect='Effect2', drop=True), results.solution['Effect2(operation)|total_per_timestep'].fillna(0), ) xr.testing.assert_allclose( - results.effects_per_component('operation').sum('component')['Effect3'], + results.effects_per_component['operation'].sum('component').sel(effect='Effect3', drop=True), results.solution['Effect3(operation)|total_per_timestep'].fillna(0), ) # Invest mode checks xr.testing.assert_allclose( - results.effects_per_component('invest').sum('component')['costs'], results.solution['costs(invest)|total'] + results.effects_per_component['invest'].sum('component').sel(effect='costs', drop=True), + results.solution['costs(invest)|total'], ) xr.testing.assert_allclose( - results.effects_per_component('invest').sum('component')['Effect1'], + results.effects_per_component['invest'].sum('component').sel(effect='Effect1', drop=True), results.solution['Effect1(invest)|total'], ) xr.testing.assert_allclose( - results.effects_per_component('invest').sum('component')['Effect2'], + results.effects_per_component['invest'].sum('component').sel(effect='Effect2', drop=True), results.solution['Effect2(invest)|total'], ) xr.testing.assert_allclose( - results.effects_per_component('invest').sum('component')['Effect3'], + results.effects_per_component['invest'].sum('component').sel(effect='Effect3', drop=True), results.solution['Effect3(invest)|total'], ) # Total mode checks xr.testing.assert_allclose( - results.effects_per_component('total').sum('component')['costs'], results.solution['costs|total'] + results.effects_per_component['total'].sum('component').sel(effect='costs', drop=True), + results.solution['costs|total'], ) xr.testing.assert_allclose( - results.effects_per_component('total').sum('component')['Effect1'], results.solution['Effect1|total'] + results.effects_per_component['total'].sum('component').sel(effect='Effect1', drop=True), + results.solution['Effect1|total'], ) xr.testing.assert_allclose( - results.effects_per_component('total').sum('component')['Effect2'], results.solution['Effect2|total'] + results.effects_per_component['total'].sum('component').sel(effect='Effect2', drop=True), + results.solution['Effect2|total'], ) xr.testing.assert_allclose( - results.effects_per_component('total').sum('component')['Effect3'], results.solution['Effect3|total'] + results.effects_per_component['total'].sum('component').sel(effect='Effect3', drop=True), + results.solution['Effect3|total'], ) From b694dbe1b7605495a1e5902fdbacd8f7e3d24877 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 24 Jul 2025 13:13:41 +0200 Subject: [PATCH 267/448] Update Test for multiple dims Fix Dim order in scaled_bounds_with_state Bugfix logic in .use_switch_on --- flixopt/interface.py | 6 +- flixopt/modeling.py | 4 +- tests/test_flow.py | 220 ++++++++++++++++++++++++++----------------- 3 files changed, 140 insertions(+), 90 deletions(-) diff --git a/flixopt/interface.py b/flixopt/interface.py index 8bcab1de0..cae1757c7 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -246,8 +246,8 @@ def maximum_or_fixed_size(self) -> NonTemporalData: class OnOffParameters(Interface): def __init__( self, - effects_per_switch_on: Optional['NonTemporalEffectsUser'] = None, - effects_per_running_hour: Optional['NonTemporalEffectsUser'] = None, + effects_per_switch_on: Optional['TemporalEffectsUser'] = None, + effects_per_running_hour: Optional['TemporalEffectsUser'] = None, on_hours_total_min: Optional[int] = None, on_hours_total_max: Optional[int] = None, consecutive_on_hours_min: Optional[TemporalDataUser] = None, @@ -343,7 +343,7 @@ def use_switch_on(self) -> bool: return True return any( - param is not None or param != {} + param is not None and param != {} for param in [ self.effects_per_switch_on, self.switch_on_total_max, diff --git a/flixopt/modeling.py b/flixopt/modeling.py index 14f8c45f3..11880c5e8 100644 --- a/flixopt/modeling.py +++ b/flixopt/modeling.py @@ -536,8 +536,8 @@ def scaled_bounds_with_state( ) scaling_upper = model.add_constraints(variable <= scaling_variable * rel_upper, name=f'{name}|ub2') - big_m_upper = scaling_max * rel_upper - big_m_lower = np.maximum(CONFIG.modeling.EPSILON, scaling_min * rel_lower) + big_m_upper = rel_upper * scaling_max + big_m_lower = np.maximum(CONFIG.modeling.EPSILON, rel_lower * scaling_min) binary_upper = model.add_constraints(variable_state * big_m_upper >= variable, name=f'{name}|ub1') binary_lower = model.add_constraints(variable_state * big_m_lower <= variable, name=f'{name}|lb1') diff --git a/tests/test_flow.py b/tests/test_flow.py index 357b0a352..0d60a8bfe 100644 --- a/tests/test_flow.py +++ b/tests/test_flow.py @@ -14,7 +14,7 @@ class TestFlowModel: def test_flow_minimal(self, basic_flow_system_linopy_coords, coords_config): """Test that flow model constraints are correctly generated.""" flow_system, coords_config = basic_flow_system_linopy_coords, coords_config - timesteps = flow_system.timesteps + flow = fx.Flow('Wärme', bus='Fernwärme', size=100) flow_system.add_elements(fx.Sink('Sink', sink=flow)) @@ -24,10 +24,12 @@ def test_flow_minimal(self, basic_flow_system_linopy_coords, coords_config): assert_conequal( model.constraints['Sink(Wärme)|total_flow_hours'], flow.submodel.variables['Sink(Wärme)|total_flow_hours'] - == (flow.submodel.variables['Sink(Wärme)|flow_rate'] * model.hours_per_step).sum(), + == (flow.submodel.variables['Sink(Wärme)|flow_rate'] * model.hours_per_step).sum('time'), + ) + assert_var_equal(flow.submodel.flow_rate, model.add_variables(lower=0, upper=100, coords=model.get_coords())) + assert_var_equal( + flow.submodel.total_flow_hours, model.add_variables(lower=0, coords=model.get_coords(['year', 'scenario'])) ) - assert_var_equal(flow.submodel.flow_rate, model.add_variables(lower=0, upper=100, coords=(timesteps,))) - assert_var_equal(flow.submodel.total_flow_hours, model.add_variables(lower=0)) assert_sets_equal( set(flow.submodel.variables), @@ -39,6 +41,7 @@ def test_flow_minimal(self, basic_flow_system_linopy_coords, coords_config): def test_flow(self, basic_flow_system_linopy_coords, coords_config): flow_system, coords_config = basic_flow_system_linopy_coords, coords_config timesteps = flow_system.timesteps + flow = fx.Flow( 'Wärme', bus='Fernwärme', @@ -58,17 +61,23 @@ def test_flow(self, basic_flow_system_linopy_coords, coords_config): assert_conequal( model.constraints['Sink(Wärme)|total_flow_hours'], flow.submodel.variables['Sink(Wärme)|total_flow_hours'] - == (flow.submodel.variables['Sink(Wärme)|flow_rate'] * model.hours_per_step).sum(), + == (flow.submodel.variables['Sink(Wärme)|flow_rate'] * model.hours_per_step).sum('time'), ) - assert_var_equal(flow.submodel.total_flow_hours, model.add_variables(lower=10, upper=1000)) + assert_var_equal( + flow.submodel.total_flow_hours, + model.add_variables(lower=10, upper=1000, coords=model.get_coords(['year', 'scenario'])), + ) + + assert flow.relative_minimum.dims == tuple(model.get_coords()) + assert flow.relative_maximum.dims == tuple(model.get_coords()) assert_var_equal( flow.submodel.flow_rate, model.add_variables( - lower=np.linspace(0, 0.5, timesteps.size) * 100, - upper=np.linspace(0.5, 1, timesteps.size) * 100, - coords=(timesteps,), + lower=flow.relative_minimum * 100, + upper=flow.relative_maximum * 100, + coords=model.get_coords(), ), ) @@ -168,28 +177,32 @@ def test_flow_invest(self, basic_flow_system_linopy_coords, coords_config): ) # size - assert_var_equal(model['Sink(Wärme)|size'], model.add_variables(lower=20, upper=100)) + assert_var_equal( + model['Sink(Wärme)|size'], + model.add_variables(lower=20, upper=100, coords=model.get_coords(['year', 'scenario'])), + ) + + assert flow.relative_minimum.dims == tuple(model.get_coords()) + assert flow.relative_maximum.dims == tuple(model.get_coords()) # flow_rate assert_var_equal( flow.submodel.flow_rate, model.add_variables( - lower=np.linspace(0.1, 0.5, timesteps.size) * 20, - upper=np.linspace(0.5, 1, timesteps.size) * 100, - coords=(timesteps,), + lower=flow.relative_minimum * 20, + upper=flow.relative_maximum * 100, + coords=model.get_coords(), ), ) assert_conequal( model.constraints['Sink(Wärme)|flow_rate|lb'], flow.submodel.variables['Sink(Wärme)|flow_rate'] - >= flow.submodel.variables['Sink(Wärme)|size'] - * xr.DataArray(np.linspace(0.1, 0.5, timesteps.size), coords=(timesteps,)), + >= flow.submodel.variables['Sink(Wärme)|size'] * flow.relative_minimum, ) assert_conequal( model.constraints['Sink(Wärme)|flow_rate|ub'], flow.submodel.variables['Sink(Wärme)|flow_rate'] - <= flow.submodel.variables['Sink(Wärme)|size'] - * xr.DataArray(np.linspace(0.5, 1, timesteps.size), coords=(timesteps,)), + <= flow.submodel.variables['Sink(Wärme)|size'] * flow.relative_maximum, ) def test_flow_invest_optional(self, basic_flow_system_linopy_coords, coords_config): @@ -224,30 +237,37 @@ def test_flow_invest_optional(self, basic_flow_system_linopy_coords, coords_conf msg='Incorrect constraints', ) - assert_var_equal(model['Sink(Wärme)|size'], model.add_variables(lower=0, upper=100)) + assert_var_equal( + model['Sink(Wärme)|size'], + model.add_variables(lower=0, upper=100, coords=model.get_coords(['year', 'scenario'])), + ) + + assert_var_equal( + model['Sink(Wärme)|is_invested'], + model.add_variables(binary=True, coords=model.get_coords(['year', 'scenario'])), + ) - assert_var_equal(model['Sink(Wärme)|is_invested'], model.add_variables(binary=True)) + assert flow.relative_minimum.dims == tuple(model.get_coords()) + assert flow.relative_maximum.dims == tuple(model.get_coords()) # flow_rate assert_var_equal( flow.submodel.flow_rate, model.add_variables( lower=0, # Optional investment - upper=np.linspace(0.5, 1, timesteps.size) * 100, - coords=(timesteps,), + upper=flow.relative_maximum * 100, + coords=model.get_coords(), ), ) assert_conequal( model.constraints['Sink(Wärme)|flow_rate|lb'], flow.submodel.variables['Sink(Wärme)|flow_rate'] - >= flow.submodel.variables['Sink(Wärme)|size'] - * xr.DataArray(np.linspace(0.1, 0.5, timesteps.size), coords=(timesteps,)), + >= flow.submodel.variables['Sink(Wärme)|size'] * flow.relative_minimum, ) assert_conequal( model.constraints['Sink(Wärme)|flow_rate|ub'], flow.submodel.variables['Sink(Wärme)|flow_rate'] - <= flow.submodel.variables['Sink(Wärme)|size'] - * xr.DataArray(np.linspace(0.5, 1, timesteps.size), coords=(timesteps,)), + <= flow.submodel.variables['Sink(Wärme)|size'] * flow.relative_maximum, ) # Is invested @@ -292,30 +312,37 @@ def test_flow_invest_optional_wo_min_size(self, basic_flow_system_linopy_coords, msg='Incorrect constraints', ) - assert_var_equal(model['Sink(Wärme)|size'], model.add_variables(lower=0, upper=100)) + assert_var_equal( + model['Sink(Wärme)|size'], + model.add_variables(lower=0, upper=100, coords=model.get_coords(['year', 'scenario'])), + ) - assert_var_equal(model['Sink(Wärme)|is_invested'], model.add_variables(binary=True)) + assert_var_equal( + model['Sink(Wärme)|is_invested'], + model.add_variables(binary=True, coords=model.get_coords(['year', 'scenario'])), + ) + + assert flow.relative_minimum.dims == tuple(model.get_coords()) + assert flow.relative_maximum.dims == tuple(model.get_coords()) # flow_rate assert_var_equal( flow.submodel.flow_rate, model.add_variables( lower=0, # Optional investment - upper=np.linspace(0.5, 1, timesteps.size) * 100, - coords=(timesteps,), + upper=flow.relative_maximum * 100, + coords=model.get_coords(), ), ) assert_conequal( model.constraints['Sink(Wärme)|flow_rate|lb'], flow.submodel.variables['Sink(Wärme)|flow_rate'] - >= flow.submodel.variables['Sink(Wärme)|size'] - * xr.DataArray(np.linspace(0.1, 0.5, timesteps.size), coords=(timesteps,)), + >= flow.submodel.variables['Sink(Wärme)|size'] * flow.relative_minimum, ) assert_conequal( model.constraints['Sink(Wärme)|flow_rate|ub'], flow.submodel.variables['Sink(Wärme)|flow_rate'] - <= flow.submodel.variables['Sink(Wärme)|size'] - * xr.DataArray(np.linspace(0.5, 1, timesteps.size), coords=(timesteps,)), + <= flow.submodel.variables['Sink(Wärme)|size'] * flow.relative_maximum, ) # Is invested @@ -358,34 +385,37 @@ def test_flow_invest_wo_min_size_non_optional(self, basic_flow_system_linopy_coo msg='Incorrect constraints', ) - assert_var_equal(model['Sink(Wärme)|size'], model.add_variables(lower=1e-5, upper=100)) + assert_var_equal( + model['Sink(Wärme)|size'], + model.add_variables(lower=1e-5, upper=100, coords=model.get_coords(['year', 'scenario'])), + ) + + assert flow.relative_minimum.dims == tuple(model.get_coords()) + assert flow.relative_maximum.dims == tuple(model.get_coords()) # flow_rate assert_var_equal( flow.submodel.flow_rate, model.add_variables( - lower=np.linspace(0.1, 0.5, timesteps.size) * 1e-5, - upper=np.linspace(0.5, 1, timesteps.size) * 100, - coords=(timesteps,), + lower=flow.relative_minimum * 1e-5, + upper=flow.relative_maximum * 100, + coords=model.get_coords(), ), ) assert_conequal( model.constraints['Sink(Wärme)|flow_rate|lb'], flow.submodel.variables['Sink(Wärme)|flow_rate'] - >= flow.submodel.variables['Sink(Wärme)|size'] - * xr.DataArray(np.linspace(0.1, 0.5, timesteps.size), coords=(timesteps,)), + >= flow.submodel.variables['Sink(Wärme)|size'] * flow.relative_minimum, ) assert_conequal( model.constraints['Sink(Wärme)|flow_rate|ub'], flow.submodel.variables['Sink(Wärme)|flow_rate'] - <= flow.submodel.variables['Sink(Wärme)|size'] - * xr.DataArray(np.linspace(0.5, 1, timesteps.size), coords=(timesteps,)), + <= flow.submodel.variables['Sink(Wärme)|size'] * flow.relative_maximum, ) def test_flow_invest_fixed_size(self, basic_flow_system_linopy_coords, coords_config): """Test flow with fixed size investment.""" flow_system, coords_config = basic_flow_system_linopy_coords, coords_config - timesteps = flow_system.timesteps flow = fx.Flow( 'Wärme', @@ -405,11 +435,14 @@ def test_flow_invest_fixed_size(self, basic_flow_system_linopy_coords, coords_co ) # Check that size is fixed to 75 - assert_var_equal(flow.submodel.variables['Sink(Wärme)|size'], model.add_variables(lower=75, upper=75)) + assert_var_equal( + flow.submodel.variables['Sink(Wärme)|size'], + model.add_variables(lower=75, upper=75, coords=model.get_coords(['year', 'scenario'])), + ) # Check flow rate bounds assert_var_equal( - flow.submodel.flow_rate, model.add_variables(lower=0.2 * 75, upper=0.9 * 75, coords=(timesteps,)) + flow.submodel.flow_rate, model.add_variables(lower=0.2 * 75, upper=0.9 * 75, coords=model.get_coords()) ) def test_flow_invest_with_effects(self, basic_flow_system_linopy_coords, coords_config): @@ -485,13 +518,13 @@ class TestFlowOnModel: def test_flow_on(self, basic_flow_system_linopy_coords, coords_config): flow_system, coords_config = basic_flow_system_linopy_coords, coords_config - timesteps = flow_system.timesteps + flow = fx.Flow( 'Wärme', bus='Fernwärme', size=100, - relative_minimum=xr.DataArray(0.2, coords=(timesteps,)), - relative_maximum=xr.DataArray(0.8, coords=(timesteps,)), + relative_minimum=0.2, + relative_maximum=0.8, on_off_parameters=fx.OnOffParameters(), ) flow_system.add_elements(fx.Sink('Sink', sink=flow)) @@ -519,18 +552,18 @@ def test_flow_on(self, basic_flow_system_linopy_coords, coords_config): model.add_variables( lower=0, upper=0.8 * 100, - coords=(timesteps,), + coords=model.get_coords(), ), ) # OnOff assert_var_equal( flow.submodel.on_off.on, - model.add_variables(binary=True, coords=(timesteps,)), + model.add_variables(binary=True, coords=model.get_coords()), ) assert_var_equal( model.variables['Sink(Wärme)|on_hours_total'], - model.add_variables(lower=0), + model.add_variables(lower=0, coords=model.get_coords(['year', 'scenario'])), ) assert_conequal( model.constraints['Sink(Wärme)|flow_rate|lb'], @@ -544,15 +577,15 @@ def test_flow_on(self, basic_flow_system_linopy_coords, coords_config): assert_conequal( model.constraints['Sink(Wärme)|on_hours_total'], flow.submodel.variables['Sink(Wärme)|on_hours_total'] - == (flow.submodel.variables['Sink(Wärme)|on'] * model.hours_per_step).sum(), + == (flow.submodel.variables['Sink(Wärme)|on'] * model.hours_per_step).sum('time'), ) def test_effects_per_running_hour(self, basic_flow_system_linopy_coords, coords_config): flow_system, coords_config = basic_flow_system_linopy_coords, coords_config timesteps = flow_system.timesteps - costs_per_running_hour = xr.DataArray(np.linspace(1, 2, timesteps.size), coords=(timesteps,)) - co2_per_running_hour = xr.DataArray(np.linspace(4, 5, timesteps.size), coords=(timesteps,)) + costs_per_running_hour = np.linspace(1, 2, timesteps.size) + co2_per_running_hour = np.linspace(4, 5, timesteps.size) flow = fx.Flow( 'Wärme', @@ -589,6 +622,12 @@ def test_effects_per_running_hour(self, basic_flow_system_linopy_coords, coords_ assert 'Sink(Wärme)->costs(operation)' in set(costs.submodel.constraints) assert 'Sink(Wärme)->CO2(operation)' in set(co2.submodel.constraints) + costs_per_running_hour = flow.on_off_parameters.effects_per_running_hour['costs'] + co2_per_running_hour = flow.on_off_parameters.effects_per_running_hour['CO2'] + + assert costs_per_running_hour.dims == tuple(model.get_coords()) + assert co2_per_running_hour.dims == tuple(model.get_coords()) + assert_conequal( model.constraints['Sink(Wärme)->costs(operation)'], model.variables['Sink(Wärme)->costs(operation)'] @@ -604,7 +643,6 @@ def test_effects_per_running_hour(self, basic_flow_system_linopy_coords, coords_ def test_consecutive_on_hours(self, basic_flow_system_linopy_coords, coords_config): """Test flow with minimum and maximum consecutive on hours.""" flow_system, coords_config = basic_flow_system_linopy_coords, coords_config - timesteps = flow_system.timesteps flow = fx.Flow( 'Wärme', @@ -642,7 +680,7 @@ def test_consecutive_on_hours(self, basic_flow_system_linopy_coords, coords_conf assert_var_equal( model.variables['Sink(Wärme)|consecutive_on_hours'], - model.add_variables(lower=0, upper=8, coords=(timesteps,)), + model.add_variables(lower=0, upper=8, coords=model.get_coords()), ) mega = model.hours_per_step.sum('time') @@ -687,7 +725,6 @@ def test_consecutive_on_hours(self, basic_flow_system_linopy_coords, coords_conf def test_consecutive_on_hours_previous(self, basic_flow_system_linopy_coords, coords_config): """Test flow with minimum and maximum consecutive on hours.""" flow_system, coords_config = basic_flow_system_linopy_coords, coords_config - timesteps = flow_system.timesteps flow = fx.Flow( 'Wärme', @@ -724,7 +761,7 @@ def test_consecutive_on_hours_previous(self, basic_flow_system_linopy_coords, co assert_var_equal( model.variables['Sink(Wärme)|consecutive_on_hours'], - model.add_variables(lower=0, upper=8, coords=(timesteps,)), + model.add_variables(lower=0, upper=8, coords=model.get_coords()), ) mega = model.hours_per_step.sum('time') + model.hours_per_step.isel(time=0) * 3 @@ -769,7 +806,6 @@ def test_consecutive_on_hours_previous(self, basic_flow_system_linopy_coords, co def test_consecutive_off_hours(self, basic_flow_system_linopy_coords, coords_config): """Test flow with minimum and maximum consecutive off hours.""" flow_system, coords_config = basic_flow_system_linopy_coords, coords_config - timesteps = flow_system.timesteps flow = fx.Flow( 'Wärme', @@ -807,7 +843,7 @@ def test_consecutive_off_hours(self, basic_flow_system_linopy_coords, coords_con assert_var_equal( model.variables['Sink(Wärme)|consecutive_off_hours'], - model.add_variables(lower=0, upper=12, coords=(timesteps,)), + model.add_variables(lower=0, upper=12, coords=model.get_coords()), ) mega = model.hours_per_step.sum('time') + model.hours_per_step.isel(time=0) * 1 # previously off for 1h @@ -852,7 +888,6 @@ def test_consecutive_off_hours(self, basic_flow_system_linopy_coords, coords_con def test_consecutive_off_hours_previous(self, basic_flow_system_linopy_coords, coords_config): """Test flow with minimum and maximum consecutive off hours.""" flow_system, coords_config = basic_flow_system_linopy_coords, coords_config - timesteps = flow_system.timesteps flow = fx.Flow( 'Wärme', @@ -891,7 +926,7 @@ def test_consecutive_off_hours_previous(self, basic_flow_system_linopy_coords, c assert_var_equal( model.variables['Sink(Wärme)|consecutive_off_hours'], - model.add_variables(lower=0, upper=12, coords=(timesteps,)), + model.add_variables(lower=0, upper=12, coords=model.get_coords()), ) mega = model.hours_per_step.sum('time') + model.hours_per_step.isel(time=0) * 2 @@ -974,7 +1009,10 @@ def test_switch_on_constraints(self, basic_flow_system_linopy_coords, coords_con ) # Check switch_on_nr variable bounds - assert_var_equal(flow.submodel.variables['Sink(Wärme)|switch|count'], model.add_variables(lower=0, upper=5)) + assert_var_equal( + flow.submodel.variables['Sink(Wärme)|switch|count'], + model.add_variables(lower=0, upper=5, coords=model.get_coords(['year', 'scenario'])), + ) # Verify switch_on_nr constraint (limits number of startups) assert_conequal( @@ -1017,14 +1055,15 @@ def test_on_hours_limits(self, basic_flow_system_linopy_coords, coords_config): # Check on_hours_total variable bounds assert_var_equal( - flow.submodel.variables['Sink(Wärme)|on_hours_total'], model.add_variables(lower=20, upper=100) + flow.submodel.variables['Sink(Wärme)|on_hours_total'], + model.add_variables(lower=20, upper=100, coords=model.get_coords(['year', 'scenario'])), ) # Check on_hours_total constraint assert_conequal( model.constraints['Sink(Wärme)|on_hours_total'], flow.submodel.variables['Sink(Wärme)|on_hours_total'] - == (flow.submodel.variables['Sink(Wärme)|on'] * model.hours_per_step).sum(), + == (flow.submodel.variables['Sink(Wärme)|on'] * model.hours_per_step).sum('time'), ) @@ -1033,13 +1072,12 @@ class TestFlowOnInvestModel: def test_flow_on_invest_optional(self, basic_flow_system_linopy_coords, coords_config): flow_system, coords_config = basic_flow_system_linopy_coords, coords_config - timesteps = flow_system.timesteps flow = fx.Flow( 'Wärme', bus='Fernwärme', size=fx.InvestParameters(minimum_size=20, maximum_size=200, optional=True), - relative_minimum=xr.DataArray(0.2, coords=(timesteps,)), - relative_maximum=xr.DataArray(0.8, coords=(timesteps,)), + relative_minimum=0.2, + relative_maximum=0.8, on_off_parameters=fx.OnOffParameters(), ) flow_system.add_elements(fx.Sink('Sink', sink=flow)) @@ -1079,18 +1117,18 @@ def test_flow_on_invest_optional(self, basic_flow_system_linopy_coords, coords_c model.add_variables( lower=0, upper=0.8 * 200, - coords=(timesteps,), + coords=model.get_coords(), ), ) # OnOff assert_var_equal( flow.submodel.on_off.on, - model.add_variables(binary=True, coords=(timesteps,)), + model.add_variables(binary=True, coords=model.get_coords()), ) assert_var_equal( model.variables['Sink(Wärme)|on_hours_total'], - model.add_variables(lower=0), + model.add_variables(lower=0, coords=model.get_coords(['year', 'scenario'])), ) assert_conequal( model.constraints['Sink(Wärme)|size|lb'], @@ -1111,11 +1149,14 @@ def test_flow_on_invest_optional(self, basic_flow_system_linopy_coords, coords_c assert_conequal( model.constraints['Sink(Wärme)|on_hours_total'], flow.submodel.variables['Sink(Wärme)|on_hours_total'] - == (flow.submodel.variables['Sink(Wärme)|on'] * model.hours_per_step).sum(), + == (flow.submodel.variables['Sink(Wärme)|on'] * model.hours_per_step).sum('time'), ) # Investment - assert_var_equal(model['Sink(Wärme)|size'], model.add_variables(lower=0, upper=200)) + assert_var_equal( + model['Sink(Wärme)|size'], + model.add_variables(lower=0, upper=200, coords=model.get_coords(['year', 'scenario'])), + ) mega = 0.2 * 200 # Relative minimum * maximum size assert_conequal( @@ -1132,13 +1173,12 @@ def test_flow_on_invest_optional(self, basic_flow_system_linopy_coords, coords_c def test_flow_on_invest_non_optional(self, basic_flow_system_linopy_coords, coords_config): flow_system, coords_config = basic_flow_system_linopy_coords, coords_config - timesteps = flow_system.timesteps flow = fx.Flow( 'Wärme', bus='Fernwärme', size=fx.InvestParameters(minimum_size=20, maximum_size=200, optional=False), - relative_minimum=xr.DataArray(0.2, coords=(timesteps,)), - relative_maximum=xr.DataArray(0.8, coords=(timesteps,)), + relative_minimum=0.2, + relative_maximum=0.8, on_off_parameters=fx.OnOffParameters(), ) flow_system.add_elements(fx.Sink('Sink', sink=flow)) @@ -1175,18 +1215,18 @@ def test_flow_on_invest_non_optional(self, basic_flow_system_linopy_coords, coor model.add_variables( lower=0, upper=0.8 * 200, - coords=(timesteps,), + coords=model.get_coords(), ), ) # OnOff assert_var_equal( flow.submodel.on_off.on, - model.add_variables(binary=True, coords=(timesteps,)), + model.add_variables(binary=True, coords=model.get_coords()), ) assert_var_equal( model.variables['Sink(Wärme)|on_hours_total'], - model.add_variables(lower=0), + model.add_variables(lower=0, coords=model.get_coords(['year', 'scenario'])), ) assert_conequal( model.constraints['Sink(Wärme)|flow_rate|lb1'], @@ -1199,11 +1239,14 @@ def test_flow_on_invest_non_optional(self, basic_flow_system_linopy_coords, coor assert_conequal( model.constraints['Sink(Wärme)|on_hours_total'], flow.submodel.variables['Sink(Wärme)|on_hours_total'] - == (flow.submodel.variables['Sink(Wärme)|on'] * model.hours_per_step).sum(), + == (flow.submodel.variables['Sink(Wärme)|on'] * model.hours_per_step).sum('time'), ) # Investment - assert_var_equal(model['Sink(Wärme)|size'], model.add_variables(lower=20, upper=200)) + assert_var_equal( + model['Sink(Wärme)|size'], + model.add_variables(lower=20, upper=200, coords=model.get_coords(['year', 'scenario'])), + ) mega = 0.2 * 200 # Relative minimum * maximum size assert_conequal( @@ -1231,7 +1274,10 @@ def test_fixed_relative_profile(self, basic_flow_system_linopy_coords, coords_co profile = np.sin(np.linspace(0, 2 * np.pi, len(timesteps))) * 0.5 + 0.5 # Values between 0 and 1 flow = fx.Flow( - 'Wärme', bus='Fernwärme', size=100, fixed_relative_profile=xr.DataArray(profile, coords=(timesteps,)) + 'Wärme', + bus='Fernwärme', + size=100, + fixed_relative_profile=profile, ) flow_system.add_elements(fx.Sink('Sink', sink=flow)) @@ -1239,7 +1285,11 @@ def test_fixed_relative_profile(self, basic_flow_system_linopy_coords, coords_co assert_var_equal( flow.submodel.variables['Sink(Wärme)|flow_rate'], - model.add_variables(lower=profile * 100, upper=profile * 100, coords=(timesteps,)), + model.add_variables( + lower=flow.fixed_relative_profile * 100, + upper=flow.fixed_relative_profile * 100, + coords=model.get_coords(), + ), ) def test_fixed_profile_with_investment(self, basic_flow_system_linopy_coords, coords_config): @@ -1254,7 +1304,7 @@ def test_fixed_profile_with_investment(self, basic_flow_system_linopy_coords, co 'Wärme', bus='Fernwärme', size=fx.InvestParameters(minimum_size=50, maximum_size=200, optional=True), - fixed_relative_profile=xr.DataArray(profile, coords=(timesteps,)), + fixed_relative_profile=profile, ) flow_system.add_elements(fx.Sink('Sink', sink=flow)) @@ -1262,14 +1312,14 @@ def test_fixed_profile_with_investment(self, basic_flow_system_linopy_coords, co assert_var_equal( flow.submodel.variables['Sink(Wärme)|flow_rate'], - model.add_variables(lower=0, upper=profile * 200, coords=(timesteps,)), + model.add_variables(lower=0, upper=flow.fixed_relative_profile * 200, coords=model.get_coords()), ) # The constraint should link flow_rate to size * profile assert_conequal( model.constraints['Sink(Wärme)|flow_rate|fixed'], flow.submodel.variables['Sink(Wärme)|flow_rate'] - == flow.submodel.variables['Sink(Wärme)|size'] * xr.DataArray(profile, coords=(timesteps,)), + == flow.submodel.variables['Sink(Wärme)|size'] * flow.fixed_relative_profile, ) From 262e8b4f3a69125dc890a2c6815e3c1f5f9cbfe4 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 28 Jul 2025 21:33:45 +0200 Subject: [PATCH 268/448] Fix test with multiple dims --- tests/test_linear_converter.py | 24 ++++++++++-------- tests/test_storage.py | 46 ++++++++++++++++++++-------------- 2 files changed, 40 insertions(+), 30 deletions(-) diff --git a/tests/test_linear_converter.py b/tests/test_linear_converter.py index e90d52f40..62e5cbcad 100644 --- a/tests/test_linear_converter.py +++ b/tests/test_linear_converter.py @@ -176,7 +176,7 @@ def test_linear_converter_with_on_off(self, basic_flow_system_linopy_coords, coo assert_conequal( model.constraints['Converter|on_hours_total'], model.variables['Converter|on_hours_total'] - == (model.variables['Converter|on'] * model.hours_per_step).sum(), + == (model.variables['Converter|on'] * model.hours_per_step).sum('time'), ) # Check conversion constraint @@ -282,16 +282,19 @@ def test_edge_case_time_varying_conversion(self, basic_flow_system_linopy_coords # Check that the correct constraint was created assert 'VariableConverter|conversion_0' in model.constraints + factor = converter.conversion_factors[0]['electricity'] + + assert factor.dims == tuple(model.get_coords()) + # Verify the constraint has the time-varying coefficient assert_conequal( model.constraints['VariableConverter|conversion_0'], - input_flow.submodel.flow_rate * fluctuating_cop == output_flow.submodel.flow_rate * 1.0, + input_flow.submodel.flow_rate * factor == output_flow.submodel.flow_rate * 1.0, ) def test_piecewise_conversion(self, basic_flow_system_linopy_coords, coords_config): """Test a LinearConverter with PiecewiseConversion.""" flow_system, coords_config = basic_flow_system_linopy_coords, coords_config - timesteps = flow_system.timesteps # Create input and output flows input_flow = fx.Flow('input', bus='input_bus', size=100) @@ -339,9 +342,9 @@ def test_piecewise_conversion(self, basic_flow_system_linopy_coords, coords_conf lambda1 = model.variables[f'Converter|Piece_{i}|lambda1'] inside_piece = model.variables[f'Converter|Piece_{i}|inside_piece'] - assert_var_equal(inside_piece, model.add_variables(binary=True, coords=(timesteps,))) - assert_var_equal(lambda0, model.add_variables(lower=0, upper=1, coords=(timesteps,))) - assert_var_equal(lambda1, model.add_variables(lower=0, upper=1, coords=(timesteps,))) + assert_var_equal(inside_piece, model.add_variables(binary=True, coords=model.get_coords())) + assert_var_equal(lambda0, model.add_variables(lower=0, upper=1, coords=model.get_coords())) + assert_var_equal(lambda1, model.add_variables(lower=0, upper=1, coords=model.get_coords())) # Check that the inside_piece constraint exists assert f'Converter|Piece_{i}|inside_piece' in model.constraints @@ -380,7 +383,6 @@ def test_piecewise_conversion(self, basic_flow_system_linopy_coords, coords_conf def test_piecewise_conversion_with_onoff(self, basic_flow_system_linopy_coords, coords_config): """Test a LinearConverter with PiecewiseConversion and OnOffParameters.""" flow_system, coords_config = basic_flow_system_linopy_coords, coords_config - timesteps = flow_system.timesteps # Create input and output flows input_flow = fx.Flow('input', bus='input_bus', size=100) @@ -444,9 +446,9 @@ def test_piecewise_conversion_with_onoff(self, basic_flow_system_linopy_coords, lambda1 = model.variables[f'Converter|Piece_{i}|lambda1'] inside_piece = model.variables[f'Converter|Piece_{i}|inside_piece'] - assert_var_equal(inside_piece, model.add_variables(binary=True, coords=(timesteps,))) - assert_var_equal(lambda0, model.add_variables(lower=0, upper=1, coords=(timesteps,))) - assert_var_equal(lambda1, model.add_variables(lower=0, upper=1, coords=(timesteps,))) + assert_var_equal(inside_piece, model.add_variables(binary=True, coords=model.get_coords())) + assert_var_equal(lambda0, model.add_variables(lower=0, upper=1, coords=model.get_coords())) + assert_var_equal(lambda1, model.add_variables(lower=0, upper=1, coords=model.get_coords())) # Check that the inside_piece constraint exists assert f'Converter|Piece_{i}|inside_piece' in model.constraints @@ -485,7 +487,7 @@ def test_piecewise_conversion_with_onoff(self, basic_flow_system_linopy_coords, assert 'Converter|on_hours_total' in model.constraints assert_conequal( model.constraints['Converter|on_hours_total'], - model['Converter|on_hours_total'] == (model['Converter|on'] * model.hours_per_step).sum(), + model['Converter|on_hours_total'] == (model['Converter|on'] * model.hours_per_step).sum('time'), ) # Verify that the costs effect is applied diff --git a/tests/test_storage.py b/tests/test_storage.py index 479f66a87..76226be3b 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -14,8 +14,6 @@ class TestStorageModel: def test_basic_storage(self, basic_flow_system_linopy_coords, coords_config): """Test that basic storage model variables and constraints are correctly generated.""" flow_system, coords_config = basic_flow_system_linopy_coords, coords_config - timesteps = flow_system.timesteps - timesteps_extra = flow_system.timesteps_extra # Create a simple storage storage = fx.Storage( @@ -55,13 +53,14 @@ def test_basic_storage(self, basic_flow_system_linopy_coords, coords_config): # Check variable properties assert_var_equal( - model['TestStorage(Q_th_in)|flow_rate'], model.add_variables(lower=0, upper=20, coords=(timesteps,)) + model['TestStorage(Q_th_in)|flow_rate'], model.add_variables(lower=0, upper=20, coords=model.get_coords()) ) assert_var_equal( - model['TestStorage(Q_th_out)|flow_rate'], model.add_variables(lower=0, upper=20, coords=(timesteps,)) + model['TestStorage(Q_th_out)|flow_rate'], model.add_variables(lower=0, upper=20, coords=model.get_coords()) ) assert_var_equal( - model['TestStorage|charge_state'], model.add_variables(lower=0, upper=30, coords=(timesteps_extra,)) + model['TestStorage|charge_state'], + model.add_variables(lower=0, upper=30, coords=model.get_coords(extra_timestep=True)), ) # Check constraint formulations @@ -88,8 +87,6 @@ def test_basic_storage(self, basic_flow_system_linopy_coords, coords_config): def test_lossy_storage(self, basic_flow_system_linopy_coords, coords_config): """Test that basic storage model variables and constraints are correctly generated.""" flow_system, coords_config = basic_flow_system_linopy_coords, coords_config - timesteps = flow_system.timesteps - timesteps_extra = flow_system.timesteps_extra # Create a simple storage storage = fx.Storage( @@ -132,13 +129,14 @@ def test_lossy_storage(self, basic_flow_system_linopy_coords, coords_config): # Check variable properties assert_var_equal( - model['TestStorage(Q_th_in)|flow_rate'], model.add_variables(lower=0, upper=20, coords=(timesteps,)) + model['TestStorage(Q_th_in)|flow_rate'], model.add_variables(lower=0, upper=20, coords=model.get_coords()) ) assert_var_equal( - model['TestStorage(Q_th_out)|flow_rate'], model.add_variables(lower=0, upper=20, coords=(timesteps,)) + model['TestStorage(Q_th_out)|flow_rate'], model.add_variables(lower=0, upper=20, coords=model.get_coords()) ) assert_var_equal( - model['TestStorage|charge_state'], model.add_variables(lower=0, upper=30, coords=(timesteps_extra,)) + model['TestStorage|charge_state'], + model.add_variables(lower=0, upper=30, coords=model.get_coords(extra_timestep=True)), ) # Check constraint formulations @@ -173,8 +171,6 @@ def test_lossy_storage(self, basic_flow_system_linopy_coords, coords_config): def test_charge_state_bounds(self, basic_flow_system_linopy_coords, coords_config): """Test that basic storage model variables and constraints are correctly generated.""" flow_system, coords_config = basic_flow_system_linopy_coords, coords_config - timesteps = flow_system.timesteps - timesteps_extra = flow_system.timesteps_extra # Create a simple storage storage = fx.Storage( @@ -216,17 +212,23 @@ def test_charge_state_bounds(self, basic_flow_system_linopy_coords, coords_confi # Check variable properties assert_var_equal( - model['TestStorage(Q_th_in)|flow_rate'], model.add_variables(lower=0, upper=20, coords=(timesteps,)) + model['TestStorage(Q_th_in)|flow_rate'], model.add_variables(lower=0, upper=20, coords=model.get_coords()) ) assert_var_equal( - model['TestStorage(Q_th_out)|flow_rate'], model.add_variables(lower=0, upper=20, coords=(timesteps,)) + model['TestStorage(Q_th_out)|flow_rate'], model.add_variables(lower=0, upper=20, coords=model.get_coords()) ) assert_var_equal( model['TestStorage|charge_state'], model.add_variables( - lower=np.array([0.07, 0.11, 0.15, 0.19, 0.23, 0.27, 0.31, 0.35, 0.39, 0.43, 0.43]) * 30, - upper=np.array([0.14, 0.22, 0.3, 0.38, 0.46, 0.54, 0.62, 0.7, 0.78, 0.86, 0.86]) * 30, - coords=(timesteps_extra,), + lower=storage.relative_minimum_charge_state.reindex( + time=model.get_coords(extra_timestep=True)['time'] + ).ffill('time') + * 30, + upper=storage.relative_maximum_charge_state.reindex( + time=model.get_coords(extra_timestep=True)['time'] + ).ffill('time') + * 30, + coords=model.get_coords(extra_timestep=True), ), ) @@ -286,8 +288,14 @@ def test_storage_with_investment(self, basic_flow_system_linopy_coords, coords_c assert con_name in model.constraints, f'Missing investment constraint: {con_name}' # Check variable properties - assert_var_equal(model['InvestStorage|size'], model.add_variables(lower=0, upper=100)) - assert_var_equal(model['InvestStorage|is_invested'], model.add_variables(binary=True)) + assert_var_equal( + model['InvestStorage|size'], + model.add_variables(lower=0, upper=100, coords=model.get_coords(['year', 'scenario'])), + ) + assert_var_equal( + model['InvestStorage|is_invested'], + model.add_variables(binary=True, coords=model.get_coords(['year', 'scenario'])), + ) assert_conequal( model.constraints['InvestStorage|size|ub'], model.variables['InvestStorage|size'] <= model.variables['InvestStorage|is_invested'] * 100, From 2a469f19f2d39f355d77f34571f5987cf30f5b10 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 28 Jul 2025 21:36:33 +0200 Subject: [PATCH 269/448] Fix test with multiple dims --- tests/test_component.py | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/tests/test_component.py b/tests/test_component.py index 2ed9bea3c..384a5ed28 100644 --- a/tests/test_component.py +++ b/tests/test_component.py @@ -74,7 +74,7 @@ def test_component(self, basic_flow_system_linopy_coords, coords_config): def test_on_with_multiple_flows(self, basic_flow_system_linopy_coords, coords_config): """Test that flow model constraints are correctly generated.""" flow_system, coords_config = basic_flow_system_linopy_coords, coords_config - timesteps = flow_system.timesteps + ub_out2 = np.linspace(1, 1.5, 10).round(2) inputs = [ fx.Flow('In1', 'Fernwärme', relative_minimum=np.ones(10) * 0.1, size=100), @@ -132,12 +132,16 @@ def test_on_with_multiple_flows(self, basic_flow_system_linopy_coords, coords_co msg='Incorrect constraints', ) + upper_bound_flow_rate = outputs[1].relative_maximum + + assert upper_bound_flow_rate.dims == tuple(model.get_coords()) + assert_var_equal( model['TestComponent(Out2)|flow_rate'], - model.add_variables(lower=0, upper=300 * ub_out2, coords=(timesteps,)), + model.add_variables(lower=0, upper=300 * upper_bound_flow_rate, coords=model.get_coords()), ) - assert_var_equal(model['TestComponent|on'], model.add_variables(binary=True, coords=(timesteps,))) - assert_var_equal(model['TestComponent(Out2)|on'], model.add_variables(binary=True, coords=(timesteps,))) + assert_var_equal(model['TestComponent|on'], model.add_variables(binary=True, coords=model.get_coords())) + assert_var_equal(model['TestComponent(Out2)|on'], model.add_variables(binary=True, coords=model.get_coords())) assert_conequal( model.constraints['TestComponent(Out2)|flow_rate|lb'], @@ -146,7 +150,7 @@ def test_on_with_multiple_flows(self, basic_flow_system_linopy_coords, coords_co assert_conequal( model.constraints['TestComponent(Out2)|flow_rate|ub'], model.variables['TestComponent(Out2)|flow_rate'] - <= model.variables['TestComponent(Out2)|on'] * 300 * ub_out2, + <= model.variables['TestComponent(Out2)|on'] * 300 * upper_bound_flow_rate, ) assert_conequal( @@ -173,7 +177,6 @@ def test_on_with_multiple_flows(self, basic_flow_system_linopy_coords, coords_co def test_on_with_single_flow(self, basic_flow_system_linopy_coords, coords_config): """Test that flow model constraints are correctly generated.""" flow_system, coords_config = basic_flow_system_linopy_coords, coords_config - timesteps = flow_system.timesteps inputs = [ fx.Flow('In1', 'Fernwärme', relative_minimum=np.ones(10) * 0.1, size=100), ] @@ -211,10 +214,10 @@ def test_on_with_single_flow(self, basic_flow_system_linopy_coords, coords_confi ) assert_var_equal( - model['TestComponent(In1)|flow_rate'], model.add_variables(lower=0, upper=100, coords=(timesteps,)) + model['TestComponent(In1)|flow_rate'], model.add_variables(lower=0, upper=100, coords=model.get_coords()) ) - assert_var_equal(model['TestComponent|on'], model.add_variables(binary=True, coords=(timesteps,))) - assert_var_equal(model['TestComponent(In1)|on'], model.add_variables(binary=True, coords=(timesteps,))) + assert_var_equal(model['TestComponent|on'], model.add_variables(binary=True, coords=model.get_coords())) + assert_var_equal(model['TestComponent(In1)|on'], model.add_variables(binary=True, coords=model.get_coords())) assert_conequal( model.constraints['TestComponent(In1)|flow_rate|lb'], @@ -233,7 +236,7 @@ def test_on_with_single_flow(self, basic_flow_system_linopy_coords, coords_confi def test_previous_states_with_multiple_flows(self, basic_flow_system_linopy_coords, coords_config): """Test that flow model constraints are correctly generated.""" flow_system, coords_config = basic_flow_system_linopy_coords, coords_config - timesteps = flow_system.timesteps + ub_out2 = np.linspace(1, 1.5, 10).round(2) inputs = [ fx.Flow( @@ -304,12 +307,16 @@ def test_previous_states_with_multiple_flows(self, basic_flow_system_linopy_coor msg='Incorrect constraints', ) + upper_bound_flow_rate = outputs[1].relative_maximum + + assert upper_bound_flow_rate.dims == tuple(model.get_coords()) + assert_var_equal( model['TestComponent(Out2)|flow_rate'], - model.add_variables(lower=0, upper=300 * ub_out2, coords=(timesteps,)), + model.add_variables(lower=0, upper=300 * upper_bound_flow_rate, coords=model.get_coords()), ) - assert_var_equal(model['TestComponent|on'], model.add_variables(binary=True, coords=(timesteps,))) - assert_var_equal(model['TestComponent(Out2)|on'], model.add_variables(binary=True, coords=(timesteps,))) + assert_var_equal(model['TestComponent|on'], model.add_variables(binary=True, coords=model.get_coords())) + assert_var_equal(model['TestComponent(Out2)|on'], model.add_variables(binary=True, coords=model.get_coords())) assert_conequal( model.constraints['TestComponent(Out2)|flow_rate|lb'], @@ -318,7 +325,7 @@ def test_previous_states_with_multiple_flows(self, basic_flow_system_linopy_coor assert_conequal( model.constraints['TestComponent(Out2)|flow_rate|ub'], model.variables['TestComponent(Out2)|flow_rate'] - <= model.variables['TestComponent(Out2)|on'] * 300 * ub_out2, + <= model.variables['TestComponent(Out2)|on'] * 300 * upper_bound_flow_rate, ) assert_conequal( From 159bcb3c11a6fd5165b0820a317f1d75032bd7c8 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 28 Jul 2025 21:36:53 +0200 Subject: [PATCH 270/448] New test --- tests/test_component.py | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/tests/test_component.py b/tests/test_component.py index 384a5ed28..1d1792a65 100644 --- a/tests/test_component.py +++ b/tests/test_component.py @@ -349,6 +349,47 @@ def test_previous_states_with_multiple_flows(self, basic_flow_system_linopy_coor + 1e-5, ) + def test_previous_states_with_multiple_flows_2(self, basic_flow_system_linopy_coords, coords_config): + """Test that flow model constraints are correctly generated.""" + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config + + ub_out2 = np.linspace(1, 1.5, 10).round(2) + inputs = [ + fx.Flow( + 'In1', + 'Fernwärme', + relative_minimum=np.ones(10) * 0.1, + size=100, + previous_flow_rate=np.array([0, 0, 1e-6, 1e-5, 1e-4, 3, 4]), + on_off_parameters=fx.OnOffParameters(consecutive_on_hours_min=3), + ), + ] + outputs = [ + fx.Flow('Out1', 'Gas', relative_minimum=np.ones(10) * 0.2, size=200, previous_flow_rate=[3, 4, 5]), + fx.Flow( + 'Out2', + 'Gas', + relative_minimum=np.ones(10) * 0.3, + relative_maximum=ub_out2, + size=300, + previous_flow_rate=20, + ), + ] + comp = flixopt.elements.Component( + 'TestComponent', + inputs=inputs, + outputs=outputs, + on_off_parameters=fx.OnOffParameters(consecutive_on_hours_min=3), + ) + flow_system.add_elements(comp) + create_linopy_model(flow_system) + + assert_conequal( + comp.submodel.constraints['TestComponent|consecutive_on_hours|initial'], + comp.submodel.variables['TestComponent|consecutive_on_hours'].isel(time=0) + == comp.submodel.variables['TestComponent|on'].isel(time=0) * 5, + ) + class TestTransmissionModel: def test_transmission_basic(self, basic_flow_system, highs_solver): From e764a1392113cce01f0726d7bc76be88c6c4fdd9 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 28 Jul 2025 21:45:33 +0200 Subject: [PATCH 271/448] New test for previous flow_rates --- flixopt/elements.py | 4 +++- tests/test_component.py | 32 ++++++++++++++++++++++++++------ 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/flixopt/elements.py b/flixopt/elements.py index 499ab66db..e1c0fcbc3 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -199,6 +199,7 @@ def __init__( If the load-profile is just an upper limit, use relative_maximum instead. previous_flow_rate: previous flow rate of the flow. Used to determine if and how long the flow is already on / off. If None, the flow is considered to be off for one timestep. + Currently does not support different values in different years or scenarios! meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types. """ super().__init__(label, meta_data=meta_data) @@ -305,7 +306,8 @@ def _plausibility_checks(self) -> None: ] ): raise TypeError( - f'previous_flow_rate must be None, a scalar, a list of scalars or a 1D-numpy-array. Got {type(self.previous_flow_rate)}' + f'previous_flow_rate must be None, a scalar, a list of scalars or a 1D-numpy-array. Got {type(self.previous_flow_rate)}.' + f'Different values in different years or scenarios are not yetsupported.' ) @property diff --git a/tests/test_component.py b/tests/test_component.py index 1d1792a65..ab05db5e8 100644 --- a/tests/test_component.py +++ b/tests/test_component.py @@ -349,8 +349,26 @@ def test_previous_states_with_multiple_flows(self, basic_flow_system_linopy_coor + 1e-5, ) - def test_previous_states_with_multiple_flows_2(self, basic_flow_system_linopy_coords, coords_config): - """Test that flow model constraints are correctly generated.""" + @pytest.mark.parametrize( + 'in1_previous_flow_rate, out1_previous_flow_rate, out2_previous_flow_rate, previous_on_hours', + [ + (None, None, None, 0), + (np.array([0, 1e-6, 1e-4, 5]), None, None, 2), + (np.array([0, 5, 0, 5]), None, None, 1), + (np.array([0, 5, 0, 0]), 3, 0, 1), + (np.array([0, 0, 2, 0, 4, 5]), [3, 4, 5], None, 4), + ], + ) + def test_previous_states_with_multiple_flows_parameterized( + self, + basic_flow_system_linopy_coords, + coords_config, + in1_previous_flow_rate, + out1_previous_flow_rate, + out2_previous_flow_rate, + previous_on_hours, + ): + """Test that flow model constraints are correctly generated with different previous flow rates and constraint factors.""" flow_system, coords_config = basic_flow_system_linopy_coords, coords_config ub_out2 = np.linspace(1, 1.5, 10).round(2) @@ -360,19 +378,21 @@ def test_previous_states_with_multiple_flows_2(self, basic_flow_system_linopy_co 'Fernwärme', relative_minimum=np.ones(10) * 0.1, size=100, - previous_flow_rate=np.array([0, 0, 1e-6, 1e-5, 1e-4, 3, 4]), + previous_flow_rate=in1_previous_flow_rate, on_off_parameters=fx.OnOffParameters(consecutive_on_hours_min=3), ), ] outputs = [ - fx.Flow('Out1', 'Gas', relative_minimum=np.ones(10) * 0.2, size=200, previous_flow_rate=[3, 4, 5]), + fx.Flow( + 'Out1', 'Gas', relative_minimum=np.ones(10) * 0.2, size=200, previous_flow_rate=out1_previous_flow_rate + ), fx.Flow( 'Out2', 'Gas', relative_minimum=np.ones(10) * 0.3, relative_maximum=ub_out2, size=300, - previous_flow_rate=20, + previous_flow_rate=out2_previous_flow_rate, ), ] comp = flixopt.elements.Component( @@ -387,7 +407,7 @@ def test_previous_states_with_multiple_flows_2(self, basic_flow_system_linopy_co assert_conequal( comp.submodel.constraints['TestComponent|consecutive_on_hours|initial'], comp.submodel.variables['TestComponent|consecutive_on_hours'].isel(time=0) - == comp.submodel.variables['TestComponent|on'].isel(time=0) * 5, + == comp.submodel.variables['TestComponent|on'].isel(time=0) * (previous_on_hours + 1), ) From 2c1c7501a674e6e0328894e1772817c4b6f22383 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 29 Jul 2025 14:52:49 +0200 Subject: [PATCH 272/448] Add Model for YearAwareInvestments --- flixopt/features.py | 108 +++++++++++++++++++++++++++++++++++++++++++- flixopt/modeling.py | 66 +++++++++++++++++++++++++++ 2 files changed, 172 insertions(+), 2 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index d07844c8d..fd48e0654 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -19,8 +19,6 @@ class InvestmentModel(Submodel): - """Investment model using factory patterns but keeping old interface""" - def __init__( self, model: FlowSystemModel, @@ -126,6 +124,112 @@ def is_invested(self) -> Optional[linopy.Variable]: return self._variables['is_invested'] +class YearAwareInvestmentModel(Submodel): + def __init__( + self, + model: FlowSystemModel, + label_of_element: str, + parameters: InvestParameters, + label_of_model: Optional[str] = None, + ): + """ + This feature model is used to model the investment of a variable. + It applies the corresponding bounds to the variable and the on/off state of the variable. + + Args: + model: The optimization model instance + label_of_element: The label of the parent (Element). Used to construct the full label of the model. + parameters: The parameters of the feature model. + label_of_model: The label of the model. This is needed to construct the full label of the model. + + """ + self.piecewise_effects: Optional[PiecewiseEffectsModel] = None + self.parameters = parameters + super().__init__(model, label_of_element=label_of_element, label_of_model=label_of_model) + + def _do_modeling(self): + self._basic_modeling() + self._custom_modeling() + + def _basic_modeling(self): + size_min, size_max = (self.parameters.minimum_or_fixed_size, self.parameters.maximum_or_fixed_size) + self.add_variables( + short_name='size', + lower=0 if self.parameters.optional else size_min, + upper=size_max, + coords=self._model.get_coords(['year', 'scenario']), + ) + + if self.parameters.optional or self.parameters.year_aware: + self.add_variables( + binary=True, + coords=self._model.get_coords(['year', 'scenario']), + short_name='is_invested', + ) + + BoundingPatterns.bounds_with_state( + self, + variable=self.size, + variable_state=self.is_invested, + bounds=(self.parameters.minimum_or_fixed_size, self.parameters.maximum_or_fixed_size), + ) + + if self.parameters.year_aware: + # Track when the investment/divestment happens + increase = self.add_variables( + binary=True, + coords=self._model.get_coords(['year', 'scenario']), + short_name='increase', + ) + decrease = self.add_variables( + binary=True, + coords=self._model.get_coords(['year', 'scenario']), + short_name='decrease', + ) + BoundingPatterns.state_transition_bounds( + self, + state_variable=self.is_invested, + switch_on=increase, + switch_off=decrease, + name=self.is_invested.name, + previous_state=0, + coord='year', + ) + + self.add_constraints(increase.sum('year') <= 1, name=f'{increase.name}|count') + self.add_constraints(decrease.sum('year') <= 1, name=f'{decrease.name}|count') + + # Ensures size can only change when increase or decrease is 1 + BoundingPatterns.continuous_transition_bounds( + model=self, + continuous_variable=self.size, + switch_on=increase, + switch_off=decrease, + name=self.size.name, + max_change=size_max, + previous_value=0, + coord='year', + ) + + super()._do_modeling() + + def _custom_modeling(self): + # Add usefull constraints for investment decisions, parameterized by the user + pass + + @property + def size(self) -> linopy.Variable: + """Investment size variable""" + return self._variables['size'] + + @property + def is_invested(self) -> Optional[linopy.Variable]: + """Binary investment decision variable""" + if 'is_invested' not in self._variables: + return None + return self._variables['is_invested'] + + class OnOffModel(Submodel): """OnOff model using factory patterns""" diff --git a/flixopt/modeling.py b/flixopt/modeling.py index 11880c5e8..874ef1c0f 100644 --- a/flixopt/modeling.py +++ b/flixopt/modeling.py @@ -588,3 +588,69 @@ def state_transition_bounds( mutex = model.add_constraints(switch_on + switch_off <= 1, name=f'{name}|mutex') return transition, initial, mutex + + @staticmethod + def continuous_transition_bounds( + model: Submodel, + continuous_variable: linopy.Variable, + switch_on: linopy.Variable, + switch_off: linopy.Variable, + name: str, + max_change: Union[float, xr.DataArray], + previous_value: Union[float, xr.DataArray] = 0, + coord: str = 'time', + ) -> Tuple[linopy.Constraint, linopy.Constraint, linopy.Constraint, linopy.Constraint]: + """ + Constrains a continuous variable to only change when switch variables are active. + + Mathematical formulation: + -max_change * (switch_on[t] + switch_off[t]) <= continuous[t] - continuous[t-1] <= max_change * (switch_on[t] + switch_off[t]) ∀t > 0 + -max_change * (switch_on[0] + switch_off[0]) <= continuous[0] - previous_value <= max_change * (switch_on[0] + switch_off[0]) + switch_on[t], switch_off[t] ∈ {0, 1} + + This ensures the continuous variable can only change when switch_on or switch_off is 1. + When both switches are 0, the variable must stay exactly constant. + + Args: + model: The submodel to add constraints to + continuous_variable: The continuous variable to constrain + switch_on: Binary variable indicating when changes are allowed (typically transitions to active state) + switch_off: Binary variable indicating when changes are allowed (typically transitions to inactive state) + name: Base name for the constraints + max_change: Maximum possible change in the continuous variable (Big-M value) + previous_value: Initial value of the continuous variable before first period + coord: Coordinate name for time dimension + + Returns: + Tuple of constraints: (transition_upper, transition_lower, initial_upper, initial_lower) + """ + if not isinstance(model, Submodel): + raise ValueError('BoundingPatterns.continuous_transition_bounds() can only be used with a Submodel') + + # Transition constraints for t > 0: continuous variable can only change when switches are active + transition_upper = model.add_constraints( + continuous_variable.isel({coord: slice(1, None)}) - continuous_variable.isel({coord: slice(None, -1)}) + <= max_change * (switch_on.isel({coord: slice(1, None)}) + switch_off.isel({coord: slice(1, None)})), + name=f'{name}|transition_ub', + ) + + transition_lower = model.add_constraints( + continuous_variable.isel({coord: slice(None, -1)}) - continuous_variable.isel({coord: slice(1, None)}) + <= max_change * (switch_on.isel({coord: slice(1, None)}) + switch_off.isel({coord: slice(1, None)})), + name=f'{name}|transition_lb', + ) + + # Initial constraints for t = 0 + initial_upper = model.add_constraints( + continuous_variable.isel({coord: 0}) - previous_value + <= max_change * (switch_on.isel({coord: 0}) + switch_off.isel({coord: 0})), + name=f'{name}|initial_ub', + ) + + initial_lower = model.add_constraints( + previous_value - continuous_variable.isel({coord: 0}) + <= max_change * (switch_on.isel({coord: 0}) + switch_off.isel({coord: 0})), + name=f'{name}|initial_lb', + ) + + return transition_upper, transition_lower, initial_upper, initial_lower From 70979fb89bd1d1fbe37c1ecf62a9a850d3066113 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 29 Jul 2025 15:46:03 +0200 Subject: [PATCH 273/448] Add FlowSystem.years_per_year attribute and "years_of_last_year" parameter to FlowSystem() --- flixopt/flow_system.py | 14 ++++ flixopt/interface.py | 158 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 172 insertions(+) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index dd202114e..3cead49e0 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -63,6 +63,7 @@ def __init__( scenarios: Optional[pd.Index] = None, hours_of_last_timestep: Optional[float] = None, hours_of_previous_timesteps: Optional[Union[int, float, np.ndarray]] = None, + years_of_last_year: Optional[int] = None, weights: Optional[NonTemporalDataUser] = None, ): """ @@ -86,6 +87,8 @@ def __init__( self.years = None if years is None else self._validate_years(years) + self.years_per_year = None if years is None else self.calculate_years_per_year(years, years_of_last_year) + self.scenarios = None if scenarios is None else self._validate_scenarios(scenarios) self.weights = weights @@ -176,6 +179,17 @@ def calculate_hours_per_timestep(timesteps_extra: pd.DatetimeIndex) -> xr.DataAr hours_per_step, coords={'time': timesteps_extra[:-1]}, dims='time', name='hours_per_timestep' ) + @staticmethod + def calculate_years_per_year(years: pd.Index, years_of_last_year: Optional[int] = None) -> xr.DataArray: + """Calculate duration of each timestep as a 1D DataArray.""" + years_per_year = np.diff(years) + return xr.DataArray( + np.append(years_per_year, years_of_last_year or years_per_year[-1]), + coords={'year': years}, + dims='year', + name='years_per_year', + ) + @staticmethod def _calculate_hours_of_previous_timesteps( timesteps: pd.DatetimeIndex, hours_of_previous_timesteps: Optional[Union[float, np.ndarray]] diff --git a/flixopt/interface.py b/flixopt/interface.py index cae1757c7..f981cd121 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -242,6 +242,164 @@ def maximum_or_fixed_size(self) -> NonTemporalData: return self.fixed_size if self.fixed_size is not None else self.maximum_size +@register_class_for_io +class YearAwareInvestParameters(Interface): + """ + Parameters for year-aware investment modeling with multi-year optimization and timing constraints. + + This interface supports investment decisions that can change over time, with constraints on + when investments can be made, modified, or removed. Useful for modeling: + - Capacity expansion planning over multiple years + - Technology replacement strategies + - Retrofit and upgrade scheduling + - Investment timing optimization with changing costs/benefits + """ + + def __init__( + self, + # Basic sizing parameters + minimum_size: Optional[Scalar] = None, + maximum_size: Optional[Scalar] = None, + fixed_size: Optional[Scalar] = None, + previous_size: Scalar = 0, + # Timing constraints - flexible combinations + fixed_start_year: Optional[int] = None, + fixed_end_year: Optional[int] = None, + fixed_duration: Optional[int] = None, + earliest_start_year: Optional[int] = None, + latest_start_year: Optional[int] = None, + earliest_end_year: Optional[int] = None, + latest_end_year: Optional[int] = None, + minimum_duration: Optional[int] = None, + maximum_duration: Optional[int] = None, + # Direct effects + effects_of_investment_per_size: Optional['NonTemporalEffectsUser'] = None, + effects_of_investment: Optional['NonTemporalEffectsUser'] = None, + # Divestment constraints + allow_divestment: bool = False, + effects_of_divestment_per_size: Optional['NonTemporalEffectsUser'] = None, + effects_of_divestment: Optional['NonTemporalEffectsUser'] = None, + ): + """ + Initialize year-aware investment parameters. + + Args: + minimum_size: Minimum investment size when invested. Defaults to CONFIG.modeling.EPSILON. + maximum_size: Maximum possible investment size. Defaults to CONFIG.modeling.BIG. + fixed_size: If specified, investment size is fixed to this value (if investment is taken). + previous_size: THe size previous to the evaluated period (relevant for divestment decisions). + + fixed_start_year: If specified, investment must start in this exact year. + fixed_end_year: If specified, investment must end in this exact year. + fixed_duration: If specified, investment must last exactly this many years. + earliest_start_year: Earliest year investment can start. + latest_start_year: Latest year investment can start. + earliest_end_year: Earliest year investment can end. + latest_end_year: Latest year investment can end. + minimum_duration: Minimum duration investment must last (in years). + maximum_duration: Maximum duration investment can last (in years). + + effects_per_size: Effects applied per unit of investment size for each year invested. + Example: {'costs': 100} applies 100 * size * years_invested to total costs. + effects_per_investment: One-time effects applied when investment decision is made. + Example: {'costs': 1000} applies 1000 to costs in the investment year. + + allow_divestment: If True, investment can be terminated before the end of time horizon. + If False, once invested, remains invested until end of horizon. + effects_per_divestment: One-time effects applied when divestment decision is made. + Example: {'costs': 500} applies 500 to costs in the divestment year. + """ + self.minimum_size = minimum_size if minimum_size is not None else CONFIG.modeling.EPSILON + self.maximum_size = maximum_size if maximum_size is not None else CONFIG.modeling.BIG + self.fixed_size = fixed_size + + self.fixed_start_year = fixed_start_year + self.fixed_end_year = fixed_end_year + self.fixed_duration = fixed_duration + self.earliest_start_year = earliest_start_year + self.latest_start_year = latest_start_year + self.earliest_end_year = earliest_end_year + self.latest_end_year = latest_end_year + self.minimum_duration = minimum_duration + self.maximum_duration = maximum_duration + + self.effects_of_investment_per_size: 'NonTemporalEffectsUser' = ( + effects_of_investment_per_size if effects_of_investment_per_size is not None else {} + ) + self.effects_of_investment: 'NonTemporalEffectsUser' = ( + effects_of_investment if effects_of_investment is not None else {} + ) + + self.allow_divestment = allow_divestment + self.effects_of_divestment: 'NonTemporalEffectsUser' = ( + effects_of_divestment if effects_of_divestment is not None else {} + ) + self.effects_of_divestment_per_size: 'NonTemporalEffectsUser' = ( + effects_of_divestment_per_size if effects_of_divestment_per_size is not None else {} + ) + + def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): + """Transform all parameter data to match the flow system's coordinate structure.""" + self._plausibility_checks(flow_system) + + self.effects_of_investment_per_size = flow_system.fit_effects_to_model_coords( + label_prefix=name_prefix, + effect_values=self.effects_of_investment_per_size, + label_suffix='effects_of_investment_per_size', + has_time_dim=False, + ) + self.effects_of_investment = flow_system.fit_effects_to_model_coords( + label_prefix=name_prefix, + effect_values=self.effects_of_investment, + label_suffix='effects_of_investment', + has_time_dim=False, + ) + self.effects_of_divestment = flow_system.fit_effects_to_model_coords( + label_prefix=name_prefix, + effect_values=self.effects_of_divestment, + label_suffix='effects_of_divestment', + has_time_dim=False, + ) + + self.effects_of_divestment_per_size = flow_system.fit_effects_to_model_coords( + label_prefix=name_prefix, + effect_values=self.effects_of_divestment_per_size, + label_suffix='effects_of_divestment_per_size', + has_time_dim=False, + ) + + self.minimum_size = flow_system.fit_to_model_coords( + f'{name_prefix}|minimum_size', self.minimum_size, has_time_dim=False + ) + self.maximum_size = flow_system.fit_to_model_coords( + f'{name_prefix}|maximum_size', self.maximum_size, has_time_dim=False + ) + if self.fixed_size is not None: + self.fixed_size = flow_system.fit_to_model_coords( + f'{name_prefix}|fixed_size', self.fixed_size, has_time_dim=False + ) + + def _plausibility_checks(self, flow_system): + """Validate parameter consistency and compatibility with the flow system.""" + if flow_system.years is None: + raise ValueError("YearAwareInvestParameters requires the flow_system to have a 'years' dimension.") + + @property + def minimum_or_fixed_size(self) -> NonTemporalData: + """Get the effective minimum size (fixed size takes precedence).""" + return self.fixed_size if self.fixed_size is not None else self.minimum_size + + @property + def maximum_or_fixed_size(self) -> NonTemporalData: + """Get the effective maximum size (fixed size takes precedence).""" + return self.fixed_size if self.fixed_size is not None else self.maximum_size + + @property + def is_fixed_size(self) -> bool: + """Check if investment size is fixed.""" + return self.fixed_size is not None + + @register_class_for_io class OnOffParameters(Interface): def __init__( From c9c4ddfcf6d01562412ad2c1b75c7bf310c89a49 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 29 Jul 2025 16:09:11 +0200 Subject: [PATCH 274/448] Add YearAwareInvestmentModel --- flixopt/__init__.py | 1 + flixopt/calculation.py | 8 ++- flixopt/commons.py | 11 +++- flixopt/elements.py | 45 +++++++++------ flixopt/features.py | 128 ++++++++++++++++++++++++----------------- flixopt/modeling.py | 2 +- 6 files changed, 121 insertions(+), 74 deletions(-) diff --git a/flixopt/__init__.py b/flixopt/__init__.py index b92766449..7e82cd2ba 100644 --- a/flixopt/__init__.py +++ b/flixopt/__init__.py @@ -25,6 +25,7 @@ Storage, TimeSeriesData, Transmission, + YearAwareInvestParameters, change_logging_level, linear_converters, plotting, diff --git a/flixopt/calculation.py b/flixopt/calculation.py index d4d4e306d..a8bc4c22c 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -28,7 +28,7 @@ from .config import CONFIG from .core import DataConverter, Scalar, TimeSeriesData, drop_constant_arrays from .elements import Component -from .features import InvestmentModel +from .features import InvestmentModel, YearAwareInvestmentModel from .flow_system import FlowSystem from .results import CalculationResults, SegmentedCalculationResults from .solvers import _Solver @@ -117,13 +117,15 @@ def main_results(self) -> Dict[str, Union[Scalar, Dict]]: model.label_of_element: model.size.solution for component in self.flow_system.components.values() for model in component.submodel.all_submodels - if isinstance(model, InvestmentModel) and model.size.solution.max() >= CONFIG.modeling.EPSILON + if isinstance(model, (InvestmentModel, YearAwareInvestmentModel)) + and model.size.solution.max() >= CONFIG.modeling.EPSILON }, 'Not invested': { model.label_of_element: model.size.solution for component in self.flow_system.components.values() for model in component.submodel.all_submodels - if isinstance(model, InvestmentModel) and model.size.solution.max() < CONFIG.modeling.EPSILON + if isinstance(model, (InvestmentModel, YearAwareInvestmentModel)) + and model.size.solution.max() < CONFIG.modeling.EPSILON }, }, 'Buses with excess': [ diff --git a/flixopt/commons.py b/flixopt/commons.py index 68412d6fe..77750525f 100644 --- a/flixopt/commons.py +++ b/flixopt/commons.py @@ -18,7 +18,15 @@ from .effects import Effect from .elements import Bus, Flow from .flow_system import FlowSystem -from .interface import InvestParameters, OnOffParameters, Piece, Piecewise, PiecewiseConversion, PiecewiseEffects +from .interface import ( + InvestParameters, + OnOffParameters, + Piece, + Piecewise, + PiecewiseConversion, + PiecewiseEffects, + YearAwareInvestParameters, +) __all__ = [ 'TimeSeriesData', @@ -48,4 +56,5 @@ 'results', 'linear_converters', 'solvers', + 'YearAwareInvestParameters', ] diff --git a/flixopt/elements.py b/flixopt/elements.py index e1c0fcbc3..deb7ea219 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -13,8 +13,8 @@ from .config import CONFIG from .core import PlausibilityError, Scalar, TemporalData, TemporalDataUser from .effects import TemporalEffectsUser -from .features import InvestmentModel, ModelingPrimitives, OnOffModel -from .interface import InvestParameters, OnOffParameters +from .features import InvestmentModel, ModelingPrimitives, OnOffModel, YearAwareInvestmentModel +from .interface import InvestParameters, OnOffParameters, YearAwareInvestParameters from .modeling import BoundingPatterns, ModelingUtilitiesAbstract from .structure import Element, ElementModel, FlowSystemModel, register_class_for_io @@ -158,7 +158,7 @@ def __init__( self, label: str, bus: str, - size: Union[Scalar, InvestParameters] = None, + size: Union[Scalar, InvestParameters, YearAwareInvestParameters] = None, fixed_relative_profile: Optional[TemporalDataUser] = None, relative_minimum: TemporalDataUser = 0, relative_maximum: TemporalDataUser = 1, @@ -266,7 +266,7 @@ def transform_data(self, flow_system: 'FlowSystem'): if self.on_off_parameters is not None: self.on_off_parameters.transform_data(flow_system, self.label_full) - if isinstance(self.size, InvestParameters): + if isinstance(self.size, (InvestParameters, YearAwareInvestParameters)): self.size.transform_data(flow_system, self.label_full) else: self.size = flow_system.fit_to_model_coords(f'{self.label_full}|size', self.size, has_time_dim=False) @@ -276,7 +276,7 @@ def _plausibility_checks(self) -> None: if np.any(self.relative_minimum > self.relative_maximum): raise PlausibilityError(self.label_full + ': Take care, that relative_minimum <= relative_maximum!') - if not isinstance(self.size, InvestParameters) and ( + if not isinstance(self.size, (InvestParameters, YearAwareInvestParameters)) and ( np.any(self.size == CONFIG.modeling.BIG) and self.fixed_relative_profile is not None ): # Default Size --> Most likely by accident logger.warning( @@ -377,15 +377,28 @@ def _create_on_off_model(self): ) def _create_investment_model(self): - self.add_submodels( - InvestmentModel( - model=self._model, - label_of_element=self.label_of_element, - parameters=self.element.size, - label_of_model=self.label_of_element, - ), - 'investment', - ) + if isinstance(self.element.size, InvestParameters): + self.add_submodels( + InvestmentModel( + model=self._model, + label_of_element=self.label_of_element, + parameters=self.element.size, + label_of_model=self.label_of_element, + ), + 'investment', + ) + elif isinstance(self.element.size, YearAwareInvestParameters): + self.add_submodels( + YearAwareInvestmentModel( + model=self._model, + label_of_element=self.label_of_element, + parameters=self.element.size, + label_of_model=self.label_of_element, + ), + 'investment', + ) + else: + raise ValueError(f'Invalid InvestParameters type: {type(self.element.size)}') def _constraint_flow_rate(self): if not self.with_investment and not self.with_on_off: @@ -435,7 +448,7 @@ def with_on_off(self) -> bool: @property def with_investment(self) -> bool: - return isinstance(self.element.size, InvestParameters) + return isinstance(self.element.size, (InvestParameters, YearAwareInvestParameters)) # Properties for clean access to variables @property @@ -508,7 +521,7 @@ def absolute_flow_rate_bounds(self) -> Tuple[TemporalData, TemporalData]: if not self.with_investment: # Basic case without investment and without OnOff lb = lb_relative * self.element.size - elif not self.element.size.optional: + elif isinstance(self.element.size, InvestParameters) and not self.element.size.optional: # With non-optional Investment lb = lb_relative * self.element.size.minimum_or_fixed_size diff --git a/flixopt/features.py b/flixopt/features.py index fd48e0654..a82c1e838 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -11,7 +11,7 @@ from .config import CONFIG from .core import FlowSystemDimensions, NonTemporalData, Scalar, TemporalData -from .interface import InvestParameters, OnOffParameters, Piecewise, PiecewiseEffects +from .interface import InvestParameters, OnOffParameters, Piecewise, PiecewiseEffects, YearAwareInvestParameters from .modeling import BoundingPatterns, ModelingPrimitives, ModelingUtilities from .structure import FlowSystemModel, Submodel @@ -125,11 +125,13 @@ def is_invested(self) -> Optional[linopy.Variable]: class YearAwareInvestmentModel(Submodel): + parameters: YearAwareInvestParameters + def __init__( self, model: FlowSystemModel, label_of_element: str, - parameters: InvestParameters, + parameters: YearAwareInvestParameters, label_of_model: Optional[str] = None, ): """ @@ -150,73 +152,93 @@ def __init__( def _do_modeling(self): self._basic_modeling() self._custom_modeling() + self._add_effects() + + super()._do_modeling() def _basic_modeling(self): - size_min, size_max = (self.parameters.minimum_or_fixed_size, self.parameters.maximum_or_fixed_size) + _, size_max = (self.parameters.minimum_or_fixed_size, self.parameters.maximum_or_fixed_size) self.add_variables( short_name='size', - lower=0 if self.parameters.optional else size_min, + lower=0, upper=size_max, coords=self._model.get_coords(['year', 'scenario']), ) - if self.parameters.optional or self.parameters.year_aware: - self.add_variables( - binary=True, - coords=self._model.get_coords(['year', 'scenario']), - short_name='is_invested', - ) - - BoundingPatterns.bounds_with_state( - self, - variable=self.size, - variable_state=self.is_invested, - bounds=(self.parameters.minimum_or_fixed_size, self.parameters.maximum_or_fixed_size), - ) + self.add_variables( + binary=True, + coords=self._model.get_coords(['year', 'scenario']), + short_name='is_invested', + ) - if self.parameters.year_aware: - # Track when the investment/divestment happens - increase = self.add_variables( - binary=True, - coords=self._model.get_coords(['year', 'scenario']), - short_name='increase', - ) - decrease = self.add_variables( - binary=True, - coords=self._model.get_coords(['year', 'scenario']), - short_name='decrease', - ) - BoundingPatterns.state_transition_bounds( - self, - state_variable=self.is_invested, - switch_on=increase, - switch_off=decrease, - name=self.is_invested.name, - previous_state=0, - coord='year', - ) + BoundingPatterns.bounds_with_state( + self, + variable=self.size, + variable_state=self.is_invested, + bounds=(self.parameters.minimum_or_fixed_size, self.parameters.maximum_or_fixed_size), + ) - self.add_constraints(increase.sum('year') <= 1, name=f'{increase.name}|count') - self.add_constraints(decrease.sum('year') <= 1, name=f'{decrease.name}|count') - - # Ensures size can only change when increase or decrease is 1 - BoundingPatterns.continuous_transition_bounds( - model=self, - continuous_variable=self.size, - switch_on=increase, - switch_off=decrease, - name=self.size.name, - max_change=size_max, - previous_value=0, - coord='year', - ) + increase = self.add_variables( + binary=True, + coords=self._model.get_coords(['year', 'scenario']), + short_name='increase', + ) + decrease = self.add_variables( + binary=True, + coords=self._model.get_coords(['year', 'scenario']), + short_name='decrease', + ) + BoundingPatterns.state_transition_bounds( + self, + state_variable=self.is_invested, + switch_on=increase, + switch_off=decrease, + name=self.is_invested.name, + previous_state=0, + coord='year', + ) - super()._do_modeling() + self.add_constraints(increase.sum('year') <= 1, name=f'{increase.name}|count') + self.add_constraints(decrease.sum('year') <= 1, name=f'{decrease.name}|count') + + # Ensures size can only change when increase or decrease is 1 + BoundingPatterns.continuous_transition_bounds( + model=self, + continuous_variable=self.size, + switch_on=increase, + switch_off=decrease, + name=self.size.name, + max_change=size_max, + previous_value=0, + coord='year', + ) def _custom_modeling(self): # Add usefull constraints for investment decisions, parameterized by the user + pass + def _add_effects(self): + if self.parameters.effects_of_investment: + self._model.effects.add_share_to_effects( + name=self.label_of_element, + expressions={ + effect: self.is_invested * factor if self.is_invested is not None else factor + for effect, factor in self.parameters.fix_effects.items() + }, + target='invest', + ) + + if self.parameters.effects_of_investment_per_size: + self._model.effects.add_share_to_effects( + name=self.label_of_element, + expressions={ + effect: self.size * factor + for effect, factor in self.parameters.effects_of_investment_per_size.items() + }, + target='invest', + ) + @property def size(self) -> linopy.Variable: """Investment size variable""" diff --git a/flixopt/modeling.py b/flixopt/modeling.py index 874ef1c0f..f523346cc 100644 --- a/flixopt/modeling.py +++ b/flixopt/modeling.py @@ -648,7 +648,7 @@ def continuous_transition_bounds( ) initial_lower = model.add_constraints( - previous_value - continuous_variable.isel({coord: 0}) + -continuous_variable.isel({coord: 0}) + previous_value <= max_change * (switch_on.isel({coord: 0}) + switch_off.isel({coord: 0})), name=f'{name}|initial_lb', ) From 85ec956a21a8b88d025299f55f86adb3f1f32867 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 29 Jul 2025 16:09:50 +0200 Subject: [PATCH 275/448] Add new Interface --- flixopt/interface.py | 41 +++++------------------------------------ 1 file changed, 5 insertions(+), 36 deletions(-) diff --git a/flixopt/interface.py b/flixopt/interface.py index f981cd121..5b11de05f 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -261,24 +261,17 @@ def __init__( minimum_size: Optional[Scalar] = None, maximum_size: Optional[Scalar] = None, fixed_size: Optional[Scalar] = None, - previous_size: Scalar = 0, # Timing constraints - flexible combinations - fixed_start_year: Optional[int] = None, - fixed_end_year: Optional[int] = None, - fixed_duration: Optional[int] = None, earliest_start_year: Optional[int] = None, latest_start_year: Optional[int] = None, earliest_end_year: Optional[int] = None, latest_end_year: Optional[int] = None, - minimum_duration: Optional[int] = None, - maximum_duration: Optional[int] = None, + duration: Optional[int] = None, + # minimum_duration: Optional[int] = None, + # maximum_duration: Optional[int] = None, # Direct effects effects_of_investment_per_size: Optional['NonTemporalEffectsUser'] = None, effects_of_investment: Optional['NonTemporalEffectsUser'] = None, - # Divestment constraints - allow_divestment: bool = False, - effects_of_divestment_per_size: Optional['NonTemporalEffectsUser'] = None, - effects_of_divestment: Optional['NonTemporalEffectsUser'] = None, ): """ Initialize year-aware investment parameters. @@ -313,15 +306,12 @@ def __init__( self.maximum_size = maximum_size if maximum_size is not None else CONFIG.modeling.BIG self.fixed_size = fixed_size - self.fixed_start_year = fixed_start_year - self.fixed_end_year = fixed_end_year - self.fixed_duration = fixed_duration self.earliest_start_year = earliest_start_year self.latest_start_year = latest_start_year self.earliest_end_year = earliest_end_year self.latest_end_year = latest_end_year - self.minimum_duration = minimum_duration - self.maximum_duration = maximum_duration + # self.minimum_duration = minimum_duration + # self.maximum_duration = maximum_duration self.effects_of_investment_per_size: 'NonTemporalEffectsUser' = ( effects_of_investment_per_size if effects_of_investment_per_size is not None else {} @@ -330,14 +320,6 @@ def __init__( effects_of_investment if effects_of_investment is not None else {} ) - self.allow_divestment = allow_divestment - self.effects_of_divestment: 'NonTemporalEffectsUser' = ( - effects_of_divestment if effects_of_divestment is not None else {} - ) - self.effects_of_divestment_per_size: 'NonTemporalEffectsUser' = ( - effects_of_divestment_per_size if effects_of_divestment_per_size is not None else {} - ) - def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): """Transform all parameter data to match the flow system's coordinate structure.""" self._plausibility_checks(flow_system) @@ -354,19 +336,6 @@ def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): label_suffix='effects_of_investment', has_time_dim=False, ) - self.effects_of_divestment = flow_system.fit_effects_to_model_coords( - label_prefix=name_prefix, - effect_values=self.effects_of_divestment, - label_suffix='effects_of_divestment', - has_time_dim=False, - ) - - self.effects_of_divestment_per_size = flow_system.fit_effects_to_model_coords( - label_prefix=name_prefix, - effect_values=self.effects_of_divestment_per_size, - label_suffix='effects_of_divestment_per_size', - has_time_dim=False, - ) self.minimum_size = flow_system.fit_to_model_coords( f'{name_prefix}|minimum_size', self.minimum_size, has_time_dim=False From 18146c2d85d7813b6c98efbd46dc50747a65f453 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 29 Jul 2025 16:26:43 +0200 Subject: [PATCH 276/448] Improve YearAwareInvestmentModel --- flixopt/features.py | 86 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 75 insertions(+), 11 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index a82c1e838..2d491c7ee 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -153,7 +153,6 @@ def _do_modeling(self): self._basic_modeling() self._custom_modeling() self._add_effects() - super()._do_modeling() def _basic_modeling(self): @@ -214,22 +213,65 @@ def _basic_modeling(self): ) def _custom_modeling(self): - # Add usefull constraints for investment decisions, parameterized by the user + """Add timing constraints for investment decisions based on user parameters.""" + + # Get the year dimension coordinates + year_coords = self._model.get_coords(['year'])[0] if self._model.get_coords(['year']) else None + if year_coords is None: + return # No year dimension, skip timing constraints + + # Apply timing constraints based on parameters + self._apply_start_year_constraints(self.increase) + self._apply_end_year_constraints(self.decrease) + + def _apply_start_year_constraints(self, increase): + """Apply start year related constraints.""" + if self.parameters.earliest_start_year is not None: + # TODO: ensure that the year is present + self.add_constraints( + increase.sel(year=slice(None, self.parameters.earliest_start_year)) == 0, + name=f'{increase.name}|earliest_start', + ) + if self.parameters.latest_start_year is not None: + # TODO: ensure that the year is present + self.add_constraints( + increase.sel(year=slice(self.parameters.latest_start_year + 1, None)) == 0, + name=f'{increase.name}|latest_start', + ) - pass + def _apply_end_year_constraints(self, decrease): + """Apply end year related constraints.""" + + if self.parameters.earliest_end_year is not None: + # TODO: ensure that the year is present + self.add_constraints( + decrease.sel(year=slice(None, self.parameters.earliest_end_year)) == 0, + name=f'{decrease.name}|earliest_end', + ) + if self.parameters.latest_end_year is not None: + # TODO: ensure that the year is present + self.add_constraints( + decrease.sel(year=slice(self.parameters.latest_end_year + 1, None)) == 0, + name=f'{decrease.name}|latest_end', + ) def _add_effects(self): + """Add investment effects to the model.""" + if self.parameters.effects_of_investment: - self._model.effects.add_share_to_effects( - name=self.label_of_element, - expressions={ - effect: self.is_invested * factor if self.is_invested is not None else factor - for effect, factor in self.parameters.fix_effects.items() - }, - target='invest', - ) + # One-time effects when investment is made + increase = self._variables.get('increase') + if increase is not None: + self._model.effects.add_share_to_effects( + name=self.label_of_element, + expressions={ + effect: increase * factor for effect, factor in self.parameters.effects_of_investment.items() + }, + target='invest', + ) if self.parameters.effects_of_investment_per_size: + # Annual effects proportional to investment size self._model.effects.add_share_to_effects( name=self.label_of_element, expressions={ @@ -239,6 +281,18 @@ def _add_effects(self): target='invest', ) + if self.parameters.effects_of_divestment and self.parameters.allow_divestment: + # One-time effects when divestment is made + decrease = self._variables.get('decrease') + if decrease is not None: + self._model.effects.add_share_to_effects( + name=self.label_of_element, + expressions={ + effect: decrease * factor for effect, factor in self.parameters.effects_of_divestment.items() + }, + target='invest', + ) + @property def size(self) -> linopy.Variable: """Investment size variable""" @@ -251,6 +305,16 @@ def is_invested(self) -> Optional[linopy.Variable]: return None return self._variables['is_invested'] + @property + def increase(self) -> linopy.Variable: + """Binary increase decision variable""" + return self._variables['increase'] + + @property + def decrease(self) -> linopy.Variable: + """Binary decrease decision variable""" + return self._variables['decrease'] + class OnOffModel(Submodel): """OnOff model using factory patterns""" From 015aa1dbc94a687c255589315f1fbb862471600b Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 30 Jul 2025 10:35:46 +0200 Subject: [PATCH 277/448] Rename and improve --- flixopt/__init__.py | 2 +- flixopt/calculation.py | 6 +- flixopt/commons.py | 4 +- flixopt/elements.py | 16 +-- flixopt/features.py | 123 +++++++--------- flixopt/interface.py | 311 +++++++++++++++++++++++++++++++++++------ 6 files changed, 331 insertions(+), 131 deletions(-) diff --git a/flixopt/__init__.py b/flixopt/__init__.py index 7e82cd2ba..1fea6ef4b 100644 --- a/flixopt/__init__.py +++ b/flixopt/__init__.py @@ -12,6 +12,7 @@ FlowSystem, FullCalculation, InvestParameters, + InvestTimingParameters, LinearConverter, OnOffParameters, Piece, @@ -25,7 +26,6 @@ Storage, TimeSeriesData, Transmission, - YearAwareInvestParameters, change_logging_level, linear_converters, plotting, diff --git a/flixopt/calculation.py b/flixopt/calculation.py index a8bc4c22c..1fb13d743 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -28,7 +28,7 @@ from .config import CONFIG from .core import DataConverter, Scalar, TimeSeriesData, drop_constant_arrays from .elements import Component -from .features import InvestmentModel, YearAwareInvestmentModel +from .features import InvestmentModel, InvestmentTimingModel from .flow_system import FlowSystem from .results import CalculationResults, SegmentedCalculationResults from .solvers import _Solver @@ -117,14 +117,14 @@ def main_results(self) -> Dict[str, Union[Scalar, Dict]]: model.label_of_element: model.size.solution for component in self.flow_system.components.values() for model in component.submodel.all_submodels - if isinstance(model, (InvestmentModel, YearAwareInvestmentModel)) + if isinstance(model, (InvestmentModel, InvestmentTimingModel)) and model.size.solution.max() >= CONFIG.modeling.EPSILON }, 'Not invested': { model.label_of_element: model.size.solution for component in self.flow_system.components.values() for model in component.submodel.all_submodels - if isinstance(model, (InvestmentModel, YearAwareInvestmentModel)) + if isinstance(model, (InvestmentModel, InvestmentTimingModel)) and model.size.solution.max() < CONFIG.modeling.EPSILON }, }, diff --git a/flixopt/commons.py b/flixopt/commons.py index 77750525f..68461f10e 100644 --- a/flixopt/commons.py +++ b/flixopt/commons.py @@ -20,12 +20,12 @@ from .flow_system import FlowSystem from .interface import ( InvestParameters, + InvestTimingParameters, OnOffParameters, Piece, Piecewise, PiecewiseConversion, PiecewiseEffects, - YearAwareInvestParameters, ) __all__ = [ @@ -56,5 +56,5 @@ 'results', 'linear_converters', 'solvers', - 'YearAwareInvestParameters', + 'InvestTimingParameters', ] diff --git a/flixopt/elements.py b/flixopt/elements.py index deb7ea219..2aed7d713 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -13,8 +13,8 @@ from .config import CONFIG from .core import PlausibilityError, Scalar, TemporalData, TemporalDataUser from .effects import TemporalEffectsUser -from .features import InvestmentModel, ModelingPrimitives, OnOffModel, YearAwareInvestmentModel -from .interface import InvestParameters, OnOffParameters, YearAwareInvestParameters +from .features import InvestmentModel, InvestmentTimingModel, ModelingPrimitives, OnOffModel +from .interface import InvestParameters, InvestTimingParameters, OnOffParameters from .modeling import BoundingPatterns, ModelingUtilitiesAbstract from .structure import Element, ElementModel, FlowSystemModel, register_class_for_io @@ -158,7 +158,7 @@ def __init__( self, label: str, bus: str, - size: Union[Scalar, InvestParameters, YearAwareInvestParameters] = None, + size: Union[Scalar, InvestParameters, InvestTimingParameters] = None, fixed_relative_profile: Optional[TemporalDataUser] = None, relative_minimum: TemporalDataUser = 0, relative_maximum: TemporalDataUser = 1, @@ -266,7 +266,7 @@ def transform_data(self, flow_system: 'FlowSystem'): if self.on_off_parameters is not None: self.on_off_parameters.transform_data(flow_system, self.label_full) - if isinstance(self.size, (InvestParameters, YearAwareInvestParameters)): + if isinstance(self.size, (InvestParameters, InvestTimingParameters)): self.size.transform_data(flow_system, self.label_full) else: self.size = flow_system.fit_to_model_coords(f'{self.label_full}|size', self.size, has_time_dim=False) @@ -276,7 +276,7 @@ def _plausibility_checks(self) -> None: if np.any(self.relative_minimum > self.relative_maximum): raise PlausibilityError(self.label_full + ': Take care, that relative_minimum <= relative_maximum!') - if not isinstance(self.size, (InvestParameters, YearAwareInvestParameters)) and ( + if not isinstance(self.size, (InvestParameters, InvestTimingParameters)) and ( np.any(self.size == CONFIG.modeling.BIG) and self.fixed_relative_profile is not None ): # Default Size --> Most likely by accident logger.warning( @@ -387,9 +387,9 @@ def _create_investment_model(self): ), 'investment', ) - elif isinstance(self.element.size, YearAwareInvestParameters): + elif isinstance(self.element.size, InvestTimingParameters): self.add_submodels( - YearAwareInvestmentModel( + InvestmentTimingModel( model=self._model, label_of_element=self.label_of_element, parameters=self.element.size, @@ -448,7 +448,7 @@ def with_on_off(self) -> bool: @property def with_investment(self) -> bool: - return isinstance(self.element.size, (InvestParameters, YearAwareInvestParameters)) + return isinstance(self.element.size, (InvestParameters, InvestTimingParameters)) # Properties for clean access to variables @property diff --git a/flixopt/features.py b/flixopt/features.py index 2d491c7ee..c669abea5 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -8,10 +8,19 @@ import linopy import numpy as np +import xarray as xr from .config import CONFIG from .core import FlowSystemDimensions, NonTemporalData, Scalar, TemporalData -from .interface import InvestParameters, OnOffParameters, Piecewise, PiecewiseEffects, YearAwareInvestParameters +from .interface import ( + FixedEndInvestTimingParameters, + FixedStartInvestTimingParameters, + InvestParameters, + InvestTimingParameters, + OnOffParameters, + Piecewise, + PiecewiseEffects, +) from .modeling import BoundingPatterns, ModelingPrimitives, ModelingUtilities from .structure import FlowSystemModel, Submodel @@ -124,30 +133,18 @@ def is_invested(self) -> Optional[linopy.Variable]: return self._variables['is_invested'] -class YearAwareInvestmentModel(Submodel): - parameters: YearAwareInvestParameters +class InvestmentTimingModel(Submodel): + parameters: InvestTimingParameters def __init__( self, model: FlowSystemModel, label_of_element: str, - parameters: YearAwareInvestParameters, + parameters: InvestTimingParameters, label_of_model: Optional[str] = None, ): - """ - This feature model is used to model the investment of a variable. - It applies the corresponding bounds to the variable and the on/off state of the variable. - - Args: - model: The optimization model instance - label_of_element: The label of the parent (Element). Used to construct the full label of the model. - parameters: The parameters of the feature model. - label_of_model: The label of the model. This is needed to construct the full label of the model. - - """ - self.piecewise_effects: Optional[PiecewiseEffectsModel] = None self.parameters = parameters - super().__init__(model, label_of_element=label_of_element, label_of_model=label_of_model) + super().__init__(model, label_of_element, label_of_model) def _do_modeling(self): self._basic_modeling() @@ -156,7 +153,8 @@ def _do_modeling(self): super()._do_modeling() def _basic_modeling(self): - _, size_max = (self.parameters.minimum_or_fixed_size, self.parameters.maximum_or_fixed_size) + size_min, size_max = self.parameters.minimum_or_fixed_size, self.parameters.maximum_or_fixed_size + self.add_variables( short_name='size', lower=0, @@ -174,7 +172,7 @@ def _basic_modeling(self): self, variable=self.size, variable_state=self.is_invested, - bounds=(self.parameters.minimum_or_fixed_size, self.parameters.maximum_or_fixed_size), + bounds=(size_min, size_max), ) increase = self.add_variables( @@ -197,9 +195,6 @@ def _basic_modeling(self): coord='year', ) - self.add_constraints(increase.sum('year') <= 1, name=f'{increase.name}|count') - self.add_constraints(decrease.sum('year') <= 1, name=f'{decrease.name}|count') - # Ensures size can only change when increase or decrease is 1 BoundingPatterns.continuous_transition_bounds( model=self, @@ -212,47 +207,20 @@ def _basic_modeling(self): coord='year', ) - def _custom_modeling(self): - """Add timing constraints for investment decisions based on user parameters.""" - - # Get the year dimension coordinates - year_coords = self._model.get_coords(['year'])[0] if self._model.get_coords(['year']) else None - if year_coords is None: - return # No year dimension, skip timing constraints + self.add_constraints(self.increase.sum('year') <= 1, name=f'{self.increase.name}|count') + self.add_constraints(self.decrease.sum('year') <= 1, name=f'{self.decrease.name}|count') - # Apply timing constraints based on parameters - self._apply_start_year_constraints(self.increase) - self._apply_end_year_constraints(self.decrease) - - def _apply_start_year_constraints(self, increase): - """Apply start year related constraints.""" - if self.parameters.earliest_start_year is not None: - # TODO: ensure that the year is present - self.add_constraints( - increase.sel(year=slice(None, self.parameters.earliest_start_year)) == 0, - name=f'{increase.name}|earliest_start', - ) - if self.parameters.latest_start_year is not None: - # TODO: ensure that the year is present - self.add_constraints( - increase.sel(year=slice(self.parameters.latest_start_year + 1, None)) == 0, - name=f'{increase.name}|latest_start', - ) - - def _apply_end_year_constraints(self, decrease): - """Apply end year related constraints.""" + def _custom_modeling(self): + # Fixed end and start year + self.add_constraints( + self.increase.sel(year=self.parameters.start_year) == self.decrease.sel(year=self.parameters.end_year), + name=f'{self.increase.name}|fixed_start_and_end', + ) - if self.parameters.earliest_end_year is not None: - # TODO: ensure that the year is present + if not self.parameters.optional: self.add_constraints( - decrease.sel(year=slice(None, self.parameters.earliest_end_year)) == 0, - name=f'{decrease.name}|earliest_end', - ) - if self.parameters.latest_end_year is not None: - # TODO: ensure that the year is present - self.add_constraints( - decrease.sel(year=slice(self.parameters.latest_end_year + 1, None)) == 0, - name=f'{decrease.name}|latest_end', + self.increase.sel(year=self.parameters.start_year) == 1, + name=f'{self.increase.name}|non_optional', ) def _add_effects(self): @@ -281,18 +249,6 @@ def _add_effects(self): target='invest', ) - if self.parameters.effects_of_divestment and self.parameters.allow_divestment: - # One-time effects when divestment is made - decrease = self._variables.get('decrease') - if decrease is not None: - self._model.effects.add_share_to_effects( - name=self.label_of_element, - expressions={ - effect: decrease * factor for effect, factor in self.parameters.effects_of_divestment.items() - }, - target='invest', - ) - @property def size(self) -> linopy.Variable: """Investment size variable""" @@ -316,6 +272,29 @@ def decrease(self) -> linopy.Variable: return self._variables['decrease'] +class FixedStartInvementTimingModel(InvestmentTimingModel): + parameters: FixedStartInvestTimingParameters + + def _custom_modeling(self): + # Fixed start year + self.add_constraints( + (self.increase * 1).where( + xr.DataArray( + self._model.flow_system.years != self.parameters.start_year, + coords=self.get_coords(['year', 'scenario']), + ) + ) + == 0, + name=f'{self.increase.name}|fixed_start_and_end', + ) + + if not self.parameters.optional: + self.add_constraints( + self.increase.sel(year=self.parameters.start_year) == 1, + name=f'{self.increase.name}|non_optional', + ) + + class OnOffModel(Submodel): """OnOff model using factory patterns""" diff --git a/flixopt/interface.py b/flixopt/interface.py index 5b11de05f..926e481b7 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -242,17 +242,12 @@ def maximum_or_fixed_size(self) -> NonTemporalData: return self.fixed_size if self.fixed_size is not None else self.maximum_size +# Base interface for common parameters @register_class_for_io -class YearAwareInvestParameters(Interface): +class _BaseYearAwareInvestParameters(Interface): """ - Parameters for year-aware investment modeling with multi-year optimization and timing constraints. - - This interface supports investment decisions that can change over time, with constraints on - when investments can be made, modified, or removed. Useful for modeling: - - Capacity expansion planning over multiple years - - Technology replacement strategies - - Retrofit and upgrade scheduling - - Investment timing optimization with changing costs/benefits + Base parameters for year-aware investment modeling. + Contains common sizing and effects parameters used by all variants. """ def __init__( @@ -261,57 +256,27 @@ def __init__( minimum_size: Optional[Scalar] = None, maximum_size: Optional[Scalar] = None, fixed_size: Optional[Scalar] = None, - # Timing constraints - flexible combinations - earliest_start_year: Optional[int] = None, - latest_start_year: Optional[int] = None, - earliest_end_year: Optional[int] = None, - latest_end_year: Optional[int] = None, - duration: Optional[int] = None, - # minimum_duration: Optional[int] = None, - # maximum_duration: Optional[int] = None, + optional: bool = False, # Direct effects effects_of_investment_per_size: Optional['NonTemporalEffectsUser'] = None, effects_of_investment: Optional['NonTemporalEffectsUser'] = None, ): """ - Initialize year-aware investment parameters. + Initialize base year-aware investment parameters. Args: minimum_size: Minimum investment size when invested. Defaults to CONFIG.modeling.EPSILON. maximum_size: Maximum possible investment size. Defaults to CONFIG.modeling.BIG. - fixed_size: If specified, investment size is fixed to this value (if investment is taken). - previous_size: THe size previous to the evaluated period (relevant for divestment decisions). - - fixed_start_year: If specified, investment must start in this exact year. - fixed_end_year: If specified, investment must end in this exact year. - fixed_duration: If specified, investment must last exactly this many years. - earliest_start_year: Earliest year investment can start. - latest_start_year: Latest year investment can start. - earliest_end_year: Earliest year investment can end. - latest_end_year: Latest year investment can end. - minimum_duration: Minimum duration investment must last (in years). - maximum_duration: Maximum duration investment can last (in years). - - effects_per_size: Effects applied per unit of investment size for each year invested. + fixed_size: If specified, investment size is fixed to this value. + effects_of_investment_per_size: Effects applied per unit of investment size for each year invested. Example: {'costs': 100} applies 100 * size * years_invested to total costs. - effects_per_investment: One-time effects applied when investment decision is made. + effects_of_investment: One-time effects applied when investment decision is made. Example: {'costs': 1000} applies 1000 to costs in the investment year. - - allow_divestment: If True, investment can be terminated before the end of time horizon. - If False, once invested, remains invested until end of horizon. - effects_per_divestment: One-time effects applied when divestment decision is made. - Example: {'costs': 500} applies 500 to costs in the divestment year. """ self.minimum_size = minimum_size if minimum_size is not None else CONFIG.modeling.EPSILON self.maximum_size = maximum_size if maximum_size is not None else CONFIG.modeling.BIG self.fixed_size = fixed_size - - self.earliest_start_year = earliest_start_year - self.latest_start_year = latest_start_year - self.earliest_end_year = earliest_end_year - self.latest_end_year = latest_end_year - # self.minimum_duration = minimum_duration - # self.maximum_duration = maximum_duration + self.optional = optional self.effects_of_investment_per_size: 'NonTemporalEffectsUser' = ( effects_of_investment_per_size if effects_of_investment_per_size is not None else {} @@ -369,6 +334,262 @@ def is_fixed_size(self) -> bool: return self.fixed_size is not None +# Variant 1: Fixed Start and End Year +@register_class_for_io +class InvestTimingParameters(_BaseYearAwareInvestParameters): + """ + Investment with fixed start and end years. + + This is the simplest variant - investment is completely scheduled. + No optimization variables needed for timing, just size optimization. + """ + + def __init__( + self, + start_year: int, + end_year: int, + # Base parameters + minimum_size: Optional[Scalar] = None, + maximum_size: Optional[Scalar] = None, + fixed_size: Optional[Scalar] = None, + optional: bool = False, + effects_of_investment_per_size: Optional['NonTemporalEffectsUser'] = None, + effects_of_investment: Optional['NonTemporalEffectsUser'] = None, + ): + """ + Initialize fixed start and end year investment parameters. + + Args: + start_year: Year when investment must start (0-indexed). + end_year: Year when investment must end (0-indexed). + **kwargs: Base parameters (size, effects) + """ + super().__init__( + minimum_size=minimum_size, + maximum_size=maximum_size, + fixed_size=fixed_size, + optional=optional, + effects_of_investment_per_size=effects_of_investment_per_size, + effects_of_investment=effects_of_investment, + ) + + self.start_year = start_year + self.end_year = end_year + + def _plausibility_checks(self, flow_system): + """Validate parameter consistency.""" + super()._plausibility_checks(flow_system) + + if self.start_year < flow_system.years[0] or self.start_year > flow_system.years[-1]: + raise ValueError( + f'start_year ({self.start_year}) must be between {flow_system.years[0]} and {flow_system.years[-1]}' + ) + + if self.end_year < flow_system.years[0] or self.end_year > flow_system.years[-1]: + raise ValueError(f'end_year ({self.end_year}) must be between 0 and {flow_system.years[-1]}') + + if self.start_year >= self.end_year: + raise ValueError(f'start_year ({self.start_year}) must be before end_year ({self.end_year})') + + @property + def duration(self) -> int: + """Get the investment duration.""" + return self.end_year - self.start_year + 1 + + +# Variant 2: Fixed Start Year, Variable End Year +@register_class_for_io +class FixedStartInvestTimingParameters(_BaseYearAwareInvestParameters): + """ + Investment with fixed start year but optimizable end year. + + Start timing is known, but duration/end timing is optimized. + Good for modeling investments that must start at a specific time + but can run for different durations. + """ + + def __init__( + self, + start_year: int, + earliest_end_year: Optional[int] = None, + latest_end_year: Optional[int] = None, + # Base parameters + minimum_size: Optional[Scalar] = None, + maximum_size: Optional[Scalar] = None, + fixed_size: Optional[Scalar] = None, + effects_of_investment_per_size: Optional['NonTemporalEffectsUser'] = None, + effects_of_investment: Optional['NonTemporalEffectsUser'] = None, + effects_of_divestment: Optional['NonTemporalEffectsUser'] = None, + ): + """ + Initialize fixed start, variable end investment parameters. + + Args: + start_year: Year when investment must start (0-indexed). + earliest_end_year: Earliest year investment can end (0-indexed). + latest_end_year: Latest year investment can end (0-indexed). + effects_of_divestment: One-time effects when investment ends. + **kwargs: Base parameters (size, effects) + """ + super().__init__( + minimum_size=minimum_size, + maximum_size=maximum_size, + fixed_size=fixed_size, + effects_of_investment_per_size=effects_of_investment_per_size, + effects_of_investment=effects_of_investment, + ) + + self.start_year = start_year + self.earliest_end_year = earliest_end_year + self.latest_end_year = latest_end_year + self.effects_of_divestment: 'NonTemporalEffectsUser' = ( + effects_of_divestment if effects_of_divestment is not None else {} + ) + + def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): + """Transform parameter data.""" + super().transform_data(flow_system, name_prefix) + + self.effects_of_divestment = flow_system.fit_effects_to_model_coords( + label_prefix=name_prefix, + effect_values=self.effects_of_divestment, + label_suffix='effects_of_divestment', + has_time_dim=False, + ) + + def _plausibility_checks(self, flow_system): + """Validate parameter consistency.""" + super()._plausibility_checks(flow_system) + + total_years = len(flow_system.years) + + if self.start_year < 0 or self.start_year >= total_years: + raise ValueError(f'start_year ({self.start_year}) must be between 0 and {total_years - 1}') + + if self.earliest_end_year is not None: + if self.earliest_end_year <= self.start_year or self.earliest_end_year >= total_years: + raise ValueError( + f'earliest_end_year ({self.earliest_end_year}) must be after start_year and before {total_years}' + ) + + if self.latest_end_year is not None: + if self.latest_end_year <= self.start_year or self.latest_end_year >= total_years: + raise ValueError( + f'latest_end_year ({self.latest_end_year}) must be after start_year and before {total_years}' + ) + + if ( + self.earliest_end_year is not None + and self.latest_end_year is not None + and self.earliest_end_year > self.latest_end_year + ): + raise ValueError( + f'earliest_end_year ({self.earliest_end_year}) must be <= latest_end_year ({self.latest_end_year})' + ) + + if self.minimum_duration is not None and self.minimum_duration < 1: + raise ValueError(f'minimum_duration ({self.minimum_duration}) must be at least 1') + + if self.maximum_duration is not None and self.maximum_duration < 1: + raise ValueError(f'maximum_duration ({self.maximum_duration}) must be at least 1') + + if ( + self.minimum_duration is not None + and self.maximum_duration is not None + and self.minimum_duration > self.maximum_duration + ): + raise ValueError( + f'minimum_duration ({self.minimum_duration}) must be <= maximum_duration ({self.maximum_duration})' + ) + + +# Variant 3: Variable Start Year, Fixed End Year +@register_class_for_io +class FixedEndInvestTimingParameters(_BaseYearAwareInvestParameters): + """ + Investment with optimizable start year but fixed end year. + + End timing is known (e.g., regulatory deadline), but start timing is optimized. + Good for modeling investments that must be completed by a specific deadline + but timing of start can be optimized. + """ + + def __init__( + self, + end_year: int, + earliest_start_year: Optional[int] = None, + latest_start_year: Optional[int] = None, + # Base parameters + minimum_size: Optional[Scalar] = None, + maximum_size: Optional[Scalar] = None, + fixed_size: Optional[Scalar] = None, + effects_of_investment_per_size: Optional['NonTemporalEffectsUser'] = None, + effects_of_investment: Optional['NonTemporalEffectsUser'] = None, + effects_of_divestment: Optional['NonTemporalEffectsUser'] = None, + ): + """ + Initialize variable start, fixed end investment parameters. + + Args: + end_year: Year when investment must end (0-indexed). + earliest_start_year: Earliest year investment can start (0-indexed). + latest_start_year: Latest year investment can start (0-indexed). + effects_of_divestment: One-time effects when investment ends. + **kwargs: Base parameters (size, effects) + """ + super().__init__( + minimum_size=minimum_size, + maximum_size=maximum_size, + fixed_size=fixed_size, + effects_of_investment_per_size=effects_of_investment_per_size, + effects_of_investment=effects_of_investment, + ) + + self.end_year = end_year + self.earliest_start_year = earliest_start_year + self.latest_start_year = latest_start_year + self.effects_of_divestment: 'NonTemporalEffectsUser' = ( + effects_of_divestment if effects_of_divestment is not None else {} + ) + + def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): + """Transform parameter data.""" + super().transform_data(flow_system, name_prefix) + + self.effects_of_divestment = flow_system.fit_effects_to_model_coords( + label_prefix=name_prefix, + effect_values=self.effects_of_divestment, + label_suffix='effects_of_divestment', + has_time_dim=False, + ) + + def _plausibility_checks(self, flow_system): + """Validate parameter consistency.""" + super()._plausibility_checks(flow_system) + + total_years = len(flow_system.years) + + if self.end_year < 0 or self.end_year >= total_years: + raise ValueError(f'end_year ({self.end_year}) must be between 0 and {total_years - 1}') + + if self.earliest_start_year is not None: + if self.earliest_start_year < 0 or self.earliest_start_year >= self.end_year: + raise ValueError(f'earliest_start_year ({self.earliest_start_year}) must be >= 0 and < end_year') + + if self.latest_start_year is not None: + if self.latest_start_year < 0 or self.latest_start_year >= self.end_year: + raise ValueError(f'latest_start_year ({self.latest_start_year}) must be >= 0 and < end_year') + + if ( + self.earliest_start_year is not None + and self.latest_start_year is not None + and self.earliest_start_year > self.latest_start_year + ): + raise ValueError( + f'earliest_start_year ({self.earliest_start_year}) must be <= latest_start_year ({self.latest_start_year})' + ) + + @register_class_for_io class OnOffParameters(Interface): def __init__( From e6da5ada16479cfc9960dac11621d9821bc64222 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 30 Jul 2025 12:03:40 +0200 Subject: [PATCH 278/448] Move piecewise_effects --- flixopt/features.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index c669abea5..bd6abc6f0 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -78,19 +78,6 @@ def _create_variables_and_constraints(self): bounds=(self.parameters.minimum_or_fixed_size, self.parameters.maximum_or_fixed_size), ) - if self.parameters.piecewise_effects: - self.piecewise_effects = self.add_submodels( - PiecewiseEffectsModel( - model=self._model, - label_of_element=self.label_of_element, - label_of_model=f'{self.label_of_element}|PiecewiseEffects', - piecewise_origin=(self.size.name, self.parameters.piecewise_effects.piecewise_origin), - piecewise_shares=self.parameters.piecewise_effects.piecewise_shares, - zero_point=self.is_invested, - ), - short_name='segments', - ) - def _add_effects(self): """Add investment effects""" if self.parameters.fix_effects: @@ -120,6 +107,19 @@ def _add_effects(self): target='invest', ) + if self.parameters.piecewise_effects: + self.piecewise_effects = self.add_submodels( + PiecewiseEffectsModel( + model=self._model, + label_of_element=self.label_of_element, + label_of_model=f'{self.label_of_element}|PiecewiseEffects', + piecewise_origin=(self.size.name, self.parameters.piecewise_effects.piecewise_origin), + piecewise_shares=self.parameters.piecewise_effects.piecewise_shares, + zero_point=self.is_invested, + ), + short_name='segments', + ) + @property def size(self) -> linopy.Variable: """Investment size variable""" From aafe95bb23c8e5b5016b771413132294db251caf Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 30 Jul 2025 12:04:40 +0200 Subject: [PATCH 279/448] COmbine TImingInvestment into a single interface --- flixopt/features.py | 59 +++++++------ flixopt/interface.py | 200 ++----------------------------------------- 2 files changed, 36 insertions(+), 223 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index bd6abc6f0..d7dd03cdc 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -13,8 +13,6 @@ from .config import CONFIG from .core import FlowSystemDimensions, NonTemporalData, Scalar, TemporalData from .interface import ( - FixedEndInvestTimingParameters, - FixedStartInvestTimingParameters, InvestParameters, InvestTimingParameters, OnOffParameters, @@ -147,10 +145,14 @@ def __init__( super().__init__(model, label_of_element, label_of_model) def _do_modeling(self): + super()._do_modeling() self._basic_modeling() - self._custom_modeling() self._add_effects() - super()._do_modeling() + + if self.parameters.start_year is not None and self.parameters.end_year is not None: + self._fixed_start_fixed_end_constraints() + elif self.parameters.end_year is not None: + self._fixed_end_constraints() def _basic_modeling(self): size_min, size_max = self.parameters.minimum_or_fixed_size, self.parameters.maximum_or_fixed_size @@ -210,19 +212,6 @@ def _basic_modeling(self): self.add_constraints(self.increase.sum('year') <= 1, name=f'{self.increase.name}|count') self.add_constraints(self.decrease.sum('year') <= 1, name=f'{self.decrease.name}|count') - def _custom_modeling(self): - # Fixed end and start year - self.add_constraints( - self.increase.sel(year=self.parameters.start_year) == self.decrease.sel(year=self.parameters.end_year), - name=f'{self.increase.name}|fixed_start_and_end', - ) - - if not self.parameters.optional: - self.add_constraints( - self.increase.sel(year=self.parameters.start_year) == 1, - name=f'{self.increase.name}|non_optional', - ) - def _add_effects(self): """Add investment effects to the model.""" @@ -249,6 +238,25 @@ def _add_effects(self): target='invest', ) + def _fixed_start_fixed_end_constraints(self): + """Add constraints for fixed start year.""" + self.add_constraints( + self.increase.sel(year=self.parameters.start_year) == self.decrease.sel(year=self.parameters.end_year), + name=f'{self.increase.name}|fixed_start_and_end', + ) + + if not self.parameters.optional: + self.add_constraints( + self.increase.sel(year=self.parameters.start_year) == 1, + name=f'{self.increase.name}|non_optional', + ) + + def _fixed_end_constraints(self): + pass + + def _fixed_start_constraints(self): + pass + @property def size(self) -> linopy.Variable: """Investment size variable""" @@ -272,19 +280,14 @@ def decrease(self) -> linopy.Variable: return self._variables['decrease'] -class FixedStartInvementTimingModel(InvestmentTimingModel): - parameters: FixedStartInvestTimingParameters +class FixedStartFixedEndInvestmentTimingModel(InvestmentTimingModel): + parameters: InvestTimingParameters + + def _basic_modeling(self): + super()._basic_modeling() - def _custom_modeling(self): - # Fixed start year self.add_constraints( - (self.increase * 1).where( - xr.DataArray( - self._model.flow_system.years != self.parameters.start_year, - coords=self.get_coords(['year', 'scenario']), - ) - ) - == 0, + self.increase.sel(year=self.parameters.start_year) == self.decrease.sel(year=self.parameters.end_year), name=f'{self.increase.name}|fixed_start_and_end', ) diff --git a/flixopt/interface.py b/flixopt/interface.py index 926e481b7..1825ea773 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -346,8 +346,8 @@ class InvestTimingParameters(_BaseYearAwareInvestParameters): def __init__( self, - start_year: int, - end_year: int, + start_year: Optional[int] = None, + end_year: Optional[int] = None, # Base parameters minimum_size: Optional[Scalar] = None, maximum_size: Optional[Scalar] = None, @@ -380,6 +380,9 @@ def _plausibility_checks(self, flow_system): """Validate parameter consistency.""" super()._plausibility_checks(flow_system) + if self.start_year is None and self.end_year is None: + raise ValueError('Either start_year or end_year must be specified.') + if self.start_year < flow_system.years[0] or self.start_year > flow_system.years[-1]: raise ValueError( f'start_year ({self.start_year}) must be between {flow_system.years[0]} and {flow_system.years[-1]}' @@ -397,199 +400,6 @@ def duration(self) -> int: return self.end_year - self.start_year + 1 -# Variant 2: Fixed Start Year, Variable End Year -@register_class_for_io -class FixedStartInvestTimingParameters(_BaseYearAwareInvestParameters): - """ - Investment with fixed start year but optimizable end year. - - Start timing is known, but duration/end timing is optimized. - Good for modeling investments that must start at a specific time - but can run for different durations. - """ - - def __init__( - self, - start_year: int, - earliest_end_year: Optional[int] = None, - latest_end_year: Optional[int] = None, - # Base parameters - minimum_size: Optional[Scalar] = None, - maximum_size: Optional[Scalar] = None, - fixed_size: Optional[Scalar] = None, - effects_of_investment_per_size: Optional['NonTemporalEffectsUser'] = None, - effects_of_investment: Optional['NonTemporalEffectsUser'] = None, - effects_of_divestment: Optional['NonTemporalEffectsUser'] = None, - ): - """ - Initialize fixed start, variable end investment parameters. - - Args: - start_year: Year when investment must start (0-indexed). - earliest_end_year: Earliest year investment can end (0-indexed). - latest_end_year: Latest year investment can end (0-indexed). - effects_of_divestment: One-time effects when investment ends. - **kwargs: Base parameters (size, effects) - """ - super().__init__( - minimum_size=minimum_size, - maximum_size=maximum_size, - fixed_size=fixed_size, - effects_of_investment_per_size=effects_of_investment_per_size, - effects_of_investment=effects_of_investment, - ) - - self.start_year = start_year - self.earliest_end_year = earliest_end_year - self.latest_end_year = latest_end_year - self.effects_of_divestment: 'NonTemporalEffectsUser' = ( - effects_of_divestment if effects_of_divestment is not None else {} - ) - - def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): - """Transform parameter data.""" - super().transform_data(flow_system, name_prefix) - - self.effects_of_divestment = flow_system.fit_effects_to_model_coords( - label_prefix=name_prefix, - effect_values=self.effects_of_divestment, - label_suffix='effects_of_divestment', - has_time_dim=False, - ) - - def _plausibility_checks(self, flow_system): - """Validate parameter consistency.""" - super()._plausibility_checks(flow_system) - - total_years = len(flow_system.years) - - if self.start_year < 0 or self.start_year >= total_years: - raise ValueError(f'start_year ({self.start_year}) must be between 0 and {total_years - 1}') - - if self.earliest_end_year is not None: - if self.earliest_end_year <= self.start_year or self.earliest_end_year >= total_years: - raise ValueError( - f'earliest_end_year ({self.earliest_end_year}) must be after start_year and before {total_years}' - ) - - if self.latest_end_year is not None: - if self.latest_end_year <= self.start_year or self.latest_end_year >= total_years: - raise ValueError( - f'latest_end_year ({self.latest_end_year}) must be after start_year and before {total_years}' - ) - - if ( - self.earliest_end_year is not None - and self.latest_end_year is not None - and self.earliest_end_year > self.latest_end_year - ): - raise ValueError( - f'earliest_end_year ({self.earliest_end_year}) must be <= latest_end_year ({self.latest_end_year})' - ) - - if self.minimum_duration is not None and self.minimum_duration < 1: - raise ValueError(f'minimum_duration ({self.minimum_duration}) must be at least 1') - - if self.maximum_duration is not None and self.maximum_duration < 1: - raise ValueError(f'maximum_duration ({self.maximum_duration}) must be at least 1') - - if ( - self.minimum_duration is not None - and self.maximum_duration is not None - and self.minimum_duration > self.maximum_duration - ): - raise ValueError( - f'minimum_duration ({self.minimum_duration}) must be <= maximum_duration ({self.maximum_duration})' - ) - - -# Variant 3: Variable Start Year, Fixed End Year -@register_class_for_io -class FixedEndInvestTimingParameters(_BaseYearAwareInvestParameters): - """ - Investment with optimizable start year but fixed end year. - - End timing is known (e.g., regulatory deadline), but start timing is optimized. - Good for modeling investments that must be completed by a specific deadline - but timing of start can be optimized. - """ - - def __init__( - self, - end_year: int, - earliest_start_year: Optional[int] = None, - latest_start_year: Optional[int] = None, - # Base parameters - minimum_size: Optional[Scalar] = None, - maximum_size: Optional[Scalar] = None, - fixed_size: Optional[Scalar] = None, - effects_of_investment_per_size: Optional['NonTemporalEffectsUser'] = None, - effects_of_investment: Optional['NonTemporalEffectsUser'] = None, - effects_of_divestment: Optional['NonTemporalEffectsUser'] = None, - ): - """ - Initialize variable start, fixed end investment parameters. - - Args: - end_year: Year when investment must end (0-indexed). - earliest_start_year: Earliest year investment can start (0-indexed). - latest_start_year: Latest year investment can start (0-indexed). - effects_of_divestment: One-time effects when investment ends. - **kwargs: Base parameters (size, effects) - """ - super().__init__( - minimum_size=minimum_size, - maximum_size=maximum_size, - fixed_size=fixed_size, - effects_of_investment_per_size=effects_of_investment_per_size, - effects_of_investment=effects_of_investment, - ) - - self.end_year = end_year - self.earliest_start_year = earliest_start_year - self.latest_start_year = latest_start_year - self.effects_of_divestment: 'NonTemporalEffectsUser' = ( - effects_of_divestment if effects_of_divestment is not None else {} - ) - - def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): - """Transform parameter data.""" - super().transform_data(flow_system, name_prefix) - - self.effects_of_divestment = flow_system.fit_effects_to_model_coords( - label_prefix=name_prefix, - effect_values=self.effects_of_divestment, - label_suffix='effects_of_divestment', - has_time_dim=False, - ) - - def _plausibility_checks(self, flow_system): - """Validate parameter consistency.""" - super()._plausibility_checks(flow_system) - - total_years = len(flow_system.years) - - if self.end_year < 0 or self.end_year >= total_years: - raise ValueError(f'end_year ({self.end_year}) must be between 0 and {total_years - 1}') - - if self.earliest_start_year is not None: - if self.earliest_start_year < 0 or self.earliest_start_year >= self.end_year: - raise ValueError(f'earliest_start_year ({self.earliest_start_year}) must be >= 0 and < end_year') - - if self.latest_start_year is not None: - if self.latest_start_year < 0 or self.latest_start_year >= self.end_year: - raise ValueError(f'latest_start_year ({self.latest_start_year}) must be >= 0 and < end_year') - - if ( - self.earliest_start_year is not None - and self.latest_start_year is not None - and self.earliest_start_year > self.latest_start_year - ): - raise ValueError( - f'earliest_start_year ({self.earliest_start_year}) must be <= latest_start_year ({self.latest_start_year})' - ) - - @register_class_for_io class OnOffParameters(Interface): def __init__( From 9316d8a6482534c1d2ba7ba1a869ea27756a82bf Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 30 Jul 2025 13:42:24 +0200 Subject: [PATCH 280/448] Add model tests for investment --- tests/test_models.py | 125 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 tests/test_models.py diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 000000000..c446313b8 --- /dev/null +++ b/tests/test_models.py @@ -0,0 +1,125 @@ +import numpy as np +import pandas as pd +import pytest +import linopy + +import flixopt as fx + +from .conftest import assert_conequal, assert_sets_equal, assert_var_equal, create_linopy_model, LoadProfiles, Effects, Sinks, Sources, Buses + +@pytest.fixture +def flow_system() -> fx.FlowSystem: + """Create basic elements for component testing with coordinate parametrization.""" + years = pd.Index([2020, 2021, 2022, 2023, 2024], name='year') + timesteps = pd.date_range('2020-01-01', periods=24, freq='h', name='time') + flow_system = fx.FlowSystem(timesteps=timesteps, years=years) + + thermal_load = LoadProfiles.random_thermal(len(timesteps)) + p_el = LoadProfiles.random_electrical(len(timesteps)) + + costs = Effects.costs() + heat_load = Sinks.heat_load(thermal_load) + gas_source = Sources.gas_with_costs() + electricity_sink = Sinks.electricity_feed_in(p_el) + + flow_system.add_elements(*Buses.defaults()) + flow_system.add_elements(costs, heat_load, gas_source, electricity_sink) + + return flow_system + + +class TestYearAwareInvestParameters: + """Test the YearAwareInvestParameters interface.""" + + def test_basic_initialization(self): + """Test basic parameter initialization.""" + params = fx.YearAwareInvestParameters( + minimum_size=10, + maximum_size=100, + ) + + assert params.minimum_size == 10 + assert params.maximum_size == 100 + assert params.fixed_size is None + assert not params.allow_divestment + assert params.fixed_start_year is None + assert params.fixed_end_year is None + assert params.fixed_duration is None + + def test_fixed_size_initialization(self): + """Test initialization with fixed size.""" + params = fx.YearAwareInvestParameters(fixed_size=50) + + assert params.minimum_or_fixed_size == 50 + assert params.maximum_or_fixed_size == 50 + assert params.is_fixed_size + + def test_timing_constraints_initialization(self): + """Test initialization with various timing constraints.""" + params = fx.YearAwareInvestParameters( + fixed_start_year=2, + minimum_duration=3, + maximum_duration=5, + earliest_end_year=4, + ) + + assert params.fixed_start_year == 2 + assert params.minimum_duration == 3 + assert params.maximum_duration == 5 + assert params.earliest_end_year == 4 + + def test_effects_initialization(self): + """Test initialization with effects.""" + params = fx.YearAwareInvestParameters( + effects_of_investment={'costs': 1000}, + effects_of_investment_per_size={'costs': 100}, + allow_divestment=True, + effects_of_divestment={'costs': 500}, + effects_of_divestment_per_size={'costs': 50}, + ) + + assert params.effects_of_investment == {'costs': 1000} + assert params.effects_of_investment_per_size == {'costs': 100} + assert params.allow_divestment + assert params.effects_of_divestment == {'costs': 500} + assert params.effects_of_divestment_per_size == {'costs': 50} + + def test_property_methods(self): + """Test property methods.""" + # Test with fixed size + params_fixed = fx.YearAwareInvestParameters(fixed_size=50) + assert params_fixed.minimum_or_fixed_size == 50 + assert params_fixed.maximum_or_fixed_size == 50 + assert params_fixed.is_fixed_size + + # Test with min/max size + params_range = fx.YearAwareInvestParameters(minimum_size=10, maximum_size=100) + assert params_range.minimum_or_fixed_size == 10 + assert params_range.maximum_or_fixed_size == 100 + assert not params_range.is_fixed_size + + +class TestYearAwareInvestmentModelDirect: + """Test the YearAwareInvestmentModel class directly with linopy.""" + + def test_flow_invest_new(self, flow_system): + flow = fx.Flow( + 'Wärme', + bus='Fernwärme', + size=fx.InvestTimingParameters(start_year=2021, end_year=2023, minimum_size=20, maximum_size=1000, effects_of_investment_per_size=200), + relative_maximum=np.linspace(0.5, 1, flow_system.timesteps.size), + ) + + flow_system.add_elements(fx.Source('Source', source=flow)) + calculation = fx.FullCalculation('GenericName', flow_system) + calculation.do_modeling() + #calculation.model.add_constraints(calculation.model['Source(Wärme)|decrease'].isel(year=2) == 1) + calculation.solve(fx.solvers.HighsSolver(0, 60)) + + ds = calculation.results['Source'].solution + filtered_ds = ds[[v for v in ds.data_vars if ds[v].dims == ('year',)]] + print(filtered_ds.to_pandas().T) + + print('##') + + From 5ecb3c9ffb23e3c81c5c77e96a96a2f0f6557a9f Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 31 Jul 2025 17:16:03 +0200 Subject: [PATCH 281/448] Add size_changes variables --- flixopt/features.py | 106 ++++++++++++++++++++------------------------ flixopt/modeling.py | 77 ++++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+), 57 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index d7dd03cdc..db645c612 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -177,86 +177,68 @@ def _basic_modeling(self): bounds=(size_min, size_max), ) - increase = self.add_variables( + has_increase = self.add_variables( binary=True, coords=self._model.get_coords(['year', 'scenario']), - short_name='increase', + short_name='has_increase', ) - decrease = self.add_variables( + has_decrease = self.add_variables( binary=True, coords=self._model.get_coords(['year', 'scenario']), - short_name='decrease', + short_name='has_decrease', ) BoundingPatterns.state_transition_bounds( self, state_variable=self.is_invested, - switch_on=increase, - switch_off=decrease, + switch_on=has_increase, + switch_off=has_decrease, name=self.is_invested.name, previous_state=0, coord='year', ) + self.add_variables( + coords=self._model.get_coords(['year', 'scenario']), + short_name='size_increase', + lower=0, + upper=size_max, + ) + + self.add_variables( + coords=self._model.get_coords(['year', 'scenario']), + short_name='size_decrease', + lower=0, + upper=size_max, + ) + # Ensures size can only change when increase or decrease is 1 BoundingPatterns.continuous_transition_bounds( model=self, continuous_variable=self.size, - switch_on=increase, - switch_off=decrease, + switch_on=has_increase, + switch_off=has_decrease, name=self.size.name, max_change=size_max, previous_value=0, coord='year', ) - self.add_constraints(self.increase.sum('year') <= 1, name=f'{self.increase.name}|count') - self.add_constraints(self.decrease.sum('year') <= 1, name=f'{self.decrease.name}|count') - - def _add_effects(self): - """Add investment effects to the model.""" - - if self.parameters.effects_of_investment: - # One-time effects when investment is made - increase = self._variables.get('increase') - if increase is not None: - self._model.effects.add_share_to_effects( - name=self.label_of_element, - expressions={ - effect: increase * factor for effect, factor in self.parameters.effects_of_investment.items() - }, - target='invest', - ) - - if self.parameters.effects_of_investment_per_size: - # Annual effects proportional to investment size - self._model.effects.add_share_to_effects( - name=self.label_of_element, - expressions={ - effect: self.size * factor - for effect, factor in self.parameters.effects_of_investment_per_size.items() - }, - target='invest', - ) + self.add_constraints(self.has_increase.sum('year') <= 1, name=f'{self.has_increase.name}|count') + self.add_constraints(self.has_decrease.sum('year') <= 1, name=f'{self.has_decrease.name}|count') - def _fixed_start_fixed_end_constraints(self): - """Add constraints for fixed start year.""" - self.add_constraints( - self.increase.sel(year=self.parameters.start_year) == self.decrease.sel(year=self.parameters.end_year), - name=f'{self.increase.name}|fixed_start_and_end', + BoundingPatterns.link_changes_to_level_with_binaries( + self, + level_variable=self.size, + increase_variable=self.size_increase, + decrease_variable=self.size_decrease, + increase_binary=self.has_increase, + decrease_binary=self.has_decrease, + name=f'{self.label_of_element}|size_changes', + max_change=size_max, + initial_level=0, + coord='year', ) - if not self.parameters.optional: - self.add_constraints( - self.increase.sel(year=self.parameters.start_year) == 1, - name=f'{self.increase.name}|non_optional', - ) - - def _fixed_end_constraints(self): - pass - - def _fixed_start_constraints(self): - pass - @property def size(self) -> linopy.Variable: """Investment size variable""" @@ -270,14 +252,24 @@ def is_invested(self) -> Optional[linopy.Variable]: return self._variables['is_invested'] @property - def increase(self) -> linopy.Variable: + def has_increase(self) -> linopy.Variable: """Binary increase decision variable""" - return self._variables['increase'] + return self._variables['has_increase'] + + @property + def has_decrease(self) -> linopy.Variable: + """Binary decrease decision variable""" + return self._variables['has_decrease'] @property - def decrease(self) -> linopy.Variable: + def size_decrease(self) -> linopy.Variable: """Binary decrease decision variable""" - return self._variables['decrease'] + return self._variables['size_decrease'] + + @property + def size_increase(self) -> linopy.Variable: + """Binary increase decision variable""" + return self._variables['size_increase'] class FixedStartFixedEndInvestmentTimingModel(InvestmentTimingModel): diff --git a/flixopt/modeling.py b/flixopt/modeling.py index f523346cc..ef7cdc27a 100644 --- a/flixopt/modeling.py +++ b/flixopt/modeling.py @@ -654,3 +654,80 @@ def continuous_transition_bounds( ) return transition_upper, transition_lower, initial_upper, initial_lower + + @staticmethod + def link_changes_to_level_with_binaries( + model: Submodel, + level_variable: linopy.Variable, + increase_variable: linopy.Variable, + decrease_variable: linopy.Variable, + increase_binary: linopy.Variable, + decrease_binary: linopy.Variable, + name: str, + max_change: Union[float, xr.DataArray], + initial_level: Union[float, xr.DataArray] = 0, + coord: str = 'year', + ) -> Tuple[linopy.Constraint, linopy.Constraint, linopy.Constraint, linopy.Constraint, linopy.Constraint]: + """ + Link changes to level evolution with binary control and mutual exclusivity. + + Creates the complete constraint system for ALL time periods: + 1. level[0] = initial_level + increase[0] - decrease[0] + 2. level[t] = level[t-1] + increase[t] - decrease[t] ∀t > 0 + 3. increase[t] <= max_change * increase_binary[t] ∀t + 4. decrease[t] <= max_change * decrease_binary[t] ∀t + 5. increase_binary[t] + decrease_binary[t] <= 1 ∀t + + Args: + model: The submodel to add constraints to + increase_variable: Incremental additions for ALL periods (>= 0) + decrease_variable: Incremental reductions for ALL periods (>= 0) + increase_binary: Binary indicators for increases for ALL periods + decrease_binary: Binary indicators for decreases for ALL periods + level_variable: Level variable for ALL periods + name: Base name for constraints + max_change: Maximum change per period + initial_level: Starting level before first period + coord: Time coordinate name + + Returns: + Tuple of (initial_constraint, transition_constraints, increase_bounds, decrease_bounds, mutual_exclusion) + """ + if not isinstance(model, Submodel): + raise ValueError('BoundingPatterns.link_changes_to_level_with_binaries() can only be used with a Submodel') + + # 1. Initial period: level[0] = initial_level + increase[0] - decrease[0] + initial_constraint = model.add_constraints( + level_variable.isel({coord: 0}) + == initial_level + increase_variable.isel({coord: 0}) - decrease_variable.isel({coord: 0}), + name=f'{name}|initial_level', + ) + + # 2. Transition periods: level[t] = level[t-1] + increase[t] - decrease[t] for t > 0 + transition_constraints = model.add_constraints( + level_variable.isel({coord: slice(1, None)}) + == level_variable.isel({coord: slice(None, -1)}) + + increase_variable.isel({coord: slice(1, None)}) + - decrease_variable.isel({coord: slice(1, None)}), + name=f'{name}|transitions', + ) + + # 3. Increase bounds: increase[t] <= max_change * increase_binary[t] for all t + increase_bounds = model.add_constraints( + increase_variable <= increase_binary * max_change, + name=f'{name}|increase_bounds', + ) + + # 4. Decrease bounds: decrease[t] <= max_change * decrease_binary[t] for all t + decrease_bounds = model.add_constraints( + decrease_variable <= decrease_binary * max_change, + name=f'{name}|decrease_bounds', + ) + + # 5. Mutual exclusivity: increase_binary[t] + decrease_binary[t] <= 1 for all t + mutual_exclusion = model.add_constraints( + increase_binary + decrease_binary <= 1, + name=f'{name}|mutual_exclusion', + ) + + return initial_constraint, transition_constraints, increase_bounds, decrease_bounds, mutual_exclusion From b1c9b71f29871369b37cfb1a3e4a2b7dc4198b6e Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 31 Jul 2025 17:25:57 +0200 Subject: [PATCH 282/448] Add size_changes variables --- flixopt/features.py | 79 +++++++++++++++++++++++++++++++-------------- 1 file changed, 54 insertions(+), 25 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index db645c612..a1f084240 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -177,12 +177,12 @@ def _basic_modeling(self): bounds=(size_min, size_max), ) - has_increase = self.add_variables( + self.add_variables( binary=True, coords=self._model.get_coords(['year', 'scenario']), short_name='has_increase', ) - has_decrease = self.add_variables( + self.add_variables( binary=True, coords=self._model.get_coords(['year', 'scenario']), short_name='has_decrease', @@ -190,12 +190,14 @@ def _basic_modeling(self): BoundingPatterns.state_transition_bounds( self, state_variable=self.is_invested, - switch_on=has_increase, - switch_off=has_decrease, + switch_on=self.has_increase, + switch_off=self.has_decrease, name=self.is_invested.name, previous_state=0, coord='year', ) + self.add_constraints(self.has_increase.sum('year') <= 1, name=f'{self.has_increase.name}|count') + self.add_constraints(self.has_decrease.sum('year') <= 1, name=f'{self.has_decrease.name}|count') self.add_variables( coords=self._model.get_coords(['year', 'scenario']), @@ -203,29 +205,12 @@ def _basic_modeling(self): lower=0, upper=size_max, ) - self.add_variables( coords=self._model.get_coords(['year', 'scenario']), short_name='size_decrease', lower=0, upper=size_max, ) - - # Ensures size can only change when increase or decrease is 1 - BoundingPatterns.continuous_transition_bounds( - model=self, - continuous_variable=self.size, - switch_on=has_increase, - switch_off=has_decrease, - name=self.size.name, - max_change=size_max, - previous_value=0, - coord='year', - ) - - self.add_constraints(self.has_increase.sum('year') <= 1, name=f'{self.has_increase.name}|count') - self.add_constraints(self.has_decrease.sum('year') <= 1, name=f'{self.has_decrease.name}|count') - BoundingPatterns.link_changes_to_level_with_binaries( self, level_variable=self.size, @@ -239,6 +224,49 @@ def _basic_modeling(self): coord='year', ) + def _add_effects(self): + """Add investment effects to the model.""" + + if self.parameters.effects_of_investment: + # One-time effects when investment is made + increase = self._variables.get('increase') + if increase is not None: + self._model.effects.add_share_to_effects( + name=self.label_of_element, + expressions={ + effect: increase * factor for effect, factor in self.parameters.effects_of_investment.items() + }, + target='invest', + ) + + if self.parameters.effects_of_investment_per_size: + # Annual effects proportional to investment size + self._model.effects.add_share_to_effects( + name=self.label_of_element, + expressions={ + effect: self.size * factor + for effect, factor in self.parameters.effects_of_investment_per_size.items() + }, + target='invest', + ) + + def _fixed_start_fixed_end_constraints(self): + """Add constraints for fixed start year.""" + self.add_constraints( + self.has_increase.sel(year=self.parameters.start_year) + == self.has_decrease.sel(year=self.parameters.end_year), + name=f'{self.has_increase.name}|fixed_start_and_end', + ) + + if not self.parameters.optional: + self.add_constraints( + self.has_increase.sel(year=self.parameters.start_year) == 1, + name=f'{self.has_increase.name}|non_optional', + ) + + def _fixed_end_constraints(self): + pass + @property def size(self) -> linopy.Variable: """Investment size variable""" @@ -279,14 +307,15 @@ def _basic_modeling(self): super()._basic_modeling() self.add_constraints( - self.increase.sel(year=self.parameters.start_year) == self.decrease.sel(year=self.parameters.end_year), - name=f'{self.increase.name}|fixed_start_and_end', + self.has_increase.sel(year=self.parameters.start_year) + == self.has_decrease.sel(year=self.parameters.end_year), + name=f'{self.has_increase.name}|fixed_start_and_end', ) if not self.parameters.optional: self.add_constraints( - self.increase.sel(year=self.parameters.start_year) == 1, - name=f'{self.increase.name}|non_optional', + self.has_increase.sel(year=self.parameters.start_year) == 1, + name=f'{self.has_increase.name}|non_optional', ) From 3c98553a0c53323424f34c19b42c289120899abe Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 31 Jul 2025 18:07:45 +0200 Subject: [PATCH 283/448] Improve InvestmentModel --- flixopt/features.py | 91 +++++++++++++++++++++++++++++--------------- flixopt/interface.py | 89 ++++++++++++++++++++++++++++++++++++------- tests/test_models.py | 25 ++++++++---- 3 files changed, 155 insertions(+), 50 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index a1f084240..44bea1329 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -149,10 +149,10 @@ def _do_modeling(self): self._basic_modeling() self._add_effects() - if self.parameters.start_year is not None and self.parameters.end_year is not None: - self._fixed_start_fixed_end_constraints() - elif self.parameters.end_year is not None: - self._fixed_end_constraints() + if self.parameters.start_year is not None: + self._fixed_start_constraint() + if self.parameters.end_year is not None: + self._fixed_end_constraint() def _basic_modeling(self): size_min, size_max = self.parameters.minimum_or_fixed_size, self.parameters.maximum_or_fixed_size @@ -170,6 +170,20 @@ def _basic_modeling(self): short_name='is_invested', ) + if self.parameters.optional_investment: + self.add_variables( + binary=True, + coords=self._model.get_coords(['scenario']), + short_name='investment_used', + ) + + if self.parameters.optional_divestment: + self.add_variables( + binary=True, + coords=self._model.get_coords(['scenario']), + short_name='divestment_used', + ) + BoundingPatterns.bounds_with_state( self, variable=self.size, @@ -180,12 +194,12 @@ def _basic_modeling(self): self.add_variables( binary=True, coords=self._model.get_coords(['year', 'scenario']), - short_name='has_increase', + short_name='size|has_increase', ) self.add_variables( binary=True, coords=self._model.get_coords(['year', 'scenario']), - short_name='has_decrease', + short_name='size|has_decrease', ) BoundingPatterns.state_transition_bounds( self, @@ -196,20 +210,26 @@ def _basic_modeling(self): previous_state=0, coord='year', ) - self.add_constraints(self.has_increase.sum('year') <= 1, name=f'{self.has_increase.name}|count') - self.add_constraints(self.has_decrease.sum('year') <= 1, name=f'{self.has_decrease.name}|count') + self.add_constraints( + self.has_increase.sum('year') == (self.investment_used if self.investment_used is not None else 1), + name=f'{self.has_increase.name}|count', + ) + self.add_constraints( + self.has_decrease.sum('year') == (self.divestment_used if self.divestment_used is not None else 1), + name=f'{self.has_decrease.name}|count', + ) self.add_variables( coords=self._model.get_coords(['year', 'scenario']), - short_name='size_increase', + short_name='size|increase', lower=0, upper=size_max, ) self.add_variables( coords=self._model.get_coords(['year', 'scenario']), - short_name='size_decrease', + short_name='size|decrease', lower=0, - upper=size_max, + upper=CONFIG.modeling.BIG, ) BoundingPatterns.link_changes_to_level_with_binaries( self, @@ -218,7 +238,7 @@ def _basic_modeling(self): decrease_variable=self.size_decrease, increase_binary=self.has_increase, decrease_binary=self.has_decrease, - name=f'{self.label_of_element}|size_changes', + name=f'{self.label_of_element}|size|changes', max_change=size_max, initial_level=0, coord='year', @@ -250,22 +270,19 @@ def _add_effects(self): target='invest', ) - def _fixed_start_fixed_end_constraints(self): - """Add constraints for fixed start year.""" + def _fixed_start_constraint(self): self.add_constraints( self.has_increase.sel(year=self.parameters.start_year) - == self.has_decrease.sel(year=self.parameters.end_year), - name=f'{self.has_increase.name}|fixed_start_and_end', + == (self.investment_used if self.investment_used is not None else 1), + short_name='size|changes|fixed_start', ) - if not self.parameters.optional: - self.add_constraints( - self.has_increase.sel(year=self.parameters.start_year) == 1, - name=f'{self.has_increase.name}|non_optional', - ) - - def _fixed_end_constraints(self): - pass + def _fixed_end_constraint(self): + self.add_constraints( + self.has_decrease.sel(year=self.parameters.end_year) + == (self.divestment_used if self.divestment_used is not None else 1), + short_name='size|changes|fixed_end', + ) @property def size(self) -> linopy.Variable: @@ -282,22 +299,36 @@ def is_invested(self) -> Optional[linopy.Variable]: @property def has_increase(self) -> linopy.Variable: """Binary increase decision variable""" - return self._variables['has_increase'] + return self._variables['size|has_increase'] @property def has_decrease(self) -> linopy.Variable: """Binary decrease decision variable""" - return self._variables['has_decrease'] + return self._variables['size|has_decrease'] @property def size_decrease(self) -> linopy.Variable: """Binary decrease decision variable""" - return self._variables['size_decrease'] + return self._variables['size|decrease'] @property def size_increase(self) -> linopy.Variable: """Binary increase decision variable""" - return self._variables['size_increase'] + return self._variables['size|increase'] + + @property + def investment_used(self) -> Optional[linopy.Variable]: + """Binary investment decision variable""" + if 'investment_used' not in self._variables: + return None + return self._variables['investment_used'] + + @property + def divestment_used(self) -> Optional[linopy.Variable]: + """Binary investment decision variable""" + if 'divestment_used' not in self._variables: + return None + return self._variables['divestment_used'] class FixedStartFixedEndInvestmentTimingModel(InvestmentTimingModel): @@ -309,13 +340,13 @@ def _basic_modeling(self): self.add_constraints( self.has_increase.sel(year=self.parameters.start_year) == self.has_decrease.sel(year=self.parameters.end_year), - name=f'{self.has_increase.name}|fixed_start_and_end', + short_name='size|changes|fixed_start_and_end', ) if not self.parameters.optional: self.add_constraints( self.has_increase.sel(year=self.parameters.start_year) == 1, - name=f'{self.has_increase.name}|non_optional', + name='size|changes|non_optional', ) diff --git a/flixopt/interface.py b/flixopt/interface.py index 1825ea773..3763d01be 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -336,7 +336,7 @@ def is_fixed_size(self) -> bool: # Variant 1: Fixed Start and End Year @register_class_for_io -class InvestTimingParameters(_BaseYearAwareInvestParameters): +class InvestTimingParameters(Interface): """ Investment with fixed start and end years. @@ -348,11 +348,13 @@ def __init__( self, start_year: Optional[int] = None, end_year: Optional[int] = None, - # Base parameters minimum_size: Optional[Scalar] = None, maximum_size: Optional[Scalar] = None, fixed_size: Optional[Scalar] = None, - optional: bool = False, + optional_investment: bool = False, + optional_divestment: bool = False, + fix_effects: Optional['NonTemporalEffectsUser'] = None, + specific_effects: Optional['NonTemporalEffectsUser'] = None, # costs per Flow-Unit/Storage-Size/... effects_of_investment_per_size: Optional['NonTemporalEffectsUser'] = None, effects_of_investment: Optional['NonTemporalEffectsUser'] = None, ): @@ -362,23 +364,41 @@ def __init__( Args: start_year: Year when investment must start (0-indexed). end_year: Year when investment must end (0-indexed). - **kwargs: Base parameters (size, effects) + minimum_size: Minimum possible size of the investment. + maximum_size: Maximum possible size of the investment. + fixed_size: If specified, investment size is fixed to this value. + optional_investment: If False, the investment is mandatory. + optional_divestment: If False, the divestment is mandatory. + specific_effects: Specific costs, e.g., in €/kW_nominal or €/m²_nominal. + Example: {costs: 3, CO2: 0.3} with costs and CO2 representing an Object of class Effect + (Attention: Annualize costs to chosen period!) + effects_of_investment: Effects depending on when an investment decision is made. These can occur in the investment year or in multiple years. + If the effects need to occur in multiple years, you need to pass an xr.DataArray with the coord 'year_of_investment'. + Example: {'costs': 1000} applies 1000 to costs in the investment year. + """ - super().__init__( - minimum_size=minimum_size, - maximum_size=maximum_size, - fixed_size=fixed_size, - optional=optional, - effects_of_investment_per_size=effects_of_investment_per_size, - effects_of_investment=effects_of_investment, - ) + self.minimum_size = minimum_size if minimum_size is not None else CONFIG.modeling.EPSILON + self.maximum_size = maximum_size if maximum_size is not None else CONFIG.modeling.BIG + self.fixed_size = fixed_size + self.optional_investment = optional_investment + self.optional_divestment = optional_divestment self.start_year = start_year self.end_year = end_year + self.fix_effects: 'NonTemporalEffectsUser' = fix_effects if fix_effects is not None else {} + self.specific_effects: 'NonTemporalEffectsUser' = specific_effects if specific_effects is not None else {} + self.effects_of_investment_per_size: 'NonTemporalEffectsUser' = ( + effects_of_investment_per_size if effects_of_investment_per_size is not None else {} + ) + self.effects_of_investment: 'NonTemporalEffectsUser' = ( + effects_of_investment if effects_of_investment is not None else {} + ) + def _plausibility_checks(self, flow_system): """Validate parameter consistency.""" - super()._plausibility_checks(flow_system) + if flow_system.years is None: + raise ValueError("YearAwareInvestParameters requires the flow_system to have a 'years' dimension.") if self.start_year is None and self.end_year is None: raise ValueError('Either start_year or end_year must be specified.') @@ -399,6 +419,49 @@ def duration(self) -> int: """Get the investment duration.""" return self.end_year - self.start_year + 1 + def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): + """Transform all parameter data to match the flow system's coordinate structure.""" + self._plausibility_checks(flow_system) + + self.effects_of_investment_per_size = flow_system.fit_effects_to_model_coords( + label_prefix=name_prefix, + effect_values=self.effects_of_investment_per_size, + label_suffix='effects_of_investment_per_size', + has_time_dim=False, + ) + self.effects_of_investment = flow_system.fit_effects_to_model_coords( + label_prefix=name_prefix, + effect_values=self.effects_of_investment, + label_suffix='effects_of_investment', + has_time_dim=False, + ) + + self.minimum_size = flow_system.fit_to_model_coords( + f'{name_prefix}|minimum_size', self.minimum_size, has_time_dim=False + ) + self.maximum_size = flow_system.fit_to_model_coords( + f'{name_prefix}|maximum_size', self.maximum_size, has_time_dim=False + ) + if self.fixed_size is not None: + self.fixed_size = flow_system.fit_to_model_coords( + f'{name_prefix}|fixed_size', self.fixed_size, has_time_dim=False + ) + + @property + def minimum_or_fixed_size(self) -> NonTemporalData: + """Get the effective minimum size (fixed size takes precedence).""" + return self.fixed_size if self.fixed_size is not None else self.minimum_size + + @property + def maximum_or_fixed_size(self) -> NonTemporalData: + """Get the effective maximum size (fixed size takes precedence).""" + return self.fixed_size if self.fixed_size is not None else self.maximum_size + + @property + def is_fixed_size(self) -> bool: + """Check if investment size is fixed.""" + return self.fixed_size is not None + @register_class_for_io class OnOffParameters(Interface): diff --git a/tests/test_models.py b/tests/test_models.py index c446313b8..97480b900 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,11 +1,22 @@ +import linopy import numpy as np import pandas as pd import pytest -import linopy import flixopt as fx -from .conftest import assert_conequal, assert_sets_equal, assert_var_equal, create_linopy_model, LoadProfiles, Effects, Sinks, Sources, Buses +from .conftest import ( + Buses, + Effects, + LoadProfiles, + Sinks, + Sources, + assert_conequal, + assert_sets_equal, + assert_var_equal, + create_linopy_model, +) + @pytest.fixture def flow_system() -> fx.FlowSystem: @@ -106,20 +117,20 @@ def test_flow_invest_new(self, flow_system): flow = fx.Flow( 'Wärme', bus='Fernwärme', - size=fx.InvestTimingParameters(start_year=2021, end_year=2023, minimum_size=20, maximum_size=1000, effects_of_investment_per_size=200), + size=fx.InvestTimingParameters( + start_year=2021, end_year=2023, minimum_size=900, maximum_size=1000, effects_of_investment_per_size=200 + ), relative_maximum=np.linspace(0.5, 1, flow_system.timesteps.size), ) flow_system.add_elements(fx.Source('Source', source=flow)) calculation = fx.FullCalculation('GenericName', flow_system) calculation.do_modeling() - #calculation.model.add_constraints(calculation.model['Source(Wärme)|decrease'].isel(year=2) == 1) + # calculation.model.add_constraints(calculation.model['Source(Wärme)|decrease'].isel(year=2) == 1) calculation.solve(fx.solvers.HighsSolver(0, 60)) ds = calculation.results['Source'].solution filtered_ds = ds[[v for v in ds.data_vars if ds[v].dims == ('year',)]] - print(filtered_ds.to_pandas().T) + print(filtered_ds.round(0).to_pandas().T) print('##') - - From 5688c8ffeef6a3c57b035547f8324016574d488e Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 4 Aug 2025 11:59:23 +0200 Subject: [PATCH 284/448] Improve InvestmentModel --- flixopt/features.py | 51 +++++++++++++++++++++++++++------------------ 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index 44bea1329..18c43fdb2 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -157,6 +157,7 @@ def _do_modeling(self): def _basic_modeling(self): size_min, size_max = self.parameters.minimum_or_fixed_size, self.parameters.maximum_or_fixed_size + ######################################################################## self.add_variables( short_name='size', lower=0, @@ -170,20 +171,6 @@ def _basic_modeling(self): short_name='is_invested', ) - if self.parameters.optional_investment: - self.add_variables( - binary=True, - coords=self._model.get_coords(['scenario']), - short_name='investment_used', - ) - - if self.parameters.optional_divestment: - self.add_variables( - binary=True, - coords=self._model.get_coords(['scenario']), - short_name='divestment_used', - ) - BoundingPatterns.bounds_with_state( self, variable=self.size, @@ -191,6 +178,7 @@ def _basic_modeling(self): bounds=(size_min, size_max), ) + ######################################################################## self.add_variables( binary=True, coords=self._model.get_coords(['year', 'scenario']), @@ -210,15 +198,38 @@ def _basic_modeling(self): previous_state=0, coord='year', ) + + ######################################################################## + self.add_variables( + binary=True, + coords=self._model.get_coords(['scenario']), + short_name='size|investment_used', + ) + self.add_variables( + binary=True, + coords=self._model.get_coords(['scenario']), + short_name='size|divestment_used', + ) self.add_constraints( - self.has_increase.sum('year') == (self.investment_used if self.investment_used is not None else 1), + self.has_increase.sum('year') == self.investment_used, name=f'{self.has_increase.name}|count', ) self.add_constraints( - self.has_decrease.sum('year') == (self.divestment_used if self.divestment_used is not None else 1), + self.has_decrease.sum('year') == self.divestment_used, name=f'{self.has_decrease.name}|count', ) + if not self.parameters.optional_investment: + self.add_constraints( + self.investment_used == 1, + name='investment_used|fixed', + ) + if not self.parameters.optional_divestment: + self.add_constraints( + self.divestment_used == 1, + name='divestment_used|fixed', + ) + ######################################################################## self.add_variables( coords=self._model.get_coords(['year', 'scenario']), short_name='size|increase', @@ -319,16 +330,16 @@ def size_increase(self) -> linopy.Variable: @property def investment_used(self) -> Optional[linopy.Variable]: """Binary investment decision variable""" - if 'investment_used' not in self._variables: + if 'size|investment_used' not in self._variables: return None - return self._variables['investment_used'] + return self._variables['size|investment_used'] @property def divestment_used(self) -> Optional[linopy.Variable]: """Binary investment decision variable""" - if 'divestment_used' not in self._variables: + if 'size|divestment_used' not in self._variables: return None - return self._variables['divestment_used'] + return self._variables['size|divestment_used'] class FixedStartFixedEndInvestmentTimingModel(InvestmentTimingModel): From 84c8f1518a2bb89838aa3db9133142bdbee22574 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 4 Aug 2025 12:51:23 +0200 Subject: [PATCH 285/448] Rename parameters --- flixopt/features.py | 17 ++++++++--------- flixopt/interface.py | 36 ++++++++++++++++++++---------------- tests/test_models.py | 18 +++++++++++------- 3 files changed, 39 insertions(+), 32 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index 18c43fdb2..e1a53df46 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -10,7 +10,6 @@ import numpy as np import xarray as xr -from .config import CONFIG from .core import FlowSystemDimensions, NonTemporalData, Scalar, TemporalData from .interface import ( InvestParameters, @@ -149,9 +148,9 @@ def _do_modeling(self): self._basic_modeling() self._add_effects() - if self.parameters.start_year is not None: + if self.parameters.year_of_investment is not None: self._fixed_start_constraint() - if self.parameters.end_year is not None: + if self.parameters.year_of_decommissioning is not None: self._fixed_end_constraint() def _basic_modeling(self): @@ -240,7 +239,7 @@ def _basic_modeling(self): coords=self._model.get_coords(['year', 'scenario']), short_name='size|decrease', lower=0, - upper=CONFIG.modeling.BIG, + upper=size_max, ) BoundingPatterns.link_changes_to_level_with_binaries( self, @@ -283,14 +282,14 @@ def _add_effects(self): def _fixed_start_constraint(self): self.add_constraints( - self.has_increase.sel(year=self.parameters.start_year) + self.has_increase.sel(year=self.parameters.year_of_investment) == (self.investment_used if self.investment_used is not None else 1), short_name='size|changes|fixed_start', ) def _fixed_end_constraint(self): self.add_constraints( - self.has_decrease.sel(year=self.parameters.end_year) + self.has_decrease.sel(year=self.parameters.year_of_decommissioning) == (self.divestment_used if self.divestment_used is not None else 1), short_name='size|changes|fixed_end', ) @@ -349,14 +348,14 @@ def _basic_modeling(self): super()._basic_modeling() self.add_constraints( - self.has_increase.sel(year=self.parameters.start_year) - == self.has_decrease.sel(year=self.parameters.end_year), + self.has_increase.sel(year=self.parameters.year_of_investment) + == self.has_decrease.sel(year=self.parameters.year_of_decommissioning), short_name='size|changes|fixed_start_and_end', ) if not self.parameters.optional: self.add_constraints( - self.has_increase.sel(year=self.parameters.start_year) == 1, + self.has_increase.sel(year=self.parameters.year_of_investment) == 1, name='size|changes|non_optional', ) diff --git a/flixopt/interface.py b/flixopt/interface.py index 3763d01be..b564c8c68 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -346,8 +346,8 @@ class InvestTimingParameters(Interface): def __init__( self, - start_year: Optional[int] = None, - end_year: Optional[int] = None, + year_of_investment: Optional[int] = None, + year_of_decommissioning: Optional[int] = None, minimum_size: Optional[Scalar] = None, maximum_size: Optional[Scalar] = None, fixed_size: Optional[Scalar] = None, @@ -362,8 +362,8 @@ def __init__( Initialize fixed start and end year investment parameters. Args: - start_year: Year when investment must start (0-indexed). - end_year: Year when investment must end (0-indexed). + year_of_investment: Year in which the investment occurs. The unit is there present from this year onwards. + year_of_decommissioning: Year in which the unit is decommissioned. The unit is there present up to this year (exclusive). minimum_size: Minimum possible size of the investment. maximum_size: Maximum possible size of the investment. fixed_size: If specified, investment size is fixed to this value. @@ -383,8 +383,8 @@ def __init__( self.optional_investment = optional_investment self.optional_divestment = optional_divestment - self.start_year = start_year - self.end_year = end_year + self.year_of_investment = year_of_investment + self.year_of_decommissioning = year_of_decommissioning self.fix_effects: 'NonTemporalEffectsUser' = fix_effects if fix_effects is not None else {} self.specific_effects: 'NonTemporalEffectsUser' = specific_effects if specific_effects is not None else {} @@ -400,24 +400,28 @@ def _plausibility_checks(self, flow_system): if flow_system.years is None: raise ValueError("YearAwareInvestParameters requires the flow_system to have a 'years' dimension.") - if self.start_year is None and self.end_year is None: - raise ValueError('Either start_year or end_year must be specified.') + if self.year_of_investment is None and self.year_of_decommissioning is None: + raise ValueError('Either year_of_investment or year_of_decommissioning must be specified.') - if self.start_year < flow_system.years[0] or self.start_year > flow_system.years[-1]: + if self.year_of_investment < flow_system.years[0] or self.year_of_investment > flow_system.years[-1]: raise ValueError( - f'start_year ({self.start_year}) must be between {flow_system.years[0]} and {flow_system.years[-1]}' + f'year_of_investment ({self.year_of_investment}) must be between {flow_system.years[0]} and {flow_system.years[-1]}' ) - if self.end_year < flow_system.years[0] or self.end_year > flow_system.years[-1]: - raise ValueError(f'end_year ({self.end_year}) must be between 0 and {flow_system.years[-1]}') + if self.year_of_decommissioning < flow_system.years[0] or self.year_of_decommissioning > flow_system.years[-1]: + raise ValueError( + f'year_of_decommissioning ({self.year_of_decommissioning}) must be between {flow_system.years[0]} and {flow_system.years[-1]}' + ) - if self.start_year >= self.end_year: - raise ValueError(f'start_year ({self.start_year}) must be before end_year ({self.end_year})') + if self.year_of_investment >= self.year_of_decommissioning: + raise ValueError( + f'year_of_investment ({self.year_of_investment}) must be before year_of_decommissioning ({self.year_of_decommissioning})' + ) @property def duration(self) -> int: - """Get the investment duration.""" - return self.end_year - self.start_year + 1 + """Get the duration of the investment.""" + return self.year_of_decommissioning - self.year_of_investment def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): """Transform all parameter data to match the flow system's coordinate structure.""" diff --git a/tests/test_models.py b/tests/test_models.py index 97480b900..ac040b1e1 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -53,8 +53,8 @@ def test_basic_initialization(self): assert params.maximum_size == 100 assert params.fixed_size is None assert not params.allow_divestment - assert params.fixed_start_year is None - assert params.fixed_end_year is None + assert params.fixed_year_of_investment is None + assert params.fixed_year_of_decommissioning is None assert params.fixed_duration is None def test_fixed_size_initialization(self): @@ -68,16 +68,16 @@ def test_fixed_size_initialization(self): def test_timing_constraints_initialization(self): """Test initialization with various timing constraints.""" params = fx.YearAwareInvestParameters( - fixed_start_year=2, + fixed_year_of_investment=2, minimum_duration=3, maximum_duration=5, - earliest_end_year=4, + earliest_year_of_decommissioning=4, ) - assert params.fixed_start_year == 2 + assert params.fixed_year_of_investment == 2 assert params.minimum_duration == 3 assert params.maximum_duration == 5 - assert params.earliest_end_year == 4 + assert params.earliest_year_of_decommissioning == 4 def test_effects_initialization(self): """Test initialization with effects.""" @@ -118,7 +118,11 @@ def test_flow_invest_new(self, flow_system): 'Wärme', bus='Fernwärme', size=fx.InvestTimingParameters( - start_year=2021, end_year=2023, minimum_size=900, maximum_size=1000, effects_of_investment_per_size=200 + year_of_investment=2021, + year_of_decommissioning=2023, + minimum_size=900, + maximum_size=1000, + effects_of_investment_per_size=200, ), relative_maximum=np.linspace(0.5, 1, flow_system.timesteps.size), ) From 566bcdf8ce408d3b9286c3e90d6b99404c94d49d Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 4 Aug 2025 12:55:06 +0200 Subject: [PATCH 286/448] remove old code --- flixopt/features.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index e1a53df46..8950c8849 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -341,25 +341,6 @@ def divestment_used(self) -> Optional[linopy.Variable]: return self._variables['size|divestment_used'] -class FixedStartFixedEndInvestmentTimingModel(InvestmentTimingModel): - parameters: InvestTimingParameters - - def _basic_modeling(self): - super()._basic_modeling() - - self.add_constraints( - self.has_increase.sel(year=self.parameters.year_of_investment) - == self.has_decrease.sel(year=self.parameters.year_of_decommissioning), - short_name='size|changes|fixed_start_and_end', - ) - - if not self.parameters.optional: - self.add_constraints( - self.has_increase.sel(year=self.parameters.year_of_investment) == 1, - name='size|changes|non_optional', - ) - - class OnOffModel(Submodel): """OnOff model using factory patterns""" From c334515d8fc090cb50c3bb3b04dc21098990147e Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 4 Aug 2025 13:37:50 +0200 Subject: [PATCH 287/448] Add a duration_in_years to the InvestTimingParameters --- flixopt/features.py | 45 ++++++++++++++++++++++++++++++-------------- flixopt/interface.py | 30 ++++++++++++++++++----------- 2 files changed, 50 insertions(+), 25 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index 8950c8849..88c94f97c 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -152,6 +152,8 @@ def _do_modeling(self): self._fixed_start_constraint() if self.parameters.year_of_decommissioning is not None: self._fixed_end_constraint() + if self.parameters.duration_in_years is not None: + self._fixed_duration_constraint() def _basic_modeling(self): size_min, size_max = self.parameters.minimum_or_fixed_size, self.parameters.maximum_or_fixed_size @@ -211,22 +213,32 @@ def _basic_modeling(self): ) self.add_constraints( self.has_increase.sum('year') == self.investment_used, - name=f'{self.has_increase.name}|count', + short_name='size|has_increase|count', ) self.add_constraints( self.has_decrease.sum('year') == self.divestment_used, - name=f'{self.has_decrease.name}|count', + short_name='size|has_decrease|count', ) if not self.parameters.optional_investment: self.add_constraints( self.investment_used == 1, - name='investment_used|fixed', + short_name='investment_used|fixed', ) if not self.parameters.optional_divestment: self.add_constraints( self.divestment_used == 1, - name='divestment_used|fixed', + short_name='divestment_used|fixed', ) + self.add_variables( + lower=0, + upper=self.parameters.duration_in_years if self.parameters.duration_in_years is not None else np.inf, + coords=self._model.get_coords(['scenario']), + short_name='duration', + ) + self.add_constraints( + self.duration == (self.is_invested * self._model.flow_system.years_per_year).sum('year'), + short_name='duration|fixed', + ) ######################################################################## self.add_variables( @@ -282,18 +294,22 @@ def _add_effects(self): def _fixed_start_constraint(self): self.add_constraints( - self.has_increase.sel(year=self.parameters.year_of_investment) - == (self.investment_used if self.investment_used is not None else 1), + self.has_increase.sel(year=self.parameters.year_of_investment) == self.investment_used, short_name='size|changes|fixed_start', ) def _fixed_end_constraint(self): self.add_constraints( - self.has_decrease.sel(year=self.parameters.year_of_decommissioning) - == (self.divestment_used if self.divestment_used is not None else 1), + self.has_decrease.sel(year=self.parameters.year_of_decommissioning) == self.divestment_used, short_name='size|changes|fixed_end', ) + def _fixed_duration_constraint(self): + self.add_constraints( + self.duration == self.parameters.duration_in_years, + short_name='size|duration|fixed', + ) + @property def size(self) -> linopy.Variable: """Investment size variable""" @@ -327,19 +343,20 @@ def size_increase(self) -> linopy.Variable: return self._variables['size|increase'] @property - def investment_used(self) -> Optional[linopy.Variable]: + def investment_used(self) -> linopy.Variable: """Binary investment decision variable""" - if 'size|investment_used' not in self._variables: - return None return self._variables['size|investment_used'] @property - def divestment_used(self) -> Optional[linopy.Variable]: + def divestment_used(self) -> linopy.Variable: """Binary investment decision variable""" - if 'size|divestment_used' not in self._variables: - return None return self._variables['size|divestment_used'] + @property + def duration(self) -> linopy.Variable: + """Investment duration variable""" + return self._variables['duration'] + class OnOffModel(Submodel): """OnOff model using factory patterns""" diff --git a/flixopt/interface.py b/flixopt/interface.py index b564c8c68..459e6fcd1 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -348,6 +348,7 @@ def __init__( self, year_of_investment: Optional[int] = None, year_of_decommissioning: Optional[int] = None, + duration_in_years: Optional[int] = None, minimum_size: Optional[Scalar] = None, maximum_size: Optional[Scalar] = None, fixed_size: Optional[Scalar] = None, @@ -362,8 +363,9 @@ def __init__( Initialize fixed start and end year investment parameters. Args: - year_of_investment: Year in which the investment occurs. The unit is there present from this year onwards. - year_of_decommissioning: Year in which the unit is decommissioned. The unit is there present up to this year (exclusive). + year_of_investment: Year in which the investment occurs (inclusive). Present from this year onwards. + year_of_decommissioning: Year in which the unit is decommissioned (exclusive). Present up to this year. + duration_in_years: Duration of the investment in years. minimum_size: Minimum possible size of the investment. maximum_size: Maximum possible size of the investment. fixed_size: If specified, investment size is fixed to this value. @@ -385,6 +387,7 @@ def __init__( self.year_of_investment = year_of_investment self.year_of_decommissioning = year_of_decommissioning + self.duration_in_years = duration_in_years self.fix_effects: 'NonTemporalEffectsUser' = fix_effects if fix_effects is not None else {} self.specific_effects: 'NonTemporalEffectsUser' = specific_effects if specific_effects is not None else {} @@ -400,15 +403,25 @@ def _plausibility_checks(self, flow_system): if flow_system.years is None: raise ValueError("YearAwareInvestParameters requires the flow_system to have a 'years' dimension.") - if self.year_of_investment is None and self.year_of_decommissioning is None: - raise ValueError('Either year_of_investment or year_of_decommissioning must be specified.') + if all( + [param is None for param in (self.year_of_investment, self.year_of_decommissioning, self.duration_in_years)] + ): + # TODO: Should this be an exception or rather a warning? Is there a valid use case for this? + # And a mathematically valid formulation (regarding the effects especially)? + raise ValueError( + 'Either year_of_investment, year_of_decommissioning or duration_in_years must be specified.' + ) - if self.year_of_investment < flow_system.years[0] or self.year_of_investment > flow_system.years[-1]: + if self.year_of_investment is not None and not ( + flow_system.years[0] <= self.year_of_investment <= flow_system.years[-1] + ): raise ValueError( f'year_of_investment ({self.year_of_investment}) must be between {flow_system.years[0]} and {flow_system.years[-1]}' ) - if self.year_of_decommissioning < flow_system.years[0] or self.year_of_decommissioning > flow_system.years[-1]: + if self.year_of_decommissioning is not None and not ( + flow_system.years[0] <= self.year_of_decommissioning <= flow_system.years[-1] + ): raise ValueError( f'year_of_decommissioning ({self.year_of_decommissioning}) must be between {flow_system.years[0]} and {flow_system.years[-1]}' ) @@ -418,11 +431,6 @@ def _plausibility_checks(self, flow_system): f'year_of_investment ({self.year_of_investment}) must be before year_of_decommissioning ({self.year_of_decommissioning})' ) - @property - def duration(self) -> int: - """Get the duration of the investment.""" - return self.year_of_decommissioning - self.year_of_investment - def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): """Transform all parameter data to match the flow system's coordinate structure.""" self._plausibility_checks(flow_system) From 98216e29af10ffb365351c99da55687939a1c23d Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 5 Aug 2025 08:52:22 +0200 Subject: [PATCH 288/448] Improve handling of fixed_duration --- flixopt/features.py | 55 +++++++++++++++++++++++++++++++++------------ 1 file changed, 41 insertions(+), 14 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index 88c94f97c..ffbfd7809 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -229,16 +229,6 @@ def _basic_modeling(self): self.divestment_used == 1, short_name='divestment_used|fixed', ) - self.add_variables( - lower=0, - upper=self.parameters.duration_in_years if self.parameters.duration_in_years is not None else np.inf, - coords=self._model.get_coords(['scenario']), - short_name='duration', - ) - self.add_constraints( - self.duration == (self.is_invested * self._model.flow_system.years_per_year).sum('year'), - short_name='duration|fixed', - ) ######################################################################## self.add_variables( @@ -305,10 +295,47 @@ def _fixed_end_constraint(self): ) def _fixed_duration_constraint(self): - self.add_constraints( - self.duration == self.parameters.duration_in_years, - short_name='size|duration|fixed', - ) + years = self._model.flow_system.years + years_of_decommissioning = years + self.parameters.duration_in_years + + # Filter and get actual selected years in one step + valid_mask = years_of_decommissioning <= years[-1] + if valid_mask.any(): + valid_years_of_investment = years[valid_mask] + valid_years_of_decommissioning = years_of_decommissioning[valid_mask] + actual_years_of_decommissioning = ( + self.has_decrease.sel(year=valid_years_of_decommissioning, method='bfill').coords['year'].values + ) + + # Warning for mismatched years + mismatched = valid_years_of_decommissioning != actual_years_of_decommissioning + for inv_year, target_year, actual_year in zip( + valid_years_of_investment[mismatched], + valid_years_of_decommissioning[mismatched], + actual_years_of_decommissioning[mismatched], + strict=False, + ): + logger.warning( + f'year_of_decommissioning {target_year} for {self.size.name} not in flow_system years. For an investment in year {inv_year}, the year_of_decommissioning is set to {actual_year}' + ) + + group = xr.DataArray( + actual_years_of_decommissioning, # values: the actual decommissioning years + coords={'year': valid_years_of_investment}, # coordinates: investment years + dims=['year'], + name='year_of_decommissioning', + ) + + # Now you can use proper xarray groupby + grouped_increases = ( + self.has_increase.sel(year=valid_years_of_investment).groupby(group).sum('year_of_decommissioning') + ) + + # Create constraints + self.add_constraints( + self.has_decrease.sel(year=grouped_increases.coords['year_of_decommissioning']) == grouped_increases, + short_name='size|changes|fixed_duration', + ) @property def size(self) -> linopy.Variable: From 7b45a19a4d2400a8bbbbdd120ac965eb8c238699 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 5 Aug 2025 10:42:23 +0200 Subject: [PATCH 289/448] Improve validation and make Investment/divestment optional by default --- flixopt/interface.py | 43 ++++++++++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/flixopt/interface.py b/flixopt/interface.py index 459e6fcd1..2772c73b6 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -334,7 +334,6 @@ def is_fixed_size(self) -> bool: return self.fixed_size is not None -# Variant 1: Fixed Start and End Year @register_class_for_io class InvestTimingParameters(Interface): """ @@ -352,8 +351,8 @@ def __init__( minimum_size: Optional[Scalar] = None, maximum_size: Optional[Scalar] = None, fixed_size: Optional[Scalar] = None, - optional_investment: bool = False, - optional_divestment: bool = False, + optional_investment: bool = True, + optional_divestment: bool = True, fix_effects: Optional['NonTemporalEffectsUser'] = None, specific_effects: Optional['NonTemporalEffectsUser'] = None, # costs per Flow-Unit/Storage-Size/... effects_of_investment_per_size: Optional['NonTemporalEffectsUser'] = None, @@ -412,21 +411,39 @@ def _plausibility_checks(self, flow_system): 'Either year_of_investment, year_of_decommissioning or duration_in_years must be specified.' ) - if self.year_of_investment is not None and not ( - flow_system.years[0] <= self.year_of_investment <= flow_system.years[-1] - ): - raise ValueError( - f'year_of_investment ({self.year_of_investment}) must be between {flow_system.years[0]} and {flow_system.years[-1]}' + if ( + sum( + [ + param is not None + for param in (self.year_of_investment, self.year_of_decommissioning, self.duration_in_years) + ] ) - - if self.year_of_decommissioning is not None and not ( - flow_system.years[0] <= self.year_of_decommissioning <= flow_system.years[-1] + > 2 ): + # TODO: Should this be an exception or rather a warning? raise ValueError( - f'year_of_decommissioning ({self.year_of_decommissioning}) must be between {flow_system.years[0]} and {flow_system.years[-1]}' + f'InvestmentParameters is overdefined. Not all of {self.year_of_investment=}, ' + f'{self.year_of_decommissioning=} and {self.duration_in_years=} can be specified.' ) - if self.year_of_investment >= self.year_of_decommissioning: + if self.year_of_investment is not None: + if not (flow_system.years[0] <= self.year_of_investment <= flow_system.years[-1]): + raise ValueError( + f'year_of_investment ({self.year_of_investment}) must be between ' + f'{flow_system.years[0]} and {flow_system.years[-1]}' + ) + + if self.year_of_decommissioning is not None: + if not (flow_system.years[0] <= self.year_of_decommissioning <= flow_system.years[-1]): + raise ValueError( + f'year_of_decommissioning ({self.year_of_decommissioning}) must be between {flow_system.years[0]} and {flow_system.years[-1]}' + ) + + if ( + self.year_of_decommissioning is not None + and self.year_of_investment is not None + and self.year_of_investment >= self.year_of_decommissioning + ): raise ValueError( f'year_of_investment ({self.year_of_investment}) must be before year_of_decommissioning ({self.year_of_decommissioning})' ) From b919c0a9dc26ba155bc03047cf9d46e9d5f65efe Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 5 Aug 2025 15:16:44 +0200 Subject: [PATCH 290/448] Rename some vars and improve previous handling --- flixopt/features.py | 55 ++++++++++++++++++++++++++++---------------- flixopt/interface.py | 16 ++++++++++--- flixopt/modeling.py | 6 ++--- 3 files changed, 51 insertions(+), 26 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index ffbfd7809..17a99d4eb 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -131,6 +131,16 @@ def is_invested(self) -> Optional[linopy.Variable]: class InvestmentTimingModel(Submodel): + """ + This feature model is used to model the timing of investments. + + Such an Investment is defined by a size, a year_of_investment, and a year_of_decommissioning. + In between these years, the size of the investment cannot vary. Outside, its 0. + The year_of_investment is defined as the year, in which the size increases from 0 to the chosen size. + The year_of_decommissioning is defined as the year, in which the size returns to 0. + The year_of_decommissioning can be after the years in the FlowSystem. THis results in no decommissioning. + """ + parameters: InvestTimingParameters def __init__( @@ -183,18 +193,18 @@ def _basic_modeling(self): self.add_variables( binary=True, coords=self._model.get_coords(['year', 'scenario']), - short_name='size|has_increase', + short_name='size|year_of_investment', ) self.add_variables( binary=True, coords=self._model.get_coords(['year', 'scenario']), - short_name='size|has_decrease', + short_name='size|year_of_decommissioning', ) BoundingPatterns.state_transition_bounds( self, state_variable=self.is_invested, - switch_on=self.has_increase, - switch_off=self.has_decrease, + switch_on=self.year_of_investment, + switch_off=self.year_of_decommissioning, name=self.is_invested.name, previous_state=0, coord='year', @@ -212,12 +222,12 @@ def _basic_modeling(self): short_name='size|divestment_used', ) self.add_constraints( - self.has_increase.sum('year') == self.investment_used, - short_name='size|has_increase|count', + self.year_of_investment.sum('year') == self.investment_used, + short_name='size|year_of_investment|count', ) self.add_constraints( - self.has_decrease.sum('year') == self.divestment_used, - short_name='size|has_decrease|count', + self.year_of_decommissioning.sum('year') == self.divestment_used, + short_name='size|year_of_decommissioning|count', ) if not self.parameters.optional_investment: self.add_constraints( @@ -248,11 +258,11 @@ def _basic_modeling(self): level_variable=self.size, increase_variable=self.size_increase, decrease_variable=self.size_decrease, - increase_binary=self.has_increase, - decrease_binary=self.has_decrease, + increase_binary=self.year_of_investment, + decrease_binary=self.year_of_decommissioning, name=f'{self.label_of_element}|size|changes', max_change=size_max, - initial_level=0, + initial_level=self.parameters.previous_size if self.parameters.previous_size is not None else 0, coord='year', ) @@ -284,13 +294,13 @@ def _add_effects(self): def _fixed_start_constraint(self): self.add_constraints( - self.has_increase.sel(year=self.parameters.year_of_investment) == self.investment_used, + self.year_of_investment.sel(year=self.parameters.year_of_investment) == self.investment_used, short_name='size|changes|fixed_start', ) def _fixed_end_constraint(self): self.add_constraints( - self.has_decrease.sel(year=self.parameters.year_of_decommissioning) == self.divestment_used, + self.year_of_decommissioning.sel(year=self.parameters.year_of_decommissioning) == self.divestment_used, short_name='size|changes|fixed_end', ) @@ -304,7 +314,9 @@ def _fixed_duration_constraint(self): valid_years_of_investment = years[valid_mask] valid_years_of_decommissioning = years_of_decommissioning[valid_mask] actual_years_of_decommissioning = ( - self.has_decrease.sel(year=valid_years_of_decommissioning, method='bfill').coords['year'].values + self.years_of_decommissioning.sel(year=valid_years_of_decommissioning, method='bfill') + .coords['year'] + .values ) # Warning for mismatched years @@ -328,12 +340,15 @@ def _fixed_duration_constraint(self): # Now you can use proper xarray groupby grouped_increases = ( - self.has_increase.sel(year=valid_years_of_investment).groupby(group).sum('year_of_decommissioning') + self.year_of_investment.sel(year=valid_years_of_investment) + .groupby(group) + .sum('year_of_decommissioning') ) # Create constraints self.add_constraints( - self.has_decrease.sel(year=grouped_increases.coords['year_of_decommissioning']) == grouped_increases, + self.year_of_decommissioning.sel(year=grouped_increases.coords['year_of_decommissioning']) + == grouped_increases, short_name='size|changes|fixed_duration', ) @@ -350,14 +365,14 @@ def is_invested(self) -> Optional[linopy.Variable]: return self._variables['is_invested'] @property - def has_increase(self) -> linopy.Variable: + def year_of_investment(self) -> linopy.Variable: """Binary increase decision variable""" - return self._variables['size|has_increase'] + return self._variables['size|year_of_investment'] @property - def has_decrease(self) -> linopy.Variable: + def year_of_decommissioning(self) -> linopy.Variable: """Binary decrease decision variable""" - return self._variables['size|has_decrease'] + return self._variables['size|year_of_decommissioning'] @property def size_decrease(self) -> linopy.Variable: diff --git a/flixopt/interface.py b/flixopt/interface.py index 2772c73b6..958390e2f 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -357,14 +357,19 @@ def __init__( specific_effects: Optional['NonTemporalEffectsUser'] = None, # costs per Flow-Unit/Storage-Size/... effects_of_investment_per_size: Optional['NonTemporalEffectsUser'] = None, effects_of_investment: Optional['NonTemporalEffectsUser'] = None, + previous_size: Scalar = 0, ): """ - Initialize fixed start and end year investment parameters. + These parameters are used to include the timing of investments in the model. + Two out of three parameters (year_of_investment, year_of_decommissioning, duration_in_years) can be fixed. Args: year_of_investment: Year in which the investment occurs (inclusive). Present from this year onwards. + If None, the year_of_investment is not fixed. year_of_decommissioning: Year in which the unit is decommissioned (exclusive). Present up to this year. - duration_in_years: Duration of the investment in years. + If None, the year_of_decommissioning is not fixed. + duration_in_years: Duration between year_of_investment and year_of_decommissioning. + If None, the duration is not fixed. minimum_size: Minimum possible size of the investment. maximum_size: Maximum possible size of the investment. fixed_size: If specified, investment size is fixed to this value. @@ -376,6 +381,7 @@ def __init__( effects_of_investment: Effects depending on when an investment decision is made. These can occur in the investment year or in multiple years. If the effects need to occur in multiple years, you need to pass an xr.DataArray with the coord 'year_of_investment'. Example: {'costs': 1000} applies 1000 to costs in the investment year. + previous_size: The size of the investment before the first period. Defaults to 0. """ self.minimum_size = minimum_size if minimum_size is not None else CONFIG.modeling.EPSILON @@ -397,6 +403,8 @@ def __init__( effects_of_investment if effects_of_investment is not None else {} ) + self.previous_size = previous_size + def _plausibility_checks(self, flow_system): """Validate parameter consistency.""" if flow_system.years is None: @@ -442,7 +450,7 @@ def _plausibility_checks(self, flow_system): if ( self.year_of_decommissioning is not None and self.year_of_investment is not None - and self.year_of_investment >= self.year_of_decommissioning + and not self.year_of_investment < self.year_of_decommissioning ): raise ValueError( f'year_of_investment ({self.year_of_investment}) must be before year_of_decommissioning ({self.year_of_decommissioning})' @@ -476,6 +484,8 @@ def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): f'{name_prefix}|fixed_size', self.fixed_size, has_time_dim=False ) + # TODO: self.previous_size to only scenarios + @property def minimum_or_fixed_size(self) -> NonTemporalData: """Get the effective minimum size (fixed size takes precedence).""" diff --git a/flixopt/modeling.py b/flixopt/modeling.py index ef7cdc27a..f0e354bc5 100644 --- a/flixopt/modeling.py +++ b/flixopt/modeling.py @@ -696,10 +696,10 @@ def link_changes_to_level_with_binaries( if not isinstance(model, Submodel): raise ValueError('BoundingPatterns.link_changes_to_level_with_binaries() can only be used with a Submodel') - # 1. Initial period: level[0] = initial_level + increase[0] - decrease[0] + # 1. Initial period: level[0] - initial_level = increase[0] - decrease[0] initial_constraint = model.add_constraints( - level_variable.isel({coord: 0}) - == initial_level + increase_variable.isel({coord: 0}) - decrease_variable.isel({coord: 0}), + level_variable.isel({coord: 0}) - initial_level + == increase_variable.isel({coord: 0}) - decrease_variable.isel({coord: 0}), name=f'{name}|initial_level', ) From 8e21165c8c6fe6886924f4fdb830c302087f3331 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 5 Aug 2025 15:20:31 +0200 Subject: [PATCH 291/448] Add validation for previous size --- flixopt/interface.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/flixopt/interface.py b/flixopt/interface.py index 958390e2f..0011d4b2d 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -456,6 +456,13 @@ def _plausibility_checks(self, flow_system): f'year_of_investment ({self.year_of_investment}) must be before year_of_decommissioning ({self.year_of_decommissioning})' ) + if self.previous_size != 0: + if not self.minimum_size <= self.previous_size <= self.maximum_size: + raise ValueError( + f'previous_size ({self.previous_size}) must be zero orbetween minimum_size ({self.minimum_size}) ' + f'and maximum_size ({self.maximum_size})' + ) + def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): """Transform all parameter data to match the flow system's coordinate structure.""" self._plausibility_checks(flow_system) From c2ce728280ad6b1e945120355c4ca5adb86d73e5 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 5 Aug 2025 15:29:27 +0200 Subject: [PATCH 292/448] Change fit_to_model_coords to work with a Collection of dims --- flixopt/flow_system.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 3cead49e0..80290f6c7 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -7,7 +7,7 @@ import pathlib import warnings from io import StringIO -from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Tuple, Union +from typing import TYPE_CHECKING, Any, Collection, Dict, List, Literal, Optional, Tuple, Union import numpy as np import pandas as pd @@ -363,6 +363,7 @@ def fit_to_model_coords( name: str, data: Optional[Union[TemporalDataUser, NonTemporalDataUser]], has_time_dim: bool = True, + dims: Optional[Collection[FlowSystemDimensions]] = None, ) -> Optional[Union[TemporalData, NonTemporalData]]: """ Fit data to model coordinate system (currently time, but extensible). @@ -371,6 +372,7 @@ def fit_to_model_coords( name: Name of the data data: Data to fit to model coordinates has_time_dim: Wether to use the time dimension or not + dims: Collection of dimension names to use for fitting. If None, all dimensions are used. Returns: xr.DataArray aligned to model coordinate system. If data is None, returns None. @@ -378,10 +380,19 @@ def fit_to_model_coords( if data is None: return None - coords = self.coords + if dims is None: + coords = self.coords - if not has_time_dim: - coords.pop('time') + if not has_time_dim: + warnings.warn( + 'has_time_dim is deprecated. Please pass dims to fit_to_model_coords instead.', + DeprecationWarning, + stacklevel=2, + ) + coords.pop('time') + else: + coords = self.coords + coords = {k: coords[k] for k in dims if k in coords} # Rest of your method stays the same, just pass coords if isinstance(data, TimeSeriesData): @@ -404,6 +415,7 @@ def fit_effects_to_model_coords( effect_values: Optional[Union[TemporalEffectsUser, NonTemporalEffectsUser]], label_suffix: Optional[str] = None, has_time_dim: bool = True, + dims: Optional[Collection[FlowSystemDimensions]] = None, ) -> Optional[Union[TemporalEffects, NonTemporalEffects]]: """ Transform EffectValues from the user to Internal Datatypes aligned with model coordinates. @@ -415,7 +427,10 @@ def fit_effects_to_model_coords( return { effect: self.fit_to_model_coords( - '|'.join(filter(None, [label_prefix, effect, label_suffix])), value, has_time_dim=has_time_dim + '|'.join(filter(None, [label_prefix, effect, label_suffix])), + value, + has_time_dim=has_time_dim, + dims=dims, ) for effect, value in effect_values_dict.items() } From 02c1f31703e9f4a0e960e6ee8876c60525a67976 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 5 Aug 2025 15:34:42 +0200 Subject: [PATCH 293/448] Improve fit_to_model_coords --- flixopt/components.py | 12 ++++++------ flixopt/effects.py | 17 ++++++++++------- flixopt/elements.py | 10 +++++----- flixopt/flow_system.py | 2 +- flixopt/interface.py | 43 +++++++++++++++++++++--------------------- flixopt/structure.py | 2 +- 6 files changed, 45 insertions(+), 41 deletions(-) diff --git a/flixopt/components.py b/flixopt/components.py index d483ee28c..933ef1791 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -223,29 +223,29 @@ def transform_data(self, flow_system: 'FlowSystem') -> None: ) if not isinstance(self.initial_charge_state, str): self.initial_charge_state = flow_system.fit_to_model_coords( - f'{self.label_full}|initial_charge_state', self.initial_charge_state, has_time_dim=False + f'{self.label_full}|initial_charge_state', self.initial_charge_state, dims=['year', 'scenario'] ) self.minimal_final_charge_state = flow_system.fit_to_model_coords( - f'{self.label_full}|minimal_final_charge_state', self.minimal_final_charge_state, has_time_dim=False + f'{self.label_full}|minimal_final_charge_state', self.minimal_final_charge_state, dims=['year', 'scenario'] ) self.maximal_final_charge_state = flow_system.fit_to_model_coords( - f'{self.label_full}|maximal_final_charge_state', self.maximal_final_charge_state, has_time_dim=False + f'{self.label_full}|maximal_final_charge_state', self.maximal_final_charge_state, dims=['year', 'scenario'] ) self.relative_minimum_final_charge_state = flow_system.fit_to_model_coords( f'{self.label_full}|relative_minimum_final_charge_state', self.relative_minimum_final_charge_state, - has_time_dim=False, + dims=['year', 'scenario'], ) self.relative_maximum_final_charge_state = flow_system.fit_to_model_coords( f'{self.label_full}|relative_maximum_final_charge_state', self.relative_maximum_final_charge_state, - has_time_dim=False, + dims=['year', 'scenario'], ) if isinstance(self.capacity_in_flow_hours, InvestParameters): self.capacity_in_flow_hours.transform_data(flow_system, f'{self.label_full}|InvestParameters') else: self.capacity_in_flow_hours = flow_system.fit_to_model_coords( - f'{self.label_full}|capacity_in_flow_hours', self.capacity_in_flow_hours, has_time_dim=False + f'{self.label_full}|capacity_in_flow_hours', self.capacity_in_flow_hours, dims=['year', 'scenario'] ) def _plausibility_checks(self) -> None: diff --git a/flixopt/effects.py b/flixopt/effects.py index 79a44e67a..d0b552bf8 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -105,27 +105,30 @@ def transform_data(self, flow_system: 'FlowSystem'): ) self.minimum_operation = flow_system.fit_to_model_coords( - f'{self.label_full}|minimum_operation', self.minimum_operation, has_time_dim=False + f'{self.label_full}|minimum_operation', self.minimum_operation, dims=['year', 'scenario'] ) self.maximum_operation = flow_system.fit_to_model_coords( - f'{self.label_full}|maximum_operation', self.maximum_operation, has_time_dim=False + f'{self.label_full}|maximum_operation', self.maximum_operation, dims=['year', 'scenario'] ) self.minimum_invest = flow_system.fit_to_model_coords( - f'{self.label_full}|minimum_invest', self.minimum_invest, has_time_dim=False + f'{self.label_full}|minimum_invest', self.minimum_invest, dims=['year', 'scenario'] ) self.maximum_invest = flow_system.fit_to_model_coords( - f'{self.label_full}|maximum_invest', self.maximum_invest, has_time_dim=False + f'{self.label_full}|maximum_invest', self.maximum_invest, dims=['year', 'scenario'] ) self.minimum_total = flow_system.fit_to_model_coords( f'{self.label_full}|minimum_total', self.minimum_total, - has_time_dim=False, + dims=['year', 'scenario'], ) self.maximum_total = flow_system.fit_to_model_coords( - f'{self.label_full}|maximum_total', self.maximum_total, has_time_dim=False + f'{self.label_full}|maximum_total', self.maximum_total, dims=['year', 'scenario'] ) self.specific_share_to_other_effects_invest = flow_system.fit_effects_to_model_coords( - f'{self.label_full}|invest->', self.specific_share_to_other_effects_invest, 'invest', has_time_dim=False + f'{self.label_full}|invest->', + self.specific_share_to_other_effects_invest, + 'invest', + dims=['year', 'scenario'], ) def create_model(self, model: FlowSystemModel) -> 'EffectModel': diff --git a/flixopt/elements.py b/flixopt/elements.py index 2aed7d713..fed8126d2 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -252,16 +252,16 @@ def transform_data(self, flow_system: 'FlowSystem'): self.label_full, self.effects_per_flow_hour, 'per_flow_hour' ) self.flow_hours_total_max = flow_system.fit_to_model_coords( - f'{self.label_full}|flow_hours_total_max', self.flow_hours_total_max, has_time_dim=False + f'{self.label_full}|flow_hours_total_max', self.flow_hours_total_max, dims=['year', 'scenario'] ) self.flow_hours_total_min = flow_system.fit_to_model_coords( - f'{self.label_full}|flow_hours_total_min', self.flow_hours_total_min, has_time_dim=False + f'{self.label_full}|flow_hours_total_min', self.flow_hours_total_min, dims=['year', 'scenario'] ) self.load_factor_max = flow_system.fit_to_model_coords( - f'{self.label_full}|load_factor_max', self.load_factor_max, has_time_dim=False + f'{self.label_full}|load_factor_max', self.load_factor_max, dims=['year', 'scenario'] ) self.load_factor_min = flow_system.fit_to_model_coords( - f'{self.label_full}|load_factor_min', self.load_factor_min, has_time_dim=False + f'{self.label_full}|load_factor_min', self.load_factor_min, dims=['year', 'scenario'] ) if self.on_off_parameters is not None: @@ -269,7 +269,7 @@ def transform_data(self, flow_system: 'FlowSystem'): if isinstance(self.size, (InvestParameters, InvestTimingParameters)): self.size.transform_data(flow_system, self.label_full) else: - self.size = flow_system.fit_to_model_coords(f'{self.label_full}|size', self.size, has_time_dim=False) + self.size = flow_system.fit_to_model_coords(f'{self.label_full}|size', self.size, dims=['year', 'scenario']) def _plausibility_checks(self) -> None: # TODO: Incorporate into Variable? (Lower_bound can not be greater than upper bound diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 80290f6c7..97db2879e 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -441,7 +441,7 @@ def connect_and_transform(self): logger.debug('FlowSystem already connected and transformed') return - self.weights = self.fit_to_model_coords('weights', self.weights, has_time_dim=False) + self.weights = self.fit_to_model_coords('weights', self.weights, dims=['year', 'scenario']) if self.weights is not None and self.weights.sum() != 1: logger.warning( f'Scenario weights are not normalized to 1. This is recomended for a better scaled model. ' diff --git a/flixopt/interface.py b/flixopt/interface.py index 0011d4b2d..4a7aae255 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -33,8 +33,9 @@ def __init__(self, start: TemporalDataUser, end: TemporalDataUser): self.has_time_dim = False def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): - self.start = flow_system.fit_to_model_coords(f'{name_prefix}|start', self.start, has_time_dim=self.has_time_dim) - self.end = flow_system.fit_to_model_coords(f'{name_prefix}|end', self.end, has_time_dim=self.has_time_dim) + dims = None if self.has_time_dim else ['year', 'scenario'] + self.start = flow_system.fit_to_model_coords(f'{name_prefix}|start', self.start, dims=dims) + self.end = flow_system.fit_to_model_coords(f'{name_prefix}|end', self.end, dims=dims) @register_class_for_io @@ -189,33 +190,33 @@ def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): label_prefix=name_prefix, effect_values=self.fix_effects, label_suffix='fix_effects', - has_time_dim=False, + dims=['year', 'scenario'], ) self.divest_effects = flow_system.fit_effects_to_model_coords( label_prefix=name_prefix, effect_values=self.divest_effects, label_suffix='divest_effects', - has_time_dim=False, + dims=['year', 'scenario'], ) self.specific_effects = flow_system.fit_effects_to_model_coords( label_prefix=name_prefix, effect_values=self.specific_effects, label_suffix='specific_effects', - has_time_dim=False, + dims=['year', 'scenario'], ) if self.piecewise_effects is not None: self.piecewise_effects.has_time_dim = False self.piecewise_effects.transform_data(flow_system, f'{name_prefix}|PiecewiseEffects') self.minimum_size = flow_system.fit_to_model_coords( - f'{name_prefix}|minimum_size', self.minimum_size, has_time_dim=False + f'{name_prefix}|minimum_size', self.minimum_size, dims=['year', 'scenario'] ) self.maximum_size = flow_system.fit_to_model_coords( - f'{name_prefix}|maximum_size', self.maximum_size, has_time_dim=False + f'{name_prefix}|maximum_size', self.maximum_size, dims=['year', 'scenario'] ) if self.fixed_size is not None: self.fixed_size = flow_system.fit_to_model_coords( - f'{name_prefix}|fixed_size', self.fixed_size, has_time_dim=False + f'{name_prefix}|fixed_size', self.fixed_size, dims=['year', 'scenario'] ) def _plausibility_checks(self, flow_system): @@ -293,24 +294,24 @@ def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): label_prefix=name_prefix, effect_values=self.effects_of_investment_per_size, label_suffix='effects_of_investment_per_size', - has_time_dim=False, + dims=['year', 'scenario'], ) self.effects_of_investment = flow_system.fit_effects_to_model_coords( label_prefix=name_prefix, effect_values=self.effects_of_investment, label_suffix='effects_of_investment', - has_time_dim=False, + dims=['year', 'scenario'], ) self.minimum_size = flow_system.fit_to_model_coords( - f'{name_prefix}|minimum_size', self.minimum_size, has_time_dim=False + f'{name_prefix}|minimum_size', self.minimum_size, dims=['year', 'scenario'] ) self.maximum_size = flow_system.fit_to_model_coords( - f'{name_prefix}|maximum_size', self.maximum_size, has_time_dim=False + f'{name_prefix}|maximum_size', self.maximum_size, dims=['year', 'scenario'] ) if self.fixed_size is not None: self.fixed_size = flow_system.fit_to_model_coords( - f'{name_prefix}|fixed_size', self.fixed_size, has_time_dim=False + f'{name_prefix}|fixed_size', self.fixed_size, dims=['year', 'scenario'] ) def _plausibility_checks(self, flow_system): @@ -471,24 +472,24 @@ def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): label_prefix=name_prefix, effect_values=self.effects_of_investment_per_size, label_suffix='effects_of_investment_per_size', - has_time_dim=False, + dims=['year', 'scenario'], ) self.effects_of_investment = flow_system.fit_effects_to_model_coords( label_prefix=name_prefix, effect_values=self.effects_of_investment, label_suffix='effects_of_investment', - has_time_dim=False, + dims=['year', 'scenario'], ) self.minimum_size = flow_system.fit_to_model_coords( - f'{name_prefix}|minimum_size', self.minimum_size, has_time_dim=False + f'{name_prefix}|minimum_size', self.minimum_size, dims=['year', 'scenario'] ) self.maximum_size = flow_system.fit_to_model_coords( - f'{name_prefix}|maximum_size', self.maximum_size, has_time_dim=False + f'{name_prefix}|maximum_size', self.maximum_size, dims=['year', 'scenario'] ) if self.fixed_size is not None: self.fixed_size = flow_system.fit_to_model_coords( - f'{name_prefix}|fixed_size', self.fixed_size, has_time_dim=False + f'{name_prefix}|fixed_size', self.fixed_size, dims=['year', 'scenario'] ) # TODO: self.previous_size to only scenarios @@ -579,13 +580,13 @@ def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): f'{name_prefix}|consecutive_off_hours_max', self.consecutive_off_hours_max ) self.on_hours_total_max = flow_system.fit_to_model_coords( - f'{name_prefix}|on_hours_total_max', self.on_hours_total_max, has_time_dim=False + f'{name_prefix}|on_hours_total_max', self.on_hours_total_max, dims=['year', 'scenario'] ) self.on_hours_total_min = flow_system.fit_to_model_coords( - f'{name_prefix}|on_hours_total_min', self.on_hours_total_min, has_time_dim=False + f'{name_prefix}|on_hours_total_min', self.on_hours_total_min, dims=['year', 'scenario'] ) self.switch_on_total_max = flow_system.fit_to_model_coords( - f'{name_prefix}|switch_on_total_max', self.switch_on_total_max, has_time_dim=False + f'{name_prefix}|switch_on_total_max', self.switch_on_total_max, dims=['year', 'scenario'] ) @property diff --git a/flixopt/structure.py b/flixopt/structure.py index da67f9620..24934547b 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -178,7 +178,7 @@ def get_coords( def weights(self) -> Union[int, xr.DataArray]: """Returns the scenario weights of the FlowSystem. If None, return weights that are normalized to 1 (one)""" if self.flow_system.weights is None: - weights = self.flow_system.fit_to_model_coords('weights', 1, has_time_dim=False) + weights = self.flow_system.fit_to_model_coords('weights', 1, dims=['year', 'scenario']) return weights / weights.sum() From 5f8588760510cc426ba9d175cad67ffdec63c8a9 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 5 Aug 2025 15:36:26 +0200 Subject: [PATCH 294/448] Improve test --- tests/test_models.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/tests/test_models.py b/tests/test_models.py index ac040b1e1..37a9fb96c 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -21,7 +21,7 @@ @pytest.fixture def flow_system() -> fx.FlowSystem: """Create basic elements for component testing with coordinate parametrization.""" - years = pd.Index([2020, 2021, 2022, 2023, 2024], name='year') + years = pd.Index([2020, 2021, 2022, 2023, 2024, 2030], name='year') timesteps = pd.date_range('2020-01-01', periods=24, freq='h', name='time') flow_system = fx.FlowSystem(timesteps=timesteps, years=years) @@ -118,11 +118,13 @@ def test_flow_invest_new(self, flow_system): 'Wärme', bus='Fernwärme', size=fx.InvestTimingParameters( - year_of_investment=2021, - year_of_decommissioning=2023, + year_of_investment=2020, + year_of_decommissioning=2030, + # duration_in_years=3, minimum_size=900, maximum_size=1000, - effects_of_investment_per_size=200, + effects_of_investment_per_size=200 * 1e5, + previous_size=900, ), relative_maximum=np.linspace(0.5, 1, flow_system.timesteps.size), ) @@ -131,10 +133,13 @@ def test_flow_invest_new(self, flow_system): calculation = fx.FullCalculation('GenericName', flow_system) calculation.do_modeling() # calculation.model.add_constraints(calculation.model['Source(Wärme)|decrease'].isel(year=2) == 1) - calculation.solve(fx.solvers.HighsSolver(0, 60)) + calculation.solve(fx.solvers.GurobiSolver(0, 60)) ds = calculation.results['Source'].solution - filtered_ds = ds[[v for v in ds.data_vars if ds[v].dims == ('year',)]] - print(filtered_ds.round(0).to_pandas().T) + filtered_ds_year = ds[[v for v in ds.data_vars if ds[v].dims == ('year',)]] + print(filtered_ds_year.round(0).to_pandas().T) + + filtered_ds_scalar = ds[[v for v in ds.data_vars if ds[v].dims == tuple()]] + print(filtered_ds_scalar.round(0).to_pandas().T) print('##') From 815b8bf6bc3743144ff76d2b5976c50f3b9b9f47 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 6 Aug 2025 14:41:13 +0200 Subject: [PATCH 295/448] Update transform_data() --- flixopt/components.py | 6 +++--- flixopt/effects.py | 2 +- flixopt/elements.py | 6 +++--- flixopt/interface.py | 24 ++++++++---------------- flixopt/structure.py | 3 ++- 5 files changed, 17 insertions(+), 24 deletions(-) diff --git a/flixopt/components.py b/flixopt/components.py index 933ef1791..a4a45d7d6 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -93,7 +93,7 @@ def _plausibility_checks(self) -> None: f'(in {self.label_full}) and variable size is uncommon. Please check if this is intended!' ) - def transform_data(self, flow_system: 'FlowSystem'): + def transform_data(self, flow_system: 'FlowSystem', name_prefix: str = '') -> None: super().transform_data(flow_system) if self.conversion_factors: self.conversion_factors = self._transform_conversion_factors(flow_system) @@ -206,7 +206,7 @@ def create_model(self, model: FlowSystemModel) -> 'StorageModel': self.submodel = StorageModel(model, self) return self.submodel - def transform_data(self, flow_system: 'FlowSystem') -> None: + def transform_data(self, flow_system: 'FlowSystem', name_prefix: str = '') -> None: super().transform_data(flow_system) self.relative_minimum_charge_state = flow_system.fit_to_model_coords( f'{self.label_full}|relative_minimum_charge_state', @@ -392,7 +392,7 @@ def create_model(self, model) -> 'TransmissionModel': self.submodel = TransmissionModel(model, self) return self.submodel - def transform_data(self, flow_system: 'FlowSystem') -> None: + def transform_data(self, flow_system: 'FlowSystem', name_prefix: str = '') -> None: super().transform_data(flow_system) self.relative_losses = flow_system.fit_to_model_coords( f'{self.label_full}|relative_losses', self.relative_losses diff --git a/flixopt/effects.py b/flixopt/effects.py index d0b552bf8..da032878f 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -91,7 +91,7 @@ def __init__( self.minimum_total = minimum_total self.maximum_total = maximum_total - def transform_data(self, flow_system: 'FlowSystem'): + def transform_data(self, flow_system: 'FlowSystem', name_prefix: str = '') -> None: self.minimum_operation_per_hour = flow_system.fit_to_model_coords( f'{self.label_full}|minimum_operation_per_hour', self.minimum_operation_per_hour ) diff --git a/flixopt/elements.py b/flixopt/elements.py index fed8126d2..6791bf6ae 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -70,7 +70,7 @@ def create_model(self, model: FlowSystemModel) -> 'ComponentModel': self.submodel = ComponentModel(model, self) return self.submodel - def transform_data(self, flow_system: 'FlowSystem') -> None: + def transform_data(self, flow_system: 'FlowSystem', name_prefix: str = '') -> None: if self.on_off_parameters is not None: self.on_off_parameters.transform_data(flow_system, self.label_full) @@ -118,7 +118,7 @@ def create_model(self, model: FlowSystemModel) -> 'BusModel': self.submodel = BusModel(model, self) return self.submodel - def transform_data(self, flow_system: 'FlowSystem'): + def transform_data(self, flow_system: 'FlowSystem', name_prefix: str = '') -> None: self.excess_penalty_per_flow_hour = flow_system.fit_to_model_coords( f'{self.label_full}|excess_penalty_per_flow_hour', self.excess_penalty_per_flow_hour ) @@ -238,7 +238,7 @@ def create_model(self, model: FlowSystemModel) -> 'FlowModel': self.submodel = FlowModel(model, self) return self.submodel - def transform_data(self, flow_system: 'FlowSystem'): + def transform_data(self, flow_system: 'FlowSystem', name_prefix: str = '') -> None: self.relative_minimum = flow_system.fit_to_model_coords( f'{self.label_full}|relative_minimum', self.relative_minimum ) diff --git a/flixopt/interface.py b/flixopt/interface.py index 4a7aae255..d06d0ccb1 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -32,7 +32,7 @@ def __init__(self, start: TemporalDataUser, end: TemporalDataUser): self.end = end self.has_time_dim = False - def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): + def transform_data(self, flow_system: 'FlowSystem', name_prefix: str = '') -> None: dims = None if self.has_time_dim else ['year', 'scenario'] self.start = flow_system.fit_to_model_coords(f'{name_prefix}|start', self.start, dims=dims) self.end = flow_system.fit_to_model_coords(f'{name_prefix}|end', self.end, dims=dims) @@ -69,7 +69,7 @@ def __getitem__(self, index) -> Piece: def __iter__(self) -> Iterator[Piece]: return iter(self.pieces) # Enables iteration like for piece in piecewise: ... - def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): + def transform_data(self, flow_system: 'FlowSystem', name_prefix: str = '') -> None: for i, piece in enumerate(self.pieces): piece.transform_data(flow_system, f'{name_prefix}|Piece{i}') @@ -102,7 +102,7 @@ def has_time_dim(self, value): def items(self): return self.piecewises.items() - def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): + def transform_data(self, flow_system: 'FlowSystem', name_prefix: str = '') -> None: for name, piecewise in self.piecewises.items(): piecewise.transform_data(flow_system, f'{name_prefix}|{name}') @@ -133,7 +133,7 @@ def has_time_dim(self, value): for piecewise in self.piecewise_shares.values(): piecewise.has_time_dim = value - def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): + def transform_data(self, flow_system: 'FlowSystem', name_prefix: str = '') -> None: self.piecewise_origin.transform_data(flow_system, f'{name_prefix}|PiecewiseEffects|origin') for effect, piecewise in self.piecewise_shares.items(): piecewise.transform_data(flow_system, f'{name_prefix}|PiecewiseEffects|{effect}') @@ -184,7 +184,7 @@ def __init__( self.maximum_size = maximum_size if maximum_size is not None else CONFIG.modeling.BIG # default maximum self.investment_scenarios = investment_scenarios - def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): + def transform_data(self, flow_system: 'FlowSystem', name_prefix: str = '') -> None: self._plausibility_checks(flow_system) self.fix_effects = flow_system.fit_effects_to_model_coords( label_prefix=name_prefix, @@ -286,7 +286,7 @@ def __init__( effects_of_investment if effects_of_investment is not None else {} ) - def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): + def transform_data(self, flow_system: 'FlowSystem', name_prefix: str = '') -> None: """Transform all parameter data to match the flow system's coordinate structure.""" self._plausibility_checks(flow_system) @@ -356,8 +356,6 @@ def __init__( optional_divestment: bool = True, fix_effects: Optional['NonTemporalEffectsUser'] = None, specific_effects: Optional['NonTemporalEffectsUser'] = None, # costs per Flow-Unit/Storage-Size/... - effects_of_investment_per_size: Optional['NonTemporalEffectsUser'] = None, - effects_of_investment: Optional['NonTemporalEffectsUser'] = None, previous_size: Scalar = 0, ): """ @@ -397,12 +395,6 @@ def __init__( self.fix_effects: 'NonTemporalEffectsUser' = fix_effects if fix_effects is not None else {} self.specific_effects: 'NonTemporalEffectsUser' = specific_effects if specific_effects is not None else {} - self.effects_of_investment_per_size: 'NonTemporalEffectsUser' = ( - effects_of_investment_per_size if effects_of_investment_per_size is not None else {} - ) - self.effects_of_investment: 'NonTemporalEffectsUser' = ( - effects_of_investment if effects_of_investment is not None else {} - ) self.previous_size = previous_size @@ -464,7 +456,7 @@ def _plausibility_checks(self, flow_system): f'and maximum_size ({self.maximum_size})' ) - def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): + def transform_data(self, flow_system: 'FlowSystem', name_prefix: str = '') -> None: """Transform all parameter data to match the flow system's coordinate structure.""" self._plausibility_checks(flow_system) @@ -560,7 +552,7 @@ def __init__( self.switch_on_total_max: Scalar = switch_on_total_max self.force_switch_on: bool = force_switch_on - def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): + def transform_data(self, flow_system: 'FlowSystem', name_prefix: str = '') -> None: self.effects_per_switch_on = flow_system.fit_effects_to_model_coords( name_prefix, self.effects_per_switch_on, 'per_switch_on' ) diff --git a/flixopt/structure.py b/flixopt/structure.py index 24934547b..dec476ac7 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -225,11 +225,12 @@ class Interface: transform_data(flow_system): Transform data to match FlowSystem dimensions """ - def transform_data(self, flow_system: 'FlowSystem'): + def transform_data(self, flow_system: 'FlowSystem', name_prefix: str = '') -> None: """Transform the data of the interface to match the FlowSystem's dimensions. Args: flow_system: The FlowSystem containing timing and dimensional information + name_prefix: The prefix to use for the names of the variables. Defaults to '', which results in no prefix. Raises: NotImplementedError: Must be implemented by subclasses From 1ffe27faed2093a96c44d4f8cf49f3e83f8bd4ed Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 7 Aug 2025 09:30:51 +0200 Subject: [PATCH 296/448] Add new "year of investment" coord to FlowSystem --- flixopt/flow_system.py | 34 +++++++++++++++------------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 97db2879e..59b79831f 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -79,15 +79,17 @@ def __init__( weights: The weights of each year and scenario. If None, all have the same weight (normalized to 1). Its recommended to scale the weights to sum up to 1. """ self.timesteps = self._validate_timesteps(timesteps) - self.timesteps_extra = self._create_timesteps_with_extra(timesteps, hours_of_last_timestep) self.hours_of_previous_timesteps = self._calculate_hours_of_previous_timesteps( timesteps, hours_of_previous_timesteps ) - self.years = None if years is None else self._validate_years(years) - - self.years_per_year = None if years is None else self.calculate_years_per_year(years, years_of_last_year) + if years is None: + self.years, self.years_per_year, self.years_of_investment = None, None, None + else: + self.years = self._validate_years(years) + self.years_per_year = self.calculate_years_per_year(self.years, years_of_last_year) + self.years_of_investment = self.years.rename('year_of_investment') self.scenarios = None if scenarios is None else self._validate_scenarios(scenarios) @@ -362,8 +364,8 @@ def fit_to_model_coords( self, name: str, data: Optional[Union[TemporalDataUser, NonTemporalDataUser]], - has_time_dim: bool = True, dims: Optional[Collection[FlowSystemDimensions]] = None, + with_year_of_investment: bool = False, ) -> Optional[Union[TemporalData, NonTemporalData]]: """ Fit data to model coordinate system (currently time, but extensible). @@ -371,8 +373,8 @@ def fit_to_model_coords( Args: name: Name of the data data: Data to fit to model coordinates - has_time_dim: Wether to use the time dimension or not dims: Collection of dimension names to use for fitting. If None, all dimensions are used. + with_year_of_investment: Wether to use the year_of_investment dimension or not. Only if "year" is in dims. Returns: xr.DataArray aligned to model coordinate system. If data is None, returns None. @@ -380,20 +382,14 @@ def fit_to_model_coords( if data is None: return None - if dims is None: - coords = self.coords + coords = self.coords - if not has_time_dim: - warnings.warn( - 'has_time_dim is deprecated. Please pass dims to fit_to_model_coords instead.', - DeprecationWarning, - stacklevel=2, - ) - coords.pop('time') - else: - coords = self.coords + if dims is not None: coords = {k: coords[k] for k in dims if k in coords} + if with_year_of_investment and 'year' in coords: + coords['year_of_investment'] = coords['year'].rename('year_of_investment') + # Rest of your method stays the same, just pass coords if isinstance(data, TimeSeriesData): try: @@ -414,8 +410,8 @@ def fit_effects_to_model_coords( label_prefix: Optional[str], effect_values: Optional[Union[TemporalEffectsUser, NonTemporalEffectsUser]], label_suffix: Optional[str] = None, - has_time_dim: bool = True, dims: Optional[Collection[FlowSystemDimensions]] = None, + with_year_of_investment: bool = False, ) -> Optional[Union[TemporalEffects, NonTemporalEffects]]: """ Transform EffectValues from the user to Internal Datatypes aligned with model coordinates. @@ -429,8 +425,8 @@ def fit_effects_to_model_coords( effect: self.fit_to_model_coords( '|'.join(filter(None, [label_prefix, effect, label_suffix])), value, - has_time_dim=has_time_dim, dims=dims, + with_year_of_investment=with_year_of_investment, ) for effect, value in effect_values_dict.items() } From 3632965e9e76660d69db2aabf73645055f1fb25a Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 7 Aug 2025 11:01:11 +0200 Subject: [PATCH 297/448] Add 'year_of_investment' dimension to FlowSystem --- flixopt/features.py | 33 ++++++++++++++++++--------------- flixopt/interface.py | 24 ++++++++---------------- 2 files changed, 26 insertions(+), 31 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index 17a99d4eb..4665aea90 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -262,32 +262,35 @@ def _basic_modeling(self): decrease_binary=self.year_of_decommissioning, name=f'{self.label_of_element}|size|changes', max_change=size_max, - initial_level=self.parameters.previous_size if self.parameters.previous_size is not None else 0, + initial_level=0, coord='year', ) def _add_effects(self): """Add investment effects to the model.""" - if self.parameters.effects_of_investment: - # One-time effects when investment is made - increase = self._variables.get('increase') - if increase is not None: - self._model.effects.add_share_to_effects( - name=self.label_of_element, - expressions={ - effect: increase * factor for effect, factor in self.parameters.effects_of_investment.items() - }, - target='invest', - ) + if self.parameters.fix_effects: + # Effects depending on when the investment is made + remapped_variable = self.year_of_investment.rename({'year': 'year_of_investment'}) + + self._model.effects.add_share_to_effects( + name=self.label_of_element, + expressions={ + effect: (remapped_variable * factor).sum('year_of_investment') + for effect, factor in self.parameters.fix_effects.items() + }, + target='invest', + ) - if self.parameters.effects_of_investment_per_size: + if self.parameters.specific_effects: # Annual effects proportional to investment size + remapped_variable = self.size_increase.rename({'year': 'year_of_investment'}) + self._model.effects.add_share_to_effects( name=self.label_of_element, expressions={ - effect: self.size * factor - for effect, factor in self.parameters.effects_of_investment_per_size.items() + effect: (remapped_variable * factor).sum('year_of_investment') + for effect, factor in self.parameters.specific_effects.items() }, target='invest', ) diff --git a/flixopt/interface.py b/flixopt/interface.py index d06d0ccb1..4ca5093ca 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -356,7 +356,6 @@ def __init__( optional_divestment: bool = True, fix_effects: Optional['NonTemporalEffectsUser'] = None, specific_effects: Optional['NonTemporalEffectsUser'] = None, # costs per Flow-Unit/Storage-Size/... - previous_size: Scalar = 0, ): """ These parameters are used to include the timing of investments in the model. @@ -396,8 +395,6 @@ def __init__( self.fix_effects: 'NonTemporalEffectsUser' = fix_effects if fix_effects is not None else {} self.specific_effects: 'NonTemporalEffectsUser' = specific_effects if specific_effects is not None else {} - self.previous_size = previous_size - def _plausibility_checks(self, flow_system): """Validate parameter consistency.""" if flow_system.years is None: @@ -449,28 +446,23 @@ def _plausibility_checks(self, flow_system): f'year_of_investment ({self.year_of_investment}) must be before year_of_decommissioning ({self.year_of_decommissioning})' ) - if self.previous_size != 0: - if not self.minimum_size <= self.previous_size <= self.maximum_size: - raise ValueError( - f'previous_size ({self.previous_size}) must be zero orbetween minimum_size ({self.minimum_size}) ' - f'and maximum_size ({self.maximum_size})' - ) - def transform_data(self, flow_system: 'FlowSystem', name_prefix: str = '') -> None: """Transform all parameter data to match the flow system's coordinate structure.""" self._plausibility_checks(flow_system) - self.effects_of_investment_per_size = flow_system.fit_effects_to_model_coords( + self.fix_effects = flow_system.fit_effects_to_model_coords( label_prefix=name_prefix, - effect_values=self.effects_of_investment_per_size, - label_suffix='effects_of_investment_per_size', + effect_values=self.fix_effects, + label_suffix='fix_effects', dims=['year', 'scenario'], + with_year_of_investment=True, ) - self.effects_of_investment = flow_system.fit_effects_to_model_coords( + self.specific_effects = flow_system.fit_effects_to_model_coords( label_prefix=name_prefix, - effect_values=self.effects_of_investment, - label_suffix='effects_of_investment', + effect_values=self.specific_effects, + label_suffix='specific_effects', dims=['year', 'scenario'], + with_year_of_investment=True, ) self.minimum_size = flow_system.fit_to_model_coords( From 9d78314da37bdccf7fa074fb1c234198d1c1a240 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 7 Aug 2025 12:13:41 +0200 Subject: [PATCH 298/448] Improve InvestmentTiming --- flixopt/features.py | 103 +++++++++++++++++----------------------- flixopt/interface.py | 109 ++++++++++++++++++++++++------------------- 2 files changed, 103 insertions(+), 109 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index 4665aea90..f21b73d48 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -136,9 +136,6 @@ class InvestmentTimingModel(Submodel): Such an Investment is defined by a size, a year_of_investment, and a year_of_decommissioning. In between these years, the size of the investment cannot vary. Outside, its 0. - The year_of_investment is defined as the year, in which the size increases from 0 to the chosen size. - The year_of_decommissioning is defined as the year, in which the size returns to 0. - The year_of_decommissioning can be after the years in the FlowSystem. THis results in no decommissioning. """ parameters: InvestTimingParameters @@ -158,10 +155,8 @@ def _do_modeling(self): self._basic_modeling() self._add_effects() - if self.parameters.year_of_investment is not None: - self._fixed_start_constraint() - if self.parameters.year_of_decommissioning is not None: - self._fixed_end_constraint() + self._constraint_investment() + self._constraint_decommissioning() if self.parameters.duration_in_years is not None: self._fixed_duration_constraint() @@ -175,13 +170,11 @@ def _basic_modeling(self): upper=size_max, coords=self._model.get_coords(['year', 'scenario']), ) - self.add_variables( binary=True, coords=self._model.get_coords(['year', 'scenario']), short_name='is_invested', ) - BoundingPatterns.bounds_with_state( self, variable=self.size, @@ -193,52 +186,30 @@ def _basic_modeling(self): self.add_variables( binary=True, coords=self._model.get_coords(['year', 'scenario']), - short_name='size|year_of_investment', + short_name='size|investment_occurs', ) self.add_variables( binary=True, coords=self._model.get_coords(['year', 'scenario']), - short_name='size|year_of_decommissioning', + short_name='size|decommissioning_occurs', ) BoundingPatterns.state_transition_bounds( self, state_variable=self.is_invested, - switch_on=self.year_of_investment, - switch_off=self.year_of_decommissioning, + switch_on=self.investment_occurs, + switch_off=self.decommissioning_occurs, name=self.is_invested.name, previous_state=0, coord='year', ) - - ######################################################################## - self.add_variables( - binary=True, - coords=self._model.get_coords(['scenario']), - short_name='size|investment_used', - ) - self.add_variables( - binary=True, - coords=self._model.get_coords(['scenario']), - short_name='size|divestment_used', - ) self.add_constraints( - self.year_of_investment.sum('year') == self.investment_used, - short_name='size|year_of_investment|count', + self.investment_occurs.sum('year') <= 1, + short_name='investment_occurs|once', ) self.add_constraints( - self.year_of_decommissioning.sum('year') == self.divestment_used, - short_name='size|year_of_decommissioning|count', + self.decommissioning_occurs.sum('year') <= 1, + short_name='decommissioning_occurs|once', ) - if not self.parameters.optional_investment: - self.add_constraints( - self.investment_used == 1, - short_name='investment_used|fixed', - ) - if not self.parameters.optional_divestment: - self.add_constraints( - self.divestment_used == 1, - short_name='divestment_used|fixed', - ) ######################################################################## self.add_variables( @@ -258,8 +229,8 @@ def _basic_modeling(self): level_variable=self.size, increase_variable=self.size_increase, decrease_variable=self.size_decrease, - increase_binary=self.year_of_investment, - decrease_binary=self.year_of_decommissioning, + increase_binary=self.investment_occurs, + decrease_binary=self.decommissioning_occurs, name=f'{self.label_of_element}|size|changes', max_change=size_max, initial_level=0, @@ -271,7 +242,7 @@ def _add_effects(self): if self.parameters.fix_effects: # Effects depending on when the investment is made - remapped_variable = self.year_of_investment.rename({'year': 'year_of_investment'}) + remapped_variable = self.investment_occurs.rename({'year': 'year_of_investment'}) self._model.effects.add_share_to_effects( name=self.label_of_element, @@ -295,17 +266,29 @@ def _add_effects(self): target='invest', ) - def _fixed_start_constraint(self): - self.add_constraints( - self.year_of_investment.sel(year=self.parameters.year_of_investment) == self.investment_used, - short_name='size|changes|fixed_start', - ) + def _constraint_investment(self): + if self.parameters.force_investment.sum() > 0: + self.add_constraints( + self.investment_occurs == self.parameters.force_investment, + short_name='size|changes|fixed_start', + ) + else: + self.add_constraints( + self.investment_occurs <= self.parameters.allow_investment, + short_name='size|changes|restricted_start', + ) - def _fixed_end_constraint(self): - self.add_constraints( - self.year_of_decommissioning.sel(year=self.parameters.year_of_decommissioning) == self.divestment_used, - short_name='size|changes|fixed_end', - ) + def _constraint_decommissioning(self): + if self.parameters.force_decommissioning.sum() > 0: + self.add_constraints( + self.decommissioning_occurs == self.parameters.force_decommissioning, + short_name='size|changes|fixed_end', + ) + else: + self.add_constraints( + self.decommissioning_occurs <= self.parameters.allow_decommissioning, + short_name='size|changes|restricted_end', + ) def _fixed_duration_constraint(self): years = self._model.flow_system.years @@ -368,14 +351,14 @@ def is_invested(self) -> Optional[linopy.Variable]: return self._variables['is_invested'] @property - def year_of_investment(self) -> linopy.Variable: + def investment_occurs(self) -> linopy.Variable: """Binary increase decision variable""" - return self._variables['size|year_of_investment'] + return self._variables['size|investment_occurs'] @property - def year_of_decommissioning(self) -> linopy.Variable: + def decommissioning_occurs(self) -> linopy.Variable: """Binary decrease decision variable""" - return self._variables['size|year_of_decommissioning'] + return self._variables['size|decommissioning_occurs'] @property def size_decrease(self) -> linopy.Variable: @@ -388,14 +371,14 @@ def size_increase(self) -> linopy.Variable: return self._variables['size|increase'] @property - def investment_used(self) -> linopy.Variable: + def investment_used(self) -> linopy.LinearExpression: """Binary investment decision variable""" - return self._variables['size|investment_used'] + return self.investment_occurs.sum('year') @property - def divestment_used(self) -> linopy.Variable: + def divestment_used(self) -> linopy.LinearExpression: """Binary investment decision variable""" - return self._variables['size|divestment_used'] + return self.decommissioning_occurs.sum('year') @property def duration(self) -> linopy.Variable: diff --git a/flixopt/interface.py b/flixopt/interface.py index 4ca5093ca..3ca65868c 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -6,6 +6,8 @@ import logging from typing import TYPE_CHECKING, Dict, Iterator, List, Literal, Optional, Union +import xarray as xr + from .config import CONFIG from .core import NonTemporalData, NonTemporalDataUser, Scalar, TemporalDataUser from .structure import Interface, register_class_for_io @@ -335,6 +337,10 @@ def is_fixed_size(self) -> bool: return self.fixed_size is not None +YearOfInvestmentData = Union[int, float, xr.DataArray] +YearOfInvestmentDataBool = Union[bool, xr.DataArray] + + @register_class_for_io class InvestTimingParameters(Interface): """ @@ -346,20 +352,24 @@ class InvestTimingParameters(Interface): def __init__( self, - year_of_investment: Optional[int] = None, - year_of_decommissioning: Optional[int] = None, - duration_in_years: Optional[int] = None, - minimum_size: Optional[Scalar] = None, - maximum_size: Optional[Scalar] = None, - fixed_size: Optional[Scalar] = None, - optional_investment: bool = True, - optional_divestment: bool = True, + allow_investment: YearOfInvestmentDataBool = True, + allow_decommissioning: YearOfInvestmentDataBool = True, + force_investment: YearOfInvestmentDataBool = False, + force_decommissioning: YearOfInvestmentDataBool = False, + duration_in_years: Optional[YearOfInvestmentData] = None, + minimum_size: Optional[YearOfInvestmentData] = None, + maximum_size: Optional[YearOfInvestmentData] = None, + fixed_size: Optional[YearOfInvestmentData] = None, fix_effects: Optional['NonTemporalEffectsUser'] = None, specific_effects: Optional['NonTemporalEffectsUser'] = None, # costs per Flow-Unit/Storage-Size/... ): """ These parameters are used to include the timing of investments in the model. Two out of three parameters (year_of_investment, year_of_decommissioning, duration_in_years) can be fixed. + This has a 'year_of_investment' dimension in some parameters: + allow_investment: Whether investment is allowed in a certain year + allow_decommissioning: Whether divestment is allowed in a certain year + duration_between_investment_and_decommissioning: Duration between investment and decommissioning Args: year_of_investment: Year in which the investment occurs (inclusive). Present from this year onwards. @@ -385,11 +395,11 @@ def __init__( self.minimum_size = minimum_size if minimum_size is not None else CONFIG.modeling.EPSILON self.maximum_size = maximum_size if maximum_size is not None else CONFIG.modeling.BIG self.fixed_size = fixed_size - self.optional_investment = optional_investment - self.optional_divestment = optional_divestment - self.year_of_investment = year_of_investment - self.year_of_decommissioning = year_of_decommissioning + self.allow_investment = allow_investment + self.allow_decommissioning = allow_decommissioning + self.force_investment = force_investment + self.force_decommissioning = force_decommissioning self.duration_in_years = duration_in_years self.fix_effects: 'NonTemporalEffectsUser' = fix_effects if fix_effects is not None else {} @@ -400,56 +410,48 @@ def _plausibility_checks(self, flow_system): if flow_system.years is None: raise ValueError("YearAwareInvestParameters requires the flow_system to have a 'years' dimension.") - if all( - [param is None for param in (self.year_of_investment, self.year_of_decommissioning, self.duration_in_years)] - ): + if (self.force_investment.sum('year') > 1).any(): + raise ValueError('force_investment can only be True for a single year.') + if (self.force_decommissioning.sum('year') > 1).any(): + raise ValueError('force_decommissioning can only be True for a single year.') + + specify_timing = ( + (self.duration_in_years is None) + + bool((self.force_investment.sum('year') > 1).any()) + + bool((self.force_decommissioning.sum('year') > 1).any()) + ) + + if specify_timing == 0: # TODO: Should this be an exception or rather a warning? Is there a valid use case for this? # And a mathematically valid formulation (regarding the effects especially)? raise ValueError( - 'Either year_of_investment, year_of_decommissioning or duration_in_years must be specified.' + 'Either the the duration of an investment needs to be set, or the investment or decommissioning ' + 'needs to be forced in one year.' ) - if ( - sum( - [ - param is not None - for param in (self.year_of_investment, self.year_of_decommissioning, self.duration_in_years) - ] - ) - > 2 - ): - # TODO: Should this be an exception or rather a warning? + if specify_timing == 3: + # TODO: Should this be an exception or rather a warning? Is there a valid use case for this? + # And a mathematically valid formulation (regarding the effects especially)? raise ValueError( - f'InvestmentParameters is overdefined. Not all of {self.year_of_investment=}, ' - f'{self.year_of_decommissioning=} and {self.duration_in_years=} can be specified.' + 'Either the the duration of an investment needs to be set, or the investment or decommissioning ' + 'needs to be forced in one year.' ) - if self.year_of_investment is not None: - if not (flow_system.years[0] <= self.year_of_investment <= flow_system.years[-1]): + if (self.force_investment.sum('year') >= 1).any() and (self.force_decommissioning.sum('year') >= 1).any(): + year_of_forced_investment = ( + self.force_investment.where(self.force_investment) * self.force_investment.year + ).sum('year') + year_of_forced_decommissioning = ( + self.force_decommissioning.where(self.force_decommissioning) * self.force_decommissioning.year + ).sum('year') + if not (year_of_forced_investment < year_of_forced_decommissioning).all(): raise ValueError( - f'year_of_investment ({self.year_of_investment}) must be between ' - f'{flow_system.years[0]} and {flow_system.years[-1]}' + f'force_investment needs to be before force_decommissioning. Got:\n' + f'{self.force_investment}\nand\n{self.force_decommissioning}' ) - if self.year_of_decommissioning is not None: - if not (flow_system.years[0] <= self.year_of_decommissioning <= flow_system.years[-1]): - raise ValueError( - f'year_of_decommissioning ({self.year_of_decommissioning}) must be between {flow_system.years[0]} and {flow_system.years[-1]}' - ) - - if ( - self.year_of_decommissioning is not None - and self.year_of_investment is not None - and not self.year_of_investment < self.year_of_decommissioning - ): - raise ValueError( - f'year_of_investment ({self.year_of_investment}) must be before year_of_decommissioning ({self.year_of_decommissioning})' - ) - def transform_data(self, flow_system: 'FlowSystem', name_prefix: str = '') -> None: """Transform all parameter data to match the flow system's coordinate structure.""" - self._plausibility_checks(flow_system) - self.fix_effects = flow_system.fit_effects_to_model_coords( label_prefix=name_prefix, effect_values=self.fix_effects, @@ -465,6 +467,13 @@ def transform_data(self, flow_system: 'FlowSystem', name_prefix: str = '') -> No with_year_of_investment=True, ) + self.force_investment = flow_system.fit_to_model_coords( + f'{name_prefix}|force_investment', self.force_investment, dims=['year', 'scenario'] + ) + self.force_decommissioning = flow_system.fit_to_model_coords( + f'{name_prefix}|force_decommissioning', self.force_decommissioning, dims=['year', 'scenario'] + ) + self.minimum_size = flow_system.fit_to_model_coords( f'{name_prefix}|minimum_size', self.minimum_size, dims=['year', 'scenario'] ) @@ -478,6 +487,8 @@ def transform_data(self, flow_system: 'FlowSystem', name_prefix: str = '') -> No # TODO: self.previous_size to only scenarios + self._plausibility_checks(flow_system) + @property def minimum_or_fixed_size(self) -> NonTemporalData: """Get the effective minimum size (fixed size takes precedence).""" From ca6fb0679e4cc097d1a54fafd5978e039f883d74 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 7 Aug 2025 12:42:56 +0200 Subject: [PATCH 299/448] Improve InvestmentTiming --- flixopt/features.py | 14 +++++--------- flixopt/interface.py | 30 +++++++++++------------------- 2 files changed, 16 insertions(+), 28 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index f21b73d48..7ac2a0c5b 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -158,7 +158,7 @@ def _do_modeling(self): self._constraint_investment() self._constraint_decommissioning() if self.parameters.duration_in_years is not None: - self._fixed_duration_constraint() + self._constraint_duration_between_investment_and_decommissioning() def _basic_modeling(self): size_min, size_max = self.parameters.minimum_or_fixed_size, self.parameters.maximum_or_fixed_size @@ -290,7 +290,7 @@ def _constraint_decommissioning(self): short_name='size|changes|restricted_end', ) - def _fixed_duration_constraint(self): + def _constraint_duration_between_investment_and_decommissioning(self): years = self._model.flow_system.years years_of_decommissioning = years + self.parameters.duration_in_years @@ -300,9 +300,7 @@ def _fixed_duration_constraint(self): valid_years_of_investment = years[valid_mask] valid_years_of_decommissioning = years_of_decommissioning[valid_mask] actual_years_of_decommissioning = ( - self.years_of_decommissioning.sel(year=valid_years_of_decommissioning, method='bfill') - .coords['year'] - .values + self.investment_occurs.sel(year=valid_years_of_decommissioning, method='bfill').coords['year'].values ) # Warning for mismatched years @@ -326,14 +324,12 @@ def _fixed_duration_constraint(self): # Now you can use proper xarray groupby grouped_increases = ( - self.year_of_investment.sel(year=valid_years_of_investment) - .groupby(group) - .sum('year_of_decommissioning') + self.investment_occurs.sel(year=valid_years_of_investment).groupby(group).sum('year_of_decommissioning') ) # Create constraints self.add_constraints( - self.year_of_decommissioning.sel(year=grouped_increases.coords['year_of_decommissioning']) + self.decommissioning_occurs.sel(year=grouped_increases.coords['year_of_decommissioning']) == grouped_increases, short_name='size|changes|fixed_duration', ) diff --git a/flixopt/interface.py b/flixopt/interface.py index 3ca65868c..993f9ec7c 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -372,24 +372,16 @@ def __init__( duration_between_investment_and_decommissioning: Duration between investment and decommissioning Args: - year_of_investment: Year in which the investment occurs (inclusive). Present from this year onwards. - If None, the year_of_investment is not fixed. - year_of_decommissioning: Year in which the unit is decommissioned (exclusive). Present up to this year. - If None, the year_of_decommissioning is not fixed. - duration_in_years: Duration between year_of_investment and year_of_decommissioning. - If None, the duration is not fixed. - minimum_size: Minimum possible size of the investment. - maximum_size: Maximum possible size of the investment. - fixed_size: If specified, investment size is fixed to this value. - optional_investment: If False, the investment is mandatory. - optional_divestment: If False, the divestment is mandatory. - specific_effects: Specific costs, e.g., in €/kW_nominal or €/m²_nominal. - Example: {costs: 3, CO2: 0.3} with costs and CO2 representing an Object of class Effect - (Attention: Annualize costs to chosen period!) - effects_of_investment: Effects depending on when an investment decision is made. These can occur in the investment year or in multiple years. - If the effects need to occur in multiple years, you need to pass an xr.DataArray with the coord 'year_of_investment'. - Example: {'costs': 1000} applies 1000 to costs in the investment year. - previous_size: The size of the investment before the first period. Defaults to 0. + allow_investment: Allow investment in a certain year. By default, allow it in all years. + allow_decommissioning: Allow decommissioning in a certain year. By default, allow it in all years. + force_investment: Force the investment to occur in a certain year. + force_decommissioning: Force the decommissioning to occur in a certain year. + duration_in_years: Fix the duration between the year of investment and the year ofdecommissioning. + minimum_size: Minimum possible size of the investment. Can depend on the year of investment. + maximum_size: Maximum possible size of the investment. Can depend on the year of investment. + fixed_size: Fix the size of the investment. Can depend on the year of investment. Can still be 0 if not forced. + specific_effects: Effects of the Investment, dependent on the size. Take care. These are not broadcasted internally! + fix_effects: Effects of the Investment, independent of the size. Take care. These are not broadcasted internally! """ self.minimum_size = minimum_size if minimum_size is not None else CONFIG.modeling.EPSILON @@ -416,7 +408,7 @@ def _plausibility_checks(self, flow_system): raise ValueError('force_decommissioning can only be True for a single year.') specify_timing = ( - (self.duration_in_years is None) + (self.duration_in_years is not None) + bool((self.force_investment.sum('year') > 1).any()) + bool((self.force_decommissioning.sum('year') > 1).any()) ) From abbe5349ed50b3524c071ec2e22b666f9950a1e6 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 7 Aug 2025 12:56:56 +0200 Subject: [PATCH 300/448] Add specific_effect back --- flixopt/effects.py | 4 ++-- flixopt/features.py | 6 +++--- flixopt/interface.py | 30 ++++++++++++++++++++++++++---- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/flixopt/effects.py b/flixopt/effects.py index da032878f..d7392e239 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -13,7 +13,7 @@ import numpy as np import xarray as xr -from .core import Scalar, TemporalData, TemporalDataUser +from .core import NonTemporalDataUser, Scalar, TemporalData, TemporalDataUser from .features import ShareAllocationModel from .structure import Element, ElementModel, FlowSystemModel, Interface, Submodel, register_class_for_io @@ -192,7 +192,7 @@ def _do_modeling(self): TemporalEffectsUser = Union[TemporalDataUser, Dict[str, TemporalDataUser]] # User-specified Shares to Effects """ This datatype is used to define a temporal share to an effect by a certain attribute. """ -NonTemporalEffectsUser = Union[Scalar, Dict[str, Scalar]] # User-specified Shares to Effects +NonTemporalEffectsUser = Union[NonTemporalDataUser, Dict[str, NonTemporalDataUser]] # User-specified Shares to Effects """ This datatype is used to define a scalar share to an effect by a certain attribute. """ TemporalEffects = Dict[str, TemporalData] # User-specified Shares to Effects diff --git a/flixopt/features.py b/flixopt/features.py index 7ac2a0c5b..281694f75 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -248,12 +248,12 @@ def _add_effects(self): name=self.label_of_element, expressions={ effect: (remapped_variable * factor).sum('year_of_investment') - for effect, factor in self.parameters.fix_effects.items() + for effect, factor in self.parameters.fixed_effects_by_investment_year.items() }, target='invest', ) - if self.parameters.specific_effects: + if self.parameters.specific_effects_by_investment_year: # Annual effects proportional to investment size remapped_variable = self.size_increase.rename({'year': 'year_of_investment'}) @@ -261,7 +261,7 @@ def _add_effects(self): name=self.label_of_element, expressions={ effect: (remapped_variable * factor).sum('year_of_investment') - for effect, factor in self.parameters.specific_effects.items() + for effect, factor in self.parameters.specific_effects_by_investment_year.items() }, target='invest', ) diff --git a/flixopt/interface.py b/flixopt/interface.py index 993f9ec7c..02336de61 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -337,8 +337,10 @@ def is_fixed_size(self) -> bool: return self.fixed_size is not None -YearOfInvestmentData = Union[int, float, xr.DataArray] -YearOfInvestmentDataBool = Union[bool, xr.DataArray] +YearOfInvestmentData = NonTemporalDataUser +"""This datatype is used to define things related to the year of investment.""" +YearOfInvestmentDataBool = Union[bool, YearOfInvestmentData] +"""This datatype is used to define things with boolean data related to the year of investment.""" @register_class_for_io @@ -356,12 +358,14 @@ def __init__( allow_decommissioning: YearOfInvestmentDataBool = True, force_investment: YearOfInvestmentDataBool = False, force_decommissioning: YearOfInvestmentDataBool = False, - duration_in_years: Optional[YearOfInvestmentData] = None, + duration_in_years: Optional[Scalar] = None, minimum_size: Optional[YearOfInvestmentData] = None, maximum_size: Optional[YearOfInvestmentData] = None, fixed_size: Optional[YearOfInvestmentData] = None, fix_effects: Optional['NonTemporalEffectsUser'] = None, specific_effects: Optional['NonTemporalEffectsUser'] = None, # costs per Flow-Unit/Storage-Size/... + fixed_effects_by_investment_year: Optional[YearOfInvestmentData] = None, + specific_effects_by_investment_year: Optional[YearOfInvestmentData] = None, ): """ These parameters are used to include the timing of investments in the model. @@ -396,6 +400,12 @@ def __init__( self.fix_effects: 'NonTemporalEffectsUser' = fix_effects if fix_effects is not None else {} self.specific_effects: 'NonTemporalEffectsUser' = specific_effects if specific_effects is not None else {} + self.fixed_effects_by_investment_year = ( + fixed_effects_by_investment_year if fixed_effects_by_investment_year is not None else {} + ) + self.specific_effects_by_investment_year = ( + specific_effects_by_investment_year if specific_effects_by_investment_year is not None else {} + ) def _plausibility_checks(self, flow_system): """Validate parameter consistency.""" @@ -449,13 +459,25 @@ def transform_data(self, flow_system: 'FlowSystem', name_prefix: str = '') -> No effect_values=self.fix_effects, label_suffix='fix_effects', dims=['year', 'scenario'], - with_year_of_investment=True, ) self.specific_effects = flow_system.fit_effects_to_model_coords( label_prefix=name_prefix, effect_values=self.specific_effects, label_suffix='specific_effects', dims=['year', 'scenario'], + ) + self.fixed_effects_by_investment_year = flow_system.fit_effects_to_model_coords( + label_prefix=name_prefix, + effect_values=self.fixed_effects_by_investment_year, + label_suffix='fixed_effects_by_investment_year', + dims=['year', 'scenario'], + with_year_of_investment=True, + ) + self.specific_effects_by_investment_year = flow_system.fit_effects_to_model_coords( + label_prefix=name_prefix, + effect_values=self.specific_effects_by_investment_year, + label_suffix='specific_effects_by_investment_year', + dims=['year', 'scenario'], with_year_of_investment=True, ) From 6f550dd3af0ee8a6b0468227db8c3ee1ddf653e5 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 7 Aug 2025 16:04:54 +0200 Subject: [PATCH 301/448] add effects_by_investment_year back --- flixopt/features.py | 17 +++++++++++++ flixopt/interface.py | 57 ++++++++++++++++++++++++++++++-------------- 2 files changed, 56 insertions(+), 18 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index 281694f75..af8c42909 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -241,6 +241,23 @@ def _add_effects(self): """Add investment effects to the model.""" if self.parameters.fix_effects: + self._model.effects.add_share_to_effects( + name=self.label_of_element, + expressions={ + effect: self.is_invested * factor if self.is_invested is not None else factor + for effect, factor in self.parameters.fix_effects.items() + }, + target='invest', + ) + + if self.parameters.specific_effects: + self._model.effects.add_share_to_effects( + name=self.label_of_element, + expressions={effect: self.size * factor for effect, factor in self.parameters.specific_effects.items()}, + target='invest', + ) + + if self.parameters.fixed_effects_by_investment_year: # Effects depending on when the investment is made remapped_variable = self.investment_occurs.rename({'year': 'year_of_investment'}) diff --git a/flixopt/interface.py b/flixopt/interface.py index 02336de61..61b17ecc7 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -364,8 +364,8 @@ def __init__( fixed_size: Optional[YearOfInvestmentData] = None, fix_effects: Optional['NonTemporalEffectsUser'] = None, specific_effects: Optional['NonTemporalEffectsUser'] = None, # costs per Flow-Unit/Storage-Size/... - fixed_effects_by_investment_year: Optional[YearOfInvestmentData] = None, - specific_effects_by_investment_year: Optional[YearOfInvestmentData] = None, + fixed_effects_by_investment_year: Optional[xr.DataArray] = None, + specific_effects_by_investment_year: Optional[xr.DataArray] = None, ): """ These parameters are used to include the timing of investments in the model. @@ -384,8 +384,15 @@ def __init__( minimum_size: Minimum possible size of the investment. Can depend on the year of investment. maximum_size: Maximum possible size of the investment. Can depend on the year of investment. fixed_size: Fix the size of the investment. Can depend on the year of investment. Can still be 0 if not forced. - specific_effects: Effects of the Investment, dependent on the size. Take care. These are not broadcasted internally! - fix_effects: Effects of the Investment, independent of the size. Take care. These are not broadcasted internally! + specific_effects: Effects dependent on the size. + fix_effects: Effects of the Investment, independent of the size. + + fixed_effects_by_investment_year: Effects dependent on the year of investment. + The passed xr.DataArray needs to match the FlowSystem dimensions (except time, but including "year_of_investment"). No internal Broadcasting! + "year_of_investment" has the same values as the year dimension. Access it through `flow_system.year_of_investment`. + specific_effects_by_investment_year: Effects dependent on the year of investment and the chosen size. + The passed xr.DataArray needs to match the FlowSystem dimensions (except time, but including "year_of_investment"). No internal Broadcasting! + "year_of_investment" has the same values as the year dimension. Access it through `flow_system.year_of_investment`. """ self.minimum_size = minimum_size if minimum_size is not None else CONFIG.modeling.EPSILON @@ -466,20 +473,6 @@ def transform_data(self, flow_system: 'FlowSystem', name_prefix: str = '') -> No label_suffix='specific_effects', dims=['year', 'scenario'], ) - self.fixed_effects_by_investment_year = flow_system.fit_effects_to_model_coords( - label_prefix=name_prefix, - effect_values=self.fixed_effects_by_investment_year, - label_suffix='fixed_effects_by_investment_year', - dims=['year', 'scenario'], - with_year_of_investment=True, - ) - self.specific_effects_by_investment_year = flow_system.fit_effects_to_model_coords( - label_prefix=name_prefix, - effect_values=self.specific_effects_by_investment_year, - label_suffix='specific_effects_by_investment_year', - dims=['year', 'scenario'], - with_year_of_investment=True, - ) self.force_investment = flow_system.fit_to_model_coords( f'{name_prefix}|force_investment', self.force_investment, dims=['year', 'scenario'] @@ -501,6 +494,34 @@ def transform_data(self, flow_system: 'FlowSystem', name_prefix: str = '') -> No # TODO: self.previous_size to only scenarios + # No Broadcasting! Until a safe way is established, we need to do check for this! + for effect, da in self.fixed_effects_by_investment_year.items(): + if set(da.dims) != set(list(flow_system.coords) + ['year_of_investment']): + raise ValueError( + f'fixed_effects_by_investment_year need to have the same dimensions as the FlowSystem. ' + f'Got {da.dims} and {list(flow_system.coords)} for effect {effect}' + ) + for effect, da in self.specific_effects_by_investment_year.items(): + if set(da.dims) != set(list(flow_system.coords) + ['year_of_investment']): + raise ValueError( + f'specific_effects_by_investment_year need to have the same dimensions as the FlowSystem. ' + f'Got {da.dims} and {list(flow_system.coords)} for effect {effect}' + ) + self.fixed_effects_by_investment_year = flow_system.fit_effects_to_model_coords( + label_prefix=name_prefix, + effect_values=self.fixed_effects_by_investment_year, + label_suffix='fixed_effects_by_investment_year', + dims=['year', 'scenario'], + with_year_of_investment=True, + ) + self.specific_effects_by_investment_year = flow_system.fit_effects_to_model_coords( + label_prefix=name_prefix, + effect_values=self.specific_effects_by_investment_year, + label_suffix='specific_effects_by_investment_year', + dims=['year', 'scenario'], + with_year_of_investment=True, + ) + self._plausibility_checks(flow_system) @property From b6185780adbba06e82093f4da43fa4401b5352d9 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 7 Aug 2025 17:02:56 +0200 Subject: [PATCH 302/448] Add year_of_investment to FLowSystem.sel() --- flixopt/flow_system.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 59b79831f..69813f4d6 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -709,19 +709,23 @@ def sel( if not self.connected_and_transformed: self.connect_and_transform() + ds = self.to_dataset() + # Build indexers dict from non-None parameters indexers = {} if time is not None: indexers['time'] = time if year is not None: indexers['year'] = year + if 'year_of_investment' in ds.dims: + indexers['year_of_investment'] = year if scenario is not None: indexers['scenario'] = scenario if not indexers: return self.copy() # Return a copy when no selection - selected_dataset = self.to_dataset().sel(**indexers) + selected_dataset = ds.sel(**indexers) return self.__class__.from_dataset(selected_dataset) def isel( @@ -744,19 +748,23 @@ def isel( if not self.connected_and_transformed: self.connect_and_transform() + ds = self.to_dataset() + # Build indexers dict from non-None parameters indexers = {} if time is not None: indexers['time'] = time if year is not None: indexers['year'] = year + if 'year_of_investment' in ds.dims: + indexers['year_of_investment'] = year if scenario is not None: indexers['scenario'] = scenario if not indexers: return self.copy() # Return a copy when no selection - selected_dataset = self.to_dataset().isel(**indexers) + selected_dataset = ds.isel(**indexers) return self.__class__.from_dataset(selected_dataset) def resample( From 4ff3e31699b4f001233e043565071d0704cf9029 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 7 Aug 2025 17:03:17 +0200 Subject: [PATCH 303/448] Improve Interface --- flixopt/interface.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/flixopt/interface.py b/flixopt/interface.py index 61b17ecc7..92da4bfba 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -385,12 +385,18 @@ def __init__( maximum_size: Maximum possible size of the investment. Can depend on the year of investment. fixed_size: Fix the size of the investment. Can depend on the year of investment. Can still be 0 if not forced. specific_effects: Effects dependent on the size. + These will occur in each year, depending on the size in that year. fix_effects: Effects of the Investment, independent of the size. + These will occur in each year, depending on wether the size is greater zero in that year. fixed_effects_by_investment_year: Effects dependent on the year of investment. + These effects will depend on the year of the investment. The actual effects can occur in other years, + letting you model things like annuities, which depend on when an investment was taken. The passed xr.DataArray needs to match the FlowSystem dimensions (except time, but including "year_of_investment"). No internal Broadcasting! "year_of_investment" has the same values as the year dimension. Access it through `flow_system.year_of_investment`. specific_effects_by_investment_year: Effects dependent on the year of investment and the chosen size. + These effects will depend on the year of the investment. The actual effects can occur in other years, + letting you model things like annuities, which depend on when an investment was taken. The passed xr.DataArray needs to match the FlowSystem dimensions (except time, but including "year_of_investment"). No internal Broadcasting! "year_of_investment" has the same values as the year dimension. Access it through `flow_system.year_of_investment`. @@ -495,17 +501,25 @@ def transform_data(self, flow_system: 'FlowSystem', name_prefix: str = '') -> No # TODO: self.previous_size to only scenarios # No Broadcasting! Until a safe way is established, we need to do check for this! + self.fixed_effects_by_investment_year = flow_system.effects.create_effect_values_dict( + self.fixed_effects_by_investment_year + ) for effect, da in self.fixed_effects_by_investment_year.items(): - if set(da.dims) != set(list(flow_system.coords) + ['year_of_investment']): + dims = set(da.coords) + if not {'year_of_investment', 'year'}.issubset(dims): raise ValueError( - f'fixed_effects_by_investment_year need to have the same dimensions as the FlowSystem. ' - f'Got {da.dims} and {list(flow_system.coords)} for effect {effect}' + f'fixed_effects_by_investment_year need to have a "year_of_investment" dimension and a ' + f'"year" dimension. Got {dims} for effect {effect}' ) + self.specific_effects_by_investment_year = flow_system.effects.create_effect_values_dict( + self.specific_effects_by_investment_year + ) for effect, da in self.specific_effects_by_investment_year.items(): - if set(da.dims) != set(list(flow_system.coords) + ['year_of_investment']): + dims = set(da.coords) + if not {'year_of_investment', 'year'}.issubset(dims): raise ValueError( - f'specific_effects_by_investment_year need to have the same dimensions as the FlowSystem. ' - f'Got {da.dims} and {list(flow_system.coords)} for effect {effect}' + f'specific_effects_by_investment_year need to have a "year_of_investment" dimension and a ' + f'"year" dimension. Got {dims} for effect {effect}' ) self.fixed_effects_by_investment_year = flow_system.fit_effects_to_model_coords( label_prefix=name_prefix, From 98b6072a1e61502c9cc73e5b2a90ed525d91a5ba Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 7 Aug 2025 17:13:19 +0200 Subject: [PATCH 304/448] Handle selection of years properly again --- flixopt/flow_system.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 69813f4d6..0acee1d55 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -726,6 +726,12 @@ def sel( return self.copy() # Return a copy when no selection selected_dataset = ds.sel(**indexers) + if 'year_of_investment' in selected_dataset.coords and selected_dataset.coords['year_of_investment'].size == 1: + logger.critical( + 'Selected a single year while using InvestmentTiming. This is not supported and will lead to Errors ' + 'when trying to create a Calculation from this FlowSystem. Please select multiple years instead, ' + 'or remove the InvestmentTimingParameters.' + ) return self.__class__.from_dataset(selected_dataset) def isel( @@ -765,6 +771,12 @@ def isel( return self.copy() # Return a copy when no selection selected_dataset = ds.isel(**indexers) + if 'year_of_investment' in selected_dataset.coords and selected_dataset.coords['year_of_investment'].size == 1: + logger.critical( + 'Selected a single year while using InvestmentTiming. This is not supported and will lead to Errors ' + 'when trying to create a Calculation from this FlowSystem. Please select multiple years instead, ' + 'or remove the InvestmentTimingParameters.' + ) return self.__class__.from_dataset(selected_dataset) def resample( From fa4c107c7dd4839b047b830021a99a0a9cdf7283 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 7 Aug 2025 22:30:29 +0200 Subject: [PATCH 305/448] Temp --- tests/test_models.py | 102 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 97 insertions(+), 5 deletions(-) diff --git a/tests/test_models.py b/tests/test_models.py index 37a9fb96c..9e2cce36a 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,7 +1,10 @@ +from typing import Union + import linopy import numpy as np import pandas as pd import pytest +import xarray as xr import flixopt as fx @@ -18,6 +21,75 @@ ) +def calculate_annual_payment(total_cost: float, remaining_years: int, discount_rate: float) -> float: + """Calculate annualized payment for given remaining years. + + Args: + total_cost: Total cost to be annualized. + remaining_years: Number of remaining years. + discount_rate: Discount rate for annualization. + + Returns: + Annual payment amount. + """ + if remaining_years == 1: + return total_cost + + return ( + total_cost + * (discount_rate * (1 + discount_rate) ** remaining_years) + / ((1 + discount_rate) ** remaining_years - 1) + ) + + +def create_annualized_effects( + year_of_investments: Union[range, list, pd.Index], + all_years: Union[range, list, pd.Index], + total_cost: float, + discount_rate: float, + horizon_end: int, + extra_dim: str = 'year_of_investment', +) -> xr.DataArray: + """Create a 2D effects array for annualized costs. + + Creates an array where investing in year Y results in annualized costs + applied to years Y through horizon_end. + + Args: + year_of_investments: Years when investment decisions can be made. + all_years: All years in the model (for the 'year' dimension). + total_cost: Total upfront cost to be annualized. + discount_rate: Discount rate for annualization calculation. + horizon_end: Last year when effects apply. + extra_dim: Name for the investment year dimension. + + Returns: + xr.DataArray with dimensions [extra_dim, 'year'] containing annualized costs. + """ + + # Convert to lists for easier iteration + year_of_investments_list = list(year_of_investments) + all_years_list = list(all_years) + + # Initialize cost matrix + cost_matrix = np.zeros((len(year_of_investments_list), len(all_years_list))) + + # Fill matrix with annualized costs + for i, year_of_investment in enumerate(year_of_investments_list): + remaining_years = horizon_end - year_of_investment + 1 + if remaining_years > 0: + annual_cost = calculate_annual_payment(total_cost, remaining_years, discount_rate) + + # Apply cost to years from year_of_investment through horizon_end + for j, cost_year in enumerate(all_years_list): + if year_of_investment <= cost_year <= horizon_end: + cost_matrix[i, j] = annual_cost + + return xr.DataArray( + cost_matrix, coords={extra_dim: year_of_investments_list, 'year': all_years_list}, dims=[extra_dim, 'year'] + ) + + @pytest.fixture def flow_system() -> fx.FlowSystem: """Create basic elements for component testing with coordinate parametrization.""" @@ -114,17 +186,30 @@ class TestYearAwareInvestmentModelDirect: """Test the YearAwareInvestmentModel class directly with linopy.""" def test_flow_invest_new(self, flow_system): + da = xr.DataArray( + [25, 30, 35, 40, 45, 50], + coords=(flow_system.years_of_investment,), + ).expand_dims(year=flow_system.years) + da = da.where(da.year >= da.year_of_investment).fillna(0) + flow = fx.Flow( 'Wärme', bus='Fernwärme', size=fx.InvestTimingParameters( - year_of_investment=2020, - year_of_decommissioning=2030, - # duration_in_years=3, + force_investment=xr.DataArray( + [False if year != 2021 else True for year in flow_system.years], coords=(flow_system.years,) + ), + # year_of_decommissioning=2030, + duration_in_years=2, minimum_size=900, maximum_size=1000, - effects_of_investment_per_size=200 * 1e5, - previous_size=900, + specific_effects=xr.DataArray( + [25, 30, 35, 40, 45, 50], + coords=(flow_system.years,), + ) + * 0, + # fix_effects=-2e3, + specific_effects_by_investment_year=da, ), relative_maximum=np.linspace(0.5, 1, flow_system.timesteps.size), ) @@ -135,6 +220,11 @@ def test_flow_invest_new(self, flow_system): # calculation.model.add_constraints(calculation.model['Source(Wärme)|decrease'].isel(year=2) == 1) calculation.solve(fx.solvers.GurobiSolver(0, 60)) + calculation = fx.FullCalculation('GenericName', flow_system.sel(year=[2022, 2030])) + calculation.do_modeling() + # calculation.model.add_constraints(calculation.model['Source(Wärme)|decrease'].isel(year=2) == 1) + calculation.solve(fx.solvers.GurobiSolver(0, 60)) + ds = calculation.results['Source'].solution filtered_ds_year = ds[[v for v in ds.data_vars if ds[v].dims == ('year',)]] print(filtered_ds_year.round(0).to_pandas().T) @@ -142,4 +232,6 @@ def test_flow_invest_new(self, flow_system): filtered_ds_scalar = ds[[v for v in ds.data_vars if ds[v].dims == tuple()]] print(filtered_ds_scalar.round(0).to_pandas().T) + print(calculation.results.solution['costs(invest)|total'].to_pandas()) + print('##') From f29dcdef68acae412938735bb585310ec97f8eee Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 13 Aug 2025 17:48:48 +0200 Subject: [PATCH 306/448] Make ModelingPrimitives.consecutive_duration_tracking() dim-agnostic --- flixopt/features.py | 4 ++++ flixopt/modeling.py | 41 +++++++++++++++++++++-------------------- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index af8c42909..ffa70d528 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -479,6 +479,8 @@ def _do_modeling(self): short_name='consecutive_on_hours', minimum_duration=self.parameters.consecutive_on_hours_min, maximum_duration=self.parameters.consecutive_on_hours_max, + duration_per_step=self.hours_per_step, + duration_dim='time', previous_duration=self._get_previous_on_duration(), ) @@ -490,6 +492,8 @@ def _do_modeling(self): short_name='consecutive_off_hours', minimum_duration=self.parameters.consecutive_off_hours_min, maximum_duration=self.parameters.consecutive_off_hours_max, + duration_per_step=self.hours_per_step, + duration_dim='time', previous_duration=self._get_previous_off_duration(), ) # TODO: diff --git a/flixopt/modeling.py b/flixopt/modeling.py index f0e354bc5..a630ac33e 100644 --- a/flixopt/modeling.py +++ b/flixopt/modeling.py @@ -229,6 +229,8 @@ def consecutive_duration_tracking( short_name: str = None, minimum_duration: Optional[TemporalData] = None, maximum_duration: Optional[TemporalData] = None, + duration_dim: str = 'time', + duration_per_step: Union[Scalar, xr.DataArray] = None, previous_duration: TemporalData = 0, ) -> Tuple[linopy.Variable, Tuple[linopy.Constraint, linopy.Constraint, linopy.Constraint]]: """ @@ -236,9 +238,9 @@ def consecutive_duration_tracking( Mathematical formulation: duration[t] ≤ state[t] * M ∀t - duration[t+1] ≤ duration[t] + hours_per_step[t] ∀t - duration[t+1] ≥ duration[t] + hours_per_step[t] + (state[t+1] - 1) * M ∀t - duration[0] = (hours_per_step[0] + previous_duration) * state[0] + duration[t+1] ≤ duration[t] + duration_per_step[t] ∀t + duration[t+1] ≥ duration[t] + duration_per_step[t] + (state[t+1] - 1) * M ∀t + duration[0] = (duration_per_step[0] + previous_duration) * state[0] If minimum_duration provided: duration[t] ≥ (state[t-1] - state[t]) * minimum_duration[t-1] ∀t > 0 @@ -257,14 +259,13 @@ def consecutive_duration_tracking( if not isinstance(model, Submodel): raise ValueError('ModelingPrimitives.sum_up_variable() can only be used with a Submodel') - hours_per_step = model.hours_per_step - mega = hours_per_step.sum('time') + previous_duration # Big-M value + mega = duration_per_step.sum(duration_dim) + previous_duration # Big-M value # Duration variable duration = model.add_variables( lower=0, upper=maximum_duration if maximum_duration is not None else mega, - coords=model.get_coords(), + coords=state_variable.coords, name=name, short_name=short_name, ) @@ -274,25 +275,25 @@ def consecutive_duration_tracking( # Upper bound: duration[t] ≤ state[t] * M constraints['ub'] = model.add_constraints(duration <= state_variable * mega, name=f'{duration.name}|ub') - # Forward constraint: duration[t+1] ≤ duration[t] + hours_per_step[t] + # Forward constraint: duration[t+1] ≤ duration[t] + duration_per_step[t] constraints['forward'] = model.add_constraints( - duration.isel(time=slice(1, None)) - <= duration.isel(time=slice(None, -1)) + hours_per_step.isel(time=slice(None, -1)), + duration.isel({duration_dim: slice(1, None)}) + <= duration.isel({duration_dim: slice(None, -1)}) + duration_per_step.isel({duration_dim: slice(None, -1)}), name=f'{duration.name}|forward', ) - # Backward constraint: duration[t+1] ≥ duration[t] + hours_per_step[t] + (state[t+1] - 1) * M + # Backward constraint: duration[t+1] ≥ duration[t] + duration_per_step[t] + (state[t+1] - 1) * M constraints['backward'] = model.add_constraints( - duration.isel(time=slice(1, None)) - >= duration.isel(time=slice(None, -1)) - + hours_per_step.isel(time=slice(None, -1)) - + (state_variable.isel(time=slice(1, None)) - 1) * mega, + duration.isel({duration_dim: slice(1, None)}) + >= duration.isel({duration_dim: slice(None, -1)}) + + duration_per_step.isel({duration_dim: slice(None, -1)}) + + (state_variable.isel({duration_dim: slice(1, None)}) - 1) * mega, name=f'{duration.name}|backward', ) - # Initial condition: duration[0] = (hours_per_step[0] + previous_duration) * state[0] + # Initial condition: duration[0] = (duration_per_step[0] + previous_duration) * state[0] constraints['initial'] = model.add_constraints( - duration.isel(time=0) == (hours_per_step.isel(time=0) + previous_duration) * state_variable.isel(time=0), + duration.isel({duration_dim: 0}) == (duration_per_step.isel({duration_dim: 0}) + previous_duration) * state_variable.isel({duration_dim: 0}), name=f'{duration.name}|initial', ) @@ -300,15 +301,15 @@ def consecutive_duration_tracking( if minimum_duration is not None: constraints['lb'] = model.add_constraints( duration - >= (state_variable.isel(time=slice(None, -1)) - state_variable.isel(time=slice(1, None))) - * minimum_duration.isel(time=slice(None, -1)), + >= (state_variable.isel({duration_dim: slice(None, -1)}) - state_variable.isel({duration_dim: slice(1, None)})) + * minimum_duration.isel({duration_dim: slice(None, -1)}), name=f'{duration.name}|lb', ) # Handle initial condition for minimum duration - if previous_duration > 0 and previous_duration < minimum_duration.isel(time=0).max(): + if previous_duration > 0 and previous_duration < minimum_duration.isel({duration_dim: 0}).max(): constraints['initial_lb'] = model.add_constraints( - state_variable.isel(time=0) == 1, name=f'{duration.name}|initial_lb' + state_variable.isel({duration_dim: 0}) == 1, name=f'{duration.name}|initial_lb' ) variables = {'duration': duration} From d7574d8ad1eb2860fcd3b8d63f5ab6a1926948fd Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 13 Aug 2025 18:56:41 +0200 Subject: [PATCH 307/448] Use new lifetime variable and constraining methods --- flixopt/features.py | 72 +++++++++++++---------------------- flixopt/interface.py | 91 +++++++++++++++++++++++++++++++++----------- 2 files changed, 94 insertions(+), 69 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index ffa70d528..02b4aa5b4 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -157,8 +157,6 @@ def _do_modeling(self): self._constraint_investment() self._constraint_decommissioning() - if self.parameters.duration_in_years is not None: - self._constraint_duration_between_investment_and_decommissioning() def _basic_modeling(self): size_min, size_max = self.parameters.minimum_or_fixed_size, self.parameters.maximum_or_fixed_size @@ -211,6 +209,27 @@ def _basic_modeling(self): short_name='decommissioning_occurs|once', ) + ######################################################################## + previous_lifetime = self.parameters.previous_lifetime if self.parameters.previous_lifetime is not None else 0 + self.add_variables( + lower=0, + upper=self.parameters.maximum_or_fixed_lifetime if self.parameters.maximum_or_fixed_lifetime is not None else self._model.flow_system.years_per_year.sum('year') + previous_lifetime, + coords=self._model.get_coords(['scenario']), + short_name='size|lifetime', + ) + self.add_constraints( + self.lifetime == (self.is_invested * self._model.flow_system.years_per_year).sum('year') + + self.is_invested.isel(year=0) * previous_lifetime, + short_name='size|lifetime', + ) + if self.parameters.minimum_or_fixed_lifetime is not None: + self.add_constraints( + self.lifetime + self.is_invested.isel(year=-1) * self.parameters.minimum_or_fixed_lifetime + >= + self.investment_occurs * self.parameters.minimum_or_fixed_lifetime, + short_name='size|lifetime|lb', + ) + ######################################################################## self.add_variables( coords=self._model.get_coords(['year', 'scenario']), @@ -307,50 +326,6 @@ def _constraint_decommissioning(self): short_name='size|changes|restricted_end', ) - def _constraint_duration_between_investment_and_decommissioning(self): - years = self._model.flow_system.years - years_of_decommissioning = years + self.parameters.duration_in_years - - # Filter and get actual selected years in one step - valid_mask = years_of_decommissioning <= years[-1] - if valid_mask.any(): - valid_years_of_investment = years[valid_mask] - valid_years_of_decommissioning = years_of_decommissioning[valid_mask] - actual_years_of_decommissioning = ( - self.investment_occurs.sel(year=valid_years_of_decommissioning, method='bfill').coords['year'].values - ) - - # Warning for mismatched years - mismatched = valid_years_of_decommissioning != actual_years_of_decommissioning - for inv_year, target_year, actual_year in zip( - valid_years_of_investment[mismatched], - valid_years_of_decommissioning[mismatched], - actual_years_of_decommissioning[mismatched], - strict=False, - ): - logger.warning( - f'year_of_decommissioning {target_year} for {self.size.name} not in flow_system years. For an investment in year {inv_year}, the year_of_decommissioning is set to {actual_year}' - ) - - group = xr.DataArray( - actual_years_of_decommissioning, # values: the actual decommissioning years - coords={'year': valid_years_of_investment}, # coordinates: investment years - dims=['year'], - name='year_of_decommissioning', - ) - - # Now you can use proper xarray groupby - grouped_increases = ( - self.investment_occurs.sel(year=valid_years_of_investment).groupby(group).sum('year_of_decommissioning') - ) - - # Create constraints - self.add_constraints( - self.decommissioning_occurs.sel(year=grouped_increases.coords['year_of_decommissioning']) - == grouped_increases, - short_name='size|changes|fixed_duration', - ) - @property def size(self) -> linopy.Variable: """Investment size variable""" @@ -393,6 +368,11 @@ def divestment_used(self) -> linopy.LinearExpression: """Binary investment decision variable""" return self.decommissioning_occurs.sum('year') + @property + def lifetime(self) -> linopy.Variable: + """Lifetime variable""" + return self._variables['size|lifetime'] + @property def duration(self) -> linopy.Variable: """Investment duration variable""" diff --git a/flixopt/interface.py b/flixopt/interface.py index 92da4bfba..f2ef2ee75 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -358,7 +358,9 @@ def __init__( allow_decommissioning: YearOfInvestmentDataBool = True, force_investment: YearOfInvestmentDataBool = False, force_decommissioning: YearOfInvestmentDataBool = False, - duration_in_years: Optional[Scalar] = None, + fixed_lifetime: Optional[Scalar] = None, + minimum_lifetime: Optional[Scalar] = None, + maximum_lifetime: Optional[Scalar] = None, minimum_size: Optional[YearOfInvestmentData] = None, maximum_size: Optional[YearOfInvestmentData] = None, fixed_size: Optional[YearOfInvestmentData] = None, @@ -366,6 +368,7 @@ def __init__( specific_effects: Optional['NonTemporalEffectsUser'] = None, # costs per Flow-Unit/Storage-Size/... fixed_effects_by_investment_year: Optional[xr.DataArray] = None, specific_effects_by_investment_year: Optional[xr.DataArray] = None, + previous_lifetime: Optional[Scalar] = None, ): """ These parameters are used to include the timing of investments in the model. @@ -380,7 +383,7 @@ def __init__( allow_decommissioning: Allow decommissioning in a certain year. By default, allow it in all years. force_investment: Force the investment to occur in a certain year. force_decommissioning: Force the decommissioning to occur in a certain year. - duration_in_years: Fix the duration between the year of investment and the year ofdecommissioning. + fixed_lifetime: Fix the lifetime of an investment (duration between investment and decommissioning). minimum_size: Minimum possible size of the investment. Can depend on the year of investment. maximum_size: Maximum possible size of the investment. Can depend on the year of investment. fixed_size: Fix the size of the investment. Can depend on the year of investment. Can still be 0 if not forced. @@ -409,7 +412,11 @@ def __init__( self.allow_decommissioning = allow_decommissioning self.force_investment = force_investment self.force_decommissioning = force_decommissioning - self.duration_in_years = duration_in_years + + self.maximum_lifetime = maximum_lifetime + self.minimum_lifetime = minimum_lifetime + self.fixed_lifetime = fixed_lifetime + self.previous_lifetime = previous_lifetime self.fix_effects: 'NonTemporalEffectsUser' = fix_effects if fix_effects is not None else {} self.specific_effects: 'NonTemporalEffectsUser' = specific_effects if specific_effects is not None else {} @@ -430,41 +437,60 @@ def _plausibility_checks(self, flow_system): if (self.force_decommissioning.sum('year') > 1).any(): raise ValueError('force_decommissioning can only be True for a single year.') + if (self.force_investment.sum('year') == 1).any() and (self.force_decommissioning.sum('year') == 1).any(): + year_of_forced_investment = ( + self.force_investment.where(self.force_investment) * self.force_investment.year + ).sum('year') + year_of_forced_decommissioning = ( + self.force_decommissioning.where(self.force_decommissioning) * self.force_decommissioning.year + ).sum('year') + if not (year_of_forced_investment < year_of_forced_decommissioning).all(): + raise ValueError( + f'force_investment needs to be before force_decommissioning. Got:\n' + f'{self.force_investment}\nand\n{self.force_decommissioning}' + ) + + if self.previous_lifetime is not None: + if self.fixed_size is None: + #TODO: Might be only a warning + raise ValueError('previous_lifetime can only be used if fixed_size is defined.') + if self.force_investment is False: + #TODO: Might be only a warning + raise ValueError('previous_lifetime can only be used if force_investment is True.') + + if self.minimum_or_fixed_lifetime is not None and self.maximum_or_fixed_lifetime is not None: + lifetime_range = self.maximum_or_fixed_lifetime - self.minimum_or_fixed_lifetime + safe_lifetime_range = flow_system.years_per_year.max().item() + if (safe_lifetime_range > lifetime_range).any(): + logger.warning( + f'Plausibility Check in {self.__class__.__name__}:\n' + f' The lifetime of the investment is tightly constrainted.' + f' The yearly resolution of the FlowSystem is up to {safe_lifetime_range} years.\n' + f' This can prevent certain years of investment or lead to infeasibilities.\n' + f' Consider using more years in your model (currently: {flow_system.years=})\n' + f' or relax the lifetime limits to span {safe_lifetime_range} years to resolve this issue.' + ) + specify_timing = ( - (self.duration_in_years is not None) + (self.fixed_lifetime is not None) + bool((self.force_investment.sum('year') > 1).any()) + bool((self.force_decommissioning.sum('year') > 1).any()) ) if specify_timing == 0: - # TODO: Should this be an exception or rather a warning? Is there a valid use case for this? - # And a mathematically valid formulation (regarding the effects especially)? - raise ValueError( + # TODO: Is there a valid use case for this? Should this be checked at all? + logger.warning( 'Either the the duration of an investment needs to be set, or the investment or decommissioning ' 'needs to be forced in one year.' ) if specify_timing == 3: - # TODO: Should this be an exception or rather a warning? Is there a valid use case for this? - # And a mathematically valid formulation (regarding the effects especially)? - raise ValueError( + # TODO: Is there a valid use case for this? Should this be checked at all? + logger.warning( 'Either the the duration of an investment needs to be set, or the investment or decommissioning ' 'needs to be forced in one year.' ) - if (self.force_investment.sum('year') >= 1).any() and (self.force_decommissioning.sum('year') >= 1).any(): - year_of_forced_investment = ( - self.force_investment.where(self.force_investment) * self.force_investment.year - ).sum('year') - year_of_forced_decommissioning = ( - self.force_decommissioning.where(self.force_decommissioning) * self.force_decommissioning.year - ).sum('year') - if not (year_of_forced_investment < year_of_forced_decommissioning).all(): - raise ValueError( - f'force_investment needs to be before force_decommissioning. Got:\n' - f'{self.force_investment}\nand\n{self.force_decommissioning}' - ) - def transform_data(self, flow_system: 'FlowSystem', name_prefix: str = '') -> None: """Transform all parameter data to match the flow system's coordinate structure.""" self.fix_effects = flow_system.fit_effects_to_model_coords( @@ -479,6 +505,15 @@ def transform_data(self, flow_system: 'FlowSystem', name_prefix: str = '') -> No label_suffix='specific_effects', dims=['year', 'scenario'], ) + self.maximum_lifetime = flow_system.fit_to_model_coords( + f'{name_prefix}|maximum_lifetime', self.maximum_lifetime, dims=['scenario'] + ) + self.minimum_lifetime = flow_system.fit_to_model_coords( + f'{name_prefix}|minimum_lifetime', self.minimum_lifetime, dims=['scenario'] + ) + self.fixed_lifetime = flow_system.fit_to_model_coords( + f'{name_prefix}|fixed_lifetime', self.fixed_lifetime, dims=['scenario'] + ) self.force_investment = flow_system.fit_to_model_coords( f'{name_prefix}|force_investment', self.force_investment, dims=['year', 'scenario'] @@ -553,6 +588,16 @@ def is_fixed_size(self) -> bool: """Check if investment size is fixed.""" return self.fixed_size is not None + @property + def minimum_or_fixed_lifetime(self) -> NonTemporalData: + """Get the effective minimum lifetime (fixed lifetime takes precedence).""" + return self.fixed_lifetime if self.fixed_lifetime is not None else self.minimum_lifetime + + @property + def maximum_or_fixed_lifetime(self) -> NonTemporalData: + """Get the effective maximum lifetime (fixed lifetime takes precedence).""" + return self.fixed_lifetime if self.fixed_lifetime is not None else self.maximum_lifetime + @register_class_for_io class OnOffParameters(Interface): From 1e619378f941a582ca39ad2b354945580fbbe4f0 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 13 Aug 2025 19:12:24 +0200 Subject: [PATCH 308/448] Improve Plausibility check --- flixopt/interface.py | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/flixopt/interface.py b/flixopt/interface.py index f2ef2ee75..99927e56f 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -452,23 +452,38 @@ def _plausibility_checks(self, flow_system): if self.previous_lifetime is not None: if self.fixed_size is None: - #TODO: Might be only a warning + # TODO: Might be only a warning raise ValueError('previous_lifetime can only be used if fixed_size is defined.') if self.force_investment is False: - #TODO: Might be only a warning + # TODO: Might be only a warning raise ValueError('previous_lifetime can only be used if force_investment is True.') if self.minimum_or_fixed_lifetime is not None and self.maximum_or_fixed_lifetime is not None: - lifetime_range = self.maximum_or_fixed_lifetime - self.minimum_or_fixed_lifetime - safe_lifetime_range = flow_system.years_per_year.max().item() - if (safe_lifetime_range > lifetime_range).any(): + years = flow_system.years.values + + infeasible_years = [] + for i, inv_year in enumerate(years[:-1]): # Exclude last year + future_years = years[i + 1 :] # All years after investment + min_decomm = self.minimum_or_fixed_lifetime + inv_year + max_decomm = self.maximum_or_fixed_lifetime + inv_year + if max_decomm >= years[-1]: + continue + + # Check if any future year falls in decommissioning window + future_years_da = xr.DataArray(future_years, dims=['year']) + valid_decomm = ((min_decomm <= future_years_da) & (future_years_da <= max_decomm)).any('year') + if not valid_decomm.all(): + infeasible_years.append(inv_year) + + if infeasible_years: logger.warning( f'Plausibility Check in {self.__class__.__name__}:\n' - f' The lifetime of the investment is tightly constrainted.' - f' The yearly resolution of the FlowSystem is up to {safe_lifetime_range} years.\n' - f' This can prevent certain years of investment or lead to infeasibilities.\n' - f' Consider using more years in your model (currently: {flow_system.years=})\n' - f' or relax the lifetime limits to span {safe_lifetime_range} years to resolve this issue.' + f' Investment years with no feasible decommissioning: {[int(year) for year in infeasible_years]}\n' + f' Consider relaxing the lifetime constraints or including more years into your model.\n' + f' Lifetime:\n' + f' min={self.minimum_or_fixed_lifetime}\n' + f' max={self.maximum_or_fixed_lifetime}\n' + f' Model years: {list(flow_system.years)}\n' ) specify_timing = ( From da3f29f6ddac4372dbecd475fa44de44580a16a1 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 13 Aug 2025 19:35:33 +0200 Subject: [PATCH 309/448] Improve InvestmentTImingParameters --- flixopt/interface.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/flixopt/interface.py b/flixopt/interface.py index 99927e56f..0ee8d1859 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -356,8 +356,8 @@ def __init__( self, allow_investment: YearOfInvestmentDataBool = True, allow_decommissioning: YearOfInvestmentDataBool = True, - force_investment: YearOfInvestmentDataBool = False, - force_decommissioning: YearOfInvestmentDataBool = False, + force_investment: YearOfInvestmentDataBool = False, # TODO: Allow to simply pass the year + force_decommissioning: YearOfInvestmentDataBool = False, # TODO: Allow to simply pass the year fixed_lifetime: Optional[Scalar] = None, minimum_lifetime: Optional[Scalar] = None, maximum_lifetime: Optional[Scalar] = None, @@ -492,18 +492,11 @@ def _plausibility_checks(self, flow_system): + bool((self.force_decommissioning.sum('year') > 1).any()) ) - if specify_timing == 0: + if specify_timing in (0, 3): # TODO: Is there a valid use case for this? Should this be checked at all? logger.warning( - 'Either the the duration of an investment needs to be set, or the investment or decommissioning ' - 'needs to be forced in one year.' - ) - - if specify_timing == 3: - # TODO: Is there a valid use case for this? Should this be checked at all? - logger.warning( - 'Either the the duration of an investment needs to be set, or the investment or decommissioning ' - 'needs to be forced in one year.' + 'Either the the lifetime of an investment should be fixed, or the investment or decommissioning ' + 'needs to be forced in a certain year.' ) def transform_data(self, flow_system: 'FlowSystem', name_prefix: str = '') -> None: From 331ed06728744ff4063728d8373faed8deb0faad Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 13 Aug 2025 19:59:39 +0200 Subject: [PATCH 310/448] Improve weights --- flixopt/flow_system.py | 2 +- flixopt/structure.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 0acee1d55..7177cbe62 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -76,7 +76,7 @@ def __init__( If None, the first time increment of time_series is used. This is needed to calculate previous durations (for example consecutive_on_hours). If you use an array, take care that its long enough to cover all previous values! - weights: The weights of each year and scenario. If None, all have the same weight (normalized to 1). Its recommended to scale the weights to sum up to 1. + weights: The weights of each year and scenario. If None, all scenarios have the same weight, while the years have the weight of their represented year (all normalized to 1). Its recommended to scale the weights to sum up to 1. """ self.timesteps = self._validate_timesteps(timesteps) self.timesteps_extra = self._create_timesteps_with_extra(timesteps, hours_of_last_timestep) diff --git a/flixopt/structure.py b/flixopt/structure.py index dec476ac7..413c6d513 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -178,7 +178,9 @@ def get_coords( def weights(self) -> Union[int, xr.DataArray]: """Returns the scenario weights of the FlowSystem. If None, return weights that are normalized to 1 (one)""" if self.flow_system.weights is None: - weights = self.flow_system.fit_to_model_coords('weights', 1, dims=['year', 'scenario']) + weights = self.flow_system.fit_to_model_coords( + 'weights', self.flow_system.years_per_year, dims=['year', 'scenario'] + ) return weights / weights.sum() From 16c74812346e3bf13ebddd4ea9d4239887fc0ff3 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 13 Aug 2025 19:59:48 +0200 Subject: [PATCH 311/448] Adjust test --- tests/test_models.py | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/tests/test_models.py b/tests/test_models.py index 9e2cce36a..7545371b5 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -93,7 +93,7 @@ def create_annualized_effects( @pytest.fixture def flow_system() -> fx.FlowSystem: """Create basic elements for component testing with coordinate parametrization.""" - years = pd.Index([2020, 2021, 2022, 2023, 2024, 2030], name='year') + years = pd.Index([2020, 2021, 2022, 2023, 2024, 2026, 2028, 2030], name='year') timesteps = pd.date_range('2020-01-01', periods=24, freq='h', name='time') flow_system = fx.FlowSystem(timesteps=timesteps, years=years) @@ -106,6 +106,7 @@ def flow_system() -> fx.FlowSystem: electricity_sink = Sinks.electricity_feed_in(p_el) flow_system.add_elements(*Buses.defaults()) + flow_system.buses['Fernwärme'].excess_penalty_per_flow_hour = 0 flow_system.add_elements(costs, heat_load, gas_source, electricity_sink) return flow_system @@ -187,29 +188,27 @@ class TestYearAwareInvestmentModelDirect: def test_flow_invest_new(self, flow_system): da = xr.DataArray( - [25, 30, 35, 40, 45, 50], + [10] * 8, coords=(flow_system.years_of_investment,), ).expand_dims(year=flow_system.years) - da = da.where(da.year >= da.year_of_investment).fillna(0) + da = da.where(da.year == da.year_of_investment).fillna(0) flow = fx.Flow( 'Wärme', bus='Fernwärme', size=fx.InvestTimingParameters( - force_investment=xr.DataArray( - [False if year != 2021 else True for year in flow_system.years], coords=(flow_system.years,) - ), # year_of_decommissioning=2030, - duration_in_years=2, - minimum_size=900, - maximum_size=1000, + minimum_lifetime=2, + maximum_lifetime=3, + minimum_size=9, + maximum_size=10, specific_effects=xr.DataArray( - [25, 30, 35, 40, 45, 50], + [25, 30, 35, 40, 45, 50, 55, 60], coords=(flow_system.years,), ) - * 0, + * -0, # fix_effects=-2e3, - specific_effects_by_investment_year=da, + specific_effects_by_investment_year=-1 * da, ), relative_maximum=np.linspace(0.5, 1, flow_system.timesteps.size), ) @@ -217,12 +216,7 @@ def test_flow_invest_new(self, flow_system): flow_system.add_elements(fx.Source('Source', source=flow)) calculation = fx.FullCalculation('GenericName', flow_system) calculation.do_modeling() - # calculation.model.add_constraints(calculation.model['Source(Wärme)|decrease'].isel(year=2) == 1) - calculation.solve(fx.solvers.GurobiSolver(0, 60)) - - calculation = fx.FullCalculation('GenericName', flow_system.sel(year=[2022, 2030])) - calculation.do_modeling() - # calculation.model.add_constraints(calculation.model['Source(Wärme)|decrease'].isel(year=2) == 1) + # calculation.model.add_constraints(calculation.model['Source(Wärme)|is_invested'].sel(year=2022) == 1) calculation.solve(fx.solvers.GurobiSolver(0, 60)) ds = calculation.results['Source'].solution From e9e5e049b79c98defd5053cebb581ff7f36a72a3 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 13 Aug 2025 20:04:18 +0200 Subject: [PATCH 312/448] Remove old classes --- flixopt/interface.py | 94 +------------------------------------------- 1 file changed, 1 insertion(+), 93 deletions(-) diff --git a/flixopt/interface.py b/flixopt/interface.py index 0ee8d1859..736e82f08 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -245,98 +245,6 @@ def maximum_or_fixed_size(self) -> NonTemporalData: return self.fixed_size if self.fixed_size is not None else self.maximum_size -# Base interface for common parameters -@register_class_for_io -class _BaseYearAwareInvestParameters(Interface): - """ - Base parameters for year-aware investment modeling. - Contains common sizing and effects parameters used by all variants. - """ - - def __init__( - self, - # Basic sizing parameters - minimum_size: Optional[Scalar] = None, - maximum_size: Optional[Scalar] = None, - fixed_size: Optional[Scalar] = None, - optional: bool = False, - # Direct effects - effects_of_investment_per_size: Optional['NonTemporalEffectsUser'] = None, - effects_of_investment: Optional['NonTemporalEffectsUser'] = None, - ): - """ - Initialize base year-aware investment parameters. - - Args: - minimum_size: Minimum investment size when invested. Defaults to CONFIG.modeling.EPSILON. - maximum_size: Maximum possible investment size. Defaults to CONFIG.modeling.BIG. - fixed_size: If specified, investment size is fixed to this value. - effects_of_investment_per_size: Effects applied per unit of investment size for each year invested. - Example: {'costs': 100} applies 100 * size * years_invested to total costs. - effects_of_investment: One-time effects applied when investment decision is made. - Example: {'costs': 1000} applies 1000 to costs in the investment year. - """ - self.minimum_size = minimum_size if minimum_size is not None else CONFIG.modeling.EPSILON - self.maximum_size = maximum_size if maximum_size is not None else CONFIG.modeling.BIG - self.fixed_size = fixed_size - self.optional = optional - - self.effects_of_investment_per_size: 'NonTemporalEffectsUser' = ( - effects_of_investment_per_size if effects_of_investment_per_size is not None else {} - ) - self.effects_of_investment: 'NonTemporalEffectsUser' = ( - effects_of_investment if effects_of_investment is not None else {} - ) - - def transform_data(self, flow_system: 'FlowSystem', name_prefix: str = '') -> None: - """Transform all parameter data to match the flow system's coordinate structure.""" - self._plausibility_checks(flow_system) - - self.effects_of_investment_per_size = flow_system.fit_effects_to_model_coords( - label_prefix=name_prefix, - effect_values=self.effects_of_investment_per_size, - label_suffix='effects_of_investment_per_size', - dims=['year', 'scenario'], - ) - self.effects_of_investment = flow_system.fit_effects_to_model_coords( - label_prefix=name_prefix, - effect_values=self.effects_of_investment, - label_suffix='effects_of_investment', - dims=['year', 'scenario'], - ) - - self.minimum_size = flow_system.fit_to_model_coords( - f'{name_prefix}|minimum_size', self.minimum_size, dims=['year', 'scenario'] - ) - self.maximum_size = flow_system.fit_to_model_coords( - f'{name_prefix}|maximum_size', self.maximum_size, dims=['year', 'scenario'] - ) - if self.fixed_size is not None: - self.fixed_size = flow_system.fit_to_model_coords( - f'{name_prefix}|fixed_size', self.fixed_size, dims=['year', 'scenario'] - ) - - def _plausibility_checks(self, flow_system): - """Validate parameter consistency and compatibility with the flow system.""" - if flow_system.years is None: - raise ValueError("YearAwareInvestParameters requires the flow_system to have a 'years' dimension.") - - @property - def minimum_or_fixed_size(self) -> NonTemporalData: - """Get the effective minimum size (fixed size takes precedence).""" - return self.fixed_size if self.fixed_size is not None else self.minimum_size - - @property - def maximum_or_fixed_size(self) -> NonTemporalData: - """Get the effective maximum size (fixed size takes precedence).""" - return self.fixed_size if self.fixed_size is not None else self.maximum_size - - @property - def is_fixed_size(self) -> bool: - """Check if investment size is fixed.""" - return self.fixed_size is not None - - YearOfInvestmentData = NonTemporalDataUser """This datatype is used to define things related to the year of investment.""" YearOfInvestmentDataBool = Union[bool, YearOfInvestmentData] @@ -430,7 +338,7 @@ def __init__( def _plausibility_checks(self, flow_system): """Validate parameter consistency.""" if flow_system.years is None: - raise ValueError("YearAwareInvestParameters requires the flow_system to have a 'years' dimension.") + raise ValueError("InvestTimingParameters requires the flow_system to have a 'years' dimension.") if (self.force_investment.sum('year') > 1).any(): raise ValueError('force_investment can only be True for a single year.') From 1474af6ecd88aa987bdff6ed35afae35ee6ca590 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 13 Aug 2025 20:10:19 +0200 Subject: [PATCH 313/448] V3.0.0/main fit to model coords improve (#295) * Change fit_to_model_coords to work with a Collection of dims * Improve fit_to_model_coords --- flixopt/components.py | 12 ++++++------ flixopt/effects.py | 17 ++++++++++------- flixopt/elements.py | 10 +++++----- flixopt/flow_system.py | 27 +++++++++++++++++++++------ flixopt/interface.py | 23 ++++++++++++----------- flixopt/structure.py | 2 +- 6 files changed, 55 insertions(+), 36 deletions(-) diff --git a/flixopt/components.py b/flixopt/components.py index d483ee28c..933ef1791 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -223,29 +223,29 @@ def transform_data(self, flow_system: 'FlowSystem') -> None: ) if not isinstance(self.initial_charge_state, str): self.initial_charge_state = flow_system.fit_to_model_coords( - f'{self.label_full}|initial_charge_state', self.initial_charge_state, has_time_dim=False + f'{self.label_full}|initial_charge_state', self.initial_charge_state, dims=['year', 'scenario'] ) self.minimal_final_charge_state = flow_system.fit_to_model_coords( - f'{self.label_full}|minimal_final_charge_state', self.minimal_final_charge_state, has_time_dim=False + f'{self.label_full}|minimal_final_charge_state', self.minimal_final_charge_state, dims=['year', 'scenario'] ) self.maximal_final_charge_state = flow_system.fit_to_model_coords( - f'{self.label_full}|maximal_final_charge_state', self.maximal_final_charge_state, has_time_dim=False + f'{self.label_full}|maximal_final_charge_state', self.maximal_final_charge_state, dims=['year', 'scenario'] ) self.relative_minimum_final_charge_state = flow_system.fit_to_model_coords( f'{self.label_full}|relative_minimum_final_charge_state', self.relative_minimum_final_charge_state, - has_time_dim=False, + dims=['year', 'scenario'], ) self.relative_maximum_final_charge_state = flow_system.fit_to_model_coords( f'{self.label_full}|relative_maximum_final_charge_state', self.relative_maximum_final_charge_state, - has_time_dim=False, + dims=['year', 'scenario'], ) if isinstance(self.capacity_in_flow_hours, InvestParameters): self.capacity_in_flow_hours.transform_data(flow_system, f'{self.label_full}|InvestParameters') else: self.capacity_in_flow_hours = flow_system.fit_to_model_coords( - f'{self.label_full}|capacity_in_flow_hours', self.capacity_in_flow_hours, has_time_dim=False + f'{self.label_full}|capacity_in_flow_hours', self.capacity_in_flow_hours, dims=['year', 'scenario'] ) def _plausibility_checks(self) -> None: diff --git a/flixopt/effects.py b/flixopt/effects.py index 79a44e67a..d0b552bf8 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -105,27 +105,30 @@ def transform_data(self, flow_system: 'FlowSystem'): ) self.minimum_operation = flow_system.fit_to_model_coords( - f'{self.label_full}|minimum_operation', self.minimum_operation, has_time_dim=False + f'{self.label_full}|minimum_operation', self.minimum_operation, dims=['year', 'scenario'] ) self.maximum_operation = flow_system.fit_to_model_coords( - f'{self.label_full}|maximum_operation', self.maximum_operation, has_time_dim=False + f'{self.label_full}|maximum_operation', self.maximum_operation, dims=['year', 'scenario'] ) self.minimum_invest = flow_system.fit_to_model_coords( - f'{self.label_full}|minimum_invest', self.minimum_invest, has_time_dim=False + f'{self.label_full}|minimum_invest', self.minimum_invest, dims=['year', 'scenario'] ) self.maximum_invest = flow_system.fit_to_model_coords( - f'{self.label_full}|maximum_invest', self.maximum_invest, has_time_dim=False + f'{self.label_full}|maximum_invest', self.maximum_invest, dims=['year', 'scenario'] ) self.minimum_total = flow_system.fit_to_model_coords( f'{self.label_full}|minimum_total', self.minimum_total, - has_time_dim=False, + dims=['year', 'scenario'], ) self.maximum_total = flow_system.fit_to_model_coords( - f'{self.label_full}|maximum_total', self.maximum_total, has_time_dim=False + f'{self.label_full}|maximum_total', self.maximum_total, dims=['year', 'scenario'] ) self.specific_share_to_other_effects_invest = flow_system.fit_effects_to_model_coords( - f'{self.label_full}|invest->', self.specific_share_to_other_effects_invest, 'invest', has_time_dim=False + f'{self.label_full}|invest->', + self.specific_share_to_other_effects_invest, + 'invest', + dims=['year', 'scenario'], ) def create_model(self, model: FlowSystemModel) -> 'EffectModel': diff --git a/flixopt/elements.py b/flixopt/elements.py index e1c0fcbc3..160dac660 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -252,16 +252,16 @@ def transform_data(self, flow_system: 'FlowSystem'): self.label_full, self.effects_per_flow_hour, 'per_flow_hour' ) self.flow_hours_total_max = flow_system.fit_to_model_coords( - f'{self.label_full}|flow_hours_total_max', self.flow_hours_total_max, has_time_dim=False + f'{self.label_full}|flow_hours_total_max', self.flow_hours_total_max, dims=['year', 'scenario'] ) self.flow_hours_total_min = flow_system.fit_to_model_coords( - f'{self.label_full}|flow_hours_total_min', self.flow_hours_total_min, has_time_dim=False + f'{self.label_full}|flow_hours_total_min', self.flow_hours_total_min, dims=['year', 'scenario'] ) self.load_factor_max = flow_system.fit_to_model_coords( - f'{self.label_full}|load_factor_max', self.load_factor_max, has_time_dim=False + f'{self.label_full}|load_factor_max', self.load_factor_max, dims=['year', 'scenario'] ) self.load_factor_min = flow_system.fit_to_model_coords( - f'{self.label_full}|load_factor_min', self.load_factor_min, has_time_dim=False + f'{self.label_full}|load_factor_min', self.load_factor_min, dims=['year', 'scenario'] ) if self.on_off_parameters is not None: @@ -269,7 +269,7 @@ def transform_data(self, flow_system: 'FlowSystem'): if isinstance(self.size, InvestParameters): self.size.transform_data(flow_system, self.label_full) else: - self.size = flow_system.fit_to_model_coords(f'{self.label_full}|size', self.size, has_time_dim=False) + self.size = flow_system.fit_to_model_coords(f'{self.label_full}|size', self.size, dims=['year', 'scenario']) def _plausibility_checks(self) -> None: # TODO: Incorporate into Variable? (Lower_bound can not be greater than upper bound diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index dd202114e..e8ddc22b9 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -7,7 +7,7 @@ import pathlib import warnings from io import StringIO -from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Tuple, Union +from typing import TYPE_CHECKING, Any, Collection, Dict, List, Literal, Optional, Tuple, Union import numpy as np import pandas as pd @@ -349,6 +349,7 @@ def fit_to_model_coords( name: str, data: Optional[Union[TemporalDataUser, NonTemporalDataUser]], has_time_dim: bool = True, + dims: Optional[Collection[FlowSystemDimensions]] = None, ) -> Optional[Union[TemporalData, NonTemporalData]]: """ Fit data to model coordinate system (currently time, but extensible). @@ -357,6 +358,7 @@ def fit_to_model_coords( name: Name of the data data: Data to fit to model coordinates has_time_dim: Wether to use the time dimension or not + dims: Collection of dimension names to use for fitting. If None, all dimensions are used. Returns: xr.DataArray aligned to model coordinate system. If data is None, returns None. @@ -364,10 +366,19 @@ def fit_to_model_coords( if data is None: return None - coords = self.coords + if dims is None: + coords = self.coords - if not has_time_dim: - coords.pop('time') + if not has_time_dim: + warnings.warn( + 'has_time_dim is deprecated. Please pass dims to fit_to_model_coords instead.', + DeprecationWarning, + stacklevel=2, + ) + coords.pop('time') + else: + coords = self.coords + coords = {k: coords[k] for k in dims if k in coords} # Rest of your method stays the same, just pass coords if isinstance(data, TimeSeriesData): @@ -390,6 +401,7 @@ def fit_effects_to_model_coords( effect_values: Optional[Union[TemporalEffectsUser, NonTemporalEffectsUser]], label_suffix: Optional[str] = None, has_time_dim: bool = True, + dims: Optional[Collection[FlowSystemDimensions]] = None, ) -> Optional[Union[TemporalEffects, NonTemporalEffects]]: """ Transform EffectValues from the user to Internal Datatypes aligned with model coordinates. @@ -401,7 +413,10 @@ def fit_effects_to_model_coords( return { effect: self.fit_to_model_coords( - '|'.join(filter(None, [label_prefix, effect, label_suffix])), value, has_time_dim=has_time_dim + '|'.join(filter(None, [label_prefix, effect, label_suffix])), + value, + has_time_dim=has_time_dim, + dims=dims, ) for effect, value in effect_values_dict.items() } @@ -412,7 +427,7 @@ def connect_and_transform(self): logger.debug('FlowSystem already connected and transformed') return - self.weights = self.fit_to_model_coords('weights', self.weights, has_time_dim=False) + self.weights = self.fit_to_model_coords('weights', self.weights, dims=['year', 'scenario']) if self.weights is not None and self.weights.sum() != 1: logger.warning( f'Scenario weights are not normalized to 1. This is recomended for a better scaled model. ' diff --git a/flixopt/interface.py b/flixopt/interface.py index cae1757c7..7cb9604ac 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -33,8 +33,9 @@ def __init__(self, start: TemporalDataUser, end: TemporalDataUser): self.has_time_dim = False def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): - self.start = flow_system.fit_to_model_coords(f'{name_prefix}|start', self.start, has_time_dim=self.has_time_dim) - self.end = flow_system.fit_to_model_coords(f'{name_prefix}|end', self.end, has_time_dim=self.has_time_dim) + dims = None if self.has_time_dim else ['year', 'scenario'] + self.start = flow_system.fit_to_model_coords(f'{name_prefix}|start', self.start, dims=dims) + self.end = flow_system.fit_to_model_coords(f'{name_prefix}|end', self.end, dims=dims) @register_class_for_io @@ -189,33 +190,33 @@ def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): label_prefix=name_prefix, effect_values=self.fix_effects, label_suffix='fix_effects', - has_time_dim=False, + dims=['year', 'scenario'], ) self.divest_effects = flow_system.fit_effects_to_model_coords( label_prefix=name_prefix, effect_values=self.divest_effects, label_suffix='divest_effects', - has_time_dim=False, + dims=['year', 'scenario'], ) self.specific_effects = flow_system.fit_effects_to_model_coords( label_prefix=name_prefix, effect_values=self.specific_effects, label_suffix='specific_effects', - has_time_dim=False, + dims=['year', 'scenario'], ) if self.piecewise_effects is not None: self.piecewise_effects.has_time_dim = False self.piecewise_effects.transform_data(flow_system, f'{name_prefix}|PiecewiseEffects') self.minimum_size = flow_system.fit_to_model_coords( - f'{name_prefix}|minimum_size', self.minimum_size, has_time_dim=False + f'{name_prefix}|minimum_size', self.minimum_size, dims=['year', 'scenario'] ) self.maximum_size = flow_system.fit_to_model_coords( - f'{name_prefix}|maximum_size', self.maximum_size, has_time_dim=False + f'{name_prefix}|maximum_size', self.maximum_size, dims=['year', 'scenario'] ) if self.fixed_size is not None: self.fixed_size = flow_system.fit_to_model_coords( - f'{name_prefix}|fixed_size', self.fixed_size, has_time_dim=False + f'{name_prefix}|fixed_size', self.fixed_size, dims=['year', 'scenario'] ) def _plausibility_checks(self, flow_system): @@ -312,13 +313,13 @@ def transform_data(self, flow_system: 'FlowSystem', name_prefix: str): f'{name_prefix}|consecutive_off_hours_max', self.consecutive_off_hours_max ) self.on_hours_total_max = flow_system.fit_to_model_coords( - f'{name_prefix}|on_hours_total_max', self.on_hours_total_max, has_time_dim=False + f'{name_prefix}|on_hours_total_max', self.on_hours_total_max, dims=['year', 'scenario'] ) self.on_hours_total_min = flow_system.fit_to_model_coords( - f'{name_prefix}|on_hours_total_min', self.on_hours_total_min, has_time_dim=False + f'{name_prefix}|on_hours_total_min', self.on_hours_total_min, dims=['year', 'scenario'] ) self.switch_on_total_max = flow_system.fit_to_model_coords( - f'{name_prefix}|switch_on_total_max', self.switch_on_total_max, has_time_dim=False + f'{name_prefix}|switch_on_total_max', self.switch_on_total_max, dims=['year', 'scenario'] ) @property diff --git a/flixopt/structure.py b/flixopt/structure.py index da67f9620..24934547b 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -178,7 +178,7 @@ def get_coords( def weights(self) -> Union[int, xr.DataArray]: """Returns the scenario weights of the FlowSystem. If None, return weights that are normalized to 1 (one)""" if self.flow_system.weights is None: - weights = self.flow_system.fit_to_model_coords('weights', 1, has_time_dim=False) + weights = self.flow_system.fit_to_model_coords('weights', 1, dims=['year', 'scenario']) return weights / weights.sum() From 3f03d22985578eb0774bef5efb7e86eb004a15fd Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 2 Sep 2025 10:20:18 +0200 Subject: [PATCH 314/448] ruff format --- flixopt/features.py | 10 ++++++---- flixopt/modeling.py | 8 ++++++-- flixopt/structure.py | 2 +- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index 02b4aa5b4..d2af15aff 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -213,20 +213,22 @@ def _basic_modeling(self): previous_lifetime = self.parameters.previous_lifetime if self.parameters.previous_lifetime is not None else 0 self.add_variables( lower=0, - upper=self.parameters.maximum_or_fixed_lifetime if self.parameters.maximum_or_fixed_lifetime is not None else self._model.flow_system.years_per_year.sum('year') + previous_lifetime, + upper=self.parameters.maximum_or_fixed_lifetime + if self.parameters.maximum_or_fixed_lifetime is not None + else self._model.flow_system.years_per_year.sum('year') + previous_lifetime, coords=self._model.get_coords(['scenario']), short_name='size|lifetime', ) self.add_constraints( - self.lifetime == (self.is_invested * self._model.flow_system.years_per_year).sum('year') + self.lifetime + == (self.is_invested * self._model.flow_system.years_per_year).sum('year') + self.is_invested.isel(year=0) * previous_lifetime, short_name='size|lifetime', ) if self.parameters.minimum_or_fixed_lifetime is not None: self.add_constraints( self.lifetime + self.is_invested.isel(year=-1) * self.parameters.minimum_or_fixed_lifetime - >= - self.investment_occurs * self.parameters.minimum_or_fixed_lifetime, + >= self.investment_occurs * self.parameters.minimum_or_fixed_lifetime, short_name='size|lifetime|lb', ) diff --git a/flixopt/modeling.py b/flixopt/modeling.py index a630ac33e..c5205b07f 100644 --- a/flixopt/modeling.py +++ b/flixopt/modeling.py @@ -293,7 +293,8 @@ def consecutive_duration_tracking( # Initial condition: duration[0] = (duration_per_step[0] + previous_duration) * state[0] constraints['initial'] = model.add_constraints( - duration.isel({duration_dim: 0}) == (duration_per_step.isel({duration_dim: 0}) + previous_duration) * state_variable.isel({duration_dim: 0}), + duration.isel({duration_dim: 0}) + == (duration_per_step.isel({duration_dim: 0}) + previous_duration) * state_variable.isel({duration_dim: 0}), name=f'{duration.name}|initial', ) @@ -301,7 +302,10 @@ def consecutive_duration_tracking( if minimum_duration is not None: constraints['lb'] = model.add_constraints( duration - >= (state_variable.isel({duration_dim: slice(None, -1)}) - state_variable.isel({duration_dim: slice(1, None)})) + >= ( + state_variable.isel({duration_dim: slice(None, -1)}) + - state_variable.isel({duration_dim: slice(1, None)}) + ) * minimum_duration.isel({duration_dim: slice(None, -1)}), name=f'{duration.name}|lb', ) diff --git a/flixopt/structure.py b/flixopt/structure.py index 6371d634c..9fdc9206f 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -181,7 +181,7 @@ def weights(self) -> Union[int, xr.DataArray]: weights = self.flow_system.fit_to_model_coords( 'weights', 1 if self.flow_system.years is None else self.flow_system.years_per_year, - dims=['year', 'scenario'] + dims=['year', 'scenario'], ) return weights / weights.sum() From 42e4f59878a81ca27034bfa845c2f9c27229548a Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 23 Sep 2025 11:37:58 +0200 Subject: [PATCH 315/448] Revert changes --- flixopt/components.py | 6 +++--- flixopt/effects.py | 2 +- flixopt/elements.py | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/flixopt/components.py b/flixopt/components.py index 14274a191..98624cfe7 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -204,7 +204,7 @@ def _plausibility_checks(self) -> None: f'(in {self.label_full}) and variable size is uncommon. Please check if this is intended!' ) - def transform_data(self, flow_system: FlowSystem) -> None: + def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None: super().transform_data(flow_system) if self.conversion_factors: self.conversion_factors = self._transform_conversion_factors(flow_system) @@ -420,7 +420,7 @@ def create_model(self, model: FlowSystemModel) -> StorageModel: self.submodel = StorageModel(model, self) return self.submodel - def transform_data(self, flow_system: FlowSystem) -> None: + def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None: super().transform_data(flow_system) self.relative_minimum_charge_state = flow_system.fit_to_model_coords( f'{self.label_full}|relative_minimum_charge_state', @@ -691,7 +691,7 @@ def create_model(self, model) -> TransmissionModel: self.submodel = TransmissionModel(model, self) return self.submodel - def transform_data(self, flow_system: FlowSystem) -> None: + def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None: super().transform_data(flow_system) self.relative_losses = flow_system.fit_to_model_coords( f'{self.label_full}|relative_losses', self.relative_losses diff --git a/flixopt/effects.py b/flixopt/effects.py index a7920b1ec..3390bd463 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -174,7 +174,7 @@ def __init__( self.minimum_total = minimum_total self.maximum_total = maximum_total - def transform_data(self, flow_system: FlowSystem): + def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None: self.minimum_operation_per_hour = flow_system.fit_to_model_coords( f'{self.label_full}|minimum_operation_per_hour', self.minimum_operation_per_hour ) diff --git a/flixopt/elements.py b/flixopt/elements.py index ee6005d36..89be7e3d9 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -97,7 +97,7 @@ def create_model(self, model: FlowSystemModel) -> ComponentModel: self.submodel = ComponentModel(model, self) return self.submodel - def transform_data(self, flow_system: FlowSystem) -> None: + def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None: if self.on_off_parameters is not None: self.on_off_parameters.transform_data(flow_system, self.label_full) @@ -189,7 +189,7 @@ def create_model(self, model: FlowSystemModel) -> BusModel: self.submodel = BusModel(model, self) return self.submodel - def transform_data(self, flow_system: FlowSystem): + def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None: self.excess_penalty_per_flow_hour = flow_system.fit_to_model_coords( f'{self.label_full}|excess_penalty_per_flow_hour', self.excess_penalty_per_flow_hour ) @@ -417,7 +417,7 @@ def create_model(self, model: FlowSystemModel) -> FlowModel: self.submodel = FlowModel(model, self) return self.submodel - def transform_data(self, flow_system: FlowSystem): + def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None: self.relative_minimum = flow_system.fit_to_model_coords( f'{self.label_full}|relative_minimum', self.relative_minimum ) From e1dcfb8b34dfc3ee6157bc537e3f66ac06fb622c Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 23 Sep 2025 11:39:23 +0200 Subject: [PATCH 316/448] Update type hints --- tests/test_models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_models.py b/tests/test_models.py index 7545371b5..3fc38c1f1 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -43,8 +43,8 @@ def calculate_annual_payment(total_cost: float, remaining_years: int, discount_r def create_annualized_effects( - year_of_investments: Union[range, list, pd.Index], - all_years: Union[range, list, pd.Index], + year_of_investments: range | list | pd.Index, + all_years: range | list | pd.Index, total_cost: float, discount_rate: float, horizon_end: int, From d1a6cae63a986c0fca3df6dbc206cdbff70fa4f8 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 24 Sep 2025 12:50:56 +0200 Subject: [PATCH 317/448] Increase time delay of pypi install retrys in CI-CD --- .github/workflows/python-app.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml index 20011db36..88163eea8 100644 --- a/.github/workflows/python-app.yaml +++ b/.github/workflows/python-app.yaml @@ -313,7 +313,7 @@ jobs: # Wait and retry while PyPI indexes the package INSTALL_SUCCESS=false - for d in 10 20 40 80 120; do + for d in 10 20 40 80 120 240 480; do sleep "$d" echo "Attempting to install $PACKAGE_NAME==$VERSION from PyPI (retry after ${d}s)..." From 0552d8a23ad3a9bee026659a587100db9c5b65a0 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 25 Sep 2025 23:24:09 +0200 Subject: [PATCH 318/448] Feature/changelog (#353) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Improved Changelog to use emojis * Improved Changelog to use emojis * Move to gitmoji * Add Dependency section * Reorder headings * - Removed bold formatting from all version headings (following Keep a Changelog style) - Added backticks around networkx package name for better readability - Capitalized "Dash" as a proper noun in the network visualization section - Fixed grammar: added comma in "Sink, Source, and SourceAndSink" and improved sentence structure - Fixed typos: "where" → "were" and "lead" → "led" in the storage losses section * Fix CHANGELOG.md * Add changelog extraction for docs * Add gitmoji reference to CHANGELOG.md * Update extract-changelog.py * Update extract-changelog.py * Add navigation links to extract-changelog.py * Update CI * Update extract_release_notes.py * Revert "Update extract_release_notes.py" This reverts commit 2beaee4bfba6822aa857f04cfef6b8f0d3604e6d. * Make CHANGELOG.md headers not bold * Update extract-changelog.py * More updates to CHANGELOG.md * Rename script file * Fix SUMMARY.md * Fix comments in extract_changelog.py * Add GitHub tag link only for valid PEP 440 versions (skip e.g. "Unreleased") * ⏺ Fixed the typos and capitalization issues in CHANGELOG.md: - "Formating" → "Formatting" - "PR's" → "PRs" - "contributers" → "contributors" - "Github" → "GitHub" * Add a template section * Revert a chnage in SUMMARY.md * Add release notes --- .github/workflows/python-app.yaml | 11 +- CHANGELOG.md | 222 ++++++++++++++++++++---------- docs/SUMMARY.md | 2 +- scripts/extract_changelog.py | 148 ++++++++++++++++++++ 4 files changed, 309 insertions(+), 74 deletions(-) create mode 100644 scripts/extract_changelog.py diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml index 88163eea8..e44cf7fd7 100644 --- a/.github/workflows/python-app.yaml +++ b/.github/workflows/python-app.yaml @@ -363,10 +363,15 @@ jobs: with: python-version: ${{ env.PYTHON_VERSION }} - - name: Sync changelog to docs + - name: Extract changelog to docs run: | - cp CHANGELOG.md docs/changelog.md - echo "✅ Synced changelog to docs" + # Install packaging dependency for changelog extraction + uv pip install --system packaging + + # Extract individual release files + python scripts/extract_changelog.py + + echo "✅ Extracted changelog to docs/changelog/" - name: Install documentation dependencies run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a08bbf8b..46ab6270c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,50 +1,92 @@ # Changelog -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +Formatting is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) & [Gitmoji](https://gitmoji.dev). +For more details regarding the individual PRs and contributors, please refer to our [GitHub releases](https://github.com/flixOpt/flixopt/releases). +--- ## [2.1.9] - 2025-09-23 -Small Bugfix which was supposed to be fixed in 2.1.8 -### Fixed -- Fix error handling in network visualization if networkx is not installed. +**Summary:** Small bugfix release addressing network visualization error handling. + +### 🐛 Fixed +- Fix error handling in network visualization if `networkx` is not installed +--- ## [2.1.8] - 2025-09-22 -This release focuses on code quality improvements, enhanced documentation, and bug fixes for heat pump components and visualization features. -### Added +**Summary:** Code quality improvements, enhanced documentation, and bug fixes for heat pump components and visualization features. + +### ✨ Added - Extra Check for HeatPumpWithSource.COP to be strictly > 1 to avoid division by zero - Apply deterministic color assignment by using sorted() in `plotting.py` - Add missing args in docstrings in `plotting.py`, `solvers.py`, and `core.py`. -### Changed +### ♻️ Changed - Greatly improved docstrings and documentation of all public classes - Make path handling to be gentle about missing .html suffix in `plotting.py` - Default for `relative_losses` in `Transmission` is now 0 instead of None @@ -52,27 +94,34 @@ This release focuses on code quality improvements, enhanced documentation, and b - Fix some docstrings in plotting.py - Change assertions to raise Exceptions in `plotting.py` -### Fixed -- Fix color scheme selection in network_app; color pickers now update when a scheme is selected. -- Fix error handling in network visualization if networkx is not installed. -- Fix broken links in docs. -- Fix COP getter and setter of `HeatPumpWithSource` returning and setting wrong conversion factors. +### 🐛 Fixed + +**Core Components:** +- Fix COP getter and setter of `HeatPumpWithSource` returning and setting wrong conversion factors - Fix custom compression levels in `io.save_dataset_to_netcdf` -- Fix `total_max` did not work when total min was not used. +- Fix `total_max` did not work when total min was not used + +**Visualization:** +- Fix color scheme selection in network_app; color pickers now update when a scheme is selected + +### 📝 Docs +- Fix broken links in docs +- Fix some docstrings in plotting.py -### *Development* +### 👷 Development - Pin dev dependencies to specific versions - Improve CI workflows to run faster and smarter +--- + ## [2.1.7] - 2025-09-13 -This update is a maintenance release to improve Code Quality, CI and update the dependencies. -There are no changes or new features. +**Summary:** Maintenance release to improve Code Quality, CI and update the dependencies. There are no changes or new features. -### Added -- Added __version__ to flixopt +### ✨ Added +- Added `__version__` to flixopt -### *Development* +### 👷 Development - ruff format the whole Codebase - Added renovate config - Added pre-commit @@ -81,108 +130,141 @@ There are no changes or new features. - Updated Dependencies - Updated Issue Templates +--- ## [2.1.6] - 2025-09-02 -### Changed -- `Sink`, `Source` and `SourceAndSink` now accept multiple `flows` as `inputs` and `outputs` instead of just one. This enables to model more use cases using these classes. [[#291](https://github.com/flixOpt/flixopt/pull/291) by [@FBumann](https://github.com/FBumann)] -- Further, both `Sink` and `Source` now have a `prevent_simultaneous_flow_rates` argument to prevent simultaneous flow rates of more than one of their Flows. [[#291](https://github.com/flixOpt/flixopt/pull/291) by [@FBumann](https://github.com/FBumann)] +**Summary:** Enhanced Sink/Source components with multi-flow support and new interactive network visualization. + +### ✨ Added +- **Network Visualization**: Added `FlowSystem.start_network_app()` and `FlowSystem.stop_network_app()` to easily visualize the network structure of a flow system in an interactive Dash web app + - *Note: This is still experimental and might change in the future* + +### ♻️ Changed +- **Multi-Flow Support**: `Sink`, `Source`, and `SourceAndSink` now accept multiple `flows` as `inputs` and `outputs` instead of just one. This enables modeling more use cases with these classes +- **Flow Control**: Both `Sink` and `Source` now have a `prevent_simultaneous_flow_rates` argument to prevent simultaneous flow rates of more than one of their flows -### Added -- Added `FlowSystem.start_network_app()` and `FlowSystem.stop_network_app()` to easily visualize the network structure of a flow system in an interactive dash web app. This is still experimental and might change in the future. [[#293](https://github.com/flixOpt/flixopt/pull/293) by [@FBumann](https://github.com/FBumann)] +### 🗑️ Deprecated +- For the classes `Sink`, `Source` and `SourceAndSink`: `.sink`, `.source` and `.prevent_simultaneous_sink_and_source` are deprecated in favor of the new arguments `inputs`, `outputs` and `prevent_simultaneous_flow_rates` -### Deprecated -- For the classes `Sink`, `Source` and `SourceAndSink`: `.sink`, `.source` and `.prevent_simultaneous_sink_and_source` are deprecated in favor of the new arguments `inputs`, `outputs` and `prevent_simultaneous_flow_rates`. [[#291](https://github.com/flixOpt/flixopt/pull/291) by [@FBumann](https://github.com/FBumann)] +### 🐛 Fixed +- Fixed testing issue with new `linopy` version 0.5.6 -### Fixed -- Fixed testing issue with new `linopy` version 0.5.6 [[#296](https://github.com/flixOpt/flixopt/pull/296) by [@FBumann](https://github.com/FBumann)] +--- ## [2.1.5] - 2025-07-08 -### Fixed +### 🐛 Fixed - Fixed Docs deployment +--- + ## [2.1.4] - 2025-07-08 -### Fixed +### 🐛 Fixed - Fixing release notes of 2.1.3, as well as documentation build. +--- ## [2.1.3] - 2025-07-08 -### Fixed +### 🐛 Fixed - Using `Effect.maximum_operation_per_hour` raised an error, needing an extra timestep. This has been fixed thanks to @PRse4. +--- + ## [2.1.2] - 2025-06-14 -### Fixed -- Storage losses per hour where not calculated correctly, as mentioned by @brokenwings01. This might have lead to issues with modeling large losses and long timesteps. +### 🐛 Fixed +- Storage losses per hour were not calculated correctly, as mentioned by @brokenwings01. This might have led to issues when modeling large losses and long timesteps. - Old implementation: $c(\text{t}_{i}) \cdot (1-\dot{\text{c}}_\text{rel,loss}(\text{t}_i)) \cdot \Delta \text{t}_{i}$ - Correct implementation: $c(\text{t}_{i}) \cdot (1-\dot{\text{c}}_\text{rel,loss}(\text{t}_i)) ^{\Delta \text{t}_{i}}$ -### Known issues +### 🚧 Known Issues - Just to mention: Plotly >= 6 may raise errors if "nbformat" is not installed. We pinned plotly to <6, but this may be fixed in the future. -## [2.1.1] - 2025-05-08 +--- -### Fixed -- Fixed bug in the `_ElementResults.constraints` not returning the constraints but rather the variables +## [2.1.1] - 2025-05-08 -### Changed +### ♻️ Changed - Improved docstring and tests +### 🐛 Fixed +- Fixed bug in the `_ElementResults.constraints` not returning the constraints but rather the variables + +--- ## [2.1.0] - 2025-04-11 -### Added +### ✨ Added - Python 3.13 support added - Logger warning if relative_minimum is used without on_off_parameters in Flow - Greatly improved internal testing infrastructure by leveraging linopy's testing framework -### Fixed -- Fixed the lower bound of `flow_rate` when using optional investments without OnOffParameters -- Fixed bug that prevented divest effects from working -- Added lower bounds of 0 to two unbounded vars (numerical improvement) - -### Changed -- **BREAKING**: Restructured the modeling of the On/Off state of Flows or Components +### 💥 Breaking Changes +- Restructured the modeling of the On/Off state of Flows or Components - Variable renaming: `...|consecutive_on_hours` → `...|ConsecutiveOn|hours` - Variable renaming: `...|consecutive_off_hours` → `...|ConsecutiveOff|hours` - Constraint renaming: `...|consecutive_on_hours_con1` → `...|ConsecutiveOn|con1` - Similar pattern for all consecutive on/off constraints +### 🐛 Fixed +- Fixed the lower bound of `flow_rate` when using optional investments without OnOffParameters +- Fixed bug that prevented divest effects from working +- Added lower bounds of 0 to two unbounded vars (numerical improvement) + +--- + ## [2.0.1] - 2025-04-10 -### Added +### ✨ Added - Logger warning if relative_minimum is used without on_off_parameters in Flow -### Fixed +### 🐛 Fixed - Replace "|" with "__" in filenames when saving figures (Windows compatibility) - Fixed bug that prevented the load factor from working without InvestmentParameters ## [2.0.0] - 2025-03-29 -### Changed -- **BREAKING**: Complete migration from Pyomo to Linopy optimization framework -- **BREAKING**: Redesigned data handling to rely on xarray.Dataset throughout the package -- **BREAKING**: Framework renamed from flixOpt to flixopt (`import flixopt as fx`) -- **BREAKING**: Results handling completely redesigned with new `CalculationResults` class +**Summary:** 💥 **MAJOR RELEASE** - Complete framework migration from Pyomo to Linopy with redesigned architecture. -### Added +### ✨ Added + +**Model Capabilities:** - Full model serialization support - save and restore unsolved Models - Enhanced model documentation with YAML export containing human-readable mathematical formulations - Extend flixopt models with native linopy language support - Full Model Export/Import capabilities via linopy.Model + +**Results & Data:** - Unified solution exploration through `Calculation.results` attribute - Compression support for result files - `to_netcdf/from_netcdf` methods for FlowSystem and core components - xarray integration for TimeSeries with improved datatypes support -- Google Style Docstrings throughout the codebase -### Fixed +### 💥 Breaking Changes + +**Framework Migration:** +- **Optimization Engine**: Complete migration from Pyomo to Linopy optimization framework +- **Package Import**: Framework renamed from flixOpt to flixopt (`import flixopt as fx`) +- **Data Architecture**: Redesigned data handling to rely on xarray.Dataset throughout the package +- **Results System**: Results handling completely redesigned with new `CalculationResults` class + +**Variable Structure:** +- Restructured the modeling of the On/Off state of Flows or Components + - Variable renaming: `...|consecutive_on_hours` → `...|ConsecutiveOn|hours` + - Variable renaming: `...|consecutive_off_hours` → `...|ConsecutiveOff|hours` + - Constraint renaming: `...|consecutive_on_hours_con1` → `...|ConsecutiveOn|con1` + - Similar pattern for all consecutive on/off constraints + +### 🔥 Removed +- **Pyomo dependency** (replaced by linopy) +- **Period concepts** in time management (simplified to timesteps) + +### 🐛 Fixed - Improved infeasible model detection and reporting - Enhanced time series management and serialization - Reduced file size through improved compression -### Removed -- **BREAKING**: Pyomo dependency (replaced by linopy) -- Period concepts in time management (simplified to timesteps) +### 📝 Docs +- Google Style Docstrings throughout the codebase diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index df8f39d9a..f66c4e5e5 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -4,4 +4,4 @@ - [Examples](examples/) - [Contribute](contribute.md) - [API Reference](api-reference/) -- [Release Notes](changelog.md) +- [Release Notes](changelog/) diff --git a/scripts/extract_changelog.py b/scripts/extract_changelog.py new file mode 100644 index 000000000..c2c34d35b --- /dev/null +++ b/scripts/extract_changelog.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +""" +Extract individual releases from CHANGELOG.md to docs/changelog/ +Simple script to create one file per release. +""" + +import os +import re +from pathlib import Path + +from packaging.version import InvalidVersion, Version +from packaging.version import parse as parse_version + + +def extract_releases(): + """Extract releases from CHANGELOG.md and save to individual files.""" + + changelog_path = Path('CHANGELOG.md') + output_dir = Path('docs/changelog') + + if not changelog_path.exists(): + print('❌ CHANGELOG.md not found') + return + + # Create output directory + output_dir.mkdir(parents=True, exist_ok=True) + + # Read changelog + with open(changelog_path, encoding='utf-8') as f: + content = f.read() + + # Remove template section (HTML comments) + content = re.sub(r'', '', content, flags=re.DOTALL) + + # Split by release headers + sections = re.split(r'^## \[', content, flags=re.MULTILINE) + + releases = [] + for section in sections[1:]: # Skip first empty section + # Extract version and date from start of section + match = re.match(r'([^\]]+)\] - ([^\n]+)\n(.*)', section, re.DOTALL) + if match: + version, date, release_content = match.groups() + releases.append((version, date.strip(), release_content.strip())) + + print(f'🔍 Found {len(releases)} releases') + + # Sort releases by version (oldest first) to keep existing file prefixes stable. + def version_key(release): + try: + return parse_version(release[0]) + except InvalidVersion: + return parse_version('0.0.0') # fallback for invalid versions + + releases.sort(key=version_key, reverse=False) + + # Show what we captured for debugging + if releases: + print(f'🔧 First release content length: {len(releases[0][2])}') + + for i, (version_str, date, release_content) in enumerate(releases): + # Clean up version for filename with numeric prefix (newest first) + index = 99999 - i # Newest first, while keeping the same file names for old releases + prefix = f'{index:05d}' # Zero-padded 5-digit number + filename = f'{prefix}-v{version_str.replace(" ", "-")}.md' + filepath = output_dir / filename + + # Clean up content - remove trailing --- separators and emojis from headers + cleaned_content = re.sub(r'\s*---\s*$', '', release_content.strip()) + + # Generate navigation links + nav_links = [] + + # Previous version (older release) + if i > 0: + prev_index = 99999 - (i - 1) + prev_version = releases[i - 1][0] + prev_filename = f'{prev_index:05d}-v{prev_version.replace(" ", "-")}.md' + nav_links.append(f'← [Previous: {prev_version}]({prev_filename})') + + # Next version (newer release) + if i < len(releases) - 1: + next_index = 99999 - (i + 1) + next_version = releases[i + 1][0] + next_filename = f'{next_index:05d}-v{next_version.replace(" ", "-")}.md' + nav_links.append(f'[Next: {next_version}]({next_filename}) →') + + # Always add link back to index + nav_links.append('[📋 All Releases](index.md)') + # Add GitHub tag link only for valid PEP 440 versions (skip e.g. "Unreleased") + ver_obj = parse_version(version_str) + if isinstance(ver_obj, Version): + nav_links.append(f'[🏷️ GitHub Release](https://github.com/flixOpt/flixopt/releases/tag/v{version_str})') + # Create content with navigation + content_lines = [ + f'# {version_str} - {date.strip()}', + '', + ' | '.join(nav_links), + '', + '---', + '', + cleaned_content, + '', + '---', + '', + ' | '.join(nav_links), + ] + + # Write file + with open(filepath, 'w', encoding='utf-8') as f: + f.write('\n'.join(content_lines)) + + print(f'✅ Created {filename}') + + print(f'🎉 Extracted {len(releases)} releases to docs/changelog/') + + +def extract_index(): + changelog_path = Path('CHANGELOG.md') + output_dir = Path('docs/changelog') + index_path = output_dir / 'index.md' + + if not changelog_path.exists(): + print('❌ CHANGELOG.md not found') + return + + # Create output directory + output_dir.mkdir(parents=True, exist_ok=True) + + # Read changelog + with open(changelog_path, encoding='utf-8') as f: + content = f.read() + + intro_match = re.search(r'# Changelog\s+([\s\S]*?)(?= --- +## [2.1.10] - 2025-09-29 +**Summary:** This release is a Documentation and Development release. + +### 📝 Docs +- Improved CHANGELOG.md formatting by adding better categories and formating by Gitmoji. +- Added a script to extract the release notes from the CHANGELOG.md file for better organized documentation. + +### 👷 Development +- Improved `renovate.config` +- Sped up CI by not running examples in every run and using `pytest-xdist` + +--- + ## [2.1.9] - 2025-09-23 **Summary:** Small bugfix release addressing network visualization error handling. @@ -150,6 +163,9 @@ Until here --> ### 🐛 Fixed - Fixed testing issue with new `linopy` version 0.5.6 +### 👷 Development +- Added dependency "nbformat>=4.2.0" to dev dependencies to resolve issue with plotly CI + --- ## [2.1.5] - 2025-07-08 From bbb387159b8cc69232cae85e148e12c58c2715ea Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 29 Sep 2025 18:02:29 +0200 Subject: [PATCH 322/448] Exclude packages --- pyproject.toml | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 902694a82..be07d86f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -116,11 +116,48 @@ documentation = "https://flixopt.github.io/flixopt/" [tool.setuptools.packages.find] where = ["."] -exclude = ["tests", "docs", "examples", "examples.*", "Tutorials", ".git", ".vscode", "build", ".venv", "venv/"] +include = ["flixopt*"] +exclude = [ + "tests*", + "docs*", + "examples*", + "Tutorials*", + "scripts*", + "pics*", + "results*", + "lib*", + ".git*", + ".github*", + ".vscode*", + ".idea*", + "build*", + "dist*", + ".venv*", + "venv*", + "*.egg-info*", + "__pycache__*", + ".pytest_cache*", + ".ruff_cache*" +] [tool.setuptools.package-data] "flixopt" = ["config.yaml"] +[tool.setuptools] +include-package-data = false + +[tool.setuptools.exclude-package-data] +"*" = [ + "*.log", + "*.html", + "*.json", + "*.DS_Store", + "temp-*", + "*.pyc", + "*.pyo", + "__pycache__" +] + [tool.setuptools_scm] version_scheme = "post-release" From 6041b58868c7234e82e7a74f15a0f4f6655eb848 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 29 Sep 2025 18:17:40 +0200 Subject: [PATCH 323/448] Update packaging configuration - Add MANIFEST.in to define files included in source distributions - Adjust pyproject.toml to simplify package exclusions and include essential metadata --- MANIFEST.in | 26 ++++++++++++++++++++++++++ pyproject.toml | 40 ++++------------------------------------ 2 files changed, 30 insertions(+), 36 deletions(-) create mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 000000000..72a1ff8eb --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,26 @@ +# Include essential files from source distribution +include LICENSE +include README.md +include CHANGELOG.md +include pyproject.toml + +# Include package source and data +recursive-include flixopt *.py *.yaml + +# Exclude everything else +global-exclude *.pyc *.pyo __pycache__ +prune .github +prune docs +prune examples +prune tests +prune pics +prune scripts +prune build +prune dist +prune .venv +prune venv +exclude .gitignore +exclude .pre-commit-config.yaml +exclude renovate.json +exclude mkdocs.yml +exclude test_package.sh diff --git a/pyproject.toml b/pyproject.toml index be07d86f1..dcc7ffa12 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ dynamic = ["version"] description = "Vector based energy and material flow optimization framework in Python." readme = "README.md" requires-python = ">=3.10" -license = { text = "MIT License" } +license = "MIT" authors = [ { name = "Chair of Building Energy Systems and Heat Supply, TU Dresden", email = "peter.stange@tu-dresden.de" }, { name = "Felix Bumann", email = "felixbumann387@gmail.com" }, @@ -22,7 +22,6 @@ maintainers = [ keywords = ["optimization", "energy systems", "numerical analysis"] classifiers = [ "Development Status :: 3 - Alpha", - "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -30,7 +29,6 @@ classifiers = [ "Intended Audience :: Developers", "Intended Audience :: Science/Research", "Topic :: Scientific/Engineering", - "License :: OSI Approved :: MIT License", ] dependencies = [ # Core scientific computing @@ -117,46 +115,16 @@ documentation = "https://flixopt.github.io/flixopt/" [tool.setuptools.packages.find] where = ["."] include = ["flixopt*"] -exclude = [ - "tests*", - "docs*", - "examples*", - "Tutorials*", - "scripts*", - "pics*", - "results*", - "lib*", - ".git*", - ".github*", - ".vscode*", - ".idea*", - "build*", - "dist*", - ".venv*", - "venv*", - "*.egg-info*", - "__pycache__*", - ".pytest_cache*", - ".ruff_cache*" -] +exclude = ["tests*", "docs*", "examples*", "Tutorials*"] [tool.setuptools.package-data] "flixopt" = ["config.yaml"] [tool.setuptools] -include-package-data = false +include-package-data = true [tool.setuptools.exclude-package-data] -"*" = [ - "*.log", - "*.html", - "*.json", - "*.DS_Store", - "temp-*", - "*.pyc", - "*.pyo", - "__pycache__" -] +"*" = ["*.md", ".git*", "*.ipynb", "renovate.json"] [tool.setuptools_scm] version_scheme = "post-release" From 0899360144f4946918f9f844af242bca52d4041b Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 30 Sep 2025 10:35:54 +0200 Subject: [PATCH 324/448] Change to save netcdf files with 'h5netcdf' instead of netcdf4. Following xarray default (#374) - Updated `save_dataset_to_netcdf` and related methods to accept the `engine` parameter. - Changed default netCDF engine to `h5netcdf` across the codebase. - Updated dependencies to include `h5netcdf`. --- flixopt/calculation.py | 2 +- flixopt/flow_system.py | 10 ++++++++-- flixopt/io.py | 2 ++ flixopt/results.py | 6 +++--- pyproject.toml | 2 +- 5 files changed, 15 insertions(+), 7 deletions(-) diff --git a/flixopt/calculation.py b/flixopt/calculation.py index c912b083b..a695b285b 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -164,7 +164,7 @@ def solve(self, solver: _Solver, log_file: pathlib.Path | None = None, log_main_ from .io import document_linopy_model document_linopy_model(self.model, paths.model_documentation) - self.flow_system.to_netcdf(paths.flow_system) + self.flow_system.to_netcdf(paths.flow_system, engine='h5netcdf') raise RuntimeError( f'Model was infeasible. Please check {paths.model_documentation=} and {paths.flow_system=} for more information.' ) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 63b6c6c52..4fedbddfd 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -211,7 +211,13 @@ def as_dataset(self, constants_in_dataset: bool = False) -> xr.Dataset: ds.attrs = self.as_dict(data_mode='name') return ds - def to_netcdf(self, path: str | pathlib.Path, compression: int = 0, constants_in_dataset: bool = True): + def to_netcdf( + self, + path: str | pathlib.Path, + compression: int = 0, + constants_in_dataset: bool = True, + engine: str = 'h5netcdf', + ): """ Saves the FlowSystem to a netCDF file. Args: @@ -220,7 +226,7 @@ def to_netcdf(self, path: str | pathlib.Path, compression: int = 0, constants_in constants_in_dataset: If True, constants are included as Dataset variables. """ ds = self.as_dataset(constants_in_dataset=constants_in_dataset) - fx_io.save_dataset_to_netcdf(ds, path, compression=compression) + fx_io.save_dataset_to_netcdf(ds, path, compression=compression, engine='h5netcdf') logger.info(f'Saved FlowSystem to {path}') def plot_network( diff --git a/flixopt/io.py b/flixopt/io.py index 35304634d..07e990c45 100644 --- a/flixopt/io.py +++ b/flixopt/io.py @@ -208,6 +208,7 @@ def save_dataset_to_netcdf( ds: xr.Dataset, path: str | pathlib.Path, compression: int = 0, + engine: str = 'h5netcdf', ) -> None: """ Save a dataset to a netcdf file. Store the attrs as a json string in the 'attrs' attribute. @@ -240,6 +241,7 @@ def save_dataset_to_netcdf( encoding=None if not apply_encoding else {data_var: {'zlib': True, 'complevel': compression} for data_var in ds.data_vars}, + engine='h5netcdf', ) diff --git a/flixopt/results.py b/flixopt/results.py index 5a76e3e0a..a34875381 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -328,8 +328,8 @@ def to_file( paths = fx_io.CalculationResultsPaths(folder, name) - fx_io.save_dataset_to_netcdf(self.solution, paths.solution, compression=compression) - fx_io.save_dataset_to_netcdf(self.flow_system, paths.flow_system, compression=compression) + fx_io.save_dataset_to_netcdf(self.solution, paths.solution, compression=compression, engine='h5netcdf') + fx_io.save_dataset_to_netcdf(self.flow_system, paths.flow_system, compression=compression, engine='h5netcdf') with open(paths.summary, 'w', encoding='utf-8') as f: yaml.dump(self.summary, f, allow_unicode=True, sort_keys=False, indent=4, width=1000) @@ -338,7 +338,7 @@ def to_file( if self.model is None: logger.critical('No model in the CalculationResults. Saving the model is not possible.') else: - self.model.to_netcdf(paths.linopy_model) + self.model.to_netcdf(paths.linopy_model, engine='h5netcdf') if document_model: if self.model is None: diff --git a/pyproject.toml b/pyproject.toml index dcc7ffa12..1a3ece7ca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ dependencies = [ "xarray >= 2024.2.0, < 2026.0", # CalVer: allow through next calendar year # Optimization and data handling "linopy >= 0.5.1, < 0.6", # Widened from patch pin to minor range - "netcdf4 >= 1.6.1, < 2", + "h5netcdf>=1.0.0, < 2", # Utilities "pyyaml >= 6.0.0, < 7", "rich >= 13.0.0, < 15", From fa61e6e263651ff53a14e83887f2aa405e6681af Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 30 Sep 2025 11:20:09 +0200 Subject: [PATCH 325/448] Feature/h5netcdf update (#375) * Change to save netcdf files with 'h5netcdf' instead of netcdf4. Following xarray default - Updated `save_dataset_to_netcdf` and related methods to accept the `engine` parameter. - Changed default netCDF engine to `h5netcdf` across the codebase. - Updated dependencies to include `h5netcdf`. * Revert some chnages to simply change the engine centrally * Formating * ruff format * 1. flixopt/io.py:229-236 - Changed compression check from netCDF4 to h5netcdf to match the engine being used 2. flixopt/io.py:258 - Added explicit engine='h5netcdf' parameter to load_dataset_from_netcdf for consistency * Update CHANGELOG.md --- CHANGELOG.md | 1 + flixopt/flow_system.py | 9 ++------- flixopt/io.py | 8 ++++---- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a59261e5..522da6881 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ Please keep the format of the changelog consistent with the other releases, so t ### 💥 Breaking Changes ### ♻️ Changed +- Using `h5netcdf` instead of `netCDF4` for dataset I/O operations. This follows the update in `xarray==2025.09.01` ### 🗑️ Deprecated diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 4fedbddfd..a07e3a38f 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -211,15 +211,10 @@ def as_dataset(self, constants_in_dataset: bool = False) -> xr.Dataset: ds.attrs = self.as_dict(data_mode='name') return ds - def to_netcdf( - self, - path: str | pathlib.Path, - compression: int = 0, - constants_in_dataset: bool = True, - engine: str = 'h5netcdf', - ): + def to_netcdf(self, path: str | pathlib.Path, compression: int = 0, constants_in_dataset: bool = True) -> None: """ Saves the FlowSystem to a netCDF file. + Args: path: The path to the netCDF file. compression: The compression level to use when saving the file. diff --git a/flixopt/io.py b/flixopt/io.py index 07e990c45..191df8a11 100644 --- a/flixopt/io.py +++ b/flixopt/io.py @@ -227,12 +227,12 @@ def save_dataset_to_netcdf( apply_encoding = False if compression != 0: - if importlib.util.find_spec('netCDF4') is not None: + if importlib.util.find_spec('h5netcdf') is not None: apply_encoding = True else: logger.warning( - 'Dataset was exported without compression due to missing dependency "netcdf4".' - 'Install netcdf4 via `pip install netcdf4`.' + 'Dataset was exported without compression due to missing dependency "h5netcdf".' + 'Install h5netcdf via `pip install h5netcdf`.' ) ds = ds.copy(deep=True) ds.attrs = {'attrs': json.dumps(ds.attrs)} @@ -255,7 +255,7 @@ def load_dataset_from_netcdf(path: str | pathlib.Path) -> xr.Dataset: Returns: Dataset: Loaded dataset. """ - ds = xr.load_dataset(str(path)) + ds = xr.load_dataset(str(path), engine='h5netcdf') ds.attrs = json.loads(ds.attrs['attrs']) return ds From 80a9071ebc915da3371487bd9b7d25ef94224976 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 30 Sep 2025 13:55:50 +0200 Subject: [PATCH 326/448] Update Renovate config: set custom rules for CalVer dependencies - Disable automerge - Add 14-day minimum release age - high PR priority schedule checks. --- CHANGELOG.md | 3 +-- renovate.json | 9 +++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 522da6881..ebb2562a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,10 +57,9 @@ Please keep the format of the changelog consistent with the other releases, so t ### 🔒 Security ### 📦 Dependencies +- Updated `renovate.config` to treat CalVer packages (xarray and dask) with more care ### 📝 Docs -- Improved CHANGELOG.md formatting by adding better categories and formating by Gitmoji. -- Added a script to extract the release notes from the CHANGELOG.md file for better organized documentation. ### 👷 Development diff --git a/renovate.json b/renovate.json index 611f1cdb7..b8d59be9b 100644 --- a/renovate.json +++ b/renovate.json @@ -26,6 +26,15 @@ "matchCurrentVersion": "!/^0/", "automerge": true, "automergeType": "pr" + }, + { + "description": "CalVer packages (xarray, dask) can have breaking changes in any release - never automerge, longer release age", + "matchPackageNames": ["xarray", "dask"], + "automerge": false, + "minimumReleaseAge": "14 days", + "schedule": ["* * * * *"], + "labels": ["calver", "breaking-change-risk", "dependencies"], + "prPriority": 10 } ] } From 55dea59ea7a54f6ed7d6ef397248b1737ad81f55 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 30 Sep 2025 14:03:15 +0200 Subject: [PATCH 327/448] Allow automerge on dev dependencies with renovate.json --- renovate.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/renovate.json b/renovate.json index b8d59be9b..c08d535c5 100644 --- a/renovate.json +++ b/renovate.json @@ -17,9 +17,13 @@ "minimumReleaseAge": "7 days", "packageRules": [ { + "description": "Group and automerge dev and docs dependencies", "matchDepTypes": ["dev", "docs"], + "groupName": "dev dependencies", "rangeStrategy": "pin", - "minimumReleaseAge": "14 days" + "minimumReleaseAge": "14 days", + "automerge": true, + "automergeType": "pr" }, { "matchUpdateTypes": ["patch"], @@ -30,7 +34,6 @@ { "description": "CalVer packages (xarray, dask) can have breaking changes in any release - never automerge, longer release age", "matchPackageNames": ["xarray", "dask"], - "automerge": false, "minimumReleaseAge": "14 days", "schedule": ["* * * * *"], "labels": ["calver", "breaking-change-risk", "dependencies"], From 4b2b31f8c65bd1d34a0d07af4bb646e619ea1cc0 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 30 Sep 2025 14:30:52 +0200 Subject: [PATCH 328/448] Add "separateMinorPatch": false to renovate.json --- renovate.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/renovate.json b/renovate.json index c08d535c5..16b85fe71 100644 --- a/renovate.json +++ b/renovate.json @@ -23,7 +23,8 @@ "rangeStrategy": "pin", "minimumReleaseAge": "14 days", "automerge": true, - "automergeType": "pr" + "automergeType": "pr", + "separateMinorPatch": false }, { "matchUpdateTypes": ["patch"], From dee2b23a0f7f87a94694ddab2cd8abdcb46f67b3 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 5 Oct 2025 13:42:27 +0200 Subject: [PATCH 329/448] Fix `charge_state` Constraint in `Storage` leading to incorrect losses in discharge and therefore incorrect charge states and discharge values (#347) * Fix equation in Storage * Fix test for equation in Storage * Update CHANGELOG.md * Improve Changelog Message --- CHANGELOG.md | 18 ++++++++++++++++++ flixopt/components.py | 2 +- tests/test_storage.py | 2 +- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ebb2562a1..e9d7edaf6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -90,6 +90,24 @@ Until here --> --- +## [Unreleased] - ????-??-?? + +### Added + +### Changed + +### Deprecated + +### Removed + +### Fixed +- Fix `charge_state` Constraint in `Storage` leading to incorrect losses in discharge and therefore incorrect charge states and discharge values. + +### Known issues + +### *Development* + + ## [2.1.8] - 2025-09-22 **Summary:** Code quality improvements, enhanced documentation, and bug fixes for heat pump components and visualization features. diff --git a/flixopt/components.py b/flixopt/components.py index 9dd0fc52b..2ad8d90e8 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -791,7 +791,7 @@ def do_modeling(self): charge_state.isel(time=slice(1, None)) == charge_state.isel(time=slice(None, -1)) * ((1 - rel_loss) ** hours_per_step) + charge_rate * eff_charge * hours_per_step - - discharge_rate * eff_discharge * hours_per_step, + - discharge_rate * hours_per_step / eff_discharge, name=f'{self.label_full}|charge_state', ), 'charge_state', diff --git a/tests/test_storage.py b/tests/test_storage.py index e8a95a2a1..5971c2f5c 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -158,7 +158,7 @@ def test_lossy_storage(self, basic_flow_system_linopy): charge_state.isel(time=slice(1, None)) == charge_state.isel(time=slice(None, -1)) * (1 - rel_loss) ** hours_per_step + charge_rate * eff_charge * hours_per_step - - discharge_rate * eff_discharge * hours_per_step, + - discharge_rate / eff_discharge * hours_per_step, ) # Check initial charge state constraint From 8791012783b00eed19ea02905965bff8985ac01c Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 5 Oct 2025 13:48:19 +0200 Subject: [PATCH 330/448] Fix CHANGELOG.md --- CHANGELOG.md | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9d7edaf6..8a948cba9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,7 +57,6 @@ Please keep the format of the changelog consistent with the other releases, so t ### 🔒 Security ### 📦 Dependencies -- Updated `renovate.config` to treat CalVer packages (xarray and dask) with more care ### 📝 Docs @@ -68,6 +67,20 @@ Please keep the format of the changelog consistent with the other releases, so t Until here --> --- +## [v2.1.11] - 2025-10-05 +Important bugfix in `Storage` leading to wrong results due to incorrect discharge losses. + +### ♻️ Changed +- Using `h5netcdf` instead of `netCDF4` for dataset I/O operations. This follows the update in `xarray==2025.09.01` + +### 🐛 Fixed +- Fix `charge_state` Constraint in `Storage` leading to incorrect losses in discharge and therefore incorrect charge states and discharge values. + +### 📦 Dependencies +- Updated `renovate.config` to treat CalVer packages (xarray and dask) with more care + +--- + ## [2.1.10] - 2025-09-29 **Summary:** This release is a Documentation and Development release. @@ -90,24 +103,6 @@ Until here --> --- -## [Unreleased] - ????-??-?? - -### Added - -### Changed - -### Deprecated - -### Removed - -### Fixed -- Fix `charge_state` Constraint in `Storage` leading to incorrect losses in discharge and therefore incorrect charge states and discharge values. - -### Known issues - -### *Development* - - ## [2.1.8] - 2025-09-22 **Summary:** Code quality improvements, enhanced documentation, and bug fixes for heat pump components and visualization features. From eb6bab063b74f40f33f42581cd3d050f7feab955 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 5 Oct 2025 13:52:44 +0200 Subject: [PATCH 331/448] Simplify changes from next release --- flixopt/calculation.py | 2 +- flixopt/flow_system.py | 2 +- flixopt/io.py | 10 +++++----- flixopt/results.py | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/flixopt/calculation.py b/flixopt/calculation.py index a695b285b..c912b083b 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -164,7 +164,7 @@ def solve(self, solver: _Solver, log_file: pathlib.Path | None = None, log_main_ from .io import document_linopy_model document_linopy_model(self.model, paths.model_documentation) - self.flow_system.to_netcdf(paths.flow_system, engine='h5netcdf') + self.flow_system.to_netcdf(paths.flow_system) raise RuntimeError( f'Model was infeasible. Please check {paths.model_documentation=} and {paths.flow_system=} for more information.' ) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index a07e3a38f..604b1ca1e 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -221,7 +221,7 @@ def to_netcdf(self, path: str | pathlib.Path, compression: int = 0, constants_in constants_in_dataset: If True, constants are included as Dataset variables. """ ds = self.as_dataset(constants_in_dataset=constants_in_dataset) - fx_io.save_dataset_to_netcdf(ds, path, compression=compression, engine='h5netcdf') + fx_io.save_dataset_to_netcdf(ds, path, compression=compression) logger.info(f'Saved FlowSystem to {path}') def plot_network( diff --git a/flixopt/io.py b/flixopt/io.py index 191df8a11..314f693db 100644 --- a/flixopt/io.py +++ b/flixopt/io.py @@ -208,7 +208,7 @@ def save_dataset_to_netcdf( ds: xr.Dataset, path: str | pathlib.Path, compression: int = 0, - engine: str = 'h5netcdf', + engine: Literal['netcdf4', 'scipy', 'h5netcdf'] = 'h5netcdf', ) -> None: """ Save a dataset to a netcdf file. Store the attrs as a json string in the 'attrs' attribute. @@ -227,12 +227,12 @@ def save_dataset_to_netcdf( apply_encoding = False if compression != 0: - if importlib.util.find_spec('h5netcdf') is not None: + if importlib.util.find_spec(engine) is not None: apply_encoding = True else: logger.warning( - 'Dataset was exported without compression due to missing dependency "h5netcdf".' - 'Install h5netcdf via `pip install h5netcdf`.' + f'Dataset was exported without compression due to missing dependency "{engine}".' + f'Install {engine} via `pip install {engine}`.' ) ds = ds.copy(deep=True) ds.attrs = {'attrs': json.dumps(ds.attrs)} @@ -241,7 +241,7 @@ def save_dataset_to_netcdf( encoding=None if not apply_encoding else {data_var: {'zlib': True, 'complevel': compression} for data_var in ds.data_vars}, - engine='h5netcdf', + engine=engine, ) diff --git a/flixopt/results.py b/flixopt/results.py index a34875381..d93285d2c 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -328,8 +328,8 @@ def to_file( paths = fx_io.CalculationResultsPaths(folder, name) - fx_io.save_dataset_to_netcdf(self.solution, paths.solution, compression=compression, engine='h5netcdf') - fx_io.save_dataset_to_netcdf(self.flow_system, paths.flow_system, compression=compression, engine='h5netcdf') + fx_io.save_dataset_to_netcdf(self.solution, paths.solution, compression=compression) + fx_io.save_dataset_to_netcdf(self.flow_system, paths.flow_system, compression=compression) with open(paths.summary, 'w', encoding='utf-8') as f: yaml.dump(self.summary, f, allow_unicode=True, sort_keys=False, indent=4, width=1000) From 1281d8273879136f2c8bc5c7823fbd94c8f19d2c Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 5 Oct 2025 13:54:30 +0200 Subject: [PATCH 332/448] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a948cba9..846a365e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,7 +68,7 @@ Until here --> --- ## [v2.1.11] - 2025-10-05 -Important bugfix in `Storage` leading to wrong results due to incorrect discharge losses. +**Summary:** Important bugfix in `Storage` leading to wrong results due to incorrect discharge losses. ### ♻️ Changed - Using `h5netcdf` instead of `netCDF4` for dataset I/O operations. This follows the update in `xarray==2025.09.01` From f8c196e673dc2396a734d7f0dd79997fc5f68848 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 5 Oct 2025 14:07:42 +0200 Subject: [PATCH 333/448] Fix CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 846a365e4..baf95cbda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,7 +67,7 @@ Please keep the format of the changelog consistent with the other releases, so t Until here --> --- -## [v2.1.11] - 2025-10-05 +## [2.1.11] - 2025-10-05 **Summary:** Important bugfix in `Storage` leading to wrong results due to incorrect discharge losses. ### ♻️ Changed From f61a978bc0e9d75ca2e102eda61dacd631c3c573 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 5 Oct 2025 13:44:42 +0000 Subject: [PATCH 334/448] chore(deps): update dependency mkdocs-material to v9.6.20 (#369) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1a3ece7ca..f0f4822d6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -95,7 +95,7 @@ dev = [ # Documentation building docs = [ - "mkdocs-material==9.6.19", + "mkdocs-material==9.6.20", "mkdocstrings-python==1.18.2", "mkdocs-table-reader-plugin==3.1.0", "mkdocs-gen-files==0.5.0", From 2cc5af76479643cdba6eff3c2055f323a4699808 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 6 Oct 2025 15:04:28 +0200 Subject: [PATCH 335/448] Improve renovate.json to automerge ruff despite 0.x version --- renovate.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/renovate.json b/renovate.json index 16b85fe71..ded1fbf17 100644 --- a/renovate.json +++ b/renovate.json @@ -39,6 +39,13 @@ "schedule": ["* * * * *"], "labels": ["calver", "breaking-change-risk", "dependencies"], "prPriority": 10 + }, + { + "description": "Automerge ruff patches despite 0.x version", + "matchPackageNames": ["ruff"], + "matchUpdateTypes": ["patch"], + "automerge": true, + "automergeType": "pr" } ] } From bd1ef9cea1f09376c496a66f7206483aab3731fa Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 6 Oct 2025 16:46:59 +0000 Subject: [PATCH 336/448] chore(deps): update dependency tsam to v2.3.9 (#379) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f0f4822d6..b3e923975 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,7 +83,7 @@ dev = [ "ruff==0.13.0", "pre-commit==4.3.0", "pyvis==0.3.2", - "tsam==2.3.1", + "tsam==2.3.9", "scipy==1.15.1", "gurobipy==12.0.3", "dash==3.0.0", From 5e66d5085d50bd3d6def9baa49f5ccfe2672c579 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 09:11:11 +0000 Subject: [PATCH 337/448] chore(deps): update dependency ruff to v0.13.2 (#378) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b3e923975..6a523bbb4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,7 +80,7 @@ dev = [ "pytest==8.4.2", "pytest-xdist==3.8.0", "nbformat==5.10.4", - "ruff==0.13.0", + "ruff==0.13.2", "pre-commit==4.3.0", "pyvis==0.3.2", "tsam==2.3.9", From 493ca97604fba6af31562d6e5c646828522d7462 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 9 Oct 2025 11:30:32 +0200 Subject: [PATCH 338/448] Feature/Improve Configuration options and handling (#385) * Refactor configuration management: remove dataclass-based schema and simplify CONFIG structure. * Refactor configuration loading: switch from `os` to `pathlib`, streamline YAML loading logic. * Refactor logging setup: split handler creation into dedicated functions, simplify configuration logic. * Improve logging configurability and safety - Add support for `RotatingFileHandler` to prevent large log files. - Introduce `console` flag for optional console logging. - Default to `NullHandler` when no handlers are configured for better library behavior. * Temp * Temp * Temp * Temp * Temp * Temp * Refactor configuration and logging: remove unused `merge_configs` function, streamline logging setup, and encapsulate `_setup_logging` as an internal function. * Remove unused `change_logging_level` import and export. * Add tests for config.py * Expand `config.py` test coverage: add tests for custom config loading, logging setup, dict roundtrip, and attribute modification. * Expand `test_config.py` coverage: add modeling config persistence test, refine logging reset, and improve partial config load assertions. * Expand `test_config.py` coverage: add teardown for state cleanup and reset modeling config in setup. * Add `CONFIG.reset()` method and expand test coverage to verify default restoration * Refactor `CONFIG` to centralize defaults in `_DEFAULTS` and ensure `reset()` aligns with them; add test to verify consistency. * Refactor `_DEFAULTS` to use `MappingProxyType` for immutability, restructure config hierarchy, and simplify `reset()` implementation for maintainability; update tests accordingly. * Mark `TestConfigModule` tests to run in a single worker with `@pytest.mark.xdist_group` to prevent global config interference. * Add default log file * Update CHANGELOG.md * Readd change_logging_level() for backwards compatability * Add more options to config.py * Add a docstring to config.y * Add a docstring to config.y * rename parameter message_format * Improve color config * Improve color config * Update CHANGELOG.md * Improve color handling * Improve color handling * Remove console Logging explicityl from examples * Make log to console the default * Make log to console the default * Add individual level parameters for console and file * Add extra Handler section * Use dedicated levels for both handlers * Switch back to not use Handlers * Revert "Switch back to not use Handlers" This reverts commit 05bbccb5d3d750e1b972799ae004d433bb798406. * Revert "Use dedicated levels for both handlers" This reverts commit ed0542bcb0db2d36e5aaec9f1f1e38aa5bc0c6b2. * Revert "Add extra Handler section" This reverts commit a133cc87c3567f0d4e40b43f60943afe7f6b9aaa. * Revert "Add individual level parameters for console and file" This reverts commit 19f81c9e05065de7dcf74bf4750f653d51a2ecbe. * Fix CHANGELOG.md --- CHANGELOG.md | 13 +- flixopt/__init__.py | 2 - flixopt/calculation.py | 4 +- flixopt/config.py | 679 +++++++++++++++++++++++++++++------------ flixopt/config.yaml | 10 - flixopt/elements.py | 10 +- flixopt/features.py | 12 +- flixopt/interface.py | 8 +- tests/test_config.py | 480 +++++++++++++++++++++++++++++ 9 files changed, 995 insertions(+), 223 deletions(-) delete mode 100644 flixopt/config.yaml create mode 100644 tests/test_config.py diff --git a/CHANGELOG.md b/CHANGELOG.md index baf95cbda..70660c6dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,15 +42,23 @@ Please keep the format of the changelog consistent with the other releases, so t ## [Unreleased] - ????-??-?? ### ✨ Added +- Added `CONFIG.reset()` method to restore configuration to default values +- Added configurable log file rotation settings: `CONFIG.Logging.max_file_size` and `CONFIG.Logging.backup_count` +- Added configurable log format settings: `CONFIG.Logging.date_format` and `CONFIG.Logging.format` +- Added configurable console settings: `CONFIG.Logging.console_width` and `CONFIG.Logging.show_path` +- Added `CONFIG.Logging.Colors` nested class for customizable log level colors using ANSI escape codes (works with both standard and Rich handlers) ### 💥 Breaking Changes ### ♻️ Changed -- Using `h5netcdf` instead of `netCDF4` for dataset I/O operations. This follows the update in `xarray==2025.09.01` +- Logging and Configuration management changed ### 🗑️ Deprecated +- `change_logging_level()` function is now deprecated in favor of `CONFIG.Logging.level` and `CONFIG.apply()`. Will be removed in version 3.0.0. ### 🔥 Removed +- Removed unused `config.merge_configs` function from configuration module + ### 🐛 Fixed @@ -61,6 +69,8 @@ Please keep the format of the changelog consistent with the other releases, so t ### 📝 Docs ### 👷 Development +- Greatly expanded test coverage for `config.py` module +- Added `@pytest.mark.xdist_group` to `TestConfigModule` tests to prevent global config interference ### 🚧 Known Issues @@ -78,6 +88,7 @@ Until here --> ### 📦 Dependencies - Updated `renovate.config` to treat CalVer packages (xarray and dask) with more care +- Updated packaging configuration --- diff --git a/flixopt/__init__.py b/flixopt/__init__.py index 34306ae32..d8ad05f19 100644 --- a/flixopt/__init__.py +++ b/flixopt/__init__.py @@ -35,5 +35,3 @@ results, solvers, ) - -CONFIG.load_config() diff --git a/flixopt/calculation.py b/flixopt/calculation.py index c912b083b..4dc13889c 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -91,13 +91,13 @@ def main_results(self) -> dict[str, Scalar | dict]: model.label_of_element: float(model.size.solution) for component in self.flow_system.components.values() for model in component.model.all_sub_models - if isinstance(model, InvestmentModel) and float(model.size.solution) >= CONFIG.modeling.EPSILON + if isinstance(model, InvestmentModel) and float(model.size.solution) >= CONFIG.Modeling.epsilon }, 'Not invested': { model.label_of_element: float(model.size.solution) for component in self.flow_system.components.values() for model in component.model.all_sub_models - if isinstance(model, InvestmentModel) and float(model.size.solution) < CONFIG.modeling.EPSILON + if isinstance(model, InvestmentModel) and float(model.size.solution) < CONFIG.Modeling.epsilon }, }, 'Buses with excess': [ diff --git a/flixopt/config.py b/flixopt/config.py index 74e33e3ee..2ec5bf88c 100644 --- a/flixopt/config.py +++ b/flixopt/config.py @@ -1,168 +1,345 @@ from __future__ import annotations import logging -import os -import types -from dataclasses import dataclass, fields, is_dataclass -from typing import Annotated, Literal, get_type_hints +import warnings +from logging.handlers import RotatingFileHandler +from pathlib import Path +from types import MappingProxyType +from typing import Literal import yaml from rich.console import Console from rich.logging import RichHandler +from rich.style import Style +from rich.theme import Theme -logger = logging.getLogger('flixopt') - - -def merge_configs(defaults: dict, overrides: dict) -> dict: - """ - Merge the default configuration with user-provided overrides. - Args: - defaults: Default configuration dictionary. - overrides: User configuration dictionary. - Returns: - Merged configuration dictionary. - """ - for key, value in overrides.items(): - if isinstance(value, dict) and key in defaults and isinstance(defaults[key], dict): - # Recursively merge nested dictionaries - defaults[key] = merge_configs(defaults[key], value) - else: - # Override the default value - defaults[key] = value - return defaults - - -def dataclass_from_dict_with_validation(cls, data: dict): - """ - Recursively initialize a dataclass from a dictionary. - """ - if not is_dataclass(cls): - raise TypeError(f'{cls} must be a dataclass') - - # Get resolved type hints to handle postponed evaluation - type_hints = get_type_hints(cls) - - # Build kwargs for the dataclass constructor - kwargs = {} - for field in fields(cls): - field_name = field.name - # Use resolved type from get_type_hints instead of field.type - field_type = type_hints.get(field_name, field.type) - field_value = data.get(field_name) - - # If the field type is a dataclass and the value is a dict, recursively initialize - if is_dataclass(field_type) and isinstance(field_value, dict): - kwargs[field_name] = dataclass_from_dict_with_validation(field_type, field_value) - else: - kwargs[field_name] = field_value # Pass as-is if no special handling is needed - - return cls(**kwargs) +__all__ = ['CONFIG', 'change_logging_level'] +logger = logging.getLogger('flixopt') -@dataclass() -class ValidatedConfig: - def __setattr__(self, name, value): - if field := self.__dataclass_fields__.get(name): - # Get resolved type hints to handle postponed evaluation - type_hints = get_type_hints(self.__class__, include_extras=True) - field_type = type_hints.get(name, field.type) - if metadata := getattr(field_type, '__metadata__', None): - assert metadata[0](value), f'Invalid value passed to {name!r}: {value=}' - super().__setattr__(name, value) +# SINGLE SOURCE OF TRUTH - immutable to prevent accidental modification +_DEFAULTS = MappingProxyType( + { + 'config_name': 'flixopt', + 'logging': MappingProxyType( + { + 'level': 'INFO', + 'file': 'flixopt.log', + 'rich': False, + 'console': True, + 'max_file_size': 10_485_760, # 10MB + 'backup_count': 5, + 'date_format': '%Y-%m-%d %H:%M:%S', + 'format': '%(message)s', + 'console_width': 120, + 'show_path': False, + 'colors': MappingProxyType( + { + 'DEBUG': '\033[32m', # Green + 'INFO': '\033[34m', # Blue + 'WARNING': '\033[33m', # Yellow + 'ERROR': '\033[31m', # Red + 'CRITICAL': '\033[1m\033[31m', # Bold Red + } + ), + } + ), + 'modeling': MappingProxyType( + { + 'big': 10_000_000, + 'epsilon': 1e-5, + 'big_binary_bound': 100_000, + } + ), + } +) -@dataclass -class LoggingConfig(ValidatedConfig): - level: Annotated[ - Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'], - lambda level: level in ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'], - ] - file: Annotated[str, lambda file: isinstance(file, str)] - rich: Annotated[bool, lambda rich: isinstance(rich, bool)] +class CONFIG: + """Configuration for flixopt library. + + The CONFIG class provides centralized configuration for logging and modeling parameters. + All changes require calling ``CONFIG.apply()`` to take effect. + + By default, logging outputs to both console and file ('flixopt.log'). + + Attributes: + Logging: Nested class containing all logging configuration options. + Colors: Nested subclass under Logging containing ANSI color codes for log levels. + Modeling: Nested class containing optimization modeling parameters. + config_name (str): Name of the configuration (default: 'flixopt'). + + Logging Attributes: + level (str): Logging level: 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'. + Default: 'INFO' + file (str | None): Log file path. Default: 'flixopt.log'. + Set to None to disable file logging. + console (bool): Enable console (stdout) logging. Default: True + rich (bool): Use Rich library for enhanced console output. Default: False + max_file_size (int): Maximum log file size in bytes before rotation. + Default: 10485760 (10MB) + backup_count (int): Number of backup log files to keep. Default: 5 + date_format (str): Date/time format for log messages. + Default: '%Y-%m-%d %H:%M:%S' + format (str): Log message format string. Default: '%(message)s' + console_width (int): Console width for Rich handler. Default: 120 + show_path (bool): Show file paths in log messages. Default: False + + Colors Attributes: + DEBUG (str): ANSI color code for DEBUG level. Default: '\\033[32m' (green) + INFO (str): ANSI color code for INFO level. Default: '\\033[34m' (blue) + WARNING (str): ANSI color code for WARNING level. Default: '\\033[33m' (yellow) + ERROR (str): ANSI color code for ERROR level. Default: '\\033[31m' (red) + CRITICAL (str): ANSI color code for CRITICAL level. Default: '\\033[1m\\033[31m' (bold red) + + Works with both Rich and standard console handlers. + Rich automatically converts ANSI codes using Style.from_ansi(). + + Common ANSI codes: + + - '\\033[30m' - Black + - '\\033[31m' - Red + - '\\033[32m' - Green + - '\\033[33m' - Yellow + - '\\033[34m' - Blue + - '\\033[35m' - Magenta + - '\\033[36m' - Cyan + - '\\033[37m' - White + - '\\033[1m\\033[3Xm' - Bold color (replace X with color code 0-7) + - '\\033[2m\\033[3Xm' - Dim color (replace X with color code 0-7) + + Examples: + + - Magenta: '\\033[35m' + - Bold cyan: '\\033[1m\\033[36m' + - Dim green: '\\033[2m\\033[32m' + + Modeling Attributes: + big (int): Large number for optimization constraints. Default: 10000000 + epsilon (float): Small tolerance value. Default: 1e-5 + big_binary_bound (int): Upper bound for binary variable constraints. + Default: 100000 + + Examples: + Basic configuration:: + + from flixopt import CONFIG + + CONFIG.Logging.console = True + CONFIG.Logging.level = 'DEBUG' + CONFIG.apply() + + Configure log file rotation:: + + CONFIG.Logging.file = 'myapp.log' + CONFIG.Logging.max_file_size = 5_242_880 # 5 MB + CONFIG.Logging.backup_count = 3 + CONFIG.apply() + + Customize log colors:: + + CONFIG.Logging.Colors.INFO = '\\033[35m' # Magenta + CONFIG.Logging.Colors.DEBUG = '\\033[36m' # Cyan + CONFIG.Logging.Colors.ERROR = '\\033[1m\\033[31m' # Bold red + CONFIG.apply() + + Use Rich handler with custom colors:: + + CONFIG.Logging.console = True + CONFIG.Logging.rich = True + CONFIG.Logging.console_width = 100 + CONFIG.Logging.show_path = True + CONFIG.Logging.Colors.INFO = '\\033[36m' # Cyan + CONFIG.apply() + + Load from YAML file:: + + CONFIG.load_from_file('config.yaml') + + Example YAML config file: + + .. code-block:: yaml + + logging: + level: DEBUG + console: true + file: app.log + rich: true + max_file_size: 5242880 # 5MB + backup_count: 3 + date_format: '%H:%M:%S' + console_width: 100 + show_path: true + colors: + DEBUG: "\\033[36m" # Cyan + INFO: "\\033[32m" # Green + WARNING: "\\033[33m" # Yellow + ERROR: "\\033[31m" # Red + CRITICAL: "\\033[1m\\033[31m" # Bold red + + modeling: + big: 20000000 + epsilon: 1e-6 + big_binary_bound: 200000 + + Reset to defaults:: -@dataclass -class ModelingConfig(ValidatedConfig): - BIG: Annotated[int, lambda x: isinstance(x, int)] - EPSILON: Annotated[float, lambda x: isinstance(x, float)] - BIG_BINARY_BOUND: Annotated[int, lambda x: isinstance(x, int)] + CONFIG.reset() + + Export current configuration:: + config_dict = CONFIG.to_dict() + import yaml -@dataclass -class ConfigSchema(ValidatedConfig): - config_name: Annotated[str, lambda x: isinstance(x, str)] - logging: LoggingConfig - modeling: ModelingConfig + with open('my_config.yaml', 'w') as f: + yaml.dump(config_dict, f) + """ + class Logging: + level: str = _DEFAULTS['logging']['level'] + file: str | None = _DEFAULTS['logging']['file'] + rich: bool = _DEFAULTS['logging']['rich'] + console: bool = _DEFAULTS['logging']['console'] + max_file_size: int = _DEFAULTS['logging']['max_file_size'] + backup_count: int = _DEFAULTS['logging']['backup_count'] + date_format: str = _DEFAULTS['logging']['date_format'] + format: str = _DEFAULTS['logging']['format'] + console_width: int = _DEFAULTS['logging']['console_width'] + show_path: bool = _DEFAULTS['logging']['show_path'] + + class Colors: + DEBUG: str = _DEFAULTS['logging']['colors']['DEBUG'] + INFO: str = _DEFAULTS['logging']['colors']['INFO'] + WARNING: str = _DEFAULTS['logging']['colors']['WARNING'] + ERROR: str = _DEFAULTS['logging']['colors']['ERROR'] + CRITICAL: str = _DEFAULTS['logging']['colors']['CRITICAL'] + + class Modeling: + big: int = _DEFAULTS['modeling']['big'] + epsilon: float = _DEFAULTS['modeling']['epsilon'] + big_binary_bound: int = _DEFAULTS['modeling']['big_binary_bound'] + + config_name: str = _DEFAULTS['config_name'] -class CONFIG: - """ - A configuration class that stores global configuration values as class attributes. - """ + @classmethod + def reset(cls): + """Reset all configuration values to defaults.""" + for key, value in _DEFAULTS['logging'].items(): + if key == 'colors': + # Reset nested Colors class + for color_key, color_value in value.items(): + setattr(cls.Logging.Colors, color_key, color_value) + else: + setattr(cls.Logging, key, value) + + for key, value in _DEFAULTS['modeling'].items(): + setattr(cls.Modeling, key, value) + + cls.config_name = _DEFAULTS['config_name'] + cls.apply() - config_name: str = None - modeling: ModelingConfig = None - logging: LoggingConfig = None + @classmethod + def apply(cls): + """Apply current configuration to logging system.""" + # Convert Colors class attributes to dict + colors_dict = { + 'DEBUG': cls.Logging.Colors.DEBUG, + 'INFO': cls.Logging.Colors.INFO, + 'WARNING': cls.Logging.Colors.WARNING, + 'ERROR': cls.Logging.Colors.ERROR, + 'CRITICAL': cls.Logging.Colors.CRITICAL, + } + + _setup_logging( + default_level=cls.Logging.level, + log_file=cls.Logging.file, + use_rich_handler=cls.Logging.rich, + console=cls.Logging.console, + max_file_size=cls.Logging.max_file_size, + backup_count=cls.Logging.backup_count, + date_format=cls.Logging.date_format, + format=cls.Logging.format, + console_width=cls.Logging.console_width, + show_path=cls.Logging.show_path, + colors=colors_dict, + ) @classmethod - def load_config(cls, user_config_file: str | None = None): - """ - Initialize configuration using defaults or user-specified file. - """ - # Default config file - default_config_path = os.path.join(os.path.dirname(__file__), 'config.yaml') - - if user_config_file is None: - with open(default_config_path) as file: - new_config = yaml.safe_load(file) - elif not os.path.exists(user_config_file): - raise FileNotFoundError(f'Config file not found: {user_config_file}') - else: - with open(user_config_file) as user_file: - new_config = yaml.safe_load(user_file) + def load_from_file(cls, config_file: str | Path): + """Load configuration from YAML file and apply it.""" + config_path = Path(config_file) + if not config_path.exists(): + raise FileNotFoundError(f'Config file not found: {config_file}') - # Convert the merged config to ConfigSchema - config_data = dataclass_from_dict_with_validation(ConfigSchema, new_config) + with config_path.open() as file: + config_dict = yaml.safe_load(file) + cls._apply_config_dict(config_dict) - # Store the configuration in the class as class attributes - cls.logging = config_data.logging - cls.modeling = config_data.modeling - cls.config_name = config_data.config_name + cls.apply() - setup_logging(default_level=cls.logging.level, log_file=cls.logging.file, use_rich_handler=cls.logging.rich) + @classmethod + def _apply_config_dict(cls, config_dict: dict): + """Apply configuration dictionary to class attributes.""" + for key, value in config_dict.items(): + if key == 'logging' and isinstance(value, dict): + for nested_key, nested_value in value.items(): + if nested_key == 'colors' and isinstance(nested_value, dict): + # Handle nested colors under logging + for color_key, color_value in nested_value.items(): + setattr(cls.Logging.Colors, color_key, color_value) + else: + setattr(cls.Logging, nested_key, nested_value) + elif key == 'modeling' and isinstance(value, dict): + for nested_key, nested_value in value.items(): + setattr(cls.Modeling, nested_key, nested_value) + elif hasattr(cls, key): + setattr(cls, key, value) @classmethod def to_dict(cls): - """ - Convert the configuration class into a dictionary for JSON serialization. - Handles dataclasses and simple types like str, int, etc. - """ - config_dict = {} - for attribute, value in cls.__dict__.items(): - # Only consider attributes (not methods, etc.) - if ( - not attribute.startswith('_') - and not isinstance(value, (types.FunctionType, types.MethodType)) - and not isinstance(value, classmethod) - ): - if is_dataclass(value): - config_dict[attribute] = value.__dict__ - else: # Assuming only basic types here! - config_dict[attribute] = value - - return config_dict + """Convert the configuration class into a dictionary for JSON serialization.""" + return { + 'config_name': cls.config_name, + 'logging': { + 'level': cls.Logging.level, + 'file': cls.Logging.file, + 'rich': cls.Logging.rich, + 'console': cls.Logging.console, + 'max_file_size': cls.Logging.max_file_size, + 'backup_count': cls.Logging.backup_count, + 'date_format': cls.Logging.date_format, + 'format': cls.Logging.format, + 'console_width': cls.Logging.console_width, + 'show_path': cls.Logging.show_path, + 'colors': { + 'DEBUG': cls.Logging.Colors.DEBUG, + 'INFO': cls.Logging.Colors.INFO, + 'WARNING': cls.Logging.Colors.WARNING, + 'ERROR': cls.Logging.Colors.ERROR, + 'CRITICAL': cls.Logging.Colors.CRITICAL, + }, + }, + 'modeling': { + 'big': cls.Modeling.big, + 'epsilon': cls.Modeling.epsilon, + 'big_binary_bound': cls.Modeling.big_binary_bound, + }, + } class MultilineFormater(logging.Formatter): + """Formatter that handles multi-line messages with consistent prefixes.""" + + def __init__(self, fmt=None, datefmt=None): + super().__init__(fmt=fmt, datefmt=datefmt) + def format(self, record): message_lines = record.getMessage().split('\n') - - # Prepare the log prefix (timestamp + log level) timestamp = self.formatTime(record, self.datefmt) - log_level = record.levelname.ljust(8) # Align log levels for consistency + log_level = record.levelname.ljust(8) log_prefix = f'{timestamp} | {log_level} |' - # Format all lines first_line = [f'{log_prefix} {message_lines[0]}'] if len(message_lines) > 1: lines = first_line + [f'{log_prefix} {line}' for line in message_lines[1:]] @@ -173,96 +350,212 @@ def format(self, record): class ColoredMultilineFormater(MultilineFormater): - # ANSI escape codes for colors - COLORS = { - 'DEBUG': '\033[32m', # Green - 'INFO': '\033[34m', # Blue - 'WARNING': '\033[33m', # Yellow - 'ERROR': '\033[31m', # Red - 'CRITICAL': '\033[1m\033[31m', # Bold Red - } + """Formatter that adds ANSI colors to multi-line log messages.""" + RESET = '\033[0m' + def __init__(self, fmt=None, datefmt=None, colors=None): + super().__init__(fmt=fmt, datefmt=datefmt) + self.COLORS = ( + colors + if colors is not None + else { + 'DEBUG': '\033[32m', + 'INFO': '\033[34m', + 'WARNING': '\033[33m', + 'ERROR': '\033[31m', + 'CRITICAL': '\033[1m\033[31m', + } + ) + def format(self, record): lines = super().format(record).splitlines() log_color = self.COLORS.get(record.levelname, self.RESET) + formatted_lines = [f'{log_color}{line}{self.RESET}' for line in lines] + return '\n'.join(formatted_lines) - # Create a formatted message for each line separately - formatted_lines = [] - for line in lines: - formatted_lines.append(f'{log_color}{line}{self.RESET}') - return '\n'.join(formatted_lines) +def _create_console_handler( + use_rich: bool = False, + console_width: int = 120, + show_path: bool = False, + date_format: str = '%Y-%m-%d %H:%M:%S', + format: str = '%(message)s', + colors: dict[str, str] | None = None, +) -> logging.Handler: + """Create a console (stdout) logging handler. + Args: + use_rich: If True, use RichHandler with color support. + console_width: Width of the console for Rich handler. + show_path: Show file paths in log messages (Rich only). + date_format: Date/time format string. + format: Log message format string. + colors: Dictionary of ANSI color codes for each log level. -def _get_logging_handler(log_file: str | None = None, use_rich_handler: bool = False) -> logging.Handler: - """Returns a logging handler for the given log file.""" - if use_rich_handler and log_file is None: - # RichHandler for console output - console = Console(width=120) - rich_handler = RichHandler( + Returns: + Configured logging handler (RichHandler or StreamHandler). + """ + if use_rich: + # Convert ANSI codes to Rich theme + if colors: + theme_dict = {} + for level, ansi_code in colors.items(): + # Rich can parse ANSI codes directly! + try: + style = Style.from_ansi(ansi_code) + theme_dict[f'logging.level.{level.lower()}'] = style + except Exception: + # Fallback to default if parsing fails + pass + + theme = Theme(theme_dict) if theme_dict else None + else: + theme = None + + console = Console(width=console_width, theme=theme) + handler = RichHandler( console=console, rich_tracebacks=True, omit_repeated_times=True, - show_path=False, - log_time_format='%Y-%m-%d %H:%M:%S', - ) - rich_handler.setFormatter(logging.Formatter('%(message)s')) # Simplified formatting - - return rich_handler - elif log_file is None: - # Regular Logger with custom formating enabled - file_handler = logging.StreamHandler() - file_handler.setFormatter( - ColoredMultilineFormater( - fmt='%(message)s', - datefmt='%Y-%m-%d %H:%M:%S', - ) + show_path=show_path, + log_time_format=date_format, ) - return file_handler + handler.setFormatter(logging.Formatter(format)) else: - # FileHandler for file output - file_handler = logging.FileHandler(log_file) - file_handler.setFormatter( - MultilineFormater( - fmt='%(message)s', - datefmt='%Y-%m-%d %H:%M:%S', - ) - ) - return file_handler + handler = logging.StreamHandler() + handler.setFormatter(ColoredMultilineFormater(fmt=format, datefmt=date_format, colors=colors)) + + return handler + + +def _create_file_handler( + log_file: str, + max_file_size: int = 10_485_760, + backup_count: int = 5, + date_format: str = '%Y-%m-%d %H:%M:%S', + format: str = '%(message)s', +) -> RotatingFileHandler: + """Create a rotating file handler to prevent huge log files. + Args: + log_file: Path to the log file. + max_file_size: Maximum size in bytes before rotation. + backup_count: Number of backup files to keep. + date_format: Date/time format string. + format: Log message format string. + + Returns: + Configured RotatingFileHandler (without colors). + """ + handler = RotatingFileHandler( + log_file, + maxBytes=max_file_size, + backupCount=backup_count, + encoding='utf-8', + ) + handler.setFormatter(MultilineFormater(fmt=format, datefmt=date_format)) + return handler -def setup_logging( + +def _setup_logging( default_level: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] = 'INFO', - log_file: str | None = 'flixopt.log', + log_file: str | None = None, use_rich_handler: bool = False, + console: bool = False, + max_file_size: int = 10_485_760, + backup_count: int = 5, + date_format: str = '%Y-%m-%d %H:%M:%S', + format: str = '%(message)s', + console_width: int = 120, + show_path: bool = False, + colors: dict[str, str] | None = None, ): - """Setup logging configuration""" - logger = logging.getLogger('flixopt') # Use a specific logger name for your package - logger.setLevel(get_logging_level_by_name(default_level)) - # Clear existing handlers - if logger.hasHandlers(): - logger.handlers.clear() + """Internal function to setup logging - use CONFIG.apply() instead. - logger.addHandler(_get_logging_handler(use_rich_handler=use_rich_handler)) - if log_file is not None: - logger.addHandler(_get_logging_handler(log_file, use_rich_handler=False)) + Configures the flixopt logger with console and/or file handlers. + If no handlers are configured, adds NullHandler (library best practice). - return logger + Args: + default_level: Logging level for the logger. + log_file: Path to log file (None to disable file logging). + use_rich_handler: Use Rich for enhanced console output. + console: Enable console logging. + max_file_size: Maximum log file size before rotation. + backup_count: Number of backup log files to keep. + date_format: Date/time format for log messages. + format: Log message format string. + console_width: Console width for Rich handler. + show_path: Show file paths in log messages (Rich only). + colors: ANSI color codes for each log level. + """ + logger = logging.getLogger('flixopt') + logger.setLevel(getattr(logging, default_level.upper())) + logger.propagate = False # Prevent duplicate logs + logger.handlers.clear() + + if console: + logger.addHandler( + _create_console_handler( + use_rich=use_rich_handler, + console_width=console_width, + show_path=show_path, + date_format=date_format, + format=format, + colors=colors, + ) + ) + + if log_file: + logger.addHandler( + _create_file_handler( + log_file=log_file, + max_file_size=max_file_size, + backup_count=backup_count, + date_format=date_format, + format=format, + ) + ) + # Library best practice: NullHandler if no handlers configured + if not logger.handlers: + logger.addHandler(logging.NullHandler()) -def get_logging_level_by_name(level_name: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']) -> int: - possible_logging_levels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] - if level_name.upper() not in possible_logging_levels: - raise ValueError(f'Invalid logging level {level_name}') - else: - logging_level = getattr(logging, level_name.upper(), logging.WARNING) - return logging_level + return logger def change_logging_level(level_name: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']): + """ + Change the logging level for the flixopt logger and all its handlers. + + .. deprecated:: 2.1.11 + Use ``CONFIG.Logging.level = level_name`` and ``CONFIG.apply()`` instead. + This function will be removed in version 3.0.0. + + Parameters + ---------- + level_name : {'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'} + The logging level to set. + + Examples + -------- + >>> change_logging_level('DEBUG') # deprecated + >>> # Use this instead: + >>> CONFIG.Logging.level = 'DEBUG' + >>> CONFIG.apply() + """ + warnings.warn( + 'change_logging_level is deprecated and will be removed in version 3.0.0. ' + 'Use CONFIG.Logging.level = level_name and CONFIG.apply() instead.', + DeprecationWarning, + stacklevel=2, + ) logger = logging.getLogger('flixopt') - logging_level = get_logging_level_by_name(level_name) + logging_level = getattr(logging, level_name.upper()) logger.setLevel(logging_level) for handler in logger.handlers: handler.setLevel(logging_level) + + +# Initialize default config +CONFIG.apply() diff --git a/flixopt/config.yaml b/flixopt/config.yaml deleted file mode 100644 index e5336eeef..000000000 --- a/flixopt/config.yaml +++ /dev/null @@ -1,10 +0,0 @@ -# Default configuration of flixopt -config_name: flixopt # Name of the config file. This has no effect on the configuration itself. -logging: - level: INFO - file: flixopt.log - rich: false # logging output is formatted using rich. This is only advisable when using a proper terminal -modeling: - BIG: 10000000 # 1e notation not possible in yaml - EPSILON: 0.00001 - BIG_BINARY_BOUND: 100000 diff --git a/flixopt/elements.py b/flixopt/elements.py index 22256b636..21783808c 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -248,7 +248,7 @@ class Flow(Element): size: Flow capacity or nominal rating. Can be: - Scalar value for fixed capacity - InvestParameters for investment-based sizing decisions - - None to use large default value (CONFIG.modeling.BIG) + - None to use large default value (CONFIG.Modeling.big) relative_minimum: Minimum flow rate as fraction of size. Example: 0.2 means flow cannot go below 20% of rated capacity. relative_maximum: Maximum flow rate as fraction of size (typically 1.0). @@ -356,7 +356,7 @@ class Flow(Element): `relative_maximum` for upper bounds on optimization variables. Notes: - - Default size (CONFIG.modeling.BIG) is used when size=None + - Default size (CONFIG.Modeling.big) is used when size=None - list inputs for previous_flow_rate are converted to NumPy arrays - Flow direction is determined by component input/output designation @@ -383,7 +383,7 @@ def __init__( meta_data: dict | None = None, ): super().__init__(label, meta_data=meta_data) - self.size = CONFIG.modeling.BIG if size is None else size + self.size = CONFIG.Modeling.big if size is None else size self.relative_minimum = relative_minimum self.relative_maximum = relative_maximum self.fixed_relative_profile = fixed_relative_profile @@ -455,11 +455,11 @@ def _plausibility_checks(self) -> None: raise PlausibilityError(self.label_full + ': Take care, that relative_minimum <= relative_maximum!') if ( - self.size == CONFIG.modeling.BIG and self.fixed_relative_profile is not None + self.size == CONFIG.Modeling.big and self.fixed_relative_profile is not None ): # Default Size --> Most likely by accident logger.warning( f'Flow "{self.label}" has no size assigned, but a "fixed_relative_profile". ' - f'The default size is {CONFIG.modeling.BIG}. As "flow_rate = size * fixed_relative_profile", ' + f'The default size is {CONFIG.Modeling.big}. As "flow_rate = size * fixed_relative_profile", ' f'the resulting flow_rate will be very high. To fix this, assign a size to the Flow {self}.' ) diff --git a/flixopt/features.py b/flixopt/features.py index 5528917e0..7aafe242d 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -143,7 +143,7 @@ def _create_bounds_for_optional_investment(self): # eq2: P_invest >= isInvested * max(epsilon, investSize_min) self.add( self._model.add_constraints( - self.size >= self.is_invested * np.maximum(CONFIG.modeling.EPSILON, self.parameters.minimum_size), + self.size >= self.is_invested * np.maximum(CONFIG.Modeling.epsilon, self.parameters.minimum_size), name=f'{self.label_full}|is_invested_lb', ), 'is_invested_lb', @@ -304,7 +304,7 @@ def _add_defining_constraints(self): # Constraint: on * lower_bound <= def_var self.add( self._model.add_constraints( - self.on * np.maximum(CONFIG.modeling.EPSILON, lb) <= def_var, name=f'{self.label_full}|on_con1' + self.on * np.maximum(CONFIG.Modeling.epsilon, lb) <= def_var, name=f'{self.label_full}|on_con1' ), 'on_con1', ) @@ -314,7 +314,7 @@ def _add_defining_constraints(self): else: # Case for multiple defining variables ub = sum(bound[1] for bound in self._defining_bounds) / nr_of_def_vars - lb = CONFIG.modeling.EPSILON # TODO: Can this be a bigger value? (maybe the smallest bound?) + lb = CONFIG.Modeling.epsilon # TODO: Can this be a bigger value? (maybe the smallest bound?) # Constraint: on * epsilon <= sum(all_defining_variables) self.add( @@ -337,7 +337,7 @@ def _add_defining_constraints(self): @property def previous_states(self) -> np.ndarray: """Computes the previous states {0, 1} of defining variables as a binary array from their previous values.""" - return StateModel.compute_previous_states(self._previous_values, epsilon=CONFIG.modeling.EPSILON) + return StateModel.compute_previous_states(self._previous_values, epsilon=CONFIG.Modeling.epsilon) @property def previous_on_states(self) -> np.ndarray: @@ -603,14 +603,14 @@ def compute_consecutive_hours_in_state( elif np.isscalar(binary_values) and not np.isscalar(hours_per_timestep): return binary_values * hours_per_timestep[-1] - if np.isclose(binary_values[-1], 0, atol=CONFIG.modeling.EPSILON): + if np.isclose(binary_values[-1], 0, atol=CONFIG.Modeling.epsilon): return 0 if np.isscalar(hours_per_timestep): hours_per_timestep = np.ones(len(binary_values)) * hours_per_timestep hours_per_timestep: np.ndarray - indexes_with_zero_values = np.where(np.isclose(binary_values, 0, atol=CONFIG.modeling.EPSILON))[0] + indexes_with_zero_values = np.where(np.isclose(binary_values, 0, atol=CONFIG.Modeling.epsilon))[0] if len(indexes_with_zero_values) == 0: nr_of_indexes_with_consecutive_ones = len(binary_values) else: diff --git a/flixopt/interface.py b/flixopt/interface.py index e72e28b90..72737cc45 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -650,10 +650,10 @@ class InvestParameters(Interface): fixed_size: When specified, creates a binary investment decision at exactly this size. When None, allows continuous sizing between minimum and maximum bounds. minimum_size: Lower bound for continuous sizing decisions. Defaults to a small - positive value (CONFIG.modeling.EPSILON) to avoid numerical issues. + positive value (CONFIG.Modeling.epsilon) to avoid numerical issues. Ignored when fixed_size is specified. maximum_size: Upper bound for continuous sizing decisions. Defaults to a large - value (CONFIG.modeling.BIG) representing unlimited capacity. + value (CONFIG.Modeling.big) representing unlimited capacity. Ignored when fixed_size is specified. optional: Controls whether investment is required. When True (default), optimization can choose not to invest. When False, forces investment @@ -833,8 +833,8 @@ def __init__( self.optional = optional self.specific_effects: EffectValuesUserScalar = specific_effects or {} self.piecewise_effects = piecewise_effects - self._minimum_size = minimum_size if minimum_size is not None else CONFIG.modeling.EPSILON - self._maximum_size = maximum_size if maximum_size is not None else CONFIG.modeling.BIG # default maximum + self._minimum_size = minimum_size if minimum_size is not None else CONFIG.Modeling.epsilon + self._maximum_size = maximum_size if maximum_size is not None else CONFIG.Modeling.big # default maximum def transform_data(self, flow_system: FlowSystem): self.fix_effects = flow_system.effects.create_effect_values_dict(self.fix_effects) diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 000000000..c486d22c6 --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,480 @@ +"""Tests for the config module.""" + +import logging +import sys +from pathlib import Path + +import pytest + +from flixopt.config import _DEFAULTS, CONFIG, _setup_logging + + +# All tests in this class will run in the same worker to prevent issues with global config altering +@pytest.mark.xdist_group(name='config_tests') +class TestConfigModule: + """Test the CONFIG class and logging setup.""" + + def setup_method(self): + """Reset CONFIG to defaults before each test.""" + CONFIG.reset() + + def teardown_method(self): + """Clean up after each test to prevent state leakage.""" + CONFIG.reset() + + def test_config_defaults(self): + """Test that CONFIG has correct default values.""" + assert CONFIG.Logging.level == 'INFO' + assert CONFIG.Logging.file == 'flixopt.log' + assert CONFIG.Logging.rich is False + assert CONFIG.Logging.console is True + assert CONFIG.Modeling.big == 10_000_000 + assert CONFIG.Modeling.epsilon == 1e-5 + assert CONFIG.Modeling.big_binary_bound == 100_000 + assert CONFIG.config_name == 'flixopt' + + def test_module_initialization(self): + """Test that logging is initialized on module import.""" + # Apply config to ensure handlers are initialized + CONFIG.apply() + logger = logging.getLogger('flixopt') + # Should have at least one handler (file handler by default) + assert len(logger.handlers) >= 1 + # Should have a file handler with default settings + assert any(isinstance(h, logging.handlers.RotatingFileHandler) for h in logger.handlers) + + def test_config_apply_console(self): + """Test applying config with console logging enabled.""" + CONFIG.Logging.console = True + CONFIG.Logging.level = 'DEBUG' + CONFIG.apply() + + logger = logging.getLogger('flixopt') + assert logger.level == logging.DEBUG + # Should have a StreamHandler for console output + assert any(isinstance(h, logging.StreamHandler) for h in logger.handlers) + # Should not have NullHandler when console is enabled + assert not any(isinstance(h, logging.NullHandler) for h in logger.handlers) + + def test_config_apply_file(self, tmp_path): + """Test applying config with file logging enabled.""" + log_file = tmp_path / 'test.log' + CONFIG.Logging.file = str(log_file) + CONFIG.Logging.level = 'WARNING' + CONFIG.apply() + + logger = logging.getLogger('flixopt') + assert logger.level == logging.WARNING + # Should have a RotatingFileHandler for file output + from logging.handlers import RotatingFileHandler + + assert any(isinstance(h, RotatingFileHandler) for h in logger.handlers) + + def test_config_apply_rich(self): + """Test applying config with rich logging enabled.""" + CONFIG.Logging.console = True + CONFIG.Logging.rich = True + CONFIG.apply() + + logger = logging.getLogger('flixopt') + # Should have a RichHandler + from rich.logging import RichHandler + + assert any(isinstance(h, RichHandler) for h in logger.handlers) + + def test_config_apply_multiple_changes(self): + """Test applying multiple config changes at once.""" + CONFIG.Logging.console = True + CONFIG.Logging.level = 'ERROR' + CONFIG.apply() + + logger = logging.getLogger('flixopt') + assert logger.level == logging.ERROR + assert any(isinstance(h, logging.StreamHandler) for h in logger.handlers) + + def test_config_to_dict(self): + """Test converting CONFIG to dictionary.""" + CONFIG.Logging.level = 'DEBUG' + CONFIG.Logging.console = True + + config_dict = CONFIG.to_dict() + + assert config_dict['config_name'] == 'flixopt' + assert config_dict['logging']['level'] == 'DEBUG' + assert config_dict['logging']['console'] is True + assert config_dict['logging']['file'] == 'flixopt.log' + assert config_dict['logging']['rich'] is False + assert 'modeling' in config_dict + assert config_dict['modeling']['big'] == 10_000_000 + + def test_config_load_from_file(self, tmp_path): + """Test loading configuration from YAML file.""" + config_file = tmp_path / 'config.yaml' + config_content = """ +config_name: test_config +logging: + level: DEBUG + console: true + rich: false +modeling: + big: 20000000 + epsilon: 1e-6 +""" + config_file.write_text(config_content) + + CONFIG.load_from_file(config_file) + + assert CONFIG.config_name == 'test_config' + assert CONFIG.Logging.level == 'DEBUG' + assert CONFIG.Logging.console is True + assert CONFIG.Modeling.big == 20000000 + # YAML may load epsilon as string, so convert for comparison + assert float(CONFIG.Modeling.epsilon) == 1e-6 + + def test_config_load_from_file_not_found(self): + """Test that loading from non-existent file raises error.""" + with pytest.raises(FileNotFoundError): + CONFIG.load_from_file('nonexistent_config.yaml') + + def test_config_load_from_file_partial(self, tmp_path): + """Test loading partial configuration (should keep unspecified settings).""" + config_file = tmp_path / 'partial_config.yaml' + config_content = """ +logging: + level: ERROR +""" + config_file.write_text(config_content) + + # Set a non-default value first + CONFIG.Logging.console = True + CONFIG.apply() + + CONFIG.load_from_file(config_file) + + # Should update level but keep other settings + assert CONFIG.Logging.level == 'ERROR' + # Verify console setting is preserved (not in YAML) + assert CONFIG.Logging.console is True + + def test_setup_logging_silent_default(self): + """Test that _setup_logging creates silent logger by default.""" + _setup_logging() + + logger = logging.getLogger('flixopt') + # Should have NullHandler when console=False and log_file=None + assert any(isinstance(h, logging.NullHandler) for h in logger.handlers) + assert not logger.propagate + + def test_setup_logging_with_console(self): + """Test _setup_logging with console output.""" + _setup_logging(console=True, default_level='DEBUG') + + logger = logging.getLogger('flixopt') + assert logger.level == logging.DEBUG + assert any(isinstance(h, logging.StreamHandler) for h in logger.handlers) + + def test_setup_logging_clears_handlers(self): + """Test that _setup_logging clears existing handlers.""" + logger = logging.getLogger('flixopt') + + # Add a dummy handler + dummy_handler = logging.NullHandler() + logger.addHandler(dummy_handler) + _ = len(logger.handlers) + + _setup_logging(console=True) + + # Should have cleared old handlers and added new one + assert dummy_handler not in logger.handlers + + def test_change_logging_level_removed(self): + """Test that change_logging_level function is deprecated but still exists.""" + # This function is deprecated - users should use CONFIG.apply() instead + import flixopt + + # Function should still exist but be deprecated + assert hasattr(flixopt, 'change_logging_level') + + # Should emit deprecation warning when called + with pytest.warns(DeprecationWarning, match='change_logging_level is deprecated'): + flixopt.change_logging_level('DEBUG') + + def test_public_api(self): + """Test that CONFIG and change_logging_level are exported from config module.""" + from flixopt import config + + # CONFIG should be accessible + assert hasattr(config, 'CONFIG') + + # change_logging_level should be accessible (but deprecated) + assert hasattr(config, 'change_logging_level') + + # _setup_logging should exist but be marked as private + assert hasattr(config, '_setup_logging') + + # merge_configs should not exist (was removed) + assert not hasattr(config, 'merge_configs') + + def test_logging_levels(self): + """Test all valid logging levels.""" + levels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] + + for level in levels: + CONFIG.Logging.level = level + CONFIG.Logging.console = True + CONFIG.apply() + + logger = logging.getLogger('flixopt') + assert logger.level == getattr(logging, level) + + def test_logger_propagate_disabled(self): + """Test that logger propagation is disabled.""" + CONFIG.apply() + logger = logging.getLogger('flixopt') + assert not logger.propagate + + def test_file_handler_rotation(self, tmp_path): + """Test that file handler uses rotation.""" + log_file = tmp_path / 'rotating.log' + CONFIG.Logging.file = str(log_file) + CONFIG.apply() + + logger = logging.getLogger('flixopt') + from logging.handlers import RotatingFileHandler + + file_handlers = [h for h in logger.handlers if isinstance(h, RotatingFileHandler)] + assert len(file_handlers) == 1 + + handler = file_handlers[0] + # Check rotation settings + assert handler.maxBytes == 10_485_760 # 10MB + assert handler.backupCount == 5 + + def test_custom_config_yaml_complete(self, tmp_path): + """Test loading a complete custom configuration.""" + config_file = tmp_path / 'custom_config.yaml' + config_content = """ +config_name: my_custom_config +logging: + level: CRITICAL + console: true + rich: true + file: /tmp/custom.log +modeling: + big: 50000000 + epsilon: 1e-4 + big_binary_bound: 200000 +""" + config_file.write_text(config_content) + + CONFIG.load_from_file(config_file) + + # Check all settings were applied + assert CONFIG.config_name == 'my_custom_config' + assert CONFIG.Logging.level == 'CRITICAL' + assert CONFIG.Logging.console is True + assert CONFIG.Logging.rich is True + assert CONFIG.Logging.file == '/tmp/custom.log' + assert CONFIG.Modeling.big == 50000000 + assert float(CONFIG.Modeling.epsilon) == 1e-4 + assert CONFIG.Modeling.big_binary_bound == 200000 + + # Verify logging was applied + logger = logging.getLogger('flixopt') + assert logger.level == logging.CRITICAL + + def test_config_file_with_console_and_file(self, tmp_path): + """Test configuration with both console and file logging enabled.""" + log_file = tmp_path / 'test.log' + config_file = tmp_path / 'config.yaml' + config_content = f""" +logging: + level: INFO + console: true + rich: false + file: {log_file} +""" + config_file.write_text(config_content) + + CONFIG.load_from_file(config_file) + + logger = logging.getLogger('flixopt') + # Should have both StreamHandler and RotatingFileHandler + from logging.handlers import RotatingFileHandler + + assert any(isinstance(h, logging.StreamHandler) for h in logger.handlers) + assert any(isinstance(h, RotatingFileHandler) for h in logger.handlers) + # Should NOT have NullHandler when console/file are enabled + assert not any(isinstance(h, logging.NullHandler) for h in logger.handlers) + + def test_config_to_dict_roundtrip(self, tmp_path): + """Test that config can be saved to dict, modified, and restored.""" + # Set custom values + CONFIG.Logging.level = 'WARNING' + CONFIG.Logging.console = True + CONFIG.Modeling.big = 99999999 + + # Save to dict + config_dict = CONFIG.to_dict() + + # Verify dict structure + assert config_dict['logging']['level'] == 'WARNING' + assert config_dict['logging']['console'] is True + assert config_dict['modeling']['big'] == 99999999 + + # Could be written to YAML and loaded back + yaml_file = tmp_path / 'saved_config.yaml' + import yaml + + with open(yaml_file, 'w') as f: + yaml.dump(config_dict, f) + + # Reset config + CONFIG.Logging.level = 'INFO' + CONFIG.Logging.console = False + CONFIG.Modeling.big = 10_000_000 + + # Load back from file + CONFIG.load_from_file(yaml_file) + + # Should match original values + assert CONFIG.Logging.level == 'WARNING' + assert CONFIG.Logging.console is True + assert CONFIG.Modeling.big == 99999999 + + def test_config_file_with_only_modeling(self, tmp_path): + """Test config file that only sets modeling parameters.""" + config_file = tmp_path / 'modeling_only.yaml' + config_content = """ +modeling: + big: 999999 + epsilon: 0.001 +""" + config_file.write_text(config_content) + + # Set logging config before loading + original_level = CONFIG.Logging.level + CONFIG.load_from_file(config_file) + + # Modeling should be updated + assert CONFIG.Modeling.big == 999999 + assert float(CONFIG.Modeling.epsilon) == 0.001 + + # Logging should keep default/previous values + assert CONFIG.Logging.level == original_level + + def test_config_attribute_modification(self): + """Test that config attributes can be modified directly.""" + # Store original values + original_big = CONFIG.Modeling.big + original_level = CONFIG.Logging.level + + # Modify attributes + CONFIG.Modeling.big = 12345678 + CONFIG.Modeling.epsilon = 1e-8 + CONFIG.Logging.level = 'DEBUG' + CONFIG.Logging.console = True + + # Verify modifications + assert CONFIG.Modeling.big == 12345678 + assert CONFIG.Modeling.epsilon == 1e-8 + assert CONFIG.Logging.level == 'DEBUG' + assert CONFIG.Logging.console is True + + # Reset + CONFIG.Modeling.big = original_big + CONFIG.Logging.level = original_level + CONFIG.Logging.console = False + + def test_logger_actually_logs(self, tmp_path): + """Test that the logger actually writes log messages.""" + log_file = tmp_path / 'actual_test.log' + CONFIG.Logging.file = str(log_file) + CONFIG.Logging.level = 'DEBUG' + CONFIG.apply() + + logger = logging.getLogger('flixopt') + test_message = 'Test log message from config test' + logger.debug(test_message) + + # Check that file was created and contains the message + assert log_file.exists() + log_content = log_file.read_text() + assert test_message in log_content + + def test_modeling_config_persistence(self): + """Test that Modeling config is independent of Logging config.""" + # Set custom modeling values + CONFIG.Modeling.big = 99999999 + CONFIG.Modeling.epsilon = 1e-8 + + # Change and apply logging config + CONFIG.Logging.console = True + CONFIG.apply() + + # Modeling values should be unchanged + assert CONFIG.Modeling.big == 99999999 + assert CONFIG.Modeling.epsilon == 1e-8 + + def test_config_reset(self): + """Test that CONFIG.reset() restores all defaults.""" + # Modify all config values + CONFIG.Logging.level = 'DEBUG' + CONFIG.Logging.console = False + CONFIG.Logging.rich = True + CONFIG.Logging.file = '/tmp/test.log' + CONFIG.Modeling.big = 99999999 + CONFIG.Modeling.epsilon = 1e-8 + CONFIG.Modeling.big_binary_bound = 500000 + CONFIG.config_name = 'test_config' + + # Reset should restore all defaults + CONFIG.reset() + + # Verify all values are back to defaults + assert CONFIG.Logging.level == 'INFO' + assert CONFIG.Logging.console is True + assert CONFIG.Logging.rich is False + assert CONFIG.Logging.file == 'flixopt.log' + assert CONFIG.Modeling.big == 10_000_000 + assert CONFIG.Modeling.epsilon == 1e-5 + assert CONFIG.Modeling.big_binary_bound == 100_000 + assert CONFIG.config_name == 'flixopt' + + # Verify logging was also reset + logger = logging.getLogger('flixopt') + assert logger.level == logging.INFO + assert any(isinstance(h, logging.handlers.RotatingFileHandler) for h in logger.handlers) + + def test_reset_matches_class_defaults(self): + """Test that reset() values match the _DEFAULTS constants. + + This ensures the reset() method and class attribute defaults + stay synchronized by using the same source of truth (_DEFAULTS). + """ + # Modify all values to something different + CONFIG.Logging.level = 'CRITICAL' + CONFIG.Logging.file = '/tmp/test.log' + CONFIG.Logging.rich = True + CONFIG.Logging.console = True + CONFIG.Modeling.big = 999999 + CONFIG.Modeling.epsilon = 1e-10 + CONFIG.Modeling.big_binary_bound = 999999 + CONFIG.config_name = 'modified' + + # Verify values are actually different from defaults + assert CONFIG.Logging.level != _DEFAULTS['logging']['level'] + assert CONFIG.Modeling.big != _DEFAULTS['modeling']['big'] + + # Now reset + CONFIG.reset() + + # Verify reset() restored exactly the _DEFAULTS values + assert CONFIG.Logging.level == _DEFAULTS['logging']['level'] + assert CONFIG.Logging.file == _DEFAULTS['logging']['file'] + assert CONFIG.Logging.rich == _DEFAULTS['logging']['rich'] + assert CONFIG.Logging.console == _DEFAULTS['logging']['console'] + assert CONFIG.Modeling.big == _DEFAULTS['modeling']['big'] + assert CONFIG.Modeling.epsilon == _DEFAULTS['modeling']['epsilon'] + assert CONFIG.Modeling.big_binary_bound == _DEFAULTS['modeling']['big_binary_bound'] + assert CONFIG.config_name == _DEFAULTS['config_name'] From 906fe9968f986ab1deee51c1e19f6a8df879bf30 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 9 Oct 2025 11:39:04 +0200 Subject: [PATCH 339/448] Update CHANGELOG.md --- CHANGELOG.md | 37 ++++++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70660c6dd..23ecb066b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm Formatting is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) & [Gitmoji](https://gitmoji.dev). For more details regarding the individual PRs and contributors, please refer to our [GitHub releases](https://github.com/flixOpt/flixopt/releases). +--- + + +## [v2.2.0] - 2025-10-11 +**Summary:** This release is a Configuration and Logging management release. + +### ✨ Added +- Added `CONFIG.reset()` method to restore configuration to default values +- Added configurable log file rotation settings: `CONFIG.Logging.max_file_size` and `CONFIG.Logging.backup_count` +- Added configurable log format settings: `CONFIG.Logging.date_format` and `CONFIG.Logging.format` +- Added configurable console settings: `CONFIG.Logging.console_width` and `CONFIG.Logging.show_path` +- Added `CONFIG.Logging.Colors` nested class for customizable log level colors using ANSI escape codes (works with both standard and Rich handlers) + +### ♻️ Changed +- Logging and Configuration management changed + +### 🗑️ Deprecated +- `change_logging_level()` function is now deprecated in favor of `CONFIG.Logging.level` and `CONFIG.apply()`. Will be removed in version 3.0.0. + +### 🔥 Removed +- Removed unused `config.merge_configs` function from configuration module + +### 👷 Development +- Greatly expanded test coverage for `config.py` module +- Added `@pytest.mark.xdist_group` to `TestConfigModule` tests to prevent global config interference + --- ## [2.1.11] - 2025-10-05 From f957ee83ed51a4c68cd0ea636ac8bcfbaea12a6b Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 9 Oct 2025 11:43:11 +0200 Subject: [PATCH 340/448] Fix CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23ecb066b..4ce346940 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,7 +67,7 @@ Please keep the format of the changelog consistent with the other releases, so t Until here --> -## [v2.2.0] - 2025-10-11 +## [2.2.0] - 2025-10-11 **Summary:** This release is a Configuration and Logging management release. ### ✨ Added From f3e765c39736385f96810668f9af73ea4b85dbc5 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 9 Oct 2025 13:21:54 +0200 Subject: [PATCH 341/448] Allow blank issues --- .github/ISSUE_TEMPLATE/config.yml | 2 +- .github/ISSUE_TEMPLATE/general_issue.yml | 40 ------------------------ CHANGELOG.md | 1 + 3 files changed, 2 insertions(+), 41 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/general_issue.yml diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index bc07496e8..94d96c479 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,4 +1,4 @@ -blank_issues_enabled: false +blank_issues_enabled: true contact_links: - name: 🤔 Modeling Questions url: https://github.com/flixOpt/flixopt/discussions/categories/q-a diff --git a/.github/ISSUE_TEMPLATE/general_issue.yml b/.github/ISSUE_TEMPLATE/general_issue.yml deleted file mode 100644 index f2578b9ce..000000000 --- a/.github/ISSUE_TEMPLATE/general_issue.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: 📝 General Issue -description: For issues that don't fit the specific templates below -title: "" -body: - - type: markdown - attributes: - value: | - **For specific issue types, please use the dedicated templates:** - - 🐛 **Bug Report** - Something is broken or not working as expected - - ✨ **Feature Request** - Suggest new functionality - - **For other topics, consider using Discussions instead:** - - 🤔 [Modeling Questions](https://github.com/flixOpt/flixopt/discussions/categories/q-a) - How to model specific energy systems - - ⚡ [Performance Help](https://github.com/flixOpt/flixopt/discussions/categories/performance) - Optimization speed and memory issues - - - type: textarea - id: issue-description - attributes: - label: Issue Description - description: Describe your issue, question, or concern - placeholder: | - Please describe: - - What you're trying to accomplish - - What's not working as expected - - Any relevant context or background - validations: - required: true - - - type: textarea - id: context - attributes: - label: Additional Context - description: Code examples, environment details, error messages, etc. - placeholder: | - Optional: Add any relevant details like: - - Code snippets - - Error messages - - Environment info - - Screenshots - render: markdown diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ce346940..df33609d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,7 @@ Please keep the format of the changelog consistent with the other releases, so t ### 📝 Docs ### 👷 Development +- Enable blank issues ### 🚧 Known Issues From b4dde0686e4743bcaf33fab221c7b7f95b852e28 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 13 Oct 2025 18:19:10 +0200 Subject: [PATCH 342/448] Add Multi-Period-modeling and stochastic modeling to flixopt (#348) * V3.0.0/main (#284) * Bugfix plot_node_balance_pie() * Scenarios/fixes (#252) * BUGFIX missing conversion to TimeSeries * BUGFIX missing conversion to TimeSeries * Bugfix node_balance with flow_hours: Negate correctly * Scenarios/filter (#253) * Add containts and startswith to filter_solution * Scenarios/drop suffix (#251) Drop suffixes in plots and add the option to drop suffixes to sanitize_dataset() * Scenarios/bar plot (#254) * Add stacked bar style to plotting methods * Rename mode to style (line, bar, area, ...) * Bugfix plotting * Fix example_calculation_types.py * Scenarios/fixes (#255) * Fix indexing issue with only one scenario * Bugfix Cooling Tower * Add option for balanced Storage Flows (equalize size of charging and discharging) * Add option for balanced Storage Flows * Change error to warning (non-fixed size with piecewise conversion AND fixed_flow_rate with OnOff) * Bugfix in DataConverter * BUGFIX: Typo (total_max/total_min in Effect) * Bugfix in node_balance() (negating did not work when using flow_hours mode * Scenarios/effects (#256) * Add methods to track effect shares of components and Flows * Add option to include flows when retrieving effects * Add properties and methods to store effect results in a dataset * Reorder methods * Rename and improve docs * Bugfix test class name * Fix the Network algorithm to calculate the sum of parallel paths, and be independent on nr of nodes and complexity of the network * Add tests for the newtork chaining and the results of effect shares * Add methods to check for circular references * Add test to check for circular references * Update cycle checker to return the found cycles * Add checks in results to confirm effects are computed correctly * BUGFIX: Remove +1 from prior testing * Add option for grouped bars to plotting.with_plotly() and make lines of stacked bar plots invisible * Reconstruct FlowSystem in CalculationResults on demand. DEPRECATION in CalculationResults * ruff check * Bugfix: save flow_system data, not the flow_system * Update tests * Scenarios/datasets results (#257) * Use dataarray instead of dataset * Change effects dataset to dataarray and use nan when no share was found * Add method for flow_rates dataset * Add methods to get flow_rates and flow_hours as datasets * Rename the dataarrays to the flow * Preserve index order * Improve filter_edges_dataset() * Simplify _create_flow_rates_dataarray() * Add dataset for sizes of Flows * Extend results structure to contain flows AND start/end infos * Add FlowResults Object * BUGFIX:Typo in _ElementResults.constraints * Add flows to results of Nodes * Simplify dataarray creation and improve FlowResults * Add nice docstrings * Improve filtering of flow results * Improve filtering of flow results. Add attribute of component * Add big dataarray with all variables but indexed * Revert "Add big dataarray with all variables but indexed" This reverts commit 08cd8a14fcf28248bf4a4c0a0fe1bae718269731. * Improve filtering method for coords filter and add error handling for restoring the flow system * Remove unnecessary methods in results .from_json() * Ensure consistent coord ordering in Effects dataarray * Rename get_effects_per_component() * Make effects_per_component() a dataset instead of a dataarray * Improve backwards compatability * ruff check * ruff check * Scenarios/deprecation (#258) * Deprecate .active_timesteps * Improve logger warning * Starting release notes * Bugfix in plausibility_check: Index 0 * Set bargap to 0 in stacked bars * Ensure the size is always properly indexed in results. * ruff check * BUGFIX in extract data, that causes coords in linopy to be incorrect (scalar xarray.DataArrays) * Improve yaml formatting for model documentation (#259) * Make the size/capacity a TimeSeries (#260) * Scenarios/plot network (#262) * Catch bug in plot_network with 2D arrays * Add plot_network() to test_io.py * Update deploy-docs.yaml: Run on Release publishing instead of creation and only run for stable releases (vx.y.z) * Bugfix DataConverter and add tests (#263) * Fix doc deployment to not publish on non stable releases * Remove unused code * Remove legend placing for better auto placing in plotly * Fix plotly dependency * Improve validation when adding new effects * Moved release notes to CHANGELOG.md * Try to add to_dataset to Elements * Remove TimeSeries * Remove TimeSeries * Rename conversion method to pattern: to_... * Move methods to FlowSystem * Drop nan values across time dimension if present * Allow lists of values to create DataArray * Update resolving of FlowSystem * Simplify TimeSeriesData * Move TImeSeriesData to Structure and simplyfy to inherrit from xarray.DataArray * Adjust IO * Move TimeSeriesData back to core.py and fix Conversion * Adjust IO to account for attrs of DataArrays in a Dataset * Rename transforming and connection methods in FlowSystem * Compacted IO methods * Remove infos() * remove from_dict() and to_dict() * Update __str__ of Interface * Improve str and repr * Improve str and repr * Add docstring * Unify IO stuff in Interface class * Improve test tu utilize __eq__ method * Make Interface class more robust and improve exceptions * Add option to copy Interfaces (And the FlowSystem) * Make a copy of a FLowSytsem that gets reused in a second Calculation * Remove test_timeseries.py * Reorganizing Datatypes * Remove TImeSeries and TimeSeriesCollection entirely * Remove old method * Add option to get structure with stats of dataarrays * Change __str__ method * Remove old methods * remove old imports * Add isel, sel and resample methods to FlowSystem * Remove need for timeseries with extra timestep * Simplify IO of FLowSystem * Remove parameter timesteps from IO * Improve Exceptions and Docstrings * Improve isel sel and resample methods * Change test * Bugfix * Improve * Improve * Add test for Storage Bounds * Add test for Storage Bounds * CHANGELOG.md * ruff check * Improve types * CHANGELOG.md * Bugfix in Storage * Revert changes in example_calculation_types.py * Revert changes in simple_example.py * Add convenient access to Elements in FlowSystem * Get Aggregated Calculation Working * Segmented running with wrong results * Use new persistent FLowSystem to create Calculations upfront * Improve SegmentedCalcualtion * Improve SegmentedCalcualtion * Fix SegmentedResults IO * ruff check * Update example * Updated logger essages to use .label_full instead of .label * Re-add parameters. Use deprecation warning instead * Update changelog * Improve warning message * Merge * Merge * Fit scenario weights to model coords when transforming * Merge * Removing logic between minimum, maximum and fixed size from InvestParameters * Remove selected_timesteps * Improve TypeHints * New property on InvestParameters for min/max/fixed size * Move logic for InvestParameters in Transmission to from Model to Interface * Make transformation of data more hierarchical (Flows after Components) * Add scenario validation * Change Transmission to have a "balanced" attribute. Change Tests accordingly * Improve index validations * rename method in tests * Update DataConverter * Add DataFrame Support back * Add copy() to DataConverter * Update fit_to_model_coords to take a list of coords * Make the DataConverter more universal by accepting a list of coords/dims * Update DataConverter for n-d arrays * Update DataConverter for n-d arrays * Add extra tests for 3-dims * Add FLowSystemDimension Type * Revert some logic about the fit_to_model coords * Adjust FLowSystem IO for scenarios * BUGFIX: Raise Exception instead of logging * Change usage of TimeSeriesData * Adjust logic to handle non scalars * Adjust logic to _resolve_dataarray_reference into separate method * Update IO of FlowSystem * Improve get_coords() * Adjust FlowSystem init for correct IO * Add scenario to sel and isel methods, and dont normalize scenario weights * Improve scenario_weights_handling * Add warning for not scaled weights * Update test_scenarios.py * Improve util method * Add objective to solution dataset. * Update handling of scenario_weights update tests * Ruff check. Fix type hints * Fix type hints and improve None handling * Fix coords in AggregatedCalculation * Improve Error Messages of DataConversion * Allow multi dim data conversion and broadcasting by length * Improve DataConverter to handle multi-dim arrays * Rename methods and remove unused code * Improve DataConverter by better splitting handling per datatype. Series only matches index (for one dim). Numpy matches shape * Add test for error handling * Update scenario example * Fix Handling of TimeSeriesData * Improve DataConverter * Fix resampling of the FlowSystem * Improve Warning Message * Add example that leverages resampling * Add example that leverages resampling adn fixing of Investments * Add flag to Calculation if its modeled * Make flag for connected_and_transformed FLowSystem public * Make Calcualtion Methods return themselfes to make them chainable * Improve example * Improve Unreleased CHANGELOG.md * Add year coord to FlowSystem * Improve dimension handling * Change plotting to use an indexer instead * Change plotting to use an indexer instead * Use tuples to set dimensions in Models * Bugfix in validation logic and test * Improve Errors * Improve weights handling and rescaling if None * Fix typehint * Update Broadcasting in Storage Bounds and improve type hints * Make .get_model_coords() return an actual xr.Coordinates Object * Improve get_coords() * Rename SystemModel to FlowSystemModel * First steps * Improve Feature Patterns * Improve acess to variables via short names * Improve * Add naming options to big_m_binary_bounds() * Fix and improve FLowModeling with Investment * Improve * Tyring to improve the Methods for bounding variables in different scenarios * Improve BoundingPatterns * Improve BoundingPatterns * Improve BoundingPatterns * Fix duration Modeling * Fix On + Size * Fix InvestmentModel * Fix Models * Update constraint names in test * Fix OnOffModel for multiple Flows * Update constraint names in tests * Simplify * Improve handling of vars/cons and models * Revising the basic structure of a class Model * Revising the basic structure of a class Model * Simplify and focus more on own Model class * Update tests * Improve state computation in ModelingUtilities * Improve handling of previous flowrates * Imropove repr and submodel acess * Update access pattern in tests * Fix PiecewiseEffects and StorageModel * Fix StorageModel and Remove PreventSimultaniousUseModel * Fix Aggregation and SegmentedCalculation * Update tests * Loosen precision in tests * Update test_on_hours_computation.py and some types * Rename class Model to Submodel * rename sub_model to submodel everywhere * rename self.model to self.submodel everywhere * Rename .model with .submodel if its only a submodel * Rename .sub_models with .submodels * Improve repr * Improve repr * Include def do_modeling() into __init__() of models * Make properties private * Improve Inheritance of Models * V3.0.0/plotting (#285) * Use indexer to reliably plot solutions with and wihtout scenarios/years * ruff check * Improve typehints * Update CHANGELOG.md * Bugfix from renaming to .submodel * Bugfix from renaming to .submodel * Improve indexer in results plotting * rename register_submodel() to .add_submodels() adn add SUbmodels collection class * Add nice repr to FlowSystemModel and Submodel * Bugfix .variables and .constraints * Add type checks to modeling.py * Improve assertion in tests * Improve docstrings and register ElementModels directly in FlowSystemModel * Improve __repr__() * ruff check * Use new method to compare sets in tests * ruff check * Update Contribute.md, some dependencies and add pre-commit * Pre commit hook * Run Pre-Commit Hook for the first time * Fix link in README.md * Update Effect name in tests to be 'costs' instead of 'Costs' Everywhere Simplify testing by creating a Element Library * Improve some of the modeling and coord handling * Add tests with years and scenarios * Update tests to run with multiple coords * Fix Effects dataset computation in case of empty effects * Update Test for multiple dims Fix Dim order in scaled_bounds_with_state Bugfix logic in .use_switch_on * Fix test with multiple dims * Fix test with multiple dims * New test * New test for previous flow_rates * V3.0.0/main fit to model coords improve (#295) * Change fit_to_model_coords to work with a Collection of dims * Improve fit_to_model_coords * Improve CHANGELOG.md * Update pyproject.toml * new ruff check * Merge branch 'main' into dev # Conflicts: # CHANGELOG.md # flixopt/network_app.py * Update CHANGELOG.md * Fix Error message * Revert changes * Feature/v3/update (#352) * Remove need for timeseries with extra timestep * Simplify IO of FLowSystem * Remove parameter timesteps from IO * Improve Exceptions and Docstrings * Improve isel sel and resample methods * Change test * Bugfix * Improve * Improve * Add test for Storage Bounds * Add test for Storage Bounds * CHANGELOG.md * ruff check * Improve types * CHANGELOG.md * Bugfix in Storage * Revert changes in example_calculation_types.py * Revert changes in simple_example.py * Add convenient access to Elements in FlowSystem * Get Aggregated Calculation Working * Segmented running with wrong results * Use new persistent FLowSystem to create Calculations upfront * Improve SegmentedCalcualtion * Improve SegmentedCalcualtion * Fix SegmentedResults IO * ruff check * Update example * Updated logger essages to use .label_full instead of .label * Re-add parameters. Use deprecation warning instead * Update changelog * Improve warning message * Merge * Merge * Fit scenario weights to model coords when transforming * Merge * Removing logic between minimum, maximum and fixed size from InvestParameters * Remove selected_timesteps * Improve TypeHints * New property on InvestParameters for min/max/fixed size * Move logic for InvestParameters in Transmission to from Model to Interface * Make transformation of data more hierarchical (Flows after Components) * Add scenario validation * Change Transmission to have a "balanced" attribute. Change Tests accordingly * Improve index validations * rename method in tests * Update DataConverter * Add DataFrame Support back * Add copy() to DataConverter * Update fit_to_model_coords to take a list of coords * Make the DataConverter more universal by accepting a list of coords/dims * Update DataConverter for n-d arrays * Update DataConverter for n-d arrays * Add extra tests for 3-dims * Add FLowSystemDimension Type * Revert some logic about the fit_to_model coords * Adjust FLowSystem IO for scenarios * BUGFIX: Raise Exception instead of logging * Change usage of TimeSeriesData * Adjust logic to handle non scalars * Adjust logic to _resolve_dataarray_reference into separate method * Update IO of FlowSystem * Improve get_coords() * Adjust FlowSystem init for correct IO * Add scenario to sel and isel methods, and dont normalize scenario weights * Improve scenario_weights_handling * Add warning for not scaled weights * Update test_scenarios.py * Improve util method * Add objective to solution dataset. * Update handling of scenario_weights update tests * Ruff check. Fix type hints * Fix type hints and improve None handling * Fix coords in AggregatedCalculation * Improve Error Messages of DataConversion * Allow multi dim data conversion and broadcasting by length * Improve DataConverter to handle multi-dim arrays * Rename methods and remove unused code * Improve DataConverter by better splitting handling per datatype. Series only matches index (for one dim). Numpy matches shape * Add test for error handling * Update scenario example * Fix Handling of TimeSeriesData * Improve DataConverter * Fix resampling of the FlowSystem * Improve Warning Message * Add example that leverages resampling * Add example that leverages resampling adn fixing of Investments * Add flag to Calculation if its modeled * Make flag for connected_and_transformed FLowSystem public * Make Calcualtion Methods return themselfes to make them chainable * Improve example * Improve Unreleased CHANGELOG.md * Add year coord to FlowSystem * Improve dimension handling * Change plotting to use an indexer instead * Change plotting to use an indexer instead * Use tuples to set dimensions in Models * Bugfix in validation logic and test * Improve Errors * Improve weights handling and rescaling if None * Fix typehint * Update Broadcasting in Storage Bounds and improve type hints * Make .get_model_coords() return an actual xr.Coordinates Object * Improve get_coords() * Rename SystemModel to FlowSystemModel * First steps * Improve Feature Patterns * Improve acess to variables via short names * Improve * Add naming options to big_m_binary_bounds() * Fix and improve FLowModeling with Investment * Improve * Tyring to improve the Methods for bounding variables in different scenarios * Improve BoundingPatterns * Improve BoundingPatterns * Improve BoundingPatterns * Fix duration Modeling * Fix On + Size * Fix InvestmentModel * Fix Models * Update constraint names in test * Fix OnOffModel for multiple Flows * Update constraint names in tests * Simplify * Improve handling of vars/cons and models * Revising the basic structure of a class Model * Revising the basic structure of a class Model * Simplify and focus more on own Model class * Update tests * Improve state computation in ModelingUtilities * Improve handling of previous flowrates * Imropove repr and submodel acess * Update access pattern in tests * Fix PiecewiseEffects and StorageModel * Fix StorageModel and Remove PreventSimultaniousUseModel * Fix Aggregation and SegmentedCalculation * Update tests * Loosen precision in tests * Update test_on_hours_computation.py and some types * Rename class Model to Submodel * rename sub_model to submodel everywhere * rename self.model to self.submodel everywhere * Rename .model with .submodel if its only a submodel * Rename .sub_models with .submodels * Improve repr * Improve repr * Include def do_modeling() into __init__() of models * Make properties private * Improve Inheritance of Models * V3.0.0/plotting (#285) * Use indexer to reliably plot solutions with and wihtout scenarios/years * ruff check * Improve typehints * Update CHANGELOG.md * Bugfix from renaming to .submodel * Bugfix from renaming to .submodel * Improve indexer in results plotting * rename register_submodel() to .add_submodels() adn add SUbmodels collection class * Add nice repr to FlowSystemModel and Submodel * Bugfix .variables and .constraints * Add type checks to modeling.py * Improve assertion in tests * Improve docstrings and register ElementModels directly in FlowSystemModel * Improve __repr__() * ruff check * Use new method to compare sets in tests * ruff check * Update Contribute.md, some dependencies and add pre-commit * Pre commit hook * Run Pre-Commit Hook for the first time * Fix link in README.md * Update Effect name in tests to be 'costs' instead of 'Costs' Everywhere Simplify testing by creating a Element Library * Improve some of the modeling and coord handling * Add tests with years and scenarios * Update tests to run with multiple coords * Fix Effects dataset computation in case of empty effects * Update Test for multiple dims Fix Dim order in scaled_bounds_with_state Bugfix logic in .use_switch_on * Fix test with multiple dims * Fix test with multiple dims * New test * New test for previous flow_rates * Add Model for YearAwareInvestments * Add FlowSystem.years_per_year attribute and "years_of_last_year" parameter to FlowSystem() * Add YearAwareInvestmentModel * Add new Interface * Improve YearAwareInvestmentModel * Rename and improve * Move piecewise_effects * COmbine TImingInvestment into a single interface * Add model tests for investment * Add size_changes variables * Add size_changes variables * Improve InvestmentModel * Improve InvestmentModel * Rename parameters * remove old code * Add a duration_in_years to the InvestTimingParameters * Improve handling of fixed_duration * Improve validation and make Investment/divestment optional by default * Rename some vars and improve previous handling * Add validation for previous size * Change fit_to_model_coords to work with a Collection of dims * Improve fit_to_model_coords * Improve test * Update transform_data() * Add new "year of investment" coord to FlowSystem * Add 'year_of_investment' dimension to FlowSystem * Improve InvestmentTiming * Improve InvestmentTiming * Add specific_effect back * add effects_by_investment_year back * Add year_of_investment to FLowSystem.sel() * Improve Interface * Handle selection of years properly again * Temp * Make ModelingPrimitives.consecutive_duration_tracking() dim-agnostic * Use new lifetime variable and constraining methods * Improve Plausibility check * Improve InvestmentTImingParameters * Improve weights * Adjust test * Remove old classes * V3.0.0/main fit to model coords improve (#295) * Change fit_to_model_coords to work with a Collection of dims * Improve fit_to_model_coords * ruff format * Revert changes * Update type hints * Revert changes introduced by new Multiperiod Invest parameters * Improve CHnagelog and docstring of Storage * Improve Changelog * Improve InvestmentModel * Improve InvestmentModel to have 2 cases. One without years and one with * Improve InvestmentModel to have 2 cases. One without years and one with years. Further, remove investment_scenarios parameter * Revert some changes regarding Investments * Typo * Remove Investment test file (only local testing) * More reverted changes * More reverted changes * Add years_of_last_year to docstring * Revert change from Investment * Revert change from Investment * Remove old todos.txt file * Fix typos in CHANGELOG.md * Improve usage of name_prefix to intelligently join with the label * Ensure IO of years_of_last_year * Typo * Typo * activat tests on pulls to feature/v3 * activat tests on pulls to feature/v3/main * Feature/v3/low-impact-improvements (#355) * Fix typo * Prefer robust scalar extraction for timestep sizes in aggregation * Improve docs and error messages * Update examples * Use validated timesteps * Remove unnessesary import * Use FlowSystem.model instead of FlowSystem.submodel * Fix Error message * Improve CHANGELOG.md * Use self.standard_effect instead of provate self._standard_effect and update docstring * in calculate_all_conversion_paths, use `collections.deque` for efficiency on large graphs * Make aggregation_parameters.hours_per_period more robust by using rounding * Improve import and typos * Improve docstring * Use validated timesteps * Improve error * Improve warning * Improve type hint * Improve CHANGELOG.md: typos, wording and duplicate entries * Improve CI (#357) Separate example testing from other tests by marking them. By default, purest doesn't run the example tests * Feature/v3/data converter (#356) * Update DataConverter * Update tests of error messages * Update tests of error messages * Update Dataconverter to allow bool values * fix tests * Improve code order of prefix in transform_data() * Move pytest-xdist to dev deps * Fix transform_data to not pass a prefix to flow * Move to unreleased Add emojis to CHANGELOG.md * Feature/v3/feature/308 rename effect domains (#365) * Rename effect domains * Rename effect domains * Ensure backwards compatability * Improve * Improve * Bugfix IO with deprectaed params * Add guards for extra kwargs * Add guards for extra kwargs * centralize logic for deprectaed params * Move handlign from centralized back to classes in a dedicated method * Improce property handling * Move handling to Interface class * Getting lost * Revert "Getting lost" This reverts commit 3c0db766a5770f5e19fe07459f46b44d8f8952ee. * Revert "Move handling to Interface class" This reverts commit 09bdeec2362f773af5e8e2bf11e2c6703d0b693b. * Revert "Improce property handling" This reverts commit 5fe2c6461a56cb9c68c5b419e7db2d00f443ff1a. * Revert "Move handlign from centralized back to classes in a dedicated method" This reverts commit 9f4c1f6acddd50b60199fd67ea6be55491752faa. * Revert "centralize logic for deprectaed params" This reverts commit 4a8257422e94dbdfe3c4249ad70e4642e79122a1. * Add "" to warnings * Revert change in examples * Improve BackwardsCompatibleDataset * Add unit tests for backwards compatability * Remove backwards compatible dataset * Renamed maximum_temporal_per_hour to maximum_per_hour and minimum_temporal_per_hour to minimum_per_hour * Add entires to CHANGELOG.md * Remove backwards compatible dataset * Remove unused imports * Move to unreleased * Catch up on missed renamings from merge * Catch up on missed renamings from merge * Typo * Typo * Several small improvements or potential future bug preventions * Feature/v3/feature/305 rename specific share to other effects to specific share from effect (#366) * Step 1 * Bugfix * Make fit_effects_to_model_coords() more flexible * Fix dims * Update conftest.py * Typos * Improve Effect examples * Add extra validation for Effect Shares * Feature/v3/feature/367 rename year dimension to period (#370) * The framework now uses "period" instead of "year" as the dimension name and "periodic" instead of "nontemporal" for the effect domain * Update CHANGELOG.md * Remove periods_of_last_period parameter and adjust weights calculation * Bugfix * Bugfix * Switch from "as_time_series": bool to "dims": [time, period, scenario] arguments * Improve normalization of weights * Update tests * Typos in docs * Improve docstrings * Improve docstrings * Update CHANGELOG.md * Improved tests: added extra time+scenarios combination * Add rename and improve CHANGELOG.md * Made CHANGELOG.md more concise * Simplify array summation and improve `np.isclose` usage in `modeling` and `aggregation` modules. * Make storage and load profile methods flexible by introducing `timesteps_length` parameter; update test configurations accordingly. * Refine error messages in `ModelingPrimitives` to correctly reference updated method names. * Enhance test fixtures by adding `ids` for parameterized tests, improve input flexibility with dynamic timestep length, and refine error message sorting logic. * Refactor variable selection and constraint logic in `aggregation.py` for handling more than only a time dimension * Adjust constraint in `aggregation.py` to enforce stricter summation limit (1 instead of 1.1) * Reverse transition constraint inequality for consistency in `modeling.py`. * Update dependency to use h5netcdf instead of netcdf4 * Feature/v3/several improvements (#372) * Update deprecated properties to use new aggregation attributes in `core.py`. * Refactor `drop_constant_arrays` in `core.py` to improve clarity, add type hints, and enhance logging for dropped variables. * Bugfix example_calculation_types.py and two_stage_optimization.py * Use time selection more explicitly * Refactor plausibility checks in `components.py` to handle string-based `initial_charge_state` more robustly and simplify capacity bounds retrieval using `InvestParameters`. * Refactor `create_transmission_equation` in `components.py` to handle `relative_losses` gracefully when unset and simplify the constraint definition. * Update pytest `addopts` formatting in `pyproject.toml` to work with both unix and windows * Refine null value handling when resolving dataarrays` to check for 'time' dimension before dropping all-null values. * Refactor flow system restoration to improve exception handling and ensure logger state resets. * Refactor imports in `elements.py` to remove unused `ModelingPrimitives` from `features` and include it from `modeling` instead. * Refactor `count_consecutive_states` in `modeling.py` to enhance documentation, improve edge case handling, and simplify array processing. * Refactor `drop_constant_arrays` to handle NaN cases with `skipna` and sort dropped variables for better logging; streamline logger state restoration in `results.py`. * Temp * Improve NAN handling in count_consecutive_states() * Refactor plausibility checks in `components.py` to prevent initial capacity from constraining investment decisions and improve error messaging. * Feature/v3/feature/no warnings in tests (#373) * Refactor examples to not use deprectaed patterns * Refactor tests to replace deprecated `sink`/`source` properties with `inputs`/`outputs` in component definitions. * Use 'h' instead of deprectaed 'H' in coordinate freq in tests; adjust `xr.concat` in `results.py` to use `join='outer'` for safer merging. * Refactor plot tests to use non-interactive backends, save plots as files, and close figures to prevent memory leaks. * Refactor plot tests to use non-interactive Plotly renderer (`json`), add cleanup with `tearDown`, and ensure compatibility with non-interactive Matplotlib backends. * Configure pytest filters to treat most warnings as errors, ignore specific third-party warnings, and display all warnings from internal code. * Revert "Configure pytest filters to treat most warnings as errors, ignore specific third-party warnings, and display all warnings from internal code." This reverts commit 0928b2632a1193e7703e72d56d666041d52bfcf9. * Refactor plotting logic to prevent memory leaks, improve backend handling, and add test fixtures for cleanup and non-interactive configurations. * Update pytest filterwarnings to treat most warnings as errors, ignore specific third-party warnings, and display all internal warnings. * Suppress specific third-party warnings in `__init__.py` to reduce noise for end users. * Update pytest warning filters: treat internal warnings as errors, revert treating most third-party warnings as errors. * Suppress additional third-party warnings in `__init__.py` to minimize runtime noise. * Update pytest warning filters: suppress specific third-party warnings and add detailed context for `__init__.py` filters. * Sync and consolidate third-party warning filters in `__init__.py` and `pyproject.toml` to suppress runtime noise effectively. * Expand and clarify third-party warning filters in `__init__.py` and `pyproject.toml` for improved runtime consistency and reduced noise. * Update deprecated code in tests * Refactor backend checks in `plotting.py` and streamline test fixtures for consistency in handling non-interactive backends. * Refactor plotting logic to handle test environments explicitly, remove unused Plotly configuration, and improve figure cleanup in tests. * Add entry to CHANGELOG.md * Typos in example * Reogranize Docs (#377) * Improve effects parameter naming in InvestParameters (#389) * FIrst Try * Improve deprecation * Update usage of deprectated parameters * Improve None handling * Add extra kwargs handling * Improve deprecation * Use custom method for kwargs * Add deprecation method * Apply deprecation method to other classes * Apply to effects.py as well * Update usage of deprectaed parameters * Update CHANGELOG.md * Update Docs * Feature/v3/feature/test examples dependent (#390) * Update example test to run dependent examples in order * Update example test to run dependent examples in order * Update CHANGELOG.md * Improve test directory handling * Improve test directory handling * Typo * Feature/v3/feature/rename investparameter optional to mandatory (#392) * Change .optional to .mandatory * Change .optional to .mandatory * Remove not needed properties * Improve deprectation warnings * Improve deprectation of "optional" * Remove all usages of old "optional" parameter in code * Typo * Imrpove readability * Adjust some logging levels * Add scenarios and periods to repr and str of FlowSystem * Feature/v3/feature/386 use better default logging colors and dont log to file by default (#394) * Fix `charge_state` Constraint in `Storage` leading to incorrect losses in discharge and therefore incorrect charge states and discharge values (#347) * Fix equation in Storage * Fix test for equation in Storage * Update CHANGELOG.md * Improve Changelog Message * Fix CHANGELOG.md * Simplify changes from next release * Update CHANGELOG.md * Fix CHANGELOG.md * chore(deps): update dependency mkdocs-material to v9.6.20 (#369) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Improve renovate.json to automerge ruff despite 0.x version * chore(deps): update dependency tsam to v2.3.9 (#379) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency ruff to v0.13.2 (#378) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Feature/Improve Configuration options and handling (#385) * Refactor configuration management: remove dataclass-based schema and simplify CONFIG structure. * Refactor configuration loading: switch from `os` to `pathlib`, streamline YAML loading logic. * Refactor logging setup: split handler creation into dedicated functions, simplify configuration logic. * Improve logging configurability and safety - Add support for `RotatingFileHandler` to prevent large log files. - Introduce `console` flag for optional console logging. - Default to `NullHandler` when no handlers are configured for better library behavior. * Temp * Temp * Temp * Temp * Temp * Temp * Refactor configuration and logging: remove unused `merge_configs` function, streamline logging setup, and encapsulate `_setup_logging` as an internal function. * Remove unused `change_logging_level` import and export. * Add tests for config.py * Expand `config.py` test coverage: add tests for custom config loading, logging setup, dict roundtrip, and attribute modification. * Expand `test_config.py` coverage: add modeling config persistence test, refine logging reset, and improve partial config load assertions. * Expand `test_config.py` coverage: add teardown for state cleanup and reset modeling config in setup. * Add `CONFIG.reset()` method and expand test coverage to verify default restoration * Refactor `CONFIG` to centralize defaults in `_DEFAULTS` and ensure `reset()` aligns with them; add test to verify consistency. * Refactor `_DEFAULTS` to use `MappingProxyType` for immutability, restructure config hierarchy, and simplify `reset()` implementation for maintainability; update tests accordingly. * Mark `TestConfigModule` tests to run in a single worker with `@pytest.mark.xdist_group` to prevent global config interference. * Add default log file * Update CHANGELOG.md * Readd change_logging_level() for backwards compatability * Add more options to config.py * Add a docstring to config.y * Add a docstring to config.y * rename parameter message_format * Improve color config * Improve color config * Update CHANGELOG.md * Improve color handling * Improve color handling * Remove console Logging explicityl from examples * Make log to console the default * Make log to console the default * Add individual level parameters for console and file * Add extra Handler section * Use dedicated levels for both handlers * Switch back to not use Handlers * Revert "Switch back to not use Handlers" This reverts commit 05bbccb5d3d750e1b972799ae004d433bb798406. * Revert "Use dedicated levels for both handlers" This reverts commit ed0542bcb0db2d36e5aaec9f1f1e38aa5bc0c6b2. * Revert "Add extra Handler section" This reverts commit a133cc87c3567f0d4e40b43f60943afe7f6b9aaa. * Revert "Add individual level parameters for console and file" This reverts commit 19f81c9e05065de7dcf74bf4750f653d51a2ecbe. * Fix CHANGELOG.md * Update CHANGELOG.md * Fix CHANGELOG.md * Allow blank issues * Change default logging behaviour to other colors and no file logging * Use white for INFO * Use terminal default for INFO * Explicitly use stdout for StreamHandler * Use terminal default for Logging color * Add option for loggger name * Update CHANGELOG.md * Ensure custom formats are being applied * Catch empty config files * Update test to match new defaults --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Fix warnings filter * Remove config file (#391) * Remove config file * Remove yaml in MANIFEST.in * Improve config console logger: Allow stderr and improve multiline format (#395) * Add some validation to config.py * Improve file Permission handling in config.py * Remove unwanted return * Improve Docstrings in config.py * Improve Docstrings in config.py * Typo * Use code block in docstring * Allow stderr for console logging * Make docstrings more compact * Make docstrings more compact * Updated to actually use stderr * Simplify format() * Improve format * Add extra validation * Update CHANGELOG.md * Feature/v3/feature/381 feature equalize sizes and or flow rates between scenarios (#396) * First try * Centralize in FlowSystem * Add centralized handling * Logical Bug * Add to IO * Add test * Add some error handling and logging * Rename variable * Change parameter naming * Remove not needed method * Refactor to reduce duplication * Change defaults * Change defaults * Change defaults * Update docs * Update docs * Update docs * Update docs * Feature/v3/feature/Linked investments over multiple periods * Reorganize InvestmentParameters to always create the binary investment variable * Add new variable that indicates wether investment was taken, independent of period and allow linked periods * Improve Handling of linked periods * Improve Handling of linked periods * Add examples * Typos * Fix: reference invested only after it exists * Improve readbility of equation * Update from Merge * Improve InvestmentModel * Improve readability * Improve readability and reorder methods * Improve logging * Improve InvestmentModel * Rename to "invested" * Update CHANGELOG.md * Bugfix * Improve docstring * Improve InvestmentModel to be more inline with the previous Version * Improve Exceptions and add a meaningfull comment in InvestParameters * Typo * Feature/v3/feature/common resources in examples (#401) * Typo * Typos in scenario_example.py * Improve data files in examples * Improve data files in examples * Handle local install more gracefully with __version__ * Remove bad example * Increase timeout in examples * Improve test_examples.py * Improve example * Fixx Error message in test * Fix: Dependecy issue with python 3.10 * run ci on more branches if there are prs * Minor improvements and Update to the CHANGELOG.md * Feature/v3/feature/last minute improvements (#403) * Typos oin CHANGELOG.md * Add error handling in exmaple * Surface warnings during tests (avoid hiding deprecations) * Add missing docs file * Imrpve Releasnotes of v2.2.0 * Improve docs * Remove some filterwarnings from tsam --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/python-app.yaml | 4 +- CHANGELOG.md | 147 +- MANIFEST.in | 2 +- README.md | 128 +- docs/SUMMARY.md | 7 - docs/images/flixopt-icon.svg | 2 +- docs/index.md | 81 +- .../Effects, Penalty & Objective.md | 132 -- docs/user-guide/Mathematical Notation/Flow.md | 26 - .../Mathematical Notation/InvestParameters.md | 3 - .../Mathematical Notation/LinearConverter.md | 21 - .../Mathematical Notation/OnOffParameters.md | 3 - .../user-guide/Mathematical Notation/index.md | 22 - docs/user-guide/index.md | 43 +- .../mathematical-notation/dimensions.md | 264 ++++ .../effects-penalty-objective.md | 286 ++++ .../elements}/Bus.md | 16 + .../mathematical-notation/elements/Flow.md | 64 + .../elements/LinearConverter.md | 50 + .../elements}/Storage.md | 41 +- .../features/InvestParameters.md | 302 ++++ .../features/OnOffParameters.md | 307 ++++ .../features}/Piecewise.md | 0 .../user-guide/mathematical-notation/index.md | 123 ++ .../modeling-patterns/bounds-and-states.md | 165 ++ .../modeling-patterns/duration-tracking.md | 159 ++ .../modeling-patterns/index.md | 54 + .../modeling-patterns/state-transitions.md | 227 +++ .../others.md | 0 docs/user-guide/recipes/index.md | 47 + examples/00_Minmal/minimal_example.py | 9 +- examples/01_Simple/simple_example.py | 20 +- examples/02_Complex/complex_example.py | 67 +- .../02_Complex/complex_example_results.py | 3 + .../example_calculation_types.py | 57 +- examples/04_Scenarios/scenario_example.py | 144 ++ .../two_stage_optimization.py | 179 +++ .../Zeitreihen2020.csv | 0 flixopt/__init__.py | 36 +- flixopt/aggregation.py | 79 +- flixopt/calculation.py | 381 +++-- flixopt/components.py | 699 +++++---- flixopt/config.py | 418 ++--- flixopt/core.py | 1362 +++++++---------- flixopt/effects.py | 705 +++++++-- flixopt/elements.py | 654 ++++---- flixopt/features.py | 1270 +++++---------- flixopt/flow_system.py | 905 ++++++++--- flixopt/interface.py | 450 ++++-- flixopt/io.py | 151 +- flixopt/linear_converters.py | 28 +- flixopt/modeling.py | 759 +++++++++ flixopt/plotting.py | 140 +- flixopt/results.py | 1065 +++++++++++-- flixopt/structure.py | 1320 +++++++++++----- flixopt/utils.py | 53 +- mkdocs.yml | 53 +- pics/flixopt-icon.svg | 2 +- pyproject.toml | 30 +- scripts/gen_ref_pages.py | 17 +- tests/conftest.py | 843 +++++++--- tests/run_all_tests.py | 2 +- tests/test_bus.py | 59 +- tests/test_component.py | 625 ++++++-- tests/test_config.py | 16 +- tests/test_cycle_detection.py | 200 +++ tests/test_dataconverter.py | 1351 ++++++++++++++-- tests/test_effect.py | 373 +++-- tests/test_effects_shares_summation.py | 225 +++ tests/test_examples.py | 81 +- tests/test_flow.py | 1106 +++++++------ tests/test_functional.py | 174 +-- tests/test_integration.py | 169 +- tests/test_invest_parameters_deprecation.py | 344 +++++ tests/test_io.py | 26 +- tests/test_linear_converter.py | 111 +- tests/test_on_hours_computation.py | 105 +- tests/test_plots.py | 59 +- tests/test_results_plots.py | 7 +- tests/test_scenarios.py | 692 +++++++++ tests/test_storage.py | 206 ++- tests/test_timeseries.py | 603 -------- tests/todos.txt | 5 - 83 files changed, 14804 insertions(+), 6360 deletions(-) delete mode 100644 docs/SUMMARY.md delete mode 100644 docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md delete mode 100644 docs/user-guide/Mathematical Notation/Flow.md delete mode 100644 docs/user-guide/Mathematical Notation/InvestParameters.md delete mode 100644 docs/user-guide/Mathematical Notation/LinearConverter.md delete mode 100644 docs/user-guide/Mathematical Notation/OnOffParameters.md delete mode 100644 docs/user-guide/Mathematical Notation/index.md create mode 100644 docs/user-guide/mathematical-notation/dimensions.md create mode 100644 docs/user-guide/mathematical-notation/effects-penalty-objective.md rename docs/user-guide/{Mathematical Notation => mathematical-notation/elements}/Bus.md (78%) create mode 100644 docs/user-guide/mathematical-notation/elements/Flow.md create mode 100644 docs/user-guide/mathematical-notation/elements/LinearConverter.md rename docs/user-guide/{Mathematical Notation => mathematical-notation/elements}/Storage.md (52%) create mode 100644 docs/user-guide/mathematical-notation/features/InvestParameters.md create mode 100644 docs/user-guide/mathematical-notation/features/OnOffParameters.md rename docs/user-guide/{Mathematical Notation => mathematical-notation/features}/Piecewise.md (100%) create mode 100644 docs/user-guide/mathematical-notation/index.md create mode 100644 docs/user-guide/mathematical-notation/modeling-patterns/bounds-and-states.md create mode 100644 docs/user-guide/mathematical-notation/modeling-patterns/duration-tracking.md create mode 100644 docs/user-guide/mathematical-notation/modeling-patterns/index.md create mode 100644 docs/user-guide/mathematical-notation/modeling-patterns/state-transitions.md rename docs/user-guide/{Mathematical Notation => mathematical-notation}/others.md (100%) create mode 100644 docs/user-guide/recipes/index.md create mode 100644 examples/04_Scenarios/scenario_example.py create mode 100644 examples/05_Two-stage-optimization/two_stage_optimization.py rename examples/{03_Calculation_types => resources}/Zeitreihen2020.csv (100%) create mode 100644 flixopt/modeling.py create mode 100644 tests/test_cycle_detection.py create mode 100644 tests/test_effects_shares_summation.py create mode 100644 tests/test_invest_parameters_deprecation.py create mode 100644 tests/test_scenarios.py delete mode 100644 tests/todos.txt diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml index 9002ce54c..8e445974c 100644 --- a/.github/workflows/python-app.yaml +++ b/.github/workflows/python-app.yaml @@ -5,7 +5,7 @@ on: branches: [main] # Only main branch tags: ['v*.*.*'] pull_request: - branches: [main, dev] + branches: [main, 'dev*', 'dev/**', 'feature/**'] types: [opened, synchronize, reopened] paths-ignore: - 'docs/**' @@ -88,7 +88,7 @@ jobs: uv pip install --system .[dev] - name: Run tests - run: pytest -v -p no:warnings --numprocesses=auto + run: pytest -v --numprocesses=auto test-examples: runs-on: ubuntu-24.04 diff --git a/CHANGELOG.md b/CHANGELOG.md index df33609d8..b5de41f19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ Please keep the format of the changelog consistent with the other releases, so t --- + ## [Unreleased] - ????-??-?? ### ✨ Added @@ -62,12 +63,149 @@ Please keep the format of the changelog consistent with the other releases, so t ### 📝 Docs ### 👷 Development -- Enable blank issues ### 🚧 Known Issues +--- Until here --> +## [3.0.0] - 2025-10-13 +**Summary**: This release introduces new model dimensions (periods and scenarios) for multi-period investments and stochastic modeling, along with a redesigned effect sharing system and enhanced I/O capabilities. + +### ✨ Added + +**New model dimensions:** + +- **Period dimension**: Enables multi-period investment modeling with distinct decisions in each period for transformation pathway optimization +- **Scenario dimension**: Supports stochastic modeling with weighted scenarios for robust decision-making under uncertainty (demand, prices, weather) + - Control variable independence across scenarios via `scenario_independent_sizes` and `scenario_independent_flow_rates` parameters + - By default, investment sizes are shared across scenarios while flow rates vary per scenario + +**Redesigned effect sharing system:** + +Effects now use intuitive `share_from_*` syntax that clearly shows contribution sources: + +```python +costs = fx.Effect('costs', '€', 'Total costs', + share_from_temporal={'CO2': 0.2}, # From temporal effects + share_from_periodic={'land': 100}) # From periodic effects +``` + +This replaces `specific_share_to_other_effects_*` parameters and inverts the direction for clearer relationships. + +**Enhanced I/O and data handling:** + +- NetCDF/JSON serialization for all Interface objects and FlowSystem with round-trip support +- FlowSystem manipulation: `sel()`, `isel()`, `resample()`, `copy()`, `__eq__()` methods +- Direct access to FlowSystem from results without manual restoring (lazily loaded) +- New `FlowResults` class and precomputed DataArrays for sizes/flow_rates/flow_hours +- `effects_per_component` dataset for component impact evaluation, including all indirect effects through effect shares + +**Other additions:** + +- Balanced storage - charging and discharging sizes can be forced equal via `balanced` parameter +- New Storage parameters: `relative_minimum_final_charge_state` and `relative_maximum_final_charge_state` for final state control +- Improved filter methods in results +- Example for 2-stage investment decisions leveraging FlowSystem resampling + +### 💥 Breaking Changes + +- `relative_minimum_charge_state` and `relative_maximum_charge_state` don't have an extra timestep anymore. +- Renamed class `SystemModel` to `FlowSystemModel` +- Renamed class `Model` to `Submodel` +- Renamed `mode` parameter in plotting methods to `style` +- Renamed investment binary variable `is_invested` to `invested` in `InvestmentModel` +- `Calculation.do_modeling()` now returns the `Calculation` object instead of its `linopy.Model`. Callers that previously accessed the linopy model directly should now use `calculation.do_modeling().model` instead of `calculation.do_modeling()`. + +### ♻️ Changed + +- FlowSystems cannot be shared across multiple Calculations anymore. A copy of the FlowSystem is created instead, making every Calculation independent +- Each Subcalculation in `SegmentedCalculation` now has its own distinct `FlowSystem` object +- Type system overhaul - added clear separation between temporal and non-temporal data throughout codebase for better clarity +- Enhanced FlowSystem interface with improved `__repr__()` and `__str__()` methods +- Improved Model Structure - Views and organisation is now divided into: + - Model: The main Model (linopy.Model) that is used to create and store the variables and constraints for the FlowSystem. + - Submodel: The base class for all submodels. Each is a subset of the Model, for simpler access and clearer code. +- Made docstrings in `config.py` more compact and easier to read +- Improved format handling in configuration module +- Enhanced console output to support both `stdout` and `stderr` stream selection +- Added `show_logger_name` parameter to `CONFIG.Logging` for displaying logger names in messages + +### 🗑️ Deprecated + +- The `agg_group` and `agg_weight` parameters of `TimeSeriesData` are deprecated and will be removed in a future version. Use `aggregation_group` and `aggregation_weight` instead. +- The `active_timesteps` parameter of `Calculation` is deprecated and will be removed in a future version. Use the new `sel(time=...)` method on the FlowSystem instead. +- The assignment of Bus Objects to Flow.bus is deprecated and will be removed in a future version. Use the label of the Bus instead. +- The usage of Effects objects in Dicts to assign shares to Effects is deprecated and will be removed in a future version. Use the label of the Effect instead. +- **InvestParameters** parameters renamed for improved clarity around investment and retirement effects: + - `fix_effects` → `effects_of_investment` + - `specific_effects` → `effects_of_investment_per_size` + - `divest_effects` → `effects_of_retirement` + - `piecewise_effects` → `piecewise_effects_of_investment` +- **Effect** parameters renamed: + - `minimum_investment` → `minimum_periodic` + - `maximum_investment` → `maximum_periodic` + - `minimum_operation` → `minimum_temporal` + - `maximum_operation` → `maximum_temporal` + - `minimum_operation_per_hour` → `minimum_per_hour` + - `maximum_operation_per_hour` → `maximum_per_hour` +- **Component** parameters renamed: + - `Source.source` → `Source.outputs` + - `Sink.sink` → `Sink.inputs` + - `SourceAndSink.source` → `SourceAndSink.outputs` + - `SourceAndSink.sink` → `SourceAndSink.inputs` + - `SourceAndSink.prevent_simultaneous_sink_and_source` → `SourceAndSink.prevent_simultaneous_flow_rates` + +### 🔥 Removed + +- **Effect share parameters**: The old `specific_share_to_other_effects_*` parameters were replaced WITHOUT DEPRECATION + - `specific_share_to_other_effects_operation` → `share_from_temporal` (with inverted direction) + - `specific_share_to_other_effects_invest` → `share_from_periodic` (with inverted direction) + +### 🐛 Fixed + +- Enhanced NetCDF I/O with proper attribute preservation for DataArrays +- Improved error handling and validation in serialization processes +- Better type consistency across all framework components +- Added extra validation in `config.py` to improve error handling + +### 📝 Docs + +- Reorganized mathematical notation docs: moved to lowercase `mathematical-notation/` with subdirectories (`elements/`, `features/`, `modeling-patterns/`) +- Added comprehensive documentation pages: `dimensions.md` (time/period/scenario), `effects-penalty-objective.md`, modeling patterns +- Enhanced all element pages with implementation details, cross-references, and "See Also" sections +- Rewrote README and landing page with clearer vision, roadmap, and universal applicability emphasis +- Removed deprecated `docs/SUMMARY.md`, updated `mkdocs.yml` for new structure +- Tightened docstrings in core modules with better cross-referencing +- Added recipes section to docs + +### 🚧 Known Issues + +- IO for single Interfaces/Elements to Datasets might not work properly if the Interface/Element is not part of a fully transformed and connected FlowSystem. This arises from Numeric Data not being stored as xr.DataArray by the user. To avoid this, always use the `to_dataset()` on Elements inside a FlowSystem that's connected and transformed. + +### 👷 Development + +- **Centralized deprecation pattern**: Added `_handle_deprecated_kwarg()` helper method to `Interface` base class that provides reusable deprecation handling with consistent warnings, conflict detection, and optional value transformation. Applied across 5 classes (InvestParameters, Source, Sink, SourceAndSink, Effect) reducing deprecation boilerplate by 72%. +- FlowSystem data management simplified - removed `time_series_collection` pattern in favor of direct timestep properties +- Change modeling hierarchy to allow for more flexibility in future development. This leads to minimal changes in the access and creation of Submodels and their variables. +- Added new module `.modeling` that contains modeling primitives and utilities +- Clearer separation between the main Model and "Submodels" +- Improved access to the Submodels and their variables, constraints and submodels +- Added `__repr__()` for Submodels to easily inspect its content +- Enhanced data handling methods + - `fit_to_model_coords()` method for data alignment + - `fit_effects_to_model_coords()` method for effect data processing + - `connect_and_transform()` method replacing several operations +- **Testing improvements**: Eliminated warnings during test execution + - Updated deprecated code patterns in tests and examples (e.g., `sink`/`source` → `inputs`/`outputs`, `'H'` → `'h'` frequency) + - Refactored plotting logic to handle test environments explicitly with non-interactive backends + - Added comprehensive warning filters in `__init__.py` and `pyproject.toml` to suppress third-party library warnings + - Improved test fixtures with proper figure cleanup to prevent memory leaks + - Enhanced backend detection and handling in `plotting.py` for both Matplotlib and Plotly + - Always run dependent tests in order + +--- + ## [2.2.0] - 2025-10-11 **Summary:** This release is a Configuration and Logging management release. @@ -77,9 +215,16 @@ Until here --> - Added configurable log format settings: `CONFIG.Logging.date_format` and `CONFIG.Logging.format` - Added configurable console settings: `CONFIG.Logging.console_width` and `CONFIG.Logging.show_path` - Added `CONFIG.Logging.Colors` nested class for customizable log level colors using ANSI escape codes (works with both standard and Rich handlers) +- All examples now enable console logging to demonstrate proper logging usage +- Console logging now outputs to `sys.stdout` instead of `sys.stderr` for better compatibility with output redirection + +### 💥 Breaking Changes +- Console logging is now disabled by default (`CONFIG.Logging.console = False`). Enable it explicitly in your scripts with `CONFIG.Logging.console = True` and `CONFIG.apply()` +- File logging is now disabled by default (`CONFIG.Logging.file = None`). Set a file path to enable file logging ### ♻️ Changed - Logging and Configuration management changed +- Improved default logging colors: DEBUG is now gray (`\033[90m`) for de-emphasized messages, INFO uses terminal default color (`\033[0m`) for clean output ### 🗑️ Deprecated - `change_logging_level()` function is now deprecated in favor of `CONFIG.Logging.level` and `CONFIG.apply()`. Will be removed in version 3.0.0. diff --git a/MANIFEST.in b/MANIFEST.in index 72a1ff8eb..383cbef76 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -5,7 +5,7 @@ include CHANGELOG.md include pyproject.toml # Include package source and data -recursive-include flixopt *.py *.yaml +recursive-include flixopt *.py # Exclude everything else global-exclude *.pyc *.pyo __pycache__ diff --git a/README.md b/README.md index edb77e74d..957274f1c 100644 --- a/README.md +++ b/README.md @@ -8,84 +8,122 @@ --- -## 🚀 Purpose +## 🎯 Vision -**flixopt** is a Python-based optimization framework designed to tackle energy and material flow problems using mixed-integer linear programming (MILP). +**FlixOpt aims to be the most accessible and flexible Python framework for energy and material flow optimization.** -**flixopt** bridges the gap between high-level energy systems models like [FINE](https://github.com/FZJ-IEK3-VSA/FINE) used for design and (multi-period) investment decisions and low-level dispatch optimization tools used for operation decisions. +We believe that optimization modeling should be **approachable for beginners** yet **powerful for experts**. Too often, frameworks force you to choose between ease of use and flexibility. FlixOpt refuses this compromise. -**flixopt** leverages the fast and efficient [linopy](https://github.com/PyPSA/linopy/) for the mathematical modeling and [xarray](https://github.com/pydata/xarray) for data handling. +### Where We're Going -**flixopt** provides a user-friendly interface with options for advanced users. +**Short-term goals:** +- **Multi-dimensional modeling**: Full support for multi-period investments and scenario-based stochastic optimization (periods and scenarios are in active development) +- **Enhanced component library**: More pre-built, domain-specific components (sector coupling, hydrogen systems, thermal networks, demand-side management) -It was originally developed by [TU Dresden](https://github.com/gewv-tu-dresden) as part of the SMARTBIOGRID project, funded by the German Federal Ministry for Economic Affairs and Energy (FKZ: 03KB159B). Building on the Matlab-based flixOptMat framework (developed in the FAKS project), FlixOpt also incorporates concepts from [oemof/solph](https://github.com/oemof/oemof-solph). +**Medium-term vision:** +- **Modeling to generate alternatives (MGA)**: Built-in support for exploring near-optimal solution spaces to produce more robust, diverse solutions under uncertainty +- **Interactive tutorials**: Browser-based, reactive tutorials for learning FlixOpt without local installation +- **Standardized cost calculations**: Align with industry standards (VDI 2067) for CAPEX/OPEX calculations +- **Advanced result analysis**: Time-series aggregation, automated reporting, and rich visualization options ---- +**Long-term vision:** +- **Showcase universal applicability**: FlixOpt already handles any flow-based system (supply chains, water networks, production planning, chemical processes) - we need more examples and domain-specific component libraries to demonstrate this +- **Seamless integration**: First-class support for coupling with simulation tools, databases, existing energy system models, and GIS data +- **Robust optimization**: Built-in uncertainty quantification and stochastic programming capabilities +- **Community ecosystem**: Rich library of user-contributed components, examples, and domain-specific extensions +- **Model validation tools**: Automated checks for physical plausibility, data consistency, and common modeling errors + +### Why FlixOpt Exists -## 🌟 Key Features +FlixOpt is a **general-purpose framework for modeling any system involving flows and conversions** - energy, materials, fluids, goods, or data. While energy systems are our primary focus, the same mathematical foundation applies to supply chains, water networks, production lines, and more. -- **High-level Interface** with low-level control - - User-friendly interface for defining flow systems - - Pre-defined components like CHP, Heat Pump, Cooling Tower, etc. - - Fine-grained control for advanced configurations +We bridge the gap between high-level strategic models (like [FINE](https://github.com/FZJ-IEK3-VSA/FINE)) for long-term planning and low-level dispatch tools for operations. FlixOpt is the **sweet spot** for: -- **Investment Optimization** - - Combined dispatch and investment optimization - - Size optimization and discrete investment decisions - - Combined with On/Off variables and constraints +- **Researchers** who need to prototype quickly but may require deep customization later +- **Engineers** who want reliable, tested components without black-box abstractions +- **Students** learning optimization who benefit from clear, Pythonic interfaces +- **Practitioners** who need to move from model to production-ready results +- **Domain experts** from any field where things flow, transform, and need optimizing -- **Effects, not only Costs --> Multi-criteria Optimization** - - flixopt abstracts costs as so called 'Effects'. This allows to model costs, CO2-emissions, primary-energy-demand or area-demand at the same time. - - Effects can interact with each other(e.g., specific CO2 costs) - - Any of these `Effects` can be used as the optimization objective. - - A **Weigted Sum** of Effects can be used as the optimization objective. - - Every Effect can be constrained ($\epsilon$-constraint method). +Built on modern foundations ([linopy](https://github.com/PyPSA/linopy/) and [xarray](https://github.com/pydata/xarray)), FlixOpt delivers both **performance** and **transparency**. You can inspect everything, extend anything, and trust that your model does exactly what you designed. -- **Calculation Modes** - - **Full** - Solve the model with highest accuracy and computational requirements. - - **Segmented** - Speed up solving by using a rolling horizon. - - **Aggregated** - Speed up solving by identifying typical periods using [TSAM](https://github.com/FZJ-IEK3-VSA/tsam). Suitable for large models. +Originally developed at [TU Dresden](https://github.com/gewv-tu-dresden) for the SMARTBIOGRID project (funded by the German Federal Ministry for Economic Affairs and Energy, FKZ: 03KB159B), FlixOpt has evolved from the Matlab-based flixOptMat framework while incorporating the best ideas from [oemof/solph](https://github.com/oemof/oemof-solph). --- -## 📦 Installation +## 🌟 What Makes FlixOpt Different -Install FlixOpt via pip. -`pip install flixopt` -With [HiGHS](https://github.com/ERGO-Code/HiGHS?tab=readme-ov-file) included out of the box, flixopt is ready to use.. +### Start Simple, Scale Complex +Define a working model in minutes with high-level components, then drill down to fine-grained control when needed. No rewriting, no framework switching. -We recommend installing FlixOpt with all dependencies, which enables additional features like interactive network visualizations ([pyvis](https://github.com/WestHealth/pyvis)) and time series aggregation ([tsam](https://github.com/FZJ-IEK3-VSA/tsam)). -`pip install "flixopt[full]"` +```python +import flixopt as fx ---- +# Simple start +boiler = fx.Boiler("Boiler", eta=0.9, ...) + +# Advanced control when needed - extend with native linopy +boiler.model.add_constraints(custom_constraint, name="my_constraint") +``` -## 📚 Documentation +### Multi-Criteria Optimization Done Right +Model costs, emissions, resource use, and any custom metric simultaneously as **Effects**. Optimize any single Effect, use weighted combinations, or apply ε-constraints: -The documentation is available at [https://flixopt.github.io/flixopt/latest/](https://flixopt.github.io/flixopt/latest/) +```python +costs = fx.Effect('costs', '€', 'Total costs', + share_from_temporal={'CO2': 180}) # 180 €/tCO2 +co2 = fx.Effect('CO2', 'kg', 'Emissions', maximum_periodic=50000) +``` + +### Performance at Any Scale +Choose the right calculation mode for your problem: +- **Full** - Maximum accuracy for smaller problems +- **Segmented** - Rolling horizon for large time series +- **Aggregated** - Typical periods using [TSAM](https://github.com/FZJ-IEK3-VSA/tsam) for massive models + +### Built for Reproducibility +Every result file is self-contained with complete model information. Load it months later and know exactly what you optimized. Export to NetCDF, share with colleagues, archive for compliance. --- -## 🎯️ Solver Integration +## 🚀 Quick Start + +```bash +pip install flixopt +``` -By default, FlixOpt uses the open-source solver [HiGHS](https://highs.dev/) which is installed by default. However, it is compatible with additional solvers such as: +That's it. FlixOpt comes with the [HiGHS](https://highs.dev/) solver included - you're ready to optimize. +Many more solvers are supported (gurobi, cplex, cbc, glpk, ...) -- [Gurobi](https://www.gurobi.com/) -- [CBC](https://github.com/coin-or/Cbc) -- [GLPK](https://www.gnu.org/software/glpk/) -- [CPLEX](https://www.ibm.com/analytics/cplex-optimizer) +For additional features (interactive network visualization, time series aggregation): +```bash +pip install "flixopt[full]" +``` -For detailed licensing and installation instructions, refer to the respective solver documentation. +**Next steps:** +- 📚 [Full Documentation](https://flixopt.github.io/flixopt/latest/) +- 💡 [Examples](https://flixopt.github.io/flixopt/latest/examples/) +- 🔧 [API Reference](https://flixopt.github.io/flixopt/latest/api-reference/) --- -## 🛠 Development Setup -Look into our docs for [development setup](https://flixopt.github.io/flixopt/latest/contribute/) +## 🤝 Contributing + +FlixOpt thrives on community input. Whether you're fixing bugs, adding components, improving docs, or sharing use cases - we welcome your contributions. + +See our [contribution guide](https://flixopt.github.io/flixopt/latest/contribute/) to get started. --- ## 📖 Citation -If you use FlixOpt in your research or project, please cite the following: +If FlixOpt supports your research or project, please cite: - **Main Citation:** [DOI:10.18086/eurosun.2022.04.07](https://doi.org/10.18086/eurosun.2022.04.07) - **Short Overview:** [DOI:10.13140/RG.2.2.14948.24969](https://doi.org/10.13140/RG.2.2.14948.24969) + +--- + +## 📄 License + +MIT License - See [LICENSE](https://github.com/flixopt/flixopt/blob/main/LICENSE) for details. diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md deleted file mode 100644 index f66c4e5e5..000000000 --- a/docs/SUMMARY.md +++ /dev/null @@ -1,7 +0,0 @@ -- [Home](index.md) -- [Getting Started](getting-started.md) -- [User Guide](user-guide/) -- [Examples](examples/) -- [Contribute](contribute.md) -- [API Reference](api-reference/) -- [Release Notes](changelog/) diff --git a/docs/images/flixopt-icon.svg b/docs/images/flixopt-icon.svg index 04a6a6851..08fe340f9 100644 --- a/docs/images/flixopt-icon.svg +++ b/docs/images/flixopt-icon.svg @@ -1 +1 @@ -flixOpt \ No newline at end of file +flixOpt diff --git a/docs/index.md b/docs/index.md index 04020639e..2c6420f7f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,24 +1,85 @@ # FlixOpt -**FlixOpt** is a Python-based optimization framework designed to tackle energy and material flow problems using mixed-integer linear programming (MILP). +## 🎯 Vision -It borrows concepts from both [FINE](https://github.com/FZJ-IEK3-VSA/FINE) and [oemof.solph](https://github.com/oemof/oemof-solph). +**FlixOpt aims to be the most accessible and flexible Python framework for energy and material flow optimization.** -## Why FlixOpt? +We believe that optimization modeling should be **approachable for beginners** yet **powerful for experts**. Too often, frameworks force you to choose between ease of use and flexibility. FlixOpt refuses this compromise. -FlixOpt is designed as a general-purpose optimization framework to get your model running quickly, without sacrificing flexibility down the road: +### Where We're Going -- **Easy to Use API**: FlixOpt provides a Pythonic, object-oriented interface that makes mathematical optimization more accessible to Python developers. +**Short-term goals:** -- **Approachable Learning Curve**: Designed to be accessible from the start, with options for more detailed models down the road. +- **Multi-dimensional modeling**: Multi-period investments and scenario-based stochastic optimization are available (periods and scenarios are in active development for enhanced features) +- **Enhanced component library**: More pre-built, domain-specific components (sector coupling, hydrogen systems, thermal networks, demand-side management) -- **Domain Independence**: While frameworks like oemof and FINE excel at energy system modeling with domain-specific components, FlixOpt offers a more general mathematical approach that can be applied across different fields. +**Medium-term vision:** -- **Extensibility**: Easily add custom constraints or variables to any FlixOpt Model using [linopy](https://github.com/PyPSA/linopy). Tailor any FlixOpt model to your specific needs without loosing the convenience of the framework. +- **Modeling to generate alternatives (MGA)**: Built-in support for exploring near-optimal solution spaces to produce more robust, diverse solutions under uncertainty +- **Interactive tutorials**: Browser-based, reactive tutorials for learning FlixOpt without local installation ([marimo](https://marimo.io)) +- **Standardized cost calculations**: Align with industry standards (VDI 2067) for CAPEX/OPEX calculations +- **Advanced result analysis**: Time-series aggregation, automated reporting, and rich visualization options +- **Recipe collection**: Community-driven library of common modeling patterns, data manipulation techniques, and optimization strategies (see [Recipes](user-guide/recipes/index.md) - help wanted!) -- **Solver Agnostic**: Work with different solvers through a consistent interface. +**Long-term vision:** -- **Results File I/O**: Built to analyze results independent of running the optimization. +- **Showcase universal applicability**: FlixOpt already handles any flow-based system (supply chains, water networks, production planning, chemical processes) - we need more examples and domain-specific component libraries to demonstrate this +- **Seamless integration**: First-class support for coupling with simulation tools, databases, existing energy system models, and GIS data +- **Robust optimization**: Built-in uncertainty quantification and stochastic programming capabilities +- **Community ecosystem**: Rich library of user-contributed components, examples, and domain-specific extensions +- **Model validation tools**: Automated checks for physical plausibility, data consistency, and common modeling errors + +### Why FlixOpt Exists + +FlixOpt is a **general-purpose framework for modeling any system involving flows and conversions** - energy, materials, fluids, goods, or data. While energy systems are our primary focus, the same mathematical foundation applies to supply chains, water networks, production lines, and more. + +We bridge the gap between high-level strategic models (like [FINE](https://github.com/FZJ-IEK3-VSA/FINE)) for long-term planning and low-level dispatch tools for operations. FlixOpt is the **sweet spot** for: + +- **Researchers** who need to prototype quickly but may require deep customization later +- **Engineers** who want reliable, tested components without black-box abstractions +- **Students** learning optimization who benefit from clear, Pythonic interfaces +- **Practitioners** who need to move from model to production-ready results +- **Domain experts** from any field where things flow, transform, and need optimizing + +Built on modern foundations ([linopy](https://github.com/PyPSA/linopy/) and [xarray](https://github.com/pydata/xarray)), FlixOpt delivers both **performance** and **transparency**. You can inspect everything, extend anything, and trust that your model does exactly what you designed. + +Originally developed at [TU Dresden](https://github.com/gewv-tu-dresden) for the SMARTBIOGRID project (funded by the German Federal Ministry for Economic Affairs and Energy, FKZ: 03KB159B), FlixOpt has evolved from the Matlab-based flixOptMat framework while incorporating the best ideas from [oemof/solph](https://github.com/oemof/oemof-solph). + +--- + +## What Makes FlixOpt Different + +### Start Simple, Scale Complex +Define a working model in minutes with high-level components, then drill down to fine-grained control when needed. No rewriting, no framework switching. + +```python +import flixopt as fx + +# Simple start +boiler = fx.Boiler("Boiler", eta=0.9, ...) + +# Advanced control when needed - extend with native linopy +boiler.model.add_constraints(custom_constraint, name="my_constraint") +``` + +### Multi-Criteria Optimization Done Right +Model costs, emissions, resource use, and any custom metric simultaneously as **Effects**. Optimize any single Effect, use weighted combinations, or apply ε-constraints: + +```python +costs = fx.Effect('costs', '€', 'Total costs', + share_from_temporal={'CO2': 180}) # 180 €/tCO2 +co2 = fx.Effect('CO2', 'kg', 'Emissions', maximum_periodic=50000) +``` + +### Performance at Any Scale +Choose the right calculation mode for your problem: + +- **Full** - Maximum accuracy for smaller problems +- **Segmented** - Rolling horizon for large time series +- **Aggregated** - Typical periods using [TSAM](https://github.com/FZJ-IEK3-VSA/tsam) for massive models + +### Built for Reproducibility +Every result file is self-contained with complete model information. Load it months later and know exactly what you optimized. Export to NetCDF, share with colleagues, archive for compliance.
![FlixOpt Conceptual Usage](./images/architecture_flixOpt.png) diff --git a/docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md b/docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md deleted file mode 100644 index 7da311c37..000000000 --- a/docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md +++ /dev/null @@ -1,132 +0,0 @@ -## Effects -[`Effects`][flixopt.effects.Effect] are used to allocate things like costs, emissions, or other "effects" occurring in the system. -These arise from so called **Shares**, which originate from **Elements** like [Flows](Flow.md). - -**Example:** - -[`Flows`][flixopt.elements.Flow] have an attribute called `effects_per_flow_hour`, defining the effect amount of per flow hour. -Associated effects could be: -- costs - given in [€/kWh]... -- ...or emissions - given in [kg/kWh]. -- -Effects are allocated separately for investments and operation. - -### Shares to Effects - -$$ \label{eq:Share_invest} -s_{l \rightarrow e, \text{inv}} = \sum_{v \in \mathcal{V}_{l, \text{inv}}} v \cdot \text a_{v \rightarrow e} -$$ - -$$ \label{eq:Share_operation} -s_{l \rightarrow e, \text{op}}(\text{t}_i) = \sum_{v \in \mathcal{V}_{l,\text{op}}} v(\text{t}_i) \cdot \text a_{v \rightarrow e}(\text{t}_i) -$$ - -With: - -- $\text{t}_i$ being the time step -- $\mathcal{V_l}$ being the set of all optimization variables of element $e$ -- $\mathcal{V}_{l, \text{inv}}$ being the set of all optimization variables of element $e$ related to investment -- $\mathcal{V}_{l, \text{op}}$ being the set of all optimization variables of element $e$ related to operation -- $v$ being an optimization variable of the element $l$ -- $v(\text{t}_i)$ being an optimization variable of the element $l$ at timestep $\text{t}_i$ -- $\text a_{v \rightarrow e}$ being the factor between the optimization variable $v$ to effect $e$ -- $\text a_{v \rightarrow e}(\text{t}_i)$ being the factor between the optimization variable $v$ to effect $e$ for timestep $\text{t}_i$ -- $s_{l \rightarrow e, \text{inv}}$ being the share of element $l$ to the investment part of effect $e$ -- $s_{l \rightarrow e, \text{op}}(\text{t}_i)$ being the share of element $l$ to the operation part of effect $e$ - -### Shares between different Effects - -Furthermore, the Effect $x$ can contribute a share to another Effect ${e} \in \mathcal{E}\backslash x$. -This share is defined by the factor $\text r_{x \rightarrow e}$. - -For example, the Effect "CO$_2$ emissions" (unit: kg) -can cause an additional share to Effect "monetary costs" (unit: €). -In this case, the factor $\text a_{x \rightarrow e}$ is the specific CO$_2$ price in €/kg. However, circular references have to be avoided. - -The overall sum of investment shares of an Effect $e$ is given by $\eqref{eq:Effect_invest}$ - -$$ \label{eq:Effect_invest} -E_{e, \text{inv}} = -\sum_{l \in \mathcal{L}} s_{l \rightarrow e,\text{inv}} + -\sum_{x \in \mathcal{E}\backslash e} E_{x, \text{inv}} \cdot \text{r}_{x \rightarrow e,\text{inv}} -$$ - -The overall sum of operation shares is given by $\eqref{eq:Effect_Operation}$ - -$$ \label{eq:Effect_Operation} -E_{e, \text{op}}(\text{t}_{i}) = -\sum_{l \in \mathcal{L}} s_{l \rightarrow e, \text{op}}(\text{t}_i) + -\sum_{x \in \mathcal{E}\backslash e} E_{x, \text{op}}(\text{t}_i) \cdot \text{r}_{x \rightarrow {e},\text{op}}(\text{t}_i) -$$ - -and totals to $\eqref{eq:Effect_Operation_total}$ -$$\label{eq:Effect_Operation_total} -E_{e,\text{op},\text{tot}} = \sum_{i=1}^n E_{e,\text{op}}(\text{t}_{i}) -$$ - -With: - -- $\mathcal{L}$ being the set of all elements in the FlowSystem -- $\mathcal{E}$ being the set of all effects in the FlowSystem -- $\text r_{x \rightarrow e, \text{inv}}$ being the factor between the invest part of Effect $x$ and Effect $e$ -- $\text r_{x \rightarrow e, \text{op}}(\text{t}_i)$ being the factor between the operation part of Effect $x$ and Effect $e$ - -- $\text{t}_i$ being the time step -- $s_{l \rightarrow e, \text{inv}}$ being the share of element $l$ to the investment part of effect $e$ -- $s_{l \rightarrow e, \text{op}}(\text{t}_i)$ being the share of element $l$ to the operation part of effect $e$ - - -The total of an effect $E_{e}$ is given as $\eqref{eq:Effect_Total}$ - -$$ \label{eq:Effect_Total} -E_{e} = E_{\text{inv},e} +E_{\text{op},\text{tot},e} -$$ - -### Constraining Effects - -For each variable $v \in \{ E_{e,\text{inv}}, E_{e,\text{op},\text{tot}}, E_e\}$, a lower bound $v^\text{L}$ and upper bound $v^\text{U}$ can be defined as - -$$ \label{eq:Bounds_Single} -\text v^\text{L} \leq v \leq \text v^\text{U} -$$ - -Furthermore, bounds for the operational shares can be set for each time step - -$$ \label{eq:Bounds_Time_Steps} -\text E_{e,\text{op}}^\text{L}(\text{t}_i) \leq E_{e,\text{op}}(\text{t}_i) \leq \text E_{e,\text{op}}^\text{U}(\text{t}_i) -$$ - -## Penalty - -Additionally to the user defined [Effects](#effects), a Penalty $\Phi$ is part of every FlixOpt Model. -Its used to prevent unsolvable problems and simplify troubleshooting. -Shares to the penalty can originate from every Element and are constructed similarly to -$\eqref{Share_invest}$ and $\eqref{Share_operation}$. - -$$ \label{eq:Penalty} -\Phi = \sum_{l \in \mathcal{L}} \left( s_{l \rightarrow \Phi} +\sum_{\text{t}_i \in \mathcal{T}} s_{l \rightarrow \Phi}(\text{t}_{i}) \right) -$$ - -With: - -- $\mathcal{L}$ being the set of all elements in the FlowSystem -- $\mathcal{T}$ being the set of all timesteps -- $s_{l \rightarrow \Phi}$ being the share of element $l$ to the penalty - -At the moment, penalties only occur in [Buses](Bus.md) - -## Objective - -The optimization objective of a FlixOpt Model is defined as $\eqref{eq:Objective}$ -$$ \label{eq:Objective} -\min(E_{\Omega} + \Phi) -$$ - -With: - -- $\Omega$ being the chosen **Objective [Effect](#effects)** (see $\eqref{eq:Effect_Total}$) -- $\Phi$ being the [Penalty](#penalty) - -This approach allows for a multi-criteria optimization using both... - - ... the **Weighted Sum** method, as the chosen **Objective Effect** can incorporate other Effects. - - ... the ($\epsilon$-constraint method) by constraining effects. diff --git a/docs/user-guide/Mathematical Notation/Flow.md b/docs/user-guide/Mathematical Notation/Flow.md deleted file mode 100644 index 78135e822..000000000 --- a/docs/user-guide/Mathematical Notation/Flow.md +++ /dev/null @@ -1,26 +0,0 @@ -The flow_rate is the main optimization variable of the Flow. It's limited by the size of the Flow and relative bounds \eqref{eq:flow_rate}. - -$$ \label{eq:flow_rate} - \text P \cdot \text p^{\text{L}}_{\text{rel}}(\text{t}_{i}) - \leq p(\text{t}_{i}) \leq - \text P \cdot \text p^{\text{U}}_{\text{rel}}(\text{t}_{i}) -$$ - -With: - -- $\text P$ being the size of the Flow -- $p(\text{t}_{i})$ being the flow-rate at time $\text{t}_{i}$ -- $\text p^{\text{L}}_{\text{rel}}(\text{t}_{i})$ being the relative lower bound (typically 0) -- $\text p^{\text{U}}_{\text{rel}}(\text{t}_{i})$ being the relative upper bound (typically 1) - -With $\text p^{\text{L}}_{\text{rel}}(\text{t}_{i}) = 0$ and $\text p^{\text{U}}_{\text{rel}}(\text{t}_{i}) = 1$, -equation \eqref{eq:flow_rate} simplifies to - -$$ - 0 \leq p(\text{t}_{i}) \leq \text P -$$ - - -This mathematical formulation can be extended by using [OnOffParameters](./OnOffParameters.md) -to define the on/off state of the Flow, or by using [InvestParameters](./InvestParameters.md) -to change the size of the Flow from a constant to an optimization variable. diff --git a/docs/user-guide/Mathematical Notation/InvestParameters.md b/docs/user-guide/Mathematical Notation/InvestParameters.md deleted file mode 100644 index d3cd4f81e..000000000 --- a/docs/user-guide/Mathematical Notation/InvestParameters.md +++ /dev/null @@ -1,3 +0,0 @@ -# InvestParameters - -This is a work in progress. diff --git a/docs/user-guide/Mathematical Notation/LinearConverter.md b/docs/user-guide/Mathematical Notation/LinearConverter.md deleted file mode 100644 index bf1279c32..000000000 --- a/docs/user-guide/Mathematical Notation/LinearConverter.md +++ /dev/null @@ -1,21 +0,0 @@ -[`LinearConverters`][flixopt.components.LinearConverter] define a ratio between incoming and outgoing [Flows](Flow.md). - -$$ \label{eq:Linear-Transformer-Ratio} - \sum_{f_{\text{in}} \in \mathcal F_{in}} \text a_{f_{\text{in}}}(\text{t}_i) \cdot p_{f_\text{in}}(\text{t}_i) = \sum_{f_{\text{out}} \in \mathcal F_{out}} \text b_{f_\text{out}}(\text{t}_i) \cdot p_{f_\text{out}}(\text{t}_i) -$$ - -With: - -- $\mathcal F_{in}$ and $\mathcal F_{out}$ being the set of all incoming and outgoing flows -- $p_{f_\text{in}}(\text{t}_i)$ and $p_{f_\text{out}}(\text{t}_i)$ being the flow-rate at time $\text{t}_i$ for flow $f_\text{in}$ and $f_\text{out}$, respectively -- $\text a_{f_\text{in}}(\text{t}_i)$ and $\text b_{f_\text{out}}(\text{t}_i)$ being the ratio of the flow-rate at time $\text{t}_i$ for flow $f_\text{in}$ and $f_\text{out}$, respectively - -With one incoming **Flow** and one outgoing **Flow**, this can be simplified to: - -$$ \label{eq:Linear-Transformer-Ratio-simple} - \text a(\text{t}_i) \cdot p_{f_\text{in}}(\text{t}_i) = p_{f_\text{out}}(\text{t}_i) -$$ - -where $\text a$ can be interpreted as the conversion efficiency of the **LinearConverter**. -#### Piecewise Conversion factors -The conversion efficiency can be defined as a piecewise linear approximation. See [Piecewise](Piecewise.md) for more details. diff --git a/docs/user-guide/Mathematical Notation/OnOffParameters.md b/docs/user-guide/Mathematical Notation/OnOffParameters.md deleted file mode 100644 index ca22d7d33..000000000 --- a/docs/user-guide/Mathematical Notation/OnOffParameters.md +++ /dev/null @@ -1,3 +0,0 @@ -# OnOffParameters - -This is a work in progress. diff --git a/docs/user-guide/Mathematical Notation/index.md b/docs/user-guide/Mathematical Notation/index.md deleted file mode 100644 index b76a1ba1f..000000000 --- a/docs/user-guide/Mathematical Notation/index.md +++ /dev/null @@ -1,22 +0,0 @@ - -# Mathematical Notation - -## Naming Conventions - -FlixOpt uses the following naming conventions: - -- All optimization variables are denoted by italic letters (e.g., $x$, $y$, $z$) -- All parameters and constants are denoted by non italic small letters (e.g., $\text{a}$, $\text{b}$, $\text{c}$) -- All Sets are denoted by greek capital letters (e.g., $\mathcal{F}$, $\mathcal{E}$) -- All units of a set are denoted by greek small letters (e.g., $\mathcal{f}$, $\mathcal{e}$) -- The letter $i$ is used to denote an index (e.g., $i=1,\dots,\text n$) -- All time steps are denoted by the letter $\text{t}$ (e.g., $\text{t}_0$, $\text{t}_1$, $\text{t}_i$) - -## Timesteps -Time steps are defined as a sequence of discrete time steps $\text{t}_i \in \mathcal{T} \quad \text{for} \quad i \in \{1, 2, \dots, \text{n}\}$ (left-aligned in its timespan). -From this sequence, the corresponding time intervals $\Delta \text{t}_i \in \Delta \mathcal{T}$ are derived as - -$$\Delta \text{t}_i = \text{t}_{i+1} - \text{t}_i \quad \text{for} \quad i \in \{1, 2, \dots, \text{n}-1\}$$ - -The final time interval $\Delta \text{t}_\text n$ defaults to $\Delta \text{t}_\text n = \Delta \text{t}_{\text n-1}$, but is of course customizable. -Non-equidistant time steps are also supported. diff --git a/docs/user-guide/index.md b/docs/user-guide/index.md index bc1738997..df97bf768 100644 --- a/docs/user-guide/index.md +++ b/docs/user-guide/index.md @@ -1,6 +1,8 @@ # FlixOpt Concepts -FlixOpt is built around a set of core concepts that work together to represent and optimize energy and material flow systems. This page provides a high-level overview of these concepts and how they interact. +FlixOpt is built around a set of core concepts that work together to represent and optimize **any system involving flows and conversions** - whether that's energy systems, material flows, supply chains, water networks, or production processes. + +This page provides a high-level overview of these concepts and how they interact. ## Core Concepts @@ -45,28 +47,49 @@ Examples: ### Components -[`Component`][flixopt.elements.Component] objects usually represent physical entities in your system that interact with [`Flows`][flixopt.elements.Flow]. They include: +[`Component`][flixopt.elements.Component] objects usually represent physical entities in your system that interact with [`Flows`][flixopt.elements.Flow]. The generic component types work across all domains: - [`LinearConverters`][flixopt.components.LinearConverter] - Converts input flows to output flows with (piecewise) linear relationships + - *Energy: boilers, heat pumps, turbines* + - *Manufacturing: assembly lines, processing equipment* + - *Chemistry: reactors, separators* - [`Storages`][flixopt.components.Storage] - Stores energy or material over time -- [`Sources`][flixopt.components.Source] / [`Sinks`][flixopt.components.Sink] / [`SourceAndSinks`][flixopt.components.SourceAndSink] - Produce or consume flows. They are usually used to model external demands or supplies. + - *Energy: batteries, thermal storage, gas storage* + - *Logistics: warehouses, buffer inventory* + - *Water: reservoirs, tanks* +- [`Sources`][flixopt.components.Source] / [`Sinks`][flixopt.components.Sink] / [`SourceAndSinks`][flixopt.components.SourceAndSink] - Produce or consume flows + - *Energy: demands, renewable generation* + - *Manufacturing: raw material supply, product demand* + - *Supply chain: suppliers, customers* - [`Transmissions`][flixopt.components.Transmission] - Moves flows between locations with possible losses -- Specialized [`LinearConverters`][flixopt.components.LinearConverter] like [`Boilers`][flixopt.linear_converters.Boiler], [`HeatPumps`][flixopt.linear_converters.HeatPump], [`CHPs`][flixopt.linear_converters.CHP], etc. These simplify the usage of the `LinearConverter` class and can also be used as blueprint on how to define custom classes or parameterize existing ones. + - *Energy: pipelines, power lines* + - *Logistics: transport routes* + - *Water: distribution networks* + +**Pre-built specialized components** for energy systems include [`Boilers`][flixopt.linear_converters.Boiler], [`HeatPumps`][flixopt.linear_converters.HeatPump], [`CHPs`][flixopt.linear_converters.CHP], etc. These can serve as blueprints for custom domain-specific components. ### Effects -[`Effect`][flixopt.effects.Effect] objects represent impacts or metrics related to your system, such as: +[`Effect`][flixopt.effects.Effect] objects represent impacts or metrics related to your system. While commonly used to allocate costs, they're completely flexible: +**Energy systems:** - Costs (investment, operation) - Emissions (CO₂, NOx, etc.) -- Resource consumption -- Area demand +- Primary energy consumption + +**Other domains:** +- Production time, labor hours (manufacturing) +- Water consumption, wastewater (process industries) +- Transport distance, vehicle utilization (logistics) +- Space consumption +- Any custom metric relevant to your domain These can be freely defined and crosslink to each other (`CO₂` ──[specific CO₂-costs]─→ `Costs`). One effect is designated as the **optimization objective** (typically Costs), while others can be constrained. -This approach allows for a multi-criteria optimization using both... - - ... the **Weigted Sum**Method, by Optimizing a theoretical Effect which other Effects crosslink to. - - ... the ($\epsilon$-constraint method) by constraining effects. +This approach allows for multi-criteria optimization using both: + + - **Weighted Sum Method**: Optimize a theoretical Effect which other Effects crosslink to + - **ε-constraint method**: Constrain effects to specific limits ### Calculation diff --git a/docs/user-guide/mathematical-notation/dimensions.md b/docs/user-guide/mathematical-notation/dimensions.md new file mode 100644 index 000000000..d1bc99c8e --- /dev/null +++ b/docs/user-guide/mathematical-notation/dimensions.md @@ -0,0 +1,264 @@ +# Dimensions + +FlixOpt's `FlowSystem` supports multiple dimensions for modeling optimization problems. Understanding these dimensions is crucial for interpreting the mathematical formulations presented in this documentation. + +## The Three Dimensions + +FlixOpt models can have up to three dimensions: + +1. **Time (`time`)** - **MANDATORY** + - Represents the temporal evolution of the system + - Defined via `pd.DatetimeIndex` + - Must contain at least 2 timesteps + - All optimization variables and constraints evolve over time +2. **Period (`period`)** - **OPTIONAL** + - Represents independent planning periods (e.g., years 2020, 2021, 2022) + - Defined via `pd.Index` with integer values + - Used for multi-period optimization such as investment planning across years + - Each period is independent with its own time series +3. **Scenario (`scenario`)** - **OPTIONAL** + - Represents alternative futures or uncertainty realizations (e.g., "Base Case", "High Demand") + - Defined via `pd.Index` with any labels + - Scenarios within the same period share the same time dimension + - Used for stochastic optimization or scenario comparison + +--- + +## Dimensional Structure + +**Coordinate System:** + +```python +FlowSystemDimensions = Literal['time', 'period', 'scenario'] + +coords = { + 'time': pd.DatetimeIndex, # Always present + 'period': pd.Index | None, # Optional + 'scenario': pd.Index | None # Optional +} +``` + +**Example:** +```python +import pandas as pd +import numpy as np +import flixopt as fx + +timesteps = pd.date_range('2020-01-01', periods=24, freq='h') +scenarios = pd.Index(['Base Case', 'High Demand']) +periods = pd.Index([2020, 2021, 2022]) + +flow_system = fx.FlowSystem( + timesteps=timesteps, + periods=periods, + scenarios=scenarios, + weights=np.array([0.5, 0.5]) # Scenario weights +) +``` + +This creates a system with: +- 24 time steps per scenario per period +- 2 scenarios with equal weights (0.5 each) +- 3 periods (years) +- **Total decision space:** 24 × 2 × 3 = 144 time-scenario-period combinations + +--- + +## Independence of Formulations + +**All mathematical formulations in this documentation are independent of whether periods or scenarios are present.** + +The equations shown throughout this documentation (for [Flow](elements/Flow.md), [Storage](elements/Storage.md), [Bus](elements/Bus.md), etc.) are written with only the time index $\text{t}_i$. When periods and/or scenarios are added, **the same equations apply** - they are simply expanded to additional dimensions. + +### How Dimensions Expand Formulations + +**Flow rate bounds** (from [Flow](elements/Flow.md)): + +$$ +\text{P} \cdot \text{p}^{\text{L}}_{\text{rel}}(\text{t}_{i}) \leq p(\text{t}_{i}) \leq \text{P} \cdot \text{p}^{\text{U}}_{\text{rel}}(\text{t}_{i}) +$$ + +This equation remains valid regardless of dimensions: + +| Dimensions Present | Variable Indexing | Interpretation | +|-------------------|-------------------|----------------| +| Time only | $p(\text{t}_i)$ | Flow rate at time $\text{t}_i$ | +| Time + Scenario | $p(\text{t}_i, s)$ | Flow rate at time $\text{t}_i$ in scenario $s$ | +| Time + Period | $p(\text{t}_i, y)$ | Flow rate at time $\text{t}_i$ in period $y$ | +| Time + Period + Scenario | $p(\text{t}_i, y, s)$ | Flow rate at time $\text{t}_i$ in period $y$, scenario $s$ | + +**The mathematical relationship remains identical** - only the indexing expands. + +--- + +## Independence Between Scenarios and Periods + +**There is no interconnection between scenarios and periods, except for shared investment decisions within a period.** + +### Scenario Independence + +Scenarios within a period are **operationally independent**: + +- Each scenario has its own operational variables: $p(\text{t}_i, s_1)$ and $p(\text{t}_i, s_2)$ are independent +- Scenarios cannot exchange energy, information, or resources +- Storage states are separate: $c(\text{t}_i, s_1) \neq c(\text{t}_i, s_2)$ +- Binary states (on/off) are independent: $s(\text{t}_i, s_1)$ vs $s(\text{t}_i, s_2)$ + +Scenarios are connected **only through the objective function** via weights: + +$$ +\min \quad \sum_{s \in \mathcal{S}} w_s \cdot \text{Objective}_s +$$ + +Where: +- $\mathcal{S}$ is the set of scenarios +- $w_s$ is the weight for scenario $s$ +- The optimizer balances performance across scenarios according to their weights + +### Period Independence + +Periods are **completely independent** optimization problems: + +- Each period has separate operational variables +- Each period has separate investment decisions +- No temporal coupling between periods (e.g., storage state at end of period $y$ does not affect period $y+1$) +- Periods cannot exchange resources or information + +Periods are connected **only through weighted aggregation** in the objective: + +$$ +\min \quad \sum_{y \in \mathcal{Y}} w_y \cdot \text{Objective}_y +$$ + +### Shared Periodic Decisions: The Exception + +**Investment decisions (sizes) can be shared across all scenarios:** + +By default, sizes (e.g., Storage capacity, Thermal power, ...) are **scenario-independent** but **flow_rates are scenario-specific**. + +**Example - Flow with investment:** + +$$ +v_\text{invest}(y) = s_\text{invest}(y) \cdot \text{size}_\text{fixed} \quad \text{(one decision per period)} +$$ + +$$ +p(\text{t}_i, y, s) \leq v_\text{invest}(y) \cdot \text{rel}_\text{upper} \quad \forall s \in \mathcal{S} \quad \text{(same capacity for all scenarios)} +$$ + +**Interpretation:** +- "We decide once in period $y$ how much capacity to build" (periodic decision) +- "This capacity is then operated differently in each scenario $s$ within period $y$" (temporal decisions) +- "Periodic effects (investment) are incurred once per period, temporal effects (operational) are weighted across scenarios" + +This reflects real-world investment under uncertainty: you build capacity once (periodic/investment decision), but it operates under different conditions (temporal/operational decisions per scenario). + +**Mathematical Flexibility:** + +Variables can be either scenario-independent or scenario-specific: + +| Variable Type | Scenario-Independent | Scenario-Specific | +|---------------|---------------------|-------------------| +| **Sizes** (e.g., $\text{P}$) | $\text{P}(y)$ - Single value per period | $\text{P}(y, s)$ - Different per scenario | +| **Flow rates** (e.g., $p(\text{t}_i)$) | $p(\text{t}_i, y)$ - Same across scenarios | $p(\text{t}_i, y, s)$ - Different per scenario | + +**Use Cases:** + +*Investment problems (with InvestParameters):* +- **Sizes shared** (default): Investment under uncertainty - build capacity that performs well across all scenarios +- **Sizes vary**: Scenario-specific capacity planning where different investments can be made for each future +- **Selected sizes shared**: Mix of shared critical infrastructure and scenario-specific optional/flexible capacity + +*Dispatch problems (fixed sizes, no investments):* +- **Flow rates shared**: Robust dispatch - find a single operational strategy that works across all forecast scenarios (e.g., day-ahead unit commitment under demand/weather uncertainty) +- **Flow rates vary** (default): Scenario-adaptive dispatch - optimize operations for each scenario's specific conditions (demand, weather, prices) + +For implementation details on controlling scenario independence, see the [`FlowSystem`][flixopt.flow_system.FlowSystem] API reference. + +--- + +## Dimensional Impact on Objective Function + +The objective function aggregates effects across all dimensions with weights: + +### Time Only +$$ +\min \quad \sum_{\text{t}_i \in \mathcal{T}} \sum_{e \in \mathcal{E}} s_{e}(\text{t}_i) +$$ + +### Time + Scenario +$$ +\min \quad \sum_{s \in \mathcal{S}} w_s \cdot \left( \sum_{\text{t}_i \in \mathcal{T}} \sum_{e \in \mathcal{E}} s_{e}(\text{t}_i, s) \right) +$$ + +### Time + Period +$$ +\min \quad \sum_{y \in \mathcal{Y}} w_y \cdot \left( \sum_{\text{t}_i \in \mathcal{T}} \sum_{e \in \mathcal{E}} s_{e}(\text{t}_i, y) \right) +$$ + +### Time + Period + Scenario (Full Multi-Dimensional) +$$ +\min \quad \sum_{y \in \mathcal{Y}} \sum_{s \in \mathcal{S}} w_{y,s} \cdot \left( \sum_{\text{t}_i \in \mathcal{T}} \sum_{e \in \mathcal{E}} s_{e}(\text{t}_i, y, s) \right) +$$ + +Where: +- $\mathcal{T}$ is the set of time steps +- $\mathcal{E}$ is the set of effects +- $\mathcal{S}$ is the set of scenarios +- $\mathcal{Y}$ is the set of periods +- $s_{e}(\cdots)$ are the effect contributions (costs, emissions, etc.) +- $w_s, w_y, w_{y,s}$ are the dimension weights + +**See [Effects, Penalty & Objective](effects-penalty-objective.md) for complete formulations including:** +- How temporal and periodic effects expand with dimensions +- Detailed objective function for each dimensional case +- Periodic (investment) vs temporal (operational) effect handling + +--- + +## Weights + +Weights determine the relative importance of scenarios and periods in the objective function. + +**Specification:** + +```python +flow_system = fx.FlowSystem( + timesteps=timesteps, + periods=periods, + scenarios=scenarios, + weights=weights # Shape depends on dimensions +) +``` + +**Weight Dimensions:** + +| Dimensions Present | Weight Shape | Example | Meaning | +|-------------------|--------------|---------|---------| +| Time + Scenario | 1D array of length `n_scenarios` | `[0.3, 0.7]` | Scenario probabilities | +| Time + Period | 1D array of length `n_periods` | `[0.5, 0.3, 0.2]` | Period importance | +| Time + Period + Scenario | 2D array `(n_periods, n_scenarios)` | `[[0.25, 0.25], [0.25, 0.25]]` | Combined weights | + +**Default:** If not specified, all scenarios/periods have equal weight (normalized to sum to 1). + +**Normalization:** Set `normalize_weights=True` in `Calculation` to automatically normalize weights to sum to 1. + +--- + +## Summary Table + +| Dimension | Required? | Independence | Typical Use Case | +|-----------|-----------|--------------|------------------| +| **time** | ✅ Yes | Variables evolve over time via constraints (e.g., storage balance) | All optimization problems | +| **scenario** | ❌ No | Fully independent operations; shared investments within period | Uncertainty modeling, risk assessment | +| **period** | ❌ No | Fully independent; no coupling between periods | Multi-year planning, long-term investment | + +**Key Principle:** All constraints and formulations operate **within** each (period, scenario) combination independently. Only the objective function couples them via weighted aggregation. + +--- + +## See Also + +- [Effects, Penalty & Objective](effects-penalty-objective.md) - How dimensions affect the objective function +- [InvestParameters](features/InvestParameters.md) - Investment decisions across scenarios +- [FlowSystem API][flixopt.flow_system.FlowSystem] - Creating multi-dimensional systems diff --git a/docs/user-guide/mathematical-notation/effects-penalty-objective.md b/docs/user-guide/mathematical-notation/effects-penalty-objective.md new file mode 100644 index 000000000..0759ef5ee --- /dev/null +++ b/docs/user-guide/mathematical-notation/effects-penalty-objective.md @@ -0,0 +1,286 @@ +# Effects, Penalty & Objective + +## Effects + +[`Effects`][flixopt.effects.Effect] are used to quantify system-wide impacts like costs, emissions, or resource consumption. These arise from **shares** contributed by **Elements** such as [Flows](elements/Flow.md), [Storage](elements/Storage.md), and other components. + +**Example:** + +[`Flows`][flixopt.elements.Flow] have an attribute `effects_per_flow_hour` that defines the effect contribution per flow-hour: +- Costs (€/kWh) +- Emissions (kg CO₂/kWh) +- Primary energy consumption (kWh_primary/kWh) + +Effects are categorized into two domains: + +1. **Temporal effects** - Time-dependent contributions (e.g., operational costs, hourly emissions) +2. **Periodic effects** - Time-independent contributions (e.g., investment costs, fixed annual fees) + +### Multi-Dimensional Effects + +**The formulations below are written with time index $\text{t}_i$ only, but automatically expand when periods and/or scenarios are present.** + +When the FlowSystem has additional dimensions (see [Dimensions](dimensions.md)): + +- **Temporal effects** are indexed by all present dimensions: $E_{e,\text{temp}}(\text{t}_i, y, s)$ +- **Periodic effects** are indexed by period only (scenario-independent within a period): $E_{e,\text{per}}(y)$ +- Effects are aggregated with dimension weights in the objective function + +For complete details on how dimensions affect effects and the objective, see [Dimensions](dimensions.md). + +--- + +## Effect Formulation + +### Shares from Elements + +Each element $l$ contributes shares to effect $e$ in both temporal and periodic domains: + +**Periodic shares** (time-independent): +$$ \label{eq:Share_periodic} +s_{l \rightarrow e, \text{per}} = \sum_{v \in \mathcal{V}_{l, \text{per}}} v \cdot \text{a}_{v \rightarrow e} +$$ + +**Temporal shares** (time-dependent): +$$ \label{eq:Share_temporal} +s_{l \rightarrow e, \text{temp}}(\text{t}_i) = \sum_{v \in \mathcal{V}_{l,\text{temp}}} v(\text{t}_i) \cdot \text{a}_{v \rightarrow e}(\text{t}_i) +$$ + +Where: + +- $\text{t}_i$ is the time step +- $\mathcal{V}_l$ is the set of all optimization variables of element $l$ +- $\mathcal{V}_{l, \text{per}}$ is the subset of periodic (investment-related) variables +- $\mathcal{V}_{l, \text{temp}}$ is the subset of temporal (operational) variables +- $v$ is an optimization variable +- $v(\text{t}_i)$ is the variable value at timestep $\text{t}_i$ +- $\text{a}_{v \rightarrow e}$ is the effect factor (e.g., €/kW for investment, €/kWh for operation) +- $s_{l \rightarrow e, \text{per}}$ is the periodic share of element $l$ to effect $e$ +- $s_{l \rightarrow e, \text{temp}}(\text{t}_i)$ is the temporal share of element $l$ to effect $e$ + +**Examples:** +- **Periodic share**: Investment cost = $\text{size} \cdot \text{specific\_cost}$ (€/kW) +- **Temporal share**: Operational cost = $\text{flow\_rate}(\text{t}_i) \cdot \text{price}(\text{t}_i)$ (€/kWh) + +--- + +### Cross-Effect Contributions + +Effects can contribute shares to other effects, enabling relationships like carbon pricing or resource accounting. + +An effect $x$ can contribute to another effect $e \in \mathcal{E}\backslash x$ via conversion factors: + +**Example:** CO₂ emissions (kg) → Monetary costs (€) +- Effect $x$: "CO₂ emissions" (unit: kg) +- Effect $e$: "costs" (unit: €) +- Factor $\text{r}_{x \rightarrow e}$: CO₂ price (€/kg) + +**Note:** Circular references must be avoided. + +### Total Effect Calculation + +**Periodic effects** aggregate element shares and cross-effect contributions: + +$$ \label{eq:Effect_periodic} +E_{e, \text{per}} = +\sum_{l \in \mathcal{L}} s_{l \rightarrow e,\text{per}} + +\sum_{x \in \mathcal{E}\backslash e} E_{x, \text{per}} \cdot \text{r}_{x \rightarrow e,\text{per}} +$$ + +**Temporal effects** at each timestep: + +$$ \label{eq:Effect_temporal} +E_{e, \text{temp}}(\text{t}_{i}) = +\sum_{l \in \mathcal{L}} s_{l \rightarrow e, \text{temp}}(\text{t}_i) + +\sum_{x \in \mathcal{E}\backslash e} E_{x, \text{temp}}(\text{t}_i) \cdot \text{r}_{x \rightarrow {e},\text{temp}}(\text{t}_i) +$$ + +**Total temporal effects** (sum over all timesteps): + +$$\label{eq:Effect_temporal_total} +E_{e,\text{temp},\text{tot}} = \sum_{i=1}^n E_{e,\text{temp}}(\text{t}_{i}) +$$ + +**Total effect** (combining both domains): + +$$ \label{eq:Effect_Total} +E_{e} = E_{e,\text{per}} + E_{e,\text{temp},\text{tot}} +$$ + +Where: + +- $\mathcal{L}$ is the set of all elements in the FlowSystem +- $\mathcal{E}$ is the set of all effects +- $\text{r}_{x \rightarrow e, \text{per}}$ is the periodic conversion factor from effect $x$ to effect $e$ +- $\text{r}_{x \rightarrow e, \text{temp}}(\text{t}_i)$ is the temporal conversion factor + +--- + +### Constraining Effects + +Effects can be bounded to enforce limits on costs, emissions, or other impacts: + +**Total bounds** (apply to $E_{e,\text{per}}$, $E_{e,\text{temp},\text{tot}}$, or $E_e$): + +$$ \label{eq:Bounds_Total} +E^\text{L} \leq E \leq E^\text{U} +$$ + +**Temporal bounds per timestep:** + +$$ \label{eq:Bounds_Timestep} +E_{e,\text{temp}}^\text{L}(\text{t}_i) \leq E_{e,\text{temp}}(\text{t}_i) \leq E_{e,\text{temp}}^\text{U}(\text{t}_i) +$$ + +**Implementation:** See [`Effect`][flixopt.effects.Effect] parameters: +- `minimum_temporal`, `maximum_temporal` - Total temporal bounds +- `minimum_per_hour`, `maximum_per_hour` - Hourly temporal bounds +- `minimum_periodic`, `maximum_periodic` - Periodic bounds +- `minimum_total`, `maximum_total` - Combined total bounds + +--- + +## Penalty + +In addition to user-defined [Effects](#effects), every FlixOpt model includes a **Penalty** term $\Phi$ to: +- Prevent infeasible problems +- Simplify troubleshooting by allowing constraint violations with high cost + +Penalty shares originate from elements, similar to effect shares: + +$$ \label{eq:Penalty} +\Phi = \sum_{l \in \mathcal{L}} \left( s_{l \rightarrow \Phi} +\sum_{\text{t}_i \in \mathcal{T}} s_{l \rightarrow \Phi}(\text{t}_{i}) \right) +$$ + +Where: + +- $\mathcal{L}$ is the set of all elements +- $\mathcal{T}$ is the set of all timesteps +- $s_{l \rightarrow \Phi}$ is the penalty share from element $l$ + +**Current usage:** Penalties primarily occur in [Buses](elements/Bus.md) via the `excess_penalty_per_flow_hour` parameter, which allows nodal imbalances at a high cost. + +--- + +## Objective Function + +The optimization objective minimizes the chosen effect plus any penalties: + +$$ \label{eq:Objective} +\min \left( E_{\Omega} + \Phi \right) +$$ + +Where: + +- $E_{\Omega}$ is the chosen **objective effect** (see $\eqref{eq:Effect_Total}$) +- $\Phi$ is the [penalty](#penalty) term + +One effect must be designated as the objective via `is_objective=True`. + +### Multi-Criteria Optimization + +This formulation supports multiple optimization approaches: + +**1. Weighted Sum Method** +- The objective effect can incorporate other effects via cross-effect factors +- Example: Minimize costs while including carbon pricing: $\text{CO}_2 \rightarrow \text{costs}$ + +**2. ε-Constraint Method** +- Optimize one effect while constraining others +- Example: Minimize costs subject to $\text{CO}_2 \leq 1000$ kg + +--- + +## Objective with Multiple Dimensions + +When the FlowSystem includes **periods** and/or **scenarios** (see [Dimensions](dimensions.md)), the objective aggregates effects across all dimensions using weights. + +### Time Only (Base Case) + +$$ +\min \quad E_{\Omega} + \Phi = \sum_{\text{t}_i \in \mathcal{T}} E_{\Omega,\text{temp}}(\text{t}_i) + E_{\Omega,\text{per}} + \Phi +$$ + +Where: +- Temporal effects sum over time: $\sum_{\text{t}_i} E_{\Omega,\text{temp}}(\text{t}_i)$ +- Periodic effects are constant: $E_{\Omega,\text{per}}$ +- Penalty sums over time: $\Phi = \sum_{\text{t}_i} \Phi(\text{t}_i)$ + +--- + +### Time + Scenario + +$$ +\min \quad \sum_{s \in \mathcal{S}} w_s \cdot \left( E_{\Omega}(s) + \Phi(s) \right) +$$ + +Where: +- $\mathcal{S}$ is the set of scenarios +- $w_s$ is the weight for scenario $s$ (typically scenario probability) +- Periodic effects are **shared across scenarios**: $E_{\Omega,\text{per}}$ (same for all $s$) +- Temporal effects are **scenario-specific**: $E_{\Omega,\text{temp}}(s) = \sum_{\text{t}_i} E_{\Omega,\text{temp}}(\text{t}_i, s)$ +- Penalties are **scenario-specific**: $\Phi(s) = \sum_{\text{t}_i} \Phi(\text{t}_i, s)$ + +**Interpretation:** +- Investment decisions (periodic) made once, used across all scenarios +- Operations (temporal) differ by scenario +- Objective balances expected value across scenarios + +--- + +### Time + Period + +$$ +\min \quad \sum_{y \in \mathcal{Y}} w_y \cdot \left( E_{\Omega}(y) + \Phi(y) \right) +$$ + +Where: +- $\mathcal{Y}$ is the set of periods (e.g., years) +- $w_y$ is the weight for period $y$ (typically annual discount factor) +- Each period $y$ has **independent** periodic and temporal effects +- Each period $y$ has **independent** investment and operational decisions + +--- + +### Time + Period + Scenario (Full Multi-Dimensional) + +$$ +\min \quad \sum_{y \in \mathcal{Y}} \left[ w_y \cdot E_{\Omega,\text{per}}(y) + \sum_{s \in \mathcal{S}} w_{y,s} \cdot \left( E_{\Omega,\text{temp}}(y,s) + \Phi(y,s) \right) \right] +$$ + +Where: +- $\mathcal{S}$ is the set of scenarios +- $\mathcal{Y}$ is the set of periods +- $w_y$ is the period weight (for periodic effects) +- $w_{y,s}$ is the combined period-scenario weight (for temporal effects) +- **Periodic effects** $E_{\Omega,\text{per}}(y)$ are period-specific but **scenario-independent** +- **Temporal effects** $E_{\Omega,\text{temp}}(y,s) = \sum_{\text{t}_i} E_{\Omega,\text{temp}}(\text{t}_i, y, s)$ are **fully indexed** +- **Penalties** $\Phi(y,s)$ are **fully indexed** + +**Key Principle:** +- Scenarios and periods are **operationally independent** (no energy/resource exchange) +- Coupled **only through the weighted objective function** +- **Periodic effects within a period are shared across all scenarios** (investment made once per period) +- **Temporal effects are independent per scenario** (different operations under different conditions) + +--- + +## Summary + +| Concept | Formulation | Time Dependency | Dimension Indexing | +|---------|-------------|-----------------|-------------------| +| **Temporal share** | $s_{l \rightarrow e, \text{temp}}(\text{t}_i)$ | Time-dependent | $(t, y, s)$ when present | +| **Periodic share** | $s_{l \rightarrow e, \text{per}}$ | Time-independent | $(y)$ when periods present | +| **Total temporal effect** | $E_{e,\text{temp},\text{tot}} = \sum_{\text{t}_i} E_{e,\text{temp}}(\text{t}_i)$ | Sum over time | Depends on dimensions | +| **Total periodic effect** | $E_{e,\text{per}}$ | Constant | $(y)$ when periods present | +| **Total effect** | $E_e = E_{e,\text{per}} + E_{e,\text{temp},\text{tot}}$ | Combined | Depends on dimensions | +| **Objective** | $\min(E_{\Omega} + \Phi)$ | With weights when multi-dimensional | See formulations above | + +--- + +## See Also + +- [Dimensions](dimensions.md) - Complete explanation of multi-dimensional modeling +- [Flow](elements/Flow.md) - Temporal effect contributions via `effects_per_flow_hour` +- [InvestParameters](features/InvestParameters.md) - Periodic effect contributions via investment +- [Effect API][flixopt.effects.Effect] - Implementation details and parameters diff --git a/docs/user-guide/Mathematical Notation/Bus.md b/docs/user-guide/mathematical-notation/elements/Bus.md similarity index 78% rename from docs/user-guide/Mathematical Notation/Bus.md rename to docs/user-guide/mathematical-notation/elements/Bus.md index 6ba17eede..bfe57d234 100644 --- a/docs/user-guide/Mathematical Notation/Bus.md +++ b/docs/user-guide/mathematical-notation/elements/Bus.md @@ -31,3 +31,19 @@ With: - $\text{t}_i$ being the time step - $s_{b \rightarrow \Phi}(\text{t}_i)$ being the penalty term - $\text a_{b \rightarrow \Phi}(\text{t}_i)$ being the penalty coefficient (`excess_penalty_per_flow_hour`) + +--- + +## Implementation + +**Python Class:** [`Bus`][flixopt.elements.Bus] + +See the API documentation for implementation details and usage examples. + +--- + +## See Also + +- [Flow](../elements/Flow.md) - Definition of flow rates in the balance +- [Effects, Penalty & Objective](../effects-penalty-objective.md) - How penalties are included in the objective function +- [Modeling Patterns](../modeling-patterns/index.md) - Mathematical building blocks diff --git a/docs/user-guide/mathematical-notation/elements/Flow.md b/docs/user-guide/mathematical-notation/elements/Flow.md new file mode 100644 index 000000000..5914ba911 --- /dev/null +++ b/docs/user-guide/mathematical-notation/elements/Flow.md @@ -0,0 +1,64 @@ +# Flow + +The flow_rate is the main optimization variable of the Flow. It's limited by the size of the Flow and relative bounds \eqref{eq:flow_rate}. + +$$ \label{eq:flow_rate} + \text P \cdot \text p^{\text{L}}_{\text{rel}}(\text{t}_{i}) + \leq p(\text{t}_{i}) \leq + \text P \cdot \text p^{\text{U}}_{\text{rel}}(\text{t}_{i}) +$$ + +With: + +- $\text P$ being the size of the Flow +- $p(\text{t}_{i})$ being the flow-rate at time $\text{t}_{i}$ +- $\text p^{\text{L}}_{\text{rel}}(\text{t}_{i})$ being the relative lower bound (typically 0) +- $\text p^{\text{U}}_{\text{rel}}(\text{t}_{i})$ being the relative upper bound (typically 1) + +With $\text p^{\text{L}}_{\text{rel}}(\text{t}_{i}) = 0$ and $\text p^{\text{U}}_{\text{rel}}(\text{t}_{i}) = 1$, +equation \eqref{eq:flow_rate} simplifies to + +$$ + 0 \leq p(\text{t}_{i}) \leq \text P +$$ + + +This mathematical formulation can be extended by using [OnOffParameters](../features/OnOffParameters.md) +to define the on/off state of the Flow, or by using [InvestParameters](../features/InvestParameters.md) +to change the size of the Flow from a constant to an optimization variable. + +--- + +## Mathematical Patterns Used + +Flow formulation uses the following modeling patterns: + +- **[Scaled Bounds](../modeling-patterns/bounds-and-states.md#scaled-bounds)** - Basic flow rate bounds (equation $\eqref{eq:flow_rate}$) +- **[Scaled Bounds with State](../modeling-patterns/bounds-and-states.md#scaled-bounds-with-state)** - When combined with [OnOffParameters](../features/OnOffParameters.md) +- **[Bounds with State](../modeling-patterns/bounds-and-states.md#bounds-with-state)** - Investment decisions with [InvestParameters](../features/InvestParameters.md) + +--- + +## Implementation + +**Python Class:** [`Flow`][flixopt.elements.Flow] + +**Key Parameters:** +- `size`: Flow size $\text{P}$ (can be fixed or variable with InvestParameters) +- `relative_minimum`, `relative_maximum`: Relative bounds $\text{p}^{\text{L}}_{\text{rel}}, \text{p}^{\text{U}}_{\text{rel}}$ +- `effects_per_flow_hour`: Operational effects (costs, emissions, etc.) +- `invest_parameters`: Optional investment modeling (see [InvestParameters](../features/InvestParameters.md)) +- `on_off_parameters`: Optional on/off operation (see [OnOffParameters](../features/OnOffParameters.md)) + +See the [`Flow`][flixopt.elements.Flow] API documentation for complete parameter list and usage examples. + +--- + +## See Also + +- [OnOffParameters](../features/OnOffParameters.md) - Binary on/off operation +- [InvestParameters](../features/InvestParameters.md) - Variable flow sizing +- [Bus](../elements/Bus.md) - Flow balance constraints +- [LinearConverter](../elements/LinearConverter.md) - Flow ratio constraints +- [Storage](../elements/Storage.md) - Flow integration over time +- [Modeling Patterns](../modeling-patterns/index.md) - Mathematical building blocks diff --git a/docs/user-guide/mathematical-notation/elements/LinearConverter.md b/docs/user-guide/mathematical-notation/elements/LinearConverter.md new file mode 100644 index 000000000..b007aa7f5 --- /dev/null +++ b/docs/user-guide/mathematical-notation/elements/LinearConverter.md @@ -0,0 +1,50 @@ +[`LinearConverters`][flixopt.components.LinearConverter] define a ratio between incoming and outgoing [Flows](../elements/Flow.md). + +$$ \label{eq:Linear-Transformer-Ratio} + \sum_{f_{\text{in}} \in \mathcal F_{in}} \text a_{f_{\text{in}}}(\text{t}_i) \cdot p_{f_\text{in}}(\text{t}_i) = \sum_{f_{\text{out}} \in \mathcal F_{out}} \text b_{f_\text{out}}(\text{t}_i) \cdot p_{f_\text{out}}(\text{t}_i) +$$ + +With: + +- $\mathcal F_{in}$ and $\mathcal F_{out}$ being the set of all incoming and outgoing flows +- $p_{f_\text{in}}(\text{t}_i)$ and $p_{f_\text{out}}(\text{t}_i)$ being the flow-rate at time $\text{t}_i$ for flow $f_\text{in}$ and $f_\text{out}$, respectively +- $\text a_{f_\text{in}}(\text{t}_i)$ and $\text b_{f_\text{out}}(\text{t}_i)$ being the ratio of the flow-rate at time $\text{t}_i$ for flow $f_\text{in}$ and $f_\text{out}$, respectively + +With one incoming **Flow** and one outgoing **Flow**, this can be simplified to: + +$$ \label{eq:Linear-Transformer-Ratio-simple} + \text a(\text{t}_i) \cdot p_{f_\text{in}}(\text{t}_i) = p_{f_\text{out}}(\text{t}_i) +$$ + +where $\text a$ can be interpreted as the conversion efficiency of the **LinearConverter**. + +#### Piecewise Conversion factors +The conversion efficiency can be defined as a piecewise linear approximation. See [Piecewise](../features/Piecewise.md) for more details. + +--- + +## Implementation + +**Python Class:** [`LinearConverter`][flixopt.components.LinearConverter] + +**Specialized Linear Converters:** + +FlixOpt provides specialized linear converter classes for common applications: + +- **[`HeatPump`][flixopt.linear_converters.HeatPump]** - Coefficient of Performance (COP) based conversion +- **[`Power2Heat`][flixopt.linear_converters.Power2Heat]** - Electric heating with efficiency ≤ 1 +- **[`CHP`][flixopt.linear_converters.CHP]** - Combined heat and power generation +- **[`Boiler`][flixopt.linear_converters.Boiler]** - Fuel to heat conversion + +These classes handle the mathematical formulation automatically based on physical relationships. + +See the API documentation for implementation details and usage examples. + +--- + +## See Also + +- [Flow](../elements/Flow.md) - Definition of flow rates +- [Piecewise](../features/Piecewise.md) - Non-linear conversion efficiency modeling +- [InvestParameters](../features/InvestParameters.md) - Variable converter sizing +- [Modeling Patterns](../modeling-patterns/index.md) - Mathematical building blocks diff --git a/docs/user-guide/Mathematical Notation/Storage.md b/docs/user-guide/mathematical-notation/elements/Storage.md similarity index 52% rename from docs/user-guide/Mathematical Notation/Storage.md rename to docs/user-guide/mathematical-notation/elements/Storage.md index 63f01d198..cd7046592 100644 --- a/docs/user-guide/Mathematical Notation/Storage.md +++ b/docs/user-guide/mathematical-notation/elements/Storage.md @@ -1,5 +1,5 @@ # Storages -**Storages** have one incoming and one outgoing **[Flow](Flow.md)** with a charging and discharging efficiency. +**Storages** have one incoming and one outgoing **[Flow](../elements/Flow.md)** with a charging and discharging efficiency. A storage has a state of charge $c(\text{t}_i)$ which is limited by its `size` $\text C$ and relative bounds $\eqref{eq:Storage_Bounds}$. $$ \label{eq:Storage_Bounds} @@ -25,9 +25,9 @@ $ \dot{ \text c}_\text{rel, loss}(\text{t}_i)$ expresses the "loss fraction per $$ \begin{align*} - c(\text{t}_{i+1}) &= c(\text{t}_{i}) \cdot (1-\dot{\text{c}}_\text{rel,loss}(\text{t}_i) \cdot \Delta \text{t}_{i}) \\ + c(\text{t}_{i+1}) &= c(\text{t}_{i}) \cdot (1-\dot{\text{c}}_\text{rel,loss}(\text{t}_i))^{\Delta \text{t}_{i}} \\ &\quad + p_{f_\text{in}}(\text{t}_i) \cdot \Delta \text{t}_i \cdot \eta_\text{in}(\text{t}_i) \\ - &\quad - \frac{p_{f_\text{out}}(\text{t}_i) \cdot \Delta \text{t}_i}{\eta_\text{out}(\text{t}_i)} + &\quad - p_{f_\text{out}}(\text{t}_i) \cdot \Delta \text{t}_i \cdot \eta_\text{out}(\text{t}_i) \tag{3} \end{align*} $$ @@ -42,3 +42,38 @@ Where: - $\eta_\text{in}(\text{t}_i)$ is the charging efficiency at time $\text{t}_i$ - $p_{f_\text{out}}(\text{t}_i)$ is the output flow rate at time $\text{t}_i$ - $\eta_\text{out}(\text{t}_i)$ is the discharging efficiency at time $\text{t}_i$ + +--- + +## Mathematical Patterns Used + +Storage formulation uses the following modeling patterns: + +- **[Basic Bounds](../modeling-patterns/bounds-and-states.md#basic-bounds)** - For charge state bounds (equation $\eqref{eq:Storage_Bounds}$) +- **[Scaled Bounds](../modeling-patterns/bounds-and-states.md#scaled-bounds)** - For flow rate bounds relative to storage size + +When combined with investment parameters, storage can use: +- **[Bounds with State](../modeling-patterns/bounds-and-states.md#bounds-with-state)** - Investment decisions (see [InvestParameters](../features/InvestParameters.md)) + +--- + +## Implementation + +**Python Class:** [`Storage`][flixopt.components.Storage] + +**Key Parameters:** +- `capacity_in_flow_hours`: Storage capacity $\text{C}$ +- `relative_loss_per_hour`: Self-discharge rate $\dot{\text{c}}_\text{rel,loss}$ +- `initial_charge_state`: Initial charge $c(\text{t}_0)$ +- `minimal_final_charge_state`, `maximal_final_charge_state`: Final charge bounds $c(\text{t}_\text{end})$ (optional) +- `eta_charge`, `eta_discharge`: Charging/discharging efficiencies $\eta_\text{in}, \eta_\text{out}$ + +See the [`Storage`][flixopt.components.Storage] API documentation for complete parameter list and usage examples. + +--- + +## See Also + +- [Flow](../elements/Flow.md) - Input and output flow definitions +- [InvestParameters](../features/InvestParameters.md) - Variable storage sizing +- [Modeling Patterns](../modeling-patterns/index.md) - Mathematical building blocks diff --git a/docs/user-guide/mathematical-notation/features/InvestParameters.md b/docs/user-guide/mathematical-notation/features/InvestParameters.md new file mode 100644 index 000000000..14fe02c79 --- /dev/null +++ b/docs/user-guide/mathematical-notation/features/InvestParameters.md @@ -0,0 +1,302 @@ +# InvestParameters + +[`InvestParameters`][flixopt.interface.InvestParameters] model investment decisions in optimization problems, enabling both binary (invest/don't invest) and continuous sizing choices with comprehensive cost modeling. + +## Investment Decision Types + +FlixOpt supports two main types of investment decisions: + +### Binary Investment + +Fixed-size investment creating a yes/no decision (e.g., install a 100 kW generator): + +$$\label{eq:invest_binary} +v_\text{invest} = s_\text{invest} \cdot \text{size}_\text{fixed} +$$ + +With: +- $v_\text{invest}$ being the resulting investment size +- $s_\text{invest} \in \{0, 1\}$ being the binary investment decision +- $\text{size}_\text{fixed}$ being the predefined component size + +**Behavior:** +- $s_\text{invest} = 0$: no investment ($v_\text{invest} = 0$) +- $s_\text{invest} = 1$: invest at fixed size ($v_\text{invest} = \text{size}_\text{fixed}$) + +--- + +### Continuous Sizing + +Variable-size investment with bounds (e.g., battery capacity from 10-1000 kWh): + +$$\label{eq:invest_continuous} +s_\text{invest} \cdot \text{size}_\text{min} \leq v_\text{invest} \leq s_\text{invest} \cdot \text{size}_\text{max} +$$ + +With: +- $v_\text{invest}$ being the investment size variable (continuous) +- $s_\text{invest} \in \{0, 1\}$ being the binary investment decision +- $\text{size}_\text{min}$ being the minimum investment size (if investing) +- $\text{size}_\text{max}$ being the maximum investment size + +**Behavior:** +- $s_\text{invest} = 0$: no investment ($v_\text{invest} = 0$) +- $s_\text{invest} = 1$: invest with size in $[\text{size}_\text{min}, \text{size}_\text{max}]$ + +This uses the **bounds with state** pattern described in [Bounds and States](../modeling-patterns/bounds-and-states.md#bounds-with-state). + +--- + +### Optional vs. Mandatory Investment + +The `mandatory` parameter controls whether investment is required: + +**Optional Investment** (`mandatory=False`, default): +$$\label{eq:invest_optional} +s_\text{invest} \in \{0, 1\} +$$ + +The optimization can freely choose to invest or not. + +**Mandatory Investment** (`mandatory=True`): +$$\label{eq:invest_mandatory} +s_\text{invest} = 1 +$$ + +The investment must occur (useful for mandatory upgrades or replacements). + +--- + +## Effect Modeling + +Investment effects (costs, emissions, etc.) are modeled using three components: + +### Fixed Effects + +One-time effects incurred if investment is made, independent of size: + +$$\label{eq:invest_fixed_effects} +E_{e,\text{fix}} = s_\text{invest} \cdot \text{fix}_e +$$ + +With: +- $E_{e,\text{fix}}$ being the fixed contribution to effect $e$ +- $\text{fix}_e$ being the fixed effect value (e.g., fixed installation cost) + +**Examples:** +- Fixed installation costs (permits, grid connection) +- One-time environmental impacts (land preparation) +- Fixed labor or administrative costs + +--- + +### Specific Effects + +Effects proportional to investment size (per-unit costs): + +$$\label{eq:invest_specific_effects} +E_{e,\text{spec}} = v_\text{invest} \cdot \text{spec}_e +$$ + +With: +- $E_{e,\text{spec}}$ being the size-dependent contribution to effect $e$ +- $\text{spec}_e$ being the specific effect value per unit size (e.g., €/kW) + +**Examples:** +- Equipment costs (€/kW) +- Material requirements (kg steel/kW) +- Recurring costs (€/kW/year maintenance) + +--- + +### Piecewise Effects + +Non-linear effect relationships using piecewise linear approximations: + +$$\label{eq:invest_piecewise_effects} +E_{e,\text{pw}} = \sum_{k=1}^{K} \lambda_k \cdot r_{e,k} +$$ + +Subject to: +$$ +v_\text{invest} = \sum_{k=1}^{K} \lambda_k \cdot v_k +$$ + +With: +- $E_{e,\text{pw}}$ being the piecewise contribution to effect $e$ +- $\lambda_k$ being the piecewise lambda variables (see [Piecewise](../features/Piecewise.md)) +- $r_{e,k}$ being the effect rate at piece $k$ +- $v_k$ being the size points defining the pieces + +**Use cases:** +- Economies of scale (bulk discounts) +- Technology learning curves +- Threshold effects (capacity tiers with different costs) + +See [Piecewise](../features/Piecewise.md) for detailed mathematical formulation. + +--- + +### Retirement Effects + +Effects incurred if investment is NOT made (when retiring/not replacing existing equipment): + +$$\label{eq:invest_retirement_effects} +E_{e,\text{retirement}} = (1 - s_\text{invest}) \cdot \text{retirement}_e +$$ + +With: +- $E_{e,\text{retirement}}$ being the retirement contribution to effect $e$ +- $\text{retirement}_e$ being the retirement effect value + +**Behavior:** +- $s_\text{invest} = 0$: retirement effects are incurred +- $s_\text{invest} = 1$: no retirement effects + +**Examples:** +- Demolition or disposal costs +- Decommissioning expenses +- Contractual penalties for not investing +- Opportunity costs or lost revenues + +--- + +### Total Investment Effects + +The total contribution to effect $e$ from an investment is: + +$$\label{eq:invest_total_effects} +E_{e,\text{invest}} = E_{e,\text{fix}} + E_{e,\text{spec}} + E_{e,\text{pw}} + E_{e,\text{retirement}} +$$ + +Effects integrate into the overall system effects as described in [Effects, Penalty & Objective](../effects-penalty-objective.md). + +--- + +## Integration with Components + +Investment parameters modify component sizing: + +### Without Investment +Component size is a fixed parameter: +$$ +\text{size} = \text{size}_\text{nominal} +$$ + +### With Investment +Component size becomes a variable: +$$ +\text{size} = v_\text{invest} +$$ + +This size variable then appears in component constraints. For example, flow rate bounds become: + +$$ +v_\text{invest} \cdot \text{rel}_\text{lower} \leq p(t) \leq v_\text{invest} \cdot \text{rel}_\text{upper} +$$ + +Using the **scaled bounds** pattern from [Bounds and States](../modeling-patterns/bounds-and-states.md#scaled-bounds). + +--- + +## Cost Annualization + +**Important:** All investment cost values must be properly weighted to match the optimization model's time horizon. + +For long-term investments, costs should be annualized: + +$$\label{eq:annualization} +\text{cost}_\text{annual} = \frac{\text{cost}_\text{capital} \cdot r}{1 - (1 + r)^{-n}} +$$ + +With: +- $\text{cost}_\text{capital}$ being the upfront investment cost +- $r$ being the discount rate +- $n$ being the equipment lifetime in years + +**Example:** €1,000,000 equipment with 20-year life and 5% discount rate +$$ +\text{cost}_\text{annual} = \frac{1{,}000{,}000 \cdot 0.05}{1 - (1.05)^{-20}} \approx €80{,}243/\text{year} +$$ + +--- + +## Implementation + +**Python Class:** [`InvestParameters`][flixopt.interface.InvestParameters] + +**Key Parameters:** +- `fixed_size`: For binary investments (mutually exclusive with continuous sizing) +- `minimum_size`, `maximum_size`: For continuous sizing +- `mandatory`: Whether investment is required (default: `False`) +- `effects_of_investment`: Fixed effects incurred when investing (replaces deprecated `fix_effects`) +- `effects_of_investment_per_size`: Per-unit effects proportional to size (replaces deprecated `specific_effects`) +- `piecewise_effects_of_investment`: Non-linear effect modeling (replaces deprecated `piecewise_effects`) +- `effects_of_retirement`: Effects for not investing (replaces deprecated `divest_effects`) + +See the [`InvestParameters`][flixopt.interface.InvestParameters] API documentation for complete parameter list and usage examples. + +**Used in:** +- [`Flow`][flixopt.elements.Flow] - Flexible capacity decisions +- [`Storage`][flixopt.components.Storage] - Storage sizing optimization +- [`LinearConverter`][flixopt.components.LinearConverter] - Converter capacity planning +- All components supporting investment decisions + +--- + +## Examples + +### Binary Investment (Solar Panels) +```python +solar_investment = InvestParameters( + fixed_size=100, # 100 kW system + mandatory=False, # Optional investment (default) + effects_of_investment={'cost': 25000}, # Installation costs + effects_of_investment_per_size={'cost': 1200}, # €1200/kW +) +``` + +### Continuous Sizing (Battery) +```python +battery_investment = InvestParameters( + minimum_size=10, # kWh + maximum_size=1000, + mandatory=False, # Optional investment (default) + effects_of_investment={'cost': 5000}, # Grid connection + effects_of_investment_per_size={'cost': 600}, # €600/kWh +) +``` + +### With Retirement Costs (Replacement) +```python +boiler_replacement = InvestParameters( + minimum_size=50, # kW + maximum_size=200, + mandatory=False, # Optional investment (default) + effects_of_investment={'cost': 15000}, + effects_of_investment_per_size={'cost': 400}, + effects_of_retirement={'cost': 8000}, # Demolition if not replaced +) +``` + +### Economies of Scale (Piecewise) +```python +battery_investment = InvestParameters( + minimum_size=10, + maximum_size=1000, + piecewise_effects_of_investment=PiecewiseEffects( + piecewise_origin=Piecewise([ + Piece(0, 100), # Small + Piece(100, 500), # Medium + Piece(500, 1000), # Large + ]), + piecewise_shares={ + 'cost': Piecewise([ + Piece(800, 750), # €800-750/kWh + Piece(750, 600), # €750-600/kWh + Piece(600, 500), # €600-500/kWh (bulk discount) + ]) + }, + ), +) +``` diff --git a/docs/user-guide/mathematical-notation/features/OnOffParameters.md b/docs/user-guide/mathematical-notation/features/OnOffParameters.md new file mode 100644 index 000000000..4ec6a9726 --- /dev/null +++ b/docs/user-guide/mathematical-notation/features/OnOffParameters.md @@ -0,0 +1,307 @@ +# OnOffParameters + +[`OnOffParameters`][flixopt.interface.OnOffParameters] model equipment that operates in discrete on/off states rather than continuous operation. This captures realistic operational constraints including startup costs, minimum run times, cycling limitations, and maintenance scheduling. + +## Binary State Variable + +Equipment operation is modeled using a binary state variable: + +$$\label{eq:onoff_state} +s(t) \in \{0, 1\} \quad \forall t +$$ + +With: +- $s(t) = 1$: equipment is operating (on state) +- $s(t) = 0$: equipment is shutdown (off state) + +This state variable controls the equipment's operational constraints and modifies flow bounds using the **bounds with state** pattern from [Bounds and States](../modeling-patterns/bounds-and-states.md#bounds-with-state). + +--- + +## State Transitions and Switching + +State transitions are tracked using switch variables (see [State Transitions](../modeling-patterns/state-transitions.md#binary-state-transitions)): + +$$\label{eq:onoff_transitions} +s^\text{on}(t) - s^\text{off}(t) = s(t) - s(t-1) \quad \forall t > 0 +$$ + +$$\label{eq:onoff_switch_exclusivity} +s^\text{on}(t) + s^\text{off}(t) \leq 1 \quad \forall t +$$ + +With: +- $s^\text{on}(t) \in \{0, 1\}$: equals 1 when switching from off to on (startup) +- $s^\text{off}(t) \in \{0, 1\}$: equals 1 when switching from on to off (shutdown) + +**Behavior:** +- Off → On: $s^\text{on}(t) = 1, s^\text{off}(t) = 0$ +- On → Off: $s^\text{on}(t) = 0, s^\text{off}(t) = 1$ +- No change: $s^\text{on}(t) = 0, s^\text{off}(t) = 0$ + +--- + +## Effects and Costs + +### Switching Effects + +Effects incurred when equipment starts up: + +$$\label{eq:onoff_switch_effects} +E_{e,\text{switch}} = \sum_{t} s^\text{on}(t) \cdot \text{effect}_{e,\text{switch}} +$$ + +With: +- $\text{effect}_{e,\text{switch}}$ being the effect value per startup event + +**Examples:** +- Startup fuel consumption +- Wear and tear costs +- Labor costs for startup procedures +- Inrush power demands + +--- + +### Running Effects + +Effects incurred while equipment is operating: + +$$\label{eq:onoff_running_effects} +E_{e,\text{run}} = \sum_{t} s(t) \cdot \Delta t \cdot \text{effect}_{e,\text{run}} +$$ + +With: +- $\text{effect}_{e,\text{run}}$ being the effect rate per operating hour +- $\Delta t$ being the time step duration + +**Examples:** +- Fixed operating and maintenance costs +- Auxiliary power consumption +- Consumable materials +- Emissions while running + +--- + +## Operating Hour Constraints + +### Total Operating Hours + +Bounds on total operating time across the planning horizon: + +$$\label{eq:onoff_total_hours} +h_\text{min} \leq \sum_{t} s(t) \cdot \Delta t \leq h_\text{max} +$$ + +With: +- $h_\text{min}$ being the minimum total operating hours +- $h_\text{max}$ being the maximum total operating hours + +**Use cases:** +- Minimum runtime requirements (contracts, maintenance) +- Maximum runtime limits (fuel availability, permits, equipment life) + +--- + +### Consecutive Operating Hours + +**Minimum Consecutive On-Time:** + +Enforces minimum runtime once started using duration tracking (see [Duration Tracking](../modeling-patterns/duration-tracking.md#minimum-duration-constraints)): + +$$\label{eq:onoff_min_on_duration} +d^\text{on}(t) \geq (s(t-1) - s(t)) \cdot h^\text{on}_\text{min} \quad \forall t > 0 +$$ + +With: +- $d^\text{on}(t)$ being the consecutive on-time duration at time $t$ +- $h^\text{on}_\text{min}$ being the minimum required on-time + +**Behavior:** +- When shutting down at time $t$: enforces equipment was on for at least $h^\text{on}_\text{min}$ prior to the switch +- Prevents short cycling and frequent startups + +**Maximum Consecutive On-Time:** + +Limits continuous operation before requiring shutdown: + +$$\label{eq:onoff_max_on_duration} +d^\text{on}(t) \leq h^\text{on}_\text{max} \quad \forall t +$$ + +**Use cases:** +- Mandatory maintenance intervals +- Process batch time limits +- Thermal cycling requirements + +--- + +### Consecutive Shutdown Hours + +**Minimum Consecutive Off-Time:** + +Enforces minimum shutdown duration before restarting: + +$$\label{eq:onoff_min_off_duration} +d^\text{off}(t) \geq (s(t) - s(t-1)) \cdot h^\text{off}_\text{min} \quad \forall t > 0 +$$ + +With: +- $d^\text{off}(t)$ being the consecutive off-time duration at time $t$ +- $h^\text{off}_\text{min}$ being the minimum required off-time + +**Use cases:** +- Cooling periods +- Maintenance requirements +- Process stabilization + +**Maximum Consecutive Off-Time:** + +Limits shutdown duration before mandatory restart: + +$$\label{eq:onoff_max_off_duration} +d^\text{off}(t) \leq h^\text{off}_\text{max} \quad \forall t +$$ + +**Use cases:** +- Equipment preservation requirements +- Process stability needs +- Contractual minimum activity levels + +--- + +## Cycling Limits + +Maximum number of startups across the planning horizon: + +$$\label{eq:onoff_max_switches} +\sum_{t} s^\text{on}(t) \leq n_\text{max} +$$ + +With: +- $n_\text{max}$ being the maximum allowed number of startups + +**Use cases:** +- Preventing excessive equipment wear +- Grid stability requirements +- Operational complexity limits +- Maintenance budget constraints + +--- + +## Integration with Flow Bounds + +OnOffParameters modify flow rate bounds by coupling them to the on/off state. + +**Without OnOffParameters** (continuous operation): +$$ +P \cdot \text{rel}_\text{lower} \leq p(t) \leq P \cdot \text{rel}_\text{upper} +$$ + +**With OnOffParameters** (binary operation): +$$ +s(t) \cdot P \cdot \max(\varepsilon, \text{rel}_\text{lower}) \leq p(t) \leq s(t) \cdot P \cdot \text{rel}_\text{upper} +$$ + +Using the **bounds with state** pattern from [Bounds and States](../modeling-patterns/bounds-and-states.md#bounds-with-state). + +**Behavior:** +- When $s(t) = 0$: flow is forced to zero +- When $s(t) = 1$: flow follows normal bounds + +--- + +## Complete Formulation Summary + +For equipment with OnOffParameters, the complete constraint system includes: + +1. **State variable:** $s(t) \in \{0, 1\}$ +2. **Switch tracking:** $s^\text{on}(t) - s^\text{off}(t) = s(t) - s(t-1)$ +3. **Switch exclusivity:** $s^\text{on}(t) + s^\text{off}(t) \leq 1$ +4. **Duration tracking:** + - On-duration: $d^\text{on}(t)$ following duration tracking pattern + - Off-duration: $d^\text{off}(t)$ following duration tracking pattern +5. **Minimum on-time:** $d^\text{on}(t) \geq (s(t-1) - s(t)) \cdot h^\text{on}_\text{min}$ +6. **Maximum on-time:** $d^\text{on}(t) \leq h^\text{on}_\text{max}$ +7. **Minimum off-time:** $d^\text{off}(t) \geq (s(t) - s(t-1)) \cdot h^\text{off}_\text{min}$ +8. **Maximum off-time:** $d^\text{off}(t) \leq h^\text{off}_\text{max}$ +9. **Total hours:** $h_\text{min} \leq \sum_t s(t) \cdot \Delta t \leq h_\text{max}$ +10. **Cycling limit:** $\sum_t s^\text{on}(t) \leq n_\text{max}$ +11. **Flow bounds:** $s(t) \cdot P \cdot \text{rel}_\text{lower} \leq p(t) \leq s(t) \cdot P \cdot \text{rel}_\text{upper}$ + +--- + +## Implementation + +**Python Class:** [`OnOffParameters`][flixopt.interface.OnOffParameters] + +**Key Parameters:** +- `effects_per_switch_on`: Costs per startup event +- `effects_per_running_hour`: Costs per hour of operation +- `on_hours_total_min`, `on_hours_total_max`: Total runtime bounds +- `consecutive_on_hours_min`, `consecutive_on_hours_max`: Consecutive runtime bounds +- `consecutive_off_hours_min`, `consecutive_off_hours_max`: Consecutive shutdown bounds +- `switch_on_total_max`: Maximum number of startups +- `force_switch_on`: Create switch variables even without limits (for tracking) + +See the [`OnOffParameters`][flixopt.interface.OnOffParameters] API documentation for complete parameter list and usage examples. + +**Mathematical Patterns Used:** +- [State Transitions](../modeling-patterns/state-transitions.md#binary-state-transitions) - Switch tracking +- [Duration Tracking](../modeling-patterns/duration-tracking.md) - Consecutive time constraints +- [Bounds with State](../modeling-patterns/bounds-and-states.md#bounds-with-state) - Flow control + +**Used in:** +- [`Flow`][flixopt.elements.Flow] - On/off operation for flows +- All components supporting discrete operational states + +--- + +## Examples + +### Power Plant with Startup Costs +```python +power_plant = OnOffParameters( + effects_per_switch_on={'startup_cost': 25000}, # €25k per startup + effects_per_running_hour={'fixed_om': 125}, # €125/hour while running + consecutive_on_hours_min=8, # Minimum 8-hour run + consecutive_off_hours_min=4, # 4-hour cooling period + on_hours_total_max=6000, # Annual limit +) +``` + +### Batch Process with Cycling Limits +```python +batch_reactor = OnOffParameters( + effects_per_switch_on={'setup_cost': 1500}, + consecutive_on_hours_min=12, # 12-hour minimum batch + consecutive_on_hours_max=24, # 24-hour maximum batch + consecutive_off_hours_min=6, # Cleaning time + switch_on_total_max=200, # Max 200 batches +) +``` + +### HVAC with Cycle Prevention +```python +hvac = OnOffParameters( + effects_per_switch_on={'compressor_wear': 0.5}, + consecutive_on_hours_min=1, # Prevent short cycling + consecutive_off_hours_min=0.5, # 30-min minimum off + switch_on_total_max=2000, # Limit compressor starts +) +``` + +### Backup Generator with Testing Requirements +```python +backup_gen = OnOffParameters( + effects_per_switch_on={'fuel_priming': 50}, # L diesel + consecutive_on_hours_min=0.5, # 30-min test duration + consecutive_off_hours_max=720, # Test every 30 days + on_hours_total_min=26, # Weekly testing requirement +) +``` + +--- + +## Notes + +**Time Series Boundary:** The final time period constraints for consecutive_on_hours_min/max and consecutive_off_hours_min/max are not enforced at the end of the planning horizon. This allows optimization to end with ongoing campaigns that may be shorter/longer than specified, as they extend beyond the modeled period. diff --git a/docs/user-guide/Mathematical Notation/Piecewise.md b/docs/user-guide/mathematical-notation/features/Piecewise.md similarity index 100% rename from docs/user-guide/Mathematical Notation/Piecewise.md rename to docs/user-guide/mathematical-notation/features/Piecewise.md diff --git a/docs/user-guide/mathematical-notation/index.md b/docs/user-guide/mathematical-notation/index.md new file mode 100644 index 000000000..ae89f3b67 --- /dev/null +++ b/docs/user-guide/mathematical-notation/index.md @@ -0,0 +1,123 @@ + +# Mathematical Notation + +This section provides the **mathematical formulations** underlying FlixOpt's optimization models. It is intended as **reference documentation** for users who want to understand the mathematical details behind the high-level FlixOpt API described in the [FlixOpt Concepts](../index.md) guide. + +**For typical usage**, refer to the [FlixOpt Concepts](../index.md) guide, [Examples](../../examples/), and [API Reference](../../api-reference/) - you don't need to understand these mathematical formulations to use FlixOpt effectively. + +--- + +## Naming Conventions + +FlixOpt uses the following naming conventions: + +- All optimization variables are denoted by italic letters (e.g., $x$, $y$, $z$) +- All parameters and constants are denoted by non italic small letters (e.g., $\text{a}$, $\text{b}$, $\text{c}$) +- All Sets are denoted by greek capital letters (e.g., $\mathcal{F}$, $\mathcal{E}$) +- All units of a set are denoted by greek small letters (e.g., $\mathcal{f}$, $\mathcal{e}$) +- The letter $i$ is used to denote an index (e.g., $i=1,\dots,\text n$) +- All time steps are denoted by the letter $\text{t}$ (e.g., $\text{t}_0$, $\text{t}_1$, $\text{t}_i$) + +## Dimensions and Time Steps + +FlixOpt supports multi-dimensional optimization with up to three dimensions: **time** (mandatory), **period** (optional), and **scenario** (optional). + +**All mathematical formulations in this documentation are independent of whether periods or scenarios are present.** The equations shown are written with time index $\text{t}_i$ only, but automatically expand to additional dimensions when periods/scenarios are added. + +For complete details on dimensions, their relationships, and influence on formulations, see **[Dimensions](dimensions.md)**. + +### Time Steps + +Time steps are defined as a sequence of discrete time steps $\text{t}_i \in \mathcal{T} \quad \text{for} \quad i \in \{1, 2, \dots, \text{n}\}$ (left-aligned in its timespan). +From this sequence, the corresponding time intervals $\Delta \text{t}_i \in \Delta \mathcal{T}$ are derived as + +$$\Delta \text{t}_i = \text{t}_{i+1} - \text{t}_i \quad \text{for} \quad i \in \{1, 2, \dots, \text{n}-1\}$$ + +The final time interval $\Delta \text{t}_\text n$ defaults to $\Delta \text{t}_\text n = \Delta \text{t}_{\text n-1}$, but is of course customizable. +Non-equidistant time steps are also supported. + +--- + +## Documentation Structure + +This reference is organized to match the FlixOpt API structure: + +### Elements +Mathematical formulations for core FlixOpt elements (corresponding to [`flixopt.elements`][flixopt.elements]): + +- [Flow](elements/Flow.md) - Flow rate constraints and bounds +- [Bus](elements/Bus.md) - Nodal balance equations +- [Storage](elements/Storage.md) - Storage balance and charge state evolution +- [LinearConverter](elements/LinearConverter.md) - Linear conversion relationships + +**User API:** When you create a `Flow`, `Bus`, `Storage`, or `LinearConverter` in your FlixOpt model, these mathematical formulations are automatically applied. + +### Features +Mathematical formulations for optional features (corresponding to parameters in FlixOpt classes): + +- [InvestParameters](features/InvestParameters.md) - Investment decision modeling +- [OnOffParameters](features/OnOffParameters.md) - Binary on/off operation +- [Piecewise](features/Piecewise.md) - Piecewise linear approximations + +**User API:** When you pass `invest_parameters` or `on_off_parameters` to a `Flow` or component, these formulations are applied. + +### System-Level +- [Effects, Penalty & Objective](effects-penalty-objective.md) - Cost allocation and objective function + +**User API:** When you create [`Effect`][flixopt.effects.Effect] objects and set `effects_per_flow_hour`, these formulations govern how costs are calculated. + +### Modeling Patterns (Advanced) +**Internal implementation details** - These low-level patterns are used internally by Elements and Features. They are documented here for: + +- Developers extending FlixOpt +- Advanced users debugging models or understanding solver behavior +- Researchers comparing mathematical formulations + +**Normal users do not need to read this section** - the patterns are automatically applied when you use Elements and Features: + +- [Bounds and States](modeling-patterns/bounds-and-states.md) - Variable bounding patterns +- [Duration Tracking](modeling-patterns/duration-tracking.md) - Consecutive time period tracking +- [State Transitions](modeling-patterns/state-transitions.md) - State change modeling + +--- + +## Quick Reference + +### Components Cross-Reference + +| Concept | Documentation | Python Class | +|---------|---------------|--------------| +| **Flow rate bounds** | [Flow](elements/Flow.md) | [`Flow`][flixopt.elements.Flow] | +| **Bus balance** | [Bus](elements/Bus.md) | [`Bus`][flixopt.elements.Bus] | +| **Storage balance** | [Storage](elements/Storage.md) | [`Storage`][flixopt.components.Storage] | +| **Linear conversion** | [LinearConverter](elements/LinearConverter.md) | [`LinearConverter`][flixopt.components.LinearConverter] | + +### Features Cross-Reference + +| Concept | Documentation | Python Class | +|---------|---------------|--------------| +| **Binary investment** | [InvestParameters](features/InvestParameters.md) | [`InvestParameters`][flixopt.interface.InvestParameters] | +| **On/off operation** | [OnOffParameters](features/OnOffParameters.md) | [`OnOffParameters`][flixopt.interface.OnOffParameters] | +| **Piecewise segments** | [Piecewise](features/Piecewise.md) | [`Piecewise`][flixopt.interface.Piecewise] | + +### Modeling Patterns Cross-Reference + +| Pattern | Documentation | Implementation | +|---------|---------------|----------------| +| **Basic bounds** | [bounds-and-states](modeling-patterns/bounds-and-states.md#basic-bounds) | [`BoundingPatterns.basic_bounds()`][flixopt.modeling.BoundingPatterns.basic_bounds] | +| **Bounds with state** | [bounds-and-states](modeling-patterns/bounds-and-states.md#bounds-with-state) | [`BoundingPatterns.bounds_with_state()`][flixopt.modeling.BoundingPatterns.bounds_with_state] | +| **Scaled bounds** | [bounds-and-states](modeling-patterns/bounds-and-states.md#scaled-bounds) | [`BoundingPatterns.scaled_bounds()`][flixopt.modeling.BoundingPatterns.scaled_bounds] | +| **Duration tracking** | [duration-tracking](modeling-patterns/duration-tracking.md) | [`ModelingPrimitives.consecutive_duration_tracking()`][flixopt.modeling.ModelingPrimitives.consecutive_duration_tracking] | +| **State transitions** | [state-transitions](modeling-patterns/state-transitions.md) | [`BoundingPatterns.state_transition_bounds()`][flixopt.modeling.BoundingPatterns.state_transition_bounds] | + +### Python Class Lookup + +| Class | Documentation | API Reference | +|-------|---------------|---------------| +| `Flow` | [Flow](elements/Flow.md) | [`Flow`][flixopt.elements.Flow] | +| `Bus` | [Bus](elements/Bus.md) | [`Bus`][flixopt.elements.Bus] | +| `Storage` | [Storage](elements/Storage.md) | [`Storage`][flixopt.components.Storage] | +| `LinearConverter` | [LinearConverter](elements/LinearConverter.md) | [`LinearConverter`][flixopt.components.LinearConverter] | +| `InvestParameters` | [InvestParameters](features/InvestParameters.md) | [`InvestParameters`][flixopt.interface.InvestParameters] | +| `OnOffParameters` | [OnOffParameters](features/OnOffParameters.md) | [`OnOffParameters`][flixopt.interface.OnOffParameters] | +| `Piecewise` | [Piecewise](features/Piecewise.md) | [`Piecewise`][flixopt.interface.Piecewise] | diff --git a/docs/user-guide/mathematical-notation/modeling-patterns/bounds-and-states.md b/docs/user-guide/mathematical-notation/modeling-patterns/bounds-and-states.md new file mode 100644 index 000000000..d5821948f --- /dev/null +++ b/docs/user-guide/mathematical-notation/modeling-patterns/bounds-and-states.md @@ -0,0 +1,165 @@ +# Bounds and States + +This document describes the mathematical formulations for variable bounding patterns used throughout FlixOpt. These patterns define how optimization variables are constrained, both with and without state control. + +## Basic Bounds + +The simplest bounding pattern constrains a variable between lower and upper bounds. + +$$\label{eq:basic_bounds} +\text{lower} \leq v \leq \text{upper} +$$ + +With: +- $v$ being the optimization variable +- $\text{lower}$ being the lower bound (constant or time-dependent) +- $\text{upper}$ being the upper bound (constant or time-dependent) + +**Implementation:** [`BoundingPatterns.basic_bounds()`][flixopt.modeling.BoundingPatterns.basic_bounds] + +**Used in:** +- Storage charge state bounds (see [Storage](../elements/Storage.md)) +- Flow rate absolute bounds + +--- + +## Bounds with State + +When a variable should only be non-zero if a binary state variable is active (e.g., on/off operation, investment decisions), the bounds are controlled by the state: + +$$\label{eq:bounds_with_state} +s \cdot \max(\varepsilon, \text{lower}) \leq v \leq s \cdot \text{upper} +$$ + +With: +- $v$ being the optimization variable +- $s \in \{0, 1\}$ being the binary state variable +- $\text{lower}$ being the lower bound when active +- $\text{upper}$ being the upper bound when active +- $\varepsilon$ being a small positive number to ensure numerical stability + +**Behavior:** +- When $s = 0$: variable is forced to zero ($0 \leq v \leq 0$) +- When $s = 1$: variable can take values in $[\text{lower}, \text{upper}]$ + +**Implementation:** [`BoundingPatterns.bounds_with_state()`][flixopt.modeling.BoundingPatterns.bounds_with_state] + +**Used in:** +- Flow rates with on/off operation (see [OnOffParameters](../features/OnOffParameters.md)) +- Investment size decisions (see [InvestParameters](../features/InvestParameters.md)) + +--- + +## Scaled Bounds + +When a variable's bounds depend on another variable (e.g., flow rate scaled by component size), scaled bounds are used: + +$$\label{eq:scaled_bounds} +v_\text{scale} \cdot \text{rel}_\text{lower} \leq v \leq v_\text{scale} \cdot \text{rel}_\text{upper} +$$ + +With: +- $v$ being the optimization variable (e.g., flow rate) +- $v_\text{scale}$ being the scaling variable (e.g., component size) +- $\text{rel}_\text{lower}$ being the relative lower bound factor (typically 0) +- $\text{rel}_\text{upper}$ being the relative upper bound factor (typically 1) + +**Example:** Flow rate bounds +- If $v_\text{scale} = P$ (flow size) and $\text{rel}_\text{upper} = 1$ +- Then: $0 \leq p(t_i) \leq P$ (see [Flow](../elements/Flow.md)) + +**Implementation:** [`BoundingPatterns.scaled_bounds()`][flixopt.modeling.BoundingPatterns.scaled_bounds] + +**Used in:** +- Flow rate constraints (see [Flow](../elements/Flow.md) equation 1) +- Storage charge state constraints (see [Storage](../elements/Storage.md) equation 1) + +--- + +## Scaled Bounds with State + +Combining scaled bounds with binary state control requires a Big-M formulation to handle both the scaling and the on/off behavior: + +$$\label{eq:scaled_bounds_with_state_1} +(s - 1) \cdot M_\text{misc} + v_\text{scale} \cdot \text{rel}_\text{lower} \leq v \leq v_\text{scale} \cdot \text{rel}_\text{upper} +$$ + +$$\label{eq:scaled_bounds_with_state_2} +s \cdot M_\text{lower} \leq v \leq s \cdot M_\text{upper} +$$ + +With: +- $v$ being the optimization variable +- $v_\text{scale}$ being the scaling variable +- $s \in \{0, 1\}$ being the binary state variable +- $\text{rel}_\text{lower}$ being the relative lower bound factor +- $\text{rel}_\text{upper}$ being the relative upper bound factor +- $M_\text{misc} = v_\text{scale,max} \cdot \text{rel}_\text{lower}$ +- $M_\text{upper} = v_\text{scale,max} \cdot \text{rel}_\text{upper}$ +- $M_\text{lower} = \max(\varepsilon, v_\text{scale,min} \cdot \text{rel}_\text{lower})$ + +Where $v_\text{scale,max}$ and $v_\text{scale,min}$ are the maximum and minimum possible values of the scaling variable. + +**Behavior:** +- When $s = 0$: variable is forced to zero +- When $s = 1$: variable follows scaled bounds $v_\text{scale} \cdot \text{rel}_\text{lower} \leq v \leq v_\text{scale} \cdot \text{rel}_\text{upper}$ + +**Implementation:** [`BoundingPatterns.scaled_bounds_with_state()`][flixopt.modeling.BoundingPatterns.scaled_bounds_with_state] + +**Used in:** +- Flow rates with on/off operation and investment sizing +- Components combining [OnOffParameters](../features/OnOffParameters.md) and [InvestParameters](../features/InvestParameters.md) + +--- + +## Expression Tracking + +Sometimes it's necessary to create an auxiliary variable that equals an expression: + +$$\label{eq:expression_tracking} +v_\text{tracker} = \text{expression} +$$ + +With optional bounds: + +$$\label{eq:expression_tracking_bounds} +\text{lower} \leq v_\text{tracker} \leq \text{upper} +$$ + +With: +- $v_\text{tracker}$ being the auxiliary tracking variable +- $\text{expression}$ being a linear expression of other variables +- $\text{lower}, \text{upper}$ being optional bounds on the tracker + +**Use cases:** +- Creating named variables for complex expressions +- Bounding intermediate results +- Simplifying constraint formulations + +**Implementation:** [`ModelingPrimitives.expression_tracking_variable()`][flixopt.modeling.ModelingPrimitives.expression_tracking_variable] + +--- + +## Mutual Exclusivity + +When multiple binary variables should not be active simultaneously (at most one can be 1): + +$$\label{eq:mutual_exclusivity} +\sum_{i} s_i(t) \leq \text{tolerance} \quad \forall t +$$ + +With: +- $s_i(t) \in \{0, 1\}$ being binary state variables +- $\text{tolerance}$ being the maximum number of simultaneously active states (typically 1) +- $t$ being the time index + +**Use cases:** +- Ensuring only one operating mode is active +- Mutual exclusion of operation and maintenance states +- Enforcing single-choice decisions + +**Implementation:** [`ModelingPrimitives.mutual_exclusivity_constraint()`][flixopt.modeling.ModelingPrimitives.mutual_exclusivity_constraint] + +**Used in:** +- Operating mode selection +- Piecewise linear function segments (see [Piecewise](../features/Piecewise.md)) diff --git a/docs/user-guide/mathematical-notation/modeling-patterns/duration-tracking.md b/docs/user-guide/mathematical-notation/modeling-patterns/duration-tracking.md new file mode 100644 index 000000000..5d430d28c --- /dev/null +++ b/docs/user-guide/mathematical-notation/modeling-patterns/duration-tracking.md @@ -0,0 +1,159 @@ +# Duration Tracking + +Duration tracking allows monitoring how long a binary state has been consecutively active. This is essential for modeling minimum run times, ramp-up periods, and similar time-dependent constraints. + +## Consecutive Duration Tracking + +For a binary state variable $s(t) \in \{0, 1\}$, the consecutive duration $d(t)$ tracks how long the state has been continuously active. + +### Duration Upper Bound + +The duration cannot exceed zero when the state is inactive: + +$$\label{eq:duration_upper} +d(t) \leq s(t) \cdot M \quad \forall t +$$ + +With: +- $d(t)$ being the duration variable (continuous, non-negative) +- $s(t) \in \{0, 1\}$ being the binary state variable +- $M$ being a sufficiently large constant (big-M) + +**Behavior:** +- When $s(t) = 0$: forces $d(t) \leq 0$, thus $d(t) = 0$ +- When $s(t) = 1$: allows $d(t)$ to be positive + +--- + +### Duration Accumulation + +While the state is active, the duration increases by the time step size: + +$$\label{eq:duration_accumulation_upper} +d(t+1) \leq d(t) + \Delta d(t) \quad \forall t +$$ + +$$\label{eq:duration_accumulation_lower} +d(t+1) \geq d(t) + \Delta d(t) + (s(t+1) - 1) \cdot M \quad \forall t +$$ + +With: +- $\Delta d(t)$ being the duration increment for time step $t$ (typically $\Delta t_i$ from the time series) +- $M$ being a sufficiently large constant + +**Behavior:** +- When $s(t+1) = 1$: both inequalities enforce $d(t+1) = d(t) + \Delta d(t)$ +- When $s(t+1) = 0$: only the upper bound applies, and $d(t+1) = 0$ (from equation $\eqref{eq:duration_upper}$) + +--- + +### Initial Duration + +The duration at the first time step depends on both the state and any previous duration: + +$$\label{eq:duration_initial} +d(0) = (\Delta d(0) + d_\text{prev}) \cdot s(0) +$$ + +With: +- $d_\text{prev}$ being the duration from before the optimization period +- $\Delta d(0)$ being the duration increment for the first time step + +**Behavior:** +- When $s(0) = 1$: duration continues from previous period +- When $s(0) = 0$: duration resets to zero + +--- + +### Complete Formulation + +Combining all constraints: + +$$ +\begin{align} +d(t) &\leq s(t) \cdot M && \forall t \label{eq:duration_complete_1} \\ +d(t+1) &\leq d(t) + \Delta d(t) && \forall t \label{eq:duration_complete_2} \\ +d(t+1) &\geq d(t) + \Delta d(t) + (s(t+1) - 1) \cdot M && \forall t \label{eq:duration_complete_3} \\ +d(0) &= (\Delta d(0) + d_\text{prev}) \cdot s(0) && \label{eq:duration_complete_4} +\end{align} +$$ + +--- + +## Minimum Duration Constraints + +To enforce a minimum consecutive duration (e.g., minimum run time), an additional constraint links the duration to state changes: + +$$\label{eq:minimum_duration} +d(t) \geq (s(t-1) - s(t)) \cdot d_\text{min}(t-1) \quad \forall t > 0 +$$ + +With: +- $d_\text{min}(t)$ being the required minimum duration at time $t$ + +**Behavior:** +- When shutting down ($s(t-1) = 1, s(t) = 0$): enforces $d(t-1) \geq d_\text{min}(t-1)$ +- This ensures the state was active for at least $d_\text{min}$ before turning off +- When state is constant or turning on: constraint is non-binding + +--- + +## Implementation + +**Function:** [`ModelingPrimitives.consecutive_duration_tracking()`][flixopt.modeling.ModelingPrimitives.consecutive_duration_tracking] + +See the API documentation for complete parameter list and usage details. + +--- + +## Use Cases + +### Minimum Run Time + +Ensuring equipment runs for a minimum duration once started: + +```python +# State: 1 when running, 0 when off +# Require at least 2 hours of operation +duration = modeling.consecutive_duration_tracking( + state_variable=on_state, + duration_per_step=time_step_hours, + minimum_duration=2.0 +) +``` + +### Ramp-Up Tracking + +Tracking time since startup for gradual ramp-up constraints: + +```python +# Track startup duration +startup_duration = modeling.consecutive_duration_tracking( + state_variable=on_state, + duration_per_step=time_step_hours +) +# Constrain output based on startup duration +# (additional constraints would link output to startup_duration) +``` + +### Cooldown Requirements + +Tracking time in a state before allowing transitions: + +```python +# Track maintenance duration +maintenance_duration = modeling.consecutive_duration_tracking( + state_variable=maintenance_state, + duration_per_step=time_step_hours, + minimum_duration=scheduled_maintenance_hours +) +``` + +--- + +## Used In + +This pattern is used in: +- [`OnOffParameters`](../features/OnOffParameters.md) - Minimum on/off times +- Operating mode constraints with minimum durations +- Startup/shutdown sequence modeling diff --git a/docs/user-guide/mathematical-notation/modeling-patterns/index.md b/docs/user-guide/mathematical-notation/modeling-patterns/index.md new file mode 100644 index 000000000..15ff8dbd2 --- /dev/null +++ b/docs/user-guide/mathematical-notation/modeling-patterns/index.md @@ -0,0 +1,54 @@ +# Modeling Patterns + +This section documents the fundamental mathematical patterns used throughout FlixOpt for constructing optimization models. These patterns are implemented in `flixopt.modeling` and provide reusable building blocks for creating constraints. + +## Overview + +The modeling patterns are organized into three categories: + +1. **[Bounds and States](bounds-and-states.md)** - Variable bounding with optional state control +2. **[Duration Tracking](duration-tracking.md)** - Tracking consecutive durations of states +3. **[State Transitions](state-transitions.md)** - Modeling state changes and transitions + +## Pattern Categories + +### Bounding Patterns + +These patterns define how optimization variables are constrained within bounds: + +- **Basic Bounds** - Simple upper and lower bounds on variables +- **Bounds with State** - Binary-controlled bounds (on/off states) +- **Scaled Bounds** - Bounds dependent on another variable (e.g., size) +- **Scaled Bounds with State** - Combination of scaling and binary control + +### Tracking Patterns + +These patterns track properties over time: + +- **Expression Tracking** - Creating auxiliary variables that track expressions +- **Consecutive Duration Tracking** - Tracking how long a state has been active +- **Mutual Exclusivity** - Ensuring only one of multiple options is active + +### Transition Patterns + +These patterns model changes between states: + +- **State Transitions** - Tracking switches between binary states (on→off, off→on) +- **Continuous Transitions** - Linking continuous variable changes to switches +- **Level Changes with Binaries** - Controlled increases/decreases in levels + +## Usage in Components + +These patterns are used throughout FlixOpt components: + +- [`Flow`][flixopt.elements.Flow] uses **scaled bounds with state** for flow rate constraints +- [`Storage`][flixopt.components.Storage] uses **basic bounds** for charge state +- [`OnOffParameters`](../features/OnOffParameters.md) uses **state transitions** for startup/shutdown +- [`InvestParameters`](../features/InvestParameters.md) uses **bounds with state** for investment decisions + +## Implementation + +All patterns are implemented in [`flixopt.modeling`][flixopt.modeling] module: + +- [`ModelingPrimitives`][flixopt.modeling.ModelingPrimitives] - Core constraint patterns +- [`BoundingPatterns`][flixopt.modeling.BoundingPatterns] - Specialized bounding patterns diff --git a/docs/user-guide/mathematical-notation/modeling-patterns/state-transitions.md b/docs/user-guide/mathematical-notation/modeling-patterns/state-transitions.md new file mode 100644 index 000000000..dc75a8008 --- /dev/null +++ b/docs/user-guide/mathematical-notation/modeling-patterns/state-transitions.md @@ -0,0 +1,227 @@ +# State Transitions + +State transition patterns model changes between discrete states and link them to continuous variables. These patterns are essential for modeling startup/shutdown events, switching behavior, and controlled changes in system operation. + +## Binary State Transitions + +For a binary state variable $s(t) \in \{0, 1\}$, state transitions track when the state switches on or off. + +### Switch Variables + +Two binary variables track the transitions: +- $s^\text{on}(t) \in \{0, 1\}$: equals 1 when switching from off to on +- $s^\text{off}(t) \in \{0, 1\}$: equals 1 when switching from on to off + +### Transition Tracking + +The state change equals the difference between switch-on and switch-off: + +$$\label{eq:state_transition} +s^\text{on}(t) - s^\text{off}(t) = s(t) - s(t-1) \quad \forall t > 0 +$$ + +$$\label{eq:state_transition_initial} +s^\text{on}(0) - s^\text{off}(0) = s(0) - s_\text{prev} +$$ + +With: +- $s(t)$ being the binary state variable +- $s_\text{prev}$ being the state before the optimization period +- $s^\text{on}(t), s^\text{off}(t)$ being the switch variables + +**Behavior:** +- Off → On ($s(t-1)=0, s(t)=1$): $s^\text{on}(t)=1, s^\text{off}(t)=0$ +- On → Off ($s(t-1)=1, s(t)=0$): $s^\text{on}(t)=0, s^\text{off}(t)=1$ +- No change: $s^\text{on}(t)=0, s^\text{off}(t)=0$ + +--- + +### Mutual Exclusivity of Switches + +A state cannot switch on and off simultaneously: + +$$\label{eq:switch_exclusivity} +s^\text{on}(t) + s^\text{off}(t) \leq 1 \quad \forall t +$$ + +This ensures: +- At most one switch event per time step +- No simultaneous on/off switching + +--- + +### Complete State Transition Formulation + +$$ +\begin{align} +s^\text{on}(t) - s^\text{off}(t) &= s(t) - s(t-1) && \forall t > 0 \label{eq:transition_complete_1} \\ +s^\text{on}(0) - s^\text{off}(0) &= s(0) - s_\text{prev} && \label{eq:transition_complete_2} \\ +s^\text{on}(t) + s^\text{off}(t) &\leq 1 && \forall t \label{eq:transition_complete_3} \\ +s^\text{on}(t), s^\text{off}(t) &\in \{0, 1\} && \forall t \label{eq:transition_complete_4} +\end{align} +$$ + +**Implementation:** [`BoundingPatterns.state_transition_bounds()`][flixopt.modeling.BoundingPatterns.state_transition_bounds] + +--- + +## Continuous Transitions + +When a continuous variable should only change when certain switch events occur, continuous transition bounds link the variable changes to binary switches. + +### Change Bounds with Switches + +$$\label{eq:continuous_transition} +-\Delta v^\text{max} \cdot (s^\text{on}(t) + s^\text{off}(t)) \leq v(t) - v(t-1) \leq \Delta v^\text{max} \cdot (s^\text{on}(t) + s^\text{off}(t)) \quad \forall t > 0 +$$ + +$$\label{eq:continuous_transition_initial} +-\Delta v^\text{max} \cdot (s^\text{on}(0) + s^\text{off}(0)) \leq v(0) - v_\text{prev} \leq \Delta v^\text{max} \cdot (s^\text{on}(0) + s^\text{off}(0)) +$$ + +With: +- $v(t)$ being the continuous variable +- $v_\text{prev}$ being the value before the optimization period +- $\Delta v^\text{max}$ being the maximum allowed change +- $s^\text{on}(t), s^\text{off}(t) \in \{0, 1\}$ being switch binary variables + +**Behavior:** +- When $s^\text{on}(t) = 0$ and $s^\text{off}(t) = 0$: forces $v(t) = v(t-1)$ (no change) +- When $s^\text{on}(t) = 1$ or $s^\text{off}(t) = 1$: allows change up to $\pm \Delta v^\text{max}$ + +**Implementation:** [`BoundingPatterns.continuous_transition_bounds()`][flixopt.modeling.BoundingPatterns.continuous_transition_bounds] + +--- + +## Level Changes with Binaries + +This pattern models a level variable that can increase or decrease, with changes controlled by binary variables. This is useful for inventory management, capacity adjustments, or gradual state changes. + +### Level Evolution + +The level evolves based on increases and decreases: + +$$\label{eq:level_initial} +\ell(0) = \ell_\text{init} + \ell^\text{inc}(0) - \ell^\text{dec}(0) +$$ + +$$\label{eq:level_evolution} +\ell(t) = \ell(t-1) + \ell^\text{inc}(t) - \ell^\text{dec}(t) \quad \forall t > 0 +$$ + +With: +- $\ell(t)$ being the level variable +- $\ell_\text{init}$ being the initial level +- $\ell^\text{inc}(t)$ being the increase in level at time $t$ (non-negative) +- $\ell^\text{dec}(t)$ being the decrease in level at time $t$ (non-negative) + +--- + +### Change Bounds with Binary Control + +Changes are bounded and controlled by binary variables: + +$$\label{eq:increase_bound} +\ell^\text{inc}(t) \leq \Delta \ell^\text{max} \cdot b^\text{inc}(t) \quad \forall t +$$ + +$$\label{eq:decrease_bound} +\ell^\text{dec}(t) \leq \Delta \ell^\text{max} \cdot b^\text{dec}(t) \quad \forall t +$$ + +With: +- $\Delta \ell^\text{max}$ being the maximum change per time step +- $b^\text{inc}(t), b^\text{dec}(t) \in \{0, 1\}$ being binary control variables + +--- + +### Mutual Exclusivity of Changes + +Simultaneous increase and decrease are prevented: + +$$\label{eq:change_exclusivity} +b^\text{inc}(t) + b^\text{dec}(t) \leq 1 \quad \forall t +$$ + +This ensures: +- Level can only increase OR decrease (or stay constant) in each time step +- No simultaneous contradictory changes + +--- + +### Complete Level Change Formulation + +$$ +\begin{align} +\ell(0) &= \ell_\text{init} + \ell^\text{inc}(0) - \ell^\text{dec}(0) && \label{eq:level_complete_1} \\ +\ell(t) &= \ell(t-1) + \ell^\text{inc}(t) - \ell^\text{dec}(t) && \forall t > 0 \label{eq:level_complete_2} \\ +\ell^\text{inc}(t) &\leq \Delta \ell^\text{max} \cdot b^\text{inc}(t) && \forall t \label{eq:level_complete_3} \\ +\ell^\text{dec}(t) &\leq \Delta \ell^\text{max} \cdot b^\text{dec}(t) && \forall t \label{eq:level_complete_4} \\ +b^\text{inc}(t) + b^\text{dec}(t) &\leq 1 && \forall t \label{eq:level_complete_5} \\ +b^\text{inc}(t), b^\text{dec}(t) &\in \{0, 1\} && \forall t \label{eq:level_complete_6} +\end{align} +$$ + +**Implementation:** [`BoundingPatterns.link_changes_to_level_with_binaries()`][flixopt.modeling.BoundingPatterns.link_changes_to_level_with_binaries] + +--- + +## Use Cases + +### Startup/Shutdown Costs + +Track startup and shutdown events to apply costs: + +```python +# Create switch variables +switch_on, switch_off = modeling.state_transition_bounds( + state_variable=on_state, + previous_state=previous_on_state +) + +# Apply costs to switches +startup_cost = switch_on * startup_cost_per_event +shutdown_cost = switch_off * shutdown_cost_per_event +``` + +### Limited Switching + +Restrict the number of state changes: + +```python +# Track all switches +switch_on, switch_off = modeling.state_transition_bounds( + state_variable=on_state +) + +# Limit total switches +model.add_constraint( + (switch_on + switch_off).sum() <= max_switches +) +``` + +### Gradual Capacity Changes + +Model systems where capacity can be incrementally adjusted: + +```python +# Level represents installed capacity +level_var, increase, decrease, inc_binary, dec_binary = \ + modeling.link_changes_to_level_with_binaries( + initial_level=current_capacity, + max_change=max_capacity_change_per_period + ) + +# Constrain total increases +model.add_constraint(increase.sum() <= max_total_expansion) +``` + +--- + +## Used In + +These patterns are used in: +- [`OnOffParameters`](../features/OnOffParameters.md) - Startup/shutdown tracking and costs +- Operating mode switching with transition costs +- Investment planning with staged capacity additions +- Inventory management with controlled stock changes diff --git a/docs/user-guide/Mathematical Notation/others.md b/docs/user-guide/mathematical-notation/others.md similarity index 100% rename from docs/user-guide/Mathematical Notation/others.md rename to docs/user-guide/mathematical-notation/others.md diff --git a/docs/user-guide/recipes/index.md b/docs/user-guide/recipes/index.md new file mode 100644 index 000000000..8ac7d1812 --- /dev/null +++ b/docs/user-guide/recipes/index.md @@ -0,0 +1,47 @@ +# Recipes + +**Coming Soon!** 🚧 + +This section will contain quick, copy-paste ready code snippets for common FlixOpt patterns. + +--- + +## What Will Be Here? + +Short, focused code snippets showing **how to do specific things** in FlixOpt: + +- Common modeling patterns +- Integration with other tools +- Performance optimizations +- Domain-specific solutions +- Data analysis shortcuts + +Unlike full examples, recipes will be focused snippets showing a single concept. + +--- + +## Planned Topics + +- **Storage Patterns** - Batteries, thermal storage, seasonal storage +- **Multi-Criteria Optimization** - Balance multiple objectives +- **Data I/O** - Loading time series from CSV, databases, APIs +- **Data Manipulation** - Common xarray operations for parameterization and analysis +- **Investment Optimization** - Size optimization strategies +- **Renewable Integration** - Solar, wind capacity optimization +- **On/Off Constraints** - Minimum runtime, startup costs +- **Large-Scale Problems** - Segmented and aggregated calculations +- **Custom Constraints** - Extend models with linopy +- **Domain-Specific Patterns** - District heating, microgrids, industrial processes + +--- + +## Want to Contribute? + +**We need your help!** If you have recurring modeling patterns or clever solutions to share, please contribute via [GitHub issues](https://github.com/flixopt/flixopt/issues) or pull requests. + +Guidelines: +1. Keep it short (< 100 lines of code) +2. Focus on one specific technique +3. Add brief explanation and when to use it + +Check the [contribution guide](../../contribute.md) for details. diff --git a/examples/00_Minmal/minimal_example.py b/examples/00_Minmal/minimal_example.py index e9ef241ff..81b7c2dba 100644 --- a/examples/00_Minmal/minimal_example.py +++ b/examples/00_Minmal/minimal_example.py @@ -9,6 +9,9 @@ import flixopt as fx if __name__ == '__main__': + # Enable console logging + fx.CONFIG.Logging.console = True + fx.CONFIG.apply() # --- Define the Flow System, that will hold all elements, and the time steps you want to model --- timesteps = pd.date_range('2020-01-01', periods=3, freq='h') flow_system = fx.FlowSystem(timesteps) @@ -37,13 +40,15 @@ # Heat load component with a fixed thermal demand profile heat_load = fx.Sink( 'Heat Demand', - sink=fx.Flow(label='Thermal Load', bus='District Heating', size=1, fixed_relative_profile=thermal_load_profile), + inputs=[ + fx.Flow(label='Thermal Load', bus='District Heating', size=1, fixed_relative_profile=thermal_load_profile) + ], ) # Gas source component with cost-effect per flow hour gas_source = fx.Source( 'Natural Gas Tariff', - source=fx.Flow(label='Gas Flow', bus='Natural Gas', size=1000, effects_per_flow_hour=0.04), # 0.04 €/kWh + outputs=[fx.Flow(label='Gas Flow', bus='Natural Gas', size=1000, effects_per_flow_hour=0.04)], # 0.04 €/kWh ) # --- Build the Flow System --- diff --git a/examples/01_Simple/simple_example.py b/examples/01_Simple/simple_example.py index 8239f805a..ee90af47a 100644 --- a/examples/01_Simple/simple_example.py +++ b/examples/01_Simple/simple_example.py @@ -8,6 +8,9 @@ import flixopt as fx if __name__ == '__main__': + # Enable console logging + fx.CONFIG.Logging.console = True + fx.CONFIG.apply() # --- Create Time Series Data --- # Heat demand profile (e.g., kW) over time and corresponding power prices heat_demand_per_h = np.array([30, 0, 90, 110, 110, 20, 20, 20, 20]) @@ -29,6 +32,7 @@ description='Kosten', is_standard=True, # standard effect: no explicit value needed for costs is_objective=True, # Minimizing costs as the optimization objective + share_from_temporal={'CO2': 0.2}, ) # CO2 emissions effect with an associated cost impact @@ -36,8 +40,7 @@ label='CO2', unit='kg', description='CO2_e-Emissionen', - specific_share_to_other_effects_operation={costs.label: 0.2}, - maximum_operation_per_hour=1000, # Max CO2 emissions per hour + maximum_per_hour=1000, # Max CO2 emissions per hour ) # --- Define Flow System Components --- @@ -64,9 +67,10 @@ label='Storage', charging=fx.Flow('Q_th_load', bus='Fernwärme', size=1000), discharging=fx.Flow('Q_th_unload', bus='Fernwärme', size=1000), - capacity_in_flow_hours=fx.InvestParameters(fix_effects=20, fixed_size=30, optional=False), + capacity_in_flow_hours=fx.InvestParameters(effects_of_investment=20, fixed_size=30, mandatory=True), initial_charge_state=0, # Initial storage state: empty - relative_maximum_charge_state=1 / 100 * np.array([80, 70, 80, 80, 80, 80, 80, 80, 80, 80]), + relative_maximum_charge_state=1 / 100 * np.array([80, 70, 80, 80, 80, 80, 80, 80, 80]), + relative_maximum_final_charge_state=0.8, eta_charge=0.9, eta_discharge=1, # Efficiency factors for charging/discharging relative_loss_per_hour=0.08, # 8% loss per hour. Absolute loss depends on current charge state @@ -76,18 +80,20 @@ # Heat Demand Sink: Represents a fixed heat demand profile heat_sink = fx.Sink( label='Heat Demand', - sink=fx.Flow(label='Q_th_Last', bus='Fernwärme', size=1, fixed_relative_profile=heat_demand_per_h), + inputs=[fx.Flow(label='Q_th_Last', bus='Fernwärme', size=1, fixed_relative_profile=heat_demand_per_h)], ) # Gas Source: Gas tariff source with associated costs and CO2 emissions gas_source = fx.Source( label='Gastarif', - source=fx.Flow(label='Q_Gas', bus='Gas', size=1000, effects_per_flow_hour={costs.label: 0.04, CO2.label: 0.3}), + outputs=[ + fx.Flow(label='Q_Gas', bus='Gas', size=1000, effects_per_flow_hour={costs.label: 0.04, CO2.label: 0.3}) + ], ) # Power Sink: Represents the export of electricity to the grid power_sink = fx.Sink( - label='Einspeisung', sink=fx.Flow(label='P_el', bus='Strom', effects_per_flow_hour=-1 * power_prices) + label='Einspeisung', inputs=[fx.Flow(label='P_el', bus='Strom', effects_per_flow_hour=-1 * power_prices)] ) # --- Build the Flow System --- diff --git a/examples/02_Complex/complex_example.py b/examples/02_Complex/complex_example.py index 175211c26..805cb08f6 100644 --- a/examples/02_Complex/complex_example.py +++ b/examples/02_Complex/complex_example.py @@ -9,6 +9,9 @@ import flixopt as fx if __name__ == '__main__': + # Enable console logging + fx.CONFIG.Logging.console = True + fx.CONFIG.apply() # --- Experiment Options --- # Configure options for testing various parameters and behaviors check_penalty = False @@ -40,8 +43,8 @@ # --- Define Effects --- # Specify effects related to costs, CO2 emissions, and primary energy consumption - Costs = fx.Effect('costs', '€', 'Kosten', is_standard=True, is_objective=True) - CO2 = fx.Effect('CO2', 'kg', 'CO2_e-Emissionen', specific_share_to_other_effects_operation={Costs.label: 0.2}) + Costs = fx.Effect('costs', '€', 'Kosten', is_standard=True, is_objective=True, share_from_temporal={'CO2': 0.2}) + CO2 = fx.Effect('CO2', 'kg', 'CO2_e-Emissionen') PE = fx.Effect('PE', 'kWh_PE', 'Primärenergie', maximum_total=3.5e3) # --- Define Components --- @@ -57,10 +60,10 @@ label='Q_th', # Thermal output bus='Fernwärme', # Linked bus size=fx.InvestParameters( - fix_effects=1000, # Fixed investment costs + effects_of_investment=1000, # Fixed investment costs fixed_size=50, # Fixed size - optional=False, # Forced investment - specific_effects={Costs.label: 10, PE.label: 2}, # Specific costs + mandatory=True, # Forced investment + effects_of_investment_per_size={Costs.label: 10, PE.label: 2}, # Specific costs ), load_factor_max=1.0, # Maximum load factor (50 kW) load_factor_min=0.1, # Minimum load factor (5 kW) @@ -72,9 +75,8 @@ on_hours_total_min=0, # Minimum operating hours on_hours_total_max=1000, # Maximum operating hours consecutive_on_hours_max=10, # Max consecutive operating hours - consecutive_on_hours_min=np.array( - [1, 1, 1, 1, 1, 2, 2, 2, 2] - ), # min consecutive operation hoursconsecutive_off_hours_max=10, # Max consecutive off hours + consecutive_on_hours_min=np.array([1, 1, 1, 1, 1, 2, 2, 2, 2]), # min consecutive operation hours + consecutive_off_hours_max=10, # Max consecutive off hours effects_per_switch_on=0.01, # Cost per switch-on switch_on_total_max=1000, # Max number of starts ), @@ -130,8 +132,8 @@ charging=fx.Flow('Q_th_load', bus='Fernwärme', size=1e4), discharging=fx.Flow('Q_th_unload', bus='Fernwärme', size=1e4), capacity_in_flow_hours=fx.InvestParameters( - piecewise_effects=segmented_investment_effects, # Investment effects - optional=False, # Forced investment + piecewise_effects_of_investment=segmented_investment_effects, # Investment effects + mandatory=True, # Forced investment minimum_size=0, maximum_size=1000, # Optimizing between 0 and 1000 kWh ), @@ -147,33 +149,39 @@ # 5.a) Heat demand profile Waermelast = fx.Sink( 'Wärmelast', - sink=fx.Flow( - 'Q_th_Last', # Heat sink - bus='Fernwärme', # Linked bus - size=1, - fixed_relative_profile=heat_demand, # Fixed demand profile - ), + inputs=[ + fx.Flow( + 'Q_th_Last', # Heat sink + bus='Fernwärme', # Linked bus + size=1, + fixed_relative_profile=heat_demand, # Fixed demand profile + ) + ], ) # 5.b) Gas tariff Gasbezug = fx.Source( 'Gastarif', - source=fx.Flow( - 'Q_Gas', - bus='Gas', # Gas source - size=1000, # Nominal size - effects_per_flow_hour={Costs.label: 0.04, CO2.label: 0.3}, - ), + outputs=[ + fx.Flow( + 'Q_Gas', + bus='Gas', # Gas source + size=1000, # Nominal size + effects_per_flow_hour={Costs.label: 0.04, CO2.label: 0.3}, + ) + ], ) # 5.c) Feed-in of electricity Stromverkauf = fx.Sink( 'Einspeisung', - sink=fx.Flow( - 'P_el', - bus='Strom', # Feed-in tariff for electricity - effects_per_flow_hour=-1 * electricity_price, # Negative price for feed-in - ), + inputs=[ + fx.Flow( + 'P_el', + bus='Strom', # Feed-in tariff for electricity + effects_per_flow_hour=-1 * electricity_price, # Negative price for feed-in + ) + ], ) # --- Build FlowSystem --- @@ -182,7 +190,10 @@ flow_system.add_elements(bhkw_2) if use_chp_with_piecewise_conversion else flow_system.add_elements(bhkw) pprint(flow_system) # Get a string representation of the FlowSystem - flow_system.start_network_app() # Start the network app. DOes only work with extra dependencies installed + try: + flow_system.start_network_app() # Start the network app + except ImportError as e: + print(f'Network app requires extra dependencies: {e}') # --- Solve FlowSystem --- calculation = fx.FullCalculation('complex example', flow_system, time_indices) diff --git a/examples/02_Complex/complex_example_results.py b/examples/02_Complex/complex_example_results.py index 3be201ae8..5020f71fe 100644 --- a/examples/02_Complex/complex_example_results.py +++ b/examples/02_Complex/complex_example_results.py @@ -5,6 +5,9 @@ import flixopt as fx if __name__ == '__main__': + # Enable console logging + fx.CONFIG.Logging.console = True + fx.CONFIG.apply() # --- Load Results --- try: results = fx.results.CalculationResults.from_file('results', 'complex example') diff --git a/examples/03_Calculation_types/example_calculation_types.py b/examples/03_Calculation_types/example_calculation_types.py index a92a20163..05b25e782 100644 --- a/examples/03_Calculation_types/example_calculation_types.py +++ b/examples/03_Calculation_types/example_calculation_types.py @@ -11,6 +11,9 @@ import flixopt as fx if __name__ == '__main__': + # Enable console logging + fx.CONFIG.Logging.console = True + fx.CONFIG.apply() # Calculation Types full, segmented, aggregated = True, True, True @@ -30,7 +33,9 @@ excess_penalty = 1e5 # or set to None if not needed # Data Import - data_import = pd.read_csv(pathlib.Path('Zeitreihen2020.csv'), index_col=0).sort_index() + data_import = pd.read_csv( + pathlib.Path(__file__).parent.parent / 'resources' / 'Zeitreihen2020.csv', index_col=0 + ).sort_index() filtered_data = data_import['2020-01-01':'2020-01-02 23:45:00'] # filtered_data = data_import[0:500] # Alternatively filter by index @@ -45,9 +50,9 @@ # TimeSeriesData objects TS_heat_demand = fx.TimeSeriesData(heat_demand) - TS_electricity_demand = fx.TimeSeriesData(electricity_demand, agg_weight=0.7) - TS_electricity_price_sell = fx.TimeSeriesData(-(electricity_demand - 0.5), agg_group='p_el') - TS_electricity_price_buy = fx.TimeSeriesData(electricity_price + 0.5, agg_group='p_el') + TS_electricity_demand = fx.TimeSeriesData(electricity_demand, aggregation_weight=0.7) + TS_electricity_price_sell = fx.TimeSeriesData(-(electricity_price - 0.5), aggregation_group='p_el') + TS_electricity_price_buy = fx.TimeSeriesData(electricity_price + 0.5, aggregation_group='p_el') flow_system = fx.FlowSystem(timesteps) flow_system.add_elements( @@ -108,36 +113,43 @@ # 4. Sinks and Sources # Heat Load Profile a_waermelast = fx.Sink( - 'Wärmelast', sink=fx.Flow('Q_th_Last', bus='Fernwärme', size=1, fixed_relative_profile=TS_heat_demand) + 'Wärmelast', inputs=[fx.Flow('Q_th_Last', bus='Fernwärme', size=1, fixed_relative_profile=TS_heat_demand)] ) # Electricity Feed-in a_strom_last = fx.Sink( - 'Stromlast', sink=fx.Flow('P_el_Last', bus='Strom', size=1, fixed_relative_profile=TS_electricity_demand) + 'Stromlast', inputs=[fx.Flow('P_el_Last', bus='Strom', size=1, fixed_relative_profile=TS_electricity_demand)] ) # Gas Tariff a_gas_tarif = fx.Source( 'Gastarif', - source=fx.Flow('Q_Gas', bus='Gas', size=1000, effects_per_flow_hour={costs.label: gas_price, CO2.label: 0.3}), + outputs=[ + fx.Flow('Q_Gas', bus='Gas', size=1000, effects_per_flow_hour={costs.label: gas_price, CO2.label: 0.3}) + ], ) # Coal Tariff a_kohle_tarif = fx.Source( 'Kohletarif', - source=fx.Flow('Q_Kohle', bus='Kohle', size=1000, effects_per_flow_hour={costs.label: 4.6, CO2.label: 0.3}), + outputs=[fx.Flow('Q_Kohle', bus='Kohle', size=1000, effects_per_flow_hour={costs.label: 4.6, CO2.label: 0.3})], ) # Electricity Tariff and Feed-in a_strom_einspeisung = fx.Sink( - 'Einspeisung', sink=fx.Flow('P_el', bus='Strom', size=1000, effects_per_flow_hour=TS_electricity_price_sell) + 'Einspeisung', inputs=[fx.Flow('P_el', bus='Strom', size=1000, effects_per_flow_hour=TS_electricity_price_sell)] ) a_strom_tarif = fx.Source( 'Stromtarif', - source=fx.Flow( - 'P_el', bus='Strom', size=1000, effects_per_flow_hour={costs.label: TS_electricity_price_buy, CO2: 0.3} - ), + outputs=[ + fx.Flow( + 'P_el', + bus='Strom', + size=1000, + effects_per_flow_hour={costs.label: TS_electricity_price_buy, CO2.label: 0.3}, + ) + ], ) # Flow System Setup @@ -161,12 +173,12 @@ if full: calculation = fx.FullCalculation('Full', flow_system) calculation.do_modeling() - calculation.solve(fx.solvers.HighsSolver(0, 60)) + calculation.solve(fx.solvers.HighsSolver(0.01 / 100, 60)) calculations.append(calculation) if segmented: calculation = fx.SegmentedCalculation('Segmented', flow_system, segment_length, overlap_length) - calculation.do_modeling_and_solve(fx.solvers.HighsSolver(0, 60)) + calculation.do_modeling_and_solve(fx.solvers.HighsSolver(0.01 / 100, 60)) calculations.append(calculation) if aggregated: @@ -175,7 +187,7 @@ aggregation_parameters.time_series_for_low_peaks = [TS_electricity_demand, TS_heat_demand] calculation = fx.AggregatedCalculation('Aggregated', flow_system, aggregation_parameters) calculation.do_modeling() - calculation.solve(fx.solvers.HighsSolver(0, 60)) + calculation.solve(fx.solvers.HighsSolver(0.01 / 100, 60)) calculations.append(calculation) # Get solutions for plotting for different calculations @@ -191,34 +203,35 @@ def get_solutions(calcs: list, variable: str) -> xr.Dataset: # --- Plotting for comparison --- fx.plotting.with_plotly( get_solutions(calculations, 'Speicher|charge_state').to_dataframe(), - mode='line', + style='line', title='Charge State Comparison', ylabel='Charge state', ).write_html('results/Charge State.html') fx.plotting.with_plotly( get_solutions(calculations, 'BHKW2(Q_th)|flow_rate').to_dataframe(), - mode='line', + style='line', title='BHKW2(Q_th) Flow Rate Comparison', ylabel='Flow rate', ).write_html('results/BHKW2 Thermal Power.html') fx.plotting.with_plotly( - get_solutions(calculations, 'costs(operation)|total_per_timestep').to_dataframe(), - mode='line', + get_solutions(calculations, 'costs(temporal)|per_timestep').to_dataframe(), + style='line', title='Operation Cost Comparison', ylabel='Costs [€]', ).write_html('results/Operation Costs.html') fx.plotting.with_plotly( - pd.DataFrame(get_solutions(calculations, 'costs(operation)|total_per_timestep').to_dataframe().sum()).T, - mode='bar', + pd.DataFrame(get_solutions(calculations, 'costs(temporal)|per_timestep').to_dataframe().sum()).T, + style='stacked_bar', title='Total Cost Comparison', ylabel='Costs [€]', ).update_layout(barmode='group').write_html('results/Total Costs.html') fx.plotting.with_plotly( - pd.DataFrame([calc.durations for calc in calculations], index=[calc.name for calc in calculations]), 'bar' + pd.DataFrame([calc.durations for calc in calculations], index=[calc.name for calc in calculations]), + 'stacked_bar', ).update_layout(title='Duration Comparison', xaxis_title='Calculation type', yaxis_title='Time (s)').write_html( 'results/Speed Comparison.html' ) diff --git a/examples/04_Scenarios/scenario_example.py b/examples/04_Scenarios/scenario_example.py new file mode 100644 index 000000000..f06760603 --- /dev/null +++ b/examples/04_Scenarios/scenario_example.py @@ -0,0 +1,144 @@ +""" +This script shows how to use the flixopt framework to model a simple energy system. +""" + +import numpy as np +import pandas as pd + +import flixopt as fx + +if __name__ == '__main__': + # Create datetime array starting from '2020-01-01' for the given time period + timesteps = pd.date_range('2020-01-01', periods=9, freq='h') + scenarios = pd.Index(['Base Case', 'High Demand']) + periods = pd.Index([2020, 2021, 2022]) + + # --- Create Time Series Data --- + # Heat demand profile (e.g., kW) over time and corresponding power prices + heat_demand_per_h = pd.DataFrame( + {'Base Case': [30, 0, 90, 110, 110, 20, 20, 20, 20], 'High Demand': [30, 0, 100, 118, 125, 20, 20, 20, 20]}, + index=timesteps, + ) + power_prices = np.array([0.08, 0.09, 0.10]) + + flow_system = fx.FlowSystem(timesteps=timesteps, periods=periods, scenarios=scenarios, weights=np.array([0.5, 0.6])) + + # --- Define Energy Buses --- + # These represent nodes, where the used medias are balanced (electricity, heat, and gas) + flow_system.add_elements(fx.Bus(label='Strom'), fx.Bus(label='Fernwärme'), fx.Bus(label='Gas')) + + # --- Define Effects (Objective and CO2 Emissions) --- + # Cost effect: used as the optimization objective --> minimizing costs + costs = fx.Effect( + label='costs', + unit='€', + description='Kosten', + is_standard=True, # standard effect: no explicit value needed for costs + is_objective=True, # Minimizing costs as the optimization objective + share_from_temporal={'CO2': 0.2}, + ) + + # CO2 emissions effect with an associated cost impact + CO2 = fx.Effect( + label='CO2', + unit='kg', + description='CO2_e-Emissionen', + maximum_per_hour=1000, # Max CO2 emissions per hour + ) + + # --- Define Flow System Components --- + # Boiler: Converts fuel (gas) into thermal energy (heat) + boiler = fx.linear_converters.Boiler( + label='Boiler', + eta=0.5, + Q_th=fx.Flow( + label='Q_th', + bus='Fernwärme', + size=50, + relative_minimum=0.1, + relative_maximum=1, + on_off_parameters=fx.OnOffParameters(), + ), + Q_fu=fx.Flow(label='Q_fu', bus='Gas'), + ) + + # Combined Heat and Power (CHP): Generates both electricity and heat from fuel + chp = fx.linear_converters.CHP( + label='CHP', + eta_th=0.5, + eta_el=0.4, + P_el=fx.Flow('P_el', bus='Strom', size=60, relative_minimum=5 / 60, on_off_parameters=fx.OnOffParameters()), + Q_th=fx.Flow('Q_th', bus='Fernwärme'), + Q_fu=fx.Flow('Q_fu', bus='Gas'), + ) + + # Storage: Energy storage system with charging and discharging capabilities + storage = fx.Storage( + label='Storage', + charging=fx.Flow('Q_th_load', bus='Fernwärme', size=1000), + discharging=fx.Flow('Q_th_unload', bus='Fernwärme', size=1000), + capacity_in_flow_hours=fx.InvestParameters(effects_of_investment=20, fixed_size=30, mandatory=True), + initial_charge_state=0, # Initial storage state: empty + relative_maximum_charge_state=np.array([80, 70, 80, 80, 80, 80, 80, 80, 80]) * 0.01, + relative_maximum_final_charge_state=0.8, + eta_charge=0.9, + eta_discharge=1, # Efficiency factors for charging/discharging + relative_loss_per_hour=0.08, # 8% loss per hour. Absolute loss depends on current charge state + prevent_simultaneous_charge_and_discharge=True, # Prevent charging and discharging at the same time + ) + + # Heat Demand Sink: Represents a fixed heat demand profile + heat_sink = fx.Sink( + label='Heat Demand', + inputs=[fx.Flow(label='Q_th_Last', bus='Fernwärme', size=1, fixed_relative_profile=heat_demand_per_h)], + ) + + # Gas Source: Gas tariff source with associated costs and CO2 emissions + gas_source = fx.Source( + label='Gastarif', + outputs=[ + fx.Flow(label='Q_Gas', bus='Gas', size=1000, effects_per_flow_hour={costs.label: 0.04, CO2.label: 0.3}) + ], + ) + + # Power Sink: Represents the export of electricity to the grid + power_sink = fx.Sink( + label='Einspeisung', inputs=[fx.Flow(label='P_el', bus='Strom', effects_per_flow_hour=-1 * power_prices)] + ) + + # --- Build the Flow System --- + # Add all defined components and effects to the flow system + flow_system.add_elements(costs, CO2, boiler, storage, chp, heat_sink, gas_source, power_sink) + + # Visualize the flow system for validation purposes + flow_system.plot_network(show=True) + + # --- Define and Run Calculation --- + # Create a calculation object to model the Flow System + calculation = fx.FullCalculation(name='Sim1', flow_system=flow_system) + calculation.do_modeling() # Translate the model to a solvable form, creating equations and Variables + + # --- Solve the Calculation and Save Results --- + calculation.solve(fx.solvers.HighsSolver(mip_gap=0, time_limit_seconds=30)) + + calculation.results.plot_heatmap('CHP(Q_th)|flow_rate') + + # --- Analyze Results --- + calculation.results['Fernwärme'].plot_node_balance_pie() + calculation.results['Fernwärme'].plot_node_balance(style='stacked_bar') + calculation.results['Storage'].plot_node_balance() + calculation.results.plot_heatmap('CHP(Q_th)|flow_rate') + + # Convert the results for the storage component to a dataframe and display + df = calculation.results['Storage'].node_balance_with_charge_state() + print(df) + + # Plot charge state using matplotlib + fig, ax = calculation.results['Storage'].plot_charge_state(engine='matplotlib') + # Customize the plot further if needed + ax.set_title('Storage Charge State Over Time') + # Or save the figure + # fig.savefig('storage_charge_state.png') + + # Save results to file for later usage + calculation.results.to_file() diff --git a/examples/05_Two-stage-optimization/two_stage_optimization.py b/examples/05_Two-stage-optimization/two_stage_optimization.py new file mode 100644 index 000000000..b6072a3c2 --- /dev/null +++ b/examples/05_Two-stage-optimization/two_stage_optimization.py @@ -0,0 +1,179 @@ +""" +This script demonstrates how to use downsampling of a FlowSystem to effectively reduce the size of a model. +This can be very useful when working with large models or during development, +as it can drastically reduce the computational time. +This leads to faster results and easier debugging. +A common use case is to optimize the investments of a model with a downsampled version of the original model, and then fix the computed sizes when calculating the actual dispatch. +While the final optimum might differ from the global optimum, the solving will be much faster. +""" + +import logging +import pathlib +import timeit + +import pandas as pd +import xarray as xr + +import flixopt as fx + +logger = logging.getLogger('flixopt') + +if __name__ == '__main__': + # Data Import + data_import = pd.read_csv( + pathlib.Path(__file__).parent.parent / 'resources' / 'Zeitreihen2020.csv', index_col=0 + ).sort_index() + filtered_data = data_import[:500] + + filtered_data.index = pd.to_datetime(filtered_data.index) + timesteps = filtered_data.index + + # Access specific columns and convert to 1D-numpy array + electricity_demand = filtered_data['P_Netz/MW'].to_numpy() + heat_demand = filtered_data['Q_Netz/MW'].to_numpy() + electricity_price = filtered_data['Strompr.€/MWh'].to_numpy() + gas_price = filtered_data['Gaspr.€/MWh'].to_numpy() + + flow_system = fx.FlowSystem(timesteps) + flow_system.add_elements( + fx.Bus('Strom'), + fx.Bus('Fernwärme'), + fx.Bus('Gas'), + fx.Bus('Kohle'), + fx.Effect('costs', '€', 'Kosten', is_standard=True, is_objective=True), + fx.Effect('CO2', 'kg', 'CO2_e-Emissionen'), + fx.Effect('PE', 'kWh_PE', 'Primärenergie'), + fx.linear_converters.Boiler( + 'Kessel', + eta=0.85, + Q_th=fx.Flow(label='Q_th', bus='Fernwärme'), + Q_fu=fx.Flow( + label='Q_fu', + bus='Gas', + size=fx.InvestParameters( + effects_of_investment_per_size={'costs': 1_000}, minimum_size=10, maximum_size=500 + ), + relative_minimum=0.2, + previous_flow_rate=20, + on_off_parameters=fx.OnOffParameters(effects_per_switch_on=300), + ), + ), + fx.linear_converters.CHP( + 'BHKW2', + eta_th=0.58, + eta_el=0.22, + on_off_parameters=fx.OnOffParameters( + effects_per_switch_on=1_000, consecutive_on_hours_min=10, consecutive_off_hours_min=10 + ), + P_el=fx.Flow('P_el', bus='Strom'), + Q_th=fx.Flow('Q_th', bus='Fernwärme'), + Q_fu=fx.Flow( + 'Q_fu', + bus='Kohle', + size=fx.InvestParameters( + effects_of_investment_per_size={'costs': 3_000}, minimum_size=10, maximum_size=500 + ), + relative_minimum=0.3, + previous_flow_rate=100, + ), + ), + fx.Storage( + 'Speicher', + capacity_in_flow_hours=fx.InvestParameters( + minimum_size=10, maximum_size=1000, effects_of_investment_per_size={'costs': 60} + ), + initial_charge_state='lastValueOfSim', + eta_charge=1, + eta_discharge=1, + relative_loss_per_hour=0.001, + prevent_simultaneous_charge_and_discharge=True, + charging=fx.Flow('Q_th_load', size=137, bus='Fernwärme'), + discharging=fx.Flow('Q_th_unload', size=158, bus='Fernwärme'), + ), + fx.Sink( + 'Wärmelast', inputs=[fx.Flow('Q_th_Last', bus='Fernwärme', size=1, fixed_relative_profile=heat_demand)] + ), + fx.Source( + 'Gastarif', + outputs=[fx.Flow('Q_Gas', bus='Gas', size=1000, effects_per_flow_hour={'costs': gas_price, 'CO2': 0.3})], + ), + fx.Source( + 'Kohletarif', + outputs=[fx.Flow('Q_Kohle', bus='Kohle', size=1000, effects_per_flow_hour={'costs': 4.6, 'CO2': 0.3})], + ), + fx.Source( + 'Einspeisung', + outputs=[ + fx.Flow( + 'P_el', bus='Strom', size=1000, effects_per_flow_hour={'costs': electricity_price + 0.5, 'CO2': 0.3} + ) + ], + ), + fx.Sink( + 'Stromlast', + inputs=[fx.Flow('P_el_Last', bus='Strom', size=1, fixed_relative_profile=electricity_demand)], + ), + fx.Source( + 'Stromtarif', + outputs=[ + fx.Flow('P_el', bus='Strom', size=1000, effects_per_flow_hour={'costs': electricity_price, 'CO2': 0.3}) + ], + ), + ) + + # Separate optimization of flow sizes and dispatch + start = timeit.default_timer() + calculation_sizing = fx.FullCalculation('Sizing', flow_system.resample('4h')) + calculation_sizing.do_modeling() + calculation_sizing.solve(fx.solvers.HighsSolver(0.1 / 100, 600)) + timer_sizing = timeit.default_timer() - start + + start = timeit.default_timer() + calculation_dispatch = fx.FullCalculation('Dispatch', flow_system) + calculation_dispatch.do_modeling() + calculation_dispatch.fix_sizes(calculation_sizing.results.solution) + calculation_dispatch.solve(fx.solvers.HighsSolver(0.1 / 100, 600)) + timer_dispatch = timeit.default_timer() - start + + if (calculation_dispatch.results.sizes().round(5) == calculation_sizing.results.sizes().round(5)).all().item(): + logger.info('Sizes were correctly equalized') + else: + raise RuntimeError('Sizes were not correctly equalized') + + # Optimization of both flow sizes and dispatch together + start = timeit.default_timer() + calculation_combined = fx.FullCalculation('Combined', flow_system) + calculation_combined.do_modeling() + calculation_combined.solve(fx.solvers.HighsSolver(0.1 / 100, 600)) + timer_combined = timeit.default_timer() - start + + # Comparison of results + comparison = xr.concat( + [calculation_combined.results.solution, calculation_dispatch.results.solution], dim='mode' + ).assign_coords(mode=['Combined', 'Two-stage']) + comparison['Duration [s]'] = xr.DataArray([timer_combined, timer_sizing + timer_dispatch], dims='mode') + + comparison_main = comparison[ + [ + 'Duration [s]', + 'costs', + 'costs(periodic)', + 'costs(temporal)', + 'BHKW2(Q_fu)|size', + 'Kessel(Q_fu)|size', + 'Speicher|size', + ] + ] + comparison_main = xr.concat( + [ + comparison_main, + ( + (comparison_main.sel(mode='Two-stage') - comparison_main.sel(mode='Combined')) + / comparison_main.sel(mode='Combined') + * 100 + ).assign_coords(mode='Diff [%]'), + ], + dim='mode', + ) + + print(comparison_main.to_pandas().T.round(2)) diff --git a/examples/03_Calculation_types/Zeitreihen2020.csv b/examples/resources/Zeitreihen2020.csv similarity index 100% rename from examples/03_Calculation_types/Zeitreihen2020.csv rename to examples/resources/Zeitreihen2020.csv diff --git a/flixopt/__init__.py b/flixopt/__init__.py index d8ad05f19..8fc4e4851 100644 --- a/flixopt/__init__.py +++ b/flixopt/__init__.py @@ -2,9 +2,14 @@ This module bundles all common functionality of flixopt and sets up the logging """ -from importlib.metadata import version +import warnings +from importlib.metadata import PackageNotFoundError, version -__version__ = version('flixopt') +try: + __version__ = version('flixopt') +except PackageNotFoundError: + # Package is not installed (development mode without editable install) + __version__ = '0.0.0.dev0' from .commons import ( CONFIG, @@ -35,3 +40,30 @@ results, solvers, ) + +# === Runtime warning suppression for third-party libraries === +# These warnings are from dependencies and cannot be fixed by end users. +# They are suppressed at runtime to provide a cleaner user experience. +# These filters match the test configuration in pyproject.toml for consistency. + +# tsam: Time series aggregation library +# - UserWarning: Informational message about minimal value constraints during clustering. +warnings.filterwarnings('ignore', category=UserWarning, message='.*minimal value.*exceeds.*', module='tsam') +# TODO: Might be able to fix it in flixopt? + +# linopy: Linear optimization library +# - UserWarning: Coordinate mismatch warnings that don't affect functionality and are expected. +warnings.filterwarnings( + 'ignore', category=UserWarning, message='Coordinates across variables not equal', module='linopy' +) +# - FutureWarning: join parameter default will change in future versions +warnings.filterwarnings( + 'ignore', + category=FutureWarning, + message="In a future version of xarray the default value for join will change from join='outer' to join='exact'", + module='linopy', +) + +# numpy: Core numerical library +# - RuntimeWarning: Binary incompatibility warnings from compiled extensions (safe to ignore). numpy 1->2 +warnings.filterwarnings('ignore', category=RuntimeWarning, message='numpy\\.ndarray size changed') diff --git a/flixopt/aggregation.py b/flixopt/aggregation.py index 4e6c3892e..91ef618a9 100644 --- a/flixopt/aggregation.py +++ b/flixopt/aggregation.py @@ -22,8 +22,8 @@ from .components import Storage from .structure import ( - Model, - SystemModel, + FlowSystemModel, + Submodel, ) if TYPE_CHECKING: @@ -274,25 +274,25 @@ def use_extreme_periods(self): @property def labels_for_high_peaks(self) -> list[str]: - return [ts.label for ts in self.time_series_for_high_peaks] + return [ts.name for ts in self.time_series_for_high_peaks] @property def labels_for_low_peaks(self) -> list[str]: - return [ts.label for ts in self.time_series_for_low_peaks] + return [ts.name for ts in self.time_series_for_low_peaks] @property def use_low_peaks(self) -> bool: return bool(self.time_series_for_low_peaks) -class AggregationModel(Model): - """The AggregationModel holds equations and variables related to the Aggregation of a FLowSystem. +class AggregationModel(Submodel): + """The AggregationModel holds equations and variables related to the Aggregation of a FlowSystem. It creates Equations that equates indices of variables, and introduces penalties related to binary variables, that escape the equation to their related binaries in other periods""" def __init__( self, - model: SystemModel, + model: FlowSystemModel, aggregation_parameters: AggregationParameters, flow_system: FlowSystem, aggregation_data: Aggregation, @@ -301,7 +301,7 @@ def __init__( """ Modeling-Element for "index-equating"-equations """ - super().__init__(model, label_of_element='Aggregation', label_full='Aggregation') + super().__init__(model, label_of_element='Aggregation', label_of_model='Aggregation') self.flow_system = flow_system self.aggregation_parameters = aggregation_parameters self.aggregation_data = aggregation_data @@ -315,22 +315,24 @@ def do_modeling(self): indices = self.aggregation_data.get_equation_indices(skip_first_index_of_period=True) - time_variables: set[str] = {k for k, v in self._model.variables.data.items() if 'time' in v.indexes} - binary_variables: set[str] = {k for k, v in self._model.variables.data.items() if k in self._model.binaries} + time_variables: set[str] = { + name for name in self._model.variables if 'time' in self._model.variables[name].dims + } + binary_variables: set[str] = set(self._model.variables.binaries) binary_time_variables: set[str] = time_variables & binary_variables for component in components: if isinstance(component, Storage) and not self.aggregation_parameters.fix_storage_flows: continue # Fix Nothing in The Storage - all_variables_of_component = set(component.model.variables) + all_variables_of_component = set(component.submodel.variables) if self.aggregation_parameters.aggregate_data_and_fix_non_binary_vars: - relevant_variables = component.model.variables[all_variables_of_component & time_variables] + relevant_variables = component.submodel.variables[all_variables_of_component & time_variables] else: - relevant_variables = component.model.variables[all_variables_of_component & binary_time_variables] + relevant_variables = component.submodel.variables[all_variables_of_component & binary_time_variables] for variable in relevant_variables: - self._equate_indices(component.model.variables[variable], indices) + self._equate_indices(component.submodel.variables[variable], indices) penalty = self.aggregation_parameters.penalty_of_period_freedom if (self.aggregation_parameters.percentage_of_period_freedom > 0) and penalty != 0: @@ -343,12 +345,9 @@ def _equate_indices(self, variable: linopy.Variable, indices: tuple[np.ndarray, # Gleichung: # eq1: x(p1,t) - x(p3,t) = 0 # wobei p1 und p3 im gleichen Cluster sind und t = 0..N_p - con = self.add( - self._model.add_constraints( - variable.isel(time=indices[0]) - variable.isel(time=indices[1]) == 0, - name=f'{self.label_full}|equate_indices|{variable.name}', - ), - f'equate_indices|{variable.name}', + con = self.add_constraints( + variable.isel(time=indices[0]) - variable.isel(time=indices[1]) == 0, + short_name=f'equate_indices|{variable.name}', ) # Korrektur: (bisher nur für Binärvariablen:) @@ -356,23 +355,11 @@ def _equate_indices(self, variable: linopy.Variable, indices: tuple[np.ndarray, variable.name in self._model.variables.binaries and self.aggregation_parameters.percentage_of_period_freedom > 0 ): - var_k1 = self.add( - self._model.add_variables( - binary=True, - coords={'time': variable.isel(time=indices[0]).indexes['time']}, - name=f'{self.label_full}|correction1|{variable.name}', - ), - f'correction1|{variable.name}', - ) + sel = variable.isel(time=indices[0]) + coords = {d: sel.indexes[d] for d in sel.dims} + var_k1 = self.add_variables(binary=True, coords=coords, short_name=f'correction1|{variable.name}') - var_k0 = self.add( - self._model.add_variables( - binary=True, - coords={'time': variable.isel(time=indices[0]).indexes['time']}, - name=f'{self.label_full}|correction0|{variable.name}', - ), - f'correction0|{variable.name}', - ) + var_k0 = self.add_variables(binary=True, coords=coords, short_name=f'correction0|{variable.name}') # equation extends ... # --> On(p3) can be 0/1 independent of On(p1,t)! @@ -383,21 +370,13 @@ def _equate_indices(self, variable: linopy.Variable, indices: tuple[np.ndarray, con.lhs += 1 * var_k1 - 1 * var_k0 # interlock var_k1 and var_K2: - # eq: var_k0(t)+var_k1(t) <= 1.1 - self.add( - self._model.add_constraints( - var_k0 + var_k1 <= 1.1, name=f'{self.label_full}|lock_k0_and_k1|{variable.name}' - ), - f'lock_k0_and_k1|{variable.name}', - ) + # eq: var_k0(t)+var_k1(t) <= 1 + self.add_constraints(var_k0 + var_k1 <= 1, short_name=f'lock_k0_and_k1|{variable.name}') # Begrenzung der Korrektur-Anzahl: # eq: sum(K) <= n_Corr_max - self.add( - self._model.add_constraints( - sum(var_k0) + sum(var_k1) - <= round(self.aggregation_parameters.percentage_of_period_freedom / 100 * length), - name=f'{self.label_full}|limit_corrections|{variable.name}', - ), - f'limit_corrections|{variable.name}', + limit = int(np.floor(self.aggregation_parameters.percentage_of_period_freedom / 100 * length)) + self.add_constraints( + var_k0.sum(dim='time') + var_k1.sum(dim='time') <= limit, + short_name=f'limit_corrections|{variable.name}', ) diff --git a/flixopt/calculation.py b/flixopt/calculation.py index 4dc13889c..9e35b2dee 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -1,11 +1,11 @@ """ This module contains the Calculation functionality for the flixopt framework. -It is used to calculate a SystemModel for a given FlowSystem through a solver. +It is used to calculate a FlowSystemModel for a given FlowSystem through a solver. There are three different Calculation types: - 1. FullCalculation: Calculates the SystemModel for the full FlowSystem - 2. AggregatedCalculation: Calculates the SystemModel for the full FlowSystem, but aggregates the TimeSeriesData. + 1. FullCalculation: Calculates the FlowSystemModel for the full FlowSystem + 2. AggregatedCalculation: Calculates the FlowSystemModel for the full FlowSystem, but aggregates the TimeSeriesData. This simplifies the mathematical model and usually speeds up the solving process. - 3. SegmentedCalculation: Solves a SystemModel for each individual Segment of the FlowSystem. + 3. SegmentedCalculation: Solves a FlowSystemModel for each individual Segment of the FlowSystem. """ from __future__ import annotations @@ -14,7 +14,9 @@ import math import pathlib import timeit -from typing import TYPE_CHECKING, Any +import warnings +from collections import Counter +from typing import TYPE_CHECKING, Annotated, Any import numpy as np import yaml @@ -24,17 +26,18 @@ from .aggregation import AggregationModel, AggregationParameters from .components import Storage from .config import CONFIG +from .core import DataConverter, Scalar, TimeSeriesData, drop_constant_arrays from .features import InvestmentModel +from .flow_system import FlowSystem from .results import CalculationResults, SegmentedCalculationResults if TYPE_CHECKING: import pandas as pd + import xarray as xr - from .core import Scalar from .elements import Component - from .flow_system import FlowSystem from .solvers import _Solver - from .structure import SystemModel + from .structure import FlowSystemModel logger = logging.getLogger('flixopt') @@ -42,26 +45,50 @@ class Calculation: """ class for defined way of solving a flow_system optimization + + Args: + name: name of calculation + flow_system: flow_system which should be calculated + folder: folder where results should be saved. If None, then the current working directory is used. + normalize_weights: Whether to automatically normalize the weights (periods and scenarios) to sum up to 1 when solving. + active_timesteps: Deprecated. Use FlowSystem.sel(time=...) or FlowSystem.isel(time=...) instead. """ def __init__( self, name: str, flow_system: FlowSystem, - active_timesteps: pd.DatetimeIndex | None = None, + active_timesteps: Annotated[ + pd.DatetimeIndex | None, + 'DEPRECATED: Use flow_system.sel(time=...) or flow_system.isel(time=...) instead', + ] = None, folder: pathlib.Path | None = None, + normalize_weights: bool = True, ): - """ - Args: - name: name of calculation - flow_system: flow_system which should be calculated - active_timesteps: list with indices, which should be used for calculation. If None, then all timesteps are used. - folder: folder where results should be saved. If None, then the current working directory is used. - """ self.name = name + if flow_system.used_in_calculation: + logger.warning( + f'This FlowSystem is already used in a calculation:\n{flow_system}\n' + f'Creating a copy of the FlowSystem for Calculation "{self.name}".' + ) + flow_system = flow_system.copy() + + if active_timesteps is not None: + warnings.warn( + "The 'active_timesteps' parameter is deprecated and will be removed in a future version. " + 'Use flow_system.sel(time=timesteps) or flow_system.isel(time=indices) before passing ' + 'the FlowSystem to the Calculation instead.', + DeprecationWarning, + stacklevel=2, + ) + flow_system = flow_system.sel(time=active_timesteps) + self._active_timesteps = active_timesteps # deprecated + self.normalize_weights = normalize_weights + + flow_system._used_in_calculation = True + self.flow_system = flow_system - self.model: SystemModel | None = None - self.active_timesteps = active_timesteps + self.model: FlowSystemModel | None = None self.durations = {'modeling': 0.0, 'solving': 0.0, 'saving': 0.0} self.folder = pathlib.Path.cwd() / 'results' if folder is None else pathlib.Path(folder) @@ -71,56 +98,59 @@ def __init__( raise NotADirectoryError(f'Path {self.folder} exists and is not a directory.') self.folder.mkdir(parents=False, exist_ok=True) + self._modeled = False + @property def main_results(self) -> dict[str, Scalar | dict]: from flixopt.features import InvestmentModel - return { + main_results = { 'Objective': self.model.objective.value, - 'Penalty': float(self.model.effects.penalty.total.solution.values), + 'Penalty': self.model.effects.penalty.total.solution.values, 'Effects': { f'{effect.label} [{effect.unit}]': { - 'operation': float(effect.model.operation.total.solution.values), - 'invest': float(effect.model.invest.total.solution.values), - 'total': float(effect.model.total.solution.values), + 'temporal': effect.submodel.temporal.total.solution.values, + 'periodic': effect.submodel.periodic.total.solution.values, + 'total': effect.submodel.total.solution.values, } for effect in self.flow_system.effects }, 'Invest-Decisions': { 'Invested': { - model.label_of_element: float(model.size.solution) + model.label_of_element: model.size.solution for component in self.flow_system.components.values() - for model in component.model.all_sub_models - if isinstance(model, InvestmentModel) and float(model.size.solution) >= CONFIG.Modeling.epsilon + for model in component.submodel.all_submodels + if isinstance(model, InvestmentModel) and model.size.solution.max() >= CONFIG.Modeling.epsilon }, 'Not invested': { - model.label_of_element: float(model.size.solution) + model.label_of_element: model.size.solution for component in self.flow_system.components.values() - for model in component.model.all_sub_models - if isinstance(model, InvestmentModel) and float(model.size.solution) < CONFIG.Modeling.epsilon + for model in component.submodel.all_submodels + if isinstance(model, InvestmentModel) and model.size.solution.max() < CONFIG.Modeling.epsilon }, }, 'Buses with excess': [ { bus.label_full: { - 'input': float(np.sum(bus.model.excess_input.solution.values)), - 'output': float(np.sum(bus.model.excess_output.solution.values)), + 'input': bus.submodel.excess_input.solution.sum('time'), + 'output': bus.submodel.excess_output.solution.sum('time'), } } for bus in self.flow_system.buses.values() if bus.with_excess and ( - float(np.sum(bus.model.excess_input.solution.values)) > 1e-3 - or float(np.sum(bus.model.excess_output.solution.values)) > 1e-3 + bus.submodel.excess_input.solution.sum() > 1e-3 or bus.submodel.excess_output.solution.sum() > 1e-3 ) ], } + return utils.round_nested_floats(main_results) + @property def summary(self): return { 'Name': self.name, - 'Number of timesteps': len(self.flow_system.time_series_collection.timesteps), + 'Number of timesteps': len(self.flow_system.timesteps), 'Calculation Type': self.__class__.__name__, 'Constraints': self.model.constraints.ncons, 'Variables': self.model.variables.nvars, @@ -129,6 +159,19 @@ def summary(self): 'Config': CONFIG.to_dict(), } + @property + def active_timesteps(self) -> pd.DatetimeIndex: + warnings.warn( + 'active_timesteps is deprecated. Use flow_system.sel(time=...) or flow_system.isel(time=...) instead.', + DeprecationWarning, + stacklevel=2, + ) + return self._active_timesteps + + @property + def modeled(self) -> bool: + return True if self.model is not None else False + class FullCalculation(Calculation): """ @@ -136,19 +179,55 @@ class FullCalculation(Calculation): This is the most comprehensive calculation type that considers every time step in the optimization, providing the most accurate but computationally intensive solution. + + Args: + name: name of calculation + flow_system: flow_system which should be calculated + folder: folder where results should be saved. If None, then the current working directory is used. + normalize_weights: Whether to automatically normalize the weights (periods and scenarios) to sum up to 1 when solving. + active_timesteps: Deprecated. Use FlowSystem.sel(time=...) or FlowSystem.isel(time=...) instead. """ - def do_modeling(self) -> SystemModel: + def do_modeling(self) -> FullCalculation: t_start = timeit.default_timer() - self._activate_time_series() + self.flow_system.connect_and_transform() - self.model = self.flow_system.create_model() + self.model = self.flow_system.create_model(self.normalize_weights) self.model.do_modeling() self.durations['modeling'] = round(timeit.default_timer() - t_start, 2) - return self.model + return self + + def fix_sizes(self, ds: xr.Dataset, decimal_rounding: int | None = 5) -> FullCalculation: + """Fix the sizes of the calculations to specified values. - def solve(self, solver: _Solver, log_file: pathlib.Path | None = None, log_main_results: bool = True): + Args: + ds: The dataset that contains the variable names mapped to their sizes. If None, the dataset is loaded from the results. + decimal_rounding: The number of decimal places to round the sizes to. If no rounding is applied, numerical errors might lead to infeasibility. + """ + if not self.modeled: + raise RuntimeError('Model was not created. Call do_modeling() first.') + if decimal_rounding is not None: + ds = ds.round(decimal_rounding) + + for name, da in ds.data_vars.items(): + if '|size' not in name: + continue + if name not in self.model.variables: + logger.debug(f'Variable {name} not found in calculation model. Skipping.') + continue + + con = self.model.add_constraints( + self.model[name] == da, + name=f'{name}-fixed', + ) + logger.debug(f'Fixed "{name}":\n{con}') + + return self + + def solve( + self, solver: _Solver, log_file: pathlib.Path | None = None, log_main_results: bool = True + ) -> FullCalculation: t_start = timeit.default_timer() self.model.solve( @@ -171,11 +250,10 @@ def solve(self, solver: _Solver, log_file: pathlib.Path | None = None, log_main_ # Log the formatted output if log_main_results: - logger.info(f'{" Main Results ":#^80}') logger.info( - '\n' + f'{" Main Results ":#^80}\n' + yaml.dump( - utils.round_floats(self.main_results), + utils.round_nested_floats(self.main_results), default_flow_style=False, sort_keys=False, allow_unicode=True, @@ -185,11 +263,7 @@ def solve(self, solver: _Solver, log_file: pathlib.Path | None = None, log_main_ self.results = CalculationResults.from_calculation(self) - def _activate_time_series(self): - self.flow_system.transform_data() - self.flow_system.time_series_collection.activate_timesteps( - active_timesteps=self.active_timesteps, - ) + return self class AggregatedCalculation(FullCalculation): @@ -221,29 +295,34 @@ def __init__( flow_system: FlowSystem, aggregation_parameters: AggregationParameters, components_to_clusterize: list[Component] | None = None, - active_timesteps: pd.DatetimeIndex | None = None, + active_timesteps: Annotated[ + pd.DatetimeIndex | None, + 'DEPRECATED: Use flow_system.sel(time=...) or flow_system.isel(time=...) instead', + ] = None, folder: pathlib.Path | None = None, ): + if flow_system.scenarios is not None: + raise ValueError('Aggregation is not supported for scenarios yet. Please use FullCalculation instead.') super().__init__(name, flow_system, active_timesteps, folder=folder) self.aggregation_parameters = aggregation_parameters self.components_to_clusterize = components_to_clusterize self.aggregation = None - def do_modeling(self) -> SystemModel: + def do_modeling(self) -> AggregatedCalculation: t_start = timeit.default_timer() - self._activate_time_series() + self.flow_system.connect_and_transform() self._perform_aggregation() # Model the System - self.model = self.flow_system.create_model() + self.model = self.flow_system.create_model(self.normalize_weights) self.model.do_modeling() - # Add Aggregation Model after modeling the rest + # Add Aggregation Submodel after modeling the rest self.aggregation = AggregationModel( self.model, self.aggregation_parameters, self.flow_system, self.aggregation, self.components_to_clusterize ) self.aggregation.do_modeling() self.durations['modeling'] = round(timeit.default_timer() - t_start, 2) - return self.model + return self def _perform_aggregation(self): from .aggregation import Aggregation @@ -251,41 +330,34 @@ def _perform_aggregation(self): t_start_agg = timeit.default_timer() # Validation - dt_min, dt_max = ( - np.min(self.flow_system.time_series_collection.hours_per_timestep), - np.max(self.flow_system.time_series_collection.hours_per_timestep), - ) + dt_min = float(self.flow_system.hours_per_timestep.min().item()) + dt_max = float(self.flow_system.hours_per_timestep.max().item()) if not dt_min == dt_max: raise ValueError( f'Aggregation failed due to inconsistent time step sizes:' f'delta_t varies from {dt_min} to {dt_max} hours.' ) - steps_per_period = ( - self.aggregation_parameters.hours_per_period - / self.flow_system.time_series_collection.hours_per_timestep.max() - ) - is_integer = ( - self.aggregation_parameters.hours_per_period - % self.flow_system.time_series_collection.hours_per_timestep.max() - ).item() == 0 - if not (steps_per_period.size == 1 and is_integer): + ratio = self.aggregation_parameters.hours_per_period / dt_max + if not np.isclose(ratio, round(ratio), atol=1e-9): raise ValueError( f'The selected {self.aggregation_parameters.hours_per_period=} does not match the time ' - f'step size of {dt_min} hours). It must be a multiple of {dt_min} hours.' + f'step size of {dt_max} hours. It must be an integer multiple of {dt_max} hours.' ) logger.info(f'{"":#^80}') logger.info(f'{" Aggregating TimeSeries Data ":#^80}') + ds = self.flow_system.to_dataset() + + temporaly_changing_ds = drop_constant_arrays(ds, dim='time') + # Aggregation - creation of aggregated timeseries: self.aggregation = Aggregation( - original_data=self.flow_system.time_series_collection.to_dataframe( - include_extra_timestep=False - ), # Exclude last row (NaN) + original_data=temporaly_changing_ds.to_dataframe(), hours_per_time_step=float(dt_min), hours_per_period=self.aggregation_parameters.hours_per_period, nr_of_periods=self.aggregation_parameters.nr_of_periods, - weights=self.flow_system.time_series_collection.calculate_aggregation_weights(), + weights=self.calculate_aggregation_weights(temporaly_changing_ds), time_series_for_high_peaks=self.aggregation_parameters.labels_for_high_peaks, time_series_for_low_peaks=self.aggregation_parameters.labels_for_low_peaks, ) @@ -293,11 +365,45 @@ def _perform_aggregation(self): self.aggregation.cluster() self.aggregation.plot(show=True, save=self.folder / 'aggregation.html') if self.aggregation_parameters.aggregate_data_and_fix_non_binary_vars: - self.flow_system.time_series_collection.insert_new_data( - self.aggregation.aggregated_data, include_extra_timestep=False - ) + ds = self.flow_system.to_dataset() + for name, series in self.aggregation.aggregated_data.items(): + da = ( + DataConverter.to_dataarray(series, self.flow_system.coords) + .rename(name) + .assign_attrs(ds[name].attrs) + ) + if TimeSeriesData.is_timeseries_data(da): + da = TimeSeriesData.from_dataarray(da) + + ds[name] = da + + self.flow_system = FlowSystem.from_dataset(ds) + self.flow_system.connect_and_transform() self.durations['aggregation'] = round(timeit.default_timer() - t_start_agg, 2) + @classmethod + def calculate_aggregation_weights(cls, ds: xr.Dataset) -> dict[str, float]: + """Calculate weights for all datavars in the dataset. Weights are pulled from the attrs of the datavars.""" + + groups = [da.attrs['aggregation_group'] for da in ds.data_vars.values() if 'aggregation_group' in da.attrs] + group_counts = Counter(groups) + + # Calculate weight for each group (1/count) + group_weights = {group: 1 / count for group, count in group_counts.items()} + + weights = {} + for name, da in ds.data_vars.items(): + group_weight = group_weights.get(da.attrs.get('aggregation_group')) + if group_weight is not None: + weights[name] = group_weight + else: + weights[name] = da.attrs.get('aggregation_weight', 1) + + if np.all(np.isclose(list(weights.values()), 1, atol=1e-6)): + logger.info('All Aggregation weights were set to 1') + + return weights + class SegmentedCalculation(Calculation): """Solve large optimization problems by dividing time horizon into (overlapping) segments. @@ -423,20 +529,17 @@ def __init__( self.nr_of_previous_values = nr_of_previous_values self.sub_calculations: list[FullCalculation] = [] - self.all_timesteps = self.flow_system.time_series_collection.all_timesteps - self.all_timesteps_extra = self.flow_system.time_series_collection.all_timesteps_extra - self.segment_names = [ f'Segment_{i + 1}' for i in range(math.ceil(len(self.all_timesteps) / self.timesteps_per_segment)) ] - self.active_timesteps_per_segment = self._calculate_timesteps_of_segment() + self._timesteps_per_segment = self._calculate_timesteps_per_segment() assert timesteps_per_segment > 2, 'The Segment length must be greater 2, due to unwanted internal side effects' assert self.timesteps_per_segment_with_overlap <= len(self.all_timesteps), ( f'{self.timesteps_per_segment_with_overlap=} cant be greater than the total length {len(self.all_timesteps)}' ) - self.flow_system._connect_network() # Connect network to ensure that all FLows know their Component + self.flow_system._connect_network() # Connect network to ensure that all Flows know their Component # Storing all original start values self._original_start_values = { **{flow.label_full: flow.previous_flow_rate for flow in self.flow_system.flows.values()}, @@ -448,104 +551,118 @@ def __init__( } self._transfered_start_values: list[dict[str, Any]] = [] - def do_modeling_and_solve( - self, solver: _Solver, log_file: pathlib.Path | None = None, log_main_results: bool = False - ): - logger.info(f'{"":#^80}') - logger.info(f'{" Segmented Solving ":#^80}') - + def _create_sub_calculations(self): for i, (segment_name, timesteps_of_segment) in enumerate( - zip(self.segment_names, self.active_timesteps_per_segment, strict=False) + zip(self.segment_names, self._timesteps_per_segment, strict=True) ): - if self.sub_calculations: - self._transfer_start_values(i) + calc = FullCalculation(f'{self.name}-{segment_name}', self.flow_system.sel(time=timesteps_of_segment)) + calc.flow_system._connect_network() # Connect to have Correct names of Flows! + self.sub_calculations.append(calc) logger.info( f'{segment_name} [{i + 1:>2}/{len(self.segment_names):<2}] ' f'({timesteps_of_segment[0]} -> {timesteps_of_segment[-1]}):' ) - calculation = FullCalculation( - f'{self.name}-{segment_name}', self.flow_system, active_timesteps=timesteps_of_segment + def do_modeling_and_solve( + self, solver: _Solver, log_file: pathlib.Path | None = None, log_main_results: bool = False + ) -> SegmentedCalculation: + logger.info(f'{"":#^80}') + logger.info(f'{" Segmented Solving ":#^80}') + self._create_sub_calculations() + + for i, calculation in enumerate(self.sub_calculations): + logger.info( + f'{self.segment_names[i]} [{i + 1:>2}/{len(self.segment_names):<2}] ' + f'({calculation.flow_system.timesteps[0]} -> {calculation.flow_system.timesteps[-1]}):' ) - self.sub_calculations.append(calculation) + + if i > 0 and self.nr_of_previous_values > 0: + self._transfer_start_values(i) + calculation.do_modeling() - invest_elements = [ - model.label_full - for component in self.flow_system.components.values() - for model in component.model.all_sub_models - if isinstance(model, InvestmentModel) - ] - if invest_elements: - logger.critical( - f'Investments are not supported in Segmented Calculation! ' - f'Following InvestmentModels were found: {invest_elements}' - ) + + # Warn about Investments, but only in fist run + if i == 0: + invest_elements = [ + model.label_full + for component in calculation.flow_system.components.values() + for model in component.submodel.all_submodels + if isinstance(model, InvestmentModel) + ] + if invest_elements: + logger.critical( + f'Investments are not supported in Segmented Calculation! ' + f'Following InvestmentModels were found: {invest_elements}' + ) + calculation.solve( solver, log_file=pathlib.Path(log_file) if log_file is not None else self.folder / f'{self.name}.log', log_main_results=log_main_results, ) - self._reset_start_values() - for calc in self.sub_calculations: for key, value in calc.durations.items(): self.durations[key] += value self.results = SegmentedCalculationResults.from_calculation(self) - def _transfer_start_values(self, segment_index: int): + return self + + def _transfer_start_values(self, i: int): """ This function gets the last values of the previous solved segment and inserts them as start values for the next segment """ - timesteps_of_prior_segment = self.active_timesteps_per_segment[segment_index - 1] + timesteps_of_prior_segment = self.sub_calculations[i - 1].flow_system.timesteps_extra - start = self.active_timesteps_per_segment[segment_index][0] + start = self.sub_calculations[i].flow_system.timesteps[0] start_previous_values = timesteps_of_prior_segment[self.timesteps_per_segment - self.nr_of_previous_values] end_previous_values = timesteps_of_prior_segment[self.timesteps_per_segment - 1] logger.debug( - f'start of next segment: {start}. indices of previous values: {start_previous_values}:{end_previous_values}' + f'Start of next segment: {start}. Indices of previous values: {start_previous_values} -> {end_previous_values}' ) + current_flow_system = self.sub_calculations[i - 1].flow_system + next_flow_system = self.sub_calculations[i].flow_system + start_values_of_this_segment = {} - for flow in self.flow_system.flows.values(): - flow.previous_flow_rate = flow.model.flow_rate.solution.sel( + + for current_flow in current_flow_system.flows.values(): + next_flow = next_flow_system.flows[current_flow.label_full] + next_flow.previous_flow_rate = current_flow.submodel.flow_rate.solution.sel( time=slice(start_previous_values, end_previous_values) ).values - start_values_of_this_segment[flow.label_full] = flow.previous_flow_rate - for comp in self.flow_system.components.values(): - if isinstance(comp, Storage): - comp.initial_charge_state = comp.model.charge_state.solution.sel(time=start).item() - start_values_of_this_segment[comp.label_full] = comp.initial_charge_state + start_values_of_this_segment[current_flow.label_full] = next_flow.previous_flow_rate - self._transfered_start_values.append(start_values_of_this_segment) + for current_comp in current_flow_system.components.values(): + next_comp = next_flow_system.components[current_comp.label_full] + if isinstance(next_comp, Storage): + next_comp.initial_charge_state = current_comp.submodel.charge_state.solution.sel(time=start).item() + start_values_of_this_segment[current_comp.label_full] = next_comp.initial_charge_state - def _reset_start_values(self): - """This resets the start values of all Elements to its original state""" - for flow in self.flow_system.flows.values(): - flow.previous_flow_rate = self._original_start_values[flow.label_full] - for comp in self.flow_system.components.values(): - if isinstance(comp, Storage): - comp.initial_charge_state = self._original_start_values[comp.label_full] + self._transfered_start_values.append(start_values_of_this_segment) - def _calculate_timesteps_of_segment(self) -> list[pd.DatetimeIndex]: - active_timesteps_per_segment = [] + def _calculate_timesteps_per_segment(self) -> list[pd.DatetimeIndex]: + timesteps_per_segment = [] for i, _ in enumerate(self.segment_names): start = self.timesteps_per_segment * i end = min(start + self.timesteps_per_segment_with_overlap, len(self.all_timesteps)) - active_timesteps_per_segment.append(self.all_timesteps[start:end]) - return active_timesteps_per_segment + timesteps_per_segment.append(self.all_timesteps[start:end]) + return timesteps_per_segment @property def timesteps_per_segment_with_overlap(self): return self.timesteps_per_segment + self.overlap_timesteps @property - def start_values_of_segments(self) -> dict[int, dict[str, Any]]: + def start_values_of_segments(self) -> list[dict[str, Any]]: """Gives an overview of the start values of all Segments""" - return { - 0: {element.label_full: value for element, value in self._original_start_values.items()}, - **{i: start_values for i, start_values in enumerate(self._transfered_start_values, 1)}, - } + return [{name: value for name, value in self._original_start_values.items()}] + [ + start_values for start_values in self._transfered_start_values + ] + + @property + def all_timesteps(self) -> pd.DatetimeIndex: + return self.flow_system.timesteps diff --git a/flixopt/components.py b/flixopt/components.py index 2ad8d90e8..c40e6af88 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -9,13 +9,14 @@ from typing import TYPE_CHECKING, Literal import numpy as np +import xarray as xr -from . import utils -from .core import NumericData, NumericDataTS, PlausibilityError, Scalar, TimeSeries +from .core import PeriodicDataUser, PlausibilityError, TemporalData, TemporalDataUser from .elements import Component, ComponentModel, Flow -from .features import InvestmentModel, OnOffModel, PiecewiseModel +from .features import InvestmentModel, PiecewiseModel from .interface import InvestParameters, OnOffParameters, PiecewiseConversion -from .structure import SystemModel, register_class_for_io +from .modeling import BoundingPatterns +from .structure import FlowSystemModel, register_class_for_io if TYPE_CHECKING: import linopy @@ -39,6 +40,10 @@ class LinearConverter(Component): straightforward linear relationships, or piecewise conversion for complex non-linear behavior approximated through piecewise linear segments. + Mathematical Formulation: + See the complete mathematical model in the documentation: + [LinearConverter](../user-guide/mathematical-notation/elements/LinearConverter.md) + Args: label: The label of the Element. Used to identify it in the FlowSystem. inputs: list of input Flows that feed into the converter. @@ -141,11 +146,11 @@ class LinearConverter(Component): Note: Conversion factors define linear relationships where the sum of (coefficient × flow_rate) equals zero for each equation: factor1×flow1 + factor2×flow2 + ... = 0 - Conversion factors define linear relationships. - `{flow1: a1, flow2: a2, ...}` leads to `a1×flow_rate1 + a2×flow_rate2 + ... = 0` - Unfortunately the current input format doest read intuitively: - {"electricity": 1, "H2": 50} means that the electricity_in flow rate is multiplied by 1 - and the hydrogen_out flow rate is multiplied by 50. THis leads to 50 electricity --> 1 H2. + Conversion factors define linear relationships: + `{flow1: a1, flow2: a2, ...}` yields `a1×flow_rate1 + a2×flow_rate2 + ... = 0`. + Note: The input format may be unintuitive. For example, + `{"electricity": 1, "H2": 50}` implies `1×electricity = 50×H2`, + i.e., 50 units of electricity produce 1 unit of H2. The system must have fewer conversion factors than total flows (degrees of freedom > 0) to avoid over-constraining the problem. For n total flows, use at most n-1 conversion factors. @@ -161,7 +166,7 @@ def __init__( inputs: list[Flow], outputs: list[Flow], on_off_parameters: OnOffParameters | None = None, - conversion_factors: list[dict[str, NumericDataTS]] | None = None, + conversion_factors: list[dict[str, TemporalDataUser]] | None = None, piecewise_conversion: PiecewiseConversion | None = None, meta_data: dict | None = None, ): @@ -169,10 +174,10 @@ def __init__( self.conversion_factors = conversion_factors or [] self.piecewise_conversion = piecewise_conversion - def create_model(self, model: SystemModel) -> LinearConverterModel: + def create_model(self, model: FlowSystemModel) -> LinearConverterModel: self._plausibility_checks() - self.model = LinearConverterModel(model, self) - return self.model + self.submodel = LinearConverterModel(model, self) + return self.submodel def _plausibility_checks(self) -> None: super()._plausibility_checks() @@ -198,26 +203,29 @@ def _plausibility_checks(self) -> None: if self.piecewise_conversion: for flow in self.flows.values(): if isinstance(flow.size, InvestParameters) and flow.size.fixed_size is None: - raise PlausibilityError( - f'piecewise_conversion (in {self.label_full}) and variable size ' - f'(in flow {flow.label_full}) do not make sense together!' + logger.warning( + f'Using a Flow with variable size (InvestParameters without fixed_size) ' + f'and a piecewise_conversion in {self.label_full} is uncommon. Please verify intent ' + f'({flow.label_full}).' ) - def transform_data(self, flow_system: FlowSystem): - super().transform_data(flow_system) + def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None: + prefix = '|'.join(filter(None, [name_prefix, self.label_full])) + super().transform_data(flow_system, prefix) if self.conversion_factors: self.conversion_factors = self._transform_conversion_factors(flow_system) if self.piecewise_conversion: - self.piecewise_conversion.transform_data(flow_system, f'{self.label_full}|PiecewiseConversion') + self.piecewise_conversion.has_time_dim = True + self.piecewise_conversion.transform_data(flow_system, f'{prefix}|PiecewiseConversion') - def _transform_conversion_factors(self, flow_system: FlowSystem) -> list[dict[str, TimeSeries]]: - """macht alle Faktoren, die nicht TimeSeries sind, zu TimeSeries""" + def _transform_conversion_factors(self, flow_system: FlowSystem) -> list[dict[str, xr.DataArray]]: + """Converts all conversion factors to internal datatypes""" list_of_conversion_factors = [] for idx, conversion_factor in enumerate(self.conversion_factors): transformed_dict = {} for flow, values in conversion_factor.items(): # TODO: Might be better to use the label of the component instead of the flow - ts = flow_system.create_time_series(f'{self.flows[flow].label_full}|conversion_factor{idx}', values) + ts = flow_system.fit_to_model_coords(f'{self.flows[flow].label_full}|conversion_factor{idx}', values) if ts is None: raise PlausibilityError(f'{self.label_full}: conversion factor for flow "{flow}" must not be None') transformed_dict[flow] = ts @@ -243,37 +251,41 @@ class Storage(Component): final state constraints, and time-varying parameters. It supports both fixed-size and investment-optimized storage systems with comprehensive techno-economic modeling. + Mathematical Formulation: + See the complete mathematical model in the documentation: + [Storage](../user-guide/mathematical-notation/elements/Storage.md) + + - Equation (1): Charge state bounds + - Equation (3): Storage balance (charge state evolution) + + Variable Mapping: + - ``capacity_in_flow_hours`` → C (storage capacity) + - ``charge_state`` → c(t_i) (state of charge at time t_i) + - ``relative_loss_per_hour`` → ċ_rel,loss (self-discharge rate) + - ``eta_charge`` → η_in (charging efficiency) + - ``eta_discharge`` → η_out (discharging efficiency) + Args: - label: The label of the Element. Used to identify it in the FlowSystem. - charging: Incoming flow for loading the storage. Represents energy or material - flowing into the storage system. - discharging: Outgoing flow for unloading the storage. Represents energy or - material flowing out of the storage system. - capacity_in_flow_hours: Nominal capacity/size of the storage in flow-hours - (e.g., kWh for electrical storage, m³ or kg for material storage). Can be a scalar - for fixed capacity or InvestParameters for optimization. - relative_minimum_charge_state: Minimum relative charge state (0-1 range). - Prevents deep discharge that could damage equipment. Default is 0. - relative_maximum_charge_state: Maximum relative charge state (0-1 range). - Accounts for practical capacity limits, safety margins or temperature impacts. Default is 1. - initial_charge_state: Storage charge state at the beginning of the time horizon. - Can be numeric value or 'lastValueOfSim', which is recommended for if the initial start state is not known. - Default is 0. - minimal_final_charge_state: Minimum absolute charge state required at the end - of the time horizon. Useful for ensuring energy security or meeting contracts. - maximal_final_charge_state: Maximum absolute charge state allowed at the end - of the time horizon. Useful for preventing overcharge or managing inventory. - eta_charge: Charging efficiency factor (0-1 range). Accounts for conversion - losses during charging. Default is 1 (perfect efficiency). - eta_discharge: Discharging efficiency factor (0-1 range). Accounts for - conversion losses during discharging. Default is 1 (perfect efficiency). - relative_loss_per_hour: Self-discharge rate per hour (typically 0-0.1 range). - Represents standby losses, leakage, or degradation. Default is 0. - prevent_simultaneous_charge_and_discharge: If True, prevents charging and - discharging simultaneously. Increases binary variables but improves model - realism and solution interpretation. Default is True. - meta_data: Used to store additional information about the Element. Not used - internally, but saved in results. Only use Python native types. + label: Element identifier used in the FlowSystem. + charging: Incoming flow for loading the storage. + discharging: Outgoing flow for unloading the storage. + capacity_in_flow_hours: Storage capacity in flow-hours (kWh, m³, kg). + Scalar for fixed size or InvestParameters for optimization. + relative_minimum_charge_state: Minimum charge state (0-1). Default: 0. + relative_maximum_charge_state: Maximum charge state (0-1). Default: 1. + initial_charge_state: Charge at start. Numeric or 'lastValueOfSim'. Default: 0. + minimal_final_charge_state: Minimum absolute charge required at end (optional). + maximal_final_charge_state: Maximum absolute charge allowed at end (optional). + relative_minimum_final_charge_state: Minimum relative charge at end. + Defaults to last value of relative_minimum_charge_state. + relative_maximum_final_charge_state: Maximum relative charge at end. + Defaults to last value of relative_maximum_charge_state. + eta_charge: Charging efficiency (0-1). Default: 1. + eta_discharge: Discharging efficiency (0-1). Default: 1. + relative_loss_per_hour: Self-discharge per hour (0-0.1). Default: 0. + prevent_simultaneous_charge_and_discharge: Prevent charging and discharging + simultaneously. Adds binary variables. Default: True. + meta_data: Additional information stored in results. Python native types only. Examples: Battery energy storage system: @@ -349,20 +361,19 @@ class Storage(Component): ``` Note: - Charge state evolution follows the equation: - charge[t+1] = charge[t] × (1-loss_rate)^hours_per_step + - charge_flow[t] × eta_charge × hours_per_step - - discharge_flow[t] × hours_per_step / eta_discharge + **Mathematical formulation**: See [Storage](../user-guide/mathematical-notation/elements/Storage.md) + for charge state evolution equations and balance constraints. - All efficiency parameters (eta_charge, eta_discharge) are dimensionless (0-1 range). - The relative_loss_per_hour parameter represents exponential decay per hour. + **Efficiency parameters** (eta_charge, eta_discharge) are dimensionless (0-1 range). + The relative_loss_per_hour represents exponential decay per hour. - When prevent_simultaneous_charge_and_discharge is True, binary variables are - created to enforce mutual exclusivity, which increases solution time but - prevents unrealistic simultaneous charging and discharging. + **Binary variables**: When prevent_simultaneous_charge_and_discharge is True, binary + variables enforce mutual exclusivity, increasing solution time but preventing unrealistic + simultaneous charging and discharging. - Initial and final charge state constraints use absolute values (not relative), - matching the capacity_in_flow_hours units. + **Units**: Flow rates and charge states are related by the concept of 'flow hours' (=flow_rate * time). + With flow rates in kW, the charge state is therefore (usually) kWh. + With flow rates in m3/h, the charge state is therefore in m3. """ def __init__( @@ -370,16 +381,19 @@ def __init__( label: str, charging: Flow, discharging: Flow, - capacity_in_flow_hours: Scalar | InvestParameters, - relative_minimum_charge_state: NumericData = 0, - relative_maximum_charge_state: NumericData = 1, - initial_charge_state: Scalar | Literal['lastValueOfSim'] = 0, - minimal_final_charge_state: Scalar | None = None, - maximal_final_charge_state: Scalar | None = None, - eta_charge: NumericData = 1, - eta_discharge: NumericData = 1, - relative_loss_per_hour: NumericData = 0, + capacity_in_flow_hours: PeriodicDataUser | InvestParameters, + relative_minimum_charge_state: TemporalDataUser = 0, + relative_maximum_charge_state: TemporalDataUser = 1, + initial_charge_state: PeriodicDataUser | Literal['lastValueOfSim'] = 0, + minimal_final_charge_state: PeriodicDataUser | None = None, + maximal_final_charge_state: PeriodicDataUser | None = None, + relative_minimum_final_charge_state: PeriodicDataUser | None = None, + relative_maximum_final_charge_state: PeriodicDataUser | None = None, + eta_charge: TemporalDataUser = 1, + eta_discharge: TemporalDataUser = 1, + relative_loss_per_hour: TemporalDataUser = 0, prevent_simultaneous_charge_and_discharge: bool = True, + balanced: bool = False, meta_data: dict | None = None, ): # TODO: fixed_relative_chargeState implementieren @@ -394,72 +408,125 @@ def __init__( self.charging = charging self.discharging = discharging self.capacity_in_flow_hours = capacity_in_flow_hours - self.relative_minimum_charge_state: NumericDataTS = relative_minimum_charge_state - self.relative_maximum_charge_state: NumericDataTS = relative_maximum_charge_state + self.relative_minimum_charge_state: TemporalDataUser = relative_minimum_charge_state + self.relative_maximum_charge_state: TemporalDataUser = relative_maximum_charge_state + + self.relative_minimum_final_charge_state = relative_minimum_final_charge_state + self.relative_maximum_final_charge_state = relative_maximum_final_charge_state self.initial_charge_state = initial_charge_state self.minimal_final_charge_state = minimal_final_charge_state self.maximal_final_charge_state = maximal_final_charge_state - self.eta_charge: NumericDataTS = eta_charge - self.eta_discharge: NumericDataTS = eta_discharge - self.relative_loss_per_hour: NumericDataTS = relative_loss_per_hour + self.eta_charge: TemporalDataUser = eta_charge + self.eta_discharge: TemporalDataUser = eta_discharge + self.relative_loss_per_hour: TemporalDataUser = relative_loss_per_hour self.prevent_simultaneous_charge_and_discharge = prevent_simultaneous_charge_and_discharge + self.balanced = balanced - def create_model(self, model: SystemModel) -> StorageModel: + def create_model(self, model: FlowSystemModel) -> StorageModel: self._plausibility_checks() - self.model = StorageModel(model, self) - return self.model - - def transform_data(self, flow_system: FlowSystem) -> None: - super().transform_data(flow_system) - self.relative_minimum_charge_state = flow_system.create_time_series( - f'{self.label_full}|relative_minimum_charge_state', + self.submodel = StorageModel(model, self) + return self.submodel + + def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None: + prefix = '|'.join(filter(None, [name_prefix, self.label_full])) + super().transform_data(flow_system, prefix) + self.relative_minimum_charge_state = flow_system.fit_to_model_coords( + f'{prefix}|relative_minimum_charge_state', self.relative_minimum_charge_state, - needs_extra_timestep=True, ) - self.relative_maximum_charge_state = flow_system.create_time_series( - f'{self.label_full}|relative_maximum_charge_state', + self.relative_maximum_charge_state = flow_system.fit_to_model_coords( + f'{prefix}|relative_maximum_charge_state', self.relative_maximum_charge_state, - needs_extra_timestep=True, ) - self.eta_charge = flow_system.create_time_series(f'{self.label_full}|eta_charge', self.eta_charge) - self.eta_discharge = flow_system.create_time_series(f'{self.label_full}|eta_discharge', self.eta_discharge) - self.relative_loss_per_hour = flow_system.create_time_series( - f'{self.label_full}|relative_loss_per_hour', self.relative_loss_per_hour + self.eta_charge = flow_system.fit_to_model_coords(f'{prefix}|eta_charge', self.eta_charge) + self.eta_discharge = flow_system.fit_to_model_coords(f'{prefix}|eta_discharge', self.eta_discharge) + self.relative_loss_per_hour = flow_system.fit_to_model_coords( + f'{prefix}|relative_loss_per_hour', self.relative_loss_per_hour + ) + if not isinstance(self.initial_charge_state, str): + self.initial_charge_state = flow_system.fit_to_model_coords( + f'{prefix}|initial_charge_state', self.initial_charge_state, dims=['period', 'scenario'] + ) + self.minimal_final_charge_state = flow_system.fit_to_model_coords( + f'{prefix}|minimal_final_charge_state', self.minimal_final_charge_state, dims=['period', 'scenario'] + ) + self.maximal_final_charge_state = flow_system.fit_to_model_coords( + f'{prefix}|maximal_final_charge_state', self.maximal_final_charge_state, dims=['period', 'scenario'] + ) + self.relative_minimum_final_charge_state = flow_system.fit_to_model_coords( + f'{prefix}|relative_minimum_final_charge_state', + self.relative_minimum_final_charge_state, + dims=['period', 'scenario'], + ) + self.relative_maximum_final_charge_state = flow_system.fit_to_model_coords( + f'{prefix}|relative_maximum_final_charge_state', + self.relative_maximum_final_charge_state, + dims=['period', 'scenario'], ) if isinstance(self.capacity_in_flow_hours, InvestParameters): - self.capacity_in_flow_hours.transform_data(flow_system) + self.capacity_in_flow_hours.transform_data(flow_system, f'{prefix}|InvestParameters') + else: + self.capacity_in_flow_hours = flow_system.fit_to_model_coords( + f'{prefix}|capacity_in_flow_hours', self.capacity_in_flow_hours, dims=['period', 'scenario'] + ) def _plausibility_checks(self) -> None: """ Check for infeasible or uncommon combinations of parameters """ super()._plausibility_checks() - if utils.is_number(self.initial_charge_state): - if isinstance(self.capacity_in_flow_hours, InvestParameters): - if self.capacity_in_flow_hours.fixed_size is None: - maximum_capacity = self.capacity_in_flow_hours.maximum_size - minimum_capacity = self.capacity_in_flow_hours.minimum_size - else: - maximum_capacity = self.capacity_in_flow_hours.fixed_size - minimum_capacity = self.capacity_in_flow_hours.fixed_size + + # Validate string values and set flag + initial_is_last = False + if isinstance(self.initial_charge_state, str): + if self.initial_charge_state == 'lastValueOfSim': + initial_is_last = True else: - maximum_capacity = self.capacity_in_flow_hours - minimum_capacity = self.capacity_in_flow_hours + raise PlausibilityError(f'initial_charge_state has undefined value: {self.initial_charge_state}') - minimum_initial_capacity = maximum_capacity * self.relative_minimum_charge_state.isel(time=1) - maximum_initial_capacity = minimum_capacity * self.relative_maximum_charge_state.isel(time=1) - if self.initial_charge_state > maximum_initial_capacity: - raise ValueError( - f'{self.label_full}: {self.initial_charge_state=} is above allowed maximum {maximum_initial_capacity}' + # Use new InvestParameters methods to get capacity bounds + if isinstance(self.capacity_in_flow_hours, InvestParameters): + minimum_capacity = self.capacity_in_flow_hours.minimum_or_fixed_size + maximum_capacity = self.capacity_in_flow_hours.maximum_or_fixed_size + else: + maximum_capacity = self.capacity_in_flow_hours + minimum_capacity = self.capacity_in_flow_hours + + # Initial capacity should not constraint investment decision + minimum_initial_capacity = maximum_capacity * self.relative_minimum_charge_state.isel(time=0) + maximum_initial_capacity = minimum_capacity * self.relative_maximum_charge_state.isel(time=0) + + # Only perform numeric comparisons if not using 'lastValueOfSim' + if not initial_is_last: + if (self.initial_charge_state > maximum_initial_capacity).any(): + raise PlausibilityError( + f'{self.label_full}: {self.initial_charge_state=} ' + f'is constraining the investment decision. Chosse a value above {maximum_initial_capacity}' ) - if self.initial_charge_state < minimum_initial_capacity: - raise ValueError( - f'{self.label_full}: {self.initial_charge_state=} is below allowed minimum {minimum_initial_capacity}' + if (self.initial_charge_state < minimum_initial_capacity).any(): + raise PlausibilityError( + f'{self.label_full}: {self.initial_charge_state=} ' + f'is constraining the investment decision. Chosse a value below {minimum_initial_capacity}' + ) + + if self.balanced: + if not isinstance(self.charging.size, InvestParameters) or not isinstance( + self.discharging.size, InvestParameters + ): + raise PlausibilityError( + f'Balancing charging and discharging Flows in {self.label_full} is only possible with Investments.' + ) + + if (self.charging.size.minimum_size > self.discharging.size.maximum_size).any() or ( + self.charging.size.maximum_size < self.discharging.size.minimum_size + ).any(): + raise PlausibilityError( + f'Balancing charging and discharging Flows in {self.label_full} need compatible minimum and maximum sizes.' + f'Got: {self.charging.size.minimum_size=}, {self.charging.size.maximum_size=} and ' + f'{self.discharging.size.minimum_size=}, {self.discharging.size.maximum_size=}.' ) - elif self.initial_charge_state != 'lastValueOfSim': - raise ValueError(f'{self.label_full}: {self.initial_charge_state=} has an invalid value') @register_class_for_io @@ -491,6 +558,7 @@ class Transmission(Component): prevent_simultaneous_flows_in_both_directions: If True, prevents simultaneous flow in both directions. Increases binary variables but reflects physical reality for most transmission systems. Default is True. + balanced: Whether to equate the size of the in1 and in2 Flow. Needs InvestParameters in both Flows. meta_data: Used to store additional information. Not used internally but saved in results. Only use Python native types. @@ -579,10 +647,11 @@ def __init__( out1: Flow, in2: Flow | None = None, out2: Flow | None = None, - relative_losses: NumericDataTS = 0, - absolute_losses: NumericDataTS | None = None, - on_off_parameters: OnOffParameters | None = None, + relative_losses: TemporalDataUser | None = None, + absolute_losses: TemporalDataUser | None = None, + on_off_parameters: OnOffParameters = None, prevent_simultaneous_flows_in_both_directions: bool = True, + balanced: bool = False, meta_data: dict | None = None, ): super().__init__( @@ -602,6 +671,7 @@ def __init__( self.relative_losses = relative_losses self.absolute_losses = absolute_losses + self.balanced = balanced def _plausibility_checks(self): super()._plausibility_checks() @@ -614,51 +684,47 @@ def _plausibility_checks(self): assert self.out2.bus == self.in1.bus, ( f'Input 1 and Output 2 do not start/end at the same Bus: {self.in1.bus=}, {self.out2.bus=}' ) - # Check Investments - for flow in [self.out1, self.in2, self.out2]: - if flow is not None and isinstance(flow.size, InvestParameters): + + if self.balanced: + if self.in2 is None: + raise ValueError('Balanced Transmission needs InvestParameters in both in-Flows') + if not isinstance(self.in1.size, InvestParameters) or not isinstance(self.in2.size, InvestParameters): + raise ValueError('Balanced Transmission needs InvestParameters in both in-Flows') + if (self.in1.size.minimum_or_fixed_size > self.in2.size.maximum_or_fixed_size).any() or ( + self.in1.size.maximum_or_fixed_size < self.in2.size.minimum_or_fixed_size + ).any(): raise ValueError( - 'Transmission currently does not support separate InvestParameters for Flows. ' - 'Please use Flow in1. The size of in2 is equal to in1. THis is handled internally' + f'Balanced Transmission needs compatible minimum and maximum sizes.' + f'Got: {self.in1.size.minimum_size=}, {self.in1.size.maximum_size=}, {self.in1.size.fixed_size=} and ' + f'{self.in2.size.minimum_size=}, {self.in2.size.maximum_size=}, {self.in2.size.fixed_size=}.' ) def create_model(self, model) -> TransmissionModel: self._plausibility_checks() - self.model = TransmissionModel(model, self) - return self.model + self.submodel = TransmissionModel(model, self) + return self.submodel - def transform_data(self, flow_system: FlowSystem) -> None: - super().transform_data(flow_system) - self.relative_losses = flow_system.create_time_series( - f'{self.label_full}|relative_losses', self.relative_losses - ) - self.absolute_losses = flow_system.create_time_series( - f'{self.label_full}|absolute_losses', self.absolute_losses - ) + def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None: + prefix = '|'.join(filter(None, [name_prefix, self.label_full])) + super().transform_data(flow_system, prefix) + self.relative_losses = flow_system.fit_to_model_coords(f'{prefix}|relative_losses', self.relative_losses) + self.absolute_losses = flow_system.fit_to_model_coords(f'{prefix}|absolute_losses', self.absolute_losses) class TransmissionModel(ComponentModel): - def __init__(self, model: SystemModel, element: Transmission): - super().__init__(model, element) - self.element: Transmission = element - self.on_off: OnOffModel | None = None + element: Transmission - def do_modeling(self): - """Initiates all FlowModels""" - # Force On Variable if absolute losses are present - if (self.element.absolute_losses is not None) and np.any(self.element.absolute_losses.active_data != 0): - for flow in self.element.inputs + self.element.outputs: + def __init__(self, model: FlowSystemModel, element: Transmission): + if (element.absolute_losses is not None) and np.any(element.absolute_losses != 0): + for flow in element.inputs + element.outputs: if flow.on_off_parameters is None: flow.on_off_parameters = OnOffParameters() - # Make sure either None or both in Flows have InvestParameters - if self.element.in2 is not None: - if isinstance(self.element.in1.size, InvestParameters) and not isinstance( - self.element.in2.size, InvestParameters - ): - self.element.in2.size = InvestParameters(maximum_size=self.element.in1.size.maximum_size) + super().__init__(model, element) - super().do_modeling() + def _do_modeling(self): + """Initiates all FlowModels""" + super()._do_modeling() # first direction self.create_transmission_equation('dir1', self.element.in1, self.element.out1) @@ -668,43 +734,37 @@ def do_modeling(self): self.create_transmission_equation('dir2', self.element.in2, self.element.out2) # equate size of both directions - if isinstance(self.element.in1.size, InvestParameters) and self.element.in2 is not None: + if self.element.balanced: # eq: in1.size = in2.size - self.add( - self._model.add_constraints( - self.element.in1.model._investment.size == self.element.in2.model._investment.size, - name=f'{self.label_full}|same_size', - ), - 'same_size', + self.add_constraints( + self.element.in1.submodel._investment.size == self.element.in2.submodel._investment.size, + short_name='same_size', ) def create_transmission_equation(self, name: str, in_flow: Flow, out_flow: Flow) -> linopy.Constraint: """Creates an Equation for the Transmission efficiency and adds it to the model""" # eq: out(t) + on(t)*loss_abs(t) = in(t)*(1 - loss_rel(t)) - con_transmission = self.add( - self._model.add_constraints( - out_flow.model.flow_rate == -in_flow.model.flow_rate * (self.element.relative_losses.active_data - 1), - name=f'{self.label_full}|{name}', - ), - name, + rel_losses = 0 if self.element.relative_losses is None else self.element.relative_losses + con_transmission = self.add_constraints( + out_flow.submodel.flow_rate == in_flow.submodel.flow_rate * (1 - rel_losses), + short_name=name, ) if self.element.absolute_losses is not None: - con_transmission.lhs += in_flow.model.on_off.on * self.element.absolute_losses.active_data + con_transmission.lhs += in_flow.submodel.on_off.on * self.element.absolute_losses return con_transmission class LinearConverterModel(ComponentModel): - def __init__(self, model: SystemModel, element: LinearConverter): - super().__init__(model, element) - self.element: LinearConverter = element - self.on_off: OnOffModel | None = None - self.piecewise_conversion: PiecewiseModel | None = None + element: LinearConverter - def do_modeling(self): - super().do_modeling() + def __init__(self, model: FlowSystemModel, element: LinearConverter): + self.piecewise_conversion: PiecewiseConversion | None = None + super().__init__(model, element) + def _do_modeling(self): + super()._do_modeling() # conversion_factors: if self.element.conversion_factors: all_input_flows = set(self.element.inputs) @@ -716,146 +776,132 @@ def do_modeling(self): used_inputs: set[Flow] = all_input_flows & used_flows used_outputs: set[Flow] = all_output_flows & used_flows - self.add( - self._model.add_constraints( - sum([flow.model.flow_rate * conv_factors[flow.label].active_data for flow in used_inputs]) - == sum([flow.model.flow_rate * conv_factors[flow.label].active_data for flow in used_outputs]), - name=f'{self.label_full}|conversion_{i}', - ) + self.add_constraints( + sum([flow.submodel.flow_rate * conv_factors[flow.label] for flow in used_inputs]) + == sum([flow.submodel.flow_rate * conv_factors[flow.label] for flow in used_outputs]), + short_name=f'conversion_{i}', ) else: # TODO: Improve Inclusion of OnOffParameters. Instead of creating a Binary in every flow, the binary could only be part of the Piece itself piecewise_conversion = { - self.element.flows[flow].model.flow_rate.name: piecewise + self.element.flows[flow].submodel.flow_rate.name: piecewise for flow, piecewise in self.element.piecewise_conversion.items() } - self.piecewise_conversion = self.add( + self.piecewise_conversion = self.add_submodels( PiecewiseModel( model=self._model, label_of_element=self.label_of_element, + label_of_model=f'{self.label_of_element}', piecewise_variables=piecewise_conversion, zero_point=self.on_off.on if self.on_off is not None else False, - as_time_series=True, - ) + dims=('time', 'period', 'scenario'), + ), + short_name='PiecewiseConversion', ) - self.piecewise_conversion.do_modeling() class StorageModel(ComponentModel): - """Model of Storage""" + """Submodel of Storage""" + + element: Storage - def __init__(self, model: SystemModel, element: Storage): + def __init__(self, model: FlowSystemModel, element: Storage): super().__init__(model, element) - self.element: Storage = element - self.charge_state: linopy.Variable | None = None - self.netto_discharge: linopy.Variable | None = None - self._investment: InvestmentModel | None = None - - def do_modeling(self): - super().do_modeling() - - lb, ub = self.absolute_charge_state_bounds - self.charge_state = self.add( - self._model.add_variables( - lower=lb, upper=ub, coords=self._model.coords_extra, name=f'{self.label_full}|charge_state' - ), - 'charge_state', - ) - self.netto_discharge = self.add( - self._model.add_variables(coords=self._model.coords, name=f'{self.label_full}|netto_discharge'), - 'netto_discharge', + + def _do_modeling(self): + super()._do_modeling() + + lb, ub = self._absolute_charge_state_bounds + self.add_variables( + lower=lb, + upper=ub, + coords=self._model.get_coords(extra_timestep=True), + short_name='charge_state', ) + + self.add_variables(coords=self._model.get_coords(), short_name='netto_discharge') + # netto_discharge: # eq: nettoFlow(t) - discharging(t) + charging(t) = 0 - self.add( - self._model.add_constraints( - self.netto_discharge - == self.element.discharging.model.flow_rate - self.element.charging.model.flow_rate, - name=f'{self.label_full}|netto_discharge', - ), - 'netto_discharge', + self.add_constraints( + self.netto_discharge + == self.element.discharging.submodel.flow_rate - self.element.charging.submodel.flow_rate, + short_name='netto_discharge', ) charge_state = self.charge_state - rel_loss = self.element.relative_loss_per_hour.active_data + rel_loss = self.element.relative_loss_per_hour hours_per_step = self._model.hours_per_step - charge_rate = self.element.charging.model.flow_rate - discharge_rate = self.element.discharging.model.flow_rate - eff_charge = self.element.eta_charge.active_data - eff_discharge = self.element.eta_discharge.active_data - - self.add( - self._model.add_constraints( - charge_state.isel(time=slice(1, None)) - == charge_state.isel(time=slice(None, -1)) * ((1 - rel_loss) ** hours_per_step) - + charge_rate * eff_charge * hours_per_step - - discharge_rate * hours_per_step / eff_discharge, - name=f'{self.label_full}|charge_state', - ), - 'charge_state', + charge_rate = self.element.charging.submodel.flow_rate + discharge_rate = self.element.discharging.submodel.flow_rate + eff_charge = self.element.eta_charge + eff_discharge = self.element.eta_discharge + + self.add_constraints( + charge_state.isel(time=slice(1, None)) + == charge_state.isel(time=slice(None, -1)) * ((1 - rel_loss) ** hours_per_step) + + charge_rate * eff_charge * hours_per_step + - discharge_rate * hours_per_step / eff_discharge, + short_name='charge_state', ) if isinstance(self.element.capacity_in_flow_hours, InvestParameters): - self._investment = InvestmentModel( - model=self._model, - label_of_element=self.label_of_element, - parameters=self.element.capacity_in_flow_hours, - defining_variable=self.charge_state, - relative_bounds_of_defining_variable=self.relative_charge_state_bounds, + self.add_submodels( + InvestmentModel( + model=self._model, + label_of_element=self.label_of_element, + label_of_model=self.label_of_element, + parameters=self.element.capacity_in_flow_hours, + ), + short_name='investment', + ) + + BoundingPatterns.scaled_bounds( + self, + variable=self.charge_state, + scaling_variable=self.investment.size, + relative_bounds=self._relative_charge_state_bounds, ) - self.sub_models.append(self._investment) - self._investment.do_modeling() # Initial charge state self._initial_and_final_charge_state() + if self.element.balanced: + self.add_constraints( + self.element.charging.submodel._investment.size * 1 + == self.element.discharging.submodel._investment.size * 1, + short_name='balanced_sizes', + ) + def _initial_and_final_charge_state(self): if self.element.initial_charge_state is not None: - name_short = 'initial_charge_state' - name = f'{self.label_full}|{name_short}' - - if utils.is_number(self.element.initial_charge_state): - self.add( - self._model.add_constraints( - self.charge_state.isel(time=0) == self.element.initial_charge_state, name=name - ), - name_short, - ) - elif self.element.initial_charge_state == 'lastValueOfSim': - self.add( - self._model.add_constraints( - self.charge_state.isel(time=0) == self.charge_state.isel(time=-1), name=name - ), - name_short, + if isinstance(self.element.initial_charge_state, str): + self.add_constraints( + self.charge_state.isel(time=0) == self.charge_state.isel(time=-1), short_name='initial_charge_state' ) - else: # TODO: Validation in Storage Class, not in Model - raise PlausibilityError( - f'initial_charge_state has undefined value: {self.element.initial_charge_state}' + else: + self.add_constraints( + self.charge_state.isel(time=0) == self.element.initial_charge_state, + short_name='initial_charge_state', ) if self.element.maximal_final_charge_state is not None: - self.add( - self._model.add_constraints( - self.charge_state.isel(time=-1) <= self.element.maximal_final_charge_state, - name=f'{self.label_full}|final_charge_max', - ), - 'final_charge_max', + self.add_constraints( + self.charge_state.isel(time=-1) <= self.element.maximal_final_charge_state, + short_name='final_charge_max', ) if self.element.minimal_final_charge_state is not None: - self.add( - self._model.add_constraints( - self.charge_state.isel(time=-1) >= self.element.minimal_final_charge_state, - name=f'{self.label_full}|final_charge_min', - ), - 'final_charge_min', + self.add_constraints( + self.charge_state.isel(time=-1) >= self.element.minimal_final_charge_state, + short_name='final_charge_min', ) @property - def absolute_charge_state_bounds(self) -> tuple[NumericData, NumericData]: - relative_lower_bound, relative_upper_bound = self.relative_charge_state_bounds + def _absolute_charge_state_bounds(self) -> tuple[TemporalData, TemporalData]: + relative_lower_bound, relative_upper_bound = self._relative_charge_state_bounds if not isinstance(self.element.capacity_in_flow_hours, InvestParameters): return ( relative_lower_bound * self.element.capacity_in_flow_hours, @@ -868,11 +914,55 @@ def absolute_charge_state_bounds(self) -> tuple[NumericData, NumericData]: ) @property - def relative_charge_state_bounds(self) -> tuple[NumericData, NumericData]: - return ( - self.element.relative_minimum_charge_state.active_data, - self.element.relative_maximum_charge_state.active_data, - ) + def _relative_charge_state_bounds(self) -> tuple[xr.DataArray, xr.DataArray]: + """ + Get relative charge state bounds with final timestep values. + + Returns: + Tuple of (minimum_bounds, maximum_bounds) DataArrays extending to final timestep + """ + final_coords = {'time': [self._model.flow_system.timesteps_extra[-1]]} + + # Get final minimum charge state + if self.element.relative_minimum_final_charge_state is None: + min_final = self.element.relative_minimum_charge_state.isel(time=-1, drop=True) + else: + min_final = self.element.relative_minimum_final_charge_state + min_final = min_final.expand_dims('time').assign_coords(time=final_coords['time']) + + # Get final maximum charge state + if self.element.relative_maximum_final_charge_state is None: + max_final = self.element.relative_maximum_charge_state.isel(time=-1, drop=True) + else: + max_final = self.element.relative_maximum_final_charge_state + max_final = max_final.expand_dims('time').assign_coords(time=final_coords['time']) + # Concatenate with original bounds + min_bounds = xr.concat([self.element.relative_minimum_charge_state, min_final], dim='time') + max_bounds = xr.concat([self.element.relative_maximum_charge_state, max_final], dim='time') + + return min_bounds, max_bounds + + @property + def _investment(self) -> InvestmentModel | None: + """Deprecated alias for investment""" + return self.investment + + @property + def investment(self) -> InvestmentModel | None: + """OnOff feature""" + if 'investment' not in self.submodels: + return None + return self.submodels['investment'] + + @property + def charge_state(self) -> linopy.Variable: + """Charge state variable""" + return self['charge_state'] + + @property + def netto_discharge(self) -> linopy.Variable: + """Netto discharge variable""" + return self['netto_discharge'] @register_class_for_io @@ -970,36 +1060,19 @@ def __init__( meta_data: dict | None = None, **kwargs, ): - source = kwargs.pop('source', None) - sink = kwargs.pop('sink', None) - prevent_simultaneous_sink_and_source = kwargs.pop('prevent_simultaneous_sink_and_source', None) - if source is not None: - warnings.warn( - 'The use of the source argument is deprecated. Use the outputs argument instead.', - DeprecationWarning, - stacklevel=2, - ) - if outputs is not None: - raise ValueError('Either source or outputs can be specified, but not both.') - outputs = [source] - - if sink is not None: - warnings.warn( - 'The use of the sink argument is deprecated. Use the inputs argument instead.', - DeprecationWarning, - stacklevel=2, - ) - if inputs is not None: - raise ValueError('Either sink or inputs can be specified, but not both.') - inputs = [sink] - - if prevent_simultaneous_sink_and_source is not None: - warnings.warn( - 'The use of the prevent_simultaneous_sink_and_source argument is deprecated. Use the prevent_simultaneous_flow_rates argument instead.', - DeprecationWarning, - stacklevel=2, - ) - prevent_simultaneous_flow_rates = prevent_simultaneous_sink_and_source + # Handle deprecated parameters using centralized helper + outputs = self._handle_deprecated_kwarg(kwargs, 'source', 'outputs', outputs, transform=lambda x: [x]) + inputs = self._handle_deprecated_kwarg(kwargs, 'sink', 'inputs', inputs, transform=lambda x: [x]) + prevent_simultaneous_flow_rates = self._handle_deprecated_kwarg( + kwargs, + 'prevent_simultaneous_sink_and_source', + 'prevent_simultaneous_flow_rates', + prevent_simultaneous_flow_rates, + check_conflict=False, + ) + + # Validate any remaining unexpected kwargs + self._validate_kwargs(kwargs) super().__init__( label, @@ -1122,16 +1195,11 @@ def __init__( prevent_simultaneous_flow_rates: bool = False, **kwargs, ): - source = kwargs.pop('source', None) - if source is not None: - warnings.warn( - 'The use of the source argument is deprecated. Use the outputs argument instead.', - DeprecationWarning, - stacklevel=2, - ) - if outputs is not None: - raise ValueError('Either source or outputs can be specified, but not both.') - outputs = [source] + # Handle deprecated parameter using centralized helper + outputs = self._handle_deprecated_kwarg(kwargs, 'source', 'outputs', outputs, transform=lambda x: [x]) + + # Validate any remaining unexpected kwargs + self._validate_kwargs(kwargs) self.prevent_simultaneous_flow_rates = prevent_simultaneous_flow_rates super().__init__( @@ -1250,16 +1318,11 @@ def __init__( Note: The deprecated `sink` kwarg is accepted for compatibility but will be removed in future releases. """ - sink = kwargs.pop('sink', None) - if sink is not None: - warnings.warn( - 'The use of the sink argument is deprecated. Use the inputs argument instead.', - DeprecationWarning, - stacklevel=2, - ) - if inputs is not None: - raise ValueError('Either sink or inputs can be specified, but not both.') - inputs = [sink] + # Handle deprecated parameter using centralized helper + inputs = self._handle_deprecated_kwarg(kwargs, 'sink', 'inputs', inputs, transform=lambda x: [x]) + + # Validate any remaining unexpected kwargs + self._validate_kwargs(kwargs) self.prevent_simultaneous_flow_rates = prevent_simultaneous_flow_rates super().__init__( diff --git a/flixopt/config.py b/flixopt/config.py index 2ec5bf88c..4ac8263b2 100644 --- a/flixopt/config.py +++ b/flixopt/config.py @@ -1,6 +1,7 @@ from __future__ import annotations import logging +import sys import warnings from logging.handlers import RotatingFileHandler from pathlib import Path @@ -25,19 +26,20 @@ 'logging': MappingProxyType( { 'level': 'INFO', - 'file': 'flixopt.log', + 'file': None, 'rich': False, - 'console': True, + 'console': False, 'max_file_size': 10_485_760, # 10MB 'backup_count': 5, 'date_format': '%Y-%m-%d %H:%M:%S', 'format': '%(message)s', 'console_width': 120, 'show_path': False, + 'show_logger_name': False, 'colors': MappingProxyType( { - 'DEBUG': '\033[32m', # Green - 'INFO': '\033[34m', # Blue + 'DEBUG': '\033[90m', # Bright Black/Gray + 'INFO': '\033[0m', # Default/White 'WARNING': '\033[33m', # Yellow 'ERROR': '\033[31m', # Red 'CRITICAL': '\033[1m\033[31m', # Bold Red @@ -59,156 +61,111 @@ class CONFIG: """Configuration for flixopt library. - The CONFIG class provides centralized configuration for logging and modeling parameters. - All changes require calling ``CONFIG.apply()`` to take effect. - - By default, logging outputs to both console and file ('flixopt.log'). + Always call ``CONFIG.apply()`` after changes. Attributes: - Logging: Nested class containing all logging configuration options. - Colors: Nested subclass under Logging containing ANSI color codes for log levels. - Modeling: Nested class containing optimization modeling parameters. - config_name (str): Name of the configuration (default: 'flixopt'). - - Logging Attributes: - level (str): Logging level: 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'. - Default: 'INFO' - file (str | None): Log file path. Default: 'flixopt.log'. - Set to None to disable file logging. - console (bool): Enable console (stdout) logging. Default: True - rich (bool): Use Rich library for enhanced console output. Default: False - max_file_size (int): Maximum log file size in bytes before rotation. - Default: 10485760 (10MB) - backup_count (int): Number of backup log files to keep. Default: 5 - date_format (str): Date/time format for log messages. - Default: '%Y-%m-%d %H:%M:%S' - format (str): Log message format string. Default: '%(message)s' - console_width (int): Console width for Rich handler. Default: 120 - show_path (bool): Show file paths in log messages. Default: False - - Colors Attributes: - DEBUG (str): ANSI color code for DEBUG level. Default: '\\033[32m' (green) - INFO (str): ANSI color code for INFO level. Default: '\\033[34m' (blue) - WARNING (str): ANSI color code for WARNING level. Default: '\\033[33m' (yellow) - ERROR (str): ANSI color code for ERROR level. Default: '\\033[31m' (red) - CRITICAL (str): ANSI color code for CRITICAL level. Default: '\\033[1m\\033[31m' (bold red) - - Works with both Rich and standard console handlers. - Rich automatically converts ANSI codes using Style.from_ansi(). - - Common ANSI codes: - - - '\\033[30m' - Black - - '\\033[31m' - Red - - '\\033[32m' - Green - - '\\033[33m' - Yellow - - '\\033[34m' - Blue - - '\\033[35m' - Magenta - - '\\033[36m' - Cyan - - '\\033[37m' - White - - '\\033[1m\\033[3Xm' - Bold color (replace X with color code 0-7) - - '\\033[2m\\033[3Xm' - Dim color (replace X with color code 0-7) - - Examples: - - - Magenta: '\\033[35m' - - Bold cyan: '\\033[1m\\033[36m' - - Dim green: '\\033[2m\\033[32m' - - Modeling Attributes: - big (int): Large number for optimization constraints. Default: 10000000 - epsilon (float): Small tolerance value. Default: 1e-5 - big_binary_bound (int): Upper bound for binary variable constraints. - Default: 100000 + Logging: Logging configuration. + Modeling: Optimization modeling parameters. + config_name: Configuration name. Examples: - Basic configuration:: - - from flixopt import CONFIG - - CONFIG.Logging.console = True - CONFIG.Logging.level = 'DEBUG' - CONFIG.apply() - - Configure log file rotation:: - - CONFIG.Logging.file = 'myapp.log' - CONFIG.Logging.max_file_size = 5_242_880 # 5 MB - CONFIG.Logging.backup_count = 3 - CONFIG.apply() + ```python + CONFIG.Logging.console = True + CONFIG.Logging.level = 'DEBUG' + CONFIG.apply() + ``` + + Load from YAML file: + + ```yaml + logging: + level: DEBUG + console: true + file: app.log + ``` + """ - Customize log colors:: + class Logging: + """Logging configuration. + + Silent by default. Enable via ``console=True`` or ``file='path'``. + + Attributes: + level: Logging level. + file: Log file path for file logging. + console: Enable console output. + rich: Use Rich library for enhanced output. + max_file_size: Max file size before rotation. + backup_count: Number of backup files to keep. + date_format: Date/time format string. + format: Log message format string. + console_width: Console width for Rich handler. + show_path: Show file paths in messages. + show_logger_name: Show logger name in messages. + Colors: ANSI color codes for log levels. - CONFIG.Logging.Colors.INFO = '\\033[35m' # Magenta - CONFIG.Logging.Colors.DEBUG = '\\033[36m' # Cyan - CONFIG.Logging.Colors.ERROR = '\\033[1m\\033[31m' # Bold red + Examples: + ```python + # File logging with rotation + CONFIG.Logging.file = 'app.log' + CONFIG.Logging.max_file_size = 5_242_880 # 5MB CONFIG.apply() - Use Rich handler with custom colors:: - - CONFIG.Logging.console = True + # Rich handler with stdout + CONFIG.Logging.console = True # or 'stdout' CONFIG.Logging.rich = True - CONFIG.Logging.console_width = 100 - CONFIG.Logging.show_path = True - CONFIG.Logging.Colors.INFO = '\\033[36m' # Cyan CONFIG.apply() - Load from YAML file:: - - CONFIG.load_from_file('config.yaml') - - Example YAML config file: - - .. code-block:: yaml - - logging: - level: DEBUG - console: true - file: app.log - rich: true - max_file_size: 5242880 # 5MB - backup_count: 3 - date_format: '%H:%M:%S' - console_width: 100 - show_path: true - colors: - DEBUG: "\\033[36m" # Cyan - INFO: "\\033[32m" # Green - WARNING: "\\033[33m" # Yellow - ERROR: "\\033[31m" # Red - CRITICAL: "\\033[1m\\033[31m" # Bold red - - modeling: - big: 20000000 - epsilon: 1e-6 - big_binary_bound: 200000 - - Reset to defaults:: - - CONFIG.reset() - - Export current configuration:: - - config_dict = CONFIG.to_dict() - import yaml - - with open('my_config.yaml', 'w') as f: - yaml.dump(config_dict, f) - """ + # Console output to stderr + CONFIG.Logging.console = 'stderr' + CONFIG.apply() + ``` + """ - class Logging: - level: str = _DEFAULTS['logging']['level'] + level: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] = _DEFAULTS['logging']['level'] file: str | None = _DEFAULTS['logging']['file'] rich: bool = _DEFAULTS['logging']['rich'] - console: bool = _DEFAULTS['logging']['console'] + console: bool | Literal['stdout', 'stderr'] = _DEFAULTS['logging']['console'] max_file_size: int = _DEFAULTS['logging']['max_file_size'] backup_count: int = _DEFAULTS['logging']['backup_count'] date_format: str = _DEFAULTS['logging']['date_format'] format: str = _DEFAULTS['logging']['format'] console_width: int = _DEFAULTS['logging']['console_width'] show_path: bool = _DEFAULTS['logging']['show_path'] + show_logger_name: bool = _DEFAULTS['logging']['show_logger_name'] class Colors: + """ANSI color codes for log levels. + + Attributes: + DEBUG: ANSI color for DEBUG level. + INFO: ANSI color for INFO level. + WARNING: ANSI color for WARNING level. + ERROR: ANSI color for ERROR level. + CRITICAL: ANSI color for CRITICAL level. + + Examples: + ```python + CONFIG.Logging.Colors.INFO = '\\033[32m' # Green + CONFIG.Logging.Colors.ERROR = '\\033[1m\\033[31m' # Bold red + CONFIG.apply() + ``` + + Common ANSI codes: + - '\\033[30m' - Black + - '\\033[31m' - Red + - '\\033[32m' - Green + - '\\033[33m' - Yellow + - '\\033[34m' - Blue + - '\\033[35m' - Magenta + - '\\033[36m' - Cyan + - '\\033[37m' - White + - '\\033[90m' - Bright Black/Gray + - '\\033[0m' - Reset to default + - '\\033[1m\\033[3Xm' - Bold (replace X with color code 0-7) + - '\\033[2m\\033[3Xm' - Dim (replace X with color code 0-7) + """ + DEBUG: str = _DEFAULTS['logging']['colors']['DEBUG'] INFO: str = _DEFAULTS['logging']['colors']['INFO'] WARNING: str = _DEFAULTS['logging']['colors']['WARNING'] @@ -216,6 +173,14 @@ class Colors: CRITICAL: str = _DEFAULTS['logging']['colors']['CRITICAL'] class Modeling: + """Optimization modeling parameters. + + Attributes: + big: Large number for big-M constraints. + epsilon: Tolerance for numerical comparisons. + big_binary_bound: Upper bound for binary constraints. + """ + big: int = _DEFAULTS['modeling']['big'] epsilon: float = _DEFAULTS['modeling']['epsilon'] big_binary_bound: int = _DEFAULTS['modeling']['big_binary_bound'] @@ -250,6 +215,18 @@ def apply(cls): 'ERROR': cls.Logging.Colors.ERROR, 'CRITICAL': cls.Logging.Colors.CRITICAL, } + valid_levels = list(colors_dict) + if cls.Logging.level.upper() not in valid_levels: + raise ValueError(f"Invalid log level '{cls.Logging.level}'. Must be one of: {', '.join(valid_levels)}") + + if cls.Logging.max_file_size <= 0: + raise ValueError('max_file_size must be positive') + + if cls.Logging.backup_count < 0: + raise ValueError('backup_count must be non-negative') + + if cls.Logging.console not in (False, True, 'stdout', 'stderr'): + raise ValueError(f"console must be False, True, 'stdout', or 'stderr', got {cls.Logging.console}") _setup_logging( default_level=cls.Logging.level, @@ -262,25 +239,37 @@ def apply(cls): format=cls.Logging.format, console_width=cls.Logging.console_width, show_path=cls.Logging.show_path, + show_logger_name=cls.Logging.show_logger_name, colors=colors_dict, ) @classmethod def load_from_file(cls, config_file: str | Path): - """Load configuration from YAML file and apply it.""" + """Load configuration from YAML file and apply it. + + Args: + config_file: Path to the YAML configuration file. + + Raises: + FileNotFoundError: If the config file does not exist. + """ config_path = Path(config_file) if not config_path.exists(): raise FileNotFoundError(f'Config file not found: {config_file}') with config_path.open() as file: - config_dict = yaml.safe_load(file) + config_dict = yaml.safe_load(file) or {} cls._apply_config_dict(config_dict) cls.apply() @classmethod def _apply_config_dict(cls, config_dict: dict): - """Apply configuration dictionary to class attributes.""" + """Apply configuration dictionary to class attributes. + + Args: + config_dict: Dictionary containing configuration values. + """ for key, value in config_dict.items(): if key == 'logging' and isinstance(value, dict): for nested_key, nested_value in value.items(): @@ -298,7 +287,11 @@ def _apply_config_dict(cls, config_dict: dict): @classmethod def to_dict(cls): - """Convert the configuration class into a dictionary for JSON serialization.""" + """Convert the configuration class into a dictionary for JSON serialization. + + Returns: + Dictionary representation of the current configuration. + """ return { 'config_name': cls.config_name, 'logging': { @@ -312,6 +305,7 @@ def to_dict(cls): 'format': cls.Logging.format, 'console_width': cls.Logging.console_width, 'show_path': cls.Logging.show_path, + 'show_logger_name': cls.Logging.show_logger_name, 'colors': { 'DEBUG': cls.Logging.Colors.DEBUG, 'INFO': cls.Logging.Colors.INFO, @@ -328,40 +322,67 @@ def to_dict(cls): } -class MultilineFormater(logging.Formatter): - """Formatter that handles multi-line messages with consistent prefixes.""" +class MultilineFormatter(logging.Formatter): + """Formatter that handles multi-line messages with consistent prefixes. + + Args: + fmt: Log message format string. + datefmt: Date/time format string. + show_logger_name: Show logger name in log messages. + """ - def __init__(self, fmt=None, datefmt=None): + def __init__(self, fmt: str = '%(message)s', datefmt: str | None = None, show_logger_name: bool = False): super().__init__(fmt=fmt, datefmt=datefmt) + self.show_logger_name = show_logger_name - def format(self, record): - message_lines = record.getMessage().split('\n') + def format(self, record) -> str: + record.message = record.getMessage() + message_lines = self._style.format(record).split('\n') timestamp = self.formatTime(record, self.datefmt) log_level = record.levelname.ljust(8) - log_prefix = f'{timestamp} | {log_level} |' - first_line = [f'{log_prefix} {message_lines[0]}'] - if len(message_lines) > 1: - lines = first_line + [f'{log_prefix} {line}' for line in message_lines[1:]] + if self.show_logger_name: + # Truncate long logger names for readability + logger_name = record.name if len(record.name) <= 20 else f'...{record.name[-17:]}' + log_prefix = f'{timestamp} | {log_level} | {logger_name.ljust(20)} |' else: - lines = first_line + log_prefix = f'{timestamp} | {log_level} |' + + indent = ' ' * (len(log_prefix) + 1) # +1 for the space after prefix + + lines = [f'{log_prefix} {message_lines[0]}'] + if len(message_lines) > 1: + lines.extend([f'{indent}{line}' for line in message_lines[1:]]) return '\n'.join(lines) -class ColoredMultilineFormater(MultilineFormater): - """Formatter that adds ANSI colors to multi-line log messages.""" +class ColoredMultilineFormatter(MultilineFormatter): + """Formatter that adds ANSI colors to multi-line log messages. + + Args: + fmt: Log message format string. + datefmt: Date/time format string. + colors: Dictionary of ANSI color codes for each log level. + show_logger_name: Show logger name in log messages. + """ RESET = '\033[0m' - def __init__(self, fmt=None, datefmt=None, colors=None): - super().__init__(fmt=fmt, datefmt=datefmt) + def __init__( + self, + fmt: str | None = None, + datefmt: str | None = None, + colors: dict[str, str] | None = None, + show_logger_name: bool = False, + ): + super().__init__(fmt=fmt, datefmt=datefmt, show_logger_name=show_logger_name) self.COLORS = ( colors if colors is not None else { - 'DEBUG': '\033[32m', - 'INFO': '\033[34m', + 'DEBUG': '\033[90m', + 'INFO': '\033[0m', 'WARNING': '\033[33m', 'ERROR': '\033[31m', 'CRITICAL': '\033[1m\033[31m', @@ -377,18 +398,22 @@ def format(self, record): def _create_console_handler( use_rich: bool = False, + stream: Literal['stdout', 'stderr'] = 'stdout', console_width: int = 120, show_path: bool = False, + show_logger_name: bool = False, date_format: str = '%Y-%m-%d %H:%M:%S', format: str = '%(message)s', colors: dict[str, str] | None = None, ) -> logging.Handler: - """Create a console (stdout) logging handler. + """Create a console logging handler. Args: use_rich: If True, use RichHandler with color support. + stream: Output stream console_width: Width of the console for Rich handler. show_path: Show file paths in log messages (Rich only). + show_logger_name: Show logger name in log messages. date_format: Date/time format string. format: Log message format string. colors: Dictionary of ANSI color codes for each log level. @@ -396,6 +421,9 @@ def _create_console_handler( Returns: Configured logging handler (RichHandler or StreamHandler). """ + # Determine the stream object + stream_obj = sys.stdout if stream == 'stdout' else sys.stderr + if use_rich: # Convert ANSI codes to Rich theme if colors: @@ -413,7 +441,7 @@ def _create_console_handler( else: theme = None - console = Console(width=console_width, theme=theme) + console = Console(width=console_width, theme=theme, file=stream_obj) handler = RichHandler( console=console, rich_tracebacks=True, @@ -423,8 +451,15 @@ def _create_console_handler( ) handler.setFormatter(logging.Formatter(format)) else: - handler = logging.StreamHandler() - handler.setFormatter(ColoredMultilineFormater(fmt=format, datefmt=date_format, colors=colors)) + handler = logging.StreamHandler(stream=stream_obj) + handler.setFormatter( + ColoredMultilineFormatter( + fmt=format, + datefmt=date_format, + colors=colors, + show_logger_name=show_logger_name, + ) + ) return handler @@ -433,6 +468,7 @@ def _create_file_handler( log_file: str, max_file_size: int = 10_485_760, backup_count: int = 5, + show_logger_name: bool = False, date_format: str = '%Y-%m-%d %H:%M:%S', format: str = '%(message)s', ) -> RotatingFileHandler: @@ -442,19 +478,41 @@ def _create_file_handler( log_file: Path to the log file. max_file_size: Maximum size in bytes before rotation. backup_count: Number of backup files to keep. + show_logger_name: Show logger name in log messages. date_format: Date/time format string. format: Log message format string. Returns: Configured RotatingFileHandler (without colors). """ - handler = RotatingFileHandler( - log_file, - maxBytes=max_file_size, - backupCount=backup_count, - encoding='utf-8', + + # Ensure parent directory exists + log_path = Path(log_file) + try: + log_path.parent.mkdir(parents=True, exist_ok=True) + except PermissionError as e: + raise PermissionError(f"Cannot create log directory '{log_path.parent}': Permission denied") from e + + try: + handler = RotatingFileHandler( + log_file, + maxBytes=max_file_size, + backupCount=backup_count, + encoding='utf-8', + ) + except PermissionError as e: + raise PermissionError( + f"Cannot write to log file '{log_file}': Permission denied. " + f'Choose a different location or check file permissions.' + ) from e + + handler.setFormatter( + MultilineFormatter( + fmt=format, + datefmt=date_format, + show_logger_name=show_logger_name, + ) ) - handler.setFormatter(MultilineFormater(fmt=format, datefmt=date_format)) return handler @@ -462,15 +520,16 @@ def _setup_logging( default_level: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] = 'INFO', log_file: str | None = None, use_rich_handler: bool = False, - console: bool = False, + console: bool | Literal['stdout', 'stderr'] = False, max_file_size: int = 10_485_760, backup_count: int = 5, date_format: str = '%Y-%m-%d %H:%M:%S', format: str = '%(message)s', console_width: int = 120, show_path: bool = False, + show_logger_name: bool = False, colors: dict[str, str] | None = None, -): +) -> None: """Internal function to setup logging - use CONFIG.apply() instead. Configures the flixopt logger with console and/or file handlers. @@ -487,6 +546,7 @@ def _setup_logging( format: Log message format string. console_width: Console width for Rich handler. show_path: Show file paths in log messages (Rich only). + show_logger_name: Show logger name in log messages. colors: ANSI color codes for each log level. """ logger = logging.getLogger('flixopt') @@ -494,12 +554,17 @@ def _setup_logging( logger.propagate = False # Prevent duplicate logs logger.handlers.clear() + # Handle console parameter: False = disabled, True = stdout, 'stdout' = stdout, 'stderr' = stderr if console: + # Convert True to 'stdout', keep 'stdout'/'stderr' as-is + stream = 'stdout' if console is True else console logger.addHandler( _create_console_handler( use_rich=use_rich_handler, + stream=stream, console_width=console_width, show_path=show_path, + show_logger_name=show_logger_name, date_format=date_format, format=format, colors=colors, @@ -512,6 +577,7 @@ def _setup_logging( log_file=log_file, max_file_size=max_file_size, backup_count=backup_count, + show_logger_name=show_logger_name, date_format=date_format, format=format, ) @@ -521,28 +587,22 @@ def _setup_logging( if not logger.handlers: logger.addHandler(logging.NullHandler()) - return logger - def change_logging_level(level_name: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']): - """ - Change the logging level for the flixopt logger and all its handlers. + """Change the logging level for the flixopt logger and all its handlers. .. deprecated:: 2.1.11 Use ``CONFIG.Logging.level = level_name`` and ``CONFIG.apply()`` instead. This function will be removed in version 3.0.0. - Parameters - ---------- - level_name : {'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'} - The logging level to set. - - Examples - -------- - >>> change_logging_level('DEBUG') # deprecated - >>> # Use this instead: - >>> CONFIG.Logging.level = 'DEBUG' - >>> CONFIG.apply() + Args: + level_name: The logging level to set. + + Examples: + >>> change_logging_level('DEBUG') # deprecated + >>> # Use this instead: + >>> CONFIG.Logging.level = 'DEBUG' + >>> CONFIG.apply() """ warnings.warn( 'change_logging_level is deprecated and will be removed in version 3.0.0. ' diff --git a/flixopt/core.py b/flixopt/core.py index 532792e63..c163de554 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -3,13 +3,10 @@ It provides Datatypes, logging functionality, and some functions to transform data structures. """ -import inspect -import json import logging -import pathlib -from collections import Counter -from collections.abc import Iterator -from typing import Any, Literal, Optional +import warnings +from itertools import permutations +from typing import Literal, Union import numpy as np import pandas as pd @@ -18,10 +15,16 @@ logger = logging.getLogger('flixopt') Scalar = int | float -"""A type representing a single number, either integer or float.""" +"""A single number, either integer or float.""" -NumericData = int | float | np.integer | np.floating | np.ndarray | pd.Series | pd.DataFrame | xr.DataArray -"""Represents any form of numeric data, from simple scalars to complex data structures.""" +PeriodicDataUser = int | float | np.integer | np.floating | np.ndarray | pd.Series | pd.DataFrame | xr.DataArray +"""User data which has no time dimension. Internally converted to a Scalar or an xr.DataArray without a time dimension.""" + +PeriodicData = xr.DataArray +"""Internally used datatypes for periodic data.""" + +FlowSystemDimensions = Literal['time', 'period', 'scenario'] +"""Possible dimensions of a FlowSystem.""" class PlausibilityError(Exception): @@ -36,948 +39,607 @@ class ConversionError(Exception): pass -class DataConverter: - """ - Converts various data types into xarray.DataArray with a timesteps index. - - Supports: scalars, arrays, Series, DataFrames, and DataArrays. - """ - - @staticmethod - def as_dataarray(data: NumericData, timesteps: pd.DatetimeIndex) -> xr.DataArray: - """Convert data to xarray.DataArray with specified timesteps index.""" - if not isinstance(timesteps, pd.DatetimeIndex) or len(timesteps) == 0: - raise ValueError(f'Timesteps must be a non-empty DatetimeIndex, got {type(timesteps).__name__}') - if not timesteps.name == 'time': - raise ConversionError(f'DatetimeIndex is not named correctly. Must be named "time", got {timesteps.name=}') - - coords = [timesteps] - dims = ['time'] - expected_shape = (len(timesteps),) - - try: - if isinstance(data, (int, float, np.integer, np.floating)): - return xr.DataArray(data, coords=coords, dims=dims) - elif isinstance(data, pd.DataFrame): - if not data.index.equals(timesteps): - raise ConversionError( - f"DataFrame index doesn't match timesteps index. " - f'Its missing the following time steps: {timesteps.difference(data.index)}. ' - f'Some parameters might need an extra timestep at the end.' - ) - if not len(data.columns) == 1: - raise ConversionError('DataFrame must have exactly one column') - return xr.DataArray(data.values.flatten(), coords=coords, dims=dims) - elif isinstance(data, pd.Series): - if not data.index.equals(timesteps): - raise ConversionError( - f"Series index doesn't match timesteps index. " - f'Its missing the following time steps: {timesteps.difference(data.index)}. ' - f'Some parameters might need an extra timestep at the end.' - ) - return xr.DataArray(data.values, coords=coords, dims=dims) - elif isinstance(data, np.ndarray): - if data.ndim != 1: - raise ConversionError(f'Array must be 1-dimensional, got {data.ndim}') - elif data.shape[0] != expected_shape[0]: - raise ConversionError(f"Array shape {data.shape} doesn't match expected {expected_shape}") - return xr.DataArray(data, coords=coords, dims=dims) - elif isinstance(data, xr.DataArray): - if data.dims != tuple(dims): - raise ConversionError(f"DataArray dimensions {data.dims} don't match expected {dims}") - if data.sizes[dims[0]] != len(coords[0]): - raise ConversionError( - f"DataArray length {data.sizes[dims[0]]} doesn't match expected {len(coords[0])}" - ) - return data.copy(deep=True) - else: - raise ConversionError(f'Unsupported type: {type(data).__name__}') - except Exception as e: - if isinstance(e, ConversionError): - raise - raise ConversionError(f'Converting data {type(data)} to xarray.Dataset raised an error: {str(e)}') from e - - -class TimeSeriesData: - """ - TimeSeriesData wraps time series data with aggregation metadata for optimization. - - This class combines time series data with special characteristics needed for aggregated calculations. - It allows grouping related time series to prevent overweighting in optimization models. - - Example: - When you have multiple solar time series, they should share aggregation weight: - ```python - solar1 = TimeSeriesData(sol_array_1, agg_group='solar') - solar2 = TimeSeriesData(sol_array_2, agg_group='solar') - solar3 = TimeSeriesData(sol_array_3, agg_group='solar') - # These 3 series share one weight (each gets weight = 1/3 instead of 1) - ``` - - Args: - data: The timeseries data, which can be a scalar, array, or numpy array. - agg_group: The group this TimeSeriesData belongs to. agg_weight is split between group members. Default is None. - agg_weight: The weight for calculation_type 'aggregated', should be between 0 and 1. Default is None. - - Raises: - ValueError: If both agg_group and agg_weight are set. - """ - - # TODO: Move to Interface.py - def __init__(self, data: NumericData, agg_group: str | None = None, agg_weight: float | None = None): - self.data = data - self.agg_group = agg_group - self.agg_weight = agg_weight - if (agg_group is not None) and (agg_weight is not None): - raise ValueError('Either or explicit can be used. Not both!') - self.label: str | None = None - - def __repr__(self): - # Get the constructor arguments and their current values - init_signature = inspect.signature(self.__init__) - init_args = init_signature.parameters - - # Create a dictionary with argument names and their values - args_str = ', '.join(f'{name}={repr(getattr(self, name, None))}' for name in init_args if name != 'self') - return f'{self.__class__.__name__}({args_str})' - - def __str__(self): - return str(self.data) +class TimeSeriesData(xr.DataArray): + """Minimal TimeSeriesData that inherits from xr.DataArray with aggregation metadata.""" - -NumericDataTS = NumericData | TimeSeriesData -"""Represents either standard numeric data or TimeSeriesData.""" - - -class TimeSeries: - """ - A class representing time series data with active and stored states. - - TimeSeries provides a way to store time-indexed data and work with temporal subsets. - It supports arithmetic operations, aggregation, and JSON serialization. - - Attributes: - name (str): The name of the time series - aggregation_weight (Optional[float]): Weight used for aggregation - aggregation_group (Optional[str]): Group name for shared aggregation weighting - needs_extra_timestep (bool): Whether this series needs an extra timestep - """ - - @classmethod - def from_datasource( - cls, - data: NumericData, - name: str, - timesteps: pd.DatetimeIndex, - aggregation_weight: float | None = None, - aggregation_group: str | None = None, - needs_extra_timestep: bool = False, - ) -> 'TimeSeries': - """ - Initialize the TimeSeries from multiple data sources. - - Args: - data: The time series data - name: The name of the TimeSeries - timesteps: The timesteps of the TimeSeries - aggregation_weight: The weight in aggregation calculations - aggregation_group: Group this TimeSeries belongs to for aggregation weight sharing - needs_extra_timestep: Whether this series requires an extra timestep - - Returns: - A new TimeSeries instance - """ - return cls( - DataConverter.as_dataarray(data, timesteps), - name, - aggregation_weight, - aggregation_group, - needs_extra_timestep, - ) - - @classmethod - def from_json(cls, data: dict[str, Any] | None = None, path: str | None = None) -> 'TimeSeries': - """ - Load a TimeSeries from a dictionary or json file. - - Args: - data: Dictionary containing TimeSeries data - path: Path to a JSON file containing TimeSeries data - - Returns: - A new TimeSeries instance - - Raises: - ValueError: If both path and data are provided or neither is provided - """ - if (path is None and data is None) or (path is not None and data is not None): - raise ValueError("Exactly one of 'path' or 'data' must be provided") - - if path is not None: - with open(path) as f: - data = json.load(f) - - # Convert ISO date strings to datetime objects - data['data']['coords']['time']['data'] = pd.to_datetime(data['data']['coords']['time']['data']) - - # Create the TimeSeries instance - return cls( - data=xr.DataArray.from_dict(data['data']), - name=data['name'], - aggregation_weight=data['aggregation_weight'], - aggregation_group=data['aggregation_group'], - needs_extra_timestep=data['needs_extra_timestep'], - ) + __slots__ = () # No additional instance attributes - everything goes in attrs def __init__( self, - data: xr.DataArray, - name: str, - aggregation_weight: float | None = None, + *args, aggregation_group: str | None = None, - needs_extra_timestep: bool = False, + aggregation_weight: float | None = None, + agg_group: str | None = None, + agg_weight: float | None = None, + **kwargs, ): """ - Initialize a TimeSeries with a DataArray. - - Args: - data: The DataArray containing time series data - name: The name of the TimeSeries - aggregation_weight: The weight in aggregation calculations - aggregation_group: Group this TimeSeries belongs to for weight sharing - needs_extra_timestep: Whether this series requires an extra timestep - - Raises: - ValueError: If data doesn't have a 'time' index or has more than 1 dimension - """ - if 'time' not in data.indexes: - raise ValueError(f'DataArray must have a "time" index. Got {data.indexes}') - if data.ndim > 1: - raise ValueError(f'Number of dimensions of DataArray must be 1. Got {data.ndim}') - - self.name = name - self.aggregation_weight = aggregation_weight - self.aggregation_group = aggregation_group - self.needs_extra_timestep = needs_extra_timestep - - # Data management - self._stored_data = data.copy(deep=True) - self._backup = self._stored_data.copy(deep=True) - self._active_timesteps = self._stored_data.indexes['time'] - self._active_data = None - self._update_active_data() - - def reset(self): - """ - Reset active timesteps to the full set of stored timesteps. - """ - self.active_timesteps = None - - def restore_data(self): - """ - Restore stored_data from the backup and reset active timesteps. - """ - self._stored_data = self._backup.copy(deep=True) - self.reset() - - def to_json(self, path: pathlib.Path | None = None) -> dict[str, Any]: - """ - Save the TimeSeries to a dictionary or JSON file. - Args: - path: Optional path to save JSON file - - Returns: - Dictionary representation of the TimeSeries - """ - data = { - 'name': self.name, - 'aggregation_weight': self.aggregation_weight, - 'aggregation_group': self.aggregation_group, - 'needs_extra_timestep': self.needs_extra_timestep, - 'data': self.active_data.to_dict(), - } - - # Convert datetime objects to ISO strings - data['data']['coords']['time']['data'] = [date.isoformat() for date in data['data']['coords']['time']['data']] - - # Save to file if path is provided - if path is not None: - indent = 4 if len(self.active_timesteps) <= 480 else None - with open(path, 'w', encoding='utf-8') as f: - json.dump(data, f, indent=indent, ensure_ascii=False) - - return data - - @property - def stats(self) -> str: - """ - Return a statistical summary of the active data. - - Returns: - String representation of data statistics - """ - return get_numeric_stats(self.active_data, padd=0) - - def _update_active_data(self): - """ - Update the active data based on active_timesteps. - """ - self._active_data = self._stored_data.sel(time=self.active_timesteps) + *args: Arguments passed to DataArray + aggregation_group: Aggregation group name + aggregation_weight: Aggregation weight (0-1) + agg_group: Deprecated, use aggregation_group instead + agg_weight: Deprecated, use aggregation_weight instead + **kwargs: Additional arguments passed to DataArray + """ + if agg_group is not None: + warnings.warn('agg_group is deprecated, use aggregation_group instead', DeprecationWarning, stacklevel=2) + aggregation_group = agg_group + if agg_weight is not None: + warnings.warn('agg_weight is deprecated, use aggregation_weight instead', DeprecationWarning, stacklevel=2) + aggregation_weight = agg_weight + + if (aggregation_group is not None) and (aggregation_weight is not None): + raise ValueError('Use either aggregation_group or aggregation_weight, not both') + + # Let xarray handle all the initialization complexity + super().__init__(*args, **kwargs) + + # Add our metadata to attrs after initialization + if aggregation_group is not None: + self.attrs['aggregation_group'] = aggregation_group + if aggregation_weight is not None: + self.attrs['aggregation_weight'] = aggregation_weight + + # Always mark as TimeSeriesData + self.attrs['__timeseries_data__'] = True + + def fit_to_coords( + self, + coords: dict[str, pd.Index], + name: str | None = None, + ) -> 'TimeSeriesData': + """Fit the data to the given coordinates. Returns a new TimeSeriesData object if the current coords are different.""" + if self.coords.equals(xr.Coordinates(coords)): + return self + + da = DataConverter.to_dataarray(self.data, coords=coords) + return self.__class__( + da, + aggregation_group=self.aggregation_group, + aggregation_weight=self.aggregation_weight, + name=name if name is not None else self.name, + ) @property - def all_equal(self) -> bool: - """Check if all values in the series are equal.""" - return np.unique(self.active_data.values).size == 1 + def aggregation_group(self) -> str | None: + return self.attrs.get('aggregation_group') @property - def active_timesteps(self) -> pd.DatetimeIndex: - """Get the current active timesteps.""" - return self._active_timesteps - - @active_timesteps.setter - def active_timesteps(self, timesteps: pd.DatetimeIndex | None): - """ - Set active_timesteps and refresh active_data. + def aggregation_weight(self) -> float | None: + return self.attrs.get('aggregation_weight') - Args: - timesteps: New timesteps to activate, or None to use all stored timesteps - - Raises: - TypeError: If timesteps is not a pandas DatetimeIndex or None - """ - if timesteps is None: - self._active_timesteps = self.stored_data.indexes['time'] - elif isinstance(timesteps, pd.DatetimeIndex): - self._active_timesteps = timesteps - else: - raise TypeError('active_timesteps must be a pandas DatetimeIndex or None') - - self._update_active_data() - - @property - def active_data(self) -> xr.DataArray: - """Get a view of stored_data based on active_timesteps.""" - return self._active_data - - @property - def stored_data(self) -> xr.DataArray: - """Get a copy of the full stored data.""" - return self._stored_data.copy() + @classmethod + def from_dataarray( + cls, da: xr.DataArray, aggregation_group: str | None = None, aggregation_weight: float | None = None + ): + """Create TimeSeriesData from DataArray, extracting metadata from attrs.""" + # Get aggregation metadata from attrs or parameters + final_aggregation_group = ( + aggregation_group if aggregation_group is not None else da.attrs.get('aggregation_group') + ) + final_aggregation_weight = ( + aggregation_weight if aggregation_weight is not None else da.attrs.get('aggregation_weight') + ) - @stored_data.setter - def stored_data(self, value: NumericData): - """ - Update stored_data and refresh active_data. + return cls(da, aggregation_group=final_aggregation_group, aggregation_weight=final_aggregation_weight) - Args: - value: New data to store - """ - new_data = DataConverter.as_dataarray(value, timesteps=self.active_timesteps) + @classmethod + def is_timeseries_data(cls, obj) -> bool: + """Check if an object is TimeSeriesData.""" + return isinstance(obj, xr.DataArray) and obj.attrs.get('__timeseries_data__', False) - # Skip if data is unchanged to avoid overwriting backup - if new_data.equals(self._stored_data): - return + def __repr__(self): + agg_info = [] + if self.aggregation_group: + agg_info.append(f"aggregation_group='{self.aggregation_group}'") + if self.aggregation_weight is not None: + agg_info.append(f'aggregation_weight={self.aggregation_weight}') - self._stored_data = new_data - self.active_timesteps = None # Reset to full timeline + info_str = f'TimeSeriesData({", ".join(agg_info)})' if agg_info else 'TimeSeriesData' + return f'{info_str}\n{super().__repr__()}' @property - def sel(self): - return self.active_data.sel + def agg_group(self): + warnings.warn('agg_group is deprecated, use aggregation_group instead', DeprecationWarning, stacklevel=2) + return self.aggregation_group @property - def isel(self): - return self.active_data.isel - - def _apply_operation(self, other, op): - """Apply an operation between this TimeSeries and another object.""" - if isinstance(other, TimeSeries): - other = other.active_data - return op(self.active_data, other) - - def __add__(self, other): - return self._apply_operation(other, lambda x, y: x + y) - - def __sub__(self, other): - return self._apply_operation(other, lambda x, y: x - y) - - def __mul__(self, other): - return self._apply_operation(other, lambda x, y: x * y) - - def __truediv__(self, other): - return self._apply_operation(other, lambda x, y: x / y) + def agg_weight(self): + warnings.warn('agg_weight is deprecated, use aggregation_weight instead', DeprecationWarning, stacklevel=2) + return self.aggregation_weight - def __radd__(self, other): - return other + self.active_data - def __rsub__(self, other): - return other - self.active_data +TemporalDataUser = ( + int | float | np.integer | np.floating | np.ndarray | pd.Series | pd.DataFrame | xr.DataArray | TimeSeriesData +) +"""User data which might have a time dimension. Internally converted to an xr.DataArray with time dimension.""" - def __rmul__(self, other): - return other * self.active_data +TemporalData = xr.DataArray | TimeSeriesData +"""Internally used datatypes for temporal data (data with a time dimension).""" - def __rtruediv__(self, other): - return other / self.active_data - def __neg__(self) -> xr.DataArray: - return -self.active_data - - def __pos__(self) -> xr.DataArray: - return +self.active_data - - def __abs__(self) -> xr.DataArray: - return abs(self.active_data) - - def __gt__(self, other): - """ - Compare if this TimeSeries is greater than another. - - Args: - other: Another TimeSeries to compare with +class DataConverter: + """ + Converts various data types into xarray.DataArray with specified target coordinates. + + This converter handles intelligent dimension matching and broadcasting to ensure + the output DataArray always conforms to the specified coordinate structure. + + Supported input types: + - Scalars: int, float, np.number (broadcast to all target dimensions) + - 1D data: np.ndarray, pd.Series, single-column DataFrame (matched by length/index) + - Multi-dimensional arrays: np.ndarray, DataFrame (matched by shape) + - xr.DataArray: validated and potentially broadcast to target dimensions + + The converter uses smart matching strategies: + - Series: matched by exact index comparison + - 1D arrays: matched by length to target coordinates + - Multi-dimensional arrays: matched by shape permutation analysis + - DataArrays: validated for compatibility and broadcast as needed + """ - Returns: - True if all values in this TimeSeries are greater than other + @staticmethod + def _match_series_by_index_alignment( + data: pd.Series, target_coords: dict[str, pd.Index], target_dims: tuple[str, ...] + ) -> xr.DataArray: """ - if isinstance(other, TimeSeries): - return self.active_data > other.active_data - return self.active_data > other + Match pandas Series to target dimension by exact index comparison. - def __ge__(self, other): - """ - Compare if this TimeSeries is greater than or equal to another. + Attempts to find a target dimension whose coordinates exactly match + the Series index values, ensuring proper alignment. Args: - other: Another TimeSeries to compare with + data: pandas Series to convert + target_coords: Available target coordinates {dim_name: coordinate_index} + target_dims: Target dimension names to consider for matching Returns: - True if all values in this TimeSeries are greater than or equal to other - """ - if isinstance(other, TimeSeries): - return self.active_data >= other.active_data - return self.active_data >= other - - def __lt__(self, other): - """ - Compare if this TimeSeries is less than another. + DataArray with Series matched to the appropriate dimension - Args: - other: Another TimeSeries to compare with + Raises: + ConversionError: If Series cannot be matched to any target dimension, + or if no target dimensions provided for multi-element Series + """ + # Handle edge case: no target dimensions + if len(target_dims) == 0: + if len(data) != 1: + raise ConversionError( + f'Cannot convert multi-element Series without target dimensions. ' + f'Series has {len(data)} elements but no target dimensions specified.' + ) + return xr.DataArray(data.iloc[0]) + + # Attempt exact index matching with each target dimension + for dim_name in target_dims: + target_index = target_coords[dim_name] + if data.index.equals(target_index): + return xr.DataArray(data.values.copy(), coords={dim_name: target_index}, dims=dim_name) + + # No exact matches found + available_lengths = {dim: len(target_coords[dim]) for dim in target_dims} + raise ConversionError( + f'Series index does not match any target dimension coordinates. ' + f'Series length: {len(data)}, available coordinate lengths: {available_lengths}' + ) - Returns: - True if all values in this TimeSeries are less than other + @staticmethod + def _match_1d_array_by_length( + data: np.ndarray, target_coords: dict[str, pd.Index], target_dims: tuple[str, ...] + ) -> xr.DataArray: """ - if isinstance(other, TimeSeries): - return self.active_data < other.active_data - return self.active_data < other + Match 1D numpy array to target dimension by length comparison. - def __le__(self, other): - """ - Compare if this TimeSeries is less than or equal to another. + Finds target dimensions whose coordinate length matches the array length. + Requires unique length match to avoid ambiguity. Args: - other: Another TimeSeries to compare with + data: 1D numpy array to convert + target_coords: Available target coordinates {dim_name: coordinate_index} + target_dims: Target dimension names to consider for matching Returns: - True if all values in this TimeSeries are less than or equal to other - """ - if isinstance(other, TimeSeries): - return self.active_data <= other.active_data - return self.active_data <= other - - def __eq__(self, other): - """ - Compare if this TimeSeries is equal to another. + DataArray with array matched to the uniquely identified dimension - Args: - other: Another TimeSeries to compare with + Raises: + ConversionError: If array length matches zero or multiple target dimensions, + or if no target dimensions provided for multi-element array + """ + # Handle edge case: no target dimensions + if len(target_dims) == 0: + if len(data) != 1: + raise ConversionError( + f'Cannot convert multi-element array without target dimensions. Array has {len(data)} elements.' + ) + return xr.DataArray(data[0]) + + # Find all dimensions with matching lengths + array_length = len(data) + matching_dims = [] + coordinate_lengths = {} + + for dim_name in target_dims: + coord_length = len(target_coords[dim_name]) + coordinate_lengths[dim_name] = coord_length + if array_length == coord_length: + matching_dims.append(dim_name) + + # Validate matching results + if len(matching_dims) == 0: + raise ConversionError( + f'Array length {array_length} does not match any target dimension lengths: {coordinate_lengths}' + ) + elif len(matching_dims) > 1: + raise ConversionError( + f'Array length {array_length} matches multiple dimensions: {matching_dims}. ' + f'Cannot uniquely determine target dimension. Consider using explicit ' + f'dimension specification or converting to DataArray manually.' + ) - Returns: - True if all values in this TimeSeries are equal to other - """ - if isinstance(other, TimeSeries): - return self.active_data == other.active_data - return self.active_data == other + # Create DataArray with the uniquely matched dimension + matched_dim = matching_dims[0] + return xr.DataArray(data.copy(), coords={matched_dim: target_coords[matched_dim]}, dims=matched_dim) - def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): + @staticmethod + def _match_multidim_array_by_shape_permutation( + data: np.ndarray, target_coords: dict[str, pd.Index], target_dims: tuple[str, ...] + ) -> xr.DataArray: """ - Handle NumPy universal functions. + Match multi-dimensional array to target dimensions using shape permutation analysis. - This allows NumPy functions to work with TimeSeries objects. - """ - # Convert any TimeSeries inputs to their active_data - inputs = [x.active_data if isinstance(x, TimeSeries) else x for x in inputs] - return getattr(ufunc, method)(*inputs, **kwargs) + Analyzes all possible mappings between array shape and target coordinate lengths + to find the unique valid dimension assignment. - def __repr__(self): - """ - Get a string representation of the TimeSeries. + Args: + data: Multi-dimensional numpy array to convert + target_coords: Available target coordinates {dim_name: coordinate_index} + target_dims: Target dimension names to consider for matching Returns: - String showing TimeSeries details - """ - attrs = { - 'name': self.name, - 'aggregation_weight': self.aggregation_weight, - 'aggregation_group': self.aggregation_group, - 'needs_extra_timestep': self.needs_extra_timestep, - 'shape': self.active_data.shape, - 'time_range': f'{self.active_timesteps[0]} to {self.active_timesteps[-1]}', - } - attr_str = ', '.join(f'{k}={repr(v)}' for k, v in attrs.items()) - return f'TimeSeries({attr_str})' - - def __str__(self): - """ - Get a human-readable string representation. + DataArray with array dimensions mapped to target dimensions by shape - Returns: - Descriptive string with statistics - """ - return f"TimeSeries '{self.name}': {self.stats}" + Raises: + ConversionError: If array shape cannot be uniquely mapped to target dimensions, + or if no target dimensions provided for multi-element array + """ + # Handle edge case: no target dimensions + if len(target_dims) == 0: + if data.size != 1: + raise ConversionError( + f'Cannot convert multi-element array without target dimensions. ' + f'Array has {data.size} elements with shape {data.shape}.' + ) + return xr.DataArray(data.item()) + + array_shape = data.shape + coordinate_lengths = {dim: len(target_coords[dim]) for dim in target_dims} + + # Find all valid dimension permutations that match the array shape + valid_mappings = [] + for dim_permutation in permutations(target_dims, data.ndim): + shape_matches = all( + array_shape[i] == coordinate_lengths[dim_permutation[i]] for i in range(len(dim_permutation)) + ) + if shape_matches: + valid_mappings.append(dim_permutation) + + # Validate mapping results + if len(valid_mappings) == 0: + raise ConversionError( + f'Array shape {array_shape} cannot be mapped to any combination of target ' + f'coordinate lengths: {coordinate_lengths}. Consider reshaping the array ' + f'or adjusting target coordinates.' + ) + if len(valid_mappings) > 1: + raise ConversionError( + f'Array shape {array_shape} matches multiple dimension combinations: ' + f'{valid_mappings}. Cannot uniquely determine dimension mapping. ' + f'Consider using explicit dimension specification.' + ) -class TimeSeriesCollection: - """ - Collection of TimeSeries objects with shared timestep management. + # Create DataArray with the uniquely determined mapping + matched_dims = valid_mappings[0] + matched_coords = {dim: target_coords[dim] for dim in matched_dims} - TimeSeriesCollection handles multiple TimeSeries objects with synchronized - timesteps, provides operations on collections, and manages extra timesteps. - """ + return xr.DataArray(data.copy(), coords=matched_coords, dims=matched_dims) - def __init__( - self, - timesteps: pd.DatetimeIndex, - hours_of_last_timestep: float | None = None, - hours_of_previous_timesteps: float | np.ndarray | None = None, - ): - """ - Args: - timesteps: The timesteps of the Collection. - hours_of_last_timestep: The duration of the last time step. Uses the last time interval if not specified - hours_of_previous_timesteps: The duration of previous timesteps. - If None, the first time increment of time_series is used. - This is needed to calculate previous durations (for example consecutive_on_hours). - If you use an array, take care that its long enough to cover all previous values! + @staticmethod + def _broadcast_dataarray_to_target_specification( + source_data: xr.DataArray, target_coords: dict[str, pd.Index], target_dims: tuple[str, ...] + ) -> xr.DataArray: """ - # Prepare and validate timesteps - self._validate_timesteps(timesteps) - self.hours_of_previous_timesteps = self._calculate_hours_of_previous_timesteps( - timesteps, hours_of_previous_timesteps - ) - - # Set up timesteps and hours - self.all_timesteps = timesteps - self.all_timesteps_extra = self._create_timesteps_with_extra(timesteps, hours_of_last_timestep) - self.all_hours_per_timestep = self.calculate_hours_per_timestep(self.all_timesteps_extra) - - # Active timestep tracking - self._active_timesteps = None - self._active_timesteps_extra = None - self._active_hours_per_timestep = None - - # Dictionary of time series by name - self.time_series_data: dict[str, TimeSeries] = {} + Broadcast DataArray to conform to target coordinate and dimension specification. - # Aggregation - self.group_weights: dict[str, float] = {} - self.weights: dict[str, float] = {} - - @classmethod - def with_uniform_timesteps( - cls, start_time: pd.Timestamp, periods: int, freq: str, hours_per_step: float | None = None - ) -> 'TimeSeriesCollection': - """Create a collection with uniform timesteps.""" - timesteps = pd.date_range(start_time, periods=periods, freq=freq, name='time') - return cls(timesteps, hours_of_previous_timesteps=hours_per_step) - - def create_time_series( - self, data: NumericData | TimeSeriesData, name: str, needs_extra_timestep: bool = False - ) -> TimeSeries: - """ - Creates a TimeSeries from the given data and adds it to the collection. + Performs comprehensive validation and broadcasting to ensure the result exactly + matches the target specification. Handles scalar expansion, dimension validation, + coordinate compatibility checking, and broadcasting to additional dimensions. Args: - data: The data to create the TimeSeries from. - name: The name of the TimeSeries. - needs_extra_timestep: Whether to create an additional timestep at the end of the timesteps. + source_data: Source DataArray to broadcast + target_coords: Target coordinates {dim_name: coordinate_index} + target_dims: Target dimension names in desired order Returns: - The created TimeSeries. + DataArray broadcast to target specification with proper dimension ordering - """ - # Check for duplicate name - if name in self.time_series_data: - raise ValueError(f"TimeSeries '{name}' already exists in this collection") - - # Determine which timesteps to use - timesteps_to_use = self.timesteps_extra if needs_extra_timestep else self.timesteps - - # Create the time series - if isinstance(data, TimeSeriesData): - time_series = TimeSeries.from_datasource( - name=name, - data=data.data, - timesteps=timesteps_to_use, - aggregation_weight=data.agg_weight, - aggregation_group=data.agg_group, - needs_extra_timestep=needs_extra_timestep, - ) - # Connect the user time series to the created TimeSeries - data.label = name - else: - time_series = TimeSeries.from_datasource( - name=name, data=data, timesteps=timesteps_to_use, needs_extra_timestep=needs_extra_timestep + Raises: + ConversionError: If broadcasting is impossible due to incompatible dimensions + or coordinate mismatches + """ + # Validate: cannot reduce dimensions + if len(source_data.dims) > len(target_dims): + raise ConversionError( + f'Cannot reduce DataArray dimensionality from {len(source_data.dims)} ' + f'to {len(target_dims)} dimensions. Source dims: {source_data.dims}, ' + f'target dims: {target_dims}' ) - # Add to the collection - self.add_time_series(time_series) + # Validate: all source dimensions must exist in target + missing_dims = set(source_data.dims) - set(target_dims) + if missing_dims: + raise ConversionError( + f'Source DataArray has dimensions {missing_dims} not present in target dimensions {target_dims}' + ) - return time_series + # Validate: coordinate compatibility for overlapping dimensions + for dim in source_data.dims: + if dim in source_data.coords and dim in target_coords: + source_coords = source_data.coords[dim] + target_coords_for_dim = target_coords[dim] - def calculate_aggregation_weights(self) -> dict[str, float]: - """Calculate and return aggregation weights for all time series.""" - self.group_weights = self._calculate_group_weights() - self.weights = self._calculate_weights() + if not np.array_equal(source_coords.values, target_coords_for_dim.values): + raise ConversionError( + f'Coordinate mismatch for dimension "{dim}". ' + f'Source and target coordinates have different values.' + ) - if np.all(np.isclose(list(self.weights.values()), 1, atol=1e-6)): - logger.info('All Aggregation weights were set to 1') + # Create target template for broadcasting + target_shape = [len(target_coords[dim]) for dim in target_dims] + target_template = xr.DataArray(np.empty(target_shape), coords=target_coords, dims=target_dims) - return self.weights + # Perform broadcasting and ensure proper dimension ordering + broadcasted = source_data.broadcast_like(target_template) + return broadcasted.transpose(*target_dims) - def activate_timesteps(self, active_timesteps: pd.DatetimeIndex | None = None): - """ - Update active timesteps for the collection and all time series. - If no arguments are provided, the active timesteps are reset. + @classmethod + def to_dataarray( + cls, + data: int + | float + | bool + | np.integer + | np.floating + | np.bool_ + | np.ndarray + | pd.Series + | pd.DataFrame + | xr.DataArray, + coords: dict[str, pd.Index] | None = None, + ) -> xr.DataArray: + """ + Convert various data types to xarray.DataArray with specified target coordinates. + + This is the main conversion method that intelligently handles different input types + and ensures the result conforms to the specified coordinate structure through + smart dimension matching and broadcasting. Args: - active_timesteps: The active timesteps of the model. - If None, the all timesteps of the TimeSeriesCollection are taken. - """ - if active_timesteps is None: - return self.reset() - - if not np.all(np.isin(active_timesteps, self.all_timesteps)): - raise ValueError('active_timesteps must be a subset of the timesteps of the TimeSeriesCollection') - - # Calculate derived timesteps - self._active_timesteps = active_timesteps - first_ts_index = np.where(self.all_timesteps == active_timesteps[0])[0][0] - last_ts_idx = np.where(self.all_timesteps == active_timesteps[-1])[0][0] - self._active_timesteps_extra = self.all_timesteps_extra[first_ts_index : last_ts_idx + 2] - self._active_hours_per_timestep = self.all_hours_per_timestep.isel(time=slice(first_ts_index, last_ts_idx + 1)) + data: Input data to convert. Supported types: + - Scalars: int, float, bool, np.integer, np.floating, np.bool_ + - Arrays: np.ndarray (1D and multi-dimensional) + - Pandas: pd.Series, pd.DataFrame + - xarray: xr.DataArray + coords: Target coordinate specification as {dimension_name: coordinate_index}. + All coordinate indices must be pandas.Index objects. - # Update all time series - self._update_time_series_timesteps() - - def reset(self): - """Reset active timesteps to defaults for all time series.""" - self._active_timesteps = None - self._active_timesteps_extra = None - self._active_hours_per_timestep = None - - for time_series in self.time_series_data.values(): - time_series.reset() - - def restore_data(self): - """Restore original data for all time series.""" - for time_series in self.time_series_data.values(): - time_series.restore_data() - - def add_time_series(self, time_series: TimeSeries): - """Add an existing TimeSeries to the collection.""" - if time_series.name in self.time_series_data: - raise ValueError(f"TimeSeries '{time_series.name}' already exists in this collection") + Returns: + DataArray conforming to the target coordinate specification, + with input data appropriately matched and broadcast - self.time_series_data[time_series.name] = time_series + Raises: + ConversionError: If data type is unsupported, conversion fails, + or broadcasting to target coordinates is impossible + + Examples: + # Scalar broadcasting + >>> coords = {'x': pd.Index([1, 2, 3]), 'y': pd.Index(['a', 'b'])} + >>> converter.to_dataarray(42, coords) + # Returns: DataArray with shape (3, 2), all values = 42 + + # Series index matching + >>> series = pd.Series([10, 20, 30], index=[1, 2, 3]) + >>> converter.to_dataarray(series, coords) + # Returns: DataArray matched to 'x' dimension, broadcast to 'y' + + # Array shape matching + >>> array = np.array([[1, 2], [3, 4], [5, 6]]) # Shape (3, 2) + >>> converter.to_dataarray(array, coords) + # Returns: DataArray with dimensions ('x', 'y') based on shape + """ + # Prepare and validate target specification + if coords is None: + coords = {} + + validated_coords, target_dims = cls._validate_and_prepare_target_coordinates(coords) + + # Convert input data to intermediate DataArray based on type + if isinstance(data, (int, float, bool, np.integer, np.floating, np.bool_)): + # Scalar values - create scalar DataArray + intermediate = xr.DataArray(data.item() if hasattr(data, 'item') else data) + + elif isinstance(data, np.ndarray): + # NumPy arrays - dispatch based on dimensionality + if data.ndim == 0: + # 0-dimensional array (scalar) + intermediate = xr.DataArray(data.item()) + elif data.ndim == 1: + # 1-dimensional array + intermediate = cls._match_1d_array_by_length(data, validated_coords, target_dims) + else: + # Multi-dimensional array + intermediate = cls._match_multidim_array_by_shape_permutation(data, validated_coords, target_dims) + + elif isinstance(data, pd.Series): + # Pandas Series - validate and match by index + if isinstance(data.index, pd.MultiIndex): + raise ConversionError('MultiIndex Series are not supported. Please use a single-level index.') + intermediate = cls._match_series_by_index_alignment(data, validated_coords, target_dims) + + elif isinstance(data, pd.DataFrame): + # Pandas DataFrame - validate and convert + if isinstance(data.index, pd.MultiIndex): + raise ConversionError('MultiIndex DataFrames are not supported. Please use a single-level index.') + if len(data.columns) == 0 or data.empty: + raise ConversionError('DataFrame must have at least one column and cannot be empty.') + + if len(data.columns) == 1: + # Single-column DataFrame - treat as Series + series_data = data.iloc[:, 0] + intermediate = cls._match_series_by_index_alignment(series_data, validated_coords, target_dims) + else: + # Multi-column DataFrame - treat as multi-dimensional array + intermediate = cls._match_multidim_array_by_shape_permutation( + data.to_numpy(), validated_coords, target_dims + ) - def insert_new_data(self, data: pd.DataFrame, include_extra_timestep: bool = False): - """ - Update time series with new data from a DataFrame. + elif isinstance(data, xr.DataArray): + # Existing DataArray - use as-is + intermediate = data.copy() - Args: - data: DataFrame containing new data with timestamps as index - include_extra_timestep: Whether the provided data already includes the extra timestep, by default False - """ - if not isinstance(data, pd.DataFrame): - raise TypeError(f'data must be a pandas DataFrame, got {type(data).__name__}') - - # Check if the DataFrame index matches the expected timesteps - expected_timesteps = self.timesteps_extra if include_extra_timestep else self.timesteps - if not data.index.equals(expected_timesteps): - raise ValueError( - f'DataFrame index must match {"collection timesteps with extra timestep" if include_extra_timestep else "collection timesteps"}' + else: + # Unsupported data type + supported_types = [ + 'int', + 'float', + 'bool', + 'np.integer', + 'np.floating', + 'np.bool_', + 'np.ndarray', + 'pd.Series', + 'pd.DataFrame', + 'xr.DataArray', + ] + raise ConversionError( + f'Unsupported data type: {type(data).__name__}. Supported types: {", ".join(supported_types)}' ) - for name, ts in self.time_series_data.items(): - if name in data.columns: - if not ts.needs_extra_timestep: - # For time series without extra timestep - if include_extra_timestep: - # If data includes extra timestep but series doesn't need it, exclude the last point - ts.stored_data = data[name].iloc[:-1] - else: - # Use data as is - ts.stored_data = data[name] - else: - # For time series with extra timestep - if include_extra_timestep: - # Data already includes extra timestep - ts.stored_data = data[name] - else: - # Need to add extra timestep - extrapolate from the last value - extra_step_value = data[name].iloc[-1] - extra_step_index = pd.DatetimeIndex([self.timesteps_extra[-1]], name='time') - extra_step_series = pd.Series([extra_step_value], index=extra_step_index) - - # Combine the regular data with the extra timestep - ts.stored_data = pd.concat([data[name], extra_step_series]) - - logger.debug(f'Updated data for {name}') - - def to_dataframe( - self, filtered: Literal['all', 'constant', 'non_constant'] = 'non_constant', include_extra_timestep: bool = True - ) -> pd.DataFrame: - """ - Convert collection to DataFrame with optional filtering and timestep control. - - Args: - filtered: Filter time series by variability, by default 'non_constant' - include_extra_timestep: Whether to include the extra timestep in the result, by default True + # Broadcast intermediate result to target specification + return cls._broadcast_dataarray_to_target_specification(intermediate, validated_coords, target_dims) - Returns: - DataFrame representation of the collection + @staticmethod + def _validate_and_prepare_target_coordinates( + coords: dict[str, pd.Index], + ) -> tuple[dict[str, pd.Index], tuple[str, ...]]: """ - include_constants = filtered != 'non_constant' - ds = self.to_dataset(include_constants=include_constants) - - if not include_extra_timestep: - ds = ds.isel(time=slice(None, -1)) - - df = ds.to_dataframe() + Validate and prepare target coordinate specification for DataArray creation. - # Apply filtering - if filtered == 'all': - return df - elif filtered == 'constant': - return df.loc[:, df.nunique() == 1] - elif filtered == 'non_constant': - return df.loc[:, df.nunique() > 1] - else: - raise ValueError("filtered must be one of: 'all', 'constant', 'non_constant'") - - def to_dataset(self, include_constants: bool = True) -> xr.Dataset: - """ - Combine all time series into a single Dataset with all timesteps. + Performs comprehensive validation of coordinate inputs and prepares them + for use in DataArray construction with appropriate naming and type checking. Args: - include_constants: Whether to include time series with constant values, by default True + coords: Raw coordinate specification {dimension_name: coordinate_index} Returns: - Dataset containing all selected time series with all timesteps - """ - # Determine which series to include - if include_constants: - series_to_include = self.time_series_data.values() - else: - series_to_include = self.non_constants - - # Create individual datasets and merge them - ds = xr.merge([ts.active_data.to_dataset(name=ts.name) for ts in series_to_include]) - - # Ensure the correct time coordinates - ds = ds.reindex(time=self.timesteps_extra) - - ds.attrs.update( - { - 'timesteps_extra': f'{self.timesteps_extra[0]} ... {self.timesteps_extra[-1]} | len={len(self.timesteps_extra)}', - 'hours_per_timestep': self._format_stats(self.hours_per_timestep), - } - ) - - return ds - - def _update_time_series_timesteps(self): - """Update active timesteps for all time series.""" - for ts in self.time_series_data.values(): - if ts.needs_extra_timestep: - ts.active_timesteps = self.timesteps_extra - else: - ts.active_timesteps = self.timesteps + Tuple of (validated_coordinates_dict, dimension_names_tuple) - @staticmethod - def _validate_timesteps(timesteps: pd.DatetimeIndex): - """Validate timesteps format and rename if needed.""" - if not isinstance(timesteps, pd.DatetimeIndex): - raise TypeError('timesteps must be a pandas DatetimeIndex') - - if len(timesteps) < 2: - raise ValueError('timesteps must contain at least 2 timestamps') - - # Ensure timesteps has the required name - if timesteps.name != 'time': - logger.warning('Renamed timesteps to "time" (was "%s")', timesteps.name) - timesteps.name = 'time' - - @staticmethod - def _create_timesteps_with_extra( - timesteps: pd.DatetimeIndex, hours_of_last_timestep: float | None - ) -> pd.DatetimeIndex: - """Create timesteps with an extra step at the end.""" - if hours_of_last_timestep is not None: - # Create the extra timestep using the specified duration - last_date = pd.DatetimeIndex([timesteps[-1] + pd.Timedelta(hours=hours_of_last_timestep)], name='time') - else: - # Use the last interval as the extra timestep duration - last_date = pd.DatetimeIndex([timesteps[-1] + (timesteps[-1] - timesteps[-2])], name='time') - - # Combine with original timesteps - return pd.DatetimeIndex(timesteps.append(last_date), name='time') - - @staticmethod - def _calculate_hours_of_previous_timesteps( - timesteps: pd.DatetimeIndex, hours_of_previous_timesteps: float | np.ndarray | None - ) -> float | np.ndarray: - """Calculate duration of regular timesteps.""" - if hours_of_previous_timesteps is not None: - return hours_of_previous_timesteps - - # Calculate from the first interval - first_interval = timesteps[1] - timesteps[0] - return first_interval.total_seconds() / 3600 # Convert to hours + Raises: + ConversionError: If any coordinates are invalid, improperly typed, + or have inconsistent naming + """ + validated_coords = {} + dimension_names = [] - @staticmethod - def calculate_hours_per_timestep(timesteps_extra: pd.DatetimeIndex) -> xr.DataArray: - """Calculate duration of each timestep.""" - # Calculate differences between consecutive timestamps - hours_per_step = np.diff(timesteps_extra) / pd.Timedelta(hours=1) + for dim_name, coord_index in coords.items(): + # Type validation + if not isinstance(coord_index, pd.Index): + raise ConversionError( + f'Coordinate for dimension "{dim_name}" must be a pandas.Index, got {type(coord_index).__name__}' + ) - return xr.DataArray( - data=hours_per_step, coords={'time': timesteps_extra[:-1]}, dims=('time',), name='hours_per_step' - ) + # Non-empty validation + if len(coord_index) == 0: + raise ConversionError(f'Coordinate for dimension "{dim_name}" cannot be empty') - def _calculate_group_weights(self) -> dict[str, float]: - """Calculate weights for aggregation groups.""" - # Count series in each group - groups = [ts.aggregation_group for ts in self.time_series_data.values() if ts.aggregation_group is not None] - group_counts = Counter(groups) - - # Calculate weight for each group (1/count) - return {group: 1 / count for group, count in group_counts.items()} - - def _calculate_weights(self) -> dict[str, float]: - """Calculate weights for all time series.""" - # Calculate weight for each time series - weights = {} - for name, ts in self.time_series_data.items(): - if ts.aggregation_group is not None: - # Use group weight - weights[name] = self.group_weights.get(ts.aggregation_group, 1) - else: - # Use individual weight or default to 1 - weights[name] = ts.aggregation_weight or 1 + # Ensure coordinate index has consistent naming + if coord_index.name != dim_name: + coord_index = coord_index.rename(dim_name) - return weights + # Special validation for time dimensions (common pattern) + if dim_name == 'time' and not isinstance(coord_index, pd.DatetimeIndex): + raise ConversionError( + f'Dimension named "time" should use DatetimeIndex for proper ' + f'time-series functionality, got {type(coord_index).__name__}' + ) - def _format_stats(self, data) -> str: - """Format statistics for a data array.""" - if hasattr(data, 'values'): - values = data.values - else: - values = np.asarray(data) + validated_coords[dim_name] = coord_index + dimension_names.append(dim_name) - mean_val = np.mean(values) - min_val = np.min(values) - max_val = np.max(values) + return validated_coords, tuple(dimension_names) - return f'mean: {mean_val:.2f}, min: {min_val:.2f}, max: {max_val:.2f}' - def __getitem__(self, name: str) -> TimeSeries: - """Get a TimeSeries by name.""" +def get_dataarray_stats(arr: xr.DataArray) -> dict: + """Generate statistical summary of a DataArray.""" + stats = {} + if arr.dtype.kind in 'biufc': # bool, int, uint, float, complex try: - return self.time_series_data[name] - except KeyError as e: - raise KeyError(f'TimeSeries "{name}" not found in the TimeSeriesCollection') from e - - def __iter__(self) -> Iterator[TimeSeries]: - """Iterate through all TimeSeries in the collection.""" - return iter(self.time_series_data.values()) - - def __len__(self) -> int: - """Get the number of TimeSeries in the collection.""" - return len(self.time_series_data) - - def __contains__(self, item: str | TimeSeries) -> bool: - """Check if a TimeSeries exists in the collection.""" - if isinstance(item, str): - return item in self.time_series_data - elif isinstance(item, TimeSeries): - return any([item is ts for ts in self.time_series_data.values()]) - return False + stats.update( + { + 'min': float(arr.min().values), + 'max': float(arr.max().values), + 'mean': float(arr.mean().values), + 'median': float(arr.median().values), + 'std': float(arr.std().values), + 'count': int(arr.count().values), # non-null count + } + ) - @property - def non_constants(self) -> list[TimeSeries]: - """Get time series with varying values.""" - return [ts for ts in self.time_series_data.values() if not ts.all_equal] + # Add null count only if there are nulls + null_count = int(arr.isnull().sum().values) + if null_count > 0: + stats['nulls'] = null_count - @property - def constants(self) -> list[TimeSeries]: - """Get time series with constant values.""" - return [ts for ts in self.time_series_data.values() if ts.all_equal] + except Exception: + pass - @property - def timesteps(self) -> pd.DatetimeIndex: - """Get the active timesteps.""" - return self.all_timesteps if self._active_timesteps is None else self._active_timesteps + return stats - @property - def timesteps_extra(self) -> pd.DatetimeIndex: - """Get the active timesteps with extra step.""" - return self.all_timesteps_extra if self._active_timesteps_extra is None else self._active_timesteps_extra - @property - def hours_per_timestep(self) -> xr.DataArray: - """Get the duration of each active timestep.""" - return ( - self.all_hours_per_timestep if self._active_hours_per_timestep is None else self._active_hours_per_timestep - ) +def drop_constant_arrays(ds: xr.Dataset, dim: str = 'time', drop_arrays_without_dim: bool = True) -> xr.Dataset: + """Drop variables with constant values along a dimension. - @property - def hours_of_last_timestep(self) -> float: - """Get the duration of the last timestep.""" - return float(self.hours_per_timestep[-1].item()) - - def __repr__(self): - return f'TimeSeriesCollection:\n{self.to_dataset()}' - - def __str__(self): - longest_name = max([time_series.name for time_series in self.time_series_data], key=len) + Args: + ds: Input dataset to filter. + dim: Dimension along which to check for constant values. + drop_arrays_without_dim: If True, also drop variables that don't have the specified dimension. - stats_summary = '\n'.join( - [ - f' - {time_series.name:<{len(longest_name)}}: {get_numeric_stats(time_series.active_data)}' - for time_series in self.time_series_data - ] + Returns: + Dataset with constant variables removed. + """ + drop_vars = [] + + for name, da in ds.data_vars.items(): + # Skip variables without the dimension + if dim not in da.dims: + if drop_arrays_without_dim: + drop_vars.append(name) + continue + + # Check if variable is constant along the dimension + if (da.max(dim, skipna=True) == da.min(dim, skipna=True)).all().item(): + drop_vars.append(name) + + if drop_vars: + drop_vars = sorted(drop_vars) + logger.debug( + f'Dropping {len(drop_vars)} constant/dimension-less arrays: {drop_vars[:5]}{"..." if len(drop_vars) > 5 else ""}' ) - return ( - f'TimeSeriesCollection with {len(self.time_series_data)} series\n' - f' Time Range: {self.timesteps[0]} → {self.timesteps[-1]}\n' - f' No. of timesteps: {len(self.timesteps)} + 1 extra\n' - f' Hours per timestep: {get_numeric_stats(self.hours_per_timestep)}\n' - f' Time Series Data:\n' - f'{stats_summary}' - ) + return ds.drop_vars(drop_vars) -def get_numeric_stats(data: xr.DataArray, decimals: int = 2, padd: int = 10) -> str: - """Calculates the mean, median, min, max, and standard deviation of a numeric DataArray.""" - format_spec = f'>{padd}.{decimals}f' if padd else f'.{decimals}f' - if np.unique(data).size == 1: - return f'{data.max().item():{format_spec}} (constant)' - mean = data.mean().item() - median = data.median().item() - min_val = data.min().item() - max_val = data.max().item() - std = data.std().item() - return f'{mean:{format_spec}} (mean), {median:{format_spec}} (median), {min_val:{format_spec}} (min), {max_val:{format_spec}} (max), {std:{format_spec}} (std)' +# Backward compatibility aliases +# TODO: Needed? +NonTemporalDataUser = PeriodicDataUser +NonTemporalData = PeriodicData diff --git a/flixopt/effects.py b/flixopt/effects.py index 31c941e11..2c7607b02 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -9,14 +9,16 @@ import logging import warnings +from collections import deque from typing import TYPE_CHECKING, Literal import linopy import numpy as np +import xarray as xr -from .core import NumericDataTS, Scalar, TimeSeries +from .core import PeriodicDataUser, Scalar, TemporalData, TemporalDataUser from .features import ShareAllocationModel -from .structure import Element, ElementModel, Model, SystemModel, register_class_for_io +from .structure import Element, ElementModel, FlowSystemModel, Submodel, register_class_for_io if TYPE_CHECKING: from collections.abc import Iterator @@ -48,36 +50,48 @@ class Effect(Element): without effect dictionaries. Used for simplified effect specification (and less boilerplate code). is_objective: If True, this effect serves as the optimization objective function. Only one effect can be marked as objective per optimization. - specific_share_to_other_effects_operation: Operational cross-effect contributions. - Maps this effect's operational values to contributions to other effects - specific_share_to_other_effects_invest: Investment cross-effect contributions. - Maps this effect's investment values to contributions to other effects. - minimum_operation: Minimum allowed total operational contribution across all timesteps. - maximum_operation: Maximum allowed total operational contribution across all timesteps. - minimum_operation_per_hour: Minimum allowed operational contribution per timestep. - maximum_operation_per_hour: Maximum allowed operational contribution per timestep. - minimum_invest: Minimum allowed total investment contribution. - maximum_invest: Maximum allowed total investment contribution. - minimum_total: Minimum allowed total effect (operation + investment combined). - maximum_total: Maximum allowed total effect (operation + investment combined). + share_from_temporal: Temporal cross-effect contributions. + Maps temporal contributions from other effects to this effect + share_from_periodic: Periodic cross-effect contributions. + Maps periodic contributions from other effects to this effect. + minimum_temporal: Minimum allowed total contribution across all timesteps. + maximum_temporal: Maximum allowed total contribution across all timesteps. + minimum_per_hour: Minimum allowed contribution per hour. + maximum_per_hour: Maximum allowed contribution per hour. + minimum_periodic: Minimum allowed total periodic contribution. + maximum_periodic: Maximum allowed total periodic contribution. + minimum_total: Minimum allowed total effect (temporal + periodic combined). + maximum_total: Maximum allowed total effect (temporal + periodic combined). meta_data: Used to store additional information. Not used internally but saved in results. Only use Python native types. + **Deprecated Parameters** (for backwards compatibility): + minimum_operation: Use `minimum_temporal` instead. + maximum_operation: Use `maximum_temporal` instead. + minimum_invest: Use `minimum_periodic` instead. + maximum_invest: Use `maximum_periodic` instead. + minimum_operation_per_hour: Use `minimum_per_hour` instead. + maximum_operation_per_hour: Use `maximum_per_hour` instead. + Examples: Basic cost objective: ```python - cost_effect = Effect(label='system_costs', unit='€', description='Total system costs', is_objective=True) + cost_effect = Effect( + label='system_costs', + unit='€', + description='Total system costs', + is_objective=True, + ) ``` - CO2 emissions with carbon pricing: + CO2 emissions: ```python co2_effect = Effect( - label='co2_emissions', + label='CO2', unit='kg_CO2', description='Carbon dioxide emissions', - specific_share_to_other_effects_operation={'costs': 50}, # €50/t_CO2 maximum_total=1_000_000, # 1000 t CO2 annual limit ) ``` @@ -100,7 +114,21 @@ class Effect(Element): label='primary_energy', unit='kWh_primary', description='Primary energy consumption', - specific_share_to_other_effects_operation={'costs': 0.08}, # €0.08/kWh + ) + ``` + + Cost objective with carbon and primary energy pricing: + + ```python + cost_effect = Effect( + label='system_costs', + unit='€', + description='Total system costs', + is_objective=True, + share_from_temporal={ + 'primary_energy': 0.08, # 0.08 €/kWh_primary + 'CO2': 0.2, # Carbon pricing: 0.2 €/kg_CO2 into costs if used on a cost effect + }, ) ``` @@ -111,8 +139,8 @@ class Effect(Element): label='water_consumption', unit='m³', description='Industrial water usage', - minimum_operation_per_hour=10, # Minimum 10 m³/h for process stability - maximum_operation_per_hour=500, # Maximum 500 m³/h capacity limit + minimum_per_hour=10, # Minimum 10 m³/h for process stability + maximum_per_hour=500, # Maximum 500 m³/h capacity limit maximum_total=100_000, # Annual permit limit: 100,000 m³ ) ``` @@ -127,8 +155,7 @@ class Effect(Element): across all contributions to each effect manually. Effects are accumulated as: - - Total = Σ(operational contributions) + Σ(investment contributions) - - Cross-effects add to target effects based on specific_share ratios + - Total = Σ(temporal contributions) + Σ(periodic contributions) """ @@ -140,53 +167,220 @@ def __init__( meta_data: dict | None = None, is_standard: bool = False, is_objective: bool = False, - specific_share_to_other_effects_operation: EffectValuesUser | None = None, - specific_share_to_other_effects_invest: EffectValuesUser | None = None, - minimum_operation: Scalar | None = None, - maximum_operation: Scalar | None = None, - minimum_invest: Scalar | None = None, - maximum_invest: Scalar | None = None, - minimum_operation_per_hour: NumericDataTS | None = None, - maximum_operation_per_hour: NumericDataTS | None = None, + share_from_temporal: TemporalEffectsUser | None = None, + share_from_periodic: PeriodicEffectsUser | None = None, + minimum_temporal: PeriodicEffectsUser | None = None, + maximum_temporal: PeriodicEffectsUser | None = None, + minimum_periodic: PeriodicEffectsUser | None = None, + maximum_periodic: PeriodicEffectsUser | None = None, + minimum_per_hour: TemporalDataUser | None = None, + maximum_per_hour: TemporalDataUser | None = None, minimum_total: Scalar | None = None, maximum_total: Scalar | None = None, + **kwargs, ): super().__init__(label, meta_data=meta_data) - self.label = label self.unit = unit self.description = description self.is_standard = is_standard self.is_objective = is_objective - self.specific_share_to_other_effects_operation: EffectValuesUser = ( - specific_share_to_other_effects_operation or {} - ) - self.specific_share_to_other_effects_invest: EffectValuesUser = specific_share_to_other_effects_invest or {} - self.minimum_operation = minimum_operation - self.maximum_operation = maximum_operation - self.minimum_operation_per_hour = minimum_operation_per_hour - self.maximum_operation_per_hour = maximum_operation_per_hour - self.minimum_invest = minimum_invest - self.maximum_invest = maximum_invest + self.share_from_temporal: TemporalEffectsUser = share_from_temporal if share_from_temporal is not None else {} + self.share_from_periodic: PeriodicEffectsUser = share_from_periodic if share_from_periodic is not None else {} + + # Handle backwards compatibility for deprecated parameters using centralized helper + minimum_temporal = self._handle_deprecated_kwarg( + kwargs, 'minimum_operation', 'minimum_temporal', minimum_temporal + ) + maximum_temporal = self._handle_deprecated_kwarg( + kwargs, 'maximum_operation', 'maximum_temporal', maximum_temporal + ) + minimum_periodic = self._handle_deprecated_kwarg(kwargs, 'minimum_invest', 'minimum_periodic', minimum_periodic) + maximum_periodic = self._handle_deprecated_kwarg(kwargs, 'maximum_invest', 'maximum_periodic', maximum_periodic) + minimum_per_hour = self._handle_deprecated_kwarg( + kwargs, 'minimum_operation_per_hour', 'minimum_per_hour', minimum_per_hour + ) + maximum_per_hour = self._handle_deprecated_kwarg( + kwargs, 'maximum_operation_per_hour', 'maximum_per_hour', maximum_per_hour + ) + + # Validate any remaining unexpected kwargs + self._validate_kwargs(kwargs) + + # Set attributes directly + self.minimum_temporal = minimum_temporal + self.maximum_temporal = maximum_temporal + self.minimum_periodic = minimum_periodic + self.maximum_periodic = maximum_periodic + self.minimum_per_hour = minimum_per_hour + self.maximum_per_hour = maximum_per_hour self.minimum_total = minimum_total self.maximum_total = maximum_total - def transform_data(self, flow_system: FlowSystem): - self.minimum_operation_per_hour = flow_system.create_time_series( - f'{self.label_full}|minimum_operation_per_hour', self.minimum_operation_per_hour + # Backwards compatible properties (deprecated) + @property + def minimum_operation(self): + """DEPRECATED: Use 'minimum_temporal' property instead.""" + warnings.warn( + "Property 'minimum_operation' is deprecated. Use 'minimum_temporal' instead.", + DeprecationWarning, + stacklevel=2, + ) + return self.minimum_temporal + + @minimum_operation.setter + def minimum_operation(self, value): + """DEPRECATED: Use 'minimum_temporal' property instead.""" + warnings.warn( + "Property 'minimum_operation' is deprecated. Use 'minimum_temporal' instead.", + DeprecationWarning, + stacklevel=2, + ) + self.minimum_temporal = value + + @property + def maximum_operation(self): + """DEPRECATED: Use 'maximum_temporal' property instead.""" + warnings.warn( + "Property 'maximum_operation' is deprecated. Use 'maximum_temporal' instead.", + DeprecationWarning, + stacklevel=2, + ) + return self.maximum_temporal + + @maximum_operation.setter + def maximum_operation(self, value): + """DEPRECATED: Use 'maximum_temporal' property instead.""" + warnings.warn( + "Property 'maximum_operation' is deprecated. Use 'maximum_temporal' instead.", + DeprecationWarning, + stacklevel=2, + ) + self.maximum_temporal = value + + @property + def minimum_invest(self): + """DEPRECATED: Use 'minimum_periodic' property instead.""" + warnings.warn( + "Property 'minimum_invest' is deprecated. Use 'minimum_periodic' instead.", + DeprecationWarning, + stacklevel=2, ) - self.maximum_operation_per_hour = flow_system.create_time_series( - f'{self.label_full}|maximum_operation_per_hour', - self.maximum_operation_per_hour, + return self.minimum_periodic + + @minimum_invest.setter + def minimum_invest(self, value): + """DEPRECATED: Use 'minimum_periodic' property instead.""" + warnings.warn( + "Property 'minimum_invest' is deprecated. Use 'minimum_periodic' instead.", + DeprecationWarning, + stacklevel=2, ) + self.minimum_periodic = value - self.specific_share_to_other_effects_operation = flow_system.create_effect_time_series( - f'{self.label_full}|operation->', self.specific_share_to_other_effects_operation, 'operation' + @property + def maximum_invest(self): + """DEPRECATED: Use 'maximum_periodic' property instead.""" + warnings.warn( + "Property 'maximum_invest' is deprecated. Use 'maximum_periodic' instead.", + DeprecationWarning, + stacklevel=2, + ) + return self.maximum_periodic + + @maximum_invest.setter + def maximum_invest(self, value): + """DEPRECATED: Use 'maximum_periodic' property instead.""" + warnings.warn( + "Property 'maximum_invest' is deprecated. Use 'maximum_periodic' instead.", + DeprecationWarning, + stacklevel=2, + ) + self.maximum_periodic = value + + @property + def minimum_operation_per_hour(self): + """DEPRECATED: Use 'minimum_per_hour' property instead.""" + warnings.warn( + "Property 'minimum_operation_per_hour' is deprecated. Use 'minimum_per_hour' instead.", + DeprecationWarning, + stacklevel=2, + ) + return self.minimum_per_hour + + @minimum_operation_per_hour.setter + def minimum_operation_per_hour(self, value): + """DEPRECATED: Use 'minimum_per_hour' property instead.""" + warnings.warn( + "Property 'minimum_operation_per_hour' is deprecated. Use 'minimum_per_hour' instead.", + DeprecationWarning, + stacklevel=2, + ) + self.minimum_per_hour = value + + @property + def maximum_operation_per_hour(self): + """DEPRECATED: Use 'maximum_per_hour' property instead.""" + warnings.warn( + "Property 'maximum_operation_per_hour' is deprecated. Use 'maximum_per_hour' instead.", + DeprecationWarning, + stacklevel=2, + ) + return self.maximum_per_hour + + @maximum_operation_per_hour.setter + def maximum_operation_per_hour(self, value): + """DEPRECATED: Use 'maximum_per_hour' property instead.""" + warnings.warn( + "Property 'maximum_operation_per_hour' is deprecated. Use 'maximum_per_hour' instead.", + DeprecationWarning, + stacklevel=2, + ) + self.maximum_per_hour = value + + def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None: + prefix = '|'.join(filter(None, [name_prefix, self.label_full])) + self.minimum_per_hour = flow_system.fit_to_model_coords(f'{prefix}|minimum_per_hour', self.minimum_per_hour) + + self.maximum_per_hour = flow_system.fit_to_model_coords(f'{prefix}|maximum_per_hour', self.maximum_per_hour) + + self.share_from_temporal = flow_system.fit_effects_to_model_coords( + label_prefix=None, + effect_values=self.share_from_temporal, + label_suffix=f'(temporal)->{prefix}(temporal)', + dims=['time', 'period', 'scenario'], + ) + self.share_from_periodic = flow_system.fit_effects_to_model_coords( + label_prefix=None, + effect_values=self.share_from_periodic, + label_suffix=f'(periodic)->{prefix}(periodic)', + dims=['period', 'scenario'], + ) + + self.minimum_temporal = flow_system.fit_to_model_coords( + f'{prefix}|minimum_temporal', self.minimum_temporal, dims=['period', 'scenario'] + ) + self.maximum_temporal = flow_system.fit_to_model_coords( + f'{prefix}|maximum_temporal', self.maximum_temporal, dims=['period', 'scenario'] + ) + self.minimum_periodic = flow_system.fit_to_model_coords( + f'{prefix}|minimum_periodic', self.minimum_periodic, dims=['period', 'scenario'] + ) + self.maximum_periodic = flow_system.fit_to_model_coords( + f'{prefix}|maximum_periodic', self.maximum_periodic, dims=['period', 'scenario'] + ) + self.minimum_total = flow_system.fit_to_model_coords( + f'{prefix}|minimum_total', + self.minimum_total, + dims=['period', 'scenario'], + ) + self.maximum_total = flow_system.fit_to_model_coords( + f'{prefix}|maximum_total', self.maximum_total, dims=['period', 'scenario'] ) - def create_model(self, model: SystemModel) -> EffectModel: + def create_model(self, model: FlowSystemModel) -> EffectModel: self._plausibility_checks() - self.model = EffectModel(model, self) - return self.model + self.submodel = EffectModel(model, self) + return self.submodel def _plausibility_checks(self) -> None: # TODO: Check for plausibility @@ -194,70 +388,64 @@ def _plausibility_checks(self) -> None: class EffectModel(ElementModel): - def __init__(self, model: SystemModel, element: Effect): + element: Effect # Type hint + + def __init__(self, model: FlowSystemModel, element: Effect): super().__init__(model, element) - self.element: Effect = element + + def _do_modeling(self): self.total: linopy.Variable | None = None - self.invest: ShareAllocationModel = self.add( + self.periodic: ShareAllocationModel = self.add_submodels( ShareAllocationModel( - self._model, - False, - self.label_of_element, - 'invest', - label_full=f'{self.label_full}(invest)', - total_max=self.element.maximum_invest, - total_min=self.element.minimum_invest, - ) + model=self._model, + dims=('period', 'scenario'), + label_of_element=self.label_of_element, + label_of_model=f'{self.label_of_model}(periodic)', + total_max=self.element.maximum_periodic, + total_min=self.element.minimum_periodic, + ), + short_name='periodic', ) - self.operation: ShareAllocationModel = self.add( + self.temporal: ShareAllocationModel = self.add_submodels( ShareAllocationModel( - self._model, - True, - self.label_of_element, - 'operation', - label_full=f'{self.label_full}(operation)', - total_max=self.element.maximum_operation, - total_min=self.element.minimum_operation, - min_per_hour=self.element.minimum_operation_per_hour.active_data - if self.element.minimum_operation_per_hour is not None - else None, - max_per_hour=self.element.maximum_operation_per_hour.active_data - if self.element.maximum_operation_per_hour is not None - else None, - ) + model=self._model, + dims=('time', 'period', 'scenario'), + label_of_element=self.label_of_element, + label_of_model=f'{self.label_of_model}(temporal)', + total_max=self.element.maximum_temporal, + total_min=self.element.minimum_temporal, + min_per_hour=self.element.minimum_per_hour if self.element.minimum_per_hour is not None else None, + max_per_hour=self.element.maximum_per_hour if self.element.maximum_per_hour is not None else None, + ), + short_name='temporal', ) - def do_modeling(self): - for model in self.sub_models: - model.do_modeling() - - self.total = self.add( - self._model.add_variables( - lower=self.element.minimum_total if self.element.minimum_total is not None else -np.inf, - upper=self.element.maximum_total if self.element.maximum_total is not None else np.inf, - coords=None, - name=f'{self.label_full}|total', - ), - 'total', + self.total = self.add_variables( + lower=self.element.minimum_total if self.element.minimum_total is not None else -np.inf, + upper=self.element.maximum_total if self.element.maximum_total is not None else np.inf, + coords=self._model.get_coords(['period', 'scenario']), + name=self.label_full, ) - self.add( - self._model.add_constraints( - self.total == self.operation.total.sum() + self.invest.total.sum(), name=f'{self.label_full}|total' - ), - 'total', + self.add_constraints( + self.total == self.temporal.total + self.periodic.total, name=self.label_full, short_name='total' ) -EffectValuesExpr = dict[str, linopy.LinearExpression] # Used to create Shares -EffectTimeSeries = dict[str, TimeSeries] # Used internally to index values -EffectValuesDict = dict[str, NumericDataTS] # How effect values are stored -EffectValuesUser = NumericDataTS | dict[str, NumericDataTS] # User-specified Shares to Effects -""" This datatype is used to define the share to an effect by a certain attribute. """ +TemporalEffectsUser = TemporalDataUser | dict[str, TemporalDataUser] # User-specified Shares to Effects +""" This datatype is used to define a temporal share to an effect by a certain attribute. """ + +PeriodicEffectsUser = PeriodicDataUser | dict[str, PeriodicDataUser] # User-specified Shares to Effects +""" This datatype is used to define a scalar share to an effect by a certain attribute. """ + +TemporalEffects = dict[str, TemporalData] # User-specified Shares to Effects +""" This datatype is used internally to handle temporal shares to an effect. """ + +PeriodicEffects = dict[str, Scalar] +""" This datatype is used internally to handle scalar shares to an effect. """ -EffectValuesUserScalar = Scalar | dict[str, Scalar] # User-specified Shares to Effects -""" This datatype is used to define the share to an effect by a certain attribute. Only scalars are allowed. """ +EffectExpr = dict[str, linopy.LinearExpression] # Used to create Shares class EffectCollection: @@ -270,13 +458,13 @@ def __init__(self, *effects: Effect): self._standard_effect: Effect | None = None self._objective_effect: Effect | None = None - self.model: EffectCollectionModel | None = None + self.submodel: EffectCollectionModel | None = None self.add_effects(*effects) - def create_model(self, model: SystemModel) -> EffectCollectionModel: + def create_model(self, model: FlowSystemModel) -> EffectCollectionModel: self._plausibility_checks() - self.model = EffectCollectionModel(model, self) - return self.model + self.submodel = EffectCollectionModel(model, self) + return self.submodel def add_effects(self, *effects: Effect) -> None: for effect in list(effects): @@ -289,20 +477,24 @@ def add_effects(self, *effects: Effect) -> None: self._effects[effect.label] = effect logger.info(f'Registered new Effect: {effect.label}') - def create_effect_values_dict(self, effect_values_user: EffectValuesUser) -> EffectValuesDict | None: + def create_effect_values_dict( + self, effect_values_user: PeriodicEffectsUser | TemporalEffectsUser + ) -> dict[str, Scalar | TemporalDataUser] | None: """ Converts effect values into a dictionary. If a scalar is provided, it is associated with a default effect type. Examples -------- - effect_values_user = 20 -> {None: 20} - effect_values_user = None -> None - effect_values_user = {effect1: 20, effect2: 0.3} -> {effect1: 20, effect2: 0.3} + effect_values_user = 20 -> {'': 20} + effect_values_user = {None: 20} -> {'': 20} + effect_values_user = None -> None + effect_values_user = {'effect1': 20, 'effect2': 0.3} -> {'effect1': 20, 'effect2': 0.3} Returns ------- dict or None - A dictionary with None or Effect as the key, or None if input is None. + A dictionary keyed by effect label, or None if input is None. + Note: a standard effect must be defined when passing scalars or None labels. """ def get_effect_label(eff: Effect | str) -> str: @@ -315,6 +507,8 @@ def get_effect_label(eff: Effect | str) -> str: stacklevel=2, ) return eff.label + elif eff is None: + return self.standard_effect.label else: return eff @@ -326,26 +520,23 @@ def get_effect_label(eff: Effect | str) -> str: def _plausibility_checks(self) -> None: # Check circular loops in effects: - # TODO: Improve checks!! Only most basic case covered... + temporal, periodic = self.calculate_effect_share_factors() - def error_str(effect_label: str, share_ffect_label: str): - return ( - f' {effect_label} -> has share in: {share_ffect_label}\n' - f' {share_ffect_label} -> has share in: {effect_label}' - ) + # Validate all referenced sources exist + unknown = {src for src, _ in list(temporal.keys()) + list(periodic.keys()) if src not in self.effects} + if unknown: + raise KeyError(f'Unknown effects used in in effect share mappings: {sorted(unknown)}') - for effect in self.effects.values(): - # Effekt darf nicht selber als Share in seinen ShareEffekten auftauchen: - # operation: - for target_effect in effect.specific_share_to_other_effects_operation.keys(): - assert effect not in self[target_effect].specific_share_to_other_effects_operation.keys(), ( - f'Error: circular operation-shares \n{error_str(effect.label, self[target_effect].label)}' - ) - # invest: - for target_effect in effect.specific_share_to_other_effects_invest.keys(): - assert effect not in self[target_effect].specific_share_to_other_effects_invest.keys(), ( - f'Error: circular invest-shares \n{error_str(effect.label, self[target_effect].label)}' - ) + temporal_cycles = detect_cycles(tuples_to_adjacency_list([key for key in temporal])) + periodic_cycles = detect_cycles(tuples_to_adjacency_list([key for key in periodic])) + + if temporal_cycles: + cycle_str = '\n'.join([' -> '.join(cycle) for cycle in temporal_cycles]) + raise ValueError(f'Error: circular temporal-shares detected:\n{cycle_str}') + + if periodic_cycles: + cycle_str = '\n'.join([' -> '.join(cycle) for cycle in periodic_cycles]) + raise ValueError(f'Error: circular periodic-shares detected:\n{cycle_str}') def __getitem__(self, effect: str | Effect | None) -> Effect: """ @@ -378,7 +569,10 @@ def __contains__(self, item: str | Effect) -> bool: if isinstance(item, str): return item in self.effects # Check if the label exists elif isinstance(item, Effect): - return item in self.effects.values() # Check if the object exists + if item.label_full in self.effects: + return True + if item in self.effects.values(): # Check if the object exists + return True return False @property @@ -388,7 +582,10 @@ def effects(self) -> dict[str, Effect]: @property def standard_effect(self) -> Effect: if self._standard_effect is None: - raise KeyError('No standard-effect specified!') + raise KeyError( + 'No standard-effect specified! Either set an effect through is_standard=True ' + 'or pass a mapping when specifying effect values: {effect_label: value}.' + ) return self._standard_effect @standard_effect.setter @@ -409,60 +606,248 @@ def objective_effect(self, value: Effect) -> None: raise ValueError(f'An objective-effect already exists! ({self._objective_effect.label=})') self._objective_effect = value - -class EffectCollectionModel(Model): + def calculate_effect_share_factors( + self, + ) -> tuple[ + dict[tuple[str, str], xr.DataArray], + dict[tuple[str, str], xr.DataArray], + ]: + shares_periodic = {} + for name, effect in self.effects.items(): + if effect.share_from_periodic: + for source, data in effect.share_from_periodic.items(): + if source not in shares_periodic: + shares_periodic[source] = {} + shares_periodic[source][name] = data + shares_periodic = calculate_all_conversion_paths(shares_periodic) + + shares_temporal = {} + for name, effect in self.effects.items(): + if effect.share_from_temporal: + for source, data in effect.share_from_temporal.items(): + if source not in shares_temporal: + shares_temporal[source] = {} + shares_temporal[source][name] = data + shares_temporal = calculate_all_conversion_paths(shares_temporal) + + return shares_temporal, shares_periodic + + +class EffectCollectionModel(Submodel): """ Handling all Effects """ - def __init__(self, model: SystemModel, effects: EffectCollection): - super().__init__(model, label_of_element='Effects') + def __init__(self, model: FlowSystemModel, effects: EffectCollection): self.effects = effects self.penalty: ShareAllocationModel | None = None + super().__init__(model, label_of_element='Effects') def add_share_to_effects( self, name: str, - expressions: EffectValuesExpr, - target: Literal['operation', 'invest'], + expressions: EffectExpr, + target: Literal['temporal', 'periodic'], ) -> None: for effect, expression in expressions.items(): - if target == 'operation': - self.effects[effect].model.operation.add_share(name, expression) - elif target == 'invest': - self.effects[effect].model.invest.add_share(name, expression) + if target == 'temporal': + self.effects[effect].submodel.temporal.add_share( + name, + expression, + dims=('time', 'period', 'scenario'), + ) + elif target == 'periodic': + self.effects[effect].submodel.periodic.add_share( + name, + expression, + dims=('period', 'scenario'), + ) else: raise ValueError(f'Target {target} not supported!') def add_share_to_penalty(self, name: str, expression: linopy.LinearExpression) -> None: if expression.ndim != 0: raise TypeError(f'Penalty shares must be scalar expressions! ({expression.ndim=})') - self.penalty.add_share(name, expression) + self.penalty.add_share(name, expression, dims=()) - def do_modeling(self): + def _do_modeling(self): + super()._do_modeling() for effect in self.effects: effect.create_model(self._model) - self.penalty = self.add( - ShareAllocationModel(self._model, shares_are_time_series=False, label_of_element='Penalty') + self.penalty = self.add_submodels( + ShareAllocationModel(self._model, dims=(), label_of_element='Penalty'), + short_name='penalty', ) - for model in [effect.model for effect in self.effects] + [self.penalty]: - model.do_modeling() self._add_share_between_effects() - self._model.add_objective(self.effects.objective_effect.model.total + self.penalty.total) + self._model.add_objective( + (self.effects.objective_effect.submodel.total * self._model.weights).sum() + self.penalty.total.sum() + ) def _add_share_between_effects(self): - for origin_effect in self.effects: - # 1. operation: -> hier sind es Zeitreihen (share_TS) - for target_effect, time_series in origin_effect.specific_share_to_other_effects_operation.items(): - self.effects[target_effect].model.operation.add_share( - origin_effect.model.operation.label_full, - origin_effect.model.operation.total_per_timestep * time_series.active_data, + for target_effect in self.effects: + # 1. temporal: <- receiving temporal shares from other effects + for source_effect, time_series in target_effect.share_from_temporal.items(): + target_effect.submodel.temporal.add_share( + self.effects[source_effect].submodel.temporal.label_full, + self.effects[source_effect].submodel.temporal.total_per_timestep * time_series, + dims=('time', 'period', 'scenario'), ) - # 2. invest: -> hier ist es Scalar (share) - for target_effect, factor in origin_effect.specific_share_to_other_effects_invest.items(): - self.effects[target_effect].model.invest.add_share( - origin_effect.model.invest.label_full, - origin_effect.model.invest.total * factor, + # 2. periodic: <- receiving periodic shares from other effects + for source_effect, factor in target_effect.share_from_periodic.items(): + target_effect.submodel.periodic.add_share( + self.effects[source_effect].submodel.periodic.label_full, + self.effects[source_effect].submodel.periodic.total * factor, + dims=('period', 'scenario'), ) + + +def calculate_all_conversion_paths( + conversion_dict: dict[str, dict[str, Scalar | xr.DataArray]], +) -> dict[tuple[str, str], xr.DataArray]: + """ + Calculates all possible direct and indirect conversion factors between units/domains. + This function uses Breadth-First Search (BFS) to find all possible conversion paths + between different units or domains in a conversion graph. It computes both direct + conversions (explicitly provided in the input) and indirect conversions (derived + through intermediate units). + Args: + conversion_dict: A nested dictionary where: + - Outer keys represent origin units/domains + - Inner dictionaries map target units/domains to their conversion factors + - Conversion factors can be integers, floats, or numpy arrays + Returns: + A dictionary mapping (origin, target) tuples to their respective conversion factors. + Each key is a tuple of strings representing the origin and target units/domains. + Each value is the conversion factor (int, float, or numpy array) from origin to target. + """ + # Initialize the result dictionary to accumulate all paths + result = {} + + # Add direct connections to the result first + for origin, targets in conversion_dict.items(): + for target, factor in targets.items(): + result[(origin, target)] = factor + + # Track all paths by keeping path history to avoid cycles + # Iterate over each domain in the dictionary + for origin in conversion_dict: + # Keep track of visited paths to avoid repeating calculations + processed_paths = set() + # Use a queue with (current_domain, factor, path_history) + queue = deque([(origin, 1, [origin])]) + + while queue: + current_domain, factor, path = queue.popleft() + + # Skip if we've processed this exact path before + path_key = tuple(path) + if path_key in processed_paths: + continue + processed_paths.add(path_key) + + # Iterate over the neighbors of the current domain + for target, conversion_factor in conversion_dict.get(current_domain, {}).items(): + # Skip if target would create a cycle + if target in path: + continue + + # Calculate the indirect conversion factor + indirect_factor = factor * conversion_factor + new_path = path + [target] + + # Only consider paths starting at origin and ending at some target + if len(new_path) > 2 and new_path[0] == origin: + # Update the result dictionary - accumulate factors from different paths + if (origin, target) in result: + result[(origin, target)] = result[(origin, target)] + indirect_factor + else: + result[(origin, target)] = indirect_factor + + # Add new path to queue for further exploration + queue.append((target, indirect_factor, new_path)) + + # Convert all values to DataArrays + result = {key: value if isinstance(value, xr.DataArray) else xr.DataArray(value) for key, value in result.items()} + + return result + + +def detect_cycles(graph: dict[str, list[str]]) -> list[list[str]]: + """ + Detects cycles in a directed graph using DFS. + + Args: + graph: Adjacency list representation of the graph + + Returns: + List of cycles found, where each cycle is a list of nodes + """ + # Track nodes in current recursion stack + visiting = set() + # Track nodes that have been fully explored + visited = set() + # Store all found cycles + cycles = [] + + def dfs_find_cycles(node, path=None): + if path is None: + path = [] + + # Current path to this node + current_path = path + [node] + # Add node to current recursion stack + visiting.add(node) + + # Check all neighbors + for neighbor in graph.get(node, []): + # If neighbor is in current path, we found a cycle + if neighbor in visiting: + # Get the cycle by extracting the relevant portion of the path + cycle_start = current_path.index(neighbor) + cycle = current_path[cycle_start:] + [neighbor] + cycles.append(cycle) + # If neighbor hasn't been fully explored, check it + elif neighbor not in visited: + dfs_find_cycles(neighbor, current_path) + + # Remove node from current path and mark as fully explored + visiting.remove(node) + visited.add(node) + + # Check each unvisited node + for node in graph: + if node not in visited: + dfs_find_cycles(node) + + return cycles + + +def tuples_to_adjacency_list(edges: list[tuple[str, str]]) -> dict[str, list[str]]: + """ + Converts a list of edge tuples (source, target) to an adjacency list representation. + + Args: + edges: List of (source, target) tuples representing directed edges + + Returns: + Dictionary mapping each source node to a list of its target nodes + """ + graph = {} + + for source, target in edges: + if source not in graph: + graph[source] = [] + graph[source].append(target) + + # Ensure target nodes with no outgoing edges are in the graph + if target not in graph: + graph[target] = [] + + return graph + + +# Backward compatibility aliases +NonTemporalEffectsUser = PeriodicEffectsUser +NonTemporalEffects = PeriodicEffects diff --git a/flixopt/elements.py b/flixopt/elements.py index 21783808c..25e399811 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -9,17 +9,19 @@ from typing import TYPE_CHECKING import numpy as np +import xarray as xr from .config import CONFIG -from .core import NumericData, NumericDataTS, PlausibilityError, Scalar -from .features import InvestmentModel, OnOffModel, PreventSimultaneousUsageModel +from .core import PlausibilityError, Scalar, TemporalData, TemporalDataUser +from .features import InvestmentModel, OnOffModel from .interface import InvestParameters, OnOffParameters -from .structure import Element, ElementModel, SystemModel, register_class_for_io +from .modeling import BoundingPatterns, ModelingPrimitives, ModelingUtilitiesAbstract +from .structure import Element, ElementModel, FlowSystemModel, register_class_for_io if TYPE_CHECKING: import linopy - from .effects import EffectValuesUser + from .effects import TemporalEffectsUser from .flow_system import FlowSystem logger = logging.getLogger('flixopt') @@ -90,20 +92,18 @@ def __init__( self.flows: dict[str, Flow] = {flow.label: flow for flow in self.inputs + self.outputs} - def create_model(self, model: SystemModel) -> ComponentModel: + def create_model(self, model: FlowSystemModel) -> ComponentModel: self._plausibility_checks() - self.model = ComponentModel(model, self) - return self.model + self.submodel = ComponentModel(model, self) + return self.submodel - def transform_data(self, flow_system: FlowSystem) -> None: + def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None: + prefix = '|'.join(filter(None, [name_prefix, self.label_full])) if self.on_off_parameters is not None: - self.on_off_parameters.transform_data(flow_system, self.label_full) + self.on_off_parameters.transform_data(flow_system, prefix) - def infos(self, use_numpy=True, use_element_label: bool = False) -> dict: - infos = super().infos(use_numpy, use_element_label) - infos['inputs'] = [flow.infos(use_numpy, use_element_label) for flow in self.inputs] - infos['outputs'] = [flow.infos(use_numpy, use_element_label) for flow in self.outputs] - return infos + for flow in self.inputs + self.outputs: + flow.transform_data(flow_system) # Flow doesnt need the name_prefix def _check_unique_flow_labels(self): all_flow_labels = [flow.label for flow in self.inputs + self.outputs] @@ -126,6 +126,10 @@ class Bus(Element): physical or logical connection points for energy carriers (electricity, heat, gas) or material flows between different Components. + Mathematical Formulation: + See the complete mathematical model in the documentation: + [Bus](../user-guide/mathematical-notation/elements/Bus.md) + Args: label: The label of the Element. Used to identify it in the FlowSystem. excess_penalty_per_flow_hour: Penalty costs for bus balance violations. @@ -177,7 +181,7 @@ class Bus(Element): def __init__( self, label: str, - excess_penalty_per_flow_hour: NumericData | NumericDataTS | None = 1e5, + excess_penalty_per_flow_hour: TemporalDataUser | None = 1e5, meta_data: dict | None = None, ): super().__init__(label, meta_data=meta_data) @@ -185,14 +189,15 @@ def __init__( self.inputs: list[Flow] = [] self.outputs: list[Flow] = [] - def create_model(self, model: SystemModel) -> BusModel: + def create_model(self, model: FlowSystemModel) -> BusModel: self._plausibility_checks() - self.model = BusModel(model, self) - return self.model + self.submodel = BusModel(model, self) + return self.submodel - def transform_data(self, flow_system: FlowSystem): - self.excess_penalty_per_flow_hour = flow_system.create_time_series( - f'{self.label_full}|excess_penalty_per_flow_hour', self.excess_penalty_per_flow_hour + def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None: + prefix = '|'.join(filter(None, [name_prefix, self.label_full])) + self.excess_penalty_per_flow_hour = flow_system.fit_to_model_coords( + f'{prefix}|excess_penalty_per_flow_hour', self.excess_penalty_per_flow_hour ) def _plausibility_checks(self) -> None: @@ -200,7 +205,7 @@ def _plausibility_checks(self) -> None: zero_penalty = np.all(np.equal(self.excess_penalty_per_flow_hour, 0)) if zero_penalty: logger.warning( - f'In Bus {self.label}, the excess_penalty_per_flow_hour is 0. Use "None" or a value > 0.' + f'In Bus {self.label_full}, the excess_penalty_per_flow_hour is 0. Use "None" or a value > 0.' ) @property @@ -241,39 +246,27 @@ class Flow(Element): - **InvestParameters**: Used for `size` when flow Size is an investment decision - **OnOffParameters**: Used for `on_off_parameters` when flow has discrete states + Mathematical Formulation: + See the complete mathematical model in the documentation: + [Flow](../user-guide/mathematical-notation/elements/Flow.md) + Args: - label: Unique identifier for the flow within its component. - The full label combines component and flow labels. - bus: Label of the bus this flow connects to. Must match a bus in the FlowSystem. - size: Flow capacity or nominal rating. Can be: - - Scalar value for fixed capacity - - InvestParameters for investment-based sizing decisions - - None to use large default value (CONFIG.Modeling.big) - relative_minimum: Minimum flow rate as fraction of size. - Example: 0.2 means flow cannot go below 20% of rated capacity. - relative_maximum: Maximum flow rate as fraction of size (typically 1.0). - Values >1.0 allow temporary overload operation. - load_factor_min: Minimum average utilization over the time horizon (0-1). - Calculated as total flow hours divided by (size × total time). - load_factor_max: Maximum average utilization over the time horizon (0-1). - Useful for equipment duty cycle limits or maintenance scheduling. - effects_per_flow_hour: Operational costs and impacts per unit of flow-time. - Dictionary mapping effect names to unit costs (e.g., fuel costs, emissions). - on_off_parameters: Binary operation constraints using OnOffParameters. - Enables modeling of startup costs, minimum run times, cycling limits. - Only relevant when relative_minimum > 0 or discrete operation is required. - flow_hours_total_max: Maximum cumulative flow-hours over time horizon. - Alternative to load_factor_max for absolute energy/material limits. - flow_hours_total_min: Minimum cumulative flow-hours over time horizon. - Alternative to load_factor_min for contractual or operational requirements. - fixed_relative_profile: Predetermined flow pattern as fraction of size. - When specified, flow rate becomes: size × fixed_relative_profile(t). - Used for: demand profiles, renewable generation, fixed schedules. - previous_flow_rate: Initial flow state for startup/shutdown dynamics. - Used with on_off_parameters to determine initial on/off status. - If None, assumes flow was off in previous time period. - meta_data: Additional information stored with results but not used in optimization. - Must contain only Python native types (dict, list, str, int, float, bool). + label: Unique flow identifier within its component. + bus: Bus label this flow connects to. + size: Flow capacity. Scalar, InvestParameters, or None (uses CONFIG.Modeling.big). + relative_minimum: Minimum flow rate as fraction of size (0-1). Default: 0. + relative_maximum: Maximum flow rate as fraction of size. Default: 1. + load_factor_min: Minimum average utilization (0-1). Default: 0. + load_factor_max: Maximum average utilization (0-1). Default: 1. + effects_per_flow_hour: Operational costs/impacts per flow-hour. + Dict mapping effect names to values (e.g., {'cost': 45, 'CO2': 0.8}). + on_off_parameters: Binary operation constraints (OnOffParameters). Default: None. + flow_hours_total_max: Maximum cumulative flow-hours. Alternative to load_factor_max. + flow_hours_total_min: Minimum cumulative flow-hours. Alternative to load_factor_min. + fixed_relative_profile: Predetermined pattern as fraction of size. + Flow rate = size × fixed_relative_profile(t). + previous_flow_rate: Initial flow state for on/off dynamics. Default: None (off). + meta_data: Additional info stored in results. Python native types only. Examples: Basic power flow with fixed capacity: @@ -315,7 +308,7 @@ class Flow(Element): effects_per_switch_on={'startup_cost': 100, 'wear': 0.1}, consecutive_on_hours_min=2, # Must run at least 2 hours consecutive_off_hours_min=1, # Must stay off at least 1 hour - switch_on_total_max=200, # Maximum 200 starts per year + switch_on_total_max=200, # Maximum 200 starts per period ), ) ``` @@ -369,17 +362,17 @@ def __init__( self, label: str, bus: str, - size: Scalar | InvestParameters | None = None, - fixed_relative_profile: NumericDataTS | None = None, - relative_minimum: NumericDataTS = 0, - relative_maximum: NumericDataTS = 1, - effects_per_flow_hour: EffectValuesUser | None = None, + size: Scalar | InvestParameters = None, + fixed_relative_profile: TemporalDataUser | None = None, + relative_minimum: TemporalDataUser = 0, + relative_maximum: TemporalDataUser = 1, + effects_per_flow_hour: TemporalEffectsUser | None = None, on_off_parameters: OnOffParameters | None = None, flow_hours_total_max: Scalar | None = None, flow_hours_total_min: Scalar | None = None, load_factor_min: Scalar | None = None, load_factor_max: Scalar | None = None, - previous_flow_rate: NumericData | None = None, + previous_flow_rate: Scalar | list[Scalar] | None = None, meta_data: dict | None = None, ): super().__init__(label, meta_data=meta_data) @@ -396,9 +389,7 @@ def __init__( self.flow_hours_total_min = flow_hours_total_min self.on_off_parameters = on_off_parameters - self.previous_flow_rate = ( - previous_flow_rate if not isinstance(previous_flow_rate, list) else np.array(previous_flow_rate) - ) + self.previous_flow_rate = previous_flow_rate self.component: str = 'UnknownComponent' self.is_input_in_component: bool | None = None @@ -415,68 +406,80 @@ def __init__( self.bus = bus self._bus_object = None - def create_model(self, model: SystemModel) -> FlowModel: + def create_model(self, model: FlowSystemModel) -> FlowModel: self._plausibility_checks() - self.model = FlowModel(model, self) - return self.model - - def transform_data(self, flow_system: FlowSystem): - self.relative_minimum = flow_system.create_time_series( - f'{self.label_full}|relative_minimum', self.relative_minimum + self.submodel = FlowModel(model, self) + return self.submodel + + def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None: + prefix = '|'.join(filter(None, [name_prefix, self.label_full])) + self.relative_minimum = flow_system.fit_to_model_coords(f'{prefix}|relative_minimum', self.relative_minimum) + self.relative_maximum = flow_system.fit_to_model_coords(f'{prefix}|relative_maximum', self.relative_maximum) + self.fixed_relative_profile = flow_system.fit_to_model_coords( + f'{prefix}|fixed_relative_profile', self.fixed_relative_profile + ) + self.effects_per_flow_hour = flow_system.fit_effects_to_model_coords( + prefix, self.effects_per_flow_hour, 'per_flow_hour' + ) + self.flow_hours_total_max = flow_system.fit_to_model_coords( + f'{prefix}|flow_hours_total_max', self.flow_hours_total_max, dims=['period', 'scenario'] ) - self.relative_maximum = flow_system.create_time_series( - f'{self.label_full}|relative_maximum', self.relative_maximum + self.flow_hours_total_min = flow_system.fit_to_model_coords( + f'{prefix}|flow_hours_total_min', self.flow_hours_total_min, dims=['period', 'scenario'] ) - self.fixed_relative_profile = flow_system.create_time_series( - f'{self.label_full}|fixed_relative_profile', self.fixed_relative_profile + self.load_factor_max = flow_system.fit_to_model_coords( + f'{prefix}|load_factor_max', self.load_factor_max, dims=['period', 'scenario'] ) - self.effects_per_flow_hour = flow_system.create_effect_time_series( - self.label_full, self.effects_per_flow_hour, 'per_flow_hour' + self.load_factor_min = flow_system.fit_to_model_coords( + f'{prefix}|load_factor_min', self.load_factor_min, dims=['period', 'scenario'] ) + if self.on_off_parameters is not None: - self.on_off_parameters.transform_data(flow_system, self.label_full) + self.on_off_parameters.transform_data(flow_system, prefix) if isinstance(self.size, InvestParameters): - self.size.transform_data(flow_system) - - def infos(self, use_numpy: bool = True, use_element_label: bool = False) -> dict: - infos = super().infos(use_numpy, use_element_label) - infos['is_input_in_component'] = self.is_input_in_component - return infos - - def to_dict(self) -> dict: - data = super().to_dict() - if isinstance(data.get('previous_flow_rate'), np.ndarray): - data['previous_flow_rate'] = data['previous_flow_rate'].tolist() - return data + self.size.transform_data(flow_system, prefix) + else: + self.size = flow_system.fit_to_model_coords(f'{prefix}|size', self.size, dims=['period', 'scenario']) def _plausibility_checks(self) -> None: # TODO: Incorporate into Variable? (Lower_bound can not be greater than upper bound - if np.any(self.relative_minimum > self.relative_maximum): + if (self.relative_minimum > self.relative_maximum).any(): raise PlausibilityError(self.label_full + ': Take care, that relative_minimum <= relative_maximum!') - if ( - self.size == CONFIG.Modeling.big and self.fixed_relative_profile is not None + if not isinstance(self.size, InvestParameters) and ( + np.any(self.size == CONFIG.Modeling.big) and self.fixed_relative_profile is not None ): # Default Size --> Most likely by accident logger.warning( - f'Flow "{self.label}" has no size assigned, but a "fixed_relative_profile". ' + f'Flow "{self.label_full}" has no size assigned, but a "fixed_relative_profile". ' f'The default size is {CONFIG.Modeling.big}. As "flow_rate = size * fixed_relative_profile", ' f'the resulting flow_rate will be very high. To fix this, assign a size to the Flow {self}.' ) if self.fixed_relative_profile is not None and self.on_off_parameters is not None: - raise ValueError( - f'Flow {self.label} has both a fixed_relative_profile and an on_off_parameters. This is not supported. ' - f'Use relative_minimum and relative_maximum instead, ' - f'if you want to allow flows to be switched on and off.' + logger.warning( + f'Flow {self.label_full} has both a fixed_relative_profile and an on_off_parameters.' + f'This will allow the flow to be switched on and off, effectively differing from the fixed_flow_rate.' ) - if (self.relative_minimum > 0).any() and self.on_off_parameters is None: + if np.any(self.relative_minimum > 0) and self.on_off_parameters is None: logger.warning( - f'Flow {self.label} has a relative_minimum of {self.relative_minimum.active_data} and no on_off_parameters. ' + f'Flow {self.label_full} has a relative_minimum of {self.relative_minimum} and no on_off_parameters. ' f'This prevents the flow_rate from switching off (flow_rate = 0). ' f'Consider using on_off_parameters to allow the flow to be switched on and off.' ) + if self.previous_flow_rate is not None: + if not any( + [ + isinstance(self.previous_flow_rate, np.ndarray) and self.previous_flow_rate.ndim == 1, + isinstance(self.previous_flow_rate, (int, float, list)), + ] + ): + raise TypeError( + f'previous_flow_rate must be None, a scalar, a list of scalars or a 1D-numpy-array. Got {type(self.previous_flow_rate)}. ' + f'Different values in different periods or scenarios are not yet supported.' + ) + @property def label_full(self) -> str: return f'{self.component}({self.label})' @@ -486,237 +489,294 @@ def size_is_fixed(self) -> bool: # Wenn kein InvestParameters existiert --> True; Wenn Investparameter, den Wert davon nehmen return False if (isinstance(self.size, InvestParameters) and self.size.fixed_size is None) else True - @property - def invest_is_optional(self) -> bool: - # Wenn kein InvestParameters existiert: # Investment ist nicht optional -> Keine Variable --> False - return False if (isinstance(self.size, InvestParameters) and not self.size.optional) else True - class FlowModel(ElementModel): - def __init__(self, model: SystemModel, element: Flow): + element: Flow # Type hint + + def __init__(self, model: FlowSystemModel, element: Flow): super().__init__(model, element) - self.element: Flow = element - self.flow_rate: linopy.Variable | None = None - self.total_flow_hours: linopy.Variable | None = None - self.on_off: OnOffModel | None = None - self._investment: InvestmentModel | None = None - - def do_modeling(self): - # eq relative_minimum(t) * size <= flow_rate(t) <= relative_maximum(t) * size - self.flow_rate: linopy.Variable = self.add( - self._model.add_variables( - lower=self.flow_rate_lower_bound, - upper=self.flow_rate_upper_bound, - coords=self._model.coords, - name=f'{self.label_full}|flow_rate', + def _do_modeling(self): + super()._do_modeling() + # Main flow rate variable + self.add_variables( + lower=self.absolute_flow_rate_bounds[0], + upper=self.absolute_flow_rate_bounds[1], + coords=self._model.get_coords(), + short_name='flow_rate', + ) + + self._constraint_flow_rate() + + # Total flow hours tracking + ModelingPrimitives.expression_tracking_variable( + model=self, + name=f'{self.label_full}|total_flow_hours', + tracked_expression=(self.flow_rate * self._model.hours_per_step).sum('time'), + bounds=( + self.element.flow_hours_total_min if self.element.flow_hours_total_min is not None else 0, + self.element.flow_hours_total_max if self.element.flow_hours_total_max is not None else None, ), - 'flow_rate', + coords=['period', 'scenario'], + short_name='total_flow_hours', ) - # OnOff - if self.element.on_off_parameters is not None: - self.on_off: OnOffModel = self.add( - OnOffModel( - model=self._model, - label_of_element=self.label_of_element, - on_off_parameters=self.element.on_off_parameters, - defining_variables=[self.flow_rate], - defining_bounds=[self.flow_rate_bounds_on], - previous_values=[self.element.previous_flow_rate], - ), - 'on_off', - ) - self.on_off.do_modeling() + # Load factor constraints + self._create_bounds_for_load_factor() - # Investment - if isinstance(self.element.size, InvestParameters): - self._investment: InvestmentModel = self.add( - InvestmentModel( - model=self._model, - label_of_element=self.label_of_element, - parameters=self.element.size, - defining_variable=self.flow_rate, - relative_bounds_of_defining_variable=( - self.flow_rate_lower_bound_relative, - self.flow_rate_upper_bound_relative, - ), - on_variable=self.on_off.on if self.on_off is not None else None, - ), - 'investment', - ) - self._investment.do_modeling() - - self.total_flow_hours = self.add( - self._model.add_variables( - lower=self.element.flow_hours_total_min if self.element.flow_hours_total_min is not None else 0, - upper=self.element.flow_hours_total_max if self.element.flow_hours_total_max is not None else np.inf, - coords=None, - name=f'{self.label_full}|total_flow_hours', + # Effects + self._create_shares() + + def _create_on_off_model(self): + on = self.add_variables(binary=True, short_name='on', coords=self._model.get_coords()) + self.add_submodels( + OnOffModel( + model=self._model, + label_of_element=self.label_of_element, + parameters=self.element.on_off_parameters, + on_variable=on, + previous_states=self.previous_states, + label_of_model=self.label_of_element, ), - 'total_flow_hours', + short_name='on_off', ) - self.add( - self._model.add_constraints( - self.total_flow_hours == (self.flow_rate * self._model.hours_per_step).sum(), - name=f'{self.label_full}|total_flow_hours', + def _create_investment_model(self): + self.add_submodels( + InvestmentModel( + model=self._model, + label_of_element=self.label_of_element, + parameters=self.element.size, + label_of_model=self.label_of_element, ), - 'total_flow_hours', + 'investment', ) - # Load factor - self._create_bounds_for_load_factor() + def _constraint_flow_rate(self): + if not self.with_investment and not self.with_on_off: + # Most basic case. Already covered by direct variable bounds + pass + + elif self.with_on_off and not self.with_investment: + # OnOff, but no Investment + self._create_on_off_model() + bounds = self.relative_flow_rate_bounds + BoundingPatterns.bounds_with_state( + self, + variable=self.flow_rate, + bounds=(bounds[0] * self.element.size, bounds[1] * self.element.size), + variable_state=self.on_off.on, + ) - # Shares - self._create_shares() + elif self.with_investment and not self.with_on_off: + # Investment, but no OnOff + self._create_investment_model() + BoundingPatterns.scaled_bounds( + self, + variable=self.flow_rate, + scaling_variable=self.investment.size, + relative_bounds=self.relative_flow_rate_bounds, + ) + + elif self.with_investment and self.with_on_off: + # Investment and OnOff + self._create_investment_model() + self._create_on_off_model() + + BoundingPatterns.scaled_bounds_with_state( + model=self, + variable=self.flow_rate, + scaling_variable=self._investment.size, + relative_bounds=self.relative_flow_rate_bounds, + scaling_bounds=(self.element.size.minimum_or_fixed_size, self.element.size.maximum_or_fixed_size), + variable_state=self.on_off.on, + ) + else: + raise Exception('Not valid') + + @property + def with_on_off(self) -> bool: + return self.element.on_off_parameters is not None + + @property + def with_investment(self) -> bool: + return isinstance(self.element.size, InvestParameters) + + # Properties for clean access to variables + @property + def flow_rate(self) -> linopy.Variable: + """Main flow rate variable""" + return self['flow_rate'] + + @property + def total_flow_hours(self) -> linopy.Variable: + """Total flow hours variable""" + return self['total_flow_hours'] + + def results_structure(self): + return { + **super().results_structure(), + 'start': self.element.bus if self.element.is_input_in_component else self.element.component, + 'end': self.element.component if self.element.is_input_in_component else self.element.bus, + 'component': self.element.component, + } def _create_shares(self): - # Arbeitskosten: - if self.element.effects_per_flow_hour != {}: + # Effects per flow hour + if self.element.effects_per_flow_hour: self._model.effects.add_share_to_effects( - name=self.label_full, # Use the full label of the element + name=self.label_full, expressions={ - effect: self.flow_rate * self._model.hours_per_step * factor.active_data + effect: self.flow_rate * self._model.hours_per_step * factor for effect, factor in self.element.effects_per_flow_hour.items() }, - target='operation', + target='temporal', ) def _create_bounds_for_load_factor(self): - # TODO: Add Variable load_factor for better evaluation? + """Create load factor constraints using current approach""" + # Get the size (either from element or investment) + size = self.investment.size if self.with_investment else self.element.size - # eq: var_sumFlowHours <= size * dt_tot * load_factor_max + # Maximum load factor constraint if self.element.load_factor_max is not None: - name_short = 'load_factor_max' - flow_hours_per_size_max = self._model.hours_per_step.sum() * self.element.load_factor_max - size = self.element.size if self._investment is None else self._investment.size - - self.add( - self._model.add_constraints( - self.total_flow_hours <= size * flow_hours_per_size_max, - name=f'{self.label_full}|{name_short}', - ), - name_short, + flow_hours_per_size_max = self._model.hours_per_step.sum('time') * self.element.load_factor_max + self.add_constraints( + self.total_flow_hours <= size * flow_hours_per_size_max, + short_name='load_factor_max', ) - # eq: size * sum(dt)* load_factor_min <= var_sumFlowHours + # Minimum load factor constraint if self.element.load_factor_min is not None: - name_short = 'load_factor_min' - flow_hours_per_size_min = self._model.hours_per_step.sum() * self.element.load_factor_min - size = self.element.size if self._investment is None else self._investment.size - - self.add( - self._model.add_constraints( - self.total_flow_hours >= size * flow_hours_per_size_min, - name=f'{self.label_full}|{name_short}', - ), - name_short, + flow_hours_per_size_min = self._model.hours_per_step.sum('time') * self.element.load_factor_min + self.add_constraints( + self.total_flow_hours >= size * flow_hours_per_size_min, + short_name='load_factor_min', ) @property - def flow_rate_bounds_on(self) -> tuple[NumericData, NumericData]: - """Returns absolute flow rate bounds. Important for OnOffModel""" - relative_minimum, relative_maximum = self.flow_rate_lower_bound_relative, self.flow_rate_upper_bound_relative - size = self.element.size - if not isinstance(size, InvestParameters): - return relative_minimum * size, relative_maximum * size - if size.fixed_size is not None: - return relative_minimum * size.fixed_size, relative_maximum * size.fixed_size - return relative_minimum * size.minimum_size, relative_maximum * size.maximum_size + def relative_flow_rate_bounds(self) -> tuple[TemporalData, TemporalData]: + if self.element.fixed_relative_profile is not None: + return self.element.fixed_relative_profile, self.element.fixed_relative_profile + return self.element.relative_minimum, self.element.relative_maximum @property - def flow_rate_lower_bound_relative(self) -> NumericData: - """Returns the lower bound of the flow_rate relative to its size""" - fixed_profile = self.element.fixed_relative_profile - if fixed_profile is None: - return self.element.relative_minimum.active_data - return fixed_profile.active_data + def absolute_flow_rate_bounds(self) -> tuple[TemporalData, TemporalData]: + """ + Returns the absolute bounds the flow_rate can reach. + Further constraining might be needed + """ + lb_relative, ub_relative = self.relative_flow_rate_bounds + + lb = 0 + if not self.with_on_off: + if not self.with_investment: + # Basic case without investment and without OnOff + lb = lb_relative * self.element.size + elif self.with_investment and self.element.size.mandatory: + # With mandatory Investment + lb = lb_relative * self.element.size.minimum_or_fixed_size + + if self.with_investment: + ub = ub_relative * self.element.size.maximum_or_fixed_size + else: + ub = ub_relative * self.element.size + + return lb, ub @property - def flow_rate_upper_bound_relative(self) -> NumericData: - """Returns the upper bound of the flow_rate relative to its size""" - fixed_profile = self.element.fixed_relative_profile - if fixed_profile is None: - return self.element.relative_maximum.active_data - return fixed_profile.active_data + def on_off(self) -> OnOffModel | None: + """OnOff feature""" + if 'on_off' not in self.submodels: + return None + return self.submodels['on_off'] @property - def flow_rate_lower_bound(self) -> NumericData: - """ - Returns the minimum bound the flow_rate can reach. - Further constraining might be done in OnOffModel and InvestmentModel - """ - if self.element.on_off_parameters is not None: - return 0 - if isinstance(self.element.size, InvestParameters): - if self.element.size.optional: - return 0 - return self.flow_rate_lower_bound_relative * self.element.size.minimum_size - return self.flow_rate_lower_bound_relative * self.element.size + def _investment(self) -> InvestmentModel | None: + """Deprecated alias for investment""" + return self.investment @property - def flow_rate_upper_bound(self) -> NumericData: - """ - Returns the maximum bound the flow_rate can reach. - Further constraining might be done in OnOffModel and InvestmentModel - """ - if isinstance(self.element.size, InvestParameters): - return self.flow_rate_upper_bound_relative * self.element.size.maximum_size - return self.flow_rate_upper_bound_relative * self.element.size + def investment(self) -> InvestmentModel | None: + """OnOff feature""" + if 'investment' not in self.submodels: + return None + return self.submodels['investment'] + + @property + def previous_states(self) -> TemporalData | None: + """Previous states of the flow rate""" + # TODO: This would be nicer to handle in the Flow itself, and allow DataArrays as well. + previous_flow_rate = self.element.previous_flow_rate + if previous_flow_rate is None: + return None + + return ModelingUtilitiesAbstract.to_binary( + values=xr.DataArray( + [previous_flow_rate] if np.isscalar(previous_flow_rate) else previous_flow_rate, dims='time' + ), + epsilon=CONFIG.Modeling.epsilon, + dims='time', + ) class BusModel(ElementModel): - def __init__(self, model: SystemModel, element: Bus): - super().__init__(model, element) - self.element: Bus = element + element: Bus # Type hint + + def __init__(self, model: FlowSystemModel, element: Bus): self.excess_input: linopy.Variable | None = None self.excess_output: linopy.Variable | None = None + super().__init__(model, element) - def do_modeling(self) -> None: + def _do_modeling(self) -> None: + super()._do_modeling() # inputs == outputs for flow in self.element.inputs + self.element.outputs: - self.add(flow.model.flow_rate, flow.label_full) - inputs = sum([flow.model.flow_rate for flow in self.element.inputs]) - outputs = sum([flow.model.flow_rate for flow in self.element.outputs]) - eq_bus_balance = self.add(self._model.add_constraints(inputs == outputs, name=f'{self.label_full}|balance')) + self.register_variable(flow.submodel.flow_rate, flow.label_full) + inputs = sum([flow.submodel.flow_rate for flow in self.element.inputs]) + outputs = sum([flow.submodel.flow_rate for flow in self.element.outputs]) + eq_bus_balance = self.add_constraints(inputs == outputs, short_name='balance') # Fehlerplus/-minus: if self.element.with_excess: - excess_penalty = np.multiply( - self._model.hours_per_step, self.element.excess_penalty_per_flow_hour.active_data - ) - self.excess_input = self.add( - self._model.add_variables(lower=0, coords=self._model.coords, name=f'{self.label_full}|excess_input'), - 'excess_input', - ) - self.excess_output = self.add( - self._model.add_variables(lower=0, coords=self._model.coords, name=f'{self.label_full}|excess_output'), - 'excess_output', + excess_penalty = np.multiply(self._model.hours_per_step, self.element.excess_penalty_per_flow_hour) + + self.excess_input = self.add_variables(lower=0, coords=self._model.get_coords(), short_name='excess_input') + + self.excess_output = self.add_variables( + lower=0, coords=self._model.get_coords(), short_name='excess_output' ) + eq_bus_balance.lhs -= -self.excess_input + self.excess_output self._model.effects.add_share_to_penalty(self.label_of_element, (self.excess_input * excess_penalty).sum()) self._model.effects.add_share_to_penalty(self.label_of_element, (self.excess_output * excess_penalty).sum()) def results_structure(self): - inputs = [flow.model.flow_rate.name for flow in self.element.inputs] - outputs = [flow.model.flow_rate.name for flow in self.element.outputs] + inputs = [flow.submodel.flow_rate.name for flow in self.element.inputs] + outputs = [flow.submodel.flow_rate.name for flow in self.element.outputs] if self.excess_input is not None: inputs.append(self.excess_input.name) if self.excess_output is not None: outputs.append(self.excess_output.name) - return {**super().results_structure(), 'inputs': inputs, 'outputs': outputs} + return { + **super().results_structure(), + 'inputs': inputs, + 'outputs': outputs, + 'flows': [flow.label_full for flow in self.element.inputs + self.element.outputs], + } class ComponentModel(ElementModel): - def __init__(self, model: SystemModel, element: Component): - super().__init__(model, element) - self.element: Component = element + element: Component # Type hint + + def __init__(self, model: FlowSystemModel, element: Component): self.on_off: OnOffModel | None = None + super().__init__(model, element) - def do_modeling(self): + def _do_modeling(self): """Initiates all FlowModels""" + super()._do_modeling() all_flows = self.element.inputs + self.element.outputs if self.element.on_off_parameters: for flow in all_flows: @@ -729,34 +789,64 @@ def do_modeling(self): flow.on_off_parameters = OnOffParameters() for flow in all_flows: - self.add(flow.create_model(self._model), flow.label) - - for sub_model in self.sub_models: - sub_model.do_modeling() + self.add_submodels(flow.create_model(self._model), short_name=flow.label) if self.element.on_off_parameters: - self.on_off = self.add( - OnOffModel( - self._model, - self.element.on_off_parameters, - self.label_of_element, - defining_variables=[flow.model.flow_rate for flow in all_flows], - defining_bounds=[flow.model.flow_rate_bounds_on for flow in all_flows], - previous_values=[flow.previous_flow_rate for flow in all_flows], + on = self.add_variables(binary=True, short_name='on', coords=self._model.get_coords()) + if len(all_flows) == 1: + self.add_constraints(on == all_flows[0].submodel.on_off.on, short_name='on') + else: + flow_ons = [flow.submodel.on_off.on for flow in all_flows] + # TODO: Is the EPSILON even necessary? + self.add_constraints(on <= sum(flow_ons) + CONFIG.Modeling.epsilon, short_name='on|ub') + self.add_constraints( + on >= sum(flow_ons) / (len(flow_ons) + CONFIG.Modeling.epsilon), short_name='on|lb' ) - ) - self.on_off.do_modeling() + self.on_off = self.add_submodels( + OnOffModel( + model=self._model, + label_of_element=self.label_of_element, + parameters=self.element.on_off_parameters, + on_variable=on, + label_of_model=self.label_of_element, + previous_states=self.previous_states, + ), + short_name='on_off', + ) if self.element.prevent_simultaneous_flows: # Simultanious Useage --> Only One FLow is On at a time, but needs a Binary for every flow - on_variables = [flow.model.on_off.on for flow in self.element.prevent_simultaneous_flows] - simultaneous_use = self.add(PreventSimultaneousUsageModel(self._model, on_variables, self.label_full)) - simultaneous_use.do_modeling() + ModelingPrimitives.mutual_exclusivity_constraint( + self, + binary_variables=[flow.submodel.on_off.on for flow in self.element.prevent_simultaneous_flows], + short_name='prevent_simultaneous_use', + ) def results_structure(self): return { **super().results_structure(), - 'inputs': [flow.model.flow_rate.name for flow in self.element.inputs], - 'outputs': [flow.model.flow_rate.name for flow in self.element.outputs], + 'inputs': [flow.submodel.flow_rate.name for flow in self.element.inputs], + 'outputs': [flow.submodel.flow_rate.name for flow in self.element.outputs], + 'flows': [flow.label_full for flow in self.element.inputs + self.element.outputs], } + + @property + def previous_states(self) -> xr.DataArray | None: + """Previous state of the component, derived from its flows""" + if self.element.on_off_parameters is None: + raise ValueError(f'OnOffModel not present in \n{self}\nCant access previous_states') + + previous_states = [flow.submodel.on_off._previous_states for flow in self.element.inputs + self.element.outputs] + previous_states = [da for da in previous_states if da is not None] + + if not previous_states: # Empty list + return None + + max_len = max(da.sizes['time'] for da in previous_states) + + padded_previous_states = [ + da.assign_coords(time=range(-da.sizes['time'], 0)).reindex(time=range(-max_len, 0), fill_value=0) + for da in previous_states + ] + return xr.concat(padded_previous_states, dim='flow').any(dim='flow').astype(int) diff --git a/flixopt/features.py b/flixopt/features.py index 7aafe242d..0d1fc7784 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -11,837 +11,366 @@ import linopy import numpy as np -from .config import CONFIG -from .core import NumericData, Scalar, TimeSeries -from .structure import Model, SystemModel +from .modeling import BoundingPatterns, ModelingPrimitives, ModelingUtilities +from .structure import FlowSystemModel, Submodel if TYPE_CHECKING: + from .core import FlowSystemDimensions, Scalar, TemporalData from .interface import InvestParameters, OnOffParameters, Piecewise logger = logging.getLogger('flixopt') -class InvestmentModel(Model): - """Class for modeling an investment""" +class InvestmentModel(Submodel): + """ + This feature model is used to model the investment of a variable. + It applies the corresponding bounds to the variable and the on/off state of the variable. + + Args: + model: The optimization model instance + label_of_element: The label of the parent (Element). Used to construct the full label of the model. + parameters: The parameters of the feature model. + label_of_model: The label of the model. This is needed to construct the full label of the model. + + """ + + parameters: InvestParameters def __init__( self, - model: SystemModel, + model: FlowSystemModel, label_of_element: str, parameters: InvestParameters, - defining_variable: linopy.Variable, - relative_bounds_of_defining_variable: tuple[NumericData, NumericData], - label: str | None = None, - on_variable: linopy.Variable | None = None, + label_of_model: str | None = None, ): - super().__init__(model, label_of_element, label) - self.size: Scalar | linopy.Variable | None = None - self.is_invested: linopy.Variable | None = None - self.piecewise_effects: PiecewiseEffectsModel | None = None - - self._on_variable = on_variable - self._defining_variable = defining_variable - self._relative_bounds_of_defining_variable = relative_bounds_of_defining_variable self.parameters = parameters + super().__init__(model, label_of_element=label_of_element, label_of_model=label_of_model) + + def _do_modeling(self): + super()._do_modeling() + self._create_variables_and_constraints() + self._add_effects() + + def _create_variables_and_constraints(self): + size_min, size_max = (self.parameters.minimum_or_fixed_size, self.parameters.maximum_or_fixed_size) + if self.parameters.linked_periods is not None: + # Mask size bounds: linked_periods is a binary DataArray that zeros out non-linked periods + size_min = size_min * self.parameters.linked_periods + size_max = size_max * self.parameters.linked_periods + + self.add_variables( + short_name='size', + lower=size_min if self.parameters.mandatory else 0, + upper=size_max, + coords=self._model.get_coords(['period', 'scenario']), + ) - def do_modeling(self): - if self.parameters.fixed_size and not self.parameters.optional: - self.size = self.add( - self._model.add_variables( - lower=self.parameters.fixed_size, upper=self.parameters.fixed_size, name=f'{self.label_full}|size' - ), - 'size', + if not self.parameters.mandatory: + self.add_variables( + binary=True, + coords=self._model.get_coords(['period', 'scenario']), + short_name='invested', ) - else: - self.size = self.add( - self._model.add_variables( - lower=0 if self.parameters.optional else self.parameters.minimum_size, - upper=self.parameters.maximum_size, - name=f'{self.label_full}|size', - ), - 'size', + BoundingPatterns.bounds_with_state( + self, + variable=self.size, + variable_state=self._variables['invested'], + bounds=(self.parameters.minimum_or_fixed_size, self.parameters.maximum_or_fixed_size), ) - # Optional - if self.parameters.optional: - self.is_invested = self.add( - self._model.add_variables(binary=True, name=f'{self.label_full}|is_invested'), 'is_invested' + if self.parameters.linked_periods is not None: + masked_size = self.size.where(self.parameters.linked_periods, drop=True) + self.add_constraints( + masked_size.isel(period=slice(None, -1)) == masked_size.isel(period=slice(1, None)), + short_name='linked_periods', ) - self._create_bounds_for_optional_investment() - - # Bounds for defining variable - self._create_bounds_for_defining_variable() - - self._create_shares() - - def _create_shares(self): - # fix_effects: - fix_effects = self.parameters.fix_effects - if fix_effects != {}: + def _add_effects(self): + """Add investment effects""" + if self.parameters.effects_of_investment: self._model.effects.add_share_to_effects( name=self.label_of_element, expressions={ - effect: self.is_invested * factor if self.is_invested is not None else factor - for effect, factor in fix_effects.items() + effect: self.invested * factor if self.invested is not None else factor + for effect, factor in self.parameters.effects_of_investment.items() }, - target='invest', + target='periodic', ) - if self.parameters.divest_effects != {} and self.parameters.optional: - # share: divest_effects - isInvested * divest_effects + if self.parameters.effects_of_retirement and not self.parameters.mandatory: self._model.effects.add_share_to_effects( name=self.label_of_element, expressions={ - effect: -self.is_invested * factor + factor - for effect, factor in self.parameters.divest_effects.items() + effect: -self.invested * factor + factor + for effect, factor in self.parameters.effects_of_retirement.items() }, - target='invest', + target='periodic', ) - if self.parameters.specific_effects != {}: + if self.parameters.effects_of_investment_per_size: self._model.effects.add_share_to_effects( name=self.label_of_element, - expressions={effect: self.size * factor for effect, factor in self.parameters.specific_effects.items()}, - target='invest', + expressions={ + effect: self.size * factor + for effect, factor in self.parameters.effects_of_investment_per_size.items() + }, + target='periodic', ) - if self.parameters.piecewise_effects: - self.piecewise_effects = self.add( + if self.parameters.piecewise_effects_of_investment: + self.piecewise_effects = self.add_submodels( PiecewiseEffectsModel( model=self._model, label_of_element=self.label_of_element, - piecewise_origin=(self.size.name, self.parameters.piecewise_effects.piecewise_origin), - piecewise_shares=self.parameters.piecewise_effects.piecewise_shares, - zero_point=self.is_invested, - ), - 'segments', - ) - self.piecewise_effects.do_modeling() - - def _create_bounds_for_optional_investment(self): - if self.parameters.fixed_size: - # eq: investment_size = isInvested * fixed_size - self.add( - self._model.add_constraints( - self.size == self.is_invested * self.parameters.fixed_size, name=f'{self.label_full}|is_invested' - ), - 'is_invested', - ) - - else: - # eq1: P_invest <= isInvested * investSize_max - self.add( - self._model.add_constraints( - self.size <= self.is_invested * self.parameters.maximum_size, - name=f'{self.label_full}|is_invested_ub', - ), - 'is_invested_ub', - ) - - # eq2: P_invest >= isInvested * max(epsilon, investSize_min) - self.add( - self._model.add_constraints( - self.size >= self.is_invested * np.maximum(CONFIG.Modeling.epsilon, self.parameters.minimum_size), - name=f'{self.label_full}|is_invested_lb', - ), - 'is_invested_lb', - ) - - def _create_bounds_for_defining_variable(self): - variable = self._defining_variable - lb_relative, ub_relative = self._relative_bounds_of_defining_variable - if np.all(lb_relative == ub_relative): - self.add( - self._model.add_constraints( - variable == self.size * ub_relative, name=f'{self.label_full}|fix_{variable.name}' - ), - f'fix_{variable.name}', - ) - if self._on_variable is not None: - raise ValueError( - f'Flow {self.label_full} has a fixed relative flow rate and an on_variable.' - f'This combination is currently not supported.' - ) - return - - # eq: defining_variable(t) <= size * upper_bound(t) - self.add( - self._model.add_constraints( - variable <= self.size * ub_relative, name=f'{self.label_full}|ub_{variable.name}' - ), - f'ub_{variable.name}', - ) - - if self._on_variable is None: - # eq: defining_variable(t) >= investment_size * relative_minimum(t) - self.add( - self._model.add_constraints( - variable >= self.size * lb_relative, name=f'{self.label_full}|lb_{variable.name}' - ), - f'lb_{variable.name}', - ) - else: - ## 2. Gleichung: Minimum durch Investmentgröße und On - # eq: defining_variable(t) >= mega * (On(t)-1) + size * relative_minimum(t) - # ... mit mega = relative_maximum * maximum_size - # äquivalent zu:. - # eq: - defining_variable(t) + mega * On(t) + size * relative_minimum(t) <= + mega - mega = lb_relative * self.parameters.maximum_size - on = self._on_variable - self.add( - self._model.add_constraints( - variable >= mega * (on - 1) + self.size * lb_relative, name=f'{self.label_full}|lb_{variable.name}' - ), - f'lb_{variable.name}', - ) - # anmerkung: Glg bei Spezialfall relative_minimum = 0 redundant zu OnOff ?? - - -class StateModel(Model): - """ - Handles basic on/off binary states for defining variables - """ - - def __init__( - self, - model: SystemModel, - label_of_element: str, - defining_variables: list[linopy.Variable], - defining_bounds: list[tuple[NumericData, NumericData]], - previous_values: list[NumericData | None] | None = None, - use_off: bool = True, - on_hours_total_min: NumericData | None = 0, - on_hours_total_max: NumericData | None = None, - effects_per_running_hour: dict[str, NumericData] | None = None, - label: str | None = None, - ): - """ - Models binary state variables based on a continous variable. - - Args: - model: The SystemModel that is used to create the model. - label_of_element: The label of the parent (Element). Used to construct the full label of the model. - defining_variables: List of Variables that are used to define the state - defining_bounds: List of Tuples, defining the absolute bounds of each defining variable - previous_values: List of previous values of the defining variables - use_off: Whether to use the off state or not - on_hours_total_min: min. overall sum of operating hours. - on_hours_total_max: max. overall sum of operating hours. - effects_per_running_hour: Costs per operating hours - label: Label of the OnOffModel - """ - super().__init__(model, label_of_element, label) - assert len(defining_variables) == len(defining_bounds), 'Every defining Variable needs bounds to Model OnOff' - self._defining_variables = defining_variables - self._defining_bounds = defining_bounds - self._previous_values = previous_values or [] - self._on_hours_total_min = on_hours_total_min if on_hours_total_min is not None else 0 - self._on_hours_total_max = on_hours_total_max if on_hours_total_max is not None else np.inf - self._use_off = use_off - self._effects_per_running_hour = effects_per_running_hour or {} - - self.on = None - self.total_on_hours: linopy.Variable | None = None - self.off = None - - def do_modeling(self): - self.on = self.add( - self._model.add_variables( - name=f'{self.label_full}|on', - binary=True, - coords=self._model.coords, - ), - 'on', - ) - - self.total_on_hours = self.add( - self._model.add_variables( - lower=self._on_hours_total_min, - upper=self._on_hours_total_max, - coords=None, - name=f'{self.label_full}|on_hours_total', - ), - 'on_hours_total', - ) - - self.add( - self._model.add_constraints( - self.total_on_hours == (self.on * self._model.hours_per_step).sum(), - name=f'{self.label_full}|on_hours_total', - ), - 'on_hours_total', - ) - - # Add defining constraints for each variable - self._add_defining_constraints() - - if self._use_off: - self.off = self.add( - self._model.add_variables( - name=f'{self.label_full}|off', - binary=True, - coords=self._model.coords, - ), - 'off', - ) - - # Constraint: on + off = 1 - self.add(self._model.add_constraints(self.on + self.off == 1, name=f'{self.label_full}|off'), 'off') - - return self - - def _add_defining_constraints(self): - """Add constraints that link defining variables to the on state""" - nr_of_def_vars = len(self._defining_variables) - - if nr_of_def_vars == 1: - # Case for a single defining variable - def_var = self._defining_variables[0] - lb, ub = self._defining_bounds[0] - - # Constraint: on * lower_bound <= def_var - self.add( - self._model.add_constraints( - self.on * np.maximum(CONFIG.Modeling.epsilon, lb) <= def_var, name=f'{self.label_full}|on_con1' - ), - 'on_con1', - ) - - # Constraint: on * upper_bound >= def_var - self.add(self._model.add_constraints(self.on * ub >= def_var, name=f'{self.label_full}|on_con2'), 'on_con2') - else: - # Case for multiple defining variables - ub = sum(bound[1] for bound in self._defining_bounds) / nr_of_def_vars - lb = CONFIG.Modeling.epsilon # TODO: Can this be a bigger value? (maybe the smallest bound?) - - # Constraint: on * epsilon <= sum(all_defining_variables) - self.add( - self._model.add_constraints( - self.on * lb <= sum(self._defining_variables), name=f'{self.label_full}|on_con1' + label_of_model=f'{self.label_of_element}|PiecewiseEffects', + piecewise_origin=(self.size.name, self.parameters.piecewise_effects_of_investment.piecewise_origin), + piecewise_shares=self.parameters.piecewise_effects_of_investment.piecewise_shares, + zero_point=self.invested, ), - 'on_con1', + short_name='segments', ) - # Constraint to ensure all variables are zero when off. - # Divide by nr_of_def_vars to improve numerical stability (smaller factors) - self.add( - self._model.add_constraints( - self.on * ub >= sum([def_var / nr_of_def_vars for def_var in self._defining_variables]), - name=f'{self.label_full}|on_con2', - ), - 'on_con2', - ) - - @property - def previous_states(self) -> np.ndarray: - """Computes the previous states {0, 1} of defining variables as a binary array from their previous values.""" - return StateModel.compute_previous_states(self._previous_values, epsilon=CONFIG.Modeling.epsilon) - @property - def previous_on_states(self) -> np.ndarray: - return self.previous_states + def size(self) -> linopy.Variable: + """Investment size variable""" + return self._variables['size'] @property - def previous_off_states(self): - return 1 - self.previous_states + def invested(self) -> linopy.Variable | None: + """Binary investment decision variable""" + if 'invested' not in self._variables: + return None + return self._variables['invested'] - @staticmethod - def compute_previous_states(previous_values: list[NumericData | None] | None, epsilon: float = 1e-5) -> np.ndarray: - """Computes the previous states {0, 1} of defining variables as a binary array from their previous values.""" - if not previous_values or all([val is None for val in previous_values]): - return np.array([0]) - # Convert to 2D-array and compute binary on/off states - previous_values = np.array([values for values in previous_values if values is not None]) # Filter out None - if previous_values.ndim > 1: - return np.any(~np.isclose(previous_values, 0, atol=epsilon), axis=0).astype(int) - - return (~np.isclose(previous_values, 0, atol=epsilon)).astype(int) - - -class SwitchStateModel(Model): - """ - Handles switch on/off transitions - """ +class OnOffModel(Submodel): + """OnOff model using factory patterns""" def __init__( self, - model: SystemModel, + model: FlowSystemModel, label_of_element: str, - state_variable: linopy.Variable, - previous_state=0, - switch_on_max: Scalar | None = None, - label: str | None = None, - ): - super().__init__(model, label_of_element, label) - self._state_variable = state_variable - self.previous_state = previous_state - self._switch_on_max = switch_on_max if switch_on_max is not None else np.inf - - self.switch_on = None - self.switch_off = None - self.switch_on_nr = None - - def do_modeling(self): - """Create switch variables and constraints""" - - # Create switch variables - self.switch_on = self.add( - self._model.add_variables(binary=True, name=f'{self.label_full}|switch_on', coords=self._model.coords), - 'switch_on', - ) - - self.switch_off = self.add( - self._model.add_variables(binary=True, name=f'{self.label_full}|switch_off', coords=self._model.coords), - 'switch_off', - ) - - # Create count variable for number of switches - self.switch_on_nr = self.add( - self._model.add_variables( - upper=self._switch_on_max, - lower=0, - name=f'{self.label_full}|switch_on_nr', - ), - 'switch_on_nr', - ) - - # Add switch constraints for all entries after the first timestep - self.add( - self._model.add_constraints( - self.switch_on.isel(time=slice(1, None)) - self.switch_off.isel(time=slice(1, None)) - == self._state_variable.isel(time=slice(1, None)) - self._state_variable.isel(time=slice(None, -1)), - name=f'{self.label_full}|switch_con', - ), - 'switch_con', - ) - - # Initial switch constraint - self.add( - self._model.add_constraints( - self.switch_on.isel(time=0) - self.switch_off.isel(time=0) - == self._state_variable.isel(time=0) - self.previous_state, - name=f'{self.label_full}|initial_switch_con', - ), - 'initial_switch_con', - ) - - # Mutual exclusivity constraint - self.add( - self._model.add_constraints( - self.switch_on + self.switch_off <= 1.1, name=f'{self.label_full}|switch_on_or_off' - ), - 'switch_on_or_off', - ) - - # Total switch-on count constraint - self.add( - self._model.add_constraints( - self.switch_on_nr == self.switch_on.sum('time'), name=f'{self.label_full}|switch_on_nr' - ), - 'switch_on_nr', - ) - - return self - - -class ConsecutiveStateModel(Model): - """ - Handles tracking consecutive durations in a state - """ - - def __init__( - self, - model: SystemModel, - label_of_element: str, - state_variable: linopy.Variable, - minimum_duration: NumericData | None = None, - maximum_duration: NumericData | None = None, - previous_states: NumericData | None = None, - label: str | None = None, + parameters: OnOffParameters, + on_variable: linopy.Variable, + previous_states: TemporalData | None, + label_of_model: str | None = None, ): """ - Model and constraint the consecutive duration of a state variable. + This feature model is used to model the on/off state of flow_rate(s). It does not matter of the flow_rates are + bounded by a size variable or by a hard bound. THe used bound here is the absolute highest/lowest bound! Args: - model: The SystemModel that is used to create the model. + model: The optimization model instance label_of_element: The label of the parent (Element). Used to construct the full label of the model. - state_variable: The state variable that is used to model the duration. state = {0, 1} - minimum_duration: The minimum duration of the state variable. - maximum_duration: The maximum duration of the state variable. - previous_states: The previous states of the state variable. - label: The label of the model. Used to construct the full label of the model. + parameters: The parameters of the feature model. + on_variable: The variable that determines the on state + previous_states: The previous flow_rates + label_of_model: The label of the model. This is needed to construct the full label of the model. """ - super().__init__(model, label_of_element, label) - self._state_variable = state_variable + self.on = on_variable self._previous_states = previous_states - self._minimum_duration = minimum_duration - self._maximum_duration = maximum_duration - - if isinstance(self._minimum_duration, TimeSeries): - self._minimum_duration = self._minimum_duration.active_data - if isinstance(self._maximum_duration, TimeSeries): - self._maximum_duration = self._maximum_duration.active_data - - self.duration = None - - def do_modeling(self): - """Create consecutive duration variables and constraints""" - # Get the hours per step - hours_per_step = self._model.hours_per_step - mega = hours_per_step.sum('time') + self.previous_duration - - # Create the duration variable - self.duration = self.add( - self._model.add_variables( - lower=0, - upper=self._maximum_duration if self._maximum_duration is not None else mega, - coords=self._model.coords, - name=f'{self.label_full}|hours', - ), - 'hours', - ) - - # Add constraints - - # Upper bound constraint - self.add( - self._model.add_constraints(self.duration <= self._state_variable * mega, name=f'{self.label_full}|con1'), - 'con1', - ) - - # Forward constraint - self.add( - self._model.add_constraints( - self.duration.isel(time=slice(1, None)) - <= self.duration.isel(time=slice(None, -1)) + hours_per_step.isel(time=slice(None, -1)), - name=f'{self.label_full}|con2a', - ), - 'con2a', - ) - - # Backward constraint - self.add( - self._model.add_constraints( - self.duration.isel(time=slice(1, None)) - >= self.duration.isel(time=slice(None, -1)) - + hours_per_step.isel(time=slice(None, -1)) - + (self._state_variable.isel(time=slice(1, None)) - 1) * mega, - name=f'{self.label_full}|con2b', - ), - 'con2b', - ) - - # Add minimum duration constraints if specified - if self._minimum_duration is not None: - self.add( - self._model.add_constraints( - self.duration - >= ( - self._state_variable.isel(time=slice(None, -1)) - self._state_variable.isel(time=slice(1, None)) - ) - * self._minimum_duration.isel(time=slice(None, -1)), - name=f'{self.label_full}|minimum', - ), - 'minimum', - ) - - # Handle initial condition - if 0 < self.previous_duration < self._minimum_duration.isel(time=0): - self.add( - self._model.add_constraints( - self._state_variable.isel(time=0) == 1, name=f'{self.label_full}|initial_minimum' - ), - 'initial_minimum', - ) - - # Set initial value - self.add( - self._model.add_constraints( - self.duration.isel(time=0) - == (hours_per_step.isel(time=0) + self.previous_duration) * self._state_variable.isel(time=0), - name=f'{self.label_full}|initial', - ), - 'initial', - ) - - return self - - @property - def previous_duration(self) -> Scalar: - """Computes the previous duration of the state variable""" - # TODO: Allow for other/dynamic timestep resolutions - return ConsecutiveStateModel.compute_consecutive_hours_in_state( - self._previous_states, self._model.hours_per_step.isel(time=0).item() - ) - - @staticmethod - def compute_consecutive_hours_in_state( - binary_values: NumericData, hours_per_timestep: int | float | np.ndarray - ) -> Scalar: - """ - Computes the final consecutive duration in state 'on' (=1) in hours, from a binary array. - - Args: - binary_values: An int or 1D binary array containing only `0`s and `1`s. - hours_per_timestep: The duration of each timestep in hours. - If a scalar is provided, it is used for all timesteps. - If an array is provided, it must be as long as the last consecutive duration in binary_values. - - Returns: - The duration of the binary variable in hours. - - Raises - ------ - TypeError - If the length of binary_values and dt_in_hours is not equal, but None is a scalar. - """ - if np.isscalar(binary_values) and np.isscalar(hours_per_timestep): - return binary_values * hours_per_timestep - elif np.isscalar(binary_values) and not np.isscalar(hours_per_timestep): - return binary_values * hours_per_timestep[-1] - - if np.isclose(binary_values[-1], 0, atol=CONFIG.Modeling.epsilon): - return 0 - - if np.isscalar(hours_per_timestep): - hours_per_timestep = np.ones(len(binary_values)) * hours_per_timestep - hours_per_timestep: np.ndarray - - indexes_with_zero_values = np.where(np.isclose(binary_values, 0, atol=CONFIG.Modeling.epsilon))[0] - if len(indexes_with_zero_values) == 0: - nr_of_indexes_with_consecutive_ones = len(binary_values) - else: - nr_of_indexes_with_consecutive_ones = len(binary_values) - indexes_with_zero_values[-1] - 1 - - if len(hours_per_timestep) < nr_of_indexes_with_consecutive_ones: - raise ValueError( - f'When trying to calculate the consecutive duration, the length of the last duration ' - f'({nr_of_indexes_with_consecutive_ones}) is longer than the provided hours_per_timestep ({len(hours_per_timestep)}), ' - f'as {binary_values=}' - ) - - return np.sum( - binary_values[-nr_of_indexes_with_consecutive_ones:] - * hours_per_timestep[-nr_of_indexes_with_consecutive_ones:] - ) - - -class OnOffModel(Model): - """ - Class for modeling the on and off state of a variable - Uses component models to create a modular implementation - """ - - def __init__( - self, - model: SystemModel, - on_off_parameters: OnOffParameters, - label_of_element: str, - defining_variables: list[linopy.Variable], - defining_bounds: list[tuple[NumericData, NumericData]], - previous_values: list[NumericData | None], - label: str | None = None, - ): - """ - Constructor for OnOffModel - - Args: - model: Reference to the SystemModel - on_off_parameters: Parameters for the OnOffModel - label_of_element: Label of the Parent - defining_variables: List of Variables that are used to define the OnOffModel - defining_bounds: List of Tuples, defining the absolute bounds of each defining variable - previous_values: List of previous values of the defining variables - label: Label of the OnOffModel - """ - super().__init__(model, label_of_element, label) - self.parameters = on_off_parameters - self._defining_variables = defining_variables - self._defining_bounds = defining_bounds - self._previous_values = previous_values - - self.state_model = None - self.switch_state_model = None - self.consecutive_on_model = None - self.consecutive_off_model = None - - def do_modeling(self): - """Create all variables and constraints for the OnOffModel""" - - # Create binary state component - self.state_model = StateModel( - model=self._model, - label_of_element=self.label_of_element, - defining_variables=self._defining_variables, - defining_bounds=self._defining_bounds, - previous_values=self._previous_values, - use_off=self.parameters.use_off, - on_hours_total_min=self.parameters.on_hours_total_min, - on_hours_total_max=self.parameters.on_hours_total_max, - effects_per_running_hour=self.parameters.effects_per_running_hour, + self.parameters = parameters + super().__init__(model, label_of_element, label_of_model=label_of_model) + + def _do_modeling(self): + super()._do_modeling() + + if self.parameters.use_off: + off = self.add_variables(binary=True, short_name='off', coords=self._model.get_coords()) + self.add_constraints(self.on + off == 1, short_name='complementary') + + # 3. Total duration tracking using existing pattern + ModelingPrimitives.expression_tracking_variable( + self, + tracked_expression=(self.on * self._model.hours_per_step).sum('time'), + bounds=( + self.parameters.on_hours_total_min if self.parameters.on_hours_total_min is not None else 0, + self.parameters.on_hours_total_max if self.parameters.on_hours_total_max is not None else np.inf, + ), # TODO: self._model.hours_per_step.sum('time').item() + self._get_previous_on_duration()) + short_name='on_hours_total', + coords=['period', 'scenario'], ) - self.add(self.state_model) - self.state_model.do_modeling() - # Create switch component if needed + # 4. Switch tracking using existing pattern if self.parameters.use_switch_on: - self.switch_state_model = SwitchStateModel( - model=self._model, - label_of_element=self.label_of_element, - state_variable=self.state_model.on, - previous_state=self.state_model.previous_on_states[-1], - switch_on_max=self.parameters.switch_on_total_max, - ) - self.add(self.switch_state_model) - self.switch_state_model.do_modeling() + self.add_variables(binary=True, short_name='switch|on', coords=self.get_coords()) + self.add_variables(binary=True, short_name='switch|off', coords=self.get_coords()) + + BoundingPatterns.state_transition_bounds( + self, + state_variable=self.on, + switch_on=self.switch_on, + switch_off=self.switch_off, + name=f'{self.label_of_model}|switch', + previous_state=self._previous_states.isel(time=-1) if self._previous_states is not None else 0, + coord='time', + ) + + if self.parameters.switch_on_total_max is not None: + count = self.add_variables( + lower=0, + upper=self.parameters.switch_on_total_max, + coords=self._model.get_coords(('period', 'scenario')), + short_name='switch|count', + ) + self.add_constraints(count == self.switch_on.sum('time'), short_name='switch|count') - # Create consecutive on hours component if needed + # 5. Consecutive on duration using existing pattern if self.parameters.use_consecutive_on_hours: - self.consecutive_on_model = ConsecutiveStateModel( - model=self._model, - label_of_element=self.label_of_element, - state_variable=self.state_model.on, + ModelingPrimitives.consecutive_duration_tracking( + self, + state_variable=self.on, + short_name='consecutive_on_hours', minimum_duration=self.parameters.consecutive_on_hours_min, maximum_duration=self.parameters.consecutive_on_hours_max, - previous_states=self.state_model.previous_on_states, - label='ConsecutiveOn', + duration_per_step=self.hours_per_step, + duration_dim='time', + previous_duration=self._get_previous_on_duration(), ) - self.add(self.consecutive_on_model) - self.consecutive_on_model.do_modeling() - # Create consecutive off hours component if needed + # 6. Consecutive off duration using existing pattern if self.parameters.use_consecutive_off_hours: - self.consecutive_off_model = ConsecutiveStateModel( - model=self._model, - label_of_element=self.label_of_element, - state_variable=self.state_model.off, + ModelingPrimitives.consecutive_duration_tracking( + self, + state_variable=self.off, + short_name='consecutive_off_hours', minimum_duration=self.parameters.consecutive_off_hours_min, maximum_duration=self.parameters.consecutive_off_hours_max, - previous_states=self.state_model.previous_off_states, - label='ConsecutiveOff', + duration_per_step=self.hours_per_step, + duration_dim='time', + previous_duration=self._get_previous_off_duration(), ) - self.add(self.consecutive_off_model) - self.consecutive_off_model.do_modeling() + # TODO: - self._create_shares() + self._add_effects() - def _create_shares(self): + def _add_effects(self): + """Add operational effects""" if self.parameters.effects_per_running_hour: self._model.effects.add_share_to_effects( name=self.label_of_element, expressions={ - effect: self.state_model.on * factor * self._model.hours_per_step + effect: self.on * factor * self._model.hours_per_step for effect, factor in self.parameters.effects_per_running_hour.items() }, - target='operation', + target='temporal', ) if self.parameters.effects_per_switch_on: self._model.effects.add_share_to_effects( name=self.label_of_element, expressions={ - effect: self.switch_state_model.switch_on * factor - for effect, factor in self.parameters.effects_per_switch_on.items() + effect: self.switch_on * factor for effect, factor in self.parameters.effects_per_switch_on.items() }, - target='operation', + target='temporal', ) + # Properties access variables from Submodel's tracking system + @property - def on(self): - return self.state_model.on + def on_hours_total(self) -> linopy.Variable: + """Total on hours variable""" + return self['on_hours_total'] @property - def off(self): - return self.state_model.off + def off(self) -> linopy.Variable | None: + """Binary off state variable""" + return self.get('off') @property - def switch_on(self): - return self.switch_state_model.switch_on + def switch_on(self) -> linopy.Variable | None: + """Switch on variable""" + return self.get('switch|on') @property - def switch_off(self): - return self.switch_state_model.switch_off + def switch_off(self) -> linopy.Variable | None: + """Switch off variable""" + return self.get('switch|off') @property - def switch_on_nr(self): - return self.switch_state_model.switch_on_nr + def switch_on_nr(self) -> linopy.Variable | None: + """Number of switch-ons variable""" + return self.get('switch|count') @property - def consecutive_on_hours(self): - return self.consecutive_on_model.duration + def consecutive_on_hours(self) -> linopy.Variable | None: + """Consecutive on hours variable""" + return self.get('consecutive_on_hours') @property - def consecutive_off_hours(self): - return self.consecutive_off_model.duration + def consecutive_off_hours(self) -> linopy.Variable | None: + """Consecutive off hours variable""" + return self.get('consecutive_off_hours') + + def _get_previous_on_duration(self): + """Get previous on duration. Previously OFF by default, for one timestep""" + hours_per_step = self._model.hours_per_step.isel(time=0).min().item() + if self._previous_states is None: + return 0 + else: + return ModelingUtilities.compute_consecutive_hours_in_state(self._previous_states, hours_per_step) + + def _get_previous_off_duration(self): + """Get previous off duration. Previously OFF by default, for one timestep""" + hours_per_step = self._model.hours_per_step.isel(time=0).min().item() + if self._previous_states is None: + return hours_per_step + else: + return ModelingUtilities.compute_consecutive_hours_in_state(self._previous_states * -1 + 1, hours_per_step) -class PieceModel(Model): +class PieceModel(Submodel): """Class for modeling a linear piece of one or more variables in parallel""" def __init__( self, - model: SystemModel, + model: FlowSystemModel, label_of_element: str, - label: str, - as_time_series: bool = True, + label_of_model: str, + dims: FlowSystemDimensions | None, ): - super().__init__(model, label_of_element, label) self.inside_piece: linopy.Variable | None = None self.lambda0: linopy.Variable | None = None self.lambda1: linopy.Variable | None = None - self._as_time_series = as_time_series + self.dims = dims - def do_modeling(self): - self.inside_piece = self.add( - self._model.add_variables( - binary=True, - name=f'{self.label_full}|inside_piece', - coords=self._model.coords if self._as_time_series else None, - ), - 'inside_piece', - ) + super().__init__(model, label_of_element, label_of_model) - self.lambda0 = self.add( - self._model.add_variables( - lower=0, - upper=1, - name=f'{self.label_full}|lambda0', - coords=self._model.coords if self._as_time_series else None, - ), - 'lambda0', + def _do_modeling(self): + super()._do_modeling() + self.inside_piece = self.add_variables( + binary=True, + short_name='inside_piece', + coords=self._model.get_coords(dims=self.dims), + ) + self.lambda0 = self.add_variables( + lower=0, + upper=1, + short_name='lambda0', + coords=self._model.get_coords(dims=self.dims), ) - self.lambda1 = self.add( - self._model.add_variables( - lower=0, - upper=1, - name=f'{self.label_full}|lambda1', - coords=self._model.coords if self._as_time_series else None, - ), - 'lambda1', + self.lambda1 = self.add_variables( + lower=0, + upper=1, + short_name='lambda1', + coords=self._model.get_coords(dims=self.dims), ) # eq: lambda0(t) + lambda1(t) = inside_piece(t) - self.add( - self._model.add_constraints( - self.inside_piece == self.lambda0 + self.lambda1, name=f'{self.label_full}|inside_piece' - ), - 'inside_piece', - ) + self.add_constraints(self.inside_piece == self.lambda0 + self.lambda1, short_name='inside_piece') -class PiecewiseModel(Model): +class PiecewiseModel(Submodel): def __init__( self, - model: SystemModel, + model: FlowSystemModel, label_of_element: str, + label_of_model: str, piecewise_variables: dict[str, Piecewise], zero_point: bool | linopy.Variable | None, - as_time_series: bool, - label: str = '', + dims: FlowSystemDimensions | None, ): """ Modeling a Piecewise relation between miultiple variables. @@ -849,50 +378,54 @@ def __init__( Each Piece is a tuple of (start, end). Args: - model: The SystemModel that is used to create the model. + model: The FlowSystemModel that is used to create the model. label_of_element: The label of the parent (Element). Used to construct the full label of the model. - label: The label of the model. Used to construct the full label of the model. + label_of_model: The label of the model. Used to construct the full label of the model. piecewise_variables: The variables to which the Pieces are assigned. zero_point: A variable that can be used to define a zero point for the Piecewise relation. If None or False, no zero point is defined. - as_time_series: Whether the Piecewise relation is defined for a TimeSeries or a single variable. + dims: The dimensions used for variable creation. If None, all dimensions are used. """ - super().__init__(model, label_of_element, label) self._piecewise_variables = piecewise_variables self._zero_point = zero_point - self._as_time_series = as_time_series + self.dims = dims self.pieces: list[PieceModel] = [] self.zero_point: linopy.Variable | None = None + super().__init__(model, label_of_element=label_of_element, label_of_model=label_of_model) + + def _do_modeling(self): + super()._do_modeling() + # Validate all piecewise variables have the same number of segments + segment_counts = [len(pw) for pw in self._piecewise_variables.values()] + if not all(count == segment_counts[0] for count in segment_counts): + raise ValueError(f'All piecewises must have the same number of pieces, got {segment_counts}') - def do_modeling(self): for i in range(len(list(self._piecewise_variables.values())[0])): - new_piece = self.add( + new_piece = self.add_submodels( PieceModel( model=self._model, label_of_element=self.label_of_element, - label=f'Piece_{i}', - as_time_series=self._as_time_series, - ) + label_of_model=f'{self.label_of_element}|Piece_{i}', + dims=self.dims, + ), + short_name=f'Piece_{i}', ) self.pieces.append(new_piece) - new_piece.do_modeling() for var_name in self._piecewise_variables: variable = self._model.variables[var_name] - self.add( - self._model.add_constraints( - variable - == sum( - [ - piece_model.lambda0 * piece_bounds.start + piece_model.lambda1 * piece_bounds.end - for piece_model, piece_bounds in zip( - self.pieces, self._piecewise_variables[var_name], strict=False - ) - ] - ), - name=f'{self.label_full}|{var_name}|lambda', + self.add_constraints( + variable + == sum( + [ + piece_model.lambda0 * piece_bounds.start + piece_model.lambda1 * piece_bounds.end + for piece_model, piece_bounds in zip( + self.pieces, self._piecewise_variables[var_name], strict=False + ) + ] ), - f'{var_name}|lambda', + name=f'{self.label_full}|{var_name}|lambda', + short_name=f'{var_name}|lambda', ) # a) eq: Segment1.onSeg(t) + Segment2.onSeg(t) + ... = 1 Aufenthalt nur in Segmenten erlaubt @@ -901,43 +434,98 @@ def do_modeling(self): self.zero_point = self._zero_point rhs = self.zero_point elif self._zero_point is True: - self.zero_point = self.add( - self._model.add_variables( - coords=self._model.coords, binary=True, name=f'{self.label_full}|zero_point' - ), - 'zero_point', + self.zero_point = self.add_variables( + coords=self._model.get_coords(self.dims), + binary=True, + short_name='zero_point', ) rhs = self.zero_point else: rhs = 1 - self.add( - self._model.add_constraints( - sum([piece.inside_piece for piece in self.pieces]) <= rhs, - name=f'{self.label_full}|{variable.name}|single_segment', - ), - f'{var_name}|single_segment', + self.add_constraints( + sum([piece.inside_piece for piece in self.pieces]) <= rhs, + name=f'{self.label_full}|{variable.name}|single_segment', + short_name=f'{var_name}|single_segment', ) -class ShareAllocationModel(Model): +class PiecewiseEffectsModel(Submodel): + def __init__( + self, + model: FlowSystemModel, + label_of_element: str, + label_of_model: str, + piecewise_origin: tuple[str, Piecewise], + piecewise_shares: dict[str, Piecewise], + zero_point: bool | linopy.Variable | None, + ): + origin_count = len(piecewise_origin[1]) + share_counts = [len(pw) for pw in piecewise_shares.values()] + if not all(count == origin_count for count in share_counts): + raise ValueError( + f'Piece count mismatch: piecewise_origin has {origin_count} segments, ' + f'but piecewise_shares have {share_counts}' + ) + self._zero_point = zero_point + self._piecewise_origin = piecewise_origin + self._piecewise_shares = piecewise_shares + self.shares: dict[str, linopy.Variable] = {} + + self.piecewise_model: PiecewiseModel | None = None + + super().__init__(model, label_of_element=label_of_element, label_of_model=label_of_model) + + def _do_modeling(self): + self.shares = { + effect: self.add_variables(coords=self._model.get_coords(['period', 'scenario']), short_name=effect) + for effect in self._piecewise_shares + } + + piecewise_variables = { + self._piecewise_origin[0]: self._piecewise_origin[1], + **{ + self.shares[effect_label].name: self._piecewise_shares[effect_label] + for effect_label in self._piecewise_shares + }, + } + + self.piecewise_model = self.add_submodels( + PiecewiseModel( + model=self._model, + label_of_element=self.label_of_element, + piecewise_variables=piecewise_variables, + zero_point=self._zero_point, + dims=('period', 'scenario'), + label_of_model=f'{self.label_of_element}|PiecewiseEffects', + ), + short_name='PiecewiseEffects', + ) + + # Shares + self._model.effects.add_share_to_effects( + name=self.label_of_element, + expressions={effect: variable * 1 for effect, variable in self.shares.items()}, + target='periodic', + ) + + +class ShareAllocationModel(Submodel): def __init__( self, - model: SystemModel, - shares_are_time_series: bool, + model: FlowSystemModel, + dims: list[FlowSystemDimensions], label_of_element: str | None = None, - label: str | None = None, - label_full: str | None = None, + label_of_model: str | None = None, total_max: Scalar | None = None, total_min: Scalar | None = None, - max_per_hour: NumericData | None = None, - min_per_hour: NumericData | None = None, + max_per_hour: TemporalData | None = None, + min_per_hour: TemporalData | None = None, ): - super().__init__(model, label_of_element=label_of_element, label=label, label_full=label_full) - if not shares_are_time_series: # If the condition is True - assert max_per_hour is None and min_per_hour is None, ( - 'Both max_per_hour and min_per_hour cannot be used when shares_are_time_series is False' - ) + if 'time' not in dims and (max_per_hour is not None or min_per_hour is not None): + raise ValueError('Both max_per_hour and min_per_hour cannot be used when has_time_dim is False') + + self._dims = dims self.total_per_timestep: linopy.Variable | None = None self.total: linopy.Variable | None = None self.shares: dict[str, linopy.Variable] = {} @@ -947,51 +535,43 @@ def __init__( self._eq_total: linopy.Constraint | None = None # Parameters - self._shares_are_time_series = shares_are_time_series self._total_max = total_max if total_max is not None else np.inf self._total_min = total_min if total_min is not None else -np.inf self._max_per_hour = max_per_hour if max_per_hour is not None else np.inf self._min_per_hour = min_per_hour if min_per_hour is not None else -np.inf - def do_modeling(self): - self.total = self.add( - self._model.add_variables( - lower=self._total_min, upper=self._total_max, coords=None, name=f'{self.label_full}|total' - ), - 'total', + super().__init__(model, label_of_element=label_of_element, label_of_model=label_of_model) + + def _do_modeling(self): + super()._do_modeling() + self.total = self.add_variables( + lower=self._total_min, + upper=self._total_max, + coords=self._model.get_coords([dim for dim in self._dims if dim != 'time']), + name=self.label_full, + short_name='total', ) # eq: sum = sum(share_i) # skalar - self._eq_total = self.add( - self._model.add_constraints(self.total == 0, name=f'{self.label_full}|total'), 'total' - ) + self._eq_total = self.add_constraints(self.total == 0, name=self.label_full) - if self._shares_are_time_series: - self.total_per_timestep = self.add( - self._model.add_variables( - lower=-np.inf - if (self._min_per_hour is None) - else np.multiply(self._min_per_hour, self._model.hours_per_step), - upper=np.inf - if (self._max_per_hour is None) - else np.multiply(self._max_per_hour, self._model.hours_per_step), - coords=self._model.coords, - name=f'{self.label_full}|total_per_timestep', - ), - 'total_per_timestep', + if 'time' in self._dims: + self.total_per_timestep = self.add_variables( + lower=-np.inf if (self._min_per_hour is None) else self._min_per_hour * self._model.hours_per_step, + upper=np.inf if (self._max_per_hour is None) else self._max_per_hour * self._model.hours_per_step, + coords=self._model.get_coords(self._dims), + short_name='per_timestep', ) - self._eq_total_per_timestep = self.add( - self._model.add_constraints(self.total_per_timestep == 0, name=f'{self.label_full}|total_per_timestep'), - 'total_per_timestep', - ) + self._eq_total_per_timestep = self.add_constraints(self.total_per_timestep == 0, short_name='per_timestep') # Add it to the total - self._eq_total.lhs -= self.total_per_timestep.sum() + self._eq_total.lhs -= self.total_per_timestep.sum(dim='time') def add_share( self, name: str, expression: linopy.LinearExpression, + dims: list[FlowSystemDimensions] | None = None, ): """ Add a share to the share allocation model. If the share already exists, the expression is added to the existing share. @@ -1002,124 +582,32 @@ def add_share( Args: name: The name of the share. expression: The expression of the share. Added to the right hand side of the constraint. + dims: The dimensions of the share. Defaults to all dimensions. Dims are ordered automatically """ + if dims is None: + dims = self._dims + else: + if 'time' in dims and 'time' not in self._dims: + raise ValueError('Cannot add share with time-dim to a model without time-dim') + if 'period' in dims and 'period' not in self._dims: + raise ValueError('Cannot add share with period-dim to a model without period-dim') + if 'scenario' in dims and 'scenario' not in self._dims: + raise ValueError('Cannot add share with scenario-dim to a model without scenario-dim') + if name in self.shares: self.share_constraints[name].lhs -= expression else: - self.shares[name] = self.add( - self._model.add_variables( - coords=None - if isinstance(expression, linopy.LinearExpression) - and expression.ndim == 0 - or not isinstance(expression, linopy.LinearExpression) - else self._model.coords, - name=f'{name}->{self.label_full}', - ), - name, + self.shares[name] = self.add_variables( + coords=self._model.get_coords(dims), + name=f'{name}->{self.label_full}', + short_name=name, ) - self.share_constraints[name] = self.add( - self._model.add_constraints(self.shares[name] == expression, name=f'{name}->{self.label_full}'), name + + self.share_constraints[name] = self.add_constraints( + self.shares[name] == expression, name=f'{name}->{self.label_full}' ) - if self.shares[name].ndim == 0: + + if 'time' not in dims: self._eq_total.lhs -= self.shares[name] else: self._eq_total_per_timestep.lhs -= self.shares[name] - - -class PiecewiseEffectsModel(Model): - def __init__( - self, - model: SystemModel, - label_of_element: str, - piecewise_origin: tuple[str, Piecewise], - piecewise_shares: dict[str, Piecewise], - zero_point: bool | linopy.Variable | None, - label: str = 'PiecewiseEffects', - ): - super().__init__(model, label_of_element, label) - assert len(piecewise_origin[1]) == len(list(piecewise_shares.values())[0]), ( - 'Piece length of variable_segments and share_segments must be equal' - ) - self._zero_point = zero_point - self._piecewise_origin = piecewise_origin - self._piecewise_shares = piecewise_shares - self.shares: dict[str, linopy.Variable] = {} - - self.piecewise_model: PiecewiseModel | None = None - - def do_modeling(self): - self.shares = { - effect: self.add(self._model.add_variables(coords=None, name=f'{self.label_full}|{effect}'), f'{effect}') - for effect in self._piecewise_shares - } - - piecewise_variables = { - self._piecewise_origin[0]: self._piecewise_origin[1], - **{ - self.shares[effect_label].name: self._piecewise_shares[effect_label] - for effect_label in self._piecewise_shares - }, - } - - self.piecewise_model = self.add( - PiecewiseModel( - model=self._model, - label_of_element=self.label_of_element, - piecewise_variables=piecewise_variables, - zero_point=self._zero_point, - as_time_series=False, - label='PiecewiseEffects', - ) - ) - - self.piecewise_model.do_modeling() - - # Shares - self._model.effects.add_share_to_effects( - name=self.label_of_element, - expressions={effect: variable * 1 for effect, variable in self.shares.items()}, - target='invest', - ) - - -class PreventSimultaneousUsageModel(Model): - """ - Prevents multiple Multiple Binary variables from being 1 at the same time - - Only 'classic type is modeled for now (# "classic" -> alle Flows brauchen Binärvariable:) - In 'new', the binary Variables need to be forced beforehand, which is not that straight forward... --> TODO maybe - - - # "new": - # eq: flow_1.on(t) + flow_2.on(t) + .. + flow_i.val(t)/flow_i.max <= 1 (1 Flow ohne Binärvariable!) - - # Anmerkung: Patrick Schönfeld (oemof, custom/link.py) macht bei 2 Flows ohne Binärvariable dies: - # 1) bin + flow1/flow1_max <= 1 - # 2) bin - flow2/flow2_max >= 0 - # 3) geht nur, wenn alle flow.min >= 0 - # --> könnte man auch umsetzen (statt force_on_variable() für die Flows, aber sollte aufs selbe wie "new" kommen) - """ - - def __init__( - self, - model: SystemModel, - variables: list[linopy.Variable], - label_of_element: str, - label: str = 'PreventSimultaneousUsage', - ): - super().__init__(model, label_of_element, label) - self._simultanious_use_variables = variables - assert len(self._simultanious_use_variables) >= 2, ( - f'Model {self.__class__.__name__} must get at least two variables' - ) - for variable in self._simultanious_use_variables: # classic - assert variable.attrs['binary'], f'Variable {variable} must be binary for use in {self.__class__.__name__}' - - def do_modeling(self): - # eq: sum(flow_i.on(t)) <= 1.1 (1 wird etwas größer gewählt wg. Binärvariablengenauigkeit) - self.add( - self._model.add_constraints( - sum(self._simultanious_use_variables) <= 1.1, name=f'{self.label_full}|prevent_simultaneous_use' - ), - 'prevent_simultaneous_use', - ) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 604b1ca1e..ad43c183b 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -7,30 +7,43 @@ import json import logging import warnings -from io import StringIO -from typing import TYPE_CHECKING, Literal +from typing import TYPE_CHECKING, Any, Literal, Optional +import numpy as np import pandas as pd -from rich.console import Console -from rich.pretty import Pretty - -from . import io as fx_io -from .core import NumericData, TimeSeries, TimeSeriesCollection, TimeSeriesData -from .effects import Effect, EffectCollection, EffectTimeSeries, EffectValuesDict, EffectValuesUser +import xarray as xr + +from .core import ( + ConversionError, + DataConverter, + FlowSystemDimensions, + PeriodicData, + PeriodicDataUser, + TemporalData, + TemporalDataUser, + TimeSeriesData, +) +from .effects import ( + Effect, + EffectCollection, + PeriodicEffects, + PeriodicEffectsUser, + TemporalEffects, + TemporalEffectsUser, +) from .elements import Bus, Component, Flow -from .structure import CLASS_REGISTRY, Element, SystemModel +from .structure import Element, FlowSystemModel, Interface if TYPE_CHECKING: import pathlib + from collections.abc import Collection - import numpy as np import pyvis - import xarray as xr logger = logging.getLogger('flixopt') -class FlowSystem: +class FlowSystem(Interface): """ A FlowSystem organizes the high level Elements (Components, Buses & Effects). @@ -38,106 +51,391 @@ class FlowSystem: Args: timesteps: The timesteps of the model. + periods: The periods of the model. + scenarios: The scenarios of the model. hours_of_last_timestep: The duration of the last time step. Uses the last time interval if not specified hours_of_previous_timesteps: The duration of previous timesteps. If None, the first time increment of time_series is used. This is needed to calculate previous durations (for example consecutive_on_hours). If you use an array, take care that its long enough to cover all previous values! + weights: The weights of each period and scenario. If None, all scenarios have the same weight (normalized to 1). + Its recommended to normalize the weights to sum up to 1. + scenario_independent_sizes: Controls whether investment sizes are equalized across scenarios. + - True: All sizes are shared/equalized across scenarios + - False: All sizes are optimized separately per scenario + - list[str]: Only specified components (by label_full) are equalized across scenarios + scenario_independent_flow_rates: Controls whether flow rates are equalized across scenarios. + - True: All flow rates are shared/equalized across scenarios + - False: All flow rates are optimized separately per scenario + - list[str]: Only specified flows (by label_full) are equalized across scenarios Notes: - Creates an empty registry for components and buses, an empty EffectCollection, and a placeholder for a SystemModel. - - The instance starts disconnected (self._connected == False) and will be connected automatically when trying to solve a calculation. + - The instance starts disconnected (self._connected_and_transformed == False) and will be + connected_and_transformed automatically when trying to solve a calculation. """ def __init__( self, timesteps: pd.DatetimeIndex, + periods: pd.Index | None = None, + scenarios: pd.Index | None = None, hours_of_last_timestep: float | None = None, hours_of_previous_timesteps: int | float | np.ndarray | None = None, + weights: PeriodicDataUser | None = None, + scenario_independent_sizes: bool | list[str] = True, + scenario_independent_flow_rates: bool | list[str] = False, ): - """ - Initialize a FlowSystem that manages components, buses, effects, and their time-series. + self.timesteps = self._validate_timesteps(timesteps) + self.timesteps_extra = self._create_timesteps_with_extra(self.timesteps, hours_of_last_timestep) + self.hours_of_previous_timesteps = self._calculate_hours_of_previous_timesteps( + self.timesteps, hours_of_previous_timesteps + ) - Parameters: - timesteps: DatetimeIndex defining the primary timesteps for the system's TimeSeriesCollection. - hours_of_last_timestep: Duration (in hours) of the final timestep; if None, inferred from timesteps or defaults in TimeSeriesCollection. - hours_of_previous_timesteps: Scalar or array-like durations (in hours) for the preceding timesteps; used to configure non-uniform timestep lengths. + self.periods = None if periods is None else self._validate_periods(periods) + self.scenarios = None if scenarios is None else self._validate_scenarios(scenarios) - Notes: - Creates an empty registry for components and buses, an empty EffectCollection, and a placeholder for a SystemModel. - The instance starts disconnected (self._connected == False) and with no active network visualization app. - This can also be triggered manually with `_connect_network()`. - """ - self.time_series_collection = TimeSeriesCollection( - timesteps=timesteps, - hours_of_last_timestep=hours_of_last_timestep, - hours_of_previous_timesteps=hours_of_previous_timesteps, - ) + self.weights = weights + + hours_per_timestep = self.calculate_hours_per_timestep(self.timesteps_extra) + + self.hours_of_last_timestep = hours_per_timestep[-1].item() - # defaults: + self.hours_per_timestep = self.fit_to_model_coords('hours_per_timestep', hours_per_timestep) + + # Element collections self.components: dict[str, Component] = {} self.buses: dict[str, Bus] = {} self.effects: EffectCollection = EffectCollection() - self.model: SystemModel | None = None + self.model: FlowSystemModel | None = None - self._connected = False + self._connected_and_transformed = False + self._used_in_calculation = False self._network_app = None - @classmethod - def from_dataset(cls, ds: xr.Dataset): - timesteps_extra = pd.DatetimeIndex(ds.attrs['timesteps_extra'], name='time') - hours_of_last_timestep = TimeSeriesCollection.calculate_hours_per_timestep(timesteps_extra).isel(time=-1).item() - - flow_system = FlowSystem( - timesteps=timesteps_extra[:-1], - hours_of_last_timestep=hours_of_last_timestep, - hours_of_previous_timesteps=ds.attrs['hours_of_previous_timesteps'], + # Use properties to validate and store scenario dimension settings + self.scenario_independent_sizes = scenario_independent_sizes + self.scenario_independent_flow_rates = scenario_independent_flow_rates + + @staticmethod + def _validate_timesteps(timesteps: pd.DatetimeIndex) -> pd.DatetimeIndex: + """Validate timesteps format and rename if needed.""" + if not isinstance(timesteps, pd.DatetimeIndex): + raise TypeError('timesteps must be a pandas DatetimeIndex') + if len(timesteps) < 2: + raise ValueError('timesteps must contain at least 2 timestamps') + if timesteps.name != 'time': + timesteps.name = 'time' + if not timesteps.is_monotonic_increasing: + raise ValueError('timesteps must be sorted') + return timesteps + + @staticmethod + def _validate_scenarios(scenarios: pd.Index) -> pd.Index: + """ + Validate and prepare scenario index. + + Args: + scenarios: The scenario index to validate + """ + if not isinstance(scenarios, pd.Index) or len(scenarios) == 0: + raise ConversionError('Scenarios must be a non-empty Index') + + if scenarios.name != 'scenario': + scenarios = scenarios.rename('scenario') + + return scenarios + + @staticmethod + def _validate_periods(periods: pd.Index) -> pd.Index: + """ + Validate and prepare period index. + + Args: + periods: The period index to validate + """ + if not isinstance(periods, pd.Index) or len(periods) == 0: + raise ConversionError(f'Periods must be a non-empty Index. Got {periods}') + + if not ( + periods.dtype.kind == 'i' # integer dtype + and periods.is_monotonic_increasing # rising + and periods.is_unique + ): + raise ConversionError(f'Periods must be a monotonically increasing and unique Index. Got {periods}') + + if periods.name != 'period': + periods = periods.rename('period') + + return periods + + @staticmethod + def _create_timesteps_with_extra( + timesteps: pd.DatetimeIndex, hours_of_last_timestep: float | None + ) -> pd.DatetimeIndex: + """Create timesteps with an extra step at the end.""" + if hours_of_last_timestep is None: + hours_of_last_timestep = (timesteps[-1] - timesteps[-2]) / pd.Timedelta(hours=1) + + last_date = pd.DatetimeIndex([timesteps[-1] + pd.Timedelta(hours=hours_of_last_timestep)], name='time') + return pd.DatetimeIndex(timesteps.append(last_date), name='time') + + @staticmethod + def calculate_hours_per_timestep(timesteps_extra: pd.DatetimeIndex) -> xr.DataArray: + """Calculate duration of each timestep as a 1D DataArray.""" + hours_per_step = np.diff(timesteps_extra) / pd.Timedelta(hours=1) + return xr.DataArray( + hours_per_step, coords={'time': timesteps_extra[:-1]}, dims='time', name='hours_per_timestep' ) - structure = fx_io.insert_dataarray({key: ds.attrs[key] for key in ['components', 'buses', 'effects']}, ds) - flow_system.add_elements( - *[Bus.from_dict(bus) for bus in structure['buses'].values()] - + [Effect.from_dict(effect) for effect in structure['effects'].values()] - + [CLASS_REGISTRY[comp['__class__']].from_dict(comp) for comp in structure['components'].values()] + @staticmethod + def _calculate_hours_of_previous_timesteps( + timesteps: pd.DatetimeIndex, hours_of_previous_timesteps: float | np.ndarray | None + ) -> float | np.ndarray: + """Calculate duration of regular timesteps.""" + if hours_of_previous_timesteps is not None: + return hours_of_previous_timesteps + # Calculate from the first interval + first_interval = timesteps[1] - timesteps[0] + return first_interval.total_seconds() / 3600 # Convert to hours + + def _create_reference_structure(self) -> tuple[dict, dict[str, xr.DataArray]]: + """ + Override Interface method to handle FlowSystem-specific serialization. + Combines custom FlowSystem logic with Interface pattern for nested objects. + + Returns: + Tuple of (reference_structure, extracted_arrays_dict) + """ + # Start with Interface base functionality for constructor parameters + reference_structure, all_extracted_arrays = super()._create_reference_structure() + + # Remove timesteps, as it's directly stored in dataset index + reference_structure.pop('timesteps', None) + + # Extract from components + components_structure = {} + for comp_label, component in self.components.items(): + comp_structure, comp_arrays = component._create_reference_structure() + all_extracted_arrays.update(comp_arrays) + components_structure[comp_label] = comp_structure + reference_structure['components'] = components_structure + + # Extract from buses + buses_structure = {} + for bus_label, bus in self.buses.items(): + bus_structure, bus_arrays = bus._create_reference_structure() + all_extracted_arrays.update(bus_arrays) + buses_structure[bus_label] = bus_structure + reference_structure['buses'] = buses_structure + + # Extract from effects + effects_structure = {} + for effect in self.effects: + effect_structure, effect_arrays = effect._create_reference_structure() + all_extracted_arrays.update(effect_arrays) + effects_structure[effect.label] = effect_structure + reference_structure['effects'] = effects_structure + + return reference_structure, all_extracted_arrays + + def to_dataset(self) -> xr.Dataset: + """ + Convert the FlowSystem to an xarray Dataset. + Ensures FlowSystem is connected before serialization. + + Returns: + xr.Dataset: Dataset containing all DataArrays with structure in attributes + """ + if not self.connected_and_transformed: + logger.warning('FlowSystem is not connected_and_transformed. Connecting and transforming data now.') + self.connect_and_transform() + + return super().to_dataset() + + @classmethod + def from_dataset(cls, ds: xr.Dataset) -> FlowSystem: + """ + Create a FlowSystem from an xarray Dataset. + Handles FlowSystem-specific reconstruction logic. + + Args: + ds: Dataset containing the FlowSystem data + + Returns: + FlowSystem instance + """ + # Get the reference structure from attrs + reference_structure = dict(ds.attrs) + + # Create arrays dictionary from dataset variables + arrays_dict = {name: array for name, array in ds.data_vars.items()} + + # Create FlowSystem instance with constructor parameters + flow_system = cls( + timesteps=ds.indexes['time'], + periods=ds.indexes.get('period'), + scenarios=ds.indexes.get('scenario'), + weights=cls._resolve_dataarray_reference(reference_structure['weights'], arrays_dict) + if 'weights' in reference_structure + else None, + hours_of_last_timestep=reference_structure.get('hours_of_last_timestep'), + hours_of_previous_timesteps=reference_structure.get('hours_of_previous_timesteps'), + scenario_independent_sizes=reference_structure.get('scenario_independent_sizes', True), + scenario_independent_flow_rates=reference_structure.get('scenario_independent_flow_rates', False), ) + + # Restore components + components_structure = reference_structure.get('components', {}) + for comp_label, comp_data in components_structure.items(): + component = cls._resolve_reference_structure(comp_data, arrays_dict) + if not isinstance(component, Component): + logger.critical(f'Restoring component {comp_label} failed.') + flow_system._add_components(component) + + # Restore buses + buses_structure = reference_structure.get('buses', {}) + for bus_label, bus_data in buses_structure.items(): + bus = cls._resolve_reference_structure(bus_data, arrays_dict) + if not isinstance(bus, Bus): + logger.critical(f'Restoring bus {bus_label} failed.') + flow_system._add_buses(bus) + + # Restore effects + effects_structure = reference_structure.get('effects', {}) + for effect_label, effect_data in effects_structure.items(): + effect = cls._resolve_reference_structure(effect_data, arrays_dict) + if not isinstance(effect, Effect): + logger.critical(f'Restoring effect {effect_label} failed.') + flow_system._add_effects(effect) + return flow_system - @classmethod - def from_dict(cls, data: dict) -> FlowSystem: + def to_netcdf(self, path: str | pathlib.Path, compression: int = 0): """ - Load a FlowSystem from a dictionary. + Save the FlowSystem to a NetCDF file. + Ensures FlowSystem is connected before saving. Args: - data: Dictionary containing the FlowSystem data. + path: The path to the netCDF file. + compression: The compression level to use when saving the file. """ - timesteps_extra = pd.DatetimeIndex(data['timesteps_extra'], name='time') - hours_of_last_timestep = TimeSeriesCollection.calculate_hours_per_timestep(timesteps_extra).isel(time=-1).item() + if not self.connected_and_transformed: + logger.warning('FlowSystem is not connected. Calling connect_and_transform() now.') + self.connect_and_transform() - flow_system = FlowSystem( - timesteps=timesteps_extra[:-1], - hours_of_last_timestep=hours_of_last_timestep, - hours_of_previous_timesteps=data['hours_of_previous_timesteps'], - ) + super().to_netcdf(path, compression) + logger.info(f'Saved FlowSystem to {path}') - flow_system.add_elements(*[Bus.from_dict(bus) for bus in data['buses'].values()]) + def get_structure(self, clean: bool = False, stats: bool = False) -> dict: + """ + Get FlowSystem structure. + Ensures FlowSystem is connected before getting structure. - flow_system.add_elements(*[Effect.from_dict(effect) for effect in data['effects'].values()]) + Args: + clean: If True, remove None and empty dicts and lists. + stats: If True, replace DataArray references with statistics + """ + if not self.connected_and_transformed: + logger.warning('FlowSystem is not connected. Calling connect_and_transform() now.') + self.connect_and_transform() - flow_system.add_elements( - *[CLASS_REGISTRY[comp['__class__']].from_dict(comp) for comp in data['components'].values()] - ) + return super().get_structure(clean, stats) - flow_system.transform_data() + def to_json(self, path: str | pathlib.Path): + """ + Save the flow system to a JSON file. + Ensures FlowSystem is connected before saving. - return flow_system + Args: + path: The path to the JSON file. + """ + if not self.connected_and_transformed: + logger.warning( + 'FlowSystem needs to be connected and transformed before saving to JSON. Calling connect_and_transform() now.' + ) + self.connect_and_transform() - @classmethod - def from_netcdf(cls, path: str | pathlib.Path): + super().to_json(path) + + def fit_to_model_coords( + self, + name: str, + data: TemporalDataUser | PeriodicDataUser | None, + dims: Collection[FlowSystemDimensions] | None = None, + ) -> TemporalData | PeriodicData | None: + """ + Fit data to model coordinate system (currently time, but extensible). + + Args: + name: Name of the data + data: Data to fit to model coordinates + dims: Collection of dimension names to use for fitting. If None, all dimensions are used. + + Returns: + xr.DataArray aligned to model coordinate system. If data is None, returns None. + """ + if data is None: + return None + + coords = self.coords + + if dims is not None: + coords = {k: coords[k] for k in dims if k in coords} + + # Rest of your method stays the same, just pass coords + if isinstance(data, TimeSeriesData): + try: + data.name = name # Set name of previous object! + return data.fit_to_coords(coords) + except ConversionError as e: + raise ConversionError( + f'Could not convert time series data "{name}" to DataArray:\n{data}\nOriginal Error: {e}' + ) from e + + try: + return DataConverter.to_dataarray(data, coords=coords).rename(name) + except ConversionError as e: + raise ConversionError(f'Could not convert data "{name}" to DataArray:\n{data}\nOriginal Error: {e}') from e + + def fit_effects_to_model_coords( + self, + label_prefix: str | None, + effect_values: TemporalEffectsUser | PeriodicEffectsUser | None, + label_suffix: str | None = None, + dims: Collection[FlowSystemDimensions] | None = None, + delimiter: str = '|', + ) -> TemporalEffects | PeriodicEffects | None: """ - Load a FlowSystem from a netcdf file + Transform EffectValues from the user to Internal Datatypes aligned with model coordinates. """ - return cls.from_dataset(fx_io.load_dataset_from_netcdf(path)) + if effect_values is None: + return None + + effect_values_dict = self.effects.create_effect_values_dict(effect_values) + + return { + effect: self.fit_to_model_coords( + str(delimiter).join(filter(None, [label_prefix, effect, label_suffix])), + value, + dims=dims, + ) + for effect, value in effect_values_dict.items() + } + + def connect_and_transform(self): + """Transform data for all elements using the new simplified approach.""" + if self.connected_and_transformed: + logger.debug('FlowSystem already connected and transformed') + return + + self.weights = self.fit_to_model_coords('weights', self.weights, dims=['period', 'scenario']) + + self._connect_network() + for element in list(self.components.values()) + list(self.effects.effects.values()) + list(self.buses.values()): + element.transform_data(self) + self._connected_and_transformed = True def add_elements(self, *elements: Element) -> None: """ @@ -147,12 +445,12 @@ def add_elements(self, *elements: Element) -> None: *elements: childs of Element like Boiler, HeatPump, Bus,... modeling Elements """ - if self._connected: + if self.connected_and_transformed: warnings.warn( 'You are adding elements to an already connected FlowSystem. This is not recommended (But it works).', stacklevel=2, ) - self._connected = False + self._connected_and_transformed = False for new_element in list(elements): if isinstance(new_element, Component): self._add_components(new_element) @@ -165,64 +463,19 @@ def add_elements(self, *elements: Element) -> None: f'Tried to add incompatible object to FlowSystem: {type(new_element)=}: {new_element=} ' ) - def to_json(self, path: str | pathlib.Path): - """ - Saves the flow system to a json file. - This not meant to be reloaded and recreate the object, - but rather used to document or compare the flow_system to others. - - Args: - path: The path to the json file. - """ - with open(path, 'w', encoding='utf-8') as f: - json.dump(self.as_dict('stats'), f, indent=4, ensure_ascii=False) - - def as_dict(self, data_mode: Literal['data', 'name', 'stats'] = 'data') -> dict: - """Convert the object to a dictionary representation.""" - data = { - 'components': { - comp.label: comp.to_dict() - for comp in sorted(self.components.values(), key=lambda component: component.label.upper()) - }, - 'buses': { - bus.label: bus.to_dict() for bus in sorted(self.buses.values(), key=lambda bus: bus.label.upper()) - }, - 'effects': { - effect.label: effect.to_dict() - for effect in sorted(self.effects, key=lambda effect: effect.label.upper()) - }, - 'timesteps_extra': [date.isoformat() for date in self.time_series_collection.timesteps_extra], - 'hours_of_previous_timesteps': self.time_series_collection.hours_of_previous_timesteps, - } - if data_mode == 'data': - return fx_io.replace_timeseries(data, 'data') - elif data_mode == 'stats': - return fx_io.remove_none_and_empty(fx_io.replace_timeseries(data, data_mode)) - return fx_io.replace_timeseries(data, data_mode) - - def as_dataset(self, constants_in_dataset: bool = False) -> xr.Dataset: + def create_model(self, normalize_weights: bool = True) -> FlowSystemModel: """ - Convert the FlowSystem to a xarray Dataset. + Create a linopy model from the FlowSystem. Args: - constants_in_dataset: If True, constants are included as Dataset variables. + normalize_weights: Whether to automatically normalize the weights (periods and scenarios) to sum up to 1 when solving. """ - ds = self.time_series_collection.to_dataset(include_constants=constants_in_dataset) - ds.attrs = self.as_dict(data_mode='name') - return ds - - def to_netcdf(self, path: str | pathlib.Path, compression: int = 0, constants_in_dataset: bool = True) -> None: - """ - Saves the FlowSystem to a netCDF file. - - Args: - path: The path to the netCDF file. - compression: The compression level to use when saving the file. - constants_in_dataset: If True, constants are included as Dataset variables. - """ - ds = self.as_dataset(constants_in_dataset=constants_in_dataset) - fx_io.save_dataset_to_netcdf(ds, path, compression=compression) - logger.info(f'Saved FlowSystem to {path}') + if not self.connected_and_transformed: + raise RuntimeError( + 'FlowSystem is not connected_and_transformed. Call FlowSystem.connect_and_transform() first.' + ) + self.model = FlowSystemModel(self, normalize_weights) + return self.model def plot_network( self, @@ -283,7 +536,7 @@ def start_network_app(self): f'Original error: {VISUALIZATION_ERROR}' ) - if not self._connected: + if not self._connected_and_transformed: self._connect_network() if self._network_app is not None: @@ -305,7 +558,7 @@ def stop_network_app(self): ) if self._network_app is None: - logger.warning('No network app is currently running. Cant stop it') + logger.warning("No network app is currently running. Can't stop it") return try: @@ -318,8 +571,8 @@ def stop_network_app(self): self._network_app = None def network_infos(self) -> tuple[dict[str, dict[str, str]], dict[str, dict[str, str]]]: - if not self._connected: - self._connect_network() + if not self.connected_and_transformed: + self.connect_and_transform() nodes = { node.label_full: { 'label': node.label, @@ -341,67 +594,6 @@ def network_infos(self) -> tuple[dict[str, dict[str, str]], dict[str, dict[str, return nodes, edges - def transform_data(self): - if not self._connected: - self._connect_network() - for element in self.all_elements.values(): - element.transform_data(self) - - def create_time_series( - self, - name: str, - data: NumericData | TimeSeriesData | TimeSeries | None, - needs_extra_timestep: bool = False, - ) -> TimeSeries | None: - """ - Tries to create a TimeSeries from NumericData Data and adds it to the time_series_collection - If the data already is a TimeSeries, nothing happens and the TimeSeries gets reset and returned - If the data is a TimeSeriesData, it is converted to a TimeSeries, and the aggregation weights are applied. - If the data is None, nothing happens. - """ - - if data is None: - return None - elif isinstance(data, TimeSeries): - data.restore_data() - if data in self.time_series_collection: - return data - return self.time_series_collection.create_time_series( - data=data.active_data, name=name, needs_extra_timestep=needs_extra_timestep - ) - return self.time_series_collection.create_time_series( - data=data, name=name, needs_extra_timestep=needs_extra_timestep - ) - - def create_effect_time_series( - self, - label_prefix: str | None, - effect_values: EffectValuesUser, - label_suffix: str | None = None, - ) -> EffectTimeSeries | None: - """ - Transform EffectValues to EffectTimeSeries. - Creates a TimeSeries for each key in the nested_values dictionary, using the value as the data. - - The resulting label of the TimeSeries is the label of the parent_element, - followed by the label of the Effect in the nested_values and the label_suffix. - If the key in the EffectValues is None, the alias 'Standard_Effect' is used - """ - effect_values_dict: EffectValuesDict | None = self.effects.create_effect_values_dict(effect_values) - if effect_values_dict is None: - return None - - return { - effect: self.create_time_series('|'.join(filter(None, [label_prefix, effect, label_suffix])), value) - for effect, value in effect_values_dict.items() - } - - def create_model(self) -> SystemModel: - if not self._connected: - raise RuntimeError('FlowSystem is not connected. Call FlowSystem.connect() first.') - self.model = SystemModel(self) - return self.model - def _check_if_element_is_unique(self, element: Element) -> None: """ checks if element or label of element already exists in list @@ -410,25 +602,25 @@ def _check_if_element_is_unique(self, element: Element) -> None: element: new element to check """ if element in self.all_elements.values(): - raise ValueError(f'Element {element.label} already added to FlowSystem!') + raise ValueError(f'Element {element.label_full} already added to FlowSystem!') # check if name is already used: if element.label_full in self.all_elements: - raise ValueError(f'Label of Element {element.label} already used in another element!') + raise ValueError(f'Label of Element {element.label_full} already used in another element!') def _add_effects(self, *args: Effect) -> None: self.effects.add_effects(*args) def _add_components(self, *components: Component) -> None: for new_component in list(components): - logger.info(f'Registered new Component: {new_component.label}') + logger.info(f'Registered new Component: {new_component.label_full}') self._check_if_element_is_unique(new_component) # check if already exists: - self.components[new_component.label] = new_component # Add to existing components + self.components[new_component.label_full] = new_component # Add to existing components def _add_buses(self, *buses: Bus): for new_bus in list(buses): - logger.info(f'Registered new Bus: {new_bus.label}') + logger.info(f'Registered new Bus: {new_bus.label_full}') self._check_if_element_is_unique(new_bus) # check if already exists: - self.buses[new_bus.label] = new_bus # Add to existing components + self.buses[new_bus.label_full] = new_bus # Add to existing components def _connect_network(self): """Connects the network of components and buses. Can be rerun without changes if no elements were added""" @@ -440,7 +632,7 @@ def _connect_network(self): # Add Bus if not already added (deprecated) if flow._bus_object is not None and flow._bus_object not in self.buses.values(): warnings.warn( - f'The Bus {flow._bus_object.label} was added to the FlowSystem from {flow.label_full}.' + f'The Bus {flow._bus_object.label_full} was added to the FlowSystem from {flow.label_full}.' f'This is deprecated and will be removed in the future. ' f'Please pass the Bus.label to the Flow and the Bus to the FlowSystem instead.', DeprecationWarning, @@ -463,17 +655,106 @@ def _connect_network(self): f'Connected {len(self.buses)} Buses and {len(self.components)} ' f'via {len(self.flows)} Flows inside the FlowSystem.' ) - self._connected = True - def __repr__(self): - return f'<{self.__class__.__name__} with {len(self.components)} components and {len(self.effects)} effects>' + def __repr__(self) -> str: + """Compact representation for debugging.""" + status = '✓' if self.connected_and_transformed else '⚠' + + # Build dimension info + dims = f'{len(self.timesteps)} timesteps [{self.timesteps[0].strftime("%Y-%m-%d")} to {self.timesteps[-1].strftime("%Y-%m-%d")}]' + if self.periods is not None: + dims += f', {len(self.periods)} periods' + if self.scenarios is not None: + dims += f', {len(self.scenarios)} scenarios' + + return f'FlowSystem({dims}, {len(self.components)} Components, {len(self.buses)} Buses, {len(self.effects)} Effects, {status})' + + def __str__(self) -> str: + """Structured summary for users.""" + + def format_elements(element_names: list, label: str, alignment: int = 12): + name_list = ', '.join(element_names[:3]) + if len(element_names) > 3: + name_list += f' ... (+{len(element_names) - 3} more)' + + suffix = f' ({name_list})' if element_names else '' + padding = alignment - len(label) - 1 # -1 for the colon + return f'{label}:{"":<{padding}} {len(element_names)}{suffix}' + + time_period = f'Time period: {self.timesteps[0].date()} to {self.timesteps[-1].date()}' + freq_str = str(self.timesteps.freq).replace('<', '').replace('>', '') if self.timesteps.freq else 'irregular' + + lines = [ + f'Timesteps: {len(self.timesteps)} ({freq_str}) [{time_period}]', + ] + + # Add periods if present + if self.periods is not None: + period_names = ', '.join(str(p) for p in self.periods[:3]) + if len(self.periods) > 3: + period_names += f' ... (+{len(self.periods) - 3} more)' + lines.append(f'Periods: {len(self.periods)} ({period_names})') + + # Add scenarios if present + if self.scenarios is not None: + scenario_names = ', '.join(str(s) for s in self.scenarios[:3]) + if len(self.scenarios) > 3: + scenario_names += f' ... (+{len(self.scenarios) - 3} more)' + lines.append(f'Scenarios: {len(self.scenarios)} ({scenario_names})') + + lines.extend( + [ + format_elements(list(self.components.keys()), 'Components'), + format_elements(list(self.buses.keys()), 'Buses'), + format_elements(list(self.effects.effects.keys()), 'Effects'), + f'Status: {"Connected & Transformed" if self.connected_and_transformed else "Not connected"}', + ] + ) + lines = ['FlowSystem:', f'{"─" * max(len(line) for line in lines)}'] + lines + + return '\n'.join(lines) + + def __eq__(self, other: FlowSystem): + """Check if two FlowSystems are equal by comparing their dataset representations.""" + if not isinstance(other, FlowSystem): + raise NotImplementedError('Comparison with other types is not implemented for class FlowSystem') + + ds_me = self.to_dataset() + ds_other = other.to_dataset() + + try: + xr.testing.assert_equal(ds_me, ds_other) + except AssertionError: + return False + + if ds_me.attrs != ds_other.attrs: + return False - def __str__(self): - with StringIO() as output_buffer: - console = Console(file=output_buffer, width=1000) # Adjust width as needed - console.print(Pretty(self.as_dict('stats'), expand_all=True, indent_guides=True)) - value = output_buffer.getvalue() - return value + return True + + def __getitem__(self, item) -> Element: + """Get element by exact label with helpful error messages.""" + if item in self.all_elements: + return self.all_elements[item] + + # Provide helpful error with suggestions + from difflib import get_close_matches + + suggestions = get_close_matches(item, self.all_elements.keys(), n=3, cutoff=0.6) + + if suggestions: + suggestion_str = ', '.join(f"'{s}'" for s in suggestions) + raise KeyError(f"Element '{item}' not found. Did you mean: {suggestion_str}?") + else: + raise KeyError(f"Element '{item}' not found in FlowSystem") + + def __contains__(self, item: str) -> bool: + """Check if element exists in the FlowSystem.""" + return item in self.all_elements + + def __iter__(self): + """Iterate over element labels.""" + return iter(self.all_elements.keys()) @property def flows(self) -> dict[str, Flow]: @@ -483,3 +764,217 @@ def flows(self) -> dict[str, Flow]: @property def all_elements(self) -> dict[str, Element]: return {**self.components, **self.effects.effects, **self.flows, **self.buses} + + @property + def coords(self) -> dict[FlowSystemDimensions, pd.Index]: + active_coords = {'time': self.timesteps} + if self.periods is not None: + active_coords['period'] = self.periods + if self.scenarios is not None: + active_coords['scenario'] = self.scenarios + return active_coords + + @property + def used_in_calculation(self) -> bool: + return self._used_in_calculation + + def _validate_scenario_parameter(self, value: bool | list[str], param_name: str, element_type: str) -> None: + """ + Validate scenario parameter value. + + Args: + value: The value to validate + param_name: Name of the parameter (for error messages) + element_type: Type of elements expected in list (e.g., 'component label_full', 'flow label_full') + + Raises: + TypeError: If value is not bool or list[str] + ValueError: If list contains non-string elements + """ + if isinstance(value, bool): + return # Valid + elif isinstance(value, list): + if not all(isinstance(item, str) for item in value): + raise ValueError(f'{param_name} list must contain only strings ({element_type} values)') + else: + raise TypeError(f'{param_name} must be bool or list[str], got {type(value).__name__}') + + @property + def scenario_independent_sizes(self) -> bool | list[str]: + """ + Controls whether investment sizes are equalized across scenarios. + + Returns: + bool or list[str]: Configuration for scenario-independent sizing + """ + return self._scenario_independent_sizes + + @scenario_independent_sizes.setter + def scenario_independent_sizes(self, value: bool | list[str]) -> None: + """ + Set whether investment sizes should be equalized across scenarios. + + Args: + value: True (all equalized), False (all vary), or list of component label_full strings to equalize + + Raises: + TypeError: If value is not bool or list[str] + ValueError: If list contains non-string elements + """ + self._validate_scenario_parameter(value, 'scenario_independent_sizes', 'Element.label_full') + self._scenario_independent_sizes = value + + @property + def scenario_independent_flow_rates(self) -> bool | list[str]: + """ + Controls whether flow rates are equalized across scenarios. + + Returns: + bool or list[str]: Configuration for scenario-independent flow rates + """ + return self._scenario_independent_flow_rates + + @scenario_independent_flow_rates.setter + def scenario_independent_flow_rates(self, value: bool | list[str]) -> None: + """ + Set whether flow rates should be equalized across scenarios. + + Args: + value: True (all equalized), False (all vary), or list of flow label_full strings to equalize + + Raises: + TypeError: If value is not bool or list[str] + ValueError: If list contains non-string elements + """ + self._validate_scenario_parameter(value, 'scenario_independent_flow_rates', 'Flow.label_full') + self._scenario_independent_flow_rates = value + + def sel( + self, + time: str | slice | list[str] | pd.Timestamp | pd.DatetimeIndex | None = None, + period: int | slice | list[int] | pd.Index | None = None, + scenario: str | slice | list[str] | pd.Index | None = None, + ) -> FlowSystem: + """ + Select a subset of the flowsystem by the time coordinate. + + Args: + time: Time selection (e.g., slice('2023-01-01', '2023-12-31'), '2023-06-15', or list of times) + period: Period selection (e.g., slice(2023, 2024), or list of periods) + scenario: Scenario selection (e.g., slice('scenario1', 'scenario2'), or list of scenarios) + + Returns: + FlowSystem: New FlowSystem with selected data + """ + if not self.connected_and_transformed: + self.connect_and_transform() + + ds = self.to_dataset() + + # Build indexers dict from non-None parameters + indexers = {} + if time is not None: + indexers['time'] = time + if period is not None: + indexers['period'] = period + if scenario is not None: + indexers['scenario'] = scenario + + if not indexers: + return self.copy() # Return a copy when no selection + + selected_dataset = ds.sel(**indexers) + return self.__class__.from_dataset(selected_dataset) + + def isel( + self, + time: int | slice | list[int] | None = None, + period: int | slice | list[int] | None = None, + scenario: int | slice | list[int] | None = None, + ) -> FlowSystem: + """ + Select a subset of the flowsystem by integer indices. + + Args: + time: Time selection by integer index (e.g., slice(0, 100), 50, or [0, 5, 10]) + period: Period selection by integer index (e.g., slice(0, 100), 50, or [0, 5, 10]) + scenario: Scenario selection by integer index (e.g., slice(0, 3), 50, or [0, 5, 10]) + + Returns: + FlowSystem: New FlowSystem with selected data + """ + if not self.connected_and_transformed: + self.connect_and_transform() + + ds = self.to_dataset() + + # Build indexers dict from non-None parameters + indexers = {} + if time is not None: + indexers['time'] = time + if period is not None: + indexers['period'] = period + if scenario is not None: + indexers['scenario'] = scenario + + if not indexers: + return self.copy() # Return a copy when no selection + + selected_dataset = ds.isel(**indexers) + return self.__class__.from_dataset(selected_dataset) + + def resample( + self, + time: str, + method: Literal['mean', 'sum', 'max', 'min', 'first', 'last', 'std', 'var', 'median', 'count'] = 'mean', + **kwargs: Any, + ) -> FlowSystem: + """ + Create a resampled FlowSystem by resampling data along the time dimension (like xr.Dataset.resample()). + Only resamples data variables that have a time dimension. + + Args: + time: Resampling frequency (e.g., '3h', '2D', '1M') + method: Resampling method. Recommended: 'mean', 'first', 'last', 'max', 'min' + **kwargs: Additional arguments passed to xarray.resample() + + Returns: + FlowSystem: New FlowSystem with resampled data + """ + if not self.connected_and_transformed: + self.connect_and_transform() + + dataset = self.to_dataset() + + # Separate variables with and without time dimension + time_vars = {} + non_time_vars = {} + + for var_name, var in dataset.data_vars.items(): + if 'time' in var.dims: + time_vars[var_name] = var + else: + non_time_vars[var_name] = var + + # Only resample variables that have time dimension + time_dataset = dataset[list(time_vars.keys())] + resampler = time_dataset.resample(time=time, **kwargs) + + if hasattr(resampler, method): + resampled_time_data = getattr(resampler, method)() + else: + available_methods = ['mean', 'sum', 'max', 'min', 'first', 'last', 'std', 'var', 'median', 'count'] + raise ValueError(f'Unsupported resampling method: {method}. Available: {available_methods}') + + # Combine resampled time variables with non-time variables + if non_time_vars: + non_time_dataset = dataset[list(non_time_vars.keys())] + resampled_dataset = xr.merge([resampled_time_data, non_time_dataset]) + else: + resampled_dataset = resampled_time_data + + return self.__class__.from_dataset(resampled_dataset) + + @property + def connected_and_transformed(self) -> bool: + return self._connected_and_transformed diff --git a/flixopt/interface.py b/flixopt/interface.py index 72737cc45..ab47c2522 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -6,7 +6,12 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING +import warnings +from typing import TYPE_CHECKING, Literal, Optional + +import numpy as np +import pandas as pd +import xarray as xr from .config import CONFIG from .structure import Interface, register_class_for_io @@ -14,8 +19,8 @@ if TYPE_CHECKING: # for type checking and preventing circular imports from collections.abc import Iterator - from .core import NumericData - from .effects import EffectValuesUser, EffectValuesUserScalar + from .core import PeriodicData, PeriodicDataUser, Scalar, TemporalDataUser + from .effects import PeriodicEffectsUser, TemporalEffectsUser from .flow_system import FlowSystem @@ -68,21 +73,21 @@ class Piece(Interface): """ - def __init__(self, start: NumericData, end: NumericData): + def __init__(self, start: TemporalDataUser, end: TemporalDataUser): self.start = start self.end = end + self.has_time_dim = False - def transform_data(self, flow_system: FlowSystem, name_prefix: str): - self.start = flow_system.create_time_series(f'{name_prefix}|start', self.start) - self.end = flow_system.create_time_series(f'{name_prefix}|end', self.end) + def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None: + dims = None if self.has_time_dim else ['period', 'scenario'] + self.start = flow_system.fit_to_model_coords(f'{name_prefix}|start', self.start, dims=dims) + self.end = flow_system.fit_to_model_coords(f'{name_prefix}|end', self.end, dims=dims) @register_class_for_io class Piecewise(Interface): - """Define a piecewise linear function by combining multiple `Piece`s together. - - This class creates complex non-linear relationships by combining multiple - Piece objects into a single piecewise linear function. + """ + Define a Piecewise, consisting of a list of Pieces. Args: pieces: list of Piece objects defining the linear segments. The arrangement @@ -192,6 +197,17 @@ class Piecewise(Interface): def __init__(self, pieces: list[Piece]): self.pieces = pieces + self._has_time_dim = False + + @property + def has_time_dim(self): + return self._has_time_dim + + @has_time_dim.setter + def has_time_dim(self, value): + self._has_time_dim = value + for piece in self.pieces: + piece.has_time_dim = value def __len__(self): """ @@ -208,7 +224,7 @@ def __getitem__(self, index) -> Piece: def __iter__(self) -> Iterator[Piece]: return iter(self.pieces) # Enables iteration like for piece in piecewise: ... - def transform_data(self, flow_system: FlowSystem, name_prefix: str): + def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None: for i, piece in enumerate(self.pieces): piece.transform_data(flow_system, f'{name_prefix}|Piece{i}') @@ -228,6 +244,10 @@ class PiecewiseConversion(Interface): When the equipment operates at a given point, ALL flows scale proportionally within their respective pieces. + Mathematical Formulation: + See the complete mathematical model in the documentation: + [Piecewise](../user-guide/mathematical-notation/features/Piecewise.md) + Args: piecewises: Dictionary mapping flow labels to their Piecewise functions. Keys are flow identifiers (e.g., 'electricity_in', 'heat_out', 'fuel_consumed'). @@ -408,6 +428,18 @@ class PiecewiseConversion(Interface): def __init__(self, piecewises: dict[str, Piecewise]): self.piecewises = piecewises + self._has_time_dim = True + self.has_time_dim = True # Initial propagation + + @property + def has_time_dim(self): + return self._has_time_dim + + @has_time_dim.setter + def has_time_dim(self, value): + self._has_time_dim = value + for piecewise in self.piecewises.values(): + piecewise.has_time_dim = value def items(self): """ @@ -418,7 +450,7 @@ def items(self): """ return self.piecewises.items() - def transform_data(self, flow_system: FlowSystem, name_prefix: str): + def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None: for name, piecewise in self.piecewises.items(): piecewise.transform_data(flow_system, f'{name_prefix}|{name}') @@ -616,12 +648,24 @@ class PiecewiseEffects(Interface): def __init__(self, piecewise_origin: Piecewise, piecewise_shares: dict[str, Piecewise]): self.piecewise_origin = piecewise_origin self.piecewise_shares = piecewise_shares + self._has_time_dim = False + self.has_time_dim = False # Initial propagation + + @property + def has_time_dim(self): + return self._has_time_dim - def transform_data(self, flow_system: FlowSystem, name_prefix: str): - raise NotImplementedError('PiecewiseEffects is not yet implemented for non scalar shares') - # self.piecewise_origin.transform_data(flow_system, f'{name_prefix}|PiecewiseEffects|origin') - # for name, piecewise in self.piecewise_shares.items(): - # piecewise.transform_data(flow_system, f'{name_prefix}|PiecewiseEffects|{name}') + @has_time_dim.setter + def has_time_dim(self, value): + self._has_time_dim = value + self.piecewise_origin.has_time_dim = value + for piecewise in self.piecewise_shares.values(): + piecewise.has_time_dim = value + + def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None: + self.piecewise_origin.transform_data(flow_system, f'{name_prefix}|PiecewiseEffects|origin') + for effect, piecewise in self.piecewise_shares.items(): + piecewise.transform_data(flow_system, f'{name_prefix}|PiecewiseEffects|{effect}') @register_class_for_io @@ -646,30 +690,41 @@ class InvestParameters(Interface): - **Piecewise Effects**: Non-linear relationships (bulk discounts, learning curves) - **Divestment Effects**: Penalties for not investing (demolition, opportunity costs) + Mathematical Formulation: + See the complete mathematical model in the documentation: + [InvestParameters](../user-guide/mathematical-notation/features/InvestParameters.md) + Args: - fixed_size: When specified, creates a binary investment decision at exactly - this size. When None, allows continuous sizing between minimum and maximum bounds. - minimum_size: Lower bound for continuous sizing decisions. Defaults to a small - positive value (CONFIG.Modeling.epsilon) to avoid numerical issues. - Ignored when fixed_size is specified. - maximum_size: Upper bound for continuous sizing decisions. Defaults to a large - value (CONFIG.Modeling.big) representing unlimited capacity. - Ignored when fixed_size is specified. - optional: Controls whether investment is required. When True (default), - optimization can choose not to invest. When False, forces investment + fixed_size: Creates binary decision at this exact size. None allows continuous sizing. + minimum_size: Lower bound for continuous sizing. Default: CONFIG.Modeling.epsilon. + Ignored if fixed_size is specified. + maximum_size: Upper bound for continuous sizing. Default: CONFIG.Modeling.big. + Ignored if fixed_size is specified. + mandatory: Controls whether investment is required. When True, forces investment to occur (useful for mandatory upgrades or replacement decisions). - fix_effects: Fixed costs incurred once if investment is made, regardless - of size. Dictionary mapping effect names to values - (e.g., {'cost': 10000, 'CO2_construction': 500}). - specific_effects: Variable costs proportional to investment size, representing - per-unit costs (€/kW, €/m²). Dictionary mapping effect names to unit values - (e.g., {'cost': 1200, 'steel_required': 0.5}). - piecewise_effects: Non-linear cost relationships using PiecewiseEffects for - economies of scale, learning curves, or threshold effects. Can be combined - with fix_effects and specific_effects. - divest_effects: Costs incurred if the investment is NOT made, such as - demolition of existing equipment, contractual penalties, or lost opportunities. - Dictionary mapping effect names to values. + When False (default), optimization can choose not to invest. + With multiple periods, at least one period has to have an investment. + effects_of_investment: Fixed costs if investment is made, regardless of size. + Dict: {'effect_name': value} (e.g., {'cost': 10000}). + effects_of_investment_per_size: Variable costs proportional to size (per-unit costs). + Dict: {'effect_name': value/unit} (e.g., {'cost': 1200}). + piecewise_effects_of_investment: Non-linear costs using PiecewiseEffects. + Combinable with effects_of_investment and effects_of_investment_per_size. + effects_of_retirement: Costs incurred if NOT investing (demolition, penalties). + Dict: {'effect_name': value}. + + Deprecated Args: + fix_effects: **Deprecated**. Use `effects_of_investment` instead. + Will be removed in version 4.0. + specific_effects: **Deprecated**. Use `effects_of_investment_per_size` instead. + Will be removed in version 4.0. + divest_effects: **Deprecated**. Use `effects_of_retirement` instead. + Will be removed in version 4.0. + piecewise_effects: **Deprecated**. Use `piecewise_effects_of_investment` instead. + Will be removed in version 4.0. + optional: DEPRECATED. Use `mandatory` instead. Opposite of `mandatory`. + Will be removed in version 4.0. + linked_periods: Describes which periods are linked. 1 means linked, 0 means size=0. None means no linked periods. Cost Annualization Requirements: All cost values must be properly weighted to match the optimization model's time horizon. @@ -687,12 +742,12 @@ class InvestParameters(Interface): ```python solar_investment = InvestParameters( fixed_size=100, # 100 kW system (binary decision) - optional=True, - fix_effects={ + mandatory=False, # Investment is optional + effects_of_investment={ 'cost': 25000, # Installation and permitting costs 'CO2': -50000, # Avoided emissions over lifetime }, - specific_effects={ + effects_of_investment_per_size={ 'cost': 1200, # €1200/kW for panels (annualized) 'CO2': -800, # kg CO2 avoided per kW annually }, @@ -705,12 +760,12 @@ class InvestParameters(Interface): battery_investment = InvestParameters( minimum_size=10, # Minimum viable system size (kWh) maximum_size=1000, # Maximum installable capacity - optional=True, - fix_effects={ + mandatory=False, # Investment is optional + effects_of_investment={ 'cost': 5000, # Grid connection and control system 'installation_time': 2, # Days for fixed components }, - piecewise_effects=PiecewiseEffects( + piecewise_effects_of_investment=PiecewiseEffects( piecewise_origin=Piecewise( [ Piece(0, 100), # Small systems @@ -731,22 +786,22 @@ class InvestParameters(Interface): ) ``` - Mandatory replacement with divestment costs: + Mandatory replacement with retirement costs: ```python boiler_replacement = InvestParameters( minimum_size=50, maximum_size=200, - optional=True, # Can choose not to replace - fix_effects={ + mandatory=False, # Can choose not to replace + effects_of_investment={ 'cost': 15000, # Installation costs 'disruption': 3, # Days of downtime }, - specific_effects={ + effects_of_investment_per_size={ 'cost': 400, # €400/kW capacity 'maintenance': 25, # Annual maintenance per kW }, - divest_effects={ + effects_of_retirement={ 'cost': 8000, # Demolition if not replaced 'environmental': 100, # Disposal fees }, @@ -759,16 +814,16 @@ class InvestParameters(Interface): # Gas turbine option gas_turbine = InvestParameters( fixed_size=50, # MW - fix_effects={'cost': 2500000, 'CO2': 1250000}, - specific_effects={'fuel_cost': 45, 'maintenance': 12}, + effects_of_investment={'cost': 2500000, 'CO2': 1250000}, + effects_of_investment_per_size={'fuel_cost': 45, 'maintenance': 12}, ) # Wind farm option wind_farm = InvestParameters( minimum_size=20, maximum_size=100, - fix_effects={'cost': 1000000, 'CO2': -5000000}, - specific_effects={'cost': 1800000, 'land_use': 0.5}, + effects_of_investment={'cost': 1000000, 'CO2': -5000000}, + effects_of_investment_per_size={'cost': 1800000, 'land_use': 0.5}, ) ``` @@ -778,7 +833,7 @@ class InvestParameters(Interface): hydrogen_electrolyzer = InvestParameters( minimum_size=1, maximum_size=50, # MW - piecewise_effects=PiecewiseEffects( + piecewise_effects_of_investment=PiecewiseEffects( piecewise_origin=Piecewise( [ Piece(0, 5), # Small scale: early adoption @@ -818,36 +873,188 @@ class InvestParameters(Interface): def __init__( self, - fixed_size: int | float | None = None, - minimum_size: int | float | None = None, - maximum_size: int | float | None = None, - optional: bool = True, # Investition ist weglassbar - fix_effects: EffectValuesUserScalar | None = None, - specific_effects: EffectValuesUserScalar | None = None, # costs per Flow-Unit/Storage-Size/... - piecewise_effects: PiecewiseEffects | None = None, - divest_effects: EffectValuesUserScalar | None = None, + fixed_size: PeriodicDataUser | None = None, + minimum_size: PeriodicDataUser | None = None, + maximum_size: PeriodicDataUser | None = None, + mandatory: bool = False, + effects_of_investment: PeriodicEffectsUser | None = None, + effects_of_investment_per_size: PeriodicEffectsUser | None = None, + effects_of_retirement: PeriodicEffectsUser | None = None, + piecewise_effects_of_investment: PiecewiseEffects | None = None, + linked_periods: PeriodicDataUser | tuple[int, int] | None = None, + **kwargs, ): - self.fix_effects: EffectValuesUserScalar = fix_effects or {} - self.divest_effects: EffectValuesUserScalar = divest_effects or {} + # Handle deprecated parameters using centralized helper + effects_of_investment = self._handle_deprecated_kwarg( + kwargs, 'fix_effects', 'effects_of_investment', effects_of_investment + ) + effects_of_investment_per_size = self._handle_deprecated_kwarg( + kwargs, 'specific_effects', 'effects_of_investment_per_size', effects_of_investment_per_size + ) + effects_of_retirement = self._handle_deprecated_kwarg( + kwargs, 'divest_effects', 'effects_of_retirement', effects_of_retirement + ) + piecewise_effects_of_investment = self._handle_deprecated_kwarg( + kwargs, 'piecewise_effects', 'piecewise_effects_of_investment', piecewise_effects_of_investment + ) + # For mandatory parameter with non-None default, disable conflict checking + if 'optional' in kwargs: + warnings.warn( + 'Deprecated parameter "optional" used. Check conflicts with new parameter "mandatory" manually!', + DeprecationWarning, + stacklevel=2, + ) + mandatory = self._handle_deprecated_kwarg( + kwargs, 'optional', 'mandatory', mandatory, transform=lambda x: not x, check_conflict=False + ) + + # Validate any remaining unexpected kwargs + self._validate_kwargs(kwargs) + + self.effects_of_investment: PeriodicEffectsUser = ( + effects_of_investment if effects_of_investment is not None else {} + ) + self.effects_of_retirement: PeriodicEffectsUser = ( + effects_of_retirement if effects_of_retirement is not None else {} + ) self.fixed_size = fixed_size - self.optional = optional - self.specific_effects: EffectValuesUserScalar = specific_effects or {} - self.piecewise_effects = piecewise_effects - self._minimum_size = minimum_size if minimum_size is not None else CONFIG.Modeling.epsilon - self._maximum_size = maximum_size if maximum_size is not None else CONFIG.Modeling.big # default maximum + self.mandatory = mandatory + self.effects_of_investment_per_size: PeriodicEffectsUser = ( + effects_of_investment_per_size if effects_of_investment_per_size is not None else {} + ) + self.piecewise_effects_of_investment = piecewise_effects_of_investment + self.minimum_size = minimum_size if minimum_size is not None else CONFIG.Modeling.epsilon + self.maximum_size = maximum_size if maximum_size is not None else CONFIG.Modeling.big # default maximum + self.linked_periods = linked_periods + + def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None: + self.effects_of_investment = flow_system.fit_effects_to_model_coords( + label_prefix=name_prefix, + effect_values=self.effects_of_investment, + label_suffix='effects_of_investment', + dims=['period', 'scenario'], + ) + self.effects_of_retirement = flow_system.fit_effects_to_model_coords( + label_prefix=name_prefix, + effect_values=self.effects_of_retirement, + label_suffix='effects_of_retirement', + dims=['period', 'scenario'], + ) + self.effects_of_investment_per_size = flow_system.fit_effects_to_model_coords( + label_prefix=name_prefix, + effect_values=self.effects_of_investment_per_size, + label_suffix='effects_of_investment_per_size', + dims=['period', 'scenario'], + ) - def transform_data(self, flow_system: FlowSystem): - self.fix_effects = flow_system.effects.create_effect_values_dict(self.fix_effects) - self.divest_effects = flow_system.effects.create_effect_values_dict(self.divest_effects) - self.specific_effects = flow_system.effects.create_effect_values_dict(self.specific_effects) + if self.piecewise_effects_of_investment is not None: + self.piecewise_effects_of_investment.has_time_dim = False + self.piecewise_effects_of_investment.transform_data(flow_system, f'{name_prefix}|PiecewiseEffects') + + self.minimum_size = flow_system.fit_to_model_coords( + f'{name_prefix}|minimum_size', self.minimum_size, dims=['period', 'scenario'] + ) + self.maximum_size = flow_system.fit_to_model_coords( + f'{name_prefix}|maximum_size', self.maximum_size, dims=['period', 'scenario'] + ) + # Convert tuple (first_period, last_period) to DataArray if needed + if isinstance(self.linked_periods, (tuple, list)): + if len(self.linked_periods) != 2: + raise TypeError( + f'If you provide a tuple to "linked_periods", it needs to be len=2. Got {len(self.linked_periods)=}' + ) + logger.debug(f'Computing linked_periods from {self.linked_periods}') + start, end = self.linked_periods + if start not in flow_system.periods.values: + logger.warning( + f'Start of linked periods ({start} not found in periods directly: {flow_system.periods.values}' + ) + if end not in flow_system.periods.values: + logger.warning( + f'End of linked periods ({end} not found in periods directly: {flow_system.periods.values}' + ) + self.linked_periods = self.compute_linked_periods(start, end, flow_system.periods) + logger.debug(f'Computed {self.linked_periods=}') + + self.linked_periods = flow_system.fit_to_model_coords( + f'{name_prefix}|linked_periods', self.linked_periods, dims=['period', 'scenario'] + ) + self.fixed_size = flow_system.fit_to_model_coords( + f'{name_prefix}|fixed_size', self.fixed_size, dims=['period', 'scenario'] + ) @property - def minimum_size(self): - return self.fixed_size or self._minimum_size + def optional(self) -> bool: + """DEPRECATED: Use 'mandatory' property instead. Returns the opposite of 'mandatory'.""" + import warnings + + warnings.warn("Property 'optional' is deprecated. Use 'mandatory' instead.", DeprecationWarning, stacklevel=2) + return not self.mandatory + + @optional.setter + def optional(self, value: bool): + """DEPRECATED: Use 'mandatory' property instead. Sets the opposite of the given value to 'mandatory'.""" + warnings.warn("Property 'optional' is deprecated. Use 'mandatory' instead.", DeprecationWarning, stacklevel=2) + self.mandatory = not value @property - def maximum_size(self): - return self.fixed_size or self._maximum_size + def fix_effects(self) -> PeriodicEffectsUser: + """Deprecated property. Use effects_of_investment instead.""" + warnings.warn( + 'The fix_effects property is deprecated. Use effects_of_investment instead.', + DeprecationWarning, + stacklevel=2, + ) + return self.effects_of_investment + + @property + def specific_effects(self) -> PeriodicEffectsUser: + """Deprecated property. Use effects_of_investment_per_size instead.""" + warnings.warn( + 'The specific_effects property is deprecated. Use effects_of_investment_per_size instead.', + DeprecationWarning, + stacklevel=2, + ) + return self.effects_of_investment_per_size + + @property + def divest_effects(self) -> PeriodicEffectsUser: + """Deprecated property. Use effects_of_retirement instead.""" + warnings.warn( + 'The divest_effects property is deprecated. Use effects_of_retirement instead.', + DeprecationWarning, + stacklevel=2, + ) + return self.effects_of_retirement + + @property + def piecewise_effects(self) -> PiecewiseEffects | None: + """Deprecated property. Use piecewise_effects_of_investment instead.""" + warnings.warn( + 'The piecewise_effects property is deprecated. Use piecewise_effects_of_investment instead.', + DeprecationWarning, + stacklevel=2, + ) + return self.piecewise_effects_of_investment + + @property + def minimum_or_fixed_size(self) -> PeriodicData: + return self.fixed_size if self.fixed_size is not None else self.minimum_size + + @property + def maximum_or_fixed_size(self) -> PeriodicData: + return self.fixed_size if self.fixed_size is not None else self.maximum_size + + @staticmethod + def compute_linked_periods(first_period: int, last_period: int, periods: pd.Index | list[int]) -> xr.DataArray: + return xr.DataArray( + xr.where( + (first_period <= np.array(periods)) & (np.array(periods) <= last_period), + 1, + 0, + ), + coords=(pd.Index(periods, name='period'),), + ).rename('linked_periods') @register_class_for_io @@ -872,6 +1079,10 @@ class OnOffParameters(Interface): - **Backup Equipment**: Emergency generators, standby systems - **Process Equipment**: Compressors, pumps with operational constraints + Mathematical Formulation: + See the complete mathematical model in the documentation: + [OnOffParameters](../user-guide/mathematical-notation/features/OnOffParameters.md) + Args: effects_per_switch_on: Costs or impacts incurred for each transition from off state (var_on=0) to on state (var_on=1). Represents startup costs, @@ -950,7 +1161,7 @@ class OnOffParameters(Interface): consecutive_on_hours_min=12, # Minimum batch size (12 hours) consecutive_on_hours_max=24, # Maximum batch size (24 hours) consecutive_off_hours_min=6, # Cleaning and setup time - switch_on_total_max=200, # Maximum 200 batches per year + switch_on_total_max=200, # Maximum 200 batches per period on_hours_total_max=4000, # Maximum production time ) ``` @@ -1030,47 +1241,60 @@ class OnOffParameters(Interface): def __init__( self, - effects_per_switch_on: EffectValuesUser | None = None, - effects_per_running_hour: EffectValuesUser | None = None, + effects_per_switch_on: TemporalEffectsUser | None = None, + effects_per_running_hour: TemporalEffectsUser | None = None, on_hours_total_min: int | None = None, on_hours_total_max: int | None = None, - consecutive_on_hours_min: NumericData | None = None, - consecutive_on_hours_max: NumericData | None = None, - consecutive_off_hours_min: NumericData | None = None, - consecutive_off_hours_max: NumericData | None = None, + consecutive_on_hours_min: TemporalDataUser | None = None, + consecutive_on_hours_max: TemporalDataUser | None = None, + consecutive_off_hours_min: TemporalDataUser | None = None, + consecutive_off_hours_max: TemporalDataUser | None = None, switch_on_total_max: int | None = None, force_switch_on: bool = False, ): - self.effects_per_switch_on: EffectValuesUser = effects_per_switch_on or {} - self.effects_per_running_hour: EffectValuesUser = effects_per_running_hour or {} - self.on_hours_total_min = on_hours_total_min - self.on_hours_total_max = on_hours_total_max - self.consecutive_on_hours_min = consecutive_on_hours_min - self.consecutive_on_hours_max = consecutive_on_hours_max - self.consecutive_off_hours_min = consecutive_off_hours_min - self.consecutive_off_hours_max = consecutive_off_hours_max - self.switch_on_total_max = switch_on_total_max + self.effects_per_switch_on: TemporalEffectsUser = ( + effects_per_switch_on if effects_per_switch_on is not None else {} + ) + self.effects_per_running_hour: TemporalEffectsUser = ( + effects_per_running_hour if effects_per_running_hour is not None else {} + ) + self.on_hours_total_min: Scalar = on_hours_total_min + self.on_hours_total_max: Scalar = on_hours_total_max + self.consecutive_on_hours_min: TemporalDataUser = consecutive_on_hours_min + self.consecutive_on_hours_max: TemporalDataUser = consecutive_on_hours_max + self.consecutive_off_hours_min: TemporalDataUser = consecutive_off_hours_min + self.consecutive_off_hours_max: TemporalDataUser = consecutive_off_hours_max + self.switch_on_total_max: Scalar = switch_on_total_max self.force_switch_on: bool = force_switch_on - def transform_data(self, flow_system: FlowSystem, name_prefix: str): - self.effects_per_switch_on = flow_system.create_effect_time_series( + def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None: + self.effects_per_switch_on = flow_system.fit_effects_to_model_coords( name_prefix, self.effects_per_switch_on, 'per_switch_on' ) - self.effects_per_running_hour = flow_system.create_effect_time_series( + self.effects_per_running_hour = flow_system.fit_effects_to_model_coords( name_prefix, self.effects_per_running_hour, 'per_running_hour' ) - self.consecutive_on_hours_min = flow_system.create_time_series( + self.consecutive_on_hours_min = flow_system.fit_to_model_coords( f'{name_prefix}|consecutive_on_hours_min', self.consecutive_on_hours_min ) - self.consecutive_on_hours_max = flow_system.create_time_series( + self.consecutive_on_hours_max = flow_system.fit_to_model_coords( f'{name_prefix}|consecutive_on_hours_max', self.consecutive_on_hours_max ) - self.consecutive_off_hours_min = flow_system.create_time_series( + self.consecutive_off_hours_min = flow_system.fit_to_model_coords( f'{name_prefix}|consecutive_off_hours_min', self.consecutive_off_hours_min ) - self.consecutive_off_hours_max = flow_system.create_time_series( + self.consecutive_off_hours_max = flow_system.fit_to_model_coords( f'{name_prefix}|consecutive_off_hours_max', self.consecutive_off_hours_max ) + self.on_hours_total_max = flow_system.fit_to_model_coords( + f'{name_prefix}|on_hours_total_max', self.on_hours_total_max, dims=['period', 'scenario'] + ) + self.on_hours_total_min = flow_system.fit_to_model_coords( + f'{name_prefix}|on_hours_total_min', self.on_hours_total_min, dims=['period', 'scenario'] + ) + self.switch_on_total_max = flow_system.fit_to_model_coords( + f'{name_prefix}|switch_on_total_max', self.switch_on_total_max, dims=['period', 'scenario'] + ) @property def use_off(self) -> bool: @@ -1089,16 +1313,14 @@ def use_consecutive_off_hours(self) -> bool: @property def use_switch_on(self) -> bool: - """Determines whether a Variable for SWITCH-ON is needed or not""" - return ( - any( - param not in (None, {}) - for param in [ - self.effects_per_switch_on, - self.switch_on_total_max, - self.on_hours_total_min, - self.on_hours_total_max, - ] - ) - or self.force_switch_on + """Determines whether a variable for switch_on is needed or not""" + if self.force_switch_on: + return True + + return any( + param is not None and param != {} + for param in [ + self.effects_per_switch_on, + self.switch_on_total_max, + ] ) diff --git a/flixopt/io.py b/flixopt/io.py index 314f693db..53d3d8e8a 100644 --- a/flixopt/io.py +++ b/flixopt/io.py @@ -11,50 +11,12 @@ import xarray as xr import yaml -from .core import TimeSeries - if TYPE_CHECKING: import linopy logger = logging.getLogger('flixopt') -def replace_timeseries(obj, mode: Literal['name', 'stats', 'data'] = 'name'): - """Recursively replaces TimeSeries objects with their names prefixed by '::::'.""" - if isinstance(obj, dict): - return {k: replace_timeseries(v, mode) for k, v in obj.items()} - elif isinstance(obj, list): - return [replace_timeseries(v, mode) for v in obj] - elif isinstance(obj, TimeSeries): # Adjust this based on the actual class - if obj.all_equal: - return obj.active_data.values[0].item() - elif mode == 'name': - return f'::::{obj.name}' - elif mode == 'stats': - return obj.stats - elif mode == 'data': - return obj - else: - raise ValueError(f'Invalid mode {mode}') - else: - return obj - - -def insert_dataarray(obj, ds: xr.Dataset): - """Recursively inserts TimeSeries objects into a dataset.""" - if isinstance(obj, dict): - return {k: insert_dataarray(v, ds) for k, v in obj.items()} - elif isinstance(obj, list): - return [insert_dataarray(v, ds) for v in obj] - elif isinstance(obj, str) and obj.startswith('::::'): - da = ds[obj[4:]] - if da.isel(time=-1).isnull(): - return da.isel(time=slice(0, -1)) - return da - else: - return obj - - def remove_none_and_empty(obj): """Recursively removes None and empty dicts and lists values from a dictionary or list.""" @@ -83,15 +45,17 @@ def _save_to_yaml(data, output_file='formatted_output.yaml'): output_file (str): Path to output YAML file """ # Process strings to normalize all newlines and handle special patterns - processed_data = _process_complex_strings(data) + processed_data = _normalize_complex_data(data) # Define a custom representer for strings def represent_str(dumper, data): - # Use literal block style (|) for any string with newlines + # Use literal block style (|) for multi-line strings if '\n' in data: + # Clean up formatting for literal block style + data = data.strip() # Remove leading/trailing whitespace return dumper.represent_scalar('tag:yaml.org,2002:str', data, style='|') - # Use quoted style for strings with special characters to ensure proper parsing + # Use quoted style for strings with special characters elif any(char in data for char in ':`{}[]#,&*!|>%@'): return dumper.represent_scalar('tag:yaml.org,2002:str', data, style='"') @@ -101,53 +65,80 @@ def represent_str(dumper, data): # Add the string representer to SafeDumper yaml.add_representer(str, represent_str, Dumper=yaml.SafeDumper) + # Configure dumper options for better formatting + class CustomDumper(yaml.SafeDumper): + def increase_indent(self, flow=False, indentless=False): + return super().increase_indent(flow, False) + # Write to file with settings that ensure proper formatting with open(output_file, 'w', encoding='utf-8') as file: yaml.dump( processed_data, file, - Dumper=yaml.SafeDumper, + Dumper=CustomDumper, sort_keys=False, # Preserve dictionary order default_flow_style=False, # Use block style for mappings - width=float('inf'), # Don't wrap long lines + width=1000, # Set a reasonable line width allow_unicode=True, # Support Unicode characters + indent=2, # Set consistent indentation ) -def _process_complex_strings(data): +def _normalize_complex_data(data): """ - Process dictionary data recursively with comprehensive string normalization. - Handles various types of strings and special formatting. + Recursively normalize strings in complex data structures. + + Handles dictionaries, lists, and strings, applying various text normalization + rules while preserving important formatting elements. Args: - data: The data to process (dict, list, str, or other) + data: Any data type (dict, list, str, or primitive) Returns: - Processed data with normalized strings + Data with all strings normalized according to defined rules """ if isinstance(data, dict): - return {k: _process_complex_strings(v) for k, v in data.items()} + return {key: _normalize_complex_data(value) for key, value in data.items()} + elif isinstance(data, list): - return [_process_complex_strings(item) for item in data] + return [_normalize_complex_data(item) for item in data] + elif isinstance(data, str): - # Step 1: Normalize line endings to \n - normalized = data.replace('\r\n', '\n').replace('\r', '\n') + return _normalize_string_content(data) + + else: + return data + + +def _normalize_string_content(text): + """ + Apply comprehensive string normalization rules. + + Args: + text: The string to normalize - # Step 2: Handle escaped newlines with robust regex - normalized = re.sub(r'(? dict[str, str]: @@ -211,7 +202,7 @@ def save_dataset_to_netcdf( engine: Literal['netcdf4', 'scipy', 'h5netcdf'] = 'h5netcdf', ) -> None: """ - Save a dataset to a netcdf file. Store the attrs as a json string in the 'attrs' attribute. + Save a dataset to a netcdf file. Store all attrs as JSON strings in 'attrs' attributes. Args: ds: Dataset to save. @@ -234,8 +225,20 @@ def save_dataset_to_netcdf( f'Dataset was exported without compression due to missing dependency "{engine}".' f'Install {engine} via `pip install {engine}`.' ) + ds = ds.copy(deep=True) ds.attrs = {'attrs': json.dumps(ds.attrs)} + + # Convert all DataArray attrs to JSON strings + for var_name, data_var in ds.data_vars.items(): + if data_var.attrs: # Only if there are attrs + ds[var_name].attrs = {'attrs': json.dumps(data_var.attrs)} + + # Also handle coordinate attrs if they exist + for coord_name, coord_var in ds.coords.items(): + if hasattr(coord_var, 'attrs') and coord_var.attrs: + ds[coord_name].attrs = {'attrs': json.dumps(coord_var.attrs)} + ds.to_netcdf( path, encoding=None @@ -247,16 +250,30 @@ def save_dataset_to_netcdf( def load_dataset_from_netcdf(path: str | pathlib.Path) -> xr.Dataset: """ - Load a dataset from a netcdf file. Load the attrs from the 'attrs' attribute. + Load a dataset from a netcdf file. Load all attrs from 'attrs' attributes. Args: path: Path to load the dataset from. Returns: - Dataset: Loaded dataset. + Dataset: Loaded dataset with restored attrs. """ ds = xr.load_dataset(str(path), engine='h5netcdf') - ds.attrs = json.loads(ds.attrs['attrs']) + + # Restore Dataset attrs + if 'attrs' in ds.attrs: + ds.attrs = json.loads(ds.attrs['attrs']) + + # Restore DataArray attrs + for var_name, data_var in ds.data_vars.items(): + if 'attrs' in data_var.attrs: + ds[var_name].attrs = json.loads(data_var.attrs['attrs']) + + # Restore coordinate attrs + for coord_name, coord_var in ds.coords.items(): + if hasattr(coord_var, 'attrs') and 'attrs' in coord_var.attrs: + ds[coord_name].attrs = json.loads(coord_var.attrs['attrs']) + return ds diff --git a/flixopt/linear_converters.py b/flixopt/linear_converters.py index aa2df9fc9..47c545506 100644 --- a/flixopt/linear_converters.py +++ b/flixopt/linear_converters.py @@ -10,7 +10,7 @@ import numpy as np from .components import LinearConverter -from .core import NumericDataTS, TimeSeriesData +from .core import TemporalDataUser, TimeSeriesData from .structure import register_class_for_io if TYPE_CHECKING: @@ -76,7 +76,7 @@ class Boiler(LinearConverter): def __init__( self, label: str, - eta: NumericDataTS, + eta: TemporalDataUser, Q_fu: Flow, Q_th: Flow, on_off_parameters: OnOffParameters | None = None, @@ -163,7 +163,7 @@ class Power2Heat(LinearConverter): def __init__( self, label: str, - eta: NumericDataTS, + eta: TemporalDataUser, P_el: Flow, Q_th: Flow, on_off_parameters: OnOffParameters | None = None, @@ -250,7 +250,7 @@ class HeatPump(LinearConverter): def __init__( self, label: str, - COP: NumericDataTS, + COP: TemporalDataUser, P_el: Flow, Q_th: Flow, on_off_parameters: OnOffParameters | None = None, @@ -339,7 +339,7 @@ class CoolingTower(LinearConverter): def __init__( self, label: str, - specific_electricity_demand: NumericDataTS, + specific_electricity_demand: TemporalDataUser, P_el: Flow, Q_th: Flow, on_off_parameters: OnOffParameters | None = None, @@ -349,7 +349,7 @@ def __init__( label, inputs=[P_el, Q_th], outputs=[], - conversion_factors=[{P_el.label: 1, Q_th.label: -specific_electricity_demand}], + conversion_factors=[{P_el.label: -1, Q_th.label: specific_electricity_demand}], on_off_parameters=on_off_parameters, meta_data=meta_data, ) @@ -361,12 +361,12 @@ def __init__( @property def specific_electricity_demand(self): - return -self.conversion_factors[0][self.Q_th.label] + return self.conversion_factors[0][self.Q_th.label] @specific_electricity_demand.setter def specific_electricity_demand(self, value): check_bounds(value, 'specific_electricity_demand', self.label_full, 0, 1) - self.conversion_factors[0][self.Q_th.label] = -value + self.conversion_factors[0][self.Q_th.label] = value @register_class_for_io @@ -437,8 +437,8 @@ class CHP(LinearConverter): def __init__( self, label: str, - eta_th: NumericDataTS, - eta_el: NumericDataTS, + eta_th: TemporalDataUser, + eta_el: TemporalDataUser, Q_fu: Flow, P_el: Flow, Q_th: Flow, @@ -551,7 +551,7 @@ class HeatPumpWithSource(LinearConverter): def __init__( self, label: str, - COP: NumericDataTS, + COP: TemporalDataUser, P_el: Flow, Q_ab: Flow, Q_th: Flow, @@ -589,11 +589,11 @@ def COP(self, value): # noqa: N802 def check_bounds( - value: NumericDataTS, + value: TemporalDataUser, parameter_label: str, element_label: str, - lower_bound: NumericDataTS, - upper_bound: NumericDataTS, + lower_bound: TemporalDataUser, + upper_bound: TemporalDataUser, ) -> None: """ Check if the value is within the bounds. The bounds are exclusive. diff --git a/flixopt/modeling.py b/flixopt/modeling.py new file mode 100644 index 000000000..88e652bc9 --- /dev/null +++ b/flixopt/modeling.py @@ -0,0 +1,759 @@ +import logging + +import linopy +import numpy as np +import xarray as xr + +from .config import CONFIG +from .core import TemporalData +from .structure import Submodel + +logger = logging.getLogger('flixopt') + + +class ModelingUtilitiesAbstract: + """Utility functions for modeling calculations - leveraging xarray for temporal data""" + + @staticmethod + def to_binary( + values: xr.DataArray, + epsilon: float | None = None, + dims: str | list[str] | None = None, + ) -> xr.DataArray: + """ + Converts a DataArray to binary {0, 1} values. + + Args: + values: Input DataArray to convert to binary + epsilon: Tolerance for zero detection (uses CONFIG.Modeling.epsilon if None) + dims: Dims to keep. Other dimensions are collapsed using .any() -> If any value is 1, all are 1. + + Returns: + Binary DataArray with same shape (or collapsed if collapse_non_time=True) + """ + if not isinstance(values, xr.DataArray): + values = xr.DataArray(values, dims=['time'], coords={'time': range(len(values))}) + + if epsilon is None: + epsilon = CONFIG.Modeling.epsilon + + if values.size == 0: + return xr.DataArray(0) if values.item() < epsilon else xr.DataArray(1) + + # Convert to binary states + binary_states = np.abs(values) >= epsilon + + # Optionally collapse dimensions using .any() + if dims is not None: + dims = [dims] if isinstance(dims, str) else dims + + binary_states = binary_states.any(dim=[d for d in binary_states.dims if d not in dims]) + + return binary_states.astype(int) + + @staticmethod + def count_consecutive_states( + binary_values: xr.DataArray | np.ndarray | list[int, float], + dim: str = 'time', + epsilon: float | None = None, + ) -> float: + """Count consecutive steps in the final active state of a binary time series. + + This function counts how many consecutive time steps the series remains "on" + (non-zero) at the end of the time series. If the final state is "off", returns 0. + + Args: + binary_values: Binary DataArray with values close to 0 (off) or 1 (on). + dim: Dimension along which to count consecutive states. + epsilon: Tolerance for zero detection. Uses CONFIG.Modeling.epsilon if None. + + Returns: + Sum of values in the final consecutive "on" period. Returns 0.0 if the + final state is "off". + + Examples: + >>> arr = xr.DataArray([0, 0, 1, 1, 1, 0, 1, 1], dims=['time']) + >>> ModelingUtilitiesAbstract.count_consecutive_states(arr) + 2.0 + + >>> arr = [0, 0, 1, 0, 1, 1, 1, 1] + >>> ModelingUtilitiesAbstract.count_consecutive_states(arr) + 4.0 + """ + epsilon = epsilon or CONFIG.Modeling.epsilon + + if isinstance(binary_values, xr.DataArray): + # xarray path + other_dims = [d for d in binary_values.dims if d != dim] + if other_dims: + binary_values = binary_values.any(dim=other_dims) + arr = binary_values.values + else: + # numpy/array-like path + arr = np.asarray(binary_values) + + # Flatten to 1D if needed + arr = arr.ravel() if arr.ndim > 1 else arr + + # Handle edge cases + if arr.size == 0: + return 0.0 + if arr.size == 1: + return float(arr[0]) if not np.isclose(arr[0], 0, atol=epsilon) else 0.0 + + # Return 0 if final state is off + if np.isclose(arr[-1], 0, atol=epsilon): + return 0.0 + + # Find the last zero position (treat NaNs as off) + arr = np.nan_to_num(arr, nan=0.0) + is_zero = np.isclose(arr, 0, atol=epsilon) + zero_indices = np.where(is_zero)[0] + + # Calculate sum from last zero to end + start_idx = zero_indices[-1] + 1 if zero_indices.size > 0 else 0 + + return float(np.sum(arr[start_idx:])) + + +class ModelingUtilities: + @staticmethod + def compute_consecutive_hours_in_state( + binary_values: TemporalData, + hours_per_timestep: int | float, + epsilon: float = None, + ) -> float: + """ + Computes the final consecutive duration in state 'on' (=1) in hours. + + Args: + binary_values: Binary DataArray with 'time' dim, or scalar/array + hours_per_timestep: Duration of each timestep in hours + epsilon: Tolerance for zero detection (uses CONFIG.Modeling.epsilon if None) + + Returns: + The duration of the final consecutive 'on' period in hours + """ + if not isinstance(hours_per_timestep, (int, float)): + raise TypeError(f'hours_per_timestep must be a scalar, got {type(hours_per_timestep)}') + + return ( + ModelingUtilitiesAbstract.count_consecutive_states(binary_values=binary_values, epsilon=epsilon) + * hours_per_timestep + ) + + @staticmethod + def compute_previous_states(previous_values: xr.DataArray | None, epsilon: float | None = None) -> xr.DataArray: + return ModelingUtilitiesAbstract.to_binary(values=previous_values, epsilon=epsilon, dims='time') + + @staticmethod + def compute_previous_on_duration( + previous_values: xr.DataArray, hours_per_step: xr.DataArray | float | int + ) -> float: + return ( + ModelingUtilitiesAbstract.count_consecutive_states(ModelingUtilitiesAbstract.to_binary(previous_values)) + * hours_per_step + ) + + @staticmethod + def compute_previous_off_duration( + previous_values: xr.DataArray, hours_per_step: xr.DataArray | float | int + ) -> float: + """ + Compute previous consecutive 'off' duration. + + Args: + previous_values: DataArray with 'time' dimension + hours_per_step: Duration of each timestep in hours + + Returns: + Previous consecutive off duration in hours + """ + if previous_values is None or previous_values.size == 0: + return 0.0 + + previous_states = ModelingUtilities.compute_previous_states(previous_values) + previous_off_states = 1 - previous_states + return ModelingUtilities.compute_consecutive_hours_in_state(previous_off_states, hours_per_step) + + @staticmethod + def get_most_recent_state(previous_values: xr.DataArray | None) -> int: + """ + Get the most recent binary state from previous values. + + Args: + previous_values: DataArray with 'time' dimension + + Returns: + Most recent binary state (0 or 1) + """ + if previous_values is None or previous_values.size == 0: + return 0 + + previous_states = ModelingUtilities.compute_previous_states(previous_values) + return int(previous_states.isel(time=-1).item()) + + +class ModelingPrimitives: + """Mathematical modeling primitives returning (variables, constraints) tuples""" + + @staticmethod + def expression_tracking_variable( + model: Submodel, + tracked_expression, + name: str = None, + short_name: str = None, + bounds: tuple[TemporalData, TemporalData] = None, + coords: str | list[str] | None = None, + ) -> tuple[linopy.Variable, linopy.Constraint]: + """ + Creates variable that equals a given expression. + + Mathematical formulation: + tracker = expression + lower ≤ tracker ≤ upper (if bounds provided) + + Returns: + variables: {'tracker': tracker_var} + constraints: {'tracking': constraint} + """ + if not isinstance(model, Submodel): + raise ValueError('ModelingPrimitives.expression_tracking_variable() can only be used with a Submodel') + + if not bounds: + tracker = model.add_variables(name=name, coords=model.get_coords(coords), short_name=short_name) + else: + tracker = model.add_variables( + lower=bounds[0] if bounds[0] is not None else -np.inf, + upper=bounds[1] if bounds[1] is not None else np.inf, + name=name, + coords=model.get_coords(coords), + short_name=short_name, + ) + + # Constraint: tracker = expression + tracking = model.add_constraints(tracker == tracked_expression, name=name, short_name=short_name) + + return tracker, tracking + + @staticmethod + def consecutive_duration_tracking( + model: Submodel, + state_variable: linopy.Variable, + name: str = None, + short_name: str = None, + minimum_duration: TemporalData | None = None, + maximum_duration: TemporalData | None = None, + duration_dim: str = 'time', + duration_per_step: int | float | TemporalData = None, + previous_duration: TemporalData = 0, + ) -> tuple[linopy.Variable, tuple[linopy.Constraint, linopy.Constraint, linopy.Constraint]]: + """ + Creates consecutive duration tracking for a binary state variable. + + Mathematical formulation: + duration[t] ≤ state[t] * M ∀t + duration[t+1] ≤ duration[t] + duration_per_step[t] ∀t + duration[t+1] ≥ duration[t] + duration_per_step[t] + (state[t+1] - 1) * M ∀t + duration[0] = (duration_per_step[0] + previous_duration) * state[0] + + If minimum_duration provided: + duration[t] ≥ (state[t-1] - state[t]) * minimum_duration[t-1] ∀t > 0 + + Args: + name: Name of the duration variable + state_variable: Binary state variable to track duration for + minimum_duration: Optional minimum consecutive duration + maximum_duration: Optional maximum consecutive duration + previous_duration: Duration from before first timestep + + Returns: + variables: {'duration': duration_var} + constraints: {'ub': constraint, 'forward': constraint, 'backward': constraint, ...} + """ + if not isinstance(model, Submodel): + raise ValueError('ModelingPrimitives.consecutive_duration_tracking() can only be used with a Submodel') + + mega = duration_per_step.sum(duration_dim) + previous_duration # Big-M value + + # Duration variable + duration = model.add_variables( + lower=0, + upper=maximum_duration if maximum_duration is not None else mega, + coords=state_variable.coords, + name=name, + short_name=short_name, + ) + + constraints = {} + + # Upper bound: duration[t] ≤ state[t] * M + constraints['ub'] = model.add_constraints(duration <= state_variable * mega, name=f'{duration.name}|ub') + + # Forward constraint: duration[t+1] ≤ duration[t] + duration_per_step[t] + constraints['forward'] = model.add_constraints( + duration.isel({duration_dim: slice(1, None)}) + <= duration.isel({duration_dim: slice(None, -1)}) + duration_per_step.isel({duration_dim: slice(None, -1)}), + name=f'{duration.name}|forward', + ) + + # Backward constraint: duration[t+1] ≥ duration[t] + duration_per_step[t] + (state[t+1] - 1) * M + constraints['backward'] = model.add_constraints( + duration.isel({duration_dim: slice(1, None)}) + >= duration.isel({duration_dim: slice(None, -1)}) + + duration_per_step.isel({duration_dim: slice(None, -1)}) + + (state_variable.isel({duration_dim: slice(1, None)}) - 1) * mega, + name=f'{duration.name}|backward', + ) + + # Initial condition: duration[0] = (duration_per_step[0] + previous_duration) * state[0] + constraints['initial'] = model.add_constraints( + duration.isel({duration_dim: 0}) + == (duration_per_step.isel({duration_dim: 0}) + previous_duration) * state_variable.isel({duration_dim: 0}), + name=f'{duration.name}|initial', + ) + + # Minimum duration constraint if provided + if minimum_duration is not None: + constraints['lb'] = model.add_constraints( + duration + >= ( + state_variable.isel({duration_dim: slice(None, -1)}) + - state_variable.isel({duration_dim: slice(1, None)}) + ) + * minimum_duration.isel({duration_dim: slice(None, -1)}), + name=f'{duration.name}|lb', + ) + + # Handle initial condition for minimum duration + prev = ( + float(previous_duration) + if not isinstance(previous_duration, xr.DataArray) + else float(previous_duration.max().item()) + ) + min0 = float(minimum_duration.isel({duration_dim: 0}).max().item()) + if prev > 0 and prev < min0: + constraints['initial_lb'] = model.add_constraints( + state_variable.isel({duration_dim: 0}) == 1, name=f'{duration.name}|initial_lb' + ) + + variables = {'duration': duration} + + return variables, constraints + + @staticmethod + def mutual_exclusivity_constraint( + model: Submodel, + binary_variables: list[linopy.Variable], + tolerance: float = 1, + short_name: str = 'mutual_exclusivity', + ) -> linopy.Constraint: + """ + Creates mutual exclusivity constraint for binary variables. + + Mathematical formulation: + Σ(binary_vars[i]) ≤ tolerance ∀t + + Ensures at most one binary variable can be 1 at any time. + Tolerance > 1.0 accounts for binary variable numerical precision. + + Args: + binary_variables: List of binary variables that should be mutually exclusive + tolerance: Upper bound + short_name: Short name of the constraint + + Returns: + variables: {} (no new variables created) + constraints: {'mutual_exclusivity': constraint} + + Raises: + AssertionError: If fewer than 2 variables provided or variables aren't binary + """ + if not isinstance(model, Submodel): + raise ValueError('ModelingPrimitives.mutual_exclusivity_constraint() can only be used with a Submodel') + + assert len(binary_variables) >= 2, ( + f'Mutual exclusivity requires at least 2 variables, got {len(binary_variables)}' + ) + + for var in binary_variables: + assert var.attrs.get('binary', False), ( + f'Variable {var.name} must be binary for mutual exclusivity constraint' + ) + + # Create mutual exclusivity constraint + mutual_exclusivity = model.add_constraints(sum(binary_variables) <= tolerance, short_name=short_name) + + return mutual_exclusivity + + +class BoundingPatterns: + """High-level patterns that compose primitives and return (variables, constraints) tuples""" + + @staticmethod + def basic_bounds( + model: Submodel, + variable: linopy.Variable, + bounds: tuple[TemporalData, TemporalData], + name: str = None, + ): + """Create simple bounds. + variable ∈ [lower_bound, upper_bound] + + Mathematical Formulation: + lower_bound ≤ variable ≤ upper_bound + + Args: + model: The optimization model instance + variable: Variable to be bounded + bounds: Tuple of (lower_bound, upper_bound) absolute bounds + + Returns: + Tuple containing: + - variables (Dict): Empty dict + - constraints (Dict[str, linopy.Constraint]): 'ub', 'lb' + """ + if not isinstance(model, Submodel): + raise ValueError('BoundingPatterns.basic_bounds() can only be used with a Submodel') + + lower_bound, upper_bound = bounds + name = name or f'{variable.name}' + + upper_constraint = model.add_constraints(variable <= upper_bound, name=f'{name}|ub') + lower_constraint = model.add_constraints(variable >= lower_bound, name=f'{name}|lb') + + return [lower_constraint, upper_constraint] + + @staticmethod + def bounds_with_state( + model: Submodel, + variable: linopy.Variable, + bounds: tuple[TemporalData, TemporalData], + variable_state: linopy.Variable, + name: str = None, + ) -> list[linopy.Constraint]: + """Constraint a variable to bounds, that can be escaped from to 0 by a binary variable. + variable ∈ {0, [max(ε, lower_bound), upper_bound]} + + Mathematical Formulation: + - variable_state * max(ε, lower_bound) ≤ variable ≤ variable_state * upper_bound + + Use Cases: + - Investment decisions + - Unit commitment (on/off states) + + Args: + model: The optimization model instance + variable: Variable to be bounded + bounds: Tuple of (lower_bound, upper_bound) absolute bounds + variable_state: Binary variable controlling the bounds + + Returns: + Tuple containing: + - variables (Dict): Empty dict + - constraints (Dict[str, linopy.Constraint]): 'ub', 'lb' + """ + if not isinstance(model, Submodel): + raise ValueError('BoundingPatterns.bounds_with_state() can only be used with a Submodel') + + lower_bound, upper_bound = bounds + name = name or f'{variable.name}' + + if np.allclose(lower_bound, upper_bound, atol=1e-10, equal_nan=True): + fix_constraint = model.add_constraints(variable == variable_state * upper_bound, name=f'{name}|fix') + return [fix_constraint] + + epsilon = np.maximum(CONFIG.Modeling.epsilon, lower_bound) + + upper_constraint = model.add_constraints(variable <= variable_state * upper_bound, name=f'{name}|ub') + lower_constraint = model.add_constraints(variable >= variable_state * epsilon, name=f'{name}|lb') + + return [lower_constraint, upper_constraint] + + @staticmethod + def scaled_bounds( + model: Submodel, + variable: linopy.Variable, + scaling_variable: linopy.Variable, + relative_bounds: tuple[TemporalData, TemporalData], + name: str = None, + ) -> list[linopy.Constraint]: + """Constraint a variable by scaling bounds, dependent on another variable. + variable ∈ [lower_bound * scaling_variable, upper_bound * scaling_variable] + + Mathematical Formulation: + scaling_variable * lower_factor ≤ variable ≤ scaling_variable * upper_factor + + Use Cases: + - Flow rates bounded by equipment capacity + - Production levels scaled by plant size + + Args: + model: The optimization model instance + variable: Variable to be bounded + scaling_variable: Variable that scales the bound factors + relative_bounds: Tuple of (lower_factor, upper_factor) relative to scaling variable + + Returns: + Tuple containing: + - variables (Dict): Empty dict + - constraints (Dict[str, linopy.Constraint]): 'ub', 'lb' + """ + if not isinstance(model, Submodel): + raise ValueError('BoundingPatterns.scaled_bounds() can only be used with a Submodel') + + rel_lower, rel_upper = relative_bounds + name = name or f'{variable.name}' + + if np.allclose(rel_lower, rel_upper, atol=1e-10, equal_nan=True): + return [model.add_constraints(variable == scaling_variable * rel_lower, name=f'{name}|fixed')] + + upper_constraint = model.add_constraints(variable <= scaling_variable * rel_upper, name=f'{name}|ub') + lower_constraint = model.add_constraints(variable >= scaling_variable * rel_lower, name=f'{name}|lb') + + return [lower_constraint, upper_constraint] + + @staticmethod + def scaled_bounds_with_state( + model: Submodel, + variable: linopy.Variable, + scaling_variable: linopy.Variable, + relative_bounds: tuple[TemporalData, TemporalData], + scaling_bounds: tuple[TemporalData, TemporalData], + variable_state: linopy.Variable, + name: str = None, + ) -> list[linopy.Constraint]: + """Constraint a variable by scaling bounds with binary state control. + + variable ∈ {0, [max(ε, lower_relative_bound) * scaling_variable, upper_relative_bound * scaling_variable]} + + Mathematical Formulation (Big-M): + (variable_state - 1) * M_misc + scaling_variable * rel_lower ≤ variable ≤ scaling_variable * rel_upper + variable_state * big_m_lower ≤ variable ≤ variable_state * big_m_upper + + Where: + M_misc = scaling_max * rel_lower + big_m_upper = scaling_max * rel_upper + big_m_lower = max(ε, scaling_min * rel_lower) + + Args: + model: The optimization model instance + variable: Variable to be bounded + scaling_variable: Variable that scales the bound factors + relative_bounds: Tuple of (lower_factor, upper_factor) relative to scaling variable + scaling_bounds: Tuple of (scaling_min, scaling_max) bounds of the scaling variable + variable_state: Binary variable for on/off control + name: Optional name prefix for constraints + + Returns: + List[linopy.Constraint]: List of constraint objects + """ + if not isinstance(model, Submodel): + raise ValueError('BoundingPatterns.scaled_bounds_with_state() can only be used with a Submodel') + + rel_lower, rel_upper = relative_bounds + scaling_min, scaling_max = scaling_bounds + name = name or f'{variable.name}' + + big_m_misc = scaling_max * rel_lower + + scaling_lower = model.add_constraints( + variable >= (variable_state - 1) * big_m_misc + scaling_variable * rel_lower, name=f'{name}|lb2' + ) + scaling_upper = model.add_constraints(variable <= scaling_variable * rel_upper, name=f'{name}|ub2') + + big_m_upper = rel_upper * scaling_max + big_m_lower = np.maximum(CONFIG.Modeling.epsilon, rel_lower * scaling_min) + + binary_upper = model.add_constraints(variable_state * big_m_upper >= variable, name=f'{name}|ub1') + binary_lower = model.add_constraints(variable_state * big_m_lower <= variable, name=f'{name}|lb1') + + return [scaling_lower, scaling_upper, binary_lower, binary_upper] + + @staticmethod + def state_transition_bounds( + model: Submodel, + state_variable: linopy.Variable, + switch_on: linopy.Variable, + switch_off: linopy.Variable, + name: str, + previous_state=0, + coord: str = 'time', + ) -> tuple[linopy.Constraint, linopy.Constraint, linopy.Constraint]: + """ + Creates switch-on/off variables with state transition logic. + + Mathematical formulation: + switch_on[t] - switch_off[t] = state[t] - state[t-1] ∀t > 0 + switch_on[0] - switch_off[0] = state[0] - previous_state + switch_on[t] + switch_off[t] ≤ 1 ∀t + switch_on[t], switch_off[t] ∈ {0, 1} + + Returns: + variables: {'switch_on': binary_var, 'switch_off': binary_var} + constraints: {'transition': constraint, 'initial': constraint, 'mutex': constraint} + """ + if not isinstance(model, Submodel): + raise ValueError('ModelingPrimitives.state_transition_bounds() can only be used with a Submodel') + + # State transition constraints for t > 0 + transition = model.add_constraints( + switch_on.isel({coord: slice(1, None)}) - switch_off.isel({coord: slice(1, None)}) + == state_variable.isel({coord: slice(1, None)}) - state_variable.isel({coord: slice(None, -1)}), + name=f'{name}|transition', + ) + + # Initial state transition for t = 0 + initial = model.add_constraints( + switch_on.isel({coord: 0}) - switch_off.isel({coord: 0}) + == state_variable.isel({coord: 0}) - previous_state, + name=f'{name}|initial', + ) + + # At most one switch per timestep + mutex = model.add_constraints(switch_on + switch_off <= 1, name=f'{name}|mutex') + + return transition, initial, mutex + + @staticmethod + def continuous_transition_bounds( + model: Submodel, + continuous_variable: linopy.Variable, + switch_on: linopy.Variable, + switch_off: linopy.Variable, + name: str, + max_change: float | xr.DataArray, + previous_value: float | xr.DataArray = 0.0, + coord: str = 'time', + ) -> tuple[linopy.Constraint, linopy.Constraint, linopy.Constraint, linopy.Constraint]: + """ + Constrains a continuous variable to only change when switch variables are active. + + Mathematical formulation: + -max_change * (switch_on[t] + switch_off[t]) <= continuous[t] - continuous[t-1] <= max_change * (switch_on[t] + switch_off[t]) ∀t > 0 + -max_change * (switch_on[0] + switch_off[0]) <= continuous[0] - previous_value <= max_change * (switch_on[0] + switch_off[0]) + switch_on[t], switch_off[t] ∈ {0, 1} + + This ensures the continuous variable can only change when switch_on or switch_off is 1. + When both switches are 0, the variable must stay exactly constant. + + Args: + model: The submodel to add constraints to + continuous_variable: The continuous variable to constrain + switch_on: Binary variable indicating when changes are allowed (typically transitions to active state) + switch_off: Binary variable indicating when changes are allowed (typically transitions to inactive state) + name: Base name for the constraints + max_change: Maximum possible change in the continuous variable (Big-M value) + previous_value: Initial value of the continuous variable before first period + coord: Coordinate name for time dimension + + Returns: + Tuple of constraints: (transition_upper, transition_lower, initial_upper, initial_lower) + """ + if not isinstance(model, Submodel): + raise ValueError('ModelingPrimitives.continuous_transition_bounds() can only be used with a Submodel') + + # Transition constraints for t > 0: continuous variable can only change when switches are active + transition_upper = model.add_constraints( + continuous_variable.isel({coord: slice(1, None)}) - continuous_variable.isel({coord: slice(None, -1)}) + <= max_change * (switch_on.isel({coord: slice(1, None)}) + switch_off.isel({coord: slice(1, None)})), + name=f'{name}|transition_ub', + ) + + transition_lower = model.add_constraints( + -(continuous_variable.isel({coord: slice(1, None)}) - continuous_variable.isel({coord: slice(None, -1)})) + <= max_change * (switch_on.isel({coord: slice(1, None)}) + switch_off.isel({coord: slice(1, None)})), + name=f'{name}|transition_lb', + ) + + # Initial constraints for t = 0 + initial_upper = model.add_constraints( + continuous_variable.isel({coord: 0}) - previous_value + <= max_change * (switch_on.isel({coord: 0}) + switch_off.isel({coord: 0})), + name=f'{name}|initial_ub', + ) + + initial_lower = model.add_constraints( + -continuous_variable.isel({coord: 0}) + previous_value + <= max_change * (switch_on.isel({coord: 0}) + switch_off.isel({coord: 0})), + name=f'{name}|initial_lb', + ) + + return transition_upper, transition_lower, initial_upper, initial_lower + + @staticmethod + def link_changes_to_level_with_binaries( + model: Submodel, + level_variable: linopy.Variable, + increase_variable: linopy.Variable, + decrease_variable: linopy.Variable, + increase_binary: linopy.Variable, + decrease_binary: linopy.Variable, + name: str, + max_change: float | xr.DataArray, + initial_level: float | xr.DataArray = 0.0, + coord: str = 'period', + ) -> tuple[linopy.Constraint, linopy.Constraint, linopy.Constraint, linopy.Constraint, linopy.Constraint]: + """ + Link changes to level evolution with binary control and mutual exclusivity. + + Creates the complete constraint system for ALL time periods: + 1. level[0] = initial_level + increase[0] - decrease[0] + 2. level[t] = level[t-1] + increase[t] - decrease[t] ∀t > 0 + 3. increase[t] <= max_change * increase_binary[t] ∀t + 4. decrease[t] <= max_change * decrease_binary[t] ∀t + 5. increase_binary[t] + decrease_binary[t] <= 1 ∀t + + Args: + model: The submodel to add constraints to + increase_variable: Incremental additions for ALL periods (>= 0) + decrease_variable: Incremental reductions for ALL periods (>= 0) + increase_binary: Binary indicators for increases for ALL periods + decrease_binary: Binary indicators for decreases for ALL periods + level_variable: Level variable for ALL periods + name: Base name for constraints + max_change: Maximum change per period + initial_level: Starting level before first period + coord: Time coordinate name + + Returns: + Tuple of (initial_constraint, transition_constraints, increase_bounds, decrease_bounds, mutual_exclusion) + """ + if not isinstance(model, Submodel): + raise ValueError('BoundingPatterns.link_changes_to_level_with_binaries() can only be used with a Submodel') + + # 1. Initial period: level[0] - initial_level = increase[0] - decrease[0] + initial_constraint = model.add_constraints( + level_variable.isel({coord: 0}) - initial_level + == increase_variable.isel({coord: 0}) - decrease_variable.isel({coord: 0}), + name=f'{name}|initial_level', + ) + + # 2. Transition periods: level[t] = level[t-1] + increase[t] - decrease[t] for t > 0 + transition_constraints = model.add_constraints( + level_variable.isel({coord: slice(1, None)}) + == level_variable.isel({coord: slice(None, -1)}) + + increase_variable.isel({coord: slice(1, None)}) + - decrease_variable.isel({coord: slice(1, None)}), + name=f'{name}|transitions', + ) + + # 3. Increase bounds: increase[t] <= max_change * increase_binary[t] for all t + increase_bounds = model.add_constraints( + increase_variable <= increase_binary * max_change, + name=f'{name}|increase_bounds', + ) + + # 4. Decrease bounds: decrease[t] <= max_change * decrease_binary[t] for all t + decrease_bounds = model.add_constraints( + decrease_variable <= decrease_binary * max_change, + name=f'{name}|decrease_bounds', + ) + + # 5. Mutual exclusivity: increase_binary[t] + decrease_binary[t] <= 1 for all t + mutual_exclusion = model.add_constraints( + increase_binary + decrease_binary <= 1, + name=f'{name}|mutual_exclusion', + ) + + return initial_constraint, transition_constraints, increase_bounds, decrease_bounds, mutual_exclusion diff --git a/flixopt/plotting.py b/flixopt/plotting.py index 950172c6a..356f013c0 100644 --- a/flixopt/plotting.py +++ b/flixopt/plotting.py @@ -27,9 +27,11 @@ import itertools import logging +import os import pathlib from typing import TYPE_CHECKING, Any, Literal +import matplotlib import matplotlib.colors as mcolors import matplotlib.pyplot as plt import numpy as np @@ -200,7 +202,7 @@ def _generate_colors_from_colormap(self, colormap_name: str, num_colors: int) -> try: colorscale = px.colors.get_colorscale(colormap_name) except PlotlyError as e: - logger.warning(f"Colorscale '{colormap_name}' not found in Plotly. Using {self.default_colormap}: {e}") + logger.error(f"Colorscale '{colormap_name}' not found in Plotly. Using {self.default_colormap}: {e}") colorscale = px.colors.get_colorscale(self.default_colormap) # Generate evenly spaced points @@ -211,9 +213,7 @@ def _generate_colors_from_colormap(self, colormap_name: str, num_colors: int) -> try: cmap = plt.get_cmap(colormap_name, num_colors) except ValueError as e: - logger.warning( - f"Colormap '{colormap_name}' not found in Matplotlib. Using {self.default_colormap}: {e}" - ) + logger.error(f"Colormap '{colormap_name}' not found in Matplotlib. Using {self.default_colormap}: {e}") cmap = plt.get_cmap(self.default_colormap, num_colors) return [cmap(i) for i in range(num_colors)] @@ -230,7 +230,7 @@ def _handle_color_list(self, colors: list[str], num_labels: int) -> list[str]: list of colors matching the number of labels """ if len(colors) == 0: - logger.warning(f'Empty color list provided. Using {self.default_colormap} instead.') + logger.error(f'Empty color list provided. Using {self.default_colormap} instead.') return self._generate_colors_from_colormap(self.default_colormap, num_labels) if len(colors) < num_labels: @@ -302,7 +302,7 @@ def process_colors( Either a list of colors or a dictionary mapping labels to colors """ if len(labels) == 0: - logger.warning('No labels provided for color assignment.') + logger.error('No labels provided for color assignment.') return {} if return_mapping else [] # Process based on type of colors input @@ -313,7 +313,7 @@ def process_colors( elif isinstance(colors, dict): color_list = self._handle_color_dict(colors, labels) else: - logger.warning( + logger.error( f'Unsupported color specification type: {type(colors)}. Using {self.default_colormap} instead.' ) color_list = self._generate_colors_from_colormap(self.default_colormap, len(labels)) @@ -327,7 +327,7 @@ def process_colors( def with_plotly( data: pd.DataFrame, - mode: Literal['bar', 'line', 'area'] = 'area', + style: Literal['stacked_bar', 'line', 'area', 'grouped_bar'] = 'stacked_bar', colors: ColorType = 'viridis', title: str = '', ylabel: str = '', @@ -340,7 +340,7 @@ def with_plotly( Args: data: A DataFrame containing the data to plot, where the index represents time (e.g., hours), and each column represents a separate data series. - mode: The plotting mode. Use 'bar' for stacked bar charts, 'line' for stepped lines, + style: The plotting style. Use 'stacked_bar' for stacked bar charts, 'line' for stepped lines, or 'area' for stacked area charts. colors: Color specification, can be: - A string with a colorscale name (e.g., 'viridis', 'plasma') @@ -354,8 +354,8 @@ def with_plotly( Returns: A Plotly figure object containing the generated plot. """ - if mode not in ('bar', 'line', 'area'): - raise ValueError(f"'mode' must be one of {{'bar','line','area'}}, got {mode!r}") + if style not in ('stacked_bar', 'line', 'area', 'grouped_bar'): + raise ValueError(f"'style' must be one of {{'stacked_bar','line','area', 'grouped_bar'}}, got {style!r}") if data.empty: return go.Figure() @@ -363,23 +363,34 @@ def with_plotly( fig = fig if fig is not None else go.Figure() - if mode == 'bar': + if style == 'stacked_bar': for i, column in enumerate(data.columns): fig.add_trace( go.Bar( x=data.index, y=data[column], name=column, - marker=dict(color=processed_colors[i]), + marker=dict( + color=processed_colors[i], line=dict(width=0, color='rgba(0,0,0,0)') + ), # Transparent line with 0 width ) ) fig.update_layout( - barmode='relative' if mode == 'bar' else None, + barmode='relative', bargap=0, # No space between bars - bargroupgap=0, # No space between groups of bars + bargroupgap=0, # No space between grouped bars ) - elif mode == 'line': + if style == 'grouped_bar': + for i, column in enumerate(data.columns): + fig.add_trace(go.Bar(x=data.index, y=data[column], name=column, marker=dict(color=processed_colors[i]))) + + fig.update_layout( + barmode='group', + bargap=0.2, # No space between bars + bargroupgap=0, # space between grouped bars + ) + elif style == 'line': for i, column in enumerate(data.columns): fig.add_trace( go.Scatter( @@ -390,7 +401,7 @@ def with_plotly( line=dict(shape='hv', color=processed_colors[i]), ) ) - elif mode == 'area': + elif style == 'area': data = data.copy() data[(data > -1e-5) & (data < 1e-5)] = 0 # Preventing issues with plotting # Split columns into positive, negative, and mixed categories @@ -400,7 +411,7 @@ def with_plotly( mixed_columns = list(set(data.columns) - set(positive_columns + negative_columns)) if mixed_columns: - logger.warning( + logger.error( f'Data for plotting stacked lines contains columns with both positive and negative values:' f' {mixed_columns}. These can not be stacked, and are printed as simple lines' ) @@ -450,14 +461,6 @@ def with_plotly( plot_bgcolor='rgba(0,0,0,0)', # Transparent background paper_bgcolor='rgba(0,0,0,0)', # Transparent paper background font=dict(size=14), # Increase font size for better readability - legend=dict( - orientation='h', # Horizontal legend - yanchor='bottom', - y=-0.3, # Adjusts how far below the plot it appears - xanchor='center', - x=0.5, - title_text=None, # Removes legend title for a cleaner look - ), ) return fig @@ -465,7 +468,7 @@ def with_plotly( def with_matplotlib( data: pd.DataFrame, - mode: Literal['bar', 'line'] = 'bar', + style: Literal['stacked_bar', 'line'] = 'stacked_bar', colors: ColorType = 'viridis', title: str = '', ylabel: str = '', @@ -480,7 +483,7 @@ def with_matplotlib( Args: data: A DataFrame containing the data to plot. The index should represent time (e.g., hours), and each column represents a separate data series. - mode: Plotting mode. Use 'bar' for stacked bar charts or 'line' for stepped lines. + style: Plotting style. Use 'stacked_bar' for stacked bar charts or 'line' for stepped lines. colors: Color specification, can be: - A string with a colormap name (e.g., 'viridis', 'plasma') - A list of color strings (e.g., ['#ff0000', '#00ff00']) @@ -496,20 +499,19 @@ def with_matplotlib( A tuple containing the Matplotlib figure and axes objects used for the plot. Notes: - - If `mode` is 'bar', bars are stacked for both positive and negative values. + - If `style` is 'stacked_bar', bars are stacked for both positive and negative values. Negative values are stacked separately without extra labels in the legend. - - If `mode` is 'line', stepped lines are drawn for each data series. - - The legend is placed below the plot to accommodate multiple data series. + - If `style` is 'line', stepped lines are drawn for each data series. """ - if mode not in ('bar', 'line'): - raise ValueError(f"'mode' must be one of {{'bar','line'}} for matplotlib, got {mode!r}") + if style not in ('stacked_bar', 'line'): + raise ValueError(f"'style' must be one of {{'stacked_bar','line'}} for matplotlib, got {style!r}") if fig is None or ax is None: fig, ax = plt.subplots(figsize=figsize) processed_colors = ColorProcessor(engine='matplotlib').process_colors(colors, list(data.columns)) - if mode == 'bar': + if style == 'stacked_bar': cumulative_positive = np.zeros(len(data)) cumulative_negative = np.zeros(len(data)) width = data.index.to_series().diff().dropna().min() # Minimum time difference @@ -540,7 +542,7 @@ def with_matplotlib( ) cumulative_negative += negative_values.values - elif mode == 'line': + elif style == 'line': for i, column in enumerate(data.columns): ax.step(data.index, data[column], where='post', color=processed_colors[i], label=column) @@ -780,7 +782,7 @@ def heat_map_data_from_df( minimum_time_diff_in_min = diffs.min().total_seconds() / 60 time_intervals = {'min': 1, '15min': 15, 'h': 60, 'D': 24 * 60, 'W': 7 * 24 * 60} if time_intervals[steps_per_period] > minimum_time_diff_in_min: - logger.warning( + logger.error( f'To compute the heatmap, the data was aggregated from {minimum_time_diff_in_min:.2f} min to ' f'{time_intervals[steps_per_period]:.2f} min. Mean values are displayed.' ) @@ -890,11 +892,9 @@ def plot_network( worked = webbrowser.open(f'file://{path.resolve()}', 2) if not worked: - logger.warning( - f'Showing the network in the Browser went wrong. Open it manually. Its saved under {path}' - ) + logger.error(f'Showing the network in the Browser went wrong. Open it manually. Its saved under {path}') except Exception as e: - logger.warning( + logger.error( f'Showing the network in the Browser went wrong. Open it manually. Its saved under {path}: {e}' ) @@ -933,7 +933,7 @@ def pie_with_plotly( """ if data.empty: - logger.warning('Empty DataFrame provided for pie chart. Returning empty figure.') + logger.error('Empty DataFrame provided for pie chart. Returning empty figure.') return go.Figure() # Create a copy to avoid modifying the original DataFrame @@ -941,7 +941,7 @@ def pie_with_plotly( # Check if any negative values and warn if (data_copy < 0).any().any(): - logger.warning('Negative values detected in data. Using absolute values for pie chart.') + logger.error('Negative values detected in data. Using absolute values for pie chart.') data_copy = data_copy.abs() # If data has multiple rows, sum them to get total for each column @@ -1023,7 +1023,7 @@ def pie_with_matplotlib( """ if data.empty: - logger.warning('Empty DataFrame provided for pie chart. Returning empty figure.') + logger.error('Empty DataFrame provided for pie chart. Returning empty figure.') if fig is None or ax is None: fig, ax = plt.subplots(figsize=figsize) return fig, ax @@ -1033,7 +1033,7 @@ def pie_with_matplotlib( # Check if any negative values and warn if (data_copy < 0).any().any(): - logger.warning('Negative values detected in data. Using absolute values for pie chart.') + logger.error('Negative values detected in data. Using absolute values for pie chart.') data_copy = data_copy.abs() # If data has multiple rows, sum them to get total for each column @@ -1138,7 +1138,7 @@ def dual_pie_with_plotly( # Check for empty data if data_left.empty and data_right.empty: - logger.warning('Both datasets are empty. Returning empty figure.') + logger.error('Both datasets are empty. Returning empty figure.') return go.Figure() # Create a subplot figure @@ -1161,7 +1161,7 @@ def preprocess_series(series: pd.Series): """ # Handle negative values if (series < 0).any(): - logger.warning('Negative values detected in data. Using absolute values for pie chart.') + logger.error('Negative values detected in data. Using absolute values for pie chart.') series = series.abs() # Remove zeros @@ -1246,7 +1246,6 @@ def create_pie_trace(data_series, side): paper_bgcolor='rgba(0,0,0,0)', # Transparent paper background font=dict(size=14), margin=dict(t=80, b=50, l=30, r=30), - legend=dict(orientation='h', yanchor='bottom', y=-0.2, xanchor='center', x=0.5, font=dict(size=12)), ) return fig @@ -1290,7 +1289,7 @@ def dual_pie_with_matplotlib( """ # Check for empty data if data_left.empty and data_right.empty: - logger.warning('Both datasets are empty. Returning empty figure.') + logger.error('Both datasets are empty. Returning empty figure.') if fig is None: fig, axes = plt.subplots(1, 2, figsize=figsize) return fig, axes @@ -1308,7 +1307,7 @@ def preprocess_series(series: pd.Series): """ # Handle negative values if (series < 0).any(): - logger.warning('Negative values detected in data. Using absolute values for pie chart.') + logger.error('Negative values detected in data. Using absolute values for pie chart.') series = series.abs() # Remove zeros @@ -1449,20 +1448,49 @@ def export_figure( if filename.suffix != '.html': logger.warning(f'To save a Plotly figure, using .html. Adjusting suffix for {filename}') filename = filename.with_suffix('.html') - if show and not save: - fig.show() - elif save and show: - plotly.offline.plot(fig, filename=str(filename)) - elif save and not show: - fig.write_html(str(filename)) + + try: + is_test_env = 'PYTEST_CURRENT_TEST' in os.environ + + if is_test_env: + # Test environment: never open browser, only save if requested + if save: + fig.write_html(str(filename)) + # Ignore show flag in tests + else: + # Production environment: respect show and save flags + if save and show: + # Save and auto-open in browser + plotly.offline.plot(fig, filename=str(filename)) + elif save and not show: + # Save without opening + fig.write_html(str(filename)) + elif show and not save: + # Show interactively without saving + fig.show() + # If neither save nor show: do nothing + finally: + # Cleanup to prevent socket warnings + if hasattr(fig, '_renderer'): + fig._renderer = None + return figure_like elif isinstance(figure_like, tuple): fig, ax = figure_like if show: - fig.show() + # Only show if using interactive backend and not in test environment + backend = matplotlib.get_backend().lower() + is_interactive = backend not in {'agg', 'pdf', 'ps', 'svg', 'template'} + is_test_env = 'PYTEST_CURRENT_TEST' in os.environ + + if is_interactive and not is_test_env: + plt.show() + if save: fig.savefig(str(filename), dpi=300) + plt.close(fig) # Close figure to free memory + return fig, ax raise TypeError(f'Figure type not supported: {type(figure_like)}') diff --git a/flixopt/results.py b/flixopt/results.py index d93285d2c..e571bc558 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -4,7 +4,8 @@ import json import logging import pathlib -from typing import TYPE_CHECKING, Literal +import warnings +from typing import TYPE_CHECKING, Any, Literal import linopy import numpy as np @@ -15,18 +16,25 @@ from . import io as fx_io from . import plotting -from .core import TimeSeriesCollection +from .flow_system import FlowSystem if TYPE_CHECKING: import matplotlib.pyplot as plt import pyvis from .calculation import Calculation, SegmentedCalculation + from .core import FlowSystemDimensions logger = logging.getLogger('flixopt') +class _FlowSystemRestorationError(Exception): + """Exception raised when a FlowSystem cannot be restored from dataset.""" + + pass + + class CalculationResults: """Comprehensive container for optimization calculation results and analysis tools. @@ -51,7 +59,7 @@ class CalculationResults: Attributes: solution: Dataset containing all optimization variable solutions - flow_system: Dataset with complete system configuration and parameters. Restore the used FlowSystem for further analysis. + flow_system_data: Dataset with complete system configuration and parameters. Restore the used FlowSystem for further analysis. summary: Calculation metadata including solver status, timing, and statistics name: Unique identifier for this calculation model: Original linopy optimization model (if available) @@ -134,7 +142,7 @@ def from_file(cls, folder: str | pathlib.Path, name: str) -> CalculationResults: return cls( solution=fx_io.load_dataset_from_netcdf(paths.solution), - flow_system=fx_io.load_dataset_from_netcdf(paths.flow_system), + flow_system_data=fx_io.load_dataset_from_netcdf(paths.flow_system), name=name, folder=folder, model=model, @@ -153,7 +161,7 @@ def from_calculation(cls, calculation: Calculation) -> CalculationResults: """ return cls( solution=calculation.model.solution, - flow_system=calculation.flow_system.as_dataset(constants_in_dataset=True), + flow_system_data=calculation.flow_system.to_dataset(), summary=calculation.summary, model=calculation.model, name=calculation.name, @@ -163,41 +171,73 @@ def from_calculation(cls, calculation: Calculation) -> CalculationResults: def __init__( self, solution: xr.Dataset, - flow_system: xr.Dataset, + flow_system_data: xr.Dataset, name: str, summary: dict, folder: pathlib.Path | None = None, model: linopy.Model | None = None, + **kwargs, # To accept old "flow_system" parameter ): """Initialize CalculationResults with optimization data. Usually, this class is instantiated by the Calculation class, or by loading from file. Args: solution: Optimization solution dataset. - flow_system: Flow system configuration dataset. + flow_system_data: Flow system configuration dataset. name: Calculation name. summary: Calculation metadata. folder: Results storage folder. model: Linopy optimization model. + Deprecated: + flow_system: Use flow_system_data instead. """ + # Handle potential old "flow_system" parameter for backward compatibility + if 'flow_system' in kwargs and flow_system_data is None: + flow_system_data = kwargs.pop('flow_system') + warnings.warn( + "The 'flow_system' parameter is deprecated. Use 'flow_system_data' instead." + "Acess is now by '.flow_system_data', while '.flow_system' returns the restored FlowSystem.", + DeprecationWarning, + stacklevel=2, + ) + self.solution = solution - self.flow_system = flow_system + self.flow_system_data = flow_system_data self.summary = summary self.name = name self.model = model self.folder = pathlib.Path(folder) if folder is not None else pathlib.Path.cwd() / 'results' self.components = { - label: ComponentResults.from_json(self, infos) for label, infos in self.solution.attrs['Components'].items() + label: ComponentResults(self, **infos) for label, infos in self.solution.attrs['Components'].items() } - self.buses = {label: BusResults.from_json(self, infos) for label, infos in self.solution.attrs['Buses'].items()} + self.buses = {label: BusResults(self, **infos) for label, infos in self.solution.attrs['Buses'].items()} - self.effects = { - label: EffectResults.from_json(self, infos) for label, infos in self.solution.attrs['Effects'].items() - } + self.effects = {label: EffectResults(self, **infos) for label, infos in self.solution.attrs['Effects'].items()} + + if 'Flows' not in self.solution.attrs: + warnings.warn( + 'No Data about flows found in the results. This data is only included since v2.2.0. Some functionality ' + 'is not availlable. We recommend to evaluate your results with a version <2.2.0.', + stacklevel=2, + ) + self.flows = {} + else: + self.flows = { + label: FlowResults(self, **infos) for label, infos in self.solution.attrs.get('Flows', {}).items() + } self.timesteps_extra = self.solution.indexes['time'] - self.hours_per_timestep = TimeSeriesCollection.calculate_hours_per_timestep(self.timesteps_extra) + self.hours_per_timestep = FlowSystem.calculate_hours_per_timestep(self.timesteps_extra) + self.scenarios = self.solution.indexes['scenario'] if 'scenario' in self.solution.indexes else None + + self._effect_share_factors = None + self._flow_system = None + + self._flow_rates = None + self._flow_hours = None + self._sizes = None + self._effects_per_component = None def __getitem__(self, key: str) -> ComponentResults | BusResults | EffectResults: if key in self.components: @@ -206,6 +246,8 @@ def __getitem__(self, key: str) -> ComponentResults | BusResults | EffectResults return self.buses[key] if key in self.effects: return self.effects[key] + if key in self.flows: + return self.flows[key] raise KeyError(f'No element with label {key} found.') @property @@ -216,7 +258,12 @@ def storages(self) -> list[ComponentResults]: @property def objective(self) -> float: """Get optimization objective value.""" - return self.summary['Main Results']['Objective'] + # Deprecated. Fallback + if 'objective' not in self.solution: + logger.warning('Objective not found in solution. Fallback to summary (rounded value). This is deprecated') + return self.summary['Main Results']['Objective'] + + return self.solution['objective'].item() @property def variables(self) -> linopy.Variables: @@ -232,21 +279,411 @@ def constraints(self) -> linopy.Constraints: raise ValueError('The linopy model is not available.') return self.model.constraints + @property + def effect_share_factors(self): + if self._effect_share_factors is None: + effect_share_factors = self.flow_system.effects.calculate_effect_share_factors() + self._effect_share_factors = {'temporal': effect_share_factors[0], 'periodic': effect_share_factors[1]} + return self._effect_share_factors + + @property + def flow_system(self) -> FlowSystem: + """The restored flow_system that was used to create the calculation. + Contains all input parameters.""" + if self._flow_system is None: + old_level = logger.level + logger.level = logging.CRITICAL + try: + self._flow_system = FlowSystem.from_dataset(self.flow_system_data) + self._flow_system._connect_network() + except Exception as e: + logger.critical( + f'Not able to restore FlowSystem from dataset. Some functionality is not availlable. {e}' + ) + raise _FlowSystemRestorationError(f'Not able to restore FlowSystem from dataset. {e}') from e + finally: + logger.level = old_level + return self._flow_system + def filter_solution( - self, variable_dims: Literal['scalar', 'time'] | None = None, element: str | None = None + self, + variable_dims: Literal['scalar', 'time', 'scenario', 'timeonly', 'scenarioonly'] | None = None, + element: str | None = None, + timesteps: pd.DatetimeIndex | None = None, + scenarios: pd.Index | None = None, + contains: str | list[str] | None = None, + startswith: str | list[str] | None = None, ) -> xr.Dataset: """Filter solution by variable dimension and/or element. Args: - variable_dims: Variable dimension to filter ('scalar' or 'time'). - element: Element label to filter. + variable_dims: The dimension of which to get variables from. + - 'scalar': Get scalar variables (without dimensions) + - 'time': Get time-dependent variables (with a time dimension) + - 'scenario': Get scenario-dependent variables (with ONLY a scenario dimension) + - 'timeonly': Get time-dependent variables (with ONLY a time dimension) + - 'scenarioonly': Get scenario-dependent variables (with ONLY a scenario dimension) + element: The element to filter for. + timesteps: Optional time indexes to select. Can be: + - pd.DatetimeIndex: Multiple timesteps + - str/pd.Timestamp: Single timestep + Defaults to all available timesteps. + scenarios: Optional scenario indexes to select. Can be: + - pd.Index: Multiple scenarios + - str/int: Single scenario (int is treated as a label, not an index position) + Defaults to all available scenarios. + contains: Filter variables that contain this string or strings. + If a list is provided, variables must contain ALL strings in the list. + startswith: Filter variables that start with this string or strings. + If a list is provided, variables must start with ANY of the strings in the list. + """ + return filter_dataset( + self.solution if element is None else self[element].solution, + variable_dims=variable_dims, + timesteps=timesteps, + scenarios=scenarios, + contains=contains, + startswith=startswith, + ) + + @property + def effects_per_component(self) -> xr.Dataset: + """Returns a dataset containing effect results for each mode, aggregated by Component + + Returns: + An xarray Dataset with an additional component dimension and effects as variables. + """ + if self._effects_per_component is None: + self._effects_per_component = xr.Dataset( + { + mode: self._create_effects_dataset(mode).to_dataarray('effect', name=mode) + for mode in ['temporal', 'periodic', 'total'] + } + ) + dim_order = ['time', 'period', 'scenario', 'component', 'effect'] + self._effects_per_component = self._effects_per_component.transpose(*dim_order, missing_dims='ignore') + + return self._effects_per_component + + def flow_rates( + self, + start: str | list[str] | None = None, + end: str | list[str] | None = None, + component: str | list[str] | None = None, + ) -> xr.DataArray: + """Returns a DataArray containing the flow rates of each Flow. + + Args: + start: Optional source node(s) to filter by. Can be a single node name or a list of names. + end: Optional destination node(s) to filter by. Can be a single node name or a list of names. + component: Optional component(s) to filter by. Can be a single component name or a list of names. + + Further usage: + Convert the dataarray to a dataframe: + >>>results.flow_rates().to_pandas() + Get the max or min over time: + >>>results.flow_rates().max('time') + Sum up the flow rates of flows with the same start and end: + >>>results.flow_rates(end='Fernwärme').groupby('start').sum(dim='flow') + To recombine filtered dataarrays, use `xr.concat` with dim 'flow': + >>>xr.concat([results.flow_rates(start='Fernwärme'), results.flow_rates(end='Fernwärme')], dim='flow') + """ + if self._flow_rates is None: + self._flow_rates = self._assign_flow_coords( + xr.concat( + [flow.flow_rate.rename(flow.label) for flow in self.flows.values()], + dim=pd.Index(self.flows.keys(), name='flow'), + ) + ).rename('flow_rates') + filters = {k: v for k, v in {'start': start, 'end': end, 'component': component}.items() if v is not None} + return filter_dataarray_by_coord(self._flow_rates, **filters) + + def flow_hours( + self, + start: str | list[str] | None = None, + end: str | list[str] | None = None, + component: str | list[str] | None = None, + ) -> xr.DataArray: + """Returns a DataArray containing the flow hours of each Flow. + + Flow hours represent the total energy/material transferred over time, + calculated by multiplying flow rates by the duration of each timestep. + + Args: + start: Optional source node(s) to filter by. Can be a single node name or a list of names. + end: Optional destination node(s) to filter by. Can be a single node name or a list of names. + component: Optional component(s) to filter by. Can be a single component name or a list of names. + + Further usage: + Convert the dataarray to a dataframe: + >>>results.flow_hours().to_pandas() + Sum up the flow hours over time: + >>>results.flow_hours().sum('time') + Sum up the flow hours of flows with the same start and end: + >>>results.flow_hours(end='Fernwärme').groupby('start').sum(dim='flow') + To recombine filtered dataarrays, use `xr.concat` with dim 'flow': + >>>xr.concat([results.flow_hours(start='Fernwärme'), results.flow_hours(end='Fernwärme')], dim='flow') + + """ + if self._flow_hours is None: + self._flow_hours = (self.flow_rates() * self.hours_per_timestep).rename('flow_hours') + filters = {k: v for k, v in {'start': start, 'end': end, 'component': component}.items() if v is not None} + return filter_dataarray_by_coord(self._flow_hours, **filters) + + def sizes( + self, + start: str | list[str] | None = None, + end: str | list[str] | None = None, + component: str | list[str] | None = None, + ) -> xr.DataArray: + """Returns a dataset with the sizes of the Flows. + Args: + start: Optional source node(s) to filter by. Can be a single node name or a list of names. + end: Optional destination node(s) to filter by. Can be a single node name or a list of names. + component: Optional component(s) to filter by. Can be a single component name or a list of names. + + Further usage: + Convert the dataarray to a dataframe: + >>>results.sizes().to_pandas() + To recombine filtered dataarrays, use `xr.concat` with dim 'flow': + >>>xr.concat([results.sizes(start='Fernwärme'), results.sizes(end='Fernwärme')], dim='flow') + + """ + if self._sizes is None: + self._sizes = self._assign_flow_coords( + xr.concat( + [flow.size.rename(flow.label) for flow in self.flows.values()], + dim=pd.Index(self.flows.keys(), name='flow'), + ) + ).rename('flow_sizes') + filters = {k: v for k, v in {'start': start, 'end': end, 'component': component}.items() if v is not None} + return filter_dataarray_by_coord(self._sizes, **filters) + + def _assign_flow_coords(self, da: xr.DataArray): + # Add start and end coordinates + da = da.assign_coords( + { + 'start': ('flow', [flow.start for flow in self.flows.values()]), + 'end': ('flow', [flow.end for flow in self.flows.values()]), + 'component': ('flow', [flow.component for flow in self.flows.values()]), + } + ) + + # Ensure flow is the last dimension if needed + existing_dims = [d for d in da.dims if d != 'flow'] + da = da.transpose(*(existing_dims + ['flow'])) + return da + + def get_effect_shares( + self, + element: str, + effect: str, + mode: Literal['temporal', 'periodic'] | None = None, + include_flows: bool = False, + ) -> xr.Dataset: + """Retrieves individual effect shares for a specific element and effect. + Either for temporal, investment, or both modes combined. + Only includes the direct shares. + + Args: + element: The element identifier for which to retrieve effect shares. + effect: The effect identifier for which to retrieve shares. + mode: Optional. The mode to retrieve shares for. Can be 'temporal', 'periodic', + or None to retrieve both. Defaults to None. + + Returns: + An xarray Dataset containing the requested effect shares. If mode is None, + returns a merged Dataset containing both temporal and investment shares. + + Raises: + ValueError: If the specified effect is not available or if mode is invalid. + """ + if effect not in self.effects: + raise ValueError(f'Effect {effect} is not available.') + + if mode is None: + return xr.merge( + [ + self.get_effect_shares( + element=element, effect=effect, mode='temporal', include_flows=include_flows + ), + self.get_effect_shares( + element=element, effect=effect, mode='periodic', include_flows=include_flows + ), + ] + ) + + if mode not in ['temporal', 'periodic']: + raise ValueError(f'Mode {mode} is not available. Choose between "temporal" and "periodic".') + + ds = xr.Dataset() + + label = f'{element}->{effect}({mode})' + if label in self.solution: + ds = xr.Dataset({label: self.solution[label]}) + + if include_flows: + if element not in self.components: + raise ValueError(f'Only use Components when retrieving Effects including flows. Got {element}') + flows = [ + label.split('|')[0] for label in self.components[element].inputs + self.components[element].outputs + ] + return xr.merge( + [ds] + + [ + self.get_effect_shares(element=flow, effect=effect, mode=mode, include_flows=False) + for flow in flows + ] + ) + + return ds + + def _compute_effect_total( + self, + element: str, + effect: str, + mode: Literal['temporal', 'periodic', 'total'] = 'total', + include_flows: bool = False, + ) -> xr.DataArray: + """Calculates the total effect for a specific element and effect. + + This method computes the total direct and indirect effects for a given element + and effect, considering the conversion factors between different effects. + + Args: + element: The element identifier for which to calculate total effects. + effect: The effect identifier to calculate. + mode: The calculation mode. Options are: + 'temporal': Returns temporal effects. + 'periodic': Returns investment-specific effects. + 'total': Returns the sum of temporal effects and periodic effects. Defaults to 'total'. + include_flows: Whether to include effects from flows connected to this element. + + Returns: + An xarray DataArray containing the total effects, named with pattern + '{element}->{effect}' for mode='total' or '{element}->{effect}({mode})' + for other modes. + + Raises: + ValueError: If the specified effect is not available. + """ + if effect not in self.effects: + raise ValueError(f'Effect {effect} is not available.') + + if mode == 'total': + temporal = self._compute_effect_total( + element=element, effect=effect, mode='temporal', include_flows=include_flows + ) + periodic = self._compute_effect_total( + element=element, effect=effect, mode='periodic', include_flows=include_flows + ) + if periodic.isnull().all() and temporal.isnull().all(): + return xr.DataArray(np.nan) + if temporal.isnull().all(): + return periodic.rename(f'{element}->{effect}') + temporal = temporal.sum('time') + if periodic.isnull().all(): + return temporal.rename(f'{element}->{effect}') + if 'time' in temporal.indexes: + temporal = temporal.sum('time') + return periodic + temporal + + total = xr.DataArray(0) + share_exists = False + + relevant_conversion_factors = { + key[0]: value for key, value in self.effect_share_factors[mode].items() if key[1] == effect + } + relevant_conversion_factors[effect] = 1 # Share to itself is 1 + + for target_effect, conversion_factor in relevant_conversion_factors.items(): + label = f'{element}->{target_effect}({mode})' + if label in self.solution: + share_exists = True + da = self.solution[label] + total = da * conversion_factor + total + + if include_flows: + if element not in self.components: + raise ValueError(f'Only use Components when retrieving Effects including flows. Got {element}') + flows = [ + label.split('|')[0] for label in self.components[element].inputs + self.components[element].outputs + ] + for flow in flows: + label = f'{flow}->{target_effect}({mode})' + if label in self.solution: + share_exists = True + da = self.solution[label] + total = da * conversion_factor + total + if not share_exists: + total = xr.DataArray(np.nan) + return total.rename(f'{element}->{effect}({mode})') + + def _create_effects_dataset(self, mode: Literal['temporal', 'periodic', 'total']) -> xr.Dataset: + """Creates a dataset containing effect totals for all components (including their flows). + The dataset does contain the direct as well as the indirect effects of each component. + + Args: + mode: The calculation mode ('temporal', 'periodic', or 'total'). Returns: - xr.Dataset: Filtered solution dataset. + An xarray Dataset with components as dimension and effects as variables. """ - if element is not None: - return filter_dataset(self[element].solution, variable_dims) - return filter_dataset(self.solution, variable_dims) + ds = xr.Dataset() + all_arrays = {} + template = None # Template is needed to determine the dimensions of the arrays. This handles the case of no shares for an effect + + components_list = list(self.components) + + # First pass: collect arrays and find template + for effect in self.effects: + effect_arrays = [] + for component in components_list: + da = self._compute_effect_total(element=component, effect=effect, mode=mode, include_flows=True) + effect_arrays.append(da) + + if template is None and (da.dims or not da.isnull().all()): + template = da + + all_arrays[effect] = effect_arrays + + # Ensure we have a template + if template is None: + raise ValueError( + f"No template with proper dimensions found for mode '{mode}'. " + f'All computed arrays are scalars, which indicates a data issue.' + ) + + # Second pass: process all effects (guaranteed to include all) + for effect in self.effects: + dataarrays = all_arrays[effect] + component_arrays = [] + + for component, arr in zip(components_list, dataarrays, strict=False): + # Expand scalar NaN arrays to match template dimensions + if not arr.dims and np.isnan(arr.item()): + arr = xr.full_like(template, np.nan, dtype=float).rename(arr.name) + + component_arrays.append(arr.expand_dims(component=[component])) + + ds[effect] = xr.concat(component_arrays, dim='component', coords='minimal', join='outer').rename(effect) + + # For now include a test to ensure correctness + suffix = { + 'temporal': '(temporal)|per_timestep', + 'periodic': '(periodic)', + 'total': '', + } + for effect in self.effects: + label = f'{effect}{suffix[mode]}' + computed = ds[effect].sum('component') + found = self.solution[label] + if not np.allclose(computed.values, found.fillna(0).values): + logger.critical( + f'Results for {effect}({mode}) in effects_dataset doesnt match {label}\n{computed=}\n, {found=}' + ) + + return ds def plot_heatmap( self, @@ -257,9 +694,52 @@ def plot_heatmap( save: bool | pathlib.Path = False, show: bool = True, engine: plotting.PlottingEngine = 'plotly', + indexer: dict[FlowSystemDimensions, Any] | None = None, ) -> plotly.graph_objs.Figure | tuple[plt.Figure, plt.Axes]: + """ + Plots a heatmap of the solution of a variable. + + Args: + variable_name: The name of the variable to plot. + heatmap_timeframes: The timeframes to use for the heatmap. + heatmap_timesteps_per_frame: The timesteps per frame to use for the heatmap. + color_map: The color map to use for the heatmap. + save: Whether to save the plot or not. If a path is provided, the plot will be saved at that location. + show: Whether to show the plot or not. + engine: The engine to use for plotting. Can be either 'plotly' or 'matplotlib'. + indexer: Optional selection dict, e.g., {'scenario': 'base', 'period': 2024}. + If None, uses first value for each dimension. + If empty dict {}, uses all values. + + Examples: + Basic usage (uses first scenario, first period, all time): + + >>> results.plot_heatmap('Battery|charge_state') + + Select specific scenario and period: + + >>> results.plot_heatmap('Boiler(Qth)|flow_rate', indexer={'scenario': 'base', 'period': 2024}) + + Time filtering (summer months only): + + >>> results.plot_heatmap( + ... 'Boiler(Qth)|flow_rate', + ... indexer={ + ... 'scenario': 'base', + ... 'time': results.solution.time[results.solution.time.dt.month.isin([6, 7, 8])], + ... }, + ... ) + + Save to specific location: + + >>> results.plot_heatmap( + ... 'Boiler(Qth)|flow_rate', indexer={'scenario': 'base'}, save='path/to/my_heatmap.html' + ... ) + """ + dataarray = self.solution[variable_name] + return plot_heatmap( - dataarray=self.solution[variable_name], + dataarray=dataarray, name=variable_name, folder=self.folder, heatmap_timeframes=heatmap_timeframes, @@ -268,6 +748,7 @@ def plot_heatmap( save=save, show=show, engine=engine, + indexer=indexer, ) def plot_network( @@ -288,16 +769,9 @@ def plot_network( path: Save path for network HTML. show: Whether to display the plot. """ - try: - from .flow_system import FlowSystem - - flow_system = FlowSystem.from_dataset(self.flow_system) - except Exception as e: - logger.critical(f'Could not reconstruct the flow_system from dataset: {e}') - return None if path is None: path = self.folder / f'{self.name}--network.html' - return flow_system.plot_network(controls=controls, path=path, show=show) + return self.flow_system.plot_network(controls=controls, path=path, show=show) def to_file( self, @@ -329,7 +803,7 @@ def to_file( paths = fx_io.CalculationResultsPaths(folder, name) fx_io.save_dataset_to_netcdf(self.solution, paths.solution, compression=compression) - fx_io.save_dataset_to_netcdf(self.flow_system, paths.flow_system, compression=compression) + fx_io.save_dataset_to_netcdf(self.flow_system_data, paths.flow_system, compression=compression) with open(paths.summary, 'w', encoding='utf-8') as f: yaml.dump(self.summary, f, allow_unicode=True, sort_keys=False, indent=4, width=1000) @@ -350,10 +824,6 @@ def to_file( class _ElementResults: - @classmethod - def from_json(cls, calculation_results, json_data: dict) -> _ElementResults: - return cls(calculation_results, json_data['label'], json_data['variables'], json_data['constraints']) - def __init__( self, calculation_results: CalculationResults, label: str, variables: list[str], constraints: list[str] ): @@ -386,30 +856,49 @@ def constraints(self) -> linopy.Constraints: raise ValueError('The linopy model is not available.') return self._calculation_results.model.constraints[self._constraint_names] - def filter_solution(self, variable_dims: Literal['scalar', 'time'] | None = None) -> xr.Dataset: - """Filter element solution by dimension. + def filter_solution( + self, + variable_dims: Literal['scalar', 'time', 'scenario', 'timeonly', 'scenarioonly'] | None = None, + timesteps: pd.DatetimeIndex | None = None, + scenarios: pd.Index | None = None, + contains: str | list[str] | None = None, + startswith: str | list[str] | None = None, + ) -> xr.Dataset: + """ + Filter the solution to a specific variable dimension and element. + If no element is specified, all elements are included. Args: - variable_dims: Variable dimension to filter. - - Returns: - xr.Dataset: Filtered solution dataset. + variable_dims: The dimension of which to get variables from. + - 'scalar': Get scalar variables (without dimensions) + - 'time': Get time-dependent variables (with a time dimension) + - 'scenario': Get scenario-dependent variables (with ONLY a scenario dimension) + - 'timeonly': Get time-dependent variables (with ONLY a time dimension) + - 'scenarioonly': Get scenario-dependent variables (with ONLY a scenario dimension) + timesteps: Optional time indexes to select. Can be: + - pd.DatetimeIndex: Multiple timesteps + - str/pd.Timestamp: Single timestep + Defaults to all available timesteps. + scenarios: Optional scenario indexes to select. Can be: + - pd.Index: Multiple scenarios + - str/int: Single scenario (int is treated as a label, not an index position) + Defaults to all available scenarios. + contains: Filter variables that contain this string or strings. + If a list is provided, variables must contain ALL strings in the list. + startswith: Filter variables that start with this string or strings. + If a list is provided, variables must start with ANY of the strings in the list. """ - return filter_dataset(self.solution, variable_dims) + return filter_dataset( + self.solution, + variable_dims=variable_dims, + timesteps=timesteps, + scenarios=scenarios, + contains=contains, + startswith=startswith, + ) class _NodeResults(_ElementResults): - @classmethod - def from_json(cls, calculation_results, json_data: dict) -> _NodeResults: - return cls( - calculation_results, - json_data['label'], - json_data['variables'], - json_data['constraints'], - json_data['inputs'], - json_data['outputs'], - ) - def __init__( self, calculation_results: CalculationResults, @@ -418,10 +907,12 @@ def __init__( constraints: list[str], inputs: list[str], outputs: list[str], + flows: list[str], ): super().__init__(calculation_results, label, variables, constraints) self.inputs = inputs self.outputs = outputs + self.flows = flows def plot_node_balance( self, @@ -429,32 +920,47 @@ def plot_node_balance( show: bool = True, colors: plotting.ColorType = 'viridis', engine: plotting.PlottingEngine = 'plotly', + indexer: dict[FlowSystemDimensions, Any] | None = None, + mode: Literal['flow_rate', 'flow_hours'] = 'flow_rate', + style: Literal['area', 'stacked_bar', 'line'] = 'stacked_bar', + drop_suffix: bool = True, ) -> plotly.graph_objs.Figure | tuple[plt.Figure, plt.Axes]: - """Plot node balance flows. - + """ + Plots the node balance of the Component or Bus. Args: - save: Whether to save plot (path or boolean). - show: Whether to display plot. - colors: Color scheme. Also see plotly. - engine: Plotting engine ('plotly' or 'matplotlib'). - - Returns: - Figure object. + save: Whether to save the plot or not. If a path is provided, the plot will be saved at that location. + show: Whether to show the plot or not. + colors: The colors to use for the plot. See `flixopt.plotting.ColorType` for options. + engine: The engine to use for plotting. Can be either 'plotly' or 'matplotlib'. + indexer: Optional selection dict, e.g., {'scenario': 'base', 'period': 2024}. + If None, uses first value for each dimension (except time). + If empty dict {}, uses all values. + style: The style to use for the dataset. Can be 'flow_rate' or 'flow_hours'. + - 'flow_rate': Returns the flow_rates of the Node. + - 'flow_hours': Returns the flow_hours of the Node. [flow_hours(t) = flow_rate(t) * dt(t)]. Renames suffixes to |flow_hours. + drop_suffix: Whether to drop the suffix from the variable names. """ + ds = self.node_balance(with_last_timestep=True, mode=mode, drop_suffix=drop_suffix, indexer=indexer) + + ds, suffix_parts = _apply_indexer_to_data(ds, indexer, drop=True) + suffix = '--' + '-'.join(suffix_parts) if suffix_parts else '' + + title = f'{self.label} (flow rates){suffix}' if mode == 'flow_rate' else f'{self.label} (flow hours){suffix}' + if engine == 'plotly': figure_like = plotting.with_plotly( - self.node_balance(with_last_timestep=True).to_dataframe(), + ds.to_dataframe(), colors=colors, - mode='area', - title=f'Flow rates of {self.label}', + style=style, + title=title, ) default_filetype = '.html' elif engine == 'matplotlib': figure_like = plotting.with_matplotlib( - self.node_balance(with_last_timestep=True).to_dataframe(), + ds.to_dataframe(), colors=colors, - mode='bar', - title=f'Flow rates of {self.label}', + style=style, + title=title, ) default_filetype = '.png' else: @@ -462,7 +968,7 @@ def plot_node_balance( return plotting.export_figure( figure_like=figure_like, - default_path=self._calculation_results.folder / f'{self.label} (flow rates)', + default_path=self._calculation_results.folder / title, default_filetype=default_filetype, user_path=None if isinstance(save, bool) else pathlib.Path(save), show=show, @@ -477,9 +983,9 @@ def plot_node_balance_pie( save: bool | pathlib.Path = False, show: bool = True, engine: plotting.PlottingEngine = 'plotly', + indexer: dict[FlowSystemDimensions, Any] | None = None, ) -> plotly.graph_objs.Figure | tuple[plt.Figure, list[plt.Axes]]: """Plot pie chart of flow hours distribution. - Args: lower_percentage_group: Percentage threshold for "Others" grouping. colors: Color scheme. Also see plotly. @@ -487,32 +993,40 @@ def plot_node_balance_pie( save: Whether to save plot. show: Whether to display plot. engine: Plotting engine ('plotly' or 'matplotlib'). + indexer: Optional selection dict, e.g., {'scenario': 'base', 'period': 2024}. + If None, uses first value for each dimension. + If empty dict {}, uses all values. """ - inputs = ( - sanitize_dataset( - ds=self.solution[self.inputs], - threshold=1e-5, - drop_small_vars=True, - zero_small_values=True, - ) - * self._calculation_results.hours_per_timestep + inputs = sanitize_dataset( + ds=self.solution[self.inputs] * self._calculation_results.hours_per_timestep, + threshold=1e-5, + drop_small_vars=True, + zero_small_values=True, + drop_suffix='|', ) - outputs = ( - sanitize_dataset( - ds=self.solution[self.outputs], - threshold=1e-5, - drop_small_vars=True, - zero_small_values=True, - ) - * self._calculation_results.hours_per_timestep + outputs = sanitize_dataset( + ds=self.solution[self.outputs] * self._calculation_results.hours_per_timestep, + threshold=1e-5, + drop_small_vars=True, + zero_small_values=True, + drop_suffix='|', ) + inputs, suffix_parts = _apply_indexer_to_data(inputs, indexer, drop=True) + outputs, suffix_parts = _apply_indexer_to_data(outputs, indexer, drop=True) + suffix = '--' + '-'.join(suffix_parts) if suffix_parts else '' + + title = f'{self.label} (total flow hours){suffix}' + + inputs = inputs.sum('time') + outputs = outputs.sum('time') + if engine == 'plotly': figure_like = plotting.dual_pie_with_plotly( - inputs.to_dataframe().sum(), - outputs.to_dataframe().sum(), + data_left=inputs.to_pandas(), + data_right=outputs.to_pandas(), colors=colors, - title=f'Flow hours of {self.label}', + title=title, text_info=text_info, subtitles=('Inputs', 'Outputs'), legend_title='Flows', @@ -522,10 +1036,10 @@ def plot_node_balance_pie( elif engine == 'matplotlib': logger.debug('Parameter text_info is not supported for matplotlib') figure_like = plotting.dual_pie_with_matplotlib( - inputs.to_dataframe().sum(), - outputs.to_dataframe().sum(), + data_left=inputs.to_pandas(), + data_right=outputs.to_pandas(), colors=colors, - title=f'Total flow hours of {self.label}', + title=title, subtitles=('Inputs', 'Outputs'), legend_title='Flows', lower_percentage_group=lower_percentage_group, @@ -536,7 +1050,7 @@ def plot_node_balance_pie( return plotting.export_figure( figure_like=figure_like, - default_path=self._calculation_results.folder / f'{self.label} (total flow hours)', + default_path=self._calculation_results.folder / title, default_filetype=default_filetype, user_path=None if isinstance(save, bool) else pathlib.Path(save), show=show, @@ -549,9 +1063,29 @@ def node_balance( negate_outputs: bool = False, threshold: float | None = 1e-5, with_last_timestep: bool = False, + mode: Literal['flow_rate', 'flow_hours'] = 'flow_rate', + drop_suffix: bool = False, + indexer: dict[FlowSystemDimensions, Any] | None = None, ) -> xr.Dataset: - return sanitize_dataset( - ds=self.solution[self.inputs + self.outputs], + """ + Returns a dataset with the node balance of the Component or Bus. + Args: + negate_inputs: Whether to negate the input flow_rates of the Node. + negate_outputs: Whether to negate the output flow_rates of the Node. + threshold: The threshold for small values. Variables with all values below the threshold are dropped. + with_last_timestep: Whether to include the last timestep in the dataset. + mode: The mode to use for the dataset. Can be 'flow_rate' or 'flow_hours'. + - 'flow_rate': Returns the flow_rates of the Node. + - 'flow_hours': Returns the flow_hours of the Node. [flow_hours(t) = flow_rate(t) * dt(t)]. Renames suffixes to |flow_hours. + drop_suffix: Whether to drop the suffix from the variable names. + indexer: Optional selection dict, e.g., {'scenario': 'base', 'period': 2024}. + If None, uses first value for each dimension. + If empty dict {}, uses all values. + """ + ds = self.solution[self.inputs + self.outputs] + + ds = sanitize_dataset( + ds=ds, threshold=threshold, timesteps=self._calculation_results.timesteps_extra if with_last_timestep else None, negate=( @@ -563,8 +1097,17 @@ def node_balance( if negate_inputs else None ), + drop_suffix='|' if drop_suffix else None, ) + ds, _ = _apply_indexer_to_data(ds, indexer, drop=True) + + if mode == 'flow_hours': + ds = ds * self._calculation_results.hours_per_timestep + ds = ds.rename_vars({var: var.replace('flow_rate', 'flow_hours') for var in ds.data_vars}) + + return ds + class BusResults(_NodeResults): """Results container for energy/material balance nodes in the system.""" @@ -594,48 +1137,68 @@ def plot_charge_state( show: bool = True, colors: plotting.ColorType = 'viridis', engine: plotting.PlottingEngine = 'plotly', + style: Literal['area', 'stacked_bar', 'line'] = 'stacked_bar', + indexer: dict[FlowSystemDimensions, Any] | None = None, ) -> plotly.graph_objs.Figure: """Plot storage charge state over time, combined with the node balance. Args: - save: Whether to save plot. - show: Whether to display plot. + save: Whether to save the plot or not. If a path is provided, the plot will be saved at that location. + show: Whether to show the plot or not. colors: Color scheme. Also see plotly. - engine: Plotting engine (only 'plotly' supported). - - Returns: - plotly.graph_objs.Figure: Charge state plot. + engine: Plotting engine to use. Only 'plotly' is implemented atm. + style: The colors to use for the plot. See `flixopt.plotting.ColorType` for options. + indexer: Optional selection dict, e.g., {'scenario': 'base', 'period': 2024}. + If None, uses first value for each dimension. + If empty dict {}, uses all values. Raises: ValueError: If component is not a storage. """ - if engine != 'plotly': - raise NotImplementedError( - f'Plotting engine "{engine}" not implemented for ComponentResults.plot_charge_state.' - ) - if not self.is_storage: raise ValueError(f'Cant plot charge_state. "{self.label}" is not a storage') - fig = plotting.with_plotly( - self.node_balance(with_last_timestep=True).to_dataframe(), - colors=colors, - mode='area', - title=f'Operation Balance of {self.label}', - ) + ds = self.node_balance(with_last_timestep=True, indexer=indexer) + charge_state = self.charge_state + + ds, suffix_parts = _apply_indexer_to_data(ds, indexer, drop=True) + charge_state, suffix_parts = _apply_indexer_to_data(charge_state, indexer, drop=True) + suffix = '--' + '-'.join(suffix_parts) if suffix_parts else '' - # TODO: Use colors for charge state? + title = f'Operation Balance of {self.label}{suffix}' - charge_state = self.charge_state.to_dataframe() - fig.add_trace( - plotly.graph_objs.Scatter( - x=charge_state.index, y=charge_state.values.flatten(), mode='lines', name=self._charge_state + if engine == 'plotly': + fig = plotting.with_plotly( + ds.to_dataframe(), + colors=colors, + style=style, + title=title, ) - ) + + # TODO: Use colors for charge state? + + charge_state = charge_state.to_dataframe() + fig.add_trace( + plotly.graph_objs.Scatter( + x=charge_state.index, y=charge_state.values.flatten(), mode='lines', name=self._charge_state + ) + ) + elif engine == 'matplotlib': + fig, ax = plotting.with_matplotlib( + ds.to_dataframe(), + colors=colors, + style=style, + title=title, + ) + + charge_state = charge_state.to_dataframe() + ax.plot(charge_state.index, charge_state.values.flatten(), label=self._charge_state) + fig.tight_layout() + fig = fig, ax return plotting.export_figure( fig, - default_path=self._calculation_results.folder / f'{self.label} (charge state)', + default_path=self._calculation_results.folder / title, default_filetype='.html', user_path=None if isinstance(save, bool) else pathlib.Path(save), show=show, @@ -692,6 +1255,42 @@ def get_shares_from(self, element: str): return self.solution[[name for name in self._variable_names if name.startswith(f'{element}->')]] +class FlowResults(_ElementResults): + def __init__( + self, + calculation_results: CalculationResults, + label: str, + variables: list[str], + constraints: list[str], + start: str, + end: str, + component: str, + ): + super().__init__(calculation_results, label, variables, constraints) + self.start = start + self.end = end + self.component = component + + @property + def flow_rate(self) -> xr.DataArray: + return self.solution[f'{self.label}|flow_rate'] + + @property + def flow_hours(self) -> xr.DataArray: + return (self.flow_rate * self._calculation_results.hours_per_timestep).rename(f'{self.label}|flow_hours') + + @property + def size(self) -> xr.DataArray: + name = f'{self.label}|size' + if name in self.solution: + return self.solution[name] + try: + return self._calculation_results.flow_system.flows[self.label].size.rename(name) + except _FlowSystemRestorationError: + logger.critical(f'Size of flow {self.label}.size not availlable. Returning NaN') + return xr.DataArray(np.nan).rename(name) + + class SegmentedCalculationResults: """Results container for segmented optimization calculations with temporal decomposition. @@ -780,7 +1379,7 @@ class SegmentedCalculationResults: identify potential issues from segmentation approach. Common Use Cases: - - **Large-Scale Analysis**: Annual or multi-year optimization results + - **Large-Scale Analysis**: Annual or multi-period optimization results - **Memory-Constrained Systems**: Results from systems exceeding hardware limits - **Segment Validation**: Verifying segmentation approach effectiveness - **Performance Monitoring**: Comparing segmented vs. full-horizon solutions @@ -816,7 +1415,7 @@ def from_file(cls, folder: str | pathlib.Path, name: str): with open(path.with_suffix('.json'), encoding='utf-8') as f: meta_data = json.load(f) return cls( - [CalculationResults.from_file(folder, name) for name in meta_data['sub_calculations']], + [CalculationResults.from_file(folder, sub_name) for sub_name in meta_data['sub_calculations']], all_timesteps=pd.DatetimeIndex( [datetime.datetime.fromisoformat(date) for date in meta_data['all_timesteps']], name='time' ), @@ -841,7 +1440,7 @@ def __init__( self.overlap_timesteps = overlap_timesteps self.name = name self.folder = pathlib.Path(folder) if folder is not None else pathlib.Path.cwd() / 'results' - self.hours_per_timestep = TimeSeriesCollection.calculate_hours_per_timestep(self.all_timesteps) + self.hours_per_timestep = FlowSystem.calculate_hours_per_timestep(self.all_timesteps) @property def meta_data(self) -> dict[str, int | list[str]]: @@ -926,7 +1525,7 @@ def to_file(self, folder: str | pathlib.Path | None = None, name: str | None = N f'Folder {folder} and its parent do not exist. Please create them first.' ) from e for segment in self.segment_results: - segment.to_file(folder=folder, name=f'{name}-{segment.name}', compression=compression) + segment.to_file(folder=folder, name=segment.name, compression=compression) with open(path.with_suffix('.json'), 'w', encoding='utf-8') as f: json.dump(self.meta_data, f, indent=4, ensure_ascii=False) @@ -943,6 +1542,7 @@ def plot_heatmap( save: bool | pathlib.Path = False, show: bool = True, engine: plotting.PlottingEngine = 'plotly', + indexer: dict[str, Any] | None = None, ): """Plot heatmap of time series data. @@ -956,10 +1556,14 @@ def plot_heatmap( save: Whether to save plot. show: Whether to display plot. engine: Plotting engine. - - Returns: - Figure object. + indexer: Optional selection dict, e.g., {'scenario': 'base', 'period': 2024}. + If None, uses first value for each dimension. + If empty dict {}, uses all values. """ + dataarray, suffix_parts = _apply_indexer_to_data(dataarray, indexer, drop=True) + suffix = '--' + '-'.join(suffix_parts) if suffix_parts else '' + name = name if not suffix_parts else name + suffix + heatmap_data = plotting.heat_map_data_from_df( dataarray.to_dataframe(name), heatmap_timeframes, heatmap_timesteps_per_frame, 'ffill' ) @@ -996,6 +1600,7 @@ def sanitize_dataset( negate: list[str] | None = None, drop_small_vars: bool = True, zero_small_values: bool = False, + drop_suffix: str | None = None, ) -> xr.Dataset: """Clean dataset by handling small values and reindexing time. @@ -1006,9 +1611,7 @@ def sanitize_dataset( negate: Variables to negate. drop_small_vars: Whether to drop variables below threshold. zero_small_values: Whether to zero values below threshold. - - Returns: - xr.Dataset: Sanitized dataset. + drop_suffix: Drop suffix of data var names. Split by the provided str. """ # Create a copy to avoid modifying the original ds = ds.copy() @@ -1044,28 +1647,206 @@ def sanitize_dataset( if timesteps is not None and not ds.indexes['time'].equals(timesteps): ds = ds.reindex({'time': timesteps}, fill_value=np.nan) + if drop_suffix is not None: + if not isinstance(drop_suffix, str): + raise ValueError(f'Only pass str values to drop suffixes. Got {drop_suffix}') + unique_dict = {} + for var in ds.data_vars: + new_name = var.split(drop_suffix)[0] + + # If name already exists, keep original name + if new_name in unique_dict.values(): + unique_dict[var] = var + else: + unique_dict[var] = new_name + ds = ds.rename(unique_dict) + return ds def filter_dataset( ds: xr.Dataset, - variable_dims: Literal['scalar', 'time'] | None = None, + variable_dims: Literal['scalar', 'time', 'scenario', 'timeonly', 'scenarioonly'] | None = None, + timesteps: pd.DatetimeIndex | str | pd.Timestamp | None = None, + scenarios: pd.Index | str | int | None = None, + contains: str | list[str] | None = None, + startswith: str | list[str] | None = None, ) -> xr.Dataset: - """Filter dataset by variable dimensions. + """Filter dataset by variable dimensions, indexes, and with string filters for variable names. Args: - ds: Dataset to filter. - variable_dims: Variable dimension to filter ('scalar' or 'time'). + ds: The dataset to filter. + variable_dims: The dimension of which to get variables from. + - 'scalar': Get scalar variables (without dimensions) + - 'time': Get time-dependent variables (with a time dimension) + - 'scenario': Get scenario-dependent variables (with ONLY a scenario dimension) + - 'timeonly': Get time-dependent variables (with ONLY a time dimension) + - 'scenarioonly': Get scenario-dependent variables (with ONLY a scenario dimension) + timesteps: Optional time indexes to select. Can be: + - pd.DatetimeIndex: Multiple timesteps + - str/pd.Timestamp: Single timestep + Defaults to all available timesteps. + scenarios: Optional scenario indexes to select. Can be: + - pd.Index: Multiple scenarios + - str/int: Single scenario (int is treated as a label, not an index position) + Defaults to all available scenarios. + contains: Filter variables that contain this string or strings. + If a list is provided, variables must contain ALL strings in the list. + startswith: Filter variables that start with this string or strings. + If a list is provided, variables must start with ANY of the strings in the list. + """ + # First filter by dimensions + filtered_ds = ds.copy() + if variable_dims is not None: + if variable_dims == 'scalar': + filtered_ds = filtered_ds[[v for v in filtered_ds.data_vars if not filtered_ds[v].dims]] + elif variable_dims == 'time': + filtered_ds = filtered_ds[[v for v in filtered_ds.data_vars if 'time' in filtered_ds[v].dims]] + elif variable_dims == 'scenario': + filtered_ds = filtered_ds[[v for v in filtered_ds.data_vars if 'scenario' in filtered_ds[v].dims]] + elif variable_dims == 'timeonly': + filtered_ds = filtered_ds[[v for v in filtered_ds.data_vars if filtered_ds[v].dims == ('time',)]] + elif variable_dims == 'scenarioonly': + filtered_ds = filtered_ds[[v for v in filtered_ds.data_vars if filtered_ds[v].dims == ('scenario',)]] + else: + raise ValueError(f'Unknown variable_dims "{variable_dims}" for filter_dataset') + + # Filter by 'contains' parameter + if contains is not None: + if isinstance(contains, str): + # Single string - keep variables that contain this string + filtered_ds = filtered_ds[[v for v in filtered_ds.data_vars if contains in v]] + elif isinstance(contains, list) and all(isinstance(s, str) for s in contains): + # List of strings - keep variables that contain ALL strings in the list + filtered_ds = filtered_ds[[v for v in filtered_ds.data_vars if all(s in v for s in contains)]] + else: + raise TypeError(f"'contains' must be a string or list of strings, got {type(contains)}") + + # Filter by 'startswith' parameter + if startswith is not None: + if isinstance(startswith, str): + # Single string - keep variables that start with this string + filtered_ds = filtered_ds[[v for v in filtered_ds.data_vars if v.startswith(startswith)]] + elif isinstance(startswith, list) and all(isinstance(s, str) for s in startswith): + # List of strings - keep variables that start with ANY of the strings in the list + filtered_ds = filtered_ds[[v for v in filtered_ds.data_vars if any(v.startswith(s) for s in startswith)]] + else: + raise TypeError(f"'startswith' must be a string or list of strings, got {type(startswith)}") + + # Handle time selection if needed + if timesteps is not None and 'time' in filtered_ds.dims: + try: + filtered_ds = filtered_ds.sel(time=timesteps) + except KeyError as e: + available_times = set(filtered_ds.indexes['time']) + requested_times = set([timesteps]) if not isinstance(timesteps, pd.Index) else set(timesteps) + missing_times = requested_times - available_times + raise ValueError( + f'Timesteps not found in dataset: {missing_times}. Available times: {available_times}' + ) from e + + # Handle scenario selection if needed + if scenarios is not None and 'scenario' in filtered_ds.dims: + try: + filtered_ds = filtered_ds.sel(scenario=scenarios) + except KeyError as e: + available_scenarios = set(filtered_ds.indexes['scenario']) + requested_scenarios = set([scenarios]) if not isinstance(scenarios, pd.Index) else set(scenarios) + missing_scenarios = requested_scenarios - available_scenarios + raise ValueError( + f'Scenarios not found in dataset: {missing_scenarios}. Available scenarios: {available_scenarios}' + ) from e + + return filtered_ds + + +def filter_dataarray_by_coord(da: xr.DataArray, **kwargs: str | list[str] | None) -> xr.DataArray: + """Filter flows by node and component attributes. + + Filters are applied in the order they are specified. All filters must match for an edge to be included. + + To recombine filtered dataarrays, use `xr.concat`. + + xr.concat([res.sizes(start='Fernwärme'), res.sizes(end='Fernwärme')], dim='flow') + + Args: + da: Flow DataArray with network metadata coordinates. + **kwargs: Coord filters as name=value pairs. Returns: - xr.Dataset: Filtered dataset. + Filtered DataArray with matching edges. + + Raises: + AttributeError: If required coordinates are missing. + ValueError: If specified nodes don't exist or no matches found. """ - if variable_dims is None: - return ds - if variable_dims == 'scalar': - return ds[[name for name, da in ds.data_vars.items() if len(da.dims) == 0]] - elif variable_dims == 'time': - return ds[[name for name, da in ds.data_vars.items() if 'time' in da.dims]] + # Helper function to process filters + def apply_filter(array, coord_name: str, coord_values: Any | list[Any]): + # Verify coord exists + if coord_name not in array.coords: + raise AttributeError(f"Missing required coordinate '{coord_name}'") + + # Convert single value to list + val_list = [coord_values] if isinstance(coord_values, str) else coord_values + + # Verify coord_values exist + available = set(array[coord_name].values) + missing = [v for v in val_list if v not in available] + if missing: + raise ValueError(f'{coord_name.title()} value(s) not found: {missing}') + + # Apply filter + return array.where( + array[coord_name].isin(val_list) if isinstance(coord_values, list) else array[coord_name] == coord_values, + drop=True, + ) + + # Apply filters from kwargs + filters = {k: v for k, v in kwargs.items() if v is not None} + try: + for coord, values in filters.items(): + da = apply_filter(da, coord, values) + except ValueError as e: + raise ValueError(f'No edges match criteria: {filters}') from e + + # Verify results exist + if da.size == 0: + raise ValueError(f'No edges match criteria: {filters}') + + return da + + +def _apply_indexer_to_data( + data: xr.DataArray | xr.Dataset, indexer: dict[str, Any] | None = None, drop=False +) -> tuple[xr.DataArray | xr.Dataset, list[str]]: + """ + Apply indexer selection or auto-select first values for non-time dimensions. + + Args: + data: xarray Dataset or DataArray + indexer: Optional selection dict + If None, uses first value for each dimension (except time). + If empty dict {}, uses all values. + + Returns: + Tuple of (selected_data, selection_string) + """ + selection_string = [] + + if indexer is not None: + # User provided indexer + data = data.sel(indexer, drop=drop) + selection_string.extend(f'{v}[{k}]' for k, v in indexer.items()) else: - raise ValueError(f'Not allowed value for "filter_dataset()": {variable_dims=}') + # Auto-select first value for each dimension except 'time' + selection = {} + for dim in data.dims: + if dim != 'time' and dim in data.coords: + first_value = data.coords[dim].values[0] + selection[dim] = first_value + selection_string.append(f'{first_value}[{dim}]') + if selection: + data = data.sel(selection, drop=drop) + + return data, selection_string diff --git a/flixopt/structure.py b/flixopt/structure.py index c5519066c..72efc3df2 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -8,22 +8,27 @@ import inspect import json import logging -from datetime import datetime +from dataclasses import dataclass from io import StringIO -from typing import TYPE_CHECKING, Any, Literal +from typing import ( + TYPE_CHECKING, + Any, + Literal, +) import linopy import numpy as np +import pandas as pd import xarray as xr from rich.console import Console from rich.pretty import Pretty -from .core import TimeSeries, TimeSeriesData +from . import io as fx_io +from .core import TimeSeriesData, get_dataarray_stats if TYPE_CHECKING: # for type checking and preventing circular imports import pathlib - - import pandas as pd + from collections.abc import Collection, ItemsView, Iterator from .effects import EffectCollectionModel from .flow_system import FlowSystem @@ -46,215 +51,806 @@ def register_class_for_io(cls): return cls -class SystemModel(linopy.Model): +class SubmodelsMixin: + """Mixin that provides submodel functionality for both FlowSystemModel and Submodel.""" + + submodels: Submodels + + @property + def all_submodels(self) -> list[Submodel]: + """Get all submodels including nested ones recursively.""" + direct_submodels = list(self.submodels.values()) + + # Recursively collect nested sub-models + nested_submodels = [] + for submodel in direct_submodels: + nested_submodels.extend(submodel.all_submodels) + + return direct_submodels + nested_submodels + + def add_submodels(self, submodel: Submodel, short_name: str = None) -> Submodel: + """Register a sub-model with the model""" + if short_name is None: + short_name = submodel.__class__.__name__ + if short_name in self.submodels: + raise ValueError(f'Short name "{short_name}" already assigned to model') + self.submodels.add(submodel, name=short_name) + + return submodel + + +class FlowSystemModel(linopy.Model, SubmodelsMixin): """ - The SystemModel is the linopy Model that is used to create the mathematical model of the flow_system. + The FlowSystemModel is the linopy Model that is used to create the mathematical model of the flow_system. It is used to create and store the variables and constraints for the flow_system. + + Args: + flow_system: The flow_system that is used to create the model. + normalize_weights: Whether to automatically normalize the weights to sum up to 1 when solving. """ - def __init__(self, flow_system: FlowSystem): - """ - Args: - flow_system: The flow_system that is used to create the model. - """ + def __init__(self, flow_system: FlowSystem, normalize_weights: bool): super().__init__(force_dim_names=True) self.flow_system = flow_system - self.time_series_collection = flow_system.time_series_collection + self.normalize_weights = normalize_weights self.effects: EffectCollectionModel | None = None + self.submodels: Submodels = Submodels({}) def do_modeling(self): self.effects = self.flow_system.effects.create_model(self) - self.effects.do_modeling() - component_models = [component.create_model(self) for component in self.flow_system.components.values()] - bus_models = [bus.create_model(self) for bus in self.flow_system.buses.values()] - for component_model in component_models: - component_model.do_modeling() - for bus_model in bus_models: # Buses after Components, because FlowModels are created in ComponentModels - bus_model.do_modeling() + for component in self.flow_system.components.values(): + component.create_model(self) + for bus in self.flow_system.buses.values(): + bus.create_model(self) + + # Add scenario equality constraints after all elements are modeled + self._add_scenario_equality_constraints() + + def _add_scenario_equality_for_parameter_type( + self, + parameter_type: Literal['flow_rate', 'size'], + config: bool | list[str], + ): + """Add scenario equality constraints for a specific parameter type. + + Args: + parameter_type: The type of parameter ('flow_rate' or 'size') + config: Configuration value (True = equalize all, False = equalize none, list = equalize these) + """ + if config is False: + return # All vary per scenario, no constraints needed + + suffix = f'|{parameter_type}' + if config is True: + # All should be scenario-independent + vars_to_constrain = [var for var in self.variables if var.endswith(suffix)] + else: + # Only those in the list should be scenario-independent + all_vars = [var for var in self.variables if var.endswith(suffix)] + to_equalize = {f'{element}{suffix}' for element in config} + vars_to_constrain = [var for var in all_vars if var in to_equalize] + + # Validate that all specified variables exist + missing_vars = [v for v in vars_to_constrain if v not in self.variables] + if missing_vars: + param_name = 'scenario_independent_sizes' if parameter_type == 'size' else 'scenario_independent_flow_rates' + raise ValueError(f'{param_name} contains invalid labels: {missing_vars}') + + logger.debug(f'Adding scenario equality constraints for {len(vars_to_constrain)} {parameter_type} variables') + for var in vars_to_constrain: + self.add_constraints( + self.variables[var].isel(scenario=0) == self.variables[var].isel(scenario=slice(1, None)), + name=f'{var}|scenario_independent', + ) + + def _add_scenario_equality_constraints(self): + """Add equality constraints to equalize variables across scenarios based on FlowSystem configuration.""" + # Only proceed if we have scenarios + if self.flow_system.scenarios is None or len(self.flow_system.scenarios) <= 1: + return + + self._add_scenario_equality_for_parameter_type('flow_rate', self.flow_system.scenario_independent_flow_rates) + self._add_scenario_equality_for_parameter_type('size', self.flow_system.scenario_independent_sizes) @property def solution(self): solution = super().solution + solution['objective'] = self.objective.value solution.attrs = { 'Components': { - comp.label_full: comp.model.results_structure() + comp.label_full: comp.submodel.results_structure() for comp in sorted( self.flow_system.components.values(), key=lambda component: component.label_full.upper() ) }, 'Buses': { - bus.label_full: bus.model.results_structure() + bus.label_full: bus.submodel.results_structure() for bus in sorted(self.flow_system.buses.values(), key=lambda bus: bus.label_full.upper()) }, 'Effects': { - effect.label_full: effect.model.results_structure() + effect.label_full: effect.submodel.results_structure() for effect in sorted(self.flow_system.effects, key=lambda effect: effect.label_full.upper()) }, + 'Flows': { + flow.label_full: flow.submodel.results_structure() + for flow in sorted(self.flow_system.flows.values(), key=lambda flow: flow.label_full.upper()) + }, } - return solution.reindex(time=self.time_series_collection.timesteps_extra) + return solution.reindex(time=self.flow_system.timesteps_extra) @property def hours_per_step(self): - return self.time_series_collection.hours_per_timestep + return self.flow_system.hours_per_timestep @property def hours_of_previous_timesteps(self): - return self.time_series_collection.hours_of_previous_timesteps + return self.flow_system.hours_of_previous_timesteps - @property - def coords(self) -> tuple[pd.DatetimeIndex]: - return (self.time_series_collection.timesteps,) + def get_coords( + self, + dims: Collection[str] | None = None, + extra_timestep: bool = False, + ) -> xr.Coordinates | None: + """ + Returns the coordinates of the model + + Args: + dims: The dimensions to include in the coordinates. If None, includes all dimensions + extra_timestep: If True, uses extra timesteps instead of regular timesteps + + Returns: + The coordinates of the model, or None if no coordinates are available + + Raises: + ValueError: If extra_timestep=True but 'time' is not in dims + """ + if extra_timestep and dims is not None and 'time' not in dims: + raise ValueError('extra_timestep=True requires "time" to be included in dims') + + if dims is None: + coords = dict(self.flow_system.coords) + else: + coords = {k: v for k, v in self.flow_system.coords.items() if k in dims} + + if extra_timestep and coords: + coords['time'] = self.flow_system.timesteps_extra + + return xr.Coordinates(coords) if coords else None @property - def coords_extra(self) -> tuple[pd.DatetimeIndex]: - return (self.time_series_collection.timesteps_extra,) + def weights(self) -> int | xr.DataArray: + """Returns the weights of the FlowSystem. Normalizes to 1 if normalize_weights is True""" + if self.flow_system.weights is not None: + weights = self.flow_system.weights + else: + weights = self.flow_system.fit_to_model_coords('weights', 1, dims=['period', 'scenario']) + + if not self.normalize_weights: + return weights + + return weights / weights.sum() + + def __repr__(self) -> str: + """ + Return a string representation of the FlowSystemModel, borrowed from linopy.Model. + """ + # Extract content from existing representations + sections = { + f'Variables: [{len(self.variables)}]': self.variables.__repr__().split('\n', 2)[2], + f'Constraints: [{len(self.constraints)}]': self.constraints.__repr__().split('\n', 2)[2], + f'Submodels: [{len(self.submodels)}]': self.submodels.__repr__().split('\n', 2)[2], + 'Status': self.status, + } + + # Format sections with headers and underlines + formatted_sections = [] + for section_header, section_content in sections.items(): + formatted_sections.append(f'{section_header}\n{"-" * len(section_header)}\n{section_content}') + + title = f'FlowSystemModel ({self.type})' + all_sections = '\n'.join(formatted_sections) + + return f'{title}\n{"=" * len(title)}\n\n{all_sections}' class Interface: """ - This class is used to collect arguments about a Model. Its the base class for all Elements and Models in flixopt. + Base class for all Elements and Models in flixopt that provides serialization capabilities. + + This class enables automatic serialization/deserialization of objects containing xarray DataArrays + and nested Interface objects to/from xarray Datasets and NetCDF files. It uses introspection + of constructor parameters to automatically handle most serialization scenarios. + + Key Features: + - Automatic extraction and restoration of xarray DataArrays + - Support for nested Interface objects + - NetCDF and JSON export/import + - Recursive handling of complex nested structures + + Subclasses must implement: + transform_data(flow_system): Transform data to match FlowSystem dimensions """ - def transform_data(self, flow_system: FlowSystem): - """Transforms the data of the interface to match the FlowSystem's dimensions""" - raise NotImplementedError('Every Interface needs a transform_data() method') + def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None: + """Transform the data of the interface to match the FlowSystem's dimensions. + + Args: + flow_system: The FlowSystem containing timing and dimensional information + name_prefix: The prefix to use for the names of the variables. Defaults to '', which results in no prefix. - def infos(self, use_numpy: bool = True, use_element_label: bool = False) -> dict: + Raises: + NotImplementedError: Must be implemented by subclasses """ - Generate a dictionary representation of the object's constructor arguments. - Excludes default values and empty dictionaries and lists. - Converts data to be compatible with JSON. + raise NotImplementedError('Every Interface subclass needs a transform_data() method') - Args: - use_numpy: Whether to convert NumPy arrays to lists. Defaults to True. - If True, numeric numpy arrays (`np.ndarray`) are preserved as-is. - If False, they are converted to lists. - use_element_label: Whether to use the element label instead of the infos of the element. Defaults to False. - Note that Elements used as keys in dictionaries are always converted to their labels. + def _create_reference_structure(self) -> tuple[dict, dict[str, xr.DataArray]]: + """ + Convert all DataArrays to references and extract them. + This is the core method that both to_dict() and to_dataset() build upon. Returns: - A dictionary representation of the object's constructor arguments. + Tuple of (reference_structure, extracted_arrays_dict) + Raises: + ValueError: If DataArrays don't have unique names or are duplicated """ - # Get the constructor arguments and their default values - init_params = sorted( - inspect.signature(self.__init__).parameters.items(), - key=lambda x: (x[0].lower() != 'label', x[0].lower()), # Prioritize 'label' - ) - # Build a dict of attribute=value pairs, excluding defaults - details = {'class': ':'.join([cls.__name__ for cls in self.__class__.__mro__])} - for name, param in init_params: - if name == 'self': + # Get constructor parameters using caching for performance + if not hasattr(self, '_cached_init_params'): + self._cached_init_params = list(inspect.signature(self.__init__).parameters.keys()) + + # Process all constructor parameters + reference_structure = {'__class__': self.__class__.__name__} + all_extracted_arrays = {} + + for name in self._cached_init_params: + if name == 'self': # Skip self and timesteps. Timesteps are directly stored in Datasets + continue + + value = getattr(self, name, None) + + if value is None: continue - value, default = getattr(self, name, None), param.default - # Ignore default values and empty dicts and list - if np.all(value == default) or (isinstance(value, (dict, list)) and not value): + if isinstance(value, pd.Index): + logger.debug(f'Skipping {name=} because it is an Index') continue - details[name] = copy_and_convert_datatypes(value, use_numpy, use_element_label) - return details - def to_json(self, path: str | pathlib.Path): + # Extract arrays and get reference structure + processed_value, extracted_arrays = self._extract_dataarrays_recursive(value, name) + + # Check for array name conflicts + conflicts = set(all_extracted_arrays.keys()) & set(extracted_arrays.keys()) + if conflicts: + raise ValueError( + f'DataArray name conflicts detected: {conflicts}. ' + f'Each DataArray must have a unique name for serialization.' + ) + + # Add extracted arrays to the collection + all_extracted_arrays.update(extracted_arrays) + + # Only store in structure if it's not None/empty after processing + if processed_value is not None and not self._is_empty_container(processed_value): + reference_structure[name] = processed_value + + return reference_structure, all_extracted_arrays + + @staticmethod + def _is_empty_container(obj) -> bool: + """Check if object is an empty container (dict, list, tuple, set).""" + return isinstance(obj, (dict, list, tuple, set)) and len(obj) == 0 + + def _extract_dataarrays_recursive(self, obj, context_name: str = '') -> tuple[Any, dict[str, xr.DataArray]]: """ - Saves the element to a json file. - This not meant to be reloaded and recreate the object, but rather used to document or compare the object. + Recursively extract DataArrays from nested structures. Args: - path: The path to the json file. - """ - data = get_compact_representation(self.infos(use_numpy=True, use_element_label=True)) - with open(path, 'w', encoding='utf-8') as f: - json.dump(data, f, indent=4, ensure_ascii=False) + obj: Object to process + context_name: Name context for better error messages - def to_dict(self) -> dict: - """Convert the object to a dictionary representation.""" - data = {'__class__': self.__class__.__name__} + Returns: + Tuple of (processed_object_with_references, extracted_arrays_dict) - # Get the constructor parameters - init_params = inspect.signature(self.__init__).parameters + Raises: + ValueError: If DataArrays don't have unique names + """ + extracted_arrays = {} + + # Handle DataArrays directly - use their unique name + if isinstance(obj, xr.DataArray): + if not obj.name: + raise ValueError( + f'DataArrays must have a unique name for serialization. ' + f'Unnamed DataArray found in {context_name}. Please set array.name = "unique_name"' + ) - for name in init_params: - if name == 'self': - continue + array_name = str(obj.name) # Ensure string type + if array_name in extracted_arrays: + raise ValueError( + f'DataArray name "{array_name}" is duplicated in {context_name}. ' + f'Each DataArray must have a unique name for serialization.' + ) - value = getattr(self, name, None) - data[name] = self._serialize_value(value) - - return data - - def _serialize_value(self, value: Any): - """Helper method to serialize a value based on its type.""" - if value is None: - return None - elif isinstance(value, Interface): - return value.to_dict() - elif isinstance(value, (list, tuple)): - return self._serialize_list(value) - elif isinstance(value, dict): - return self._serialize_dict(value) + extracted_arrays[array_name] = obj + return f':::{array_name}', extracted_arrays + + # Handle Interface objects - extract their DataArrays too + elif isinstance(obj, Interface): + try: + interface_structure, interface_arrays = obj._create_reference_structure() + extracted_arrays.update(interface_arrays) + return interface_structure, extracted_arrays + except Exception as e: + raise ValueError(f'Failed to process nested Interface object in {context_name}: {e}') from e + + # Handle sequences (lists, tuples) + elif isinstance(obj, (list, tuple)): + processed_items = [] + for i, item in enumerate(obj): + item_context = f'{context_name}[{i}]' if context_name else f'item[{i}]' + processed_item, nested_arrays = self._extract_dataarrays_recursive(item, item_context) + extracted_arrays.update(nested_arrays) + processed_items.append(processed_item) + return processed_items, extracted_arrays + + # Handle dictionaries + elif isinstance(obj, dict): + processed_dict = {} + for key, value in obj.items(): + key_context = f'{context_name}.{key}' if context_name else str(key) + processed_value, nested_arrays = self._extract_dataarrays_recursive(value, key_context) + extracted_arrays.update(nested_arrays) + processed_dict[key] = processed_value + return processed_dict, extracted_arrays + + # Handle sets (convert to list for JSON compatibility) + elif isinstance(obj, set): + processed_items = [] + for i, item in enumerate(obj): + item_context = f'{context_name}.set_item[{i}]' if context_name else f'set_item[{i}]' + processed_item, nested_arrays = self._extract_dataarrays_recursive(item, item_context) + extracted_arrays.update(nested_arrays) + processed_items.append(processed_item) + return processed_items, extracted_arrays + + # For all other types, serialize to basic types else: - return value + return self._serialize_to_basic_types(obj), extracted_arrays + + def _handle_deprecated_kwarg( + self, + kwargs: dict, + old_name: str, + new_name: str, + current_value: Any = None, + transform: callable = None, + check_conflict: bool = True, + ) -> Any: + """ + Handle a deprecated keyword argument by issuing a warning and returning the appropriate value. + + This centralizes the deprecation pattern used across multiple classes (Source, Sink, InvestParameters, etc.). + + Args: + kwargs: Dictionary of keyword arguments to check and modify + old_name: Name of the deprecated parameter + new_name: Name of the replacement parameter + current_value: Current value of the new parameter (if already set) + transform: Optional callable to transform the old value before returning (e.g., lambda x: [x] to wrap in list) + check_conflict: Whether to check if both old and new parameters are specified (default: True). + Note: For parameters with non-None default values (e.g., bool parameters with default=False), + set check_conflict=False since we cannot distinguish between an explicit value and the default. + + Returns: + The value to use (either from old parameter or current_value) + + Raises: + ValueError: If both old and new parameters are specified and check_conflict is True + + Example: + # For parameters where None is the default (conflict checking works): + value = self._handle_deprecated_kwarg(kwargs, 'old_param', 'new_param', current_value) + + # For parameters with non-None defaults (disable conflict checking): + mandatory = self._handle_deprecated_kwarg( + kwargs, 'optional', 'mandatory', mandatory, + transform=lambda x: not x, + check_conflict=False # Cannot detect if mandatory was explicitly passed + ) + """ + import warnings + + old_value = kwargs.pop(old_name, None) + if old_value is not None: + warnings.warn( + f'The use of the "{old_name}" argument is deprecated. Use the "{new_name}" argument instead.', + DeprecationWarning, + stacklevel=3, # Stack: this method -> __init__ -> caller + ) + # Check for conflicts: only raise error if both were explicitly provided + if check_conflict and current_value is not None: + raise ValueError(f'Either {old_name} or {new_name} can be specified, but not both.') + + # Apply transformation if provided + if transform is not None: + return transform(old_value) + return old_value + + return current_value + + def _validate_kwargs(self, kwargs: dict, class_name: str = None) -> None: + """ + Validate that no unexpected keyword arguments are present in kwargs. + + This method uses inspect to get the actual function signature and filters out + any parameters that are not defined in the __init__ method, while also + handling the special case of 'kwargs' itself which can appear during deserialization. - def _serialize_list(self, items): - """Serialize a list of items.""" - return [self._serialize_value(item) for item in items] + Args: + kwargs: Dictionary of keyword arguments to validate + class_name: Optional class name for error messages. If None, uses self.__class__.__name__ + + Raises: + TypeError: If unexpected keyword arguments are found + """ + if not kwargs: + return - def _serialize_dict(self, d): - """Serialize a dictionary of items.""" - return {k: self._serialize_value(v) for k, v in d.items()} + import inspect + + sig = inspect.signature(self.__init__) + known_params = set(sig.parameters.keys()) - {'self', 'kwargs'} + # Also filter out 'kwargs' itself which can appear during deserialization + extra_kwargs = {k: v for k, v in kwargs.items() if k not in known_params and k != 'kwargs'} + + if extra_kwargs: + class_name = class_name or self.__class__.__name__ + unexpected_params = ', '.join(f"'{param}'" for param in extra_kwargs.keys()) + raise TypeError(f'{class_name}.__init__() got unexpected keyword argument(s): {unexpected_params}') @classmethod - def _deserialize_dict(cls, data: dict) -> dict | Interface: - if '__class__' in data: - class_name = data.pop('__class__') - try: - class_type = CLASS_REGISTRY[class_name] - if issubclass(class_type, Interface): - # Use _deserialize_dict to process the arguments - processed_data = {k: cls._deserialize_value(v) for k, v in data.items()} - return class_type(**processed_data) - else: - raise ValueError(f'Class "{class_name}" is not an Interface.') - except (AttributeError, KeyError) as e: - raise ValueError(f'Class "{class_name}" could not get reconstructed.') from e - else: - return {k: cls._deserialize_value(v) for k, v in data.items()} + def _resolve_dataarray_reference( + cls, reference: str, arrays_dict: dict[str, xr.DataArray] + ) -> xr.DataArray | TimeSeriesData: + """ + Resolve a single DataArray reference (:::name) to actual DataArray or TimeSeriesData. + + Args: + reference: Reference string starting with ":::" + arrays_dict: Dictionary of available DataArrays + + Returns: + Resolved DataArray or TimeSeriesData object + + Raises: + ValueError: If referenced array is not found + """ + array_name = reference[3:] # Remove ":::" prefix + if array_name not in arrays_dict: + raise ValueError(f"Referenced DataArray '{array_name}' not found in dataset") + + array = arrays_dict[array_name] + + # Handle null values with warning + if array.isnull().any(): + logger.error(f"DataArray '{array_name}' contains null values. Dropping all-null along present dims.") + if 'time' in array.dims: + array = array.dropna(dim='time', how='all') + + # Check if this should be restored as TimeSeriesData + if TimeSeriesData.is_timeseries_data(array): + return TimeSeriesData.from_dataarray(array) + + return array @classmethod - def _deserialize_list(cls, data: list) -> list: - return [cls._deserialize_value(value) for value in data] + def _resolve_reference_structure(cls, structure, arrays_dict: dict[str, xr.DataArray]): + """ + Convert reference structure back to actual objects using provided arrays. + + Args: + structure: Structure containing references (:::name) or special type markers + arrays_dict: Dictionary of available DataArrays + + Returns: + Structure with references resolved to actual DataArrays or objects + + Raises: + ValueError: If referenced arrays are not found or class is not registered + """ + # Handle DataArray references + if isinstance(structure, str) and structure.startswith(':::'): + return cls._resolve_dataarray_reference(structure, arrays_dict) + + elif isinstance(structure, list): + resolved_list = [] + for item in structure: + resolved_item = cls._resolve_reference_structure(item, arrays_dict) + if resolved_item is not None: # Filter out None values from missing references + resolved_list.append(resolved_item) + return resolved_list + + elif isinstance(structure, dict): + if structure.get('__class__'): + class_name = structure['__class__'] + if class_name not in CLASS_REGISTRY: + raise ValueError( + f"Class '{class_name}' not found in CLASS_REGISTRY. " + f'Available classes: {list(CLASS_REGISTRY.keys())}' + ) + + # This is a nested Interface object - restore it recursively + nested_class = CLASS_REGISTRY[class_name] + # Remove the __class__ key and process the rest + nested_data = {k: v for k, v in structure.items() if k != '__class__'} + # Resolve references in the nested data + resolved_nested_data = cls._resolve_reference_structure(nested_data, arrays_dict) + + try: + return nested_class(**resolved_nested_data) + except Exception as e: + raise ValueError(f'Failed to create instance of {class_name}: {e}') from e + else: + # Regular dictionary - resolve references in values + resolved_dict = {} + for key, value in structure.items(): + resolved_value = cls._resolve_reference_structure(value, arrays_dict) + if resolved_value is not None or value is None: # Keep None values if they were originally None + resolved_dict[key] = resolved_value + return resolved_dict + + else: + return structure + + def _serialize_to_basic_types(self, obj): + """ + Convert object to basic Python types only (no DataArrays, no custom objects). + + Args: + obj: Object to serialize + + Returns: + Object converted to basic Python types (str, int, float, bool, list, dict) + """ + if obj is None or isinstance(obj, (str, int, float, bool)): + return obj + elif isinstance(obj, np.integer): + return int(obj) + elif isinstance(obj, np.floating): + return float(obj) + elif isinstance(obj, np.bool_): + return bool(obj) + elif isinstance(obj, (np.ndarray, pd.Series, pd.DataFrame)): + return obj.tolist() if hasattr(obj, 'tolist') else list(obj) + elif isinstance(obj, dict): + return {k: self._serialize_to_basic_types(v) for k, v in obj.items()} + elif isinstance(obj, (list, tuple)): + return [self._serialize_to_basic_types(item) for item in obj] + elif isinstance(obj, set): + return [self._serialize_to_basic_types(item) for item in obj] + elif hasattr(obj, 'isoformat'): # datetime objects + return obj.isoformat() + elif hasattr(obj, '__dict__'): # Custom objects with attributes + logger.warning(f'Converting custom object {type(obj)} to dict representation: {obj}') + return {str(k): self._serialize_to_basic_types(v) for k, v in obj.__dict__.items()} + else: + # For any other object, try to convert to string as fallback + logger.error(f'Converting unknown type {type(obj)} to string: {obj}') + return str(obj) + + def to_dataset(self) -> xr.Dataset: + """ + Convert the object to an xarray Dataset representation. + All DataArrays become dataset variables, everything else goes to attrs. + + Its recommended to only call this method on Interfaces with all numeric data stored as xr.DataArrays. + Interfaces inside a FlowSystem are automatically converted this form after connecting and transforming the FlowSystem. + + Returns: + xr.Dataset: Dataset containing all DataArrays with basic objects only in attributes + + Raises: + ValueError: If serialization fails due to naming conflicts or invalid data + """ + try: + reference_structure, extracted_arrays = self._create_reference_structure() + # Create the dataset with extracted arrays as variables and structure as attrs + return xr.Dataset(extracted_arrays, attrs=reference_structure) + except Exception as e: + raise ValueError( + f'Failed to convert {self.__class__.__name__} to dataset. Its recommended to only call this method on ' + f'a fully connected and transformed FlowSystem, or Interfaces inside such a FlowSystem.' + f'Original Error: {e}' + ) from e + + def to_netcdf(self, path: str | pathlib.Path, compression: int = 0): + """ + Save the object to a NetCDF file. + + Args: + path: Path to save the NetCDF file + compression: Compression level (0-9) + + Raises: + ValueError: If serialization fails + IOError: If file cannot be written + """ + try: + ds = self.to_dataset() + fx_io.save_dataset_to_netcdf(ds, path, compression=compression) + except Exception as e: + raise OSError(f'Failed to save {self.__class__.__name__} to NetCDF file {path}: {e}') from e @classmethod - def _deserialize_value(cls, value: Any): - """Helper method to deserialize a value based on its type.""" - if value is None: - return None - elif isinstance(value, dict): - return cls._deserialize_dict(value) - elif isinstance(value, list): - return cls._deserialize_list(value) - return value + def from_dataset(cls, ds: xr.Dataset) -> Interface: + """ + Create an instance from an xarray Dataset. + + Args: + ds: Dataset containing the object data + + Returns: + Interface instance + + Raises: + ValueError: If dataset format is invalid or class mismatch + """ + try: + # Get class name and verify it matches + class_name = ds.attrs.get('__class__') + if class_name and class_name != cls.__name__: + logger.warning(f"Dataset class '{class_name}' doesn't match target class '{cls.__name__}'") + + # Get the reference structure from attrs + reference_structure = dict(ds.attrs) + + # Remove the class name since it's not a constructor parameter + reference_structure.pop('__class__', None) + + # Create arrays dictionary from dataset variables + arrays_dict = {name: array for name, array in ds.data_vars.items()} + + # Resolve all references using the centralized method + resolved_params = cls._resolve_reference_structure(reference_structure, arrays_dict) + + return cls(**resolved_params) + except Exception as e: + raise ValueError(f'Failed to create {cls.__name__} from dataset: {e}') from e @classmethod - def from_dict(cls, data: dict) -> Interface: + def from_netcdf(cls, path: str | pathlib.Path) -> Interface: """ - Create an instance from a dictionary representation. + Load an instance from a NetCDF file. Args: - data: Dictionary containing the data for the object. + path: Path to the NetCDF file + + Returns: + Interface instance + + Raises: + IOError: If file cannot be read + ValueError: If file format is invalid """ - return cls._deserialize_dict(data) + try: + ds = fx_io.load_dataset_from_netcdf(path) + return cls.from_dataset(ds) + except Exception as e: + raise OSError(f'Failed to load {cls.__name__} from NetCDF file {path}: {e}') from e - def __repr__(self): - # Get the constructor arguments and their current values - init_signature = inspect.signature(self.__init__) - init_args = init_signature.parameters + def get_structure(self, clean: bool = False, stats: bool = False) -> dict: + """ + Get object structure as a dictionary. - # Create a dictionary with argument names and their values - args_str = ', '.join(f'{name}={repr(getattr(self, name, None))}' for name in init_args if name != 'self') - return f'{self.__class__.__name__}({args_str})' + Args: + clean: If True, remove None and empty dicts and lists. + stats: If True, replace DataArray references with statistics + + Returns: + Dictionary representation of the object structure + """ + reference_structure, extracted_arrays = self._create_reference_structure() + + if stats: + # Replace references with statistics + reference_structure = self._replace_references_with_stats(reference_structure, extracted_arrays) + + if clean: + return fx_io.remove_none_and_empty(reference_structure) + return reference_structure + + def _replace_references_with_stats(self, structure, arrays_dict: dict[str, xr.DataArray]): + """Replace DataArray references with statistical summaries.""" + if isinstance(structure, str) and structure.startswith(':::'): + array_name = structure[3:] + if array_name in arrays_dict: + return get_dataarray_stats(arrays_dict[array_name]) + return structure + + elif isinstance(structure, dict): + return {k: self._replace_references_with_stats(v, arrays_dict) for k, v in structure.items()} + + elif isinstance(structure, list): + return [self._replace_references_with_stats(item, arrays_dict) for item in structure] + + return structure + + def to_json(self, path: str | pathlib.Path): + """ + Save the object to a JSON file. + This is meant for documentation and comparison, not for reloading. + + Args: + path: The path to the JSON file. + + Raises: + IOError: If file cannot be written + """ + try: + # Use the stats mode for JSON export (cleaner output) + data = self.get_structure(clean=True, stats=True) + with open(path, 'w', encoding='utf-8') as f: + json.dump(data, f, indent=4, ensure_ascii=False) + except Exception as e: + raise OSError(f'Failed to save {self.__class__.__name__} to JSON file {path}: {e}') from e + + def __repr__(self): + """Return a detailed string representation for debugging.""" + try: + # Get the constructor arguments and their current values + init_signature = inspect.signature(self.__init__) + init_args = init_signature.parameters + + # Create a dictionary with argument names and their values, with better formatting + args_parts = [] + for name in init_args: + if name == 'self': + continue + value = getattr(self, name, None) + # Truncate long representations + value_repr = repr(value) + if len(value_repr) > 50: + value_repr = value_repr[:47] + '...' + args_parts.append(f'{name}={value_repr}') + + args_str = ', '.join(args_parts) + return f'{self.__class__.__name__}({args_str})' + except Exception: + # Fallback if introspection fails + return f'{self.__class__.__name__}()' def __str__(self): - return get_str_representation(self.infos(use_numpy=True, use_element_label=True)) + """Return a user-friendly string representation.""" + try: + data = self.get_structure(clean=True, stats=True) + with StringIO() as output_buffer: + console = Console(file=output_buffer, width=1000) # Adjust width as needed + console.print(Pretty(data, expand_all=True, indent_guides=True)) + return output_buffer.getvalue() + except Exception: + # Fallback if structure generation fails + return f'{self.__class__.__name__} instance' + + def copy(self) -> Interface: + """ + Create a copy of the Interface object. + + Uses the existing serialization infrastructure to ensure proper copying + of all DataArrays and nested objects. + + Returns: + A new instance of the same class with copied data. + """ + # Convert to dataset, copy it, and convert back + dataset = self.to_dataset().copy(deep=True) + return self.__class__.from_dataset(dataset) + + def __copy__(self): + """Support for copy.copy().""" + return self.copy() + + def __deepcopy__(self, memo): + """Support for copy.deepcopy().""" + return self.copy() class Element(Interface): @@ -268,14 +864,14 @@ def __init__(self, label: str, meta_data: dict | None = None): """ self.label = Element._valid_label(label) self.meta_data = meta_data if meta_data is not None else {} - self.model: ElementModel | None = None + self.submodel: ElementModel | None = None def _plausibility_checks(self) -> None: """This function is used to do some basic plausibility checks for each Element during initialization. This is run after all data is transformed to the correct format/type""" raise NotImplementedError('Every Element needs a _plausibility_checks() method') - def create_model(self, model: SystemModel) -> ElementModel: + def create_model(self, model: FlowSystemModel) -> ElementModel: raise NotImplementedError('Every Element needs a create_model() method') @property @@ -299,64 +895,100 @@ def _valid_label(label: str) -> str: f'Use any other symbol instead' ) if label.endswith(' '): - logger.warning(f'Label "{label}" ends with a space. This will be removed.') + logger.error(f'Label "{label}" ends with a space. This will be removed.') return label.rstrip() return label -class Model: - """Stores Variables and Constraints.""" +class Submodel(SubmodelsMixin): + """Stores Variables and Constraints. Its a subset of a FlowSystemModel. + Variables and constraints are stored in the main FlowSystemModel, and are referenced here. + Can have other Submodels assigned, and can be a Submodel of another Submodel. + """ - def __init__(self, model: SystemModel, label_of_element: str, label: str = '', label_full: str | None = None): + def __init__(self, model: FlowSystemModel, label_of_element: str, label_of_model: str | None = None): """ Args: - model: The SystemModel that is used to create the model. + model: The FlowSystemModel that is used to create the model. label_of_element: The label of the parent (Element). Used to construct the full label of the model. - label: The label of the model. Used to construct the full label of the model. - label_full: The full label of the model. Can overwrite the full label constructed from the other labels. + label_of_model: The label of the model. Used as a prefix in all variables and constraints. """ self._model = model self.label_of_element = label_of_element - self._label = label - self._label_full = label_full - - self._variables_direct: list[str] = [] - self._constraints_direct: list[str] = [] - self.sub_models: list[Model] = [] - - self._variables_short: dict[str, str] = {} - self._constraints_short: dict[str, str] = {} - self._sub_models_short: dict[str, str] = {} - logger.debug(f'Created {self.__class__.__name__} "{self.label_full}"') - - def do_modeling(self): - raise NotImplementedError('Every Model needs a do_modeling() method') - - def add( - self, item: linopy.Variable | linopy.Constraint | Model, short_name: str | None = None - ) -> linopy.Variable | linopy.Constraint | Model: - """ - Add a variable, constraint or sub-model to the model - - Args: - item: The variable, constraint or sub-model to add to the model - short_name: The short name of the variable, constraint or sub-model. If not provided, the full name is used. - """ - # TODO: Check uniquenes of short names - if isinstance(item, linopy.Variable): - self._variables_direct.append(item.name) - self._variables_short[item.name] = short_name or item.name - elif isinstance(item, linopy.Constraint): - self._constraints_direct.append(item.name) - self._constraints_short[item.name] = short_name or item.name - elif isinstance(item, Model): - self.sub_models.append(item) - self._sub_models_short[item.label_full] = short_name or item.label_full - else: - raise ValueError( - f'Item must be a linopy.Variable, linopy.Constraint or flixopt.structure.Model, got {type(item)}' - ) - return item + self.label_of_model = label_of_model if label_of_model is not None else self.label_of_element + + self._variables: dict[str, linopy.Variable] = {} # Mapping from short name to variable + self._constraints: dict[str, linopy.Constraint] = {} # Mapping from short name to constraint + self.submodels: Submodels = Submodels({}) + + logger.debug(f'Creating {self.__class__.__name__} "{self.label_full}"') + self._do_modeling() + + def add_variables(self, short_name: str = None, **kwargs) -> linopy.Variable: + """Create and register a variable in one step""" + if kwargs.get('name') is None: + if short_name is None: + raise ValueError('Short name must be provided when no name is given') + kwargs['name'] = f'{self.label_of_model}|{short_name}' + + variable = self._model.add_variables(**kwargs) + self.register_variable(variable, short_name) + return variable + + def add_constraints(self, expression, short_name: str = None, **kwargs) -> linopy.Constraint: + """Create and register a constraint in one step""" + if kwargs.get('name') is None: + if short_name is None: + raise ValueError('Short name must be provided when no name is given') + kwargs['name'] = f'{self.label_of_model}|{short_name}' + + constraint = self._model.add_constraints(expression, **kwargs) + self.register_constraint(constraint, short_name) + return constraint + + def register_variable(self, variable: linopy.Variable, short_name: str = None) -> linopy.Variable: + """Register a variable with the model""" + if short_name is None: + short_name = variable.name + elif short_name in self._variables: + raise ValueError(f'Short name "{short_name}" already assigned to model variables') + + self._variables[short_name] = variable + return variable + + def register_constraint(self, constraint: linopy.Constraint, short_name: str = None) -> linopy.Constraint: + """Register a constraint with the model""" + if short_name is None: + short_name = constraint.name + elif short_name in self._constraints: + raise ValueError(f'Short name "{short_name}" already assigned to model constraint') + + self._constraints[short_name] = constraint + return constraint + + def __getitem__(self, key: str) -> linopy.Variable: + """Get a variable by its short name""" + if key in self._variables: + return self._variables[key] + raise KeyError(f'Variable "{key}" not found in model "{self.label_full}"') + + def __contains__(self, name: str) -> bool: + """Check if a variable exists in the model""" + return name in self._variables or name in self.variables + + def get(self, name: str, default=None): + """Get variable by short name, returning default if not found""" + try: + return self[name] + except KeyError: + return default + + def get_coords( + self, + dims: Collection[str] | None = None, + extra_timestep: bool = False, + ) -> xr.Coordinates | None: + return self._model.get_coords(dims=dims, extra_timestep=extra_timestep) def filter_variables( self, @@ -381,252 +1013,158 @@ def filter_variables( return all_variables[[name for name in all_variables if 'time' in all_variables[name].dims]] raise ValueError(f'Invalid length "{length}", must be one of "scalar", "time" or None') - @property - def label(self) -> str: - return self._label if self._label else self.label_of_element - @property def label_full(self) -> str: - """Used to construct the names of variables and constraints""" - if self._label_full: - return self._label_full - elif self._label: - return f'{self.label_of_element}|{self.label}' - return self.label_of_element + return self.label_of_model @property def variables_direct(self) -> linopy.Variables: - return self._model.variables[self._variables_direct] + """Variables of the model, excluding those of sub-models""" + return self._model.variables[[var.name for var in self._variables.values()]] @property def constraints_direct(self) -> linopy.Constraints: - return self._model.constraints[self._constraints_direct] + """Constraints of the model, excluding those of sub-models""" + return self._model.constraints[[con.name for con in self._constraints.values()]] @property - def _variables(self) -> list[str]: - all_variables = self._variables_direct.copy() - for sub_model in self.sub_models: - for variable in sub_model._variables: - if variable in all_variables: - raise KeyError( - f"Duplicate key found: '{variable}' in both {self.label_full} and {sub_model.label_full}!" - ) - all_variables.append(variable) - return all_variables + def constraints(self) -> linopy.Constraints: + """All constraints of the model, including those of all sub-models""" + names = list(self.constraints_direct) + [ + constraint_name for submodel in self.submodels.values() for constraint_name in submodel.constraints + ] - @property - def _constraints(self) -> list[str]: - all_constraints = self._constraints_direct.copy() - for sub_model in self.sub_models: - for constraint in sub_model._constraints: - if constraint in all_constraints: - raise KeyError(f"Duplicate key found: '{constraint}' in both main model and submodel!") - all_constraints.append(constraint) - return all_constraints + return self._model.constraints[names] @property def variables(self) -> linopy.Variables: - return self._model.variables[self._variables] + """All variables of the model, including those of all sub-models""" + names = list(self.variables_direct) + [ + variable_name for submodel in self.submodels.values() for variable_name in submodel.variables + ] - @property - def constraints(self) -> linopy.Constraints: - return self._model.constraints[self._constraints] + return self._model.variables[names] - @property - def all_sub_models(self) -> list[Model]: - return [model for sub_model in self.sub_models for model in [sub_model] + sub_model.all_sub_models] + def __repr__(self) -> str: + """ + Return a string representation of the linopy model. + """ + # Extract content from existing representations + sections = { + f'Variables: [{len(self.variables)}/{len(self._model.variables)}]': self.variables.__repr__().split( + '\n', 2 + )[2], + f'Constraints: [{len(self.constraints)}/{len(self._model.constraints)}]': self.constraints.__repr__().split( + '\n', 2 + )[2], + f'Submodels: [{len(self.submodels)}]': self.submodels.__repr__().split('\n', 2)[2], + } + # Format sections with headers and underlines + formatted_sections = [] + for section_header, section_content in sections.items(): + formatted_sections.append(f'{section_header}\n{"-" * len(section_header)}\n{section_content}') -class ElementModel(Model): - """Stores the mathematical Variables and Constraints for Elements""" + model_string = f'Submodel "{self.label_of_model}":' + all_sections = '\n'.join(formatted_sections) - def __init__(self, model: SystemModel, element: Element): - """ - Args: - model: The SystemModel that is used to create the model. - element: The element this model is created for. - """ - super().__init__(model, label_of_element=element.label_full, label=element.label, label_full=element.label_full) - self.element = element + return f'{model_string}\n{"=" * len(model_string)}\n\n{all_sections}' - def results_structure(self): - return { - 'label': self.label, - 'label_full': self.label_full, - 'variables': list(self.variables), - 'constraints': list(self.constraints), - } + @property + def hours_per_step(self): + return self._model.hours_per_step + def _do_modeling(self): + """Called at the end of initialization. Override in subclasses to create variables and constraints.""" + pass -def copy_and_convert_datatypes(data: Any, use_numpy: bool = True, use_element_label: bool = False) -> Any: - """ - Converts values in a nested data structure into JSON-compatible types while preserving or transforming numpy arrays - and custom `Element` objects based on the specified options. - The function handles various data types and transforms them into a consistent, readable format: - - Primitive types (`int`, `float`, `str`, `bool`, `None`) are returned as-is. - - Numpy scalars are converted to their corresponding Python scalar types. - - Collections (`list`, `tuple`, `set`, `dict`) are recursively processed to ensure all elements are compatible. - - Numpy arrays are preserved or converted to lists, depending on `use_numpy`. - - Custom `Element` objects can be represented either by their `label` or their initialization parameters as a dictionary. - - Timestamps (`datetime`) are converted to ISO 8601 strings. +@dataclass(repr=False) +class Submodels: + """A simple collection for storing submodels with easy access and representation.""" - Args: - data: The input data to process, which may be deeply nested and contain a mix of types. - use_numpy: If `True`, numeric numpy arrays (`np.ndarray`) are preserved as-is. If `False`, they are converted to lists. - Default is `True`. - use_element_label: If `True`, `Element` objects are represented by their `label`. If `False`, they are converted into a dictionary - based on their initialization parameters. Default is `False`. - - Returns: - A transformed version of the input data, containing only JSON-compatible types: - - `int`, `float`, `str`, `bool`, `None` - - `list`, `dict` - - `np.ndarray` (if `use_numpy=True`. This is NOT JSON-compatible) - - Raises: - TypeError: If the data cannot be converted to the specified types. - - Examples: - >>> copy_and_convert_datatypes({'a': np.array([1, 2, 3]), 'b': Element(label='example')}) - {'a': array([1, 2, 3]), 'b': {'class': 'Element', 'label': 'example'}} - - >>> copy_and_convert_datatypes({'a': np.array([1, 2, 3]), 'b': Element(label='example')}, use_numpy=False) - {'a': [1, 2, 3], 'b': {'class': 'Element', 'label': 'example'}} - - Notes: - - The function gracefully handles unexpected types by issuing a warning and returning a deep copy of the data. - - Empty collections (lists, dictionaries) and default parameter values in `Element` objects are omitted from the output. - - Numpy arrays with non-numeric data types are automatically converted to lists. - """ - if isinstance(data, np.integer): # This must be checked before checking for regular int and float! - return int(data) - elif isinstance(data, np.floating): - return float(data) - - elif isinstance(data, (int, float, str, bool, type(None))): - return data - elif isinstance(data, datetime): - return data.isoformat() - - elif isinstance(data, (tuple, set)): - return copy_and_convert_datatypes([item for item in data], use_numpy, use_element_label) - elif isinstance(data, dict): - return { - copy_and_convert_datatypes(key, use_numpy, use_element_label=True): copy_and_convert_datatypes( - value, use_numpy, use_element_label - ) - for key, value in data.items() - } - elif isinstance(data, list): # Shorten arrays/lists to be readable - if use_numpy and all([isinstance(value, (int, float)) for value in data]): - return np.array([item for item in data]) - else: - return [copy_and_convert_datatypes(item, use_numpy, use_element_label) for item in data] + data: dict[str, Submodel] - elif isinstance(data, np.ndarray): - if not use_numpy: - return copy_and_convert_datatypes(data.tolist(), use_numpy, use_element_label) - elif use_numpy and np.issubdtype(data.dtype, np.number): - return data - else: - logger.critical( - f'An np.array with non-numeric content was found: {data=}.It will be converted to a list instead' - ) - return copy_and_convert_datatypes(data.tolist(), use_numpy, use_element_label) + def __getitem__(self, name: str) -> Submodel: + """Get a submodel by its name.""" + return self.data[name] - elif isinstance(data, TimeSeries): - return copy_and_convert_datatypes(data.active_data, use_numpy, use_element_label) - elif isinstance(data, TimeSeriesData): - return copy_and_convert_datatypes(data.data, use_numpy, use_element_label) + def __getattr__(self, name: str) -> Submodel: + """Get a submodel by attribute access.""" + if name in self.data: + return self.data[name] + raise AttributeError(f"Submodels has no attribute '{name}'") - elif isinstance(data, Interface): - if use_element_label and isinstance(data, Element): - return data.label - return data.infos(use_numpy, use_element_label) - elif isinstance(data, xr.DataArray): - # TODO: This is a temporary basic work around - return copy_and_convert_datatypes(data.values, use_numpy, use_element_label) - else: - raise TypeError(f'copy_and_convert_datatypes() did get unexpected data of type "{type(data)}": {data=}') + def __len__(self) -> int: + return len(self.data) + def __iter__(self) -> Iterator[str]: + return iter(self.data) -def get_compact_representation(data: Any, array_threshold: int = 50, decimals: int = 2) -> dict: - """ - Generate a compact json serializable representation of deeply nested data. - Numpy arrays are statistically described if they exceed a threshold and converted to lists. + def __contains__(self, name: str) -> bool: + return name in self.data - Args: - data (Any): The data to format and represent. - array_threshold (int): Maximum length of NumPy arrays to display. Longer arrays are statistically described. - decimals (int): Number of decimal places in which to describe the arrays. + def __repr__(self) -> str: + """Simple representation of the submodels collection.""" + if not self.data: + return 'flixopt.structure.Submodels:\n----------------------------\n \n' - Returns: - dict: A dictionary representation of the data - """ + total_vars = sum(len(submodel.variables) for submodel in self.data.values()) + total_cons = sum(len(submodel.constraints) for submodel in self.data.values()) - def format_np_array_if_found(value: Any) -> Any: - """Recursively processes the data, formatting NumPy arrays.""" - if isinstance(value, (int, float, str, bool, type(None))): - return value - elif isinstance(value, np.ndarray): - return describe_numpy_arrays(value) - elif isinstance(value, dict): - return {format_np_array_if_found(k): format_np_array_if_found(v) for k, v in value.items()} - elif isinstance(value, (list, tuple, set)): - return [format_np_array_if_found(v) for v in value] - else: - logger.warning( - f'Unexpected value found when trying to format numpy array numpy array: {type(value)=}; {value=}' - ) - return value + title = ( + f'flixopt.structure.Submodels ({total_vars} vars, {total_cons} constraints, {len(self.data)} submodels):' + ) + underline = '-' * len(title) - def describe_numpy_arrays(arr: np.ndarray) -> str | list: - """Shortens NumPy arrays if they exceed the specified length.""" + if not self.data: + return f'{title}\n{underline}\n \n' + sub_models_string = '' + for name, submodel in self.data.items(): + type_name = submodel.__class__.__name__ + var_count = len(submodel.variables) + con_count = len(submodel.constraints) + sub_models_string += f'\n * {name} [{type_name}] ({var_count}v/{con_count}c)' - def normalized_center_of_mass(array: Any) -> float: - # position in array (0 bis 1 normiert) - positions = np.linspace(0, 1, len(array)) # weights w_i - # mass center - if np.sum(array) == 0: - return np.nan - else: - return np.sum(positions * array) / np.sum(array) - - if arr.size > array_threshold: # Calculate basic statistics - fmt = f'.{decimals}f' - return ( - f'Array (min={np.min(arr):{fmt}}, max={np.max(arr):{fmt}}, mean={np.mean(arr):{fmt}}, ' - f'median={np.median(arr):{fmt}}, std={np.std(arr):{fmt}}, len={len(arr)}, ' - f'center={normalized_center_of_mass(arr):{fmt}})' - ) - else: - return np.around(arr, decimals=decimals).tolist() + return f'{title}\n{underline}{sub_models_string}\n' - # Process the data to handle NumPy arrays - formatted_data = format_np_array_if_found(copy_and_convert_datatypes(data, use_numpy=True)) + def items(self) -> ItemsView[str, Submodel]: + return self.data.items() - return formatted_data + def keys(self): + return self.data.keys() + def values(self): + return self.data.values() -def get_str_representation(data: Any, array_threshold: int = 50, decimals: int = 2) -> str: - """ - Generate a string representation of deeply nested data using `rich.print`. - NumPy arrays are shortened to the specified length and converted to strings. + def add(self, submodel: Submodel, name: str) -> None: + """Add a submodel to the collection.""" + self.data[name] = submodel - Args: - data (Any): The data to format and represent. - array_threshold (int): Maximum length of NumPy arrays to display. Longer arrays are statistically described. - decimals (int): Number of decimal places in which to describe the arrays. + def get(self, name: str, default=None): + """Get submodel by name, returning default if not found.""" + return self.data.get(name, default) - Returns: - str: The formatted string representation of the data. + +class ElementModel(Submodel): + """ + Stores the mathematical Variables and Constraints for Elements. + ElementModels are directly registered in the main FlowSystemModel """ - formatted_data = get_compact_representation(data, array_threshold, decimals) + def __init__(self, model: FlowSystemModel, element: Element): + """ + Args: + model: The FlowSystemModel that is used to create the model. + element: The element this model is created for. + """ + self.element = element + super().__init__(model, label_of_element=element.label_full, label_of_model=element.label_full) + self._model.add_submodels(self, short_name=self.label_of_model) - # Use Rich to format and print the data - with StringIO() as output_buffer: - console = Console(file=output_buffer, width=1000) # Adjust width as needed - console.print(Pretty(formatted_data, expand_all=True, indent_guides=True)) - return output_buffer.getvalue() + def results_structure(self): + return { + 'label': self.label_full, + 'variables': list(self.variables), + 'constraints': list(self.constraints), + } diff --git a/flixopt/utils.py b/flixopt/utils.py index 30ac46c97..f1e12b9dc 100644 --- a/flixopt/utils.py +++ b/flixopt/utils.py @@ -5,37 +5,60 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING, Any, Literal +from typing import Literal -if TYPE_CHECKING: - import numpy as np - import xarray as xr +import numpy as np +import xarray as xr logger = logging.getLogger('flixopt') -def is_number(number_alias: int | float | str) -> bool: - """Returns True if value is a number or a number-like string.""" - try: - float(number_alias) - return True - except (ValueError, TypeError): - return False +def round_nested_floats(obj, decimals=2): + """Recursively round floating point numbers in nested data structures. + This function traverses nested data structures (dictionaries, lists) and rounds + any floating point numbers to the specified number of decimal places. It handles + various data types including NumPy arrays and xarray DataArrays by converting + them to lists with rounded values. -def round_floats(obj, decimals=2): + Args: + obj: The object to process. Can be a dict, list, float, int, numpy.ndarray, + xarray.DataArray, or any other type. + decimals (int, optional): Number of decimal places to round to. Defaults to 2. + + Returns: + The processed object with the same structure as the input, but with all + floating point numbers rounded to the specified precision. NumPy arrays + and xarray DataArrays are converted to lists. + + Examples: + >>> data = {'a': 3.14159, 'b': [1.234, 2.678]} + >>> round_nested_floats(data, decimals=2) + {'a': 3.14, 'b': [1.23, 2.68]} + + >>> import numpy as np + >>> arr = np.array([1.234, 5.678]) + >>> round_nested_floats(arr, decimals=1) + [1.2, 5.7] + """ if isinstance(obj, dict): - return {k: round_floats(v, decimals) for k, v in obj.items()} + return {k: round_nested_floats(v, decimals) for k, v in obj.items()} elif isinstance(obj, list): - return [round_floats(v, decimals) for v in obj] + return [round_nested_floats(v, decimals) for v in obj] elif isinstance(obj, float): return round(obj, decimals) + elif isinstance(obj, int): + return obj + elif isinstance(obj, np.ndarray): + return np.round(obj, decimals).tolist() + elif isinstance(obj, xr.DataArray): + return obj.round(decimals).values.tolist() return obj def convert_dataarray( data: xr.DataArray, mode: Literal['py', 'numpy', 'xarray', 'structure'] -) -> list[Any] | np.ndarray | xr.DataArray | str: +) -> list | np.ndarray | xr.DataArray | str: """ Convert a DataArray to a different format. diff --git a/mkdocs.yml b/mkdocs.yml index 98747d987..b7c03faac 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -8,6 +8,56 @@ site_url: https://flixopt.github.io/flixopt/ repo_url: https://github.com/flixOpt/flixopt repo_name: flixOpt/flixopt +nav: + - Home: index.md + - Getting Started: getting-started.md + - User Guide: + - user-guide/index.md + - Recipes: user-guide/recipes/index.md + - Mathematical Notation: + - Overview: user-guide/mathematical-notation/index.md + - Dimensions: user-guide/mathematical-notation/dimensions.md + - Elements: + - Flow: user-guide/mathematical-notation/elements/Flow.md + - Bus: user-guide/mathematical-notation/elements/Bus.md + - Storage: user-guide/mathematical-notation/elements/Storage.md + - LinearConverter: user-guide/mathematical-notation/elements/LinearConverter.md + - Features: + - InvestParameters: user-guide/mathematical-notation/features/InvestParameters.md + - OnOffParameters: user-guide/mathematical-notation/features/OnOffParameters.md + - Piecewise: user-guide/mathematical-notation/features/Piecewise.md + - Effects, Penalty & Objective: user-guide/mathematical-notation/effects-penalty-objective.md + - Modeling Patterns: + - Overview: user-guide/mathematical-notation/modeling-patterns/index.md + - Bounds and States: user-guide/mathematical-notation/modeling-patterns/bounds-and-states.md + - Duration Tracking: user-guide/mathematical-notation/modeling-patterns/duration-tracking.md + - State Transitions: user-guide/mathematical-notation/modeling-patterns/state-transitions.md + - Examples: examples/ + - Contribute: contribute.md + - API Reference: + - api-reference/index.md + - Aggregation: api-reference/aggregation.md + - Calculation: api-reference/calculation.md + - Commons: api-reference/commons.md + - Components: api-reference/components.md + - Config: api-reference/config.md + - Core: api-reference/core.md + - Effects: api-reference/effects.md + - Elements: api-reference/elements.md + - Features: api-reference/features.md + - Flow System: api-reference/flow_system.md + - Interface: api-reference/interface.md + - IO: api-reference/io.md + - Linear Converters: api-reference/linear_converters.md + - Modeling: api-reference/modeling.md + - Network App: api-reference/network_app.md + - Plotting: api-reference/plotting.md + - Results: api-reference/results.md + - Solvers: api-reference/solvers.md + - Structure: api-reference/structure.md + - Utils: api-reference/utils.md + - Release Notes: changelog/ + theme: name: material @@ -88,9 +138,6 @@ plugins: - gen-files: scripts: - scripts/gen_ref_pages.py - - literate-nav: - nav_file: SUMMARY.md - implicit_index: true # This makes index.md the default landing page - mkdocstrings: # Handles automatic API documentation generation default_handler: python # Sets Python as the default language handlers: diff --git a/pics/flixopt-icon.svg b/pics/flixopt-icon.svg index 04a6a6851..08fe340f9 100644 --- a/pics/flixopt-icon.svg +++ b/pics/flixopt-icon.svg @@ -1 +1 @@ -flixOpt \ No newline at end of file +flixOpt diff --git a/pyproject.toml b/pyproject.toml index 6a523bbb4..3bb68efb4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,6 +48,9 @@ dependencies = [ # Visualization "matplotlib >= 3.5.2, < 4", "plotly >= 5.15.0, < 7", + + # Fix for numexpr compatibility issue with numpy 1.26.4 on Python 3.10 + "numexpr >= 2.8.4, < 2.14; python_version < '3.11'", # Avoid 2.14.0 on older Python ] [project.optional-dependencies] @@ -116,10 +119,6 @@ documentation = "https://flixopt.github.io/flixopt/" where = ["."] include = ["flixopt*"] exclude = ["tests*", "docs*", "examples*", "Tutorials*"] - -[tool.setuptools.package-data] -"flixopt" = ["config.yaml"] - [tool.setuptools] include-package-data = true @@ -187,7 +186,28 @@ markers = [ "slow: marks tests as slow", "examples: marks example tests (run only on releases)", ] -addopts = "-m 'not examples'" # Skip examples by default +addopts = '-m "not examples"' # Skip examples by default + +# Warning filter configuration for pytest +# Filters are processed in order; first match wins +# Format: "action:message:category:module" +filterwarnings = [ + # === Default behavior: show all warnings === + "default", + + # === Treat flixopt warnings as errors (strict mode for our code) === + # This ensures we catch deprecations, future changes, and user warnings in our own code + "error::DeprecationWarning:flixopt", + "error::FutureWarning:flixopt", + "error::UserWarning:flixopt", + + # === Third-party warnings (mirrored from __init__.py) === + "ignore:.*minimal value.*exceeds.*:UserWarning:tsam", + "ignore:Coordinates across variables not equal:UserWarning:linopy", + "ignore:.*join will change from join='outer' to join='exact'.*:FutureWarning:linopy", + "ignore:numpy\\.ndarray size changed:RuntimeWarning", + "ignore:.*network visualization is still experimental.*:UserWarning:flixopt", +] [tool.bandit] skips = ["B101", "B506"] # assert_used and yaml_load diff --git a/scripts/gen_ref_pages.py b/scripts/gen_ref_pages.py index f2de8a701..3c8eb600a 100644 --- a/scripts/gen_ref_pages.py +++ b/scripts/gen_ref_pages.py @@ -1,4 +1,4 @@ -"""Generate the code reference pages and navigation.""" +"""Generate the code reference pages.""" import sys from pathlib import Path @@ -9,11 +9,11 @@ root = Path(__file__).parent.parent sys.path.insert(0, str(root)) -nav = mkdocs_gen_files.Nav() - src = root / 'flixopt' api_dir = 'api-reference' +generated_files = [] + for path in sorted(src.rglob('*.py')): module_path = path.relative_to(src).with_suffix('') doc_path = path.relative_to(src).with_suffix('.md') @@ -30,10 +30,8 @@ elif parts[-1] == '__main__' or parts[-1].startswith('_'): continue - # Only add to navigation if there are actual parts + # Only generate documentation if there are actual parts if parts: - nav[parts] = doc_path.as_posix() - # Generate documentation file - always using the flixopt prefix with mkdocs_gen_files.open(full_doc_path, 'w') as fd: # Use 'flixopt.' prefix for all module references @@ -41,6 +39,7 @@ fd.write(f'::: {module_id}\n options:\n inherited_members: true\n') mkdocs_gen_files.set_edit_path(full_doc_path, path.relative_to(root)) + generated_files.append(str(full_doc_path)) # Create an index file for the API reference with mkdocs_gen_files.open(f'{api_dir}/index.md', 'w') as index_file: @@ -50,5 +49,7 @@ 'For more information on how to use the classes and functions, see the [User Guide](../user-guide/index.md) section.\n' ) -with mkdocs_gen_files.open(f'{api_dir}/SUMMARY.md', 'w') as nav_file: - nav_file.writelines(nav.build_literate_nav()) +# Print generated files for validation +print(f'Generated {len(generated_files)} API reference files:') +for file in sorted(generated_files): + print(f' - {file}') diff --git a/tests/conftest.py b/tests/conftest.py index ac2bab5f4..ac5255562 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,6 +5,7 @@ """ import os +from collections.abc import Iterable import linopy.testing import numpy as np @@ -13,7 +14,11 @@ import xarray as xr import flixopt as fx -from flixopt.structure import SystemModel +from flixopt.structure import FlowSystemModel + +# ============================================================================ +# SOLVER FIXTURES +# ============================================================================ @pytest.fixture() @@ -23,28 +28,371 @@ def highs_solver(): @pytest.fixture() def gurobi_solver(): + pytest.importorskip('gurobipy', reason='Gurobi not available in this environment') return fx.solvers.GurobiSolver(mip_gap=0, time_limit_seconds=300) -@pytest.fixture(params=[highs_solver, gurobi_solver]) +@pytest.fixture(params=[highs_solver, gurobi_solver], ids=['highs', 'gurobi']) def solver_fixture(request): return request.getfixturevalue(request.param.__name__) -# Custom assertion function -def assert_almost_equal_numeric( - actual, desired, err_msg, relative_error_range_in_percent=0.011, absolute_tolerance=1e-9 -): - """ - Custom assertion function for comparing numeric values with relative and absolute tolerances - """ - relative_tol = relative_error_range_in_percent / 100 +# ================================= +# COORDINATE CONFIGURATION FIXTURES +# ================================= + + +@pytest.fixture( + params=[ + { + 'timesteps': pd.date_range('2020-01-01', periods=10, freq='h', name='time'), + 'periods': None, + 'scenarios': None, + }, + { + 'timesteps': pd.date_range('2020-01-01', periods=10, freq='h', name='time'), + 'periods': None, + 'scenarios': pd.Index(['A', 'B'], name='scenario'), + }, + { + 'timesteps': pd.date_range('2020-01-01', periods=10, freq='h', name='time'), + 'periods': pd.Index([2020, 2030, 2040], name='period'), + 'scenarios': None, + }, + { + 'timesteps': pd.date_range('2020-01-01', periods=10, freq='h', name='time'), + 'periods': pd.Index([2020, 2030, 2040], name='period'), + 'scenarios': pd.Index(['A', 'B'], name='scenario'), + }, + ], + ids=['time_only', 'time+scenarios', 'time+periods', 'time+periods+scenarios'], +) +def coords_config(request): + """Coordinate configurations for parametrized testing.""" + return request.param + + +# ============================================================================ +# HIERARCHICAL ELEMENT LIBRARY +# ============================================================================ + + +class Buses: + """Standard buses used across flow systems""" + + @staticmethod + def electricity(): + return fx.Bus('Strom') + + @staticmethod + def heat(): + return fx.Bus('Fernwärme') + + @staticmethod + def gas(): + return fx.Bus('Gas') + + @staticmethod + def coal(): + return fx.Bus('Kohle') + + @staticmethod + def defaults(): + """Get all standard buses at once""" + return [Buses.electricity(), Buses.heat(), Buses.gas()] + + +class Effects: + """Standard effects used across flow systems""" + + @staticmethod + def costs(): + return fx.Effect('costs', '€', 'Kosten', is_standard=True, is_objective=True) + + @staticmethod + def costs_with_co2_share(): + return fx.Effect('costs', '€', 'Kosten', is_standard=True, is_objective=True, share_from_temporal={'CO2': 0.2}) + + @staticmethod + def co2(): + return fx.Effect('CO2', 'kg', 'CO2_e-Emissionen') + + @staticmethod + def primary_energy(): + return fx.Effect('PE', 'kWh_PE', 'Primärenergie') + + +class Converters: + """Energy conversion components""" + + class Boilers: + @staticmethod + def simple(): + """Simple boiler from simple_flow_system""" + return fx.linear_converters.Boiler( + 'Boiler', + eta=0.5, + Q_th=fx.Flow( + 'Q_th', + bus='Fernwärme', + size=50, + relative_minimum=5 / 50, + relative_maximum=1, + on_off_parameters=fx.OnOffParameters(), + ), + Q_fu=fx.Flow('Q_fu', bus='Gas'), + ) + + @staticmethod + def complex(): + """Complex boiler with investment parameters from flow_system_complex""" + return fx.linear_converters.Boiler( + 'Kessel', + eta=0.5, + on_off_parameters=fx.OnOffParameters(effects_per_running_hour={'costs': 0, 'CO2': 1000}), + Q_th=fx.Flow( + 'Q_th', + bus='Fernwärme', + load_factor_max=1.0, + load_factor_min=0.1, + relative_minimum=5 / 50, + relative_maximum=1, + previous_flow_rate=50, + size=fx.InvestParameters( + effects_of_investment=1000, + fixed_size=50, + mandatory=True, + effects_of_investment_per_size={'costs': 10, 'PE': 2}, + ), + on_off_parameters=fx.OnOffParameters( + on_hours_total_min=0, + on_hours_total_max=1000, + consecutive_on_hours_max=10, + consecutive_on_hours_min=1, + consecutive_off_hours_max=10, + effects_per_switch_on=0.01, + switch_on_total_max=1000, + ), + flow_hours_total_max=1e6, + ), + Q_fu=fx.Flow('Q_fu', bus='Gas', size=200, relative_minimum=0, relative_maximum=1), + ) + + class CHPs: + @staticmethod + def simple(): + """Simple CHP from simple_flow_system""" + return fx.linear_converters.CHP( + 'CHP_unit', + eta_th=0.5, + eta_el=0.4, + P_el=fx.Flow( + 'P_el', bus='Strom', size=60, relative_minimum=5 / 60, on_off_parameters=fx.OnOffParameters() + ), + Q_th=fx.Flow('Q_th', bus='Fernwärme'), + Q_fu=fx.Flow('Q_fu', bus='Gas'), + ) + + @staticmethod + def base(): + """CHP from flow_system_base""" + return fx.linear_converters.CHP( + 'KWK', + eta_th=0.5, + eta_el=0.4, + on_off_parameters=fx.OnOffParameters(effects_per_switch_on=0.01), + P_el=fx.Flow('P_el', bus='Strom', size=60, relative_minimum=5 / 60, previous_flow_rate=10), + Q_th=fx.Flow('Q_th', bus='Fernwärme', size=1e3), + Q_fu=fx.Flow('Q_fu', bus='Gas', size=1e3), + ) + + class LinearConverters: + @staticmethod + def piecewise(): + """Piecewise converter from flow_system_piecewise_conversion""" + return fx.LinearConverter( + 'KWK', + inputs=[fx.Flow('Q_fu', bus='Gas')], + outputs=[ + fx.Flow('P_el', bus='Strom', size=60, relative_maximum=55, previous_flow_rate=10), + fx.Flow('Q_th', bus='Fernwärme'), + ], + piecewise_conversion=fx.PiecewiseConversion( + { + 'P_el': fx.Piecewise([fx.Piece(5, 30), fx.Piece(40, 60)]), + 'Q_th': fx.Piecewise([fx.Piece(6, 35), fx.Piece(45, 100)]), + 'Q_fu': fx.Piecewise([fx.Piece(12, 70), fx.Piece(90, 200)]), + } + ), + on_off_parameters=fx.OnOffParameters(effects_per_switch_on=0.01), + ) + + @staticmethod + def segments(timesteps_length): + """Segments converter with time-varying piecewise conversion""" + return fx.LinearConverter( + 'KWK', + inputs=[fx.Flow('Q_fu', bus='Gas')], + outputs=[ + fx.Flow('P_el', bus='Strom', size=60, relative_maximum=55, previous_flow_rate=10), + fx.Flow('Q_th', bus='Fernwärme'), + ], + piecewise_conversion=fx.PiecewiseConversion( + { + 'P_el': fx.Piecewise( + [ + fx.Piece(np.linspace(5, 6, timesteps_length), 30), + fx.Piece(40, np.linspace(60, 70, timesteps_length)), + ] + ), + 'Q_th': fx.Piecewise([fx.Piece(6, 35), fx.Piece(45, 100)]), + 'Q_fu': fx.Piecewise([fx.Piece(12, 70), fx.Piece(90, 200)]), + } + ), + on_off_parameters=fx.OnOffParameters(effects_per_switch_on=0.01), + ) + + +class Storage: + """Energy storage components""" + + @staticmethod + def simple(timesteps_length=9): + """Simple storage from simple_flow_system""" + # Create pattern [80.0, 70.0, 80.0] and repeat/slice to match timesteps_length + pattern = [80.0, 70.0, 80.0, 80, 80, 80, 80, 80, 80] + charge_state_values = (pattern * ((timesteps_length // len(pattern)) + 1))[:timesteps_length] + + return fx.Storage( + 'Speicher', + charging=fx.Flow('Q_th_load', bus='Fernwärme', size=1e4), + discharging=fx.Flow('Q_th_unload', bus='Fernwärme', size=1e4), + capacity_in_flow_hours=fx.InvestParameters(effects_of_investment=20, fixed_size=30, mandatory=True), + initial_charge_state=0, + relative_maximum_charge_state=1 / 100 * np.array(charge_state_values), + relative_maximum_final_charge_state=0.8, + eta_charge=0.9, + eta_discharge=1, + relative_loss_per_hour=0.08, + prevent_simultaneous_charge_and_discharge=True, + ) + + @staticmethod + def complex(): + """Complex storage with piecewise investment from flow_system_complex""" + invest_speicher = fx.InvestParameters( + effects_of_investment=0, + piecewise_effects_of_investment=fx.PiecewiseEffects( + piecewise_origin=fx.Piecewise([fx.Piece(5, 25), fx.Piece(25, 100)]), + piecewise_shares={ + 'costs': fx.Piecewise([fx.Piece(50, 250), fx.Piece(250, 800)]), + 'PE': fx.Piecewise([fx.Piece(5, 25), fx.Piece(25, 100)]), + }, + ), + mandatory=True, + effects_of_investment_per_size={'costs': 0.01, 'CO2': 0.01}, + minimum_size=0, + maximum_size=1000, + ) + return fx.Storage( + 'Speicher', + charging=fx.Flow('Q_th_load', bus='Fernwärme', size=1e4), + discharging=fx.Flow('Q_th_unload', bus='Fernwärme', size=1e4), + capacity_in_flow_hours=invest_speicher, + initial_charge_state=0, + maximal_final_charge_state=10, + eta_charge=0.9, + eta_discharge=1, + relative_loss_per_hour=0.08, + prevent_simultaneous_charge_and_discharge=True, + ) + + +class LoadProfiles: + """Standard load and price profiles""" + + @staticmethod + def thermal_simple(timesteps_length=9): + # Create pattern and repeat/slice to match timesteps_length + pattern = [30.0, 0.0, 90.0, 110, 110, 20, 20, 20, 20] + values = (pattern * ((timesteps_length // len(pattern)) + 1))[:timesteps_length] + return np.array(values) + + @staticmethod + def thermal_complex(): + return np.array([30, 0, 90, 110, 110, 20, 20, 20, 20]) + + @staticmethod + def electrical_simple(timesteps_length=9): + # Create array of 80.0 repeated to match timesteps_length + return np.array([80.0 / 1000] * timesteps_length) + + @staticmethod + def electrical_scenario(): + return np.array([0.08, 0.1, 0.15]) + + @staticmethod + def electrical_complex(timesteps_length=9): + # Create array of 40 repeated to match timesteps_length + return np.array([40] * timesteps_length) + + @staticmethod + def random_thermal(length=10, seed=42): + np.random.seed(seed) + return np.array([np.random.random() for _ in range(length)]) * 180 + + @staticmethod + def random_electrical(length=10, seed=42): + np.random.seed(seed) + return (np.array([np.random.random() for _ in range(length)]) + 0.5) / 1.5 * 50 + + +class Sinks: + """Energy sinks (loads)""" + + @staticmethod + def heat_load(thermal_profile): + """Create thermal heat load sink""" + return fx.Sink( + 'Wärmelast', inputs=[fx.Flow('Q_th_Last', bus='Fernwärme', size=1, fixed_relative_profile=thermal_profile)] + ) + + @staticmethod + def electricity_feed_in(electrical_price_profile): + """Create electricity feed-in sink""" + return fx.Sink( + 'Einspeisung', inputs=[fx.Flow('P_el', bus='Strom', effects_per_flow_hour=-1 * electrical_price_profile)] + ) + + @staticmethod + def electricity_load(electrical_profile): + """Create electrical load sink (for flow_system_long)""" + return fx.Sink( + 'Stromlast', inputs=[fx.Flow('P_el_Last', bus='Strom', size=1, fixed_relative_profile=electrical_profile)] + ) + + +class Sources: + """Energy sources""" + + @staticmethod + def gas_with_costs_and_co2(): + """Standard gas tariff with CO2 emissions""" + source = Sources.gas_with_costs() + source.outputs[0].effects_per_flow_hour = {'costs': 0.04, 'CO2': 0.3} + return source + + @staticmethod + def gas_with_costs(): + """Simple gas tariff without CO2""" + return fx.Source( + 'Gastarif', outputs=[fx.Flow(label='Q_Gas', bus='Gas', size=1000, effects_per_flow_hour={'costs': 0.04})] + ) - if isinstance(desired, (int, float)): - delta = abs(relative_tol * desired) if desired != 0 else absolute_tolerance - assert np.isclose(actual, desired, atol=delta), err_msg - else: - np.testing.assert_allclose(actual, desired, rtol=relative_tol, atol=absolute_tolerance, err_msg=err_msg) + +# ============================================================================ +# RECREATED FIXTURES USING HIERARCHICAL LIBRARY +# ============================================================================ @pytest.fixture @@ -52,71 +400,60 @@ def simple_flow_system() -> fx.FlowSystem: """ Create a simple energy system for testing """ - base_thermal_load = np.array([30.0, 0.0, 90.0, 110, 110, 20, 20, 20, 20]) - base_electrical_price = 1 / 1000 * np.array([80.0, 80.0, 80.0, 80, 80, 80, 80, 80, 80]) base_timesteps = pd.date_range('2020-01-01', periods=9, freq='h', name='time') + timesteps_length = len(base_timesteps) + base_thermal_load = LoadProfiles.thermal_simple(timesteps_length) + base_electrical_price = LoadProfiles.electrical_simple(timesteps_length) + # Define effects - costs = fx.Effect('costs', '€', 'Kosten', is_standard=True, is_objective=True) - co2 = fx.Effect( - 'CO2', - 'kg', - 'CO2_e-Emissionen', - specific_share_to_other_effects_operation={costs.label: 0.2}, - maximum_operation_per_hour=1000, - ) + costs = Effects.costs_with_co2_share() + co2 = Effects.co2() + co2.maximum_per_hour = 1000 # Create components - boiler = fx.linear_converters.Boiler( - 'Boiler', - eta=0.5, - Q_th=fx.Flow( - 'Q_th', - bus='Fernwärme', - size=50, - relative_minimum=5 / 50, - relative_maximum=1, - on_off_parameters=fx.OnOffParameters(), - ), - Q_fu=fx.Flow('Q_fu', bus='Gas'), - ) + boiler = Converters.Boilers.simple() + chp = Converters.CHPs.simple() + storage = Storage.simple(timesteps_length) + heat_load = Sinks.heat_load(base_thermal_load) + gas_tariff = Sources.gas_with_costs_and_co2() + electricity_feed_in = Sinks.electricity_feed_in(base_electrical_price) - chp = fx.linear_converters.CHP( - 'CHP_unit', - eta_th=0.5, - eta_el=0.4, - P_el=fx.Flow('P_el', bus='Strom', size=60, relative_minimum=5 / 60, on_off_parameters=fx.OnOffParameters()), - Q_th=fx.Flow('Q_th', bus='Fernwärme'), - Q_fu=fx.Flow('Q_fu', bus='Gas'), - ) + # Create flow system + flow_system = fx.FlowSystem(base_timesteps) + flow_system.add_elements(*Buses.defaults()) + flow_system.add_elements(storage, costs, co2, boiler, heat_load, gas_tariff, electricity_feed_in, chp) - storage = fx.Storage( - 'Speicher', - charging=fx.Flow('Q_th_load', bus='Fernwärme', size=1e4), - discharging=fx.Flow('Q_th_unload', bus='Fernwärme', size=1e4), - capacity_in_flow_hours=fx.InvestParameters(fix_effects=20, fixed_size=30, optional=False), - initial_charge_state=0, - relative_maximum_charge_state=1 / 100 * np.array([80.0, 70.0, 80.0, 80, 80, 80, 80, 80, 80, 80]), - eta_charge=0.9, - eta_discharge=1, - relative_loss_per_hour=0.08, - prevent_simultaneous_charge_and_discharge=True, - ) + return flow_system - heat_load = fx.Sink( - 'Wärmelast', sink=fx.Flow('Q_th_Last', bus='Fernwärme', size=1, fixed_relative_profile=base_thermal_load) - ) - gas_tariff = fx.Source( - 'Gastarif', source=fx.Flow('Q_Gas', bus='Gas', size=1000, effects_per_flow_hour={'costs': 0.04, 'CO2': 0.3}) - ) +@pytest.fixture +def simple_flow_system_scenarios() -> fx.FlowSystem: + """ + Create a simple energy system for testing + """ + base_timesteps = pd.date_range('2020-01-01', periods=9, freq='h', name='time') + timesteps_length = len(base_timesteps) + base_thermal_load = LoadProfiles.thermal_simple(timesteps_length) + base_electrical_price = LoadProfiles.electrical_scenario() - electricity_feed_in = fx.Sink( - 'Einspeisung', sink=fx.Flow('P_el', bus='Strom', effects_per_flow_hour=-1 * base_electrical_price) - ) + # Define effects + costs = Effects.costs_with_co2_share() + co2 = Effects.co2() + co2.maximum_per_hour = 1000 + + # Create components + boiler = Converters.Boilers.simple() + chp = Converters.CHPs.simple() + storage = Storage.simple(timesteps_length) + heat_load = Sinks.heat_load(base_thermal_load) + gas_tariff = Sources.gas_with_costs_and_co2() + electricity_feed_in = Sinks.electricity_feed_in(base_electrical_price) # Create flow system - flow_system = fx.FlowSystem(base_timesteps) - flow_system.add_elements(fx.Bus('Strom'), fx.Bus('Fernwärme'), fx.Bus('Gas')) + flow_system = fx.FlowSystem( + base_timesteps, scenarios=pd.Index(['A', 'B', 'C']), weights=np.array([0.5, 0.25, 0.25]) + ) + flow_system.add_elements(*Buses.defaults()) flow_system.add_elements(storage, costs, co2, boiler, heat_load, gas_tariff, electricity_feed_in, chp) return flow_system @@ -126,18 +463,17 @@ def simple_flow_system() -> fx.FlowSystem: def basic_flow_system() -> fx.FlowSystem: """Create basic elements for component testing""" flow_system = fx.FlowSystem(pd.date_range('2020-01-01', periods=10, freq='h', name='time')) - thermal_load = np.array([np.random.random() for _ in range(10)]) * 180 - p_el = (np.array([np.random.random() for _ in range(10)]) + 0.5) / 1.5 * 50 - flow_system.add_elements( - fx.Bus('Strom'), - fx.Bus('Fernwärme'), - fx.Bus('Gas'), - fx.Effect('Costs', '€', 'Kosten', is_standard=True, is_objective=True), - fx.Sink('Wärmelast', sink=fx.Flow('Q_th_Last', 'Fernwärme', size=1, fixed_relative_profile=thermal_load)), - fx.Source('Gastarif', source=fx.Flow('Q_Gas', 'Gas', size=1000, effects_per_flow_hour=0.04)), - fx.Sink('Einspeisung', sink=fx.Flow('P_el', 'Strom', effects_per_flow_hour=-1 * p_el)), - ) + thermal_load = LoadProfiles.random_thermal(10) + p_el = LoadProfiles.random_electrical(10) + + costs = Effects.costs() + heat_load = Sinks.heat_load(thermal_load) + gas_source = Sources.gas_with_costs() + electricity_sink = Sinks.electricity_feed_in(p_el) + + flow_system.add_elements(*Buses.defaults()) + flow_system.add_elements(costs, heat_load, gas_source, electricity_sink) return flow_system @@ -147,79 +483,26 @@ def flow_system_complex() -> fx.FlowSystem: """ Helper method to create a base model with configurable parameters """ - thermal_load = np.array([30, 0, 90, 110, 110, 20, 20, 20, 20]) - electrical_load = np.array([40, 40, 40, 40, 40, 40, 40, 40, 40]) + thermal_load = LoadProfiles.thermal_complex() + electrical_load = LoadProfiles.electrical_complex() flow_system = fx.FlowSystem(pd.date_range('2020-01-01', periods=9, freq='h', name='time')) + # Define the components and flow_system - flow_system.add_elements( - fx.Effect('costs', '€', 'Kosten', is_standard=True, is_objective=True), - fx.Effect('CO2', 'kg', 'CO2_e-Emissionen', specific_share_to_other_effects_operation={'costs': 0.2}), - fx.Effect('PE', 'kWh_PE', 'Primärenergie', maximum_total=3.5e3), - fx.Bus('Strom'), - fx.Bus('Fernwärme'), - fx.Bus('Gas'), - fx.Sink('Wärmelast', sink=fx.Flow('Q_th_Last', 'Fernwärme', size=1, fixed_relative_profile=thermal_load)), - fx.Source( - 'Gastarif', source=fx.Flow('Q_Gas', 'Gas', size=1000, effects_per_flow_hour={'costs': 0.04, 'CO2': 0.3}) - ), - fx.Sink('Einspeisung', sink=fx.Flow('P_el', 'Strom', effects_per_flow_hour=-1 * electrical_load)), - ) + costs = Effects.costs() + co2 = Effects.co2() + costs.share_from_temporal = {'CO2': 0.2} + pe = Effects.primary_energy() + pe.maximum_total = 3.5e3 - boiler = fx.linear_converters.Boiler( - 'Kessel', - eta=0.5, - on_off_parameters=fx.OnOffParameters(effects_per_running_hour={'costs': 0, 'CO2': 1000}), - Q_th=fx.Flow( - 'Q_th', - bus='Fernwärme', - load_factor_max=1.0, - load_factor_min=0.1, - relative_minimum=5 / 50, - relative_maximum=1, - previous_flow_rate=50, - size=fx.InvestParameters( - fix_effects=1000, fixed_size=50, optional=False, specific_effects={'costs': 10, 'PE': 2} - ), - on_off_parameters=fx.OnOffParameters( - on_hours_total_min=0, - on_hours_total_max=1000, - consecutive_on_hours_max=10, - consecutive_on_hours_min=1, - consecutive_off_hours_max=10, - effects_per_switch_on=0.01, - switch_on_total_max=1000, - ), - flow_hours_total_max=1e6, - ), - Q_fu=fx.Flow('Q_fu', bus='Gas', size=200, relative_minimum=0, relative_maximum=1), - ) + heat_load = Sinks.heat_load(thermal_load) + gas_tariff = Sources.gas_with_costs_and_co2() + electricity_feed_in = Sinks.electricity_feed_in(electrical_load) - invest_speicher = fx.InvestParameters( - fix_effects=0, - piecewise_effects=fx.PiecewiseEffects( - piecewise_origin=fx.Piecewise([fx.Piece(5, 25), fx.Piece(25, 100)]), - piecewise_shares={ - 'costs': fx.Piecewise([fx.Piece(50, 250), fx.Piece(250, 800)]), - 'PE': fx.Piecewise([fx.Piece(5, 25), fx.Piece(25, 100)]), - }, - ), - optional=False, - specific_effects={'costs': 0.01, 'CO2': 0.01}, - minimum_size=0, - maximum_size=1000, - ) - speicher = fx.Storage( - 'Speicher', - charging=fx.Flow('Q_th_load', bus='Fernwärme', size=1e4), - discharging=fx.Flow('Q_th_unload', bus='Fernwärme', size=1e4), - capacity_in_flow_hours=invest_speicher, - initial_charge_state=0, - maximal_final_charge_state=10, - eta_charge=0.9, - eta_discharge=1, - relative_loss_per_hour=0.08, - prevent_simultaneous_charge_and_discharge=True, - ) + flow_system.add_elements(*Buses.defaults()) + flow_system.add_elements(costs, co2, pe, heat_load, gas_tariff, electricity_feed_in) + + boiler = Converters.Boilers.complex() + speicher = Storage.complex() flow_system.add_elements(boiler, speicher) @@ -232,45 +515,16 @@ def flow_system_base(flow_system_complex) -> fx.FlowSystem: Helper method to create a base model with configurable parameters """ flow_system = flow_system_complex - - flow_system.add_elements( - fx.linear_converters.CHP( - 'KWK', - eta_th=0.5, - eta_el=0.4, - on_off_parameters=fx.OnOffParameters(effects_per_switch_on=0.01), - P_el=fx.Flow('P_el', bus='Strom', size=60, relative_minimum=5 / 60, previous_flow_rate=10), - Q_th=fx.Flow('Q_th', bus='Fernwärme', size=1e3), - Q_fu=fx.Flow('Q_fu', bus='Gas', size=1e3), - ) - ) - + chp = Converters.CHPs.base() + flow_system.add_elements(chp) return flow_system @pytest.fixture def flow_system_piecewise_conversion(flow_system_complex) -> fx.FlowSystem: flow_system = flow_system_complex - - flow_system.add_elements( - fx.LinearConverter( - 'KWK', - inputs=[fx.Flow('Q_fu', bus='Gas')], - outputs=[ - fx.Flow('P_el', bus='Strom', size=60, relative_maximum=55, previous_flow_rate=10), - fx.Flow('Q_th', bus='Fernwärme'), - ], - piecewise_conversion=fx.PiecewiseConversion( - { - 'P_el': fx.Piecewise([fx.Piece(5, 30), fx.Piece(40, 60)]), - 'Q_th': fx.Piecewise([fx.Piece(6, 35), fx.Piece(45, 100)]), - 'Q_fu': fx.Piecewise([fx.Piece(12, 70), fx.Piece(90, 200)]), - } - ), - on_off_parameters=fx.OnOffParameters(effects_per_switch_on=0.01), - ) - ) - + converter = Converters.LinearConverters.piecewise() + flow_system.add_elements(converter) return flow_system @@ -280,38 +534,16 @@ def flow_system_segments_of_flows_2(flow_system_complex) -> fx.FlowSystem: Use segments/Piecewise with numeric data """ flow_system = flow_system_complex - - flow_system.add_elements( - fx.LinearConverter( - 'KWK', - inputs=[fx.Flow('Q_fu', bus='Gas')], - outputs=[ - fx.Flow('P_el', bus='Strom', size=60, relative_maximum=55, previous_flow_rate=10), - fx.Flow('Q_th', bus='Fernwärme'), - ], - piecewise_conversion=fx.PiecewiseConversion( - { - 'P_el': fx.Piecewise( - [ - fx.Piece(np.linspace(5, 6, len(flow_system.time_series_collection.timesteps)), 30), - fx.Piece(40, np.linspace(60, 70, len(flow_system.time_series_collection.timesteps))), - ] - ), - 'Q_th': fx.Piecewise([fx.Piece(6, 35), fx.Piece(45, 100)]), - 'Q_fu': fx.Piecewise([fx.Piece(12, 70), fx.Piece(90, 200)]), - } - ), - on_off_parameters=fx.OnOffParameters(effects_per_switch_on=0.01), - ) - ) - + converter = Converters.LinearConverters.segments(len(flow_system.timesteps)) + flow_system.add_elements(converter) return flow_system @pytest.fixture def flow_system_long(): """ - Fixture to create and return the flow system with loaded data + Special fixture with CSV data loading - kept separate for backward compatibility + Uses library components where possible, but has special elements inline """ # Load data filename = os.path.join(os.path.dirname(__file__), 'ressources', 'Zeitreihen2020.csv') @@ -326,38 +558,38 @@ def flow_system_long(): thermal_load_ts, electrical_load_ts = ( fx.TimeSeriesData(thermal_load), - fx.TimeSeriesData(electrical_load, agg_weight=0.7), + fx.TimeSeriesData(electrical_load, aggregation_weight=0.7), ) p_feed_in, p_sell = ( - fx.TimeSeriesData(-(p_el - 0.5), agg_group='p_el'), - fx.TimeSeriesData(p_el + 0.5, agg_group='p_el'), + fx.TimeSeriesData(-(p_el - 0.5), aggregation_group='p_el'), + fx.TimeSeriesData(p_el + 0.5, aggregation_group='p_el'), ) flow_system = fx.FlowSystem(pd.DatetimeIndex(data.index)) flow_system.add_elements( - fx.Bus('Strom'), - fx.Bus('Fernwärme'), - fx.Bus('Gas'), - fx.Bus('Kohle'), - fx.Effect('costs', '€', 'Kosten', is_standard=True, is_objective=True), - fx.Effect('CO2', 'kg', 'CO2_e-Emissionen'), - fx.Effect('PE', 'kWh_PE', 'Primärenergie'), + *Buses.defaults(), + Buses.coal(), + Effects.costs(), + Effects.co2(), + Effects.primary_energy(), fx.Sink( - 'Wärmelast', sink=fx.Flow('Q_th_Last', bus='Fernwärme', size=1, fixed_relative_profile=thermal_load_ts) + 'Wärmelast', inputs=[fx.Flow('Q_th_Last', bus='Fernwärme', size=1, fixed_relative_profile=thermal_load_ts)] + ), + fx.Sink( + 'Stromlast', inputs=[fx.Flow('P_el_Last', bus='Strom', size=1, fixed_relative_profile=electrical_load_ts)] ), - fx.Sink('Stromlast', sink=fx.Flow('P_el_Last', bus='Strom', size=1, fixed_relative_profile=electrical_load_ts)), fx.Source( 'Kohletarif', - source=fx.Flow('Q_Kohle', bus='Kohle', size=1000, effects_per_flow_hour={'costs': 4.6, 'CO2': 0.3}), + outputs=[fx.Flow('Q_Kohle', bus='Kohle', size=1000, effects_per_flow_hour={'costs': 4.6, 'CO2': 0.3})], ), fx.Source( 'Gastarif', - source=fx.Flow('Q_Gas', bus='Gas', size=1000, effects_per_flow_hour={'costs': gas_price, 'CO2': 0.3}), + outputs=[fx.Flow('Q_Gas', bus='Gas', size=1000, effects_per_flow_hour={'costs': gas_price, 'CO2': 0.3})], ), - fx.Sink('Einspeisung', sink=fx.Flow('P_el', bus='Strom', size=1000, effects_per_flow_hour=p_feed_in)), + fx.Sink('Einspeisung', inputs=[fx.Flow('P_el', bus='Strom', size=1000, effects_per_flow_hour=p_feed_in)]), fx.Source( 'Stromtarif', - source=fx.Flow('P_el', bus='Strom', size=1000, effects_per_flow_hour={'costs': p_sell, 'CO2': 0.3}), + outputs=[fx.Flow('P_el', bus='Strom', size=1000, effects_per_flow_hour={'costs': p_sell, 'CO2': 0.3})], ), ) @@ -406,6 +638,71 @@ def flow_system_long(): } +@pytest.fixture(params=['h', '3h'], ids=['hourly', '3-hourly']) +def timesteps_linopy(request): + return pd.date_range('2020-01-01', periods=10, freq=request.param, name='time') + + +@pytest.fixture +def basic_flow_system_linopy(timesteps_linopy) -> fx.FlowSystem: + """Create basic elements for component testing""" + flow_system = fx.FlowSystem(timesteps_linopy) + + n = len(flow_system.timesteps) + thermal_load = LoadProfiles.random_thermal(n) + p_el = LoadProfiles.random_electrical(n) + + costs = Effects.costs() + heat_load = Sinks.heat_load(thermal_load) + gas_source = Sources.gas_with_costs() + electricity_sink = Sinks.electricity_feed_in(p_el) + + flow_system.add_elements(*Buses.defaults()) + flow_system.add_elements(costs, heat_load, gas_source, electricity_sink) + + return flow_system + + +@pytest.fixture +def basic_flow_system_linopy_coords(coords_config) -> fx.FlowSystem: + """Create basic elements for component testing with coordinate parametrization.""" + flow_system = fx.FlowSystem(**coords_config) + + thermal_load = LoadProfiles.random_thermal(10) + p_el = LoadProfiles.random_electrical(10) + + costs = Effects.costs() + heat_load = Sinks.heat_load(thermal_load) + gas_source = Sources.gas_with_costs() + electricity_sink = Sinks.electricity_feed_in(p_el) + + flow_system.add_elements(*Buses.defaults()) + flow_system.add_elements(costs, heat_load, gas_source, electricity_sink) + + return flow_system + + +# ============================================================================ +# UTILITY FUNCTIONS (kept for backward compatibility) +# ============================================================================ + + +# Custom assertion function +def assert_almost_equal_numeric( + actual, desired, err_msg, relative_error_range_in_percent=0.011, absolute_tolerance=1e-7 +): + """ + Custom assertion function for comparing numeric values with relative and absolute tolerances + """ + relative_tol = relative_error_range_in_percent / 100 + + if isinstance(desired, (int, float)): + delta = abs(relative_tol * desired) if desired != 0 else absolute_tolerance + assert np.isclose(actual, desired, atol=delta), err_msg + else: + np.testing.assert_allclose(actual, desired, rtol=relative_tol, atol=absolute_tolerance, err_msg=err_msg) + + def create_calculation_and_solve( flow_system: fx.FlowSystem, solver, name: str, allow_infeasible: bool = False ) -> fx.FullCalculation: @@ -421,37 +718,21 @@ def create_calculation_and_solve( return calculation -def create_linopy_model(flow_system: fx.FlowSystem) -> SystemModel: +def create_linopy_model(flow_system: fx.FlowSystem) -> FlowSystemModel: + """ + Create a FlowSystemModel from a FlowSystem by performing the modeling phase. + + Args: + flow_system: The FlowSystem to build the model from. + + Returns: + FlowSystemModel: The built model from FullCalculation.do_modeling(). + """ calculation = fx.FullCalculation('GenericName', flow_system) calculation.do_modeling() return calculation.model -@pytest.fixture(params=['h', '3h']) -def timesteps_linopy(request): - return pd.date_range('2020-01-01', periods=10, freq=request.param, name='time') - - -@pytest.fixture -def basic_flow_system_linopy(timesteps_linopy) -> fx.FlowSystem: - """Create basic elements for component testing""" - flow_system = fx.FlowSystem(timesteps_linopy) - thermal_load = np.array([np.random.random() for _ in range(10)]) * 180 - p_el = (np.array([np.random.random() for _ in range(10)]) + 0.5) / 1.5 * 50 - - flow_system.add_elements( - fx.Bus('Strom'), - fx.Bus('Fernwärme'), - fx.Bus('Gas'), - fx.Effect('Costs', '€', 'Kosten', is_standard=True, is_objective=True), - fx.Sink('Wärmelast', sink=fx.Flow('Q_th_Last', 'Fernwärme', size=1, fixed_relative_profile=thermal_load)), - fx.Source('Gastarif', source=fx.Flow('Q_Gas', 'Gas', size=1000, effects_per_flow_hour=0.04)), - fx.Sink('Einspeisung', sink=fx.Flow('P_el', 'Strom', effects_per_flow_hour=-1 * p_el)), - ) - - return flow_system - - def assert_conequal(actual: linopy.Constraint, desired: linopy.Constraint): """Assert that two constraints are equal with detailed error messages.""" @@ -506,3 +787,65 @@ def assert_var_equal(actual: linopy.Variable, desired: linopy.Variable): if actual.coord_dims != desired.coord_dims: raise AssertionError(f"{name} coordinate dimensions don't match: {actual.coord_dims} != {desired.coord_dims}") + + +def assert_sets_equal(set1: Iterable, set2: Iterable, msg=''): + """Assert two sets are equal with custom error message.""" + set1, set2 = set(set1), set(set2) + + extra = set1 - set2 + missing = set2 - set1 + + if extra or missing: + parts = [] + if extra: + parts.append(f'Extra: {sorted(extra, key=repr)}') + if missing: + parts.append(f'Missing: {sorted(missing, key=repr)}') + + error_msg = ', '.join(parts) + if msg: + error_msg = f'{msg}: {error_msg}' + + raise AssertionError(error_msg) + + +# ============================================================================ +# PLOTTING CLEANUP FIXTURES +# ============================================================================ + + +@pytest.fixture(autouse=True) +def cleanup_figures(): + """ + Cleanup matplotlib figures after each test. + + This fixture runs automatically after every test to: + - Close all matplotlib figures to prevent memory leaks + """ + yield + # Close all matplotlib figures + import matplotlib.pyplot as plt + + plt.close('all') + + +@pytest.fixture(scope='session', autouse=True) +def set_test_environment(): + """ + Configure plotting for test environment. + + This fixture runs once per test session to: + - Set matplotlib to use non-interactive 'Agg' backend + - Set plotly to use non-interactive 'json' renderer + - Prevent GUI windows from opening during tests + """ + import matplotlib + + matplotlib.use('Agg') # Use non-interactive backend + + import plotly.io as pio + + pio.renderers.default = 'json' # Use non-interactive renderer + + yield diff --git a/tests/run_all_tests.py b/tests/run_all_tests.py index 5597a47f3..83b6dfacf 100644 --- a/tests/run_all_tests.py +++ b/tests/run_all_tests.py @@ -7,4 +7,4 @@ import pytest if __name__ == '__main__': - pytest.main(['test_functional.py', '--disable-warnings']) + pytest.main(['test_integration.py', '--disable-warnings']) diff --git a/tests/test_bus.py b/tests/test_bus.py index 2462ab14f..0a5b19d8d 100644 --- a/tests/test_bus.py +++ b/tests/test_bus.py @@ -6,47 +6,50 @@ class TestBusModel: """Test the FlowModel class.""" - def test_bus(self, basic_flow_system_linopy): + def test_bus(self, basic_flow_system_linopy_coords, coords_config): """Test that flow model constraints are correctly generated.""" - flow_system = basic_flow_system_linopy + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config bus = fx.Bus('TestBus', excess_penalty_per_flow_hour=None) flow_system.add_elements( bus, - fx.Sink('WärmelastTest', sink=fx.Flow('Q_th_Last', 'TestBus')), - fx.Source('GastarifTest', source=fx.Flow('Q_Gas', 'TestBus')), + fx.Sink('WärmelastTest', inputs=[fx.Flow('Q_th_Last', 'TestBus')]), + fx.Source('GastarifTest', outputs=[fx.Flow('Q_Gas', 'TestBus')]), ) model = create_linopy_model(flow_system) - assert set(bus.model.variables) == {'WärmelastTest(Q_th_Last)|flow_rate', 'GastarifTest(Q_Gas)|flow_rate'} - assert set(bus.model.constraints) == {'TestBus|balance'} + assert set(bus.submodel.variables) == {'WärmelastTest(Q_th_Last)|flow_rate', 'GastarifTest(Q_Gas)|flow_rate'} + assert set(bus.submodel.constraints) == {'TestBus|balance'} assert_conequal( model.constraints['TestBus|balance'], model.variables['GastarifTest(Q_Gas)|flow_rate'] == model.variables['WärmelastTest(Q_th_Last)|flow_rate'], ) - def test_bus_penalty(self, basic_flow_system_linopy): + def test_bus_penalty(self, basic_flow_system_linopy_coords, coords_config): """Test that flow model constraints are correctly generated.""" - flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config bus = fx.Bus('TestBus') flow_system.add_elements( bus, - fx.Sink('WärmelastTest', sink=fx.Flow('Q_th_Last', 'TestBus')), - fx.Source('GastarifTest', source=fx.Flow('Q_Gas', 'TestBus')), + fx.Sink('WärmelastTest', inputs=[fx.Flow('Q_th_Last', 'TestBus')]), + fx.Source('GastarifTest', outputs=[fx.Flow('Q_Gas', 'TestBus')]), ) model = create_linopy_model(flow_system) - assert set(bus.model.variables) == { + assert set(bus.submodel.variables) == { 'TestBus|excess_input', 'TestBus|excess_output', 'WärmelastTest(Q_th_Last)|flow_rate', 'GastarifTest(Q_Gas)|flow_rate', } - assert set(bus.model.constraints) == {'TestBus|balance'} + assert set(bus.submodel.constraints) == {'TestBus|balance'} - assert_var_equal(model.variables['TestBus|excess_input'], model.add_variables(lower=0, coords=(timesteps,))) - assert_var_equal(model.variables['TestBus|excess_output'], model.add_variables(lower=0, coords=(timesteps,))) + assert_var_equal( + model.variables['TestBus|excess_input'], model.add_variables(lower=0, coords=model.get_coords()) + ) + assert_var_equal( + model.variables['TestBus|excess_output'], model.add_variables(lower=0, coords=model.get_coords()) + ) assert_conequal( model.constraints['TestBus|balance'], @@ -63,3 +66,29 @@ def test_bus_penalty(self, basic_flow_system_linopy): == (model.variables['TestBus|excess_input'] * 1e5 * model.hours_per_step).sum() + (model.variables['TestBus|excess_output'] * 1e5 * model.hours_per_step).sum(), ) + + def test_bus_with_coords(self, basic_flow_system_linopy_coords, coords_config): + """Test bus behavior across different coordinate configurations.""" + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config + bus = fx.Bus('TestBus', excess_penalty_per_flow_hour=None) + flow_system.add_elements( + bus, + fx.Sink('WärmelastTest', inputs=[fx.Flow('Q_th_Last', 'TestBus')]), + fx.Source('GastarifTest', outputs=[fx.Flow('Q_Gas', 'TestBus')]), + ) + model = create_linopy_model(flow_system) + + # Same core assertions as your existing test + assert set(bus.submodel.variables) == {'WärmelastTest(Q_th_Last)|flow_rate', 'GastarifTest(Q_Gas)|flow_rate'} + assert set(bus.submodel.constraints) == {'TestBus|balance'} + + assert_conequal( + model.constraints['TestBus|balance'], + model.variables['GastarifTest(Q_Gas)|flow_rate'] == model.variables['WärmelastTest(Q_th_Last)|flow_rate'], + ) + + # Just verify coordinate dimensions are correct + gas_var = model.variables['GastarifTest(Q_Gas)|flow_rate'] + if flow_system.scenarios is not None: + assert 'scenario' in gas_var.dims + assert 'time' in gas_var.dims diff --git a/tests/test_component.py b/tests/test_component.py index 47a6219da..be1eecf3b 100644 --- a/tests/test_component.py +++ b/tests/test_component.py @@ -4,13 +4,19 @@ import flixopt as fx import flixopt.elements -from .conftest import assert_conequal, assert_var_equal, create_linopy_model +from .conftest import ( + assert_almost_equal_numeric, + assert_conequal, + assert_sets_equal, + assert_var_equal, + create_calculation_and_solve, + create_linopy_model, +) class TestComponentModel: - def test_flow_label_check(self, basic_flow_system_linopy): + def test_flow_label_check(self): """Test that flow model constraints are correctly generated.""" - _ = basic_flow_system_linopy inputs = [ fx.Flow('Q_th_Last', 'Fernwärme', relative_minimum=np.ones(10) * 0.1), fx.Flow('Q_Gas', 'Fernwärme', relative_minimum=np.ones(10) * 0.1), @@ -22,9 +28,9 @@ def test_flow_label_check(self, basic_flow_system_linopy): with pytest.raises(ValueError, match='Flow names must be unique!'): _ = flixopt.elements.Component('TestComponent', inputs=inputs, outputs=outputs) - def test_component(self, basic_flow_system_linopy): + def test_component(self, basic_flow_system_linopy_coords, coords_config): """Test that flow model constraints are correctly generated.""" - flow_system = basic_flow_system_linopy + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config inputs = [ fx.Flow('In1', 'Fernwärme', relative_minimum=np.ones(10) * 0.1), fx.Flow('In2', 'Fernwärme', relative_minimum=np.ones(10) * 0.1), @@ -37,28 +43,36 @@ def test_component(self, basic_flow_system_linopy): flow_system.add_elements(comp) _ = create_linopy_model(flow_system) - assert { - 'TestComponent(In1)|flow_rate', - 'TestComponent(In1)|total_flow_hours', - 'TestComponent(In2)|flow_rate', - 'TestComponent(In2)|total_flow_hours', - 'TestComponent(Out1)|flow_rate', - 'TestComponent(Out1)|total_flow_hours', - 'TestComponent(Out2)|flow_rate', - 'TestComponent(Out2)|total_flow_hours', - } == set(comp.model.variables) - - assert { - 'TestComponent(In1)|total_flow_hours', - 'TestComponent(In2)|total_flow_hours', - 'TestComponent(Out1)|total_flow_hours', - 'TestComponent(Out2)|total_flow_hours', - } == set(comp.model.constraints) - - def test_on_with_multiple_flows(self, basic_flow_system_linopy): + assert_sets_equal( + set(comp.submodel.variables), + { + 'TestComponent(In1)|flow_rate', + 'TestComponent(In1)|total_flow_hours', + 'TestComponent(In2)|flow_rate', + 'TestComponent(In2)|total_flow_hours', + 'TestComponent(Out1)|flow_rate', + 'TestComponent(Out1)|total_flow_hours', + 'TestComponent(Out2)|flow_rate', + 'TestComponent(Out2)|total_flow_hours', + }, + msg='Incorrect variables', + ) + + assert_sets_equal( + set(comp.submodel.constraints), + { + 'TestComponent(In1)|total_flow_hours', + 'TestComponent(In2)|total_flow_hours', + 'TestComponent(Out1)|total_flow_hours', + 'TestComponent(Out2)|total_flow_hours', + }, + msg='Incorrect constraints', + ) + + def test_on_with_multiple_flows(self, basic_flow_system_linopy_coords, coords_config): """Test that flow model constraints are correctly generated.""" - flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config + ub_out2 = np.linspace(1, 1.5, 10).round(2) inputs = [ fx.Flow('In1', 'Fernwärme', relative_minimum=np.ones(10) * 0.1, size=100), @@ -73,81 +87,94 @@ def test_on_with_multiple_flows(self, basic_flow_system_linopy): flow_system.add_elements(comp) model = create_linopy_model(flow_system) - assert { - 'TestComponent(In1)|flow_rate', - 'TestComponent(In1)|total_flow_hours', - 'TestComponent(In1)|on', - 'TestComponent(In1)|on_hours_total', - 'TestComponent(Out1)|flow_rate', - 'TestComponent(Out1)|total_flow_hours', - 'TestComponent(Out1)|on', - 'TestComponent(Out1)|on_hours_total', - 'TestComponent(Out2)|flow_rate', - 'TestComponent(Out2)|total_flow_hours', - 'TestComponent(Out2)|on', - 'TestComponent(Out2)|on_hours_total', - 'TestComponent|on', - 'TestComponent|on_hours_total', - } == set(comp.model.variables) - - assert { - 'TestComponent(In1)|total_flow_hours', - 'TestComponent(In1)|on_con1', - 'TestComponent(In1)|on_con2', - 'TestComponent(In1)|on_hours_total', - 'TestComponent(Out1)|total_flow_hours', - 'TestComponent(Out1)|on_con1', - 'TestComponent(Out1)|on_con2', - 'TestComponent(Out1)|on_hours_total', - 'TestComponent(Out2)|total_flow_hours', - 'TestComponent(Out2)|on_con1', - 'TestComponent(Out2)|on_con2', - 'TestComponent(Out2)|on_hours_total', - 'TestComponent|on_con1', - 'TestComponent|on_con2', - 'TestComponent|on_hours_total', - } == set(comp.model.constraints) + assert_sets_equal( + set(comp.submodel.variables), + { + 'TestComponent(In1)|flow_rate', + 'TestComponent(In1)|total_flow_hours', + 'TestComponent(In1)|on', + 'TestComponent(In1)|on_hours_total', + 'TestComponent(Out1)|flow_rate', + 'TestComponent(Out1)|total_flow_hours', + 'TestComponent(Out1)|on', + 'TestComponent(Out1)|on_hours_total', + 'TestComponent(Out2)|flow_rate', + 'TestComponent(Out2)|total_flow_hours', + 'TestComponent(Out2)|on', + 'TestComponent(Out2)|on_hours_total', + 'TestComponent|on', + 'TestComponent|on_hours_total', + }, + msg='Incorrect variables', + ) + + assert_sets_equal( + set(comp.submodel.constraints), + { + 'TestComponent(In1)|total_flow_hours', + 'TestComponent(In1)|flow_rate|lb', + 'TestComponent(In1)|flow_rate|ub', + 'TestComponent(In1)|on_hours_total', + 'TestComponent(Out1)|total_flow_hours', + 'TestComponent(Out1)|flow_rate|lb', + 'TestComponent(Out1)|flow_rate|ub', + 'TestComponent(Out1)|on_hours_total', + 'TestComponent(Out2)|total_flow_hours', + 'TestComponent(Out2)|flow_rate|lb', + 'TestComponent(Out2)|flow_rate|ub', + 'TestComponent(Out2)|on_hours_total', + 'TestComponent|on|lb', + 'TestComponent|on|ub', + 'TestComponent|on_hours_total', + }, + msg='Incorrect constraints', + ) + + upper_bound_flow_rate = outputs[1].relative_maximum + + assert upper_bound_flow_rate.dims == tuple(model.get_coords()) assert_var_equal( model['TestComponent(Out2)|flow_rate'], - model.add_variables(lower=0, upper=300 * ub_out2, coords=(timesteps,)), + model.add_variables(lower=0, upper=300 * upper_bound_flow_rate, coords=model.get_coords()), ) - assert_var_equal(model['TestComponent|on'], model.add_variables(binary=True, coords=(timesteps,))) - assert_var_equal(model['TestComponent(Out2)|on'], model.add_variables(binary=True, coords=(timesteps,))) + assert_var_equal(model['TestComponent|on'], model.add_variables(binary=True, coords=model.get_coords())) + assert_var_equal(model['TestComponent(Out2)|on'], model.add_variables(binary=True, coords=model.get_coords())) assert_conequal( - model.constraints['TestComponent(Out2)|on_con1'], - model.variables['TestComponent(Out2)|on'] * 0.3 * 300 <= model.variables['TestComponent(Out2)|flow_rate'], + model.constraints['TestComponent(Out2)|flow_rate|lb'], + model.variables['TestComponent(Out2)|flow_rate'] >= model.variables['TestComponent(Out2)|on'] * 0.3 * 300, ) assert_conequal( - model.constraints['TestComponent(Out2)|on_con2'], - model.variables['TestComponent(Out2)|on'] * 300 * ub_out2 - >= model.variables['TestComponent(Out2)|flow_rate'], + model.constraints['TestComponent(Out2)|flow_rate|ub'], + model.variables['TestComponent(Out2)|flow_rate'] + <= model.variables['TestComponent(Out2)|on'] * 300 * upper_bound_flow_rate, ) assert_conequal( - model.constraints['TestComponent|on_con1'], - model.variables['TestComponent|on'] * 1e-5 - <= model.variables['TestComponent(In1)|flow_rate'] - + model.variables['TestComponent(Out1)|flow_rate'] - + model.variables['TestComponent(Out2)|flow_rate'], + model.constraints['TestComponent|on|lb'], + model.variables['TestComponent|on'] + >= ( + model.variables['TestComponent(In1)|on'] + + model.variables['TestComponent(Out1)|on'] + + model.variables['TestComponent(Out2)|on'] + ) + / (3 + 1e-5), ) - # TODO: Might there be a better way to no use 1e-5? assert_conequal( - model.constraints['TestComponent|on_con2'], - model.variables['TestComponent|on'] * (100 + 200 + 300 * ub_out2) / 3 - >= ( - model.variables['TestComponent(In1)|flow_rate'] - + model.variables['TestComponent(Out1)|flow_rate'] - + model.variables['TestComponent(Out2)|flow_rate'] + model.constraints['TestComponent|on|ub'], + model.variables['TestComponent|on'] + <= ( + model.variables['TestComponent(In1)|on'] + + model.variables['TestComponent(Out1)|on'] + + model.variables['TestComponent(Out2)|on'] ) - / 3, + + 1e-5, ) - def test_on_with_single_flow(self, basic_flow_system_linopy): + def test_on_with_single_flow(self, basic_flow_system_linopy_coords, coords_config): """Test that flow model constraints are correctly generated.""" - flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config inputs = [ fx.Flow('In1', 'Fernwärme', relative_minimum=np.ones(10) * 0.1, size=100), ] @@ -158,45 +185,417 @@ def test_on_with_single_flow(self, basic_flow_system_linopy): flow_system.add_elements(comp) model = create_linopy_model(flow_system) - assert { - 'TestComponent(In1)|flow_rate', - 'TestComponent(In1)|total_flow_hours', - 'TestComponent(In1)|on', - 'TestComponent(In1)|on_hours_total', - 'TestComponent|on', - 'TestComponent|on_hours_total', - } == set(comp.model.variables) - - assert { - 'TestComponent(In1)|total_flow_hours', - 'TestComponent(In1)|on_con1', - 'TestComponent(In1)|on_con2', - 'TestComponent(In1)|on_hours_total', - 'TestComponent|on_con1', - 'TestComponent|on_con2', - 'TestComponent|on_hours_total', - } == set(comp.model.constraints) + assert_sets_equal( + set(comp.submodel.variables), + { + 'TestComponent(In1)|flow_rate', + 'TestComponent(In1)|total_flow_hours', + 'TestComponent(In1)|on', + 'TestComponent(In1)|on_hours_total', + 'TestComponent|on', + 'TestComponent|on_hours_total', + }, + msg='Incorrect variables', + ) + + assert_sets_equal( + set(comp.submodel.constraints), + { + 'TestComponent(In1)|total_flow_hours', + 'TestComponent(In1)|flow_rate|lb', + 'TestComponent(In1)|flow_rate|ub', + 'TestComponent(In1)|on_hours_total', + 'TestComponent|on', + 'TestComponent|on_hours_total', + }, + msg='Incorrect constraints', + ) assert_var_equal( - model['TestComponent(In1)|flow_rate'], model.add_variables(lower=0, upper=100, coords=(timesteps,)) + model['TestComponent(In1)|flow_rate'], model.add_variables(lower=0, upper=100, coords=model.get_coords()) ) - assert_var_equal(model['TestComponent|on'], model.add_variables(binary=True, coords=(timesteps,))) - assert_var_equal(model['TestComponent(In1)|on'], model.add_variables(binary=True, coords=(timesteps,))) + assert_var_equal(model['TestComponent|on'], model.add_variables(binary=True, coords=model.get_coords())) + assert_var_equal(model['TestComponent(In1)|on'], model.add_variables(binary=True, coords=model.get_coords())) assert_conequal( - model.constraints['TestComponent(In1)|on_con1'], - model.variables['TestComponent(In1)|on'] * 0.1 * 100 <= model.variables['TestComponent(In1)|flow_rate'], + model.constraints['TestComponent(In1)|flow_rate|lb'], + model.variables['TestComponent(In1)|flow_rate'] >= model.variables['TestComponent(In1)|on'] * 0.1 * 100, ) assert_conequal( - model.constraints['TestComponent(In1)|on_con2'], - model.variables['TestComponent(In1)|on'] * 100 >= model.variables['TestComponent(In1)|flow_rate'], + model.constraints['TestComponent(In1)|flow_rate|ub'], + model.variables['TestComponent(In1)|flow_rate'] <= model.variables['TestComponent(In1)|on'] * 100, ) assert_conequal( - model.constraints['TestComponent|on_con1'], - model.variables['TestComponent|on'] * 0.1 * 100 <= model.variables['TestComponent(In1)|flow_rate'], + model.constraints['TestComponent|on'], + model.variables['TestComponent|on'] == model.variables['TestComponent(In1)|on'], + ) + + def test_previous_states_with_multiple_flows(self, basic_flow_system_linopy_coords, coords_config): + """Test that flow model constraints are correctly generated.""" + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config + + ub_out2 = np.linspace(1, 1.5, 10).round(2) + inputs = [ + fx.Flow( + 'In1', + 'Fernwärme', + relative_minimum=np.ones(10) * 0.1, + size=100, + previous_flow_rate=np.array([0, 0, 1e-6, 1e-5, 1e-4, 3, 4]), + ), + ] + outputs = [ + fx.Flow('Out1', 'Gas', relative_minimum=np.ones(10) * 0.2, size=200, previous_flow_rate=[3, 4, 5]), + fx.Flow( + 'Out2', + 'Gas', + relative_minimum=np.ones(10) * 0.3, + relative_maximum=ub_out2, + size=300, + previous_flow_rate=20, + ), + ] + comp = flixopt.elements.Component( + 'TestComponent', inputs=inputs, outputs=outputs, on_off_parameters=fx.OnOffParameters() + ) + flow_system.add_elements(comp) + model = create_linopy_model(flow_system) + + assert_sets_equal( + set(comp.submodel.variables), + { + 'TestComponent(In1)|flow_rate', + 'TestComponent(In1)|total_flow_hours', + 'TestComponent(In1)|on', + 'TestComponent(In1)|on_hours_total', + 'TestComponent(Out1)|flow_rate', + 'TestComponent(Out1)|total_flow_hours', + 'TestComponent(Out1)|on', + 'TestComponent(Out1)|on_hours_total', + 'TestComponent(Out2)|flow_rate', + 'TestComponent(Out2)|total_flow_hours', + 'TestComponent(Out2)|on', + 'TestComponent(Out2)|on_hours_total', + 'TestComponent|on', + 'TestComponent|on_hours_total', + }, + msg='Incorrect variables', + ) + + assert_sets_equal( + set(comp.submodel.constraints), + { + 'TestComponent(In1)|total_flow_hours', + 'TestComponent(In1)|flow_rate|lb', + 'TestComponent(In1)|flow_rate|ub', + 'TestComponent(In1)|on_hours_total', + 'TestComponent(Out1)|total_flow_hours', + 'TestComponent(Out1)|flow_rate|lb', + 'TestComponent(Out1)|flow_rate|ub', + 'TestComponent(Out1)|on_hours_total', + 'TestComponent(Out2)|total_flow_hours', + 'TestComponent(Out2)|flow_rate|lb', + 'TestComponent(Out2)|flow_rate|ub', + 'TestComponent(Out2)|on_hours_total', + 'TestComponent|on|lb', + 'TestComponent|on|ub', + 'TestComponent|on_hours_total', + }, + msg='Incorrect constraints', + ) + + upper_bound_flow_rate = outputs[1].relative_maximum + + assert upper_bound_flow_rate.dims == tuple(model.get_coords()) + + assert_var_equal( + model['TestComponent(Out2)|flow_rate'], + model.add_variables(lower=0, upper=300 * upper_bound_flow_rate, coords=model.get_coords()), + ) + assert_var_equal(model['TestComponent|on'], model.add_variables(binary=True, coords=model.get_coords())) + assert_var_equal(model['TestComponent(Out2)|on'], model.add_variables(binary=True, coords=model.get_coords())) + + assert_conequal( + model.constraints['TestComponent(Out2)|flow_rate|lb'], + model.variables['TestComponent(Out2)|flow_rate'] >= model.variables['TestComponent(Out2)|on'] * 0.3 * 300, + ) + assert_conequal( + model.constraints['TestComponent(Out2)|flow_rate|ub'], + model.variables['TestComponent(Out2)|flow_rate'] + <= model.variables['TestComponent(Out2)|on'] * 300 * upper_bound_flow_rate, + ) + + assert_conequal( + model.constraints['TestComponent|on|lb'], + model.variables['TestComponent|on'] + >= ( + model.variables['TestComponent(In1)|on'] + + model.variables['TestComponent(Out1)|on'] + + model.variables['TestComponent(Out2)|on'] + ) + / (3 + 1e-5), + ) + assert_conequal( + model.constraints['TestComponent|on|ub'], + model.variables['TestComponent|on'] + <= ( + model.variables['TestComponent(In1)|on'] + + model.variables['TestComponent(Out1)|on'] + + model.variables['TestComponent(Out2)|on'] + ) + + 1e-5, + ) + + @pytest.mark.parametrize( + 'in1_previous_flow_rate, out1_previous_flow_rate, out2_previous_flow_rate, previous_on_hours', + [ + (None, None, None, 0), + (np.array([0, 1e-6, 1e-4, 5]), None, None, 2), + (np.array([0, 5, 0, 5]), None, None, 1), + (np.array([0, 5, 0, 0]), 3, 0, 1), + (np.array([0, 0, 2, 0, 4, 5]), [3, 4, 5], None, 4), + ], + ) + def test_previous_states_with_multiple_flows_parameterized( + self, + basic_flow_system_linopy_coords, + coords_config, + in1_previous_flow_rate, + out1_previous_flow_rate, + out2_previous_flow_rate, + previous_on_hours, + ): + """Test that flow model constraints are correctly generated with different previous flow rates and constraint factors.""" + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config + + ub_out2 = np.linspace(1, 1.5, 10).round(2) + inputs = [ + fx.Flow( + 'In1', + 'Fernwärme', + relative_minimum=np.ones(10) * 0.1, + size=100, + previous_flow_rate=in1_previous_flow_rate, + on_off_parameters=fx.OnOffParameters(consecutive_on_hours_min=3), + ), + ] + outputs = [ + fx.Flow( + 'Out1', 'Gas', relative_minimum=np.ones(10) * 0.2, size=200, previous_flow_rate=out1_previous_flow_rate + ), + fx.Flow( + 'Out2', + 'Gas', + relative_minimum=np.ones(10) * 0.3, + relative_maximum=ub_out2, + size=300, + previous_flow_rate=out2_previous_flow_rate, + ), + ] + comp = flixopt.elements.Component( + 'TestComponent', + inputs=inputs, + outputs=outputs, + on_off_parameters=fx.OnOffParameters(consecutive_on_hours_min=3), ) + flow_system.add_elements(comp) + create_linopy_model(flow_system) + assert_conequal( - model.constraints['TestComponent|on_con2'], - model.variables['TestComponent|on'] * 100 >= model.variables['TestComponent(In1)|flow_rate'], + comp.submodel.constraints['TestComponent|consecutive_on_hours|initial'], + comp.submodel.variables['TestComponent|consecutive_on_hours'].isel(time=0) + == comp.submodel.variables['TestComponent|on'].isel(time=0) * (previous_on_hours + 1), + ) + + +class TestTransmissionModel: + def test_transmission_basic(self, basic_flow_system, highs_solver): + """Test basic transmission functionality""" + flow_system = basic_flow_system + flow_system.add_elements(fx.Bus('Wärme lokal')) + + boiler = fx.linear_converters.Boiler( + 'Boiler', eta=0.5, Q_th=fx.Flow('Q_th', bus='Wärme lokal'), Q_fu=fx.Flow('Q_fu', bus='Gas') + ) + + transmission = fx.Transmission( + 'Rohr', + relative_losses=0.2, + absolute_losses=20, + in1=fx.Flow( + 'Rohr1', 'Wärme lokal', size=fx.InvestParameters(effects_of_investment_per_size=5, maximum_size=1e6) + ), + out1=fx.Flow('Rohr2', 'Fernwärme', size=1000), + ) + + flow_system.add_elements(transmission, boiler) + + _ = create_calculation_and_solve(flow_system, highs_solver, 'test_transmission_basic') + + # Assertions + assert_almost_equal_numeric( + transmission.in1.submodel.on_off.on.solution.values, + np.array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1]), + 'On does not work properly', + ) + + assert_almost_equal_numeric( + transmission.in1.submodel.flow_rate.solution.values * 0.8 - 20, + transmission.out1.submodel.flow_rate.solution.values, + 'Losses are not computed correctly', + ) + + def test_transmission_balanced(self, basic_flow_system, highs_solver): + """Test advanced transmission functionality""" + flow_system = basic_flow_system + flow_system.add_elements(fx.Bus('Wärme lokal')) + + boiler = fx.linear_converters.Boiler( + 'Boiler_Standard', + eta=0.9, + Q_th=fx.Flow('Q_th', bus='Fernwärme', relative_maximum=np.array([0, 0, 0, 1, 1, 1, 1, 1, 1, 1])), + Q_fu=fx.Flow('Q_fu', bus='Gas'), + ) + + boiler2 = fx.linear_converters.Boiler( + 'Boiler_backup', eta=0.4, Q_th=fx.Flow('Q_th', bus='Wärme lokal'), Q_fu=fx.Flow('Q_fu', bus='Gas') + ) + + last2 = fx.Sink( + 'Wärmelast2', + inputs=[ + fx.Flow( + 'Q_th_Last', + bus='Wärme lokal', + size=1, + fixed_relative_profile=flow_system.components['Wärmelast'].inputs[0].fixed_relative_profile + * np.array([0, 0, 0, 0, 0, 1, 1, 1, 1, 1]), + ) + ], + ) + + transmission = fx.Transmission( + 'Rohr', + relative_losses=0.2, + absolute_losses=20, + in1=fx.Flow( + 'Rohr1a', + bus='Wärme lokal', + size=fx.InvestParameters(effects_of_investment_per_size=5, maximum_size=1000), + ), + out1=fx.Flow('Rohr1b', 'Fernwärme', size=1000), + in2=fx.Flow('Rohr2a', 'Fernwärme', size=fx.InvestParameters()), + out2=fx.Flow('Rohr2b', bus='Wärme lokal', size=1000), + balanced=True, + ) + + flow_system.add_elements(transmission, boiler, boiler2, last2) + + calculation = create_calculation_and_solve(flow_system, highs_solver, 'test_transmission_advanced') + + # Assertions + assert_almost_equal_numeric( + transmission.in1.submodel.on_off.on.solution.values, + np.array([1, 1, 1, 0, 0, 0, 0, 0, 0, 0]), + 'On does not work properly', + ) + + assert_almost_equal_numeric( + calculation.results.model.variables['Rohr(Rohr1b)|flow_rate'].solution.values, + transmission.out1.submodel.flow_rate.solution.values, + 'Flow rate of Rohr__Rohr1b is not correct', + ) + + assert_almost_equal_numeric( + transmission.in1.submodel.flow_rate.solution.values * 0.8 + - np.array([20 if val > 0.1 else 0 for val in transmission.in1.submodel.flow_rate.solution.values]), + transmission.out1.submodel.flow_rate.solution.values, + 'Losses are not computed correctly', + ) + + assert_almost_equal_numeric( + transmission.in1.submodel._investment.size.solution.item(), + transmission.in2.submodel._investment.size.solution.item(), + 'The Investments are not equated correctly', + ) + + def test_transmission_unbalanced(self, basic_flow_system, highs_solver): + """Test advanced transmission functionality""" + flow_system = basic_flow_system + flow_system.add_elements(fx.Bus('Wärme lokal')) + + boiler = fx.linear_converters.Boiler( + 'Boiler_Standard', + eta=0.9, + Q_th=fx.Flow('Q_th', bus='Fernwärme', relative_maximum=np.array([0, 0, 0, 1, 1, 1, 1, 1, 1, 1])), + Q_fu=fx.Flow('Q_fu', bus='Gas'), + ) + + boiler2 = fx.linear_converters.Boiler( + 'Boiler_backup', eta=0.4, Q_th=fx.Flow('Q_th', bus='Wärme lokal'), Q_fu=fx.Flow('Q_fu', bus='Gas') + ) + + last2 = fx.Sink( + 'Wärmelast2', + inputs=[ + fx.Flow( + 'Q_th_Last', + bus='Wärme lokal', + size=1, + fixed_relative_profile=flow_system.components['Wärmelast'].inputs[0].fixed_relative_profile + * np.array([0, 0, 0, 0, 0, 1, 1, 1, 1, 1]), + ) + ], + ) + + transmission = fx.Transmission( + 'Rohr', + relative_losses=0.2, + absolute_losses=20, + in1=fx.Flow( + 'Rohr1a', + bus='Wärme lokal', + size=fx.InvestParameters(effects_of_investment_per_size=50, maximum_size=1000), + ), + out1=fx.Flow('Rohr1b', 'Fernwärme', size=1000), + in2=fx.Flow( + 'Rohr2a', + 'Fernwärme', + size=fx.InvestParameters(effects_of_investment_per_size=100, minimum_size=10, mandatory=True), + ), + out2=fx.Flow('Rohr2b', bus='Wärme lokal', size=1000), + balanced=False, + ) + + flow_system.add_elements(transmission, boiler, boiler2, last2) + + calculation = create_calculation_and_solve(flow_system, highs_solver, 'test_transmission_advanced') + + # Assertions + assert_almost_equal_numeric( + transmission.in1.submodel.on_off.on.solution.values, + np.array([1, 1, 1, 0, 0, 0, 0, 0, 0, 0]), + 'On does not work properly', + ) + + assert_almost_equal_numeric( + calculation.results.model.variables['Rohr(Rohr1b)|flow_rate'].solution.values, + transmission.out1.submodel.flow_rate.solution.values, + 'Flow rate of Rohr__Rohr1b is not correct', + ) + + assert_almost_equal_numeric( + transmission.in1.submodel.flow_rate.solution.values * 0.8 + - np.array([20 if val > 0.1 else 0 for val in transmission.in1.submodel.flow_rate.solution.values]), + transmission.out1.submodel.flow_rate.solution.values, + 'Losses are not computed correctly', + ) + + assert transmission.in1.submodel._investment.size.solution.item() > 11 + + assert_almost_equal_numeric( + transmission.in2.submodel._investment.size.solution.item(), + 10, + 'Sizing does not work properly', ) diff --git a/tests/test_config.py b/tests/test_config.py index c486d22c6..60ed80555 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -25,9 +25,9 @@ def teardown_method(self): def test_config_defaults(self): """Test that CONFIG has correct default values.""" assert CONFIG.Logging.level == 'INFO' - assert CONFIG.Logging.file == 'flixopt.log' + assert CONFIG.Logging.file is None assert CONFIG.Logging.rich is False - assert CONFIG.Logging.console is True + assert CONFIG.Logging.console is False assert CONFIG.Modeling.big == 10_000_000 assert CONFIG.Modeling.epsilon == 1e-5 assert CONFIG.Modeling.big_binary_bound == 100_000 @@ -39,9 +39,9 @@ def test_module_initialization(self): CONFIG.apply() logger = logging.getLogger('flixopt') # Should have at least one handler (file handler by default) - assert len(logger.handlers) >= 1 + assert len(logger.handlers) == 1 # Should have a file handler with default settings - assert any(isinstance(h, logging.handlers.RotatingFileHandler) for h in logger.handlers) + assert isinstance(logger.handlers[0], logging.NullHandler) def test_config_apply_console(self): """Test applying config with console logging enabled.""" @@ -102,7 +102,7 @@ def test_config_to_dict(self): assert config_dict['config_name'] == 'flixopt' assert config_dict['logging']['level'] == 'DEBUG' assert config_dict['logging']['console'] is True - assert config_dict['logging']['file'] == 'flixopt.log' + assert config_dict['logging']['file'] is None assert config_dict['logging']['rich'] is False assert 'modeling' in config_dict assert config_dict['modeling']['big'] == 10_000_000 @@ -433,9 +433,9 @@ def test_config_reset(self): # Verify all values are back to defaults assert CONFIG.Logging.level == 'INFO' - assert CONFIG.Logging.console is True + assert CONFIG.Logging.console is False assert CONFIG.Logging.rich is False - assert CONFIG.Logging.file == 'flixopt.log' + assert CONFIG.Logging.file is None assert CONFIG.Modeling.big == 10_000_000 assert CONFIG.Modeling.epsilon == 1e-5 assert CONFIG.Modeling.big_binary_bound == 100_000 @@ -444,7 +444,7 @@ def test_config_reset(self): # Verify logging was also reset logger = logging.getLogger('flixopt') assert logger.level == logging.INFO - assert any(isinstance(h, logging.handlers.RotatingFileHandler) for h in logger.handlers) + assert isinstance(logger.handlers[0], logging.NullHandler) def test_reset_matches_class_defaults(self): """Test that reset() values match the _DEFAULTS constants. diff --git a/tests/test_cycle_detection.py b/tests/test_cycle_detection.py new file mode 100644 index 000000000..753a9a3e5 --- /dev/null +++ b/tests/test_cycle_detection.py @@ -0,0 +1,200 @@ +import pytest + +from flixopt.effects import detect_cycles + + +def test_empty_graph(): + """Test that an empty graph has no cycles.""" + assert detect_cycles({}) == [] + + +def test_single_node(): + """Test that a graph with a single node and no edges has no cycles.""" + assert detect_cycles({'A': []}) == [] + + +def test_self_loop(): + """Test that a graph with a self-loop has a cycle.""" + cycles = detect_cycles({'A': ['A']}) + assert len(cycles) == 1 + assert cycles[0] == ['A', 'A'] + + +def test_simple_cycle(): + """Test that a simple cycle is detected.""" + graph = {'A': ['B'], 'B': ['C'], 'C': ['A']} + cycles = detect_cycles(graph) + assert len(cycles) == 1 + assert cycles[0] == ['A', 'B', 'C', 'A'] or cycles[0] == ['B', 'C', 'A', 'B'] or cycles[0] == ['C', 'A', 'B', 'C'] + + +def test_no_cycles(): + """Test that a directed acyclic graph has no cycles.""" + graph = {'A': ['B', 'C'], 'B': ['D', 'E'], 'C': ['F'], 'D': [], 'E': [], 'F': []} + assert detect_cycles(graph) == [] + + +def test_multiple_cycles(): + """Test that a graph with multiple cycles is detected.""" + graph = {'A': ['B', 'D'], 'B': ['C'], 'C': ['A'], 'D': ['E'], 'E': ['D']} + cycles = detect_cycles(graph) + assert len(cycles) == 2 + + # Check that both cycles are detected (order might vary) + cycle_strings = [','.join(cycle) for cycle in cycles] + assert ( + any('A,B,C,A' in s for s in cycle_strings) + or any('B,C,A,B' in s for s in cycle_strings) + or any('C,A,B,C' in s for s in cycle_strings) + ) + assert any('D,E,D' in s for s in cycle_strings) or any('E,D,E' in s for s in cycle_strings) + + +def test_hidden_cycle(): + """Test that a cycle hidden deep in the graph is detected.""" + graph = { + 'A': ['B', 'C'], + 'B': ['D'], + 'C': ['E'], + 'D': ['F'], + 'E': ['G'], + 'F': ['H'], + 'G': ['I'], + 'H': ['J'], + 'I': ['K'], + 'J': ['L'], + 'K': ['M'], + 'L': ['N'], + 'M': ['N'], + 'N': ['O'], + 'O': ['P'], + 'P': ['Q'], + 'Q': ['O'], # Hidden cycle O->P->Q->O + } + cycles = detect_cycles(graph) + assert len(cycles) == 1 + + # Check that the O-P-Q cycle is detected + cycle = cycles[0] + assert 'O' in cycle and 'P' in cycle and 'Q' in cycle + + # Check that they appear in the correct order + o_index = cycle.index('O') + p_index = cycle.index('P') + q_index = cycle.index('Q') + + # Check the cycle order is correct (allowing for different starting points) + cycle_len = len(cycle) + assert ( + (p_index == (o_index + 1) % cycle_len and q_index == (p_index + 1) % cycle_len) + or (q_index == (o_index + 1) % cycle_len and p_index == (q_index + 1) % cycle_len) + or (o_index == (p_index + 1) % cycle_len and q_index == (o_index + 1) % cycle_len) + ) + + +def test_disconnected_graph(): + """Test with a disconnected graph.""" + graph = {'A': ['B'], 'B': ['C'], 'C': [], 'D': ['E'], 'E': ['F'], 'F': []} + assert detect_cycles(graph) == [] + + +def test_disconnected_graph_with_cycle(): + """Test with a disconnected graph containing a cycle in one component.""" + graph = { + 'A': ['B'], + 'B': ['C'], + 'C': [], + 'D': ['E'], + 'E': ['F'], + 'F': ['D'], # Cycle in D->E->F->D + } + cycles = detect_cycles(graph) + assert len(cycles) == 1 + + # Check that the D-E-F cycle is detected + cycle = cycles[0] + assert 'D' in cycle and 'E' in cycle and 'F' in cycle + + # Check if they appear in the correct order + d_index = cycle.index('D') + e_index = cycle.index('E') + f_index = cycle.index('F') + + # Check the cycle order is correct (allowing for different starting points) + cycle_len = len(cycle) + assert ( + (e_index == (d_index + 1) % cycle_len and f_index == (e_index + 1) % cycle_len) + or (f_index == (d_index + 1) % cycle_len and e_index == (f_index + 1) % cycle_len) + or (d_index == (e_index + 1) % cycle_len and f_index == (d_index + 1) % cycle_len) + ) + + +def test_complex_dag(): + """Test with a complex directed acyclic graph.""" + graph = { + 'A': ['B', 'C', 'D'], + 'B': ['E', 'F'], + 'C': ['E', 'G'], + 'D': ['G', 'H'], + 'E': ['I', 'J'], + 'F': ['J', 'K'], + 'G': ['K', 'L'], + 'H': ['L', 'M'], + 'I': ['N'], + 'J': ['N', 'O'], + 'K': ['O', 'P'], + 'L': ['P', 'Q'], + 'M': ['Q'], + 'N': ['R'], + 'O': ['R', 'S'], + 'P': ['S'], + 'Q': ['S'], + 'R': [], + 'S': [], + } + assert detect_cycles(graph) == [] + + +def test_missing_node_in_connections(): + """Test behavior when a node referenced in edges doesn't have its own key.""" + graph = { + 'A': ['B', 'C'], + 'B': ['D'], + # C and D don't have their own entries + } + assert detect_cycles(graph) == [] + + +def test_non_string_keys(): + """Test with non-string keys to ensure the algorithm is generic.""" + graph = {1: [2, 3], 2: [4], 3: [4], 4: []} + assert detect_cycles(graph) == [] + + graph_with_cycle = {1: [2], 2: [3], 3: [1]} + cycles = detect_cycles(graph_with_cycle) + assert len(cycles) == 1 + assert cycles[0] == [1, 2, 3, 1] or cycles[0] == [2, 3, 1, 2] or cycles[0] == [3, 1, 2, 3] + + +def test_complex_network_with_many_nodes(): + """Test with a large network to check performance and correctness.""" + graph = {} + # Create a large DAG + for i in range(100): + # Connect each node to the next few nodes + graph[i] = [j for j in range(i + 1, min(i + 5, 100))] + + # No cycles in this arrangement + assert detect_cycles(graph) == [] + + # Add a single back edge to create a cycle + graph[99] = [0] # This creates a cycle + cycles = detect_cycles(graph) + assert len(cycles) >= 1 + # The cycle might include many nodes, but must contain both 0 and 99 + any_cycle_has_both = any(0 in cycle and 99 in cycle for cycle in cycles) + assert any_cycle_has_both + + +if __name__ == '__main__': + pytest.main(['-v']) diff --git a/tests/test_dataconverter.py b/tests/test_dataconverter.py index 49f1438e7..0f12a1af3 100644 --- a/tests/test_dataconverter.py +++ b/tests/test_dataconverter.py @@ -3,110 +3,1259 @@ import pytest import xarray as xr -from flixopt.core import ConversionError, DataConverter # Adjust this import to match your project structure +from flixopt.core import ( # Adjust this import to match your project structure + ConversionError, + DataConverter, + TimeSeriesData, +) @pytest.fixture -def sample_time_index(request): - index = pd.date_range('2024-01-01', periods=5, freq='D', name='time') - return index - - -def test_scalar_conversion(sample_time_index): - # Test scalar conversion - result = DataConverter.as_dataarray(42, sample_time_index) - assert isinstance(result, xr.DataArray) - assert result.shape == (len(sample_time_index),) - assert result.dims == ('time',) - assert np.all(result.values == 42) - - -def test_series_conversion(sample_time_index): - series = pd.Series([1, 2, 3, 4, 5], index=sample_time_index) - - # Test Series conversion - result = DataConverter.as_dataarray(series, sample_time_index) - assert isinstance(result, xr.DataArray) - assert result.shape == (5,) - assert result.dims == ('time',) - assert np.array_equal(result.values, series.values) - - -def test_dataframe_conversion(sample_time_index): - # Create a single-column DataFrame - df = pd.DataFrame({'A': [1, 2, 3, 4, 5]}, index=sample_time_index) - - # Test DataFrame conversion - result = DataConverter.as_dataarray(df, sample_time_index) - assert isinstance(result, xr.DataArray) - assert result.shape == (5,) - assert result.dims == ('time',) - assert np.array_equal(result.values.flatten(), df['A'].values) - - -def test_ndarray_conversion(sample_time_index): - # Test 1D array conversion - arr_1d = np.array([1, 2, 3, 4, 5]) - result = DataConverter.as_dataarray(arr_1d, sample_time_index) - assert result.shape == (5,) - assert result.dims == ('time',) - assert np.array_equal(result.values, arr_1d) - - -def test_dataarray_conversion(sample_time_index): - # Create a DataArray - original = xr.DataArray(data=np.array([1, 2, 3, 4, 5]), coords={'time': sample_time_index}, dims=['time']) - - # Test DataArray conversion - result = DataConverter.as_dataarray(original, sample_time_index) - assert result.shape == (5,) - assert result.dims == ('time',) - assert np.array_equal(result.values, original.values) - - # Ensure it's a copy - result[0] = 999 - assert original[0].item() == 1 # Original should be unchanged - - -def test_invalid_inputs(sample_time_index): - # Test invalid input type - with pytest.raises(ConversionError): - DataConverter.as_dataarray('invalid_string', sample_time_index) - - # Test mismatched Series index - mismatched_series = pd.Series([1, 2, 3, 4, 5, 6], index=pd.date_range('2025-01-01', periods=6, freq='D')) - with pytest.raises(ConversionError): - DataConverter.as_dataarray(mismatched_series, sample_time_index) - - # Test DataFrame with multiple columns - df_multi_col = pd.DataFrame({'A': [1, 2, 3, 4, 5], 'B': [6, 7, 8, 9, 10]}, index=sample_time_index) - with pytest.raises(ConversionError): - DataConverter.as_dataarray(df_multi_col, sample_time_index) - - # Test mismatched array shape - with pytest.raises(ConversionError): - DataConverter.as_dataarray(np.array([1, 2, 3]), sample_time_index) # Wrong length - - # Test multi-dimensional array - with pytest.raises(ConversionError): - DataConverter.as_dataarray(np.array([[1, 2], [3, 4]]), sample_time_index) # 2D array not allowed - - -def test_time_index_validation(): - # Test with unnamed index - unnamed_index = pd.date_range('2024-01-01', periods=5, freq='D') - with pytest.raises(ConversionError): - DataConverter.as_dataarray(42, unnamed_index) - - # Test with empty index - empty_index = pd.DatetimeIndex([], name='time') - with pytest.raises(ValueError): - DataConverter.as_dataarray(42, empty_index) - - # Test with non-DatetimeIndex - wrong_type_index = pd.Index([1, 2, 3, 4, 5], name='time') - with pytest.raises(ValueError): - DataConverter.as_dataarray(42, wrong_type_index) +def time_coords(): + return pd.date_range('2024-01-01', periods=5, freq='D', name='time') + + +@pytest.fixture +def scenario_coords(): + return pd.Index(['baseline', 'high', 'low'], name='scenario') + + +@pytest.fixture +def region_coords(): + return pd.Index(['north', 'south', 'east'], name='region') + + +@pytest.fixture +def standard_coords(): + """Standard coordinates with unique lengths for easy testing.""" + return { + 'time': pd.date_range('2024-01-01', periods=5, freq='D', name='time'), # length 5 + 'scenario': pd.Index(['A', 'B', 'C'], name='scenario'), # length 3 + 'region': pd.Index(['north', 'south'], name='region'), # length 2 + } + + +class TestScalarConversion: + """Test scalar data conversions with different coordinate configurations.""" + + def test_scalar_no_coords(self): + """Scalar without coordinates should create 0D DataArray.""" + result = DataConverter.to_dataarray(42) + assert result.shape == () + assert result.dims == () + assert result.item() == 42 + + def test_scalar_single_coord(self, time_coords): + """Scalar with single coordinate should broadcast.""" + result = DataConverter.to_dataarray(42, coords={'time': time_coords}) + assert result.shape == (5,) + assert result.dims == ('time',) + assert np.all(result.values == 42) + + def test_scalar_multiple_coords(self, time_coords, scenario_coords): + """Scalar with multiple coordinates should broadcast to all.""" + result = DataConverter.to_dataarray(42, coords={'time': time_coords, 'scenario': scenario_coords}) + assert result.shape == (5, 3) + assert result.dims == ('time', 'scenario') + assert np.all(result.values == 42) + + def test_numpy_scalars(self, time_coords): + """Test numpy scalar types.""" + for scalar in [np.int32(42), np.int64(42), np.float32(42.5), np.float64(42.5)]: + result = DataConverter.to_dataarray(scalar, coords={'time': time_coords}) + assert result.shape == (5,) + assert np.all(result.values == scalar.item()) + + def test_scalar_many_dimensions(self, standard_coords): + """Scalar should broadcast to any number of dimensions.""" + coords = {**standard_coords, 'technology': pd.Index(['solar', 'wind'], name='technology')} + + result = DataConverter.to_dataarray(42, coords=coords) + assert result.shape == (5, 3, 2, 2) + assert result.dims == ('time', 'scenario', 'region', 'technology') + assert np.all(result.values == 42) + + +class TestOneDimensionalArrayConversion: + """Test 1D numpy array and pandas Series conversions.""" + + def test_1d_array_no_coords(self): + """1D array without coords should fail unless single element.""" + # Multi-element fails + with pytest.raises(ConversionError): + DataConverter.to_dataarray(np.array([1, 2, 3])) + + # Single element succeeds + result = DataConverter.to_dataarray(np.array([42])) + assert result.shape == () + assert result.item() == 42 + + def test_1d_array_matching_coord(self, time_coords): + """1D array matching coordinate length should work.""" + arr = np.array([10, 20, 30, 40, 50]) + result = DataConverter.to_dataarray(arr, coords={'time': time_coords}) + assert result.shape == (5,) + assert result.dims == ('time',) + assert np.array_equal(result.values, arr) + + def test_1d_array_mismatched_coord(self, time_coords): + """1D array not matching coordinate length should fail.""" + arr = np.array([10, 20, 30]) # Length 3, time_coords has length 5 + with pytest.raises(ConversionError): + DataConverter.to_dataarray(arr, coords={'time': time_coords}) + + def test_1d_array_broadcast_to_multiple_coords(self, time_coords, scenario_coords): + """1D array should broadcast to matching dimension.""" + # Array matching time dimension + time_arr = np.array([10, 20, 30, 40, 50]) + result = DataConverter.to_dataarray(time_arr, coords={'time': time_coords, 'scenario': scenario_coords}) + assert result.shape == (5, 3) + assert result.dims == ('time', 'scenario') + + # Each scenario should have the same time values + for scenario in scenario_coords: + assert np.array_equal(result.sel(scenario=scenario).values, time_arr) + + # Array matching scenario dimension + scenario_arr = np.array([100, 200, 300]) + result = DataConverter.to_dataarray(scenario_arr, coords={'time': time_coords, 'scenario': scenario_coords}) + assert result.shape == (5, 3) + assert result.dims == ('time', 'scenario') + + # Each time should have the same scenario values + for time in time_coords: + assert np.array_equal(result.sel(time=time).values, scenario_arr) + + def test_1d_array_ambiguous_length(self): + """Array length matching multiple dimensions should fail.""" + # Both dimensions have length 3 + coords_3x3 = { + 'time': pd.date_range('2024-01-01', periods=3, freq='D', name='time'), + 'scenario': pd.Index(['A', 'B', 'C'], name='scenario'), + } + arr = np.array([1, 2, 3]) + + with pytest.raises(ConversionError, match='matches multiple dimension'): + DataConverter.to_dataarray(arr, coords=coords_3x3) + + def test_1d_array_broadcast_to_many_dimensions(self, standard_coords): + """1D array should broadcast to many dimensions.""" + # Array matching time dimension + time_arr = np.array([10, 20, 30, 40, 50]) + result = DataConverter.to_dataarray(time_arr, coords=standard_coords) + + assert result.shape == (5, 3, 2) + assert result.dims == ('time', 'scenario', 'region') + + # Check broadcasting - all scenarios and regions should have same time values + for scenario in standard_coords['scenario']: + for region in standard_coords['region']: + assert np.array_equal(result.sel(scenario=scenario, region=region).values, time_arr) + + +class TestSeriesConversion: + """Test pandas Series conversions.""" + + def test_series_no_coords(self): + """Series without coords should fail unless single element.""" + # Multi-element fails + series = pd.Series([1, 2, 3]) + with pytest.raises(ConversionError): + DataConverter.to_dataarray(series) + + # Single element succeeds + single_series = pd.Series([42]) + result = DataConverter.to_dataarray(single_series) + assert result.shape == () + assert result.item() == 42 + + def test_series_matching_index(self, time_coords, scenario_coords): + """Series with matching index should work.""" + # Time-indexed series + time_series = pd.Series([10, 20, 30, 40, 50], index=time_coords) + result = DataConverter.to_dataarray(time_series, coords={'time': time_coords}) + assert result.shape == (5,) + assert result.dims == ('time',) + assert np.array_equal(result.values, time_series.values) + + # Scenario-indexed series + scenario_series = pd.Series([100, 200, 300], index=scenario_coords) + result = DataConverter.to_dataarray(scenario_series, coords={'scenario': scenario_coords}) + assert result.shape == (3,) + assert result.dims == ('scenario',) + assert np.array_equal(result.values, scenario_series.values) + + def test_series_mismatched_index(self, time_coords): + """Series with non-matching index should fail.""" + wrong_times = pd.date_range('2025-01-01', periods=5, freq='D', name='time') + series = pd.Series([10, 20, 30, 40, 50], index=wrong_times) + + with pytest.raises(ConversionError): + DataConverter.to_dataarray(series, coords={'time': time_coords}) + + def test_series_broadcast_to_multiple_coords(self, time_coords, scenario_coords): + """Series should broadcast to non-matching dimensions.""" + # Time series broadcast to scenarios + time_series = pd.Series([10, 20, 30, 40, 50], index=time_coords) + result = DataConverter.to_dataarray(time_series, coords={'time': time_coords, 'scenario': scenario_coords}) + assert result.shape == (5, 3) + + for scenario in scenario_coords: + assert np.array_equal(result.sel(scenario=scenario).values, time_series.values) + + def test_series_wrong_dimension(self, time_coords, region_coords): + """Series indexed by dimension not in coords should fail.""" + wrong_series = pd.Series([1, 2, 3], index=region_coords) + + with pytest.raises(ConversionError): + DataConverter.to_dataarray(wrong_series, coords={'time': time_coords}) + + def test_series_broadcast_to_many_dimensions(self, standard_coords): + """Series should broadcast to many dimensions.""" + time_series = pd.Series([100, 200, 300, 400, 500], index=standard_coords['time']) + result = DataConverter.to_dataarray(time_series, coords=standard_coords) + + assert result.shape == (5, 3, 2) + assert result.dims == ('time', 'scenario', 'region') + + # Check that all non-time dimensions have the same time series values + for scenario in standard_coords['scenario']: + for region in standard_coords['region']: + assert np.array_equal(result.sel(scenario=scenario, region=region).values, time_series.values) + + +class TestDataFrameConversion: + """Test pandas DataFrame conversions.""" + + def test_single_column_dataframe(self, time_coords): + """Single-column DataFrame should work like Series.""" + df = pd.DataFrame({'value': [10, 20, 30, 40, 50]}, index=time_coords) + result = DataConverter.to_dataarray(df, coords={'time': time_coords}) + + assert result.shape == (5,) + assert result.dims == ('time',) + assert np.array_equal(result.values, df['value'].values) + + def test_multi_column_dataframe_accepted(self, time_coords, scenario_coords): + """Multi-column DataFrame should now be accepted and converted via numpy array path.""" + df = pd.DataFrame( + {'value1': [10, 20, 30, 40, 50], 'value2': [15, 25, 35, 45, 55], 'value3': [12, 22, 32, 42, 52]}, + index=time_coords, + ) + + # Should work by converting to numpy array (5x3) and matching to time x scenario + result = DataConverter.to_dataarray(df, coords={'time': time_coords, 'scenario': scenario_coords}) + + assert result.shape == (5, 3) + assert result.dims == ('time', 'scenario') + assert np.array_equal(result.values, df.to_numpy()) + + def test_empty_dataframe_rejected(self, time_coords): + """Empty DataFrame should be rejected.""" + df = pd.DataFrame(index=time_coords) # No columns + + with pytest.raises(ConversionError, match='DataFrame must have at least one column'): + DataConverter.to_dataarray(df, coords={'time': time_coords}) + + def test_dataframe_broadcast(self, time_coords, scenario_coords): + """Single-column DataFrame should broadcast like Series.""" + df = pd.DataFrame({'power': [10, 20, 30, 40, 50]}, index=time_coords) + result = DataConverter.to_dataarray(df, coords={'time': time_coords, 'scenario': scenario_coords}) + + assert result.shape == (5, 3) + for scenario in scenario_coords: + assert np.array_equal(result.sel(scenario=scenario).values, df['power'].values) + + +class TestMultiDimensionalArrayConversion: + """Test multi-dimensional numpy array conversions.""" + + def test_2d_array_unique_dimensions(self, standard_coords): + """2D array with unique dimension lengths should work.""" + # 5x3 array should map to time x scenario + data_2d = np.random.rand(5, 3) + result = DataConverter.to_dataarray( + data_2d, coords={'time': standard_coords['time'], 'scenario': standard_coords['scenario']} + ) + + assert result.shape == (5, 3) + assert result.dims == ('time', 'scenario') + assert np.array_equal(result.values, data_2d) + + # 3x5 array should map to scenario x time + data_2d_flipped = np.random.rand(3, 5) + result_flipped = DataConverter.to_dataarray( + data_2d_flipped, coords={'time': standard_coords['time'], 'scenario': standard_coords['scenario']} + ) + + assert result_flipped.shape == (5, 3) + assert result_flipped.dims == ('time', 'scenario') + assert np.array_equal(result_flipped.values.transpose(), data_2d_flipped) + + def test_2d_array_broadcast_to_3d(self, standard_coords): + """2D array should broadcast to additional dimensions when using partial matching.""" + # With improved integration, 2D array (5x3) should match time×scenario and broadcast to region + data_2d = np.random.rand(5, 3) + result = DataConverter.to_dataarray(data_2d, coords=standard_coords) + + assert result.shape == (5, 3, 2) + assert result.dims == ('time', 'scenario', 'region') + + # Check that all regions have the same time x scenario data + for region in standard_coords['region']: + assert np.array_equal(result.sel(region=region).values, data_2d) + + def test_3d_array_unique_dimensions(self, standard_coords): + """3D array with unique dimension lengths should work.""" + # 5x3x2 array should map to time x scenario x region + data_3d = np.random.rand(5, 3, 2) + result = DataConverter.to_dataarray(data_3d, coords=standard_coords) + + assert result.shape == (5, 3, 2) + assert result.dims == ('time', 'scenario', 'region') + assert np.array_equal(result.values, data_3d) + + def test_3d_array_different_permutation(self, standard_coords): + """3D array with different dimension order should work.""" + # 2x5x3 array should map to region x time x scenario + data_3d = np.random.rand(2, 5, 3) + result = DataConverter.to_dataarray(data_3d, coords=standard_coords) + + assert result.shape == (5, 3, 2) + assert result.dims == ('time', 'scenario', 'region') + assert np.array_equal(result.transpose('region', 'time', 'scenario').values, data_3d) + + def test_4d_array_unique_dimensions(self): + """4D array with unique dimension lengths should work.""" + coords = { + 'time': pd.date_range('2024-01-01', periods=2, freq='D', name='time'), # length 2 + 'scenario': pd.Index(['A', 'B', 'C'], name='scenario'), # length 3 + 'region': pd.Index(['north', 'south', 'east', 'west'], name='region'), # length 4 + 'technology': pd.Index(['solar', 'wind', 'gas', 'coal', 'hydro'], name='technology'), # length 5 + } + + # 3x5x2x4 array should map to scenario x technology x time x region + data_4d = np.random.rand(3, 5, 2, 4) + result = DataConverter.to_dataarray(data_4d, coords=coords) + + assert result.shape == (2, 3, 4, 5) + assert result.dims == ('time', 'scenario', 'region', 'technology') + assert np.array_equal(result.transpose('scenario', 'technology', 'time', 'region').values, data_4d) + + def test_2d_array_ambiguous_dimensions_error(self): + """2D array with ambiguous dimension lengths should fail.""" + # Both dimensions have length 3 + coords_ambiguous = { + 'scenario': pd.Index(['A', 'B', 'C'], name='scenario'), # length 3 + 'region': pd.Index(['north', 'south', 'east'], name='region'), # length 3 + } + + data_2d = np.random.rand(3, 3) + with pytest.raises(ConversionError, match='matches multiple dimension combinations'): + DataConverter.to_dataarray(data_2d, coords=coords_ambiguous) + + def test_multid_array_no_coords(self): + """Multi-D arrays without coords should fail unless scalar.""" + # Multi-element fails + data_2d = np.random.rand(2, 3) + with pytest.raises(ConversionError, match='Cannot convert multi-element array without target dimensions'): + DataConverter.to_dataarray(data_2d) + + # Single element succeeds + single_element = np.array([[42]]) + result = DataConverter.to_dataarray(single_element) + assert result.shape == () + assert result.item() == 42 + + def test_array_no_matching_dimensions_error(self, standard_coords): + """Array with no matching dimension lengths should fail.""" + # 7x8 array - no dimension has length 7 or 8 + data_2d = np.random.rand(7, 8) + coords_2d = { + 'time': standard_coords['time'], # length 5 + 'scenario': standard_coords['scenario'], # length 3 + } + + with pytest.raises(ConversionError, match='cannot be mapped to any combination'): + DataConverter.to_dataarray(data_2d, coords=coords_2d) + + def test_multid_array_special_values(self, standard_coords): + """Multi-D arrays should preserve special values.""" + # Create 2D array with special values + data_2d = np.array( + [[1.0, np.nan, 3.0], [np.inf, 5.0, -np.inf], [7.0, 8.0, 9.0], [10.0, np.nan, 12.0], [13.0, 14.0, np.inf]] + ) + + result = DataConverter.to_dataarray( + data_2d, coords={'time': standard_coords['time'], 'scenario': standard_coords['scenario']} + ) + + assert result.shape == (5, 3) + assert np.array_equal(np.isnan(result.values), np.isnan(data_2d)) + assert np.array_equal(np.isinf(result.values), np.isinf(data_2d)) + + def test_multid_array_dtype_preservation(self, standard_coords): + """Multi-D arrays should preserve data types.""" + # Integer array + int_data = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12], [13, 14, 15]], dtype=np.int32) + + result_int = DataConverter.to_dataarray( + int_data, coords={'time': standard_coords['time'], 'scenario': standard_coords['scenario']} + ) + + assert result_int.dtype == np.int32 + assert np.array_equal(result_int.values, int_data) + + # Boolean array + bool_data = np.array( + [[True, False, True], [False, True, False], [True, True, False], [False, False, True], [True, False, True]] + ) + + result_bool = DataConverter.to_dataarray( + bool_data, coords={'time': standard_coords['time'], 'scenario': standard_coords['scenario']} + ) + + assert result_bool.dtype == bool + assert np.array_equal(result_bool.values, bool_data) + + +class TestDataArrayConversion: + """Test xarray DataArray conversions.""" + + def test_compatible_dataarray(self, time_coords): + """Compatible DataArray should pass through.""" + original = xr.DataArray([10, 20, 30, 40, 50], coords={'time': time_coords}, dims='time') + result = DataConverter.to_dataarray(original, coords={'time': time_coords}) + + assert result.shape == (5,) + assert result.dims == ('time',) + assert np.array_equal(result.values, original.values) + + # Should be a copy + result[0] = 999 + assert original[0].item() == 10 + + def test_incompatible_dataarray_coords(self, time_coords): + """DataArray with wrong coordinates should fail.""" + wrong_times = pd.date_range('2025-01-01', periods=5, freq='D', name='time') + original = xr.DataArray([10, 20, 30, 40, 50], coords={'time': wrong_times}, dims='time') + + with pytest.raises(ConversionError): + DataConverter.to_dataarray(original, coords={'time': time_coords}) + + def test_incompatible_dataarray_dims(self, time_coords): + """DataArray with wrong dimensions should fail.""" + original = xr.DataArray([10, 20, 30, 40, 50], coords={'wrong_dim': range(5)}, dims='wrong_dim') + + with pytest.raises(ConversionError): + DataConverter.to_dataarray(original, coords={'time': time_coords}) + + def test_dataarray_broadcast(self, time_coords, scenario_coords): + """DataArray should broadcast to additional dimensions.""" + # 1D time DataArray to 2D time+scenario + original = xr.DataArray([10, 20, 30, 40, 50], coords={'time': time_coords}, dims='time') + result = DataConverter.to_dataarray(original, coords={'time': time_coords, 'scenario': scenario_coords}) + + assert result.shape == (5, 3) + assert result.dims == ('time', 'scenario') + + for scenario in scenario_coords: + assert np.array_equal(result.sel(scenario=scenario).values, original.values) + + def test_scalar_dataarray_broadcast(self, time_coords, scenario_coords): + """Scalar DataArray should broadcast to all dimensions.""" + scalar_da = xr.DataArray(42) + result = DataConverter.to_dataarray(scalar_da, coords={'time': time_coords, 'scenario': scenario_coords}) + + assert result.shape == (5, 3) + assert np.all(result.values == 42) + + def test_2d_dataarray_broadcast_to_more_dimensions(self, standard_coords): + """DataArray should broadcast to additional dimensions.""" + # Start with 2D DataArray + original = xr.DataArray( + [[10, 20, 30], [40, 50, 60], [70, 80, 90], [100, 110, 120], [130, 140, 150]], + coords={'time': standard_coords['time'], 'scenario': standard_coords['scenario']}, + dims=('time', 'scenario'), + ) + + # Broadcast to 3D + result = DataConverter.to_dataarray(original, coords=standard_coords) + + assert result.shape == (5, 3, 2) + assert result.dims == ('time', 'scenario', 'region') + + # Check that all regions have the same time+scenario values + for region in standard_coords['region']: + assert np.array_equal(result.sel(region=region).values, original.values) + + +class TestTimeSeriesDataConversion: + """Test TimeSeriesData conversions.""" + + def test_timeseries_data_basic(self, time_coords): + """TimeSeriesData should work like DataArray.""" + data_array = xr.DataArray([10, 20, 30, 40, 50], coords={'time': time_coords}, dims='time') + ts_data = TimeSeriesData(data_array, aggregation_group='test') + + result = DataConverter.to_dataarray(ts_data, coords={'time': time_coords}) + + assert result.shape == (5,) + assert result.dims == ('time',) + assert np.array_equal(result.values, [10, 20, 30, 40, 50]) + + def test_timeseries_data_broadcast(self, time_coords, scenario_coords): + """TimeSeriesData should broadcast to additional dimensions.""" + data_array = xr.DataArray([10, 20, 30, 40, 50], coords={'time': time_coords}, dims='time') + ts_data = TimeSeriesData(data_array) + + result = DataConverter.to_dataarray(ts_data, coords={'time': time_coords, 'scenario': scenario_coords}) + + assert result.shape == (5, 3) + for scenario in scenario_coords: + assert np.array_equal(result.sel(scenario=scenario).values, [10, 20, 30, 40, 50]) + + +class TestAsDataArrayAlias: + """Test that to_dataarray works as an alias for to_dataarray.""" + + def test_to_dataarray_is_alias(self, time_coords, scenario_coords): + """to_dataarray should work identically to to_dataarray.""" + # Test with scalar + result_to = DataConverter.to_dataarray(42, coords={'time': time_coords}) + result_as = DataConverter.to_dataarray(42, coords={'time': time_coords}) + assert np.array_equal(result_to.values, result_as.values) + assert result_to.dims == result_as.dims + assert result_to.shape == result_as.shape + + # Test with array + arr = np.array([10, 20, 30, 40, 50]) + result_to_arr = DataConverter.to_dataarray(arr, coords={'time': time_coords}) + result_as_arr = DataConverter.to_dataarray(arr, coords={'time': time_coords}) + assert np.array_equal(result_to_arr.values, result_as_arr.values) + assert result_to_arr.dims == result_as_arr.dims + + # Test with Series + series = pd.Series([100, 200, 300, 400, 500], index=time_coords) + result_to_series = DataConverter.to_dataarray(series, coords={'time': time_coords, 'scenario': scenario_coords}) + result_as_series = DataConverter.to_dataarray(series, coords={'time': time_coords, 'scenario': scenario_coords}) + assert np.array_equal(result_to_series.values, result_as_series.values) + assert result_to_series.dims == result_as_series.dims + + +class TestCustomDimensions: + """Test with custom dimension names beyond time/scenario.""" + + def test_custom_single_dimension(self, region_coords): + """Test with custom dimension name.""" + result = DataConverter.to_dataarray(42, coords={'region': region_coords}) + assert result.shape == (3,) + assert result.dims == ('region',) + assert np.all(result.values == 42) + + def test_custom_multiple_dimensions(self): + """Test with multiple custom dimensions.""" + products = pd.Index(['A', 'B'], name='product') + technologies = pd.Index(['solar', 'wind', 'gas'], name='technology') + + # Array matching technology dimension + arr = np.array([100, 150, 80]) + result = DataConverter.to_dataarray(arr, coords={'product': products, 'technology': technologies}) + + assert result.shape == (2, 3) + assert result.dims == ('product', 'technology') + + # Should broadcast across products + for product in products: + assert np.array_equal(result.sel(product=product).values, arr) + + def test_mixed_dimension_types(self): + """Test mixing time dimension with custom dimensions.""" + time_coords = pd.date_range('2024-01-01', periods=3, freq='D', name='time') + regions = pd.Index(['north', 'south'], name='region') + + # Time series should broadcast to regions + time_series = pd.Series([10, 20, 30], index=time_coords) + result = DataConverter.to_dataarray(time_series, coords={'time': time_coords, 'region': regions}) + + assert result.shape == (3, 2) + assert result.dims == ('time', 'region') + + def test_custom_dimensions_complex(self): + """Test complex scenario with custom dimensions.""" + coords = { + 'product': pd.Index(['A', 'B'], name='product'), + 'factory': pd.Index(['F1', 'F2', 'F3'], name='factory'), + 'quarter': pd.Index(['Q1', 'Q2', 'Q3', 'Q4'], name='quarter'), + } + + # Array matching factory dimension + factory_arr = np.array([100, 200, 300]) + result = DataConverter.to_dataarray(factory_arr, coords=coords) + + assert result.shape == (2, 3, 4) + assert result.dims == ('product', 'factory', 'quarter') + + # Check broadcasting + for product in coords['product']: + for quarter in coords['quarter']: + slice_data = result.sel(product=product, quarter=quarter) + assert np.array_equal(slice_data.values, factory_arr) + + +class TestValidation: + """Test coordinate validation.""" + + def test_empty_coords(self): + """Empty coordinates should work for scalars.""" + result = DataConverter.to_dataarray(42, coords={}) + assert result.shape == () + assert result.item() == 42 + + def test_invalid_coord_type(self): + """Non-pandas Index coordinates should fail.""" + with pytest.raises(ConversionError): + DataConverter.to_dataarray(42, coords={'time': [1, 2, 3]}) + + def test_empty_coord_index(self): + """Empty coordinate index should fail.""" + empty_index = pd.Index([], name='time') + with pytest.raises(ConversionError): + DataConverter.to_dataarray(42, coords={'time': empty_index}) + + def test_time_coord_validation(self): + """Time coordinates must be DatetimeIndex.""" + # Non-datetime index with name 'time' should fail + wrong_time = pd.Index([1, 2, 3], name='time') + with pytest.raises(ConversionError, match='DatetimeIndex'): + DataConverter.to_dataarray(42, coords={'time': wrong_time}) + + def test_coord_naming(self, time_coords): + """Coordinates should be auto-renamed to match dimension.""" + # Unnamed time index should be renamed + unnamed_time = time_coords.rename(None) + result = DataConverter.to_dataarray(42, coords={'time': unnamed_time}) + assert result.coords['time'].name == 'time' + + +class TestErrorHandling: + """Test error handling and edge cases.""" + + def test_unsupported_data_types(self, time_coords): + """Unsupported data types should fail with clear messages.""" + unsupported = ['string', object(), None, {'dict': 'value'}, [1, 2, 3]] + + for data in unsupported: + with pytest.raises(ConversionError): + DataConverter.to_dataarray(data, coords={'time': time_coords}) + + def test_dimension_mismatch_messages(self, time_coords, scenario_coords): + """Error messages should be informative.""" + # Array with wrong length + wrong_arr = np.array([1, 2]) # Length 2, but no dimension has length 2 + with pytest.raises(ConversionError, match='does not match any target dimension lengths'): + DataConverter.to_dataarray(wrong_arr, coords={'time': time_coords, 'scenario': scenario_coords}) + + def test_multidimensional_array_dimension_count_mismatch(self, standard_coords): + """Array with wrong number of dimensions should fail with clear error.""" + # 4D array with 3D coordinates + data_4d = np.random.rand(5, 3, 2, 4) + with pytest.raises(ConversionError, match='cannot be mapped to any combination'): + DataConverter.to_dataarray(data_4d, coords=standard_coords) + + def test_error_message_quality(self, standard_coords): + """Error messages should include helpful information.""" + # Wrong shape array + data_2d = np.random.rand(7, 8) + coords_2d = { + 'time': standard_coords['time'], # length 5 + 'scenario': standard_coords['scenario'], # length 3 + } + + try: + DataConverter.to_dataarray(data_2d, coords=coords_2d) + raise AssertionError('Should have raised ConversionError') + except ConversionError as e: + error_msg = str(e) + assert 'Array shape (7, 8)' in error_msg + assert 'target coordinate lengths:' in error_msg + + +class TestDataIntegrity: + """Test data copying and integrity.""" + + def test_array_copy_independence(self, time_coords): + """Converted arrays should be independent copies.""" + original_arr = np.array([10, 20, 30, 40, 50]) + result = DataConverter.to_dataarray(original_arr, coords={'time': time_coords}) + + # Modify result + result[0] = 999 + + # Original should be unchanged + assert original_arr[0] == 10 + + def test_series_copy_independence(self, time_coords): + """Converted Series should be independent copies.""" + original_series = pd.Series([10, 20, 30, 40, 50], index=time_coords) + result = DataConverter.to_dataarray(original_series, coords={'time': time_coords}) + + # Modify result + result[0] = 999 + + # Original should be unchanged + assert original_series.iloc[0] == 10 + + def test_dataframe_copy_independence(self, time_coords): + """Converted DataFrames should be independent copies.""" + original_df = pd.DataFrame({'value': [10, 20, 30, 40, 50]}, index=time_coords) + result = DataConverter.to_dataarray(original_df, coords={'time': time_coords}) + + # Modify result + result[0] = 999 + + # Original should be unchanged + assert original_df.loc[time_coords[0], 'value'] == 10 + + def test_multid_array_copy_independence(self, standard_coords): + """Multi-D arrays should be independent copies.""" + original_data = np.random.rand(5, 3) + result = DataConverter.to_dataarray( + original_data, coords={'time': standard_coords['time'], 'scenario': standard_coords['scenario']} + ) + + # Modify result + result[0, 0] = 999 + + # Original should be unchanged + assert original_data[0, 0] != 999 + + +class TestBooleanValues: + """Test handling of boolean values and arrays.""" + + def test_scalar_boolean_to_dataarray(self, time_coords): + """Scalar boolean values should work with to_dataarray.""" + result_true = DataConverter.to_dataarray(True, coords={'time': time_coords}) + assert result_true.shape == (5,) + assert result_true.dtype == bool + assert np.all(result_true.values) + + result_false = DataConverter.to_dataarray(False, coords={'time': time_coords}) + assert result_false.shape == (5,) + assert result_false.dtype == bool + assert not np.any(result_false.values) + + def test_numpy_boolean_scalar(self, time_coords): + """Numpy boolean scalars should work.""" + result_np_true = DataConverter.to_dataarray(np.bool_(True), coords={'time': time_coords}) + assert result_np_true.shape == (5,) + assert result_np_true.dtype == bool + assert np.all(result_np_true.values) + + result_np_false = DataConverter.to_dataarray(np.bool_(False), coords={'time': time_coords}) + assert result_np_false.shape == (5,) + assert result_np_false.dtype == bool + assert not np.any(result_np_false.values) + + def test_boolean_array_to_dataarray(self, time_coords): + """Boolean arrays should work with to_dataarray.""" + bool_arr = np.array([True, False, True, False, True]) + result = DataConverter.to_dataarray(bool_arr, coords={'time': time_coords}) + assert result.shape == (5,) + assert result.dims == ('time',) + assert result.dtype == bool + assert np.array_equal(result.values, bool_arr) + + def test_boolean_no_coords(self): + """Boolean scalar without coordinates should create 0D DataArray.""" + result = DataConverter.to_dataarray(True) + assert result.shape == () + assert result.dims == () + assert result.item() + + result_as = DataConverter.to_dataarray(False) + assert result_as.shape == () + assert result_as.dims == () + assert not result_as.item() + + def test_boolean_multidimensional_broadcast(self, standard_coords): + """Boolean values should broadcast to multiple dimensions.""" + result = DataConverter.to_dataarray(True, coords=standard_coords) + assert result.shape == (5, 3, 2) + assert result.dims == ('time', 'scenario', 'region') + assert result.dtype == bool + assert np.all(result.values) + + result_as = DataConverter.to_dataarray(False, coords=standard_coords) + assert result_as.shape == (5, 3, 2) + assert result_as.dims == ('time', 'scenario', 'region') + assert result_as.dtype == bool + assert not np.any(result_as.values) + + def test_boolean_series(self, time_coords): + """Boolean Series should work.""" + bool_series = pd.Series([True, False, True, False, True], index=time_coords) + result = DataConverter.to_dataarray(bool_series, coords={'time': time_coords}) + assert result.shape == (5,) + assert result.dtype == bool + assert np.array_equal(result.values, bool_series.values) + + result_as = DataConverter.to_dataarray(bool_series, coords={'time': time_coords}) + assert result_as.shape == (5,) + assert result_as.dtype == bool + assert np.array_equal(result_as.values, bool_series.values) + + def test_boolean_dataframe(self, time_coords): + """Boolean DataFrame should work.""" + bool_df = pd.DataFrame({'values': [True, False, True, False, True]}, index=time_coords) + result = DataConverter.to_dataarray(bool_df, coords={'time': time_coords}) + assert result.shape == (5,) + assert result.dtype == bool + assert np.array_equal(result.values, bool_df['values'].values) + + result_as = DataConverter.to_dataarray(bool_df, coords={'time': time_coords}) + assert result_as.shape == (5,) + assert result_as.dtype == bool + assert np.array_equal(result_as.values, bool_df['values'].values) + + def test_multidimensional_boolean_array(self, standard_coords): + """Multi-dimensional boolean arrays should work.""" + bool_data = np.array( + [[True, False, True], [False, True, False], [True, True, False], [False, False, True], [True, False, True]] + ) + result = DataConverter.to_dataarray( + bool_data, coords={'time': standard_coords['time'], 'scenario': standard_coords['scenario']} + ) + assert result.shape == (5, 3) + assert result.dtype == bool + assert np.array_equal(result.values, bool_data) + + result_as = DataConverter.to_dataarray( + bool_data, coords={'time': standard_coords['time'], 'scenario': standard_coords['scenario']} + ) + assert result_as.shape == (5, 3) + assert result_as.dtype == bool + assert np.array_equal(result_as.values, bool_data) + + +class TestSpecialValues: + """Test handling of special numeric values.""" + + def test_nan_values(self, time_coords): + """NaN values should be preserved.""" + arr_with_nan = np.array([1, np.nan, 3, np.nan, 5]) + result = DataConverter.to_dataarray(arr_with_nan, coords={'time': time_coords}) + + assert np.array_equal(np.isnan(result.values), np.isnan(arr_with_nan)) + assert np.array_equal(result.values[~np.isnan(result.values)], arr_with_nan[~np.isnan(arr_with_nan)]) + + def test_infinite_values(self, time_coords): + """Infinite values should be preserved.""" + arr_with_inf = np.array([1, np.inf, 3, -np.inf, 5]) + result = DataConverter.to_dataarray(arr_with_inf, coords={'time': time_coords}) + + assert np.array_equal(result.values, arr_with_inf) + + def test_boolean_values(self, time_coords): + """Boolean values should be preserved.""" + bool_arr = np.array([True, False, True, False, True]) + result = DataConverter.to_dataarray(bool_arr, coords={'time': time_coords}) + + assert result.dtype == bool + assert np.array_equal(result.values, bool_arr) + + def test_mixed_numeric_types(self, time_coords): + """Mixed integer/float should become float.""" + mixed_arr = np.array([1, 2.5, 3, 4.5, 5]) + result = DataConverter.to_dataarray(mixed_arr, coords={'time': time_coords}) + + assert np.issubdtype(result.dtype, np.floating) + assert np.array_equal(result.values, mixed_arr) + + def test_special_values_in_multid_arrays(self, standard_coords): + """Special values should be preserved in multi-D arrays and broadcasting.""" + # Array with NaN and inf + special_arr = np.array([1, np.nan, np.inf, -np.inf, 5]) + result = DataConverter.to_dataarray(special_arr, coords=standard_coords) + + assert result.shape == (5, 3, 2) + + # Check that special values are preserved in all broadcasts + for scenario in standard_coords['scenario']: + for region in standard_coords['region']: + slice_data = result.sel(scenario=scenario, region=region) + assert np.array_equal(np.isnan(slice_data.values), np.isnan(special_arr)) + assert np.array_equal(np.isinf(slice_data.values), np.isinf(special_arr)) + + +class TestAdvancedBroadcasting: + """Test advanced broadcasting scenarios and edge cases.""" + + def test_partial_dimension_matching_with_broadcasting(self, standard_coords): + """Test that partial dimension matching works with the improved integration.""" + # 1D array matching one dimension should broadcast to all target dimensions + time_arr = np.array([10, 20, 30, 40, 50]) # matches time (length 5) + result = DataConverter.to_dataarray(time_arr, coords=standard_coords) + + assert result.shape == (5, 3, 2) + assert result.dims == ('time', 'scenario', 'region') + + # Verify broadcasting + for scenario in standard_coords['scenario']: + for region in standard_coords['region']: + assert np.array_equal(result.sel(scenario=scenario, region=region).values, time_arr) + + def test_complex_multid_scenario(self): + """Complex real-world scenario with multi-D array and broadcasting.""" + # Energy system data: time x technology, broadcast to regions + coords = { + 'time': pd.date_range('2024-01-01', periods=24, freq='h', name='time'), # 24 hours + 'technology': pd.Index(['solar', 'wind', 'gas', 'coal'], name='technology'), # 4 technologies + 'region': pd.Index(['north', 'south', 'east'], name='region'), # 3 regions + } + + # Capacity factors: 24 x 4 (will broadcast to 24 x 4 x 3) + capacity_factors = np.random.rand(24, 4) + + result = DataConverter.to_dataarray(capacity_factors, coords=coords) + + assert result.shape == (24, 4, 3) + assert result.dims == ('time', 'technology', 'region') + assert isinstance(result.indexes['time'], pd.DatetimeIndex) + + # Verify broadcasting: all regions should have same time×technology data + for region in coords['region']: + assert np.array_equal(result.sel(region=region).values, capacity_factors) + + def test_ambiguous_length_handling(self): + """Test handling of ambiguous length scenarios across different data types.""" + # All dimensions have length 3 + coords_3x3x3 = { + 'time': pd.date_range('2024-01-01', periods=3, freq='D', name='time'), + 'scenario': pd.Index(['A', 'B', 'C'], name='scenario'), + 'region': pd.Index(['X', 'Y', 'Z'], name='region'), + } + + # 1D array - should fail + arr_1d = np.array([1, 2, 3]) + with pytest.raises(ConversionError, match='matches multiple dimension'): + DataConverter.to_dataarray(arr_1d, coords=coords_3x3x3) + + # 2D array - should fail + arr_2d = np.random.rand(3, 3) + with pytest.raises(ConversionError, match='matches multiple dimension'): + DataConverter.to_dataarray(arr_2d, coords=coords_3x3x3) + + # 3D array - should fail + arr_3d = np.random.rand(3, 3, 3) + with pytest.raises(ConversionError, match='matches multiple dimension'): + DataConverter.to_dataarray(arr_3d, coords=coords_3x3x3) + + def test_mixed_broadcasting_scenarios(self): + """Test various broadcasting scenarios with different input types.""" + coords = { + 'time': pd.date_range('2024-01-01', periods=4, freq='D', name='time'), # length 4 + 'scenario': pd.Index(['A', 'B'], name='scenario'), # length 2 + 'region': pd.Index(['north', 'south', 'east'], name='region'), # length 3 + 'product': pd.Index(['X', 'Y', 'Z', 'W', 'V'], name='product'), # length 5 + } + + # Scalar to 4D + scalar_result = DataConverter.to_dataarray(42, coords=coords) + assert scalar_result.shape == (4, 2, 3, 5) + assert np.all(scalar_result.values == 42) + + # 1D array (length 4, matches time) to 4D + arr_1d = np.array([10, 20, 30, 40]) + arr_result = DataConverter.to_dataarray(arr_1d, coords=coords) + assert arr_result.shape == (4, 2, 3, 5) + # Verify broadcasting + for scenario in coords['scenario']: + for region in coords['region']: + for product in coords['product']: + assert np.array_equal( + arr_result.sel(scenario=scenario, region=region, product=product).values, arr_1d + ) + + # 2D array (4x2, matches time×scenario) to 4D + arr_2d = np.random.rand(4, 2) + arr_2d_result = DataConverter.to_dataarray(arr_2d, coords=coords) + assert arr_2d_result.shape == (4, 2, 3, 5) + # Verify broadcasting + for region in coords['region']: + for product in coords['product']: + assert np.array_equal(arr_2d_result.sel(region=region, product=product).values, arr_2d) + + +class TestAmbiguousDimensionLengthHandling: + """Test that DataConverter correctly raises errors when multiple dimensions have the same length.""" + + def test_1d_array_ambiguous_dimensions_simple(self): + """Test 1D array with two dimensions of same length should fail.""" + # Both dimensions have length 3 + coords_ambiguous = { + 'scenario': pd.Index(['A', 'B', 'C'], name='scenario'), # length 3 + 'region': pd.Index(['north', 'south', 'east'], name='region'), # length 3 + } + + arr_1d = np.array([1, 2, 3]) # length 3 - matches both dimensions + + with pytest.raises(ConversionError, match='matches multiple dimension'): + DataConverter.to_dataarray(arr_1d, coords=coords_ambiguous) + + def test_1d_array_ambiguous_dimensions_complex(self): + """Test 1D array with multiple dimensions of same length.""" + # Three dimensions have length 4 + coords_4x4x4 = { + 'time': pd.date_range('2024-01-01', periods=4, freq='D', name='time'), # length 4 + 'scenario': pd.Index(['A', 'B', 'C', 'D'], name='scenario'), # length 4 + 'region': pd.Index(['north', 'south', 'east', 'west'], name='region'), # length 4 + 'product': pd.Index(['X', 'Y'], name='product'), # length 2 - unique + } + + # Array matching the ambiguous length + arr_1d = np.array([10, 20, 30, 40]) # length 4 - matches time, scenario, region + + with pytest.raises(ConversionError, match='matches multiple dimension'): + DataConverter.to_dataarray(arr_1d, coords=coords_4x4x4) + + # Array matching the unique length should work + arr_1d_unique = np.array([100, 200]) # length 2 - matches only product + result = DataConverter.to_dataarray(arr_1d_unique, coords=coords_4x4x4) + assert result.shape == (4, 4, 4, 2) # broadcast to all dimensions + assert result.dims == ('time', 'scenario', 'region', 'product') + + def test_2d_array_ambiguous_dimensions_both_same(self): + """Test 2D array where both dimensions have the same ambiguous length.""" + # All dimensions have length 3 + coords_3x3x3 = { + 'time': pd.date_range('2024-01-01', periods=3, freq='D', name='time'), # length 3 + 'scenario': pd.Index(['A', 'B', 'C'], name='scenario'), # length 3 + 'region': pd.Index(['X', 'Y', 'Z'], name='region'), # length 3 + } + + # 3x3 array - could be any combination of the three dimensions + arr_2d = np.random.rand(3, 3) + + with pytest.raises(ConversionError, match='matches multiple dimension'): + DataConverter.to_dataarray(arr_2d, coords=coords_3x3x3) + + def test_2d_array_one_dimension_ambiguous(self): + """Test 2D array where only one dimension length is ambiguous.""" + coords_mixed = { + 'time': pd.date_range('2024-01-01', periods=5, freq='D', name='time'), # length 5 - unique + 'scenario': pd.Index(['A', 'B', 'C'], name='scenario'), # length 3 + 'region': pd.Index(['X', 'Y', 'Z'], name='region'), # length 3 - same as scenario + 'product': pd.Index(['P1', 'P2'], name='product'), # length 2 - unique + } + + # 5x3 array - first dimension clearly maps to time (unique length 5) + # but second dimension could be scenario or region (both length 3) + arr_5x3 = np.random.rand(5, 3) + + with pytest.raises(ConversionError, match='matches multiple dimension'): + DataConverter.to_dataarray(arr_5x3, coords=coords_mixed) + + # 5x2 array should work - dimensions are unambiguous + arr_5x2 = np.random.rand(5, 2) + result = DataConverter.to_dataarray( + arr_5x2, coords={'time': coords_mixed['time'], 'product': coords_mixed['product']} + ) + assert result.shape == (5, 2) + assert result.dims == ('time', 'product') + + def test_3d_array_all_dimensions_ambiguous(self): + """Test 3D array where all dimension lengths are ambiguous.""" + # All dimensions have length 2 + coords_2x2x2x2 = { + 'scenario': pd.Index(['A', 'B'], name='scenario'), # length 2 + 'region': pd.Index(['north', 'south'], name='region'), # length 2 + 'technology': pd.Index(['solar', 'wind'], name='technology'), # length 2 + 'product': pd.Index(['X', 'Y'], name='product'), # length 2 + } + + # 2x2x2 array - could be any combination of 3 dimensions from the 4 available + arr_3d = np.random.rand(2, 2, 2) + + with pytest.raises(ConversionError, match='matches multiple dimension'): + DataConverter.to_dataarray(arr_3d, coords=coords_2x2x2x2) + + def test_3d_array_partial_ambiguity(self): + """Test 3D array with partial dimension ambiguity.""" + coords_partial = { + 'time': pd.date_range('2024-01-01', periods=4, freq='D', name='time'), # length 4 - unique + 'scenario': pd.Index(['A', 'B', 'C'], name='scenario'), # length 3 + 'region': pd.Index(['X', 'Y', 'Z'], name='region'), # length 3 - same as scenario + 'technology': pd.Index(['solar', 'wind'], name='technology'), # length 2 - unique + } + + # 4x3x2 array - first and third dimensions are unique, middle is ambiguous + # This should still fail because middle dimension (length 3) could be scenario or region + arr_4x3x2 = np.random.rand(4, 3, 2) + + with pytest.raises(ConversionError, match='matches multiple dimension'): + DataConverter.to_dataarray(arr_4x3x2, coords=coords_partial) + + def test_pandas_series_ambiguous_dimensions(self): + """Test pandas Series with ambiguous dimension lengths.""" + coords_ambiguous = { + 'scenario': pd.Index(['A', 'B', 'C'], name='scenario'), # length 3 + 'region': pd.Index(['north', 'south', 'east'], name='region'), # length 3 + } + + # Series with length 3 but index that doesn't match either coordinate exactly + generic_series = pd.Series([10, 20, 30], index=[0, 1, 2]) + + # Should fail because length matches multiple dimensions and index doesn't match any + with pytest.raises(ConversionError, match='Series index does not match any target dimension coordinates'): + DataConverter.to_dataarray(generic_series, coords=coords_ambiguous) + + # Series with index that matches one of the ambiguous coordinates should work + scenario_series = pd.Series([10, 20, 30], index=coords_ambiguous['scenario']) + result = DataConverter.to_dataarray(scenario_series, coords=coords_ambiguous) + assert result.shape == (3, 3) # should broadcast to both dimensions + assert result.dims == ('scenario', 'region') + + def test_edge_case_many_same_lengths(self): + """Test edge case with many dimensions having the same length.""" + # Five dimensions all have length 2 + coords_many = { + 'dim1': pd.Index(['A', 'B'], name='dim1'), + 'dim2': pd.Index(['X', 'Y'], name='dim2'), + 'dim3': pd.Index(['P', 'Q'], name='dim3'), + 'dim4': pd.Index(['M', 'N'], name='dim4'), + 'dim5': pd.Index(['U', 'V'], name='dim5'), + } + + # 1D array + arr_1d = np.array([1, 2]) + with pytest.raises(ConversionError, match='matches multiple dimension'): + DataConverter.to_dataarray(arr_1d, coords=coords_many) + + # 2D array + arr_2d = np.random.rand(2, 2) + with pytest.raises(ConversionError, match='matches multiple dimension'): + DataConverter.to_dataarray(arr_2d, coords=coords_many) + + # 3D array + arr_3d = np.random.rand(2, 2, 2) + with pytest.raises(ConversionError, match='matches multiple dimension'): + DataConverter.to_dataarray(arr_3d, coords=coords_many) + + def test_mixed_lengths_with_duplicates(self): + """Test mixed scenario with some duplicate and some unique lengths.""" + coords_mixed = { + 'time': pd.date_range('2024-01-01', periods=8, freq='D', name='time'), # length 8 - unique + 'scenario': pd.Index(['A', 'B', 'C'], name='scenario'), # length 3 + 'region': pd.Index(['X', 'Y', 'Z'], name='region'), # length 3 - same as scenario + 'technology': pd.Index(['solar'], name='technology'), # length 1 - unique + 'product': pd.Index(['P1', 'P2', 'P3', 'P4', 'P5'], name='product'), # length 5 - unique + } + + # Arrays with unique lengths should work + arr_8 = np.arange(8) + result_8 = DataConverter.to_dataarray(arr_8, coords=coords_mixed) + assert result_8.dims == ('time', 'scenario', 'region', 'technology', 'product') + + arr_1 = np.array([42]) + result_1 = DataConverter.to_dataarray(arr_1, coords={'technology': coords_mixed['technology']}) + assert result_1.shape == (1,) + + arr_5 = np.arange(5) + result_5 = DataConverter.to_dataarray(arr_5, coords={'product': coords_mixed['product']}) + assert result_5.shape == (5,) + + # Arrays with ambiguous length should fail + arr_3 = np.array([1, 2, 3]) # matches both scenario and region + with pytest.raises(ConversionError, match='matches multiple dimension'): + DataConverter.to_dataarray(arr_3, coords=coords_mixed) + + def test_dataframe_with_ambiguous_dimensions(self): + """Test DataFrame handling with ambiguous dimensions.""" + coords_ambiguous = { + 'scenario': pd.Index(['A', 'B', 'C'], name='scenario'), # length 3 + 'region': pd.Index(['X', 'Y', 'Z'], name='region'), # length 3 + } + + # Multi-column DataFrame with ambiguous dimensions + df = pd.DataFrame({'col1': [1, 2, 3], 'col2': [4, 5, 6], 'col3': [7, 8, 9]}) # 3x3 DataFrame + + # Should fail due to ambiguous dimensions + with pytest.raises(ConversionError, match='matches multiple dimension'): + DataConverter.to_dataarray(df, coords=coords_ambiguous) + + def test_error_message_quality_for_ambiguous_dimensions(self): + """Test that error messages for ambiguous dimensions are helpful.""" + coords_ambiguous = { + 'scenario': pd.Index(['A', 'B', 'C'], name='scenario'), + 'region': pd.Index(['north', 'south', 'east'], name='region'), + 'technology': pd.Index(['solar', 'wind', 'gas'], name='technology'), + } + + # 1D array case + arr_1d = np.array([1, 2, 3]) + try: + DataConverter.to_dataarray(arr_1d, coords=coords_ambiguous) + raise AssertionError('Should have raised ConversionError') + except ConversionError as e: + error_msg = str(e) + assert 'matches multiple dimension' in error_msg + assert 'scenario' in error_msg + assert 'region' in error_msg + assert 'technology' in error_msg + + # 2D array case + arr_2d = np.random.rand(3, 3) + try: + DataConverter.to_dataarray(arr_2d, coords=coords_ambiguous) + raise AssertionError('Should have raised ConversionError') + except ConversionError as e: + error_msg = str(e) + assert 'matches multiple dimension combinations' in error_msg + assert '(3, 3)' in error_msg + + def test_ambiguous_with_broadcasting_target(self): + """Test ambiguous dimensions when target includes broadcasting.""" + coords_ambiguous_plus = { + 'time': pd.date_range('2024-01-01', periods=5, freq='D', name='time'), # length 5 + 'scenario': pd.Index(['A', 'B', 'C'], name='scenario'), # length 3 + 'region': pd.Index(['X', 'Y', 'Z'], name='region'), # length 3 - same as scenario + 'technology': pd.Index(['solar', 'wind'], name='technology'), # length 2 + } + + # 1D array with ambiguous length, but targeting broadcast scenario + arr_3 = np.array([10, 20, 30]) # length 3, matches scenario and region + + # Should fail even though it would broadcast to other dimensions + with pytest.raises(ConversionError, match='matches multiple dimension'): + DataConverter.to_dataarray(arr_3, coords=coords_ambiguous_plus) + + # 2D array with one ambiguous dimension + arr_5x3 = np.random.rand(5, 3) # 5 is unique (time), 3 is ambiguous (scenario/region) + + with pytest.raises(ConversionError, match='matches multiple dimension'): + DataConverter.to_dataarray(arr_5x3, coords=coords_ambiguous_plus) + + def test_time_dimension_ambiguity(self): + """Test ambiguity specifically involving time dimension.""" + # Create scenario where time has same length as another dimension + coords_time_ambiguous = { + 'time': pd.date_range('2024-01-01', periods=3, freq='D', name='time'), # length 3 + 'scenario': pd.Index(['base', 'high', 'low'], name='scenario'), # length 3 - same as time + 'region': pd.Index(['north', 'south'], name='region'), # length 2 - unique + } + + # Time-indexed series should work even with ambiguous lengths (index matching takes precedence) + time_series = pd.Series([100, 200, 300], index=coords_time_ambiguous['time']) + result = DataConverter.to_dataarray(time_series, coords=coords_time_ambiguous) + assert result.shape == (3, 3, 2) + assert result.dims == ('time', 'scenario', 'region') + + # But generic array with length 3 should still fail + generic_array = np.array([100, 200, 300]) + with pytest.raises(ConversionError, match='matches multiple dimension'): + DataConverter.to_dataarray(generic_array, coords=coords_time_ambiguous) if __name__ == '__main__': diff --git a/tests/test_effect.py b/tests/test_effect.py index b4a618ea6..cd3edc537 100644 --- a/tests/test_effect.py +++ b/tests/test_effect.py @@ -1,163 +1,342 @@ +import numpy as np +import xarray as xr + import flixopt as fx -from .conftest import assert_conequal, assert_var_equal, create_linopy_model +from .conftest import ( + assert_conequal, + assert_sets_equal, + assert_var_equal, + create_calculation_and_solve, + create_linopy_model, +) -class TestBusModel: +class TestEffectModel: """Test the FlowModel class.""" - def test_minimal(self, basic_flow_system_linopy): - flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + def test_minimal(self, basic_flow_system_linopy_coords, coords_config): + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config effect = fx.Effect('Effect1', '€', 'Testing Effect') flow_system.add_elements(effect) model = create_linopy_model(flow_system) - assert set(effect.model.variables) == { - 'Effect1(invest)|total', - 'Effect1(operation)|total', - 'Effect1(operation)|total_per_timestep', - 'Effect1|total', - } - assert set(effect.model.constraints) == { - 'Effect1(invest)|total', - 'Effect1(operation)|total', - 'Effect1(operation)|total_per_timestep', - 'Effect1|total', - } + assert_sets_equal( + set(effect.submodel.variables), + { + 'Effect1(periodic)', + 'Effect1(temporal)', + 'Effect1(temporal)|per_timestep', + 'Effect1', + }, + msg='Incorrect variables', + ) - assert_var_equal(model.variables['Effect1|total'], model.add_variables()) - assert_var_equal(model.variables['Effect1(invest)|total'], model.add_variables()) - assert_var_equal(model.variables['Effect1(operation)|total'], model.add_variables()) + assert_sets_equal( + set(effect.submodel.constraints), + { + 'Effect1(periodic)', + 'Effect1(temporal)', + 'Effect1(temporal)|per_timestep', + 'Effect1', + }, + msg='Incorrect constraints', + ) + + assert_var_equal( + model.variables['Effect1'], model.add_variables(coords=model.get_coords(['period', 'scenario'])) + ) + assert_var_equal( + model.variables['Effect1(periodic)'], model.add_variables(coords=model.get_coords(['period', 'scenario'])) + ) + assert_var_equal( + model.variables['Effect1(temporal)'], + model.add_variables(coords=model.get_coords(['period', 'scenario'])), + ) assert_var_equal( - model.variables['Effect1(operation)|total_per_timestep'], model.add_variables(coords=(timesteps,)) + model.variables['Effect1(temporal)|per_timestep'], model.add_variables(coords=model.get_coords()) ) assert_conequal( - model.constraints['Effect1|total'], - model.variables['Effect1|total'] - == model.variables['Effect1(operation)|total'] + model.variables['Effect1(invest)|total'], + model.constraints['Effect1'], + model.variables['Effect1'] == model.variables['Effect1(temporal)'] + model.variables['Effect1(periodic)'], ) - assert_conequal(model.constraints['Effect1(invest)|total'], model.variables['Effect1(invest)|total'] == 0) + # In minimal/bounds tests with no contributing components, periodic totals should be zero + assert_conequal(model.constraints['Effect1(periodic)'], model.variables['Effect1(periodic)'] == 0) assert_conequal( - model.constraints['Effect1(operation)|total'], - model.variables['Effect1(operation)|total'] - == model.variables['Effect1(operation)|total_per_timestep'].sum(), + model.constraints['Effect1(temporal)'], + model.variables['Effect1(temporal)'] == model.variables['Effect1(temporal)|per_timestep'].sum('time'), ) assert_conequal( - model.constraints['Effect1(operation)|total_per_timestep'], - model.variables['Effect1(operation)|total_per_timestep'] == 0, + model.constraints['Effect1(temporal)|per_timestep'], + model.variables['Effect1(temporal)|per_timestep'] == 0, ) - def test_bounds(self, basic_flow_system_linopy): - flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + def test_bounds(self, basic_flow_system_linopy_coords, coords_config): + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config effect = fx.Effect( 'Effect1', '€', 'Testing Effect', - minimum_operation=1.0, - maximum_operation=1.1, - minimum_invest=2.0, - maximum_invest=2.1, + minimum_temporal=1.0, + maximum_temporal=1.1, + minimum_periodic=2.0, + maximum_periodic=2.1, minimum_total=3.0, maximum_total=3.1, - minimum_operation_per_hour=4.0, - maximum_operation_per_hour=4.1, + minimum_per_hour=4.0, + maximum_per_hour=4.1, ) flow_system.add_elements(effect) model = create_linopy_model(flow_system) - assert set(effect.model.variables) == { - 'Effect1(invest)|total', - 'Effect1(operation)|total', - 'Effect1(operation)|total_per_timestep', - 'Effect1|total', - } - assert set(effect.model.constraints) == { - 'Effect1(invest)|total', - 'Effect1(operation)|total', - 'Effect1(operation)|total_per_timestep', - 'Effect1|total', - } + assert_sets_equal( + set(effect.submodel.variables), + { + 'Effect1(periodic)', + 'Effect1(temporal)', + 'Effect1(temporal)|per_timestep', + 'Effect1', + }, + msg='Incorrect variables', + ) + + assert_sets_equal( + set(effect.submodel.constraints), + { + 'Effect1(periodic)', + 'Effect1(temporal)', + 'Effect1(temporal)|per_timestep', + 'Effect1', + }, + msg='Incorrect constraints', + ) - assert_var_equal(model.variables['Effect1|total'], model.add_variables(lower=3.0, upper=3.1)) - assert_var_equal(model.variables['Effect1(invest)|total'], model.add_variables(lower=2.0, upper=2.1)) - assert_var_equal(model.variables['Effect1(operation)|total'], model.add_variables(lower=1.0, upper=1.1)) assert_var_equal( - model.variables['Effect1(operation)|total_per_timestep'], + model.variables['Effect1'], + model.add_variables(lower=3.0, upper=3.1, coords=model.get_coords(['period', 'scenario'])), + ) + assert_var_equal( + model.variables['Effect1(periodic)'], + model.add_variables(lower=2.0, upper=2.1, coords=model.get_coords(['period', 'scenario'])), + ) + assert_var_equal( + model.variables['Effect1(temporal)'], + model.add_variables(lower=1.0, upper=1.1, coords=model.get_coords(['period', 'scenario'])), + ) + assert_var_equal( + model.variables['Effect1(temporal)|per_timestep'], model.add_variables( - lower=4.0 * model.hours_per_step, upper=4.1 * model.hours_per_step, coords=(timesteps,) + lower=4.0 * model.hours_per_step, + upper=4.1 * model.hours_per_step, + coords=model.get_coords(['time', 'period', 'scenario']), ), ) assert_conequal( - model.constraints['Effect1|total'], - model.variables['Effect1|total'] - == model.variables['Effect1(operation)|total'] + model.variables['Effect1(invest)|total'], + model.constraints['Effect1'], + model.variables['Effect1'] == model.variables['Effect1(temporal)'] + model.variables['Effect1(periodic)'], ) - assert_conequal(model.constraints['Effect1(invest)|total'], model.variables['Effect1(invest)|total'] == 0) + # In minimal/bounds tests with no contributing components, periodic totals should be zero + assert_conequal(model.constraints['Effect1(periodic)'], model.variables['Effect1(periodic)'] == 0) assert_conequal( - model.constraints['Effect1(operation)|total'], - model.variables['Effect1(operation)|total'] - == model.variables['Effect1(operation)|total_per_timestep'].sum(), + model.constraints['Effect1(temporal)'], + model.variables['Effect1(temporal)'] == model.variables['Effect1(temporal)|per_timestep'].sum('time'), ) assert_conequal( - model.constraints['Effect1(operation)|total_per_timestep'], - model.variables['Effect1(operation)|total_per_timestep'] == 0, + model.constraints['Effect1(temporal)|per_timestep'], + model.variables['Effect1(temporal)|per_timestep'] == 0, ) - def test_shares(self, basic_flow_system_linopy): - flow_system = basic_flow_system_linopy + def test_shares(self, basic_flow_system_linopy_coords, coords_config): + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config effect1 = fx.Effect( 'Effect1', '€', 'Testing Effect', - specific_share_to_other_effects_operation={'Effect2': 1.1, 'Effect3': 1.2}, - specific_share_to_other_effects_invest={'Effect2': 2.1, 'Effect3': 2.2}, ) - effect2 = fx.Effect('Effect2', '€', 'Testing Effect') - effect3 = fx.Effect('Effect3', '€', 'Testing Effect') + effect2 = fx.Effect( + 'Effect2', + '€', + 'Testing Effect', + share_from_temporal={'Effect1': 1.1}, + share_from_periodic={'Effect1': 2.1}, + ) + effect3 = fx.Effect( + 'Effect3', + '€', + 'Testing Effect', + share_from_temporal={'Effect1': 1.2}, + share_from_periodic={'Effect1': 2.2}, + ) flow_system.add_elements(effect1, effect2, effect3) model = create_linopy_model(flow_system) - assert set(effect2.model.variables) == { - 'Effect2(invest)|total', - 'Effect2(operation)|total', - 'Effect2(operation)|total_per_timestep', - 'Effect2|total', - 'Effect1(invest)->Effect2(invest)', - 'Effect1(operation)->Effect2(operation)', - } - assert set(effect2.model.constraints) == { - 'Effect2(invest)|total', - 'Effect2(operation)|total', - 'Effect2(operation)|total_per_timestep', - 'Effect2|total', - 'Effect1(invest)->Effect2(invest)', - 'Effect1(operation)->Effect2(operation)', - } + assert_sets_equal( + set(effect2.submodel.variables), + { + 'Effect2(periodic)', + 'Effect2(temporal)', + 'Effect2(temporal)|per_timestep', + 'Effect2', + 'Effect1(periodic)->Effect2(periodic)', + 'Effect1(temporal)->Effect2(temporal)', + }, + msg='Incorrect variables for effect2', + ) + + assert_sets_equal( + set(effect2.submodel.constraints), + { + 'Effect2(periodic)', + 'Effect2(temporal)', + 'Effect2(temporal)|per_timestep', + 'Effect2', + 'Effect1(periodic)->Effect2(periodic)', + 'Effect1(temporal)->Effect2(temporal)', + }, + msg='Incorrect constraints for effect2', + ) assert_conequal( - model.constraints['Effect2(invest)|total'], - model.variables['Effect2(invest)|total'] == model.variables['Effect1(invest)->Effect2(invest)'], + model.constraints['Effect2(periodic)'], + model.variables['Effect2(periodic)'] == model.variables['Effect1(periodic)->Effect2(periodic)'], ) assert_conequal( - model.constraints['Effect2(operation)|total_per_timestep'], - model.variables['Effect2(operation)|total_per_timestep'] - == model.variables['Effect1(operation)->Effect2(operation)'], + model.constraints['Effect2(temporal)|per_timestep'], + model.variables['Effect2(temporal)|per_timestep'] + == model.variables['Effect1(temporal)->Effect2(temporal)'], ) assert_conequal( - model.constraints['Effect1(operation)->Effect2(operation)'], - model.variables['Effect1(operation)->Effect2(operation)'] - == model.variables['Effect1(operation)|total_per_timestep'] * 1.1, + model.constraints['Effect1(temporal)->Effect2(temporal)'], + model.variables['Effect1(temporal)->Effect2(temporal)'] + == model.variables['Effect1(temporal)|per_timestep'] * 1.1, ) assert_conequal( - model.constraints['Effect1(invest)->Effect2(invest)'], - model.variables['Effect1(invest)->Effect2(invest)'] == model.variables['Effect1(invest)|total'] * 2.1, + model.constraints['Effect1(periodic)->Effect2(periodic)'], + model.variables['Effect1(periodic)->Effect2(periodic)'] == model.variables['Effect1(periodic)'] * 2.1, + ) + + +class TestEffectResults: + def test_shares(self, basic_flow_system_linopy_coords, coords_config): + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config + effect1 = fx.Effect('Effect1', '€', 'Testing Effect', share_from_temporal={'costs': 0.5}) + effect2 = fx.Effect( + 'Effect2', + '€', + 'Testing Effect', + share_from_temporal={'Effect1': 1.1}, + share_from_periodic={'Effect1': 2.1}, + ) + effect3 = fx.Effect( + 'Effect3', + '€', + 'Testing Effect', + share_from_temporal={'Effect1': 1.2, 'Effect2': 5}, + share_from_periodic={'Effect1': 2.2}, + ) + flow_system.add_elements( + effect1, + effect2, + effect3, + fx.linear_converters.Boiler( + 'Boiler', + eta=0.5, + Q_th=fx.Flow( + 'Q_th', + bus='Fernwärme', + size=fx.InvestParameters(effects_of_investment_per_size=10, minimum_size=20, mandatory=True), + ), + Q_fu=fx.Flow('Q_fu', bus='Gas'), + ), + ) + + results = create_calculation_and_solve(flow_system, fx.solvers.HighsSolver(0.01, 60), 'Sim1').results + + effect_share_factors = { + 'temporal': { + ('costs', 'Effect1'): 0.5, + ('costs', 'Effect2'): 0.5 * 1.1, + ('costs', 'Effect3'): 0.5 * 1.1 * 5 + 0.5 * 1.2, # This is where the issue lies + ('Effect1', 'Effect2'): 1.1, + ('Effect1', 'Effect3'): 1.2 + 1.1 * 5, + ('Effect2', 'Effect3'): 5, + }, + 'periodic': { + ('Effect1', 'Effect2'): 2.1, + ('Effect1', 'Effect3'): 2.2, + }, + } + for key, value in effect_share_factors['temporal'].items(): + np.testing.assert_allclose(results.effect_share_factors['temporal'][key].values, value) + + for key, value in effect_share_factors['periodic'].items(): + np.testing.assert_allclose(results.effect_share_factors['periodic'][key].values, value) + + xr.testing.assert_allclose( + results.effects_per_component['temporal'].sum('component').sel(effect='costs', drop=True), + results.solution['costs(temporal)|per_timestep'].fillna(0), + ) + + xr.testing.assert_allclose( + results.effects_per_component['temporal'].sum('component').sel(effect='Effect1', drop=True), + results.solution['Effect1(temporal)|per_timestep'].fillna(0), + ) + + xr.testing.assert_allclose( + results.effects_per_component['temporal'].sum('component').sel(effect='Effect2', drop=True), + results.solution['Effect2(temporal)|per_timestep'].fillna(0), + ) + + xr.testing.assert_allclose( + results.effects_per_component['temporal'].sum('component').sel(effect='Effect3', drop=True), + results.solution['Effect3(temporal)|per_timestep'].fillna(0), + ) + + # periodic mode checks + xr.testing.assert_allclose( + results.effects_per_component['periodic'].sum('component').sel(effect='costs', drop=True), + results.solution['costs(periodic)'], + ) + + xr.testing.assert_allclose( + results.effects_per_component['periodic'].sum('component').sel(effect='Effect1', drop=True), + results.solution['Effect1(periodic)'], + ) + + xr.testing.assert_allclose( + results.effects_per_component['periodic'].sum('component').sel(effect='Effect2', drop=True), + results.solution['Effect2(periodic)'], + ) + + xr.testing.assert_allclose( + results.effects_per_component['periodic'].sum('component').sel(effect='Effect3', drop=True), + results.solution['Effect3(periodic)'], + ) + + # Total mode checks + xr.testing.assert_allclose( + results.effects_per_component['total'].sum('component').sel(effect='costs', drop=True), + results.solution['costs'], + ) + + xr.testing.assert_allclose( + results.effects_per_component['total'].sum('component').sel(effect='Effect1', drop=True), + results.solution['Effect1'], + ) + + xr.testing.assert_allclose( + results.effects_per_component['total'].sum('component').sel(effect='Effect2', drop=True), + results.solution['Effect2'], + ) + + xr.testing.assert_allclose( + results.effects_per_component['total'].sum('component').sel(effect='Effect3', drop=True), + results.solution['Effect3'], ) diff --git a/tests/test_effects_shares_summation.py b/tests/test_effects_shares_summation.py new file mode 100644 index 000000000..312934732 --- /dev/null +++ b/tests/test_effects_shares_summation.py @@ -0,0 +1,225 @@ +import pytest +import xarray as xr + +from flixopt.effects import calculate_all_conversion_paths + + +def test_direct_conversions(): + """Test direct conversions with simple scalar values.""" + conversion_dict = {'A': {'B': xr.DataArray(2.0)}, 'B': {'C': xr.DataArray(3.0)}} + + result = calculate_all_conversion_paths(conversion_dict) + + # Check direct conversions + assert ('A', 'B') in result + assert ('B', 'C') in result + assert result[('A', 'B')].item() == 2.0 + assert result[('B', 'C')].item() == 3.0 + + # Check indirect conversion + assert ('A', 'C') in result + assert result[('A', 'C')].item() == 6.0 # 2.0 * 3.0 + + +def test_multiple_paths(): + """Test multiple paths between nodes that should be summed.""" + conversion_dict = { + 'A': {'B': xr.DataArray(2.0), 'C': xr.DataArray(3.0)}, + 'B': {'D': xr.DataArray(4.0)}, + 'C': {'D': xr.DataArray(5.0)}, + } + + result = calculate_all_conversion_paths(conversion_dict) + + # A to D should sum two paths: A->B->D (2*4=8) and A->C->D (3*5=15) + assert ('A', 'D') in result + assert result[('A', 'D')].item() == 8.0 + 15.0 + + +def test_xarray_conversions(): + """Test with xarray DataArrays that have dimensions.""" + # Create DataArrays with a time dimension + time_points = [1, 2, 3] + a_to_b = xr.DataArray([2.0, 2.1, 2.2], dims='time', coords={'time': time_points}) + b_to_c = xr.DataArray([3.0, 3.1, 3.2], dims='time', coords={'time': time_points}) + + conversion_dict = {'A': {'B': a_to_b}, 'B': {'C': b_to_c}} + + result = calculate_all_conversion_paths(conversion_dict) + + # Check indirect conversion preserves dimensions + assert ('A', 'C') in result + assert result[('A', 'C')].dims == ('time',) + + # Check values at each time point + for i, t in enumerate(time_points): + expected = a_to_b.values[i] * b_to_c.values[i] + assert pytest.approx(result[('A', 'C')].sel(time=t).item()) == expected + + +def test_long_paths(): + """Test with longer paths (more than one intermediate node).""" + conversion_dict = { + 'A': {'B': xr.DataArray(2.0)}, + 'B': {'C': xr.DataArray(3.0)}, + 'C': {'D': xr.DataArray(4.0)}, + 'D': {'E': xr.DataArray(5.0)}, + } + + result = calculate_all_conversion_paths(conversion_dict) + + # Check the full path A->B->C->D->E + assert ('A', 'E') in result + expected = 2.0 * 3.0 * 4.0 * 5.0 # 120.0 + assert result[('A', 'E')].item() == expected + + +def test_diamond_paths(): + """Test with a diamond shape graph with multiple paths to the same destination.""" + conversion_dict = { + 'A': {'B': xr.DataArray(2.0), 'C': xr.DataArray(3.0)}, + 'B': {'D': xr.DataArray(4.0)}, + 'C': {'D': xr.DataArray(5.0)}, + 'D': {'E': xr.DataArray(6.0)}, + } + + result = calculate_all_conversion_paths(conversion_dict) + + # A to E should go through both paths: + # A->B->D->E (2*4*6=48) and A->C->D->E (3*5*6=90) + assert ('A', 'E') in result + expected = 48.0 + 90.0 # 138.0 + assert result[('A', 'E')].item() == expected + + +def test_effect_shares_example(): + """Test the specific example from the effects share factors test.""" + # Create the conversion dictionary based on test example + conversion_dict = { + 'costs': {'Effect1': xr.DataArray(0.5)}, + 'Effect1': {'Effect2': xr.DataArray(1.1), 'Effect3': xr.DataArray(1.2)}, + 'Effect2': {'Effect3': xr.DataArray(5.0)}, + } + + result = calculate_all_conversion_paths(conversion_dict) + + # Test direct paths + assert result[('costs', 'Effect1')].item() == 0.5 + assert result[('Effect1', 'Effect2')].item() == 1.1 + assert result[('Effect2', 'Effect3')].item() == 5.0 + + # Test indirect paths + # costs -> Effect2 = costs -> Effect1 -> Effect2 = 0.5 * 1.1 + assert result[('costs', 'Effect2')].item() == 0.5 * 1.1 + + # costs -> Effect3 has two paths: + # 1. costs -> Effect1 -> Effect3 = 0.5 * 1.2 = 0.6 + # 2. costs -> Effect1 -> Effect2 -> Effect3 = 0.5 * 1.1 * 5 = 2.75 + # Total = 0.6 + 2.75 = 3.35 + assert result[('costs', 'Effect3')].item() == 0.5 * 1.2 + 0.5 * 1.1 * 5 + + # Effect1 -> Effect3 has two paths: + # 1. Effect1 -> Effect2 -> Effect3 = 1.1 * 5.0 = 5.5 + # 2. Effect1 -> Effect3 = 1.2 + # Total = 0.6 + 2.75 = 3.35 + assert result[('Effect1', 'Effect3')].item() == 1.2 + 1.1 * 5.0 + + +def test_empty_conversion_dict(): + """Test with an empty conversion dictionary.""" + result = calculate_all_conversion_paths({}) + assert len(result) == 0 + + +def test_no_indirect_paths(): + """Test with a dictionary that has no indirect paths.""" + conversion_dict = {'A': {'B': xr.DataArray(2.0)}, 'C': {'D': xr.DataArray(3.0)}} + + result = calculate_all_conversion_paths(conversion_dict) + + # Only direct paths should exist + assert len(result) == 2 + assert ('A', 'B') in result + assert ('C', 'D') in result + assert result[('A', 'B')].item() == 2.0 + assert result[('C', 'D')].item() == 3.0 + + +def test_complex_network(): + """Test with a complex network of many nodes and multiple paths, without circular references.""" + # Create a directed acyclic graph with many nodes + # Structure resembles a layered network with multiple paths + conversion_dict = { + 'A': {'B': xr.DataArray(1.5), 'C': xr.DataArray(2.0), 'D': xr.DataArray(0.5)}, + 'B': {'E': xr.DataArray(3.0), 'F': xr.DataArray(1.2)}, + 'C': {'E': xr.DataArray(0.8), 'G': xr.DataArray(2.5)}, + 'D': {'G': xr.DataArray(1.8), 'H': xr.DataArray(3.2)}, + 'E': {'I': xr.DataArray(0.7), 'J': xr.DataArray(1.4)}, + 'F': {'J': xr.DataArray(2.2), 'K': xr.DataArray(0.9)}, + 'G': {'K': xr.DataArray(1.6), 'L': xr.DataArray(2.8)}, + 'H': {'L': xr.DataArray(0.4), 'M': xr.DataArray(1.1)}, + 'I': {'N': xr.DataArray(2.3)}, + 'J': {'N': xr.DataArray(1.9), 'O': xr.DataArray(0.6)}, + 'K': {'O': xr.DataArray(3.5), 'P': xr.DataArray(1.3)}, + 'L': {'P': xr.DataArray(2.7), 'Q': xr.DataArray(0.8)}, + 'M': {'Q': xr.DataArray(2.1)}, + 'N': {'R': xr.DataArray(1.7)}, + 'O': {'R': xr.DataArray(2.9), 'S': xr.DataArray(1.0)}, + 'P': {'S': xr.DataArray(2.4)}, + 'Q': {'S': xr.DataArray(1.5)}, + } + + result = calculate_all_conversion_paths(conversion_dict) + + # Check some direct paths + assert result[('A', 'B')].item() == 1.5 + assert result[('D', 'H')].item() == 3.2 + assert result[('G', 'L')].item() == 2.8 + + # Check some two-step paths + assert result[('A', 'E')].item() == 1.5 * 3.0 + 2.0 * 0.8 # A->B->E + A->C->E + assert result[('B', 'J')].item() == 3.0 * 1.4 + 1.2 * 2.2 # B->E->J + B->F->J + + # Check some three-step paths + # A->B->E->I + # A->C->E->I + expected_a_to_i = 1.5 * 3.0 * 0.7 + 2.0 * 0.8 * 0.7 + assert pytest.approx(result[('A', 'I')].item()) == expected_a_to_i + + # Check some four-step paths + # A->B->E->I->N + # A->C->E->I->N + expected_a_to_n = 1.5 * 3.0 * 0.7 * 2.3 + 2.0 * 0.8 * 0.7 * 2.3 + expected_a_to_n += 1.5 * 3.0 * 1.4 * 1.9 + 2.0 * 0.8 * 1.4 * 1.9 # A->B->E->J->N + A->C->E->J->N + expected_a_to_n += 1.5 * 1.2 * 2.2 * 1.9 # A->B->F->J->N + assert pytest.approx(result[('A', 'N')].item()) == expected_a_to_n + + # Check a very long path from A to S + # This should include: + # A->B->E->J->O->S + # A->B->F->K->O->S + # A->C->E->J->O->S + # A->C->G->K->O->S + # A->D->G->K->O->S + # A->D->H->L->P->S + # A->D->H->M->Q->S + # And many more + assert ('A', 'S') in result + + # There are many paths to R from A - check their existence + assert ('A', 'R') in result + + # Check that there's no direct path from A to R + # But there should be indirect paths + assert ('A', 'R') in result + assert 'A' not in conversion_dict.get('R', {}) + + # Count the number of paths calculated to verify algorithm explored all connections + # In a DAG with 19 nodes (A through S), the maximum number of pairs is 19*18 = 342 + # But we won't have all possible connections due to the structure + # Just verify we have a reasonable number + assert len(result) > 50 + + +if __name__ == '__main__': + pytest.main() diff --git a/tests/test_examples.py b/tests/test_examples.py index f29ea66b6..eca79d7c7 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -1,6 +1,7 @@ import os import subprocess import sys +from contextlib import contextmanager from pathlib import Path import pytest @@ -8,42 +9,78 @@ # Path to the examples directory EXAMPLES_DIR = Path(__file__).parent.parent / 'examples' +# Examples that have dependencies and must run in sequence +DEPENDENT_EXAMPLES = ( + '02_Complex/complex_example.py', + '02_Complex/complex_example_results.py', +) + + +@contextmanager +def working_directory(path): + """Context manager for changing the working directory.""" + original_cwd = os.getcwd() + try: + os.chdir(path) + yield + finally: + os.chdir(original_cwd) + @pytest.mark.parametrize( 'example_script', sorted( - EXAMPLES_DIR.rglob('*.py'), key=lambda path: (str(path.parent), path.name) - ), # Sort by parent and script name - ids=lambda path: str(path.relative_to(EXAMPLES_DIR)), # Show relative file paths + [p for p in EXAMPLES_DIR.rglob('*.py') if str(p.relative_to(EXAMPLES_DIR)) not in DEPENDENT_EXAMPLES], + key=lambda path: (str(path.parent), path.name), + ), + ids=lambda path: str(path.relative_to(EXAMPLES_DIR)).replace(os.sep, '/'), ) @pytest.mark.examples -def test_example_scripts(example_script): +def test_independent_examples(example_script): """ - Test all example scripts in the examples directory. + Test independent example scripts. Ensures they run without errors. Changes the current working directory to the directory of the example script. Runs them alphabetically. - This imitates behaviour of running the script directly + This imitates behaviour of running the script directly. """ - script_dir = example_script.parent - original_cwd = os.getcwd() + with working_directory(example_script.parent): + timeout = 600 + try: + result = subprocess.run( + [sys.executable, example_script.name], + capture_output=True, + text=True, + timeout=timeout, + ) + except subprocess.TimeoutExpired: + pytest.fail(f'Script {example_script} timed out after {timeout} seconds') - try: - # Change the working directory to the script's location - os.chdir(script_dir) - - # Run the script - result = subprocess.run( - [sys.executable, example_script.name], - capture_output=True, - text=True, + assert result.returncode == 0, ( + f'Script {example_script} failed:\nSTDERR:\n{result.stderr}\nSTDOUT:\n{result.stdout}' ) - assert result.returncode == 0, f'Script {example_script} failed:\n{result.stderr}' - finally: - # Restore the original working directory - os.chdir(original_cwd) + +@pytest.mark.examples +def test_dependent_examples(): + """Test examples that must run in order (complex_example.py generates data for complex_example_results.py).""" + for script_path in DEPENDENT_EXAMPLES: + script_full_path = EXAMPLES_DIR / script_path + + with working_directory(script_full_path.parent): + timeout = 600 + try: + result = subprocess.run( + [sys.executable, script_full_path.name], + capture_output=True, + text=True, + timeout=timeout, + ) + except subprocess.TimeoutExpired: + pytest.fail(f'Script {script_path} timed out after {timeout} seconds') + + assert result.returncode == 0, f'{script_path} failed:\nSTDERR:\n{result.stderr}\nSTDOUT:\n{result.stdout}' if __name__ == '__main__': - pytest.main(['-v', '--disable-warnings']) + pytest.main(['-v', '--disable-warnings', '-m', 'examples']) diff --git a/tests/test_flow.py b/tests/test_flow.py index f7c5d8a69..8a011939f 100644 --- a/tests/test_flow.py +++ b/tests/test_flow.py @@ -4,36 +4,44 @@ import flixopt as fx -from .conftest import assert_conequal, assert_var_equal, create_linopy_model +from .conftest import assert_conequal, assert_sets_equal, assert_var_equal, create_linopy_model class TestFlowModel: """Test the FlowModel class.""" - def test_flow_minimal(self, basic_flow_system_linopy): + def test_flow_minimal(self, basic_flow_system_linopy_coords, coords_config): """Test that flow model constraints are correctly generated.""" - flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config + flow = fx.Flow('Wärme', bus='Fernwärme', size=100) - flow_system.add_elements(fx.Sink('Sink', sink=flow)) + flow_system.add_elements(fx.Sink('Sink', inputs=[flow])) model = create_linopy_model(flow_system) assert_conequal( model.constraints['Sink(Wärme)|total_flow_hours'], - flow.model.variables['Sink(Wärme)|total_flow_hours'] - == (flow.model.variables['Sink(Wärme)|flow_rate'] * model.hours_per_step).sum(), + flow.submodel.variables['Sink(Wärme)|total_flow_hours'] + == (flow.submodel.variables['Sink(Wärme)|flow_rate'] * model.hours_per_step).sum('time'), + ) + assert_var_equal(flow.submodel.flow_rate, model.add_variables(lower=0, upper=100, coords=model.get_coords())) + assert_var_equal( + flow.submodel.total_flow_hours, + model.add_variables(lower=0, coords=model.get_coords(['period', 'scenario'])), + ) + + assert_sets_equal( + set(flow.submodel.variables), + {'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate'}, + msg='Incorrect variables', ) - assert_var_equal(flow.model.flow_rate, model.add_variables(lower=0, upper=100, coords=(timesteps,))) - assert_var_equal(flow.model.total_flow_hours, model.add_variables(lower=0)) + assert_sets_equal(set(flow.submodel.constraints), {'Sink(Wärme)|total_flow_hours'}, msg='Incorrect constraints') - assert set(flow.model.variables) == set(['Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate']) - assert set(flow.model.constraints) == set(['Sink(Wärme)|total_flow_hours']) + def test_flow(self, basic_flow_system_linopy_coords, coords_config): + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config + timesteps = flow_system.timesteps - def test_flow(self, basic_flow_system_linopy): - flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps flow = fx.Flow( 'Wärme', bus='Fernwärme', @@ -46,342 +54,400 @@ def test_flow(self, basic_flow_system_linopy): load_factor_max=0.9, ) - flow_system.add_elements(fx.Sink('Sink', sink=flow)) + flow_system.add_elements(fx.Sink('Sink', inputs=[flow])) model = create_linopy_model(flow_system) # total_flow_hours assert_conequal( model.constraints['Sink(Wärme)|total_flow_hours'], - flow.model.variables['Sink(Wärme)|total_flow_hours'] - == (flow.model.variables['Sink(Wärme)|flow_rate'] * model.hours_per_step).sum(), + flow.submodel.variables['Sink(Wärme)|total_flow_hours'] + == (flow.submodel.variables['Sink(Wärme)|flow_rate'] * model.hours_per_step).sum('time'), ) - assert_var_equal(flow.model.total_flow_hours, model.add_variables(lower=10, upper=1000)) + assert_var_equal( + flow.submodel.total_flow_hours, + model.add_variables(lower=10, upper=1000, coords=model.get_coords(['period', 'scenario'])), + ) + + assert flow.relative_minimum.dims == tuple(model.get_coords()) + assert flow.relative_maximum.dims == tuple(model.get_coords()) assert_var_equal( - flow.model.flow_rate, + flow.submodel.flow_rate, model.add_variables( - lower=np.linspace(0, 0.5, timesteps.size) * 100, - upper=np.linspace(0.5, 1, timesteps.size) * 100, - coords=(timesteps,), + lower=flow.relative_minimum * 100, + upper=flow.relative_maximum * 100, + coords=model.get_coords(), ), ) assert_conequal( model.constraints['Sink(Wärme)|load_factor_min'], - flow.model.variables['Sink(Wärme)|total_flow_hours'] >= model.hours_per_step.sum('time') * 0.1 * 100, + flow.submodel.variables['Sink(Wärme)|total_flow_hours'] >= model.hours_per_step.sum('time') * 0.1 * 100, ) assert_conequal( model.constraints['Sink(Wärme)|load_factor_max'], - flow.model.variables['Sink(Wärme)|total_flow_hours'] <= model.hours_per_step.sum('time') * 0.9 * 100, + flow.submodel.variables['Sink(Wärme)|total_flow_hours'] <= model.hours_per_step.sum('time') * 0.9 * 100, ) - assert set(flow.model.variables) == set(['Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate']) - assert set(flow.model.constraints) == set( - ['Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|load_factor_max', 'Sink(Wärme)|load_factor_min'] + assert_sets_equal( + set(flow.submodel.variables), + {'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate'}, + msg='Incorrect variables', + ) + assert_sets_equal( + set(flow.submodel.constraints), + {'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|load_factor_max', 'Sink(Wärme)|load_factor_min'}, + msg='Incorrect constraints', ) - def test_effects_per_flow_hour(self, basic_flow_system_linopy): - flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + def test_effects_per_flow_hour(self, basic_flow_system_linopy_coords, coords_config): + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config + timesteps = flow_system.timesteps costs_per_flow_hour = xr.DataArray(np.linspace(1, 2, timesteps.size), coords=(timesteps,)) co2_per_flow_hour = xr.DataArray(np.linspace(4, 5, timesteps.size), coords=(timesteps,)) flow = fx.Flow( - 'Wärme', bus='Fernwärme', effects_per_flow_hour={'Costs': costs_per_flow_hour, 'CO2': co2_per_flow_hour} + 'Wärme', bus='Fernwärme', effects_per_flow_hour={'costs': costs_per_flow_hour, 'CO2': co2_per_flow_hour} ) - flow_system.add_elements(fx.Sink('Sink', sink=flow), fx.Effect('CO2', 't', '')) + flow_system.add_elements(fx.Sink('Sink', inputs=[flow]), fx.Effect('CO2', 't', '')) model = create_linopy_model(flow_system) - costs, co2 = flow_system.effects['Costs'], flow_system.effects['CO2'] + costs, co2 = flow_system.effects['costs'], flow_system.effects['CO2'] - assert set(flow.model.variables) == {'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate'} - assert set(flow.model.constraints) == {'Sink(Wärme)|total_flow_hours'} + assert_sets_equal( + set(flow.submodel.variables), + {'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate'}, + msg='Incorrect variables', + ) + assert_sets_equal(set(flow.submodel.constraints), {'Sink(Wärme)|total_flow_hours'}, msg='Incorrect constraints') - assert 'Sink(Wärme)->Costs(operation)' in set(costs.model.constraints) - assert 'Sink(Wärme)->CO2(operation)' in set(co2.model.constraints) + assert 'Sink(Wärme)->costs(temporal)' in set(costs.submodel.constraints) + assert 'Sink(Wärme)->CO2(temporal)' in set(co2.submodel.constraints) assert_conequal( - model.constraints['Sink(Wärme)->Costs(operation)'], - model.variables['Sink(Wärme)->Costs(operation)'] - == flow.model.variables['Sink(Wärme)|flow_rate'] * model.hours_per_step * costs_per_flow_hour, + model.constraints['Sink(Wärme)->costs(temporal)'], + model.variables['Sink(Wärme)->costs(temporal)'] + == flow.submodel.variables['Sink(Wärme)|flow_rate'] * model.hours_per_step * costs_per_flow_hour, ) assert_conequal( - model.constraints['Sink(Wärme)->CO2(operation)'], - model.variables['Sink(Wärme)->CO2(operation)'] - == flow.model.variables['Sink(Wärme)|flow_rate'] * model.hours_per_step * co2_per_flow_hour, + model.constraints['Sink(Wärme)->CO2(temporal)'], + model.variables['Sink(Wärme)->CO2(temporal)'] + == flow.submodel.variables['Sink(Wärme)|flow_rate'] * model.hours_per_step * co2_per_flow_hour, ) class TestFlowInvestModel: """Test the FlowModel class.""" - def test_flow_invest(self, basic_flow_system_linopy): - flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + def test_flow_invest(self, basic_flow_system_linopy_coords, coords_config): + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config + timesteps = flow_system.timesteps flow = fx.Flow( 'Wärme', bus='Fernwärme', - size=fx.InvestParameters(minimum_size=20, maximum_size=100, optional=False), + size=fx.InvestParameters(minimum_size=20, maximum_size=100, mandatory=True), relative_minimum=np.linspace(0.1, 0.5, timesteps.size), relative_maximum=np.linspace(0.5, 1, timesteps.size), ) - flow_system.add_elements(fx.Sink('Sink', sink=flow)) + flow_system.add_elements(fx.Sink('Sink', inputs=[flow])) model = create_linopy_model(flow_system) - assert set(flow.model.variables) == set( - [ + assert_sets_equal( + set(flow.submodel.variables), + { 'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate', 'Sink(Wärme)|size', - ] + }, + msg='Incorrect variables', ) - assert set(flow.model.constraints) == set( - [ + assert_sets_equal( + set(flow.submodel.constraints), + { 'Sink(Wärme)|total_flow_hours', - 'Sink(Wärme)|lb_Sink(Wärme)|flow_rate', - 'Sink(Wärme)|ub_Sink(Wärme)|flow_rate', - ] + 'Sink(Wärme)|flow_rate|ub', + 'Sink(Wärme)|flow_rate|lb', + }, + msg='Incorrect constraints', ) # size - assert_var_equal(model['Sink(Wärme)|size'], model.add_variables(lower=20, upper=100)) + assert_var_equal( + model['Sink(Wärme)|size'], + model.add_variables(lower=20, upper=100, coords=model.get_coords(['period', 'scenario'])), + ) + + assert flow.relative_minimum.dims == tuple(model.get_coords()) + assert flow.relative_maximum.dims == tuple(model.get_coords()) # flow_rate assert_var_equal( - flow.model.flow_rate, + flow.submodel.flow_rate, model.add_variables( - lower=np.linspace(0.1, 0.5, timesteps.size) * 20, - upper=np.linspace(0.5, 1, timesteps.size) * 100, - coords=(timesteps,), + lower=flow.relative_minimum * 20, + upper=flow.relative_maximum * 100, + coords=model.get_coords(), ), ) assert_conequal( - model.constraints['Sink(Wärme)|lb_Sink(Wärme)|flow_rate'], - flow.model.variables['Sink(Wärme)|flow_rate'] - >= flow.model.variables['Sink(Wärme)|size'] - * xr.DataArray(np.linspace(0.1, 0.5, timesteps.size), coords=(timesteps,)), + model.constraints['Sink(Wärme)|flow_rate|lb'], + flow.submodel.variables['Sink(Wärme)|flow_rate'] + >= flow.submodel.variables['Sink(Wärme)|size'] * flow.relative_minimum, ) assert_conequal( - model.constraints['Sink(Wärme)|ub_Sink(Wärme)|flow_rate'], - flow.model.variables['Sink(Wärme)|flow_rate'] - <= flow.model.variables['Sink(Wärme)|size'] - * xr.DataArray(np.linspace(0.5, 1, timesteps.size), coords=(timesteps,)), + model.constraints['Sink(Wärme)|flow_rate|ub'], + flow.submodel.variables['Sink(Wärme)|flow_rate'] + <= flow.submodel.variables['Sink(Wärme)|size'] * flow.relative_maximum, ) - def test_flow_invest_optional(self, basic_flow_system_linopy): - flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + def test_flow_invest_optional(self, basic_flow_system_linopy_coords, coords_config): + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config + timesteps = flow_system.timesteps flow = fx.Flow( 'Wärme', bus='Fernwärme', - size=fx.InvestParameters(minimum_size=20, maximum_size=100, optional=True), + size=fx.InvestParameters(minimum_size=20, maximum_size=100, mandatory=False), relative_minimum=np.linspace(0.1, 0.5, timesteps.size), relative_maximum=np.linspace(0.5, 1, timesteps.size), ) - flow_system.add_elements(fx.Sink('Sink', sink=flow)) + flow_system.add_elements(fx.Sink('Sink', inputs=[flow])) model = create_linopy_model(flow_system) - assert set(flow.model.variables) == set( - ['Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate', 'Sink(Wärme)|size', 'Sink(Wärme)|is_invested'] + assert_sets_equal( + set(flow.submodel.variables), + {'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate', 'Sink(Wärme)|size', 'Sink(Wärme)|invested'}, + msg='Incorrect variables', ) - assert set(flow.model.constraints) == set( - [ + assert_sets_equal( + set(flow.submodel.constraints), + { 'Sink(Wärme)|total_flow_hours', - 'Sink(Wärme)|is_invested_ub', - 'Sink(Wärme)|is_invested_lb', - 'Sink(Wärme)|lb_Sink(Wärme)|flow_rate', - 'Sink(Wärme)|ub_Sink(Wärme)|flow_rate', - ] + 'Sink(Wärme)|size|lb', + 'Sink(Wärme)|size|ub', + 'Sink(Wärme)|flow_rate|lb', + 'Sink(Wärme)|flow_rate|ub', + }, + msg='Incorrect constraints', ) - assert_var_equal(model['Sink(Wärme)|size'], model.add_variables(lower=0, upper=100)) + assert_var_equal( + model['Sink(Wärme)|size'], + model.add_variables(lower=0, upper=100, coords=model.get_coords(['period', 'scenario'])), + ) - assert_var_equal(model['Sink(Wärme)|is_invested'], model.add_variables(binary=True)) + assert_var_equal( + model['Sink(Wärme)|invested'], + model.add_variables(binary=True, coords=model.get_coords(['period', 'scenario'])), + ) + + assert flow.relative_minimum.dims == tuple(model.get_coords()) + assert flow.relative_maximum.dims == tuple(model.get_coords()) # flow_rate assert_var_equal( - flow.model.flow_rate, + flow.submodel.flow_rate, model.add_variables( lower=0, # Optional investment - upper=np.linspace(0.5, 1, timesteps.size) * 100, - coords=(timesteps,), + upper=flow.relative_maximum * 100, + coords=model.get_coords(), ), ) assert_conequal( - model.constraints['Sink(Wärme)|lb_Sink(Wärme)|flow_rate'], - flow.model.variables['Sink(Wärme)|flow_rate'] - >= flow.model.variables['Sink(Wärme)|size'] - * xr.DataArray(np.linspace(0.1, 0.5, timesteps.size), coords=(timesteps,)), + model.constraints['Sink(Wärme)|flow_rate|lb'], + flow.submodel.variables['Sink(Wärme)|flow_rate'] + >= flow.submodel.variables['Sink(Wärme)|size'] * flow.relative_minimum, ) assert_conequal( - model.constraints['Sink(Wärme)|ub_Sink(Wärme)|flow_rate'], - flow.model.variables['Sink(Wärme)|flow_rate'] - <= flow.model.variables['Sink(Wärme)|size'] - * xr.DataArray(np.linspace(0.5, 1, timesteps.size), coords=(timesteps,)), + model.constraints['Sink(Wärme)|flow_rate|ub'], + flow.submodel.variables['Sink(Wärme)|flow_rate'] + <= flow.submodel.variables['Sink(Wärme)|size'] * flow.relative_maximum, ) # Is invested assert_conequal( - model.constraints['Sink(Wärme)|is_invested_ub'], - flow.model.variables['Sink(Wärme)|size'] <= flow.model.variables['Sink(Wärme)|is_invested'] * 100, + model.constraints['Sink(Wärme)|size|ub'], + flow.submodel.variables['Sink(Wärme)|size'] <= flow.submodel.variables['Sink(Wärme)|invested'] * 100, ) assert_conequal( - model.constraints['Sink(Wärme)|is_invested_lb'], - flow.model.variables['Sink(Wärme)|size'] >= flow.model.variables['Sink(Wärme)|is_invested'] * 20, + model.constraints['Sink(Wärme)|size|lb'], + flow.submodel.variables['Sink(Wärme)|size'] >= flow.submodel.variables['Sink(Wärme)|invested'] * 20, ) - def test_flow_invest_optional_wo_min_size(self, basic_flow_system_linopy): - flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + def test_flow_invest_optional_wo_min_size(self, basic_flow_system_linopy_coords, coords_config): + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config + timesteps = flow_system.timesteps flow = fx.Flow( 'Wärme', bus='Fernwärme', - size=fx.InvestParameters(maximum_size=100, optional=True), + size=fx.InvestParameters(maximum_size=100, mandatory=False), relative_minimum=np.linspace(0.1, 0.5, timesteps.size), relative_maximum=np.linspace(0.5, 1, timesteps.size), ) - flow_system.add_elements(fx.Sink('Sink', sink=flow)) + flow_system.add_elements(fx.Sink('Sink', inputs=[flow])) model = create_linopy_model(flow_system) - assert set(flow.model.variables) == set( - ['Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate', 'Sink(Wärme)|size', 'Sink(Wärme)|is_invested'] + assert_sets_equal( + set(flow.submodel.variables), + {'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate', 'Sink(Wärme)|size', 'Sink(Wärme)|invested'}, + msg='Incorrect variables', ) - assert set(flow.model.constraints) == set( - [ + assert_sets_equal( + set(flow.submodel.constraints), + { 'Sink(Wärme)|total_flow_hours', - 'Sink(Wärme)|is_invested_ub', - 'Sink(Wärme)|is_invested_lb', - 'Sink(Wärme)|lb_Sink(Wärme)|flow_rate', - 'Sink(Wärme)|ub_Sink(Wärme)|flow_rate', - ] + 'Sink(Wärme)|size|ub', + 'Sink(Wärme)|size|lb', + 'Sink(Wärme)|flow_rate|lb', + 'Sink(Wärme)|flow_rate|ub', + }, + msg='Incorrect constraints', ) - assert_var_equal(model['Sink(Wärme)|size'], model.add_variables(lower=0, upper=100)) + assert_var_equal( + model['Sink(Wärme)|size'], + model.add_variables(lower=0, upper=100, coords=model.get_coords(['period', 'scenario'])), + ) - assert_var_equal(model['Sink(Wärme)|is_invested'], model.add_variables(binary=True)) + assert_var_equal( + model['Sink(Wärme)|invested'], + model.add_variables(binary=True, coords=model.get_coords(['period', 'scenario'])), + ) + + assert flow.relative_minimum.dims == tuple(model.get_coords()) + assert flow.relative_maximum.dims == tuple(model.get_coords()) # flow_rate assert_var_equal( - flow.model.flow_rate, + flow.submodel.flow_rate, model.add_variables( lower=0, # Optional investment - upper=np.linspace(0.5, 1, timesteps.size) * 100, - coords=(timesteps,), + upper=flow.relative_maximum * 100, + coords=model.get_coords(), ), ) assert_conequal( - model.constraints['Sink(Wärme)|lb_Sink(Wärme)|flow_rate'], - flow.model.variables['Sink(Wärme)|flow_rate'] - >= flow.model.variables['Sink(Wärme)|size'] - * xr.DataArray(np.linspace(0.1, 0.5, timesteps.size), coords=(timesteps,)), + model.constraints['Sink(Wärme)|flow_rate|lb'], + flow.submodel.variables['Sink(Wärme)|flow_rate'] + >= flow.submodel.variables['Sink(Wärme)|size'] * flow.relative_minimum, ) assert_conequal( - model.constraints['Sink(Wärme)|ub_Sink(Wärme)|flow_rate'], - flow.model.variables['Sink(Wärme)|flow_rate'] - <= flow.model.variables['Sink(Wärme)|size'] - * xr.DataArray(np.linspace(0.5, 1, timesteps.size), coords=(timesteps,)), + model.constraints['Sink(Wärme)|flow_rate|ub'], + flow.submodel.variables['Sink(Wärme)|flow_rate'] + <= flow.submodel.variables['Sink(Wärme)|size'] * flow.relative_maximum, ) # Is invested assert_conequal( - model.constraints['Sink(Wärme)|is_invested_ub'], - flow.model.variables['Sink(Wärme)|size'] <= flow.model.variables['Sink(Wärme)|is_invested'] * 100, + model.constraints['Sink(Wärme)|size|ub'], + flow.submodel.variables['Sink(Wärme)|size'] <= flow.submodel.variables['Sink(Wärme)|invested'] * 100, ) assert_conequal( - model.constraints['Sink(Wärme)|is_invested_lb'], - flow.model.variables['Sink(Wärme)|size'] >= flow.model.variables['Sink(Wärme)|is_invested'] * 1e-5, + model.constraints['Sink(Wärme)|size|lb'], + flow.submodel.variables['Sink(Wärme)|size'] >= flow.submodel.variables['Sink(Wärme)|invested'] * 1e-5, ) - def test_flow_invest_wo_min_size_non_optional(self, basic_flow_system_linopy): - flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + def test_flow_invest_wo_min_size_non_optional(self, basic_flow_system_linopy_coords, coords_config): + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config + timesteps = flow_system.timesteps flow = fx.Flow( 'Wärme', bus='Fernwärme', - size=fx.InvestParameters(maximum_size=100, optional=False), + size=fx.InvestParameters(maximum_size=100, mandatory=True), relative_minimum=np.linspace(0.1, 0.5, timesteps.size), relative_maximum=np.linspace(0.5, 1, timesteps.size), ) - flow_system.add_elements(fx.Sink('Sink', sink=flow)) + flow_system.add_elements(fx.Sink('Sink', inputs=[flow])) model = create_linopy_model(flow_system) - assert set(flow.model.variables) == set( - ['Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate', 'Sink(Wärme)|size'] + assert_sets_equal( + set(flow.submodel.variables), + {'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate', 'Sink(Wärme)|size'}, + msg='Incorrect variables', ) - assert set(flow.model.constraints) == set( - [ + assert_sets_equal( + set(flow.submodel.constraints), + { 'Sink(Wärme)|total_flow_hours', - 'Sink(Wärme)|lb_Sink(Wärme)|flow_rate', - 'Sink(Wärme)|ub_Sink(Wärme)|flow_rate', - ] + 'Sink(Wärme)|flow_rate|lb', + 'Sink(Wärme)|flow_rate|ub', + }, + msg='Incorrect constraints', ) - assert_var_equal(model['Sink(Wärme)|size'], model.add_variables(lower=1e-5, upper=100)) + assert_var_equal( + model['Sink(Wärme)|size'], + model.add_variables(lower=1e-5, upper=100, coords=model.get_coords(['period', 'scenario'])), + ) + + assert flow.relative_minimum.dims == tuple(model.get_coords()) + assert flow.relative_maximum.dims == tuple(model.get_coords()) # flow_rate assert_var_equal( - flow.model.flow_rate, + flow.submodel.flow_rate, model.add_variables( - lower=np.linspace(0.1, 0.5, timesteps.size) * 1e-5, - upper=np.linspace(0.5, 1, timesteps.size) * 100, - coords=(timesteps,), + lower=flow.relative_minimum * 1e-5, + upper=flow.relative_maximum * 100, + coords=model.get_coords(), ), ) assert_conequal( - model.constraints['Sink(Wärme)|lb_Sink(Wärme)|flow_rate'], - flow.model.variables['Sink(Wärme)|flow_rate'] - >= flow.model.variables['Sink(Wärme)|size'] - * xr.DataArray(np.linspace(0.1, 0.5, timesteps.size), coords=(timesteps,)), + model.constraints['Sink(Wärme)|flow_rate|lb'], + flow.submodel.variables['Sink(Wärme)|flow_rate'] + >= flow.submodel.variables['Sink(Wärme)|size'] * flow.relative_minimum, ) assert_conequal( - model.constraints['Sink(Wärme)|ub_Sink(Wärme)|flow_rate'], - flow.model.variables['Sink(Wärme)|flow_rate'] - <= flow.model.variables['Sink(Wärme)|size'] - * xr.DataArray(np.linspace(0.5, 1, timesteps.size), coords=(timesteps,)), + model.constraints['Sink(Wärme)|flow_rate|ub'], + flow.submodel.variables['Sink(Wärme)|flow_rate'] + <= flow.submodel.variables['Sink(Wärme)|size'] * flow.relative_maximum, ) - def test_flow_invest_fixed_size(self, basic_flow_system_linopy): + def test_flow_invest_fixed_size(self, basic_flow_system_linopy_coords, coords_config): """Test flow with fixed size investment.""" - flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config flow = fx.Flow( 'Wärme', bus='Fernwärme', - size=fx.InvestParameters(fixed_size=75, optional=False), + size=fx.InvestParameters(fixed_size=75, mandatory=True), relative_minimum=0.2, relative_maximum=0.9, ) - flow_system.add_elements(fx.Sink('Sink', sink=flow)) + flow_system.add_elements(fx.Sink('Sink', inputs=[flow])) model = create_linopy_model(flow_system) - assert set(flow.model.variables) == { - 'Sink(Wärme)|total_flow_hours', - 'Sink(Wärme)|flow_rate', - 'Sink(Wärme)|size', - } + assert_sets_equal( + set(flow.submodel.variables), + {'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate', 'Sink(Wärme)|size'}, + msg='Incorrect variables', + ) # Check that size is fixed to 75 - assert_var_equal(flow.model.variables['Sink(Wärme)|size'], model.add_variables(lower=75, upper=75)) + assert_var_equal( + flow.submodel.variables['Sink(Wärme)|size'], + model.add_variables(lower=75, upper=75, coords=model.get_coords(['period', 'scenario'])), + ) # Check flow rate bounds - assert_var_equal(flow.model.flow_rate, model.add_variables(lower=0.2 * 75, upper=0.9 * 75, coords=(timesteps,))) + assert_var_equal( + flow.submodel.flow_rate, model.add_variables(lower=0.2 * 75, upper=0.9 * 75, coords=model.get_coords()) + ) - def test_flow_invest_with_effects(self, basic_flow_system_linopy): + def test_flow_invest_with_effects(self, basic_flow_system_linopy_coords, coords_config): """Test flow with investment effects.""" - flow_system = basic_flow_system_linopy + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config # Create effects co2 = fx.Effect(label='CO2', unit='ton', description='CO2 emissions') @@ -392,35 +458,36 @@ def test_flow_invest_with_effects(self, basic_flow_system_linopy): size=fx.InvestParameters( minimum_size=20, maximum_size=100, - optional=True, - fix_effects={'Costs': 1000, 'CO2': 5}, # Fixed investment effects - specific_effects={'Costs': 500, 'CO2': 0.1}, # Specific investment effects + mandatory=False, + effects_of_investment={'costs': 1000, 'CO2': 5}, # Fixed investment effects + effects_of_investment_per_size={'costs': 500, 'CO2': 0.1}, # Specific investment effects ), ) - flow_system.add_elements(fx.Sink('Sink', sink=flow), co2) + flow_system.add_elements(fx.Sink('Sink', inputs=[flow]), co2) model = create_linopy_model(flow_system) # Check investment effects - assert 'Sink(Wärme)->Costs(invest)' in model.variables - assert 'Sink(Wärme)->CO2(invest)' in model.variables + assert 'Sink(Wärme)->costs(periodic)' in model.variables + assert 'Sink(Wärme)->CO2(periodic)' in model.variables - # Check fix effects (applied only when is_invested=1) + # Check fix effects (applied only when invested=1) assert_conequal( - model.constraints['Sink(Wärme)->Costs(invest)'], - model.variables['Sink(Wärme)->Costs(invest)'] - == flow.model.variables['Sink(Wärme)|is_invested'] * 1000 + flow.model.variables['Sink(Wärme)|size'] * 500, + model.constraints['Sink(Wärme)->costs(periodic)'], + model.variables['Sink(Wärme)->costs(periodic)'] + == flow.submodel.variables['Sink(Wärme)|invested'] * 1000 + + flow.submodel.variables['Sink(Wärme)|size'] * 500, ) assert_conequal( - model.constraints['Sink(Wärme)->CO2(invest)'], - model.variables['Sink(Wärme)->CO2(invest)'] - == flow.model.variables['Sink(Wärme)|is_invested'] * 5 + flow.model.variables['Sink(Wärme)|size'] * 0.1, + model.constraints['Sink(Wärme)->CO2(periodic)'], + model.variables['Sink(Wärme)->CO2(periodic)'] + == flow.submodel.variables['Sink(Wärme)|invested'] * 5 + flow.submodel.variables['Sink(Wärme)|size'] * 0.1, ) - def test_flow_invest_divest_effects(self, basic_flow_system_linopy): + def test_flow_invest_divest_effects(self, basic_flow_system_linopy_coords, coords_config): """Test flow with divestment effects.""" - flow_system = basic_flow_system_linopy + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config flow = fx.Flow( 'Wärme', @@ -428,136 +495,153 @@ def test_flow_invest_divest_effects(self, basic_flow_system_linopy): size=fx.InvestParameters( minimum_size=20, maximum_size=100, - optional=True, - divest_effects={'Costs': 500}, # Cost incurred when NOT investing + mandatory=False, + effects_of_retirement={'costs': 500}, # Cost incurred when NOT investing ), ) - flow_system.add_elements(fx.Sink('Sink', sink=flow)) + flow_system.add_elements(fx.Sink('Sink', inputs=[flow])) model = create_linopy_model(flow_system) # Check divestment effects - assert 'Sink(Wärme)->Costs(invest)' in model.constraints + assert 'Sink(Wärme)->costs(periodic)' in model.constraints assert_conequal( - model.constraints['Sink(Wärme)->Costs(invest)'], - model.variables['Sink(Wärme)->Costs(invest)'] + (model.variables['Sink(Wärme)|is_invested'] - 1) * 500 == 0, + model.constraints['Sink(Wärme)->costs(periodic)'], + model.variables['Sink(Wärme)->costs(periodic)'] + (model.variables['Sink(Wärme)|invested'] - 1) * 500 == 0, ) class TestFlowOnModel: """Test the FlowModel class.""" - def test_flow_on(self, basic_flow_system_linopy): - flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + def test_flow_on(self, basic_flow_system_linopy_coords, coords_config): + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config + flow = fx.Flow( 'Wärme', bus='Fernwärme', size=100, - relative_minimum=xr.DataArray(0.2, coords=(timesteps,)), - relative_maximum=xr.DataArray(0.8, coords=(timesteps,)), + relative_minimum=0.2, + relative_maximum=0.8, on_off_parameters=fx.OnOffParameters(), ) - flow_system.add_elements(fx.Sink('Sink', sink=flow)) + flow_system.add_elements(fx.Sink('Sink', inputs=[flow])) model = create_linopy_model(flow_system) - assert set(flow.model.variables) == set( - ['Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate', 'Sink(Wärme)|on', 'Sink(Wärme)|on_hours_total'] + assert_sets_equal( + set(flow.submodel.variables), + {'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate', 'Sink(Wärme)|on', 'Sink(Wärme)|on_hours_total'}, + msg='Incorrect variables', ) - assert set(flow.model.constraints) == set( - [ + assert_sets_equal( + set(flow.submodel.constraints), + { 'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|on_hours_total', - 'Sink(Wärme)|on_con1', - 'Sink(Wärme)|on_con2', - ] + 'Sink(Wärme)|flow_rate|lb', + 'Sink(Wärme)|flow_rate|ub', + }, + msg='Incorrect constraints', ) # flow_rate assert_var_equal( - flow.model.flow_rate, + flow.submodel.flow_rate, model.add_variables( lower=0, upper=0.8 * 100, - coords=(timesteps,), + coords=model.get_coords(), ), ) # OnOff assert_var_equal( - flow.model.on_off.on, - model.add_variables(binary=True, coords=(timesteps,)), + flow.submodel.on_off.on, + model.add_variables(binary=True, coords=model.get_coords()), ) assert_var_equal( model.variables['Sink(Wärme)|on_hours_total'], - model.add_variables(lower=0), + model.add_variables(lower=0, coords=model.get_coords(['period', 'scenario'])), ) assert_conequal( - model.constraints['Sink(Wärme)|on_con1'], - flow.model.variables['Sink(Wärme)|on'] * 0.2 * 100 <= flow.model.variables['Sink(Wärme)|flow_rate'], + model.constraints['Sink(Wärme)|flow_rate|lb'], + flow.submodel.variables['Sink(Wärme)|flow_rate'] >= flow.submodel.variables['Sink(Wärme)|on'] * 0.2 * 100, ) assert_conequal( - model.constraints['Sink(Wärme)|on_con2'], - flow.model.variables['Sink(Wärme)|on'] * 0.8 * 100 >= flow.model.variables['Sink(Wärme)|flow_rate'], + model.constraints['Sink(Wärme)|flow_rate|ub'], + flow.submodel.variables['Sink(Wärme)|flow_rate'] <= flow.submodel.variables['Sink(Wärme)|on'] * 0.8 * 100, ) assert_conequal( model.constraints['Sink(Wärme)|on_hours_total'], - flow.model.variables['Sink(Wärme)|on_hours_total'] - == (flow.model.variables['Sink(Wärme)|on'] * model.hours_per_step).sum(), + flow.submodel.variables['Sink(Wärme)|on_hours_total'] + == (flow.submodel.variables['Sink(Wärme)|on'] * model.hours_per_step).sum('time'), ) - def test_effects_per_running_hour(self, basic_flow_system_linopy): - flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + def test_effects_per_running_hour(self, basic_flow_system_linopy_coords, coords_config): + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config + timesteps = flow_system.timesteps - costs_per_running_hour = xr.DataArray(np.linspace(1, 2, timesteps.size), coords=(timesteps,)) - co2_per_running_hour = xr.DataArray(np.linspace(4, 5, timesteps.size), coords=(timesteps,)) + costs_per_running_hour = np.linspace(1, 2, timesteps.size) + co2_per_running_hour = np.linspace(4, 5, timesteps.size) flow = fx.Flow( 'Wärme', bus='Fernwärme', on_off_parameters=fx.OnOffParameters( - effects_per_running_hour={'Costs': costs_per_running_hour, 'CO2': co2_per_running_hour} + effects_per_running_hour={'costs': costs_per_running_hour, 'CO2': co2_per_running_hour} ), ) - flow_system.add_elements(fx.Sink('Sink', sink=flow), fx.Effect('CO2', 't', '')) + flow_system.add_elements(fx.Sink('Sink', inputs=[flow]), fx.Effect('CO2', 't', '')) model = create_linopy_model(flow_system) - costs, co2 = flow_system.effects['Costs'], flow_system.effects['CO2'] - - assert set(flow.model.variables) == { - 'Sink(Wärme)|total_flow_hours', - 'Sink(Wärme)|flow_rate', - 'Sink(Wärme)|on', - 'Sink(Wärme)|on_hours_total', - } - assert set(flow.model.constraints) == { - 'Sink(Wärme)|total_flow_hours', - 'Sink(Wärme)|on_con1', - 'Sink(Wärme)|on_con2', - 'Sink(Wärme)|on_hours_total', - } - - assert 'Sink(Wärme)->Costs(operation)' in set(costs.model.constraints) - assert 'Sink(Wärme)->CO2(operation)' in set(co2.model.constraints) + costs, co2 = flow_system.effects['costs'], flow_system.effects['CO2'] + + assert_sets_equal( + set(flow.submodel.variables), + { + 'Sink(Wärme)|total_flow_hours', + 'Sink(Wärme)|flow_rate', + 'Sink(Wärme)|on', + 'Sink(Wärme)|on_hours_total', + }, + msg='Incorrect variables', + ) + assert_sets_equal( + set(flow.submodel.constraints), + { + 'Sink(Wärme)|total_flow_hours', + 'Sink(Wärme)|flow_rate|lb', + 'Sink(Wärme)|flow_rate|ub', + 'Sink(Wärme)|on_hours_total', + }, + msg='Incorrect constraints', + ) + + assert 'Sink(Wärme)->costs(temporal)' in set(costs.submodel.constraints) + assert 'Sink(Wärme)->CO2(temporal)' in set(co2.submodel.constraints) + + costs_per_running_hour = flow.on_off_parameters.effects_per_running_hour['costs'] + co2_per_running_hour = flow.on_off_parameters.effects_per_running_hour['CO2'] + + assert costs_per_running_hour.dims == tuple(model.get_coords()) + assert co2_per_running_hour.dims == tuple(model.get_coords()) assert_conequal( - model.constraints['Sink(Wärme)->Costs(operation)'], - model.variables['Sink(Wärme)->Costs(operation)'] - == flow.model.variables['Sink(Wärme)|on'] * model.hours_per_step * costs_per_running_hour, + model.constraints['Sink(Wärme)->costs(temporal)'], + model.variables['Sink(Wärme)->costs(temporal)'] + == flow.submodel.variables['Sink(Wärme)|on'] * model.hours_per_step * costs_per_running_hour, ) assert_conequal( - model.constraints['Sink(Wärme)->CO2(operation)'], - model.variables['Sink(Wärme)->CO2(operation)'] - == flow.model.variables['Sink(Wärme)|on'] * model.hours_per_step * co2_per_running_hour, + model.constraints['Sink(Wärme)->CO2(temporal)'], + model.variables['Sink(Wärme)->CO2(temporal)'] + == flow.submodel.variables['Sink(Wärme)|on'] * model.hours_per_step * co2_per_running_hour, ) - def test_consecutive_on_hours(self, basic_flow_system_linopy): + def test_consecutive_on_hours(self, basic_flow_system_linopy_coords, coords_config): """Test flow with minimum and maximum consecutive on hours.""" - flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config flow = fx.Flow( 'Wärme', @@ -569,56 +653,67 @@ def test_consecutive_on_hours(self, basic_flow_system_linopy): ), ) - flow_system.add_elements(fx.Sink('Sink', sink=flow)) + flow_system.add_elements(fx.Sink('Sink', inputs=[flow])) model = create_linopy_model(flow_system) - assert {'Sink(Wärme)|ConsecutiveOn|hours', 'Sink(Wärme)|on'}.issubset(set(flow.model.variables)) - - assert { - 'Sink(Wärme)|ConsecutiveOn|con1', - 'Sink(Wärme)|ConsecutiveOn|con2a', - 'Sink(Wärme)|ConsecutiveOn|con2b', - 'Sink(Wärme)|ConsecutiveOn|initial', - 'Sink(Wärme)|ConsecutiveOn|minimum', - }.issubset(set(flow.model.constraints)) + assert {'Sink(Wärme)|consecutive_on_hours', 'Sink(Wärme)|on'}.issubset(set(flow.submodel.variables)) + + assert_sets_equal( + { + 'Sink(Wärme)|consecutive_on_hours|ub', + 'Sink(Wärme)|consecutive_on_hours|forward', + 'Sink(Wärme)|consecutive_on_hours|backward', + 'Sink(Wärme)|consecutive_on_hours|initial', + 'Sink(Wärme)|consecutive_on_hours|lb', + } + & set(flow.submodel.constraints), + { + 'Sink(Wärme)|consecutive_on_hours|ub', + 'Sink(Wärme)|consecutive_on_hours|forward', + 'Sink(Wärme)|consecutive_on_hours|backward', + 'Sink(Wärme)|consecutive_on_hours|initial', + 'Sink(Wärme)|consecutive_on_hours|lb', + }, + msg='Missing consecutive on hours constraints', + ) assert_var_equal( - model.variables['Sink(Wärme)|ConsecutiveOn|hours'], - model.add_variables(lower=0, upper=8, coords=(timesteps,)), + model.variables['Sink(Wärme)|consecutive_on_hours'], + model.add_variables(lower=0, upper=8, coords=model.get_coords()), ) mega = model.hours_per_step.sum('time') assert_conequal( - model.constraints['Sink(Wärme)|ConsecutiveOn|con1'], - model.variables['Sink(Wärme)|ConsecutiveOn|hours'] <= model.variables['Sink(Wärme)|on'] * mega, + model.constraints['Sink(Wärme)|consecutive_on_hours|ub'], + model.variables['Sink(Wärme)|consecutive_on_hours'] <= model.variables['Sink(Wärme)|on'] * mega, ) assert_conequal( - model.constraints['Sink(Wärme)|ConsecutiveOn|con2a'], - model.variables['Sink(Wärme)|ConsecutiveOn|hours'].isel(time=slice(1, None)) - <= model.variables['Sink(Wärme)|ConsecutiveOn|hours'].isel(time=slice(None, -1)) + model.constraints['Sink(Wärme)|consecutive_on_hours|forward'], + model.variables['Sink(Wärme)|consecutive_on_hours'].isel(time=slice(1, None)) + <= model.variables['Sink(Wärme)|consecutive_on_hours'].isel(time=slice(None, -1)) + model.hours_per_step.isel(time=slice(None, -1)), ) # eq: duration(t) >= duration(t - 1) + dt(t) + (On(t) - 1) * BIG assert_conequal( - model.constraints['Sink(Wärme)|ConsecutiveOn|con2b'], - model.variables['Sink(Wärme)|ConsecutiveOn|hours'].isel(time=slice(1, None)) - >= model.variables['Sink(Wärme)|ConsecutiveOn|hours'].isel(time=slice(None, -1)) + model.constraints['Sink(Wärme)|consecutive_on_hours|backward'], + model.variables['Sink(Wärme)|consecutive_on_hours'].isel(time=slice(1, None)) + >= model.variables['Sink(Wärme)|consecutive_on_hours'].isel(time=slice(None, -1)) + model.hours_per_step.isel(time=slice(None, -1)) + (model.variables['Sink(Wärme)|on'].isel(time=slice(1, None)) - 1) * mega, ) assert_conequal( - model.constraints['Sink(Wärme)|ConsecutiveOn|initial'], - model.variables['Sink(Wärme)|ConsecutiveOn|hours'].isel(time=0) + model.constraints['Sink(Wärme)|consecutive_on_hours|initial'], + model.variables['Sink(Wärme)|consecutive_on_hours'].isel(time=0) == model.variables['Sink(Wärme)|on'].isel(time=0) * model.hours_per_step.isel(time=0), ) assert_conequal( - model.constraints['Sink(Wärme)|ConsecutiveOn|minimum'], - model.variables['Sink(Wärme)|ConsecutiveOn|hours'] + model.constraints['Sink(Wärme)|consecutive_on_hours|lb'], + model.variables['Sink(Wärme)|consecutive_on_hours'] >= ( model.variables['Sink(Wärme)|on'].isel(time=slice(None, -1)) - model.variables['Sink(Wärme)|on'].isel(time=slice(1, None)) @@ -626,10 +721,9 @@ def test_consecutive_on_hours(self, basic_flow_system_linopy): * 2, ) - def test_consecutive_on_hours_previous(self, basic_flow_system_linopy): + def test_consecutive_on_hours_previous(self, basic_flow_system_linopy_coords, coords_config): """Test flow with minimum and maximum consecutive on hours.""" - flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config flow = fx.Flow( 'Wärme', @@ -642,56 +736,65 @@ def test_consecutive_on_hours_previous(self, basic_flow_system_linopy): previous_flow_rate=np.array([10, 20, 30, 0, 20, 20, 30]), # Previously on for 3 steps ) - flow_system.add_elements(fx.Sink('Sink', sink=flow)) + flow_system.add_elements(fx.Sink('Sink', inputs=[flow])) model = create_linopy_model(flow_system) - assert {'Sink(Wärme)|ConsecutiveOn|hours', 'Sink(Wärme)|on'}.issubset(set(flow.model.variables)) - - assert { - 'Sink(Wärme)|ConsecutiveOn|con1', - 'Sink(Wärme)|ConsecutiveOn|con2a', - 'Sink(Wärme)|ConsecutiveOn|con2b', - 'Sink(Wärme)|ConsecutiveOn|initial', - 'Sink(Wärme)|ConsecutiveOn|minimum', - }.issubset(set(flow.model.constraints)) + assert {'Sink(Wärme)|consecutive_on_hours', 'Sink(Wärme)|on'}.issubset(set(flow.submodel.variables)) + + assert_sets_equal( + { + 'Sink(Wärme)|consecutive_on_hours|lb', + 'Sink(Wärme)|consecutive_on_hours|forward', + 'Sink(Wärme)|consecutive_on_hours|backward', + 'Sink(Wärme)|consecutive_on_hours|initial', + } + & set(flow.submodel.constraints), + { + 'Sink(Wärme)|consecutive_on_hours|lb', + 'Sink(Wärme)|consecutive_on_hours|forward', + 'Sink(Wärme)|consecutive_on_hours|backward', + 'Sink(Wärme)|consecutive_on_hours|initial', + }, + msg='Missing consecutive on hours constraints for previous states', + ) assert_var_equal( - model.variables['Sink(Wärme)|ConsecutiveOn|hours'], - model.add_variables(lower=0, upper=8, coords=(timesteps,)), + model.variables['Sink(Wärme)|consecutive_on_hours'], + model.add_variables(lower=0, upper=8, coords=model.get_coords()), ) mega = model.hours_per_step.sum('time') + model.hours_per_step.isel(time=0) * 3 assert_conequal( - model.constraints['Sink(Wärme)|ConsecutiveOn|con1'], - model.variables['Sink(Wärme)|ConsecutiveOn|hours'] <= model.variables['Sink(Wärme)|on'] * mega, + model.constraints['Sink(Wärme)|consecutive_on_hours|ub'], + model.variables['Sink(Wärme)|consecutive_on_hours'] <= model.variables['Sink(Wärme)|on'] * mega, ) assert_conequal( - model.constraints['Sink(Wärme)|ConsecutiveOn|con2a'], - model.variables['Sink(Wärme)|ConsecutiveOn|hours'].isel(time=slice(1, None)) - <= model.variables['Sink(Wärme)|ConsecutiveOn|hours'].isel(time=slice(None, -1)) + model.constraints['Sink(Wärme)|consecutive_on_hours|forward'], + model.variables['Sink(Wärme)|consecutive_on_hours'].isel(time=slice(1, None)) + <= model.variables['Sink(Wärme)|consecutive_on_hours'].isel(time=slice(None, -1)) + model.hours_per_step.isel(time=slice(None, -1)), ) # eq: duration(t) >= duration(t - 1) + dt(t) + (On(t) - 1) * BIG assert_conequal( - model.constraints['Sink(Wärme)|ConsecutiveOn|con2b'], - model.variables['Sink(Wärme)|ConsecutiveOn|hours'].isel(time=slice(1, None)) - >= model.variables['Sink(Wärme)|ConsecutiveOn|hours'].isel(time=slice(None, -1)) + model.constraints['Sink(Wärme)|consecutive_on_hours|backward'], + model.variables['Sink(Wärme)|consecutive_on_hours'].isel(time=slice(1, None)) + >= model.variables['Sink(Wärme)|consecutive_on_hours'].isel(time=slice(None, -1)) + model.hours_per_step.isel(time=slice(None, -1)) + (model.variables['Sink(Wärme)|on'].isel(time=slice(1, None)) - 1) * mega, ) assert_conequal( - model.constraints['Sink(Wärme)|ConsecutiveOn|initial'], - model.variables['Sink(Wärme)|ConsecutiveOn|hours'].isel(time=0) + model.constraints['Sink(Wärme)|consecutive_on_hours|initial'], + model.variables['Sink(Wärme)|consecutive_on_hours'].isel(time=0) == model.variables['Sink(Wärme)|on'].isel(time=0) * (model.hours_per_step.isel(time=0) * (1 + 3)), ) assert_conequal( - model.constraints['Sink(Wärme)|ConsecutiveOn|minimum'], - model.variables['Sink(Wärme)|ConsecutiveOn|hours'] + model.constraints['Sink(Wärme)|consecutive_on_hours|lb'], + model.variables['Sink(Wärme)|consecutive_on_hours'] >= ( model.variables['Sink(Wärme)|on'].isel(time=slice(None, -1)) - model.variables['Sink(Wärme)|on'].isel(time=slice(1, None)) @@ -699,10 +802,9 @@ def test_consecutive_on_hours_previous(self, basic_flow_system_linopy): * 2, ) - def test_consecutive_off_hours(self, basic_flow_system_linopy): + def test_consecutive_off_hours(self, basic_flow_system_linopy_coords, coords_config): """Test flow with minimum and maximum consecutive off hours.""" - flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config flow = fx.Flow( 'Wärme', @@ -714,56 +816,67 @@ def test_consecutive_off_hours(self, basic_flow_system_linopy): ), ) - flow_system.add_elements(fx.Sink('Sink', sink=flow)) + flow_system.add_elements(fx.Sink('Sink', inputs=[flow])) model = create_linopy_model(flow_system) - assert {'Sink(Wärme)|ConsecutiveOff|hours', 'Sink(Wärme)|off'}.issubset(set(flow.model.variables)) - - assert { - 'Sink(Wärme)|ConsecutiveOff|con1', - 'Sink(Wärme)|ConsecutiveOff|con2a', - 'Sink(Wärme)|ConsecutiveOff|con2b', - 'Sink(Wärme)|ConsecutiveOff|initial', - 'Sink(Wärme)|ConsecutiveOff|minimum', - }.issubset(set(flow.model.constraints)) + assert {'Sink(Wärme)|consecutive_off_hours', 'Sink(Wärme)|off'}.issubset(set(flow.submodel.variables)) + + assert_sets_equal( + { + 'Sink(Wärme)|consecutive_off_hours|ub', + 'Sink(Wärme)|consecutive_off_hours|forward', + 'Sink(Wärme)|consecutive_off_hours|backward', + 'Sink(Wärme)|consecutive_off_hours|initial', + 'Sink(Wärme)|consecutive_off_hours|lb', + } + & set(flow.submodel.constraints), + { + 'Sink(Wärme)|consecutive_off_hours|ub', + 'Sink(Wärme)|consecutive_off_hours|forward', + 'Sink(Wärme)|consecutive_off_hours|backward', + 'Sink(Wärme)|consecutive_off_hours|initial', + 'Sink(Wärme)|consecutive_off_hours|lb', + }, + msg='Missing consecutive off hours constraints', + ) assert_var_equal( - model.variables['Sink(Wärme)|ConsecutiveOff|hours'], - model.add_variables(lower=0, upper=12, coords=(timesteps,)), + model.variables['Sink(Wärme)|consecutive_off_hours'], + model.add_variables(lower=0, upper=12, coords=model.get_coords()), ) mega = model.hours_per_step.sum('time') + model.hours_per_step.isel(time=0) * 1 # previously off for 1h assert_conequal( - model.constraints['Sink(Wärme)|ConsecutiveOff|con1'], - model.variables['Sink(Wärme)|ConsecutiveOff|hours'] <= model.variables['Sink(Wärme)|off'] * mega, + model.constraints['Sink(Wärme)|consecutive_off_hours|ub'], + model.variables['Sink(Wärme)|consecutive_off_hours'] <= model.variables['Sink(Wärme)|off'] * mega, ) assert_conequal( - model.constraints['Sink(Wärme)|ConsecutiveOff|con2a'], - model.variables['Sink(Wärme)|ConsecutiveOff|hours'].isel(time=slice(1, None)) - <= model.variables['Sink(Wärme)|ConsecutiveOff|hours'].isel(time=slice(None, -1)) + model.constraints['Sink(Wärme)|consecutive_off_hours|forward'], + model.variables['Sink(Wärme)|consecutive_off_hours'].isel(time=slice(1, None)) + <= model.variables['Sink(Wärme)|consecutive_off_hours'].isel(time=slice(None, -1)) + model.hours_per_step.isel(time=slice(None, -1)), ) # eq: duration(t) >= duration(t - 1) + dt(t) + (On(t) - 1) * BIG assert_conequal( - model.constraints['Sink(Wärme)|ConsecutiveOff|con2b'], - model.variables['Sink(Wärme)|ConsecutiveOff|hours'].isel(time=slice(1, None)) - >= model.variables['Sink(Wärme)|ConsecutiveOff|hours'].isel(time=slice(None, -1)) + model.constraints['Sink(Wärme)|consecutive_off_hours|backward'], + model.variables['Sink(Wärme)|consecutive_off_hours'].isel(time=slice(1, None)) + >= model.variables['Sink(Wärme)|consecutive_off_hours'].isel(time=slice(None, -1)) + model.hours_per_step.isel(time=slice(None, -1)) + (model.variables['Sink(Wärme)|off'].isel(time=slice(1, None)) - 1) * mega, ) assert_conequal( - model.constraints['Sink(Wärme)|ConsecutiveOff|initial'], - model.variables['Sink(Wärme)|ConsecutiveOff|hours'].isel(time=0) + model.constraints['Sink(Wärme)|consecutive_off_hours|initial'], + model.variables['Sink(Wärme)|consecutive_off_hours'].isel(time=0) == model.variables['Sink(Wärme)|off'].isel(time=0) * (model.hours_per_step.isel(time=0) * (1 + 1)), ) assert_conequal( - model.constraints['Sink(Wärme)|ConsecutiveOff|minimum'], - model.variables['Sink(Wärme)|ConsecutiveOff|hours'] + model.constraints['Sink(Wärme)|consecutive_off_hours|lb'], + model.variables['Sink(Wärme)|consecutive_off_hours'] >= ( model.variables['Sink(Wärme)|off'].isel(time=slice(None, -1)) - model.variables['Sink(Wärme)|off'].isel(time=slice(1, None)) @@ -771,10 +884,9 @@ def test_consecutive_off_hours(self, basic_flow_system_linopy): * 4, ) - def test_consecutive_off_hours_previous(self, basic_flow_system_linopy): + def test_consecutive_off_hours_previous(self, basic_flow_system_linopy_coords, coords_config): """Test flow with minimum and maximum consecutive off hours.""" - flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config flow = fx.Flow( 'Wärme', @@ -787,56 +899,67 @@ def test_consecutive_off_hours_previous(self, basic_flow_system_linopy): previous_flow_rate=np.array([10, 20, 30, 0, 20, 0, 0]), # Previously off for 2 steps ) - flow_system.add_elements(fx.Sink('Sink', sink=flow)) + flow_system.add_elements(fx.Sink('Sink', inputs=[flow])) model = create_linopy_model(flow_system) - assert {'Sink(Wärme)|ConsecutiveOff|hours', 'Sink(Wärme)|off'}.issubset(set(flow.model.variables)) - - assert { - 'Sink(Wärme)|ConsecutiveOff|con1', - 'Sink(Wärme)|ConsecutiveOff|con2a', - 'Sink(Wärme)|ConsecutiveOff|con2b', - 'Sink(Wärme)|ConsecutiveOff|initial', - 'Sink(Wärme)|ConsecutiveOff|minimum', - }.issubset(set(flow.model.constraints)) + assert {'Sink(Wärme)|consecutive_off_hours', 'Sink(Wärme)|off'}.issubset(set(flow.submodel.variables)) + + assert_sets_equal( + { + 'Sink(Wärme)|consecutive_off_hours|ub', + 'Sink(Wärme)|consecutive_off_hours|forward', + 'Sink(Wärme)|consecutive_off_hours|backward', + 'Sink(Wärme)|consecutive_off_hours|initial', + 'Sink(Wärme)|consecutive_off_hours|lb', + } + & set(flow.submodel.constraints), + { + 'Sink(Wärme)|consecutive_off_hours|ub', + 'Sink(Wärme)|consecutive_off_hours|forward', + 'Sink(Wärme)|consecutive_off_hours|backward', + 'Sink(Wärme)|consecutive_off_hours|initial', + 'Sink(Wärme)|consecutive_off_hours|lb', + }, + msg='Missing consecutive off hours constraints for previous states', + ) assert_var_equal( - model.variables['Sink(Wärme)|ConsecutiveOff|hours'], - model.add_variables(lower=0, upper=12, coords=(timesteps,)), + model.variables['Sink(Wärme)|consecutive_off_hours'], + model.add_variables(lower=0, upper=12, coords=model.get_coords()), ) mega = model.hours_per_step.sum('time') + model.hours_per_step.isel(time=0) * 2 assert_conequal( - model.constraints['Sink(Wärme)|ConsecutiveOff|con1'], - model.variables['Sink(Wärme)|ConsecutiveOff|hours'] <= model.variables['Sink(Wärme)|off'] * mega, + model.constraints['Sink(Wärme)|consecutive_off_hours|ub'], + model.variables['Sink(Wärme)|consecutive_off_hours'] <= model.variables['Sink(Wärme)|off'] * mega, ) assert_conequal( - model.constraints['Sink(Wärme)|ConsecutiveOff|con2a'], - model.variables['Sink(Wärme)|ConsecutiveOff|hours'].isel(time=slice(1, None)) - <= model.variables['Sink(Wärme)|ConsecutiveOff|hours'].isel(time=slice(None, -1)) + model.constraints['Sink(Wärme)|consecutive_off_hours|forward'], + model.variables['Sink(Wärme)|consecutive_off_hours'].isel(time=slice(1, None)) + <= model.variables['Sink(Wärme)|consecutive_off_hours'].isel(time=slice(None, -1)) + model.hours_per_step.isel(time=slice(None, -1)), ) # eq: duration(t) >= duration(t - 1) + dt(t) + (On(t) - 1) * BIG assert_conequal( - model.constraints['Sink(Wärme)|ConsecutiveOff|con2b'], - model.variables['Sink(Wärme)|ConsecutiveOff|hours'].isel(time=slice(1, None)) - >= model.variables['Sink(Wärme)|ConsecutiveOff|hours'].isel(time=slice(None, -1)) + model.constraints['Sink(Wärme)|consecutive_off_hours|backward'], + model.variables['Sink(Wärme)|consecutive_off_hours'].isel(time=slice(1, None)) + >= model.variables['Sink(Wärme)|consecutive_off_hours'].isel(time=slice(None, -1)) + model.hours_per_step.isel(time=slice(None, -1)) + (model.variables['Sink(Wärme)|off'].isel(time=slice(1, None)) - 1) * mega, ) assert_conequal( - model.constraints['Sink(Wärme)|ConsecutiveOff|initial'], - model.variables['Sink(Wärme)|ConsecutiveOff|hours'].isel(time=0) + model.constraints['Sink(Wärme)|consecutive_off_hours|initial'], + model.variables['Sink(Wärme)|consecutive_off_hours'].isel(time=0) == model.variables['Sink(Wärme)|off'].isel(time=0) * (model.hours_per_step.isel(time=0) * (1 + 2)), ) assert_conequal( - model.constraints['Sink(Wärme)|ConsecutiveOff|minimum'], - model.variables['Sink(Wärme)|ConsecutiveOff|hours'] + model.constraints['Sink(Wärme)|consecutive_off_hours|lb'], + model.variables['Sink(Wärme)|consecutive_off_hours'] >= ( model.variables['Sink(Wärme)|off'].isel(time=slice(None, -1)) - model.variables['Sink(Wärme)|off'].isel(time=slice(1, None)) @@ -844,9 +967,9 @@ def test_consecutive_off_hours_previous(self, basic_flow_system_linopy): * 4, ) - def test_switch_on_constraints(self, basic_flow_system_linopy): + def test_switch_on_constraints(self, basic_flow_system_linopy_coords, coords_config): """Test flow with constraints on the number of startups.""" - flow_system = basic_flow_system_linopy + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config flow = fx.Flow( 'Wärme', @@ -854,48 +977,61 @@ def test_switch_on_constraints(self, basic_flow_system_linopy): size=100, on_off_parameters=fx.OnOffParameters( switch_on_total_max=5, # Maximum 5 startups - effects_per_switch_on={'Costs': 100}, # 100 EUR startup cost + effects_per_switch_on={'costs': 100}, # 100 EUR startup cost ), ) - flow_system.add_elements(fx.Sink('Sink', sink=flow)) + flow_system.add_elements(fx.Sink('Sink', inputs=[flow])) model = create_linopy_model(flow_system) # Check that variables exist - assert {'Sink(Wärme)|switch_on', 'Sink(Wärme)|switch_off', 'Sink(Wärme)|switch_on_nr'}.issubset( - set(flow.model.variables) + assert {'Sink(Wärme)|switch|on', 'Sink(Wärme)|switch|off', 'Sink(Wärme)|switch|count'}.issubset( + set(flow.submodel.variables) ) # Check that constraints exist - assert { - 'Sink(Wärme)|switch_con', - 'Sink(Wärme)|initial_switch_con', - 'Sink(Wärme)|switch_on_or_off', - 'Sink(Wärme)|switch_on_nr', - }.issubset(set(flow.model.constraints)) + assert_sets_equal( + { + 'Sink(Wärme)|switch|transition', + 'Sink(Wärme)|switch|initial', + 'Sink(Wärme)|switch|mutex', + 'Sink(Wärme)|switch|count', + } + & set(flow.submodel.constraints), + { + 'Sink(Wärme)|switch|transition', + 'Sink(Wärme)|switch|initial', + 'Sink(Wärme)|switch|mutex', + 'Sink(Wärme)|switch|count', + }, + msg='Missing switch constraints', + ) # Check switch_on_nr variable bounds - assert_var_equal(flow.model.variables['Sink(Wärme)|switch_on_nr'], model.add_variables(lower=0, upper=5)) + assert_var_equal( + flow.submodel.variables['Sink(Wärme)|switch|count'], + model.add_variables(lower=0, upper=5, coords=model.get_coords(['period', 'scenario'])), + ) # Verify switch_on_nr constraint (limits number of startups) assert_conequal( - model.constraints['Sink(Wärme)|switch_on_nr'], - flow.model.variables['Sink(Wärme)|switch_on_nr'] - == flow.model.variables['Sink(Wärme)|switch_on'].sum('time'), + model.constraints['Sink(Wärme)|switch|count'], + flow.submodel.variables['Sink(Wärme)|switch|count'] + == flow.submodel.variables['Sink(Wärme)|switch|on'].sum('time'), ) # Check that startup cost effect constraint exists - assert 'Sink(Wärme)->Costs(operation)' in model.constraints + assert 'Sink(Wärme)->costs(temporal)' in model.constraints # Verify the startup cost effect constraint assert_conequal( - model.constraints['Sink(Wärme)->Costs(operation)'], - model.variables['Sink(Wärme)->Costs(operation)'] == flow.model.variables['Sink(Wärme)|switch_on'] * 100, + model.constraints['Sink(Wärme)->costs(temporal)'], + model.variables['Sink(Wärme)->costs(temporal)'] == flow.submodel.variables['Sink(Wärme)|switch|on'] * 100, ) - def test_on_hours_limits(self, basic_flow_system_linopy): + def test_on_hours_limits(self, basic_flow_system_linopy_coords, coords_config): """Test flow with limits on total on hours.""" - flow_system = basic_flow_system_linopy + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config flow = fx.Flow( 'Wärme', @@ -907,224 +1043,258 @@ def test_on_hours_limits(self, basic_flow_system_linopy): ), ) - flow_system.add_elements(fx.Sink('Sink', sink=flow)) + flow_system.add_elements(fx.Sink('Sink', inputs=[flow])) model = create_linopy_model(flow_system) # Check that variables exist - assert {'Sink(Wärme)|on', 'Sink(Wärme)|on_hours_total'}.issubset(set(flow.model.variables)) + assert {'Sink(Wärme)|on', 'Sink(Wärme)|on_hours_total'}.issubset(set(flow.submodel.variables)) # Check that constraints exist assert 'Sink(Wärme)|on_hours_total' in model.constraints # Check on_hours_total variable bounds - assert_var_equal(flow.model.variables['Sink(Wärme)|on_hours_total'], model.add_variables(lower=20, upper=100)) + assert_var_equal( + flow.submodel.variables['Sink(Wärme)|on_hours_total'], + model.add_variables(lower=20, upper=100, coords=model.get_coords(['period', 'scenario'])), + ) # Check on_hours_total constraint assert_conequal( model.constraints['Sink(Wärme)|on_hours_total'], - flow.model.variables['Sink(Wärme)|on_hours_total'] - == (flow.model.variables['Sink(Wärme)|on'] * model.hours_per_step).sum(), + flow.submodel.variables['Sink(Wärme)|on_hours_total'] + == (flow.submodel.variables['Sink(Wärme)|on'] * model.hours_per_step).sum('time'), ) class TestFlowOnInvestModel: """Test the FlowModel class.""" - def test_flow_on_invest_optional(self, basic_flow_system_linopy): - flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + def test_flow_on_invest_optional(self, basic_flow_system_linopy_coords, coords_config): + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config flow = fx.Flow( 'Wärme', bus='Fernwärme', - size=fx.InvestParameters(minimum_size=20, maximum_size=200, optional=True), - relative_minimum=xr.DataArray(0.2, coords=(timesteps,)), - relative_maximum=xr.DataArray(0.8, coords=(timesteps,)), + size=fx.InvestParameters(minimum_size=20, maximum_size=200, mandatory=False), + relative_minimum=0.2, + relative_maximum=0.8, on_off_parameters=fx.OnOffParameters(), ) - flow_system.add_elements(fx.Sink('Sink', sink=flow)) + flow_system.add_elements(fx.Sink('Sink', inputs=[flow])) model = create_linopy_model(flow_system) - assert set(flow.model.variables) == set( - [ + assert_sets_equal( + set(flow.submodel.variables), + { 'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate', - 'Sink(Wärme)|is_invested', + 'Sink(Wärme)|invested', 'Sink(Wärme)|size', 'Sink(Wärme)|on', 'Sink(Wärme)|on_hours_total', - ] + }, + msg='Incorrect variables', ) - assert set(flow.model.constraints) == set( - [ + assert_sets_equal( + set(flow.submodel.constraints), + { 'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|on_hours_total', - 'Sink(Wärme)|on_con1', - 'Sink(Wärme)|on_con2', - 'Sink(Wärme)|is_invested_lb', - 'Sink(Wärme)|is_invested_ub', - 'Sink(Wärme)|lb_Sink(Wärme)|flow_rate', - 'Sink(Wärme)|ub_Sink(Wärme)|flow_rate', - ] + 'Sink(Wärme)|flow_rate|lb1', + 'Sink(Wärme)|flow_rate|ub1', + 'Sink(Wärme)|size|lb', + 'Sink(Wärme)|size|ub', + 'Sink(Wärme)|flow_rate|lb2', + 'Sink(Wärme)|flow_rate|ub2', + }, + msg='Incorrect constraints', ) # flow_rate assert_var_equal( - flow.model.flow_rate, + flow.submodel.flow_rate, model.add_variables( lower=0, upper=0.8 * 200, - coords=(timesteps,), + coords=model.get_coords(), ), ) # OnOff assert_var_equal( - flow.model.on_off.on, - model.add_variables(binary=True, coords=(timesteps,)), + flow.submodel.on_off.on, + model.add_variables(binary=True, coords=model.get_coords()), ) assert_var_equal( model.variables['Sink(Wärme)|on_hours_total'], - model.add_variables(lower=0), + model.add_variables(lower=0, coords=model.get_coords(['period', 'scenario'])), ) assert_conequal( - model.constraints['Sink(Wärme)|on_con1'], - flow.model.variables['Sink(Wärme)|on'] * 0.2 * 20 <= flow.model.variables['Sink(Wärme)|flow_rate'], + model.constraints['Sink(Wärme)|size|lb'], + flow.submodel.variables['Sink(Wärme)|size'] >= flow.submodel.variables['Sink(Wärme)|invested'] * 20, ) assert_conequal( - model.constraints['Sink(Wärme)|on_con2'], - flow.model.variables['Sink(Wärme)|on'] * 0.8 * 200 >= flow.model.variables['Sink(Wärme)|flow_rate'], + model.constraints['Sink(Wärme)|size|ub'], + flow.submodel.variables['Sink(Wärme)|size'] <= flow.submodel.variables['Sink(Wärme)|invested'] * 200, + ) + assert_conequal( + model.constraints['Sink(Wärme)|flow_rate|lb1'], + flow.submodel.variables['Sink(Wärme)|on'] * 0.2 * 20 <= flow.submodel.variables['Sink(Wärme)|flow_rate'], + ) + assert_conequal( + model.constraints['Sink(Wärme)|flow_rate|ub1'], + flow.submodel.variables['Sink(Wärme)|on'] * 0.8 * 200 >= flow.submodel.variables['Sink(Wärme)|flow_rate'], ) assert_conequal( model.constraints['Sink(Wärme)|on_hours_total'], - flow.model.variables['Sink(Wärme)|on_hours_total'] - == (flow.model.variables['Sink(Wärme)|on'] * model.hours_per_step).sum(), + flow.submodel.variables['Sink(Wärme)|on_hours_total'] + == (flow.submodel.variables['Sink(Wärme)|on'] * model.hours_per_step).sum('time'), ) # Investment - assert_var_equal(model['Sink(Wärme)|size'], model.add_variables(lower=0, upper=200)) + assert_var_equal( + model['Sink(Wärme)|size'], + model.add_variables(lower=0, upper=200, coords=model.get_coords(['period', 'scenario'])), + ) mega = 0.2 * 200 # Relative minimum * maximum size assert_conequal( - model.constraints['Sink(Wärme)|lb_Sink(Wärme)|flow_rate'], - flow.model.variables['Sink(Wärme)|flow_rate'] - >= flow.model.variables['Sink(Wärme)|on'] * mega + flow.model.variables['Sink(Wärme)|size'] * 0.2 - mega, + model.constraints['Sink(Wärme)|flow_rate|lb2'], + flow.submodel.variables['Sink(Wärme)|flow_rate'] + >= flow.submodel.variables['Sink(Wärme)|on'] * mega + + flow.submodel.variables['Sink(Wärme)|size'] * 0.2 + - mega, ) assert_conequal( - model.constraints['Sink(Wärme)|ub_Sink(Wärme)|flow_rate'], - flow.model.variables['Sink(Wärme)|flow_rate'] <= flow.model.variables['Sink(Wärme)|size'] * 0.8, + model.constraints['Sink(Wärme)|flow_rate|ub2'], + flow.submodel.variables['Sink(Wärme)|flow_rate'] <= flow.submodel.variables['Sink(Wärme)|size'] * 0.8, ) - def test_flow_on_invest_non_optional(self, basic_flow_system_linopy): - flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + def test_flow_on_invest_non_optional(self, basic_flow_system_linopy_coords, coords_config): + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config flow = fx.Flow( 'Wärme', bus='Fernwärme', - size=fx.InvestParameters(minimum_size=20, maximum_size=200, optional=False), - relative_minimum=xr.DataArray(0.2, coords=(timesteps,)), - relative_maximum=xr.DataArray(0.8, coords=(timesteps,)), + size=fx.InvestParameters(minimum_size=20, maximum_size=200, mandatory=True), + relative_minimum=0.2, + relative_maximum=0.8, on_off_parameters=fx.OnOffParameters(), ) - flow_system.add_elements(fx.Sink('Sink', sink=flow)) + flow_system.add_elements(fx.Sink('Sink', inputs=[flow])) model = create_linopy_model(flow_system) - assert set(flow.model.variables) == set( - [ + assert_sets_equal( + set(flow.submodel.variables), + { 'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate', 'Sink(Wärme)|size', 'Sink(Wärme)|on', 'Sink(Wärme)|on_hours_total', - ] + }, + msg='Incorrect variables', ) - assert set(flow.model.constraints) == set( - [ + assert_sets_equal( + set(flow.submodel.constraints), + { 'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|on_hours_total', - 'Sink(Wärme)|on_con1', - 'Sink(Wärme)|on_con2', - 'Sink(Wärme)|lb_Sink(Wärme)|flow_rate', - 'Sink(Wärme)|ub_Sink(Wärme)|flow_rate', - ] + 'Sink(Wärme)|flow_rate|lb1', + 'Sink(Wärme)|flow_rate|ub1', + 'Sink(Wärme)|flow_rate|lb2', + 'Sink(Wärme)|flow_rate|ub2', + }, + msg='Incorrect constraints', ) # flow_rate assert_var_equal( - flow.model.flow_rate, + flow.submodel.flow_rate, model.add_variables( lower=0, upper=0.8 * 200, - coords=(timesteps,), + coords=model.get_coords(), ), ) # OnOff assert_var_equal( - flow.model.on_off.on, - model.add_variables(binary=True, coords=(timesteps,)), + flow.submodel.on_off.on, + model.add_variables(binary=True, coords=model.get_coords()), ) assert_var_equal( model.variables['Sink(Wärme)|on_hours_total'], - model.add_variables(lower=0), + model.add_variables(lower=0, coords=model.get_coords(['period', 'scenario'])), ) assert_conequal( - model.constraints['Sink(Wärme)|on_con1'], - flow.model.variables['Sink(Wärme)|on'] * 0.2 * 20 <= flow.model.variables['Sink(Wärme)|flow_rate'], + model.constraints['Sink(Wärme)|flow_rate|lb1'], + flow.submodel.variables['Sink(Wärme)|on'] * 0.2 * 20 <= flow.submodel.variables['Sink(Wärme)|flow_rate'], ) assert_conequal( - model.constraints['Sink(Wärme)|on_con2'], - flow.model.variables['Sink(Wärme)|on'] * 0.8 * 200 >= flow.model.variables['Sink(Wärme)|flow_rate'], + model.constraints['Sink(Wärme)|flow_rate|ub1'], + flow.submodel.variables['Sink(Wärme)|on'] * 0.8 * 200 >= flow.submodel.variables['Sink(Wärme)|flow_rate'], ) assert_conequal( model.constraints['Sink(Wärme)|on_hours_total'], - flow.model.variables['Sink(Wärme)|on_hours_total'] - == (flow.model.variables['Sink(Wärme)|on'] * model.hours_per_step).sum(), + flow.submodel.variables['Sink(Wärme)|on_hours_total'] + == (flow.submodel.variables['Sink(Wärme)|on'] * model.hours_per_step).sum('time'), ) # Investment - assert_var_equal(model['Sink(Wärme)|size'], model.add_variables(lower=20, upper=200)) + assert_var_equal( + model['Sink(Wärme)|size'], + model.add_variables(lower=20, upper=200, coords=model.get_coords(['period', 'scenario'])), + ) mega = 0.2 * 200 # Relative minimum * maximum size assert_conequal( - model.constraints['Sink(Wärme)|lb_Sink(Wärme)|flow_rate'], - flow.model.variables['Sink(Wärme)|flow_rate'] - >= flow.model.variables['Sink(Wärme)|on'] * mega + flow.model.variables['Sink(Wärme)|size'] * 0.2 - mega, + model.constraints['Sink(Wärme)|flow_rate|lb2'], + flow.submodel.variables['Sink(Wärme)|flow_rate'] + >= flow.submodel.variables['Sink(Wärme)|on'] * mega + + flow.submodel.variables['Sink(Wärme)|size'] * 0.2 + - mega, ) assert_conequal( - model.constraints['Sink(Wärme)|ub_Sink(Wärme)|flow_rate'], - flow.model.variables['Sink(Wärme)|flow_rate'] <= flow.model.variables['Sink(Wärme)|size'] * 0.8, + model.constraints['Sink(Wärme)|flow_rate|ub2'], + flow.submodel.variables['Sink(Wärme)|flow_rate'] <= flow.submodel.variables['Sink(Wärme)|size'] * 0.8, ) class TestFlowWithFixedProfile: """Test Flow with fixed relative profile.""" - def test_fixed_relative_profile(self, basic_flow_system_linopy): + def test_fixed_relative_profile(self, basic_flow_system_linopy_coords, coords_config): """Test flow with a fixed relative profile.""" - flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config + timesteps = flow_system.timesteps # Create a time-varying profile (e.g., for a load or renewable generation) profile = np.sin(np.linspace(0, 2 * np.pi, len(timesteps))) * 0.5 + 0.5 # Values between 0 and 1 flow = fx.Flow( - 'Wärme', bus='Fernwärme', size=100, fixed_relative_profile=xr.DataArray(profile, coords=(timesteps,)) + 'Wärme', + bus='Fernwärme', + size=100, + fixed_relative_profile=profile, ) - flow_system.add_elements(fx.Sink('Sink', sink=flow)) + flow_system.add_elements(fx.Sink('Sink', inputs=[flow])) model = create_linopy_model(flow_system) assert_var_equal( - flow.model.variables['Sink(Wärme)|flow_rate'], - model.add_variables(lower=profile * 100, upper=profile * 100, coords=(timesteps,)), + flow.submodel.variables['Sink(Wärme)|flow_rate'], + model.add_variables( + lower=flow.fixed_relative_profile * 100, + upper=flow.fixed_relative_profile * 100, + coords=model.get_coords(), + ), ) - def test_fixed_profile_with_investment(self, basic_flow_system_linopy): + def test_fixed_profile_with_investment(self, basic_flow_system_linopy_coords, coords_config): """Test flow with fixed profile and investment.""" - flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config + timesteps = flow_system.timesteps # Create a fixed profile profile = np.sin(np.linspace(0, 2 * np.pi, len(timesteps))) * 0.5 + 0.5 @@ -1132,23 +1302,23 @@ def test_fixed_profile_with_investment(self, basic_flow_system_linopy): flow = fx.Flow( 'Wärme', bus='Fernwärme', - size=fx.InvestParameters(minimum_size=50, maximum_size=200, optional=True), - fixed_relative_profile=xr.DataArray(profile, coords=(timesteps,)), + size=fx.InvestParameters(minimum_size=50, maximum_size=200, mandatory=False), + fixed_relative_profile=profile, ) - flow_system.add_elements(fx.Sink('Sink', sink=flow)) + flow_system.add_elements(fx.Sink('Sink', inputs=[flow])) model = create_linopy_model(flow_system) assert_var_equal( - flow.model.variables['Sink(Wärme)|flow_rate'], - model.add_variables(lower=0, upper=profile * 200, coords=(timesteps,)), + flow.submodel.variables['Sink(Wärme)|flow_rate'], + model.add_variables(lower=0, upper=flow.fixed_relative_profile * 200, coords=model.get_coords()), ) # The constraint should link flow_rate to size * profile assert_conequal( - model.constraints['Sink(Wärme)|fix_Sink(Wärme)|flow_rate'], - flow.model.variables['Sink(Wärme)|flow_rate'] - == flow.model.variables['Sink(Wärme)|size'] * xr.DataArray(profile, coords=(timesteps,)), + model.constraints['Sink(Wärme)|flow_rate|fixed'], + flow.submodel.variables['Sink(Wärme)|flow_rate'] + == flow.submodel.variables['Sink(Wärme)|size'] * flow.fixed_relative_profile, ) diff --git a/tests/test_functional.py b/tests/test_functional.py index 5db83f656..0f9fe02ef 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -73,9 +73,9 @@ def flow_system_base(timesteps: pd.DatetimeIndex) -> fx.FlowSystem: flow_system.add_elements( fx.Sink( label='Wärmelast', - sink=fx.Flow(label='Wärme', bus='Fernwärme', fixed_relative_profile=data.thermal_demand, size=1), + inputs=[fx.Flow(label='Wärme', bus='Fernwärme', fixed_relative_profile=data.thermal_demand, size=1)], ), - fx.Source(label='Gastarif', source=fx.Flow(label='Gas', bus='Gas', effects_per_flow_hour=1)), + fx.Source(label='Gastarif', outputs=[fx.Flow(label='Gas', bus='Gas', effects_per_flow_hour=1)]), ) return flow_system @@ -112,7 +112,7 @@ def test_solve_and_load(solver_fixture, time_steps_fixture): def test_minimal_model(solver_fixture, time_steps_fixture): results = solve_and_load(flow_system_minimal(time_steps_fixture), solver_fixture) - assert_allclose(results.model.variables['costs|total'].solution.values, 80, rtol=1e-5, atol=1e-10) + assert_allclose(results.model.variables['costs'].solution.values, 80, rtol=1e-5, atol=1e-10) assert_allclose( results.model.variables['Boiler(Q_th)|flow_rate'].solution.values, @@ -122,14 +122,14 @@ def test_minimal_model(solver_fixture, time_steps_fixture): ) assert_allclose( - results.model.variables['costs(operation)|total_per_timestep'].solution.values, + results.model.variables['costs(temporal)|per_timestep'].solution.values, [-0.0, 20.0, 40.0, -0.0, 20.0], rtol=1e-5, atol=1e-10, ) assert_allclose( - results.model.variables['Gastarif(Gas)->costs(operation)'].solution.values, + results.model.variables['Gastarif(Gas)->costs(temporal)'].solution.values, [-0.0, 20.0, 40.0, -0.0, 20.0], rtol=1e-5, atol=1e-10, @@ -146,7 +146,7 @@ def test_fixed_size(solver_fixture, time_steps_fixture): Q_th=fx.Flow( 'Q_th', bus='Fernwärme', - size=fx.InvestParameters(fixed_size=1000, fix_effects=10, specific_effects=1), + size=fx.InvestParameters(fixed_size=1000, effects_of_investment=10, effects_of_investment_per_size=1), ), ) ) @@ -155,25 +155,25 @@ def test_fixed_size(solver_fixture, time_steps_fixture): boiler = flow_system.all_elements['Boiler'] costs = flow_system.effects['costs'] assert_allclose( - costs.model.total.solution.item(), + costs.submodel.total.solution.item(), 80 + 1000 * 1 + 10, rtol=1e-5, atol=1e-10, err_msg='The total costs does not have the right value', ) assert_allclose( - boiler.Q_th.model._investment.size.solution.item(), + boiler.Q_th.submodel._investment.size.solution.item(), 1000, rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__Investment_size" does not have the right value', ) assert_allclose( - boiler.Q_th.model._investment.is_invested.solution.item(), + boiler.Q_th.submodel._investment.invested.solution.item(), 1, rtol=1e-5, atol=1e-10, - err_msg='"Boiler__Q_th__Investment_size" does not have the right value', + err_msg='"Boiler__Q_th__invested" does not have the right value', ) @@ -187,7 +187,7 @@ def test_optimize_size(solver_fixture, time_steps_fixture): Q_th=fx.Flow( 'Q_th', bus='Fernwärme', - size=fx.InvestParameters(fix_effects=10, specific_effects=1), + size=fx.InvestParameters(effects_of_investment=10, effects_of_investment_per_size=1), ), ) ) @@ -196,21 +196,21 @@ def test_optimize_size(solver_fixture, time_steps_fixture): boiler = flow_system.all_elements['Boiler'] costs = flow_system.effects['costs'] assert_allclose( - costs.model.total.solution.item(), + costs.submodel.total.solution.item(), 80 + 20 * 1 + 10, rtol=1e-5, atol=1e-10, err_msg='The total costs does not have the right value', ) assert_allclose( - boiler.Q_th.model._investment.size.solution.item(), + boiler.Q_th.submodel._investment.size.solution.item(), 20, rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__Investment_size" does not have the right value', ) assert_allclose( - boiler.Q_th.model._investment.is_invested.solution.item(), + boiler.Q_th.submodel._investment.invested.solution.item(), 1, rtol=1e-5, atol=1e-10, @@ -228,7 +228,7 @@ def test_size_bounds(solver_fixture, time_steps_fixture): Q_th=fx.Flow( 'Q_th', bus='Fernwärme', - size=fx.InvestParameters(minimum_size=40, fix_effects=10, specific_effects=1), + size=fx.InvestParameters(minimum_size=40, effects_of_investment=10, effects_of_investment_per_size=1), ), ) ) @@ -237,21 +237,21 @@ def test_size_bounds(solver_fixture, time_steps_fixture): boiler = flow_system.all_elements['Boiler'] costs = flow_system.effects['costs'] assert_allclose( - costs.model.total.solution.item(), + costs.submodel.total.solution.item(), 80 + 40 * 1 + 10, rtol=1e-5, atol=1e-10, err_msg='The total costs does not have the right value', ) assert_allclose( - boiler.Q_th.model._investment.size.solution.item(), + boiler.Q_th.submodel._investment.size.solution.item(), 40, rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__Investment_size" does not have the right value', ) assert_allclose( - boiler.Q_th.model._investment.is_invested.solution.item(), + boiler.Q_th.submodel._investment.invested.solution.item(), 1, rtol=1e-5, atol=1e-10, @@ -269,7 +269,9 @@ def test_optional_invest(solver_fixture, time_steps_fixture): Q_th=fx.Flow( 'Q_th', bus='Fernwärme', - size=fx.InvestParameters(optional=True, minimum_size=40, fix_effects=10, specific_effects=1), + size=fx.InvestParameters( + mandatory=False, minimum_size=40, effects_of_investment=10, effects_of_investment_per_size=1 + ), ), ), fx.linear_converters.Boiler( @@ -279,7 +281,9 @@ def test_optional_invest(solver_fixture, time_steps_fixture): Q_th=fx.Flow( 'Q_th', bus='Fernwärme', - size=fx.InvestParameters(optional=True, minimum_size=50, fix_effects=10, specific_effects=1), + size=fx.InvestParameters( + mandatory=False, minimum_size=50, effects_of_investment=10, effects_of_investment_per_size=1 + ), ), ), ) @@ -289,21 +293,21 @@ def test_optional_invest(solver_fixture, time_steps_fixture): boiler_optional = flow_system.all_elements['Boiler_optional'] costs = flow_system.effects['costs'] assert_allclose( - costs.model.total.solution.item(), + costs.submodel.total.solution.item(), 80 + 40 * 1 + 10, rtol=1e-5, atol=1e-10, err_msg='The total costs does not have the right value', ) assert_allclose( - boiler.Q_th.model._investment.size.solution.item(), + boiler.Q_th.submodel._investment.size.solution.item(), 40, rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__Investment_size" does not have the right value', ) assert_allclose( - boiler.Q_th.model._investment.is_invested.solution.item(), + boiler.Q_th.submodel._investment.invested.solution.item(), 1, rtol=1e-5, atol=1e-10, @@ -311,74 +315,20 @@ def test_optional_invest(solver_fixture, time_steps_fixture): ) assert_allclose( - boiler_optional.Q_th.model._investment.size.solution.item(), + boiler_optional.Q_th.submodel._investment.size.solution.item(), 0, rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__Investment_size" does not have the right value', ) assert_allclose( - boiler_optional.Q_th.model._investment.is_invested.solution.item(), + boiler_optional.Q_th.submodel._investment.invested.solution.item(), 0, rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__IsInvested" does not have the right value', ) - def test_fixed_relative_profile(self): - self.flow_system = self.create_model(self.datetime_array) - self.flow_system.add_elements( - fx.linear_converters.Boiler( - 'Boiler', - 0.5, - Q_fu=fx.Flow('Q_fu', bus=self.get_element('Gas')), - Q_th=fx.Flow( - 'Q_th', - bus=self.get_element('Fernwärme'), - size=fx.InvestParameters(optional=True, minimum_size=40, fix_effects=10, specific_effects=1), - ), - ), - fx.linear_converters.Boiler( - 'Boiler_optional', - 0.5, - Q_fu=fx.Flow('Q_fu', bus=self.get_element('Gas')), - Q_th=fx.Flow( - 'Q_th', - bus=self.get_element('Fernwärme'), - size=fx.InvestParameters(optional=True, minimum_size=50, fix_effects=10, specific_effects=1), - ), - ), - ) - self.flow_system.add_elements( - fx.Source( - 'Wärmequelle', - source=fx.Flow( - 'Q_th', - bus=self.get_element('Fernwärme'), - fixed_relative_profile=np.linspace(0, 5, len(self.datetime_array)), - size=fx.InvestParameters(optional=False, minimum_size=2, maximum_size=5), - ), - ) - ) - self.get_element('Fernwärme').excess_penalty_per_flow_hour = 1e5 - - self.solve_and_load(self.flow_system) - source = self.get_element('Wärmequelle') - assert_allclose( - source.source.model.flow_rate.result, - np.linspace(0, 5, len(self.datetime_array)) * source.source.model._investment.size.result, - rtol=self.mip_gap, - atol=1e-10, - err_msg='The total costs does not have the right value', - ) - assert_allclose( - source.source.model._investment.size.result, - 2, - rtol=self.mip_gap, - atol=1e-10, - err_msg='The total costs does not have the right value', - ) - def test_on(solver_fixture, time_steps_fixture): """Tests if the On Variable is correctly created and calculated in a Flow""" @@ -396,7 +346,7 @@ def test_on(solver_fixture, time_steps_fixture): boiler = flow_system.all_elements['Boiler'] costs = flow_system.effects['costs'] assert_allclose( - costs.model.total.solution.item(), + costs.submodel.total.solution.item(), 80, rtol=1e-5, atol=1e-10, @@ -404,14 +354,14 @@ def test_on(solver_fixture, time_steps_fixture): ) assert_allclose( - boiler.Q_th.model.on_off.on.solution.values, + boiler.Q_th.submodel.on_off.on.solution.values, [0, 1, 1, 0, 1], rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__on" does not have the right value', ) assert_allclose( - boiler.Q_th.model.flow_rate.solution.values, + boiler.Q_th.submodel.flow_rate.solution.values, [0, 10, 20, 0, 10], rtol=1e-5, atol=1e-10, @@ -440,7 +390,7 @@ def test_off(solver_fixture, time_steps_fixture): boiler = flow_system.all_elements['Boiler'] costs = flow_system.effects['costs'] assert_allclose( - costs.model.total.solution.item(), + costs.submodel.total.solution.item(), 80, rtol=1e-5, atol=1e-10, @@ -448,21 +398,21 @@ def test_off(solver_fixture, time_steps_fixture): ) assert_allclose( - boiler.Q_th.model.on_off.on.solution.values, + boiler.Q_th.submodel.on_off.on.solution.values, [0, 1, 1, 0, 1], rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__on" does not have the right value', ) assert_allclose( - boiler.Q_th.model.on_off.off.solution.values, - 1 - boiler.Q_th.model.on_off.on.solution.values, + boiler.Q_th.submodel.on_off.off.solution.values, + 1 - boiler.Q_th.submodel.on_off.on.solution.values, rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__off" does not have the right value', ) assert_allclose( - boiler.Q_th.model.flow_rate.solution.values, + boiler.Q_th.submodel.flow_rate.solution.values, [0, 10, 20, 0, 10], rtol=1e-5, atol=1e-10, @@ -491,7 +441,7 @@ def test_switch_on_off(solver_fixture, time_steps_fixture): boiler = flow_system.all_elements['Boiler'] costs = flow_system.effects['costs'] assert_allclose( - costs.model.total.solution.item(), + costs.submodel.total.solution.item(), 80, rtol=1e-5, atol=1e-10, @@ -499,28 +449,28 @@ def test_switch_on_off(solver_fixture, time_steps_fixture): ) assert_allclose( - boiler.Q_th.model.on_off.on.solution.values, + boiler.Q_th.submodel.on_off.on.solution.values, [0, 1, 1, 0, 1], rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__on" does not have the right value', ) assert_allclose( - boiler.Q_th.model.on_off.switch_on.solution.values, + boiler.Q_th.submodel.on_off.switch_on.solution.values, [0, 1, 0, 0, 1], rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__switch_on" does not have the right value', ) assert_allclose( - boiler.Q_th.model.on_off.switch_off.solution.values, + boiler.Q_th.submodel.on_off.switch_off.solution.values, [0, 0, 0, 1, 0], rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__switch_on" does not have the right value', ) assert_allclose( - boiler.Q_th.model.flow_rate.solution.values, + boiler.Q_th.submodel.flow_rate.solution.values, [0, 10, 20, 0, 10], rtol=1e-5, atol=1e-10, @@ -555,7 +505,7 @@ def test_on_total_max(solver_fixture, time_steps_fixture): boiler = flow_system.all_elements['Boiler'] costs = flow_system.effects['costs'] assert_allclose( - costs.model.total.solution.item(), + costs.submodel.total.solution.item(), 140, rtol=1e-5, atol=1e-10, @@ -563,14 +513,14 @@ def test_on_total_max(solver_fixture, time_steps_fixture): ) assert_allclose( - boiler.Q_th.model.on_off.on.solution.values, + boiler.Q_th.submodel.on_off.on.solution.values, [0, 0, 1, 0, 0], rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__on" does not have the right value', ) assert_allclose( - boiler.Q_th.model.flow_rate.solution.values, + boiler.Q_th.submodel.flow_rate.solution.values, [0, 0, 20, 0, 0], rtol=1e-5, atol=1e-10, @@ -605,7 +555,7 @@ def test_on_total_bounds(solver_fixture, time_steps_fixture): ), ), ) - flow_system.all_elements['Wärmelast'].sink.fixed_relative_profile = np.array( + flow_system.all_elements['Wärmelast'].inputs[0].fixed_relative_profile = np.array( [0, 10, 20, 0, 12] ) # Else its non deterministic @@ -614,7 +564,7 @@ def test_on_total_bounds(solver_fixture, time_steps_fixture): boiler_backup = flow_system.all_elements['Boiler_backup'] costs = flow_system.effects['costs'] assert_allclose( - costs.model.total.solution.item(), + costs.submodel.total.solution.item(), 114, rtol=1e-5, atol=1e-10, @@ -622,14 +572,14 @@ def test_on_total_bounds(solver_fixture, time_steps_fixture): ) assert_allclose( - boiler.Q_th.model.on_off.on.solution.values, + boiler.Q_th.submodel.on_off.on.solution.values, [0, 0, 1, 0, 1], rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__on" does not have the right value', ) assert_allclose( - boiler.Q_th.model.flow_rate.solution.values, + boiler.Q_th.submodel.flow_rate.solution.values, [0, 0, 20, 0, 12 - 1e-5], rtol=1e-5, atol=1e-10, @@ -637,14 +587,14 @@ def test_on_total_bounds(solver_fixture, time_steps_fixture): ) assert_allclose( - sum(boiler_backup.Q_th.model.on_off.on.solution.values), + sum(boiler_backup.Q_th.submodel.on_off.on.solution.values), 3, rtol=1e-5, atol=1e-10, err_msg='"Boiler_backup__Q_th__on" does not have the right value', ) assert_allclose( - boiler_backup.Q_th.model.flow_rate.solution.values, + boiler_backup.Q_th.submodel.flow_rate.solution.values, [0, 10, 1.0e-05, 0, 1.0e-05], rtol=1e-5, atol=1e-10, @@ -674,7 +624,7 @@ def test_consecutive_on_off(solver_fixture, time_steps_fixture): Q_th=fx.Flow('Q_th', bus='Fernwärme', size=100), ), ) - flow_system.all_elements['Wärmelast'].sink.fixed_relative_profile = np.array([5, 10, 20, 18, 12]) + flow_system.all_elements['Wärmelast'].inputs[0].fixed_relative_profile = np.array([5, 10, 20, 18, 12]) # Else its non deterministic solve_and_load(flow_system, solver_fixture) @@ -682,7 +632,7 @@ def test_consecutive_on_off(solver_fixture, time_steps_fixture): boiler_backup = flow_system.all_elements['Boiler_backup'] costs = flow_system.effects['costs'] assert_allclose( - costs.model.total.solution.item(), + costs.submodel.total.solution.item(), 190, rtol=1e-5, atol=1e-10, @@ -690,14 +640,14 @@ def test_consecutive_on_off(solver_fixture, time_steps_fixture): ) assert_allclose( - boiler.Q_th.model.on_off.on.solution.values, + boiler.Q_th.submodel.on_off.on.solution.values, [1, 1, 0, 1, 1], rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__on" does not have the right value', ) assert_allclose( - boiler.Q_th.model.flow_rate.solution.values, + boiler.Q_th.submodel.flow_rate.solution.values, [5, 10, 0, 18, 12], rtol=1e-5, atol=1e-10, @@ -705,7 +655,7 @@ def test_consecutive_on_off(solver_fixture, time_steps_fixture): ) assert_allclose( - boiler_backup.Q_th.model.flow_rate.solution.values, + boiler_backup.Q_th.submodel.flow_rate.solution.values, [0, 0, 20, 0, 0], rtol=1e-5, atol=1e-10, @@ -736,7 +686,7 @@ def test_consecutive_off(solver_fixture, time_steps_fixture): ), ), ) - flow_system.all_elements['Wärmelast'].sink.fixed_relative_profile = np.array( + flow_system.all_elements['Wärmelast'].inputs[0].fixed_relative_profile = np.array( [5, 0, 20, 18, 12] ) # Else its non deterministic @@ -745,7 +695,7 @@ def test_consecutive_off(solver_fixture, time_steps_fixture): boiler_backup = flow_system.all_elements['Boiler_backup'] costs = flow_system.effects['costs'] assert_allclose( - costs.model.total.solution.item(), + costs.submodel.total.solution.item(), 110, rtol=1e-5, atol=1e-10, @@ -753,21 +703,21 @@ def test_consecutive_off(solver_fixture, time_steps_fixture): ) assert_allclose( - boiler_backup.Q_th.model.on_off.on.solution.values, + boiler_backup.Q_th.submodel.on_off.on.solution.values, [0, 0, 1, 0, 0], rtol=1e-5, atol=1e-10, err_msg='"Boiler_backup__Q_th__on" does not have the right value', ) assert_allclose( - boiler_backup.Q_th.model.on_off.off.solution.values, + boiler_backup.Q_th.submodel.on_off.off.solution.values, [1, 1, 0, 1, 1], rtol=1e-5, atol=1e-10, err_msg='"Boiler_backup__Q_th__off" does not have the right value', ) assert_allclose( - boiler_backup.Q_th.model.flow_rate.solution.values, + boiler_backup.Q_th.submodel.flow_rate.solution.values, [0, 0, 1e-5, 0, 0], rtol=1e-5, atol=1e-10, @@ -775,7 +725,7 @@ def test_consecutive_off(solver_fixture, time_steps_fixture): ) assert_allclose( - boiler.Q_th.model.flow_rate.solution.values, + boiler.Q_th.submodel.flow_rate.solution.values, [5, 0, 20 - 1e-5, 18, 12], rtol=1e-5, atol=1e-10, diff --git a/tests/test_integration.py b/tests/test_integration.py index 42fb5f0b7..6e5da63d6 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -20,12 +20,12 @@ def test_simple_flow_system(self, simple_flow_system, highs_solver): # Cost assertions assert_almost_equal_numeric( - effects['costs'].model.total.solution.item(), 81.88394666666667, 'costs doesnt match expected value' + effects['costs'].submodel.total.solution.item(), 81.88394666666667, 'costs doesnt match expected value' ) # CO2 assertions assert_almost_equal_numeric( - effects['CO2'].model.total.solution.item(), 255.09184, 'CO2 doesnt match expected value' + effects['CO2'].submodel.total.solution.item(), 255.09184, 'CO2 doesnt match expected value' ) def test_model_components(self, simple_flow_system, highs_solver): @@ -37,14 +37,14 @@ def test_model_components(self, simple_flow_system, highs_solver): # Boiler assertions assert_almost_equal_numeric( - comps['Boiler'].Q_th.model.flow_rate.solution.values, + comps['Boiler'].Q_th.submodel.flow_rate.solution.values, [0, 0, 0, 28.4864, 35, 0, 0, 0, 0], 'Q_th doesnt match expected value', ) # CHP unit assertions assert_almost_equal_numeric( - comps['CHP_unit'].Q_th.model.flow_rate.solution.values, + comps['CHP_unit'].Q_th.submodel.flow_rate.solution.values, [30.0, 26.66666667, 75.0, 75.0, 75.0, 20.0, 20.0, 20.0, 20.0], 'Q_th doesnt match expected value', ) @@ -63,114 +63,11 @@ def test_results_persistence(self, simple_flow_system, highs_solver): # Verify key variables from loaded results assert_almost_equal_numeric( - results.solution['costs|total'].values, + results.solution['costs'].values, 81.88394666666667, 'costs doesnt match expected value', ) - assert_almost_equal_numeric(results.solution['CO2|total'].values, 255.09184, 'CO2 doesnt match expected value') - - -class TestComponents: - def test_transmission_basic(self, basic_flow_system, highs_solver): - """Test basic transmission functionality""" - flow_system = basic_flow_system - flow_system.add_elements(fx.Bus('Wärme lokal')) - - boiler = fx.linear_converters.Boiler( - 'Boiler', eta=0.5, Q_th=fx.Flow('Q_th', bus='Wärme lokal'), Q_fu=fx.Flow('Q_fu', bus='Gas') - ) - - transmission = fx.Transmission( - 'Rohr', - relative_losses=0.2, - absolute_losses=20, - in1=fx.Flow('Rohr1', 'Wärme lokal', size=fx.InvestParameters(specific_effects=5, maximum_size=1e6)), - out1=fx.Flow('Rohr2', 'Fernwärme', size=1000), - ) - - flow_system.add_elements(transmission, boiler) - - _ = create_calculation_and_solve(flow_system, highs_solver, 'test_transmission_basic') - - # Assertions - assert_almost_equal_numeric( - transmission.in1.model.on_off.on.solution.values, - np.array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1]), - 'On does not work properly', - ) - - assert_almost_equal_numeric( - transmission.in1.model.flow_rate.solution.values * 0.8 - 20, - transmission.out1.model.flow_rate.solution.values, - 'Losses are not computed correctly', - ) - - def test_transmission_advanced(self, basic_flow_system, highs_solver): - """Test advanced transmission functionality""" - flow_system = basic_flow_system - flow_system.add_elements(fx.Bus('Wärme lokal')) - - boiler = fx.linear_converters.Boiler( - 'Boiler_Standard', - eta=0.9, - Q_th=fx.Flow('Q_th', bus='Fernwärme', relative_maximum=np.array([0, 0, 0, 1, 1, 1, 1, 1, 1, 1])), - Q_fu=fx.Flow('Q_fu', bus='Gas'), - ) - - boiler2 = fx.linear_converters.Boiler( - 'Boiler_backup', eta=0.4, Q_th=fx.Flow('Q_th', bus='Wärme lokal'), Q_fu=fx.Flow('Q_fu', bus='Gas') - ) - - last2 = fx.Sink( - 'Wärmelast2', - sink=fx.Flow( - 'Q_th_Last', - bus='Wärme lokal', - size=1, - fixed_relative_profile=flow_system.components['Wärmelast'].sink.fixed_relative_profile - * np.array([0, 0, 0, 0, 0, 1, 1, 1, 1, 1]), - ), - ) - - transmission = fx.Transmission( - 'Rohr', - relative_losses=0.2, - absolute_losses=20, - in1=fx.Flow('Rohr1a', bus='Wärme lokal', size=fx.InvestParameters(specific_effects=5, maximum_size=1000)), - out1=fx.Flow('Rohr1b', 'Fernwärme', size=1000), - in2=fx.Flow('Rohr2a', 'Fernwärme', size=1000), - out2=fx.Flow('Rohr2b', bus='Wärme lokal', size=1000), - ) - - flow_system.add_elements(transmission, boiler, boiler2, last2) - - calculation = create_calculation_and_solve(flow_system, highs_solver, 'test_transmission_advanced') - - # Assertions - assert_almost_equal_numeric( - transmission.in1.model.on_off.on.solution.values, - np.array([1, 1, 1, 0, 0, 0, 0, 0, 0, 0]), - 'On does not work properly', - ) - - assert_almost_equal_numeric( - calculation.results.model.variables['Rohr(Rohr1b)|flow_rate'].solution.values, - transmission.out1.model.flow_rate.solution.values, - 'Flow rate of Rohr__Rohr1b is not correct', - ) - - assert_almost_equal_numeric( - transmission.in1.model.flow_rate.solution.values * 0.8 - - np.array([20 if val > 0.1 else 0 for val in transmission.in1.model.flow_rate.solution.values]), - transmission.out1.model.flow_rate.solution.values, - 'Losses are not computed correctly', - ) - - assert_almost_equal_numeric( - transmission.in1.model._investment.size.solution.item(), - transmission.in2.model._investment.size.solution.item(), - 'The Investments are not equated correctly', - ) + assert_almost_equal_numeric(results.solution['CO2'].values, 255.09184, 'CO2 doesnt match expected value') class TestComplex: @@ -179,13 +76,13 @@ def test_basic_flow_system(self, flow_system_base, highs_solver): # Assertions assert_almost_equal_numeric( - calculation.results.model['costs|total'].solution.item(), + calculation.results.model['costs'].solution.item(), -11597.873624489237, 'costs doesnt match expected value', ) assert_almost_equal_numeric( - calculation.results.model['costs(operation)|total_per_timestep'].solution.values, + calculation.results.model['costs(temporal)|per_timestep'].solution.values, [ -2.38500000e03, -2.21681333e03, @@ -201,55 +98,55 @@ def test_basic_flow_system(self, flow_system_base, highs_solver): ) assert_almost_equal_numeric( - sum(calculation.results.model['CO2(operation)->costs(operation)'].solution.values), + sum(calculation.results.model['CO2(temporal)->costs(temporal)'].solution.values), 258.63729669618675, 'costs doesnt match expected value', ) assert_almost_equal_numeric( - sum(calculation.results.model['Kessel(Q_th)->costs(operation)'].solution.values), + sum(calculation.results.model['Kessel(Q_th)->costs(temporal)'].solution.values), 0.01, 'costs doesnt match expected value', ) assert_almost_equal_numeric( - sum(calculation.results.model['Kessel->costs(operation)'].solution.values), + sum(calculation.results.model['Kessel->costs(temporal)'].solution.values), -0.0, 'costs doesnt match expected value', ) assert_almost_equal_numeric( - sum(calculation.results.model['Gastarif(Q_Gas)->costs(operation)'].solution.values), + sum(calculation.results.model['Gastarif(Q_Gas)->costs(temporal)'].solution.values), 39.09153113079115, 'costs doesnt match expected value', ) assert_almost_equal_numeric( - sum(calculation.results.model['Einspeisung(P_el)->costs(operation)'].solution.values), + sum(calculation.results.model['Einspeisung(P_el)->costs(temporal)'].solution.values), -14196.61245231646, 'costs doesnt match expected value', ) assert_almost_equal_numeric( - sum(calculation.results.model['KWK->costs(operation)'].solution.values), + sum(calculation.results.model['KWK->costs(temporal)'].solution.values), 0.0, 'costs doesnt match expected value', ) assert_almost_equal_numeric( - calculation.results.model['Kessel(Q_th)->costs(invest)'].solution.values, + calculation.results.model['Kessel(Q_th)->costs(periodic)'].solution.values, 1000 + 500, 'costs doesnt match expected value', ) assert_almost_equal_numeric( - calculation.results.model['Speicher->costs(invest)'].solution.values, + calculation.results.model['Speicher->costs(periodic)'].solution.values, 800 + 1, 'costs doesnt match expected value', ) assert_almost_equal_numeric( - calculation.results.model['CO2(operation)|total'].solution.values, + calculation.results.model['CO2(temporal)'].solution.values, 1293.1864834809337, 'CO2 doesnt match expected value', ) assert_almost_equal_numeric( - calculation.results.model['CO2(invest)|total'].solution.values, + calculation.results.model['CO2(periodic)'].solution.values, 0.9999999999999994, 'CO2 doesnt match expected value', ) @@ -317,38 +214,38 @@ def test_piecewise_conversion(self, flow_system_piecewise_conversion, highs_solv # Compare expected values with actual values assert_almost_equal_numeric( - effects['costs'].model.total.solution.item(), -10710.997365760755, 'costs doesnt match expected value' + effects['costs'].submodel.total.solution.item(), -10710.997365760755, 'costs doesnt match expected value' ) assert_almost_equal_numeric( - effects['CO2'].model.total.solution.item(), 1278.7939026086956, 'CO2 doesnt match expected value' + effects['CO2'].submodel.total.solution.item(), 1278.7939026086956, 'CO2 doesnt match expected value' ) assert_almost_equal_numeric( - comps['Kessel'].Q_th.model.flow_rate.solution.values, + comps['Kessel'].Q_th.submodel.flow_rate.solution.values, [0, 0, 0, 45, 0, 0, 0, 0, 0], 'Kessel doesnt match expected value', ) kwk_flows = {flow.label: flow for flow in comps['KWK'].inputs + comps['KWK'].outputs} assert_almost_equal_numeric( - kwk_flows['Q_th'].model.flow_rate.solution.values, + kwk_flows['Q_th'].submodel.flow_rate.solution.values, [45.0, 45.0, 64.5962087, 100.0, 61.3136, 45.0, 45.0, 12.86469565, 0.0], 'KWK Q_th doesnt match expected value', ) assert_almost_equal_numeric( - kwk_flows['P_el'].model.flow_rate.solution.values, + kwk_flows['P_el'].submodel.flow_rate.solution.values, [40.0, 40.0, 47.12589407, 60.0, 45.93221818, 40.0, 40.0, 10.91784108, -0.0], 'KWK P_el doesnt match expected value', ) assert_almost_equal_numeric( - comps['Speicher'].model.netto_discharge.solution.values, + comps['Speicher'].submodel.netto_discharge.solution.values, [-15.0, -45.0, 25.4037913, -35.0, 48.6864, -25.0, -25.0, 7.13530435, 20.0], 'Speicher nettoFlow doesnt match expected value', ) assert_almost_equal_numeric( - comps['Speicher'].model.variables['Speicher|PiecewiseEffects|costs'].solution.values, + comps['Speicher'].submodel.variables['Speicher|PiecewiseEffects|costs'].solution.values, 454.74666666666667, - 'Speicher investCosts_segmented_costs doesnt match expected value', + 'Speicher investcosts_segmented_costs doesnt match expected value', ) @@ -407,17 +304,23 @@ def test_modeling_types_costs(self, modeling_calculation): if modeling_type in ['full', 'aggregated']: assert_almost_equal_numeric( - calc.results.model['costs|total'].solution.item(), + calc.results.model['costs'].solution.item(), expected_costs[modeling_type], - f'Costs do not match for {modeling_type} modeling type', + f'costs do not match for {modeling_type} modeling type', ) else: assert_almost_equal_numeric( - calc.results.solution_without_overlap('costs(operation)|total_per_timestep').sum(), + calc.results.solution_without_overlap('costs(temporal)|per_timestep').sum(), expected_costs[modeling_type], - f'Costs do not match for {modeling_type} modeling type', + f'costs do not match for {modeling_type} modeling type', ) + def test_segmented_io(self, modeling_calculation): + calc, modeling_type = modeling_calculation + if modeling_type == 'segmented': + calc.results.to_file() + _ = fx.results.SegmentedCalculationResults.from_file(calc.folder, calc.name) + if __name__ == '__main__': pytest.main(['-v']) diff --git a/tests/test_invest_parameters_deprecation.py b/tests/test_invest_parameters_deprecation.py new file mode 100644 index 000000000..438d7f4b8 --- /dev/null +++ b/tests/test_invest_parameters_deprecation.py @@ -0,0 +1,344 @@ +""" +Test backward compatibility and deprecation warnings for InvestParameters. + +This test verifies that: +1. Old parameter names (fix_effects, specific_effects, divest_effects, piecewise_effects) still work with warnings +2. New parameter names (effects_of_investment, effects_of_investment_per_size, effects_of_retirement, piecewise_effects_of_investment) work correctly +3. Both old and new approaches produce equivalent results +""" + +import warnings + +import pytest + +from flixopt.interface import InvestParameters + + +class TestInvestParametersDeprecation: + """Test suite for InvestParameters parameter deprecation.""" + + def test_new_parameters_no_warnings(self): + """Test that new parameter names don't trigger warnings.""" + with warnings.catch_warnings(): + warnings.simplefilter('error', DeprecationWarning) + # Should not raise DeprecationWarning + params = InvestParameters( + fixed_size=100, + effects_of_investment={'cost': 25000}, + effects_of_investment_per_size={'cost': 1200}, + effects_of_retirement={'cost': 5000}, + ) + assert params.effects_of_investment == {'cost': 25000} + assert params.effects_of_investment_per_size == {'cost': 1200} + assert params.effects_of_retirement == {'cost': 5000} + + def test_old_fix_effects_deprecation_warning(self): + """Test that fix_effects triggers deprecation warning.""" + with pytest.warns(DeprecationWarning, match='fix_effects.*deprecated.*effects_of_investment'): + params = InvestParameters(fix_effects={'cost': 25000}) + # Verify backward compatibility + assert params.effects_of_investment == {'cost': 25000} + + # Accessing the property also triggers warning + with pytest.warns(DeprecationWarning, match='fix_effects.*deprecated.*effects_of_investment'): + assert params.fix_effects == {'cost': 25000} + + def test_old_specific_effects_deprecation_warning(self): + """Test that specific_effects triggers deprecation warning.""" + with pytest.warns(DeprecationWarning, match='specific_effects.*deprecated.*effects_of_investment_per_size'): + params = InvestParameters(specific_effects={'cost': 1200}) + # Verify backward compatibility + assert params.effects_of_investment_per_size == {'cost': 1200} + + # Accessing the property also triggers warning + with pytest.warns(DeprecationWarning, match='specific_effects.*deprecated.*effects_of_investment_per_size'): + assert params.specific_effects == {'cost': 1200} + + def test_old_divest_effects_deprecation_warning(self): + """Test that divest_effects triggers deprecation warning.""" + with pytest.warns(DeprecationWarning, match='divest_effects.*deprecated.*effects_of_retirement'): + params = InvestParameters(divest_effects={'cost': 5000}) + # Verify backward compatibility + assert params.effects_of_retirement == {'cost': 5000} + + # Accessing the property also triggers warning + with pytest.warns(DeprecationWarning, match='divest_effects.*deprecated.*effects_of_retirement'): + assert params.divest_effects == {'cost': 5000} + + def test_old_piecewise_effects_deprecation_warning(self): + """Test that piecewise_effects triggers deprecation warning.""" + from flixopt.interface import Piece, Piecewise, PiecewiseEffects + + test_piecewise = PiecewiseEffects( + piecewise_origin=Piecewise([Piece(0, 100)]), + piecewise_shares={'cost': Piecewise([Piece(800, 600)])}, + ) + with pytest.warns(DeprecationWarning, match='piecewise_effects.*deprecated.*piecewise_effects_of_investment'): + params = InvestParameters(piecewise_effects=test_piecewise) + # Verify backward compatibility + assert params.piecewise_effects_of_investment is test_piecewise + + # Accessing the property also triggers warning + with pytest.warns(DeprecationWarning, match='piecewise_effects.*deprecated.*piecewise_effects_of_investment'): + assert params.piecewise_effects is test_piecewise + + def test_all_old_parameters_together(self): + """Test all old parameters work together with warnings.""" + from flixopt.interface import Piece, Piecewise, PiecewiseEffects + + test_piecewise = PiecewiseEffects( + piecewise_origin=Piecewise([Piece(0, 100)]), + piecewise_shares={'cost': Piecewise([Piece(800, 600)])}, + ) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter('always', DeprecationWarning) + params = InvestParameters( + fixed_size=100, + fix_effects={'cost': 25000}, + specific_effects={'cost': 1200}, + divest_effects={'cost': 5000}, + piecewise_effects=test_piecewise, + ) + + # Should trigger 4 deprecation warnings (from kwargs) + assert len([warning for warning in w if issubclass(warning.category, DeprecationWarning)]) == 4 + + # Verify all mappings work (accessing new properties - no warnings) + assert params.effects_of_investment == {'cost': 25000} + assert params.effects_of_investment_per_size == {'cost': 1200} + assert params.effects_of_retirement == {'cost': 5000} + assert params.piecewise_effects_of_investment is test_piecewise + + # Verify old attributes still work (accessing deprecated properties - triggers warnings) + with pytest.warns(DeprecationWarning): + assert params.fix_effects == {'cost': 25000} + with pytest.warns(DeprecationWarning): + assert params.specific_effects == {'cost': 1200} + with pytest.warns(DeprecationWarning): + assert params.divest_effects == {'cost': 5000} + with pytest.warns(DeprecationWarning): + assert params.piecewise_effects is test_piecewise + + def test_both_old_and_new_raises_error(self): + """Test that specifying both old and new parameter names raises ValueError.""" + # fix_effects + effects_of_investment + with pytest.raises( + ValueError, match='Either fix_effects or effects_of_investment can be specified, but not both' + ): + InvestParameters( + fix_effects={'cost': 10000}, + effects_of_investment={'cost': 25000}, + ) + + # specific_effects + effects_of_investment_per_size + with pytest.raises( + ValueError, + match='Either specific_effects or effects_of_investment_per_size can be specified, but not both', + ): + InvestParameters( + specific_effects={'cost': 1200}, + effects_of_investment_per_size={'cost': 1500}, + ) + + # divest_effects + effects_of_retirement + with pytest.raises( + ValueError, match='Either divest_effects or effects_of_retirement can be specified, but not both' + ): + InvestParameters( + divest_effects={'cost': 5000}, + effects_of_retirement={'cost': 6000}, + ) + + # piecewise_effects + piecewise_effects_of_investment + from flixopt.interface import Piece, Piecewise, PiecewiseEffects + + test_piecewise1 = PiecewiseEffects( + piecewise_origin=Piecewise([Piece(0, 100)]), + piecewise_shares={'cost': Piecewise([Piece(800, 600)])}, + ) + test_piecewise2 = PiecewiseEffects( + piecewise_origin=Piecewise([Piece(0, 200)]), + piecewise_shares={'cost': Piecewise([Piece(900, 700)])}, + ) + with pytest.raises( + ValueError, + match='Either piecewise_effects or piecewise_effects_of_investment can be specified, but not both', + ): + InvestParameters( + piecewise_effects=test_piecewise1, + piecewise_effects_of_investment=test_piecewise2, + ) + + def test_piecewise_effects_of_investment_new_parameter(self): + """Test that piecewise_effects_of_investment works correctly.""" + from flixopt.interface import Piece, Piecewise, PiecewiseEffects + + test_piecewise = PiecewiseEffects( + piecewise_origin=Piecewise([Piece(0, 100)]), + piecewise_shares={'cost': Piecewise([Piece(800, 600)])}, + ) + + with warnings.catch_warnings(): + warnings.simplefilter('error', DeprecationWarning) + # Should not raise DeprecationWarning when using new parameter + params = InvestParameters(piecewise_effects_of_investment=test_piecewise) + assert params.piecewise_effects_of_investment is test_piecewise + + # Accessing deprecated property triggers warning + with pytest.warns(DeprecationWarning): + assert params.piecewise_effects is test_piecewise + + def test_backward_compatibility_with_features(self): + """Test that old attribute names remain accessible for features.py compatibility.""" + from flixopt.interface import Piece, Piecewise, PiecewiseEffects + + test_piecewise = PiecewiseEffects( + piecewise_origin=Piecewise([Piece(0, 100)]), + piecewise_shares={'cost': Piecewise([Piece(800, 600)])}, + ) + + params = InvestParameters( + effects_of_investment={'cost': 25000}, + effects_of_investment_per_size={'cost': 1200}, + effects_of_retirement={'cost': 5000}, + piecewise_effects_of_investment=test_piecewise, + ) + + # Old properties should still be accessible (for features.py) but with warnings + with pytest.warns(DeprecationWarning): + assert params.fix_effects == {'cost': 25000} + with pytest.warns(DeprecationWarning): + assert params.specific_effects == {'cost': 1200} + with pytest.warns(DeprecationWarning): + assert params.divest_effects == {'cost': 5000} + with pytest.warns(DeprecationWarning): + assert params.piecewise_effects is test_piecewise + + # Properties should return the same objects as the new attributes + with pytest.warns(DeprecationWarning): + assert params.fix_effects is params.effects_of_investment + with pytest.warns(DeprecationWarning): + assert params.specific_effects is params.effects_of_investment_per_size + with pytest.warns(DeprecationWarning): + assert params.divest_effects is params.effects_of_retirement + with pytest.warns(DeprecationWarning): + assert params.piecewise_effects is params.piecewise_effects_of_investment + + def test_empty_parameters(self): + """Test that empty/None parameters work correctly.""" + params = InvestParameters() + + assert params.effects_of_investment == {} + assert params.effects_of_investment_per_size == {} + assert params.effects_of_retirement == {} + assert params.piecewise_effects_of_investment is None + + # Old properties should also be empty (but with warnings) + with pytest.warns(DeprecationWarning): + assert params.fix_effects == {} + with pytest.warns(DeprecationWarning): + assert params.specific_effects == {} + with pytest.warns(DeprecationWarning): + assert params.divest_effects == {} + with pytest.warns(DeprecationWarning): + assert params.piecewise_effects is None + + def test_mixed_old_and_new_parameters(self): + """Test mixing old and new parameter names (not recommended but should work).""" + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter('always', DeprecationWarning) + params = InvestParameters( + effects_of_investment={'cost': 25000}, # New + specific_effects={'cost': 1200}, # Old + effects_of_retirement={'cost': 5000}, # New + ) + + # Should only warn about the old parameter + assert len([warning for warning in w if issubclass(warning.category, DeprecationWarning)]) == 1 + + # All should work correctly + assert params.effects_of_investment == {'cost': 25000} + assert params.effects_of_investment_per_size == {'cost': 1200} + assert params.effects_of_retirement == {'cost': 5000} + + def test_unexpected_keyword_arguments(self): + """Test that unexpected keyword arguments raise TypeError.""" + # Single unexpected argument + with pytest.raises( + TypeError, match="InvestParameters.__init__\\(\\) got unexpected keyword argument\\(s\\): 'invalid_param'" + ): + InvestParameters(invalid_param='value') + + # Multiple unexpected arguments + with pytest.raises( + TypeError, + match="InvestParameters.__init__\\(\\) got unexpected keyword argument\\(s\\): 'param1', 'param2'", + ): + InvestParameters(param1='value1', param2='value2') + + # Mix of valid and invalid arguments + with pytest.raises( + TypeError, match="InvestParameters.__init__\\(\\) got unexpected keyword argument\\(s\\): 'typo'" + ): + InvestParameters(effects_of_investment={'cost': 100}, typo='value') + + def test_optional_parameter_deprecation(self): + """Test that optional parameter triggers deprecation warning and maps to mandatory.""" + # Test optional=True (should map to mandatory=False) + with pytest.warns(DeprecationWarning, match='optional.*deprecated.*mandatory'): + params = InvestParameters(optional=True) + assert params.mandatory is False + + # Test optional=False (should map to mandatory=True) + with pytest.warns(DeprecationWarning, match='optional.*deprecated.*mandatory'): + params = InvestParameters(optional=False) + assert params.mandatory is True + + def test_mandatory_parameter_no_warning(self): + """Test that mandatory parameter doesn't trigger warnings.""" + with warnings.catch_warnings(): + warnings.simplefilter('error', DeprecationWarning) + # Test mandatory=True + params = InvestParameters(mandatory=True) + assert params.mandatory is True + + # Test mandatory=False (explicit) + params = InvestParameters(mandatory=False) + assert params.mandatory is False + + def test_mandatory_default_value(self): + """Test that default value of mandatory is False when neither optional nor mandatory is specified.""" + params = InvestParameters() + assert params.mandatory is False + + def test_both_optional_and_mandatory_no_error(self): + """Test that specifying both optional and mandatory doesn't raise error. + + Note: Conflict checking is disabled for mandatory/optional because mandatory has + a non-None default value (False), making it impossible to distinguish between + an explicit mandatory=False and the default value. The deprecated optional + parameter will take precedence when both are specified. + """ + # When both are specified, optional takes precedence (with deprecation warning) + with pytest.warns(DeprecationWarning, match='optional.*deprecated.*mandatory'): + params = InvestParameters(optional=True, mandatory=False) + # optional=True should result in mandatory=False + assert params.mandatory is False + + with pytest.warns(DeprecationWarning, match='optional.*deprecated.*mandatory'): + params = InvestParameters(optional=False, mandatory=True) + # optional=False should result in mandatory=True (optional takes precedence) + assert params.mandatory is True + + def test_optional_property_deprecation(self): + """Test that accessing optional property triggers deprecation warning.""" + params = InvestParameters(mandatory=True) + + # Reading the property triggers warning + with pytest.warns(DeprecationWarning, match="Property 'optional' is deprecated"): + assert params.optional is False + + # Setting the property triggers warning + with pytest.warns(DeprecationWarning, match="Property 'optional' is deprecated"): + params.optional = True + assert params.mandatory is False diff --git a/tests/test_io.py b/tests/test_io.py index 2ec74955f..f5ca2174a 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -9,10 +9,19 @@ flow_system_long, flow_system_segments_of_flows_2, simple_flow_system, + simple_flow_system_scenarios, ) -@pytest.fixture(params=[flow_system_base, flow_system_segments_of_flows_2, simple_flow_system, flow_system_long]) +@pytest.fixture( + params=[ + flow_system_base, + simple_flow_system_scenarios, + flow_system_segments_of_flows_2, + simple_flow_system, + flow_system_long, + ] +) def flow_system(request): fs = request.getfixturevalue(request.param.__name__) if isinstance(fs, fx.FlowSystem): @@ -26,6 +35,7 @@ def test_flow_system_file_io(flow_system, highs_solver): calculation_0 = fx.FullCalculation('IO', flow_system=flow_system) calculation_0.do_modeling() calculation_0.solve(highs_solver) + calculation_0.flow_system.plot_network() calculation_0.results.to_file() paths = CalculationResultsPaths(calculation_0.folder, calculation_0.name) @@ -34,6 +44,7 @@ def test_flow_system_file_io(flow_system, highs_solver): calculation_1 = fx.FullCalculation('Loaded_IO', flow_system=flow_system_1) calculation_1.do_modeling() calculation_1.solve(highs_solver) + calculation_1.flow_system.plot_network() assert_almost_equal_numeric( calculation_0.results.model.objective.value, @@ -42,18 +53,19 @@ def test_flow_system_file_io(flow_system, highs_solver): ) assert_almost_equal_numeric( - calculation_0.results.solution['costs|total'].values, - calculation_1.results.solution['costs|total'].values, + calculation_0.results.solution['costs'].values, + calculation_1.results.solution['costs'].values, 'costs doesnt match expected value', ) def test_flow_system_io(flow_system): - di = flow_system.as_dict() - _ = fx.FlowSystem.from_dict(di) + flow_system.to_json('fs.json') + + ds = flow_system.to_dataset() + new_fs = fx.FlowSystem.from_dataset(ds) - ds = flow_system.as_dataset() - _ = fx.FlowSystem.from_dataset(ds) + assert flow_system == new_fs print(flow_system) flow_system.__repr__() diff --git a/tests/test_linear_converter.py b/tests/test_linear_converter.py index 93ace3e78..1884c8d72 100644 --- a/tests/test_linear_converter.py +++ b/tests/test_linear_converter.py @@ -10,9 +10,9 @@ class TestLinearConverterModel: """Test the LinearConverterModel class.""" - def test_basic_linear_converter(self, basic_flow_system_linopy): + def test_basic_linear_converter(self, basic_flow_system_linopy_coords, coords_config): """Test basic initialization and modeling of a LinearConverter.""" - flow_system = basic_flow_system_linopy + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config # Create input and output flows input_flow = fx.Flow('input', bus='input_bus', size=100) @@ -40,13 +40,13 @@ def test_basic_linear_converter(self, basic_flow_system_linopy): # Check conversion constraint (input * 0.8 == output * 1.0) assert_conequal( model.constraints['Converter|conversion_0'], - input_flow.model.flow_rate * 0.8 == output_flow.model.flow_rate * 1.0, + input_flow.submodel.flow_rate * 0.8 == output_flow.submodel.flow_rate * 1.0, ) - def test_linear_converter_time_varying(self, basic_flow_system_linopy): + def test_linear_converter_time_varying(self, basic_flow_system_linopy_coords, coords_config): """Test a LinearConverter with time-varying conversion factors.""" - flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config + timesteps = flow_system.timesteps # Create time-varying efficiency (e.g., temperature-dependent) varying_efficiency = np.linspace(0.7, 0.9, len(timesteps)) @@ -78,12 +78,12 @@ def test_linear_converter_time_varying(self, basic_flow_system_linopy): # Check conversion constraint (input * efficiency_series == output * 1.0) assert_conequal( model.constraints['Converter|conversion_0'], - input_flow.model.flow_rate * efficiency_series == output_flow.model.flow_rate * 1.0, + input_flow.submodel.flow_rate * efficiency_series == output_flow.submodel.flow_rate * 1.0, ) - def test_linear_converter_multiple_factors(self, basic_flow_system_linopy): + def test_linear_converter_multiple_factors(self, basic_flow_system_linopy_coords, coords_config): """Test a LinearConverter with multiple conversion factors.""" - flow_system = basic_flow_system_linopy + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config # Create flows input_flow1 = fx.Flow('input1', bus='input_bus1', size=100) @@ -119,24 +119,24 @@ def test_linear_converter_multiple_factors(self, basic_flow_system_linopy): # Check conversion constraint 1 (input1 * 0.8 == output1 * 1.0) assert_conequal( model.constraints['Converter|conversion_0'], - input_flow1.model.flow_rate * 0.8 == output_flow1.model.flow_rate * 1.0, + input_flow1.submodel.flow_rate * 0.8 == output_flow1.submodel.flow_rate * 1.0, ) # Check conversion constraint 2 (input2 * 0.5 == output2 * 1.0) assert_conequal( model.constraints['Converter|conversion_1'], - input_flow2.model.flow_rate * 0.5 == output_flow2.model.flow_rate * 1.0, + input_flow2.submodel.flow_rate * 0.5 == output_flow2.submodel.flow_rate * 1.0, ) # Check conversion constraint 3 (input1 * 0.2 == output2 * 0.3) assert_conequal( model.constraints['Converter|conversion_2'], - input_flow1.model.flow_rate * 0.2 == output_flow2.model.flow_rate * 0.3, + input_flow1.submodel.flow_rate * 0.2 == output_flow2.submodel.flow_rate * 0.3, ) - def test_linear_converter_with_on_off(self, basic_flow_system_linopy): + def test_linear_converter_with_on_off(self, basic_flow_system_linopy_coords, coords_config): """Test a LinearConverter with OnOffParameters.""" - flow_system = basic_flow_system_linopy + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config # Create input and output flows input_flow = fx.Flow('input', bus='input_bus', size=100) @@ -144,7 +144,7 @@ def test_linear_converter_with_on_off(self, basic_flow_system_linopy): # Create OnOffParameters on_off_params = fx.OnOffParameters( - on_hours_total_min=10, on_hours_total_max=40, effects_per_running_hour={'Costs': 5} + on_hours_total_min=10, on_hours_total_max=40, effects_per_running_hour={'costs': 5} ) # Create a linear converter with OnOffParameters @@ -173,27 +173,26 @@ def test_linear_converter_with_on_off(self, basic_flow_system_linopy): # Check on_hours_total constraint assert_conequal( model.constraints['Converter|on_hours_total'], - converter.model.on_off.variables['Converter|on_hours_total'] - == (converter.model.on_off.variables['Converter|on'] * model.hours_per_step).sum(), + model.variables['Converter|on_hours_total'] + == (model.variables['Converter|on'] * model.hours_per_step).sum('time'), ) # Check conversion constraint assert_conequal( model.constraints['Converter|conversion_0'], - input_flow.model.flow_rate * 0.8 == output_flow.model.flow_rate * 1.0, + input_flow.submodel.flow_rate * 0.8 == output_flow.submodel.flow_rate * 1.0, ) # Check on_off effects - assert 'Converter->Costs(operation)' in model.constraints + assert 'Converter->costs(temporal)' in model.constraints assert_conequal( - model.constraints['Converter->Costs(operation)'], - model.variables['Converter->Costs(operation)'] - == converter.model.on_off.variables['Converter|on'] * model.hours_per_step * 5, + model.constraints['Converter->costs(temporal)'], + model.variables['Converter->costs(temporal)'] == model.variables['Converter|on'] * model.hours_per_step * 5, ) - def test_linear_converter_multidimensional(self, basic_flow_system_linopy): + def test_linear_converter_multidimensional(self, basic_flow_system_linopy_coords, coords_config): """Test LinearConverter with multiple inputs, outputs, and connections between them.""" - flow_system = basic_flow_system_linopy + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config # Create a more complex setup with multiple flows input_flow1 = fx.Flow('fuel', bus='fuel_bus', size=100) @@ -232,23 +231,23 @@ def test_linear_converter_multidimensional(self, basic_flow_system_linopy): # Check the conversion equations assert_conequal( model.constraints['MultiConverter|conversion_0'], - input_flow1.model.flow_rate * 0.7 == output_flow1.model.flow_rate * 1.0, + input_flow1.submodel.flow_rate * 0.7 == output_flow1.submodel.flow_rate * 1.0, ) assert_conequal( model.constraints['MultiConverter|conversion_1'], - input_flow2.model.flow_rate * 0.3 == output_flow2.model.flow_rate * 1.0, + input_flow2.submodel.flow_rate * 0.3 == output_flow2.submodel.flow_rate * 1.0, ) assert_conequal( model.constraints['MultiConverter|conversion_2'], - input_flow1.model.flow_rate * 0.1 == output_flow2.model.flow_rate * 0.5, + input_flow1.submodel.flow_rate * 0.1 == output_flow2.submodel.flow_rate * 0.5, ) - def test_edge_case_time_varying_conversion(self, basic_flow_system_linopy): + def test_edge_case_time_varying_conversion(self, basic_flow_system_linopy_coords, coords_config): """Test edge case with extreme time-varying conversion factors.""" - flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config + timesteps = flow_system.timesteps # Create fluctuating conversion efficiency (e.g., for a heat pump) # Values range from very low (0.1) to very high (5.0) @@ -280,16 +279,19 @@ def test_edge_case_time_varying_conversion(self, basic_flow_system_linopy): # Check that the correct constraint was created assert 'VariableConverter|conversion_0' in model.constraints + factor = converter.conversion_factors[0]['electricity'] + + assert factor.dims == tuple(model.get_coords()) + # Verify the constraint has the time-varying coefficient assert_conequal( model.constraints['VariableConverter|conversion_0'], - input_flow.model.flow_rate * fluctuating_cop == output_flow.model.flow_rate * 1.0, + input_flow.submodel.flow_rate * factor == output_flow.submodel.flow_rate * 1.0, ) - def test_piecewise_conversion(self, basic_flow_system_linopy): + def test_piecewise_conversion(self, basic_flow_system_linopy_coords, coords_config): """Test a LinearConverter with PiecewiseConversion.""" - flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config # Create input and output flows input_flow = fx.Flow('input', bus='input_bus', size=100) @@ -318,11 +320,11 @@ def test_piecewise_conversion(self, basic_flow_system_linopy): # Create model with the piecewise conversion model = create_linopy_model(flow_system) - # Verify that PiecewiseModel was created and added as a sub_model - assert converter.model.piecewise_conversion is not None + # Verify that PiecewiseModel was created and added as a submodel + assert converter.submodel.piecewise_conversion is not None # Get the PiecewiseModel instance - piecewise_model = converter.model.piecewise_conversion + piecewise_model = converter.submodel.piecewise_conversion # Check that we have the expected pieces (2 in this case) assert len(piecewise_model.pieces) == 2 @@ -337,9 +339,9 @@ def test_piecewise_conversion(self, basic_flow_system_linopy): lambda1 = model.variables[f'Converter|Piece_{i}|lambda1'] inside_piece = model.variables[f'Converter|Piece_{i}|inside_piece'] - assert_var_equal(inside_piece, model.add_variables(binary=True, coords=(timesteps,))) - assert_var_equal(lambda0, model.add_variables(lower=0, upper=1, coords=(timesteps,))) - assert_var_equal(lambda1, model.add_variables(lower=0, upper=1, coords=(timesteps,))) + assert_var_equal(inside_piece, model.add_variables(binary=True, coords=model.get_coords())) + assert_var_equal(lambda0, model.add_variables(lower=0, upper=1, coords=model.get_coords())) + assert_var_equal(lambda1, model.add_variables(lower=0, upper=1, coords=model.get_coords())) # Check that the inside_piece constraint exists assert f'Converter|Piece_{i}|inside_piece' in model.constraints @@ -375,10 +377,9 @@ def test_piecewise_conversion(self, basic_flow_system_linopy): <= 1, ) - def test_piecewise_conversion_with_onoff(self, basic_flow_system_linopy): + def test_piecewise_conversion_with_onoff(self, basic_flow_system_linopy_coords, coords_config): """Test a LinearConverter with PiecewiseConversion and OnOffParameters.""" - flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config # Create input and output flows input_flow = fx.Flow('input', bus='input_bus', size=100) @@ -396,7 +397,7 @@ def test_piecewise_conversion_with_onoff(self, basic_flow_system_linopy): # Create OnOffParameters on_off_params = fx.OnOffParameters( - on_hours_total_min=10, on_hours_total_max=40, effects_per_running_hour={'Costs': 5} + on_hours_total_min=10, on_hours_total_max=40, effects_per_running_hour={'costs': 5} ) # Create a linear converter with piecewise conversion and on/off parameters @@ -418,11 +419,11 @@ def test_piecewise_conversion_with_onoff(self, basic_flow_system_linopy): # Create model with the piecewise conversion model = create_linopy_model(flow_system) - # Verify that PiecewiseModel was created and added as a sub_model - assert converter.model.piecewise_conversion is not None + # Verify that PiecewiseModel was created and added as a submodel + assert converter.submodel.piecewise_conversion is not None # Get the PiecewiseModel instance - piecewise_model = converter.model.piecewise_conversion + piecewise_model = converter.submodel.piecewise_conversion # Check that we have the expected pieces (2 in this case) assert len(piecewise_model.pieces) == 2 @@ -442,9 +443,9 @@ def test_piecewise_conversion_with_onoff(self, basic_flow_system_linopy): lambda1 = model.variables[f'Converter|Piece_{i}|lambda1'] inside_piece = model.variables[f'Converter|Piece_{i}|inside_piece'] - assert_var_equal(inside_piece, model.add_variables(binary=True, coords=(timesteps,))) - assert_var_equal(lambda0, model.add_variables(lower=0, upper=1, coords=(timesteps,))) - assert_var_equal(lambda1, model.add_variables(lower=0, upper=1, coords=(timesteps,))) + assert_var_equal(inside_piece, model.add_variables(binary=True, coords=model.get_coords())) + assert_var_equal(lambda0, model.add_variables(lower=0, upper=1, coords=model.get_coords())) + assert_var_equal(lambda1, model.add_variables(lower=0, upper=1, coords=model.get_coords())) # Check that the inside_piece constraint exists assert f'Converter|Piece_{i}|inside_piece' in model.constraints @@ -483,16 +484,14 @@ def test_piecewise_conversion_with_onoff(self, basic_flow_system_linopy): assert 'Converter|on_hours_total' in model.constraints assert_conequal( model.constraints['Converter|on_hours_total'], - converter.model.on_off.variables['Converter|on_hours_total'] - == (converter.model.on_off.variables['Converter|on'] * model.hours_per_step).sum(), + model['Converter|on_hours_total'] == (model['Converter|on'] * model.hours_per_step).sum('time'), ) # Verify that the costs effect is applied - assert 'Converter->Costs(operation)' in model.constraints + assert 'Converter->costs(temporal)' in model.constraints assert_conequal( - model.constraints['Converter->Costs(operation)'], - model.variables['Converter->Costs(operation)'] - == converter.model.on_off.variables['Converter|on'] * model.hours_per_step * 5, + model.constraints['Converter->costs(temporal)'], + model.variables['Converter->costs(temporal)'] == model.variables['Converter|on'] * model.hours_per_step * 5, ) diff --git a/tests/test_on_hours_computation.py b/tests/test_on_hours_computation.py index cd0e637d0..578fd7792 100644 --- a/tests/test_on_hours_computation.py +++ b/tests/test_on_hours_computation.py @@ -1,108 +1,99 @@ import numpy as np import pytest +import xarray as xr -from flixopt.features import ConsecutiveStateModel, StateModel +from flixopt.modeling import ModelingUtilities class TestComputeConsecutiveDuration: - """Tests for the compute_consecutive_duration static method.""" + """Tests for the compute_consecutive_hours_in_state static method.""" @pytest.mark.parametrize( 'binary_values, hours_per_timestep, expected', [ - # Case 1: Both scalar inputs - (1, 5, 5), - (0, 3, 0), - # Case 2: Scalar binary, array hours - (1, np.array([1, 2, 3]), 3), - (0, np.array([2, 4, 6]), 0), - # Case 3: Array binary, scalar hours - (np.array([0, 0, 1, 1, 1, 0]), 2, 0), - (np.array([0, 1, 1, 0, 1, 1]), 1, 2), - (np.array([1, 1, 1]), 2, 6), - # Case 4: Both array inputs - (np.array([0, 1, 1, 0, 1, 1]), np.array([1, 2, 3, 4, 5, 6]), 11), # 5+6 - (np.array([1, 0, 0, 1, 1, 1]), np.array([2, 2, 2, 3, 4, 5]), 12), # 3+4+5 - # Case 5: Edge cases - (np.array([1]), np.array([4]), 4), - (np.array([0]), np.array([3]), 0), + # Case 1: Single timestep DataArrays + (xr.DataArray([1], dims=['time']), 5, 5), + (xr.DataArray([0], dims=['time']), 3, 0), + # Case 2: Array binary, scalar hours + (xr.DataArray([0, 0, 1, 1, 1, 0], dims=['time']), 2, 0), + (xr.DataArray([0, 1, 1, 0, 1, 1], dims=['time']), 1, 2), + (xr.DataArray([1, 1, 1], dims=['time']), 2, 6), + # Case 3: Edge cases + (xr.DataArray([1], dims=['time']), 4, 4), + (xr.DataArray([0], dims=['time']), 3, 0), + # Case 4: More complex patterns + (xr.DataArray([1, 0, 0, 1, 1, 1], dims=['time']), 2, 6), # 3 consecutive at end * 2 hours + (xr.DataArray([0, 1, 1, 1, 0, 0], dims=['time']), 1, 0), # ends with 0 ], ) def test_compute_duration(self, binary_values, hours_per_timestep, expected): - """Test compute_consecutive_duration with various inputs.""" - result = ConsecutiveStateModel.compute_consecutive_hours_in_state(binary_values, hours_per_timestep) + """Test compute_consecutive_hours_in_state with various inputs.""" + result = ModelingUtilities.compute_consecutive_hours_in_state(binary_values, hours_per_timestep) assert np.isclose(result, expected) @pytest.mark.parametrize( 'binary_values, hours_per_timestep', [ - # Case: Incompatible array lengths - (np.array([1, 1, 1, 1, 1]), np.array([1, 2])), + # Case: hours_per_timestep must be scalar + (xr.DataArray([1, 1, 1, 1, 1], dims=['time']), np.array([1, 2])), ], ) def test_compute_duration_raises_error(self, binary_values, hours_per_timestep): """Test error conditions.""" - with pytest.raises(ValueError): - ConsecutiveStateModel.compute_consecutive_hours_in_state(binary_values, hours_per_timestep) + with pytest.raises(TypeError): + ModelingUtilities.compute_consecutive_hours_in_state(binary_values, hours_per_timestep) class TestComputePreviousOnStates: - """Tests for the compute_previous_on_states static method.""" + """Tests for the compute_previous_states static method.""" @pytest.mark.parametrize( 'previous_values, expected', [ - # Case 1: Empty list - ([], np.array([0])), - # Case 2: All None values - ([None, None], np.array([0])), - # Case 3: Single value arrays - ([np.array([0])], np.array([0])), - ([np.array([1])], np.array([1])), - ([np.array([0.001])], np.array([1])), # Using default epsilon - ([np.array([1e-4])], np.array([1])), - ([np.array([1e-8])], np.array([0])), - # Case 4: Multiple 1D arrays - ([np.array([0, 5, 0]), np.array([0, 0, 1])], np.array([0, 1, 1])), - ([np.array([0.1, 0, 0.3]), None, np.array([0, 0, 0])], np.array([1, 0, 1])), - ([np.array([0, 0, 0]), np.array([0, 1, 0])], np.array([0, 1, 0])), - ([np.array([0.1, 0, 0]), np.array([0, 0, 0.2])], np.array([1, 0, 1])), - # Case 6: Mix of None and 1D arrays - ([None, np.array([0, 0, 0]), np.array([0, 1, 0]), np.array([0, 0, 0])], np.array([0, 1, 0])), - ([np.array([0, 0, 0]), None, np.array([0, 0, 0]), np.array([0, 0, 0])], np.array([0, 0, 0])), + # Case 1: Single value DataArrays + (xr.DataArray([0], dims=['time']), xr.DataArray([0], dims=['time'])), + (xr.DataArray([1], dims=['time']), xr.DataArray([1], dims=['time'])), + (xr.DataArray([0.001], dims=['time']), xr.DataArray([1], dims=['time'])), # Using default epsilon + (xr.DataArray([1e-4], dims=['time']), xr.DataArray([1], dims=['time'])), + (xr.DataArray([1e-8], dims=['time']), xr.DataArray([0], dims=['time'])), + # Case 1: Multiple timestep DataArrays + (xr.DataArray([0, 5, 0], dims=['time']), xr.DataArray([0, 1, 0], dims=['time'])), + (xr.DataArray([0.1, 0, 0.3], dims=['time']), xr.DataArray([1, 0, 1], dims=['time'])), + (xr.DataArray([0, 0, 0], dims=['time']), xr.DataArray([0, 0, 0], dims=['time'])), + (xr.DataArray([0.1, 0, 0.2], dims=['time']), xr.DataArray([1, 0, 1], dims=['time'])), ], ) def test_compute_previous_on_states(self, previous_values, expected): - """Test compute_previous_on_states with various inputs.""" - result = StateModel.compute_previous_states(previous_values) - np.testing.assert_array_equal(result, expected) + """Test compute_previous_states with various inputs.""" + result = ModelingUtilities.compute_previous_states(previous_values) + xr.testing.assert_equal(result, expected) @pytest.mark.parametrize( 'previous_values, epsilon, expected', [ # Testing with different epsilon values - ([np.array([1e-6, 1e-4, 1e-2])], 1e-3, np.array([0, 0, 1])), - ([np.array([1e-6, 1e-4, 1e-2])], 1e-5, np.array([0, 1, 1])), - ([np.array([1e-6, 1e-4, 1e-2])], 1e-1, np.array([0, 0, 0])), + (xr.DataArray([1e-6, 1e-4, 1e-2], dims=['time']), 1e-3, xr.DataArray([0, 0, 1], dims=['time'])), + (xr.DataArray([1e-6, 1e-4, 1e-2], dims=['time']), 1e-5, xr.DataArray([0, 1, 1], dims=['time'])), + (xr.DataArray([1e-6, 1e-4, 1e-2], dims=['time']), 1e-1, xr.DataArray([0, 0, 0], dims=['time'])), # Mixed case with custom epsilon - ([np.array([0.05, 0.005, 0.0005])], 0.01, np.array([1, 0, 0])), + (xr.DataArray([0.05, 0.005, 0.0005], dims=['time']), 0.01, xr.DataArray([1, 0, 0], dims=['time'])), ], ) def test_compute_previous_on_states_with_epsilon(self, previous_values, epsilon, expected): - """Test compute_previous_on_states with custom epsilon values.""" - result = StateModel.compute_previous_states(previous_values, epsilon) - np.testing.assert_array_equal(result, expected) + """Test compute_previous_states with custom epsilon values.""" + result = ModelingUtilities.compute_previous_states(previous_values, epsilon) + xr.testing.assert_equal(result, expected) @pytest.mark.parametrize( 'previous_values, expected_shape', [ # Check that output shapes match expected dimensions - ([np.array([0, 1, 0, 1])], (4,)), - ([np.array([0, 1]), np.array([1, 0]), np.array([0, 0])], (2,)), - ([np.array([0, 1]), np.array([1, 0])], (2,)), + (xr.DataArray([0, 1, 0, 1], dims=['time']), (4,)), + (xr.DataArray([0, 1], dims=['time']), (2,)), + (xr.DataArray([1, 0], dims=['time']), (2,)), ], ) def test_output_shapes(self, previous_values, expected_shape): """Test that output array has the correct shape.""" - result = StateModel.compute_previous_states(previous_values) + result = ModelingUtilities.compute_previous_states(previous_values) assert result.shape == expected_shape diff --git a/tests/test_plots.py b/tests/test_plots.py index 0c38f760c..61c26c510 100644 --- a/tests/test_plots.py +++ b/tests/test_plots.py @@ -7,7 +7,6 @@ import matplotlib.pyplot as plt import numpy as np import pandas as pd -import plotly import pytest from flixopt import plotting @@ -18,6 +17,14 @@ class TestPlots(unittest.TestCase): def setUp(self): np.random.seed(72) + def tearDown(self): + """Cleanup matplotlib and plotly resources""" + plt.close('all') + # Force garbage collection to cleanup any lingering resources + import gc + + gc.collect() + @staticmethod def get_sample_data( nr_of_columns: int = 7, @@ -51,38 +58,44 @@ def get_sample_data( def test_bar_plots(self): data = self.get_sample_data(nr_of_columns=10, nr_of_periods=1, time_steps_per_period=24) - plotly.offline.plot(plotting.with_plotly(data, 'bar')) - plotting.with_matplotlib(data, 'bar') - plt.show() + # Create plotly figure (json renderer doesn't need .show()) + _ = plotting.with_plotly(data, 'stacked_bar') + plotting.with_matplotlib(data, 'stacked_bar') + plt.savefig(f'test_plot_{self._testMethodName}.png', bbox_inches='tight') + plt.close('all') # Close all figures to prevent memory leaks data = self.get_sample_data( nr_of_columns=10, nr_of_periods=5, time_steps_per_period=24, drop_fraction_of_indices=0.3 ) - plotly.offline.plot(plotting.with_plotly(data, 'bar')) - plotting.with_matplotlib(data, 'bar') - plt.show() + # Create plotly figure (json renderer doesn't need .show()) + _ = plotting.with_plotly(data, 'stacked_bar') + plotting.with_matplotlib(data, 'stacked_bar') + plt.savefig(f'test_plot_{self._testMethodName}.png', bbox_inches='tight') + plt.close('all') # Close all figures to prevent memory leaks def test_line_plots(self): data = self.get_sample_data(nr_of_columns=10, nr_of_periods=1, time_steps_per_period=24) - plotly.offline.plot(plotting.with_plotly(data, 'line')) + _ = plotting.with_plotly(data, 'line') plotting.with_matplotlib(data, 'line') - plt.show() + plt.savefig(f'test_plot_{self._testMethodName}.png', bbox_inches='tight') + plt.close('all') # Close all figures to prevent memory leaks data = self.get_sample_data( nr_of_columns=10, nr_of_periods=5, time_steps_per_period=24, drop_fraction_of_indices=0.3 ) - plotly.offline.plot(plotting.with_plotly(data, 'line')) + _ = plotting.with_plotly(data, 'line') plotting.with_matplotlib(data, 'line') - plt.show() + plt.savefig(f'test_plot_{self._testMethodName}.png', bbox_inches='tight') + plt.close('all') # Close all figures to prevent memory leaks def test_stacked_line_plots(self): data = self.get_sample_data(nr_of_columns=10, nr_of_periods=1, time_steps_per_period=24) - plotly.offline.plot(plotting.with_plotly(data, 'area')) + _ = plotting.with_plotly(data, 'area') data = self.get_sample_data( nr_of_columns=10, nr_of_periods=5, time_steps_per_period=24, drop_fraction_of_indices=0.3 ) - plotly.offline.plot(plotting.with_plotly(data, 'area')) + _ = plotting.with_plotly(data, 'area') def test_heat_map_plots(self): # Generate single-column data with datetime index for heatmap @@ -91,9 +104,10 @@ def test_heat_map_plots(self): # Convert data for heatmap plotting using 'day' as period and 'hour' steps heatmap_data = plotting.reshape_to_2d(data.iloc[:, 0].values.flatten(), 24) # Plotting heatmaps with Plotly and Matplotlib - plotly.offline.plot(plotting.heat_map_plotly(pd.DataFrame(heatmap_data))) + _ = plotting.heat_map_plotly(pd.DataFrame(heatmap_data)) plotting.heat_map_matplotlib(pd.DataFrame(heatmap_data)) - plt.show() + plt.savefig(f'test_plot_{self._testMethodName}.png', bbox_inches='tight') + plt.close('all') # Close all figures to prevent memory leaks def test_heat_map_plots_resampling(self): date_range = pd.date_range(start='2023-01-01', end='2023-03-21', freq='5min') @@ -113,21 +127,24 @@ def test_heat_map_plots_resampling(self): data = df_irregular # Convert data for heatmap plotting using 'day' as period and 'hour' steps heatmap_data = plotting.heat_map_data_from_df(data, 'MS', 'D') - plotly.offline.plot(plotting.heat_map_plotly(heatmap_data)) + _ = plotting.heat_map_plotly(heatmap_data) plotting.heat_map_matplotlib(pd.DataFrame(heatmap_data)) - plt.show() + plt.savefig(f'test_plot_{self._testMethodName}.png', bbox_inches='tight') + plt.close('all') # Close all figures to prevent memory leaks heatmap_data = plotting.heat_map_data_from_df(data, 'W', 'h', fill='ffill') # Plotting heatmaps with Plotly and Matplotlib - plotly.offline.plot(plotting.heat_map_plotly(pd.DataFrame(heatmap_data))) + _ = plotting.heat_map_plotly(pd.DataFrame(heatmap_data)) plotting.heat_map_matplotlib(pd.DataFrame(heatmap_data)) - plt.show() + plt.savefig(f'test_plot_{self._testMethodName}.png', bbox_inches='tight') + plt.close('all') # Close all figures to prevent memory leaks heatmap_data = plotting.heat_map_data_from_df(data, 'D', 'h', fill='ffill') # Plotting heatmaps with Plotly and Matplotlib - plotly.offline.plot(plotting.heat_map_plotly(pd.DataFrame(heatmap_data))) + _ = plotting.heat_map_plotly(pd.DataFrame(heatmap_data)) plotting.heat_map_matplotlib(pd.DataFrame(heatmap_data)) - plt.show() + plt.savefig(f'test_plot_{self._testMethodName}.png', bbox_inches='tight') + plt.close('all') # Close all figures to prevent memory leaks if __name__ == '__main__': diff --git a/tests/test_results_plots.py b/tests/test_results_plots.py index ec50555a3..35a219e31 100644 --- a/tests/test_results_plots.py +++ b/tests/test_results_plots.py @@ -59,12 +59,7 @@ def test_results_plots(flow_system, plotting_engine, show, save, color_spec): ) results['Speicher'].plot_node_balance_pie(engine=plotting_engine, save=save, show=show, colors=color_spec) - - if plotting_engine == 'matplotlib': - with pytest.raises(NotImplementedError): - results['Speicher'].plot_charge_state(engine=plotting_engine) - else: - results['Speicher'].plot_charge_state(engine=plotting_engine) + results['Speicher'].plot_charge_state(engine=plotting_engine) plt.close('all') diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py new file mode 100644 index 000000000..3f0637c91 --- /dev/null +++ b/tests/test_scenarios.py @@ -0,0 +1,692 @@ +import numpy as np +import pandas as pd +import pytest +from linopy.testing import assert_linequal + +import flixopt as fx +from flixopt.commons import Effect, InvestParameters, Sink, Source, Storage +from flixopt.elements import Bus, Flow +from flixopt.flow_system import FlowSystem + +from .conftest import create_calculation_and_solve, create_linopy_model + + +@pytest.fixture +def test_system(): + """Create a basic test system with scenarios.""" + # Create a two-day time index with hourly resolution + timesteps = pd.date_range('2023-01-01', periods=48, freq='h', name='time') + + # Create two scenarios + scenarios = pd.Index(['Scenario A', 'Scenario B'], name='scenario') + + # Create scenario weights + weights = np.array([0.7, 0.3]) + + # Create a flow system with scenarios + flow_system = FlowSystem( + timesteps=timesteps, + scenarios=scenarios, + weights=weights, # Use TimeSeriesData for weights + ) + + # Create demand profiles that differ between scenarios + # Scenario A: Higher demand in first day, lower in second day + # Scenario B: Lower demand in first day, higher in second day + demand_profile_a = np.concatenate( + [ + np.sin(np.linspace(0, 2 * np.pi, 24)) * 5 + 10, # Day 1, max ~15 + np.sin(np.linspace(0, 2 * np.pi, 24)) * 2 + 5, # Day 2, max ~7 + ] + ) + + demand_profile_b = np.concatenate( + [ + np.sin(np.linspace(0, 2 * np.pi, 24)) * 2 + 5, # Day 1, max ~7 + np.sin(np.linspace(0, 2 * np.pi, 24)) * 5 + 10, # Day 2, max ~15 + ] + ) + + # Stack the profiles into a 2D array (time, scenario) + demand_profiles = np.column_stack([demand_profile_a, demand_profile_b]) + + # Create the necessary model elements + # Create buses + electricity_bus = Bus('Electricity') + + # Create a demand sink with scenario-dependent profiles + demand = Flow(label='Demand', bus=electricity_bus.label_full, fixed_relative_profile=demand_profiles) + demand_sink = Sink('Demand', inputs=[demand]) + + # Create a power source with investment option + power_gen = Flow( + label='Generation', + bus=electricity_bus.label_full, + size=InvestParameters( + minimum_size=0, + maximum_size=20, + effects_of_investment_per_size={'costs': 100}, # €/kW + ), + effects_per_flow_hour={'costs': 20}, # €/MWh + ) + generator = Source('Generator', outputs=[power_gen]) + + # Create a storage for electricity + storage_charge = Flow(label='Charge', bus=electricity_bus.label_full, size=10) + storage_discharge = Flow(label='Discharge', bus=electricity_bus.label_full, size=10) + storage = Storage( + label='Battery', + charging=storage_charge, + discharging=storage_discharge, + capacity_in_flow_hours=InvestParameters( + minimum_size=0, + maximum_size=50, + effects_of_investment_per_size={'costs': 50}, # €/kWh + ), + eta_charge=0.95, + eta_discharge=0.95, + initial_charge_state='lastValueOfSim', + ) + + # Create effects and objective + cost_effect = Effect(label='costs', unit='€', description='Total costs', is_standard=True, is_objective=True) + + # Add all elements to the flow system + flow_system.add_elements(electricity_bus, generator, demand_sink, storage, cost_effect) + + # Return the created system and its components + return { + 'flow_system': flow_system, + 'timesteps': timesteps, + 'scenarios': scenarios, + 'electricity_bus': electricity_bus, + 'demand': demand, + 'demand_sink': demand_sink, + 'generator': generator, + 'power_gen': power_gen, + 'storage': storage, + 'storage_charge': storage_charge, + 'storage_discharge': storage_discharge, + 'cost_effect': cost_effect, + } + + +@pytest.fixture +def flow_system_complex_scenarios() -> fx.FlowSystem: + """ + Helper method to create a base model with configurable parameters + """ + thermal_load = np.array([30, 0, 90, 110, 110, 20, 20, 20, 20]) + electrical_load = np.array([40, 40, 40, 40, 40, 40, 40, 40, 40]) + flow_system = fx.FlowSystem( + pd.date_range('2020-01-01', periods=9, freq='h', name='time'), + scenarios=pd.Index(['A', 'B', 'C'], name='scenario'), + ) + # Define the components and flow_system + flow_system.add_elements( + fx.Effect('costs', '€', 'Kosten', is_standard=True, is_objective=True, share_from_temporal={'CO2': 0.2}), + fx.Effect('CO2', 'kg', 'CO2_e-Emissionen'), + fx.Effect('PE', 'kWh_PE', 'Primärenergie', maximum_total=3.5e3), + fx.Bus('Strom'), + fx.Bus('Fernwärme'), + fx.Bus('Gas'), + fx.Sink('Wärmelast', inputs=[fx.Flow('Q_th_Last', 'Fernwärme', size=1, fixed_relative_profile=thermal_load)]), + fx.Source( + 'Gastarif', outputs=[fx.Flow('Q_Gas', 'Gas', size=1000, effects_per_flow_hour={'costs': 0.04, 'CO2': 0.3})] + ), + fx.Sink('Einspeisung', inputs=[fx.Flow('P_el', 'Strom', effects_per_flow_hour=-1 * electrical_load)]), + ) + + boiler = fx.linear_converters.Boiler( + 'Kessel', + eta=0.5, + on_off_parameters=fx.OnOffParameters(effects_per_running_hour={'costs': 0, 'CO2': 1000}), + Q_th=fx.Flow( + 'Q_th', + bus='Fernwärme', + load_factor_max=1.0, + load_factor_min=0.1, + relative_minimum=5 / 50, + relative_maximum=1, + previous_flow_rate=50, + size=fx.InvestParameters( + effects_of_investment=1000, + fixed_size=50, + mandatory=True, + effects_of_investment_per_size={'costs': 10, 'PE': 2}, + ), + on_off_parameters=fx.OnOffParameters( + on_hours_total_min=0, + on_hours_total_max=1000, + consecutive_on_hours_max=10, + consecutive_on_hours_min=1, + consecutive_off_hours_max=10, + effects_per_switch_on=0.01, + switch_on_total_max=1000, + ), + flow_hours_total_max=1e6, + ), + Q_fu=fx.Flow('Q_fu', bus='Gas', size=200, relative_minimum=0, relative_maximum=1), + ) + + invest_speicher = fx.InvestParameters( + effects_of_investment=0, + piecewise_effects_of_investment=fx.PiecewiseEffects( + piecewise_origin=fx.Piecewise([fx.Piece(5, 25), fx.Piece(25, 100)]), + piecewise_shares={ + 'costs': fx.Piecewise([fx.Piece(50, 250), fx.Piece(250, 800)]), + 'PE': fx.Piecewise([fx.Piece(5, 25), fx.Piece(25, 100)]), + }, + ), + mandatory=True, + effects_of_investment_per_size={'costs': 0.01, 'CO2': 0.01}, + minimum_size=0, + maximum_size=1000, + ) + speicher = fx.Storage( + 'Speicher', + charging=fx.Flow('Q_th_load', bus='Fernwärme', size=1e4), + discharging=fx.Flow('Q_th_unload', bus='Fernwärme', size=1e4), + capacity_in_flow_hours=invest_speicher, + initial_charge_state=0, + maximal_final_charge_state=10, + eta_charge=0.9, + eta_discharge=1, + relative_loss_per_hour=0.08, + prevent_simultaneous_charge_and_discharge=True, + ) + + flow_system.add_elements(boiler, speicher) + + return flow_system + + +@pytest.fixture +def flow_system_piecewise_conversion_scenarios(flow_system_complex_scenarios) -> fx.FlowSystem: + """ + Use segments/Piecewise with numeric data + """ + flow_system = flow_system_complex_scenarios + + flow_system.add_elements( + fx.LinearConverter( + 'KWK', + inputs=[fx.Flow('Q_fu', bus='Gas')], + outputs=[ + fx.Flow('P_el', bus='Strom', size=60, relative_maximum=55, previous_flow_rate=10), + fx.Flow('Q_th', bus='Fernwärme'), + ], + piecewise_conversion=fx.PiecewiseConversion( + { + 'P_el': fx.Piecewise( + [ + fx.Piece(np.linspace(5, 6, len(flow_system.timesteps)), 30), + fx.Piece(40, np.linspace(60, 70, len(flow_system.timesteps))), + ] + ), + 'Q_th': fx.Piecewise([fx.Piece(6, 35), fx.Piece(45, 100)]), + 'Q_fu': fx.Piecewise([fx.Piece(12, 70), fx.Piece(90, 200)]), + } + ), + on_off_parameters=fx.OnOffParameters(effects_per_switch_on=0.01), + ) + ) + + return flow_system + + +def test_weights(flow_system_piecewise_conversion_scenarios): + """Test that scenario weights are correctly used in the model.""" + scenarios = flow_system_piecewise_conversion_scenarios.scenarios + weights = np.linspace(0.5, 1, len(scenarios)) + flow_system_piecewise_conversion_scenarios.weights = weights + model = create_linopy_model(flow_system_piecewise_conversion_scenarios) + normalized_weights = ( + flow_system_piecewise_conversion_scenarios.weights / flow_system_piecewise_conversion_scenarios.weights.sum() + ) + np.testing.assert_allclose(model.weights.values, normalized_weights) + assert_linequal( + model.objective.expression, (model.variables['costs'] * normalized_weights).sum() + model.variables['Penalty'] + ) + assert np.isclose(model.weights.sum().item(), 1) + + +def test_weights_io(flow_system_piecewise_conversion_scenarios): + """Test that scenario weights are correctly used in the model.""" + scenarios = flow_system_piecewise_conversion_scenarios.scenarios + weights = np.linspace(0.5, 1, len(scenarios)) / np.sum(np.linspace(0.5, 1, len(scenarios))) + flow_system_piecewise_conversion_scenarios.weights = weights + model = create_linopy_model(flow_system_piecewise_conversion_scenarios) + np.testing.assert_allclose(model.weights.values, weights) + assert_linequal(model.objective.expression, (model.variables['costs'] * weights).sum() + model.variables['Penalty']) + assert np.isclose(model.weights.sum().item(), 1.0) + + +def test_scenario_dimensions_in_variables(flow_system_piecewise_conversion_scenarios): + """Test that all time variables are correctly broadcasted to scenario dimensions.""" + model = create_linopy_model(flow_system_piecewise_conversion_scenarios) + for var in model.variables: + assert model.variables[var].dims in [('time', 'scenario'), ('scenario',), ()] + + +def test_full_scenario_optimization(flow_system_piecewise_conversion_scenarios): + """Test a full optimization with scenarios and verify results.""" + scenarios = flow_system_piecewise_conversion_scenarios.scenarios + weights = np.linspace(0.5, 1, len(scenarios)) / np.sum(np.linspace(0.5, 1, len(scenarios))) + flow_system_piecewise_conversion_scenarios.weights = weights + calc = create_calculation_and_solve( + flow_system_piecewise_conversion_scenarios, + solver=fx.solvers.GurobiSolver(mip_gap=0.01, time_limit_seconds=60), + name='test_full_scenario', + ) + calc.results.to_file() + + res = fx.results.CalculationResults.from_file('results', 'test_full_scenario') + fx.FlowSystem.from_dataset(res.flow_system_data) + calc = create_calculation_and_solve( + flow_system_piecewise_conversion_scenarios, + solver=fx.solvers.GurobiSolver(mip_gap=0.01, time_limit_seconds=60), + name='test_full_scenario', + ) + + +@pytest.mark.skip(reason='This test is taking too long with highs and is too big for gurobipy free') +def test_io_persistance(flow_system_piecewise_conversion_scenarios): + """Test a full optimization with scenarios and verify results.""" + scenarios = flow_system_piecewise_conversion_scenarios.scenarios + weights = np.linspace(0.5, 1, len(scenarios)) / np.sum(np.linspace(0.5, 1, len(scenarios))) + flow_system_piecewise_conversion_scenarios.weights = weights + calc = create_calculation_and_solve( + flow_system_piecewise_conversion_scenarios, + solver=fx.solvers.HighsSolver(mip_gap=0.001, time_limit_seconds=60), + name='test_full_scenario', + ) + calc.results.to_file() + + res = fx.results.CalculationResults.from_file('results', 'test_full_scenario') + flow_system_2 = fx.FlowSystem.from_dataset(res.flow_system_data) + calc_2 = create_calculation_and_solve( + flow_system_2, + solver=fx.solvers.HighsSolver(mip_gap=0.001, time_limit_seconds=60), + name='test_full_scenario_2', + ) + + np.testing.assert_allclose(calc.results.objective, calc_2.results.objective, rtol=0.001) + + +def test_scenarios_selection(flow_system_piecewise_conversion_scenarios): + flow_system_full = flow_system_piecewise_conversion_scenarios + scenarios = flow_system_full.scenarios + weights = np.linspace(0.5, 1, len(scenarios)) / np.sum(np.linspace(0.5, 1, len(scenarios))) + flow_system_full.weights = weights + flow_system = flow_system_full.sel(scenario=scenarios[0:2]) + + assert flow_system.scenarios.equals(flow_system_full.scenarios[0:2]) + + np.testing.assert_allclose(flow_system.weights.values, flow_system_full.weights[0:2]) + + calc = fx.FullCalculation(flow_system=flow_system, name='test_full_scenario', normalize_weights=False) + calc.do_modeling() + calc.solve(fx.solvers.GurobiSolver(mip_gap=0.01, time_limit_seconds=60)) + + calc.results.to_file() + + np.testing.assert_allclose( + calc.results.objective, + ((calc.results.solution['costs'] * flow_system.weights).sum() + calc.results.solution['Penalty']).item(), + ) ## Account for rounding errors + + assert calc.results.solution.indexes['scenario'].equals(flow_system_full.scenarios[0:2]) + + +def test_sizes_per_scenario_default(): + """Test that scenario_independent_sizes defaults to True (sizes equalized) and flow_rates to False (vary).""" + timesteps = pd.date_range('2023-01-01', periods=24, freq='h') + scenarios = pd.Index(['base', 'high'], name='scenario') + + fs = fx.FlowSystem(timesteps=timesteps, scenarios=scenarios) + + assert fs.scenario_independent_sizes is True + assert fs.scenario_independent_flow_rates is False + + +def test_sizes_per_scenario_bool(): + """Test scenario_independent_sizes with boolean values.""" + timesteps = pd.date_range('2023-01-01', periods=24, freq='h') + scenarios = pd.Index(['base', 'high'], name='scenario') + + # Test False (vary per scenario) + fs1 = fx.FlowSystem(timesteps=timesteps, scenarios=scenarios, scenario_independent_sizes=False) + assert fs1.scenario_independent_sizes is False + + # Test True (equalized across scenarios) + fs2 = fx.FlowSystem(timesteps=timesteps, scenarios=scenarios, scenario_independent_sizes=True) + assert fs2.scenario_independent_sizes is True + + +def test_sizes_per_scenario_list(): + """Test scenario_independent_sizes with list of element labels.""" + timesteps = pd.date_range('2023-01-01', periods=24, freq='h') + scenarios = pd.Index(['base', 'high'], name='scenario') + + fs = fx.FlowSystem( + timesteps=timesteps, + scenarios=scenarios, + scenario_independent_sizes=['solar->grid', 'battery->grid'], + ) + + assert fs.scenario_independent_sizes == ['solar->grid', 'battery->grid'] + + +def test_flow_rates_per_scenario_default(): + """Test that scenario_independent_flow_rates defaults to False (flow rates vary by scenario).""" + timesteps = pd.date_range('2023-01-01', periods=24, freq='h') + scenarios = pd.Index(['base', 'high'], name='scenario') + + fs = fx.FlowSystem(timesteps=timesteps, scenarios=scenarios) + + assert fs.scenario_independent_flow_rates is False + + +def test_flow_rates_per_scenario_bool(): + """Test scenario_independent_flow_rates with boolean values.""" + timesteps = pd.date_range('2023-01-01', periods=24, freq='h') + scenarios = pd.Index(['base', 'high'], name='scenario') + + # Test False (vary per scenario) + fs1 = fx.FlowSystem(timesteps=timesteps, scenarios=scenarios, scenario_independent_flow_rates=False) + assert fs1.scenario_independent_flow_rates is False + + # Test True (equalized across scenarios) + fs2 = fx.FlowSystem(timesteps=timesteps, scenarios=scenarios, scenario_independent_flow_rates=True) + assert fs2.scenario_independent_flow_rates is True + + +def test_scenario_parameters_property_setters(): + """Test that scenario parameters can be changed via property setters.""" + timesteps = pd.date_range('2023-01-01', periods=24, freq='h') + scenarios = pd.Index(['base', 'high'], name='scenario') + + fs = fx.FlowSystem(timesteps=timesteps, scenarios=scenarios) + + # Change scenario_independent_sizes + fs.scenario_independent_sizes = True + assert fs.scenario_independent_sizes is True + + fs.scenario_independent_sizes = ['component1', 'component2'] + assert fs.scenario_independent_sizes == ['component1', 'component2'] + + # Change scenario_independent_flow_rates + fs.scenario_independent_flow_rates = True + assert fs.scenario_independent_flow_rates is True + + fs.scenario_independent_flow_rates = ['flow1', 'flow2'] + assert fs.scenario_independent_flow_rates == ['flow1', 'flow2'] + + +def test_scenario_parameters_validation(): + """Test that scenario parameters are validated correctly.""" + timesteps = pd.date_range('2023-01-01', periods=24, freq='h') + scenarios = pd.Index(['base', 'high'], name='scenario') + + fs = fx.FlowSystem(timesteps=timesteps, scenarios=scenarios) + + # Test invalid type + with pytest.raises(TypeError, match='must be bool or list'): + fs.scenario_independent_sizes = 'invalid' + + # Test invalid list content + with pytest.raises(ValueError, match='must contain only strings'): + fs.scenario_independent_sizes = [1, 2, 3] + + +def test_size_equality_constraints(): + """Test that size equality constraints are created when scenario_independent_sizes=True.""" + timesteps = pd.date_range('2023-01-01', periods=24, freq='h') + scenarios = pd.Index(['base', 'high'], name='scenario') + + fs = fx.FlowSystem( + timesteps=timesteps, + scenarios=scenarios, + scenario_independent_sizes=True, # Sizes should be equalized + scenario_independent_flow_rates=False, # Flow rates can vary + ) + + bus = fx.Bus('grid') + source = fx.Source( + label='solar', + outputs=[ + fx.Flow( + label='out', + bus='grid', + size=fx.InvestParameters( + minimum_size=10, + maximum_size=100, + effects_of_investment_per_size={'cost': 100}, + ), + ) + ], + ) + + fs.add_elements(bus, source, fx.Effect('cost', 'Total cost', '€', is_objective=True)) + + calc = fx.FullCalculation('test', fs) + calc.do_modeling() + + # Check that size equality constraint exists + constraint_names = [str(c) for c in calc.model.constraints] + size_constraints = [c for c in constraint_names if 'scenario_independent' in c and 'size' in c] + + assert len(size_constraints) > 0, 'Size equality constraint should exist' + + +def test_flow_rate_equality_constraints(): + """Test that flow_rate equality constraints are created when scenario_independent_flow_rates=True.""" + timesteps = pd.date_range('2023-01-01', periods=24, freq='h') + scenarios = pd.Index(['base', 'high'], name='scenario') + + fs = fx.FlowSystem( + timesteps=timesteps, + scenarios=scenarios, + scenario_independent_sizes=False, # Sizes can vary + scenario_independent_flow_rates=True, # Flow rates should be equalized + ) + + bus = fx.Bus('grid') + source = fx.Source( + label='solar', + outputs=[ + fx.Flow( + label='out', + bus='grid', + size=fx.InvestParameters( + minimum_size=10, + maximum_size=100, + effects_of_investment_per_size={'cost': 100}, + ), + ) + ], + ) + + fs.add_elements(bus, source, fx.Effect('cost', 'Total cost', '€', is_objective=True)) + + calc = fx.FullCalculation('test', fs) + calc.do_modeling() + + # Check that flow_rate equality constraint exists + constraint_names = [str(c) for c in calc.model.constraints] + flow_rate_constraints = [c for c in constraint_names if 'scenario_independent' in c and 'flow_rate' in c] + + assert len(flow_rate_constraints) > 0, 'Flow rate equality constraint should exist' + + +def test_selective_scenario_independence(): + """Test selective scenario independence with specific element lists.""" + timesteps = pd.date_range('2023-01-01', periods=24, freq='h') + scenarios = pd.Index(['base', 'high'], name='scenario') + + fs = fx.FlowSystem( + timesteps=timesteps, + scenarios=scenarios, + scenario_independent_sizes=['solar(out)'], # Only solar size is equalized + scenario_independent_flow_rates=['demand(in)'], # Only demand flow_rate is equalized + ) + + bus = fx.Bus('grid') + source = fx.Source( + label='solar', + outputs=[ + fx.Flow( + label='out', + bus='grid', + size=fx.InvestParameters( + minimum_size=10, maximum_size=100, effects_of_investment_per_size={'cost': 100} + ), + ) + ], + ) + sink = fx.Sink( + label='demand', + inputs=[fx.Flow(label='in', bus='grid', size=50)], + ) + + fs.add_elements(bus, source, sink, fx.Effect('cost', 'Total cost', '€', is_objective=True)) + + calc = fx.FullCalculation('test', fs) + calc.do_modeling() + + constraint_names = [str(c) for c in calc.model.constraints] + + # Solar SHOULD have size constraints (it's in the list, so equalized) + solar_size_constraints = [c for c in constraint_names if 'solar(out)|size' in c and 'scenario_independent' in c] + assert len(solar_size_constraints) > 0 + + # Solar should NOT have flow_rate constraints (not in the list, so varies per scenario) + solar_flow_constraints = [ + c for c in constraint_names if 'solar(out)|flow_rate' in c and 'scenario_independent' in c + ] + assert len(solar_flow_constraints) == 0 + + # Demand should NOT have size constraints (no InvestParameters, size is fixed) + demand_size_constraints = [c for c in constraint_names if 'demand(in)|size' in c and 'scenario_independent' in c] + assert len(demand_size_constraints) == 0 + + # Demand SHOULD have flow_rate constraints (it's in the list, so equalized) + demand_flow_constraints = [ + c for c in constraint_names if 'demand(in)|flow_rate' in c and 'scenario_independent' in c + ] + assert len(demand_flow_constraints) > 0 + + +def test_scenario_parameters_io_persistence(): + """Test that scenario_independent_sizes and scenario_independent_flow_rates persist through IO operations.""" + import shutil + import tempfile + + timesteps = pd.date_range('2023-01-01', periods=24, freq='h') + scenarios = pd.Index(['base', 'high'], name='scenario') + + # Create FlowSystem with custom scenario parameters + fs_original = fx.FlowSystem( + timesteps=timesteps, + scenarios=scenarios, + scenario_independent_sizes=['solar(out)'], + scenario_independent_flow_rates=True, + ) + + bus = fx.Bus('grid') + source = fx.Source( + label='solar', + outputs=[ + fx.Flow( + label='out', + bus='grid', + size=fx.InvestParameters( + minimum_size=10, maximum_size=100, effects_of_investment_per_size={'cost': 100} + ), + ) + ], + ) + + fs_original.add_elements(bus, source, fx.Effect('cost', 'Total cost', '€', is_objective=True)) + + # Save to dataset + fs_original.connect_and_transform() + ds = fs_original.to_dataset() + + # Load from dataset + fs_loaded = fx.FlowSystem.from_dataset(ds) + + # Verify parameters persisted + assert fs_loaded.scenario_independent_sizes == fs_original.scenario_independent_sizes + assert fs_loaded.scenario_independent_flow_rates == fs_original.scenario_independent_flow_rates + + +def test_scenario_parameters_io_with_calculation(): + """Test that scenario parameters persist through full calculation IO.""" + import shutil + import tempfile + + timesteps = pd.date_range('2023-01-01', periods=24, freq='h') + scenarios = pd.Index(['base', 'high'], name='scenario') + + fs = fx.FlowSystem( + timesteps=timesteps, + scenarios=scenarios, + scenario_independent_sizes=True, + scenario_independent_flow_rates=['demand(in)'], + ) + + bus = fx.Bus('grid') + source = fx.Source( + label='solar', + outputs=[ + fx.Flow( + label='out', + bus='grid', + size=fx.InvestParameters( + minimum_size=10, maximum_size=100, effects_of_investment_per_size={'cost': 100} + ), + ) + ], + ) + sink = fx.Sink( + label='demand', + inputs=[fx.Flow(label='in', bus='grid', size=50)], + ) + + fs.add_elements(bus, source, sink, fx.Effect('cost', 'Total cost', '€', is_objective=True)) + + # Create temp directory for results + temp_dir = tempfile.mkdtemp() + + try: + # Solve and save + calc = fx.FullCalculation('test_io', fs, folder=temp_dir) + calc.do_modeling() + calc.solve(fx.solvers.HighsSolver(mip_gap=0.01, time_limit_seconds=60)) + calc.results.to_file() + + # Load results + results = fx.results.CalculationResults.from_file(temp_dir, 'test_io') + fs_loaded = fx.FlowSystem.from_dataset(results.flow_system_data) + + # Verify parameters persisted + assert fs_loaded.scenario_independent_sizes == fs.scenario_independent_sizes + assert fs_loaded.scenario_independent_flow_rates == fs.scenario_independent_flow_rates + + # Verify constraints are recreated correctly + calc2 = fx.FullCalculation('test_io_2', fs_loaded, folder=temp_dir) + calc2.do_modeling() + + constraint_names1 = [str(c) for c in calc.model.constraints] + constraint_names2 = [str(c) for c in calc2.model.constraints] + + size_constraints1 = [c for c in constraint_names1 if 'scenario_independent' in c and 'size' in c] + size_constraints2 = [c for c in constraint_names2 if 'scenario_independent' in c and 'size' in c] + + assert len(size_constraints1) == len(size_constraints2) + + finally: + # Clean up + shutil.rmtree(temp_dir) diff --git a/tests/test_storage.py b/tests/test_storage.py index 5971c2f5c..8d0c495c2 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -1,3 +1,4 @@ +import numpy as np import pytest import flixopt as fx @@ -8,11 +9,9 @@ class TestStorageModel: """Test that storage model variables and constraints are correctly generated.""" - def test_basic_storage(self, basic_flow_system_linopy): + def test_basic_storage(self, basic_flow_system_linopy_coords, coords_config): """Test that basic storage model variables and constraints are correctly generated.""" - flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps - timesteps_extra = flow_system.time_series_collection.timesteps_extra + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config # Create a simple storage storage = fx.Storage( @@ -52,13 +51,14 @@ def test_basic_storage(self, basic_flow_system_linopy): # Check variable properties assert_var_equal( - model['TestStorage(Q_th_in)|flow_rate'], model.add_variables(lower=0, upper=20, coords=(timesteps,)) + model['TestStorage(Q_th_in)|flow_rate'], model.add_variables(lower=0, upper=20, coords=model.get_coords()) ) assert_var_equal( - model['TestStorage(Q_th_out)|flow_rate'], model.add_variables(lower=0, upper=20, coords=(timesteps,)) + model['TestStorage(Q_th_out)|flow_rate'], model.add_variables(lower=0, upper=20, coords=model.get_coords()) ) assert_var_equal( - model['TestStorage|charge_state'], model.add_variables(lower=0, upper=30, coords=(timesteps_extra,)) + model['TestStorage|charge_state'], + model.add_variables(lower=0, upper=30, coords=model.get_coords(extra_timestep=True)), ) # Check constraint formulations @@ -82,11 +82,9 @@ def test_basic_storage(self, basic_flow_system_linopy): model.variables['TestStorage|charge_state'].isel(time=0) == 0, ) - def test_lossy_storage(self, basic_flow_system_linopy): + def test_lossy_storage(self, basic_flow_system_linopy_coords, coords_config): """Test that basic storage model variables and constraints are correctly generated.""" - flow_system = basic_flow_system_linopy - timesteps = flow_system.time_series_collection.timesteps - timesteps_extra = flow_system.time_series_collection.timesteps_extra + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config # Create a simple storage storage = fx.Storage( @@ -129,13 +127,14 @@ def test_lossy_storage(self, basic_flow_system_linopy): # Check variable properties assert_var_equal( - model['TestStorage(Q_th_in)|flow_rate'], model.add_variables(lower=0, upper=20, coords=(timesteps,)) + model['TestStorage(Q_th_in)|flow_rate'], model.add_variables(lower=0, upper=20, coords=model.get_coords()) ) assert_var_equal( - model['TestStorage(Q_th_out)|flow_rate'], model.add_variables(lower=0, upper=20, coords=(timesteps,)) + model['TestStorage(Q_th_out)|flow_rate'], model.add_variables(lower=0, upper=20, coords=model.get_coords()) ) assert_var_equal( - model['TestStorage|charge_state'], model.add_variables(lower=0, upper=30, coords=(timesteps_extra,)) + model['TestStorage|charge_state'], + model.add_variables(lower=0, upper=30, coords=model.get_coords(extra_timestep=True)), ) # Check constraint formulations @@ -167,9 +166,94 @@ def test_lossy_storage(self, basic_flow_system_linopy): model.variables['TestStorage|charge_state'].isel(time=0) == 0, ) - def test_storage_with_investment(self, basic_flow_system_linopy): + def test_charge_state_bounds(self, basic_flow_system_linopy_coords, coords_config): + """Test that basic storage model variables and constraints are correctly generated.""" + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config + + # Create a simple storage + storage = fx.Storage( + 'TestStorage', + charging=fx.Flow('Q_th_in', bus='Fernwärme', size=20), + discharging=fx.Flow('Q_th_out', bus='Fernwärme', size=20), + capacity_in_flow_hours=30, # 30 kWh storage capacity + initial_charge_state=3, + prevent_simultaneous_charge_and_discharge=True, + relative_maximum_charge_state=np.array([0.14, 0.22, 0.3, 0.38, 0.46, 0.54, 0.62, 0.7, 0.78, 0.86]), + relative_minimum_charge_state=np.array([0.07, 0.11, 0.15, 0.19, 0.23, 0.27, 0.31, 0.35, 0.39, 0.43]), + ) + + flow_system.add_elements(storage) + model = create_linopy_model(flow_system) + + # Check that all expected variables exist - linopy model variables are accessed by indexing + expected_variables = { + 'TestStorage(Q_th_in)|flow_rate', + 'TestStorage(Q_th_in)|total_flow_hours', + 'TestStorage(Q_th_out)|flow_rate', + 'TestStorage(Q_th_out)|total_flow_hours', + 'TestStorage|charge_state', + 'TestStorage|netto_discharge', + } + for var_name in expected_variables: + assert var_name in model.variables, f'Missing variable: {var_name}' + + # Check that all expected constraints exist - linopy model constraints are accessed by indexing + expected_constraints = { + 'TestStorage(Q_th_in)|total_flow_hours', + 'TestStorage(Q_th_out)|total_flow_hours', + 'TestStorage|netto_discharge', + 'TestStorage|charge_state', + 'TestStorage|initial_charge_state', + } + for con_name in expected_constraints: + assert con_name in model.constraints, f'Missing constraint: {con_name}' + + # Check variable properties + assert_var_equal( + model['TestStorage(Q_th_in)|flow_rate'], model.add_variables(lower=0, upper=20, coords=model.get_coords()) + ) + assert_var_equal( + model['TestStorage(Q_th_out)|flow_rate'], model.add_variables(lower=0, upper=20, coords=model.get_coords()) + ) + assert_var_equal( + model['TestStorage|charge_state'], + model.add_variables( + lower=storage.relative_minimum_charge_state.reindex( + time=model.get_coords(extra_timestep=True)['time'] + ).ffill('time') + * 30, + upper=storage.relative_maximum_charge_state.reindex( + time=model.get_coords(extra_timestep=True)['time'] + ).ffill('time') + * 30, + coords=model.get_coords(extra_timestep=True), + ), + ) + + # Check constraint formulations + assert_conequal( + model.constraints['TestStorage|netto_discharge'], + model.variables['TestStorage|netto_discharge'] + == model.variables['TestStorage(Q_th_out)|flow_rate'] - model.variables['TestStorage(Q_th_in)|flow_rate'], + ) + + charge_state = model.variables['TestStorage|charge_state'] + assert_conequal( + model.constraints['TestStorage|charge_state'], + charge_state.isel(time=slice(1, None)) + == charge_state.isel(time=slice(None, -1)) + + model.variables['TestStorage(Q_th_in)|flow_rate'] * model.hours_per_step + - model.variables['TestStorage(Q_th_out)|flow_rate'] * model.hours_per_step, + ) + # Check initial charge state constraint + assert_conequal( + model.constraints['TestStorage|initial_charge_state'], + model.variables['TestStorage|charge_state'].isel(time=0) == 3, + ) + + def test_storage_with_investment(self, basic_flow_system_linopy_coords, coords_config): """Test storage with investment parameters.""" - flow_system = basic_flow_system_linopy + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config # Create storage with investment parameters storage = fx.Storage( @@ -177,7 +261,11 @@ def test_storage_with_investment(self, basic_flow_system_linopy): charging=fx.Flow('Q_th_in', bus='Fernwärme', size=20), discharging=fx.Flow('Q_th_out', bus='Fernwärme', size=20), capacity_in_flow_hours=fx.InvestParameters( - fix_effects=100, specific_effects=10, minimum_size=20, maximum_size=100, optional=True + effects_of_investment=100, + effects_of_investment_per_size=10, + minimum_size=20, + maximum_size=100, + mandatory=False, ), initial_charge_state=0, eta_charge=0.9, @@ -193,29 +281,35 @@ def test_storage_with_investment(self, basic_flow_system_linopy): for var_name in { 'InvestStorage|charge_state', 'InvestStorage|size', - 'InvestStorage|is_invested', + 'InvestStorage|invested', }: assert var_name in model.variables, f'Missing investment variable: {var_name}' # Check investment constraints exist - for con_name in {'InvestStorage|is_invested_ub', 'InvestStorage|is_invested_lb'}: + for con_name in {'InvestStorage|size|ub', 'InvestStorage|size|lb'}: assert con_name in model.constraints, f'Missing investment constraint: {con_name}' # Check variable properties - assert_var_equal(model['InvestStorage|size'], model.add_variables(lower=0, upper=100)) - assert_var_equal(model['InvestStorage|is_invested'], model.add_variables(binary=True)) + assert_var_equal( + model['InvestStorage|size'], + model.add_variables(lower=0, upper=100, coords=model.get_coords(['period', 'scenario'])), + ) + assert_var_equal( + model['InvestStorage|invested'], + model.add_variables(binary=True, coords=model.get_coords(['period', 'scenario'])), + ) assert_conequal( - model.constraints['InvestStorage|is_invested_ub'], - model.variables['InvestStorage|size'] <= model.variables['InvestStorage|is_invested'] * 100, + model.constraints['InvestStorage|size|ub'], + model.variables['InvestStorage|size'] <= model.variables['InvestStorage|invested'] * 100, ) assert_conequal( - model.constraints['InvestStorage|is_invested_lb'], - model.variables['InvestStorage|size'] >= model.variables['InvestStorage|is_invested'] * 20, + model.constraints['InvestStorage|size|lb'], + model.variables['InvestStorage|size'] >= model.variables['InvestStorage|invested'] * 20, ) - def test_storage_with_final_state_constraints(self, basic_flow_system_linopy): + def test_storage_with_final_state_constraints(self, basic_flow_system_linopy_coords, coords_config): """Test storage with final state constraints.""" - flow_system = basic_flow_system_linopy + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config # Create storage with final state constraints storage = fx.Storage( @@ -258,9 +352,9 @@ def test_storage_with_final_state_constraints(self, basic_flow_system_linopy): model.variables['FinalStateStorage|charge_state'].isel(time=-1) <= 25, ) - def test_storage_cyclic_initialization(self, basic_flow_system_linopy): + def test_storage_cyclic_initialization(self, basic_flow_system_linopy_coords, coords_config): """Test storage with cyclic initialization.""" - flow_system = basic_flow_system_linopy + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config # Create storage with cyclic initialization storage = fx.Storage( @@ -291,9 +385,9 @@ def test_storage_cyclic_initialization(self, basic_flow_system_linopy): 'prevent_simultaneous', [True, False], ) - def test_simultaneous_charge_discharge(self, basic_flow_system_linopy, prevent_simultaneous): + def test_simultaneous_charge_discharge(self, basic_flow_system_linopy_coords, coords_config, prevent_simultaneous): """Test prevent_simultaneous_charge_and_discharge parameter.""" - flow_system = basic_flow_system_linopy + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config # Create storage with or without simultaneous charge/discharge prevention storage = fx.Storage( @@ -321,35 +415,41 @@ def test_simultaneous_charge_discharge(self, basic_flow_system_linopy, prevent_s assert var_name in model.variables, f'Missing binary variable: {var_name}' # Check for constraints that enforce either charging or discharging - constraint_name = 'SimultaneousStorage|PreventSimultaneousUsage|prevent_simultaneous_use' + constraint_name = 'SimultaneousStorage|prevent_simultaneous_use' assert constraint_name in model.constraints, 'Missing constraint to prevent simultaneous operation' assert_conequal( - model.constraints['SimultaneousStorage|PreventSimultaneousUsage|prevent_simultaneous_use'], + model.constraints['SimultaneousStorage|prevent_simultaneous_use'], model.variables['SimultaneousStorage(Q_th_in)|on'] + model.variables['SimultaneousStorage(Q_th_out)|on'] - <= 1.1, + <= 1, ) @pytest.mark.parametrize( - 'optional,minimum_size,expected_vars,expected_constraints', + 'mandatory,minimum_size,expected_vars,expected_constraints', [ - (True, None, {'InvestStorage|is_invested'}, {'InvestStorage|is_invested_lb'}), - (True, 20, {'InvestStorage|is_invested'}, {'InvestStorage|is_invested_lb'}), - (False, None, set(), set()), - (False, 20, set(), set()), + (False, None, {'InvestStorage|invested'}, {'InvestStorage|size|lb'}), + (False, 20, {'InvestStorage|invested'}, {'InvestStorage|size|lb'}), + (True, None, set(), set()), + (True, 20, set(), set()), ], ) def test_investment_parameters( - self, basic_flow_system_linopy, optional, minimum_size, expected_vars, expected_constraints + self, + basic_flow_system_linopy_coords, + coords_config, + mandatory, + minimum_size, + expected_vars, + expected_constraints, ): """Test different investment parameter combinations.""" - flow_system = basic_flow_system_linopy + flow_system, coords_config = basic_flow_system_linopy_coords, coords_config # Create investment parameters invest_params = { - 'fix_effects': 100, - 'specific_effects': 10, - 'optional': optional, + 'effects_of_investment': 100, + 'effects_of_investment_per_size': 10, + 'mandatory': mandatory, } if minimum_size is not None: invest_params['minimum_size'] = minimum_size @@ -371,20 +471,18 @@ def test_investment_parameters( # Check that expected variables exist for var_name in expected_vars: - if optional: + if not mandatory: # Optional investment (mandatory=False) assert var_name in model.variables, f'Expected variable {var_name} not found' # Check that expected constraints exist for constraint_name in expected_constraints: - if optional: + if not mandatory: # Optional investment (mandatory=False) assert constraint_name in model.constraints, f'Expected constraint {constraint_name} not found' - # If optional is False, is_invested should be fixed to 1 - if not optional: - # Check that the is_invested variable exists and is fixed to 1 - if 'InvestStorage|is_invested' in model.variables: - var = model.variables['InvestStorage|is_invested'] + # If mandatory is True, invested should be fixed to 1 + if mandatory: + # Check that the invested variable exists and is fixed to 1 + if 'InvestStorage|invested' in model.variables: + var = model.variables['InvestStorage|invested'] # Check if the lower and upper bounds are both 1 - assert var.upper == 1 and var.lower == 1, ( - 'is_invested variable should be fixed to 1 when optional=False' - ) + assert var.upper == 1 and var.lower == 1, 'invested variable should be fixed to 1 when mandatory=True' diff --git a/tests/test_timeseries.py b/tests/test_timeseries.py index 36ede05c4..e69de29bb 100644 --- a/tests/test_timeseries.py +++ b/tests/test_timeseries.py @@ -1,603 +0,0 @@ -import tempfile -from pathlib import Path - -import numpy as np -import pandas as pd -import pytest -import xarray as xr - -from flixopt.core import TimeSeries, TimeSeriesCollection, TimeSeriesData - - -@pytest.fixture -def sample_timesteps(): - """Create a sample time index with the required 'time' name.""" - return pd.date_range('2023-01-01', periods=5, freq='D', name='time') - - -@pytest.fixture -def simple_dataarray(sample_timesteps): - """Create a simple DataArray with time dimension.""" - return xr.DataArray([10, 20, 30, 40, 50], coords={'time': sample_timesteps}, dims=['time']) - - -@pytest.fixture -def sample_timeseries(simple_dataarray): - """Create a sample TimeSeries object.""" - return TimeSeries(simple_dataarray, name='Test Series') - - -class TestTimeSeries: - """Test suite for TimeSeries class.""" - - def test_initialization(self, simple_dataarray): - """Test basic initialization of TimeSeries.""" - ts = TimeSeries(simple_dataarray, name='Test Series') - - # Check basic properties - assert ts.name == 'Test Series' - assert ts.aggregation_weight is None - assert ts.aggregation_group is None - - # Check data initialization - assert isinstance(ts.stored_data, xr.DataArray) - assert ts.stored_data.equals(simple_dataarray) - assert ts.active_data.equals(simple_dataarray) - - # Check backup was created - assert ts._backup.equals(simple_dataarray) - - # Check active timesteps - assert ts.active_timesteps.equals(simple_dataarray.indexes['time']) - - def test_initialization_with_aggregation_params(self, simple_dataarray): - """Test initialization with aggregation parameters.""" - ts = TimeSeries( - simple_dataarray, name='Weighted Series', aggregation_weight=0.5, aggregation_group='test_group' - ) - - assert ts.name == 'Weighted Series' - assert ts.aggregation_weight == 0.5 - assert ts.aggregation_group == 'test_group' - - def test_initialization_validation(self, sample_timesteps): - """Test validation during initialization.""" - # Test missing time dimension - invalid_data = xr.DataArray([1, 2, 3], dims=['invalid_dim']) - with pytest.raises(ValueError, match='must have a "time" index'): - TimeSeries(invalid_data, name='Invalid Series') - - # Test multi-dimensional data - multi_dim_data = xr.DataArray( - [[1, 2, 3], [4, 5, 6]], coords={'dim1': [0, 1], 'time': sample_timesteps[:3]}, dims=['dim1', 'time'] - ) - with pytest.raises(ValueError, match='dimensions of DataArray must be 1'): - TimeSeries(multi_dim_data, name='Multi-dim Series') - - def test_active_timesteps_getter_setter(self, sample_timeseries, sample_timesteps): - """Test active_timesteps getter and setter.""" - # Initial state should use all timesteps - assert sample_timeseries.active_timesteps.equals(sample_timesteps) - - # Set to a subset - subset_index = sample_timesteps[1:3] - sample_timeseries.active_timesteps = subset_index - assert sample_timeseries.active_timesteps.equals(subset_index) - - # Active data should reflect the subset - assert sample_timeseries.active_data.equals(sample_timeseries.stored_data.sel(time=subset_index)) - - # Reset to full index - sample_timeseries.active_timesteps = None - assert sample_timeseries.active_timesteps.equals(sample_timesteps) - - # Test invalid type - with pytest.raises(TypeError, match='must be a pandas DatetimeIndex'): - sample_timeseries.active_timesteps = 'invalid' - - def test_reset(self, sample_timeseries, sample_timesteps): - """Test reset method.""" - # Set to subset first - subset_index = sample_timesteps[1:3] - sample_timeseries.active_timesteps = subset_index - - # Reset - sample_timeseries.reset() - - # Should be back to full index - assert sample_timeseries.active_timesteps.equals(sample_timesteps) - assert sample_timeseries.active_data.equals(sample_timeseries.stored_data) - - def test_restore_data(self, sample_timeseries, simple_dataarray): - """Test restore_data method.""" - # Modify the stored data - new_data = xr.DataArray([1, 2, 3, 4, 5], coords={'time': sample_timeseries.active_timesteps}, dims=['time']) - - # Store original data for comparison - original_data = sample_timeseries.stored_data - - # Set new data - sample_timeseries.stored_data = new_data - assert sample_timeseries.stored_data.equals(new_data) - - # Restore from backup - sample_timeseries.restore_data() - - # Should be back to original data - assert sample_timeseries.stored_data.equals(original_data) - assert sample_timeseries.active_data.equals(original_data) - - def test_stored_data_setter(self, sample_timeseries, sample_timesteps): - """Test stored_data setter with different data types.""" - # Test with a Series - series_data = pd.Series([5, 6, 7, 8, 9], index=sample_timesteps) - sample_timeseries.stored_data = series_data - assert np.array_equal(sample_timeseries.stored_data.values, series_data.values) - - # Test with a single-column DataFrame - df_data = pd.DataFrame({'col1': [15, 16, 17, 18, 19]}, index=sample_timesteps) - sample_timeseries.stored_data = df_data - assert np.array_equal(sample_timeseries.stored_data.values, df_data['col1'].values) - - # Test with a NumPy array - array_data = np.array([25, 26, 27, 28, 29]) - sample_timeseries.stored_data = array_data - assert np.array_equal(sample_timeseries.stored_data.values, array_data) - - # Test with a scalar - sample_timeseries.stored_data = 42 - assert np.all(sample_timeseries.stored_data.values == 42) - - # Test with another DataArray - another_dataarray = xr.DataArray([30, 31, 32, 33, 34], coords={'time': sample_timesteps}, dims=['time']) - sample_timeseries.stored_data = another_dataarray - assert sample_timeseries.stored_data.equals(another_dataarray) - - def test_stored_data_setter_no_change(self, sample_timeseries): - """Test stored_data setter when data doesn't change.""" - # Get current data - current_data = sample_timeseries.stored_data - current_backup = sample_timeseries._backup - - # Set the same data - sample_timeseries.stored_data = current_data - - # Backup shouldn't change - assert sample_timeseries._backup is current_backup # Should be the same object - - def test_from_datasource(self, sample_timesteps): - """Test from_datasource class method.""" - # Test with scalar - ts_scalar = TimeSeries.from_datasource(42, 'Scalar Series', sample_timesteps) - assert np.all(ts_scalar.stored_data.values == 42) - - # Test with Series - series_data = pd.Series([1, 2, 3, 4, 5], index=sample_timesteps) - ts_series = TimeSeries.from_datasource(series_data, 'Series Data', sample_timesteps) - assert np.array_equal(ts_series.stored_data.values, series_data.values) - - # Test with aggregation parameters - ts_with_agg = TimeSeries.from_datasource( - series_data, 'Aggregated Series', sample_timesteps, aggregation_weight=0.7, aggregation_group='group1' - ) - assert ts_with_agg.aggregation_weight == 0.7 - assert ts_with_agg.aggregation_group == 'group1' - - def test_to_json_from_json(self, sample_timeseries): - """Test to_json and from_json methods.""" - # Test to_json (dictionary only) - json_dict = sample_timeseries.to_json() - assert json_dict['name'] == sample_timeseries.name - assert 'data' in json_dict - assert 'coords' in json_dict['data'] - assert 'time' in json_dict['data']['coords'] - - # Test to_json with file saving - with tempfile.TemporaryDirectory() as tmpdirname: - filepath = Path(tmpdirname) / 'timeseries.json' - sample_timeseries.to_json(filepath) - assert filepath.exists() - - # Test from_json with file loading - loaded_ts = TimeSeries.from_json(path=filepath) - assert loaded_ts.name == sample_timeseries.name - assert np.array_equal(loaded_ts.stored_data.values, sample_timeseries.stored_data.values) - - # Test from_json with dictionary - loaded_ts_dict = TimeSeries.from_json(data=json_dict) - assert loaded_ts_dict.name == sample_timeseries.name - assert np.array_equal(loaded_ts_dict.stored_data.values, sample_timeseries.stored_data.values) - - # Test validation in from_json - with pytest.raises(ValueError, match="one of 'path' or 'data'"): - TimeSeries.from_json(data=json_dict, path='dummy.json') - - def test_all_equal(self, sample_timesteps): - """Test all_equal property.""" - # All equal values - equal_data = xr.DataArray([5, 5, 5, 5, 5], coords={'time': sample_timesteps}, dims=['time']) - ts_equal = TimeSeries(equal_data, 'Equal Series') - assert ts_equal.all_equal is True - - # Not all equal - unequal_data = xr.DataArray([5, 5, 6, 5, 5], coords={'time': sample_timesteps}, dims=['time']) - ts_unequal = TimeSeries(unequal_data, 'Unequal Series') - assert ts_unequal.all_equal is False - - def test_arithmetic_operations(self, sample_timeseries): - """Test arithmetic operations.""" - # Create a second TimeSeries for testing - data2 = xr.DataArray([1, 2, 3, 4, 5], coords={'time': sample_timeseries.active_timesteps}, dims=['time']) - ts2 = TimeSeries(data2, 'Second Series') - - # Test operations between two TimeSeries objects - assert np.array_equal( - (sample_timeseries + ts2).values, sample_timeseries.active_data.values + ts2.active_data.values - ) - assert np.array_equal( - (sample_timeseries - ts2).values, sample_timeseries.active_data.values - ts2.active_data.values - ) - assert np.array_equal( - (sample_timeseries * ts2).values, sample_timeseries.active_data.values * ts2.active_data.values - ) - assert np.array_equal( - (sample_timeseries / ts2).values, sample_timeseries.active_data.values / ts2.active_data.values - ) - - # Test operations with DataArrays - assert np.array_equal((sample_timeseries + data2).values, sample_timeseries.active_data.values + data2.values) - assert np.array_equal((data2 + sample_timeseries).values, data2.values + sample_timeseries.active_data.values) - - # Test operations with scalars - assert np.array_equal((sample_timeseries + 5).values, sample_timeseries.active_data.values + 5) - assert np.array_equal((5 + sample_timeseries).values, 5 + sample_timeseries.active_data.values) - - # Test unary operations - assert np.array_equal((-sample_timeseries).values, -sample_timeseries.active_data.values) - assert np.array_equal((+sample_timeseries).values, +sample_timeseries.active_data.values) - assert np.array_equal((abs(sample_timeseries)).values, abs(sample_timeseries.active_data.values)) - - def test_comparison_operations(self, sample_timesteps): - """Test comparison operations.""" - data1 = xr.DataArray([10, 20, 30, 40, 50], coords={'time': sample_timesteps}, dims=['time']) - data2 = xr.DataArray([5, 10, 15, 20, 25], coords={'time': sample_timesteps}, dims=['time']) - - ts1 = TimeSeries(data1, 'Series 1') - ts2 = TimeSeries(data2, 'Series 2') - - # Test __gt__ method - assert (ts1 > ts2).all().item() - - # Test with mixed values - data3 = xr.DataArray([5, 25, 15, 45, 25], coords={'time': sample_timesteps}, dims=['time']) - ts3 = TimeSeries(data3, 'Series 3') - - assert not (ts1 > ts3).all().item() # Not all values in ts1 are greater than ts3 - - def test_numpy_ufunc(self, sample_timeseries): - """Test numpy ufunc compatibility.""" - # Test basic numpy functions - assert np.array_equal(np.add(sample_timeseries, 5).values, np.add(sample_timeseries.active_data, 5).values) - - assert np.array_equal( - np.multiply(sample_timeseries, 2).values, np.multiply(sample_timeseries.active_data, 2).values - ) - - # Test with two TimeSeries objects - data2 = xr.DataArray([1, 2, 3, 4, 5], coords={'time': sample_timeseries.active_timesteps}, dims=['time']) - ts2 = TimeSeries(data2, 'Second Series') - - assert np.array_equal( - np.add(sample_timeseries, ts2).values, np.add(sample_timeseries.active_data, ts2.active_data).values - ) - - def test_sel_and_isel_properties(self, sample_timeseries): - """Test sel and isel properties.""" - # Test that sel property works - selected = sample_timeseries.sel(time=sample_timeseries.active_timesteps[0]) - assert selected.item() == sample_timeseries.active_data.values[0] - - # Test that isel property works - indexed = sample_timeseries.isel(time=0) - assert indexed.item() == sample_timeseries.active_data.values[0] - - -@pytest.fixture -def sample_collection(sample_timesteps): - """Create a sample TimeSeriesCollection.""" - return TimeSeriesCollection(sample_timesteps) - - -@pytest.fixture -def populated_collection(sample_collection): - """Create a TimeSeriesCollection with test data.""" - # Add a constant time series - sample_collection.create_time_series(42, 'constant_series') - - # Add a varying time series - varying_data = np.array([10, 20, 30, 40, 50]) - sample_collection.create_time_series(varying_data, 'varying_series') - - # Add a time series with extra timestep - sample_collection.create_time_series( - np.array([1, 2, 3, 4, 5, 6]), 'extra_timestep_series', needs_extra_timestep=True - ) - - # Add series with aggregation settings - sample_collection.create_time_series( - TimeSeriesData(np.array([5, 5, 5, 5, 5]), agg_group='group1'), 'group1_series1' - ) - sample_collection.create_time_series( - TimeSeriesData(np.array([6, 6, 6, 6, 6]), agg_group='group1'), 'group1_series2' - ) - sample_collection.create_time_series( - TimeSeriesData(np.array([10, 10, 10, 10, 10]), agg_weight=0.5), 'weighted_series' - ) - - return sample_collection - - -class TestTimeSeriesCollection: - """Test suite for TimeSeriesCollection.""" - - def test_initialization(self, sample_timesteps): - """Test basic initialization.""" - collection = TimeSeriesCollection(sample_timesteps) - - assert collection.all_timesteps.equals(sample_timesteps) - assert len(collection.all_timesteps_extra) == len(sample_timesteps) + 1 - assert isinstance(collection.all_hours_per_timestep, xr.DataArray) - assert len(collection) == 0 - - def test_initialization_with_custom_hours(self, sample_timesteps): - """Test initialization with custom hour settings.""" - # Test with last timestep duration - last_timestep_hours = 12 - collection = TimeSeriesCollection(sample_timesteps, hours_of_last_timestep=last_timestep_hours) - - # Verify the last timestep duration - extra_step_delta = collection.all_timesteps_extra[-1] - collection.all_timesteps_extra[-2] - assert extra_step_delta == pd.Timedelta(hours=last_timestep_hours) - - # Test with previous timestep duration - hours_per_step = 8 - collection2 = TimeSeriesCollection(sample_timesteps, hours_of_previous_timesteps=hours_per_step) - - assert collection2.hours_of_previous_timesteps == hours_per_step - - def test_create_time_series(self, sample_collection): - """Test creating time series.""" - # Test scalar - ts1 = sample_collection.create_time_series(42, 'scalar_series') - assert ts1.name == 'scalar_series' - assert np.all(ts1.active_data.values == 42) - - # Test numpy array - data = np.array([1, 2, 3, 4, 5]) - ts2 = sample_collection.create_time_series(data, 'array_series') - assert np.array_equal(ts2.active_data.values, data) - - # Test with TimeSeriesData - ts3 = sample_collection.create_time_series(TimeSeriesData(10, agg_weight=0.7), 'weighted_series') - assert ts3.aggregation_weight == 0.7 - - # Test with extra timestep - ts4 = sample_collection.create_time_series(5, 'extra_series', needs_extra_timestep=True) - assert ts4.needs_extra_timestep - assert len(ts4.active_data) == len(sample_collection.timesteps_extra) - - # Test duplicate name - with pytest.raises(ValueError, match='already exists'): - sample_collection.create_time_series(1, 'scalar_series') - - def test_access_time_series(self, populated_collection): - """Test accessing time series.""" - # Test __getitem__ - ts = populated_collection['varying_series'] - assert ts.name == 'varying_series' - - # Test __contains__ with string - assert 'constant_series' in populated_collection - assert 'nonexistent_series' not in populated_collection - - # Test __contains__ with TimeSeries object - assert populated_collection['varying_series'] in populated_collection - - # Test __iter__ - names = [ts.name for ts in populated_collection] - assert len(names) == 6 - assert 'varying_series' in names - - # Test access to non-existent series - with pytest.raises(KeyError): - populated_collection['nonexistent_series'] - - def test_constants_and_non_constants(self, populated_collection): - """Test constants and non_constants properties.""" - # Test constants - constants = populated_collection.constants - assert len(constants) == 4 # constant_series, group1_series1, group1_series2, weighted_series - assert all(ts.all_equal for ts in constants) - - # Test non_constants - non_constants = populated_collection.non_constants - assert len(non_constants) == 2 # varying_series, extra_timestep_series - assert all(not ts.all_equal for ts in non_constants) - - # Test modifying a series changes the results - populated_collection['constant_series'].stored_data = np.array([1, 2, 3, 4, 5]) - updated_constants = populated_collection.constants - assert len(updated_constants) == 3 # One less constant - assert 'constant_series' not in [ts.name for ts in updated_constants] - - def test_timesteps_properties(self, populated_collection, sample_timesteps): - """Test timestep-related properties.""" - # Test default (all) timesteps - assert populated_collection.timesteps.equals(sample_timesteps) - assert len(populated_collection.timesteps_extra) == len(sample_timesteps) + 1 - - # Test activating a subset - subset = sample_timesteps[1:3] - populated_collection.activate_timesteps(subset) - - assert populated_collection.timesteps.equals(subset) - assert len(populated_collection.timesteps_extra) == len(subset) + 1 - - # Check that time series were updated - assert populated_collection['varying_series'].active_timesteps.equals(subset) - assert populated_collection['extra_timestep_series'].active_timesteps.equals( - populated_collection.timesteps_extra - ) - - # Test reset - populated_collection.reset() - assert populated_collection.timesteps.equals(sample_timesteps) - - def test_to_dataframe_and_dataset(self, populated_collection): - """Test conversion to DataFrame and Dataset.""" - # Test to_dataset - ds = populated_collection.to_dataset() - assert isinstance(ds, xr.Dataset) - assert len(ds.data_vars) == 6 - - # Test to_dataframe with different filters - df_all = populated_collection.to_dataframe(filtered='all') - assert len(df_all.columns) == 6 - - df_constant = populated_collection.to_dataframe(filtered='constant') - assert len(df_constant.columns) == 4 - - df_non_constant = populated_collection.to_dataframe(filtered='non_constant') - assert len(df_non_constant.columns) == 2 - - # Test invalid filter - with pytest.raises(ValueError): - populated_collection.to_dataframe(filtered='invalid') - - def test_calculate_aggregation_weights(self, populated_collection): - """Test aggregation weight calculation.""" - weights = populated_collection.calculate_aggregation_weights() - - # Group weights should be 0.5 each (1/2) - assert populated_collection.group_weights['group1'] == 0.5 - - # Series in group1 should have weight 0.5 - assert weights['group1_series1'] == 0.5 - assert weights['group1_series2'] == 0.5 - - # Series with explicit weight should have that weight - assert weights['weighted_series'] == 0.5 - - # Series without group or weight should have weight 1 - assert weights['constant_series'] == 1 - - def test_insert_new_data(self, populated_collection, sample_timesteps): - """Test inserting new data.""" - # Create new data - new_data = pd.DataFrame( - { - 'constant_series': [100, 100, 100, 100, 100], - 'varying_series': [5, 10, 15, 20, 25], - # extra_timestep_series is omitted to test partial updates - }, - index=sample_timesteps, - ) - - # Insert data - populated_collection.insert_new_data(new_data) - - # Verify updates - assert np.all(populated_collection['constant_series'].active_data.values == 100) - assert np.array_equal(populated_collection['varying_series'].active_data.values, np.array([5, 10, 15, 20, 25])) - - # Series not in the DataFrame should be unchanged - assert np.array_equal( - populated_collection['extra_timestep_series'].active_data.values[:-1], np.array([1, 2, 3, 4, 5]) - ) - - # Test with mismatched index - bad_index = pd.date_range('2023-02-01', periods=5, freq='D', name='time') - bad_data = pd.DataFrame({'constant_series': [1, 1, 1, 1, 1]}, index=bad_index) - - with pytest.raises(ValueError, match='must match collection timesteps'): - populated_collection.insert_new_data(bad_data) - - def test_restore_data(self, populated_collection): - """Test restoring original data.""" - # Capture original data - original_values = {name: ts.stored_data.copy() for name, ts in populated_collection.time_series_data.items()} - - # Modify data - new_data = pd.DataFrame( - { - name: np.ones(len(populated_collection.timesteps)) * 999 - for name in populated_collection.time_series_data - if not populated_collection[name].needs_extra_timestep - }, - index=populated_collection.timesteps, - ) - - populated_collection.insert_new_data(new_data) - - # Verify data was changed - assert np.all(populated_collection['constant_series'].active_data.values == 999) - - # Restore data - populated_collection.restore_data() - - # Verify data was restored - for name, original in original_values.items(): - restored = populated_collection[name].stored_data - assert np.array_equal(restored.values, original.values) - - def test_class_method_with_uniform_timesteps(self): - """Test the with_uniform_timesteps class method.""" - collection = TimeSeriesCollection.with_uniform_timesteps( - start_time=pd.Timestamp('2023-01-01'), periods=24, freq='h', hours_per_step=1 - ) - - assert len(collection.timesteps) == 24 - assert collection.hours_of_previous_timesteps == 1 - assert (collection.timesteps[1] - collection.timesteps[0]) == pd.Timedelta(hours=1) - - def test_hours_per_timestep(self, populated_collection): - """Test hours_per_timestep calculation.""" - # Standard case - uniform timesteps - hours = populated_collection.hours_per_timestep.values - assert np.allclose(hours, 24) # Default is daily timesteps - - # Create non-uniform timesteps - non_uniform_times = pd.DatetimeIndex( - [ - pd.Timestamp('2023-01-01'), - pd.Timestamp('2023-01-02'), - pd.Timestamp('2023-01-03 12:00:00'), # 1.5 days from previous - pd.Timestamp('2023-01-04'), # 0.5 days from previous - pd.Timestamp('2023-01-06'), # 2 days from previous - ], - name='time', - ) - - collection = TimeSeriesCollection(non_uniform_times) - hours = collection.hours_per_timestep.values - - # Expected hours between timestamps - expected = np.array([24, 36, 12, 48, 48]) - assert np.allclose(hours, expected) - - def test_validation_and_errors(self, sample_timesteps): - """Test validation and error handling.""" - # Test non-DatetimeIndex - with pytest.raises(TypeError, match='must be a pandas DatetimeIndex'): - TimeSeriesCollection(pd.Index([1, 2, 3, 4, 5])) - - # Test too few timesteps - with pytest.raises(ValueError, match='must contain at least 2 timestamps'): - TimeSeriesCollection(pd.DatetimeIndex([pd.Timestamp('2023-01-01')], name='time')) - - # Test invalid active_timesteps - collection = TimeSeriesCollection(sample_timesteps) - invalid_timesteps = pd.date_range('2024-01-01', periods=3, freq='D', name='time') - - with pytest.raises(ValueError, match='must be a subset'): - collection.activate_timesteps(invalid_timesteps) diff --git a/tests/todos.txt b/tests/todos.txt deleted file mode 100644 index d4628c259..000000000 --- a/tests/todos.txt +++ /dev/null @@ -1,5 +0,0 @@ -# testing of - # abschnittsweise linear testen - # Komponenten mit offenen Flows - # Binärvariablen ohne max-Wert-Vorgabe des Flows (Binärungenauigkeitsproblem) - # Medien-zulässigkeit From 1bf330a97ae6dc8ac88bc421fcdfd55d3cc83db1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 13 Oct 2025 19:39:17 +0200 Subject: [PATCH 343/448] chore(deps): update dependency ruff to v0.13.3 (#400) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3bb68efb4..45a6e0046 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,7 +83,7 @@ dev = [ "pytest==8.4.2", "pytest-xdist==3.8.0", "nbformat==5.10.4", - "ruff==0.13.2", + "ruff==0.13.3", "pre-commit==4.3.0", "pyvis==0.3.2", "tsam==2.3.9", From 06bb79ded5456e5468d4731e0c4273ea79f7cbe6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 13 Oct 2025 19:39:36 +0200 Subject: [PATCH 344/448] chore(deps): update dependency mkdocs-material to v9.6.21 (#399) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 45a6e0046..6ea2f232e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -98,7 +98,7 @@ dev = [ # Documentation building docs = [ - "mkdocs-material==9.6.20", + "mkdocs-material==9.6.21", "mkdocstrings-python==1.18.2", "mkdocs-table-reader-plugin==3.1.0", "mkdocs-gen-files==0.5.0", From 0f805657d2dfcb05beadfa11c7cdada50c310557 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 13 Oct 2025 19:39:49 +0200 Subject: [PATCH 345/448] chore(deps): update dependency astral-sh/uv to v0.8.23 (#368) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/python-app.yaml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml index 8e445974c..fe86c8dfe 100644 --- a/.github/workflows/python-app.yaml +++ b/.github/workflows/python-app.yaml @@ -35,7 +35,7 @@ jobs: - name: Set up uv uses: astral-sh/setup-uv@v6 with: - version: "0.8.19" + version: "0.8.23" enable-cache: true - name: Set up Python @@ -75,7 +75,7 @@ jobs: - name: Set up uv uses: astral-sh/setup-uv@v6 with: - version: "0.8.19" + version: "0.8.23" enable-cache: true - name: Set up Python ${{ matrix.python-version }} @@ -104,7 +104,7 @@ jobs: - name: Set up uv uses: astral-sh/setup-uv@v6 with: - version: "0.8.19" + version: "0.8.23" enable-cache: true - name: Set up Python ${{ env.PYTHON_VERSION }} @@ -130,7 +130,7 @@ jobs: - name: Set up uv uses: astral-sh/setup-uv@v6 with: - version: "0.8.19" + version: "0.8.23" enable-cache: true - name: Set up Python @@ -170,7 +170,7 @@ jobs: - name: Set up uv uses: astral-sh/setup-uv@v6 with: - version: "0.8.19" + version: "0.8.23" enable-cache: true - name: Set up Python @@ -212,7 +212,7 @@ jobs: - name: Set up uv uses: astral-sh/setup-uv@v6 with: - version: "0.8.19" + version: "0.8.23" enable-cache: true - name: Set up Python @@ -298,7 +298,7 @@ jobs: - name: Set up uv uses: astral-sh/setup-uv@v6 with: - version: "0.8.19" + version: "0.8.23" enable-cache: true - name: Set up Python @@ -384,7 +384,7 @@ jobs: - name: Set up uv uses: astral-sh/setup-uv@v6 with: - version: "0.8.19" + version: "0.8.23" enable-cache: true - name: Set up Python From 6d2b56ed386a9c2dc0812ac1b6b78d908eaa3217 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 14 Oct 2025 00:22:33 +0200 Subject: [PATCH 346/448] Fix docs and add migration guide (#404) * Add a migration guide * Add a migration guide * Remove not needed docs files * Add missing type hints * Fix docs * Make fancy migration guide * Fix: Aggregation objects stored in separate attributes * Type hints * Remove duplicates in mkdocs.yml * Type hints * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md --- CHANGELOG.md | 14 + docs/faq/contribute.md | 61 -- docs/faq/index.md | 3 - .../user-guide/mathematical-notation/index.md | 2 +- docs/user-guide/migration-guide-v3.md | 550 ++++++++++++++++++ flixopt/calculation.py | 14 +- flixopt/config.py | 2 +- flixopt/core.py | 6 +- flixopt/modeling.py | 6 +- flixopt/results.py | 4 +- flixopt/utils.py | 8 +- mkdocs.yml | 29 +- scripts/extract_changelog.py | 8 +- 13 files changed, 595 insertions(+), 112 deletions(-) delete mode 100644 docs/faq/contribute.md delete mode 100644 docs/faq/index.md create mode 100644 docs/user-guide/migration-guide-v3.md diff --git a/CHANGELOG.md b/CHANGELOG.md index b5de41f19..0e5f9ff3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,8 +69,22 @@ Please keep the format of the changelog consistent with the other releases, so t --- Until here --> +## [3.0.1] - 2025-10-14 +**Summary**: Adding a Migration Guide for the new **flixopt 3** and fixing docs +See the [Migration Guide](https://flixopt.github.io/flixopt/user-guide/migration-guide-v3/). + +### 📝 Docs +- Fixed deployed docs +- Added Migration Guide for flixopt 3 + +### 👷 Development +- Added missing type hints + +--- + ## [3.0.0] - 2025-10-13 **Summary**: This release introduces new model dimensions (periods and scenarios) for multi-period investments and stochastic modeling, along with a redesigned effect sharing system and enhanced I/O capabilities. +For detailed migration instructions, see the [Migration Guide](https://flixopt.github.io/flixopt/user-guide/migration-guide-v3/). ### ✨ Added diff --git a/docs/faq/contribute.md b/docs/faq/contribute.md deleted file mode 100644 index ff31c9f1f..000000000 --- a/docs/faq/contribute.md +++ /dev/null @@ -1,61 +0,0 @@ -# Contributing to the Project - -We warmly welcome contributions from the community! This guide will help you get started with contributing to our project. - -## Development Setup -1. Clone the repository `git clone https://github.com/flixOpt/flixopt.git` -2. Install the development dependencies `pip install -e ".[dev]"` -3. Install pre-commit hooks `pre-commit install` (one-time setup) -4. Run `pytest` to ensure your code passes all tests - -## Code Quality -We use [Ruff](https://github.com/astral-sh/ruff) for linting and formatting. After the one-time setup above, **code quality checks run automatically on every commit**. - -To run manually: -- `ruff check --fix .` to check and fix linting issues -- `ruff format .` to format code - -## Documentation (Optional) -FlixOpt uses [mkdocs](https://www.mkdocs.org/) to generate documentation. -To work on documentation: -```bash -pip install -e ".[docs]" -mkdocs serve -``` -Then navigate to http://127.0.0.1:8000/ - -## Testing -- `pytest` to run the test suite -- You can also run the provided python script `run_all_test.py` - ---- -# Best practices - -## Coding Guidelines - -- Follow PEP 8 style guidelines -- Write clear, commented code -- Include type hints -- Create or update tests for new functionality -- Ensure 100% test coverage for new code - -## Branches -As we start to think FlixOpt in **Releases**, we decided to introduce multiple **dev**-branches instead of only one: -Following the **Semantic Versioning** guidelines, we introduced: -- `next/patch`: This is where all pull requests for the next patch release (1.0.x) go. -- `next/minor`: This is where all pull requests for the next minor release (1.x.0) go. -- `next/major`: This is where all pull requests for the next major release (x.0.0) go. - -Everything else remains in `feature/...`-branches. - -## Pull requests -Every feature or bugfix should be merged into one of the 3 [release branches](#branches), using **Squash and merge** or a regular **single commit**. -At some point, `next/minor` or `next/major` will get merged into `main` using a regular **Merge** (not squash). -*This ensures that Features are kept separate, and the `next/...`branches stay in synch with ``main`.* - -## Releases -As stated, we follow **Semantic Versioning**. -Right after one of the 3 [release branches](#branches) is merged into main, a **Tag** should be added to the merge commit and pushed to the main branch. The tag has the form `v1.2.3`. -With this tag, a release with **Release Notes** must be created. - -*This is our current best practice* diff --git a/docs/faq/index.md b/docs/faq/index.md deleted file mode 100644 index 6a245edd3..000000000 --- a/docs/faq/index.md +++ /dev/null @@ -1,3 +0,0 @@ -# Frequently Asked Questions - -## Work in progress diff --git a/docs/user-guide/mathematical-notation/index.md b/docs/user-guide/mathematical-notation/index.md index ae89f3b67..05d1fed60 100644 --- a/docs/user-guide/mathematical-notation/index.md +++ b/docs/user-guide/mathematical-notation/index.md @@ -3,7 +3,7 @@ This section provides the **mathematical formulations** underlying FlixOpt's optimization models. It is intended as **reference documentation** for users who want to understand the mathematical details behind the high-level FlixOpt API described in the [FlixOpt Concepts](../index.md) guide. -**For typical usage**, refer to the [FlixOpt Concepts](../index.md) guide, [Examples](../../examples/), and [API Reference](../../api-reference/) - you don't need to understand these mathematical formulations to use FlixOpt effectively. +**For typical usage**, refer to the [FlixOpt Concepts](../index.md) guide, [Examples](../../examples/index.md), and [API Reference](../../api-reference/index.md) - you don't need to understand these mathematical formulations to use FlixOpt effectively. --- diff --git a/docs/user-guide/migration-guide-v3.md b/docs/user-guide/migration-guide-v3.md new file mode 100644 index 000000000..4f85d901b --- /dev/null +++ b/docs/user-guide/migration-guide-v3.md @@ -0,0 +1,550 @@ +# Migration Guide: Upgrading to v3.0.0 + +This guide helps you migrate your flixopt code from v2.x to v3.0.0. Version 3.0.0 introduces powerful new features like multi-period investments and scenario-based stochastic optimization, along with a redesigned effect sharing system. + +!!! tip "Quick Start" + 1. **Update your installation:** + ```bash + pip install --upgrade flixopt + ``` + 2. **Review breaking changes** in the sections below + 3. **Update deprecated parameters** to their new names + 4. **Test your code** with the new version + +--- + +## Breaking Changes + +### 1. Effect Sharing System Redesign + +!!! warning "Breaking Change - No Deprecation" + The effect sharing syntax has been inverted and simplified. This change was made WITHOUT deprecation warnings due to the fundamental restructuring. + +**What changed:** Effects now "pull" shares from other effects instead of "pushing" them. + +=== "v2.x (Old)" + + ```python + # Effects "pushed" shares to other effects + CO2 = fx.Effect('CO2', 'kg', 'CO2 emissions', + specific_share_to_other_effects_operation={'costs': 0.2}) + + land = fx.Effect('land', 'm²', 'Land usage', + specific_share_to_other_effects_invest={'costs': 100}) + + costs = fx.Effect('costs', '€', 'Total costs') + ``` + +=== "v3.0.0 (New)" + + ```python + # Effects "pull" shares from other effects (clearer direction) + CO2 = fx.Effect('CO2', 'kg', 'CO2 emissions') + + land = fx.Effect('land', 'm²', 'Land usage') + + costs = fx.Effect('costs', '€', 'Total costs', + share_from_temporal={'CO2': 0.2}, # From temporal (operation) effects + share_from_periodic={'land': 100}) # From periodic (investment) effects + ``` + +!!! success "Migration Steps" + 1. Find all uses of `specific_share_to_other_effects_operation` and `specific_share_to_other_effects_invest` + 2. Move the share definition to the **receiving** effect + 3. Rename parameters: + - `specific_share_to_other_effects_operation` → `share_from_temporal` + - `specific_share_to_other_effects_invest` → `share_from_periodic` + +--- + +### 2. Class and Variable Renaming + +=== "v2.x (Old)" + + ```python + # In optimization results + results.solution['component|is_invested'] + ``` + +=== "v3.0.0 (New)" + + ```python + # In optimization results + results.solution['component|invested'] + ``` + +--- + +### 3. Calculation API Change + +!!! info "Method Chaining Support" + `Calculation.do_modeling()` now returns the Calculation object to enable method chaining. + +=== "v2.x (Old)" + + ```python + calculation = fx.FullCalculation('my_calc', flow_system) + linopy_model = calculation.do_modeling() # Returned linopy.Model + + # Access model directly from return value + print(linopy_model) + ``` + +=== "v3.0.0 (New)" + + ```python + calculation = fx.FullCalculation('my_calc', flow_system) + calculation.do_modeling() # Returns Calculation object + linopy_model = calculation.model # Access model via property + + # This enables chaining operations + fx.FullCalculation('my_calc', flow_system).do_modeling().solve() + ``` + +!!! tip "Migration" + If you used the return value of `do_modeling()`, update to access `.model` property instead. + +--- + +### 4. Storage Charge State Bounds + +!!! warning "Array Dimensions Changed" + `relative_minimum_charge_state` and `relative_maximum_charge_state` no longer have an extra timestep. + +**Impact:** If you provided arrays with `len(timesteps) + 1` elements, reduce to `len(timesteps)`. + +=== "v2.x (Old)" + + ```python + # Array with extra timestep + storage = fx.Storage( + 'storage', + relative_minimum_charge_state=np.array([0.2, 0.2, 0.2, 0.2, 0.2]) # 5 values for 4 timesteps + ) + ``` + +=== "v3.0.0 (New)" + + ```python + # Array matches timesteps + storage = fx.Storage( + 'storage', + relative_minimum_charge_state=np.array([0.2, 0.2, 0.2, 0.2]), # 4 values for 4 timesteps + relative_minimum_final_charge_state=0.3 # Specify the final value directly + ) + ``` + +!!! note "Final State Control" + Use the new `relative_minimum_final_charge_state` and `relative_maximum_final_charge_state` parameters to explicitly control the final charge state. + +--- + +### 5. Plotting Parameter Rename + +=== "v2.x (Old)" + + ```python + results.plot_heatmap('component|variable', mode='line') + ``` + +=== "v3.0.0 (New)" + + ```python + results.plot_heatmap('component|variable', style='line') + ``` + +--- + +## Deprecated Parameters (Still Supported) + +!!! info "Gradual Migration" + These parameters still work but will be removed in a future version. Update them at your convenience - deprecation warnings will guide you. + +### InvestParameters + +**Parameter Changes:** + +| Old Parameter (v2.x) | New Parameter (v3.0.0) | +|---------------------|----------------------| +| `fix_effects` | `effects_of_investment` | +| `specific_effects` | `effects_of_investment_per_size` | +| `divest_effects` | `effects_of_retirement` | +| `piecewise_effects` | `piecewise_effects_of_investment` | + +=== "v2.x (Deprecated)" + + ```python + fx.InvestParameters( + fix_effects=1000, + specific_effects={'costs': 10}, + divest_effects=100, + piecewise_effects=my_piecewise, + ) + ``` + +=== "v3.0.0 (Recommended)" + + ```python + fx.InvestParameters( + effects_of_investment=1000, + effects_of_investment_per_size={'costs': 10}, + effects_of_retirement=100, + piecewise_effects_of_investment=my_piecewise, + ) + ``` + +### Effect + +**Parameter Changes:** + +| Old Parameter (v2.x) | New Parameter (v3.0.0) | +|---------------------|----------------------| +| `minimum_investment` | `minimum_periodic` | +| `maximum_investment` | `maximum_periodic` | +| `minimum_operation` | `minimum_temporal` | +| `maximum_operation` | `maximum_temporal` | +| `minimum_operation_per_hour` | `minimum_per_hour` | +| `maximum_operation_per_hour` | `maximum_per_hour` | + +=== "v2.x (Deprecated)" + + ```python + fx.Effect( + 'my_effect', 'unit', 'description', + minimum_investment=10, + maximum_investment=100, + minimum_operation=5, + maximum_operation=50, + minimum_operation_per_hour=1, + maximum_operation_per_hour=10, + ) + ``` + +=== "v3.0.0 (Recommended)" + + ```python + fx.Effect( + 'my_effect', 'unit', 'description', + minimum_periodic=10, + maximum_periodic=100, + minimum_temporal=5, + maximum_temporal=50, + minimum_per_hour=1, + maximum_per_hour=10, + ) + ``` + +### Component Parameters + +=== "v2.x (Deprecated)" + + ```python + fx.Source('my_source', source=flow) + + fx.Sink('my_sink', sink=flow) + + fx.SourceAndSink( + 'my_source_sink', + source=flow1, + sink=flow2, + prevent_simultaneous_sink_and_source=True + ) + ``` + +=== "v3.0.0 (Recommended)" + + ```python + fx.Source('my_source', outputs=flow) + + fx.Sink('my_sink', inputs=flow) + + fx.SourceAndSink( + 'my_source_sink', + outputs=flow1, + inputs=flow2, + prevent_simultaneous_flow_rates=True + ) + ``` + +### TimeSeriesData + +=== "v2.x (Deprecated)" + + ```python + fx.TimeSeriesData( + agg_group='group1', + agg_weight=2.0 + ) + ``` + +=== "v3.0.0 (Recommended)" + + ```python + fx.TimeSeriesData( + aggregation_group='group1', + aggregation_weight=2.0 + ) + ``` + +### Calculation + +=== "v2.x (Deprecated)" + + ```python + calculation = fx.FullCalculation( + 'calc', + flow_system, + active_timesteps=[0, 1, 2] + ) + ``` + +=== "v3.0.0 (Recommended)" + + ```python + # Use FlowSystem selection methods + flow_system_subset = flow_system.sel(time=slice('2020-01-01', '2020-01-03')) + calculation = fx.FullCalculation('calc', flow_system_subset) + + # Or with isel for index-based selection + flow_system_subset = flow_system.isel(time=slice(0, 3)) + calculation = fx.FullCalculation('calc', flow_system_subset) + ``` + +--- + +## New Features in v3.0.0 + +### 1. Multi-Period Investments + +Model transformation pathways with distinct investment decisions in each period: + +```python +import pandas as pd + +# Define multiple investment periods +periods = pd.Index(['2020', '2030']) +flow_system = fx.FlowSystem(time=timesteps, periods=periods) + +# Components can now invest differently in each period +solar = fx.Source( + 'solar', + outputs=[fx.Flow( + 'P_el', + bus='electricity', + size=fx.InvestParameters( + minimum_size=0, + maximum_size=1000, + effects_of_investment_per_size={'costs': 100} + ) + )] +) +``` + +### 2. Scenario-Based Stochastic Optimization + +Model uncertainty with weighted scenarios: + +```python +# Define scenarios with probabilities +scenarios = pd.Index(['low_demand', 'base', 'high_demand'], name='scenario') +scenario_weights = [0.2, 0.6, 0.2] # Probabilities + +flow_system = fx.FlowSystem( + time=timesteps, + scenarios=scenarios, + scenario_weights=scenario_weights +) + +# Define scenario-dependent data +demand = xr.DataArray( + data=[[70, 80, 90], # low_demand scenario + [90, 100, 110], # base scenario + [110, 120, 130]], # high_demand scenario + dims=['scenario', 'time'], + coords={'scenario': scenarios, 'time': timesteps} +) + +``` + +**Control variable independence:** +```python +# By default: investment sizes are shared across scenarios, flow rates vary +# To make sizes scenario-independent: +flow_system = fx.FlowSystem( + time=timesteps, + scenarios=scenarios, + scenario_independent_sizes=True # Each scenario gets its own capacity +) +``` + +### 3. Enhanced I/O and Data Handling + +```python +# Save and load FlowSystem +flow_system.to_netcdf('my_system.nc') +flow_system_loaded = fx.FlowSystem.from_netcdf('my_system.nc') + +# Manipulate FlowSystem +fs_subset = flow_system.sel(time=slice('2020-01', '2020-06')) +fs_resampled = flow_system.resample(time='D') # Resample to daily +fs_copy = flow_system.copy() + +# Access FlowSystem from results (lazily loaded) +results = calculation.results +original_fs = results.flow_system # No manual restoration needed +``` + +### 4. Effects Per Component + +Analyze the impact of each component, including indirect effects through effect shares: + +```python +# Get dataset showing contribution of each component to all effects +effects_ds = calculation.results.effects_per_component() + +print(effects_ds['costs']) # Total costs by component +print(effects_ds['CO2']) # CO2 emissions by component (including indirect) +``` + +### 5. Balanced Storage + +Force charging and discharging capacities to be equal: + +```python +storage = fx.Storage( + 'storage', + charging=fx.Flow('charge', bus='electricity', size=fx.InvestParameters(effects_per_size=100, minimum_size=5)), + discharging=fx.Flow('discharge', bus='electricity', size=fx.InvestParameters(), + balanced=True, # Ensures charge_size == discharge_size + capacity_in_flow_hours=100 +) +``` + +### 6. Final Charge State Control + +Set bounds on the storage state at the end of the optimization: + +```python +storage = fx.Storage( + 'storage', + charging=fx.Flow('charge', bus='electricity', size=100), + discharging=fx.Flow('discharge', bus='electricity', size=100), + capacity_in_flow_hours=10, + relative_minimum_final_charge_state=0.5, # End at least 50% charged + relative_maximum_final_charge_state=0.8 # End at most 80% charged +) +``` + +--- + +## Configuration Changes + +### Logging (v2.2.0+) + +**Breaking change:** Console and file logging are now disabled by default. + +```python +import flixopt as fx + +# Enable console logging +fx.CONFIG.Logging.console = True +fx.CONFIG.Logging.level = 'INFO' +fx.CONFIG.apply() + +# Enable file logging +fx.CONFIG.Logging.file = 'flixopt.log' +fx.CONFIG.apply() + +# Deprecated: change_logging_level() - will be removed in future +# fx.change_logging_level('INFO') # ❌ Old way +``` + +--- + +## Testing Your Migration + +### 1. Check for Deprecation Warnings + +Run your code and watch for deprecation warnings: + +```python +import warnings +warnings.filterwarnings('default', category=DeprecationWarning) + +# Run your flixopt code +# Review any DeprecationWarning messages +``` + +### 2. Validate Results + +Compare results from v2.x and v3.0.0 to ensure consistency: + +```python +# Save v2.x results before upgrading +calculation.results.to_file('results_v2.nc') + +# After upgrading, compare +results_v3 = calculation.results +results_v2 = fx.CalculationResults.from_file('results_v2.nc') + +# Check key variables match (within numerical tolerance) +import numpy as np +v2_costs = results_v2['effect_values'].sel(effect='costs') +v3_costs = results_v3['effect_values'].sel(effect='costs') +np.testing.assert_allclose(v2_costs, v3_costs, rtol=1e-5) +``` + +--- + +## Common Migration Issues + +### Issue: "Effect share parameters not working" + +**Solution:** Effect sharing was completely redesigned. Move share definitions to the **receiving** effect using `share_from_temporal` and `share_from_periodic`. + +### Issue: "Storage charge state has wrong dimensions" + +**Solution:** Remove the extra timestep from charge state bound arrays. + +### Issue: "Import error with Bus assignment" + +**Solution:** Pass bus labels (strings) instead of Bus objects to `Flow.bus`. + +```python +# Old +my_bus = fx.Bus('electricity') +flow = fx.Flow('P_el', bus=my_bus) # ❌ + +# New +my_bus = fx.Bus('electricity') +flow = fx.Flow('P_el', bus='electricity') # ✅ +``` + +### Issue: "AttributeError: module 'flixopt' has no attribute 'SystemModel'" + +**Solution:** Rename `SystemModel` → `FlowSystemModel` + + +--- + +## Getting Help + +- **Documentation:** [https://flixopt.github.io/flixopt/](https://flixopt.github.io/flixopt/) +- **GitHub Issues:** [https://github.com/flixOpt/flixopt/issues](https://github.com/flixOpt/flixopt/issues) +- **Changelog:** [Full v3.0.0 release notes](https://flixopt.github.io/flixopt/changelog/) + +--- + +## Summary Checklist + +- [ ] Update flixopt: `pip install --upgrade flixopt` +- [ ] Update effect sharing syntax (no deprecation warning!) +- [ ] Update `Calculation.do_modeling()` usage +- [ ] Fix storage charge state array dimensions +- [ ] Rename `mode` → `style` in plotting calls +- [ ] Update deprecated parameter names (optional, but recommended) +- [ ] Enable logging explicitly if needed +- [ ] Test your code thoroughly +- [ ] Explore new features (periods, scenarios, enhanced I/O) + +**Welcome to flixopt v3.0.0!** 🎉 diff --git a/flixopt/calculation.py b/flixopt/calculation.py index 9e35b2dee..9d2164e1e 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -23,7 +23,7 @@ from . import io as fx_io from . import utils as utils -from .aggregation import AggregationModel, AggregationParameters +from .aggregation import Aggregation, AggregationModel, AggregationParameters from .components import Storage from .config import CONFIG from .core import DataConverter, Scalar, TimeSeriesData, drop_constant_arrays @@ -286,7 +286,10 @@ class AggregatedCalculation(FullCalculation): This equalizes variables in the components according to the typical periods computed in the aggregation active_timesteps: DatetimeIndex of timesteps to use for calculation. If None, all timesteps are used folder: Folder where results should be saved. If None, current working directory is used - aggregation: contains the aggregation model + + Attributes: + aggregation (Aggregation | None): Contains the clustered time series data + aggregation_model (AggregationModel | None): Contains Variables and Constraints that equalize clusters of the time series data """ def __init__( @@ -306,7 +309,8 @@ def __init__( super().__init__(name, flow_system, active_timesteps, folder=folder) self.aggregation_parameters = aggregation_parameters self.components_to_clusterize = components_to_clusterize - self.aggregation = None + self.aggregation: Aggregation | None = None + self.aggregation_model: AggregationModel | None = None def do_modeling(self) -> AggregatedCalculation: t_start = timeit.default_timer() @@ -317,10 +321,10 @@ def do_modeling(self) -> AggregatedCalculation: self.model = self.flow_system.create_model(self.normalize_weights) self.model.do_modeling() # Add Aggregation Submodel after modeling the rest - self.aggregation = AggregationModel( + self.aggregation_model = AggregationModel( self.model, self.aggregation_parameters, self.flow_system, self.aggregation, self.components_to_clusterize ) - self.aggregation.do_modeling() + self.aggregation_model.do_modeling() self.durations['modeling'] = round(timeit.default_timer() - t_start, 2) return self diff --git a/flixopt/config.py b/flixopt/config.py index 4ac8263b2..a7549a3ec 100644 --- a/flixopt/config.py +++ b/flixopt/config.py @@ -286,7 +286,7 @@ def _apply_config_dict(cls, config_dict: dict): setattr(cls, key, value) @classmethod - def to_dict(cls): + def to_dict(cls) -> dict: """Convert the configuration class into a dictionary for JSON serialization. Returns: diff --git a/flixopt/core.py b/flixopt/core.py index c163de554..917ee2984 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -6,7 +6,7 @@ import logging import warnings from itertools import permutations -from typing import Literal, Union +from typing import Any, Literal, Union import numpy as np import pandas as pd @@ -46,12 +46,12 @@ class TimeSeriesData(xr.DataArray): def __init__( self, - *args, + *args: Any, aggregation_group: str | None = None, aggregation_weight: float | None = None, agg_group: str | None = None, agg_weight: float | None = None, - **kwargs, + **kwargs: Any, ): """ Args: diff --git a/flixopt/modeling.py b/flixopt/modeling.py index 88e652bc9..c7f0bf314 100644 --- a/flixopt/modeling.py +++ b/flixopt/modeling.py @@ -396,7 +396,7 @@ def basic_bounds( variable: linopy.Variable, bounds: tuple[TemporalData, TemporalData], name: str = None, - ): + ) -> list[linopy.constraints.Constraint]: """Create simple bounds. variable ∈ [lower_bound, upper_bound] @@ -409,9 +409,7 @@ def basic_bounds( bounds: Tuple of (lower_bound, upper_bound) absolute bounds Returns: - Tuple containing: - - variables (Dict): Empty dict - - constraints (Dict[str, linopy.Constraint]): 'ub', 'lb' + List containing lower_bound and upper_bound constraints """ if not isinstance(model, Submodel): raise ValueError('BoundingPatterns.basic_bounds() can only be used with a Submodel') diff --git a/flixopt/results.py b/flixopt/results.py index e571bc558..2e951af70 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -1243,7 +1243,7 @@ def node_balance_with_charge_state( class EffectResults(_ElementResults): """Results for an Effect""" - def get_shares_from(self, element: str): + def get_shares_from(self, element: str) -> xr.Dataset: """Get effect shares from specific element. Args: @@ -1399,7 +1399,7 @@ def from_calculation(cls, calculation: SegmentedCalculation): ) @classmethod - def from_file(cls, folder: str | pathlib.Path, name: str): + def from_file(cls, folder: str | pathlib.Path, name: str) -> SegmentedCalculationResults: """Load SegmentedCalculationResults from saved files. Args: diff --git a/flixopt/utils.py b/flixopt/utils.py index f1e12b9dc..dd1f93d64 100644 --- a/flixopt/utils.py +++ b/flixopt/utils.py @@ -5,7 +5,7 @@ from __future__ import annotations import logging -from typing import Literal +from typing import Any, Literal import numpy as np import xarray as xr @@ -13,7 +13,7 @@ logger = logging.getLogger('flixopt') -def round_nested_floats(obj, decimals=2): +def round_nested_floats(obj: dict | list | float | int | Any, decimals: int = 2) -> dict | list | float | int | Any: """Recursively round floating point numbers in nested data structures. This function traverses nested data structures (dictionaries, lists) and rounds @@ -27,9 +27,7 @@ def round_nested_floats(obj, decimals=2): decimals (int, optional): Number of decimal places to round to. Defaults to 2. Returns: - The processed object with the same structure as the input, but with all - floating point numbers rounded to the specified precision. NumPy arrays - and xarray DataArrays are converted to lists. + The processed object with the same structure as the input, but with all floating point numbers rounded to the specified precision. NumPy arrays and xarray DataArrays are converted to lists. Examples: >>> data = {'a': 3.14159, 'b': [1.234, 2.678]} diff --git a/mkdocs.yml b/mkdocs.yml index b7c03faac..89bca4793 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -13,6 +13,7 @@ nav: - Getting Started: getting-started.md - User Guide: - user-guide/index.md + - Migration to v3.0.0: user-guide/migration-guide-v3.md - Recipes: user-guide/recipes/index.md - Mathematical Notation: - Overview: user-guide/mathematical-notation/index.md @@ -34,28 +35,7 @@ nav: - State Transitions: user-guide/mathematical-notation/modeling-patterns/state-transitions.md - Examples: examples/ - Contribute: contribute.md - - API Reference: - - api-reference/index.md - - Aggregation: api-reference/aggregation.md - - Calculation: api-reference/calculation.md - - Commons: api-reference/commons.md - - Components: api-reference/components.md - - Config: api-reference/config.md - - Core: api-reference/core.md - - Effects: api-reference/effects.md - - Elements: api-reference/elements.md - - Features: api-reference/features.md - - Flow System: api-reference/flow_system.md - - Interface: api-reference/interface.md - - IO: api-reference/io.md - - Linear Converters: api-reference/linear_converters.md - - Modeling: api-reference/modeling.md - - Network App: api-reference/network_app.md - - Plotting: api-reference/plotting.md - - Results: api-reference/results.md - - Solvers: api-reference/solvers.md - - Structure: api-reference/structure.md - - Utils: api-reference/utils.md + - API Reference: api-reference/ - Release Notes: changelog/ @@ -99,12 +79,10 @@ theme: - content.code.copy - content.code.annotate - content.tooltips - - content.code.copy - navigation.footer.version markdown_extensions: - admonition - - codehilite - markdown_include.include: base_path: docs - pymdownx.highlight: @@ -112,7 +90,6 @@ markdown_extensions: line_spans: __span pygments_lang_class: true - pymdownx.inlinehilite - - pymdownx.snippets - pymdownx.superfences - attr_list - abbr @@ -135,6 +112,8 @@ plugins: - include-markdown - mike: version_selector: true + - literate-nav: + nav_file: SUMMARY.md - gen-files: scripts: - scripts/gen_ref_pages.py diff --git a/scripts/extract_changelog.py b/scripts/extract_changelog.py index c2c34d35b..d05229896 100644 --- a/scripts/extract_changelog.py +++ b/scripts/extract_changelog.py @@ -136,9 +136,13 @@ def extract_index(): raise ValueError('Intro section not found before comment block') final_content = intro_match.group(1).strip() - # Write file + # Write simple index without TOC (literate-nav plugin handles navigation) with open(index_path, 'w', encoding='utf-8') as f: - f.write('\n'.join(['# Changelog\n', final_content])) + f.write( + '# Changelog\n\n' + + final_content + + '\n\nUse the navigation menu to browse through individual release notes.' + ) print('✅ Created index.md') From d6bb01f5324b0a1d72daf4b657bd127e45a0c4aa Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 14 Oct 2025 00:52:39 +0200 Subject: [PATCH 347/448] Fix broken link in docs --- docs/user-guide/migration-guide-v3.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user-guide/migration-guide-v3.md b/docs/user-guide/migration-guide-v3.md index 4f85d901b..190503dc3 100644 --- a/docs/user-guide/migration-guide-v3.md +++ b/docs/user-guide/migration-guide-v3.md @@ -531,7 +531,7 @@ flow = fx.Flow('P_el', bus='electricity') # ✅ - **Documentation:** [https://flixopt.github.io/flixopt/](https://flixopt.github.io/flixopt/) - **GitHub Issues:** [https://github.com/flixOpt/flixopt/issues](https://github.com/flixOpt/flixopt/issues) -- **Changelog:** [Full v3.0.0 release notes](https://flixopt.github.io/flixopt/changelog/) +- **Changelog:** [Full v3.0.0 release notes](https://flixopt.github.io/flixopt/latest/changelog/99984-v3.0.0/) --- From 924ef17d22866a7716ce8e15873fe51f3349ac5e Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 15 Oct 2025 00:13:46 +0200 Subject: [PATCH 348/448] Feature/readme and vision (#405) * Update README.md * Update README.md * Update README.md * Add more badges because they are cool * Add more badges because they are cool * Add more badges because they are cool * Update Development Status to "Stable" * Update the vision * Update the "Why FlixOpt Exists" * Typos * Improve "User-Friendly Design" * Restructure the README.md * Update getting started * Tighten README.md * Add Roaadmap to docs * Make roadmap more compact * Updaet roadmap.md * Update roadmap.md * Update mkdocs.yml * Update roadmap.md --- README.md | 185 ++++++++++++++++++++++++++++++++---------------- docs/roadmap.md | 49 +++++++++++++ mkdocs.yml | 5 +- pyproject.toml | 2 +- 4 files changed, 177 insertions(+), 64 deletions(-) create mode 100644 docs/roadmap.md diff --git a/README.md b/README.md index 957274f1c..0a90dcb33 100644 --- a/README.md +++ b/README.md @@ -3,115 +3,178 @@ [![Documentation](https://img.shields.io/badge/docs-latest-brightgreen.svg)](https://flixopt.github.io/flixopt/latest/) [![Build Status](https://github.com/flixOpt/flixopt/actions/workflows/python-app.yaml/badge.svg)](https://github.com/flixOpt/flixopt/actions/workflows/python-app.yaml) [![PyPI version](https://img.shields.io/pypi/v/flixopt)](https://pypi.org/project/flixopt/) +[![PyPI status](https://img.shields.io/pypi/status/flixopt.svg)](https://pypi.org/project/flixopt/) [![Python Versions](https://img.shields.io/pypi/pyversions/flixopt.svg)](https://pypi.org/project/flixopt/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![PyPI downloads](https://img.shields.io/pypi/dm/flixopt)](https://pypi.org/project/flixopt/) +[![GitHub last commit](https://img.shields.io/github/last-commit/flixOpt/flixopt)](https://github.com/flixOpt/flixopt/commits/main) +[![GitHub issues](https://img.shields.io/github/issues/flixOpt/flixopt)](https://github.com/flixOpt/flixopt/issues) +[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/flixOpt/flixopt/main.svg)](https://results.pre-commit.ci/latest/github/flixOpt/flixopt/main) +[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) +[![Powered by linopy](https://img.shields.io/badge/powered%20by-linopy-blue)](https://github.com/PyPSA/linopy/) +[![Powered by xarray](https://img.shields.io/badge/powered%20by-xarray-blue)](https://xarray.dev/) +[![DOI](https://img.shields.io/badge/DOI-10.18086%2Feurosun.2022.04.07-blue)](https://doi.org/10.18086/eurosun.2022.04.07) +[![GitHub stars](https://img.shields.io/github/stars/flixOpt/flixopt?style=social)](https://github.com/flixOpt/flixopt/stargazers) --- -## 🎯 Vision +**FlixOpt is a Python framework for optimizing energy and material flow systems** - from district heating networks to industrial production lines, from renewable energy portfolios to supply chain logistics. -**FlixOpt aims to be the most accessible and flexible Python framework for energy and material flow optimization.** +**Start simple, scale complex:** Build a working optimization model in minutes, then progressively add detail - multi-period investments, stochastic scenarios, custom constraints - without rewriting your code. -We believe that optimization modeling should be **approachable for beginners** yet **powerful for experts**. Too often, frameworks force you to choose between ease of use and flexibility. FlixOpt refuses this compromise. - -### Where We're Going +--- -**Short-term goals:** -- **Multi-dimensional modeling**: Full support for multi-period investments and scenario-based stochastic optimization (periods and scenarios are in active development) -- **Enhanced component library**: More pre-built, domain-specific components (sector coupling, hydrogen systems, thermal networks, demand-side management) +## 🚀 Quick Start -**Medium-term vision:** -- **Modeling to generate alternatives (MGA)**: Built-in support for exploring near-optimal solution spaces to produce more robust, diverse solutions under uncertainty -- **Interactive tutorials**: Browser-based, reactive tutorials for learning FlixOpt without local installation -- **Standardized cost calculations**: Align with industry standards (VDI 2067) for CAPEX/OPEX calculations -- **Advanced result analysis**: Time-series aggregation, automated reporting, and rich visualization options +```bash +pip install flixopt +``` -**Long-term vision:** -- **Showcase universal applicability**: FlixOpt already handles any flow-based system (supply chains, water networks, production planning, chemical processes) - we need more examples and domain-specific component libraries to demonstrate this -- **Seamless integration**: First-class support for coupling with simulation tools, databases, existing energy system models, and GIS data -- **Robust optimization**: Built-in uncertainty quantification and stochastic programming capabilities -- **Community ecosystem**: Rich library of user-contributed components, examples, and domain-specific extensions -- **Model validation tools**: Automated checks for physical plausibility, data consistency, and common modeling errors +That's it! FlixOpt comes with the [HiGHS](https://highs.dev/) solver included. You're ready to optimize. -### Why FlixOpt Exists +**The basic workflow:** -FlixOpt is a **general-purpose framework for modeling any system involving flows and conversions** - energy, materials, fluids, goods, or data. While energy systems are our primary focus, the same mathematical foundation applies to supply chains, water networks, production lines, and more. +```python +import flixopt as fx -We bridge the gap between high-level strategic models (like [FINE](https://github.com/FZJ-IEK3-VSA/FINE)) for long-term planning and low-level dispatch tools for operations. FlixOpt is the **sweet spot** for: +# 1. Define your system structure +flow_system = fx.FlowSystem(timesteps) +flow_system.add_elements(buses, components, effects) -- **Researchers** who need to prototype quickly but may require deep customization later -- **Engineers** who want reliable, tested components without black-box abstractions -- **Students** learning optimization who benefit from clear, Pythonic interfaces -- **Practitioners** who need to move from model to production-ready results -- **Domain experts** from any field where things flow, transform, and need optimizing +# 2. Create and solve +calculation = fx.FullCalculation("MyModel", flow_system) +calculation.solve() -Built on modern foundations ([linopy](https://github.com/PyPSA/linopy/) and [xarray](https://github.com/pydata/xarray)), FlixOpt delivers both **performance** and **transparency**. You can inspect everything, extend anything, and trust that your model does exactly what you designed. +# 3. Analyze results +calculation.results.solution +``` -Originally developed at [TU Dresden](https://github.com/gewv-tu-dresden) for the SMARTBIOGRID project (funded by the German Federal Ministry for Economic Affairs and Energy, FKZ: 03KB159B), FlixOpt has evolved from the Matlab-based flixOptMat framework while incorporating the best ideas from [oemof/solph](https://github.com/oemof/oemof-solph). +**Get started with real examples:** +- 📚 [Full Documentation](https://flixopt.github.io/flixopt/latest/) +- 💡 [Examples Gallery](https://flixopt.github.io/flixopt/latest/examples/) - Complete working examples from simple to complex +- 🔧 [API Reference](https://flixopt.github.io/flixopt/latest/api-reference/) --- -## 🌟 What Makes FlixOpt Different +## 🌟 Why FlixOpt? -### Start Simple, Scale Complex -Define a working model in minutes with high-level components, then drill down to fine-grained control when needed. No rewriting, no framework switching. +### Progressive Enhancement - Your Model Grows With You +**Start simple:** ```python -import flixopt as fx - -# Simple start +# Basic single-period model +flow_system = fx.FlowSystem(timesteps) boiler = fx.Boiler("Boiler", eta=0.9, ...) - -# Advanced control when needed - extend with native linopy -boiler.model.add_constraints(custom_constraint, name="my_constraint") ``` -### Multi-Criteria Optimization Done Right -Model costs, emissions, resource use, and any custom metric simultaneously as **Effects**. Optimize any single Effect, use weighted combinations, or apply ε-constraints: +**Add complexity incrementally:** +- **Investment decisions** → Add `InvestParameters` to components +- **Multi-period planning** → Add `periods` dimension to FlowSystem +- **Uncertainty modeling** → Add `scenarios` dimension with probabilities +- **Custom constraints** → Extend with native linopy syntax -```python -costs = fx.Effect('costs', '€', 'Total costs', - share_from_temporal={'CO2': 180}) # 180 €/tCO2 -co2 = fx.Effect('CO2', 'kg', 'Emissions', maximum_periodic=50000) -``` +**No refactoring required.** Your component definitions stay the same - periods, scenarios, and features are added as dimensions and parameters. + +→ [Learn more about multi-period and stochastic modeling](https://flixopt.github.io/flixopt/latest/user-guide/mathematical-notation/dimensions/) + +### For Everyone + +- **Beginners:** High-level components that "just work" +- **Experts:** Full access to modify models with linopy +- **Researchers:** Quick prototyping with customization options +- **Engineers:** Reliable, tested components without black boxes +- **Students:** Clear, Pythonic interfaces for learning optimization -### Performance at Any Scale -Choose the right calculation mode for your problem: -- **Full** - Maximum accuracy for smaller problems -- **Segmented** - Rolling horizon for large time series -- **Aggregated** - Typical periods using [TSAM](https://github.com/FZJ-IEK3-VSA/tsam) for massive models +### Key Features -### Built for Reproducibility -Every result file is self-contained with complete model information. Load it months later and know exactly what you optimized. Export to NetCDF, share with colleagues, archive for compliance. +**Multi-criteria optimization:** Model costs, emissions, resource use - any custom metric. Optimize single objectives or use weighted combinations and ε-constraints. +→ [Effects documentation](https://flixopt.github.io/flixopt/latest/user-guide/mathematical-notation/effects-penalty-objective/) + +**Performance at any scale:** Choose calculation modes without changing your model - Full, Segmented, or Aggregated (using [TSAM](https://github.com/FZJ-IEK3-VSA/tsam)). +→ [Calculation modes](https://flixopt.github.io/flixopt/latest/api-reference/calculation/) + +**Built for reproducibility:** Self-contained NetCDF result files with complete model information. Load results months later - everything is preserved. +→ [Results documentation](https://flixopt.github.io/flixopt/latest/api-reference/results/) + +**Flexible data operations:** Transform FlowSystems with xarray-style operations (`sel()`, `resample()`) for multi-stage optimization. --- -## 🚀 Quick Start +## 🎯 What is FlixOpt? + +### A General-Purpose Flow Optimization Framework + +FlixOpt models **any system involving flows and conversions:** + +- **Energy systems:** District heating/cooling, microgrids, renewable portfolios, sector coupling +- **Material flows:** Supply chains, production lines, chemical processes +- **Integrated systems:** Water-energy nexus, industrial symbiosis + +While energy systems are our primary focus, the same foundation applies universally. This enables coupling different system types within integrated models. + +### Modern Foundations + +Built on [linopy](https://github.com/PyPSA/linopy/) and [xarray](https://github.com/pydata/xarray), FlixOpt delivers **performance** and **transparency**. Full access to variables, constraints, and model structure. Extend anything with native linopy syntax. + +### Our Position + +We bridge the gap between high-level strategic models (like [FINE](https://github.com/FZJ-IEK3-VSA/FINE)) and low-level dispatch tools - similar to [PyPSA](https://docs.pypsa.org/latest/). FlixOpt is the sweet spot for detailed operational planning and long-term investment analysis in the **same framework**. + +### Academic Roots + +Originally developed at [TU Dresden](https://github.com/gewv-tu-dresden) for the SMARTBIOGRID project (funded by the German Federal Ministry for Economic Affairs and Energy, FKZ: 03KB159B). FlixOpt evolved from the MATLAB-based flixOptMat framework while incorporating best practices from [oemof/solph](https://github.com/oemof/oemof-solph). + +--- + +## 🛣️ Roadmap + +**FlixOpt aims to be the most accessible, flexible, and universal Python framework for energy and material flow optimization.** We believe optimization modeling should be approachable for beginners yet powerful for experts, minimizing context switching between different planning horizons. + +**Current focus:** +- Enhanced component library (sector coupling, hydrogen, thermal networks) +- Examples showcasing multi-period and stochastic modeling +- Advanced result analysis and visualization + +**Future vision:** +- Modeling to generate alternatives (MGA) for robust decision-making +- Advanced stochastic optimization (two-stage, CVaR) +- Community ecosystem of user-contributed components + +→ [Full roadmap and vision](https://flixopt.github.io/flixopt/latest/roadmap/) + +--- + +## 🛠️ Installation + +### Basic Installation ```bash pip install flixopt ``` -That's it. FlixOpt comes with the [HiGHS](https://highs.dev/) solver included - you're ready to optimize. -Many more solvers are supported (gurobi, cplex, cbc, glpk, ...) +Includes the [HiGHS](https://highs.dev/) solver - you're ready to optimize immediately. + +### Full Installation For additional features (interactive network visualization, time series aggregation): + ```bash pip install "flixopt[full]" ``` -**Next steps:** -- 📚 [Full Documentation](https://flixopt.github.io/flixopt/latest/) -- 💡 [Examples](https://flixopt.github.io/flixopt/latest/examples/) -- 🔧 [API Reference](https://flixopt.github.io/flixopt/latest/api-reference/) +### Solver Support + +FlixOpt supports many solvers via linopy: **HiGHS** (included), **Gurobi**, **CPLEX**, **CBC**, **GLPK**, and more. + +→ [Installation guide](https://flixopt.github.io/flixopt/latest/getting-started/) --- ## 🤝 Contributing -FlixOpt thrives on community input. Whether you're fixing bugs, adding components, improving docs, or sharing use cases - we welcome your contributions. +FlixOpt thrives on community input. Whether you're fixing bugs, adding components, improving docs, or sharing use cases - **we welcome your contributions.** -See our [contribution guide](https://flixopt.github.io/flixopt/latest/contribute/) to get started. +→ [Contribution guide](https://flixopt.github.io/flixopt/latest/contribute/) --- diff --git a/docs/roadmap.md b/docs/roadmap.md new file mode 100644 index 000000000..fbad1043c --- /dev/null +++ b/docs/roadmap.md @@ -0,0 +1,49 @@ +# Roadmap and Vision + +## 🎯 Our Vision + +**FlixOpt aims to be the most accessible, flexible, and universal Python framework for energy and material flow optimization.** + +We believe optimization modeling should be **approachable for beginners** yet **powerful for experts**, minimizing context switching between **short-term dispatch** and **long-term investment** planning. + +--- + +## 🚀 Short-term (Next 6 months) + +- **Recipe collection** - Community-driven library of common modeling patterns, data manipulation techniques, and optimization strategies +- **Examples of stochastic and multi-period modeling** - The new v3.0 features currently lack comprehensive showcases +- **Advanced result analysis** - Automated reporting and enhanced visualization options +- **Interactive tutorials** - Browser-based, reactive tutorials for learning FlixOpt without local installation using [Marimo](https://marimo.io/) + +## 🔮 Medium-term (6-12 months) + +- **Modeling to Generate Alternatives (MGA)** - Built-in support for exploring near-optimal solution spaces to produce more robust, diverse solutions under uncertainty. See [PyPSA](https://docs.pypsa.org/latest/user-guide/optimization/modelling-to-generate-alternatives/) and [Calliope](https://calliope.readthedocs.io/en/latest/examples/modes/) for reference implementations +- **Advanced stochastic optimization** - Build sophisticated new `Calculation` classes to perform different stochastic optimization approaches, like PyPSA's [two-stage stochastic programming and risk preferences with Conditional Value-at-Risk (CVaR)](https://docs.pypsa.org/latest/user-guide/optimization/stochastic/) +- **Enhanced component library** - More pre-built, domain-specific components (sector coupling, hydrogen systems, thermal networks, demand-side management) + +## 🌟 Long-term (12+ months) + +- **Showcase universal applicability** - FlixOpt already handles any flow-based system (supply chains, water networks, production planning, chemical processes) - we need more examples and domain-specific component libraries to demonstrate this +- **Community ecosystem** - Rich library of user-contributed components, examples, and domain-specific extensions + +--- + +## 🤝 How to Help + +- **Code**: Implement features, fix bugs, add tests +- **Docs**: Write tutorials, improve examples, create case studies +- **Components**: Contribute domain-specific components +- **Feedback**: [Report issues](https://github.com/flixOpt/flixopt/issues), [join discussions](https://github.com/flixOpt/flixopt/discussions) + +See our [contribution guide](contribute.md) to get started. + +--- + +## 📅 Release Philosophy + +FlixOpt follows [semantic versioning](https://semver.org/): +- **Major** (v3→v4): Breaking changes, major features +- **Minor** (v3.0→v3.1): New features, backward compatible +- **Patch** (v3.0.0→v3.0.1): Bug fixes only + +Target: Patch releases as needed, minor releases every 2-3 months. diff --git a/mkdocs.yml b/mkdocs.yml index 89bca4793..72ecbe549 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -10,11 +10,10 @@ repo_name: flixOpt/flixopt nav: - Home: index.md - - Getting Started: getting-started.md - User Guide: - user-guide/index.md - Migration to v3.0.0: user-guide/migration-guide-v3.md - - Recipes: user-guide/recipes/index.md + - Getting Started: getting-started.md - Mathematical Notation: - Overview: user-guide/mathematical-notation/index.md - Dimensions: user-guide/mathematical-notation/dimensions.md @@ -33,6 +32,8 @@ nav: - Bounds and States: user-guide/mathematical-notation/modeling-patterns/bounds-and-states.md - Duration Tracking: user-guide/mathematical-notation/modeling-patterns/duration-tracking.md - State Transitions: user-guide/mathematical-notation/modeling-patterns/state-transitions.md + - Recipes: user-guide/recipes/index.md + - Roadmap: roadmap.md - Examples: examples/ - Contribute: contribute.md - API Reference: api-reference/ diff --git a/pyproject.toml b/pyproject.toml index 6ea2f232e..391f23c3a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ maintainers = [ ] keywords = ["optimization", "energy systems", "numerical analysis"] classifiers = [ - "Development Status :: 3 - Alpha", + "Development Status :: 5 - Production/Stable", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", From 79233f1cf9477735973f26c2ff66018f497e2be6 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 15 Oct 2025 00:24:45 +0200 Subject: [PATCH 349/448] Update CHANGELOG.md --- CHANGELOG.md | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e5f9ff3d..2a043fc56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,9 +69,23 @@ Please keep the format of the changelog consistent with the other releases, so t --- Until here --> +## [3.0.2] - 2025-10-15 +**Summary**: This is a follow-up release to **[v3.0.0](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0)**, improving the documentation. + +**Note**: If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/). + +### 📝 Docs +- Update the Readme +- Add a project roadmap to the docs +- Change Development status to "Production/Stable" +- Regroup parts in docs + +--- + ## [3.0.1] - 2025-10-14 -**Summary**: Adding a Migration Guide for the new **flixopt 3** and fixing docs -See the [Migration Guide](https://flixopt.github.io/flixopt/user-guide/migration-guide-v3/). +**Summary**: This is a follow-up release to **[v3.0.0](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0)**, adding a Migration Guide and bugfixing the docs. + +**Note**: If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/). ### 📝 Docs - Fixed deployed docs @@ -84,7 +98,8 @@ See the [Migration Guide](https://flixopt.github.io/flixopt/user-guide/migration ## [3.0.0] - 2025-10-13 **Summary**: This release introduces new model dimensions (periods and scenarios) for multi-period investments and stochastic modeling, along with a redesigned effect sharing system and enhanced I/O capabilities. -For detailed migration instructions, see the [Migration Guide](https://flixopt.github.io/flixopt/user-guide/migration-guide-v3/). + +**Note**: If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/). ### ✨ Added From 64eb5ed924d5189f20e53605135f30d499e7b62e Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 15 Oct 2025 00:35:04 +0200 Subject: [PATCH 350/448] Fix workflow to run example test IN PARALLEL and WITH WARNINGS --- .github/workflows/python-app.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml index fe86c8dfe..7c1962f03 100644 --- a/.github/workflows/python-app.yaml +++ b/.github/workflows/python-app.yaml @@ -117,7 +117,7 @@ jobs: uv pip install --system .[dev] - name: Run example tests - run: pytest -v -p no:warnings -m examples + run: pytest -v -m examples --numprocesses=auto security: name: Security Scan From e04c5aa799d42f47298c41a87a27e1e6d3aa1390 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 15 Oct 2025 00:50:31 +0200 Subject: [PATCH 351/448] Update CI --- .github/workflows/python-app.yaml | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml index 7c1962f03..d8caba0d4 100644 --- a/.github/workflows/python-app.yaml +++ b/.github/workflows/python-app.yaml @@ -342,25 +342,20 @@ jobs: # Wait and retry while PyPI indexes the package INSTALL_SUCCESS=false - for d in 10 20 40 80 120 240 480; do + for d in 10 20 40 60 90 120 180 300 480 600; do # Total: up to ~30 minutes sleep "$d" echo "Attempting to install $PACKAGE_NAME==$VERSION from PyPI (retry after ${d}s)..." - - # Install specific version and verify it matches - if uv pip install "$PACKAGE_NAME==$VERSION" && \ + # Install directly from pypi, potentially mitigatiing caches + if uv pip install --index-url https://pypi.org/simple/ "$PACKAGE_NAME==$VERSION" && \ python -c "from importlib.metadata import version; installed = version('$PACKAGE_NAME'); print(f'Installed: {installed}'); assert '$VERSION' == installed"; then INSTALL_SUCCESS=true break fi done - # Check if installation succeeded if [ "$INSTALL_SUCCESS" = "false" ]; then echo "ERROR: Failed to install $PACKAGE_NAME==$VERSION from PyPI after all retries" - echo "This could indicate:" - echo " - PyPI indexing issues" - echo " - Package upload problems" - echo " - Version mismatch between tag and package" + echo "Check: https://pypi.org/project/$PACKAGE_NAME/$VERSION/" exit 1 fi From c70a54c8f59a97951d39ec8d63f0d801363c0edc Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 15 Oct 2025 02:25:00 +0200 Subject: [PATCH 352/448] Revert Development increase to new Status "Beta" --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 391f23c3a..9e1821a4b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ maintainers = [ ] keywords = ["optimization", "energy systems", "numerical analysis"] classifiers = [ - "Development Status :: 5 - Production/Stable", + "Development Status :: 4 - Beta", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", From 8294a64783e6120410fcc94c14af3cddaed67673 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 15 Oct 2025 09:01:43 +0200 Subject: [PATCH 353/448] Use Readme in docs for maintainability --- docs/index.md | 108 +++++--------------------------------------------- 1 file changed, 11 insertions(+), 97 deletions(-) diff --git a/docs/index.md b/docs/index.md index 2c6420f7f..6723c4d5e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,108 +1,22 @@ -# FlixOpt - -## 🎯 Vision - -**FlixOpt aims to be the most accessible and flexible Python framework for energy and material flow optimization.** - -We believe that optimization modeling should be **approachable for beginners** yet **powerful for experts**. Too often, frameworks force you to choose between ease of use and flexibility. FlixOpt refuses this compromise. - -### Where We're Going - -**Short-term goals:** - -- **Multi-dimensional modeling**: Multi-period investments and scenario-based stochastic optimization are available (periods and scenarios are in active development for enhanced features) -- **Enhanced component library**: More pre-built, domain-specific components (sector coupling, hydrogen systems, thermal networks, demand-side management) - -**Medium-term vision:** - -- **Modeling to generate alternatives (MGA)**: Built-in support for exploring near-optimal solution spaces to produce more robust, diverse solutions under uncertainty -- **Interactive tutorials**: Browser-based, reactive tutorials for learning FlixOpt without local installation ([marimo](https://marimo.io)) -- **Standardized cost calculations**: Align with industry standards (VDI 2067) for CAPEX/OPEX calculations -- **Advanced result analysis**: Time-series aggregation, automated reporting, and rich visualization options -- **Recipe collection**: Community-driven library of common modeling patterns, data manipulation techniques, and optimization strategies (see [Recipes](user-guide/recipes/index.md) - help wanted!) - -**Long-term vision:** - -- **Showcase universal applicability**: FlixOpt already handles any flow-based system (supply chains, water networks, production planning, chemical processes) - we need more examples and domain-specific component libraries to demonstrate this -- **Seamless integration**: First-class support for coupling with simulation tools, databases, existing energy system models, and GIS data -- **Robust optimization**: Built-in uncertainty quantification and stochastic programming capabilities -- **Community ecosystem**: Rich library of user-contributed components, examples, and domain-specific extensions -- **Model validation tools**: Automated checks for physical plausibility, data consistency, and common modeling errors - -### Why FlixOpt Exists - -FlixOpt is a **general-purpose framework for modeling any system involving flows and conversions** - energy, materials, fluids, goods, or data. While energy systems are our primary focus, the same mathematical foundation applies to supply chains, water networks, production lines, and more. - -We bridge the gap between high-level strategic models (like [FINE](https://github.com/FZJ-IEK3-VSA/FINE)) for long-term planning and low-level dispatch tools for operations. FlixOpt is the **sweet spot** for: - -- **Researchers** who need to prototype quickly but may require deep customization later -- **Engineers** who want reliable, tested components without black-box abstractions -- **Students** learning optimization who benefit from clear, Pythonic interfaces -- **Practitioners** who need to move from model to production-ready results -- **Domain experts** from any field where things flow, transform, and need optimizing - -Built on modern foundations ([linopy](https://github.com/PyPSA/linopy/) and [xarray](https://github.com/pydata/xarray)), FlixOpt delivers both **performance** and **transparency**. You can inspect everything, extend anything, and trust that your model does exactly what you designed. - -Originally developed at [TU Dresden](https://github.com/gewv-tu-dresden) for the SMARTBIOGRID project (funded by the German Federal Ministry for Economic Affairs and Energy, FKZ: 03KB159B), FlixOpt has evolved from the Matlab-based flixOptMat framework while incorporating the best ideas from [oemof/solph](https://github.com/oemof/oemof-solph). +{% + include-markdown "../README.md" +%} --- -## What Makes FlixOpt Different - -### Start Simple, Scale Complex -Define a working model in minutes with high-level components, then drill down to fine-grained control when needed. No rewriting, no framework switching. - -```python -import flixopt as fx - -# Simple start -boiler = fx.Boiler("Boiler", eta=0.9, ...) - -# Advanced control when needed - extend with native linopy -boiler.model.add_constraints(custom_constraint, name="my_constraint") -``` - -### Multi-Criteria Optimization Done Right -Model costs, emissions, resource use, and any custom metric simultaneously as **Effects**. Optimize any single Effect, use weighted combinations, or apply ε-constraints: - -```python -costs = fx.Effect('costs', '€', 'Total costs', - share_from_temporal={'CO2': 180}) # 180 €/tCO2 -co2 = fx.Effect('CO2', 'kg', 'Emissions', maximum_periodic=50000) -``` - -### Performance at Any Scale -Choose the right calculation mode for your problem: - -- **Full** - Maximum accuracy for smaller problems -- **Segmented** - Rolling horizon for large time series -- **Aggregated** - Typical periods using [TSAM](https://github.com/FZJ-IEK3-VSA/tsam) for massive models - -### Built for Reproducibility -Every result file is self-contained with complete model information. Load it months later and know exactly what you optimized. Export to NetCDF, share with colleagues, archive for compliance. +## Documentation Architecture
![FlixOpt Conceptual Usage](./images/architecture_flixOpt.png)
Conceptual Usage and IO operations of FlixOpt
-## Installation - -```bash -pip install flixopt -``` - -For more detailed installation options, see the [Getting Started](getting-started.md) guide. - -## License - -FlixOpt is released under the MIT License. See [LICENSE](https://github.com/flixopt/flixopt/blob/main/LICENSE) for details. - -## Citation - -If you use FlixOpt in your research or project, please cite: +--- -- **Main Citation:** [DOI:10.18086/eurosun.2022.04.07](https://doi.org/10.18086/eurosun.2022.04.07) -- **Short Overview:** [DOI:10.13140/RG.2.2.14948.24969](https://doi.org/10.13140/RG.2.2.14948.24969) +## Next Steps -*A more sophisticated paper is in progress* +- **New to FlixOpt?** Start with the [Getting Started Guide](getting-started.md) +- **Want to see examples?** Check out the [Examples Gallery](examples/index.md) +- **Need API details?** Browse the [API Reference](api-reference/index.md) +- **Looking for advanced patterns?** See [Recipes](user-guide/recipes/index.md) +- **Curious about the future?** Read our [Roadmap](roadmap.md) From 552569ac25bab187b111fcf36fd3cfd367fe791f Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 16 Oct 2025 13:58:00 +0200 Subject: [PATCH 354/448] Fix/update migration guide and changelog (#411) * Fix Docs * Update Changelog and migration guide for missing breaking changes * Update Migration guide * Improve * Use tabs in mkdocs * Update varaibel renaming * Update variable renaming * From main * Update * Update * Update * Add links * Add emojis * compact 2 * Enable plugin * Update * Update * Update CHangelog.md --- CHANGELOG.md | 40 +- docs/user-guide/migration-guide-v3.md | 615 +++++++------------------- mkdocs.yml | 1 + 3 files changed, 183 insertions(+), 473 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a043fc56..632fcf7a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,8 @@ Please keep the format of the changelog consistent with the other releases, so t ### 📦 Dependencies ### 📝 Docs +- Updated Migration Guide and added missing entries. +- Improved Changelog of v3.0.0 ### 👷 Development @@ -139,17 +141,43 @@ This replaces `specific_share_to_other_effects_*` parameters and inverts the dir ### 💥 Breaking Changes -- `relative_minimum_charge_state` and `relative_maximum_charge_state` don't have an extra timestep anymore. +**API and Behavior Changes:** + +- **Effect system redesigned** (no deprecation): + - **Terminology changes**: Effect domains renamed for clarity: `operation` → `temporal`, `invest`/`investment` → `periodic` + - **Sharing system**: The old `specific_share_to_other_effects_*` parameters were completely replaced with the new `share_from_temporal` and `share_from_periodic` syntax (see 🔥 Removed section) +- **FlowSystem independence**: FlowSystems cannot be shared across multiple Calculations anymore. A copy of the FlowSystem is created instead, making every Calculation independent. Each Subcalculation in `SegmentedCalculation` now has its own distinct `FlowSystem` object +- **Bus and Effect object assignment**: Direct assignment of Bus/Effect objects is no longer supported. Use labels (strings) instead: + - `Flow.bus` must receive a string label, not a Bus object + - Effect shares must use effect labels (strings) in dictionaries, not Effect objects +- **Logging defaults** (from v2.2.0): Console and file logging are now disabled by default. Enable explicitly with `CONFIG.Logging.console = True` and `CONFIG.apply()` + +**Class and Method Renaming:** + - Renamed class `SystemModel` to `FlowSystemModel` - Renamed class `Model` to `Submodel` - Renamed `mode` parameter in plotting methods to `style` -- Renamed investment binary variable `is_invested` to `invested` in `InvestmentModel` -- `Calculation.do_modeling()` now returns the `Calculation` object instead of its `linopy.Model`. Callers that previously accessed the linopy model directly should now use `calculation.do_modeling().model` instead of `calculation.do_modeling()`. +- `Calculation.do_modeling()` now returns the `Calculation` object instead of its `linopy.Model`. Callers that previously accessed the linopy model directly should now use `calculation.do_modeling().model` instead of `calculation.do_modeling()` + +**Variable Renaming in Results:** + +- Investment binary variable: `is_invested` → `invested` in `InvestmentModel` +- Switch tracking variables in `OnOffModel`: + - `switch_on` → `switch|on` + - `switch_off` → `switch|off` + - `switch_on_nr` → `switch|count` +- Effect submodel variables (following terminology changes): + - `Effect(invest)|total` → `Effect(periodic)` + - `Effect(operation)|total` → `Effect(temporal)` + - `Effect(operation)|total_per_timestep` → `Effect(temporal)|per_timestep` + - `Effect|total` → `Effect` + +**Data Structure Changes:** + +- `relative_minimum_charge_state` and `relative_maximum_charge_state` don't have an extra timestep anymore. Use the new `relative_minimum_final_charge_state` and `relative_maximum_final_charge_state` parameters for final state control ### ♻️ Changed -- FlowSystems cannot be shared across multiple Calculations anymore. A copy of the FlowSystem is created instead, making every Calculation independent -- Each Subcalculation in `SegmentedCalculation` now has its own distinct `FlowSystem` object - Type system overhaul - added clear separation between temporal and non-temporal data throughout codebase for better clarity - Enhanced FlowSystem interface with improved `__repr__()` and `__str__()` methods - Improved Model Structure - Views and organisation is now divided into: @@ -164,8 +192,6 @@ This replaces `specific_share_to_other_effects_*` parameters and inverts the dir - The `agg_group` and `agg_weight` parameters of `TimeSeriesData` are deprecated and will be removed in a future version. Use `aggregation_group` and `aggregation_weight` instead. - The `active_timesteps` parameter of `Calculation` is deprecated and will be removed in a future version. Use the new `sel(time=...)` method on the FlowSystem instead. -- The assignment of Bus Objects to Flow.bus is deprecated and will be removed in a future version. Use the label of the Bus instead. -- The usage of Effects objects in Dicts to assign shares to Effects is deprecated and will be removed in a future version. Use the label of the Effect instead. - **InvestParameters** parameters renamed for improved clarity around investment and retirement effects: - `fix_effects` → `effects_of_investment` - `specific_effects` → `effects_of_investment_per_size` diff --git a/docs/user-guide/migration-guide-v3.md b/docs/user-guide/migration-guide-v3.md index 190503dc3..a98824de1 100644 --- a/docs/user-guide/migration-guide-v3.md +++ b/docs/user-guide/migration-guide-v3.md @@ -1,550 +1,233 @@ -# Migration Guide: Upgrading to v3.0.0 - -This guide helps you migrate your flixopt code from v2.x to v3.0.0. Version 3.0.0 introduces powerful new features like multi-period investments and scenario-based stochastic optimization, along with a redesigned effect sharing system. +# Migration Guide: v2.x → v3.0.0 !!! tip "Quick Start" - 1. **Update your installation:** - ```bash - pip install --upgrade flixopt - ``` - 2. **Review breaking changes** in the sections below - 3. **Update deprecated parameters** to their new names - 4. **Test your code** with the new version + ```bash + pip install --upgrade flixopt + ``` + Review [breaking changes](#breaking-changes), update [deprecated parameters](#deprecated-parameters), test thoroughly. --- -## Breaking Changes +## 💥 Breaking Changes -### 1. Effect Sharing System Redesign +### Effect System Redesign -!!! warning "Breaking Change - No Deprecation" - The effect sharing syntax has been inverted and simplified. This change was made WITHOUT deprecation warnings due to the fundamental restructuring. +Terminology changed and sharing system inverted: effects now "pull" shares. -**What changed:** Effects now "pull" shares from other effects instead of "pushing" them. - -=== "v2.x (Old)" +| Concept | Old (v2.x) | New (v3.0.0) | +|---------|------------|--------------| +| Time-varying effects | `operation` | `temporal` | +| Investment effects | `invest` / `investment` | `periodic` | +| Share to other effects (operation) | `specific_share_to_other_effects_operation` | `share_from_temporal` | +| Share to other effects (invest) | `specific_share_to_other_effects_invest` | `share_from_periodic` | +=== "v2.x" ```python - # Effects "pushed" shares to other effects - CO2 = fx.Effect('CO2', 'kg', 'CO2 emissions', + CO2 = fx.Effect('CO2', 'kg', 'CO2', specific_share_to_other_effects_operation={'costs': 0.2}) - - land = fx.Effect('land', 'm²', 'Land usage', - specific_share_to_other_effects_invest={'costs': 100}) - - costs = fx.Effect('costs', '€', 'Total costs') + costs = fx.Effect('costs', '€', 'Total') ``` -=== "v3.0.0 (New)" - +=== "v3.0.0" ```python - # Effects "pull" shares from other effects (clearer direction) - CO2 = fx.Effect('CO2', 'kg', 'CO2 emissions') - - land = fx.Effect('land', 'm²', 'Land usage') - - costs = fx.Effect('costs', '€', 'Total costs', - share_from_temporal={'CO2': 0.2}, # From temporal (operation) effects - share_from_periodic={'land': 100}) # From periodic (investment) effects + CO2 = fx.Effect('CO2', 'kg', 'CO2') + costs = fx.Effect('costs', '€', 'Total', + share_from_temporal={'CO2': 0.2}) # Pull from CO2 ``` -!!! success "Migration Steps" - 1. Find all uses of `specific_share_to_other_effects_operation` and `specific_share_to_other_effects_invest` - 2. Move the share definition to the **receiving** effect - 3. Rename parameters: - - `specific_share_to_other_effects_operation` → `share_from_temporal` - - `specific_share_to_other_effects_invest` → `share_from_periodic` +!!! warning "No deprecation warning" + Move shares to receiving effect and update parameter names throughout your code. --- -### 2. Class and Variable Renaming - -=== "v2.x (Old)" +### Variable Names - ```python - # In optimization results - results.solution['component|is_invested'] - ``` - -=== "v3.0.0 (New)" - - ```python - # In optimization results - results.solution['component|invested'] - ``` +| Category | Old (v2.x) | New (v3.0.0) | +|---------------------------------|------------|--------------| +| Investment | `is_invested` | `invested` | +| Switching | `switch_on` | `switch|on` | +| Switching | `switch_off` | `switch|off` | +| Switching | `switch_on_nr` | `switch|count` | +| Effects | `Effect(invest)|total` | `Effect(periodic)` | +| Effects | `Effect(operation)|total` | `Effect(temporal)` | +| Effects | `Effect(operation)|total_per_timestep` | `Effect(temporal)|per_timestep` | +| Effects | `Effect|total` | `Effect` | --- -### 3. Calculation API Change - -!!! info "Method Chaining Support" - `Calculation.do_modeling()` now returns the Calculation object to enable method chaining. +### String Labels -=== "v2.x (Old)" +| What | Old (v2.x) | New (v3.0.0) | +|------|------------|--------------| +| Bus assignment | `bus=my_bus` (object) | `bus='electricity'` (string) | +| Effect shares | `{CO2: 0.2}` (object key) | `{'CO2': 0.2}` (string key) | +=== "v2.x" ```python - calculation = fx.FullCalculation('my_calc', flow_system) - linopy_model = calculation.do_modeling() # Returned linopy.Model - - # Access model directly from return value - print(linopy_model) + flow = fx.Flow('P_el', bus=my_bus) # ❌ Object + costs = fx.Effect('costs', '€', share_from_temporal={CO2: 0.2}) # ❌ ``` -=== "v3.0.0 (New)" - +=== "v3.0.0" ```python - calculation = fx.FullCalculation('my_calc', flow_system) - calculation.do_modeling() # Returns Calculation object - linopy_model = calculation.model # Access model via property - - # This enables chaining operations - fx.FullCalculation('my_calc', flow_system).do_modeling().solve() + flow = fx.Flow('P_el', bus='electricity') # ✅ String + costs = fx.Effect('costs', '€', share_from_temporal={'CO2': 0.2}) # ✅ ``` -!!! tip "Migration" - If you used the return value of `do_modeling()`, update to access `.model` property instead. - --- -### 4. Storage Charge State Bounds - -!!! warning "Array Dimensions Changed" - `relative_minimum_charge_state` and `relative_maximum_charge_state` no longer have an extra timestep. - -**Impact:** If you provided arrays with `len(timesteps) + 1` elements, reduce to `len(timesteps)`. - -=== "v2.x (Old)" - - ```python - # Array with extra timestep - storage = fx.Storage( - 'storage', - relative_minimum_charge_state=np.array([0.2, 0.2, 0.2, 0.2, 0.2]) # 5 values for 4 timesteps - ) - ``` - -=== "v3.0.0 (New)" +### FlowSystem & Calculation - ```python - # Array matches timesteps - storage = fx.Storage( - 'storage', - relative_minimum_charge_state=np.array([0.2, 0.2, 0.2, 0.2]), # 4 values for 4 timesteps - relative_minimum_final_charge_state=0.3 # Specify the final value directly - ) - ``` - -!!! note "Final State Control" - Use the new `relative_minimum_final_charge_state` and `relative_maximum_final_charge_state` parameters to explicitly control the final charge state. +| Change | Description | +|--------|-------------| +| **FlowSystem copying** | Each `Calculation` gets its own copy (independent) | +| **do_modeling() return** | Returns `Calculation` object (access model via `.model` property) | +| **Storage arrays** | Arrays match timestep count (no extra element) | +| **Final charge state** | Use `relative_minimum_final_charge_state` / `relative_maximum_final_charge_state` | --- -### 5. Plotting Parameter Rename - -=== "v2.x (Old)" - - ```python - results.plot_heatmap('component|variable', mode='line') - ``` +### Other Changes -=== "v3.0.0 (New)" - - ```python - results.plot_heatmap('component|variable', style='line') - ``` +| Category | Old (v2.x) | New (v3.0.0) | +|----------|------------|--------------| +| Plotting parameter | `mode='line'` | `style='line'` | +| System model class | `SystemModel` | `FlowSystemModel` | +| Element submodel | `Model` | `Submodel` | +| Logging default | Enabled | Disabled | +| Enable logging | (default) | `fx.CONFIG.Logging.console = True; fx.CONFIG.apply()` | --- -## Deprecated Parameters (Still Supported) - -!!! info "Gradual Migration" - These parameters still work but will be removed in a future version. Update them at your convenience - deprecation warnings will guide you. +## 🗑️ Deprecated Parameters -### InvestParameters +??? abstract "InvestParameters" -**Parameter Changes:** + | Old (v2.x) | New (v3.0.0) | + |------------|--------------| + | `fix_effects` | `effects_of_investment` | + | `specific_effects` | `effects_of_investment_per_size` | + | `divest_effects` | `effects_of_retirement` | + | `piecewise_effects` | `piecewise_effects_of_investment` | -| Old Parameter (v2.x) | New Parameter (v3.0.0) | -|---------------------|----------------------| -| `fix_effects` | `effects_of_investment` | -| `specific_effects` | `effects_of_investment_per_size` | -| `divest_effects` | `effects_of_retirement` | -| `piecewise_effects` | `piecewise_effects_of_investment` | +??? abstract "Effect" -=== "v2.x (Deprecated)" + | Old (v2.x) | New (v3.0.0) | + |------------|--------------| + | `minimum_investment` | `minimum_periodic` | + | `maximum_investment` | `maximum_periodic` | + | `minimum_operation` | `minimum_temporal` | + | `maximum_operation` | `maximum_temporal` | + | `minimum_operation_per_hour` | `minimum_per_hour` | + | `maximum_operation_per_hour` | `maximum_per_hour` | - ```python - fx.InvestParameters( - fix_effects=1000, - specific_effects={'costs': 10}, - divest_effects=100, - piecewise_effects=my_piecewise, - ) - ``` - -=== "v3.0.0 (Recommended)" - - ```python - fx.InvestParameters( - effects_of_investment=1000, - effects_of_investment_per_size={'costs': 10}, - effects_of_retirement=100, - piecewise_effects_of_investment=my_piecewise, - ) - ``` - -### Effect - -**Parameter Changes:** - -| Old Parameter (v2.x) | New Parameter (v3.0.0) | -|---------------------|----------------------| -| `minimum_investment` | `minimum_periodic` | -| `maximum_investment` | `maximum_periodic` | -| `minimum_operation` | `minimum_temporal` | -| `maximum_operation` | `maximum_temporal` | -| `minimum_operation_per_hour` | `minimum_per_hour` | -| `maximum_operation_per_hour` | `maximum_per_hour` | - -=== "v2.x (Deprecated)" - - ```python - fx.Effect( - 'my_effect', 'unit', 'description', - minimum_investment=10, - maximum_investment=100, - minimum_operation=5, - maximum_operation=50, - minimum_operation_per_hour=1, - maximum_operation_per_hour=10, - ) - ``` +??? abstract "Components" -=== "v3.0.0 (Recommended)" + | Old (v2.x) | New (v3.0.0) | + |------------|--------------| + | `source` (parameter) | `outputs` | + | `sink` (parameter) | `inputs` | + | `prevent_simultaneous_sink_and_source` | `prevent_simultaneous_flow_rates` | - ```python - fx.Effect( - 'my_effect', 'unit', 'description', - minimum_periodic=10, - maximum_periodic=100, - minimum_temporal=5, - maximum_temporal=50, - minimum_per_hour=1, - maximum_per_hour=10, - ) - ``` +??? abstract "TimeSeriesData" -### Component Parameters + | Old (v2.x) | New (v3.0.0) | + |------------|--------------| + | `agg_group` | `aggregation_group` | + | `agg_weight` | `aggregation_weight` | -=== "v2.x (Deprecated)" +??? abstract "Calculation" - ```python - fx.Source('my_source', source=flow) + | Old (v2.x) | New (v3.0.0) | + |------------|--------------| + | `active_timesteps=[0, 1, 2]` | Use `flow_system.sel()` or `flow_system.isel()` | - fx.Sink('my_sink', sink=flow) +--- - fx.SourceAndSink( - 'my_source_sink', - source=flow1, - sink=flow2, - prevent_simultaneous_sink_and_source=True - ) - ``` +## ✨ New Features -=== "v3.0.0 (Recommended)" +??? success "Multi-Period Investments" ```python - fx.Source('my_source', outputs=flow) - - fx.Sink('my_sink', inputs=flow) - - fx.SourceAndSink( - 'my_source_sink', - outputs=flow1, - inputs=flow2, - prevent_simultaneous_flow_rates=True - ) + periods = pd.Index(['2020', '2030']) + flow_system = fx.FlowSystem(time=timesteps, periods=periods) ``` -### TimeSeriesData +??? success "Scenario-Based Optimization" -=== "v2.x (Deprecated)" + | Parameter | Description | Example | + |-----------|-------------|---------| + | `scenarios` | Scenario index | `pd.Index(['low', 'base', 'high'], name='scenario')` | + | `scenario_weights` | Probabilities | `[0.2, 0.6, 0.2]` | + | `scenario_independent_sizes` | Separate capacities per scenario | `True` / `False` (default) | ```python - fx.TimeSeriesData( - agg_group='group1', - agg_weight=2.0 + flow_system = fx.FlowSystem( + time=timesteps, + scenarios=scenarios, + scenario_weights=[0.2, 0.6, 0.2], + scenario_independent_sizes=True ) ``` -=== "v3.0.0 (Recommended)" +??? success "Enhanced I/O" - ```python - fx.TimeSeriesData( - aggregation_group='group1', - aggregation_weight=2.0 - ) - ``` - -### Calculation - -=== "v2.x (Deprecated)" - - ```python - calculation = fx.FullCalculation( - 'calc', - flow_system, - active_timesteps=[0, 1, 2] - ) - ``` + | Method | Description | + |--------|-------------| + | `flow_system.to_netcdf('file.nc')` | Save FlowSystem | + | `fx.FlowSystem.from_netcdf('file.nc')` | Load FlowSystem | + | `flow_system.sel(time=slice(...))` | Select by label | + | `flow_system.isel(time=slice(...))` | Select by index | + | `flow_system.resample(time='D')` | Resample timeseries | + | `flow_system.copy()` | Deep copy | + | `results.flow_system` | Access from results | -=== "v3.0.0 (Recommended)" +??? success "Effects Per Component" ```python - # Use FlowSystem selection methods - flow_system_subset = flow_system.sel(time=slice('2020-01-01', '2020-01-03')) - calculation = fx.FullCalculation('calc', flow_system_subset) + effects_ds = results.effects_per_component - # Or with isel for index-based selection - flow_system_subset = flow_system.isel(time=slice(0, 3)) - calculation = fx.FullCalculation('calc', flow_system_subset) + # Access effect contributions by component + print(effects_ds['total'].sel(effect='costs')) # Total effects + print(effects_ds['temporal'].sel(effect='CO2')) # Temporal effects + print(effects_ds['periodic'].sel(effect='costs')) # Periodic effects ``` ---- - -## New Features in v3.0.0 - -### 1. Multi-Period Investments - -Model transformation pathways with distinct investment decisions in each period: - -```python -import pandas as pd - -# Define multiple investment periods -periods = pd.Index(['2020', '2030']) -flow_system = fx.FlowSystem(time=timesteps, periods=periods) - -# Components can now invest differently in each period -solar = fx.Source( - 'solar', - outputs=[fx.Flow( - 'P_el', - bus='electricity', - size=fx.InvestParameters( - minimum_size=0, - maximum_size=1000, - effects_of_investment_per_size={'costs': 100} - ) - )] -) -``` - -### 2. Scenario-Based Stochastic Optimization - -Model uncertainty with weighted scenarios: - -```python -# Define scenarios with probabilities -scenarios = pd.Index(['low_demand', 'base', 'high_demand'], name='scenario') -scenario_weights = [0.2, 0.6, 0.2] # Probabilities - -flow_system = fx.FlowSystem( - time=timesteps, - scenarios=scenarios, - scenario_weights=scenario_weights -) - -# Define scenario-dependent data -demand = xr.DataArray( - data=[[70, 80, 90], # low_demand scenario - [90, 100, 110], # base scenario - [110, 120, 130]], # high_demand scenario - dims=['scenario', 'time'], - coords={'scenario': scenarios, 'time': timesteps} -) - -``` - -**Control variable independence:** -```python -# By default: investment sizes are shared across scenarios, flow rates vary -# To make sizes scenario-independent: -flow_system = fx.FlowSystem( - time=timesteps, - scenarios=scenarios, - scenario_independent_sizes=True # Each scenario gets its own capacity -) -``` - -### 3. Enhanced I/O and Data Handling - -```python -# Save and load FlowSystem -flow_system.to_netcdf('my_system.nc') -flow_system_loaded = fx.FlowSystem.from_netcdf('my_system.nc') - -# Manipulate FlowSystem -fs_subset = flow_system.sel(time=slice('2020-01', '2020-06')) -fs_resampled = flow_system.resample(time='D') # Resample to daily -fs_copy = flow_system.copy() - -# Access FlowSystem from results (lazily loaded) -results = calculation.results -original_fs = results.flow_system # No manual restoration needed -``` - -### 4. Effects Per Component - -Analyze the impact of each component, including indirect effects through effect shares: - -```python -# Get dataset showing contribution of each component to all effects -effects_ds = calculation.results.effects_per_component() - -print(effects_ds['costs']) # Total costs by component -print(effects_ds['CO2']) # CO2 emissions by component (including indirect) -``` - -### 5. Balanced Storage - -Force charging and discharging capacities to be equal: - -```python -storage = fx.Storage( - 'storage', - charging=fx.Flow('charge', bus='electricity', size=fx.InvestParameters(effects_per_size=100, minimum_size=5)), - discharging=fx.Flow('discharge', bus='electricity', size=fx.InvestParameters(), - balanced=True, # Ensures charge_size == discharge_size - capacity_in_flow_hours=100 -) -``` - -### 6. Final Charge State Control - -Set bounds on the storage state at the end of the optimization: - -```python -storage = fx.Storage( - 'storage', - charging=fx.Flow('charge', bus='electricity', size=100), - discharging=fx.Flow('discharge', bus='electricity', size=100), - capacity_in_flow_hours=10, - relative_minimum_final_charge_state=0.5, # End at least 50% charged - relative_maximum_final_charge_state=0.8 # End at most 80% charged -) -``` - ---- - -## Configuration Changes - -### Logging (v2.2.0+) +??? success "Storage Features" -**Breaking change:** Console and file logging are now disabled by default. - -```python -import flixopt as fx - -# Enable console logging -fx.CONFIG.Logging.console = True -fx.CONFIG.Logging.level = 'INFO' -fx.CONFIG.apply() - -# Enable file logging -fx.CONFIG.Logging.file = 'flixopt.log' -fx.CONFIG.apply() - -# Deprecated: change_logging_level() - will be removed in future -# fx.change_logging_level('INFO') # ❌ Old way -``` + | Feature | Parameter | Description | + |---------|-----------|-------------| + | **Balanced storage** | `balanced=True` | Ensures charge_size == discharge_size | + | **Final state min** | `relative_minimum_final_charge_state=0.5` | End at least 50% charged | + | **Final state max** | `relative_maximum_final_charge_state=0.8` | End at most 80% charged | --- -## Testing Your Migration - -### 1. Check for Deprecation Warnings - -Run your code and watch for deprecation warnings: - -```python -import warnings -warnings.filterwarnings('default', category=DeprecationWarning) +## 🔧 Common Issues -# Run your flixopt code -# Review any DeprecationWarning messages -``` - -### 2. Validate Results - -Compare results from v2.x and v3.0.0 to ensure consistency: - -```python -# Save v2.x results before upgrading -calculation.results.to_file('results_v2.nc') - -# After upgrading, compare -results_v3 = calculation.results -results_v2 = fx.CalculationResults.from_file('results_v2.nc') - -# Check key variables match (within numerical tolerance) -import numpy as np -v2_costs = results_v2['effect_values'].sel(effect='costs') -v3_costs = results_v3['effect_values'].sel(effect='costs') -np.testing.assert_allclose(v2_costs, v3_costs, rtol=1e-5) -``` +| Issue | Solution | +|-------|----------| +| Effect shares not working | See [Effect System Redesign](#effect-system-redesign) | +| Storage dimensions wrong | See [FlowSystem & Calculation](#flowsystem-calculation) | +| Bus assignment error | See [String Labels](#string-labels) | +| KeyError in results | See [Variable Names](#variable-names) | +| `AttributeError: model` | Rename `.model` → `.submodel` | +| No logging | See [Other Changes](#other-changes) | --- -## Common Migration Issues - -### Issue: "Effect share parameters not working" - -**Solution:** Effect sharing was completely redesigned. Move share definitions to the **receiving** effect using `share_from_temporal` and `share_from_periodic`. +## ✅ Checklist -### Issue: "Storage charge state has wrong dimensions" - -**Solution:** Remove the extra timestep from charge state bound arrays. - -### Issue: "Import error with Bus assignment" - -**Solution:** Pass bus labels (strings) instead of Bus objects to `Flow.bus`. - -```python -# Old -my_bus = fx.Bus('electricity') -flow = fx.Flow('P_el', bus=my_bus) # ❌ - -# New -my_bus = fx.Bus('electricity') -flow = fx.Flow('P_el', bus='electricity') # ✅ -``` - -### Issue: "AttributeError: module 'flixopt' has no attribute 'SystemModel'" - -**Solution:** Rename `SystemModel` → `FlowSystemModel` - - ---- - -## Getting Help - -- **Documentation:** [https://flixopt.github.io/flixopt/](https://flixopt.github.io/flixopt/) -- **GitHub Issues:** [https://github.com/flixOpt/flixopt/issues](https://github.com/flixOpt/flixopt/issues) -- **Changelog:** [Full v3.0.0 release notes](https://flixopt.github.io/flixopt/latest/changelog/99984-v3.0.0/) +| Category | Tasks | +|----------|-------| +| **Install** | • `pip install --upgrade flixopt` | +| **Breaking changes** | • Update [effect sharing](#effect-system-redesign)
• Update [variable names](#variable-names)
• Update [string labels](#string-labels)
• Fix [storage arrays](#flowsystem-calculation)
• Update [Calculation API](#flowsystem-calculation)
• Rename plotting `mode` → `style`
• Update [class names](#other-changes) | +| **Configuration** | • Enable [logging](#other-changes) if needed | +| **Deprecated** | • Update [deprecated parameters](#deprecated-parameters) (recommended) | +| **Testing** | • Test thoroughly
• Validate results match v2.x | --- -## Summary Checklist - -- [ ] Update flixopt: `pip install --upgrade flixopt` -- [ ] Update effect sharing syntax (no deprecation warning!) -- [ ] Update `Calculation.do_modeling()` usage -- [ ] Fix storage charge state array dimensions -- [ ] Rename `mode` → `style` in plotting calls -- [ ] Update deprecated parameter names (optional, but recommended) -- [ ] Enable logging explicitly if needed -- [ ] Test your code thoroughly -- [ ] Explore new features (periods, scenarios, enhanced I/O) +:material-book: [Docs](https://flixopt.github.io/flixopt/) • :material-github: [Issues](https://github.com/flixOpt/flixopt/issues) • :material-text-box: [Changelog](https://flixopt.github.io/flixopt/latest/changelog/99984-v3.0.0/) -**Welcome to flixopt v3.0.0!** 🎉 +!!! success "Welcome to flixopt v3.0.0! 🎉" diff --git a/mkdocs.yml b/mkdocs.yml index 72ecbe549..8ed2c4b2c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -91,6 +91,7 @@ markdown_extensions: line_spans: __span pygments_lang_class: true - pymdownx.inlinehilite + - pymdownx.details - pymdownx.superfences - attr_list - abbr From 236a387497e1094f983693172185ac64cc26b675 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 16 Oct 2025 15:15:37 +0200 Subject: [PATCH 355/448] Fix/revert rename of mode to style in plotting (#412) * Readd removed code * Readd removed code * Remove escaping | * Update * Update * Temp * update examples * Update CHANGELOG.md * Update Migration Guide * Typo * Fix CHANGELOG.md --- CHANGELOG.md | 2 ++ docs/user-guide/migration-guide-v3.md | 3 +- .../example_calculation_types.py | 10 +++--- examples/04_Scenarios/scenario_example.py | 2 +- flixopt/plotting.py | 32 +++++++++---------- flixopt/results.py | 31 ++++++++++-------- 6 files changed, 42 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 632fcf7a4..4f18f346b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,8 @@ Please keep the format of the changelog consistent with the other releases, so t ### 💥 Breaking Changes ### ♻️ Changed +- Reverted breaking change from v3.0.0: continue to use `mode parameter in plotting instead of new `style` +- Renamed new `mode` parameter in plotting methods to `unit_type` ### 🗑️ Deprecated diff --git a/docs/user-guide/migration-guide-v3.md b/docs/user-guide/migration-guide-v3.md index a98824de1..2a9cab97a 100644 --- a/docs/user-guide/migration-guide-v3.md +++ b/docs/user-guide/migration-guide-v3.md @@ -91,7 +91,6 @@ Terminology changed and sharing system inverted: effects now "pull" shares. | Category | Old (v2.x) | New (v3.0.0) | |----------|------------|--------------| -| Plotting parameter | `mode='line'` | `style='line'` | | System model class | `SystemModel` | `FlowSystemModel` | | Element submodel | `Model` | `Submodel` | | Logging default | Enabled | Disabled | @@ -221,7 +220,7 @@ Terminology changed and sharing system inverted: effects now "pull" shares. | Category | Tasks | |----------|-------| | **Install** | • `pip install --upgrade flixopt` | -| **Breaking changes** | • Update [effect sharing](#effect-system-redesign)
• Update [variable names](#variable-names)
• Update [string labels](#string-labels)
• Fix [storage arrays](#flowsystem-calculation)
• Update [Calculation API](#flowsystem-calculation)
• Rename plotting `mode` → `style`
• Update [class names](#other-changes) | +| **Breaking changes** | • Update [effect sharing](#effect-system-redesign)
• Update [variable names](#variable-names)
• Update [string labels](#string-labels)
• Fix [storage arrays](#flowsystem-calculation)
• Update [Calculation API](#flowsystem-calculation)
• Update [class names](#other-changes) | | **Configuration** | • Enable [logging](#other-changes) if needed | | **Deprecated** | • Update [deprecated parameters](#deprecated-parameters) (recommended) | | **Testing** | • Test thoroughly
• Validate results match v2.x | diff --git a/examples/03_Calculation_types/example_calculation_types.py b/examples/03_Calculation_types/example_calculation_types.py index 05b25e782..3f9ae665b 100644 --- a/examples/03_Calculation_types/example_calculation_types.py +++ b/examples/03_Calculation_types/example_calculation_types.py @@ -203,35 +203,35 @@ def get_solutions(calcs: list, variable: str) -> xr.Dataset: # --- Plotting for comparison --- fx.plotting.with_plotly( get_solutions(calculations, 'Speicher|charge_state').to_dataframe(), - style='line', + mode='line', title='Charge State Comparison', ylabel='Charge state', ).write_html('results/Charge State.html') fx.plotting.with_plotly( get_solutions(calculations, 'BHKW2(Q_th)|flow_rate').to_dataframe(), - style='line', + mode='line', title='BHKW2(Q_th) Flow Rate Comparison', ylabel='Flow rate', ).write_html('results/BHKW2 Thermal Power.html') fx.plotting.with_plotly( get_solutions(calculations, 'costs(temporal)|per_timestep').to_dataframe(), - style='line', + mode='line', title='Operation Cost Comparison', ylabel='Costs [€]', ).write_html('results/Operation Costs.html') fx.plotting.with_plotly( pd.DataFrame(get_solutions(calculations, 'costs(temporal)|per_timestep').to_dataframe().sum()).T, - style='stacked_bar', + mode='stacked_bar', title='Total Cost Comparison', ylabel='Costs [€]', ).update_layout(barmode='group').write_html('results/Total Costs.html') fx.plotting.with_plotly( pd.DataFrame([calc.durations for calc in calculations], index=[calc.name for calc in calculations]), - 'stacked_bar', + mode='stacked_bar', ).update_layout(title='Duration Comparison', xaxis_title='Calculation type', yaxis_title='Time (s)').write_html( 'results/Speed Comparison.html' ) diff --git a/examples/04_Scenarios/scenario_example.py b/examples/04_Scenarios/scenario_example.py index f06760603..6aa3c0c89 100644 --- a/examples/04_Scenarios/scenario_example.py +++ b/examples/04_Scenarios/scenario_example.py @@ -125,7 +125,7 @@ # --- Analyze Results --- calculation.results['Fernwärme'].plot_node_balance_pie() - calculation.results['Fernwärme'].plot_node_balance(style='stacked_bar') + calculation.results['Fernwärme'].plot_node_balance(mode='stacked_bar') calculation.results['Storage'].plot_node_balance() calculation.results.plot_heatmap('CHP(Q_th)|flow_rate') diff --git a/flixopt/plotting.py b/flixopt/plotting.py index 356f013c0..218a8ab0e 100644 --- a/flixopt/plotting.py +++ b/flixopt/plotting.py @@ -327,7 +327,7 @@ def process_colors( def with_plotly( data: pd.DataFrame, - style: Literal['stacked_bar', 'line', 'area', 'grouped_bar'] = 'stacked_bar', + mode: Literal['stacked_bar', 'line', 'area', 'grouped_bar'] = 'stacked_bar', colors: ColorType = 'viridis', title: str = '', ylabel: str = '', @@ -340,7 +340,7 @@ def with_plotly( Args: data: A DataFrame containing the data to plot, where the index represents time (e.g., hours), and each column represents a separate data series. - style: The plotting style. Use 'stacked_bar' for stacked bar charts, 'line' for stepped lines, + mode: The plotting mode. Use 'stacked_bar' for stacked bar charts, 'line' for stepped lines, or 'area' for stacked area charts. colors: Color specification, can be: - A string with a colorscale name (e.g., 'viridis', 'plasma') @@ -354,8 +354,8 @@ def with_plotly( Returns: A Plotly figure object containing the generated plot. """ - if style not in ('stacked_bar', 'line', 'area', 'grouped_bar'): - raise ValueError(f"'style' must be one of {{'stacked_bar','line','area', 'grouped_bar'}}, got {style!r}") + if mode not in ('stacked_bar', 'line', 'area', 'grouped_bar'): + raise ValueError(f"'mode' must be one of {{'stacked_bar','line','area', 'grouped_bar'}}, got {mode!r}") if data.empty: return go.Figure() @@ -363,7 +363,7 @@ def with_plotly( fig = fig if fig is not None else go.Figure() - if style == 'stacked_bar': + if mode == 'stacked_bar': for i, column in enumerate(data.columns): fig.add_trace( go.Bar( @@ -381,7 +381,7 @@ def with_plotly( bargap=0, # No space between bars bargroupgap=0, # No space between grouped bars ) - if style == 'grouped_bar': + if mode == 'grouped_bar': for i, column in enumerate(data.columns): fig.add_trace(go.Bar(x=data.index, y=data[column], name=column, marker=dict(color=processed_colors[i]))) @@ -390,7 +390,7 @@ def with_plotly( bargap=0.2, # No space between bars bargroupgap=0, # space between grouped bars ) - elif style == 'line': + elif mode == 'line': for i, column in enumerate(data.columns): fig.add_trace( go.Scatter( @@ -401,7 +401,7 @@ def with_plotly( line=dict(shape='hv', color=processed_colors[i]), ) ) - elif style == 'area': + elif mode == 'area': data = data.copy() data[(data > -1e-5) & (data < 1e-5)] = 0 # Preventing issues with plotting # Split columns into positive, negative, and mixed categories @@ -468,7 +468,7 @@ def with_plotly( def with_matplotlib( data: pd.DataFrame, - style: Literal['stacked_bar', 'line'] = 'stacked_bar', + mode: Literal['stacked_bar', 'line'] = 'stacked_bar', colors: ColorType = 'viridis', title: str = '', ylabel: str = '', @@ -483,7 +483,7 @@ def with_matplotlib( Args: data: A DataFrame containing the data to plot. The index should represent time (e.g., hours), and each column represents a separate data series. - style: Plotting style. Use 'stacked_bar' for stacked bar charts or 'line' for stepped lines. + mode: Plotting mode. Use 'stacked_bar' for stacked bar charts or 'line' for stepped lines. colors: Color specification, can be: - A string with a colormap name (e.g., 'viridis', 'plasma') - A list of color strings (e.g., ['#ff0000', '#00ff00']) @@ -499,19 +499,19 @@ def with_matplotlib( A tuple containing the Matplotlib figure and axes objects used for the plot. Notes: - - If `style` is 'stacked_bar', bars are stacked for both positive and negative values. + - If `mode` is 'stacked_bar', bars are stacked for both positive and negative values. Negative values are stacked separately without extra labels in the legend. - - If `style` is 'line', stepped lines are drawn for each data series. + - If `mode` is 'line', stepped lines are drawn for each data series. """ - if style not in ('stacked_bar', 'line'): - raise ValueError(f"'style' must be one of {{'stacked_bar','line'}} for matplotlib, got {style!r}") + if mode not in ('stacked_bar', 'line'): + raise ValueError(f"'mode' must be one of {{'stacked_bar','line'}} for matplotlib, got {mode!r}") if fig is None or ax is None: fig, ax = plt.subplots(figsize=figsize) processed_colors = ColorProcessor(engine='matplotlib').process_colors(colors, list(data.columns)) - if style == 'stacked_bar': + if mode == 'stacked_bar': cumulative_positive = np.zeros(len(data)) cumulative_negative = np.zeros(len(data)) width = data.index.to_series().diff().dropna().min() # Minimum time difference @@ -542,7 +542,7 @@ def with_matplotlib( ) cumulative_negative += negative_values.values - elif style == 'line': + elif mode == 'line': for i, column in enumerate(data.columns): ax.step(data.index, data[column], where='post', color=processed_colors[i], label=column) diff --git a/flixopt/results.py b/flixopt/results.py index 2e951af70..b55d48744 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -921,8 +921,8 @@ def plot_node_balance( colors: plotting.ColorType = 'viridis', engine: plotting.PlottingEngine = 'plotly', indexer: dict[FlowSystemDimensions, Any] | None = None, - mode: Literal['flow_rate', 'flow_hours'] = 'flow_rate', - style: Literal['area', 'stacked_bar', 'line'] = 'stacked_bar', + unit_type: Literal['flow_rate', 'flow_hours'] = 'flow_rate', + mode: Literal['area', 'stacked_bar', 'line'] = 'stacked_bar', drop_suffix: bool = True, ) -> plotly.graph_objs.Figure | tuple[plt.Figure, plt.Axes]: """ @@ -935,23 +935,26 @@ def plot_node_balance( indexer: Optional selection dict, e.g., {'scenario': 'base', 'period': 2024}. If None, uses first value for each dimension (except time). If empty dict {}, uses all values. - style: The style to use for the dataset. Can be 'flow_rate' or 'flow_hours'. + unit_type: The unit type to use for the dataset. Can be 'flow_rate' or 'flow_hours'. - 'flow_rate': Returns the flow_rates of the Node. - 'flow_hours': Returns the flow_hours of the Node. [flow_hours(t) = flow_rate(t) * dt(t)]. Renames suffixes to |flow_hours. + mode: The plotting mode. Use 'stacked_bar' for stacked bar charts, 'line' for stepped lines, or 'area' for stacked area charts. drop_suffix: Whether to drop the suffix from the variable names. """ - ds = self.node_balance(with_last_timestep=True, mode=mode, drop_suffix=drop_suffix, indexer=indexer) + ds = self.node_balance(with_last_timestep=True, unit_type=unit_type, drop_suffix=drop_suffix, indexer=indexer) ds, suffix_parts = _apply_indexer_to_data(ds, indexer, drop=True) suffix = '--' + '-'.join(suffix_parts) if suffix_parts else '' - title = f'{self.label} (flow rates){suffix}' if mode == 'flow_rate' else f'{self.label} (flow hours){suffix}' + title = ( + f'{self.label} (flow rates){suffix}' if unit_type == 'flow_rate' else f'{self.label} (flow hours){suffix}' + ) if engine == 'plotly': figure_like = plotting.with_plotly( ds.to_dataframe(), colors=colors, - style=style, + mode=mode, title=title, ) default_filetype = '.html' @@ -959,7 +962,7 @@ def plot_node_balance( figure_like = plotting.with_matplotlib( ds.to_dataframe(), colors=colors, - style=style, + mode=mode, title=title, ) default_filetype = '.png' @@ -1063,7 +1066,7 @@ def node_balance( negate_outputs: bool = False, threshold: float | None = 1e-5, with_last_timestep: bool = False, - mode: Literal['flow_rate', 'flow_hours'] = 'flow_rate', + unit_type: Literal['flow_rate', 'flow_hours'] = 'flow_rate', drop_suffix: bool = False, indexer: dict[FlowSystemDimensions, Any] | None = None, ) -> xr.Dataset: @@ -1074,7 +1077,7 @@ def node_balance( negate_outputs: Whether to negate the output flow_rates of the Node. threshold: The threshold for small values. Variables with all values below the threshold are dropped. with_last_timestep: Whether to include the last timestep in the dataset. - mode: The mode to use for the dataset. Can be 'flow_rate' or 'flow_hours'. + unit_type: The unit type to use for the dataset. Can be 'flow_rate' or 'flow_hours'. - 'flow_rate': Returns the flow_rates of the Node. - 'flow_hours': Returns the flow_hours of the Node. [flow_hours(t) = flow_rate(t) * dt(t)]. Renames suffixes to |flow_hours. drop_suffix: Whether to drop the suffix from the variable names. @@ -1102,7 +1105,7 @@ def node_balance( ds, _ = _apply_indexer_to_data(ds, indexer, drop=True) - if mode == 'flow_hours': + if unit_type == 'flow_hours': ds = ds * self._calculation_results.hours_per_timestep ds = ds.rename_vars({var: var.replace('flow_rate', 'flow_hours') for var in ds.data_vars}) @@ -1137,7 +1140,7 @@ def plot_charge_state( show: bool = True, colors: plotting.ColorType = 'viridis', engine: plotting.PlottingEngine = 'plotly', - style: Literal['area', 'stacked_bar', 'line'] = 'stacked_bar', + mode: Literal['area', 'stacked_bar', 'line'] = 'stacked_bar', indexer: dict[FlowSystemDimensions, Any] | None = None, ) -> plotly.graph_objs.Figure: """Plot storage charge state over time, combined with the node balance. @@ -1147,7 +1150,7 @@ def plot_charge_state( show: Whether to show the plot or not. colors: Color scheme. Also see plotly. engine: Plotting engine to use. Only 'plotly' is implemented atm. - style: The colors to use for the plot. See `flixopt.plotting.ColorType` for options. + mode: The plotting mode. Use 'stacked_bar' for stacked bar charts, 'line' for stepped lines, or 'area' for stacked area charts. indexer: Optional selection dict, e.g., {'scenario': 'base', 'period': 2024}. If None, uses first value for each dimension. If empty dict {}, uses all values. @@ -1171,7 +1174,7 @@ def plot_charge_state( fig = plotting.with_plotly( ds.to_dataframe(), colors=colors, - style=style, + mode=mode, title=title, ) @@ -1187,7 +1190,7 @@ def plot_charge_state( fig, ax = plotting.with_matplotlib( ds.to_dataframe(), colors=colors, - style=style, + mode=mode, title=title, ) From f332c007bc22b7c02116e4583df1f14a9bfddd9b Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 16 Oct 2025 15:18:09 +0200 Subject: [PATCH 356/448] Update CHANGELOG.md --- CHANGELOG.md | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f18f346b..a2c7168e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,7 +41,6 @@ Please keep the format of the changelog consistent with the other releases, so t --- - ## [Unreleased] - ????-??-?? ### ✨ Added @@ -49,8 +48,6 @@ Please keep the format of the changelog consistent with the other releases, so t ### 💥 Breaking Changes ### ♻️ Changed -- Reverted breaking change from v3.0.0: continue to use `mode parameter in plotting instead of new `style` -- Renamed new `mode` parameter in plotting methods to `unit_type` ### 🗑️ Deprecated @@ -63,8 +60,6 @@ Please keep the format of the changelog consistent with the other releases, so t ### 📦 Dependencies ### 📝 Docs -- Updated Migration Guide and added missing entries. -- Improved Changelog of v3.0.0 ### 👷 Development @@ -73,6 +68,19 @@ Please keep the format of the changelog consistent with the other releases, so t --- Until here --> +## [3.0.3] - 2025-10-16 +**Summary**: Hotfixing new plotting parameter `style`. Continue to use `mode`. + +### 🐛 Fixed +- Reverted breaking change from v3.0.0: continue to use `mode parameter in plotting instead of new `style` +- Renamed new `mode` parameter in plotting methods to `unit_type` + +### 📝 Docs +- Updated Migration Guide and added missing entries. +- Improved Changelog of v3.0.0 + +--- + ## [3.0.2] - 2025-10-15 **Summary**: This is a follow-up release to **[v3.0.0](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0)**, improving the documentation. From f6aef3a1b527515cfec4fc83b4a168e5c37dc853 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 16 Oct 2025 16:00:53 +0200 Subject: [PATCH 357/448] Improve CHANGELOG.md --- CHANGELOG.md | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2c7168e8..7cc3be435 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm Formatting is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) & [Gitmoji](https://gitmoji.dev). For more details regarding the individual PRs and contributors, please refer to our [GitHub releases](https://github.com/flixOpt/flixopt/releases). +!!! tip + + If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/). + --- ## [3.0.3] - 2025-10-16 **Summary**: Hotfixing new plotting parameter `style`. Continue to use `mode`. +**Note**: If upgrading from v2.x, see the [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/) and [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0). + ### 🐛 Fixed - Reverted breaking change from v3.0.0: continue to use `mode parameter in plotting instead of new `style` - Renamed new `mode` parameter in plotting methods to `unit_type` @@ -84,7 +96,7 @@ Until here --> ## [3.0.2] - 2025-10-15 **Summary**: This is a follow-up release to **[v3.0.0](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0)**, improving the documentation. -**Note**: If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/). +**Note**: If upgrading from v2.x, see the [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/) and [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0). ### 📝 Docs - Update the Readme @@ -97,7 +109,7 @@ Until here --> ## [3.0.1] - 2025-10-14 **Summary**: This is a follow-up release to **[v3.0.0](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0)**, adding a Migration Guide and bugfixing the docs. -**Note**: If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/). +**Note**: If upgrading from v2.x, see the [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/) and [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0). ### 📝 Docs - Fixed deployed docs @@ -111,7 +123,7 @@ Until here --> ## [3.0.0] - 2025-10-13 **Summary**: This release introduces new model dimensions (periods and scenarios) for multi-period investments and stochastic modeling, along with a redesigned effect sharing system and enhanced I/O capabilities. -**Note**: If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/). +**Note**: If upgrading from v2.x, see the [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/) and [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0). ### ✨ Added From c72357e524250e33e3ef3c395567791cac68007d Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 17 Oct 2025 11:25:28 +0200 Subject: [PATCH 358/448] Add error handling for empty buses (#416) --- flixopt/elements.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/flixopt/elements.py b/flixopt/elements.py index 25e399811..a0fd306c0 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -207,6 +207,10 @@ def _plausibility_checks(self) -> None: logger.warning( f'In Bus {self.label_full}, the excess_penalty_per_flow_hour is 0. Use "None" or a value > 0.' ) + if len(self.inputs) == 0 and len(self.outputs) == 0: + raise ValueError( + f'Bus "{self.label_full}" has no Flows connected to it. Please remove it from the FlowSystem' + ) @property def with_excess(self) -> bool: From 8323b27b6b6c9eb6c124ed4bd8ab8f4a4ad3fe05 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 17 Oct 2025 11:26:03 +0200 Subject: [PATCH 359/448] Fix/409 bug error handling in investparameterslinked periods (#415) * Add early check for non existant periods with linked periods as a tuple used * Add extra except phrase to version handling in init --- flixopt/__init__.py | 2 +- flixopt/interface.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/flixopt/__init__.py b/flixopt/__init__.py index 8fc4e4851..3633d86a1 100644 --- a/flixopt/__init__.py +++ b/flixopt/__init__.py @@ -7,7 +7,7 @@ try: __version__ = version('flixopt') -except PackageNotFoundError: +except (PackageNotFoundError, TypeError): # Package is not installed (development mode without editable install) __version__ = '0.0.0.dev0' diff --git a/flixopt/interface.py b/flixopt/interface.py index ab47c2522..3b7d24da7 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -963,6 +963,11 @@ def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None raise TypeError( f'If you provide a tuple to "linked_periods", it needs to be len=2. Got {len(self.linked_periods)=}' ) + if flow_system.periods is None: + raise ValueError( + f'Cannot use linked_periods={self.linked_periods} when FlowSystem has no periods defined. ' + f'Please define periods in FlowSystem or use linked_periods=None.' + ) logger.debug(f'Computing linked_periods from {self.linked_periods}') start, end = self.linked_periods if start not in flow_system.periods.values: From babe326c7f3bbb12e6935112caefd7848e133139 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 18 Oct 2025 21:43:53 +0200 Subject: [PATCH 360/448] Feature/pretty docs with theme (#413) * Fix Docs * Update Changelog and migration guide for missing breaking changes * Update Migration guide * Improve * Use tabs in mkdocs * Update varaibel renaming * Update variable renaming * From main * Update * Update * Update * Add links * Add emojis * compact 2 * Enable plugin * Update * Update * Customize * Customize * Add deps * Make index.md pretty * Fix * Fix * Fix icons * Fix quick nav * Use regular emojis * Update landing page * Add semantic colors * Add semantic colors * More css * Revert "More css" This reverts commit 31cf8c8a6fd141c3d3bc92aa2eb53245e24bfd0a. * Revert "Add semantic colors" This reverts commit 1a1f4a7e6b7137cb80306333a8bcff431d31381c. * Revert "Add semantic colors" This reverts commit dcd6eadb686e4f335a86f373d7056b86a3ad5735. * Fix * Pin mkdocs * Add rel="noopener noreferrer" to external links opened in new * Remove duplication * Change colors * Change colors * Revert "Change colors" This reverts commit a91372a2edfb870f67e42ed0dd0856910f27353a. * Revert "Change colors" This reverts commit 7e1c277e6481f7f9ea809f60fc03a39fd167a411. * Add entry to CHANGELOG.md --- CHANGELOG.md | 1 + docs/index.md | 144 ++++++++++++-- docs/stylesheets/extra.css | 396 +++++++++++++++++++++++++++++++++++++ mkdocs.yml | 267 +++++++++++++++++++------ pyproject.toml | 11 +- 5 files changed, 747 insertions(+), 72 deletions(-) create mode 100644 docs/stylesheets/extra.css diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cc3be435..41dae8ad4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,7 @@ If upgrading from v2.x, see the [Migration Guide](https://flixopt.github.io/flix ### 📦 Dependencies ### 📝 Docs +- Improve docs visually ### 👷 Development diff --git a/docs/index.md b/docs/index.md index 6723c4d5e..a7cd67f8c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,22 +1,144 @@ -{% - include-markdown "../README.md" -%} - --- +title: Home +hide: + - navigation + - toc +--- + +
+ +

flixOpt

+ +

Energy and Material Flow Optimization Framework

+ +

Model, optimize, and analyze complex energy systems with a powerful Python framework designed for flexibility and performance.

+ +

+ 🚀 Get Started + 💡 View Examples + ⭐ GitHub +

+ +
+ +## :material-map-marker-path: Quick Navigation + + + +## 🏗️ Framework Architecture + +
![FlixOpt Conceptual Usage](./images/architecture_flixOpt.png)
Conceptual Usage and IO operations of FlixOpt
+**FlixOpt** provides a complete workflow for energy system optimization: + +- **:material-file-code: Define** your system using Python components +- **:material-cog: Optimize** with powerful solvers (HiGHS, Gurobi, CPLEX) +- **:material-chart-box: Analyze** results with built-in visualization tools +- **:material-export: Export** to various formats for further analysis + +
+ +## :material-account-group: Community & Support + +
+ +
+ +:fontawesome-brands-github:{ .feature-icon } + +### GitHub + +Report issues, request features, and contribute to the codebase + +[Visit Repository →](https://github.com/flixOpt/flixopt){target="_blank" rel="noopener noreferrer"} + +
+ +
+ +:material-forum:{ .feature-icon } + +### Discussions + +Ask questions and share your projects with the community + +[Join Discussion →](https://github.com/flixOpt/flixopt/discussions){target="_blank" rel="noopener noreferrer"} + +
+ +
+ +:material-book-open-page-variant:{ .feature-icon } + +### Contributing + +Help improve FlixOpt by contributing code, docs, or examples + +[Learn How →](contribute/){target="_blank" rel="noopener noreferrer"} + +
+ +
+ + +## :material-file-document-edit: Recent Updates + +!!! tip "What's New in v3.0.0" + Major improvements and breaking changes. Check the [Migration Guide](user-guide/migration-guide-v3.md) for upgrading from v2.x. + +📋 See the full [Release Notes](changelog/) for detailed version history. + --- -## Next Steps +
+ +

Ready to optimize your energy system?

+ +

+ ▶️ Start Building +

-- **New to FlixOpt?** Start with the [Getting Started Guide](getting-started.md) -- **Want to see examples?** Check out the [Examples Gallery](examples/index.md) -- **Need API details?** Browse the [API Reference](api-reference/index.md) -- **Looking for advanced patterns?** See [Recipes](user-guide/recipes/index.md) -- **Curious about the future?** Read our [Roadmap](roadmap.md) +
+ +--- + +{% + include-markdown "../README.md" + start="## Installation" + end="## License" +%} diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css new file mode 100644 index 000000000..79dfc9a15 --- /dev/null +++ b/docs/stylesheets/extra.css @@ -0,0 +1,396 @@ +/* ============================================================================ + flixOpt Custom Styling + ========================================================================= */ + +/* Root variables for easy customization */ +:root { + /* Spacing */ + --content-padding: 2rem; + + /* Typography */ + --heading-font-weight: 600; + + /* Colors - enhance teal theme */ + --flixopt-teal: #009688; + --flixopt-teal-light: #4DB6AC; + --flixopt-teal-dark: #00796B; +} + +/* Dark mode adjustments */ +[data-md-color-scheme="slate"] { + --md-code-bg-color: #1e1e1e; +} + +/* ============================================================================ + Typography Improvements + ========================================================================= */ + +/* Better line height for readability */ +.md-typeset { + line-height: 1.7; +} + +/* Enhanced headings */ +.md-typeset h1 { + font-weight: var(--heading-font-weight); + letter-spacing: -0.02em; + margin-top: 0; +} + +.md-typeset h2 { + font-weight: var(--heading-font-weight); + border-bottom: 1px solid var(--md-default-fg-color--lightest); + padding-bottom: 0.3em; + margin-top: 2em; +} + +/* Better code inline */ +.md-typeset code { + padding: 0.15em 0.4em; + border-radius: 0.25em; + font-size: 0.875em; +} + +/* ============================================================================ + Navigation Enhancements + ========================================================================= */ + +/* Smooth hover effects on navigation */ +.md-nav__link:hover { + opacity: 0.7; + transition: opacity 0.2s ease; +} + +/* Active navigation item enhancement */ +.md-nav__link--active { + font-weight: 600; + border-left: 3px solid var(--md-primary-fg-color); + padding-left: calc(1.2rem - 3px) !important; +} + +/* ============================================================================ + Code Block Improvements + ========================================================================= */ + +/* Better code block styling */ +.md-typeset .highlight { + border-radius: 0.5rem; + margin: 1.5em 0; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +[data-md-color-scheme="slate"] .md-typeset .highlight { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); +} + +/* Line numbers styling */ +.md-typeset .highlight .linenos { + user-select: none; + opacity: 0.5; +} + +/* Copy button enhancement */ +.md-clipboard { + opacity: 0; + transition: opacity 0.2s ease; +} + +.highlight:hover .md-clipboard { + opacity: 1; +} + +/* ============================================================================ + Admonitions & Callouts + ========================================================================= */ + +/* Enhanced admonitions */ +.md-typeset .admonition { + border-radius: 0.5rem; + border-left-width: 0.25rem; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); +} + +/* ============================================================================ + Tables + ========================================================================= */ + +/* Better table styling */ +.md-typeset table:not([class]) { + border-radius: 0.5rem; + overflow: hidden; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); +} + +.md-typeset table:not([class]) th { + background-color: var(--md-primary-fg-color); + color: white; + font-weight: 600; + text-align: left; +} + +.md-typeset table:not([class]) tr:hover { + background-color: var(--md-default-fg-color--lightest); + transition: background-color 0.2s ease; +} + +/* ============================================================================ + API Documentation Styling + ========================================================================= */ + +/* Better spacing for API docs */ +.doc-heading { + margin-top: 2rem !important; +} + +/* Parameter tables */ +.doc-md-description table { + width: 100%; + font-size: 0.9em; +} + +/* Signature styling */ +.doc-signature { + font-family: var(--md-code-font); + background-color: var(--md-code-bg-color); + border-radius: 0.5rem; + padding: 1rem; + overflow-x: auto; +} + +/* ============================================================================ + Home Page Hero (optional enhancement) + ========================================================================= */ + +.hero { + text-align: center; + padding: 4rem 2rem; + background: linear-gradient(135deg, var(--flixopt-teal-light) 0%, var(--flixopt-teal-dark) 100%); + color: white; + border-radius: 1rem; + margin-bottom: 2rem; +} + +.hero h1 { + font-size: 3rem; + margin-bottom: 1rem; + color: white; + border: none; +} + +.hero p { + font-size: 1.25rem; + opacity: 0.9; +} + +/* ============================================================================ + Responsive Design + ========================================================================= */ + +@media screen and (max-width: 76.1875em) { + .md-typeset h1 { + font-size: 2rem; + } +} + +@media screen and (max-width: 44.9375em) { + :root { + --content-padding: 1rem; + } + + .hero h1 { + font-size: 2rem; + } + + .hero p { + font-size: 1rem; + } +} + +/* ============================================================================ + Print Styles + ========================================================================= */ + +@media print { + .md-typeset { + font-size: 0.9rem; + } + + .md-header, + .md-sidebar, + .md-footer { + display: none; + } +} + +/* ============================================================================ + Home Page Inline Styles (moved from docs/index.md) + ========================================================================= */ + +.hero-section { + text-align: center; + padding: 4rem 2rem 3rem 2rem; + background: linear-gradient(135deg, rgba(0, 150, 136, 0.1) 0%, rgba(0, 121, 107, 0.1) 100%); + border-radius: 1rem; + margin-bottom: 3rem; +} + +.hero-section h1 { + font-size: 3.5rem; + font-weight: 700; + margin-bottom: 1rem; + background: linear-gradient(135deg, #009688 0%, #00796B 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.hero-section .tagline { + font-size: 1.5rem; + color: var(--md-default-fg-color--light); + margin-bottom: 2rem; + font-weight: 300; +} + +.hero-buttons { + display: flex; + gap: 1rem; + justify-content: center; + flex-wrap: wrap; + margin-top: 2rem; +} + +.feature-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 2rem; + margin: 3rem 0; +} + +.feature-card { + padding: 2rem; + border-radius: 0.75rem; + background: var(--md-code-bg-color); + border: 1px solid var(--md-default-fg-color--lightest); + transition: all 0.3s ease; + text-align: center; +} + +.feature-card:hover { + transform: translateY(-4px); + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1); + border-color: var(--md-primary-fg-color); +} + +.feature-icon { + font-size: 3rem; + margin-bottom: 1rem; + display: block; +} + +.feature-card h3 { + margin-top: 0; + margin-bottom: 0.5rem; + font-size: 1.25rem; +} + +.feature-card p { + color: var(--md-default-fg-color--light); + margin: 0; + font-size: 0.95rem; + line-height: 1.6; +} + +.stats-banner { + display: flex; + justify-content: space-around; + padding: 2rem; + background: var(--md-code-bg-color); + border-radius: 0.75rem; + margin: 3rem 0; + text-align: center; + flex-wrap: wrap; + gap: 2rem; +} + +.stat-item { + flex: 1; + min-width: 150px; +} + +.stat-number { + font-size: 2.5rem; + font-weight: 700; + color: var(--md-primary-fg-color); + display: block; +} + +.stat-label { + color: var(--md-default-fg-color--light); + font-size: 0.9rem; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.architecture-section { + margin: 4rem 0; + padding: 2rem; + background: var(--md-code-bg-color); + border-radius: 0.75rem; +} + +.quick-links { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 1.5rem; + margin: 3rem 0; +} + +.quick-link-card { + padding: 1.5rem; + border-left: 4px solid var(--md-primary-fg-color); + background: var(--md-code-bg-color); + border-radius: 0.5rem; + transition: all 0.2s ease; + text-decoration: none; + display: block; +} + +.quick-link-card:hover { + background: var(--md-default-fg-color--lightest); + transform: translateX(4px); +} + +.quick-link-card h3 { + margin: 0 0 0.5rem 0; + font-size: 1.1rem; + color: var(--md-primary-fg-color); +} + +.quick-link-card p { + margin: 0; + color: var(--md-default-fg-color--light); + font-size: 0.9rem; +} + +@media screen and (max-width: 768px) { + .hero-section h1 { + font-size: 2.5rem; + } + + .hero-section .tagline { + font-size: 1.2rem; + } + + .hero-buttons { + flex-direction: column; + align-items: stretch; + } + + .feature-grid { + grid-template-columns: 1fr; + } + + .stats-banner { + flex-direction: column; + } +} diff --git a/mkdocs.yml b/mkdocs.yml index 8ed2c4b2c..cc108e237 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,4 +1,4 @@ -# Options: +# flixOpt Documentation Configuration # https://mkdocstrings.github.io/python/usage/configuration/docstrings/ # https://squidfunk.github.io/mkdocs-material/setup/ @@ -39,126 +39,281 @@ nav: - API Reference: api-reference/ - Release Notes: changelog/ - theme: name: material + language: en + palette: - # Light mode + # Palette toggle for automatic mode + - media: "(prefers-color-scheme)" + toggle: + icon: material/brightness-auto + name: Switch to light mode + + # Palette toggle for light mode - media: "(prefers-color-scheme: light)" scheme: default primary: teal - accent: blue + accent: cyan toggle: icon: material/brightness-7 name: Switch to dark mode - # Dark mode + + # Palette toggle for dark mode - media: "(prefers-color-scheme: dark)" scheme: slate - primary: teal # Can be different from light mode - accent: blue + primary: teal + accent: cyan toggle: icon: material/brightness-4 - name: Switch to light mode + name: Switch to system preference + + font: + text: Inter # Modern, readable font + code: Fira Code # Beautiful code font with ligatures + logo: images/flixopt-icon.svg favicon: images/flixopt-icon.svg + icon: repo: fontawesome/brands/github + edit: material/pencil + view: material/eye + annotation: material/plus-circle + features: + # Navigation - navigation.instant - navigation.instant.progress + - navigation.instant.prefetch - navigation.tracking - navigation.tabs + - navigation.tabs.sticky - navigation.sections + - navigation.expand # Expand navigation by default + - navigation.path # Show breadcrumb path + - navigation.prune # Only render visible navigation + - navigation.indexes - navigation.top - navigation.footer + + # Table of contents - toc.follow - - navigation.indexes + - toc.integrate # Integrate TOC into navigation (optional) + + # Search - search.suggest - search.highlight + - search.share + + # Content - content.action.edit - content.action.view - content.code.copy + - content.code.select - content.code.annotate - content.tooltips - - navigation.footer.version + - content.tabs.link # Link content tabs across pages + + # Header + - announce.dismiss # Allow dismissing announcements markdown_extensions: + # Content formatting + - abbr - admonition - - markdown_include.include: - base_path: docs + - attr_list + - def_list + - footnotes + - md_in_html + - tables + - toc: + permalink: true + permalink_title: Anchor link to this section + toc_depth: 3 + + # Code blocks - pymdownx.highlight: anchor_linenums: true line_spans: __span pygments_lang_class: true + auto_title: true - pymdownx.inlinehilite + - pymdownx.snippets: + base_path: .. + check_paths: true + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format + + # Enhanced content - pymdownx.details - - pymdownx.superfences - - attr_list - - abbr - - md_in_html - - footnotes - - tables - pymdownx.tabbed: alternate_style: true + combine_header_slug: true + - pymdownx.tasklist: + custom_checkbox: true + + # Typography + - pymdownx.betterem: + smart_enable: all + - pymdownx.caret + - pymdownx.mark + - pymdownx.tilde + - pymdownx.smartsymbols + - pymdownx.keys + + # Math - pymdownx.arithmatex: generic: true + + # Icons & emojis - pymdownx.emoji: emoji_index: !!python/name:material.extensions.emoji.twemoji emoji_generator: !!python/name:material.extensions.emoji.to_svg - - pymdownx.snippets: - base_path: .. + options: + custom_icons: + - overrides/.icons + + # Legacy support + - markdown_include.include: + base_path: docs plugins: - - search # Enables the search functionality in the documentation - - table-reader # Allows including tables from external files + - search: + separator: '[\s\u200b\-_,:!=\[\]()"`/]+|\.(?!\d)|&[lg]t;|(?!\b)(?=[A-Z][a-z])' + + - table-reader + - include-markdown + - mike: + alias_type: symlink + redirect_template: null + deploy_prefix: '' + canonical_version: null version_selector: true + css_dir: css + javascript_dir: js + - literate-nav: nav_file: SUMMARY.md + implicit_index: true + - gen-files: scripts: - - scripts/gen_ref_pages.py - - mkdocstrings: # Handles automatic API documentation generation - default_handler: python # Sets Python as the default language - handlers: - python: # Configuration for Python code documentation - options: - docstring_style: google # Sets google as the docstring style - modernize_annotations: true # Improves type annotations - merge_init_into_class: true # Promotes constructor parameters to class-level documentation - docstring_section_style: table # Renders parameter sections as a table (also: list, spacy) - - members_order: source # Orders members as they appear in the source code - inherited_members: false # Include members inherited from parent classes - show_if_no_docstring: false # Documents objects even if they don't have docstrings - - group_by_category: true - heading_level: 1 # Sets the base heading level for documented objects - line_length: 80 - filters: ["!^_", "^__init__$"] - show_root_heading: true # whether the documented object's name should be displayed as a heading at the beginning of its documentation - show_source: false # Shows the source code implementation from documentation - show_object_full_path: false # Displays simple class names instead of full import paths - show_docstring_attributes: true # Shows class attributes in the documentation - show_category_heading: true # Displays category headings (Methods, Attributes, etc.) for organization - show_signature: true # Shows method signatures with parameters - show_signature_annotations: true # Includes type annotations in the signatures when available - show_root_toc_entry: false # Whether to show a link to the root of the documentation in the sidebar - separate_signature: true # Displays signatures separate from descriptions for cleaner layout - - extra: - infer_type_annotations: true # Uses Python type hints to supplement docstring information + - scripts/gen_ref_pages.py + + - mkdocstrings: + enabled: !ENV [ENABLE_MKDOCSTRINGS, true] + default_handler: python + handlers: + python: + paths: [.] + import: + - https://docs.python.org/3/objects.inv + - https://numpy.org/doc/stable/objects.inv + - https://pandas.pydata.org/docs/objects.inv + options: + # Docstring parsing + docstring_style: google + docstring_section_style: table + + # Member ordering and filtering + members_order: source + inherited_members: false + show_if_no_docstring: false + filters: ["!^_", "^__init__$"] + group_by_category: true + + # Headings and structure + heading_level: 1 + show_root_heading: true + show_root_toc_entry: false + show_category_heading: true + + # Signatures + show_signature: true + show_signature_annotations: true + separate_signature: true + line_length: 80 + + # Source and paths + show_source: false + show_object_full_path: false + + # Attributes and annotations + show_docstring_attributes: true + modernize_annotations: true + merge_init_into_class: true + + # Improved type hints + annotations_path: brief + + # Optional: Add git info + - git-revision-date-localized: + enable_creation_date: true + type: timeago + fallback_to_build_date: true + + # Optional: Add better navigation + - tags: + tags_file: tags.md + + # Optional: Minify HTML in production + - minify: + minify_html: true + minify_js: true + minify_css: true + htmlmin_opts: + remove_comments: true extra: version: provider: mike default: latest + alias: true + + social: + - icon: fontawesome/brands/github + link: https://github.com/flixOpt/flixopt + name: flixOpt on GitHub + - icon: fontawesome/brands/python + link: https://pypi.org/project/flixopt/ + name: flixOpt on PyPI + + analytics: + provider: google + property: !ENV GOOGLE_ANALYTICS_KEY + feedback: + title: Was this page helpful? + ratings: + - icon: material/emoticon-happy-outline + name: This page was helpful + data: 1 + note: >- + Thanks for your feedback! + - icon: material/emoticon-sad-outline + name: This page could be improved + data: 0 + note: >- + Thanks for your feedback! Help us improve by + opening an issue. + + status: + new: Recently added + deprecated: Deprecated + +extra_css: + - stylesheets/extra.css extra_javascript: - - javascripts/mathjax.js # Custom MathJax 3 CDN Configuration - - https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js #MathJax 3 CDN - - https://polyfill.io/v3/polyfill.min.js?features=es6 #Support for older browsers + - javascripts/mathjax.js + - https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js + - https://polyfill.io/v3/polyfill.min.js?features=es6 watch: - flixopt + - docs diff --git a/pyproject.toml b/pyproject.toml index 9e1821a4b..227eca49e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,9 +34,9 @@ dependencies = [ # Core scientific computing "numpy >= 1.21.5, < 3", "pandas >= 2.0.0, < 3", - "xarray >= 2024.2.0, < 2026.0", # CalVer: allow through next calendar year + "xarray >= 2024.2.0, < 2026.0", # CalVer: allow through next calendar year # Optimization and data handling - "linopy >= 0.5.1, < 0.6", # Widened from patch pin to minor range + "linopy >= 0.5.1, < 0.6", # Widened from patch pin to minor range "h5netcdf>=1.0.0, < 2", # Utilities "pyyaml >= 6.0.0, < 7", @@ -44,13 +44,11 @@ dependencies = [ "tomli >= 2.0.1, < 3; python_version < '3.11'", # Only needed with python 3.10 or earlier # Default solver "highspy >= 1.5.3, < 2", - # Visualization "matplotlib >= 3.5.2, < 4", "plotly >= 5.15.0, < 7", - # Fix for numexpr compatibility issue with numpy 1.26.4 on Python 3.10 - "numexpr >= 2.8.4, < 2.14; python_version < '3.11'", # Avoid 2.14.0 on older Python + "numexpr >= 2.8.4, < 2.14; python_version < '3.11'", # Avoid 2.14.0 on older Python ] [project.optional-dependencies] @@ -98,6 +96,7 @@ dev = [ # Documentation building docs = [ + "mkdocs==1.6.1", "mkdocs-material==9.6.21", "mkdocstrings-python==1.18.2", "mkdocs-table-reader-plugin==3.1.0", @@ -108,6 +107,8 @@ docs = [ "pymdown-extensions==10.16.1", "pygments==2.19.2", "mike==2.1.3", + "mkdocs-git-revision-date-localized-plugin==1.4.7", + "mkdocs-minify-plugin==0.8.0", ] [project.urls] From 84ab912279f416d156d9d6547284318127be7ecd Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 18 Oct 2025 21:48:00 +0200 Subject: [PATCH 361/448] Update CHANGELOG.md --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41dae8ad4..7b826569f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,13 +64,15 @@ If upgrading from v2.x, see the [Migration Guide](https://flixopt.github.io/flix ### 🔥 Removed ### 🐛 Fixed +- Add error handling for empty buses to prevent cryptic errors +- Add early validation for non-existent periods when using linked periods with tuples ### 🔒 Security ### 📦 Dependencies ### 📝 Docs -- Improve docs visually +- Improve docs visually with new Material theme and enhanced styling ### 👷 Development From 75a23915bd81e92aa16edf40959bf3461bab3265 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 19 Oct 2025 18:14:54 +0200 Subject: [PATCH 362/448] Feature/398 feature facet plots in results (#419) (#422) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feature/398 feature facet plots in results (#419) * Add animation and faceting options to plots * Adjust size of the frame * Utilize plotly express directly * Rmeocve old class * Use plotly express and modify stackgroup afterwards * Add modifications also to animations * Mkae more compact * Remove height stuff * Remove line and make set opacity =0 for area * Integrate faceting and animating into existing with_plotly method * Improve results.py * Improve results.py * Move check if dims are found to plotting.py * Fix usage of indexer * Change selection string with indexer * Change behaviout of parameter "indexing" * Update CHANGELOG.md * Add new selection parameter to plotting methods * deprectae old indexer parameter * deprectae old indexer parameter * Add test * Add test * Add test * Add test * Fix not supportet check for matplotlib * Typo in CHANGELOG.md * Feature/398 feature facet plots in results heatmaps (#418) * Add animation and faceting options to plots * Adjust size of the frame * Utilize plotly express directly * Rmeocve old class * Use plotly express and modify stackgroup afterwards * Add modifications also to animations * Mkae more compact * Remove height stuff * Remove line and make set opacity =0 for area * Integrate faceting and animating into existing with_plotly method * Improve results.py * Improve results.py * Move check if dims are found to plotting.py * Fix usage of indexer * Change selection string with indexer * Change behaviout of parameter "indexing" * Update CHANGELOG.md * Add new selection parameter to plotting methods * deprectae old indexer parameter * deprectae old indexer parameter * Add test * Add test * Add test * Add test * Add heatmap support * Unify to a single heatmap method per engine * Change defaults * readd time reshaping * readd time reshaping * lengthen scenario example * Update * Improve heatmap plotting * Improve heatmap plotting * Moved reshaping to plotting.py * COmbinations are possible! * Improve 'auto'behavioour * Improve 'auto' behavioour * Improve 'auto' behavioour * Allow multiple variables in a heatmap * Update modeule level plot_heatmap() * remove code duplication * Allow Dataset instead of List of DataArrays * Allow Dataset instead of List of DataArrays * Update plot tests * FIx Missing renme in ElementResults.plot_heatmap() * Update API * Feature/398 feature facet plots in results charge state (#417) * Add animation and faceting options to plots * Adjust size of the frame * Utilize plotly express directly * Rmeocve old class * Use plotly express and modify stackgroup afterwards * Add modifications also to animations * Mkae more compact * Remove height stuff * Remove line and make set opacity =0 for area * Integrate faceting and animating into existing with_plotly method * Improve results.py * Improve results.py * Move check if dims are found to plotting.py * Fix usage of indexer * Change selection string with indexer * Change behaviout of parameter "indexing" * Update CHANGELOG.md * Add new selection parameter to plotting methods * deprectae old indexer parameter * deprectae old indexer parameter * Add test * Add test * Add test * Add test * Add heatmap support * Unify to a single heatmap method per engine * Change defaults * readd time reshaping * readd time reshaping * lengthen scenario example * Update * Improve heatmap plotting * Improve heatmap plotting * Moved reshaping to plotting.py * COmbinations are possible! * Improve 'auto'behavioour * Improve 'auto' behavioour * Improve 'auto' behavioour * Allow multiple variables in a heatmap * Update modeule level plot_heatmap() * remove code duplication * Allow Dataset instead of List of DataArrays * Allow Dataset instead of List of DataArrays * Add tests * More examples * Update plot_charge state() * Try 1 * Try 2 * Add more examples * Add more examples * Add smooth line for charge state and use "area" as default * Update scenario_example.py * Update tests * Fix Error handling in plot_heatmap() * Feature/398 feature facet plots in results pie (#421) * Add animation and faceting options to plots * Adjust size of the frame * Utilize plotly express directly * Rmeocve old class * Use plotly express and modify stackgroup afterwards * Add modifications also to animations * Mkae more compact * Remove height stuff * Remove line and make set opacity =0 for area * Integrate faceting and animating into existing with_plotly method * Improve results.py * Improve results.py * Move check if dims are found to plotting.py * Fix usage of indexer * Change selection string with indexer * Change behaviout of parameter "indexing" * Update CHANGELOG.md * Add new selection parameter to plotting methods * deprectae old indexer parameter * deprectae old indexer parameter * Add test * Add test * Add test * Add test * Add heatmap support * Unify to a single heatmap method per engine * Change defaults * readd time reshaping * readd time reshaping * lengthen scenario example * Update * Improve heatmap plotting * Improve heatmap plotting * Moved reshaping to plotting.py * COmbinations are possible! * Improve 'auto'behavioour * Improve 'auto' behavioour * Improve 'auto' behavioour * Allow multiple variables in a heatmap * Update modeule level plot_heatmap() * remove code duplication * Allow Dataset instead of List of DataArrays * Allow Dataset instead of List of DataArrays * Add tests * More examples * Update plot_charge state() * Try 1 * Try 2 * Add more examples * Add more examples * Add smooth line for charge state and use "area" as default * Update scenario_example.py * Update tests * Handle extra dims in pie plots by selecting the first * 6. Optimized time-step check - Replaced pandas Series diff() with NumPy np.diff() for better performance - Changed check from > 0 to > 1 (can't calculate diff with 0 or 1 element) - Converted to seconds first, then to minutes to avoid pandas timedelta conversion issues * Typo * Improve type handling * Update other tests * Handle backwards compatability * Add better error messages if both new and old api are used * Add old api explicitly * Add old api explicitly * Improve consistency and properly deprectae the indexer parameter * Remove amount of new tests * Remove amount of new tests * Fix CONTRIBUTING.md * Remove old test file * Add tests/test_heatmap_reshape.py * Add tests/test_heatmap_reshape.py * Remove unused method * - Implemented dashed line styling for "mixed" variables (variables with both positive and negative values) - Only stack "positive" and "negative" classifications, not "mixed" or "zero" * - Added fill parameter to module-level plot_heatmap function (line 1914) - Added fill parameter to CalculationResults.plot_heatmap method (line 702) - Forwarded fill parameter to both heatmap_with_plotly and heatmap_with_matplotlib functions * - Added np.random.seed(42) for reproducible test results - Added specific size assertions to all tests: - Daily/hourly pattern: 3 days × 24 hours - Weekly/daily pattern: 1 week × 7 days - Irregular data: 25 hours × 60 minutes - Multidimensional: 2 days × 24 hours with preserved scenario dimension * Improve Error Message if too many dims for matplotlib * Improve Error Message if too many dims for matplotlib * Improve Error Message if too many dims for matplotlib * Rename _apply_indexer_to_data() to _apply_selection_to_data() * Bugfix * Update CHANGELOG.md * Catch edge case in with_plotly() * Add strict=True * Improve scenario_example.py * Improve scenario_example.py * Change logging level in essage about time reshape * Update CHANGELOG.md --- .github/CONTRIBUTING.md | 2 +- CHANGELOG.md | 10 + examples/04_Scenarios/scenario_example.py | 128 ++- flixopt/plotting.py | 986 +++++++++++++++------- flixopt/results.py | 793 +++++++++++++---- tests/ressources/Sim1--flow_system.nc4 | Bin 0 -> 218834 bytes tests/ressources/Sim1--solution.nc4 | Bin 0 -> 210822 bytes tests/ressources/Sim1--summary.yaml | 92 ++ tests/test_heatmap_reshape.py | 91 ++ tests/test_plots.py | 151 ---- tests/test_results_plots.py | 31 +- 11 files changed, 1632 insertions(+), 652 deletions(-) create mode 100644 tests/ressources/Sim1--flow_system.nc4 create mode 100644 tests/ressources/Sim1--solution.nc4 create mode 100644 tests/ressources/Sim1--summary.yaml create mode 100644 tests/test_heatmap_reshape.py delete mode 100644 tests/test_plots.py diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 2a51618d9..e9876c089 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -12,7 +12,7 @@ Thanks for your interest in contributing to FlixOpt! 🚀 2. **Install for Development** ```bash - pip install -e ".[full]" + pip install -e ".[full, dev, docs]" ``` 3. **Make Changes & Submit PR** diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b826569f..e8836e87d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,12 +54,21 @@ If upgrading from v2.x, see the [Migration Guide](https://flixopt.github.io/flix ### ✨ Added +- **Faceting and animation support for plots**: All plotting methods now support `facet_by` and `animate_by` parameters for creating subplot grids and animations with multidimensional data (scenarios, periods, etc.) +- **New `select` parameter**: Added to all plotting methods for flexible data selection using single values, lists, slices, and index arrays +- **Heatmap `fill` parameter**: Added `fill` parameter to heatmap plotting methods to control how missing values are filled after reshaping ('ffill' or 'bfill') +- **Dashed line styling**: Area plots now automatically style "mixed" variables (containing both positive and negative values) with dashed lines, while only stacking purely positive or negative variables ### 💥 Breaking Changes ### ♻️ Changed +- **Selection behavior**: Changed default selection behavior in plotting methods - no longer automatically selects first value for non-time dimensions. Use `select` parameter for explicit selection +- **Improved error messages**: Enhanced error messages when using matplotlib engine with multidimensional data, providing clearer guidance on dimension requirements +- Improved `scenario_example.py` +- Improved error handling in `plot_heatmap()` method for better dimension validation ### 🗑️ Deprecated +- **`indexer` parameter**: The `indexer` parameter in all plotting methods is deprecated in favor of the new `select` parameter with enhanced functionality ### 🔥 Removed @@ -75,6 +84,7 @@ If upgrading from v2.x, see the [Migration Guide](https://flixopt.github.io/flix - Improve docs visually with new Material theme and enhanced styling ### 👷 Development +- Renamed `_apply_indexer_to_data()` to `_apply_selection_to_data()` for consistency with new API ### 🚧 Known Issues diff --git a/examples/04_Scenarios/scenario_example.py b/examples/04_Scenarios/scenario_example.py index 6aa3c0c89..834e55782 100644 --- a/examples/04_Scenarios/scenario_example.py +++ b/examples/04_Scenarios/scenario_example.py @@ -8,20 +8,80 @@ import flixopt as fx if __name__ == '__main__': - # Create datetime array starting from '2020-01-01' for the given time period - timesteps = pd.date_range('2020-01-01', periods=9, freq='h') + # Create datetime array starting from '2020-01-01' for one week + timesteps = pd.date_range('2020-01-01', periods=24 * 7, freq='h') scenarios = pd.Index(['Base Case', 'High Demand']) periods = pd.Index([2020, 2021, 2022]) # --- Create Time Series Data --- - # Heat demand profile (e.g., kW) over time and corresponding power prices - heat_demand_per_h = pd.DataFrame( - {'Base Case': [30, 0, 90, 110, 110, 20, 20, 20, 20], 'High Demand': [30, 0, 100, 118, 125, 20, 20, 20, 20]}, - index=timesteps, + # Realistic daily patterns: morning/evening peaks, night/midday lows + np.random.seed(42) + n_hours = len(timesteps) + + # Heat demand: 24-hour patterns (kW) for Base Case and High Demand scenarios + base_daily_pattern = np.array( + [22, 20, 18, 18, 20, 25, 40, 70, 95, 110, 85, 65, 60, 58, 62, 68, 75, 88, 105, 125, 130, 122, 95, 35] + ) + high_daily_pattern = np.array( + [28, 25, 22, 22, 24, 30, 52, 88, 118, 135, 105, 80, 75, 72, 75, 82, 92, 108, 128, 148, 155, 145, 115, 48] + ) + + # Tile and add variation + base_demand = np.tile(base_daily_pattern, n_hours // 24 + 1)[:n_hours] * ( + 1 + np.random.uniform(-0.05, 0.05, n_hours) + ) + high_demand = np.tile(high_daily_pattern, n_hours // 24 + 1)[:n_hours] * ( + 1 + np.random.uniform(-0.07, 0.07, n_hours) + ) + + heat_demand_per_h = pd.DataFrame({'Base Case': base_demand, 'High Demand': high_demand}, index=timesteps) + + # Power prices: hourly factors (night low, peak high) and period escalation (2020-2022) + hourly_price_factors = np.array( + [ + 0.70, + 0.65, + 0.62, + 0.60, + 0.62, + 0.70, + 0.95, + 1.15, + 1.30, + 1.25, + 1.10, + 1.00, + 0.95, + 0.90, + 0.88, + 0.92, + 1.00, + 1.10, + 1.25, + 1.40, + 1.35, + 1.20, + 0.95, + 0.80, + ] ) - power_prices = np.array([0.08, 0.09, 0.10]) + period_base_prices = np.array([0.075, 0.095, 0.135]) # €/kWh for 2020, 2021, 2022 - flow_system = fx.FlowSystem(timesteps=timesteps, periods=periods, scenarios=scenarios, weights=np.array([0.5, 0.6])) + price_series = np.zeros((n_hours, 3)) + for period_idx, base_price in enumerate(period_base_prices): + price_series[:, period_idx] = ( + np.tile(hourly_price_factors, n_hours // 24 + 1)[:n_hours] + * base_price + * (1 + np.random.uniform(-0.03, 0.03, n_hours)) + ) + + power_prices = price_series.mean(axis=0) + + # Scenario weights: probability of each scenario occurring + # Base Case: 60% probability, High Demand: 40% probability + scenario_weights = np.array([0.6, 0.4]) + + flow_system = fx.FlowSystem(timesteps=timesteps, periods=periods, scenarios=scenarios, weights=scenario_weights) # --- Define Energy Buses --- # These represent nodes, where the used medias are balanced (electricity, heat, and gas) @@ -35,22 +95,24 @@ description='Kosten', is_standard=True, # standard effect: no explicit value needed for costs is_objective=True, # Minimizing costs as the optimization objective - share_from_temporal={'CO2': 0.2}, + share_from_temporal={'CO2': 0.2}, # Carbon price: 0.2 €/kg CO2 (e.g., carbon tax) ) - # CO2 emissions effect with an associated cost impact + # CO2 emissions effect with constraint + # Maximum of 1000 kg CO2/hour represents a regulatory or voluntary emissions limit CO2 = fx.Effect( label='CO2', unit='kg', description='CO2_e-Emissionen', - maximum_per_hour=1000, # Max CO2 emissions per hour + maximum_per_hour=1000, # Regulatory emissions limit: 1000 kg CO2/hour ) # --- Define Flow System Components --- # Boiler: Converts fuel (gas) into thermal energy (heat) + # Modern condensing gas boiler with realistic efficiency boiler = fx.linear_converters.Boiler( label='Boiler', - eta=0.5, + eta=0.92, # Realistic efficiency for modern condensing gas boiler (92%) Q_th=fx.Flow( label='Q_th', bus='Fernwärme', @@ -63,27 +125,28 @@ ) # Combined Heat and Power (CHP): Generates both electricity and heat from fuel + # Modern CHP unit with realistic efficiencies (total efficiency ~88%) chp = fx.linear_converters.CHP( label='CHP', - eta_th=0.5, - eta_el=0.4, + eta_th=0.48, # Realistic thermal efficiency (48%) + eta_el=0.40, # Realistic electrical efficiency (40%) P_el=fx.Flow('P_el', bus='Strom', size=60, relative_minimum=5 / 60, on_off_parameters=fx.OnOffParameters()), Q_th=fx.Flow('Q_th', bus='Fernwärme'), Q_fu=fx.Flow('Q_fu', bus='Gas'), ) - # Storage: Energy storage system with charging and discharging capabilities + # Storage: Thermal energy storage system with charging and discharging capabilities + # Realistic thermal storage parameters (e.g., insulated hot water tank) storage = fx.Storage( label='Storage', charging=fx.Flow('Q_th_load', bus='Fernwärme', size=1000), discharging=fx.Flow('Q_th_unload', bus='Fernwärme', size=1000), capacity_in_flow_hours=fx.InvestParameters(effects_of_investment=20, fixed_size=30, mandatory=True), initial_charge_state=0, # Initial storage state: empty - relative_maximum_charge_state=np.array([80, 70, 80, 80, 80, 80, 80, 80, 80]) * 0.01, - relative_maximum_final_charge_state=0.8, - eta_charge=0.9, - eta_discharge=1, # Efficiency factors for charging/discharging - relative_loss_per_hour=0.08, # 8% loss per hour. Absolute loss depends on current charge state + relative_maximum_final_charge_state=np.array([0.8, 0.5, 0.1]), + eta_charge=0.95, # Realistic charging efficiency (~95%) + eta_discharge=0.98, # Realistic discharging efficiency (~98%) + relative_loss_per_hour=np.array([0.008, 0.015]), # Realistic thermal losses: 0.8-1.5% per hour prevent_simultaneous_charge_and_discharge=True, # Prevent charging and discharging at the same time ) @@ -94,10 +157,22 @@ ) # Gas Source: Gas tariff source with associated costs and CO2 emissions + # Realistic gas prices varying by period (reflecting 2020-2022 energy crisis) + # 2020: 0.04 €/kWh, 2021: 0.06 €/kWh, 2022: 0.11 €/kWh + gas_prices_per_period = np.array([0.04, 0.06, 0.11]) + + # CO2 emissions factor for natural gas: ~0.202 kg CO2/kWh (realistic value) + gas_co2_emissions = 0.202 + gas_source = fx.Source( label='Gastarif', outputs=[ - fx.Flow(label='Q_Gas', bus='Gas', size=1000, effects_per_flow_hour={costs.label: 0.04, CO2.label: 0.3}) + fx.Flow( + label='Q_Gas', + bus='Gas', + size=1000, + effects_per_flow_hour={costs.label: gas_prices_per_period, CO2.label: gas_co2_emissions}, + ) ], ) @@ -124,21 +199,14 @@ calculation.results.plot_heatmap('CHP(Q_th)|flow_rate') # --- Analyze Results --- - calculation.results['Fernwärme'].plot_node_balance_pie() calculation.results['Fernwärme'].plot_node_balance(mode='stacked_bar') - calculation.results['Storage'].plot_node_balance() calculation.results.plot_heatmap('CHP(Q_th)|flow_rate') + calculation.results['Storage'].plot_charge_state() + calculation.results['Fernwärme'].plot_node_balance_pie(select={'period': 2020, 'scenario': 'Base Case'}) # Convert the results for the storage component to a dataframe and display df = calculation.results['Storage'].node_balance_with_charge_state() print(df) - # Plot charge state using matplotlib - fig, ax = calculation.results['Storage'].plot_charge_state(engine='matplotlib') - # Customize the plot further if needed - ax.set_title('Storage Charge State Over Time') - # Or save the figure - # fig.savefig('storage_charge_state.png') - # Save results to file for later usage calculation.results.to_file() diff --git a/flixopt/plotting.py b/flixopt/plotting.py index 218a8ab0e..bd1f3c2c4 100644 --- a/flixopt/plotting.py +++ b/flixopt/plotting.py @@ -39,6 +39,7 @@ import plotly.express as px import plotly.graph_objects as go import plotly.offline +import xarray as xr from plotly.exceptions import PlotlyError if TYPE_CHECKING: @@ -326,143 +327,269 @@ def process_colors( def with_plotly( - data: pd.DataFrame, + data: pd.DataFrame | xr.DataArray | xr.Dataset, mode: Literal['stacked_bar', 'line', 'area', 'grouped_bar'] = 'stacked_bar', colors: ColorType = 'viridis', title: str = '', ylabel: str = '', xlabel: str = 'Time in h', fig: go.Figure | None = None, + facet_by: str | list[str] | None = None, + animate_by: str | None = None, + facet_cols: int = 3, + shared_yaxes: bool = True, + shared_xaxes: bool = True, ) -> go.Figure: """ - Plot a DataFrame with Plotly, using either stacked bars or stepped lines. + Plot data with Plotly using facets (subplots) and/or animation for multidimensional data. + + Uses Plotly Express for convenient faceting and animation with automatic styling. + For simple plots without faceting, can optionally add to an existing figure. Args: - data: A DataFrame containing the data to plot, where the index represents time (e.g., hours), - and each column represents a separate data series. - mode: The plotting mode. Use 'stacked_bar' for stacked bar charts, 'line' for stepped lines, - or 'area' for stacked area charts. - colors: Color specification, can be: - - A string with a colorscale name (e.g., 'viridis', 'plasma') - - A list of color strings (e.g., ['#ff0000', '#00ff00']) - - A dictionary mapping column names to colors (e.g., {'Column1': '#ff0000'}) - title: The title of the plot. + data: A DataFrame or xarray DataArray/Dataset to plot. + mode: The plotting mode. Use 'stacked_bar' for stacked bar charts, 'line' for lines, + 'area' for stacked area charts, or 'grouped_bar' for grouped bar charts. + colors: Color specification (colormap, list, or dict mapping labels to colors). + title: The main title of the plot. ylabel: The label for the y-axis. xlabel: The label for the x-axis. - fig: A Plotly figure object to plot on. If not provided, a new figure will be created. + fig: A Plotly figure object to plot on (only for simple plots without faceting). + If not provided, a new figure will be created. + facet_by: Dimension(s) to create facets for. Creates a subplot grid. + Can be a single dimension name or list of dimensions (max 2 for facet_row and facet_col). + If the dimension doesn't exist in the data, it will be silently ignored. + animate_by: Dimension to animate over. Creates animation frames. + If the dimension doesn't exist in the data, it will be silently ignored. + facet_cols: Number of columns in the facet grid (used when facet_by is single dimension). + shared_yaxes: Whether subplots share y-axes. + shared_xaxes: Whether subplots share x-axes. Returns: - A Plotly figure object containing the generated plot. + A Plotly figure object containing the faceted/animated plot. + + Examples: + Simple plot: + + ```python + fig = with_plotly(df, mode='area', title='Energy Mix') + ``` + + Facet by scenario: + + ```python + fig = with_plotly(ds, facet_by='scenario', facet_cols=2) + ``` + + Animate by period: + + ```python + fig = with_plotly(ds, animate_by='period') + ``` + + Facet and animate: + + ```python + fig = with_plotly(ds, facet_by='scenario', animate_by='period') + ``` """ if mode not in ('stacked_bar', 'line', 'area', 'grouped_bar'): raise ValueError(f"'mode' must be one of {{'stacked_bar','line','area', 'grouped_bar'}}, got {mode!r}") - if data.empty: - return go.Figure() - processed_colors = ColorProcessor(engine='plotly').process_colors(colors, list(data.columns)) - - fig = fig if fig is not None else go.Figure() + # Handle empty data + if isinstance(data, pd.DataFrame) and data.empty: + return go.Figure() + elif isinstance(data, xr.DataArray) and data.size == 0: + return go.Figure() + elif isinstance(data, xr.Dataset) and len(data.data_vars) == 0: + return go.Figure() - if mode == 'stacked_bar': - for i, column in enumerate(data.columns): - fig.add_trace( - go.Bar( - x=data.index, - y=data[column], - name=column, - marker=dict( - color=processed_colors[i], line=dict(width=0, color='rgba(0,0,0,0)') - ), # Transparent line with 0 width + # Warn if fig parameter is used with faceting + if fig is not None and (facet_by is not None or animate_by is not None): + logger.warning('The fig parameter is ignored when using faceting or animation. Creating a new figure.') + fig = None + + # Convert xarray to long-form DataFrame for Plotly Express + if isinstance(data, (xr.DataArray, xr.Dataset)): + # Convert to long-form (tidy) DataFrame + # Structure: time, variable, value, scenario, period, ... (all dims as columns) + if isinstance(data, xr.Dataset): + # Stack all data variables into long format + df_long = data.to_dataframe().reset_index() + # Melt to get: time, scenario, period, ..., variable, value + id_vars = [dim for dim in data.dims] + value_vars = list(data.data_vars) + df_long = df_long.melt(id_vars=id_vars, value_vars=value_vars, var_name='variable', value_name='value') + else: + # DataArray + df_long = data.to_dataframe().reset_index() + if data.name: + df_long = df_long.rename(columns={data.name: 'value'}) + else: + # Unnamed DataArray, find the value column + non_dim_cols = [col for col in df_long.columns if col not in data.dims] + if len(non_dim_cols) != 1: + raise ValueError( + f'Expected exactly one non-dimension column for unnamed DataArray, ' + f'but found {len(non_dim_cols)}: {non_dim_cols}' + ) + value_col = non_dim_cols[0] + df_long = df_long.rename(columns={value_col: 'value'}) + df_long['variable'] = data.name or 'data' + else: + # Already a DataFrame - convert to long format for Plotly Express + df_long = data.reset_index() + if 'time' not in df_long.columns: + # First column is probably time + df_long = df_long.rename(columns={df_long.columns[0]: 'time'}) + # Melt to long format + id_vars = [ + col + for col in df_long.columns + if col in ['time', 'scenario', 'period'] + or col in (facet_by if isinstance(facet_by, list) else [facet_by] if facet_by else []) + ] + value_vars = [col for col in df_long.columns if col not in id_vars] + df_long = df_long.melt(id_vars=id_vars, value_vars=value_vars, var_name='variable', value_name='value') + + # Validate facet_by and animate_by dimensions exist in the data + available_dims = [col for col in df_long.columns if col not in ['variable', 'value']] + + # Check facet_by dimensions + if facet_by is not None: + if isinstance(facet_by, str): + if facet_by not in available_dims: + logger.debug( + f"Dimension '{facet_by}' not found in data. Available dimensions: {available_dims}. " + f'Ignoring facet_by parameter.' ) - ) - - fig.update_layout( - barmode='relative', - bargap=0, # No space between bars - bargroupgap=0, # No space between grouped bars + facet_by = None + elif isinstance(facet_by, list): + # Filter out dimensions that don't exist + missing_dims = [dim for dim in facet_by if dim not in available_dims] + facet_by = [dim for dim in facet_by if dim in available_dims] + if missing_dims: + logger.debug( + f'Dimensions {missing_dims} not found in data. Available dimensions: {available_dims}. ' + f'Using only existing dimensions: {facet_by if facet_by else "none"}.' + ) + if len(facet_by) == 0: + facet_by = None + + # Check animate_by dimension + if animate_by is not None and animate_by not in available_dims: + logger.debug( + f"Dimension '{animate_by}' not found in data. Available dimensions: {available_dims}. " + f'Ignoring animate_by parameter.' ) - if mode == 'grouped_bar': - for i, column in enumerate(data.columns): - fig.add_trace(go.Bar(x=data.index, y=data[column], name=column, marker=dict(color=processed_colors[i]))) + animate_by = None + + # Setup faceting parameters for Plotly Express + facet_row = None + facet_col = None + if facet_by: + if isinstance(facet_by, str): + # Single facet dimension - use facet_col with facet_col_wrap + facet_col = facet_by + elif len(facet_by) == 1: + facet_col = facet_by[0] + elif len(facet_by) == 2: + # Two facet dimensions - use facet_row and facet_col + facet_row = facet_by[0] + facet_col = facet_by[1] + else: + raise ValueError(f'facet_by can have at most 2 dimensions, got {len(facet_by)}') + + # Process colors + all_vars = df_long['variable'].unique().tolist() + processed_colors = ColorProcessor(engine='plotly').process_colors(colors, all_vars) + color_discrete_map = {var: color for var, color in zip(all_vars, processed_colors, strict=True)} + + # Create plot using Plotly Express based on mode + common_args = { + 'data_frame': df_long, + 'x': 'time', + 'y': 'value', + 'color': 'variable', + 'facet_row': facet_row, + 'facet_col': facet_col, + 'animation_frame': animate_by, + 'color_discrete_map': color_discrete_map, + 'title': title, + 'labels': {'value': ylabel, 'time': xlabel, 'variable': ''}, + } - fig.update_layout( - barmode='group', - bargap=0.2, # No space between bars - bargroupgap=0, # space between grouped bars - ) + # Add facet_col_wrap for single facet dimension + if facet_col and not facet_row: + common_args['facet_col_wrap'] = facet_cols + + if mode == 'stacked_bar': + fig = px.bar(**common_args) + fig.update_traces(marker_line_width=0) + fig.update_layout(barmode='relative', bargap=0, bargroupgap=0) + elif mode == 'grouped_bar': + fig = px.bar(**common_args) + fig.update_layout(barmode='group', bargap=0.2, bargroupgap=0) elif mode == 'line': - for i, column in enumerate(data.columns): - fig.add_trace( - go.Scatter( - x=data.index, - y=data[column], - mode='lines', - name=column, - line=dict(shape='hv', color=processed_colors[i]), - ) - ) + fig = px.line(**common_args, line_shape='hv') # Stepped lines elif mode == 'area': - data = data.copy() - data[(data > -1e-5) & (data < 1e-5)] = 0 # Preventing issues with plotting - # Split columns into positive, negative, and mixed categories - positive_columns = list(data.columns[(data >= 0).where(~np.isnan(data), True).all()]) - negative_columns = list(data.columns[(data <= 0).where(~np.isnan(data), True).all()]) - negative_columns = [column for column in negative_columns if column not in positive_columns] - mixed_columns = list(set(data.columns) - set(positive_columns + negative_columns)) - - if mixed_columns: - logger.error( - f'Data for plotting stacked lines contains columns with both positive and negative values:' - f' {mixed_columns}. These can not be stacked, and are printed as simple lines' - ) + # Use Plotly Express to create the area plot (preserves animation, legends, faceting) + fig = px.area(**common_args, line_shape='hv') - # Get color mapping for all columns - colors_stacked = {column: processed_colors[i] for i, column in enumerate(data.columns)} - - for column in positive_columns + negative_columns: - fig.add_trace( - go.Scatter( - x=data.index, - y=data[column], - mode='lines', - name=column, - line=dict(shape='hv', color=colors_stacked[column]), - fill='tonexty', - stackgroup='pos' if column in positive_columns else 'neg', - ) - ) + # Classify each variable based on its values + variable_classification = {} + for var in all_vars: + var_data = df_long[df_long['variable'] == var]['value'] + var_data_clean = var_data[(var_data < -1e-5) | (var_data > 1e-5)] - for column in mixed_columns: - fig.add_trace( - go.Scatter( - x=data.index, - y=data[column], - mode='lines', - name=column, - line=dict(shape='hv', color=colors_stacked[column], dash='dash'), + if len(var_data_clean) == 0: + variable_classification[var] = 'zero' + else: + has_pos, has_neg = (var_data_clean > 0).any(), (var_data_clean < 0).any() + variable_classification[var] = ( + 'mixed' if has_pos and has_neg else ('negative' if has_neg else 'positive') ) - ) - # Update layout for better aesthetics + # Log warning for mixed variables + mixed_vars = [v for v, c in variable_classification.items() if c == 'mixed'] + if mixed_vars: + logger.warning(f'Variables with both positive and negative values: {mixed_vars}. Plotted as dashed lines.') + + all_traces = list(fig.data) + for frame in fig.frames: + all_traces.extend(frame.data) + + for trace in all_traces: + cls = variable_classification.get(trace.name, None) + # Only stack positive and negative, not mixed or zero + trace.stackgroup = cls if cls in ('positive', 'negative') else None + + if cls in ('positive', 'negative'): + # Stacked area: add opacity to avoid hiding layers, remove line border + if hasattr(trace, 'line') and trace.line.color: + trace.fillcolor = trace.line.color + trace.line.width = 0 + elif cls == 'mixed': + # Mixed variables: show as dashed line, not stacked + if hasattr(trace, 'line'): + trace.line.width = 2 + trace.line.dash = 'dash' + if hasattr(trace, 'fill'): + trace.fill = None + + # Update layout with basic styling (Plotly Express handles sizing automatically) fig.update_layout( - title=title, - yaxis=dict( - title=ylabel, - showgrid=True, # Enable grid lines on the y-axis - gridcolor='lightgrey', # Customize grid line color - gridwidth=0.5, # Customize grid line width - ), - xaxis=dict( - title=xlabel, - showgrid=True, # Enable grid lines on the x-axis - gridcolor='lightgrey', # Customize grid line color - gridwidth=0.5, # Customize grid line width - ), - plot_bgcolor='rgba(0,0,0,0)', # Transparent background - paper_bgcolor='rgba(0,0,0,0)', # Transparent paper background - font=dict(size=14), # Increase font size for better readability + plot_bgcolor='rgba(0,0,0,0)', + paper_bgcolor='rgba(0,0,0,0)', + font=dict(size=12), ) + # Update axes to share if requested (Plotly Express already handles this, but we can customize) + if not shared_yaxes: + fig.update_yaxes(matches=None) + if not shared_xaxes: + fig.update_xaxes(matches=None) + return fig @@ -562,213 +689,110 @@ def with_matplotlib( return fig, ax -def heat_map_matplotlib( - data: pd.DataFrame, - color_map: str = 'viridis', - title: str = '', - xlabel: str = 'Period', - ylabel: str = 'Step', - figsize: tuple[float, float] = (12, 6), -) -> tuple[plt.Figure, plt.Axes]: - """ - Plots a DataFrame as a heatmap using Matplotlib. The columns of the DataFrame will be displayed on the x-axis, - the index will be displayed on the y-axis, and the values will represent the 'heat' intensity in the plot. - - Args: - data: A DataFrame containing the data to be visualized. The index will be used for the y-axis, and columns will be used for the x-axis. - The values in the DataFrame will be represented as colors in the heatmap. - color_map: The colormap to use for the heatmap. Default is 'viridis'. Matplotlib supports various colormaps like 'plasma', 'inferno', 'cividis', etc. - title: The title of the plot. - xlabel: The label for the x-axis. - ylabel: The label for the y-axis. - figsize: The size of the figure to create. Default is (12, 6), which results in a width of 12 inches and a height of 6 inches. - - Returns: - A tuple containing the Matplotlib `Figure` and `Axes` objects. The `Figure` contains the overall plot, while the `Axes` is the area - where the heatmap is drawn. These can be used for further customization or saving the plot to a file. - - Notes: - - The y-axis is flipped so that the first row of the DataFrame is displayed at the top of the plot. - - The color scale is normalized based on the minimum and maximum values in the DataFrame. - - The x-axis labels (periods) are placed at the top of the plot. - - The colorbar is added horizontally at the bottom of the plot, with a label. - """ - - # Get the min and max values for color normalization - color_bar_min, color_bar_max = data.min().min(), data.max().max() - - # Create the heatmap plot - fig, ax = plt.subplots(figsize=figsize) - ax.pcolormesh(data.values, cmap=color_map, shading='auto') - ax.invert_yaxis() # Flip the y-axis to start at the top - - # Adjust ticks and labels for x and y axes - ax.set_xticks(np.arange(len(data.columns)) + 0.5) - ax.set_xticklabels(data.columns, ha='center') - ax.set_yticks(np.arange(len(data.index)) + 0.5) - ax.set_yticklabels(data.index, va='center') - - # Add labels to the axes - ax.set_xlabel(xlabel, ha='center') - ax.set_ylabel(ylabel, va='center') - ax.set_title(title) - - # Position x-axis labels at the top - ax.xaxis.set_label_position('top') - ax.xaxis.set_ticks_position('top') - - # Add the colorbar - sm1 = plt.cm.ScalarMappable(cmap=color_map, norm=plt.Normalize(vmin=color_bar_min, vmax=color_bar_max)) - sm1.set_array([]) - fig.colorbar(sm1, ax=ax, pad=0.12, aspect=15, fraction=0.2, orientation='horizontal') - - fig.tight_layout() - - return fig, ax - - -def heat_map_plotly( - data: pd.DataFrame, - color_map: str = 'viridis', - title: str = '', - xlabel: str = 'Period', - ylabel: str = 'Step', - categorical_labels: bool = True, -) -> go.Figure: - """ - Plots a DataFrame as a heatmap using Plotly. The columns of the DataFrame will be mapped to the x-axis, - and the index will be displayed on the y-axis. The values in the DataFrame will represent the 'heat' in the plot. - - Args: - data: A DataFrame with the data to be visualized. The index will be used for the y-axis, and columns will be used for the x-axis. - The values in the DataFrame will be represented as colors in the heatmap. - color_map: The color scale to use for the heatmap. Default is 'viridis'. Plotly supports various color scales like 'Cividis', 'Inferno', etc. - title: The title of the heatmap. Default is an empty string. - xlabel: The label for the x-axis. Default is 'Period'. - ylabel: The label for the y-axis. Default is 'Step'. - categorical_labels: If True, the x and y axes are treated as categorical data (i.e., the index and columns will not be interpreted as continuous data). - Default is True. If False, the axes are treated as continuous, which may be useful for time series or numeric data. - - Returns: - A Plotly figure object containing the heatmap. This can be further customized and saved - or displayed using `fig.show()`. - - Notes: - The color bar is automatically scaled to the minimum and maximum values in the data. - The y-axis is reversed to display the first row at the top. +def reshape_data_for_heatmap( + data: xr.DataArray, + reshape_time: tuple[Literal['YS', 'MS', 'W', 'D', 'h', '15min', 'min'], Literal['W', 'D', 'h', '15min', 'min']] + | Literal['auto'] + | None = 'auto', + facet_by: str | list[str] | None = None, + animate_by: str | None = None, + fill: Literal['ffill', 'bfill'] | None = 'ffill', +) -> xr.DataArray: """ + Reshape data for heatmap visualization, handling time dimension intelligently. - color_bar_min, color_bar_max = data.min().min(), data.max().max() # Min and max values for color scaling - # Define the figure - fig = go.Figure( - data=go.Heatmap( - z=data.values, - x=data.columns, - y=data.index, - colorscale=color_map, - zmin=color_bar_min, - zmax=color_bar_max, - colorbar=dict( - title=dict(text='Color Bar Label', side='right'), - orientation='h', - xref='container', - yref='container', - len=0.8, # Color bar length relative to plot - x=0.5, - y=0.1, - ), - ) - ) - - # Set axis labels and style - fig.update_layout( - title=title, - xaxis=dict(title=xlabel, side='top', type='category' if categorical_labels else None), - yaxis=dict(title=ylabel, autorange='reversed', type='category' if categorical_labels else None), - ) - - return fig - - -def reshape_to_2d(data_1d: np.ndarray, nr_of_steps_per_column: int) -> np.ndarray: - """ - Reshapes a 1D numpy array into a 2D array suitable for plotting as a colormap. + This function decides whether to reshape the 'time' dimension based on the reshape_time parameter: + - 'auto': Automatically reshapes if only 'time' dimension would remain for heatmap + - Tuple: Explicitly reshapes time with specified parameters + - None: No reshaping (returns data as-is) - The reshaped array will have the number of rows corresponding to the steps per column - (e.g., 24 hours per day) and columns representing time periods (e.g., days or months). + All non-time dimensions are preserved during reshaping. Args: - data_1d: A 1D numpy array with the data to reshape. - nr_of_steps_per_column: The number of steps (rows) per column in the resulting 2D array. For example, - this could be 24 (for hours) or 31 (for days in a month). + data: DataArray to reshape for heatmap visualization. + reshape_time: Reshaping configuration: + - 'auto' (default): Auto-reshape if needed based on facet_by/animate_by + - Tuple (timeframes, timesteps_per_frame): Explicit time reshaping + - None: No reshaping + facet_by: Dimension(s) used for faceting (used in 'auto' decision). + animate_by: Dimension used for animation (used in 'auto' decision). + fill: Method to fill missing values: 'ffill' or 'bfill'. Default is 'ffill'. Returns: - The reshaped 2D array. Each internal array corresponds to one column, with the specified number of steps. - Each column might represents a time period (e.g., day, month, etc.). - """ - - # Step 1: Ensure the input is a 1D array. - if data_1d.ndim != 1: - raise ValueError('Input must be a 1D array') - - # Step 2: Convert data to float type to allow NaN padding - if data_1d.dtype != np.float64: - data_1d = data_1d.astype(np.float64) + Reshaped DataArray. If time reshaping is applied, 'time' dimension is replaced + by 'timestep' and 'timeframe'. All other dimensions are preserved. - # Step 3: Calculate the number of columns required - total_steps = len(data_1d) - cols = len(data_1d) // nr_of_steps_per_column # Base number of columns - - # If there's a remainder, add an extra column to hold the remaining values - if total_steps % nr_of_steps_per_column != 0: - cols += 1 + Examples: + Auto-reshaping: - # Step 4: Pad the 1D data to match the required number of rows and columns - padded_data = np.pad( - data_1d, (0, cols * nr_of_steps_per_column - total_steps), mode='constant', constant_values=np.nan - ) + ```python + # Will auto-reshape because only 'time' remains after faceting/animation + data = reshape_data_for_heatmap(data, reshape_time='auto', facet_by='scenario', animate_by='period') + ``` - # Step 5: Reshape the padded data into a 2D array - data_2d = padded_data.reshape(cols, nr_of_steps_per_column) + Explicit reshaping: - return data_2d.T + ```python + # Explicitly reshape to daily pattern + data = reshape_data_for_heatmap(data, reshape_time=('D', 'h')) + ``` + No reshaping: -def heat_map_data_from_df( - df: pd.DataFrame, - periods: Literal['YS', 'MS', 'W', 'D', 'h', '15min', 'min'], - steps_per_period: Literal['W', 'D', 'h', '15min', 'min'], - fill: Literal['ffill', 'bfill'] | None = None, -) -> pd.DataFrame: + ```python + # Keep data as-is + data = reshape_data_for_heatmap(data, reshape_time=None) + ``` """ - Reshapes a DataFrame with a DateTime index into a 2D array for heatmap plotting, - based on a specified sample rate. - Only specific combinations of `periods` and `steps_per_period` are supported; invalid combinations raise an assertion. - - Args: - df: A DataFrame with a DateTime index containing the data to reshape. - periods: The time interval of each period (columns of the heatmap), - such as 'YS' (year start), 'W' (weekly), 'D' (daily), 'h' (hourly) etc. - steps_per_period: The time interval within each period (rows in the heatmap), - such as 'YS' (year start), 'W' (weekly), 'D' (daily), 'h' (hourly) etc. - fill: Method to fill missing values: 'ffill' for forward fill or 'bfill' for backward fill. + # If no time dimension, return data as-is + if 'time' not in data.dims: + return data + + # Handle None (disabled) - return data as-is + if reshape_time is None: + return data + + # Determine timeframes and timesteps_per_frame based on reshape_time parameter + if reshape_time == 'auto': + # Check if we need automatic time reshaping + facet_dims_used = [] + if facet_by: + facet_dims_used = [facet_by] if isinstance(facet_by, str) else list(facet_by) + if animate_by: + facet_dims_used.append(animate_by) + + # Get dimensions that would remain for heatmap + potential_heatmap_dims = [dim for dim in data.dims if dim not in facet_dims_used] + + # Auto-reshape if only 'time' dimension remains + if len(potential_heatmap_dims) == 1 and potential_heatmap_dims[0] == 'time': + logger.debug( + "Auto-applying time reshaping: Only 'time' dimension remains after faceting/animation. " + "Using default timeframes='D' and timesteps_per_frame='h'. " + "To customize, use reshape_time=('D', 'h') or disable with reshape_time=None." + ) + timeframes, timesteps_per_frame = 'D', 'h' + else: + # No reshaping needed + return data + elif isinstance(reshape_time, tuple): + # Explicit reshaping + timeframes, timesteps_per_frame = reshape_time + else: + raise ValueError(f"reshape_time must be 'auto', a tuple like ('D', 'h'), or None. Got: {reshape_time}") - Returns: - A DataFrame suitable for heatmap plotting, with rows representing steps within each period - and columns representing each period. - """ - assert pd.api.types.is_datetime64_any_dtype(df.index), ( - 'The index of the DataFrame must be datetime to transform it properly for a heatmap plot' - ) + # Validate that time is datetime + if not np.issubdtype(data.coords['time'].dtype, np.datetime64): + raise ValueError(f'Time dimension must be datetime-based, got {data.coords["time"].dtype}') - # Define formats for different combinations of `periods` and `steps_per_period` + # Define formats for different combinations formats = { ('YS', 'W'): ('%Y', '%W'), ('YS', 'D'): ('%Y', '%j'), # day of year ('YS', 'h'): ('%Y', '%j %H:00'), ('MS', 'D'): ('%Y-%m', '%d'), # day of month ('MS', 'h'): ('%Y-%m', '%d %H:00'), - ('W', 'D'): ('%Y-w%W', '%w_%A'), # week and day of week (with prefix for proper sorting) + ('W', 'D'): ('%Y-w%W', '%w_%A'), # week and day of week ('W', 'h'): ('%Y-w%W', '%w_%A %H:00'), ('D', 'h'): ('%Y-%m-%d', '%H:00'), # Day and hour ('D', '15min'): ('%Y-%m-%d', '%H:%M'), # Day and minute @@ -776,43 +800,64 @@ def heat_map_data_from_df( ('h', 'min'): ('%Y-%m-%d %H:00', '%M'), # minute of hour } - if df.empty: - raise ValueError('DataFrame is empty.') - diffs = df.index.to_series().diff().dropna() - minimum_time_diff_in_min = diffs.min().total_seconds() / 60 - time_intervals = {'min': 1, '15min': 15, 'h': 60, 'D': 24 * 60, 'W': 7 * 24 * 60} - if time_intervals[steps_per_period] > minimum_time_diff_in_min: - logger.error( - f'To compute the heatmap, the data was aggregated from {minimum_time_diff_in_min:.2f} min to ' - f'{time_intervals[steps_per_period]:.2f} min. Mean values are displayed.' - ) - - # Select the format based on the `periods` and `steps_per_period` combination - format_pair = (periods, steps_per_period) + format_pair = (timeframes, timesteps_per_frame) if format_pair not in formats: raise ValueError(f'{format_pair} is not a valid format. Choose from {list(formats.keys())}') period_format, step_format = formats[format_pair] - df = df.sort_index() # Ensure DataFrame is sorted by time index + # Check if resampling is needed + if data.sizes['time'] > 1: + # Use NumPy for more efficient timedelta computation + time_values = data.coords['time'].values # Already numpy datetime64[ns] + # Calculate differences and convert to minutes + time_diffs = np.diff(time_values).astype('timedelta64[s]').astype(float) / 60.0 + if time_diffs.size > 0: + min_time_diff_min = np.nanmin(time_diffs) + time_intervals = {'min': 1, '15min': 15, 'h': 60, 'D': 24 * 60, 'W': 7 * 24 * 60} + if time_intervals[timesteps_per_frame] > min_time_diff_min: + logger.warning( + f'Resampling data from {min_time_diff_min:.2f} min to ' + f'{time_intervals[timesteps_per_frame]:.2f} min. Mean values are displayed.' + ) - resampled_data = df.resample(steps_per_period).mean() # Resample and fill any gaps with NaN + # Resample along time dimension + resampled = data.resample(time=timesteps_per_frame).mean() - if fill == 'ffill': # Apply fill method if specified - resampled_data = resampled_data.ffill() + # Apply fill if specified + if fill == 'ffill': + resampled = resampled.ffill(dim='time') elif fill == 'bfill': - resampled_data = resampled_data.bfill() + resampled = resampled.bfill(dim='time') + + # Create period and step labels + time_values = pd.to_datetime(resampled.coords['time'].values) + period_labels = time_values.strftime(period_format) + step_labels = time_values.strftime(step_format) + + # Handle special case for weekly day format + if '%w_%A' in step_format: + step_labels = pd.Series(step_labels).replace('0_Sunday', '7_Sunday').values + + # Add period and step as coordinates + resampled = resampled.assign_coords( + { + 'timeframe': ('time', period_labels), + 'timestep': ('time', step_labels), + } + ) - resampled_data['period'] = resampled_data.index.strftime(period_format) - resampled_data['step'] = resampled_data.index.strftime(step_format) - if '%w_%A' in step_format: # Shift index of strings to ensure proper sorting - resampled_data['step'] = resampled_data['step'].apply( - lambda x: x.replace('0_Sunday', '7_Sunday') if '0_Sunday' in x else x - ) + # Convert to multi-index and unstack + resampled = resampled.set_index(time=['timeframe', 'timestep']) + result = resampled.unstack('time') - # Pivot the table so periods are columns and steps are indices - df_pivoted = resampled_data.pivot(columns='period', index='step', values=df.columns[0]) + # Ensure timestep and timeframe come first in dimension order + # Get other dimensions + other_dims = [d for d in result.dims if d not in ['timestep', 'timeframe']] - return df_pivoted + # Reorder: timestep, timeframe, then other dimensions + result = result.transpose('timestep', 'timeframe', *other_dims) + + return result def plot_network( @@ -1413,6 +1458,311 @@ def preprocess_series(series: pd.Series): return fig, axes +def heatmap_with_plotly( + data: xr.DataArray, + colors: ColorType = 'viridis', + title: str = '', + facet_by: str | list[str] | None = None, + animate_by: str | None = None, + facet_cols: int = 3, + reshape_time: tuple[Literal['YS', 'MS', 'W', 'D', 'h', '15min', 'min'], Literal['W', 'D', 'h', '15min', 'min']] + | Literal['auto'] + | None = 'auto', + fill: Literal['ffill', 'bfill'] | None = 'ffill', +) -> go.Figure: + """ + Plot a heatmap visualization using Plotly's imshow with faceting and animation support. + + This function creates heatmap visualizations from xarray DataArrays, supporting + multi-dimensional data through faceting (subplots) and animation. It automatically + handles dimension reduction and data reshaping for optimal heatmap display. + + Automatic Time Reshaping: + If only the 'time' dimension remains after faceting/animation (making the data 1D), + the function automatically reshapes time into a 2D format using default values + (timeframes='D', timesteps_per_frame='h'). This creates a daily pattern heatmap + showing hours vs days. + + Args: + data: An xarray DataArray containing the data to visualize. Should have at least + 2 dimensions, or a 'time' dimension that can be reshaped into 2D. + colors: Color specification (colormap name, list, or dict). Common options: + 'viridis', 'plasma', 'RdBu', 'portland'. + title: The main title of the heatmap. + facet_by: Dimension to create facets for. Creates a subplot grid. + Can be a single dimension name or list (only first dimension used). + Note: px.imshow only supports single-dimension faceting. + If the dimension doesn't exist in the data, it will be silently ignored. + animate_by: Dimension to animate over. Creates animation frames. + If the dimension doesn't exist in the data, it will be silently ignored. + facet_cols: Number of columns in the facet grid (used with facet_by). + reshape_time: Time reshaping configuration: + - 'auto' (default): Automatically applies ('D', 'h') if only 'time' dimension remains + - Tuple like ('D', 'h'): Explicit time reshaping (days vs hours) + - None: Disable time reshaping (will error if only 1D time data) + fill: Method to fill missing values when reshaping time: 'ffill' or 'bfill'. Default is 'ffill'. + + Returns: + A Plotly figure object containing the heatmap visualization. + + Examples: + Simple heatmap: + + ```python + fig = heatmap_with_plotly(data_array, colors='RdBu', title='Temperature Map') + ``` + + Facet by scenario: + + ```python + fig = heatmap_with_plotly(data_array, facet_by='scenario', facet_cols=2) + ``` + + Animate by period: + + ```python + fig = heatmap_with_plotly(data_array, animate_by='period') + ``` + + Automatic time reshaping (when only time dimension remains): + + ```python + # Data with dims ['time', 'scenario', 'period'] + # After faceting and animation, only 'time' remains -> auto-reshapes to (timestep, timeframe) + fig = heatmap_with_plotly(data_array, facet_by='scenario', animate_by='period') + ``` + + Explicit time reshaping: + + ```python + fig = heatmap_with_plotly(data_array, facet_by='scenario', animate_by='period', reshape_time=('W', 'D')) + ``` + """ + # Handle empty data + if data.size == 0: + return go.Figure() + + # Apply time reshaping using the new unified function + data = reshape_data_for_heatmap( + data, reshape_time=reshape_time, facet_by=facet_by, animate_by=animate_by, fill=fill + ) + + # Get available dimensions + available_dims = list(data.dims) + + # Validate and filter facet_by dimensions + if facet_by is not None: + if isinstance(facet_by, str): + if facet_by not in available_dims: + logger.debug( + f"Dimension '{facet_by}' not found in data. Available dimensions: {available_dims}. " + f'Ignoring facet_by parameter.' + ) + facet_by = None + elif isinstance(facet_by, list): + missing_dims = [dim for dim in facet_by if dim not in available_dims] + facet_by = [dim for dim in facet_by if dim in available_dims] + if missing_dims: + logger.debug( + f'Dimensions {missing_dims} not found in data. Available dimensions: {available_dims}. ' + f'Using only existing dimensions: {facet_by if facet_by else "none"}.' + ) + if len(facet_by) == 0: + facet_by = None + + # Validate animate_by dimension + if animate_by is not None and animate_by not in available_dims: + logger.debug( + f"Dimension '{animate_by}' not found in data. Available dimensions: {available_dims}. " + f'Ignoring animate_by parameter.' + ) + animate_by = None + + # Determine which dimensions are used for faceting/animation + facet_dims = [] + if facet_by: + facet_dims = [facet_by] if isinstance(facet_by, str) else facet_by + if animate_by: + facet_dims.append(animate_by) + + # Get remaining dimensions for the heatmap itself + heatmap_dims = [dim for dim in available_dims if dim not in facet_dims] + + if len(heatmap_dims) < 2: + # Need at least 2 dimensions for a heatmap + logger.error( + f'Heatmap requires at least 2 dimensions for rows and columns. ' + f'After faceting/animation, only {len(heatmap_dims)} dimension(s) remain: {heatmap_dims}' + ) + return go.Figure() + + # Setup faceting parameters for Plotly Express + # Note: px.imshow only supports facet_col, not facet_row + facet_col_param = None + if facet_by: + if isinstance(facet_by, str): + facet_col_param = facet_by + elif len(facet_by) == 1: + facet_col_param = facet_by[0] + elif len(facet_by) >= 2: + # px.imshow doesn't support facet_row, so we can only facet by one dimension + # Use the first dimension and warn about the rest + facet_col_param = facet_by[0] + logger.warning( + f'px.imshow only supports faceting by a single dimension. ' + f'Using {facet_by[0]} for faceting. Dimensions {facet_by[1:]} will be ignored. ' + f'Consider using animate_by for additional dimensions.' + ) + + # Create the imshow plot - px.imshow can work directly with xarray DataArrays + common_args = { + 'img': data, + 'color_continuous_scale': colors if isinstance(colors, str) else 'viridis', + 'title': title, + } + + # Add faceting if specified + if facet_col_param: + common_args['facet_col'] = facet_col_param + if facet_cols: + common_args['facet_col_wrap'] = facet_cols + + # Add animation if specified + if animate_by: + common_args['animation_frame'] = animate_by + + try: + fig = px.imshow(**common_args) + except Exception as e: + logger.error(f'Error creating imshow plot: {e}. Falling back to basic heatmap.') + # Fallback: create a simple heatmap without faceting + fig = px.imshow( + data.values, + color_continuous_scale=colors if isinstance(colors, str) else 'viridis', + title=title, + ) + + # Update layout with basic styling + fig.update_layout( + plot_bgcolor='rgba(0,0,0,0)', + paper_bgcolor='rgba(0,0,0,0)', + font=dict(size=12), + ) + + return fig + + +def heatmap_with_matplotlib( + data: xr.DataArray, + colors: ColorType = 'viridis', + title: str = '', + figsize: tuple[float, float] = (12, 6), + fig: plt.Figure | None = None, + ax: plt.Axes | None = None, + reshape_time: tuple[Literal['YS', 'MS', 'W', 'D', 'h', '15min', 'min'], Literal['W', 'D', 'h', '15min', 'min']] + | Literal['auto'] + | None = 'auto', + fill: Literal['ffill', 'bfill'] | None = 'ffill', +) -> tuple[plt.Figure, plt.Axes]: + """ + Plot a heatmap visualization using Matplotlib's imshow. + + This function creates a basic 2D heatmap from an xarray DataArray using matplotlib's + imshow function. For multi-dimensional data, only the first two dimensions are used. + + Args: + data: An xarray DataArray containing the data to visualize. Should have at least + 2 dimensions. If more than 2 dimensions exist, additional dimensions will + be reduced by taking the first slice. + colors: Color specification. Should be a colormap name (e.g., 'viridis', 'RdBu'). + title: The title of the heatmap. + figsize: The size of the figure (width, height) in inches. + fig: A Matplotlib figure object to plot on. If not provided, a new figure will be created. + ax: A Matplotlib axes object to plot on. If not provided, a new axes will be created. + reshape_time: Time reshaping configuration: + - 'auto' (default): Automatically applies ('D', 'h') if only 'time' dimension + - Tuple like ('D', 'h'): Explicit time reshaping (days vs hours) + - None: Disable time reshaping + fill: Method to fill missing values when reshaping time: 'ffill' or 'bfill'. Default is 'ffill'. + + Returns: + A tuple containing the Matplotlib figure and axes objects used for the plot. + + Notes: + - Matplotlib backend doesn't support faceting or animation. Use plotly engine for those features. + - The y-axis is automatically inverted to display data with origin at top-left. + - A colorbar is added to show the value scale. + + Examples: + ```python + fig, ax = heatmap_with_matplotlib(data_array, colors='RdBu', title='Temperature') + plt.savefig('heatmap.png') + ``` + + Time reshaping: + + ```python + fig, ax = heatmap_with_matplotlib(data_array, reshape_time=('D', 'h')) + ``` + """ + # Handle empty data + if data.size == 0: + if fig is None or ax is None: + fig, ax = plt.subplots(figsize=figsize) + return fig, ax + + # Apply time reshaping using the new unified function + # Matplotlib doesn't support faceting/animation, so we pass None for those + data = reshape_data_for_heatmap(data, reshape_time=reshape_time, facet_by=None, animate_by=None, fill=fill) + + # Create figure and axes if not provided + if fig is None or ax is None: + fig, ax = plt.subplots(figsize=figsize) + + # Extract data values + # If data has more than 2 dimensions, we need to reduce it + if isinstance(data, xr.DataArray): + # Get the first 2 dimensions + dims = list(data.dims) + if len(dims) > 2: + logger.warning( + f'Data has {len(dims)} dimensions: {dims}. ' + f'Only the first 2 will be used for the heatmap. ' + f'Use the plotly engine for faceting/animation support.' + ) + # Select only the first 2 dimensions by taking first slice of others + selection = {dim: 0 for dim in dims[2:]} + data = data.isel(selection) + + values = data.values + x_labels = data.dims[1] if len(data.dims) > 1 else 'x' + y_labels = data.dims[0] if len(data.dims) > 0 else 'y' + else: + values = data + x_labels = 'x' + y_labels = 'y' + + # Process colormap + cmap = colors if isinstance(colors, str) else 'viridis' + + # Create the heatmap using imshow + im = ax.imshow(values, cmap=cmap, aspect='auto', origin='upper') + + # Add colorbar + cbar = plt.colorbar(im, ax=ax, orientation='horizontal', pad=0.1, aspect=15, fraction=0.05) + cbar.set_label('Value') + + # Set labels and title + ax.set_xlabel(str(x_labels).capitalize()) + ax.set_ylabel(str(y_labels).capitalize()) + ax.set_title(title) + + # Apply tight layout + fig.tight_layout() + + return fig, ax + + def export_figure( figure_like: go.Figure | tuple[plt.Figure, plt.Axes], default_path: pathlib.Path, diff --git a/flixopt/results.py b/flixopt/results.py index b55d48744..a58f0dc1e 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -10,7 +10,6 @@ import linopy import numpy as np import pandas as pd -import plotly import xarray as xr import yaml @@ -20,6 +19,7 @@ if TYPE_CHECKING: import matplotlib.pyplot as plt + import plotly import pyvis from .calculation import Calculation, SegmentedCalculation @@ -195,8 +195,8 @@ def __init__( if 'flow_system' in kwargs and flow_system_data is None: flow_system_data = kwargs.pop('flow_system') warnings.warn( - "The 'flow_system' parameter is deprecated. Use 'flow_system_data' instead." - "Acess is now by '.flow_system_data', while '.flow_system' returns the restored FlowSystem.", + "The 'flow_system' parameter is deprecated. Use 'flow_system_data' instead. " + "Access is now via '.flow_system_data', while '.flow_system' returns the restored FlowSystem.", DeprecationWarning, stacklevel=2, ) @@ -687,68 +687,117 @@ def _create_effects_dataset(self, mode: Literal['temporal', 'periodic', 'total'] def plot_heatmap( self, - variable_name: str, - heatmap_timeframes: Literal['YS', 'MS', 'W', 'D', 'h', '15min', 'min'] = 'D', - heatmap_timesteps_per_frame: Literal['W', 'D', 'h', '15min', 'min'] = 'h', - color_map: str = 'portland', + variable_name: str | list[str], save: bool | pathlib.Path = False, show: bool = True, + colors: plotting.ColorType = 'viridis', engine: plotting.PlottingEngine = 'plotly', + select: dict[FlowSystemDimensions, Any] | None = None, + facet_by: str | list[str] | None = 'scenario', + animate_by: str | None = 'period', + facet_cols: int = 3, + reshape_time: tuple[Literal['YS', 'MS', 'W', 'D', 'h', '15min', 'min'], Literal['W', 'D', 'h', '15min', 'min']] + | Literal['auto'] + | None = 'auto', + fill: Literal['ffill', 'bfill'] | None = 'ffill', + # Deprecated parameters (kept for backwards compatibility) indexer: dict[FlowSystemDimensions, Any] | None = None, + heatmap_timeframes: Literal['YS', 'MS', 'W', 'D', 'h', '15min', 'min'] | None = None, + heatmap_timesteps_per_frame: Literal['W', 'D', 'h', '15min', 'min'] | None = None, + color_map: str | None = None, ) -> plotly.graph_objs.Figure | tuple[plt.Figure, plt.Axes]: """ - Plots a heatmap of the solution of a variable. + Plots a heatmap visualization of a variable using imshow or time-based reshaping. + + Supports multiple visualization features that can be combined: + - **Multi-variable**: Plot multiple variables on a single heatmap (creates 'variable' dimension) + - **Time reshaping**: Converts 'time' dimension into 2D (e.g., hours vs days) + - **Faceting**: Creates subplots for different dimension values + - **Animation**: Animates through dimension values (Plotly only) Args: - variable_name: The name of the variable to plot. - heatmap_timeframes: The timeframes to use for the heatmap. - heatmap_timesteps_per_frame: The timesteps per frame to use for the heatmap. - color_map: The color map to use for the heatmap. + variable_name: The name of the variable to plot, or a list of variable names. + When a list is provided, variables are combined into a single DataArray + with a new 'variable' dimension. save: Whether to save the plot or not. If a path is provided, the plot will be saved at that location. show: Whether to show the plot or not. + colors: Color scheme for the heatmap. See `flixopt.plotting.ColorType` for options. engine: The engine to use for plotting. Can be either 'plotly' or 'matplotlib'. - indexer: Optional selection dict, e.g., {'scenario': 'base', 'period': 2024}. - If None, uses first value for each dimension. - If empty dict {}, uses all values. + select: Optional data selection dict. Supports single values, lists, slices, and index arrays. + Applied BEFORE faceting/animation/reshaping. + facet_by: Dimension(s) to create facets (subplots) for. Can be a single dimension name (str) + or list of dimensions. Each unique value combination creates a subplot. Ignored if not found. + animate_by: Dimension to animate over (Plotly only). Creates animation frames that cycle through + dimension values. Only one dimension can be animated. Ignored if not found. + facet_cols: Number of columns in the facet grid layout (default: 3). + reshape_time: Time reshaping configuration (default: 'auto'): + - 'auto': Automatically applies ('D', 'h') when only 'time' dimension remains + - Tuple: Explicit reshaping, e.g. ('D', 'h') for days vs hours, + ('MS', 'D') for months vs days, ('W', 'h') for weeks vs hours + - None: Disable auto-reshaping (will error if only 1D time data) + Supported timeframes: 'YS', 'MS', 'W', 'D', 'h', '15min', 'min' + fill: Method to fill missing values after reshape: 'ffill' (forward fill) or 'bfill' (backward fill). + Default is 'ffill'. Examples: - Basic usage (uses first scenario, first period, all time): + Direct imshow mode (default): + + >>> results.plot_heatmap('Battery|charge_state', select={'scenario': 'base'}) + + Facet by scenario: + + >>> results.plot_heatmap('Boiler(Qth)|flow_rate', facet_by='scenario', facet_cols=2) - >>> results.plot_heatmap('Battery|charge_state') + Animate by period: - Select specific scenario and period: + >>> results.plot_heatmap('Boiler(Qth)|flow_rate', select={'scenario': 'base'}, animate_by='period') - >>> results.plot_heatmap('Boiler(Qth)|flow_rate', indexer={'scenario': 'base', 'period': 2024}) + Time reshape mode - daily patterns: - Time filtering (summer months only): + >>> results.plot_heatmap('Boiler(Qth)|flow_rate', select={'scenario': 'base'}, reshape_time=('D', 'h')) + + Combined: time reshaping with faceting and animation: >>> results.plot_heatmap( - ... 'Boiler(Qth)|flow_rate', - ... indexer={ - ... 'scenario': 'base', - ... 'time': results.solution.time[results.solution.time.dt.month.isin([6, 7, 8])], - ... }, + ... 'Boiler(Qth)|flow_rate', facet_by='scenario', animate_by='period', reshape_time=('D', 'h') ... ) - Save to specific location: + Multi-variable heatmap (variables as one axis): >>> results.plot_heatmap( - ... 'Boiler(Qth)|flow_rate', indexer={'scenario': 'base'}, save='path/to/my_heatmap.html' + ... ['Boiler(Q_th)|flow_rate', 'CHP(Q_th)|flow_rate', 'HeatStorage|charge_state'], + ... select={'scenario': 'base', 'period': 1}, + ... reshape_time=None, ... ) - """ - dataarray = self.solution[variable_name] + Multi-variable with time reshaping: + + >>> results.plot_heatmap( + ... ['Boiler(Q_th)|flow_rate', 'CHP(Q_th)|flow_rate'], + ... facet_by='scenario', + ... animate_by='period', + ... reshape_time=('D', 'h'), + ... ) + """ + # Delegate to module-level plot_heatmap function return plot_heatmap( - dataarray=dataarray, - name=variable_name, + data=self.solution[variable_name], + name=variable_name if isinstance(variable_name, str) else None, folder=self.folder, - heatmap_timeframes=heatmap_timeframes, - heatmap_timesteps_per_frame=heatmap_timesteps_per_frame, - color_map=color_map, + colors=colors, save=save, show=show, engine=engine, + select=select, + facet_by=facet_by, + animate_by=animate_by, + facet_cols=facet_cols, + reshape_time=reshape_time, + fill=fill, indexer=indexer, + heatmap_timeframes=heatmap_timeframes, + heatmap_timesteps_per_frame=heatmap_timesteps_per_frame, + color_map=color_map, ) def plot_network( @@ -920,30 +969,107 @@ def plot_node_balance( show: bool = True, colors: plotting.ColorType = 'viridis', engine: plotting.PlottingEngine = 'plotly', - indexer: dict[FlowSystemDimensions, Any] | None = None, + select: dict[FlowSystemDimensions, Any] | None = None, unit_type: Literal['flow_rate', 'flow_hours'] = 'flow_rate', mode: Literal['area', 'stacked_bar', 'line'] = 'stacked_bar', drop_suffix: bool = True, + facet_by: str | list[str] | None = 'scenario', + animate_by: str | None = 'period', + facet_cols: int = 3, + # Deprecated parameter (kept for backwards compatibility) + indexer: dict[FlowSystemDimensions, Any] | None = None, ) -> plotly.graph_objs.Figure | tuple[plt.Figure, plt.Axes]: """ - Plots the node balance of the Component or Bus. + Plots the node balance of the Component or Bus with optional faceting and animation. + Args: save: Whether to save the plot or not. If a path is provided, the plot will be saved at that location. show: Whether to show the plot or not. colors: The colors to use for the plot. See `flixopt.plotting.ColorType` for options. engine: The engine to use for plotting. Can be either 'plotly' or 'matplotlib'. - indexer: Optional selection dict, e.g., {'scenario': 'base', 'period': 2024}. - If None, uses first value for each dimension (except time). - If empty dict {}, uses all values. + select: Optional data selection dict. Supports: + - Single values: {'scenario': 'base', 'period': 2024} + - Multiple values: {'scenario': ['base', 'high', 'renewable']} + - Slices: {'time': slice('2024-01', '2024-06')} + - Index arrays: {'time': time_array} + Note: Applied BEFORE faceting/animation. unit_type: The unit type to use for the dataset. Can be 'flow_rate' or 'flow_hours'. - 'flow_rate': Returns the flow_rates of the Node. - 'flow_hours': Returns the flow_hours of the Node. [flow_hours(t) = flow_rate(t) * dt(t)]. Renames suffixes to |flow_hours. mode: The plotting mode. Use 'stacked_bar' for stacked bar charts, 'line' for stepped lines, or 'area' for stacked area charts. drop_suffix: Whether to drop the suffix from the variable names. + facet_by: Dimension(s) to create facets (subplots) for. Can be a single dimension name (str) + or list of dimensions. Each unique value combination creates a subplot. Ignored if not found. + Example: 'scenario' creates one subplot per scenario. + Example: ['scenario', 'period'] creates a grid of subplots for each scenario-period combination. + animate_by: Dimension to animate over (Plotly only). Creates animation frames that cycle through + dimension values. Only one dimension can be animated. Ignored if not found. + facet_cols: Number of columns in the facet grid layout (default: 3). + + Examples: + Basic plot (current behavior): + + >>> results['Boiler'].plot_node_balance() + + Facet by scenario: + + >>> results['Boiler'].plot_node_balance(facet_by='scenario', facet_cols=2) + + Animate by period: + + >>> results['Boiler'].plot_node_balance(animate_by='period') + + Facet by scenario AND animate by period: + + >>> results['Boiler'].plot_node_balance(facet_by='scenario', animate_by='period') + + Select single scenario, then facet by period: + + >>> results['Boiler'].plot_node_balance(select={'scenario': 'base'}, facet_by='period') + + Select multiple scenarios and facet by them: + + >>> results['Boiler'].plot_node_balance( + ... select={'scenario': ['base', 'high', 'renewable']}, facet_by='scenario' + ... ) + + Time range selection (summer months only): + + >>> results['Boiler'].plot_node_balance(select={'time': slice('2024-06', '2024-08')}, facet_by='scenario') """ - ds = self.node_balance(with_last_timestep=True, unit_type=unit_type, drop_suffix=drop_suffix, indexer=indexer) + # Handle deprecated indexer parameter + if indexer is not None: + # Check for conflict with new parameter + if select is not None: + raise ValueError( + "Cannot use both deprecated parameter 'indexer' and new parameter 'select'. Use only 'select'." + ) + + import warnings + + warnings.warn( + "The 'indexer' parameter is deprecated and will be removed in a future version. Use 'select' instead.", + DeprecationWarning, + stacklevel=2, + ) + select = indexer - ds, suffix_parts = _apply_indexer_to_data(ds, indexer, drop=True) + if engine not in {'plotly', 'matplotlib'}: + raise ValueError(f'Engine "{engine}" not supported. Use one of ["plotly", "matplotlib"]') + + # Don't pass select/indexer to node_balance - we'll apply it afterwards + ds = self.node_balance(with_last_timestep=True, unit_type=unit_type, drop_suffix=drop_suffix) + + ds, suffix_parts = _apply_selection_to_data(ds, select=select, drop=True) + + # Matplotlib requires only 'time' dimension; check for extras after selection + if engine == 'matplotlib': + extra_dims = [d for d in ds.dims if d != 'time'] + if extra_dims: + raise ValueError( + f'Matplotlib engine only supports a single time axis, but found extra dimensions: {extra_dims}. ' + f'Please use select={{...}} to reduce dimensions or switch to engine="plotly" for faceting/animation.' + ) suffix = '--' + '-'.join(suffix_parts) if suffix_parts else '' title = ( @@ -952,13 +1078,16 @@ def plot_node_balance( if engine == 'plotly': figure_like = plotting.with_plotly( - ds.to_dataframe(), + ds, + facet_by=facet_by, + animate_by=animate_by, colors=colors, mode=mode, title=title, + facet_cols=facet_cols, ) default_filetype = '.html' - elif engine == 'matplotlib': + else: figure_like = plotting.with_matplotlib( ds.to_dataframe(), colors=colors, @@ -966,8 +1095,6 @@ def plot_node_balance( title=title, ) default_filetype = '.png' - else: - raise ValueError(f'Engine "{engine}" not supported. Use "plotly" or "matplotlib"') return plotting.export_figure( figure_like=figure_like, @@ -986,9 +1113,19 @@ def plot_node_balance_pie( save: bool | pathlib.Path = False, show: bool = True, engine: plotting.PlottingEngine = 'plotly', + select: dict[FlowSystemDimensions, Any] | None = None, + # Deprecated parameter (kept for backwards compatibility) indexer: dict[FlowSystemDimensions, Any] | None = None, ) -> plotly.graph_objs.Figure | tuple[plt.Figure, list[plt.Axes]]: """Plot pie chart of flow hours distribution. + + Note: + Pie charts require scalar data (no extra dimensions beyond time). + If your data has dimensions like 'scenario' or 'period', either: + + - Use `select` to choose specific values: `select={'scenario': 'base', 'period': 2024}` + - Let auto-selection choose the first value (a warning will be logged) + Args: lower_percentage_group: Percentage threshold for "Others" grouping. colors: Color scheme. Also see plotly. @@ -996,10 +1133,35 @@ def plot_node_balance_pie( save: Whether to save plot. show: Whether to display plot. engine: Plotting engine ('plotly' or 'matplotlib'). - indexer: Optional selection dict, e.g., {'scenario': 'base', 'period': 2024}. - If None, uses first value for each dimension. - If empty dict {}, uses all values. + select: Optional data selection dict. Supports single values, lists, slices, and index arrays. + Use this to select specific scenario/period before creating the pie chart. + + Examples: + Basic usage (auto-selects first scenario/period if present): + + >>> results['Bus'].plot_node_balance_pie() + + Explicitly select a scenario and period: + + >>> results['Bus'].plot_node_balance_pie(select={'scenario': 'high_demand', 'period': 2030}) """ + # Handle deprecated indexer parameter + if indexer is not None: + # Check for conflict with new parameter + if select is not None: + raise ValueError( + "Cannot use both deprecated parameter 'indexer' and new parameter 'select'. Use only 'select'." + ) + + import warnings + + warnings.warn( + "The 'indexer' parameter is deprecated and will be removed in a future version. Use 'select' instead.", + DeprecationWarning, + stacklevel=2, + ) + select = indexer + inputs = sanitize_dataset( ds=self.solution[self.inputs] * self._calculation_results.hours_per_timestep, threshold=1e-5, @@ -1015,15 +1177,46 @@ def plot_node_balance_pie( drop_suffix='|', ) - inputs, suffix_parts = _apply_indexer_to_data(inputs, indexer, drop=True) - outputs, suffix_parts = _apply_indexer_to_data(outputs, indexer, drop=True) - suffix = '--' + '-'.join(suffix_parts) if suffix_parts else '' - - title = f'{self.label} (total flow hours){suffix}' + inputs, suffix_parts = _apply_selection_to_data(inputs, select=select, drop=True) + outputs, suffix_parts = _apply_selection_to_data(outputs, select=select, drop=True) + # Sum over time dimension inputs = inputs.sum('time') outputs = outputs.sum('time') + # Auto-select first value for any remaining dimensions (scenario, period, etc.) + # Pie charts need scalar data, so we automatically reduce extra dimensions + extra_dims_inputs = [dim for dim in inputs.dims if dim != 'time'] + extra_dims_outputs = [dim for dim in outputs.dims if dim != 'time'] + extra_dims = list(set(extra_dims_inputs + extra_dims_outputs)) + + if extra_dims: + auto_select = {} + for dim in extra_dims: + # Get first value of this dimension + if dim in inputs.coords: + first_val = inputs.coords[dim].values[0] + elif dim in outputs.coords: + first_val = outputs.coords[dim].values[0] + else: + continue + auto_select[dim] = first_val + logger.info( + f'Pie chart auto-selected {dim}={first_val} (first value). ' + f'Use select={{"{dim}": value}} to choose a different value.' + ) + + # Apply auto-selection + inputs = inputs.sel(auto_select) + outputs = outputs.sel(auto_select) + + # Update suffix with auto-selected values + auto_suffix_parts = [f'{dim}={val}' for dim, val in auto_select.items()] + suffix_parts.extend(auto_suffix_parts) + + suffix = '--' + '-'.join(suffix_parts) if suffix_parts else '' + title = f'{self.label} (total flow hours){suffix}' + if engine == 'plotly': figure_like = plotting.dual_pie_with_plotly( data_left=inputs.to_pandas(), @@ -1068,6 +1261,8 @@ def node_balance( with_last_timestep: bool = False, unit_type: Literal['flow_rate', 'flow_hours'] = 'flow_rate', drop_suffix: bool = False, + select: dict[FlowSystemDimensions, Any] | None = None, + # Deprecated parameter (kept for backwards compatibility) indexer: dict[FlowSystemDimensions, Any] | None = None, ) -> xr.Dataset: """ @@ -1081,10 +1276,25 @@ def node_balance( - 'flow_rate': Returns the flow_rates of the Node. - 'flow_hours': Returns the flow_hours of the Node. [flow_hours(t) = flow_rate(t) * dt(t)]. Renames suffixes to |flow_hours. drop_suffix: Whether to drop the suffix from the variable names. - indexer: Optional selection dict, e.g., {'scenario': 'base', 'period': 2024}. - If None, uses first value for each dimension. - If empty dict {}, uses all values. + select: Optional data selection dict. Supports single values, lists, slices, and index arrays. """ + # Handle deprecated indexer parameter + if indexer is not None: + # Check for conflict with new parameter + if select is not None: + raise ValueError( + "Cannot use both deprecated parameter 'indexer' and new parameter 'select'. Use only 'select'." + ) + + import warnings + + warnings.warn( + "The 'indexer' parameter is deprecated and will be removed in a future version. Use 'select' instead.", + DeprecationWarning, + stacklevel=2, + ) + select = indexer + ds = self.solution[self.inputs + self.outputs] ds = sanitize_dataset( @@ -1103,7 +1313,7 @@ def node_balance( drop_suffix='|' if drop_suffix else None, ) - ds, _ = _apply_indexer_to_data(ds, indexer, drop=True) + ds, _ = _apply_selection_to_data(ds, select=select, drop=True) if unit_type == 'flow_hours': ds = ds * self._calculation_results.hours_per_timestep @@ -1140,10 +1350,15 @@ def plot_charge_state( show: bool = True, colors: plotting.ColorType = 'viridis', engine: plotting.PlottingEngine = 'plotly', - mode: Literal['area', 'stacked_bar', 'line'] = 'stacked_bar', + mode: Literal['area', 'stacked_bar', 'line'] = 'area', + select: dict[FlowSystemDimensions, Any] | None = None, + facet_by: str | list[str] | None = 'scenario', + animate_by: str | None = 'period', + facet_cols: int = 3, + # Deprecated parameter (kept for backwards compatibility) indexer: dict[FlowSystemDimensions, Any] | None = None, ) -> plotly.graph_objs.Figure: - """Plot storage charge state over time, combined with the node balance. + """Plot storage charge state over time, combined with the node balance with optional faceting and animation. Args: save: Whether to save the plot or not. If a path is provided, the plot will be saved at that location. @@ -1151,42 +1366,120 @@ def plot_charge_state( colors: Color scheme. Also see plotly. engine: Plotting engine to use. Only 'plotly' is implemented atm. mode: The plotting mode. Use 'stacked_bar' for stacked bar charts, 'line' for stepped lines, or 'area' for stacked area charts. - indexer: Optional selection dict, e.g., {'scenario': 'base', 'period': 2024}. - If None, uses first value for each dimension. - If empty dict {}, uses all values. + select: Optional data selection dict. Supports single values, lists, slices, and index arrays. + Applied BEFORE faceting/animation. + facet_by: Dimension(s) to create facets (subplots) for. Can be a single dimension name (str) + or list of dimensions. Each unique value combination creates a subplot. Ignored if not found. + animate_by: Dimension to animate over (Plotly only). Creates animation frames that cycle through + dimension values. Only one dimension can be animated. Ignored if not found. + facet_cols: Number of columns in the facet grid layout (default: 3). Raises: ValueError: If component is not a storage. + + Examples: + Basic plot: + + >>> results['Storage'].plot_charge_state() + + Facet by scenario: + + >>> results['Storage'].plot_charge_state(facet_by='scenario', facet_cols=2) + + Animate by period: + + >>> results['Storage'].plot_charge_state(animate_by='period') + + Facet by scenario AND animate by period: + + >>> results['Storage'].plot_charge_state(facet_by='scenario', animate_by='period') """ + # Handle deprecated indexer parameter + if indexer is not None: + # Check for conflict with new parameter + if select is not None: + raise ValueError( + "Cannot use both deprecated parameter 'indexer' and new parameter 'select'. Use only 'select'." + ) + + import warnings + + warnings.warn( + "The 'indexer' parameter is deprecated and will be removed in a future version. Use 'select' instead.", + DeprecationWarning, + stacklevel=2, + ) + select = indexer + if not self.is_storage: raise ValueError(f'Cant plot charge_state. "{self.label}" is not a storage') - ds = self.node_balance(with_last_timestep=True, indexer=indexer) - charge_state = self.charge_state + # Get node balance and charge state + ds = self.node_balance(with_last_timestep=True) + charge_state_da = self.charge_state - ds, suffix_parts = _apply_indexer_to_data(ds, indexer, drop=True) - charge_state, suffix_parts = _apply_indexer_to_data(charge_state, indexer, drop=True) + # Apply select filtering + ds, suffix_parts = _apply_selection_to_data(ds, select=select, drop=True) + charge_state_da, _ = _apply_selection_to_data(charge_state_da, select=select, drop=True) suffix = '--' + '-'.join(suffix_parts) if suffix_parts else '' title = f'Operation Balance of {self.label}{suffix}' if engine == 'plotly': - fig = plotting.with_plotly( - ds.to_dataframe(), + # Plot flows (node balance) with the specified mode + figure_like = plotting.with_plotly( + ds, + facet_by=facet_by, + animate_by=animate_by, colors=colors, mode=mode, title=title, + facet_cols=facet_cols, ) - # TODO: Use colors for charge state? + # Create a dataset with just charge_state and plot it as lines + # This ensures proper handling of facets and animation + charge_state_ds = charge_state_da.to_dataset(name=self._charge_state) - charge_state = charge_state.to_dataframe() - fig.add_trace( - plotly.graph_objs.Scatter( - x=charge_state.index, y=charge_state.values.flatten(), mode='lines', name=self._charge_state - ) + # Plot charge_state with mode='line' to get Scatter traces + charge_state_fig = plotting.with_plotly( + charge_state_ds, + facet_by=facet_by, + animate_by=animate_by, + colors=colors, + mode='line', # Always line for charge_state + title='', # No title needed for this temp figure + facet_cols=facet_cols, ) + + # Add charge_state traces to the main figure + # This preserves subplot assignments and animation frames + for trace in charge_state_fig.data: + trace.line.width = 2 # Make charge_state line more prominent + trace.line.shape = 'linear' # Smooth line for charge state (not stepped like flows) + figure_like.add_trace(trace) + + # Also add traces from animation frames if they exist + # Both figures use the same animate_by parameter, so they should have matching frames + if hasattr(charge_state_fig, 'frames') and charge_state_fig.frames: + # Add charge_state traces to each frame + for i, frame in enumerate(charge_state_fig.frames): + if i < len(figure_like.frames): + for trace in frame.data: + trace.line.width = 2 + trace.line.shape = 'linear' # Smooth line for charge state + figure_like.frames[i].data = figure_like.frames[i].data + (trace,) + + default_filetype = '.html' elif engine == 'matplotlib': + # Matplotlib requires only 'time' dimension; check for extras after selection + extra_dims = [d for d in ds.dims if d != 'time'] + if extra_dims: + raise ValueError( + f'Matplotlib engine only supports a single time axis, but found extra dimensions: {extra_dims}. ' + f'Please use select={{...}} to reduce dimensions or switch to engine="plotly" for faceting/animation.' + ) + # For matplotlib, plot flows (node balance), then add charge_state as line fig, ax = plotting.with_matplotlib( ds.to_dataframe(), colors=colors, @@ -1194,15 +1487,25 @@ def plot_charge_state( title=title, ) - charge_state = charge_state.to_dataframe() - ax.plot(charge_state.index, charge_state.values.flatten(), label=self._charge_state) + # Add charge_state as a line overlay + charge_state_df = charge_state_da.to_dataframe() + ax.plot( + charge_state_df.index, + charge_state_df.values.flatten(), + label=self._charge_state, + linewidth=2, + color='black', + ) + ax.legend() fig.tight_layout() - fig = fig, ax + + figure_like = fig, ax + default_filetype = '.png' return plotting.export_figure( - fig, + figure_like=figure_like, default_path=self._calculation_results.folder / title, - default_filetype='.html', + default_filetype=default_filetype, user_path=None if isinstance(save, bool) else pathlib.Path(save), show=show, save=True if save else False, @@ -1476,37 +1779,95 @@ def solution_without_overlap(self, variable_name: str) -> xr.DataArray: def plot_heatmap( self, variable_name: str, - heatmap_timeframes: Literal['YS', 'MS', 'W', 'D', 'h', '15min', 'min'] = 'D', - heatmap_timesteps_per_frame: Literal['W', 'D', 'h', '15min', 'min'] = 'h', - color_map: str = 'portland', + reshape_time: tuple[Literal['YS', 'MS', 'W', 'D', 'h', '15min', 'min'], Literal['W', 'D', 'h', '15min', 'min']] + | Literal['auto'] + | None = 'auto', + colors: str = 'portland', save: bool | pathlib.Path = False, show: bool = True, engine: plotting.PlottingEngine = 'plotly', + facet_by: str | list[str] | None = None, + animate_by: str | None = None, + facet_cols: int = 3, + fill: Literal['ffill', 'bfill'] | None = 'ffill', + # Deprecated parameters (kept for backwards compatibility) + heatmap_timeframes: Literal['YS', 'MS', 'W', 'D', 'h', '15min', 'min'] | None = None, + heatmap_timesteps_per_frame: Literal['W', 'D', 'h', '15min', 'min'] | None = None, + color_map: str | None = None, ) -> plotly.graph_objs.Figure | tuple[plt.Figure, plt.Axes]: """Plot heatmap of variable solution across segments. Args: variable_name: Variable to plot. - heatmap_timeframes: Time aggregation level. - heatmap_timesteps_per_frame: Timesteps per frame. - color_map: Color scheme. Also see plotly. + reshape_time: Time reshaping configuration (default: 'auto'): + - 'auto': Automatically applies ('D', 'h') when only 'time' dimension remains + - Tuple like ('D', 'h'): Explicit reshaping (days vs hours) + - None: Disable time reshaping + colors: Color scheme. See plotting.ColorType for options. save: Whether to save plot. show: Whether to display plot. engine: Plotting engine. + facet_by: Dimension(s) to create facets (subplots) for. + animate_by: Dimension to animate over (Plotly only). + facet_cols: Number of columns in the facet grid layout. + fill: Method to fill missing values: 'ffill' or 'bfill'. + heatmap_timeframes: (Deprecated) Use reshape_time instead. + heatmap_timesteps_per_frame: (Deprecated) Use reshape_time instead. + color_map: (Deprecated) Use colors instead. Returns: Figure object. """ + # Handle deprecated parameters + if heatmap_timeframes is not None or heatmap_timesteps_per_frame is not None: + # Check for conflict with new parameter + if reshape_time != 'auto': # Check if user explicitly set reshape_time + raise ValueError( + "Cannot use both deprecated parameters 'heatmap_timeframes'/'heatmap_timesteps_per_frame' " + "and new parameter 'reshape_time'. Use only 'reshape_time'." + ) + + import warnings + + warnings.warn( + "The 'heatmap_timeframes' and 'heatmap_timesteps_per_frame' parameters are deprecated. " + "Use 'reshape_time=(timeframes, timesteps_per_frame)' instead.", + DeprecationWarning, + stacklevel=2, + ) + # Override reshape_time if old parameters provided + if heatmap_timeframes is not None and heatmap_timesteps_per_frame is not None: + reshape_time = (heatmap_timeframes, heatmap_timesteps_per_frame) + + if color_map is not None: + # Check for conflict with new parameter + if colors != 'portland': # Check if user explicitly set colors + raise ValueError( + "Cannot use both deprecated parameter 'color_map' and new parameter 'colors'. Use only 'colors'." + ) + + import warnings + + warnings.warn( + "The 'color_map' parameter is deprecated. Use 'colors' instead.", + DeprecationWarning, + stacklevel=2, + ) + colors = color_map + return plot_heatmap( - dataarray=self.solution_without_overlap(variable_name), + data=self.solution_without_overlap(variable_name), name=variable_name, folder=self.folder, - heatmap_timeframes=heatmap_timeframes, - heatmap_timesteps_per_frame=heatmap_timesteps_per_frame, - color_map=color_map, + reshape_time=reshape_time, + colors=colors, save=save, show=show, engine=engine, + facet_by=facet_by, + animate_by=animate_by, + facet_cols=facet_cols, + fill=fill, ) def to_file(self, folder: str | pathlib.Path | None = None, name: str | None = None, compression: int = 5): @@ -1536,59 +1897,212 @@ def to_file(self, folder: str | pathlib.Path | None = None, name: str | None = N def plot_heatmap( - dataarray: xr.DataArray, - name: str, - folder: pathlib.Path, - heatmap_timeframes: Literal['YS', 'MS', 'W', 'D', 'h', '15min', 'min'] = 'D', - heatmap_timesteps_per_frame: Literal['W', 'D', 'h', '15min', 'min'] = 'h', - color_map: str = 'portland', + data: xr.DataArray | xr.Dataset, + name: str | None = None, + folder: pathlib.Path | None = None, + colors: plotting.ColorType = 'viridis', save: bool | pathlib.Path = False, show: bool = True, engine: plotting.PlottingEngine = 'plotly', + select: dict[str, Any] | None = None, + facet_by: str | list[str] | None = None, + animate_by: str | None = None, + facet_cols: int = 3, + reshape_time: tuple[Literal['YS', 'MS', 'W', 'D', 'h', '15min', 'min'], Literal['W', 'D', 'h', '15min', 'min']] + | Literal['auto'] + | None = 'auto', + fill: Literal['ffill', 'bfill'] | None = 'ffill', + # Deprecated parameters (kept for backwards compatibility) indexer: dict[str, Any] | None = None, + heatmap_timeframes: Literal['YS', 'MS', 'W', 'D', 'h', '15min', 'min'] | None = None, + heatmap_timesteps_per_frame: Literal['W', 'D', 'h', '15min', 'min'] | None = None, + color_map: str | None = None, ): - """Plot heatmap of time series data. + """Plot heatmap visualization with support for multi-variable, faceting, and animation. + + This function provides a standalone interface to the heatmap plotting capabilities, + supporting the same modern features as CalculationResults.plot_heatmap(). Args: - dataarray: Data to plot. - name: Variable name for title. - folder: Save folder. - heatmap_timeframes: Time aggregation level. - heatmap_timesteps_per_frame: Timesteps per frame. - color_map: Color scheme. Also see plotly. - save: Whether to save plot. - show: Whether to display plot. - engine: Plotting engine. - indexer: Optional selection dict, e.g., {'scenario': 'base', 'period': 2024}. - If None, uses first value for each dimension. - If empty dict {}, uses all values. + data: Data to plot. Can be a single DataArray or an xarray Dataset. + When a Dataset is provided, all data variables are combined along a new 'variable' dimension. + name: Optional name for the title. If not provided, uses the DataArray name or + generates a default title for Datasets. + folder: Save folder for the plot. Defaults to current directory if not provided. + colors: Color scheme for the heatmap. See `flixopt.plotting.ColorType` for options. + save: Whether to save the plot or not. If a path is provided, the plot will be saved at that location. + show: Whether to show the plot or not. + engine: The engine to use for plotting. Can be either 'plotly' or 'matplotlib'. + select: Optional data selection dict. Supports single values, lists, slices, and index arrays. + facet_by: Dimension(s) to create facets (subplots) for. Can be a single dimension name (str) + or list of dimensions. Each unique value combination creates a subplot. + animate_by: Dimension to animate over (Plotly only). Creates animation frames. + facet_cols: Number of columns in the facet grid layout (default: 3). + reshape_time: Time reshaping configuration (default: 'auto'): + - 'auto': Automatically applies ('D', 'h') when only 'time' dimension remains + - Tuple: Explicit reshaping, e.g. ('D', 'h') for days vs hours + - None: Disable auto-reshaping + fill: Method to fill missing values after reshape: 'ffill' (forward fill) or 'bfill' (backward fill). + Default is 'ffill'. + + Examples: + Single DataArray with time reshaping: + + >>> plot_heatmap(data, name='Temperature', folder=Path('.'), reshape_time=('D', 'h')) + + Dataset with multiple variables (facet by variable): + + >>> dataset = xr.Dataset({'Boiler': data1, 'CHP': data2, 'Storage': data3}) + >>> plot_heatmap( + ... dataset, + ... folder=Path('.'), + ... facet_by='variable', + ... reshape_time=('D', 'h'), + ... ) + + Dataset with animation by variable: + + >>> plot_heatmap(dataset, animate_by='variable', reshape_time=('D', 'h')) """ - dataarray, suffix_parts = _apply_indexer_to_data(dataarray, indexer, drop=True) + # Handle deprecated heatmap time parameters + if heatmap_timeframes is not None or heatmap_timesteps_per_frame is not None: + # Check for conflict with new parameter + if reshape_time != 'auto': # User explicitly set reshape_time + raise ValueError( + "Cannot use both deprecated parameters 'heatmap_timeframes'/'heatmap_timesteps_per_frame' " + "and new parameter 'reshape_time'. Use only 'reshape_time'." + ) + + import warnings + + warnings.warn( + "The 'heatmap_timeframes' and 'heatmap_timesteps_per_frame' parameters are deprecated. " + "Use 'reshape_time=(timeframes, timesteps_per_frame)' instead.", + DeprecationWarning, + stacklevel=2, + ) + # Override reshape_time if both old parameters provided + if heatmap_timeframes is not None and heatmap_timesteps_per_frame is not None: + reshape_time = (heatmap_timeframes, heatmap_timesteps_per_frame) + + # Handle deprecated color_map parameter + if color_map is not None: + # Check for conflict with new parameter + if colors != 'viridis': # User explicitly set colors + raise ValueError( + "Cannot use both deprecated parameter 'color_map' and new parameter 'colors'. Use only 'colors'." + ) + + import warnings + + warnings.warn( + "The 'color_map' parameter is deprecated. Use 'colors' instead.", + DeprecationWarning, + stacklevel=2, + ) + colors = color_map + + # Handle deprecated indexer parameter + if indexer is not None: + # Check for conflict with new parameter + if select is not None: # User explicitly set select + raise ValueError( + "Cannot use both deprecated parameter 'indexer' and new parameter 'select'. Use only 'select'." + ) + + import warnings + + warnings.warn( + "The 'indexer' parameter is deprecated. Use 'select' instead.", + DeprecationWarning, + stacklevel=2, + ) + select = indexer + + # Convert Dataset to DataArray with 'variable' dimension + if isinstance(data, xr.Dataset): + # Extract all data variables from the Dataset + variable_names = list(data.data_vars) + dataarrays = [data[var] for var in variable_names] + + # Combine into single DataArray with 'variable' dimension + data = xr.concat(dataarrays, dim='variable') + data = data.assign_coords(variable=variable_names) + + # Use Dataset variable names for title if name not provided + if name is None: + title_name = f'Heatmap of {len(variable_names)} variables' + else: + title_name = name + else: + # Single DataArray + if name is None: + title_name = data.name if data.name else 'Heatmap' + else: + title_name = name + + # Apply select filtering + data, suffix_parts = _apply_selection_to_data(data, select=select, drop=True) suffix = '--' + '-'.join(suffix_parts) if suffix_parts else '' - name = name if not suffix_parts else name + suffix - heatmap_data = plotting.heat_map_data_from_df( - dataarray.to_dataframe(name), heatmap_timeframes, heatmap_timesteps_per_frame, 'ffill' - ) + # Matplotlib heatmaps require at most 2D data + # Time dimension will be reshaped to 2D (timeframe × timestep), so can't have other dims alongside it + if engine == 'matplotlib': + dims = list(data.dims) - xlabel, ylabel = f'timeframe [{heatmap_timeframes}]', f'timesteps [{heatmap_timesteps_per_frame}]' + # If 'time' dimension exists and will be reshaped, we can't have any other dimensions + if 'time' in dims and len(dims) > 1 and reshape_time is not None: + extra_dims = [d for d in dims if d != 'time'] + raise ValueError( + f'Matplotlib heatmaps with time reshaping cannot have additional dimensions. ' + f'Found extra dimensions: {extra_dims}. ' + f'Use select={{...}} to reduce to time only, use "reshape_time=None" or switch to engine="plotly" or use for multi-dimensional support.' + ) + # If no 'time' dimension (already reshaped or different data), allow at most 2 dimensions + elif 'time' not in dims and len(dims) > 2: + raise ValueError( + f'Matplotlib heatmaps support at most 2 dimensions, but data has {len(dims)}: {dims}. ' + f'Use select={{...}} to reduce dimensions or switch to engine="plotly".' + ) + # Build title + title = f'{title_name}{suffix}' + if isinstance(reshape_time, tuple): + timeframes, timesteps_per_frame = reshape_time + title += f' ({timeframes} vs {timesteps_per_frame})' + + # Plot with appropriate engine if engine == 'plotly': - figure_like = plotting.heat_map_plotly( - heatmap_data, title=name, color_map=color_map, xlabel=xlabel, ylabel=ylabel + figure_like = plotting.heatmap_with_plotly( + data=data, + facet_by=facet_by, + animate_by=animate_by, + colors=colors, + title=title, + facet_cols=facet_cols, + reshape_time=reshape_time, + fill=fill, ) default_filetype = '.html' elif engine == 'matplotlib': - figure_like = plotting.heat_map_matplotlib( - heatmap_data, title=name, color_map=color_map, xlabel=xlabel, ylabel=ylabel + figure_like = plotting.heatmap_with_matplotlib( + data=data, + colors=colors, + title=title, + reshape_time=reshape_time, + fill=fill, ) default_filetype = '.png' else: raise ValueError(f'Engine "{engine}" not supported. Use "plotly" or "matplotlib"') + # Set default folder if not provided + if folder is None: + folder = pathlib.Path('.') + return plotting.export_figure( figure_like=figure_like, - default_path=folder / f'{name} ({heatmap_timeframes}-{heatmap_timesteps_per_frame})', + default_path=folder / title, default_filetype=default_filetype, user_path=None if isinstance(save, bool) else pathlib.Path(save), show=show, @@ -1790,8 +2304,13 @@ def apply_filter(array, coord_name: str, coord_values: Any | list[Any]): if coord_name not in array.coords: raise AttributeError(f"Missing required coordinate '{coord_name}'") - # Convert single value to list - val_list = [coord_values] if isinstance(coord_values, str) else coord_values + # Normalize to list for sequence-like inputs (excluding strings) + if isinstance(coord_values, str): + val_list = [coord_values] + elif isinstance(coord_values, (list, tuple, np.ndarray, pd.Index)): + val_list = list(coord_values) + else: + val_list = [coord_values] # Verify coord_values exist available = set(array[coord_name].values) @@ -1801,7 +2320,7 @@ def apply_filter(array, coord_name: str, coord_values: Any | list[Any]): # Apply filter return array.where( - array[coord_name].isin(val_list) if isinstance(coord_values, list) else array[coord_name] == coord_values, + array[coord_name].isin(val_list) if len(val_list) > 1 else array[coord_name] == val_list[0], drop=True, ) @@ -1820,36 +2339,26 @@ def apply_filter(array, coord_name: str, coord_values: Any | list[Any]): return da -def _apply_indexer_to_data( - data: xr.DataArray | xr.Dataset, indexer: dict[str, Any] | None = None, drop=False +def _apply_selection_to_data( + data: xr.DataArray | xr.Dataset, + select: dict[str, Any] | None = None, + drop=False, ) -> tuple[xr.DataArray | xr.Dataset, list[str]]: """ - Apply indexer selection or auto-select first values for non-time dimensions. + Apply selection to data. Args: data: xarray Dataset or DataArray - indexer: Optional selection dict - If None, uses first value for each dimension (except time). - If empty dict {}, uses all values. + select: Optional selection dict + drop: Whether to drop dimensions after selection Returns: Tuple of (selected_data, selection_string) """ selection_string = [] - if indexer is not None: - # User provided indexer - data = data.sel(indexer, drop=drop) - selection_string.extend(f'{v}[{k}]' for k, v in indexer.items()) - else: - # Auto-select first value for each dimension except 'time' - selection = {} - for dim in data.dims: - if dim != 'time' and dim in data.coords: - first_value = data.coords[dim].values[0] - selection[dim] = first_value - selection_string.append(f'{first_value}[{dim}]') - if selection: - data = data.sel(selection, drop=drop) + if select: + data = data.sel(select, drop=drop) + selection_string.extend(f'{dim}={val}' for dim, val in select.items()) return data, selection_string diff --git a/tests/ressources/Sim1--flow_system.nc4 b/tests/ressources/Sim1--flow_system.nc4 new file mode 100644 index 0000000000000000000000000000000000000000..b56abf52da478f9fff54cad88c180808f50f5011 GIT binary patch literal 218834 zcmeI531C#k`M~ET8$byVAVDr6Ajly{IOPxugv1~akl=w>mSwXcTa#?u-9W$qQk8nw zqP0~NFSOpZ*0!Ry)zW&`t7_F+Yw@T@QLGpJYxVzqZ{~aZb~bN!v)RZdexu~=_q}iC z&HU!=?EB_==atPb?RIGYLj{H|U4%>InIGxT*D5@BVz#Lwna;1K4-UsAW%HM1jrpWt zU$y<;>sdoh`|M*>G{Y-DY?oINmn!+tEMef8f`O1TA+oasWk5gX&OtjvbQ1ZZi|~ZQ z!4Sy(48trC7RpQr&S8+Bg?Kt21gSE0d1mPs9mq1Rs?5LeXAFHv_SiXi$hBtCyA3vs zz8K4ZoM4bCM~>O~c;V*qnbQT&WGMoevuUETR00}@G$0U{8O~gau(Zgf3;b8SXl_LX z#On?JB20vszhH4mdBuVy#qd!JOC{7TL@Alx+#b?gWr2!q5?)ysMNgxDa(+Z zi&w_fWR1-cgec3*5h6NTOr6&}qUp|vAorN7mgOEVnVS+^d6Vr{#hM}uXa4=);|qiH zmY2;3?r@)Ug*;V`{NyBg%NSz1vfMoGhgo7taoNJ$5l*$#8BD2`>U~f}mEzOyyl{r( z+*{*}Rf>U*$#53p^c)$Bz9e)jj!rXYEPZO6bB8x_8$rStZ=e!TQ3D#FK{n{#ai$ zYNP0jXUS$;)m&+1iQXJ1FLj@~qRafk+BtSDM7pXegpb(7!Aw~^^{y;;>G6`8W>wW&U)nSx}&X_TSB#2DEEpC5Z zrLW!xf9k^Skbk2OZNtF^pOZ}$WYHaj+X)S;JT)P-^ag6{19gxDD$f`?FW|571ZD2oW?-&LQ|b%Wtv|J)sK_@tSnG2tSh#vrQ?dm^+f)h`9ZwYuG75DTsH$?; zdxD-?Ul?jJwvbEemQ+*vw56u6EZ1JeA3^R+oVrRz*xmyqOp+;2t-2o6!fbmHtpm zIX2X#Oj1T|8B21hQivn#NX(vG+S)2 z$5vC_6-~NvQ&q$g>QJ?5NyUt^#kdkia&hgpX56|;4}5tyKz7ydi?6E6=M9^`aDKCd zweSmKmfjl(SwC8ifP~@qSLH`TV>_YDs2$KDqGjZqJ+n0dc*Fru$&Gio zmcEt0Xd{S@4>t0-q_&|Z?5XqNLyqF^sjIYI%etr5cxpU#USB0#L;h+&GR0*}LDYUP zPzmR(-wGvT5zHTB{7Qi??o$XF%g31b#uM;L>3U>|WGcbWdc@EoOb;}Worq4}qpDJx zi=+(j=Xa?ruf{UwC)6|pbM>TB5e^2-CtBN+7hVX>61MLV!$_9Zb6Nai$kZc|R52h@ znx9J`#Wr|?lQogV<6FcqQr4sZVW#a5d{oa^(PchQxNyF&7QSY&-<=A7-Dy!@9=kO} z_Se-nnCdo;Sn1IB zM*h+|q!VKul|t0FEMW>%72kz4nr#Km$Au_23~LP>oG|nlRy9*)(C&IoC&6m*EMtfm zXb2}?Wy+0mbgC#}s37tj7+nSkt18G1_c;UYQ? zXNi@#3w3BP3V%k&wTiS$254< zkG@Cf(H)X(iff2@&>a4RS#f5h`)o% zaRFsu&G*1}fli``HUwP^K6e#Vr5if%>Y<+kd$`W9wHeq(jHGj;#?Ce~6j2N{y%yQT zBrXlw*~)U-+GffDHg4b8l3ITVJJ6ad5%u6{MItqMe0bN|9^J7ZF{hw|#vJ3IzE$i| z?h5}%#=cML4>db#L8kstw8Kya-IeOG!<4COoW@S(c|yLzV)$d8rExDBu@YjNiePnh zE4!8ILb3~A2<+C!U^?7|AwRebg%gV=7L6^M0RI;j70odJ7pjxn9GXLWc%Z29!ENGo zuMPUv27-Q%9A2ROQ)m1Ks&79RBS9cN<*%DmTYAbLb)xI`*sqO&^klEPl`TD8w?LX( zdb(~OO;73c*YuRm08LNn4Ak_L&LB-s_QYGy{}Gx!U3ZwKr*wvEdb;i?O;72J*7TIl zQJS988LR2ZUX8q1Jjs5dW>43hr0FRg7+Nrc&S9FSr*vpclI$s+8Ja!ihX(w0`&kb5 zb2U9(cb=xF>lSNzy6&->p3*7P^pws5O;718)bwP(NYj&jxuz%kC7Pb>kJt2Mzf9AU z{c=rD_9tk1vR|R;iNh(Hp3+&V=_wtzrl)jPYkEq@qv->vECx-|8GzIT+) z{hB?c^LtHC={%_EDV>KjJ*D#}O;71Otm!G8M>IX9^O&Y5`^Pms**~S}>AFvAdP?UR zO;71OujwhB7c@Pk^P;AwbY9W)l+NEYJ*D%irl)jX*YuRmKQ%q2vscqoI&W%vvVTj{ zll{Azp04|zrl)k?*YuRmhnk+!*{|s-osTp(xIt4^!=iA)C?l)ca74?He^C%Pw8~l^ps8)O;70@qUkA}ZknFb>8|N1ogSK= z(&??~DV=;xPw5nBdP+xO6A!BMB=fO-5O}k*%;#I`__jKlv%8jJe}Dl~0CQn>7tL}l z4g`XgFvJNHj?Abrvt1n~{?ZMS`fRS7$A7w_v;t;}3i@{ED9Cgq<}R^RG>SqH)m334 zHHO*d-^lU%r|dHugvcbt|CUG-dLGXOha+_c+3I?9#9XwF1E#R@y;^$AQRzn?urv(%$l3T9M*m*~ z?hT(@a;>rbu467}Ty@${r=DfF`k%YY=<&p%1!&PXr}z+fxW7vnAXT0z9zK3!*)yW3 zuQl#SmePZCtfuN^jS?SDx;F;}2ZjMBP1~9TgkljE9~SoyJ4zSDu)2 zR-)8fWPGXjOfem@())GstHxDd{Pd%Hz3=~U#Ja)f?J}nC8xo2wS#Qsoq9;V?wp47; zbAlyV^~%#jx0f%;-nw@FU(SAVz>N!TFm5~dzunNZ5C>I@5Qt^iNj&g}foC+Xnt4Vi z*K03bbGXUi*zI%Ajb(6KpB4Z5>aOoyu<6s0vxZfRmB&4`7!$>6w$}tET}L!~U;O3! zu|)QuLS2 z&A+UC$o6YG-}pq?n}6-~*kS$lP8N^24?PA881F`EQB^B|=P7{a{{8WNa4SyjwDpP4 z9yZH2IIe;n zDaL&&f940NU-{fJoReh0JSf~I!Rx7km6aYE#BO#Q0^~`3biLs-&6bjn1^srd{I0c_ z*qkI{s4jo_^)~}OUeM!)lU{Cw;JmoFkDB5_BZw(4E*^l{%Fc3qw^U{jGlZGM3}VMv zvI~rVUJS+%XvkgM_P3>9VPB)QeED9N1ZPh@_3qxe|2q2vvm3e|uiDZ@##9Y-#GFx8 zb~V$B$hn$~&n*{~T-3)b9O3(i7qk#`e5n->;ZGfU?_bOUuEum*T0pwL=x8q>&ib;; zINK|?P5OTS{iyGUnEU8}rIM@C$3kyC1_e)8Yu+J4ZPv9u{FL!4jg`9Sb8d_et+k z=Cn;9?CFN-bS*?*kR~!F+=Fb*i5RK*E`scsnK?95M|yh)DrDR`c@R=Na;y}8sVL4KO4HB#KfP!?XoJeO?!yCsgWz1 ztJdlsqV3xyGrfx=*&d=dJTiVS^S*=6zkTV(N2SXC7we)6b*ogV&N%&=ZPz=HdWTK@ zCF_1^FG#bWojL*V-iQ}UH0Pf>uBd;9o&I?A_#6{p0!)AjFaajO1lk_~=`+kSjMn)K zX>kUn-&R#3DV{|zmA?TZc?8Z5VQ$(-ZS&!}c4rG0h1lgjAYa}6sq0KvFYQ-9Lc7{9 zO&$(%Ci*OM|2Aq>}1IL>voN!~|1yQavT+AUjxuJ>104X^yyP<@Fk4%6G zFaajO1egF5U;<2_!xOOPj~>j4z}>e=&NvYf$LnwcV&ZXMIdpuQwT@}MRHXREDM z<|%E$iNKeYKXk*q<%>#1&L#a`fmZ;7?>k)l=kwt?VhQACgn057r*si_f_A*P|Ib^x zh&v#k)5J@GafgU<&`uD4dgGy<;xX9SWr)~Pab3Pxe5epp#Ly?+C=d(5cB&YE)yRJ0 zR2Zh5CN>q{&`-<*+p*%bU6YO!0rTUorV-=C0}ySJ=zICr$zlQIy}!u0E_aGJ8yw#v zQ9HWdbg>SyGfC_`&|{vt()BRme`;&7C;;sY@mlpCO2lPwiSgo*n^!CpyI@dpiWqgq z6^q3!X0-A7|6M8$12$vDyS05!5JiygF!A>94nINM10P3=>!3&4OtapYeEWlg_+~a$g^%i*X42VSnD63%BE-n=}gRB%91{cfC zv)$;dycMibW?5}GJKPKf=B;Kq|MJf3NoTjh*)hUeBP{P>+cHG|c}D47pTfC(@GCcp%k025#W?VCX6O%up_ zKoZT6X#(hN*!>ioFK(Cd`PudHpdZUjYk$r4!KM?Not6FSo$@R`V&mgAK3ccUR%WTK z3&v7i`0+V=;E-By@`p8P#O^QyPJs%ZwO%i2GS z_R*rF^li)KM%?0$DnqIHw&*j@*m03He^(_IIXHP|);#2hY1F4(npegnhf#P~U;<2l z2`~XBzyz2;ha(U*g4F$AvV8ipZtBmhUG6z#rhM=}thEpRY1!P^T88xHC(Rd?9_te= zL)!DAj8DsuQb+XINzYLt=DUooH|id*)R{c{@yJK^ei0eR%ik@bc;(@ndEx8-ZTrK! zbiI}xqwf7xkIBd}_x^GmBQszXW-hFzbeH?W-pZ=U#eqPu(qHEZ`$A^4JeZmaizXoy zntu)T$|Z8A=!kxt^e15^D-5r}x@OTGQto1B3MUuvEtg2Z4iI?B904k>V80!)AjFaajO1egF5U;^!%fRq1_?EJuo*neo> zHvs3J2`~XBzyz286JP>NfC;pKfE*?~n0q~5f1k{52JH2C4CDmIiD<9KS@8Mh3h^Qi zS*8E&s<_Rk{c2^Jya433TSa*Dmpk6Ta^>VaE^neqm(OahE)S8VvsO4QCcp%k z025#WOn?bw9)Zl;nBt`SC8e_n7Q8`2g~T%=)9;YXg=^a#l6jh346^%C{|X86M=gCn zK!P=7KfFd3{bYF%DX$@;eJp>MC;bt8&c_*m=-&`l=k;Uhe}h0dT-T0u3CL};pJv`A z!CN8m=pbt=(9+sU%fw%~7;h=wI1t?OKz<_Ch4ZA}^w-5g$b!8t7J=M0b@Ao@K3_)V zwH=+D*JS5=zOFg3Jm1p>mp?KACcp%k025#WOn?b6feuFC;CplZv!z>|0p8s4P`P$* z4jrzx@#cPZ^Rz#ta3?u=d&$nu{eZo_6sU6)Ccp%k025#WOn?b60VdEs3AD|`(-i>2 z?lzsG$tyu_yNRc%Z@_Z%u06-K4H5KZ?L<~$d?S}n?qH6dAkl%jxKhMI!AvpQ&iAHL_ z(tLKDet-ch6+>j2wUqfU6JP>NfC(@GCcp%kK>H>T^;G}P=w_bkm*S&ns(V|~Vo{lqYP#`_e z+`r0Ol$)lZg#^2YeAZuf@*hxlL(02{48R1kvs{0yi$D0_yn~1wd{|X1<8P+h^VAbQ z*iWD3G0xr4WA}Ty1>lPo}c&@xToL#%lop5TJYD&CrmV7 zY&`pfEuhS%On?b60Vco%m;e)K{{+wruvUS*bAo&@w(nWkHai-mK40wTXHF`UF2u~1 zZYr3-iOU@!OaF0HC8C?RvC>e>DDtg|y2XKz2{q_B%Kv4o===;)PE#?S<@3 zCWg!wOn?b60Vco%m;e)C0!*NT5Rm@d$8GG-%~w+^ajG4LR{L}RzAAJ4xlK?YyZ?ss zKkf1skTY>}#YeI?jEAhXCdj+z+1B1YX?gQp7xMM5-4g%ai1YTjwEOy}16}*Q5Y*%l z$eDO=ocG@&sq4b|{&4Dh$aRilo~hRQ9Oz24!42=1m;fN3W&%ur2`~XBzyz2;J0l>y zhAUd@H6$2A+*Tvq02)VYwa+>7&iH=CH#vrQ7F4<2uQ*$i&jC5L`M&rNK4jxK9E6ks z$D8E>#}5Wb06W#0wsTw_&1-mzuJ7b!^dtj^W0<`R2QsW?0!)AjFaajO1eid_ByceH z+DOkHDw7A~<#fzCNxoEZYHv)iTlagzcn9$oW1Oy$*%SUYY0w#8@P})nebEY+eiiz+b{tpzyz286JP>NfC(^x z4naVAZMU>^E7-L3Wmx;>e)fQzjEalL&}G9#V@dR5tOhgipjx(@k8}y;b!7a=?ma&+ z(~HRGf}Cml_x723%yt}5x0Xnbdql$1`!7=W=8rz<9hzv3gjx}tywidt)4Z9z(Dw-{8uiB) zNyJ`?8=240cEjB-4)|HLo9pDQ_M%)RIR2Ks)dYn2G!tL~On?b60Vco%m;e*#I0WV` zUr;VO4Wt4Y;KU3UcW;$)zf<>0A7}8!7tcR}Z1~uL2%tZk{I zKIuvXG9xG74}4L6DfGeXBu4+!zQ6Bw4eX_wd>P1X*Bg=gaQ(ISzWrX*jDYJ0%SjG8 z)YLDr_Pmg{{6drO200VG)4#7MzM{qVY1{Oq+^QkFO_Og2`LfGj1CmbOY@$`wdo?c> z7c^j?MF;#g9LaV1} z5*1yDW2{f+2dDcva<4|)v&00L025#WOn?b60Vco%m_Yj>FmL&yQqjqJ-^G!zUUH-u zMENfVae^33;ua9ci$W6bG{qq#z6S2e5HXa*f=)t=6-SUb&lHD|_;-jq1eRv15HQak zN#a$eID*8-KpX=3QsF)G>?jfs>x>r~P2zkI$BCmz41zdEj3M!E5NCk73I*WT%m8y0 zmVh_{im1Xm5GRQu5^piZ2_(J>;#4t_#9rV!O$KunN3b~bvu zLO@Z>4i}UFb~5`Br+r1@b#Ps{u|=Nxj$E(FH-MapeHkZ~?)(MzLNwpql%AdreCco0 zs>f&EY`)@BU-fw6VTRQz0BJ3xI@X`3^?KMlS@05gvvFVoOn?b60Vco%m;e*#I0U5E zbCBP-QqxO5EaSsx3p||FSPkiXCygX69MR=Wh?apQuPilYpf=qY~B?rA0RyZf;q6aMjKR9_oy`rp~g+ZD( zgl;zm_KGA2{>ucI025#WOn?b60Vco%Iue15@P;4Qlo{UeV^Fj9@xI4F&NOfM^E=AB z;kcW07WdDRK{6=XoEze0x__jvvS zeN`i0h<~G}Z2SY}3v`7KZ@SMt$;mrTw2ur#H$xG7$Ap59On?b60Vco%m;e)C0!*O8 z6UYefxO{eIc*plZb=$q;UxA!y-tp6So_~_8w6Pg6R@`L(en+yiT=y)LmDuuW&C<~y za~q-;B6_TNXO>KLjd>2L1LmA(K_zvEWyf8tWykaVwf;)5h*@r|VqqF3d4>H`^_J&o zUNVmzE3kZE0!)AjFaajO1eibvB_JQTpUtcX?ujc?ec(?2Ds;P9@ONAKyJH{ zyVRY6slPb-JXyJ!G4s~)rXKuWs+B9fsUr1ak$V9=2C2PR4W^PeCG0bAcfR!Lt*O^hyXWvy&3ob*xEf#akvZP<^Kc8;z3CS;`9+X3(VKp^_X9a? zB8j1QmqDap5jtj0=7=bpK5n(0OM z%zX>wOj`%J=B)e;rDY4|3B!xUm*M;)5p2*P&c?$M~O`ycgv0FFm{j4he#jmo+p&JUZQ^d0RXqn8@&v2`~XBzyz28 z6JP>NfC+Rw0`rzHC>J@NId?S*`PS9Dx5z>c46c=)*}$D&f3d9N6`V_C5=hgM+kA9q zdlr62z0q=xnA}xN{u*3khh{P@Kkq5z^DE5aMs-yq%smuQ;!eE9u$h+YtF zvS>PT&7A^AQH#XxE1$Yc6dZ~MEAHQSm)HunW5lw#cm75cz{jcLkE2f5EnWlnYlt|# z&x7}gt2zlWR($#Kp5KZK%+C6eqv`GuluIBDB{a zz3=I*-OVh4_dQ0v@+fWI_xFAW_Y8zoz3+UrK{0vXsZTe)Ztn|`L2@T=JlXZUx3f3i zUcX43ZzjM5m;e)C0!)AjFaajeVgjXQ^KFxu!DoH!M>q5{TeO(FkA z-)eJWIe5&j@ye`FH;bg~ufvr09z`jm8;v1O6q%W#nG|J$j3JKDui_1a!l9;mU(j7u z6Ik!A4m1QmSwXQTs#rtxSj9^wM#l;IDb6Gnr%#E$E>!RHhZ^eEj$G>Y)yRTxDaku3k=ZrWCp5vCw;bK-%3s%Oi&P6*;$5t9z&~VHavMiW~t?UE9u`2?^8g8 z*zyjSus+k|&q2<__4~PVu!0TIRpi4&5_hS;rshOXO@mLCqPb}*ct+kJ(ng2_Tc%#- zq6UhTkz-`-{PeeF#7cFt?SNFeP8ku&LG&l5zONq9W--zOTA!Exy@=e~F;Q8;1egF5 zU;<2l2`~XB&<+W-Q!gnuM?R=#f|v9KRJ+|v`cjks2Xfmv1*wmnr^icra9ic_h%NfC(@GCcp%~0RruGto^6MF5`Ed z0bVKu!*xVFW4VU?@!QCFxZPMTr`K1>*ZqZzkBnz5Q$F=gGNx6f-DRG`)pDW!vX97M zq&+p<`XIJYoj)eyQSprBrK|5FNfC(@GCcp$b5P|5v zH+Uh$`Pq3>4cAmv#Nn~~Nj9^|?} z?2P#Xiezpp2oXn=`hs=q_v{MR`r^hMct6FQt}ch0cgBsf`ezhnhKe#6_pclo_SM!0 zf}WaDV`rP?iYRWpMaEF(eE2pHz;!Xr??#!PX-9ka?QUufM<;JT(Yl$uSN;9E)nXWex%7p!F) z@2e@DWr{^Eyu^7FEOzW6!lJKOR#IBBtfaiS#Jy-i#d0C~zyvnzgQ)a`Jt1E>A-}`G z0_VC_`fGi4A%CDw#Yi;&4X@!cZ41r62Cl$_Knuy~2yeZQ3RkX<62;*9Xt@f?U=RDx zwo5|nzb0BPy(C&bZ*R1`es{EdBW}5E3!Cq-3HWhj^!fKHqUBeMqvhL=kCqEpN6WMx zpRPhn?n$Oq^(526d6H@AJIS=Von%^nPO`qLeCr^Z(I_qU9u+MY&5V{mSrIJ<{n7H` zrs(pG-W)B{#AnLSCu&YJ$uv`$WSWLdvOdZ9`kzE!|E-F`Bh9_qPsRnmhUgA=FT94k zym)CaQ11(d{k{++gNfC(@GCcp$bGyy07 zp?8v{u44ZoNm6Xa1egF5U;<2l2`~XBzy#Vqf#@A-U=*%?b@yz;g+qB5CRc=E1DCbF zrUiBDe4+4CPta5A3&RLVliaArS5@WnhC}W^mD_Jxz{p>C1MZLt!Ep66k1&!)$yi$+ z*UAl+-QH?XaIMcB3VXsn8b32!{Z+hy6c4sJ4*NYdQRdr6(PaiG^P|W-<}y`&*dr$@ z_6t)e_COW87sd94J<60u@C?`C%Ba9@ROt`N{L{#u;Toju29ceWm70LLS5MGg9cT#B z(4XNNtRf7I?8EI107`R^$zA2~LIo7j$e`gWOu~Eu4ICP-VanXxFjO z^L+SL5@>Dj!3OR<_*ULGTxcf11egF5U;<2l2{3_gCxPhR1DIWz_o#2Z;p(Ai9HE?G zc)wJ*+FcXyRK~uKi6-G1E}RC8A$}~`Z$MLZ4OdSYN%V`lRzsb<8alf>Uyvq-i(cD7 z-4QZhWNUfqweqD(H2v3b(O_z#crtG^P1tbdtGrQ(VtY~Ki=Z%_S!~}4%^EjcG!MOJR1Ac1^)i{5;iA2_4v(_YiO|e*!v%Xfo59Im z5~rEzhO4WxkK}>gVs?{yWwhbSjY+e)7n#xQcEi;zfxXNF&3-prv@h9VQD2$J7v}D> zWP*lkh)P+#a1CD)hqzw9pxN%D*7hb`$h`@QHy@Y)6JP>NfC(@GCcp%k025#WOn?b| zTM4wbci|fDUHG=%I$Ulhzyz286JP>NfC(^xZy^EuK4^JkB>)=w&h6q7(_n|E%-jc! zUKdP98XBicv_`m_6xL|8l4<0b?5&YyrLabhl}zKqWN%I3Pzq~uhLULl z2H9H^E0n^TFrj3c3PJWX#erm+xNM804eUcZXecnh*`@E67 zxkvOqXteJeou_@@NTz+?NM3Ed*;ZbU_JO1Gv=1D~v=1D~v=1D~+sdNzL;J$fdD<6_ zWZD;wWZD;wWZD;wWZD;wWZD;wWZD;w*2wHCyj zS_?W)tp%N@)`DbeEy$i)3p!7&1uZlBu;IdulD{Jhc`iQ)@x?)LM{C ztp&-{T98bw1*J!=1Ffzo@?yzF`2x~JXa+A6E$G0 z)LG_o7qaM7UJZ@4V9}tSJJ#tbC@e}H;hN?u1Z7?&-e!y?=%1Kb7g%Y>Oa3AN0T~M* zfu0vbgz&cfy#5*zL5XsSrduZgjgz?%)IfVsPzzqQn$?2WCORm@080rUo_ln+5@j!r z%NX@1Uey6?*+&8q_KE23c5QbWm_z*K1shG+Cbb67{g6r&fwbN zVc{pFL4HZ|LU2>qryRC@N)%=+zh6OkOdJ@C!7rxc&Q3c&Gl{kT%9}SjkK57Uge_T| z=?0rUdi(^qk3ky00rNWY`rw}hgp5bt7qMaaUDPb|Udivh{6<}~yB1_*&62PGiXTN7 znK3S#+s;W4GXKs)_XPdGZb-^-Pb+noI7>X`NkbBDNJ@9Rik)Tp*W&!EoW<}B<9a0| zO@~`c{#C_Jt~*J_CtsLbRMEY%sMvj7ZW%Om66gslGR5QeN@Bq)A$q#AtlR}TQY`1;^$B<$qr#OH zS6Omfs#IC*n;6ivpDMUYN-Ojx!qWR-=M{BVczo3?<MWqjIdh zfk!~RwJSiRPk}_{DmsD4WG$YNiAVbb#WUsPI*XNVC z<3#u=#kF8}$_sOAP%GK3ycO|wIMJuPDR$=*ocvVL)SJAeHtg$t#N3sHj^Bn-n}YX3 zZ}&2cd-;GB6!g5Jmu0$2%1fQD@`{ov*4}1L0Mpi9XG-+fCw!BAK>52nR5P22p>Gab zmR3GK83m1z`Q;wC(-v7#7_mMaFa^3Fd{wRtHL+R@s+Gx0fY-|0Vbtta9Nzw+=!x2o za^`yyMmVSCmlRq7m=k;$m=XblXKHRXKvYoJ=v0eHP4ardB-v!M0x_Yp_7DHL1*Y*H zcUk@vrxg)K`rwfXl?7At%ceMUp?`_L)EHml^myF4g|2d;z%NmmBxn}9^9vQ4e$vIg z*H3~EV*mD)Ta~I*Q4**asNts|m=v%msAyZ5%5vBBPQMQpNVBxeIUOFUa?4%QDvCY% zB~Eumd2U6y{%nLg_QY))$=rDI&jVVsB#FNdnB)Ry!v#-ZW|ctgYqjpT3&)XY0RRm zL-J`SX##EmH$th>p-+kD_AF;v$#qv$q@*}g%cdze1tksgS^tb0PwPH^;VlL7Ej?JW zc{H$O^mEbrzp+`J2y3w`oihrY<>k40LvSqqlf|vn@5QNPxGKLmzobB1$EMb;0~pGQ zEfrD!a@HL<6B%!hGuPI(ubG?KN6F}>{+rZzpt1g)fWyPXpH8V3gFy*)$$9 ztr@_YPoFWxnQ*8N{9MbXcT`zKlXcNc)yAz&9Zco*HlI=)j?{b(E*o_4siDk0&Ds{@ zJv_>%HF!q#=^m7P9$Br_bz?r(^ycTiPz5cEQ`cwaf1?WA>?QF|ZJ6f68GPK-=-ZYQ(6p(Z z&!d((U)_#->JP;xUrs((TMEE$efnr_sKzf1(&98G*w&{baaqVAX$89XE^Hz0jrPH9 zBw4$XH@Yqc3)R1xlCSrqDqL@hPq&5|c~kwkB?eZ|GBMTvCiI zeunvt9KIv9{;tOW>PB6x7bD~fP!pePP@ITI+tkG8>N9fGq$b>oPmA%28#R$-m$#Jw zJ)JA_=JENfV!grmCi_&OL; z>aQqRYp*6&tM1jxWYiqBc{OF_An;w+{gMGs$l!9%4q zB#SARws`eX509vi=j97md#&_@3v zB_7WUOT6@ecF_LCN<8Xcro>}86-ADf#B_7LJti+?8X-Yhv_gW<$?UX6;XooMRM~O%Mxk^0h&sXB{ybF|gw6jo&M?1GF z@uim3Y*@Pl-qUOZZ-qyDo>Jf3&G z5|4JCQ{vIi1|=Tt;NoGtUeL}VsJ}&tNBuXHcs%c$ zN<7;6w-S$b{-eaBo$X3I+S#GRqn&q^c(n7L5|4J?SK`smhe|x!`ACUJJ0C0YXy-E} z9_{Q=;?d4tB_8d3uEe9AgGxNwIi$p+9bEE^{R-`Tt>~kjZ1LHm5eV#oFPK zpEmfK^4y7uarmgjMZba9rb$bFqOkBV&A7wD3qR3yQ=2sLs#G)mRnOckS8?$r`Nb7- z=*Qm+O1#*dgEt(Vy`bIdWfLyS%;bGbeVMeSSJCgcL7CQQ{9}OJy$q~+BoP53Km>>Y z5g-CYfCw~90y8ETrB1gXYWO;OU+MDP85E(vhTw2`Qu@MLWA5ZZS5+5lYQUNC} zF+-$`$45<^keo5O*@ua{e;bmsDKYATd5?FjEd4Dy?z>H{&o>mE(f{6=9Vf2z+;n77 z?^O9>nc&6fkky~9iE|~Gc zlI!|74T44+~tvzEqg9sbNXcmPprMQ$=Hmy^XI=aZ_IgHIuHBemWQ>w z=0EVkjLALUIe6cY$CI`sR^GMz$KG$IEco#4vUk=Wf3?N6<6i9i^q8oMsh1tw|HN67 z)@I!L@%GQR9!PA#4(7IfUvGtZ5xfzGrG#rUCcm`%(w0B%x$x!9i(h;F(}P#X4*9st zrbxDGaJ!wN!MZ?$9X^@2GUh=0yd4t{wEuWzWz5`T7krs>E6$ia^o9t>j1K>4J$kz%eC&eD22b_O zj7Z5TOX(M(Ip-d0Gy2(#>@}{zt9GyGzj)(AV{U(7MMT);Ips&9en|W|CFZhrdzW_p zY0EBFk~n_?FLOfV#HTyev?cMkm!eZ$FPk(x6y>aQ_Nmfn8d;OSGY`O}i6nT@OI^)3lzMt&*=*UZd`RK9% z9ro`y+H`6q>p$f59pV%p!zo_*Iq%V(2Znw1>yMosH+M`pHhXI2%^Nqb8#-yy)x{k? z`QqD44t_g5@r}r9+@&|)w{LWZ*>B9#%lW7$<8y9gWw)bw<2sz2cl?!;3EzFPqE%$) ztcSkra$Bb_KGX4CI;ZPwy$Ql2qh8Y@ymro;F+XqFb?CPTS85B6 zCMPsK_E@*OBiQ%JEpGvZ0yZb+=i(8&*owW8!)8pL^juTN>JFC-Km6X*N>4lXKv}!3 z+{(({d0~IKzQy9a54X%|vSewW?MF6_+}Hc*J2N`H)2qednJ2%B-*GhM*UEd2Ep+HT zIQO}ZMS8x-qj@I}4vX6O!jz1CoA=L7|8;TB`2+4ON(14&oc$x)9ldZ|ujcHA3%j@G z>MT4mB6-{5tqXR}&1uIH?$Pdou2cBbN#z%}*687jK>$Hfhb5i{d*R+i?^+$73t!j5z-td~<8s^8^?Jr?$pkj-~JsE0O; zUsM<8eRg1NyT~PdV&0#g;40l2-6v!D3BAItcV{mG*#B%ARrK6BtGh0XU@za=VSv{( zWXzXsDkU%YGo4n4W+i=*I%&5M4PUbTLM!^Xf}vw4%SokI;*)1@Y}(@6OXGT+fB&ixx5w;!G-}|; zKaV;w?*AUVAmiBg`U#f5{*qgiv$nTKjx=z)_<;V1|G6rC{gvZGSFV43bB{YJzF8cZ zxBixXQ7M~U!w#P@son8e-^SduYv_Z&<*iSMOX<{d?ykIPTmQxXd@cN!D7FCK*1r5e>Gf+LUvPAveiOQX*Eap864xXnMs)H` z=}~X-s#{~24E8CkZZ^=FevUXZ&dJq!1D$a&{Kf-+mf+77{8^#88|W;R!>}cgM-MUh z#dO^GP0ALNSo^Pavw;qOR^rcBxB!iF=x}n*GgS;+riEgNaqrg~D(j|3eEF5RM)4r8 ze{V_>9Bjj>sIA?ADlR{=D_j;0sM?{Scp^XqhyW2F0z`laG)e;e*_3bG*_oxR5hI|t z$$`-5QT$~Njh&`X_7~(A!<#AjxXRb>w!j<^j;8Y09q_5YsZMUd$`|i8+_%BHZn)4E z>B3v$@v;EMs~b2Xi}7M(Vt>mG#1^Crb?b~hqn{UQ5$t}b?YV*QFvqiUkvA`BFfQGs zD?~|f?J;|D@jdxSp0V5ILJwmpN@}oMjDG!8JnV0(dp@Q7lv)ipLq*bfGBLoQz4hvOJw!n;>QxUH6ulO)^fFP(>V5Lsozf*YLHC8|v`~1`JJ;S_ z#WEysTaS{%6-Hab2uSdY$M8zWBX1kt*MeI(#&a)h6@9V3a4dq~UShynVRGsV;{M}Z zICf_F?VoaO|H013#`}!_T;AB7imz>V-@}?DLn9zY zKZa^rGB0_ke*NXO$%4Zgi6iLI4*tt1{MTPrOINODBOTFsJbO(C9MHkTqj^A~wQkQl z;J}BJUVQrcp2i?jNHJb^11~>gN5mocK_kStZn3abGmK&cD__2xBwND&kLdjC@PRAE zV2{m!L|P~Y0Qgk8o#QK)O-N$Chk04nI2=~O)+R^S%7-K(Km>>Y5g-CYfCw~n0(Il? zyZ}$VJcR5?eM+S;ShFYf&^tV_PzlUN4vj5mfuA4m^N+AM7fCvx)B0vO)KqDlO zb?8SsdwaIhRU19A&bim@MO2^;521w2kjKY2KPLUtBwa%7(t z&eC8PhpueI8~=-7Pr=vD?D8-AoyNL@^{y;sM_D|Z3C`gl#^Rn%WSL;GJA2{VLtWUH zFoV&9-4>I4COZpY4PYO{qbRi^XQVd?_2J zm!Z`oxf9t!Q0T`-{c`=~ELJyGar0|euwh_q5WA_N8iicdpoKVT_C zEcCG+%su!3M*LpKx|vAUYU!3a2Bf9*1( z=}gheSG$pf4vL;CzLjN-!Rw7zg$LuJ5h8sO0U|&IhyW2F0z`laG;jjqDLYrw^tW=g zI(p~#zw)UY-^C01sT<$%R9{& zI+2ZF9PFIo5r#(9Uk)lxF^&!4)sNkkoa3rFQYsN30z`la5CI}U1c(3;Xb1#^r+!Wy z4~m>Fw{Dstk<@xnx=ZyuLn(R5!Frj}T z@5{=ABqBfrhyW2F0z`laG&Tap0ziG3olgz%V86Yv+-D`s`diDsPt{TSyg|4&Qa)ee zC_erKjPLIrnBt}J1rE#IGqS2Ua9t4-jg z)ZmraXn2h}cdXM>P*{|j;dYl5x=Qjr&T?J7C5+}c^lu6Jr>2R4Bb+?MBQD|Osr`zl zZ_|})^l@s2WbS=l6e?sxGVAsUgY{uZW*`*B-0|QQev5oI$mO@*&wt;`&U4O$wCaTW z$#bp_Cek4SM1Tko0U|&IhyW2F0*!)z@YX6q{OdkV_6RsHN=Ckm4Voozk48G7@HGnl z!LOS-i;BTzHTJKH(zkAcU-!8zjQc};;Pa&XImAtT#% z`d82LqY+Mx?>tk^kcVu6^*?_QHK-51^H3;?*>^ryk?D!y!<`d|wWK_zZR~4(HzR$cgYr+0d+#UC?gz zvMNfR7W!4C-+#+%z*ybSRCY;8ms|5xxg}6g>y&wGgr)`dJvr-r-U4Es@+*6EILuh5 z98~GJjbZtH(9sqqO`mo?PIc!8Pbj_)t*kY{rLRPQ2oM1xKm>>Y5g-CYppg-%!eb1| zU#Tl^>eJ?8g3$1$>TFf)zV~t#+cmP7zq=#Ev#G=7($tCaw6&g19oIs@`n_jU)1gAV zBn$aF#t<^#U){WV`s(0S(YEt_-Sm?O{>T_*)gfQl%7Y{#Km>>Y5g-CYfCvx)BG8x! z2=DIRI^O34!z$W~`Q*j;#g^JuM$tI5PX5&*0!coSL<@ zUG|EzhphGTJJz4p<8#60*5m%LSso4Y%w|dWPOczPy-&M&>ed0WU#1h zB=bC)X=5KQ^JzRD)Oj5HpEnYeD%uH3M?`j3>ov0H4HgD#wnkPIX3s#8w?r0(#o!my zF*|ClPQVleU+p|%zV~o-!hMQoOlwrDLydHZ01+SpM1Tko0U|&Ih(N<6VCO$1RL3%bN34AyKeDD)T$6pVaI@U3Sm4#B}+@XjIL5iKfE_dBt^wDsmU{FQ~r z+4-g|?J~&tLyAwT&J9Kd$WO^cfCvx)B0vO)01+Sp4U0hCcnPld;+c4jgi-4iWB(Xg zKfDCk`O@NST@3PdZ|q+Fhg?QEYQ#lh%|o}?(ErPPweC7^-xN&O5sMAKyHMmEAr8#% zV#7VrgZPiin}WEgHq`NEHhtx`tmt?KO{zUFdU%-QkW*B%K5S-_3q|6k!BLGoMa~B~ z8m`9Re-Q>fT|DCISbo+Y(w3CMp?bm2^^OgY{V2KK0SHkt5g-CYfCvx)B0vNh7lFE& z4lDvl<_G5~AlJ>l|G{mu!rL%3ukt;5L;P5sq5h~|(_f;;$9_DTnO&>Y5g-CYfCvzQMn>R|I|CW&C9W-( zb6*gC@j&*Fpy$=j;0D6O!k^h7402bK&oc2fm9ZU|gQq&!Nt+P*C9ZrQB(s}46Xad@ z^mq&oY7jG+=hX2GW`~;un2>GIG>Y5g-CYpz#o>;yJg@Ub_p; zb8zpmm#pTc7gKt8~Vj!Hv&8J6u>M+o(=NHGq&W5nUy<2 zyk+X5-D0eNqHMuhuP43jLgCW<-ZLu?D;OUExo%!H9XxT$6_Q>}4t8G1kZKxvSn)>a zoyuy0BQ+vG1c(3;AOb{y2oM1x&;SX@x8TAweG?WfT=;QzUVvS^{m-k1c*j%R$8*L( zZ$WG5BZmAvt@+UGX4iXxA}<8FK2FJnW-9VP>Qk2m-S4ZI^UL8-HP#kZ8patb4Kv*C zvO-r$zQ@V;C-Sr6P!rt(SMcGH>++P~<}Z2f(7@Wc_U`-S6<5dao!&M}ka(FF9zxI~ zGI+&ByeSL@``)Qzk;u;nrg#XhDR_gn`ypY4Zo+Qt<2*ez@W-b#_7NBoX%GP-Km>>Y z5g-CYfCvzQMnXV%Vy(ioWL_aHOgJd&TVOtNTkcA>N*JbZa>Q5~OKrwdPl8&A$2xFt zrTubpy@ul)d3vSb=UiHX13hB&n9Q-`M~@qun>~6wEN+g6#Fp@npM$^Vrr9{o90~3} zGl%P$>Y z5g-CYfCw~n0_hV*k7W@Zuo}Z)hmApO-Yr6oU$~V&k~5ZYXL-*%MI3!LDgt%0Ui=kc zWOn8@De|izFJ71XTCiSAFY;P|AO#{o1c(3;AOb{y2>fvfG?2NK=|2ni>Gz&nxfObb z`Eh&^$c=Mu<=~^?8@Wp(wyt^aC&Ay?x+driCNDkrPiQ3YRhB*>JBvkhM8yj5sRy%8 zC_V*ZH`W=&cR}pI5>WgM`buY(h~f!I?a7i*><$x0U0D|t$AQ?Jorz+lo_ZFFf7esH zqPSg8?S^7(1V52JH_4+?LF~!8V`?sly`fw(ECz7^q|5LMh(lN}6n_P=7pTi{RwP&M zgW^P8G=7_55s3X*3Z`xZu@6MZ5DW8716h9*FVMvSD9#4)EC!7z!YUBEg1QX<1~Cyv zFJ$-<#2)Ny6k|{071Y<(7HZ!=Zwr438-krr-CZOH{5)6jt((dmG&2|w9Vrk2B0vO) z01+SpM1Tkofd)sQfxfd_8P4C{V~o2iH5_j#<@vN4u4P4c2#l!%$oAwl&wNA~+SV7Mo2J zPApTdB0D7!0U|&IhyW2F0z`laG%x}UWXAa8e&T_&h8|2FZ;hIIF!<<;N3#dBT({@- zVEzVjy9P{~^;WTAoj=rdwPU#ky3`d!c%<{p2yBL5ZS z#_OYh5?}g!$a-UKXPmE7d~@DCcpx8LaG*~jKm>>Y5g-CYfCvx)BG8x!G?1~GN6xGt zV>91I+-I!eO$7bxj-#|0cD;2MoyxqohPougJ0U|&IhyW2F0z|-< zfbe>Ltm9wm&{_+p1(#pe`juFQ{uZ?L4D+Lo^3iO>m?}1c(3;AOb{y2oM1xKm-~f zfd;ywbA01+SpM1Tko0U|&Ih(Kc|P__4~u8T$QSB*dA-tUpgUhkI| zhD*Pf_xl8#+wA>5smQBA4!!paM)kA)-rjFX`ZeL)`;E>MB?{fI5^t6@#GT!?fE-+H zdCAUO+@!DV4rVR6Mit=~#Yl^~L*7TNK-1#}0V2`wMns3Mu}S{lvv7V+*)4L3B>< zdVQLi^#hNhhyW2F0z`la5CI}U1c*QbBOttGR>wQIEqp_G;WH$ipuOkz+(3Ak@(<9lQ$gzASDw4B0vO)01+SpM4*8YFuW@LK~K0E z;HU(eV+;$^yibNlXZw0dQTh+EglnZS;)xQqakC=LezT%9j!cA4Gr=YOtyncNK^-1kDwXkR)eC?Oo zXAmTn&OC1b|DMunSu)elQp#JWrW3{Jr{Iy70$&^bN|p-cuNC_aymcZRMO}cIxcz4{-D|8kW=X;#2i6j|?bC|z{2?vE2h=1d53U}zynLm{~ z4}S*dH9LEI6nQVmjdy)~*C!|E*Yz+(?=gT7LVX{ynf}7^jiRLTGkrpK7HhKfz0aJi z8FY~ZcJSSk*RrD180*45dUkLHTLp@J+3a6#o540lGS-J3F6%UleF?D%tj&?@XS1hZ zBkgW%)qsO@8J|Z_Vgo%>Z(*@OVj%m)eGtCFY;&lcWO2tU?`F5aDb8f> z<#*iAW`pioEcui3A7&Gw45zcTb5=dV_QKZ=?CDKg9%ZRup(p#t%%sOyCR|IYY}uR+ zkF$5dSPwR*^4MAy3&wh~|FSz@Ws9JY{aD0-#I0;D6f%ij@zF1DvZoNAgEhO8iozW4BJw^a3j?oL_=bN)-UuUiYOou- z(r@yrvPphe8METW=0Oz1dz%ok^hpGW01+SpM1Tko0V2@Q2^de+;4}!=n;!#?K~#H% zK$`~QZf^akf(aIN{VU>K`iW=jLJk^2&>jbl-+c9hFjybPZ$5+yfj4YS)_3mp6hXY@{7j}!dx9?uCx5+(v@?Iirv?V+-pZ*zD_bS;jM9(yUaB9cjP^*j7YUkBbt8?2EsC=f?q-Uzhny8D zuBDp%VlygtHJi-IbrzdUX1yo0qzWDCJAno|cJ|HQ`m=iDU1($8>v3y^_Z0bkkQ?vV z*>4lFUV#e!F@xRM7~UcSQMrJ|#sVRtbRs|mhyW2F0z`la5CJ04XbAAV{%}{wIhmqb zIkI-qQ^QySIF>Q+mUSGvC^IYbqRg=wnYr1c$4`Kl=)wQxd*K)6d-BVj_#U=B82+L) ze_5r_HO*PV_av06`U@=f+4OV3m>q-!{Zo@0WZ+>%bEZAEFkH+@Xx)b138R8p=2}hO#=a^~HYc z`Ce=-1|(4bABk3ZV~kb)q>oiD9cz`_o@s7e#aQlv%pjWKmBxcM+bfm&_SI5^U#}xr*RS@YQq{ zbbK@i{%C!Is;in$A0~Ve&Cv(zqGZ7}6CUTZQdlig+#TI1&Cyq8>Ex3oV_g4aBLn(d znuG7p&qMo6S4nxP(^XzkGDS9*vDiZO#zYSKtd9ijyf_@;2vp`diZ@5^Cj=r!=|q4C z5CI}U1c(3;AOb|7;S#WV^WcfT9C>%8=D;3T>QB6$WKf@{ynP!!7#Wv=jx5%1~E4VQoS8$_j426hn3`aaL9mh9N zHbyX{zA-`}WgLP)eH>~)*%(HU`o;i(l+pi3eOwWavawZ!)Hk+(kTR|qM}1r|jxr7) zAYWq}2bmvNl%u|}CxcAK73P?3Y_K5HamBfjE?1nRY%CX%`o@Y7DI3c@q--qWkTNdR zKzqh244IBgEHE8cP@s%!Bv4*fD!w5PT)~d%xPl#JT)~bqu3$$QSFoduE7(!S73?U- zE}hxOI+h3THe4CD|6~ER_U#E@`J7i9h0}7gh|9T(~4i9W%z{IWu@B)nd!lKFx zz(s~lHStw=&Cx_;U@^*;o5on01K!@zgVZqI7coZH9JoV52Vuj|sD$F>@{*K$oTWJU zraAa88}iUZS2qUX9O&FM=dYz0dw&{(a*k#q8`IN`lNf_^@Ozgcw6na{VkrcU;AxKL zQaK!yjd46j3mF?JhZ1gnRzzry7@5+^V%%q(1BU=L2j6;v2k&Yp%VwoGHmEty6?*J6 z_f=Or3p}pr&RtiKe=ngl=tFJ2|MuklH|X}K!V>`^Km>>Y5g-CYfCvx)B0vO)z#ox7 zZASx!#Ne=>poha{7>i>@I2!jyWK4A?0z`la5CI}U1c(3;AOekrz>LWSd2vlLVvar5 z?d}M+DLv|~>cn8}`!-$or1sE+G?eK`1c(3;AOb{y2oM1xKm>@u??<4vvjf>QJMjB8 z9-Wy85CI}U1c(3;AOb{y2%KU9=3TsUBcL3GQxOMPs+H97wY3KUbw3h?&5_q?&5{|xQiFcxQiF+ z<1St(<1SvPkGpuGjJtTDKJMa$GVbDq`nZc1%D9Uc>f9~s*+QVJEFdcXC!gSol z3uWBJ3uWB@3iBIVTgeDx7b__n`&CKV*mO$D#?DewHa3WovauPIl&?H*Xm)K!3ntTO afuu Date: Sun, 19 Oct 2025 18:26:33 +0200 Subject: [PATCH 363/448] Improved CHANGELOG.md Update CHANGELOG.md --- CHANGELOG.md | 48 ++++++++++++++++++++++-------------------------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8836e87d..33c24aad8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,46 +50,42 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp ## [Unreleased] - ????-??-?? -If upgrading from v2.x, see the [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/) and [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0). +Until here --> +## [3.1.0] - 2025-10-19 -### ✨ Added -- **Faceting and animation support for plots**: All plotting methods now support `facet_by` and `animate_by` parameters for creating subplot grids and animations with multidimensional data (scenarios, periods, etc.) -- **New `select` parameter**: Added to all plotting methods for flexible data selection using single values, lists, slices, and index arrays -- **Heatmap `fill` parameter**: Added `fill` parameter to heatmap plotting methods to control how missing values are filled after reshaping ('ffill' or 'bfill') -- **Dashed line styling**: Area plots now automatically style "mixed" variables (containing both positive and negative values) with dashed lines, while only stacking purely positive or negative variables +**Summary**: This release adds faceting and animation support for multidimensional plots and redesigns the documentation website. Plotting results across scenarios or periods is now significantly simpler (Plotly only). -### 💥 Breaking Changes +If upgrading from v2.x, see the [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/) and [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0). + +### ✨ Added +- **Faceting and animation for multidimensional plots**: All plotting methods now support `facet_by` and `animate_by` parameters to create subplot grids and animations from multidimensional data (scenarios, periods, etc.). *Plotly only.* +- **Flexible data selection with `select` parameter**: Select data using single values, lists, slices, or index arrays for precise control over what gets plotted +- **Heatmap fill control**: New `fill` parameter in heatmap methods controls how missing values are filled after reshaping (`'ffill'` or `'bfill'`) +- **Smart line styling for mixed variables**: Area plots now automatically style variables containing both positive and negative values with dashed lines, while stacking purely positive or negative variables ### ♻️ Changed -- **Selection behavior**: Changed default selection behavior in plotting methods - no longer automatically selects first value for non-time dimensions. Use `select` parameter for explicit selection -- **Improved error messages**: Enhanced error messages when using matplotlib engine with multidimensional data, providing clearer guidance on dimension requirements -- Improved `scenario_example.py` -- Improved error handling in `plot_heatmap()` method for better dimension validation +- **Breaking: Selection behavior**: Plotting methods no longer automatically select the first value for non-time dimensions. Use the `select` parameter for explicit selection of scenarios, periods, or other dimensions +- **Better error messages**: Enhanced error messages when using Matplotlib with multidimensional data, with clearer guidance on dimension requirements and suggestions to use Plotly +- **Improved examples**: Enhanced `scenario_example.py` with better demonstration of new features +- **Robust validation**: Improved dimension validation in `plot_heatmap()` with clearer error messages ### 🗑️ Deprecated -- **`indexer` parameter**: The `indexer` parameter in all plotting methods is deprecated in favor of the new `select` parameter with enhanced functionality - -### 🔥 Removed +- **`indexer` parameter**: Use the new `select` parameter instead. The `indexer` parameter will be removed in v4.0.0 +- **`heatmap_timeframes` and `heatmap_timesteps_per_frame` parameters**: Use the new `reshape_time=(timeframes, timesteps_per_frame)` parameter instead in heatmap plotting methods +- **`color_map` parameter**: Use the new `colors` parameter instead in heatmap plotting methods ### 🐛 Fixed -- Add error handling for empty buses to prevent cryptic errors -- Add early validation for non-existent periods when using linked periods with tuples +- Fixed cryptic errors when working with empty buses by adding proper validation +- Added early validation for non-existent periods when using linked periods with tuples -### 🔒 Security - -### 📦 Dependencies - -### 📝 Docs -- Improve docs visually with new Material theme and enhanced styling +### 📝 Documentation +- **Redesigned documentation website** with custom css ### 👷 Development -- Renamed `_apply_indexer_to_data()` to `_apply_selection_to_data()` for consistency with new API - -### 🚧 Known Issues +- Renamed internal `_apply_indexer_to_data()` to `_apply_selection_to_data()` for consistency with new API naming --- -Until here --> ## [3.0.3] - 2025-10-16 **Summary**: Hotfixing new plotting parameter `style`. Continue to use `mode`. From 6b587e23ed7fc58438349a568558afe29c6f0e85 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 19 Oct 2025 18:56:24 +0200 Subject: [PATCH 364/448] Readd Changelog Unreleased section --- CHANGELOG.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33c24aad8..d1117dc20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,32 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp ## [Unreleased] - ????-??-?? +If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/). + +### ✨ Added + +### 💥 Breaking Changes + +### ♻️ Changed + +### 🗑️ Deprecated + +### 🔥 Removed + +### 🐛 Fixed + +### 🔒 Security + +### 📦 Dependencies + +### 📝 Docs + +### 👷 Development + +### 🚧 Known Issues + +--- + Until here --> ## [3.1.0] - 2025-10-19 From 9b498e7e6a095c58506f83799db5728b921766f7 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 19 Oct 2025 22:27:10 +0200 Subject: [PATCH 365/448] Convert numpy style docstrings to google style --- flixopt/components.py | 22 ++++++++++++---------- flixopt/effects.py | 24 +++++++++++------------- flixopt/structure.py | 9 +++------ 3 files changed, 26 insertions(+), 29 deletions(-) diff --git a/flixopt/components.py b/flixopt/components.py index c40e6af88..09156e1dc 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -1304,16 +1304,18 @@ def __init__( prevent_simultaneous_flow_rates: bool = False, **kwargs, ): - """ - Initialize a Sink (consumes flow from the system). - - Supports legacy `sink=` keyword for backward compatibility (deprecated): if `sink` is provided it is used as the single input flow and a DeprecationWarning is issued; specifying both `inputs` and `sink` raises ValueError. - - Parameters: - label (str): Unique element label. - inputs (list[Flow], optional): Input flows for the sink. - meta_data (dict, optional): Arbitrary metadata attached to the element. - prevent_simultaneous_flow_rates (bool, optional): If True, prevents simultaneous nonzero flow rates across the element's inputs by wiring that restriction into the base Component setup. + """Initialize a Sink (consumes flow from the system). + + Supports legacy `sink=` keyword for backward compatibility (deprecated): if `sink` is provided + it is used as the single input flow and a DeprecationWarning is issued; specifying both + `inputs` and `sink` raises ValueError. + + Args: + label: Unique element label. + inputs: Input flows for the sink. + meta_data: Arbitrary metadata attached to the element. + prevent_simultaneous_flow_rates: If True, prevents simultaneous nonzero flow rates + across the element's inputs by wiring that restriction into the base Component setup. Note: The deprecated `sink` kwarg is accepted for compatibility but will be removed in future releases. diff --git a/flixopt/effects.py b/flixopt/effects.py index 2c7607b02..6225734fe 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -480,19 +480,17 @@ def add_effects(self, *effects: Effect) -> None: def create_effect_values_dict( self, effect_values_user: PeriodicEffectsUser | TemporalEffectsUser ) -> dict[str, Scalar | TemporalDataUser] | None: - """ - Converts effect values into a dictionary. If a scalar is provided, it is associated with a default effect type. - - Examples - -------- - effect_values_user = 20 -> {'': 20} - effect_values_user = {None: 20} -> {'': 20} - effect_values_user = None -> None - effect_values_user = {'effect1': 20, 'effect2': 0.3} -> {'effect1': 20, 'effect2': 0.3} - - Returns - ------- - dict or None + """Converts effect values into a dictionary. If a scalar is provided, it is associated with a default effect type. + + Examples: + ```python + effect_values_user = 20 -> {'': 20} + effect_values_user = {None: 20} -> {'': 20} + effect_values_user = None -> None + effect_values_user = {'effect1': 20, 'effect2': 0.3} -> {'effect1': 20, 'effect2': 0.3} + ``` + + Returns: A dictionary keyed by effect label, or None if input is None. Note: a standard effect must be defined when passing scalars or None labels. """ diff --git a/flixopt/structure.py b/flixopt/structure.py index 72efc3df2..07c558eee 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -880,13 +880,10 @@ def label_full(self) -> str: @staticmethod def _valid_label(label: str) -> str: - """ - Checks if the label is valid. If not, it is replaced by the default label + """Checks if the label is valid. If not, it is replaced by the default label. - Raises - ------ - ValueError - If the label is not valid + Raises: + ValueError: If the label is not valid. """ not_allowed = ['(', ')', '|', '->', '\\', '-slash-'] # \\ is needed to check for \ if any([sign in label for sign in not_allowed]): From 434eebe75572bf6c63f7958953a89a267e75ee67 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 20 Oct 2025 12:45:02 +0200 Subject: [PATCH 366/448] Fix/effects per component bug (#430) * First try. * Add explicit template creation for effects dataset * Add periods Attribute to CalculationResults * Improve templating for effects dataset * Update CHANGELOG.md * Fix coord order * Fix coord order --- CHANGELOG.md | 1 + flixopt/results.py | 44 ++++++++++++++++++++++++++++++-------------- 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1117dc20..dce533058 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,7 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp ### 🔥 Removed ### 🐛 Fixed +- Fixed ValueError in effects_per_component when all periodic effects are scalars/NaN by explicitly creating mode-specific templates (via _create_template_for_mode) with correct dimensions ### 🔒 Security diff --git a/flixopt/results.py b/flixopt/results.py index a58f0dc1e..75f8f300e 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -230,6 +230,7 @@ def __init__( self.timesteps_extra = self.solution.indexes['time'] self.hours_per_timestep = FlowSystem.calculate_hours_per_timestep(self.timesteps_extra) self.scenarios = self.solution.indexes['scenario'] if 'scenario' in self.solution.indexes else None + self.periods = self.solution.indexes['period'] if 'period' in self.solution.indexes else None self._effect_share_factors = None self._flow_system = None @@ -619,6 +620,30 @@ def _compute_effect_total( total = xr.DataArray(np.nan) return total.rename(f'{element}->{effect}({mode})') + def _create_template_for_mode(self, mode: Literal['temporal', 'periodic', 'total']) -> xr.DataArray: + """Create a template DataArray with the correct dimensions for a given mode. + + Args: + mode: The calculation mode ('temporal', 'periodic', or 'total'). + + Returns: + A DataArray filled with NaN, with dimensions appropriate for the mode. + """ + coords = {} + if mode == 'temporal': + coords['time'] = self.timesteps_extra + if self.periods is not None: + coords['period'] = self.periods + if self.scenarios is not None: + coords['scenario'] = self.scenarios + + # Create template with appropriate shape + if coords: + shape = tuple(len(coords[dim]) for dim in coords) + return xr.DataArray(np.full(shape, np.nan, dtype=float), coords=coords, dims=list(coords.keys())) + else: + return xr.DataArray(np.nan) + def _create_effects_dataset(self, mode: Literal['temporal', 'periodic', 'total']) -> xr.Dataset: """Creates a dataset containing effect totals for all components (including their flows). The dataset does contain the direct as well as the indirect effects of each component. @@ -629,32 +654,23 @@ def _create_effects_dataset(self, mode: Literal['temporal', 'periodic', 'total'] Returns: An xarray Dataset with components as dimension and effects as variables. """ + # Create template with correct dimensions for this mode + template = self._create_template_for_mode(mode) + ds = xr.Dataset() all_arrays = {} - template = None # Template is needed to determine the dimensions of the arrays. This handles the case of no shares for an effect - components_list = list(self.components) - # First pass: collect arrays and find template + # Collect arrays for all effects and components for effect in self.effects: effect_arrays = [] for component in components_list: da = self._compute_effect_total(element=component, effect=effect, mode=mode, include_flows=True) effect_arrays.append(da) - if template is None and (da.dims or not da.isnull().all()): - template = da - all_arrays[effect] = effect_arrays - # Ensure we have a template - if template is None: - raise ValueError( - f"No template with proper dimensions found for mode '{mode}'. " - f'All computed arrays are scalars, which indicates a data issue.' - ) - - # Second pass: process all effects (guaranteed to include all) + # Process all effects: expand scalar NaN arrays to match template dimensions for effect in self.effects: dataarrays = all_arrays[effect] component_arrays = [] From 27e475410fe735c9dd9cec202112f7bbc760a63c Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 20 Oct 2025 12:46:45 +0200 Subject: [PATCH 367/448] Update CHANGELOG.md --- CHANGELOG.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dce533058..060f9b654 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,7 +63,6 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp ### 🔥 Removed ### 🐛 Fixed -- Fixed ValueError in effects_per_component when all periodic effects are scalars/NaN by explicitly creating mode-specific templates (via _create_template_for_mode) with correct dimensions ### 🔒 Security @@ -79,6 +78,16 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp Until here --> +## [3.1.1] - 2025-10-20 +**Summary**: Fixed a bug when acessing the `effects_per_component` dataset in results without periodic effects. + +If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/). + +### 🐛 Fixed +- Fixed ValueError in effects_per_component when all periodic effects are scalars/NaN by explicitly creating mode-specific templates (via _create_template_for_mode) with correct dimensions + +--- + ## [3.1.0] - 2025-10-19 **Summary**: This release adds faceting and animation support for multidimensional plots and redesigns the documentation website. Plotting results across scenarios or periods is now significantly simpler (Plotly only). From 63b39af59fff6fed33dc7fe546ea1e6675737951 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 20 Oct 2025 12:47:40 +0200 Subject: [PATCH 368/448] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 060f9b654..591232bfc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -86,6 +86,9 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp ### 🐛 Fixed - Fixed ValueError in effects_per_component when all periodic effects are scalars/NaN by explicitly creating mode-specific templates (via _create_template_for_mode) with correct dimensions +### 👷 Development +- Converted all remaining numpy style docstrings to google style + --- ## [3.1.0] - 2025-10-19 From 28a6b9a5d40b6950910f140cb654c0ffbdb16deb Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 20 Oct 2025 18:36:07 +0200 Subject: [PATCH 369/448] Fix concurrency testing issue --- CHANGELOG.md | 1 + tests/test_io.py | 14 +++++++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 591232bfc..976e6316b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,6 +71,7 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp ### 📝 Docs ### 👷 Development +- Fixed concurrency issue in CI ### 🚧 Known Issues diff --git a/tests/test_io.py b/tests/test_io.py index f5ca2174a..dbbc4cc72 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -1,3 +1,6 @@ +import uuid + +import numpy as np import pytest import flixopt as fx @@ -31,8 +34,13 @@ def flow_system(request): @pytest.mark.slow -def test_flow_system_file_io(flow_system, highs_solver): - calculation_0 = fx.FullCalculation('IO', flow_system=flow_system) +def test_flow_system_file_io(flow_system, highs_solver, request): + # Use UUID to ensure unique names across parallel test workers + unique_id = uuid.uuid4().hex[:12] + worker_id = getattr(request.config, 'workerinput', {}).get('workerid', 'main') + test_id = f'{worker_id}-{unique_id}' + + calculation_0 = fx.FullCalculation(f'IO-{test_id}', flow_system=flow_system) calculation_0.do_modeling() calculation_0.solve(highs_solver) calculation_0.flow_system.plot_network() @@ -41,7 +49,7 @@ def test_flow_system_file_io(flow_system, highs_solver): paths = CalculationResultsPaths(calculation_0.folder, calculation_0.name) flow_system_1 = fx.FlowSystem.from_netcdf(paths.flow_system) - calculation_1 = fx.FullCalculation('Loaded_IO', flow_system=flow_system_1) + calculation_1 = fx.FullCalculation(f'Loaded_IO-{test_id}', flow_system=flow_system_1) calculation_1.do_modeling() calculation_1.solve(highs_solver) calculation_1.flow_system.plot_network() From 4296cde9a7dc214cd9b0d2e5fb8e8f2da3b947b7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 20 Oct 2025 21:01:54 +0200 Subject: [PATCH 370/448] chore(deps): update dependency astral-sh/uv to v0.9.2 (#425) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/python-app.yaml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml index d8caba0d4..30f5ddbf8 100644 --- a/.github/workflows/python-app.yaml +++ b/.github/workflows/python-app.yaml @@ -35,7 +35,7 @@ jobs: - name: Set up uv uses: astral-sh/setup-uv@v6 with: - version: "0.8.23" + version: "0.9.2" enable-cache: true - name: Set up Python @@ -75,7 +75,7 @@ jobs: - name: Set up uv uses: astral-sh/setup-uv@v6 with: - version: "0.8.23" + version: "0.9.2" enable-cache: true - name: Set up Python ${{ matrix.python-version }} @@ -104,7 +104,7 @@ jobs: - name: Set up uv uses: astral-sh/setup-uv@v6 with: - version: "0.8.23" + version: "0.9.2" enable-cache: true - name: Set up Python ${{ env.PYTHON_VERSION }} @@ -130,7 +130,7 @@ jobs: - name: Set up uv uses: astral-sh/setup-uv@v6 with: - version: "0.8.23" + version: "0.9.2" enable-cache: true - name: Set up Python @@ -170,7 +170,7 @@ jobs: - name: Set up uv uses: astral-sh/setup-uv@v6 with: - version: "0.8.23" + version: "0.9.2" enable-cache: true - name: Set up Python @@ -212,7 +212,7 @@ jobs: - name: Set up uv uses: astral-sh/setup-uv@v6 with: - version: "0.8.23" + version: "0.9.2" enable-cache: true - name: Set up Python @@ -298,7 +298,7 @@ jobs: - name: Set up uv uses: astral-sh/setup-uv@v6 with: - version: "0.8.23" + version: "0.9.2" enable-cache: true - name: Set up Python @@ -379,7 +379,7 @@ jobs: - name: Set up uv uses: astral-sh/setup-uv@v6 with: - version: "0.8.23" + version: "0.9.2" enable-cache: true - name: Set up Python From dbec1bbc654110cf67eb75dcb90b28c639c9dd66 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 21 Oct 2025 13:35:42 +0200 Subject: [PATCH 371/448] Improve examples --- examples/01_Simple/simple_example.py | 2 +- examples/03_Calculation_types/example_calculation_types.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/01_Simple/simple_example.py b/examples/01_Simple/simple_example.py index ee90af47a..906c24622 100644 --- a/examples/01_Simple/simple_example.py +++ b/examples/01_Simple/simple_example.py @@ -114,7 +114,7 @@ # --- Analyze Results --- calculation.results['Fernwärme'].plot_node_balance_pie() calculation.results['Fernwärme'].plot_node_balance() - calculation.results['Storage'].plot_node_balance() + calculation.results['Storage'].plot_charge_state() calculation.results.plot_heatmap('CHP(Q_th)|flow_rate') # Convert the results for the storage component to a dataframe and display diff --git a/examples/03_Calculation_types/example_calculation_types.py b/examples/03_Calculation_types/example_calculation_types.py index 3f9ae665b..8df8e742f 100644 --- a/examples/03_Calculation_types/example_calculation_types.py +++ b/examples/03_Calculation_types/example_calculation_types.py @@ -36,7 +36,7 @@ data_import = pd.read_csv( pathlib.Path(__file__).parent.parent / 'resources' / 'Zeitreihen2020.csv', index_col=0 ).sort_index() - filtered_data = data_import['2020-01-01':'2020-01-02 23:45:00'] + filtered_data = data_import['2020-01-01':'2020-01-07 23:45:00'] # filtered_data = data_import[0:500] # Alternatively filter by index filtered_data.index = pd.to_datetime(filtered_data.index) From 4305ed222acb4fd3528434637929a90c1e04281b Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 22 Oct 2025 17:55:26 +0200 Subject: [PATCH 372/448] Fix docstring --- CHANGELOG.md | 1 + flixopt/interface.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 976e6316b..dda77c01a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,7 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp ### 📦 Dependencies ### 📝 Docs +- Moved `linked_periods` into correct section of the docstring (was in deprecated params) ### 👷 Development - Fixed concurrency issue in CI diff --git a/flixopt/interface.py b/flixopt/interface.py index 3b7d24da7..8264e2392 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -712,6 +712,8 @@ class InvestParameters(Interface): Combinable with effects_of_investment and effects_of_investment_per_size. effects_of_retirement: Costs incurred if NOT investing (demolition, penalties). Dict: {'effect_name': value}. + linked_periods: Describes which periods are linked. 1 means linked, 0 means size=0. None means no linked periods. + For convenience, pass a tuple containing the first and last period (2025, 2039), linking them and those in between Deprecated Args: fix_effects: **Deprecated**. Use `effects_of_investment` instead. @@ -724,7 +726,6 @@ class InvestParameters(Interface): Will be removed in version 4.0. optional: DEPRECATED. Use `mandatory` instead. Opposite of `mandatory`. Will be removed in version 4.0. - linked_periods: Describes which periods are linked. 1 means linked, 0 means size=0. None means no linked periods. Cost Annualization Requirements: All cost values must be properly weighted to match the optimization model's time horizon. From feb0ced8201058e50031495ebcf895fc8c0b6d2b Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 24 Oct 2025 12:27:09 +0200 Subject: [PATCH 373/448] Feature/plotting kwargs and streamline data conversion (#439) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feature/398 feature facet plots in results (#419) * Add animation and faceting options to plots * Adjust size of the frame * Utilize plotly express directly * Rmeocve old class * Use plotly express and modify stackgroup afterwards * Add modifications also to animations * Mkae more compact * Remove height stuff * Remove line and make set opacity =0 for area * Integrate faceting and animating into existing with_plotly method * Improve results.py * Improve results.py * Move check if dims are found to plotting.py * Fix usage of indexer * Change selection string with indexer * Change behaviout of parameter "indexing" * Update CHANGELOG.md * Add new selection parameter to plotting methods * deprectae old indexer parameter * deprectae old indexer parameter * Add test * Add test * Add test * Add test * Fix not supportet check for matplotlib * Typo in CHANGELOG.md * Feature/398 feature facet plots in results heatmaps (#418) * Add animation and faceting options to plots * Adjust size of the frame * Utilize plotly express directly * Rmeocve old class * Use plotly express and modify stackgroup afterwards * Add modifications also to animations * Mkae more compact * Remove height stuff * Remove line and make set opacity =0 for area * Integrate faceting and animating into existing with_plotly method * Improve results.py * Improve results.py * Move check if dims are found to plotting.py * Fix usage of indexer * Change selection string with indexer * Change behaviout of parameter "indexing" * Update CHANGELOG.md * Add new selection parameter to plotting methods * deprectae old indexer parameter * deprectae old indexer parameter * Add test * Add test * Add test * Add test * Add heatmap support * Unify to a single heatmap method per engine * Change defaults * readd time reshaping * readd time reshaping * lengthen scenario example * Update * Improve heatmap plotting * Improve heatmap plotting * Moved reshaping to plotting.py * COmbinations are possible! * Improve 'auto'behavioour * Improve 'auto' behavioour * Improve 'auto' behavioour * Allow multiple variables in a heatmap * Update modeule level plot_heatmap() * remove code duplication * Allow Dataset instead of List of DataArrays * Allow Dataset instead of List of DataArrays * Update plot tests * FIx Missing renme in ElementResults.plot_heatmap() * Update API * Feature/398 feature facet plots in results charge state (#417) * Add animation and faceting options to plots * Adjust size of the frame * Utilize plotly express directly * Rmeocve old class * Use plotly express and modify stackgroup afterwards * Add modifications also to animations * Mkae more compact * Remove height stuff * Remove line and make set opacity =0 for area * Integrate faceting and animating into existing with_plotly method * Improve results.py * Improve results.py * Move check if dims are found to plotting.py * Fix usage of indexer * Change selection string with indexer * Change behaviout of parameter "indexing" * Update CHANGELOG.md * Add new selection parameter to plotting methods * deprectae old indexer parameter * deprectae old indexer parameter * Add test * Add test * Add test * Add test * Add heatmap support * Unify to a single heatmap method per engine * Change defaults * readd time reshaping * readd time reshaping * lengthen scenario example * Update * Improve heatmap plotting * Improve heatmap plotting * Moved reshaping to plotting.py * COmbinations are possible! * Improve 'auto'behavioour * Improve 'auto' behavioour * Improve 'auto' behavioour * Allow multiple variables in a heatmap * Update modeule level plot_heatmap() * remove code duplication * Allow Dataset instead of List of DataArrays * Allow Dataset instead of List of DataArrays * Add tests * More examples * Update plot_charge state() * Try 1 * Try 2 * Add more examples * Add more examples * Add smooth line for charge state and use "area" as default * Update scenario_example.py * Update tests * Fix Error handling in plot_heatmap() * Feature/398 feature facet plots in results pie (#421) * Add animation and faceting options to plots * Adjust size of the frame * Utilize plotly express directly * Rmeocve old class * Use plotly express and modify stackgroup afterwards * Add modifications also to animations * Mkae more compact * Remove height stuff * Remove line and make set opacity =0 for area * Integrate faceting and animating into existing with_plotly method * Improve results.py * Improve results.py * Move check if dims are found to plotting.py * Fix usage of indexer * Change selection string with indexer * Change behaviout of parameter "indexing" * Update CHANGELOG.md * Add new selection parameter to plotting methods * deprectae old indexer parameter * deprectae old indexer parameter * Add test * Add test * Add test * Add test * Add heatmap support * Unify to a single heatmap method per engine * Change defaults * readd time reshaping * readd time reshaping * lengthen scenario example * Update * Improve heatmap plotting * Improve heatmap plotting * Moved reshaping to plotting.py * COmbinations are possible! * Improve 'auto'behavioour * Improve 'auto' behavioour * Improve 'auto' behavioour * Allow multiple variables in a heatmap * Update modeule level plot_heatmap() * remove code duplication * Allow Dataset instead of List of DataArrays * Allow Dataset instead of List of DataArrays * Add tests * More examples * Update plot_charge state() * Try 1 * Try 2 * Add more examples * Add more examples * Add smooth line for charge state and use "area" as default * Update scenario_example.py * Update tests * Handle extra dims in pie plots by selecting the first * 6. Optimized time-step check - Replaced pandas Series diff() with NumPy np.diff() for better performance - Changed check from > 0 to > 1 (can't calculate diff with 0 or 1 element) - Converted to seconds first, then to minutes to avoid pandas timedelta conversion issues * Typo * Improve type handling * Update other tests * Handle backwards compatability * Add better error messages if both new and old api are used * Add old api explicitly * Add old api explicitly * Improve consistency and properly deprectae the indexer parameter * Remove amount of new tests * Remove amount of new tests * Fix CONTRIBUTING.md * Remove old test file * Add tests/test_heatmap_reshape.py * Add tests/test_heatmap_reshape.py * Remove unused method * - Implemented dashed line styling for "mixed" variables (variables with both positive and negative values) - Only stack "positive" and "negative" classifications, not "mixed" or "zero" * - Added fill parameter to module-level plot_heatmap function (line 1914) - Added fill parameter to CalculationResults.plot_heatmap method (line 702) - Forwarded fill parameter to both heatmap_with_plotly and heatmap_with_matplotlib functions * - Added np.random.seed(42) for reproducible test results - Added specific size assertions to all tests: - Daily/hourly pattern: 3 days × 24 hours - Weekly/daily pattern: 1 week × 7 days - Irregular data: 25 hours × 60 minutes - Multidimensional: 2 days × 24 hours with preserved scenario dimension * Improve Error Message if too many dims for matplotlib * Improve Error Message if too many dims for matplotlib * Improve Error Message if too many dims for matplotlib * Rename _apply_indexer_to_data() to _apply_selection_to_data() * Bugfix * Update CHANGELOG.md * Catch edge case in with_plotly() * Add strict=True * Improve scenario_example.py * Improve scenario_example.py * Change logging level in essage about time reshape * Update CHANGELOG.md * Add XarrayColorMapper * Add XarrayColorMapper to CalculationResults * Renamed variable * Add test file * Improve integration of the ColorMapper * Improve integration of the ColorMapper * Update resolve_colors and move to plotting.py * Temporalily add example script to show/document intended usage * Add method create_color_mapper * Improve docstring * Remove example file again * Update CHANGELOG.md * Add create_color_mapper to SegmentedResults * Add create_color_mapper to complex_example * Missed some renames * Fix warning in plot_charge_state() * Allow for discrete color assignments with rules * Remove some half baked validation * Add more color families * Use 1:7 colors for more distinct colors * Add color mapper to complex example * Update CHANGELOG.md * Convert numpy style docstrings to google style * Use re.search instead of re.match * Update tests * Applying ordering to Dataset as well * This approach: - Prevents silent data loss when values like 1, 1.0, and "1" collide - Provides actionable error messages showing exactly which values are problematic - Allows users to fix their data rather than hiding the issue * Improve Error Message * Enable sorting fpr Datasets * completed the integration of XarrayColorMapper into both with_plotly and with_matplotlib * simplified with_matplotlib significantly * Update plotting methods to focus on xr.DataArray only * Remove duplication * Remove duplication * Make check faster * Make check faster * Make check faster * Fixx plotting issues * Switch back to Dataset first * Remove redundant code * XarrayColorMapper is now Dataset-only! * Update tests accordingly * Fix issue in aggregation.py with new plotting * Fix issue plotting in examples (using dataframes) * Fix issue plotting in examples (using dataframes) * Fix issue plotting scalar Datasets * Improve labeling of plots * Improve handling of time reshape in plots * Update usage of plotting methods * Update pie plots to use Dataset instead of DataFrame * Makde charge state line in plots black always * Improve examples * Make plotting methods much more flexible * Add test * Add plotting kwargs to plotting functions * add imshow kwargs * Fix nans in plots * Replace XarrayColorMapper with ComponentColorManager * Add repr and str method * Added tests * Add caching to ColorManager * Test caching * Change default colormap and improve colormap settings * Automatically initiallize the ColorManager * Use Dark24 as the default colormap * Rename auto_group_components() to apply_colors() * Use ColorManager in examples * Fix tests * Extend config to cover plotting settings * Centralize behaviour * More config options * More config options * More config options * Rename config parameter * Improve color defaults * Removed 'auto' and Simplified Color Resolution * Fix ColorProcessor to accept qualitative colorscales * Fix ColorProcessor to accept qualitative colorscales * Remove old test * Simplified tests * Update examples and CHANGELOG.md * Update method name * Update examples * extended ComponentColorManager to support flow-level color distinctio * Change default flow shading * Improve Setup_colors * Use external dependency for color handling * Make colour dependency optional * streamlined the ColorManager configuration API * streamlined the ColorManager configuration API * Update usages of new api * Update CHANGELOG.md * Update examples * use turbo as the new default sequential colormap * Add support for direct mappings of components * Add support for direct mappings of components * Add configurable flow_variation * Update tests * Make color getter mroe robust * Make color getter mroe robust * Temp * Update default colormap * Update default colormap to default colorscale * Update default Improve colorscale handling * Update default color families * Update plotly template * Update example * Simplify test * Update color family defaults * Simplify documentation * Typo * Make matplotlib backend switch more robst * Update setup_colors() in SegmentedCalculationResults * Fix example * Update CHANGELOG.md * Simplify export_figure() * Update Examples and CHANGELOG.md * Simplified Config * Simplified Colormanagement * Simplified Colormanagement * Simplified Colormanagement * Add element name itself to color dict * Fix examples * Bugfix * Bugfix * Remove coloring related stuff * Reverse color and CONFIG related changes in plotting.py * Reverse color and CONFIG related changes in plotting.py * Reverse color and CONFIG related changes in plotting.py * Reverse color and CONFIG related changes in plotting.py * Reverse color and CONFIG related changes in plotting.py * Update CHANGELOG.md * Update CHANGELOG.md * Remove duplicate resolve color calls * Improve pie plot * Simplify pie plot * Simplify pie plot * Bugfix * Bugfix * Bugfix * Bugfix * Bugfix * Add Series Support for plotting * Simplify pie plots to be Series First to remove unnessesary data transformations * Update Typehints * Test datatypes * Test datatypes and plotting modes * Test datatypes and plotting modes * Test datatypes and plotting modes * Test datatypes and plotting modes * Update CHANGELOG.md * Potential Bugfix * Fix: plotly kwargs usage * Update docstrings and fix usage of kwargs * Improve error handling * Remove trace and layout kwargs * Prevent potential bugs in plotting.py * Improve tests --- CHANGELOG.md | 6 + .../02_Complex/complex_example_results.py | 5 +- .../example_calculation_types.py | 13 +- flixopt/aggregation.py | 10 +- flixopt/plotting.py | 1065 ++++++++--------- flixopt/results.py | 226 +++- tests/test_plotting_api.py | 138 +++ 7 files changed, 899 insertions(+), 564 deletions(-) create mode 100644 tests/test_plotting_api.py diff --git a/CHANGELOG.md b/CHANGELOG.md index dda77c01a..ca9600f04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,16 +53,22 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/). ### ✨ Added +- Support for plotting kwargs in `results.py`, passed to plotly express and matplotlib. ### 💥 Breaking Changes ### ♻️ Changed +- **Template integration**: Plotly templates now fully control plot styling without hardcoded overrides +- **Dataset first plotting**: Underlying plotting methods in `plotting.py` now use `xr.Dataset` as the main datatype. DataFrames are automatically converted via `_ensure_dataset()`. Both DataFrames and Datasets can be passed to plotting functions without code changes. ### 🗑️ Deprecated ### 🔥 Removed +- Removed `plotting.pie_with_plotly()` method as it was not used ### 🐛 Fixed +- Improved error messages for `engine='matplotlib'` with multidimensional data +- Better dimension validation in `results.plot_heatmap()` ### 🔒 Security diff --git a/examples/02_Complex/complex_example_results.py b/examples/02_Complex/complex_example_results.py index 5020f71fe..96d06dd04 100644 --- a/examples/02_Complex/complex_example_results.py +++ b/examples/02_Complex/complex_example_results.py @@ -25,8 +25,9 @@ # --- Detailed Plots --- # In depth plot for individual flow rates ('__' is used as the delimiter between Component and Flow results.plot_heatmap('Wärmelast(Q_th_Last)|flow_rate') - for flow_rate in results['BHKW2'].inputs + results['BHKW2'].outputs: - results.plot_heatmap(flow_rate) + for bus in results.buses.values(): + bus.plot_node_balance_pie(show=False, save=f'results/{bus.label}--pie.html') + bus.plot_node_balance(show=False, save=f'results/{bus.label}--balance.html') # --- Plotting internal variables manually --- results.plot_heatmap('BHKW2(Q_th)|on') diff --git a/examples/03_Calculation_types/example_calculation_types.py b/examples/03_Calculation_types/example_calculation_types.py index 8df8e742f..c5df50034 100644 --- a/examples/03_Calculation_types/example_calculation_types.py +++ b/examples/03_Calculation_types/example_calculation_types.py @@ -202,35 +202,38 @@ def get_solutions(calcs: list, variable: str) -> xr.Dataset: # --- Plotting for comparison --- fx.plotting.with_plotly( - get_solutions(calculations, 'Speicher|charge_state').to_dataframe(), + get_solutions(calculations, 'Speicher|charge_state'), mode='line', title='Charge State Comparison', ylabel='Charge state', + xlabel='Time in h', ).write_html('results/Charge State.html') fx.plotting.with_plotly( - get_solutions(calculations, 'BHKW2(Q_th)|flow_rate').to_dataframe(), + get_solutions(calculations, 'BHKW2(Q_th)|flow_rate'), mode='line', title='BHKW2(Q_th) Flow Rate Comparison', ylabel='Flow rate', + xlabel='Time in h', ).write_html('results/BHKW2 Thermal Power.html') fx.plotting.with_plotly( - get_solutions(calculations, 'costs(temporal)|per_timestep').to_dataframe(), + get_solutions(calculations, 'costs(temporal)|per_timestep'), mode='line', title='Operation Cost Comparison', ylabel='Costs [€]', + xlabel='Time in h', ).write_html('results/Operation Costs.html') fx.plotting.with_plotly( - pd.DataFrame(get_solutions(calculations, 'costs(temporal)|per_timestep').to_dataframe().sum()).T, + get_solutions(calculations, 'costs(temporal)|per_timestep').sum('time'), mode='stacked_bar', title='Total Cost Comparison', ylabel='Costs [€]', ).update_layout(barmode='group').write_html('results/Total Costs.html') fx.plotting.with_plotly( - pd.DataFrame([calc.durations for calc in calculations], index=[calc.name for calc in calculations]), + pd.DataFrame([calc.durations for calc in calculations], index=[calc.name for calc in calculations]).to_xarray(), mode='stacked_bar', ).update_layout(title='Duration Comparison', xaxis_title='Calculation type', yaxis_title='Time (s)').write_html( 'results/Speed Comparison.html' diff --git a/flixopt/aggregation.py b/flixopt/aggregation.py index 91ef618a9..53770e140 100644 --- a/flixopt/aggregation.py +++ b/flixopt/aggregation.py @@ -150,13 +150,17 @@ def plot(self, colormap: str = 'viridis', show: bool = True, save: pathlib.Path df_agg = self.aggregated_data.copy().rename( columns={col: f'Aggregated - {col}' for col in self.aggregated_data.columns} ) - fig = plotting.with_plotly(df_org, 'line', colors=colormap) + fig = plotting.with_plotly(df_org.to_xarray(), 'line', colors=colormap, xlabel='Time in h') for trace in fig.data: trace.update(dict(line=dict(dash='dash'))) - fig = plotting.with_plotly(df_agg, 'line', colors=colormap, fig=fig) + fig2 = plotting.with_plotly(df_agg.to_xarray(), 'line', colors=colormap, xlabel='Time in h') + for trace in fig2.data: + fig.add_trace(trace) fig.update_layout( - title='Original vs Aggregated Data (original = ---)', xaxis_title='Index', yaxis_title='Value' + title='Original vs Aggregated Data (original = ---)', + xaxis_title='Time in h', + yaxis_title='Value', ) plotting.export_figure( diff --git a/flixopt/plotting.py b/flixopt/plotting.py index bd1f3c2c4..a024c97fc 100644 --- a/flixopt/plotting.py +++ b/flixopt/plotting.py @@ -42,6 +42,8 @@ import xarray as xr from plotly.exceptions import PlotlyError +from .config import CONFIG + if TYPE_CHECKING: import pyvis @@ -326,36 +328,99 @@ def process_colors( return color_list +def _ensure_dataset(data: xr.Dataset | pd.DataFrame | pd.Series) -> xr.Dataset: + """Convert DataFrame or Series to Dataset if needed.""" + if isinstance(data, xr.Dataset): + return data + elif isinstance(data, pd.DataFrame): + # Convert DataFrame to Dataset + return data.to_xarray() + elif isinstance(data, pd.Series): + # Convert Series to DataFrame first, then to Dataset + return data.to_frame().to_xarray() + else: + raise TypeError(f'Data must be xr.Dataset, pd.DataFrame, or pd.Series, got {type(data).__name__}') + + +def _validate_plotting_data(data: xr.Dataset, allow_empty: bool = False) -> None: + """Validate dataset for plotting (checks for empty data, non-numeric types, etc.).""" + # Check for empty data + if not allow_empty and len(data.data_vars) == 0: + raise ValueError('Empty Dataset provided (no variables). Cannot create plot.') + + # Check if dataset has any data (xarray uses nbytes for total size) + if all(data[var].size == 0 for var in data.data_vars) if len(data.data_vars) > 0 else True: + if not allow_empty and len(data.data_vars) > 0: + raise ValueError('Dataset has zero size. Cannot create plot.') + if len(data.data_vars) == 0: + return # Empty dataset, nothing to validate + return + + # Check for non-numeric data types + for var in data.data_vars: + dtype = data[var].dtype + if not np.issubdtype(dtype, np.number): + raise TypeError( + f"Variable '{var}' has non-numeric dtype '{dtype}'. " + f'Plotting requires numeric data types (int, float, etc.).' + ) + + # Warn about NaN/Inf values + for var in data.data_vars: + if np.isnan(data[var].values).any(): + logger.debug(f"Variable '{var}' contains NaN values which may affect visualization.") + if np.isinf(data[var].values).any(): + logger.debug(f"Variable '{var}' contains Inf values which may affect visualization.") + + +def resolve_colors( + data: xr.Dataset, + colors: ColorType, + engine: PlottingEngine = 'plotly', +) -> dict[str, str]: + """Resolve colors parameter to a dict mapping variable names to colors.""" + # Get variable names from Dataset (always strings and unique) + labels = list(data.data_vars.keys()) + + # If explicit dict provided, use it directly + if isinstance(colors, dict): + return colors + + # If string or list, use ColorProcessor (traditional behavior) + if isinstance(colors, (str, list)): + processor = ColorProcessor(engine=engine) + return processor.process_colors(colors, labels, return_mapping=True) + + raise TypeError(f'Wrong type passed to resolve_colors(): {type(colors)}') + + def with_plotly( - data: pd.DataFrame | xr.DataArray | xr.Dataset, + data: xr.Dataset | pd.DataFrame | pd.Series, mode: Literal['stacked_bar', 'line', 'area', 'grouped_bar'] = 'stacked_bar', colors: ColorType = 'viridis', title: str = '', ylabel: str = '', - xlabel: str = 'Time in h', - fig: go.Figure | None = None, + xlabel: str = '', facet_by: str | list[str] | None = None, animate_by: str | None = None, - facet_cols: int = 3, + facet_cols: int | None = None, shared_yaxes: bool = True, shared_xaxes: bool = True, + **px_kwargs: Any, ) -> go.Figure: """ Plot data with Plotly using facets (subplots) and/or animation for multidimensional data. Uses Plotly Express for convenient faceting and animation with automatic styling. - For simple plots without faceting, can optionally add to an existing figure. Args: - data: A DataFrame or xarray DataArray/Dataset to plot. + data: An xarray Dataset, pandas DataFrame, or pandas Series to plot. mode: The plotting mode. Use 'stacked_bar' for stacked bar charts, 'line' for lines, 'area' for stacked area charts, or 'grouped_bar' for grouped bar charts. colors: Color specification (colormap, list, or dict mapping labels to colors). title: The main title of the plot. ylabel: The label for the y-axis. xlabel: The label for the x-axis. - fig: A Plotly figure object to plot on (only for simple plots without faceting). - If not provided, a new figure will be created. facet_by: Dimension(s) to create facets for. Creates a subplot grid. Can be a single dimension name or list of dimensions (max 2 for facet_row and facet_col). If the dimension doesn't exist in the data, it will be silently ignored. @@ -364,93 +429,113 @@ def with_plotly( facet_cols: Number of columns in the facet grid (used when facet_by is single dimension). shared_yaxes: Whether subplots share y-axes. shared_xaxes: Whether subplots share x-axes. + **px_kwargs: Additional keyword arguments passed to the underlying Plotly Express function + (px.bar, px.line, px.area). These override default arguments if provided. + Examples: range_x=[0, 100], range_y=[0, 50], category_orders={...}, line_shape='linear' Returns: - A Plotly figure object containing the faceted/animated plot. + A Plotly figure object containing the faceted/animated plot. You can further customize + the returned figure using Plotly's methods (e.g., fig.update_traces(), fig.update_layout()). Examples: Simple plot: ```python - fig = with_plotly(df, mode='area', title='Energy Mix') + fig = with_plotly(dataset, mode='area', title='Energy Mix') ``` Facet by scenario: ```python - fig = with_plotly(ds, facet_by='scenario', facet_cols=2) + fig = with_plotly(dataset, facet_by='scenario', facet_cols=2) ``` Animate by period: ```python - fig = with_plotly(ds, animate_by='period') + fig = with_plotly(dataset, animate_by='period') ``` Facet and animate: ```python - fig = with_plotly(ds, facet_by='scenario', animate_by='period') + fig = with_plotly(dataset, facet_by='scenario', animate_by='period') + ``` + + Customize with Plotly Express kwargs: + + ```python + fig = with_plotly(dataset, range_y=[0, 100], line_shape='linear') + ``` + + Further customize the returned figure: + + ```python + fig = with_plotly(dataset, mode='line') + fig.update_traces(line={'width': 5, 'dash': 'dot'}) + fig.update_layout(template='plotly_dark', width=1200, height=600) ``` """ if mode not in ('stacked_bar', 'line', 'area', 'grouped_bar'): raise ValueError(f"'mode' must be one of {{'stacked_bar','line','area', 'grouped_bar'}}, got {mode!r}") + # Ensure data is a Dataset and validate it + data = _ensure_dataset(data) + _validate_plotting_data(data, allow_empty=True) + # Handle empty data - if isinstance(data, pd.DataFrame) and data.empty: - return go.Figure() - elif isinstance(data, xr.DataArray) and data.size == 0: - return go.Figure() - elif isinstance(data, xr.Dataset) and len(data.data_vars) == 0: + if len(data.data_vars) == 0: + logger.error('with_plotly() got an empty Dataset.') return go.Figure() - # Warn if fig parameter is used with faceting - if fig is not None and (facet_by is not None or animate_by is not None): - logger.warning('The fig parameter is ignored when using faceting or animation. Creating a new figure.') - fig = None - - # Convert xarray to long-form DataFrame for Plotly Express - if isinstance(data, (xr.DataArray, xr.Dataset)): - # Convert to long-form (tidy) DataFrame - # Structure: time, variable, value, scenario, period, ... (all dims as columns) - if isinstance(data, xr.Dataset): - # Stack all data variables into long format - df_long = data.to_dataframe().reset_index() - # Melt to get: time, scenario, period, ..., variable, value - id_vars = [dim for dim in data.dims] - value_vars = list(data.data_vars) - df_long = df_long.melt(id_vars=id_vars, value_vars=value_vars, var_name='variable', value_name='value') - else: - # DataArray - df_long = data.to_dataframe().reset_index() - if data.name: - df_long = df_long.rename(columns={data.name: 'value'}) - else: - # Unnamed DataArray, find the value column - non_dim_cols = [col for col in df_long.columns if col not in data.dims] - if len(non_dim_cols) != 1: - raise ValueError( - f'Expected exactly one non-dimension column for unnamed DataArray, ' - f'but found {len(non_dim_cols)}: {non_dim_cols}' + # Handle all-scalar datasets (where all variables have no dimensions) + # This occurs when all variables are scalar values with dims=() + if all(len(data[var].dims) == 0 for var in data.data_vars): + # Create a simple DataFrame with variable names as x-axis + variables = list(data.data_vars.keys()) + values = [float(data[var].values) for var in data.data_vars] + + # Resolve colors + color_discrete_map = resolve_colors(data, colors, engine='plotly') + marker_colors = [color_discrete_map.get(var, '#636EFA') for var in variables] + + # Create simple plot based on mode using go (not px) for better color control + if mode in ('stacked_bar', 'grouped_bar'): + fig = go.Figure(data=[go.Bar(x=variables, y=values, marker_color=marker_colors)]) + elif mode == 'line': + fig = go.Figure( + data=[ + go.Scatter( + x=variables, + y=values, + mode='lines+markers', + marker=dict(color=marker_colors, size=8), + line=dict(color='lightgray'), ) - value_col = non_dim_cols[0] - df_long = df_long.rename(columns={value_col: 'value'}) - df_long['variable'] = data.name or 'data' - else: - # Already a DataFrame - convert to long format for Plotly Express - df_long = data.reset_index() - if 'time' not in df_long.columns: - # First column is probably time - df_long = df_long.rename(columns={df_long.columns[0]: 'time'}) - # Melt to long format - id_vars = [ - col - for col in df_long.columns - if col in ['time', 'scenario', 'period'] - or col in (facet_by if isinstance(facet_by, list) else [facet_by] if facet_by else []) - ] - value_vars = [col for col in df_long.columns if col not in id_vars] - df_long = df_long.melt(id_vars=id_vars, value_vars=value_vars, var_name='variable', value_name='value') + ] + ) + elif mode == 'area': + fig = go.Figure( + data=[ + go.Scatter( + x=variables, + y=values, + fill='tozeroy', + marker=dict(color=marker_colors, size=8), + line=dict(color='lightgray'), + ) + ] + ) + else: + raise ValueError('"mode" must be one of "stacked_bar", "grouped_bar", "line", "area"') + + fig.update_layout(title=title, xaxis_title=xlabel, yaxis_title=ylabel, showlegend=False) + return fig + + # Convert Dataset to long-form DataFrame for Plotly Express + # Structure: time, variable, value, scenario, period, ... (all dims as columns) + dim_names = list(data.dims) + df_long = data.to_dataframe().reset_index().melt(id_vars=dim_names, var_name='variable', value_name='value') # Validate facet_by and animate_by dimensions exist in the data available_dims = [col for col in df_long.columns if col not in ['variable', 'value']] @@ -505,10 +590,32 @@ def with_plotly( processed_colors = ColorProcessor(engine='plotly').process_colors(colors, all_vars) color_discrete_map = {var: color for var, color in zip(all_vars, processed_colors, strict=True)} + # Determine which dimension to use for x-axis + # Collect dimensions used for faceting and animation + used_dims = set() + if facet_row: + used_dims.add(facet_row) + if facet_col: + used_dims.add(facet_col) + if animate_by: + used_dims.add(animate_by) + + # Find available dimensions for x-axis (not used for faceting/animation) + x_candidates = [d for d in available_dims if d not in used_dims] + + # Use 'time' if available, otherwise use the first available dimension + if 'time' in x_candidates: + x_dim = 'time' + elif len(x_candidates) > 0: + x_dim = x_candidates[0] + else: + # Fallback: use the first dimension (shouldn't happen in normal cases) + x_dim = available_dims[0] if available_dims else 'time' + # Create plot using Plotly Express based on mode common_args = { 'data_frame': df_long, - 'x': 'time', + 'x': x_dim, 'y': 'value', 'color': 'variable', 'facet_row': facet_row, @@ -516,13 +623,22 @@ def with_plotly( 'animation_frame': animate_by, 'color_discrete_map': color_discrete_map, 'title': title, - 'labels': {'value': ylabel, 'time': xlabel, 'variable': ''}, + 'labels': {'value': ylabel, x_dim: xlabel, 'variable': ''}, } # Add facet_col_wrap for single facet dimension if facet_col and not facet_row: common_args['facet_col_wrap'] = facet_cols + # Add mode-specific defaults (before px_kwargs so they can be overridden) + if mode in ('line', 'area'): + common_args['line_shape'] = 'hv' # Stepped lines by default + + # Allow callers to pass any px.* keyword args (e.g., category_orders, range_x/y, line_shape) + # These will override the defaults set above + if px_kwargs: + common_args.update(px_kwargs) + if mode == 'stacked_bar': fig = px.bar(**common_args) fig.update_traces(marker_line_width=0) @@ -531,10 +647,10 @@ def with_plotly( fig = px.bar(**common_args) fig.update_layout(barmode='group', bargap=0.2, bargroupgap=0) elif mode == 'line': - fig = px.line(**common_args, line_shape='hv') # Stepped lines + fig = px.line(**common_args) elif mode == 'area': # Use Plotly Express to create the area plot (preserves animation, legends, faceting) - fig = px.area(**common_args, line_shape='hv') + fig = px.area(**common_args) # Classify each variable based on its values variable_classification = {} @@ -577,13 +693,6 @@ def with_plotly( if hasattr(trace, 'fill'): trace.fill = None - # Update layout with basic styling (Plotly Express handles sizing automatically) - fig.update_layout( - plot_bgcolor='rgba(0,0,0,0)', - paper_bgcolor='rgba(0,0,0,0)', - font=dict(size=12), - ) - # Update axes to share if requested (Plotly Express already handles this, but we can customize) if not shared_yaxes: fig.update_yaxes(matches=None) @@ -594,33 +703,32 @@ def with_plotly( def with_matplotlib( - data: pd.DataFrame, + data: xr.Dataset | pd.DataFrame | pd.Series, mode: Literal['stacked_bar', 'line'] = 'stacked_bar', colors: ColorType = 'viridis', title: str = '', ylabel: str = '', xlabel: str = 'Time in h', figsize: tuple[int, int] = (12, 6), - fig: plt.Figure | None = None, - ax: plt.Axes | None = None, + plot_kwargs: dict[str, Any] | None = None, ) -> tuple[plt.Figure, plt.Axes]: """ - Plot a DataFrame with Matplotlib using stacked bars or stepped lines. + Plot data with Matplotlib using stacked bars or stepped lines. Args: - data: A DataFrame containing the data to plot. The index should represent time (e.g., hours), - and each column represents a separate data series. + data: An xarray Dataset, pandas DataFrame, or pandas Series to plot. After conversion to DataFrame, + the index represents time and each column represents a separate data series (variables). mode: Plotting mode. Use 'stacked_bar' for stacked bar charts or 'line' for stepped lines. - colors: Color specification, can be: - - A string with a colormap name (e.g., 'viridis', 'plasma') + colors: Color specification. Can be: + - A colormap name (e.g., 'turbo', 'plasma') - A list of color strings (e.g., ['#ff0000', '#00ff00']) - - A dictionary mapping column names to colors (e.g., {'Column1': '#ff0000'}) + - A dict mapping column names to colors (e.g., {'Column1': '#ff0000'}) title: The title of the plot. ylabel: The ylabel of the plot. xlabel: The xlabel of the plot. - figsize: Specify the size of the figure - fig: A Matplotlib figure object to plot on. If not provided, a new figure will be created. - ax: A Matplotlib axes object to plot on. If not provided, a new axes will be created. + figsize: Specify the size of the figure (width, height) in inches. + plot_kwargs: Optional dict of parameters to pass to ax.bar() or ax.step() plotting calls. + Use this to customize plot properties (e.g., linewidth, alpha, edgecolor). Returns: A tuple containing the Matplotlib figure and axes objects used for the plot. @@ -633,45 +741,111 @@ def with_matplotlib( if mode not in ('stacked_bar', 'line'): raise ValueError(f"'mode' must be one of {{'stacked_bar','line'}} for matplotlib, got {mode!r}") - if fig is None or ax is None: - fig, ax = plt.subplots(figsize=figsize) + # Ensure data is a Dataset and validate it + data = _ensure_dataset(data) + _validate_plotting_data(data, allow_empty=True) + + # Create new figure and axes + fig, ax = plt.subplots(figsize=figsize) + + # Initialize plot_kwargs if not provided + if plot_kwargs is None: + plot_kwargs = {} + + # Handle all-scalar datasets (where all variables have no dimensions) + # This occurs when all variables are scalar values with dims=() + if all(len(data[var].dims) == 0 for var in data.data_vars): + # Create simple bar/line plot with variable names as x-axis + variables = list(data.data_vars.keys()) + values = [float(data[var].values) for var in data.data_vars] + + # Resolve colors + color_discrete_map = resolve_colors(data, colors, engine='matplotlib') + colors_list = [color_discrete_map.get(var, '#808080') for var in variables] + + # Create plot based on mode + if mode == 'stacked_bar': + ax.bar(variables, values, color=colors_list, **plot_kwargs) + elif mode == 'line': + ax.plot( + variables, + values, + marker='o', + color=colors_list[0] if len(set(colors_list)) == 1 else None, + **plot_kwargs, + ) + # If different colors, plot each point separately + if len(set(colors_list)) > 1: + ax.clear() + for i, (var, val) in enumerate(zip(variables, values, strict=False)): + ax.plot([i], [val], marker='o', color=colors_list[i], label=var, **plot_kwargs) + ax.set_xticks(range(len(variables))) + ax.set_xticklabels(variables) + + ax.set_xlabel(xlabel, ha='center') + ax.set_ylabel(ylabel, va='center') + ax.set_title(title) + ax.grid(color='lightgrey', linestyle='-', linewidth=0.5, axis='y') + fig.tight_layout() + + return fig, ax + + # Resolve colors first (includes validation) + color_discrete_map = resolve_colors(data, colors, engine='matplotlib') - processed_colors = ColorProcessor(engine='matplotlib').process_colors(colors, list(data.columns)) + # Convert Dataset to DataFrame for matplotlib plotting (naturally wide-form) + df = data.to_dataframe() + + # Get colors in column order + processed_colors = [color_discrete_map.get(str(col), '#808080') for col in df.columns] if mode == 'stacked_bar': - cumulative_positive = np.zeros(len(data)) - cumulative_negative = np.zeros(len(data)) - width = data.index.to_series().diff().dropna().min() # Minimum time difference + cumulative_positive = np.zeros(len(df)) + cumulative_negative = np.zeros(len(df)) + + # Robust bar width: handle datetime-like, numeric, and single-point indexes + if len(df.index) > 1: + delta = pd.Index(df.index).to_series().diff().dropna().min() + if hasattr(delta, 'total_seconds'): # datetime-like + width = delta.total_seconds() / 86400.0 # Matplotlib date units = days + else: + width = float(delta) + else: + width = 0.8 # reasonable default for a single bar - for i, column in enumerate(data.columns): - positive_values = np.clip(data[column], 0, None) # Keep only positive values - negative_values = np.clip(data[column], None, 0) # Keep only negative values + for i, column in enumerate(df.columns): + # Fill NaNs to avoid breaking stacking math + series = df[column].fillna(0) + positive_values = np.clip(series, 0, None) # Keep only positive values + negative_values = np.clip(series, None, 0) # Keep only negative values # Plot positive bars ax.bar( - data.index, + df.index, positive_values, bottom=cumulative_positive, color=processed_colors[i], label=column, width=width, align='center', + **plot_kwargs, ) cumulative_positive += positive_values.values # Plot negative bars ax.bar( - data.index, + df.index, negative_values, bottom=cumulative_negative, color=processed_colors[i], label='', # No label for negative bars width=width, align='center', + **plot_kwargs, ) cumulative_negative += negative_values.values elif mode == 'line': - for i, column in enumerate(data.columns): - ax.step(data.index, data[column], where='post', color=processed_colors[i], label=column) + for i, column in enumerate(df.columns): + ax.step(df.index, df[column], where='post', color=processed_colors[i], label=column, **plot_kwargs) # Aesthetics ax.set_xlabel(xlabel, ha='center') @@ -944,228 +1118,104 @@ def plot_network( ) -def pie_with_plotly( - data: pd.DataFrame, - colors: ColorType = 'viridis', - title: str = '', - legend_title: str = '', - hole: float = 0.0, - fig: go.Figure | None = None, -) -> go.Figure: +def preprocess_data_for_pie( + data: xr.Dataset | pd.DataFrame | pd.Series, + lower_percentage_threshold: float = 5.0, +) -> pd.Series: """ - Create a pie chart with Plotly to visualize the proportion of values in a DataFrame. + Preprocess data for pie chart display. - Args: - data: A DataFrame containing the data to plot. If multiple rows exist, - they will be summed unless a specific index value is passed. - colors: Color specification, can be: - - A string with a colorscale name (e.g., 'viridis', 'plasma') - - A list of color strings (e.g., ['#ff0000', '#00ff00']) - - A dictionary mapping column names to colors (e.g., {'Column1': '#ff0000'}) - title: The title of the plot. - legend_title: The title for the legend. - hole: Size of the hole in the center for creating a donut chart (0.0 to 1.0). - fig: A Plotly figure object to plot on. If not provided, a new figure will be created. - - Returns: - A Plotly figure object containing the generated pie chart. - - Notes: - - Negative values are not appropriate for pie charts and will be converted to absolute values with a warning. - - If the data contains very small values (less than 1% of the total), they can be grouped into an "Other" category - for better readability. - - By default, the sum of all columns is used for the pie chart. For time series data, consider preprocessing. - - """ - if data.empty: - logger.error('Empty DataFrame provided for pie chart. Returning empty figure.') - return go.Figure() - - # Create a copy to avoid modifying the original DataFrame - data_copy = data.copy() - - # Check if any negative values and warn - if (data_copy < 0).any().any(): - logger.error('Negative values detected in data. Using absolute values for pie chart.') - data_copy = data_copy.abs() - - # If data has multiple rows, sum them to get total for each column - if len(data_copy) > 1: - data_sum = data_copy.sum() - else: - data_sum = data_copy.iloc[0] - - # Get labels (column names) and values - labels = data_sum.index.tolist() - values = data_sum.values.tolist() - - # Apply color mapping using the unified color processor - processed_colors = ColorProcessor(engine='plotly').process_colors(colors, labels) - - # Create figure if not provided - fig = fig if fig is not None else go.Figure() - - # Add pie trace - fig.add_trace( - go.Pie( - labels=labels, - values=values, - hole=hole, - marker=dict(colors=processed_colors), - textinfo='percent+label+value', - textposition='inside', - insidetextorientation='radial', - ) - ) - - # Update layout for better aesthetics - fig.update_layout( - title=title, - legend_title=legend_title, - plot_bgcolor='rgba(0,0,0,0)', # Transparent background - paper_bgcolor='rgba(0,0,0,0)', # Transparent paper background - font=dict(size=14), # Increase font size for better readability - ) - - return fig - - -def pie_with_matplotlib( - data: pd.DataFrame, - colors: ColorType = 'viridis', - title: str = '', - legend_title: str = 'Categories', - hole: float = 0.0, - figsize: tuple[int, int] = (10, 8), - fig: plt.Figure | None = None, - ax: plt.Axes | None = None, -) -> tuple[plt.Figure, plt.Axes]: - """ - Create a pie chart with Matplotlib to visualize the proportion of values in a DataFrame. + Groups items that are individually below the threshold percentage into an "Other" category. + Converts various input types to a pandas Series for uniform handling. Args: - data: A DataFrame containing the data to plot. If multiple rows exist, - they will be summed unless a specific index value is passed. - colors: Color specification, can be: - - A string with a colormap name (e.g., 'viridis', 'plasma') - - A list of color strings (e.g., ['#ff0000', '#00ff00']) - - A dictionary mapping column names to colors (e.g., {'Column1': '#ff0000'}) - title: The title of the plot. - legend_title: The title for the legend. - hole: Size of the hole in the center for creating a donut chart (0.0 to 1.0). - figsize: The size of the figure (width, height) in inches. - fig: A Matplotlib figure object to plot on. If not provided, a new figure will be created. - ax: A Matplotlib axes object to plot on. If not provided, a new axes will be created. + data: Input data (xarray Dataset, DataFrame, or Series) + lower_percentage_threshold: Percentage threshold - items below this are grouped into "Other" Returns: - A tuple containing the Matplotlib figure and axes objects used for the plot. - - Notes: - - Negative values are not appropriate for pie charts and will be converted to absolute values with a warning. - - If the data contains very small values (less than 1% of the total), they can be grouped into an "Other" category - for better readability. - - By default, the sum of all columns is used for the pie chart. For time series data, consider preprocessing. - + Processed pandas Series with small items grouped into "Other" """ - if data.empty: - logger.error('Empty DataFrame provided for pie chart. Returning empty figure.') - if fig is None or ax is None: - fig, ax = plt.subplots(figsize=figsize) - return fig, ax + # Convert to Series + if isinstance(data, xr.Dataset): + # Sum all dimensions for each variable to get total values + values = {} + for var in data.data_vars: + var_data = data[var] + if len(var_data.dims) > 0: + total_value = float(var_data.sum().item()) + else: + total_value = float(var_data.item()) - # Create a copy to avoid modifying the original DataFrame - data_copy = data.copy() + # Handle negative values + if total_value < 0: + logger.warning(f'Negative value for {var}: {total_value}. Using absolute value.') + total_value = abs(total_value) - # Check if any negative values and warn - if (data_copy < 0).any().any(): - logger.error('Negative values detected in data. Using absolute values for pie chart.') - data_copy = data_copy.abs() + values[var] = total_value - # If data has multiple rows, sum them to get total for each column - if len(data_copy) > 1: - data_sum = data_copy.sum() - else: - data_sum = data_copy.iloc[0] + series = pd.Series(values) - # Get labels (column names) and values - labels = data_sum.index.tolist() - values = data_sum.values.tolist() + elif isinstance(data, pd.DataFrame): + # Sum across all columns if DataFrame + series = data.sum(axis=0) + # Handle negative values + negative_mask = series < 0 + if negative_mask.any(): + logger.warning(f'Negative values found: {series[negative_mask].to_dict()}. Using absolute values.') + series = series.abs() - # Apply color mapping using the unified color processor - processed_colors = ColorProcessor(engine='matplotlib').process_colors(colors, labels) + else: # pd.Series + series = data.copy() + # Handle negative values + negative_mask = series < 0 + if negative_mask.any(): + logger.warning(f'Negative values found: {series[negative_mask].to_dict()}. Using absolute values.') + series = series.abs() - # Create figure and axis if not provided - if fig is None or ax is None: - fig, ax = plt.subplots(figsize=figsize) + # Only keep positive values + series = series[series > 0] - # Draw the pie chart - wedges, texts, autotexts = ax.pie( - values, - labels=labels, - colors=processed_colors, - autopct='%1.1f%%', - startangle=90, - shadow=False, - wedgeprops=dict(width=0.5) if hole > 0 else None, # Set width for donut - ) + if series.empty or lower_percentage_threshold <= 0: + return series - # Adjust the wedgeprops to make donut hole size consistent with plotly - # For matplotlib, the hole size is determined by the wedge width - # Convert hole parameter to wedge width - if hole > 0: - # Adjust hole size to match plotly's hole parameter - # In matplotlib, wedge width is relative to the radius (which is 1) - # For plotly, hole is a fraction of the radius - wedge_width = 1 - hole - for wedge in wedges: - wedge.set_width(wedge_width) - - # Customize the appearance - # Make autopct text more visible - for autotext in autotexts: - autotext.set_fontsize(10) - autotext.set_color('white') - - # Set aspect ratio to be equal to ensure a circular pie - ax.set_aspect('equal') - - # Add title - if title: - ax.set_title(title, fontsize=16) + # Calculate percentages + total = series.sum() + percentages = (series / total) * 100 - # Create a legend if there are many segments - if len(labels) > 6: - ax.legend(wedges, labels, title=legend_title, loc='center left', bbox_to_anchor=(1, 0, 0.5, 1)) + # Find items below and above threshold + below_threshold = series[percentages < lower_percentage_threshold] + above_threshold = series[percentages >= lower_percentage_threshold] - # Apply tight layout - fig.tight_layout() + # Only group if there are at least 2 items below threshold + if len(below_threshold) > 1: + # Create new series with items above threshold + "Other" + result = above_threshold.copy() + result['Other'] = below_threshold.sum() + return result - return fig, ax + return series def dual_pie_with_plotly( - data_left: pd.Series, - data_right: pd.Series, + data_left: xr.Dataset | pd.DataFrame | pd.Series, + data_right: xr.Dataset | pd.DataFrame | pd.Series, colors: ColorType = 'viridis', title: str = '', subtitles: tuple[str, str] = ('Left Chart', 'Right Chart'), legend_title: str = '', hole: float = 0.2, lower_percentage_group: float = 5.0, - hover_template: str = '%{label}: %{value} (%{percent})', text_info: str = 'percent+label', text_position: str = 'inside', + hover_template: str = '%{label}: %{value} (%{percent})', ) -> go.Figure: """ - Create two pie charts side by side with Plotly, with consistent coloring across both charts. + Create two pie charts side by side with Plotly. Args: - data_left: Series for the left pie chart. - data_right: Series for the right pie chart. - colors: Color specification, can be: - - A string with a colorscale name (e.g., 'viridis', 'plasma') - - A list of color strings (e.g., ['#ff0000', '#00ff00']) - - A dictionary mapping category names to colors (e.g., {'Category1': '#ff0000'}) + data_left: Data for the left pie chart. Variables are summed across all dimensions. + data_right: Data for the right pie chart. Variables are summed across all dimensions. + colors: Color specification (colorscale name, list of colors, or dict mapping) title: The main title of the plot. subtitles: Tuple containing the subtitles for (left, right) charts. legend_title: The title for the legend. @@ -1177,119 +1227,64 @@ def dual_pie_with_plotly( text_position: Position of text: 'inside', 'outside', 'auto', or 'none'. Returns: - A Plotly figure object containing the generated dual pie chart. + Plotly Figure object """ - from plotly.subplots import make_subplots + # Preprocess data to Series + left_series = preprocess_data_for_pie(data_left, lower_percentage_group) + right_series = preprocess_data_for_pie(data_right, lower_percentage_group) - # Check for empty data - if data_left.empty and data_right.empty: - logger.error('Both datasets are empty. Returning empty figure.') - return go.Figure() + # Extract labels and values + left_labels = left_series.index.tolist() + left_values = left_series.values.tolist() - # Create a subplot figure - fig = make_subplots( - rows=1, cols=2, specs=[[{'type': 'pie'}, {'type': 'pie'}]], subplot_titles=subtitles, horizontal_spacing=0.05 - ) + right_labels = right_series.index.tolist() + right_values = right_series.values.tolist() - # Process series to handle negative values and apply minimum percentage threshold - def preprocess_series(series: pd.Series): - """ - Preprocess a series for pie chart display by handling negative values - and grouping the smallest parts together if they collectively represent - less than the specified percentage threshold. - - Args: - series: The series to preprocess - - Returns: - A preprocessed pandas Series - """ - # Handle negative values - if (series < 0).any(): - logger.error('Negative values detected in data. Using absolute values for pie chart.') - series = series.abs() - - # Remove zeros - series = series[series > 0] - - # Apply minimum percentage threshold if needed - if lower_percentage_group and not series.empty: - total = series.sum() - if total > 0: - # Sort series by value (ascending) - sorted_series = series.sort_values() - - # Calculate cumulative percentage contribution - cumulative_percent = (sorted_series.cumsum() / total) * 100 - - # Find entries that collectively make up less than lower_percentage_group - to_group = cumulative_percent <= lower_percentage_group + # Get all unique labels for consistent coloring + all_labels = sorted(set(left_labels) | set(right_labels)) - if to_group.sum() > 1: - # Create "Other" category for the smallest values that together are < threshold - other_sum = sorted_series[to_group].sum() - - # Keep only values that aren't in the "Other" group - result_series = series[~series.index.isin(sorted_series[to_group].index)] - - # Add the "Other" category if it has a value - if other_sum > 0: - result_series['Other'] = other_sum - - return result_series - - return series - - data_left_processed = preprocess_series(data_left) - data_right_processed = preprocess_series(data_right) - - # Get unique set of all labels for consistent coloring - all_labels = sorted(set(data_left_processed.index) | set(data_right_processed.index)) - - # Get consistent color mapping for both charts using our unified function + # Create color map color_map = ColorProcessor(engine='plotly').process_colors(colors, all_labels, return_mapping=True) - # Function to create a pie trace with consistently mapped colors - def create_pie_trace(data_series, side): - if data_series.empty: - return None - - labels = data_series.index.tolist() - values = data_series.values.tolist() - trace_colors = [color_map[label] for label in labels] - - return go.Pie( - labels=labels, - values=values, - name=side, - marker=dict(colors=trace_colors), - hole=hole, - textinfo=text_info, - textposition=text_position, - insidetextorientation='radial', - hovertemplate=hover_template, - sort=True, # Sort values by default (largest first) + # Create figure + fig = go.Figure() + + # Add left pie + if left_labels: + fig.add_trace( + go.Pie( + labels=left_labels, + values=left_values, + name=subtitles[0], + marker=dict(colors=[color_map.get(label, '#636EFA') for label in left_labels]), + hole=hole, + textinfo=text_info, + textposition=text_position, + hovertemplate=hover_template, + domain=dict(x=[0, 0.48]), + ) ) - # Add left pie if data exists - left_trace = create_pie_trace(data_left_processed, subtitles[0]) - if left_trace: - left_trace.domain = dict(x=[0, 0.48]) - fig.add_trace(left_trace, row=1, col=1) - - # Add right pie if data exists - right_trace = create_pie_trace(data_right_processed, subtitles[1]) - if right_trace: - right_trace.domain = dict(x=[0.52, 1]) - fig.add_trace(right_trace, row=1, col=2) + # Add right pie + if right_labels: + fig.add_trace( + go.Pie( + labels=right_labels, + values=right_values, + name=subtitles[1], + marker=dict(colors=[color_map.get(label, '#636EFA') for label in right_labels]), + hole=hole, + textinfo=text_info, + textposition=text_position, + hovertemplate=hover_template, + domain=dict(x=[0.52, 1]), + ) + ) # Update layout fig.update_layout( title=title, legend_title=legend_title, - plot_bgcolor='rgba(0,0,0,0)', # Transparent background - paper_bgcolor='rgba(0,0,0,0)', # Transparent paper background - font=dict(size=14), margin=dict(t=80, b=50, l=30, r=30), ) @@ -1297,8 +1292,8 @@ def create_pie_trace(data_series, side): def dual_pie_with_matplotlib( - data_left: pd.Series, - data_right: pd.Series, + data_left: xr.Dataset | pd.DataFrame | pd.Series, + data_right: xr.Dataset | pd.DataFrame | pd.Series, colors: ColorType = 'viridis', title: str = '', subtitles: tuple[str, str] = ('Left Chart', 'Right Chart'), @@ -1306,154 +1301,99 @@ def dual_pie_with_matplotlib( hole: float = 0.2, lower_percentage_group: float = 5.0, figsize: tuple[int, int] = (14, 7), - fig: plt.Figure | None = None, - axes: list[plt.Axes] | None = None, ) -> tuple[plt.Figure, list[plt.Axes]]: """ - Create two pie charts side by side with Matplotlib, with consistent coloring across both charts. - Leverages the existing pie_with_matplotlib function. + Create two pie charts side by side with Matplotlib. Args: - data_left: Series for the left pie chart. - data_right: Series for the right pie chart. - colors: Color specification, can be: - - A string with a colormap name (e.g., 'viridis', 'plasma') - - A list of color strings (e.g., ['#ff0000', '#00ff00']) - - A dictionary mapping category names to colors (e.g., {'Category1': '#ff0000'}) + data_left: Data for the left pie chart. + data_right: Data for the right pie chart. + colors: Color specification (colormap name, list of colors, or dict mapping) title: The main title of the plot. subtitles: Tuple containing the subtitles for (left, right) charts. legend_title: The title for the legend. hole: Size of the hole in the center for creating donut charts (0.0 to 1.0). lower_percentage_group: Whether to group small segments (below percentage) into an "Other" category. figsize: The size of the figure (width, height) in inches. - fig: A Matplotlib figure object to plot on. If not provided, a new figure will be created. - axes: A list of Matplotlib axes objects to plot on. If not provided, new axes will be created. Returns: - A tuple containing the Matplotlib figure and list of axes objects used for the plot. + Tuple of (Figure, list of Axes) """ - # Check for empty data - if data_left.empty and data_right.empty: - logger.error('Both datasets are empty. Returning empty figure.') - if fig is None: - fig, axes = plt.subplots(1, 2, figsize=figsize) - return fig, axes - - # Create figure and axes if not provided - if fig is None or axes is None: - fig, axes = plt.subplots(1, 2, figsize=figsize) - - # Process series to handle negative values and apply minimum percentage threshold - def preprocess_series(series: pd.Series): - """ - Preprocess a series for pie chart display by handling negative values - and grouping the smallest parts together if they collectively represent - less than the specified percentage threshold. - """ - # Handle negative values - if (series < 0).any(): - logger.error('Negative values detected in data. Using absolute values for pie chart.') - series = series.abs() - - # Remove zeros - series = series[series > 0] - - # Apply minimum percentage threshold if needed - if lower_percentage_group and not series.empty: - total = series.sum() - if total > 0: - # Sort series by value (ascending) - sorted_series = series.sort_values() - - # Calculate cumulative percentage contribution - cumulative_percent = (sorted_series.cumsum() / total) * 100 + # Preprocess data to Series + left_series = preprocess_data_for_pie(data_left, lower_percentage_group) + right_series = preprocess_data_for_pie(data_right, lower_percentage_group) - # Find entries that collectively make up less than lower_percentage_group - to_group = cumulative_percent <= lower_percentage_group + # Extract labels and values + left_labels = left_series.index.tolist() + left_values = left_series.values.tolist() - if to_group.sum() > 1: - # Create "Other" category for the smallest values that together are < threshold - other_sum = sorted_series[to_group].sum() + right_labels = right_series.index.tolist() + right_values = right_series.values.tolist() - # Keep only values that aren't in the "Other" group - result_series = series[~series.index.isin(sorted_series[to_group].index)] + # Get all unique labels for consistent coloring + all_labels = sorted(set(left_labels) | set(right_labels)) - # Add the "Other" category if it has a value - if other_sum > 0: - result_series['Other'] = other_sum - - return result_series - - return series + # Create color map + color_map = ColorProcessor(engine='matplotlib').process_colors(colors, all_labels, return_mapping=True) - # Preprocess data - data_left_processed = preprocess_series(data_left) - data_right_processed = preprocess_series(data_right) + # Create figure + fig, axes = plt.subplots(1, 2, figsize=figsize) - # Convert Series to DataFrames for pie_with_matplotlib - df_left = pd.DataFrame(data_left_processed).T if not data_left_processed.empty else pd.DataFrame() - df_right = pd.DataFrame(data_right_processed).T if not data_right_processed.empty else pd.DataFrame() + def draw_pie(ax, labels, values, subtitle): + """Draw a single pie chart.""" + if not labels: + ax.set_title(subtitle) + ax.axis('off') + return - # Get unique set of all labels for consistent coloring - all_labels = sorted(set(data_left_processed.index) | set(data_right_processed.index)) + chart_colors = [color_map[label] for label in labels] - # Get consistent color mapping for both charts using our unified function - color_map = ColorProcessor(engine='matplotlib').process_colors(colors, all_labels, return_mapping=True) + # Draw pie + wedges, texts, autotexts = ax.pie( + values, + labels=labels, + colors=chart_colors, + autopct='%1.1f%%', + startangle=90, + wedgeprops=dict(width=1 - hole) if hole > 0 else None, + ) - # Configure colors for each DataFrame based on the consistent mapping - left_colors = [color_map[col] for col in df_left.columns] if not df_left.empty else [] - right_colors = [color_map[col] for col in df_right.columns] if not df_right.empty else [] + # Style text + for autotext in autotexts: + autotext.set_fontsize(10) + autotext.set_color('white') + autotext.set_weight('bold') - # Create left pie chart - if not df_left.empty: - pie_with_matplotlib(data=df_left, colors=left_colors, title=subtitles[0], hole=hole, fig=fig, ax=axes[0]) - else: - axes[0].set_title(subtitles[0]) - axes[0].axis('off') + ax.set_aspect('equal') + ax.set_title(subtitle, fontsize=14, pad=20) - # Create right pie chart - if not df_right.empty: - pie_with_matplotlib(data=df_right, colors=right_colors, title=subtitles[1], hole=hole, fig=fig, ax=axes[1]) - else: - axes[1].set_title(subtitles[1]) - axes[1].axis('off') + # Draw both pies + draw_pie(axes[0], left_labels, left_values, subtitles[0]) + draw_pie(axes[1], right_labels, right_values, subtitles[1]) # Add main title if title: fig.suptitle(title, fontsize=16, y=0.98) - # Adjust layout - fig.tight_layout() - - # Create a unified legend if both charts have data - if not df_left.empty and not df_right.empty: - # Remove individual legends - for ax in axes: - if ax.get_legend(): - ax.get_legend().remove() - - # Create handles for the unified legend - handles = [] - labels_for_legend = [] - - for label in all_labels: - color = color_map[label] - patch = plt.Line2D([0], [0], marker='o', color='w', markerfacecolor=color, markersize=10, label=label) - handles.append(patch) - labels_for_legend.append(label) + # Create unified legend + if left_labels or right_labels: + handles = [ + plt.Line2D([0], [0], marker='o', color='w', markerfacecolor=color_map[label], markersize=10) + for label in all_labels + ] - # Add unified legend fig.legend( handles=handles, - labels=labels_for_legend, + labels=all_labels, title=legend_title, loc='lower center', - bbox_to_anchor=(0.5, 0), - ncol=min(len(all_labels), 5), # Limit columns to 5 for readability + bbox_to_anchor=(0.5, -0.02), + ncol=min(len(all_labels), 5), ) - # Add padding at the bottom for the legend - fig.subplots_adjust(bottom=0.2) + fig.subplots_adjust(bottom=0.15) + + fig.tight_layout() return fig, axes @@ -1469,6 +1409,7 @@ def heatmap_with_plotly( | Literal['auto'] | None = 'auto', fill: Literal['ffill', 'bfill'] | None = 'ffill', + **imshow_kwargs: Any, ) -> go.Figure: """ Plot a heatmap visualization using Plotly's imshow with faceting and animation support. @@ -1501,6 +1442,11 @@ def heatmap_with_plotly( - Tuple like ('D', 'h'): Explicit time reshaping (days vs hours) - None: Disable time reshaping (will error if only 1D time data) fill: Method to fill missing values when reshaping time: 'ffill' or 'bfill'. Default is 'ffill'. + **imshow_kwargs: Additional keyword arguments to pass to plotly.express.imshow. + Common options include: + - aspect: 'auto', 'equal', or a number for aspect ratio + - zmin, zmax: Minimum and maximum values for color scale + - labels: Dict to customize axis labels Returns: A Plotly figure object containing the heatmap visualization. @@ -1589,12 +1535,26 @@ def heatmap_with_plotly( heatmap_dims = [dim for dim in available_dims if dim not in facet_dims] if len(heatmap_dims) < 2: - # Need at least 2 dimensions for a heatmap - logger.error( - f'Heatmap requires at least 2 dimensions for rows and columns. ' - f'After faceting/animation, only {len(heatmap_dims)} dimension(s) remain: {heatmap_dims}' - ) - return go.Figure() + # Handle single-dimension case by adding variable name as a dimension + if len(heatmap_dims) == 1: + # Get the variable name, or use a default + var_name = data.name if data.name else 'value' + + # Expand the DataArray by adding a new dimension with the variable name + data = data.expand_dims({'variable': [var_name]}) + + # Update available dimensions + available_dims = list(data.dims) + heatmap_dims = [dim for dim in available_dims if dim not in facet_dims] + + logger.debug(f'Only 1 dimension remaining for heatmap. Added variable dimension: {var_name}') + else: + # No dimensions at all - cannot create a heatmap + logger.error( + f'Heatmap requires at least 1 dimension. ' + f'After faceting/animation, {len(heatmap_dims)} dimension(s) remain: {heatmap_dims}' + ) + return go.Figure() # Setup faceting parameters for Plotly Express # Note: px.imshow only supports facet_col, not facet_row @@ -1631,23 +1591,21 @@ def heatmap_with_plotly( if animate_by: common_args['animation_frame'] = animate_by + # Merge in additional imshow kwargs + common_args.update(imshow_kwargs) + try: fig = px.imshow(**common_args) except Exception as e: logger.error(f'Error creating imshow plot: {e}. Falling back to basic heatmap.') # Fallback: create a simple heatmap without faceting - fig = px.imshow( - data.values, - color_continuous_scale=colors if isinstance(colors, str) else 'viridis', - title=title, - ) - - # Update layout with basic styling - fig.update_layout( - plot_bgcolor='rgba(0,0,0,0)', - paper_bgcolor='rgba(0,0,0,0)', - font=dict(size=12), - ) + fallback_args = { + 'img': data.values, + 'color_continuous_scale': colors if isinstance(colors, str) else 'viridis', + 'title': title, + } + fallback_args.update(imshow_kwargs) + fig = px.imshow(**fallback_args) return fig @@ -1657,12 +1615,15 @@ def heatmap_with_matplotlib( colors: ColorType = 'viridis', title: str = '', figsize: tuple[float, float] = (12, 6), - fig: plt.Figure | None = None, - ax: plt.Axes | None = None, reshape_time: tuple[Literal['YS', 'MS', 'W', 'D', 'h', '15min', 'min'], Literal['W', 'D', 'h', '15min', 'min']] | Literal['auto'] | None = 'auto', fill: Literal['ffill', 'bfill'] | None = 'ffill', + vmin: float | None = None, + vmax: float | None = None, + imshow_kwargs: dict[str, Any] | None = None, + cbar_kwargs: dict[str, Any] | None = None, + **kwargs: Any, ) -> tuple[plt.Figure, plt.Axes]: """ Plot a heatmap visualization using Matplotlib's imshow. @@ -1674,16 +1635,25 @@ def heatmap_with_matplotlib( data: An xarray DataArray containing the data to visualize. Should have at least 2 dimensions. If more than 2 dimensions exist, additional dimensions will be reduced by taking the first slice. - colors: Color specification. Should be a colormap name (e.g., 'viridis', 'RdBu'). + colors: Color specification. Should be a colormap name (e.g., 'turbo', 'RdBu'). title: The title of the heatmap. figsize: The size of the figure (width, height) in inches. - fig: A Matplotlib figure object to plot on. If not provided, a new figure will be created. - ax: A Matplotlib axes object to plot on. If not provided, a new axes will be created. reshape_time: Time reshaping configuration: - 'auto' (default): Automatically applies ('D', 'h') if only 'time' dimension - Tuple like ('D', 'h'): Explicit time reshaping (days vs hours) - None: Disable time reshaping fill: Method to fill missing values when reshaping time: 'ffill' or 'bfill'. Default is 'ffill'. + vmin: Minimum value for color scale. If None, uses data minimum. + vmax: Maximum value for color scale. If None, uses data maximum. + imshow_kwargs: Optional dict of parameters to pass to ax.imshow(). + Use this to customize image properties (e.g., interpolation, aspect). + cbar_kwargs: Optional dict of parameters to pass to plt.colorbar(). + Use this to customize colorbar properties (e.g., orientation, label). + **kwargs: Additional keyword arguments passed to ax.imshow(). + Common options include: + - interpolation: 'nearest', 'bilinear', 'bicubic', etc. + - alpha: Transparency level (0-1) + - extent: [left, right, bottom, top] for axis limits Returns: A tuple containing the Matplotlib figure and axes objects used for the plot. @@ -1705,19 +1675,33 @@ def heatmap_with_matplotlib( fig, ax = heatmap_with_matplotlib(data_array, reshape_time=('D', 'h')) ``` """ + # Initialize kwargs if not provided + if imshow_kwargs is None: + imshow_kwargs = {} + if cbar_kwargs is None: + cbar_kwargs = {} + + # Merge any additional kwargs into imshow_kwargs + # This allows users to pass imshow options directly + imshow_kwargs.update(kwargs) + # Handle empty data if data.size == 0: - if fig is None or ax is None: - fig, ax = plt.subplots(figsize=figsize) + fig, ax = plt.subplots(figsize=figsize) return fig, ax # Apply time reshaping using the new unified function # Matplotlib doesn't support faceting/animation, so we pass None for those data = reshape_data_for_heatmap(data, reshape_time=reshape_time, facet_by=None, animate_by=None, fill=fill) - # Create figure and axes if not provided - if fig is None or ax is None: - fig, ax = plt.subplots(figsize=figsize) + # Handle single-dimension case by adding variable name as a dimension + if isinstance(data, xr.DataArray) and len(data.dims) == 1: + var_name = data.name if data.name else 'value' + data = data.expand_dims({'variable': [var_name]}) + logger.debug(f'Only 1 dimension in data. Added variable dimension: {var_name}') + + # Create figure and axes + fig, ax = plt.subplots(figsize=figsize) # Extract data values # If data has more than 2 dimensions, we need to reduce it @@ -1745,12 +1729,19 @@ def heatmap_with_matplotlib( # Process colormap cmap = colors if isinstance(colors, str) else 'viridis' - # Create the heatmap using imshow - im = ax.imshow(values, cmap=cmap, aspect='auto', origin='upper') + # Create the heatmap using imshow with user customizations + imshow_defaults = {'cmap': cmap, 'aspect': 'auto', 'origin': 'upper', 'vmin': vmin, 'vmax': vmax} + imshow_defaults.update(imshow_kwargs) # User kwargs override defaults + im = ax.imshow(values, **imshow_defaults) + + # Add colorbar with user customizations + cbar_defaults = {'ax': ax, 'orientation': 'horizontal', 'pad': 0.1, 'aspect': 15, 'fraction': 0.05} + cbar_defaults.update(cbar_kwargs) # User kwargs override defaults + cbar = plt.colorbar(im, **cbar_defaults) - # Add colorbar - cbar = plt.colorbar(im, ax=ax, orientation='horizontal', pad=0.1, aspect=15, fraction=0.05) - cbar.set_label('Value') + # Set colorbar label if not overridden by user + if 'label' not in cbar_kwargs: + cbar.set_label('Value') # Set labels and title ax.set_xlabel(str(x_labels).capitalize()) @@ -1770,6 +1761,7 @@ def export_figure( user_path: pathlib.Path | None = None, show: bool = True, save: bool = False, + dpi: int = 300, ) -> go.Figure | tuple[plt.Figure, plt.Axes]: """ Export a figure to a file and or show it. @@ -1781,6 +1773,7 @@ def export_figure( user_path: An optional user-specified file path. show: Whether to display the figure (default: True). save: Whether to save the figure (default: False). + dpi: DPI (dots per inch) for saving Matplotlib figures. If None, Matplotlib rcParams are used. Raises: ValueError: If no default filetype is provided and the path doesn't specify a filetype. @@ -1838,7 +1831,7 @@ def export_figure( plt.show() if save: - fig.savefig(str(filename), dpi=300) + fig.savefig(str(filename), dpi=dpi) plt.close(fig) # Close figure to free memory return fig, ax diff --git a/flixopt/results.py b/flixopt/results.py index 75f8f300e..576ff9ec1 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -107,6 +107,20 @@ class CalculationResults: ).mean() ``` + Configure automatic color management for plots: + + ```python + # Dict-based configuration: + results.setup_colors({'Solar*': 'Oranges', 'Wind*': 'Blues', 'Battery': 'green'}) + + # All plots automatically use configured colors (colors=None is the default) + results['ElectricityBus'].plot_node_balance() + results['Battery'].plot_charge_state() + + # Override when needed + results['ElectricityBus'].plot_node_balance(colors='turbo') # Ignores setup + ``` + Design Patterns: **Factory Methods**: Use `from_file()` and `from_calculation()` for creation or access directly from `Calculation.results` **Dictionary Access**: Use `results[element_label]` for element-specific results @@ -721,6 +735,7 @@ def plot_heatmap( heatmap_timeframes: Literal['YS', 'MS', 'W', 'D', 'h', '15min', 'min'] | None = None, heatmap_timesteps_per_frame: Literal['W', 'D', 'h', '15min', 'min'] | None = None, color_map: str | None = None, + **plot_kwargs: Any, ) -> plotly.graph_objs.Figure | tuple[plt.Figure, plt.Axes]: """ Plots a heatmap visualization of a variable using imshow or time-based reshaping. @@ -754,6 +769,20 @@ def plot_heatmap( Supported timeframes: 'YS', 'MS', 'W', 'D', 'h', '15min', 'min' fill: Method to fill missing values after reshape: 'ffill' (forward fill) or 'bfill' (backward fill). Default is 'ffill'. + **plot_kwargs: Additional plotting customization options. + Common options: + + - **dpi** (int): Export resolution for saved plots. Default: 300. + + For heatmaps specifically: + + - **vmin** (float): Minimum value for color scale (both engines). + - **vmax** (float): Maximum value for color scale (both engines). + + For Matplotlib heatmaps: + + - **imshow_kwargs** (dict): Additional kwargs for matplotlib's imshow (e.g., interpolation, aspect). + - **cbar_kwargs** (dict): Additional kwargs for colorbar customization. Examples: Direct imshow mode (default): @@ -794,6 +823,18 @@ def plot_heatmap( ... animate_by='period', ... reshape_time=('D', 'h'), ... ) + + High-resolution export with custom color range: + + >>> results.plot_heatmap('Battery|charge_state', save=True, dpi=600, vmin=0, vmax=100) + + Matplotlib heatmap with custom imshow settings: + + >>> results.plot_heatmap( + ... 'Boiler(Q_th)|flow_rate', + ... engine='matplotlib', + ... imshow_kwargs={'interpolation': 'bilinear', 'aspect': 'auto'}, + ... ) """ # Delegate to module-level plot_heatmap function return plot_heatmap( @@ -814,6 +855,7 @@ def plot_heatmap( heatmap_timeframes=heatmap_timeframes, heatmap_timesteps_per_frame=heatmap_timesteps_per_frame, color_map=color_map, + **plot_kwargs, ) def plot_network( @@ -994,6 +1036,7 @@ def plot_node_balance( facet_cols: int = 3, # Deprecated parameter (kept for backwards compatibility) indexer: dict[FlowSystemDimensions, Any] | None = None, + **plot_kwargs: Any, ) -> plotly.graph_objs.Figure | tuple[plt.Figure, plt.Axes]: """ Plots the node balance of the Component or Bus with optional faceting and animation. @@ -1021,6 +1064,27 @@ def plot_node_balance( animate_by: Dimension to animate over (Plotly only). Creates animation frames that cycle through dimension values. Only one dimension can be animated. Ignored if not found. facet_cols: Number of columns in the facet grid layout (default: 3). + **plot_kwargs: Additional plotting customization options passed to underlying plotting functions. + + Common options: + + - **dpi** (int): Export resolution in dots per inch. Default: 300. + + **For Plotly engine** (`engine='plotly'`): + + - Any Plotly Express parameter for px.bar()/px.line()/px.area() + Example: `range_y=[0, 100]`, `line_shape='linear'` + + **For Matplotlib engine** (`engine='matplotlib'`): + + - **plot_kwargs** (dict): Customize plot via `ax.bar()` or `ax.step()`. + Example: `plot_kwargs={'linewidth': 3, 'alpha': 0.7, 'edgecolor': 'black'}` + + See :func:`flixopt.plotting.with_plotly` and :func:`flixopt.plotting.with_matplotlib` + for complete parameter reference. + + Note: For Plotly, you can further customize the returned figure using `fig.update_traces()` + and `fig.update_layout()` after calling this method. Examples: Basic plot (current behavior): @@ -1052,6 +1116,25 @@ def plot_node_balance( Time range selection (summer months only): >>> results['Boiler'].plot_node_balance(select={'time': slice('2024-06', '2024-08')}, facet_by='scenario') + + High-resolution export for publication: + + >>> results['Boiler'].plot_node_balance(engine='matplotlib', save='figure.png', dpi=600) + + Plotly Express customization (e.g., set y-axis range): + + >>> results['Boiler'].plot_node_balance(range_y=[0, 100]) + + Custom matplotlib appearance: + + >>> results['Boiler'].plot_node_balance(engine='matplotlib', plot_kwargs={'linewidth': 3, 'alpha': 0.7}) + + Further customize Plotly figure after creation: + + >>> fig = results['Boiler'].plot_node_balance(mode='line', show=False) + >>> fig.update_traces(line={'width': 5, 'dash': 'dot'}) + >>> fig.update_layout(template='plotly_dark', width=1200, height=600) + >>> fig.show() """ # Handle deprecated indexer parameter if indexer is not None: @@ -1073,8 +1156,11 @@ def plot_node_balance( if engine not in {'plotly', 'matplotlib'}: raise ValueError(f'Engine "{engine}" not supported. Use one of ["plotly", "matplotlib"]') + # Extract dpi for export_figure + dpi = plot_kwargs.pop('dpi', None) # None uses CONFIG.Plotting.default_dpi + # Don't pass select/indexer to node_balance - we'll apply it afterwards - ds = self.node_balance(with_last_timestep=True, unit_type=unit_type, drop_suffix=drop_suffix) + ds = self.node_balance(with_last_timestep=False, unit_type=unit_type, drop_suffix=drop_suffix) ds, suffix_parts = _apply_selection_to_data(ds, select=select, drop=True) @@ -1101,14 +1187,17 @@ def plot_node_balance( mode=mode, title=title, facet_cols=facet_cols, + xlabel='Time in h', + **plot_kwargs, ) default_filetype = '.html' else: figure_like = plotting.with_matplotlib( - ds.to_dataframe(), + ds, colors=colors, mode=mode, title=title, + **plot_kwargs, ) default_filetype = '.png' @@ -1119,6 +1208,7 @@ def plot_node_balance( user_path=None if isinstance(save, bool) else pathlib.Path(save), show=show, save=True if save else False, + dpi=dpi, ) def plot_node_balance_pie( @@ -1132,6 +1222,7 @@ def plot_node_balance_pie( select: dict[FlowSystemDimensions, Any] | None = None, # Deprecated parameter (kept for backwards compatibility) indexer: dict[FlowSystemDimensions, Any] | None = None, + **plot_kwargs: Any, ) -> plotly.graph_objs.Figure | tuple[plt.Figure, list[plt.Axes]]: """Plot pie chart of flow hours distribution. @@ -1151,6 +1242,17 @@ def plot_node_balance_pie( engine: Plotting engine ('plotly' or 'matplotlib'). select: Optional data selection dict. Supports single values, lists, slices, and index arrays. Use this to select specific scenario/period before creating the pie chart. + **plot_kwargs: Additional plotting customization options. + + Common options: + + - **dpi** (int): Export resolution in dots per inch. Default: 300. + - **hover_template** (str): Hover text template (Plotly only). + Example: `hover_template='%{label}: %{value} (%{percent})'` + - **text_position** (str): Text position ('inside', 'outside', 'auto'). + - **hole** (float): Size of donut hole (0.0 to 1.0). + + See :func:`flixopt.plotting.dual_pie_with_plotly` for complete reference. Examples: Basic usage (auto-selects first scenario/period if present): @@ -1160,6 +1262,14 @@ def plot_node_balance_pie( Explicitly select a scenario and period: >>> results['Bus'].plot_node_balance_pie(select={'scenario': 'high_demand', 'period': 2030}) + + Create a donut chart with custom hover text: + + >>> results['Bus'].plot_node_balance_pie(hole=0.4, hover_template='%{label}: %{value:.2f} (%{percent})') + + High-resolution export: + + >>> results['Bus'].plot_node_balance_pie(save='figure.png', dpi=600) """ # Handle deprecated indexer parameter if indexer is not None: @@ -1178,6 +1288,9 @@ def plot_node_balance_pie( ) select = indexer + # Extract dpi for export_figure + dpi = plot_kwargs.pop('dpi', None) # None uses CONFIG.Plotting.default_dpi + inputs = sanitize_dataset( ds=self.solution[self.inputs] * self._calculation_results.hours_per_timestep, threshold=1e-5, @@ -1193,8 +1306,9 @@ def plot_node_balance_pie( drop_suffix='|', ) - inputs, suffix_parts = _apply_selection_to_data(inputs, select=select, drop=True) - outputs, suffix_parts = _apply_selection_to_data(outputs, select=select, drop=True) + inputs, suffix_parts_in = _apply_selection_to_data(inputs, select=select, drop=True) + outputs, suffix_parts_out = _apply_selection_to_data(outputs, select=select, drop=True) + suffix_parts = suffix_parts_in + suffix_parts_out # Sum over time dimension inputs = inputs.sum('time') @@ -1204,7 +1318,7 @@ def plot_node_balance_pie( # Pie charts need scalar data, so we automatically reduce extra dimensions extra_dims_inputs = [dim for dim in inputs.dims if dim != 'time'] extra_dims_outputs = [dim for dim in outputs.dims if dim != 'time'] - extra_dims = list(set(extra_dims_inputs + extra_dims_outputs)) + extra_dims = sorted(set(extra_dims_inputs + extra_dims_outputs)) if extra_dims: auto_select = {} @@ -1222,27 +1336,28 @@ def plot_node_balance_pie( f'Use select={{"{dim}": value}} to choose a different value.' ) - # Apply auto-selection - inputs = inputs.sel(auto_select) - outputs = outputs.sel(auto_select) + # Apply auto-selection only for coords present in each dataset + inputs = inputs.sel({k: v for k, v in auto_select.items() if k in inputs.coords}) + outputs = outputs.sel({k: v for k, v in auto_select.items() if k in outputs.coords}) # Update suffix with auto-selected values auto_suffix_parts = [f'{dim}={val}' for dim, val in auto_select.items()] suffix_parts.extend(auto_suffix_parts) - suffix = '--' + '-'.join(suffix_parts) if suffix_parts else '' + suffix = '--' + '-'.join(sorted(set(suffix_parts))) if suffix_parts else '' title = f'{self.label} (total flow hours){suffix}' if engine == 'plotly': figure_like = plotting.dual_pie_with_plotly( - data_left=inputs.to_pandas(), - data_right=outputs.to_pandas(), + data_left=inputs, + data_right=outputs, colors=colors, title=title, text_info=text_info, subtitles=('Inputs', 'Outputs'), legend_title='Flows', lower_percentage_group=lower_percentage_group, + **plot_kwargs, ) default_filetype = '.html' elif engine == 'matplotlib': @@ -1255,6 +1370,7 @@ def plot_node_balance_pie( subtitles=('Inputs', 'Outputs'), legend_title='Flows', lower_percentage_group=lower_percentage_group, + **plot_kwargs, ) default_filetype = '.png' else: @@ -1267,6 +1383,7 @@ def plot_node_balance_pie( user_path=None if isinstance(save, bool) else pathlib.Path(save), show=show, save=True if save else False, + dpi=dpi, ) def node_balance( @@ -1373,6 +1490,7 @@ def plot_charge_state( facet_cols: int = 3, # Deprecated parameter (kept for backwards compatibility) indexer: dict[FlowSystemDimensions, Any] | None = None, + **plot_kwargs: Any, ) -> plotly.graph_objs.Figure: """Plot storage charge state over time, combined with the node balance with optional faceting and animation. @@ -1389,6 +1507,26 @@ def plot_charge_state( animate_by: Dimension to animate over (Plotly only). Creates animation frames that cycle through dimension values. Only one dimension can be animated. Ignored if not found. facet_cols: Number of columns in the facet grid layout (default: 3). + **plot_kwargs: Additional plotting customization options passed to underlying plotting functions. + + Common options: + + - **dpi** (int): Export resolution in dots per inch. Default: 300. + + **For Plotly engine:** + + - Any Plotly Express parameter for px.bar()/px.line()/px.area() + Example: `range_y=[0, 100]`, `line_shape='linear'` + + **For Matplotlib engine:** + + - **plot_kwargs** (dict): Customize plot via `ax.bar()` or `ax.step()`. + + See :func:`flixopt.plotting.with_plotly` and :func:`flixopt.plotting.with_matplotlib` + for complete parameter reference. + + Note: For Plotly, you can further customize the returned figure using `fig.update_traces()` + and `fig.update_layout()` after calling this method. Raises: ValueError: If component is not a storage. @@ -1409,6 +1547,16 @@ def plot_charge_state( Facet by scenario AND animate by period: >>> results['Storage'].plot_charge_state(facet_by='scenario', animate_by='period') + + Custom layout after creation: + + >>> fig = results['Storage'].plot_charge_state(show=False) + >>> fig.update_layout(template='plotly_dark', height=800) + >>> fig.show() + + High-resolution export: + + >>> results['Storage'].plot_charge_state(save='storage.png', dpi=600) """ # Handle deprecated indexer parameter if indexer is not None: @@ -1427,11 +1575,17 @@ def plot_charge_state( ) select = indexer + # Extract dpi for export_figure + dpi = plot_kwargs.pop('dpi', None) # None uses CONFIG.Plotting.default_dpi + + # Extract charge state line color (for overlay customization) + overlay_color = plot_kwargs.pop('charge_state_line_color', 'black') + if not self.is_storage: raise ValueError(f'Cant plot charge_state. "{self.label}" is not a storage') # Get node balance and charge state - ds = self.node_balance(with_last_timestep=True) + ds = self.node_balance(with_last_timestep=True).fillna(0) charge_state_da = self.charge_state # Apply select filtering @@ -1451,11 +1605,12 @@ def plot_charge_state( mode=mode, title=title, facet_cols=facet_cols, + xlabel='Time in h', + **plot_kwargs, ) - # Create a dataset with just charge_state and plot it as lines - # This ensures proper handling of facets and animation - charge_state_ds = charge_state_da.to_dataset(name=self._charge_state) + # Prepare charge_state as Dataset for plotting + charge_state_ds = xr.Dataset({self._charge_state: charge_state_da}) # Plot charge_state with mode='line' to get Scatter traces charge_state_fig = plotting.with_plotly( @@ -1466,6 +1621,8 @@ def plot_charge_state( mode='line', # Always line for charge_state title='', # No title needed for this temp figure facet_cols=facet_cols, + xlabel='Time in h', + **plot_kwargs, ) # Add charge_state traces to the main figure @@ -1473,6 +1630,7 @@ def plot_charge_state( for trace in charge_state_fig.data: trace.line.width = 2 # Make charge_state line more prominent trace.line.shape = 'linear' # Smooth line for charge state (not stepped like flows) + trace.line.color = overlay_color figure_like.add_trace(trace) # Also add traces from animation frames if they exist @@ -1484,6 +1642,7 @@ def plot_charge_state( for trace in frame.data: trace.line.width = 2 trace.line.shape = 'linear' # Smooth line for charge state + trace.line.color = overlay_color figure_like.frames[i].data = figure_like.frames[i].data + (trace,) default_filetype = '.html' @@ -1497,10 +1656,11 @@ def plot_charge_state( ) # For matplotlib, plot flows (node balance), then add charge_state as line fig, ax = plotting.with_matplotlib( - ds.to_dataframe(), + ds, colors=colors, mode=mode, title=title, + **plot_kwargs, ) # Add charge_state as a line overlay @@ -1510,9 +1670,18 @@ def plot_charge_state( charge_state_df.values.flatten(), label=self._charge_state, linewidth=2, - color='black', + color=overlay_color, + ) + # Recreate legend with the same styling as with_matplotlib + handles, labels = ax.get_legend_handles_labels() + ax.legend( + handles, + labels, + loc='upper center', + bbox_to_anchor=(0.5, -0.15), + ncol=5, + frameon=False, ) - ax.legend() fig.tight_layout() figure_like = fig, ax @@ -1525,6 +1694,7 @@ def plot_charge_state( user_path=None if isinstance(save, bool) else pathlib.Path(save), show=show, save=True if save else False, + dpi=dpi, ) def node_balance_with_charge_state( @@ -1810,6 +1980,7 @@ def plot_heatmap( heatmap_timeframes: Literal['YS', 'MS', 'W', 'D', 'h', '15min', 'min'] | None = None, heatmap_timesteps_per_frame: Literal['W', 'D', 'h', '15min', 'min'] | None = None, color_map: str | None = None, + **plot_kwargs: Any, ) -> plotly.graph_objs.Figure | tuple[plt.Figure, plt.Axes]: """Plot heatmap of variable solution across segments. @@ -1830,6 +2001,17 @@ def plot_heatmap( heatmap_timeframes: (Deprecated) Use reshape_time instead. heatmap_timesteps_per_frame: (Deprecated) Use reshape_time instead. color_map: (Deprecated) Use colors instead. + **plot_kwargs: Additional plotting customization options. + Common options: + + - **dpi** (int): Export resolution for saved plots. Default: 300. + - **vmin** (float): Minimum value for color scale. + - **vmax** (float): Maximum value for color scale. + + For Matplotlib heatmaps: + + - **imshow_kwargs** (dict): Additional kwargs for matplotlib's imshow. + - **cbar_kwargs** (dict): Additional kwargs for colorbar customization. Returns: Figure object. @@ -1884,6 +2066,7 @@ def plot_heatmap( animate_by=animate_by, facet_cols=facet_cols, fill=fill, + **plot_kwargs, ) def to_file(self, folder: str | pathlib.Path | None = None, name: str | None = None, compression: int = 5): @@ -1933,6 +2116,7 @@ def plot_heatmap( heatmap_timeframes: Literal['YS', 'MS', 'W', 'D', 'h', '15min', 'min'] | None = None, heatmap_timesteps_per_frame: Literal['W', 'D', 'h', '15min', 'min'] | None = None, color_map: str | None = None, + **plot_kwargs: Any, ): """Plot heatmap visualization with support for multi-variable, faceting, and animation. @@ -2087,6 +2271,9 @@ def plot_heatmap( timeframes, timesteps_per_frame = reshape_time title += f' ({timeframes} vs {timesteps_per_frame})' + # Extract dpi before passing to plotting functions + dpi = plot_kwargs.pop('dpi', None) # None uses CONFIG.Plotting.default_dpi + # Plot with appropriate engine if engine == 'plotly': figure_like = plotting.heatmap_with_plotly( @@ -2098,6 +2285,7 @@ def plot_heatmap( facet_cols=facet_cols, reshape_time=reshape_time, fill=fill, + **plot_kwargs, ) default_filetype = '.html' elif engine == 'matplotlib': @@ -2107,6 +2295,7 @@ def plot_heatmap( title=title, reshape_time=reshape_time, fill=fill, + **plot_kwargs, ) default_filetype = '.png' else: @@ -2123,6 +2312,7 @@ def plot_heatmap( user_path=None if isinstance(save, bool) else pathlib.Path(save), show=show, save=True if save else False, + dpi=dpi, ) diff --git a/tests/test_plotting_api.py b/tests/test_plotting_api.py new file mode 100644 index 000000000..141623cae --- /dev/null +++ b/tests/test_plotting_api.py @@ -0,0 +1,138 @@ +"""Smoke tests for plotting API robustness improvements.""" + +import numpy as np +import pandas as pd +import pytest +import xarray as xr + +from flixopt import plotting + + +@pytest.fixture +def sample_dataset(): + """Create a sample xarray Dataset for testing.""" + rng = np.random.default_rng(0) + time = np.arange(10) + data = xr.Dataset( + { + 'var1': (['time'], rng.random(10)), + 'var2': (['time'], rng.random(10)), + 'var3': (['time'], rng.random(10)), + }, + coords={'time': time}, + ) + return data + + +@pytest.fixture +def sample_dataframe(): + """Create a sample pandas DataFrame for testing.""" + rng = np.random.default_rng(1) + time = np.arange(10) + df = pd.DataFrame({'var1': rng.random(10), 'var2': rng.random(10), 'var3': rng.random(10)}, index=time) + df.index.name = 'time' + return df + + +def test_kwargs_passthrough_plotly(sample_dataset): + """Test that px_kwargs are passed through and figure can be customized after creation.""" + # Test that px_kwargs are passed through + fig = plotting.with_plotly( + sample_dataset, + mode='line', + range_y=[0, 100], + ) + assert list(fig.layout.yaxis.range) == [0, 100] + + # Test that figure can be customized after creation + fig.update_traces(line={'width': 5}) + fig.update_layout(width=1200, height=600) + assert fig.layout.width == 1200 + assert fig.layout.height == 600 + assert all(getattr(t, 'line', None) and t.line.width == 5 for t in fig.data) + + +def test_dataframe_support_plotly(sample_dataframe): + """Test that DataFrames are accepted by plotting functions.""" + fig = plotting.with_plotly(sample_dataframe, mode='line') + assert fig is not None + + +def test_data_validation_non_numeric(): + """Test that validation catches non-numeric data.""" + data = xr.Dataset({'var1': (['time'], ['a', 'b', 'c'])}, coords={'time': [0, 1, 2]}) + + with pytest.raises(TypeError, match='non-?numeric'): + plotting.with_plotly(data) + + +def test_ensure_dataset_invalid_type(): + """Test that invalid types raise error via the public API.""" + with pytest.raises(TypeError, match='xr\\.Dataset|pd\\.DataFrame'): + plotting.with_plotly([1, 2, 3], mode='line') + + +@pytest.mark.parametrize( + 'engine,mode,data_type', + [ + *[ + (e, m, dt) + for e in ['plotly', 'matplotlib'] + for m in ['stacked_bar', 'line', 'area', 'grouped_bar'] + for dt in ['dataset', 'dataframe', 'series'] + if not (e == 'matplotlib' and m in ['area', 'grouped_bar']) + ], + ], +) +def test_all_data_types_and_modes(engine, mode, data_type): + """Test that Dataset, DataFrame, and Series work with all plotting modes.""" + time = pd.date_range('2020-01-01', periods=5, freq='h') + + data = { + 'dataset': xr.Dataset( + {'A': (['time'], [1, 2, 3, 4, 5]), 'B': (['time'], [5, 4, 3, 2, 1])}, coords={'time': time} + ), + 'dataframe': pd.DataFrame({'A': [1, 2, 3, 4, 5], 'B': [5, 4, 3, 2, 1]}, index=time), + 'series': pd.Series([1, 2, 3, 4, 5], index=time, name='A'), + }[data_type] + + if engine == 'plotly': + fig = plotting.with_plotly(data, mode=mode) + assert fig is not None and len(fig.data) > 0 + else: + fig, ax = plotting.with_matplotlib(data, mode=mode) + assert fig is not None and ax is not None + + +@pytest.mark.parametrize( + 'engine,data_type', [(e, dt) for e in ['plotly', 'matplotlib'] for dt in ['dataset', 'dataframe', 'series']] +) +def test_pie_plots(engine, data_type): + """Test pie charts with all data types, including automatic summing.""" + time = pd.date_range('2020-01-01', periods=5, freq='h') + + # Single-value data + single_data = { + 'dataset': xr.Dataset({'A': xr.DataArray(10), 'B': xr.DataArray(20), 'C': xr.DataArray(30)}), + 'dataframe': pd.DataFrame({'A': [10], 'B': [20], 'C': [30]}), + 'series': pd.Series({'A': 10, 'B': 20, 'C': 30}), + }[data_type] + + # Multi-dimensional data (for summing test) + multi_data = { + 'dataset': xr.Dataset( + {'A': (['time'], [1, 2, 3, 4, 5]), 'B': (['time'], [5, 5, 5, 5, 5])}, coords={'time': time} + ), + 'dataframe': pd.DataFrame({'A': [1, 2, 3, 4, 5], 'B': [5, 5, 5, 5, 5]}, index=time), + 'series': pd.Series([1, 2, 3, 4, 5], index=time, name='A'), + }[data_type] + + for data in [single_data, multi_data]: + if engine == 'plotly': + fig = plotting.dual_pie_with_plotly(data, data) + assert fig is not None and len(fig.data) >= 2 + if data is multi_data and data_type != 'series': + assert sum(fig.data[0].values) == pytest.approx(40) + else: + fig, axes = plotting.dual_pie_with_matplotlib(data, data) + assert fig is not None and len(axes) == 2 From ae56e6c659012dc2ff9931da078b1efd614e20ca Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 24 Oct 2025 16:25:53 +0200 Subject: [PATCH 374/448] Feature/component colors (#440) * Add new config options for plotting * Use turbo instead of viridis * Update plotting.py to use updated color management * update color management * Add rgb to hex for matplotlib * Add rgb to hex for matplotlib * Remove colormanager class * Update type hints * Update type hints and use Config defaults * Add stable colors * V1 * V2 * Use calculation.colors if direct colors is None * Bugfix * Bugfix * Update setup_colors * Add color setup to examples * Final touches * Update CHANGELOG.md * Update CHANGELOG.md * Bugfix * Update fro SegmentedCalculationResults * Default show = False in tests * Bugfix * Bugfix * Add show default to plot_network * Make _rgb_string_to_hex more robust * Improve Error Handling * Overwrite colors explicitly in setup_colors * Improve config loader * Update CHANGELOG.md * Make colors arg always overwrite the default behaviour --- CHANGELOG.md | 30 +- examples/01_Simple/simple_example.py | 3 + examples/04_Scenarios/scenario_example.py | 9 + flixopt/aggregation.py | 11 +- flixopt/color_processing.py | 261 +++++++++++++++ flixopt/config.py | 54 +++ flixopt/flow_system.py | 8 +- flixopt/plotting.py | 379 +++++----------------- flixopt/results.py | 293 +++++++++++++++-- tests/conftest.py | 2 + tests/test_results_plots.py | 4 +- 11 files changed, 729 insertions(+), 325 deletions(-) create mode 100644 flixopt/color_processing.py diff --git a/CHANGELOG.md b/CHANGELOG.md index ca9600f04..8eb16a4c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,17 +54,39 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp ### ✨ Added - Support for plotting kwargs in `results.py`, passed to plotly express and matplotlib. +- **Color management system**: New `color_processing.py` module with `process_colors()` function for unified color handling across plotting backends + - Supports flexible color inputs: colorscale names (e.g., 'turbo', 'plasma'), color lists, and label-to-color dictionaries + - Automatic fallback handling when requested colorscales are unavailable + - Seamless integration with both Plotly and Matplotlib colorscales + - Automatic rgba→hex color conversion for Matplotlib compatibility +- **Component color grouping**: Added `setup_colors()` method to `CalculationResults` and `SegmentedCalculationResults` to create color mappings with similar colors for all variables of a component + - Allows grouping components by custom colorscales: `{'CHP': 'red', 'Greys': ['Gastarif', 'Einspeisung'], 'Storage': 'blue'}` + - Colors are automatically assigned using default colorscale if not specified + - For segmented calculations, colors are propagated to all segments for consistent visualization + - Explicit `colors` arguments in plot methods override configured colors (when provided) +- **Plotting configuration**: New `CONFIG.Plotting` section with extensive customization options: + - `default_show`: Control default visibility of plots + - `default_engine`: Choose between 'plotly' or 'matplotlib' + - `default_dpi`: Configure resolution for saved plots (with matplotlib) + - `default_facet_cols`: Set default columns for faceted plots + - `default_sequential_colorscale`: Default for heatmaps and continuous data (default: 'turbo') + - `default_qualitative_colorscale`: Default for categorical plots (default: 'plotly') ### 💥 Breaking Changes ### ♻️ Changed - **Template integration**: Plotly templates now fully control plot styling without hardcoded overrides - **Dataset first plotting**: Underlying plotting methods in `plotting.py` now use `xr.Dataset` as the main datatype. DataFrames are automatically converted via `_ensure_dataset()`. Both DataFrames and Datasets can be passed to plotting functions without code changes. +- **Color terminology**: Standardized terminology from "colormap" to "colorscale" throughout the codebase for consistency with Plotly conventions +- **Default colorscales**: Changed default sequential colorscale from 'viridis' to 'turbo' for better perceptual uniformity; qualitative colorscale now defaults to 'plotly' +- **Aggregation plotting**: `Aggregation.plot()` now respects `CONFIG.Plotting.default_qualitative_colorscale` and uses `process_colors()` for consistent color handling ### 🗑️ Deprecated ### 🔥 Removed -- Removed `plotting.pie_with_plotly()` method as it was not used +- Removed `plotting.pie_with_plotly()` method as it was not used +- Removed `ColorProcessor` class - replaced by simpler `process_colors()` function +- Removed `resolve_colors()` helper function - color resolution now handled directly by `process_colors()` ### 🐛 Fixed - Improved error messages for `engine='matplotlib'` with multidimensional data @@ -76,9 +98,15 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp ### 📝 Docs - Moved `linked_periods` into correct section of the docstring (was in deprecated params) +- Updated terminology in docstrings from "colormap" to "colorscale" for consistency +- Enhanced examples to demonstrate `setup_colors()` usage: + - `simple_example.py`: Shows automatic color assignment and optional custom configuration + - `scenario_example.py`: Demonstrates component grouping with custom colorscales ### 👷 Development - Fixed concurrency issue in CI +- **Code architecture**: Extracted color processing logic into dedicated `color_processing.py` module for better separation of concerns +- Refactored from class-based (`ColorProcessor`) to function-based color handling for simpler API and reduced complexity ### 🚧 Known Issues diff --git a/examples/01_Simple/simple_example.py b/examples/01_Simple/simple_example.py index 906c24622..6b62d6712 100644 --- a/examples/01_Simple/simple_example.py +++ b/examples/01_Simple/simple_example.py @@ -112,6 +112,9 @@ calculation.solve(fx.solvers.HighsSolver(mip_gap=0, time_limit_seconds=30)) # --- Analyze Results --- + # Colors are automatically assigned using default colormap + # Optional: Configure custom colors with + calculation.results.setup_colors() calculation.results['Fernwärme'].plot_node_balance_pie() calculation.results['Fernwärme'].plot_node_balance() calculation.results['Storage'].plot_charge_state() diff --git a/examples/04_Scenarios/scenario_example.py b/examples/04_Scenarios/scenario_example.py index 834e55782..d258d4142 100644 --- a/examples/04_Scenarios/scenario_example.py +++ b/examples/04_Scenarios/scenario_example.py @@ -196,6 +196,15 @@ # --- Solve the Calculation and Save Results --- calculation.solve(fx.solvers.HighsSolver(mip_gap=0, time_limit_seconds=30)) + calculation.results.setup_colors( + { + 'CHP': 'red', + 'Greys': ['Gastarif', 'Einspeisung', 'Heat Demand'], + 'Storage': 'blue', + 'Boiler': 'orange', + } + ) + calculation.results.plot_heatmap('CHP(Q_th)|flow_rate') # --- Analyze Results --- diff --git a/flixopt/aggregation.py b/flixopt/aggregation.py index 53770e140..cd0fdde3c 100644 --- a/flixopt/aggregation.py +++ b/flixopt/aggregation.py @@ -20,7 +20,9 @@ except ImportError: TSAM_AVAILABLE = False +from .color_processing import process_colors from .components import Storage +from .config import CONFIG from .structure import ( FlowSystemModel, Submodel, @@ -141,7 +143,7 @@ def describe_clusters(self) -> str: def use_extreme_periods(self): return self.time_series_for_high_peaks or self.time_series_for_low_peaks - def plot(self, colormap: str = 'viridis', show: bool = True, save: pathlib.Path | None = None) -> go.Figure: + def plot(self, colormap: str | None = None, show: bool = True, save: pathlib.Path | None = None) -> go.Figure: from . import plotting df_org = self.original_data.copy().rename( @@ -150,10 +152,13 @@ def plot(self, colormap: str = 'viridis', show: bool = True, save: pathlib.Path df_agg = self.aggregated_data.copy().rename( columns={col: f'Aggregated - {col}' for col in self.aggregated_data.columns} ) - fig = plotting.with_plotly(df_org.to_xarray(), 'line', colors=colormap, xlabel='Time in h') + colors = list( + process_colors(colormap or CONFIG.Plotting.default_qualitative_colorscale, list(df_org.columns)).values() + ) + fig = plotting.with_plotly(df_org.to_xarray(), 'line', colors=colors, xlabel='Time in h') for trace in fig.data: trace.update(dict(line=dict(dash='dash'))) - fig2 = plotting.with_plotly(df_agg.to_xarray(), 'line', colors=colormap, xlabel='Time in h') + fig2 = plotting.with_plotly(df_agg.to_xarray(), 'line', colors=colors, xlabel='Time in h') for trace in fig2.data: fig.add_trace(trace) diff --git a/flixopt/color_processing.py b/flixopt/color_processing.py new file mode 100644 index 000000000..2959acc82 --- /dev/null +++ b/flixopt/color_processing.py @@ -0,0 +1,261 @@ +"""Simplified color handling for visualization. + +This module provides clean color processing that transforms various input formats +into a label-to-color mapping dictionary, without needing to know about the plotting engine. +""" + +from __future__ import annotations + +import logging + +import matplotlib.colors as mcolors +import matplotlib.pyplot as plt +import plotly.express as px +from plotly.exceptions import PlotlyError + +logger = logging.getLogger('flixopt') + + +def _rgb_string_to_hex(color: str) -> str: + """Convert Plotly RGB/RGBA string format to hex. + + Args: + color: Color in format 'rgb(R, G, B)', 'rgba(R, G, B, A)' or already in hex + + Returns: + Color in hex format '#RRGGBB' + """ + color = color.strip() + + # If already hex, return as-is + if color.startswith('#'): + return color + + # Try to parse rgb() or rgba() + try: + if color.startswith('rgb('): + # Extract RGB values from 'rgb(R, G, B)' format + rgb_str = color[4:-1] # Remove 'rgb(' and ')' + elif color.startswith('rgba('): + # Extract RGBA values from 'rgba(R, G, B, A)' format + rgb_str = color[5:-1] # Remove 'rgba(' and ')' + else: + return color + + # Split on commas and parse first three components + components = rgb_str.split(',') + if len(components) < 3: + return color + + # Parse and clamp the first three components + r = max(0, min(255, int(round(float(components[0].strip()))))) + g = max(0, min(255, int(round(float(components[1].strip()))))) + b = max(0, min(255, int(round(float(components[2].strip()))))) + + return f'#{r:02x}{g:02x}{b:02x}' + except (ValueError, IndexError): + # If parsing fails, return original + return color + + +def process_colors( + colors: None | str | list[str] | dict[str, str], + labels: list[str], + default_colorscale: str = 'turbo', +) -> dict[str, str]: + """Process color input and return a label-to-color mapping. + + This function takes flexible color input and always returns a dictionary + mapping each label to a specific color string. The plotting engine can then + use this mapping as needed. + + Args: + colors: Color specification in one of four formats: + - None: Use the default colorscale + - str: Name of a colorscale (e.g., 'turbo', 'plasma', 'Set1', 'portland') + - list[str]: List of color strings (hex, named colors, etc.) + - dict[str, str]: Direct label-to-color mapping + labels: List of labels that need colors assigned + default_colorscale: Fallback colorscale name if requested scale not found + + Returns: + Dictionary mapping each label to a color string + + Examples: + >>> # Using None - applies default colorscale + >>> process_colors(None, ['A', 'B', 'C']) + {'A': '#0d0887', 'B': '#7e03a8', 'C': '#cc4778'} + + >>> # Using a colorscale name + >>> process_colors('plasma', ['A', 'B', 'C']) + {'A': '#0d0887', 'B': '#7e03a8', 'C': '#cc4778'} + + >>> # Using a list of colors + >>> process_colors(['red', 'blue', 'green'], ['A', 'B', 'C']) + {'A': 'red', 'B': 'blue', 'C': 'green'} + + >>> # Using a pre-made mapping + >>> process_colors({'A': 'red', 'B': 'blue'}, ['A', 'B', 'C']) + {'A': 'red', 'B': 'blue', 'C': '#0d0887'} # C gets color from default scale + """ + if not labels: + return {} + + # Case 1: Already a mapping dictionary + if isinstance(colors, dict): + return _fill_missing_colors(colors, labels, default_colorscale) + + # Case 2: None or colorscale name (string) + if colors is None or isinstance(colors, str): + colorscale_name = colors if colors is not None else default_colorscale + color_list = _get_colors_from_scale(colorscale_name, len(labels), default_colorscale) + return dict(zip(labels, color_list, strict=False)) + + # Case 3: List of colors + if isinstance(colors, list): + if len(colors) == 0: + logger.warning(f'Empty color list provided. Using {default_colorscale} instead.') + color_list = _get_colors_from_scale(default_colorscale, len(labels), default_colorscale) + return dict(zip(labels, color_list, strict=False)) + + if len(colors) < len(labels): + logger.debug( + f'Not enough colors provided ({len(colors)}) for all labels ({len(labels)}). Colors will cycle.' + ) + + # Cycle through colors if we don't have enough + return {label: colors[i % len(colors)] for i, label in enumerate(labels)} + + raise TypeError(f'colors must be None, str, list, or dict, got {type(colors)}') + + +def _fill_missing_colors( + color_mapping: dict[str, str], + labels: list[str], + default_colorscale: str, +) -> dict[str, str]: + """Fill in missing labels in a color mapping using a colorscale. + + Args: + color_mapping: Partial label-to-color mapping + labels: All labels that need colors + default_colorscale: Colorscale to use for missing labels + + Returns: + Complete label-to-color mapping + """ + missing_labels = [label for label in labels if label not in color_mapping] + + if not missing_labels: + return color_mapping.copy() + + # Log warning about missing labels + logger.debug(f'Labels missing colors: {missing_labels}. Using {default_colorscale} for these.') + + # Get colors for missing labels + missing_colors = _get_colors_from_scale(default_colorscale, len(missing_labels), default_colorscale) + + # Combine existing and new colors + result = color_mapping.copy() + result.update(dict(zip(missing_labels, missing_colors, strict=False))) + return result + + +def _get_colors_from_scale( + colorscale_name: str, + num_colors: int, + fallback_scale: str, +) -> list[str]: + """Extract a list of colors from a named colorscale. + + Tries to get colors from the named scale (Plotly first, then Matplotlib), + falls back to the fallback scale if not found. + + Args: + colorscale_name: Name of the colorscale to try + num_colors: Number of colors needed + fallback_scale: Fallback colorscale name if first fails + + Returns: + List of color strings (hex format) + """ + # Try to get the requested colorscale + colors = _try_get_colorscale(colorscale_name, num_colors) + + if colors is not None: + return colors + + # Fallback to default + logger.warning(f"Colorscale '{colorscale_name}' not found. Using '{fallback_scale}' instead.") + + colors = _try_get_colorscale(fallback_scale, num_colors) + + if colors is not None: + return colors + + # Ultimate fallback: just use basic colors + logger.warning(f"Fallback colorscale '{fallback_scale}' also not found. Using basic colors.") + basic_colors = [ + '#1f77b4', + '#ff7f0e', + '#2ca02c', + '#d62728', + '#9467bd', + '#8c564b', + '#e377c2', + '#7f7f7f', + '#bcbd22', + '#17becf', + ] + return [basic_colors[i % len(basic_colors)] for i in range(num_colors)] + + +def _try_get_colorscale(colorscale_name: str, num_colors: int) -> list[str] | None: + """Try to get colors from Plotly or Matplotlib colorscales. + + Tries Plotly colorscales first (both qualitative and sequential), + then falls back to Matplotlib colorscales. + + Args: + colorscale_name: Name of the colorscale + num_colors: Number of colors needed + + Returns: + List of color strings (hex format) if successful, None if colorscale not found + """ + # First try Plotly qualitative (discrete) color sequences + colorscale_title = colorscale_name.title() + if hasattr(px.colors.qualitative, colorscale_title): + color_list = getattr(px.colors.qualitative, colorscale_title) + # Convert to hex format for matplotlib compatibility + return [_rgb_string_to_hex(color_list[i % len(color_list)]) for i in range(num_colors)] + + # Then try Plotly sequential/continuous colorscales + try: + colorscale = px.colors.get_colorscale(colorscale_name) + # Sample evenly from the colorscale + if num_colors == 1: + sample_points = [0.5] + else: + sample_points = [i / (num_colors - 1) for i in range(num_colors)] + colors = px.colors.sample_colorscale(colorscale, sample_points) + # Convert to hex format for matplotlib compatibility + return [_rgb_string_to_hex(c) for c in colors] + except (PlotlyError, ValueError): + pass + + # Finally try Matplotlib colorscales + try: + cmap = plt.get_cmap(colorscale_name) + + # Sample evenly from the colorscale + if num_colors == 1: + colors = [cmap(0.5)] + else: + colors = [cmap(i / (num_colors - 1)) for i in range(num_colors)] + + # Convert RGBA tuples to hex strings + return [mcolors.rgb2hex(color[:3]) for color in colors] + + except (ValueError, KeyError): + return None diff --git a/flixopt/config.py b/flixopt/config.py index a7549a3ec..b7162e55f 100644 --- a/flixopt/config.py +++ b/flixopt/config.py @@ -54,6 +54,16 @@ 'big_binary_bound': 100_000, } ), + 'plotting': MappingProxyType( + { + 'default_show': True, + 'default_engine': 'plotly', + 'default_dpi': 300, + 'default_facet_cols': 3, + 'default_sequential_colorscale': 'turbo', + 'default_qualitative_colorscale': 'plotly', + } + ), } ) @@ -185,6 +195,42 @@ class Modeling: epsilon: float = _DEFAULTS['modeling']['epsilon'] big_binary_bound: int = _DEFAULTS['modeling']['big_binary_bound'] + class Plotting: + """Plotting configuration. + + Configure backends via environment variables: + - Matplotlib: Set `MPLBACKEND` environment variable (e.g., 'Agg', 'TkAgg') + - Plotly: Set `PLOTLY_RENDERER` or use `plotly.io.renderers.default` + + Attributes: + default_show: Default value for the `show` parameter in plot methods. + default_engine: Default plotting engine. + default_dpi: Default DPI for saved plots. + default_facet_cols: Default number of columns for faceted plots. + default_sequential_colorscale: Default colorscale for heatmaps and continuous data. + default_qualitative_colorscale: Default colormap for categorical plots (bar/line/area charts). + + Examples: + ```python + # Set consistent theming + CONFIG.Plotting.plotly_template = 'plotly_dark' + CONFIG.apply() + + # Configure default export and color settings + CONFIG.Plotting.default_dpi = 600 + CONFIG.Plotting.default_sequential_colorscale = 'plasma' + CONFIG.Plotting.default_qualitative_colorscale = 'Dark24' + CONFIG.apply() + ``` + """ + + default_show: bool = _DEFAULTS['plotting']['default_show'] + default_engine: Literal['plotly', 'matplotlib'] = _DEFAULTS['plotting']['default_engine'] + default_dpi: int = _DEFAULTS['plotting']['default_dpi'] + default_facet_cols: int = _DEFAULTS['plotting']['default_facet_cols'] + default_sequential_colorscale: str = _DEFAULTS['plotting']['default_sequential_colorscale'] + default_qualitative_colorscale: str = _DEFAULTS['plotting']['default_qualitative_colorscale'] + config_name: str = _DEFAULTS['config_name'] @classmethod @@ -319,6 +365,14 @@ def to_dict(cls) -> dict: 'epsilon': cls.Modeling.epsilon, 'big_binary_bound': cls.Modeling.big_binary_bound, }, + 'plotting': { + 'default_show': cls.Plotting.default_show, + 'default_engine': cls.Plotting.default_engine, + 'default_dpi': cls.Plotting.default_dpi, + 'default_facet_cols': cls.Plotting.default_facet_cols, + 'default_sequential_colorscale': cls.Plotting.default_sequential_colorscale, + 'default_qualitative_colorscale': cls.Plotting.default_qualitative_colorscale, + }, } diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index ad43c183b..fd0f6a98d 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -4,7 +4,6 @@ from __future__ import annotations -import json import logging import warnings from typing import TYPE_CHECKING, Any, Literal, Optional @@ -13,6 +12,7 @@ import pandas as pd import xarray as xr +from .config import CONFIG from .core import ( ConversionError, DataConverter, @@ -484,7 +484,7 @@ def plot_network( | list[ Literal['nodes', 'edges', 'layout', 'interaction', 'manipulation', 'physics', 'selection', 'renderer'] ] = True, - show: bool = False, + show: bool | None = None, ) -> pyvis.network.Network | None: """ Visualizes the network structure of a FlowSystem using PyVis, saving it as an interactive HTML file. @@ -514,7 +514,9 @@ def plot_network( from . import plotting node_infos, edge_infos = self.network_infos() - return plotting.plot_network(node_infos, edge_infos, path, controls, show) + return plotting.plot_network( + node_infos, edge_infos, path, controls, show if show is not None else CONFIG.Plotting.default_show + ) def start_network_app(self): """Visualizes the network structure of a FlowSystem using Dash, Cytoscape, and networkx. diff --git a/flixopt/plotting.py b/flixopt/plotting.py index a024c97fc..045cf7e99 100644 --- a/flixopt/plotting.py +++ b/flixopt/plotting.py @@ -40,8 +40,8 @@ import plotly.graph_objects as go import plotly.offline import xarray as xr -from plotly.exceptions import PlotlyError +from .color_processing import process_colors from .config import CONFIG if TYPE_CHECKING: @@ -49,7 +49,7 @@ logger = logging.getLogger('flixopt') -# Define the colors for the 'portland' colormap in matplotlib +# Define the colors for the 'portland' colorscale in matplotlib _portland_colors = [ [12 / 255, 51 / 255, 131 / 255], # Dark blue [10 / 255, 136 / 255, 186 / 255], # Light blue @@ -58,7 +58,7 @@ [217 / 255, 30 / 255, 30 / 255], # Red ] -# Check if the colormap already exists before registering it +# Check if the colorscale already exists before registering it if hasattr(plt, 'colormaps'): # Matplotlib >= 3.7 registry = plt.colormaps if 'portland' not in registry: @@ -73,9 +73,9 @@ Color specifications can take several forms to accommodate different use cases: -**Named Colormaps** (str): - - Standard colormaps: 'viridis', 'plasma', 'cividis', 'tab10', 'Set1' - - Energy-focused: 'portland' (custom flixopt colormap for energy systems) +**Named colorscales** (str): + - Standard colorscales: 'turbo', 'plasma', 'cividis', 'tab10', 'Set1' + - Energy-focused: 'portland' (custom flixopt colorscale for energy systems) - Backend-specific maps available in Plotly and Matplotlib **Color Lists** (list[str]): @@ -90,8 +90,8 @@ Examples: ```python - # Named colormap - colors = 'viridis' # Automatic color generation + # Named colorscale + colors = 'turbo' # Automatic color generation # Explicit color list colors = ['red', 'blue', 'green', '#FFD700'] @@ -114,7 +114,7 @@ References: - HTML Color Names: https://htmlcolorcodes.com/color-names/ - - Matplotlib Colormaps: https://matplotlib.org/stable/tutorials/colors/colormaps.html + - Matplotlib colorscales: https://matplotlib.org/stable/tutorials/colors/colorscales.html - Plotly Built-in Colorscales: https://plotly.com/python/builtin-colorscales/ """ @@ -122,212 +122,6 @@ """Identifier for the plotting engine to use.""" -class ColorProcessor: - """Intelligent color management system for consistent multi-backend visualization. - - This class provides unified color processing across Plotly and Matplotlib backends, - ensuring consistent visual appearance regardless of the plotting engine used. - It handles color palette generation, named colormap translation, and intelligent - color cycling for complex datasets with many categories. - - Key Features: - **Backend Agnostic**: Automatic color format conversion between engines - **Palette Management**: Support for named colormaps, custom palettes, and color lists - **Intelligent Cycling**: Smart color assignment for datasets with many categories - **Fallback Handling**: Graceful degradation when requested colormaps are unavailable - **Energy System Colors**: Built-in palettes optimized for energy system visualization - - Color Input Types: - - **Named Colormaps**: 'viridis', 'plasma', 'portland', 'tab10', etc. - - **Color Lists**: ['red', 'blue', 'green'] or ['#FF0000', '#0000FF', '#00FF00'] - - **Label Dictionaries**: {'Generator': 'red', 'Storage': 'blue', 'Load': 'green'} - - Examples: - Basic color processing: - - ```python - # Initialize for Plotly backend - processor = ColorProcessor(engine='plotly', default_colormap='viridis') - - # Process different color specifications - colors = processor.process_colors('plasma', ['Gen1', 'Gen2', 'Storage']) - colors = processor.process_colors(['red', 'blue', 'green'], ['A', 'B', 'C']) - colors = processor.process_colors({'Wind': 'skyblue', 'Solar': 'gold'}, ['Wind', 'Solar', 'Gas']) - - # Switch to Matplotlib - processor = ColorProcessor(engine='matplotlib') - mpl_colors = processor.process_colors('tab10', component_labels) - ``` - - Energy system visualization: - - ```python - # Specialized energy system palette - energy_colors = { - 'Natural_Gas': '#8B4513', # Brown - 'Electricity': '#FFD700', # Gold - 'Heat': '#FF4500', # Red-orange - 'Cooling': '#87CEEB', # Sky blue - 'Hydrogen': '#E6E6FA', # Lavender - 'Battery': '#32CD32', # Lime green - } - - processor = ColorProcessor('plotly') - flow_colors = processor.process_colors(energy_colors, flow_labels) - ``` - - Args: - engine: Plotting backend ('plotly' or 'matplotlib'). Determines output color format. - default_colormap: Fallback colormap when requested palettes are unavailable. - Common options: 'viridis', 'plasma', 'tab10', 'portland'. - - """ - - def __init__(self, engine: PlottingEngine = 'plotly', default_colormap: str = 'viridis'): - """Initialize the color processor with specified backend and defaults.""" - if engine not in ['plotly', 'matplotlib']: - raise TypeError(f'engine must be "plotly" or "matplotlib", but is {engine}') - self.engine = engine - self.default_colormap = default_colormap - - def _generate_colors_from_colormap(self, colormap_name: str, num_colors: int) -> list[Any]: - """ - Generate colors from a named colormap. - - Args: - colormap_name: Name of the colormap - num_colors: Number of colors to generate - - Returns: - list of colors in the format appropriate for the engine - """ - if self.engine == 'plotly': - try: - colorscale = px.colors.get_colorscale(colormap_name) - except PlotlyError as e: - logger.error(f"Colorscale '{colormap_name}' not found in Plotly. Using {self.default_colormap}: {e}") - colorscale = px.colors.get_colorscale(self.default_colormap) - - # Generate evenly spaced points - color_points = [i / (num_colors - 1) for i in range(num_colors)] if num_colors > 1 else [0] - return px.colors.sample_colorscale(colorscale, color_points) - - else: # matplotlib - try: - cmap = plt.get_cmap(colormap_name, num_colors) - except ValueError as e: - logger.error(f"Colormap '{colormap_name}' not found in Matplotlib. Using {self.default_colormap}: {e}") - cmap = plt.get_cmap(self.default_colormap, num_colors) - - return [cmap(i) for i in range(num_colors)] - - def _handle_color_list(self, colors: list[str], num_labels: int) -> list[str]: - """ - Handle a list of colors, cycling if necessary. - - Args: - colors: list of color strings - num_labels: Number of labels that need colors - - Returns: - list of colors matching the number of labels - """ - if len(colors) == 0: - logger.error(f'Empty color list provided. Using {self.default_colormap} instead.') - return self._generate_colors_from_colormap(self.default_colormap, num_labels) - - if len(colors) < num_labels: - logger.warning( - f'Not enough colors provided ({len(colors)}) for all labels ({num_labels}). Colors will cycle.' - ) - # Cycle through the colors - color_iter = itertools.cycle(colors) - return [next(color_iter) for _ in range(num_labels)] - else: - # Trim if necessary - if len(colors) > num_labels: - logger.warning( - f'More colors provided ({len(colors)}) than labels ({num_labels}). Extra colors will be ignored.' - ) - return colors[:num_labels] - - def _handle_color_dict(self, colors: dict[str, str], labels: list[str]) -> list[str]: - """ - Handle a dictionary mapping labels to colors. - - Args: - colors: Dictionary mapping labels to colors - labels: list of labels that need colors - - Returns: - list of colors in the same order as labels - """ - if len(colors) == 0: - logger.warning(f'Empty color dictionary provided. Using {self.default_colormap} instead.') - return self._generate_colors_from_colormap(self.default_colormap, len(labels)) - - # Find missing labels - missing_labels = sorted(set(labels) - set(colors.keys())) - if missing_labels: - logger.warning( - f'Some labels have no color specified: {missing_labels}. Using {self.default_colormap} for these.' - ) - - # Generate colors for missing labels - missing_colors = self._generate_colors_from_colormap(self.default_colormap, len(missing_labels)) - - # Create a copy to avoid modifying the original - colors_copy = colors.copy() - for i, label in enumerate(missing_labels): - colors_copy[label] = missing_colors[i] - else: - colors_copy = colors - - # Create color list in the same order as labels - return [colors_copy[label] for label in labels] - - def process_colors( - self, - colors: ColorType, - labels: list[str], - return_mapping: bool = False, - ) -> list[Any] | dict[str, Any]: - """ - Process colors for the specified labels. - - Args: - colors: Color specification (colormap name, list of colors, or label-to-color mapping) - labels: list of data labels that need colors assigned - return_mapping: If True, returns a dictionary mapping labels to colors; - if False, returns a list of colors in the same order as labels - - Returns: - Either a list of colors or a dictionary mapping labels to colors - """ - if len(labels) == 0: - logger.error('No labels provided for color assignment.') - return {} if return_mapping else [] - - # Process based on type of colors input - if isinstance(colors, str): - color_list = self._generate_colors_from_colormap(colors, len(labels)) - elif isinstance(colors, list): - color_list = self._handle_color_list(colors, len(labels)) - elif isinstance(colors, dict): - color_list = self._handle_color_dict(colors, labels) - else: - logger.error( - f'Unsupported color specification type: {type(colors)}. Using {self.default_colormap} instead.' - ) - color_list = self._generate_colors_from_colormap(self.default_colormap, len(labels)) - - # Return either a list or a mapping - if return_mapping: - return {label: color_list[i] for i, label in enumerate(labels)} - else: - return color_list - - def _ensure_dataset(data: xr.Dataset | pd.DataFrame | pd.Series) -> xr.Dataset: """Convert DataFrame or Series to Dataset if needed.""" if isinstance(data, xr.Dataset): @@ -373,31 +167,10 @@ def _validate_plotting_data(data: xr.Dataset, allow_empty: bool = False) -> None logger.debug(f"Variable '{var}' contains Inf values which may affect visualization.") -def resolve_colors( - data: xr.Dataset, - colors: ColorType, - engine: PlottingEngine = 'plotly', -) -> dict[str, str]: - """Resolve colors parameter to a dict mapping variable names to colors.""" - # Get variable names from Dataset (always strings and unique) - labels = list(data.data_vars.keys()) - - # If explicit dict provided, use it directly - if isinstance(colors, dict): - return colors - - # If string or list, use ColorProcessor (traditional behavior) - if isinstance(colors, (str, list)): - processor = ColorProcessor(engine=engine) - return processor.process_colors(colors, labels, return_mapping=True) - - raise TypeError(f'Wrong type passed to resolve_colors(): {type(colors)}') - - def with_plotly( data: xr.Dataset | pd.DataFrame | pd.Series, mode: Literal['stacked_bar', 'line', 'area', 'grouped_bar'] = 'stacked_bar', - colors: ColorType = 'viridis', + colors: ColorType | None = None, title: str = '', ylabel: str = '', xlabel: str = '', @@ -417,7 +190,7 @@ def with_plotly( data: An xarray Dataset, pandas DataFrame, or pandas Series to plot. mode: The plotting mode. Use 'stacked_bar' for stacked bar charts, 'line' for lines, 'area' for stacked area charts, or 'grouped_bar' for grouped bar charts. - colors: Color specification (colormap, list, or dict mapping labels to colors). + colors: Color specification (colorscale, list, or dict mapping labels to colors). title: The main title of the plot. ylabel: The label for the y-axis. xlabel: The label for the x-axis. @@ -476,9 +249,16 @@ def with_plotly( fig.update_layout(template='plotly_dark', width=1200, height=600) ``` """ + if colors is None: + colors = CONFIG.Plotting.default_qualitative_colorscale + if mode not in ('stacked_bar', 'line', 'area', 'grouped_bar'): raise ValueError(f"'mode' must be one of {{'stacked_bar','line','area', 'grouped_bar'}}, got {mode!r}") + # Apply CONFIG defaults if not explicitly set + if facet_cols is None: + facet_cols = CONFIG.Plotting.default_facet_cols + # Ensure data is a Dataset and validate it data = _ensure_dataset(data) _validate_plotting_data(data, allow_empty=True) @@ -496,7 +276,9 @@ def with_plotly( values = [float(data[var].values) for var in data.data_vars] # Resolve colors - color_discrete_map = resolve_colors(data, colors, engine='plotly') + color_discrete_map = process_colors( + colors, variables, default_colorscale=CONFIG.Plotting.default_qualitative_colorscale + ) marker_colors = [color_discrete_map.get(var, '#636EFA') for var in variables] # Create simple plot based on mode using go (not px) for better color control @@ -587,8 +369,9 @@ def with_plotly( # Process colors all_vars = df_long['variable'].unique().tolist() - processed_colors = ColorProcessor(engine='plotly').process_colors(colors, all_vars) - color_discrete_map = {var: color for var, color in zip(all_vars, processed_colors, strict=True)} + color_discrete_map = process_colors( + colors, all_vars, default_colorscale=CONFIG.Plotting.default_qualitative_colorscale + ) # Determine which dimension to use for x-axis # Collect dimensions used for faceting and animation @@ -705,7 +488,7 @@ def with_plotly( def with_matplotlib( data: xr.Dataset | pd.DataFrame | pd.Series, mode: Literal['stacked_bar', 'line'] = 'stacked_bar', - colors: ColorType = 'viridis', + colors: ColorType | None = None, title: str = '', ylabel: str = '', xlabel: str = 'Time in h', @@ -720,7 +503,7 @@ def with_matplotlib( the index represents time and each column represents a separate data series (variables). mode: Plotting mode. Use 'stacked_bar' for stacked bar charts or 'line' for stepped lines. colors: Color specification. Can be: - - A colormap name (e.g., 'turbo', 'plasma') + - A colorscale name (e.g., 'turbo', 'plasma') - A list of color strings (e.g., ['#ff0000', '#00ff00']) - A dict mapping column names to colors (e.g., {'Column1': '#ff0000'}) title: The title of the plot. @@ -738,6 +521,9 @@ def with_matplotlib( Negative values are stacked separately without extra labels in the legend. - If `mode` is 'line', stepped lines are drawn for each data series. """ + if colors is None: + colors = CONFIG.Plotting.default_qualitative_colorscale + if mode not in ('stacked_bar', 'line'): raise ValueError(f"'mode' must be one of {{'stacked_bar','line'}} for matplotlib, got {mode!r}") @@ -760,7 +546,9 @@ def with_matplotlib( values = [float(data[var].values) for var in data.data_vars] # Resolve colors - color_discrete_map = resolve_colors(data, colors, engine='matplotlib') + color_discrete_map = process_colors( + colors, variables, default_colorscale=CONFIG.Plotting.default_qualitative_colorscale + ) colors_list = [color_discrete_map.get(var, '#808080') for var in variables] # Create plot based on mode @@ -791,7 +579,9 @@ def with_matplotlib( return fig, ax # Resolve colors first (includes validation) - color_discrete_map = resolve_colors(data, colors, engine='matplotlib') + color_discrete_map = process_colors( + colors, list(data.data_vars), default_colorscale=CONFIG.Plotting.default_qualitative_colorscale + ) # Convert Dataset to DataFrame for matplotlib plotting (naturally wide-form) df = data.to_dataframe() @@ -1199,7 +989,7 @@ def preprocess_data_for_pie( def dual_pie_with_plotly( data_left: xr.Dataset | pd.DataFrame | pd.Series, data_right: xr.Dataset | pd.DataFrame | pd.Series, - colors: ColorType = 'viridis', + colors: ColorType | None = None, title: str = '', subtitles: tuple[str, str] = ('Left Chart', 'Right Chart'), legend_title: str = '', @@ -1229,6 +1019,9 @@ def dual_pie_with_plotly( Returns: Plotly Figure object """ + if colors is None: + colors = CONFIG.Plotting.default_qualitative_colorscale + # Preprocess data to Series left_series = preprocess_data_for_pie(data_left, lower_percentage_group) right_series = preprocess_data_for_pie(data_right, lower_percentage_group) @@ -1244,7 +1037,7 @@ def dual_pie_with_plotly( all_labels = sorted(set(left_labels) | set(right_labels)) # Create color map - color_map = ColorProcessor(engine='plotly').process_colors(colors, all_labels, return_mapping=True) + color_map = process_colors(colors, all_labels, default_colorscale=CONFIG.Plotting.default_qualitative_colorscale) # Create figure fig = go.Figure() @@ -1294,7 +1087,7 @@ def dual_pie_with_plotly( def dual_pie_with_matplotlib( data_left: xr.Dataset | pd.DataFrame | pd.Series, data_right: xr.Dataset | pd.DataFrame | pd.Series, - colors: ColorType = 'viridis', + colors: ColorType | None = None, title: str = '', subtitles: tuple[str, str] = ('Left Chart', 'Right Chart'), legend_title: str = '', @@ -1308,7 +1101,7 @@ def dual_pie_with_matplotlib( Args: data_left: Data for the left pie chart. data_right: Data for the right pie chart. - colors: Color specification (colormap name, list of colors, or dict mapping) + colors: Color specification (colorscale name, list of colors, or dict mapping) title: The main title of the plot. subtitles: Tuple containing the subtitles for (left, right) charts. legend_title: The title for the legend. @@ -1319,6 +1112,9 @@ def dual_pie_with_matplotlib( Returns: Tuple of (Figure, list of Axes) """ + if colors is None: + colors = CONFIG.Plotting.default_qualitative_colorscale + # Preprocess data to Series left_series = preprocess_data_for_pie(data_left, lower_percentage_group) right_series = preprocess_data_for_pie(data_right, lower_percentage_group) @@ -1333,8 +1129,8 @@ def dual_pie_with_matplotlib( # Get all unique labels for consistent coloring all_labels = sorted(set(left_labels) | set(right_labels)) - # Create color map - color_map = ColorProcessor(engine='matplotlib').process_colors(colors, all_labels, return_mapping=True) + # Create color map (process_colors always returns a dict) + color_map = process_colors(colors, all_labels, default_colorscale=CONFIG.Plotting.default_qualitative_colorscale) # Create figure fig, axes = plt.subplots(1, 2, figsize=figsize) @@ -1400,11 +1196,11 @@ def draw_pie(ax, labels, values, subtitle): def heatmap_with_plotly( data: xr.DataArray, - colors: ColorType = 'viridis', + colors: ColorType | None = None, title: str = '', facet_by: str | list[str] | None = None, animate_by: str | None = None, - facet_cols: int = 3, + facet_cols: int | None = None, reshape_time: tuple[Literal['YS', 'MS', 'W', 'D', 'h', '15min', 'min'], Literal['W', 'D', 'h', '15min', 'min']] | Literal['auto'] | None = 'auto', @@ -1427,8 +1223,8 @@ def heatmap_with_plotly( Args: data: An xarray DataArray containing the data to visualize. Should have at least 2 dimensions, or a 'time' dimension that can be reshaped into 2D. - colors: Color specification (colormap name, list, or dict). Common options: - 'viridis', 'plasma', 'RdBu', 'portland'. + colors: Color specification (colorscale name, list, or dict). Common options: + 'turbo', 'plasma', 'RdBu', 'portland'. title: The main title of the heatmap. facet_by: Dimension to create facets for. Creates a subplot grid. Can be a single dimension name or list (only first dimension used). @@ -1484,6 +1280,13 @@ def heatmap_with_plotly( fig = heatmap_with_plotly(data_array, facet_by='scenario', animate_by='period', reshape_time=('W', 'D')) ``` """ + if colors is None: + colors = CONFIG.Plotting.default_sequential_colorscale + + # Apply CONFIG defaults if not explicitly set + if facet_cols is None: + facet_cols = CONFIG.Plotting.default_facet_cols + # Handle empty data if data.size == 0: return go.Figure() @@ -1577,7 +1380,7 @@ def heatmap_with_plotly( # Create the imshow plot - px.imshow can work directly with xarray DataArrays common_args = { 'img': data, - 'color_continuous_scale': colors if isinstance(colors, str) else 'viridis', + 'color_continuous_scale': colors, 'title': title, } @@ -1601,7 +1404,7 @@ def heatmap_with_plotly( # Fallback: create a simple heatmap without faceting fallback_args = { 'img': data.values, - 'color_continuous_scale': colors if isinstance(colors, str) else 'viridis', + 'color_continuous_scale': colors, 'title': title, } fallback_args.update(imshow_kwargs) @@ -1612,7 +1415,7 @@ def heatmap_with_plotly( def heatmap_with_matplotlib( data: xr.DataArray, - colors: ColorType = 'viridis', + colors: ColorType | None = None, title: str = '', figsize: tuple[float, float] = (12, 6), reshape_time: tuple[Literal['YS', 'MS', 'W', 'D', 'h', '15min', 'min'], Literal['W', 'D', 'h', '15min', 'min']] @@ -1635,7 +1438,7 @@ def heatmap_with_matplotlib( data: An xarray DataArray containing the data to visualize. Should have at least 2 dimensions. If more than 2 dimensions exist, additional dimensions will be reduced by taking the first slice. - colors: Color specification. Should be a colormap name (e.g., 'turbo', 'RdBu'). + colors: Color specification. Should be a colorscale name (e.g., 'turbo', 'RdBu'). title: The title of the heatmap. figsize: The size of the figure (width, height) in inches. reshape_time: Time reshaping configuration: @@ -1675,6 +1478,9 @@ def heatmap_with_matplotlib( fig, ax = heatmap_with_matplotlib(data_array, reshape_time=('D', 'h')) ``` """ + if colors is None: + colors = CONFIG.Plotting.default_sequential_colorscale + # Initialize kwargs if not provided if imshow_kwargs is None: imshow_kwargs = {} @@ -1726,11 +1532,8 @@ def heatmap_with_matplotlib( x_labels = 'x' y_labels = 'y' - # Process colormap - cmap = colors if isinstance(colors, str) else 'viridis' - # Create the heatmap using imshow with user customizations - imshow_defaults = {'cmap': cmap, 'aspect': 'auto', 'origin': 'upper', 'vmin': vmin, 'vmax': vmax} + imshow_defaults = {'cmap': colors, 'aspect': 'auto', 'origin': 'upper', 'vmin': vmin, 'vmax': vmax} imshow_defaults.update(imshow_kwargs) # User kwargs override defaults im = ax.imshow(values, **imshow_defaults) @@ -1759,9 +1562,9 @@ def export_figure( default_path: pathlib.Path, default_filetype: str | None = None, user_path: pathlib.Path | None = None, - show: bool = True, + show: bool | None = None, save: bool = False, - dpi: int = 300, + dpi: int | None = None, ) -> go.Figure | tuple[plt.Figure, plt.Axes]: """ Export a figure to a file and or show it. @@ -1771,14 +1574,21 @@ def export_figure( default_path: The default file path if no user filename is provided. default_filetype: The default filetype if the path doesnt end with a filetype. user_path: An optional user-specified file path. - show: Whether to display the figure (default: True). + show: Whether to display the figure. If None, uses CONFIG.Plotting.default_show (default: None). save: Whether to save the figure (default: False). - dpi: DPI (dots per inch) for saving Matplotlib figures. If None, Matplotlib rcParams are used. + dpi: DPI (dots per inch) for saving Matplotlib figures. If None, uses CONFIG.Plotting.default_dpi. Raises: ValueError: If no default filetype is provided and the path doesn't specify a filetype. TypeError: If the figure type is not supported. """ + # Apply CONFIG defaults if not explicitly set + if show is None: + show = CONFIG.Plotting.default_show + + if dpi is None: + dpi = CONFIG.Plotting.default_dpi + filename = user_path or default_path filename = filename.with_name(filename.name.replace('|', '__')) if filename.suffix == '': @@ -1793,25 +1603,17 @@ def export_figure( filename = filename.with_suffix('.html') try: - is_test_env = 'PYTEST_CURRENT_TEST' in os.environ - - if is_test_env: - # Test environment: never open browser, only save if requested - if save: - fig.write_html(str(filename)) - # Ignore show flag in tests - else: - # Production environment: respect show and save flags - if save and show: - # Save and auto-open in browser - plotly.offline.plot(fig, filename=str(filename)) - elif save and not show: - # Save without opening - fig.write_html(str(filename)) - elif show and not save: - # Show interactively without saving - fig.show() - # If neither save nor show: do nothing + # Respect show and save flags (tests should set CONFIG.Plotting.default_show=False) + if save and show: + # Save and auto-open in browser + plotly.offline.plot(fig, filename=str(filename)) + elif save and not show: + # Save without opening + fig.write_html(str(filename)) + elif show and not save: + # Show interactively without saving + fig.show() + # If neither save nor show: do nothing finally: # Cleanup to prevent socket warnings if hasattr(fig, '_renderer'): @@ -1822,12 +1624,11 @@ def export_figure( elif isinstance(figure_like, tuple): fig, ax = figure_like if show: - # Only show if using interactive backend and not in test environment + # Only show if using interactive backend (tests should set CONFIG.Plotting.default_show=False) backend = matplotlib.get_backend().lower() is_interactive = backend not in {'agg', 'pdf', 'ps', 'svg', 'template'} - is_test_env = 'PYTEST_CURRENT_TEST' in os.environ - if is_interactive and not is_test_env: + if is_interactive: plt.show() if save: diff --git a/flixopt/results.py b/flixopt/results.py index 576ff9ec1..847ee5a7f 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -1,5 +1,6 @@ from __future__ import annotations +import copy import datetime import json import logging @@ -15,6 +16,8 @@ from . import io as fx_io from . import plotting +from .color_processing import process_colors +from .config import CONFIG from .flow_system import FlowSystem if TYPE_CHECKING: @@ -29,6 +32,57 @@ logger = logging.getLogger('flixopt') +def load_mapping_from_file(path: pathlib.Path) -> dict[str, str | list[str]]: + """Load color mapping from JSON or YAML file. + + Tries loader based on file suffix first, with fallback to the other format. + + Args: + path: Path to config file (.json or .yaml/.yml) + + Returns: + Dictionary mapping components to colors or colorscales to component lists + + Raises: + ValueError: If file cannot be loaded as JSON or YAML + """ + suffix = path.suffix.lower() + + if suffix == '.json': + # Try JSON first, fallback to YAML + try: + with open(path) as f: + return json.load(f) + except json.JSONDecodeError: + try: + with open(path) as f: + return yaml.safe_load(f) + except Exception: + raise ValueError(f'Could not load config from {path}') from None + elif suffix in {'.yaml', '.yml'}: + # Try YAML first, fallback to JSON + try: + with open(path) as f: + return yaml.safe_load(f) + except yaml.YAMLError: + try: + with open(path) as f: + return json.load(f) + except Exception: + raise ValueError(f'Could not load config from {path}') from None + else: + # Unknown extension, try both starting with JSON + try: + with open(path) as f: + return json.load(f) + except json.JSONDecodeError: + try: + with open(path) as f: + return yaml.safe_load(f) + except Exception: + raise ValueError(f'Could not load config from {path}') from None + + class _FlowSystemRestorationError(Exception): """Exception raised when a FlowSystem cannot be restored from dataset.""" @@ -254,6 +308,8 @@ def __init__( self._sizes = None self._effects_per_component = None + self.colors: dict[str, str] = {} + def __getitem__(self, key: str) -> ComponentResults | BusResults | EffectResults: if key in self.components: return self.components[key] @@ -320,6 +376,131 @@ def flow_system(self) -> FlowSystem: logger.level = old_level return self._flow_system + def setup_colors( + self, + config: dict[str, str | list[str]] | str | pathlib.Path | None = None, + default_colorscale: str | None = None, + ) -> dict[str, str]: + """ + Setup colors for all variables across all elements. Overwrites existing ones. + + Args: + config: Configuration for color assignment. Can be: + - dict: Maps components to colors/colorscales: + * 'component1': 'red' # Single component to single color + * 'component1': '#FF0000' # Single component to hex color + - OR maps colorscales to multiple components: + * 'colorscale_name': ['component1', 'component2'] # Colorscale across components + - str: Path to a JSON/YAML config file or a colorscale name to apply to all + - Path: Path to a JSON/YAML config file + - None: Use default_colorscale for all components + default_colorscale: Default colorscale for unconfigured components (default: 'turbo') + + Examples: + setup_colors({ + # Direct component-to-color mappings + 'Boiler1': '#FF0000', + 'CHP': 'darkred', + # Colorscale for multiple components + 'Oranges': ['Solar1', 'Solar2'], + 'Blues': ['Wind1', 'Wind2'], + 'Greens': ['Battery1', 'Battery2', 'Battery3'], + }) + + Returns: + Complete variable-to-color mapping dictionary + """ + + def get_all_variable_names(comp: str) -> list[str]: + """Collect all variables from the component, including flows and flow_hours.""" + comp_object = self.components[comp] + var_names = [comp] + list(comp_object._variable_names) + for flow in comp_object.flows: + var_names.extend([flow, f'{flow}|flow_hours']) + return var_names + + # Set default colorscale if not provided + if default_colorscale is None: + default_colorscale = CONFIG.Plotting.default_qualitative_colorscale + + # Handle different config input types + if config is None: + # Apply default colorscale to all components + config_dict = {} + elif isinstance(config, (str, pathlib.Path)): + # Try to load from file first + config_path = pathlib.Path(config) + if config_path.exists(): + # Load config from file using helper + config_dict = load_mapping_from_file(config_path) + else: + # Treat as colorscale name to apply to all components + all_components = list(self.components.keys()) + config_dict = {config: all_components} + elif isinstance(config, dict): + config_dict = config + else: + raise TypeError(f'config must be dict, str, Path, or None, got {type(config)}') + + # Step 1: Build component-to-color mapping + component_colors: dict[str, str] = {} + + # Track which components are configured + configured_components = set() + + # Process each configuration entry + for key, value in config_dict.items(): + # Check if value is a list (colorscale -> [components]) + # or a string (component -> color OR colorscale -> [components]) + + if isinstance(value, list): + # key is colorscale, value is list of components + # Format: 'Blues': ['Wind1', 'Wind2'] + components = value + colorscale_name = key + + # Validate components exist + for component in components: + if component not in self.components: + raise ValueError(f"Component '{component}' not found") + + configured_components.update(components) + + # Use process_colors to get one color per component from the colorscale + colors_for_components = process_colors(colorscale_name, components) + component_colors.update(colors_for_components) + + elif isinstance(value, str): + # Check if key is an existing component + if key in self.components: + # Format: 'CHP': 'red' (component -> color) + component, color = key, value + + configured_components.add(component) + component_colors[component] = color + else: + raise ValueError(f"Component '{key}' not found") + else: + raise TypeError(f'Config value must be str or list, got {type(value)}') + + # Step 2: Assign colors to remaining unconfigured components + remaining_components = list(set(self.components.keys()) - configured_components) + if remaining_components: + # Use default colorscale to assign one color per remaining component + default_colors = process_colors(default_colorscale, remaining_components) + component_colors.update(default_colors) + + # Step 3: Build variable-to-color mapping + # Clear existing colors to avoid stale keys + self.colors = {} + # Each component's variables all get the same color as the component + for component, color in component_colors.items(): + variable_names = get_all_variable_names(component) + for var_name in variable_names: + self.colors[var_name] = color + + return self.colors + def filter_solution( self, variable_dims: Literal['scalar', 'time', 'scenario', 'timeonly', 'scenarioonly'] | None = None, @@ -719,13 +900,13 @@ def plot_heatmap( self, variable_name: str | list[str], save: bool | pathlib.Path = False, - show: bool = True, - colors: plotting.ColorType = 'viridis', + show: bool | None = None, + colors: plotting.ColorType | None = None, engine: plotting.PlottingEngine = 'plotly', select: dict[FlowSystemDimensions, Any] | None = None, facet_by: str | list[str] | None = 'scenario', animate_by: str | None = 'period', - facet_cols: int = 3, + facet_cols: int | None = None, reshape_time: tuple[Literal['YS', 'MS', 'W', 'D', 'h', '15min', 'min'], Literal['W', 'D', 'h', '15min', 'min']] | Literal['auto'] | None = 'auto', @@ -1024,8 +1205,8 @@ def __init__( def plot_node_balance( self, save: bool | pathlib.Path = False, - show: bool = True, - colors: plotting.ColorType = 'viridis', + show: bool | None = None, + colors: plotting.ColorType | None = None, engine: plotting.PlottingEngine = 'plotly', select: dict[FlowSystemDimensions, Any] | None = None, unit_type: Literal['flow_rate', 'flow_hours'] = 'flow_rate', @@ -1033,7 +1214,7 @@ def plot_node_balance( drop_suffix: bool = True, facet_by: str | list[str] | None = 'scenario', animate_by: str | None = 'period', - facet_cols: int = 3, + facet_cols: int | None = None, # Deprecated parameter (kept for backwards compatibility) indexer: dict[FlowSystemDimensions, Any] | None = None, **plot_kwargs: Any, @@ -1183,7 +1364,7 @@ def plot_node_balance( ds, facet_by=facet_by, animate_by=animate_by, - colors=colors, + colors=colors if colors is not None else self._calculation_results.colors, mode=mode, title=title, facet_cols=facet_cols, @@ -1194,7 +1375,7 @@ def plot_node_balance( else: figure_like = plotting.with_matplotlib( ds, - colors=colors, + colors=colors if colors is not None else self._calculation_results.colors, mode=mode, title=title, **plot_kwargs, @@ -1214,10 +1395,10 @@ def plot_node_balance( def plot_node_balance_pie( self, lower_percentage_group: float = 5, - colors: plotting.ColorType = 'viridis', + colors: plotting.ColorType | None = None, text_info: str = 'percent+label+value', save: bool | pathlib.Path = False, - show: bool = True, + show: bool | None = None, engine: plotting.PlottingEngine = 'plotly', select: dict[FlowSystemDimensions, Any] | None = None, # Deprecated parameter (kept for backwards compatibility) @@ -1351,7 +1532,7 @@ def plot_node_balance_pie( figure_like = plotting.dual_pie_with_plotly( data_left=inputs, data_right=outputs, - colors=colors, + colors=colors if colors is not None else self._calculation_results.colors, title=title, text_info=text_info, subtitles=('Inputs', 'Outputs'), @@ -1365,7 +1546,7 @@ def plot_node_balance_pie( figure_like = plotting.dual_pie_with_matplotlib( data_left=inputs.to_pandas(), data_right=outputs.to_pandas(), - colors=colors, + colors=colors if colors is not None else self._calculation_results.colors, title=title, subtitles=('Inputs', 'Outputs'), legend_title='Flows', @@ -1480,14 +1661,14 @@ def charge_state(self) -> xr.DataArray: def plot_charge_state( self, save: bool | pathlib.Path = False, - show: bool = True, - colors: plotting.ColorType = 'viridis', + show: bool | None = None, + colors: plotting.ColorType | None = None, engine: plotting.PlottingEngine = 'plotly', mode: Literal['area', 'stacked_bar', 'line'] = 'area', select: dict[FlowSystemDimensions, Any] | None = None, facet_by: str | list[str] | None = 'scenario', animate_by: str | None = 'period', - facet_cols: int = 3, + facet_cols: int | None = None, # Deprecated parameter (kept for backwards compatibility) indexer: dict[FlowSystemDimensions, Any] | None = None, **plot_kwargs: Any, @@ -1601,7 +1782,7 @@ def plot_charge_state( ds, facet_by=facet_by, animate_by=animate_by, - colors=colors, + colors=colors if colors is not None else self._calculation_results.colors, mode=mode, title=title, facet_cols=facet_cols, @@ -1617,7 +1798,7 @@ def plot_charge_state( charge_state_ds, facet_by=facet_by, animate_by=animate_by, - colors=colors, + colors=colors if colors is not None else self._calculation_results.colors, mode='line', # Always line for charge_state title='', # No title needed for this temp figure facet_cols=facet_cols, @@ -1657,7 +1838,7 @@ def plot_charge_state( # For matplotlib, plot flows (node balance), then add charge_state as line fig, ax = plotting.with_matplotlib( ds, - colors=colors, + colors=colors if colors is not None else self._calculation_results.colors, mode=mode, title=title, **plot_kwargs, @@ -1933,6 +2114,7 @@ def __init__( self.name = name self.folder = pathlib.Path(folder) if folder is not None else pathlib.Path.cwd() / 'results' self.hours_per_timestep = FlowSystem.calculate_hours_per_timestep(self.all_timesteps) + self._colors = {} @property def meta_data(self) -> dict[str, int | list[str]]: @@ -1947,6 +2129,64 @@ def meta_data(self) -> dict[str, int | list[str]]: def segment_names(self) -> list[str]: return [segment.name for segment in self.segment_results] + @property + def colors(self) -> dict[str, str]: + return self._colors + + @colors.setter + def colors(self, colors: dict[str, str]): + """Applies colors to all segments""" + self._colors = colors + for segment in self.segment_results: + segment.colors = copy.deepcopy(colors) + + def setup_colors( + self, + config: dict[str, str | list[str]] | str | pathlib.Path | None = None, + default_colorscale: str | None = None, + ) -> dict[str, str]: + """ + Setup colors for all variables across all segment results. + + This method applies the same color configuration to all segments, ensuring + consistent visualization across the entire segmented calculation. The color + mapping is propagated to each segment's CalculationResults instance. + + Args: + config: Configuration for color assignment. Can be: + - dict: Maps components to colors/colorscales: + * 'component1': 'red' # Single component to single color + * 'component1': '#FF0000' # Single component to hex color + - OR maps colorscales to multiple components: + * 'colorscale_name': ['component1', 'component2'] # Colorscale across components + - str: Path to a JSON/YAML config file or a colorscale name to apply to all + - Path: Path to a JSON/YAML config file + - None: Use default_colorscale for all components + default_colorscale: Default colorscale for unconfigured components (default: 'turbo') + + Examples: + ```python + # Apply colors to all segments + segmented_results.setup_colors( + { + 'CHP': 'red', + 'Blues': ['Storage1', 'Storage2'], + 'Oranges': ['Solar1', 'Solar2'], + } + ) + + # Use a single colorscale for all components in all segments + segmented_results.setup_colors('portland') + ``` + + Returns: + Complete variable-to-color mapping dictionary from the first segment + (all segments will have the same mapping) + """ + self.colors = self.segment_results[0].setup_colors(config=config, default_colorscale=default_colorscale) + + return self.colors + def solution_without_overlap(self, variable_name: str) -> xr.DataArray: """Get variable solution removing segment overlaps. @@ -1968,13 +2208,13 @@ def plot_heatmap( reshape_time: tuple[Literal['YS', 'MS', 'W', 'D', 'h', '15min', 'min'], Literal['W', 'D', 'h', '15min', 'min']] | Literal['auto'] | None = 'auto', - colors: str = 'portland', + colors: plotting.ColorType | None = None, save: bool | pathlib.Path = False, - show: bool = True, + show: bool | None = None, engine: plotting.PlottingEngine = 'plotly', facet_by: str | list[str] | None = None, animate_by: str | None = None, - facet_cols: int = 3, + facet_cols: int | None = None, fill: Literal['ffill', 'bfill'] | None = 'ffill', # Deprecated parameters (kept for backwards compatibility) heatmap_timeframes: Literal['YS', 'MS', 'W', 'D', 'h', '15min', 'min'] | None = None, @@ -2039,7 +2279,7 @@ def plot_heatmap( if color_map is not None: # Check for conflict with new parameter - if colors != 'portland': # Check if user explicitly set colors + if colors is not None: # Check if user explicitly set colors raise ValueError( "Cannot use both deprecated parameter 'color_map' and new parameter 'colors'. Use only 'colors'." ) @@ -2099,14 +2339,14 @@ def plot_heatmap( data: xr.DataArray | xr.Dataset, name: str | None = None, folder: pathlib.Path | None = None, - colors: plotting.ColorType = 'viridis', + colors: plotting.ColorType | None = None, save: bool | pathlib.Path = False, - show: bool = True, + show: bool | None = None, engine: plotting.PlottingEngine = 'plotly', select: dict[str, Any] | None = None, facet_by: str | list[str] | None = None, animate_by: str | None = None, - facet_cols: int = 3, + facet_cols: int | None = None, reshape_time: tuple[Literal['YS', 'MS', 'W', 'D', 'h', '15min', 'min'], Literal['W', 'D', 'h', '15min', 'min']] | Literal['auto'] | None = 'auto', @@ -2187,8 +2427,7 @@ def plot_heatmap( # Handle deprecated color_map parameter if color_map is not None: - # Check for conflict with new parameter - if colors != 'viridis': # User explicitly set colors + if colors is not None: # User explicitly set colors raise ValueError( "Cannot use both deprecated parameter 'color_map' and new parameter 'colors'. Use only 'colors'." ) diff --git a/tests/conftest.py b/tests/conftest.py index ac5255562..bd940b843 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -848,4 +848,6 @@ def set_test_environment(): pio.renderers.default = 'json' # Use non-interactive renderer + fx.CONFIG.Plotting.default_show = False + yield diff --git a/tests/test_results_plots.py b/tests/test_results_plots.py index 1fd6cf7f5..a656f7c44 100644 --- a/tests/test_results_plots.py +++ b/tests/test_results_plots.py @@ -28,7 +28,7 @@ def plotting_engine(request): @pytest.fixture( params=[ - 'viridis', # Test string colormap + 'turbo', # Test string colormap ['#ff0000', '#00ff00', '#0000ff', '#ffff00', '#ff00ff', '#00ffff'], # Test color list { 'Boiler(Q_th)|flow_rate': '#ff0000', @@ -51,7 +51,7 @@ def test_results_plots(flow_system, plotting_engine, show, save, color_spec): # Matplotlib doesn't support faceting/animation, so disable them for matplotlib engine heatmap_kwargs = { 'reshape_time': ('D', 'h'), - 'colors': 'viridis', # Note: heatmap only accepts string colormap + 'colors': 'turbo', # Note: heatmap only accepts string colormap 'save': save, 'show': show, 'engine': plotting_engine, From 2b7278329c9ada3d487b9850909faf84042319df Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 26 Oct 2025 12:10:56 +0100 Subject: [PATCH 375/448] Feature/centralized io (#441) * Add new config options for plotting * Use turbo instead of viridis * Update plotting.py to use updated color management * update color management * Add rgb to hex for matplotlib * Add rgb to hex for matplotlib * Remove colormanager class * Update type hints * Update type hints and use Config defaults * Add stable colors * V1 * V2 * Use calculation.colors if direct colors is None * Bugfix * Bugfix * Update setup_colors * Add color setup to examples * Final touches * Update CHANGELOG.md * Update CHANGELOG.md * Bugfix * Update fro SegmentedCalculationResults * Default show = False in tests * Bugfix * Bugfix * Add show default to plot_network * Make _rgb_string_to_hex more robust * Improve Error Handling * Overwrite colors explicitly in setup_colors * Improve config loader * Update CHANGELOG.md * Make colors arg always overwrite the default behaviour * centralize yaml and json io * Improve docstring an use safe=True * Move round_nested_floats to io.py and remove utils.py module * Rename special yaml safe method * Remove import utils * Ensure native types * Use safe dump everywhere and normalize file suffixes * Avoid double rounding * Set indent to 4 consistently * Simplify netcdf file io * Improve benchmark_file_io.py * Improve benchmark_file_io.py * Revert to using netcdf4 for file io * Remove temporary benchmark file * Typo * Typo --- CHANGELOG.md | 1 + flixopt/calculation.py | 5 +- flixopt/config.py | 9 +- flixopt/io.py | 261 +++++++++++++++++++++++++++++++++++++---- flixopt/results.py | 52 +------- flixopt/structure.py | 4 +- flixopt/utils.py | 86 -------------- pyproject.toml | 2 +- 8 files changed, 255 insertions(+), 165 deletions(-) delete mode 100644 flixopt/utils.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 8eb16a4c6..663f087a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -80,6 +80,7 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp - **Color terminology**: Standardized terminology from "colormap" to "colorscale" throughout the codebase for consistency with Plotly conventions - **Default colorscales**: Changed default sequential colorscale from 'viridis' to 'turbo' for better perceptual uniformity; qualitative colorscale now defaults to 'plotly' - **Aggregation plotting**: `Aggregation.plot()` now respects `CONFIG.Plotting.default_qualitative_colorscale` and uses `process_colors()` for consistent color handling +- **netcdf engine**: Following the xarray revert in `xarray==2025.09.2` and after running some benchmarks, we go back to using the netcdf4 engine ### 🗑️ Deprecated diff --git a/flixopt/calculation.py b/flixopt/calculation.py index 9d2164e1e..f744c5247 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -22,7 +22,6 @@ import yaml from . import io as fx_io -from . import utils as utils from .aggregation import Aggregation, AggregationModel, AggregationParameters from .components import Storage from .config import CONFIG @@ -144,7 +143,7 @@ def main_results(self) -> dict[str, Scalar | dict]: ], } - return utils.round_nested_floats(main_results) + return fx_io.round_nested_floats(main_results) @property def summary(self): @@ -253,7 +252,7 @@ def solve( logger.info( f'{" Main Results ":#^80}\n' + yaml.dump( - utils.round_nested_floats(self.main_results), + self.main_results, default_flow_style=False, sort_keys=False, allow_unicode=True, diff --git a/flixopt/config.py b/flixopt/config.py index b7162e55f..670f86da2 100644 --- a/flixopt/config.py +++ b/flixopt/config.py @@ -8,7 +8,6 @@ from types import MappingProxyType from typing import Literal -import yaml from rich.console import Console from rich.logging import RichHandler from rich.style import Style @@ -299,13 +298,15 @@ def load_from_file(cls, config_file: str | Path): Raises: FileNotFoundError: If the config file does not exist. """ + # Import here to avoid circular import + from . import io as fx_io + config_path = Path(config_file) if not config_path.exists(): raise FileNotFoundError(f'Config file not found: {config_file}') - with config_path.open() as file: - config_dict = yaml.safe_load(file) or {} - cls._apply_config_dict(config_dict) + config_dict = fx_io.load_yaml(config_path) + cls._apply_config_dict(config_dict) cls.apply() diff --git a/flixopt/io.py b/flixopt/io.py index 53d3d8e8a..059670ddd 100644 --- a/flixopt/io.py +++ b/flixopt/io.py @@ -1,13 +1,13 @@ from __future__ import annotations -import importlib.util import json import logging import pathlib import re from dataclasses import dataclass -from typing import TYPE_CHECKING, Literal +from typing import TYPE_CHECKING, Any +import numpy as np import xarray as xr import yaml @@ -34,7 +34,235 @@ def remove_none_and_empty(obj): return obj -def _save_to_yaml(data, output_file='formatted_output.yaml'): +def round_nested_floats(obj: dict | list | float | int | Any, decimals: int = 2) -> dict | list | float | int | Any: + """Recursively round floating point numbers in nested data structures and convert it to python native types. + + This function traverses nested data structures (dictionaries, lists) and rounds + any floating point numbers to the specified number of decimal places. It handles + various data types including NumPy arrays and xarray DataArrays by converting + them to lists with rounded values. + + Args: + obj: The object to process. Can be a dict, list, float, int, numpy.ndarray, + xarray.DataArray, or any other type. + decimals (int, optional): Number of decimal places to round to. Defaults to 2. + + Returns: + The processed object with the same structure as the input, but with all floating point numbers rounded to the specified precision. NumPy arrays and xarray DataArrays are converted to lists. + + Examples: + >>> data = {'a': 3.14159, 'b': [1.234, 2.678]} + >>> round_nested_floats(data, decimals=2) + {'a': 3.14, 'b': [1.23, 2.68]} + + >>> import numpy as np + >>> arr = np.array([1.234, 5.678]) + >>> round_nested_floats(arr, decimals=1) + [1.2, 5.7] + """ + if isinstance(obj, dict): + return {k: round_nested_floats(v, decimals) for k, v in obj.items()} + elif isinstance(obj, list): + return [round_nested_floats(v, decimals) for v in obj] + elif isinstance(obj, np.floating): + return round(float(obj), decimals) + elif isinstance(obj, np.integer): + return int(obj) + elif isinstance(obj, np.bool_): + return bool(obj) + elif isinstance(obj, float): + return round(obj, decimals) + elif isinstance(obj, int): + return obj + elif isinstance(obj, np.ndarray): + return np.round(obj, decimals).tolist() + elif isinstance(obj, xr.DataArray): + return obj.round(decimals).values.tolist() + return obj + + +# ============================================================================ +# Centralized JSON and YAML I/O Functions +# ============================================================================ + + +def load_json(path: str | pathlib.Path) -> dict | list: + """ + Load data from a JSON file. + + Args: + path: Path to the JSON file. + + Returns: + Loaded data (typically dict or list). + + Raises: + FileNotFoundError: If the file does not exist. + json.JSONDecodeError: If the file is not valid JSON. + """ + path = pathlib.Path(path) + with open(path, encoding='utf-8') as f: + return json.load(f) + + +def save_json( + data: dict | list, + path: str | pathlib.Path, + indent: int = 4, + ensure_ascii: bool = False, + **kwargs, +) -> None: + """ + Save data to a JSON file with consistent formatting. + + Args: + data: Data to save (dict or list). + path: Path to save the JSON file. + indent: Number of spaces for indentation (default: 4). + ensure_ascii: If False, allow Unicode characters (default: False). + **kwargs: Additional arguments to pass to json.dump(). + """ + path = pathlib.Path(path) + with open(path, 'w', encoding='utf-8') as f: + json.dump(data, f, indent=indent, ensure_ascii=ensure_ascii, **kwargs) + + +def load_yaml(path: str | pathlib.Path) -> dict | list: + """ + Load data from a YAML file. + + Args: + path: Path to the YAML file. + + Returns: + Loaded data (typically dict or list), or empty dict if file is empty. + + Raises: + FileNotFoundError: If the file does not exist. + yaml.YAMLError: If the file is not valid YAML. + Note: Returns {} for empty YAML files instead of None. + """ + path = pathlib.Path(path) + with open(path, encoding='utf-8') as f: + return yaml.safe_load(f) or {} + + +def _load_yaml_unsafe(path: str | pathlib.Path) -> dict | list: + """ + INTERNAL: Load YAML allowing arbitrary tags. Do not use on untrusted input. + + This function exists only for loading internally-generated files that may + contain custom YAML tags. Never use this on user-provided files. + + Args: + path: Path to the YAML file. + + Returns: + Loaded data (typically dict or list), or empty dict if file is empty. + """ + path = pathlib.Path(path) + with open(path, encoding='utf-8') as f: + return yaml.unsafe_load(f) or {} + + +def save_yaml( + data: dict | list, + path: str | pathlib.Path, + indent: int = 4, + width: int = 1000, + allow_unicode: bool = True, + sort_keys: bool = False, + **kwargs, +) -> None: + """ + Save data to a YAML file with consistent formatting. + + Args: + data: Data to save (dict or list). + path: Path to save the YAML file. + indent: Number of spaces for indentation (default: 4). + width: Maximum line width (default: 1000). + allow_unicode: If True, allow Unicode characters (default: True). + sort_keys: If True, sort dictionary keys (default: False). + **kwargs: Additional arguments to pass to yaml.safe_dump(). + """ + path = pathlib.Path(path) + with open(path, 'w', encoding='utf-8') as f: + yaml.safe_dump( + data, + f, + indent=indent, + width=width, + allow_unicode=allow_unicode, + sort_keys=sort_keys, + default_flow_style=False, + **kwargs, + ) + + +def load_config_file(path: str | pathlib.Path) -> dict: + """ + Load a configuration file, automatically detecting JSON or YAML format. + + This function intelligently tries to load the file based on its extension, + with fallback support if the primary format fails. + + Supported extensions: + - .json: Tries JSON first, falls back to YAML + - .yaml, .yml: Tries YAML first, falls back to JSON + - Others: Tries YAML, then JSON + + Args: + path: Path to the configuration file. + + Returns: + Loaded configuration as a dictionary. + + Raises: + FileNotFoundError: If the file does not exist. + ValueError: If neither JSON nor YAML parsing succeeds. + """ + path = pathlib.Path(path) + + if not path.exists(): + raise FileNotFoundError(f'Configuration file not found: {path}') + + # Try based on file extension + # Normalize extension to lowercase for case-insensitive matching + suffix = path.suffix.lower() + + if suffix == '.json': + try: + return load_json(path) + except json.JSONDecodeError: + logger.warning(f'Failed to parse {path} as JSON, trying YAML') + try: + return load_yaml(path) + except yaml.YAMLError as e: + raise ValueError(f'Failed to parse {path} as JSON or YAML') from e + + elif suffix in ['.yaml', '.yml']: + try: + return load_yaml(path) + except yaml.YAMLError: + logger.warning(f'Failed to parse {path} as YAML, trying JSON') + try: + return load_json(path) + except json.JSONDecodeError as e: + raise ValueError(f'Failed to parse {path} as YAML or JSON') from e + + else: + # Unknown extension, try YAML first (more common for config) + try: + return load_yaml(path) + except yaml.YAMLError: + try: + return load_json(path) + except json.JSONDecodeError as e: + raise ValueError(f'Failed to parse {path} as YAML or JSON') from e + + +def _save_yaml_multiline(data, output_file='formatted_output.yaml'): """ Save dictionary data to YAML with proper multi-line string formatting. Handles complex string patterns including backticks, special characters, @@ -62,14 +290,14 @@ def represent_str(dumper, data): # Use plain style for simple strings return dumper.represent_scalar('tag:yaml.org,2002:str', data) - # Add the string representer to SafeDumper - yaml.add_representer(str, represent_str, Dumper=yaml.SafeDumper) - # Configure dumper options for better formatting class CustomDumper(yaml.SafeDumper): def increase_indent(self, flow=False, indentless=False): return super().increase_indent(flow, False) + # Bind representer locally to CustomDumper to avoid global side effects + CustomDumper.add_representer(str, represent_str) + # Write to file with settings that ensure proper formatting with open(output_file, 'w', encoding='utf-8') as file: yaml.dump( @@ -80,7 +308,7 @@ def increase_indent(self, flow=False, indentless=False): default_flow_style=False, # Use block style for mappings width=1000, # Set a reasonable line width allow_unicode=True, # Support Unicode characters - indent=2, # Set consistent indentation + indent=4, # Set consistent indentation ) @@ -190,7 +418,7 @@ def document_linopy_model(model: linopy.Model, path: pathlib.Path | None = None) if path is not None: if path.suffix not in ['.yaml', '.yml']: raise ValueError(f'Invalid file extension for path {path}. Only .yaml and .yml are supported') - _save_to_yaml(documentation, str(path)) + _save_yaml_multiline(documentation, str(path)) return documentation @@ -199,7 +427,6 @@ def save_dataset_to_netcdf( ds: xr.Dataset, path: str | pathlib.Path, compression: int = 0, - engine: Literal['netcdf4', 'scipy', 'h5netcdf'] = 'h5netcdf', ) -> None: """ Save a dataset to a netcdf file. Store all attrs as JSON strings in 'attrs' attributes. @@ -216,16 +443,6 @@ def save_dataset_to_netcdf( if path.suffix not in ['.nc', '.nc4']: raise ValueError(f'Invalid file extension for path {path}. Only .nc and .nc4 are supported') - apply_encoding = False - if compression != 0: - if importlib.util.find_spec(engine) is not None: - apply_encoding = True - else: - logger.warning( - f'Dataset was exported without compression due to missing dependency "{engine}".' - f'Install {engine} via `pip install {engine}`.' - ) - ds = ds.copy(deep=True) ds.attrs = {'attrs': json.dumps(ds.attrs)} @@ -242,9 +459,9 @@ def save_dataset_to_netcdf( ds.to_netcdf( path, encoding=None - if not apply_encoding + if compression == 0 else {data_var: {'zlib': True, 'complevel': compression} for data_var in ds.data_vars}, - engine=engine, + engine='netcdf4', ) @@ -258,7 +475,7 @@ def load_dataset_from_netcdf(path: str | pathlib.Path) -> xr.Dataset: Returns: Dataset: Loaded dataset with restored attrs. """ - ds = xr.load_dataset(str(path), engine='h5netcdf') + ds = xr.load_dataset(str(path), engine='netcdf4') # Restore Dataset attrs if 'attrs' in ds.attrs: diff --git a/flixopt/results.py b/flixopt/results.py index 847ee5a7f..950570df3 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -2,7 +2,6 @@ import copy import datetime -import json import logging import pathlib import warnings @@ -12,7 +11,6 @@ import numpy as np import pandas as pd import xarray as xr -import yaml from . import io as fx_io from . import plotting @@ -46,41 +44,7 @@ def load_mapping_from_file(path: pathlib.Path) -> dict[str, str | list[str]]: Raises: ValueError: If file cannot be loaded as JSON or YAML """ - suffix = path.suffix.lower() - - if suffix == '.json': - # Try JSON first, fallback to YAML - try: - with open(path) as f: - return json.load(f) - except json.JSONDecodeError: - try: - with open(path) as f: - return yaml.safe_load(f) - except Exception: - raise ValueError(f'Could not load config from {path}') from None - elif suffix in {'.yaml', '.yml'}: - # Try YAML first, fallback to JSON - try: - with open(path) as f: - return yaml.safe_load(f) - except yaml.YAMLError: - try: - with open(path) as f: - return json.load(f) - except Exception: - raise ValueError(f'Could not load config from {path}') from None - else: - # Unknown extension, try both starting with JSON - try: - with open(path) as f: - return json.load(f) - except json.JSONDecodeError: - try: - with open(path) as f: - return yaml.safe_load(f) - except Exception: - raise ValueError(f'Could not load config from {path}') from None + return fx_io.load_config_file(path) class _FlowSystemRestorationError(Exception): @@ -205,8 +169,7 @@ def from_file(cls, folder: str | pathlib.Path, name: str) -> CalculationResults: except Exception as e: logger.critical(f'Could not load the linopy model "{name}" from file ("{paths.linopy_model}"): {e}') - with open(paths.summary, encoding='utf-8') as f: - summary = yaml.load(f, Loader=yaml.FullLoader) + summary = fx_io.load_yaml(paths.summary) return cls( solution=fx_io.load_dataset_from_netcdf(paths.solution), @@ -1093,14 +1056,13 @@ def to_file( fx_io.save_dataset_to_netcdf(self.solution, paths.solution, compression=compression) fx_io.save_dataset_to_netcdf(self.flow_system_data, paths.flow_system, compression=compression) - with open(paths.summary, 'w', encoding='utf-8') as f: - yaml.dump(self.summary, f, allow_unicode=True, sort_keys=False, indent=4, width=1000) + fx_io.save_yaml(self.summary, paths.summary) if save_linopy_model: if self.model is None: logger.critical('No model in the CalculationResults. Saving the model is not possible.') else: - self.model.to_netcdf(paths.linopy_model, engine='h5netcdf') + self.model.to_netcdf(paths.linopy_model, engine='netcdf4') if document_model: if self.model is None: @@ -2085,8 +2047,7 @@ def from_file(cls, folder: str | pathlib.Path, name: str) -> SegmentedCalculatio folder = pathlib.Path(folder) path = folder / name logger.info(f'loading calculation "{name}" from file ("{path.with_suffix(".nc4")}")') - with open(path.with_suffix('.json'), encoding='utf-8') as f: - meta_data = json.load(f) + meta_data = fx_io.load_json(path.with_suffix('.json')) return cls( [CalculationResults.from_file(folder, sub_name) for sub_name in meta_data['sub_calculations']], all_timesteps=pd.DatetimeIndex( @@ -2330,8 +2291,7 @@ def to_file(self, folder: str | pathlib.Path | None = None, name: str | None = N for segment in self.segment_results: segment.to_file(folder=folder, name=segment.name, compression=compression) - with open(path.with_suffix('.json'), 'w', encoding='utf-8') as f: - json.dump(self.meta_data, f, indent=4, ensure_ascii=False) + fx_io.save_json(self.meta_data, path.with_suffix('.json')) logger.info(f'Saved calculation "{name}" to {path}') diff --git a/flixopt/structure.py b/flixopt/structure.py index 07c558eee..6ea618454 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -6,7 +6,6 @@ from __future__ import annotations import inspect -import json import logging from dataclasses import dataclass from io import StringIO @@ -788,8 +787,7 @@ def to_json(self, path: str | pathlib.Path): try: # Use the stats mode for JSON export (cleaner output) data = self.get_structure(clean=True, stats=True) - with open(path, 'w', encoding='utf-8') as f: - json.dump(data, f, indent=4, ensure_ascii=False) + fx_io.save_json(data, path) except Exception as e: raise OSError(f'Failed to save {self.__class__.__name__} to JSON file {path}: {e}') from e diff --git a/flixopt/utils.py b/flixopt/utils.py deleted file mode 100644 index dd1f93d64..000000000 --- a/flixopt/utils.py +++ /dev/null @@ -1,86 +0,0 @@ -""" -This module contains several utility functions used throughout the flixopt framework. -""" - -from __future__ import annotations - -import logging -from typing import Any, Literal - -import numpy as np -import xarray as xr - -logger = logging.getLogger('flixopt') - - -def round_nested_floats(obj: dict | list | float | int | Any, decimals: int = 2) -> dict | list | float | int | Any: - """Recursively round floating point numbers in nested data structures. - - This function traverses nested data structures (dictionaries, lists) and rounds - any floating point numbers to the specified number of decimal places. It handles - various data types including NumPy arrays and xarray DataArrays by converting - them to lists with rounded values. - - Args: - obj: The object to process. Can be a dict, list, float, int, numpy.ndarray, - xarray.DataArray, or any other type. - decimals (int, optional): Number of decimal places to round to. Defaults to 2. - - Returns: - The processed object with the same structure as the input, but with all floating point numbers rounded to the specified precision. NumPy arrays and xarray DataArrays are converted to lists. - - Examples: - >>> data = {'a': 3.14159, 'b': [1.234, 2.678]} - >>> round_nested_floats(data, decimals=2) - {'a': 3.14, 'b': [1.23, 2.68]} - - >>> import numpy as np - >>> arr = np.array([1.234, 5.678]) - >>> round_nested_floats(arr, decimals=1) - [1.2, 5.7] - """ - if isinstance(obj, dict): - return {k: round_nested_floats(v, decimals) for k, v in obj.items()} - elif isinstance(obj, list): - return [round_nested_floats(v, decimals) for v in obj] - elif isinstance(obj, float): - return round(obj, decimals) - elif isinstance(obj, int): - return obj - elif isinstance(obj, np.ndarray): - return np.round(obj, decimals).tolist() - elif isinstance(obj, xr.DataArray): - return obj.round(decimals).values.tolist() - return obj - - -def convert_dataarray( - data: xr.DataArray, mode: Literal['py', 'numpy', 'xarray', 'structure'] -) -> list | np.ndarray | xr.DataArray | str: - """ - Convert a DataArray to a different format. - - Args: - data: The DataArray to convert. - mode: The mode to convert to. - - 'py': Convert to python native types (for json) - - 'numpy': Convert to numpy array - - 'xarray': Convert to xarray.DataArray - - 'structure': Convert to strings (for structure, storing variable names) - - Returns: - The converted data. - - Raises: - ValueError: If the mode is unknown. - """ - if mode == 'numpy': - return data.values - elif mode == 'py': - return data.values.tolist() - elif mode == 'xarray': - return data - elif mode == 'structure': - return f':::{data.name}' - else: - raise ValueError(f'Unknown mode {mode}') diff --git a/pyproject.toml b/pyproject.toml index 227eca49e..8c44d025f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ dependencies = [ "xarray >= 2024.2.0, < 2026.0", # CalVer: allow through next calendar year # Optimization and data handling "linopy >= 0.5.1, < 0.6", # Widened from patch pin to minor range - "h5netcdf>=1.0.0, < 2", + "netcdf4 >= 1.6.1, < 2", # Utilities "pyyaml >= 6.0.0, < 7", "rich >= 13.0.0, < 15", From a6ee2c2e5f15faabf5bf61996abf5e2c08a54f7a Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 26 Oct 2025 12:19:40 +0100 Subject: [PATCH 376/448] Update CHANGELOG.md --- CHANGELOG.md | 97 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 59 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 663f087a0..eef465ad9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,8 +21,9 @@ Please keep the format of the changelog consistent with the other releases, so t ## [Template] - ????-??-?? -If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/). +**Summary**: +If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/). ### ✨ Added @@ -50,64 +51,29 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp ## [Unreleased] - ????-??-?? +**Summary**: + If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/). ### ✨ Added -- Support for plotting kwargs in `results.py`, passed to plotly express and matplotlib. -- **Color management system**: New `color_processing.py` module with `process_colors()` function for unified color handling across plotting backends - - Supports flexible color inputs: colorscale names (e.g., 'turbo', 'plasma'), color lists, and label-to-color dictionaries - - Automatic fallback handling when requested colorscales are unavailable - - Seamless integration with both Plotly and Matplotlib colorscales - - Automatic rgba→hex color conversion for Matplotlib compatibility -- **Component color grouping**: Added `setup_colors()` method to `CalculationResults` and `SegmentedCalculationResults` to create color mappings with similar colors for all variables of a component - - Allows grouping components by custom colorscales: `{'CHP': 'red', 'Greys': ['Gastarif', 'Einspeisung'], 'Storage': 'blue'}` - - Colors are automatically assigned using default colorscale if not specified - - For segmented calculations, colors are propagated to all segments for consistent visualization - - Explicit `colors` arguments in plot methods override configured colors (when provided) -- **Plotting configuration**: New `CONFIG.Plotting` section with extensive customization options: - - `default_show`: Control default visibility of plots - - `default_engine`: Choose between 'plotly' or 'matplotlib' - - `default_dpi`: Configure resolution for saved plots (with matplotlib) - - `default_facet_cols`: Set default columns for faceted plots - - `default_sequential_colorscale`: Default for heatmaps and continuous data (default: 'turbo') - - `default_qualitative_colorscale`: Default for categorical plots (default: 'plotly') ### 💥 Breaking Changes ### ♻️ Changed -- **Template integration**: Plotly templates now fully control plot styling without hardcoded overrides -- **Dataset first plotting**: Underlying plotting methods in `plotting.py` now use `xr.Dataset` as the main datatype. DataFrames are automatically converted via `_ensure_dataset()`. Both DataFrames and Datasets can be passed to plotting functions without code changes. -- **Color terminology**: Standardized terminology from "colormap" to "colorscale" throughout the codebase for consistency with Plotly conventions -- **Default colorscales**: Changed default sequential colorscale from 'viridis' to 'turbo' for better perceptual uniformity; qualitative colorscale now defaults to 'plotly' -- **Aggregation plotting**: `Aggregation.plot()` now respects `CONFIG.Plotting.default_qualitative_colorscale` and uses `process_colors()` for consistent color handling -- **netcdf engine**: Following the xarray revert in `xarray==2025.09.2` and after running some benchmarks, we go back to using the netcdf4 engine ### 🗑️ Deprecated ### 🔥 Removed -- Removed `plotting.pie_with_plotly()` method as it was not used -- Removed `ColorProcessor` class - replaced by simpler `process_colors()` function -- Removed `resolve_colors()` helper function - color resolution now handled directly by `process_colors()` ### 🐛 Fixed -- Improved error messages for `engine='matplotlib'` with multidimensional data -- Better dimension validation in `results.plot_heatmap()` ### 🔒 Security ### 📦 Dependencies ### 📝 Docs -- Moved `linked_periods` into correct section of the docstring (was in deprecated params) -- Updated terminology in docstrings from "colormap" to "colorscale" for consistency -- Enhanced examples to demonstrate `setup_colors()` usage: - - `simple_example.py`: Shows automatic color assignment and optional custom configuration - - `scenario_example.py`: Demonstrates component grouping with custom colorscales ### 👷 Development -- Fixed concurrency issue in CI -- **Code architecture**: Extracted color processing logic into dedicated `color_processing.py` module for better separation of concerns -- Refactored from class-based (`ColorProcessor`) to function-based color handling for simpler API and reduced complexity ### 🚧 Known Issues @@ -115,6 +81,61 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp Until here --> +## [3.2.0] - 2025-10-26 + +**Summary**: Enhanced plotting capabilities with consistent color management, custom plotting kwargs support, and centralized I/O handling. + +If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/). + +### ✨ Added + +**Color management:** +- **`setup_colors()` method** for `CalculationResults` and `SegmentedCalculationResults` to configure consistent colors across all plots + - Group components by colorscales: `results.setup_colors({'CHP': 'reds', 'Storage': 'blues', 'Greys': ['Grid', 'Demand']})` + - Automatically propagates to all segments in segmented calculations + - Colors persist across all plot calls unless explicitly overridden +- **Flexible color inputs**: Supports colorscale names (e.g., 'turbo', 'plasma'), color lists, or label-to-color dictionaries +- **Cross-backend compatibility**: Seamless color handling for both Plotly and Matplotlib + +**Plotting customization:** +- **Plotting kwargs support**: Pass additional arguments to plotting backends via `px_kwargs`, `plot_kwargs`, and `backend_kwargs` parameters +- **New `CONFIG.Plotting` configuration section**: + - `default_show`: Control default plot visibility + - `default_engine`: Choose 'plotly' or 'matplotlib' + - `default_dpi`: Set resolution for saved plots + - `default_facet_cols`: Configure default faceting columns + - `default_sequential_colorscale`: Default for heatmaps (now 'turbo') + - `default_qualitative_colorscale`: Default for categorical plots (now 'plotly') + +**I/O improvements:** +- Centralized JSON/YAML I/O with auto-format detection +- Enhanced NetCDF handling with consistent engine usage +- Better numeric formatting in YAML exports + +### ♻️ Changed +- **Default colorscale**: Changed from 'viridis' to 'turbo' for better perceptual uniformity +- **Color terminology**: Standardized from "colormap" to "colorscale" throughout for Plotly consistency +- **Plotting internals**: Now use `xr.Dataset` as primary data type (DataFrames automatically converted) +- **NetCDF engine**: Switched back to netcdf4 engine following xarray updates and performance benchmarks + +### 🔥 Removed +- Removed unused `plotting.pie_with_plotly()` method + +### 🐛 Fixed +- Improved error messages when using `engine='matplotlib'` with multidimensional data +- Better dimension validation in `results.plot_heatmap()` + +### 📝 Docs +- Enhanced examples demonstrating `setup_colors()` usage +- Updated terminology from "colormap" to "colorscale" in docstrings + +### 👷 Development +- Fixed concurrency issue in CI +- Centralized color processing logic into dedicated module +- Refactored to function-based color handling for simpler API + +--- + ## [3.1.1] - 2025-10-20 **Summary**: Fixed a bug when acessing the `effects_per_component` dataset in results without periodic effects. From 612ef70dffd37aefa63318b4eec739da7bce7914 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 26 Oct 2025 15:09:32 +0100 Subject: [PATCH 377/448] Fix CI for examples --- .../05_Two-stage-optimization/two_stage_optimization.py | 6 +++--- tests/test_examples.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/05_Two-stage-optimization/two_stage_optimization.py b/examples/05_Two-stage-optimization/two_stage_optimization.py index b6072a3c2..dde3ae069 100644 --- a/examples/05_Two-stage-optimization/two_stage_optimization.py +++ b/examples/05_Two-stage-optimization/two_stage_optimization.py @@ -123,16 +123,16 @@ # Separate optimization of flow sizes and dispatch start = timeit.default_timer() - calculation_sizing = fx.FullCalculation('Sizing', flow_system.resample('4h')) + calculation_sizing = fx.FullCalculation('Sizing', flow_system.resample('2h')) calculation_sizing.do_modeling() - calculation_sizing.solve(fx.solvers.HighsSolver(0.1 / 100, 600)) + calculation_sizing.solve(fx.solvers.HighsSolver(0.1 / 100, 60)) timer_sizing = timeit.default_timer() - start start = timeit.default_timer() calculation_dispatch = fx.FullCalculation('Dispatch', flow_system) calculation_dispatch.do_modeling() calculation_dispatch.fix_sizes(calculation_sizing.results.solution) - calculation_dispatch.solve(fx.solvers.HighsSolver(0.1 / 100, 600)) + calculation_dispatch.solve(fx.solvers.HighsSolver(0.1 / 100, 60)) timer_dispatch = timeit.default_timer() - start if (calculation_dispatch.results.sizes().round(5) == calculation_sizing.results.sizes().round(5)).all().item(): diff --git a/tests/test_examples.py b/tests/test_examples.py index eca79d7c7..5381a6f49 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -45,7 +45,7 @@ def test_independent_examples(example_script): This imitates behaviour of running the script directly. """ with working_directory(example_script.parent): - timeout = 600 + timeout = 800 try: result = subprocess.run( [sys.executable, example_script.name], From 16aebe27385cc241b65b26e29dd2b393d69d448b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 27 Oct 2025 01:48:58 +0000 Subject: [PATCH 378/448] chore(deps): update dependency mkdocs-material to v9.6.22 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8c44d025f..cedcc3350 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -97,7 +97,7 @@ dev = [ # Documentation building docs = [ "mkdocs==1.6.1", - "mkdocs-material==9.6.21", + "mkdocs-material==9.6.22", "mkdocstrings-python==1.18.2", "mkdocs-table-reader-plugin==3.1.0", "mkdocs-gen-files==0.5.0", From 1794d0b3ad4228b4e9629138551a8299e0cc7fbf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 18:41:22 +0100 Subject: [PATCH 379/448] chore(deps): update dependency astral-sh/uv to v0.9.5 (#444) --- .github/workflows/python-app.yaml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml index 30f5ddbf8..48b712517 100644 --- a/.github/workflows/python-app.yaml +++ b/.github/workflows/python-app.yaml @@ -35,7 +35,7 @@ jobs: - name: Set up uv uses: astral-sh/setup-uv@v6 with: - version: "0.9.2" + version: "0.9.5" enable-cache: true - name: Set up Python @@ -75,7 +75,7 @@ jobs: - name: Set up uv uses: astral-sh/setup-uv@v6 with: - version: "0.9.2" + version: "0.9.5" enable-cache: true - name: Set up Python ${{ matrix.python-version }} @@ -104,7 +104,7 @@ jobs: - name: Set up uv uses: astral-sh/setup-uv@v6 with: - version: "0.9.2" + version: "0.9.5" enable-cache: true - name: Set up Python ${{ env.PYTHON_VERSION }} @@ -130,7 +130,7 @@ jobs: - name: Set up uv uses: astral-sh/setup-uv@v6 with: - version: "0.9.2" + version: "0.9.5" enable-cache: true - name: Set up Python @@ -170,7 +170,7 @@ jobs: - name: Set up uv uses: astral-sh/setup-uv@v6 with: - version: "0.9.2" + version: "0.9.5" enable-cache: true - name: Set up Python @@ -212,7 +212,7 @@ jobs: - name: Set up uv uses: astral-sh/setup-uv@v6 with: - version: "0.9.2" + version: "0.9.5" enable-cache: true - name: Set up Python @@ -298,7 +298,7 @@ jobs: - name: Set up uv uses: astral-sh/setup-uv@v6 with: - version: "0.9.2" + version: "0.9.5" enable-cache: true - name: Set up Python @@ -379,7 +379,7 @@ jobs: - name: Set up uv uses: astral-sh/setup-uv@v6 with: - version: "0.9.2" + version: "0.9.5" enable-cache: true - name: Set up Python From 633a067214e5453c191ce554b1646fa125b38c92 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 28 Oct 2025 19:42:38 +0100 Subject: [PATCH 380/448] Improve bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 42 ++++++++++++++++++++------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 94b4491a5..9e7a03844 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -38,18 +38,40 @@ body: description: | Provide the smallest possible code example that reproduces the bug. See [how to create minimal bug reports](https://matthewrocklin.com/minimal-bug-reports). - placeholder: | - import flixopt as fx + value: | import pandas as pd + import numpy as np + import flixopt as fx - # Minimal example that reproduces the bug - timesteps = pd.date_range('2024-01-01', periods=24, freq='h') - flow_system = fx.FlowSystem(timesteps) + fx.CONFIG.Logging.console = True + fx.CONFIG.Logging.level = 'DEBUG' + fx.CONFIG.apply() + flow_system = fx.FlowSystem(pd.date_range('2020-01-01', periods=3, freq='h')) - # Add components that trigger the bug... + flow_system.add_elements( + fx.Bus('Heat'), + fx.Bus('Gas'), + fx.Effect('Costs', '€', 'Cost', is_standard=True, is_objective=True), + fx.linear_converters.Boiler( + 'Boiler', + eta=0.5, + Q_th=fx.Flow(label='Heat', bus='Heat', size=50), + Q_fu=fx.Flow(label='Gas', bus='Gas'), + ), + fx.Sink( + 'Sink', + inputs=[ + fx.Flow(label='Demand', bus='Heat', size=1, fixed_relative_profile=np.array([30, 0, 20])) + ], + ), + fx.Source( + 'Source', + outputs=[fx.Flow(label='Gas', bus='Gas', size=1000, effects_per_flow_hour=0.04)], + ), + ) + + calculation = fx.FullCalculation('Simulation1', flow_system).do_modeling().solve(fx.solvers.HighsSolver(0.01, 60)) - # Show the problematic operation - result = flow_system.solve() # This should fail/behave unexpectedly render: python validations: required: true @@ -82,7 +104,7 @@ body: label: Operating System placeholder: "e.g., Windows 11, macOS 14.2, Ubuntu 22.04" validations: - required: true + required: false - type: input id: python-version @@ -90,7 +112,7 @@ body: label: Python Version placeholder: "e.g., 3.11.5" validations: - required: true + required: false - type: textarea id: environment From acf35ebd2275332af10b7e55fc89d2e41a30334d Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 28 Oct 2025 19:42:51 +0100 Subject: [PATCH 381/448] Make minimal_example actually minimal --- examples/00_Minmal/minimal_example.py | 87 ++++++++------------------- 1 file changed, 24 insertions(+), 63 deletions(-) diff --git a/examples/00_Minmal/minimal_example.py b/examples/00_Minmal/minimal_example.py index 81b7c2dba..6a0ed3831 100644 --- a/examples/00_Minmal/minimal_example.py +++ b/examples/00_Minmal/minimal_example.py @@ -1,76 +1,37 @@ """ -This script shows how to use the flixopt framework to model a super minimalistic energy system. +This script shows how to use the flixopt framework to model a super minimalistic energy system in the most concise way possible. +THis can also be used to create proposals for new features, bug reports etc """ import numpy as np import pandas as pd -from rich.pretty import pprint import flixopt as fx if __name__ == '__main__': - # Enable console logging fx.CONFIG.Logging.console = True fx.CONFIG.apply() - # --- Define the Flow System, that will hold all elements, and the time steps you want to model --- - timesteps = pd.date_range('2020-01-01', periods=3, freq='h') - flow_system = fx.FlowSystem(timesteps) - - # --- Define Thermal Load Profile --- - # Load profile (e.g., kW) for heating demand over time - thermal_load_profile = np.array([30, 0, 20]) - - # --- Define Energy Buses --- - # These are balancing nodes (inputs=outputs) and balance the different energy carriers your system - flow_system.add_elements(fx.Bus('District Heating'), fx.Bus('Natural Gas')) - - # --- Define Objective Effect (Cost) --- - # Cost effect representing the optimization objective (minimizing costs) - cost_effect = fx.Effect('costs', '€', 'Cost', is_standard=True, is_objective=True) - - # --- Define Flow System Components --- - # Boiler component with thermal output (heat) and fuel input (gas) - boiler = fx.linear_converters.Boiler( - 'Boiler', - eta=0.5, - Q_th=fx.Flow(label='Thermal Output', bus='District Heating', size=50), - Q_fu=fx.Flow(label='Fuel Input', bus='Natural Gas'), + flow_system = fx.FlowSystem(pd.date_range('2020-01-01', periods=3, freq='h')) + + flow_system.add_elements( + fx.Bus('Heat'), + fx.Bus('Gas'), + fx.Effect('Costs', '€', 'Cost', is_standard=True, is_objective=True), + fx.linear_converters.Boiler( + 'Boiler', + eta=0.5, + Q_th=fx.Flow(label='Heat', bus='Heat', size=50), + Q_fu=fx.Flow(label='Gas', bus='Gas'), + ), + fx.Sink( + 'Sink', + inputs=[fx.Flow(label='Demand', bus='Heat', size=1, fixed_relative_profile=np.array([30, 0, 20]))], + ), + fx.Source( + 'Source', + outputs=[fx.Flow(label='Gas', bus='Gas', size=1000, effects_per_flow_hour=0.04)], + ), ) - # Heat load component with a fixed thermal demand profile - heat_load = fx.Sink( - 'Heat Demand', - inputs=[ - fx.Flow(label='Thermal Load', bus='District Heating', size=1, fixed_relative_profile=thermal_load_profile) - ], - ) - - # Gas source component with cost-effect per flow hour - gas_source = fx.Source( - 'Natural Gas Tariff', - outputs=[fx.Flow(label='Gas Flow', bus='Natural Gas', size=1000, effects_per_flow_hour=0.04)], # 0.04 €/kWh - ) - - # --- Build the Flow System --- - # Add all components and effects to the system - flow_system.add_elements(cost_effect, boiler, heat_load, gas_source) - - # --- Define, model and solve a Calculation --- - calculation = fx.FullCalculation('Simulation1', flow_system) - calculation.do_modeling() - calculation.solve(fx.solvers.HighsSolver(0.01, 60)) - - # --- Analyze Results --- - # Access the results of an element - df1 = calculation.results['costs'].filter_solution('time').to_dataframe() - - # Plot the results of a specific element - calculation.results['District Heating'].plot_node_balance_pie() - calculation.results['District Heating'].plot_node_balance() - - # Save results to a file - df2 = calculation.results['District Heating'].node_balance().to_dataframe() - # df2.to_csv('results/District Heating.csv') # Save results to csv - - # Print infos to the console. - pprint(calculation.summary) + calculation = fx.FullCalculation('Simulation1', flow_system).do_modeling().solve(fx.solvers.HighsSolver(0.01, 60)) + calculation.results['Heat'].plot_node_balance() From 6ed521f3809e6abfaaacbc02a607c758a8d8ede9 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 28 Oct 2025 19:50:13 +0100 Subject: [PATCH 382/448] Add task.yml and strip down config.yml Make issue templates more minimalistic --- .github/ISSUE_TEMPLATE/bug_report.yml | 80 ++------------ .github/ISSUE_TEMPLATE/config.yml | 8 +- .github/ISSUE_TEMPLATE/feature_request.yml | 117 +++------------------ .github/ISSUE_TEMPLATE/task.yml | 35 ++++++ CHANGELOG.md | 1 + 5 files changed, 62 insertions(+), 179 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/task.yml diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 9e7a03844..3b1a32fb2 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -6,35 +6,21 @@ body: - type: markdown attributes: value: | - Thanks for taking the time to fill out this bug report! - - **Before submitting**: Please search [existing issues](https://github.com/flixOpt/flixopt/issues) to avoid duplicates. - -- type: checkboxes - id: checks - attributes: - label: Version Confirmation - description: Please confirm you can reproduce this on a supported version - options: - - label: I have confirmed this bug exists on the latest [release](https://github.com/flixOpt/flixopt/releases) of FlixOpt - required: true + **Quick guide**: Describe what's broken, provide code to reproduce if possible. + For simple bugs, just fill the first field. - type: textarea id: problem attributes: - label: Bug Description - description: Clearly describe what went wrong - placeholder: | - What happened? What did you expect to happen instead? - - Include any error messages or unexpected outputs. + label: What's broken? + description: Describe the bug - what happened vs. what you expected validations: required: true - type: textarea id: example attributes: - label: Minimal Reproducible Example + label: Code to reproduce description: | Provide the smallest possible code example that reproduces the bug. See [how to create minimal bug reports](https://matthewrocklin.com/minimal-bug-reports). @@ -73,61 +59,17 @@ body: calculation = fx.FullCalculation('Simulation1', flow_system).do_modeling().solve(fx.solvers.HighsSolver(0.01, 60)) render: python - validations: - required: true - type: textarea id: error-output attributes: - label: Error Output - description: If there's an error message, paste the full traceback here + label: Error message + description: Paste the full traceback if there is one render: shell -- type: dropdown - id: solver - attributes: - label: Solver Used - description: Which solver were you using? - options: - - HiGHS (default) - - Gurobi - - CPLEX - - GLPK - - CBC - - Other (specify below) - validations: - required: true - -- type: input - id: os - attributes: - label: Operating System - placeholder: "e.g., Windows 11, macOS 14.2, Ubuntu 22.04" - validations: - required: false - -- type: input - id: python-version - attributes: - label: Python Version - placeholder: "e.g., 3.11.5" - validations: - required: false - - type: textarea - id: environment + id: context attributes: - label: Environment Info - description: | - Run one of these commands and paste the output: - - `pip freeze` - - `conda env export` - render: shell - value: > -
- - ``` - Replace this with your environment info - ``` - -
+ label: Additional context + description: Solver, Python/OS version, environment details, or anything else relevant + placeholder: "HiGHS solver, Python 3.11, macOS 14" diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 94d96c479..0c30b34f2 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -3,12 +3,6 @@ contact_links: - name: 🤔 Modeling Questions url: https://github.com/flixOpt/flixopt/discussions/categories/q-a about: "How to model specific energy systems, components, and constraints" - - name: ⚡ Performance & Optimization - url: https://github.com/flixOpt/flixopt/discussions/categories/performance - about: "Solver performance, memory usage, and optimization speed issues" - - name: 💡 Ideas & Suggestions - url: https://github.com/flixOpt/flixopt/discussions/categories/ideas - about: "Share ideas and discuss potential improvements with the community" - name: 📖 Documentation url: https://flixopt.github.io/flixopt/latest/ - about: "Browse guides, API reference, and examples" + about: "Guides, API reference, and examples" diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index dd5c8def2..1c48cf10c 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -6,122 +6,33 @@ body: - type: markdown attributes: value: | - Thanks for suggesting a new feature! - - **Before submitting**: Please search [existing issues](https://github.com/flixOpt/flixopt/issues) and check our [roadmap](https://github.com/flixOpt/flixopt/discussions) to avoid duplicates. - -- type: checkboxes - id: checks - attributes: - label: Prerequisites - options: - - label: I have searched existing issues and discussions - required: true - - label: I have checked the [documentation](https://flixopt.github.io/flixopt/latest/) - required: true - -- type: dropdown - id: feature-type - attributes: - label: Feature Category - description: What type of feature is this? - options: - - New Component (storage, generation, conversion, etc.) - - Enhancement to Existing Component - - New Optimization Feature - - Data Input/Output Improvement - - Results/Visualization Enhancement - - Performance/Solver Improvement - - API/Usability Improvement - - Documentation/Examples - - Other - validations: - required: true + **Quick guide**: Describe what you want and why it's useful. + Skip optional fields for simple ideas. - type: textarea - id: problem + id: description attributes: - label: Problem Statement - description: What problem would this feature solve? - placeholder: | - Current limitation: "FlixOpt doesn't support [specific energy system component/feature]..." - - Impact: "This prevents users from modeling [specific scenarios]..." - -- type: textarea - id: solution - attributes: - label: Proposed Solution - description: Describe your proposed solution in detail - placeholder: | - I propose adding a new component/feature that would... - - Key capabilities: - - Feature 1 - - Feature 2 - - Feature 3 + label: What feature or improvement? + description: Describe what should be added or changed validations: required: true - type: textarea id: use-case attributes: - label: Use Case & Examples - description: Provide concrete examples of how this would be used - placeholder: | - Real-world scenario: "I'm modeling a microgrid with battery storage and need to..." - - Specific requirements: - - Must handle [specific constraint] - - Should support [specific behavior] - - Would benefit [specific user group] - validations: - required: true + label: Use case + description: What problem does this solve? What would you use it for? + placeholder: "When modeling X, I need to do Y..." - type: textarea - id: code-example + id: api-idea attributes: - label: Desired API (Optional) - description: Show how you'd like to use this feature + label: API idea (optional) + description: How might it work? Sketch a rough API if you have ideas placeholder: | - # Example of proposed API - component = fx.NewComponent( + # Example: + component = fx.NewThing( label='example', - parameter1=value1, - parameter2=value2 + param=value ) - - flow_system.add_component(component) render: python - -- type: textarea - id: alternatives - attributes: - label: Alternatives Considered - description: What workarounds or alternatives have you tried? - placeholder: | - Current workaround: "I'm currently using [existing component] but it doesn't support..." - - Other approaches considered: "I looked into [alternative] but..." - -- type: dropdown - id: priority - attributes: - label: Priority/Impact - description: How important is this feature for your work? - options: - - Critical - Blocking important work - - High - Would significantly improve workflow - - Medium - Nice to have enhancement - - Low - Minor improvement - -- type: textarea - id: additional-context - attributes: - label: Additional Context - description: References, papers, examples from other tools, etc. - placeholder: | - References: - - Research paper: [Title and link] - - Similar feature in [other tool]: [description] - - Industry standard: [description] diff --git a/.github/ISSUE_TEMPLATE/task.yml b/.github/ISSUE_TEMPLATE/task.yml new file mode 100644 index 000000000..bb4741e02 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/task.yml @@ -0,0 +1,35 @@ +name: 📋 Task +description: Track work items, refactoring, cleanup, or general todos +title: "[TASK] " +labels: ["type: task"] +body: +- type: markdown + attributes: + value: | + **Quick guide**: Describe what needs to be done. + Use this for refactoring, cleanup, documentation, or general work items. + +- type: textarea + id: description + attributes: + label: What needs to be done? + description: Describe the task + validations: + required: true + +- type: textarea + id: details + attributes: + label: Details + description: Context, steps, requirements, or anything else relevant + placeholder: | + - Step 1 + - Step 2 + - Related to: #123 + +- type: textarea + id: acceptance + attributes: + label: Done when... + description: What defines this task as complete? + placeholder: "Tests pass, code is clean, docs updated..." diff --git a/CHANGELOG.md b/CHANGELOG.md index eef465ad9..66a5571fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,7 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp ### 📝 Docs ### 👷 Development +- Improved issue templates ### 🚧 Known Issues From dd33f6aa5948dfa84b87bc156f5c38b6fe8f45d4 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 29 Oct 2025 11:04:18 +0100 Subject: [PATCH 383/448] Fix Bug in resample regarding the duration of the last timestep duration (#442) * Fix Bug in resample regarding the duration of the last timestep duration * Typo * Improve type hints * Update CHANGELOG.md * Fix typehints --- CHANGELOG.md | 1 + flixopt/flow_system.py | 12 ++++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66a5571fa..d3d5e2018 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,6 +66,7 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp ### 🔥 Removed ### 🐛 Fixed +- Fixed resampling of FlowSystem to reset `hours_of_last_timestep` and `hours_of_previous_timesteps` properly ### 🔒 Security diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index fd0f6a98d..9f29ea96e 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -80,7 +80,7 @@ def __init__( timesteps: pd.DatetimeIndex, periods: pd.Index | None = None, scenarios: pd.Index | None = None, - hours_of_last_timestep: float | None = None, + hours_of_last_timestep: int | float | None = None, hours_of_previous_timesteps: int | float | np.ndarray | None = None, weights: PeriodicDataUser | None = None, scenario_independent_sizes: bool | list[str] = True, @@ -929,6 +929,8 @@ def resample( self, time: str, method: Literal['mean', 'sum', 'max', 'min', 'first', 'last', 'std', 'var', 'median', 'count'] = 'mean', + hours_of_last_timestep: int | float | None = None, + hours_of_previous_timesteps: int | float | np.ndarray | None = None, **kwargs: Any, ) -> FlowSystem: """ @@ -938,10 +940,12 @@ def resample( Args: time: Resampling frequency (e.g., '3h', '2D', '1M') method: Resampling method. Recommended: 'mean', 'first', 'last', 'max', 'min' + hours_of_last_timestep: New duration of the last time step. Defaults to the last time interval of the new timesteps + hours_of_previous_timesteps: New duration of the previous timestep. Defaults to the first time increment of the new timesteps **kwargs: Additional arguments passed to xarray.resample() Returns: - FlowSystem: New FlowSystem with resampled data + FlowSystem: New resampled FlowSystem """ if not self.connected_and_transformed: self.connect_and_transform() @@ -975,6 +979,10 @@ def resample( else: resampled_dataset = resampled_time_data + # Let FlowSystem recalculate or use explicitly set value + resampled_dataset.attrs['hours_of_last_timestep'] = hours_of_last_timestep + resampled_dataset.attrs['hours_of_previous_timesteps'] = hours_of_previous_timesteps + return self.__class__.from_dataset(resampled_dataset) @property From c40cc9d27bfe6a8a990b01a2465d57d2c8e1b94a Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 29 Oct 2025 12:44:03 +0100 Subject: [PATCH 384/448] Update CI: RUn examples on push to main to catch errors pre release --- .github/workflows/python-app.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml index 48b712517..f4dbc28c5 100644 --- a/.github/workflows/python-app.yaml +++ b/.github/workflows/python-app.yaml @@ -95,7 +95,7 @@ jobs: timeout-minutes: 45 needs: lint # Only run examples on releases (tags) - if: startsWith(github.ref, 'refs/tags/v') + if: startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'push' && github.ref == 'refs/heads/main') steps: - name: Check out code From 98db5e4cdd62a9312e36120e4f648ecb04fc82b3 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 29 Oct 2025 12:57:28 +0100 Subject: [PATCH 385/448] Update CHANGELOG.md --- CHANGELOG.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3d5e2018..fec1a0d46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,7 +66,6 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp ### 🔥 Removed ### 🐛 Fixed -- Fixed resampling of FlowSystem to reset `hours_of_last_timestep` and `hours_of_previous_timesteps` properly ### 🔒 Security @@ -75,7 +74,6 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp ### 📝 Docs ### 👷 Development -- Improved issue templates ### 🚧 Known Issues @@ -83,6 +81,20 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp Until here --> +## [3.2.1] - 2025-10-29 + +**Summary**: + +If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/). + +### 🐛 Fixed +- Fixed resampling of FlowSystem to reset `hours_of_last_timestep` and `hours_of_previous_timesteps` properly + +### 👷 Development +- Improved issue templates + +--- + ## [3.2.0] - 2025-10-26 **Summary**: Enhanced plotting capabilities with consistent color management, custom plotting kwargs support, and centralized I/O handling. From 2b2e835cc1feeba603065d4638740b4677e0e572 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Wed, 29 Oct 2025 22:32:00 +0100 Subject: [PATCH 386/448] Feature/containers-and-reprs-for-better-element-organization (#443) * Improve __str__ of FlowSystem * Use ElementContainer class * Inherrit from dict * Improve repr * Assign flow.componet right away after the flow is passed to a Component * Add FLowContainer * Improve Error Message * Improve repr of FlowSystem * Use ElementContainer in results.py * Use a Mixin instead and use in results as well * Improve Results container usage * Simplify display * Make CalculationResults iterable over sub containers * Create CompositeContainerMixin and use in FLowSystem and CalcualtionResults * Remove from commons.py * Re-add status * Remove redundant stuff and add deprecation * Add type annotation to CompositeContainerMixin * Use ContainerMixin in EffectsCollection * Optimize acess patterns * Remove unneded method * Iterate group containers to collect values and pairs in one pass. * Add sorting to containers * Reorder container groups * Add guardfs for flow results * Rename _set_flow_labels and validate that the FLow is not already connected to another Component * Add reprs * Add reprs to results classes * Improve the reprs * Remove code duplication * Improve repr of ELements * Improve Error message * Remove redundant sum('time') * Pre compile re pattern * Make code more concise * Synchronise order of Containers in both FlowSystem and CalculationResults * Add note about caching * Update CHANGELOG.md * Changed from O(n) element in self.values() to O(1) label-based checking, avoiding expensive container merges. * Added early validation to ensure flows in prevent_simultaneous_flows belong to the component, preventing modeling order issues. * Import Statement Fix (structure.py:10, 916) * Flow repr Improvement * Optimized Uniqueness Check * Fixed separator mangling that produced strings like "(kg)objective" instead of "(kg) | objective" * Flows Property Caching * Fixed __iter__ method (effects.py:591-592) Updated all code that relied on old behavior * Deterministic Ordering for Effects * Removed Redundant Import * Deterministic Ordering for Flows * Deduplicate prevent_simultaneous_flows * Improved DataArray Handling in repr * Enhanced Bus repr * Enhanced Component repr * Update CHANGELOG.md * Simplify reprs * Simplify reprs * Simplify repr formating of numbers * Reformat reprs * Reformat reprs * Reformat reprs * Reformat reprs * Reformat reprs * Reformat reprs * Reformat reprs * Reformat reprs * Reformat reprs * Reformat reprs * Use helper formats to create the reprs * Remove str from Interface class * Improve FlowSystem repr * Improve Reprs of Containers * Numeric formatter handles NaN/Inf/empty inputs * Effect repr shows constraints for both min and max bounds * Improve repr code * Improve detection of defaults * Removed the unused _format_size method (dead code with no call sites * Simplified flows container construction by passing the list directly instead of converting to dict first * Moved ResultsContainer import to module level for consistency * prevents mutation of the original self.solution.attrs * Fix return type annotation to match base class. * Extract _format_value_for_repr() as dedicated method * FIx type hint * Improve Error message * Typo in docs * Fix indents in docs and CHANGELOG.md * Improve documentation of the FLowSystem access --- CHANGELOG.md | 71 +++---- docs/user-guide/index.md | 39 ++-- flixopt/calculation.py | 2 +- flixopt/components.py | 10 + flixopt/effects.py | 50 +++-- flixopt/elements.py | 55 +++++- flixopt/flow_system.py | 223 +++++++++++++--------- flixopt/interface.py | 21 +++ flixopt/io.py | 347 +++++++++++++++++++++++++++++++++++ flixopt/results.py | 68 ++++--- flixopt/structure.py | 387 ++++++++++++++++++++++++++++++++++----- tests/test_functional.py | 36 ++-- 12 files changed, 1062 insertions(+), 247 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fec1a0d46..4794f92ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,8 +60,17 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp ### 💥 Breaking Changes ### ♻️ Changed +**Improved repr methods:** +- **Results classes** (`ComponentResults`, `BusResults`, `FlowResults`, `EffectResults`) now show concise header with key metadata followed by xarray Dataset repr +- **Element classes** (`Component`, `Bus`, `Flow`, `Effect`, `Storage`) now show one-line summaries with essential information (connections, sizes, capacities, constraints) + +**Container-based access:** +- **FlowSystem** now provides dict-like access patterns for all elements +- Use `flow_system['element_label']`, `flow_system.keys()`, `flow_system.values()`, and `flow_system.items()` for unified element access +- Specialized containers (`components`, `buses`, `effects`, `flows`) offer type-specific access with helpful error messages ### 🗑️ Deprecated +- **`FlowSystem.all_elements`** property is deprecated in favor of dict-like interface (`flow_system['label']`, `.keys()`, `.values()`, `.items()`). Will be removed in v4.0.0. ### 🔥 Removed @@ -105,21 +114,21 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp **Color management:** - **`setup_colors()` method** for `CalculationResults` and `SegmentedCalculationResults` to configure consistent colors across all plots - - Group components by colorscales: `results.setup_colors({'CHP': 'reds', 'Storage': 'blues', 'Greys': ['Grid', 'Demand']})` - - Automatically propagates to all segments in segmented calculations - - Colors persist across all plot calls unless explicitly overridden + - Group components by colorscales: `results.setup_colors({'CHP': 'reds', 'Storage': 'blues', 'Greys': ['Grid', 'Demand']})` + - Automatically propagates to all segments in segmented calculations + - Colors persist across all plot calls unless explicitly overridden - **Flexible color inputs**: Supports colorscale names (e.g., 'turbo', 'plasma'), color lists, or label-to-color dictionaries - **Cross-backend compatibility**: Seamless color handling for both Plotly and Matplotlib **Plotting customization:** - **Plotting kwargs support**: Pass additional arguments to plotting backends via `px_kwargs`, `plot_kwargs`, and `backend_kwargs` parameters - **New `CONFIG.Plotting` configuration section**: - - `default_show`: Control default plot visibility - - `default_engine`: Choose 'plotly' or 'matplotlib' - - `default_dpi`: Set resolution for saved plots - - `default_facet_cols`: Configure default faceting columns - - `default_sequential_colorscale`: Default for heatmaps (now 'turbo') - - `default_qualitative_colorscale`: Default for categorical plots (now 'plotly') + - `default_show`: Control default plot visibility + - `default_engine`: Choose 'plotly' or 'matplotlib' + - `default_dpi`: Set resolution for saved plots + - `default_facet_cols`: Configure default faceting columns + - `default_sequential_colorscale`: Default for heatmaps (now 'turbo') + - `default_qualitative_colorscale`: Default for categorical plots (now 'plotly') **I/O improvements:** - Centralized JSON/YAML I/O with auto-format detection @@ -286,12 +295,12 @@ This replaces `specific_share_to_other_effects_*` parameters and inverts the dir **API and Behavior Changes:** - **Effect system redesigned** (no deprecation): - - **Terminology changes**: Effect domains renamed for clarity: `operation` → `temporal`, `invest`/`investment` → `periodic` - - **Sharing system**: The old `specific_share_to_other_effects_*` parameters were completely replaced with the new `share_from_temporal` and `share_from_periodic` syntax (see 🔥 Removed section) + - **Terminology changes**: Effect domains renamed for clarity: `operation` → `temporal`, `invest`/`investment` → `periodic` + - **Sharing system**: The old `specific_share_to_other_effects_*` parameters were completely replaced with the new `share_from_temporal` and `share_from_periodic` syntax (see 🔥 Removed section) - **FlowSystem independence**: FlowSystems cannot be shared across multiple Calculations anymore. A copy of the FlowSystem is created instead, making every Calculation independent. Each Subcalculation in `SegmentedCalculation` now has its own distinct `FlowSystem` object - **Bus and Effect object assignment**: Direct assignment of Bus/Effect objects is no longer supported. Use labels (strings) instead: - - `Flow.bus` must receive a string label, not a Bus object - - Effect shares must use effect labels (strings) in dictionaries, not Effect objects + - `Flow.bus` must receive a string label, not a Bus object + - Effect shares must use effect labels (strings) in dictionaries, not Effect objects - **Logging defaults** (from v2.2.0): Console and file logging are now disabled by default. Enable explicitly with `CONFIG.Logging.console = True` and `CONFIG.apply()` **Class and Method Renaming:** @@ -305,14 +314,14 @@ This replaces `specific_share_to_other_effects_*` parameters and inverts the dir - Investment binary variable: `is_invested` → `invested` in `InvestmentModel` - Switch tracking variables in `OnOffModel`: - - `switch_on` → `switch|on` - - `switch_off` → `switch|off` - - `switch_on_nr` → `switch|count` + - `switch_on` → `switch|on` + - `switch_off` → `switch|off` + - `switch_on_nr` → `switch|count` - Effect submodel variables (following terminology changes): - - `Effect(invest)|total` → `Effect(periodic)` - - `Effect(operation)|total` → `Effect(temporal)` - - `Effect(operation)|total_per_timestep` → `Effect(temporal)|per_timestep` - - `Effect|total` → `Effect` + - `Effect(invest)|total` → `Effect(periodic)` + - `Effect(operation)|total` → `Effect(temporal)` + - `Effect(operation)|total_per_timestep` → `Effect(temporal)|per_timestep` + - `Effect|total` → `Effect` **Data Structure Changes:** @@ -533,7 +542,7 @@ This replaces `specific_share_to_other_effects_*` parameters and inverts the dir ### ✨ Added - **Network Visualization**: Added `FlowSystem.start_network_app()` and `FlowSystem.stop_network_app()` to easily visualize the network structure of a flow system in an interactive Dash web app - - *Note: This is still experimental and might change in the future* + - *Note: This is still experimental and might change in the future* ### ♻️ Changed - **Multi-Flow Support**: `Sink`, `Source`, and `SourceAndSink` now accept multiple `flows` as `inputs` and `outputs` instead of just one. This enables modeling more use cases with these classes @@ -575,8 +584,8 @@ This replaces `specific_share_to_other_effects_*` parameters and inverts the dir ### 🐛 Fixed - Storage losses per hour were not calculated correctly, as mentioned by @brokenwings01. This might have led to issues when modeling large losses and long timesteps. - - Old implementation: $c(\text{t}_{i}) \cdot (1-\dot{\text{c}}_\text{rel,loss}(\text{t}_i)) \cdot \Delta \text{t}_{i}$ - - Correct implementation: $c(\text{t}_{i}) \cdot (1-\dot{\text{c}}_\text{rel,loss}(\text{t}_i)) ^{\Delta \text{t}_{i}}$ + - Old implementation: $c(\text{t}_{i}) \cdot (1-\dot{\text{c}}_\text{rel,loss}(\text{t}_i)) \cdot \Delta \text{t}_{i}$ + - Correct implementation: $c(\text{t}_{i}) \cdot (1-\dot{\text{c}}_\text{rel,loss}(\text{t}_i)) ^{\Delta \text{t}_{i}}$ ### 🚧 Known Issues - Just to mention: Plotly >= 6 may raise errors if "nbformat" is not installed. We pinned plotly to <6, but this may be fixed in the future. @@ -601,10 +610,10 @@ This replaces `specific_share_to_other_effects_*` parameters and inverts the dir ### 💥 Breaking Changes - Restructured the modeling of the On/Off state of Flows or Components - - Variable renaming: `...|consecutive_on_hours` → `...|ConsecutiveOn|hours` - - Variable renaming: `...|consecutive_off_hours` → `...|ConsecutiveOff|hours` - - Constraint renaming: `...|consecutive_on_hours_con1` → `...|ConsecutiveOn|con1` - - Similar pattern for all consecutive on/off constraints + - Variable renaming: `...|consecutive_on_hours` → `...|ConsecutiveOn|hours` + - Variable renaming: `...|consecutive_off_hours` → `...|ConsecutiveOff|hours` + - Constraint renaming: `...|consecutive_on_hours_con1` → `...|ConsecutiveOn|con1` + - Similar pattern for all consecutive on/off constraints ### 🐛 Fixed - Fixed the lower bound of `flow_rate` when using optional investments without OnOffParameters @@ -650,10 +659,10 @@ This replaces `specific_share_to_other_effects_*` parameters and inverts the dir **Variable Structure:** - Restructured the modeling of the On/Off state of Flows or Components - - Variable renaming: `...|consecutive_on_hours` → `...|ConsecutiveOn|hours` - - Variable renaming: `...|consecutive_off_hours` → `...|ConsecutiveOff|hours` - - Constraint renaming: `...|consecutive_on_hours_con1` → `...|ConsecutiveOn|con1` - - Similar pattern for all consecutive on/off constraints + - Variable renaming: `...|consecutive_on_hours` → `...|ConsecutiveOn|hours` + - Variable renaming: `...|consecutive_off_hours` → `...|ConsecutiveOff|hours` + - Constraint renaming: `...|consecutive_on_hours_con1` → `...|ConsecutiveOn|con1` + - Similar pattern for all consecutive on/off constraints ### 🔥 Removed - **Pyomo dependency** (replaced by linopy) diff --git a/docs/user-guide/index.md b/docs/user-guide/index.md index df97bf768..b20d15263 100644 --- a/docs/user-guide/index.md +++ b/docs/user-guide/index.md @@ -15,15 +15,22 @@ Every FlixOpt model starts with creating a FlowSystem. It: - Contains and connects [components](#components), [buses](#buses), and [flows](#flows) - Manages the [effects](#effects) (objectives and constraints) +FlowSystem provides two ways to access elements: + +- **Dict-like interface**: Access any element by label: `flow_system['Boiler']`, `'Boiler' in flow_system`, `flow_system.keys()` +- **Direct containers**: Access type-specific containers: `flow_system.components`, `flow_system.buses`, `flow_system.effects`, `flow_system.flows` + +Element labels must be unique across all types. See the [`FlowSystem` API reference][flixopt.flow_system.FlowSystem] for detailed examples and usage patterns. + ### Flows [`Flow`][flixopt.elements.Flow] objects represent the movement of energy or material between a [Bus](#buses) and a [Component](#components) in a predefined direction. -- Have a `size` which, generally speaking, defines how fast energy or material can be moved. Usually measured in MW, kW, m³/h, etc. -- Have a `flow_rate`, which is defines how fast energy or material is transported. Usually measured in MW, kW, m³/h, etc. +- Have a `size` which, generally speaking, defines how much energy or material can be moved. Usually measured in MW, kW, m³/h, etc. +- Have a `flow_rate`, which defines how fast energy or material is transported. Usually measured in MW, kW, m³/h, etc. - Have constraints to limit the flow-rate (min/max, total flow hours, on/off etc.) - Can have fixed profiles (for demands or renewable generation) -- Can have [Effects](#effects) associated by their use (operation, investment, on/off, ...) +- Can have [Effects](#effects) associated by their use (costs, emissions, labour, ...) #### Flow Hours While the **Flow Rate** defines the rate in which energy or material is transported, the **Flow Hours** define the amount of energy or material that is transported. @@ -50,21 +57,21 @@ Examples: [`Component`][flixopt.elements.Component] objects usually represent physical entities in your system that interact with [`Flows`][flixopt.elements.Flow]. The generic component types work across all domains: - [`LinearConverters`][flixopt.components.LinearConverter] - Converts input flows to output flows with (piecewise) linear relationships - - *Energy: boilers, heat pumps, turbines* - - *Manufacturing: assembly lines, processing equipment* - - *Chemistry: reactors, separators* + - *Energy: boilers, heat pumps, turbines* + - *Manufacturing: assembly lines, processing equipment* + - *Chemistry: reactors, separators* - [`Storages`][flixopt.components.Storage] - Stores energy or material over time - - *Energy: batteries, thermal storage, gas storage* - - *Logistics: warehouses, buffer inventory* - - *Water: reservoirs, tanks* + - *Energy: batteries, thermal storage, gas storage* + - *Logistics: warehouses, buffer inventory* + - *Water: reservoirs, tanks* - [`Sources`][flixopt.components.Source] / [`Sinks`][flixopt.components.Sink] / [`SourceAndSinks`][flixopt.components.SourceAndSink] - Produce or consume flows - - *Energy: demands, renewable generation* - - *Manufacturing: raw material supply, product demand* - - *Supply chain: suppliers, customers* + - *Energy: demands, renewable generation* + - *Manufacturing: raw material supply, product demand* + - *Supply chain: suppliers, customers* - [`Transmissions`][flixopt.components.Transmission] - Moves flows between locations with possible losses - - *Energy: pipelines, power lines* - - *Logistics: transport routes* - - *Water: distribution networks* + - *Energy: pipelines, power lines* + - *Logistics: transport routes* + - *Water: distribution networks* **Pre-built specialized components** for energy systems include [`Boilers`][flixopt.linear_converters.Boiler], [`HeatPumps`][flixopt.linear_converters.HeatPump], [`CHPs`][flixopt.linear_converters.CHP], etc. These can serve as blueprints for custom domain-specific components. @@ -105,7 +112,7 @@ FlixOpt offers different calculation modes: The results of a calculation are stored in a [`CalculationResults`][flixopt.results.CalculationResults] object. This object contains the solutions of the optimization as well as all information about the [`Calculation`][flixopt.calculation.Calculation] and the [`FlowSystem`][flixopt.flow_system.FlowSystem] it was created from. -The solutions is stored as an `xarray.Dataset`, but can be accessed through their assotiated Component, Bus or Effect. +The solution is stored as an `xarray.Dataset`, but can be accessed through their assotiated Component, Bus or Effect. This [`CalculationResults`][flixopt.results.CalculationResults] object can be saved to file and reloaded from file, allowing you to analyze the results anytime after the solve. diff --git a/flixopt/calculation.py b/flixopt/calculation.py index f744c5247..5e919dbf5 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -112,7 +112,7 @@ def main_results(self) -> dict[str, Scalar | dict]: 'periodic': effect.submodel.periodic.total.solution.values, 'total': effect.submodel.total.solution.values, } - for effect in self.flow_system.effects + for effect in sorted(self.flow_system.effects.values(), key=lambda e: e.label_full.upper()) }, 'Invest-Decisions': { 'Invested': { diff --git a/flixopt/components.py b/flixopt/components.py index 09156e1dc..8f89378ae 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -11,6 +11,7 @@ import numpy as np import xarray as xr +from . import io as fx_io from .core import PeriodicDataUser, PlausibilityError, TemporalData, TemporalDataUser from .elements import Component, ComponentModel, Flow from .features import InvestmentModel, PiecewiseModel @@ -528,6 +529,15 @@ def _plausibility_checks(self) -> None: f'{self.discharging.size.minimum_size=}, {self.discharging.size.maximum_size=}.' ) + def __repr__(self) -> str: + """Return string representation.""" + # Use build_repr_from_init directly to exclude charging and discharging + return fx_io.build_repr_from_init( + self, + excluded_params={'self', 'label', 'charging', 'discharging', 'kwargs'}, + skip_default_size=True, + ) + fx_io.format_flow_details(self) + @register_class_for_io class Transmission(Component): diff --git a/flixopt/effects.py b/flixopt/effects.py index 6225734fe..757549223 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -16,9 +16,10 @@ import numpy as np import xarray as xr +from . import io as fx_io from .core import PeriodicDataUser, Scalar, TemporalData, TemporalDataUser from .features import ShareAllocationModel -from .structure import Element, ElementModel, FlowSystemModel, Submodel, register_class_for_io +from .structure import Element, ElementContainer, ElementModel, FlowSystemModel, Submodel, register_class_for_io if TYPE_CHECKING: from collections.abc import Iterator @@ -448,13 +449,13 @@ def _do_modeling(self): EffectExpr = dict[str, linopy.LinearExpression] # Used to create Shares -class EffectCollection: +class EffectCollection(ElementContainer[Effect]): """ Handling all Effects """ def __init__(self, *effects: Effect): - self._effects = {} + super().__init__(element_type_name='effects') self._standard_effect: Effect | None = None self._objective_effect: Effect | None = None @@ -474,7 +475,7 @@ def add_effects(self, *effects: Effect) -> None: self.standard_effect = effect if effect.is_objective: self.objective_effect = effect - self._effects[effect.label] = effect + self.add(effect) # Use the inherited add() method from ElementContainer logger.info(f'Registered new Effect: {effect.label}') def create_effect_values_dict( @@ -520,10 +521,13 @@ def _plausibility_checks(self) -> None: # Check circular loops in effects: temporal, periodic = self.calculate_effect_share_factors() - # Validate all referenced sources exist - unknown = {src for src, _ in list(temporal.keys()) + list(periodic.keys()) if src not in self.effects} + # Validate all referenced effects (both sources and targets) exist + edges = list(temporal.keys()) + list(periodic.keys()) + unknown_sources = {src for src, _ in edges if src not in self} + unknown_targets = {tgt for _, tgt in edges if tgt not in self} + unknown = unknown_sources | unknown_targets if unknown: - raise KeyError(f'Unknown effects used in in effect share mappings: {sorted(unknown)}') + raise KeyError(f'Unknown effects used in effect share mappings: {sorted(unknown)}') temporal_cycles = detect_cycles(tuples_to_adjacency_list([key for key in temporal])) periodic_cycles = detect_cycles(tuples_to_adjacency_list([key for key in periodic])) @@ -552,31 +556,23 @@ def __getitem__(self, effect: str | Effect | None) -> Effect: else: raise KeyError(f'Effect {effect} not found!') try: - return self.effects[effect] + return super().__getitem__(effect) # Leverage ContainerMixin suggestions except KeyError as e: - raise KeyError(f'Effect "{effect}" not found! Add it to the FlowSystem first!') from e + # Extract the original message and append context for cleaner output + original_msg = str(e).strip('\'"') + raise KeyError(f'{original_msg} Add the effect to the FlowSystem first.') from None - def __iter__(self) -> Iterator[Effect]: - return iter(self._effects.values()) - - def __len__(self) -> int: - return len(self._effects) + def __iter__(self) -> Iterator[str]: + return iter(self.keys()) # Iterate over keys like a normal dict def __contains__(self, item: str | Effect) -> bool: """Check if the effect exists. Checks for label or object""" if isinstance(item, str): - return item in self.effects # Check if the label exists + return super().__contains__(item) # Check if the label exists elif isinstance(item, Effect): - if item.label_full in self.effects: - return True - if item in self.effects.values(): # Check if the object exists - return True + return item.label_full in self and self[item.label_full] is item return False - @property - def effects(self) -> dict[str, Effect]: - return self._effects - @property def standard_effect(self) -> Effect: if self._standard_effect is None: @@ -611,7 +607,7 @@ def calculate_effect_share_factors( dict[tuple[str, str], xr.DataArray], ]: shares_periodic = {} - for name, effect in self.effects.items(): + for name, effect in self.items(): if effect.share_from_periodic: for source, data in effect.share_from_periodic.items(): if source not in shares_periodic: @@ -620,7 +616,7 @@ def calculate_effect_share_factors( shares_periodic = calculate_all_conversion_paths(shares_periodic) shares_temporal = {} - for name, effect in self.effects.items(): + for name, effect in self.items(): if effect.share_from_temporal: for source, data in effect.share_from_temporal.items(): if source not in shares_temporal: @@ -670,7 +666,7 @@ def add_share_to_penalty(self, name: str, expression: linopy.LinearExpression) - def _do_modeling(self): super()._do_modeling() - for effect in self.effects: + for effect in self.effects.values(): effect.create_model(self._model) self.penalty = self.add_submodels( ShareAllocationModel(self._model, dims=(), label_of_element='Penalty'), @@ -684,7 +680,7 @@ def _do_modeling(self): ) def _add_share_between_effects(self): - for target_effect in self.effects: + for target_effect in self.effects.values(): # 1. temporal: <- receiving temporal shares from other effects for source_effect, time_series in target_effect.share_from_temporal.items(): target_effect.submodel.temporal.add_share( diff --git a/flixopt/elements.py b/flixopt/elements.py index a0fd306c0..2a9a2cf4f 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -11,6 +11,7 @@ import numpy as np import xarray as xr +from . import io as fx_io from .config import CONFIG from .core import PlausibilityError, Scalar, TemporalData, TemporalDataUser from .features import InvestmentModel, OnOffModel @@ -86,10 +87,12 @@ def __init__( super().__init__(label, meta_data=meta_data) self.inputs: list[Flow] = inputs or [] self.outputs: list[Flow] = outputs or [] - self._check_unique_flow_labels() self.on_off_parameters = on_off_parameters self.prevent_simultaneous_flows: list[Flow] = prevent_simultaneous_flows or [] + self._check_unique_flow_labels() + self._connect_flows() + self.flows: dict[str, Flow] = {flow.label: flow for flow in self.inputs + self.outputs} def create_model(self, model: FlowSystemModel) -> ComponentModel: @@ -115,6 +118,48 @@ def _check_unique_flow_labels(self): def _plausibility_checks(self) -> None: self._check_unique_flow_labels() + def _connect_flows(self): + # Inputs + for flow in self.inputs: + if flow.component not in ('UnknownComponent', self.label_full): + raise ValueError( + f'Flow "{flow.label}" already assigned to component "{flow.component}". ' + f'Cannot attach to "{self.label_full}".' + ) + flow.component = self.label_full + flow.is_input_in_component = True + # Outputs + for flow in self.outputs: + if flow.component not in ('UnknownComponent', self.label_full): + raise ValueError( + f'Flow "{flow.label}" already assigned to component "{flow.component}". ' + f'Cannot attach to "{self.label_full}".' + ) + flow.component = self.label_full + flow.is_input_in_component = False + + # Validate prevent_simultaneous_flows: only allow local flows + if self.prevent_simultaneous_flows: + # Deduplicate while preserving order + seen = set() + self.prevent_simultaneous_flows = [ + f for f in self.prevent_simultaneous_flows if id(f) not in seen and not seen.add(id(f)) + ] + local = set(self.inputs + self.outputs) + foreign = [f for f in self.prevent_simultaneous_flows if f not in local] + if foreign: + names = ', '.join(f.label_full for f in foreign) + raise ValueError( + f'prevent_simultaneous_flows for "{self.label_full}" must reference its own flows. ' + f'Foreign flows detected: {names}' + ) + + def __repr__(self) -> str: + """Return string representation with flow information.""" + return fx_io.build_repr_from_init( + self, excluded_params={'self', 'label', 'inputs', 'outputs', 'kwargs'}, skip_default_size=True + ) + fx_io.format_flow_details(self) + @register_class_for_io class Bus(Element): @@ -216,6 +261,10 @@ def _plausibility_checks(self) -> None: def with_excess(self) -> bool: return False if self.excess_penalty_per_flow_hour is None else True + def __repr__(self) -> str: + """Return string representation.""" + return super().__repr__() + fx_io.format_flow_details(self) + @register_class_for_io class Connection: @@ -493,6 +542,10 @@ def size_is_fixed(self) -> bool: # Wenn kein InvestParameters existiert --> True; Wenn Investparameter, den Wert davon nehmen return False if (isinstance(self.size, InvestParameters) and self.size.fixed_size is None) else True + def _format_invest_params(self, params: InvestParameters) -> str: + """Format InvestParameters for display.""" + return f'size: {params.format_for_repr()}' + class FlowModel(ElementModel): element: Flow # Type hint diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 9f29ea96e..cf958d9d1 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -6,12 +6,14 @@ import logging import warnings +from itertools import chain from typing import TYPE_CHECKING, Any, Literal, Optional import numpy as np import pandas as pd import xarray as xr +from . import io as fx_io from .config import CONFIG from .core import ( ConversionError, @@ -32,7 +34,7 @@ TemporalEffectsUser, ) from .elements import Bus, Component, Flow -from .structure import Element, FlowSystemModel, Interface +from .structure import CompositeContainerMixin, Element, ElementContainer, FlowSystemModel, Interface if TYPE_CHECKING: import pathlib @@ -43,11 +45,13 @@ logger = logging.getLogger('flixopt') -class FlowSystem(Interface): +class FlowSystem(Interface, CompositeContainerMixin[Element]): """ - A FlowSystem organizes the high level Elements (Components, Buses & Effects). + A FlowSystem organizes the high level Elements (Components, Buses, Effects & Flows). - This is the main container class that users work with to build and manage their System. + This is the main container class that users work with to build and manage their energy or material flow system. + FlowSystem provides both direct container access (via .components, .buses, .effects, .flows) and a unified + dict-like interface for accessing any element by label across all container types. Args: timesteps: The timesteps of the model. @@ -69,10 +73,74 @@ class FlowSystem(Interface): - False: All flow rates are optimized separately per scenario - list[str]: Only specified flows (by label_full) are equalized across scenarios + Examples: + Creating a FlowSystem and accessing elements: + + >>> import flixopt as fx + >>> import pandas as pd + >>> timesteps = pd.date_range('2023-01-01', periods=24, freq='h') + >>> flow_system = fx.FlowSystem(timesteps) + >>> + >>> # Add elements to the system + >>> boiler = fx.Component('Boiler', inputs=[heat_flow], on_off_parameters=...) + >>> heat_bus = fx.Bus('Heat', excess_penalty_per_flow_hour=1e4) + >>> costs = fx.Effect('costs', is_objective=True, is_standard=True) + >>> flow_system.add_elements(boiler, heat_bus, costs) + + Unified dict-like access (recommended for most cases): + + >>> # Access any element by label, regardless of type + >>> boiler = flow_system['Boiler'] # Returns Component + >>> heat_bus = flow_system['Heat'] # Returns Bus + >>> costs = flow_system['costs'] # Returns Effect + >>> + >>> # Check if element exists + >>> if 'Boiler' in flow_system: + ... print('Boiler found in system') + >>> + >>> # Iterate over all elements + >>> for label in flow_system.keys(): + ... element = flow_system[label] + ... print(f'{label}: {type(element).__name__}') + >>> + >>> # Get all element labels and objects + >>> all_labels = list(flow_system.keys()) + >>> all_elements = list(flow_system.values()) + >>> for label, element in flow_system.items(): + ... print(f'{label}: {element}') + + Direct container access for type-specific operations: + + >>> # Access specific container when you need type filtering + >>> for component in flow_system.components.values(): + ... print(f'{component.label}: {len(component.inputs)} inputs') + >>> + >>> # Access buses directly + >>> for bus in flow_system.buses.values(): + ... print(f'{bus.label}') + >>> + >>> # Flows are automatically collected from all components + >>> for flow in flow_system.flows.values(): + ... print(f'{flow.label_full}: {flow.size}') + >>> + >>> # Access effects + >>> for effect in flow_system.effects.values(): + ... print(f'{effect.label}') + Notes: + - The dict-like interface (`flow_system['element']`) searches across all containers + (components, buses, effects, flows) to find the element with the matching label. + - Element labels must be unique across all container types. Attempting to add + elements with duplicate labels will raise an error, ensuring each label maps to exactly one element. + - The `.all_elements` property is deprecated. Use the dict-like interface instead: + `flow_system['element']`, `'element' in flow_system`, `flow_system.keys()`, + `flow_system.values()`, or `flow_system.items()`. + - Direct container access (`.components`, `.buses`, `.effects`, `.flows`) is useful + when you need type-specific filtering or operations. + - The `.flows` container is automatically populated from all component inputs and outputs. - Creates an empty registry for components and buses, an empty EffectCollection, and a placeholder for a SystemModel. - The instance starts disconnected (self._connected_and_transformed == False) and will be - connected_and_transformed automatically when trying to solve a calculation. + connected_and_transformed automatically when trying to solve a calculation. """ def __init__( @@ -104,8 +172,8 @@ def __init__( self.hours_per_timestep = self.fit_to_model_coords('hours_per_timestep', hours_per_timestep) # Element collections - self.components: dict[str, Component] = {} - self.buses: dict[str, Bus] = {} + self.components: ElementContainer[Component] = ElementContainer(element_type_name='components') + self.buses: ElementContainer[Bus] = ElementContainer(element_type_name='buses') self.effects: EffectCollection = EffectCollection() self.model: FlowSystemModel | None = None @@ -113,6 +181,7 @@ def __init__( self._used_in_calculation = False self._network_app = None + self._flows_cache: ElementContainer[Flow] | None = None # Use properties to validate and store scenario dimension settings self.scenario_independent_sizes = scenario_independent_sizes @@ -232,7 +301,7 @@ def _create_reference_structure(self) -> tuple[dict, dict[str, xr.DataArray]]: # Extract from effects effects_structure = {} - for effect in self.effects: + for effect in self.effects.values(): effect_structure, effect_arrays = effect._create_reference_structure() all_extracted_arrays.update(effect_arrays) effects_structure[effect.label] = effect_structure @@ -433,7 +502,7 @@ def connect_and_transform(self): self.weights = self.fit_to_model_coords('weights', self.weights, dims=['period', 'scenario']) self._connect_network() - for element in list(self.components.values()) + list(self.effects.effects.values()) + list(self.buses.values()): + for element in chain(self.components.values(), self.effects.values(), self.buses.values()): element.transform_data(self) self._connected_and_transformed = True @@ -581,7 +650,7 @@ def network_infos(self) -> tuple[dict[str, dict[str, str]], dict[str, dict[str, 'class': 'Bus' if isinstance(node, Bus) else 'Component', 'infos': node.__str__(), } - for node in list(self.components.values()) + list(self.buses.values()) + for node in chain(self.components.values(), self.buses.values()) } edges = { @@ -603,10 +672,8 @@ def _check_if_element_is_unique(self, element: Element) -> None: Args: element: new element to check """ - if element in self.all_elements.values(): - raise ValueError(f'Element {element.label_full} already added to FlowSystem!') # check if name is already used: - if element.label_full in self.all_elements: + if element.label_full in self: raise ValueError(f'Label of Element {element.label_full} already used in another element!') def _add_effects(self, *args: Effect) -> None: @@ -616,13 +683,15 @@ def _add_components(self, *components: Component) -> None: for new_component in list(components): logger.info(f'Registered new Component: {new_component.label_full}') self._check_if_element_is_unique(new_component) # check if already exists: - self.components[new_component.label_full] = new_component # Add to existing components + self.components.add(new_component) # Add to existing components + self._flows_cache = None # Invalidate flows cache def _add_buses(self, *buses: Bus): for new_bus in list(buses): logger.info(f'Registered new Bus: {new_bus.label_full}') self._check_if_element_is_unique(new_bus) # check if already exists: - self.buses[new_bus.label_full] = new_bus # Add to existing components + self.buses.add(new_bus) # Add to existing buses + self._flows_cache = None # Invalidate flows cache def _connect_network(self): """Connects the network of components and buses. Can be rerun without changes if no elements were added""" @@ -632,7 +701,7 @@ def _connect_network(self): flow.is_input_in_component = True if flow in component.inputs else False # Add Bus if not already added (deprecated) - if flow._bus_object is not None and flow._bus_object not in self.buses.values(): + if flow._bus_object is not None and flow._bus_object.label_full not in self.buses: warnings.warn( f'The Bus {flow._bus_object.label_full} was added to the FlowSystem from {flow.label_full}.' f'This is deprecated and will be removed in the future. ' @@ -659,62 +728,40 @@ def _connect_network(self): ) def __repr__(self) -> str: - """Compact representation for debugging.""" - status = '✓' if self.connected_and_transformed else '⚠' - - # Build dimension info - dims = f'{len(self.timesteps)} timesteps [{self.timesteps[0].strftime("%Y-%m-%d")} to {self.timesteps[-1].strftime("%Y-%m-%d")}]' - if self.periods is not None: - dims += f', {len(self.periods)} periods' - if self.scenarios is not None: - dims += f', {len(self.scenarios)} scenarios' - - return f'FlowSystem({dims}, {len(self.components)} Components, {len(self.buses)} Buses, {len(self.effects)} Effects, {status})' - - def __str__(self) -> str: - """Structured summary for users.""" - - def format_elements(element_names: list, label: str, alignment: int = 12): - name_list = ', '.join(element_names[:3]) - if len(element_names) > 3: - name_list += f' ... (+{len(element_names) - 3} more)' + """Return a detailed string representation showing all containers.""" + r = fx_io.format_title_with_underline('FlowSystem', '=') - suffix = f' ({name_list})' if element_names else '' - padding = alignment - len(label) - 1 # -1 for the colon - return f'{label}:{"":<{padding}} {len(element_names)}{suffix}' - - time_period = f'Time period: {self.timesteps[0].date()} to {self.timesteps[-1].date()}' + # Timestep info + time_period = f'{self.timesteps[0].date()} to {self.timesteps[-1].date()}' freq_str = str(self.timesteps.freq).replace('<', '').replace('>', '') if self.timesteps.freq else 'irregular' - - lines = [ - f'Timesteps: {len(self.timesteps)} ({freq_str}) [{time_period}]', - ] + r += f'Timesteps: {len(self.timesteps)} ({freq_str}) [{time_period}]\n' # Add periods if present if self.periods is not None: period_names = ', '.join(str(p) for p in self.periods[:3]) if len(self.periods) > 3: period_names += f' ... (+{len(self.periods) - 3} more)' - lines.append(f'Periods: {len(self.periods)} ({period_names})') + r += f'Periods: {len(self.periods)} ({period_names})\n' + else: + r += 'Periods: None\n' # Add scenarios if present if self.scenarios is not None: scenario_names = ', '.join(str(s) for s in self.scenarios[:3]) if len(self.scenarios) > 3: scenario_names += f' ... (+{len(self.scenarios) - 3} more)' - lines.append(f'Scenarios: {len(self.scenarios)} ({scenario_names})') - - lines.extend( - [ - format_elements(list(self.components.keys()), 'Components'), - format_elements(list(self.buses.keys()), 'Buses'), - format_elements(list(self.effects.effects.keys()), 'Effects'), - f'Status: {"Connected & Transformed" if self.connected_and_transformed else "Not connected"}', - ] - ) - lines = ['FlowSystem:', f'{"─" * max(len(line) for line in lines)}'] + lines + r += f'Scenarios: {len(self.scenarios)} ({scenario_names})\n' + else: + r += 'Scenarios: None\n' + + # Add status + status = '✓' if self.connected_and_transformed else '⚠' + r += f'Status: {status}\n' + + # Add grouped container view + r += '\n' + self._format_grouped_containers() - return '\n'.join(lines) + return r def __eq__(self, other: FlowSystem): """Check if two FlowSystems are equal by comparing their dataset representations.""" @@ -734,38 +781,46 @@ def __eq__(self, other: FlowSystem): return True - def __getitem__(self, item) -> Element: - """Get element by exact label with helpful error messages.""" - if item in self.all_elements: - return self.all_elements[item] - - # Provide helpful error with suggestions - from difflib import get_close_matches - - suggestions = get_close_matches(item, self.all_elements.keys(), n=3, cutoff=0.6) - - if suggestions: - suggestion_str = ', '.join(f"'{s}'" for s in suggestions) - raise KeyError(f"Element '{item}' not found. Did you mean: {suggestion_str}?") - else: - raise KeyError(f"Element '{item}' not found in FlowSystem") - - def __contains__(self, item: str) -> bool: - """Check if element exists in the FlowSystem.""" - return item in self.all_elements - - def __iter__(self): - """Iterate over element labels.""" - return iter(self.all_elements.keys()) + def _get_container_groups(self) -> dict[str, ElementContainer]: + """Return ordered container groups for CompositeContainerMixin.""" + return { + 'Components': self.components, + 'Buses': self.buses, + 'Effects': self.effects, + 'Flows': self.flows, + } @property - def flows(self) -> dict[str, Flow]: - set_of_flows = {flow for comp in self.components.values() for flow in comp.inputs + comp.outputs} - return {flow.label_full: flow for flow in set_of_flows} + def flows(self) -> ElementContainer[Flow]: + if self._flows_cache is None: + flows = [f for c in self.components.values() for f in c.inputs + c.outputs] + # Deduplicate by id and sort for reproducibility + flows = sorted({id(f): f for f in flows}.values(), key=lambda f: f.label_full.lower()) + self._flows_cache = ElementContainer(flows, element_type_name='flows') + return self._flows_cache @property def all_elements(self) -> dict[str, Element]: - return {**self.components, **self.effects.effects, **self.flows, **self.buses} + """ + Get all elements as a dictionary. + + .. deprecated:: 3.2.0 + Use dict-like interface instead: `flow_system['element']`, `'element' in flow_system`, + `flow_system.keys()`, `flow_system.values()`, or `flow_system.items()`. + This property will be removed in v4.0.0. + + Returns: + Dictionary mapping element labels to element objects. + """ + warnings.warn( + "The 'all_elements' property is deprecated. Use dict-like interface instead: " + "flow_system['element'], 'element' in flow_system, flow_system.keys(), " + 'flow_system.values(), or flow_system.items(). ' + 'This property will be removed in v4.0.0.', + DeprecationWarning, + stacklevel=2, + ) + return {**self.components, **self.effects, **self.flows, **self.buses} @property def coords(self) -> dict[FlowSystemDimensions, pd.Index]: diff --git a/flixopt/interface.py b/flixopt/interface.py index 8264e2392..eae0a8511 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -1051,6 +1051,27 @@ def minimum_or_fixed_size(self) -> PeriodicData: def maximum_or_fixed_size(self) -> PeriodicData: return self.fixed_size if self.fixed_size is not None else self.maximum_size + def format_for_repr(self) -> str: + """Format InvestParameters for display in repr methods. + + Returns: + Formatted string showing size information + """ + from .io import numeric_to_str_for_repr + + if self.fixed_size is not None: + val = numeric_to_str_for_repr(self.fixed_size) + status = 'mandatory' if self.mandatory else 'optional' + return f'{val} ({status})' + + # Show range if available + parts = [] + if self.minimum_size is not None: + parts.append(f'min: {numeric_to_str_for_repr(self.minimum_size)}') + if self.maximum_size is not None: + parts.append(f'max: {numeric_to_str_for_repr(self.maximum_size)}') + return ', '.join(parts) if parts else 'invest' + @staticmethod def compute_linked_periods(first_period: int, last_period: int, periods: pd.Index | list[int]) -> xr.DataArray: return xr.DataArray( diff --git a/flixopt/io.py b/flixopt/io.py index 059670ddd..8df03401c 100644 --- a/flixopt/io.py +++ b/flixopt/io.py @@ -1,5 +1,6 @@ from __future__ import annotations +import inspect import json import logging import pathlib @@ -8,6 +9,7 @@ from typing import TYPE_CHECKING, Any import numpy as np +import pandas as pd import xarray as xr import yaml @@ -547,3 +549,348 @@ def update(self, new_name: str | None = None, new_folder: pathlib.Path | None = raise FileNotFoundError(f'Folder {new_folder} does not exist or is not a directory.') self.folder = new_folder self._update_paths() + + +def numeric_to_str_for_repr( + value: int | float | np.integer | np.floating | np.ndarray | pd.Series | pd.DataFrame | xr.DataArray, + precision: int = 1, + atol: float = 1e-10, +) -> str: + """Format value for display in repr methods. + + For single values or uniform arrays, returns the formatted value. + For arrays with variation, returns a range showing min-max. + + Args: + value: Numeric value or container (DataArray, array, Series, DataFrame) + precision: Number of decimal places (default: 1) + atol: Absolute tolerance for considering values equal (default: 1e-10) + + Returns: + Formatted string representation: + - Single/uniform values: "100.0" + - Nearly uniform values: "~100.0" (values differ slightly but display similarly) + - Varying values: "50.0-150.0" (shows range from min to max) + + Raises: + TypeError: If value cannot be converted to numeric format + """ + # Handle simple scalar types + if isinstance(value, (int, float, np.integer, np.floating)): + return f'{float(value):.{precision}f}' + + # Extract array data for variation checking + arr = None + if isinstance(value, xr.DataArray): + arr = value.values.flatten() + elif isinstance(value, (np.ndarray, pd.Series)): + arr = np.asarray(value).flatten() + elif isinstance(value, pd.DataFrame): + arr = value.values.flatten() + else: + # Fallback for unknown types + try: + return f'{float(value):.{precision}f}' + except (TypeError, ValueError) as e: + raise TypeError(f'Cannot format value of type {type(value).__name__} for repr') from e + + # Normalize dtype and handle empties + arr = arr.astype(float, copy=False) + if arr.size == 0: + return '?' + + # Filter non-finite values + finite = arr[np.isfinite(arr)] + if finite.size == 0: + return 'nan' + + # Check for single value + if finite.size == 1: + return f'{float(finite[0]):.{precision}f}' + + # Check if all values are the same or very close + min_val = float(np.nanmin(finite)) + max_val = float(np.nanmax(finite)) + + # First check: values are essentially identical + if np.allclose(min_val, max_val, atol=atol): + return f'{float(np.mean(finite)):.{precision}f}' + + # Second check: display values are the same but actual values differ slightly + min_str = f'{min_val:.{precision}f}' + max_str = f'{max_val:.{precision}f}' + if min_str == max_str: + return f'~{min_str}' + + # Values vary significantly - show range + return f'{min_str}-{max_str}' + + +def _format_value_for_repr(value) -> str: + """Format a single value for display in repr. + + Args: + value: The value to format + + Returns: + Formatted string representation of the value + """ + # Format numeric types using specialized formatter + if isinstance(value, (int, float, np.integer, np.floating, np.ndarray, pd.Series, pd.DataFrame, xr.DataArray)): + try: + return numeric_to_str_for_repr(value) + except Exception: + value_repr = repr(value) + if len(value_repr) > 50: + value_repr = value_repr[:47] + '...' + return value_repr + + # Format dicts with numeric/array values nicely + elif isinstance(value, dict): + try: + formatted_items = [] + for k, v in value.items(): + if isinstance( + v, (int, float, np.integer, np.floating, np.ndarray, pd.Series, pd.DataFrame, xr.DataArray) + ): + v_str = numeric_to_str_for_repr(v) + else: + v_str = repr(v) + if len(v_str) > 30: + v_str = v_str[:27] + '...' + formatted_items.append(f'{repr(k)}: {v_str}') + value_repr = '{' + ', '.join(formatted_items) + '}' + if len(value_repr) > 50: + value_repr = value_repr[:47] + '...' + return value_repr + except Exception: + value_repr = repr(value) + if len(value_repr) > 50: + value_repr = value_repr[:47] + '...' + return value_repr + + # Default repr with truncation + else: + value_repr = repr(value) + if len(value_repr) > 50: + value_repr = value_repr[:47] + '...' + return value_repr + + +def build_repr_from_init( + obj: object, + excluded_params: set[str] | None = None, + label_as_positional: bool = True, + skip_default_size: bool = False, +) -> str: + """Build a repr string from __init__ signature, showing non-default parameter values. + + This utility function extracts common repr logic used across flixopt classes. + It introspects the __init__ method to build a constructor-style repr showing + only parameters that differ from their defaults. + + Args: + obj: The object to create repr for + excluded_params: Set of parameter names to exclude (e.g., {'self', 'inputs', 'outputs'}) + Default excludes 'self', 'label', and 'kwargs' + label_as_positional: If True and 'label' param exists, show it as first positional arg + skip_default_size: If True, skip 'size' parameter when it equals CONFIG.Modeling.big + + Returns: + Formatted repr string like: ClassName("label", param=value) + """ + if excluded_params is None: + excluded_params = {'self', 'label', 'kwargs'} + else: + # Always exclude 'self' + excluded_params = excluded_params | {'self'} + + try: + # Get the constructor arguments and their current values + init_signature = inspect.signature(obj.__init__) + init_params = init_signature.parameters + + # Check if this has a 'label' parameter - if so, show it first as positional + has_label = 'label' in init_params and label_as_positional + + # Build kwargs for non-default parameters + kwargs_parts = [] + label_value = None + + for param_name, param in init_params.items(): + # Skip *args and **kwargs + if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD): + continue + + # Handle label separately if showing as positional (check BEFORE excluded_params) + if param_name == 'label' and has_label: + label_value = getattr(obj, param_name, None) + continue + + # Now check if parameter should be excluded + if param_name in excluded_params: + continue + + # Get current value + value = getattr(obj, param_name, None) + + # Skip if value matches default + if param.default != inspect.Parameter.empty: + # Special handling for empty containers (even if default was None) + if isinstance(value, (dict, list, tuple, set)) and len(value) == 0: + if param.default is None or ( + isinstance(param.default, (dict, list, tuple, set)) and len(param.default) == 0 + ): + continue + + # Handle array comparisons (xarray, numpy) + elif isinstance(value, (xr.DataArray, np.ndarray)): + try: + if isinstance(param.default, (xr.DataArray, np.ndarray)): + # Compare arrays element-wise + if isinstance(value, xr.DataArray) and isinstance(param.default, xr.DataArray): + if value.equals(param.default): + continue + elif np.array_equal(value, param.default): + continue + elif isinstance(param.default, (int, float, np.integer, np.floating)): + # Compare array to scalar (e.g., after transform_data converts scalar to DataArray) + if isinstance(value, xr.DataArray): + if np.all(value.values == float(param.default)): + continue + elif isinstance(value, np.ndarray): + if np.all(value == float(param.default)): + continue + except Exception: + pass # If comparison fails, include in repr + + # Handle numeric comparisons (deals with 0 vs 0.0, int vs float) + elif isinstance(value, (int, float, np.integer, np.floating)) and isinstance( + param.default, (int, float, np.integer, np.floating) + ): + try: + if float(value) == float(param.default): + continue + except (ValueError, TypeError): + pass + + elif value == param.default: + continue + + # Skip None values if default is None + if value is None and param.default is None: + continue + + # Special case: hide CONFIG.Modeling.big for size parameter + if skip_default_size and param_name == 'size': + from .config import CONFIG + + try: + if isinstance(value, (int, float, np.integer, np.floating)): + if float(value) == CONFIG.Modeling.big: + continue + except Exception: + pass + + # Format value using helper function + value_repr = _format_value_for_repr(value) + kwargs_parts.append(f'{param_name}={value_repr}') + + # Build args string with label first as positional if present + if has_label and label_value is not None: + # Use label_full if available, otherwise label + if hasattr(obj, 'label_full'): + label_repr = repr(obj.label_full) + else: + label_repr = repr(label_value) + + if len(label_repr) > 50: + label_repr = label_repr[:47] + '...' + args_str = label_repr + if kwargs_parts: + args_str += ', ' + ', '.join(kwargs_parts) + else: + args_str = ', '.join(kwargs_parts) + + # Build final repr + class_name = obj.__class__.__name__ + + return f'{class_name}({args_str})' + + except Exception: + # Fallback if introspection fails + return f'{obj.__class__.__name__}()' + + +def format_flow_details(obj, has_inputs: bool = True, has_outputs: bool = True) -> str: + """Format inputs and outputs as indented bullet list. + + Args: + obj: Object with 'inputs' and/or 'outputs' attributes + has_inputs: Whether to check for inputs + has_outputs: Whether to check for outputs + + Returns: + Formatted string with flow details (including leading newline), or empty string if no flows + """ + flow_lines = [] + + if has_inputs and hasattr(obj, 'inputs') and obj.inputs: + flow_lines.append(' inputs:') + for flow in obj.inputs: + flow_lines.append(f' * {repr(flow)}') + + if has_outputs and hasattr(obj, 'outputs') and obj.outputs: + flow_lines.append(' outputs:') + for flow in obj.outputs: + flow_lines.append(f' * {repr(flow)}') + + return '\n' + '\n'.join(flow_lines) if flow_lines else '' + + +def format_title_with_underline(title: str, underline_char: str = '-') -> str: + """Format a title with underline of matching length. + + Args: + title: The title text + underline_char: Character to use for underline (default: '-') + + Returns: + Formatted string: "Title\\n-----\\n" + """ + return f'{title}\n{underline_char * len(title)}\n' + + +def format_sections_with_headers(sections: dict[str, str], underline_char: str = '-') -> list[str]: + """Format sections with underlined headers. + + Args: + sections: Dict mapping section headers to content + underline_char: Character for underlining headers + + Returns: + List of formatted section strings + """ + formatted_sections = [] + for section_header, section_content in sections.items(): + underline = underline_char * len(section_header) + formatted_sections.append(f'{section_header}\n{underline}\n{section_content}') + return formatted_sections + + +def build_metadata_info(parts: list[str], prefix: str = ' | ') -> str: + """Build metadata info string from parts. + + Args: + parts: List of metadata strings (empty strings are filtered out) + prefix: Prefix to add if parts is non-empty + + Returns: + Formatted info string or empty string + """ + # Filter out empty strings + parts = [p for p in parts if p] + if not parts: + return '' + info = ' | '.join(parts) + return prefix + info if prefix else info diff --git a/flixopt/results.py b/flixopt/results.py index 950570df3..9232f0175 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -17,6 +17,7 @@ from .color_processing import process_colors from .config import CONFIG from .flow_system import FlowSystem +from .structure import CompositeContainerMixin, ElementContainer, ResultsContainer if TYPE_CHECKING: import matplotlib.pyplot as plt @@ -53,7 +54,7 @@ class _FlowSystemRestorationError(Exception): pass -class CalculationResults: +class CalculationResults(CompositeContainerMixin['ComponentResults | BusResults | EffectResults | FlowResults']): """Comprehensive container for optimization calculation results and analysis tools. This class provides unified access to all optimization results including flow rates, @@ -238,13 +239,18 @@ def __init__( self.name = name self.model = model self.folder = pathlib.Path(folder) if folder is not None else pathlib.Path.cwd() / 'results' - self.components = { + + # Create ResultsContainers for better access patterns + components_dict = { label: ComponentResults(self, **infos) for label, infos in self.solution.attrs['Components'].items() } + self.components = ResultsContainer(elements=components_dict, element_type_name='component results') - self.buses = {label: BusResults(self, **infos) for label, infos in self.solution.attrs['Buses'].items()} + buses_dict = {label: BusResults(self, **infos) for label, infos in self.solution.attrs['Buses'].items()} + self.buses = ResultsContainer(elements=buses_dict, element_type_name='bus results') - self.effects = {label: EffectResults(self, **infos) for label, infos in self.solution.attrs['Effects'].items()} + effects_dict = {label: EffectResults(self, **infos) for label, infos in self.solution.attrs['Effects'].items()} + self.effects = ResultsContainer(elements=effects_dict, element_type_name='effect results') if 'Flows' not in self.solution.attrs: warnings.warn( @@ -252,11 +258,14 @@ def __init__( 'is not availlable. We recommend to evaluate your results with a version <2.2.0.', stacklevel=2, ) - self.flows = {} + flows_dict = {} + self._has_flow_data = False else: - self.flows = { + flows_dict = { label: FlowResults(self, **infos) for label, infos in self.solution.attrs.get('Flows', {}).items() } + self._has_flow_data = True + self.flows = ResultsContainer(elements=flows_dict, element_type_name='flow results') self.timesteps_extra = self.solution.indexes['time'] self.hours_per_timestep = FlowSystem.calculate_hours_per_timestep(self.timesteps_extra) @@ -273,16 +282,22 @@ def __init__( self.colors: dict[str, str] = {} - def __getitem__(self, key: str) -> ComponentResults | BusResults | EffectResults: - if key in self.components: - return self.components[key] - if key in self.buses: - return self.buses[key] - if key in self.effects: - return self.effects[key] - if key in self.flows: - return self.flows[key] - raise KeyError(f'No element with label {key} found.') + def _get_container_groups(self) -> dict[str, ResultsContainer]: + """Return ordered container groups for CompositeContainerMixin.""" + return { + 'Components': self.components, + 'Buses': self.buses, + 'Effects': self.effects, + 'Flows': self.flows, + } + + def __repr__(self) -> str: + """Return grouped representation of all results.""" + r = fx_io.format_title_with_underline(self.__class__.__name__, '=') + r += f'Name: "{self.name}"\nFolder: {self.folder}\n' + # Add grouped container view + r += '\n' + self._format_grouped_containers() + return r @property def storages(self) -> list[ComponentResults]: @@ -547,6 +562,8 @@ def flow_rates( To recombine filtered dataarrays, use `xr.concat` with dim 'flow': >>>xr.concat([results.flow_rates(start='Fernwärme'), results.flow_rates(end='Fernwärme')], dim='flow') """ + if not self._has_flow_data: + raise ValueError('Flow data is not available in this results object (pre-v2.2.0).') if self._flow_rates is None: self._flow_rates = self._assign_flow_coords( xr.concat( @@ -608,6 +625,8 @@ def sizes( >>>xr.concat([results.sizes(start='Fernwärme'), results.sizes(end='Fernwärme')], dim='flow') """ + if not self._has_flow_data: + raise ValueError('Flow data is not available in this results object (pre-v2.2.0).') if self._sizes is None: self._sizes = self._assign_flow_coords( xr.concat( @@ -620,11 +639,12 @@ def sizes( def _assign_flow_coords(self, da: xr.DataArray): # Add start and end coordinates + flows_list = list(self.flows.values()) da = da.assign_coords( { - 'start': ('flow', [flow.start for flow in self.flows.values()]), - 'end': ('flow', [flow.end for flow in self.flows.values()]), - 'component': ('flow', [flow.component for flow in self.flows.values()]), + 'start': ('flow', [flow.start for flow in flows_list]), + 'end': ('flow', [flow.end for flow in flows_list]), + 'component': ('flow', [flow.component for flow in flows_list]), } ) @@ -743,8 +763,6 @@ def _compute_effect_total( temporal = temporal.sum('time') if periodic.isnull().all(): return temporal.rename(f'{element}->{effect}') - if 'time' in temporal.indexes: - temporal = temporal.sum('time') return periodic + temporal total = xr.DataArray(0) @@ -1106,6 +1124,14 @@ def constraints(self) -> linopy.Constraints: raise ValueError('The linopy model is not available.') return self._calculation_results.model.constraints[self._constraint_names] + def __repr__(self) -> str: + """Return string representation with element info and dataset preview.""" + class_name = self.__class__.__name__ + header = f'{class_name}: "{self.label}"' + sol = self.solution.copy(deep=False) + sol.attrs = {} + return f'{header}\n{"-" * len(header)}\n{repr(sol)}' + def filter_solution( self, variable_dims: Literal['scalar', 'time', 'scenario', 'timeonly', 'scenarioonly'] | None = None, diff --git a/flixopt/structure.py b/flixopt/structure.py index 6ea618454..065769cd2 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -7,12 +7,16 @@ import inspect import logging +import re from dataclasses import dataclass +from difflib import get_close_matches from io import StringIO from typing import ( TYPE_CHECKING, Any, + Generic, Literal, + TypeVar, ) import linopy @@ -168,7 +172,7 @@ def solution(self): }, 'Effects': { effect.label_full: effect.submodel.results_structure() - for effect in sorted(self.flow_system.effects, key=lambda effect: effect.label_full.upper()) + for effect in sorted(self.flow_system.effects.values(), key=lambda effect: effect.label_full.upper()) }, 'Flows': { flow.label_full: flow.submodel.results_structure() @@ -242,9 +246,7 @@ def __repr__(self) -> str: } # Format sections with headers and underlines - formatted_sections = [] - for section_header, section_content in sections.items(): - formatted_sections.append(f'{section_header}\n{"-" * len(section_header)}\n{section_content}') + formatted_sections = fx_io.format_sections_with_headers(sections) title = f'FlowSystemModel ({self.type})' all_sections = '\n'.join(formatted_sections) @@ -793,40 +795,7 @@ def to_json(self, path: str | pathlib.Path): def __repr__(self): """Return a detailed string representation for debugging.""" - try: - # Get the constructor arguments and their current values - init_signature = inspect.signature(self.__init__) - init_args = init_signature.parameters - - # Create a dictionary with argument names and their values, with better formatting - args_parts = [] - for name in init_args: - if name == 'self': - continue - value = getattr(self, name, None) - # Truncate long representations - value_repr = repr(value) - if len(value_repr) > 50: - value_repr = value_repr[:47] + '...' - args_parts.append(f'{name}={value_repr}') - - args_str = ', '.join(args_parts) - return f'{self.__class__.__name__}({args_str})' - except Exception: - # Fallback if introspection fails - return f'{self.__class__.__name__}()' - - def __str__(self): - """Return a user-friendly string representation.""" - try: - data = self.get_structure(clean=True, stats=True) - with StringIO() as output_buffer: - console = Console(file=output_buffer, width=1000) # Adjust width as needed - console.print(Pretty(data, expand_all=True, indent_guides=True)) - return output_buffer.getvalue() - except Exception: - # Fallback if structure generation fails - return f'{self.__class__.__name__} instance' + return fx_io.build_repr_from_init(self, excluded_params={'self', 'label', 'kwargs'}) def copy(self) -> Interface: """ @@ -876,6 +845,10 @@ def create_model(self, model: FlowSystemModel) -> ElementModel: def label_full(self) -> str: return self.label + def __repr__(self) -> str: + """Return string representation.""" + return fx_io.build_repr_from_init(self, excluded_params={'self', 'label', 'kwargs'}, skip_default_size=True) + @staticmethod def _valid_label(label: str) -> str: """Checks if the label is valid. If not, it is replaced by the default label. @@ -895,6 +868,329 @@ def _valid_label(label: str) -> str: return label +# Precompiled regex pattern for natural sorting +_NATURAL_SPLIT = re.compile(r'(\d+)') + + +def _natural_sort_key(text): + """Sort key for natural ordering (e.g., bus1, bus2, bus10 instead of bus1, bus10, bus2).""" + return [int(c) if c.isdigit() else c.lower() for c in _NATURAL_SPLIT.split(text)] + + +# Type variable for containers +T = TypeVar('T') + + +class ContainerMixin(dict[str, T]): + """ + Mixin providing shared container functionality with nice repr and error messages. + + Subclasses must implement _get_label() to extract the label from elements. + """ + + def __init__( + self, + elements: list[T] | dict[str, T] | None = None, + element_type_name: str = 'elements', + ): + """ + Args: + elements: Initial elements to add (list or dict) + element_type_name: Name for display (e.g., 'components', 'buses') + """ + super().__init__() + self._element_type_name = element_type_name + + if elements is not None: + if isinstance(elements, dict): + for element in elements.values(): + self.add(element) + else: + for element in elements: + self.add(element) + + def _get_label(self, element: T) -> str: + """ + Extract label from element. Must be implemented by subclasses. + + Args: + element: Element to get label from + + Returns: + Label string + """ + raise NotImplementedError('Subclasses must implement _get_label()') + + def add(self, element: T) -> None: + """Add an element to the container.""" + label = self._get_label(element) + if label in self: + raise ValueError( + f'Element with label "{label}" already exists in {self._element_type_name}. ' + f'Each element must have a unique label.' + ) + self[label] = element + + def __setitem__(self, label: str, element: T) -> None: + """Set element with validation.""" + element_label = self._get_label(element) + if label != element_label: + raise ValueError( + f'Key "{label}" does not match element label "{element_label}". ' + f'Use the correct label as key or use .add() method.' + ) + super().__setitem__(label, element) + + def __getitem__(self, label: str) -> T: + """ + Get element by label with helpful error messages. + + Args: + label: Label of the element to retrieve + + Returns: + The element with the given label + + Raises: + KeyError: If element is not found, with suggestions for similar labels + """ + try: + return super().__getitem__(label) + except KeyError: + # Provide helpful error with close matches suggestions + suggestions = get_close_matches(label, self.keys(), n=3, cutoff=0.6) + error_msg = f'Element "{label}" not found in {self._element_type_name}.' + if suggestions: + error_msg += f' Did you mean: {", ".join(suggestions)}?' + else: + available = list(self.keys()) + if len(available) <= 5: + error_msg += f' Available: {", ".join(available)}' + else: + error_msg += f' Available: {", ".join(available[:5])} ... (+{len(available) - 5} more)' + raise KeyError(error_msg) from None + + def __repr__(self) -> str: + """Return a string representation similar to linopy.model.Variables.""" + count = len(self) + title = f'{self._element_type_name.capitalize()} ({count} item{"s" if count != 1 else ""})' + + if not self: + r = fx_io.format_title_with_underline(title) + r += '\n' + else: + r = fx_io.format_title_with_underline(title) + for name in sorted(self.keys(), key=_natural_sort_key): + r += f' * {name}\n' + + return r + + +class ElementContainer(ContainerMixin[T]): + """ + Container for Element objects (Component, Bus, Flow, Effect). + + Uses element.label_full for keying. + """ + + def _get_label(self, element: T) -> str: + """Extract label_full from Element.""" + return element.label_full + + +class ResultsContainer(ContainerMixin[T]): + """ + Container for Results objects (ComponentResults, BusResults, etc). + + Uses element.label for keying. + """ + + def _get_label(self, element: T) -> str: + """Extract label from Results object.""" + return element.label + + +T_element = TypeVar('T_element') + + +class CompositeContainerMixin(Generic[T_element]): + """ + Mixin providing unified dict-like access across multiple typed containers. + + This mixin enables classes that manage multiple containers (e.g., components, + buses, effects, flows) to provide a unified interface for accessing elements + across all containers, as if they were a single collection. + + Type Parameter: + T_element: The type of elements stored in the containers. Can be a union type + for containers holding multiple types (e.g., 'ComponentResults | BusResults'). + + Key Features: + - Dict-like access: `obj['element_name']` searches all containers + - Iteration: `for label in obj:` iterates over all elements + - Membership: `'element' in obj` checks across all containers + - Standard dict methods: keys(), values(), items() + - Grouped display: Formatted repr showing elements by type + - Type hints: Full IDE and type checker support + + Subclasses must implement: + _get_container_groups() -> dict[str, dict]: + Returns a dictionary mapping group names (e.g., 'Components', 'Buses') + to container dictionaries. Containers are displayed in the order returned. + + Example: + ```python + class MySystem(CompositeContainerMixin[Component | Bus]): + def __init__(self): + self.components = {'Boiler': Component(...), 'CHP': Component(...)} + self.buses = {'Heat': Bus(...), 'Power': Bus(...)} + + def _get_container_groups(self): + return { + 'Components': self.components, + 'Buses': self.buses, + } + + + system = MySystem() + comp = system['Boiler'] # Type: Component | Bus (with proper IDE support) + 'Heat' in system # True + labels = system.keys() # Type: list[str] + elements = system.values() # Type: list[Component | Bus] + ``` + + Integration with ContainerMixin: + This mixin is designed to work alongside ContainerMixin-based containers + (ElementContainer, ResultsContainer) by aggregating them into a unified + interface while preserving their individual functionality. + """ + + def _get_container_groups(self) -> dict[str, ContainerMixin[Any]]: + """ + Return ordered dict of container groups to aggregate. + + Returns: + Dictionary mapping group names to container objects (e.g., ElementContainer, ResultsContainer). + Group names should be capitalized (e.g., 'Components', 'Buses'). + Order determines display order in __repr__. + + Example: + ```python + return { + 'Components': self.components, + 'Buses': self.buses, + 'Effects': self.effects, + } + ``` + """ + raise NotImplementedError('Subclasses must implement _get_container_groups()') + + def __getitem__(self, key: str) -> T_element: + """ + Get element by label, searching all containers. + + Args: + key: Element label to find + + Returns: + The element with the given label + + Raises: + KeyError: If element not found, with helpful suggestions + """ + # Search all containers in order + for container in self._get_container_groups().values(): + if key in container: + return container[key] + + # Element not found - provide helpful error + all_elements = {} + for container in self._get_container_groups().values(): + all_elements.update(container) + + suggestions = get_close_matches(key, all_elements.keys(), n=3, cutoff=0.6) + error_msg = f'Element "{key}" not found.' + + if suggestions: + error_msg += f' Did you mean: {", ".join(suggestions)}?' + else: + available = list(all_elements.keys()) + if len(available) <= 5: + error_msg += f' Available: {", ".join(available)}' + else: + error_msg += f' Available: {", ".join(available[:5])} ... (+{len(available) - 5} more)' + + raise KeyError(error_msg) + + def __iter__(self): + """Iterate over all element labels across all containers.""" + for container in self._get_container_groups().values(): + yield from container.keys() + + def __len__(self) -> int: + """Return total count of elements across all containers.""" + return sum(len(container) for container in self._get_container_groups().values()) + + def __contains__(self, key: str) -> bool: + """Check if element exists in any container.""" + return any(key in container for container in self._get_container_groups().values()) + + def keys(self) -> list[str]: + """Return all element labels across all containers.""" + return list(self) + + def values(self) -> list[T_element]: + """Return all element objects across all containers.""" + vals = [] + for container in self._get_container_groups().values(): + vals.extend(container.values()) + return vals + + def items(self) -> list[tuple[str, T_element]]: + """Return (label, element) pairs for all elements.""" + items = [] + for container in self._get_container_groups().values(): + items.extend(container.items()) + return items + + def _format_grouped_containers(self, title: str | None = None) -> str: + """ + Format containers as grouped string representation using each container's repr. + + Args: + title: Optional title for the representation. If None, no title is shown. + + Returns: + Formatted string with groups and their elements. + Empty groups are automatically hidden. + + Example output: + ``` + Components (1 item) + ------------------- + * Boiler + + Buses (2 items) + --------------- + * Heat + * Power + ``` + """ + parts = [] + + if title: + parts.append(fx_io.format_title_with_underline(title)) + + container_groups = self._get_container_groups() + for container in container_groups.values(): + if container: # Only show non-empty groups + if parts: # Add spacing between sections + parts.append('') + parts.append(repr(container).rstrip('\n')) + + return '\n'.join(parts) + + class Submodel(SubmodelsMixin): """Stores Variables and Constraints. Its a subset of a FlowSystemModel. Variables and constraints are stored in the main FlowSystemModel, and are referenced here. @@ -1056,9 +1352,7 @@ def __repr__(self) -> str: } # Format sections with headers and underlines - formatted_sections = [] - for section_header, section_content in sections.items(): - formatted_sections.append(f'{section_header}\n{"-" * len(section_header)}\n{section_content}') + formatted_sections = fx_io.format_sections_with_headers(sections) model_string = f'Submodel "{self.label_of_model}":' all_sections = '\n'.join(formatted_sections) @@ -1102,7 +1396,7 @@ def __contains__(self, name: str) -> bool: def __repr__(self) -> str: """Simple representation of the submodels collection.""" if not self.data: - return 'flixopt.structure.Submodels:\n----------------------------\n \n' + return fx_io.format_title_with_underline('flixopt.structure.Submodels') + ' \n' total_vars = sum(len(submodel.variables) for submodel in self.data.values()) total_cons = sum(len(submodel.constraints) for submodel in self.data.values()) @@ -1110,18 +1404,15 @@ def __repr__(self) -> str: title = ( f'flixopt.structure.Submodels ({total_vars} vars, {total_cons} constraints, {len(self.data)} submodels):' ) - underline = '-' * len(title) - if not self.data: - return f'{title}\n{underline}\n \n' - sub_models_string = '' + result = fx_io.format_title_with_underline(title) for name, submodel in self.data.items(): type_name = submodel.__class__.__name__ var_count = len(submodel.variables) con_count = len(submodel.constraints) - sub_models_string += f'\n * {name} [{type_name}] ({var_count}v/{con_count}c)' + result += f' * {name} [{type_name}] ({var_count}v/{con_count}c)\n' - return f'{title}\n{underline}{sub_models_string}\n' + return result def items(self) -> ItemsView[str, Submodel]: return self.data.items() diff --git a/tests/test_functional.py b/tests/test_functional.py index 0f9fe02ef..a83bf112f 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -152,7 +152,7 @@ def test_fixed_size(solver_fixture, time_steps_fixture): ) solve_and_load(flow_system, solver_fixture) - boiler = flow_system.all_elements['Boiler'] + boiler = flow_system['Boiler'] costs = flow_system.effects['costs'] assert_allclose( costs.submodel.total.solution.item(), @@ -193,7 +193,7 @@ def test_optimize_size(solver_fixture, time_steps_fixture): ) solve_and_load(flow_system, solver_fixture) - boiler = flow_system.all_elements['Boiler'] + boiler = flow_system['Boiler'] costs = flow_system.effects['costs'] assert_allclose( costs.submodel.total.solution.item(), @@ -234,7 +234,7 @@ def test_size_bounds(solver_fixture, time_steps_fixture): ) solve_and_load(flow_system, solver_fixture) - boiler = flow_system.all_elements['Boiler'] + boiler = flow_system['Boiler'] costs = flow_system.effects['costs'] assert_allclose( costs.submodel.total.solution.item(), @@ -289,8 +289,8 @@ def test_optional_invest(solver_fixture, time_steps_fixture): ) solve_and_load(flow_system, solver_fixture) - boiler = flow_system.all_elements['Boiler'] - boiler_optional = flow_system.all_elements['Boiler_optional'] + boiler = flow_system['Boiler'] + boiler_optional = flow_system['Boiler_optional'] costs = flow_system.effects['costs'] assert_allclose( costs.submodel.total.solution.item(), @@ -343,7 +343,7 @@ def test_on(solver_fixture, time_steps_fixture): ) solve_and_load(flow_system, solver_fixture) - boiler = flow_system.all_elements['Boiler'] + boiler = flow_system['Boiler'] costs = flow_system.effects['costs'] assert_allclose( costs.submodel.total.solution.item(), @@ -387,7 +387,7 @@ def test_off(solver_fixture, time_steps_fixture): ) solve_and_load(flow_system, solver_fixture) - boiler = flow_system.all_elements['Boiler'] + boiler = flow_system['Boiler'] costs = flow_system.effects['costs'] assert_allclose( costs.submodel.total.solution.item(), @@ -438,7 +438,7 @@ def test_switch_on_off(solver_fixture, time_steps_fixture): ) solve_and_load(flow_system, solver_fixture) - boiler = flow_system.all_elements['Boiler'] + boiler = flow_system['Boiler'] costs = flow_system.effects['costs'] assert_allclose( costs.submodel.total.solution.item(), @@ -502,7 +502,7 @@ def test_on_total_max(solver_fixture, time_steps_fixture): ) solve_and_load(flow_system, solver_fixture) - boiler = flow_system.all_elements['Boiler'] + boiler = flow_system['Boiler'] costs = flow_system.effects['costs'] assert_allclose( costs.submodel.total.solution.item(), @@ -555,13 +555,13 @@ def test_on_total_bounds(solver_fixture, time_steps_fixture): ), ), ) - flow_system.all_elements['Wärmelast'].inputs[0].fixed_relative_profile = np.array( + flow_system['Wärmelast'].inputs[0].fixed_relative_profile = np.array( [0, 10, 20, 0, 12] ) # Else its non deterministic solve_and_load(flow_system, solver_fixture) - boiler = flow_system.all_elements['Boiler'] - boiler_backup = flow_system.all_elements['Boiler_backup'] + boiler = flow_system['Boiler'] + boiler_backup = flow_system['Boiler_backup'] costs = flow_system.effects['costs'] assert_allclose( costs.submodel.total.solution.item(), @@ -624,12 +624,12 @@ def test_consecutive_on_off(solver_fixture, time_steps_fixture): Q_th=fx.Flow('Q_th', bus='Fernwärme', size=100), ), ) - flow_system.all_elements['Wärmelast'].inputs[0].fixed_relative_profile = np.array([5, 10, 20, 18, 12]) + flow_system['Wärmelast'].inputs[0].fixed_relative_profile = np.array([5, 10, 20, 18, 12]) # Else its non deterministic solve_and_load(flow_system, solver_fixture) - boiler = flow_system.all_elements['Boiler'] - boiler_backup = flow_system.all_elements['Boiler_backup'] + boiler = flow_system['Boiler'] + boiler_backup = flow_system['Boiler_backup'] costs = flow_system.effects['costs'] assert_allclose( costs.submodel.total.solution.item(), @@ -686,13 +686,13 @@ def test_consecutive_off(solver_fixture, time_steps_fixture): ), ), ) - flow_system.all_elements['Wärmelast'].inputs[0].fixed_relative_profile = np.array( + flow_system['Wärmelast'].inputs[0].fixed_relative_profile = np.array( [5, 0, 20, 18, 12] ) # Else its non deterministic solve_and_load(flow_system, solver_fixture) - boiler = flow_system.all_elements['Boiler'] - boiler_backup = flow_system.all_elements['Boiler_backup'] + boiler = flow_system['Boiler'] + boiler_backup = flow_system['Boiler_backup'] costs = flow_system.effects['costs'] assert_allclose( costs.submodel.total.solution.item(), From 3e9839b3ea5e5108061b671cbeb7a36f21793931 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 30 Oct 2025 08:55:24 +0100 Subject: [PATCH 387/448] Update CHANGELOG.md --- CHANGELOG.md | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4794f92ea..dcebbf0fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,17 +60,8 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp ### 💥 Breaking Changes ### ♻️ Changed -**Improved repr methods:** -- **Results classes** (`ComponentResults`, `BusResults`, `FlowResults`, `EffectResults`) now show concise header with key metadata followed by xarray Dataset repr -- **Element classes** (`Component`, `Bus`, `Flow`, `Effect`, `Storage`) now show one-line summaries with essential information (connections, sizes, capacities, constraints) - -**Container-based access:** -- **FlowSystem** now provides dict-like access patterns for all elements -- Use `flow_system['element_label']`, `flow_system.keys()`, `flow_system.values()`, and `flow_system.items()` for unified element access -- Specialized containers (`components`, `buses`, `effects`, `flows`) offer type-specific access with helpful error messages ### 🗑️ Deprecated -- **`FlowSystem.all_elements`** property is deprecated in favor of dict-like interface (`flow_system['label']`, `.keys()`, `.values()`, `.items()`). Will be removed in v4.0.0. ### 🔥 Removed @@ -90,6 +81,27 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp Until here --> +## [3.3.0] - 2025-10-30 + +**Summary**: Better access to Elements stored in the FLowSystem and better representations (repr) + +If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/). + +### ♻️ Changed +**Improved repr methods:** +- **Results classes** (`ComponentResults`, `BusResults`, `FlowResults`, `EffectResults`) now show concise header with key metadata followed by xarray Dataset repr +- **Element classes** (`Component`, `Bus`, `Flow`, `Effect`, `Storage`) now show one-line summaries with essential information (connections, sizes, capacities, constraints) + +**Container-based access:** +- **FlowSystem** now provides dict-like access patterns for all elements +- Use `flow_system['element_label']`, `flow_system.keys()`, `flow_system.values()`, and `flow_system.items()` for unified element access +- Specialized containers (`components`, `buses`, `effects`, `flows`) offer type-specific access with helpful error messages + +### 🗑️ Deprecated +- **`FlowSystem.all_elements`** property is deprecated in favor of dict-like interface (`flow_system['label']`, `.keys()`, `.values()`, `.items()`). Will be removed in v4.0.0. + +--- + ## [3.2.1] - 2025-10-29 **Summary**: From 4bf5cfc452c1161a76d2ffcf63ad649d87b00692 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 30 Oct 2025 09:07:16 +0100 Subject: [PATCH 388/448] Feature/improve docs (#449) * Improve docs * Improve docs * Improve docs and fix links * Update contribute.md * Update contribute.md * Improve CONTRIBUTE.md * Update CHANGELOG.md --- CHANGELOG.md | 2 + CONTRIBUTE.md | 135 ++++++++++++++++++ docs/contribute.md | 46 +----- docs/getting-started.md | 2 +- docs/index.md | 4 +- .../user-guide/{index.md => core-concepts.md} | 17 +-- .../user-guide/mathematical-notation/index.md | 4 +- mkdocs.yml | 4 +- tests/run_all_tests.py | 10 -- 9 files changed, 154 insertions(+), 70 deletions(-) create mode 100644 CONTRIBUTE.md rename docs/user-guide/{index.md => core-concepts.md} (93%) delete mode 100644 tests/run_all_tests.py diff --git a/CHANGELOG.md b/CHANGELOG.md index dcebbf0fd..8b94b8bda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,6 +72,8 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp ### 📦 Dependencies ### 📝 Docs +- Add more comprehensive `CONTRIBUTE.md` +- Improve logical structure in User Guide ### 👷 Development diff --git a/CONTRIBUTE.md b/CONTRIBUTE.md new file mode 100644 index 000000000..5e4b7d7ee --- /dev/null +++ b/CONTRIBUTE.md @@ -0,0 +1,135 @@ +# Contributing to FlixOpt + +We warmly welcome contributions from the community! Whether you're fixing bugs, adding features, improving documentation, or sharing examples, your contributions are valuable. + +## Ways to Contribute + +### 🐛 Report Issues +Found a bug or have a feature request? Please [open an issue](https://github.com/flixOpt/flixopt/issues) on GitHub. + +When reporting issues, please include: +- A clear description of the problem +- Steps to reproduce the issue +- Expected vs. actual behavior +- Your environment (OS, Python version, FlixOpt version) +- Minimal code example if applicable + +### 💡 Share Examples +Help others learn FlixOpt by contributing examples: +- Real-world use cases +- Tutorial notebooks +- Integration examples with other tools +- Add them to the `examples/` directory + +### 📖 Improve Documentation +Documentation improvements are always welcome: +- Fix typos or clarify existing docs +- Add missing documentation +- Translate documentation +- Improve code comments + +### 🔧 Submit Code Contributions +Ready to contribute code? Great! See the sections below for setup and guidelines. + +--- + +## Development Setup + +### Getting Started +1. Fork and clone the repository: + ```bash + git clone https://github.com/flixOpt/flixopt.git + cd flixopt + ``` + +2. Install development dependencies: + ```bash + pip install -e ".[full, dev]" + ``` + +3. Set up pre-commit hooks (one-time setup): + ```bash + pre-commit install + ``` + +4. Verify your setup: + ```bash + pytest + ``` + +### Working with Documentation +FlixOpt uses [mkdocs](https://www.mkdocs.org/) to generate documentation. + +To work on documentation: +```bash +pip install -e ".[docs]" +mkdocs serve +``` +Then navigate to http://127.0.0.1:8000/ + +--- + +## Code Quality Standards + +### Automated Checks +We use [Ruff](https://github.com/astral-sh/ruff) for linting and formatting. After the one-time setup above, **code quality checks run automatically on every commit**. + +### Manual Checks +To run checks manually: +- `ruff check --fix .` - Check and fix linting issues +- `ruff format .` - Format code +- `pre-commit run --all-files` - Run all pre-commit checks + +### Testing +- `pytest` - Run the test suite +- Ensure all tests pass before submitting a PR + +### Coding Guidelines +- Follow [PEP 8](https://pep8.org/) style guidelines +- Write clear, self-documenting code with helpful comments +- Include type hints for function signatures +- Create or update tests for new functionality +- Aim for 100% test coverage for new code + +--- + +## Workflow + +### Branches & Pull Requests +1. Create a feature branch from `main`: + ```bash + git checkout -b feature/your-feature-name + ``` + +2. Make your changes and commit them with clear messages + +3. Push your branch and open a Pull Request + +4. Ensure all CI checks pass + +### Branch Naming +- Features: `feature/feature-name` +- Bug fixes: `fix/bug-description` +- Documentation: `docs/what-changed` + +### Commit Messages +- Use clear, descriptive commit messages +- Start with a verb (Add, Fix, Update, Remove, etc.) +- Keep the first line under 72 characters + +--- + +## Releases + +We follow **Semantic Versioning** (MAJOR.MINOR.PATCH). Releases are created manually from the `main` branch by maintainers. + +--- + +## Questions? + +If you have questions or need help, feel free to: +- Open a discussion on GitHub +- Ask in an issue +- Reach out to the maintainers + +Thank you for contributing to FlixOpt! diff --git a/docs/contribute.md b/docs/contribute.md index 44af34069..e1b93aecb 100644 --- a/docs/contribute.md +++ b/docs/contribute.md @@ -1,45 +1 @@ -# Contributing to the Project - -We warmly welcome contributions from the community! This guide will help you get started with contributing to our project. - -## Development Setup -1. Clone the repository `git clone https://github.com/flixOpt/flixopt.git` -2. Install the development dependencies `pip install -e ".[dev]"` -3. Install pre-commit hooks `pre-commit install` (one-time setup) -4. Run `pytest` to ensure your code passes all tests - -## Code Quality -We use [Ruff](https://github.com/astral-sh/ruff) for linting and formatting. After the one-time setup above, **code quality checks run automatically on every commit**. - -To run manually: -- `ruff check --fix .` to check and fix linting issues -- `ruff format .` to format code or -- `pre-commit run` or `pre-commit run --all-files` to trigger all checks - -## Documentation (Optional) -FlixOpt uses [mkdocs](https://www.mkdocs.org/) to generate documentation. -To work on documentation: -```bash -pip install -e ".[docs]" -mkdocs serve -``` -Then navigate to http://127.0.0.1:8000/ - -## Testing -- `pytest` to run the test suite -- You can also run the provided python script `run_all_test.py` - ---- -# Best practices - -## Coding Guidelines - -- Follow PEP 8 style guidelines -- Write clear, commented code -- Include type hints -- Create or update tests for new functionality -- Ensure 100% test coverage for new code - -## Branches & Releases -New features should be branched from `main` into `feature/*` -As stated, we follow **Semantic Versioning**. Releases are created manually from the `main` branch. +{! ../CONTRIBUTE.md !} diff --git a/docs/getting-started.md b/docs/getting-started.md index 9af389755..044ffb872 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -37,6 +37,6 @@ Working with FlixOpt follows a general pattern: Now that you've installed FlixOpt and understand the basic workflow, you can: -- Learn about the [core concepts of FlixOpt](user-guide/index.md) +- Learn about the [core concepts of flixopt](user-guide/core-concepts.md) - Explore some [examples](examples/index.md) - Check the [API reference](api-reference/index.md) for detailed documentation diff --git a/docs/index.md b/docs/index.md index a7cd67f8c..c9b01f284 100644 --- a/docs/index.md +++ b/docs/index.md @@ -139,6 +139,6 @@ Help improve FlixOpt by contributing code, docs, or examples {% include-markdown "../README.md" - start="## Installation" - end="## License" + start="## 🛠️ Installation" + end="## 📄 License" %} diff --git a/docs/user-guide/index.md b/docs/user-guide/core-concepts.md similarity index 93% rename from docs/user-guide/index.md rename to docs/user-guide/core-concepts.md index b20d15263..ae3ac2876 100644 --- a/docs/user-guide/index.md +++ b/docs/user-guide/core-concepts.md @@ -1,10 +1,10 @@ -# FlixOpt Concepts +# Core concepts of flixopt FlixOpt is built around a set of core concepts that work together to represent and optimize **any system involving flows and conversions** - whether that's energy systems, material flows, supply chains, water networks, or production processes. This page provides a high-level overview of these concepts and how they interact. -## Core Concepts +## Main building blocks ### FlowSystem @@ -121,19 +121,20 @@ This [`CalculationResults`][flixopt.results.CalculationResults] object can be sa The process of working with FlixOpt can be divided into 3 steps: 1. Create a [`FlowSystem`][flixopt.flow_system.FlowSystem], containing all the elements and data of your system - - Define the time series of your system - - Add [`Components`][flixopt.components] like [`Boilers`][flixopt.linear_converters.Boiler], [`HeatPumps`][flixopt.linear_converters.HeatPump], [`CHPs`][flixopt.linear_converters.CHP], etc. - - Add [`Buses`][flixopt.elements.Bus] as connection points in your system + - Define the time horizon of your system (and optionally your periods and scenarios, see [Dimensions](mathematical-notation/dimensions.md))) - Add [`Effects`][flixopt.effects.Effect] to represent costs, emissions, etc. - - *This [`FlowSystem`][flixopt.flow_system.FlowSystem] can also be loaded from a netCDF file* + - Add [`Buses`][flixopt.elements.Bus] as connection points in your systeand [`Sinks`][flixopt.components.Sink] & [`Sources`][flixopt.components.Source] as connections to the outer world (markets, power grid, ...) + - Add [`Components`][flixopt.components] like [`Boilers`][flixopt.linear_converters.Boiler], [`HeatPumps`][flixopt.linear_converters.HeatPump], [`CHPs`][flixopt.linear_converters.CHP], etc. + - Add + - [`FlowSystems`][flixopt.flow_system.FlowSystem] can also be loaded from a netCDF file* 2. Translate the model to a mathematical optimization problem - Create a [`Calculation`][flixopt.calculation.Calculation] from your FlowSystem and choose a Solver - - ...The Calculation is translated internaly to a mathematical optimization problem... + - ...The Calculation is translated internally to a mathematical optimization problem... - ...and solved by the chosen solver. 3. Analyze the results - The results are stored in a [`CalculationResults`][flixopt.results.CalculationResults] object - This object can be saved to file and reloaded from file, retaining all information about the calculation - - As it contains the used [`FlowSystem`][flixopt.flow_system.FlowSystem], it can be used to start a new calculation + - As it contains the used [`FlowSystem`][flixopt.flow_system.FlowSystem], it fully documents all assumptions taken to create the results.
![FlixOpt Conceptual Usage](../images/architecture_flixOpt.png) diff --git a/docs/user-guide/mathematical-notation/index.md b/docs/user-guide/mathematical-notation/index.md index 05d1fed60..27e7b7e9a 100644 --- a/docs/user-guide/mathematical-notation/index.md +++ b/docs/user-guide/mathematical-notation/index.md @@ -1,9 +1,9 @@ # Mathematical Notation -This section provides the **mathematical formulations** underlying FlixOpt's optimization models. It is intended as **reference documentation** for users who want to understand the mathematical details behind the high-level FlixOpt API described in the [FlixOpt Concepts](../index.md) guide. +This section provides the **mathematical formulations** underlying FlixOpt's optimization models. It is intended as **reference documentation** for users who want to understand the mathematical details behind the high-level FlixOpt API described in the [FlixOpt Concepts](../core-concepts.md) guide. -**For typical usage**, refer to the [FlixOpt Concepts](../index.md) guide, [Examples](../../examples/index.md), and [API Reference](../../api-reference/index.md) - you don't need to understand these mathematical formulations to use FlixOpt effectively. +**For typical usage**, refer to the [FlixOpt Concepts](../core-concepts.md) guide, [Examples](../../examples/index.md), and [API Reference](../../api-reference/index.md) - you don't need to understand these mathematical formulations to use FlixOpt effectively. --- diff --git a/mkdocs.yml b/mkdocs.yml index cc108e237..7d3490360 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -11,9 +11,9 @@ repo_name: flixOpt/flixopt nav: - Home: index.md - User Guide: - - user-guide/index.md - - Migration to v3.0.0: user-guide/migration-guide-v3.md - Getting Started: getting-started.md + - Core Concepts: user-guide/core-concepts.md + - Migration to v3.0.0: user-guide/migration-guide-v3.md - Mathematical Notation: - Overview: user-guide/mathematical-notation/index.md - Dimensions: user-guide/mathematical-notation/dimensions.md diff --git a/tests/run_all_tests.py b/tests/run_all_tests.py deleted file mode 100644 index 83b6dfacf..000000000 --- a/tests/run_all_tests.py +++ /dev/null @@ -1,10 +0,0 @@ -""" -Run this script to run all tests with pytest -Alternatively, use 'python -m unittest discover -s tests' in the command line (using unittest) -or run testmodules individually -""" - -import pytest - -if __name__ == '__main__': - pytest.main(['test_integration.py', '--disable-warnings']) From 5905d9d3a362d3529d5dfe0af0fe4e5cc734bda5 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 30 Oct 2025 09:09:58 +0100 Subject: [PATCH 389/448] Typo and update CONTRIBUTE.md --- CONTRIBUTE.md | 35 +++++++++++++++++++++++++++++++- docs/user-guide/core-concepts.md | 2 +- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTE.md b/CONTRIBUTE.md index 5e4b7d7ee..5c73ba04b 100644 --- a/CONTRIBUTE.md +++ b/CONTRIBUTE.md @@ -81,8 +81,41 @@ To run checks manually: - `pre-commit run --all-files` - Run all pre-commit checks ### Testing -- `pytest` - Run the test suite +All tests are located in the `tests/` directory with a flat structure: +- `test_component.py` - Component tests +- `test_flow.py` - Flow tests +- `test_storage.py` - Storage tests +- etc. + +#### Running Tests +- `pytest` - Run the full test suite (excluding examples by default) +- `pytest tests/test_component.py` - Run a specific test file +- `pytest tests/test_component.py::TestClassName` - Run a specific test class +- `pytest tests/test_component.py::TestClassName::test_method` - Run a specific test +- `pytest -m slow` - Run only slow tests +- `pytest -m examples` - Run example tests (normally skipped) +- `pytest -k "keyword"` - Run tests matching a keyword + +#### Common Test Patterns +The `tests/conftest.py` file provides shared fixtures: +- `solver_fixture` - Parameterized solver fixture (HiGHS, Gurobi) +- `highs_solver` - HiGHS solver instance +- Coordinate configuration fixtures for timesteps, periods, scenarios + +Use these fixtures by adding them as function parameters: +```python +def test_my_feature(solver_fixture): + # solver_fixture is automatically provided by pytest + model = fx.FlowSystem(...) + model.solve(solver_fixture) +``` + +#### Testing Guidelines +- Write tests for all new functionality - Ensure all tests pass before submitting a PR +- Aim for 100% test coverage for new code +- Use descriptive test names that explain what's being tested +- Add the `@pytest.mark.slow` decorator for tests that take >5 seconds ### Coding Guidelines - Follow [PEP 8](https://pep8.org/) style guidelines diff --git a/docs/user-guide/core-concepts.md b/docs/user-guide/core-concepts.md index ae3ac2876..bf52a26ba 100644 --- a/docs/user-guide/core-concepts.md +++ b/docs/user-guide/core-concepts.md @@ -123,7 +123,7 @@ The process of working with FlixOpt can be divided into 3 steps: 1. Create a [`FlowSystem`][flixopt.flow_system.FlowSystem], containing all the elements and data of your system - Define the time horizon of your system (and optionally your periods and scenarios, see [Dimensions](mathematical-notation/dimensions.md))) - Add [`Effects`][flixopt.effects.Effect] to represent costs, emissions, etc. - - Add [`Buses`][flixopt.elements.Bus] as connection points in your systeand [`Sinks`][flixopt.components.Sink] & [`Sources`][flixopt.components.Source] as connections to the outer world (markets, power grid, ...) + - Add [`Buses`][flixopt.elements.Bus] as connection points in your system and [`Sinks`][flixopt.components.Sink] & [`Sources`][flixopt.components.Source] as connections to the outer world (markets, power grid, ...) - Add [`Components`][flixopt.components] like [`Boilers`][flixopt.linear_converters.Boiler], [`HeatPumps`][flixopt.linear_converters.HeatPump], [`CHPs`][flixopt.linear_converters.CHP], etc. - Add - [`FlowSystems`][flixopt.flow_system.FlowSystem] can also be loaded from a netCDF file* From 0fc96d4821bfd329ba129f497dc1b7a5de4e2ef5 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 30 Oct 2025 10:40:09 +0100 Subject: [PATCH 390/448] 447 fix bug with switch on and multiple periods or scenarios (#450) * Use more sophisticated check for absent values * Moved new method _has_value * Update CHANGELOG.md --- CHANGELOG.md | 1 + flixopt/interface.py | 4 ++-- flixopt/structure.py | 27 +++++++++++++++++++++++++++ 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b94b8bda..e3e3eee20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,6 +66,7 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp ### 🔥 Removed ### 🐛 Fixed +- Using `switch_on_total_max` with periods or scenarios failed ### 🔒 Security diff --git a/flixopt/interface.py b/flixopt/interface.py index eae0a8511..21cbc82b9 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -7,7 +7,7 @@ import logging import warnings -from typing import TYPE_CHECKING, Literal, Optional +from typing import TYPE_CHECKING, Any import numpy as np import pandas as pd @@ -1345,7 +1345,7 @@ def use_switch_on(self) -> bool: return True return any( - param is not None and param != {} + self._has_value(param) for param in [ self.effects_per_switch_on, self.switch_on_total_max, diff --git a/flixopt/structure.py b/flixopt/structure.py index 065769cd2..e54680592 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -508,6 +508,33 @@ def _validate_kwargs(self, kwargs: dict, class_name: str = None) -> None: unexpected_params = ', '.join(f"'{param}'" for param in extra_kwargs.keys()) raise TypeError(f'{class_name}.__init__() got unexpected keyword argument(s): {unexpected_params}') + @staticmethod + def _has_value(param: Any) -> bool: + """Check if a parameter has a meaningful value. + + Args: + param: The parameter to check. + + Returns: + False for: + - None + - Empty collections (dict, list, tuple, set, frozenset) + + True for all other values, including: + - Non-empty collections + - xarray DataArrays (even if they contain NaN/empty data) + - Scalar values (0, False, empty strings, etc.) + - NumPy arrays (even if empty - use .size to check those explicitly) + """ + if param is None: + return False + + # Check for empty collections (but not strings, arrays, or DataArrays) + if isinstance(param, (dict, list, tuple, set, frozenset)) and len(param) == 0: + return False + + return True + @classmethod def _resolve_dataarray_reference( cls, reference: str, arrays_dict: dict[str, xr.DataArray] From 2cdc9fd2c42e742d7a1da78472248ce8666ccbd3 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 30 Oct 2025 10:57:29 +0100 Subject: [PATCH 391/448] Feature/yaml representer (#451) * Improve yaml dumper for summary.yaml * Update CHANGELOG.md --- CHANGELOG.md | 1 + flixopt/io.py | 61 +++++++++++++++++++++++++++++++++++++--------- flixopt/results.py | 2 +- 3 files changed, 51 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3e3eee20..88129a963 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,7 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp ### 💥 Breaking Changes ### ♻️ Changed +- Improved `summary.yaml` to use a compacted list representation for periods and scenarios ### 🗑️ Deprecated diff --git a/flixopt/io.py b/flixopt/io.py index 8df03401c..7f832ed0e 100644 --- a/flixopt/io.py +++ b/flixopt/io.py @@ -174,6 +174,7 @@ def save_yaml( width: int = 1000, allow_unicode: bool = True, sort_keys: bool = False, + compact_numeric_lists: bool = False, **kwargs, ) -> None: """ @@ -186,20 +187,56 @@ def save_yaml( width: Maximum line width (default: 1000). allow_unicode: If True, allow Unicode characters (default: True). sort_keys: If True, sort dictionary keys (default: False). - **kwargs: Additional arguments to pass to yaml.safe_dump(). + compact_numeric_lists: If True, format numeric lists inline for better readability (default: False). + **kwargs: Additional arguments to pass to yaml.dump(). """ path = pathlib.Path(path) - with open(path, 'w', encoding='utf-8') as f: - yaml.safe_dump( - data, - f, - indent=indent, - width=width, - allow_unicode=allow_unicode, - sort_keys=sort_keys, - default_flow_style=False, - **kwargs, - ) + + if compact_numeric_lists: + # Define custom representer for compact numeric lists + def represent_list(dumper, data): + """ + Custom representer for lists to format them inline (flow style) + but only if they contain only numbers or nested numeric lists. + """ + if data and all( + isinstance(item, (int, float, np.integer, np.floating)) + or (isinstance(item, list) and all(isinstance(x, (int, float, np.integer, np.floating)) for x in item)) + for item in data + ): + return dumper.represent_sequence('tag:yaml.org,2002:seq', data, flow_style=True) + return dumper.represent_sequence('tag:yaml.org,2002:seq', data, flow_style=False) + + # Create custom dumper with the representer + class CompactDumper(yaml.SafeDumper): + pass + + CompactDumper.add_representer(list, represent_list) + + with open(path, 'w', encoding='utf-8') as f: + yaml.dump( + data, + f, + Dumper=CompactDumper, + indent=indent, + width=width, + allow_unicode=allow_unicode, + sort_keys=sort_keys, + default_flow_style=False, + **kwargs, + ) + else: + with open(path, 'w', encoding='utf-8') as f: + yaml.safe_dump( + data, + f, + indent=indent, + width=width, + allow_unicode=allow_unicode, + sort_keys=sort_keys, + default_flow_style=False, + **kwargs, + ) def load_config_file(path: str | pathlib.Path) -> dict: diff --git a/flixopt/results.py b/flixopt/results.py index 9232f0175..26eaf9d5d 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -1074,7 +1074,7 @@ def to_file( fx_io.save_dataset_to_netcdf(self.solution, paths.solution, compression=compression) fx_io.save_dataset_to_netcdf(self.flow_system_data, paths.flow_system, compression=compression) - fx_io.save_yaml(self.summary, paths.summary) + fx_io.save_yaml(self.summary, paths.summary, compact_numeric_lists=True) if save_linopy_model: if self.model is None: From 3b4809a4b8d85b597da4959f7bf8461fa43697fd Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 30 Oct 2025 10:59:47 +0100 Subject: [PATCH 392/448] Update CHANGELOG.md --- CHANGELOG.md | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88129a963..8d81c9a0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,22 +60,18 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp ### 💥 Breaking Changes ### ♻️ Changed -- Improved `summary.yaml` to use a compacted list representation for periods and scenarios ### 🗑️ Deprecated ### 🔥 Removed ### 🐛 Fixed -- Using `switch_on_total_max` with periods or scenarios failed ### 🔒 Security ### 📦 Dependencies ### 📝 Docs -- Add more comprehensive `CONTRIBUTE.md` -- Improve logical structure in User Guide ### 👷 Development @@ -85,6 +81,24 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp Until here --> +## [3.3.1] - 2025-10-30 + +**Summary**: Small Bugfix and improving readability + +If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/). + +### ♻️ Changed +- Improved `summary.yaml` to use a compacted list representation for periods and scenarios + +### 🐛 Fixed +- Using `switch_on_total_max` with periods or scenarios failed + +### 📝 Docs +- Add more comprehensive `CONTRIBUTE.md` +- Improve logical structure in User Guide + +--- + ## [3.3.0] - 2025-10-30 **Summary**: Better access to Elements stored in the FLowSystem and better representations (repr) From b91f804fa5d23836af90daef8dc68906aff454de Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 30 Oct 2025 13:47:29 +0100 Subject: [PATCH 393/448] Added type hint to all submodel attributes (#424) * Added type hint to all submodel attributes * Updated CHANGELOG.md * Fix type hints to include None --- CHANGELOG.md | 1 + flixopt/calculation.py | 4 +++- flixopt/components.py | 6 ++++++ flixopt/effects.py | 6 +++++- flixopt/elements.py | 4 ++++ flixopt/flow_system.py | 2 ++ flixopt/results.py | 2 ++ flixopt/structure.py | 4 +++- 8 files changed, 26 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d81c9a0b..cf3401514 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,7 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp ### 📝 Docs ### 👷 Development +- Added type hints for submodel in all Interface classes ### 🚧 Known Issues diff --git a/flixopt/calculation.py b/flixopt/calculation.py index 5e919dbf5..b1f0d0c6b 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -53,6 +53,8 @@ class for defined way of solving a flow_system optimization active_timesteps: Deprecated. Use FlowSystem.sel(time=...) or FlowSystem.isel(time=...) instead. """ + model: FlowSystemModel | None + def __init__( self, name: str, @@ -87,7 +89,7 @@ def __init__( flow_system._used_in_calculation = True self.flow_system = flow_system - self.model: FlowSystemModel | None = None + self.model = None self.durations = {'modeling': 0.0, 'solving': 0.0, 'saving': 0.0} self.folder = pathlib.Path.cwd() / 'results' if folder is None else pathlib.Path(folder) diff --git a/flixopt/components.py b/flixopt/components.py index 8f89378ae..e4209c8ac 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -161,6 +161,8 @@ class LinearConverter(Component): """ + submodel: LinearConverterModel | None + def __init__( self, label: str, @@ -377,6 +379,8 @@ class Storage(Component): With flow rates in m3/h, the charge state is therefore in m3. """ + submodel: StorageModel | None + def __init__( self, label: str, @@ -650,6 +654,8 @@ class Transmission(Component): """ + submodel: TransmissionModel | None + def __init__( self, label: str, diff --git a/flixopt/effects.py b/flixopt/effects.py index 757549223..8d8efbf4c 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -160,6 +160,8 @@ class Effect(Element): """ + submodel: EffectModel | None + def __init__( self, label: str, @@ -454,12 +456,14 @@ class EffectCollection(ElementContainer[Effect]): Handling all Effects """ + submodel: EffectCollectionModel | None + def __init__(self, *effects: Effect): super().__init__(element_type_name='effects') self._standard_effect: Effect | None = None self._objective_effect: Effect | None = None - self.submodel: EffectCollectionModel | None = None + self.submodel = None self.add_effects(*effects) def create_model(self, model: FlowSystemModel) -> EffectCollectionModel: diff --git a/flixopt/elements.py b/flixopt/elements.py index 2a9a2cf4f..337f34fce 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -223,6 +223,8 @@ class Bus(Element): by the FlowSystem during system setup. """ + submodel: BusModel | None + def __init__( self, label: str, @@ -411,6 +413,8 @@ class Flow(Element): """ + submodel: FlowModel | None + def __init__( self, label: str, diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index cf958d9d1..9bc7f7f99 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -143,6 +143,8 @@ class FlowSystem(Interface, CompositeContainerMixin[Element]): connected_and_transformed automatically when trying to solve a calculation. """ + model: FlowSystemModel | None + def __init__( self, timesteps: pd.DatetimeIndex, diff --git a/flixopt/results.py b/flixopt/results.py index 26eaf9d5d..91a530075 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -148,6 +148,8 @@ class CalculationResults(CompositeContainerMixin['ComponentResults | BusResults """ + model: linopy.Model | None + @classmethod def from_file(cls, folder: str | pathlib.Path, name: str) -> CalculationResults: """Load CalculationResults from saved files. diff --git a/flixopt/structure.py b/flixopt/structure.py index e54680592..e2aa6ee87 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -850,6 +850,8 @@ def __deepcopy__(self, memo): class Element(Interface): """This class is the basic Element of flixopt. Every Element has a label""" + submodel: ElementModel | None + def __init__(self, label: str, meta_data: dict | None = None): """ Args: @@ -858,7 +860,7 @@ def __init__(self, label: str, meta_data: dict | None = None): """ self.label = Element._valid_label(label) self.meta_data = meta_data if meta_data is not None else {} - self.submodel: ElementModel | None = None + self.submodel = None def _plausibility_checks(self) -> None: """This function is used to do some basic plausibility checks for each Element during initialization. From 28f0472d2dc151deb37a6a98a1f9ef50aea2be36 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 31 Oct 2025 16:31:10 +0100 Subject: [PATCH 394/448] chore(deps): update dependency dash to v3.2.0 (#426) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index cedcc3350..29c669b3e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -87,7 +87,7 @@ dev = [ "tsam==2.3.9", "scipy==1.15.1", "gurobipy==12.0.3", - "dash==3.0.0", + "dash==3.2.0", "dash-cytoscape==1.0.2", "dash-daq==0.6.0", "networkx==3.0.0", From 3fd56b5e38c5103eb772b7cda616471a2ec98c5e Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 31 Oct 2025 18:04:54 +0100 Subject: [PATCH 395/448] Merge from main --- .github/CONTRIBUTING.md | 2 +- .github/ISSUE_TEMPLATE/bug_report.yml | 118 +- .github/ISSUE_TEMPLATE/config.yml | 10 +- .github/ISSUE_TEMPLATE/feature_request.yml | 117 +- .github/ISSUE_TEMPLATE/general_issue.yml | 40 - .github/ISSUE_TEMPLATE/task.yml | 35 + .github/workflows/python-app.yaml | 77 +- CHANGELOG.md | 750 +- CONTRIBUTE.md | 168 + MANIFEST.in | 26 + README.md | 187 +- docs/SUMMARY.md | 7 - docs/contribute.md | 46 +- docs/faq/contribute.md | 61 - docs/faq/index.md | 3 - docs/getting-started.md | 2 +- docs/index.md | 143 +- docs/roadmap.md | 49 + docs/stylesheets/extra.css | 396 + .../Effects, Penalty & Objective.md | 132 - docs/user-guide/Mathematical Notation/Flow.md | 26 - .../Mathematical Notation/InvestParameters.md | 3 - .../Mathematical Notation/LinearConverter.md | 21 - .../Mathematical Notation/OnOffParameters.md | 3 - .../user-guide/Mathematical Notation/index.md | 22 - .../user-guide/{index.md => core-concepts.md} | 75 +- .../mathematical-notation/dimensions.md | 264 + .../effects-penalty-objective.md | 286 + .../elements}/Bus.md | 16 + .../mathematical-notation/elements/Flow.md | 64 + .../elements/LinearConverter.md | 50 + .../elements}/Storage.md | 41 +- .../features/InvestParameters.md | 302 + .../features/OnOffParameters.md | 307 + .../features}/Piecewise.md | 0 .../user-guide/mathematical-notation/index.md | 123 + .../modeling-patterns/bounds-and-states.md | 165 + .../modeling-patterns/duration-tracking.md | 159 + .../modeling-patterns/index.md | 54 + .../modeling-patterns/state-transitions.md | 227 + .../others.md | 0 docs/user-guide/migration-guide-v3.md | 232 + docs/user-guide/recipes/index.md | 47 + examples/00_Minmal/minimal_example.py | 86 +- examples/01_Simple/simple_example.py | 22 +- examples/02_Complex/complex_example.py | 67 +- .../02_Complex/complex_example_results.py | 8 +- .../example_calculation_types.py | 57 +- examples/04_Scenarios/scenario_example.py | 144 +- .../Zeitreihen2020.csv | 35137 ---------------- .../two_stage_optimization.py | 65 +- .../Zeitreihen2020.csv | 0 flixopt/__init__.py | 36 +- flixopt/aggregation.py | 47 +- flixopt/color_processing.py | 261 + flixopt/config.yaml | 10 - flixopt/utils.py | 88 - mkdocs.yml | 305 +- pyproject.toml | 62 +- renovate.json | 32 +- scripts/extract_changelog.py | 152 + scripts/gen_ref_pages.py | 17 +- tests/conftest.py | 188 +- tests/ressources/Sim1--flow_system.nc4 | Bin 0 -> 218834 bytes tests/ressources/Sim1--solution.nc4 | Bin 0 -> 210822 bytes tests/ressources/Sim1--summary.yaml | 92 + tests/run_all_tests.py | 10 - tests/test_bus.py | 12 +- tests/test_component.py | 52 +- tests/test_config.py | 480 + tests/test_dataconverter.py | 193 +- tests/test_effect.py | 250 +- tests/test_examples.py | 83 +- tests/test_flow.py | 180 +- tests/test_functional.py | 72 +- tests/test_heatmap_reshape.py | 91 + tests/test_integration.py | 32 +- tests/test_invest_parameters_deprecation.py | 344 + tests/test_io.py | 18 +- tests/test_linear_converter.py | 14 +- tests/test_models.py | 231 - tests/test_plots.py | 134 - tests/test_plotting_api.py | 138 + tests/test_results_plots.py | 33 +- tests/test_scenarios.py | 405 +- tests/test_storage.py | 56 +- tests/todos.txt | 5 - 87 files changed, 7371 insertions(+), 37194 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/general_issue.yml create mode 100644 .github/ISSUE_TEMPLATE/task.yml create mode 100644 CONTRIBUTE.md create mode 100644 MANIFEST.in delete mode 100644 docs/SUMMARY.md delete mode 100644 docs/faq/contribute.md delete mode 100644 docs/faq/index.md create mode 100644 docs/roadmap.md create mode 100644 docs/stylesheets/extra.css delete mode 100644 docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md delete mode 100644 docs/user-guide/Mathematical Notation/Flow.md delete mode 100644 docs/user-guide/Mathematical Notation/InvestParameters.md delete mode 100644 docs/user-guide/Mathematical Notation/LinearConverter.md delete mode 100644 docs/user-guide/Mathematical Notation/OnOffParameters.md delete mode 100644 docs/user-guide/Mathematical Notation/index.md rename docs/user-guide/{index.md => core-concepts.md} (63%) create mode 100644 docs/user-guide/mathematical-notation/dimensions.md create mode 100644 docs/user-guide/mathematical-notation/effects-penalty-objective.md rename docs/user-guide/{Mathematical Notation => mathematical-notation/elements}/Bus.md (78%) create mode 100644 docs/user-guide/mathematical-notation/elements/Flow.md create mode 100644 docs/user-guide/mathematical-notation/elements/LinearConverter.md rename docs/user-guide/{Mathematical Notation => mathematical-notation/elements}/Storage.md (52%) create mode 100644 docs/user-guide/mathematical-notation/features/InvestParameters.md create mode 100644 docs/user-guide/mathematical-notation/features/OnOffParameters.md rename docs/user-guide/{Mathematical Notation => mathematical-notation/features}/Piecewise.md (100%) create mode 100644 docs/user-guide/mathematical-notation/index.md create mode 100644 docs/user-guide/mathematical-notation/modeling-patterns/bounds-and-states.md create mode 100644 docs/user-guide/mathematical-notation/modeling-patterns/duration-tracking.md create mode 100644 docs/user-guide/mathematical-notation/modeling-patterns/index.md create mode 100644 docs/user-guide/mathematical-notation/modeling-patterns/state-transitions.md rename docs/user-guide/{Mathematical Notation => mathematical-notation}/others.md (100%) create mode 100644 docs/user-guide/migration-guide-v3.md create mode 100644 docs/user-guide/recipes/index.md delete mode 100644 examples/05_Two-stage-optimization/Zeitreihen2020.csv rename examples/{03_Calculation_types => resources}/Zeitreihen2020.csv (100%) create mode 100644 flixopt/color_processing.py delete mode 100644 flixopt/config.yaml delete mode 100644 flixopt/utils.py create mode 100644 scripts/extract_changelog.py create mode 100644 tests/ressources/Sim1--flow_system.nc4 create mode 100644 tests/ressources/Sim1--solution.nc4 create mode 100644 tests/ressources/Sim1--summary.yaml delete mode 100644 tests/run_all_tests.py create mode 100644 tests/test_config.py create mode 100644 tests/test_heatmap_reshape.py create mode 100644 tests/test_invest_parameters_deprecation.py delete mode 100644 tests/test_models.py delete mode 100644 tests/test_plots.py create mode 100644 tests/test_plotting_api.py delete mode 100644 tests/todos.txt diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 2a51618d9..e9876c089 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -12,7 +12,7 @@ Thanks for your interest in contributing to FlixOpt! 🚀 2. **Install for Development** ```bash - pip install -e ".[full]" + pip install -e ".[full, dev, docs]" ``` 3. **Make Changes & Submit PR** diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 94b4491a5..3b1a32fb2 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -6,106 +6,70 @@ body: - type: markdown attributes: value: | - Thanks for taking the time to fill out this bug report! - - **Before submitting**: Please search [existing issues](https://github.com/flixOpt/flixopt/issues) to avoid duplicates. - -- type: checkboxes - id: checks - attributes: - label: Version Confirmation - description: Please confirm you can reproduce this on a supported version - options: - - label: I have confirmed this bug exists on the latest [release](https://github.com/flixOpt/flixopt/releases) of FlixOpt - required: true + **Quick guide**: Describe what's broken, provide code to reproduce if possible. + For simple bugs, just fill the first field. - type: textarea id: problem attributes: - label: Bug Description - description: Clearly describe what went wrong - placeholder: | - What happened? What did you expect to happen instead? - - Include any error messages or unexpected outputs. + label: What's broken? + description: Describe the bug - what happened vs. what you expected validations: required: true - type: textarea id: example attributes: - label: Minimal Reproducible Example + label: Code to reproduce description: | Provide the smallest possible code example that reproduces the bug. See [how to create minimal bug reports](https://matthewrocklin.com/minimal-bug-reports). - placeholder: | - import flixopt as fx + value: | import pandas as pd + import numpy as np + import flixopt as fx - # Minimal example that reproduces the bug - timesteps = pd.date_range('2024-01-01', periods=24, freq='h') - flow_system = fx.FlowSystem(timesteps) + fx.CONFIG.Logging.console = True + fx.CONFIG.Logging.level = 'DEBUG' + fx.CONFIG.apply() + flow_system = fx.FlowSystem(pd.date_range('2020-01-01', periods=3, freq='h')) - # Add components that trigger the bug... + flow_system.add_elements( + fx.Bus('Heat'), + fx.Bus('Gas'), + fx.Effect('Costs', '€', 'Cost', is_standard=True, is_objective=True), + fx.linear_converters.Boiler( + 'Boiler', + eta=0.5, + Q_th=fx.Flow(label='Heat', bus='Heat', size=50), + Q_fu=fx.Flow(label='Gas', bus='Gas'), + ), + fx.Sink( + 'Sink', + inputs=[ + fx.Flow(label='Demand', bus='Heat', size=1, fixed_relative_profile=np.array([30, 0, 20])) + ], + ), + fx.Source( + 'Source', + outputs=[fx.Flow(label='Gas', bus='Gas', size=1000, effects_per_flow_hour=0.04)], + ), + ) + + calculation = fx.FullCalculation('Simulation1', flow_system).do_modeling().solve(fx.solvers.HighsSolver(0.01, 60)) - # Show the problematic operation - result = flow_system.solve() # This should fail/behave unexpectedly render: python - validations: - required: true - type: textarea id: error-output attributes: - label: Error Output - description: If there's an error message, paste the full traceback here + label: Error message + description: Paste the full traceback if there is one render: shell -- type: dropdown - id: solver - attributes: - label: Solver Used - description: Which solver were you using? - options: - - HiGHS (default) - - Gurobi - - CPLEX - - GLPK - - CBC - - Other (specify below) - validations: - required: true - -- type: input - id: os - attributes: - label: Operating System - placeholder: "e.g., Windows 11, macOS 14.2, Ubuntu 22.04" - validations: - required: true - -- type: input - id: python-version - attributes: - label: Python Version - placeholder: "e.g., 3.11.5" - validations: - required: true - - type: textarea - id: environment + id: context attributes: - label: Environment Info - description: | - Run one of these commands and paste the output: - - `pip freeze` - - `conda env export` - render: shell - value: > -
- - ``` - Replace this with your environment info - ``` - -
+ label: Additional context + description: Solver, Python/OS version, environment details, or anything else relevant + placeholder: "HiGHS solver, Python 3.11, macOS 14" diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index bc07496e8..0c30b34f2 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,14 +1,8 @@ -blank_issues_enabled: false +blank_issues_enabled: true contact_links: - name: 🤔 Modeling Questions url: https://github.com/flixOpt/flixopt/discussions/categories/q-a about: "How to model specific energy systems, components, and constraints" - - name: ⚡ Performance & Optimization - url: https://github.com/flixOpt/flixopt/discussions/categories/performance - about: "Solver performance, memory usage, and optimization speed issues" - - name: 💡 Ideas & Suggestions - url: https://github.com/flixOpt/flixopt/discussions/categories/ideas - about: "Share ideas and discuss potential improvements with the community" - name: 📖 Documentation url: https://flixopt.github.io/flixopt/latest/ - about: "Browse guides, API reference, and examples" + about: "Guides, API reference, and examples" diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index dd5c8def2..1c48cf10c 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -6,122 +6,33 @@ body: - type: markdown attributes: value: | - Thanks for suggesting a new feature! - - **Before submitting**: Please search [existing issues](https://github.com/flixOpt/flixopt/issues) and check our [roadmap](https://github.com/flixOpt/flixopt/discussions) to avoid duplicates. - -- type: checkboxes - id: checks - attributes: - label: Prerequisites - options: - - label: I have searched existing issues and discussions - required: true - - label: I have checked the [documentation](https://flixopt.github.io/flixopt/latest/) - required: true - -- type: dropdown - id: feature-type - attributes: - label: Feature Category - description: What type of feature is this? - options: - - New Component (storage, generation, conversion, etc.) - - Enhancement to Existing Component - - New Optimization Feature - - Data Input/Output Improvement - - Results/Visualization Enhancement - - Performance/Solver Improvement - - API/Usability Improvement - - Documentation/Examples - - Other - validations: - required: true + **Quick guide**: Describe what you want and why it's useful. + Skip optional fields for simple ideas. - type: textarea - id: problem + id: description attributes: - label: Problem Statement - description: What problem would this feature solve? - placeholder: | - Current limitation: "FlixOpt doesn't support [specific energy system component/feature]..." - - Impact: "This prevents users from modeling [specific scenarios]..." - -- type: textarea - id: solution - attributes: - label: Proposed Solution - description: Describe your proposed solution in detail - placeholder: | - I propose adding a new component/feature that would... - - Key capabilities: - - Feature 1 - - Feature 2 - - Feature 3 + label: What feature or improvement? + description: Describe what should be added or changed validations: required: true - type: textarea id: use-case attributes: - label: Use Case & Examples - description: Provide concrete examples of how this would be used - placeholder: | - Real-world scenario: "I'm modeling a microgrid with battery storage and need to..." - - Specific requirements: - - Must handle [specific constraint] - - Should support [specific behavior] - - Would benefit [specific user group] - validations: - required: true + label: Use case + description: What problem does this solve? What would you use it for? + placeholder: "When modeling X, I need to do Y..." - type: textarea - id: code-example + id: api-idea attributes: - label: Desired API (Optional) - description: Show how you'd like to use this feature + label: API idea (optional) + description: How might it work? Sketch a rough API if you have ideas placeholder: | - # Example of proposed API - component = fx.NewComponent( + # Example: + component = fx.NewThing( label='example', - parameter1=value1, - parameter2=value2 + param=value ) - - flow_system.add_component(component) render: python - -- type: textarea - id: alternatives - attributes: - label: Alternatives Considered - description: What workarounds or alternatives have you tried? - placeholder: | - Current workaround: "I'm currently using [existing component] but it doesn't support..." - - Other approaches considered: "I looked into [alternative] but..." - -- type: dropdown - id: priority - attributes: - label: Priority/Impact - description: How important is this feature for your work? - options: - - Critical - Blocking important work - - High - Would significantly improve workflow - - Medium - Nice to have enhancement - - Low - Minor improvement - -- type: textarea - id: additional-context - attributes: - label: Additional Context - description: References, papers, examples from other tools, etc. - placeholder: | - References: - - Research paper: [Title and link] - - Similar feature in [other tool]: [description] - - Industry standard: [description] diff --git a/.github/ISSUE_TEMPLATE/general_issue.yml b/.github/ISSUE_TEMPLATE/general_issue.yml deleted file mode 100644 index f2578b9ce..000000000 --- a/.github/ISSUE_TEMPLATE/general_issue.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: 📝 General Issue -description: For issues that don't fit the specific templates below -title: "" -body: - - type: markdown - attributes: - value: | - **For specific issue types, please use the dedicated templates:** - - 🐛 **Bug Report** - Something is broken or not working as expected - - ✨ **Feature Request** - Suggest new functionality - - **For other topics, consider using Discussions instead:** - - 🤔 [Modeling Questions](https://github.com/flixOpt/flixopt/discussions/categories/q-a) - How to model specific energy systems - - ⚡ [Performance Help](https://github.com/flixOpt/flixopt/discussions/categories/performance) - Optimization speed and memory issues - - - type: textarea - id: issue-description - attributes: - label: Issue Description - description: Describe your issue, question, or concern - placeholder: | - Please describe: - - What you're trying to accomplish - - What's not working as expected - - Any relevant context or background - validations: - required: true - - - type: textarea - id: context - attributes: - label: Additional Context - description: Code examples, environment details, error messages, etc. - placeholder: | - Optional: Add any relevant details like: - - Code snippets - - Error messages - - Environment info - - Screenshots - render: markdown diff --git a/.github/ISSUE_TEMPLATE/task.yml b/.github/ISSUE_TEMPLATE/task.yml new file mode 100644 index 000000000..bb4741e02 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/task.yml @@ -0,0 +1,35 @@ +name: 📋 Task +description: Track work items, refactoring, cleanup, or general todos +title: "[TASK] " +labels: ["type: task"] +body: +- type: markdown + attributes: + value: | + **Quick guide**: Describe what needs to be done. + Use this for refactoring, cleanup, documentation, or general work items. + +- type: textarea + id: description + attributes: + label: What needs to be done? + description: Describe the task + validations: + required: true + +- type: textarea + id: details + attributes: + label: Details + description: Context, steps, requirements, or anything else relevant + placeholder: | + - Step 1 + - Step 2 + - Related to: #123 + +- type: textarea + id: acceptance + attributes: + label: Done when... + description: What defines this task as complete? + placeholder: "Tests pass, code is clean, docs updated..." diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml index 20011db36..f4dbc28c5 100644 --- a/.github/workflows/python-app.yaml +++ b/.github/workflows/python-app.yaml @@ -5,7 +5,7 @@ on: branches: [main] # Only main branch tags: ['v*.*.*'] pull_request: - branches: [main, dev] + branches: [main, 'dev*', 'dev/**', 'feature/**'] types: [opened, synchronize, reopened] paths-ignore: - 'docs/**' @@ -35,7 +35,7 @@ jobs: - name: Set up uv uses: astral-sh/setup-uv@v6 with: - version: "0.8.19" + version: "0.9.5" enable-cache: true - name: Set up Python @@ -75,7 +75,7 @@ jobs: - name: Set up uv uses: astral-sh/setup-uv@v6 with: - version: "0.8.19" + version: "0.9.5" enable-cache: true - name: Set up Python ${{ matrix.python-version }} @@ -85,10 +85,39 @@ jobs: - name: Install dependencies run: | - uv pip install --system .[dev] "pytest-xdist==3.*" + uv pip install --system .[dev] - name: Run tests - run: pytest -v -p no:warnings --numprocesses=auto + run: pytest -v --numprocesses=auto + + test-examples: + runs-on: ubuntu-24.04 + timeout-minutes: 45 + needs: lint + # Only run examples on releases (tags) + if: startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'push' && github.ref == 'refs/heads/main') + + steps: + - name: Check out code + uses: actions/checkout@v5 + + - name: Set up uv + uses: astral-sh/setup-uv@v6 + with: + version: "0.9.5" + enable-cache: true + + - name: Set up Python ${{ env.PYTHON_VERSION }} + uses: actions/setup-python@v6 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install dependencies + run: | + uv pip install --system .[dev] + + - name: Run example tests + run: pytest -v -m examples --numprocesses=auto security: name: Security Scan @@ -101,7 +130,7 @@ jobs: - name: Set up uv uses: astral-sh/setup-uv@v6 with: - version: "0.8.19" + version: "0.9.5" enable-cache: true - name: Set up Python @@ -129,7 +158,7 @@ jobs: runs-on: ubuntu-24.04 permissions: contents: write - needs: [lint, test, security] + needs: [lint, test, test-examples, security] if: startsWith(github.ref, 'refs/tags/v') steps: @@ -141,7 +170,7 @@ jobs: - name: Set up uv uses: astral-sh/setup-uv@v6 with: - version: "0.8.19" + version: "0.9.5" enable-cache: true - name: Set up Python @@ -168,7 +197,7 @@ jobs: publish-testpypi: name: Publish to TestPyPI runs-on: ubuntu-24.04 - needs: [test, create-release] # Run after tests and release creation + needs: [test, test-examples, create-release] # Run after tests and release creation if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') # Only on tag push environment: name: testpypi @@ -183,7 +212,7 @@ jobs: - name: Set up uv uses: astral-sh/setup-uv@v6 with: - version: "0.8.19" + version: "0.9.5" enable-cache: true - name: Set up Python @@ -269,7 +298,7 @@ jobs: - name: Set up uv uses: astral-sh/setup-uv@v6 with: - version: "0.8.19" + version: "0.9.5" enable-cache: true - name: Set up Python @@ -313,25 +342,20 @@ jobs: # Wait and retry while PyPI indexes the package INSTALL_SUCCESS=false - for d in 10 20 40 80 120; do + for d in 10 20 40 60 90 120 180 300 480 600; do # Total: up to ~30 minutes sleep "$d" echo "Attempting to install $PACKAGE_NAME==$VERSION from PyPI (retry after ${d}s)..." - - # Install specific version and verify it matches - if uv pip install "$PACKAGE_NAME==$VERSION" && \ + # Install directly from pypi, potentially mitigatiing caches + if uv pip install --index-url https://pypi.org/simple/ "$PACKAGE_NAME==$VERSION" && \ python -c "from importlib.metadata import version; installed = version('$PACKAGE_NAME'); print(f'Installed: {installed}'); assert '$VERSION' == installed"; then INSTALL_SUCCESS=true break fi done - # Check if installation succeeded if [ "$INSTALL_SUCCESS" = "false" ]; then echo "ERROR: Failed to install $PACKAGE_NAME==$VERSION from PyPI after all retries" - echo "This could indicate:" - echo " - PyPI indexing issues" - echo " - Package upload problems" - echo " - Version mismatch between tag and package" + echo "Check: https://pypi.org/project/$PACKAGE_NAME/$VERSION/" exit 1 fi @@ -355,7 +379,7 @@ jobs: - name: Set up uv uses: astral-sh/setup-uv@v6 with: - version: "0.8.19" + version: "0.9.5" enable-cache: true - name: Set up Python @@ -363,10 +387,15 @@ jobs: with: python-version: ${{ env.PYTHON_VERSION }} - - name: Sync changelog to docs + - name: Extract changelog to docs run: | - cp CHANGELOG.md docs/changelog.md - echo "✅ Synced changelog to docs" + # Install packaging dependency for changelog extraction + uv pip install --system packaging + + # Extract individual release files + python scripts/extract_changelog.py + + echo "✅ Extracted changelog to docs/changelog/" - name: Install documentation dependencies run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index f59b76ec6..cf3401514 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,142 +1,527 @@ # Changelog -All notable changes to this project will be documented in this file. +This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +Formatting is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) & [Gitmoji](https://gitmoji.dev). +For more details regarding the individual PRs and contributors, please refer to our [GitHub releases](https://github.com/flixOpt/flixopt/releases). -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +!!! tip + + If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/). + +--- -## [Unreleased] - ????-??-?? -Multi-Period and stochastic modeling is coming to flixopt in this release. - -In this release, we introduce the following new features: -#### Multi-period-support -A flixopt model might be modeled with a "year" dimension. -This enables to model transformation pathways over multiple years. - -#### Stochastic modeling -A flixopt model can be modeled with a scenario dimension. -Scenarios can be weighted and variables can be equated across scenarios. This enables to model uncertainties in the flow system, such as: -* Different demand profiles -* Different price forecasts -* Different weather conditions - -Common use cases are: -* Find the best overall investment decision for possible scenarios (robust decision-making) -* Find the best dispatch for the most important assets under uncertain price and weather conditions - -The weighted sum of the total objective effect of each scenario is used as the objective of the optimization. - -#### Improved Data handling: IO, resampling and more through xarray -* IO for all Interfaces and the FlowSystem with round-trip serialization support - * NetCDF export/import capabilities for all Interface objects and FlowSystem - * JSON export for documentation purposes - * Recursive handling of nested Interface objects -* FlowSystem data manipulation methods - * `sel()` and `isel()` methods for temporal data selection - * `resample()` method for temporal resampling - * `copy()` method to create a copy of a FlowSystem, including all underlying Elements and their data - * `__eq__()` method for FlowSystem comparison - -* Core data handling improvements - * `get_dataarray_stats()` function for statistical summaries - * Enhanced `DataConverter` class with better TimeSeriesData support - - -### Added -* FlowSystem Restoring: The used FlowSystem will now get restired from the results (lazily). ALll Parameters can be safely acessed anytime after the solve. -* FLowResults added as a new class to store the results of Flows. They can now be accessed directly. -* Added precomputed DataArrays for `size`s, `flow_rate`s and `flow_hour`s. -* Added `effects_per_component()`-Dataset to Results that stores the direct (and indirect) effects of each component. This greatly improves the evaluation of the impact of individual Components, even with many and complex effects. -* Improved filter methods for Results -* Balanced storage - Storage charging and discharging sizes can now be forced to be equal in when optimizing their size. -* Added Example for 2-stage Investment decisions leveraging the resampling of a FlowSystem -* New Storage Parameter: `relative_minimum_final_charge_state` and `relative_maximum_final_charge_state` parameter for final state control - -### Changed -* **BREAKING**: `relative_minimum_charge_state` and `relative_maximum_charge_state` don't have an extra timestep anymore. The final charge state can now be constrained by parameters `relative_minimum_final_charge_state` and `relative_maximum_final_charge_state` instead -* **BREAKING**: Renamed class `SystemModel` to `FlowSystemModel` -* **BREAKING**: Renamed class `Model` to `Submodel` -* **BREAKING**: Renamed `mode` parameter in plotting methods to `style` -* FlowSystems can not be shared across multiple Calculations anymore. A copy of the FlowSystem is created instead, making every Calculation independent -* Each Subcalculation in `SegmentedCalculation` now has its own distinct `FlowSystem` object -* Type system overhaul - added clear separation between temporal and non-temporal data throughout codebase for better clarity -* Enhanced FlowSystem interface with improved `__repr__()` and `__str__()` methods -* Improved Model Structure - Views and organisation is now divided into: - * Model: The main Model (linopy.Model) that is used to create and store the variables and constraints for the flow_system. - * Submodel: The base class for all submodels. Each is a subset of the Model, for simpler acess and clearer code. - -### Deprecated -* The `agg_group` and `agg_weight` parameters of `TimeSeriesData` are deprecated and will be removed in a future version. Use `aggregation_group` and `aggregation_weight` instead. -* The `active_timesteps` parameter of `Calculation` is deprecated and will be removed in a future version. Use the new `sel(time=...)` method on the FlowSystem instead. -* The assignment of Bus Objects to Flow.bus is deprecated and will be removed in a future version. Use the label of the Bus instead. -* The usage of Effects objects in Dicts to assign shares to Effects is deprecated and will be removed in a future version. Use the label of the Effect instead. - -### Removed - -### Fixed -* Enhanced NetCDF I/O with proper attribute preservation for DataArrays -* Improved error handling and validation in serialization processes -* Better type consistency across all framework components - -### Known issues -* IO for single Interfaces/Elemenets to Datasets might not work properly if the Interface/Element is not part of a fully transformed and connected FlowSystem. This arrises from Numeric Data not being stored as xr.DataArray by the user. To avoid this, always use the `to_dataset()` on Elements inside a FlowSystem thats connected and transformed. - -### *Development* -* **BREAKING**: Calculation.do_modeling() now returns the Calculation object instead of its linopy.Model -* **BREAKING**: Renamed class `SystemModel` to `FlowSystemModel` -* **BREAKING**: Renamed class `Model` to `Submodel` -* FlowSystem data management simplified - removed `time_series_collection` pattern in favor of direct timestep properties -* Change modeling hierarchy to allow for more flexibility in future development. This leads to minimal changes in the access and creation of Submodels and their variables. -* Added new module `.modeling`that contains Modelling primitives and utilities -* Clearer separation between the main Model and "Submodels" -* Improved access to the Submodels and their variables, constraints and submodels -* Added __repr__() for Submodels to easily inspect its content -* Enhanced data handling methods - * `fit_to_model_coords()` method for data alignment - * `fit_effects_to_model_coords()` method for effect data processing - * `connect_and_transform()` method replacing several operations +## [3.3.1] - 2025-10-30 + +**Summary**: Small Bugfix and improving readability + +If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/). + +### ♻️ Changed +- Improved `summary.yaml` to use a compacted list representation for periods and scenarios + +### 🐛 Fixed +- Using `switch_on_total_max` with periods or scenarios failed + +### 📝 Docs +- Add more comprehensive `CONTRIBUTE.md` +- Improve logical structure in User Guide + +--- + +## [3.3.0] - 2025-10-30 + +**Summary**: Better access to Elements stored in the FLowSystem and better representations (repr) + +If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/). + +### ♻️ Changed +**Improved repr methods:** +- **Results classes** (`ComponentResults`, `BusResults`, `FlowResults`, `EffectResults`) now show concise header with key metadata followed by xarray Dataset repr +- **Element classes** (`Component`, `Bus`, `Flow`, `Effect`, `Storage`) now show one-line summaries with essential information (connections, sizes, capacities, constraints) + +**Container-based access:** +- **FlowSystem** now provides dict-like access patterns for all elements +- Use `flow_system['element_label']`, `flow_system.keys()`, `flow_system.values()`, and `flow_system.items()` for unified element access +- Specialized containers (`components`, `buses`, `effects`, `flows`) offer type-specific access with helpful error messages + +### 🗑️ Deprecated +- **`FlowSystem.all_elements`** property is deprecated in favor of dict-like interface (`flow_system['label']`, `.keys()`, `.values()`, `.items()`). Will be removed in v4.0.0. + +--- + +## [3.2.1] - 2025-10-29 + +**Summary**: + +If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/). + +### 🐛 Fixed +- Fixed resampling of FlowSystem to reset `hours_of_last_timestep` and `hours_of_previous_timesteps` properly + +### 👷 Development +- Improved issue templates + +--- + +## [3.2.0] - 2025-10-26 + +**Summary**: Enhanced plotting capabilities with consistent color management, custom plotting kwargs support, and centralized I/O handling. + +If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/). + +### ✨ Added + +**Color management:** +- **`setup_colors()` method** for `CalculationResults` and `SegmentedCalculationResults` to configure consistent colors across all plots + - Group components by colorscales: `results.setup_colors({'CHP': 'reds', 'Storage': 'blues', 'Greys': ['Grid', 'Demand']})` + - Automatically propagates to all segments in segmented calculations + - Colors persist across all plot calls unless explicitly overridden +- **Flexible color inputs**: Supports colorscale names (e.g., 'turbo', 'plasma'), color lists, or label-to-color dictionaries +- **Cross-backend compatibility**: Seamless color handling for both Plotly and Matplotlib + +**Plotting customization:** +- **Plotting kwargs support**: Pass additional arguments to plotting backends via `px_kwargs`, `plot_kwargs`, and `backend_kwargs` parameters +- **New `CONFIG.Plotting` configuration section**: + - `default_show`: Control default plot visibility + - `default_engine`: Choose 'plotly' or 'matplotlib' + - `default_dpi`: Set resolution for saved plots + - `default_facet_cols`: Configure default faceting columns + - `default_sequential_colorscale`: Default for heatmaps (now 'turbo') + - `default_qualitative_colorscale`: Default for categorical plots (now 'plotly') + +**I/O improvements:** +- Centralized JSON/YAML I/O with auto-format detection +- Enhanced NetCDF handling with consistent engine usage +- Better numeric formatting in YAML exports + +### ♻️ Changed +- **Default colorscale**: Changed from 'viridis' to 'turbo' for better perceptual uniformity +- **Color terminology**: Standardized from "colormap" to "colorscale" throughout for Plotly consistency +- **Plotting internals**: Now use `xr.Dataset` as primary data type (DataFrames automatically converted) +- **NetCDF engine**: Switched back to netcdf4 engine following xarray updates and performance benchmarks + +### 🔥 Removed +- Removed unused `plotting.pie_with_plotly()` method + +### 🐛 Fixed +- Improved error messages when using `engine='matplotlib'` with multidimensional data +- Better dimension validation in `results.plot_heatmap()` + +### 📝 Docs +- Enhanced examples demonstrating `setup_colors()` usage +- Updated terminology from "colormap" to "colorscale" in docstrings + +### 👷 Development +- Fixed concurrency issue in CI +- Centralized color processing logic into dedicated module +- Refactored to function-based color handling for simpler API + +--- + +## [3.1.1] - 2025-10-20 +**Summary**: Fixed a bug when acessing the `effects_per_component` dataset in results without periodic effects. + +If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/). + +### 🐛 Fixed +- Fixed ValueError in effects_per_component when all periodic effects are scalars/NaN by explicitly creating mode-specific templates (via _create_template_for_mode) with correct dimensions + +### 👷 Development +- Converted all remaining numpy style docstrings to google style + +--- + +## [3.1.0] - 2025-10-19 + +**Summary**: This release adds faceting and animation support for multidimensional plots and redesigns the documentation website. Plotting results across scenarios or periods is now significantly simpler (Plotly only). + +If upgrading from v2.x, see the [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/) and [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0). + +### ✨ Added +- **Faceting and animation for multidimensional plots**: All plotting methods now support `facet_by` and `animate_by` parameters to create subplot grids and animations from multidimensional data (scenarios, periods, etc.). *Plotly only.* +- **Flexible data selection with `select` parameter**: Select data using single values, lists, slices, or index arrays for precise control over what gets plotted +- **Heatmap fill control**: New `fill` parameter in heatmap methods controls how missing values are filled after reshaping (`'ffill'` or `'bfill'`) +- **Smart line styling for mixed variables**: Area plots now automatically style variables containing both positive and negative values with dashed lines, while stacking purely positive or negative variables + +### ♻️ Changed +- **Breaking: Selection behavior**: Plotting methods no longer automatically select the first value for non-time dimensions. Use the `select` parameter for explicit selection of scenarios, periods, or other dimensions +- **Better error messages**: Enhanced error messages when using Matplotlib with multidimensional data, with clearer guidance on dimension requirements and suggestions to use Plotly +- **Improved examples**: Enhanced `scenario_example.py` with better demonstration of new features +- **Robust validation**: Improved dimension validation in `plot_heatmap()` with clearer error messages + +### 🗑️ Deprecated +- **`indexer` parameter**: Use the new `select` parameter instead. The `indexer` parameter will be removed in v4.0.0 +- **`heatmap_timeframes` and `heatmap_timesteps_per_frame` parameters**: Use the new `reshape_time=(timeframes, timesteps_per_frame)` parameter instead in heatmap plotting methods +- **`color_map` parameter**: Use the new `colors` parameter instead in heatmap plotting methods +### 🐛 Fixed +- Fixed cryptic errors when working with empty buses by adding proper validation +- Added early validation for non-existent periods when using linked periods with tuples + +### 📝 Documentation +- **Redesigned documentation website** with custom css + +### 👷 Development +- Renamed internal `_apply_indexer_to_data()` to `_apply_selection_to_data()` for consistency with new API naming + +--- + +## [3.0.3] - 2025-10-16 +**Summary**: Hotfixing new plotting parameter `style`. Continue to use `mode`. + +**Note**: If upgrading from v2.x, see the [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/) and [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0). + +### 🐛 Fixed +- Reverted breaking change from v3.0.0: continue to use `mode parameter in plotting instead of new `style` +- Renamed new `mode` parameter in plotting methods to `unit_type` + +### 📝 Docs +- Updated Migration Guide and added missing entries. +- Improved Changelog of v3.0.0 + +--- + +## [3.0.2] - 2025-10-15 +**Summary**: This is a follow-up release to **[v3.0.0](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0)**, improving the documentation. + +**Note**: If upgrading from v2.x, see the [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/) and [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0). + +### 📝 Docs +- Update the Readme +- Add a project roadmap to the docs +- Change Development status to "Production/Stable" +- Regroup parts in docs + +--- + +## [3.0.1] - 2025-10-14 +**Summary**: This is a follow-up release to **[v3.0.0](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0)**, adding a Migration Guide and bugfixing the docs. + +**Note**: If upgrading from v2.x, see the [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/) and [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0). + +### 📝 Docs +- Fixed deployed docs +- Added Migration Guide for flixopt 3 + +### 👷 Development +- Added missing type hints + +--- + +## [3.0.0] - 2025-10-13 +**Summary**: This release introduces new model dimensions (periods and scenarios) for multi-period investments and stochastic modeling, along with a redesigned effect sharing system and enhanced I/O capabilities. + +**Note**: If upgrading from v2.x, see the [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/) and [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0). + +### ✨ Added + +**New model dimensions:** + +- **Period dimension**: Enables multi-period investment modeling with distinct decisions in each period for transformation pathway optimization +- **Scenario dimension**: Supports stochastic modeling with weighted scenarios for robust decision-making under uncertainty (demand, prices, weather) + - Control variable independence across scenarios via `scenario_independent_sizes` and `scenario_independent_flow_rates` parameters + - By default, investment sizes are shared across scenarios while flow rates vary per scenario + +**Redesigned effect sharing system:** + +Effects now use intuitive `share_from_*` syntax that clearly shows contribution sources: + +```python +costs = fx.Effect('costs', '€', 'Total costs', + share_from_temporal={'CO2': 0.2}, # From temporal effects + share_from_periodic={'land': 100}) # From periodic effects +``` + +This replaces `specific_share_to_other_effects_*` parameters and inverts the direction for clearer relationships. + +**Enhanced I/O and data handling:** + +- NetCDF/JSON serialization for all Interface objects and FlowSystem with round-trip support +- FlowSystem manipulation: `sel()`, `isel()`, `resample()`, `copy()`, `__eq__()` methods +- Direct access to FlowSystem from results without manual restoring (lazily loaded) +- New `FlowResults` class and precomputed DataArrays for sizes/flow_rates/flow_hours +- `effects_per_component` dataset for component impact evaluation, including all indirect effects through effect shares + +**Other additions:** + +- Balanced storage - charging and discharging sizes can be forced equal via `balanced` parameter +- New Storage parameters: `relative_minimum_final_charge_state` and `relative_maximum_final_charge_state` for final state control +- Improved filter methods in results +- Example for 2-stage investment decisions leveraging FlowSystem resampling + +### 💥 Breaking Changes + +**API and Behavior Changes:** + +- **Effect system redesigned** (no deprecation): + - **Terminology changes**: Effect domains renamed for clarity: `operation` → `temporal`, `invest`/`investment` → `periodic` + - **Sharing system**: The old `specific_share_to_other_effects_*` parameters were completely replaced with the new `share_from_temporal` and `share_from_periodic` syntax (see 🔥 Removed section) +- **FlowSystem independence**: FlowSystems cannot be shared across multiple Calculations anymore. A copy of the FlowSystem is created instead, making every Calculation independent. Each Subcalculation in `SegmentedCalculation` now has its own distinct `FlowSystem` object +- **Bus and Effect object assignment**: Direct assignment of Bus/Effect objects is no longer supported. Use labels (strings) instead: + - `Flow.bus` must receive a string label, not a Bus object + - Effect shares must use effect labels (strings) in dictionaries, not Effect objects +- **Logging defaults** (from v2.2.0): Console and file logging are now disabled by default. Enable explicitly with `CONFIG.Logging.console = True` and `CONFIG.apply()` + +**Class and Method Renaming:** + +- Renamed class `SystemModel` to `FlowSystemModel` +- Renamed class `Model` to `Submodel` +- Renamed `mode` parameter in plotting methods to `style` +- `Calculation.do_modeling()` now returns the `Calculation` object instead of its `linopy.Model`. Callers that previously accessed the linopy model directly should now use `calculation.do_modeling().model` instead of `calculation.do_modeling()` + +**Variable Renaming in Results:** + +- Investment binary variable: `is_invested` → `invested` in `InvestmentModel` +- Switch tracking variables in `OnOffModel`: + - `switch_on` → `switch|on` + - `switch_off` → `switch|off` + - `switch_on_nr` → `switch|count` +- Effect submodel variables (following terminology changes): + - `Effect(invest)|total` → `Effect(periodic)` + - `Effect(operation)|total` → `Effect(temporal)` + - `Effect(operation)|total_per_timestep` → `Effect(temporal)|per_timestep` + - `Effect|total` → `Effect` + +**Data Structure Changes:** + +- `relative_minimum_charge_state` and `relative_maximum_charge_state` don't have an extra timestep anymore. Use the new `relative_minimum_final_charge_state` and `relative_maximum_final_charge_state` parameters for final state control + +### ♻️ Changed + +- Type system overhaul - added clear separation between temporal and non-temporal data throughout codebase for better clarity +- Enhanced FlowSystem interface with improved `__repr__()` and `__str__()` methods +- Improved Model Structure - Views and organisation is now divided into: + - Model: The main Model (linopy.Model) that is used to create and store the variables and constraints for the FlowSystem. + - Submodel: The base class for all submodels. Each is a subset of the Model, for simpler access and clearer code. +- Made docstrings in `config.py` more compact and easier to read +- Improved format handling in configuration module +- Enhanced console output to support both `stdout` and `stderr` stream selection +- Added `show_logger_name` parameter to `CONFIG.Logging` for displaying logger names in messages + +### 🗑️ Deprecated + +- The `agg_group` and `agg_weight` parameters of `TimeSeriesData` are deprecated and will be removed in a future version. Use `aggregation_group` and `aggregation_weight` instead. +- The `active_timesteps` parameter of `Calculation` is deprecated and will be removed in a future version. Use the new `sel(time=...)` method on the FlowSystem instead. +- **InvestParameters** parameters renamed for improved clarity around investment and retirement effects: + - `fix_effects` → `effects_of_investment` + - `specific_effects` → `effects_of_investment_per_size` + - `divest_effects` → `effects_of_retirement` + - `piecewise_effects` → `piecewise_effects_of_investment` +- **Effect** parameters renamed: + - `minimum_investment` → `minimum_periodic` + - `maximum_investment` → `maximum_periodic` + - `minimum_operation` → `minimum_temporal` + - `maximum_operation` → `maximum_temporal` + - `minimum_operation_per_hour` → `minimum_per_hour` + - `maximum_operation_per_hour` → `maximum_per_hour` +- **Component** parameters renamed: + - `Source.source` → `Source.outputs` + - `Sink.sink` → `Sink.inputs` + - `SourceAndSink.source` → `SourceAndSink.outputs` + - `SourceAndSink.sink` → `SourceAndSink.inputs` + - `SourceAndSink.prevent_simultaneous_sink_and_source` → `SourceAndSink.prevent_simultaneous_flow_rates` + +### 🔥 Removed + +- **Effect share parameters**: The old `specific_share_to_other_effects_*` parameters were replaced WITHOUT DEPRECATION + - `specific_share_to_other_effects_operation` → `share_from_temporal` (with inverted direction) + - `specific_share_to_other_effects_invest` → `share_from_periodic` (with inverted direction) + +### 🐛 Fixed + +- Enhanced NetCDF I/O with proper attribute preservation for DataArrays +- Improved error handling and validation in serialization processes +- Better type consistency across all framework components +- Added extra validation in `config.py` to improve error handling + +### 📝 Docs + +- Reorganized mathematical notation docs: moved to lowercase `mathematical-notation/` with subdirectories (`elements/`, `features/`, `modeling-patterns/`) +- Added comprehensive documentation pages: `dimensions.md` (time/period/scenario), `effects-penalty-objective.md`, modeling patterns +- Enhanced all element pages with implementation details, cross-references, and "See Also" sections +- Rewrote README and landing page with clearer vision, roadmap, and universal applicability emphasis +- Removed deprecated `docs/SUMMARY.md`, updated `mkdocs.yml` for new structure +- Tightened docstrings in core modules with better cross-referencing +- Added recipes section to docs + +### 🚧 Known Issues + +- IO for single Interfaces/Elements to Datasets might not work properly if the Interface/Element is not part of a fully transformed and connected FlowSystem. This arises from Numeric Data not being stored as xr.DataArray by the user. To avoid this, always use the `to_dataset()` on Elements inside a FlowSystem that's connected and transformed. + +### 👷 Development + +- **Centralized deprecation pattern**: Added `_handle_deprecated_kwarg()` helper method to `Interface` base class that provides reusable deprecation handling with consistent warnings, conflict detection, and optional value transformation. Applied across 5 classes (InvestParameters, Source, Sink, SourceAndSink, Effect) reducing deprecation boilerplate by 72%. +- FlowSystem data management simplified - removed `time_series_collection` pattern in favor of direct timestep properties +- Change modeling hierarchy to allow for more flexibility in future development. This leads to minimal changes in the access and creation of Submodels and their variables. +- Added new module `.modeling` that contains modeling primitives and utilities +- Clearer separation between the main Model and "Submodels" +- Improved access to the Submodels and their variables, constraints and submodels +- Added `__repr__()` for Submodels to easily inspect its content +- Enhanced data handling methods + - `fit_to_model_coords()` method for data alignment + - `fit_effects_to_model_coords()` method for effect data processing + - `connect_and_transform()` method replacing several operations +- **Testing improvements**: Eliminated warnings during test execution + - Updated deprecated code patterns in tests and examples (e.g., `sink`/`source` → `inputs`/`outputs`, `'H'` → `'h'` frequency) + - Refactored plotting logic to handle test environments explicitly with non-interactive backends + - Added comprehensive warning filters in `__init__.py` and `pyproject.toml` to suppress third-party library warnings + - Improved test fixtures with proper figure cleanup to prevent memory leaks + - Enhanced backend detection and handling in `plotting.py` for both Matplotlib and Plotly + - Always run dependent tests in order + +--- + +## [2.2.0] - 2025-10-11 +**Summary:** This release is a Configuration and Logging management release. + +### ✨ Added +- Added `CONFIG.reset()` method to restore configuration to default values +- Added configurable log file rotation settings: `CONFIG.Logging.max_file_size` and `CONFIG.Logging.backup_count` +- Added configurable log format settings: `CONFIG.Logging.date_format` and `CONFIG.Logging.format` +- Added configurable console settings: `CONFIG.Logging.console_width` and `CONFIG.Logging.show_path` +- Added `CONFIG.Logging.Colors` nested class for customizable log level colors using ANSI escape codes (works with both standard and Rich handlers) +- All examples now enable console logging to demonstrate proper logging usage +- Console logging now outputs to `sys.stdout` instead of `sys.stderr` for better compatibility with output redirection + +### 💥 Breaking Changes +- Console logging is now disabled by default (`CONFIG.Logging.console = False`). Enable it explicitly in your scripts with `CONFIG.Logging.console = True` and `CONFIG.apply()` +- File logging is now disabled by default (`CONFIG.Logging.file = None`). Set a file path to enable file logging + +### ♻️ Changed +- Logging and Configuration management changed +- Improved default logging colors: DEBUG is now gray (`\033[90m`) for de-emphasized messages, INFO uses terminal default color (`\033[0m`) for clean output + +### 🗑️ Deprecated +- `change_logging_level()` function is now deprecated in favor of `CONFIG.Logging.level` and `CONFIG.apply()`. Will be removed in version 3.0.0. + +### 🔥 Removed +- Removed unused `config.merge_configs` function from configuration module + +### 👷 Development +- Greatly expanded test coverage for `config.py` module +- Added `@pytest.mark.xdist_group` to `TestConfigModule` tests to prevent global config interference + +--- + +## [2.1.11] - 2025-10-05 +**Summary:** Important bugfix in `Storage` leading to wrong results due to incorrect discharge losses. + +### ♻️ Changed +- Using `h5netcdf` instead of `netCDF4` for dataset I/O operations. This follows the update in `xarray==2025.09.01` + +### 🐛 Fixed +- Fix `charge_state` Constraint in `Storage` leading to incorrect losses in discharge and therefore incorrect charge states and discharge values. + +### 📦 Dependencies +- Updated `renovate.config` to treat CalVer packages (xarray and dask) with more care +- Updated packaging configuration + +--- + +## [2.1.10] - 2025-09-29 +**Summary:** This release is a Documentation and Development release. + +### 📝 Docs +- Improved CHANGELOG.md formatting by adding better categories and formating by Gitmoji. +- Added a script to extract the release notes from the CHANGELOG.md file for better organized documentation. + +### 👷 Development +- Improved `renovate.config` +- Sped up CI by not running examples in every run and using `pytest-xdist` + +--- ## [2.1.9] - 2025-09-23 -Small Bugfix which was supposed to be fixed in 2.1.8 -### Fixed -- Fix error handling in network visualization if networkx is not installed. +**Summary:** Small bugfix release addressing network visualization error handling. + +### 🐛 Fixed +- Fix error handling in network visualization if `networkx` is not installed +--- ## [2.1.8] - 2025-09-22 -This release focuses on code quality improvements, enhanced documentation, and bug fixes for heat pump components and visualization features. -### Added +**Summary:** Code quality improvements, enhanced documentation, and bug fixes for heat pump components and visualization features. + +### ✨ Added - Extra Check for HeatPumpWithSource.COP to be strictly > 1 to avoid division by zero - Apply deterministic color assignment by using sorted() in `plotting.py` - Add missing args in docstrings in `plotting.py`, `solvers.py`, and `core.py`. -### Changed +### ♻️ Changed - Greatly improved docstrings and documentation of all public classes - Make path handling to be gentle about missing .html suffix in `plotting.py` - Default for `relative_losses` in `Transmission` is now 0 instead of None @@ -144,27 +529,34 @@ This release focuses on code quality improvements, enhanced documentation, and b - Fix some docstrings in plotting.py - Change assertions to raise Exceptions in `plotting.py` -### Fixed -- Fix color scheme selection in network_app; color pickers now update when a scheme is selected. -- Fix error handling in network visualization if networkx is not installed. -- Fix broken links in docs. -- Fix COP getter and setter of `HeatPumpWithSource` returning and setting wrong conversion factors. +### 🐛 Fixed + +**Core Components:** +- Fix COP getter and setter of `HeatPumpWithSource` returning and setting wrong conversion factors - Fix custom compression levels in `io.save_dataset_to_netcdf` -- Fix `total_max` did not work when total min was not used. +- Fix `total_max` did not work when total min was not used + +**Visualization:** +- Fix color scheme selection in network_app; color pickers now update when a scheme is selected + +### 📝 Docs +- Fix broken links in docs +- Fix some docstrings in plotting.py -### *Development* +### 👷 Development - Pin dev dependencies to specific versions - Improve CI workflows to run faster and smarter +--- + ## [2.1.7] - 2025-09-13 -This update is a maintenance release to improve Code Quality, CI and update the dependencies. -There are no changes or new features. +**Summary:** Maintenance release to improve Code Quality, CI and update the dependencies. There are no changes or new features. -### Added -- Added __version__ to flixopt +### ✨ Added +- Added `__version__` to flixopt -### *Development* +### 👷 Development - ruff format the whole Codebase - Added renovate config - Added pre-commit @@ -173,108 +565,144 @@ There are no changes or new features. - Updated Dependencies - Updated Issue Templates +--- ## [2.1.6] - 2025-09-02 -### Changed -- `Sink`, `Source` and `SourceAndSink` now accept multiple `flows` as `inputs` and `outputs` instead of just one. This enables to model more use cases using these classes. [[#291](https://github.com/flixOpt/flixopt/pull/291) by [@FBumann](https://github.com/FBumann)] -- Further, both `Sink` and `Source` now have a `prevent_simultaneous_flow_rates` argument to prevent simultaneous flow rates of more than one of their Flows. [[#291](https://github.com/flixOpt/flixopt/pull/291) by [@FBumann](https://github.com/FBumann)] +**Summary:** Enhanced Sink/Source components with multi-flow support and new interactive network visualization. + +### ✨ Added +- **Network Visualization**: Added `FlowSystem.start_network_app()` and `FlowSystem.stop_network_app()` to easily visualize the network structure of a flow system in an interactive Dash web app + - *Note: This is still experimental and might change in the future* + +### ♻️ Changed +- **Multi-Flow Support**: `Sink`, `Source`, and `SourceAndSink` now accept multiple `flows` as `inputs` and `outputs` instead of just one. This enables modeling more use cases with these classes +- **Flow Control**: Both `Sink` and `Source` now have a `prevent_simultaneous_flow_rates` argument to prevent simultaneous flow rates of more than one of their flows -### Added -- Added `FlowSystem.start_network_app()` and `FlowSystem.stop_network_app()` to easily visualize the network structure of a flow system in an interactive dash web app. This is still experimental and might change in the future. [[#293](https://github.com/flixOpt/flixopt/pull/293) by [@FBumann](https://github.com/FBumann)] +### 🗑️ Deprecated +- For the classes `Sink`, `Source` and `SourceAndSink`: `.sink`, `.source` and `.prevent_simultaneous_sink_and_source` are deprecated in favor of the new arguments `inputs`, `outputs` and `prevent_simultaneous_flow_rates` -### Deprecated -- For the classes `Sink`, `Source` and `SourceAndSink`: `.sink`, `.source` and `.prevent_simultaneous_sink_and_source` are deprecated in favor of the new arguments `inputs`, `outputs` and `prevent_simultaneous_flow_rates`. [[#291](https://github.com/flixOpt/flixopt/pull/291) by [@FBumann](https://github.com/FBumann)] +### 🐛 Fixed +- Fixed testing issue with new `linopy` version 0.5.6 -### Fixed -- Fixed testing issue with new `linopy` version 0.5.6 [[#296](https://github.com/flixOpt/flixopt/pull/296) by [@FBumann](https://github.com/FBumann)] +### 👷 Development +- Added dependency "nbformat>=4.2.0" to dev dependencies to resolve issue with plotly CI + +--- ## [2.1.5] - 2025-07-08 -### Fixed +### 🐛 Fixed - Fixed Docs deployment +--- + ## [2.1.4] - 2025-07-08 -### Fixed +### 🐛 Fixed - Fixing release notes of 2.1.3, as well as documentation build. +--- ## [2.1.3] - 2025-07-08 -### Fixed +### 🐛 Fixed - Using `Effect.maximum_operation_per_hour` raised an error, needing an extra timestep. This has been fixed thanks to @PRse4. +--- + ## [2.1.2] - 2025-06-14 -### Fixed -- Storage losses per hour where not calculated correctly, as mentioned by @brokenwings01. This might have lead to issues with modeling large losses and long timesteps. - - Old implementation: $c(\text{t}_{i}) \cdot (1-\dot{\text{c}}_\text{rel,loss}(\text{t}_i)) \cdot \Delta \text{t}_{i}$ - - Correct implementation: $c(\text{t}_{i}) \cdot (1-\dot{\text{c}}_\text{rel,loss}(\text{t}_i)) ^{\Delta \text{t}_{i}}$ +### 🐛 Fixed +- Storage losses per hour were not calculated correctly, as mentioned by @brokenwings01. This might have led to issues when modeling large losses and long timesteps. + - Old implementation: $c(\text{t}_{i}) \cdot (1-\dot{\text{c}}_\text{rel,loss}(\text{t}_i)) \cdot \Delta \text{t}_{i}$ + - Correct implementation: $c(\text{t}_{i}) \cdot (1-\dot{\text{c}}_\text{rel,loss}(\text{t}_i)) ^{\Delta \text{t}_{i}}$ -### Known issues +### 🚧 Known Issues - Just to mention: Plotly >= 6 may raise errors if "nbformat" is not installed. We pinned plotly to <6, but this may be fixed in the future. -## [2.1.1] - 2025-05-08 +--- -### Fixed -- Fixed bug in the `_ElementResults.constraints` not returning the constraints but rather the variables +## [2.1.1] - 2025-05-08 -### Changed +### ♻️ Changed - Improved docstring and tests +### 🐛 Fixed +- Fixed bug in the `_ElementResults.constraints` not returning the constraints but rather the variables + +--- ## [2.1.0] - 2025-04-11 -### Added +### ✨ Added - Python 3.13 support added - Logger warning if relative_minimum is used without on_off_parameters in Flow - Greatly improved internal testing infrastructure by leveraging linopy's testing framework -### Fixed +### 💥 Breaking Changes +- Restructured the modeling of the On/Off state of Flows or Components + - Variable renaming: `...|consecutive_on_hours` → `...|ConsecutiveOn|hours` + - Variable renaming: `...|consecutive_off_hours` → `...|ConsecutiveOff|hours` + - Constraint renaming: `...|consecutive_on_hours_con1` → `...|ConsecutiveOn|con1` + - Similar pattern for all consecutive on/off constraints + +### 🐛 Fixed - Fixed the lower bound of `flow_rate` when using optional investments without OnOffParameters - Fixed bug that prevented divest effects from working - Added lower bounds of 0 to two unbounded vars (numerical improvement) -### Changed -- **BREAKING**: Restructured the modeling of the On/Off state of Flows or Components - - Variable renaming: `...|consecutive_on_hours` → `...|ConsecutiveOn|hours` - - Variable renaming: `...|consecutive_off_hours` → `...|ConsecutiveOff|hours` - - Constraint renaming: `...|consecutive_on_hours_con1` → `...|ConsecutiveOn|con1` - - Similar pattern for all consecutive on/off constraints +--- ## [2.0.1] - 2025-04-10 -### Added +### ✨ Added - Logger warning if relative_minimum is used without on_off_parameters in Flow -### Fixed +### 🐛 Fixed - Replace "|" with "__" in filenames when saving figures (Windows compatibility) - Fixed bug that prevented the load factor from working without InvestmentParameters ## [2.0.0] - 2025-03-29 -### Changed -- **BREAKING**: Complete migration from Pyomo to Linopy optimization framework -- **BREAKING**: Redesigned data handling to rely on xarray.Dataset throughout the package -- **BREAKING**: Framework renamed from flixOpt to flixopt (`import flixopt as fx`) -- **BREAKING**: Results handling completely redesigned with new `CalculationResults` class +**Summary:** 💥 **MAJOR RELEASE** - Complete framework migration from Pyomo to Linopy with redesigned architecture. -### Added +### ✨ Added + +**Model Capabilities:** - Full model serialization support - save and restore unsolved Models - Enhanced model documentation with YAML export containing human-readable mathematical formulations - Extend flixopt models with native linopy language support - Full Model Export/Import capabilities via linopy.Model + +**Results & Data:** - Unified solution exploration through `Calculation.results` attribute - Compression support for result files - `to_netcdf/from_netcdf` methods for FlowSystem and core components - xarray integration for TimeSeries with improved datatypes support -- Google Style Docstrings throughout the codebase -### Fixed +### 💥 Breaking Changes + +**Framework Migration:** +- **Optimization Engine**: Complete migration from Pyomo to Linopy optimization framework +- **Package Import**: Framework renamed from flixOpt to flixopt (`import flixopt as fx`) +- **Data Architecture**: Redesigned data handling to rely on xarray.Dataset throughout the package +- **Results System**: Results handling completely redesigned with new `CalculationResults` class + +**Variable Structure:** +- Restructured the modeling of the On/Off state of Flows or Components + - Variable renaming: `...|consecutive_on_hours` → `...|ConsecutiveOn|hours` + - Variable renaming: `...|consecutive_off_hours` → `...|ConsecutiveOff|hours` + - Constraint renaming: `...|consecutive_on_hours_con1` → `...|ConsecutiveOn|con1` + - Similar pattern for all consecutive on/off constraints + +### 🔥 Removed +- **Pyomo dependency** (replaced by linopy) +- **Period concepts** in time management (simplified to timesteps) + +### 🐛 Fixed - Improved infeasible model detection and reporting - Enhanced time series management and serialization - Reduced file size through improved compression -### Removed -- **BREAKING**: Pyomo dependency (replaced by linopy) -- Period concepts in time management (simplified to timesteps) +### 📝 Docs +- Google Style Docstrings throughout the codebase diff --git a/CONTRIBUTE.md b/CONTRIBUTE.md new file mode 100644 index 000000000..5c73ba04b --- /dev/null +++ b/CONTRIBUTE.md @@ -0,0 +1,168 @@ +# Contributing to FlixOpt + +We warmly welcome contributions from the community! Whether you're fixing bugs, adding features, improving documentation, or sharing examples, your contributions are valuable. + +## Ways to Contribute + +### 🐛 Report Issues +Found a bug or have a feature request? Please [open an issue](https://github.com/flixOpt/flixopt/issues) on GitHub. + +When reporting issues, please include: +- A clear description of the problem +- Steps to reproduce the issue +- Expected vs. actual behavior +- Your environment (OS, Python version, FlixOpt version) +- Minimal code example if applicable + +### 💡 Share Examples +Help others learn FlixOpt by contributing examples: +- Real-world use cases +- Tutorial notebooks +- Integration examples with other tools +- Add them to the `examples/` directory + +### 📖 Improve Documentation +Documentation improvements are always welcome: +- Fix typos or clarify existing docs +- Add missing documentation +- Translate documentation +- Improve code comments + +### 🔧 Submit Code Contributions +Ready to contribute code? Great! See the sections below for setup and guidelines. + +--- + +## Development Setup + +### Getting Started +1. Fork and clone the repository: + ```bash + git clone https://github.com/flixOpt/flixopt.git + cd flixopt + ``` + +2. Install development dependencies: + ```bash + pip install -e ".[full, dev]" + ``` + +3. Set up pre-commit hooks (one-time setup): + ```bash + pre-commit install + ``` + +4. Verify your setup: + ```bash + pytest + ``` + +### Working with Documentation +FlixOpt uses [mkdocs](https://www.mkdocs.org/) to generate documentation. + +To work on documentation: +```bash +pip install -e ".[docs]" +mkdocs serve +``` +Then navigate to http://127.0.0.1:8000/ + +--- + +## Code Quality Standards + +### Automated Checks +We use [Ruff](https://github.com/astral-sh/ruff) for linting and formatting. After the one-time setup above, **code quality checks run automatically on every commit**. + +### Manual Checks +To run checks manually: +- `ruff check --fix .` - Check and fix linting issues +- `ruff format .` - Format code +- `pre-commit run --all-files` - Run all pre-commit checks + +### Testing +All tests are located in the `tests/` directory with a flat structure: +- `test_component.py` - Component tests +- `test_flow.py` - Flow tests +- `test_storage.py` - Storage tests +- etc. + +#### Running Tests +- `pytest` - Run the full test suite (excluding examples by default) +- `pytest tests/test_component.py` - Run a specific test file +- `pytest tests/test_component.py::TestClassName` - Run a specific test class +- `pytest tests/test_component.py::TestClassName::test_method` - Run a specific test +- `pytest -m slow` - Run only slow tests +- `pytest -m examples` - Run example tests (normally skipped) +- `pytest -k "keyword"` - Run tests matching a keyword + +#### Common Test Patterns +The `tests/conftest.py` file provides shared fixtures: +- `solver_fixture` - Parameterized solver fixture (HiGHS, Gurobi) +- `highs_solver` - HiGHS solver instance +- Coordinate configuration fixtures for timesteps, periods, scenarios + +Use these fixtures by adding them as function parameters: +```python +def test_my_feature(solver_fixture): + # solver_fixture is automatically provided by pytest + model = fx.FlowSystem(...) + model.solve(solver_fixture) +``` + +#### Testing Guidelines +- Write tests for all new functionality +- Ensure all tests pass before submitting a PR +- Aim for 100% test coverage for new code +- Use descriptive test names that explain what's being tested +- Add the `@pytest.mark.slow` decorator for tests that take >5 seconds + +### Coding Guidelines +- Follow [PEP 8](https://pep8.org/) style guidelines +- Write clear, self-documenting code with helpful comments +- Include type hints for function signatures +- Create or update tests for new functionality +- Aim for 100% test coverage for new code + +--- + +## Workflow + +### Branches & Pull Requests +1. Create a feature branch from `main`: + ```bash + git checkout -b feature/your-feature-name + ``` + +2. Make your changes and commit them with clear messages + +3. Push your branch and open a Pull Request + +4. Ensure all CI checks pass + +### Branch Naming +- Features: `feature/feature-name` +- Bug fixes: `fix/bug-description` +- Documentation: `docs/what-changed` + +### Commit Messages +- Use clear, descriptive commit messages +- Start with a verb (Add, Fix, Update, Remove, etc.) +- Keep the first line under 72 characters + +--- + +## Releases + +We follow **Semantic Versioning** (MAJOR.MINOR.PATCH). Releases are created manually from the `main` branch by maintainers. + +--- + +## Questions? + +If you have questions or need help, feel free to: +- Open a discussion on GitHub +- Ask in an issue +- Reach out to the maintainers + +Thank you for contributing to FlixOpt! diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 000000000..383cbef76 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,26 @@ +# Include essential files from source distribution +include LICENSE +include README.md +include CHANGELOG.md +include pyproject.toml + +# Include package source and data +recursive-include flixopt *.py + +# Exclude everything else +global-exclude *.pyc *.pyo __pycache__ +prune .github +prune docs +prune examples +prune tests +prune pics +prune scripts +prune build +prune dist +prune .venv +prune venv +exclude .gitignore +exclude .pre-commit-config.yaml +exclude renovate.json +exclude mkdocs.yml +exclude test_package.sh diff --git a/README.md b/README.md index edb77e74d..0a90dcb33 100644 --- a/README.md +++ b/README.md @@ -3,89 +3,190 @@ [![Documentation](https://img.shields.io/badge/docs-latest-brightgreen.svg)](https://flixopt.github.io/flixopt/latest/) [![Build Status](https://github.com/flixOpt/flixopt/actions/workflows/python-app.yaml/badge.svg)](https://github.com/flixOpt/flixopt/actions/workflows/python-app.yaml) [![PyPI version](https://img.shields.io/pypi/v/flixopt)](https://pypi.org/project/flixopt/) +[![PyPI status](https://img.shields.io/pypi/status/flixopt.svg)](https://pypi.org/project/flixopt/) [![Python Versions](https://img.shields.io/pypi/pyversions/flixopt.svg)](https://pypi.org/project/flixopt/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![PyPI downloads](https://img.shields.io/pypi/dm/flixopt)](https://pypi.org/project/flixopt/) +[![GitHub last commit](https://img.shields.io/github/last-commit/flixOpt/flixopt)](https://github.com/flixOpt/flixopt/commits/main) +[![GitHub issues](https://img.shields.io/github/issues/flixOpt/flixopt)](https://github.com/flixOpt/flixopt/issues) +[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/flixOpt/flixopt/main.svg)](https://results.pre-commit.ci/latest/github/flixOpt/flixopt/main) +[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) +[![Powered by linopy](https://img.shields.io/badge/powered%20by-linopy-blue)](https://github.com/PyPSA/linopy/) +[![Powered by xarray](https://img.shields.io/badge/powered%20by-xarray-blue)](https://xarray.dev/) +[![DOI](https://img.shields.io/badge/DOI-10.18086%2Feurosun.2022.04.07-blue)](https://doi.org/10.18086/eurosun.2022.04.07) +[![GitHub stars](https://img.shields.io/github/stars/flixOpt/flixopt?style=social)](https://github.com/flixOpt/flixopt/stargazers) --- -## 🚀 Purpose +**FlixOpt is a Python framework for optimizing energy and material flow systems** - from district heating networks to industrial production lines, from renewable energy portfolios to supply chain logistics. -**flixopt** is a Python-based optimization framework designed to tackle energy and material flow problems using mixed-integer linear programming (MILP). +**Start simple, scale complex:** Build a working optimization model in minutes, then progressively add detail - multi-period investments, stochastic scenarios, custom constraints - without rewriting your code. -**flixopt** bridges the gap between high-level energy systems models like [FINE](https://github.com/FZJ-IEK3-VSA/FINE) used for design and (multi-period) investment decisions and low-level dispatch optimization tools used for operation decisions. +--- + +## 🚀 Quick Start + +```bash +pip install flixopt +``` + +That's it! FlixOpt comes with the [HiGHS](https://highs.dev/) solver included. You're ready to optimize. + +**The basic workflow:** -**flixopt** leverages the fast and efficient [linopy](https://github.com/PyPSA/linopy/) for the mathematical modeling and [xarray](https://github.com/pydata/xarray) for data handling. +```python +import flixopt as fx -**flixopt** provides a user-friendly interface with options for advanced users. +# 1. Define your system structure +flow_system = fx.FlowSystem(timesteps) +flow_system.add_elements(buses, components, effects) -It was originally developed by [TU Dresden](https://github.com/gewv-tu-dresden) as part of the SMARTBIOGRID project, funded by the German Federal Ministry for Economic Affairs and Energy (FKZ: 03KB159B). Building on the Matlab-based flixOptMat framework (developed in the FAKS project), FlixOpt also incorporates concepts from [oemof/solph](https://github.com/oemof/oemof-solph). +# 2. Create and solve +calculation = fx.FullCalculation("MyModel", flow_system) +calculation.solve() + +# 3. Analyze results +calculation.results.solution +``` + +**Get started with real examples:** +- 📚 [Full Documentation](https://flixopt.github.io/flixopt/latest/) +- 💡 [Examples Gallery](https://flixopt.github.io/flixopt/latest/examples/) - Complete working examples from simple to complex +- 🔧 [API Reference](https://flixopt.github.io/flixopt/latest/api-reference/) --- -## 🌟 Key Features +## 🌟 Why FlixOpt? + +### Progressive Enhancement - Your Model Grows With You + +**Start simple:** +```python +# Basic single-period model +flow_system = fx.FlowSystem(timesteps) +boiler = fx.Boiler("Boiler", eta=0.9, ...) +``` + +**Add complexity incrementally:** +- **Investment decisions** → Add `InvestParameters` to components +- **Multi-period planning** → Add `periods` dimension to FlowSystem +- **Uncertainty modeling** → Add `scenarios` dimension with probabilities +- **Custom constraints** → Extend with native linopy syntax -- **High-level Interface** with low-level control - - User-friendly interface for defining flow systems - - Pre-defined components like CHP, Heat Pump, Cooling Tower, etc. - - Fine-grained control for advanced configurations +**No refactoring required.** Your component definitions stay the same - periods, scenarios, and features are added as dimensions and parameters. -- **Investment Optimization** - - Combined dispatch and investment optimization - - Size optimization and discrete investment decisions - - Combined with On/Off variables and constraints +→ [Learn more about multi-period and stochastic modeling](https://flixopt.github.io/flixopt/latest/user-guide/mathematical-notation/dimensions/) -- **Effects, not only Costs --> Multi-criteria Optimization** - - flixopt abstracts costs as so called 'Effects'. This allows to model costs, CO2-emissions, primary-energy-demand or area-demand at the same time. - - Effects can interact with each other(e.g., specific CO2 costs) - - Any of these `Effects` can be used as the optimization objective. - - A **Weigted Sum** of Effects can be used as the optimization objective. - - Every Effect can be constrained ($\epsilon$-constraint method). +### For Everyone -- **Calculation Modes** - - **Full** - Solve the model with highest accuracy and computational requirements. - - **Segmented** - Speed up solving by using a rolling horizon. - - **Aggregated** - Speed up solving by identifying typical periods using [TSAM](https://github.com/FZJ-IEK3-VSA/tsam). Suitable for large models. +- **Beginners:** High-level components that "just work" +- **Experts:** Full access to modify models with linopy +- **Researchers:** Quick prototyping with customization options +- **Engineers:** Reliable, tested components without black boxes +- **Students:** Clear, Pythonic interfaces for learning optimization + +### Key Features + +**Multi-criteria optimization:** Model costs, emissions, resource use - any custom metric. Optimize single objectives or use weighted combinations and ε-constraints. +→ [Effects documentation](https://flixopt.github.io/flixopt/latest/user-guide/mathematical-notation/effects-penalty-objective/) + +**Performance at any scale:** Choose calculation modes without changing your model - Full, Segmented, or Aggregated (using [TSAM](https://github.com/FZJ-IEK3-VSA/tsam)). +→ [Calculation modes](https://flixopt.github.io/flixopt/latest/api-reference/calculation/) + +**Built for reproducibility:** Self-contained NetCDF result files with complete model information. Load results months later - everything is preserved. +→ [Results documentation](https://flixopt.github.io/flixopt/latest/api-reference/results/) + +**Flexible data operations:** Transform FlowSystems with xarray-style operations (`sel()`, `resample()`) for multi-stage optimization. --- -## 📦 Installation +## 🎯 What is FlixOpt? + +### A General-Purpose Flow Optimization Framework + +FlixOpt models **any system involving flows and conversions:** + +- **Energy systems:** District heating/cooling, microgrids, renewable portfolios, sector coupling +- **Material flows:** Supply chains, production lines, chemical processes +- **Integrated systems:** Water-energy nexus, industrial symbiosis + +While energy systems are our primary focus, the same foundation applies universally. This enables coupling different system types within integrated models. -Install FlixOpt via pip. -`pip install flixopt` -With [HiGHS](https://github.com/ERGO-Code/HiGHS?tab=readme-ov-file) included out of the box, flixopt is ready to use.. +### Modern Foundations -We recommend installing FlixOpt with all dependencies, which enables additional features like interactive network visualizations ([pyvis](https://github.com/WestHealth/pyvis)) and time series aggregation ([tsam](https://github.com/FZJ-IEK3-VSA/tsam)). -`pip install "flixopt[full]"` +Built on [linopy](https://github.com/PyPSA/linopy/) and [xarray](https://github.com/pydata/xarray), FlixOpt delivers **performance** and **transparency**. Full access to variables, constraints, and model structure. Extend anything with native linopy syntax. + +### Our Position + +We bridge the gap between high-level strategic models (like [FINE](https://github.com/FZJ-IEK3-VSA/FINE)) and low-level dispatch tools - similar to [PyPSA](https://docs.pypsa.org/latest/). FlixOpt is the sweet spot for detailed operational planning and long-term investment analysis in the **same framework**. + +### Academic Roots + +Originally developed at [TU Dresden](https://github.com/gewv-tu-dresden) for the SMARTBIOGRID project (funded by the German Federal Ministry for Economic Affairs and Energy, FKZ: 03KB159B). FlixOpt evolved from the MATLAB-based flixOptMat framework while incorporating best practices from [oemof/solph](https://github.com/oemof/oemof-solph). --- -## 📚 Documentation +## 🛣️ Roadmap + +**FlixOpt aims to be the most accessible, flexible, and universal Python framework for energy and material flow optimization.** We believe optimization modeling should be approachable for beginners yet powerful for experts, minimizing context switching between different planning horizons. -The documentation is available at [https://flixopt.github.io/flixopt/latest/](https://flixopt.github.io/flixopt/latest/) +**Current focus:** +- Enhanced component library (sector coupling, hydrogen, thermal networks) +- Examples showcasing multi-period and stochastic modeling +- Advanced result analysis and visualization + +**Future vision:** +- Modeling to generate alternatives (MGA) for robust decision-making +- Advanced stochastic optimization (two-stage, CVaR) +- Community ecosystem of user-contributed components + +→ [Full roadmap and vision](https://flixopt.github.io/flixopt/latest/roadmap/) --- -## 🎯️ Solver Integration +## 🛠️ Installation + +### Basic Installation + +```bash +pip install flixopt +``` + +Includes the [HiGHS](https://highs.dev/) solver - you're ready to optimize immediately. -By default, FlixOpt uses the open-source solver [HiGHS](https://highs.dev/) which is installed by default. However, it is compatible with additional solvers such as: +### Full Installation -- [Gurobi](https://www.gurobi.com/) -- [CBC](https://github.com/coin-or/Cbc) -- [GLPK](https://www.gnu.org/software/glpk/) -- [CPLEX](https://www.ibm.com/analytics/cplex-optimizer) +For additional features (interactive network visualization, time series aggregation): -For detailed licensing and installation instructions, refer to the respective solver documentation. +```bash +pip install "flixopt[full]" +``` + +### Solver Support + +FlixOpt supports many solvers via linopy: **HiGHS** (included), **Gurobi**, **CPLEX**, **CBC**, **GLPK**, and more. + +→ [Installation guide](https://flixopt.github.io/flixopt/latest/getting-started/) --- -## 🛠 Development Setup -Look into our docs for [development setup](https://flixopt.github.io/flixopt/latest/contribute/) +## 🤝 Contributing + +FlixOpt thrives on community input. Whether you're fixing bugs, adding components, improving docs, or sharing use cases - **we welcome your contributions.** + +→ [Contribution guide](https://flixopt.github.io/flixopt/latest/contribute/) --- ## 📖 Citation -If you use FlixOpt in your research or project, please cite the following: +If FlixOpt supports your research or project, please cite: - **Main Citation:** [DOI:10.18086/eurosun.2022.04.07](https://doi.org/10.18086/eurosun.2022.04.07) - **Short Overview:** [DOI:10.13140/RG.2.2.14948.24969](https://doi.org/10.13140/RG.2.2.14948.24969) + +--- + +## 📄 License + +MIT License - See [LICENSE](https://github.com/flixopt/flixopt/blob/main/LICENSE) for details. diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md deleted file mode 100644 index df8f39d9a..000000000 --- a/docs/SUMMARY.md +++ /dev/null @@ -1,7 +0,0 @@ -- [Home](index.md) -- [Getting Started](getting-started.md) -- [User Guide](user-guide/) -- [Examples](examples/) -- [Contribute](contribute.md) -- [API Reference](api-reference/) -- [Release Notes](changelog.md) diff --git a/docs/contribute.md b/docs/contribute.md index 44af34069..e1b93aecb 100644 --- a/docs/contribute.md +++ b/docs/contribute.md @@ -1,45 +1 @@ -# Contributing to the Project - -We warmly welcome contributions from the community! This guide will help you get started with contributing to our project. - -## Development Setup -1. Clone the repository `git clone https://github.com/flixOpt/flixopt.git` -2. Install the development dependencies `pip install -e ".[dev]"` -3. Install pre-commit hooks `pre-commit install` (one-time setup) -4. Run `pytest` to ensure your code passes all tests - -## Code Quality -We use [Ruff](https://github.com/astral-sh/ruff) for linting and formatting. After the one-time setup above, **code quality checks run automatically on every commit**. - -To run manually: -- `ruff check --fix .` to check and fix linting issues -- `ruff format .` to format code or -- `pre-commit run` or `pre-commit run --all-files` to trigger all checks - -## Documentation (Optional) -FlixOpt uses [mkdocs](https://www.mkdocs.org/) to generate documentation. -To work on documentation: -```bash -pip install -e ".[docs]" -mkdocs serve -``` -Then navigate to http://127.0.0.1:8000/ - -## Testing -- `pytest` to run the test suite -- You can also run the provided python script `run_all_test.py` - ---- -# Best practices - -## Coding Guidelines - -- Follow PEP 8 style guidelines -- Write clear, commented code -- Include type hints -- Create or update tests for new functionality -- Ensure 100% test coverage for new code - -## Branches & Releases -New features should be branched from `main` into `feature/*` -As stated, we follow **Semantic Versioning**. Releases are created manually from the `main` branch. +{! ../CONTRIBUTE.md !} diff --git a/docs/faq/contribute.md b/docs/faq/contribute.md deleted file mode 100644 index ff31c9f1f..000000000 --- a/docs/faq/contribute.md +++ /dev/null @@ -1,61 +0,0 @@ -# Contributing to the Project - -We warmly welcome contributions from the community! This guide will help you get started with contributing to our project. - -## Development Setup -1. Clone the repository `git clone https://github.com/flixOpt/flixopt.git` -2. Install the development dependencies `pip install -e ".[dev]"` -3. Install pre-commit hooks `pre-commit install` (one-time setup) -4. Run `pytest` to ensure your code passes all tests - -## Code Quality -We use [Ruff](https://github.com/astral-sh/ruff) for linting and formatting. After the one-time setup above, **code quality checks run automatically on every commit**. - -To run manually: -- `ruff check --fix .` to check and fix linting issues -- `ruff format .` to format code - -## Documentation (Optional) -FlixOpt uses [mkdocs](https://www.mkdocs.org/) to generate documentation. -To work on documentation: -```bash -pip install -e ".[docs]" -mkdocs serve -``` -Then navigate to http://127.0.0.1:8000/ - -## Testing -- `pytest` to run the test suite -- You can also run the provided python script `run_all_test.py` - ---- -# Best practices - -## Coding Guidelines - -- Follow PEP 8 style guidelines -- Write clear, commented code -- Include type hints -- Create or update tests for new functionality -- Ensure 100% test coverage for new code - -## Branches -As we start to think FlixOpt in **Releases**, we decided to introduce multiple **dev**-branches instead of only one: -Following the **Semantic Versioning** guidelines, we introduced: -- `next/patch`: This is where all pull requests for the next patch release (1.0.x) go. -- `next/minor`: This is where all pull requests for the next minor release (1.x.0) go. -- `next/major`: This is where all pull requests for the next major release (x.0.0) go. - -Everything else remains in `feature/...`-branches. - -## Pull requests -Every feature or bugfix should be merged into one of the 3 [release branches](#branches), using **Squash and merge** or a regular **single commit**. -At some point, `next/minor` or `next/major` will get merged into `main` using a regular **Merge** (not squash). -*This ensures that Features are kept separate, and the `next/...`branches stay in synch with ``main`.* - -## Releases -As stated, we follow **Semantic Versioning**. -Right after one of the 3 [release branches](#branches) is merged into main, a **Tag** should be added to the merge commit and pushed to the main branch. The tag has the form `v1.2.3`. -With this tag, a release with **Release Notes** must be created. - -*This is our current best practice* diff --git a/docs/faq/index.md b/docs/faq/index.md deleted file mode 100644 index 6a245edd3..000000000 --- a/docs/faq/index.md +++ /dev/null @@ -1,3 +0,0 @@ -# Frequently Asked Questions - -## Work in progress diff --git a/docs/getting-started.md b/docs/getting-started.md index 9af389755..044ffb872 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -37,6 +37,6 @@ Working with FlixOpt follows a general pattern: Now that you've installed FlixOpt and understand the basic workflow, you can: -- Learn about the [core concepts of FlixOpt](user-guide/index.md) +- Learn about the [core concepts of flixopt](user-guide/core-concepts.md) - Explore some [examples](examples/index.md) - Check the [API reference](api-reference/index.md) for detailed documentation diff --git a/docs/index.md b/docs/index.md index 04020639e..c9b01f284 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,47 +1,144 @@ -# FlixOpt +--- +title: Home +hide: + - navigation + - toc +--- -**FlixOpt** is a Python-based optimization framework designed to tackle energy and material flow problems using mixed-integer linear programming (MILP). +
-It borrows concepts from both [FINE](https://github.com/FZJ-IEK3-VSA/FINE) and [oemof.solph](https://github.com/oemof/oemof-solph). +

flixOpt

-## Why FlixOpt? +

Energy and Material Flow Optimization Framework

-FlixOpt is designed as a general-purpose optimization framework to get your model running quickly, without sacrificing flexibility down the road: +

Model, optimize, and analyze complex energy systems with a powerful Python framework designed for flexibility and performance.

-- **Easy to Use API**: FlixOpt provides a Pythonic, object-oriented interface that makes mathematical optimization more accessible to Python developers. +

+ 🚀 Get Started + 💡 View Examples + ⭐ GitHub +

-- **Approachable Learning Curve**: Designed to be accessible from the start, with options for more detailed models down the road. +
-- **Domain Independence**: While frameworks like oemof and FINE excel at energy system modeling with domain-specific components, FlixOpt offers a more general mathematical approach that can be applied across different fields. +## :material-map-marker-path: Quick Navigation -- **Extensibility**: Easily add custom constraints or variables to any FlixOpt Model using [linopy](https://github.com/PyPSA/linopy). Tailor any FlixOpt model to your specific needs without loosing the convenience of the framework. + + +## 🏗️ Framework Architecture + +
![FlixOpt Conceptual Usage](./images/architecture_flixOpt.png)
Conceptual Usage and IO operations of FlixOpt
-## Installation +**FlixOpt** provides a complete workflow for energy system optimization: + +- **:material-file-code: Define** your system using Python components +- **:material-cog: Optimize** with powerful solvers (HiGHS, Gurobi, CPLEX) +- **:material-chart-box: Analyze** results with built-in visualization tools +- **:material-export: Export** to various formats for further analysis + +
+ +## :material-account-group: Community & Support + +
+ +
+ +:fontawesome-brands-github:{ .feature-icon } + +### GitHub + +Report issues, request features, and contribute to the codebase + +[Visit Repository →](https://github.com/flixOpt/flixopt){target="_blank" rel="noopener noreferrer"} + +
+ +
+ +:material-forum:{ .feature-icon } + +### Discussions + +Ask questions and share your projects with the community + +[Join Discussion →](https://github.com/flixOpt/flixopt/discussions){target="_blank" rel="noopener noreferrer"} + +
+ +
+ +:material-book-open-page-variant:{ .feature-icon } + +### Contributing + +Help improve FlixOpt by contributing code, docs, or examples + +[Learn How →](contribute/){target="_blank" rel="noopener noreferrer"} + +
+ +
+ + +## :material-file-document-edit: Recent Updates + +!!! tip "What's New in v3.0.0" + Major improvements and breaking changes. Check the [Migration Guide](user-guide/migration-guide-v3.md) for upgrading from v2.x. -```bash -pip install flixopt -``` +📋 See the full [Release Notes](changelog/) for detailed version history. -For more detailed installation options, see the [Getting Started](getting-started.md) guide. +--- -## License +
-FlixOpt is released under the MIT License. See [LICENSE](https://github.com/flixopt/flixopt/blob/main/LICENSE) for details. +

Ready to optimize your energy system?

-## Citation +

+ ▶️ Start Building +

-If you use FlixOpt in your research or project, please cite: +
-- **Main Citation:** [DOI:10.18086/eurosun.2022.04.07](https://doi.org/10.18086/eurosun.2022.04.07) -- **Short Overview:** [DOI:10.13140/RG.2.2.14948.24969](https://doi.org/10.13140/RG.2.2.14948.24969) +--- -*A more sophisticated paper is in progress* +{% + include-markdown "../README.md" + start="## 🛠️ Installation" + end="## 📄 License" +%} diff --git a/docs/roadmap.md b/docs/roadmap.md new file mode 100644 index 000000000..fbad1043c --- /dev/null +++ b/docs/roadmap.md @@ -0,0 +1,49 @@ +# Roadmap and Vision + +## 🎯 Our Vision + +**FlixOpt aims to be the most accessible, flexible, and universal Python framework for energy and material flow optimization.** + +We believe optimization modeling should be **approachable for beginners** yet **powerful for experts**, minimizing context switching between **short-term dispatch** and **long-term investment** planning. + +--- + +## 🚀 Short-term (Next 6 months) + +- **Recipe collection** - Community-driven library of common modeling patterns, data manipulation techniques, and optimization strategies +- **Examples of stochastic and multi-period modeling** - The new v3.0 features currently lack comprehensive showcases +- **Advanced result analysis** - Automated reporting and enhanced visualization options +- **Interactive tutorials** - Browser-based, reactive tutorials for learning FlixOpt without local installation using [Marimo](https://marimo.io/) + +## 🔮 Medium-term (6-12 months) + +- **Modeling to Generate Alternatives (MGA)** - Built-in support for exploring near-optimal solution spaces to produce more robust, diverse solutions under uncertainty. See [PyPSA](https://docs.pypsa.org/latest/user-guide/optimization/modelling-to-generate-alternatives/) and [Calliope](https://calliope.readthedocs.io/en/latest/examples/modes/) for reference implementations +- **Advanced stochastic optimization** - Build sophisticated new `Calculation` classes to perform different stochastic optimization approaches, like PyPSA's [two-stage stochastic programming and risk preferences with Conditional Value-at-Risk (CVaR)](https://docs.pypsa.org/latest/user-guide/optimization/stochastic/) +- **Enhanced component library** - More pre-built, domain-specific components (sector coupling, hydrogen systems, thermal networks, demand-side management) + +## 🌟 Long-term (12+ months) + +- **Showcase universal applicability** - FlixOpt already handles any flow-based system (supply chains, water networks, production planning, chemical processes) - we need more examples and domain-specific component libraries to demonstrate this +- **Community ecosystem** - Rich library of user-contributed components, examples, and domain-specific extensions + +--- + +## 🤝 How to Help + +- **Code**: Implement features, fix bugs, add tests +- **Docs**: Write tutorials, improve examples, create case studies +- **Components**: Contribute domain-specific components +- **Feedback**: [Report issues](https://github.com/flixOpt/flixopt/issues), [join discussions](https://github.com/flixOpt/flixopt/discussions) + +See our [contribution guide](contribute.md) to get started. + +--- + +## 📅 Release Philosophy + +FlixOpt follows [semantic versioning](https://semver.org/): +- **Major** (v3→v4): Breaking changes, major features +- **Minor** (v3.0→v3.1): New features, backward compatible +- **Patch** (v3.0.0→v3.0.1): Bug fixes only + +Target: Patch releases as needed, minor releases every 2-3 months. diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css new file mode 100644 index 000000000..79dfc9a15 --- /dev/null +++ b/docs/stylesheets/extra.css @@ -0,0 +1,396 @@ +/* ============================================================================ + flixOpt Custom Styling + ========================================================================= */ + +/* Root variables for easy customization */ +:root { + /* Spacing */ + --content-padding: 2rem; + + /* Typography */ + --heading-font-weight: 600; + + /* Colors - enhance teal theme */ + --flixopt-teal: #009688; + --flixopt-teal-light: #4DB6AC; + --flixopt-teal-dark: #00796B; +} + +/* Dark mode adjustments */ +[data-md-color-scheme="slate"] { + --md-code-bg-color: #1e1e1e; +} + +/* ============================================================================ + Typography Improvements + ========================================================================= */ + +/* Better line height for readability */ +.md-typeset { + line-height: 1.7; +} + +/* Enhanced headings */ +.md-typeset h1 { + font-weight: var(--heading-font-weight); + letter-spacing: -0.02em; + margin-top: 0; +} + +.md-typeset h2 { + font-weight: var(--heading-font-weight); + border-bottom: 1px solid var(--md-default-fg-color--lightest); + padding-bottom: 0.3em; + margin-top: 2em; +} + +/* Better code inline */ +.md-typeset code { + padding: 0.15em 0.4em; + border-radius: 0.25em; + font-size: 0.875em; +} + +/* ============================================================================ + Navigation Enhancements + ========================================================================= */ + +/* Smooth hover effects on navigation */ +.md-nav__link:hover { + opacity: 0.7; + transition: opacity 0.2s ease; +} + +/* Active navigation item enhancement */ +.md-nav__link--active { + font-weight: 600; + border-left: 3px solid var(--md-primary-fg-color); + padding-left: calc(1.2rem - 3px) !important; +} + +/* ============================================================================ + Code Block Improvements + ========================================================================= */ + +/* Better code block styling */ +.md-typeset .highlight { + border-radius: 0.5rem; + margin: 1.5em 0; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +[data-md-color-scheme="slate"] .md-typeset .highlight { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); +} + +/* Line numbers styling */ +.md-typeset .highlight .linenos { + user-select: none; + opacity: 0.5; +} + +/* Copy button enhancement */ +.md-clipboard { + opacity: 0; + transition: opacity 0.2s ease; +} + +.highlight:hover .md-clipboard { + opacity: 1; +} + +/* ============================================================================ + Admonitions & Callouts + ========================================================================= */ + +/* Enhanced admonitions */ +.md-typeset .admonition { + border-radius: 0.5rem; + border-left-width: 0.25rem; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); +} + +/* ============================================================================ + Tables + ========================================================================= */ + +/* Better table styling */ +.md-typeset table:not([class]) { + border-radius: 0.5rem; + overflow: hidden; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); +} + +.md-typeset table:not([class]) th { + background-color: var(--md-primary-fg-color); + color: white; + font-weight: 600; + text-align: left; +} + +.md-typeset table:not([class]) tr:hover { + background-color: var(--md-default-fg-color--lightest); + transition: background-color 0.2s ease; +} + +/* ============================================================================ + API Documentation Styling + ========================================================================= */ + +/* Better spacing for API docs */ +.doc-heading { + margin-top: 2rem !important; +} + +/* Parameter tables */ +.doc-md-description table { + width: 100%; + font-size: 0.9em; +} + +/* Signature styling */ +.doc-signature { + font-family: var(--md-code-font); + background-color: var(--md-code-bg-color); + border-radius: 0.5rem; + padding: 1rem; + overflow-x: auto; +} + +/* ============================================================================ + Home Page Hero (optional enhancement) + ========================================================================= */ + +.hero { + text-align: center; + padding: 4rem 2rem; + background: linear-gradient(135deg, var(--flixopt-teal-light) 0%, var(--flixopt-teal-dark) 100%); + color: white; + border-radius: 1rem; + margin-bottom: 2rem; +} + +.hero h1 { + font-size: 3rem; + margin-bottom: 1rem; + color: white; + border: none; +} + +.hero p { + font-size: 1.25rem; + opacity: 0.9; +} + +/* ============================================================================ + Responsive Design + ========================================================================= */ + +@media screen and (max-width: 76.1875em) { + .md-typeset h1 { + font-size: 2rem; + } +} + +@media screen and (max-width: 44.9375em) { + :root { + --content-padding: 1rem; + } + + .hero h1 { + font-size: 2rem; + } + + .hero p { + font-size: 1rem; + } +} + +/* ============================================================================ + Print Styles + ========================================================================= */ + +@media print { + .md-typeset { + font-size: 0.9rem; + } + + .md-header, + .md-sidebar, + .md-footer { + display: none; + } +} + +/* ============================================================================ + Home Page Inline Styles (moved from docs/index.md) + ========================================================================= */ + +.hero-section { + text-align: center; + padding: 4rem 2rem 3rem 2rem; + background: linear-gradient(135deg, rgba(0, 150, 136, 0.1) 0%, rgba(0, 121, 107, 0.1) 100%); + border-radius: 1rem; + margin-bottom: 3rem; +} + +.hero-section h1 { + font-size: 3.5rem; + font-weight: 700; + margin-bottom: 1rem; + background: linear-gradient(135deg, #009688 0%, #00796B 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.hero-section .tagline { + font-size: 1.5rem; + color: var(--md-default-fg-color--light); + margin-bottom: 2rem; + font-weight: 300; +} + +.hero-buttons { + display: flex; + gap: 1rem; + justify-content: center; + flex-wrap: wrap; + margin-top: 2rem; +} + +.feature-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 2rem; + margin: 3rem 0; +} + +.feature-card { + padding: 2rem; + border-radius: 0.75rem; + background: var(--md-code-bg-color); + border: 1px solid var(--md-default-fg-color--lightest); + transition: all 0.3s ease; + text-align: center; +} + +.feature-card:hover { + transform: translateY(-4px); + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1); + border-color: var(--md-primary-fg-color); +} + +.feature-icon { + font-size: 3rem; + margin-bottom: 1rem; + display: block; +} + +.feature-card h3 { + margin-top: 0; + margin-bottom: 0.5rem; + font-size: 1.25rem; +} + +.feature-card p { + color: var(--md-default-fg-color--light); + margin: 0; + font-size: 0.95rem; + line-height: 1.6; +} + +.stats-banner { + display: flex; + justify-content: space-around; + padding: 2rem; + background: var(--md-code-bg-color); + border-radius: 0.75rem; + margin: 3rem 0; + text-align: center; + flex-wrap: wrap; + gap: 2rem; +} + +.stat-item { + flex: 1; + min-width: 150px; +} + +.stat-number { + font-size: 2.5rem; + font-weight: 700; + color: var(--md-primary-fg-color); + display: block; +} + +.stat-label { + color: var(--md-default-fg-color--light); + font-size: 0.9rem; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.architecture-section { + margin: 4rem 0; + padding: 2rem; + background: var(--md-code-bg-color); + border-radius: 0.75rem; +} + +.quick-links { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 1.5rem; + margin: 3rem 0; +} + +.quick-link-card { + padding: 1.5rem; + border-left: 4px solid var(--md-primary-fg-color); + background: var(--md-code-bg-color); + border-radius: 0.5rem; + transition: all 0.2s ease; + text-decoration: none; + display: block; +} + +.quick-link-card:hover { + background: var(--md-default-fg-color--lightest); + transform: translateX(4px); +} + +.quick-link-card h3 { + margin: 0 0 0.5rem 0; + font-size: 1.1rem; + color: var(--md-primary-fg-color); +} + +.quick-link-card p { + margin: 0; + color: var(--md-default-fg-color--light); + font-size: 0.9rem; +} + +@media screen and (max-width: 768px) { + .hero-section h1 { + font-size: 2.5rem; + } + + .hero-section .tagline { + font-size: 1.2rem; + } + + .hero-buttons { + flex-direction: column; + align-items: stretch; + } + + .feature-grid { + grid-template-columns: 1fr; + } + + .stats-banner { + flex-direction: column; + } +} diff --git a/docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md b/docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md deleted file mode 100644 index 7da311c37..000000000 --- a/docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md +++ /dev/null @@ -1,132 +0,0 @@ -## Effects -[`Effects`][flixopt.effects.Effect] are used to allocate things like costs, emissions, or other "effects" occurring in the system. -These arise from so called **Shares**, which originate from **Elements** like [Flows](Flow.md). - -**Example:** - -[`Flows`][flixopt.elements.Flow] have an attribute called `effects_per_flow_hour`, defining the effect amount of per flow hour. -Associated effects could be: -- costs - given in [€/kWh]... -- ...or emissions - given in [kg/kWh]. -- -Effects are allocated separately for investments and operation. - -### Shares to Effects - -$$ \label{eq:Share_invest} -s_{l \rightarrow e, \text{inv}} = \sum_{v \in \mathcal{V}_{l, \text{inv}}} v \cdot \text a_{v \rightarrow e} -$$ - -$$ \label{eq:Share_operation} -s_{l \rightarrow e, \text{op}}(\text{t}_i) = \sum_{v \in \mathcal{V}_{l,\text{op}}} v(\text{t}_i) \cdot \text a_{v \rightarrow e}(\text{t}_i) -$$ - -With: - -- $\text{t}_i$ being the time step -- $\mathcal{V_l}$ being the set of all optimization variables of element $e$ -- $\mathcal{V}_{l, \text{inv}}$ being the set of all optimization variables of element $e$ related to investment -- $\mathcal{V}_{l, \text{op}}$ being the set of all optimization variables of element $e$ related to operation -- $v$ being an optimization variable of the element $l$ -- $v(\text{t}_i)$ being an optimization variable of the element $l$ at timestep $\text{t}_i$ -- $\text a_{v \rightarrow e}$ being the factor between the optimization variable $v$ to effect $e$ -- $\text a_{v \rightarrow e}(\text{t}_i)$ being the factor between the optimization variable $v$ to effect $e$ for timestep $\text{t}_i$ -- $s_{l \rightarrow e, \text{inv}}$ being the share of element $l$ to the investment part of effect $e$ -- $s_{l \rightarrow e, \text{op}}(\text{t}_i)$ being the share of element $l$ to the operation part of effect $e$ - -### Shares between different Effects - -Furthermore, the Effect $x$ can contribute a share to another Effect ${e} \in \mathcal{E}\backslash x$. -This share is defined by the factor $\text r_{x \rightarrow e}$. - -For example, the Effect "CO$_2$ emissions" (unit: kg) -can cause an additional share to Effect "monetary costs" (unit: €). -In this case, the factor $\text a_{x \rightarrow e}$ is the specific CO$_2$ price in €/kg. However, circular references have to be avoided. - -The overall sum of investment shares of an Effect $e$ is given by $\eqref{eq:Effect_invest}$ - -$$ \label{eq:Effect_invest} -E_{e, \text{inv}} = -\sum_{l \in \mathcal{L}} s_{l \rightarrow e,\text{inv}} + -\sum_{x \in \mathcal{E}\backslash e} E_{x, \text{inv}} \cdot \text{r}_{x \rightarrow e,\text{inv}} -$$ - -The overall sum of operation shares is given by $\eqref{eq:Effect_Operation}$ - -$$ \label{eq:Effect_Operation} -E_{e, \text{op}}(\text{t}_{i}) = -\sum_{l \in \mathcal{L}} s_{l \rightarrow e, \text{op}}(\text{t}_i) + -\sum_{x \in \mathcal{E}\backslash e} E_{x, \text{op}}(\text{t}_i) \cdot \text{r}_{x \rightarrow {e},\text{op}}(\text{t}_i) -$$ - -and totals to $\eqref{eq:Effect_Operation_total}$ -$$\label{eq:Effect_Operation_total} -E_{e,\text{op},\text{tot}} = \sum_{i=1}^n E_{e,\text{op}}(\text{t}_{i}) -$$ - -With: - -- $\mathcal{L}$ being the set of all elements in the FlowSystem -- $\mathcal{E}$ being the set of all effects in the FlowSystem -- $\text r_{x \rightarrow e, \text{inv}}$ being the factor between the invest part of Effect $x$ and Effect $e$ -- $\text r_{x \rightarrow e, \text{op}}(\text{t}_i)$ being the factor between the operation part of Effect $x$ and Effect $e$ - -- $\text{t}_i$ being the time step -- $s_{l \rightarrow e, \text{inv}}$ being the share of element $l$ to the investment part of effect $e$ -- $s_{l \rightarrow e, \text{op}}(\text{t}_i)$ being the share of element $l$ to the operation part of effect $e$ - - -The total of an effect $E_{e}$ is given as $\eqref{eq:Effect_Total}$ - -$$ \label{eq:Effect_Total} -E_{e} = E_{\text{inv},e} +E_{\text{op},\text{tot},e} -$$ - -### Constraining Effects - -For each variable $v \in \{ E_{e,\text{inv}}, E_{e,\text{op},\text{tot}}, E_e\}$, a lower bound $v^\text{L}$ and upper bound $v^\text{U}$ can be defined as - -$$ \label{eq:Bounds_Single} -\text v^\text{L} \leq v \leq \text v^\text{U} -$$ - -Furthermore, bounds for the operational shares can be set for each time step - -$$ \label{eq:Bounds_Time_Steps} -\text E_{e,\text{op}}^\text{L}(\text{t}_i) \leq E_{e,\text{op}}(\text{t}_i) \leq \text E_{e,\text{op}}^\text{U}(\text{t}_i) -$$ - -## Penalty - -Additionally to the user defined [Effects](#effects), a Penalty $\Phi$ is part of every FlixOpt Model. -Its used to prevent unsolvable problems and simplify troubleshooting. -Shares to the penalty can originate from every Element and are constructed similarly to -$\eqref{Share_invest}$ and $\eqref{Share_operation}$. - -$$ \label{eq:Penalty} -\Phi = \sum_{l \in \mathcal{L}} \left( s_{l \rightarrow \Phi} +\sum_{\text{t}_i \in \mathcal{T}} s_{l \rightarrow \Phi}(\text{t}_{i}) \right) -$$ - -With: - -- $\mathcal{L}$ being the set of all elements in the FlowSystem -- $\mathcal{T}$ being the set of all timesteps -- $s_{l \rightarrow \Phi}$ being the share of element $l$ to the penalty - -At the moment, penalties only occur in [Buses](Bus.md) - -## Objective - -The optimization objective of a FlixOpt Model is defined as $\eqref{eq:Objective}$ -$$ \label{eq:Objective} -\min(E_{\Omega} + \Phi) -$$ - -With: - -- $\Omega$ being the chosen **Objective [Effect](#effects)** (see $\eqref{eq:Effect_Total}$) -- $\Phi$ being the [Penalty](#penalty) - -This approach allows for a multi-criteria optimization using both... - - ... the **Weighted Sum** method, as the chosen **Objective Effect** can incorporate other Effects. - - ... the ($\epsilon$-constraint method) by constraining effects. diff --git a/docs/user-guide/Mathematical Notation/Flow.md b/docs/user-guide/Mathematical Notation/Flow.md deleted file mode 100644 index 78135e822..000000000 --- a/docs/user-guide/Mathematical Notation/Flow.md +++ /dev/null @@ -1,26 +0,0 @@ -The flow_rate is the main optimization variable of the Flow. It's limited by the size of the Flow and relative bounds \eqref{eq:flow_rate}. - -$$ \label{eq:flow_rate} - \text P \cdot \text p^{\text{L}}_{\text{rel}}(\text{t}_{i}) - \leq p(\text{t}_{i}) \leq - \text P \cdot \text p^{\text{U}}_{\text{rel}}(\text{t}_{i}) -$$ - -With: - -- $\text P$ being the size of the Flow -- $p(\text{t}_{i})$ being the flow-rate at time $\text{t}_{i}$ -- $\text p^{\text{L}}_{\text{rel}}(\text{t}_{i})$ being the relative lower bound (typically 0) -- $\text p^{\text{U}}_{\text{rel}}(\text{t}_{i})$ being the relative upper bound (typically 1) - -With $\text p^{\text{L}}_{\text{rel}}(\text{t}_{i}) = 0$ and $\text p^{\text{U}}_{\text{rel}}(\text{t}_{i}) = 1$, -equation \eqref{eq:flow_rate} simplifies to - -$$ - 0 \leq p(\text{t}_{i}) \leq \text P -$$ - - -This mathematical formulation can be extended by using [OnOffParameters](./OnOffParameters.md) -to define the on/off state of the Flow, or by using [InvestParameters](./InvestParameters.md) -to change the size of the Flow from a constant to an optimization variable. diff --git a/docs/user-guide/Mathematical Notation/InvestParameters.md b/docs/user-guide/Mathematical Notation/InvestParameters.md deleted file mode 100644 index d3cd4f81e..000000000 --- a/docs/user-guide/Mathematical Notation/InvestParameters.md +++ /dev/null @@ -1,3 +0,0 @@ -# InvestParameters - -This is a work in progress. diff --git a/docs/user-guide/Mathematical Notation/LinearConverter.md b/docs/user-guide/Mathematical Notation/LinearConverter.md deleted file mode 100644 index bf1279c32..000000000 --- a/docs/user-guide/Mathematical Notation/LinearConverter.md +++ /dev/null @@ -1,21 +0,0 @@ -[`LinearConverters`][flixopt.components.LinearConverter] define a ratio between incoming and outgoing [Flows](Flow.md). - -$$ \label{eq:Linear-Transformer-Ratio} - \sum_{f_{\text{in}} \in \mathcal F_{in}} \text a_{f_{\text{in}}}(\text{t}_i) \cdot p_{f_\text{in}}(\text{t}_i) = \sum_{f_{\text{out}} \in \mathcal F_{out}} \text b_{f_\text{out}}(\text{t}_i) \cdot p_{f_\text{out}}(\text{t}_i) -$$ - -With: - -- $\mathcal F_{in}$ and $\mathcal F_{out}$ being the set of all incoming and outgoing flows -- $p_{f_\text{in}}(\text{t}_i)$ and $p_{f_\text{out}}(\text{t}_i)$ being the flow-rate at time $\text{t}_i$ for flow $f_\text{in}$ and $f_\text{out}$, respectively -- $\text a_{f_\text{in}}(\text{t}_i)$ and $\text b_{f_\text{out}}(\text{t}_i)$ being the ratio of the flow-rate at time $\text{t}_i$ for flow $f_\text{in}$ and $f_\text{out}$, respectively - -With one incoming **Flow** and one outgoing **Flow**, this can be simplified to: - -$$ \label{eq:Linear-Transformer-Ratio-simple} - \text a(\text{t}_i) \cdot p_{f_\text{in}}(\text{t}_i) = p_{f_\text{out}}(\text{t}_i) -$$ - -where $\text a$ can be interpreted as the conversion efficiency of the **LinearConverter**. -#### Piecewise Conversion factors -The conversion efficiency can be defined as a piecewise linear approximation. See [Piecewise](Piecewise.md) for more details. diff --git a/docs/user-guide/Mathematical Notation/OnOffParameters.md b/docs/user-guide/Mathematical Notation/OnOffParameters.md deleted file mode 100644 index ca22d7d33..000000000 --- a/docs/user-guide/Mathematical Notation/OnOffParameters.md +++ /dev/null @@ -1,3 +0,0 @@ -# OnOffParameters - -This is a work in progress. diff --git a/docs/user-guide/Mathematical Notation/index.md b/docs/user-guide/Mathematical Notation/index.md deleted file mode 100644 index b76a1ba1f..000000000 --- a/docs/user-guide/Mathematical Notation/index.md +++ /dev/null @@ -1,22 +0,0 @@ - -# Mathematical Notation - -## Naming Conventions - -FlixOpt uses the following naming conventions: - -- All optimization variables are denoted by italic letters (e.g., $x$, $y$, $z$) -- All parameters and constants are denoted by non italic small letters (e.g., $\text{a}$, $\text{b}$, $\text{c}$) -- All Sets are denoted by greek capital letters (e.g., $\mathcal{F}$, $\mathcal{E}$) -- All units of a set are denoted by greek small letters (e.g., $\mathcal{f}$, $\mathcal{e}$) -- The letter $i$ is used to denote an index (e.g., $i=1,\dots,\text n$) -- All time steps are denoted by the letter $\text{t}$ (e.g., $\text{t}_0$, $\text{t}_1$, $\text{t}_i$) - -## Timesteps -Time steps are defined as a sequence of discrete time steps $\text{t}_i \in \mathcal{T} \quad \text{for} \quad i \in \{1, 2, \dots, \text{n}\}$ (left-aligned in its timespan). -From this sequence, the corresponding time intervals $\Delta \text{t}_i \in \Delta \mathcal{T}$ are derived as - -$$\Delta \text{t}_i = \text{t}_{i+1} - \text{t}_i \quad \text{for} \quad i \in \{1, 2, \dots, \text{n}-1\}$$ - -The final time interval $\Delta \text{t}_\text n$ defaults to $\Delta \text{t}_\text n = \Delta \text{t}_{\text n-1}$, but is of course customizable. -Non-equidistant time steps are also supported. diff --git a/docs/user-guide/index.md b/docs/user-guide/core-concepts.md similarity index 63% rename from docs/user-guide/index.md rename to docs/user-guide/core-concepts.md index bc1738997..bf52a26ba 100644 --- a/docs/user-guide/index.md +++ b/docs/user-guide/core-concepts.md @@ -1,8 +1,10 @@ -# FlixOpt Concepts +# Core concepts of flixopt -FlixOpt is built around a set of core concepts that work together to represent and optimize energy and material flow systems. This page provides a high-level overview of these concepts and how they interact. +FlixOpt is built around a set of core concepts that work together to represent and optimize **any system involving flows and conversions** - whether that's energy systems, material flows, supply chains, water networks, or production processes. -## Core Concepts +This page provides a high-level overview of these concepts and how they interact. + +## Main building blocks ### FlowSystem @@ -13,15 +15,22 @@ Every FlixOpt model starts with creating a FlowSystem. It: - Contains and connects [components](#components), [buses](#buses), and [flows](#flows) - Manages the [effects](#effects) (objectives and constraints) +FlowSystem provides two ways to access elements: + +- **Dict-like interface**: Access any element by label: `flow_system['Boiler']`, `'Boiler' in flow_system`, `flow_system.keys()` +- **Direct containers**: Access type-specific containers: `flow_system.components`, `flow_system.buses`, `flow_system.effects`, `flow_system.flows` + +Element labels must be unique across all types. See the [`FlowSystem` API reference][flixopt.flow_system.FlowSystem] for detailed examples and usage patterns. + ### Flows [`Flow`][flixopt.elements.Flow] objects represent the movement of energy or material between a [Bus](#buses) and a [Component](#components) in a predefined direction. -- Have a `size` which, generally speaking, defines how fast energy or material can be moved. Usually measured in MW, kW, m³/h, etc. -- Have a `flow_rate`, which is defines how fast energy or material is transported. Usually measured in MW, kW, m³/h, etc. +- Have a `size` which, generally speaking, defines how much energy or material can be moved. Usually measured in MW, kW, m³/h, etc. +- Have a `flow_rate`, which defines how fast energy or material is transported. Usually measured in MW, kW, m³/h, etc. - Have constraints to limit the flow-rate (min/max, total flow hours, on/off etc.) - Can have fixed profiles (for demands or renewable generation) -- Can have [Effects](#effects) associated by their use (operation, investment, on/off, ...) +- Can have [Effects](#effects) associated by their use (costs, emissions, labour, ...) #### Flow Hours While the **Flow Rate** defines the rate in which energy or material is transported, the **Flow Hours** define the amount of energy or material that is transported. @@ -45,28 +54,49 @@ Examples: ### Components -[`Component`][flixopt.elements.Component] objects usually represent physical entities in your system that interact with [`Flows`][flixopt.elements.Flow]. They include: +[`Component`][flixopt.elements.Component] objects usually represent physical entities in your system that interact with [`Flows`][flixopt.elements.Flow]. The generic component types work across all domains: - [`LinearConverters`][flixopt.components.LinearConverter] - Converts input flows to output flows with (piecewise) linear relationships + - *Energy: boilers, heat pumps, turbines* + - *Manufacturing: assembly lines, processing equipment* + - *Chemistry: reactors, separators* - [`Storages`][flixopt.components.Storage] - Stores energy or material over time -- [`Sources`][flixopt.components.Source] / [`Sinks`][flixopt.components.Sink] / [`SourceAndSinks`][flixopt.components.SourceAndSink] - Produce or consume flows. They are usually used to model external demands or supplies. + - *Energy: batteries, thermal storage, gas storage* + - *Logistics: warehouses, buffer inventory* + - *Water: reservoirs, tanks* +- [`Sources`][flixopt.components.Source] / [`Sinks`][flixopt.components.Sink] / [`SourceAndSinks`][flixopt.components.SourceAndSink] - Produce or consume flows + - *Energy: demands, renewable generation* + - *Manufacturing: raw material supply, product demand* + - *Supply chain: suppliers, customers* - [`Transmissions`][flixopt.components.Transmission] - Moves flows between locations with possible losses -- Specialized [`LinearConverters`][flixopt.components.LinearConverter] like [`Boilers`][flixopt.linear_converters.Boiler], [`HeatPumps`][flixopt.linear_converters.HeatPump], [`CHPs`][flixopt.linear_converters.CHP], etc. These simplify the usage of the `LinearConverter` class and can also be used as blueprint on how to define custom classes or parameterize existing ones. + - *Energy: pipelines, power lines* + - *Logistics: transport routes* + - *Water: distribution networks* + +**Pre-built specialized components** for energy systems include [`Boilers`][flixopt.linear_converters.Boiler], [`HeatPumps`][flixopt.linear_converters.HeatPump], [`CHPs`][flixopt.linear_converters.CHP], etc. These can serve as blueprints for custom domain-specific components. ### Effects -[`Effect`][flixopt.effects.Effect] objects represent impacts or metrics related to your system, such as: +[`Effect`][flixopt.effects.Effect] objects represent impacts or metrics related to your system. While commonly used to allocate costs, they're completely flexible: +**Energy systems:** - Costs (investment, operation) - Emissions (CO₂, NOx, etc.) -- Resource consumption -- Area demand +- Primary energy consumption + +**Other domains:** +- Production time, labor hours (manufacturing) +- Water consumption, wastewater (process industries) +- Transport distance, vehicle utilization (logistics) +- Space consumption +- Any custom metric relevant to your domain These can be freely defined and crosslink to each other (`CO₂` ──[specific CO₂-costs]─→ `Costs`). One effect is designated as the **optimization objective** (typically Costs), while others can be constrained. -This approach allows for a multi-criteria optimization using both... - - ... the **Weigted Sum**Method, by Optimizing a theoretical Effect which other Effects crosslink to. - - ... the ($\epsilon$-constraint method) by constraining effects. +This approach allows for multi-criteria optimization using both: + + - **Weighted Sum Method**: Optimize a theoretical Effect which other Effects crosslink to + - **ε-constraint method**: Constrain effects to specific limits ### Calculation @@ -82,7 +112,7 @@ FlixOpt offers different calculation modes: The results of a calculation are stored in a [`CalculationResults`][flixopt.results.CalculationResults] object. This object contains the solutions of the optimization as well as all information about the [`Calculation`][flixopt.calculation.Calculation] and the [`FlowSystem`][flixopt.flow_system.FlowSystem] it was created from. -The solutions is stored as an `xarray.Dataset`, but can be accessed through their assotiated Component, Bus or Effect. +The solution is stored as an `xarray.Dataset`, but can be accessed through their assotiated Component, Bus or Effect. This [`CalculationResults`][flixopt.results.CalculationResults] object can be saved to file and reloaded from file, allowing you to analyze the results anytime after the solve. @@ -91,19 +121,20 @@ This [`CalculationResults`][flixopt.results.CalculationResults] object can be sa The process of working with FlixOpt can be divided into 3 steps: 1. Create a [`FlowSystem`][flixopt.flow_system.FlowSystem], containing all the elements and data of your system - - Define the time series of your system - - Add [`Components`][flixopt.components] like [`Boilers`][flixopt.linear_converters.Boiler], [`HeatPumps`][flixopt.linear_converters.HeatPump], [`CHPs`][flixopt.linear_converters.CHP], etc. - - Add [`Buses`][flixopt.elements.Bus] as connection points in your system + - Define the time horizon of your system (and optionally your periods and scenarios, see [Dimensions](mathematical-notation/dimensions.md))) - Add [`Effects`][flixopt.effects.Effect] to represent costs, emissions, etc. - - *This [`FlowSystem`][flixopt.flow_system.FlowSystem] can also be loaded from a netCDF file* + - Add [`Buses`][flixopt.elements.Bus] as connection points in your system and [`Sinks`][flixopt.components.Sink] & [`Sources`][flixopt.components.Source] as connections to the outer world (markets, power grid, ...) + - Add [`Components`][flixopt.components] like [`Boilers`][flixopt.linear_converters.Boiler], [`HeatPumps`][flixopt.linear_converters.HeatPump], [`CHPs`][flixopt.linear_converters.CHP], etc. + - Add + - [`FlowSystems`][flixopt.flow_system.FlowSystem] can also be loaded from a netCDF file* 2. Translate the model to a mathematical optimization problem - Create a [`Calculation`][flixopt.calculation.Calculation] from your FlowSystem and choose a Solver - - ...The Calculation is translated internaly to a mathematical optimization problem... + - ...The Calculation is translated internally to a mathematical optimization problem... - ...and solved by the chosen solver. 3. Analyze the results - The results are stored in a [`CalculationResults`][flixopt.results.CalculationResults] object - This object can be saved to file and reloaded from file, retaining all information about the calculation - - As it contains the used [`FlowSystem`][flixopt.flow_system.FlowSystem], it can be used to start a new calculation + - As it contains the used [`FlowSystem`][flixopt.flow_system.FlowSystem], it fully documents all assumptions taken to create the results.
![FlixOpt Conceptual Usage](../images/architecture_flixOpt.png) diff --git a/docs/user-guide/mathematical-notation/dimensions.md b/docs/user-guide/mathematical-notation/dimensions.md new file mode 100644 index 000000000..d1bc99c8e --- /dev/null +++ b/docs/user-guide/mathematical-notation/dimensions.md @@ -0,0 +1,264 @@ +# Dimensions + +FlixOpt's `FlowSystem` supports multiple dimensions for modeling optimization problems. Understanding these dimensions is crucial for interpreting the mathematical formulations presented in this documentation. + +## The Three Dimensions + +FlixOpt models can have up to three dimensions: + +1. **Time (`time`)** - **MANDATORY** + - Represents the temporal evolution of the system + - Defined via `pd.DatetimeIndex` + - Must contain at least 2 timesteps + - All optimization variables and constraints evolve over time +2. **Period (`period`)** - **OPTIONAL** + - Represents independent planning periods (e.g., years 2020, 2021, 2022) + - Defined via `pd.Index` with integer values + - Used for multi-period optimization such as investment planning across years + - Each period is independent with its own time series +3. **Scenario (`scenario`)** - **OPTIONAL** + - Represents alternative futures or uncertainty realizations (e.g., "Base Case", "High Demand") + - Defined via `pd.Index` with any labels + - Scenarios within the same period share the same time dimension + - Used for stochastic optimization or scenario comparison + +--- + +## Dimensional Structure + +**Coordinate System:** + +```python +FlowSystemDimensions = Literal['time', 'period', 'scenario'] + +coords = { + 'time': pd.DatetimeIndex, # Always present + 'period': pd.Index | None, # Optional + 'scenario': pd.Index | None # Optional +} +``` + +**Example:** +```python +import pandas as pd +import numpy as np +import flixopt as fx + +timesteps = pd.date_range('2020-01-01', periods=24, freq='h') +scenarios = pd.Index(['Base Case', 'High Demand']) +periods = pd.Index([2020, 2021, 2022]) + +flow_system = fx.FlowSystem( + timesteps=timesteps, + periods=periods, + scenarios=scenarios, + weights=np.array([0.5, 0.5]) # Scenario weights +) +``` + +This creates a system with: +- 24 time steps per scenario per period +- 2 scenarios with equal weights (0.5 each) +- 3 periods (years) +- **Total decision space:** 24 × 2 × 3 = 144 time-scenario-period combinations + +--- + +## Independence of Formulations + +**All mathematical formulations in this documentation are independent of whether periods or scenarios are present.** + +The equations shown throughout this documentation (for [Flow](elements/Flow.md), [Storage](elements/Storage.md), [Bus](elements/Bus.md), etc.) are written with only the time index $\text{t}_i$. When periods and/or scenarios are added, **the same equations apply** - they are simply expanded to additional dimensions. + +### How Dimensions Expand Formulations + +**Flow rate bounds** (from [Flow](elements/Flow.md)): + +$$ +\text{P} \cdot \text{p}^{\text{L}}_{\text{rel}}(\text{t}_{i}) \leq p(\text{t}_{i}) \leq \text{P} \cdot \text{p}^{\text{U}}_{\text{rel}}(\text{t}_{i}) +$$ + +This equation remains valid regardless of dimensions: + +| Dimensions Present | Variable Indexing | Interpretation | +|-------------------|-------------------|----------------| +| Time only | $p(\text{t}_i)$ | Flow rate at time $\text{t}_i$ | +| Time + Scenario | $p(\text{t}_i, s)$ | Flow rate at time $\text{t}_i$ in scenario $s$ | +| Time + Period | $p(\text{t}_i, y)$ | Flow rate at time $\text{t}_i$ in period $y$ | +| Time + Period + Scenario | $p(\text{t}_i, y, s)$ | Flow rate at time $\text{t}_i$ in period $y$, scenario $s$ | + +**The mathematical relationship remains identical** - only the indexing expands. + +--- + +## Independence Between Scenarios and Periods + +**There is no interconnection between scenarios and periods, except for shared investment decisions within a period.** + +### Scenario Independence + +Scenarios within a period are **operationally independent**: + +- Each scenario has its own operational variables: $p(\text{t}_i, s_1)$ and $p(\text{t}_i, s_2)$ are independent +- Scenarios cannot exchange energy, information, or resources +- Storage states are separate: $c(\text{t}_i, s_1) \neq c(\text{t}_i, s_2)$ +- Binary states (on/off) are independent: $s(\text{t}_i, s_1)$ vs $s(\text{t}_i, s_2)$ + +Scenarios are connected **only through the objective function** via weights: + +$$ +\min \quad \sum_{s \in \mathcal{S}} w_s \cdot \text{Objective}_s +$$ + +Where: +- $\mathcal{S}$ is the set of scenarios +- $w_s$ is the weight for scenario $s$ +- The optimizer balances performance across scenarios according to their weights + +### Period Independence + +Periods are **completely independent** optimization problems: + +- Each period has separate operational variables +- Each period has separate investment decisions +- No temporal coupling between periods (e.g., storage state at end of period $y$ does not affect period $y+1$) +- Periods cannot exchange resources or information + +Periods are connected **only through weighted aggregation** in the objective: + +$$ +\min \quad \sum_{y \in \mathcal{Y}} w_y \cdot \text{Objective}_y +$$ + +### Shared Periodic Decisions: The Exception + +**Investment decisions (sizes) can be shared across all scenarios:** + +By default, sizes (e.g., Storage capacity, Thermal power, ...) are **scenario-independent** but **flow_rates are scenario-specific**. + +**Example - Flow with investment:** + +$$ +v_\text{invest}(y) = s_\text{invest}(y) \cdot \text{size}_\text{fixed} \quad \text{(one decision per period)} +$$ + +$$ +p(\text{t}_i, y, s) \leq v_\text{invest}(y) \cdot \text{rel}_\text{upper} \quad \forall s \in \mathcal{S} \quad \text{(same capacity for all scenarios)} +$$ + +**Interpretation:** +- "We decide once in period $y$ how much capacity to build" (periodic decision) +- "This capacity is then operated differently in each scenario $s$ within period $y$" (temporal decisions) +- "Periodic effects (investment) are incurred once per period, temporal effects (operational) are weighted across scenarios" + +This reflects real-world investment under uncertainty: you build capacity once (periodic/investment decision), but it operates under different conditions (temporal/operational decisions per scenario). + +**Mathematical Flexibility:** + +Variables can be either scenario-independent or scenario-specific: + +| Variable Type | Scenario-Independent | Scenario-Specific | +|---------------|---------------------|-------------------| +| **Sizes** (e.g., $\text{P}$) | $\text{P}(y)$ - Single value per period | $\text{P}(y, s)$ - Different per scenario | +| **Flow rates** (e.g., $p(\text{t}_i)$) | $p(\text{t}_i, y)$ - Same across scenarios | $p(\text{t}_i, y, s)$ - Different per scenario | + +**Use Cases:** + +*Investment problems (with InvestParameters):* +- **Sizes shared** (default): Investment under uncertainty - build capacity that performs well across all scenarios +- **Sizes vary**: Scenario-specific capacity planning where different investments can be made for each future +- **Selected sizes shared**: Mix of shared critical infrastructure and scenario-specific optional/flexible capacity + +*Dispatch problems (fixed sizes, no investments):* +- **Flow rates shared**: Robust dispatch - find a single operational strategy that works across all forecast scenarios (e.g., day-ahead unit commitment under demand/weather uncertainty) +- **Flow rates vary** (default): Scenario-adaptive dispatch - optimize operations for each scenario's specific conditions (demand, weather, prices) + +For implementation details on controlling scenario independence, see the [`FlowSystem`][flixopt.flow_system.FlowSystem] API reference. + +--- + +## Dimensional Impact on Objective Function + +The objective function aggregates effects across all dimensions with weights: + +### Time Only +$$ +\min \quad \sum_{\text{t}_i \in \mathcal{T}} \sum_{e \in \mathcal{E}} s_{e}(\text{t}_i) +$$ + +### Time + Scenario +$$ +\min \quad \sum_{s \in \mathcal{S}} w_s \cdot \left( \sum_{\text{t}_i \in \mathcal{T}} \sum_{e \in \mathcal{E}} s_{e}(\text{t}_i, s) \right) +$$ + +### Time + Period +$$ +\min \quad \sum_{y \in \mathcal{Y}} w_y \cdot \left( \sum_{\text{t}_i \in \mathcal{T}} \sum_{e \in \mathcal{E}} s_{e}(\text{t}_i, y) \right) +$$ + +### Time + Period + Scenario (Full Multi-Dimensional) +$$ +\min \quad \sum_{y \in \mathcal{Y}} \sum_{s \in \mathcal{S}} w_{y,s} \cdot \left( \sum_{\text{t}_i \in \mathcal{T}} \sum_{e \in \mathcal{E}} s_{e}(\text{t}_i, y, s) \right) +$$ + +Where: +- $\mathcal{T}$ is the set of time steps +- $\mathcal{E}$ is the set of effects +- $\mathcal{S}$ is the set of scenarios +- $\mathcal{Y}$ is the set of periods +- $s_{e}(\cdots)$ are the effect contributions (costs, emissions, etc.) +- $w_s, w_y, w_{y,s}$ are the dimension weights + +**See [Effects, Penalty & Objective](effects-penalty-objective.md) for complete formulations including:** +- How temporal and periodic effects expand with dimensions +- Detailed objective function for each dimensional case +- Periodic (investment) vs temporal (operational) effect handling + +--- + +## Weights + +Weights determine the relative importance of scenarios and periods in the objective function. + +**Specification:** + +```python +flow_system = fx.FlowSystem( + timesteps=timesteps, + periods=periods, + scenarios=scenarios, + weights=weights # Shape depends on dimensions +) +``` + +**Weight Dimensions:** + +| Dimensions Present | Weight Shape | Example | Meaning | +|-------------------|--------------|---------|---------| +| Time + Scenario | 1D array of length `n_scenarios` | `[0.3, 0.7]` | Scenario probabilities | +| Time + Period | 1D array of length `n_periods` | `[0.5, 0.3, 0.2]` | Period importance | +| Time + Period + Scenario | 2D array `(n_periods, n_scenarios)` | `[[0.25, 0.25], [0.25, 0.25]]` | Combined weights | + +**Default:** If not specified, all scenarios/periods have equal weight (normalized to sum to 1). + +**Normalization:** Set `normalize_weights=True` in `Calculation` to automatically normalize weights to sum to 1. + +--- + +## Summary Table + +| Dimension | Required? | Independence | Typical Use Case | +|-----------|-----------|--------------|------------------| +| **time** | ✅ Yes | Variables evolve over time via constraints (e.g., storage balance) | All optimization problems | +| **scenario** | ❌ No | Fully independent operations; shared investments within period | Uncertainty modeling, risk assessment | +| **period** | ❌ No | Fully independent; no coupling between periods | Multi-year planning, long-term investment | + +**Key Principle:** All constraints and formulations operate **within** each (period, scenario) combination independently. Only the objective function couples them via weighted aggregation. + +--- + +## See Also + +- [Effects, Penalty & Objective](effects-penalty-objective.md) - How dimensions affect the objective function +- [InvestParameters](features/InvestParameters.md) - Investment decisions across scenarios +- [FlowSystem API][flixopt.flow_system.FlowSystem] - Creating multi-dimensional systems diff --git a/docs/user-guide/mathematical-notation/effects-penalty-objective.md b/docs/user-guide/mathematical-notation/effects-penalty-objective.md new file mode 100644 index 000000000..0759ef5ee --- /dev/null +++ b/docs/user-guide/mathematical-notation/effects-penalty-objective.md @@ -0,0 +1,286 @@ +# Effects, Penalty & Objective + +## Effects + +[`Effects`][flixopt.effects.Effect] are used to quantify system-wide impacts like costs, emissions, or resource consumption. These arise from **shares** contributed by **Elements** such as [Flows](elements/Flow.md), [Storage](elements/Storage.md), and other components. + +**Example:** + +[`Flows`][flixopt.elements.Flow] have an attribute `effects_per_flow_hour` that defines the effect contribution per flow-hour: +- Costs (€/kWh) +- Emissions (kg CO₂/kWh) +- Primary energy consumption (kWh_primary/kWh) + +Effects are categorized into two domains: + +1. **Temporal effects** - Time-dependent contributions (e.g., operational costs, hourly emissions) +2. **Periodic effects** - Time-independent contributions (e.g., investment costs, fixed annual fees) + +### Multi-Dimensional Effects + +**The formulations below are written with time index $\text{t}_i$ only, but automatically expand when periods and/or scenarios are present.** + +When the FlowSystem has additional dimensions (see [Dimensions](dimensions.md)): + +- **Temporal effects** are indexed by all present dimensions: $E_{e,\text{temp}}(\text{t}_i, y, s)$ +- **Periodic effects** are indexed by period only (scenario-independent within a period): $E_{e,\text{per}}(y)$ +- Effects are aggregated with dimension weights in the objective function + +For complete details on how dimensions affect effects and the objective, see [Dimensions](dimensions.md). + +--- + +## Effect Formulation + +### Shares from Elements + +Each element $l$ contributes shares to effect $e$ in both temporal and periodic domains: + +**Periodic shares** (time-independent): +$$ \label{eq:Share_periodic} +s_{l \rightarrow e, \text{per}} = \sum_{v \in \mathcal{V}_{l, \text{per}}} v \cdot \text{a}_{v \rightarrow e} +$$ + +**Temporal shares** (time-dependent): +$$ \label{eq:Share_temporal} +s_{l \rightarrow e, \text{temp}}(\text{t}_i) = \sum_{v \in \mathcal{V}_{l,\text{temp}}} v(\text{t}_i) \cdot \text{a}_{v \rightarrow e}(\text{t}_i) +$$ + +Where: + +- $\text{t}_i$ is the time step +- $\mathcal{V}_l$ is the set of all optimization variables of element $l$ +- $\mathcal{V}_{l, \text{per}}$ is the subset of periodic (investment-related) variables +- $\mathcal{V}_{l, \text{temp}}$ is the subset of temporal (operational) variables +- $v$ is an optimization variable +- $v(\text{t}_i)$ is the variable value at timestep $\text{t}_i$ +- $\text{a}_{v \rightarrow e}$ is the effect factor (e.g., €/kW for investment, €/kWh for operation) +- $s_{l \rightarrow e, \text{per}}$ is the periodic share of element $l$ to effect $e$ +- $s_{l \rightarrow e, \text{temp}}(\text{t}_i)$ is the temporal share of element $l$ to effect $e$ + +**Examples:** +- **Periodic share**: Investment cost = $\text{size} \cdot \text{specific\_cost}$ (€/kW) +- **Temporal share**: Operational cost = $\text{flow\_rate}(\text{t}_i) \cdot \text{price}(\text{t}_i)$ (€/kWh) + +--- + +### Cross-Effect Contributions + +Effects can contribute shares to other effects, enabling relationships like carbon pricing or resource accounting. + +An effect $x$ can contribute to another effect $e \in \mathcal{E}\backslash x$ via conversion factors: + +**Example:** CO₂ emissions (kg) → Monetary costs (€) +- Effect $x$: "CO₂ emissions" (unit: kg) +- Effect $e$: "costs" (unit: €) +- Factor $\text{r}_{x \rightarrow e}$: CO₂ price (€/kg) + +**Note:** Circular references must be avoided. + +### Total Effect Calculation + +**Periodic effects** aggregate element shares and cross-effect contributions: + +$$ \label{eq:Effect_periodic} +E_{e, \text{per}} = +\sum_{l \in \mathcal{L}} s_{l \rightarrow e,\text{per}} + +\sum_{x \in \mathcal{E}\backslash e} E_{x, \text{per}} \cdot \text{r}_{x \rightarrow e,\text{per}} +$$ + +**Temporal effects** at each timestep: + +$$ \label{eq:Effect_temporal} +E_{e, \text{temp}}(\text{t}_{i}) = +\sum_{l \in \mathcal{L}} s_{l \rightarrow e, \text{temp}}(\text{t}_i) + +\sum_{x \in \mathcal{E}\backslash e} E_{x, \text{temp}}(\text{t}_i) \cdot \text{r}_{x \rightarrow {e},\text{temp}}(\text{t}_i) +$$ + +**Total temporal effects** (sum over all timesteps): + +$$\label{eq:Effect_temporal_total} +E_{e,\text{temp},\text{tot}} = \sum_{i=1}^n E_{e,\text{temp}}(\text{t}_{i}) +$$ + +**Total effect** (combining both domains): + +$$ \label{eq:Effect_Total} +E_{e} = E_{e,\text{per}} + E_{e,\text{temp},\text{tot}} +$$ + +Where: + +- $\mathcal{L}$ is the set of all elements in the FlowSystem +- $\mathcal{E}$ is the set of all effects +- $\text{r}_{x \rightarrow e, \text{per}}$ is the periodic conversion factor from effect $x$ to effect $e$ +- $\text{r}_{x \rightarrow e, \text{temp}}(\text{t}_i)$ is the temporal conversion factor + +--- + +### Constraining Effects + +Effects can be bounded to enforce limits on costs, emissions, or other impacts: + +**Total bounds** (apply to $E_{e,\text{per}}$, $E_{e,\text{temp},\text{tot}}$, or $E_e$): + +$$ \label{eq:Bounds_Total} +E^\text{L} \leq E \leq E^\text{U} +$$ + +**Temporal bounds per timestep:** + +$$ \label{eq:Bounds_Timestep} +E_{e,\text{temp}}^\text{L}(\text{t}_i) \leq E_{e,\text{temp}}(\text{t}_i) \leq E_{e,\text{temp}}^\text{U}(\text{t}_i) +$$ + +**Implementation:** See [`Effect`][flixopt.effects.Effect] parameters: +- `minimum_temporal`, `maximum_temporal` - Total temporal bounds +- `minimum_per_hour`, `maximum_per_hour` - Hourly temporal bounds +- `minimum_periodic`, `maximum_periodic` - Periodic bounds +- `minimum_total`, `maximum_total` - Combined total bounds + +--- + +## Penalty + +In addition to user-defined [Effects](#effects), every FlixOpt model includes a **Penalty** term $\Phi$ to: +- Prevent infeasible problems +- Simplify troubleshooting by allowing constraint violations with high cost + +Penalty shares originate from elements, similar to effect shares: + +$$ \label{eq:Penalty} +\Phi = \sum_{l \in \mathcal{L}} \left( s_{l \rightarrow \Phi} +\sum_{\text{t}_i \in \mathcal{T}} s_{l \rightarrow \Phi}(\text{t}_{i}) \right) +$$ + +Where: + +- $\mathcal{L}$ is the set of all elements +- $\mathcal{T}$ is the set of all timesteps +- $s_{l \rightarrow \Phi}$ is the penalty share from element $l$ + +**Current usage:** Penalties primarily occur in [Buses](elements/Bus.md) via the `excess_penalty_per_flow_hour` parameter, which allows nodal imbalances at a high cost. + +--- + +## Objective Function + +The optimization objective minimizes the chosen effect plus any penalties: + +$$ \label{eq:Objective} +\min \left( E_{\Omega} + \Phi \right) +$$ + +Where: + +- $E_{\Omega}$ is the chosen **objective effect** (see $\eqref{eq:Effect_Total}$) +- $\Phi$ is the [penalty](#penalty) term + +One effect must be designated as the objective via `is_objective=True`. + +### Multi-Criteria Optimization + +This formulation supports multiple optimization approaches: + +**1. Weighted Sum Method** +- The objective effect can incorporate other effects via cross-effect factors +- Example: Minimize costs while including carbon pricing: $\text{CO}_2 \rightarrow \text{costs}$ + +**2. ε-Constraint Method** +- Optimize one effect while constraining others +- Example: Minimize costs subject to $\text{CO}_2 \leq 1000$ kg + +--- + +## Objective with Multiple Dimensions + +When the FlowSystem includes **periods** and/or **scenarios** (see [Dimensions](dimensions.md)), the objective aggregates effects across all dimensions using weights. + +### Time Only (Base Case) + +$$ +\min \quad E_{\Omega} + \Phi = \sum_{\text{t}_i \in \mathcal{T}} E_{\Omega,\text{temp}}(\text{t}_i) + E_{\Omega,\text{per}} + \Phi +$$ + +Where: +- Temporal effects sum over time: $\sum_{\text{t}_i} E_{\Omega,\text{temp}}(\text{t}_i)$ +- Periodic effects are constant: $E_{\Omega,\text{per}}$ +- Penalty sums over time: $\Phi = \sum_{\text{t}_i} \Phi(\text{t}_i)$ + +--- + +### Time + Scenario + +$$ +\min \quad \sum_{s \in \mathcal{S}} w_s \cdot \left( E_{\Omega}(s) + \Phi(s) \right) +$$ + +Where: +- $\mathcal{S}$ is the set of scenarios +- $w_s$ is the weight for scenario $s$ (typically scenario probability) +- Periodic effects are **shared across scenarios**: $E_{\Omega,\text{per}}$ (same for all $s$) +- Temporal effects are **scenario-specific**: $E_{\Omega,\text{temp}}(s) = \sum_{\text{t}_i} E_{\Omega,\text{temp}}(\text{t}_i, s)$ +- Penalties are **scenario-specific**: $\Phi(s) = \sum_{\text{t}_i} \Phi(\text{t}_i, s)$ + +**Interpretation:** +- Investment decisions (periodic) made once, used across all scenarios +- Operations (temporal) differ by scenario +- Objective balances expected value across scenarios + +--- + +### Time + Period + +$$ +\min \quad \sum_{y \in \mathcal{Y}} w_y \cdot \left( E_{\Omega}(y) + \Phi(y) \right) +$$ + +Where: +- $\mathcal{Y}$ is the set of periods (e.g., years) +- $w_y$ is the weight for period $y$ (typically annual discount factor) +- Each period $y$ has **independent** periodic and temporal effects +- Each period $y$ has **independent** investment and operational decisions + +--- + +### Time + Period + Scenario (Full Multi-Dimensional) + +$$ +\min \quad \sum_{y \in \mathcal{Y}} \left[ w_y \cdot E_{\Omega,\text{per}}(y) + \sum_{s \in \mathcal{S}} w_{y,s} \cdot \left( E_{\Omega,\text{temp}}(y,s) + \Phi(y,s) \right) \right] +$$ + +Where: +- $\mathcal{S}$ is the set of scenarios +- $\mathcal{Y}$ is the set of periods +- $w_y$ is the period weight (for periodic effects) +- $w_{y,s}$ is the combined period-scenario weight (for temporal effects) +- **Periodic effects** $E_{\Omega,\text{per}}(y)$ are period-specific but **scenario-independent** +- **Temporal effects** $E_{\Omega,\text{temp}}(y,s) = \sum_{\text{t}_i} E_{\Omega,\text{temp}}(\text{t}_i, y, s)$ are **fully indexed** +- **Penalties** $\Phi(y,s)$ are **fully indexed** + +**Key Principle:** +- Scenarios and periods are **operationally independent** (no energy/resource exchange) +- Coupled **only through the weighted objective function** +- **Periodic effects within a period are shared across all scenarios** (investment made once per period) +- **Temporal effects are independent per scenario** (different operations under different conditions) + +--- + +## Summary + +| Concept | Formulation | Time Dependency | Dimension Indexing | +|---------|-------------|-----------------|-------------------| +| **Temporal share** | $s_{l \rightarrow e, \text{temp}}(\text{t}_i)$ | Time-dependent | $(t, y, s)$ when present | +| **Periodic share** | $s_{l \rightarrow e, \text{per}}$ | Time-independent | $(y)$ when periods present | +| **Total temporal effect** | $E_{e,\text{temp},\text{tot}} = \sum_{\text{t}_i} E_{e,\text{temp}}(\text{t}_i)$ | Sum over time | Depends on dimensions | +| **Total periodic effect** | $E_{e,\text{per}}$ | Constant | $(y)$ when periods present | +| **Total effect** | $E_e = E_{e,\text{per}} + E_{e,\text{temp},\text{tot}}$ | Combined | Depends on dimensions | +| **Objective** | $\min(E_{\Omega} + \Phi)$ | With weights when multi-dimensional | See formulations above | + +--- + +## See Also + +- [Dimensions](dimensions.md) - Complete explanation of multi-dimensional modeling +- [Flow](elements/Flow.md) - Temporal effect contributions via `effects_per_flow_hour` +- [InvestParameters](features/InvestParameters.md) - Periodic effect contributions via investment +- [Effect API][flixopt.effects.Effect] - Implementation details and parameters diff --git a/docs/user-guide/Mathematical Notation/Bus.md b/docs/user-guide/mathematical-notation/elements/Bus.md similarity index 78% rename from docs/user-guide/Mathematical Notation/Bus.md rename to docs/user-guide/mathematical-notation/elements/Bus.md index 6ba17eede..bfe57d234 100644 --- a/docs/user-guide/Mathematical Notation/Bus.md +++ b/docs/user-guide/mathematical-notation/elements/Bus.md @@ -31,3 +31,19 @@ With: - $\text{t}_i$ being the time step - $s_{b \rightarrow \Phi}(\text{t}_i)$ being the penalty term - $\text a_{b \rightarrow \Phi}(\text{t}_i)$ being the penalty coefficient (`excess_penalty_per_flow_hour`) + +--- + +## Implementation + +**Python Class:** [`Bus`][flixopt.elements.Bus] + +See the API documentation for implementation details and usage examples. + +--- + +## See Also + +- [Flow](../elements/Flow.md) - Definition of flow rates in the balance +- [Effects, Penalty & Objective](../effects-penalty-objective.md) - How penalties are included in the objective function +- [Modeling Patterns](../modeling-patterns/index.md) - Mathematical building blocks diff --git a/docs/user-guide/mathematical-notation/elements/Flow.md b/docs/user-guide/mathematical-notation/elements/Flow.md new file mode 100644 index 000000000..5914ba911 --- /dev/null +++ b/docs/user-guide/mathematical-notation/elements/Flow.md @@ -0,0 +1,64 @@ +# Flow + +The flow_rate is the main optimization variable of the Flow. It's limited by the size of the Flow and relative bounds \eqref{eq:flow_rate}. + +$$ \label{eq:flow_rate} + \text P \cdot \text p^{\text{L}}_{\text{rel}}(\text{t}_{i}) + \leq p(\text{t}_{i}) \leq + \text P \cdot \text p^{\text{U}}_{\text{rel}}(\text{t}_{i}) +$$ + +With: + +- $\text P$ being the size of the Flow +- $p(\text{t}_{i})$ being the flow-rate at time $\text{t}_{i}$ +- $\text p^{\text{L}}_{\text{rel}}(\text{t}_{i})$ being the relative lower bound (typically 0) +- $\text p^{\text{U}}_{\text{rel}}(\text{t}_{i})$ being the relative upper bound (typically 1) + +With $\text p^{\text{L}}_{\text{rel}}(\text{t}_{i}) = 0$ and $\text p^{\text{U}}_{\text{rel}}(\text{t}_{i}) = 1$, +equation \eqref{eq:flow_rate} simplifies to + +$$ + 0 \leq p(\text{t}_{i}) \leq \text P +$$ + + +This mathematical formulation can be extended by using [OnOffParameters](../features/OnOffParameters.md) +to define the on/off state of the Flow, or by using [InvestParameters](../features/InvestParameters.md) +to change the size of the Flow from a constant to an optimization variable. + +--- + +## Mathematical Patterns Used + +Flow formulation uses the following modeling patterns: + +- **[Scaled Bounds](../modeling-patterns/bounds-and-states.md#scaled-bounds)** - Basic flow rate bounds (equation $\eqref{eq:flow_rate}$) +- **[Scaled Bounds with State](../modeling-patterns/bounds-and-states.md#scaled-bounds-with-state)** - When combined with [OnOffParameters](../features/OnOffParameters.md) +- **[Bounds with State](../modeling-patterns/bounds-and-states.md#bounds-with-state)** - Investment decisions with [InvestParameters](../features/InvestParameters.md) + +--- + +## Implementation + +**Python Class:** [`Flow`][flixopt.elements.Flow] + +**Key Parameters:** +- `size`: Flow size $\text{P}$ (can be fixed or variable with InvestParameters) +- `relative_minimum`, `relative_maximum`: Relative bounds $\text{p}^{\text{L}}_{\text{rel}}, \text{p}^{\text{U}}_{\text{rel}}$ +- `effects_per_flow_hour`: Operational effects (costs, emissions, etc.) +- `invest_parameters`: Optional investment modeling (see [InvestParameters](../features/InvestParameters.md)) +- `on_off_parameters`: Optional on/off operation (see [OnOffParameters](../features/OnOffParameters.md)) + +See the [`Flow`][flixopt.elements.Flow] API documentation for complete parameter list and usage examples. + +--- + +## See Also + +- [OnOffParameters](../features/OnOffParameters.md) - Binary on/off operation +- [InvestParameters](../features/InvestParameters.md) - Variable flow sizing +- [Bus](../elements/Bus.md) - Flow balance constraints +- [LinearConverter](../elements/LinearConverter.md) - Flow ratio constraints +- [Storage](../elements/Storage.md) - Flow integration over time +- [Modeling Patterns](../modeling-patterns/index.md) - Mathematical building blocks diff --git a/docs/user-guide/mathematical-notation/elements/LinearConverter.md b/docs/user-guide/mathematical-notation/elements/LinearConverter.md new file mode 100644 index 000000000..b007aa7f5 --- /dev/null +++ b/docs/user-guide/mathematical-notation/elements/LinearConverter.md @@ -0,0 +1,50 @@ +[`LinearConverters`][flixopt.components.LinearConverter] define a ratio between incoming and outgoing [Flows](../elements/Flow.md). + +$$ \label{eq:Linear-Transformer-Ratio} + \sum_{f_{\text{in}} \in \mathcal F_{in}} \text a_{f_{\text{in}}}(\text{t}_i) \cdot p_{f_\text{in}}(\text{t}_i) = \sum_{f_{\text{out}} \in \mathcal F_{out}} \text b_{f_\text{out}}(\text{t}_i) \cdot p_{f_\text{out}}(\text{t}_i) +$$ + +With: + +- $\mathcal F_{in}$ and $\mathcal F_{out}$ being the set of all incoming and outgoing flows +- $p_{f_\text{in}}(\text{t}_i)$ and $p_{f_\text{out}}(\text{t}_i)$ being the flow-rate at time $\text{t}_i$ for flow $f_\text{in}$ and $f_\text{out}$, respectively +- $\text a_{f_\text{in}}(\text{t}_i)$ and $\text b_{f_\text{out}}(\text{t}_i)$ being the ratio of the flow-rate at time $\text{t}_i$ for flow $f_\text{in}$ and $f_\text{out}$, respectively + +With one incoming **Flow** and one outgoing **Flow**, this can be simplified to: + +$$ \label{eq:Linear-Transformer-Ratio-simple} + \text a(\text{t}_i) \cdot p_{f_\text{in}}(\text{t}_i) = p_{f_\text{out}}(\text{t}_i) +$$ + +where $\text a$ can be interpreted as the conversion efficiency of the **LinearConverter**. + +#### Piecewise Conversion factors +The conversion efficiency can be defined as a piecewise linear approximation. See [Piecewise](../features/Piecewise.md) for more details. + +--- + +## Implementation + +**Python Class:** [`LinearConverter`][flixopt.components.LinearConverter] + +**Specialized Linear Converters:** + +FlixOpt provides specialized linear converter classes for common applications: + +- **[`HeatPump`][flixopt.linear_converters.HeatPump]** - Coefficient of Performance (COP) based conversion +- **[`Power2Heat`][flixopt.linear_converters.Power2Heat]** - Electric heating with efficiency ≤ 1 +- **[`CHP`][flixopt.linear_converters.CHP]** - Combined heat and power generation +- **[`Boiler`][flixopt.linear_converters.Boiler]** - Fuel to heat conversion + +These classes handle the mathematical formulation automatically based on physical relationships. + +See the API documentation for implementation details and usage examples. + +--- + +## See Also + +- [Flow](../elements/Flow.md) - Definition of flow rates +- [Piecewise](../features/Piecewise.md) - Non-linear conversion efficiency modeling +- [InvestParameters](../features/InvestParameters.md) - Variable converter sizing +- [Modeling Patterns](../modeling-patterns/index.md) - Mathematical building blocks diff --git a/docs/user-guide/Mathematical Notation/Storage.md b/docs/user-guide/mathematical-notation/elements/Storage.md similarity index 52% rename from docs/user-guide/Mathematical Notation/Storage.md rename to docs/user-guide/mathematical-notation/elements/Storage.md index 63f01d198..cd7046592 100644 --- a/docs/user-guide/Mathematical Notation/Storage.md +++ b/docs/user-guide/mathematical-notation/elements/Storage.md @@ -1,5 +1,5 @@ # Storages -**Storages** have one incoming and one outgoing **[Flow](Flow.md)** with a charging and discharging efficiency. +**Storages** have one incoming and one outgoing **[Flow](../elements/Flow.md)** with a charging and discharging efficiency. A storage has a state of charge $c(\text{t}_i)$ which is limited by its `size` $\text C$ and relative bounds $\eqref{eq:Storage_Bounds}$. $$ \label{eq:Storage_Bounds} @@ -25,9 +25,9 @@ $ \dot{ \text c}_\text{rel, loss}(\text{t}_i)$ expresses the "loss fraction per $$ \begin{align*} - c(\text{t}_{i+1}) &= c(\text{t}_{i}) \cdot (1-\dot{\text{c}}_\text{rel,loss}(\text{t}_i) \cdot \Delta \text{t}_{i}) \\ + c(\text{t}_{i+1}) &= c(\text{t}_{i}) \cdot (1-\dot{\text{c}}_\text{rel,loss}(\text{t}_i))^{\Delta \text{t}_{i}} \\ &\quad + p_{f_\text{in}}(\text{t}_i) \cdot \Delta \text{t}_i \cdot \eta_\text{in}(\text{t}_i) \\ - &\quad - \frac{p_{f_\text{out}}(\text{t}_i) \cdot \Delta \text{t}_i}{\eta_\text{out}(\text{t}_i)} + &\quad - p_{f_\text{out}}(\text{t}_i) \cdot \Delta \text{t}_i \cdot \eta_\text{out}(\text{t}_i) \tag{3} \end{align*} $$ @@ -42,3 +42,38 @@ Where: - $\eta_\text{in}(\text{t}_i)$ is the charging efficiency at time $\text{t}_i$ - $p_{f_\text{out}}(\text{t}_i)$ is the output flow rate at time $\text{t}_i$ - $\eta_\text{out}(\text{t}_i)$ is the discharging efficiency at time $\text{t}_i$ + +--- + +## Mathematical Patterns Used + +Storage formulation uses the following modeling patterns: + +- **[Basic Bounds](../modeling-patterns/bounds-and-states.md#basic-bounds)** - For charge state bounds (equation $\eqref{eq:Storage_Bounds}$) +- **[Scaled Bounds](../modeling-patterns/bounds-and-states.md#scaled-bounds)** - For flow rate bounds relative to storage size + +When combined with investment parameters, storage can use: +- **[Bounds with State](../modeling-patterns/bounds-and-states.md#bounds-with-state)** - Investment decisions (see [InvestParameters](../features/InvestParameters.md)) + +--- + +## Implementation + +**Python Class:** [`Storage`][flixopt.components.Storage] + +**Key Parameters:** +- `capacity_in_flow_hours`: Storage capacity $\text{C}$ +- `relative_loss_per_hour`: Self-discharge rate $\dot{\text{c}}_\text{rel,loss}$ +- `initial_charge_state`: Initial charge $c(\text{t}_0)$ +- `minimal_final_charge_state`, `maximal_final_charge_state`: Final charge bounds $c(\text{t}_\text{end})$ (optional) +- `eta_charge`, `eta_discharge`: Charging/discharging efficiencies $\eta_\text{in}, \eta_\text{out}$ + +See the [`Storage`][flixopt.components.Storage] API documentation for complete parameter list and usage examples. + +--- + +## See Also + +- [Flow](../elements/Flow.md) - Input and output flow definitions +- [InvestParameters](../features/InvestParameters.md) - Variable storage sizing +- [Modeling Patterns](../modeling-patterns/index.md) - Mathematical building blocks diff --git a/docs/user-guide/mathematical-notation/features/InvestParameters.md b/docs/user-guide/mathematical-notation/features/InvestParameters.md new file mode 100644 index 000000000..14fe02c79 --- /dev/null +++ b/docs/user-guide/mathematical-notation/features/InvestParameters.md @@ -0,0 +1,302 @@ +# InvestParameters + +[`InvestParameters`][flixopt.interface.InvestParameters] model investment decisions in optimization problems, enabling both binary (invest/don't invest) and continuous sizing choices with comprehensive cost modeling. + +## Investment Decision Types + +FlixOpt supports two main types of investment decisions: + +### Binary Investment + +Fixed-size investment creating a yes/no decision (e.g., install a 100 kW generator): + +$$\label{eq:invest_binary} +v_\text{invest} = s_\text{invest} \cdot \text{size}_\text{fixed} +$$ + +With: +- $v_\text{invest}$ being the resulting investment size +- $s_\text{invest} \in \{0, 1\}$ being the binary investment decision +- $\text{size}_\text{fixed}$ being the predefined component size + +**Behavior:** +- $s_\text{invest} = 0$: no investment ($v_\text{invest} = 0$) +- $s_\text{invest} = 1$: invest at fixed size ($v_\text{invest} = \text{size}_\text{fixed}$) + +--- + +### Continuous Sizing + +Variable-size investment with bounds (e.g., battery capacity from 10-1000 kWh): + +$$\label{eq:invest_continuous} +s_\text{invest} \cdot \text{size}_\text{min} \leq v_\text{invest} \leq s_\text{invest} \cdot \text{size}_\text{max} +$$ + +With: +- $v_\text{invest}$ being the investment size variable (continuous) +- $s_\text{invest} \in \{0, 1\}$ being the binary investment decision +- $\text{size}_\text{min}$ being the minimum investment size (if investing) +- $\text{size}_\text{max}$ being the maximum investment size + +**Behavior:** +- $s_\text{invest} = 0$: no investment ($v_\text{invest} = 0$) +- $s_\text{invest} = 1$: invest with size in $[\text{size}_\text{min}, \text{size}_\text{max}]$ + +This uses the **bounds with state** pattern described in [Bounds and States](../modeling-patterns/bounds-and-states.md#bounds-with-state). + +--- + +### Optional vs. Mandatory Investment + +The `mandatory` parameter controls whether investment is required: + +**Optional Investment** (`mandatory=False`, default): +$$\label{eq:invest_optional} +s_\text{invest} \in \{0, 1\} +$$ + +The optimization can freely choose to invest or not. + +**Mandatory Investment** (`mandatory=True`): +$$\label{eq:invest_mandatory} +s_\text{invest} = 1 +$$ + +The investment must occur (useful for mandatory upgrades or replacements). + +--- + +## Effect Modeling + +Investment effects (costs, emissions, etc.) are modeled using three components: + +### Fixed Effects + +One-time effects incurred if investment is made, independent of size: + +$$\label{eq:invest_fixed_effects} +E_{e,\text{fix}} = s_\text{invest} \cdot \text{fix}_e +$$ + +With: +- $E_{e,\text{fix}}$ being the fixed contribution to effect $e$ +- $\text{fix}_e$ being the fixed effect value (e.g., fixed installation cost) + +**Examples:** +- Fixed installation costs (permits, grid connection) +- One-time environmental impacts (land preparation) +- Fixed labor or administrative costs + +--- + +### Specific Effects + +Effects proportional to investment size (per-unit costs): + +$$\label{eq:invest_specific_effects} +E_{e,\text{spec}} = v_\text{invest} \cdot \text{spec}_e +$$ + +With: +- $E_{e,\text{spec}}$ being the size-dependent contribution to effect $e$ +- $\text{spec}_e$ being the specific effect value per unit size (e.g., €/kW) + +**Examples:** +- Equipment costs (€/kW) +- Material requirements (kg steel/kW) +- Recurring costs (€/kW/year maintenance) + +--- + +### Piecewise Effects + +Non-linear effect relationships using piecewise linear approximations: + +$$\label{eq:invest_piecewise_effects} +E_{e,\text{pw}} = \sum_{k=1}^{K} \lambda_k \cdot r_{e,k} +$$ + +Subject to: +$$ +v_\text{invest} = \sum_{k=1}^{K} \lambda_k \cdot v_k +$$ + +With: +- $E_{e,\text{pw}}$ being the piecewise contribution to effect $e$ +- $\lambda_k$ being the piecewise lambda variables (see [Piecewise](../features/Piecewise.md)) +- $r_{e,k}$ being the effect rate at piece $k$ +- $v_k$ being the size points defining the pieces + +**Use cases:** +- Economies of scale (bulk discounts) +- Technology learning curves +- Threshold effects (capacity tiers with different costs) + +See [Piecewise](../features/Piecewise.md) for detailed mathematical formulation. + +--- + +### Retirement Effects + +Effects incurred if investment is NOT made (when retiring/not replacing existing equipment): + +$$\label{eq:invest_retirement_effects} +E_{e,\text{retirement}} = (1 - s_\text{invest}) \cdot \text{retirement}_e +$$ + +With: +- $E_{e,\text{retirement}}$ being the retirement contribution to effect $e$ +- $\text{retirement}_e$ being the retirement effect value + +**Behavior:** +- $s_\text{invest} = 0$: retirement effects are incurred +- $s_\text{invest} = 1$: no retirement effects + +**Examples:** +- Demolition or disposal costs +- Decommissioning expenses +- Contractual penalties for not investing +- Opportunity costs or lost revenues + +--- + +### Total Investment Effects + +The total contribution to effect $e$ from an investment is: + +$$\label{eq:invest_total_effects} +E_{e,\text{invest}} = E_{e,\text{fix}} + E_{e,\text{spec}} + E_{e,\text{pw}} + E_{e,\text{retirement}} +$$ + +Effects integrate into the overall system effects as described in [Effects, Penalty & Objective](../effects-penalty-objective.md). + +--- + +## Integration with Components + +Investment parameters modify component sizing: + +### Without Investment +Component size is a fixed parameter: +$$ +\text{size} = \text{size}_\text{nominal} +$$ + +### With Investment +Component size becomes a variable: +$$ +\text{size} = v_\text{invest} +$$ + +This size variable then appears in component constraints. For example, flow rate bounds become: + +$$ +v_\text{invest} \cdot \text{rel}_\text{lower} \leq p(t) \leq v_\text{invest} \cdot \text{rel}_\text{upper} +$$ + +Using the **scaled bounds** pattern from [Bounds and States](../modeling-patterns/bounds-and-states.md#scaled-bounds). + +--- + +## Cost Annualization + +**Important:** All investment cost values must be properly weighted to match the optimization model's time horizon. + +For long-term investments, costs should be annualized: + +$$\label{eq:annualization} +\text{cost}_\text{annual} = \frac{\text{cost}_\text{capital} \cdot r}{1 - (1 + r)^{-n}} +$$ + +With: +- $\text{cost}_\text{capital}$ being the upfront investment cost +- $r$ being the discount rate +- $n$ being the equipment lifetime in years + +**Example:** €1,000,000 equipment with 20-year life and 5% discount rate +$$ +\text{cost}_\text{annual} = \frac{1{,}000{,}000 \cdot 0.05}{1 - (1.05)^{-20}} \approx €80{,}243/\text{year} +$$ + +--- + +## Implementation + +**Python Class:** [`InvestParameters`][flixopt.interface.InvestParameters] + +**Key Parameters:** +- `fixed_size`: For binary investments (mutually exclusive with continuous sizing) +- `minimum_size`, `maximum_size`: For continuous sizing +- `mandatory`: Whether investment is required (default: `False`) +- `effects_of_investment`: Fixed effects incurred when investing (replaces deprecated `fix_effects`) +- `effects_of_investment_per_size`: Per-unit effects proportional to size (replaces deprecated `specific_effects`) +- `piecewise_effects_of_investment`: Non-linear effect modeling (replaces deprecated `piecewise_effects`) +- `effects_of_retirement`: Effects for not investing (replaces deprecated `divest_effects`) + +See the [`InvestParameters`][flixopt.interface.InvestParameters] API documentation for complete parameter list and usage examples. + +**Used in:** +- [`Flow`][flixopt.elements.Flow] - Flexible capacity decisions +- [`Storage`][flixopt.components.Storage] - Storage sizing optimization +- [`LinearConverter`][flixopt.components.LinearConverter] - Converter capacity planning +- All components supporting investment decisions + +--- + +## Examples + +### Binary Investment (Solar Panels) +```python +solar_investment = InvestParameters( + fixed_size=100, # 100 kW system + mandatory=False, # Optional investment (default) + effects_of_investment={'cost': 25000}, # Installation costs + effects_of_investment_per_size={'cost': 1200}, # €1200/kW +) +``` + +### Continuous Sizing (Battery) +```python +battery_investment = InvestParameters( + minimum_size=10, # kWh + maximum_size=1000, + mandatory=False, # Optional investment (default) + effects_of_investment={'cost': 5000}, # Grid connection + effects_of_investment_per_size={'cost': 600}, # €600/kWh +) +``` + +### With Retirement Costs (Replacement) +```python +boiler_replacement = InvestParameters( + minimum_size=50, # kW + maximum_size=200, + mandatory=False, # Optional investment (default) + effects_of_investment={'cost': 15000}, + effects_of_investment_per_size={'cost': 400}, + effects_of_retirement={'cost': 8000}, # Demolition if not replaced +) +``` + +### Economies of Scale (Piecewise) +```python +battery_investment = InvestParameters( + minimum_size=10, + maximum_size=1000, + piecewise_effects_of_investment=PiecewiseEffects( + piecewise_origin=Piecewise([ + Piece(0, 100), # Small + Piece(100, 500), # Medium + Piece(500, 1000), # Large + ]), + piecewise_shares={ + 'cost': Piecewise([ + Piece(800, 750), # €800-750/kWh + Piece(750, 600), # €750-600/kWh + Piece(600, 500), # €600-500/kWh (bulk discount) + ]) + }, + ), +) +``` diff --git a/docs/user-guide/mathematical-notation/features/OnOffParameters.md b/docs/user-guide/mathematical-notation/features/OnOffParameters.md new file mode 100644 index 000000000..4ec6a9726 --- /dev/null +++ b/docs/user-guide/mathematical-notation/features/OnOffParameters.md @@ -0,0 +1,307 @@ +# OnOffParameters + +[`OnOffParameters`][flixopt.interface.OnOffParameters] model equipment that operates in discrete on/off states rather than continuous operation. This captures realistic operational constraints including startup costs, minimum run times, cycling limitations, and maintenance scheduling. + +## Binary State Variable + +Equipment operation is modeled using a binary state variable: + +$$\label{eq:onoff_state} +s(t) \in \{0, 1\} \quad \forall t +$$ + +With: +- $s(t) = 1$: equipment is operating (on state) +- $s(t) = 0$: equipment is shutdown (off state) + +This state variable controls the equipment's operational constraints and modifies flow bounds using the **bounds with state** pattern from [Bounds and States](../modeling-patterns/bounds-and-states.md#bounds-with-state). + +--- + +## State Transitions and Switching + +State transitions are tracked using switch variables (see [State Transitions](../modeling-patterns/state-transitions.md#binary-state-transitions)): + +$$\label{eq:onoff_transitions} +s^\text{on}(t) - s^\text{off}(t) = s(t) - s(t-1) \quad \forall t > 0 +$$ + +$$\label{eq:onoff_switch_exclusivity} +s^\text{on}(t) + s^\text{off}(t) \leq 1 \quad \forall t +$$ + +With: +- $s^\text{on}(t) \in \{0, 1\}$: equals 1 when switching from off to on (startup) +- $s^\text{off}(t) \in \{0, 1\}$: equals 1 when switching from on to off (shutdown) + +**Behavior:** +- Off → On: $s^\text{on}(t) = 1, s^\text{off}(t) = 0$ +- On → Off: $s^\text{on}(t) = 0, s^\text{off}(t) = 1$ +- No change: $s^\text{on}(t) = 0, s^\text{off}(t) = 0$ + +--- + +## Effects and Costs + +### Switching Effects + +Effects incurred when equipment starts up: + +$$\label{eq:onoff_switch_effects} +E_{e,\text{switch}} = \sum_{t} s^\text{on}(t) \cdot \text{effect}_{e,\text{switch}} +$$ + +With: +- $\text{effect}_{e,\text{switch}}$ being the effect value per startup event + +**Examples:** +- Startup fuel consumption +- Wear and tear costs +- Labor costs for startup procedures +- Inrush power demands + +--- + +### Running Effects + +Effects incurred while equipment is operating: + +$$\label{eq:onoff_running_effects} +E_{e,\text{run}} = \sum_{t} s(t) \cdot \Delta t \cdot \text{effect}_{e,\text{run}} +$$ + +With: +- $\text{effect}_{e,\text{run}}$ being the effect rate per operating hour +- $\Delta t$ being the time step duration + +**Examples:** +- Fixed operating and maintenance costs +- Auxiliary power consumption +- Consumable materials +- Emissions while running + +--- + +## Operating Hour Constraints + +### Total Operating Hours + +Bounds on total operating time across the planning horizon: + +$$\label{eq:onoff_total_hours} +h_\text{min} \leq \sum_{t} s(t) \cdot \Delta t \leq h_\text{max} +$$ + +With: +- $h_\text{min}$ being the minimum total operating hours +- $h_\text{max}$ being the maximum total operating hours + +**Use cases:** +- Minimum runtime requirements (contracts, maintenance) +- Maximum runtime limits (fuel availability, permits, equipment life) + +--- + +### Consecutive Operating Hours + +**Minimum Consecutive On-Time:** + +Enforces minimum runtime once started using duration tracking (see [Duration Tracking](../modeling-patterns/duration-tracking.md#minimum-duration-constraints)): + +$$\label{eq:onoff_min_on_duration} +d^\text{on}(t) \geq (s(t-1) - s(t)) \cdot h^\text{on}_\text{min} \quad \forall t > 0 +$$ + +With: +- $d^\text{on}(t)$ being the consecutive on-time duration at time $t$ +- $h^\text{on}_\text{min}$ being the minimum required on-time + +**Behavior:** +- When shutting down at time $t$: enforces equipment was on for at least $h^\text{on}_\text{min}$ prior to the switch +- Prevents short cycling and frequent startups + +**Maximum Consecutive On-Time:** + +Limits continuous operation before requiring shutdown: + +$$\label{eq:onoff_max_on_duration} +d^\text{on}(t) \leq h^\text{on}_\text{max} \quad \forall t +$$ + +**Use cases:** +- Mandatory maintenance intervals +- Process batch time limits +- Thermal cycling requirements + +--- + +### Consecutive Shutdown Hours + +**Minimum Consecutive Off-Time:** + +Enforces minimum shutdown duration before restarting: + +$$\label{eq:onoff_min_off_duration} +d^\text{off}(t) \geq (s(t) - s(t-1)) \cdot h^\text{off}_\text{min} \quad \forall t > 0 +$$ + +With: +- $d^\text{off}(t)$ being the consecutive off-time duration at time $t$ +- $h^\text{off}_\text{min}$ being the minimum required off-time + +**Use cases:** +- Cooling periods +- Maintenance requirements +- Process stabilization + +**Maximum Consecutive Off-Time:** + +Limits shutdown duration before mandatory restart: + +$$\label{eq:onoff_max_off_duration} +d^\text{off}(t) \leq h^\text{off}_\text{max} \quad \forall t +$$ + +**Use cases:** +- Equipment preservation requirements +- Process stability needs +- Contractual minimum activity levels + +--- + +## Cycling Limits + +Maximum number of startups across the planning horizon: + +$$\label{eq:onoff_max_switches} +\sum_{t} s^\text{on}(t) \leq n_\text{max} +$$ + +With: +- $n_\text{max}$ being the maximum allowed number of startups + +**Use cases:** +- Preventing excessive equipment wear +- Grid stability requirements +- Operational complexity limits +- Maintenance budget constraints + +--- + +## Integration with Flow Bounds + +OnOffParameters modify flow rate bounds by coupling them to the on/off state. + +**Without OnOffParameters** (continuous operation): +$$ +P \cdot \text{rel}_\text{lower} \leq p(t) \leq P \cdot \text{rel}_\text{upper} +$$ + +**With OnOffParameters** (binary operation): +$$ +s(t) \cdot P \cdot \max(\varepsilon, \text{rel}_\text{lower}) \leq p(t) \leq s(t) \cdot P \cdot \text{rel}_\text{upper} +$$ + +Using the **bounds with state** pattern from [Bounds and States](../modeling-patterns/bounds-and-states.md#bounds-with-state). + +**Behavior:** +- When $s(t) = 0$: flow is forced to zero +- When $s(t) = 1$: flow follows normal bounds + +--- + +## Complete Formulation Summary + +For equipment with OnOffParameters, the complete constraint system includes: + +1. **State variable:** $s(t) \in \{0, 1\}$ +2. **Switch tracking:** $s^\text{on}(t) - s^\text{off}(t) = s(t) - s(t-1)$ +3. **Switch exclusivity:** $s^\text{on}(t) + s^\text{off}(t) \leq 1$ +4. **Duration tracking:** + - On-duration: $d^\text{on}(t)$ following duration tracking pattern + - Off-duration: $d^\text{off}(t)$ following duration tracking pattern +5. **Minimum on-time:** $d^\text{on}(t) \geq (s(t-1) - s(t)) \cdot h^\text{on}_\text{min}$ +6. **Maximum on-time:** $d^\text{on}(t) \leq h^\text{on}_\text{max}$ +7. **Minimum off-time:** $d^\text{off}(t) \geq (s(t) - s(t-1)) \cdot h^\text{off}_\text{min}$ +8. **Maximum off-time:** $d^\text{off}(t) \leq h^\text{off}_\text{max}$ +9. **Total hours:** $h_\text{min} \leq \sum_t s(t) \cdot \Delta t \leq h_\text{max}$ +10. **Cycling limit:** $\sum_t s^\text{on}(t) \leq n_\text{max}$ +11. **Flow bounds:** $s(t) \cdot P \cdot \text{rel}_\text{lower} \leq p(t) \leq s(t) \cdot P \cdot \text{rel}_\text{upper}$ + +--- + +## Implementation + +**Python Class:** [`OnOffParameters`][flixopt.interface.OnOffParameters] + +**Key Parameters:** +- `effects_per_switch_on`: Costs per startup event +- `effects_per_running_hour`: Costs per hour of operation +- `on_hours_total_min`, `on_hours_total_max`: Total runtime bounds +- `consecutive_on_hours_min`, `consecutive_on_hours_max`: Consecutive runtime bounds +- `consecutive_off_hours_min`, `consecutive_off_hours_max`: Consecutive shutdown bounds +- `switch_on_total_max`: Maximum number of startups +- `force_switch_on`: Create switch variables even without limits (for tracking) + +See the [`OnOffParameters`][flixopt.interface.OnOffParameters] API documentation for complete parameter list and usage examples. + +**Mathematical Patterns Used:** +- [State Transitions](../modeling-patterns/state-transitions.md#binary-state-transitions) - Switch tracking +- [Duration Tracking](../modeling-patterns/duration-tracking.md) - Consecutive time constraints +- [Bounds with State](../modeling-patterns/bounds-and-states.md#bounds-with-state) - Flow control + +**Used in:** +- [`Flow`][flixopt.elements.Flow] - On/off operation for flows +- All components supporting discrete operational states + +--- + +## Examples + +### Power Plant with Startup Costs +```python +power_plant = OnOffParameters( + effects_per_switch_on={'startup_cost': 25000}, # €25k per startup + effects_per_running_hour={'fixed_om': 125}, # €125/hour while running + consecutive_on_hours_min=8, # Minimum 8-hour run + consecutive_off_hours_min=4, # 4-hour cooling period + on_hours_total_max=6000, # Annual limit +) +``` + +### Batch Process with Cycling Limits +```python +batch_reactor = OnOffParameters( + effects_per_switch_on={'setup_cost': 1500}, + consecutive_on_hours_min=12, # 12-hour minimum batch + consecutive_on_hours_max=24, # 24-hour maximum batch + consecutive_off_hours_min=6, # Cleaning time + switch_on_total_max=200, # Max 200 batches +) +``` + +### HVAC with Cycle Prevention +```python +hvac = OnOffParameters( + effects_per_switch_on={'compressor_wear': 0.5}, + consecutive_on_hours_min=1, # Prevent short cycling + consecutive_off_hours_min=0.5, # 30-min minimum off + switch_on_total_max=2000, # Limit compressor starts +) +``` + +### Backup Generator with Testing Requirements +```python +backup_gen = OnOffParameters( + effects_per_switch_on={'fuel_priming': 50}, # L diesel + consecutive_on_hours_min=0.5, # 30-min test duration + consecutive_off_hours_max=720, # Test every 30 days + on_hours_total_min=26, # Weekly testing requirement +) +``` + +--- + +## Notes + +**Time Series Boundary:** The final time period constraints for consecutive_on_hours_min/max and consecutive_off_hours_min/max are not enforced at the end of the planning horizon. This allows optimization to end with ongoing campaigns that may be shorter/longer than specified, as they extend beyond the modeled period. diff --git a/docs/user-guide/Mathematical Notation/Piecewise.md b/docs/user-guide/mathematical-notation/features/Piecewise.md similarity index 100% rename from docs/user-guide/Mathematical Notation/Piecewise.md rename to docs/user-guide/mathematical-notation/features/Piecewise.md diff --git a/docs/user-guide/mathematical-notation/index.md b/docs/user-guide/mathematical-notation/index.md new file mode 100644 index 000000000..27e7b7e9a --- /dev/null +++ b/docs/user-guide/mathematical-notation/index.md @@ -0,0 +1,123 @@ + +# Mathematical Notation + +This section provides the **mathematical formulations** underlying FlixOpt's optimization models. It is intended as **reference documentation** for users who want to understand the mathematical details behind the high-level FlixOpt API described in the [FlixOpt Concepts](../core-concepts.md) guide. + +**For typical usage**, refer to the [FlixOpt Concepts](../core-concepts.md) guide, [Examples](../../examples/index.md), and [API Reference](../../api-reference/index.md) - you don't need to understand these mathematical formulations to use FlixOpt effectively. + +--- + +## Naming Conventions + +FlixOpt uses the following naming conventions: + +- All optimization variables are denoted by italic letters (e.g., $x$, $y$, $z$) +- All parameters and constants are denoted by non italic small letters (e.g., $\text{a}$, $\text{b}$, $\text{c}$) +- All Sets are denoted by greek capital letters (e.g., $\mathcal{F}$, $\mathcal{E}$) +- All units of a set are denoted by greek small letters (e.g., $\mathcal{f}$, $\mathcal{e}$) +- The letter $i$ is used to denote an index (e.g., $i=1,\dots,\text n$) +- All time steps are denoted by the letter $\text{t}$ (e.g., $\text{t}_0$, $\text{t}_1$, $\text{t}_i$) + +## Dimensions and Time Steps + +FlixOpt supports multi-dimensional optimization with up to three dimensions: **time** (mandatory), **period** (optional), and **scenario** (optional). + +**All mathematical formulations in this documentation are independent of whether periods or scenarios are present.** The equations shown are written with time index $\text{t}_i$ only, but automatically expand to additional dimensions when periods/scenarios are added. + +For complete details on dimensions, their relationships, and influence on formulations, see **[Dimensions](dimensions.md)**. + +### Time Steps + +Time steps are defined as a sequence of discrete time steps $\text{t}_i \in \mathcal{T} \quad \text{for} \quad i \in \{1, 2, \dots, \text{n}\}$ (left-aligned in its timespan). +From this sequence, the corresponding time intervals $\Delta \text{t}_i \in \Delta \mathcal{T}$ are derived as + +$$\Delta \text{t}_i = \text{t}_{i+1} - \text{t}_i \quad \text{for} \quad i \in \{1, 2, \dots, \text{n}-1\}$$ + +The final time interval $\Delta \text{t}_\text n$ defaults to $\Delta \text{t}_\text n = \Delta \text{t}_{\text n-1}$, but is of course customizable. +Non-equidistant time steps are also supported. + +--- + +## Documentation Structure + +This reference is organized to match the FlixOpt API structure: + +### Elements +Mathematical formulations for core FlixOpt elements (corresponding to [`flixopt.elements`][flixopt.elements]): + +- [Flow](elements/Flow.md) - Flow rate constraints and bounds +- [Bus](elements/Bus.md) - Nodal balance equations +- [Storage](elements/Storage.md) - Storage balance and charge state evolution +- [LinearConverter](elements/LinearConverter.md) - Linear conversion relationships + +**User API:** When you create a `Flow`, `Bus`, `Storage`, or `LinearConverter` in your FlixOpt model, these mathematical formulations are automatically applied. + +### Features +Mathematical formulations for optional features (corresponding to parameters in FlixOpt classes): + +- [InvestParameters](features/InvestParameters.md) - Investment decision modeling +- [OnOffParameters](features/OnOffParameters.md) - Binary on/off operation +- [Piecewise](features/Piecewise.md) - Piecewise linear approximations + +**User API:** When you pass `invest_parameters` or `on_off_parameters` to a `Flow` or component, these formulations are applied. + +### System-Level +- [Effects, Penalty & Objective](effects-penalty-objective.md) - Cost allocation and objective function + +**User API:** When you create [`Effect`][flixopt.effects.Effect] objects and set `effects_per_flow_hour`, these formulations govern how costs are calculated. + +### Modeling Patterns (Advanced) +**Internal implementation details** - These low-level patterns are used internally by Elements and Features. They are documented here for: + +- Developers extending FlixOpt +- Advanced users debugging models or understanding solver behavior +- Researchers comparing mathematical formulations + +**Normal users do not need to read this section** - the patterns are automatically applied when you use Elements and Features: + +- [Bounds and States](modeling-patterns/bounds-and-states.md) - Variable bounding patterns +- [Duration Tracking](modeling-patterns/duration-tracking.md) - Consecutive time period tracking +- [State Transitions](modeling-patterns/state-transitions.md) - State change modeling + +--- + +## Quick Reference + +### Components Cross-Reference + +| Concept | Documentation | Python Class | +|---------|---------------|--------------| +| **Flow rate bounds** | [Flow](elements/Flow.md) | [`Flow`][flixopt.elements.Flow] | +| **Bus balance** | [Bus](elements/Bus.md) | [`Bus`][flixopt.elements.Bus] | +| **Storage balance** | [Storage](elements/Storage.md) | [`Storage`][flixopt.components.Storage] | +| **Linear conversion** | [LinearConverter](elements/LinearConverter.md) | [`LinearConverter`][flixopt.components.LinearConverter] | + +### Features Cross-Reference + +| Concept | Documentation | Python Class | +|---------|---------------|--------------| +| **Binary investment** | [InvestParameters](features/InvestParameters.md) | [`InvestParameters`][flixopt.interface.InvestParameters] | +| **On/off operation** | [OnOffParameters](features/OnOffParameters.md) | [`OnOffParameters`][flixopt.interface.OnOffParameters] | +| **Piecewise segments** | [Piecewise](features/Piecewise.md) | [`Piecewise`][flixopt.interface.Piecewise] | + +### Modeling Patterns Cross-Reference + +| Pattern | Documentation | Implementation | +|---------|---------------|----------------| +| **Basic bounds** | [bounds-and-states](modeling-patterns/bounds-and-states.md#basic-bounds) | [`BoundingPatterns.basic_bounds()`][flixopt.modeling.BoundingPatterns.basic_bounds] | +| **Bounds with state** | [bounds-and-states](modeling-patterns/bounds-and-states.md#bounds-with-state) | [`BoundingPatterns.bounds_with_state()`][flixopt.modeling.BoundingPatterns.bounds_with_state] | +| **Scaled bounds** | [bounds-and-states](modeling-patterns/bounds-and-states.md#scaled-bounds) | [`BoundingPatterns.scaled_bounds()`][flixopt.modeling.BoundingPatterns.scaled_bounds] | +| **Duration tracking** | [duration-tracking](modeling-patterns/duration-tracking.md) | [`ModelingPrimitives.consecutive_duration_tracking()`][flixopt.modeling.ModelingPrimitives.consecutive_duration_tracking] | +| **State transitions** | [state-transitions](modeling-patterns/state-transitions.md) | [`BoundingPatterns.state_transition_bounds()`][flixopt.modeling.BoundingPatterns.state_transition_bounds] | + +### Python Class Lookup + +| Class | Documentation | API Reference | +|-------|---------------|---------------| +| `Flow` | [Flow](elements/Flow.md) | [`Flow`][flixopt.elements.Flow] | +| `Bus` | [Bus](elements/Bus.md) | [`Bus`][flixopt.elements.Bus] | +| `Storage` | [Storage](elements/Storage.md) | [`Storage`][flixopt.components.Storage] | +| `LinearConverter` | [LinearConverter](elements/LinearConverter.md) | [`LinearConverter`][flixopt.components.LinearConverter] | +| `InvestParameters` | [InvestParameters](features/InvestParameters.md) | [`InvestParameters`][flixopt.interface.InvestParameters] | +| `OnOffParameters` | [OnOffParameters](features/OnOffParameters.md) | [`OnOffParameters`][flixopt.interface.OnOffParameters] | +| `Piecewise` | [Piecewise](features/Piecewise.md) | [`Piecewise`][flixopt.interface.Piecewise] | diff --git a/docs/user-guide/mathematical-notation/modeling-patterns/bounds-and-states.md b/docs/user-guide/mathematical-notation/modeling-patterns/bounds-and-states.md new file mode 100644 index 000000000..d5821948f --- /dev/null +++ b/docs/user-guide/mathematical-notation/modeling-patterns/bounds-and-states.md @@ -0,0 +1,165 @@ +# Bounds and States + +This document describes the mathematical formulations for variable bounding patterns used throughout FlixOpt. These patterns define how optimization variables are constrained, both with and without state control. + +## Basic Bounds + +The simplest bounding pattern constrains a variable between lower and upper bounds. + +$$\label{eq:basic_bounds} +\text{lower} \leq v \leq \text{upper} +$$ + +With: +- $v$ being the optimization variable +- $\text{lower}$ being the lower bound (constant or time-dependent) +- $\text{upper}$ being the upper bound (constant or time-dependent) + +**Implementation:** [`BoundingPatterns.basic_bounds()`][flixopt.modeling.BoundingPatterns.basic_bounds] + +**Used in:** +- Storage charge state bounds (see [Storage](../elements/Storage.md)) +- Flow rate absolute bounds + +--- + +## Bounds with State + +When a variable should only be non-zero if a binary state variable is active (e.g., on/off operation, investment decisions), the bounds are controlled by the state: + +$$\label{eq:bounds_with_state} +s \cdot \max(\varepsilon, \text{lower}) \leq v \leq s \cdot \text{upper} +$$ + +With: +- $v$ being the optimization variable +- $s \in \{0, 1\}$ being the binary state variable +- $\text{lower}$ being the lower bound when active +- $\text{upper}$ being the upper bound when active +- $\varepsilon$ being a small positive number to ensure numerical stability + +**Behavior:** +- When $s = 0$: variable is forced to zero ($0 \leq v \leq 0$) +- When $s = 1$: variable can take values in $[\text{lower}, \text{upper}]$ + +**Implementation:** [`BoundingPatterns.bounds_with_state()`][flixopt.modeling.BoundingPatterns.bounds_with_state] + +**Used in:** +- Flow rates with on/off operation (see [OnOffParameters](../features/OnOffParameters.md)) +- Investment size decisions (see [InvestParameters](../features/InvestParameters.md)) + +--- + +## Scaled Bounds + +When a variable's bounds depend on another variable (e.g., flow rate scaled by component size), scaled bounds are used: + +$$\label{eq:scaled_bounds} +v_\text{scale} \cdot \text{rel}_\text{lower} \leq v \leq v_\text{scale} \cdot \text{rel}_\text{upper} +$$ + +With: +- $v$ being the optimization variable (e.g., flow rate) +- $v_\text{scale}$ being the scaling variable (e.g., component size) +- $\text{rel}_\text{lower}$ being the relative lower bound factor (typically 0) +- $\text{rel}_\text{upper}$ being the relative upper bound factor (typically 1) + +**Example:** Flow rate bounds +- If $v_\text{scale} = P$ (flow size) and $\text{rel}_\text{upper} = 1$ +- Then: $0 \leq p(t_i) \leq P$ (see [Flow](../elements/Flow.md)) + +**Implementation:** [`BoundingPatterns.scaled_bounds()`][flixopt.modeling.BoundingPatterns.scaled_bounds] + +**Used in:** +- Flow rate constraints (see [Flow](../elements/Flow.md) equation 1) +- Storage charge state constraints (see [Storage](../elements/Storage.md) equation 1) + +--- + +## Scaled Bounds with State + +Combining scaled bounds with binary state control requires a Big-M formulation to handle both the scaling and the on/off behavior: + +$$\label{eq:scaled_bounds_with_state_1} +(s - 1) \cdot M_\text{misc} + v_\text{scale} \cdot \text{rel}_\text{lower} \leq v \leq v_\text{scale} \cdot \text{rel}_\text{upper} +$$ + +$$\label{eq:scaled_bounds_with_state_2} +s \cdot M_\text{lower} \leq v \leq s \cdot M_\text{upper} +$$ + +With: +- $v$ being the optimization variable +- $v_\text{scale}$ being the scaling variable +- $s \in \{0, 1\}$ being the binary state variable +- $\text{rel}_\text{lower}$ being the relative lower bound factor +- $\text{rel}_\text{upper}$ being the relative upper bound factor +- $M_\text{misc} = v_\text{scale,max} \cdot \text{rel}_\text{lower}$ +- $M_\text{upper} = v_\text{scale,max} \cdot \text{rel}_\text{upper}$ +- $M_\text{lower} = \max(\varepsilon, v_\text{scale,min} \cdot \text{rel}_\text{lower})$ + +Where $v_\text{scale,max}$ and $v_\text{scale,min}$ are the maximum and minimum possible values of the scaling variable. + +**Behavior:** +- When $s = 0$: variable is forced to zero +- When $s = 1$: variable follows scaled bounds $v_\text{scale} \cdot \text{rel}_\text{lower} \leq v \leq v_\text{scale} \cdot \text{rel}_\text{upper}$ + +**Implementation:** [`BoundingPatterns.scaled_bounds_with_state()`][flixopt.modeling.BoundingPatterns.scaled_bounds_with_state] + +**Used in:** +- Flow rates with on/off operation and investment sizing +- Components combining [OnOffParameters](../features/OnOffParameters.md) and [InvestParameters](../features/InvestParameters.md) + +--- + +## Expression Tracking + +Sometimes it's necessary to create an auxiliary variable that equals an expression: + +$$\label{eq:expression_tracking} +v_\text{tracker} = \text{expression} +$$ + +With optional bounds: + +$$\label{eq:expression_tracking_bounds} +\text{lower} \leq v_\text{tracker} \leq \text{upper} +$$ + +With: +- $v_\text{tracker}$ being the auxiliary tracking variable +- $\text{expression}$ being a linear expression of other variables +- $\text{lower}, \text{upper}$ being optional bounds on the tracker + +**Use cases:** +- Creating named variables for complex expressions +- Bounding intermediate results +- Simplifying constraint formulations + +**Implementation:** [`ModelingPrimitives.expression_tracking_variable()`][flixopt.modeling.ModelingPrimitives.expression_tracking_variable] + +--- + +## Mutual Exclusivity + +When multiple binary variables should not be active simultaneously (at most one can be 1): + +$$\label{eq:mutual_exclusivity} +\sum_{i} s_i(t) \leq \text{tolerance} \quad \forall t +$$ + +With: +- $s_i(t) \in \{0, 1\}$ being binary state variables +- $\text{tolerance}$ being the maximum number of simultaneously active states (typically 1) +- $t$ being the time index + +**Use cases:** +- Ensuring only one operating mode is active +- Mutual exclusion of operation and maintenance states +- Enforcing single-choice decisions + +**Implementation:** [`ModelingPrimitives.mutual_exclusivity_constraint()`][flixopt.modeling.ModelingPrimitives.mutual_exclusivity_constraint] + +**Used in:** +- Operating mode selection +- Piecewise linear function segments (see [Piecewise](../features/Piecewise.md)) diff --git a/docs/user-guide/mathematical-notation/modeling-patterns/duration-tracking.md b/docs/user-guide/mathematical-notation/modeling-patterns/duration-tracking.md new file mode 100644 index 000000000..5d430d28c --- /dev/null +++ b/docs/user-guide/mathematical-notation/modeling-patterns/duration-tracking.md @@ -0,0 +1,159 @@ +# Duration Tracking + +Duration tracking allows monitoring how long a binary state has been consecutively active. This is essential for modeling minimum run times, ramp-up periods, and similar time-dependent constraints. + +## Consecutive Duration Tracking + +For a binary state variable $s(t) \in \{0, 1\}$, the consecutive duration $d(t)$ tracks how long the state has been continuously active. + +### Duration Upper Bound + +The duration cannot exceed zero when the state is inactive: + +$$\label{eq:duration_upper} +d(t) \leq s(t) \cdot M \quad \forall t +$$ + +With: +- $d(t)$ being the duration variable (continuous, non-negative) +- $s(t) \in \{0, 1\}$ being the binary state variable +- $M$ being a sufficiently large constant (big-M) + +**Behavior:** +- When $s(t) = 0$: forces $d(t) \leq 0$, thus $d(t) = 0$ +- When $s(t) = 1$: allows $d(t)$ to be positive + +--- + +### Duration Accumulation + +While the state is active, the duration increases by the time step size: + +$$\label{eq:duration_accumulation_upper} +d(t+1) \leq d(t) + \Delta d(t) \quad \forall t +$$ + +$$\label{eq:duration_accumulation_lower} +d(t+1) \geq d(t) + \Delta d(t) + (s(t+1) - 1) \cdot M \quad \forall t +$$ + +With: +- $\Delta d(t)$ being the duration increment for time step $t$ (typically $\Delta t_i$ from the time series) +- $M$ being a sufficiently large constant + +**Behavior:** +- When $s(t+1) = 1$: both inequalities enforce $d(t+1) = d(t) + \Delta d(t)$ +- When $s(t+1) = 0$: only the upper bound applies, and $d(t+1) = 0$ (from equation $\eqref{eq:duration_upper}$) + +--- + +### Initial Duration + +The duration at the first time step depends on both the state and any previous duration: + +$$\label{eq:duration_initial} +d(0) = (\Delta d(0) + d_\text{prev}) \cdot s(0) +$$ + +With: +- $d_\text{prev}$ being the duration from before the optimization period +- $\Delta d(0)$ being the duration increment for the first time step + +**Behavior:** +- When $s(0) = 1$: duration continues from previous period +- When $s(0) = 0$: duration resets to zero + +--- + +### Complete Formulation + +Combining all constraints: + +$$ +\begin{align} +d(t) &\leq s(t) \cdot M && \forall t \label{eq:duration_complete_1} \\ +d(t+1) &\leq d(t) + \Delta d(t) && \forall t \label{eq:duration_complete_2} \\ +d(t+1) &\geq d(t) + \Delta d(t) + (s(t+1) - 1) \cdot M && \forall t \label{eq:duration_complete_3} \\ +d(0) &= (\Delta d(0) + d_\text{prev}) \cdot s(0) && \label{eq:duration_complete_4} +\end{align} +$$ + +--- + +## Minimum Duration Constraints + +To enforce a minimum consecutive duration (e.g., minimum run time), an additional constraint links the duration to state changes: + +$$\label{eq:minimum_duration} +d(t) \geq (s(t-1) - s(t)) \cdot d_\text{min}(t-1) \quad \forall t > 0 +$$ + +With: +- $d_\text{min}(t)$ being the required minimum duration at time $t$ + +**Behavior:** +- When shutting down ($s(t-1) = 1, s(t) = 0$): enforces $d(t-1) \geq d_\text{min}(t-1)$ +- This ensures the state was active for at least $d_\text{min}$ before turning off +- When state is constant or turning on: constraint is non-binding + +--- + +## Implementation + +**Function:** [`ModelingPrimitives.consecutive_duration_tracking()`][flixopt.modeling.ModelingPrimitives.consecutive_duration_tracking] + +See the API documentation for complete parameter list and usage details. + +--- + +## Use Cases + +### Minimum Run Time + +Ensuring equipment runs for a minimum duration once started: + +```python +# State: 1 when running, 0 when off +# Require at least 2 hours of operation +duration = modeling.consecutive_duration_tracking( + state_variable=on_state, + duration_per_step=time_step_hours, + minimum_duration=2.0 +) +``` + +### Ramp-Up Tracking + +Tracking time since startup for gradual ramp-up constraints: + +```python +# Track startup duration +startup_duration = modeling.consecutive_duration_tracking( + state_variable=on_state, + duration_per_step=time_step_hours +) +# Constrain output based on startup duration +# (additional constraints would link output to startup_duration) +``` + +### Cooldown Requirements + +Tracking time in a state before allowing transitions: + +```python +# Track maintenance duration +maintenance_duration = modeling.consecutive_duration_tracking( + state_variable=maintenance_state, + duration_per_step=time_step_hours, + minimum_duration=scheduled_maintenance_hours +) +``` + +--- + +## Used In + +This pattern is used in: +- [`OnOffParameters`](../features/OnOffParameters.md) - Minimum on/off times +- Operating mode constraints with minimum durations +- Startup/shutdown sequence modeling diff --git a/docs/user-guide/mathematical-notation/modeling-patterns/index.md b/docs/user-guide/mathematical-notation/modeling-patterns/index.md new file mode 100644 index 000000000..15ff8dbd2 --- /dev/null +++ b/docs/user-guide/mathematical-notation/modeling-patterns/index.md @@ -0,0 +1,54 @@ +# Modeling Patterns + +This section documents the fundamental mathematical patterns used throughout FlixOpt for constructing optimization models. These patterns are implemented in `flixopt.modeling` and provide reusable building blocks for creating constraints. + +## Overview + +The modeling patterns are organized into three categories: + +1. **[Bounds and States](bounds-and-states.md)** - Variable bounding with optional state control +2. **[Duration Tracking](duration-tracking.md)** - Tracking consecutive durations of states +3. **[State Transitions](state-transitions.md)** - Modeling state changes and transitions + +## Pattern Categories + +### Bounding Patterns + +These patterns define how optimization variables are constrained within bounds: + +- **Basic Bounds** - Simple upper and lower bounds on variables +- **Bounds with State** - Binary-controlled bounds (on/off states) +- **Scaled Bounds** - Bounds dependent on another variable (e.g., size) +- **Scaled Bounds with State** - Combination of scaling and binary control + +### Tracking Patterns + +These patterns track properties over time: + +- **Expression Tracking** - Creating auxiliary variables that track expressions +- **Consecutive Duration Tracking** - Tracking how long a state has been active +- **Mutual Exclusivity** - Ensuring only one of multiple options is active + +### Transition Patterns + +These patterns model changes between states: + +- **State Transitions** - Tracking switches between binary states (on→off, off→on) +- **Continuous Transitions** - Linking continuous variable changes to switches +- **Level Changes with Binaries** - Controlled increases/decreases in levels + +## Usage in Components + +These patterns are used throughout FlixOpt components: + +- [`Flow`][flixopt.elements.Flow] uses **scaled bounds with state** for flow rate constraints +- [`Storage`][flixopt.components.Storage] uses **basic bounds** for charge state +- [`OnOffParameters`](../features/OnOffParameters.md) uses **state transitions** for startup/shutdown +- [`InvestParameters`](../features/InvestParameters.md) uses **bounds with state** for investment decisions + +## Implementation + +All patterns are implemented in [`flixopt.modeling`][flixopt.modeling] module: + +- [`ModelingPrimitives`][flixopt.modeling.ModelingPrimitives] - Core constraint patterns +- [`BoundingPatterns`][flixopt.modeling.BoundingPatterns] - Specialized bounding patterns diff --git a/docs/user-guide/mathematical-notation/modeling-patterns/state-transitions.md b/docs/user-guide/mathematical-notation/modeling-patterns/state-transitions.md new file mode 100644 index 000000000..dc75a8008 --- /dev/null +++ b/docs/user-guide/mathematical-notation/modeling-patterns/state-transitions.md @@ -0,0 +1,227 @@ +# State Transitions + +State transition patterns model changes between discrete states and link them to continuous variables. These patterns are essential for modeling startup/shutdown events, switching behavior, and controlled changes in system operation. + +## Binary State Transitions + +For a binary state variable $s(t) \in \{0, 1\}$, state transitions track when the state switches on or off. + +### Switch Variables + +Two binary variables track the transitions: +- $s^\text{on}(t) \in \{0, 1\}$: equals 1 when switching from off to on +- $s^\text{off}(t) \in \{0, 1\}$: equals 1 when switching from on to off + +### Transition Tracking + +The state change equals the difference between switch-on and switch-off: + +$$\label{eq:state_transition} +s^\text{on}(t) - s^\text{off}(t) = s(t) - s(t-1) \quad \forall t > 0 +$$ + +$$\label{eq:state_transition_initial} +s^\text{on}(0) - s^\text{off}(0) = s(0) - s_\text{prev} +$$ + +With: +- $s(t)$ being the binary state variable +- $s_\text{prev}$ being the state before the optimization period +- $s^\text{on}(t), s^\text{off}(t)$ being the switch variables + +**Behavior:** +- Off → On ($s(t-1)=0, s(t)=1$): $s^\text{on}(t)=1, s^\text{off}(t)=0$ +- On → Off ($s(t-1)=1, s(t)=0$): $s^\text{on}(t)=0, s^\text{off}(t)=1$ +- No change: $s^\text{on}(t)=0, s^\text{off}(t)=0$ + +--- + +### Mutual Exclusivity of Switches + +A state cannot switch on and off simultaneously: + +$$\label{eq:switch_exclusivity} +s^\text{on}(t) + s^\text{off}(t) \leq 1 \quad \forall t +$$ + +This ensures: +- At most one switch event per time step +- No simultaneous on/off switching + +--- + +### Complete State Transition Formulation + +$$ +\begin{align} +s^\text{on}(t) - s^\text{off}(t) &= s(t) - s(t-1) && \forall t > 0 \label{eq:transition_complete_1} \\ +s^\text{on}(0) - s^\text{off}(0) &= s(0) - s_\text{prev} && \label{eq:transition_complete_2} \\ +s^\text{on}(t) + s^\text{off}(t) &\leq 1 && \forall t \label{eq:transition_complete_3} \\ +s^\text{on}(t), s^\text{off}(t) &\in \{0, 1\} && \forall t \label{eq:transition_complete_4} +\end{align} +$$ + +**Implementation:** [`BoundingPatterns.state_transition_bounds()`][flixopt.modeling.BoundingPatterns.state_transition_bounds] + +--- + +## Continuous Transitions + +When a continuous variable should only change when certain switch events occur, continuous transition bounds link the variable changes to binary switches. + +### Change Bounds with Switches + +$$\label{eq:continuous_transition} +-\Delta v^\text{max} \cdot (s^\text{on}(t) + s^\text{off}(t)) \leq v(t) - v(t-1) \leq \Delta v^\text{max} \cdot (s^\text{on}(t) + s^\text{off}(t)) \quad \forall t > 0 +$$ + +$$\label{eq:continuous_transition_initial} +-\Delta v^\text{max} \cdot (s^\text{on}(0) + s^\text{off}(0)) \leq v(0) - v_\text{prev} \leq \Delta v^\text{max} \cdot (s^\text{on}(0) + s^\text{off}(0)) +$$ + +With: +- $v(t)$ being the continuous variable +- $v_\text{prev}$ being the value before the optimization period +- $\Delta v^\text{max}$ being the maximum allowed change +- $s^\text{on}(t), s^\text{off}(t) \in \{0, 1\}$ being switch binary variables + +**Behavior:** +- When $s^\text{on}(t) = 0$ and $s^\text{off}(t) = 0$: forces $v(t) = v(t-1)$ (no change) +- When $s^\text{on}(t) = 1$ or $s^\text{off}(t) = 1$: allows change up to $\pm \Delta v^\text{max}$ + +**Implementation:** [`BoundingPatterns.continuous_transition_bounds()`][flixopt.modeling.BoundingPatterns.continuous_transition_bounds] + +--- + +## Level Changes with Binaries + +This pattern models a level variable that can increase or decrease, with changes controlled by binary variables. This is useful for inventory management, capacity adjustments, or gradual state changes. + +### Level Evolution + +The level evolves based on increases and decreases: + +$$\label{eq:level_initial} +\ell(0) = \ell_\text{init} + \ell^\text{inc}(0) - \ell^\text{dec}(0) +$$ + +$$\label{eq:level_evolution} +\ell(t) = \ell(t-1) + \ell^\text{inc}(t) - \ell^\text{dec}(t) \quad \forall t > 0 +$$ + +With: +- $\ell(t)$ being the level variable +- $\ell_\text{init}$ being the initial level +- $\ell^\text{inc}(t)$ being the increase in level at time $t$ (non-negative) +- $\ell^\text{dec}(t)$ being the decrease in level at time $t$ (non-negative) + +--- + +### Change Bounds with Binary Control + +Changes are bounded and controlled by binary variables: + +$$\label{eq:increase_bound} +\ell^\text{inc}(t) \leq \Delta \ell^\text{max} \cdot b^\text{inc}(t) \quad \forall t +$$ + +$$\label{eq:decrease_bound} +\ell^\text{dec}(t) \leq \Delta \ell^\text{max} \cdot b^\text{dec}(t) \quad \forall t +$$ + +With: +- $\Delta \ell^\text{max}$ being the maximum change per time step +- $b^\text{inc}(t), b^\text{dec}(t) \in \{0, 1\}$ being binary control variables + +--- + +### Mutual Exclusivity of Changes + +Simultaneous increase and decrease are prevented: + +$$\label{eq:change_exclusivity} +b^\text{inc}(t) + b^\text{dec}(t) \leq 1 \quad \forall t +$$ + +This ensures: +- Level can only increase OR decrease (or stay constant) in each time step +- No simultaneous contradictory changes + +--- + +### Complete Level Change Formulation + +$$ +\begin{align} +\ell(0) &= \ell_\text{init} + \ell^\text{inc}(0) - \ell^\text{dec}(0) && \label{eq:level_complete_1} \\ +\ell(t) &= \ell(t-1) + \ell^\text{inc}(t) - \ell^\text{dec}(t) && \forall t > 0 \label{eq:level_complete_2} \\ +\ell^\text{inc}(t) &\leq \Delta \ell^\text{max} \cdot b^\text{inc}(t) && \forall t \label{eq:level_complete_3} \\ +\ell^\text{dec}(t) &\leq \Delta \ell^\text{max} \cdot b^\text{dec}(t) && \forall t \label{eq:level_complete_4} \\ +b^\text{inc}(t) + b^\text{dec}(t) &\leq 1 && \forall t \label{eq:level_complete_5} \\ +b^\text{inc}(t), b^\text{dec}(t) &\in \{0, 1\} && \forall t \label{eq:level_complete_6} +\end{align} +$$ + +**Implementation:** [`BoundingPatterns.link_changes_to_level_with_binaries()`][flixopt.modeling.BoundingPatterns.link_changes_to_level_with_binaries] + +--- + +## Use Cases + +### Startup/Shutdown Costs + +Track startup and shutdown events to apply costs: + +```python +# Create switch variables +switch_on, switch_off = modeling.state_transition_bounds( + state_variable=on_state, + previous_state=previous_on_state +) + +# Apply costs to switches +startup_cost = switch_on * startup_cost_per_event +shutdown_cost = switch_off * shutdown_cost_per_event +``` + +### Limited Switching + +Restrict the number of state changes: + +```python +# Track all switches +switch_on, switch_off = modeling.state_transition_bounds( + state_variable=on_state +) + +# Limit total switches +model.add_constraint( + (switch_on + switch_off).sum() <= max_switches +) +``` + +### Gradual Capacity Changes + +Model systems where capacity can be incrementally adjusted: + +```python +# Level represents installed capacity +level_var, increase, decrease, inc_binary, dec_binary = \ + modeling.link_changes_to_level_with_binaries( + initial_level=current_capacity, + max_change=max_capacity_change_per_period + ) + +# Constrain total increases +model.add_constraint(increase.sum() <= max_total_expansion) +``` + +--- + +## Used In + +These patterns are used in: +- [`OnOffParameters`](../features/OnOffParameters.md) - Startup/shutdown tracking and costs +- Operating mode switching with transition costs +- Investment planning with staged capacity additions +- Inventory management with controlled stock changes diff --git a/docs/user-guide/Mathematical Notation/others.md b/docs/user-guide/mathematical-notation/others.md similarity index 100% rename from docs/user-guide/Mathematical Notation/others.md rename to docs/user-guide/mathematical-notation/others.md diff --git a/docs/user-guide/migration-guide-v3.md b/docs/user-guide/migration-guide-v3.md new file mode 100644 index 000000000..2a9cab97a --- /dev/null +++ b/docs/user-guide/migration-guide-v3.md @@ -0,0 +1,232 @@ +# Migration Guide: v2.x → v3.0.0 + +!!! tip "Quick Start" + ```bash + pip install --upgrade flixopt + ``` + Review [breaking changes](#breaking-changes), update [deprecated parameters](#deprecated-parameters), test thoroughly. + +--- + +## 💥 Breaking Changes + +### Effect System Redesign + +Terminology changed and sharing system inverted: effects now "pull" shares. + +| Concept | Old (v2.x) | New (v3.0.0) | +|---------|------------|--------------| +| Time-varying effects | `operation` | `temporal` | +| Investment effects | `invest` / `investment` | `periodic` | +| Share to other effects (operation) | `specific_share_to_other_effects_operation` | `share_from_temporal` | +| Share to other effects (invest) | `specific_share_to_other_effects_invest` | `share_from_periodic` | + +=== "v2.x" + ```python + CO2 = fx.Effect('CO2', 'kg', 'CO2', + specific_share_to_other_effects_operation={'costs': 0.2}) + costs = fx.Effect('costs', '€', 'Total') + ``` + +=== "v3.0.0" + ```python + CO2 = fx.Effect('CO2', 'kg', 'CO2') + costs = fx.Effect('costs', '€', 'Total', + share_from_temporal={'CO2': 0.2}) # Pull from CO2 + ``` + +!!! warning "No deprecation warning" + Move shares to receiving effect and update parameter names throughout your code. + +--- + +### Variable Names + +| Category | Old (v2.x) | New (v3.0.0) | +|---------------------------------|------------|--------------| +| Investment | `is_invested` | `invested` | +| Switching | `switch_on` | `switch|on` | +| Switching | `switch_off` | `switch|off` | +| Switching | `switch_on_nr` | `switch|count` | +| Effects | `Effect(invest)|total` | `Effect(periodic)` | +| Effects | `Effect(operation)|total` | `Effect(temporal)` | +| Effects | `Effect(operation)|total_per_timestep` | `Effect(temporal)|per_timestep` | +| Effects | `Effect|total` | `Effect` | + +--- + +### String Labels + +| What | Old (v2.x) | New (v3.0.0) | +|------|------------|--------------| +| Bus assignment | `bus=my_bus` (object) | `bus='electricity'` (string) | +| Effect shares | `{CO2: 0.2}` (object key) | `{'CO2': 0.2}` (string key) | + +=== "v2.x" + ```python + flow = fx.Flow('P_el', bus=my_bus) # ❌ Object + costs = fx.Effect('costs', '€', share_from_temporal={CO2: 0.2}) # ❌ + ``` + +=== "v3.0.0" + ```python + flow = fx.Flow('P_el', bus='electricity') # ✅ String + costs = fx.Effect('costs', '€', share_from_temporal={'CO2': 0.2}) # ✅ + ``` + +--- + +### FlowSystem & Calculation + +| Change | Description | +|--------|-------------| +| **FlowSystem copying** | Each `Calculation` gets its own copy (independent) | +| **do_modeling() return** | Returns `Calculation` object (access model via `.model` property) | +| **Storage arrays** | Arrays match timestep count (no extra element) | +| **Final charge state** | Use `relative_minimum_final_charge_state` / `relative_maximum_final_charge_state` | + +--- + +### Other Changes + +| Category | Old (v2.x) | New (v3.0.0) | +|----------|------------|--------------| +| System model class | `SystemModel` | `FlowSystemModel` | +| Element submodel | `Model` | `Submodel` | +| Logging default | Enabled | Disabled | +| Enable logging | (default) | `fx.CONFIG.Logging.console = True; fx.CONFIG.apply()` | + +--- + +## 🗑️ Deprecated Parameters + +??? abstract "InvestParameters" + + | Old (v2.x) | New (v3.0.0) | + |------------|--------------| + | `fix_effects` | `effects_of_investment` | + | `specific_effects` | `effects_of_investment_per_size` | + | `divest_effects` | `effects_of_retirement` | + | `piecewise_effects` | `piecewise_effects_of_investment` | + +??? abstract "Effect" + + | Old (v2.x) | New (v3.0.0) | + |------------|--------------| + | `minimum_investment` | `minimum_periodic` | + | `maximum_investment` | `maximum_periodic` | + | `minimum_operation` | `minimum_temporal` | + | `maximum_operation` | `maximum_temporal` | + | `minimum_operation_per_hour` | `minimum_per_hour` | + | `maximum_operation_per_hour` | `maximum_per_hour` | + +??? abstract "Components" + + | Old (v2.x) | New (v3.0.0) | + |------------|--------------| + | `source` (parameter) | `outputs` | + | `sink` (parameter) | `inputs` | + | `prevent_simultaneous_sink_and_source` | `prevent_simultaneous_flow_rates` | + +??? abstract "TimeSeriesData" + + | Old (v2.x) | New (v3.0.0) | + |------------|--------------| + | `agg_group` | `aggregation_group` | + | `agg_weight` | `aggregation_weight` | + +??? abstract "Calculation" + + | Old (v2.x) | New (v3.0.0) | + |------------|--------------| + | `active_timesteps=[0, 1, 2]` | Use `flow_system.sel()` or `flow_system.isel()` | + +--- + +## ✨ New Features + +??? success "Multi-Period Investments" + + ```python + periods = pd.Index(['2020', '2030']) + flow_system = fx.FlowSystem(time=timesteps, periods=periods) + ``` + +??? success "Scenario-Based Optimization" + + | Parameter | Description | Example | + |-----------|-------------|---------| + | `scenarios` | Scenario index | `pd.Index(['low', 'base', 'high'], name='scenario')` | + | `scenario_weights` | Probabilities | `[0.2, 0.6, 0.2]` | + | `scenario_independent_sizes` | Separate capacities per scenario | `True` / `False` (default) | + + ```python + flow_system = fx.FlowSystem( + time=timesteps, + scenarios=scenarios, + scenario_weights=[0.2, 0.6, 0.2], + scenario_independent_sizes=True + ) + ``` + +??? success "Enhanced I/O" + + | Method | Description | + |--------|-------------| + | `flow_system.to_netcdf('file.nc')` | Save FlowSystem | + | `fx.FlowSystem.from_netcdf('file.nc')` | Load FlowSystem | + | `flow_system.sel(time=slice(...))` | Select by label | + | `flow_system.isel(time=slice(...))` | Select by index | + | `flow_system.resample(time='D')` | Resample timeseries | + | `flow_system.copy()` | Deep copy | + | `results.flow_system` | Access from results | + +??? success "Effects Per Component" + + ```python + effects_ds = results.effects_per_component + + # Access effect contributions by component + print(effects_ds['total'].sel(effect='costs')) # Total effects + print(effects_ds['temporal'].sel(effect='CO2')) # Temporal effects + print(effects_ds['periodic'].sel(effect='costs')) # Periodic effects + ``` + +??? success "Storage Features" + + | Feature | Parameter | Description | + |---------|-----------|-------------| + | **Balanced storage** | `balanced=True` | Ensures charge_size == discharge_size | + | **Final state min** | `relative_minimum_final_charge_state=0.5` | End at least 50% charged | + | **Final state max** | `relative_maximum_final_charge_state=0.8` | End at most 80% charged | + +--- + +## 🔧 Common Issues + +| Issue | Solution | +|-------|----------| +| Effect shares not working | See [Effect System Redesign](#effect-system-redesign) | +| Storage dimensions wrong | See [FlowSystem & Calculation](#flowsystem-calculation) | +| Bus assignment error | See [String Labels](#string-labels) | +| KeyError in results | See [Variable Names](#variable-names) | +| `AttributeError: model` | Rename `.model` → `.submodel` | +| No logging | See [Other Changes](#other-changes) | + +--- + +## ✅ Checklist + +| Category | Tasks | +|----------|-------| +| **Install** | • `pip install --upgrade flixopt` | +| **Breaking changes** | • Update [effect sharing](#effect-system-redesign)
• Update [variable names](#variable-names)
• Update [string labels](#string-labels)
• Fix [storage arrays](#flowsystem-calculation)
• Update [Calculation API](#flowsystem-calculation)
• Update [class names](#other-changes) | +| **Configuration** | • Enable [logging](#other-changes) if needed | +| **Deprecated** | • Update [deprecated parameters](#deprecated-parameters) (recommended) | +| **Testing** | • Test thoroughly
• Validate results match v2.x | + +--- + +:material-book: [Docs](https://flixopt.github.io/flixopt/) • :material-github: [Issues](https://github.com/flixOpt/flixopt/issues) • :material-text-box: [Changelog](https://flixopt.github.io/flixopt/latest/changelog/99984-v3.0.0/) + +!!! success "Welcome to flixopt v3.0.0! 🎉" diff --git a/docs/user-guide/recipes/index.md b/docs/user-guide/recipes/index.md new file mode 100644 index 000000000..8ac7d1812 --- /dev/null +++ b/docs/user-guide/recipes/index.md @@ -0,0 +1,47 @@ +# Recipes + +**Coming Soon!** 🚧 + +This section will contain quick, copy-paste ready code snippets for common FlixOpt patterns. + +--- + +## What Will Be Here? + +Short, focused code snippets showing **how to do specific things** in FlixOpt: + +- Common modeling patterns +- Integration with other tools +- Performance optimizations +- Domain-specific solutions +- Data analysis shortcuts + +Unlike full examples, recipes will be focused snippets showing a single concept. + +--- + +## Planned Topics + +- **Storage Patterns** - Batteries, thermal storage, seasonal storage +- **Multi-Criteria Optimization** - Balance multiple objectives +- **Data I/O** - Loading time series from CSV, databases, APIs +- **Data Manipulation** - Common xarray operations for parameterization and analysis +- **Investment Optimization** - Size optimization strategies +- **Renewable Integration** - Solar, wind capacity optimization +- **On/Off Constraints** - Minimum runtime, startup costs +- **Large-Scale Problems** - Segmented and aggregated calculations +- **Custom Constraints** - Extend models with linopy +- **Domain-Specific Patterns** - District heating, microgrids, industrial processes + +--- + +## Want to Contribute? + +**We need your help!** If you have recurring modeling patterns or clever solutions to share, please contribute via [GitHub issues](https://github.com/flixopt/flixopt/issues) or pull requests. + +Guidelines: +1. Keep it short (< 100 lines of code) +2. Focus on one specific technique +3. Add brief explanation and when to use it + +Check the [contribution guide](../../contribute.md) for details. diff --git a/examples/00_Minmal/minimal_example.py b/examples/00_Minmal/minimal_example.py index e9ef241ff..6a0ed3831 100644 --- a/examples/00_Minmal/minimal_example.py +++ b/examples/00_Minmal/minimal_example.py @@ -1,71 +1,37 @@ """ -This script shows how to use the flixopt framework to model a super minimalistic energy system. +This script shows how to use the flixopt framework to model a super minimalistic energy system in the most concise way possible. +THis can also be used to create proposals for new features, bug reports etc """ import numpy as np import pandas as pd -from rich.pretty import pprint import flixopt as fx if __name__ == '__main__': - # --- Define the Flow System, that will hold all elements, and the time steps you want to model --- - timesteps = pd.date_range('2020-01-01', periods=3, freq='h') - flow_system = fx.FlowSystem(timesteps) - - # --- Define Thermal Load Profile --- - # Load profile (e.g., kW) for heating demand over time - thermal_load_profile = np.array([30, 0, 20]) - - # --- Define Energy Buses --- - # These are balancing nodes (inputs=outputs) and balance the different energy carriers your system - flow_system.add_elements(fx.Bus('District Heating'), fx.Bus('Natural Gas')) - - # --- Define Objective Effect (Cost) --- - # Cost effect representing the optimization objective (minimizing costs) - cost_effect = fx.Effect('costs', '€', 'Cost', is_standard=True, is_objective=True) - - # --- Define Flow System Components --- - # Boiler component with thermal output (heat) and fuel input (gas) - boiler = fx.linear_converters.Boiler( - 'Boiler', - eta=0.5, - Q_th=fx.Flow(label='Thermal Output', bus='District Heating', size=50), - Q_fu=fx.Flow(label='Fuel Input', bus='Natural Gas'), + fx.CONFIG.Logging.console = True + fx.CONFIG.apply() + flow_system = fx.FlowSystem(pd.date_range('2020-01-01', periods=3, freq='h')) + + flow_system.add_elements( + fx.Bus('Heat'), + fx.Bus('Gas'), + fx.Effect('Costs', '€', 'Cost', is_standard=True, is_objective=True), + fx.linear_converters.Boiler( + 'Boiler', + eta=0.5, + Q_th=fx.Flow(label='Heat', bus='Heat', size=50), + Q_fu=fx.Flow(label='Gas', bus='Gas'), + ), + fx.Sink( + 'Sink', + inputs=[fx.Flow(label='Demand', bus='Heat', size=1, fixed_relative_profile=np.array([30, 0, 20]))], + ), + fx.Source( + 'Source', + outputs=[fx.Flow(label='Gas', bus='Gas', size=1000, effects_per_flow_hour=0.04)], + ), ) - # Heat load component with a fixed thermal demand profile - heat_load = fx.Sink( - 'Heat Demand', - sink=fx.Flow(label='Thermal Load', bus='District Heating', size=1, fixed_relative_profile=thermal_load_profile), - ) - - # Gas source component with cost-effect per flow hour - gas_source = fx.Source( - 'Natural Gas Tariff', - source=fx.Flow(label='Gas Flow', bus='Natural Gas', size=1000, effects_per_flow_hour=0.04), # 0.04 €/kWh - ) - - # --- Build the Flow System --- - # Add all components and effects to the system - flow_system.add_elements(cost_effect, boiler, heat_load, gas_source) - - # --- Define, model and solve a Calculation --- - calculation = fx.FullCalculation('Simulation1', flow_system) - calculation.do_modeling() - calculation.solve(fx.solvers.HighsSolver(0.01, 60)) - - # --- Analyze Results --- - # Access the results of an element - df1 = calculation.results['costs'].filter_solution('time').to_dataframe() - - # Plot the results of a specific element - calculation.results['District Heating'].plot_node_balance_pie() - calculation.results['District Heating'].plot_node_balance() - - # Save results to a file - df2 = calculation.results['District Heating'].node_balance().to_dataframe() - # df2.to_csv('results/District Heating.csv') # Save results to csv - - # Print infos to the console. - pprint(calculation.summary) + calculation = fx.FullCalculation('Simulation1', flow_system).do_modeling().solve(fx.solvers.HighsSolver(0.01, 60)) + calculation.results['Heat'].plot_node_balance() diff --git a/examples/01_Simple/simple_example.py b/examples/01_Simple/simple_example.py index a201ee8c4..6b62d6712 100644 --- a/examples/01_Simple/simple_example.py +++ b/examples/01_Simple/simple_example.py @@ -8,6 +8,9 @@ import flixopt as fx if __name__ == '__main__': + # Enable console logging + fx.CONFIG.Logging.console = True + fx.CONFIG.apply() # --- Create Time Series Data --- # Heat demand profile (e.g., kW) over time and corresponding power prices heat_demand_per_h = np.array([30, 0, 90, 110, 110, 20, 20, 20, 20]) @@ -29,6 +32,7 @@ description='Kosten', is_standard=True, # standard effect: no explicit value needed for costs is_objective=True, # Minimizing costs as the optimization objective + share_from_temporal={'CO2': 0.2}, ) # CO2 emissions effect with an associated cost impact @@ -36,8 +40,7 @@ label='CO2', unit='kg', description='CO2_e-Emissionen', - specific_share_to_other_effects_operation={costs.label: 0.2}, - maximum_operation_per_hour=1000, # Max CO2 emissions per hour + maximum_per_hour=1000, # Max CO2 emissions per hour ) # --- Define Flow System Components --- @@ -64,7 +67,7 @@ label='Storage', charging=fx.Flow('Q_th_load', bus='Fernwärme', size=1000), discharging=fx.Flow('Q_th_unload', bus='Fernwärme', size=1000), - capacity_in_flow_hours=fx.InvestParameters(fix_effects=20, fixed_size=30, optional=False), + capacity_in_flow_hours=fx.InvestParameters(effects_of_investment=20, fixed_size=30, mandatory=True), initial_charge_state=0, # Initial storage state: empty relative_maximum_charge_state=1 / 100 * np.array([80, 70, 80, 80, 80, 80, 80, 80, 80]), relative_maximum_final_charge_state=0.8, @@ -77,18 +80,20 @@ # Heat Demand Sink: Represents a fixed heat demand profile heat_sink = fx.Sink( label='Heat Demand', - sink=fx.Flow(label='Q_th_Last', bus='Fernwärme', size=1, fixed_relative_profile=heat_demand_per_h), + inputs=[fx.Flow(label='Q_th_Last', bus='Fernwärme', size=1, fixed_relative_profile=heat_demand_per_h)], ) # Gas Source: Gas tariff source with associated costs and CO2 emissions gas_source = fx.Source( label='Gastarif', - source=fx.Flow(label='Q_Gas', bus='Gas', size=1000, effects_per_flow_hour={costs.label: 0.04, CO2.label: 0.3}), + outputs=[ + fx.Flow(label='Q_Gas', bus='Gas', size=1000, effects_per_flow_hour={costs.label: 0.04, CO2.label: 0.3}) + ], ) # Power Sink: Represents the export of electricity to the grid power_sink = fx.Sink( - label='Einspeisung', sink=fx.Flow(label='P_el', bus='Strom', effects_per_flow_hour=-1 * power_prices) + label='Einspeisung', inputs=[fx.Flow(label='P_el', bus='Strom', effects_per_flow_hour=-1 * power_prices)] ) # --- Build the Flow System --- @@ -107,9 +112,12 @@ calculation.solve(fx.solvers.HighsSolver(mip_gap=0, time_limit_seconds=30)) # --- Analyze Results --- + # Colors are automatically assigned using default colormap + # Optional: Configure custom colors with + calculation.results.setup_colors() calculation.results['Fernwärme'].plot_node_balance_pie() calculation.results['Fernwärme'].plot_node_balance() - calculation.results['Storage'].plot_node_balance() + calculation.results['Storage'].plot_charge_state() calculation.results.plot_heatmap('CHP(Q_th)|flow_rate') # Convert the results for the storage component to a dataframe and display diff --git a/examples/02_Complex/complex_example.py b/examples/02_Complex/complex_example.py index 175211c26..805cb08f6 100644 --- a/examples/02_Complex/complex_example.py +++ b/examples/02_Complex/complex_example.py @@ -9,6 +9,9 @@ import flixopt as fx if __name__ == '__main__': + # Enable console logging + fx.CONFIG.Logging.console = True + fx.CONFIG.apply() # --- Experiment Options --- # Configure options for testing various parameters and behaviors check_penalty = False @@ -40,8 +43,8 @@ # --- Define Effects --- # Specify effects related to costs, CO2 emissions, and primary energy consumption - Costs = fx.Effect('costs', '€', 'Kosten', is_standard=True, is_objective=True) - CO2 = fx.Effect('CO2', 'kg', 'CO2_e-Emissionen', specific_share_to_other_effects_operation={Costs.label: 0.2}) + Costs = fx.Effect('costs', '€', 'Kosten', is_standard=True, is_objective=True, share_from_temporal={'CO2': 0.2}) + CO2 = fx.Effect('CO2', 'kg', 'CO2_e-Emissionen') PE = fx.Effect('PE', 'kWh_PE', 'Primärenergie', maximum_total=3.5e3) # --- Define Components --- @@ -57,10 +60,10 @@ label='Q_th', # Thermal output bus='Fernwärme', # Linked bus size=fx.InvestParameters( - fix_effects=1000, # Fixed investment costs + effects_of_investment=1000, # Fixed investment costs fixed_size=50, # Fixed size - optional=False, # Forced investment - specific_effects={Costs.label: 10, PE.label: 2}, # Specific costs + mandatory=True, # Forced investment + effects_of_investment_per_size={Costs.label: 10, PE.label: 2}, # Specific costs ), load_factor_max=1.0, # Maximum load factor (50 kW) load_factor_min=0.1, # Minimum load factor (5 kW) @@ -72,9 +75,8 @@ on_hours_total_min=0, # Minimum operating hours on_hours_total_max=1000, # Maximum operating hours consecutive_on_hours_max=10, # Max consecutive operating hours - consecutive_on_hours_min=np.array( - [1, 1, 1, 1, 1, 2, 2, 2, 2] - ), # min consecutive operation hoursconsecutive_off_hours_max=10, # Max consecutive off hours + consecutive_on_hours_min=np.array([1, 1, 1, 1, 1, 2, 2, 2, 2]), # min consecutive operation hours + consecutive_off_hours_max=10, # Max consecutive off hours effects_per_switch_on=0.01, # Cost per switch-on switch_on_total_max=1000, # Max number of starts ), @@ -130,8 +132,8 @@ charging=fx.Flow('Q_th_load', bus='Fernwärme', size=1e4), discharging=fx.Flow('Q_th_unload', bus='Fernwärme', size=1e4), capacity_in_flow_hours=fx.InvestParameters( - piecewise_effects=segmented_investment_effects, # Investment effects - optional=False, # Forced investment + piecewise_effects_of_investment=segmented_investment_effects, # Investment effects + mandatory=True, # Forced investment minimum_size=0, maximum_size=1000, # Optimizing between 0 and 1000 kWh ), @@ -147,33 +149,39 @@ # 5.a) Heat demand profile Waermelast = fx.Sink( 'Wärmelast', - sink=fx.Flow( - 'Q_th_Last', # Heat sink - bus='Fernwärme', # Linked bus - size=1, - fixed_relative_profile=heat_demand, # Fixed demand profile - ), + inputs=[ + fx.Flow( + 'Q_th_Last', # Heat sink + bus='Fernwärme', # Linked bus + size=1, + fixed_relative_profile=heat_demand, # Fixed demand profile + ) + ], ) # 5.b) Gas tariff Gasbezug = fx.Source( 'Gastarif', - source=fx.Flow( - 'Q_Gas', - bus='Gas', # Gas source - size=1000, # Nominal size - effects_per_flow_hour={Costs.label: 0.04, CO2.label: 0.3}, - ), + outputs=[ + fx.Flow( + 'Q_Gas', + bus='Gas', # Gas source + size=1000, # Nominal size + effects_per_flow_hour={Costs.label: 0.04, CO2.label: 0.3}, + ) + ], ) # 5.c) Feed-in of electricity Stromverkauf = fx.Sink( 'Einspeisung', - sink=fx.Flow( - 'P_el', - bus='Strom', # Feed-in tariff for electricity - effects_per_flow_hour=-1 * electricity_price, # Negative price for feed-in - ), + inputs=[ + fx.Flow( + 'P_el', + bus='Strom', # Feed-in tariff for electricity + effects_per_flow_hour=-1 * electricity_price, # Negative price for feed-in + ) + ], ) # --- Build FlowSystem --- @@ -182,7 +190,10 @@ flow_system.add_elements(bhkw_2) if use_chp_with_piecewise_conversion else flow_system.add_elements(bhkw) pprint(flow_system) # Get a string representation of the FlowSystem - flow_system.start_network_app() # Start the network app. DOes only work with extra dependencies installed + try: + flow_system.start_network_app() # Start the network app + except ImportError as e: + print(f'Network app requires extra dependencies: {e}') # --- Solve FlowSystem --- calculation = fx.FullCalculation('complex example', flow_system, time_indices) diff --git a/examples/02_Complex/complex_example_results.py b/examples/02_Complex/complex_example_results.py index 3be201ae8..96d06dd04 100644 --- a/examples/02_Complex/complex_example_results.py +++ b/examples/02_Complex/complex_example_results.py @@ -5,6 +5,9 @@ import flixopt as fx if __name__ == '__main__': + # Enable console logging + fx.CONFIG.Logging.console = True + fx.CONFIG.apply() # --- Load Results --- try: results = fx.results.CalculationResults.from_file('results', 'complex example') @@ -22,8 +25,9 @@ # --- Detailed Plots --- # In depth plot for individual flow rates ('__' is used as the delimiter between Component and Flow results.plot_heatmap('Wärmelast(Q_th_Last)|flow_rate') - for flow_rate in results['BHKW2'].inputs + results['BHKW2'].outputs: - results.plot_heatmap(flow_rate) + for bus in results.buses.values(): + bus.plot_node_balance_pie(show=False, save=f'results/{bus.label}--pie.html') + bus.plot_node_balance(show=False, save=f'results/{bus.label}--balance.html') # --- Plotting internal variables manually --- results.plot_heatmap('BHKW2(Q_th)|on') diff --git a/examples/03_Calculation_types/example_calculation_types.py b/examples/03_Calculation_types/example_calculation_types.py index 7248c87ad..c5df50034 100644 --- a/examples/03_Calculation_types/example_calculation_types.py +++ b/examples/03_Calculation_types/example_calculation_types.py @@ -11,6 +11,9 @@ import flixopt as fx if __name__ == '__main__': + # Enable console logging + fx.CONFIG.Logging.console = True + fx.CONFIG.apply() # Calculation Types full, segmented, aggregated = True, True, True @@ -30,8 +33,10 @@ excess_penalty = 1e5 # or set to None if not needed # Data Import - data_import = pd.read_csv(pathlib.Path('Zeitreihen2020.csv'), index_col=0).sort_index() - filtered_data = data_import['2020-01-01':'2020-01-02 23:45:00'] + data_import = pd.read_csv( + pathlib.Path(__file__).parent.parent / 'resources' / 'Zeitreihen2020.csv', index_col=0 + ).sort_index() + filtered_data = data_import['2020-01-01':'2020-01-07 23:45:00'] # filtered_data = data_import[0:500] # Alternatively filter by index filtered_data.index = pd.to_datetime(filtered_data.index) @@ -46,7 +51,7 @@ # TimeSeriesData objects TS_heat_demand = fx.TimeSeriesData(heat_demand) TS_electricity_demand = fx.TimeSeriesData(electricity_demand, aggregation_weight=0.7) - TS_electricity_price_sell = fx.TimeSeriesData(-(electricity_demand - 0.5), aggregation_group='p_el') + TS_electricity_price_sell = fx.TimeSeriesData(-(electricity_price - 0.5), aggregation_group='p_el') TS_electricity_price_buy = fx.TimeSeriesData(electricity_price + 0.5, aggregation_group='p_el') flow_system = fx.FlowSystem(timesteps) @@ -108,36 +113,43 @@ # 4. Sinks and Sources # Heat Load Profile a_waermelast = fx.Sink( - 'Wärmelast', sink=fx.Flow('Q_th_Last', bus='Fernwärme', size=1, fixed_relative_profile=TS_heat_demand) + 'Wärmelast', inputs=[fx.Flow('Q_th_Last', bus='Fernwärme', size=1, fixed_relative_profile=TS_heat_demand)] ) # Electricity Feed-in a_strom_last = fx.Sink( - 'Stromlast', sink=fx.Flow('P_el_Last', bus='Strom', size=1, fixed_relative_profile=TS_electricity_demand) + 'Stromlast', inputs=[fx.Flow('P_el_Last', bus='Strom', size=1, fixed_relative_profile=TS_electricity_demand)] ) # Gas Tariff a_gas_tarif = fx.Source( 'Gastarif', - source=fx.Flow('Q_Gas', bus='Gas', size=1000, effects_per_flow_hour={costs.label: gas_price, CO2.label: 0.3}), + outputs=[ + fx.Flow('Q_Gas', bus='Gas', size=1000, effects_per_flow_hour={costs.label: gas_price, CO2.label: 0.3}) + ], ) # Coal Tariff a_kohle_tarif = fx.Source( 'Kohletarif', - source=fx.Flow('Q_Kohle', bus='Kohle', size=1000, effects_per_flow_hour={costs.label: 4.6, CO2.label: 0.3}), + outputs=[fx.Flow('Q_Kohle', bus='Kohle', size=1000, effects_per_flow_hour={costs.label: 4.6, CO2.label: 0.3})], ) # Electricity Tariff and Feed-in a_strom_einspeisung = fx.Sink( - 'Einspeisung', sink=fx.Flow('P_el', bus='Strom', size=1000, effects_per_flow_hour=TS_electricity_price_sell) + 'Einspeisung', inputs=[fx.Flow('P_el', bus='Strom', size=1000, effects_per_flow_hour=TS_electricity_price_sell)] ) a_strom_tarif = fx.Source( 'Stromtarif', - source=fx.Flow( - 'P_el', bus='Strom', size=1000, effects_per_flow_hour={costs.label: TS_electricity_price_buy, CO2: 0.3} - ), + outputs=[ + fx.Flow( + 'P_el', + bus='Strom', + size=1000, + effects_per_flow_hour={costs.label: TS_electricity_price_buy, CO2.label: 0.3}, + ) + ], ) # Flow System Setup @@ -190,36 +202,39 @@ def get_solutions(calcs: list, variable: str) -> xr.Dataset: # --- Plotting for comparison --- fx.plotting.with_plotly( - get_solutions(calculations, 'Speicher|charge_state').to_dataframe(), - style='line', + get_solutions(calculations, 'Speicher|charge_state'), + mode='line', title='Charge State Comparison', ylabel='Charge state', + xlabel='Time in h', ).write_html('results/Charge State.html') fx.plotting.with_plotly( - get_solutions(calculations, 'BHKW2(Q_th)|flow_rate').to_dataframe(), - style='line', + get_solutions(calculations, 'BHKW2(Q_th)|flow_rate'), + mode='line', title='BHKW2(Q_th) Flow Rate Comparison', ylabel='Flow rate', + xlabel='Time in h', ).write_html('results/BHKW2 Thermal Power.html') fx.plotting.with_plotly( - get_solutions(calculations, 'costs(operation)|total_per_timestep').to_dataframe(), - style='line', + get_solutions(calculations, 'costs(temporal)|per_timestep'), + mode='line', title='Operation Cost Comparison', ylabel='Costs [€]', + xlabel='Time in h', ).write_html('results/Operation Costs.html') fx.plotting.with_plotly( - pd.DataFrame(get_solutions(calculations, 'costs(operation)|total_per_timestep').to_dataframe().sum()).T, - style='stacked_bar', + get_solutions(calculations, 'costs(temporal)|per_timestep').sum('time'), + mode='stacked_bar', title='Total Cost Comparison', ylabel='Costs [€]', ).update_layout(barmode='group').write_html('results/Total Costs.html') fx.plotting.with_plotly( - pd.DataFrame([calc.durations for calc in calculations], index=[calc.name for calc in calculations]), - 'stacked_bar', + pd.DataFrame([calc.durations for calc in calculations], index=[calc.name for calc in calculations]).to_xarray(), + mode='stacked_bar', ).update_layout(title='Duration Comparison', xaxis_title='Calculation type', yaxis_title='Time (s)').write_html( 'results/Speed Comparison.html' ) diff --git a/examples/04_Scenarios/scenario_example.py b/examples/04_Scenarios/scenario_example.py index f675211e8..d258d4142 100644 --- a/examples/04_Scenarios/scenario_example.py +++ b/examples/04_Scenarios/scenario_example.py @@ -8,20 +8,80 @@ import flixopt as fx if __name__ == '__main__': - # Create datetime array starting from '2020-01-01' for the given time period - timesteps = pd.date_range('2020-01-01', periods=9, freq='h') + # Create datetime array starting from '2020-01-01' for one week + timesteps = pd.date_range('2020-01-01', periods=24 * 7, freq='h') scenarios = pd.Index(['Base Case', 'High Demand']) - years = pd.Index([2020, 2021, 2022]) + periods = pd.Index([2020, 2021, 2022]) # --- Create Time Series Data --- - # Heat demand profile (e.g., kW) over time and corresponding power prices - heat_demand_per_h = pd.DataFrame( - {'Base Case': [30, 0, 90, 110, 110, 20, 20, 20, 20], 'High Demand': [30, 0, 100, 118, 125, 20, 20, 20, 20]}, - index=timesteps, + # Realistic daily patterns: morning/evening peaks, night/midday lows + np.random.seed(42) + n_hours = len(timesteps) + + # Heat demand: 24-hour patterns (kW) for Base Case and High Demand scenarios + base_daily_pattern = np.array( + [22, 20, 18, 18, 20, 25, 40, 70, 95, 110, 85, 65, 60, 58, 62, 68, 75, 88, 105, 125, 130, 122, 95, 35] + ) + high_daily_pattern = np.array( + [28, 25, 22, 22, 24, 30, 52, 88, 118, 135, 105, 80, 75, 72, 75, 82, 92, 108, 128, 148, 155, 145, 115, 48] + ) + + # Tile and add variation + base_demand = np.tile(base_daily_pattern, n_hours // 24 + 1)[:n_hours] * ( + 1 + np.random.uniform(-0.05, 0.05, n_hours) + ) + high_demand = np.tile(high_daily_pattern, n_hours // 24 + 1)[:n_hours] * ( + 1 + np.random.uniform(-0.07, 0.07, n_hours) + ) + + heat_demand_per_h = pd.DataFrame({'Base Case': base_demand, 'High Demand': high_demand}, index=timesteps) + + # Power prices: hourly factors (night low, peak high) and period escalation (2020-2022) + hourly_price_factors = np.array( + [ + 0.70, + 0.65, + 0.62, + 0.60, + 0.62, + 0.70, + 0.95, + 1.15, + 1.30, + 1.25, + 1.10, + 1.00, + 0.95, + 0.90, + 0.88, + 0.92, + 1.00, + 1.10, + 1.25, + 1.40, + 1.35, + 1.20, + 0.95, + 0.80, + ] ) - power_prices = np.array([0.08, 0.09, 0.10]) + period_base_prices = np.array([0.075, 0.095, 0.135]) # €/kWh for 2020, 2021, 2022 - flow_system = fx.FlowSystem(timesteps=timesteps, years=years, scenarios=scenarios, weights=np.array([0.5, 0.6])) + price_series = np.zeros((n_hours, 3)) + for period_idx, base_price in enumerate(period_base_prices): + price_series[:, period_idx] = ( + np.tile(hourly_price_factors, n_hours // 24 + 1)[:n_hours] + * base_price + * (1 + np.random.uniform(-0.03, 0.03, n_hours)) + ) + + power_prices = price_series.mean(axis=0) + + # Scenario weights: probability of each scenario occurring + # Base Case: 60% probability, High Demand: 40% probability + scenario_weights = np.array([0.6, 0.4]) + + flow_system = fx.FlowSystem(timesteps=timesteps, periods=periods, scenarios=scenarios, weights=scenario_weights) # --- Define Energy Buses --- # These represent nodes, where the used medias are balanced (electricity, heat, and gas) @@ -35,22 +95,24 @@ description='Kosten', is_standard=True, # standard effect: no explicit value needed for costs is_objective=True, # Minimizing costs as the optimization objective + share_from_temporal={'CO2': 0.2}, # Carbon price: 0.2 €/kg CO2 (e.g., carbon tax) ) - # CO2 emissions effect with an associated cost impact + # CO2 emissions effect with constraint + # Maximum of 1000 kg CO2/hour represents a regulatory or voluntary emissions limit CO2 = fx.Effect( label='CO2', unit='kg', description='CO2_e-Emissionen', - specific_share_to_other_effects_operation={costs.label: 0.2}, - maximum_operation_per_hour=1000, # Max CO2 emissions per hour + maximum_per_hour=1000, # Regulatory emissions limit: 1000 kg CO2/hour ) # --- Define Flow System Components --- # Boiler: Converts fuel (gas) into thermal energy (heat) + # Modern condensing gas boiler with realistic efficiency boiler = fx.linear_converters.Boiler( label='Boiler', - eta=0.5, + eta=0.92, # Realistic efficiency for modern condensing gas boiler (92%) Q_th=fx.Flow( label='Q_th', bus='Fernwärme', @@ -63,45 +125,60 @@ ) # Combined Heat and Power (CHP): Generates both electricity and heat from fuel + # Modern CHP unit with realistic efficiencies (total efficiency ~88%) chp = fx.linear_converters.CHP( label='CHP', - eta_th=0.5, - eta_el=0.4, + eta_th=0.48, # Realistic thermal efficiency (48%) + eta_el=0.40, # Realistic electrical efficiency (40%) P_el=fx.Flow('P_el', bus='Strom', size=60, relative_minimum=5 / 60, on_off_parameters=fx.OnOffParameters()), Q_th=fx.Flow('Q_th', bus='Fernwärme'), Q_fu=fx.Flow('Q_fu', bus='Gas'), ) - # Storage: Energy storage system with charging and discharging capabilities + # Storage: Thermal energy storage system with charging and discharging capabilities + # Realistic thermal storage parameters (e.g., insulated hot water tank) storage = fx.Storage( label='Storage', charging=fx.Flow('Q_th_load', bus='Fernwärme', size=1000), discharging=fx.Flow('Q_th_unload', bus='Fernwärme', size=1000), - capacity_in_flow_hours=fx.InvestParameters(fix_effects=20, fixed_size=30, optional=False), + capacity_in_flow_hours=fx.InvestParameters(effects_of_investment=20, fixed_size=30, mandatory=True), initial_charge_state=0, # Initial storage state: empty - relative_maximum_charge_state=np.array([80, 70, 80, 80, 80, 80, 80, 80, 80]) * 0.01, - relative_maximum_final_charge_state=0.8, - eta_charge=0.9, - eta_discharge=1, # Efficiency factors for charging/discharging - relative_loss_per_hour=0.08, # 8% loss per hour. Absolute loss depends on current charge state + relative_maximum_final_charge_state=np.array([0.8, 0.5, 0.1]), + eta_charge=0.95, # Realistic charging efficiency (~95%) + eta_discharge=0.98, # Realistic discharging efficiency (~98%) + relative_loss_per_hour=np.array([0.008, 0.015]), # Realistic thermal losses: 0.8-1.5% per hour prevent_simultaneous_charge_and_discharge=True, # Prevent charging and discharging at the same time ) # Heat Demand Sink: Represents a fixed heat demand profile heat_sink = fx.Sink( label='Heat Demand', - sink=fx.Flow(label='Q_th_Last', bus='Fernwärme', size=1, fixed_relative_profile=heat_demand_per_h), + inputs=[fx.Flow(label='Q_th_Last', bus='Fernwärme', size=1, fixed_relative_profile=heat_demand_per_h)], ) # Gas Source: Gas tariff source with associated costs and CO2 emissions + # Realistic gas prices varying by period (reflecting 2020-2022 energy crisis) + # 2020: 0.04 €/kWh, 2021: 0.06 €/kWh, 2022: 0.11 €/kWh + gas_prices_per_period = np.array([0.04, 0.06, 0.11]) + + # CO2 emissions factor for natural gas: ~0.202 kg CO2/kWh (realistic value) + gas_co2_emissions = 0.202 + gas_source = fx.Source( label='Gastarif', - source=fx.Flow(label='Q_Gas', bus='Gas', size=1000, effects_per_flow_hour={costs.label: 0.04, CO2.label: 0.3}), + outputs=[ + fx.Flow( + label='Q_Gas', + bus='Gas', + size=1000, + effects_per_flow_hour={costs.label: gas_prices_per_period, CO2.label: gas_co2_emissions}, + ) + ], ) # Power Sink: Represents the export of electricity to the grid power_sink = fx.Sink( - label='Einspeisung', sink=fx.Flow(label='P_el', bus='Strom', effects_per_flow_hour=-1 * power_prices) + label='Einspeisung', inputs=[fx.Flow(label='P_el', bus='Strom', effects_per_flow_hour=-1 * power_prices)] ) # --- Build the Flow System --- @@ -119,19 +196,26 @@ # --- Solve the Calculation and Save Results --- calculation.solve(fx.solvers.HighsSolver(mip_gap=0, time_limit_seconds=30)) + calculation.results.setup_colors( + { + 'CHP': 'red', + 'Greys': ['Gastarif', 'Einspeisung', 'Heat Demand'], + 'Storage': 'blue', + 'Boiler': 'orange', + } + ) + calculation.results.plot_heatmap('CHP(Q_th)|flow_rate') # --- Analyze Results --- - calculation.results['Fernwärme'].plot_node_balance_pie() - calculation.results['Fernwärme'].plot_node_balance(style='stacked_bar') - calculation.results['Storage'].plot_node_balance() + calculation.results['Fernwärme'].plot_node_balance(mode='stacked_bar') calculation.results.plot_heatmap('CHP(Q_th)|flow_rate') + calculation.results['Storage'].plot_charge_state() + calculation.results['Fernwärme'].plot_node_balance_pie(select={'period': 2020, 'scenario': 'Base Case'}) # Convert the results for the storage component to a dataframe and display df = calculation.results['Storage'].node_balance_with_charge_state() print(df) - calculation.results['Storage'].plot_charge_state(engine='matplotlib') # Save results to file for later usage calculation.results.to_file() - fig, ax = calculation.results['Storage'].plot_charge_state(engine='matplotlib') diff --git a/examples/05_Two-stage-optimization/Zeitreihen2020.csv b/examples/05_Two-stage-optimization/Zeitreihen2020.csv deleted file mode 100644 index 9b660ef9c..000000000 --- a/examples/05_Two-stage-optimization/Zeitreihen2020.csv +++ /dev/null @@ -1,35137 +0,0 @@ -Zeit,P_Netz/MW,Q_Netz/MW,Strompr.€/MWh,Gaspr.€/MWh -2020-01-01 00:00:00,58.39,127.059,7.461,32.459 -2020-01-01 00:15:00,58.36,122.156,7.461,32.459 -2020-01-01 00:30:00,58.11,124.412,7.461,32.459 -2020-01-01 00:45:00,57.71,127.713,7.461,32.459 -2020-01-01 01:00:00,55.53,130.69899999999998,2.65,32.459 -2020-01-01 01:15:00,56.24,132.166,2.65,32.459 -2020-01-01 01:30:00,55.17,132.394,2.65,32.459 -2020-01-01 01:45:00,54.5,132.431,2.65,32.459 -2020-01-01 02:00:00,52.95,134.403,-2.949,32.459 -2020-01-01 02:15:00,51.75,134.755,-2.949,32.459 -2020-01-01 02:30:00,50.7,135.631,-2.949,32.459 -2020-01-01 02:45:00,50.33,138.345,-2.949,32.459 -2020-01-01 03:00:00,47.11,141.071,-3.2680000000000002,32.459 -2020-01-01 03:15:00,49.35,141.319,-3.2680000000000002,32.459 -2020-01-01 03:30:00,48.33,143.024,-3.2680000000000002,32.459 -2020-01-01 03:45:00,48.73,144.697,-3.2680000000000002,32.459 -2020-01-01 04:00:00,46.6,152.569,-3.2680000000000002,32.459 -2020-01-01 04:15:00,47.33,160.688,-3.2680000000000002,32.459 -2020-01-01 04:30:00,47.5,161.673,-3.2680000000000002,32.459 -2020-01-01 04:45:00,48.62,163.165,-3.2680000000000002,32.459 -2020-01-01 05:00:00,49.18,176.398,-3.2680000000000002,32.459 -2020-01-01 05:15:00,50.0,184.233,-3.2680000000000002,32.459 -2020-01-01 05:30:00,50.1,181.12,-3.2680000000000002,32.459 -2020-01-01 05:45:00,50.36,179.642,-3.2680000000000002,32.459 -2020-01-01 06:00:00,50.32,196.364,-3.2680000000000002,32.459 -2020-01-01 06:15:00,50.22,215.918,-3.2680000000000002,32.459 -2020-01-01 06:30:00,52.17,211.0,-3.2680000000000002,32.459 -2020-01-01 06:45:00,54.2,206.111,-3.2680000000000002,32.459 -2020-01-01 07:00:00,55.6,202.635,-3.2680000000000002,32.459 -2020-01-01 07:15:00,55.35,207.093,-3.2680000000000002,32.459 -2020-01-01 07:30:00,55.44,211.667,-3.2680000000000002,32.459 -2020-01-01 07:45:00,55.15,215.882,-3.2680000000000002,32.459 -2020-01-01 08:00:00,56.21,219.793,-2.146,32.459 -2020-01-01 08:15:00,55.94,223.388,-2.146,32.459 -2020-01-01 08:30:00,58.2,226.081,-2.146,32.459 -2020-01-01 08:45:00,56.11,227.016,-2.146,32.459 -2020-01-01 09:00:00,61.62,222.30700000000002,1.7519999999999998,32.459 -2020-01-01 09:15:00,63.5,221.018,1.7519999999999998,32.459 -2020-01-01 09:30:00,64.24,219.09,1.7519999999999998,32.459 -2020-01-01 09:45:00,64.02,215.80700000000002,1.7519999999999998,32.459 -2020-01-01 10:00:00,61.38,212.231,4.19,32.459 -2020-01-01 10:15:00,62.05,209.50099999999998,4.19,32.459 -2020-01-01 10:30:00,61.84,206.963,4.19,32.459 -2020-01-01 10:45:00,65.9,204.111,4.19,32.459 -2020-01-01 11:00:00,67.33,203.37599999999998,5.517,32.459 -2020-01-01 11:15:00,68.34,200.575,5.517,32.459 -2020-01-01 11:30:00,67.43,198.755,5.517,32.459 -2020-01-01 11:45:00,68.73,197.22099999999998,5.517,32.459 -2020-01-01 12:00:00,74.2,191.555,4.27,32.459 -2020-01-01 12:15:00,69.91,191.37,4.27,32.459 -2020-01-01 12:30:00,68.83,190.28799999999998,4.27,32.459 -2020-01-01 12:45:00,70.01,189.851,4.27,32.459 -2020-01-01 13:00:00,69.02,188.09599999999998,3.484,32.459 -2020-01-01 13:15:00,68.68,189.604,3.484,32.459 -2020-01-01 13:30:00,68.45,188.61,3.484,32.459 -2020-01-01 13:45:00,68.18,188.303,3.484,32.459 -2020-01-01 14:00:00,68.94,187.555,2.523,32.459 -2020-01-01 14:15:00,66.1,188.747,2.523,32.459 -2020-01-01 14:30:00,70.61,189.53099999999998,2.523,32.459 -2020-01-01 14:45:00,70.58,189.93,2.523,32.459 -2020-01-01 15:00:00,73.81,189.581,5.667999999999999,32.459 -2020-01-01 15:15:00,69.6,191.454,5.667999999999999,32.459 -2020-01-01 15:30:00,68.66,194.55599999999998,5.667999999999999,32.459 -2020-01-01 15:45:00,71.32,197.227,5.667999999999999,32.459 -2020-01-01 16:00:00,73.3,197.49900000000002,12.109000000000002,32.459 -2020-01-01 16:15:00,77.24,199.38099999999997,12.109000000000002,32.459 -2020-01-01 16:30:00,83.2,202.511,12.109000000000002,32.459 -2020-01-01 16:45:00,86.91,205.047,12.109000000000002,32.459 -2020-01-01 17:00:00,89.85,207.06099999999998,22.824,32.459 -2020-01-01 17:15:00,90.61,208.69799999999998,22.824,32.459 -2020-01-01 17:30:00,92.03,209.25099999999998,22.824,32.459 -2020-01-01 17:45:00,93.49,210.672,22.824,32.459 -2020-01-01 18:00:00,95.17,211.632,21.656,32.459 -2020-01-01 18:15:00,94.51,211.645,21.656,32.459 -2020-01-01 18:30:00,93.87,209.94,21.656,32.459 -2020-01-01 18:45:00,93.48,208.489,21.656,32.459 -2020-01-01 19:00:00,92.14,209.826,19.749000000000002,32.459 -2020-01-01 19:15:00,90.85,207.487,19.749000000000002,32.459 -2020-01-01 19:30:00,89.81,204.959,19.749000000000002,32.459 -2020-01-01 19:45:00,88.14,202.29,19.749000000000002,32.459 -2020-01-01 20:00:00,84.73,200.835,24.274,32.459 -2020-01-01 20:15:00,83.37,197.709,24.274,32.459 -2020-01-01 20:30:00,82.23,194.57299999999998,24.274,32.459 -2020-01-01 20:45:00,80.34,191.52200000000002,24.274,32.459 -2020-01-01 21:00:00,78.43,188.843,23.044,32.459 -2020-01-01 21:15:00,77.95,186.56099999999998,23.044,32.459 -2020-01-01 21:30:00,77.04,186.183,23.044,32.459 -2020-01-01 21:45:00,76.57,184.824,23.044,32.459 -2020-01-01 22:00:00,72.56,178.928,25.155,32.459 -2020-01-01 22:15:00,72.48,174.81799999999998,25.155,32.459 -2020-01-01 22:30:00,70.04,170.76,25.155,32.459 -2020-01-01 22:45:00,67.68,167.32,25.155,32.459 -2020-01-01 23:00:00,59.3,159.845,20.101,32.459 -2020-01-01 23:15:00,63.51,156.17700000000002,20.101,32.459 -2020-01-01 23:30:00,61.08,153.764,20.101,32.459 -2020-01-01 23:45:00,60.81,150.845,20.101,32.459 -2020-01-02 00:00:00,72.56,131.099,38.399,32.641 -2020-01-02 00:15:00,75.51,130.822,38.399,32.641 -2020-01-02 00:30:00,76.2,132.15,38.399,32.641 -2020-01-02 00:45:00,79.94,133.753,38.399,32.641 -2020-01-02 01:00:00,76.22,136.549,36.94,32.641 -2020-01-02 01:15:00,75.52,136.971,36.94,32.641 -2020-01-02 01:30:00,69.71,137.433,36.94,32.641 -2020-01-02 01:45:00,72.15,137.96200000000002,36.94,32.641 -2020-01-02 02:00:00,68.46,139.933,35.275,32.641 -2020-01-02 02:15:00,68.93,141.73,35.275,32.641 -2020-01-02 02:30:00,69.13,142.344,35.275,32.641 -2020-01-02 02:45:00,71.05,144.38,35.275,32.641 -2020-01-02 03:00:00,77.24,147.17700000000002,35.329,32.641 -2020-01-02 03:15:00,78.88,148.17700000000002,35.329,32.641 -2020-01-02 03:30:00,79.6,150.07,35.329,32.641 -2020-01-02 03:45:00,73.47,151.487,35.329,32.641 -2020-01-02 04:00:00,71.66,163.75,36.275,32.641 -2020-01-02 04:15:00,72.6,175.77599999999998,36.275,32.641 -2020-01-02 04:30:00,74.43,178.856,36.275,32.641 -2020-01-02 04:45:00,76.31,181.783,36.275,32.641 -2020-01-02 05:00:00,80.9,217.042,42.193999999999996,32.641 -2020-01-02 05:15:00,83.91,245.78799999999998,42.193999999999996,32.641 -2020-01-02 05:30:00,88.55,241.4,42.193999999999996,32.641 -2020-01-02 05:45:00,92.98,234.083,42.193999999999996,32.641 -2020-01-02 06:00:00,101.92,230.607,56.422,32.641 -2020-01-02 06:15:00,106.02,236.44099999999997,56.422,32.641 -2020-01-02 06:30:00,110.67,239.31400000000002,56.422,32.641 -2020-01-02 06:45:00,115.36,243.33,56.422,32.641 -2020-01-02 07:00:00,121.21,242.197,72.569,32.641 -2020-01-02 07:15:00,125.44,247.68200000000002,72.569,32.641 -2020-01-02 07:30:00,127.04,250.858,72.569,32.641 -2020-01-02 07:45:00,129.3,252.55900000000003,72.569,32.641 -2020-01-02 08:00:00,131.83,251.364,67.704,32.641 -2020-01-02 08:15:00,129.92,251.618,67.704,32.641 -2020-01-02 08:30:00,130.03,249.708,67.704,32.641 -2020-01-02 08:45:00,129.65,246.764,67.704,32.641 -2020-01-02 09:00:00,130.4,240.053,63.434,32.641 -2020-01-02 09:15:00,132.18,236.87900000000002,63.434,32.641 -2020-01-02 09:30:00,133.68,234.63099999999997,63.434,32.641 -2020-01-02 09:45:00,133.99,231.472,63.434,32.641 -2020-01-02 10:00:00,133.44,226.187,61.88399999999999,32.641 -2020-01-02 10:15:00,135.64,221.955,61.88399999999999,32.641 -2020-01-02 10:30:00,134.92,218.68400000000003,61.88399999999999,32.641 -2020-01-02 10:45:00,136.65,216.98,61.88399999999999,32.641 -2020-01-02 11:00:00,136.01,214.97299999999998,61.481,32.641 -2020-01-02 11:15:00,135.6,213.702,61.481,32.641 -2020-01-02 11:30:00,135.49,212.15200000000002,61.481,32.641 -2020-01-02 11:45:00,136.56,210.945,61.481,32.641 -2020-01-02 12:00:00,136.24,205.8,59.527,32.641 -2020-01-02 12:15:00,135.89,205.143,59.527,32.641 -2020-01-02 12:30:00,133.92,205.12400000000002,59.527,32.641 -2020-01-02 12:45:00,134.1,205.96099999999998,59.527,32.641 -2020-01-02 13:00:00,130.94,204.35,58.794,32.641 -2020-01-02 13:15:00,129.98,203.88099999999997,58.794,32.641 -2020-01-02 13:30:00,129.31,203.575,58.794,32.641 -2020-01-02 13:45:00,127.59,203.519,58.794,32.641 -2020-01-02 14:00:00,126.5,202.428,60.32,32.641 -2020-01-02 14:15:00,129.81,203.06900000000002,60.32,32.641 -2020-01-02 14:30:00,128.98,203.95,60.32,32.641 -2020-01-02 14:45:00,127.66,204.167,60.32,32.641 -2020-01-02 15:00:00,131.69,205.303,62.52,32.641 -2020-01-02 15:15:00,134.08,205.93900000000002,62.52,32.641 -2020-01-02 15:30:00,130.49,208.357,62.52,32.641 -2020-01-02 15:45:00,132.71,210.109,62.52,32.641 -2020-01-02 16:00:00,134.12,210.895,64.199,32.641 -2020-01-02 16:15:00,134.79,212.476,64.199,32.641 -2020-01-02 16:30:00,137.68,215.345,64.199,32.641 -2020-01-02 16:45:00,138.3,216.87599999999998,64.199,32.641 -2020-01-02 17:00:00,141.23,219.28900000000002,68.19800000000001,32.641 -2020-01-02 17:15:00,140.55,219.918,68.19800000000001,32.641 -2020-01-02 17:30:00,141.63,220.68200000000002,68.19800000000001,32.641 -2020-01-02 17:45:00,141.55,220.424,68.19800000000001,32.641 -2020-01-02 18:00:00,140.02,221.84400000000002,67.899,32.641 -2020-01-02 18:15:00,138.28,218.955,67.899,32.641 -2020-01-02 18:30:00,137.19,217.683,67.899,32.641 -2020-01-02 18:45:00,137.6,217.707,67.899,32.641 -2020-01-02 19:00:00,134.26,217.453,64.72399999999999,32.641 -2020-01-02 19:15:00,133.22,213.481,64.72399999999999,32.641 -2020-01-02 19:30:00,134.64,210.797,64.72399999999999,32.641 -2020-01-02 19:45:00,136.88,207.28599999999997,64.72399999999999,32.641 -2020-01-02 20:00:00,130.24,203.518,64.062,32.641 -2020-01-02 20:15:00,119.33,197.062,64.062,32.641 -2020-01-02 20:30:00,118.31,192.979,64.062,32.641 -2020-01-02 20:45:00,112.7,191.03099999999998,64.062,32.641 -2020-01-02 21:00:00,107.44,188.12900000000002,57.971000000000004,32.641 -2020-01-02 21:15:00,112.86,185.61900000000003,57.971000000000004,32.641 -2020-01-02 21:30:00,111.93,183.535,57.971000000000004,32.641 -2020-01-02 21:45:00,108.29,181.894,57.971000000000004,32.641 -2020-01-02 22:00:00,98.52,174.86,53.715,32.641 -2020-01-02 22:15:00,94.85,168.98,53.715,32.641 -2020-01-02 22:30:00,98.1,155.042,53.715,32.641 -2020-01-02 22:45:00,96.62,146.733,53.715,32.641 -2020-01-02 23:00:00,92.22,140.148,47.8,32.641 -2020-01-02 23:15:00,84.12,138.344,47.8,32.641 -2020-01-02 23:30:00,79.31,138.518,47.8,32.641 -2020-01-02 23:45:00,83.54,137.971,47.8,32.641 -2020-01-03 00:00:00,83.23,130.233,43.656000000000006,32.641 -2020-01-03 00:15:00,82.94,130.13299999999998,43.656000000000006,32.641 -2020-01-03 00:30:00,78.25,131.279,43.656000000000006,32.641 -2020-01-03 00:45:00,76.9,132.954,43.656000000000006,32.641 -2020-01-03 01:00:00,71.11,135.453,41.263000000000005,32.641 -2020-01-03 01:15:00,78.36,136.91899999999998,41.263000000000005,32.641 -2020-01-03 01:30:00,78.99,137.079,41.263000000000005,32.641 -2020-01-03 01:45:00,77.55,137.732,41.263000000000005,32.641 -2020-01-03 02:00:00,71.19,139.738,40.799,32.641 -2020-01-03 02:15:00,72.17,141.41299999999998,40.799,32.641 -2020-01-03 02:30:00,74.79,142.542,40.799,32.641 -2020-01-03 02:45:00,78.06,144.681,40.799,32.641 -2020-01-03 03:00:00,76.51,146.311,41.398,32.641 -2020-01-03 03:15:00,74.89,148.47,41.398,32.641 -2020-01-03 03:30:00,79.0,150.364,41.398,32.641 -2020-01-03 03:45:00,77.85,152.072,41.398,32.641 -2020-01-03 04:00:00,73.23,164.549,42.38,32.641 -2020-01-03 04:15:00,74.31,176.43200000000002,42.38,32.641 -2020-01-03 04:30:00,74.97,179.67700000000002,42.38,32.641 -2020-01-03 04:45:00,78.19,181.39700000000002,42.38,32.641 -2020-01-03 05:00:00,82.49,215.25400000000002,46.181000000000004,32.641 -2020-01-03 05:15:00,85.31,245.535,46.181000000000004,32.641 -2020-01-03 05:30:00,88.01,242.33599999999998,46.181000000000004,32.641 -2020-01-03 05:45:00,93.72,235.011,46.181000000000004,32.641 -2020-01-03 06:00:00,101.82,232.016,59.33,32.641 -2020-01-03 06:15:00,106.19,236.21900000000002,59.33,32.641 -2020-01-03 06:30:00,110.85,238.166,59.33,32.641 -2020-01-03 06:45:00,115.45,244.024,59.33,32.641 -2020-01-03 07:00:00,122.71,241.924,72.454,32.641 -2020-01-03 07:15:00,123.17,248.451,72.454,32.641 -2020-01-03 07:30:00,125.55,251.585,72.454,32.641 -2020-01-03 07:45:00,128.9,252.31099999999998,72.454,32.641 -2020-01-03 08:00:00,130.12,249.83599999999998,67.175,32.641 -2020-01-03 08:15:00,128.89,249.597,67.175,32.641 -2020-01-03 08:30:00,128.21,248.743,67.175,32.641 -2020-01-03 08:45:00,128.6,244.05900000000003,67.175,32.641 -2020-01-03 09:00:00,128.71,238.011,65.365,32.641 -2020-01-03 09:15:00,131.57,235.324,65.365,32.641 -2020-01-03 09:30:00,133.77,232.672,65.365,32.641 -2020-01-03 09:45:00,136.47,229.359,65.365,32.641 -2020-01-03 10:00:00,135.77,222.843,63.95,32.641 -2020-01-03 10:15:00,137.43,219.39,63.95,32.641 -2020-01-03 10:30:00,136.81,215.986,63.95,32.641 -2020-01-03 10:45:00,137.07,213.808,63.95,32.641 -2020-01-03 11:00:00,136.69,211.75,63.92100000000001,32.641 -2020-01-03 11:15:00,137.82,209.576,63.92100000000001,32.641 -2020-01-03 11:30:00,137.25,209.956,63.92100000000001,32.641 -2020-01-03 11:45:00,136.75,208.86900000000003,63.92100000000001,32.641 -2020-01-03 12:00:00,135.72,204.88400000000001,60.79600000000001,32.641 -2020-01-03 12:15:00,135.16,202.011,60.79600000000001,32.641 -2020-01-03 12:30:00,133.9,202.153,60.79600000000001,32.641 -2020-01-03 12:45:00,134.58,203.607,60.79600000000001,32.641 -2020-01-03 13:00:00,130.87,202.96200000000002,59.393,32.641 -2020-01-03 13:15:00,131.78,203.36,59.393,32.641 -2020-01-03 13:30:00,130.35,203.00799999999998,59.393,32.641 -2020-01-03 13:45:00,131.11,202.855,59.393,32.641 -2020-01-03 14:00:00,130.67,200.58700000000002,57.943999999999996,32.641 -2020-01-03 14:15:00,130.14,201.0,57.943999999999996,32.641 -2020-01-03 14:30:00,129.02,202.34799999999998,57.943999999999996,32.641 -2020-01-03 14:45:00,129.81,202.954,57.943999999999996,32.641 -2020-01-03 15:00:00,130.32,203.59099999999998,60.153999999999996,32.641 -2020-01-03 15:15:00,129.09,203.75099999999998,60.153999999999996,32.641 -2020-01-03 15:30:00,128.33,204.56099999999998,60.153999999999996,32.641 -2020-01-03 15:45:00,128.17,206.41299999999998,60.153999999999996,32.641 -2020-01-03 16:00:00,131.49,205.998,62.933,32.641 -2020-01-03 16:15:00,133.11,207.87400000000002,62.933,32.641 -2020-01-03 16:30:00,134.7,210.864,62.933,32.641 -2020-01-03 16:45:00,133.81,212.332,62.933,32.641 -2020-01-03 17:00:00,137.76,214.84900000000002,68.657,32.641 -2020-01-03 17:15:00,136.16,215.076,68.657,32.641 -2020-01-03 17:30:00,140.54,215.513,68.657,32.641 -2020-01-03 17:45:00,137.57,215.02900000000002,68.657,32.641 -2020-01-03 18:00:00,136.81,217.225,67.111,32.641 -2020-01-03 18:15:00,136.99,213.96900000000002,67.111,32.641 -2020-01-03 18:30:00,135.65,213.13,67.111,32.641 -2020-01-03 18:45:00,136.2,213.142,67.111,32.641 -2020-01-03 19:00:00,132.87,213.791,62.434,32.641 -2020-01-03 19:15:00,131.87,211.248,62.434,32.641 -2020-01-03 19:30:00,132.37,208.11900000000003,62.434,32.641 -2020-01-03 19:45:00,134.77,204.162,62.434,32.641 -2020-01-03 20:00:00,128.17,200.44400000000002,61.763000000000005,32.641 -2020-01-03 20:15:00,120.91,193.95,61.763000000000005,32.641 -2020-01-03 20:30:00,117.84,189.834,61.763000000000005,32.641 -2020-01-03 20:45:00,113.28,188.551,61.763000000000005,32.641 -2020-01-03 21:00:00,110.73,186.10299999999998,56.785,32.641 -2020-01-03 21:15:00,113.2,183.95,56.785,32.641 -2020-01-03 21:30:00,110.05,181.924,56.785,32.641 -2020-01-03 21:45:00,104.45,180.86900000000003,56.785,32.641 -2020-01-03 22:00:00,98.56,174.898,52.693000000000005,32.641 -2020-01-03 22:15:00,102.19,168.90400000000002,52.693000000000005,32.641 -2020-01-03 22:30:00,100.03,161.57,52.693000000000005,32.641 -2020-01-03 22:45:00,96.9,157.013,52.693000000000005,32.641 -2020-01-03 23:00:00,89.83,149.83100000000002,45.443999999999996,32.641 -2020-01-03 23:15:00,91.91,146.02700000000002,45.443999999999996,32.641 -2020-01-03 23:30:00,88.18,144.751,45.443999999999996,32.641 -2020-01-03 23:45:00,85.54,143.495,45.443999999999996,32.641 -2020-01-04 00:00:00,82.22,127.199,44.738,32.459 -2020-01-04 00:15:00,83.18,122.48899999999999,44.738,32.459 -2020-01-04 00:30:00,81.48,125.102,44.738,32.459 -2020-01-04 00:45:00,73.34,127.589,44.738,32.459 -2020-01-04 01:00:00,72.21,130.76,40.303000000000004,32.459 -2020-01-04 01:15:00,77.36,131.101,40.303000000000004,32.459 -2020-01-04 01:30:00,78.27,130.754,40.303000000000004,32.459 -2020-01-04 01:45:00,75.98,131.06799999999998,40.303000000000004,32.459 -2020-01-04 02:00:00,69.25,133.903,38.61,32.459 -2020-01-04 02:15:00,74.64,135.233,38.61,32.459 -2020-01-04 02:30:00,74.85,135.217,38.61,32.459 -2020-01-04 02:45:00,73.61,137.407,38.61,32.459 -2020-01-04 03:00:00,68.1,139.812,37.554,32.459 -2020-01-04 03:15:00,73.13,140.717,37.554,32.459 -2020-01-04 03:30:00,75.4,140.84799999999998,37.554,32.459 -2020-01-04 03:45:00,70.56,142.566,37.554,32.459 -2020-01-04 04:00:00,67.07,150.611,37.176,32.459 -2020-01-04 04:15:00,67.97,159.77100000000002,37.176,32.459 -2020-01-04 04:30:00,67.71,160.769,37.176,32.459 -2020-01-04 04:45:00,68.5,161.921,37.176,32.459 -2020-01-04 05:00:00,67.87,178.856,36.893,32.459 -2020-01-04 05:15:00,67.75,189.139,36.893,32.459 -2020-01-04 05:30:00,66.1,186.215,36.893,32.459 -2020-01-04 05:45:00,67.33,184.52,36.893,32.459 -2020-01-04 06:00:00,69.91,201.26,37.803000000000004,32.459 -2020-01-04 06:15:00,69.0,222.77200000000002,37.803000000000004,32.459 -2020-01-04 06:30:00,69.5,219.078,37.803000000000004,32.459 -2020-01-04 06:45:00,71.88,215.377,37.803000000000004,32.459 -2020-01-04 07:00:00,76.68,209.287,41.086999999999996,32.459 -2020-01-04 07:15:00,77.71,214.53599999999997,41.086999999999996,32.459 -2020-01-04 07:30:00,80.15,220.502,41.086999999999996,32.459 -2020-01-04 07:45:00,83.14,225.497,41.086999999999996,32.459 -2020-01-04 08:00:00,85.43,227.446,48.222,32.459 -2020-01-04 08:15:00,85.69,231.175,48.222,32.459 -2020-01-04 08:30:00,89.46,232.06099999999998,48.222,32.459 -2020-01-04 08:45:00,91.91,230.729,48.222,32.459 -2020-01-04 09:00:00,92.65,226.387,52.791000000000004,32.459 -2020-01-04 09:15:00,93.59,224.49200000000002,52.791000000000004,32.459 -2020-01-04 09:30:00,94.68,222.80200000000002,52.791000000000004,32.459 -2020-01-04 09:45:00,96.53,219.69,52.791000000000004,32.459 -2020-01-04 10:00:00,98.05,213.424,54.341,32.459 -2020-01-04 10:15:00,95.0,210.122,54.341,32.459 -2020-01-04 10:30:00,94.99,206.91099999999997,54.341,32.459 -2020-01-04 10:45:00,92.74,206.176,54.341,32.459 -2020-01-04 11:00:00,96.61,204.363,51.94,32.459 -2020-01-04 11:15:00,100.28,201.382,51.94,32.459 -2020-01-04 11:30:00,99.55,200.535,51.94,32.459 -2020-01-04 11:45:00,98.58,198.36700000000002,51.94,32.459 -2020-01-04 12:00:00,96.09,193.4,50.973,32.459 -2020-01-04 12:15:00,98.67,191.18099999999998,50.973,32.459 -2020-01-04 12:30:00,96.52,191.675,50.973,32.459 -2020-01-04 12:45:00,95.72,192.248,50.973,32.459 -2020-01-04 13:00:00,93.57,191.208,48.06399999999999,32.459 -2020-01-04 13:15:00,92.81,189.408,48.06399999999999,32.459 -2020-01-04 13:30:00,91.55,188.543,48.06399999999999,32.459 -2020-01-04 13:45:00,89.25,188.99,48.06399999999999,32.459 -2020-01-04 14:00:00,89.5,188.077,45.707,32.459 -2020-01-04 14:15:00,89.52,187.99,45.707,32.459 -2020-01-04 14:30:00,90.0,187.408,45.707,32.459 -2020-01-04 14:45:00,91.09,188.234,45.707,32.459 -2020-01-04 15:00:00,91.5,189.60299999999998,47.567,32.459 -2020-01-04 15:15:00,91.94,190.57,47.567,32.459 -2020-01-04 15:30:00,92.53,193.02900000000002,47.567,32.459 -2020-01-04 15:45:00,94.28,194.947,47.567,32.459 -2020-01-04 16:00:00,96.74,193.149,52.031000000000006,32.459 -2020-01-04 16:15:00,95.42,196.053,52.031000000000006,32.459 -2020-01-04 16:30:00,100.94,198.982,52.031000000000006,32.459 -2020-01-04 16:45:00,102.46,201.418,52.031000000000006,32.459 -2020-01-04 17:00:00,105.49,203.416,58.218999999999994,32.459 -2020-01-04 17:15:00,104.81,205.575,58.218999999999994,32.459 -2020-01-04 17:30:00,107.13,205.94299999999998,58.218999999999994,32.459 -2020-01-04 17:45:00,108.26,205.013,58.218999999999994,32.459 -2020-01-04 18:00:00,110.16,206.662,57.65,32.459 -2020-01-04 18:15:00,109.91,205.239,57.65,32.459 -2020-01-04 18:30:00,109.66,205.75799999999998,57.65,32.459 -2020-01-04 18:45:00,109.05,202.394,57.65,32.459 -2020-01-04 19:00:00,107.78,204.105,51.261,32.459 -2020-01-04 19:15:00,103.06,201.082,51.261,32.459 -2020-01-04 19:30:00,101.89,198.71599999999998,51.261,32.459 -2020-01-04 19:45:00,102.31,194.47799999999998,51.261,32.459 -2020-01-04 20:00:00,94.91,193.0,44.068000000000005,32.459 -2020-01-04 20:15:00,93.02,188.81400000000002,44.068000000000005,32.459 -2020-01-04 20:30:00,88.29,184.359,44.068000000000005,32.459 -2020-01-04 20:45:00,87.9,182.563,44.068000000000005,32.459 -2020-01-04 21:00:00,84.51,182.58599999999998,38.861,32.459 -2020-01-04 21:15:00,83.28,180.90400000000002,38.861,32.459 -2020-01-04 21:30:00,82.28,180.18200000000002,38.861,32.459 -2020-01-04 21:45:00,81.25,178.737,38.861,32.459 -2020-01-04 22:00:00,77.7,174.207,39.485,32.459 -2020-01-04 22:15:00,77.25,170.87,39.485,32.459 -2020-01-04 22:30:00,74.24,170.22299999999998,39.485,32.459 -2020-01-04 22:45:00,73.18,167.65200000000002,39.485,32.459 -2020-01-04 23:00:00,68.36,163.07399999999998,32.027,32.459 -2020-01-04 23:15:00,68.86,157.509,32.027,32.459 -2020-01-04 23:30:00,66.13,154.256,32.027,32.459 -2020-01-04 23:45:00,65.23,150.387,32.027,32.459 -2020-01-05 00:00:00,61.83,127.708,26.96,32.459 -2020-01-05 00:15:00,59.65,122.706,26.96,32.459 -2020-01-05 00:30:00,56.73,124.90899999999999,26.96,32.459 -2020-01-05 00:45:00,58.04,128.141,26.96,32.459 -2020-01-05 01:00:00,54.64,131.15200000000002,24.295,32.459 -2020-01-05 01:15:00,53.12,132.619,24.295,32.459 -2020-01-05 01:30:00,54.89,132.84799999999998,24.295,32.459 -2020-01-05 01:45:00,55.39,132.83700000000002,24.295,32.459 -2020-01-05 02:00:00,52.81,134.87,24.268,32.459 -2020-01-05 02:15:00,53.85,135.224,24.268,32.459 -2020-01-05 02:30:00,53.36,136.131,24.268,32.459 -2020-01-05 02:45:00,53.22,138.842,24.268,32.459 -2020-01-05 03:00:00,51.24,141.54,23.373,32.459 -2020-01-05 03:15:00,52.77,141.878,23.373,32.459 -2020-01-05 03:30:00,53.33,143.584,23.373,32.459 -2020-01-05 03:45:00,52.95,145.284,23.373,32.459 -2020-01-05 04:00:00,53.02,153.043,23.874000000000002,32.459 -2020-01-05 04:15:00,54.36,161.123,23.874000000000002,32.459 -2020-01-05 04:30:00,55.19,162.089,23.874000000000002,32.459 -2020-01-05 04:45:00,54.83,163.567,23.874000000000002,32.459 -2020-01-05 05:00:00,56.2,176.618,24.871,32.459 -2020-01-05 05:15:00,57.61,184.28099999999998,24.871,32.459 -2020-01-05 05:30:00,58.05,181.218,24.871,32.459 -2020-01-05 05:45:00,58.68,179.833,24.871,32.459 -2020-01-05 06:00:00,58.97,196.65900000000002,23.84,32.459 -2020-01-05 06:15:00,60.37,216.24099999999999,23.84,32.459 -2020-01-05 06:30:00,60.21,211.415,23.84,32.459 -2020-01-05 06:45:00,60.83,206.671,23.84,32.459 -2020-01-05 07:00:00,63.06,203.24599999999998,27.430999999999997,32.459 -2020-01-05 07:15:00,64.35,207.676,27.430999999999997,32.459 -2020-01-05 07:30:00,66.38,212.19400000000002,27.430999999999997,32.459 -2020-01-05 07:45:00,68.85,216.345,27.430999999999997,32.459 -2020-01-05 08:00:00,71.87,220.25099999999998,33.891999999999996,32.459 -2020-01-05 08:15:00,72.96,223.791,33.891999999999996,32.459 -2020-01-05 08:30:00,75.42,226.391,33.891999999999996,32.459 -2020-01-05 08:45:00,77.7,227.232,33.891999999999996,32.459 -2020-01-05 09:00:00,79.34,222.44799999999998,37.571,32.459 -2020-01-05 09:15:00,79.42,221.187,37.571,32.459 -2020-01-05 09:30:00,80.81,219.317,37.571,32.459 -2020-01-05 09:45:00,82.3,216.00799999999998,37.571,32.459 -2020-01-05 10:00:00,83.72,212.428,40.594,32.459 -2020-01-05 10:15:00,84.02,209.692,40.594,32.459 -2020-01-05 10:30:00,86.92,207.107,40.594,32.459 -2020-01-05 10:45:00,89.06,204.26,40.594,32.459 -2020-01-05 11:00:00,91.79,203.445,44.133,32.459 -2020-01-05 11:15:00,98.14,200.63099999999997,44.133,32.459 -2020-01-05 11:30:00,99.24,198.81799999999998,44.133,32.459 -2020-01-05 11:45:00,100.24,197.28900000000002,44.133,32.459 -2020-01-05 12:00:00,99.8,191.658,41.198,32.459 -2020-01-05 12:15:00,96.2,191.544,41.198,32.459 -2020-01-05 12:30:00,92.34,190.454,41.198,32.459 -2020-01-05 12:45:00,91.8,190.02599999999998,41.198,32.459 -2020-01-05 13:00:00,86.98,188.218,37.014,32.459 -2020-01-05 13:15:00,86.22,189.688,37.014,32.459 -2020-01-05 13:30:00,85.32,188.65200000000002,37.014,32.459 -2020-01-05 13:45:00,85.43,188.31799999999998,37.014,32.459 -2020-01-05 14:00:00,83.74,187.613,34.934,32.459 -2020-01-05 14:15:00,83.86,188.78400000000002,34.934,32.459 -2020-01-05 14:30:00,84.5,189.609,34.934,32.459 -2020-01-05 14:45:00,84.63,190.06799999999998,34.934,32.459 -2020-01-05 15:00:00,85.54,189.8,34.588,32.459 -2020-01-05 15:15:00,85.23,191.607,34.588,32.459 -2020-01-05 15:30:00,84.85,194.704,34.588,32.459 -2020-01-05 15:45:00,85.8,197.34099999999998,34.588,32.459 -2020-01-05 16:00:00,87.47,197.61700000000002,37.874,32.459 -2020-01-05 16:15:00,90.49,199.547,37.874,32.459 -2020-01-05 16:30:00,92.21,202.707,37.874,32.459 -2020-01-05 16:45:00,93.29,205.295,37.874,32.459 -2020-01-05 17:00:00,98.04,207.237,47.303999999999995,32.459 -2020-01-05 17:15:00,97.7,208.99900000000002,47.303999999999995,32.459 -2020-01-05 17:30:00,99.31,209.662,47.303999999999995,32.459 -2020-01-05 17:45:00,100.34,211.15,47.303999999999995,32.459 -2020-01-05 18:00:00,102.25,212.196,48.879,32.459 -2020-01-05 18:15:00,100.54,212.206,48.879,32.459 -2020-01-05 18:30:00,100.22,210.523,48.879,32.459 -2020-01-05 18:45:00,99.47,209.142,48.879,32.459 -2020-01-05 19:00:00,99.14,210.358,44.826,32.459 -2020-01-05 19:15:00,98.01,208.015,44.826,32.459 -2020-01-05 19:30:00,96.07,205.486,44.826,32.459 -2020-01-05 19:45:00,94.14,202.801,44.826,32.459 -2020-01-05 20:00:00,93.2,201.287,40.154,32.459 -2020-01-05 20:15:00,91.82,198.157,40.154,32.459 -2020-01-05 20:30:00,90.16,194.97099999999998,40.154,32.459 -2020-01-05 20:45:00,88.47,191.995,40.154,32.459 -2020-01-05 21:00:00,84.27,189.24099999999999,36.549,32.459 -2020-01-05 21:15:00,84.17,186.889,36.549,32.459 -2020-01-05 21:30:00,84.43,186.51,36.549,32.459 -2020-01-05 21:45:00,85.26,185.207,36.549,32.459 -2020-01-05 22:00:00,83.7,179.30900000000003,37.663000000000004,32.459 -2020-01-05 22:15:00,82.23,175.25599999999997,37.663000000000004,32.459 -2020-01-05 22:30:00,80.43,171.27900000000002,37.663000000000004,32.459 -2020-01-05 22:45:00,79.58,167.87099999999998,37.663000000000004,32.459 -2020-01-05 23:00:00,75.67,160.305,31.945,32.459 -2020-01-05 23:15:00,77.15,156.664,31.945,32.459 -2020-01-05 23:30:00,74.59,154.317,31.945,32.459 -2020-01-05 23:45:00,73.41,151.388,31.945,32.459 -2020-01-06 00:00:00,70.86,132.295,31.533,32.641 -2020-01-06 00:15:00,69.7,130.435,31.533,32.641 -2020-01-06 00:30:00,69.11,132.78799999999998,31.533,32.641 -2020-01-06 00:45:00,68.33,135.447,31.533,32.641 -2020-01-06 01:00:00,67.32,138.469,30.56,32.641 -2020-01-06 01:15:00,67.69,139.35399999999998,30.56,32.641 -2020-01-06 01:30:00,67.44,139.616,30.56,32.641 -2020-01-06 01:45:00,67.1,139.725,30.56,32.641 -2020-01-06 02:00:00,67.52,141.722,29.55,32.641 -2020-01-06 02:15:00,67.29,143.738,29.55,32.641 -2020-01-06 02:30:00,67.05,145.003,29.55,32.641 -2020-01-06 02:45:00,67.2,147.05200000000002,29.55,32.641 -2020-01-06 03:00:00,68.35,151.1,27.059,32.641 -2020-01-06 03:15:00,68.78,153.201,27.059,32.641 -2020-01-06 03:30:00,69.82,154.562,27.059,32.641 -2020-01-06 03:45:00,69.92,155.701,27.059,32.641 -2020-01-06 04:00:00,67.73,167.963,28.384,32.641 -2020-01-06 04:15:00,71.53,180.334,28.384,32.641 -2020-01-06 04:30:00,73.1,183.72799999999998,28.384,32.641 -2020-01-06 04:45:00,76.51,185.351,28.384,32.641 -2020-01-06 05:00:00,81.52,215.055,35.915,32.641 -2020-01-06 05:15:00,84.58,243.92,35.915,32.641 -2020-01-06 05:30:00,89.42,241.291,35.915,32.641 -2020-01-06 05:45:00,94.89,233.987,35.915,32.641 -2020-01-06 06:00:00,103.99,232.019,56.18,32.641 -2020-01-06 06:15:00,110.77,236.107,56.18,32.641 -2020-01-06 06:30:00,116.07,239.75,56.18,32.641 -2020-01-06 06:45:00,119.86,244.24,56.18,32.641 -2020-01-06 07:00:00,126.05,243.37599999999998,70.877,32.641 -2020-01-06 07:15:00,129.26,248.982,70.877,32.641 -2020-01-06 07:30:00,131.09,252.704,70.877,32.641 -2020-01-06 07:45:00,132.63,254.012,70.877,32.641 -2020-01-06 08:00:00,136.79,252.68599999999998,65.65,32.641 -2020-01-06 08:15:00,137.86,253.967,65.65,32.641 -2020-01-06 08:30:00,137.45,252.168,65.65,32.641 -2020-01-06 08:45:00,136.78,249.345,65.65,32.641 -2020-01-06 09:00:00,137.29,243.503,62.037,32.641 -2020-01-06 09:15:00,138.96,238.595,62.037,32.641 -2020-01-06 09:30:00,139.49,235.72099999999998,62.037,32.641 -2020-01-06 09:45:00,139.62,232.888,62.037,32.641 -2020-01-06 10:00:00,140.37,228.12599999999998,60.409,32.641 -2020-01-06 10:15:00,140.63,225.046,60.409,32.641 -2020-01-06 10:30:00,138.89,221.533,60.409,32.641 -2020-01-06 10:45:00,138.54,219.613,60.409,32.641 -2020-01-06 11:00:00,137.59,215.89700000000002,60.211999999999996,32.641 -2020-01-06 11:15:00,138.33,215.011,60.211999999999996,32.641 -2020-01-06 11:30:00,139.52,214.644,60.211999999999996,32.641 -2020-01-06 11:45:00,136.44,212.643,60.211999999999996,32.641 -2020-01-06 12:00:00,137.83,209.015,57.733000000000004,32.641 -2020-01-06 12:15:00,134.41,208.912,57.733000000000004,32.641 -2020-01-06 12:30:00,133.37,208.162,57.733000000000004,32.641 -2020-01-06 12:45:00,131.24,209.385,57.733000000000004,32.641 -2020-01-06 13:00:00,130.49,208.141,58.695,32.641 -2020-01-06 13:15:00,127.09,208.16099999999997,58.695,32.641 -2020-01-06 13:30:00,123.67,206.52900000000002,58.695,32.641 -2020-01-06 13:45:00,127.05,206.16299999999998,58.695,32.641 -2020-01-06 14:00:00,129.68,204.84099999999998,59.505,32.641 -2020-01-06 14:15:00,129.73,205.275,59.505,32.641 -2020-01-06 14:30:00,130.81,205.53400000000002,59.505,32.641 -2020-01-06 14:45:00,131.3,205.83599999999998,59.505,32.641 -2020-01-06 15:00:00,131.31,207.50599999999997,59.946000000000005,32.641 -2020-01-06 15:15:00,131.43,207.78400000000002,59.946000000000005,32.641 -2020-01-06 15:30:00,130.17,209.935,59.946000000000005,32.641 -2020-01-06 15:45:00,131.03,212.113,59.946000000000005,32.641 -2020-01-06 16:00:00,134.41,212.476,61.766999999999996,32.641 -2020-01-06 16:15:00,134.55,213.59400000000002,61.766999999999996,32.641 -2020-01-06 16:30:00,140.59,215.763,61.766999999999996,32.641 -2020-01-06 16:45:00,140.87,217.083,61.766999999999996,32.641 -2020-01-06 17:00:00,140.83,218.85299999999998,67.85600000000001,32.641 -2020-01-06 17:15:00,140.84,219.60299999999998,67.85600000000001,32.641 -2020-01-06 17:30:00,142.16,219.729,67.85600000000001,32.641 -2020-01-06 17:45:00,140.41,219.657,67.85600000000001,32.641 -2020-01-06 18:00:00,137.78,221.21599999999998,64.564,32.641 -2020-01-06 18:15:00,136.67,218.96900000000002,64.564,32.641 -2020-01-06 18:30:00,133.62,218.032,64.564,32.641 -2020-01-06 18:45:00,134.52,217.257,64.564,32.641 -2020-01-06 19:00:00,131.97,216.72299999999998,58.536,32.641 -2020-01-06 19:15:00,129.61,213.06,58.536,32.641 -2020-01-06 19:30:00,131.14,211.083,58.536,32.641 -2020-01-06 19:45:00,133.72,207.54,58.536,32.641 -2020-01-06 20:00:00,128.55,203.563,59.888999999999996,32.641 -2020-01-06 20:15:00,117.39,197.7,59.888999999999996,32.641 -2020-01-06 20:30:00,115.28,192.479,59.888999999999996,32.641 -2020-01-06 20:45:00,111.64,191.25099999999998,59.888999999999996,32.641 -2020-01-06 21:00:00,108.63,189.085,52.652,32.641 -2020-01-06 21:15:00,110.16,185.43099999999998,52.652,32.641 -2020-01-06 21:30:00,111.85,184.145,52.652,32.641 -2020-01-06 21:45:00,110.02,182.327,52.652,32.641 -2020-01-06 22:00:00,103.97,173.459,46.17,32.641 -2020-01-06 22:15:00,101.93,167.895,46.17,32.641 -2020-01-06 22:30:00,100.68,153.954,46.17,32.641 -2020-01-06 22:45:00,97.59,145.379,46.17,32.641 -2020-01-06 23:00:00,90.0,138.627,36.281,32.641 -2020-01-06 23:15:00,87.29,137.92700000000002,36.281,32.641 -2020-01-06 23:30:00,83.89,138.554,36.281,32.641 -2020-01-06 23:45:00,88.74,138.484,36.281,32.641 -2020-01-07 00:00:00,86.26,131.85,38.821999999999996,32.641 -2020-01-07 00:15:00,81.91,131.452,38.821999999999996,32.641 -2020-01-07 00:30:00,81.41,132.71200000000002,38.821999999999996,32.641 -2020-01-07 00:45:00,83.42,134.23,38.821999999999996,32.641 -2020-01-07 01:00:00,82.73,137.08700000000002,36.936,32.641 -2020-01-07 01:15:00,80.08,137.469,36.936,32.641 -2020-01-07 01:30:00,78.9,137.929,36.936,32.641 -2020-01-07 01:45:00,81.69,138.401,36.936,32.641 -2020-01-07 02:00:00,81.57,140.446,34.42,32.641 -2020-01-07 02:15:00,80.71,142.245,34.42,32.641 -2020-01-07 02:30:00,78.77,142.898,34.42,32.641 -2020-01-07 02:45:00,81.99,144.93200000000002,34.42,32.641 -2020-01-07 03:00:00,82.0,147.695,33.585,32.641 -2020-01-07 03:15:00,79.21,148.804,33.585,32.641 -2020-01-07 03:30:00,82.09,150.697,33.585,32.641 -2020-01-07 03:45:00,82.69,152.15,33.585,32.641 -2020-01-07 04:00:00,77.46,164.275,35.622,32.641 -2020-01-07 04:15:00,77.37,176.252,35.622,32.641 -2020-01-07 04:30:00,77.18,179.312,35.622,32.641 -2020-01-07 04:45:00,80.05,182.22,35.622,32.641 -2020-01-07 05:00:00,84.68,217.25599999999997,40.599000000000004,32.641 -2020-01-07 05:15:00,88.21,245.796,40.599000000000004,32.641 -2020-01-07 05:30:00,91.8,241.465,40.599000000000004,32.641 -2020-01-07 05:45:00,96.21,234.25900000000001,40.599000000000004,32.641 -2020-01-07 06:00:00,104.74,230.912,55.203,32.641 -2020-01-07 06:15:00,111.62,236.783,55.203,32.641 -2020-01-07 06:30:00,115.53,239.761,55.203,32.641 -2020-01-07 06:45:00,119.58,243.951,55.203,32.641 -2020-01-07 07:00:00,121.42,242.885,69.029,32.641 -2020-01-07 07:15:00,129.39,248.329,69.029,32.641 -2020-01-07 07:30:00,132.48,251.43200000000002,69.029,32.641 -2020-01-07 07:45:00,134.56,253.047,69.029,32.641 -2020-01-07 08:00:00,136.12,251.842,65.85300000000001,32.641 -2020-01-07 08:15:00,135.08,252.024,65.85300000000001,32.641 -2020-01-07 08:30:00,135.3,249.989,65.85300000000001,32.641 -2020-01-07 08:45:00,134.8,246.93400000000003,65.85300000000001,32.641 -2020-01-07 09:00:00,135.59,240.132,61.566,32.641 -2020-01-07 09:15:00,137.31,236.993,61.566,32.641 -2020-01-07 09:30:00,139.13,234.81900000000002,61.566,32.641 -2020-01-07 09:45:00,138.88,231.628,61.566,32.641 -2020-01-07 10:00:00,138.47,226.34,61.244,32.641 -2020-01-07 10:15:00,136.8,222.109,61.244,32.641 -2020-01-07 10:30:00,135.87,218.78,61.244,32.641 -2020-01-07 10:45:00,136.63,217.085,61.244,32.641 -2020-01-07 11:00:00,136.36,214.982,61.16,32.641 -2020-01-07 11:15:00,139.62,213.697,61.16,32.641 -2020-01-07 11:30:00,138.15,212.15400000000002,61.16,32.641 -2020-01-07 11:45:00,134.33,210.957,61.16,32.641 -2020-01-07 12:00:00,132.11,205.859,59.09,32.641 -2020-01-07 12:15:00,133.48,205.292,59.09,32.641 -2020-01-07 12:30:00,132.6,205.255,59.09,32.641 -2020-01-07 12:45:00,132.01,206.10299999999998,59.09,32.641 -2020-01-07 13:00:00,132.8,204.433,60.21,32.641 -2020-01-07 13:15:00,132.97,203.91299999999998,60.21,32.641 -2020-01-07 13:30:00,131.24,203.553,60.21,32.641 -2020-01-07 13:45:00,130.77,203.46599999999998,60.21,32.641 -2020-01-07 14:00:00,129.75,202.437,60.673,32.641 -2020-01-07 14:15:00,129.53,203.049,60.673,32.641 -2020-01-07 14:30:00,124.81,203.976,60.673,32.641 -2020-01-07 14:45:00,126.69,204.27,60.673,32.641 -2020-01-07 15:00:00,127.02,205.505,62.232,32.641 -2020-01-07 15:15:00,126.96,206.055,62.232,32.641 -2020-01-07 15:30:00,127.74,208.46,62.232,32.641 -2020-01-07 15:45:00,128.19,210.165,62.232,32.641 -2020-01-07 16:00:00,130.28,210.956,63.611999999999995,32.641 -2020-01-07 16:15:00,132.81,212.595,63.611999999999995,32.641 -2020-01-07 16:30:00,135.09,215.49900000000002,63.611999999999995,32.641 -2020-01-07 16:45:00,135.68,217.08900000000003,63.611999999999995,32.641 -2020-01-07 17:00:00,136.74,219.416,70.658,32.641 -2020-01-07 17:15:00,137.73,220.201,70.658,32.641 -2020-01-07 17:30:00,140.3,221.10299999999998,70.658,32.641 -2020-01-07 17:45:00,142.14,220.933,70.658,32.641 -2020-01-07 18:00:00,139.97,222.459,68.361,32.641 -2020-01-07 18:15:00,138.96,219.579,68.361,32.641 -2020-01-07 18:30:00,138.24,218.333,68.361,32.641 -2020-01-07 18:45:00,139.74,218.446,68.361,32.641 -2020-01-07 19:00:00,132.95,218.03900000000002,62.922,32.641 -2020-01-07 19:15:00,131.59,214.065,62.922,32.641 -2020-01-07 19:30:00,137.99,211.38400000000001,62.922,32.641 -2020-01-07 19:45:00,136.02,207.86,62.922,32.641 -2020-01-07 20:00:00,126.68,204.014,63.251999999999995,32.641 -2020-01-07 20:15:00,117.77,197.558,63.251999999999995,32.641 -2020-01-07 20:30:00,114.85,193.41400000000002,63.251999999999995,32.641 -2020-01-07 20:45:00,111.71,191.56,63.251999999999995,32.641 -2020-01-07 21:00:00,108.54,188.565,54.47,32.641 -2020-01-07 21:15:00,108.54,185.96599999999998,54.47,32.641 -2020-01-07 21:30:00,111.64,183.882,54.47,32.641 -2020-01-07 21:45:00,109.59,182.312,54.47,32.641 -2020-01-07 22:00:00,99.32,175.27200000000002,51.12,32.641 -2020-01-07 22:15:00,98.89,169.467,51.12,32.641 -2020-01-07 22:30:00,93.04,155.618,51.12,32.641 -2020-01-07 22:45:00,92.29,147.349,51.12,32.641 -2020-01-07 23:00:00,94.19,140.65200000000002,42.156000000000006,32.641 -2020-01-07 23:15:00,93.18,138.885,42.156000000000006,32.641 -2020-01-07 23:30:00,88.85,139.142,42.156000000000006,32.641 -2020-01-07 23:45:00,80.72,138.58700000000002,42.156000000000006,32.641 -2020-01-08 00:00:00,80.57,131.975,37.192,32.641 -2020-01-08 00:15:00,77.7,131.555,37.192,32.641 -2020-01-08 00:30:00,82.22,132.80200000000002,37.192,32.641 -2020-01-08 00:45:00,83.18,134.30200000000002,37.192,32.641 -2020-01-08 01:00:00,80.31,137.168,32.24,32.641 -2020-01-08 01:15:00,80.35,137.54,32.24,32.641 -2020-01-08 01:30:00,81.14,138.001,32.24,32.641 -2020-01-08 01:45:00,83.23,138.461,32.24,32.641 -2020-01-08 02:00:00,82.4,140.52,30.34,32.641 -2020-01-08 02:15:00,76.87,142.319,30.34,32.641 -2020-01-08 02:30:00,81.52,142.981,30.34,32.641 -2020-01-08 02:45:00,82.44,145.014,30.34,32.641 -2020-01-08 03:00:00,82.38,147.77100000000002,29.129,32.641 -2020-01-08 03:15:00,76.92,148.901,29.129,32.641 -2020-01-08 03:30:00,80.45,150.79399999999998,29.129,32.641 -2020-01-08 03:45:00,83.8,152.255,29.129,32.641 -2020-01-08 04:00:00,82.13,164.354,30.075,32.641 -2020-01-08 04:15:00,80.51,176.32,30.075,32.641 -2020-01-08 04:30:00,79.95,179.37900000000002,30.075,32.641 -2020-01-08 04:45:00,82.2,182.28,30.075,32.641 -2020-01-08 05:00:00,86.19,217.273,35.684,32.641 -2020-01-08 05:15:00,86.69,245.778,35.684,32.641 -2020-01-08 05:30:00,92.91,241.455,35.684,32.641 -2020-01-08 05:45:00,97.02,234.27,35.684,32.641 -2020-01-08 06:00:00,105.5,230.947,51.49,32.641 -2020-01-08 06:15:00,111.76,236.826,51.49,32.641 -2020-01-08 06:30:00,116.68,239.821,51.49,32.641 -2020-01-08 06:45:00,121.17,244.044,51.49,32.641 -2020-01-08 07:00:00,125.64,242.99099999999999,68.242,32.641 -2020-01-08 07:15:00,129.08,248.426,68.242,32.641 -2020-01-08 07:30:00,132.63,251.513,68.242,32.641 -2020-01-08 07:45:00,135.49,253.108,68.242,32.641 -2020-01-08 08:00:00,134.07,251.9,63.619,32.641 -2020-01-08 08:15:00,136.8,252.067,63.619,32.641 -2020-01-08 08:30:00,136.39,250.003,63.619,32.641 -2020-01-08 08:45:00,135.84,246.926,63.619,32.641 -2020-01-08 09:00:00,136.47,240.108,61.333,32.641 -2020-01-08 09:15:00,137.33,236.976,61.333,32.641 -2020-01-08 09:30:00,137.5,234.81799999999998,61.333,32.641 -2020-01-08 09:45:00,137.48,231.62099999999998,61.333,32.641 -2020-01-08 10:00:00,134.72,226.333,59.663000000000004,32.641 -2020-01-08 10:15:00,132.18,222.105,59.663000000000004,32.641 -2020-01-08 10:30:00,130.36,218.767,59.663000000000004,32.641 -2020-01-08 10:45:00,128.21,217.075,59.663000000000004,32.641 -2020-01-08 11:00:00,129.5,214.952,59.771,32.641 -2020-01-08 11:15:00,129.67,213.665,59.771,32.641 -2020-01-08 11:30:00,130.99,212.125,59.771,32.641 -2020-01-08 11:45:00,126.7,210.93099999999998,59.771,32.641 -2020-01-08 12:00:00,126.02,205.842,58.723,32.641 -2020-01-08 12:15:00,124.09,205.294,58.723,32.641 -2020-01-08 12:30:00,124.12,205.25099999999998,58.723,32.641 -2020-01-08 12:45:00,123.16,206.101,58.723,32.641 -2020-01-08 13:00:00,121.39,204.422,58.727,32.641 -2020-01-08 13:15:00,121.87,203.891,58.727,32.641 -2020-01-08 13:30:00,120.34,203.52,58.727,32.641 -2020-01-08 13:45:00,120.5,203.426,58.727,32.641 -2020-01-08 14:00:00,121.17,202.41299999999998,59.803999999999995,32.641 -2020-01-08 14:15:00,121.88,203.019,59.803999999999995,32.641 -2020-01-08 14:30:00,122.69,203.952,59.803999999999995,32.641 -2020-01-08 14:45:00,123.96,204.262,59.803999999999995,32.641 -2020-01-08 15:00:00,124.01,205.516,61.05,32.641 -2020-01-08 15:15:00,124.57,206.048,61.05,32.641 -2020-01-08 15:30:00,121.73,208.44799999999998,61.05,32.641 -2020-01-08 15:45:00,124.55,210.141,61.05,32.641 -2020-01-08 16:00:00,126.01,210.935,64.012,32.641 -2020-01-08 16:15:00,128.33,212.583,64.012,32.641 -2020-01-08 16:30:00,131.93,215.49400000000003,64.012,32.641 -2020-01-08 16:45:00,136.83,217.09400000000002,64.012,32.641 -2020-01-08 17:00:00,138.72,219.403,66.751,32.641 -2020-01-08 17:15:00,141.55,220.22,66.751,32.641 -2020-01-08 17:30:00,141.31,221.15200000000002,66.751,32.641 -2020-01-08 17:45:00,141.4,220.99900000000002,66.751,32.641 -2020-01-08 18:00:00,140.41,222.546,65.91199999999999,32.641 -2020-01-08 18:15:00,138.77,219.672,65.91199999999999,32.641 -2020-01-08 18:30:00,137.47,218.43200000000002,65.91199999999999,32.641 -2020-01-08 18:45:00,137.79,218.563,65.91199999999999,32.641 -2020-01-08 19:00:00,135.13,218.12400000000002,63.324,32.641 -2020-01-08 19:15:00,133.38,214.15,63.324,32.641 -2020-01-08 19:30:00,137.98,211.472,63.324,32.641 -2020-01-08 19:45:00,138.21,207.94799999999998,63.324,32.641 -2020-01-08 20:00:00,130.16,204.08599999999998,63.573,32.641 -2020-01-08 20:15:00,121.28,197.63099999999997,63.573,32.641 -2020-01-08 20:30:00,114.19,193.47799999999998,63.573,32.641 -2020-01-08 20:45:00,114.46,191.641,63.573,32.641 -2020-01-08 21:00:00,111.07,188.627,55.073,32.641 -2020-01-08 21:15:00,113.94,186.01,55.073,32.641 -2020-01-08 21:30:00,114.23,183.926,55.073,32.641 -2020-01-08 21:45:00,109.84,182.37,55.073,32.641 -2020-01-08 22:00:00,103.51,175.329,51.321999999999996,32.641 -2020-01-08 22:15:00,99.13,169.53799999999998,51.321999999999996,32.641 -2020-01-08 22:30:00,95.71,155.703,51.321999999999996,32.641 -2020-01-08 22:45:00,101.09,147.44299999999998,51.321999999999996,32.641 -2020-01-08 23:00:00,97.81,140.725,42.09,32.641 -2020-01-08 23:15:00,93.34,138.966,42.09,32.641 -2020-01-08 23:30:00,90.01,139.239,42.09,32.641 -2020-01-08 23:45:00,90.33,138.685,42.09,32.641 -2020-01-09 00:00:00,89.51,132.093,38.399,32.641 -2020-01-09 00:15:00,88.2,131.65,38.399,32.641 -2020-01-09 00:30:00,82.53,132.88299999999998,38.399,32.641 -2020-01-09 00:45:00,87.05,134.366,38.399,32.641 -2020-01-09 01:00:00,85.85,137.24,36.94,32.641 -2020-01-09 01:15:00,83.8,137.60399999999998,36.94,32.641 -2020-01-09 01:30:00,79.51,138.062,36.94,32.641 -2020-01-09 01:45:00,77.14,138.512,36.94,32.641 -2020-01-09 02:00:00,82.48,140.584,35.275,32.641 -2020-01-09 02:15:00,82.45,142.384,35.275,32.641 -2020-01-09 02:30:00,84.42,143.055,35.275,32.641 -2020-01-09 02:45:00,79.29,145.088,35.275,32.641 -2020-01-09 03:00:00,83.67,147.839,35.329,32.641 -2020-01-09 03:15:00,85.27,148.987,35.329,32.641 -2020-01-09 03:30:00,86.25,150.881,35.329,32.641 -2020-01-09 03:45:00,82.05,152.349,35.329,32.641 -2020-01-09 04:00:00,85.8,164.422,36.275,32.641 -2020-01-09 04:15:00,85.66,176.38,36.275,32.641 -2020-01-09 04:30:00,84.93,179.43599999999998,36.275,32.641 -2020-01-09 04:45:00,85.52,182.334,36.275,32.641 -2020-01-09 05:00:00,88.07,217.282,42.193999999999996,32.641 -2020-01-09 05:15:00,91.08,245.75099999999998,42.193999999999996,32.641 -2020-01-09 05:30:00,94.82,241.43599999999998,42.193999999999996,32.641 -2020-01-09 05:45:00,99.1,234.27200000000002,42.193999999999996,32.641 -2020-01-09 06:00:00,105.02,230.97299999999998,56.422,32.641 -2020-01-09 06:15:00,114.36,236.86,56.422,32.641 -2020-01-09 06:30:00,118.47,239.872,56.422,32.641 -2020-01-09 06:45:00,123.49,244.12599999999998,56.422,32.641 -2020-01-09 07:00:00,128.98,243.08900000000003,72.569,32.641 -2020-01-09 07:15:00,133.18,248.513,72.569,32.641 -2020-01-09 07:30:00,135.84,251.582,72.569,32.641 -2020-01-09 07:45:00,136.0,253.157,72.569,32.641 -2020-01-09 08:00:00,136.35,251.945,67.704,32.641 -2020-01-09 08:15:00,136.41,252.09599999999998,67.704,32.641 -2020-01-09 08:30:00,136.61,250.002,67.704,32.641 -2020-01-09 08:45:00,135.57,246.90599999999998,67.704,32.641 -2020-01-09 09:00:00,134.91,240.072,63.434,32.641 -2020-01-09 09:15:00,135.13,236.947,63.434,32.641 -2020-01-09 09:30:00,132.82,234.805,63.434,32.641 -2020-01-09 09:45:00,134.04,231.602,63.434,32.641 -2020-01-09 10:00:00,133.79,226.315,61.88399999999999,32.641 -2020-01-09 10:15:00,132.44,222.09,61.88399999999999,32.641 -2020-01-09 10:30:00,131.18,218.74200000000002,61.88399999999999,32.641 -2020-01-09 10:45:00,129.86,217.054,61.88399999999999,32.641 -2020-01-09 11:00:00,129.31,214.91099999999997,61.481,32.641 -2020-01-09 11:15:00,129.73,213.62400000000002,61.481,32.641 -2020-01-09 11:30:00,129.62,212.08599999999998,61.481,32.641 -2020-01-09 11:45:00,128.23,210.895,61.481,32.641 -2020-01-09 12:00:00,128.19,205.817,59.527,32.641 -2020-01-09 12:15:00,126.99,205.28599999999997,59.527,32.641 -2020-01-09 12:30:00,126.29,205.238,59.527,32.641 -2020-01-09 12:45:00,125.88,206.08900000000003,59.527,32.641 -2020-01-09 13:00:00,125.21,204.40099999999998,58.794,32.641 -2020-01-09 13:15:00,125.26,203.859,58.794,32.641 -2020-01-09 13:30:00,120.6,203.476,58.794,32.641 -2020-01-09 13:45:00,123.97,203.37599999999998,58.794,32.641 -2020-01-09 14:00:00,127.41,202.38099999999997,60.32,32.641 -2020-01-09 14:15:00,128.59,202.979,60.32,32.641 -2020-01-09 14:30:00,127.7,203.919,60.32,32.641 -2020-01-09 14:45:00,128.7,204.245,60.32,32.641 -2020-01-09 15:00:00,129.88,205.519,62.52,32.641 -2020-01-09 15:15:00,129.65,206.03,62.52,32.641 -2020-01-09 15:30:00,128.81,208.423,62.52,32.641 -2020-01-09 15:45:00,129.3,210.108,62.52,32.641 -2020-01-09 16:00:00,130.58,210.90200000000002,64.199,32.641 -2020-01-09 16:15:00,132.89,212.55900000000003,64.199,32.641 -2020-01-09 16:30:00,135.51,215.477,64.199,32.641 -2020-01-09 16:45:00,138.94,217.085,64.199,32.641 -2020-01-09 17:00:00,140.54,219.38,68.19800000000001,32.641 -2020-01-09 17:15:00,141.52,220.227,68.19800000000001,32.641 -2020-01-09 17:30:00,142.06,221.187,68.19800000000001,32.641 -2020-01-09 17:45:00,142.3,221.053,68.19800000000001,32.641 -2020-01-09 18:00:00,140.27,222.622,67.899,32.641 -2020-01-09 18:15:00,139.4,219.75400000000002,67.899,32.641 -2020-01-09 18:30:00,137.51,218.52,67.899,32.641 -2020-01-09 18:45:00,137.88,218.67,67.899,32.641 -2020-01-09 19:00:00,135.66,218.199,64.72399999999999,32.641 -2020-01-09 19:15:00,133.63,214.226,64.72399999999999,32.641 -2020-01-09 19:30:00,136.63,211.551,64.72399999999999,32.641 -2020-01-09 19:45:00,136.33,208.028,64.72399999999999,32.641 -2020-01-09 20:00:00,130.0,204.149,64.062,32.641 -2020-01-09 20:15:00,120.43,197.695,64.062,32.641 -2020-01-09 20:30:00,115.58,193.532,64.062,32.641 -2020-01-09 20:45:00,114.76,191.713,64.062,32.641 -2020-01-09 21:00:00,111.28,188.68,57.971000000000004,32.641 -2020-01-09 21:15:00,114.3,186.046,57.971000000000004,32.641 -2020-01-09 21:30:00,112.74,183.96200000000002,57.971000000000004,32.641 -2020-01-09 21:45:00,107.12,182.422,57.971000000000004,32.641 -2020-01-09 22:00:00,103.37,175.37599999999998,53.715,32.641 -2020-01-09 22:15:00,97.81,169.60299999999998,53.715,32.641 -2020-01-09 22:30:00,96.27,155.78,53.715,32.641 -2020-01-09 22:45:00,99.06,147.52700000000002,53.715,32.641 -2020-01-09 23:00:00,96.54,140.78799999999998,47.8,32.641 -2020-01-09 23:15:00,90.59,139.037,47.8,32.641 -2020-01-09 23:30:00,83.45,139.326,47.8,32.641 -2020-01-09 23:45:00,85.64,138.773,47.8,32.641 -2020-01-10 00:00:00,84.68,131.172,43.656000000000006,32.641 -2020-01-10 00:15:00,87.29,130.907,43.656000000000006,32.641 -2020-01-10 00:30:00,86.37,131.95600000000002,43.656000000000006,32.641 -2020-01-10 00:45:00,81.92,133.513,43.656000000000006,32.641 -2020-01-10 01:00:00,80.49,136.08100000000002,41.263000000000005,32.641 -2020-01-10 01:15:00,84.15,137.489,41.263000000000005,32.641 -2020-01-10 01:30:00,82.86,137.643,41.263000000000005,32.641 -2020-01-10 01:45:00,80.26,138.219,41.263000000000005,32.641 -2020-01-10 02:00:00,82.41,140.32299999999998,40.799,32.641 -2020-01-10 02:15:00,82.9,142.0,40.799,32.641 -2020-01-10 02:30:00,82.52,143.188,40.799,32.641 -2020-01-10 02:45:00,78.57,145.32399999999998,40.799,32.641 -2020-01-10 03:00:00,82.79,146.91,41.398,32.641 -2020-01-10 03:15:00,83.34,149.214,41.398,32.641 -2020-01-10 03:30:00,81.33,151.107,41.398,32.641 -2020-01-10 03:45:00,83.38,152.868,41.398,32.641 -2020-01-10 04:00:00,84.86,165.15900000000002,42.38,32.641 -2020-01-10 04:15:00,82.09,176.97400000000002,42.38,32.641 -2020-01-10 04:30:00,80.74,180.197,42.38,32.641 -2020-01-10 04:45:00,81.91,181.887,42.38,32.641 -2020-01-10 05:00:00,85.77,215.43599999999998,46.181000000000004,32.641 -2020-01-10 05:15:00,88.51,245.451,46.181000000000004,32.641 -2020-01-10 05:30:00,92.4,242.31599999999997,46.181000000000004,32.641 -2020-01-10 05:45:00,97.86,235.141,46.181000000000004,32.641 -2020-01-10 06:00:00,105.68,232.324,59.33,32.641 -2020-01-10 06:15:00,110.88,236.578,59.33,32.641 -2020-01-10 06:30:00,114.95,238.657,59.33,32.641 -2020-01-10 06:45:00,120.02,244.748,59.33,32.641 -2020-01-10 07:00:00,125.32,242.745,72.454,32.641 -2020-01-10 07:15:00,129.26,249.206,72.454,32.641 -2020-01-10 07:30:00,133.08,252.229,72.454,32.641 -2020-01-10 07:45:00,137.34,252.824,72.454,32.641 -2020-01-10 08:00:00,138.92,250.329,67.175,32.641 -2020-01-10 08:15:00,139.19,249.984,67.175,32.641 -2020-01-10 08:30:00,142.05,248.93900000000002,67.175,32.641 -2020-01-10 08:45:00,139.98,244.104,67.175,32.641 -2020-01-10 09:00:00,139.84,237.938,65.365,32.641 -2020-01-10 09:15:00,142.47,235.299,65.365,32.641 -2020-01-10 09:30:00,143.87,232.75599999999997,65.365,32.641 -2020-01-10 09:45:00,144.34,229.4,65.365,32.641 -2020-01-10 10:00:00,143.86,222.885,63.95,32.641 -2020-01-10 10:15:00,145.24,219.442,63.95,32.641 -2020-01-10 10:30:00,146.08,215.968,63.95,32.641 -2020-01-10 10:45:00,145.52,213.80700000000002,63.95,32.641 -2020-01-10 11:00:00,143.53,211.615,63.92100000000001,32.641 -2020-01-10 11:15:00,144.41,209.428,63.92100000000001,32.641 -2020-01-10 11:30:00,142.84,209.82,63.92100000000001,32.641 -2020-01-10 11:45:00,140.57,208.75099999999998,63.92100000000001,32.641 -2020-01-10 12:00:00,139.98,204.835,60.79600000000001,32.641 -2020-01-10 12:15:00,139.61,202.088,60.79600000000001,32.641 -2020-01-10 12:30:00,138.38,202.197,60.79600000000001,32.641 -2020-01-10 12:45:00,137.77,203.66400000000002,60.79600000000001,32.641 -2020-01-10 13:00:00,135.76,202.949,59.393,32.641 -2020-01-10 13:15:00,137.41,203.271,59.393,32.641 -2020-01-10 13:30:00,133.35,202.84099999999998,59.393,32.641 -2020-01-10 13:45:00,131.39,202.644,59.393,32.641 -2020-01-10 14:00:00,131.97,200.481,57.943999999999996,32.641 -2020-01-10 14:15:00,132.24,200.84900000000002,57.943999999999996,32.641 -2020-01-10 14:30:00,130.8,202.25,57.943999999999996,32.641 -2020-01-10 14:45:00,131.39,202.96599999999998,57.943999999999996,32.641 -2020-01-10 15:00:00,131.3,203.739,60.153999999999996,32.641 -2020-01-10 15:15:00,130.95,203.773,60.153999999999996,32.641 -2020-01-10 15:30:00,130.15,204.55,60.153999999999996,32.641 -2020-01-10 15:45:00,129.69,206.333,60.153999999999996,32.641 -2020-01-10 16:00:00,130.75,205.926,62.933,32.641 -2020-01-10 16:15:00,132.79,207.87400000000002,62.933,32.641 -2020-01-10 16:30:00,135.68,210.912,62.933,32.641 -2020-01-10 16:45:00,137.72,212.451,62.933,32.641 -2020-01-10 17:00:00,139.5,214.852,68.657,32.641 -2020-01-10 17:15:00,139.88,215.297,68.657,32.641 -2020-01-10 17:30:00,142.57,215.93400000000003,68.657,32.641 -2020-01-10 17:45:00,139.22,215.576,68.657,32.641 -2020-01-10 18:00:00,136.73,217.919,67.111,32.641 -2020-01-10 18:15:00,136.11,214.695,67.111,32.641 -2020-01-10 18:30:00,134.73,213.893,67.111,32.641 -2020-01-10 18:45:00,135.6,214.03400000000002,67.111,32.641 -2020-01-10 19:00:00,132.42,214.46200000000002,62.434,32.641 -2020-01-10 19:15:00,130.68,211.921,62.434,32.641 -2020-01-10 19:30:00,135.53,208.804,62.434,32.641 -2020-01-10 19:45:00,135.12,204.845,62.434,32.641 -2020-01-10 20:00:00,127.21,201.00900000000001,61.763000000000005,32.641 -2020-01-10 20:15:00,118.7,194.521,61.763000000000005,32.641 -2020-01-10 20:30:00,112.79,190.331,61.763000000000005,32.641 -2020-01-10 20:45:00,111.77,189.175,61.763000000000005,32.641 -2020-01-10 21:00:00,108.32,186.59400000000002,56.785,32.641 -2020-01-10 21:15:00,110.88,184.317,56.785,32.641 -2020-01-10 21:30:00,102.73,182.29,56.785,32.641 -2020-01-10 21:45:00,100.86,181.33900000000003,56.785,32.641 -2020-01-10 22:00:00,96.64,175.354,52.693000000000005,32.641 -2020-01-10 22:15:00,93.07,169.468,52.693000000000005,32.641 -2020-01-10 22:30:00,92.88,162.24,52.693000000000005,32.641 -2020-01-10 22:45:00,93.82,157.739,52.693000000000005,32.641 -2020-01-10 23:00:00,88.99,150.405,45.443999999999996,32.641 -2020-01-10 23:15:00,89.2,146.657,45.443999999999996,32.641 -2020-01-10 23:30:00,85.72,145.495,45.443999999999996,32.641 -2020-01-10 23:45:00,80.79,144.237,45.443999999999996,32.641 -2020-01-11 00:00:00,75.56,128.079,44.738,32.459 -2020-01-11 00:15:00,80.09,123.209,44.738,32.459 -2020-01-11 00:30:00,80.07,125.723,44.738,32.459 -2020-01-11 00:45:00,77.68,128.094,44.738,32.459 -2020-01-11 01:00:00,71.96,131.326,40.303000000000004,32.459 -2020-01-11 01:15:00,70.77,131.606,40.303000000000004,32.459 -2020-01-11 01:30:00,68.37,131.253,40.303000000000004,32.459 -2020-01-11 01:45:00,68.2,131.491,40.303000000000004,32.459 -2020-01-11 02:00:00,69.18,134.41899999999998,38.61,32.459 -2020-01-11 02:15:00,74.74,135.753,38.61,32.459 -2020-01-11 02:30:00,74.65,135.797,38.61,32.459 -2020-01-11 02:45:00,74.36,137.984,38.61,32.459 -2020-01-11 03:00:00,67.49,140.34799999999998,37.554,32.459 -2020-01-11 03:15:00,72.14,141.393,37.554,32.459 -2020-01-11 03:30:00,74.34,141.52200000000002,37.554,32.459 -2020-01-11 03:45:00,74.02,143.29399999999998,37.554,32.459 -2020-01-11 04:00:00,69.83,151.159,37.176,32.459 -2020-01-11 04:15:00,66.48,160.25,37.176,32.459 -2020-01-11 04:30:00,66.65,161.23,37.176,32.459 -2020-01-11 04:45:00,68.43,162.349,37.176,32.459 -2020-01-11 05:00:00,69.3,178.98,36.893,32.459 -2020-01-11 05:15:00,68.52,189.00599999999997,36.893,32.459 -2020-01-11 05:30:00,68.35,186.139,36.893,32.459 -2020-01-11 05:45:00,69.45,184.59400000000002,36.893,32.459 -2020-01-11 06:00:00,71.01,201.507,37.803000000000004,32.459 -2020-01-11 06:15:00,71.8,223.074,37.803000000000004,32.459 -2020-01-11 06:30:00,72.53,219.50099999999998,37.803000000000004,32.459 -2020-01-11 06:45:00,74.42,216.02700000000002,37.803000000000004,32.459 -2020-01-11 07:00:00,77.16,210.03799999999998,41.086999999999996,32.459 -2020-01-11 07:15:00,79.89,215.21599999999998,41.086999999999996,32.459 -2020-01-11 07:30:00,82.88,221.06599999999997,41.086999999999996,32.459 -2020-01-11 07:45:00,87.25,225.924,41.086999999999996,32.459 -2020-01-11 08:00:00,89.61,227.84799999999998,48.222,32.459 -2020-01-11 08:15:00,91.87,231.47099999999998,48.222,32.459 -2020-01-11 08:30:00,94.54,232.15900000000002,48.222,32.459 -2020-01-11 08:45:00,97.83,230.68099999999998,48.222,32.459 -2020-01-11 09:00:00,99.9,226.22400000000002,52.791000000000004,32.459 -2020-01-11 09:15:00,102.11,224.375,52.791000000000004,32.459 -2020-01-11 09:30:00,103.03,222.796,52.791000000000004,32.459 -2020-01-11 09:45:00,104.11,219.641,52.791000000000004,32.459 -2020-01-11 10:00:00,105.67,213.37900000000002,54.341,32.459 -2020-01-11 10:15:00,106.52,210.09400000000002,54.341,32.459 -2020-01-11 10:30:00,107.3,206.815,54.341,32.459 -2020-01-11 10:45:00,107.94,206.1,54.341,32.459 -2020-01-11 11:00:00,109.65,204.15400000000002,51.94,32.459 -2020-01-11 11:15:00,111.25,201.162,51.94,32.459 -2020-01-11 11:30:00,111.53,200.33,51.94,32.459 -2020-01-11 11:45:00,111.53,198.183,51.94,32.459 -2020-01-11 12:00:00,110.54,193.285,50.973,32.459 -2020-01-11 12:15:00,109.01,191.19299999999998,50.973,32.459 -2020-01-11 12:30:00,105.95,191.648,50.973,32.459 -2020-01-11 12:45:00,104.43,192.233,50.973,32.459 -2020-01-11 13:00:00,101.49,191.13,48.06399999999999,32.459 -2020-01-11 13:15:00,99.78,189.25,48.06399999999999,32.459 -2020-01-11 13:30:00,98.12,188.30700000000002,48.06399999999999,32.459 -2020-01-11 13:45:00,96.61,188.71200000000002,48.06399999999999,32.459 -2020-01-11 14:00:00,95.14,187.91299999999998,45.707,32.459 -2020-01-11 14:15:00,94.37,187.77599999999998,45.707,32.459 -2020-01-11 14:30:00,94.65,187.243,45.707,32.459 -2020-01-11 14:45:00,94.99,188.178,45.707,32.459 -2020-01-11 15:00:00,94.68,189.683,47.567,32.459 -2020-01-11 15:15:00,94.55,190.521,47.567,32.459 -2020-01-11 15:30:00,93.53,192.94,47.567,32.459 -2020-01-11 15:45:00,96.02,194.787,47.567,32.459 -2020-01-11 16:00:00,97.55,192.99599999999998,52.031000000000006,32.459 -2020-01-11 16:15:00,98.75,195.96900000000002,52.031000000000006,32.459 -2020-01-11 16:30:00,102.68,198.946,52.031000000000006,32.459 -2020-01-11 16:45:00,104.37,201.447,52.031000000000006,32.459 -2020-01-11 17:00:00,106.29,203.333,58.218999999999994,32.459 -2020-01-11 17:15:00,107.17,205.708,58.218999999999994,32.459 -2020-01-11 17:30:00,108.36,206.278,58.218999999999994,32.459 -2020-01-11 17:45:00,109.4,205.47799999999998,58.218999999999994,32.459 -2020-01-11 18:00:00,108.57,207.27200000000002,57.65,32.459 -2020-01-11 18:15:00,108.91,205.892,57.65,32.459 -2020-01-11 18:30:00,108.15,206.44799999999998,57.65,32.459 -2020-01-11 18:45:00,107.83,203.21400000000003,57.65,32.459 -2020-01-11 19:00:00,105.71,204.699,51.261,32.459 -2020-01-11 19:15:00,103.93,201.683,51.261,32.459 -2020-01-11 19:30:00,102.6,199.333,51.261,32.459 -2020-01-11 19:45:00,101.86,195.09900000000002,51.261,32.459 -2020-01-11 20:00:00,97.56,193.503,44.068000000000005,32.459 -2020-01-11 20:15:00,92.87,189.32299999999998,44.068000000000005,32.459 -2020-01-11 20:30:00,90.31,184.799,44.068000000000005,32.459 -2020-01-11 20:45:00,88.48,183.128,44.068000000000005,32.459 -2020-01-11 21:00:00,86.43,183.017,38.861,32.459 -2020-01-11 21:15:00,84.3,181.21200000000002,38.861,32.459 -2020-01-11 21:30:00,83.24,180.489,38.861,32.459 -2020-01-11 21:45:00,82.41,179.149,38.861,32.459 -2020-01-11 22:00:00,79.49,174.601,39.485,32.459 -2020-01-11 22:15:00,77.77,171.377,39.485,32.459 -2020-01-11 22:30:00,76.13,170.824,39.485,32.459 -2020-01-11 22:45:00,75.32,168.31,39.485,32.459 -2020-01-11 23:00:00,72.71,163.583,32.027,32.459 -2020-01-11 23:15:00,71.28,158.07399999999998,32.027,32.459 -2020-01-11 23:30:00,69.11,154.937,32.027,32.459 -2020-01-11 23:45:00,66.62,151.07,32.027,32.459 -2020-01-12 00:00:00,64.99,128.533,26.96,32.459 -2020-01-12 00:15:00,63.46,123.37200000000001,26.96,32.459 -2020-01-12 00:30:00,61.72,125.475,26.96,32.459 -2020-01-12 00:45:00,60.63,128.591,26.96,32.459 -2020-01-12 01:00:00,59.84,131.655,24.295,32.459 -2020-01-12 01:15:00,59.13,133.06,24.295,32.459 -2020-01-12 01:30:00,58.42,133.279,24.295,32.459 -2020-01-12 01:45:00,57.74,133.195,24.295,32.459 -2020-01-12 02:00:00,57.23,135.319,24.268,32.459 -2020-01-12 02:15:00,56.86,135.67700000000002,24.268,32.459 -2020-01-12 02:30:00,56.65,136.64600000000002,24.268,32.459 -2020-01-12 02:45:00,56.47,139.35399999999998,24.268,32.459 -2020-01-12 03:00:00,56.4,142.011,23.373,32.459 -2020-01-12 03:15:00,56.47,142.487,23.373,32.459 -2020-01-12 03:30:00,56.48,144.191,23.373,32.459 -2020-01-12 03:45:00,56.69,145.944,23.373,32.459 -2020-01-12 04:00:00,57.05,153.52700000000002,23.874000000000002,32.459 -2020-01-12 04:15:00,56.86,161.537,23.874000000000002,32.459 -2020-01-12 04:30:00,57.57,162.489,23.874000000000002,32.459 -2020-01-12 04:45:00,58.1,163.93400000000003,23.874000000000002,32.459 -2020-01-12 05:00:00,58.61,176.683,24.871,32.459 -2020-01-12 05:15:00,59.09,184.1,24.871,32.459 -2020-01-12 05:30:00,58.93,181.088,24.871,32.459 -2020-01-12 05:45:00,59.64,179.84799999999998,24.871,32.459 -2020-01-12 06:00:00,60.42,196.84599999999998,23.84,32.459 -2020-01-12 06:15:00,60.79,216.484,23.84,32.459 -2020-01-12 06:30:00,60.98,211.77,23.84,32.459 -2020-01-12 06:45:00,62.52,207.247,23.84,32.459 -2020-01-12 07:00:00,64.57,203.924,27.430999999999997,32.459 -2020-01-12 07:15:00,66.17,208.28099999999998,27.430999999999997,32.459 -2020-01-12 07:30:00,67.93,212.678,27.430999999999997,32.459 -2020-01-12 07:45:00,69.79,216.687,27.430999999999997,32.459 -2020-01-12 08:00:00,71.93,220.56599999999997,33.891999999999996,32.459 -2020-01-12 08:15:00,74.46,223.995,33.891999999999996,32.459 -2020-01-12 08:30:00,76.78,226.389,33.891999999999996,32.459 -2020-01-12 08:45:00,78.97,227.08900000000003,33.891999999999996,32.459 -2020-01-12 09:00:00,80.51,222.19299999999998,37.571,32.459 -2020-01-12 09:15:00,81.13,220.979,37.571,32.459 -2020-01-12 09:30:00,81.84,219.22099999999998,37.571,32.459 -2020-01-12 09:45:00,82.37,215.87099999999998,37.571,32.459 -2020-01-12 10:00:00,81.27,212.296,40.594,32.459 -2020-01-12 10:15:00,79.61,209.584,40.594,32.459 -2020-01-12 10:30:00,79.77,206.93400000000003,40.594,32.459 -2020-01-12 10:45:00,85.58,204.109,40.594,32.459 -2020-01-12 11:00:00,86.51,203.16299999999998,44.133,32.459 -2020-01-12 11:15:00,88.46,200.34099999999998,44.133,32.459 -2020-01-12 11:30:00,91.87,198.543,44.133,32.459 -2020-01-12 11:45:00,93.46,197.037,44.133,32.459 -2020-01-12 12:00:00,91.63,191.477,41.198,32.459 -2020-01-12 12:15:00,90.3,191.49099999999999,41.198,32.459 -2020-01-12 12:30:00,88.06,190.357,41.198,32.459 -2020-01-12 12:45:00,86.68,189.94,41.198,32.459 -2020-01-12 13:00:00,85.19,188.076,37.014,32.459 -2020-01-12 13:15:00,83.88,189.463,37.014,32.459 -2020-01-12 13:30:00,82.6,188.347,37.014,32.459 -2020-01-12 13:45:00,82.08,187.97099999999998,37.014,32.459 -2020-01-12 14:00:00,82.67,187.389,34.934,32.459 -2020-01-12 14:15:00,82.79,188.51,34.934,32.459 -2020-01-12 14:30:00,82.64,189.378,34.934,32.459 -2020-01-12 14:45:00,85.76,189.947,34.934,32.459 -2020-01-12 15:00:00,83.46,189.813,34.588,32.459 -2020-01-12 15:15:00,83.64,191.487,34.588,32.459 -2020-01-12 15:30:00,84.7,194.537,34.588,32.459 -2020-01-12 15:45:00,85.25,197.101,34.588,32.459 -2020-01-12 16:00:00,86.79,197.385,37.874,32.459 -2020-01-12 16:15:00,88.0,199.38,37.874,32.459 -2020-01-12 16:30:00,92.21,202.588,37.874,32.459 -2020-01-12 16:45:00,94.95,205.236,37.874,32.459 -2020-01-12 17:00:00,97.3,207.06599999999997,47.303999999999995,32.459 -2020-01-12 17:15:00,99.15,209.045,47.303999999999995,32.459 -2020-01-12 17:30:00,100.86,209.912,47.303999999999995,32.459 -2020-01-12 17:45:00,102.12,211.532,47.303999999999995,32.459 -2020-01-12 18:00:00,102.63,212.722,48.879,32.459 -2020-01-12 18:15:00,102.78,212.78599999999997,48.879,32.459 -2020-01-12 18:30:00,101.93,211.138,48.879,32.459 -2020-01-12 18:45:00,100.6,209.889,48.879,32.459 -2020-01-12 19:00:00,99.22,210.878,44.826,32.459 -2020-01-12 19:15:00,96.93,208.544,44.826,32.459 -2020-01-12 19:30:00,98.09,206.03599999999997,44.826,32.459 -2020-01-12 19:45:00,94.04,203.36,44.826,32.459 -2020-01-12 20:00:00,92.48,201.725,40.154,32.459 -2020-01-12 20:15:00,90.52,198.604,40.154,32.459 -2020-01-12 20:30:00,88.61,195.35299999999998,40.154,32.459 -2020-01-12 20:45:00,86.91,192.50099999999998,40.154,32.459 -2020-01-12 21:00:00,86.27,189.613,36.549,32.459 -2020-01-12 21:15:00,85.53,187.137,36.549,32.459 -2020-01-12 21:30:00,85.67,186.76,36.549,32.459 -2020-01-12 21:45:00,86.26,185.56099999999998,36.549,32.459 -2020-01-12 22:00:00,84.68,179.642,37.663000000000004,32.459 -2020-01-12 22:15:00,84.59,175.704,37.663000000000004,32.459 -2020-01-12 22:30:00,81.91,171.81099999999998,37.663000000000004,32.459 -2020-01-12 22:45:00,80.26,168.46,37.663000000000004,32.459 -2020-01-12 23:00:00,77.38,160.747,31.945,32.459 -2020-01-12 23:15:00,76.18,157.165,31.945,32.459 -2020-01-12 23:30:00,75.03,154.933,31.945,32.459 -2020-01-12 23:45:00,75.75,152.011,31.945,32.459 -2020-01-13 00:00:00,70.95,133.061,31.533,32.641 -2020-01-13 00:15:00,70.9,131.047,31.533,32.641 -2020-01-13 00:30:00,70.47,133.298,31.533,32.641 -2020-01-13 00:45:00,69.99,135.843,31.533,32.641 -2020-01-13 01:00:00,67.64,138.91,30.56,32.641 -2020-01-13 01:15:00,66.93,139.731,30.56,32.641 -2020-01-13 01:30:00,67.33,139.983,30.56,32.641 -2020-01-13 01:45:00,67.11,140.019,30.56,32.641 -2020-01-13 02:00:00,66.76,142.105,29.55,32.641 -2020-01-13 02:15:00,67.45,144.125,29.55,32.641 -2020-01-13 02:30:00,67.68,145.453,29.55,32.641 -2020-01-13 02:45:00,67.43,147.497,29.55,32.641 -2020-01-13 03:00:00,67.66,151.507,27.059,32.641 -2020-01-13 03:15:00,68.77,153.741,27.059,32.641 -2020-01-13 03:30:00,68.92,155.1,27.059,32.641 -2020-01-13 03:45:00,68.84,156.29399999999998,27.059,32.641 -2020-01-13 04:00:00,70.83,168.38299999999998,28.384,32.641 -2020-01-13 04:15:00,73.73,180.68599999999998,28.384,32.641 -2020-01-13 04:30:00,73.75,184.06799999999998,28.384,32.641 -2020-01-13 04:45:00,77.08,185.655,28.384,32.641 -2020-01-13 05:00:00,81.53,215.06400000000002,35.915,32.641 -2020-01-13 05:15:00,84.06,243.69099999999997,35.915,32.641 -2020-01-13 05:30:00,88.5,241.104,35.915,32.641 -2020-01-13 05:45:00,94.62,233.94400000000002,35.915,32.641 -2020-01-13 06:00:00,104.44,232.146,56.18,32.641 -2020-01-13 06:15:00,112.23,236.292,56.18,32.641 -2020-01-13 06:30:00,114.44,240.037,56.18,32.641 -2020-01-13 06:45:00,119.14,244.743,56.18,32.641 -2020-01-13 07:00:00,126.22,243.983,70.877,32.641 -2020-01-13 07:15:00,129.2,249.511,70.877,32.641 -2020-01-13 07:30:00,131.57,253.109,70.877,32.641 -2020-01-13 07:45:00,131.68,254.268,70.877,32.641 -2020-01-13 08:00:00,135.22,252.912,65.65,32.641 -2020-01-13 08:15:00,134.08,254.079,65.65,32.641 -2020-01-13 08:30:00,135.2,252.06799999999998,65.65,32.641 -2020-01-13 08:45:00,132.28,249.106,65.65,32.641 -2020-01-13 09:00:00,133.04,243.158,62.037,32.641 -2020-01-13 09:15:00,133.98,238.296,62.037,32.641 -2020-01-13 09:30:00,132.22,235.535,62.037,32.641 -2020-01-13 09:45:00,130.3,232.66299999999998,62.037,32.641 -2020-01-13 10:00:00,129.71,227.908,60.409,32.641 -2020-01-13 10:15:00,129.27,224.858,60.409,32.641 -2020-01-13 10:30:00,127.57,221.285,60.409,32.641 -2020-01-13 10:45:00,130.5,219.388,60.409,32.641 -2020-01-13 11:00:00,125.92,215.54,60.211999999999996,32.641 -2020-01-13 11:15:00,126.17,214.65,60.211999999999996,32.641 -2020-01-13 11:30:00,125.52,214.301,60.211999999999996,32.641 -2020-01-13 11:45:00,127.51,212.32299999999998,60.211999999999996,32.641 -2020-01-13 12:00:00,124.16,208.769,57.733000000000004,32.641 -2020-01-13 12:15:00,123.73,208.793,57.733000000000004,32.641 -2020-01-13 12:30:00,123.65,207.995,57.733000000000004,32.641 -2020-01-13 12:45:00,119.92,209.227,57.733000000000004,32.641 -2020-01-13 13:00:00,118.02,207.935,58.695,32.641 -2020-01-13 13:15:00,118.69,207.868,58.695,32.641 -2020-01-13 13:30:00,116.48,206.155,58.695,32.641 -2020-01-13 13:45:00,117.81,205.75,58.695,32.641 -2020-01-13 14:00:00,124.13,204.56,59.505,32.641 -2020-01-13 14:15:00,122.13,204.937,59.505,32.641 -2020-01-13 14:30:00,125.07,205.237,59.505,32.641 -2020-01-13 14:45:00,121.35,205.65,59.505,32.641 -2020-01-13 15:00:00,123.98,207.451,59.946000000000005,32.641 -2020-01-13 15:15:00,125.58,207.59400000000002,59.946000000000005,32.641 -2020-01-13 15:30:00,124.7,209.69,59.946000000000005,32.641 -2020-01-13 15:45:00,124.42,211.794,59.946000000000005,32.641 -2020-01-13 16:00:00,126.95,212.16400000000002,61.766999999999996,32.641 -2020-01-13 16:15:00,128.1,213.343,61.766999999999996,32.641 -2020-01-13 16:30:00,133.84,215.56,61.766999999999996,32.641 -2020-01-13 16:45:00,134.97,216.93400000000003,61.766999999999996,32.641 -2020-01-13 17:00:00,137.72,218.595,67.85600000000001,32.641 -2020-01-13 17:15:00,139.22,219.562,67.85600000000001,32.641 -2020-01-13 17:30:00,140.31,219.893,67.85600000000001,32.641 -2020-01-13 17:45:00,138.79,219.953,67.85600000000001,32.641 -2020-01-13 18:00:00,138.41,221.658,64.564,32.641 -2020-01-13 18:15:00,136.09,219.475,64.564,32.641 -2020-01-13 18:30:00,135.55,218.57299999999998,64.564,32.641 -2020-01-13 18:45:00,135.23,217.93200000000002,64.564,32.641 -2020-01-13 19:00:00,132.57,217.167,58.536,32.641 -2020-01-13 19:15:00,131.58,213.516,58.536,32.641 -2020-01-13 19:30:00,129.12,211.56400000000002,58.536,32.641 -2020-01-13 19:45:00,134.32,208.04,58.536,32.641 -2020-01-13 20:00:00,128.64,203.938,59.888999999999996,32.641 -2020-01-13 20:15:00,125.25,198.085,59.888999999999996,32.641 -2020-01-13 20:30:00,116.68,192.804,59.888999999999996,32.641 -2020-01-13 20:45:00,117.43,191.696,59.888999999999996,32.641 -2020-01-13 21:00:00,109.83,189.396,52.652,32.641 -2020-01-13 21:15:00,114.34,185.62,52.652,32.641 -2020-01-13 21:30:00,113.42,184.334,52.652,32.641 -2020-01-13 21:45:00,107.41,182.62400000000002,52.652,32.641 -2020-01-13 22:00:00,100.72,173.732,46.17,32.641 -2020-01-13 22:15:00,98.52,168.285,46.17,32.641 -2020-01-13 22:30:00,100.74,154.417,46.17,32.641 -2020-01-13 22:45:00,100.33,145.899,46.17,32.641 -2020-01-13 23:00:00,94.57,139.002,36.281,32.641 -2020-01-13 23:15:00,89.14,138.363,36.281,32.641 -2020-01-13 23:30:00,91.26,139.106,36.281,32.641 -2020-01-13 23:45:00,91.53,139.046,36.281,32.641 -2020-01-14 00:00:00,87.82,132.558,38.821999999999996,32.641 -2020-01-14 00:15:00,81.82,132.01,38.821999999999996,32.641 -2020-01-14 00:30:00,84.32,133.167,38.821999999999996,32.641 -2020-01-14 00:45:00,87.32,134.571,38.821999999999996,32.641 -2020-01-14 01:00:00,84.42,137.464,36.936,32.641 -2020-01-14 01:15:00,79.89,137.782,36.936,32.641 -2020-01-14 01:30:00,77.82,138.22899999999998,36.936,32.641 -2020-01-14 01:45:00,79.93,138.631,36.936,32.641 -2020-01-14 02:00:00,82.47,140.761,34.42,32.641 -2020-01-14 02:15:00,81.18,142.563,34.42,32.641 -2020-01-14 02:30:00,78.27,143.282,34.42,32.641 -2020-01-14 02:45:00,83.03,145.312,34.42,32.641 -2020-01-14 03:00:00,82.63,148.03799999999998,33.585,32.641 -2020-01-14 03:15:00,80.68,149.27700000000002,33.585,32.641 -2020-01-14 03:30:00,74.84,151.167,33.585,32.641 -2020-01-14 03:45:00,75.84,152.67600000000002,33.585,32.641 -2020-01-14 04:00:00,76.53,164.63299999999998,35.622,32.641 -2020-01-14 04:15:00,77.34,176.54,35.622,32.641 -2020-01-14 04:30:00,78.06,179.59400000000002,35.622,32.641 -2020-01-14 04:45:00,80.59,182.463,35.622,32.641 -2020-01-14 05:00:00,83.73,217.205,40.599000000000004,32.641 -2020-01-14 05:15:00,86.8,245.519,40.599000000000004,32.641 -2020-01-14 05:30:00,90.08,241.22400000000002,40.599000000000004,32.641 -2020-01-14 05:45:00,95.43,234.15900000000002,40.599000000000004,32.641 -2020-01-14 06:00:00,104.29,230.98,55.203,32.641 -2020-01-14 06:15:00,109.76,236.90900000000002,55.203,32.641 -2020-01-14 06:30:00,114.01,239.98,55.203,32.641 -2020-01-14 06:45:00,118.59,244.38,55.203,32.641 -2020-01-14 07:00:00,125.25,243.42,69.029,32.641 -2020-01-14 07:15:00,128.89,248.783,69.029,32.641 -2020-01-14 07:30:00,129.58,251.75599999999997,69.029,32.641 -2020-01-14 07:45:00,135.04,253.217,69.029,32.641 -2020-01-14 08:00:00,137.03,251.979,65.85300000000001,32.641 -2020-01-14 08:15:00,136.03,252.045,65.85300000000001,32.641 -2020-01-14 08:30:00,135.16,249.79,65.85300000000001,32.641 -2020-01-14 08:45:00,132.47,246.59900000000002,65.85300000000001,32.641 -2020-01-14 09:00:00,135.18,239.695,61.566,32.641 -2020-01-14 09:15:00,137.97,236.602,61.566,32.641 -2020-01-14 09:30:00,137.36,234.543,61.566,32.641 -2020-01-14 09:45:00,138.26,231.315,61.566,32.641 -2020-01-14 10:00:00,140.95,226.03599999999997,61.244,32.641 -2020-01-14 10:15:00,144.05,221.83900000000003,61.244,32.641 -2020-01-14 10:30:00,146.07,218.455,61.244,32.641 -2020-01-14 10:45:00,144.01,216.787,61.244,32.641 -2020-01-14 11:00:00,132.46,214.55200000000002,61.16,32.641 -2020-01-14 11:15:00,135.5,213.266,61.16,32.641 -2020-01-14 11:30:00,136.58,211.74099999999999,61.16,32.641 -2020-01-14 11:45:00,138.0,210.571,61.16,32.641 -2020-01-14 12:00:00,137.63,205.547,59.09,32.641 -2020-01-14 12:15:00,135.35,205.107,59.09,32.641 -2020-01-14 12:30:00,134.93,205.018,59.09,32.641 -2020-01-14 12:45:00,134.94,205.87400000000002,59.09,32.641 -2020-01-14 13:00:00,133.76,204.162,60.21,32.641 -2020-01-14 13:15:00,131.09,203.554,60.21,32.641 -2020-01-14 13:30:00,129.61,203.111,60.21,32.641 -2020-01-14 13:45:00,134.96,202.985,60.21,32.641 -2020-01-14 14:00:00,133.08,202.09599999999998,60.673,32.641 -2020-01-14 14:15:00,132.64,202.65099999999998,60.673,32.641 -2020-01-14 14:30:00,131.69,203.612,60.673,32.641 -2020-01-14 14:45:00,133.24,204.018,60.673,32.641 -2020-01-14 15:00:00,134.24,205.382,62.232,32.641 -2020-01-14 15:15:00,133.79,205.793,62.232,32.641 -2020-01-14 15:30:00,132.79,208.138,62.232,32.641 -2020-01-14 15:45:00,132.99,209.766,62.232,32.641 -2020-01-14 16:00:00,134.62,210.56599999999997,63.611999999999995,32.641 -2020-01-14 16:15:00,133.49,212.261,63.611999999999995,32.641 -2020-01-14 16:30:00,136.5,215.21200000000002,63.611999999999995,32.641 -2020-01-14 16:45:00,140.39,216.851,63.611999999999995,32.641 -2020-01-14 17:00:00,141.25,219.071,70.658,32.641 -2020-01-14 17:15:00,141.82,220.072,70.658,32.641 -2020-01-14 17:30:00,142.91,221.18200000000002,70.658,32.641 -2020-01-14 17:45:00,141.94,221.146,70.658,32.641 -2020-01-14 18:00:00,140.95,222.817,68.361,32.641 -2020-01-14 18:15:00,139.33,220.011,68.361,32.641 -2020-01-14 18:30:00,138.51,218.8,68.361,32.641 -2020-01-14 18:45:00,138.78,219.049,68.361,32.641 -2020-01-14 19:00:00,134.6,218.407,62.922,32.641 -2020-01-14 19:15:00,133.06,214.447,62.922,32.641 -2020-01-14 19:30:00,130.38,211.797,62.922,32.641 -2020-01-14 19:45:00,135.52,208.298,62.922,32.641 -2020-01-14 20:00:00,131.04,204.324,63.251999999999995,32.641 -2020-01-14 20:15:00,124.9,197.88099999999997,63.251999999999995,32.641 -2020-01-14 20:30:00,119.8,193.68200000000002,63.251999999999995,32.641 -2020-01-14 20:45:00,113.77,191.946,63.251999999999995,32.641 -2020-01-14 21:00:00,111.04,188.81599999999997,54.47,32.641 -2020-01-14 21:15:00,115.36,186.09599999999998,54.47,32.641 -2020-01-14 21:30:00,114.69,184.012,54.47,32.641 -2020-01-14 21:45:00,110.23,182.55,54.47,32.641 -2020-01-14 22:00:00,103.08,175.484,51.12,32.641 -2020-01-14 22:15:00,98.43,169.798,51.12,32.641 -2020-01-14 22:30:00,97.92,156.011,51.12,32.641 -2020-01-14 22:45:00,100.32,147.8,51.12,32.641 -2020-01-14 23:00:00,96.47,140.96200000000002,42.156000000000006,32.641 -2020-01-14 23:15:00,92.03,139.256,42.156000000000006,32.641 -2020-01-14 23:30:00,86.85,139.628,42.156000000000006,32.641 -2020-01-14 23:45:00,87.06,139.088,42.156000000000006,32.641 -2020-01-15 00:00:00,86.86,132.626,37.192,32.641 -2020-01-15 00:15:00,87.23,132.058,37.192,32.641 -2020-01-15 00:30:00,85.9,133.19799999999998,37.192,32.641 -2020-01-15 00:45:00,82.51,134.589,37.192,32.641 -2020-01-15 01:00:00,83.73,137.483,32.24,32.641 -2020-01-15 01:15:00,83.66,137.791,32.24,32.641 -2020-01-15 01:30:00,80.75,138.233,32.24,32.641 -2020-01-15 01:45:00,76.71,138.627,32.24,32.641 -2020-01-15 02:00:00,83.36,140.768,30.34,32.641 -2020-01-15 02:15:00,84.01,142.57,30.34,32.641 -2020-01-15 02:30:00,81.99,143.299,30.34,32.641 -2020-01-15 02:45:00,77.46,145.328,30.34,32.641 -2020-01-15 03:00:00,75.23,148.05,29.129,32.641 -2020-01-15 03:15:00,76.54,149.305,29.129,32.641 -2020-01-15 03:30:00,77.67,151.195,29.129,32.641 -2020-01-15 03:45:00,77.78,152.713,29.129,32.641 -2020-01-15 04:00:00,83.87,164.64700000000002,30.075,32.641 -2020-01-15 04:15:00,85.62,176.545,30.075,32.641 -2020-01-15 04:30:00,87.03,179.59900000000002,30.075,32.641 -2020-01-15 04:45:00,87.26,182.463,30.075,32.641 -2020-01-15 05:00:00,92.08,217.16400000000002,35.684,32.641 -2020-01-15 05:15:00,95.05,245.451,35.684,32.641 -2020-01-15 05:30:00,93.94,241.158,35.684,32.641 -2020-01-15 05:45:00,97.28,234.112,35.684,32.641 -2020-01-15 06:00:00,105.8,230.955,51.49,32.641 -2020-01-15 06:15:00,110.68,236.893,51.49,32.641 -2020-01-15 06:30:00,116.49,239.972,51.49,32.641 -2020-01-15 06:45:00,120.13,244.398,51.49,32.641 -2020-01-15 07:00:00,127.25,243.456,68.242,32.641 -2020-01-15 07:15:00,130.04,248.804,68.242,32.641 -2020-01-15 07:30:00,133.29,251.75599999999997,68.242,32.641 -2020-01-15 07:45:00,136.04,253.19299999999998,68.242,32.641 -2020-01-15 08:00:00,138.64,251.947,63.619,32.641 -2020-01-15 08:15:00,135.35,251.99599999999998,63.619,32.641 -2020-01-15 08:30:00,135.07,249.706,63.619,32.641 -2020-01-15 08:45:00,135.36,246.498,63.619,32.641 -2020-01-15 09:00:00,138.48,239.581,61.333,32.641 -2020-01-15 09:15:00,137.35,236.493,61.333,32.641 -2020-01-15 09:30:00,139.08,234.452,61.333,32.641 -2020-01-15 09:45:00,139.42,231.21900000000002,61.333,32.641 -2020-01-15 10:00:00,139.42,225.94299999999998,59.663000000000004,32.641 -2020-01-15 10:15:00,139.9,221.75400000000002,59.663000000000004,32.641 -2020-01-15 10:30:00,142.04,218.365,59.663000000000004,32.641 -2020-01-15 10:45:00,143.07,216.702,59.663000000000004,32.641 -2020-01-15 11:00:00,139.95,214.44799999999998,59.771,32.641 -2020-01-15 11:15:00,139.24,213.165,59.771,32.641 -2020-01-15 11:30:00,137.7,211.642,59.771,32.641 -2020-01-15 11:45:00,137.77,210.477,59.771,32.641 -2020-01-15 12:00:00,137.15,205.465,58.723,32.641 -2020-01-15 12:15:00,135.27,205.044,58.723,32.641 -2020-01-15 12:30:00,135.67,204.94400000000002,58.723,32.641 -2020-01-15 12:45:00,137.77,205.801,58.723,32.641 -2020-01-15 13:00:00,136.15,204.08700000000002,58.727,32.641 -2020-01-15 13:15:00,136.78,203.465,58.727,32.641 -2020-01-15 13:30:00,136.5,203.01,58.727,32.641 -2020-01-15 13:45:00,136.87,202.877,58.727,32.641 -2020-01-15 14:00:00,136.85,202.014,59.803999999999995,32.641 -2020-01-15 14:15:00,134.91,202.55900000000003,59.803999999999995,32.641 -2020-01-15 14:30:00,134.1,203.52200000000002,59.803999999999995,32.641 -2020-01-15 14:45:00,135.12,203.94400000000002,59.803999999999995,32.641 -2020-01-15 15:00:00,135.73,205.327,61.05,32.641 -2020-01-15 15:15:00,134.79,205.715,61.05,32.641 -2020-01-15 15:30:00,132.55,208.048,61.05,32.641 -2020-01-15 15:45:00,131.89,209.665,61.05,32.641 -2020-01-15 16:00:00,132.28,210.465,64.012,32.641 -2020-01-15 16:15:00,134.26,212.166,64.012,32.641 -2020-01-15 16:30:00,136.16,215.123,64.012,32.641 -2020-01-15 16:45:00,138.74,216.766,64.012,32.641 -2020-01-15 17:00:00,141.87,218.972,66.751,32.641 -2020-01-15 17:15:00,142.23,220.003,66.751,32.641 -2020-01-15 17:30:00,143.82,221.144,66.751,32.641 -2020-01-15 17:45:00,143.42,221.12900000000002,66.751,32.641 -2020-01-15 18:00:00,142.04,222.82,65.91199999999999,32.641 -2020-01-15 18:15:00,140.16,220.03099999999998,65.91199999999999,32.641 -2020-01-15 18:30:00,139.4,218.824,65.91199999999999,32.641 -2020-01-15 18:45:00,140.52,219.09400000000002,65.91199999999999,32.641 -2020-01-15 19:00:00,137.94,218.416,63.324,32.641 -2020-01-15 19:15:00,133.64,214.46099999999998,63.324,32.641 -2020-01-15 19:30:00,130.94,211.817,63.324,32.641 -2020-01-15 19:45:00,129.29,208.326,63.324,32.641 -2020-01-15 20:00:00,123.9,204.331,63.573,32.641 -2020-01-15 20:15:00,121.7,197.892,63.573,32.641 -2020-01-15 20:30:00,116.5,193.687,63.573,32.641 -2020-01-15 20:45:00,115.14,191.967,63.573,32.641 -2020-01-15 21:00:00,110.98,188.81799999999998,55.073,32.641 -2020-01-15 21:15:00,115.16,186.081,55.073,32.641 -2020-01-15 21:30:00,114.07,183.99599999999998,55.073,32.641 -2020-01-15 21:45:00,110.47,182.551,55.073,32.641 -2020-01-15 22:00:00,101.67,175.48,51.321999999999996,32.641 -2020-01-15 22:15:00,100.33,169.81099999999998,51.321999999999996,32.641 -2020-01-15 22:30:00,96.77,156.029,51.321999999999996,32.641 -2020-01-15 22:45:00,95.32,147.825,51.321999999999996,32.641 -2020-01-15 23:00:00,97.65,140.968,42.09,32.641 -2020-01-15 23:15:00,96.42,139.27200000000002,42.09,32.641 -2020-01-15 23:30:00,91.99,139.661,42.09,32.641 -2020-01-15 23:45:00,86.16,139.125,42.09,32.641 -2020-01-16 00:00:00,87.33,139.56,38.399,32.641 -2020-01-16 00:15:00,88.49,139.33,38.399,32.641 -2020-01-16 00:30:00,88.26,140.88299999999998,38.399,32.641 -2020-01-16 00:45:00,83.0,142.724,38.399,32.641 -2020-01-16 01:00:00,83.99,145.79399999999998,36.94,32.641 -2020-01-16 01:15:00,85.07,145.787,36.94,32.641 -2020-01-16 01:30:00,84.84,146.149,36.94,32.641 -2020-01-16 01:45:00,79.18,146.644,36.94,32.641 -2020-01-16 02:00:00,80.14,148.93200000000002,35.275,32.641 -2020-01-16 02:15:00,85.15,151.02200000000002,35.275,32.641 -2020-01-16 02:30:00,84.32,152.084,35.275,32.641 -2020-01-16 02:45:00,82.42,154.27100000000002,35.275,32.641 -2020-01-16 03:00:00,77.71,157.27,35.329,32.641 -2020-01-16 03:15:00,84.16,158.311,35.329,32.641 -2020-01-16 03:30:00,85.53,160.137,35.329,32.641 -2020-01-16 03:45:00,85.45,162.055,35.329,32.641 -2020-01-16 04:00:00,78.58,173.658,36.275,32.641 -2020-01-16 04:15:00,79.47,185.22099999999998,36.275,32.641 -2020-01-16 04:30:00,81.13,188.989,36.275,32.641 -2020-01-16 04:45:00,82.77,192.137,36.275,32.641 -2020-01-16 05:00:00,86.6,227.294,42.193999999999996,32.641 -2020-01-16 05:15:00,89.37,255.28799999999998,42.193999999999996,32.641 -2020-01-16 05:30:00,93.2,250.99099999999999,42.193999999999996,32.641 -2020-01-16 05:45:00,98.25,244.456,42.193999999999996,32.641 -2020-01-16 06:00:00,106.36,241.44099999999997,56.422,32.641 -2020-01-16 06:15:00,111.91,247.579,56.422,32.641 -2020-01-16 06:30:00,115.81,250.795,56.422,32.641 -2020-01-16 06:45:00,121.56,255.86900000000003,56.422,32.641 -2020-01-16 07:00:00,125.43,254.107,72.569,32.641 -2020-01-16 07:15:00,128.69,259.99,72.569,32.641 -2020-01-16 07:30:00,132.2,263.454,72.569,32.641 -2020-01-16 07:45:00,136.04,265.41200000000003,72.569,32.641 -2020-01-16 08:00:00,137.28,264.069,67.704,32.641 -2020-01-16 08:15:00,136.74,264.55400000000003,67.704,32.641 -2020-01-16 08:30:00,137.81,262.16900000000004,67.704,32.641 -2020-01-16 08:45:00,135.89,259.421,67.704,32.641 -2020-01-16 09:00:00,137.97,252.798,63.434,32.641 -2020-01-16 09:15:00,140.1,249.953,63.434,32.641 -2020-01-16 09:30:00,141.78,248.072,63.434,32.641 -2020-01-16 09:45:00,141.49,244.673,63.434,32.641 -2020-01-16 10:00:00,142.24,238.75599999999997,61.88399999999999,32.641 -2020-01-16 10:15:00,140.48,234.90400000000002,61.88399999999999,32.641 -2020-01-16 10:30:00,140.15,230.903,61.88399999999999,32.641 -2020-01-16 10:45:00,140.59,229.00799999999998,61.88399999999999,32.641 -2020-01-16 11:00:00,140.53,225.988,61.481,32.641 -2020-01-16 11:15:00,141.45,224.646,61.481,32.641 -2020-01-16 11:30:00,141.71,222.722,61.481,32.641 -2020-01-16 11:45:00,139.21,222.058,61.481,32.641 -2020-01-16 12:00:00,136.2,218.021,59.527,32.641 -2020-01-16 12:15:00,135.53,217.612,59.527,32.641 -2020-01-16 12:30:00,135.7,217.34900000000002,59.527,32.641 -2020-01-16 12:45:00,136.9,217.868,59.527,32.641 -2020-01-16 13:00:00,135.4,215.40400000000002,58.794,32.641 -2020-01-16 13:15:00,134.78,214.74099999999999,58.794,32.641 -2020-01-16 13:30:00,132.47,213.917,58.794,32.641 -2020-01-16 13:45:00,132.22,214.22,58.794,32.641 -2020-01-16 14:00:00,128.13,213.833,60.32,32.641 -2020-01-16 14:15:00,126.7,214.40200000000002,60.32,32.641 -2020-01-16 14:30:00,124.84,215.28599999999997,60.32,32.641 -2020-01-16 14:45:00,124.2,215.984,60.32,32.641 -2020-01-16 15:00:00,125.48,216.856,62.52,32.641 -2020-01-16 15:15:00,125.05,217.75799999999998,62.52,32.641 -2020-01-16 15:30:00,126.77,219.547,62.52,32.641 -2020-01-16 15:45:00,129.95,220.50099999999998,62.52,32.641 -2020-01-16 16:00:00,132.47,222.225,64.199,32.641 -2020-01-16 16:15:00,131.37,224.09099999999998,64.199,32.641 -2020-01-16 16:30:00,133.46,226.852,64.199,32.641 -2020-01-16 16:45:00,136.14,228.108,64.199,32.641 -2020-01-16 17:00:00,140.04,230.606,68.19800000000001,32.641 -2020-01-16 17:15:00,139.73,231.262,68.19800000000001,32.641 -2020-01-16 17:30:00,140.43,232.16,68.19800000000001,32.641 -2020-01-16 17:45:00,140.42,231.972,68.19800000000001,32.641 -2020-01-16 18:00:00,138.81,233.65400000000002,67.899,32.641 -2020-01-16 18:15:00,136.53,230.218,67.899,32.641 -2020-01-16 18:30:00,137.16,229.18400000000003,67.899,32.641 -2020-01-16 18:45:00,136.92,229.485,67.899,32.641 -2020-01-16 19:00:00,133.32,227.865,64.72399999999999,32.641 -2020-01-16 19:15:00,131.23,223.705,64.72399999999999,32.641 -2020-01-16 19:30:00,133.3,220.62099999999998,64.72399999999999,32.641 -2020-01-16 19:45:00,135.03,217.56900000000002,64.72399999999999,32.641 -2020-01-16 20:00:00,127.62,213.46200000000002,64.062,32.641 -2020-01-16 20:15:00,119.26,206.58700000000002,64.062,32.641 -2020-01-16 20:30:00,116.31,202.333,64.062,32.641 -2020-01-16 20:45:00,111.77,201.076,64.062,32.641 -2020-01-16 21:00:00,108.51,197.449,57.971000000000004,32.641 -2020-01-16 21:15:00,112.3,194.61,57.971000000000004,32.641 -2020-01-16 21:30:00,110.27,192.56900000000002,57.971000000000004,32.641 -2020-01-16 21:45:00,106.27,190.99400000000003,57.971000000000004,32.641 -2020-01-16 22:00:00,97.28,183.658,53.715,32.641 -2020-01-16 22:15:00,96.36,177.688,53.715,32.641 -2020-01-16 22:30:00,94.99,163.819,53.715,32.641 -2020-01-16 22:45:00,98.2,155.327,53.715,32.641 -2020-01-16 23:00:00,94.28,147.961,47.8,32.641 -2020-01-16 23:15:00,91.98,146.525,47.8,32.641 -2020-01-16 23:30:00,84.84,146.689,47.8,32.641 -2020-01-16 23:45:00,82.59,146.202,47.8,32.641 -2020-01-17 00:00:00,85.95,138.651,43.656000000000006,32.641 -2020-01-17 00:15:00,86.53,138.59799999999998,43.656000000000006,32.641 -2020-01-17 00:30:00,85.43,139.931,43.656000000000006,32.641 -2020-01-17 00:45:00,79.36,141.822,43.656000000000006,32.641 -2020-01-17 01:00:00,80.97,144.585,41.263000000000005,32.641 -2020-01-17 01:15:00,83.5,145.747,41.263000000000005,32.641 -2020-01-17 01:30:00,83.45,145.722,41.263000000000005,32.641 -2020-01-17 01:45:00,80.4,146.374,41.263000000000005,32.641 -2020-01-17 02:00:00,78.23,148.619,40.799,32.641 -2020-01-17 02:15:00,74.91,150.584,40.799,32.641 -2020-01-17 02:30:00,73.54,152.134,40.799,32.641 -2020-01-17 02:45:00,74.37,154.489,40.799,32.641 -2020-01-17 03:00:00,79.04,156.16899999999998,41.398,32.641 -2020-01-17 03:15:00,83.49,158.55200000000002,41.398,32.641 -2020-01-17 03:30:00,84.6,160.399,41.398,32.641 -2020-01-17 03:45:00,82.01,162.561,41.398,32.641 -2020-01-17 04:00:00,77.58,174.391,42.38,32.641 -2020-01-17 04:15:00,78.35,185.93099999999998,42.38,32.641 -2020-01-17 04:30:00,79.29,189.803,42.38,32.641 -2020-01-17 04:45:00,82.16,191.699,42.38,32.641 -2020-01-17 05:00:00,85.97,225.387,46.181000000000004,32.641 -2020-01-17 05:15:00,87.85,254.97799999999998,46.181000000000004,32.641 -2020-01-17 05:30:00,94.06,251.938,46.181000000000004,32.641 -2020-01-17 05:45:00,97.27,245.42700000000002,46.181000000000004,32.641 -2020-01-17 06:00:00,106.38,242.907,59.33,32.641 -2020-01-17 06:15:00,111.42,247.27,59.33,32.641 -2020-01-17 06:30:00,115.82,249.47,59.33,32.641 -2020-01-17 06:45:00,119.88,256.536,59.33,32.641 -2020-01-17 07:00:00,127.7,253.679,72.454,32.641 -2020-01-17 07:15:00,130.26,260.601,72.454,32.641 -2020-01-17 07:30:00,132.73,264.178,72.454,32.641 -2020-01-17 07:45:00,134.99,265.091,72.454,32.641 -2020-01-17 08:00:00,136.8,262.293,67.175,32.641 -2020-01-17 08:15:00,135.31,262.188,67.175,32.641 -2020-01-17 08:30:00,136.28,260.932,67.175,32.641 -2020-01-17 08:45:00,136.61,256.327,67.175,32.641 -2020-01-17 09:00:00,136.32,250.61700000000002,65.365,32.641 -2020-01-17 09:15:00,137.58,248.14,65.365,32.641 -2020-01-17 09:30:00,139.47,245.88400000000001,65.365,32.641 -2020-01-17 09:45:00,139.06,242.29,65.365,32.641 -2020-01-17 10:00:00,137.15,235.06400000000002,63.95,32.641 -2020-01-17 10:15:00,136.31,232.081,63.95,32.641 -2020-01-17 10:30:00,137.15,227.899,63.95,32.641 -2020-01-17 10:45:00,138.46,225.50599999999997,63.95,32.641 -2020-01-17 11:00:00,138.45,222.419,63.92100000000001,32.641 -2020-01-17 11:15:00,140.66,220.19799999999998,63.92100000000001,32.641 -2020-01-17 11:30:00,142.55,220.382,63.92100000000001,32.641 -2020-01-17 11:45:00,141.43,219.925,63.92100000000001,32.641 -2020-01-17 12:00:00,140.64,217.108,60.79600000000001,32.641 -2020-01-17 12:15:00,141.7,214.36599999999999,60.79600000000001,32.641 -2020-01-17 12:30:00,140.79,214.257,60.79600000000001,32.641 -2020-01-17 12:45:00,140.1,215.50099999999998,60.79600000000001,32.641 -2020-01-17 13:00:00,138.62,214.043,59.393,32.641 -2020-01-17 13:15:00,137.23,214.293,59.393,32.641 -2020-01-17 13:30:00,134.36,213.351,59.393,32.641 -2020-01-17 13:45:00,134.11,213.528,59.393,32.641 -2020-01-17 14:00:00,133.51,211.956,57.943999999999996,32.641 -2020-01-17 14:15:00,133.72,212.239,57.943999999999996,32.641 -2020-01-17 14:30:00,131.64,213.493,57.943999999999996,32.641 -2020-01-17 14:45:00,132.21,214.667,57.943999999999996,32.641 -2020-01-17 15:00:00,131.29,215.00400000000002,60.153999999999996,32.641 -2020-01-17 15:15:00,129.71,215.408,60.153999999999996,32.641 -2020-01-17 15:30:00,128.64,215.49900000000002,60.153999999999996,32.641 -2020-01-17 15:45:00,127.96,216.497,60.153999999999996,32.641 -2020-01-17 16:00:00,129.48,217.007,62.933,32.641 -2020-01-17 16:15:00,129.9,219.145,62.933,32.641 -2020-01-17 16:30:00,132.34,222.05,62.933,32.641 -2020-01-17 16:45:00,137.1,223.28400000000002,62.933,32.641 -2020-01-17 17:00:00,139.8,225.78599999999997,68.657,32.641 -2020-01-17 17:15:00,138.22,226.025,68.657,32.641 -2020-01-17 17:30:00,139.12,226.56900000000002,68.657,32.641 -2020-01-17 17:45:00,139.16,226.158,68.657,32.641 -2020-01-17 18:00:00,138.22,228.674,67.111,32.641 -2020-01-17 18:15:00,136.69,224.929,67.111,32.641 -2020-01-17 18:30:00,135.95,224.361,67.111,32.641 -2020-01-17 18:45:00,135.85,224.62599999999998,67.111,32.641 -2020-01-17 19:00:00,132.67,223.908,62.434,32.641 -2020-01-17 19:15:00,129.94,221.23,62.434,32.641 -2020-01-17 19:30:00,127.9,217.68,62.434,32.641 -2020-01-17 19:45:00,126.36,214.235,62.434,32.641 -2020-01-17 20:00:00,119.39,210.176,61.763000000000005,32.641 -2020-01-17 20:15:00,116.31,203.215,61.763000000000005,32.641 -2020-01-17 20:30:00,112.3,198.968,61.763000000000005,32.641 -2020-01-17 20:45:00,109.84,198.465,61.763000000000005,32.641 -2020-01-17 21:00:00,104.58,195.234,56.785,32.641 -2020-01-17 21:15:00,101.37,192.667,56.785,32.641 -2020-01-17 21:30:00,99.26,190.696,56.785,32.641 -2020-01-17 21:45:00,97.84,189.733,56.785,32.641 -2020-01-17 22:00:00,96.2,183.52700000000002,52.693000000000005,32.641 -2020-01-17 22:15:00,91.12,177.456,52.693000000000005,32.641 -2020-01-17 22:30:00,88.41,170.262,52.693000000000005,32.641 -2020-01-17 22:45:00,87.24,165.695,52.693000000000005,32.641 -2020-01-17 23:00:00,83.19,157.593,45.443999999999996,32.641 -2020-01-17 23:15:00,82.01,154.141,45.443999999999996,32.641 -2020-01-17 23:30:00,80.42,152.884,45.443999999999996,32.641 -2020-01-17 23:45:00,79.42,151.662,45.443999999999996,32.641 -2020-01-18 00:00:00,75.81,135.17700000000002,44.738,32.459 -2020-01-18 00:15:00,73.43,130.239,44.738,32.459 -2020-01-18 00:30:00,72.12,133.179,44.738,32.459 -2020-01-18 00:45:00,70.8,136.001,44.738,32.459 -2020-01-18 01:00:00,68.46,139.438,40.303000000000004,32.459 -2020-01-18 01:15:00,68.68,139.34,40.303000000000004,32.459 -2020-01-18 01:30:00,68.0,138.83100000000002,40.303000000000004,32.459 -2020-01-18 01:45:00,67.64,139.023,40.303000000000004,32.459 -2020-01-18 02:00:00,66.26,142.22299999999998,38.61,32.459 -2020-01-18 02:15:00,66.19,143.881,38.61,32.459 -2020-01-18 02:30:00,65.47,144.265,38.61,32.459 -2020-01-18 02:45:00,65.76,146.61,38.61,32.459 -2020-01-18 03:00:00,65.61,149.232,37.554,32.459 -2020-01-18 03:15:00,65.84,150.313,37.554,32.459 -2020-01-18 03:30:00,64.93,150.23,37.554,32.459 -2020-01-18 03:45:00,66.07,152.283,37.554,32.459 -2020-01-18 04:00:00,65.87,159.47899999999998,37.176,32.459 -2020-01-18 04:15:00,66.39,168.15,37.176,32.459 -2020-01-18 04:30:00,65.9,169.74400000000003,37.176,32.459 -2020-01-18 04:45:00,68.46,170.998,37.176,32.459 -2020-01-18 05:00:00,67.62,187.06400000000002,36.893,32.459 -2020-01-18 05:15:00,67.44,195.977,36.893,32.459 -2020-01-18 05:30:00,68.22,193.09,36.893,32.459 -2020-01-18 05:45:00,68.87,192.283,36.893,32.459 -2020-01-18 06:00:00,70.41,210.16299999999998,37.803000000000004,32.459 -2020-01-18 06:15:00,71.3,232.584,37.803000000000004,32.459 -2020-01-18 06:30:00,73.03,228.908,37.803000000000004,32.459 -2020-01-18 06:45:00,75.0,225.896,37.803000000000004,32.459 -2020-01-18 07:00:00,78.98,218.862,41.086999999999996,32.459 -2020-01-18 07:15:00,80.53,224.50599999999997,41.086999999999996,32.459 -2020-01-18 07:30:00,83.81,231.007,41.086999999999996,32.459 -2020-01-18 07:45:00,85.94,236.44799999999998,41.086999999999996,32.459 -2020-01-18 08:00:00,88.7,238.415,48.222,32.459 -2020-01-18 08:15:00,89.91,242.604,48.222,32.459 -2020-01-18 08:30:00,92.41,243.21,48.222,32.459 -2020-01-18 08:45:00,95.69,242.15099999999998,48.222,32.459 -2020-01-18 09:00:00,98.41,238.05700000000002,52.791000000000004,32.459 -2020-01-18 09:15:00,98.48,236.391,52.791000000000004,32.459 -2020-01-18 09:30:00,102.21,235.132,52.791000000000004,32.459 -2020-01-18 09:45:00,100.36,231.793,52.791000000000004,32.459 -2020-01-18 10:00:00,100.12,224.771,54.341,32.459 -2020-01-18 10:15:00,100.75,221.933,54.341,32.459 -2020-01-18 10:30:00,101.87,217.99599999999998,54.341,32.459 -2020-01-18 10:45:00,102.85,217.19299999999998,54.341,32.459 -2020-01-18 11:00:00,101.48,214.399,51.94,32.459 -2020-01-18 11:15:00,103.12,211.248,51.94,32.459 -2020-01-18 11:30:00,103.04,210.08900000000003,51.94,32.459 -2020-01-18 11:45:00,100.05,208.422,51.94,32.459 -2020-01-18 12:00:00,97.04,204.50599999999997,50.973,32.459 -2020-01-18 12:15:00,95.62,202.4,50.973,32.459 -2020-01-18 12:30:00,92.58,202.668,50.973,32.459 -2020-01-18 12:45:00,92.09,202.90200000000002,50.973,32.459 -2020-01-18 13:00:00,90.24,201.095,48.06399999999999,32.459 -2020-01-18 13:15:00,87.19,199.011,48.06399999999999,32.459 -2020-01-18 13:30:00,86.44,197.50099999999998,48.06399999999999,32.459 -2020-01-18 13:45:00,89.07,198.437,48.06399999999999,32.459 -2020-01-18 14:00:00,85.42,198.355,45.707,32.459 -2020-01-18 14:15:00,85.02,198.19299999999998,45.707,32.459 -2020-01-18 14:30:00,88.14,197.393,45.707,32.459 -2020-01-18 14:45:00,88.52,198.765,45.707,32.459 -2020-01-18 15:00:00,88.89,199.88299999999998,47.567,32.459 -2020-01-18 15:15:00,89.04,201.095,47.567,32.459 -2020-01-18 15:30:00,91.84,202.926,47.567,32.459 -2020-01-18 15:45:00,89.9,204.063,47.567,32.459 -2020-01-18 16:00:00,91.8,202.982,52.031000000000006,32.459 -2020-01-18 16:15:00,92.59,206.278,52.031000000000006,32.459 -2020-01-18 16:30:00,94.37,209.09900000000002,52.031000000000006,32.459 -2020-01-18 16:45:00,98.68,211.359,52.031000000000006,32.459 -2020-01-18 17:00:00,104.16,213.451,58.218999999999994,32.459 -2020-01-18 17:15:00,105.49,215.894,58.218999999999994,32.459 -2020-01-18 17:30:00,106.99,216.37900000000002,58.218999999999994,32.459 -2020-01-18 17:45:00,107.98,215.463,58.218999999999994,32.459 -2020-01-18 18:00:00,108.94,217.34,57.65,32.459 -2020-01-18 18:15:00,110.75,215.455,57.65,32.459 -2020-01-18 18:30:00,108.32,216.24099999999999,57.65,32.459 -2020-01-18 18:45:00,107.67,213.148,57.65,32.459 -2020-01-18 19:00:00,105.41,213.674,51.261,32.459 -2020-01-18 19:15:00,104.23,210.55700000000002,51.261,32.459 -2020-01-18 19:30:00,103.1,207.775,51.261,32.459 -2020-01-18 19:45:00,104.08,203.93900000000002,51.261,32.459 -2020-01-18 20:00:00,96.04,202.172,44.068000000000005,32.459 -2020-01-18 20:15:00,93.37,197.706,44.068000000000005,32.459 -2020-01-18 20:30:00,90.26,193.16400000000002,44.068000000000005,32.459 -2020-01-18 20:45:00,88.26,191.99400000000003,44.068000000000005,32.459 -2020-01-18 21:00:00,85.63,191.48,38.861,32.459 -2020-01-18 21:15:00,83.97,189.435,38.861,32.459 -2020-01-18 21:30:00,82.78,188.83599999999998,38.861,32.459 -2020-01-18 21:45:00,81.38,187.49400000000003,38.861,32.459 -2020-01-18 22:00:00,78.87,182.81099999999998,39.485,32.459 -2020-01-18 22:15:00,77.45,179.549,39.485,32.459 -2020-01-18 22:30:00,74.7,179.446,39.485,32.459 -2020-01-18 22:45:00,73.83,176.96,39.485,32.459 -2020-01-18 23:00:00,70.86,171.66400000000002,32.027,32.459 -2020-01-18 23:15:00,70.25,166.322,32.027,32.459 -2020-01-18 23:30:00,67.49,162.817,32.027,32.459 -2020-01-18 23:45:00,66.15,158.811,32.027,32.459 -2020-01-19 00:00:00,63.53,135.497,26.96,32.459 -2020-01-19 00:15:00,61.94,130.341,26.96,32.459 -2020-01-19 00:30:00,60.49,132.85,26.96,32.459 -2020-01-19 00:45:00,59.53,136.483,26.96,32.459 -2020-01-19 01:00:00,58.12,139.71,24.295,32.459 -2020-01-19 01:15:00,58.27,140.829,24.295,32.459 -2020-01-19 01:30:00,57.64,140.945,24.295,32.459 -2020-01-19 01:45:00,57.35,140.821,24.295,32.459 -2020-01-19 02:00:00,56.61,143.149,24.268,32.459 -2020-01-19 02:15:00,56.08,143.701,24.268,32.459 -2020-01-19 02:30:00,55.8,145.066,24.268,32.459 -2020-01-19 02:45:00,56.32,147.996,24.268,32.459 -2020-01-19 03:00:00,55.66,150.884,23.373,32.459 -2020-01-19 03:15:00,55.71,151.326,23.373,32.459 -2020-01-19 03:30:00,55.81,153.001,23.373,32.459 -2020-01-19 03:45:00,56.11,155.10299999999998,23.373,32.459 -2020-01-19 04:00:00,55.85,162.005,23.874000000000002,32.459 -2020-01-19 04:15:00,56.49,169.545,23.874000000000002,32.459 -2020-01-19 04:30:00,57.02,170.997,23.874000000000002,32.459 -2020-01-19 04:45:00,57.49,172.642,23.874000000000002,32.459 -2020-01-19 05:00:00,58.49,184.49599999999998,24.871,32.459 -2020-01-19 05:15:00,58.83,190.658,24.871,32.459 -2020-01-19 05:30:00,58.88,187.644,24.871,32.459 -2020-01-19 05:45:00,59.64,187.18900000000002,24.871,32.459 -2020-01-19 06:00:00,59.96,205.362,23.84,32.459 -2020-01-19 06:15:00,60.41,225.62599999999998,23.84,32.459 -2020-01-19 06:30:00,60.71,220.766,23.84,32.459 -2020-01-19 06:45:00,64.2,216.699,23.84,32.459 -2020-01-19 07:00:00,64.77,212.528,27.430999999999997,32.459 -2020-01-19 07:15:00,65.87,217.419,27.430999999999997,32.459 -2020-01-19 07:30:00,68.27,222.238,27.430999999999997,32.459 -2020-01-19 07:45:00,70.01,226.76,27.430999999999997,32.459 -2020-01-19 08:00:00,72.98,230.78099999999998,33.891999999999996,32.459 -2020-01-19 08:15:00,74.51,234.66099999999997,33.891999999999996,32.459 -2020-01-19 08:30:00,77.33,237.02900000000002,33.891999999999996,32.459 -2020-01-19 08:45:00,79.56,238.33,33.891999999999996,32.459 -2020-01-19 09:00:00,81.57,233.78099999999998,37.571,32.459 -2020-01-19 09:15:00,83.04,232.83599999999998,37.571,32.459 -2020-01-19 09:30:00,86.55,231.352,37.571,32.459 -2020-01-19 09:45:00,86.23,227.72299999999998,37.571,32.459 -2020-01-19 10:00:00,87.7,223.55200000000002,40.594,32.459 -2020-01-19 10:15:00,89.59,221.326,40.594,32.459 -2020-01-19 10:30:00,90.48,218.054,40.594,32.459 -2020-01-19 10:45:00,92.41,214.893,40.594,32.459 -2020-01-19 11:00:00,94.56,213.213,44.133,32.459 -2020-01-19 11:15:00,98.06,210.283,44.133,32.459 -2020-01-19 11:30:00,99.96,208.044,44.133,32.459 -2020-01-19 11:45:00,100.24,207.048,44.133,32.459 -2020-01-19 12:00:00,97.6,202.331,41.198,32.459 -2020-01-19 12:15:00,96.12,202.545,41.198,32.459 -2020-01-19 12:30:00,92.64,201.079,41.198,32.459 -2020-01-19 12:45:00,92.83,200.282,41.198,32.459 -2020-01-19 13:00:00,87.94,197.68400000000003,37.014,32.459 -2020-01-19 13:15:00,87.17,199.167,37.014,32.459 -2020-01-19 13:30:00,85.79,197.56099999999998,37.014,32.459 -2020-01-19 13:45:00,85.29,197.56599999999997,37.014,32.459 -2020-01-19 14:00:00,84.54,197.63099999999997,34.934,32.459 -2020-01-19 14:15:00,83.97,198.785,34.934,32.459 -2020-01-19 14:30:00,84.35,199.588,34.934,32.459 -2020-01-19 14:45:00,85.14,200.653,34.934,32.459 -2020-01-19 15:00:00,84.99,199.99,34.588,32.459 -2020-01-19 15:15:00,84.82,202.16,34.588,32.459 -2020-01-19 15:30:00,85.3,204.67700000000002,34.588,32.459 -2020-01-19 15:45:00,84.88,206.55900000000003,34.588,32.459 -2020-01-19 16:00:00,86.12,207.805,37.874,32.459 -2020-01-19 16:15:00,86.01,210.03400000000002,37.874,32.459 -2020-01-19 16:30:00,87.08,213.02700000000002,37.874,32.459 -2020-01-19 16:45:00,89.57,215.435,37.874,32.459 -2020-01-19 17:00:00,97.87,217.421,47.303999999999995,32.459 -2020-01-19 17:15:00,102.85,219.338,47.303999999999995,32.459 -2020-01-19 17:30:00,105.44,220.085,47.303999999999995,32.459 -2020-01-19 17:45:00,106.76,221.72400000000002,47.303999999999995,32.459 -2020-01-19 18:00:00,106.95,222.90200000000002,48.879,32.459 -2020-01-19 18:15:00,108.0,222.58700000000002,48.879,32.459 -2020-01-19 18:30:00,104.37,221.048,48.879,32.459 -2020-01-19 18:45:00,103.14,220.05900000000003,48.879,32.459 -2020-01-19 19:00:00,101.77,219.907,44.826,32.459 -2020-01-19 19:15:00,99.54,217.583,44.826,32.459 -2020-01-19 19:30:00,98.11,214.65400000000002,44.826,32.459 -2020-01-19 19:45:00,96.83,212.50400000000002,44.826,32.459 -2020-01-19 20:00:00,98.04,210.7,40.154,32.459 -2020-01-19 20:15:00,101.48,207.36900000000003,40.154,32.459 -2020-01-19 20:30:00,97.9,204.132,40.154,32.459 -2020-01-19 20:45:00,89.29,201.812,40.154,32.459 -2020-01-19 21:00:00,89.58,198.354,36.549,32.459 -2020-01-19 21:15:00,86.78,195.61900000000003,36.549,32.459 -2020-01-19 21:30:00,86.94,195.425,36.549,32.459 -2020-01-19 21:45:00,87.39,194.21200000000002,36.549,32.459 -2020-01-19 22:00:00,87.92,187.94099999999997,37.663000000000004,32.459 -2020-01-19 22:15:00,85.91,184.03,37.663000000000004,32.459 -2020-01-19 22:30:00,86.09,180.403,37.663000000000004,32.459 -2020-01-19 22:45:00,90.89,177.109,37.663000000000004,32.459 -2020-01-19 23:00:00,87.77,168.668,31.945,32.459 -2020-01-19 23:15:00,86.14,165.292,31.945,32.459 -2020-01-19 23:30:00,80.64,162.781,31.945,32.459 -2020-01-19 23:45:00,82.46,159.77200000000002,31.945,32.459 -2020-01-20 00:00:00,84.43,140.184,31.533,32.641 -2020-01-20 00:15:00,82.35,138.34799999999998,31.533,32.641 -2020-01-20 00:30:00,79.55,141.045,31.533,32.641 -2020-01-20 00:45:00,74.22,144.09799999999998,31.533,32.641 -2020-01-20 01:00:00,68.93,147.299,30.56,32.641 -2020-01-20 01:15:00,74.43,147.789,30.56,32.641 -2020-01-20 01:30:00,78.16,147.909,30.56,32.641 -2020-01-20 01:45:00,77.86,147.925,30.56,32.641 -2020-01-20 02:00:00,74.11,150.179,29.55,32.641 -2020-01-20 02:15:00,78.18,152.602,29.55,32.641 -2020-01-20 02:30:00,78.02,154.341,29.55,32.641 -2020-01-20 02:45:00,78.43,156.56799999999998,29.55,32.641 -2020-01-20 03:00:00,75.4,160.888,27.059,32.641 -2020-01-20 03:15:00,72.87,163.166,27.059,32.641 -2020-01-20 03:30:00,73.43,164.412,27.059,32.641 -2020-01-20 03:45:00,74.76,165.95,27.059,32.641 -2020-01-20 04:00:00,81.45,177.48,28.384,32.641 -2020-01-20 04:15:00,80.59,189.41,28.384,32.641 -2020-01-20 04:30:00,78.24,193.53599999999997,28.384,32.641 -2020-01-20 04:45:00,80.69,195.301,28.384,32.641 -2020-01-20 05:00:00,85.34,224.606,35.915,32.641 -2020-01-20 05:15:00,87.03,252.854,35.915,32.641 -2020-01-20 05:30:00,92.23,250.43200000000002,35.915,32.641 -2020-01-20 05:45:00,96.51,243.827,35.915,32.641 -2020-01-20 06:00:00,105.11,242.514,56.18,32.641 -2020-01-20 06:15:00,109.59,246.791,56.18,32.641 -2020-01-20 06:30:00,116.86,250.736,56.18,32.641 -2020-01-20 06:45:00,120.28,256.20099999999996,56.18,32.641 -2020-01-20 07:00:00,128.01,254.76,70.877,32.641 -2020-01-20 07:15:00,133.06,260.723,70.877,32.641 -2020-01-20 07:30:00,134.78,264.745,70.877,32.641 -2020-01-20 07:45:00,134.45,266.157,70.877,32.641 -2020-01-20 08:00:00,137.24,264.639,65.65,32.641 -2020-01-20 08:15:00,136.02,266.177,65.65,32.641 -2020-01-20 08:30:00,137.76,263.861,65.65,32.641 -2020-01-20 08:45:00,135.54,261.198,65.65,32.641 -2020-01-20 09:00:00,137.3,255.574,62.037,32.641 -2020-01-20 09:15:00,140.43,250.84400000000002,62.037,32.641 -2020-01-20 09:30:00,141.76,248.355,62.037,32.641 -2020-01-20 09:45:00,141.7,245.425,62.037,32.641 -2020-01-20 10:00:00,140.32,239.98,60.409,32.641 -2020-01-20 10:15:00,143.66,237.407,60.409,32.641 -2020-01-20 10:30:00,143.46,233.18,60.409,32.641 -2020-01-20 10:45:00,142.34,231.155,60.409,32.641 -2020-01-20 11:00:00,141.51,226.299,60.211999999999996,32.641 -2020-01-20 11:15:00,141.96,225.43,60.211999999999996,32.641 -2020-01-20 11:30:00,141.04,224.695,60.211999999999996,32.641 -2020-01-20 11:45:00,140.21,223.153,60.211999999999996,32.641 -2020-01-20 12:00:00,137.28,220.753,57.733000000000004,32.641 -2020-01-20 12:15:00,135.08,220.96900000000002,57.733000000000004,32.641 -2020-01-20 12:30:00,134.0,219.947,57.733000000000004,32.641 -2020-01-20 12:45:00,133.78,220.92700000000002,57.733000000000004,32.641 -2020-01-20 13:00:00,132.27,218.886,58.695,32.641 -2020-01-20 13:15:00,132.8,218.87,58.695,32.641 -2020-01-20 13:30:00,131.49,216.606,58.695,32.641 -2020-01-20 13:45:00,135.03,216.507,58.695,32.641 -2020-01-20 14:00:00,134.0,215.979,59.505,32.641 -2020-01-20 14:15:00,133.27,216.283,59.505,32.641 -2020-01-20 14:30:00,132.16,216.48,59.505,32.641 -2020-01-20 14:45:00,133.29,217.21,59.505,32.641 -2020-01-20 15:00:00,134.34,218.625,59.946000000000005,32.641 -2020-01-20 15:15:00,131.87,219.19099999999997,59.946000000000005,32.641 -2020-01-20 15:30:00,131.59,220.61599999999999,59.946000000000005,32.641 -2020-01-20 15:45:00,130.93,222.04,59.946000000000005,32.641 -2020-01-20 16:00:00,132.11,223.24900000000002,61.766999999999996,32.641 -2020-01-20 16:15:00,130.14,224.595,61.766999999999996,32.641 -2020-01-20 16:30:00,131.7,226.576,61.766999999999996,32.641 -2020-01-20 16:45:00,135.3,227.615,61.766999999999996,32.641 -2020-01-20 17:00:00,141.09,229.46099999999998,67.85600000000001,32.641 -2020-01-20 17:15:00,139.31,230.275,67.85600000000001,32.641 -2020-01-20 17:30:00,138.86,230.47799999999998,67.85600000000001,32.641 -2020-01-20 17:45:00,138.41,230.486,67.85600000000001,32.641 -2020-01-20 18:00:00,138.69,232.252,64.564,32.641 -2020-01-20 18:15:00,137.29,229.68,64.564,32.641 -2020-01-20 18:30:00,136.61,228.98,64.564,32.641 -2020-01-20 18:45:00,137.05,228.43400000000003,64.564,32.641 -2020-01-20 19:00:00,133.91,226.423,58.536,32.641 -2020-01-20 19:15:00,132.7,222.625,58.536,32.641 -2020-01-20 19:30:00,128.93,220.31099999999998,58.536,32.641 -2020-01-20 19:45:00,128.61,217.30700000000002,58.536,32.641 -2020-01-20 20:00:00,121.04,212.972,59.888999999999996,32.641 -2020-01-20 20:15:00,117.83,206.653,59.888999999999996,32.641 -2020-01-20 20:30:00,115.43,201.231,59.888999999999996,32.641 -2020-01-20 20:45:00,114.45,200.747,59.888999999999996,32.641 -2020-01-20 21:00:00,108.29,197.956,52.652,32.641 -2020-01-20 21:15:00,112.06,193.812,52.652,32.641 -2020-01-20 21:30:00,112.01,192.627,52.652,32.641 -2020-01-20 21:45:00,109.57,190.88400000000001,52.652,32.641 -2020-01-20 22:00:00,100.81,181.58599999999998,46.17,32.641 -2020-01-20 22:15:00,98.31,175.95,46.17,32.641 -2020-01-20 22:30:00,96.61,162.003,46.17,32.641 -2020-01-20 22:45:00,99.56,153.227,46.17,32.641 -2020-01-20 23:00:00,96.3,145.66,36.281,32.641 -2020-01-20 23:15:00,94.34,145.513,36.281,32.641 -2020-01-20 23:30:00,84.13,146.162,36.281,32.641 -2020-01-20 23:45:00,85.68,146.227,36.281,32.641 -2020-01-21 00:00:00,88.44,139.76,38.821999999999996,32.641 -2020-01-21 00:15:00,87.49,139.435,38.821999999999996,32.641 -2020-01-21 00:30:00,86.84,140.901,38.821999999999996,32.641 -2020-01-21 00:45:00,82.6,142.671,38.821999999999996,32.641 -2020-01-21 01:00:00,84.63,145.717,36.936,32.641 -2020-01-21 01:15:00,85.35,145.653,36.936,32.641 -2020-01-21 01:30:00,83.29,145.993,36.936,32.641 -2020-01-21 01:45:00,81.04,146.444,36.936,32.641 -2020-01-21 02:00:00,83.81,148.781,34.42,32.641 -2020-01-21 02:15:00,86.37,150.878,34.42,32.641 -2020-01-21 02:30:00,82.71,151.991,34.42,32.641 -2020-01-21 02:45:00,82.96,154.17600000000002,34.42,32.641 -2020-01-21 03:00:00,82.05,157.155,33.585,32.641 -2020-01-21 03:15:00,84.36,158.278,33.585,32.641 -2020-01-21 03:30:00,78.36,160.099,33.585,32.641 -2020-01-21 03:45:00,85.53,162.072,33.585,32.641 -2020-01-21 04:00:00,85.62,173.56400000000002,35.622,32.641 -2020-01-21 04:15:00,85.41,185.06599999999997,35.622,32.641 -2020-01-21 04:30:00,84.33,188.84900000000002,35.622,32.641 -2020-01-21 04:45:00,89.36,191.958,35.622,32.641 -2020-01-21 05:00:00,95.38,226.903,40.599000000000004,32.641 -2020-01-21 05:15:00,91.27,254.767,40.599000000000004,32.641 -2020-01-21 05:30:00,93.24,250.47400000000002,40.599000000000004,32.641 -2020-01-21 05:45:00,98.12,244.033,40.599000000000004,32.641 -2020-01-21 06:00:00,108.67,241.128,55.203,32.641 -2020-01-21 06:15:00,112.52,247.321,55.203,32.641 -2020-01-21 06:30:00,114.67,250.555,55.203,32.641 -2020-01-21 06:45:00,119.03,255.76,55.203,32.641 -2020-01-21 07:00:00,126.8,254.093,69.029,32.641 -2020-01-21 07:15:00,129.56,259.89599999999996,69.029,32.641 -2020-01-21 07:30:00,135.6,263.238,69.029,32.641 -2020-01-21 07:45:00,132.37,265.056,69.029,32.641 -2020-01-21 08:00:00,134.51,263.666,65.85300000000001,32.641 -2020-01-21 08:15:00,134.53,264.052,65.85300000000001,32.641 -2020-01-21 08:30:00,135.3,261.45099999999996,65.85300000000001,32.641 -2020-01-21 08:45:00,136.72,258.621,65.85300000000001,32.641 -2020-01-21 09:00:00,137.69,251.945,61.566,32.641 -2020-01-21 09:15:00,138.99,249.12900000000002,61.566,32.641 -2020-01-21 09:30:00,140.22,247.34099999999998,61.566,32.641 -2020-01-21 09:45:00,141.41,243.92,61.566,32.641 -2020-01-21 10:00:00,138.32,238.024,61.244,32.641 -2020-01-21 10:15:00,140.84,234.231,61.244,32.641 -2020-01-21 10:30:00,140.25,230.209,61.244,32.641 -2020-01-21 10:45:00,139.51,228.35,61.244,32.641 -2020-01-21 11:00:00,139.77,225.236,61.16,32.641 -2020-01-21 11:15:00,140.28,223.91,61.16,32.641 -2020-01-21 11:30:00,141.43,222.003,61.16,32.641 -2020-01-21 11:45:00,141.15,221.368,61.16,32.641 -2020-01-21 12:00:00,137.98,217.40200000000002,59.09,32.641 -2020-01-21 12:15:00,136.33,217.08900000000003,59.09,32.641 -2020-01-21 12:30:00,134.76,216.75599999999997,59.09,32.641 -2020-01-21 12:45:00,135.83,217.27200000000002,59.09,32.641 -2020-01-21 13:00:00,136.03,214.812,60.21,32.641 -2020-01-21 13:15:00,137.42,214.06099999999998,60.21,32.641 -2020-01-21 13:30:00,136.56,213.168,60.21,32.641 -2020-01-21 13:45:00,136.51,213.452,60.21,32.641 -2020-01-21 14:00:00,133.43,213.22400000000002,60.673,32.641 -2020-01-21 14:15:00,132.93,213.732,60.673,32.641 -2020-01-21 14:30:00,131.47,214.611,60.673,32.641 -2020-01-21 14:45:00,132.26,215.398,60.673,32.641 -2020-01-21 15:00:00,133.45,216.356,62.232,32.641 -2020-01-21 15:15:00,131.81,217.135,62.232,32.641 -2020-01-21 15:30:00,129.92,218.83700000000002,62.232,32.641 -2020-01-21 15:45:00,129.62,219.72,62.232,32.641 -2020-01-21 16:00:00,130.89,221.453,63.611999999999995,32.641 -2020-01-21 16:15:00,130.05,223.34099999999998,63.611999999999995,32.641 -2020-01-21 16:30:00,131.99,226.138,63.611999999999995,32.641 -2020-01-21 16:45:00,133.7,227.4,63.611999999999995,32.641 -2020-01-21 17:00:00,141.22,229.834,70.658,32.641 -2020-01-21 17:15:00,139.73,230.655,70.658,32.641 -2020-01-21 17:30:00,139.98,231.72299999999998,70.658,32.641 -2020-01-21 17:45:00,140.83,231.653,70.658,32.641 -2020-01-21 18:00:00,139.47,233.438,68.361,32.641 -2020-01-21 18:15:00,137.77,230.11900000000003,68.361,32.641 -2020-01-21 18:30:00,136.43,229.109,68.361,32.641 -2020-01-21 18:45:00,136.05,229.52200000000002,68.361,32.641 -2020-01-21 19:00:00,132.77,227.708,62.922,32.641 -2020-01-21 19:15:00,131.36,223.574,62.922,32.641 -2020-01-21 19:30:00,136.19,220.53799999999998,62.922,32.641 -2020-01-21 19:45:00,137.49,217.548,62.922,32.641 -2020-01-21 20:00:00,125.64,213.32299999999998,63.251999999999995,32.641 -2020-01-21 20:15:00,119.24,206.47299999999998,63.251999999999995,32.641 -2020-01-21 20:30:00,114.43,202.202,63.251999999999995,32.641 -2020-01-21 20:45:00,114.99,201.021,63.251999999999995,32.641 -2020-01-21 21:00:00,107.66,197.293,54.47,32.641 -2020-01-21 21:15:00,113.17,194.36599999999999,54.47,32.641 -2020-01-21 21:30:00,112.31,192.32,54.47,32.641 -2020-01-21 21:45:00,106.88,190.829,54.47,32.641 -2020-01-21 22:00:00,100.25,183.455,51.12,32.641 -2020-01-21 22:15:00,96.44,177.588,51.12,32.641 -2020-01-21 22:30:00,95.56,163.696,51.12,32.641 -2020-01-21 22:45:00,98.08,155.245,51.12,32.641 -2020-01-21 23:00:00,92.47,147.793,42.156000000000006,32.641 -2020-01-21 23:15:00,91.54,146.417,42.156000000000006,32.641 -2020-01-21 23:30:00,85.61,146.672,42.156000000000006,32.641 -2020-01-21 23:45:00,89.01,146.222,42.156000000000006,32.641 -2020-01-22 00:00:00,85.62,139.774,37.192,32.641 -2020-01-22 00:15:00,81.83,139.43200000000002,37.192,32.641 -2020-01-22 00:30:00,78.79,140.88,37.192,32.641 -2020-01-22 00:45:00,84.5,142.636,37.192,32.641 -2020-01-22 01:00:00,81.61,145.673,32.24,32.641 -2020-01-22 01:15:00,82.94,145.59799999999998,32.24,32.641 -2020-01-22 01:30:00,75.82,145.93200000000002,32.24,32.641 -2020-01-22 01:45:00,81.67,146.376,32.24,32.641 -2020-01-22 02:00:00,79.49,148.722,30.34,32.641 -2020-01-22 02:15:00,76.77,150.81799999999998,30.34,32.641 -2020-01-22 02:30:00,76.68,151.94299999999998,30.34,32.641 -2020-01-22 02:45:00,75.36,154.127,30.34,32.641 -2020-01-22 03:00:00,80.37,157.10399999999998,29.129,32.641 -2020-01-22 03:15:00,81.93,158.24,29.129,32.641 -2020-01-22 03:30:00,80.31,160.061,29.129,32.641 -2020-01-22 03:45:00,81.37,162.04399999999998,29.129,32.641 -2020-01-22 04:00:00,84.25,173.516,30.075,32.641 -2020-01-22 04:15:00,85.11,185.007,30.075,32.641 -2020-01-22 04:30:00,83.8,188.795,30.075,32.641 -2020-01-22 04:45:00,87.55,191.895,30.075,32.641 -2020-01-22 05:00:00,91.73,226.801,35.684,32.641 -2020-01-22 05:15:00,94.75,254.644,35.684,32.641 -2020-01-22 05:30:00,92.38,250.34900000000002,35.684,32.641 -2020-01-22 05:45:00,96.78,243.924,35.684,32.641 -2020-01-22 06:00:00,106.02,241.03900000000002,51.49,32.641 -2020-01-22 06:15:00,111.14,247.24400000000003,51.49,32.641 -2020-01-22 06:30:00,115.37,250.47799999999998,51.49,32.641 -2020-01-22 06:45:00,120.3,255.707,51.49,32.641 -2020-01-22 07:00:00,127.38,254.05900000000003,68.242,32.641 -2020-01-22 07:15:00,127.79,259.843,68.242,32.641 -2020-01-22 07:30:00,130.4,263.16,68.242,32.641 -2020-01-22 07:45:00,128.61,264.947,68.242,32.641 -2020-01-22 08:00:00,135.29,263.547,63.619,32.641 -2020-01-22 08:15:00,132.82,263.91200000000003,63.619,32.641 -2020-01-22 08:30:00,133.56,261.265,63.619,32.641 -2020-01-22 08:45:00,131.49,258.42,63.619,32.641 -2020-01-22 09:00:00,132.7,251.735,61.333,32.641 -2020-01-22 09:15:00,134.67,248.925,61.333,32.641 -2020-01-22 09:30:00,136.96,247.155,61.333,32.641 -2020-01-22 09:45:00,137.72,243.732,61.333,32.641 -2020-01-22 10:00:00,135.4,237.84,59.663000000000004,32.641 -2020-01-22 10:15:00,135.86,234.06,59.663000000000004,32.641 -2020-01-22 10:30:00,135.23,230.037,59.663000000000004,32.641 -2020-01-22 10:45:00,135.61,228.18599999999998,59.663000000000004,32.641 -2020-01-22 11:00:00,134.31,225.054,59.771,32.641 -2020-01-22 11:15:00,134.36,223.733,59.771,32.641 -2020-01-22 11:30:00,133.1,221.829,59.771,32.641 -2020-01-22 11:45:00,129.04,221.201,59.771,32.641 -2020-01-22 12:00:00,125.42,217.24900000000002,58.723,32.641 -2020-01-22 12:15:00,124.01,216.957,58.723,32.641 -2020-01-22 12:30:00,120.56,216.608,58.723,32.641 -2020-01-22 12:45:00,119.44,217.122,58.723,32.641 -2020-01-22 13:00:00,117.85,214.667,58.727,32.641 -2020-01-22 13:15:00,117.79,213.896,58.727,32.641 -2020-01-22 13:30:00,112.65,212.987,58.727,32.641 -2020-01-22 13:45:00,116.56,213.269,58.727,32.641 -2020-01-22 14:00:00,117.4,213.078,59.803999999999995,32.641 -2020-01-22 14:15:00,121.18,213.571,59.803999999999995,32.641 -2020-01-22 14:30:00,122.83,214.44799999999998,59.803999999999995,32.641 -2020-01-22 14:45:00,124.29,215.252,59.803999999999995,32.641 -2020-01-22 15:00:00,121.07,216.226,61.05,32.641 -2020-01-22 15:15:00,118.8,216.98,61.05,32.641 -2020-01-22 15:30:00,120.0,218.66099999999997,61.05,32.641 -2020-01-22 15:45:00,121.57,219.53,61.05,32.641 -2020-01-22 16:00:00,124.33,221.263,64.012,32.641 -2020-01-22 16:15:00,123.18,223.155,64.012,32.641 -2020-01-22 16:30:00,126.48,225.959,64.012,32.641 -2020-01-22 16:45:00,129.37,227.22,64.012,32.641 -2020-01-22 17:00:00,135.39,229.643,66.751,32.641 -2020-01-22 17:15:00,135.01,230.495,66.751,32.641 -2020-01-22 17:30:00,137.03,231.59799999999998,66.751,32.641 -2020-01-22 17:45:00,138.51,231.55200000000002,66.751,32.641 -2020-01-22 18:00:00,137.18,233.358,65.91199999999999,32.641 -2020-01-22 18:15:00,135.84,230.067,65.91199999999999,32.641 -2020-01-22 18:30:00,133.59,229.06,65.91199999999999,32.641 -2020-01-22 18:45:00,134.99,229.49599999999998,65.91199999999999,32.641 -2020-01-22 19:00:00,132.74,227.643,63.324,32.641 -2020-01-22 19:15:00,130.93,223.517,63.324,32.641 -2020-01-22 19:30:00,136.71,220.49200000000002,63.324,32.641 -2020-01-22 19:45:00,135.36,217.517,63.324,32.641 -2020-01-22 20:00:00,124.6,213.267,63.573,32.641 -2020-01-22 20:15:00,117.48,206.424,63.573,32.641 -2020-01-22 20:30:00,112.16,202.15200000000002,63.573,32.641 -2020-01-22 20:45:00,113.32,200.983,63.573,32.641 -2020-01-22 21:00:00,105.36,197.236,55.073,32.641 -2020-01-22 21:15:00,110.61,194.291,55.073,32.641 -2020-01-22 21:30:00,110.22,192.24599999999998,55.073,32.641 -2020-01-22 21:45:00,104.07,190.769,55.073,32.641 -2020-01-22 22:00:00,99.2,183.387,51.321999999999996,32.641 -2020-01-22 22:15:00,94.35,177.542,51.321999999999996,32.641 -2020-01-22 22:30:00,90.06,163.64,51.321999999999996,32.641 -2020-01-22 22:45:00,89.79,155.197,51.321999999999996,32.641 -2020-01-22 23:00:00,82.0,147.72899999999998,42.09,32.641 -2020-01-22 23:15:00,82.55,146.36700000000002,42.09,32.641 -2020-01-22 23:30:00,87.0,146.639,42.09,32.641 -2020-01-22 23:45:00,88.39,146.19899999999998,42.09,32.641 -2020-01-23 00:00:00,83.32,139.778,38.399,32.641 -2020-01-23 00:15:00,78.11,139.42,38.399,32.641 -2020-01-23 00:30:00,80.92,140.849,38.399,32.641 -2020-01-23 00:45:00,83.96,142.593,38.399,32.641 -2020-01-23 01:00:00,80.81,145.619,36.94,32.641 -2020-01-23 01:15:00,75.12,145.533,36.94,32.641 -2020-01-23 01:30:00,75.03,145.861,36.94,32.641 -2020-01-23 01:45:00,72.02,146.297,36.94,32.641 -2020-01-23 02:00:00,77.81,148.651,35.275,32.641 -2020-01-23 02:15:00,79.24,150.75,35.275,32.641 -2020-01-23 02:30:00,80.05,151.886,35.275,32.641 -2020-01-23 02:45:00,75.79,154.06799999999998,35.275,32.641 -2020-01-23 03:00:00,71.98,157.043,35.329,32.641 -2020-01-23 03:15:00,76.32,158.19299999999998,35.329,32.641 -2020-01-23 03:30:00,81.55,160.012,35.329,32.641 -2020-01-23 03:45:00,81.7,162.007,35.329,32.641 -2020-01-23 04:00:00,79.93,173.46,36.275,32.641 -2020-01-23 04:15:00,78.74,184.93900000000002,36.275,32.641 -2020-01-23 04:30:00,85.06,188.732,36.275,32.641 -2020-01-23 04:45:00,87.85,191.82299999999998,36.275,32.641 -2020-01-23 05:00:00,88.57,226.69,42.193999999999996,32.641 -2020-01-23 05:15:00,86.51,254.516,42.193999999999996,32.641 -2020-01-23 05:30:00,89.63,250.215,42.193999999999996,32.641 -2020-01-23 05:45:00,95.12,243.808,42.193999999999996,32.641 -2020-01-23 06:00:00,102.82,240.94299999999998,56.422,32.641 -2020-01-23 06:15:00,109.43,247.16,56.422,32.641 -2020-01-23 06:30:00,111.99,250.392,56.422,32.641 -2020-01-23 06:45:00,117.62,255.642,56.422,32.641 -2020-01-23 07:00:00,125.86,254.014,72.569,32.641 -2020-01-23 07:15:00,132.26,259.78,72.569,32.641 -2020-01-23 07:30:00,134.8,263.07,72.569,32.641 -2020-01-23 07:45:00,132.33,264.827,72.569,32.641 -2020-01-23 08:00:00,134.07,263.414,67.704,32.641 -2020-01-23 08:15:00,132.87,263.758,67.704,32.641 -2020-01-23 08:30:00,136.34,261.065,67.704,32.641 -2020-01-23 08:45:00,132.86,258.20599999999996,67.704,32.641 -2020-01-23 09:00:00,133.61,251.513,63.434,32.641 -2020-01-23 09:15:00,133.41,248.707,63.434,32.641 -2020-01-23 09:30:00,135.43,246.957,63.434,32.641 -2020-01-23 09:45:00,135.05,243.53,63.434,32.641 -2020-01-23 10:00:00,134.75,237.644,61.88399999999999,32.641 -2020-01-23 10:15:00,136.32,233.87900000000002,61.88399999999999,32.641 -2020-01-23 10:30:00,134.68,229.854,61.88399999999999,32.641 -2020-01-23 10:45:00,136.01,228.01,61.88399999999999,32.641 -2020-01-23 11:00:00,136.01,224.862,61.481,32.641 -2020-01-23 11:15:00,134.32,223.547,61.481,32.641 -2020-01-23 11:30:00,130.61,221.64700000000002,61.481,32.641 -2020-01-23 11:45:00,133.98,221.024,61.481,32.641 -2020-01-23 12:00:00,134.51,217.088,59.527,32.641 -2020-01-23 12:15:00,132.67,216.815,59.527,32.641 -2020-01-23 12:30:00,132.94,216.449,59.527,32.641 -2020-01-23 12:45:00,133.32,216.96200000000002,59.527,32.641 -2020-01-23 13:00:00,132.61,214.511,58.794,32.641 -2020-01-23 13:15:00,132.78,213.72099999999998,58.794,32.641 -2020-01-23 13:30:00,128.88,212.799,58.794,32.641 -2020-01-23 13:45:00,128.87,213.078,58.794,32.641 -2020-01-23 14:00:00,127.62,212.922,60.32,32.641 -2020-01-23 14:15:00,129.42,213.40200000000002,60.32,32.641 -2020-01-23 14:30:00,128.31,214.275,60.32,32.641 -2020-01-23 14:45:00,128.39,215.09599999999998,60.32,32.641 -2020-01-23 15:00:00,129.2,216.08700000000002,62.52,32.641 -2020-01-23 15:15:00,128.37,216.81400000000002,62.52,32.641 -2020-01-23 15:30:00,125.84,218.475,62.52,32.641 -2020-01-23 15:45:00,125.84,219.328,62.52,32.641 -2020-01-23 16:00:00,127.22,221.063,64.199,32.641 -2020-01-23 16:15:00,127.3,222.956,64.199,32.641 -2020-01-23 16:30:00,128.99,225.766,64.199,32.641 -2020-01-23 16:45:00,131.38,227.025,64.199,32.641 -2020-01-23 17:00:00,137.71,229.438,68.19800000000001,32.641 -2020-01-23 17:15:00,137.9,230.322,68.19800000000001,32.641 -2020-01-23 17:30:00,140.75,231.459,68.19800000000001,32.641 -2020-01-23 17:45:00,138.94,231.43900000000002,68.19800000000001,32.641 -2020-01-23 18:00:00,139.21,233.265,67.899,32.641 -2020-01-23 18:15:00,140.6,230.005,67.899,32.641 -2020-01-23 18:30:00,135.9,229.002,67.899,32.641 -2020-01-23 18:45:00,134.83,229.46099999999998,67.899,32.641 -2020-01-23 19:00:00,132.4,227.56799999999998,64.72399999999999,32.641 -2020-01-23 19:15:00,130.3,223.447,64.72399999999999,32.641 -2020-01-23 19:30:00,136.27,220.435,64.72399999999999,32.641 -2020-01-23 19:45:00,136.38,217.476,64.72399999999999,32.641 -2020-01-23 20:00:00,124.03,213.202,64.062,32.641 -2020-01-23 20:15:00,118.44,206.364,64.062,32.641 -2020-01-23 20:30:00,114.15,202.092,64.062,32.641 -2020-01-23 20:45:00,111.79,200.937,64.062,32.641 -2020-01-23 21:00:00,107.54,197.17,57.971000000000004,32.641 -2020-01-23 21:15:00,110.3,194.208,57.971000000000004,32.641 -2020-01-23 21:30:00,111.1,192.162,57.971000000000004,32.641 -2020-01-23 21:45:00,106.2,190.703,57.971000000000004,32.641 -2020-01-23 22:00:00,98.72,183.31099999999998,53.715,32.641 -2020-01-23 22:15:00,96.15,177.487,53.715,32.641 -2020-01-23 22:30:00,92.08,163.576,53.715,32.641 -2020-01-23 22:45:00,91.81,155.14,53.715,32.641 -2020-01-23 23:00:00,94.02,147.657,47.8,32.641 -2020-01-23 23:15:00,94.02,146.306,47.8,32.641 -2020-01-23 23:30:00,89.04,146.597,47.8,32.641 -2020-01-23 23:45:00,85.37,146.167,47.8,32.641 -2020-01-24 00:00:00,86.99,138.808,43.656000000000006,32.641 -2020-01-24 00:15:00,84.7,138.631,43.656000000000006,32.641 -2020-01-24 00:30:00,84.43,139.84,43.656000000000006,32.641 -2020-01-24 00:45:00,78.25,141.634,43.656000000000006,32.641 -2020-01-24 01:00:00,78.02,144.345,41.263000000000005,32.641 -2020-01-24 01:15:00,81.93,145.425,41.263000000000005,32.641 -2020-01-24 01:30:00,81.42,145.365,41.263000000000005,32.641 -2020-01-24 01:45:00,76.21,145.961,41.263000000000005,32.641 -2020-01-24 02:00:00,78.49,148.269,40.799,32.641 -2020-01-24 02:15:00,80.51,150.241,40.799,32.641 -2020-01-24 02:30:00,80.14,151.866,40.799,32.641 -2020-01-24 02:45:00,75.42,154.216,40.799,32.641 -2020-01-24 03:00:00,74.46,155.875,41.398,32.641 -2020-01-24 03:15:00,81.27,158.363,41.398,32.641 -2020-01-24 03:30:00,82.2,160.202,41.398,32.641 -2020-01-24 03:45:00,82.59,162.441,41.398,32.641 -2020-01-24 04:00:00,78.55,174.128,42.38,32.641 -2020-01-24 04:15:00,81.62,185.585,42.38,32.641 -2020-01-24 04:30:00,86.32,189.485,42.38,32.641 -2020-01-24 04:45:00,88.91,191.32299999999998,42.38,32.641 -2020-01-24 05:00:00,90.44,224.72799999999998,46.181000000000004,32.641 -2020-01-24 05:15:00,88.25,254.162,46.181000000000004,32.641 -2020-01-24 05:30:00,91.02,251.11,46.181000000000004,32.641 -2020-01-24 05:45:00,95.66,244.72299999999998,46.181000000000004,32.641 -2020-01-24 06:00:00,105.46,242.351,59.33,32.641 -2020-01-24 06:15:00,108.13,246.793,59.33,32.641 -2020-01-24 06:30:00,112.97,249.0,59.33,32.641 -2020-01-24 06:45:00,118.74,256.23400000000004,59.33,32.641 -2020-01-24 07:00:00,125.57,253.513,72.454,32.641 -2020-01-24 07:15:00,127.25,260.314,72.454,32.641 -2020-01-24 07:30:00,132.41,263.71299999999997,72.454,32.641 -2020-01-24 07:45:00,133.17,264.42,72.454,32.641 -2020-01-24 08:00:00,137.53,261.54900000000004,67.175,32.641 -2020-01-24 08:15:00,135.9,261.298,67.175,32.641 -2020-01-24 08:30:00,136.3,259.728,67.175,32.641 -2020-01-24 08:45:00,136.6,255.017,67.175,32.641 -2020-01-24 09:00:00,137.1,249.24099999999999,65.365,32.641 -2020-01-24 09:15:00,138.33,246.803,65.365,32.641 -2020-01-24 09:30:00,139.13,244.678,65.365,32.641 -2020-01-24 09:45:00,139.22,241.058,65.365,32.641 -2020-01-24 10:00:00,138.08,233.864,63.95,32.641 -2020-01-24 10:15:00,138.61,230.97299999999998,63.95,32.641 -2020-01-24 10:30:00,138.28,226.773,63.95,32.641 -2020-01-24 10:45:00,137.57,224.433,63.95,32.641 -2020-01-24 11:00:00,138.32,221.21900000000002,63.92100000000001,32.641 -2020-01-24 11:15:00,138.78,219.02900000000002,63.92100000000001,32.641 -2020-01-24 11:30:00,139.38,219.236,63.92100000000001,32.641 -2020-01-24 11:45:00,138.06,218.825,63.92100000000001,32.641 -2020-01-24 12:00:00,136.09,216.109,60.79600000000001,32.641 -2020-01-24 12:15:00,134.0,213.50099999999998,60.79600000000001,32.641 -2020-01-24 12:30:00,131.28,213.28599999999997,60.79600000000001,32.641 -2020-01-24 12:45:00,132.51,214.524,60.79600000000001,32.641 -2020-01-24 13:00:00,130.47,213.08599999999998,59.393,32.641 -2020-01-24 13:15:00,129.98,213.205,59.393,32.641 -2020-01-24 13:30:00,128.13,212.16400000000002,59.393,32.641 -2020-01-24 13:45:00,128.71,212.31799999999998,59.393,32.641 -2020-01-24 14:00:00,126.8,210.986,57.943999999999996,32.641 -2020-01-24 14:15:00,129.56,211.17700000000002,57.943999999999996,32.641 -2020-01-24 14:30:00,127.19,212.415,57.943999999999996,32.641 -2020-01-24 14:45:00,127.61,213.71200000000002,57.943999999999996,32.641 -2020-01-24 15:00:00,129.17,214.165,60.153999999999996,32.641 -2020-01-24 15:15:00,125.93,214.391,60.153999999999996,32.641 -2020-01-24 15:30:00,127.46,214.347,60.153999999999996,32.641 -2020-01-24 15:45:00,126.28,215.24400000000003,60.153999999999996,32.641 -2020-01-24 16:00:00,127.34,215.765,62.933,32.641 -2020-01-24 16:15:00,126.66,217.925,62.933,32.641 -2020-01-24 16:30:00,128.07,220.878,62.933,32.641 -2020-01-24 16:45:00,131.69,222.109,62.933,32.641 -2020-01-24 17:00:00,137.39,224.52900000000002,68.657,32.641 -2020-01-24 17:15:00,135.66,224.99599999999998,68.657,32.641 -2020-01-24 17:30:00,133.93,225.78099999999998,68.657,32.641 -2020-01-24 17:45:00,135.2,225.53799999999998,68.657,32.641 -2020-01-24 18:00:00,136.43,228.196,67.111,32.641 -2020-01-24 18:15:00,134.39,224.639,67.111,32.641 -2020-01-24 18:30:00,133.68,224.102,67.111,32.641 -2020-01-24 18:45:00,133.53,224.52700000000002,67.111,32.641 -2020-01-24 19:00:00,130.37,223.533,62.434,32.641 -2020-01-24 19:15:00,128.1,220.89700000000002,62.434,32.641 -2020-01-24 19:30:00,130.9,217.423,62.434,32.641 -2020-01-24 19:45:00,136.04,214.078,62.434,32.641 -2020-01-24 20:00:00,125.76,209.85,61.763000000000005,32.641 -2020-01-24 20:15:00,117.6,202.93,61.763000000000005,32.641 -2020-01-24 20:30:00,114.72,198.669,61.763000000000005,32.641 -2020-01-24 20:45:00,110.33,198.265,61.763000000000005,32.641 -2020-01-24 21:00:00,104.51,194.895,56.785,32.641 -2020-01-24 21:15:00,102.91,192.205,56.785,32.641 -2020-01-24 21:30:00,102.15,190.22799999999998,56.785,32.641 -2020-01-24 21:45:00,104.05,189.38099999999997,56.785,32.641 -2020-01-24 22:00:00,99.83,183.11700000000002,52.693000000000005,32.641 -2020-01-24 22:15:00,94.61,177.195,52.693000000000005,32.641 -2020-01-24 22:30:00,89.29,169.947,52.693000000000005,32.641 -2020-01-24 22:45:00,85.86,165.43400000000003,52.693000000000005,32.641 -2020-01-24 23:00:00,82.29,157.219,45.443999999999996,32.641 -2020-01-24 23:15:00,81.42,153.85399999999998,45.443999999999996,32.641 -2020-01-24 23:30:00,80.16,152.725,45.443999999999996,32.641 -2020-01-24 23:45:00,77.47,151.564,45.443999999999996,32.641 -2020-01-25 00:00:00,72.4,135.27200000000002,44.738,32.459 -2020-01-25 00:15:00,71.11,130.214,44.738,32.459 -2020-01-25 00:30:00,72.19,133.029,44.738,32.459 -2020-01-25 00:45:00,70.85,135.756,44.738,32.459 -2020-01-25 01:00:00,71.22,139.132,40.303000000000004,32.459 -2020-01-25 01:15:00,72.27,138.951,40.303000000000004,32.459 -2020-01-25 01:30:00,76.25,138.406,40.303000000000004,32.459 -2020-01-25 01:45:00,75.27,138.54399999999998,40.303000000000004,32.459 -2020-01-25 02:00:00,69.95,141.80200000000002,38.61,32.459 -2020-01-25 02:15:00,68.12,143.468,38.61,32.459 -2020-01-25 02:30:00,65.73,143.929,38.61,32.459 -2020-01-25 02:45:00,65.96,146.268,38.61,32.459 -2020-01-25 03:00:00,64.75,148.87,37.554,32.459 -2020-01-25 03:15:00,65.49,150.054,37.554,32.459 -2020-01-25 03:30:00,64.8,149.96,37.554,32.459 -2020-01-25 03:45:00,65.17,152.093,37.554,32.459 -2020-01-25 04:00:00,64.99,159.15,37.176,32.459 -2020-01-25 04:15:00,65.24,167.739,37.176,32.459 -2020-01-25 04:30:00,65.75,169.365,37.176,32.459 -2020-01-25 04:45:00,66.44,170.56,37.176,32.459 -2020-01-25 05:00:00,66.97,186.34799999999998,36.893,32.459 -2020-01-25 05:15:00,67.29,195.11700000000002,36.893,32.459 -2020-01-25 05:30:00,68.56,192.21200000000002,36.893,32.459 -2020-01-25 05:45:00,69.68,191.523,36.893,32.459 -2020-01-25 06:00:00,69.94,209.548,37.803000000000004,32.459 -2020-01-25 06:15:00,70.69,232.051,37.803000000000004,32.459 -2020-01-25 06:30:00,71.35,228.36900000000003,37.803000000000004,32.459 -2020-01-25 06:45:00,73.5,225.519,37.803000000000004,32.459 -2020-01-25 07:00:00,77.71,218.623,41.086999999999996,32.459 -2020-01-25 07:15:00,78.56,224.142,41.086999999999996,32.459 -2020-01-25 07:30:00,82.94,230.46200000000002,41.086999999999996,32.459 -2020-01-25 07:45:00,84.55,235.69099999999997,41.086999999999996,32.459 -2020-01-25 08:00:00,88.98,237.581,48.222,32.459 -2020-01-25 08:15:00,90.69,241.622,48.222,32.459 -2020-01-25 08:30:00,92.22,241.90599999999998,48.222,32.459 -2020-01-25 08:45:00,95.97,240.745,48.222,32.459 -2020-01-25 09:00:00,98.38,236.59099999999998,52.791000000000004,32.459 -2020-01-25 09:15:00,98.75,234.963,52.791000000000004,32.459 -2020-01-25 09:30:00,99.49,233.83700000000002,52.791000000000004,32.459 -2020-01-25 09:45:00,101.44,230.472,52.791000000000004,32.459 -2020-01-25 10:00:00,102.45,223.484,54.341,32.459 -2020-01-25 10:15:00,103.89,220.745,54.341,32.459 -2020-01-25 10:30:00,102.9,216.793,54.341,32.459 -2020-01-25 10:45:00,104.81,216.046,54.341,32.459 -2020-01-25 11:00:00,103.72,213.127,51.94,32.459 -2020-01-25 11:15:00,105.87,210.00900000000001,51.94,32.459 -2020-01-25 11:30:00,107.38,208.875,51.94,32.459 -2020-01-25 11:45:00,106.98,207.255,51.94,32.459 -2020-01-25 12:00:00,104.05,203.44,50.973,32.459 -2020-01-25 12:15:00,103.31,201.46900000000002,50.973,32.459 -2020-01-25 12:30:00,100.71,201.627,50.973,32.459 -2020-01-25 12:45:00,99.96,201.85299999999998,50.973,32.459 -2020-01-25 13:00:00,97.04,200.072,48.06399999999999,32.459 -2020-01-25 13:15:00,97.34,197.856,48.06399999999999,32.459 -2020-01-25 13:30:00,95.63,196.245,48.06399999999999,32.459 -2020-01-25 13:45:00,94.67,197.16,48.06399999999999,32.459 -2020-01-25 14:00:00,91.46,197.327,45.707,32.459 -2020-01-25 14:15:00,92.11,197.06900000000002,45.707,32.459 -2020-01-25 14:30:00,91.72,196.25,45.707,32.459 -2020-01-25 14:45:00,91.73,197.745,45.707,32.459 -2020-01-25 15:00:00,90.8,198.975,47.567,32.459 -2020-01-25 15:15:00,90.03,200.00599999999997,47.567,32.459 -2020-01-25 15:30:00,89.45,201.695,47.567,32.459 -2020-01-25 15:45:00,88.86,202.729,47.567,32.459 -2020-01-25 16:00:00,90.25,201.65900000000002,52.031000000000006,32.459 -2020-01-25 16:15:00,89.46,204.97400000000002,52.031000000000006,32.459 -2020-01-25 16:30:00,90.4,207.842,52.031000000000006,32.459 -2020-01-25 16:45:00,93.84,210.09400000000002,52.031000000000006,32.459 -2020-01-25 17:00:00,100.62,212.105,58.218999999999994,32.459 -2020-01-25 17:15:00,102.8,214.77700000000002,58.218999999999994,32.459 -2020-01-25 17:30:00,106.7,215.502,58.218999999999994,32.459 -2020-01-25 17:45:00,106.99,214.757,58.218999999999994,32.459 -2020-01-25 18:00:00,107.5,216.775,57.65,32.459 -2020-01-25 18:15:00,107.19,215.09,57.65,32.459 -2020-01-25 18:30:00,109.91,215.905,57.65,32.459 -2020-01-25 18:45:00,105.29,212.97400000000002,57.65,32.459 -2020-01-25 19:00:00,104.0,213.22099999999998,51.261,32.459 -2020-01-25 19:15:00,102.55,210.149,51.261,32.459 -2020-01-25 19:30:00,101.86,207.44799999999998,51.261,32.459 -2020-01-25 19:45:00,102.2,203.72,51.261,32.459 -2020-01-25 20:00:00,94.93,201.78,44.068000000000005,32.459 -2020-01-25 20:15:00,92.01,197.358,44.068000000000005,32.459 -2020-01-25 20:30:00,88.94,192.80700000000002,44.068000000000005,32.459 -2020-01-25 20:45:00,87.29,191.732,44.068000000000005,32.459 -2020-01-25 21:00:00,83.53,191.08,38.861,32.459 -2020-01-25 21:15:00,82.66,188.91299999999998,38.861,32.459 -2020-01-25 21:30:00,80.26,188.308,38.861,32.459 -2020-01-25 21:45:00,80.05,187.084,38.861,32.459 -2020-01-25 22:00:00,77.81,182.33900000000003,39.485,32.459 -2020-01-25 22:15:00,76.34,179.227,39.485,32.459 -2020-01-25 22:30:00,73.65,179.05900000000003,39.485,32.459 -2020-01-25 22:45:00,72.2,176.628,39.485,32.459 -2020-01-25 23:00:00,67.88,171.222,32.027,32.459 -2020-01-25 23:15:00,68.21,165.968,32.027,32.459 -2020-01-25 23:30:00,62.43,162.59,32.027,32.459 -2020-01-25 23:45:00,63.07,158.65,32.027,32.459 -2020-01-26 00:00:00,60.64,135.531,26.96,32.459 -2020-01-26 00:15:00,59.92,130.25799999999998,26.96,32.459 -2020-01-26 00:30:00,59.03,132.641,26.96,32.459 -2020-01-26 00:45:00,58.62,136.18200000000002,26.96,32.459 -2020-01-26 01:00:00,54.93,139.338,24.295,32.459 -2020-01-26 01:15:00,55.99,140.374,24.295,32.459 -2020-01-26 01:30:00,55.67,140.451,24.295,32.459 -2020-01-26 01:45:00,54.2,140.276,24.295,32.459 -2020-01-26 02:00:00,53.39,142.659,24.268,32.459 -2020-01-26 02:15:00,54.08,143.218,24.268,32.459 -2020-01-26 02:30:00,52.98,144.661,24.268,32.459 -2020-01-26 02:45:00,53.02,147.586,24.268,32.459 -2020-01-26 03:00:00,53.08,150.45600000000002,23.373,32.459 -2020-01-26 03:15:00,50.37,150.995,23.373,32.459 -2020-01-26 03:30:00,52.45,152.66,23.373,32.459 -2020-01-26 03:45:00,52.94,154.84,23.373,32.459 -2020-01-26 04:00:00,52.89,161.61,23.874000000000002,32.459 -2020-01-26 04:15:00,53.22,169.06799999999998,23.874000000000002,32.459 -2020-01-26 04:30:00,54.0,170.55700000000002,23.874000000000002,32.459 -2020-01-26 04:45:00,54.34,172.14,23.874000000000002,32.459 -2020-01-26 05:00:00,55.82,183.72400000000002,24.871,32.459 -2020-01-26 05:15:00,56.59,189.75599999999997,24.871,32.459 -2020-01-26 05:30:00,55.44,186.71400000000003,24.871,32.459 -2020-01-26 05:45:00,56.61,186.375,24.871,32.459 -2020-01-26 06:00:00,56.99,204.688,23.84,32.459 -2020-01-26 06:15:00,56.78,225.035,23.84,32.459 -2020-01-26 06:30:00,57.8,220.16099999999997,23.84,32.459 -2020-01-26 06:45:00,58.26,216.248,23.84,32.459 -2020-01-26 07:00:00,61.45,212.21599999999998,27.430999999999997,32.459 -2020-01-26 07:15:00,62.62,216.979,27.430999999999997,32.459 -2020-01-26 07:30:00,64.42,221.612,27.430999999999997,32.459 -2020-01-26 07:45:00,65.23,225.917,27.430999999999997,32.459 -2020-01-26 08:00:00,67.41,229.857,33.891999999999996,32.459 -2020-01-26 08:15:00,70.28,233.58700000000002,33.891999999999996,32.459 -2020-01-26 08:30:00,72.53,235.627,33.891999999999996,32.459 -2020-01-26 08:45:00,74.96,236.83,33.891999999999996,32.459 -2020-01-26 09:00:00,76.29,232.225,37.571,32.459 -2020-01-26 09:15:00,77.45,231.31799999999998,37.571,32.459 -2020-01-26 09:30:00,78.94,229.967,37.571,32.459 -2020-01-26 09:45:00,80.75,226.315,37.571,32.459 -2020-01-26 10:00:00,81.35,222.179,40.594,32.459 -2020-01-26 10:15:00,83.76,220.058,40.594,32.459 -2020-01-26 10:30:00,85.49,216.775,40.594,32.459 -2020-01-26 10:45:00,88.12,213.672,40.594,32.459 -2020-01-26 11:00:00,88.23,211.86900000000003,44.133,32.459 -2020-01-26 11:15:00,92.2,208.975,44.133,32.459 -2020-01-26 11:30:00,92.74,206.762,44.133,32.459 -2020-01-26 11:45:00,93.24,205.81400000000002,44.133,32.459 -2020-01-26 12:00:00,91.73,201.201,41.198,32.459 -2020-01-26 12:15:00,89.36,201.549,41.198,32.459 -2020-01-26 12:30:00,84.83,199.968,41.198,32.459 -2020-01-26 12:45:00,83.74,199.162,41.198,32.459 -2020-01-26 13:00:00,80.41,196.597,37.014,32.459 -2020-01-26 13:15:00,80.17,197.945,37.014,32.459 -2020-01-26 13:30:00,81.07,196.236,37.014,32.459 -2020-01-26 13:45:00,80.59,196.222,37.014,32.459 -2020-01-26 14:00:00,79.11,196.545,34.934,32.459 -2020-01-26 14:15:00,78.6,197.6,34.934,32.459 -2020-01-26 14:30:00,78.67,198.37900000000002,34.934,32.459 -2020-01-26 14:45:00,79.38,199.56599999999997,34.934,32.459 -2020-01-26 15:00:00,79.93,199.014,34.588,32.459 -2020-01-26 15:15:00,78.78,201.0,34.588,32.459 -2020-01-26 15:30:00,78.6,203.368,34.588,32.459 -2020-01-26 15:45:00,77.74,205.146,34.588,32.459 -2020-01-26 16:00:00,80.38,206.40099999999998,37.874,32.459 -2020-01-26 16:15:00,79.54,208.645,37.874,32.459 -2020-01-26 16:30:00,81.49,211.685,37.874,32.459 -2020-01-26 16:45:00,84.68,214.078,37.874,32.459 -2020-01-26 17:00:00,92.75,215.987,47.303999999999995,32.459 -2020-01-26 17:15:00,94.5,218.13099999999997,47.303999999999995,32.459 -2020-01-26 17:30:00,96.47,219.12099999999998,47.303999999999995,32.459 -2020-01-26 17:45:00,98.7,220.93200000000002,47.303999999999995,32.459 -2020-01-26 18:00:00,100.59,222.25099999999998,48.879,32.459 -2020-01-26 18:15:00,99.21,222.146,48.879,32.459 -2020-01-26 18:30:00,99.63,220.636,48.879,32.459 -2020-01-26 18:45:00,98.32,219.80900000000003,48.879,32.459 -2020-01-26 19:00:00,96.9,219.37599999999998,44.826,32.459 -2020-01-26 19:15:00,95.53,217.101,44.826,32.459 -2020-01-26 19:30:00,98.34,214.25599999999997,44.826,32.459 -2020-01-26 19:45:00,100.34,212.222,44.826,32.459 -2020-01-26 20:00:00,98.08,210.243,40.154,32.459 -2020-01-26 20:15:00,89.23,206.959,40.154,32.459 -2020-01-26 20:30:00,88.79,203.717,40.154,32.459 -2020-01-26 20:45:00,85.18,201.489,40.154,32.459 -2020-01-26 21:00:00,83.88,197.894,36.549,32.459 -2020-01-26 21:15:00,90.22,195.03599999999997,36.549,32.459 -2020-01-26 21:30:00,90.09,194.83700000000002,36.549,32.459 -2020-01-26 21:45:00,86.75,193.743,36.549,32.459 -2020-01-26 22:00:00,84.32,187.405,37.663000000000004,32.459 -2020-01-26 22:15:00,87.31,183.648,37.663000000000004,32.459 -2020-01-26 22:30:00,87.15,179.94400000000002,37.663000000000004,32.459 -2020-01-26 22:45:00,85.87,176.705,37.663000000000004,32.459 -2020-01-26 23:00:00,78.89,168.15599999999998,31.945,32.459 -2020-01-26 23:15:00,82.8,164.87,31.945,32.459 -2020-01-26 23:30:00,81.36,162.487,31.945,32.459 -2020-01-26 23:45:00,80.85,159.548,31.945,32.459 -2020-01-27 00:00:00,72.01,140.157,31.533,32.641 -2020-01-27 00:15:00,74.98,138.209,31.533,32.641 -2020-01-27 00:30:00,75.17,140.77700000000002,31.533,32.641 -2020-01-27 00:45:00,73.37,143.741,31.533,32.641 -2020-01-27 01:00:00,65.99,146.861,30.56,32.641 -2020-01-27 01:15:00,72.24,147.267,30.56,32.641 -2020-01-27 01:30:00,72.74,147.346,30.56,32.641 -2020-01-27 01:45:00,73.19,147.313,30.56,32.641 -2020-01-27 02:00:00,68.81,149.619,29.55,32.641 -2020-01-27 02:15:00,72.66,152.05,29.55,32.641 -2020-01-27 02:30:00,73.33,153.86700000000002,29.55,32.641 -2020-01-27 02:45:00,72.31,156.089,29.55,32.641 -2020-01-27 03:00:00,67.49,160.394,27.059,32.641 -2020-01-27 03:15:00,73.56,162.764,27.059,32.641 -2020-01-27 03:30:00,75.02,163.99900000000002,27.059,32.641 -2020-01-27 03:45:00,71.92,165.61599999999999,27.059,32.641 -2020-01-27 04:00:00,66.96,177.02,28.384,32.641 -2020-01-27 04:15:00,68.37,188.86900000000003,28.384,32.641 -2020-01-27 04:30:00,70.66,193.035,28.384,32.641 -2020-01-27 04:45:00,72.68,194.738,28.384,32.641 -2020-01-27 05:00:00,77.25,223.778,35.915,32.641 -2020-01-27 05:15:00,79.29,251.91099999999997,35.915,32.641 -2020-01-27 05:30:00,84.6,249.452,35.915,32.641 -2020-01-27 05:45:00,89.92,242.958,35.915,32.641 -2020-01-27 06:00:00,99.95,241.782,56.18,32.641 -2020-01-27 06:15:00,104.93,246.144,56.18,32.641 -2020-01-27 06:30:00,110.18,250.06400000000002,56.18,32.641 -2020-01-27 06:45:00,113.96,255.674,56.18,32.641 -2020-01-27 07:00:00,121.51,254.377,70.877,32.641 -2020-01-27 07:15:00,123.92,260.205,70.877,32.641 -2020-01-27 07:30:00,123.09,264.038,70.877,32.641 -2020-01-27 07:45:00,125.61,265.228,70.877,32.641 -2020-01-27 08:00:00,127.27,263.626,65.65,32.641 -2020-01-27 08:15:00,127.65,265.01099999999997,65.65,32.641 -2020-01-27 08:30:00,129.37,262.36,65.65,32.641 -2020-01-27 08:45:00,125.27,259.60400000000004,65.65,32.641 -2020-01-27 09:00:00,124.61,253.929,62.037,32.641 -2020-01-27 09:15:00,126.22,249.234,62.037,32.641 -2020-01-27 09:30:00,126.26,246.88,62.037,32.641 -2020-01-27 09:45:00,122.4,243.92700000000002,62.037,32.641 -2020-01-27 10:00:00,124.71,238.52,60.409,32.641 -2020-01-27 10:15:00,124.92,236.058,60.409,32.641 -2020-01-27 10:30:00,126.43,231.824,60.409,32.641 -2020-01-27 10:45:00,127.62,229.86,60.409,32.641 -2020-01-27 11:00:00,128.14,224.88299999999998,60.211999999999996,32.641 -2020-01-27 11:15:00,129.57,224.053,60.211999999999996,32.641 -2020-01-27 11:30:00,130.44,223.34599999999998,60.211999999999996,32.641 -2020-01-27 11:45:00,132.01,221.85299999999998,60.211999999999996,32.641 -2020-01-27 12:00:00,131.87,219.55700000000002,57.733000000000004,32.641 -2020-01-27 12:15:00,130.69,219.908,57.733000000000004,32.641 -2020-01-27 12:30:00,127.99,218.766,57.733000000000004,32.641 -2020-01-27 12:45:00,128.57,219.735,57.733000000000004,32.641 -2020-01-27 13:00:00,127.25,217.735,58.695,32.641 -2020-01-27 13:15:00,127.54,217.582,58.695,32.641 -2020-01-27 13:30:00,126.3,215.213,58.695,32.641 -2020-01-27 13:45:00,124.1,215.097,58.695,32.641 -2020-01-27 14:00:00,120.85,214.834,59.505,32.641 -2020-01-27 14:15:00,122.78,215.037,59.505,32.641 -2020-01-27 14:30:00,121.73,215.206,59.505,32.641 -2020-01-27 14:45:00,122.16,216.058,59.505,32.641 -2020-01-27 15:00:00,122.79,217.581,59.946000000000005,32.641 -2020-01-27 15:15:00,122.37,217.96099999999998,59.946000000000005,32.641 -2020-01-27 15:30:00,120.21,219.23,59.946000000000005,32.641 -2020-01-27 15:45:00,121.58,220.549,59.946000000000005,32.641 -2020-01-27 16:00:00,125.44,221.766,61.766999999999996,32.641 -2020-01-27 16:15:00,124.32,223.122,61.766999999999996,32.641 -2020-01-27 16:30:00,125.38,225.149,61.766999999999996,32.641 -2020-01-27 16:45:00,128.84,226.167,61.766999999999996,32.641 -2020-01-27 17:00:00,134.98,227.94099999999997,67.85600000000001,32.641 -2020-01-27 17:15:00,133.74,228.979,67.85600000000001,32.641 -2020-01-27 17:30:00,134.91,229.42700000000002,67.85600000000001,32.641 -2020-01-27 17:45:00,134.47,229.61,67.85600000000001,32.641 -2020-01-27 18:00:00,134.73,231.514,64.564,32.641 -2020-01-27 18:15:00,131.96,229.16400000000002,64.564,32.641 -2020-01-27 18:30:00,133.73,228.49200000000002,64.564,32.641 -2020-01-27 18:45:00,132.73,228.109,64.564,32.641 -2020-01-27 19:00:00,129.78,225.815,58.536,32.641 -2020-01-27 19:15:00,126.63,222.06799999999998,58.536,32.641 -2020-01-27 19:30:00,126.74,219.84400000000002,58.536,32.641 -2020-01-27 19:45:00,131.0,216.96200000000002,58.536,32.641 -2020-01-27 20:00:00,124.95,212.45,59.888999999999996,32.641 -2020-01-27 20:15:00,116.65,206.18,59.888999999999996,32.641 -2020-01-27 20:30:00,113.27,200.757,59.888999999999996,32.641 -2020-01-27 20:45:00,109.79,200.363,59.888999999999996,32.641 -2020-01-27 21:00:00,104.5,197.43400000000003,52.652,32.641 -2020-01-27 21:15:00,102.34,193.169,52.652,32.641 -2020-01-27 21:30:00,105.41,191.98,52.652,32.641 -2020-01-27 21:45:00,106.02,190.355,52.652,32.641 -2020-01-27 22:00:00,103.67,180.989,46.17,32.641 -2020-01-27 22:15:00,92.1,175.507,46.17,32.641 -2020-01-27 22:30:00,90.73,161.47299999999998,46.17,32.641 -2020-01-27 22:45:00,86.82,152.751,46.17,32.641 -2020-01-27 23:00:00,85.71,145.082,36.281,32.641 -2020-01-27 23:15:00,90.44,145.025,36.281,32.641 -2020-01-27 23:30:00,87.95,145.799,36.281,32.641 -2020-01-27 23:45:00,82.47,145.94,36.281,32.641 -2020-01-28 00:00:00,78.26,139.671,38.821999999999996,32.641 -2020-01-28 00:15:00,75.34,139.238,38.821999999999996,32.641 -2020-01-28 00:30:00,79.52,140.57399999999998,38.821999999999996,32.641 -2020-01-28 00:45:00,82.22,142.25799999999998,38.821999999999996,32.641 -2020-01-28 01:00:00,79.73,145.21200000000002,36.936,32.641 -2020-01-28 01:15:00,76.69,145.066,36.936,32.641 -2020-01-28 01:30:00,75.14,145.361,36.936,32.641 -2020-01-28 01:45:00,76.58,145.766,36.936,32.641 -2020-01-28 02:00:00,79.49,148.15200000000002,34.42,32.641 -2020-01-28 02:15:00,79.78,150.255,34.42,32.641 -2020-01-28 02:30:00,77.63,151.44899999999998,34.42,32.641 -2020-01-28 02:45:00,72.78,153.629,34.42,32.641 -2020-01-28 03:00:00,78.0,156.595,33.585,32.641 -2020-01-28 03:15:00,81.7,157.804,33.585,32.641 -2020-01-28 03:30:00,83.51,159.614,33.585,32.641 -2020-01-28 03:45:00,82.44,161.667,33.585,32.641 -2020-01-28 04:00:00,77.73,173.03900000000002,35.622,32.641 -2020-01-28 04:15:00,84.2,184.46099999999998,35.622,32.641 -2020-01-28 04:30:00,87.03,188.28799999999998,35.622,32.641 -2020-01-28 04:45:00,89.61,191.332,35.622,32.641 -2020-01-28 05:00:00,89.09,226.02,40.599000000000004,32.641 -2020-01-28 05:15:00,87.98,253.782,40.599000000000004,32.641 -2020-01-28 05:30:00,94.26,249.44400000000002,40.599000000000004,32.641 -2020-01-28 05:45:00,98.87,243.109,40.599000000000004,32.641 -2020-01-28 06:00:00,105.04,240.338,55.203,32.641 -2020-01-28 06:15:00,109.62,246.618,55.203,32.641 -2020-01-28 06:30:00,113.28,249.817,55.203,32.641 -2020-01-28 06:45:00,117.53,255.15900000000002,55.203,32.641 -2020-01-28 07:00:00,128.45,253.637,69.029,32.641 -2020-01-28 07:15:00,128.39,259.301,69.029,32.641 -2020-01-28 07:30:00,128.01,262.45099999999996,69.029,32.641 -2020-01-28 07:45:00,129.0,264.04200000000003,69.029,32.641 -2020-01-28 08:00:00,131.4,262.563,65.85300000000001,32.641 -2020-01-28 08:15:00,131.16,262.79400000000004,65.85300000000001,32.641 -2020-01-28 08:30:00,129.91,259.853,65.85300000000001,32.641 -2020-01-28 08:45:00,128.94,256.934,65.85300000000001,32.641 -2020-01-28 09:00:00,130.01,250.21099999999998,61.566,32.641 -2020-01-28 09:15:00,130.06,247.43,61.566,32.641 -2020-01-28 09:30:00,128.11,245.77700000000002,61.566,32.641 -2020-01-28 09:45:00,126.64,242.335,61.566,32.641 -2020-01-28 10:00:00,125.99,236.479,61.244,32.641 -2020-01-28 10:15:00,125.95,232.801,61.244,32.641 -2020-01-28 10:30:00,127.09,228.778,61.244,32.641 -2020-01-28 10:45:00,125.87,226.981,61.244,32.641 -2020-01-28 11:00:00,127.54,223.747,61.16,32.641 -2020-01-28 11:15:00,126.56,222.465,61.16,32.641 -2020-01-28 11:30:00,125.17,220.58599999999998,61.16,32.641 -2020-01-28 11:45:00,127.37,220.002,61.16,32.641 -2020-01-28 12:00:00,124.87,216.142,59.09,32.641 -2020-01-28 12:15:00,123.4,215.963,59.09,32.641 -2020-01-28 12:30:00,122.31,215.505,59.09,32.641 -2020-01-28 12:45:00,119.99,216.01,59.09,32.641 -2020-01-28 13:00:00,117.55,213.6,60.21,32.641 -2020-01-28 13:15:00,119.52,212.706,60.21,32.641 -2020-01-28 13:30:00,119.94,211.708,60.21,32.641 -2020-01-28 13:45:00,117.83,211.977,60.21,32.641 -2020-01-28 14:00:00,115.55,212.02200000000002,60.673,32.641 -2020-01-28 14:15:00,117.18,212.426,60.673,32.641 -2020-01-28 14:30:00,118.0,213.271,60.673,32.641 -2020-01-28 14:45:00,119.26,214.18,60.673,32.641 -2020-01-28 15:00:00,119.6,215.243,62.232,32.641 -2020-01-28 15:15:00,119.97,215.834,62.232,32.641 -2020-01-28 15:30:00,119.48,217.37400000000002,62.232,32.641 -2020-01-28 15:45:00,120.64,218.15,62.232,32.641 -2020-01-28 16:00:00,121.9,219.89,63.611999999999995,32.641 -2020-01-28 16:15:00,122.55,221.785,63.611999999999995,32.641 -2020-01-28 16:30:00,127.29,224.62599999999998,63.611999999999995,32.641 -2020-01-28 16:45:00,127.69,225.864,63.611999999999995,32.641 -2020-01-28 17:00:00,132.53,228.227,70.658,32.641 -2020-01-28 17:15:00,136.01,229.27,70.658,32.641 -2020-01-28 17:30:00,140.13,230.585,70.658,32.641 -2020-01-28 17:45:00,139.21,230.69,70.658,32.641 -2020-01-28 18:00:00,139.49,232.614,68.361,32.641 -2020-01-28 18:15:00,137.64,229.528,68.361,32.641 -2020-01-28 18:30:00,137.81,228.544,68.361,32.641 -2020-01-28 18:45:00,137.16,229.122,68.361,32.641 -2020-01-28 19:00:00,134.44,227.023,62.922,32.641 -2020-01-28 19:15:00,132.06,222.94400000000002,62.922,32.641 -2020-01-28 19:30:00,137.09,220.002,62.922,32.641 -2020-01-28 19:45:00,138.01,217.142,62.922,32.641 -2020-01-28 20:00:00,129.62,212.735,63.251999999999995,32.641 -2020-01-28 20:15:00,119.68,205.938,63.251999999999995,32.641 -2020-01-28 20:30:00,118.14,201.671,63.251999999999995,32.641 -2020-01-28 20:45:00,115.08,200.576,63.251999999999995,32.641 -2020-01-28 21:00:00,112.27,196.71099999999998,54.47,32.641 -2020-01-28 21:15:00,114.93,193.66400000000002,54.47,32.641 -2020-01-28 21:30:00,113.2,191.614,54.47,32.641 -2020-01-28 21:45:00,108.39,190.24,54.47,32.641 -2020-01-28 22:00:00,99.19,182.795,51.12,32.641 -2020-01-28 22:15:00,103.31,177.084,51.12,32.641 -2020-01-28 22:30:00,102.8,163.095,51.12,32.641 -2020-01-28 22:45:00,99.67,154.697,51.12,32.641 -2020-01-28 23:00:00,91.53,147.145,42.156000000000006,32.641 -2020-01-28 23:15:00,92.74,145.862,42.156000000000006,32.641 -2020-01-28 23:30:00,92.86,146.243,42.156000000000006,32.641 -2020-01-28 23:45:00,91.28,145.873,42.156000000000006,32.641 -2020-01-29 00:00:00,85.71,139.624,37.192,32.641 -2020-01-29 00:15:00,85.1,139.17700000000002,37.192,32.641 -2020-01-29 00:30:00,86.49,140.494,37.192,32.641 -2020-01-29 00:45:00,83.22,142.166,37.192,32.641 -2020-01-29 01:00:00,79.05,145.10299999999998,32.24,32.641 -2020-01-29 01:15:00,81.99,144.944,32.24,32.641 -2020-01-29 01:30:00,83.1,145.232,32.24,32.641 -2020-01-29 01:45:00,82.85,145.632,32.24,32.641 -2020-01-29 02:00:00,76.67,148.023,30.34,32.641 -2020-01-29 02:15:00,79.99,150.127,30.34,32.641 -2020-01-29 02:30:00,80.46,151.332,30.34,32.641 -2020-01-29 02:45:00,82.45,153.511,30.34,32.641 -2020-01-29 03:00:00,78.21,156.477,29.129,32.641 -2020-01-29 03:15:00,81.98,157.697,29.129,32.641 -2020-01-29 03:30:00,83.78,159.504,29.129,32.641 -2020-01-29 03:45:00,84.27,161.56799999999998,29.129,32.641 -2020-01-29 04:00:00,79.14,172.926,30.075,32.641 -2020-01-29 04:15:00,81.89,184.338,30.075,32.641 -2020-01-29 04:30:00,86.55,188.173,30.075,32.641 -2020-01-29 04:45:00,86.15,191.208,30.075,32.641 -2020-01-29 05:00:00,85.56,225.864,35.684,32.641 -2020-01-29 05:15:00,86.72,253.618,35.684,32.641 -2020-01-29 05:30:00,89.23,249.269,35.684,32.641 -2020-01-29 05:45:00,94.99,242.946,35.684,32.641 -2020-01-29 06:00:00,105.54,240.192,51.49,32.641 -2020-01-29 06:15:00,109.68,246.484,51.49,32.641 -2020-01-29 06:30:00,113.38,249.673,51.49,32.641 -2020-01-29 06:45:00,116.12,255.03099999999998,51.49,32.641 -2020-01-29 07:00:00,121.07,253.53,68.242,32.641 -2020-01-29 07:15:00,126.34,259.173,68.242,32.641 -2020-01-29 07:30:00,126.89,262.293,68.242,32.641 -2020-01-29 07:45:00,128.53,263.848,68.242,32.641 -2020-01-29 08:00:00,129.79,262.355,63.619,32.641 -2020-01-29 08:15:00,129.69,262.562,63.619,32.641 -2020-01-29 08:30:00,128.46,259.569,63.619,32.641 -2020-01-29 08:45:00,129.7,256.64,63.619,32.641 -2020-01-29 09:00:00,130.75,249.91400000000002,61.333,32.641 -2020-01-29 09:15:00,133.1,247.137,61.333,32.641 -2020-01-29 09:30:00,134.98,245.503,61.333,32.641 -2020-01-29 09:45:00,134.4,242.05900000000003,61.333,32.641 -2020-01-29 10:00:00,132.46,236.209,59.663000000000004,32.641 -2020-01-29 10:15:00,133.39,232.55200000000002,59.663000000000004,32.641 -2020-01-29 10:30:00,133.22,228.53099999999998,59.663000000000004,32.641 -2020-01-29 10:45:00,132.39,226.74400000000003,59.663000000000004,32.641 -2020-01-29 11:00:00,132.31,223.495,59.771,32.641 -2020-01-29 11:15:00,133.12,222.22,59.771,32.641 -2020-01-29 11:30:00,134.86,220.34599999999998,59.771,32.641 -2020-01-29 11:45:00,133.3,219.771,59.771,32.641 -2020-01-29 12:00:00,132.7,215.925,58.723,32.641 -2020-01-29 12:15:00,132.51,215.765,58.723,32.641 -2020-01-29 12:30:00,131.59,215.287,58.723,32.641 -2020-01-29 12:45:00,130.75,215.78900000000002,58.723,32.641 -2020-01-29 13:00:00,129.37,213.39,58.727,32.641 -2020-01-29 13:15:00,129.56,212.47400000000002,58.727,32.641 -2020-01-29 13:30:00,127.85,211.46099999999998,58.727,32.641 -2020-01-29 13:45:00,128.01,211.72799999999998,58.727,32.641 -2020-01-29 14:00:00,127.61,211.81799999999998,59.803999999999995,32.641 -2020-01-29 14:15:00,127.81,212.204,59.803999999999995,32.641 -2020-01-29 14:30:00,127.7,213.043,59.803999999999995,32.641 -2020-01-29 14:45:00,127.4,213.96900000000002,59.803999999999995,32.641 -2020-01-29 15:00:00,128.03,215.045,61.05,32.641 -2020-01-29 15:15:00,127.39,215.608,61.05,32.641 -2020-01-29 15:30:00,128.43,217.12,61.05,32.641 -2020-01-29 15:45:00,124.88,217.88,61.05,32.641 -2020-01-29 16:00:00,126.8,219.623,64.012,32.641 -2020-01-29 16:15:00,126.43,221.516,64.012,32.641 -2020-01-29 16:30:00,130.89,224.363,64.012,32.641 -2020-01-29 16:45:00,131.2,225.593,64.012,32.641 -2020-01-29 17:00:00,135.48,227.94799999999998,66.751,32.641 -2020-01-29 17:15:00,135.19,229.023,66.751,32.641 -2020-01-29 17:30:00,136.26,230.373,66.751,32.641 -2020-01-29 17:45:00,136.97,230.505,66.751,32.641 -2020-01-29 18:00:00,136.56,232.447,65.91199999999999,32.641 -2020-01-29 18:15:00,134.46,229.4,65.91199999999999,32.641 -2020-01-29 18:30:00,136.21,228.42,65.91199999999999,32.641 -2020-01-29 18:45:00,134.51,229.021,65.91199999999999,32.641 -2020-01-29 19:00:00,130.95,226.882,63.324,32.641 -2020-01-29 19:15:00,130.8,222.81099999999998,63.324,32.641 -2020-01-29 19:30:00,128.57,219.885,63.324,32.641 -2020-01-29 19:45:00,133.5,217.047,63.324,32.641 -2020-01-29 20:00:00,127.01,212.614,63.573,32.641 -2020-01-29 20:15:00,117.14,205.825,63.573,32.641 -2020-01-29 20:30:00,114.01,201.562,63.573,32.641 -2020-01-29 20:45:00,109.09,200.477,63.573,32.641 -2020-01-29 21:00:00,107.15,196.59400000000002,55.073,32.641 -2020-01-29 21:15:00,111.75,193.52900000000002,55.073,32.641 -2020-01-29 21:30:00,109.55,191.48,55.073,32.641 -2020-01-29 21:45:00,104.52,190.122,55.073,32.641 -2020-01-29 22:00:00,95.88,182.666,51.321999999999996,32.641 -2020-01-29 22:15:00,96.63,176.979,51.321999999999996,32.641 -2020-01-29 22:30:00,97.76,162.968,51.321999999999996,32.641 -2020-01-29 22:45:00,95.81,154.578,51.321999999999996,32.641 -2020-01-29 23:00:00,89.08,147.014,42.09,32.641 -2020-01-29 23:15:00,83.66,145.744,42.09,32.641 -2020-01-29 23:30:00,86.56,146.143,42.09,32.641 -2020-01-29 23:45:00,88.77,145.786,42.09,32.641 -2020-01-30 00:00:00,82.65,139.567,38.399,32.641 -2020-01-30 00:15:00,78.77,139.108,38.399,32.641 -2020-01-30 00:30:00,76.46,140.406,38.399,32.641 -2020-01-30 00:45:00,81.36,142.06799999999998,38.399,32.641 -2020-01-30 01:00:00,78.58,144.985,36.94,32.641 -2020-01-30 01:15:00,78.21,144.812,36.94,32.641 -2020-01-30 01:30:00,76.11,145.093,36.94,32.641 -2020-01-30 01:45:00,79.6,145.488,36.94,32.641 -2020-01-30 02:00:00,78.45,147.884,35.275,32.641 -2020-01-30 02:15:00,77.62,149.989,35.275,32.641 -2020-01-30 02:30:00,71.61,151.20600000000002,35.275,32.641 -2020-01-30 02:45:00,72.74,153.385,35.275,32.641 -2020-01-30 03:00:00,71.23,156.349,35.329,32.641 -2020-01-30 03:15:00,71.37,157.578,35.329,32.641 -2020-01-30 03:30:00,74.11,159.38299999999998,35.329,32.641 -2020-01-30 03:45:00,80.9,161.459,35.329,32.641 -2020-01-30 04:00:00,82.14,172.805,36.275,32.641 -2020-01-30 04:15:00,83.94,184.206,36.275,32.641 -2020-01-30 04:30:00,79.65,188.05,36.275,32.641 -2020-01-30 04:45:00,79.86,191.074,36.275,32.641 -2020-01-30 05:00:00,84.84,225.699,42.193999999999996,32.641 -2020-01-30 05:15:00,87.97,253.44799999999998,42.193999999999996,32.641 -2020-01-30 05:30:00,91.94,249.08700000000002,42.193999999999996,32.641 -2020-01-30 05:45:00,96.69,242.77599999999998,42.193999999999996,32.641 -2020-01-30 06:00:00,105.79,240.03900000000002,56.422,32.641 -2020-01-30 06:15:00,111.14,246.34400000000002,56.422,32.641 -2020-01-30 06:30:00,115.88,249.519,56.422,32.641 -2020-01-30 06:45:00,119.21,254.891,56.422,32.641 -2020-01-30 07:00:00,127.68,253.41299999999998,72.569,32.641 -2020-01-30 07:15:00,130.37,259.034,72.569,32.641 -2020-01-30 07:30:00,129.83,262.124,72.569,32.641 -2020-01-30 07:45:00,131.25,263.643,72.569,32.641 -2020-01-30 08:00:00,133.61,262.135,67.704,32.641 -2020-01-30 08:15:00,133.19,262.318,67.704,32.641 -2020-01-30 08:30:00,132.87,259.27099999999996,67.704,32.641 -2020-01-30 08:45:00,132.5,256.33299999999997,67.704,32.641 -2020-01-30 09:00:00,132.2,249.60299999999998,63.434,32.641 -2020-01-30 09:15:00,133.4,246.83,63.434,32.641 -2020-01-30 09:30:00,133.88,245.217,63.434,32.641 -2020-01-30 09:45:00,134.13,241.77200000000002,63.434,32.641 -2020-01-30 10:00:00,134.86,235.928,61.88399999999999,32.641 -2020-01-30 10:15:00,134.96,232.292,61.88399999999999,32.641 -2020-01-30 10:30:00,133.39,228.273,61.88399999999999,32.641 -2020-01-30 10:45:00,134.4,226.497,61.88399999999999,32.641 -2020-01-30 11:00:00,133.27,223.232,61.481,32.641 -2020-01-30 11:15:00,134.3,221.965,61.481,32.641 -2020-01-30 11:30:00,136.88,220.09599999999998,61.481,32.641 -2020-01-30 11:45:00,137.03,219.52900000000002,61.481,32.641 -2020-01-30 12:00:00,133.83,215.699,59.527,32.641 -2020-01-30 12:15:00,133.63,215.558,59.527,32.641 -2020-01-30 12:30:00,132.45,215.06,59.527,32.641 -2020-01-30 12:45:00,132.76,215.56,59.527,32.641 -2020-01-30 13:00:00,132.63,213.172,58.794,32.641 -2020-01-30 13:15:00,131.93,212.234,58.794,32.641 -2020-01-30 13:30:00,131.32,211.206,58.794,32.641 -2020-01-30 13:45:00,131.62,211.47099999999998,58.794,32.641 -2020-01-30 14:00:00,130.86,211.605,60.32,32.641 -2020-01-30 14:15:00,130.65,211.976,60.32,32.641 -2020-01-30 14:30:00,129.29,212.80599999999998,60.32,32.641 -2020-01-30 14:45:00,129.11,213.748,60.32,32.641 -2020-01-30 15:00:00,128.71,214.838,62.52,32.641 -2020-01-30 15:15:00,127.59,215.372,62.52,32.641 -2020-01-30 15:30:00,126.25,216.857,62.52,32.641 -2020-01-30 15:45:00,127.28,217.6,62.52,32.641 -2020-01-30 16:00:00,127.19,219.34400000000002,64.199,32.641 -2020-01-30 16:15:00,126.69,221.235,64.199,32.641 -2020-01-30 16:30:00,127.31,224.08700000000002,64.199,32.641 -2020-01-30 16:45:00,129.45,225.31,64.199,32.641 -2020-01-30 17:00:00,133.14,227.657,68.19800000000001,32.641 -2020-01-30 17:15:00,136.81,228.763,68.19800000000001,32.641 -2020-01-30 17:30:00,138.93,230.148,68.19800000000001,32.641 -2020-01-30 17:45:00,139.45,230.30700000000002,68.19800000000001,32.641 -2020-01-30 18:00:00,139.02,232.267,67.899,32.641 -2020-01-30 18:15:00,140.21,229.262,67.899,32.641 -2020-01-30 18:30:00,137.12,228.28400000000002,67.899,32.641 -2020-01-30 18:45:00,137.17,228.91099999999997,67.899,32.641 -2020-01-30 19:00:00,134.6,226.73,64.72399999999999,32.641 -2020-01-30 19:15:00,131.53,222.669,64.72399999999999,32.641 -2020-01-30 19:30:00,136.23,219.75900000000001,64.72399999999999,32.641 -2020-01-30 19:45:00,136.85,216.945,64.72399999999999,32.641 -2020-01-30 20:00:00,129.72,212.484,64.062,32.641 -2020-01-30 20:15:00,119.43,205.704,64.062,32.641 -2020-01-30 20:30:00,117.24,201.445,64.062,32.641 -2020-01-30 20:45:00,114.49,200.37099999999998,64.062,32.641 -2020-01-30 21:00:00,110.45,196.468,57.971000000000004,32.641 -2020-01-30 21:15:00,114.4,193.387,57.971000000000004,32.641 -2020-01-30 21:30:00,113.1,191.33700000000002,57.971000000000004,32.641 -2020-01-30 21:45:00,108.52,189.99599999999998,57.971000000000004,32.641 -2020-01-30 22:00:00,99.82,182.52700000000002,53.715,32.641 -2020-01-30 22:15:00,95.43,176.864,53.715,32.641 -2020-01-30 22:30:00,95.06,162.83100000000002,53.715,32.641 -2020-01-30 22:45:00,97.76,154.44899999999998,53.715,32.641 -2020-01-30 23:00:00,95.28,146.873,47.8,32.641 -2020-01-30 23:15:00,93.83,145.61700000000002,47.8,32.641 -2020-01-30 23:30:00,85.99,146.034,47.8,32.641 -2020-01-30 23:45:00,83.25,145.692,47.8,32.641 -2020-01-31 00:00:00,83.54,138.534,43.656000000000006,32.641 -2020-01-31 00:15:00,86.38,138.262,43.656000000000006,32.641 -2020-01-31 00:30:00,85.86,139.339,43.656000000000006,32.641 -2020-01-31 00:45:00,81.71,141.053,43.656000000000006,32.641 -2020-01-31 01:00:00,75.55,143.645,41.263000000000005,32.641 -2020-01-31 01:15:00,81.11,144.639,41.263000000000005,32.641 -2020-01-31 01:30:00,82.73,144.529,41.263000000000005,32.641 -2020-01-31 01:45:00,83.26,145.086,41.263000000000005,32.641 -2020-01-31 02:00:00,78.12,147.43200000000002,40.799,32.641 -2020-01-31 02:15:00,77.01,149.412,40.799,32.641 -2020-01-31 02:30:00,81.31,151.12,40.799,32.641 -2020-01-31 02:45:00,81.77,153.464,40.799,32.641 -2020-01-31 03:00:00,80.53,155.114,41.398,32.641 -2020-01-31 03:15:00,78.43,157.67700000000002,41.398,32.641 -2020-01-31 03:30:00,83.05,159.502,41.398,32.641 -2020-01-31 03:45:00,85.88,161.82299999999998,41.398,32.641 -2020-01-31 04:00:00,81.83,173.408,42.38,32.641 -2020-01-31 04:15:00,79.91,184.787,42.38,32.641 -2020-01-31 04:30:00,83.03,188.74200000000002,42.38,32.641 -2020-01-31 04:45:00,88.4,190.513,42.38,32.641 -2020-01-31 05:00:00,92.99,223.68099999999998,46.181000000000004,32.641 -2020-01-31 05:15:00,86.2,253.05200000000002,46.181000000000004,32.641 -2020-01-31 05:30:00,87.66,249.93200000000002,46.181000000000004,32.641 -2020-01-31 05:45:00,92.16,243.638,46.181000000000004,32.641 -2020-01-31 06:00:00,100.18,241.388,59.33,32.641 -2020-01-31 06:15:00,103.88,245.922,59.33,32.641 -2020-01-31 06:30:00,107.37,248.06099999999998,59.33,32.641 -2020-01-31 06:45:00,111.97,255.41,59.33,32.641 -2020-01-31 07:00:00,118.81,252.83900000000003,72.454,32.641 -2020-01-31 07:15:00,121.43,259.492,72.454,32.641 -2020-01-31 07:30:00,120.61,262.686,72.454,32.641 -2020-01-31 07:45:00,123.16,263.152,72.454,32.641 -2020-01-31 08:00:00,125.9,260.182,67.175,32.641 -2020-01-31 08:15:00,125.96,259.769,67.175,32.641 -2020-01-31 08:30:00,124.1,257.837,67.175,32.641 -2020-01-31 08:45:00,122.45,253.051,67.175,32.641 -2020-01-31 09:00:00,121.29,247.245,65.365,32.641 -2020-01-31 09:15:00,120.99,244.838,65.365,32.641 -2020-01-31 09:30:00,122.63,242.851,65.365,32.641 -2020-01-31 09:45:00,125.07,239.21400000000003,65.365,32.641 -2020-01-31 10:00:00,125.19,232.065,63.95,32.641 -2020-01-31 10:15:00,123.48,229.30700000000002,63.95,32.641 -2020-01-31 10:30:00,118.43,225.118,63.95,32.641 -2020-01-31 10:45:00,116.42,222.847,63.95,32.641 -2020-01-31 11:00:00,112.88,219.519,63.92100000000001,32.641 -2020-01-31 11:15:00,114.05,217.38099999999997,63.92100000000001,32.641 -2020-01-31 11:30:00,112.59,217.62,63.92100000000001,32.641 -2020-01-31 11:45:00,113.51,217.265,63.92100000000001,32.641 -2020-01-31 12:00:00,110.81,214.657,60.79600000000001,32.641 -2020-01-31 12:15:00,108.51,212.18099999999998,60.79600000000001,32.641 -2020-01-31 12:30:00,110.61,211.828,60.79600000000001,32.641 -2020-01-31 12:45:00,107.4,213.051,60.79600000000001,32.641 -2020-01-31 13:00:00,109.64,211.68400000000003,59.393,32.641 -2020-01-31 13:15:00,114.24,211.65400000000002,59.393,32.641 -2020-01-31 13:30:00,114.13,210.50400000000002,59.393,32.641 -2020-01-31 13:45:00,116.3,210.64700000000002,59.393,32.641 -2020-01-31 14:00:00,114.74,209.613,57.943999999999996,32.641 -2020-01-31 14:15:00,117.45,209.69099999999997,57.943999999999996,32.641 -2020-01-31 14:30:00,113.57,210.88299999999998,57.943999999999996,32.641 -2020-01-31 14:45:00,113.98,212.3,57.943999999999996,32.641 -2020-01-31 15:00:00,114.17,212.85,60.153999999999996,32.641 -2020-01-31 15:15:00,115.31,212.88099999999997,60.153999999999996,32.641 -2020-01-31 15:30:00,115.47,212.65400000000002,60.153999999999996,32.641 -2020-01-31 15:45:00,118.13,213.44,60.153999999999996,32.641 -2020-01-31 16:00:00,117.86,213.96900000000002,62.933,32.641 -2020-01-31 16:15:00,117.65,216.122,62.933,32.641 -2020-01-31 16:30:00,119.13,219.115,62.933,32.641 -2020-01-31 16:45:00,120.48,220.305,62.933,32.641 -2020-01-31 17:00:00,125.21,222.662,68.657,32.641 -2020-01-31 17:15:00,125.41,223.35,68.657,32.641 -2020-01-31 17:30:00,129.61,224.38299999999998,68.657,32.641 -2020-01-31 17:45:00,127.16,224.322,68.657,32.641 -2020-01-31 18:00:00,127.56,227.113,67.111,32.641 -2020-01-31 18:15:00,125.29,223.821,67.111,32.641 -2020-01-31 18:30:00,125.6,223.31,67.111,32.641 -2020-01-31 18:45:00,124.45,223.90200000000002,67.111,32.641 -2020-01-31 19:00:00,121.9,222.61900000000003,62.434,32.641 -2020-01-31 19:15:00,121.13,220.045,62.434,32.641 -2020-01-31 19:30:00,123.82,216.67700000000002,62.434,32.641 -2020-01-31 19:45:00,118.48,213.486,62.434,32.641 -2020-01-31 20:00:00,112.24,209.06799999999998,61.763000000000005,32.641 -2020-01-31 20:15:00,106.36,202.207,61.763000000000005,32.641 -2020-01-31 20:30:00,103.04,197.96599999999998,61.763000000000005,32.641 -2020-01-31 20:45:00,102.41,197.637,61.763000000000005,32.641 -2020-01-31 21:00:00,97.4,194.132,56.785,32.641 -2020-01-31 21:15:00,102.69,191.324,56.785,32.641 -2020-01-31 21:30:00,101.42,189.345,56.785,32.641 -2020-01-31 21:45:00,97.23,188.61700000000002,56.785,32.641 -2020-01-31 22:00:00,90.73,182.27200000000002,52.693000000000005,32.641 -2020-01-31 22:15:00,85.64,176.511,52.693000000000005,32.641 -2020-01-31 22:30:00,87.1,169.132,52.693000000000005,32.641 -2020-01-31 22:45:00,89.23,164.673,52.693000000000005,32.641 -2020-01-31 23:00:00,85.55,156.369,45.443999999999996,32.641 -2020-01-31 23:15:00,83.18,153.1,45.443999999999996,32.641 -2020-01-31 23:30:00,79.02,152.094,45.443999999999996,32.641 -2020-01-31 23:45:00,83.75,151.02700000000002,45.443999999999996,32.641 -2020-02-01 00:00:00,76.71,129.81,42.033,32.431999999999995 -2020-02-01 00:15:00,73.23,124.75399999999999,42.033,32.431999999999995 -2020-02-01 00:30:00,70.83,127.20700000000001,42.033,32.431999999999995 -2020-02-01 00:45:00,74.17,129.618,42.033,32.431999999999995 -2020-02-01 01:00:00,71.69,132.734,38.255,32.431999999999995 -2020-02-01 01:15:00,71.85,132.577,38.255,32.431999999999995 -2020-02-01 01:30:00,67.13,132.036,38.255,32.431999999999995 -2020-02-01 01:45:00,71.17,132.134,38.255,32.431999999999995 -2020-02-01 02:00:00,69.97,135.287,36.404,32.431999999999995 -2020-02-01 02:15:00,69.51,136.813,36.404,32.431999999999995 -2020-02-01 02:30:00,67.27,137.26,36.404,32.431999999999995 -2020-02-01 02:45:00,73.49,139.49,36.404,32.431999999999995 -2020-02-01 03:00:00,69.19,141.94,36.083,32.431999999999995 -2020-02-01 03:15:00,67.7,143.183,36.083,32.431999999999995 -2020-02-01 03:30:00,62.05,143.143,36.083,32.431999999999995 -2020-02-01 03:45:00,63.49,145.23,36.083,32.431999999999995 -2020-02-01 04:00:00,67.97,152.216,36.102,32.431999999999995 -2020-02-01 04:15:00,68.64,160.666,36.102,32.431999999999995 -2020-02-01 04:30:00,65.01,162.115,36.102,32.431999999999995 -2020-02-01 04:45:00,63.87,163.159,36.102,32.431999999999995 -2020-02-01 05:00:00,64.66,178.486,35.284,32.431999999999995 -2020-02-01 05:15:00,65.5,187.328,35.284,32.431999999999995 -2020-02-01 05:30:00,65.69,184.41,35.284,32.431999999999995 -2020-02-01 05:45:00,67.86,183.62,35.284,32.431999999999995 -2020-02-01 06:00:00,69.24,201.104,36.265,32.431999999999995 -2020-02-01 06:15:00,71.37,222.93900000000002,36.265,32.431999999999995 -2020-02-01 06:30:00,71.93,219.313,36.265,32.431999999999995 -2020-02-01 06:45:00,74.95,216.52599999999998,36.265,32.431999999999995 -2020-02-01 07:00:00,78.52,210.303,40.714,32.431999999999995 -2020-02-01 07:15:00,80.13,215.368,40.714,32.431999999999995 -2020-02-01 07:30:00,82.2,221.09,40.714,32.431999999999995 -2020-02-01 07:45:00,85.69,225.72099999999998,40.714,32.431999999999995 -2020-02-01 08:00:00,91.13,227.44099999999997,46.692,32.431999999999995 -2020-02-01 08:15:00,91.12,231.018,46.692,32.431999999999995 -2020-02-01 08:30:00,92.79,230.90099999999998,46.692,32.431999999999995 -2020-02-01 08:45:00,94.88,229.52900000000002,46.692,32.431999999999995 -2020-02-01 09:00:00,97.7,225.294,48.925,32.431999999999995 -2020-02-01 09:15:00,97.76,223.685,48.925,32.431999999999995 -2020-02-01 09:30:00,98.69,222.65200000000002,48.925,32.431999999999995 -2020-02-01 09:45:00,101.05,219.40099999999998,48.925,32.431999999999995 -2020-02-01 10:00:00,101.31,212.862,47.799,32.431999999999995 -2020-02-01 10:15:00,101.73,210.19,47.799,32.431999999999995 -2020-02-01 10:30:00,101.39,206.543,47.799,32.431999999999995 -2020-02-01 10:45:00,106.31,205.921,47.799,32.431999999999995 -2020-02-01 11:00:00,103.02,203.19299999999998,44.309,32.431999999999995 -2020-02-01 11:15:00,103.3,200.252,44.309,32.431999999999995 -2020-02-01 11:30:00,108.4,199.29,44.309,32.431999999999995 -2020-02-01 11:45:00,112.35,197.61900000000003,44.309,32.431999999999995 -2020-02-01 12:00:00,105.64,193.695,42.367,32.431999999999995 -2020-02-01 12:15:00,104.87,191.93599999999998,42.367,32.431999999999995 -2020-02-01 12:30:00,102.75,191.995,42.367,32.431999999999995 -2020-02-01 12:45:00,101.19,192.31900000000002,42.367,32.431999999999995 -2020-02-01 13:00:00,98.77,190.88400000000001,39.036,32.431999999999995 -2020-02-01 13:15:00,102.03,188.609,39.036,32.431999999999995 -2020-02-01 13:30:00,97.87,187.08700000000002,39.036,32.431999999999995 -2020-02-01 13:45:00,97.89,187.785,39.036,32.431999999999995 -2020-02-01 14:00:00,95.54,188.062,37.995,32.431999999999995 -2020-02-01 14:15:00,95.43,187.68599999999998,37.995,32.431999999999995 -2020-02-01 14:30:00,94.24,186.92700000000002,37.995,32.431999999999995 -2020-02-01 14:45:00,95.58,188.394,37.995,32.431999999999995 -2020-02-01 15:00:00,95.01,189.855,40.71,32.431999999999995 -2020-02-01 15:15:00,94.99,190.50799999999998,40.71,32.431999999999995 -2020-02-01 15:30:00,93.2,192.142,40.71,32.431999999999995 -2020-02-01 15:45:00,93.06,193.253,40.71,32.431999999999995 -2020-02-01 16:00:00,95.23,191.983,46.998000000000005,32.431999999999995 -2020-02-01 16:15:00,95.22,195.12099999999998,46.998000000000005,32.431999999999995 -2020-02-01 16:30:00,97.78,198.024,46.998000000000005,32.431999999999995 -2020-02-01 16:45:00,99.14,200.262,46.998000000000005,32.431999999999995 -2020-02-01 17:00:00,105.82,202.077,55.431000000000004,32.431999999999995 -2020-02-01 17:15:00,106.49,204.93599999999998,55.431000000000004,32.431999999999995 -2020-02-01 17:30:00,107.67,205.97799999999998,55.431000000000004,32.431999999999995 -2020-02-01 17:45:00,109.78,205.5,55.431000000000004,32.431999999999995 -2020-02-01 18:00:00,112.26,207.61599999999999,55.989,32.431999999999995 -2020-02-01 18:15:00,110.96,206.43400000000003,55.989,32.431999999999995 -2020-02-01 18:30:00,109.62,207.187,55.989,32.431999999999995 -2020-02-01 18:45:00,108.33,204.48,55.989,32.431999999999995 -2020-02-01 19:00:00,106.53,204.679,50.882,32.431999999999995 -2020-02-01 19:15:00,105.46,201.778,50.882,32.431999999999995 -2020-02-01 19:30:00,104.21,199.399,50.882,32.431999999999995 -2020-02-01 19:45:00,102.98,195.791,50.882,32.431999999999995 -2020-02-01 20:00:00,97.91,193.72299999999998,43.172,32.431999999999995 -2020-02-01 20:15:00,94.51,189.55700000000002,43.172,32.431999999999995 -2020-02-01 20:30:00,91.8,185.125,43.172,32.431999999999995 -2020-02-01 20:45:00,90.04,184.016,43.172,32.431999999999995 -2020-02-01 21:00:00,84.99,183.33900000000003,37.599000000000004,32.431999999999995 -2020-02-01 21:15:00,84.93,181.149,37.599000000000004,32.431999999999995 -2020-02-01 21:30:00,83.67,180.52,37.599000000000004,32.431999999999995 -2020-02-01 21:45:00,82.32,179.476,37.599000000000004,32.431999999999995 -2020-02-01 22:00:00,78.77,174.79,39.047,32.431999999999995 -2020-02-01 22:15:00,79.21,171.956,39.047,32.431999999999995 -2020-02-01 22:30:00,75.78,171.52200000000002,39.047,32.431999999999995 -2020-02-01 22:45:00,74.58,169.205,39.047,32.431999999999995 -2020-02-01 23:00:00,71.15,163.97299999999998,32.339,32.431999999999995 -2020-02-01 23:15:00,70.35,158.893,32.339,32.431999999999995 -2020-02-01 23:30:00,67.66,155.857,32.339,32.431999999999995 -2020-02-01 23:45:00,67.11,152.153,32.339,32.431999999999995 -2020-02-02 00:00:00,63.53,130.029,29.988000000000003,32.431999999999995 -2020-02-02 00:15:00,63.8,124.745,29.988000000000003,32.431999999999995 -2020-02-02 00:30:00,62.55,126.78,29.988000000000003,32.431999999999995 -2020-02-02 00:45:00,61.0,129.969,29.988000000000003,32.431999999999995 -2020-02-02 01:00:00,57.82,132.872,28.531999999999996,32.431999999999995 -2020-02-02 01:15:00,58.55,133.874,28.531999999999996,32.431999999999995 -2020-02-02 01:30:00,58.11,133.923,28.531999999999996,32.431999999999995 -2020-02-02 01:45:00,57.64,133.714,28.531999999999996,32.431999999999995 -2020-02-02 02:00:00,55.93,136.03,27.805999999999997,32.431999999999995 -2020-02-02 02:15:00,56.4,136.515,27.805999999999997,32.431999999999995 -2020-02-02 02:30:00,55.63,137.906,27.805999999999997,32.431999999999995 -2020-02-02 02:45:00,55.85,140.687,27.805999999999997,32.431999999999995 -2020-02-02 03:00:00,54.61,143.411,26.193,32.431999999999995 -2020-02-02 03:15:00,55.42,144.041,26.193,32.431999999999995 -2020-02-02 03:30:00,55.76,145.66,26.193,32.431999999999995 -2020-02-02 03:45:00,55.94,147.775,26.193,32.431999999999995 -2020-02-02 04:00:00,55.92,154.489,27.19,32.431999999999995 -2020-02-02 04:15:00,57.67,161.849,27.19,32.431999999999995 -2020-02-02 04:30:00,57.96,163.20600000000002,27.19,32.431999999999995 -2020-02-02 04:45:00,58.08,164.608,27.19,32.431999999999995 -2020-02-02 05:00:00,58.81,175.93599999999998,28.166999999999998,32.431999999999995 -2020-02-02 05:15:00,60.25,182.16099999999997,28.166999999999998,32.431999999999995 -2020-02-02 05:30:00,60.89,179.095,28.166999999999998,32.431999999999995 -2020-02-02 05:45:00,61.68,178.62599999999998,28.166999999999998,32.431999999999995 -2020-02-02 06:00:00,62.19,196.321,27.16,32.431999999999995 -2020-02-02 06:15:00,62.48,216.12400000000002,27.16,32.431999999999995 -2020-02-02 06:30:00,63.17,211.331,27.16,32.431999999999995 -2020-02-02 06:45:00,64.8,207.49599999999998,27.16,32.431999999999995 -2020-02-02 07:00:00,68.38,204.016,29.578000000000003,32.431999999999995 -2020-02-02 07:15:00,68.86,208.31099999999998,29.578000000000003,32.431999999999995 -2020-02-02 07:30:00,69.25,212.454,29.578000000000003,32.431999999999995 -2020-02-02 07:45:00,71.7,216.199,29.578000000000003,32.431999999999995 -2020-02-02 08:00:00,76.25,219.887,34.650999999999996,32.431999999999995 -2020-02-02 08:15:00,77.74,223.195,34.650999999999996,32.431999999999995 -2020-02-02 08:30:00,79.71,224.77,34.650999999999996,32.431999999999995 -2020-02-02 08:45:00,79.84,225.657,34.650999999999996,32.431999999999995 -2020-02-02 09:00:00,84.28,220.99099999999999,38.080999999999996,32.431999999999995 -2020-02-02 09:15:00,85.13,220.058,38.080999999999996,32.431999999999995 -2020-02-02 09:30:00,86.37,218.822,38.080999999999996,32.431999999999995 -2020-02-02 09:45:00,88.68,215.322,38.080999999999996,32.431999999999995 -2020-02-02 10:00:00,88.73,211.52200000000002,39.934,32.431999999999995 -2020-02-02 10:15:00,89.78,209.44799999999998,39.934,32.431999999999995 -2020-02-02 10:30:00,92.29,206.45,39.934,32.431999999999995 -2020-02-02 10:45:00,94.48,203.609,39.934,32.431999999999995 -2020-02-02 11:00:00,96.74,201.93599999999998,43.74100000000001,32.431999999999995 -2020-02-02 11:15:00,100.32,199.2,43.74100000000001,32.431999999999995 -2020-02-02 11:30:00,102.7,197.222,43.74100000000001,32.431999999999995 -2020-02-02 11:45:00,103.15,196.201,43.74100000000001,32.431999999999995 -2020-02-02 12:00:00,99.58,191.542,40.001999999999995,32.431999999999995 -2020-02-02 12:15:00,98.64,191.982,40.001999999999995,32.431999999999995 -2020-02-02 12:30:00,95.5,190.382,40.001999999999995,32.431999999999995 -2020-02-02 12:45:00,93.0,189.704,40.001999999999995,32.431999999999995 -2020-02-02 13:00:00,90.71,187.521,37.855,32.431999999999995 -2020-02-02 13:15:00,89.28,188.63099999999997,37.855,32.431999999999995 -2020-02-02 13:30:00,87.1,186.988,37.855,32.431999999999995 -2020-02-02 13:45:00,86.01,186.828,37.855,32.431999999999995 -2020-02-02 14:00:00,84.14,187.291,35.946999999999996,32.431999999999995 -2020-02-02 14:15:00,84.33,188.176,35.946999999999996,32.431999999999995 -2020-02-02 14:30:00,83.61,188.908,35.946999999999996,32.431999999999995 -2020-02-02 14:45:00,84.39,190.054,35.946999999999996,32.431999999999995 -2020-02-02 15:00:00,84.93,189.815,35.138000000000005,32.431999999999995 -2020-02-02 15:15:00,84.41,191.359,35.138000000000005,32.431999999999995 -2020-02-02 15:30:00,83.67,193.63299999999998,35.138000000000005,32.431999999999995 -2020-02-02 15:45:00,83.84,195.46,35.138000000000005,32.431999999999995 -2020-02-02 16:00:00,85.14,196.38099999999997,38.672,32.431999999999995 -2020-02-02 16:15:00,85.03,198.49599999999998,38.672,32.431999999999995 -2020-02-02 16:30:00,86.66,201.58599999999998,38.672,32.431999999999995 -2020-02-02 16:45:00,89.42,203.954,38.672,32.431999999999995 -2020-02-02 17:00:00,94.56,205.68900000000002,48.684,32.431999999999995 -2020-02-02 17:15:00,97.47,208.07299999999998,48.684,32.431999999999995 -2020-02-02 17:30:00,99.16,209.385,48.684,32.431999999999995 -2020-02-02 17:45:00,100.99,211.365,48.684,32.431999999999995 -2020-02-02 18:00:00,105.14,212.826,51.568999999999996,32.431999999999995 -2020-02-02 18:15:00,103.39,213.162,51.568999999999996,32.431999999999995 -2020-02-02 18:30:00,106.37,211.679,51.568999999999996,32.431999999999995 -2020-02-02 18:45:00,102.89,210.99400000000003,51.568999999999996,32.431999999999995 -2020-02-02 19:00:00,101.38,210.583,48.608000000000004,32.431999999999995 -2020-02-02 19:15:00,99.21,208.429,48.608000000000004,32.431999999999995 -2020-02-02 19:30:00,98.13,205.91099999999997,48.608000000000004,32.431999999999995 -2020-02-02 19:45:00,98.79,203.928,48.608000000000004,32.431999999999995 -2020-02-02 20:00:00,93.72,201.81799999999998,43.733999999999995,32.431999999999995 -2020-02-02 20:15:00,91.94,198.74400000000003,43.733999999999995,32.431999999999995 -2020-02-02 20:30:00,90.61,195.58700000000002,43.733999999999995,32.431999999999995 -2020-02-02 20:45:00,89.18,193.338,43.733999999999995,32.431999999999995 -2020-02-02 21:00:00,86.51,189.835,39.283,32.431999999999995 -2020-02-02 21:15:00,86.22,186.979,39.283,32.431999999999995 -2020-02-02 21:30:00,86.04,186.727,39.283,32.431999999999995 -2020-02-02 21:45:00,87.9,185.815,39.283,32.431999999999995 -2020-02-02 22:00:00,84.32,179.638,40.111,32.431999999999995 -2020-02-02 22:15:00,85.45,176.153,40.111,32.431999999999995 -2020-02-02 22:30:00,83.23,172.30900000000003,40.111,32.431999999999995 -2020-02-02 22:45:00,82.39,169.19,40.111,32.431999999999995 -2020-02-02 23:00:00,78.06,160.942,35.791,32.431999999999995 -2020-02-02 23:15:00,78.99,157.774,35.791,32.431999999999995 -2020-02-02 23:30:00,76.63,155.68200000000002,35.791,32.431999999999995 -2020-02-02 23:45:00,77.09,152.945,35.791,32.431999999999995 -2020-02-03 00:00:00,72.55,134.424,34.311,32.613 -2020-02-03 00:15:00,73.3,132.341,34.311,32.613 -2020-02-03 00:30:00,72.72,134.543,34.311,32.613 -2020-02-03 00:45:00,71.94,137.173,34.311,32.613 -2020-02-03 01:00:00,69.66,140.04,34.585,32.613 -2020-02-03 01:15:00,70.92,140.44,34.585,32.613 -2020-02-03 01:30:00,70.34,140.496,34.585,32.613 -2020-02-03 01:45:00,70.86,140.424,34.585,32.613 -2020-02-03 02:00:00,69.94,142.671,34.111,32.613 -2020-02-03 02:15:00,71.57,144.918,34.111,32.613 -2020-02-03 02:30:00,70.86,146.672,34.111,32.613 -2020-02-03 02:45:00,72.17,148.778,34.111,32.613 -2020-02-03 03:00:00,71.64,152.88299999999998,32.435,32.613 -2020-02-03 03:15:00,71.9,155.274,32.435,32.613 -2020-02-03 03:30:00,73.09,156.496,32.435,32.613 -2020-02-03 03:45:00,73.72,158.063,32.435,32.613 -2020-02-03 04:00:00,74.52,169.278,33.04,32.613 -2020-02-03 04:15:00,75.25,180.90200000000002,33.04,32.613 -2020-02-03 04:30:00,76.93,184.803,33.04,32.613 -2020-02-03 04:45:00,80.1,186.326,33.04,32.613 -2020-02-03 05:00:00,83.89,214.47799999999998,40.399,32.613 -2020-02-03 05:15:00,86.25,242.049,40.399,32.613 -2020-02-03 05:30:00,91.33,239.489,40.399,32.613 -2020-02-03 05:45:00,95.99,233.071,40.399,32.613 -2020-02-03 06:00:00,104.74,231.924,60.226000000000006,32.613 -2020-02-03 06:15:00,110.75,236.253,60.226000000000006,32.613 -2020-02-03 06:30:00,114.63,239.93900000000002,60.226000000000006,32.613 -2020-02-03 06:45:00,119.48,245.31099999999998,60.226000000000006,32.613 -2020-02-03 07:00:00,129.14,244.451,73.578,32.613 -2020-02-03 07:15:00,130.05,249.81900000000002,73.578,32.613 -2020-02-03 07:30:00,129.17,253.173,73.578,32.613 -2020-02-03 07:45:00,130.1,253.949,73.578,32.613 -2020-02-03 08:00:00,135.43,252.31099999999998,66.58,32.613 -2020-02-03 08:15:00,135.0,253.35,66.58,32.613 -2020-02-03 08:30:00,134.72,250.42,66.58,32.613 -2020-02-03 08:45:00,136.03,247.53799999999998,66.58,32.613 -2020-02-03 09:00:00,138.01,241.83900000000003,62.0,32.613 -2020-02-03 09:15:00,139.79,237.245,62.0,32.613 -2020-02-03 09:30:00,141.36,235.02900000000002,62.0,32.613 -2020-02-03 09:45:00,139.59,232.144,62.0,32.613 -2020-02-03 10:00:00,138.43,227.13400000000001,59.099,32.613 -2020-02-03 10:15:00,138.67,224.736,59.099,32.613 -2020-02-03 10:30:00,137.73,220.82299999999998,59.099,32.613 -2020-02-03 10:45:00,137.82,219.03,59.099,32.613 -2020-02-03 11:00:00,138.07,214.34599999999998,57.729,32.613 -2020-02-03 11:15:00,139.42,213.59,57.729,32.613 -2020-02-03 11:30:00,139.86,213.06599999999997,57.729,32.613 -2020-02-03 11:45:00,140.03,211.54,57.729,32.613 -2020-02-03 12:00:00,139.07,209.048,55.615,32.613 -2020-02-03 12:15:00,138.54,209.49200000000002,55.615,32.613 -2020-02-03 12:30:00,136.96,208.28099999999998,55.615,32.613 -2020-02-03 12:45:00,137.41,209.297,55.615,32.613 -2020-02-03 13:00:00,134.33,207.68,56.515,32.613 -2020-02-03 13:15:00,132.51,207.333,56.515,32.613 -2020-02-03 13:30:00,130.0,205.06400000000002,56.515,32.613 -2020-02-03 13:45:00,128.89,204.83,56.515,32.613 -2020-02-03 14:00:00,128.86,204.725,58.1,32.613 -2020-02-03 14:15:00,127.42,204.808,58.1,32.613 -2020-02-03 14:30:00,127.1,204.94799999999998,58.1,32.613 -2020-02-03 14:45:00,128.03,205.826,58.1,32.613 -2020-02-03 15:00:00,129.69,207.56599999999997,59.801,32.613 -2020-02-03 15:15:00,135.62,207.56099999999998,59.801,32.613 -2020-02-03 15:30:00,130.78,208.795,59.801,32.613 -2020-02-03 15:45:00,132.56,210.171,59.801,32.613 -2020-02-03 16:00:00,130.0,211.095,62.901,32.613 -2020-02-03 16:15:00,128.75,212.358,62.901,32.613 -2020-02-03 16:30:00,126.25,214.46200000000002,62.901,32.613 -2020-02-03 16:45:00,126.93,215.51,62.901,32.613 -2020-02-03 17:00:00,132.31,217.107,70.418,32.613 -2020-02-03 17:15:00,134.91,218.438,70.418,32.613 -2020-02-03 17:30:00,139.16,219.222,70.418,32.613 -2020-02-03 17:45:00,137.81,219.636,70.418,32.613 -2020-02-03 18:00:00,140.89,221.644,71.726,32.613 -2020-02-03 18:15:00,138.81,219.799,71.726,32.613 -2020-02-03 18:30:00,142.27,219.105,71.726,32.613 -2020-02-03 18:45:00,138.78,218.90900000000002,71.726,32.613 -2020-02-03 19:00:00,137.55,216.71,65.997,32.613 -2020-02-03 19:15:00,133.6,213.172,65.997,32.613 -2020-02-03 19:30:00,132.16,211.24599999999998,65.997,32.613 -2020-02-03 19:45:00,130.1,208.445,65.997,32.613 -2020-02-03 20:00:00,123.56,203.873,68.09100000000001,32.613 -2020-02-03 20:15:00,121.51,197.967,68.09100000000001,32.613 -2020-02-03 20:30:00,117.5,192.733,68.09100000000001,32.613 -2020-02-03 20:45:00,116.06,192.24400000000003,68.09100000000001,32.613 -2020-02-03 21:00:00,112.08,189.36900000000003,59.617,32.613 -2020-02-03 21:15:00,119.36,185.173,59.617,32.613 -2020-02-03 21:30:00,115.51,183.97799999999998,59.617,32.613 -2020-02-03 21:45:00,112.17,182.554,59.617,32.613 -2020-02-03 22:00:00,102.03,173.428,54.938,32.613 -2020-02-03 22:15:00,101.13,168.33,54.938,32.613 -2020-02-03 22:30:00,103.23,154.47899999999998,54.938,32.613 -2020-02-03 22:45:00,102.28,146.101,54.938,32.613 -2020-02-03 23:00:00,97.78,138.697,47.43,32.613 -2020-02-03 23:15:00,93.27,138.597,47.43,32.613 -2020-02-03 23:30:00,87.74,139.533,47.43,32.613 -2020-02-03 23:45:00,93.42,139.745,47.43,32.613 -2020-02-04 00:00:00,90.86,133.845,48.354,32.613 -2020-02-04 00:15:00,91.14,133.235,48.354,32.613 -2020-02-04 00:30:00,85.35,134.276,48.354,32.613 -2020-02-04 00:45:00,87.43,135.703,48.354,32.613 -2020-02-04 01:00:00,85.05,138.391,45.68600000000001,32.613 -2020-02-04 01:15:00,86.31,138.264,45.68600000000001,32.613 -2020-02-04 01:30:00,82.43,138.523,45.68600000000001,32.613 -2020-02-04 01:45:00,87.86,138.858,45.68600000000001,32.613 -2020-02-04 02:00:00,86.41,141.167,44.269,32.613 -2020-02-04 02:15:00,87.36,143.13,44.269,32.613 -2020-02-04 02:30:00,82.98,144.28,44.269,32.613 -2020-02-04 02:45:00,87.34,146.35399999999998,44.269,32.613 -2020-02-04 03:00:00,85.37,149.16899999999998,44.187,32.613 -2020-02-04 03:15:00,87.11,150.472,44.187,32.613 -2020-02-04 03:30:00,84.96,152.24200000000002,44.187,32.613 -2020-02-04 03:45:00,83.22,154.197,44.187,32.613 -2020-02-04 04:00:00,82.45,165.356,46.126999999999995,32.613 -2020-02-04 04:15:00,83.66,176.574,46.126999999999995,32.613 -2020-02-04 04:30:00,85.46,180.15099999999998,46.126999999999995,32.613 -2020-02-04 04:45:00,87.52,182.96599999999998,46.126999999999995,32.613 -2020-02-04 05:00:00,92.46,216.549,49.666000000000004,32.613 -2020-02-04 05:15:00,94.39,243.795,49.666000000000004,32.613 -2020-02-04 05:30:00,96.94,239.44099999999997,49.666000000000004,32.613 -2020-02-04 05:45:00,101.07,233.153,49.666000000000004,32.613 -2020-02-04 06:00:00,109.2,230.50799999999998,61.077,32.613 -2020-02-04 06:15:00,111.14,236.672,61.077,32.613 -2020-02-04 06:30:00,117.58,239.65099999999998,61.077,32.613 -2020-02-04 06:45:00,123.45,244.737,61.077,32.613 -2020-02-04 07:00:00,133.3,243.669,74.717,32.613 -2020-02-04 07:15:00,131.29,248.86900000000003,74.717,32.613 -2020-02-04 07:30:00,133.43,251.56799999999998,74.717,32.613 -2020-02-04 07:45:00,133.13,252.69799999999998,74.717,32.613 -2020-02-04 08:00:00,136.35,251.172,69.033,32.613 -2020-02-04 08:15:00,136.11,251.097,69.033,32.613 -2020-02-04 08:30:00,134.66,247.888,69.033,32.613 -2020-02-04 08:45:00,134.24,244.832,69.033,32.613 -2020-02-04 09:00:00,133.77,238.14700000000002,63.113,32.613 -2020-02-04 09:15:00,135.41,235.375,63.113,32.613 -2020-02-04 09:30:00,136.68,233.847,63.113,32.613 -2020-02-04 09:45:00,135.45,230.52900000000002,63.113,32.613 -2020-02-04 10:00:00,134.9,225.05700000000002,61.461999999999996,32.613 -2020-02-04 10:15:00,134.83,221.503,61.461999999999996,32.613 -2020-02-04 10:30:00,135.76,217.795,61.461999999999996,32.613 -2020-02-04 10:45:00,134.18,216.19,61.461999999999996,32.613 -2020-02-04 11:00:00,131.66,213.17,59.614,32.613 -2020-02-04 11:15:00,132.42,211.995,59.614,32.613 -2020-02-04 11:30:00,131.61,210.327,59.614,32.613 -2020-02-04 11:45:00,129.45,209.65900000000002,59.614,32.613 -2020-02-04 12:00:00,126.71,205.668,57.415,32.613 -2020-02-04 12:15:00,125.47,205.61599999999999,57.415,32.613 -2020-02-04 12:30:00,128.08,205.076,57.415,32.613 -2020-02-04 12:45:00,125.59,205.66400000000002,57.415,32.613 -2020-02-04 13:00:00,123.19,203.65400000000002,58.534,32.613 -2020-02-04 13:15:00,122.62,202.643,58.534,32.613 -2020-02-04 13:30:00,119.8,201.68,58.534,32.613 -2020-02-04 13:45:00,120.47,201.791,58.534,32.613 -2020-02-04 14:00:00,121.58,201.998,59.415,32.613 -2020-02-04 14:15:00,123.66,202.265,59.415,32.613 -2020-02-04 14:30:00,122.74,203.05599999999998,59.415,32.613 -2020-02-04 14:45:00,123.25,203.967,59.415,32.613 -2020-02-04 15:00:00,126.72,205.26,62.071999999999996,32.613 -2020-02-04 15:15:00,128.05,205.477,62.071999999999996,32.613 -2020-02-04 15:30:00,126.41,206.958,62.071999999999996,32.613 -2020-02-04 15:45:00,125.03,207.822,62.071999999999996,32.613 -2020-02-04 16:00:00,125.33,209.22799999999998,64.99,32.613 -2020-02-04 16:15:00,124.64,211.00400000000002,64.99,32.613 -2020-02-04 16:30:00,127.25,213.877,64.99,32.613 -2020-02-04 16:45:00,128.17,215.144,64.99,32.613 -2020-02-04 17:00:00,133.12,217.313,72.658,32.613 -2020-02-04 17:15:00,136.25,218.657,72.658,32.613 -2020-02-04 17:30:00,140.8,220.261,72.658,32.613 -2020-02-04 17:45:00,141.04,220.595,72.658,32.613 -2020-02-04 18:00:00,142.74,222.60299999999998,73.645,32.613 -2020-02-04 18:15:00,143.03,220.092,73.645,32.613 -2020-02-04 18:30:00,140.63,219.092,73.645,32.613 -2020-02-04 18:45:00,138.97,219.81400000000002,73.645,32.613 -2020-02-04 19:00:00,135.76,217.78,67.085,32.613 -2020-02-04 19:15:00,134.8,213.929,67.085,32.613 -2020-02-04 19:30:00,140.27,211.31400000000002,67.085,32.613 -2020-02-04 19:45:00,133.39,208.546,67.085,32.613 -2020-02-04 20:00:00,125.17,204.082,66.138,32.613 -2020-02-04 20:15:00,128.55,197.642,66.138,32.613 -2020-02-04 20:30:00,119.49,193.523,66.138,32.613 -2020-02-04 20:45:00,118.62,192.36700000000002,66.138,32.613 -2020-02-04 21:00:00,111.08,188.612,57.512,32.613 -2020-02-04 21:15:00,115.78,185.55599999999998,57.512,32.613 -2020-02-04 21:30:00,115.31,183.541,57.512,32.613 -2020-02-04 21:45:00,112.98,182.362,57.512,32.613 -2020-02-04 22:00:00,101.24,175.079,54.545,32.613 -2020-02-04 22:15:00,101.11,169.757,54.545,32.613 -2020-02-04 22:30:00,101.29,155.946,54.545,32.613 -2020-02-04 22:45:00,102.32,147.877,54.545,32.613 -2020-02-04 23:00:00,97.65,140.57399999999998,48.605,32.613 -2020-02-04 23:15:00,94.45,139.328,48.605,32.613 -2020-02-04 23:30:00,87.36,139.886,48.605,32.613 -2020-02-04 23:45:00,92.55,139.616,48.605,32.613 -2020-02-05 00:00:00,89.46,133.739,45.675,32.613 -2020-02-05 00:15:00,90.37,133.12,45.675,32.613 -2020-02-05 00:30:00,85.45,134.141,45.675,32.613 -2020-02-05 00:45:00,84.27,135.56,45.675,32.613 -2020-02-05 01:00:00,85.32,138.22299999999998,43.015,32.613 -2020-02-05 01:15:00,85.92,138.083,43.015,32.613 -2020-02-05 01:30:00,85.0,138.333,43.015,32.613 -2020-02-05 01:45:00,83.16,138.666,43.015,32.613 -2020-02-05 02:00:00,83.15,140.976,41.0,32.613 -2020-02-05 02:15:00,77.78,142.938,41.0,32.613 -2020-02-05 02:30:00,81.84,144.102,41.0,32.613 -2020-02-05 02:45:00,86.05,146.175,41.0,32.613 -2020-02-05 03:00:00,81.25,148.99200000000002,41.318000000000005,32.613 -2020-02-05 03:15:00,88.66,150.3,41.318000000000005,32.613 -2020-02-05 03:30:00,89.0,152.067,41.318000000000005,32.613 -2020-02-05 03:45:00,89.87,154.033,41.318000000000005,32.613 -2020-02-05 04:00:00,85.75,165.185,42.544,32.613 -2020-02-05 04:15:00,82.09,176.393,42.544,32.613 -2020-02-05 04:30:00,83.82,179.982,42.544,32.613 -2020-02-05 04:45:00,88.66,182.785,42.544,32.613 -2020-02-05 05:00:00,90.45,216.34400000000002,45.161,32.613 -2020-02-05 05:15:00,92.16,243.595,45.161,32.613 -2020-02-05 05:30:00,94.66,239.22400000000002,45.161,32.613 -2020-02-05 05:45:00,100.11,232.94299999999998,45.161,32.613 -2020-02-05 06:00:00,110.31,230.312,61.86600000000001,32.613 -2020-02-05 06:15:00,112.52,236.489,61.86600000000001,32.613 -2020-02-05 06:30:00,118.94,239.447,61.86600000000001,32.613 -2020-02-05 06:45:00,122.35,244.541,61.86600000000001,32.613 -2020-02-05 07:00:00,129.73,243.495,77.814,32.613 -2020-02-05 07:15:00,130.12,248.67,77.814,32.613 -2020-02-05 07:30:00,132.0,251.338,77.814,32.613 -2020-02-05 07:45:00,130.03,252.429,77.814,32.613 -2020-02-05 08:00:00,133.79,250.885,70.251,32.613 -2020-02-05 08:15:00,132.59,250.78599999999997,70.251,32.613 -2020-02-05 08:30:00,134.41,247.52200000000002,70.251,32.613 -2020-02-05 08:45:00,131.14,244.46200000000002,70.251,32.613 -2020-02-05 09:00:00,131.14,237.77700000000002,66.965,32.613 -2020-02-05 09:15:00,131.67,235.00900000000001,66.965,32.613 -2020-02-05 09:30:00,133.35,233.50099999999998,66.965,32.613 -2020-02-05 09:45:00,132.54,230.18200000000002,66.965,32.613 -2020-02-05 10:00:00,133.0,224.718,63.628,32.613 -2020-02-05 10:15:00,130.23,221.18900000000002,63.628,32.613 -2020-02-05 10:30:00,128.04,217.488,63.628,32.613 -2020-02-05 10:45:00,128.01,215.894,63.628,32.613 -2020-02-05 11:00:00,127.57,212.861,62.516999999999996,32.613 -2020-02-05 11:15:00,126.97,211.695,62.516999999999996,32.613 -2020-02-05 11:30:00,127.26,210.033,62.516999999999996,32.613 -2020-02-05 11:45:00,128.78,209.375,62.516999999999996,32.613 -2020-02-05 12:00:00,125.28,205.40099999999998,60.888999999999996,32.613 -2020-02-05 12:15:00,123.86,205.365,60.888999999999996,32.613 -2020-02-05 12:30:00,125.88,204.801,60.888999999999996,32.613 -2020-02-05 12:45:00,122.32,205.387,60.888999999999996,32.613 -2020-02-05 13:00:00,120.11,203.394,61.57899999999999,32.613 -2020-02-05 13:15:00,120.74,202.361,61.57899999999999,32.613 -2020-02-05 13:30:00,121.03,201.382,61.57899999999999,32.613 -2020-02-05 13:45:00,121.19,201.493,61.57899999999999,32.613 -2020-02-05 14:00:00,119.29,201.74900000000002,62.602,32.613 -2020-02-05 14:15:00,121.95,201.998,62.602,32.613 -2020-02-05 14:30:00,121.58,202.77700000000002,62.602,32.613 -2020-02-05 14:45:00,122.34,203.703,62.602,32.613 -2020-02-05 15:00:00,125.21,205.007,64.259,32.613 -2020-02-05 15:15:00,123.42,205.195,64.259,32.613 -2020-02-05 15:30:00,126.69,206.644,64.259,32.613 -2020-02-05 15:45:00,122.86,207.49099999999999,64.259,32.613 -2020-02-05 16:00:00,126.84,208.898,67.632,32.613 -2020-02-05 16:15:00,124.67,210.669,67.632,32.613 -2020-02-05 16:30:00,126.69,213.547,67.632,32.613 -2020-02-05 16:45:00,128.11,214.801,67.632,32.613 -2020-02-05 17:00:00,131.41,216.96400000000003,72.583,32.613 -2020-02-05 17:15:00,135.42,218.33599999999998,72.583,32.613 -2020-02-05 17:30:00,141.15,219.976,72.583,32.613 -2020-02-05 17:45:00,142.13,220.33599999999998,72.583,32.613 -2020-02-05 18:00:00,142.56,222.359,72.744,32.613 -2020-02-05 18:15:00,140.69,219.895,72.744,32.613 -2020-02-05 18:30:00,140.23,218.899,72.744,32.613 -2020-02-05 18:45:00,138.2,219.645,72.744,32.613 -2020-02-05 19:00:00,133.78,217.56900000000002,69.684,32.613 -2020-02-05 19:15:00,135.47,213.73,69.684,32.613 -2020-02-05 19:30:00,139.35,211.13400000000001,69.684,32.613 -2020-02-05 19:45:00,137.95,208.394,69.684,32.613 -2020-02-05 20:00:00,129.71,203.90200000000002,70.036,32.613 -2020-02-05 20:15:00,121.3,197.472,70.036,32.613 -2020-02-05 20:30:00,116.66,193.362,70.036,32.613 -2020-02-05 20:45:00,118.71,192.213,70.036,32.613 -2020-02-05 21:00:00,109.48,188.44,60.431999999999995,32.613 -2020-02-05 21:15:00,115.23,185.36900000000003,60.431999999999995,32.613 -2020-02-05 21:30:00,114.27,183.355,60.431999999999995,32.613 -2020-02-05 21:45:00,111.11,182.192,60.431999999999995,32.613 -2020-02-05 22:00:00,102.57,174.896,56.2,32.613 -2020-02-05 22:15:00,98.72,169.59799999999998,56.2,32.613 -2020-02-05 22:30:00,95.62,155.756,56.2,32.613 -2020-02-05 22:45:00,97.92,147.694,56.2,32.613 -2020-02-05 23:00:00,93.72,140.384,47.927,32.613 -2020-02-05 23:15:00,95.58,139.151,47.927,32.613 -2020-02-05 23:30:00,93.57,139.726,47.927,32.613 -2020-02-05 23:45:00,93.42,139.472,47.927,32.613 -2020-02-06 00:00:00,88.03,133.625,43.794,32.613 -2020-02-06 00:15:00,88.69,132.998,43.794,32.613 -2020-02-06 00:30:00,88.37,134.0,43.794,32.613 -2020-02-06 00:45:00,83.31,135.411,43.794,32.613 -2020-02-06 01:00:00,85.13,138.046,42.397,32.613 -2020-02-06 01:15:00,86.78,137.894,42.397,32.613 -2020-02-06 01:30:00,84.13,138.134,42.397,32.613 -2020-02-06 01:45:00,81.34,138.464,42.397,32.613 -2020-02-06 02:00:00,83.95,140.776,40.010999999999996,32.613 -2020-02-06 02:15:00,85.6,142.738,40.010999999999996,32.613 -2020-02-06 02:30:00,84.94,143.915,40.010999999999996,32.613 -2020-02-06 02:45:00,83.06,145.987,40.010999999999996,32.613 -2020-02-06 03:00:00,85.59,148.806,39.181,32.613 -2020-02-06 03:15:00,88.0,150.118,39.181,32.613 -2020-02-06 03:30:00,87.64,151.882,39.181,32.613 -2020-02-06 03:45:00,86.23,153.86,39.181,32.613 -2020-02-06 04:00:00,87.6,165.005,40.39,32.613 -2020-02-06 04:15:00,83.48,176.203,40.39,32.613 -2020-02-06 04:30:00,84.04,179.805,40.39,32.613 -2020-02-06 04:45:00,86.36,182.597,40.39,32.613 -2020-02-06 05:00:00,90.39,216.13299999999998,45.504,32.613 -2020-02-06 05:15:00,92.47,243.39,45.504,32.613 -2020-02-06 05:30:00,94.94,238.99900000000002,45.504,32.613 -2020-02-06 05:45:00,99.52,232.726,45.504,32.613 -2020-02-06 06:00:00,109.99,230.108,57.748000000000005,32.613 -2020-02-06 06:15:00,113.45,236.298,57.748000000000005,32.613 -2020-02-06 06:30:00,117.48,239.236,57.748000000000005,32.613 -2020-02-06 06:45:00,122.09,244.335,57.748000000000005,32.613 -2020-02-06 07:00:00,130.35,243.31099999999998,72.138,32.613 -2020-02-06 07:15:00,131.22,248.46200000000002,72.138,32.613 -2020-02-06 07:30:00,134.4,251.09599999999998,72.138,32.613 -2020-02-06 07:45:00,135.28,252.148,72.138,32.613 -2020-02-06 08:00:00,138.42,250.588,65.542,32.613 -2020-02-06 08:15:00,139.12,250.46400000000003,65.542,32.613 -2020-02-06 08:30:00,135.68,247.144,65.542,32.613 -2020-02-06 08:45:00,135.18,244.079,65.542,32.613 -2020-02-06 09:00:00,136.0,237.396,60.523,32.613 -2020-02-06 09:15:00,137.89,234.63099999999997,60.523,32.613 -2020-02-06 09:30:00,140.17,233.143,60.523,32.613 -2020-02-06 09:45:00,143.52,229.824,60.523,32.613 -2020-02-06 10:00:00,140.22,224.36900000000003,57.449,32.613 -2020-02-06 10:15:00,141.82,220.864,57.449,32.613 -2020-02-06 10:30:00,141.15,217.17,57.449,32.613 -2020-02-06 10:45:00,141.82,215.588,57.449,32.613 -2020-02-06 11:00:00,140.12,212.542,54.505,32.613 -2020-02-06 11:15:00,141.56,211.387,54.505,32.613 -2020-02-06 11:30:00,142.0,209.731,54.505,32.613 -2020-02-06 11:45:00,141.74,209.082,54.505,32.613 -2020-02-06 12:00:00,141.83,205.12400000000002,51.50899999999999,32.613 -2020-02-06 12:15:00,140.81,205.106,51.50899999999999,32.613 -2020-02-06 12:30:00,138.96,204.518,51.50899999999999,32.613 -2020-02-06 12:45:00,136.26,205.1,51.50899999999999,32.613 -2020-02-06 13:00:00,133.75,203.12599999999998,51.303999999999995,32.613 -2020-02-06 13:15:00,132.36,202.07,51.303999999999995,32.613 -2020-02-06 13:30:00,131.1,201.075,51.303999999999995,32.613 -2020-02-06 13:45:00,131.74,201.18599999999998,51.303999999999995,32.613 -2020-02-06 14:00:00,131.95,201.49400000000003,52.785,32.613 -2020-02-06 14:15:00,131.78,201.72299999999998,52.785,32.613 -2020-02-06 14:30:00,129.87,202.489,52.785,32.613 -2020-02-06 14:45:00,131.48,203.43099999999998,52.785,32.613 -2020-02-06 15:00:00,130.36,204.74599999999998,56.458999999999996,32.613 -2020-02-06 15:15:00,130.85,204.90400000000002,56.458999999999996,32.613 -2020-02-06 15:30:00,132.16,206.32,56.458999999999996,32.613 -2020-02-06 15:45:00,129.54,207.15200000000002,56.458999999999996,32.613 -2020-02-06 16:00:00,131.64,208.558,59.388000000000005,32.613 -2020-02-06 16:15:00,130.67,210.32299999999998,59.388000000000005,32.613 -2020-02-06 16:30:00,131.07,213.205,59.388000000000005,32.613 -2020-02-06 16:45:00,131.83,214.446,59.388000000000005,32.613 -2020-02-06 17:00:00,136.93,216.605,64.462,32.613 -2020-02-06 17:15:00,139.65,218.00400000000002,64.462,32.613 -2020-02-06 17:30:00,141.81,219.679,64.462,32.613 -2020-02-06 17:45:00,143.36,220.06400000000002,64.462,32.613 -2020-02-06 18:00:00,143.08,222.104,65.128,32.613 -2020-02-06 18:15:00,141.76,219.69,65.128,32.613 -2020-02-06 18:30:00,142.88,218.695,65.128,32.613 -2020-02-06 18:45:00,140.14,219.465,65.128,32.613 -2020-02-06 19:00:00,137.45,217.34799999999998,61.316,32.613 -2020-02-06 19:15:00,136.1,213.52,61.316,32.613 -2020-02-06 19:30:00,140.99,210.94400000000002,61.316,32.613 -2020-02-06 19:45:00,140.88,208.235,61.316,32.613 -2020-02-06 20:00:00,130.65,203.713,59.845,32.613 -2020-02-06 20:15:00,123.56,197.295,59.845,32.613 -2020-02-06 20:30:00,119.45,193.192,59.845,32.613 -2020-02-06 20:45:00,118.13,192.05,59.845,32.613 -2020-02-06 21:00:00,110.84,188.26,54.83,32.613 -2020-02-06 21:15:00,117.96,185.175,54.83,32.613 -2020-02-06 21:30:00,117.04,183.16,54.83,32.613 -2020-02-06 21:45:00,112.67,182.015,54.83,32.613 -2020-02-06 22:00:00,101.49,174.704,50.933,32.613 -2020-02-06 22:15:00,101.52,169.43,50.933,32.613 -2020-02-06 22:30:00,97.68,155.55700000000002,50.933,32.613 -2020-02-06 22:45:00,100.63,147.503,50.933,32.613 -2020-02-06 23:00:00,98.2,140.183,45.32899999999999,32.613 -2020-02-06 23:15:00,98.49,138.966,45.32899999999999,32.613 -2020-02-06 23:30:00,93.7,139.555,45.32899999999999,32.613 -2020-02-06 23:45:00,88.78,139.321,45.32899999999999,32.613 -2020-02-07 00:00:00,88.69,132.535,43.74,32.613 -2020-02-07 00:15:00,91.22,132.095,43.74,32.613 -2020-02-07 00:30:00,91.01,132.892,43.74,32.613 -2020-02-07 00:45:00,85.92,134.365,43.74,32.613 -2020-02-07 01:00:00,86.38,136.671,42.555,32.613 -2020-02-07 01:15:00,86.96,137.619,42.555,32.613 -2020-02-07 01:30:00,85.7,137.502,42.555,32.613 -2020-02-07 01:45:00,78.77,137.983,42.555,32.613 -2020-02-07 02:00:00,77.21,140.266,41.68600000000001,32.613 -2020-02-07 02:15:00,78.6,142.107,41.68600000000001,32.613 -2020-02-07 02:30:00,78.82,143.773,41.68600000000001,32.613 -2020-02-07 02:45:00,79.14,145.988,41.68600000000001,32.613 -2020-02-07 03:00:00,80.57,147.57399999999998,42.278999999999996,32.613 -2020-02-07 03:15:00,87.01,150.124,42.278999999999996,32.613 -2020-02-07 03:30:00,88.53,151.899,42.278999999999996,32.613 -2020-02-07 03:45:00,91.75,154.13299999999998,42.278999999999996,32.613 -2020-02-07 04:00:00,81.9,165.515,43.742,32.613 -2020-02-07 04:15:00,81.41,176.655,43.742,32.613 -2020-02-07 04:30:00,83.77,180.39,43.742,32.613 -2020-02-07 04:45:00,85.67,181.968,43.742,32.613 -2020-02-07 05:00:00,89.49,214.108,46.973,32.613 -2020-02-07 05:15:00,90.53,242.95,46.973,32.613 -2020-02-07 05:30:00,95.28,239.739,46.973,32.613 -2020-02-07 05:45:00,99.96,233.467,46.973,32.613 -2020-02-07 06:00:00,110.48,231.317,59.63399999999999,32.613 -2020-02-07 06:15:00,113.13,235.822,59.63399999999999,32.613 -2020-02-07 06:30:00,118.49,237.761,59.63399999999999,32.613 -2020-02-07 06:45:00,122.09,244.732,59.63399999999999,32.613 -2020-02-07 07:00:00,129.43,242.68599999999998,71.631,32.613 -2020-02-07 07:15:00,129.96,248.84,71.631,32.613 -2020-02-07 07:30:00,130.46,251.521,71.631,32.613 -2020-02-07 07:45:00,133.64,251.55700000000002,71.631,32.613 -2020-02-07 08:00:00,137.27,248.618,66.181,32.613 -2020-02-07 08:15:00,133.27,247.94099999999997,66.181,32.613 -2020-02-07 08:30:00,132.95,245.679,66.181,32.613 -2020-02-07 08:45:00,131.08,240.852,66.181,32.613 -2020-02-07 09:00:00,132.18,234.997,63.086000000000006,32.613 -2020-02-07 09:15:00,132.77,232.62599999999998,63.086000000000006,32.613 -2020-02-07 09:30:00,132.22,230.766,63.086000000000006,32.613 -2020-02-07 09:45:00,132.21,227.275,63.086000000000006,32.613 -2020-02-07 10:00:00,132.16,220.571,60.886,32.613 -2020-02-07 10:15:00,134.75,217.90599999999998,60.886,32.613 -2020-02-07 10:30:00,132.78,214.06599999999997,60.886,32.613 -2020-02-07 10:45:00,131.45,212.013,60.886,32.613 -2020-02-07 11:00:00,127.82,208.912,59.391000000000005,32.613 -2020-02-07 11:15:00,128.72,206.90200000000002,59.391000000000005,32.613 -2020-02-07 11:30:00,126.6,207.252,59.391000000000005,32.613 -2020-02-07 11:45:00,125.26,206.785,59.391000000000005,32.613 -2020-02-07 12:00:00,123.4,204.003,56.172,32.613 -2020-02-07 12:15:00,122.69,201.739,56.172,32.613 -2020-02-07 12:30:00,121.83,201.28900000000002,56.172,32.613 -2020-02-07 12:45:00,121.23,202.543,56.172,32.613 -2020-02-07 13:00:00,119.63,201.565,54.406000000000006,32.613 -2020-02-07 13:15:00,119.78,201.37599999999998,54.406000000000006,32.613 -2020-02-07 13:30:00,118.9,200.287,54.406000000000006,32.613 -2020-02-07 13:45:00,118.56,200.28799999999998,54.406000000000006,32.613 -2020-02-07 14:00:00,115.99,199.468,53.578,32.613 -2020-02-07 14:15:00,117.39,199.42700000000002,53.578,32.613 -2020-02-07 14:30:00,118.46,200.57,53.578,32.613 -2020-02-07 14:45:00,121.39,201.94799999999998,53.578,32.613 -2020-02-07 15:00:00,122.71,202.743,56.568999999999996,32.613 -2020-02-07 15:15:00,118.45,202.41299999999998,56.568999999999996,32.613 -2020-02-07 15:30:00,118.49,202.174,56.568999999999996,32.613 -2020-02-07 15:45:00,118.79,203.062,56.568999999999996,32.613 -2020-02-07 16:00:00,120.97,203.285,60.169,32.613 -2020-02-07 16:15:00,123.05,205.308,60.169,32.613 -2020-02-07 16:30:00,121.89,208.31900000000002,60.169,32.613 -2020-02-07 16:45:00,123.97,209.50400000000002,60.169,32.613 -2020-02-07 17:00:00,128.63,211.71099999999998,65.497,32.613 -2020-02-07 17:15:00,131.36,212.703,65.497,32.613 -2020-02-07 17:30:00,135.74,214.044,65.497,32.613 -2020-02-07 17:45:00,136.59,214.21599999999998,65.497,32.613 -2020-02-07 18:00:00,137.38,217.047,65.082,32.613 -2020-02-07 18:15:00,136.53,214.35,65.082,32.613 -2020-02-07 18:30:00,135.49,213.798,65.082,32.613 -2020-02-07 18:45:00,134.69,214.545,65.082,32.613 -2020-02-07 19:00:00,131.86,213.304,60.968,32.613 -2020-02-07 19:15:00,131.03,210.918,60.968,32.613 -2020-02-07 19:30:00,134.97,207.908,60.968,32.613 -2020-02-07 19:45:00,136.65,204.822,60.968,32.613 -2020-02-07 20:00:00,128.29,200.338,61.123000000000005,32.613 -2020-02-07 20:15:00,120.3,193.86,61.123000000000005,32.613 -2020-02-07 20:30:00,116.56,189.77,61.123000000000005,32.613 -2020-02-07 20:45:00,113.11,189.325,61.123000000000005,32.613 -2020-02-07 21:00:00,107.82,185.94299999999998,55.416000000000004,32.613 -2020-02-07 21:15:00,111.91,183.15400000000002,55.416000000000004,32.613 -2020-02-07 21:30:00,109.99,181.205,55.416000000000004,32.613 -2020-02-07 21:45:00,109.63,180.65099999999998,55.416000000000004,32.613 -2020-02-07 22:00:00,99.9,174.41299999999998,51.631,32.613 -2020-02-07 22:15:00,94.38,169.043,51.631,32.613 -2020-02-07 22:30:00,91.58,161.64,51.631,32.613 -2020-02-07 22:45:00,96.88,157.365,51.631,32.613 -2020-02-07 23:00:00,93.8,149.38299999999998,44.898,32.613 -2020-02-07 23:15:00,96.38,146.203,44.898,32.613 -2020-02-07 23:30:00,86.3,145.392,44.898,32.613 -2020-02-07 23:45:00,82.22,144.46200000000002,44.898,32.613 -2020-02-08 00:00:00,76.07,129.066,42.033,32.431999999999995 -2020-02-08 00:15:00,81.2,123.95200000000001,42.033,32.431999999999995 -2020-02-08 00:30:00,83.22,126.271,42.033,32.431999999999995 -2020-02-08 00:45:00,85.24,128.624,42.033,32.431999999999995 -2020-02-08 01:00:00,74.78,131.55700000000002,38.255,32.431999999999995 -2020-02-08 01:15:00,73.72,131.313,38.255,32.431999999999995 -2020-02-08 01:30:00,71.69,130.709,38.255,32.431999999999995 -2020-02-08 01:45:00,80.83,130.78799999999998,38.255,32.431999999999995 -2020-02-08 02:00:00,81.35,133.951,36.404,32.431999999999995 -2020-02-08 02:15:00,79.32,135.47799999999998,36.404,32.431999999999995 -2020-02-08 02:30:00,73.69,136.015,36.404,32.431999999999995 -2020-02-08 02:45:00,72.18,138.24,36.404,32.431999999999995 -2020-02-08 03:00:00,76.73,140.701,36.083,32.431999999999995 -2020-02-08 03:15:00,78.04,141.977,36.083,32.431999999999995 -2020-02-08 03:30:00,75.79,141.916,36.083,32.431999999999995 -2020-02-08 03:45:00,72.95,144.086,36.083,32.431999999999995 -2020-02-08 04:00:00,69.34,151.02,36.102,32.431999999999995 -2020-02-08 04:15:00,69.82,159.40200000000002,36.102,32.431999999999995 -2020-02-08 04:30:00,69.75,160.933,36.102,32.431999999999995 -2020-02-08 04:45:00,73.37,161.901,36.102,32.431999999999995 -2020-02-08 05:00:00,71.6,177.055,35.284,32.431999999999995 -2020-02-08 05:15:00,72.11,185.93599999999998,35.284,32.431999999999995 -2020-02-08 05:30:00,71.73,182.887,35.284,32.431999999999995 -2020-02-08 05:45:00,75.3,182.155,35.284,32.431999999999995 -2020-02-08 06:00:00,74.8,199.732,36.265,32.431999999999995 -2020-02-08 06:15:00,74.36,221.66,36.265,32.431999999999995 -2020-02-08 06:30:00,75.45,217.892,36.265,32.431999999999995 -2020-02-08 06:45:00,77.69,215.15,36.265,32.431999999999995 -2020-02-08 07:00:00,83.57,209.08700000000002,40.714,32.431999999999995 -2020-02-08 07:15:00,87.81,213.976,40.714,32.431999999999995 -2020-02-08 07:30:00,87.5,219.47400000000002,40.714,32.431999999999995 -2020-02-08 07:45:00,88.9,223.83700000000002,40.714,32.431999999999995 -2020-02-08 08:00:00,93.52,225.43400000000003,46.692,32.431999999999995 -2020-02-08 08:15:00,95.5,228.845,46.692,32.431999999999995 -2020-02-08 08:30:00,98.42,228.342,46.692,32.431999999999995 -2020-02-08 08:45:00,100.91,226.93599999999998,46.692,32.431999999999995 -2020-02-08 09:00:00,103.5,222.706,48.925,32.431999999999995 -2020-02-08 09:15:00,103.89,221.12,48.925,32.431999999999995 -2020-02-08 09:30:00,104.75,220.226,48.925,32.431999999999995 -2020-02-08 09:45:00,107.02,216.97299999999998,48.925,32.431999999999995 -2020-02-08 10:00:00,107.36,210.49,47.799,32.431999999999995 -2020-02-08 10:15:00,107.78,207.989,47.799,32.431999999999995 -2020-02-08 10:30:00,108.48,204.386,47.799,32.431999999999995 -2020-02-08 10:45:00,109.57,203.84799999999998,47.799,32.431999999999995 -2020-02-08 11:00:00,108.73,201.024,44.309,32.431999999999995 -2020-02-08 11:15:00,110.59,198.15400000000002,44.309,32.431999999999995 -2020-02-08 11:30:00,111.33,197.233,44.309,32.431999999999995 -2020-02-08 11:45:00,110.89,195.63099999999997,44.309,32.431999999999995 -2020-02-08 12:00:00,108.2,191.81599999999997,42.367,32.431999999999995 -2020-02-08 12:15:00,106.08,190.18,42.367,32.431999999999995 -2020-02-08 12:30:00,103.17,190.074,42.367,32.431999999999995 -2020-02-08 12:45:00,102.67,190.37599999999998,42.367,32.431999999999995 -2020-02-08 13:00:00,98.62,189.06799999999998,39.036,32.431999999999995 -2020-02-08 13:15:00,98.03,186.632,39.036,32.431999999999995 -2020-02-08 13:30:00,95.95,185.00099999999998,39.036,32.431999999999995 -2020-02-08 13:45:00,94.83,185.69799999999998,39.036,32.431999999999995 -2020-02-08 14:00:00,96.32,186.321,37.995,32.431999999999995 -2020-02-08 14:15:00,93.19,185.817,37.995,32.431999999999995 -2020-02-08 14:30:00,93.21,184.97400000000002,37.995,32.431999999999995 -2020-02-08 14:45:00,93.07,186.553,37.995,32.431999999999995 -2020-02-08 15:00:00,92.13,188.085,40.71,32.431999999999995 -2020-02-08 15:15:00,92.1,188.537,40.71,32.431999999999995 -2020-02-08 15:30:00,93.08,189.947,40.71,32.431999999999995 -2020-02-08 15:45:00,94.41,190.942,40.71,32.431999999999995 -2020-02-08 16:00:00,93.8,189.675,46.998000000000005,32.431999999999995 -2020-02-08 16:15:00,94.73,192.775,46.998000000000005,32.431999999999995 -2020-02-08 16:30:00,96.17,195.707,46.998000000000005,32.431999999999995 -2020-02-08 16:45:00,97.61,197.858,46.998000000000005,32.431999999999995 -2020-02-08 17:00:00,102.44,199.639,55.431000000000004,32.431999999999995 -2020-02-08 17:15:00,103.98,202.69299999999998,55.431000000000004,32.431999999999995 -2020-02-08 17:30:00,110.41,203.977,55.431000000000004,32.431999999999995 -2020-02-08 17:45:00,109.38,203.68,55.431000000000004,32.431999999999995 -2020-02-08 18:00:00,111.7,205.91099999999997,55.989,32.431999999999995 -2020-02-08 18:15:00,111.45,205.063,55.989,32.431999999999995 -2020-02-08 18:30:00,109.88,205.833,55.989,32.431999999999995 -2020-02-08 18:45:00,108.49,203.294,55.989,32.431999999999995 -2020-02-08 19:00:00,107.67,203.201,50.882,32.431999999999995 -2020-02-08 19:15:00,106.93,200.38299999999998,50.882,32.431999999999995 -2020-02-08 19:30:00,104.88,198.138,50.882,32.431999999999995 -2020-02-08 19:45:00,102.39,194.731,50.882,32.431999999999995 -2020-02-08 20:00:00,98.25,192.463,43.172,32.431999999999995 -2020-02-08 20:15:00,94.38,188.37400000000002,43.172,32.431999999999995 -2020-02-08 20:30:00,91.97,183.99599999999998,43.172,32.431999999999995 -2020-02-08 20:45:00,89.93,182.93900000000002,43.172,32.431999999999995 -2020-02-08 21:00:00,85.08,182.136,37.599000000000004,32.431999999999995 -2020-02-08 21:15:00,83.97,179.845,37.599000000000004,32.431999999999995 -2020-02-08 21:30:00,82.71,179.21599999999998,37.599000000000004,32.431999999999995 -2020-02-08 21:45:00,82.15,178.28900000000002,37.599000000000004,32.431999999999995 -2020-02-08 22:00:00,77.71,173.505,39.047,32.431999999999995 -2020-02-08 22:15:00,77.18,170.838,39.047,32.431999999999995 -2020-02-08 22:30:00,73.7,170.201,39.047,32.431999999999995 -2020-02-08 22:45:00,72.2,167.93,39.047,32.431999999999995 -2020-02-08 23:00:00,69.4,162.638,32.339,32.431999999999995 -2020-02-08 23:15:00,68.66,157.657,32.339,32.431999999999995 -2020-02-08 23:30:00,65.61,154.733,32.339,32.431999999999995 -2020-02-08 23:45:00,64.73,151.15200000000002,32.339,32.431999999999995 -2020-02-09 00:00:00,61.27,129.226,29.988000000000003,32.431999999999995 -2020-02-09 00:15:00,60.71,123.889,29.988000000000003,32.431999999999995 -2020-02-09 00:30:00,59.99,125.789,29.988000000000003,32.431999999999995 -2020-02-09 00:45:00,59.14,128.922,29.988000000000003,32.431999999999995 -2020-02-09 01:00:00,57.53,131.635,28.531999999999996,32.431999999999995 -2020-02-09 01:15:00,56.93,132.55,28.531999999999996,32.431999999999995 -2020-02-09 01:30:00,57.32,132.533,28.531999999999996,32.431999999999995 -2020-02-09 01:45:00,57.14,132.30700000000002,28.531999999999996,32.431999999999995 -2020-02-09 02:00:00,56.24,134.63,27.805999999999997,32.431999999999995 -2020-02-09 02:15:00,57.09,135.11700000000002,27.805999999999997,32.431999999999995 -2020-02-09 02:30:00,56.53,136.59799999999998,27.805999999999997,32.431999999999995 -2020-02-09 02:45:00,54.95,139.373,27.805999999999997,32.431999999999995 -2020-02-09 03:00:00,54.55,142.111,26.193,32.431999999999995 -2020-02-09 03:15:00,55.28,142.769,26.193,32.431999999999995 -2020-02-09 03:30:00,55.8,144.366,26.193,32.431999999999995 -2020-02-09 03:45:00,56.3,146.563,26.193,32.431999999999995 -2020-02-09 04:00:00,56.5,153.233,27.19,32.431999999999995 -2020-02-09 04:15:00,57.24,160.52700000000002,27.19,32.431999999999995 -2020-02-09 04:30:00,57.91,161.968,27.19,32.431999999999995 -2020-02-09 04:45:00,57.79,163.293,27.19,32.431999999999995 -2020-02-09 05:00:00,58.77,174.454,28.166999999999998,32.431999999999995 -2020-02-09 05:15:00,58.46,180.73,28.166999999999998,32.431999999999995 -2020-02-09 05:30:00,60.97,177.52599999999998,28.166999999999998,32.431999999999995 -2020-02-09 05:45:00,62.39,177.112,28.166999999999998,32.431999999999995 -2020-02-09 06:00:00,60.88,194.896,27.16,32.431999999999995 -2020-02-09 06:15:00,63.92,214.793,27.16,32.431999999999995 -2020-02-09 06:30:00,65.05,209.84799999999998,27.16,32.431999999999995 -2020-02-09 06:45:00,63.69,206.051,27.16,32.431999999999995 -2020-02-09 07:00:00,68.95,202.731,29.578000000000003,32.431999999999995 -2020-02-09 07:15:00,68.37,206.84900000000002,29.578000000000003,32.431999999999995 -2020-02-09 07:30:00,70.21,210.766,29.578000000000003,32.431999999999995 -2020-02-09 07:45:00,72.49,214.237,29.578000000000003,32.431999999999995 -2020-02-09 08:00:00,76.04,217.799,34.650999999999996,32.431999999999995 -2020-02-09 08:15:00,76.96,220.94,34.650999999999996,32.431999999999995 -2020-02-09 08:30:00,77.09,222.12400000000002,34.650999999999996,32.431999999999995 -2020-02-09 08:45:00,77.53,222.98,34.650999999999996,32.431999999999995 -2020-02-09 09:00:00,79.01,218.326,38.080999999999996,32.431999999999995 -2020-02-09 09:15:00,78.77,217.41299999999998,38.080999999999996,32.431999999999995 -2020-02-09 09:30:00,75.58,216.317,38.080999999999996,32.431999999999995 -2020-02-09 09:45:00,79.56,212.81599999999997,38.080999999999996,32.431999999999995 -2020-02-09 10:00:00,81.3,209.075,39.934,32.431999999999995 -2020-02-09 10:15:00,76.81,207.17700000000002,39.934,32.431999999999995 -2020-02-09 10:30:00,79.12,204.226,39.934,32.431999999999995 -2020-02-09 10:45:00,81.4,201.47099999999998,39.934,32.431999999999995 -2020-02-09 11:00:00,84.98,199.705,43.74100000000001,32.431999999999995 -2020-02-09 11:15:00,91.65,197.043,43.74100000000001,32.431999999999995 -2020-02-09 11:30:00,93.87,195.105,43.74100000000001,32.431999999999995 -2020-02-09 11:45:00,94.14,194.15599999999998,43.74100000000001,32.431999999999995 -2020-02-09 12:00:00,89.58,189.606,40.001999999999995,32.431999999999995 -2020-02-09 12:15:00,87.22,190.168,40.001999999999995,32.431999999999995 -2020-02-09 12:30:00,81.41,188.4,40.001999999999995,32.431999999999995 -2020-02-09 12:45:00,83.91,187.699,40.001999999999995,32.431999999999995 -2020-02-09 13:00:00,83.42,185.649,37.855,32.431999999999995 -2020-02-09 13:15:00,83.34,186.597,37.855,32.431999999999995 -2020-02-09 13:30:00,83.38,184.843,37.855,32.431999999999995 -2020-02-09 13:45:00,83.48,184.68400000000003,37.855,32.431999999999995 -2020-02-09 14:00:00,82.03,185.49900000000002,35.946999999999996,32.431999999999995 -2020-02-09 14:15:00,81.11,186.253,35.946999999999996,32.431999999999995 -2020-02-09 14:30:00,80.88,186.899,35.946999999999996,32.431999999999995 -2020-02-09 14:45:00,80.8,188.155,35.946999999999996,32.431999999999995 -2020-02-09 15:00:00,80.61,187.985,35.138000000000005,32.431999999999995 -2020-02-09 15:15:00,81.01,189.326,35.138000000000005,32.431999999999995 -2020-02-09 15:30:00,81.24,191.368,35.138000000000005,32.431999999999995 -2020-02-09 15:45:00,82.18,193.079,35.138000000000005,32.431999999999995 -2020-02-09 16:00:00,84.12,194.005,38.672,32.431999999999995 -2020-02-09 16:15:00,82.75,196.077,38.672,32.431999999999995 -2020-02-09 16:30:00,83.87,199.19400000000002,38.672,32.431999999999995 -2020-02-09 16:45:00,85.94,201.47099999999998,38.672,32.431999999999995 -2020-02-09 17:00:00,91.05,203.174,48.684,32.431999999999995 -2020-02-09 17:15:00,91.88,205.752,48.684,32.431999999999995 -2020-02-09 17:30:00,98.58,207.305,48.684,32.431999999999995 -2020-02-09 17:45:00,98.25,209.468,48.684,32.431999999999995 -2020-02-09 18:00:00,103.28,211.042,51.568999999999996,32.431999999999995 -2020-02-09 18:15:00,100.42,211.72099999999998,51.568999999999996,32.431999999999995 -2020-02-09 18:30:00,104.15,210.25400000000002,51.568999999999996,32.431999999999995 -2020-02-09 18:45:00,100.37,209.738,51.568999999999996,32.431999999999995 -2020-02-09 19:00:00,98.81,209.03599999999997,48.608000000000004,32.431999999999995 -2020-02-09 19:15:00,97.23,206.96599999999998,48.608000000000004,32.431999999999995 -2020-02-09 19:30:00,95.94,204.58599999999998,48.608000000000004,32.431999999999995 -2020-02-09 19:45:00,94.87,202.80900000000003,48.608000000000004,32.431999999999995 -2020-02-09 20:00:00,91.94,200.498,43.733999999999995,32.431999999999995 -2020-02-09 20:15:00,91.17,197.502,43.733999999999995,32.431999999999995 -2020-02-09 20:30:00,89.34,194.40599999999998,43.733999999999995,32.431999999999995 -2020-02-09 20:45:00,90.36,192.203,43.733999999999995,32.431999999999995 -2020-02-09 21:00:00,83.75,188.577,39.283,32.431999999999995 -2020-02-09 21:15:00,84.63,185.62,39.283,32.431999999999995 -2020-02-09 21:30:00,84.54,185.36900000000003,39.283,32.431999999999995 -2020-02-09 21:45:00,87.87,184.575,39.283,32.431999999999995 -2020-02-09 22:00:00,86.37,178.296,40.111,32.431999999999995 -2020-02-09 22:15:00,84.1,174.982,40.111,32.431999999999995 -2020-02-09 22:30:00,80.59,170.921,40.111,32.431999999999995 -2020-02-09 22:45:00,80.15,167.84900000000002,40.111,32.431999999999995 -2020-02-09 23:00:00,76.59,159.545,35.791,32.431999999999995 -2020-02-09 23:15:00,78.67,156.477,35.791,32.431999999999995 -2020-02-09 23:30:00,75.47,154.495,35.791,32.431999999999995 -2020-02-09 23:45:00,74.68,151.886,35.791,32.431999999999995 -2020-02-10 00:00:00,69.25,133.565,34.311,32.613 -2020-02-10 00:15:00,70.25,131.43200000000002,34.311,32.613 -2020-02-10 00:30:00,70.75,133.498,34.311,32.613 -2020-02-10 00:45:00,70.63,136.07399999999998,34.311,32.613 -2020-02-10 01:00:00,69.11,138.743,34.585,32.613 -2020-02-10 01:15:00,68.52,139.054,34.585,32.613 -2020-02-10 01:30:00,68.82,139.04399999999998,34.585,32.613 -2020-02-10 01:45:00,69.91,138.957,34.585,32.613 -2020-02-10 02:00:00,68.03,141.208,34.111,32.613 -2020-02-10 02:15:00,68.74,143.455,34.111,32.613 -2020-02-10 02:30:00,68.09,145.303,34.111,32.613 -2020-02-10 02:45:00,68.85,147.40200000000002,34.111,32.613 -2020-02-10 03:00:00,67.44,151.52200000000002,32.435,32.613 -2020-02-10 03:15:00,68.32,153.937,32.435,32.613 -2020-02-10 03:30:00,69.58,155.138,32.435,32.613 -2020-02-10 03:45:00,70.89,156.787,32.435,32.613 -2020-02-10 04:00:00,71.36,167.96099999999998,33.04,32.613 -2020-02-10 04:15:00,73.21,179.521,33.04,32.613 -2020-02-10 04:30:00,75.25,183.50900000000001,33.04,32.613 -2020-02-10 04:45:00,77.06,184.954,33.04,32.613 -2020-02-10 05:00:00,81.16,212.946,40.399,32.613 -2020-02-10 05:15:00,83.71,240.581,40.399,32.613 -2020-02-10 05:30:00,88.12,237.87599999999998,40.399,32.613 -2020-02-10 05:45:00,91.96,231.50799999999998,40.399,32.613 -2020-02-10 06:00:00,100.85,230.446,60.226000000000006,32.613 -2020-02-10 06:15:00,104.58,234.87099999999998,60.226000000000006,32.613 -2020-02-10 06:30:00,109.61,238.394,60.226000000000006,32.613 -2020-02-10 06:45:00,114.01,243.798,60.226000000000006,32.613 -2020-02-10 07:00:00,123.45,243.1,73.578,32.613 -2020-02-10 07:15:00,122.16,248.285,73.578,32.613 -2020-02-10 07:30:00,126.33,251.41,73.578,32.613 -2020-02-10 07:45:00,124.92,251.91099999999997,73.578,32.613 -2020-02-10 08:00:00,128.37,250.143,66.58,32.613 -2020-02-10 08:15:00,127.45,251.013,66.58,32.613 -2020-02-10 08:30:00,127.24,247.687,66.58,32.613 -2020-02-10 08:45:00,126.73,244.77900000000002,66.58,32.613 -2020-02-10 09:00:00,128.93,239.095,62.0,32.613 -2020-02-10 09:15:00,132.12,234.521,62.0,32.613 -2020-02-10 09:30:00,129.88,232.447,62.0,32.613 -2020-02-10 09:45:00,132.17,229.562,62.0,32.613 -2020-02-10 10:00:00,131.38,224.611,59.099,32.613 -2020-02-10 10:15:00,127.86,222.393,59.099,32.613 -2020-02-10 10:30:00,126.07,218.533,59.099,32.613 -2020-02-10 10:45:00,129.64,216.829,59.099,32.613 -2020-02-10 11:00:00,130.68,212.051,57.729,32.613 -2020-02-10 11:15:00,131.96,211.373,57.729,32.613 -2020-02-10 11:30:00,132.31,210.891,57.729,32.613 -2020-02-10 11:45:00,136.23,209.438,57.729,32.613 -2020-02-10 12:00:00,131.68,207.05599999999998,55.615,32.613 -2020-02-10 12:15:00,132.92,207.62,55.615,32.613 -2020-02-10 12:30:00,134.19,206.238,55.615,32.613 -2020-02-10 12:45:00,129.29,207.23,55.615,32.613 -2020-02-10 13:00:00,125.98,205.753,56.515,32.613 -2020-02-10 13:15:00,124.66,205.24,56.515,32.613 -2020-02-10 13:30:00,124.24,202.86,56.515,32.613 -2020-02-10 13:45:00,128.1,202.62900000000002,56.515,32.613 -2020-02-10 14:00:00,124.97,202.88400000000001,58.1,32.613 -2020-02-10 14:15:00,125.5,202.832,58.1,32.613 -2020-02-10 14:30:00,126.78,202.88299999999998,58.1,32.613 -2020-02-10 14:45:00,128.81,203.86900000000003,58.1,32.613 -2020-02-10 15:00:00,129.17,205.675,59.801,32.613 -2020-02-10 15:15:00,129.48,205.46599999999998,59.801,32.613 -2020-02-10 15:30:00,124.6,206.46400000000003,59.801,32.613 -2020-02-10 15:45:00,123.59,207.72099999999998,59.801,32.613 -2020-02-10 16:00:00,123.44,208.65,62.901,32.613 -2020-02-10 16:15:00,123.28,209.86700000000002,62.901,32.613 -2020-02-10 16:30:00,122.68,211.997,62.901,32.613 -2020-02-10 16:45:00,123.82,212.947,62.901,32.613 -2020-02-10 17:00:00,128.52,214.516,70.418,32.613 -2020-02-10 17:15:00,127.46,216.03799999999998,70.418,32.613 -2020-02-10 17:30:00,135.21,217.065,70.418,32.613 -2020-02-10 17:45:00,135.18,217.66099999999997,70.418,32.613 -2020-02-10 18:00:00,137.86,219.78099999999998,71.726,32.613 -2020-02-10 18:15:00,135.1,218.28900000000002,71.726,32.613 -2020-02-10 18:30:00,134.0,217.61,71.726,32.613 -2020-02-10 18:45:00,133.93,217.583,71.726,32.613 -2020-02-10 19:00:00,132.08,215.093,65.997,32.613 -2020-02-10 19:15:00,130.65,211.641,65.997,32.613 -2020-02-10 19:30:00,130.87,209.857,65.997,32.613 -2020-02-10 19:45:00,134.33,207.269,65.997,32.613 -2020-02-10 20:00:00,125.69,202.49400000000003,68.09100000000001,32.613 -2020-02-10 20:15:00,123.18,196.667,68.09100000000001,32.613 -2020-02-10 20:30:00,113.96,191.49900000000002,68.09100000000001,32.613 -2020-02-10 20:45:00,114.18,191.053,68.09100000000001,32.613 -2020-02-10 21:00:00,108.98,188.05599999999998,59.617,32.613 -2020-02-10 21:15:00,111.78,183.76,59.617,32.613 -2020-02-10 21:30:00,113.19,182.56599999999997,59.617,32.613 -2020-02-10 21:45:00,110.69,181.261,59.617,32.613 -2020-02-10 22:00:00,98.01,172.03099999999998,54.938,32.613 -2020-02-10 22:15:00,97.21,167.104,54.938,32.613 -2020-02-10 22:30:00,96.16,153.02700000000002,54.938,32.613 -2020-02-10 22:45:00,99.73,144.695,54.938,32.613 -2020-02-10 23:00:00,94.89,137.238,47.43,32.613 -2020-02-10 23:15:00,95.68,137.238,47.43,32.613 -2020-02-10 23:30:00,87.21,138.285,47.43,32.613 -2020-02-10 23:45:00,87.28,138.628,47.43,32.613 -2020-02-11 00:00:00,86.73,132.929,48.354,32.613 -2020-02-11 00:15:00,86.67,132.27200000000002,48.354,32.613 -2020-02-11 00:30:00,84.13,133.178,48.354,32.613 -2020-02-11 00:45:00,83.69,134.55200000000002,48.354,32.613 -2020-02-11 01:00:00,83.21,137.034,45.68600000000001,32.613 -2020-02-11 01:15:00,84.29,136.819,45.68600000000001,32.613 -2020-02-11 01:30:00,79.62,137.00799999999998,45.68600000000001,32.613 -2020-02-11 01:45:00,77.48,137.332,45.68600000000001,32.613 -2020-02-11 02:00:00,82.18,139.641,44.269,32.613 -2020-02-11 02:15:00,83.97,141.60399999999998,44.269,32.613 -2020-02-11 02:30:00,82.96,142.84799999999998,44.269,32.613 -2020-02-11 02:45:00,77.71,144.915,44.269,32.613 -2020-02-11 03:00:00,76.55,147.748,44.187,32.613 -2020-02-11 03:15:00,81.43,149.071,44.187,32.613 -2020-02-11 03:30:00,85.46,150.81799999999998,44.187,32.613 -2020-02-11 03:45:00,86.18,152.856,44.187,32.613 -2020-02-11 04:00:00,84.27,163.98,46.126999999999995,32.613 -2020-02-11 04:15:00,84.94,175.13400000000001,46.126999999999995,32.613 -2020-02-11 04:30:00,88.91,178.803,46.126999999999995,32.613 -2020-02-11 04:45:00,90.77,181.537,46.126999999999995,32.613 -2020-02-11 05:00:00,89.12,214.968,49.666000000000004,32.613 -2020-02-11 05:15:00,89.51,242.28799999999998,49.666000000000004,32.613 -2020-02-11 05:30:00,91.15,237.78400000000002,49.666000000000004,32.613 -2020-02-11 05:45:00,95.27,231.54,49.666000000000004,32.613 -2020-02-11 06:00:00,104.24,228.979,61.077,32.613 -2020-02-11 06:15:00,107.46,235.239,61.077,32.613 -2020-02-11 06:30:00,112.26,238.045,61.077,32.613 -2020-02-11 06:45:00,115.29,243.15599999999998,61.077,32.613 -2020-02-11 07:00:00,122.79,242.25,74.717,32.613 -2020-02-11 07:15:00,123.23,247.265,74.717,32.613 -2020-02-11 07:30:00,126.38,249.733,74.717,32.613 -2020-02-11 07:45:00,126.0,250.583,74.717,32.613 -2020-02-11 08:00:00,128.06,248.925,69.033,32.613 -2020-02-11 08:15:00,126.93,248.678,69.033,32.613 -2020-02-11 08:30:00,130.01,245.06900000000002,69.033,32.613 -2020-02-11 08:45:00,126.16,241.99200000000002,69.033,32.613 -2020-02-11 09:00:00,127.51,235.327,63.113,32.613 -2020-02-11 09:15:00,129.21,232.574,63.113,32.613 -2020-02-11 09:30:00,129.61,231.18599999999998,63.113,32.613 -2020-02-11 09:45:00,128.56,227.87099999999998,63.113,32.613 -2020-02-11 10:00:00,125.66,222.46099999999998,61.461999999999996,32.613 -2020-02-11 10:15:00,124.7,219.09099999999998,61.461999999999996,32.613 -2020-02-11 10:30:00,123.35,215.44,61.461999999999996,32.613 -2020-02-11 10:45:00,125.93,213.925,61.461999999999996,32.613 -2020-02-11 11:00:00,119.21,210.815,59.614,32.613 -2020-02-11 11:15:00,119.93,209.72,59.614,32.613 -2020-02-11 11:30:00,119.99,208.093,59.614,32.613 -2020-02-11 11:45:00,121.04,207.5,59.614,32.613 -2020-02-11 12:00:00,115.58,203.62099999999998,57.415,32.613 -2020-02-11 12:15:00,116.2,203.688,57.415,32.613 -2020-02-11 12:30:00,121.86,202.972,57.415,32.613 -2020-02-11 12:45:00,119.33,203.535,57.415,32.613 -2020-02-11 13:00:00,115.97,201.671,58.534,32.613 -2020-02-11 13:15:00,115.16,200.493,58.534,32.613 -2020-02-11 13:30:00,116.39,199.42,58.534,32.613 -2020-02-11 13:45:00,118.38,199.53400000000002,58.534,32.613 -2020-02-11 14:00:00,114.61,200.108,59.415,32.613 -2020-02-11 14:15:00,116.91,200.239,59.415,32.613 -2020-02-11 14:30:00,117.94,200.933,59.415,32.613 -2020-02-11 14:45:00,120.5,201.953,59.415,32.613 -2020-02-11 15:00:00,122.94,203.31099999999998,62.071999999999996,32.613 -2020-02-11 15:15:00,119.07,203.321,62.071999999999996,32.613 -2020-02-11 15:30:00,121.84,204.55900000000003,62.071999999999996,32.613 -2020-02-11 15:45:00,120.06,205.305,62.071999999999996,32.613 -2020-02-11 16:00:00,122.44,206.71400000000003,64.99,32.613 -2020-02-11 16:15:00,120.88,208.44099999999997,64.99,32.613 -2020-02-11 16:30:00,120.1,211.33900000000003,64.99,32.613 -2020-02-11 16:45:00,121.9,212.503,64.99,32.613 -2020-02-11 17:00:00,126.56,214.648,72.658,32.613 -2020-02-11 17:15:00,130.27,216.179,72.658,32.613 -2020-02-11 17:30:00,133.97,218.02700000000002,72.658,32.613 -2020-02-11 17:45:00,133.32,218.545,72.658,32.613 -2020-02-11 18:00:00,135.96,220.662,73.645,32.613 -2020-02-11 18:15:00,134.35,218.513,73.645,32.613 -2020-02-11 18:30:00,137.41,217.52700000000002,73.645,32.613 -2020-02-11 18:45:00,134.9,218.418,73.645,32.613 -2020-02-11 19:00:00,133.28,216.093,67.085,32.613 -2020-02-11 19:15:00,131.07,212.33,67.085,32.613 -2020-02-11 19:30:00,128.99,209.861,67.085,32.613 -2020-02-11 19:45:00,130.79,207.312,67.085,32.613 -2020-02-11 20:00:00,123.69,202.644,66.138,32.613 -2020-02-11 20:15:00,117.11,196.28599999999997,66.138,32.613 -2020-02-11 20:30:00,115.68,192.236,66.138,32.613 -2020-02-11 20:45:00,116.21,191.12,66.138,32.613 -2020-02-11 21:00:00,108.07,187.245,57.512,32.613 -2020-02-11 21:15:00,113.35,184.08900000000003,57.512,32.613 -2020-02-11 21:30:00,113.57,182.076,57.512,32.613 -2020-02-11 21:45:00,109.47,181.015,57.512,32.613 -2020-02-11 22:00:00,100.17,173.62599999999998,54.545,32.613 -2020-02-11 22:15:00,99.34,168.476,54.545,32.613 -2020-02-11 22:30:00,95.56,154.42700000000002,54.545,32.613 -2020-02-11 22:45:00,96.63,146.406,54.545,32.613 -2020-02-11 23:00:00,97.75,139.053,48.605,32.613 -2020-02-11 23:15:00,94.85,137.909,48.605,32.613 -2020-02-11 23:30:00,89.6,138.576,48.605,32.613 -2020-02-11 23:45:00,87.08,138.44,48.605,32.613 -2020-02-12 00:00:00,83.69,132.765,45.675,32.613 -2020-02-12 00:15:00,81.35,132.10399999999998,45.675,32.613 -2020-02-12 00:30:00,81.62,132.99,45.675,32.613 -2020-02-12 00:45:00,87.28,134.359,45.675,32.613 -2020-02-12 01:00:00,85.43,136.80700000000002,43.015,32.613 -2020-02-12 01:15:00,86.31,136.579,43.015,32.613 -2020-02-12 01:30:00,81.81,136.75799999999998,43.015,32.613 -2020-02-12 01:45:00,85.94,137.08,43.015,32.613 -2020-02-12 02:00:00,84.66,139.388,41.0,32.613 -2020-02-12 02:15:00,84.18,141.351,41.0,32.613 -2020-02-12 02:30:00,80.13,142.608,41.0,32.613 -2020-02-12 02:45:00,89.1,144.674,41.0,32.613 -2020-02-12 03:00:00,85.93,147.511,41.318000000000005,32.613 -2020-02-12 03:15:00,86.61,148.835,41.318000000000005,32.613 -2020-02-12 03:30:00,84.88,150.577,41.318000000000005,32.613 -2020-02-12 03:45:00,88.19,152.627,41.318000000000005,32.613 -2020-02-12 04:00:00,87.93,163.749,42.544,32.613 -2020-02-12 04:15:00,84.22,174.896,42.544,32.613 -2020-02-12 04:30:00,85.75,178.579,42.544,32.613 -2020-02-12 04:45:00,92.54,181.30200000000002,42.544,32.613 -2020-02-12 05:00:00,97.5,214.71400000000003,45.161,32.613 -2020-02-12 05:15:00,96.39,242.053,45.161,32.613 -2020-02-12 05:30:00,93.69,237.52200000000002,45.161,32.613 -2020-02-12 05:45:00,97.9,231.282,45.161,32.613 -2020-02-12 06:00:00,105.8,228.73,61.86600000000001,32.613 -2020-02-12 06:15:00,112.45,235.005,61.86600000000001,32.613 -2020-02-12 06:30:00,115.07,237.782,61.86600000000001,32.613 -2020-02-12 06:45:00,119.42,242.891,61.86600000000001,32.613 -2020-02-12 07:00:00,125.77,242.01,77.814,32.613 -2020-02-12 07:15:00,125.17,246.997,77.814,32.613 -2020-02-12 07:30:00,125.81,249.429,77.814,32.613 -2020-02-12 07:45:00,127.26,250.237,77.814,32.613 -2020-02-12 08:00:00,129.7,248.55900000000003,70.251,32.613 -2020-02-12 08:15:00,127.18,248.287,70.251,32.613 -2020-02-12 08:30:00,128.56,244.618,70.251,32.613 -2020-02-12 08:45:00,125.84,241.541,70.251,32.613 -2020-02-12 09:00:00,125.04,234.88099999999997,66.965,32.613 -2020-02-12 09:15:00,127.84,232.13,66.965,32.613 -2020-02-12 09:30:00,126.14,230.763,66.965,32.613 -2020-02-12 09:45:00,126.04,227.449,66.965,32.613 -2020-02-12 10:00:00,123.44,222.047,63.628,32.613 -2020-02-12 10:15:00,124.2,218.708,63.628,32.613 -2020-02-12 10:30:00,123.23,215.067,63.628,32.613 -2020-02-12 10:45:00,121.64,213.56599999999997,63.628,32.613 -2020-02-12 11:00:00,120.34,210.44400000000002,62.516999999999996,32.613 -2020-02-12 11:15:00,122.82,209.362,62.516999999999996,32.613 -2020-02-12 11:30:00,118.97,207.74099999999999,62.516999999999996,32.613 -2020-02-12 11:45:00,119.71,207.16,62.516999999999996,32.613 -2020-02-12 12:00:00,116.2,203.298,60.888999999999996,32.613 -2020-02-12 12:15:00,117.0,203.38,60.888999999999996,32.613 -2020-02-12 12:30:00,116.14,202.638,60.888999999999996,32.613 -2020-02-12 12:45:00,117.32,203.196,60.888999999999996,32.613 -2020-02-12 13:00:00,114.38,201.357,61.57899999999999,32.613 -2020-02-12 13:15:00,115.3,200.15400000000002,61.57899999999999,32.613 -2020-02-12 13:30:00,113.12,199.065,61.57899999999999,32.613 -2020-02-12 13:45:00,117.39,199.18,61.57899999999999,32.613 -2020-02-12 14:00:00,117.12,199.81,62.602,32.613 -2020-02-12 14:15:00,116.5,199.92,62.602,32.613 -2020-02-12 14:30:00,117.79,200.59799999999998,62.602,32.613 -2020-02-12 14:45:00,117.74,201.63299999999998,62.602,32.613 -2020-02-12 15:00:00,118.94,202.998,64.259,32.613 -2020-02-12 15:15:00,120.51,202.979,64.259,32.613 -2020-02-12 15:30:00,117.33,204.179,64.259,32.613 -2020-02-12 15:45:00,117.84,204.908,64.259,32.613 -2020-02-12 16:00:00,118.53,206.317,67.632,32.613 -2020-02-12 16:15:00,121.72,208.03400000000002,67.632,32.613 -2020-02-12 16:30:00,120.12,210.935,67.632,32.613 -2020-02-12 16:45:00,121.93,212.081,67.632,32.613 -2020-02-12 17:00:00,125.32,214.22400000000002,72.583,32.613 -2020-02-12 17:15:00,126.29,215.782,72.583,32.613 -2020-02-12 17:30:00,131.21,217.66400000000002,72.583,32.613 -2020-02-12 17:45:00,134.28,218.209,72.583,32.613 -2020-02-12 18:00:00,137.59,220.34099999999998,72.744,32.613 -2020-02-12 18:15:00,135.78,218.24900000000002,72.744,32.613 -2020-02-12 18:30:00,137.74,217.264,72.744,32.613 -2020-02-12 18:45:00,138.12,218.18099999999998,72.744,32.613 -2020-02-12 19:00:00,134.84,215.812,69.684,32.613 -2020-02-12 19:15:00,131.69,212.063,69.684,32.613 -2020-02-12 19:30:00,130.22,209.61700000000002,69.684,32.613 -2020-02-12 19:45:00,132.79,207.10299999999998,69.684,32.613 -2020-02-12 20:00:00,122.05,202.40400000000002,70.036,32.613 -2020-02-12 20:15:00,117.75,196.06,70.036,32.613 -2020-02-12 20:30:00,118.3,192.02200000000002,70.036,32.613 -2020-02-12 20:45:00,115.96,190.91099999999997,70.036,32.613 -2020-02-12 21:00:00,112.05,187.018,60.431999999999995,32.613 -2020-02-12 21:15:00,114.02,183.84900000000002,60.431999999999995,32.613 -2020-02-12 21:30:00,113.2,181.83599999999998,60.431999999999995,32.613 -2020-02-12 21:45:00,108.74,180.792,60.431999999999995,32.613 -2020-02-12 22:00:00,99.68,173.387,56.2,32.613 -2020-02-12 22:15:00,100.84,168.261,56.2,32.613 -2020-02-12 22:30:00,105.11,154.175,56.2,32.613 -2020-02-12 22:45:00,104.72,146.158,56.2,32.613 -2020-02-12 23:00:00,97.34,138.80100000000002,47.927,32.613 -2020-02-12 23:15:00,89.87,137.672,47.927,32.613 -2020-02-12 23:30:00,93.4,138.35399999999998,47.927,32.613 -2020-02-12 23:45:00,93.96,138.24,47.927,32.613 -2020-02-13 00:00:00,89.19,132.594,43.794,32.613 -2020-02-13 00:15:00,88.82,131.93,43.794,32.613 -2020-02-13 00:30:00,89.62,132.795,43.794,32.613 -2020-02-13 00:45:00,88.21,134.158,43.794,32.613 -2020-02-13 01:00:00,82.63,136.571,42.397,32.613 -2020-02-13 01:15:00,82.36,136.33,42.397,32.613 -2020-02-13 01:30:00,84.87,136.498,42.397,32.613 -2020-02-13 01:45:00,85.58,136.821,42.397,32.613 -2020-02-13 02:00:00,81.21,139.126,40.010999999999996,32.613 -2020-02-13 02:15:00,80.58,141.09,40.010999999999996,32.613 -2020-02-13 02:30:00,84.33,142.36,40.010999999999996,32.613 -2020-02-13 02:45:00,85.66,144.425,40.010999999999996,32.613 -2020-02-13 03:00:00,84.48,147.266,39.181,32.613 -2020-02-13 03:15:00,84.82,148.589,39.181,32.613 -2020-02-13 03:30:00,87.48,150.329,39.181,32.613 -2020-02-13 03:45:00,87.5,152.391,39.181,32.613 -2020-02-13 04:00:00,84.29,163.512,40.39,32.613 -2020-02-13 04:15:00,82.01,174.649,40.39,32.613 -2020-02-13 04:30:00,82.76,178.34900000000002,40.39,32.613 -2020-02-13 04:45:00,84.79,181.05900000000003,40.39,32.613 -2020-02-13 05:00:00,88.04,214.454,45.504,32.613 -2020-02-13 05:15:00,91.08,241.812,45.504,32.613 -2020-02-13 05:30:00,93.75,237.25400000000002,45.504,32.613 -2020-02-13 05:45:00,99.4,231.018,45.504,32.613 -2020-02-13 06:00:00,105.36,228.475,57.748000000000005,32.613 -2020-02-13 06:15:00,108.99,234.764,57.748000000000005,32.613 -2020-02-13 06:30:00,113.77,237.51,57.748000000000005,32.613 -2020-02-13 06:45:00,118.6,242.61700000000002,57.748000000000005,32.613 -2020-02-13 07:00:00,125.32,241.76,72.138,32.613 -2020-02-13 07:15:00,124.14,246.71900000000002,72.138,32.613 -2020-02-13 07:30:00,127.06,249.11700000000002,72.138,32.613 -2020-02-13 07:45:00,125.97,249.882,72.138,32.613 -2020-02-13 08:00:00,128.01,248.18200000000002,65.542,32.613 -2020-02-13 08:15:00,126.31,247.885,65.542,32.613 -2020-02-13 08:30:00,125.65,244.155,65.542,32.613 -2020-02-13 08:45:00,126.67,241.079,65.542,32.613 -2020-02-13 09:00:00,125.47,234.424,60.523,32.613 -2020-02-13 09:15:00,126.89,231.675,60.523,32.613 -2020-02-13 09:30:00,129.73,230.329,60.523,32.613 -2020-02-13 09:45:00,129.88,227.015,60.523,32.613 -2020-02-13 10:00:00,131.24,221.625,57.449,32.613 -2020-02-13 10:15:00,135.05,218.315,57.449,32.613 -2020-02-13 10:30:00,132.88,214.68400000000003,57.449,32.613 -2020-02-13 10:45:00,133.42,213.19799999999998,57.449,32.613 -2020-02-13 11:00:00,134.39,210.065,54.505,32.613 -2020-02-13 11:15:00,130.99,208.99599999999998,54.505,32.613 -2020-02-13 11:30:00,130.46,207.382,54.505,32.613 -2020-02-13 11:45:00,131.92,206.813,54.505,32.613 -2020-02-13 12:00:00,132.04,202.96599999999998,51.50899999999999,32.613 -2020-02-13 12:15:00,132.58,203.065,51.50899999999999,32.613 -2020-02-13 12:30:00,130.4,202.295,51.50899999999999,32.613 -2020-02-13 12:45:00,130.32,202.84900000000002,51.50899999999999,32.613 -2020-02-13 13:00:00,128.38,201.03599999999997,51.303999999999995,32.613 -2020-02-13 13:15:00,128.1,199.808,51.303999999999995,32.613 -2020-02-13 13:30:00,127.22,198.702,51.303999999999995,32.613 -2020-02-13 13:45:00,127.79,198.81900000000002,51.303999999999995,32.613 -2020-02-13 14:00:00,128.62,199.505,52.785,32.613 -2020-02-13 14:15:00,126.84,199.595,52.785,32.613 -2020-02-13 14:30:00,126.35,200.25599999999997,52.785,32.613 -2020-02-13 14:45:00,128.53,201.30599999999998,52.785,32.613 -2020-02-13 15:00:00,129.93,202.678,56.458999999999996,32.613 -2020-02-13 15:15:00,128.24,202.62900000000002,56.458999999999996,32.613 -2020-02-13 15:30:00,126.87,203.79,56.458999999999996,32.613 -2020-02-13 15:45:00,125.33,204.50099999999998,56.458999999999996,32.613 -2020-02-13 16:00:00,127.19,205.91099999999997,59.388000000000005,32.613 -2020-02-13 16:15:00,124.76,207.618,59.388000000000005,32.613 -2020-02-13 16:30:00,125.2,210.52200000000002,59.388000000000005,32.613 -2020-02-13 16:45:00,126.24,211.65,59.388000000000005,32.613 -2020-02-13 17:00:00,130.87,213.791,64.462,32.613 -2020-02-13 17:15:00,132.84,215.37400000000002,64.462,32.613 -2020-02-13 17:30:00,137.45,217.291,64.462,32.613 -2020-02-13 17:45:00,138.46,217.863,64.462,32.613 -2020-02-13 18:00:00,138.81,220.00900000000001,65.128,32.613 -2020-02-13 18:15:00,139.01,217.975,65.128,32.613 -2020-02-13 18:30:00,137.4,216.99200000000002,65.128,32.613 -2020-02-13 18:45:00,137.2,217.93200000000002,65.128,32.613 -2020-02-13 19:00:00,135.8,215.521,61.316,32.613 -2020-02-13 19:15:00,131.56,211.78799999999998,61.316,32.613 -2020-02-13 19:30:00,134.76,209.36599999999999,61.316,32.613 -2020-02-13 19:45:00,138.85,206.887,61.316,32.613 -2020-02-13 20:00:00,128.5,202.157,59.845,32.613 -2020-02-13 20:15:00,118.6,195.827,59.845,32.613 -2020-02-13 20:30:00,113.73,191.801,59.845,32.613 -2020-02-13 20:45:00,114.63,190.69299999999998,59.845,32.613 -2020-02-13 21:00:00,108.1,186.78400000000002,54.83,32.613 -2020-02-13 21:15:00,111.9,183.602,54.83,32.613 -2020-02-13 21:30:00,111.83,181.59,54.83,32.613 -2020-02-13 21:45:00,111.1,180.563,54.83,32.613 -2020-02-13 22:00:00,98.11,173.14,50.933,32.613 -2020-02-13 22:15:00,97.59,168.041,50.933,32.613 -2020-02-13 22:30:00,96.24,153.912,50.933,32.613 -2020-02-13 22:45:00,91.94,145.90200000000002,50.933,32.613 -2020-02-13 23:00:00,90.76,138.541,45.32899999999999,32.613 -2020-02-13 23:15:00,94.89,137.42700000000002,45.32899999999999,32.613 -2020-02-13 23:30:00,92.24,138.123,45.32899999999999,32.613 -2020-02-13 23:45:00,90.49,138.031,45.32899999999999,32.613 -2020-02-14 00:00:00,80.41,131.44799999999998,43.74,32.613 -2020-02-14 00:15:00,85.51,130.975,43.74,32.613 -2020-02-14 00:30:00,87.17,131.634,43.74,32.613 -2020-02-14 00:45:00,86.07,133.062,43.74,32.613 -2020-02-14 01:00:00,76.17,135.139,42.555,32.613 -2020-02-14 01:15:00,80.38,135.996,42.555,32.613 -2020-02-14 01:30:00,83.38,135.805,42.555,32.613 -2020-02-14 01:45:00,82.94,136.282,42.555,32.613 -2020-02-14 02:00:00,77.22,138.555,41.68600000000001,32.613 -2020-02-14 02:15:00,79.25,140.39600000000002,41.68600000000001,32.613 -2020-02-14 02:30:00,82.1,142.158,41.68600000000001,32.613 -2020-02-14 02:45:00,84.42,144.364,41.68600000000001,32.613 -2020-02-14 03:00:00,82.15,145.974,42.278999999999996,32.613 -2020-02-14 03:15:00,82.27,148.532,42.278999999999996,32.613 -2020-02-14 03:30:00,85.54,150.282,42.278999999999996,32.613 -2020-02-14 03:45:00,84.15,152.6,42.278999999999996,32.613 -2020-02-14 04:00:00,80.78,163.96400000000003,43.742,32.613 -2020-02-14 04:15:00,81.02,175.043,43.742,32.613 -2020-02-14 04:30:00,81.93,178.87900000000002,43.742,32.613 -2020-02-14 04:45:00,84.27,180.37400000000002,43.742,32.613 -2020-02-14 05:00:00,87.26,212.38099999999997,46.973,32.613 -2020-02-14 05:15:00,88.2,241.33599999999998,46.973,32.613 -2020-02-14 05:30:00,91.52,237.951,46.973,32.613 -2020-02-14 05:45:00,95.37,231.71099999999998,46.973,32.613 -2020-02-14 06:00:00,102.89,229.63400000000001,59.63399999999999,32.613 -2020-02-14 06:15:00,106.98,234.238,59.63399999999999,32.613 -2020-02-14 06:30:00,110.84,235.976,59.63399999999999,32.613 -2020-02-14 06:45:00,116.04,242.94799999999998,59.63399999999999,32.613 -2020-02-14 07:00:00,122.39,241.06900000000002,71.631,32.613 -2020-02-14 07:15:00,121.91,247.02900000000002,71.631,32.613 -2020-02-14 07:30:00,125.24,249.47099999999998,71.631,32.613 -2020-02-14 07:45:00,123.6,249.21599999999998,71.631,32.613 -2020-02-14 08:00:00,125.27,246.135,66.181,32.613 -2020-02-14 08:15:00,123.65,245.28400000000002,66.181,32.613 -2020-02-14 08:30:00,123.84,242.607,66.181,32.613 -2020-02-14 08:45:00,123.1,237.773,66.181,32.613 -2020-02-14 09:00:00,121.88,231.95,63.086000000000006,32.613 -2020-02-14 09:15:00,127.03,229.597,63.086000000000006,32.613 -2020-02-14 09:30:00,125.21,227.877,63.086000000000006,32.613 -2020-02-14 09:45:00,125.55,224.393,63.086000000000006,32.613 -2020-02-14 10:00:00,120.82,217.755,60.886,32.613 -2020-02-14 10:15:00,120.02,215.28900000000002,60.886,32.613 -2020-02-14 10:30:00,120.71,211.518,60.886,32.613 -2020-02-14 10:45:00,119.38,209.56,60.886,32.613 -2020-02-14 11:00:00,118.72,206.375,59.391000000000005,32.613 -2020-02-14 11:15:00,118.29,204.453,59.391000000000005,32.613 -2020-02-14 11:30:00,117.37,204.847,59.391000000000005,32.613 -2020-02-14 11:45:00,114.65,204.46,59.391000000000005,32.613 -2020-02-14 12:00:00,113.88,201.791,56.172,32.613 -2020-02-14 12:15:00,113.37,199.643,56.172,32.613 -2020-02-14 12:30:00,111.06,199.00900000000001,56.172,32.613 -2020-02-14 12:45:00,114.09,200.232,56.172,32.613 -2020-02-14 13:00:00,109.71,199.421,54.406000000000006,32.613 -2020-02-14 13:15:00,109.48,199.05900000000003,54.406000000000006,32.613 -2020-02-14 13:30:00,108.28,197.857,54.406000000000006,32.613 -2020-02-14 13:45:00,110.11,197.86599999999999,54.406000000000006,32.613 -2020-02-14 14:00:00,112.37,197.43200000000002,53.578,32.613 -2020-02-14 14:15:00,116.73,197.24900000000002,53.578,32.613 -2020-02-14 14:30:00,116.58,198.28400000000002,53.578,32.613 -2020-02-14 14:45:00,115.29,199.769,53.578,32.613 -2020-02-14 15:00:00,113.44,200.61700000000002,56.568999999999996,32.613 -2020-02-14 15:15:00,112.25,200.078,56.568999999999996,32.613 -2020-02-14 15:30:00,111.0,199.579,56.568999999999996,32.613 -2020-02-14 15:45:00,113.13,200.34599999999998,56.568999999999996,32.613 -2020-02-14 16:00:00,114.03,200.571,60.169,32.613 -2020-02-14 16:15:00,115.56,202.53400000000002,60.169,32.613 -2020-02-14 16:30:00,117.3,205.56599999999997,60.169,32.613 -2020-02-14 16:45:00,117.51,206.63299999999998,60.169,32.613 -2020-02-14 17:00:00,123.02,208.825,65.497,32.613 -2020-02-14 17:15:00,123.09,209.998,65.497,32.613 -2020-02-14 17:30:00,125.89,211.582,65.497,32.613 -2020-02-14 17:45:00,129.33,211.94,65.497,32.613 -2020-02-14 18:00:00,131.38,214.87599999999998,65.082,32.613 -2020-02-14 18:15:00,129.52,212.56799999999998,65.082,32.613 -2020-02-14 18:30:00,129.37,212.02700000000002,65.082,32.613 -2020-02-14 18:45:00,130.15,212.945,65.082,32.613 -2020-02-14 19:00:00,127.94,211.408,60.968,32.613 -2020-02-14 19:15:00,128.26,209.11900000000003,60.968,32.613 -2020-02-14 19:30:00,129.49,206.266,60.968,32.613 -2020-02-14 19:45:00,134.25,203.418,60.968,32.613 -2020-02-14 20:00:00,123.84,198.725,61.123000000000005,32.613 -2020-02-14 20:15:00,113.6,192.33599999999998,61.123000000000005,32.613 -2020-02-14 20:30:00,111.96,188.327,61.123000000000005,32.613 -2020-02-14 20:45:00,109.54,187.91299999999998,61.123000000000005,32.613 -2020-02-14 21:00:00,103.24,184.41400000000002,55.416000000000004,32.613 -2020-02-14 21:15:00,107.66,181.52900000000002,55.416000000000004,32.613 -2020-02-14 21:30:00,106.5,179.583,55.416000000000004,32.613 -2020-02-14 21:45:00,100.0,179.14700000000002,55.416000000000004,32.613 -2020-02-14 22:00:00,92.22,172.795,51.631,32.613 -2020-02-14 22:15:00,89.89,167.59900000000002,51.631,32.613 -2020-02-14 22:30:00,87.77,159.931,51.631,32.613 -2020-02-14 22:45:00,89.65,155.7,51.631,32.613 -2020-02-14 23:00:00,81.69,147.679,44.898,32.613 -2020-02-14 23:15:00,87.18,144.605,44.898,32.613 -2020-02-14 23:30:00,86.71,143.9,44.898,32.613 -2020-02-14 23:45:00,86.41,143.115,44.898,32.613 -2020-02-15 00:00:00,79.08,116.521,42.033,32.431999999999995 -2020-02-15 00:15:00,72.83,111.295,42.033,32.431999999999995 -2020-02-15 00:30:00,73.01,112.603,42.033,32.431999999999995 -2020-02-15 00:45:00,78.83,113.965,42.033,32.431999999999995 -2020-02-15 01:00:00,77.6,116.42,38.255,32.431999999999995 -2020-02-15 01:15:00,76.74,116.64299999999999,38.255,32.431999999999995 -2020-02-15 01:30:00,71.1,116.18299999999999,38.255,32.431999999999995 -2020-02-15 01:45:00,68.6,116.26,38.255,32.431999999999995 -2020-02-15 02:00:00,68.99,119.05,36.404,32.431999999999995 -2020-02-15 02:15:00,77.1,119.95700000000001,36.404,32.431999999999995 -2020-02-15 02:30:00,76.22,120.164,36.404,32.431999999999995 -2020-02-15 02:45:00,74.46,122.105,36.404,32.431999999999995 -2020-02-15 03:00:00,68.01,124.14200000000001,36.083,32.431999999999995 -2020-02-15 03:15:00,70.9,125.476,36.083,32.431999999999995 -2020-02-15 03:30:00,76.79,125.72399999999999,36.083,32.431999999999995 -2020-02-15 03:45:00,72.61,127.546,36.083,32.431999999999995 -2020-02-15 04:00:00,70.23,135.398,36.102,32.431999999999995 -2020-02-15 04:15:00,69.29,144.376,36.102,32.431999999999995 -2020-02-15 04:30:00,69.13,144.93200000000002,36.102,32.431999999999995 -2020-02-15 04:45:00,70.59,145.541,36.102,32.431999999999995 -2020-02-15 05:00:00,72.01,161.107,35.284,32.431999999999995 -2020-02-15 05:15:00,73.57,171.671,35.284,32.431999999999995 -2020-02-15 05:30:00,71.26,168.453,35.284,32.431999999999995 -2020-02-15 05:45:00,73.74,166.71900000000002,35.284,32.431999999999995 -2020-02-15 06:00:00,74.58,183.00599999999997,36.265,32.431999999999995 -2020-02-15 06:15:00,75.42,203.584,36.265,32.431999999999995 -2020-02-15 06:30:00,76.43,199.912,36.265,32.431999999999995 -2020-02-15 06:45:00,78.71,196.593,36.265,32.431999999999995 -2020-02-15 07:00:00,80.29,192.623,40.714,32.431999999999995 -2020-02-15 07:15:00,81.02,196.359,40.714,32.431999999999995 -2020-02-15 07:30:00,83.66,200.294,40.714,32.431999999999995 -2020-02-15 07:45:00,87.0,203.146,40.714,32.431999999999995 -2020-02-15 08:00:00,90.02,204.477,46.692,32.431999999999995 -2020-02-15 08:15:00,90.93,206.66299999999998,46.692,32.431999999999995 -2020-02-15 08:30:00,92.08,205.672,46.692,32.431999999999995 -2020-02-15 08:45:00,93.93,203.412,46.692,32.431999999999995 -2020-02-15 09:00:00,96.51,198.43,48.925,32.431999999999995 -2020-02-15 09:15:00,95.91,196.627,48.925,32.431999999999995 -2020-02-15 09:30:00,96.0,195.517,48.925,32.431999999999995 -2020-02-15 09:45:00,96.39,192.588,48.925,32.431999999999995 -2020-02-15 10:00:00,96.58,187.47799999999998,47.799,32.431999999999995 -2020-02-15 10:15:00,96.18,184.49900000000002,47.799,32.431999999999995 -2020-02-15 10:30:00,95.93,182.03599999999997,47.799,32.431999999999995 -2020-02-15 10:45:00,96.38,181.78799999999998,47.799,32.431999999999995 -2020-02-15 11:00:00,98.86,180.104,44.309,32.431999999999995 -2020-02-15 11:15:00,100.22,177.608,44.309,32.431999999999995 -2020-02-15 11:30:00,99.83,177.332,44.309,32.431999999999995 -2020-02-15 11:45:00,98.79,175.109,44.309,32.431999999999995 -2020-02-15 12:00:00,99.97,170.31400000000002,42.367,32.431999999999995 -2020-02-15 12:15:00,98.06,168.968,42.367,32.431999999999995 -2020-02-15 12:30:00,94.95,168.99599999999998,42.367,32.431999999999995 -2020-02-15 12:45:00,93.22,169.708,42.367,32.431999999999995 -2020-02-15 13:00:00,91.16,169.28,39.036,32.431999999999995 -2020-02-15 13:15:00,90.52,166.933,39.036,32.431999999999995 -2020-02-15 13:30:00,89.43,165.641,39.036,32.431999999999995 -2020-02-15 13:45:00,90.29,165.61900000000003,39.036,32.431999999999995 -2020-02-15 14:00:00,88.86,166.04,37.995,32.431999999999995 -2020-02-15 14:15:00,88.98,165.236,37.995,32.431999999999995 -2020-02-15 14:30:00,89.39,164.611,37.995,32.431999999999995 -2020-02-15 14:45:00,92.56,165.851,37.995,32.431999999999995 -2020-02-15 15:00:00,90.72,167.85299999999998,40.71,32.431999999999995 -2020-02-15 15:15:00,93.83,167.59599999999998,40.71,32.431999999999995 -2020-02-15 15:30:00,90.34,169.00400000000002,40.71,32.431999999999995 -2020-02-15 15:45:00,89.96,170.202,40.71,32.431999999999995 -2020-02-15 16:00:00,91.14,169.48,46.998000000000005,32.431999999999995 -2020-02-15 16:15:00,90.96,172.19400000000002,46.998000000000005,32.431999999999995 -2020-02-15 16:30:00,91.81,175.07299999999998,46.998000000000005,32.431999999999995 -2020-02-15 16:45:00,93.73,177.238,46.998000000000005,32.431999999999995 -2020-02-15 17:00:00,98.33,178.832,55.431000000000004,32.431999999999995 -2020-02-15 17:15:00,99.99,182.15599999999998,55.431000000000004,32.431999999999995 -2020-02-15 17:30:00,103.94,184.06900000000002,55.431000000000004,32.431999999999995 -2020-02-15 17:45:00,107.33,184.25099999999998,55.431000000000004,32.431999999999995 -2020-02-15 18:00:00,110.74,186.635,55.989,32.431999999999995 -2020-02-15 18:15:00,108.62,187.128,55.989,32.431999999999995 -2020-02-15 18:30:00,109.33,187.54,55.989,32.431999999999995 -2020-02-15 18:45:00,107.71,185.11,55.989,32.431999999999995 -2020-02-15 19:00:00,106.51,186.041,50.882,32.431999999999995 -2020-02-15 19:15:00,104.82,183.521,50.882,32.431999999999995 -2020-02-15 19:30:00,103.08,182.215,50.882,32.431999999999995 -2020-02-15 19:45:00,102.01,178.392,50.882,32.431999999999995 -2020-02-15 20:00:00,96.44,176.037,43.172,32.431999999999995 -2020-02-15 20:15:00,92.99,172.542,43.172,32.431999999999995 -2020-02-15 20:30:00,90.07,168.233,43.172,32.431999999999995 -2020-02-15 20:45:00,89.39,166.56,43.172,32.431999999999995 -2020-02-15 21:00:00,84.61,166.14,37.599000000000004,32.431999999999995 -2020-02-15 21:15:00,83.31,164.019,37.599000000000004,32.431999999999995 -2020-02-15 21:30:00,82.48,163.214,37.599000000000004,32.431999999999995 -2020-02-15 21:45:00,81.66,162.619,37.599000000000004,32.431999999999995 -2020-02-15 22:00:00,77.87,157.94799999999998,39.047,32.431999999999995 -2020-02-15 22:15:00,76.64,155.707,39.047,32.431999999999995 -2020-02-15 22:30:00,74.14,154.209,39.047,32.431999999999995 -2020-02-15 22:45:00,73.06,152.016,39.047,32.431999999999995 -2020-02-15 23:00:00,69.29,147.38299999999998,32.339,32.431999999999995 -2020-02-15 23:15:00,69.86,142.295,32.339,32.431999999999995 -2020-02-15 23:30:00,65.81,140.22,32.339,32.431999999999995 -2020-02-15 23:45:00,64.83,137.039,32.339,32.431999999999995 -2020-02-16 00:00:00,59.92,116.76700000000001,29.988000000000003,32.431999999999995 -2020-02-16 00:15:00,60.99,111.206,29.988000000000003,32.431999999999995 -2020-02-16 00:30:00,60.45,112.125,29.988000000000003,32.431999999999995 -2020-02-16 00:45:00,59.8,114.161,29.988000000000003,32.431999999999995 -2020-02-16 01:00:00,56.82,116.45,28.531999999999996,32.431999999999995 -2020-02-16 01:15:00,58.25,117.669,28.531999999999996,32.431999999999995 -2020-02-16 01:30:00,57.58,117.70200000000001,28.531999999999996,32.431999999999995 -2020-02-16 01:45:00,56.97,117.46600000000001,28.531999999999996,32.431999999999995 -2020-02-16 02:00:00,55.01,119.524,27.805999999999997,32.431999999999995 -2020-02-16 02:15:00,55.92,119.61399999999999,27.805999999999997,32.431999999999995 -2020-02-16 02:30:00,55.77,120.67200000000001,27.805999999999997,32.431999999999995 -2020-02-16 02:45:00,55.31,123.05799999999999,27.805999999999997,32.431999999999995 -2020-02-16 03:00:00,54.28,125.42200000000001,26.193,32.431999999999995 -2020-02-16 03:15:00,56.05,126.24799999999999,26.193,32.431999999999995 -2020-02-16 03:30:00,55.3,127.836,26.193,32.431999999999995 -2020-02-16 03:45:00,55.85,129.576,26.193,32.431999999999995 -2020-02-16 04:00:00,55.44,137.189,27.19,32.431999999999995 -2020-02-16 04:15:00,56.58,145.164,27.19,32.431999999999995 -2020-02-16 04:30:00,56.86,145.828,27.19,32.431999999999995 -2020-02-16 04:45:00,58.08,146.68,27.19,32.431999999999995 -2020-02-16 05:00:00,58.38,158.819,28.166999999999998,32.431999999999995 -2020-02-16 05:15:00,58.85,167.02900000000002,28.166999999999998,32.431999999999995 -2020-02-16 05:30:00,59.76,163.615,28.166999999999998,32.431999999999995 -2020-02-16 05:45:00,60.47,162.11700000000002,28.166999999999998,32.431999999999995 -2020-02-16 06:00:00,61.37,178.252,27.16,32.431999999999995 -2020-02-16 06:15:00,64.85,197.18900000000002,27.16,32.431999999999995 -2020-02-16 06:30:00,63.55,192.395,27.16,32.431999999999995 -2020-02-16 06:45:00,65.16,188.02,27.16,32.431999999999995 -2020-02-16 07:00:00,68.28,186.46,29.578000000000003,32.431999999999995 -2020-02-16 07:15:00,67.41,189.298,29.578000000000003,32.431999999999995 -2020-02-16 07:30:00,69.82,192.037,29.578000000000003,32.431999999999995 -2020-02-16 07:45:00,72.92,194.109,29.578000000000003,32.431999999999995 -2020-02-16 08:00:00,74.96,197.226,34.650999999999996,32.431999999999995 -2020-02-16 08:15:00,75.92,199.338,34.650999999999996,32.431999999999995 -2020-02-16 08:30:00,77.55,199.925,34.650999999999996,32.431999999999995 -2020-02-16 08:45:00,79.55,199.627,34.650999999999996,32.431999999999995 -2020-02-16 09:00:00,81.5,194.25599999999997,38.080999999999996,32.431999999999995 -2020-02-16 09:15:00,82.17,192.979,38.080999999999996,32.431999999999995 -2020-02-16 09:30:00,83.33,191.74900000000002,38.080999999999996,32.431999999999995 -2020-02-16 09:45:00,84.69,188.739,38.080999999999996,32.431999999999995 -2020-02-16 10:00:00,85.76,186.095,39.934,32.431999999999995 -2020-02-16 10:15:00,85.84,183.66400000000002,39.934,32.431999999999995 -2020-02-16 10:30:00,89.06,181.803,39.934,32.431999999999995 -2020-02-16 10:45:00,89.12,179.768,39.934,32.431999999999995 -2020-02-16 11:00:00,92.11,178.949,43.74100000000001,32.431999999999995 -2020-02-16 11:15:00,95.2,176.582,43.74100000000001,32.431999999999995 -2020-02-16 11:30:00,96.92,175.487,43.74100000000001,32.431999999999995 -2020-02-16 11:45:00,95.96,173.87099999999998,43.74100000000001,32.431999999999995 -2020-02-16 12:00:00,93.67,168.581,40.001999999999995,32.431999999999995 -2020-02-16 12:15:00,92.58,169.065,40.001999999999995,32.431999999999995 -2020-02-16 12:30:00,89.3,167.671,40.001999999999995,32.431999999999995 -2020-02-16 12:45:00,90.13,167.426,40.001999999999995,32.431999999999995 -2020-02-16 13:00:00,84.56,166.325,37.855,32.431999999999995 -2020-02-16 13:15:00,84.84,166.843,37.855,32.431999999999995 -2020-02-16 13:30:00,82.46,165.296,37.855,32.431999999999995 -2020-02-16 13:45:00,82.47,164.67700000000002,37.855,32.431999999999995 -2020-02-16 14:00:00,80.52,165.43,35.946999999999996,32.431999999999995 -2020-02-16 14:15:00,80.46,165.77599999999998,35.946999999999996,32.431999999999995 -2020-02-16 14:30:00,80.76,166.28,35.946999999999996,32.431999999999995 -2020-02-16 14:45:00,80.96,167.1,35.946999999999996,32.431999999999995 -2020-02-16 15:00:00,81.11,167.64,35.138000000000005,32.431999999999995 -2020-02-16 15:15:00,79.85,168.058,35.138000000000005,32.431999999999995 -2020-02-16 15:30:00,79.76,169.99,35.138000000000005,32.431999999999995 -2020-02-16 15:45:00,80.59,171.847,35.138000000000005,32.431999999999995 -2020-02-16 16:00:00,82.28,172.89,38.672,32.431999999999995 -2020-02-16 16:15:00,81.81,174.72099999999998,38.672,32.431999999999995 -2020-02-16 16:30:00,81.95,177.882,38.672,32.431999999999995 -2020-02-16 16:45:00,84.44,180.15599999999998,38.672,32.431999999999995 -2020-02-16 17:00:00,88.25,181.767,48.684,32.431999999999995 -2020-02-16 17:15:00,89.74,184.832,48.684,32.431999999999995 -2020-02-16 17:30:00,92.59,187.075,48.684,32.431999999999995 -2020-02-16 17:45:00,97.75,189.489,48.684,32.431999999999995 -2020-02-16 18:00:00,103.5,191.37400000000002,51.568999999999996,32.431999999999995 -2020-02-16 18:15:00,103.77,193.201,51.568999999999996,32.431999999999995 -2020-02-16 18:30:00,103.86,191.58,51.568999999999996,32.431999999999995 -2020-02-16 18:45:00,101.12,190.97400000000002,51.568999999999996,32.431999999999995 -2020-02-16 19:00:00,100.36,191.597,48.608000000000004,32.431999999999995 -2020-02-16 19:15:00,97.84,189.642,48.608000000000004,32.431999999999995 -2020-02-16 19:30:00,100.63,188.19299999999998,48.608000000000004,32.431999999999995 -2020-02-16 19:45:00,103.68,185.798,48.608000000000004,32.431999999999995 -2020-02-16 20:00:00,99.8,183.389,43.733999999999995,32.431999999999995 -2020-02-16 20:15:00,93.26,180.862,43.733999999999995,32.431999999999995 -2020-02-16 20:30:00,92.18,177.79,43.733999999999995,32.431999999999995 -2020-02-16 20:45:00,90.05,174.91400000000002,43.733999999999995,32.431999999999995 -2020-02-16 21:00:00,87.92,171.957,39.283,32.431999999999995 -2020-02-16 21:15:00,88.88,169.209,39.283,32.431999999999995 -2020-02-16 21:30:00,93.52,168.675,39.283,32.431999999999995 -2020-02-16 21:45:00,95.15,168.239,39.283,32.431999999999995 -2020-02-16 22:00:00,90.26,162.445,40.111,32.431999999999995 -2020-02-16 22:15:00,89.37,159.447,40.111,32.431999999999995 -2020-02-16 22:30:00,90.03,154.82,40.111,32.431999999999995 -2020-02-16 22:45:00,90.58,151.776,40.111,32.431999999999995 -2020-02-16 23:00:00,86.82,144.405,35.791,32.431999999999995 -2020-02-16 23:15:00,85.0,141.168,35.791,32.431999999999995 -2020-02-16 23:30:00,86.25,139.881,35.791,32.431999999999995 -2020-02-16 23:45:00,84.99,137.593,35.791,32.431999999999995 -2020-02-17 00:00:00,81.05,120.693,34.311,32.613 -2020-02-17 00:15:00,80.07,118.045,34.311,32.613 -2020-02-17 00:30:00,81.59,119.06200000000001,34.311,32.613 -2020-02-17 00:45:00,81.45,120.56,34.311,32.613 -2020-02-17 01:00:00,76.96,122.838,34.585,32.613 -2020-02-17 01:15:00,76.08,123.53,34.585,32.613 -2020-02-17 01:30:00,79.32,123.61200000000001,34.585,32.613 -2020-02-17 01:45:00,79.06,123.48700000000001,34.585,32.613 -2020-02-17 02:00:00,75.2,125.52799999999999,34.111,32.613 -2020-02-17 02:15:00,77.83,127.016,34.111,32.613 -2020-02-17 02:30:00,78.92,128.418,34.111,32.613 -2020-02-17 02:45:00,80.45,130.196,34.111,32.613 -2020-02-17 03:00:00,74.01,133.811,32.435,32.613 -2020-02-17 03:15:00,76.74,136.253,32.435,32.613 -2020-02-17 03:30:00,81.58,137.588,32.435,32.613 -2020-02-17 03:45:00,81.79,138.791,32.435,32.613 -2020-02-17 04:00:00,80.2,150.708,33.04,32.613 -2020-02-17 04:15:00,77.79,162.775,33.04,32.613 -2020-02-17 04:30:00,85.23,165.579,33.04,32.613 -2020-02-17 04:45:00,86.55,166.584,33.04,32.613 -2020-02-17 05:00:00,89.34,194.2,40.399,32.613 -2020-02-17 05:15:00,84.87,222.275,40.399,32.613 -2020-02-17 05:30:00,88.55,219.07,40.399,32.613 -2020-02-17 05:45:00,93.5,212.00799999999998,40.399,32.613 -2020-02-17 06:00:00,101.21,210.477,60.226000000000006,32.613 -2020-02-17 06:15:00,106.95,214.78900000000002,60.226000000000006,32.613 -2020-02-17 06:30:00,112.4,217.851,60.226000000000006,32.613 -2020-02-17 06:45:00,116.6,222.146,60.226000000000006,32.613 -2020-02-17 07:00:00,122.33,222.924,73.578,32.613 -2020-02-17 07:15:00,122.39,226.99400000000003,73.578,32.613 -2020-02-17 07:30:00,122.25,228.925,73.578,32.613 -2020-02-17 07:45:00,124.77,228.46599999999998,73.578,32.613 -2020-02-17 08:00:00,129.11,226.765,66.58,32.613 -2020-02-17 08:15:00,127.63,226.747,66.58,32.613 -2020-02-17 08:30:00,123.89,223.28599999999997,66.58,32.613 -2020-02-17 08:45:00,122.13,219.752,66.58,32.613 -2020-02-17 09:00:00,124.23,213.396,62.0,32.613 -2020-02-17 09:15:00,125.02,208.692,62.0,32.613 -2020-02-17 09:30:00,125.32,206.49,62.0,32.613 -2020-02-17 09:45:00,122.32,203.72299999999998,62.0,32.613 -2020-02-17 10:00:00,121.29,200.037,59.099,32.613 -2020-02-17 10:15:00,125.81,197.31599999999997,59.099,32.613 -2020-02-17 10:30:00,124.92,194.606,59.099,32.613 -2020-02-17 10:45:00,127.08,193.273,59.099,32.613 -2020-02-17 11:00:00,122.8,189.91400000000002,57.729,32.613 -2020-02-17 11:15:00,123.95,189.315,57.729,32.613 -2020-02-17 11:30:00,125.08,189.584,57.729,32.613 -2020-02-17 11:45:00,121.96,187.597,57.729,32.613 -2020-02-17 12:00:00,120.65,183.946,55.615,32.613 -2020-02-17 12:15:00,122.1,184.45,55.615,32.613 -2020-02-17 12:30:00,120.86,183.24200000000002,55.615,32.613 -2020-02-17 12:45:00,120.22,184.47099999999998,55.615,32.613 -2020-02-17 13:00:00,118.27,183.98,56.515,32.613 -2020-02-17 13:15:00,119.03,183.113,56.515,32.613 -2020-02-17 13:30:00,118.32,181.043,56.515,32.613 -2020-02-17 13:45:00,117.07,180.481,56.515,32.613 -2020-02-17 14:00:00,115.45,180.667,58.1,32.613 -2020-02-17 14:15:00,118.18,180.386,58.1,32.613 -2020-02-17 14:30:00,118.2,180.34400000000002,58.1,32.613 -2020-02-17 14:45:00,115.44,181.199,58.1,32.613 -2020-02-17 15:00:00,117.71,183.47299999999998,59.801,32.613 -2020-02-17 15:15:00,119.93,182.456,59.801,32.613 -2020-02-17 15:30:00,119.86,183.56599999999997,59.801,32.613 -2020-02-17 15:45:00,115.36,184.96400000000003,59.801,32.613 -2020-02-17 16:00:00,116.04,186.22400000000002,62.901,32.613 -2020-02-17 16:15:00,117.83,187.304,62.901,32.613 -2020-02-17 16:30:00,118.12,189.513,62.901,32.613 -2020-02-17 16:45:00,120.01,190.609,62.901,32.613 -2020-02-17 17:00:00,123.07,192.03900000000002,70.418,32.613 -2020-02-17 17:15:00,128.21,194.203,70.418,32.613 -2020-02-17 17:30:00,127.74,195.93200000000002,70.418,32.613 -2020-02-17 17:45:00,131.41,196.90400000000002,70.418,32.613 -2020-02-17 18:00:00,133.59,199.207,71.726,32.613 -2020-02-17 18:15:00,132.22,198.90099999999998,71.726,32.613 -2020-02-17 18:30:00,134.7,197.90400000000002,71.726,32.613 -2020-02-17 18:45:00,132.33,198.072,71.726,32.613 -2020-02-17 19:00:00,129.8,197.084,65.997,32.613 -2020-02-17 19:15:00,128.73,194.02,65.997,32.613 -2020-02-17 19:30:00,129.34,193.07299999999998,65.997,32.613 -2020-02-17 19:45:00,129.08,189.891,65.997,32.613 -2020-02-17 20:00:00,119.74,185.122,68.09100000000001,32.613 -2020-02-17 20:15:00,116.95,180.209,68.09100000000001,32.613 -2020-02-17 20:30:00,113.25,175.343,68.09100000000001,32.613 -2020-02-17 20:45:00,111.85,174.063,68.09100000000001,32.613 -2020-02-17 21:00:00,106.25,171.602,59.617,32.613 -2020-02-17 21:15:00,111.38,167.703,59.617,32.613 -2020-02-17 21:30:00,111.19,166.368,59.617,32.613 -2020-02-17 21:45:00,107.47,165.453,59.617,32.613 -2020-02-17 22:00:00,100.26,156.791,54.938,32.613 -2020-02-17 22:15:00,96.86,152.559,54.938,32.613 -2020-02-17 22:30:00,93.99,138.485,54.938,32.613 -2020-02-17 22:45:00,92.23,130.718,54.938,32.613 -2020-02-17 23:00:00,85.74,124.1,47.43,32.613 -2020-02-17 23:15:00,87.58,123.447,47.43,32.613 -2020-02-17 23:30:00,86.64,124.87200000000001,47.43,32.613 -2020-02-17 23:45:00,94.27,125.181,47.43,32.613 -2020-02-18 00:00:00,90.43,119.78299999999999,48.354,32.613 -2020-02-18 00:15:00,87.49,118.541,48.354,32.613 -2020-02-18 00:30:00,81.76,118.62799999999999,48.354,32.613 -2020-02-18 00:45:00,80.22,119.175,48.354,32.613 -2020-02-18 01:00:00,84.53,121.22,45.68600000000001,32.613 -2020-02-18 01:15:00,84.82,121.465,45.68600000000001,32.613 -2020-02-18 01:30:00,82.61,121.7,45.68600000000001,32.613 -2020-02-18 01:45:00,78.21,121.87,45.68600000000001,32.613 -2020-02-18 02:00:00,82.26,123.896,44.269,32.613 -2020-02-18 02:15:00,84.76,125.286,44.269,32.613 -2020-02-18 02:30:00,83.23,126.115,44.269,32.613 -2020-02-18 02:45:00,78.58,127.90799999999999,44.269,32.613 -2020-02-18 03:00:00,79.52,130.341,44.187,32.613 -2020-02-18 03:15:00,84.9,131.954,44.187,32.613 -2020-02-18 03:30:00,86.95,133.75799999999998,44.187,32.613 -2020-02-18 03:45:00,85.47,135.149,44.187,32.613 -2020-02-18 04:00:00,80.95,146.857,46.126999999999995,32.613 -2020-02-18 04:15:00,86.03,158.57399999999998,46.126999999999995,32.613 -2020-02-18 04:30:00,90.64,161.08700000000002,46.126999999999995,32.613 -2020-02-18 04:45:00,93.17,163.274,46.126999999999995,32.613 -2020-02-18 05:00:00,95.59,195.803,49.666000000000004,32.613 -2020-02-18 05:15:00,96.2,223.696,49.666000000000004,32.613 -2020-02-18 05:30:00,101.55,218.959,49.666000000000004,32.613 -2020-02-18 05:45:00,105.28,211.90200000000002,49.666000000000004,32.613 -2020-02-18 06:00:00,111.44,209.226,61.077,32.613 -2020-02-18 06:15:00,110.26,215.14700000000002,61.077,32.613 -2020-02-18 06:30:00,113.34,217.54,61.077,32.613 -2020-02-18 06:45:00,118.15,221.449,61.077,32.613 -2020-02-18 07:00:00,124.13,222.07,74.717,32.613 -2020-02-18 07:15:00,123.32,225.953,74.717,32.613 -2020-02-18 07:30:00,123.67,227.312,74.717,32.613 -2020-02-18 07:45:00,124.55,227.015,74.717,32.613 -2020-02-18 08:00:00,129.02,225.399,69.033,32.613 -2020-02-18 08:15:00,128.96,224.354,69.033,32.613 -2020-02-18 08:30:00,129.77,220.667,69.033,32.613 -2020-02-18 08:45:00,127.77,216.868,69.033,32.613 -2020-02-18 09:00:00,129.25,209.703,63.113,32.613 -2020-02-18 09:15:00,133.26,206.56900000000002,63.113,32.613 -2020-02-18 09:30:00,129.51,205.06,63.113,32.613 -2020-02-18 09:45:00,127.42,202.097,63.113,32.613 -2020-02-18 10:00:00,123.91,197.831,61.461999999999996,32.613 -2020-02-18 10:15:00,124.56,194.101,61.461999999999996,32.613 -2020-02-18 10:30:00,124.88,191.581,61.461999999999996,32.613 -2020-02-18 10:45:00,127.18,190.551,61.461999999999996,32.613 -2020-02-18 11:00:00,126.45,188.638,59.614,32.613 -2020-02-18 11:15:00,127.78,187.733,59.614,32.613 -2020-02-18 11:30:00,124.02,186.854,59.614,32.613 -2020-02-18 11:45:00,124.8,185.56099999999998,59.614,32.613 -2020-02-18 12:00:00,119.33,180.585,57.415,32.613 -2020-02-18 12:15:00,122.01,180.7,57.415,32.613 -2020-02-18 12:30:00,121.83,180.183,57.415,32.613 -2020-02-18 12:45:00,118.63,181.13099999999997,57.415,32.613 -2020-02-18 13:00:00,114.34,180.268,58.534,32.613 -2020-02-18 13:15:00,116.38,179.06400000000002,58.534,32.613 -2020-02-18 13:30:00,116.49,178.125,58.534,32.613 -2020-02-18 13:45:00,118.33,177.733,58.534,32.613 -2020-02-18 14:00:00,120.0,178.25900000000001,59.415,32.613 -2020-02-18 14:15:00,120.28,178.108,59.415,32.613 -2020-02-18 14:30:00,120.23,178.66400000000002,59.415,32.613 -2020-02-18 14:45:00,123.79,179.44299999999998,59.415,32.613 -2020-02-18 15:00:00,121.51,181.301,62.071999999999996,32.613 -2020-02-18 15:15:00,120.26,180.593,62.071999999999996,32.613 -2020-02-18 15:30:00,122.85,181.87900000000002,62.071999999999996,32.613 -2020-02-18 15:45:00,121.26,182.873,62.071999999999996,32.613 -2020-02-18 16:00:00,122.32,184.453,64.99,32.613 -2020-02-18 16:15:00,121.34,185.988,64.99,32.613 -2020-02-18 16:30:00,122.1,188.827,64.99,32.613 -2020-02-18 16:45:00,124.58,190.19,64.99,32.613 -2020-02-18 17:00:00,130.96,192.155,72.658,32.613 -2020-02-18 17:15:00,129.03,194.37,72.658,32.613 -2020-02-18 17:30:00,131.39,196.77700000000002,72.658,32.613 -2020-02-18 17:45:00,134.3,197.639,72.658,32.613 -2020-02-18 18:00:00,137.03,199.84099999999998,73.645,32.613 -2020-02-18 18:15:00,135.62,199.113,73.645,32.613 -2020-02-18 18:30:00,134.52,197.81,73.645,32.613 -2020-02-18 18:45:00,134.65,198.78099999999998,73.645,32.613 -2020-02-18 19:00:00,130.82,197.82299999999998,67.085,32.613 -2020-02-18 19:15:00,130.82,194.50099999999998,67.085,32.613 -2020-02-18 19:30:00,138.13,192.917,67.085,32.613 -2020-02-18 19:45:00,138.56,189.81,67.085,32.613 -2020-02-18 20:00:00,126.03,185.172,66.138,32.613 -2020-02-18 20:15:00,119.06,179.63099999999997,66.138,32.613 -2020-02-18 20:30:00,114.73,175.77599999999998,66.138,32.613 -2020-02-18 20:45:00,113.6,173.94299999999998,66.138,32.613 -2020-02-18 21:00:00,107.89,170.787,57.512,32.613 -2020-02-18 21:15:00,114.35,167.757,57.512,32.613 -2020-02-18 21:30:00,113.36,165.707,57.512,32.613 -2020-02-18 21:45:00,111.8,165.03599999999997,57.512,32.613 -2020-02-18 22:00:00,100.85,158.043,54.545,32.613 -2020-02-18 22:15:00,99.95,153.578,54.545,32.613 -2020-02-18 22:30:00,100.92,139.555,54.545,32.613 -2020-02-18 22:45:00,102.09,132.065,54.545,32.613 -2020-02-18 23:00:00,97.23,125.461,48.605,32.613 -2020-02-18 23:15:00,94.48,123.948,48.605,32.613 -2020-02-18 23:30:00,89.92,125.03,48.605,32.613 -2020-02-18 23:45:00,91.19,124.929,48.605,32.613 -2020-02-19 00:00:00,90.06,119.571,45.675,32.613 -2020-02-19 00:15:00,88.18,118.331,45.675,32.613 -2020-02-19 00:30:00,88.53,118.399,45.675,32.613 -2020-02-19 00:45:00,85.32,118.94200000000001,45.675,32.613 -2020-02-19 01:00:00,85.6,120.95299999999999,43.015,32.613 -2020-02-19 01:15:00,87.17,121.185,43.015,32.613 -2020-02-19 01:30:00,86.58,121.40700000000001,43.015,32.613 -2020-02-19 01:45:00,82.82,121.579,43.015,32.613 -2020-02-19 02:00:00,84.77,123.601,41.0,32.613 -2020-02-19 02:15:00,86.47,124.98700000000001,41.0,32.613 -2020-02-19 02:30:00,83.73,125.83200000000001,41.0,32.613 -2020-02-19 02:45:00,83.36,127.624,41.0,32.613 -2020-02-19 03:00:00,86.31,130.064,41.318000000000005,32.613 -2020-02-19 03:15:00,87.22,131.672,41.318000000000005,32.613 -2020-02-19 03:30:00,83.52,133.47,41.318000000000005,32.613 -2020-02-19 03:45:00,86.28,134.872,41.318000000000005,32.613 -2020-02-19 04:00:00,81.82,146.582,42.544,32.613 -2020-02-19 04:15:00,86.0,158.291,42.544,32.613 -2020-02-19 04:30:00,91.07,160.819,42.544,32.613 -2020-02-19 04:45:00,93.83,162.994,42.544,32.613 -2020-02-19 05:00:00,95.03,195.505,45.161,32.613 -2020-02-19 05:15:00,92.61,223.41400000000002,45.161,32.613 -2020-02-19 05:30:00,95.38,218.649,45.161,32.613 -2020-02-19 05:45:00,101.64,211.597,45.161,32.613 -2020-02-19 06:00:00,106.45,208.93200000000002,61.86600000000001,32.613 -2020-02-19 06:15:00,110.83,214.862,61.86600000000001,32.613 -2020-02-19 06:30:00,114.29,217.22099999999998,61.86600000000001,32.613 -2020-02-19 06:45:00,118.12,221.122,61.86600000000001,32.613 -2020-02-19 07:00:00,124.11,221.766,77.814,32.613 -2020-02-19 07:15:00,126.22,225.61900000000003,77.814,32.613 -2020-02-19 07:30:00,128.75,226.94299999999998,77.814,32.613 -2020-02-19 07:45:00,127.24,226.606,77.814,32.613 -2020-02-19 08:00:00,130.4,224.96900000000002,70.251,32.613 -2020-02-19 08:15:00,128.37,223.905,70.251,32.613 -2020-02-19 08:30:00,132.94,220.16299999999998,70.251,32.613 -2020-02-19 08:45:00,130.84,216.36900000000003,70.251,32.613 -2020-02-19 09:00:00,133.42,209.21200000000002,66.965,32.613 -2020-02-19 09:15:00,135.01,206.08,66.965,32.613 -2020-02-19 09:30:00,136.59,204.59099999999998,66.965,32.613 -2020-02-19 09:45:00,138.7,201.63400000000001,66.965,32.613 -2020-02-19 10:00:00,134.17,197.37900000000002,63.628,32.613 -2020-02-19 10:15:00,135.78,193.68099999999998,63.628,32.613 -2020-02-19 10:30:00,135.61,191.174,63.628,32.613 -2020-02-19 10:45:00,136.33,190.16,63.628,32.613 -2020-02-19 11:00:00,134.83,188.235,62.516999999999996,32.613 -2020-02-19 11:15:00,132.88,187.34599999999998,62.516999999999996,32.613 -2020-02-19 11:30:00,130.69,186.47299999999998,62.516999999999996,32.613 -2020-02-19 11:45:00,126.99,185.19299999999998,62.516999999999996,32.613 -2020-02-19 12:00:00,126.05,180.234,60.888999999999996,32.613 -2020-02-19 12:15:00,122.3,180.364,60.888999999999996,32.613 -2020-02-19 12:30:00,119.64,179.81799999999998,60.888999999999996,32.613 -2020-02-19 12:45:00,118.58,180.763,60.888999999999996,32.613 -2020-02-19 13:00:00,115.52,179.93,61.57899999999999,32.613 -2020-02-19 13:15:00,116.57,178.705,61.57899999999999,32.613 -2020-02-19 13:30:00,113.94,177.752,61.57899999999999,32.613 -2020-02-19 13:45:00,115.97,177.36,61.57899999999999,32.613 -2020-02-19 14:00:00,114.21,177.94400000000002,62.602,32.613 -2020-02-19 14:15:00,115.49,177.77200000000002,62.602,32.613 -2020-02-19 14:30:00,115.54,178.308,62.602,32.613 -2020-02-19 14:45:00,118.46,179.09900000000002,62.602,32.613 -2020-02-19 15:00:00,119.72,180.96599999999998,64.259,32.613 -2020-02-19 15:15:00,122.91,180.229,64.259,32.613 -2020-02-19 15:30:00,120.97,181.476,64.259,32.613 -2020-02-19 15:45:00,121.61,182.453,64.259,32.613 -2020-02-19 16:00:00,124.31,184.03400000000002,67.632,32.613 -2020-02-19 16:15:00,123.96,185.55599999999998,67.632,32.613 -2020-02-19 16:30:00,123.8,188.396,67.632,32.613 -2020-02-19 16:45:00,124.67,189.734,67.632,32.613 -2020-02-19 17:00:00,129.4,191.702,72.583,32.613 -2020-02-19 17:15:00,130.26,193.933,72.583,32.613 -2020-02-19 17:30:00,133.14,196.37,72.583,32.613 -2020-02-19 17:45:00,133.67,197.252,72.583,32.613 -2020-02-19 18:00:00,138.63,199.46599999999998,72.744,32.613 -2020-02-19 18:15:00,135.29,198.795,72.744,32.613 -2020-02-19 18:30:00,137.36,197.49099999999999,72.744,32.613 -2020-02-19 18:45:00,134.04,198.484,72.744,32.613 -2020-02-19 19:00:00,131.09,197.487,69.684,32.613 -2020-02-19 19:15:00,130.73,194.18,69.684,32.613 -2020-02-19 19:30:00,136.9,192.62099999999998,69.684,32.613 -2020-02-19 19:45:00,137.66,189.549,69.684,32.613 -2020-02-19 20:00:00,126.33,184.882,70.036,32.613 -2020-02-19 20:15:00,117.21,179.354,70.036,32.613 -2020-02-19 20:30:00,116.21,175.517,70.036,32.613 -2020-02-19 20:45:00,114.1,173.687,70.036,32.613 -2020-02-19 21:00:00,108.92,170.518,60.431999999999995,32.613 -2020-02-19 21:15:00,114.41,167.477,60.431999999999995,32.613 -2020-02-19 21:30:00,113.68,165.428,60.431999999999995,32.613 -2020-02-19 21:45:00,113.09,164.775,60.431999999999995,32.613 -2020-02-19 22:00:00,101.66,157.768,56.2,32.613 -2020-02-19 22:15:00,97.67,153.327,56.2,32.613 -2020-02-19 22:30:00,95.3,139.265,56.2,32.613 -2020-02-19 22:45:00,95.0,131.78,56.2,32.613 -2020-02-19 23:00:00,94.34,125.169,47.927,32.613 -2020-02-19 23:15:00,97.09,123.671,47.927,32.613 -2020-02-19 23:30:00,95.13,124.764,47.927,32.613 -2020-02-19 23:45:00,91.62,124.68700000000001,47.927,32.613 -2020-02-20 00:00:00,84.69,119.354,43.794,32.613 -2020-02-20 00:15:00,87.96,118.113,43.794,32.613 -2020-02-20 00:30:00,88.54,118.163,43.794,32.613 -2020-02-20 00:45:00,87.1,118.70299999999999,43.794,32.613 -2020-02-20 01:00:00,80.45,120.679,42.397,32.613 -2020-02-20 01:15:00,79.74,120.897,42.397,32.613 -2020-02-20 01:30:00,79.29,121.10799999999999,42.397,32.613 -2020-02-20 01:45:00,84.73,121.281,42.397,32.613 -2020-02-20 02:00:00,85.65,123.29799999999999,40.010999999999996,32.613 -2020-02-20 02:15:00,86.23,124.682,40.010999999999996,32.613 -2020-02-20 02:30:00,82.4,125.541,40.010999999999996,32.613 -2020-02-20 02:45:00,80.94,127.333,40.010999999999996,32.613 -2020-02-20 03:00:00,86.23,129.78,39.181,32.613 -2020-02-20 03:15:00,87.78,131.382,39.181,32.613 -2020-02-20 03:30:00,86.6,133.175,39.181,32.613 -2020-02-20 03:45:00,82.73,134.589,39.181,32.613 -2020-02-20 04:00:00,84.08,146.3,40.39,32.613 -2020-02-20 04:15:00,82.86,158.0,40.39,32.613 -2020-02-20 04:30:00,83.77,160.543,40.39,32.613 -2020-02-20 04:45:00,86.58,162.708,40.39,32.613 -2020-02-20 05:00:00,97.42,195.2,45.504,32.613 -2020-02-20 05:15:00,99.75,223.127,45.504,32.613 -2020-02-20 05:30:00,97.65,218.333,45.504,32.613 -2020-02-20 05:45:00,102.13,211.28599999999997,45.504,32.613 -2020-02-20 06:00:00,106.74,208.63,57.748000000000005,32.613 -2020-02-20 06:15:00,110.76,214.57,57.748000000000005,32.613 -2020-02-20 06:30:00,114.34,216.894,57.748000000000005,32.613 -2020-02-20 06:45:00,118.21,220.787,57.748000000000005,32.613 -2020-02-20 07:00:00,123.28,221.452,72.138,32.613 -2020-02-20 07:15:00,123.1,225.27700000000002,72.138,32.613 -2020-02-20 07:30:00,125.66,226.56400000000002,72.138,32.613 -2020-02-20 07:45:00,125.06,226.188,72.138,32.613 -2020-02-20 08:00:00,127.31,224.528,65.542,32.613 -2020-02-20 08:15:00,125.33,223.446,65.542,32.613 -2020-02-20 08:30:00,126.93,219.65,65.542,32.613 -2020-02-20 08:45:00,122.41,215.863,65.542,32.613 -2020-02-20 09:00:00,122.95,208.71200000000002,60.523,32.613 -2020-02-20 09:15:00,123.48,205.582,60.523,32.613 -2020-02-20 09:30:00,123.66,204.113,60.523,32.613 -2020-02-20 09:45:00,123.38,201.16299999999998,60.523,32.613 -2020-02-20 10:00:00,123.79,196.918,57.449,32.613 -2020-02-20 10:15:00,122.44,193.252,57.449,32.613 -2020-02-20 10:30:00,120.99,190.75900000000001,57.449,32.613 -2020-02-20 10:45:00,121.04,189.761,57.449,32.613 -2020-02-20 11:00:00,118.26,187.826,54.505,32.613 -2020-02-20 11:15:00,118.28,186.951,54.505,32.613 -2020-02-20 11:30:00,118.21,186.08599999999998,54.505,32.613 -2020-02-20 11:45:00,117.86,184.82,54.505,32.613 -2020-02-20 12:00:00,116.74,179.87599999999998,51.50899999999999,32.613 -2020-02-20 12:15:00,118.18,180.021,51.50899999999999,32.613 -2020-02-20 12:30:00,118.55,179.447,51.50899999999999,32.613 -2020-02-20 12:45:00,118.61,180.388,51.50899999999999,32.613 -2020-02-20 13:00:00,114.9,179.585,51.303999999999995,32.613 -2020-02-20 13:15:00,115.25,178.33700000000002,51.303999999999995,32.613 -2020-02-20 13:30:00,112.8,177.372,51.303999999999995,32.613 -2020-02-20 13:45:00,116.14,176.981,51.303999999999995,32.613 -2020-02-20 14:00:00,111.39,177.622,52.785,32.613 -2020-02-20 14:15:00,115.64,177.429,52.785,32.613 -2020-02-20 14:30:00,114.88,177.945,52.785,32.613 -2020-02-20 14:45:00,116.2,178.748,52.785,32.613 -2020-02-20 15:00:00,118.06,180.623,56.458999999999996,32.613 -2020-02-20 15:15:00,115.92,179.859,56.458999999999996,32.613 -2020-02-20 15:30:00,115.99,181.065,56.458999999999996,32.613 -2020-02-20 15:45:00,117.51,182.02599999999998,56.458999999999996,32.613 -2020-02-20 16:00:00,117.85,183.607,59.388000000000005,32.613 -2020-02-20 16:15:00,118.3,185.11700000000002,59.388000000000005,32.613 -2020-02-20 16:30:00,122.42,187.958,59.388000000000005,32.613 -2020-02-20 16:45:00,122.43,189.268,59.388000000000005,32.613 -2020-02-20 17:00:00,124.49,191.24099999999999,64.462,32.613 -2020-02-20 17:15:00,125.36,193.489,64.462,32.613 -2020-02-20 17:30:00,131.6,195.954,64.462,32.613 -2020-02-20 17:45:00,131.89,196.857,64.462,32.613 -2020-02-20 18:00:00,138.28,199.081,65.128,32.613 -2020-02-20 18:15:00,136.37,198.468,65.128,32.613 -2020-02-20 18:30:00,138.13,197.16400000000002,65.128,32.613 -2020-02-20 18:45:00,136.19,198.179,65.128,32.613 -2020-02-20 19:00:00,132.24,197.143,61.316,32.613 -2020-02-20 19:15:00,136.06,193.85,61.316,32.613 -2020-02-20 19:30:00,140.35,192.31599999999997,61.316,32.613 -2020-02-20 19:45:00,136.18,189.28099999999998,61.316,32.613 -2020-02-20 20:00:00,124.63,184.584,59.845,32.613 -2020-02-20 20:15:00,117.47,179.071,59.845,32.613 -2020-02-20 20:30:00,115.88,175.25,59.845,32.613 -2020-02-20 20:45:00,113.56,173.425,59.845,32.613 -2020-02-20 21:00:00,109.82,170.24099999999999,54.83,32.613 -2020-02-20 21:15:00,107.49,167.19099999999997,54.83,32.613 -2020-02-20 21:30:00,105.41,165.142,54.83,32.613 -2020-02-20 21:45:00,107.31,164.50900000000001,54.83,32.613 -2020-02-20 22:00:00,98.64,157.486,50.933,32.613 -2020-02-20 22:15:00,102.93,153.07,50.933,32.613 -2020-02-20 22:30:00,103.82,138.967,50.933,32.613 -2020-02-20 22:45:00,105.37,131.486,50.933,32.613 -2020-02-20 23:00:00,94.47,124.87,45.32899999999999,32.613 -2020-02-20 23:15:00,90.46,123.387,45.32899999999999,32.613 -2020-02-20 23:30:00,88.59,124.492,45.32899999999999,32.613 -2020-02-20 23:45:00,88.87,124.436,45.32899999999999,32.613 -2020-02-21 00:00:00,84.07,118.053,43.74,32.613 -2020-02-21 00:15:00,90.99,117.012,43.74,32.613 -2020-02-21 00:30:00,90.98,116.915,43.74,32.613 -2020-02-21 00:45:00,87.5,117.566,43.74,32.613 -2020-02-21 01:00:00,80.73,119.194,42.555,32.613 -2020-02-21 01:15:00,82.69,120.294,42.555,32.613 -2020-02-21 01:30:00,78.88,120.279,42.555,32.613 -2020-02-21 01:45:00,79.85,120.557,42.555,32.613 -2020-02-21 02:00:00,79.02,122.663,41.68600000000001,32.613 -2020-02-21 02:15:00,86.13,123.93,41.68600000000001,32.613 -2020-02-21 02:30:00,87.4,125.331,41.68600000000001,32.613 -2020-02-21 02:45:00,87.01,127.15700000000001,41.68600000000001,32.613 -2020-02-21 03:00:00,80.72,128.641,42.278999999999996,32.613 -2020-02-21 03:15:00,80.62,131.14600000000002,42.278999999999996,32.613 -2020-02-21 03:30:00,82.45,132.915,42.278999999999996,32.613 -2020-02-21 03:45:00,82.52,134.672,42.278999999999996,32.613 -2020-02-21 04:00:00,83.5,146.612,43.742,32.613 -2020-02-21 04:15:00,88.63,158.04399999999998,43.742,32.613 -2020-02-21 04:30:00,94.41,160.84,43.742,32.613 -2020-02-21 04:45:00,95.37,161.859,43.742,32.613 -2020-02-21 05:00:00,94.16,193.079,46.973,32.613 -2020-02-21 05:15:00,92.16,222.521,46.973,32.613 -2020-02-21 05:30:00,94.26,218.762,46.973,32.613 -2020-02-21 05:45:00,99.52,211.65099999999998,46.973,32.613 -2020-02-21 06:00:00,108.6,209.43200000000002,59.63399999999999,32.613 -2020-02-21 06:15:00,111.42,213.935,59.63399999999999,32.613 -2020-02-21 06:30:00,115.46,215.37400000000002,59.63399999999999,32.613 -2020-02-21 06:45:00,117.72,220.855,59.63399999999999,32.613 -2020-02-21 07:00:00,122.43,220.72799999999998,71.631,32.613 -2020-02-21 07:15:00,123.45,225.54,71.631,32.613 -2020-02-21 07:30:00,123.07,226.58900000000003,71.631,32.613 -2020-02-21 07:45:00,124.6,225.299,71.631,32.613 -2020-02-21 08:00:00,127.22,222.543,66.181,32.613 -2020-02-21 08:15:00,125.37,221.072,66.181,32.613 -2020-02-21 08:30:00,126.6,218.17700000000002,66.181,32.613 -2020-02-21 08:45:00,123.25,212.84599999999998,66.181,32.613 -2020-02-21 09:00:00,125.59,206.114,63.086000000000006,32.613 -2020-02-21 09:15:00,125.25,203.579,63.086000000000006,32.613 -2020-02-21 09:30:00,126.21,201.69799999999998,63.086000000000006,32.613 -2020-02-21 09:45:00,128.09,198.653,63.086000000000006,32.613 -2020-02-21 10:00:00,122.61,193.30700000000002,60.886,32.613 -2020-02-21 10:15:00,124.67,190.35299999999998,60.886,32.613 -2020-02-21 10:30:00,125.15,187.81599999999997,60.886,32.613 -2020-02-21 10:45:00,124.81,186.39700000000002,60.886,32.613 -2020-02-21 11:00:00,120.18,184.442,59.391000000000005,32.613 -2020-02-21 11:15:00,120.77,182.68200000000002,59.391000000000005,32.613 -2020-02-21 11:30:00,120.33,183.52599999999998,59.391000000000005,32.613 -2020-02-21 11:45:00,120.04,182.298,59.391000000000005,32.613 -2020-02-21 12:00:00,118.73,178.43900000000002,56.172,32.613 -2020-02-21 12:15:00,118.32,176.53799999999998,56.172,32.613 -2020-02-21 12:30:00,117.18,176.092,56.172,32.613 -2020-02-21 12:45:00,119.34,177.515,56.172,32.613 -2020-02-21 13:00:00,112.77,177.671,54.406000000000006,32.613 -2020-02-21 13:15:00,112.5,177.203,54.406000000000006,32.613 -2020-02-21 13:30:00,111.04,176.26,54.406000000000006,32.613 -2020-02-21 13:45:00,111.47,175.815,54.406000000000006,32.613 -2020-02-21 14:00:00,109.79,175.38299999999998,53.578,32.613 -2020-02-21 14:15:00,110.7,175.00599999999997,53.578,32.613 -2020-02-21 14:30:00,110.76,176.04,53.578,32.613 -2020-02-21 14:45:00,110.95,177.13099999999997,53.578,32.613 -2020-02-21 15:00:00,117.42,178.542,56.568999999999996,32.613 -2020-02-21 15:15:00,120.19,177.31599999999997,56.568999999999996,32.613 -2020-02-21 15:30:00,119.17,176.987,56.568999999999996,32.613 -2020-02-21 15:45:00,115.35,178.092,56.568999999999996,32.613 -2020-02-21 16:00:00,117.52,178.511,60.169,32.613 -2020-02-21 16:15:00,115.01,180.301,60.169,32.613 -2020-02-21 16:30:00,116.7,183.232,60.169,32.613 -2020-02-21 16:45:00,119.87,184.386,60.169,32.613 -2020-02-21 17:00:00,123.86,186.592,65.497,32.613 -2020-02-21 17:15:00,126.26,188.45,65.497,32.613 -2020-02-21 17:30:00,126.38,190.63400000000001,65.497,32.613 -2020-02-21 17:45:00,128.7,191.324,65.497,32.613 -2020-02-21 18:00:00,133.63,194.234,65.082,32.613 -2020-02-21 18:15:00,132.24,193.282,65.082,32.613 -2020-02-21 18:30:00,135.09,192.361,65.082,32.613 -2020-02-21 18:45:00,134.34,193.4,65.082,32.613 -2020-02-21 19:00:00,132.35,193.229,60.968,32.613 -2020-02-21 19:15:00,136.76,191.30200000000002,60.968,32.613 -2020-02-21 19:30:00,136.33,189.387,60.968,32.613 -2020-02-21 19:45:00,136.31,185.921,60.968,32.613 -2020-02-21 20:00:00,121.81,181.247,61.123000000000005,32.613 -2020-02-21 20:15:00,119.79,175.767,61.123000000000005,32.613 -2020-02-21 20:30:00,114.65,171.91400000000002,61.123000000000005,32.613 -2020-02-21 20:45:00,115.4,170.627,61.123000000000005,32.613 -2020-02-21 21:00:00,106.52,167.949,55.416000000000004,32.613 -2020-02-21 21:15:00,104.0,165.347,55.416000000000004,32.613 -2020-02-21 21:30:00,102.97,163.342,55.416000000000004,32.613 -2020-02-21 21:45:00,103.72,163.263,55.416000000000004,32.613 -2020-02-21 22:00:00,97.94,157.187,51.631,32.613 -2020-02-21 22:15:00,94.54,152.662,51.631,32.613 -2020-02-21 22:30:00,92.36,144.856,51.631,32.613 -2020-02-21 22:45:00,96.36,140.861,51.631,32.613 -2020-02-21 23:00:00,96.29,133.83,44.898,32.613 -2020-02-21 23:15:00,93.93,130.423,44.898,32.613 -2020-02-21 23:30:00,87.56,130.07399999999998,44.898,32.613 -2020-02-21 23:45:00,86.86,129.387,44.898,32.613 -2020-02-22 00:00:00,80.69,115.046,42.033,32.431999999999995 -2020-02-22 00:15:00,85.92,109.822,42.033,32.431999999999995 -2020-02-22 00:30:00,86.31,111.001,42.033,32.431999999999995 -2020-02-22 00:45:00,83.99,112.337,42.033,32.431999999999995 -2020-02-22 01:00:00,75.49,114.553,38.255,32.431999999999995 -2020-02-22 01:15:00,77.66,114.685,38.255,32.431999999999995 -2020-02-22 01:30:00,74.25,114.135,38.255,32.431999999999995 -2020-02-22 01:45:00,78.27,114.225,38.255,32.431999999999995 -2020-02-22 02:00:00,80.13,116.98200000000001,36.404,32.431999999999995 -2020-02-22 02:15:00,81.83,117.868,36.404,32.431999999999995 -2020-02-22 02:30:00,77.76,118.184,36.404,32.431999999999995 -2020-02-22 02:45:00,77.53,120.12299999999999,36.404,32.431999999999995 -2020-02-22 03:00:00,76.75,122.20700000000001,36.083,32.431999999999995 -2020-02-22 03:15:00,80.16,123.5,36.083,32.431999999999995 -2020-02-22 03:30:00,80.23,123.714,36.083,32.431999999999995 -2020-02-22 03:45:00,78.0,125.616,36.083,32.431999999999995 -2020-02-22 04:00:00,72.92,133.475,36.102,32.431999999999995 -2020-02-22 04:15:00,73.17,142.395,36.102,32.431999999999995 -2020-02-22 04:30:00,73.24,143.054,36.102,32.431999999999995 -2020-02-22 04:45:00,74.17,143.582,36.102,32.431999999999995 -2020-02-22 05:00:00,74.51,159.02200000000002,35.284,32.431999999999995 -2020-02-22 05:15:00,75.89,169.7,35.284,32.431999999999995 -2020-02-22 05:30:00,75.3,166.28599999999997,35.284,32.431999999999995 -2020-02-22 05:45:00,76.79,164.58599999999998,35.284,32.431999999999995 -2020-02-22 06:00:00,78.47,180.94,36.265,32.431999999999995 -2020-02-22 06:15:00,79.02,201.58900000000003,36.265,32.431999999999995 -2020-02-22 06:30:00,79.89,197.68200000000002,36.265,32.431999999999995 -2020-02-22 06:45:00,80.02,194.308,36.265,32.431999999999995 -2020-02-22 07:00:00,81.54,190.49099999999999,40.714,32.431999999999995 -2020-02-22 07:15:00,85.98,194.023,40.714,32.431999999999995 -2020-02-22 07:30:00,86.18,197.71,40.714,32.431999999999995 -2020-02-22 07:45:00,87.86,200.28400000000002,40.714,32.431999999999995 -2020-02-22 08:00:00,89.81,201.467,46.692,32.431999999999995 -2020-02-22 08:15:00,91.73,203.52200000000002,46.692,32.431999999999995 -2020-02-22 08:30:00,98.01,202.149,46.692,32.431999999999995 -2020-02-22 08:45:00,99.24,199.93,46.692,32.431999999999995 -2020-02-22 09:00:00,102.43,194.99599999999998,48.925,32.431999999999995 -2020-02-22 09:15:00,102.12,193.207,48.925,32.431999999999995 -2020-02-22 09:30:00,102.49,192.239,48.925,32.431999999999995 -2020-02-22 09:45:00,99.78,189.347,48.925,32.431999999999995 -2020-02-22 10:00:00,95.56,184.31099999999998,47.799,32.431999999999995 -2020-02-22 10:15:00,99.82,181.56099999999998,47.799,32.431999999999995 -2020-02-22 10:30:00,100.4,179.19,47.799,32.431999999999995 -2020-02-22 10:45:00,99.55,179.048,47.799,32.431999999999995 -2020-02-22 11:00:00,95.77,177.29,44.309,32.431999999999995 -2020-02-22 11:15:00,95.07,174.896,44.309,32.431999999999995 -2020-02-22 11:30:00,94.71,174.668,44.309,32.431999999999995 -2020-02-22 11:45:00,97.76,172.54,44.309,32.431999999999995 -2020-02-22 12:00:00,92.83,167.857,42.367,32.431999999999995 -2020-02-22 12:15:00,92.41,166.61599999999999,42.367,32.431999999999995 -2020-02-22 12:30:00,88.04,166.44400000000002,42.367,32.431999999999995 -2020-02-22 12:45:00,86.69,167.13400000000001,42.367,32.431999999999995 -2020-02-22 13:00:00,82.02,166.915,39.036,32.431999999999995 -2020-02-22 13:15:00,85.74,164.417,39.036,32.431999999999995 -2020-02-22 13:30:00,79.5,163.02700000000002,39.036,32.431999999999995 -2020-02-22 13:45:00,80.82,163.011,39.036,32.431999999999995 -2020-02-22 14:00:00,78.34,163.83100000000002,37.995,32.431999999999995 -2020-02-22 14:15:00,78.85,162.882,37.995,32.431999999999995 -2020-02-22 14:30:00,80.14,162.118,37.995,32.431999999999995 -2020-02-22 14:45:00,81.7,163.444,37.995,32.431999999999995 -2020-02-22 15:00:00,81.24,165.505,40.71,32.431999999999995 -2020-02-22 15:15:00,82.52,165.051,40.71,32.431999999999995 -2020-02-22 15:30:00,83.61,166.18200000000002,40.71,32.431999999999995 -2020-02-22 15:45:00,85.83,167.262,40.71,32.431999999999995 -2020-02-22 16:00:00,87.34,166.548,46.998000000000005,32.431999999999995 -2020-02-22 16:15:00,91.33,169.175,46.998000000000005,32.431999999999995 -2020-02-22 16:30:00,93.53,172.06,46.998000000000005,32.431999999999995 -2020-02-22 16:45:00,92.11,174.048,46.998000000000005,32.431999999999995 -2020-02-22 17:00:00,99.16,175.66,55.431000000000004,32.431999999999995 -2020-02-22 17:15:00,98.14,179.105,55.431000000000004,32.431999999999995 -2020-02-22 17:30:00,103.35,181.222,55.431000000000004,32.431999999999995 -2020-02-22 17:45:00,104.88,181.547,55.431000000000004,32.431999999999995 -2020-02-22 18:00:00,113.78,184.007,55.989,32.431999999999995 -2020-02-22 18:15:00,112.69,184.90400000000002,55.989,32.431999999999995 -2020-02-22 18:30:00,112.95,185.31,55.989,32.431999999999995 -2020-02-22 18:45:00,112.81,183.035,55.989,32.431999999999995 -2020-02-22 19:00:00,111.89,183.692,50.882,32.431999999999995 -2020-02-22 19:15:00,112.4,181.27599999999998,50.882,32.431999999999995 -2020-02-22 19:30:00,107.62,180.136,50.882,32.431999999999995 -2020-02-22 19:45:00,106.47,176.567,50.882,32.431999999999995 -2020-02-22 20:00:00,101.54,174.00799999999998,43.172,32.431999999999995 -2020-02-22 20:15:00,99.49,170.608,43.172,32.431999999999995 -2020-02-22 20:30:00,96.48,166.41400000000002,43.172,32.431999999999995 -2020-02-22 20:45:00,98.36,164.771,43.172,32.431999999999995 -2020-02-22 21:00:00,91.47,164.25099999999998,37.599000000000004,32.431999999999995 -2020-02-22 21:15:00,90.24,162.063,37.599000000000004,32.431999999999995 -2020-02-22 21:30:00,88.52,161.263,37.599000000000004,32.431999999999995 -2020-02-22 21:45:00,88.73,160.796,37.599000000000004,32.431999999999995 -2020-02-22 22:00:00,85.26,156.02100000000002,39.047,32.431999999999995 -2020-02-22 22:15:00,84.4,153.955,39.047,32.431999999999995 -2020-02-22 22:30:00,81.79,152.175,39.047,32.431999999999995 -2020-02-22 22:45:00,81.3,150.017,39.047,32.431999999999995 -2020-02-22 23:00:00,77.58,145.344,32.339,32.431999999999995 -2020-02-22 23:15:00,77.79,140.363,32.339,32.431999999999995 -2020-02-22 23:30:00,74.96,138.365,32.339,32.431999999999995 -2020-02-22 23:45:00,73.63,135.339,32.339,32.431999999999995 -2020-02-23 00:00:00,70.14,115.244,29.988000000000003,32.431999999999995 -2020-02-23 00:15:00,69.61,109.686,29.988000000000003,32.431999999999995 -2020-02-23 00:30:00,69.59,110.479,29.988000000000003,32.431999999999995 -2020-02-23 00:45:00,69.38,112.488,29.988000000000003,32.431999999999995 -2020-02-23 01:00:00,65.46,114.53399999999999,28.531999999999996,32.431999999999995 -2020-02-23 01:15:00,66.66,115.661,28.531999999999996,32.431999999999995 -2020-02-23 01:30:00,65.52,115.603,28.531999999999996,32.431999999999995 -2020-02-23 01:45:00,65.26,115.382,28.531999999999996,32.431999999999995 -2020-02-23 02:00:00,63.83,117.405,27.805999999999997,32.431999999999995 -2020-02-23 02:15:00,64.64,117.473,27.805999999999997,32.431999999999995 -2020-02-23 02:30:00,63.77,118.64200000000001,27.805999999999997,32.431999999999995 -2020-02-23 02:45:00,64.98,121.022,27.805999999999997,32.431999999999995 -2020-02-23 03:00:00,63.94,123.436,26.193,32.431999999999995 -2020-02-23 03:15:00,64.7,124.21799999999999,26.193,32.431999999999995 -2020-02-23 03:30:00,64.1,125.774,26.193,32.431999999999995 -2020-02-23 03:45:00,64.57,127.594,26.193,32.431999999999995 -2020-02-23 04:00:00,64.91,135.216,27.19,32.431999999999995 -2020-02-23 04:15:00,64.97,143.135,27.19,32.431999999999995 -2020-02-23 04:30:00,65.65,143.904,27.19,32.431999999999995 -2020-02-23 04:45:00,66.48,144.673,27.19,32.431999999999995 -2020-02-23 05:00:00,67.26,156.69,28.166999999999998,32.431999999999995 -2020-02-23 05:15:00,67.76,165.021,28.166999999999998,32.431999999999995 -2020-02-23 05:30:00,68.15,161.405,28.166999999999998,32.431999999999995 -2020-02-23 05:45:00,68.8,159.939,28.166999999999998,32.431999999999995 -2020-02-23 06:00:00,69.41,176.139,27.16,32.431999999999995 -2020-02-23 06:15:00,69.86,195.146,27.16,32.431999999999995 -2020-02-23 06:30:00,70.71,190.112,27.16,32.431999999999995 -2020-02-23 06:45:00,69.95,185.675,27.16,32.431999999999995 -2020-02-23 07:00:00,72.62,184.268,29.578000000000003,32.431999999999995 -2020-02-23 07:15:00,73.12,186.9,29.578000000000003,32.431999999999995 -2020-02-23 07:30:00,74.51,189.389,29.578000000000003,32.431999999999995 -2020-02-23 07:45:00,76.29,191.18200000000002,29.578000000000003,32.431999999999995 -2020-02-23 08:00:00,78.45,194.149,34.650999999999996,32.431999999999995 -2020-02-23 08:15:00,78.33,196.13,34.650999999999996,32.431999999999995 -2020-02-23 08:30:00,78.73,196.333,34.650999999999996,32.431999999999995 -2020-02-23 08:45:00,79.07,196.078,34.650999999999996,32.431999999999995 -2020-02-23 09:00:00,78.69,190.75900000000001,38.080999999999996,32.431999999999995 -2020-02-23 09:15:00,78.31,189.495,38.080999999999996,32.431999999999995 -2020-02-23 09:30:00,78.05,188.407,38.080999999999996,32.431999999999995 -2020-02-23 09:45:00,77.75,185.43599999999998,38.080999999999996,32.431999999999995 -2020-02-23 10:00:00,76.21,182.868,39.934,32.431999999999995 -2020-02-23 10:15:00,75.97,180.67,39.934,32.431999999999995 -2020-02-23 10:30:00,75.82,178.90400000000002,39.934,32.431999999999995 -2020-02-23 10:45:00,76.78,176.976,39.934,32.431999999999995 -2020-02-23 11:00:00,77.47,176.084,43.74100000000001,32.431999999999995 -2020-02-23 11:15:00,80.97,173.822,43.74100000000001,32.431999999999995 -2020-02-23 11:30:00,81.83,172.77700000000002,43.74100000000001,32.431999999999995 -2020-02-23 11:45:00,84.37,171.255,43.74100000000001,32.431999999999995 -2020-02-23 12:00:00,80.33,166.08,40.001999999999995,32.431999999999995 -2020-02-23 12:15:00,75.8,166.668,40.001999999999995,32.431999999999995 -2020-02-23 12:30:00,75.85,165.06900000000002,40.001999999999995,32.431999999999995 -2020-02-23 12:45:00,72.42,164.801,40.001999999999995,32.431999999999995 -2020-02-23 13:00:00,71.1,163.916,37.855,32.431999999999995 -2020-02-23 13:15:00,69.74,164.28099999999998,37.855,32.431999999999995 -2020-02-23 13:30:00,66.49,162.636,37.855,32.431999999999995 -2020-02-23 13:45:00,65.1,162.02700000000002,37.855,32.431999999999995 -2020-02-23 14:00:00,63.85,163.18200000000002,35.946999999999996,32.431999999999995 -2020-02-23 14:15:00,67.63,163.381,35.946999999999996,32.431999999999995 -2020-02-23 14:30:00,65.18,163.741,35.946999999999996,32.431999999999995 -2020-02-23 14:45:00,66.34,164.646,35.946999999999996,32.431999999999995 -2020-02-23 15:00:00,67.38,165.24599999999998,35.138000000000005,32.431999999999995 -2020-02-23 15:15:00,71.0,165.46400000000003,35.138000000000005,32.431999999999995 -2020-02-23 15:30:00,70.06,167.115,35.138000000000005,32.431999999999995 -2020-02-23 15:45:00,71.4,168.856,35.138000000000005,32.431999999999995 -2020-02-23 16:00:00,77.36,169.905,38.672,32.431999999999995 -2020-02-23 16:15:00,75.53,171.645,38.672,32.431999999999995 -2020-02-23 16:30:00,77.71,174.812,38.672,32.431999999999995 -2020-02-23 16:45:00,81.19,176.903,38.672,32.431999999999995 -2020-02-23 17:00:00,88.54,178.53599999999997,48.684,32.431999999999995 -2020-02-23 17:15:00,86.32,181.71900000000002,48.684,32.431999999999995 -2020-02-23 17:30:00,90.78,184.165,48.684,32.431999999999995 -2020-02-23 17:45:00,96.64,186.72099999999998,48.684,32.431999999999995 -2020-02-23 18:00:00,104.83,188.683,51.568999999999996,32.431999999999995 -2020-02-23 18:15:00,107.06,190.919,51.568999999999996,32.431999999999995 -2020-02-23 18:30:00,107.23,189.291,51.568999999999996,32.431999999999995 -2020-02-23 18:45:00,103.85,188.84,51.568999999999996,32.431999999999995 -2020-02-23 19:00:00,101.61,189.18900000000002,48.608000000000004,32.431999999999995 -2020-02-23 19:15:00,102.96,187.34,48.608000000000004,32.431999999999995 -2020-02-23 19:30:00,99.45,186.058,48.608000000000004,32.431999999999995 -2020-02-23 19:45:00,100.59,183.923,48.608000000000004,32.431999999999995 -2020-02-23 20:00:00,105.69,181.30900000000003,43.733999999999995,32.431999999999995 -2020-02-23 20:15:00,97.93,178.87599999999998,43.733999999999995,32.431999999999995 -2020-02-23 20:30:00,93.18,175.926,43.733999999999995,32.431999999999995 -2020-02-23 20:45:00,98.42,173.078,43.733999999999995,32.431999999999995 -2020-02-23 21:00:00,92.64,170.021,39.283,32.431999999999995 -2020-02-23 21:15:00,88.96,167.207,39.283,32.431999999999995 -2020-02-23 21:30:00,90.04,166.68,39.283,32.431999999999995 -2020-02-23 21:45:00,93.24,166.37099999999998,39.283,32.431999999999995 -2020-02-23 22:00:00,98.28,160.47,40.111,32.431999999999995 -2020-02-23 22:15:00,92.33,157.649,40.111,32.431999999999995 -2020-02-23 22:30:00,94.51,152.731,40.111,32.431999999999995 -2020-02-23 22:45:00,91.38,149.721,40.111,32.431999999999995 -2020-02-23 23:00:00,86.18,142.312,35.791,32.431999999999995 -2020-02-23 23:15:00,83.82,139.184,35.791,32.431999999999995 -2020-02-23 23:30:00,84.12,137.974,35.791,32.431999999999995 -2020-02-23 23:45:00,89.48,135.843,35.791,32.431999999999995 -2020-02-24 00:00:00,87.4,119.12100000000001,34.311,32.613 -2020-02-24 00:15:00,85.71,116.48200000000001,34.311,32.613 -2020-02-24 00:30:00,79.25,117.37,34.311,32.613 -2020-02-24 00:45:00,80.11,118.845,34.311,32.613 -2020-02-24 01:00:00,75.53,120.87299999999999,34.585,32.613 -2020-02-24 01:15:00,82.25,121.47399999999999,34.585,32.613 -2020-02-24 01:30:00,82.39,121.463,34.585,32.613 -2020-02-24 01:45:00,83.21,121.355,34.585,32.613 -2020-02-24 02:00:00,78.47,123.35799999999999,34.111,32.613 -2020-02-24 02:15:00,81.62,124.824,34.111,32.613 -2020-02-24 02:30:00,83.49,126.336,34.111,32.613 -2020-02-24 02:45:00,85.86,128.109,34.111,32.613 -2020-02-24 03:00:00,77.68,131.778,32.435,32.613 -2020-02-24 03:15:00,84.84,134.17,32.435,32.613 -2020-02-24 03:30:00,85.17,135.471,32.435,32.613 -2020-02-24 03:45:00,85.91,136.755,32.435,32.613 -2020-02-24 04:00:00,83.63,148.685,33.04,32.613 -2020-02-24 04:15:00,83.89,160.695,33.04,32.613 -2020-02-24 04:30:00,89.11,163.607,33.04,32.613 -2020-02-24 04:45:00,86.29,164.52900000000002,33.04,32.613 -2020-02-24 05:00:00,90.81,192.02599999999998,40.399,32.613 -2020-02-24 05:15:00,91.34,220.23,40.399,32.613 -2020-02-24 05:30:00,95.51,216.81900000000002,40.399,32.613 -2020-02-24 05:45:00,100.68,209.787,40.399,32.613 -2020-02-24 06:00:00,112.19,208.31799999999998,60.226000000000006,32.613 -2020-02-24 06:15:00,114.53,212.699,60.226000000000006,32.613 -2020-02-24 06:30:00,121.55,215.514,60.226000000000006,32.613 -2020-02-24 06:45:00,120.58,219.74099999999999,60.226000000000006,32.613 -2020-02-24 07:00:00,127.09,220.673,73.578,32.613 -2020-02-24 07:15:00,128.48,224.535,73.578,32.613 -2020-02-24 07:30:00,128.83,226.213,73.578,32.613 -2020-02-24 07:45:00,128.43,225.47400000000002,73.578,32.613 -2020-02-24 08:00:00,132.65,223.62099999999998,66.58,32.613 -2020-02-24 08:15:00,130.35,223.472,66.58,32.613 -2020-02-24 08:30:00,132.82,219.62400000000002,66.58,32.613 -2020-02-24 08:45:00,128.9,216.138,66.58,32.613 -2020-02-24 09:00:00,130.95,209.83700000000002,62.0,32.613 -2020-02-24 09:15:00,129.17,205.146,62.0,32.613 -2020-02-24 09:30:00,128.34,203.085,62.0,32.613 -2020-02-24 09:45:00,127.77,200.359,62.0,32.613 -2020-02-24 10:00:00,125.53,196.74900000000002,59.099,32.613 -2020-02-24 10:15:00,127.37,194.266,59.099,32.613 -2020-02-24 10:30:00,123.36,191.65599999999998,59.099,32.613 -2020-02-24 10:45:00,127.17,190.43200000000002,59.099,32.613 -2020-02-24 11:00:00,122.95,187.0,57.729,32.613 -2020-02-24 11:15:00,126.73,186.50900000000001,57.729,32.613 -2020-02-24 11:30:00,127.56,186.827,57.729,32.613 -2020-02-24 11:45:00,123.32,184.93599999999998,57.729,32.613 -2020-02-24 12:00:00,122.5,181.40099999999998,55.615,32.613 -2020-02-24 12:15:00,126.78,182.00599999999997,55.615,32.613 -2020-02-24 12:30:00,127.67,180.593,55.615,32.613 -2020-02-24 12:45:00,124.86,181.798,55.615,32.613 -2020-02-24 13:00:00,122.88,181.528,56.515,32.613 -2020-02-24 13:15:00,123.82,180.50599999999997,56.515,32.613 -2020-02-24 13:30:00,125.65,178.34,56.515,32.613 -2020-02-24 13:45:00,128.04,177.78599999999997,56.515,32.613 -2020-02-24 14:00:00,130.27,178.38099999999997,58.1,32.613 -2020-02-24 14:15:00,128.28,177.951,58.1,32.613 -2020-02-24 14:30:00,131.05,177.762,58.1,32.613 -2020-02-24 14:45:00,130.12,178.701,58.1,32.613 -2020-02-24 15:00:00,130.46,181.032,59.801,32.613 -2020-02-24 15:15:00,131.88,179.815,59.801,32.613 -2020-02-24 15:30:00,129.22,180.639,59.801,32.613 -2020-02-24 15:45:00,129.42,181.919,59.801,32.613 -2020-02-24 16:00:00,129.55,183.18599999999998,62.901,32.613 -2020-02-24 16:15:00,129.72,184.172,62.901,32.613 -2020-02-24 16:30:00,127.89,186.385,62.901,32.613 -2020-02-24 16:45:00,129.96,187.296,62.901,32.613 -2020-02-24 17:00:00,133.58,188.75,70.418,32.613 -2020-02-24 17:15:00,136.65,191.028,70.418,32.613 -2020-02-24 17:30:00,139.14,192.96,70.418,32.613 -2020-02-24 17:45:00,138.19,194.074,70.418,32.613 -2020-02-24 18:00:00,144.78,196.45,71.726,32.613 -2020-02-24 18:15:00,140.22,196.56,71.726,32.613 -2020-02-24 18:30:00,140.06,195.55599999999998,71.726,32.613 -2020-02-24 18:45:00,141.43,195.878,71.726,32.613 -2020-02-24 19:00:00,139.29,194.61599999999999,65.997,32.613 -2020-02-24 19:15:00,138.24,191.66,65.997,32.613 -2020-02-24 19:30:00,144.32,190.885,65.997,32.613 -2020-02-24 19:45:00,141.12,187.96599999999998,65.997,32.613 -2020-02-24 20:00:00,133.01,182.99099999999999,68.09100000000001,32.613 -2020-02-24 20:15:00,125.91,178.175,68.09100000000001,32.613 -2020-02-24 20:30:00,120.23,173.433,68.09100000000001,32.613 -2020-02-24 20:45:00,121.43,172.179,68.09100000000001,32.613 -2020-02-24 21:00:00,114.0,169.61900000000003,59.617,32.613 -2020-02-24 21:15:00,117.58,165.65599999999998,59.617,32.613 -2020-02-24 21:30:00,117.76,164.327,59.617,32.613 -2020-02-24 21:45:00,115.93,163.54,59.617,32.613 -2020-02-24 22:00:00,105.83,154.77,54.938,32.613 -2020-02-24 22:15:00,101.05,150.715,54.938,32.613 -2020-02-24 22:30:00,99.67,136.342,54.938,32.613 -2020-02-24 22:45:00,98.93,128.609,54.938,32.613 -2020-02-24 23:00:00,95.34,121.956,47.43,32.613 -2020-02-24 23:15:00,94.94,121.412,47.43,32.613 -2020-02-24 23:30:00,89.82,122.914,47.43,32.613 -2020-02-24 23:45:00,89.72,123.382,47.43,32.613 -2020-02-25 00:00:00,92.74,118.161,48.354,32.613 -2020-02-25 00:15:00,93.6,116.93299999999999,48.354,32.613 -2020-02-25 00:30:00,94.11,116.89200000000001,48.354,32.613 -2020-02-25 00:45:00,89.59,117.417,48.354,32.613 -2020-02-25 01:00:00,84.84,119.20700000000001,45.68600000000001,32.613 -2020-02-25 01:15:00,86.12,119.359,45.68600000000001,32.613 -2020-02-25 01:30:00,82.8,119.501,45.68600000000001,32.613 -2020-02-25 01:45:00,83.08,119.69,45.68600000000001,32.613 -2020-02-25 02:00:00,84.51,121.675,44.269,32.613 -2020-02-25 02:15:00,87.23,123.042,44.269,32.613 -2020-02-25 02:30:00,89.0,123.98200000000001,44.269,32.613 -2020-02-25 02:45:00,93.75,125.771,44.269,32.613 -2020-02-25 03:00:00,88.41,128.25799999999998,44.187,32.613 -2020-02-25 03:15:00,89.23,129.819,44.187,32.613 -2020-02-25 03:30:00,85.06,131.588,44.187,32.613 -2020-02-25 03:45:00,89.04,133.059,44.187,32.613 -2020-02-25 04:00:00,86.27,144.786,46.126999999999995,32.613 -2020-02-25 04:15:00,86.74,156.446,46.126999999999995,32.613 -2020-02-25 04:30:00,88.33,159.06799999999998,46.126999999999995,32.613 -2020-02-25 04:45:00,91.62,161.172,46.126999999999995,32.613 -2020-02-25 05:00:00,97.3,193.585,49.666000000000004,32.613 -2020-02-25 05:15:00,96.94,221.614,49.666000000000004,32.613 -2020-02-25 05:30:00,101.12,216.666,49.666000000000004,32.613 -2020-02-25 05:45:00,105.3,209.638,49.666000000000004,32.613 -2020-02-25 06:00:00,116.13,207.021,61.077,32.613 -2020-02-25 06:15:00,118.32,213.012,61.077,32.613 -2020-02-25 06:30:00,121.9,215.149,61.077,32.613 -2020-02-25 06:45:00,123.41,218.986,61.077,32.613 -2020-02-25 07:00:00,130.25,219.761,74.717,32.613 -2020-02-25 07:15:00,129.83,223.43400000000003,74.717,32.613 -2020-02-25 07:30:00,131.97,224.53900000000002,74.717,32.613 -2020-02-25 07:45:00,129.74,223.96,74.717,32.613 -2020-02-25 08:00:00,132.69,222.18900000000002,69.033,32.613 -2020-02-25 08:15:00,133.52,221.015,69.033,32.613 -2020-02-25 08:30:00,129.26,216.93599999999998,69.033,32.613 -2020-02-25 08:45:00,128.0,213.18900000000002,69.033,32.613 -2020-02-25 09:00:00,134.63,206.084,63.113,32.613 -2020-02-25 09:15:00,134.74,202.96200000000002,63.113,32.613 -2020-02-25 09:30:00,137.02,201.59400000000002,63.113,32.613 -2020-02-25 09:45:00,135.12,198.675,63.113,32.613 -2020-02-25 10:00:00,127.82,194.487,61.461999999999996,32.613 -2020-02-25 10:15:00,127.61,190.99599999999998,61.461999999999996,32.613 -2020-02-25 10:30:00,124.56,188.578,61.461999999999996,32.613 -2020-02-25 10:45:00,124.47,187.65900000000002,61.461999999999996,32.613 -2020-02-25 11:00:00,120.98,185.67700000000002,59.614,32.613 -2020-02-25 11:15:00,121.56,184.88099999999997,59.614,32.613 -2020-02-25 11:30:00,122.41,184.05200000000002,59.614,32.613 -2020-02-25 11:45:00,121.67,182.856,59.614,32.613 -2020-02-25 12:00:00,121.07,177.997,57.415,32.613 -2020-02-25 12:15:00,120.61,178.21200000000002,57.415,32.613 -2020-02-25 12:30:00,117.95,177.486,57.415,32.613 -2020-02-25 12:45:00,118.6,178.41099999999997,57.415,32.613 -2020-02-25 13:00:00,116.09,177.77200000000002,58.534,32.613 -2020-02-25 13:15:00,116.32,176.41299999999998,58.534,32.613 -2020-02-25 13:30:00,118.93,175.377,58.534,32.613 -2020-02-25 13:45:00,116.93,174.995,58.534,32.613 -2020-02-25 14:00:00,119.27,175.935,59.415,32.613 -2020-02-25 14:15:00,117.11,175.63400000000001,59.415,32.613 -2020-02-25 14:30:00,118.09,176.03900000000002,59.415,32.613 -2020-02-25 14:45:00,119.19,176.899,59.415,32.613 -2020-02-25 15:00:00,121.2,178.81400000000002,62.071999999999996,32.613 -2020-02-25 15:15:00,119.3,177.905,62.071999999999996,32.613 -2020-02-25 15:30:00,119.76,178.90200000000002,62.071999999999996,32.613 -2020-02-25 15:45:00,121.39,179.77700000000002,62.071999999999996,32.613 -2020-02-25 16:00:00,123.07,181.363,64.99,32.613 -2020-02-25 16:15:00,125.94,182.801,64.99,32.613 -2020-02-25 16:30:00,123.48,185.644,64.99,32.613 -2020-02-25 16:45:00,125.86,186.815,64.99,32.613 -2020-02-25 17:00:00,133.26,188.80900000000003,72.658,32.613 -2020-02-25 17:15:00,133.66,191.135,72.658,32.613 -2020-02-25 17:30:00,136.58,193.74400000000003,72.658,32.613 -2020-02-25 17:45:00,134.98,194.747,72.658,32.613 -2020-02-25 18:00:00,142.9,197.021,73.645,32.613 -2020-02-25 18:15:00,140.35,196.71400000000003,73.645,32.613 -2020-02-25 18:30:00,140.6,195.40400000000002,73.645,32.613 -2020-02-25 18:45:00,140.89,196.53,73.645,32.613 -2020-02-25 19:00:00,138.89,195.298,67.085,32.613 -2020-02-25 19:15:00,137.3,192.084,67.085,32.613 -2020-02-25 19:30:00,143.66,190.675,67.085,32.613 -2020-02-25 19:45:00,146.82,187.835,67.085,32.613 -2020-02-25 20:00:00,137.15,182.99099999999999,66.138,32.613 -2020-02-25 20:15:00,124.2,177.548,66.138,32.613 -2020-02-25 20:30:00,125.13,173.822,66.138,32.613 -2020-02-25 20:45:00,122.61,172.012,66.138,32.613 -2020-02-25 21:00:00,114.79,168.75900000000001,57.512,32.613 -2020-02-25 21:15:00,117.9,165.666,57.512,32.613 -2020-02-25 21:30:00,118.78,163.622,57.512,32.613 -2020-02-25 21:45:00,115.59,163.079,57.512,32.613 -2020-02-25 22:00:00,105.29,155.977,54.545,32.613 -2020-02-25 22:15:00,105.73,151.689,54.545,32.613 -2020-02-25 22:30:00,100.32,137.36,54.545,32.613 -2020-02-25 22:45:00,103.78,129.903,54.545,32.613 -2020-02-25 23:00:00,103.37,123.266,48.605,32.613 -2020-02-25 23:15:00,102.21,121.863,48.605,32.613 -2020-02-25 23:30:00,94.38,123.02,48.605,32.613 -2020-02-25 23:45:00,93.16,123.08200000000001,48.605,32.613 -2020-02-26 00:00:00,84.21,117.904,45.675,32.613 -2020-02-26 00:15:00,85.32,116.678,45.675,32.613 -2020-02-26 00:30:00,85.53,116.619,45.675,32.613 -2020-02-26 00:45:00,85.09,117.14299999999999,45.675,32.613 -2020-02-26 01:00:00,86.21,118.89200000000001,43.015,32.613 -2020-02-26 01:15:00,91.24,119.031,43.015,32.613 -2020-02-26 01:30:00,91.43,119.15899999999999,43.015,32.613 -2020-02-26 01:45:00,91.86,119.352,43.015,32.613 -2020-02-26 02:00:00,88.93,121.329,41.0,32.613 -2020-02-26 02:15:00,90.95,122.693,41.0,32.613 -2020-02-26 02:30:00,91.59,123.649,41.0,32.613 -2020-02-26 02:45:00,90.28,125.43700000000001,41.0,32.613 -2020-02-26 03:00:00,90.74,127.93299999999999,41.318000000000005,32.613 -2020-02-26 03:15:00,95.62,129.485,41.318000000000005,32.613 -2020-02-26 03:30:00,92.99,131.248,41.318000000000005,32.613 -2020-02-26 03:45:00,89.88,132.731,41.318000000000005,32.613 -2020-02-26 04:00:00,92.31,144.463,42.544,32.613 -2020-02-26 04:15:00,89.03,156.114,42.544,32.613 -2020-02-26 04:30:00,88.74,158.754,42.544,32.613 -2020-02-26 04:45:00,89.87,160.845,42.544,32.613 -2020-02-26 05:00:00,94.12,193.24400000000003,45.161,32.613 -2020-02-26 05:15:00,95.66,221.297,45.161,32.613 -2020-02-26 05:30:00,99.71,216.31599999999997,45.161,32.613 -2020-02-26 05:45:00,104.02,209.291,45.161,32.613 -2020-02-26 06:00:00,116.54,206.68099999999998,61.86600000000001,32.613 -2020-02-26 06:15:00,119.33,212.68099999999998,61.86600000000001,32.613 -2020-02-26 06:30:00,124.91,214.77900000000002,61.86600000000001,32.613 -2020-02-26 06:45:00,124.25,218.601,61.86600000000001,32.613 -2020-02-26 07:00:00,132.05,219.398,77.814,32.613 -2020-02-26 07:15:00,131.93,223.041,77.814,32.613 -2020-02-26 07:30:00,130.23,224.108,77.814,32.613 -2020-02-26 07:45:00,131.9,223.488,77.814,32.613 -2020-02-26 08:00:00,135.76,221.69299999999998,70.251,32.613 -2020-02-26 08:15:00,134.68,220.50099999999998,70.251,32.613 -2020-02-26 08:30:00,134.89,216.365,70.251,32.613 -2020-02-26 08:45:00,132.39,212.62900000000002,70.251,32.613 -2020-02-26 09:00:00,132.02,205.533,66.965,32.613 -2020-02-26 09:15:00,137.94,202.412,66.965,32.613 -2020-02-26 09:30:00,138.13,201.06400000000002,66.965,32.613 -2020-02-26 09:45:00,140.89,198.15200000000002,66.965,32.613 -2020-02-26 10:00:00,135.54,193.976,63.628,32.613 -2020-02-26 10:15:00,133.05,190.52200000000002,63.628,32.613 -2020-02-26 10:30:00,129.5,188.12099999999998,63.628,32.613 -2020-02-26 10:45:00,130.84,187.217,63.628,32.613 -2020-02-26 11:00:00,122.72,185.227,62.516999999999996,32.613 -2020-02-26 11:15:00,122.07,184.449,62.516999999999996,32.613 -2020-02-26 11:30:00,120.82,183.62599999999998,62.516999999999996,32.613 -2020-02-26 11:45:00,120.99,182.445,62.516999999999996,32.613 -2020-02-26 12:00:00,121.64,177.602,60.888999999999996,32.613 -2020-02-26 12:15:00,119.41,177.832,60.888999999999996,32.613 -2020-02-26 12:30:00,119.83,177.074,60.888999999999996,32.613 -2020-02-26 12:45:00,121.37,177.99599999999998,60.888999999999996,32.613 -2020-02-26 13:00:00,118.3,177.392,61.57899999999999,32.613 -2020-02-26 13:15:00,121.25,176.01,61.57899999999999,32.613 -2020-02-26 13:30:00,119.3,174.96,61.57899999999999,32.613 -2020-02-26 13:45:00,117.57,174.58,61.57899999999999,32.613 -2020-02-26 14:00:00,120.42,175.582,62.602,32.613 -2020-02-26 14:15:00,120.08,175.25900000000001,62.602,32.613 -2020-02-26 14:30:00,127.13,175.64,62.602,32.613 -2020-02-26 14:45:00,123.18,176.512,62.602,32.613 -2020-02-26 15:00:00,124.28,178.433,64.259,32.613 -2020-02-26 15:15:00,121.09,177.495,64.259,32.613 -2020-02-26 15:30:00,121.21,178.44799999999998,64.259,32.613 -2020-02-26 15:45:00,124.4,179.30599999999998,64.259,32.613 -2020-02-26 16:00:00,123.44,180.894,67.632,32.613 -2020-02-26 16:15:00,128.2,182.315,67.632,32.613 -2020-02-26 16:30:00,126.24,185.15900000000002,67.632,32.613 -2020-02-26 16:45:00,129.82,186.3,67.632,32.613 -2020-02-26 17:00:00,132.44,188.3,72.583,32.613 -2020-02-26 17:15:00,132.18,190.64,72.583,32.613 -2020-02-26 17:30:00,138.18,193.27700000000002,72.583,32.613 -2020-02-26 17:45:00,137.44,194.299,72.583,32.613 -2020-02-26 18:00:00,144.08,196.584,72.744,32.613 -2020-02-26 18:15:00,142.07,196.34,72.744,32.613 -2020-02-26 18:30:00,143.05,195.028,72.744,32.613 -2020-02-26 18:45:00,143.25,196.175,72.744,32.613 -2020-02-26 19:00:00,142.61,194.905,69.684,32.613 -2020-02-26 19:15:00,143.22,191.708,69.684,32.613 -2020-02-26 19:30:00,148.53,190.325,69.684,32.613 -2020-02-26 19:45:00,145.96,187.525,69.684,32.613 -2020-02-26 20:00:00,134.53,182.65200000000002,70.036,32.613 -2020-02-26 20:15:00,128.04,177.22299999999998,70.036,32.613 -2020-02-26 20:30:00,123.73,173.518,70.036,32.613 -2020-02-26 20:45:00,122.17,171.71,70.036,32.613 -2020-02-26 21:00:00,116.79,168.44400000000002,60.431999999999995,32.613 -2020-02-26 21:15:00,121.15,165.343,60.431999999999995,32.613 -2020-02-26 21:30:00,120.23,163.3,60.431999999999995,32.613 -2020-02-26 21:45:00,113.68,162.774,60.431999999999995,32.613 -2020-02-26 22:00:00,107.74,155.657,56.2,32.613 -2020-02-26 22:15:00,104.57,151.393,56.2,32.613 -2020-02-26 22:30:00,101.62,137.017,56.2,32.613 -2020-02-26 22:45:00,102.65,129.564,56.2,32.613 -2020-02-26 23:00:00,96.31,122.925,47.927,32.613 -2020-02-26 23:15:00,102.68,121.538,47.927,32.613 -2020-02-26 23:30:00,99.27,122.704,47.927,32.613 -2020-02-26 23:45:00,95.89,122.791,47.927,32.613 -2020-02-27 00:00:00,88.18,117.63799999999999,43.794,32.613 -2020-02-27 00:15:00,88.97,116.417,43.794,32.613 -2020-02-27 00:30:00,90.67,116.34,43.794,32.613 -2020-02-27 00:45:00,87.97,116.863,43.794,32.613 -2020-02-27 01:00:00,93.08,118.571,42.397,32.613 -2020-02-27 01:15:00,91.83,118.697,42.397,32.613 -2020-02-27 01:30:00,90.3,118.811,42.397,32.613 -2020-02-27 01:45:00,86.08,119.008,42.397,32.613 -2020-02-27 02:00:00,85.82,120.977,40.010999999999996,32.613 -2020-02-27 02:15:00,93.94,122.337,40.010999999999996,32.613 -2020-02-27 02:30:00,91.53,123.309,40.010999999999996,32.613 -2020-02-27 02:45:00,92.99,125.09700000000001,40.010999999999996,32.613 -2020-02-27 03:00:00,90.02,127.602,39.181,32.613 -2020-02-27 03:15:00,93.1,129.143,39.181,32.613 -2020-02-27 03:30:00,93.75,130.901,39.181,32.613 -2020-02-27 03:45:00,90.45,132.39600000000002,39.181,32.613 -2020-02-27 04:00:00,93.08,144.13299999999998,40.39,32.613 -2020-02-27 04:15:00,98.7,155.77700000000002,40.39,32.613 -2020-02-27 04:30:00,96.19,158.435,40.39,32.613 -2020-02-27 04:45:00,92.23,160.512,40.39,32.613 -2020-02-27 05:00:00,95.1,192.89700000000002,45.504,32.613 -2020-02-27 05:15:00,97.21,220.97400000000002,45.504,32.613 -2020-02-27 05:30:00,102.16,215.96099999999998,45.504,32.613 -2020-02-27 05:45:00,104.54,208.938,45.504,32.613 -2020-02-27 06:00:00,114.7,206.334,57.748000000000005,32.613 -2020-02-27 06:15:00,118.52,212.34400000000002,57.748000000000005,32.613 -2020-02-27 06:30:00,121.44,214.40099999999998,57.748000000000005,32.613 -2020-02-27 06:45:00,123.95,218.209,57.748000000000005,32.613 -2020-02-27 07:00:00,127.56,219.028,72.138,32.613 -2020-02-27 07:15:00,129.28,222.639,72.138,32.613 -2020-02-27 07:30:00,129.58,223.67,72.138,32.613 -2020-02-27 07:45:00,132.15,223.00799999999998,72.138,32.613 -2020-02-27 08:00:00,134.97,221.19,65.542,32.613 -2020-02-27 08:15:00,134.2,219.979,65.542,32.613 -2020-02-27 08:30:00,131.35,215.787,65.542,32.613 -2020-02-27 08:45:00,126.9,212.06,65.542,32.613 -2020-02-27 09:00:00,124.52,204.975,60.523,32.613 -2020-02-27 09:15:00,124.46,201.856,60.523,32.613 -2020-02-27 09:30:00,126.69,200.528,60.523,32.613 -2020-02-27 09:45:00,127.92,197.623,60.523,32.613 -2020-02-27 10:00:00,125.86,193.459,57.449,32.613 -2020-02-27 10:15:00,123.44,190.041,57.449,32.613 -2020-02-27 10:30:00,125.61,187.658,57.449,32.613 -2020-02-27 10:45:00,126.43,186.771,57.449,32.613 -2020-02-27 11:00:00,122.4,184.77200000000002,54.505,32.613 -2020-02-27 11:15:00,127.71,184.00900000000001,54.505,32.613 -2020-02-27 11:30:00,120.16,183.195,54.505,32.613 -2020-02-27 11:45:00,118.8,182.02900000000002,54.505,32.613 -2020-02-27 12:00:00,117.29,177.203,51.50899999999999,32.613 -2020-02-27 12:15:00,116.79,177.446,51.50899999999999,32.613 -2020-02-27 12:30:00,112.3,176.657,51.50899999999999,32.613 -2020-02-27 12:45:00,115.74,177.57299999999998,51.50899999999999,32.613 -2020-02-27 13:00:00,112.3,177.00599999999997,51.303999999999995,32.613 -2020-02-27 13:15:00,119.69,175.601,51.303999999999995,32.613 -2020-02-27 13:30:00,117.02,174.53799999999998,51.303999999999995,32.613 -2020-02-27 13:45:00,114.97,174.16,51.303999999999995,32.613 -2020-02-27 14:00:00,115.47,175.22400000000002,52.785,32.613 -2020-02-27 14:15:00,115.4,174.87900000000002,52.785,32.613 -2020-02-27 14:30:00,119.74,175.236,52.785,32.613 -2020-02-27 14:45:00,121.19,176.11900000000003,52.785,32.613 -2020-02-27 15:00:00,123.14,178.046,56.458999999999996,32.613 -2020-02-27 15:15:00,116.9,177.079,56.458999999999996,32.613 -2020-02-27 15:30:00,124.45,177.988,56.458999999999996,32.613 -2020-02-27 15:45:00,126.19,178.828,56.458999999999996,32.613 -2020-02-27 16:00:00,128.02,180.417,59.388000000000005,32.613 -2020-02-27 16:15:00,126.16,181.822,59.388000000000005,32.613 -2020-02-27 16:30:00,128.55,184.666,59.388000000000005,32.613 -2020-02-27 16:45:00,130.21,185.77599999999998,59.388000000000005,32.613 -2020-02-27 17:00:00,134.52,187.782,64.462,32.613 -2020-02-27 17:15:00,134.4,190.137,64.462,32.613 -2020-02-27 17:30:00,136.14,192.80200000000002,64.462,32.613 -2020-02-27 17:45:00,140.64,193.84400000000002,64.462,32.613 -2020-02-27 18:00:00,148.99,196.137,65.128,32.613 -2020-02-27 18:15:00,149.31,195.958,65.128,32.613 -2020-02-27 18:30:00,144.63,194.645,65.128,32.613 -2020-02-27 18:45:00,147.88,195.813,65.128,32.613 -2020-02-27 19:00:00,146.09,194.50400000000002,61.316,32.613 -2020-02-27 19:15:00,145.64,191.324,61.316,32.613 -2020-02-27 19:30:00,135.54,189.967,61.316,32.613 -2020-02-27 19:45:00,136.53,187.209,61.316,32.613 -2020-02-27 20:00:00,133.14,182.305,59.845,32.613 -2020-02-27 20:15:00,130.96,176.892,59.845,32.613 -2020-02-27 20:30:00,127.67,173.207,59.845,32.613 -2020-02-27 20:45:00,123.1,171.40099999999998,59.845,32.613 -2020-02-27 21:00:00,118.51,168.123,54.83,32.613 -2020-02-27 21:15:00,116.64,165.014,54.83,32.613 -2020-02-27 21:30:00,116.78,162.971,54.83,32.613 -2020-02-27 21:45:00,116.76,162.464,54.83,32.613 -2020-02-27 22:00:00,106.08,155.33,50.933,32.613 -2020-02-27 22:15:00,104.2,151.092,50.933,32.613 -2020-02-27 22:30:00,106.52,136.667,50.933,32.613 -2020-02-27 22:45:00,108.8,129.218,50.933,32.613 -2020-02-27 23:00:00,101.23,122.57600000000001,45.32899999999999,32.613 -2020-02-27 23:15:00,100.86,121.205,45.32899999999999,32.613 -2020-02-27 23:30:00,97.66,122.382,45.32899999999999,32.613 -2020-02-27 23:45:00,99.21,122.494,45.32899999999999,32.613 -2020-02-28 00:00:00,95.24,116.29,43.74,32.613 -2020-02-28 00:15:00,90.0,115.273,43.74,32.613 -2020-02-28 00:30:00,93.54,115.04799999999999,43.74,32.613 -2020-02-28 00:45:00,93.7,115.684,43.74,32.613 -2020-02-28 01:00:00,86.92,117.04,42.555,32.613 -2020-02-28 01:15:00,89.35,118.04700000000001,42.555,32.613 -2020-02-28 01:30:00,90.93,117.935,42.555,32.613 -2020-02-28 01:45:00,91.4,118.236,42.555,32.613 -2020-02-28 02:00:00,88.43,120.294,41.68600000000001,32.613 -2020-02-28 02:15:00,90.06,121.537,41.68600000000001,32.613 -2020-02-28 02:30:00,89.98,123.05,41.68600000000001,32.613 -2020-02-28 02:45:00,91.98,124.87200000000001,41.68600000000001,32.613 -2020-02-28 03:00:00,85.75,126.415,42.278999999999996,32.613 -2020-02-28 03:15:00,90.33,128.856,42.278999999999996,32.613 -2020-02-28 03:30:00,91.82,130.59,42.278999999999996,32.613 -2020-02-28 03:45:00,92.28,132.428,42.278999999999996,32.613 -2020-02-28 04:00:00,93.53,144.399,43.742,32.613 -2020-02-28 04:15:00,94.58,155.774,43.742,32.613 -2020-02-28 04:30:00,96.07,158.686,43.742,32.613 -2020-02-28 04:45:00,88.54,159.619,43.742,32.613 -2020-02-28 05:00:00,92.18,190.735,46.973,32.613 -2020-02-28 05:15:00,94.1,220.334,46.973,32.613 -2020-02-28 05:30:00,99.17,216.352,46.973,32.613 -2020-02-28 05:45:00,101.77,209.26,46.973,32.613 -2020-02-28 06:00:00,114.52,207.092,59.63399999999999,32.613 -2020-02-28 06:15:00,116.78,211.665,59.63399999999999,32.613 -2020-02-28 06:30:00,121.82,212.828,59.63399999999999,32.613 -2020-02-28 06:45:00,121.41,218.22,59.63399999999999,32.613 -2020-02-28 07:00:00,126.51,218.248,71.631,32.613 -2020-02-28 07:15:00,130.43,222.845,71.631,32.613 -2020-02-28 07:30:00,132.73,223.635,71.631,32.613 -2020-02-28 07:45:00,131.7,222.05599999999998,71.631,32.613 -2020-02-28 08:00:00,137.02,219.141,66.181,32.613 -2020-02-28 08:15:00,131.96,217.541,66.181,32.613 -2020-02-28 08:30:00,136.24,214.248,66.181,32.613 -2020-02-28 08:45:00,130.35,208.982,66.181,32.613 -2020-02-28 09:00:00,130.85,202.32,63.086000000000006,32.613 -2020-02-28 09:15:00,128.0,199.795,63.086000000000006,32.613 -2020-02-28 09:30:00,131.49,198.054,63.086000000000006,32.613 -2020-02-28 09:45:00,128.77,195.05700000000002,63.086000000000006,32.613 -2020-02-28 10:00:00,128.05,189.793,60.886,32.613 -2020-02-28 10:15:00,129.03,187.09,60.886,32.613 -2020-02-28 10:30:00,125.57,184.666,60.886,32.613 -2020-02-28 10:45:00,132.11,183.361,60.886,32.613 -2020-02-28 11:00:00,130.54,181.342,59.391000000000005,32.613 -2020-02-28 11:15:00,127.61,179.697,59.391000000000005,32.613 -2020-02-28 11:30:00,127.14,180.593,59.391000000000005,32.613 -2020-02-28 11:45:00,124.9,179.465,59.391000000000005,32.613 -2020-02-28 12:00:00,122.01,175.725,56.172,32.613 -2020-02-28 12:15:00,126.66,173.922,56.172,32.613 -2020-02-28 12:30:00,120.31,173.257,56.172,32.613 -2020-02-28 12:45:00,122.17,174.65400000000002,56.172,32.613 -2020-02-28 13:00:00,118.8,175.051,54.406000000000006,32.613 -2020-02-28 13:15:00,122.05,174.425,54.406000000000006,32.613 -2020-02-28 13:30:00,121.88,173.385,54.406000000000006,32.613 -2020-02-28 13:45:00,125.01,172.955,54.406000000000006,32.613 -2020-02-28 14:00:00,121.64,172.949,53.578,32.613 -2020-02-28 14:15:00,117.04,172.418,53.578,32.613 -2020-02-28 14:30:00,115.28,173.28900000000002,53.578,32.613 -2020-02-28 14:45:00,116.76,174.46099999999998,53.578,32.613 -2020-02-28 15:00:00,112.41,175.921,56.568999999999996,32.613 -2020-02-28 15:15:00,116.08,174.49099999999999,56.568999999999996,32.613 -2020-02-28 15:30:00,121.56,173.861,56.568999999999996,32.613 -2020-02-28 15:45:00,123.51,174.84599999999998,56.568999999999996,32.613 -2020-02-28 16:00:00,121.03,175.273,60.169,32.613 -2020-02-28 16:15:00,119.77,176.956,60.169,32.613 -2020-02-28 16:30:00,124.79,179.888,60.169,32.613 -2020-02-28 16:45:00,130.53,180.83700000000002,60.169,32.613 -2020-02-28 17:00:00,134.83,183.079,65.497,32.613 -2020-02-28 17:15:00,137.27,185.04,65.497,32.613 -2020-02-28 17:30:00,137.09,187.424,65.497,32.613 -2020-02-28 17:45:00,141.86,188.253,65.497,32.613 -2020-02-28 18:00:00,148.36,191.23,65.082,32.613 -2020-02-28 18:15:00,143.47,190.71599999999998,65.082,32.613 -2020-02-28 18:30:00,142.41,189.78599999999997,65.082,32.613 -2020-02-28 18:45:00,144.94,190.97799999999998,65.082,32.613 -2020-02-28 19:00:00,139.83,190.535,60.968,32.613 -2020-02-28 19:15:00,136.32,188.72099999999998,60.968,32.613 -2020-02-28 19:30:00,138.61,186.987,60.968,32.613 -2020-02-28 19:45:00,138.84,183.80200000000002,60.968,32.613 -2020-02-28 20:00:00,130.09,178.919,61.123000000000005,32.613 -2020-02-28 20:15:00,127.16,173.543,61.123000000000005,32.613 -2020-02-28 20:30:00,122.02,169.83,61.123000000000005,32.613 -2020-02-28 20:45:00,118.31,168.558,61.123000000000005,32.613 -2020-02-28 21:00:00,117.05,165.787,55.416000000000004,32.613 -2020-02-28 21:15:00,114.42,163.128,55.416000000000004,32.613 -2020-02-28 21:30:00,108.61,161.128,55.416000000000004,32.613 -2020-02-28 21:45:00,103.76,161.17600000000002,55.416000000000004,32.613 -2020-02-28 22:00:00,96.68,154.987,51.631,32.613 -2020-02-28 22:15:00,95.08,150.64,51.631,32.613 -2020-02-28 22:30:00,100.47,142.507,51.631,32.613 -2020-02-28 22:45:00,100.52,138.542,51.631,32.613 -2020-02-28 23:00:00,93.73,131.487,44.898,32.613 -2020-02-28 23:15:00,87.45,128.192,44.898,32.613 -2020-02-28 23:30:00,82.28,127.915,44.898,32.613 -2020-02-28 23:45:00,85.88,127.398,44.898,32.613 -2020-02-29 00:00:00,82.24,113.23700000000001,42.033,32.431999999999995 -2020-02-29 00:15:00,85.01,108.041,42.033,32.431999999999995 -2020-02-29 00:30:00,80.52,109.09299999999999,42.033,32.431999999999995 -2020-02-29 00:45:00,80.2,110.416,42.033,32.431999999999995 -2020-02-29 01:00:00,76.23,112.354,38.255,32.431999999999995 -2020-02-29 01:15:00,75.84,112.39200000000001,38.255,32.431999999999995 -2020-02-29 01:30:00,74.66,111.743,38.255,32.431999999999995 -2020-02-29 01:45:00,74.31,111.86,38.255,32.431999999999995 -2020-02-29 02:00:00,77.97,114.565,36.404,32.431999999999995 -2020-02-29 02:15:00,79.13,115.428,36.404,32.431999999999995 -2020-02-29 02:30:00,76.43,115.85600000000001,36.404,32.431999999999995 -2020-02-29 02:45:00,74.31,117.789,36.404,32.431999999999995 -2020-02-29 03:00:00,72.93,119.935,36.083,32.431999999999995 -2020-02-29 03:15:00,71.88,121.161,36.083,32.431999999999995 -2020-02-29 03:30:00,71.31,121.338,36.083,32.431999999999995 -2020-02-29 03:45:00,71.98,123.323,36.083,32.431999999999995 -2020-02-29 04:00:00,72.01,131.215,36.102,32.431999999999995 -2020-02-29 04:15:00,72.24,140.079,36.102,32.431999999999995 -2020-02-29 04:30:00,72.51,140.856,36.102,32.431999999999995 -2020-02-29 04:45:00,73.77,141.296,36.102,32.431999999999995 -2020-02-29 05:00:00,73.63,156.637,35.284,32.431999999999995 -2020-02-29 05:15:00,74.75,167.479,35.284,32.431999999999995 -2020-02-29 05:30:00,75.74,163.838,35.284,32.431999999999995 -2020-02-29 05:45:00,77.0,162.155,35.284,32.431999999999995 -2020-02-29 06:00:00,77.1,178.55700000000002,36.265,32.431999999999995 -2020-02-29 06:15:00,78.45,199.275,36.265,32.431999999999995 -2020-02-29 06:30:00,77.71,195.088,36.265,32.431999999999995 -2020-02-29 06:45:00,79.47,191.61700000000002,36.265,32.431999999999995 -2020-02-29 07:00:00,82.82,187.955,40.714,32.431999999999995 -2020-02-29 07:15:00,85.08,191.27,40.714,32.431999999999995 -2020-02-29 07:30:00,87.37,194.697,40.714,32.431999999999995 -2020-02-29 07:45:00,90.24,196.983,40.714,32.431999999999995 -2020-02-29 08:00:00,94.26,198.00400000000002,46.692,32.431999999999995 -2020-02-29 08:15:00,93.79,199.93099999999998,46.692,32.431999999999995 -2020-02-29 08:30:00,95.57,198.158,46.692,32.431999999999995 -2020-02-29 08:45:00,97.68,196.007,46.692,32.431999999999995 -2020-02-29 09:00:00,99.64,191.146,48.925,32.431999999999995 -2020-02-29 09:15:00,101.64,189.36700000000002,48.925,32.431999999999995 -2020-02-29 09:30:00,101.61,188.537,48.925,32.431999999999995 -2020-02-29 09:45:00,103.34,185.695,48.925,32.431999999999995 -2020-02-29 10:00:00,103.19,180.743,47.799,32.431999999999995 -2020-02-29 10:15:00,103.4,178.24599999999998,47.799,32.431999999999995 -2020-02-29 10:30:00,100.05,175.993,47.799,32.431999999999995 -2020-02-29 10:45:00,101.34,175.965,47.799,32.431999999999995 -2020-02-29 11:00:00,102.61,174.145,44.309,32.431999999999995 -2020-02-29 11:15:00,104.3,171.86900000000003,44.309,32.431999999999995 -2020-02-29 11:30:00,107.59,171.69299999999998,44.309,32.431999999999995 -2020-02-29 11:45:00,110.39,169.666,44.309,32.431999999999995 -2020-02-29 12:00:00,107.28,165.10299999999998,42.367,32.431999999999995 -2020-02-29 12:15:00,108.46,163.957,42.367,32.431999999999995 -2020-02-29 12:30:00,105.82,163.566,42.367,32.431999999999995 -2020-02-29 12:45:00,103.19,164.22799999999998,42.367,32.431999999999995 -2020-02-29 13:00:00,97.8,164.255,39.036,32.431999999999995 -2020-02-29 13:15:00,97.25,161.597,39.036,32.431999999999995 -2020-02-29 13:30:00,95.99,160.111,39.036,32.431999999999995 -2020-02-29 13:45:00,96.33,160.111,39.036,32.431999999999995 -2020-02-29 14:00:00,93.08,161.362,37.995,32.431999999999995 -2020-02-29 14:15:00,93.55,160.257,37.995,32.431999999999995 -2020-02-29 14:30:00,93.15,159.326,37.995,32.431999999999995 -2020-02-29 14:45:00,93.28,160.732,37.995,32.431999999999995 -2020-02-29 15:00:00,92.46,162.841,40.71,32.431999999999995 -2020-02-29 15:15:00,91.85,162.183,40.71,32.431999999999995 -2020-02-29 15:30:00,89.88,163.00799999999998,40.71,32.431999999999995 -2020-02-29 15:45:00,88.72,163.968,40.71,32.431999999999995 -2020-02-29 16:00:00,88.8,163.262,46.998000000000005,32.431999999999995 -2020-02-29 16:15:00,87.64,165.77700000000002,46.998000000000005,32.431999999999995 -2020-02-29 16:30:00,89.92,168.66400000000002,46.998000000000005,32.431999999999995 -2020-02-29 16:45:00,92.75,170.44299999999998,46.998000000000005,32.431999999999995 -2020-02-29 17:00:00,98.43,172.093,55.431000000000004,32.431999999999995 -2020-02-29 17:15:00,99.1,175.64,55.431000000000004,32.431999999999995 -2020-02-29 17:30:00,102.07,177.953,55.431000000000004,32.431999999999995 -2020-02-29 17:45:00,105.39,178.418,55.431000000000004,32.431999999999995 -2020-02-29 18:00:00,110.77,180.94299999999998,55.989,32.431999999999995 -2020-02-29 18:15:00,113.04,182.285,55.989,32.431999999999995 -2020-02-29 18:30:00,114.76,182.68,55.989,32.431999999999995 -2020-02-29 18:45:00,114.16,180.55700000000002,55.989,32.431999999999995 -2020-02-29 19:00:00,112.1,180.942,50.882,32.431999999999995 -2020-02-29 19:15:00,110.76,178.642,50.882,32.431999999999995 -2020-02-29 19:30:00,109.34,177.685,50.882,32.431999999999995 -2020-02-29 19:45:00,110.83,174.40099999999998,50.882,32.431999999999995 -2020-02-29 20:00:00,103.05,171.63400000000001,43.172,32.431999999999995 -2020-02-29 20:15:00,100.21,168.33700000000002,43.172,32.431999999999995 -2020-02-29 20:30:00,97.84,164.28599999999997,43.172,32.431999999999995 -2020-02-29 20:45:00,96.65,162.657,43.172,32.431999999999995 -2020-02-29 21:00:00,90.85,162.047,37.599000000000004,32.431999999999995 -2020-02-29 21:15:00,91.68,159.80200000000002,37.599000000000004,32.431999999999995 -2020-02-29 21:30:00,89.56,159.00799999999998,37.599000000000004,32.431999999999995 -2020-02-29 21:45:00,89.3,158.668,37.599000000000004,32.431999999999995 -2020-02-29 22:00:00,85.48,153.77700000000002,39.047,32.431999999999995 -2020-02-29 22:15:00,84.96,151.89,39.047,32.431999999999995 -2020-02-29 22:30:00,79.47,149.774,39.047,32.431999999999995 -2020-02-29 22:45:00,81.08,147.64600000000002,39.047,32.431999999999995 -2020-02-29 23:00:00,77.22,142.952,32.339,32.431999999999995 -2020-02-29 23:15:00,77.22,138.085,32.339,32.431999999999995 -2020-02-29 23:30:00,75.06,136.158,32.339,32.431999999999995 -2020-02-29 23:45:00,73.54,133.304,32.339,32.431999999999995 -2020-03-01 00:00:00,68.61,114.87200000000001,20.007,31.988000000000003 -2020-03-01 00:15:00,70.53,108.667,20.007,31.988000000000003 -2020-03-01 00:30:00,66.5,108.73299999999999,20.007,31.988000000000003 -2020-03-01 00:45:00,67.58,110.04899999999999,20.007,31.988000000000003 -2020-03-01 01:00:00,65.55,112.008,17.378,31.988000000000003 -2020-03-01 01:15:00,65.75,113.44200000000001,17.378,31.988000000000003 -2020-03-01 01:30:00,65.35,113.366,17.378,31.988000000000003 -2020-03-01 01:45:00,65.61,113.147,17.378,31.988000000000003 -2020-03-01 02:00:00,63.96,115.15,16.145,31.988000000000003 -2020-03-01 02:15:00,64.55,114.846,16.145,31.988000000000003 -2020-03-01 02:30:00,62.76,115.854,16.145,31.988000000000003 -2020-03-01 02:45:00,63.36,118.155,16.145,31.988000000000003 -2020-03-01 03:00:00,62.76,120.616,15.427999999999999,31.988000000000003 -2020-03-01 03:15:00,63.56,121.51,15.427999999999999,31.988000000000003 -2020-03-01 03:30:00,63.25,123.181,15.427999999999999,31.988000000000003 -2020-03-01 03:45:00,63.21,124.824,15.427999999999999,31.988000000000003 -2020-03-01 04:00:00,62.91,133.92700000000002,16.663,31.988000000000003 -2020-03-01 04:15:00,63.35,143.066,16.663,31.988000000000003 -2020-03-01 04:30:00,63.9,143.304,16.663,31.988000000000003 -2020-03-01 04:45:00,63.94,143.76,16.663,31.988000000000003 -2020-03-01 05:00:00,66.16,157.566,17.271,31.988000000000003 -2020-03-01 05:15:00,66.79,168.065,17.271,31.988000000000003 -2020-03-01 05:30:00,66.13,163.91,17.271,31.988000000000003 -2020-03-01 05:45:00,66.68,161.535,17.271,31.988000000000003 -2020-03-01 06:00:00,67.98,178.033,17.612000000000002,31.988000000000003 -2020-03-01 06:15:00,67.63,197.967,17.612000000000002,31.988000000000003 -2020-03-01 06:30:00,66.96,192.521,17.612000000000002,31.988000000000003 -2020-03-01 06:45:00,67.33,187.15599999999998,17.612000000000002,31.988000000000003 -2020-03-01 07:00:00,69.6,187.088,20.88,31.988000000000003 -2020-03-01 07:15:00,70.32,188.924,20.88,31.988000000000003 -2020-03-01 07:30:00,72.12,190.649,20.88,31.988000000000003 -2020-03-01 07:45:00,75.07,191.512,20.88,31.988000000000003 -2020-03-01 08:00:00,79.08,194.41400000000002,25.861,31.988000000000003 -2020-03-01 08:15:00,78.76,195.868,25.861,31.988000000000003 -2020-03-01 08:30:00,77.97,195.63400000000001,25.861,31.988000000000003 -2020-03-01 08:45:00,78.47,194.578,25.861,31.988000000000003 -2020-03-01 09:00:00,77.62,188.77599999999998,27.921999999999997,31.988000000000003 -2020-03-01 09:15:00,77.96,187.282,27.921999999999997,31.988000000000003 -2020-03-01 09:30:00,80.81,186.24200000000002,27.921999999999997,31.988000000000003 -2020-03-01 09:45:00,87.16,183.475,27.921999999999997,31.988000000000003 -2020-03-01 10:00:00,87.22,182.304,29.048000000000002,31.988000000000003 -2020-03-01 10:15:00,85.78,180.335,29.048000000000002,31.988000000000003 -2020-03-01 10:30:00,84.44,178.554,29.048000000000002,31.988000000000003 -2020-03-01 10:45:00,83.32,176.795,29.048000000000002,31.988000000000003 -2020-03-01 11:00:00,83.84,174.87900000000002,32.02,31.988000000000003 -2020-03-01 11:15:00,81.49,172.76,32.02,31.988000000000003 -2020-03-01 11:30:00,83.37,172.18400000000003,32.02,31.988000000000003 -2020-03-01 11:45:00,82.81,171.415,32.02,31.988000000000003 -2020-03-01 12:00:00,79.55,165.91400000000002,28.55,31.988000000000003 -2020-03-01 12:15:00,76.4,166.451,28.55,31.988000000000003 -2020-03-01 12:30:00,72.82,165.06900000000002,28.55,31.988000000000003 -2020-03-01 12:45:00,71.89,164.695,28.55,31.988000000000003 -2020-03-01 13:00:00,68.82,163.696,25.601999999999997,31.988000000000003 -2020-03-01 13:15:00,67.4,163.767,25.601999999999997,31.988000000000003 -2020-03-01 13:30:00,66.06,161.689,25.601999999999997,31.988000000000003 -2020-03-01 13:45:00,65.28,160.939,25.601999999999997,31.988000000000003 -2020-03-01 14:00:00,62.31,162.632,23.916999999999998,31.988000000000003 -2020-03-01 14:15:00,63.97,162.403,23.916999999999998,31.988000000000003 -2020-03-01 14:30:00,64.32,162.54399999999998,23.916999999999998,31.988000000000003 -2020-03-01 14:45:00,66.22,163.375,23.916999999999998,31.988000000000003 -2020-03-01 15:00:00,66.27,163.856,24.064,31.988000000000003 -2020-03-01 15:15:00,67.29,163.786,24.064,31.988000000000003 -2020-03-01 15:30:00,68.58,164.68200000000002,24.064,31.988000000000003 -2020-03-01 15:45:00,70.54,165.767,24.064,31.988000000000003 -2020-03-01 16:00:00,73.29,168.68,28.189,31.988000000000003 -2020-03-01 16:15:00,73.42,170.81400000000002,28.189,31.988000000000003 -2020-03-01 16:30:00,75.75,173.27599999999998,28.189,31.988000000000003 -2020-03-01 16:45:00,78.52,174.707,28.189,31.988000000000003 -2020-03-01 17:00:00,83.11,177.141,37.576,31.988000000000003 -2020-03-01 17:15:00,85.27,180.53900000000002,37.576,31.988000000000003 -2020-03-01 17:30:00,89.46,183.257,37.576,31.988000000000003 -2020-03-01 17:45:00,92.14,185.89,37.576,31.988000000000003 -2020-03-01 18:00:00,100.96,188.957,42.669,31.988000000000003 -2020-03-01 18:15:00,103.27,191.891,42.669,31.988000000000003 -2020-03-01 18:30:00,103.76,190.107,42.669,31.988000000000003 -2020-03-01 18:45:00,102.03,190.166,42.669,31.988000000000003 -2020-03-01 19:00:00,101.46,190.812,43.538999999999994,31.988000000000003 -2020-03-01 19:15:00,100.17,188.94400000000002,43.538999999999994,31.988000000000003 -2020-03-01 19:30:00,98.2,188.227,43.538999999999994,31.988000000000003 -2020-03-01 19:45:00,96.97,185.736,43.538999999999994,31.988000000000003 -2020-03-01 20:00:00,93.91,182.748,37.330999999999996,31.988000000000003 -2020-03-01 20:15:00,93.83,180.495,37.330999999999996,31.988000000000003 -2020-03-01 20:30:00,95.91,177.46200000000002,37.330999999999996,31.988000000000003 -2020-03-01 20:45:00,97.34,173.905,37.330999999999996,31.988000000000003 -2020-03-01 21:00:00,94.5,170.889,33.856,31.988000000000003 -2020-03-01 21:15:00,89.25,168.035,33.856,31.988000000000003 -2020-03-01 21:30:00,88.14,167.192,33.856,31.988000000000003 -2020-03-01 21:45:00,89.58,167.22400000000002,33.856,31.988000000000003 -2020-03-01 22:00:00,93.6,161.214,34.711999999999996,31.988000000000003 -2020-03-01 22:15:00,95.33,158.43200000000002,34.711999999999996,31.988000000000003 -2020-03-01 22:30:00,91.18,152.474,34.711999999999996,31.988000000000003 -2020-03-01 22:45:00,83.29,149.13299999999998,34.711999999999996,31.988000000000003 -2020-03-01 23:00:00,79.65,141.811,29.698,31.988000000000003 -2020-03-01 23:15:00,80.74,138.316,29.698,31.988000000000003 -2020-03-01 23:30:00,78.27,137.503,29.698,31.988000000000003 -2020-03-01 23:45:00,78.56,135.459,29.698,31.988000000000003 -2020-03-02 00:00:00,73.51,118.774,29.983,32.166 -2020-03-02 00:15:00,74.57,115.516,29.983,32.166 -2020-03-02 00:30:00,75.5,115.632,29.983,32.166 -2020-03-02 00:45:00,73.67,116.381,29.983,32.166 -2020-03-02 01:00:00,70.05,118.355,29.122,32.166 -2020-03-02 01:15:00,71.82,119.274,29.122,32.166 -2020-03-02 01:30:00,71.29,119.28299999999999,29.122,32.166 -2020-03-02 01:45:00,72.24,119.164,29.122,32.166 -2020-03-02 02:00:00,70.05,121.193,28.676,32.166 -2020-03-02 02:15:00,71.47,122.126,28.676,32.166 -2020-03-02 02:30:00,70.78,123.491,28.676,32.166 -2020-03-02 02:45:00,71.48,125.18299999999999,28.676,32.166 -2020-03-02 03:00:00,72.12,128.914,26.552,32.166 -2020-03-02 03:15:00,72.82,131.45600000000002,26.552,32.166 -2020-03-02 03:30:00,79.44,132.96200000000002,26.552,32.166 -2020-03-02 03:45:00,81.71,134.03,26.552,32.166 -2020-03-02 04:00:00,82.58,147.67700000000002,27.44,32.166 -2020-03-02 04:15:00,77.82,161.142,27.44,32.166 -2020-03-02 04:30:00,78.23,163.398,27.44,32.166 -2020-03-02 04:45:00,79.38,164.046,27.44,32.166 -2020-03-02 05:00:00,83.72,193.69400000000002,36.825,32.166 -2020-03-02 05:15:00,86.57,224.705,36.825,32.166 -2020-03-02 05:30:00,91.53,220.55200000000002,36.825,32.166 -2020-03-02 05:45:00,96.44,212.40900000000002,36.825,32.166 -2020-03-02 06:00:00,105.65,210.571,56.589,32.166 -2020-03-02 06:15:00,110.56,215.197,56.589,32.166 -2020-03-02 06:30:00,114.34,217.84599999999998,56.589,32.166 -2020-03-02 06:45:00,116.99,221.528,56.589,32.166 -2020-03-02 07:00:00,121.98,223.831,67.49,32.166 -2020-03-02 07:15:00,122.83,227.138,67.49,32.166 -2020-03-02 07:30:00,126.26,227.96599999999998,67.49,32.166 -2020-03-02 07:45:00,128.48,226.40400000000002,67.49,32.166 -2020-03-02 08:00:00,132.44,224.445,60.028,32.166 -2020-03-02 08:15:00,131.31,223.685,60.028,32.166 -2020-03-02 08:30:00,130.5,219.38,60.028,32.166 -2020-03-02 08:45:00,131.43,215.199,60.028,32.166 -2020-03-02 09:00:00,130.52,208.362,55.018,32.166 -2020-03-02 09:15:00,130.5,203.305,55.018,32.166 -2020-03-02 09:30:00,131.0,201.21099999999998,55.018,32.166 -2020-03-02 09:45:00,131.43,198.43,55.018,32.166 -2020-03-02 10:00:00,129.42,196.28799999999998,51.183,32.166 -2020-03-02 10:15:00,130.46,194.028,51.183,32.166 -2020-03-02 10:30:00,130.39,191.372,51.183,32.166 -2020-03-02 10:45:00,130.17,190.128,51.183,32.166 -2020-03-02 11:00:00,128.8,185.782,50.065,32.166 -2020-03-02 11:15:00,130.97,185.44099999999997,50.065,32.166 -2020-03-02 11:30:00,128.5,186.283,50.065,32.166 -2020-03-02 11:45:00,124.59,185.206,50.065,32.166 -2020-03-02 12:00:00,122.07,181.143,48.141999999999996,32.166 -2020-03-02 12:15:00,122.75,181.709,48.141999999999996,32.166 -2020-03-02 12:30:00,118.19,180.388,48.141999999999996,32.166 -2020-03-02 12:45:00,115.54,181.465,48.141999999999996,32.166 -2020-03-02 13:00:00,116.29,181.18,47.887,32.166 -2020-03-02 13:15:00,111.2,179.791,47.887,32.166 -2020-03-02 13:30:00,110.44,177.218,47.887,32.166 -2020-03-02 13:45:00,110.06,176.62,47.887,32.166 -2020-03-02 14:00:00,109.1,177.69,48.571000000000005,32.166 -2020-03-02 14:15:00,110.15,176.90200000000002,48.571000000000005,32.166 -2020-03-02 14:30:00,109.82,176.476,48.571000000000005,32.166 -2020-03-02 14:45:00,112.38,177.563,48.571000000000005,32.166 -2020-03-02 15:00:00,111.64,179.74599999999998,49.937,32.166 -2020-03-02 15:15:00,113.26,178.19299999999998,49.937,32.166 -2020-03-02 15:30:00,112.72,178.34599999999998,49.937,32.166 -2020-03-02 15:45:00,112.81,178.925,49.937,32.166 -2020-03-02 16:00:00,116.61,182.183,52.963,32.166 -2020-03-02 16:15:00,115.99,183.57299999999998,52.963,32.166 -2020-03-02 16:30:00,117.48,185.021,52.963,32.166 -2020-03-02 16:45:00,119.0,185.275,52.963,32.166 -2020-03-02 17:00:00,121.05,187.44,61.163999999999994,32.166 -2020-03-02 17:15:00,122.51,189.97299999999998,61.163999999999994,32.166 -2020-03-02 17:30:00,125.34,192.14,61.163999999999994,32.166 -2020-03-02 17:45:00,127.45,193.295,61.163999999999994,32.166 -2020-03-02 18:00:00,135.41,196.68599999999998,63.788999999999994,32.166 -2020-03-02 18:15:00,136.15,197.328,63.788999999999994,32.166 -2020-03-02 18:30:00,135.28,196.08900000000003,63.788999999999994,32.166 -2020-03-02 18:45:00,134.36,197.226,63.788999999999994,32.166 -2020-03-02 19:00:00,134.74,196.25,63.913000000000004,32.166 -2020-03-02 19:15:00,130.7,193.407,63.913000000000004,32.166 -2020-03-02 19:30:00,131.09,193.16099999999997,63.913000000000004,32.166 -2020-03-02 19:45:00,129.93,189.833,63.913000000000004,32.166 -2020-03-02 20:00:00,119.75,184.363,65.44,32.166 -2020-03-02 20:15:00,118.41,179.88299999999998,65.44,32.166 -2020-03-02 20:30:00,115.33,175.128,65.44,32.166 -2020-03-02 20:45:00,114.14,173.17700000000002,65.44,32.166 -2020-03-02 21:00:00,109.07,170.59400000000002,59.117,32.166 -2020-03-02 21:15:00,111.01,166.644,59.117,32.166 -2020-03-02 21:30:00,109.45,165.047,59.117,32.166 -2020-03-02 21:45:00,107.16,164.585,59.117,32.166 -2020-03-02 22:00:00,100.92,155.52,52.301,32.166 -2020-03-02 22:15:00,97.41,151.71,52.301,32.166 -2020-03-02 22:30:00,99.9,135.923,52.301,32.166 -2020-03-02 22:45:00,100.06,127.88600000000001,52.301,32.166 -2020-03-02 23:00:00,94.69,121.306,44.373000000000005,32.166 -2020-03-02 23:15:00,88.97,120.22200000000001,44.373000000000005,32.166 -2020-03-02 23:30:00,89.49,122.09700000000001,44.373000000000005,32.166 -2020-03-02 23:45:00,90.65,122.587,44.373000000000005,32.166 -2020-03-03 00:00:00,87.69,117.507,44.647,32.166 -2020-03-03 00:15:00,84.07,115.73299999999999,44.647,32.166 -2020-03-03 00:30:00,82.45,115.021,44.647,32.166 -2020-03-03 00:45:00,85.5,114.936,44.647,32.166 -2020-03-03 01:00:00,80.31,116.613,41.433,32.166 -2020-03-03 01:15:00,82.7,117.10600000000001,41.433,32.166 -2020-03-03 01:30:00,78.1,117.24600000000001,41.433,32.166 -2020-03-03 01:45:00,76.78,117.35700000000001,41.433,32.166 -2020-03-03 02:00:00,76.01,119.30799999999999,39.909,32.166 -2020-03-03 02:15:00,76.06,120.281,39.909,32.166 -2020-03-03 02:30:00,76.62,121.045,39.909,32.166 -2020-03-03 02:45:00,81.97,122.792,39.909,32.166 -2020-03-03 03:00:00,84.87,125.315,39.14,32.166 -2020-03-03 03:15:00,86.31,127.15700000000001,39.14,32.166 -2020-03-03 03:30:00,84.73,129.112,39.14,32.166 -2020-03-03 03:45:00,85.5,130.22899999999998,39.14,32.166 -2020-03-03 04:00:00,87.66,143.525,40.015,32.166 -2020-03-03 04:15:00,81.89,156.649,40.015,32.166 -2020-03-03 04:30:00,83.96,158.612,40.015,32.166 -2020-03-03 04:45:00,89.14,160.464,40.015,32.166 -2020-03-03 05:00:00,95.89,195.05900000000003,44.93600000000001,32.166 -2020-03-03 05:15:00,98.73,225.97400000000002,44.93600000000001,32.166 -2020-03-03 05:30:00,97.84,220.359,44.93600000000001,32.166 -2020-03-03 05:45:00,100.51,212.12900000000002,44.93600000000001,32.166 -2020-03-03 06:00:00,109.28,209.315,57.271,32.166 -2020-03-03 06:15:00,112.89,215.521,57.271,32.166 -2020-03-03 06:30:00,115.24,217.46900000000002,57.271,32.166 -2020-03-03 06:45:00,116.92,220.65599999999998,57.271,32.166 -2020-03-03 07:00:00,122.15,222.825,68.352,32.166 -2020-03-03 07:15:00,122.86,225.91299999999998,68.352,32.166 -2020-03-03 07:30:00,126.57,226.18099999999998,68.352,32.166 -2020-03-03 07:45:00,127.79,224.65400000000002,68.352,32.166 -2020-03-03 08:00:00,131.05,222.767,60.717,32.166 -2020-03-03 08:15:00,131.16,220.958,60.717,32.166 -2020-03-03 08:30:00,135.43,216.447,60.717,32.166 -2020-03-03 08:45:00,132.45,211.908,60.717,32.166 -2020-03-03 09:00:00,132.71,204.32299999999998,54.603,32.166 -2020-03-03 09:15:00,132.5,200.79,54.603,32.166 -2020-03-03 09:30:00,134.11,199.454,54.603,32.166 -2020-03-03 09:45:00,133.41,196.642,54.603,32.166 -2020-03-03 10:00:00,133.09,193.778,52.308,32.166 -2020-03-03 10:15:00,132.43,190.52200000000002,52.308,32.166 -2020-03-03 10:30:00,133.18,188.062,52.308,32.166 -2020-03-03 10:45:00,133.76,187.234,52.308,32.166 -2020-03-03 11:00:00,132.89,184.308,51.838,32.166 -2020-03-03 11:15:00,134.16,183.71599999999998,51.838,32.166 -2020-03-03 11:30:00,133.77,183.303,51.838,32.166 -2020-03-03 11:45:00,134.04,182.864,51.838,32.166 -2020-03-03 12:00:00,134.11,177.479,50.375,32.166 -2020-03-03 12:15:00,134.12,177.7,50.375,32.166 -2020-03-03 12:30:00,134.26,177.143,50.375,32.166 -2020-03-03 12:45:00,136.24,178.02,50.375,32.166 -2020-03-03 13:00:00,132.14,177.34099999999998,50.735,32.166 -2020-03-03 13:15:00,140.28,175.817,50.735,32.166 -2020-03-03 13:30:00,140.27,174.355,50.735,32.166 -2020-03-03 13:45:00,138.43,173.81900000000002,50.735,32.166 -2020-03-03 14:00:00,134.59,175.283,50.946000000000005,32.166 -2020-03-03 14:15:00,133.48,174.597,50.946000000000005,32.166 -2020-03-03 14:30:00,134.65,174.787,50.946000000000005,32.166 -2020-03-03 14:45:00,139.12,175.71,50.946000000000005,32.166 -2020-03-03 15:00:00,138.89,177.46400000000003,53.18,32.166 -2020-03-03 15:15:00,136.59,176.313,53.18,32.166 -2020-03-03 15:30:00,133.42,176.609,53.18,32.166 -2020-03-03 15:45:00,137.07,176.822,53.18,32.166 -2020-03-03 16:00:00,138.31,180.324,54.928999999999995,32.166 -2020-03-03 16:15:00,137.5,182.165,54.928999999999995,32.166 -2020-03-03 16:30:00,135.91,184.196,54.928999999999995,32.166 -2020-03-03 16:45:00,136.81,184.774,54.928999999999995,32.166 -2020-03-03 17:00:00,138.14,187.50099999999998,60.913000000000004,32.166 -2020-03-03 17:15:00,138.47,190.11700000000002,60.913000000000004,32.166 -2020-03-03 17:30:00,137.24,192.908,60.913000000000004,32.166 -2020-03-03 17:45:00,134.06,193.915,60.913000000000004,32.166 -2020-03-03 18:00:00,143.82,197.111,62.214,32.166 -2020-03-03 18:15:00,146.3,197.49,62.214,32.166 -2020-03-03 18:30:00,146.08,195.918,62.214,32.166 -2020-03-03 18:45:00,142.83,197.834,62.214,32.166 -2020-03-03 19:00:00,139.19,196.77599999999998,62.38,32.166 -2020-03-03 19:15:00,140.37,193.695,62.38,32.166 -2020-03-03 19:30:00,138.01,192.799,62.38,32.166 -2020-03-03 19:45:00,138.66,189.58599999999998,62.38,32.166 -2020-03-03 20:00:00,130.23,184.28,65.018,32.166 -2020-03-03 20:15:00,123.06,179.03400000000002,65.018,32.166 -2020-03-03 20:30:00,123.81,175.297,65.018,32.166 -2020-03-03 20:45:00,121.62,172.838,65.018,32.166 -2020-03-03 21:00:00,117.63,169.65599999999998,56.416000000000004,32.166 -2020-03-03 21:15:00,108.75,166.425,56.416000000000004,32.166 -2020-03-03 21:30:00,108.4,164.136,56.416000000000004,32.166 -2020-03-03 21:45:00,110.03,163.938,56.416000000000004,32.166 -2020-03-03 22:00:00,107.4,156.55,52.846000000000004,32.166 -2020-03-03 22:15:00,105.11,152.477,52.846000000000004,32.166 -2020-03-03 22:30:00,98.01,136.759,52.846000000000004,32.166 -2020-03-03 22:45:00,100.15,128.994,52.846000000000004,32.166 -2020-03-03 23:00:00,96.08,122.352,44.435,32.166 -2020-03-03 23:15:00,96.98,120.572,44.435,32.166 -2020-03-03 23:30:00,89.43,122.104,44.435,32.166 -2020-03-03 23:45:00,87.12,122.21,44.435,32.166 -2020-03-04 00:00:00,83.92,117.189,42.527,32.166 -2020-03-04 00:15:00,81.31,115.42299999999999,42.527,32.166 -2020-03-04 00:30:00,85.2,114.69200000000001,42.527,32.166 -2020-03-04 00:45:00,87.61,114.60600000000001,42.527,32.166 -2020-03-04 01:00:00,83.3,116.243,38.655,32.166 -2020-03-04 01:15:00,83.96,116.72,38.655,32.166 -2020-03-04 01:30:00,76.44,116.84299999999999,38.655,32.166 -2020-03-04 01:45:00,77.77,116.96,38.655,32.166 -2020-03-04 02:00:00,77.27,118.90100000000001,36.912,32.166 -2020-03-04 02:15:00,79.12,119.867,36.912,32.166 -2020-03-04 02:30:00,84.05,120.65,36.912,32.166 -2020-03-04 02:45:00,84.7,122.397,36.912,32.166 -2020-03-04 03:00:00,84.41,124.934,36.98,32.166 -2020-03-04 03:15:00,86.26,126.76,36.98,32.166 -2020-03-04 03:30:00,87.42,128.709,36.98,32.166 -2020-03-04 03:45:00,87.57,129.838,36.98,32.166 -2020-03-04 04:00:00,83.7,143.138,38.052,32.166 -2020-03-04 04:15:00,88.51,156.252,38.052,32.166 -2020-03-04 04:30:00,91.89,158.232,38.052,32.166 -2020-03-04 04:45:00,93.1,160.07,38.052,32.166 -2020-03-04 05:00:00,91.47,194.645,42.455,32.166 -2020-03-04 05:15:00,91.38,225.574,42.455,32.166 -2020-03-04 05:30:00,94.97,219.925,42.455,32.166 -2020-03-04 05:45:00,95.93,211.703,42.455,32.166 -2020-03-04 06:00:00,106.42,208.898,57.986000000000004,32.166 -2020-03-04 06:15:00,112.03,215.11,57.986000000000004,32.166 -2020-03-04 06:30:00,114.0,217.014,57.986000000000004,32.166 -2020-03-04 06:45:00,116.7,220.18200000000002,57.986000000000004,32.166 -2020-03-04 07:00:00,122.52,222.373,71.868,32.166 -2020-03-04 07:15:00,122.72,225.428,71.868,32.166 -2020-03-04 07:30:00,125.92,225.655,71.868,32.166 -2020-03-04 07:45:00,128.12,224.088,71.868,32.166 -2020-03-04 08:00:00,130.83,222.176,62.225,32.166 -2020-03-04 08:15:00,130.93,220.354,62.225,32.166 -2020-03-04 08:30:00,131.84,215.78599999999997,62.225,32.166 -2020-03-04 08:45:00,131.99,211.262,62.225,32.166 -2020-03-04 09:00:00,133.12,203.688,58.802,32.166 -2020-03-04 09:15:00,134.1,200.15599999999998,58.802,32.166 -2020-03-04 09:30:00,135.91,198.843,58.802,32.166 -2020-03-04 09:45:00,136.01,196.042,58.802,32.166 -2020-03-04 10:00:00,135.9,193.19299999999998,54.122,32.166 -2020-03-04 10:15:00,137.47,189.97799999999998,54.122,32.166 -2020-03-04 10:30:00,136.84,187.53799999999998,54.122,32.166 -2020-03-04 10:45:00,137.85,186.72799999999998,54.122,32.166 -2020-03-04 11:00:00,137.84,183.793,54.368,32.166 -2020-03-04 11:15:00,140.09,183.22099999999998,54.368,32.166 -2020-03-04 11:30:00,140.75,182.81599999999997,54.368,32.166 -2020-03-04 11:45:00,141.22,182.395,54.368,32.166 -2020-03-04 12:00:00,138.17,177.02900000000002,52.74,32.166 -2020-03-04 12:15:00,140.05,177.263,52.74,32.166 -2020-03-04 12:30:00,136.13,176.671,52.74,32.166 -2020-03-04 12:45:00,137.21,177.545,52.74,32.166 -2020-03-04 13:00:00,141.05,176.90599999999998,52.544,32.166 -2020-03-04 13:15:00,139.87,175.361,52.544,32.166 -2020-03-04 13:30:00,125.18,173.886,52.544,32.166 -2020-03-04 13:45:00,130.05,173.352,52.544,32.166 -2020-03-04 14:00:00,125.22,174.88400000000001,53.602,32.166 -2020-03-04 14:15:00,123.48,174.174,53.602,32.166 -2020-03-04 14:30:00,126.37,174.335,53.602,32.166 -2020-03-04 14:45:00,123.47,175.268,53.602,32.166 -2020-03-04 15:00:00,121.69,177.032,55.59,32.166 -2020-03-04 15:15:00,118.75,175.852,55.59,32.166 -2020-03-04 15:30:00,116.12,176.09900000000002,55.59,32.166 -2020-03-04 15:45:00,120.18,176.293,55.59,32.166 -2020-03-04 16:00:00,123.92,179.798,57.586999999999996,32.166 -2020-03-04 16:15:00,124.85,181.62099999999998,57.586999999999996,32.166 -2020-03-04 16:30:00,120.88,183.65099999999998,57.586999999999996,32.166 -2020-03-04 16:45:00,124.56,184.18900000000002,57.586999999999996,32.166 -2020-03-04 17:00:00,128.84,186.928,62.111999999999995,32.166 -2020-03-04 17:15:00,130.98,189.553,62.111999999999995,32.166 -2020-03-04 17:30:00,131.7,192.36900000000003,62.111999999999995,32.166 -2020-03-04 17:45:00,131.94,193.391,62.111999999999995,32.166 -2020-03-04 18:00:00,138.98,196.595,64.605,32.166 -2020-03-04 18:15:00,143.1,197.04,64.605,32.166 -2020-03-04 18:30:00,145.58,195.46400000000003,64.605,32.166 -2020-03-04 18:45:00,139.57,197.40099999999998,64.605,32.166 -2020-03-04 19:00:00,139.2,196.305,65.55199999999999,32.166 -2020-03-04 19:15:00,137.12,193.24200000000002,65.55199999999999,32.166 -2020-03-04 19:30:00,134.82,192.373,65.55199999999999,32.166 -2020-03-04 19:45:00,128.94,189.204,65.55199999999999,32.166 -2020-03-04 20:00:00,125.39,183.868,66.778,32.166 -2020-03-04 20:15:00,122.78,178.637,66.778,32.166 -2020-03-04 20:30:00,118.75,174.926,66.778,32.166 -2020-03-04 20:45:00,115.74,172.472,66.778,32.166 -2020-03-04 21:00:00,113.84,169.278,56.103,32.166 -2020-03-04 21:15:00,111.66,166.042,56.103,32.166 -2020-03-04 21:30:00,107.31,163.753,56.103,32.166 -2020-03-04 21:45:00,108.44,163.576,56.103,32.166 -2020-03-04 22:00:00,103.17,156.173,51.371,32.166 -2020-03-04 22:15:00,102.87,152.128,51.371,32.166 -2020-03-04 22:30:00,93.83,136.359,51.371,32.166 -2020-03-04 22:45:00,94.61,128.59799999999998,51.371,32.166 -2020-03-04 23:00:00,93.52,121.95,42.798,32.166 -2020-03-04 23:15:00,93.24,120.189,42.798,32.166 -2020-03-04 23:30:00,86.27,121.729,42.798,32.166 -2020-03-04 23:45:00,85.18,121.86,42.798,32.166 -2020-03-05 00:00:00,79.97,116.865,39.069,32.166 -2020-03-05 00:15:00,84.04,115.10700000000001,39.069,32.166 -2020-03-05 00:30:00,86.08,114.35799999999999,39.069,32.166 -2020-03-05 00:45:00,83.85,114.272,39.069,32.166 -2020-03-05 01:00:00,75.83,115.867,37.043,32.166 -2020-03-05 01:15:00,75.46,116.329,37.043,32.166 -2020-03-05 01:30:00,75.14,116.435,37.043,32.166 -2020-03-05 01:45:00,77.65,116.557,37.043,32.166 -2020-03-05 02:00:00,79.69,118.488,34.625,32.166 -2020-03-05 02:15:00,80.69,119.448,34.625,32.166 -2020-03-05 02:30:00,78.05,120.249,34.625,32.166 -2020-03-05 02:45:00,76.94,121.99700000000001,34.625,32.166 -2020-03-05 03:00:00,77.22,124.545,33.812,32.166 -2020-03-05 03:15:00,82.54,126.35600000000001,33.812,32.166 -2020-03-05 03:30:00,83.02,128.299,33.812,32.166 -2020-03-05 03:45:00,80.83,129.442,33.812,32.166 -2020-03-05 04:00:00,78.0,142.746,35.236999999999995,32.166 -2020-03-05 04:15:00,80.68,155.84799999999998,35.236999999999995,32.166 -2020-03-05 04:30:00,86.04,157.845,35.236999999999995,32.166 -2020-03-05 04:45:00,87.84,159.67,35.236999999999995,32.166 -2020-03-05 05:00:00,88.83,194.22299999999998,40.375,32.166 -2020-03-05 05:15:00,87.34,225.169,40.375,32.166 -2020-03-05 05:30:00,90.9,219.488,40.375,32.166 -2020-03-05 05:45:00,95.15,211.271,40.375,32.166 -2020-03-05 06:00:00,105.43,208.47400000000002,52.316,32.166 -2020-03-05 06:15:00,108.76,214.69299999999998,52.316,32.166 -2020-03-05 06:30:00,115.44,216.55200000000002,52.316,32.166 -2020-03-05 06:45:00,115.49,219.701,52.316,32.166 -2020-03-05 07:00:00,122.69,221.91299999999998,64.115,32.166 -2020-03-05 07:15:00,123.41,224.935,64.115,32.166 -2020-03-05 07:30:00,125.69,225.12099999999998,64.115,32.166 -2020-03-05 07:45:00,128.28,223.512,64.115,32.166 -2020-03-05 08:00:00,131.57,221.575,55.033,32.166 -2020-03-05 08:15:00,127.69,219.74099999999999,55.033,32.166 -2020-03-05 08:30:00,127.62,215.11599999999999,55.033,32.166 -2020-03-05 08:45:00,124.84,210.609,55.033,32.166 -2020-03-05 09:00:00,122.67,203.046,49.411,32.166 -2020-03-05 09:15:00,118.75,199.516,49.411,32.166 -2020-03-05 09:30:00,118.32,198.22400000000002,49.411,32.166 -2020-03-05 09:45:00,118.04,195.43599999999998,49.411,32.166 -2020-03-05 10:00:00,117.58,192.6,45.82899999999999,32.166 -2020-03-05 10:15:00,119.95,189.429,45.82899999999999,32.166 -2020-03-05 10:30:00,123.89,187.00799999999998,45.82899999999999,32.166 -2020-03-05 10:45:00,127.92,186.218,45.82899999999999,32.166 -2020-03-05 11:00:00,124.66,183.275,44.333,32.166 -2020-03-05 11:15:00,129.25,182.722,44.333,32.166 -2020-03-05 11:30:00,117.7,182.325,44.333,32.166 -2020-03-05 11:45:00,115.76,181.92,44.333,32.166 -2020-03-05 12:00:00,113.86,176.574,42.95,32.166 -2020-03-05 12:15:00,115.98,176.822,42.95,32.166 -2020-03-05 12:30:00,123.05,176.19299999999998,42.95,32.166 -2020-03-05 12:45:00,119.31,177.06400000000002,42.95,32.166 -2020-03-05 13:00:00,116.22,176.468,42.489,32.166 -2020-03-05 13:15:00,117.03,174.90099999999998,42.489,32.166 -2020-03-05 13:30:00,114.34,173.41299999999998,42.489,32.166 -2020-03-05 13:45:00,112.98,172.882,42.489,32.166 -2020-03-05 14:00:00,120.48,174.481,43.448,32.166 -2020-03-05 14:15:00,125.43,173.747,43.448,32.166 -2020-03-05 14:30:00,124.36,173.87900000000002,43.448,32.166 -2020-03-05 14:45:00,124.56,174.821,43.448,32.166 -2020-03-05 15:00:00,120.75,176.59599999999998,45.994,32.166 -2020-03-05 15:15:00,117.53,175.38400000000001,45.994,32.166 -2020-03-05 15:30:00,125.28,175.582,45.994,32.166 -2020-03-05 15:45:00,123.67,175.75900000000001,45.994,32.166 -2020-03-05 16:00:00,124.31,179.269,48.167,32.166 -2020-03-05 16:15:00,121.46,181.071,48.167,32.166 -2020-03-05 16:30:00,123.02,183.101,48.167,32.166 -2020-03-05 16:45:00,120.81,183.59900000000002,48.167,32.166 -2020-03-05 17:00:00,126.49,186.34900000000002,52.637,32.166 -2020-03-05 17:15:00,122.58,188.981,52.637,32.166 -2020-03-05 17:30:00,129.9,191.82299999999998,52.637,32.166 -2020-03-05 17:45:00,131.12,192.859,52.637,32.166 -2020-03-05 18:00:00,136.47,196.07,55.739,32.166 -2020-03-05 18:15:00,136.58,196.582,55.739,32.166 -2020-03-05 18:30:00,142.41,195.00400000000002,55.739,32.166 -2020-03-05 18:45:00,143.42,196.96200000000002,55.739,32.166 -2020-03-05 19:00:00,139.76,195.826,56.36600000000001,32.166 -2020-03-05 19:15:00,135.79,192.78099999999998,56.36600000000001,32.166 -2020-03-05 19:30:00,135.89,191.94099999999997,56.36600000000001,32.166 -2020-03-05 19:45:00,135.03,188.817,56.36600000000001,32.166 -2020-03-05 20:00:00,127.56,183.449,56.338,32.166 -2020-03-05 20:15:00,124.1,178.235,56.338,32.166 -2020-03-05 20:30:00,121.73,174.549,56.338,32.166 -2020-03-05 20:45:00,117.92,172.1,56.338,32.166 -2020-03-05 21:00:00,109.2,168.894,49.894,32.166 -2020-03-05 21:15:00,112.76,165.65400000000002,49.894,32.166 -2020-03-05 21:30:00,110.85,163.363,49.894,32.166 -2020-03-05 21:45:00,109.7,163.209,49.894,32.166 -2020-03-05 22:00:00,101.13,155.79,46.687,32.166 -2020-03-05 22:15:00,103.31,151.773,46.687,32.166 -2020-03-05 22:30:00,97.08,135.952,46.687,32.166 -2020-03-05 22:45:00,93.57,128.194,46.687,32.166 -2020-03-05 23:00:00,85.09,121.542,39.211,32.166 -2020-03-05 23:15:00,84.64,119.8,39.211,32.166 -2020-03-05 23:30:00,87.76,121.34700000000001,39.211,32.166 -2020-03-05 23:45:00,89.95,121.505,39.211,32.166 -2020-03-06 00:00:00,84.63,115.272,36.616,32.166 -2020-03-06 00:15:00,80.02,113.741,36.616,32.166 -2020-03-06 00:30:00,78.58,112.88,36.616,32.166 -2020-03-06 00:45:00,79.97,112.955,36.616,32.166 -2020-03-06 01:00:00,81.81,114.15899999999999,33.799,32.166 -2020-03-06 01:15:00,83.15,115.40899999999999,33.799,32.166 -2020-03-06 01:30:00,81.42,115.375,33.799,32.166 -2020-03-06 01:45:00,77.29,115.573,33.799,32.166 -2020-03-06 02:00:00,77.39,117.697,32.968,32.166 -2020-03-06 02:15:00,82.75,118.53200000000001,32.968,32.166 -2020-03-06 02:30:00,81.3,119.965,32.968,32.166 -2020-03-06 02:45:00,79.68,121.664,32.968,32.166 -2020-03-06 03:00:00,75.05,123.374,33.533,32.166 -2020-03-06 03:15:00,80.07,125.906,33.533,32.166 -2020-03-06 03:30:00,83.67,127.795,33.533,32.166 -2020-03-06 03:45:00,79.45,129.38,33.533,32.166 -2020-03-06 04:00:00,79.38,142.92700000000002,36.102,32.166 -2020-03-06 04:15:00,84.68,155.572,36.102,32.166 -2020-03-06 04:30:00,86.79,157.934,36.102,32.166 -2020-03-06 04:45:00,89.75,158.566,36.102,32.166 -2020-03-06 05:00:00,92.61,191.81799999999998,42.423,32.166 -2020-03-06 05:15:00,88.55,224.35,42.423,32.166 -2020-03-06 05:30:00,92.57,219.692,42.423,32.166 -2020-03-06 05:45:00,95.18,211.36,42.423,32.166 -2020-03-06 06:00:00,105.52,209.019,55.38,32.166 -2020-03-06 06:15:00,109.46,213.859,55.38,32.166 -2020-03-06 06:30:00,110.06,214.83900000000003,55.38,32.166 -2020-03-06 06:45:00,112.44,219.50599999999997,55.38,32.166 -2020-03-06 07:00:00,119.09,221.028,65.929,32.166 -2020-03-06 07:15:00,120.34,225.11900000000003,65.929,32.166 -2020-03-06 07:30:00,122.16,224.828,65.929,32.166 -2020-03-06 07:45:00,120.89,222.30200000000002,65.929,32.166 -2020-03-06 08:00:00,121.85,219.385,57.336999999999996,32.166 -2020-03-06 08:15:00,119.68,217.25400000000002,57.336999999999996,32.166 -2020-03-06 08:30:00,118.65,213.497,57.336999999999996,32.166 -2020-03-06 08:45:00,117.07,207.47099999999998,57.336999999999996,32.166 -2020-03-06 09:00:00,115.36,200.05700000000002,54.226000000000006,32.166 -2020-03-06 09:15:00,114.99,197.328,54.226000000000006,32.166 -2020-03-06 09:30:00,115.37,195.553,54.226000000000006,32.166 -2020-03-06 09:45:00,113.84,192.722,54.226000000000006,32.166 -2020-03-06 10:00:00,112.66,188.787,51.298,32.166 -2020-03-06 10:15:00,112.59,186.301,51.298,32.166 -2020-03-06 10:30:00,112.69,183.907,51.298,32.166 -2020-03-06 10:45:00,112.62,182.697,51.298,32.166 -2020-03-06 11:00:00,113.85,179.75400000000002,50.839,32.166 -2020-03-06 11:15:00,114.18,178.215,50.839,32.166 -2020-03-06 11:30:00,118.13,179.46599999999998,50.839,32.166 -2020-03-06 11:45:00,115.78,178.99900000000002,50.839,32.166 -2020-03-06 12:00:00,110.8,174.774,47.976000000000006,32.166 -2020-03-06 12:15:00,112.96,172.933,47.976000000000006,32.166 -2020-03-06 12:30:00,107.04,172.435,47.976000000000006,32.166 -2020-03-06 12:45:00,113.31,173.696,47.976000000000006,32.166 -2020-03-06 13:00:00,113.56,174.122,46.299,32.166 -2020-03-06 13:15:00,112.83,173.34400000000002,46.299,32.166 -2020-03-06 13:30:00,105.26,171.96599999999998,46.299,32.166 -2020-03-06 13:45:00,104.96,171.417,46.299,32.166 -2020-03-06 14:00:00,107.2,171.88099999999997,44.971000000000004,32.166 -2020-03-06 14:15:00,110.28,171.00900000000001,44.971000000000004,32.166 -2020-03-06 14:30:00,113.01,171.803,44.971000000000004,32.166 -2020-03-06 14:45:00,113.67,172.955,44.971000000000004,32.166 -2020-03-06 15:00:00,114.93,174.264,47.48,32.166 -2020-03-06 15:15:00,115.31,172.56799999999998,47.48,32.166 -2020-03-06 15:30:00,123.49,171.176,47.48,32.166 -2020-03-06 15:45:00,122.8,171.57299999999998,47.48,32.166 -2020-03-06 16:00:00,120.52,173.84599999999998,50.648,32.166 -2020-03-06 16:15:00,118.33,175.97,50.648,32.166 -2020-03-06 16:30:00,123.41,178.06799999999998,50.648,32.166 -2020-03-06 16:45:00,122.27,178.315,50.648,32.166 -2020-03-06 17:00:00,123.95,181.48,56.251000000000005,32.166 -2020-03-06 17:15:00,125.14,183.697,56.251000000000005,32.166 -2020-03-06 17:30:00,127.64,186.27200000000002,56.251000000000005,32.166 -2020-03-06 17:45:00,129.37,187.075,56.251000000000005,32.166 -2020-03-06 18:00:00,132.2,190.96,58.982,32.166 -2020-03-06 18:15:00,134.17,191.049,58.982,32.166 -2020-03-06 18:30:00,139.97,189.84,58.982,32.166 -2020-03-06 18:45:00,137.26,191.862,58.982,32.166 -2020-03-06 19:00:00,138.5,191.671,57.293,32.166 -2020-03-06 19:15:00,136.65,190.049,57.293,32.166 -2020-03-06 19:30:00,132.19,188.83900000000003,57.293,32.166 -2020-03-06 19:45:00,128.54,185.19400000000002,57.293,32.166 -2020-03-06 20:00:00,118.1,179.83599999999998,59.433,32.166 -2020-03-06 20:15:00,120.22,174.738,59.433,32.166 -2020-03-06 20:30:00,116.22,170.981,59.433,32.166 -2020-03-06 20:45:00,116.32,168.983,59.433,32.166 -2020-03-06 21:00:00,108.24,166.416,52.153999999999996,32.166 -2020-03-06 21:15:00,107.96,163.793,52.153999999999996,32.166 -2020-03-06 21:30:00,103.98,161.531,52.153999999999996,32.166 -2020-03-06 21:45:00,100.92,161.953,52.153999999999996,32.166 -2020-03-06 22:00:00,101.26,155.461,47.125,32.166 -2020-03-06 22:15:00,98.2,151.312,47.125,32.166 -2020-03-06 22:30:00,91.48,142.222,47.125,32.166 -2020-03-06 22:45:00,86.47,138.014,47.125,32.166 -2020-03-06 23:00:00,80.32,131.118,41.236000000000004,32.166 -2020-03-06 23:15:00,86.56,127.30799999999999,41.236000000000004,32.166 -2020-03-06 23:30:00,86.05,127.225,41.236000000000004,32.166 -2020-03-06 23:45:00,84.17,126.74600000000001,41.236000000000004,32.166 -2020-03-07 00:00:00,73.36,112.361,36.484,31.988000000000003 -2020-03-07 00:15:00,72.69,106.662,36.484,31.988000000000003 -2020-03-07 00:30:00,73.63,106.99600000000001,36.484,31.988000000000003 -2020-03-07 00:45:00,79.66,107.662,36.484,31.988000000000003 -2020-03-07 01:00:00,75.3,109.476,32.391999999999996,31.988000000000003 -2020-03-07 01:15:00,77.06,109.846,32.391999999999996,31.988000000000003 -2020-03-07 01:30:00,71.97,109.189,32.391999999999996,31.988000000000003 -2020-03-07 01:45:00,68.58,109.354,32.391999999999996,31.988000000000003 -2020-03-07 02:00:00,73.65,112.00200000000001,30.194000000000003,31.988000000000003 -2020-03-07 02:15:00,74.65,112.369,30.194000000000003,31.988000000000003 -2020-03-07 02:30:00,69.35,112.65100000000001,30.194000000000003,31.988000000000003 -2020-03-07 02:45:00,68.86,114.555,30.194000000000003,31.988000000000003 -2020-03-07 03:00:00,66.7,116.7,29.677,31.988000000000003 -2020-03-07 03:15:00,65.75,117.959,29.677,31.988000000000003 -2020-03-07 03:30:00,70.1,118.37299999999999,29.677,31.988000000000003 -2020-03-07 03:45:00,72.68,120.28200000000001,29.677,31.988000000000003 -2020-03-07 04:00:00,68.24,129.653,29.616,31.988000000000003 -2020-03-07 04:15:00,67.2,139.745,29.616,31.988000000000003 -2020-03-07 04:30:00,67.39,139.829,29.616,31.988000000000003 -2020-03-07 04:45:00,67.69,140.015,29.616,31.988000000000003 -2020-03-07 05:00:00,67.99,156.97299999999998,29.625,31.988000000000003 -2020-03-07 05:15:00,67.86,169.953,29.625,31.988000000000003 -2020-03-07 05:30:00,67.93,165.817,29.625,31.988000000000003 -2020-03-07 05:45:00,70.0,163.291,29.625,31.988000000000003 -2020-03-07 06:00:00,71.25,180.298,30.551,31.988000000000003 -2020-03-07 06:15:00,72.04,201.768,30.551,31.988000000000003 -2020-03-07 06:30:00,72.04,197.2,30.551,31.988000000000003 -2020-03-07 06:45:00,72.96,192.865,30.551,31.988000000000003 -2020-03-07 07:00:00,76.51,190.575,34.865,31.988000000000003 -2020-03-07 07:15:00,78.94,193.25799999999998,34.865,31.988000000000003 -2020-03-07 07:30:00,81.32,195.71099999999998,34.865,31.988000000000003 -2020-03-07 07:45:00,83.9,197.054,34.865,31.988000000000003 -2020-03-07 08:00:00,88.57,197.97299999999998,41.456,31.988000000000003 -2020-03-07 08:15:00,89.24,199.271,41.456,31.988000000000003 -2020-03-07 08:30:00,90.68,196.99599999999998,41.456,31.988000000000003 -2020-03-07 08:45:00,94.24,194.13099999999997,41.456,31.988000000000003 -2020-03-07 09:00:00,96.14,188.805,43.001999999999995,31.988000000000003 -2020-03-07 09:15:00,98.61,186.863,43.001999999999995,31.988000000000003 -2020-03-07 09:30:00,102.12,186.041,43.001999999999995,31.988000000000003 -2020-03-07 09:45:00,98.59,183.317,43.001999999999995,31.988000000000003 -2020-03-07 10:00:00,95.01,179.757,42.047,31.988000000000003 -2020-03-07 10:15:00,98.58,177.519,42.047,31.988000000000003 -2020-03-07 10:30:00,95.97,175.25900000000001,42.047,31.988000000000003 -2020-03-07 10:45:00,94.89,175.265,42.047,31.988000000000003 -2020-03-07 11:00:00,95.66,172.479,39.894,31.988000000000003 -2020-03-07 11:15:00,96.05,170.417,39.894,31.988000000000003 -2020-03-07 11:30:00,95.32,170.642,39.894,31.988000000000003 -2020-03-07 11:45:00,95.01,169.364,39.894,31.988000000000003 -2020-03-07 12:00:00,95.78,164.368,38.122,31.988000000000003 -2020-03-07 12:15:00,92.41,163.267,38.122,31.988000000000003 -2020-03-07 12:30:00,86.18,163.02,38.122,31.988000000000003 -2020-03-07 12:45:00,85.12,163.636,38.122,31.988000000000003 -2020-03-07 13:00:00,87.26,163.61,34.645,31.988000000000003 -2020-03-07 13:15:00,86.32,160.77,34.645,31.988000000000003 -2020-03-07 13:30:00,84.06,158.97299999999998,34.645,31.988000000000003 -2020-03-07 13:45:00,83.33,158.702,34.645,31.988000000000003 -2020-03-07 14:00:00,77.3,160.404,33.739000000000004,31.988000000000003 -2020-03-07 14:15:00,74.26,158.83,33.739000000000004,31.988000000000003 -2020-03-07 14:30:00,74.73,157.79399999999998,33.739000000000004,31.988000000000003 -2020-03-07 14:45:00,79.24,159.227,33.739000000000004,31.988000000000003 -2020-03-07 15:00:00,79.87,161.2,35.908,31.988000000000003 -2020-03-07 15:15:00,82.29,160.34,35.908,31.988000000000003 -2020-03-07 15:30:00,82.88,160.406,35.908,31.988000000000003 -2020-03-07 15:45:00,83.16,160.681,35.908,31.988000000000003 -2020-03-07 16:00:00,82.92,162.039,39.249,31.988000000000003 -2020-03-07 16:15:00,82.39,164.894,39.249,31.988000000000003 -2020-03-07 16:30:00,84.42,166.97,39.249,31.988000000000003 -2020-03-07 16:45:00,86.48,168.02700000000002,39.249,31.988000000000003 -2020-03-07 17:00:00,89.54,170.46900000000002,46.045,31.988000000000003 -2020-03-07 17:15:00,90.25,174.037,46.045,31.988000000000003 -2020-03-07 17:30:00,93.17,176.52700000000002,46.045,31.988000000000003 -2020-03-07 17:45:00,96.07,177.01,46.045,31.988000000000003 -2020-03-07 18:00:00,104.2,180.562,48.238,31.988000000000003 -2020-03-07 18:15:00,106.48,182.665,48.238,31.988000000000003 -2020-03-07 18:30:00,104.75,182.90599999999998,48.238,31.988000000000003 -2020-03-07 18:45:00,105.22,181.287,48.238,31.988000000000003 -2020-03-07 19:00:00,104.77,181.752,46.785,31.988000000000003 -2020-03-07 19:15:00,101.74,179.548,46.785,31.988000000000003 -2020-03-07 19:30:00,103.23,179.188,46.785,31.988000000000003 -2020-03-07 19:45:00,99.14,175.61599999999999,46.785,31.988000000000003 -2020-03-07 20:00:00,94.25,172.472,39.830999999999996,31.988000000000003 -2020-03-07 20:15:00,90.48,169.38,39.830999999999996,31.988000000000003 -2020-03-07 20:30:00,88.69,165.206,39.830999999999996,31.988000000000003 -2020-03-07 20:45:00,89.27,163.041,39.830999999999996,31.988000000000003 -2020-03-07 21:00:00,82.31,162.475,34.063,31.988000000000003 -2020-03-07 21:15:00,82.13,160.234,34.063,31.988000000000003 -2020-03-07 21:30:00,81.1,159.189,34.063,31.988000000000003 -2020-03-07 21:45:00,79.97,159.173,34.063,31.988000000000003 -2020-03-07 22:00:00,79.99,153.97299999999998,34.455999999999996,31.988000000000003 -2020-03-07 22:15:00,77.88,152.298,34.455999999999996,31.988000000000003 -2020-03-07 22:30:00,73.56,149.155,34.455999999999996,31.988000000000003 -2020-03-07 22:45:00,72.67,146.81,34.455999999999996,31.988000000000003 -2020-03-07 23:00:00,68.96,142.2,27.840999999999998,31.988000000000003 -2020-03-07 23:15:00,69.09,136.869,27.840999999999998,31.988000000000003 -2020-03-07 23:30:00,65.76,135.38299999999998,27.840999999999998,31.988000000000003 -2020-03-07 23:45:00,64.86,132.614,27.840999999999998,31.988000000000003 -2020-03-08 00:00:00,61.0,112.605,20.007,31.988000000000003 -2020-03-08 00:15:00,61.21,106.455,20.007,31.988000000000003 -2020-03-08 00:30:00,60.15,106.39200000000001,20.007,31.988000000000003 -2020-03-08 00:45:00,60.07,107.709,20.007,31.988000000000003 -2020-03-08 01:00:00,56.47,109.37899999999999,17.378,31.988000000000003 -2020-03-08 01:15:00,57.99,110.706,17.378,31.988000000000003 -2020-03-08 01:30:00,57.0,110.509,17.378,31.988000000000003 -2020-03-08 01:45:00,57.02,110.329,17.378,31.988000000000003 -2020-03-08 02:00:00,55.64,112.262,16.145,31.988000000000003 -2020-03-08 02:15:00,56.91,111.91,16.145,31.988000000000003 -2020-03-08 02:30:00,56.07,113.05,16.145,31.988000000000003 -2020-03-08 02:45:00,56.9,115.353,16.145,31.988000000000003 -2020-03-08 03:00:00,55.26,117.9,15.427999999999999,31.988000000000003 -2020-03-08 03:15:00,55.5,118.68299999999999,15.427999999999999,31.988000000000003 -2020-03-08 03:30:00,53.73,120.31200000000001,15.427999999999999,31.988000000000003 -2020-03-08 03:45:00,55.64,122.04700000000001,15.427999999999999,31.988000000000003 -2020-03-08 04:00:00,55.63,131.18200000000002,16.663,31.988000000000003 -2020-03-08 04:15:00,54.6,140.243,16.663,31.988000000000003 -2020-03-08 04:30:00,57.94,140.602,16.663,31.988000000000003 -2020-03-08 04:45:00,58.3,140.964,16.663,31.988000000000003 -2020-03-08 05:00:00,57.65,154.621,17.271,31.988000000000003 -2020-03-08 05:15:00,59.1,165.231,17.271,31.988000000000003 -2020-03-08 05:30:00,56.61,160.846,17.271,31.988000000000003 -2020-03-08 05:45:00,59.93,158.515,17.271,31.988000000000003 -2020-03-08 06:00:00,60.8,175.07299999999998,17.612000000000002,31.988000000000003 -2020-03-08 06:15:00,60.98,195.047,17.612000000000002,31.988000000000003 -2020-03-08 06:30:00,59.58,189.28799999999998,17.612000000000002,31.988000000000003 -2020-03-08 06:45:00,60.73,183.79,17.612000000000002,31.988000000000003 -2020-03-08 07:00:00,64.07,183.87099999999998,20.88,31.988000000000003 -2020-03-08 07:15:00,64.46,185.475,20.88,31.988000000000003 -2020-03-08 07:30:00,67.14,186.912,20.88,31.988000000000003 -2020-03-08 07:45:00,68.83,187.49099999999999,20.88,31.988000000000003 -2020-03-08 08:00:00,70.55,190.217,25.861,31.988000000000003 -2020-03-08 08:15:00,68.92,191.58599999999998,25.861,31.988000000000003 -2020-03-08 08:30:00,68.33,190.949,25.861,31.988000000000003 -2020-03-08 08:45:00,68.61,190.00599999999997,25.861,31.988000000000003 -2020-03-08 09:00:00,68.42,184.287,27.921999999999997,31.988000000000003 -2020-03-08 09:15:00,68.88,182.803,27.921999999999997,31.988000000000003 -2020-03-08 09:30:00,66.98,181.915,27.921999999999997,31.988000000000003 -2020-03-08 09:45:00,66.66,179.234,27.921999999999997,31.988000000000003 -2020-03-08 10:00:00,63.08,178.157,29.048000000000002,31.988000000000003 -2020-03-08 10:15:00,65.17,176.486,29.048000000000002,31.988000000000003 -2020-03-08 10:30:00,66.69,174.85,29.048000000000002,31.988000000000003 -2020-03-08 10:45:00,68.39,173.22299999999998,29.048000000000002,31.988000000000003 -2020-03-08 11:00:00,71.32,171.245,32.02,31.988000000000003 -2020-03-08 11:15:00,68.73,169.267,32.02,31.988000000000003 -2020-03-08 11:30:00,77.87,168.745,32.02,31.988000000000003 -2020-03-08 11:45:00,71.63,168.09599999999998,32.02,31.988000000000003 -2020-03-08 12:00:00,69.65,162.733,28.55,31.988000000000003 -2020-03-08 12:15:00,64.14,163.364,28.55,31.988000000000003 -2020-03-08 12:30:00,62.02,161.72799999999998,28.55,31.988000000000003 -2020-03-08 12:45:00,62.53,161.333,28.55,31.988000000000003 -2020-03-08 13:00:00,59.46,160.626,25.601999999999997,31.988000000000003 -2020-03-08 13:15:00,59.09,160.545,25.601999999999997,31.988000000000003 -2020-03-08 13:30:00,59.19,158.379,25.601999999999997,31.988000000000003 -2020-03-08 13:45:00,58.93,157.644,25.601999999999997,31.988000000000003 -2020-03-08 14:00:00,57.84,159.814,23.916999999999998,31.988000000000003 -2020-03-08 14:15:00,58.46,159.417,23.916999999999998,31.988000000000003 -2020-03-08 14:30:00,62.93,159.34799999999998,23.916999999999998,31.988000000000003 -2020-03-08 14:45:00,64.38,160.249,23.916999999999998,31.988000000000003 -2020-03-08 15:00:00,64.9,160.799,24.064,31.988000000000003 -2020-03-08 15:15:00,65.65,160.517,24.064,31.988000000000003 -2020-03-08 15:30:00,65.05,161.069,24.064,31.988000000000003 -2020-03-08 15:45:00,64.57,162.025,24.064,31.988000000000003 -2020-03-08 16:00:00,67.33,164.97099999999998,28.189,31.988000000000003 -2020-03-08 16:15:00,68.58,166.96400000000003,28.189,31.988000000000003 -2020-03-08 16:30:00,68.93,169.421,28.189,31.988000000000003 -2020-03-08 16:45:00,71.27,170.572,28.189,31.988000000000003 -2020-03-08 17:00:00,75.95,173.09099999999998,37.576,31.988000000000003 -2020-03-08 17:15:00,76.2,176.542,37.576,31.988000000000003 -2020-03-08 17:30:00,80.05,179.435,37.576,31.988000000000003 -2020-03-08 17:45:00,83.25,182.171,37.576,31.988000000000003 -2020-03-08 18:00:00,91.18,185.28900000000002,42.669,31.988000000000003 -2020-03-08 18:15:00,96.39,188.69400000000002,42.669,31.988000000000003 -2020-03-08 18:30:00,101.38,186.88299999999998,42.669,31.988000000000003 -2020-03-08 18:45:00,99.75,187.09,42.669,31.988000000000003 -2020-03-08 19:00:00,97.87,187.46400000000003,43.538999999999994,31.988000000000003 -2020-03-08 19:15:00,93.75,185.72400000000002,43.538999999999994,31.988000000000003 -2020-03-08 19:30:00,94.16,185.204,43.538999999999994,31.988000000000003 -2020-03-08 19:45:00,89.68,183.025,43.538999999999994,31.988000000000003 -2020-03-08 20:00:00,86.68,179.81799999999998,37.330999999999996,31.988000000000003 -2020-03-08 20:15:00,86.27,177.678,37.330999999999996,31.988000000000003 -2020-03-08 20:30:00,83.87,174.827,37.330999999999996,31.988000000000003 -2020-03-08 20:45:00,83.17,171.297,37.330999999999996,31.988000000000003 -2020-03-08 21:00:00,79.53,168.202,33.856,31.988000000000003 -2020-03-08 21:15:00,80.3,165.315,33.856,31.988000000000003 -2020-03-08 21:30:00,80.55,164.47400000000002,33.856,31.988000000000003 -2020-03-08 21:45:00,85.28,164.655,33.856,31.988000000000003 -2020-03-08 22:00:00,87.48,158.54,34.711999999999996,31.988000000000003 -2020-03-08 22:15:00,88.51,155.951,34.711999999999996,31.988000000000003 -2020-03-08 22:30:00,83.88,149.629,34.711999999999996,31.988000000000003 -2020-03-08 22:45:00,79.79,146.309,34.711999999999996,31.988000000000003 -2020-03-08 23:00:00,77.62,138.95,29.698,31.988000000000003 -2020-03-08 23:15:00,82.04,135.591,29.698,31.988000000000003 -2020-03-08 23:30:00,82.56,134.83100000000002,29.698,31.988000000000003 -2020-03-08 23:45:00,79.6,132.972,29.698,31.988000000000003 -2020-03-09 00:00:00,69.92,116.464,29.983,32.166 -2020-03-09 00:15:00,69.51,113.265,29.983,32.166 -2020-03-09 00:30:00,71.32,113.25399999999999,29.983,32.166 -2020-03-09 00:45:00,76.73,114.006,29.983,32.166 -2020-03-09 01:00:00,74.19,115.686,29.122,32.166 -2020-03-09 01:15:00,73.87,116.49700000000001,29.122,32.166 -2020-03-09 01:30:00,67.47,116.385,29.122,32.166 -2020-03-09 01:45:00,74.37,116.307,29.122,32.166 -2020-03-09 02:00:00,73.55,118.26100000000001,28.676,32.166 -2020-03-09 02:15:00,73.53,119.147,28.676,32.166 -2020-03-09 02:30:00,68.13,120.645,28.676,32.166 -2020-03-09 02:45:00,75.19,122.339,28.676,32.166 -2020-03-09 03:00:00,75.27,126.15799999999999,26.552,32.166 -2020-03-09 03:15:00,74.56,128.586,26.552,32.166 -2020-03-09 03:30:00,73.02,130.048,26.552,32.166 -2020-03-09 03:45:00,77.49,131.209,26.552,32.166 -2020-03-09 04:00:00,77.93,144.89,27.44,32.166 -2020-03-09 04:15:00,76.54,158.27700000000002,27.44,32.166 -2020-03-09 04:30:00,74.57,160.655,27.44,32.166 -2020-03-09 04:45:00,74.33,161.21,27.44,32.166 -2020-03-09 05:00:00,77.36,190.71,36.825,32.166 -2020-03-09 05:15:00,83.26,221.83599999999998,36.825,32.166 -2020-03-09 05:30:00,86.0,217.45,36.825,32.166 -2020-03-09 05:45:00,90.09,209.351,36.825,32.166 -2020-03-09 06:00:00,98.46,207.56799999999998,56.589,32.166 -2020-03-09 06:15:00,105.15,212.236,56.589,32.166 -2020-03-09 06:30:00,108.02,214.56799999999998,56.589,32.166 -2020-03-09 06:45:00,110.16,218.112,56.589,32.166 -2020-03-09 07:00:00,117.31,220.562,67.49,32.166 -2020-03-09 07:15:00,118.79,223.637,67.49,32.166 -2020-03-09 07:30:00,122.33,224.175,67.49,32.166 -2020-03-09 07:45:00,125.44,222.329,67.49,32.166 -2020-03-09 08:00:00,129.3,220.19400000000002,60.028,32.166 -2020-03-09 08:15:00,131.05,219.35,60.028,32.166 -2020-03-09 08:30:00,131.67,214.641,60.028,32.166 -2020-03-09 08:45:00,131.5,210.576,60.028,32.166 -2020-03-09 09:00:00,130.44,203.824,55.018,32.166 -2020-03-09 09:15:00,134.15,198.77900000000002,55.018,32.166 -2020-03-09 09:30:00,133.09,196.834,55.018,32.166 -2020-03-09 09:45:00,134.3,194.14,55.018,32.166 -2020-03-09 10:00:00,134.13,192.09400000000002,51.183,32.166 -2020-03-09 10:15:00,134.12,190.137,51.183,32.166 -2020-03-09 10:30:00,134.67,187.627,51.183,32.166 -2020-03-09 10:45:00,135.0,186.516,51.183,32.166 -2020-03-09 11:00:00,136.7,182.109,50.065,32.166 -2020-03-09 11:15:00,135.2,181.91099999999997,50.065,32.166 -2020-03-09 11:30:00,135.31,182.80700000000002,50.065,32.166 -2020-03-09 11:45:00,135.27,181.85299999999998,50.065,32.166 -2020-03-09 12:00:00,136.04,177.92700000000002,48.141999999999996,32.166 -2020-03-09 12:15:00,132.64,178.58700000000002,48.141999999999996,32.166 -2020-03-09 12:30:00,129.11,177.00799999999998,48.141999999999996,32.166 -2020-03-09 12:45:00,129.79,178.063,48.141999999999996,32.166 -2020-03-09 13:00:00,129.66,178.076,47.887,32.166 -2020-03-09 13:15:00,129.83,176.532,47.887,32.166 -2020-03-09 13:30:00,129.35,173.873,47.887,32.166 -2020-03-09 13:45:00,131.32,173.292,47.887,32.166 -2020-03-09 14:00:00,126.29,174.843,48.571000000000005,32.166 -2020-03-09 14:15:00,126.44,173.885,48.571000000000005,32.166 -2020-03-09 14:30:00,128.22,173.24599999999998,48.571000000000005,32.166 -2020-03-09 14:45:00,126.5,174.40099999999998,48.571000000000005,32.166 -2020-03-09 15:00:00,128.64,176.653,49.937,32.166 -2020-03-09 15:15:00,125.18,174.889,49.937,32.166 -2020-03-09 15:30:00,124.09,174.695,49.937,32.166 -2020-03-09 15:45:00,123.67,175.143,49.937,32.166 -2020-03-09 16:00:00,123.86,178.435,52.963,32.166 -2020-03-09 16:15:00,124.07,179.68099999999998,52.963,32.166 -2020-03-09 16:30:00,120.57,181.12400000000002,52.963,32.166 -2020-03-09 16:45:00,120.89,181.093,52.963,32.166 -2020-03-09 17:00:00,124.22,183.347,61.163999999999994,32.166 -2020-03-09 17:15:00,125.08,185.929,61.163999999999994,32.166 -2020-03-09 17:30:00,129.2,188.269,61.163999999999994,32.166 -2020-03-09 17:45:00,128.13,189.52599999999998,61.163999999999994,32.166 -2020-03-09 18:00:00,133.54,192.96599999999998,63.788999999999994,32.166 -2020-03-09 18:15:00,133.96,194.083,63.788999999999994,32.166 -2020-03-09 18:30:00,130.46,192.81599999999997,63.788999999999994,32.166 -2020-03-09 18:45:00,129.43,194.101,63.788999999999994,32.166 -2020-03-09 19:00:00,129.65,192.855,63.913000000000004,32.166 -2020-03-09 19:15:00,125.51,190.139,63.913000000000004,32.166 -2020-03-09 19:30:00,133.89,190.092,63.913000000000004,32.166 -2020-03-09 19:45:00,132.07,187.08,63.913000000000004,32.166 -2020-03-09 20:00:00,119.33,181.389,65.44,32.166 -2020-03-09 20:15:00,115.81,177.02200000000002,65.44,32.166 -2020-03-09 20:30:00,115.88,172.455,65.44,32.166 -2020-03-09 20:45:00,115.72,170.53,65.44,32.166 -2020-03-09 21:00:00,105.11,167.868,59.117,32.166 -2020-03-09 21:15:00,99.97,163.887,59.117,32.166 -2020-03-09 21:30:00,98.25,162.292,59.117,32.166 -2020-03-09 21:45:00,97.32,161.97799999999998,59.117,32.166 -2020-03-09 22:00:00,92.52,152.808,52.301,32.166 -2020-03-09 22:15:00,93.86,149.191,52.301,32.166 -2020-03-09 22:30:00,94.64,133.034,52.301,32.166 -2020-03-09 22:45:00,93.49,125.016,52.301,32.166 -2020-03-09 23:00:00,89.93,118.40299999999999,44.373000000000005,32.166 -2020-03-09 23:15:00,83.28,117.455,44.373000000000005,32.166 -2020-03-09 23:30:00,82.01,119.383,44.373000000000005,32.166 -2020-03-09 23:45:00,79.61,120.059,44.373000000000005,32.166 -2020-03-10 00:00:00,79.63,115.156,44.647,32.166 -2020-03-10 00:15:00,81.95,113.444,44.647,32.166 -2020-03-10 00:30:00,80.93,112.60600000000001,44.647,32.166 -2020-03-10 00:45:00,77.16,112.525,44.647,32.166 -2020-03-10 01:00:00,70.56,113.905,41.433,32.166 -2020-03-10 01:15:00,76.76,114.289,41.433,32.166 -2020-03-10 01:30:00,76.51,114.307,41.433,32.166 -2020-03-10 01:45:00,79.3,114.46,41.433,32.166 -2020-03-10 02:00:00,73.31,116.336,39.909,32.166 -2020-03-10 02:15:00,73.05,117.262,39.909,32.166 -2020-03-10 02:30:00,69.81,118.15700000000001,39.909,32.166 -2020-03-10 02:45:00,71.57,119.90700000000001,39.909,32.166 -2020-03-10 03:00:00,71.1,122.52,39.14,32.166 -2020-03-10 03:15:00,78.8,124.244,39.14,32.166 -2020-03-10 03:30:00,81.43,126.156,39.14,32.166 -2020-03-10 03:45:00,78.81,127.365,39.14,32.166 -2020-03-10 04:00:00,75.34,140.69799999999998,40.015,32.166 -2020-03-10 04:15:00,74.56,153.744,40.015,32.166 -2020-03-10 04:30:00,80.72,155.83,40.015,32.166 -2020-03-10 04:45:00,85.85,157.588,40.015,32.166 -2020-03-10 05:00:00,89.69,192.037,44.93600000000001,32.166 -2020-03-10 05:15:00,87.41,223.071,44.93600000000001,32.166 -2020-03-10 05:30:00,94.15,217.21900000000002,44.93600000000001,32.166 -2020-03-10 05:45:00,99.31,209.03400000000002,44.93600000000001,32.166 -2020-03-10 06:00:00,108.19,206.273,57.271,32.166 -2020-03-10 06:15:00,106.67,212.52,57.271,32.166 -2020-03-10 06:30:00,105.56,214.145,57.271,32.166 -2020-03-10 06:45:00,110.72,217.18900000000002,57.271,32.166 -2020-03-10 07:00:00,114.85,219.50599999999997,68.352,32.166 -2020-03-10 07:15:00,122.4,222.36,68.352,32.166 -2020-03-10 07:30:00,126.34,222.338,68.352,32.166 -2020-03-10 07:45:00,126.87,220.52700000000002,68.352,32.166 -2020-03-10 08:00:00,121.17,218.46200000000002,60.717,32.166 -2020-03-10 08:15:00,121.73,216.57,60.717,32.166 -2020-03-10 08:30:00,123.57,211.655,60.717,32.166 -2020-03-10 08:45:00,119.88,207.236,60.717,32.166 -2020-03-10 09:00:00,120.24,199.739,54.603,32.166 -2020-03-10 09:15:00,125.53,196.215,54.603,32.166 -2020-03-10 09:30:00,125.18,195.02900000000002,54.603,32.166 -2020-03-10 09:45:00,117.63,192.30599999999998,54.603,32.166 -2020-03-10 10:00:00,122.65,189.53900000000002,52.308,32.166 -2020-03-10 10:15:00,122.53,186.588,52.308,32.166 -2020-03-10 10:30:00,122.83,184.278,52.308,32.166 -2020-03-10 10:45:00,123.85,183.583,52.308,32.166 -2020-03-10 11:00:00,124.85,180.6,51.838,32.166 -2020-03-10 11:15:00,124.63,180.15099999999998,51.838,32.166 -2020-03-10 11:30:00,120.0,179.793,51.838,32.166 -2020-03-10 11:45:00,112.97,179.477,51.838,32.166 -2020-03-10 12:00:00,116.62,174.23,50.375,32.166 -2020-03-10 12:15:00,124.04,174.543,50.375,32.166 -2020-03-10 12:30:00,134.05,173.727,50.375,32.166 -2020-03-10 12:45:00,127.79,174.581,50.375,32.166 -2020-03-10 13:00:00,126.21,174.203,50.735,32.166 -2020-03-10 13:15:00,131.91,172.525,50.735,32.166 -2020-03-10 13:30:00,125.28,170.977,50.735,32.166 -2020-03-10 13:45:00,123.2,170.459,50.735,32.166 -2020-03-10 14:00:00,122.54,172.40599999999998,50.946000000000005,32.166 -2020-03-10 14:15:00,121.23,171.549,50.946000000000005,32.166 -2020-03-10 14:30:00,124.05,171.524,50.946000000000005,32.166 -2020-03-10 14:45:00,124.51,172.513,50.946000000000005,32.166 -2020-03-10 15:00:00,131.26,174.335,53.18,32.166 -2020-03-10 15:15:00,123.68,172.97299999999998,53.18,32.166 -2020-03-10 15:30:00,117.32,172.918,53.18,32.166 -2020-03-10 15:45:00,118.3,173.00099999999998,53.18,32.166 -2020-03-10 16:00:00,122.14,176.535,54.928999999999995,32.166 -2020-03-10 16:15:00,120.8,178.231,54.928999999999995,32.166 -2020-03-10 16:30:00,123.17,180.257,54.928999999999995,32.166 -2020-03-10 16:45:00,120.87,180.545,54.928999999999995,32.166 -2020-03-10 17:00:00,119.71,183.364,60.913000000000004,32.166 -2020-03-10 17:15:00,128.27,186.02700000000002,60.913000000000004,32.166 -2020-03-10 17:30:00,128.23,188.989,60.913000000000004,32.166 -2020-03-10 17:45:00,132.22,190.09599999999998,60.913000000000004,32.166 -2020-03-10 18:00:00,133.86,193.33900000000003,62.214,32.166 -2020-03-10 18:15:00,138.61,194.197,62.214,32.166 -2020-03-10 18:30:00,141.3,192.59599999999998,62.214,32.166 -2020-03-10 18:45:00,137.8,194.65900000000002,62.214,32.166 -2020-03-10 19:00:00,133.97,193.333,62.38,32.166 -2020-03-10 19:15:00,133.09,190.37900000000002,62.38,32.166 -2020-03-10 19:30:00,134.88,189.68400000000003,62.38,32.166 -2020-03-10 19:45:00,130.24,186.79,62.38,32.166 -2020-03-10 20:00:00,124.19,181.265,65.018,32.166 -2020-03-10 20:15:00,120.85,176.13299999999998,65.018,32.166 -2020-03-10 20:30:00,119.04,172.58599999999998,65.018,32.166 -2020-03-10 20:45:00,115.01,170.15200000000002,65.018,32.166 -2020-03-10 21:00:00,110.97,166.892,56.416000000000004,32.166 -2020-03-10 21:15:00,109.67,163.63299999999998,56.416000000000004,32.166 -2020-03-10 21:30:00,108.54,161.345,56.416000000000004,32.166 -2020-03-10 21:45:00,105.86,161.29399999999998,56.416000000000004,32.166 -2020-03-10 22:00:00,98.25,153.8,52.846000000000004,32.166 -2020-03-10 22:15:00,100.43,149.92,52.846000000000004,32.166 -2020-03-10 22:30:00,97.18,133.826,52.846000000000004,32.166 -2020-03-10 22:45:00,91.4,126.08,52.846000000000004,32.166 -2020-03-10 23:00:00,89.03,119.40799999999999,44.435,32.166 -2020-03-10 23:15:00,91.06,117.764,44.435,32.166 -2020-03-10 23:30:00,88.04,119.34700000000001,44.435,32.166 -2020-03-10 23:45:00,81.48,119.641,44.435,32.166 -2020-03-11 00:00:00,75.53,114.79700000000001,42.527,32.166 -2020-03-11 00:15:00,80.59,113.096,42.527,32.166 -2020-03-11 00:30:00,83.26,112.241,42.527,32.166 -2020-03-11 00:45:00,83.5,112.162,42.527,32.166 -2020-03-11 01:00:00,75.29,113.49700000000001,38.655,32.166 -2020-03-11 01:15:00,74.08,113.866,38.655,32.166 -2020-03-11 01:30:00,75.7,113.865,38.655,32.166 -2020-03-11 01:45:00,74.92,114.025,38.655,32.166 -2020-03-11 02:00:00,74.13,115.889,36.912,32.166 -2020-03-11 02:15:00,81.05,116.80799999999999,36.912,32.166 -2020-03-11 02:30:00,80.33,117.723,36.912,32.166 -2020-03-11 02:45:00,79.51,119.47200000000001,36.912,32.166 -2020-03-11 03:00:00,75.06,122.09899999999999,36.98,32.166 -2020-03-11 03:15:00,75.42,123.804,36.98,32.166 -2020-03-11 03:30:00,76.42,125.709,36.98,32.166 -2020-03-11 03:45:00,80.52,126.93299999999999,36.98,32.166 -2020-03-11 04:00:00,84.62,140.27200000000002,38.052,32.166 -2020-03-11 04:15:00,85.72,153.308,38.052,32.166 -2020-03-11 04:30:00,81.45,155.411,38.052,32.166 -2020-03-11 04:45:00,84.39,157.156,38.052,32.166 -2020-03-11 05:00:00,91.07,191.585,42.455,32.166 -2020-03-11 05:15:00,95.13,222.638,42.455,32.166 -2020-03-11 05:30:00,98.48,216.752,42.455,32.166 -2020-03-11 05:45:00,96.19,208.571,42.455,32.166 -2020-03-11 06:00:00,104.58,205.817,57.986000000000004,32.166 -2020-03-11 06:15:00,107.88,212.06900000000002,57.986000000000004,32.166 -2020-03-11 06:30:00,110.59,213.645,57.986000000000004,32.166 -2020-03-11 06:45:00,113.78,216.666,57.986000000000004,32.166 -2020-03-11 07:00:00,118.72,219.00400000000002,71.868,32.166 -2020-03-11 07:15:00,127.45,221.824,71.868,32.166 -2020-03-11 07:30:00,131.59,221.76,71.868,32.166 -2020-03-11 07:45:00,132.28,219.90900000000002,71.868,32.166 -2020-03-11 08:00:00,129.65,217.81900000000002,62.225,32.166 -2020-03-11 08:15:00,131.66,215.915,62.225,32.166 -2020-03-11 08:30:00,130.64,210.942,62.225,32.166 -2020-03-11 08:45:00,131.27,206.542,62.225,32.166 -2020-03-11 09:00:00,134.97,199.05900000000003,58.802,32.166 -2020-03-11 09:15:00,139.13,195.53599999999997,58.802,32.166 -2020-03-11 09:30:00,141.53,194.37099999999998,58.802,32.166 -2020-03-11 09:45:00,138.32,191.662,58.802,32.166 -2020-03-11 10:00:00,134.68,188.90900000000002,54.122,32.166 -2020-03-11 10:15:00,135.2,186.00400000000002,54.122,32.166 -2020-03-11 10:30:00,134.26,183.71599999999998,54.122,32.166 -2020-03-11 10:45:00,133.84,183.042,54.122,32.166 -2020-03-11 11:00:00,133.84,180.051,54.368,32.166 -2020-03-11 11:15:00,136.34,179.623,54.368,32.166 -2020-03-11 11:30:00,134.04,179.273,54.368,32.166 -2020-03-11 11:45:00,134.3,178.97400000000002,54.368,32.166 -2020-03-11 12:00:00,136.13,173.747,52.74,32.166 -2020-03-11 12:15:00,139.12,174.074,52.74,32.166 -2020-03-11 12:30:00,137.79,173.21900000000002,52.74,32.166 -2020-03-11 12:45:00,132.99,174.07,52.74,32.166 -2020-03-11 13:00:00,129.17,173.736,52.544,32.166 -2020-03-11 13:15:00,130.87,172.03599999999997,52.544,32.166 -2020-03-11 13:30:00,132.75,170.476,52.544,32.166 -2020-03-11 13:45:00,134.79,169.96099999999998,52.544,32.166 -2020-03-11 14:00:00,129.47,171.979,53.602,32.166 -2020-03-11 14:15:00,126.72,171.09799999999998,53.602,32.166 -2020-03-11 14:30:00,128.13,171.03900000000002,53.602,32.166 -2020-03-11 14:45:00,124.89,172.03799999999998,53.602,32.166 -2020-03-11 15:00:00,129.33,173.86900000000003,55.59,32.166 -2020-03-11 15:15:00,130.78,172.476,55.59,32.166 -2020-03-11 15:30:00,132.29,172.37,55.59,32.166 -2020-03-11 15:45:00,130.2,172.433,55.59,32.166 -2020-03-11 16:00:00,126.12,175.97400000000002,57.586999999999996,32.166 -2020-03-11 16:15:00,122.31,177.64700000000002,57.586999999999996,32.166 -2020-03-11 16:30:00,122.71,179.671,57.586999999999996,32.166 -2020-03-11 16:45:00,123.16,179.915,57.586999999999996,32.166 -2020-03-11 17:00:00,125.12,182.75099999999998,62.111999999999995,32.166 -2020-03-11 17:15:00,128.58,185.418,62.111999999999995,32.166 -2020-03-11 17:30:00,127.81,188.40200000000002,62.111999999999995,32.166 -2020-03-11 17:45:00,130.41,189.523,62.111999999999995,32.166 -2020-03-11 18:00:00,142.61,192.77200000000002,64.605,32.166 -2020-03-11 18:15:00,140.13,193.701,64.605,32.166 -2020-03-11 18:30:00,143.86,192.095,64.605,32.166 -2020-03-11 18:45:00,134.22,194.179,64.605,32.166 -2020-03-11 19:00:00,135.54,192.81400000000002,65.55199999999999,32.166 -2020-03-11 19:15:00,132.58,189.88,65.55199999999999,32.166 -2020-03-11 19:30:00,135.41,189.21400000000003,65.55199999999999,32.166 -2020-03-11 19:45:00,134.32,186.36900000000003,65.55199999999999,32.166 -2020-03-11 20:00:00,127.68,180.81099999999998,66.778,32.166 -2020-03-11 20:15:00,121.46,175.696,66.778,32.166 -2020-03-11 20:30:00,114.11,172.178,66.778,32.166 -2020-03-11 20:45:00,112.76,169.74599999999998,66.778,32.166 -2020-03-11 21:00:00,103.63,166.476,56.103,32.166 -2020-03-11 21:15:00,103.76,163.215,56.103,32.166 -2020-03-11 21:30:00,107.96,160.925,56.103,32.166 -2020-03-11 21:45:00,109.11,160.89700000000002,56.103,32.166 -2020-03-11 22:00:00,106.52,153.386,51.371,32.166 -2020-03-11 22:15:00,101.13,149.535,51.371,32.166 -2020-03-11 22:30:00,100.38,133.38299999999998,51.371,32.166 -2020-03-11 22:45:00,98.36,125.639,51.371,32.166 -2020-03-11 23:00:00,94.04,118.964,42.798,32.166 -2020-03-11 23:15:00,90.56,117.34100000000001,42.798,32.166 -2020-03-11 23:30:00,90.47,118.929,42.798,32.166 -2020-03-11 23:45:00,92.85,119.251,42.798,32.166 -2020-03-12 00:00:00,90.26,114.434,39.069,32.166 -2020-03-12 00:15:00,83.72,112.744,39.069,32.166 -2020-03-12 00:30:00,85.51,111.87,39.069,32.166 -2020-03-12 00:45:00,85.74,111.79299999999999,39.069,32.166 -2020-03-12 01:00:00,82.15,113.084,37.043,32.166 -2020-03-12 01:15:00,81.61,113.43700000000001,37.043,32.166 -2020-03-12 01:30:00,83.56,113.417,37.043,32.166 -2020-03-12 01:45:00,83.38,113.586,37.043,32.166 -2020-03-12 02:00:00,79.78,115.43700000000001,34.625,32.166 -2020-03-12 02:15:00,79.93,116.348,34.625,32.166 -2020-03-12 02:30:00,80.09,117.28200000000001,34.625,32.166 -2020-03-12 02:45:00,83.47,119.03299999999999,34.625,32.166 -2020-03-12 03:00:00,79.75,121.67200000000001,33.812,32.166 -2020-03-12 03:15:00,83.53,123.359,33.812,32.166 -2020-03-12 03:30:00,85.78,125.258,33.812,32.166 -2020-03-12 03:45:00,86.38,126.495,33.812,32.166 -2020-03-12 04:00:00,83.32,139.841,35.236999999999995,32.166 -2020-03-12 04:15:00,85.25,152.864,35.236999999999995,32.166 -2020-03-12 04:30:00,89.13,154.987,35.236999999999995,32.166 -2020-03-12 04:45:00,87.85,156.718,35.236999999999995,32.166 -2020-03-12 05:00:00,87.66,191.12900000000002,40.375,32.166 -2020-03-12 05:15:00,89.16,222.2,40.375,32.166 -2020-03-12 05:30:00,92.45,216.27900000000002,40.375,32.166 -2020-03-12 05:45:00,94.7,208.10299999999998,40.375,32.166 -2020-03-12 06:00:00,106.37,205.355,52.316,32.166 -2020-03-12 06:15:00,109.43,211.613,52.316,32.166 -2020-03-12 06:30:00,110.52,213.139,52.316,32.166 -2020-03-12 06:45:00,112.03,216.136,52.316,32.166 -2020-03-12 07:00:00,115.95,218.495,64.115,32.166 -2020-03-12 07:15:00,118.25,221.282,64.115,32.166 -2020-03-12 07:30:00,121.19,221.176,64.115,32.166 -2020-03-12 07:45:00,121.25,219.28400000000002,64.115,32.166 -2020-03-12 08:00:00,121.28,217.168,55.033,32.166 -2020-03-12 08:15:00,120.35,215.253,55.033,32.166 -2020-03-12 08:30:00,121.03,210.222,55.033,32.166 -2020-03-12 08:45:00,122.01,205.84099999999998,55.033,32.166 -2020-03-12 09:00:00,121.47,198.373,49.411,32.166 -2020-03-12 09:15:00,118.49,194.851,49.411,32.166 -2020-03-12 09:30:00,119.35,193.707,49.411,32.166 -2020-03-12 09:45:00,117.69,191.012,49.411,32.166 -2020-03-12 10:00:00,118.5,188.274,45.82899999999999,32.166 -2020-03-12 10:15:00,119.49,185.41400000000002,45.82899999999999,32.166 -2020-03-12 10:30:00,118.65,183.149,45.82899999999999,32.166 -2020-03-12 10:45:00,120.54,182.495,45.82899999999999,32.166 -2020-03-12 11:00:00,120.4,179.497,44.333,32.166 -2020-03-12 11:15:00,120.2,179.092,44.333,32.166 -2020-03-12 11:30:00,120.88,178.748,44.333,32.166 -2020-03-12 11:45:00,118.34,178.46900000000002,44.333,32.166 -2020-03-12 12:00:00,117.08,173.262,42.95,32.166 -2020-03-12 12:15:00,116.25,173.6,42.95,32.166 -2020-03-12 12:30:00,115.75,172.706,42.95,32.166 -2020-03-12 12:45:00,109.47,173.554,42.95,32.166 -2020-03-12 13:00:00,108.09,173.266,42.489,32.166 -2020-03-12 13:15:00,115.15,171.544,42.489,32.166 -2020-03-12 13:30:00,115.05,169.97099999999998,42.489,32.166 -2020-03-12 13:45:00,111.73,169.46099999999998,42.489,32.166 -2020-03-12 14:00:00,112.25,171.55,43.448,32.166 -2020-03-12 14:15:00,113.98,170.643,43.448,32.166 -2020-03-12 14:30:00,117.08,170.551,43.448,32.166 -2020-03-12 14:45:00,119.53,171.55900000000003,43.448,32.166 -2020-03-12 15:00:00,118.97,173.39700000000002,45.994,32.166 -2020-03-12 15:15:00,118.68,171.975,45.994,32.166 -2020-03-12 15:30:00,117.13,171.81599999999997,45.994,32.166 -2020-03-12 15:45:00,119.59,171.862,45.994,32.166 -2020-03-12 16:00:00,119.06,175.407,48.167,32.166 -2020-03-12 16:15:00,118.71,177.05700000000002,48.167,32.166 -2020-03-12 16:30:00,116.06,179.081,48.167,32.166 -2020-03-12 16:45:00,116.15,179.28099999999998,48.167,32.166 -2020-03-12 17:00:00,117.1,182.13099999999997,52.637,32.166 -2020-03-12 17:15:00,117.47,184.803,52.637,32.166 -2020-03-12 17:30:00,124.27,187.81,52.637,32.166 -2020-03-12 17:45:00,130.48,188.94400000000002,52.637,32.166 -2020-03-12 18:00:00,135.41,192.19799999999998,55.739,32.166 -2020-03-12 18:15:00,134.81,193.199,55.739,32.166 -2020-03-12 18:30:00,135.02,191.58900000000003,55.739,32.166 -2020-03-12 18:45:00,141.44,193.692,55.739,32.166 -2020-03-12 19:00:00,139.92,192.29,56.36600000000001,32.166 -2020-03-12 19:15:00,135.83,189.375,56.36600000000001,32.166 -2020-03-12 19:30:00,133.89,188.739,56.36600000000001,32.166 -2020-03-12 19:45:00,136.57,185.94099999999997,56.36600000000001,32.166 -2020-03-12 20:00:00,127.47,180.352,56.338,32.166 -2020-03-12 20:15:00,117.38,175.25400000000002,56.338,32.166 -2020-03-12 20:30:00,114.25,171.765,56.338,32.166 -2020-03-12 20:45:00,118.49,169.33599999999998,56.338,32.166 -2020-03-12 21:00:00,110.15,166.05599999999998,49.894,32.166 -2020-03-12 21:15:00,109.53,162.791,49.894,32.166 -2020-03-12 21:30:00,99.6,160.502,49.894,32.166 -2020-03-12 21:45:00,103.89,160.494,49.894,32.166 -2020-03-12 22:00:00,96.79,152.968,46.687,32.166 -2020-03-12 22:15:00,101.17,149.144,46.687,32.166 -2020-03-12 22:30:00,99.91,132.936,46.687,32.166 -2020-03-12 22:45:00,97.37,125.19200000000001,46.687,32.166 -2020-03-12 23:00:00,90.19,118.515,39.211,32.166 -2020-03-12 23:15:00,91.27,116.912,39.211,32.166 -2020-03-12 23:30:00,89.72,118.506,39.211,32.166 -2020-03-12 23:45:00,90.08,118.85600000000001,39.211,32.166 -2020-03-13 00:00:00,79.34,112.801,36.616,32.166 -2020-03-13 00:15:00,81.17,111.344,36.616,32.166 -2020-03-13 00:30:00,84.9,110.35700000000001,36.616,32.166 -2020-03-13 00:45:00,84.62,110.445,36.616,32.166 -2020-03-13 01:00:00,76.48,111.34,33.799,32.166 -2020-03-13 01:15:00,75.14,112.479,33.799,32.166 -2020-03-13 01:30:00,75.63,112.32,33.799,32.166 -2020-03-13 01:45:00,78.52,112.565,33.799,32.166 -2020-03-13 02:00:00,80.3,114.60600000000001,32.968,32.166 -2020-03-13 02:15:00,78.45,115.39399999999999,32.968,32.166 -2020-03-13 02:30:00,75.39,116.959,32.968,32.166 -2020-03-13 02:45:00,74.75,118.661,32.968,32.166 -2020-03-13 03:00:00,80.8,120.464,33.533,32.166 -2020-03-13 03:15:00,81.43,122.869,33.533,32.166 -2020-03-13 03:30:00,80.63,124.714,33.533,32.166 -2020-03-13 03:45:00,76.96,126.39299999999999,33.533,32.166 -2020-03-13 04:00:00,77.78,139.984,36.102,32.166 -2020-03-13 04:15:00,76.01,152.549,36.102,32.166 -2020-03-13 04:30:00,78.16,155.04,36.102,32.166 -2020-03-13 04:45:00,80.35,155.576,36.102,32.166 -2020-03-13 05:00:00,83.6,188.688,42.423,32.166 -2020-03-13 05:15:00,86.08,221.351,42.423,32.166 -2020-03-13 05:30:00,89.34,216.452,42.423,32.166 -2020-03-13 05:45:00,93.73,208.157,42.423,32.166 -2020-03-13 06:00:00,103.64,205.862,55.38,32.166 -2020-03-13 06:15:00,107.57,210.74,55.38,32.166 -2020-03-13 06:30:00,110.64,211.385,55.38,32.166 -2020-03-13 06:45:00,114.48,215.893,55.38,32.166 -2020-03-13 07:00:00,119.66,217.563,65.929,32.166 -2020-03-13 07:15:00,122.04,221.418,65.929,32.166 -2020-03-13 07:30:00,125.84,220.833,65.929,32.166 -2020-03-13 07:45:00,127.92,218.025,65.929,32.166 -2020-03-13 08:00:00,130.98,214.92700000000002,57.336999999999996,32.166 -2020-03-13 08:15:00,131.41,212.718,57.336999999999996,32.166 -2020-03-13 08:30:00,133.25,208.553,57.336999999999996,32.166 -2020-03-13 08:45:00,132.73,202.65900000000002,57.336999999999996,32.166 -2020-03-13 09:00:00,131.91,195.34099999999998,54.226000000000006,32.166 -2020-03-13 09:15:00,133.92,192.61900000000003,54.226000000000006,32.166 -2020-03-13 09:30:00,134.68,190.99200000000002,54.226000000000006,32.166 -2020-03-13 09:45:00,135.73,188.257,54.226000000000006,32.166 -2020-03-13 10:00:00,134.92,184.42,51.298,32.166 -2020-03-13 10:15:00,135.4,182.248,51.298,32.166 -2020-03-13 10:30:00,134.47,180.012,51.298,32.166 -2020-03-13 10:45:00,135.19,178.93900000000002,51.298,32.166 -2020-03-13 11:00:00,135.89,175.94299999999998,50.839,32.166 -2020-03-13 11:15:00,138.7,174.553,50.839,32.166 -2020-03-13 11:30:00,137.93,175.858,50.839,32.166 -2020-03-13 11:45:00,136.68,175.517,50.839,32.166 -2020-03-13 12:00:00,135.2,171.43099999999998,47.976000000000006,32.166 -2020-03-13 12:15:00,133.97,169.679,47.976000000000006,32.166 -2020-03-13 12:30:00,132.57,168.91400000000002,47.976000000000006,32.166 -2020-03-13 12:45:00,133.27,170.15200000000002,47.976000000000006,32.166 -2020-03-13 13:00:00,131.19,170.889,46.299,32.166 -2020-03-13 13:15:00,131.49,169.957,46.299,32.166 -2020-03-13 13:30:00,130.77,168.495,46.299,32.166 -2020-03-13 13:45:00,129.96,167.967,46.299,32.166 -2020-03-13 14:00:00,127.92,168.924,44.971000000000004,32.166 -2020-03-13 14:15:00,127.13,167.878,44.971000000000004,32.166 -2020-03-13 14:30:00,128.48,168.445,44.971000000000004,32.166 -2020-03-13 14:45:00,133.54,169.662,44.971000000000004,32.166 -2020-03-13 15:00:00,132.39,171.033,47.48,32.166 -2020-03-13 15:15:00,129.23,169.125,47.48,32.166 -2020-03-13 15:30:00,123.71,167.375,47.48,32.166 -2020-03-13 15:45:00,127.43,167.642,47.48,32.166 -2020-03-13 16:00:00,125.11,169.949,50.648,32.166 -2020-03-13 16:15:00,125.31,171.919,50.648,32.166 -2020-03-13 16:30:00,131.08,174.01,50.648,32.166 -2020-03-13 16:45:00,131.36,173.954,50.648,32.166 -2020-03-13 17:00:00,133.17,177.222,56.251000000000005,32.166 -2020-03-13 17:15:00,127.14,179.476,56.251000000000005,32.166 -2020-03-13 17:30:00,131.27,182.215,56.251000000000005,32.166 -2020-03-13 17:45:00,129.12,183.114,56.251000000000005,32.166 -2020-03-13 18:00:00,131.24,187.041,58.982,32.166 -2020-03-13 18:15:00,135.17,187.62099999999998,58.982,32.166 -2020-03-13 18:30:00,139.57,186.37900000000002,58.982,32.166 -2020-03-13 18:45:00,139.51,188.546,58.982,32.166 -2020-03-13 19:00:00,133.36,188.09,57.293,32.166 -2020-03-13 19:15:00,128.85,186.59799999999998,57.293,32.166 -2020-03-13 19:30:00,132.49,185.59400000000002,57.293,32.166 -2020-03-13 19:45:00,126.88,182.27900000000002,57.293,32.166 -2020-03-13 20:00:00,119.57,176.699,59.433,32.166 -2020-03-13 20:15:00,115.51,171.71900000000002,59.433,32.166 -2020-03-13 20:30:00,114.29,168.16099999999997,59.433,32.166 -2020-03-13 20:45:00,118.37,166.18200000000002,59.433,32.166 -2020-03-13 21:00:00,112.96,163.543,52.153999999999996,32.166 -2020-03-13 21:15:00,110.25,160.898,52.153999999999996,32.166 -2020-03-13 21:30:00,97.71,158.635,52.153999999999996,32.166 -2020-03-13 21:45:00,100.27,159.203,52.153999999999996,32.166 -2020-03-13 22:00:00,97.93,152.60299999999998,47.125,32.166 -2020-03-13 22:15:00,92.71,148.64700000000002,47.125,32.166 -2020-03-13 22:30:00,96.04,139.16299999999998,47.125,32.166 -2020-03-13 22:45:00,96.36,134.97,47.125,32.166 -2020-03-13 23:00:00,92.48,128.05200000000002,41.236000000000004,32.166 -2020-03-13 23:15:00,86.81,124.382,41.236000000000004,32.166 -2020-03-13 23:30:00,83.75,124.344,41.236000000000004,32.166 -2020-03-13 23:45:00,80.73,124.059,41.236000000000004,32.166 -2020-03-14 00:00:00,77.88,109.852,36.484,31.988000000000003 -2020-03-14 00:15:00,83.78,104.229,36.484,31.988000000000003 -2020-03-14 00:30:00,82.86,104.44,36.484,31.988000000000003 -2020-03-14 00:45:00,81.65,105.12,36.484,31.988000000000003 -2020-03-14 01:00:00,70.41,106.62100000000001,32.391999999999996,31.988000000000003 -2020-03-14 01:15:00,73.58,106.88,32.391999999999996,31.988000000000003 -2020-03-14 01:30:00,71.51,106.098,32.391999999999996,31.988000000000003 -2020-03-14 01:45:00,71.4,106.311,32.391999999999996,31.988000000000003 -2020-03-14 02:00:00,70.84,108.875,30.194000000000003,31.988000000000003 -2020-03-14 02:15:00,70.05,109.193,30.194000000000003,31.988000000000003 -2020-03-14 02:30:00,68.78,109.60700000000001,30.194000000000003,31.988000000000003 -2020-03-14 02:45:00,70.66,111.51299999999999,30.194000000000003,31.988000000000003 -2020-03-14 03:00:00,74.8,113.75299999999999,29.677,31.988000000000003 -2020-03-14 03:15:00,77.05,114.881,29.677,31.988000000000003 -2020-03-14 03:30:00,75.79,115.251,29.677,31.988000000000003 -2020-03-14 03:45:00,74.51,117.256,29.677,31.988000000000003 -2020-03-14 04:00:00,73.14,126.67299999999999,29.616,31.988000000000003 -2020-03-14 04:15:00,76.99,136.685,29.616,31.988000000000003 -2020-03-14 04:30:00,79.29,136.898,29.616,31.988000000000003 -2020-03-14 04:45:00,79.22,136.989,29.616,31.988000000000003 -2020-03-14 05:00:00,73.08,153.809,29.625,31.988000000000003 -2020-03-14 05:15:00,73.44,166.923,29.625,31.988000000000003 -2020-03-14 05:30:00,78.54,162.54399999999998,29.625,31.988000000000003 -2020-03-14 05:45:00,82.22,160.054,29.625,31.988000000000003 -2020-03-14 06:00:00,85.25,177.106,30.551,31.988000000000003 -2020-03-14 06:15:00,78.08,198.613,30.551,31.988000000000003 -2020-03-14 06:30:00,81.79,193.703,30.551,31.988000000000003 -2020-03-14 06:45:00,83.22,189.206,30.551,31.988000000000003 -2020-03-14 07:00:00,89.51,187.062,34.865,31.988000000000003 -2020-03-14 07:15:00,91.04,189.51,34.865,31.988000000000003 -2020-03-14 07:30:00,91.15,191.669,34.865,31.988000000000003 -2020-03-14 07:45:00,90.05,192.73,34.865,31.988000000000003 -2020-03-14 08:00:00,95.18,193.46599999999998,41.456,31.988000000000003 -2020-03-14 08:15:00,100.55,194.687,41.456,31.988000000000003 -2020-03-14 08:30:00,104.99,192.00400000000002,41.456,31.988000000000003 -2020-03-14 08:45:00,103.55,189.274,41.456,31.988000000000003 -2020-03-14 09:00:00,103.12,184.047,43.001999999999995,31.988000000000003 -2020-03-14 09:15:00,109.55,182.112,43.001999999999995,31.988000000000003 -2020-03-14 09:30:00,113.29,181.43599999999998,43.001999999999995,31.988000000000003 -2020-03-14 09:45:00,114.02,178.81,43.001999999999995,31.988000000000003 -2020-03-14 10:00:00,110.04,175.35,42.047,31.988000000000003 -2020-03-14 10:15:00,109.56,173.428,42.047,31.988000000000003 -2020-03-14 10:30:00,115.95,171.328,42.047,31.988000000000003 -2020-03-14 10:45:00,117.43,171.47400000000002,42.047,31.988000000000003 -2020-03-14 11:00:00,116.25,168.637,39.894,31.988000000000003 -2020-03-14 11:15:00,113.56,166.72400000000002,39.894,31.988000000000003 -2020-03-14 11:30:00,120.33,167.00400000000002,39.894,31.988000000000003 -2020-03-14 11:45:00,122.42,165.852,39.894,31.988000000000003 -2020-03-14 12:00:00,112.56,160.997,38.122,31.988000000000003 -2020-03-14 12:15:00,110.8,159.982,38.122,31.988000000000003 -2020-03-14 12:30:00,105.88,159.467,38.122,31.988000000000003 -2020-03-14 12:45:00,104.72,160.058,38.122,31.988000000000003 -2020-03-14 13:00:00,102.48,160.347,34.645,31.988000000000003 -2020-03-14 13:15:00,101.32,157.35299999999998,34.645,31.988000000000003 -2020-03-14 13:30:00,99.94,155.47299999999998,34.645,31.988000000000003 -2020-03-14 13:45:00,98.58,155.225,34.645,31.988000000000003 -2020-03-14 14:00:00,96.86,157.421,33.739000000000004,31.988000000000003 -2020-03-14 14:15:00,96.28,155.673,33.739000000000004,31.988000000000003 -2020-03-14 14:30:00,96.6,154.406,33.739000000000004,31.988000000000003 -2020-03-14 14:45:00,97.2,155.903,33.739000000000004,31.988000000000003 -2020-03-14 15:00:00,96.67,157.937,35.908,31.988000000000003 -2020-03-14 15:15:00,95.72,156.866,35.908,31.988000000000003 -2020-03-14 15:30:00,95.92,156.571,35.908,31.988000000000003 -2020-03-14 15:45:00,95.09,156.715,35.908,31.988000000000003 -2020-03-14 16:00:00,95.88,158.109,39.249,31.988000000000003 -2020-03-14 16:15:00,95.55,160.806,39.249,31.988000000000003 -2020-03-14 16:30:00,95.24,162.874,39.249,31.988000000000003 -2020-03-14 16:45:00,95.42,163.626,39.249,31.988000000000003 -2020-03-14 17:00:00,99.69,166.172,46.045,31.988000000000003 -2020-03-14 17:15:00,99.32,169.774,46.045,31.988000000000003 -2020-03-14 17:30:00,101.07,172.42700000000002,46.045,31.988000000000003 -2020-03-14 17:45:00,102.78,173.00599999999997,46.045,31.988000000000003 -2020-03-14 18:00:00,107.78,176.59599999999998,48.238,31.988000000000003 -2020-03-14 18:15:00,108.04,179.19299999999998,48.238,31.988000000000003 -2020-03-14 18:30:00,111.45,179.40099999999998,48.238,31.988000000000003 -2020-03-14 18:45:00,111.41,177.924,48.238,31.988000000000003 -2020-03-14 19:00:00,111.15,178.12599999999998,46.785,31.988000000000003 -2020-03-14 19:15:00,109.51,176.055,46.785,31.988000000000003 -2020-03-14 19:30:00,108.23,175.903,46.785,31.988000000000003 -2020-03-14 19:45:00,106.14,172.66299999999998,46.785,31.988000000000003 -2020-03-14 20:00:00,101.01,169.296,39.830999999999996,31.988000000000003 -2020-03-14 20:15:00,97.68,166.32299999999998,39.830999999999996,31.988000000000003 -2020-03-14 20:30:00,93.02,162.352,39.830999999999996,31.988000000000003 -2020-03-14 20:45:00,93.19,160.203,39.830999999999996,31.988000000000003 -2020-03-14 21:00:00,89.17,159.56799999999998,34.063,31.988000000000003 -2020-03-14 21:15:00,86.26,157.30700000000002,34.063,31.988000000000003 -2020-03-14 21:30:00,89.1,156.261,34.063,31.988000000000003 -2020-03-14 21:45:00,87.56,156.39,34.063,31.988000000000003 -2020-03-14 22:00:00,82.87,151.08100000000002,34.455999999999996,31.988000000000003 -2020-03-14 22:15:00,83.61,149.59799999999998,34.455999999999996,31.988000000000003 -2020-03-14 22:30:00,77.72,146.056,34.455999999999996,31.988000000000003 -2020-03-14 22:45:00,79.25,143.726,34.455999999999996,31.988000000000003 -2020-03-14 23:00:00,75.28,139.096,27.840999999999998,31.988000000000003 -2020-03-14 23:15:00,72.99,133.905,27.840999999999998,31.988000000000003 -2020-03-14 23:30:00,72.93,132.463,27.840999999999998,31.988000000000003 -2020-03-14 23:45:00,71.64,129.89,27.840999999999998,31.988000000000003 -2020-03-15 00:00:00,66.91,110.05799999999999,20.007,31.988000000000003 -2020-03-15 00:15:00,67.0,103.98700000000001,20.007,31.988000000000003 -2020-03-15 00:30:00,66.54,103.802,20.007,31.988000000000003 -2020-03-15 00:45:00,65.35,105.135,20.007,31.988000000000003 -2020-03-15 01:00:00,62.97,106.49,17.378,31.988000000000003 -2020-03-15 01:15:00,63.28,107.705,17.378,31.988000000000003 -2020-03-15 01:30:00,60.74,107.381,17.378,31.988000000000003 -2020-03-15 01:45:00,63.48,107.251,17.378,31.988000000000003 -2020-03-15 02:00:00,61.21,109.098,16.145,31.988000000000003 -2020-03-15 02:15:00,61.18,108.698,16.145,31.988000000000003 -2020-03-15 02:30:00,60.17,109.969,16.145,31.988000000000003 -2020-03-15 02:45:00,61.4,112.275,16.145,31.988000000000003 -2020-03-15 03:00:00,60.81,114.917,15.427999999999999,31.988000000000003 -2020-03-15 03:15:00,61.75,115.56700000000001,15.427999999999999,31.988000000000003 -2020-03-15 03:30:00,61.32,117.15100000000001,15.427999999999999,31.988000000000003 -2020-03-15 03:45:00,62.75,118.98200000000001,15.427999999999999,31.988000000000003 -2020-03-15 04:00:00,62.97,128.166,16.663,31.988000000000003 -2020-03-15 04:15:00,63.98,137.14700000000002,16.663,31.988000000000003 -2020-03-15 04:30:00,63.98,137.636,16.663,31.988000000000003 -2020-03-15 04:45:00,63.9,137.90200000000002,16.663,31.988000000000003 -2020-03-15 05:00:00,63.92,151.424,17.271,31.988000000000003 -2020-03-15 05:15:00,62.64,162.171,17.271,31.988000000000003 -2020-03-15 05:30:00,63.97,157.541,17.271,31.988000000000003 -2020-03-15 05:45:00,64.06,155.245,17.271,31.988000000000003 -2020-03-15 06:00:00,63.06,171.845,17.612000000000002,31.988000000000003 -2020-03-15 06:15:00,61.28,191.857,17.612000000000002,31.988000000000003 -2020-03-15 06:30:00,58.8,185.75099999999998,17.612000000000002,31.988000000000003 -2020-03-15 06:45:00,61.36,180.08599999999998,17.612000000000002,31.988000000000003 -2020-03-15 07:00:00,63.14,180.313,20.88,31.988000000000003 -2020-03-15 07:15:00,62.27,181.68,20.88,31.988000000000003 -2020-03-15 07:30:00,66.04,182.82299999999998,20.88,31.988000000000003 -2020-03-15 07:45:00,68.94,183.12,20.88,31.988000000000003 -2020-03-15 08:00:00,71.34,185.66299999999998,25.861,31.988000000000003 -2020-03-15 08:15:00,73.91,186.957,25.861,31.988000000000003 -2020-03-15 08:30:00,74.04,185.912,25.861,31.988000000000003 -2020-03-15 08:45:00,74.07,185.106,25.861,31.988000000000003 -2020-03-15 09:00:00,74.58,179.49,27.921999999999997,31.988000000000003 -2020-03-15 09:15:00,73.36,178.012,27.921999999999997,31.988000000000003 -2020-03-15 09:30:00,75.48,177.27,27.921999999999997,31.988000000000003 -2020-03-15 09:45:00,75.64,174.68599999999998,27.921999999999997,31.988000000000003 -2020-03-15 10:00:00,75.27,173.71099999999998,29.048000000000002,31.988000000000003 -2020-03-15 10:15:00,74.22,172.36,29.048000000000002,31.988000000000003 -2020-03-15 10:30:00,73.91,170.886,29.048000000000002,31.988000000000003 -2020-03-15 10:45:00,74.56,169.398,29.048000000000002,31.988000000000003 -2020-03-15 11:00:00,77.3,167.37099999999998,32.02,31.988000000000003 -2020-03-15 11:15:00,80.08,165.544,32.02,31.988000000000003 -2020-03-15 11:30:00,82.09,165.077,32.02,31.988000000000003 -2020-03-15 11:45:00,79.85,164.555,32.02,31.988000000000003 -2020-03-15 12:00:00,78.18,159.333,28.55,31.988000000000003 -2020-03-15 12:15:00,79.4,160.05,28.55,31.988000000000003 -2020-03-15 12:30:00,76.27,158.143,28.55,31.988000000000003 -2020-03-15 12:45:00,75.58,157.72299999999998,28.55,31.988000000000003 -2020-03-15 13:00:00,71.35,157.334,25.601999999999997,31.988000000000003 -2020-03-15 13:15:00,71.38,157.099,25.601999999999997,31.988000000000003 -2020-03-15 13:30:00,70.92,154.85,25.601999999999997,31.988000000000003 -2020-03-15 13:45:00,72.05,154.139,25.601999999999997,31.988000000000003 -2020-03-15 14:00:00,72.64,156.80700000000002,23.916999999999998,31.988000000000003 -2020-03-15 14:15:00,71.93,156.235,23.916999999999998,31.988000000000003 -2020-03-15 14:30:00,71.59,155.931,23.916999999999998,31.988000000000003 -2020-03-15 14:45:00,71.9,156.895,23.916999999999998,31.988000000000003 -2020-03-15 15:00:00,73.94,157.505,24.064,31.988000000000003 -2020-03-15 15:15:00,72.87,157.013,24.064,31.988000000000003 -2020-03-15 15:30:00,73.03,157.2,24.064,31.988000000000003 -2020-03-15 15:45:00,74.33,158.025,24.064,31.988000000000003 -2020-03-15 16:00:00,76.14,161.00799999999998,28.189,31.988000000000003 -2020-03-15 16:15:00,77.61,162.841,28.189,31.988000000000003 -2020-03-15 16:30:00,78.45,165.28900000000002,28.189,31.988000000000003 -2020-03-15 16:45:00,83.21,166.13099999999997,28.189,31.988000000000003 -2020-03-15 17:00:00,85.3,168.75799999999998,37.576,31.988000000000003 -2020-03-15 17:15:00,86.88,172.24,37.576,31.988000000000003 -2020-03-15 17:30:00,88.48,175.292,37.576,31.988000000000003 -2020-03-15 17:45:00,89.58,178.122,37.576,31.988000000000003 -2020-03-15 18:00:00,97.68,181.27700000000002,42.669,31.988000000000003 -2020-03-15 18:15:00,98.02,185.18,42.669,31.988000000000003 -2020-03-15 18:30:00,103.29,183.335,42.669,31.988000000000003 -2020-03-15 18:45:00,103.7,183.683,42.669,31.988000000000003 -2020-03-15 19:00:00,105.01,183.797,43.538999999999994,31.988000000000003 -2020-03-15 19:15:00,102.92,182.188,43.538999999999994,31.988000000000003 -2020-03-15 19:30:00,101.41,181.878,43.538999999999994,31.988000000000003 -2020-03-15 19:45:00,99.98,180.03400000000002,43.538999999999994,31.988000000000003 -2020-03-15 20:00:00,98.07,176.605,37.330999999999996,31.988000000000003 -2020-03-15 20:15:00,98.05,174.584,37.330999999999996,31.988000000000003 -2020-03-15 20:30:00,102.06,171.94,37.330999999999996,31.988000000000003 -2020-03-15 20:45:00,101.51,168.424,37.330999999999996,31.988000000000003 -2020-03-15 21:00:00,92.01,165.261,33.856,31.988000000000003 -2020-03-15 21:15:00,96.09,162.356,33.856,31.988000000000003 -2020-03-15 21:30:00,95.45,161.513,33.856,31.988000000000003 -2020-03-15 21:45:00,98.39,161.839,33.856,31.988000000000003 -2020-03-15 22:00:00,96.08,155.615,34.711999999999996,31.988000000000003 -2020-03-15 22:15:00,93.54,153.219,34.711999999999996,31.988000000000003 -2020-03-15 22:30:00,88.54,146.49200000000002,34.711999999999996,31.988000000000003 -2020-03-15 22:45:00,88.54,143.184,34.711999999999996,31.988000000000003 -2020-03-15 23:00:00,90.01,135.808,29.698,31.988000000000003 -2020-03-15 23:15:00,90.99,132.591,29.698,31.988000000000003 -2020-03-15 23:30:00,88.54,131.872,29.698,31.988000000000003 -2020-03-15 23:45:00,84.36,130.211,29.698,31.988000000000003 -2020-03-16 00:00:00,75.75,98.40299999999999,29.983,32.166 -2020-03-16 00:15:00,83.47,96.28299999999999,29.983,32.166 -2020-03-16 00:30:00,84.42,95.56,29.983,32.166 -2020-03-16 00:45:00,82.25,95.21799999999999,29.983,32.166 -2020-03-16 01:00:00,77.57,96.861,29.122,32.166 -2020-03-16 01:15:00,80.67,97.205,29.122,32.166 -2020-03-16 01:30:00,81.83,96.76299999999999,29.122,32.166 -2020-03-16 01:45:00,81.92,96.56,29.122,32.166 -2020-03-16 02:00:00,76.07,98.52600000000001,28.676,32.166 -2020-03-16 02:15:00,79.7,98.59200000000001,28.676,32.166 -2020-03-16 02:30:00,80.65,100.493,28.676,32.166 -2020-03-16 02:45:00,81.61,101.565,28.676,32.166 -2020-03-16 03:00:00,77.46,105.42399999999999,26.552,32.166 -2020-03-16 03:15:00,79.91,107.682,26.552,32.166 -2020-03-16 03:30:00,82.47,108.404,26.552,32.166 -2020-03-16 03:45:00,82.23,109.36399999999999,26.552,32.166 -2020-03-16 04:00:00,80.26,122.883,27.44,32.166 -2020-03-16 04:15:00,78.08,136.07299999999998,27.44,32.166 -2020-03-16 04:30:00,78.99,137.483,27.44,32.166 -2020-03-16 04:45:00,83.48,138.162,27.44,32.166 -2020-03-16 05:00:00,85.58,168.11,36.825,32.166 -2020-03-16 05:15:00,89.84,199.683,36.825,32.166 -2020-03-16 05:30:00,94.03,193.502,36.825,32.166 -2020-03-16 05:45:00,98.78,184.55700000000002,36.825,32.166 -2020-03-16 06:00:00,104.82,183.747,56.589,32.166 -2020-03-16 06:15:00,108.78,188.298,56.589,32.166 -2020-03-16 06:30:00,111.69,189.327,56.589,32.166 -2020-03-16 06:45:00,114.28,191.799,56.589,32.166 -2020-03-16 07:00:00,119.75,194.726,67.49,32.166 -2020-03-16 07:15:00,118.99,196.967,67.49,32.166 -2020-03-16 07:30:00,121.18,196.90599999999998,67.49,32.166 -2020-03-16 07:45:00,121.94,194.672,67.49,32.166 -2020-03-16 08:00:00,124.7,193.282,60.028,32.166 -2020-03-16 08:15:00,122.52,192.105,60.028,32.166 -2020-03-16 08:30:00,121.4,187.63,60.028,32.166 -2020-03-16 08:45:00,119.86,184.179,60.028,32.166 -2020-03-16 09:00:00,117.82,178.373,55.018,32.166 -2020-03-16 09:15:00,116.63,174.179,55.018,32.166 -2020-03-16 09:30:00,117.73,173.43099999999998,55.018,32.166 -2020-03-16 09:45:00,115.81,171.18599999999998,55.018,32.166 -2020-03-16 10:00:00,113.41,168.915,51.183,32.166 -2020-03-16 10:15:00,116.0,167.81400000000002,51.183,32.166 -2020-03-16 10:30:00,115.46,165.19799999999998,51.183,32.166 -2020-03-16 10:45:00,117.05,164.111,51.183,32.166 -2020-03-16 11:00:00,113.1,158.343,50.065,32.166 -2020-03-16 11:15:00,113.17,158.403,50.065,32.166 -2020-03-16 11:30:00,113.75,160.048,50.065,32.166 -2020-03-16 11:45:00,114.16,160.379,50.065,32.166 -2020-03-16 12:00:00,111.33,156.921,48.141999999999996,32.166 -2020-03-16 12:15:00,113.06,157.437,48.141999999999996,32.166 -2020-03-16 12:30:00,110.37,156.293,48.141999999999996,32.166 -2020-03-16 12:45:00,110.4,157.15200000000002,48.141999999999996,32.166 -2020-03-16 13:00:00,108.44,157.352,47.887,32.166 -2020-03-16 13:15:00,107.98,155.593,47.887,32.166 -2020-03-16 13:30:00,107.97,152.868,47.887,32.166 -2020-03-16 13:45:00,108.56,152.096,47.887,32.166 -2020-03-16 14:00:00,111.41,153.593,48.571000000000005,32.166 -2020-03-16 14:15:00,108.91,152.257,48.571000000000005,32.166 -2020-03-16 14:30:00,108.43,151.621,48.571000000000005,32.166 -2020-03-16 14:45:00,109.18,152.847,48.571000000000005,32.166 -2020-03-16 15:00:00,110.53,154.037,49.937,32.166 -2020-03-16 15:15:00,110.99,152.216,49.937,32.166 -2020-03-16 15:30:00,110.57,151.31,49.937,32.166 -2020-03-16 15:45:00,112.88,150.865,49.937,32.166 -2020-03-16 16:00:00,113.73,154.678,52.963,32.166 -2020-03-16 16:15:00,114.16,156.461,52.963,32.166 -2020-03-16 16:30:00,116.67,156.502,52.963,32.166 -2020-03-16 16:45:00,119.49,155.469,52.963,32.166 -2020-03-16 17:00:00,120.61,156.90200000000002,61.163999999999994,32.166 -2020-03-16 17:15:00,121.52,159.283,61.163999999999994,32.166 -2020-03-16 17:30:00,124.46,161.386,61.163999999999994,32.166 -2020-03-16 17:45:00,126.0,162.251,61.163999999999994,32.166 -2020-03-16 18:00:00,128.25,166.34799999999998,63.788999999999994,32.166 -2020-03-16 18:15:00,128.87,167.03099999999998,63.788999999999994,32.166 -2020-03-16 18:30:00,132.59,165.72,63.788999999999994,32.166 -2020-03-16 18:45:00,135.2,169.049,63.788999999999994,32.166 -2020-03-16 19:00:00,135.47,167.59,63.913000000000004,32.166 -2020-03-16 19:15:00,135.93,165.708,63.913000000000004,32.166 -2020-03-16 19:30:00,136.42,165.69400000000002,63.913000000000004,32.166 -2020-03-16 19:45:00,139.91,163.987,63.913000000000004,32.166 -2020-03-16 20:00:00,131.55,157.97,65.44,32.166 -2020-03-16 20:15:00,123.08,154.694,65.44,32.166 -2020-03-16 20:30:00,115.5,151.886,65.44,32.166 -2020-03-16 20:45:00,115.34,150.49200000000002,65.44,32.166 -2020-03-16 21:00:00,111.28,145.696,59.117,32.166 -2020-03-16 21:15:00,109.77,142.936,59.117,32.166 -2020-03-16 21:30:00,108.18,142.46200000000002,59.117,32.166 -2020-03-16 21:45:00,108.66,141.463,59.117,32.166 -2020-03-16 22:00:00,100.71,133.118,52.301,32.166 -2020-03-16 22:15:00,101.8,129.96,52.301,32.166 -2020-03-16 22:30:00,98.84,115.694,52.301,32.166 -2020-03-16 22:45:00,97.68,108.209,52.301,32.166 -2020-03-16 23:00:00,98.26,101.245,44.373000000000005,32.166 -2020-03-16 23:15:00,99.14,100.038,44.373000000000005,32.166 -2020-03-16 23:30:00,95.57,100.943,44.373000000000005,32.166 -2020-03-16 23:45:00,90.74,101.705,44.373000000000005,32.166 -2020-03-17 00:00:00,83.29,96.788,44.647,32.166 -2020-03-17 00:15:00,85.18,96.06,44.647,32.166 -2020-03-17 00:30:00,88.73,94.814,44.647,32.166 -2020-03-17 00:45:00,90.01,93.965,44.647,32.166 -2020-03-17 01:00:00,86.46,95.25399999999999,41.433,32.166 -2020-03-17 01:15:00,85.81,95.272,41.433,32.166 -2020-03-17 01:30:00,81.39,94.90299999999999,41.433,32.166 -2020-03-17 01:45:00,87.26,94.779,41.433,32.166 -2020-03-17 02:00:00,87.84,96.57700000000001,39.909,32.166 -2020-03-17 02:15:00,88.12,96.916,39.909,32.166 -2020-03-17 02:30:00,80.08,98.25200000000001,39.909,32.166 -2020-03-17 02:45:00,81.87,99.441,39.909,32.166 -2020-03-17 03:00:00,88.39,102.213,39.14,32.166 -2020-03-17 03:15:00,89.75,104.1,39.14,32.166 -2020-03-17 03:30:00,82.45,105.178,39.14,32.166 -2020-03-17 03:45:00,84.57,105.939,39.14,32.166 -2020-03-17 04:00:00,86.69,118.898,40.015,32.166 -2020-03-17 04:15:00,91.6,131.80700000000002,40.015,32.166 -2020-03-17 04:30:00,94.85,132.954,40.015,32.166 -2020-03-17 04:45:00,92.46,134.70600000000002,40.015,32.166 -2020-03-17 05:00:00,92.95,168.956,44.93600000000001,32.166 -2020-03-17 05:15:00,94.92,200.56099999999998,44.93600000000001,32.166 -2020-03-17 05:30:00,98.34,193.25,44.93600000000001,32.166 -2020-03-17 05:45:00,99.36,184.088,44.93600000000001,32.166 -2020-03-17 06:00:00,109.4,182.72,57.271,32.166 -2020-03-17 06:15:00,112.51,188.58599999999998,57.271,32.166 -2020-03-17 06:30:00,114.99,188.97299999999998,57.271,32.166 -2020-03-17 06:45:00,116.99,190.85,57.271,32.166 -2020-03-17 07:00:00,118.61,193.696,68.352,32.166 -2020-03-17 07:15:00,121.29,195.703,68.352,32.166 -2020-03-17 07:30:00,121.0,195.18,68.352,32.166 -2020-03-17 07:45:00,121.45,192.78,68.352,32.166 -2020-03-17 08:00:00,121.87,191.438,60.717,32.166 -2020-03-17 08:15:00,119.41,189.315,60.717,32.166 -2020-03-17 08:30:00,117.8,184.7,60.717,32.166 -2020-03-17 08:45:00,117.14,180.785,60.717,32.166 -2020-03-17 09:00:00,114.19,174.417,54.603,32.166 -2020-03-17 09:15:00,117.59,171.476,54.603,32.166 -2020-03-17 09:30:00,123.71,171.49200000000002,54.603,32.166 -2020-03-17 09:45:00,123.51,169.47299999999998,54.603,32.166 -2020-03-17 10:00:00,123.65,166.34900000000002,52.308,32.166 -2020-03-17 10:15:00,126.87,164.386,52.308,32.166 -2020-03-17 10:30:00,129.22,161.95,52.308,32.166 -2020-03-17 10:45:00,132.76,161.387,52.308,32.166 -2020-03-17 11:00:00,132.25,156.829,51.838,32.166 -2020-03-17 11:15:00,132.0,156.743,51.838,32.166 -2020-03-17 11:30:00,132.65,157.129,51.838,32.166 -2020-03-17 11:45:00,134.64,157.938,51.838,32.166 -2020-03-17 12:00:00,132.83,153.315,50.375,32.166 -2020-03-17 12:15:00,132.99,153.584,50.375,32.166 -2020-03-17 12:30:00,132.12,153.22299999999998,50.375,32.166 -2020-03-17 12:45:00,133.93,154.017,50.375,32.166 -2020-03-17 13:00:00,132.01,153.819,50.735,32.166 -2020-03-17 13:15:00,130.45,152.21200000000002,50.735,32.166 -2020-03-17 13:30:00,124.25,150.447,50.735,32.166 -2020-03-17 13:45:00,128.13,149.58700000000002,50.735,32.166 -2020-03-17 14:00:00,127.73,151.502,50.946000000000005,32.166 -2020-03-17 14:15:00,128.74,150.222,50.946000000000005,32.166 -2020-03-17 14:30:00,128.92,150.161,50.946000000000005,32.166 -2020-03-17 14:45:00,130.63,151.126,50.946000000000005,32.166 -2020-03-17 15:00:00,132.16,151.93,53.18,32.166 -2020-03-17 15:15:00,132.92,150.592,53.18,32.166 -2020-03-17 15:30:00,132.15,149.77,53.18,32.166 -2020-03-17 15:45:00,132.5,149.059,53.18,32.166 -2020-03-17 16:00:00,131.86,153.01,54.928999999999995,32.166 -2020-03-17 16:15:00,131.47,155.187,54.928999999999995,32.166 -2020-03-17 16:30:00,129.12,155.662,54.928999999999995,32.166 -2020-03-17 16:45:00,130.42,155.007,54.928999999999995,32.166 -2020-03-17 17:00:00,131.53,156.974,60.913000000000004,32.166 -2020-03-17 17:15:00,135.08,159.489,60.913000000000004,32.166 -2020-03-17 17:30:00,143.36,162.031,60.913000000000004,32.166 -2020-03-17 17:45:00,144.28,162.69899999999998,60.913000000000004,32.166 -2020-03-17 18:00:00,137.97,166.455,62.214,32.166 -2020-03-17 18:15:00,138.35,167.197,62.214,32.166 -2020-03-17 18:30:00,140.04,165.55599999999998,62.214,32.166 -2020-03-17 18:45:00,139.14,169.495,62.214,32.166 -2020-03-17 19:00:00,138.8,167.745,62.38,32.166 -2020-03-17 19:15:00,134.96,165.702,62.38,32.166 -2020-03-17 19:30:00,135.63,165.108,62.38,32.166 -2020-03-17 19:45:00,132.59,163.565,62.38,32.166 -2020-03-17 20:00:00,123.95,157.756,65.018,32.166 -2020-03-17 20:15:00,121.08,153.566,65.018,32.166 -2020-03-17 20:30:00,114.15,151.616,65.018,32.166 -2020-03-17 20:45:00,114.92,149.891,65.018,32.166 -2020-03-17 21:00:00,114.69,144.774,56.416000000000004,32.166 -2020-03-17 21:15:00,109.67,142.322,56.416000000000004,32.166 -2020-03-17 21:30:00,107.76,141.31,56.416000000000004,32.166 -2020-03-17 21:45:00,105.9,140.57399999999998,56.416000000000004,32.166 -2020-03-17 22:00:00,108.3,133.65,52.846000000000004,32.166 -2020-03-17 22:15:00,108.41,130.213,52.846000000000004,32.166 -2020-03-17 22:30:00,101.96,116.061,52.846000000000004,32.166 -2020-03-17 22:45:00,96.25,108.795,52.846000000000004,32.166 -2020-03-17 23:00:00,91.66,101.624,44.435,32.166 -2020-03-17 23:15:00,90.02,100.15700000000001,44.435,32.166 -2020-03-17 23:30:00,86.99,100.775,44.435,32.166 -2020-03-17 23:45:00,89.71,101.256,44.435,32.166 -2020-03-18 00:00:00,83.73,96.42299999999999,42.527,32.166 -2020-03-18 00:15:00,89.73,95.705,42.527,32.166 -2020-03-18 00:30:00,86.8,94.446,42.527,32.166 -2020-03-18 00:45:00,85.84,93.59899999999999,42.527,32.166 -2020-03-18 01:00:00,80.74,94.855,38.655,32.166 -2020-03-18 01:15:00,80.02,94.855,38.655,32.166 -2020-03-18 01:30:00,85.25,94.46799999999999,38.655,32.166 -2020-03-18 01:45:00,87.28,94.34899999999999,38.655,32.166 -2020-03-18 02:00:00,86.8,96.135,36.912,32.166 -2020-03-18 02:15:00,84.18,96.462,36.912,32.166 -2020-03-18 02:30:00,79.64,97.82,36.912,32.166 -2020-03-18 02:45:00,86.07,99.01100000000001,36.912,32.166 -2020-03-18 03:00:00,88.69,101.79799999999999,36.98,32.166 -2020-03-18 03:15:00,89.95,103.663,36.98,32.166 -2020-03-18 03:30:00,85.36,104.735,36.98,32.166 -2020-03-18 03:45:00,81.81,105.51,36.98,32.166 -2020-03-18 04:00:00,86.79,118.46600000000001,38.052,32.166 -2020-03-18 04:15:00,90.82,131.358,38.052,32.166 -2020-03-18 04:30:00,92.75,132.518,38.052,32.166 -2020-03-18 04:45:00,88.52,134.257,38.052,32.166 -2020-03-18 05:00:00,90.89,168.46599999999998,42.455,32.166 -2020-03-18 05:15:00,92.19,200.058,42.455,32.166 -2020-03-18 05:30:00,96.66,192.72400000000002,42.455,32.166 -2020-03-18 05:45:00,100.78,183.581,42.455,32.166 -2020-03-18 06:00:00,108.49,182.225,57.986000000000004,32.166 -2020-03-18 06:15:00,111.7,188.088,57.986000000000004,32.166 -2020-03-18 06:30:00,114.37,188.43599999999998,57.986000000000004,32.166 -2020-03-18 06:45:00,118.53,190.293,57.986000000000004,32.166 -2020-03-18 07:00:00,124.03,193.15400000000002,71.868,32.166 -2020-03-18 07:15:00,124.83,195.13,71.868,32.166 -2020-03-18 07:30:00,127.19,194.567,71.868,32.166 -2020-03-18 07:45:00,130.33,192.137,71.868,32.166 -2020-03-18 08:00:00,133.17,190.771,62.225,32.166 -2020-03-18 08:15:00,131.53,188.65099999999998,62.225,32.166 -2020-03-18 08:30:00,133.54,183.987,62.225,32.166 -2020-03-18 08:45:00,132.96,180.095,62.225,32.166 -2020-03-18 09:00:00,129.67,173.738,58.802,32.166 -2020-03-18 09:15:00,131.06,170.799,58.802,32.166 -2020-03-18 09:30:00,130.32,170.835,58.802,32.166 -2020-03-18 09:45:00,125.59,168.834,58.802,32.166 -2020-03-18 10:00:00,123.25,165.722,54.122,32.166 -2020-03-18 10:15:00,118.73,163.805,54.122,32.166 -2020-03-18 10:30:00,115.43,161.391,54.122,32.166 -2020-03-18 10:45:00,117.34,160.849,54.122,32.166 -2020-03-18 11:00:00,117.69,156.282,54.368,32.166 -2020-03-18 11:15:00,118.38,156.218,54.368,32.166 -2020-03-18 11:30:00,118.73,156.61,54.368,32.166 -2020-03-18 11:45:00,118.1,157.438,54.368,32.166 -2020-03-18 12:00:00,117.22,152.83700000000002,52.74,32.166 -2020-03-18 12:15:00,117.72,153.116,52.74,32.166 -2020-03-18 12:30:00,117.55,152.716,52.74,32.166 -2020-03-18 12:45:00,115.41,153.509,52.74,32.166 -2020-03-18 13:00:00,116.23,153.35399999999998,52.544,32.166 -2020-03-18 13:15:00,118.56,151.731,52.544,32.166 -2020-03-18 13:30:00,118.92,149.958,52.544,32.166 -2020-03-18 13:45:00,118.2,149.1,52.544,32.166 -2020-03-18 14:00:00,119.88,151.083,53.602,32.166 -2020-03-18 14:15:00,117.8,149.78,53.602,32.166 -2020-03-18 14:30:00,116.98,149.68200000000002,53.602,32.166 -2020-03-18 14:45:00,119.05,150.655,53.602,32.166 -2020-03-18 15:00:00,115.92,151.475,55.59,32.166 -2020-03-18 15:15:00,111.49,150.108,55.59,32.166 -2020-03-18 15:30:00,111.91,149.237,55.59,32.166 -2020-03-18 15:45:00,121.29,148.507,55.59,32.166 -2020-03-18 16:00:00,119.54,152.477,57.586999999999996,32.166 -2020-03-18 16:15:00,120.09,154.63,57.586999999999996,32.166 -2020-03-18 16:30:00,114.99,155.105,57.586999999999996,32.166 -2020-03-18 16:45:00,117.72,154.4,57.586999999999996,32.166 -2020-03-18 17:00:00,122.47,156.395,62.111999999999995,32.166 -2020-03-18 17:15:00,129.68,158.906,62.111999999999995,32.166 -2020-03-18 17:30:00,131.77,161.46200000000002,62.111999999999995,32.166 -2020-03-18 17:45:00,132.41,162.132,62.111999999999995,32.166 -2020-03-18 18:00:00,131.64,165.895,64.605,32.166 -2020-03-18 18:15:00,126.8,166.695,64.605,32.166 -2020-03-18 18:30:00,135.48,165.045,64.605,32.166 -2020-03-18 18:45:00,144.49,169.00099999999998,64.605,32.166 -2020-03-18 19:00:00,144.79,167.22,65.55199999999999,32.166 -2020-03-18 19:15:00,137.82,165.19299999999998,65.55199999999999,32.166 -2020-03-18 19:30:00,133.32,164.62400000000002,65.55199999999999,32.166 -2020-03-18 19:45:00,131.22,163.12,65.55199999999999,32.166 -2020-03-18 20:00:00,129.19,157.282,66.778,32.166 -2020-03-18 20:15:00,125.46,153.107,66.778,32.166 -2020-03-18 20:30:00,120.34,151.188,66.778,32.166 -2020-03-18 20:45:00,117.63,149.47299999999998,66.778,32.166 -2020-03-18 21:00:00,114.44,144.349,56.103,32.166 -2020-03-18 21:15:00,114.71,141.899,56.103,32.166 -2020-03-18 21:30:00,109.71,140.885,56.103,32.166 -2020-03-18 21:45:00,108.39,140.174,56.103,32.166 -2020-03-18 22:00:00,107.26,133.243,51.371,32.166 -2020-03-18 22:15:00,105.38,129.833,51.371,32.166 -2020-03-18 22:30:00,98.14,115.63600000000001,51.371,32.166 -2020-03-18 22:45:00,94.88,108.369,51.371,32.166 -2020-03-18 23:00:00,88.42,101.18700000000001,42.798,32.166 -2020-03-18 23:15:00,89.09,99.745,42.798,32.166 -2020-03-18 23:30:00,91.31,100.363,42.798,32.166 -2020-03-18 23:45:00,92.36,100.868,42.798,32.166 -2020-03-19 00:00:00,87.88,96.053,39.069,32.166 -2020-03-19 00:15:00,85.89,95.34700000000001,39.069,32.166 -2020-03-19 00:30:00,87.68,94.073,39.069,32.166 -2020-03-19 00:45:00,87.98,93.228,39.069,32.166 -2020-03-19 01:00:00,84.61,94.45200000000001,37.043,32.166 -2020-03-19 01:15:00,79.55,94.434,37.043,32.166 -2020-03-19 01:30:00,77.18,94.027,37.043,32.166 -2020-03-19 01:45:00,78.42,93.915,37.043,32.166 -2020-03-19 02:00:00,79.65,95.69,34.625,32.166 -2020-03-19 02:15:00,86.0,96.006,34.625,32.166 -2020-03-19 02:30:00,83.94,97.384,34.625,32.166 -2020-03-19 02:45:00,86.89,98.575,34.625,32.166 -2020-03-19 03:00:00,82.57,101.37899999999999,33.812,32.166 -2020-03-19 03:15:00,86.09,103.221,33.812,32.166 -2020-03-19 03:30:00,88.78,104.287,33.812,32.166 -2020-03-19 03:45:00,86.84,105.07799999999999,33.812,32.166 -2020-03-19 04:00:00,86.04,118.03,35.236999999999995,32.166 -2020-03-19 04:15:00,87.46,130.905,35.236999999999995,32.166 -2020-03-19 04:30:00,90.13,132.077,35.236999999999995,32.166 -2020-03-19 04:45:00,89.22,133.804,35.236999999999995,32.166 -2020-03-19 05:00:00,87.18,167.97099999999998,40.375,32.166 -2020-03-19 05:15:00,92.65,199.551,40.375,32.166 -2020-03-19 05:30:00,91.79,192.196,40.375,32.166 -2020-03-19 05:45:00,99.25,183.07,40.375,32.166 -2020-03-19 06:00:00,101.84,181.726,52.316,32.166 -2020-03-19 06:15:00,108.97,187.58700000000002,52.316,32.166 -2020-03-19 06:30:00,112.26,187.893,52.316,32.166 -2020-03-19 06:45:00,114.99,189.731,52.316,32.166 -2020-03-19 07:00:00,119.6,192.607,64.115,32.166 -2020-03-19 07:15:00,119.74,194.554,64.115,32.166 -2020-03-19 07:30:00,122.99,193.949,64.115,32.166 -2020-03-19 07:45:00,125.11,191.489,64.115,32.166 -2020-03-19 08:00:00,132.62,190.1,55.033,32.166 -2020-03-19 08:15:00,133.14,187.981,55.033,32.166 -2020-03-19 08:30:00,132.06,183.27,55.033,32.166 -2020-03-19 08:45:00,130.67,179.4,55.033,32.166 -2020-03-19 09:00:00,131.44,173.053,49.411,32.166 -2020-03-19 09:15:00,128.19,170.11599999999999,49.411,32.166 -2020-03-19 09:30:00,130.89,170.173,49.411,32.166 -2020-03-19 09:45:00,133.55,168.19299999999998,49.411,32.166 -2020-03-19 10:00:00,135.03,165.09099999999998,45.82899999999999,32.166 -2020-03-19 10:15:00,135.81,163.22,45.82899999999999,32.166 -2020-03-19 10:30:00,135.42,160.829,45.82899999999999,32.166 -2020-03-19 10:45:00,133.48,160.30700000000002,45.82899999999999,32.166 -2020-03-19 11:00:00,134.38,155.732,44.333,32.166 -2020-03-19 11:15:00,136.87,155.691,44.333,32.166 -2020-03-19 11:30:00,136.53,156.089,44.333,32.166 -2020-03-19 11:45:00,135.15,156.935,44.333,32.166 -2020-03-19 12:00:00,134.45,152.355,42.95,32.166 -2020-03-19 12:15:00,132.99,152.64600000000002,42.95,32.166 -2020-03-19 12:30:00,130.93,152.20600000000002,42.95,32.166 -2020-03-19 12:45:00,131.7,152.998,42.95,32.166 -2020-03-19 13:00:00,131.55,152.886,42.489,32.166 -2020-03-19 13:15:00,136.58,151.246,42.489,32.166 -2020-03-19 13:30:00,137.49,149.465,42.489,32.166 -2020-03-19 13:45:00,130.19,148.61,42.489,32.166 -2020-03-19 14:00:00,126.15,150.662,43.448,32.166 -2020-03-19 14:15:00,126.13,149.335,43.448,32.166 -2020-03-19 14:30:00,128.46,149.201,43.448,32.166 -2020-03-19 14:45:00,126.37,150.18,43.448,32.166 -2020-03-19 15:00:00,127.54,151.016,45.994,32.166 -2020-03-19 15:15:00,133.12,149.622,45.994,32.166 -2020-03-19 15:30:00,132.92,148.701,45.994,32.166 -2020-03-19 15:45:00,133.74,147.954,45.994,32.166 -2020-03-19 16:00:00,129.51,151.94,48.167,32.166 -2020-03-19 16:15:00,133.23,154.07,48.167,32.166 -2020-03-19 16:30:00,134.2,154.545,48.167,32.166 -2020-03-19 16:45:00,133.59,153.78799999999998,48.167,32.166 -2020-03-19 17:00:00,130.78,155.813,52.637,32.166 -2020-03-19 17:15:00,129.72,158.317,52.637,32.166 -2020-03-19 17:30:00,130.87,160.888,52.637,32.166 -2020-03-19 17:45:00,140.13,161.56,52.637,32.166 -2020-03-19 18:00:00,142.78,165.331,55.739,32.166 -2020-03-19 18:15:00,139.96,166.188,55.739,32.166 -2020-03-19 18:30:00,137.06,164.53099999999998,55.739,32.166 -2020-03-19 18:45:00,135.97,168.502,55.739,32.166 -2020-03-19 19:00:00,140.2,166.69099999999997,56.36600000000001,32.166 -2020-03-19 19:15:00,139.3,164.68,56.36600000000001,32.166 -2020-03-19 19:30:00,137.47,164.135,56.36600000000001,32.166 -2020-03-19 19:45:00,134.57,162.673,56.36600000000001,32.166 -2020-03-19 20:00:00,124.92,156.805,56.338,32.166 -2020-03-19 20:15:00,122.58,152.644,56.338,32.166 -2020-03-19 20:30:00,124.08,150.756,56.338,32.166 -2020-03-19 20:45:00,121.71,149.05200000000002,56.338,32.166 -2020-03-19 21:00:00,111.74,143.921,49.894,32.166 -2020-03-19 21:15:00,110.78,141.474,49.894,32.166 -2020-03-19 21:30:00,112.46,140.45600000000002,49.894,32.166 -2020-03-19 21:45:00,110.73,139.77100000000002,49.894,32.166 -2020-03-19 22:00:00,106.07,132.833,46.687,32.166 -2020-03-19 22:15:00,102.14,129.44899999999998,46.687,32.166 -2020-03-19 22:30:00,100.56,115.20700000000001,46.687,32.166 -2020-03-19 22:45:00,100.02,107.939,46.687,32.166 -2020-03-19 23:00:00,93.88,100.74600000000001,39.211,32.166 -2020-03-19 23:15:00,92.98,99.32700000000001,39.211,32.166 -2020-03-19 23:30:00,90.87,99.948,39.211,32.166 -2020-03-19 23:45:00,92.97,100.475,39.211,32.166 -2020-03-20 00:00:00,81.31,94.274,36.616,32.166 -2020-03-20 00:15:00,83.1,93.804,36.616,32.166 -2020-03-20 00:30:00,80.74,92.495,36.616,32.166 -2020-03-20 00:45:00,87.41,91.87299999999999,36.616,32.166 -2020-03-20 01:00:00,85.22,92.698,33.799,32.166 -2020-03-20 01:15:00,85.31,93.18799999999999,33.799,32.166 -2020-03-20 01:30:00,79.72,92.814,33.799,32.166 -2020-03-20 01:45:00,79.16,92.711,33.799,32.166 -2020-03-20 02:00:00,75.56,94.837,32.968,32.166 -2020-03-20 02:15:00,78.31,95.037,32.968,32.166 -2020-03-20 02:30:00,84.24,97.10799999999999,32.968,32.166 -2020-03-20 02:45:00,84.88,98.116,32.968,32.166 -2020-03-20 03:00:00,84.31,100.412,33.533,32.166 -2020-03-20 03:15:00,80.11,102.57,33.533,32.166 -2020-03-20 03:30:00,86.07,103.539,33.533,32.166 -2020-03-20 03:45:00,86.71,104.882,33.533,32.166 -2020-03-20 04:00:00,89.04,118.052,36.102,32.166 -2020-03-20 04:15:00,86.87,130.207,36.102,32.166 -2020-03-20 04:30:00,82.36,131.881,36.102,32.166 -2020-03-20 04:45:00,84.69,132.501,36.102,32.166 -2020-03-20 05:00:00,89.4,165.488,42.423,32.166 -2020-03-20 05:15:00,90.87,198.532,42.423,32.166 -2020-03-20 05:30:00,93.45,192.05200000000002,42.423,32.166 -2020-03-20 05:45:00,99.81,182.74599999999998,42.423,32.166 -2020-03-20 06:00:00,105.17,181.84099999999998,55.38,32.166 -2020-03-20 06:15:00,110.26,186.604,55.38,32.166 -2020-03-20 06:30:00,112.63,186.18099999999998,55.38,32.166 -2020-03-20 06:45:00,116.56,189.22299999999998,55.38,32.166 -2020-03-20 07:00:00,123.53,191.669,65.929,32.166 -2020-03-20 07:15:00,124.74,194.675,65.929,32.166 -2020-03-20 07:30:00,128.22,193.28099999999998,65.929,32.166 -2020-03-20 07:45:00,131.67,190.02700000000002,65.929,32.166 -2020-03-20 08:00:00,135.19,187.975,57.336999999999996,32.166 -2020-03-20 08:15:00,135.75,185.748,57.336999999999996,32.166 -2020-03-20 08:30:00,136.9,181.74200000000002,57.336999999999996,32.166 -2020-03-20 08:45:00,138.65,176.588,57.336999999999996,32.166 -2020-03-20 09:00:00,138.65,169.93400000000003,54.226000000000006,32.166 -2020-03-20 09:15:00,141.78,168.017,54.226000000000006,32.166 -2020-03-20 09:30:00,141.41,167.548,54.226000000000006,32.166 -2020-03-20 09:45:00,140.33,165.61,54.226000000000006,32.166 -2020-03-20 10:00:00,137.44,161.526,51.298,32.166 -2020-03-20 10:15:00,138.85,160.215,51.298,32.166 -2020-03-20 10:30:00,139.49,157.942,51.298,32.166 -2020-03-20 10:45:00,140.16,157.05,51.298,32.166 -2020-03-20 11:00:00,135.6,152.503,50.839,32.166 -2020-03-20 11:15:00,134.52,151.446,50.839,32.166 -2020-03-20 11:30:00,136.04,153.204,50.839,32.166 -2020-03-20 11:45:00,132.53,153.84799999999998,50.839,32.166 -2020-03-20 12:00:00,128.08,150.317,47.976000000000006,32.166 -2020-03-20 12:15:00,131.48,148.69899999999998,47.976000000000006,32.166 -2020-03-20 12:30:00,131.48,148.384,47.976000000000006,32.166 -2020-03-20 12:45:00,128.91,149.39600000000002,47.976000000000006,32.166 -2020-03-20 13:00:00,125.64,150.276,46.299,32.166 -2020-03-20 13:15:00,127.45,149.352,46.299,32.166 -2020-03-20 13:30:00,121.83,147.784,46.299,32.166 -2020-03-20 13:45:00,121.45,146.958,46.299,32.166 -2020-03-20 14:00:00,122.13,147.91299999999998,44.971000000000004,32.166 -2020-03-20 14:15:00,122.95,146.52700000000002,44.971000000000004,32.166 -2020-03-20 14:30:00,119.93,147.18,44.971000000000004,32.166 -2020-03-20 14:45:00,118.5,148.24,44.971000000000004,32.166 -2020-03-20 15:00:00,118.31,148.666,47.48,32.166 -2020-03-20 15:15:00,118.3,146.813,47.48,32.166 -2020-03-20 15:30:00,117.43,144.417,47.48,32.166 -2020-03-20 15:45:00,118.9,143.97299999999998,47.48,32.166 -2020-03-20 16:00:00,119.03,146.799,50.648,32.166 -2020-03-20 16:15:00,117.4,149.28,50.648,32.166 -2020-03-20 16:30:00,117.62,149.784,50.648,32.166 -2020-03-20 16:45:00,116.65,148.667,50.648,32.166 -2020-03-20 17:00:00,118.31,151.38299999999998,56.251000000000005,32.166 -2020-03-20 17:15:00,119.69,153.495,56.251000000000005,32.166 -2020-03-20 17:30:00,121.15,155.864,56.251000000000005,32.166 -2020-03-20 17:45:00,124.53,156.298,56.251000000000005,32.166 -2020-03-20 18:00:00,130.12,160.666,58.982,32.166 -2020-03-20 18:15:00,129.06,160.994,58.982,32.166 -2020-03-20 18:30:00,130.46,159.622,58.982,32.166 -2020-03-20 18:45:00,132.02,163.724,58.982,32.166 -2020-03-20 19:00:00,134.39,162.879,57.293,32.166 -2020-03-20 19:15:00,130.14,162.16299999999998,57.293,32.166 -2020-03-20 19:30:00,128.96,161.32399999999998,57.293,32.166 -2020-03-20 19:45:00,127.91,159.238,57.293,32.166 -2020-03-20 20:00:00,121.85,153.34799999999998,59.433,32.166 -2020-03-20 20:15:00,124.01,149.438,59.433,32.166 -2020-03-20 20:30:00,121.62,147.405,59.433,32.166 -2020-03-20 20:45:00,115.58,145.928,59.433,32.166 -2020-03-20 21:00:00,107.59,141.58700000000002,52.153999999999996,32.166 -2020-03-20 21:15:00,107.1,139.983,52.153999999999996,32.166 -2020-03-20 21:30:00,102.66,138.957,52.153999999999996,32.166 -2020-03-20 21:45:00,102.64,138.795,52.153999999999996,32.166 -2020-03-20 22:00:00,95.47,132.6,47.125,32.166 -2020-03-20 22:15:00,92.65,129.063,47.125,32.166 -2020-03-20 22:30:00,94.55,121.314,47.125,32.166 -2020-03-20 22:45:00,96.74,117.149,47.125,32.166 -2020-03-20 23:00:00,91.54,110.07799999999999,41.236000000000004,32.166 -2020-03-20 23:15:00,84.93,106.649,41.236000000000004,32.166 -2020-03-20 23:30:00,83.48,105.56200000000001,41.236000000000004,32.166 -2020-03-20 23:45:00,79.79,105.538,41.236000000000004,32.166 -2020-03-21 00:00:00,76.15,92.06,36.484,31.988000000000003 -2020-03-21 00:15:00,76.52,88.061,36.484,31.988000000000003 -2020-03-21 00:30:00,75.95,87.62700000000001,36.484,31.988000000000003 -2020-03-21 00:45:00,78.77,87.333,36.484,31.988000000000003 -2020-03-21 01:00:00,78.62,88.72,32.391999999999996,31.988000000000003 -2020-03-21 01:15:00,79.74,88.615,32.391999999999996,31.988000000000003 -2020-03-21 01:30:00,75.68,87.559,32.391999999999996,31.988000000000003 -2020-03-21 01:45:00,75.56,87.695,32.391999999999996,31.988000000000003 -2020-03-21 02:00:00,77.03,90.04299999999999,30.194000000000003,31.988000000000003 -2020-03-21 02:15:00,75.94,89.686,30.194000000000003,31.988000000000003 -2020-03-21 02:30:00,75.96,90.661,30.194000000000003,31.988000000000003 -2020-03-21 02:45:00,70.86,92.00399999999999,30.194000000000003,31.988000000000003 -2020-03-21 03:00:00,76.24,94.37,29.677,31.988000000000003 -2020-03-21 03:15:00,77.68,95.339,29.677,31.988000000000003 -2020-03-21 03:30:00,77.67,95.18299999999999,29.677,31.988000000000003 -2020-03-21 03:45:00,73.57,97.11200000000001,29.677,31.988000000000003 -2020-03-21 04:00:00,69.82,106.507,29.616,31.988000000000003 -2020-03-21 04:15:00,70.66,116.397,29.616,31.988000000000003 -2020-03-21 04:30:00,72.22,115.867,29.616,31.988000000000003 -2020-03-21 04:45:00,72.34,116.185,29.616,31.988000000000003 -2020-03-21 05:00:00,73.28,134.24200000000002,29.625,31.988000000000003 -2020-03-21 05:15:00,70.66,149.116,29.625,31.988000000000003 -2020-03-21 05:30:00,72.31,143.395,29.625,31.988000000000003 -2020-03-21 05:45:00,71.97,139.756,29.625,31.988000000000003 -2020-03-21 06:00:00,72.01,156.734,30.551,31.988000000000003 -2020-03-21 06:15:00,74.11,176.644,30.551,31.988000000000003 -2020-03-21 06:30:00,70.85,171.11,30.551,31.988000000000003 -2020-03-21 06:45:00,75.14,166.157,30.551,31.988000000000003 -2020-03-21 07:00:00,77.83,164.997,34.865,31.988000000000003 -2020-03-21 07:15:00,78.22,166.58599999999998,34.865,31.988000000000003 -2020-03-21 07:30:00,79.47,167.74200000000002,34.865,31.988000000000003 -2020-03-21 07:45:00,80.88,167.858,34.865,31.988000000000003 -2020-03-21 08:00:00,82.4,168.946,41.456,31.988000000000003 -2020-03-21 08:15:00,82.02,169.545,41.456,31.988000000000003 -2020-03-21 08:30:00,82.13,166.77,41.456,31.988000000000003 -2020-03-21 08:45:00,83.0,164.435,41.456,31.988000000000003 -2020-03-21 09:00:00,84.22,160.035,43.001999999999995,31.988000000000003 -2020-03-21 09:15:00,84.04,158.866,43.001999999999995,31.988000000000003 -2020-03-21 09:30:00,83.23,159.287,43.001999999999995,31.988000000000003 -2020-03-21 09:45:00,82.37,157.369,43.001999999999995,31.988000000000003 -2020-03-21 10:00:00,81.8,153.608,42.047,31.988000000000003 -2020-03-21 10:15:00,80.91,152.577,42.047,31.988000000000003 -2020-03-21 10:30:00,79.58,150.36700000000002,42.047,31.988000000000003 -2020-03-21 10:45:00,80.21,150.458,42.047,31.988000000000003 -2020-03-21 11:00:00,81.27,145.994,39.894,31.988000000000003 -2020-03-21 11:15:00,82.37,144.628,39.894,31.988000000000003 -2020-03-21 11:30:00,82.93,145.55,39.894,31.988000000000003 -2020-03-21 11:45:00,86.04,145.608,39.894,31.988000000000003 -2020-03-21 12:00:00,81.5,141.393,38.122,31.988000000000003 -2020-03-21 12:15:00,78.76,140.543,38.122,31.988000000000003 -2020-03-21 12:30:00,77.88,140.417,38.122,31.988000000000003 -2020-03-21 12:45:00,78.92,140.98,38.122,31.988000000000003 -2020-03-21 13:00:00,73.16,141.30200000000002,34.645,31.988000000000003 -2020-03-21 13:15:00,75.26,138.506,34.645,31.988000000000003 -2020-03-21 13:30:00,72.49,136.60399999999998,34.645,31.988000000000003 -2020-03-21 13:45:00,72.8,135.825,34.645,31.988000000000003 -2020-03-21 14:00:00,71.82,137.874,33.739000000000004,31.988000000000003 -2020-03-21 14:15:00,68.31,135.69299999999998,33.739000000000004,31.988000000000003 -2020-03-21 14:30:00,69.19,134.681,33.739000000000004,31.988000000000003 -2020-03-21 14:45:00,72.21,136.053,33.739000000000004,31.988000000000003 -2020-03-21 15:00:00,73.63,137.141,35.908,31.988000000000003 -2020-03-21 15:15:00,74.28,136.116,35.908,31.988000000000003 -2020-03-21 15:30:00,72.5,135.011,35.908,31.988000000000003 -2020-03-21 15:45:00,76.03,134.32299999999998,35.908,31.988000000000003 -2020-03-21 16:00:00,78.29,136.875,39.249,31.988000000000003 -2020-03-21 16:15:00,79.04,139.828,39.249,31.988000000000003 -2020-03-21 16:30:00,80.66,140.35,39.249,31.988000000000003 -2020-03-21 16:45:00,82.94,139.906,39.249,31.988000000000003 -2020-03-21 17:00:00,86.08,141.953,46.045,31.988000000000003 -2020-03-21 17:15:00,88.1,144.812,46.045,31.988000000000003 -2020-03-21 17:30:00,90.2,147.079,46.045,31.988000000000003 -2020-03-21 17:45:00,92.79,147.321,46.045,31.988000000000003 -2020-03-21 18:00:00,97.5,151.714,48.238,31.988000000000003 -2020-03-21 18:15:00,97.87,154.032,48.238,31.988000000000003 -2020-03-21 18:30:00,100.71,154.118,48.238,31.988000000000003 -2020-03-21 18:45:00,101.17,154.534,48.238,31.988000000000003 -2020-03-21 19:00:00,107.14,153.971,46.785,31.988000000000003 -2020-03-21 19:15:00,105.31,152.583,46.785,31.988000000000003 -2020-03-21 19:30:00,103.63,152.597,46.785,31.988000000000003 -2020-03-21 19:45:00,103.35,150.908,46.785,31.988000000000003 -2020-03-21 20:00:00,97.14,147.029,39.830999999999996,31.988000000000003 -2020-03-21 20:15:00,94.26,144.661,39.830999999999996,31.988000000000003 -2020-03-21 20:30:00,91.73,142.124,39.830999999999996,31.988000000000003 -2020-03-21 20:45:00,90.18,140.87,39.830999999999996,31.988000000000003 -2020-03-21 21:00:00,84.4,137.915,34.063,31.988000000000003 -2020-03-21 21:15:00,85.72,136.566,34.063,31.988000000000003 -2020-03-21 21:30:00,84.05,136.578,34.063,31.988000000000003 -2020-03-21 21:45:00,83.62,135.95600000000002,34.063,31.988000000000003 -2020-03-21 22:00:00,79.7,130.829,34.455999999999996,31.988000000000003 -2020-03-21 22:15:00,78.84,129.39,34.455999999999996,31.988000000000003 -2020-03-21 22:30:00,73.84,126.485,34.455999999999996,31.988000000000003 -2020-03-21 22:45:00,75.89,123.93,34.455999999999996,31.988000000000003 -2020-03-21 23:00:00,70.82,118.62,27.840999999999998,31.988000000000003 -2020-03-21 23:15:00,70.94,114.022,27.840999999999998,31.988000000000003 -2020-03-21 23:30:00,69.79,112.245,27.840999999999998,31.988000000000003 -2020-03-21 23:45:00,68.39,110.39399999999999,27.840999999999998,31.988000000000003 -2020-03-22 00:00:00,60.08,92.443,20.007,31.988000000000003 -2020-03-22 00:15:00,58.06,87.84200000000001,20.007,31.988000000000003 -2020-03-22 00:30:00,55.51,87.055,20.007,31.988000000000003 -2020-03-22 00:45:00,56.45,87.271,20.007,31.988000000000003 -2020-03-22 01:00:00,54.53,88.59700000000001,17.378,31.988000000000003 -2020-03-22 01:15:00,52.88,89.23700000000001,17.378,31.988000000000003 -2020-03-22 01:30:00,52.07,88.521,17.378,31.988000000000003 -2020-03-22 01:45:00,53.98,88.29799999999999,17.378,31.988000000000003 -2020-03-22 02:00:00,48.89,90.07600000000001,16.145,31.988000000000003 -2020-03-22 02:15:00,53.69,89.28200000000001,16.145,31.988000000000003 -2020-03-22 02:30:00,51.28,90.99600000000001,16.145,31.988000000000003 -2020-03-22 02:45:00,53.95,92.603,16.145,31.988000000000003 -2020-03-22 03:00:00,50.72,95.43799999999999,15.427999999999999,31.988000000000003 -2020-03-22 03:15:00,54.24,96.069,15.427999999999999,31.988000000000003 -2020-03-22 03:30:00,51.64,96.738,15.427999999999999,31.988000000000003 -2020-03-22 03:45:00,51.01,98.361,15.427999999999999,31.988000000000003 -2020-03-22 04:00:00,50.48,107.544,16.663,31.988000000000003 -2020-03-22 04:15:00,51.91,116.501,16.663,31.988000000000003 -2020-03-22 04:30:00,51.98,116.48,16.663,31.988000000000003 -2020-03-22 04:45:00,52.83,116.838,16.663,31.988000000000003 -2020-03-22 05:00:00,51.89,132.247,17.271,31.988000000000003 -2020-03-22 05:15:00,52.74,145.017,17.271,31.988000000000003 -2020-03-22 05:30:00,52.54,139.015,17.271,31.988000000000003 -2020-03-22 05:45:00,54.38,135.485,17.271,31.988000000000003 -2020-03-22 06:00:00,55.46,151.58700000000002,17.612000000000002,31.988000000000003 -2020-03-22 06:15:00,53.48,170.449,17.612000000000002,31.988000000000003 -2020-03-22 06:30:00,55.9,163.799,17.612000000000002,31.988000000000003 -2020-03-22 06:45:00,53.71,157.689,17.612000000000002,31.988000000000003 -2020-03-22 07:00:00,55.53,158.501,20.88,31.988000000000003 -2020-03-22 07:15:00,59.67,158.875,20.88,31.988000000000003 -2020-03-22 07:30:00,62.15,159.44299999999998,20.88,31.988000000000003 -2020-03-22 07:45:00,61.32,158.92700000000002,20.88,31.988000000000003 -2020-03-22 08:00:00,67.05,161.621,25.861,31.988000000000003 -2020-03-22 08:15:00,67.97,162.512,25.861,31.988000000000003 -2020-03-22 08:30:00,69.68,161.264,25.861,31.988000000000003 -2020-03-22 08:45:00,69.61,160.52,25.861,31.988000000000003 -2020-03-22 09:00:00,74.12,155.761,27.921999999999997,31.988000000000003 -2020-03-22 09:15:00,75.03,154.89,27.921999999999997,31.988000000000003 -2020-03-22 09:30:00,74.98,155.334,27.921999999999997,31.988000000000003 -2020-03-22 09:45:00,73.91,153.639,27.921999999999997,31.988000000000003 -2020-03-22 10:00:00,76.48,152.053,29.048000000000002,31.988000000000003 -2020-03-22 10:15:00,73.74,151.536,29.048000000000002,31.988000000000003 -2020-03-22 10:30:00,75.72,149.9,29.048000000000002,31.988000000000003 -2020-03-22 10:45:00,74.12,148.77100000000002,29.048000000000002,31.988000000000003 -2020-03-22 11:00:00,72.15,144.928,32.02,31.988000000000003 -2020-03-22 11:15:00,77.67,143.569,32.02,31.988000000000003 -2020-03-22 11:30:00,77.66,143.93200000000002,32.02,31.988000000000003 -2020-03-22 11:45:00,82.07,144.57399999999998,32.02,31.988000000000003 -2020-03-22 12:00:00,72.99,140.191,28.55,31.988000000000003 -2020-03-22 12:15:00,75.01,140.744,28.55,31.988000000000003 -2020-03-22 12:30:00,71.52,139.441,28.55,31.988000000000003 -2020-03-22 12:45:00,69.58,139.036,28.55,31.988000000000003 -2020-03-22 13:00:00,68.21,138.718,25.601999999999997,31.988000000000003 -2020-03-22 13:15:00,67.52,138.237,25.601999999999997,31.988000000000003 -2020-03-22 13:30:00,66.81,135.855,25.601999999999997,31.988000000000003 -2020-03-22 13:45:00,67.38,134.836,25.601999999999997,31.988000000000003 -2020-03-22 14:00:00,60.59,137.475,23.916999999999998,31.988000000000003 -2020-03-22 14:15:00,59.39,136.376,23.916999999999998,31.988000000000003 -2020-03-22 14:30:00,58.18,136.016,23.916999999999998,31.988000000000003 -2020-03-22 14:45:00,60.55,136.768,23.916999999999998,31.988000000000003 -2020-03-22 15:00:00,59.16,136.64700000000002,24.064,31.988000000000003 -2020-03-22 15:15:00,60.01,136.006,24.064,31.988000000000003 -2020-03-22 15:30:00,56.92,135.28799999999998,24.064,31.988000000000003 -2020-03-22 15:45:00,58.07,135.231,24.064,31.988000000000003 -2020-03-22 16:00:00,61.44,138.836,28.189,31.988000000000003 -2020-03-22 16:15:00,62.16,141.086,28.189,31.988000000000003 -2020-03-22 16:30:00,62.09,142.095,28.189,31.988000000000003 -2020-03-22 16:45:00,63.69,141.726,28.189,31.988000000000003 -2020-03-22 17:00:00,63.44,143.861,37.576,31.988000000000003 -2020-03-22 17:15:00,65.92,146.877,37.576,31.988000000000003 -2020-03-22 17:30:00,69.07,149.62,37.576,31.988000000000003 -2020-03-22 17:45:00,69.42,151.817,37.576,31.988000000000003 -2020-03-22 18:00:00,77.2,155.935,42.669,31.988000000000003 -2020-03-22 18:15:00,75.08,159.263,42.669,31.988000000000003 -2020-03-22 18:30:00,76.8,157.589,42.669,31.988000000000003 -2020-03-22 18:45:00,76.23,159.547,42.669,31.988000000000003 -2020-03-22 19:00:00,79.58,159.303,43.538999999999994,31.988000000000003 -2020-03-22 19:15:00,77.87,158.088,43.538999999999994,31.988000000000003 -2020-03-22 19:30:00,78.53,157.924,43.538999999999994,31.988000000000003 -2020-03-22 19:45:00,80.41,157.30700000000002,43.538999999999994,31.988000000000003 -2020-03-22 20:00:00,79.37,153.393,37.330999999999996,31.988000000000003 -2020-03-22 20:15:00,79.52,151.787,37.330999999999996,31.988000000000003 -2020-03-22 20:30:00,78.47,150.509,37.330999999999996,31.988000000000003 -2020-03-22 20:45:00,77.25,147.811,37.330999999999996,31.988000000000003 -2020-03-22 21:00:00,74.47,142.727,33.856,31.988000000000003 -2020-03-22 21:15:00,73.54,140.791,33.856,31.988000000000003 -2020-03-22 21:30:00,73.03,140.843,33.856,31.988000000000003 -2020-03-22 21:45:00,72.54,140.459,33.856,31.988000000000003 -2020-03-22 22:00:00,69.45,134.984,34.711999999999996,31.988000000000003 -2020-03-22 22:15:00,69.2,132.468,34.711999999999996,31.988000000000003 -2020-03-22 22:30:00,64.59,126.84,34.711999999999996,31.988000000000003 -2020-03-22 22:45:00,67.2,123.23,34.711999999999996,31.988000000000003 -2020-03-22 23:00:00,62.87,115.586,29.698,31.988000000000003 -2020-03-22 23:15:00,62.97,112.869,29.698,31.988000000000003 -2020-03-22 23:30:00,58.12,111.583,29.698,31.988000000000003 -2020-03-22 23:45:00,60.08,110.514,29.698,31.988000000000003 -2020-03-23 00:00:00,82.34,95.79,29.983,32.166 -2020-03-23 00:15:00,83.55,93.75399999999999,29.983,32.166 -2020-03-23 00:30:00,79.21,92.929,29.983,32.166 -2020-03-23 00:45:00,78.16,92.605,29.983,32.166 -2020-03-23 01:00:00,79.25,94.01700000000001,29.122,32.166 -2020-03-23 01:15:00,80.78,94.23700000000001,29.122,32.166 -2020-03-23 01:30:00,76.93,93.65899999999999,29.122,32.166 -2020-03-23 01:45:00,77.19,93.501,29.122,32.166 -2020-03-23 02:00:00,77.95,95.383,28.676,32.166 -2020-03-23 02:15:00,78.39,95.369,28.676,32.166 -2020-03-23 02:30:00,81.25,97.412,28.676,32.166 -2020-03-23 02:45:00,76.31,98.49799999999999,28.676,32.166 -2020-03-23 03:00:00,80.56,102.464,26.552,32.166 -2020-03-23 03:15:00,79.77,104.568,26.552,32.166 -2020-03-23 03:30:00,79.74,105.24799999999999,26.552,32.166 -2020-03-23 03:45:00,77.39,106.31200000000001,26.552,32.166 -2020-03-23 04:00:00,81.0,119.811,27.44,32.166 -2020-03-23 04:15:00,84.58,132.873,27.44,32.166 -2020-03-23 04:30:00,82.05,134.373,27.44,32.166 -2020-03-23 04:45:00,82.35,134.965,27.44,32.166 -2020-03-23 05:00:00,81.8,164.62599999999998,36.825,32.166 -2020-03-23 05:15:00,86.07,196.108,36.825,32.166 -2020-03-23 05:30:00,94.52,189.77700000000002,36.825,32.166 -2020-03-23 05:45:00,100.38,180.954,36.825,32.166 -2020-03-23 06:00:00,108.8,180.22799999999998,56.589,32.166 -2020-03-23 06:15:00,114.59,184.75799999999998,56.589,32.166 -2020-03-23 06:30:00,111.33,185.50099999999998,56.589,32.166 -2020-03-23 06:45:00,111.22,187.83,56.589,32.166 -2020-03-23 07:00:00,120.07,190.862,67.49,32.166 -2020-03-23 07:15:00,121.71,192.895,67.49,32.166 -2020-03-23 07:30:00,125.53,192.547,67.49,32.166 -2020-03-23 07:45:00,126.83,190.105,67.49,32.166 -2020-03-23 08:00:00,132.73,188.553,60.028,32.166 -2020-03-23 08:15:00,130.98,187.391,60.028,32.166 -2020-03-23 08:30:00,131.58,182.579,60.028,32.166 -2020-03-23 08:45:00,133.7,179.28900000000002,60.028,32.166 -2020-03-23 09:00:00,132.55,173.55599999999998,55.018,32.166 -2020-03-23 09:15:00,133.07,169.37900000000002,55.018,32.166 -2020-03-23 09:30:00,135.0,168.774,55.018,32.166 -2020-03-23 09:45:00,134.08,166.667,55.018,32.166 -2020-03-23 10:00:00,129.98,164.472,51.183,32.166 -2020-03-23 10:15:00,130.53,163.696,51.183,32.166 -2020-03-23 10:30:00,128.68,161.246,51.183,32.166 -2020-03-23 10:45:00,130.19,160.298,51.183,32.166 -2020-03-23 11:00:00,126.38,154.475,50.065,32.166 -2020-03-23 11:15:00,128.38,154.691,50.065,32.166 -2020-03-23 11:30:00,129.3,156.38,50.065,32.166 -2020-03-23 11:45:00,130.58,156.843,50.065,32.166 -2020-03-23 12:00:00,126.27,153.531,48.141999999999996,32.166 -2020-03-23 12:15:00,128.79,154.124,48.141999999999996,32.166 -2020-03-23 12:30:00,127.53,152.702,48.141999999999996,32.166 -2020-03-23 12:45:00,127.44,153.553,48.141999999999996,32.166 -2020-03-23 13:00:00,125.66,154.064,47.887,32.166 -2020-03-23 13:15:00,135.19,152.185,47.887,32.166 -2020-03-23 13:30:00,135.03,149.401,47.887,32.166 -2020-03-23 13:45:00,132.18,148.64700000000002,47.887,32.166 -2020-03-23 14:00:00,128.57,150.628,48.571000000000005,32.166 -2020-03-23 14:15:00,127.42,149.127,48.571000000000005,32.166 -2020-03-23 14:30:00,130.25,148.237,48.571000000000005,32.166 -2020-03-23 14:45:00,133.63,149.51,48.571000000000005,32.166 -2020-03-23 15:00:00,134.03,150.812,49.937,32.166 -2020-03-23 15:15:00,132.5,148.795,49.937,32.166 -2020-03-23 15:30:00,125.71,147.537,49.937,32.166 -2020-03-23 15:45:00,123.67,146.966,49.937,32.166 -2020-03-23 16:00:00,123.22,150.901,52.963,32.166 -2020-03-23 16:15:00,127.85,152.52,52.963,32.166 -2020-03-23 16:30:00,129.07,152.556,52.963,32.166 -2020-03-23 16:45:00,128.76,151.165,52.963,32.166 -2020-03-23 17:00:00,126.16,152.803,61.163999999999994,32.166 -2020-03-23 17:15:00,121.94,155.144,61.163999999999994,32.166 -2020-03-23 17:30:00,125.11,157.346,61.163999999999994,32.166 -2020-03-23 17:45:00,128.06,158.218,61.163999999999994,32.166 -2020-03-23 18:00:00,127.94,162.369,63.788999999999994,32.166 -2020-03-23 18:15:00,126.59,163.451,63.788999999999994,32.166 -2020-03-23 18:30:00,126.68,162.086,63.788999999999994,32.166 -2020-03-23 18:45:00,129.76,165.52599999999998,63.788999999999994,32.166 -2020-03-23 19:00:00,131.55,163.859,63.913000000000004,32.166 -2020-03-23 19:15:00,124.43,162.089,63.913000000000004,32.166 -2020-03-23 19:30:00,127.1,162.246,63.913000000000004,32.166 -2020-03-23 19:45:00,128.86,160.82399999999998,63.913000000000004,32.166 -2020-03-23 20:00:00,128.96,154.60299999999998,65.44,32.166 -2020-03-23 20:15:00,120.05,151.42600000000002,65.44,32.166 -2020-03-23 20:30:00,119.49,148.838,65.44,32.166 -2020-03-23 20:45:00,118.33,147.517,65.44,32.166 -2020-03-23 21:00:00,111.59,142.67700000000002,59.117,32.166 -2020-03-23 21:15:00,108.56,139.935,59.117,32.166 -2020-03-23 21:30:00,108.54,139.444,59.117,32.166 -2020-03-23 21:45:00,110.95,138.616,59.117,32.166 -2020-03-23 22:00:00,104.38,130.222,52.301,32.166 -2020-03-23 22:15:00,99.93,127.251,52.301,32.166 -2020-03-23 22:30:00,99.37,112.665,52.301,32.166 -2020-03-23 22:45:00,98.94,105.17399999999999,52.301,32.166 -2020-03-23 23:00:00,93.28,98.132,44.373000000000005,32.166 -2020-03-23 23:15:00,87.35,97.09299999999999,44.373000000000005,32.166 -2020-03-23 23:30:00,90.84,98.01299999999999,44.373000000000005,32.166 -2020-03-23 23:45:00,92.8,98.931,44.373000000000005,32.166 -2020-03-24 00:00:00,87.21,94.149,44.647,32.166 -2020-03-24 00:15:00,81.24,93.507,44.647,32.166 -2020-03-24 00:30:00,77.75,92.161,44.647,32.166 -2020-03-24 00:45:00,80.38,91.33200000000001,44.647,32.166 -2020-03-24 01:00:00,79.49,92.387,41.433,32.166 -2020-03-24 01:15:00,81.5,92.28,41.433,32.166 -2020-03-24 01:30:00,81.05,91.77600000000001,41.433,32.166 -2020-03-24 01:45:00,82.59,91.697,41.433,32.166 -2020-03-24 02:00:00,74.4,93.40899999999999,39.909,32.166 -2020-03-24 02:15:00,83.06,93.669,39.909,32.166 -2020-03-24 02:30:00,82.28,95.147,39.909,32.166 -2020-03-24 02:45:00,84.99,96.35,39.909,32.166 -2020-03-24 03:00:00,80.16,99.23100000000001,39.14,32.166 -2020-03-24 03:15:00,82.68,100.959,39.14,32.166 -2020-03-24 03:30:00,86.24,101.995,39.14,32.166 -2020-03-24 03:45:00,85.57,102.86200000000001,39.14,32.166 -2020-03-24 04:00:00,85.36,115.8,40.015,32.166 -2020-03-24 04:15:00,84.21,128.582,40.015,32.166 -2020-03-24 04:30:00,88.34,129.81799999999998,40.015,32.166 -2020-03-24 04:45:00,83.04,131.484,40.015,32.166 -2020-03-24 05:00:00,83.64,165.446,44.93600000000001,32.166 -2020-03-24 05:15:00,88.14,196.958,44.93600000000001,32.166 -2020-03-24 05:30:00,90.73,189.49900000000002,44.93600000000001,32.166 -2020-03-24 05:45:00,92.93,180.458,44.93600000000001,32.166 -2020-03-24 06:00:00,104.65,179.174,57.271,32.166 -2020-03-24 06:15:00,109.33,185.017,57.271,32.166 -2020-03-24 06:30:00,113.7,185.11599999999999,57.271,32.166 -2020-03-24 06:45:00,113.97,186.847,57.271,32.166 -2020-03-24 07:00:00,123.2,189.797,68.352,32.166 -2020-03-24 07:15:00,124.58,191.597,68.352,32.166 -2020-03-24 07:30:00,126.5,190.78599999999997,68.352,32.166 -2020-03-24 07:45:00,129.13,188.18099999999998,68.352,32.166 -2020-03-24 08:00:00,133.3,186.676,60.717,32.166 -2020-03-24 08:15:00,134.37,184.57,60.717,32.166 -2020-03-24 08:30:00,134.29,179.618,60.717,32.166 -2020-03-24 08:45:00,134.4,175.868,60.717,32.166 -2020-03-24 09:00:00,132.5,169.576,54.603,32.166 -2020-03-24 09:15:00,131.97,166.65,54.603,32.166 -2020-03-24 09:30:00,130.44,166.808,54.603,32.166 -2020-03-24 09:45:00,126.35,164.928,54.603,32.166 -2020-03-24 10:00:00,123.85,161.881,52.308,32.166 -2020-03-24 10:15:00,123.58,160.246,52.308,32.166 -2020-03-24 10:30:00,121.34,157.974,52.308,32.166 -2020-03-24 10:45:00,117.88,157.553,52.308,32.166 -2020-03-24 11:00:00,119.32,152.941,51.838,32.166 -2020-03-24 11:15:00,119.66,153.012,51.838,32.166 -2020-03-24 11:30:00,122.6,153.441,51.838,32.166 -2020-03-24 11:45:00,119.94,154.38299999999998,51.838,32.166 -2020-03-24 12:00:00,116.79,149.907,50.375,32.166 -2020-03-24 12:15:00,116.39,150.251,50.375,32.166 -2020-03-24 12:30:00,117.13,149.612,50.375,32.166 -2020-03-24 12:45:00,113.27,150.39600000000002,50.375,32.166 -2020-03-24 13:00:00,113.92,150.511,50.735,32.166 -2020-03-24 13:15:00,118.49,148.784,50.735,32.166 -2020-03-24 13:30:00,117.98,146.963,50.735,32.166 -2020-03-24 13:45:00,116.71,146.122,50.735,32.166 -2020-03-24 14:00:00,115.13,148.52100000000002,50.946000000000005,32.166 -2020-03-24 14:15:00,113.45,147.077,50.946000000000005,32.166 -2020-03-24 14:30:00,110.95,146.75799999999998,50.946000000000005,32.166 -2020-03-24 14:45:00,110.16,147.769,50.946000000000005,32.166 -2020-03-24 15:00:00,114.6,148.683,53.18,32.166 -2020-03-24 15:15:00,116.6,147.15,53.18,32.166 -2020-03-24 15:30:00,109.45,145.975,53.18,32.166 -2020-03-24 15:45:00,117.18,145.137,53.18,32.166 -2020-03-24 16:00:00,117.12,149.214,54.928999999999995,32.166 -2020-03-24 16:15:00,118.32,151.22299999999998,54.928999999999995,32.166 -2020-03-24 16:30:00,119.29,151.694,54.928999999999995,32.166 -2020-03-24 16:45:00,117.54,150.678,54.928999999999995,32.166 -2020-03-24 17:00:00,119.96,152.85299999999998,60.913000000000004,32.166 -2020-03-24 17:15:00,118.56,155.32399999999998,60.913000000000004,32.166 -2020-03-24 17:30:00,119.39,157.963,60.913000000000004,32.166 -2020-03-24 17:45:00,119.91,158.639,60.913000000000004,32.166 -2020-03-24 18:00:00,123.8,162.444,62.214,32.166 -2020-03-24 18:15:00,123.93,163.588,62.214,32.166 -2020-03-24 18:30:00,123.06,161.892,62.214,32.166 -2020-03-24 18:45:00,120.31,165.94099999999997,62.214,32.166 -2020-03-24 19:00:00,128.27,163.985,62.38,32.166 -2020-03-24 19:15:00,128.22,162.053,62.38,32.166 -2020-03-24 19:30:00,127.86,161.632,62.38,32.166 -2020-03-24 19:45:00,126.09,160.375,62.38,32.166 -2020-03-24 20:00:00,128.41,154.361,65.018,32.166 -2020-03-24 20:15:00,124.95,150.27200000000002,65.018,32.166 -2020-03-24 20:30:00,121.53,148.543,65.018,32.166 -2020-03-24 20:45:00,118.91,146.891,65.018,32.166 -2020-03-24 21:00:00,112.76,141.732,56.416000000000004,32.166 -2020-03-24 21:15:00,116.6,139.299,56.416000000000004,32.166 -2020-03-24 21:30:00,112.66,138.269,56.416000000000004,32.166 -2020-03-24 21:45:00,107.6,137.704,56.416000000000004,32.166 -2020-03-24 22:00:00,104.55,130.731,52.846000000000004,32.166 -2020-03-24 22:15:00,103.78,127.48100000000001,52.846000000000004,32.166 -2020-03-24 22:30:00,102.03,113.005,52.846000000000004,32.166 -2020-03-24 22:45:00,97.36,105.73200000000001,52.846000000000004,32.166 -2020-03-24 23:00:00,93.61,98.484,44.435,32.166 -2020-03-24 23:15:00,97.37,97.18700000000001,44.435,32.166 -2020-03-24 23:30:00,91.61,97.818,44.435,32.166 -2020-03-24 23:45:00,86.1,98.45700000000001,44.435,32.166 -2020-03-25 00:00:00,86.32,93.757,42.527,32.166 -2020-03-25 00:15:00,85.72,93.12899999999999,42.527,32.166 -2020-03-25 00:30:00,83.35,91.76899999999999,42.527,32.166 -2020-03-25 00:45:00,80.67,90.944,42.527,32.166 -2020-03-25 01:00:00,76.45,91.96600000000001,38.655,32.166 -2020-03-25 01:15:00,74.51,91.84,38.655,32.166 -2020-03-25 01:30:00,78.1,91.316,38.655,32.166 -2020-03-25 01:45:00,84.0,91.244,38.655,32.166 -2020-03-25 02:00:00,80.41,92.943,36.912,32.166 -2020-03-25 02:15:00,80.95,93.19200000000001,36.912,32.166 -2020-03-25 02:30:00,75.56,94.69,36.912,32.166 -2020-03-25 02:45:00,79.65,95.89399999999999,36.912,32.166 -2020-03-25 03:00:00,81.67,98.792,36.98,32.166 -2020-03-25 03:15:00,81.92,100.49799999999999,36.98,32.166 -2020-03-25 03:30:00,82.53,101.52600000000001,36.98,32.166 -2020-03-25 03:45:00,81.28,102.40899999999999,36.98,32.166 -2020-03-25 04:00:00,76.87,115.345,38.052,32.166 -2020-03-25 04:15:00,82.91,128.107,38.052,32.166 -2020-03-25 04:30:00,87.12,129.356,38.052,32.166 -2020-03-25 04:45:00,89.8,131.01,38.052,32.166 -2020-03-25 05:00:00,84.78,164.93,42.455,32.166 -2020-03-25 05:15:00,86.31,196.429,42.455,32.166 -2020-03-25 05:30:00,91.91,188.94799999999998,42.455,32.166 -2020-03-25 05:45:00,94.23,179.926,42.455,32.166 -2020-03-25 06:00:00,103.12,178.65200000000002,57.986000000000004,32.166 -2020-03-25 06:15:00,110.12,184.49099999999999,57.986000000000004,32.166 -2020-03-25 06:30:00,113.23,184.548,57.986000000000004,32.166 -2020-03-25 06:45:00,114.61,186.25900000000001,57.986000000000004,32.166 -2020-03-25 07:00:00,121.68,189.222,71.868,32.166 -2020-03-25 07:15:00,122.82,190.99200000000002,71.868,32.166 -2020-03-25 07:30:00,123.06,190.141,71.868,32.166 -2020-03-25 07:45:00,125.56,187.507,71.868,32.166 -2020-03-25 08:00:00,131.42,185.979,62.225,32.166 -2020-03-25 08:15:00,131.94,183.877,62.225,32.166 -2020-03-25 08:30:00,129.5,178.877,62.225,32.166 -2020-03-25 08:45:00,130.38,175.15099999999998,62.225,32.166 -2020-03-25 09:00:00,128.7,168.872,58.802,32.166 -2020-03-25 09:15:00,130.92,165.947,58.802,32.166 -2020-03-25 09:30:00,130.3,166.12400000000002,58.802,32.166 -2020-03-25 09:45:00,129.49,164.265,58.802,32.166 -2020-03-25 10:00:00,129.92,161.23,54.122,32.166 -2020-03-25 10:15:00,129.34,159.642,54.122,32.166 -2020-03-25 10:30:00,131.48,157.39600000000002,54.122,32.166 -2020-03-25 10:45:00,131.23,156.995,54.122,32.166 -2020-03-25 11:00:00,133.57,152.376,54.368,32.166 -2020-03-25 11:15:00,133.01,152.469,54.368,32.166 -2020-03-25 11:30:00,135.91,152.905,54.368,32.166 -2020-03-25 11:45:00,134.24,153.866,54.368,32.166 -2020-03-25 12:00:00,134.65,149.411,52.74,32.166 -2020-03-25 12:15:00,134.3,149.766,52.74,32.166 -2020-03-25 12:30:00,131.7,149.085,52.74,32.166 -2020-03-25 12:45:00,131.3,149.868,52.74,32.166 -2020-03-25 13:00:00,128.24,150.028,52.544,32.166 -2020-03-25 13:15:00,135.9,148.285,52.544,32.166 -2020-03-25 13:30:00,134.63,146.457,52.544,32.166 -2020-03-25 13:45:00,130.07,145.619,52.544,32.166 -2020-03-25 14:00:00,125.5,148.08700000000002,53.602,32.166 -2020-03-25 14:15:00,124.21,146.62,53.602,32.166 -2020-03-25 14:30:00,121.48,146.262,53.602,32.166 -2020-03-25 14:45:00,124.82,147.278,53.602,32.166 -2020-03-25 15:00:00,128.87,148.209,55.59,32.166 -2020-03-25 15:15:00,132.49,146.649,55.59,32.166 -2020-03-25 15:30:00,130.48,145.423,55.59,32.166 -2020-03-25 15:45:00,125.19,144.566,55.59,32.166 -2020-03-25 16:00:00,124.83,148.662,57.586999999999996,32.166 -2020-03-25 16:15:00,120.76,150.645,57.586999999999996,32.166 -2020-03-25 16:30:00,126.07,151.116,57.586999999999996,32.166 -2020-03-25 16:45:00,129.45,150.047,57.586999999999996,32.166 -2020-03-25 17:00:00,129.43,152.254,62.111999999999995,32.166 -2020-03-25 17:15:00,125.36,154.717,62.111999999999995,32.166 -2020-03-25 17:30:00,128.18,157.368,62.111999999999995,32.166 -2020-03-25 17:45:00,127.2,158.043,62.111999999999995,32.166 -2020-03-25 18:00:00,131.15,161.856,64.605,32.166 -2020-03-25 18:15:00,124.35,163.05700000000002,64.605,32.166 -2020-03-25 18:30:00,128.29,161.352,64.605,32.166 -2020-03-25 18:45:00,127.92,165.417,64.605,32.166 -2020-03-25 19:00:00,132.44,163.43200000000002,65.55199999999999,32.166 -2020-03-25 19:15:00,123.83,161.515,65.55199999999999,32.166 -2020-03-25 19:30:00,129.2,161.12,65.55199999999999,32.166 -2020-03-25 19:45:00,131.36,159.905,65.55199999999999,32.166 -2020-03-25 20:00:00,129.62,153.862,66.778,32.166 -2020-03-25 20:15:00,119.8,149.78799999999998,66.778,32.166 -2020-03-25 20:30:00,115.17,148.091,66.778,32.166 -2020-03-25 20:45:00,115.5,146.44899999999998,66.778,32.166 -2020-03-25 21:00:00,109.54,141.285,56.103,32.166 -2020-03-25 21:15:00,115.91,138.856,56.103,32.166 -2020-03-25 21:30:00,110.45,137.822,56.103,32.166 -2020-03-25 21:45:00,111.02,137.282,56.103,32.166 -2020-03-25 22:00:00,101.35,130.30200000000002,51.371,32.166 -2020-03-25 22:15:00,99.56,127.07799999999999,51.371,32.166 -2020-03-25 22:30:00,100.15,112.553,51.371,32.166 -2020-03-25 22:45:00,100.47,105.279,51.371,32.166 -2020-03-25 23:00:00,91.64,98.023,42.798,32.166 -2020-03-25 23:15:00,90.67,96.74799999999999,42.798,32.166 -2020-03-25 23:30:00,91.31,97.381,42.798,32.166 -2020-03-25 23:45:00,89.57,98.04299999999999,42.798,32.166 -2020-03-26 00:00:00,81.94,93.36200000000001,39.069,32.166 -2020-03-26 00:15:00,81.2,92.749,39.069,32.166 -2020-03-26 00:30:00,84.64,91.374,39.069,32.166 -2020-03-26 00:45:00,84.28,90.554,39.069,32.166 -2020-03-26 01:00:00,81.91,91.542,37.043,32.166 -2020-03-26 01:15:00,73.61,91.398,37.043,32.166 -2020-03-26 01:30:00,80.41,90.853,37.043,32.166 -2020-03-26 01:45:00,81.37,90.79,37.043,32.166 -2020-03-26 02:00:00,77.06,92.475,34.625,32.166 -2020-03-26 02:15:00,76.31,92.712,34.625,32.166 -2020-03-26 02:30:00,71.77,94.23,34.625,32.166 -2020-03-26 02:45:00,75.66,95.43700000000001,34.625,32.166 -2020-03-26 03:00:00,77.66,98.35,33.812,32.166 -2020-03-26 03:15:00,81.48,100.03200000000001,33.812,32.166 -2020-03-26 03:30:00,74.62,101.055,33.812,32.166 -2020-03-26 03:45:00,78.13,101.95299999999999,33.812,32.166 -2020-03-26 04:00:00,75.81,114.887,35.236999999999995,32.166 -2020-03-26 04:15:00,75.77,127.63,35.236999999999995,32.166 -2020-03-26 04:30:00,74.86,128.892,35.236999999999995,32.166 -2020-03-26 04:45:00,78.41,130.533,35.236999999999995,32.166 -2020-03-26 05:00:00,84.12,164.412,40.375,32.166 -2020-03-26 05:15:00,86.05,195.898,40.375,32.166 -2020-03-26 05:30:00,87.24,188.39700000000002,40.375,32.166 -2020-03-26 05:45:00,92.96,179.391,40.375,32.166 -2020-03-26 06:00:00,98.47,178.128,52.316,32.166 -2020-03-26 06:15:00,106.48,183.963,52.316,32.166 -2020-03-26 06:30:00,108.98,183.97799999999998,52.316,32.166 -2020-03-26 06:45:00,110.31,185.66400000000002,52.316,32.166 -2020-03-26 07:00:00,119.63,188.642,64.115,32.166 -2020-03-26 07:15:00,121.23,190.38400000000001,64.115,32.166 -2020-03-26 07:30:00,122.47,189.49099999999999,64.115,32.166 -2020-03-26 07:45:00,125.54,186.829,64.115,32.166 -2020-03-26 08:00:00,126.35,185.27700000000002,55.033,32.166 -2020-03-26 08:15:00,127.67,183.18099999999998,55.033,32.166 -2020-03-26 08:30:00,128.36,178.132,55.033,32.166 -2020-03-26 08:45:00,129.96,174.43099999999998,55.033,32.166 -2020-03-26 09:00:00,128.86,168.16400000000002,49.411,32.166 -2020-03-26 09:15:00,126.99,165.24200000000002,49.411,32.166 -2020-03-26 09:30:00,128.41,165.438,49.411,32.166 -2020-03-26 09:45:00,128.5,163.6,49.411,32.166 -2020-03-26 10:00:00,125.96,160.575,45.82899999999999,32.166 -2020-03-26 10:15:00,126.19,159.036,45.82899999999999,32.166 -2020-03-26 10:30:00,122.45,156.815,45.82899999999999,32.166 -2020-03-26 10:45:00,122.6,156.434,45.82899999999999,32.166 -2020-03-26 11:00:00,124.89,151.809,44.333,32.166 -2020-03-26 11:15:00,127.98,151.92600000000002,44.333,32.166 -2020-03-26 11:30:00,126.17,152.36700000000002,44.333,32.166 -2020-03-26 11:45:00,127.4,153.346,44.333,32.166 -2020-03-26 12:00:00,122.97,148.91299999999998,42.95,32.166 -2020-03-26 12:15:00,124.07,149.278,42.95,32.166 -2020-03-26 12:30:00,129.47,148.55700000000002,42.95,32.166 -2020-03-26 12:45:00,132.19,149.338,42.95,32.166 -2020-03-26 13:00:00,126.6,149.54399999999998,42.489,32.166 -2020-03-26 13:15:00,121.2,147.784,42.489,32.166 -2020-03-26 13:30:00,122.77,145.94799999999998,42.489,32.166 -2020-03-26 13:45:00,122.03,145.113,42.489,32.166 -2020-03-26 14:00:00,118.12,147.65200000000002,43.448,32.166 -2020-03-26 14:15:00,120.48,146.16,43.448,32.166 -2020-03-26 14:30:00,124.87,145.765,43.448,32.166 -2020-03-26 14:45:00,124.56,146.787,43.448,32.166 -2020-03-26 15:00:00,122.41,147.733,45.994,32.166 -2020-03-26 15:15:00,117.85,146.145,45.994,32.166 -2020-03-26 15:30:00,118.99,144.86700000000002,45.994,32.166 -2020-03-26 15:45:00,120.98,143.993,45.994,32.166 -2020-03-26 16:00:00,116.33,148.107,48.167,32.166 -2020-03-26 16:15:00,113.12,150.065,48.167,32.166 -2020-03-26 16:30:00,110.23,150.535,48.167,32.166 -2020-03-26 16:45:00,103.8,149.41299999999998,48.167,32.166 -2020-03-26 17:00:00,103.8,151.651,52.637,32.166 -2020-03-26 17:15:00,109.33,154.106,52.637,32.166 -2020-03-26 17:30:00,115.38,156.77100000000002,52.637,32.166 -2020-03-26 17:45:00,118.72,157.446,52.637,32.166 -2020-03-26 18:00:00,121.33,161.264,55.739,32.166 -2020-03-26 18:15:00,118.59,162.52200000000002,55.739,32.166 -2020-03-26 18:30:00,113.07,160.809,55.739,32.166 -2020-03-26 18:45:00,113.68,164.889,55.739,32.166 -2020-03-26 19:00:00,113.24,162.876,56.36600000000001,32.166 -2020-03-26 19:15:00,115.41,160.976,56.36600000000001,32.166 -2020-03-26 19:30:00,121.08,160.605,56.36600000000001,32.166 -2020-03-26 19:45:00,117.92,159.431,56.36600000000001,32.166 -2020-03-26 20:00:00,115.8,153.36,56.338,32.166 -2020-03-26 20:15:00,112.12,149.299,56.338,32.166 -2020-03-26 20:30:00,109.36,147.637,56.338,32.166 -2020-03-26 20:45:00,108.08,146.005,56.338,32.166 -2020-03-26 21:00:00,100.24,140.836,49.894,32.166 -2020-03-26 21:15:00,98.69,138.41,49.894,32.166 -2020-03-26 21:30:00,92.64,137.374,49.894,32.166 -2020-03-26 21:45:00,91.85,136.857,49.894,32.166 -2020-03-26 22:00:00,85.8,129.87,46.687,32.166 -2020-03-26 22:15:00,85.45,126.67299999999999,46.687,32.166 -2020-03-26 22:30:00,78.83,112.09899999999999,46.687,32.166 -2020-03-26 22:45:00,80.03,104.823,46.687,32.166 -2020-03-26 23:00:00,73.44,97.556,39.211,32.166 -2020-03-26 23:15:00,74.45,96.307,39.211,32.166 -2020-03-26 23:30:00,71.79,96.94200000000001,39.211,32.166 -2020-03-26 23:45:00,69.13,97.62700000000001,39.211,32.166 -2020-03-27 00:00:00,81.29,91.55799999999999,36.616,32.166 -2020-03-27 00:15:00,83.4,91.184,36.616,32.166 -2020-03-27 00:30:00,84.17,89.77600000000001,36.616,32.166 -2020-03-27 00:45:00,83.96,89.18,36.616,32.166 -2020-03-27 01:00:00,77.52,89.76700000000001,33.799,32.166 -2020-03-27 01:15:00,76.7,90.131,33.799,32.166 -2020-03-27 01:30:00,76.43,89.62,33.799,32.166 -2020-03-27 01:45:00,77.15,89.56700000000001,33.799,32.166 -2020-03-27 02:00:00,76.99,91.602,32.968,32.166 -2020-03-27 02:15:00,77.28,91.721,32.968,32.166 -2020-03-27 02:30:00,77.43,93.932,32.968,32.166 -2020-03-27 02:45:00,79.33,94.956,32.968,32.166 -2020-03-27 03:00:00,79.5,97.36200000000001,33.533,32.166 -2020-03-27 03:15:00,77.66,99.35600000000001,33.533,32.166 -2020-03-27 03:30:00,81.27,100.28299999999999,33.533,32.166 -2020-03-27 03:45:00,79.74,101.734,33.533,32.166 -2020-03-27 04:00:00,79.68,114.887,36.102,32.166 -2020-03-27 04:15:00,79.42,126.90899999999999,36.102,32.166 -2020-03-27 04:30:00,77.92,128.674,36.102,32.166 -2020-03-27 04:45:00,80.87,129.208,36.102,32.166 -2020-03-27 05:00:00,85.69,161.907,42.423,32.166 -2020-03-27 05:15:00,87.55,194.855,42.423,32.166 -2020-03-27 05:30:00,89.94,188.229,42.423,32.166 -2020-03-27 05:45:00,94.07,179.046,42.423,32.166 -2020-03-27 06:00:00,101.92,178.21599999999998,55.38,32.166 -2020-03-27 06:15:00,105.46,182.955,55.38,32.166 -2020-03-27 06:30:00,108.23,182.237,55.38,32.166 -2020-03-27 06:45:00,111.45,185.12599999999998,55.38,32.166 -2020-03-27 07:00:00,118.3,187.672,65.929,32.166 -2020-03-27 07:15:00,118.8,190.475,65.929,32.166 -2020-03-27 07:30:00,122.1,188.794,65.929,32.166 -2020-03-27 07:45:00,124.3,185.33900000000003,65.929,32.166 -2020-03-27 08:00:00,127.28,183.12400000000002,57.336999999999996,32.166 -2020-03-27 08:15:00,127.22,180.92,57.336999999999996,32.166 -2020-03-27 08:30:00,127.94,176.578,57.336999999999996,32.166 -2020-03-27 08:45:00,128.1,171.59599999999998,57.336999999999996,32.166 -2020-03-27 09:00:00,127.49,165.024,54.226000000000006,32.166 -2020-03-27 09:15:00,128.67,163.12,54.226000000000006,32.166 -2020-03-27 09:30:00,129.09,162.789,54.226000000000006,32.166 -2020-03-27 09:45:00,128.83,160.995,54.226000000000006,32.166 -2020-03-27 10:00:00,127.42,156.991,51.298,32.166 -2020-03-27 10:15:00,127.88,156.01,51.298,32.166 -2020-03-27 10:30:00,126.44,153.909,51.298,32.166 -2020-03-27 10:45:00,127.39,153.159,51.298,32.166 -2020-03-27 11:00:00,126.52,148.564,50.839,32.166 -2020-03-27 11:15:00,127.61,147.666,50.839,32.166 -2020-03-27 11:30:00,125.8,149.467,50.839,32.166 -2020-03-27 11:45:00,125.03,150.244,50.839,32.166 -2020-03-27 12:00:00,122.0,146.861,47.976000000000006,32.166 -2020-03-27 12:15:00,122.03,145.314,47.976000000000006,32.166 -2020-03-27 12:30:00,121.02,144.716,47.976000000000006,32.166 -2020-03-27 12:45:00,120.19,145.718,47.976000000000006,32.166 -2020-03-27 13:00:00,119.24,146.917,46.299,32.166 -2020-03-27 13:15:00,120.7,145.874,46.299,32.166 -2020-03-27 13:30:00,117.46,144.253,46.299,32.166 -2020-03-27 13:45:00,119.26,143.44799999999998,46.299,32.166 -2020-03-27 14:00:00,117.14,144.891,44.971000000000004,32.166 -2020-03-27 14:15:00,113.78,143.339,44.971000000000004,32.166 -2020-03-27 14:30:00,110.02,143.72899999999998,44.971000000000004,32.166 -2020-03-27 14:45:00,113.2,144.83100000000002,44.971000000000004,32.166 -2020-03-27 15:00:00,114.23,145.365,47.48,32.166 -2020-03-27 15:15:00,112.17,143.32,47.48,32.166 -2020-03-27 15:30:00,106.47,140.567,47.48,32.166 -2020-03-27 15:45:00,107.87,139.994,47.48,32.166 -2020-03-27 16:00:00,110.03,142.95,50.648,32.166 -2020-03-27 16:15:00,110.03,145.256,50.648,32.166 -2020-03-27 16:30:00,112.39,145.755,50.648,32.166 -2020-03-27 16:45:00,112.32,144.27,50.648,32.166 -2020-03-27 17:00:00,115.94,147.203,56.251000000000005,32.166 -2020-03-27 17:15:00,116.05,149.262,56.251000000000005,32.166 -2020-03-27 17:30:00,117.7,151.722,56.251000000000005,32.166 -2020-03-27 17:45:00,118.13,152.159,56.251000000000005,32.166 -2020-03-27 18:00:00,121.7,156.57299999999998,58.982,32.166 -2020-03-27 18:15:00,122.86,157.303,58.982,32.166 -2020-03-27 18:30:00,122.89,155.875,58.982,32.166 -2020-03-27 18:45:00,124.43,160.083,58.982,32.166 -2020-03-27 19:00:00,125.7,159.037,57.293,32.166 -2020-03-27 19:15:00,121.2,158.434,57.293,32.166 -2020-03-27 19:30:00,121.4,157.769,57.293,32.166 -2020-03-27 19:45:00,119.95,155.972,57.293,32.166 -2020-03-27 20:00:00,116.48,149.879,59.433,32.166 -2020-03-27 20:15:00,114.79,146.07,59.433,32.166 -2020-03-27 20:30:00,113.96,144.265,59.433,32.166 -2020-03-27 20:45:00,113.11,142.858,59.433,32.166 -2020-03-27 21:00:00,108.02,138.48,52.153999999999996,32.166 -2020-03-27 21:15:00,105.58,136.90200000000002,52.153999999999996,32.166 -2020-03-27 21:30:00,99.98,135.85399999999998,52.153999999999996,32.166 -2020-03-27 21:45:00,100.41,135.86,52.153999999999996,32.166 -2020-03-27 22:00:00,93.64,129.616,47.125,32.166 -2020-03-27 22:15:00,92.04,126.265,47.125,32.166 -2020-03-27 22:30:00,91.62,118.181,47.125,32.166 -2020-03-27 22:45:00,93.54,114.007,47.125,32.166 -2020-03-27 23:00:00,88.63,106.86399999999999,41.236000000000004,32.166 -2020-03-27 23:15:00,85.58,103.60600000000001,41.236000000000004,32.166 -2020-03-27 23:30:00,83.35,102.531,41.236000000000004,32.166 -2020-03-27 23:45:00,81.51,102.666,41.236000000000004,32.166 -2020-03-28 00:00:00,57.71,89.321,36.484,31.988000000000003 -2020-03-28 00:15:00,54.17,85.42,36.484,31.988000000000003 -2020-03-28 00:30:00,56.34,84.88799999999999,36.484,31.988000000000003 -2020-03-28 00:45:00,54.63,84.62299999999999,36.484,31.988000000000003 -2020-03-28 01:00:00,49.06,85.77,32.391999999999996,31.988000000000003 -2020-03-28 01:15:00,52.94,85.538,32.391999999999996,31.988000000000003 -2020-03-28 01:30:00,49.74,84.344,32.391999999999996,31.988000000000003 -2020-03-28 01:45:00,52.99,84.53,32.391999999999996,31.988000000000003 -2020-03-28 02:00:00,51.54,86.786,30.194000000000003,31.988000000000003 -2020-03-28 02:15:00,52.29,86.351,30.194000000000003,31.988000000000003 -2020-03-28 02:30:00,52.34,87.463,30.194000000000003,31.988000000000003 -2020-03-28 02:45:00,50.12,88.823,30.194000000000003,31.988000000000003 -2020-03-28 03:00:00,50.63,91.3,29.677,31.988000000000003 -2020-03-28 03:15:00,47.78,92.103,29.677,31.988000000000003 -2020-03-28 03:30:00,48.43,91.905,29.677,31.988000000000003 -2020-03-28 03:45:00,51.9,93.94200000000001,29.677,31.988000000000003 -2020-03-28 04:00:00,49.36,103.32,29.616,31.988000000000003 -2020-03-28 04:15:00,52.2,113.07700000000001,29.616,31.988000000000003 -2020-03-28 04:30:00,53.04,112.63799999999999,29.616,31.988000000000003 -2020-03-28 04:45:00,51.37,112.87,29.616,31.988000000000003 -2020-03-28 05:00:00,55.2,130.638,29.625,31.988000000000003 -2020-03-28 05:15:00,53.94,145.415,29.625,31.988000000000003 -2020-03-28 05:30:00,57.62,139.55100000000002,29.625,31.988000000000003 -2020-03-28 05:45:00,59.67,136.032,29.625,31.988000000000003 -2020-03-28 06:00:00,60.42,153.085,30.551,31.988000000000003 -2020-03-28 06:15:00,63.03,172.97099999999998,30.551,31.988000000000003 -2020-03-28 06:30:00,60.51,167.139,30.551,31.988000000000003 -2020-03-28 06:45:00,61.22,162.032,30.551,31.988000000000003 -2020-03-28 07:00:00,66.83,160.97,34.865,31.988000000000003 -2020-03-28 07:15:00,68.4,162.357,34.865,31.988000000000003 -2020-03-28 07:30:00,67.37,163.225,34.865,31.988000000000003 -2020-03-28 07:45:00,72.4,163.143,34.865,31.988000000000003 -2020-03-28 08:00:00,72.28,164.06799999999998,41.456,31.988000000000003 -2020-03-28 08:15:00,77.29,164.692,41.456,31.988000000000003 -2020-03-28 08:30:00,80.4,161.583,41.456,31.988000000000003 -2020-03-28 08:45:00,80.83,159.421,41.456,31.988000000000003 -2020-03-28 09:00:00,86.02,155.105,43.001999999999995,31.988000000000003 -2020-03-28 09:15:00,86.55,153.95,43.001999999999995,31.988000000000003 -2020-03-28 09:30:00,87.98,154.50799999999998,43.001999999999995,31.988000000000003 -2020-03-28 09:45:00,89.07,152.734,43.001999999999995,31.988000000000003 -2020-03-28 10:00:00,88.02,149.053,42.047,31.988000000000003 -2020-03-28 10:15:00,91.44,148.35399999999998,42.047,31.988000000000003 -2020-03-28 10:30:00,89.49,146.31799999999998,42.047,31.988000000000003 -2020-03-28 10:45:00,95.01,146.55200000000002,42.047,31.988000000000003 -2020-03-28 11:00:00,94.88,142.039,39.894,31.988000000000003 -2020-03-28 11:15:00,97.49,140.834,39.894,31.988000000000003 -2020-03-28 11:30:00,97.87,141.798,39.894,31.988000000000003 -2020-03-28 11:45:00,97.43,141.989,39.894,31.988000000000003 -2020-03-28 12:00:00,92.61,137.923,38.122,31.988000000000003 -2020-03-28 12:15:00,94.95,137.143,38.122,31.988000000000003 -2020-03-28 12:30:00,90.34,136.732,38.122,31.988000000000003 -2020-03-28 12:45:00,86.38,137.285,38.122,31.988000000000003 -2020-03-28 13:00:00,82.75,137.92700000000002,34.645,31.988000000000003 -2020-03-28 13:15:00,84.34,135.013,34.645,31.988000000000003 -2020-03-28 13:30:00,84.37,133.059,34.645,31.988000000000003 -2020-03-28 13:45:00,84.04,132.303,34.645,31.988000000000003 -2020-03-28 14:00:00,79.67,134.838,33.739000000000004,31.988000000000003 -2020-03-28 14:15:00,81.26,132.49200000000002,33.739000000000004,31.988000000000003 -2020-03-28 14:30:00,79.9,131.214,33.739000000000004,31.988000000000003 -2020-03-28 14:45:00,79.54,132.628,33.739000000000004,31.988000000000003 -2020-03-28 15:00:00,78.41,133.822,35.908,31.988000000000003 -2020-03-28 15:15:00,80.8,132.606,35.908,31.988000000000003 -2020-03-28 15:30:00,80.27,131.143,35.908,31.988000000000003 -2020-03-28 15:45:00,77.77,130.329,35.908,31.988000000000003 -2020-03-28 16:00:00,80.98,133.009,39.249,31.988000000000003 -2020-03-28 16:15:00,80.76,135.78799999999998,39.249,31.988000000000003 -2020-03-28 16:30:00,81.37,136.305,39.249,31.988000000000003 -2020-03-28 16:45:00,79.09,135.488,39.249,31.988000000000003 -2020-03-28 17:00:00,83.53,137.756,46.045,31.988000000000003 -2020-03-28 17:15:00,82.0,140.559,46.045,31.988000000000003 -2020-03-28 17:30:00,86.27,142.915,46.045,31.988000000000003 -2020-03-28 17:45:00,87.5,143.157,46.045,31.988000000000003 -2020-03-28 18:00:00,89.89,147.595,48.238,31.988000000000003 -2020-03-28 18:15:00,89.13,150.316,48.238,31.988000000000003 -2020-03-28 18:30:00,88.57,150.346,48.238,31.988000000000003 -2020-03-28 18:45:00,85.87,150.866,48.238,31.988000000000003 -2020-03-28 19:00:00,91.17,150.105,46.785,31.988000000000003 -2020-03-28 19:15:00,86.77,148.829,46.785,31.988000000000003 -2020-03-28 19:30:00,89.79,149.017,46.785,31.988000000000003 -2020-03-28 19:45:00,91.35,147.619,46.785,31.988000000000003 -2020-03-28 20:00:00,86.33,143.537,39.830999999999996,31.988000000000003 -2020-03-28 20:15:00,86.08,141.27,39.830999999999996,31.988000000000003 -2020-03-28 20:30:00,84.34,138.96200000000002,39.830999999999996,31.988000000000003 -2020-03-28 20:45:00,82.32,137.77700000000002,39.830999999999996,31.988000000000003 -2020-03-28 21:00:00,78.48,134.78799999999998,34.063,31.988000000000003 -2020-03-28 21:15:00,78.09,133.465,34.063,31.988000000000003 -2020-03-28 21:30:00,73.54,133.45600000000002,34.063,31.988000000000003 -2020-03-28 21:45:00,76.09,133.001,34.063,31.988000000000003 -2020-03-28 22:00:00,73.18,127.82700000000001,34.455999999999996,31.988000000000003 -2020-03-28 22:15:00,71.44,126.572,34.455999999999996,31.988000000000003 -2020-03-28 22:30:00,69.55,123.32799999999999,34.455999999999996,31.988000000000003 -2020-03-28 22:45:00,69.05,120.762,34.455999999999996,31.988000000000003 -2020-03-28 23:00:00,66.49,115.384,27.840999999999998,31.988000000000003 -2020-03-28 23:15:00,65.31,110.95700000000001,27.840999999999998,31.988000000000003 -2020-03-28 23:30:00,60.24,109.191,27.840999999999998,31.988000000000003 -2020-03-28 23:45:00,61.54,107.5,27.840999999999998,31.988000000000003 -2020-03-29 00:00:00,63.42,89.681,20.007,31.988000000000003 -2020-03-29 00:15:00,63.84,85.18,20.007,31.988000000000003 -2020-03-29 00:30:00,63.41,84.29700000000001,20.007,31.988000000000003 -2020-03-29 00:45:00,63.81,84.542,20.007,31.988000000000003 -2020-03-29 01:00:00,61.41,85.62700000000001,17.378,31.988000000000003 -2020-03-29 01:15:00,61.91,86.14200000000001,17.378,31.988000000000003 -2020-03-29 01:30:00,61.2,85.287,17.378,31.988000000000003 -2020-03-29 01:45:00,61.64,85.115,17.378,31.988000000000003 -2020-03-29 03:00:00,60.55,92.348,15.427999999999999,31.988000000000003 -2020-03-29 03:15:00,61.96,92.81200000000001,15.427999999999999,31.988000000000003 -2020-03-29 03:30:00,61.93,93.43799999999999,15.427999999999999,31.988000000000003 -2020-03-29 03:45:00,61.8,95.17,15.427999999999999,31.988000000000003 -2020-03-29 04:00:00,62.52,104.337,16.663,31.988000000000003 -2020-03-29 04:15:00,62.17,113.15899999999999,16.663,31.988000000000003 -2020-03-29 04:30:00,61.27,113.23,16.663,31.988000000000003 -2020-03-29 04:45:00,61.62,113.501,16.663,31.988000000000003 -2020-03-29 05:00:00,59.26,128.622,17.271,31.988000000000003 -2020-03-29 05:15:00,61.48,141.29399999999998,17.271,31.988000000000003 -2020-03-29 05:30:00,61.34,135.15,17.271,31.988000000000003 -2020-03-29 05:45:00,61.02,131.74200000000002,17.271,31.988000000000003 -2020-03-29 06:00:00,61.85,147.918,17.612000000000002,31.988000000000003 -2020-03-29 06:15:00,63.29,166.752,17.612000000000002,31.988000000000003 -2020-03-29 06:30:00,64.04,159.804,17.612000000000002,31.988000000000003 -2020-03-29 06:45:00,65.71,153.536,17.612000000000002,31.988000000000003 -2020-03-29 07:00:00,65.1,154.444,20.88,31.988000000000003 -2020-03-29 07:15:00,67.09,154.61700000000002,20.88,31.988000000000003 -2020-03-29 07:30:00,68.29,154.899,20.88,31.988000000000003 -2020-03-29 07:45:00,66.11,154.187,20.88,31.988000000000003 -2020-03-29 08:00:00,70.13,156.718,25.861,31.988000000000003 -2020-03-29 08:15:00,69.2,157.636,25.861,31.988000000000003 -2020-03-29 08:30:00,68.25,156.053,25.861,31.988000000000003 -2020-03-29 08:45:00,67.57,155.486,25.861,31.988000000000003 -2020-03-29 09:00:00,66.99,150.813,27.921999999999997,31.988000000000003 -2020-03-29 09:15:00,65.9,149.954,27.921999999999997,31.988000000000003 -2020-03-29 09:30:00,65.28,150.535,27.921999999999997,31.988000000000003 -2020-03-29 09:45:00,65.68,148.985,27.921999999999997,31.988000000000003 -2020-03-29 10:00:00,66.04,147.47799999999998,29.048000000000002,31.988000000000003 -2020-03-29 10:15:00,67.95,147.296,29.048000000000002,31.988000000000003 -2020-03-29 10:30:00,68.96,145.834,29.048000000000002,31.988000000000003 -2020-03-29 10:45:00,69.67,144.84799999999998,29.048000000000002,31.988000000000003 -2020-03-29 11:00:00,66.38,140.961,32.02,31.988000000000003 -2020-03-29 11:15:00,66.11,139.761,32.02,31.988000000000003 -2020-03-29 11:30:00,60.65,140.167,32.02,31.988000000000003 -2020-03-29 11:45:00,61.22,140.942,32.02,31.988000000000003 -2020-03-29 12:00:00,57.94,136.707,28.55,31.988000000000003 -2020-03-29 12:15:00,57.98,137.329,28.55,31.988000000000003 -2020-03-29 12:30:00,57.03,135.741,28.55,31.988000000000003 -2020-03-29 12:45:00,58.14,135.325,28.55,31.988000000000003 -2020-03-29 13:00:00,56.4,135.33,25.601999999999997,31.988000000000003 -2020-03-29 13:15:00,53.99,134.73,25.601999999999997,31.988000000000003 -2020-03-29 13:30:00,54.04,132.297,25.601999999999997,31.988000000000003 -2020-03-29 13:45:00,55.14,131.30200000000002,25.601999999999997,31.988000000000003 -2020-03-29 14:00:00,58.23,134.429,23.916999999999998,31.988000000000003 -2020-03-29 14:15:00,57.08,133.165,23.916999999999998,31.988000000000003 -2020-03-29 14:30:00,54.64,132.536,23.916999999999998,31.988000000000003 -2020-03-29 14:45:00,59.63,133.328,23.916999999999998,31.988000000000003 -2020-03-29 15:00:00,60.63,133.313,24.064,31.988000000000003 -2020-03-29 15:15:00,61.57,132.48,24.064,31.988000000000003 -2020-03-29 15:30:00,63.18,131.404,24.064,31.988000000000003 -2020-03-29 15:45:00,66.09,131.221,24.064,31.988000000000003 -2020-03-29 16:00:00,70.77,134.955,28.189,31.988000000000003 -2020-03-29 16:15:00,72.49,137.029,28.189,31.988000000000003 -2020-03-29 16:30:00,74.01,138.034,28.189,31.988000000000003 -2020-03-29 16:45:00,75.04,137.291,28.189,31.988000000000003 -2020-03-29 17:00:00,82.83,139.64700000000002,37.576,31.988000000000003 -2020-03-29 17:15:00,84.34,142.606,37.576,31.988000000000003 -2020-03-29 17:30:00,85.73,145.435,37.576,31.988000000000003 -2020-03-29 17:45:00,88.1,147.631,37.576,31.988000000000003 -2020-03-29 18:00:00,92.01,151.79,42.669,31.988000000000003 -2020-03-29 18:15:00,90.43,155.52200000000002,42.669,31.988000000000003 -2020-03-29 18:30:00,93.35,153.791,42.669,31.988000000000003 -2020-03-29 18:45:00,100.38,155.852,42.669,31.988000000000003 -2020-03-29 19:00:00,104.31,155.41299999999998,43.538999999999994,31.988000000000003 -2020-03-29 19:15:00,103.88,154.311,43.538999999999994,31.988000000000003 -2020-03-29 19:30:00,94.49,154.321,43.538999999999994,31.988000000000003 -2020-03-29 19:45:00,95.0,153.996,43.538999999999994,31.988000000000003 -2020-03-29 20:00:00,89.36,149.881,37.330999999999996,31.988000000000003 -2020-03-29 20:15:00,94.08,148.375,37.330999999999996,31.988000000000003 -2020-03-29 20:30:00,95.79,147.328,37.330999999999996,31.988000000000003 -2020-03-29 20:45:00,98.77,144.69799999999998,37.330999999999996,31.988000000000003 -2020-03-29 21:00:00,92.39,139.58100000000002,33.856,31.988000000000003 -2020-03-29 21:15:00,90.85,137.675,33.856,31.988000000000003 -2020-03-29 21:30:00,88.72,137.703,33.856,31.988000000000003 -2020-03-29 21:45:00,86.64,137.485,33.856,31.988000000000003 -2020-03-29 22:00:00,86.5,131.963,34.711999999999996,31.988000000000003 -2020-03-29 22:15:00,89.79,129.631,34.711999999999996,31.988000000000003 -2020-03-29 22:30:00,88.27,123.661,34.711999999999996,31.988000000000003 -2020-03-29 22:45:00,85.25,120.038,34.711999999999996,31.988000000000003 -2020-03-29 23:00:00,76.97,112.32799999999999,29.698,31.988000000000003 -2020-03-29 23:15:00,78.47,109.78299999999999,29.698,31.988000000000003 -2020-03-29 23:30:00,76.39,108.507,29.698,31.988000000000003 -2020-03-29 23:45:00,76.76,107.598,29.698,31.988000000000003 -2020-03-30 00:00:00,74.91,93.007,29.983,32.166 -2020-03-30 00:15:00,74.62,91.073,29.983,32.166 -2020-03-30 00:30:00,74.93,90.152,29.983,32.166 -2020-03-30 00:45:00,74.48,89.861,29.983,32.166 -2020-03-30 01:00:00,72.07,91.03,29.122,32.166 -2020-03-30 01:15:00,73.81,91.124,29.122,32.166 -2020-03-30 01:30:00,74.52,90.40700000000001,29.122,32.166 -2020-03-30 01:45:00,73.97,90.3,29.122,32.166 -2020-03-30 02:00:00,73.49,92.089,28.676,32.166 -2020-03-30 02:15:00,75.66,91.99600000000001,28.676,32.166 -2020-03-30 02:30:00,77.39,94.175,28.676,32.166 -2020-03-30 02:45:00,76.44,95.27600000000001,28.676,32.166 -2020-03-30 03:00:00,75.13,99.355,26.552,32.166 -2020-03-30 03:15:00,76.32,101.29,26.552,32.166 -2020-03-30 03:30:00,77.54,101.928,26.552,32.166 -2020-03-30 03:45:00,79.94,103.101,26.552,32.166 -2020-03-30 04:00:00,82.14,116.584,27.44,32.166 -2020-03-30 04:15:00,84.58,129.511,27.44,32.166 -2020-03-30 04:30:00,89.34,131.10299999999998,27.44,32.166 -2020-03-30 04:45:00,93.84,131.608,27.44,32.166 -2020-03-30 05:00:00,101.97,160.982,36.825,32.166 -2020-03-30 05:15:00,106.72,192.364,36.825,32.166 -2020-03-30 05:30:00,107.82,185.893,36.825,32.166 -2020-03-30 05:45:00,108.72,177.19099999999997,36.825,32.166 -2020-03-30 06:00:00,116.23,176.537,56.589,32.166 -2020-03-30 06:15:00,116.59,181.03900000000002,56.589,32.166 -2020-03-30 06:30:00,119.93,181.482,56.589,32.166 -2020-03-30 06:45:00,121.38,183.65099999999998,56.589,32.166 -2020-03-30 07:00:00,124.55,186.778,67.49,32.166 -2020-03-30 07:15:00,124.25,188.61,67.49,32.166 -2020-03-30 07:30:00,123.4,187.977,67.49,32.166 -2020-03-30 07:45:00,123.32,185.34099999999998,67.49,32.166 -2020-03-30 08:00:00,122.13,183.62599999999998,60.028,32.166 -2020-03-30 08:15:00,121.81,182.493,60.028,32.166 -2020-03-30 08:30:00,123.07,177.347,60.028,32.166 -2020-03-30 08:45:00,122.24,174.237,60.028,32.166 -2020-03-30 09:00:00,119.81,168.592,55.018,32.166 -2020-03-30 09:15:00,120.83,164.426,55.018,32.166 -2020-03-30 09:30:00,120.02,163.955,55.018,32.166 -2020-03-30 09:45:00,120.56,161.996,55.018,32.166 -2020-03-30 10:00:00,117.61,159.881,51.183,32.166 -2020-03-30 10:15:00,118.77,159.44,51.183,32.166 -2020-03-30 10:30:00,118.87,157.166,51.183,32.166 -2020-03-30 10:45:00,120.03,156.362,51.183,32.166 -2020-03-30 11:00:00,116.72,150.494,50.065,32.166 -2020-03-30 11:15:00,118.45,150.871,50.065,32.166 -2020-03-30 11:30:00,116.15,152.602,50.065,32.166 -2020-03-30 11:45:00,116.99,153.19799999999998,50.065,32.166 -2020-03-30 12:00:00,115.58,150.036,48.141999999999996,32.166 -2020-03-30 12:15:00,118.73,150.696,48.141999999999996,32.166 -2020-03-30 12:30:00,119.08,148.989,48.141999999999996,32.166 -2020-03-30 12:45:00,116.73,149.827,48.141999999999996,32.166 -2020-03-30 13:00:00,116.77,150.662,47.887,32.166 -2020-03-30 13:15:00,116.21,148.665,47.887,32.166 -2020-03-30 13:30:00,116.04,145.832,47.887,32.166 -2020-03-30 13:45:00,119.23,145.102,47.887,32.166 -2020-03-30 14:00:00,119.84,147.571,48.571000000000005,32.166 -2020-03-30 14:15:00,118.19,145.906,48.571000000000005,32.166 -2020-03-30 14:30:00,115.09,144.745,48.571000000000005,32.166 -2020-03-30 14:45:00,114.72,146.056,48.571000000000005,32.166 -2020-03-30 15:00:00,113.97,147.463,49.937,32.166 -2020-03-30 15:15:00,115.44,145.256,49.937,32.166 -2020-03-30 15:30:00,117.05,143.638,49.937,32.166 -2020-03-30 15:45:00,117.4,142.941,49.937,32.166 -2020-03-30 16:00:00,119.4,147.00799999999998,52.963,32.166 -2020-03-30 16:15:00,117.12,148.44799999999998,52.963,32.166 -2020-03-30 16:30:00,120.03,148.47899999999998,52.963,32.166 -2020-03-30 16:45:00,120.1,146.713,52.963,32.166 -2020-03-30 17:00:00,122.72,148.575,61.163999999999994,32.166 -2020-03-30 17:15:00,121.92,150.855,61.163999999999994,32.166 -2020-03-30 17:30:00,123.43,153.141,61.163999999999994,32.166 -2020-03-30 17:45:00,123.83,154.01,61.163999999999994,32.166 -2020-03-30 18:00:00,125.34,158.202,63.788999999999994,32.166 -2020-03-30 18:15:00,122.68,159.688,63.788999999999994,32.166 -2020-03-30 18:30:00,124.68,158.264,63.788999999999994,32.166 -2020-03-30 18:45:00,125.28,161.806,63.788999999999994,32.166 -2020-03-30 19:00:00,122.28,159.947,63.913000000000004,32.166 -2020-03-30 19:15:00,118.3,158.28799999999998,63.913000000000004,32.166 -2020-03-30 19:30:00,114.81,158.621,63.913000000000004,32.166 -2020-03-30 19:45:00,112.8,157.49200000000002,63.913000000000004,32.166 -2020-03-30 20:00:00,106.59,151.07,65.44,32.166 -2020-03-30 20:15:00,106.52,147.994,65.44,32.166 -2020-03-30 20:30:00,106.51,145.638,65.44,32.166 -2020-03-30 20:45:00,105.14,144.384,65.44,32.166 -2020-03-30 21:00:00,100.49,139.514,59.117,32.166 -2020-03-30 21:15:00,99.35,136.80100000000002,59.117,32.166 -2020-03-30 21:30:00,95.33,136.287,59.117,32.166 -2020-03-30 21:45:00,94.73,135.624,59.117,32.166 -2020-03-30 22:00:00,91.38,127.18299999999999,52.301,32.166 -2020-03-30 22:15:00,91.17,124.395,52.301,32.166 -2020-03-30 22:30:00,89.5,109.465,52.301,32.166 -2020-03-30 22:45:00,88.98,101.96,52.301,32.166 -2020-03-30 23:00:00,66.74,94.854,44.373000000000005,32.166 -2020-03-30 23:15:00,64.17,93.986,44.373000000000005,32.166 -2020-03-30 23:30:00,64.34,94.916,44.373000000000005,32.166 -2020-03-30 23:45:00,63.43,95.994,44.373000000000005,32.166 -2020-03-31 00:00:00,62.68,91.345,44.647,32.166 -2020-03-31 00:15:00,60.68,90.807,44.647,32.166 -2020-03-31 00:30:00,59.93,89.366,44.647,32.166 -2020-03-31 00:45:00,61.17,88.571,44.647,32.166 -2020-03-31 01:00:00,59.63,89.383,41.433,32.166 -2020-03-31 01:15:00,60.51,89.15,41.433,32.166 -2020-03-31 01:30:00,59.6,88.506,41.433,32.166 -2020-03-31 01:45:00,65.34,88.48,41.433,32.166 -2020-03-31 02:00:00,67.35,90.09700000000001,39.909,32.166 -2020-03-31 02:15:00,70.17,90.27799999999999,39.909,32.166 -2020-03-31 02:30:00,68.14,91.891,39.909,32.166 -2020-03-31 02:45:00,65.63,93.11,39.909,32.166 -2020-03-31 03:00:00,72.05,96.104,39.14,32.166 -2020-03-31 03:15:00,71.12,97.663,39.14,32.166 -2020-03-31 03:30:00,68.78,98.656,39.14,32.166 -2020-03-31 03:45:00,71.02,99.632,39.14,32.166 -2020-03-31 04:00:00,72.64,112.556,40.015,32.166 -2020-03-31 04:15:00,77.31,125.20100000000001,40.015,32.166 -2020-03-31 04:30:00,78.77,126.529,40.015,32.166 -2020-03-31 04:45:00,82.3,128.109,40.015,32.166 -2020-03-31 05:00:00,94.54,161.782,44.93600000000001,32.166 -2020-03-31 05:15:00,98.42,193.196,44.93600000000001,32.166 -2020-03-31 05:30:00,103.0,185.597,44.93600000000001,32.166 -2020-03-31 05:45:00,102.84,176.67700000000002,44.93600000000001,32.166 -2020-03-31 06:00:00,112.12,175.46099999999998,57.271,32.166 -2020-03-31 06:15:00,114.11,181.27599999999998,57.271,32.166 -2020-03-31 06:30:00,120.03,181.074,57.271,32.166 -2020-03-31 06:45:00,120.49,182.643,57.271,32.166 -2020-03-31 07:00:00,123.04,185.68599999999998,68.352,32.166 -2020-03-31 07:15:00,121.38,187.287,68.352,32.166 -2020-03-31 07:30:00,118.56,186.19099999999997,68.352,32.166 -2020-03-31 07:45:00,119.78,183.394,68.352,32.166 -2020-03-31 08:00:00,118.34,181.725,60.717,32.166 -2020-03-31 08:15:00,116.42,179.653,60.717,32.166 -2020-03-31 08:30:00,119.31,174.368,60.717,32.166 -2020-03-31 08:45:00,115.45,170.799,60.717,32.166 -2020-03-31 09:00:00,110.19,164.595,54.603,32.166 -2020-03-31 09:15:00,107.11,161.68,54.603,32.166 -2020-03-31 09:30:00,108.81,161.972,54.603,32.166 -2020-03-31 09:45:00,111.86,160.24,54.603,32.166 -2020-03-31 10:00:00,107.21,157.274,52.308,32.166 -2020-03-31 10:15:00,106.49,155.975,52.308,32.166 -2020-03-31 10:30:00,102.82,153.881,52.308,32.166 -2020-03-31 10:45:00,103.11,153.60299999999998,52.308,32.166 -2020-03-31 11:00:00,103.17,148.94899999999998,51.838,32.166 -2020-03-31 11:15:00,105.15,149.18200000000002,51.838,32.166 -2020-03-31 11:30:00,107.81,149.653,51.838,32.166 -2020-03-31 11:45:00,107.37,150.726,51.838,32.166 -2020-03-31 12:00:00,103.49,146.401,50.375,32.166 -2020-03-31 12:15:00,105.66,146.811,50.375,32.166 -2020-03-31 12:30:00,104.92,145.886,50.375,32.166 -2020-03-31 12:45:00,104.34,146.658,50.375,32.166 -2020-03-31 13:00:00,95.18,147.097,50.735,32.166 -2020-03-31 13:15:00,98.54,145.253,50.735,32.166 -2020-03-31 13:30:00,96.68,143.38299999999998,50.735,32.166 -2020-03-31 13:45:00,93.25,142.56799999999998,50.735,32.166 -2020-03-31 14:00:00,100.54,145.45600000000002,50.946000000000005,32.166 -2020-03-31 14:15:00,105.54,143.846,50.946000000000005,32.166 -2020-03-31 14:30:00,101.06,143.255,50.946000000000005,32.166 -2020-03-31 14:45:00,100.21,144.303,50.946000000000005,32.166 -2020-03-31 15:00:00,103.43,145.321,53.18,32.166 -2020-03-31 15:15:00,106.7,143.6,53.18,32.166 -2020-03-31 15:30:00,103.14,142.063,53.18,32.166 -2020-03-31 15:45:00,104.16,141.099,53.18,32.166 -2020-03-31 16:00:00,108.84,145.308,54.928999999999995,32.166 -2020-03-31 16:15:00,109.82,147.137,54.928999999999995,32.166 -2020-03-31 16:30:00,114.81,147.60299999999998,54.928999999999995,32.166 -2020-03-31 16:45:00,110.58,146.209,54.928999999999995,32.166 -2020-03-31 17:00:00,116.98,148.611,60.913000000000004,32.166 -2020-03-31 17:15:00,117.4,151.02,60.913000000000004,32.166 -2020-03-31 17:30:00,117.54,153.74,60.913000000000004,32.166 -2020-03-31 17:45:00,112.39,154.411,60.913000000000004,32.166 -2020-03-31 18:00:00,119.14,158.256,62.214,32.166 -2020-03-31 18:15:00,118.51,159.803,62.214,32.166 -2020-03-31 18:30:00,117.89,158.049,62.214,32.166 -2020-03-31 18:45:00,116.94,162.19799999999998,62.214,32.166 -2020-03-31 19:00:00,116.89,160.05200000000002,62.38,32.166 -2020-03-31 19:15:00,117.46,158.23,62.38,32.166 -2020-03-31 19:30:00,116.74,157.986,62.38,32.166 -2020-03-31 19:45:00,114.19,157.02200000000002,62.38,32.166 -2020-03-31 20:00:00,107.41,150.808,65.018,32.166 -2020-03-31 20:15:00,106.35,146.821,65.018,32.166 -2020-03-31 20:30:00,107.79,145.326,65.018,32.166 -2020-03-31 20:45:00,101.84,143.74,65.018,32.166 -2020-03-31 21:00:00,91.56,138.55100000000002,56.416000000000004,32.166 -2020-03-31 21:15:00,99.16,136.15,56.416000000000004,32.166 -2020-03-31 21:30:00,94.28,135.096,56.416000000000004,32.166 -2020-03-31 21:45:00,95.13,134.695,56.416000000000004,32.166 -2020-03-31 22:00:00,83.39,127.675,52.846000000000004,32.166 -2020-03-31 22:15:00,86.65,124.60799999999999,52.846000000000004,32.166 -2020-03-31 22:30:00,87.84,109.78200000000001,52.846000000000004,32.166 -2020-03-31 22:45:00,84.34,102.49700000000001,52.846000000000004,32.166 -2020-03-31 23:00:00,75.86,95.185,44.435,32.166 -2020-03-31 23:15:00,71.86,94.06,44.435,32.166 -2020-03-31 23:30:00,74.23,94.70100000000001,44.435,32.166 -2020-03-31 23:45:00,72.45,95.499,44.435,32.166 -2020-04-01 00:00:00,69.69,79.456,39.061,30.736 -2020-04-01 00:15:00,75.94,79.935,39.061,30.736 -2020-04-01 00:30:00,78.31,78.125,39.061,30.736 -2020-04-01 00:45:00,79.1,76.366,39.061,30.736 -2020-04-01 01:00:00,70.26,77.627,35.795,30.736 -2020-04-01 01:15:00,71.06,76.926,35.795,30.736 -2020-04-01 01:30:00,68.96,75.87100000000001,35.795,30.736 -2020-04-01 01:45:00,67.64,75.392,35.795,30.736 -2020-04-01 02:00:00,67.11,77.184,33.316,30.736 -2020-04-01 02:15:00,71.63,76.73899999999999,33.316,30.736 -2020-04-01 02:30:00,68.87,79.04899999999999,33.316,30.736 -2020-04-01 02:45:00,69.72,79.654,33.316,30.736 -2020-04-01 03:00:00,70.41,83.104,32.803000000000004,30.736 -2020-04-01 03:15:00,70.75,85.094,32.803000000000004,30.736 -2020-04-01 03:30:00,72.02,84.946,32.803000000000004,30.736 -2020-04-01 03:45:00,74.47,85.49700000000001,32.803000000000004,30.736 -2020-04-01 04:00:00,78.97,98.23200000000001,34.235,30.736 -2020-04-01 04:15:00,81.76,111.244,34.235,30.736 -2020-04-01 04:30:00,84.98,111.566,34.235,30.736 -2020-04-01 04:45:00,89.2,113.62700000000001,34.235,30.736 -2020-04-01 05:00:00,96.35,149.659,38.65,30.736 -2020-04-01 05:15:00,99.7,183.483,38.65,30.736 -2020-04-01 05:30:00,102.53,173.278,38.65,30.736 -2020-04-01 05:45:00,104.9,162.628,38.65,30.736 -2020-04-01 06:00:00,111.67,163.61700000000002,54.951,30.736 -2020-04-01 06:15:00,109.26,169.40099999999998,54.951,30.736 -2020-04-01 06:30:00,111.94,167.69799999999998,54.951,30.736 -2020-04-01 06:45:00,115.16,168.525,54.951,30.736 -2020-04-01 07:00:00,117.0,171.296,67.328,30.736 -2020-04-01 07:15:00,115.4,172.46400000000003,67.328,30.736 -2020-04-01 07:30:00,113.66,171.299,67.328,30.736 -2020-04-01 07:45:00,114.86,168.247,67.328,30.736 -2020-04-01 08:00:00,114.85,167.385,60.23,30.736 -2020-04-01 08:15:00,112.81,165.22400000000002,60.23,30.736 -2020-04-01 08:30:00,115.93,160.542,60.23,30.736 -2020-04-01 08:45:00,116.76,157.317,60.23,30.736 -2020-04-01 09:00:00,116.8,151.364,56.845,30.736 -2020-04-01 09:15:00,118.8,148.974,56.845,30.736 -2020-04-01 09:30:00,116.67,150.576,56.845,30.736 -2020-04-01 09:45:00,112.16,149.53,56.845,30.736 -2020-04-01 10:00:00,107.33,145.305,53.832,30.736 -2020-04-01 10:15:00,106.59,144.53,53.832,30.736 -2020-04-01 10:30:00,110.06,142.187,53.832,30.736 -2020-04-01 10:45:00,112.49,141.908,53.832,30.736 -2020-04-01 11:00:00,111.37,136.292,53.225,30.736 -2020-04-01 11:15:00,112.05,136.708,53.225,30.736 -2020-04-01 11:30:00,108.51,137.773,53.225,30.736 -2020-04-01 11:45:00,105.82,139.032,53.225,30.736 -2020-04-01 12:00:00,99.92,134.388,50.676,30.736 -2020-04-01 12:15:00,105.63,134.911,50.676,30.736 -2020-04-01 12:30:00,106.93,134.461,50.676,30.736 -2020-04-01 12:45:00,103.6,135.236,50.676,30.736 -2020-04-01 13:00:00,103.07,135.707,50.646,30.736 -2020-04-01 13:15:00,102.9,134.39600000000002,50.646,30.736 -2020-04-01 13:30:00,102.03,132.364,50.646,30.736 -2020-04-01 13:45:00,103.27,131.08700000000002,50.646,30.736 -2020-04-01 14:00:00,105.31,132.503,50.786,30.736 -2020-04-01 14:15:00,102.66,131.495,50.786,30.736 -2020-04-01 14:30:00,103.31,131.606,50.786,30.736 -2020-04-01 14:45:00,96.63,132.4,50.786,30.736 -2020-04-01 15:00:00,93.87,132.792,51.535,30.736 -2020-04-01 15:15:00,93.7,131.388,51.535,30.736 -2020-04-01 15:30:00,99.59,130.69299999999998,51.535,30.736 -2020-04-01 15:45:00,101.6,130.118,51.535,30.736 -2020-04-01 16:00:00,98.99,131.167,53.157,30.736 -2020-04-01 16:15:00,101.34,132.373,53.157,30.736 -2020-04-01 16:30:00,101.19,132.377,53.157,30.736 -2020-04-01 16:45:00,103.38,131.024,53.157,30.736 -2020-04-01 17:00:00,106.1,130.7,57.793,30.736 -2020-04-01 17:15:00,106.86,133.321,57.793,30.736 -2020-04-01 17:30:00,107.36,135.382,57.793,30.736 -2020-04-01 17:45:00,109.06,136.537,57.793,30.736 -2020-04-01 18:00:00,110.87,139.363,59.872,30.736 -2020-04-01 18:15:00,109.26,140.57,59.872,30.736 -2020-04-01 18:30:00,110.29,139.069,59.872,30.736 -2020-04-01 18:45:00,111.93,144.532,59.872,30.736 -2020-04-01 19:00:00,113.65,143.02200000000002,60.17100000000001,30.736 -2020-04-01 19:15:00,109.64,141.8,60.17100000000001,30.736 -2020-04-01 19:30:00,110.52,141.476,60.17100000000001,30.736 -2020-04-01 19:45:00,105.99,141.282,60.17100000000001,30.736 -2020-04-01 20:00:00,102.05,136.425,65.015,30.736 -2020-04-01 20:15:00,107.29,133.08100000000002,65.015,30.736 -2020-04-01 20:30:00,106.45,132.442,65.015,30.736 -2020-04-01 20:45:00,102.76,131.428,65.015,30.736 -2020-04-01 21:00:00,92.13,125.118,57.805,30.736 -2020-04-01 21:15:00,93.99,123.633,57.805,30.736 -2020-04-01 21:30:00,88.08,123.904,57.805,30.736 -2020-04-01 21:45:00,86.39,122.70100000000001,57.805,30.736 -2020-04-01 22:00:00,81.08,116.471,52.115,30.736 -2020-04-01 22:15:00,86.83,113.48200000000001,52.115,30.736 -2020-04-01 22:30:00,86.9,100.95,52.115,30.736 -2020-04-01 22:45:00,86.76,93.775,52.115,30.736 -2020-04-01 23:00:00,76.32,84.93299999999999,42.871,30.736 -2020-04-01 23:15:00,73.01,84.014,42.871,30.736 -2020-04-01 23:30:00,73.23,83.148,42.871,30.736 -2020-04-01 23:45:00,75.85,83.984,42.871,30.736 -2020-04-02 00:00:00,71.82,79.054,39.203,30.736 -2020-04-02 00:15:00,78.53,79.546,39.203,30.736 -2020-04-02 00:30:00,80.03,77.726,39.203,30.736 -2020-04-02 00:45:00,79.26,75.968,39.203,30.736 -2020-04-02 01:00:00,71.4,77.212,37.118,30.736 -2020-04-02 01:15:00,75.84,76.488,37.118,30.736 -2020-04-02 01:30:00,76.57,75.41199999999999,37.118,30.736 -2020-04-02 01:45:00,78.33,74.938,37.118,30.736 -2020-04-02 02:00:00,73.97,76.719,35.647,30.736 -2020-04-02 02:15:00,75.9,76.257,35.647,30.736 -2020-04-02 02:30:00,78.09,78.58800000000001,35.647,30.736 -2020-04-02 02:45:00,78.5,79.19800000000001,35.647,30.736 -2020-04-02 03:00:00,76.84,82.665,34.585,30.736 -2020-04-02 03:15:00,79.44,84.63,34.585,30.736 -2020-04-02 03:30:00,81.12,84.477,34.585,30.736 -2020-04-02 03:45:00,81.18,85.04700000000001,34.585,30.736 -2020-04-02 04:00:00,79.89,97.76100000000001,36.184,30.736 -2020-04-02 04:15:00,81.67,110.743,36.184,30.736 -2020-04-02 04:30:00,83.77,111.07,36.184,30.736 -2020-04-02 04:45:00,87.27,113.12,36.184,30.736 -2020-04-02 05:00:00,96.26,149.07299999999998,41.019,30.736 -2020-04-02 05:15:00,98.79,182.831,41.019,30.736 -2020-04-02 05:30:00,101.22,172.627,41.019,30.736 -2020-04-02 05:45:00,102.52,162.015,41.019,30.736 -2020-04-02 06:00:00,111.5,163.025,53.963,30.736 -2020-04-02 06:15:00,109.85,168.795,53.963,30.736 -2020-04-02 06:30:00,113.43,167.063,53.963,30.736 -2020-04-02 06:45:00,114.03,167.875,53.963,30.736 -2020-04-02 07:00:00,119.23,170.65400000000002,66.512,30.736 -2020-04-02 07:15:00,120.65,171.798,66.512,30.736 -2020-04-02 07:30:00,116.77,170.592,66.512,30.736 -2020-04-02 07:45:00,112.67,167.525,66.512,30.736 -2020-04-02 08:00:00,115.24,166.643,58.86,30.736 -2020-04-02 08:15:00,117.46,164.503,58.86,30.736 -2020-04-02 08:30:00,115.91,159.782,58.86,30.736 -2020-04-02 08:45:00,115.21,156.585,58.86,30.736 -2020-04-02 09:00:00,115.87,150.637,52.156000000000006,30.736 -2020-04-02 09:15:00,114.66,148.252,52.156000000000006,30.736 -2020-04-02 09:30:00,119.53,149.874,52.156000000000006,30.736 -2020-04-02 09:45:00,123.85,148.858,52.156000000000006,30.736 -2020-04-02 10:00:00,118.77,144.639,49.034,30.736 -2020-04-02 10:15:00,126.97,143.914,49.034,30.736 -2020-04-02 10:30:00,125.46,141.596,49.034,30.736 -2020-04-02 10:45:00,123.88,141.338,49.034,30.736 -2020-04-02 11:00:00,122.49,135.713,46.53,30.736 -2020-04-02 11:15:00,116.42,136.153,46.53,30.736 -2020-04-02 11:30:00,110.72,137.222,46.53,30.736 -2020-04-02 11:45:00,120.11,138.501,46.53,30.736 -2020-04-02 12:00:00,118.87,133.88299999999998,43.318000000000005,30.736 -2020-04-02 12:15:00,120.48,134.416,43.318000000000005,30.736 -2020-04-02 12:30:00,117.06,133.922,43.318000000000005,30.736 -2020-04-02 12:45:00,111.35,134.69899999999998,43.318000000000005,30.736 -2020-04-02 13:00:00,111.13,135.213,41.608000000000004,30.736 -2020-04-02 13:15:00,108.37,133.891,41.608000000000004,30.736 -2020-04-02 13:30:00,110.04,131.856,41.608000000000004,30.736 -2020-04-02 13:45:00,110.04,130.58100000000002,41.608000000000004,30.736 -2020-04-02 14:00:00,105.79,132.067,41.786,30.736 -2020-04-02 14:15:00,97.68,131.036,41.786,30.736 -2020-04-02 14:30:00,108.28,131.105,41.786,30.736 -2020-04-02 14:45:00,118.85,131.90200000000002,41.786,30.736 -2020-04-02 15:00:00,124.11,132.321,44.181999999999995,30.736 -2020-04-02 15:15:00,123.02,130.893,44.181999999999995,30.736 -2020-04-02 15:30:00,118.52,130.14700000000002,44.181999999999995,30.736 -2020-04-02 15:45:00,111.31,129.553,44.181999999999995,30.736 -2020-04-02 16:00:00,113.66,130.637,45.956,30.736 -2020-04-02 16:15:00,119.09,131.816,45.956,30.736 -2020-04-02 16:30:00,118.53,131.822,45.956,30.736 -2020-04-02 16:45:00,121.92,130.407,45.956,30.736 -2020-04-02 17:00:00,124.5,130.131,50.702,30.736 -2020-04-02 17:15:00,120.31,132.734,50.702,30.736 -2020-04-02 17:30:00,120.7,134.798,50.702,30.736 -2020-04-02 17:45:00,121.11,135.939,50.702,30.736 -2020-04-02 18:00:00,123.23,138.77700000000002,53.595,30.736 -2020-04-02 18:15:00,120.26,140.024,53.595,30.736 -2020-04-02 18:30:00,115.9,138.511,53.595,30.736 -2020-04-02 18:45:00,114.56,143.986,53.595,30.736 -2020-04-02 19:00:00,121.11,142.455,54.207,30.736 -2020-04-02 19:15:00,119.24,141.243,54.207,30.736 -2020-04-02 19:30:00,117.39,140.938,54.207,30.736 -2020-04-02 19:45:00,107.85,140.776,54.207,30.736 -2020-04-02 20:00:00,108.15,135.892,56.948,30.736 -2020-04-02 20:15:00,111.42,132.55700000000002,56.948,30.736 -2020-04-02 20:30:00,106.44,131.955,56.948,30.736 -2020-04-02 20:45:00,101.23,130.964,56.948,30.736 -2020-04-02 21:00:00,100.35,124.652,52.157,30.736 -2020-04-02 21:15:00,101.03,123.176,52.157,30.736 -2020-04-02 21:30:00,107.53,123.44,52.157,30.736 -2020-04-02 21:45:00,103.46,122.266,52.157,30.736 -2020-04-02 22:00:00,96.94,116.041,47.483000000000004,30.736 -2020-04-02 22:15:00,99.13,113.08,47.483000000000004,30.736 -2020-04-02 22:30:00,96.8,100.515,47.483000000000004,30.736 -2020-04-02 22:45:00,92.58,93.336,47.483000000000004,30.736 -2020-04-02 23:00:00,83.94,84.47,41.978,30.736 -2020-04-02 23:15:00,83.81,83.583,41.978,30.736 -2020-04-02 23:30:00,88.75,82.71600000000001,41.978,30.736 -2020-04-02 23:45:00,87.76,83.568,41.978,30.736 -2020-04-03 00:00:00,76.87,76.975,39.301,30.736 -2020-04-03 00:15:00,73.29,77.727,39.301,30.736 -2020-04-03 00:30:00,70.69,75.961,39.301,30.736 -2020-04-03 00:45:00,72.11,74.514,39.301,30.736 -2020-04-03 01:00:00,75.99,75.328,37.976,30.736 -2020-04-03 01:15:00,77.44,74.82,37.976,30.736 -2020-04-03 01:30:00,73.59,73.983,37.976,30.736 -2020-04-03 01:45:00,79.01,73.439,37.976,30.736 -2020-04-03 02:00:00,73.89,75.78399999999999,37.041,30.736 -2020-04-03 02:15:00,73.49,75.205,37.041,30.736 -2020-04-03 02:30:00,79.15,78.352,37.041,30.736 -2020-04-03 02:45:00,75.85,78.607,37.041,30.736 -2020-04-03 03:00:00,74.11,81.914,37.575,30.736 -2020-04-03 03:15:00,79.18,83.74799999999999,37.575,30.736 -2020-04-03 03:30:00,76.15,83.445,37.575,30.736 -2020-04-03 03:45:00,85.07,84.735,37.575,30.736 -2020-04-03 04:00:00,91.35,97.646,39.058,30.736 -2020-04-03 04:15:00,92.78,109.551,39.058,30.736 -2020-04-03 04:30:00,92.59,110.568,39.058,30.736 -2020-04-03 04:45:00,93.75,111.53399999999999,39.058,30.736 -2020-04-03 05:00:00,102.17,146.35299999999998,43.256,30.736 -2020-04-03 05:15:00,102.67,181.516,43.256,30.736 -2020-04-03 05:30:00,104.0,172.08599999999998,43.256,30.736 -2020-04-03 05:45:00,106.32,161.215,43.256,30.736 -2020-04-03 06:00:00,114.66,162.671,56.093999999999994,30.736 -2020-04-03 06:15:00,113.85,167.59900000000002,56.093999999999994,30.736 -2020-04-03 06:30:00,117.45,165.268,56.093999999999994,30.736 -2020-04-03 06:45:00,116.33,167.011,56.093999999999994,30.736 -2020-04-03 07:00:00,117.74,169.637,66.92699999999999,30.736 -2020-04-03 07:15:00,116.74,171.90900000000002,66.92699999999999,30.736 -2020-04-03 07:30:00,116.53,169.487,66.92699999999999,30.736 -2020-04-03 07:45:00,115.69,165.718,66.92699999999999,30.736 -2020-04-03 08:00:00,114.23,164.50900000000001,60.332,30.736 -2020-04-03 08:15:00,113.28,162.47799999999998,60.332,30.736 -2020-04-03 08:30:00,113.5,158.321,60.332,30.736 -2020-04-03 08:45:00,111.4,154.023,60.332,30.736 -2020-04-03 09:00:00,109.66,147.19899999999998,56.085,30.736 -2020-04-03 09:15:00,109.0,146.171,56.085,30.736 -2020-04-03 09:30:00,107.02,147.174,56.085,30.736 -2020-04-03 09:45:00,107.16,146.306,56.085,30.736 -2020-04-03 10:00:00,105.85,141.186,52.91,30.736 -2020-04-03 10:15:00,107.46,140.898,52.91,30.736 -2020-04-03 10:30:00,105.47,138.81799999999998,52.91,30.736 -2020-04-03 10:45:00,107.79,138.22299999999998,52.91,30.736 -2020-04-03 11:00:00,102.99,132.667,52.278999999999996,30.736 -2020-04-03 11:15:00,101.82,131.98,52.278999999999996,30.736 -2020-04-03 11:30:00,101.04,134.144,52.278999999999996,30.736 -2020-04-03 11:45:00,100.88,135.03,52.278999999999996,30.736 -2020-04-03 12:00:00,96.79,131.445,49.023999999999994,30.736 -2020-04-03 12:15:00,100.16,130.16,49.023999999999994,30.736 -2020-04-03 12:30:00,94.47,129.792,49.023999999999994,30.736 -2020-04-03 12:45:00,100.82,130.594,49.023999999999994,30.736 -2020-04-03 13:00:00,98.58,132.121,46.82,30.736 -2020-04-03 13:15:00,96.92,131.47299999999998,46.82,30.736 -2020-04-03 13:30:00,95.45,129.8,46.82,30.736 -2020-04-03 13:45:00,97.9,128.616,46.82,30.736 -2020-04-03 14:00:00,100.06,128.971,45.756,30.736 -2020-04-03 14:15:00,94.08,127.977,45.756,30.736 -2020-04-03 14:30:00,89.77,129.05200000000002,45.756,30.736 -2020-04-03 14:45:00,89.15,129.773,45.756,30.736 -2020-04-03 15:00:00,99.64,129.828,47.56,30.736 -2020-04-03 15:15:00,97.56,127.941,47.56,30.736 -2020-04-03 15:30:00,100.39,125.76799999999999,47.56,30.736 -2020-04-03 15:45:00,96.0,125.601,47.56,30.736 -2020-04-03 16:00:00,102.98,125.535,49.581,30.736 -2020-04-03 16:15:00,107.36,127.12799999999999,49.581,30.736 -2020-04-03 16:30:00,107.41,127.117,49.581,30.736 -2020-04-03 16:45:00,105.69,125.18799999999999,49.581,30.736 -2020-04-03 17:00:00,111.53,125.978,53.918,30.736 -2020-04-03 17:15:00,114.78,128.192,53.918,30.736 -2020-04-03 17:30:00,110.72,130.119,53.918,30.736 -2020-04-03 17:45:00,114.51,130.999,53.918,30.736 -2020-04-03 18:00:00,121.12,134.387,54.266000000000005,30.736 -2020-04-03 18:15:00,115.78,134.931,54.266000000000005,30.736 -2020-04-03 18:30:00,115.51,133.621,54.266000000000005,30.736 -2020-04-03 18:45:00,117.31,139.316,54.266000000000005,30.736 -2020-04-03 19:00:00,117.9,138.845,54.092,30.736 -2020-04-03 19:15:00,113.83,138.86700000000002,54.092,30.736 -2020-04-03 19:30:00,113.99,138.334,54.092,30.736 -2020-04-03 19:45:00,112.93,137.374,54.092,30.736 -2020-04-03 20:00:00,109.53,132.425,59.038999999999994,30.736 -2020-04-03 20:15:00,98.4,129.518,59.038999999999994,30.736 -2020-04-03 20:30:00,94.66,128.66899999999998,59.038999999999994,30.736 -2020-04-03 20:45:00,99.46,127.654,59.038999999999994,30.736 -2020-04-03 21:00:00,89.12,122.366,53.346000000000004,30.736 -2020-04-03 21:15:00,94.16,122.066,53.346000000000004,30.736 -2020-04-03 21:30:00,91.73,122.272,53.346000000000004,30.736 -2020-04-03 21:45:00,92.13,121.59700000000001,53.346000000000004,30.736 -2020-04-03 22:00:00,84.0,115.95,47.938,30.736 -2020-04-03 22:15:00,85.47,112.79700000000001,47.938,30.736 -2020-04-03 22:30:00,85.97,106.915,47.938,30.736 -2020-04-03 22:45:00,86.43,102.51899999999999,47.938,30.736 -2020-04-03 23:00:00,79.39,94.219,40.266,30.736 -2020-04-03 23:15:00,73.78,91.25299999999999,40.266,30.736 -2020-04-03 23:30:00,75.79,88.464,40.266,30.736 -2020-04-03 23:45:00,76.89,88.825,40.266,30.736 -2020-04-04 00:00:00,74.66,75.469,39.184,30.618000000000002 -2020-04-04 00:15:00,73.9,73.183,39.184,30.618000000000002 -2020-04-04 00:30:00,65.93,71.98100000000001,39.184,30.618000000000002 -2020-04-04 00:45:00,68.89,70.57600000000001,39.184,30.618000000000002 -2020-04-04 01:00:00,71.51,71.94,34.692,30.618000000000002 -2020-04-04 01:15:00,76.86,71.125,34.692,30.618000000000002 -2020-04-04 01:30:00,71.53,69.49,34.692,30.618000000000002 -2020-04-04 01:45:00,68.36,69.517,34.692,30.618000000000002 -2020-04-04 02:00:00,66.58,71.745,32.919000000000004,30.618000000000002 -2020-04-04 02:15:00,62.82,70.472,32.919000000000004,30.618000000000002 -2020-04-04 02:30:00,63.39,72.505,32.919000000000004,30.618000000000002 -2020-04-04 02:45:00,64.96,73.27199999999999,32.919000000000004,30.618000000000002 -2020-04-04 03:00:00,65.71,76.22399999999999,32.024,30.618000000000002 -2020-04-04 03:15:00,63.91,76.88600000000001,32.024,30.618000000000002 -2020-04-04 03:30:00,63.74,75.78699999999999,32.024,30.618000000000002 -2020-04-04 03:45:00,64.35,78.009,32.024,30.618000000000002 -2020-04-04 04:00:00,65.42,87.32600000000001,31.958000000000002,30.618000000000002 -2020-04-04 04:15:00,68.5,97.13600000000001,31.958000000000002,30.618000000000002 -2020-04-04 04:30:00,69.35,95.876,31.958000000000002,30.618000000000002 -2020-04-04 04:45:00,69.44,96.686,31.958000000000002,30.618000000000002 -2020-04-04 05:00:00,72.92,117.115,32.75,30.618000000000002 -2020-04-04 05:15:00,68.32,134.414,32.75,30.618000000000002 -2020-04-04 05:30:00,70.28,126.084,32.75,30.618000000000002 -2020-04-04 05:45:00,69.35,121.125,32.75,30.618000000000002 -2020-04-04 06:00:00,75.32,140.066,34.461999999999996,30.618000000000002 -2020-04-04 06:15:00,76.84,159.474,34.461999999999996,30.618000000000002 -2020-04-04 06:30:00,77.92,152.194,34.461999999999996,30.618000000000002 -2020-04-04 06:45:00,79.19,146.56799999999998,34.461999999999996,30.618000000000002 -2020-04-04 07:00:00,82.21,145.606,37.736,30.618000000000002 -2020-04-04 07:15:00,81.98,146.351,37.736,30.618000000000002 -2020-04-04 07:30:00,81.71,146.433,37.736,30.618000000000002 -2020-04-04 07:45:00,82.74,145.686,37.736,30.618000000000002 -2020-04-04 08:00:00,81.59,147.055,42.34,30.618000000000002 -2020-04-04 08:15:00,81.68,147.328,42.34,30.618000000000002 -2020-04-04 08:30:00,81.19,144.188,42.34,30.618000000000002 -2020-04-04 08:45:00,81.27,142.493,42.34,30.618000000000002 -2020-04-04 09:00:00,78.35,138.297,43.571999999999996,30.618000000000002 -2020-04-04 09:15:00,76.95,138.02700000000002,43.571999999999996,30.618000000000002 -2020-04-04 09:30:00,77.5,139.909,43.571999999999996,30.618000000000002 -2020-04-04 09:45:00,76.01,138.953,43.571999999999996,30.618000000000002 -2020-04-04 10:00:00,74.59,134.138,40.514,30.618000000000002 -2020-04-04 10:15:00,75.0,134.186,40.514,30.618000000000002 -2020-04-04 10:30:00,75.58,132.084,40.514,30.618000000000002 -2020-04-04 10:45:00,74.33,132.252,40.514,30.618000000000002 -2020-04-04 11:00:00,72.45,126.696,36.388000000000005,30.618000000000002 -2020-04-04 11:15:00,71.39,125.946,36.388000000000005,30.618000000000002 -2020-04-04 11:30:00,68.52,127.45,36.388000000000005,30.618000000000002 -2020-04-04 11:45:00,68.28,127.98700000000001,36.388000000000005,30.618000000000002 -2020-04-04 12:00:00,64.47,123.79799999999999,35.217,30.618000000000002 -2020-04-04 12:15:00,63.5,123.37200000000001,35.217,30.618000000000002 -2020-04-04 12:30:00,60.6,123.133,35.217,30.618000000000002 -2020-04-04 12:45:00,61.67,123.70200000000001,35.217,30.618000000000002 -2020-04-04 13:00:00,59.52,124.51700000000001,32.001999999999995,30.618000000000002 -2020-04-04 13:15:00,58.68,122.109,32.001999999999995,30.618000000000002 -2020-04-04 13:30:00,59.1,120.18700000000001,32.001999999999995,30.618000000000002 -2020-04-04 13:45:00,60.24,118.756,32.001999999999995,30.618000000000002 -2020-04-04 14:00:00,58.61,120.09200000000001,31.304000000000002,30.618000000000002 -2020-04-04 14:15:00,58.6,118.131,31.304000000000002,30.618000000000002 -2020-04-04 14:30:00,59.62,117.635,31.304000000000002,30.618000000000002 -2020-04-04 14:45:00,60.05,118.73200000000001,31.304000000000002,30.618000000000002 -2020-04-04 15:00:00,61.39,119.492,34.731,30.618000000000002 -2020-04-04 15:15:00,61.93,118.48299999999999,34.731,30.618000000000002 -2020-04-04 15:30:00,62.76,117.48299999999999,34.731,30.618000000000002 -2020-04-04 15:45:00,65.09,116.90299999999999,34.731,30.618000000000002 -2020-04-04 16:00:00,68.45,117.296,38.769,30.618000000000002 -2020-04-04 16:15:00,69.5,119.079,38.769,30.618000000000002 -2020-04-04 16:30:00,75.03,119.14,38.769,30.618000000000002 -2020-04-04 16:45:00,75.19,117.76299999999999,38.769,30.618000000000002 -2020-04-04 17:00:00,79.59,117.855,44.928000000000004,30.618000000000002 -2020-04-04 17:15:00,80.67,120.13799999999999,44.928000000000004,30.618000000000002 -2020-04-04 17:30:00,82.2,121.93700000000001,44.928000000000004,30.618000000000002 -2020-04-04 17:45:00,83.92,122.765,44.928000000000004,30.618000000000002 -2020-04-04 18:00:00,85.73,126.61399999999999,47.786,30.618000000000002 -2020-04-04 18:15:00,83.59,129.254,47.786,30.618000000000002 -2020-04-04 18:30:00,83.84,129.518,47.786,30.618000000000002 -2020-04-04 18:45:00,85.08,131.211,47.786,30.618000000000002 -2020-04-04 19:00:00,87.47,130.589,47.463,30.618000000000002 -2020-04-04 19:15:00,90.07,129.77700000000002,47.463,30.618000000000002 -2020-04-04 19:30:00,88.24,130.158,47.463,30.618000000000002 -2020-04-04 19:45:00,84.51,130.01,47.463,30.618000000000002 -2020-04-04 20:00:00,81.76,126.954,43.735,30.618000000000002 -2020-04-04 20:15:00,80.12,125.14200000000001,43.735,30.618000000000002 -2020-04-04 20:30:00,76.99,123.64399999999999,43.735,30.618000000000002 -2020-04-04 20:45:00,76.56,123.34,43.735,30.618000000000002 -2020-04-04 21:00:00,70.25,118.79799999999999,40.346,30.618000000000002 -2020-04-04 21:15:00,69.31,118.615,40.346,30.618000000000002 -2020-04-04 21:30:00,66.75,119.72200000000001,40.346,30.618000000000002 -2020-04-04 21:45:00,66.4,118.525,40.346,30.618000000000002 -2020-04-04 22:00:00,62.9,113.759,39.323,30.618000000000002 -2020-04-04 22:15:00,62.01,112.402,39.323,30.618000000000002 -2020-04-04 22:30:00,59.88,110.391,39.323,30.618000000000002 -2020-04-04 22:45:00,58.88,107.417,39.323,30.618000000000002 -2020-04-04 23:00:00,54.7,100.368,33.716,30.618000000000002 -2020-04-04 23:15:00,54.75,96.571,33.716,30.618000000000002 -2020-04-04 23:30:00,52.99,93.90100000000001,33.716,30.618000000000002 -2020-04-04 23:45:00,53.7,92.859,33.716,30.618000000000002 -2020-04-05 00:00:00,49.93,76.092,28.703000000000003,30.618000000000002 -2020-04-05 00:15:00,50.91,72.97800000000001,28.703000000000003,30.618000000000002 -2020-04-05 00:30:00,49.61,71.447,28.703000000000003,30.618000000000002 -2020-04-05 00:45:00,49.78,70.42,28.703000000000003,30.618000000000002 -2020-04-05 01:00:00,48.79,71.824,26.171,30.618000000000002 -2020-04-05 01:15:00,49.86,71.562,26.171,30.618000000000002 -2020-04-05 01:30:00,49.35,70.15,26.171,30.618000000000002 -2020-04-05 01:45:00,49.74,69.775,26.171,30.618000000000002 -2020-04-05 02:00:00,49.25,71.568,25.326999999999998,30.618000000000002 -2020-04-05 02:15:00,49.88,70.153,25.326999999999998,30.618000000000002 -2020-04-05 02:30:00,49.37,72.839,25.326999999999998,30.618000000000002 -2020-04-05 02:45:00,49.41,73.73100000000001,25.326999999999998,30.618000000000002 -2020-04-05 03:00:00,48.8,77.26,24.311999999999998,30.618000000000002 -2020-04-05 03:15:00,49.81,77.726,24.311999999999998,30.618000000000002 -2020-04-05 03:30:00,50.35,77.057,24.311999999999998,30.618000000000002 -2020-04-05 03:45:00,51.13,78.79899999999999,24.311999999999998,30.618000000000002 -2020-04-05 04:00:00,51.43,87.915,25.33,30.618000000000002 -2020-04-05 04:15:00,52.55,96.831,25.33,30.618000000000002 -2020-04-05 04:30:00,54.44,96.38600000000001,25.33,30.618000000000002 -2020-04-05 04:45:00,58.04,97.081,25.33,30.618000000000002 -2020-04-05 05:00:00,57.28,115.42299999999999,25.309,30.618000000000002 -2020-04-05 05:15:00,58.24,130.764,25.309,30.618000000000002 -2020-04-05 05:30:00,55.96,122.105,25.309,30.618000000000002 -2020-04-05 05:45:00,57.4,117.171,25.309,30.618000000000002 -2020-04-05 06:00:00,58.75,134.681,25.945999999999998,30.618000000000002 -2020-04-05 06:15:00,58.9,153.49,25.945999999999998,30.618000000000002 -2020-04-05 06:30:00,59.93,145.111,25.945999999999998,30.618000000000002 -2020-04-05 06:45:00,63.35,138.262,25.945999999999998,30.618000000000002 -2020-04-05 07:00:00,64.84,138.942,27.87,30.618000000000002 -2020-04-05 07:15:00,67.39,138.22799999999998,27.87,30.618000000000002 -2020-04-05 07:30:00,67.1,138.191,27.87,30.618000000000002 -2020-04-05 07:45:00,64.74,136.928,27.87,30.618000000000002 -2020-04-05 08:00:00,65.25,139.78799999999998,32.114000000000004,30.618000000000002 -2020-04-05 08:15:00,65.1,140.642,32.114000000000004,30.618000000000002 -2020-04-05 08:30:00,64.21,139.002,32.114000000000004,30.618000000000002 -2020-04-05 08:45:00,63.33,138.611,32.114000000000004,30.618000000000002 -2020-04-05 09:00:00,64.31,134.07,34.222,30.618000000000002 -2020-04-05 09:15:00,61.83,133.924,34.222,30.618000000000002 -2020-04-05 09:30:00,60.69,135.941,34.222,30.618000000000002 -2020-04-05 09:45:00,60.76,135.444,34.222,30.618000000000002 -2020-04-05 10:00:00,61.17,132.583,34.544000000000004,30.618000000000002 -2020-04-05 10:15:00,63.73,133.114,34.544000000000004,30.618000000000002 -2020-04-05 10:30:00,64.51,131.56,34.544000000000004,30.618000000000002 -2020-04-05 10:45:00,63.78,130.931,34.544000000000004,30.618000000000002 -2020-04-05 11:00:00,60.97,125.81200000000001,36.368,30.618000000000002 -2020-04-05 11:15:00,58.9,124.97,36.368,30.618000000000002 -2020-04-05 11:30:00,56.67,126.111,36.368,30.618000000000002 -2020-04-05 11:45:00,56.1,127.214,36.368,30.618000000000002 -2020-04-05 12:00:00,52.5,123.1,32.433,30.618000000000002 -2020-04-05 12:15:00,54.28,123.76299999999999,32.433,30.618000000000002 -2020-04-05 12:30:00,51.11,122.53399999999999,32.433,30.618000000000002 -2020-04-05 12:45:00,51.46,122.119,32.433,30.618000000000002 -2020-04-05 13:00:00,50.91,122.304,28.971999999999998,30.618000000000002 -2020-04-05 13:15:00,50.33,121.807,28.971999999999998,30.618000000000002 -2020-04-05 13:30:00,49.91,119.228,28.971999999999998,30.618000000000002 -2020-04-05 13:45:00,50.1,117.82600000000001,28.971999999999998,30.618000000000002 -2020-04-05 14:00:00,50.18,119.947,25.531999999999996,30.618000000000002 -2020-04-05 14:15:00,50.22,119.027,25.531999999999996,30.618000000000002 -2020-04-05 14:30:00,50.84,118.82700000000001,25.531999999999996,30.618000000000002 -2020-04-05 14:45:00,51.63,119.15,25.531999999999996,30.618000000000002 -2020-04-05 15:00:00,52.57,118.89200000000001,25.766,30.618000000000002 -2020-04-05 15:15:00,53.17,118.04299999999999,25.766,30.618000000000002 -2020-04-05 15:30:00,54.25,117.337,25.766,30.618000000000002 -2020-04-05 15:45:00,57.67,117.369,25.766,30.618000000000002 -2020-04-05 16:00:00,62.85,118.23899999999999,29.232,30.618000000000002 -2020-04-05 16:15:00,63.15,119.47,29.232,30.618000000000002 -2020-04-05 16:30:00,67.45,120.189,29.232,30.618000000000002 -2020-04-05 16:45:00,71.68,118.87299999999999,29.232,30.618000000000002 -2020-04-05 17:00:00,75.8,119.089,37.431,30.618000000000002 -2020-04-05 17:15:00,78.13,121.87700000000001,37.431,30.618000000000002 -2020-04-05 17:30:00,80.23,124.274,37.431,30.618000000000002 -2020-04-05 17:45:00,80.84,126.838,37.431,30.618000000000002 -2020-04-05 18:00:00,83.42,130.58700000000002,41.251999999999995,30.618000000000002 -2020-04-05 18:15:00,82.93,133.947,41.251999999999995,30.618000000000002 -2020-04-05 18:30:00,85.4,132.678,41.251999999999995,30.618000000000002 -2020-04-05 18:45:00,86.36,135.68,41.251999999999995,30.618000000000002 -2020-04-05 19:00:00,89.34,135.89700000000002,41.784,30.618000000000002 -2020-04-05 19:15:00,96.35,134.91899999999998,41.784,30.618000000000002 -2020-04-05 19:30:00,97.94,135.082,41.784,30.618000000000002 -2020-04-05 19:45:00,90.74,135.688,41.784,30.618000000000002 -2020-04-05 20:00:00,83.74,132.64,40.804,30.618000000000002 -2020-04-05 20:15:00,84.54,131.41299999999998,40.804,30.618000000000002 -2020-04-05 20:30:00,88.61,131.183,40.804,30.618000000000002 -2020-04-05 20:45:00,93.47,129.24200000000002,40.804,30.618000000000002 -2020-04-05 21:00:00,90.23,122.90100000000001,38.379,30.618000000000002 -2020-04-05 21:15:00,85.48,122.161,38.379,30.618000000000002 -2020-04-05 21:30:00,81.64,123.109,38.379,30.618000000000002 -2020-04-05 21:45:00,78.03,122.22,38.379,30.618000000000002 -2020-04-05 22:00:00,72.89,117.758,37.87,30.618000000000002 -2020-04-05 22:15:00,76.43,115.053,37.87,30.618000000000002 -2020-04-05 22:30:00,78.18,110.678,37.87,30.618000000000002 -2020-04-05 22:45:00,78.53,106.478,37.87,30.618000000000002 -2020-04-05 23:00:00,72.6,97.42299999999999,33.332,30.618000000000002 -2020-04-05 23:15:00,69.86,95.53299999999999,33.332,30.618000000000002 -2020-04-05 23:30:00,67.11,93.111,33.332,30.618000000000002 -2020-04-05 23:45:00,71.83,92.75,33.332,30.618000000000002 -2020-04-06 00:00:00,72.06,79.122,34.698,30.736 -2020-04-06 00:15:00,73.34,78.301,34.698,30.736 -2020-04-06 00:30:00,69.38,76.625,34.698,30.736 -2020-04-06 00:45:00,66.41,75.048,34.698,30.736 -2020-04-06 01:00:00,65.32,76.639,32.889,30.736 -2020-04-06 01:15:00,71.76,76.038,32.889,30.736 -2020-04-06 01:30:00,72.41,74.84100000000001,32.889,30.736 -2020-04-06 01:45:00,73.18,74.486,32.889,30.736 -2020-04-06 02:00:00,68.6,76.492,32.06,30.736 -2020-04-06 02:15:00,70.89,75.381,32.06,30.736 -2020-04-06 02:30:00,74.64,78.384,32.06,30.736 -2020-04-06 02:45:00,75.36,78.82,32.06,30.736 -2020-04-06 03:00:00,73.31,83.4,30.515,30.736 -2020-04-06 03:15:00,75.92,85.24799999999999,30.515,30.736 -2020-04-06 03:30:00,80.31,84.79899999999999,30.515,30.736 -2020-04-06 03:45:00,82.56,85.961,30.515,30.736 -2020-04-06 04:00:00,85.18,99.426,31.436,30.736 -2020-04-06 04:15:00,84.27,112.485,31.436,30.736 -2020-04-06 04:30:00,87.01,113.088,31.436,30.736 -2020-04-06 04:45:00,88.33,114.081,31.436,30.736 -2020-04-06 05:00:00,97.35,145.80200000000002,38.997,30.736 -2020-04-06 05:15:00,102.88,179.06,38.997,30.736 -2020-04-06 05:30:00,104.6,169.676,38.997,30.736 -2020-04-06 05:45:00,103.3,159.631,38.997,30.736 -2020-04-06 06:00:00,111.48,160.849,54.97,30.736 -2020-04-06 06:15:00,112.43,165.453,54.97,30.736 -2020-04-06 06:30:00,112.7,164.183,54.97,30.736 -2020-04-06 06:45:00,114.94,165.671,54.97,30.736 -2020-04-06 07:00:00,116.47,168.513,66.032,30.736 -2020-04-06 07:15:00,114.52,169.805,66.032,30.736 -2020-04-06 07:30:00,112.82,168.767,66.032,30.736 -2020-04-06 07:45:00,111.27,166.037,66.032,30.736 -2020-04-06 08:00:00,109.22,165.03599999999997,59.941,30.736 -2020-04-06 08:15:00,108.67,163.886,59.941,30.736 -2020-04-06 08:30:00,107.94,159.04,59.941,30.736 -2020-04-06 08:45:00,106.68,156.61,59.941,30.736 -2020-04-06 09:00:00,105.31,151.07299999999998,54.016000000000005,30.736 -2020-04-06 09:15:00,105.6,147.696,54.016000000000005,30.736 -2020-04-06 09:30:00,104.71,148.596,54.016000000000005,30.736 -2020-04-06 09:45:00,103.31,147.168,54.016000000000005,30.736 -2020-04-06 10:00:00,101.9,144.064,50.63,30.736 -2020-04-06 10:15:00,104.54,144.34799999999998,50.63,30.736 -2020-04-06 10:30:00,103.43,141.99200000000002,50.63,30.736 -2020-04-06 10:45:00,105.54,141.142,50.63,30.736 -2020-04-06 11:00:00,100.25,134.44,49.951,30.736 -2020-04-06 11:15:00,103.21,135.025,49.951,30.736 -2020-04-06 11:30:00,98.48,137.47,49.951,30.736 -2020-04-06 11:45:00,100.55,138.54,49.951,30.736 -2020-04-06 12:00:00,103.01,135.093,46.913000000000004,30.736 -2020-04-06 12:15:00,108.32,135.816,46.913000000000004,30.736 -2020-04-06 12:30:00,107.08,134.238,46.913000000000004,30.736 -2020-04-06 12:45:00,101.4,134.923,46.913000000000004,30.736 -2020-04-06 13:00:00,102.39,136.09,47.093999999999994,30.736 -2020-04-06 13:15:00,101.73,134.179,47.093999999999994,30.736 -2020-04-06 13:30:00,102.31,131.29,47.093999999999994,30.736 -2020-04-06 13:45:00,109.82,130.314,47.093999999999994,30.736 -2020-04-06 14:00:00,111.7,131.673,46.678000000000004,30.736 -2020-04-06 14:15:00,107.6,130.526,46.678000000000004,30.736 -2020-04-06 14:30:00,101.94,129.817,46.678000000000004,30.736 -2020-04-06 14:45:00,103.94,131.03799999999998,46.678000000000004,30.736 -2020-04-06 15:00:00,103.09,131.95600000000002,47.715,30.736 -2020-04-06 15:15:00,104.75,129.77100000000002,47.715,30.736 -2020-04-06 15:30:00,107.02,128.752,47.715,30.736 -2020-04-06 15:45:00,106.7,128.225,47.715,30.736 -2020-04-06 16:00:00,111.87,129.474,49.81100000000001,30.736 -2020-04-06 16:15:00,109.46,130.173,49.81100000000001,30.736 -2020-04-06 16:30:00,112.43,129.903,49.81100000000001,30.736 -2020-04-06 16:45:00,110.12,127.697,49.81100000000001,30.736 -2020-04-06 17:00:00,116.7,127.12899999999999,55.591,30.736 -2020-04-06 17:15:00,115.27,129.43200000000002,55.591,30.736 -2020-04-06 17:30:00,116.49,131.267,55.591,30.736 -2020-04-06 17:45:00,114.97,132.599,55.591,30.736 -2020-04-06 18:00:00,118.43,136.04,56.523,30.736 -2020-04-06 18:15:00,116.2,137.036,56.523,30.736 -2020-04-06 18:30:00,115.3,135.815,56.523,30.736 -2020-04-06 18:45:00,112.64,140.894,56.523,30.736 -2020-04-06 19:00:00,111.96,139.827,56.044,30.736 -2020-04-06 19:15:00,117.29,138.756,56.044,30.736 -2020-04-06 19:30:00,115.88,139.08,56.044,30.736 -2020-04-06 19:45:00,110.57,138.85299999999998,56.044,30.736 -2020-04-06 20:00:00,102.39,133.55,61.715,30.736 -2020-04-06 20:15:00,99.43,131.42600000000002,61.715,30.736 -2020-04-06 20:30:00,100.45,130.275,61.715,30.736 -2020-04-06 20:45:00,98.33,129.54,61.715,30.736 -2020-04-06 21:00:00,101.37,123.23200000000001,56.24,30.736 -2020-04-06 21:15:00,100.59,121.96799999999999,56.24,30.736 -2020-04-06 21:30:00,94.39,122.585,56.24,30.736 -2020-04-06 21:45:00,88.76,121.28200000000001,56.24,30.736 -2020-04-06 22:00:00,84.27,113.863,50.437,30.736 -2020-04-06 22:15:00,87.71,111.35700000000001,50.437,30.736 -2020-04-06 22:30:00,87.15,98.445,50.437,30.736 -2020-04-06 22:45:00,85.4,91.066,50.437,30.736 -2020-04-06 23:00:00,73.0,82.488,42.756,30.736 -2020-04-06 23:15:00,73.24,81.514,42.756,30.736 -2020-04-06 23:30:00,69.53,80.881,42.756,30.736 -2020-04-06 23:45:00,72.17,81.986,42.756,30.736 -2020-04-07 00:00:00,69.6,77.01899999999999,39.857,30.736 -2020-04-07 00:15:00,70.65,77.579,39.857,30.736 -2020-04-07 00:30:00,69.86,75.704,39.857,30.736 -2020-04-07 00:45:00,70.53,73.96600000000001,39.857,30.736 -2020-04-07 01:00:00,69.2,75.119,37.233000000000004,30.736 -2020-04-07 01:15:00,73.23,74.282,37.233000000000004,30.736 -2020-04-07 01:30:00,72.34,73.097,37.233000000000004,30.736 -2020-04-07 01:45:00,73.13,72.648,37.233000000000004,30.736 -2020-04-07 02:00:00,72.15,74.37,35.856,30.736 -2020-04-07 02:15:00,72.53,73.82600000000001,35.856,30.736 -2020-04-07 02:30:00,68.0,76.265,35.856,30.736 -2020-04-07 02:45:00,74.06,76.898,35.856,30.736 -2020-04-07 03:00:00,72.61,80.45100000000001,34.766999999999996,30.736 -2020-04-07 03:15:00,72.94,82.287,34.766999999999996,30.736 -2020-04-07 03:30:00,74.44,82.10799999999999,34.766999999999996,30.736 -2020-04-07 03:45:00,76.22,82.777,34.766999999999996,30.736 -2020-04-07 04:00:00,80.49,95.38600000000001,35.468,30.736 -2020-04-07 04:15:00,82.48,108.21,35.468,30.736 -2020-04-07 04:30:00,85.73,108.561,35.468,30.736 -2020-04-07 04:45:00,90.58,110.554,35.468,30.736 -2020-04-07 05:00:00,100.18,146.112,40.399,30.736 -2020-04-07 05:15:00,102.56,179.535,40.399,30.736 -2020-04-07 05:30:00,104.29,169.34400000000002,40.399,30.736 -2020-04-07 05:45:00,106.96,158.921,40.399,30.736 -2020-04-07 06:00:00,110.4,160.033,54.105,30.736 -2020-04-07 06:15:00,110.63,165.732,54.105,30.736 -2020-04-07 06:30:00,112.93,163.855,54.105,30.736 -2020-04-07 06:45:00,116.12,164.59900000000002,54.105,30.736 -2020-04-07 07:00:00,116.23,167.41099999999997,63.083,30.736 -2020-04-07 07:15:00,115.06,168.44,63.083,30.736 -2020-04-07 07:30:00,115.2,167.024,63.083,30.736 -2020-04-07 07:45:00,113.5,163.887,63.083,30.736 -2020-04-07 08:00:00,110.51,162.91,57.254,30.736 -2020-04-07 08:15:00,109.08,160.877,57.254,30.736 -2020-04-07 08:30:00,109.23,155.965,57.254,30.736 -2020-04-07 08:45:00,109.56,152.909,57.254,30.736 -2020-04-07 09:00:00,107.69,146.993,51.395,30.736 -2020-04-07 09:15:00,108.09,144.628,51.395,30.736 -2020-04-07 09:30:00,104.87,146.349,51.395,30.736 -2020-04-07 09:45:00,106.35,145.477,51.395,30.736 -2020-04-07 10:00:00,103.01,141.299,48.201,30.736 -2020-04-07 10:15:00,102.97,140.826,48.201,30.736 -2020-04-07 10:30:00,100.59,138.631,48.201,30.736 -2020-04-07 10:45:00,102.15,138.48,48.201,30.736 -2020-04-07 11:00:00,99.8,132.81,46.133,30.736 -2020-04-07 11:15:00,99.29,133.372,46.133,30.736 -2020-04-07 11:30:00,97.42,134.46,46.133,30.736 -2020-04-07 11:45:00,99.27,135.839,46.133,30.736 -2020-04-07 12:00:00,102.69,131.346,44.243,30.736 -2020-04-07 12:15:00,103.58,131.928,44.243,30.736 -2020-04-07 12:30:00,101.04,131.215,44.243,30.736 -2020-04-07 12:45:00,96.93,131.998,44.243,30.736 -2020-04-07 13:00:00,95.94,132.735,45.042,30.736 -2020-04-07 13:15:00,97.03,131.35399999999998,45.042,30.736 -2020-04-07 13:30:00,100.96,129.305,45.042,30.736 -2020-04-07 13:45:00,103.24,128.042,45.042,30.736 -2020-04-07 14:00:00,101.28,129.875,44.062,30.736 -2020-04-07 14:15:00,102.93,128.732,44.062,30.736 -2020-04-07 14:30:00,106.26,128.583,44.062,30.736 -2020-04-07 14:45:00,104.54,129.403,44.062,30.736 -2020-04-07 15:00:00,100.32,129.96200000000002,46.461999999999996,30.736 -2020-04-07 15:15:00,100.71,128.40200000000002,46.461999999999996,30.736 -2020-04-07 15:30:00,104.76,127.404,46.461999999999996,30.736 -2020-04-07 15:45:00,106.51,126.71600000000001,46.461999999999996,30.736 -2020-04-07 16:00:00,105.14,127.979,48.802,30.736 -2020-04-07 16:15:00,108.47,129.028,48.802,30.736 -2020-04-07 16:30:00,110.54,129.042,48.802,30.736 -2020-04-07 16:45:00,111.8,127.31,48.802,30.736 -2020-04-07 17:00:00,111.67,127.27799999999999,55.672,30.736 -2020-04-07 17:15:00,118.31,129.785,55.672,30.736 -2020-04-07 17:30:00,117.23,131.864,55.672,30.736 -2020-04-07 17:45:00,116.51,132.929,55.672,30.736 -2020-04-07 18:00:00,113.28,135.825,57.006,30.736 -2020-04-07 18:15:00,116.51,137.267,57.006,30.736 -2020-04-07 18:30:00,115.8,135.695,57.006,30.736 -2020-04-07 18:45:00,116.19,141.224,57.006,30.736 -2020-04-07 19:00:00,111.29,139.591,57.148,30.736 -2020-04-07 19:15:00,116.99,138.437,57.148,30.736 -2020-04-07 19:30:00,116.1,138.225,57.148,30.736 -2020-04-07 19:45:00,113.98,138.225,57.148,30.736 -2020-04-07 20:00:00,104.78,133.197,61.895,30.736 -2020-04-07 20:15:00,109.17,129.917,61.895,30.736 -2020-04-07 20:30:00,105.18,129.49200000000002,61.895,30.736 -2020-04-07 20:45:00,104.44,128.619,61.895,30.736 -2020-04-07 21:00:00,94.22,122.29899999999999,54.78,30.736 -2020-04-07 21:15:00,98.11,120.87200000000001,54.78,30.736 -2020-04-07 21:30:00,95.36,121.095,54.78,30.736 -2020-04-07 21:45:00,95.09,120.073,54.78,30.736 -2020-04-07 22:00:00,82.41,113.87100000000001,50.76,30.736 -2020-04-07 22:15:00,81.69,111.046,50.76,30.736 -2020-04-07 22:30:00,86.54,98.31200000000001,50.76,30.736 -2020-04-07 22:45:00,86.26,91.10600000000001,50.76,30.736 -2020-04-07 23:00:00,79.29,82.12700000000001,44.162,30.736 -2020-04-07 23:15:00,77.16,81.404,44.162,30.736 -2020-04-07 23:30:00,74.84,80.531,44.162,30.736 -2020-04-07 23:45:00,80.07,81.457,44.162,30.736 -2020-04-08 00:00:00,77.66,76.607,39.061,30.736 -2020-04-08 00:15:00,75.81,77.182,39.061,30.736 -2020-04-08 00:30:00,72.88,75.296,39.061,30.736 -2020-04-08 00:45:00,69.77,73.563,39.061,30.736 -2020-04-08 01:00:00,73.18,74.697,35.795,30.736 -2020-04-08 01:15:00,76.79,73.837,35.795,30.736 -2020-04-08 01:30:00,75.5,72.631,35.795,30.736 -2020-04-08 01:45:00,70.46,72.187,35.795,30.736 -2020-04-08 02:00:00,71.43,73.89699999999999,33.316,30.736 -2020-04-08 02:15:00,70.31,73.337,33.316,30.736 -2020-04-08 02:30:00,70.6,75.797,33.316,30.736 -2020-04-08 02:45:00,78.12,76.435,33.316,30.736 -2020-04-08 03:00:00,79.43,80.005,32.803000000000004,30.736 -2020-04-08 03:15:00,79.12,81.814,32.803000000000004,30.736 -2020-04-08 03:30:00,75.54,81.631,32.803000000000004,30.736 -2020-04-08 03:45:00,77.28,82.321,32.803000000000004,30.736 -2020-04-08 04:00:00,80.55,94.90899999999999,34.235,30.736 -2020-04-08 04:15:00,81.61,107.698,34.235,30.736 -2020-04-08 04:30:00,85.38,108.055,34.235,30.736 -2020-04-08 04:45:00,90.05,110.037,34.235,30.736 -2020-04-08 05:00:00,97.97,145.516,38.65,30.736 -2020-04-08 05:15:00,99.52,178.87,38.65,30.736 -2020-04-08 05:30:00,102.97,168.683,38.65,30.736 -2020-04-08 05:45:00,107.04,158.297,38.65,30.736 -2020-04-08 06:00:00,111.56,159.431,54.951,30.736 -2020-04-08 06:15:00,113.89,165.113,54.951,30.736 -2020-04-08 06:30:00,116.9,163.208,54.951,30.736 -2020-04-08 06:45:00,118.83,163.938,54.951,30.736 -2020-04-08 07:00:00,121.7,166.757,67.328,30.736 -2020-04-08 07:15:00,121.52,167.764,67.328,30.736 -2020-04-08 07:30:00,120.53,166.30700000000002,67.328,30.736 -2020-04-08 07:45:00,119.16,163.157,67.328,30.736 -2020-04-08 08:00:00,115.39,162.161,60.23,30.736 -2020-04-08 08:15:00,117.51,160.15,60.23,30.736 -2020-04-08 08:30:00,118.44,155.19899999999998,60.23,30.736 -2020-04-08 08:45:00,119.61,152.173,60.23,30.736 -2020-04-08 09:00:00,120.78,146.264,56.845,30.736 -2020-04-08 09:15:00,122.55,143.903,56.845,30.736 -2020-04-08 09:30:00,122.95,145.643,56.845,30.736 -2020-04-08 09:45:00,122.4,144.80100000000002,56.845,30.736 -2020-04-08 10:00:00,122.17,140.63,53.832,30.736 -2020-04-08 10:15:00,121.08,140.207,53.832,30.736 -2020-04-08 10:30:00,124.05,138.03799999999998,53.832,30.736 -2020-04-08 10:45:00,121.53,137.908,53.832,30.736 -2020-04-08 11:00:00,113.64,132.23,53.225,30.736 -2020-04-08 11:15:00,107.84,132.815,53.225,30.736 -2020-04-08 11:30:00,106.89,133.906,53.225,30.736 -2020-04-08 11:45:00,109.26,135.306,53.225,30.736 -2020-04-08 12:00:00,115.23,130.839,50.676,30.736 -2020-04-08 12:15:00,114.32,131.429,50.676,30.736 -2020-04-08 12:30:00,105.49,130.672,50.676,30.736 -2020-04-08 12:45:00,103.19,131.458,50.676,30.736 -2020-04-08 13:00:00,101.47,132.237,50.646,30.736 -2020-04-08 13:15:00,107.86,130.846,50.646,30.736 -2020-04-08 13:30:00,106.14,128.796,50.646,30.736 -2020-04-08 13:45:00,114.59,127.53399999999999,50.646,30.736 -2020-04-08 14:00:00,122.83,129.437,50.786,30.736 -2020-04-08 14:15:00,123.75,128.27200000000002,50.786,30.736 -2020-04-08 14:30:00,119.06,128.078,50.786,30.736 -2020-04-08 14:45:00,114.36,128.901,50.786,30.736 -2020-04-08 15:00:00,108.95,129.488,51.535,30.736 -2020-04-08 15:15:00,110.42,127.902,51.535,30.736 -2020-04-08 15:30:00,107.51,126.854,51.535,30.736 -2020-04-08 15:45:00,110.73,126.148,51.535,30.736 -2020-04-08 16:00:00,116.16,127.445,53.157,30.736 -2020-04-08 16:15:00,113.87,128.469,53.157,30.736 -2020-04-08 16:30:00,113.4,128.485,53.157,30.736 -2020-04-08 16:45:00,118.97,126.69,53.157,30.736 -2020-04-08 17:00:00,120.55,126.706,57.793,30.736 -2020-04-08 17:15:00,115.72,129.194,57.793,30.736 -2020-04-08 17:30:00,117.9,131.276,57.793,30.736 -2020-04-08 17:45:00,119.34,132.32399999999998,57.793,30.736 -2020-04-08 18:00:00,118.83,135.232,59.872,30.736 -2020-04-08 18:15:00,114.99,136.711,59.872,30.736 -2020-04-08 18:30:00,117.03,135.128,59.872,30.736 -2020-04-08 18:45:00,117.74,140.667,59.872,30.736 -2020-04-08 19:00:00,117.3,139.016,60.17100000000001,30.736 -2020-04-08 19:15:00,117.96,137.872,60.17100000000001,30.736 -2020-04-08 19:30:00,117.33,137.67700000000002,60.17100000000001,30.736 -2020-04-08 19:45:00,114.3,137.71,60.17100000000001,30.736 -2020-04-08 20:00:00,102.99,132.656,65.015,30.736 -2020-04-08 20:15:00,103.02,129.386,65.015,30.736 -2020-04-08 20:30:00,107.09,128.996,65.015,30.736 -2020-04-08 20:45:00,106.0,128.14600000000002,65.015,30.736 -2020-04-08 21:00:00,97.85,121.82600000000001,57.805,30.736 -2020-04-08 21:15:00,90.97,120.40899999999999,57.805,30.736 -2020-04-08 21:30:00,89.79,120.62299999999999,57.805,30.736 -2020-04-08 21:45:00,86.79,119.63,57.805,30.736 -2020-04-08 22:00:00,81.91,113.434,52.115,30.736 -2020-04-08 22:15:00,83.05,110.63600000000001,52.115,30.736 -2020-04-08 22:30:00,86.93,97.867,52.115,30.736 -2020-04-08 22:45:00,87.46,90.656,52.115,30.736 -2020-04-08 23:00:00,83.24,81.655,42.871,30.736 -2020-04-08 23:15:00,77.77,80.96300000000001,42.871,30.736 -2020-04-08 23:30:00,78.21,80.09,42.871,30.736 -2020-04-08 23:45:00,81.71,81.031,42.871,30.736 -2020-04-09 00:00:00,75.02,76.195,39.203,30.736 -2020-04-09 00:15:00,76.58,76.783,39.203,30.736 -2020-04-09 00:30:00,70.26,74.887,39.203,30.736 -2020-04-09 00:45:00,71.93,73.15899999999999,39.203,30.736 -2020-04-09 01:00:00,73.86,74.275,37.118,30.736 -2020-04-09 01:15:00,78.67,73.393,37.118,30.736 -2020-04-09 01:30:00,78.31,72.165,37.118,30.736 -2020-04-09 01:45:00,73.67,71.726,37.118,30.736 -2020-04-09 02:00:00,71.77,73.42399999999999,35.647,30.736 -2020-04-09 02:15:00,74.57,72.848,35.647,30.736 -2020-04-09 02:30:00,78.65,75.328,35.647,30.736 -2020-04-09 02:45:00,79.41,75.971,35.647,30.736 -2020-04-09 03:00:00,73.94,79.559,34.585,30.736 -2020-04-09 03:15:00,75.69,81.342,34.585,30.736 -2020-04-09 03:30:00,76.28,81.153,34.585,30.736 -2020-04-09 03:45:00,77.96,81.862,34.585,30.736 -2020-04-09 04:00:00,84.48,94.429,36.184,30.736 -2020-04-09 04:15:00,90.28,107.18700000000001,36.184,30.736 -2020-04-09 04:30:00,90.56,107.54799999999999,36.184,30.736 -2020-04-09 04:45:00,91.01,109.51899999999999,36.184,30.736 -2020-04-09 05:00:00,97.64,144.918,41.019,30.736 -2020-04-09 05:15:00,101.38,178.203,41.019,30.736 -2020-04-09 05:30:00,106.13,168.021,41.019,30.736 -2020-04-09 05:45:00,108.39,157.672,41.019,30.736 -2020-04-09 06:00:00,111.79,158.826,53.963,30.736 -2020-04-09 06:15:00,114.39,164.495,53.963,30.736 -2020-04-09 06:30:00,116.98,162.559,53.963,30.736 -2020-04-09 06:45:00,121.2,163.276,53.963,30.736 -2020-04-09 07:00:00,121.1,166.09900000000002,66.512,30.736 -2020-04-09 07:15:00,120.6,167.085,66.512,30.736 -2020-04-09 07:30:00,119.91,165.58700000000002,66.512,30.736 -2020-04-09 07:45:00,118.39,162.42600000000002,66.512,30.736 -2020-04-09 08:00:00,117.51,161.411,58.86,30.736 -2020-04-09 08:15:00,117.76,159.423,58.86,30.736 -2020-04-09 08:30:00,121.48,154.435,58.86,30.736 -2020-04-09 08:45:00,121.83,151.436,58.86,30.736 -2020-04-09 09:00:00,120.69,145.533,52.156000000000006,30.736 -2020-04-09 09:15:00,115.59,143.17700000000002,52.156000000000006,30.736 -2020-04-09 09:30:00,110.08,144.937,52.156000000000006,30.736 -2020-04-09 09:45:00,113.84,144.123,52.156000000000006,30.736 -2020-04-09 10:00:00,105.34,139.96200000000002,49.034,30.736 -2020-04-09 10:15:00,105.65,139.588,49.034,30.736 -2020-04-09 10:30:00,104.77,137.445,49.034,30.736 -2020-04-09 10:45:00,105.43,137.336,49.034,30.736 -2020-04-09 11:00:00,100.95,131.65,46.53,30.736 -2020-04-09 11:15:00,102.14,132.259,46.53,30.736 -2020-04-09 11:30:00,101.09,133.35299999999998,46.53,30.736 -2020-04-09 11:45:00,101.76,134.774,46.53,30.736 -2020-04-09 12:00:00,100.18,130.33100000000002,43.318000000000005,30.736 -2020-04-09 12:15:00,105.05,130.931,43.318000000000005,30.736 -2020-04-09 12:30:00,101.1,130.13,43.318000000000005,30.736 -2020-04-09 12:45:00,98.59,130.916,43.318000000000005,30.736 -2020-04-09 13:00:00,98.99,131.74,41.608000000000004,30.736 -2020-04-09 13:15:00,100.83,130.338,41.608000000000004,30.736 -2020-04-09 13:30:00,100.23,128.286,41.608000000000004,30.736 -2020-04-09 13:45:00,97.6,127.027,41.608000000000004,30.736 -2020-04-09 14:00:00,107.53,128.997,41.786,30.736 -2020-04-09 14:15:00,114.19,127.811,41.786,30.736 -2020-04-09 14:30:00,114.7,127.574,41.786,30.736 -2020-04-09 14:45:00,112.0,128.401,41.786,30.736 -2020-04-09 15:00:00,111.51,129.015,44.181999999999995,30.736 -2020-04-09 15:15:00,112.57,127.40299999999999,44.181999999999995,30.736 -2020-04-09 15:30:00,107.02,126.306,44.181999999999995,30.736 -2020-04-09 15:45:00,110.02,125.58,44.181999999999995,30.736 -2020-04-09 16:00:00,117.62,126.913,45.956,30.736 -2020-04-09 16:15:00,115.32,127.911,45.956,30.736 -2020-04-09 16:30:00,113.91,127.928,45.956,30.736 -2020-04-09 16:45:00,115.9,126.07,45.956,30.736 -2020-04-09 17:00:00,120.99,126.135,50.702,30.736 -2020-04-09 17:15:00,120.94,128.60299999999998,50.702,30.736 -2020-04-09 17:30:00,122.61,130.686,50.702,30.736 -2020-04-09 17:45:00,117.98,131.719,50.702,30.736 -2020-04-09 18:00:00,116.45,134.638,53.595,30.736 -2020-04-09 18:15:00,119.67,136.156,53.595,30.736 -2020-04-09 18:30:00,119.28,134.56,53.595,30.736 -2020-04-09 18:45:00,118.54,140.11,53.595,30.736 -2020-04-09 19:00:00,113.55,138.439,54.207,30.736 -2020-04-09 19:15:00,111.16,137.30700000000002,54.207,30.736 -2020-04-09 19:30:00,116.65,137.13,54.207,30.736 -2020-04-09 19:45:00,115.97,137.195,54.207,30.736 -2020-04-09 20:00:00,108.98,132.112,56.948,30.736 -2020-04-09 20:15:00,107.57,128.85299999999998,56.948,30.736 -2020-04-09 20:30:00,101.4,128.499,56.948,30.736 -2020-04-09 20:45:00,106.29,127.67299999999999,56.948,30.736 -2020-04-09 21:00:00,102.08,121.351,52.157,30.736 -2020-04-09 21:15:00,96.68,119.946,52.157,30.736 -2020-04-09 21:30:00,90.04,120.15100000000001,52.157,30.736 -2020-04-09 21:45:00,89.87,119.18700000000001,52.157,30.736 -2020-04-09 22:00:00,90.47,112.99700000000001,47.483000000000004,30.736 -2020-04-09 22:15:00,88.93,110.225,47.483000000000004,30.736 -2020-04-09 22:30:00,83.44,97.421,47.483000000000004,30.736 -2020-04-09 22:45:00,80.92,90.204,47.483000000000004,30.736 -2020-04-09 23:00:00,64.51,81.181,41.978,30.736 -2020-04-09 23:15:00,64.07,80.52199999999999,41.978,30.736 -2020-04-09 23:30:00,61.31,79.64699999999999,41.978,30.736 -2020-04-09 23:45:00,61.8,80.60300000000001,41.978,30.736 -2020-04-10 00:00:00,59.64,74.03399999999999,30.72,30.618000000000002 -2020-04-10 00:15:00,60.38,70.993,30.72,30.618000000000002 -2020-04-10 00:30:00,56.32,69.40899999999999,30.72,30.618000000000002 -2020-04-10 00:45:00,59.16,68.405,30.72,30.618000000000002 -2020-04-10 01:00:00,57.38,69.718,26.553,30.618000000000002 -2020-04-10 01:15:00,57.59,69.342,26.553,30.618000000000002 -2020-04-10 01:30:00,57.08,67.82300000000001,26.553,30.618000000000002 -2020-04-10 01:45:00,58.03,67.472,26.553,30.618000000000002 -2020-04-10 02:00:00,53.88,69.205,22.712,30.618000000000002 -2020-04-10 02:15:00,57.6,67.71,22.712,30.618000000000002 -2020-04-10 02:30:00,54.83,70.5,22.712,30.618000000000002 -2020-04-10 02:45:00,57.8,71.417,22.712,30.618000000000002 -2020-04-10 03:00:00,58.43,75.032,20.511999999999997,30.618000000000002 -2020-04-10 03:15:00,56.11,75.366,20.511999999999997,30.618000000000002 -2020-04-10 03:30:00,59.21,74.673,20.511999999999997,30.618000000000002 -2020-04-10 03:45:00,59.68,76.515,20.511999999999997,30.618000000000002 -2020-04-10 04:00:00,61.34,85.525,19.98,30.618000000000002 -2020-04-10 04:15:00,58.56,94.279,19.98,30.618000000000002 -2020-04-10 04:30:00,61.08,93.85799999999999,19.98,30.618000000000002 -2020-04-10 04:45:00,61.82,94.49600000000001,19.98,30.618000000000002 -2020-04-10 05:00:00,61.78,112.44,22.715,30.618000000000002 -2020-04-10 05:15:00,63.19,127.439,22.715,30.618000000000002 -2020-04-10 05:30:00,63.03,118.79899999999999,22.715,30.618000000000002 -2020-04-10 05:45:00,62.57,114.056,22.715,30.618000000000002 -2020-04-10 06:00:00,63.5,131.666,22.576999999999998,30.618000000000002 -2020-04-10 06:15:00,64.36,150.401,22.576999999999998,30.618000000000002 -2020-04-10 06:30:00,66.27,141.877,22.576999999999998,30.618000000000002 -2020-04-10 06:45:00,66.56,134.958,22.576999999999998,30.618000000000002 -2020-04-10 07:00:00,72.0,135.668,23.541999999999998,30.618000000000002 -2020-04-10 07:15:00,72.94,134.844,23.541999999999998,30.618000000000002 -2020-04-10 07:30:00,72.56,134.6,23.541999999999998,30.618000000000002 -2020-04-10 07:45:00,72.68,133.276,23.541999999999998,30.618000000000002 -2020-04-10 08:00:00,73.27,136.041,23.895,30.618000000000002 -2020-04-10 08:15:00,73.33,137.007,23.895,30.618000000000002 -2020-04-10 08:30:00,72.0,135.178,23.895,30.618000000000002 -2020-04-10 08:45:00,71.59,134.93200000000002,23.895,30.618000000000002 -2020-04-10 09:00:00,63.96,130.424,24.239,30.618000000000002 -2020-04-10 09:15:00,70.6,130.297,24.239,30.618000000000002 -2020-04-10 09:30:00,70.79,132.411,24.239,30.618000000000002 -2020-04-10 09:45:00,73.79,132.059,24.239,30.618000000000002 -2020-04-10 10:00:00,71.11,129.239,21.985,30.618000000000002 -2020-04-10 10:15:00,71.88,130.02,21.985,30.618000000000002 -2020-04-10 10:30:00,72.52,128.593,21.985,30.618000000000002 -2020-04-10 10:45:00,68.48,128.07,21.985,30.618000000000002 -2020-04-10 11:00:00,65.66,122.911,22.093000000000004,30.618000000000002 -2020-04-10 11:15:00,62.4,122.189,22.093000000000004,30.618000000000002 -2020-04-10 11:30:00,60.95,123.34700000000001,22.093000000000004,30.618000000000002 -2020-04-10 11:45:00,61.95,124.551,22.093000000000004,30.618000000000002 -2020-04-10 12:00:00,55.24,120.56299999999999,19.041,30.618000000000002 -2020-04-10 12:15:00,54.22,121.271,19.041,30.618000000000002 -2020-04-10 12:30:00,54.57,119.821,19.041,30.618000000000002 -2020-04-10 12:45:00,58.91,119.413,19.041,30.618000000000002 -2020-04-10 13:00:00,55.07,119.819,12.672,30.618000000000002 -2020-04-10 13:15:00,56.02,119.266,12.672,30.618000000000002 -2020-04-10 13:30:00,52.03,116.676,12.672,30.618000000000002 -2020-04-10 13:45:00,51.25,115.288,12.672,30.618000000000002 -2020-04-10 14:00:00,51.01,117.75200000000001,10.321,30.618000000000002 -2020-04-10 14:15:00,54.91,116.723,10.321,30.618000000000002 -2020-04-10 14:30:00,54.25,116.304,10.321,30.618000000000002 -2020-04-10 14:45:00,50.76,116.64299999999999,10.321,30.618000000000002 -2020-04-10 15:00:00,52.9,116.525,13.478,30.618000000000002 -2020-04-10 15:15:00,55.88,115.546,13.478,30.618000000000002 -2020-04-10 15:30:00,52.88,114.59100000000001,13.478,30.618000000000002 -2020-04-10 15:45:00,57.97,114.529,13.478,30.618000000000002 -2020-04-10 16:00:00,62.6,115.57799999999999,17.623,30.618000000000002 -2020-04-10 16:15:00,64.61,116.676,17.623,30.618000000000002 -2020-04-10 16:30:00,67.2,117.40700000000001,17.623,30.618000000000002 -2020-04-10 16:45:00,69.59,115.771,17.623,30.618000000000002 -2020-04-10 17:00:00,76.4,116.23299999999999,22.64,30.618000000000002 -2020-04-10 17:15:00,75.58,118.92200000000001,22.64,30.618000000000002 -2020-04-10 17:30:00,77.36,121.331,22.64,30.618000000000002 -2020-04-10 17:45:00,75.89,123.815,22.64,30.618000000000002 -2020-04-10 18:00:00,81.02,127.62,29.147,30.618000000000002 -2020-04-10 18:15:00,79.22,131.172,29.147,30.618000000000002 -2020-04-10 18:30:00,80.59,129.843,29.147,30.618000000000002 -2020-04-10 18:45:00,80.5,132.89600000000002,29.147,30.618000000000002 -2020-04-10 19:00:00,84.47,133.018,34.491,30.618000000000002 -2020-04-10 19:15:00,83.92,132.095,34.491,30.618000000000002 -2020-04-10 19:30:00,81.85,132.35,34.491,30.618000000000002 -2020-04-10 19:45:00,81.31,133.11700000000002,34.491,30.618000000000002 -2020-04-10 20:00:00,76.54,129.928,41.368,30.618000000000002 -2020-04-10 20:15:00,76.86,128.756,41.368,30.618000000000002 -2020-04-10 20:30:00,73.31,128.704,41.368,30.618000000000002 -2020-04-10 20:45:00,75.27,126.87899999999999,41.368,30.618000000000002 -2020-04-10 21:00:00,70.24,120.53399999999999,37.605,30.618000000000002 -2020-04-10 21:15:00,71.86,119.846,37.605,30.618000000000002 -2020-04-10 21:30:00,65.09,120.75200000000001,37.605,30.618000000000002 -2020-04-10 21:45:00,68.77,120.009,37.605,30.618000000000002 -2020-04-10 22:00:00,65.32,115.573,36.472,30.618000000000002 -2020-04-10 22:15:00,64.47,113.001,36.472,30.618000000000002 -2020-04-10 22:30:00,62.29,108.45200000000001,36.472,30.618000000000002 -2020-04-10 22:45:00,61.86,104.22399999999999,36.472,30.618000000000002 -2020-04-10 23:00:00,78.85,95.059,31.816,30.618000000000002 -2020-04-10 23:15:00,77.86,93.334,31.816,30.618000000000002 -2020-04-10 23:30:00,74.26,90.906,31.816,30.618000000000002 -2020-04-10 23:45:00,72.92,90.618,31.816,30.618000000000002 -2020-04-11 00:00:00,73.14,72.59,39.184,30.618000000000002 -2020-04-11 00:15:00,74.22,70.405,39.184,30.618000000000002 -2020-04-11 00:30:00,72.87,69.128,39.184,30.618000000000002 -2020-04-11 00:45:00,67.49,67.756,39.184,30.618000000000002 -2020-04-11 01:00:00,70.97,68.992,34.692,30.618000000000002 -2020-04-11 01:15:00,73.16,68.01899999999999,34.692,30.618000000000002 -2020-04-11 01:30:00,71.68,66.232,34.692,30.618000000000002 -2020-04-11 01:45:00,67.54,66.294,34.692,30.618000000000002 -2020-04-11 02:00:00,71.64,68.439,32.919000000000004,30.618000000000002 -2020-04-11 02:15:00,73.62,67.054,32.919000000000004,30.618000000000002 -2020-04-11 02:30:00,69.77,69.232,32.919000000000004,30.618000000000002 -2020-04-11 02:45:00,69.74,70.032,32.919000000000004,30.618000000000002 -2020-04-11 03:00:00,65.86,73.10600000000001,32.024,30.618000000000002 -2020-04-11 03:15:00,64.59,73.584,32.024,30.618000000000002 -2020-04-11 03:30:00,65.11,72.45,32.024,30.618000000000002 -2020-04-11 03:45:00,64.5,74.812,32.024,30.618000000000002 -2020-04-11 04:00:00,65.68,83.98200000000001,31.958000000000002,30.618000000000002 -2020-04-11 04:15:00,66.71,93.564,31.958000000000002,30.618000000000002 -2020-04-11 04:30:00,65.87,92.339,31.958000000000002,30.618000000000002 -2020-04-11 04:45:00,66.99,93.068,31.958000000000002,30.618000000000002 -2020-04-11 05:00:00,67.85,112.939,32.75,30.618000000000002 -2020-04-11 05:15:00,67.86,129.761,32.75,30.618000000000002 -2020-04-11 05:30:00,69.08,121.459,32.75,30.618000000000002 -2020-04-11 05:45:00,71.14,116.765,32.75,30.618000000000002 -2020-04-11 06:00:00,72.48,135.846,34.461999999999996,30.618000000000002 -2020-04-11 06:15:00,75.1,155.151,34.461999999999996,30.618000000000002 -2020-04-11 06:30:00,77.55,147.668,34.461999999999996,30.618000000000002 -2020-04-11 06:45:00,80.71,141.945,34.461999999999996,30.618000000000002 -2020-04-11 07:00:00,82.56,141.024,37.736,30.618000000000002 -2020-04-11 07:15:00,83.76,141.615,37.736,30.618000000000002 -2020-04-11 07:30:00,85.57,141.409,37.736,30.618000000000002 -2020-04-11 07:45:00,88.05,140.57399999999998,37.736,30.618000000000002 -2020-04-11 08:00:00,89.53,141.81,42.34,30.618000000000002 -2020-04-11 08:15:00,90.05,142.239,42.34,30.618000000000002 -2020-04-11 08:30:00,88.35,138.835,42.34,30.618000000000002 -2020-04-11 08:45:00,88.99,137.342,42.34,30.618000000000002 -2020-04-11 09:00:00,80.42,133.192,43.571999999999996,30.618000000000002 -2020-04-11 09:15:00,83.26,132.951,43.571999999999996,30.618000000000002 -2020-04-11 09:30:00,81.39,134.967,43.571999999999996,30.618000000000002 -2020-04-11 09:45:00,76.24,134.215,43.571999999999996,30.618000000000002 -2020-04-11 10:00:00,74.6,129.458,40.514,30.618000000000002 -2020-04-11 10:15:00,75.54,129.858,40.514,30.618000000000002 -2020-04-11 10:30:00,76.68,127.932,40.514,30.618000000000002 -2020-04-11 10:45:00,76.58,128.249,40.514,30.618000000000002 -2020-04-11 11:00:00,73.64,122.635,36.388000000000005,30.618000000000002 -2020-04-11 11:15:00,73.67,122.054,36.388000000000005,30.618000000000002 -2020-04-11 11:30:00,76.9,123.58200000000001,36.388000000000005,30.618000000000002 -2020-04-11 11:45:00,74.29,124.258,36.388000000000005,30.618000000000002 -2020-04-11 12:00:00,69.92,120.24700000000001,35.217,30.618000000000002 -2020-04-11 12:15:00,66.27,119.885,35.217,30.618000000000002 -2020-04-11 12:30:00,67.34,119.337,35.217,30.618000000000002 -2020-04-11 12:45:00,67.73,119.915,35.217,30.618000000000002 -2020-04-11 13:00:00,63.46,121.039,32.001999999999995,30.618000000000002 -2020-04-11 13:15:00,65.05,118.553,32.001999999999995,30.618000000000002 -2020-04-11 13:30:00,61.84,116.615,32.001999999999995,30.618000000000002 -2020-04-11 13:45:00,62.81,115.204,32.001999999999995,30.618000000000002 -2020-04-11 14:00:00,62.8,117.021,31.304000000000002,30.618000000000002 -2020-04-11 14:15:00,63.27,114.906,31.304000000000002,30.618000000000002 -2020-04-11 14:30:00,63.28,114.103,31.304000000000002,30.618000000000002 -2020-04-11 14:45:00,63.54,115.226,31.304000000000002,30.618000000000002 -2020-04-11 15:00:00,63.73,116.178,34.731,30.618000000000002 -2020-04-11 15:15:00,64.31,114.98899999999999,34.731,30.618000000000002 -2020-04-11 15:30:00,66.0,113.639,34.731,30.618000000000002 -2020-04-11 15:45:00,68.73,112.928,34.731,30.618000000000002 -2020-04-11 16:00:00,71.32,113.571,38.769,30.618000000000002 -2020-04-11 16:15:00,71.76,115.17,38.769,30.618000000000002 -2020-04-11 16:30:00,74.69,115.245,38.769,30.618000000000002 -2020-04-11 16:45:00,79.72,113.42200000000001,38.769,30.618000000000002 -2020-04-11 17:00:00,82.56,113.86,44.928000000000004,30.618000000000002 -2020-04-11 17:15:00,80.55,116.00200000000001,44.928000000000004,30.618000000000002 -2020-04-11 17:30:00,82.25,117.81700000000001,44.928000000000004,30.618000000000002 -2020-04-11 17:45:00,84.04,118.53399999999999,44.928000000000004,30.618000000000002 -2020-04-11 18:00:00,86.25,122.461,47.786,30.618000000000002 -2020-04-11 18:15:00,84.53,125.369,47.786,30.618000000000002 -2020-04-11 18:30:00,84.88,125.551,47.786,30.618000000000002 -2020-04-11 18:45:00,85.38,127.315,47.786,30.618000000000002 -2020-04-11 19:00:00,86.2,126.56,47.463,30.618000000000002 -2020-04-11 19:15:00,85.71,125.825,47.463,30.618000000000002 -2020-04-11 19:30:00,86.11,126.334,47.463,30.618000000000002 -2020-04-11 19:45:00,84.4,126.412,47.463,30.618000000000002 -2020-04-11 20:00:00,81.75,123.16,43.735,30.618000000000002 -2020-04-11 20:15:00,80.33,121.42200000000001,43.735,30.618000000000002 -2020-04-11 20:30:00,77.95,120.17399999999999,43.735,30.618000000000002 -2020-04-11 20:45:00,76.55,120.03200000000001,43.735,30.618000000000002 -2020-04-11 21:00:00,71.3,115.48700000000001,40.346,30.618000000000002 -2020-04-11 21:15:00,70.85,115.375,40.346,30.618000000000002 -2020-04-11 21:30:00,68.16,116.42200000000001,40.346,30.618000000000002 -2020-04-11 21:45:00,67.66,115.43,40.346,30.618000000000002 -2020-04-11 22:00:00,63.94,110.7,39.323,30.618000000000002 -2020-04-11 22:15:00,64.14,109.53200000000001,39.323,30.618000000000002 -2020-04-11 22:30:00,61.85,107.27600000000001,39.323,30.618000000000002 -2020-04-11 22:45:00,61.25,104.26299999999999,39.323,30.618000000000002 -2020-04-11 23:00:00,60.08,97.06,33.716,30.618000000000002 -2020-04-11 23:15:00,58.06,93.493,33.716,30.618000000000002 -2020-04-11 23:30:00,55.51,90.814,33.716,30.618000000000002 -2020-04-11 23:45:00,56.45,89.876,33.716,30.618000000000002 -2020-04-12 00:00:00,54.53,73.203,30.72,30.618000000000002 -2020-04-12 00:15:00,52.88,70.192,30.72,30.618000000000002 -2020-04-12 00:30:00,52.07,68.59,30.72,30.618000000000002 -2020-04-12 00:45:00,53.98,67.596,30.72,30.618000000000002 -2020-04-12 01:00:00,48.89,68.872,26.553,30.618000000000002 -2020-04-12 01:15:00,53.69,68.45100000000001,26.553,30.618000000000002 -2020-04-12 01:30:00,51.28,66.889,26.553,30.618000000000002 -2020-04-12 01:45:00,53.95,66.54899999999999,26.553,30.618000000000002 -2020-04-12 02:00:00,50.72,68.258,22.712,30.618000000000002 -2020-04-12 02:15:00,54.24,66.73,22.712,30.618000000000002 -2020-04-12 02:30:00,51.64,69.561,22.712,30.618000000000002 -2020-04-12 02:45:00,51.01,70.486,22.712,30.618000000000002 -2020-04-12 03:00:00,50.48,74.13600000000001,20.511999999999997,30.618000000000002 -2020-04-12 03:15:00,51.91,74.418,20.511999999999997,30.618000000000002 -2020-04-12 03:30:00,51.98,73.71600000000001,20.511999999999997,30.618000000000002 -2020-04-12 03:45:00,52.83,75.598,20.511999999999997,30.618000000000002 -2020-04-12 04:00:00,51.89,84.565,19.98,30.618000000000002 -2020-04-12 04:15:00,52.74,93.25200000000001,19.98,30.618000000000002 -2020-04-12 04:30:00,52.54,92.84100000000001,19.98,30.618000000000002 -2020-04-12 04:45:00,54.38,93.456,19.98,30.618000000000002 -2020-04-12 05:00:00,55.46,111.241,22.715,30.618000000000002 -2020-04-12 05:15:00,53.48,126.101,22.715,30.618000000000002 -2020-04-12 05:30:00,55.9,117.47200000000001,22.715,30.618000000000002 -2020-04-12 05:45:00,53.71,112.805,22.715,30.618000000000002 -2020-04-12 06:00:00,55.53,130.452,22.576999999999998,30.618000000000002 -2020-04-12 06:15:00,59.67,149.158,22.576999999999998,30.618000000000002 -2020-04-12 06:30:00,62.15,140.576,22.576999999999998,30.618000000000002 -2020-04-12 06:45:00,61.32,133.628,22.576999999999998,30.618000000000002 -2020-04-12 07:00:00,67.05,134.34799999999998,23.541999999999998,30.618000000000002 -2020-04-12 07:15:00,67.97,133.483,23.541999999999998,30.618000000000002 -2020-04-12 07:30:00,69.68,133.157,23.541999999999998,30.618000000000002 -2020-04-12 07:45:00,69.61,131.813,23.541999999999998,30.618000000000002 -2020-04-12 08:00:00,74.12,134.54,27.568,30.618000000000002 -2020-04-12 08:15:00,75.03,135.55200000000002,27.568,30.618000000000002 -2020-04-12 08:30:00,74.98,133.65,27.568,30.618000000000002 -2020-04-12 08:45:00,73.91,133.46200000000002,27.568,30.618000000000002 -2020-04-12 09:00:00,76.48,128.969,27.965,30.618000000000002 -2020-04-12 09:15:00,73.74,128.851,27.965,30.618000000000002 -2020-04-12 09:30:00,75.72,130.999,27.965,30.618000000000002 -2020-04-12 09:45:00,74.12,130.707,27.965,30.618000000000002 -2020-04-12 10:00:00,72.15,127.902,25.365,30.618000000000002 -2020-04-12 10:15:00,77.67,128.786,25.365,30.618000000000002 -2020-04-12 10:30:00,77.66,127.40899999999999,25.365,30.618000000000002 -2020-04-12 10:45:00,82.07,126.928,25.365,30.618000000000002 -2020-04-12 11:00:00,72.99,121.75399999999999,25.489,30.618000000000002 -2020-04-12 11:15:00,75.01,121.08,25.489,30.618000000000002 -2020-04-12 11:30:00,71.52,122.244,25.489,30.618000000000002 -2020-04-12 11:45:00,69.58,123.48700000000001,25.489,30.618000000000002 -2020-04-12 12:00:00,68.21,119.55,21.968000000000004,30.618000000000002 -2020-04-12 12:15:00,67.52,120.275,21.968000000000004,30.618000000000002 -2020-04-12 12:30:00,66.81,118.738,21.968000000000004,30.618000000000002 -2020-04-12 12:45:00,67.38,118.333,21.968000000000004,30.618000000000002 -2020-04-12 13:00:00,60.59,118.82600000000001,14.62,30.618000000000002 -2020-04-12 13:15:00,59.39,118.251,14.62,30.618000000000002 -2020-04-12 13:30:00,58.18,115.65799999999999,14.62,30.618000000000002 -2020-04-12 13:45:00,60.55,114.27600000000001,14.62,30.618000000000002 -2020-04-12 14:00:00,59.16,116.87700000000001,11.908,30.618000000000002 -2020-04-12 14:15:00,60.01,115.804,11.908,30.618000000000002 -2020-04-12 14:30:00,56.92,115.295,11.908,30.618000000000002 -2020-04-12 14:45:00,58.07,115.64200000000001,11.908,30.618000000000002 -2020-04-12 15:00:00,61.44,115.57700000000001,15.55,30.618000000000002 -2020-04-12 15:15:00,62.16,114.54899999999999,15.55,30.618000000000002 -2020-04-12 15:30:00,62.09,113.494,15.55,30.618000000000002 -2020-04-12 15:45:00,63.69,113.395,15.55,30.618000000000002 -2020-04-12 16:00:00,63.44,114.516,20.332,30.618000000000002 -2020-04-12 16:15:00,65.92,115.56,20.332,30.618000000000002 -2020-04-12 16:30:00,69.07,116.295,20.332,30.618000000000002 -2020-04-12 16:45:00,69.42,114.53200000000001,20.332,30.618000000000002 -2020-04-12 17:00:00,77.2,115.094,26.121,30.618000000000002 -2020-04-12 17:15:00,75.08,117.742,26.121,30.618000000000002 -2020-04-12 17:30:00,76.8,120.152,26.121,30.618000000000002 -2020-04-12 17:45:00,76.23,122.604,26.121,30.618000000000002 -2020-04-12 18:00:00,79.58,126.429,33.626999999999995,30.618000000000002 -2020-04-12 18:15:00,77.87,130.05700000000002,33.626999999999995,30.618000000000002 -2020-04-12 18:30:00,78.53,128.704,33.626999999999995,30.618000000000002 -2020-04-12 18:45:00,80.41,131.775,33.626999999999995,30.618000000000002 -2020-04-12 19:00:00,79.37,131.862,39.793,30.618000000000002 -2020-04-12 19:15:00,79.52,130.96200000000002,39.793,30.618000000000002 -2020-04-12 19:30:00,78.47,131.251,39.793,30.618000000000002 -2020-04-12 19:45:00,77.25,132.084,39.793,30.618000000000002 -2020-04-12 20:00:00,74.47,128.839,41.368,30.618000000000002 -2020-04-12 20:15:00,73.54,127.68799999999999,41.368,30.618000000000002 -2020-04-12 20:30:00,73.03,127.709,41.368,30.618000000000002 -2020-04-12 20:45:00,72.54,125.928,41.368,30.618000000000002 -2020-04-12 21:00:00,69.45,119.583,37.605,30.618000000000002 -2020-04-12 21:15:00,69.2,118.919,37.605,30.618000000000002 -2020-04-12 21:30:00,64.59,119.805,37.605,30.618000000000002 -2020-04-12 21:45:00,67.2,119.119,37.605,30.618000000000002 -2020-04-12 22:00:00,62.87,114.694,36.472,30.618000000000002 -2020-04-12 22:15:00,62.97,112.175,36.472,30.618000000000002 -2020-04-12 22:30:00,58.12,107.554,36.472,30.618000000000002 -2020-04-12 22:45:00,60.08,103.31299999999999,36.472,30.618000000000002 -2020-04-12 23:00:00,57.22,94.10700000000001,31.816,30.618000000000002 -2020-04-12 23:15:00,57.26,92.449,31.816,30.618000000000002 -2020-04-12 23:30:00,53.07,90.01700000000001,31.816,30.618000000000002 -2020-04-12 23:45:00,55.28,89.759,31.816,30.618000000000002 -2020-04-13 00:00:00,49.89,72.78699999999999,30.72,30.618000000000002 -2020-04-13 00:15:00,53.6,69.791,30.72,30.618000000000002 -2020-04-13 00:30:00,50.52,68.178,30.72,30.618000000000002 -2020-04-13 00:45:00,53.11,67.191,30.72,30.618000000000002 -2020-04-13 01:00:00,47.65,68.449,26.553,30.618000000000002 -2020-04-13 01:15:00,51.91,68.006,26.553,30.618000000000002 -2020-04-13 01:30:00,48.7,66.422,26.553,30.618000000000002 -2020-04-13 01:45:00,51.4,66.087,26.553,30.618000000000002 -2020-04-13 02:00:00,49.72,67.783,22.712,30.618000000000002 -2020-04-13 02:15:00,48.31,66.24,22.712,30.618000000000002 -2020-04-13 02:30:00,50.79,69.09100000000001,22.712,30.618000000000002 -2020-04-13 02:45:00,48.57,70.021,22.712,30.618000000000002 -2020-04-13 03:00:00,51.31,73.689,20.511999999999997,30.618000000000002 -2020-04-13 03:15:00,52.15,73.943,20.511999999999997,30.618000000000002 -2020-04-13 03:30:00,51.38,73.236,20.511999999999997,30.618000000000002 -2020-04-13 03:45:00,54.02,75.139,20.511999999999997,30.618000000000002 -2020-04-13 04:00:00,54.39,84.085,19.98,30.618000000000002 -2020-04-13 04:15:00,55.93,92.73899999999999,19.98,30.618000000000002 -2020-04-13 04:30:00,56.7,92.33200000000001,19.98,30.618000000000002 -2020-04-13 04:45:00,57.44,92.93700000000001,19.98,30.618000000000002 -2020-04-13 05:00:00,58.82,110.641,22.715,30.618000000000002 -2020-04-13 05:15:00,59.57,125.43,22.715,30.618000000000002 -2020-04-13 05:30:00,57.7,116.80799999999999,22.715,30.618000000000002 -2020-04-13 05:45:00,55.37,112.177,22.715,30.618000000000002 -2020-04-13 06:00:00,58.84,129.845,22.576999999999998,30.618000000000002 -2020-04-13 06:15:00,57.13,148.534,22.576999999999998,30.618000000000002 -2020-04-13 06:30:00,60.7,139.924,22.576999999999998,30.618000000000002 -2020-04-13 06:45:00,62.85,132.963,22.576999999999998,30.618000000000002 -2020-04-13 07:00:00,68.7,133.687,23.541999999999998,30.618000000000002 -2020-04-13 07:15:00,68.91,132.80200000000002,23.541999999999998,30.618000000000002 -2020-04-13 07:30:00,66.28,132.436,23.541999999999998,30.618000000000002 -2020-04-13 07:45:00,71.72,131.08,23.541999999999998,30.618000000000002 -2020-04-13 08:00:00,70.13,133.79,23.895,30.618000000000002 -2020-04-13 08:15:00,72.83,134.825,23.895,30.618000000000002 -2020-04-13 08:30:00,69.75,132.886,23.895,30.618000000000002 -2020-04-13 08:45:00,71.31,132.72899999999998,23.895,30.618000000000002 -2020-04-13 09:00:00,67.16,128.24200000000002,24.239,30.618000000000002 -2020-04-13 09:15:00,66.12,128.127,24.239,30.618000000000002 -2020-04-13 09:30:00,64.1,130.295,24.239,30.618000000000002 -2020-04-13 09:45:00,66.42,130.032,24.239,30.618000000000002 -2020-04-13 10:00:00,67.4,127.23700000000001,21.985,30.618000000000002 -2020-04-13 10:15:00,66.6,128.17,21.985,30.618000000000002 -2020-04-13 10:30:00,70.58,126.81700000000001,21.985,30.618000000000002 -2020-04-13 10:45:00,69.07,126.359,21.985,30.618000000000002 -2020-04-13 11:00:00,63.2,121.177,22.093000000000004,30.618000000000002 -2020-04-13 11:15:00,62.5,120.52799999999999,22.093000000000004,30.618000000000002 -2020-04-13 11:30:00,63.4,121.693,22.093000000000004,30.618000000000002 -2020-04-13 11:45:00,71.72,122.95700000000001,22.093000000000004,30.618000000000002 -2020-04-13 12:00:00,63.81,119.045,19.041,30.618000000000002 -2020-04-13 12:15:00,54.06,119.77799999999999,19.041,30.618000000000002 -2020-04-13 12:30:00,54.23,118.197,19.041,30.618000000000002 -2020-04-13 12:45:00,56.47,117.792,19.041,30.618000000000002 -2020-04-13 13:00:00,56.38,118.329,12.672,30.618000000000002 -2020-04-13 13:15:00,51.32,117.744,12.672,30.618000000000002 -2020-04-13 13:30:00,50.09,115.15,12.672,30.618000000000002 -2020-04-13 13:45:00,52.76,113.772,12.672,30.618000000000002 -2020-04-13 14:00:00,50.0,116.441,10.321,30.618000000000002 -2020-04-13 14:15:00,47.98,115.345,10.321,30.618000000000002 -2020-04-13 14:30:00,46.88,114.791,10.321,30.618000000000002 -2020-04-13 14:45:00,51.58,115.14200000000001,10.321,30.618000000000002 -2020-04-13 15:00:00,52.85,115.103,13.478,30.618000000000002 -2020-04-13 15:15:00,53.15,114.051,13.478,30.618000000000002 -2020-04-13 15:30:00,51.27,112.945,13.478,30.618000000000002 -2020-04-13 15:45:00,52.98,112.829,13.478,30.618000000000002 -2020-04-13 16:00:00,57.21,113.985,17.623,30.618000000000002 -2020-04-13 16:15:00,61.89,115.00399999999999,17.623,30.618000000000002 -2020-04-13 16:30:00,63.48,115.741,17.623,30.618000000000002 -2020-04-13 16:45:00,65.1,113.913,17.623,30.618000000000002 -2020-04-13 17:00:00,72.1,114.525,22.64,30.618000000000002 -2020-04-13 17:15:00,73.56,117.152,22.64,30.618000000000002 -2020-04-13 17:30:00,76.1,119.56299999999999,22.64,30.618000000000002 -2020-04-13 17:45:00,77.85,121.99799999999999,22.64,30.618000000000002 -2020-04-13 18:00:00,78.34,125.833,29.147,30.618000000000002 -2020-04-13 18:15:00,78.86,129.499,29.147,30.618000000000002 -2020-04-13 18:30:00,81.38,128.134,29.147,30.618000000000002 -2020-04-13 18:45:00,82.16,131.215,29.147,30.618000000000002 -2020-04-13 19:00:00,86.48,131.284,34.491,30.618000000000002 -2020-04-13 19:15:00,87.39,130.394,34.491,30.618000000000002 -2020-04-13 19:30:00,85.34,130.702,34.491,30.618000000000002 -2020-04-13 19:45:00,84.46,131.567,34.491,30.618000000000002 -2020-04-13 20:00:00,81.57,128.29399999999998,41.368,30.618000000000002 -2020-04-13 20:15:00,80.87,127.15299999999999,41.368,30.618000000000002 -2020-04-13 20:30:00,77.82,127.21,41.368,30.618000000000002 -2020-04-13 20:45:00,80.82,125.45200000000001,41.368,30.618000000000002 -2020-04-13 21:00:00,80.42,119.10700000000001,37.605,30.618000000000002 -2020-04-13 21:15:00,79.52,118.454,37.605,30.618000000000002 -2020-04-13 21:30:00,74.22,119.331,37.605,30.618000000000002 -2020-04-13 21:45:00,76.26,118.67399999999999,37.605,30.618000000000002 -2020-04-13 22:00:00,68.73,114.255,36.472,30.618000000000002 -2020-04-13 22:15:00,70.32,111.76100000000001,36.472,30.618000000000002 -2020-04-13 22:30:00,68.58,107.104,36.472,30.618000000000002 -2020-04-13 22:45:00,70.36,102.85700000000001,36.472,30.618000000000002 -2020-04-13 23:00:00,81.66,93.63,31.816,30.618000000000002 -2020-04-13 23:15:00,81.85,92.005,31.816,30.618000000000002 -2020-04-13 23:30:00,75.45,89.572,31.816,30.618000000000002 -2020-04-13 23:45:00,76.28,89.32799999999999,31.816,30.618000000000002 -2020-04-14 00:00:00,77.57,74.116,39.857,30.736 -2020-04-14 00:15:00,78.7,74.781,39.857,30.736 -2020-04-14 00:30:00,77.84,72.835,39.857,30.736 -2020-04-14 00:45:00,74.5,71.13600000000001,39.857,30.736 -2020-04-14 01:00:00,78.14,72.16,37.233000000000004,30.736 -2020-04-14 01:15:00,79.78,71.166,37.233000000000004,30.736 -2020-04-14 01:30:00,79.36,69.831,37.233000000000004,30.736 -2020-04-14 01:45:00,76.71,69.417,37.233000000000004,30.736 -2020-04-14 02:00:00,79.63,71.053,35.856,30.736 -2020-04-14 02:15:00,80.53,70.399,35.856,30.736 -2020-04-14 02:30:00,77.64,72.979,35.856,30.736 -2020-04-14 02:45:00,77.36,73.646,35.856,30.736 -2020-04-14 03:00:00,80.42,77.32,34.766999999999996,30.736 -2020-04-14 03:15:00,77.51,78.97,34.766999999999996,30.736 -2020-04-14 03:30:00,78.75,78.759,34.766999999999996,30.736 -2020-04-14 03:45:00,85.63,79.569,34.766999999999996,30.736 -2020-04-14 04:00:00,90.63,92.03,35.468,30.736 -2020-04-14 04:15:00,89.42,104.62,35.468,30.736 -2020-04-14 04:30:00,89.05,105.006,35.468,30.736 -2020-04-14 04:45:00,91.92,106.92,35.468,30.736 -2020-04-14 05:00:00,100.81,141.918,40.399,30.736 -2020-04-14 05:15:00,102.84,174.856,40.399,30.736 -2020-04-14 05:30:00,105.47,164.701,40.399,30.736 -2020-04-14 05:45:00,107.4,154.543,40.399,30.736 -2020-04-14 06:00:00,112.59,155.791,54.105,30.736 -2020-04-14 06:15:00,113.37,161.384,54.105,30.736 -2020-04-14 06:30:00,116.15,159.305,54.105,30.736 -2020-04-14 06:45:00,117.75,159.95,54.105,30.736 -2020-04-14 07:00:00,122.9,162.8,63.083,30.736 -2020-04-14 07:15:00,119.48,163.681,63.083,30.736 -2020-04-14 07:30:00,122.96,161.981,63.083,30.736 -2020-04-14 07:45:00,119.45,158.767,63.083,30.736 -2020-04-14 08:00:00,120.93,157.66,57.254,30.736 -2020-04-14 08:15:00,123.65,155.789,57.254,30.736 -2020-04-14 08:30:00,125.42,150.616,57.254,30.736 -2020-04-14 08:45:00,126.12,147.766,57.254,30.736 -2020-04-14 09:00:00,121.6,141.899,51.395,30.736 -2020-04-14 09:15:00,123.45,139.561,51.395,30.736 -2020-04-14 09:30:00,123.95,141.412,51.395,30.736 -2020-04-14 09:45:00,124.96,140.746,51.395,30.736 -2020-04-14 10:00:00,122.16,136.626,48.201,30.736 -2020-04-14 10:15:00,123.54,136.503,48.201,30.736 -2020-04-14 10:30:00,121.52,134.487,48.201,30.736 -2020-04-14 10:45:00,120.31,134.483,48.201,30.736 -2020-04-14 11:00:00,116.44,128.762,46.133,30.736 -2020-04-14 11:15:00,111.12,129.491,46.133,30.736 -2020-04-14 11:30:00,108.43,130.6,46.133,30.736 -2020-04-14 11:45:00,109.42,132.118,46.133,30.736 -2020-04-14 12:00:00,104.93,127.803,44.243,30.736 -2020-04-14 12:15:00,109.73,128.44299999999998,44.243,30.736 -2020-04-14 12:30:00,108.63,127.42299999999999,44.243,30.736 -2020-04-14 12:45:00,105.87,128.214,44.243,30.736 -2020-04-14 13:00:00,99.12,129.259,45.042,30.736 -2020-04-14 13:15:00,98.79,127.802,45.042,30.736 -2020-04-14 13:30:00,104.88,125.742,45.042,30.736 -2020-04-14 13:45:00,105.06,124.499,45.042,30.736 -2020-04-14 14:00:00,104.5,126.81,44.062,30.736 -2020-04-14 14:15:00,101.57,125.516,44.062,30.736 -2020-04-14 14:30:00,96.19,125.055,44.062,30.736 -2020-04-14 14:45:00,96.11,125.898,44.062,30.736 -2020-04-14 15:00:00,107.14,126.646,46.461999999999996,30.736 -2020-04-14 15:15:00,106.37,124.911,46.461999999999996,30.736 -2020-04-14 15:30:00,104.16,123.564,46.461999999999996,30.736 -2020-04-14 15:45:00,104.55,122.74700000000001,46.461999999999996,30.736 -2020-04-14 16:00:00,107.58,124.26,48.802,30.736 -2020-04-14 16:15:00,113.1,125.12299999999999,48.802,30.736 -2020-04-14 16:30:00,111.12,125.152,48.802,30.736 -2020-04-14 16:45:00,110.6,122.97399999999999,48.802,30.736 -2020-04-14 17:00:00,112.54,123.29,55.672,30.736 -2020-04-14 17:15:00,115.47,125.654,55.672,30.736 -2020-04-14 17:30:00,119.06,127.742,55.672,30.736 -2020-04-14 17:45:00,115.7,128.691,55.672,30.736 -2020-04-14 18:00:00,112.52,131.662,57.006,30.736 -2020-04-14 18:15:00,117.42,133.36700000000002,57.006,30.736 -2020-04-14 18:30:00,119.58,131.71200000000002,57.006,30.736 -2020-04-14 18:45:00,117.04,137.30700000000002,57.006,30.736 -2020-04-14 19:00:00,110.56,135.55100000000002,57.148,30.736 -2020-04-14 19:15:00,107.17,134.471,57.148,30.736 -2020-04-14 19:30:00,115.88,134.385,57.148,30.736 -2020-04-14 19:45:00,113.92,134.611,57.148,30.736 -2020-04-14 20:00:00,110.45,129.387,61.895,30.736 -2020-04-14 20:15:00,109.71,126.18299999999999,61.895,30.736 -2020-04-14 20:30:00,100.85,126.009,61.895,30.736 -2020-04-14 20:45:00,108.91,125.295,61.895,30.736 -2020-04-14 21:00:00,103.46,118.976,54.78,30.736 -2020-04-14 21:15:00,102.44,117.626,54.78,30.736 -2020-04-14 21:30:00,93.5,117.785,54.78,30.736 -2020-04-14 21:45:00,87.39,116.962,54.78,30.736 -2020-04-14 22:00:00,87.83,110.8,50.76,30.736 -2020-04-14 22:15:00,90.13,108.15799999999999,50.76,30.736 -2020-04-14 22:30:00,87.92,95.17399999999999,50.76,30.736 -2020-04-14 22:45:00,84.46,87.926,50.76,30.736 -2020-04-14 23:00:00,74.36,78.79899999999999,44.162,30.736 -2020-04-14 23:15:00,76.0,78.307,44.162,30.736 -2020-04-14 23:30:00,74.94,77.425,44.162,30.736 -2020-04-14 23:45:00,73.87,78.453,44.162,30.736 -2020-04-15 00:00:00,70.64,73.699,39.061,30.736 -2020-04-15 00:15:00,72.45,74.37899999999999,39.061,30.736 -2020-04-15 00:30:00,71.26,72.425,39.061,30.736 -2020-04-15 00:45:00,73.44,70.73100000000001,39.061,30.736 -2020-04-15 01:00:00,71.82,71.737,35.795,30.736 -2020-04-15 01:15:00,71.14,70.721,35.795,30.736 -2020-04-15 01:30:00,71.08,69.36399999999999,35.795,30.736 -2020-04-15 01:45:00,72.01,68.956,35.795,30.736 -2020-04-15 02:00:00,70.74,70.58,33.316,30.736 -2020-04-15 02:15:00,70.1,69.90899999999999,33.316,30.736 -2020-04-15 02:30:00,72.27,72.509,33.316,30.736 -2020-04-15 02:45:00,78.4,73.181,33.316,30.736 -2020-04-15 03:00:00,80.61,76.872,32.803000000000004,30.736 -2020-04-15 03:15:00,83.75,78.49600000000001,32.803000000000004,30.736 -2020-04-15 03:30:00,82.05,78.28,32.803000000000004,30.736 -2020-04-15 03:45:00,86.07,79.111,32.803000000000004,30.736 -2020-04-15 04:00:00,88.31,91.54899999999999,34.235,30.736 -2020-04-15 04:15:00,89.49,104.10700000000001,34.235,30.736 -2020-04-15 04:30:00,87.96,104.49600000000001,34.235,30.736 -2020-04-15 04:45:00,93.45,106.40100000000001,34.235,30.736 -2020-04-15 05:00:00,98.18,141.31799999999998,38.65,30.736 -2020-04-15 05:15:00,101.94,174.18400000000003,38.65,30.736 -2020-04-15 05:30:00,105.86,164.03599999999997,38.65,30.736 -2020-04-15 05:45:00,109.69,153.917,38.65,30.736 -2020-04-15 06:00:00,114.65,155.183,54.951,30.736 -2020-04-15 06:15:00,114.16,160.76,54.951,30.736 -2020-04-15 06:30:00,116.39,158.65200000000002,54.951,30.736 -2020-04-15 06:45:00,116.53,159.284,54.951,30.736 -2020-04-15 07:00:00,117.39,162.137,67.328,30.736 -2020-04-15 07:15:00,117.4,162.999,67.328,30.736 -2020-04-15 07:30:00,115.55,161.259,67.328,30.736 -2020-04-15 07:45:00,112.28,158.037,67.328,30.736 -2020-04-15 08:00:00,111.52,156.912,60.23,30.736 -2020-04-15 08:15:00,111.85,155.064,60.23,30.736 -2020-04-15 08:30:00,112.04,149.856,60.23,30.736 -2020-04-15 08:45:00,110.65,147.036,60.23,30.736 -2020-04-15 09:00:00,106.92,141.17600000000002,56.845,30.736 -2020-04-15 09:15:00,107.34,138.842,56.845,30.736 -2020-04-15 09:30:00,106.27,140.71,56.845,30.736 -2020-04-15 09:45:00,106.1,140.07299999999998,56.845,30.736 -2020-04-15 10:00:00,105.75,135.961,53.832,30.736 -2020-04-15 10:15:00,106.27,135.888,53.832,30.736 -2020-04-15 10:30:00,106.67,133.898,53.832,30.736 -2020-04-15 10:45:00,105.02,133.914,53.832,30.736 -2020-04-15 11:00:00,103.38,128.187,53.225,30.736 -2020-04-15 11:15:00,103.27,128.941,53.225,30.736 -2020-04-15 11:30:00,104.28,130.05100000000002,53.225,30.736 -2020-04-15 11:45:00,102.15,131.589,53.225,30.736 -2020-04-15 12:00:00,100.29,127.3,50.676,30.736 -2020-04-15 12:15:00,98.13,127.947,50.676,30.736 -2020-04-15 12:30:00,104.5,126.884,50.676,30.736 -2020-04-15 12:45:00,105.18,127.676,50.676,30.736 -2020-04-15 13:00:00,101.11,128.764,50.646,30.736 -2020-04-15 13:15:00,100.65,127.29700000000001,50.646,30.736 -2020-04-15 13:30:00,102.83,125.236,50.646,30.736 -2020-04-15 13:45:00,103.75,123.99700000000001,50.646,30.736 -2020-04-15 14:00:00,104.03,126.375,50.786,30.736 -2020-04-15 14:15:00,97.55,125.059,50.786,30.736 -2020-04-15 14:30:00,97.21,124.553,50.786,30.736 -2020-04-15 14:45:00,102.82,125.4,50.786,30.736 -2020-04-15 15:00:00,103.26,126.17299999999999,51.535,30.736 -2020-04-15 15:15:00,100.4,124.414,51.535,30.736 -2020-04-15 15:30:00,99.53,123.01799999999999,51.535,30.736 -2020-04-15 15:45:00,104.12,122.182,51.535,30.736 -2020-04-15 16:00:00,104.01,123.73200000000001,53.157,30.736 -2020-04-15 16:15:00,108.96,124.56700000000001,53.157,30.736 -2020-04-15 16:30:00,107.57,124.6,53.157,30.736 -2020-04-15 16:45:00,115.32,122.35799999999999,53.157,30.736 -2020-04-15 17:00:00,116.1,122.72399999999999,57.793,30.736 -2020-04-15 17:15:00,113.72,125.06700000000001,57.793,30.736 -2020-04-15 17:30:00,111.49,127.154,57.793,30.736 -2020-04-15 17:45:00,115.74,128.088,57.793,30.736 -2020-04-15 18:00:00,119.64,131.067,59.872,30.736 -2020-04-15 18:15:00,116.25,132.809,59.872,30.736 -2020-04-15 18:30:00,111.49,131.142,59.872,30.736 -2020-04-15 18:45:00,111.02,136.745,59.872,30.736 -2020-04-15 19:00:00,115.27,134.97299999999998,60.17100000000001,30.736 -2020-04-15 19:15:00,112.47,133.905,60.17100000000001,30.736 -2020-04-15 19:30:00,112.99,133.835,60.17100000000001,30.736 -2020-04-15 19:45:00,112.24,134.093,60.17100000000001,30.736 -2020-04-15 20:00:00,110.1,128.842,65.015,30.736 -2020-04-15 20:15:00,111.51,125.648,65.015,30.736 -2020-04-15 20:30:00,106.08,125.51100000000001,65.015,30.736 -2020-04-15 20:45:00,109.31,124.819,65.015,30.736 -2020-04-15 21:00:00,104.34,118.501,57.805,30.736 -2020-04-15 21:15:00,101.95,117.163,57.805,30.736 -2020-04-15 21:30:00,93.9,117.31200000000001,57.805,30.736 -2020-04-15 21:45:00,86.0,116.51700000000001,57.805,30.736 -2020-04-15 22:00:00,78.84,110.359,52.115,30.736 -2020-04-15 22:15:00,87.14,107.744,52.115,30.736 -2020-04-15 22:30:00,85.74,94.72200000000001,52.115,30.736 -2020-04-15 22:45:00,86.73,87.46799999999999,52.115,30.736 -2020-04-15 23:00:00,77.18,78.321,42.871,30.736 -2020-04-15 23:15:00,76.65,77.863,42.871,30.736 -2020-04-15 23:30:00,76.51,76.979,42.871,30.736 -2020-04-15 23:45:00,79.0,78.02199999999999,42.871,30.736 -2020-04-16 00:00:00,76.59,66.062,39.203,30.736 -2020-04-16 00:15:00,76.53,66.53699999999999,39.203,30.736 -2020-04-16 00:30:00,74.45,64.943,39.203,30.736 -2020-04-16 00:45:00,72.38,63.372,39.203,30.736 -2020-04-16 01:00:00,73.63,63.707,37.118,30.736 -2020-04-16 01:15:00,77.98,63.019,37.118,30.736 -2020-04-16 01:30:00,75.57,61.729,37.118,30.736 -2020-04-16 01:45:00,78.14,61.175,37.118,30.736 -2020-04-16 02:00:00,69.37,62.033,35.647,30.736 -2020-04-16 02:15:00,73.86,61.263000000000005,35.647,30.736 -2020-04-16 02:30:00,69.08,63.718999999999994,35.647,30.736 -2020-04-16 02:45:00,73.63,64.268,35.647,30.736 -2020-04-16 03:00:00,78.41,67.297,34.585,30.736 -2020-04-16 03:15:00,81.88,68.52,34.585,30.736 -2020-04-16 03:30:00,81.3,67.967,34.585,30.736 -2020-04-16 03:45:00,77.39,68.23100000000001,34.585,30.736 -2020-04-16 04:00:00,81.22,80.163,36.184,30.736 -2020-04-16 04:15:00,82.62,92.54,36.184,30.736 -2020-04-16 04:30:00,86.64,92.275,36.184,30.736 -2020-04-16 04:45:00,90.81,94.12799999999999,36.184,30.736 -2020-04-16 05:00:00,97.5,127.575,41.019,30.736 -2020-04-16 05:15:00,101.04,159.359,41.019,30.736 -2020-04-16 05:30:00,104.32,148.222,41.019,30.736 -2020-04-16 05:45:00,104.7,137.95,41.019,30.736 -2020-04-16 06:00:00,111.42,139.596,53.963,30.736 -2020-04-16 06:15:00,111.14,144.635,53.963,30.736 -2020-04-16 06:30:00,112.46,141.951,53.963,30.736 -2020-04-16 06:45:00,111.75,142.252,53.963,30.736 -2020-04-16 07:00:00,113.92,144.352,66.512,30.736 -2020-04-16 07:15:00,113.61,144.842,66.512,30.736 -2020-04-16 07:30:00,113.32,143.05100000000002,66.512,30.736 -2020-04-16 07:45:00,111.01,139.931,66.512,30.736 -2020-04-16 08:00:00,109.42,137.489,58.86,30.736 -2020-04-16 08:15:00,108.68,135.996,58.86,30.736 -2020-04-16 08:30:00,109.43,131.921,58.86,30.736 -2020-04-16 08:45:00,108.55,130.05200000000002,58.86,30.736 -2020-04-16 09:00:00,107.62,123.76799999999999,52.156000000000006,30.736 -2020-04-16 09:15:00,105.64,121.553,52.156000000000006,30.736 -2020-04-16 09:30:00,105.44,123.573,52.156000000000006,30.736 -2020-04-16 09:45:00,105.36,123.007,52.156000000000006,30.736 -2020-04-16 10:00:00,104.88,118.589,49.034,30.736 -2020-04-16 10:15:00,105.52,118.499,49.034,30.736 -2020-04-16 10:30:00,104.66,116.969,49.034,30.736 -2020-04-16 10:45:00,103.7,117.022,49.034,30.736 -2020-04-16 11:00:00,101.79,111.735,46.53,30.736 -2020-04-16 11:15:00,100.62,112.494,46.53,30.736 -2020-04-16 11:30:00,102.26,113.68,46.53,30.736 -2020-04-16 11:45:00,100.98,114.271,46.53,30.736 -2020-04-16 12:00:00,101.74,109.26899999999999,43.318000000000005,30.736 -2020-04-16 12:15:00,100.04,110.089,43.318000000000005,30.736 -2020-04-16 12:30:00,100.71,109.241,43.318000000000005,30.736 -2020-04-16 12:45:00,98.75,109.965,43.318000000000005,30.736 -2020-04-16 13:00:00,96.55,110.53399999999999,41.608000000000004,30.736 -2020-04-16 13:15:00,101.33,109.822,41.608000000000004,30.736 -2020-04-16 13:30:00,105.43,107.68799999999999,41.608000000000004,30.736 -2020-04-16 13:45:00,99.87,106.266,41.608000000000004,30.736 -2020-04-16 14:00:00,104.65,107.289,41.786,30.736 -2020-04-16 14:15:00,103.74,106.805,41.786,30.736 -2020-04-16 14:30:00,99.2,106.686,41.786,30.736 -2020-04-16 14:45:00,96.98,107.146,41.786,30.736 -2020-04-16 15:00:00,97.32,108.044,44.181999999999995,30.736 -2020-04-16 15:15:00,97.34,106.54299999999999,44.181999999999995,30.736 -2020-04-16 15:30:00,96.99,106.125,44.181999999999995,30.736 -2020-04-16 15:45:00,105.87,105.76,44.181999999999995,30.736 -2020-04-16 16:00:00,108.15,105.584,45.956,30.736 -2020-04-16 16:15:00,109.03,105.436,45.956,30.736 -2020-04-16 16:30:00,102.46,105.414,45.956,30.736 -2020-04-16 16:45:00,107.51,103.3,45.956,30.736 -2020-04-16 17:00:00,105.77,102.85,50.702,30.736 -2020-04-16 17:15:00,109.81,105.25399999999999,50.702,30.736 -2020-04-16 17:30:00,116.22,106.64299999999999,50.702,30.736 -2020-04-16 17:45:00,115.39,108.116,50.702,30.736 -2020-04-16 18:00:00,117.78,108.929,53.595,30.736 -2020-04-16 18:15:00,112.22,110.46799999999999,53.595,30.736 -2020-04-16 18:30:00,111.55,109.307,53.595,30.736 -2020-04-16 18:45:00,116.31,114.927,53.595,30.736 -2020-04-16 19:00:00,115.59,113.14,54.207,30.736 -2020-04-16 19:15:00,112.76,112.333,54.207,30.736 -2020-04-16 19:30:00,111.41,112.33,54.207,30.736 -2020-04-16 19:45:00,109.87,112.74,54.207,30.736 -2020-04-16 20:00:00,110.54,110.89399999999999,56.948,30.736 -2020-04-16 20:15:00,108.79,108.249,56.948,30.736 -2020-04-16 20:30:00,106.12,107.178,56.948,30.736 -2020-04-16 20:45:00,100.7,106.319,56.948,30.736 -2020-04-16 21:00:00,100.63,102.01,52.157,30.736 -2020-04-16 21:15:00,99.93,101.508,52.157,30.736 -2020-04-16 21:30:00,96.78,101.87899999999999,52.157,30.736 -2020-04-16 21:45:00,91.01,101.338,52.157,30.736 -2020-04-16 22:00:00,88.24,96.85600000000001,47.483000000000004,30.736 -2020-04-16 22:15:00,90.64,94.991,47.483000000000004,30.736 -2020-04-16 22:30:00,87.26,83.77600000000001,47.483000000000004,30.736 -2020-04-16 22:45:00,83.96,78.098,47.483000000000004,30.736 -2020-04-16 23:00:00,79.47,70.275,41.978,30.736 -2020-04-16 23:15:00,82.32,68.947,41.978,30.736 -2020-04-16 23:30:00,82.51,68.09100000000001,41.978,30.736 -2020-04-16 23:45:00,79.47,68.702,41.978,30.736 -2020-04-17 00:00:00,75.69,63.902,39.301,30.736 -2020-04-17 00:15:00,80.52,64.638,39.301,30.736 -2020-04-17 00:30:00,79.11,63.152,39.301,30.736 -2020-04-17 00:45:00,74.97,61.931000000000004,39.301,30.736 -2020-04-17 01:00:00,76.44,61.839,37.976,30.736 -2020-04-17 01:15:00,78.63,61.181999999999995,37.976,30.736 -2020-04-17 01:30:00,78.54,60.246,37.976,30.736 -2020-04-17 01:45:00,75.92,59.578,37.976,30.736 -2020-04-17 02:00:00,76.67,61.106,37.041,30.736 -2020-04-17 02:15:00,78.96,60.225,37.041,30.736 -2020-04-17 02:30:00,73.71,63.532,37.041,30.736 -2020-04-17 02:45:00,72.47,63.641000000000005,37.041,30.736 -2020-04-17 03:00:00,72.74,66.71300000000001,37.575,30.736 -2020-04-17 03:15:00,77.67,67.558,37.575,30.736 -2020-04-17 03:30:00,81.93,66.829,37.575,30.736 -2020-04-17 03:45:00,82.98,67.883,37.575,30.736 -2020-04-17 04:00:00,85.63,79.98899999999999,39.058,30.736 -2020-04-17 04:15:00,81.63,91.12700000000001,39.058,30.736 -2020-04-17 04:30:00,87.46,91.634,39.058,30.736 -2020-04-17 04:45:00,88.26,92.454,39.058,30.736 -2020-04-17 05:00:00,94.18,124.83,43.256,30.736 -2020-04-17 05:15:00,97.98,157.922,43.256,30.736 -2020-04-17 05:30:00,101.19,147.47299999999998,43.256,30.736 -2020-04-17 05:45:00,104.3,136.907,43.256,30.736 -2020-04-17 06:00:00,109.57,138.972,56.093999999999994,30.736 -2020-04-17 06:15:00,110.1,143.366,56.093999999999994,30.736 -2020-04-17 06:30:00,111.6,140.194,56.093999999999994,30.736 -2020-04-17 06:45:00,112.04,141.218,56.093999999999994,30.736 -2020-04-17 07:00:00,113.63,143.342,66.92699999999999,30.736 -2020-04-17 07:15:00,113.48,144.96,66.92699999999999,30.736 -2020-04-17 07:30:00,114.3,141.72799999999998,66.92699999999999,30.736 -2020-04-17 07:45:00,111.17,138.004,66.92699999999999,30.736 -2020-04-17 08:00:00,109.06,135.482,60.332,30.736 -2020-04-17 08:15:00,108.75,134.251,60.332,30.736 -2020-04-17 08:30:00,108.96,130.61,60.332,30.736 -2020-04-17 08:45:00,111.11,127.825,60.332,30.736 -2020-04-17 09:00:00,107.04,120.291,56.085,30.736 -2020-04-17 09:15:00,107.43,119.624,56.085,30.736 -2020-04-17 09:30:00,106.4,120.985,56.085,30.736 -2020-04-17 09:45:00,106.21,120.639,56.085,30.736 -2020-04-17 10:00:00,104.57,115.45100000000001,52.91,30.736 -2020-04-17 10:15:00,105.96,115.67200000000001,52.91,30.736 -2020-04-17 10:30:00,107.27,114.46600000000001,52.91,30.736 -2020-04-17 10:45:00,103.6,114.225,52.91,30.736 -2020-04-17 11:00:00,104.04,109.054,52.278999999999996,30.736 -2020-04-17 11:15:00,101.25,108.65100000000001,52.278999999999996,30.736 -2020-04-17 11:30:00,100.24,110.637,52.278999999999996,30.736 -2020-04-17 11:45:00,100.36,110.69200000000001,52.278999999999996,30.736 -2020-04-17 12:00:00,98.31,106.641,49.023999999999994,30.736 -2020-04-17 12:15:00,98.3,105.836,49.023999999999994,30.736 -2020-04-17 12:30:00,98.9,105.109,49.023999999999994,30.736 -2020-04-17 12:45:00,96.92,105.68,49.023999999999994,30.736 -2020-04-17 13:00:00,95.35,107.199,46.82,30.736 -2020-04-17 13:15:00,94.15,107.081,46.82,30.736 -2020-04-17 13:30:00,95.45,105.429,46.82,30.736 -2020-04-17 13:45:00,95.7,104.15100000000001,46.82,30.736 -2020-04-17 14:00:00,96.68,104.079,45.756,30.736 -2020-04-17 14:15:00,94.93,103.727,45.756,30.736 -2020-04-17 14:30:00,95.22,104.76,45.756,30.736 -2020-04-17 14:45:00,96.43,104.99799999999999,45.756,30.736 -2020-04-17 15:00:00,98.56,105.598,47.56,30.736 -2020-04-17 15:15:00,96.36,103.66799999999999,47.56,30.736 -2020-04-17 15:30:00,98.86,101.962,47.56,30.736 -2020-04-17 15:45:00,99.49,102.11399999999999,47.56,30.736 -2020-04-17 16:00:00,100.98,100.829,49.581,30.736 -2020-04-17 16:15:00,102.21,101.12700000000001,49.581,30.736 -2020-04-17 16:30:00,103.54,101.053,49.581,30.736 -2020-04-17 16:45:00,105.31,98.324,49.581,30.736 -2020-04-17 17:00:00,109.87,99.141,53.918,30.736 -2020-04-17 17:15:00,105.24,101.18,53.918,30.736 -2020-04-17 17:30:00,108.79,102.488,53.918,30.736 -2020-04-17 17:45:00,108.09,103.699,53.918,30.736 -2020-04-17 18:00:00,109.81,104.992,54.266000000000005,30.736 -2020-04-17 18:15:00,106.31,105.728,54.266000000000005,30.736 -2020-04-17 18:30:00,105.75,104.70200000000001,54.266000000000005,30.736 -2020-04-17 18:45:00,105.56,110.596,54.266000000000005,30.736 -2020-04-17 19:00:00,104.01,109.889,54.092,30.736 -2020-04-17 19:15:00,101.85,110.20700000000001,54.092,30.736 -2020-04-17 19:30:00,103.45,110.035,54.092,30.736 -2020-04-17 19:45:00,104.61,109.553,54.092,30.736 -2020-04-17 20:00:00,101.81,107.604,59.038999999999994,30.736 -2020-04-17 20:15:00,104.72,105.49,59.038999999999994,30.736 -2020-04-17 20:30:00,104.95,104.11200000000001,59.038999999999994,30.736 -2020-04-17 20:45:00,104.9,103.052,59.038999999999994,30.736 -2020-04-17 21:00:00,92.54,99.882,53.346000000000004,30.736 -2020-04-17 21:15:00,89.07,100.73,53.346000000000004,30.736 -2020-04-17 21:30:00,86.36,101.012,53.346000000000004,30.736 -2020-04-17 21:45:00,91.79,100.928,53.346000000000004,30.736 -2020-04-17 22:00:00,87.88,96.88600000000001,47.938,30.736 -2020-04-17 22:15:00,87.19,94.811,47.938,30.736 -2020-04-17 22:30:00,79.25,90.098,47.938,30.736 -2020-04-17 22:45:00,78.07,86.851,47.938,30.736 -2020-04-17 23:00:00,78.74,79.878,40.266,30.736 -2020-04-17 23:15:00,79.25,76.51899999999999,40.266,30.736 -2020-04-17 23:30:00,79.44,73.683,40.266,30.736 -2020-04-17 23:45:00,72.93,73.869,40.266,30.736 -2020-04-18 00:00:00,72.24,62.902,39.184,30.618000000000002 -2020-04-18 00:15:00,74.92,61.023999999999994,39.184,30.618000000000002 -2020-04-18 00:30:00,73.65,59.887,39.184,30.618000000000002 -2020-04-18 00:45:00,71.07,58.537,39.184,30.618000000000002 -2020-04-18 01:00:00,64.33,58.955,34.692,30.618000000000002 -2020-04-18 01:15:00,65.67,58.178000000000004,34.692,30.618000000000002 -2020-04-18 01:30:00,64.17,56.406000000000006,34.692,30.618000000000002 -2020-04-18 01:45:00,68.34,56.482,34.692,30.618000000000002 -2020-04-18 02:00:00,71.5,57.685,32.919000000000004,30.618000000000002 -2020-04-18 02:15:00,72.43,56.059,32.919000000000004,30.618000000000002 -2020-04-18 02:30:00,69.43,58.281000000000006,32.919000000000004,30.618000000000002 -2020-04-18 02:45:00,69.21,58.986000000000004,32.919000000000004,30.618000000000002 -2020-04-18 03:00:00,72.51,61.458999999999996,32.024,30.618000000000002 -2020-04-18 03:15:00,68.08,61.188,32.024,30.618000000000002 -2020-04-18 03:30:00,66.16,59.88399999999999,32.024,30.618000000000002 -2020-04-18 03:45:00,64.76,62.033,32.024,30.618000000000002 -2020-04-18 04:00:00,66.57,70.758,31.958000000000002,30.618000000000002 -2020-04-18 04:15:00,67.33,79.971,31.958000000000002,30.618000000000002 -2020-04-18 04:30:00,66.23,78.243,31.958000000000002,30.618000000000002 -2020-04-18 04:45:00,64.3,78.995,31.958000000000002,30.618000000000002 -2020-04-18 05:00:00,64.93,97.959,32.75,30.618000000000002 -2020-04-18 05:15:00,66.66,114.07,32.75,30.618000000000002 -2020-04-18 05:30:00,67.03,104.89,32.75,30.618000000000002 -2020-04-18 05:45:00,70.54,100.15,32.75,30.618000000000002 -2020-04-18 06:00:00,71.71,118.912,34.461999999999996,30.618000000000002 -2020-04-18 06:15:00,73.46,136.754,34.461999999999996,30.618000000000002 -2020-04-18 06:30:00,75.72,128.946,34.461999999999996,30.618000000000002 -2020-04-18 06:45:00,76.77,123.31,34.461999999999996,30.618000000000002 -2020-04-18 07:00:00,78.94,122.14299999999999,37.736,30.618000000000002 -2020-04-18 07:15:00,78.58,122.23200000000001,37.736,30.618000000000002 -2020-04-18 07:30:00,79.45,121.365,37.736,30.618000000000002 -2020-04-18 07:45:00,80.73,120.3,37.736,30.618000000000002 -2020-04-18 08:00:00,80.41,120.022,42.34,30.618000000000002 -2020-04-18 08:15:00,80.62,120.61,42.34,30.618000000000002 -2020-04-18 08:30:00,79.58,117.786,42.34,30.618000000000002 -2020-04-18 08:45:00,79.67,117.322,42.34,30.618000000000002 -2020-04-18 09:00:00,77.81,112.633,43.571999999999996,30.618000000000002 -2020-04-18 09:15:00,76.73,112.691,43.571999999999996,30.618000000000002 -2020-04-18 09:30:00,75.68,114.87700000000001,43.571999999999996,30.618000000000002 -2020-04-18 09:45:00,75.15,114.365,43.571999999999996,30.618000000000002 -2020-04-18 10:00:00,74.84,109.54700000000001,40.514,30.618000000000002 -2020-04-18 10:15:00,76.04,110.12799999999999,40.514,30.618000000000002 -2020-04-18 10:30:00,76.37,108.82600000000001,40.514,30.618000000000002 -2020-04-18 10:45:00,75.02,109.126,40.514,30.618000000000002 -2020-04-18 11:00:00,73.78,103.92299999999999,36.388000000000005,30.618000000000002 -2020-04-18 11:15:00,72.94,103.676,36.388000000000005,30.618000000000002 -2020-04-18 11:30:00,70.92,105.191,36.388000000000005,30.618000000000002 -2020-04-18 11:45:00,69.83,105.12100000000001,36.388000000000005,30.618000000000002 -2020-04-18 12:00:00,67.19,100.64200000000001,35.217,30.618000000000002 -2020-04-18 12:15:00,67.12,100.725,35.217,30.618000000000002 -2020-04-18 12:30:00,65.58,100.066,35.217,30.618000000000002 -2020-04-18 12:45:00,65.28,100.61399999999999,35.217,30.618000000000002 -2020-04-18 13:00:00,61.25,101.42399999999999,32.001999999999995,30.618000000000002 -2020-04-18 13:15:00,62.76,99.764,32.001999999999995,30.618000000000002 -2020-04-18 13:30:00,62.74,97.959,32.001999999999995,30.618000000000002 -2020-04-18 13:45:00,62.56,96.169,32.001999999999995,30.618000000000002 -2020-04-18 14:00:00,63.31,96.9,31.304000000000002,30.618000000000002 -2020-04-18 14:15:00,63.14,95.477,31.304000000000002,30.618000000000002 -2020-04-18 14:30:00,63.14,95.135,31.304000000000002,30.618000000000002 -2020-04-18 14:45:00,64.8,95.787,31.304000000000002,30.618000000000002 -2020-04-18 15:00:00,65.13,97.071,34.731,30.618000000000002 -2020-04-18 15:15:00,65.05,96.014,34.731,30.618000000000002 -2020-04-18 15:30:00,66.87,95.29700000000001,34.731,30.618000000000002 -2020-04-18 15:45:00,69.39,94.9,34.731,30.618000000000002 -2020-04-18 16:00:00,71.14,94.525,38.769,30.618000000000002 -2020-04-18 16:15:00,72.55,94.76299999999999,38.769,30.618000000000002 -2020-04-18 16:30:00,74.77,94.803,38.769,30.618000000000002 -2020-04-18 16:45:00,76.84,92.49600000000001,38.769,30.618000000000002 -2020-04-18 17:00:00,79.92,92.44,44.928000000000004,30.618000000000002 -2020-04-18 17:15:00,79.83,94.03399999999999,44.928000000000004,30.618000000000002 -2020-04-18 17:30:00,81.4,95.20200000000001,44.928000000000004,30.618000000000002 -2020-04-18 17:45:00,81.53,96.46799999999999,44.928000000000004,30.618000000000002 -2020-04-18 18:00:00,85.84,98.512,47.786,30.618000000000002 -2020-04-18 18:15:00,83.87,101.316,47.786,30.618000000000002 -2020-04-18 18:30:00,81.87,101.869,47.786,30.618000000000002 -2020-04-18 18:45:00,81.6,103.727,47.786,30.618000000000002 -2020-04-18 19:00:00,82.11,102.561,47.463,30.618000000000002 -2020-04-18 19:15:00,80.38,101.963,47.463,30.618000000000002 -2020-04-18 19:30:00,82.61,102.70299999999999,47.463,30.618000000000002 -2020-04-18 19:45:00,81.66,103.302,47.463,30.618000000000002 -2020-04-18 20:00:00,78.44,103.012,43.735,30.618000000000002 -2020-04-18 20:15:00,77.86,101.625,43.735,30.618000000000002 -2020-04-18 20:30:00,76.06,99.522,43.735,30.618000000000002 -2020-04-18 20:45:00,75.04,99.48700000000001,43.735,30.618000000000002 -2020-04-18 21:00:00,69.71,96.571,40.346,30.618000000000002 -2020-04-18 21:15:00,69.85,97.436,40.346,30.618000000000002 -2020-04-18 21:30:00,66.1,98.477,40.346,30.618000000000002 -2020-04-18 21:45:00,66.61,97.851,40.346,30.618000000000002 -2020-04-18 22:00:00,63.91,94.52,39.323,30.618000000000002 -2020-04-18 22:15:00,63.83,93.944,39.323,30.618000000000002 -2020-04-18 22:30:00,61.62,92.242,39.323,30.618000000000002 -2020-04-18 22:45:00,61.98,90.219,39.323,30.618000000000002 -2020-04-18 23:00:00,56.73,84.055,33.716,30.618000000000002 -2020-04-18 23:15:00,56.39,80.14699999999999,33.716,30.618000000000002 -2020-04-18 23:30:00,56.57,78.0,33.716,30.618000000000002 -2020-04-18 23:45:00,57.7,77.148,33.716,30.618000000000002 -2020-04-19 00:00:00,47.91,63.659,28.703000000000003,30.618000000000002 -2020-04-19 00:15:00,49.58,60.85,28.703000000000003,30.618000000000002 -2020-04-19 00:30:00,48.9,59.416000000000004,28.703000000000003,30.618000000000002 -2020-04-19 00:45:00,51.89,58.347,28.703000000000003,30.618000000000002 -2020-04-19 01:00:00,48.88,58.867,26.171,30.618000000000002 -2020-04-19 01:15:00,49.69,58.504,26.171,30.618000000000002 -2020-04-19 01:30:00,47.4,56.879,26.171,30.618000000000002 -2020-04-19 01:45:00,50.6,56.542,26.171,30.618000000000002 -2020-04-19 02:00:00,48.49,57.409,25.326999999999998,30.618000000000002 -2020-04-19 02:15:00,49.54,55.821000000000005,25.326999999999998,30.618000000000002 -2020-04-19 02:30:00,49.42,58.62,25.326999999999998,30.618000000000002 -2020-04-19 02:45:00,49.28,59.363,25.326999999999998,30.618000000000002 -2020-04-19 03:00:00,49.27,62.45399999999999,24.311999999999998,30.618000000000002 -2020-04-19 03:15:00,50.12,62.077,24.311999999999998,30.618000000000002 -2020-04-19 03:30:00,50.56,60.961999999999996,24.311999999999998,30.618000000000002 -2020-04-19 03:45:00,51.01,62.55,24.311999999999998,30.618000000000002 -2020-04-19 04:00:00,52.33,71.09100000000001,25.33,30.618000000000002 -2020-04-19 04:15:00,53.42,79.464,25.33,30.618000000000002 -2020-04-19 04:30:00,54.02,78.689,25.33,30.618000000000002 -2020-04-19 04:45:00,52.87,79.245,25.33,30.618000000000002 -2020-04-19 05:00:00,53.79,96.527,25.309,30.618000000000002 -2020-04-19 05:15:00,55.01,110.838,25.309,30.618000000000002 -2020-04-19 05:30:00,54.77,101.319,25.309,30.618000000000002 -2020-04-19 05:45:00,56.87,96.553,25.309,30.618000000000002 -2020-04-19 06:00:00,58.42,113.615,25.945999999999998,30.618000000000002 -2020-04-19 06:15:00,58.63,131.168,25.945999999999998,30.618000000000002 -2020-04-19 06:30:00,59.65,122.322,25.945999999999998,30.618000000000002 -2020-04-19 06:45:00,61.3,115.475,25.945999999999998,30.618000000000002 -2020-04-19 07:00:00,62.91,115.682,27.87,30.618000000000002 -2020-04-19 07:15:00,63.9,114.219,27.87,30.618000000000002 -2020-04-19 07:30:00,64.4,113.54899999999999,27.87,30.618000000000002 -2020-04-19 07:45:00,63.96,112.075,27.87,30.618000000000002 -2020-04-19 08:00:00,62.93,113.177,32.114000000000004,30.618000000000002 -2020-04-19 08:15:00,62.87,114.525,32.114000000000004,30.618000000000002 -2020-04-19 08:30:00,62.0,113.11399999999999,32.114000000000004,30.618000000000002 -2020-04-19 08:45:00,61.46,113.685,32.114000000000004,30.618000000000002 -2020-04-19 09:00:00,60.12,108.691,34.222,30.618000000000002 -2020-04-19 09:15:00,60.81,108.734,34.222,30.618000000000002 -2020-04-19 09:30:00,59.84,111.131,34.222,30.618000000000002 -2020-04-19 09:45:00,60.52,111.23299999999999,34.222,30.618000000000002 -2020-04-19 10:00:00,60.88,108.11,34.544000000000004,30.618000000000002 -2020-04-19 10:15:00,63.47,109.119,34.544000000000004,30.618000000000002 -2020-04-19 10:30:00,64.28,108.31299999999999,34.544000000000004,30.618000000000002 -2020-04-19 10:45:00,64.13,108.21,34.544000000000004,30.618000000000002 -2020-04-19 11:00:00,60.96,103.27799999999999,36.368,30.618000000000002 -2020-04-19 11:15:00,60.33,102.855,36.368,30.618000000000002 -2020-04-19 11:30:00,57.6,104.197,36.368,30.618000000000002 -2020-04-19 11:45:00,56.64,104.645,36.368,30.618000000000002 -2020-04-19 12:00:00,54.74,100.47,32.433,30.618000000000002 -2020-04-19 12:15:00,53.47,101.291,32.433,30.618000000000002 -2020-04-19 12:30:00,53.55,99.87299999999999,32.433,30.618000000000002 -2020-04-19 12:45:00,53.72,99.48299999999999,32.433,30.618000000000002 -2020-04-19 13:00:00,52.34,99.73100000000001,28.971999999999998,30.618000000000002 -2020-04-19 13:15:00,52.36,99.473,28.971999999999998,30.618000000000002 -2020-04-19 13:30:00,52.22,96.885,28.971999999999998,30.618000000000002 -2020-04-19 13:45:00,53.3,95.381,28.971999999999998,30.618000000000002 -2020-04-19 14:00:00,52.13,97.029,25.531999999999996,30.618000000000002 -2020-04-19 14:15:00,52.55,96.542,25.531999999999996,30.618000000000002 -2020-04-19 14:30:00,52.58,96.13799999999999,25.531999999999996,30.618000000000002 -2020-04-19 14:45:00,53.01,95.916,25.531999999999996,30.618000000000002 -2020-04-19 15:00:00,55.7,96.435,25.766,30.618000000000002 -2020-04-19 15:15:00,59.0,95.31299999999999,25.766,30.618000000000002 -2020-04-19 15:30:00,60.62,94.788,25.766,30.618000000000002 -2020-04-19 15:45:00,65.31,94.945,25.766,30.618000000000002 -2020-04-19 16:00:00,66.6,94.571,29.232,30.618000000000002 -2020-04-19 16:15:00,66.2,94.41799999999999,29.232,30.618000000000002 -2020-04-19 16:30:00,70.78,95.229,29.232,30.618000000000002 -2020-04-19 16:45:00,71.98,92.97,29.232,30.618000000000002 -2020-04-19 17:00:00,74.6,93.13,37.431,30.618000000000002 -2020-04-19 17:15:00,75.63,95.47,37.431,30.618000000000002 -2020-04-19 17:30:00,77.0,97.29899999999999,37.431,30.618000000000002 -2020-04-19 17:45:00,78.99,100.04899999999999,37.431,30.618000000000002 -2020-04-19 18:00:00,82.25,102.135,41.251999999999995,30.618000000000002 -2020-04-19 18:15:00,79.87,105.406,41.251999999999995,30.618000000000002 -2020-04-19 18:30:00,79.78,104.67200000000001,41.251999999999995,30.618000000000002 -2020-04-19 18:45:00,79.99,107.59899999999999,41.251999999999995,30.618000000000002 -2020-04-19 19:00:00,81.9,107.616,41.784,30.618000000000002 -2020-04-19 19:15:00,82.8,106.60600000000001,41.784,30.618000000000002 -2020-04-19 19:30:00,87.07,107.10799999999999,41.784,30.618000000000002 -2020-04-19 19:45:00,85.32,108.184,41.784,30.618000000000002 -2020-04-19 20:00:00,87.59,107.975,40.804,30.618000000000002 -2020-04-19 20:15:00,90.05,107.021,40.804,30.618000000000002 -2020-04-19 20:30:00,90.49,106.132,40.804,30.618000000000002 -2020-04-19 20:45:00,89.62,104.4,40.804,30.618000000000002 -2020-04-19 21:00:00,81.38,100.014,38.379,30.618000000000002 -2020-04-19 21:15:00,84.66,100.369,38.379,30.618000000000002 -2020-04-19 21:30:00,77.57,101.12200000000001,38.379,30.618000000000002 -2020-04-19 21:45:00,78.05,100.834,38.379,30.618000000000002 -2020-04-19 22:00:00,77.86,98.241,37.87,30.618000000000002 -2020-04-19 22:15:00,82.24,96.18799999999999,37.87,30.618000000000002 -2020-04-19 22:30:00,80.13,92.486,37.87,30.618000000000002 -2020-04-19 22:45:00,76.14,89.17200000000001,37.87,30.618000000000002 -2020-04-19 23:00:00,67.84,81.33,33.332,30.618000000000002 -2020-04-19 23:15:00,72.28,79.26100000000001,33.332,30.618000000000002 -2020-04-19 23:30:00,75.58,77.17699999999999,33.332,30.618000000000002 -2020-04-19 23:45:00,73.37,76.899,33.332,30.618000000000002 -2020-04-20 00:00:00,69.74,66.392,34.698,30.736 -2020-04-20 00:15:00,72.46,65.615,34.698,30.736 -2020-04-20 00:30:00,70.41,63.977,34.698,30.736 -2020-04-20 00:45:00,69.96,62.376000000000005,34.698,30.736 -2020-04-20 01:00:00,66.93,63.148,32.889,30.736 -2020-04-20 01:15:00,72.98,62.507,32.889,30.736 -2020-04-20 01:30:00,73.29,61.136,32.889,30.736 -2020-04-20 01:45:00,71.28,60.791000000000004,32.889,30.736 -2020-04-20 02:00:00,68.66,61.93899999999999,32.06,30.736 -2020-04-20 02:15:00,73.7,60.363,32.06,30.736 -2020-04-20 02:30:00,73.14,63.458999999999996,32.06,30.736 -2020-04-20 02:45:00,73.33,63.801,32.06,30.736 -2020-04-20 03:00:00,67.47,67.866,30.515,30.736 -2020-04-20 03:15:00,68.31,68.77,30.515,30.736 -2020-04-20 03:30:00,68.12,67.986,30.515,30.736 -2020-04-20 03:45:00,69.11,69.008,30.515,30.736 -2020-04-20 04:00:00,74.41,81.767,31.436,30.736 -2020-04-20 04:15:00,77.05,94.147,31.436,30.736 -2020-04-20 04:30:00,80.21,94.115,31.436,30.736 -2020-04-20 04:45:00,83.85,94.995,31.436,30.736 -2020-04-20 05:00:00,92.02,124.523,38.997,30.736 -2020-04-20 05:15:00,94.95,155.561,38.997,30.736 -2020-04-20 05:30:00,97.01,145.115,38.997,30.736 -2020-04-20 05:45:00,100.5,135.55100000000002,38.997,30.736 -2020-04-20 06:00:00,105.42,137.136,54.97,30.736 -2020-04-20 06:15:00,107.37,141.161,54.97,30.736 -2020-04-20 06:30:00,110.05,138.94799999999998,54.97,30.736 -2020-04-20 06:45:00,111.02,140.03,54.97,30.736 -2020-04-20 07:00:00,115.16,142.121,66.032,30.736 -2020-04-20 07:15:00,112.2,142.808,66.032,30.736 -2020-04-20 07:30:00,115.55,141.128,66.032,30.736 -2020-04-20 07:45:00,114.85,138.559,66.032,30.736 -2020-04-20 08:00:00,113.82,136.049,59.941,30.736 -2020-04-20 08:15:00,117.57,135.525,59.941,30.736 -2020-04-20 08:30:00,120.95,131.31799999999998,59.941,30.736 -2020-04-20 08:45:00,120.25,130.308,59.941,30.736 -2020-04-20 09:00:00,121.46,124.26299999999999,54.016000000000005,30.736 -2020-04-20 09:15:00,120.31,121.29700000000001,54.016000000000005,30.736 -2020-04-20 09:30:00,119.35,122.581,54.016000000000005,30.736 -2020-04-20 09:45:00,116.25,121.405,54.016000000000005,30.736 -2020-04-20 10:00:00,114.59,118.214,50.63,30.736 -2020-04-20 10:15:00,118.18,118.99600000000001,50.63,30.736 -2020-04-20 10:30:00,111.25,117.441,50.63,30.736 -2020-04-20 10:45:00,112.04,116.792,50.63,30.736 -2020-04-20 11:00:00,118.8,110.635,49.951,30.736 -2020-04-20 11:15:00,119.3,111.428,49.951,30.736 -2020-04-20 11:30:00,123.96,113.979,49.951,30.736 -2020-04-20 11:45:00,126.42,114.51899999999999,49.951,30.736 -2020-04-20 12:00:00,124.57,110.541,46.913000000000004,30.736 -2020-04-20 12:15:00,122.52,111.43799999999999,46.913000000000004,30.736 -2020-04-20 12:30:00,115.69,109.486,46.913000000000004,30.736 -2020-04-20 12:45:00,118.2,109.985,46.913000000000004,30.736 -2020-04-20 13:00:00,113.18,111.163,47.093999999999994,30.736 -2020-04-20 13:15:00,112.18,109.56700000000001,47.093999999999994,30.736 -2020-04-20 13:30:00,112.03,106.774,47.093999999999994,30.736 -2020-04-20 13:45:00,106.19,105.821,47.093999999999994,30.736 -2020-04-20 14:00:00,99.72,106.662,46.678000000000004,30.736 -2020-04-20 14:15:00,107.85,106.13,46.678000000000004,30.736 -2020-04-20 14:30:00,105.99,105.26899999999999,46.678000000000004,30.736 -2020-04-20 14:45:00,95.87,106.25399999999999,46.678000000000004,30.736 -2020-04-20 15:00:00,100.13,107.649,47.715,30.736 -2020-04-20 15:15:00,102.74,105.315,47.715,30.736 -2020-04-20 15:30:00,106.75,104.714,47.715,30.736 -2020-04-20 15:45:00,109.06,104.306,47.715,30.736 -2020-04-20 16:00:00,111.0,104.484,49.81100000000001,30.736 -2020-04-20 16:15:00,110.49,103.917,49.81100000000001,30.736 -2020-04-20 16:30:00,109.82,103.781,49.81100000000001,30.736 -2020-04-20 16:45:00,113.31,100.8,49.81100000000001,30.736 -2020-04-20 17:00:00,114.39,100.116,55.591,30.736 -2020-04-20 17:15:00,113.67,102.141,55.591,30.736 -2020-04-20 17:30:00,113.09,103.419,55.591,30.736 -2020-04-20 17:45:00,114.97,105.07,55.591,30.736 -2020-04-20 18:00:00,116.55,106.62,56.523,30.736 -2020-04-20 18:15:00,113.53,107.551,56.523,30.736 -2020-04-20 18:30:00,111.92,106.67,56.523,30.736 -2020-04-20 18:45:00,111.5,112.01299999999999,56.523,30.736 -2020-04-20 19:00:00,109.89,110.913,56.044,30.736 -2020-04-20 19:15:00,115.06,110.16,56.044,30.736 -2020-04-20 19:30:00,113.63,110.69200000000001,56.044,30.736 -2020-04-20 19:45:00,112.78,110.954,56.044,30.736 -2020-04-20 20:00:00,101.75,108.655,61.715,30.736 -2020-04-20 20:15:00,100.7,107.319,61.715,30.736 -2020-04-20 20:30:00,100.68,105.82600000000001,61.715,30.736 -2020-04-20 20:45:00,100.06,105.12200000000001,61.715,30.736 -2020-04-20 21:00:00,99.18,100.616,56.24,30.736 -2020-04-20 21:15:00,98.71,100.664,56.24,30.736 -2020-04-20 21:30:00,94.69,101.24700000000001,56.24,30.736 -2020-04-20 21:45:00,91.68,100.58200000000001,56.24,30.736 -2020-04-20 22:00:00,88.84,95.15299999999999,50.437,30.736 -2020-04-20 22:15:00,88.96,93.74600000000001,50.437,30.736 -2020-04-20 22:30:00,84.92,82.199,50.437,30.736 -2020-04-20 22:45:00,85.41,76.347,50.437,30.736 -2020-04-20 23:00:00,81.6,68.904,42.756,30.736 -2020-04-20 23:15:00,81.0,67.168,42.756,30.736 -2020-04-20 23:30:00,80.1,66.495,42.756,30.736 -2020-04-20 23:45:00,81.46,67.245,42.756,30.736 -2020-04-21 00:00:00,77.51,64.109,39.857,30.736 -2020-04-21 00:15:00,75.11,64.649,39.857,30.736 -2020-04-21 00:30:00,73.12,63.016999999999996,39.857,30.736 -2020-04-21 00:45:00,78.89,61.471000000000004,39.857,30.736 -2020-04-21 01:00:00,78.67,61.772,37.233000000000004,30.736 -2020-04-21 01:15:00,77.8,60.961000000000006,37.233000000000004,30.736 -2020-04-21 01:30:00,72.87,59.566,37.233000000000004,30.736 -2020-04-21 01:45:00,74.1,59.026,37.233000000000004,30.736 -2020-04-21 02:00:00,78.95,59.831,35.856,30.736 -2020-04-21 02:15:00,78.47,58.976000000000006,35.856,30.736 -2020-04-21 02:30:00,72.53,61.526,35.856,30.736 -2020-04-21 02:45:00,71.94,62.106,35.856,30.736 -2020-04-21 03:00:00,74.86,65.212,34.766999999999996,30.736 -2020-04-21 03:15:00,80.59,66.313,34.766999999999996,30.736 -2020-04-21 03:30:00,83.48,65.743,34.766999999999996,30.736 -2020-04-21 03:45:00,83.58,66.119,34.766999999999996,30.736 -2020-04-21 04:00:00,79.94,77.88,35.468,30.736 -2020-04-21 04:15:00,82.73,90.056,35.468,30.736 -2020-04-21 04:30:00,86.26,89.789,35.468,30.736 -2020-04-21 04:45:00,87.7,91.59100000000001,35.468,30.736 -2020-04-21 05:00:00,95.23,124.53200000000001,40.399,30.736 -2020-04-21 05:15:00,98.1,155.803,40.399,30.736 -2020-04-21 05:30:00,100.01,144.778,40.399,30.736 -2020-04-21 05:45:00,102.09,134.755,40.399,30.736 -2020-04-21 06:00:00,107.66,136.52100000000002,54.105,30.736 -2020-04-21 06:15:00,108.52,141.458,54.105,30.736 -2020-04-21 06:30:00,110.71,138.69,54.105,30.736 -2020-04-21 06:45:00,111.29,138.968,54.105,30.736 -2020-04-21 07:00:00,110.64,141.067,63.083,30.736 -2020-04-21 07:15:00,112.61,141.486,63.083,30.736 -2020-04-21 07:30:00,117.23,139.503,63.083,30.736 -2020-04-21 07:45:00,111.61,136.388,63.083,30.736 -2020-04-21 08:00:00,109.22,133.88299999999998,57.254,30.736 -2020-04-21 08:15:00,106.0,132.562,57.254,30.736 -2020-04-21 08:30:00,108.93,128.343,57.254,30.736 -2020-04-21 08:45:00,111.02,126.616,57.254,30.736 -2020-04-21 09:00:00,108.14,120.348,51.395,30.736 -2020-04-21 09:15:00,111.76,118.16,51.395,30.736 -2020-04-21 09:30:00,112.64,120.26700000000001,51.395,30.736 -2020-04-21 09:45:00,108.02,119.867,51.395,30.736 -2020-04-21 10:00:00,106.08,115.491,48.201,30.736 -2020-04-21 10:15:00,104.55,115.639,48.201,30.736 -2020-04-21 10:30:00,104.69,114.223,48.201,30.736 -2020-04-21 10:45:00,104.67,114.375,48.201,30.736 -2020-04-21 11:00:00,103.29,109.04799999999999,46.133,30.736 -2020-04-21 11:15:00,102.35,109.921,46.133,30.736 -2020-04-21 11:30:00,98.0,111.10700000000001,46.133,30.736 -2020-04-21 11:45:00,100.05,111.792,46.133,30.736 -2020-04-21 12:00:00,95.93,106.93700000000001,44.243,30.736 -2020-04-21 12:15:00,104.04,107.79700000000001,44.243,30.736 -2020-04-21 12:30:00,105.24,106.734,44.243,30.736 -2020-04-21 12:45:00,107.04,107.476,44.243,30.736 -2020-04-21 13:00:00,100.69,108.23700000000001,45.042,30.736 -2020-04-21 13:15:00,99.99,107.499,45.042,30.736 -2020-04-21 13:30:00,104.95,105.37799999999999,45.042,30.736 -2020-04-21 13:45:00,111.42,103.964,45.042,30.736 -2020-04-21 14:00:00,114.25,105.295,44.062,30.736 -2020-04-21 14:15:00,112.57,104.719,44.062,30.736 -2020-04-21 14:30:00,109.07,104.374,44.062,30.736 -2020-04-21 14:45:00,106.59,104.85,44.062,30.736 -2020-04-21 15:00:00,113.59,105.935,46.461999999999996,30.736 -2020-04-21 15:15:00,116.36,104.32,46.461999999999996,30.736 -2020-04-21 15:30:00,115.99,103.68299999999999,46.461999999999996,30.736 -2020-04-21 15:45:00,113.69,103.225,46.461999999999996,30.736 -2020-04-21 16:00:00,109.01,103.26299999999999,48.802,30.736 -2020-04-21 16:15:00,111.19,102.992,48.802,30.736 -2020-04-21 16:30:00,117.84,102.99799999999999,48.802,30.736 -2020-04-21 16:45:00,116.61,100.555,48.802,30.736 -2020-04-21 17:00:00,119.91,100.361,55.672,30.736 -2020-04-21 17:15:00,117.41,102.63799999999999,55.672,30.736 -2020-04-21 17:30:00,119.66,104.00299999999999,55.672,30.736 -2020-04-21 17:45:00,120.44,105.34700000000001,55.672,30.736 -2020-04-21 18:00:00,118.14,106.23299999999999,57.006,30.736 -2020-04-21 18:15:00,114.94,107.87,57.006,30.736 -2020-04-21 18:30:00,117.32,106.64200000000001,57.006,30.736 -2020-04-21 18:45:00,117.13,112.29299999999999,57.006,30.736 -2020-04-21 19:00:00,110.96,110.45100000000001,57.148,30.736 -2020-04-21 19:15:00,108.42,109.675,57.148,30.736 -2020-04-21 19:30:00,113.5,109.726,57.148,30.736 -2020-04-21 19:45:00,116.0,110.249,57.148,30.736 -2020-04-21 20:00:00,107.84,108.26299999999999,61.895,30.736 -2020-04-21 20:15:00,108.53,105.655,61.895,30.736 -2020-04-21 20:30:00,108.46,104.756,61.895,30.736 -2020-04-21 20:45:00,106.57,104.056,61.895,30.736 -2020-04-21 21:00:00,95.96,99.756,54.78,30.736 -2020-04-21 21:15:00,98.48,99.322,54.78,30.736 -2020-04-21 21:30:00,95.81,99.62700000000001,54.78,30.736 -2020-04-21 21:45:00,93.29,99.24,54.78,30.736 -2020-04-21 22:00:00,84.54,94.82799999999999,50.76,30.736 -2020-04-21 22:15:00,88.53,93.089,50.76,30.736 -2020-04-21 22:30:00,86.77,81.759,50.76,30.736 -2020-04-21 22:45:00,86.04,76.04,50.76,30.736 -2020-04-21 23:00:00,75.63,68.074,44.162,30.736 -2020-04-21 23:15:00,72.93,66.936,44.162,30.736 -2020-04-21 23:30:00,77.07,66.072,44.162,30.736 -2020-04-21 23:45:00,80.79,66.721,44.162,30.736 -2020-04-22 00:00:00,77.42,63.72,39.061,30.736 -2020-04-22 00:15:00,75.61,64.27199999999999,39.061,30.736 -2020-04-22 00:30:00,73.58,62.63399999999999,39.061,30.736 -2020-04-22 00:45:00,72.45,61.093,39.061,30.736 -2020-04-22 01:00:00,69.55,61.388000000000005,35.795,30.736 -2020-04-22 01:15:00,76.2,60.552,35.795,30.736 -2020-04-22 01:30:00,70.64,59.136,35.795,30.736 -2020-04-22 01:45:00,71.82,58.599,35.795,30.736 -2020-04-22 02:00:00,71.59,59.394,33.316,30.736 -2020-04-22 02:15:00,78.92,58.523,33.316,30.736 -2020-04-22 02:30:00,78.64,61.091,33.316,30.736 -2020-04-22 02:45:00,78.89,61.676,33.316,30.736 -2020-04-22 03:00:00,80.34,64.79899999999999,32.803000000000004,30.736 -2020-04-22 03:15:00,75.97,65.874,32.803000000000004,30.736 -2020-04-22 03:30:00,72.33,65.30199999999999,32.803000000000004,30.736 -2020-04-22 03:45:00,75.91,65.7,32.803000000000004,30.736 -2020-04-22 04:00:00,78.96,77.42699999999999,34.235,30.736 -2020-04-22 04:15:00,78.93,89.56200000000001,34.235,30.736 -2020-04-22 04:30:00,83.97,89.294,34.235,30.736 -2020-04-22 04:45:00,84.72,91.086,34.235,30.736 -2020-04-22 05:00:00,92.29,123.926,38.65,30.736 -2020-04-22 05:15:00,96.41,155.092,38.65,30.736 -2020-04-22 05:30:00,96.57,144.094,38.65,30.736 -2020-04-22 05:45:00,103.85,134.12,38.65,30.736 -2020-04-22 06:00:00,107.64,135.908,54.951,30.736 -2020-04-22 06:15:00,108.62,140.82299999999998,54.951,30.736 -2020-04-22 06:30:00,111.0,138.041,54.951,30.736 -2020-04-22 06:45:00,110.88,138.314,54.951,30.736 -2020-04-22 07:00:00,112.83,140.412,67.328,30.736 -2020-04-22 07:15:00,112.0,140.819,67.328,30.736 -2020-04-22 07:30:00,110.25,138.798,67.328,30.736 -2020-04-22 07:45:00,108.56,135.684,67.328,30.736 -2020-04-22 08:00:00,105.32,133.16899999999998,60.23,30.736 -2020-04-22 08:15:00,105.49,131.882,60.23,30.736 -2020-04-22 08:30:00,110.55,127.635,60.23,30.736 -2020-04-22 08:45:00,113.42,125.93700000000001,60.23,30.736 -2020-04-22 09:00:00,107.8,119.671,56.845,30.736 -2020-04-22 09:15:00,107.23,117.49,56.845,30.736 -2020-04-22 09:30:00,106.86,119.613,56.845,30.736 -2020-04-22 09:45:00,108.88,119.24600000000001,56.845,30.736 -2020-04-22 10:00:00,117.52,114.87799999999999,53.832,30.736 -2020-04-22 10:15:00,121.69,115.073,53.832,30.736 -2020-04-22 10:30:00,118.62,113.68,53.832,30.736 -2020-04-22 10:45:00,118.16,113.852,53.832,30.736 -2020-04-22 11:00:00,108.04,108.51700000000001,53.225,30.736 -2020-04-22 11:15:00,106.35,109.412,53.225,30.736 -2020-04-22 11:30:00,113.65,110.598,53.225,30.736 -2020-04-22 11:45:00,119.82,111.302,53.225,30.736 -2020-04-22 12:00:00,111.54,106.476,50.676,30.736 -2020-04-22 12:15:00,112.94,107.34299999999999,50.676,30.736 -2020-04-22 12:30:00,106.75,106.238,50.676,30.736 -2020-04-22 12:45:00,107.24,106.98299999999999,50.676,30.736 -2020-04-22 13:00:00,106.99,107.78200000000001,50.646,30.736 -2020-04-22 13:15:00,103.94,107.039,50.646,30.736 -2020-04-22 13:30:00,111.47,104.92,50.646,30.736 -2020-04-22 13:45:00,121.34,103.508,50.646,30.736 -2020-04-22 14:00:00,117.59,104.902,50.786,30.736 -2020-04-22 14:15:00,115.99,104.306,50.786,30.736 -2020-04-22 14:30:00,118.6,103.917,50.786,30.736 -2020-04-22 14:45:00,123.1,104.39399999999999,50.786,30.736 -2020-04-22 15:00:00,122.69,105.51799999999999,51.535,30.736 -2020-04-22 15:15:00,113.87,103.881,51.535,30.736 -2020-04-22 15:30:00,107.0,103.198,51.535,30.736 -2020-04-22 15:45:00,105.01,102.72399999999999,51.535,30.736 -2020-04-22 16:00:00,115.45,102.803,53.157,30.736 -2020-04-22 16:15:00,119.01,102.509,53.157,30.736 -2020-04-22 16:30:00,121.41,102.52,53.157,30.736 -2020-04-22 16:45:00,118.86,100.01100000000001,53.157,30.736 -2020-04-22 17:00:00,126.78,99.868,57.793,30.736 -2020-04-22 17:15:00,124.76,102.12,57.793,30.736 -2020-04-22 17:30:00,124.73,103.48,57.793,30.736 -2020-04-22 17:45:00,117.96,104.79799999999999,57.793,30.736 -2020-04-22 18:00:00,119.79,105.698,59.872,30.736 -2020-04-22 18:15:00,119.56,107.354,59.872,30.736 -2020-04-22 18:30:00,118.02,106.11200000000001,59.872,30.736 -2020-04-22 18:45:00,114.89,111.771,59.872,30.736 -2020-04-22 19:00:00,117.2,109.917,60.17100000000001,30.736 -2020-04-22 19:15:00,115.46,109.146,60.17100000000001,30.736 -2020-04-22 19:30:00,111.46,109.208,60.17100000000001,30.736 -2020-04-22 19:45:00,114.24,109.75299999999999,60.17100000000001,30.736 -2020-04-22 20:00:00,110.05,107.742,65.015,30.736 -2020-04-22 20:15:00,112.02,105.141,65.015,30.736 -2020-04-22 20:30:00,103.59,104.27600000000001,65.015,30.736 -2020-04-22 20:45:00,101.23,103.60600000000001,65.015,30.736 -2020-04-22 21:00:00,101.65,99.30799999999999,57.805,30.736 -2020-04-22 21:15:00,100.48,98.88799999999999,57.805,30.736 -2020-04-22 21:30:00,91.45,99.18,57.805,30.736 -2020-04-22 21:45:00,90.16,98.823,57.805,30.736 -2020-04-22 22:00:00,83.23,94.425,52.115,30.736 -2020-04-22 22:15:00,86.17,92.711,52.115,30.736 -2020-04-22 22:30:00,88.07,81.357,52.115,30.736 -2020-04-22 22:45:00,85.72,75.62899999999999,52.115,30.736 -2020-04-22 23:00:00,77.3,67.635,42.871,30.736 -2020-04-22 23:15:00,76.06,66.535,42.871,30.736 -2020-04-22 23:30:00,81.31,65.67,42.871,30.736 -2020-04-22 23:45:00,81.61,66.325,42.871,30.736 -2020-04-23 00:00:00,77.46,63.331,39.203,30.736 -2020-04-23 00:15:00,73.8,63.897,39.203,30.736 -2020-04-23 00:30:00,72.13,62.251000000000005,39.203,30.736 -2020-04-23 00:45:00,78.28,60.717,39.203,30.736 -2020-04-23 01:00:00,77.24,61.004,37.118,30.736 -2020-04-23 01:15:00,76.9,60.145,37.118,30.736 -2020-04-23 01:30:00,73.05,58.708,37.118,30.736 -2020-04-23 01:45:00,74.2,58.173,37.118,30.736 -2020-04-23 02:00:00,77.66,58.958,35.647,30.736 -2020-04-23 02:15:00,74.79,58.07,35.647,30.736 -2020-04-23 02:30:00,73.24,60.656000000000006,35.647,30.736 -2020-04-23 02:45:00,74.37,61.248000000000005,35.647,30.736 -2020-04-23 03:00:00,74.42,64.38600000000001,34.585,30.736 -2020-04-23 03:15:00,83.44,65.436,34.585,30.736 -2020-04-23 03:30:00,82.78,64.861,34.585,30.736 -2020-04-23 03:45:00,85.04,65.282,34.585,30.736 -2020-04-23 04:00:00,81.36,76.975,36.184,30.736 -2020-04-23 04:15:00,83.4,89.069,36.184,30.736 -2020-04-23 04:30:00,86.65,88.79899999999999,36.184,30.736 -2020-04-23 04:45:00,90.42,90.58200000000001,36.184,30.736 -2020-04-23 05:00:00,96.73,123.321,41.019,30.736 -2020-04-23 05:15:00,99.72,154.384,41.019,30.736 -2020-04-23 05:30:00,104.3,143.409,41.019,30.736 -2020-04-23 05:45:00,108.09,133.485,41.019,30.736 -2020-04-23 06:00:00,115.69,135.297,53.963,30.736 -2020-04-23 06:15:00,112.99,140.192,53.963,30.736 -2020-04-23 06:30:00,117.65,137.392,53.963,30.736 -2020-04-23 06:45:00,114.98,137.661,53.963,30.736 -2020-04-23 07:00:00,122.88,139.759,66.512,30.736 -2020-04-23 07:15:00,119.72,140.15200000000002,66.512,30.736 -2020-04-23 07:30:00,116.23,138.096,66.512,30.736 -2020-04-23 07:45:00,110.91,134.984,66.512,30.736 -2020-04-23 08:00:00,109.73,132.45600000000002,58.86,30.736 -2020-04-23 08:15:00,111.4,131.204,58.86,30.736 -2020-04-23 08:30:00,115.66,126.929,58.86,30.736 -2020-04-23 08:45:00,116.9,125.26,58.86,30.736 -2020-04-23 09:00:00,117.14,118.99799999999999,52.156000000000006,30.736 -2020-04-23 09:15:00,117.82,116.822,52.156000000000006,30.736 -2020-04-23 09:30:00,125.16,118.961,52.156000000000006,30.736 -2020-04-23 09:45:00,126.64,118.62700000000001,52.156000000000006,30.736 -2020-04-23 10:00:00,115.39,114.26899999999999,49.034,30.736 -2020-04-23 10:15:00,107.22,114.51,49.034,30.736 -2020-04-23 10:30:00,110.97,113.139,49.034,30.736 -2020-04-23 10:45:00,105.98,113.331,49.034,30.736 -2020-04-23 11:00:00,110.48,107.988,46.53,30.736 -2020-04-23 11:15:00,109.12,108.90700000000001,46.53,30.736 -2020-04-23 11:30:00,104.52,110.09200000000001,46.53,30.736 -2020-04-23 11:45:00,118.1,110.81299999999999,46.53,30.736 -2020-04-23 12:00:00,100.86,106.01799999999999,43.318000000000005,30.736 -2020-04-23 12:15:00,107.42,106.89200000000001,43.318000000000005,30.736 -2020-04-23 12:30:00,119.08,105.743,43.318000000000005,30.736 -2020-04-23 12:45:00,113.59,106.492,43.318000000000005,30.736 -2020-04-23 13:00:00,105.64,107.32799999999999,41.608000000000004,30.736 -2020-04-23 13:15:00,109.66,106.58,41.608000000000004,30.736 -2020-04-23 13:30:00,107.3,104.464,41.608000000000004,30.736 -2020-04-23 13:45:00,109.96,103.055,41.608000000000004,30.736 -2020-04-23 14:00:00,108.12,104.509,41.786,30.736 -2020-04-23 14:15:00,98.99,103.896,41.786,30.736 -2020-04-23 14:30:00,100.33,103.461,41.786,30.736 -2020-04-23 14:45:00,105.57,103.94200000000001,41.786,30.736 -2020-04-23 15:00:00,105.07,105.101,44.181999999999995,30.736 -2020-04-23 15:15:00,107.35,103.44200000000001,44.181999999999995,30.736 -2020-04-23 15:30:00,99.98,102.71700000000001,44.181999999999995,30.736 -2020-04-23 15:45:00,100.25,102.22399999999999,44.181999999999995,30.736 -2020-04-23 16:00:00,98.86,102.346,45.956,30.736 -2020-04-23 16:15:00,107.63,102.027,45.956,30.736 -2020-04-23 16:30:00,113.08,102.045,45.956,30.736 -2020-04-23 16:45:00,113.6,99.471,45.956,30.736 -2020-04-23 17:00:00,112.41,99.37799999999999,50.702,30.736 -2020-04-23 17:15:00,112.21,101.604,50.702,30.736 -2020-04-23 17:30:00,118.08,102.958,50.702,30.736 -2020-04-23 17:45:00,120.54,104.251,50.702,30.736 -2020-04-23 18:00:00,119.87,105.165,53.595,30.736 -2020-04-23 18:15:00,109.3,106.839,53.595,30.736 -2020-04-23 18:30:00,109.67,105.585,53.595,30.736 -2020-04-23 18:45:00,108.25,111.24799999999999,53.595,30.736 -2020-04-23 19:00:00,107.8,109.385,54.207,30.736 -2020-04-23 19:15:00,105.82,108.62,54.207,30.736 -2020-04-23 19:30:00,107.51,108.69200000000001,54.207,30.736 -2020-04-23 19:45:00,112.01,109.26,54.207,30.736 -2020-04-23 20:00:00,110.19,107.22,56.948,30.736 -2020-04-23 20:15:00,109.28,104.626,56.948,30.736 -2020-04-23 20:30:00,106.75,103.795,56.948,30.736 -2020-04-23 20:45:00,105.41,103.15899999999999,56.948,30.736 -2020-04-23 21:00:00,103.61,98.861,52.157,30.736 -2020-04-23 21:15:00,98.19,98.456,52.157,30.736 -2020-04-23 21:30:00,87.51,98.734,52.157,30.736 -2020-04-23 21:45:00,93.02,98.40700000000001,52.157,30.736 -2020-04-23 22:00:00,90.12,94.023,47.483000000000004,30.736 -2020-04-23 22:15:00,89.7,92.33200000000001,47.483000000000004,30.736 -2020-04-23 22:30:00,83.45,80.955,47.483000000000004,30.736 -2020-04-23 22:45:00,79.56,75.218,47.483000000000004,30.736 -2020-04-23 23:00:00,78.7,67.197,41.978,30.736 -2020-04-23 23:15:00,82.28,66.135,41.978,30.736 -2020-04-23 23:30:00,81.32,65.26899999999999,41.978,30.736 -2020-04-23 23:45:00,80.34,65.931,41.978,30.736 -2020-04-24 00:00:00,72.2,61.173,39.301,30.736 -2020-04-24 00:15:00,76.53,62.001999999999995,39.301,30.736 -2020-04-24 00:30:00,78.65,60.465,39.301,30.736 -2020-04-24 00:45:00,78.84,59.282,39.301,30.736 -2020-04-24 01:00:00,72.93,59.141999999999996,37.976,30.736 -2020-04-24 01:15:00,76.27,58.315,37.976,30.736 -2020-04-24 01:30:00,79.2,57.232,37.976,30.736 -2020-04-24 01:45:00,79.29,56.583999999999996,37.976,30.736 -2020-04-24 02:00:00,75.93,58.038999999999994,37.041,30.736 -2020-04-24 02:15:00,76.43,57.04,37.041,30.736 -2020-04-24 02:30:00,79.9,60.477,37.041,30.736 -2020-04-24 02:45:00,79.92,60.628,37.041,30.736 -2020-04-24 03:00:00,78.47,63.809,37.575,30.736 -2020-04-24 03:15:00,78.54,64.48100000000001,37.575,30.736 -2020-04-24 03:30:00,83.02,63.73,37.575,30.736 -2020-04-24 03:45:00,86.25,64.94,37.575,30.736 -2020-04-24 04:00:00,84.35,76.809,39.058,30.736 -2020-04-24 04:15:00,84.36,87.662,39.058,30.736 -2020-04-24 04:30:00,84.53,88.164,39.058,30.736 -2020-04-24 04:45:00,87.97,88.916,39.058,30.736 -2020-04-24 05:00:00,94.74,120.58200000000001,43.256,30.736 -2020-04-24 05:15:00,98.06,152.952,43.256,30.736 -2020-04-24 05:30:00,101.99,142.67,43.256,30.736 -2020-04-24 05:45:00,104.32,132.45,43.256,30.736 -2020-04-24 06:00:00,110.14,134.679,56.093999999999994,30.736 -2020-04-24 06:15:00,110.92,138.92700000000002,56.093999999999994,30.736 -2020-04-24 06:30:00,115.01,135.642,56.093999999999994,30.736 -2020-04-24 06:45:00,113.33,136.634,56.093999999999994,30.736 -2020-04-24 07:00:00,114.79,138.756,66.92699999999999,30.736 -2020-04-24 07:15:00,114.94,140.278,66.92699999999999,30.736 -2020-04-24 07:30:00,111.87,136.784,66.92699999999999,30.736 -2020-04-24 07:45:00,113.65,133.072,66.92699999999999,30.736 -2020-04-24 08:00:00,111.88,130.465,60.332,30.736 -2020-04-24 08:15:00,111.97,129.475,60.332,30.736 -2020-04-24 08:30:00,116.04,125.63600000000001,60.332,30.736 -2020-04-24 08:45:00,116.96,123.051,60.332,30.736 -2020-04-24 09:00:00,113.47,115.54,56.085,30.736 -2020-04-24 09:15:00,109.61,114.911,56.085,30.736 -2020-04-24 09:30:00,104.73,116.389,56.085,30.736 -2020-04-24 09:45:00,106.39,116.274,56.085,30.736 -2020-04-24 10:00:00,104.8,111.147,52.91,30.736 -2020-04-24 10:15:00,110.96,111.698,52.91,30.736 -2020-04-24 10:30:00,108.8,110.649,52.91,30.736 -2020-04-24 10:45:00,112.42,110.54799999999999,52.91,30.736 -2020-04-24 11:00:00,105.01,105.321,52.278999999999996,30.736 -2020-04-24 11:15:00,106.04,105.079,52.278999999999996,30.736 -2020-04-24 11:30:00,106.23,107.06299999999999,52.278999999999996,30.736 -2020-04-24 11:45:00,105.8,107.24700000000001,52.278999999999996,30.736 -2020-04-24 12:00:00,101.15,103.402,49.023999999999994,30.736 -2020-04-24 12:15:00,100.58,102.65100000000001,49.023999999999994,30.736 -2020-04-24 12:30:00,96.07,101.62299999999999,49.023999999999994,30.736 -2020-04-24 12:45:00,90.58,102.21799999999999,49.023999999999994,30.736 -2020-04-24 13:00:00,91.3,104.00399999999999,46.82,30.736 -2020-04-24 13:15:00,91.72,103.84899999999999,46.82,30.736 -2020-04-24 13:30:00,89.96,102.21700000000001,46.82,30.736 -2020-04-24 13:45:00,90.43,100.95299999999999,46.82,30.736 -2020-04-24 14:00:00,91.03,101.309,45.756,30.736 -2020-04-24 14:15:00,90.38,100.82799999999999,45.756,30.736 -2020-04-24 14:30:00,90.13,101.54700000000001,45.756,30.736 -2020-04-24 14:45:00,87.82,101.804,45.756,30.736 -2020-04-24 15:00:00,88.57,102.664,47.56,30.736 -2020-04-24 15:15:00,89.54,100.579,47.56,30.736 -2020-04-24 15:30:00,90.6,98.565,47.56,30.736 -2020-04-24 15:45:00,93.53,98.59200000000001,47.56,30.736 -2020-04-24 16:00:00,96.08,97.604,49.581,30.736 -2020-04-24 16:15:00,98.58,97.73100000000001,49.581,30.736 -2020-04-24 16:30:00,103.04,97.697,49.581,30.736 -2020-04-24 16:45:00,102.74,94.509,49.581,30.736 -2020-04-24 17:00:00,106.18,95.684,53.918,30.736 -2020-04-24 17:15:00,105.16,97.54299999999999,53.918,30.736 -2020-04-24 17:30:00,105.91,98.814,53.918,30.736 -2020-04-24 17:45:00,107.32,99.845,53.918,30.736 -2020-04-24 18:00:00,107.44,101.23700000000001,54.266000000000005,30.736 -2020-04-24 18:15:00,104.49,102.10700000000001,54.266000000000005,30.736 -2020-04-24 18:30:00,104.57,100.98899999999999,54.266000000000005,30.736 -2020-04-24 18:45:00,103.28,106.92399999999999,54.266000000000005,30.736 -2020-04-24 19:00:00,105.31,106.14399999999999,54.092,30.736 -2020-04-24 19:15:00,99.77,106.50299999999999,54.092,30.736 -2020-04-24 19:30:00,104.65,106.406,54.092,30.736 -2020-04-24 19:45:00,102.74,106.08,54.092,30.736 -2020-04-24 20:00:00,99.02,103.93700000000001,59.038999999999994,30.736 -2020-04-24 20:15:00,105.31,101.875,59.038999999999994,30.736 -2020-04-24 20:30:00,106.75,100.73700000000001,59.038999999999994,30.736 -2020-04-24 20:45:00,103.82,99.896,59.038999999999994,30.736 -2020-04-24 21:00:00,92.72,96.742,53.346000000000004,30.736 -2020-04-24 21:15:00,89.77,97.68700000000001,53.346000000000004,30.736 -2020-04-24 21:30:00,92.29,97.876,53.346000000000004,30.736 -2020-04-24 21:45:00,91.53,98.0,53.346000000000004,30.736 -2020-04-24 22:00:00,86.77,94.05799999999999,47.938,30.736 -2020-04-24 22:15:00,82.96,92.156,47.938,30.736 -2020-04-24 22:30:00,85.15,87.279,47.938,30.736 -2020-04-24 22:45:00,83.5,83.97200000000001,47.938,30.736 -2020-04-24 23:00:00,78.39,76.804,40.266,30.736 -2020-04-24 23:15:00,76.03,73.711,40.266,30.736 -2020-04-24 23:30:00,79.22,70.86399999999999,40.266,30.736 -2020-04-24 23:45:00,77.46,71.1,40.266,30.736 -2020-04-25 00:00:00,72.92,60.177,39.184,30.618000000000002 -2020-04-25 00:15:00,67.06,58.391000000000005,39.184,30.618000000000002 -2020-04-25 00:30:00,65.85,57.206,39.184,30.618000000000002 -2020-04-25 00:45:00,69.65,55.895,39.184,30.618000000000002 -2020-04-25 01:00:00,71.38,56.265,34.692,30.618000000000002 -2020-04-25 01:15:00,72.76,55.318000000000005,34.692,30.618000000000002 -2020-04-25 01:30:00,69.42,53.401,34.692,30.618000000000002 -2020-04-25 01:45:00,71.88,53.498000000000005,34.692,30.618000000000002 -2020-04-25 02:00:00,71.15,54.626000000000005,32.919000000000004,30.618000000000002 -2020-04-25 02:15:00,71.09,52.883,32.919000000000004,30.618000000000002 -2020-04-25 02:30:00,65.69,55.233999999999995,32.919000000000004,30.618000000000002 -2020-04-25 02:45:00,61.4,55.98,32.919000000000004,30.618000000000002 -2020-04-25 03:00:00,64.48,58.56100000000001,32.024,30.618000000000002 -2020-04-25 03:15:00,63.49,58.118,32.024,30.618000000000002 -2020-04-25 03:30:00,63.7,56.793,32.024,30.618000000000002 -2020-04-25 03:45:00,64.2,59.101000000000006,32.024,30.618000000000002 -2020-04-25 04:00:00,64.64,67.586,31.958000000000002,30.618000000000002 -2020-04-25 04:15:00,64.45,76.515,31.958000000000002,30.618000000000002 -2020-04-25 04:30:00,62.58,74.78,31.958000000000002,30.618000000000002 -2020-04-25 04:45:00,61.79,75.464,31.958000000000002,30.618000000000002 -2020-04-25 05:00:00,62.67,93.719,32.75,30.618000000000002 -2020-04-25 05:15:00,63.83,109.10600000000001,32.75,30.618000000000002 -2020-04-25 05:30:00,66.59,100.096,32.75,30.618000000000002 -2020-04-25 05:45:00,66.17,95.70100000000001,32.75,30.618000000000002 -2020-04-25 06:00:00,69.06,114.625,34.461999999999996,30.618000000000002 -2020-04-25 06:15:00,71.02,132.321,34.461999999999996,30.618000000000002 -2020-04-25 06:30:00,71.93,124.402,34.461999999999996,30.618000000000002 -2020-04-25 06:45:00,73.64,118.735,34.461999999999996,30.618000000000002 -2020-04-25 07:00:00,76.52,117.56200000000001,37.736,30.618000000000002 -2020-04-25 07:15:00,76.86,117.56,37.736,30.618000000000002 -2020-04-25 07:30:00,77.45,116.432,37.736,30.618000000000002 -2020-04-25 07:45:00,75.16,115.383,37.736,30.618000000000002 -2020-04-25 08:00:00,75.87,115.021,42.34,30.618000000000002 -2020-04-25 08:15:00,75.01,115.853,42.34,30.618000000000002 -2020-04-25 08:30:00,76.19,112.833,42.34,30.618000000000002 -2020-04-25 08:45:00,75.0,112.568,42.34,30.618000000000002 -2020-04-25 09:00:00,77.22,107.902,43.571999999999996,30.618000000000002 -2020-04-25 09:15:00,74.1,107.99700000000001,43.571999999999996,30.618000000000002 -2020-04-25 09:30:00,74.58,110.29799999999999,43.571999999999996,30.618000000000002 -2020-04-25 09:45:00,72.53,110.01700000000001,43.571999999999996,30.618000000000002 -2020-04-25 10:00:00,70.38,105.26,40.514,30.618000000000002 -2020-04-25 10:15:00,71.83,106.17,40.514,30.618000000000002 -2020-04-25 10:30:00,72.86,105.02600000000001,40.514,30.618000000000002 -2020-04-25 10:45:00,76.31,105.464,40.514,30.618000000000002 -2020-04-25 11:00:00,75.64,100.208,36.388000000000005,30.618000000000002 -2020-04-25 11:15:00,72.22,100.119,36.388000000000005,30.618000000000002 -2020-04-25 11:30:00,70.44,101.631,36.388000000000005,30.618000000000002 -2020-04-25 11:45:00,66.44,101.69,36.388000000000005,30.618000000000002 -2020-04-25 12:00:00,62.4,97.416,35.217,30.618000000000002 -2020-04-25 12:15:00,63.37,97.552,35.217,30.618000000000002 -2020-04-25 12:30:00,62.6,96.59299999999999,35.217,30.618000000000002 -2020-04-25 12:45:00,62.97,97.165,35.217,30.618000000000002 -2020-04-25 13:00:00,66.68,98.23899999999999,32.001999999999995,30.618000000000002 -2020-04-25 13:15:00,66.46,96.544,32.001999999999995,30.618000000000002 -2020-04-25 13:30:00,68.38,94.76,32.001999999999995,30.618000000000002 -2020-04-25 13:45:00,63.86,92.984,32.001999999999995,30.618000000000002 -2020-04-25 14:00:00,65.79,94.14200000000001,31.304000000000002,30.618000000000002 -2020-04-25 14:15:00,65.45,92.59,31.304000000000002,30.618000000000002 -2020-04-25 14:30:00,67.98,91.934,31.304000000000002,30.618000000000002 -2020-04-25 14:45:00,70.14,92.603,31.304000000000002,30.618000000000002 -2020-04-25 15:00:00,73.95,94.147,34.731,30.618000000000002 -2020-04-25 15:15:00,68.45,92.935,34.731,30.618000000000002 -2020-04-25 15:30:00,69.17,91.913,34.731,30.618000000000002 -2020-04-25 15:45:00,71.96,91.391,34.731,30.618000000000002 -2020-04-25 16:00:00,74.03,91.31200000000001,38.769,30.618000000000002 -2020-04-25 16:15:00,73.54,91.38,38.769,30.618000000000002 -2020-04-25 16:30:00,77.94,91.461,38.769,30.618000000000002 -2020-04-25 16:45:00,77.97,88.696,38.769,30.618000000000002 -2020-04-25 17:00:00,79.95,88.99700000000001,44.928000000000004,30.618000000000002 -2020-04-25 17:15:00,79.81,90.412,44.928000000000004,30.618000000000002 -2020-04-25 17:30:00,81.88,91.541,44.928000000000004,30.618000000000002 -2020-04-25 17:45:00,79.84,92.62799999999999,44.928000000000004,30.618000000000002 -2020-04-25 18:00:00,81.95,94.76899999999999,47.786,30.618000000000002 -2020-04-25 18:15:00,80.8,97.704,47.786,30.618000000000002 -2020-04-25 18:30:00,80.62,98.166,47.786,30.618000000000002 -2020-04-25 18:45:00,81.58,100.06200000000001,47.786,30.618000000000002 -2020-04-25 19:00:00,82.25,98.82600000000001,47.463,30.618000000000002 -2020-04-25 19:15:00,81.6,98.26799999999999,47.463,30.618000000000002 -2020-04-25 19:30:00,78.79,99.083,47.463,30.618000000000002 -2020-04-25 19:45:00,81.11,99.837,47.463,30.618000000000002 -2020-04-25 20:00:00,79.47,99.35600000000001,43.735,30.618000000000002 -2020-04-25 20:15:00,80.74,98.01899999999999,43.735,30.618000000000002 -2020-04-25 20:30:00,77.33,96.155,43.735,30.618000000000002 -2020-04-25 20:45:00,77.88,96.339,43.735,30.618000000000002 -2020-04-25 21:00:00,76.72,93.439,40.346,30.618000000000002 -2020-04-25 21:15:00,73.54,94.40100000000001,40.346,30.618000000000002 -2020-04-25 21:30:00,70.17,95.348,40.346,30.618000000000002 -2020-04-25 21:45:00,69.94,94.93,40.346,30.618000000000002 -2020-04-25 22:00:00,66.61,91.697,39.323,30.618000000000002 -2020-04-25 22:15:00,65.99,91.294,39.323,30.618000000000002 -2020-04-25 22:30:00,63.67,89.425,39.323,30.618000000000002 -2020-04-25 22:45:00,62.7,87.34100000000001,39.323,30.618000000000002 -2020-04-25 23:00:00,58.03,80.985,33.716,30.618000000000002 -2020-04-25 23:15:00,58.89,77.345,33.716,30.618000000000002 -2020-04-25 23:30:00,57.34,75.185,33.716,30.618000000000002 -2020-04-25 23:45:00,57.63,74.384,33.716,30.618000000000002 -2020-04-26 00:00:00,53.94,60.93899999999999,28.703000000000003,30.618000000000002 -2020-04-26 00:15:00,54.77,58.223,28.703000000000003,30.618000000000002 -2020-04-26 00:30:00,54.11,56.742,28.703000000000003,30.618000000000002 -2020-04-26 00:45:00,54.48,55.713,28.703000000000003,30.618000000000002 -2020-04-26 01:00:00,53.07,56.185,26.171,30.618000000000002 -2020-04-26 01:15:00,53.77,55.653,26.171,30.618000000000002 -2020-04-26 01:30:00,53.29,53.88399999999999,26.171,30.618000000000002 -2020-04-26 01:45:00,53.5,53.567,26.171,30.618000000000002 -2020-04-26 02:00:00,52.56,54.358999999999995,25.326999999999998,30.618000000000002 -2020-04-26 02:15:00,53.08,52.657,25.326999999999998,30.618000000000002 -2020-04-26 02:30:00,53.0,55.581,25.326999999999998,30.618000000000002 -2020-04-26 02:45:00,53.59,56.367,25.326999999999998,30.618000000000002 -2020-04-26 03:00:00,53.57,59.565,24.311999999999998,30.618000000000002 -2020-04-26 03:15:00,54.44,59.016000000000005,24.311999999999998,30.618000000000002 -2020-04-26 03:30:00,54.58,57.88,24.311999999999998,30.618000000000002 -2020-04-26 03:45:00,54.59,59.626999999999995,24.311999999999998,30.618000000000002 -2020-04-26 04:00:00,55.25,67.928,25.33,30.618000000000002 -2020-04-26 04:15:00,56.11,76.015,25.33,30.618000000000002 -2020-04-26 04:30:00,54.4,75.235,25.33,30.618000000000002 -2020-04-26 04:45:00,54.42,75.722,25.33,30.618000000000002 -2020-04-26 05:00:00,53.23,92.29700000000001,25.309,30.618000000000002 -2020-04-26 05:15:00,54.13,105.882,25.309,30.618000000000002 -2020-04-26 05:30:00,53.73,96.536,25.309,30.618000000000002 -2020-04-26 05:45:00,55.13,92.115,25.309,30.618000000000002 -2020-04-26 06:00:00,56.83,109.336,25.945999999999998,30.618000000000002 -2020-04-26 06:15:00,59.16,126.744,25.945999999999998,30.618000000000002 -2020-04-26 06:30:00,59.67,117.788,25.945999999999998,30.618000000000002 -2020-04-26 06:45:00,60.03,110.90899999999999,25.945999999999998,30.618000000000002 -2020-04-26 07:00:00,62.06,111.11,27.87,30.618000000000002 -2020-04-26 07:15:00,63.4,109.559,27.87,30.618000000000002 -2020-04-26 07:30:00,62.84,108.62899999999999,27.87,30.618000000000002 -2020-04-26 07:45:00,61.39,107.17399999999999,27.87,30.618000000000002 -2020-04-26 08:00:00,61.91,108.194,32.114000000000004,30.618000000000002 -2020-04-26 08:15:00,60.92,109.787,32.114000000000004,30.618000000000002 -2020-04-26 08:30:00,59.14,108.181,32.114000000000004,30.618000000000002 -2020-04-26 08:45:00,58.58,108.95200000000001,32.114000000000004,30.618000000000002 -2020-04-26 09:00:00,57.18,103.98299999999999,34.222,30.618000000000002 -2020-04-26 09:15:00,58.03,104.06200000000001,34.222,30.618000000000002 -2020-04-26 09:30:00,58.37,106.571,34.222,30.618000000000002 -2020-04-26 09:45:00,60.08,106.904,34.222,30.618000000000002 -2020-04-26 10:00:00,59.48,103.84200000000001,34.544000000000004,30.618000000000002 -2020-04-26 10:15:00,60.83,105.178,34.544000000000004,30.618000000000002 -2020-04-26 10:30:00,61.23,104.53,34.544000000000004,30.618000000000002 -2020-04-26 10:45:00,61.1,104.56299999999999,34.544000000000004,30.618000000000002 -2020-04-26 11:00:00,62.69,99.581,36.368,30.618000000000002 -2020-04-26 11:15:00,58.41,99.315,36.368,30.618000000000002 -2020-04-26 11:30:00,56.55,100.654,36.368,30.618000000000002 -2020-04-26 11:45:00,57.98,101.229,36.368,30.618000000000002 -2020-04-26 12:00:00,52.84,97.259,32.433,30.618000000000002 -2020-04-26 12:15:00,51.83,98.132,32.433,30.618000000000002 -2020-04-26 12:30:00,49.53,96.414,32.433,30.618000000000002 -2020-04-26 12:45:00,50.89,96.04799999999999,32.433,30.618000000000002 -2020-04-26 13:00:00,45.96,96.559,28.971999999999998,30.618000000000002 -2020-04-26 13:15:00,46.59,96.266,28.971999999999998,30.618000000000002 -2020-04-26 13:30:00,46.54,93.7,28.971999999999998,30.618000000000002 -2020-04-26 13:45:00,49.1,92.21,28.971999999999998,30.618000000000002 -2020-04-26 14:00:00,49.36,94.28200000000001,25.531999999999996,30.618000000000002 -2020-04-26 14:15:00,47.82,93.669,25.531999999999996,30.618000000000002 -2020-04-26 14:30:00,46.91,92.95,25.531999999999996,30.618000000000002 -2020-04-26 14:45:00,50.24,92.745,25.531999999999996,30.618000000000002 -2020-04-26 15:00:00,48.5,93.521,25.766,30.618000000000002 -2020-04-26 15:15:00,49.38,92.24700000000001,25.766,30.618000000000002 -2020-04-26 15:30:00,52.0,91.41799999999999,25.766,30.618000000000002 -2020-04-26 15:45:00,56.72,91.45100000000001,25.766,30.618000000000002 -2020-04-26 16:00:00,58.03,91.37200000000001,29.232,30.618000000000002 -2020-04-26 16:15:00,58.12,91.04899999999999,29.232,30.618000000000002 -2020-04-26 16:30:00,61.98,91.90100000000001,29.232,30.618000000000002 -2020-04-26 16:45:00,66.32,89.185,29.232,30.618000000000002 -2020-04-26 17:00:00,69.92,89.704,37.431,30.618000000000002 -2020-04-26 17:15:00,72.25,91.865,37.431,30.618000000000002 -2020-04-26 17:30:00,71.68,93.652,37.431,30.618000000000002 -2020-04-26 17:45:00,72.65,96.22200000000001,37.431,30.618000000000002 -2020-04-26 18:00:00,74.1,98.405,41.251999999999995,30.618000000000002 -2020-04-26 18:15:00,73.58,101.804,41.251999999999995,30.618000000000002 -2020-04-26 18:30:00,79.83,100.978,41.251999999999995,30.618000000000002 -2020-04-26 18:45:00,81.73,103.944,41.251999999999995,30.618000000000002 -2020-04-26 19:00:00,82.6,103.89399999999999,41.784,30.618000000000002 -2020-04-26 19:15:00,74.77,102.92299999999999,41.784,30.618000000000002 -2020-04-26 19:30:00,78.18,103.49700000000001,41.784,30.618000000000002 -2020-04-26 19:45:00,82.92,104.728,41.784,30.618000000000002 -2020-04-26 20:00:00,80.12,104.32799999999999,40.804,30.618000000000002 -2020-04-26 20:15:00,79.67,103.42299999999999,40.804,30.618000000000002 -2020-04-26 20:30:00,83.74,102.774,40.804,30.618000000000002 -2020-04-26 20:45:00,87.56,101.26,40.804,30.618000000000002 -2020-04-26 21:00:00,86.73,96.89200000000001,38.379,30.618000000000002 -2020-04-26 21:15:00,77.37,97.345,38.379,30.618000000000002 -2020-04-26 21:30:00,78.96,98.001,38.379,30.618000000000002 -2020-04-26 21:45:00,72.49,97.919,38.379,30.618000000000002 -2020-04-26 22:00:00,71.02,95.426,37.87,30.618000000000002 -2020-04-26 22:15:00,74.6,93.542,37.87,30.618000000000002 -2020-04-26 22:30:00,74.46,89.67299999999999,37.87,30.618000000000002 -2020-04-26 22:45:00,77.49,86.29799999999999,37.87,30.618000000000002 -2020-04-26 23:00:00,67.34,78.266,33.332,30.618000000000002 -2020-04-26 23:15:00,67.41,76.46300000000001,33.332,30.618000000000002 -2020-04-26 23:30:00,65.25,74.368,33.332,30.618000000000002 -2020-04-26 23:45:00,71.84,74.14,33.332,30.618000000000002 -2020-04-27 00:00:00,65.34,63.676,34.698,30.736 -2020-04-27 00:15:00,68.11,62.993,34.698,30.736 -2020-04-27 00:30:00,62.17,61.31,34.698,30.736 -2020-04-27 00:45:00,65.5,59.75,34.698,30.736 -2020-04-27 01:00:00,67.94,60.476000000000006,32.889,30.736 -2020-04-27 01:15:00,68.09,59.667,32.889,30.736 -2020-04-27 01:30:00,64.87,58.151,32.889,30.736 -2020-04-27 01:45:00,62.39,57.826,32.889,30.736 -2020-04-27 02:00:00,60.51,58.898999999999994,32.06,30.736 -2020-04-27 02:15:00,61.52,57.211000000000006,32.06,30.736 -2020-04-27 02:30:00,63.11,60.428999999999995,32.06,30.736 -2020-04-27 02:45:00,65.0,60.816,32.06,30.736 -2020-04-27 03:00:00,70.94,64.986,30.515,30.736 -2020-04-27 03:15:00,71.15,65.718,30.515,30.736 -2020-04-27 03:30:00,66.39,64.915,30.515,30.736 -2020-04-27 03:45:00,68.04,66.095,30.515,30.736 -2020-04-27 04:00:00,69.68,78.61399999999999,31.436,30.736 -2020-04-27 04:15:00,70.3,90.708,31.436,30.736 -2020-04-27 04:30:00,70.44,90.67,31.436,30.736 -2020-04-27 04:45:00,74.91,91.48200000000001,31.436,30.736 -2020-04-27 05:00:00,81.04,120.303,38.997,30.736 -2020-04-27 05:15:00,85.59,150.614,38.997,30.736 -2020-04-27 05:30:00,88.32,140.344,38.997,30.736 -2020-04-27 05:45:00,87.62,131.126,38.997,30.736 -2020-04-27 06:00:00,93.16,132.86700000000002,54.97,30.736 -2020-04-27 06:15:00,90.44,136.747,54.97,30.736 -2020-04-27 06:30:00,94.92,134.424,54.97,30.736 -2020-04-27 06:45:00,93.91,135.476,54.97,30.736 -2020-04-27 07:00:00,97.72,137.559,66.032,30.736 -2020-04-27 07:15:00,94.82,138.159,66.032,30.736 -2020-04-27 07:30:00,94.52,136.225,66.032,30.736 -2020-04-27 07:45:00,94.9,133.67700000000002,66.032,30.736 -2020-04-27 08:00:00,92.49,131.08700000000002,59.941,30.736 -2020-04-27 08:15:00,91.85,130.80700000000002,59.941,30.736 -2020-04-27 08:30:00,93.31,126.40799999999999,59.941,30.736 -2020-04-27 08:45:00,92.94,125.59700000000001,59.941,30.736 -2020-04-27 09:00:00,92.84,119.57600000000001,54.016000000000005,30.736 -2020-04-27 09:15:00,91.53,116.648,54.016000000000005,30.736 -2020-04-27 09:30:00,89.3,118.041,54.016000000000005,30.736 -2020-04-27 09:45:00,89.57,117.095,54.016000000000005,30.736 -2020-04-27 10:00:00,89.14,113.96600000000001,50.63,30.736 -2020-04-27 10:15:00,98.83,115.074,50.63,30.736 -2020-04-27 10:30:00,93.52,113.675,50.63,30.736 -2020-04-27 10:45:00,89.12,113.164,50.63,30.736 -2020-04-27 11:00:00,89.55,106.956,49.951,30.736 -2020-04-27 11:15:00,91.6,107.90700000000001,49.951,30.736 -2020-04-27 11:30:00,86.24,110.45299999999999,49.951,30.736 -2020-04-27 11:45:00,87.15,111.12,49.951,30.736 -2020-04-27 12:00:00,88.45,107.34700000000001,46.913000000000004,30.736 -2020-04-27 12:15:00,99.89,108.29299999999999,46.913000000000004,30.736 -2020-04-27 12:30:00,92.92,106.042,46.913000000000004,30.736 -2020-04-27 12:45:00,89.09,106.566,46.913000000000004,30.736 -2020-04-27 13:00:00,96.03,108.00299999999999,47.093999999999994,30.736 -2020-04-27 13:15:00,84.25,106.37299999999999,47.093999999999994,30.736 -2020-04-27 13:30:00,77.31,103.604,47.093999999999994,30.736 -2020-04-27 13:45:00,88.26,102.666,47.093999999999994,30.736 -2020-04-27 14:00:00,85.89,103.928,46.678000000000004,30.736 -2020-04-27 14:15:00,80.66,103.271,46.678000000000004,30.736 -2020-04-27 14:30:00,80.46,102.094,46.678000000000004,30.736 -2020-04-27 14:45:00,81.74,103.096,46.678000000000004,30.736 -2020-04-27 15:00:00,82.97,104.74700000000001,47.715,30.736 -2020-04-27 15:15:00,81.62,102.262,47.715,30.736 -2020-04-27 15:30:00,82.55,101.359,47.715,30.736 -2020-04-27 15:45:00,85.7,100.82799999999999,47.715,30.736 -2020-04-27 16:00:00,85.53,101.301,49.81100000000001,30.736 -2020-04-27 16:15:00,85.86,100.56299999999999,49.81100000000001,30.736 -2020-04-27 16:30:00,88.12,100.47,49.81100000000001,30.736 -2020-04-27 16:45:00,89.52,97.03399999999999,49.81100000000001,30.736 -2020-04-27 17:00:00,93.73,96.706,55.591,30.736 -2020-04-27 17:15:00,92.69,98.552,55.591,30.736 -2020-04-27 17:30:00,95.38,99.787,55.591,30.736 -2020-04-27 17:45:00,94.15,101.258,55.591,30.736 -2020-04-27 18:00:00,95.38,102.904,56.523,30.736 -2020-04-27 18:15:00,92.93,103.962,56.523,30.736 -2020-04-27 18:30:00,92.0,102.98899999999999,56.523,30.736 -2020-04-27 18:45:00,90.2,108.369,56.523,30.736 -2020-04-27 19:00:00,90.92,107.20299999999999,56.044,30.736 -2020-04-27 19:15:00,86.2,106.48899999999999,56.044,30.736 -2020-04-27 19:30:00,85.27,107.094,56.044,30.736 -2020-04-27 19:45:00,86.87,107.507,56.044,30.736 -2020-04-27 20:00:00,84.36,105.01899999999999,61.715,30.736 -2020-04-27 20:15:00,83.51,103.73299999999999,61.715,30.736 -2020-04-27 20:30:00,80.49,102.479,61.715,30.736 -2020-04-27 20:45:00,78.45,101.991,61.715,30.736 -2020-04-27 21:00:00,72.63,97.50399999999999,56.24,30.736 -2020-04-27 21:15:00,73.27,97.652,56.24,30.736 -2020-04-27 21:30:00,68.74,98.13600000000001,56.24,30.736 -2020-04-27 21:45:00,67.7,97.67399999999999,56.24,30.736 -2020-04-27 22:00:00,63.52,92.344,50.437,30.736 -2020-04-27 22:15:00,63.05,91.10600000000001,50.437,30.736 -2020-04-27 22:30:00,60.88,79.39,50.437,30.736 -2020-04-27 22:45:00,60.19,73.476,50.437,30.736 -2020-04-27 23:00:00,81.41,65.845,42.756,30.736 -2020-04-27 23:15:00,80.65,64.377,42.756,30.736 -2020-04-27 23:30:00,78.41,63.692,42.756,30.736 -2020-04-27 23:45:00,78.52,64.492,42.756,30.736 -2020-04-28 00:00:00,77.46,61.4,39.857,30.736 -2020-04-28 00:15:00,76.93,62.035,39.857,30.736 -2020-04-28 00:30:00,76.12,60.358000000000004,39.857,30.736 -2020-04-28 00:45:00,76.92,58.856,39.857,30.736 -2020-04-28 01:00:00,78.02,59.108999999999995,37.233000000000004,30.736 -2020-04-28 01:15:00,78.26,58.13,37.233000000000004,30.736 -2020-04-28 01:30:00,75.49,56.593,37.233000000000004,30.736 -2020-04-28 01:45:00,75.85,56.073,37.233000000000004,30.736 -2020-04-28 02:00:00,78.67,56.803000000000004,35.856,30.736 -2020-04-28 02:15:00,78.88,55.836999999999996,35.856,30.736 -2020-04-28 02:30:00,73.42,58.508,35.856,30.736 -2020-04-28 02:45:00,73.56,59.13,35.856,30.736 -2020-04-28 03:00:00,74.82,62.342,34.766999999999996,30.736 -2020-04-28 03:15:00,76.05,63.271,34.766999999999996,30.736 -2020-04-28 03:30:00,79.51,62.683,34.766999999999996,30.736 -2020-04-28 03:45:00,83.12,63.217,34.766999999999996,30.736 -2020-04-28 04:00:00,84.91,74.73899999999999,35.468,30.736 -2020-04-28 04:15:00,84.94,86.62700000000001,35.468,30.736 -2020-04-28 04:30:00,87.14,86.353,35.468,30.736 -2020-04-28 04:45:00,88.32,88.088,35.468,30.736 -2020-04-28 05:00:00,95.45,120.323,40.399,30.736 -2020-04-28 05:15:00,98.01,150.866,40.399,30.736 -2020-04-28 05:30:00,100.42,140.02200000000002,40.399,30.736 -2020-04-28 05:45:00,102.69,130.343,40.399,30.736 -2020-04-28 06:00:00,107.5,132.263,54.105,30.736 -2020-04-28 06:15:00,108.18,137.054,54.105,30.736 -2020-04-28 06:30:00,110.19,134.178,54.105,30.736 -2020-04-28 06:45:00,110.95,134.42600000000002,54.105,30.736 -2020-04-28 07:00:00,112.62,136.516,63.083,30.736 -2020-04-28 07:15:00,112.53,136.851,63.083,30.736 -2020-04-28 07:30:00,113.87,134.61700000000002,63.083,30.736 -2020-04-28 07:45:00,112.32,131.526,63.083,30.736 -2020-04-28 08:00:00,110.21,128.942,57.254,30.736 -2020-04-28 08:15:00,108.73,127.866,57.254,30.736 -2020-04-28 08:30:00,109.99,123.456,57.254,30.736 -2020-04-28 08:45:00,111.24,121.929,57.254,30.736 -2020-04-28 09:00:00,110.59,115.686,51.395,30.736 -2020-04-28 09:15:00,110.8,113.535,51.395,30.736 -2020-04-28 09:30:00,110.96,115.749,51.395,30.736 -2020-04-28 09:45:00,110.3,115.579,51.395,30.736 -2020-04-28 10:00:00,108.23,111.265,48.201,30.736 -2020-04-28 10:15:00,110.0,111.735,48.201,30.736 -2020-04-28 10:30:00,108.55,110.477,48.201,30.736 -2020-04-28 10:45:00,108.8,110.765,48.201,30.736 -2020-04-28 11:00:00,106.28,105.389,46.133,30.736 -2020-04-28 11:15:00,105.09,106.42,46.133,30.736 -2020-04-28 11:30:00,103.75,107.59899999999999,46.133,30.736 -2020-04-28 11:45:00,106.98,108.41,46.133,30.736 -2020-04-28 12:00:00,106.37,103.759,44.243,30.736 -2020-04-28 12:15:00,109.77,104.667,44.243,30.736 -2020-04-28 12:30:00,108.48,103.306,44.243,30.736 -2020-04-28 12:45:00,108.19,104.073,44.243,30.736 -2020-04-28 13:00:00,104.13,105.09,45.042,30.736 -2020-04-28 13:15:00,103.69,104.32,45.042,30.736 -2020-04-28 13:30:00,104.39,102.223,45.042,30.736 -2020-04-28 13:45:00,106.18,100.82600000000001,45.042,30.736 -2020-04-28 14:00:00,111.13,102.575,44.062,30.736 -2020-04-28 14:15:00,109.93,101.874,44.062,30.736 -2020-04-28 14:30:00,107.35,101.215,44.062,30.736 -2020-04-28 14:45:00,104.4,101.706,44.062,30.736 -2020-04-28 15:00:00,109.65,103.045,46.461999999999996,30.736 -2020-04-28 15:15:00,109.95,101.281,46.461999999999996,30.736 -2020-04-28 15:30:00,109.44,100.34299999999999,46.461999999999996,30.736 -2020-04-28 15:45:00,108.73,99.764,46.461999999999996,30.736 -2020-04-28 16:00:00,108.15,100.094,48.802,30.736 -2020-04-28 16:15:00,109.08,99.656,48.802,30.736 -2020-04-28 16:30:00,111.59,99.704,48.802,30.736 -2020-04-28 16:45:00,112.11,96.806,48.802,30.736 -2020-04-28 17:00:00,114.23,96.969,55.672,30.736 -2020-04-28 17:15:00,114.12,99.066,55.672,30.736 -2020-04-28 17:30:00,116.32,100.38799999999999,55.672,30.736 -2020-04-28 17:45:00,115.06,101.552,55.672,30.736 -2020-04-28 18:00:00,116.52,102.531,57.006,30.736 -2020-04-28 18:15:00,116.37,104.29299999999999,57.006,30.736 -2020-04-28 18:30:00,116.22,102.975,57.006,30.736 -2020-04-28 18:45:00,113.05,108.661,57.006,30.736 -2020-04-28 19:00:00,109.57,106.756,57.148,30.736 -2020-04-28 19:15:00,107.41,106.01799999999999,57.148,30.736 -2020-04-28 19:30:00,111.56,106.139,57.148,30.736 -2020-04-28 19:45:00,110.69,106.81299999999999,57.148,30.736 -2020-04-28 20:00:00,106.84,104.641,61.895,30.736 -2020-04-28 20:15:00,104.83,102.081,61.895,30.736 -2020-04-28 20:30:00,102.09,101.42,61.895,30.736 -2020-04-28 20:45:00,105.29,100.936,61.895,30.736 -2020-04-28 21:00:00,99.4,96.655,54.78,30.736 -2020-04-28 21:15:00,99.25,96.321,54.78,30.736 -2020-04-28 21:30:00,93.24,96.527,54.78,30.736 -2020-04-28 21:45:00,89.26,96.34100000000001,54.78,30.736 -2020-04-28 22:00:00,87.28,92.029,50.76,30.736 -2020-04-28 22:15:00,89.13,90.45700000000001,50.76,30.736 -2020-04-28 22:30:00,86.27,78.956,50.76,30.736 -2020-04-28 22:45:00,83.49,73.17399999999999,50.76,30.736 -2020-04-28 23:00:00,71.4,65.023,44.162,30.736 -2020-04-28 23:15:00,70.29,64.152,44.162,30.736 -2020-04-28 23:30:00,75.32,63.277,44.162,30.736 -2020-04-28 23:45:00,78.15,63.973,44.162,30.736 -2020-04-29 00:00:00,73.93,61.016999999999996,39.061,30.736 -2020-04-29 00:15:00,68.61,61.667,39.061,30.736 -2020-04-29 00:30:00,69.83,59.983999999999995,39.061,30.736 -2020-04-29 00:45:00,74.41,58.489,39.061,30.736 -2020-04-29 01:00:00,73.44,58.735,35.795,30.736 -2020-04-29 01:15:00,71.44,57.733000000000004,35.795,30.736 -2020-04-29 01:30:00,73.21,56.176,35.795,30.736 -2020-04-29 01:45:00,74.77,55.658,35.795,30.736 -2020-04-29 02:00:00,72.45,56.379,33.316,30.736 -2020-04-29 02:15:00,65.54,55.397,33.316,30.736 -2020-04-29 02:30:00,66.56,58.082,33.316,30.736 -2020-04-29 02:45:00,70.87,58.713,33.316,30.736 -2020-04-29 03:00:00,70.07,61.93899999999999,32.803000000000004,30.736 -2020-04-29 03:15:00,76.58,62.843,32.803000000000004,30.736 -2020-04-29 03:30:00,78.46,62.253,32.803000000000004,30.736 -2020-04-29 03:45:00,77.53,62.809,32.803000000000004,30.736 -2020-04-29 04:00:00,79.53,74.297,34.235,30.736 -2020-04-29 04:15:00,80.34,86.14399999999999,34.235,30.736 -2020-04-29 04:30:00,82.42,85.869,34.235,30.736 -2020-04-29 04:45:00,86.48,87.594,34.235,30.736 -2020-04-29 05:00:00,93.77,119.73,38.65,30.736 -2020-04-29 05:15:00,97.73,150.167,38.65,30.736 -2020-04-29 05:30:00,101.06,139.352,38.65,30.736 -2020-04-29 05:45:00,102.22,129.722,38.65,30.736 -2020-04-29 06:00:00,110.99,131.662,54.951,30.736 -2020-04-29 06:15:00,110.65,136.43200000000002,54.951,30.736 -2020-04-29 06:30:00,113.7,133.542,54.951,30.736 -2020-04-29 06:45:00,116.43,133.786,54.951,30.736 -2020-04-29 07:00:00,121.5,135.873,67.328,30.736 -2020-04-29 07:15:00,121.34,136.19899999999998,67.328,30.736 -2020-04-29 07:30:00,121.78,133.929,67.328,30.736 -2020-04-29 07:45:00,122.25,130.845,67.328,30.736 -2020-04-29 08:00:00,122.93,128.249,60.23,30.736 -2020-04-29 08:15:00,125.82,127.21,60.23,30.736 -2020-04-29 08:30:00,127.65,122.774,60.23,30.736 -2020-04-29 08:45:00,128.25,121.274,60.23,30.736 -2020-04-29 09:00:00,129.04,115.035,56.845,30.736 -2020-04-29 09:15:00,129.51,112.889,56.845,30.736 -2020-04-29 09:30:00,126.37,115.118,56.845,30.736 -2020-04-29 09:45:00,124.86,114.979,56.845,30.736 -2020-04-29 10:00:00,121.63,110.675,53.832,30.736 -2020-04-29 10:15:00,121.03,111.19,53.832,30.736 -2020-04-29 10:30:00,122.36,109.954,53.832,30.736 -2020-04-29 10:45:00,116.08,110.26100000000001,53.832,30.736 -2020-04-29 11:00:00,109.95,104.87799999999999,53.225,30.736 -2020-04-29 11:15:00,114.24,105.931,53.225,30.736 -2020-04-29 11:30:00,109.62,107.11,53.225,30.736 -2020-04-29 11:45:00,101.59,107.93799999999999,53.225,30.736 -2020-04-29 12:00:00,99.09,103.316,50.676,30.736 -2020-04-29 12:15:00,97.93,104.23,50.676,30.736 -2020-04-29 12:30:00,97.46,102.82700000000001,50.676,30.736 -2020-04-29 12:45:00,105.95,103.596,50.676,30.736 -2020-04-29 13:00:00,105.96,104.65100000000001,50.646,30.736 -2020-04-29 13:15:00,108.82,103.876,50.646,30.736 -2020-04-29 13:30:00,106.36,101.78200000000001,50.646,30.736 -2020-04-29 13:45:00,107.35,100.38799999999999,50.646,30.736 -2020-04-29 14:00:00,108.22,102.194,50.786,30.736 -2020-04-29 14:15:00,105.88,101.476,50.786,30.736 -2020-04-29 14:30:00,100.87,100.773,50.786,30.736 -2020-04-29 14:45:00,97.81,101.265,50.786,30.736 -2020-04-29 15:00:00,99.63,102.64,51.535,30.736 -2020-04-29 15:15:00,95.3,100.855,51.535,30.736 -2020-04-29 15:30:00,104.15,99.876,51.535,30.736 -2020-04-29 15:45:00,108.0,99.28,51.535,30.736 -2020-04-29 16:00:00,107.58,99.65299999999999,53.157,30.736 -2020-04-29 16:15:00,105.42,99.19,53.157,30.736 -2020-04-29 16:30:00,106.46,99.243,53.157,30.736 -2020-04-29 16:45:00,108.12,96.28399999999999,53.157,30.736 -2020-04-29 17:00:00,109.38,96.49600000000001,57.793,30.736 -2020-04-29 17:15:00,106.94,98.56700000000001,57.793,30.736 -2020-04-29 17:30:00,107.97,99.882,57.793,30.736 -2020-04-29 17:45:00,107.1,101.01899999999999,57.793,30.736 -2020-04-29 18:00:00,108.18,102.01299999999999,59.872,30.736 -2020-04-29 18:15:00,106.95,103.79,59.872,30.736 -2020-04-29 18:30:00,114.64,102.459,59.872,30.736 -2020-04-29 18:45:00,114.76,108.149,59.872,30.736 -2020-04-29 19:00:00,112.55,106.23700000000001,60.17100000000001,30.736 -2020-04-29 19:15:00,104.09,105.50299999999999,60.17100000000001,30.736 -2020-04-29 19:30:00,105.95,105.635,60.17100000000001,30.736 -2020-04-29 19:45:00,108.76,106.331,60.17100000000001,30.736 -2020-04-29 20:00:00,102.11,104.131,65.015,30.736 -2020-04-29 20:15:00,102.79,101.57799999999999,65.015,30.736 -2020-04-29 20:30:00,101.29,100.95100000000001,65.015,30.736 -2020-04-29 20:45:00,104.92,100.49700000000001,65.015,30.736 -2020-04-29 21:00:00,99.96,96.219,57.805,30.736 -2020-04-29 21:15:00,99.95,95.9,57.805,30.736 -2020-04-29 21:30:00,91.66,96.09100000000001,57.805,30.736 -2020-04-29 21:45:00,92.16,95.932,57.805,30.736 -2020-04-29 22:00:00,89.14,91.635,52.115,30.736 -2020-04-29 22:15:00,89.74,90.085,52.115,30.736 -2020-04-29 22:30:00,84.06,78.56,52.115,30.736 -2020-04-29 22:45:00,79.26,72.768,52.115,30.736 -2020-04-29 23:00:00,72.0,64.59100000000001,42.871,30.736 -2020-04-29 23:15:00,77.84,63.75899999999999,42.871,30.736 -2020-04-29 23:30:00,81.54,62.883,42.871,30.736 -2020-04-29 23:45:00,81.39,63.585,42.871,30.736 -2020-04-30 00:00:00,75.37,60.636,39.203,30.736 -2020-04-30 00:15:00,69.88,61.299,39.203,30.736 -2020-04-30 00:30:00,76.11,59.611999999999995,39.203,30.736 -2020-04-30 00:45:00,78.46,58.123000000000005,39.203,30.736 -2020-04-30 01:00:00,76.07,58.363,37.118,30.736 -2020-04-30 01:15:00,74.99,57.338,37.118,30.736 -2020-04-30 01:30:00,69.98,55.761,37.118,30.736 -2020-04-30 01:45:00,72.88,55.246,37.118,30.736 -2020-04-30 02:00:00,77.43,55.956,35.647,30.736 -2020-04-30 02:15:00,77.87,54.958999999999996,35.647,30.736 -2020-04-30 02:30:00,70.93,57.66,35.647,30.736 -2020-04-30 02:45:00,72.1,58.297,35.647,30.736 -2020-04-30 03:00:00,71.36,61.538000000000004,34.585,30.736 -2020-04-30 03:15:00,72.29,62.419,34.585,30.736 -2020-04-30 03:30:00,75.56,61.825,34.585,30.736 -2020-04-30 03:45:00,75.33,62.405,34.585,30.736 -2020-04-30 04:00:00,79.58,73.857,36.184,30.736 -2020-04-30 04:15:00,81.32,85.664,36.184,30.736 -2020-04-30 04:30:00,83.16,85.387,36.184,30.736 -2020-04-30 04:45:00,86.22,87.103,36.184,30.736 -2020-04-30 05:00:00,94.0,119.139,41.019,30.736 -2020-04-30 05:15:00,94.93,149.472,41.019,30.736 -2020-04-30 05:30:00,98.47,138.685,41.019,30.736 -2020-04-30 05:45:00,103.17,129.10299999999998,41.019,30.736 -2020-04-30 06:00:00,110.4,131.064,53.963,30.736 -2020-04-30 06:15:00,111.54,135.813,53.963,30.736 -2020-04-30 06:30:00,114.87,132.909,53.963,30.736 -2020-04-30 06:45:00,116.61,133.149,53.963,30.736 -2020-04-30 07:00:00,121.28,135.233,66.512,30.736 -2020-04-30 07:15:00,121.03,135.549,66.512,30.736 -2020-04-30 07:30:00,122.18,133.245,66.512,30.736 -2020-04-30 07:45:00,121.34,130.166,66.512,30.736 -2020-04-30 08:00:00,120.47,127.561,58.86,30.736 -2020-04-30 08:15:00,121.39,126.557,58.86,30.736 -2020-04-30 08:30:00,122.76,122.095,58.86,30.736 -2020-04-30 08:45:00,121.08,120.624,58.86,30.736 -2020-04-30 09:00:00,121.88,114.389,52.156000000000006,30.736 -2020-04-30 09:15:00,123.85,112.24799999999999,52.156000000000006,30.736 -2020-04-30 09:30:00,124.67,114.49,52.156000000000006,30.736 -2020-04-30 09:45:00,124.74,114.385,52.156000000000006,30.736 -2020-04-30 10:00:00,125.1,110.089,49.034,30.736 -2020-04-30 10:15:00,125.31,110.649,49.034,30.736 -2020-04-30 10:30:00,126.21,109.434,49.034,30.736 -2020-04-30 10:45:00,124.71,109.76,49.034,30.736 -2020-04-30 11:00:00,121.6,104.37200000000001,46.53,30.736 -2020-04-30 11:15:00,123.88,105.447,46.53,30.736 -2020-04-30 11:30:00,120.2,106.624,46.53,30.736 -2020-04-30 11:45:00,119.8,107.46799999999999,46.53,30.736 -2020-04-30 12:00:00,110.03,102.876,43.318000000000005,30.736 -2020-04-30 12:15:00,106.44,103.795,43.318000000000005,30.736 -2020-04-30 12:30:00,100.78,102.351,43.318000000000005,30.736 -2020-04-30 12:45:00,99.22,103.12299999999999,43.318000000000005,30.736 -2020-04-30 13:00:00,106.85,104.212,41.608000000000004,30.736 -2020-04-30 13:15:00,112.1,103.434,41.608000000000004,30.736 -2020-04-30 13:30:00,101.38,101.345,41.608000000000004,30.736 -2020-04-30 13:45:00,100.96,99.954,41.608000000000004,30.736 -2020-04-30 14:00:00,101.38,101.816,41.786,30.736 -2020-04-30 14:15:00,104.57,101.083,41.786,30.736 -2020-04-30 14:30:00,102.6,100.335,41.786,30.736 -2020-04-30 14:45:00,105.7,100.82799999999999,41.786,30.736 -2020-04-30 15:00:00,101.95,102.238,44.181999999999995,30.736 -2020-04-30 15:15:00,104.28,100.432,44.181999999999995,30.736 -2020-04-30 15:30:00,101.87,99.412,44.181999999999995,30.736 -2020-04-30 15:45:00,103.04,98.79899999999999,44.181999999999995,30.736 -2020-04-30 16:00:00,109.28,99.213,45.956,30.736 -2020-04-30 16:15:00,109.2,98.727,45.956,30.736 -2020-04-30 16:30:00,110.26,98.787,45.956,30.736 -2020-04-30 16:45:00,111.89,95.76299999999999,45.956,30.736 -2020-04-30 17:00:00,113.27,96.027,50.702,30.736 -2020-04-30 17:15:00,113.08,98.072,50.702,30.736 -2020-04-30 17:30:00,112.36,99.37899999999999,50.702,30.736 -2020-04-30 17:45:00,111.24,100.491,50.702,30.736 -2020-04-30 18:00:00,111.37,101.49600000000001,53.595,30.736 -2020-04-30 18:15:00,110.86,103.29,53.595,30.736 -2020-04-30 18:30:00,118.75,101.945,53.595,30.736 -2020-04-30 18:45:00,118.09,107.64,53.595,30.736 -2020-04-30 19:00:00,113.08,105.721,54.207,30.736 -2020-04-30 19:15:00,103.48,104.993,54.207,30.736 -2020-04-30 19:30:00,104.39,105.134,54.207,30.736 -2020-04-30 19:45:00,104.48,105.85,54.207,30.736 -2020-04-30 20:00:00,102.44,103.625,56.948,30.736 -2020-04-30 20:15:00,99.4,101.079,56.948,30.736 -2020-04-30 20:30:00,98.53,100.484,56.948,30.736 -2020-04-30 20:45:00,100.91,100.059,56.948,30.736 -2020-04-30 21:00:00,92.46,95.787,52.157,30.736 -2020-04-30 21:15:00,95.11,95.48200000000001,52.157,30.736 -2020-04-30 21:30:00,94.9,95.65799999999999,52.157,30.736 -2020-04-30 21:45:00,93.18,95.525,52.157,30.736 -2020-04-30 22:00:00,90.2,91.242,47.483000000000004,30.736 -2020-04-30 22:15:00,83.38,89.715,47.483000000000004,30.736 -2020-04-30 22:30:00,79.85,78.164,47.483000000000004,30.736 -2020-04-30 22:45:00,83.09,72.363,47.483000000000004,30.736 -2020-04-30 23:00:00,58.59,64.16199999999999,41.978,30.736 -2020-04-30 23:15:00,56.65,63.367,41.978,30.736 -2020-04-30 23:30:00,56.38,62.49100000000001,41.978,30.736 -2020-04-30 23:45:00,55.11,63.199,41.978,30.736 -2020-05-01 00:00:00,50.49,50.163000000000004,18.527,29.662 -2020-05-01 00:15:00,53.64,47.663999999999994,18.527,29.662 -2020-05-01 00:30:00,52.6,46.467,18.527,29.662 -2020-05-01 00:45:00,52.95,45.35,18.527,29.662 -2020-05-01 01:00:00,48.0,45.506,16.348,29.662 -2020-05-01 01:15:00,51.98,45.135,16.348,29.662 -2020-05-01 01:30:00,48.94,43.453,16.348,29.662 -2020-05-01 01:45:00,52.32,43.106,16.348,29.662 -2020-05-01 02:00:00,51.36,43.566,12.581,29.662 -2020-05-01 02:15:00,51.11,41.968,12.581,29.662 -2020-05-01 02:30:00,48.05,44.556999999999995,12.581,29.662 -2020-05-01 02:45:00,47.72,45.224,12.581,29.662 -2020-05-01 03:00:00,48.95,48.13399999999999,10.712,29.662 -2020-05-01 03:15:00,50.28,46.619,10.712,29.662 -2020-05-01 03:30:00,50.66,45.36600000000001,10.712,29.662 -2020-05-01 03:45:00,51.81,46.526,10.712,29.662 -2020-05-01 04:00:00,53.03,54.876000000000005,9.084,29.662 -2020-05-01 04:15:00,54.27,62.676,9.084,29.662 -2020-05-01 04:30:00,51.73,61.516999999999996,9.084,29.662 -2020-05-01 04:45:00,51.01,61.666000000000004,9.084,29.662 -2020-05-01 05:00:00,49.69,76.642,9.388,29.662 -2020-05-01 05:15:00,52.41,87.50299999999999,9.388,29.662 -2020-05-01 05:30:00,52.41,77.738,9.388,29.662 -2020-05-01 05:45:00,50.98,74.282,9.388,29.662 -2020-05-01 06:00:00,55.06,89.3,11.109000000000002,29.662 -2020-05-01 06:15:00,53.68,104.529,11.109000000000002,29.662 -2020-05-01 06:30:00,58.38,96.056,11.109000000000002,29.662 -2020-05-01 06:45:00,60.23,89.587,11.109000000000002,29.662 -2020-05-01 07:00:00,61.8,89.125,13.77,29.662 -2020-05-01 07:15:00,63.86,86.988,13.77,29.662 -2020-05-01 07:30:00,65.25,85.859,13.77,29.662 -2020-05-01 07:45:00,66.17,84.554,13.77,29.662 -2020-05-01 08:00:00,67.11,83.42200000000001,12.868,29.662 -2020-05-01 08:15:00,66.63,85.50299999999999,12.868,29.662 -2020-05-01 08:30:00,67.33,84.87100000000001,12.868,29.662 -2020-05-01 08:45:00,66.74,86.586,12.868,29.662 -2020-05-01 09:00:00,67.45,81.148,12.804,29.662 -2020-05-01 09:15:00,68.21,81.42699999999999,12.804,29.662 -2020-05-01 09:30:00,67.2,84.065,12.804,29.662 -2020-05-01 09:45:00,68.2,84.78399999999999,12.804,29.662 -2020-05-01 10:00:00,69.04,81.47,11.029000000000002,29.662 -2020-05-01 10:15:00,69.41,82.72200000000001,11.029000000000002,29.662 -2020-05-01 10:30:00,70.12,82.583,11.029000000000002,29.662 -2020-05-01 10:45:00,66.61,82.947,11.029000000000002,29.662 -2020-05-01 11:00:00,57.19,79.26,11.681,29.662 -2020-05-01 11:15:00,57.35,79.181,11.681,29.662 -2020-05-01 11:30:00,54.68,80.666,11.681,29.662 -2020-05-01 11:45:00,54.35,80.57,11.681,29.662 -2020-05-01 12:00:00,52.36,78.047,8.915,29.662 -2020-05-01 12:15:00,56.13,78.738,8.915,29.662 -2020-05-01 12:30:00,51.09,77.492,8.915,29.662 -2020-05-01 12:45:00,50.81,77.333,8.915,29.662 -2020-05-01 13:00:00,49.83,78.273,5.4639999999999995,29.662 -2020-05-01 13:15:00,45.08,78.259,5.4639999999999995,29.662 -2020-05-01 13:30:00,46.19,75.832,5.4639999999999995,29.662 -2020-05-01 13:45:00,47.46,74.252,5.4639999999999995,29.662 -2020-05-01 14:00:00,49.01,76.367,3.2939999999999996,29.662 -2020-05-01 14:15:00,48.35,75.575,3.2939999999999996,29.662 -2020-05-01 14:30:00,45.56,75.003,3.2939999999999996,29.662 -2020-05-01 14:45:00,48.43,74.546,3.2939999999999996,29.662 -2020-05-01 15:00:00,49.06,75.21,4.689,29.662 -2020-05-01 15:15:00,49.67,73.595,4.689,29.662 -2020-05-01 15:30:00,51.56,72.592,4.689,29.662 -2020-05-01 15:45:00,53.04,71.998,4.689,29.662 -2020-05-01 16:00:00,55.11,72.08,7.732,29.662 -2020-05-01 16:15:00,59.46,71.52,7.732,29.662 -2020-05-01 16:30:00,62.16,72.118,7.732,29.662 -2020-05-01 16:45:00,62.3,69.263,7.732,29.662 -2020-05-01 17:00:00,67.83,70.095,17.558,29.662 -2020-05-01 17:15:00,72.29,71.555,17.558,29.662 -2020-05-01 17:30:00,72.68,72.794,17.558,29.662 -2020-05-01 17:45:00,73.94,74.653,17.558,29.662 -2020-05-01 18:00:00,75.38,75.821,24.763,29.662 -2020-05-01 18:15:00,74.58,78.516,24.763,29.662 -2020-05-01 18:30:00,75.18,77.906,24.763,29.662 -2020-05-01 18:45:00,75.67,80.471,24.763,29.662 -2020-05-01 19:00:00,79.11,79.535,29.633000000000003,29.662 -2020-05-01 19:15:00,84.51,78.283,29.633000000000003,29.662 -2020-05-01 19:30:00,85.92,78.82,29.633000000000003,29.662 -2020-05-01 19:45:00,82.48,79.994,29.633000000000003,29.662 -2020-05-01 20:00:00,83.91,80.059,38.826,29.662 -2020-05-01 20:15:00,81.16,79.312,38.826,29.662 -2020-05-01 20:30:00,79.77,77.578,38.826,29.662 -2020-05-01 20:45:00,88.48,76.283,38.826,29.662 -2020-05-01 21:00:00,88.98,74.173,37.751,29.662 -2020-05-01 21:15:00,88.97,75.696,37.751,29.662 -2020-05-01 21:30:00,78.96,76.152,37.751,29.662 -2020-05-01 21:45:00,81.17,76.479,37.751,29.662 -2020-05-01 22:00:00,73.75,76.163,39.799,29.662 -2020-05-01 22:15:00,80.27,74.783,39.799,29.662 -2020-05-01 22:30:00,81.01,72.372,39.799,29.662 -2020-05-01 22:45:00,80.12,70.10600000000001,39.799,29.662 -2020-05-01 23:00:00,80.75,64.072,33.686,29.662 -2020-05-01 23:15:00,77.21,61.755,33.686,29.662 -2020-05-01 23:30:00,72.86,60.244,33.686,29.662 -2020-05-01 23:45:00,74.22,59.806999999999995,33.686,29.662 -2020-05-02 00:00:00,67.4,48.595,42.833999999999996,29.662 -2020-05-02 00:15:00,71.63,47.126000000000005,42.833999999999996,29.662 -2020-05-02 00:30:00,75.66,46.176,42.833999999999996,29.662 -2020-05-02 00:45:00,76.11,44.898999999999994,42.833999999999996,29.662 -2020-05-02 01:00:00,72.31,44.903999999999996,37.859,29.662 -2020-05-02 01:15:00,70.22,44.23,37.859,29.662 -2020-05-02 01:30:00,73.08,42.45,37.859,29.662 -2020-05-02 01:45:00,73.7,42.513999999999996,37.859,29.662 -2020-05-02 02:00:00,66.85,43.174,35.327,29.662 -2020-05-02 02:15:00,66.49,41.31100000000001,35.327,29.662 -2020-05-02 02:30:00,68.26,43.461000000000006,35.327,29.662 -2020-05-02 02:45:00,66.14,44.196999999999996,35.327,29.662 -2020-05-02 03:00:00,65.71,46.493,34.908,29.662 -2020-05-02 03:15:00,66.15,44.941,34.908,29.662 -2020-05-02 03:30:00,67.61,43.762,34.908,29.662 -2020-05-02 03:45:00,68.7,45.599,34.908,29.662 -2020-05-02 04:00:00,69.79,54.015,34.84,29.662 -2020-05-02 04:15:00,67.03,62.473,34.84,29.662 -2020-05-02 04:30:00,66.17,60.231,34.84,29.662 -2020-05-02 04:45:00,67.69,60.641999999999996,34.84,29.662 -2020-05-02 05:00:00,69.51,76.563,34.222,29.662 -2020-05-02 05:15:00,70.18,88.72,34.222,29.662 -2020-05-02 05:30:00,69.35,79.377,34.222,29.662 -2020-05-02 05:45:00,71.72,76.12100000000001,34.222,29.662 -2020-05-02 06:00:00,73.67,93.12,35.515,29.662 -2020-05-02 06:15:00,73.73,108.242,35.515,29.662 -2020-05-02 06:30:00,75.39,100.70299999999999,35.515,29.662 -2020-05-02 06:45:00,76.42,95.399,35.515,29.662 -2020-05-02 07:00:00,79.28,93.88799999999999,39.687,29.662 -2020-05-02 07:15:00,77.96,93.337,39.687,29.662 -2020-05-02 07:30:00,78.31,91.61399999999999,39.687,29.662 -2020-05-02 07:45:00,79.86,90.62100000000001,39.687,29.662 -2020-05-02 08:00:00,80.34,88.26799999999999,44.9,29.662 -2020-05-02 08:15:00,78.9,89.515,44.9,29.662 -2020-05-02 08:30:00,78.61,87.58,44.9,29.662 -2020-05-02 08:45:00,79.87,88.637,44.9,29.662 -2020-05-02 09:00:00,76.21,83.447,45.724,29.662 -2020-05-02 09:15:00,75.76,83.902,45.724,29.662 -2020-05-02 09:30:00,75.2,86.28399999999999,45.724,29.662 -2020-05-02 09:45:00,75.61,86.314,45.724,29.662 -2020-05-02 10:00:00,75.42,81.66199999999999,43.123999999999995,29.662 -2020-05-02 10:15:00,76.95,82.645,43.123999999999995,29.662 -2020-05-02 10:30:00,77.79,82.12200000000001,43.123999999999995,29.662 -2020-05-02 10:45:00,76.44,82.48899999999999,43.123999999999995,29.662 -2020-05-02 11:00:00,72.26,78.703,40.255,29.662 -2020-05-02 11:15:00,71.56,78.921,40.255,29.662 -2020-05-02 11:30:00,69.64,80.362,40.255,29.662 -2020-05-02 11:45:00,69.91,79.851,40.255,29.662 -2020-05-02 12:00:00,65.59,76.855,38.582,29.662 -2020-05-02 12:15:00,66.22,77.22399999999999,38.582,29.662 -2020-05-02 12:30:00,64.72,76.383,38.582,29.662 -2020-05-02 12:45:00,64.95,77.09,38.582,29.662 -2020-05-02 13:00:00,62.78,78.554,36.043,29.662 -2020-05-02 13:15:00,62.25,77.737,36.043,29.662 -2020-05-02 13:30:00,62.59,76.217,36.043,29.662 -2020-05-02 13:45:00,62.56,74.087,36.043,29.662 -2020-05-02 14:00:00,61.85,75.279,35.216,29.662 -2020-05-02 14:15:00,62.08,73.669,35.216,29.662 -2020-05-02 14:30:00,62.24,73.452,35.216,29.662 -2020-05-02 14:45:00,62.4,73.947,35.216,29.662 -2020-05-02 15:00:00,62.97,75.17399999999999,36.759,29.662 -2020-05-02 15:15:00,63.02,73.821,36.759,29.662 -2020-05-02 15:30:00,63.12,72.676,36.759,29.662 -2020-05-02 15:45:00,64.76,71.574,36.759,29.662 -2020-05-02 16:00:00,68.0,72.26,40.086,29.662 -2020-05-02 16:15:00,68.86,71.86399999999999,40.086,29.662 -2020-05-02 16:30:00,71.22,71.62,40.086,29.662 -2020-05-02 16:45:00,74.4,68.60300000000001,40.086,29.662 -2020-05-02 17:00:00,79.85,69.22399999999999,44.876999999999995,29.662 -2020-05-02 17:15:00,79.81,69.649,44.876999999999995,29.662 -2020-05-02 17:30:00,80.79,70.16199999999999,44.876999999999995,29.662 -2020-05-02 17:45:00,81.12,70.793,44.876999999999995,29.662 -2020-05-02 18:00:00,83.13,71.803,47.056000000000004,29.662 -2020-05-02 18:15:00,81.4,74.316,47.056000000000004,29.662 -2020-05-02 18:30:00,81.48,74.656,47.056000000000004,29.662 -2020-05-02 18:45:00,82.35,76.45,47.056000000000004,29.662 -2020-05-02 19:00:00,81.48,74.001,45.57,29.662 -2020-05-02 19:15:00,79.11,73.41,45.57,29.662 -2020-05-02 19:30:00,78.9,74.202,45.57,29.662 -2020-05-02 19:45:00,79.86,75.236,45.57,29.662 -2020-05-02 20:00:00,81.06,75.092,41.685,29.662 -2020-05-02 20:15:00,80.73,74.097,41.685,29.662 -2020-05-02 20:30:00,80.18,71.314,41.685,29.662 -2020-05-02 20:45:00,80.23,71.797,41.685,29.662 -2020-05-02 21:00:00,75.92,70.756,39.576,29.662 -2020-05-02 21:15:00,76.58,72.755,39.576,29.662 -2020-05-02 21:30:00,73.98,73.593,39.576,29.662 -2020-05-02 21:45:00,73.06,73.62,39.576,29.662 -2020-05-02 22:00:00,69.29,72.173,39.068000000000005,29.662 -2020-05-02 22:15:00,69.76,72.399,39.068000000000005,29.662 -2020-05-02 22:30:00,67.03,71.508,39.068000000000005,29.662 -2020-05-02 22:45:00,66.09,70.529,39.068000000000005,29.662 -2020-05-02 23:00:00,64.59,65.696,32.06,29.662 -2020-05-02 23:15:00,62.92,61.768,32.06,29.662 -2020-05-02 23:30:00,62.13,60.387,32.06,29.662 -2020-05-02 23:45:00,61.12,59.508,32.06,29.662 -2020-05-03 00:00:00,56.25,49.497,28.825,29.662 -2020-05-03 00:15:00,59.42,47.018,28.825,29.662 -2020-05-03 00:30:00,55.48,45.813,28.825,29.662 -2020-05-03 00:45:00,57.9,44.705,28.825,29.662 -2020-05-03 01:00:00,55.98,44.872,25.995,29.662 -2020-05-03 01:15:00,56.5,44.451,25.995,29.662 -2020-05-03 01:30:00,53.94,42.732,25.995,29.662 -2020-05-03 01:45:00,55.87,42.38399999999999,25.995,29.662 -2020-05-03 02:00:00,55.81,42.83,24.394000000000002,29.662 -2020-05-03 02:15:00,56.25,41.198,24.394000000000002,29.662 -2020-05-03 02:30:00,55.72,43.818000000000005,24.394000000000002,29.662 -2020-05-03 02:45:00,55.33,44.498999999999995,24.394000000000002,29.662 -2020-05-03 03:00:00,51.94,47.433,22.916999999999998,29.662 -2020-05-03 03:15:00,55.79,45.878,22.916999999999998,29.662 -2020-05-03 03:30:00,56.34,44.623999999999995,22.916999999999998,29.662 -2020-05-03 03:45:00,56.91,45.833,22.916999999999998,29.662 -2020-05-03 04:00:00,57.32,54.089,23.576999999999998,29.662 -2020-05-03 04:15:00,55.6,61.795,23.576999999999998,29.662 -2020-05-03 04:30:00,55.41,60.622,23.576999999999998,29.662 -2020-05-03 04:45:00,55.04,60.756,23.576999999999998,29.662 -2020-05-03 05:00:00,56.02,75.493,22.730999999999998,29.662 -2020-05-03 05:15:00,56.44,86.085,22.730999999999998,29.662 -2020-05-03 05:30:00,56.1,76.407,22.730999999999998,29.662 -2020-05-03 05:45:00,56.79,73.071,22.730999999999998,29.662 -2020-05-03 06:00:00,57.69,88.141,22.34,29.662 -2020-05-03 06:15:00,57.57,103.32,22.34,29.662 -2020-05-03 06:30:00,58.58,94.846,22.34,29.662 -2020-05-03 06:45:00,59.95,88.39299999999999,22.34,29.662 -2020-05-03 07:00:00,61.08,87.919,24.691999999999997,29.662 -2020-05-03 07:15:00,61.55,85.772,24.691999999999997,29.662 -2020-05-03 07:30:00,61.04,84.58,24.691999999999997,29.662 -2020-05-03 07:45:00,61.04,83.305,24.691999999999997,29.662 -2020-05-03 08:00:00,61.08,82.164,29.340999999999998,29.662 -2020-05-03 08:15:00,62.23,84.333,29.340999999999998,29.662 -2020-05-03 08:30:00,61.79,83.662,29.340999999999998,29.662 -2020-05-03 08:45:00,60.97,85.425,29.340999999999998,29.662 -2020-05-03 09:00:00,59.6,79.984,30.788,29.662 -2020-05-03 09:15:00,59.67,80.277,30.788,29.662 -2020-05-03 09:30:00,59.03,82.943,30.788,29.662 -2020-05-03 09:45:00,59.85,83.73,30.788,29.662 -2020-05-03 10:00:00,60.2,80.434,30.158,29.662 -2020-05-03 10:15:00,61.9,81.768,30.158,29.662 -2020-05-03 10:30:00,63.99,81.663,30.158,29.662 -2020-05-03 10:45:00,64.7,82.06200000000001,30.158,29.662 -2020-05-03 11:00:00,62.24,78.36,32.056,29.662 -2020-05-03 11:15:00,59.36,78.319,32.056,29.662 -2020-05-03 11:30:00,54.19,79.796,32.056,29.662 -2020-05-03 11:45:00,55.34,79.73100000000001,32.056,29.662 -2020-05-03 12:00:00,52.93,77.274,28.671999999999997,29.662 -2020-05-03 12:15:00,52.8,77.979,28.671999999999997,29.662 -2020-05-03 12:30:00,49.69,76.653,28.671999999999997,29.662 -2020-05-03 12:45:00,47.47,76.506,28.671999999999997,29.662 -2020-05-03 13:00:00,45.23,77.5,23.171,29.662 -2020-05-03 13:15:00,43.21,77.488,23.171,29.662 -2020-05-03 13:30:00,45.06,75.075,23.171,29.662 -2020-05-03 13:45:00,46.0,73.49600000000001,23.171,29.662 -2020-05-03 14:00:00,46.8,75.712,19.11,29.662 -2020-05-03 14:15:00,46.46,74.891,19.11,29.662 -2020-05-03 14:30:00,45.78,74.235,19.11,29.662 -2020-05-03 14:45:00,46.36,73.783,19.11,29.662 -2020-05-03 15:00:00,48.14,74.539,19.689,29.662 -2020-05-03 15:15:00,48.74,72.88600000000001,19.689,29.662 -2020-05-03 15:30:00,49.3,71.814,19.689,29.662 -2020-05-03 15:45:00,51.69,71.186,19.689,29.662 -2020-05-03 16:00:00,55.94,71.361,22.875,29.662 -2020-05-03 16:15:00,57.49,70.76100000000001,22.875,29.662 -2020-05-03 16:30:00,59.74,71.37899999999999,22.875,29.662 -2020-05-03 16:45:00,64.35,68.4,22.875,29.662 -2020-05-03 17:00:00,68.62,69.329,33.884,29.662 -2020-05-03 17:15:00,69.55,70.735,33.884,29.662 -2020-05-03 17:30:00,71.62,71.954,33.884,29.662 -2020-05-03 17:45:00,72.87,73.748,33.884,29.662 -2020-05-03 18:00:00,76.29,74.952,38.453,29.662 -2020-05-03 18:15:00,74.34,77.643,38.453,29.662 -2020-05-03 18:30:00,74.52,77.007,38.453,29.662 -2020-05-03 18:45:00,74.5,79.578,38.453,29.662 -2020-05-03 19:00:00,80.75,78.635,39.221,29.662 -2020-05-03 19:15:00,82.66,77.384,39.221,29.662 -2020-05-03 19:30:00,83.29,77.925,39.221,29.662 -2020-05-03 19:45:00,75.88,79.12100000000001,39.221,29.662 -2020-05-03 20:00:00,79.39,79.132,37.871,29.662 -2020-05-03 20:15:00,79.33,78.392,37.871,29.662 -2020-05-03 20:30:00,79.68,76.717,37.871,29.662 -2020-05-03 20:45:00,81.36,75.5,37.871,29.662 -2020-05-03 21:00:00,87.04,73.398,36.465,29.662 -2020-05-03 21:15:00,90.9,74.95,36.465,29.662 -2020-05-03 21:30:00,84.7,75.37,36.465,29.662 -2020-05-03 21:45:00,81.23,75.756,36.465,29.662 -2020-05-03 22:00:00,78.55,75.484,36.092,29.662 -2020-05-03 22:15:00,82.23,74.148,36.092,29.662 -2020-05-03 22:30:00,81.02,71.717,36.092,29.662 -2020-05-03 22:45:00,76.17,69.429,36.092,29.662 -2020-05-03 23:00:00,72.51,63.32899999999999,31.013,29.662 -2020-05-03 23:15:00,74.86,61.097,31.013,29.662 -2020-05-03 23:30:00,75.21,59.583999999999996,31.013,29.662 -2020-05-03 23:45:00,74.48,59.146,31.013,29.662 -2020-05-04 00:00:00,69.6,51.836999999999996,31.174,29.775 -2020-05-04 00:15:00,72.1,51.037,31.174,29.775 -2020-05-04 00:30:00,73.55,49.571999999999996,31.174,29.775 -2020-05-04 00:45:00,72.62,47.968999999999994,31.174,29.775 -2020-05-04 01:00:00,67.74,48.449,29.663,29.775 -2020-05-04 01:15:00,66.22,47.828,29.663,29.775 -2020-05-04 01:30:00,70.56,46.394,29.663,29.775 -2020-05-04 01:45:00,73.57,46.01,29.663,29.775 -2020-05-04 02:00:00,73.5,46.79600000000001,28.793000000000003,29.775 -2020-05-04 02:15:00,69.89,44.87,28.793000000000003,29.775 -2020-05-04 02:30:00,68.85,47.751000000000005,28.793000000000003,29.775 -2020-05-04 02:45:00,73.7,48.104,28.793000000000003,29.775 -2020-05-04 03:00:00,73.78,51.895,27.728,29.775 -2020-05-04 03:15:00,70.78,51.468,27.728,29.775 -2020-05-04 03:30:00,74.92,50.652,27.728,29.775 -2020-05-04 03:45:00,71.42,51.328,27.728,29.775 -2020-05-04 04:00:00,75.17,63.512,29.266,29.775 -2020-05-04 04:15:00,76.56,74.935,29.266,29.775 -2020-05-04 04:30:00,80.37,74.154,29.266,29.775 -2020-05-04 04:45:00,84.87,74.626,29.266,29.775 -2020-05-04 05:00:00,91.28,99.954,37.889,29.775 -2020-05-04 05:15:00,95.44,125.398,37.889,29.775 -2020-05-04 05:30:00,98.76,114.611,37.889,29.775 -2020-05-04 05:45:00,100.67,106.984,37.889,29.775 -2020-05-04 06:00:00,103.98,108.009,55.485,29.775 -2020-05-04 06:15:00,105.43,110.87700000000001,55.485,29.775 -2020-05-04 06:30:00,106.87,108.288,55.485,29.775 -2020-05-04 06:45:00,108.09,109.04,55.485,29.775 -2020-05-04 07:00:00,108.57,110.086,65.765,29.775 -2020-05-04 07:15:00,107.31,110.166,65.765,29.775 -2020-05-04 07:30:00,106.72,107.994,65.765,29.775 -2020-05-04 07:45:00,105.4,106.05799999999999,65.765,29.775 -2020-05-04 08:00:00,104.99,101.705,56.745,29.775 -2020-05-04 08:15:00,103.93,102.205,56.745,29.775 -2020-05-04 08:30:00,104.33,99.277,56.745,29.775 -2020-05-04 08:45:00,103.33,100.006,56.745,29.775 -2020-05-04 09:00:00,102.16,93.492,53.321999999999996,29.775 -2020-05-04 09:15:00,101.67,91.12200000000001,53.321999999999996,29.775 -2020-05-04 09:30:00,101.56,92.71799999999999,53.321999999999996,29.775 -2020-05-04 09:45:00,101.91,91.898,53.321999999999996,29.775 -2020-05-04 10:00:00,101.39,88.725,51.309,29.775 -2020-05-04 10:15:00,101.65,89.86200000000001,51.309,29.775 -2020-05-04 10:30:00,101.66,89.089,51.309,29.775 -2020-05-04 10:45:00,101.33,88.617,51.309,29.775 -2020-05-04 11:00:00,101.22,84.113,50.415,29.775 -2020-05-04 11:15:00,98.86,85.01899999999999,50.415,29.775 -2020-05-04 11:30:00,98.41,87.557,50.415,29.775 -2020-05-04 11:45:00,97.54,87.715,50.415,29.775 -2020-05-04 12:00:00,97.03,84.945,48.273,29.775 -2020-05-04 12:15:00,95.22,85.738,48.273,29.775 -2020-05-04 12:30:00,95.04,83.7,48.273,29.775 -2020-05-04 12:45:00,94.65,84.18700000000001,48.273,29.775 -2020-05-04 13:00:00,93.91,86.016,48.452,29.775 -2020-05-04 13:15:00,94.72,84.79299999999999,48.452,29.775 -2020-05-04 13:30:00,93.9,82.296,48.452,29.775 -2020-05-04 13:45:00,94.22,81.383,48.452,29.775 -2020-05-04 14:00:00,96.6,82.76899999999999,48.35,29.775 -2020-05-04 14:15:00,94.21,82.103,48.35,29.775 -2020-05-04 14:30:00,94.03,81.062,48.35,29.775 -2020-05-04 14:45:00,93.5,82.105,48.35,29.775 -2020-05-04 15:00:00,92.87,83.38600000000001,48.838,29.775 -2020-05-04 15:15:00,93.09,80.695,48.838,29.775 -2020-05-04 15:30:00,94.12,79.807,48.838,29.775 -2020-05-04 15:45:00,96.9,78.626,48.838,29.775 -2020-05-04 16:00:00,97.74,79.515,50.873000000000005,29.775 -2020-05-04 16:15:00,97.79,78.64699999999999,50.873000000000005,29.775 -2020-05-04 16:30:00,102.74,78.398,50.873000000000005,29.775 -2020-05-04 16:45:00,102.67,74.907,50.873000000000005,29.775 -2020-05-04 17:00:00,105.47,74.95100000000001,56.637,29.775 -2020-05-04 17:15:00,105.44,76.24,56.637,29.775 -2020-05-04 17:30:00,106.23,76.945,56.637,29.775 -2020-05-04 17:45:00,107.54,77.82600000000001,56.637,29.775 -2020-05-04 18:00:00,108.37,78.27,56.35,29.775 -2020-05-04 18:15:00,107.47,78.734,56.35,29.775 -2020-05-04 18:30:00,113.16,77.749,56.35,29.775 -2020-05-04 18:45:00,114.56,83.012,56.35,29.775 -2020-05-04 19:00:00,111.01,81.166,56.023,29.775 -2020-05-04 19:15:00,98.89,80.538,56.023,29.775 -2020-05-04 19:30:00,101.05,80.969,56.023,29.775 -2020-05-04 19:45:00,98.85,81.399,56.023,29.775 -2020-05-04 20:00:00,97.88,79.572,62.372,29.775 -2020-05-04 20:15:00,103.11,79.016,62.372,29.775 -2020-05-04 20:30:00,108.62,77.098,62.372,29.775 -2020-05-04 20:45:00,107.25,76.679,62.372,29.775 -2020-05-04 21:00:00,93.21,74.293,57.516999999999996,29.775 -2020-05-04 21:15:00,90.29,75.783,57.516999999999996,29.775 -2020-05-04 21:30:00,87.29,76.21,57.516999999999996,29.775 -2020-05-04 21:45:00,93.2,76.271,57.516999999999996,29.775 -2020-05-04 22:00:00,88.85,73.393,51.823,29.775 -2020-05-04 22:15:00,88.88,73.15899999999999,51.823,29.775 -2020-05-04 22:30:00,83.03,63.908,51.823,29.775 -2020-05-04 22:45:00,81.21,59.86,51.823,29.775 -2020-05-04 23:00:00,80.18,54.06100000000001,43.832,29.775 -2020-05-04 23:15:00,81.63,51.527,43.832,29.775 -2020-05-04 23:30:00,78.05,50.97,43.832,29.775 -2020-05-04 23:45:00,75.22,51.051,43.832,29.775 -2020-05-05 00:00:00,76.0,49.474,42.371,29.775 -2020-05-05 00:15:00,79.2,49.869,42.371,29.775 -2020-05-05 00:30:00,77.68,48.63,42.371,29.775 -2020-05-05 00:45:00,75.02,47.31100000000001,42.371,29.775 -2020-05-05 01:00:00,77.48,47.3,39.597,29.775 -2020-05-05 01:15:00,78.55,46.585,39.597,29.775 -2020-05-05 01:30:00,78.09,45.091,39.597,29.775 -2020-05-05 01:45:00,74.26,44.409,39.597,29.775 -2020-05-05 02:00:00,77.89,44.806999999999995,38.298,29.775 -2020-05-05 02:15:00,78.97,43.738,38.298,29.775 -2020-05-05 02:30:00,73.18,46.115,38.298,29.775 -2020-05-05 02:45:00,74.65,46.74,38.298,29.775 -2020-05-05 03:00:00,72.05,49.676,37.884,29.775 -2020-05-05 03:15:00,72.97,49.666000000000004,37.884,29.775 -2020-05-05 03:30:00,81.18,48.996,37.884,29.775 -2020-05-05 03:45:00,86.82,48.886,37.884,29.775 -2020-05-05 04:00:00,86.35,59.956,39.442,29.775 -2020-05-05 04:15:00,80.15,71.218,39.442,29.775 -2020-05-05 04:30:00,82.91,70.225,39.442,29.775 -2020-05-05 04:45:00,86.29,71.503,39.442,29.775 -2020-05-05 05:00:00,93.14,99.67200000000001,43.608000000000004,29.775 -2020-05-05 05:15:00,96.1,125.415,43.608000000000004,29.775 -2020-05-05 05:30:00,99.08,114.31700000000001,43.608000000000004,29.775 -2020-05-05 05:45:00,102.21,106.15799999999999,43.608000000000004,29.775 -2020-05-05 06:00:00,105.73,107.665,54.99100000000001,29.775 -2020-05-05 06:15:00,105.97,111.215,54.99100000000001,29.775 -2020-05-05 06:30:00,106.51,108.149,54.99100000000001,29.775 -2020-05-05 06:45:00,106.79,108.066,54.99100000000001,29.775 -2020-05-05 07:00:00,108.05,109.16,66.217,29.775 -2020-05-05 07:15:00,107.67,108.979,66.217,29.775 -2020-05-05 07:30:00,106.25,106.595,66.217,29.775 -2020-05-05 07:45:00,104.55,103.98200000000001,66.217,29.775 -2020-05-05 08:00:00,102.47,99.61399999999999,60.151,29.775 -2020-05-05 08:15:00,101.53,99.43299999999999,60.151,29.775 -2020-05-05 08:30:00,101.82,96.552,60.151,29.775 -2020-05-05 08:45:00,102.85,96.48899999999999,60.151,29.775 -2020-05-05 09:00:00,103.32,89.926,53.873000000000005,29.775 -2020-05-05 09:15:00,101.69,88.057,53.873000000000005,29.775 -2020-05-05 09:30:00,99.95,90.448,53.873000000000005,29.775 -2020-05-05 09:45:00,102.43,90.609,53.873000000000005,29.775 -2020-05-05 10:00:00,101.34,86.17399999999999,51.417,29.775 -2020-05-05 10:15:00,102.24,86.82799999999999,51.417,29.775 -2020-05-05 10:30:00,102.25,86.163,51.417,29.775 -2020-05-05 10:45:00,102.01,86.572,51.417,29.775 -2020-05-05 11:00:00,98.68,82.649,50.43600000000001,29.775 -2020-05-05 11:15:00,99.53,83.743,50.43600000000001,29.775 -2020-05-05 11:30:00,98.28,84.958,50.43600000000001,29.775 -2020-05-05 11:45:00,97.89,85.07700000000001,50.43600000000001,29.775 -2020-05-05 12:00:00,97.44,81.642,47.468,29.775 -2020-05-05 12:15:00,100.02,82.514,47.468,29.775 -2020-05-05 12:30:00,104.48,81.36,47.468,29.775 -2020-05-05 12:45:00,103.25,82.234,47.468,29.775 -2020-05-05 13:00:00,98.15,83.67200000000001,48.453,29.775 -2020-05-05 13:15:00,97.89,83.62799999999999,48.453,29.775 -2020-05-05 13:30:00,98.63,81.594,48.453,29.775 -2020-05-05 13:45:00,96.77,80.05199999999999,48.453,29.775 -2020-05-05 14:00:00,101.19,81.928,48.435,29.775 -2020-05-05 14:15:00,102.3,81.167,48.435,29.775 -2020-05-05 14:30:00,106.02,80.582,48.435,29.775 -2020-05-05 14:45:00,96.9,81.017,48.435,29.775 -2020-05-05 15:00:00,96.46,82.051,49.966,29.775 -2020-05-05 15:15:00,96.66,80.153,49.966,29.775 -2020-05-05 15:30:00,101.42,79.169,49.966,29.775 -2020-05-05 15:45:00,102.58,78.059,49.966,29.775 -2020-05-05 16:00:00,102.74,78.64699999999999,51.184,29.775 -2020-05-05 16:15:00,101.99,78.009,51.184,29.775 -2020-05-05 16:30:00,104.08,77.745,51.184,29.775 -2020-05-05 16:45:00,110.91,74.844,51.184,29.775 -2020-05-05 17:00:00,111.11,75.308,56.138999999999996,29.775 -2020-05-05 17:15:00,110.09,76.896,56.138999999999996,29.775 -2020-05-05 17:30:00,109.87,77.51899999999999,56.138999999999996,29.775 -2020-05-05 17:45:00,109.83,78.067,56.138999999999996,29.775 -2020-05-05 18:00:00,116.18,77.743,57.038000000000004,29.775 -2020-05-05 18:15:00,114.49,79.166,57.038000000000004,29.775 -2020-05-05 18:30:00,112.35,77.851,57.038000000000004,29.775 -2020-05-05 18:45:00,106.65,83.26299999999999,57.038000000000004,29.775 -2020-05-05 19:00:00,104.1,80.51100000000001,56.492,29.775 -2020-05-05 19:15:00,101.34,79.92399999999999,56.492,29.775 -2020-05-05 19:30:00,108.09,79.952,56.492,29.775 -2020-05-05 19:45:00,108.89,80.669,56.492,29.775 -2020-05-05 20:00:00,106.25,79.186,62.534,29.775 -2020-05-05 20:15:00,101.8,77.277,62.534,29.775 -2020-05-05 20:30:00,98.86,75.78699999999999,62.534,29.775 -2020-05-05 20:45:00,99.69,75.52600000000001,62.534,29.775 -2020-05-05 21:00:00,99.28,73.567,55.506,29.775 -2020-05-05 21:15:00,98.12,74.248,55.506,29.775 -2020-05-05 21:30:00,92.99,74.533,55.506,29.775 -2020-05-05 21:45:00,88.48,74.859,55.506,29.775 -2020-05-05 22:00:00,87.66,72.74600000000001,51.472,29.775 -2020-05-05 22:15:00,89.2,72.176,51.472,29.775 -2020-05-05 22:30:00,85.37,63.177,51.472,29.775 -2020-05-05 22:45:00,84.36,59.214,51.472,29.775 -2020-05-05 23:00:00,81.88,52.781000000000006,44.593,29.775 -2020-05-05 23:15:00,80.29,51.196000000000005,44.593,29.775 -2020-05-05 23:30:00,81.46,50.50899999999999,44.593,29.775 -2020-05-05 23:45:00,77.25,50.575,44.593,29.775 -2020-05-06 00:00:00,71.85,49.147,41.978,29.775 -2020-05-06 00:15:00,78.55,49.553000000000004,41.978,29.775 -2020-05-06 00:30:00,77.81,48.31,41.978,29.775 -2020-05-06 00:45:00,77.29,46.997,41.978,29.775 -2020-05-06 01:00:00,73.94,46.992,38.59,29.775 -2020-05-06 01:15:00,78.49,46.251999999999995,38.59,29.775 -2020-05-06 01:30:00,78.56,44.74100000000001,38.59,29.775 -2020-05-06 01:45:00,76.62,44.058,38.59,29.775 -2020-05-06 02:00:00,73.33,44.446999999999996,36.23,29.775 -2020-05-06 02:15:00,78.33,43.364,36.23,29.775 -2020-05-06 02:30:00,77.82,45.753,36.23,29.775 -2020-05-06 02:45:00,72.36,46.386,36.23,29.775 -2020-05-06 03:00:00,70.81,49.335,35.867,29.775 -2020-05-06 03:15:00,72.19,49.305,35.867,29.775 -2020-05-06 03:30:00,73.52,48.63399999999999,35.867,29.775 -2020-05-06 03:45:00,75.1,48.548,35.867,29.775 -2020-05-06 04:00:00,78.39,59.571999999999996,36.75,29.775 -2020-05-06 04:15:00,78.83,70.78699999999999,36.75,29.775 -2020-05-06 04:30:00,81.3,69.78699999999999,36.75,29.775 -2020-05-06 04:45:00,84.96,71.058,36.75,29.775 -2020-05-06 05:00:00,93.15,99.10799999999999,40.461,29.775 -2020-05-06 05:15:00,95.53,124.71799999999999,40.461,29.775 -2020-05-06 05:30:00,97.79,113.664,40.461,29.775 -2020-05-06 05:45:00,101.14,105.565,40.461,29.775 -2020-05-06 06:00:00,106.4,107.095,55.481,29.775 -2020-05-06 06:15:00,106.6,110.62100000000001,55.481,29.775 -2020-05-06 06:30:00,108.89,107.556,55.481,29.775 -2020-05-06 06:45:00,109.94,107.48100000000001,55.481,29.775 -2020-05-06 07:00:00,114.39,108.568,68.45,29.775 -2020-05-06 07:15:00,107.66,108.384,68.45,29.775 -2020-05-06 07:30:00,105.69,105.969,68.45,29.775 -2020-05-06 07:45:00,106.74,103.374,68.45,29.775 -2020-05-06 08:00:00,103.47,99.00200000000001,60.885,29.775 -2020-05-06 08:15:00,101.98,98.86399999999999,60.885,29.775 -2020-05-06 08:30:00,104.01,95.965,60.885,29.775 -2020-05-06 08:45:00,102.48,95.925,60.885,29.775 -2020-05-06 09:00:00,101.52,89.361,56.887,29.775 -2020-05-06 09:15:00,101.65,87.49799999999999,56.887,29.775 -2020-05-06 09:30:00,101.44,89.904,56.887,29.775 -2020-05-06 09:45:00,104.08,90.09700000000001,56.887,29.775 -2020-05-06 10:00:00,109.12,85.67200000000001,54.401,29.775 -2020-05-06 10:15:00,110.04,86.36399999999999,54.401,29.775 -2020-05-06 10:30:00,109.63,85.71600000000001,54.401,29.775 -2020-05-06 10:45:00,105.76,86.14200000000001,54.401,29.775 -2020-05-06 11:00:00,105.77,82.212,53.678000000000004,29.775 -2020-05-06 11:15:00,105.27,83.325,53.678000000000004,29.775 -2020-05-06 11:30:00,108.22,84.535,53.678000000000004,29.775 -2020-05-06 11:45:00,101.7,84.671,53.678000000000004,29.775 -2020-05-06 12:00:00,98.23,81.267,51.68,29.775 -2020-05-06 12:15:00,98.72,82.146,51.68,29.775 -2020-05-06 12:30:00,99.41,80.953,51.68,29.775 -2020-05-06 12:45:00,104.66,81.832,51.68,29.775 -2020-05-06 13:00:00,100.13,83.296,51.263000000000005,29.775 -2020-05-06 13:15:00,102.95,83.25200000000001,51.263000000000005,29.775 -2020-05-06 13:30:00,97.84,81.226,51.263000000000005,29.775 -2020-05-06 13:45:00,96.57,79.685,51.263000000000005,29.775 -2020-05-06 14:00:00,96.82,81.609,51.107,29.775 -2020-05-06 14:15:00,95.84,80.835,51.107,29.775 -2020-05-06 14:30:00,96.81,80.208,51.107,29.775 -2020-05-06 14:45:00,93.98,80.645,51.107,29.775 -2020-05-06 15:00:00,94.2,81.723,51.498000000000005,29.775 -2020-05-06 15:15:00,93.38,79.808,51.498000000000005,29.775 -2020-05-06 15:30:00,95.59,78.79,51.498000000000005,29.775 -2020-05-06 15:45:00,98.49,77.665,51.498000000000005,29.775 -2020-05-06 16:00:00,101.69,78.296,53.376999999999995,29.775 -2020-05-06 16:15:00,101.79,77.641,53.376999999999995,29.775 -2020-05-06 16:30:00,102.02,77.387,53.376999999999995,29.775 -2020-05-06 16:45:00,100.4,74.425,53.376999999999995,29.775 -2020-05-06 17:00:00,102.39,74.937,56.965,29.775 -2020-05-06 17:15:00,101.21,76.498,56.965,29.775 -2020-05-06 17:30:00,100.01,77.111,56.965,29.775 -2020-05-06 17:45:00,102.1,77.626,56.965,29.775 -2020-05-06 18:00:00,104.0,77.32,58.231,29.775 -2020-05-06 18:15:00,104.34,78.742,58.231,29.775 -2020-05-06 18:30:00,105.4,77.414,58.231,29.775 -2020-05-06 18:45:00,103.72,82.82700000000001,58.231,29.775 -2020-05-06 19:00:00,102.23,80.072,58.865,29.775 -2020-05-06 19:15:00,97.93,79.486,58.865,29.775 -2020-05-06 19:30:00,95.7,79.515,58.865,29.775 -2020-05-06 19:45:00,96.66,80.242,58.865,29.775 -2020-05-06 20:00:00,95.25,78.734,65.605,29.775 -2020-05-06 20:15:00,98.04,76.829,65.605,29.775 -2020-05-06 20:30:00,91.55,75.367,65.605,29.775 -2020-05-06 20:45:00,91.56,75.143,65.605,29.775 -2020-05-06 21:00:00,82.99,73.189,58.083999999999996,29.775 -2020-05-06 21:15:00,81.12,73.88600000000001,58.083999999999996,29.775 -2020-05-06 21:30:00,78.05,74.15100000000001,58.083999999999996,29.775 -2020-05-06 21:45:00,75.34,74.505,58.083999999999996,29.775 -2020-05-06 22:00:00,70.55,72.414,53.243,29.775 -2020-05-06 22:15:00,70.4,71.865,53.243,29.775 -2020-05-06 22:30:00,68.32,62.855,53.243,29.775 -2020-05-06 22:45:00,66.71,58.88,53.243,29.775 -2020-05-06 23:00:00,76.44,52.417,44.283,29.775 -2020-05-06 23:15:00,77.93,50.872,44.283,29.775 -2020-05-06 23:30:00,80.4,50.187,44.283,29.775 -2020-05-06 23:45:00,79.81,50.251999999999995,44.283,29.775 -2020-05-07 00:00:00,73.78,48.823,40.219,29.775 -2020-05-07 00:15:00,72.71,49.239,40.219,29.775 -2020-05-07 00:30:00,73.87,47.994,40.219,29.775 -2020-05-07 00:45:00,77.01,46.685,40.219,29.775 -2020-05-07 01:00:00,74.22,46.685,37.959,29.775 -2020-05-07 01:15:00,73.3,45.92100000000001,37.959,29.775 -2020-05-07 01:30:00,72.97,44.391999999999996,37.959,29.775 -2020-05-07 01:45:00,74.22,43.708999999999996,37.959,29.775 -2020-05-07 02:00:00,75.92,44.092,36.113,29.775 -2020-05-07 02:15:00,74.19,42.993,36.113,29.775 -2020-05-07 02:30:00,72.07,45.396,36.113,29.775 -2020-05-07 02:45:00,74.15,46.036,36.113,29.775 -2020-05-07 03:00:00,74.4,48.995,35.546,29.775 -2020-05-07 03:15:00,77.16,48.946000000000005,35.546,29.775 -2020-05-07 03:30:00,76.61,48.275,35.546,29.775 -2020-05-07 03:45:00,78.26,48.214,35.546,29.775 -2020-05-07 04:00:00,79.9,59.19,37.169000000000004,29.775 -2020-05-07 04:15:00,80.7,70.359,37.169000000000004,29.775 -2020-05-07 04:30:00,83.75,69.352,37.169000000000004,29.775 -2020-05-07 04:45:00,87.24,70.615,37.169000000000004,29.775 -2020-05-07 05:00:00,95.0,98.54899999999999,41.233000000000004,29.775 -2020-05-07 05:15:00,97.6,124.024,41.233000000000004,29.775 -2020-05-07 05:30:00,100.93,113.01700000000001,41.233000000000004,29.775 -2020-05-07 05:45:00,104.36,104.975,41.233000000000004,29.775 -2020-05-07 06:00:00,111.07,106.531,52.57,29.775 -2020-05-07 06:15:00,111.79,110.03200000000001,52.57,29.775 -2020-05-07 06:30:00,115.27,106.96700000000001,52.57,29.775 -2020-05-07 06:45:00,116.41,106.9,52.57,29.775 -2020-05-07 07:00:00,121.35,107.98,64.53,29.775 -2020-05-07 07:15:00,120.69,107.794,64.53,29.775 -2020-05-07 07:30:00,120.64,105.34899999999999,64.53,29.775 -2020-05-07 07:45:00,119.58,102.771,64.53,29.775 -2020-05-07 08:00:00,118.88,98.395,55.911,29.775 -2020-05-07 08:15:00,119.04,98.3,55.911,29.775 -2020-05-07 08:30:00,120.65,95.383,55.911,29.775 -2020-05-07 08:45:00,121.31,95.367,55.911,29.775 -2020-05-07 09:00:00,120.75,88.802,50.949,29.775 -2020-05-07 09:15:00,121.94,86.946,50.949,29.775 -2020-05-07 09:30:00,124.2,89.363,50.949,29.775 -2020-05-07 09:45:00,125.86,89.59,50.949,29.775 -2020-05-07 10:00:00,123.35,85.17299999999999,48.136,29.775 -2020-05-07 10:15:00,121.67,85.905,48.136,29.775 -2020-05-07 10:30:00,122.7,85.274,48.136,29.775 -2020-05-07 10:45:00,121.37,85.71700000000001,48.136,29.775 -2020-05-07 11:00:00,120.01,81.78,46.643,29.775 -2020-05-07 11:15:00,119.7,82.912,46.643,29.775 -2020-05-07 11:30:00,116.44,84.117,46.643,29.775 -2020-05-07 11:45:00,120.55,84.266,46.643,29.775 -2020-05-07 12:00:00,114.26,80.895,44.098,29.775 -2020-05-07 12:15:00,115.33,81.78,44.098,29.775 -2020-05-07 12:30:00,115.25,80.548,44.098,29.775 -2020-05-07 12:45:00,111.95,81.433,44.098,29.775 -2020-05-07 13:00:00,111.66,82.92299999999999,43.717,29.775 -2020-05-07 13:15:00,115.79,82.87899999999999,43.717,29.775 -2020-05-07 13:30:00,112.84,80.861,43.717,29.775 -2020-05-07 13:45:00,113.94,79.32,43.717,29.775 -2020-05-07 14:00:00,112.73,81.293,44.218999999999994,29.775 -2020-05-07 14:15:00,110.34,80.506,44.218999999999994,29.775 -2020-05-07 14:30:00,109.63,79.837,44.218999999999994,29.775 -2020-05-07 14:45:00,110.46,80.277,44.218999999999994,29.775 -2020-05-07 15:00:00,109.7,81.399,46.159,29.775 -2020-05-07 15:15:00,111.05,79.46600000000001,46.159,29.775 -2020-05-07 15:30:00,108.64,78.415,46.159,29.775 -2020-05-07 15:45:00,109.14,77.273,46.159,29.775 -2020-05-07 16:00:00,109.78,77.95100000000001,47.115,29.775 -2020-05-07 16:15:00,111.93,77.275,47.115,29.775 -2020-05-07 16:30:00,114.03,77.032,47.115,29.775 -2020-05-07 16:45:00,114.16,74.009,47.115,29.775 -2020-05-07 17:00:00,114.59,74.568,50.827,29.775 -2020-05-07 17:15:00,113.02,76.104,50.827,29.775 -2020-05-07 17:30:00,114.7,76.706,50.827,29.775 -2020-05-07 17:45:00,114.23,77.19,50.827,29.775 -2020-05-07 18:00:00,114.07,76.90100000000001,52.586000000000006,29.775 -2020-05-07 18:15:00,110.01,78.32,52.586000000000006,29.775 -2020-05-07 18:30:00,114.3,76.979,52.586000000000006,29.775 -2020-05-07 18:45:00,113.58,82.39299999999999,52.586000000000006,29.775 -2020-05-07 19:00:00,112.14,79.639,51.886,29.775 -2020-05-07 19:15:00,106.35,79.051,51.886,29.775 -2020-05-07 19:30:00,105.41,79.083,51.886,29.775 -2020-05-07 19:45:00,106.24,79.819,51.886,29.775 -2020-05-07 20:00:00,105.1,78.285,56.162,29.775 -2020-05-07 20:15:00,103.7,76.383,56.162,29.775 -2020-05-07 20:30:00,103.6,74.949,56.162,29.775 -2020-05-07 20:45:00,103.67,74.764,56.162,29.775 -2020-05-07 21:00:00,98.44,72.814,53.023,29.775 -2020-05-07 21:15:00,98.45,73.525,53.023,29.775 -2020-05-07 21:30:00,93.13,73.773,53.023,29.775 -2020-05-07 21:45:00,94.44,74.152,53.023,29.775 -2020-05-07 22:00:00,88.82,72.084,49.303999999999995,29.775 -2020-05-07 22:15:00,86.68,71.556,49.303999999999995,29.775 -2020-05-07 22:30:00,83.38,62.534,49.303999999999995,29.775 -2020-05-07 22:45:00,84.4,58.549,49.303999999999995,29.775 -2020-05-07 23:00:00,58.37,52.053999999999995,43.409,29.775 -2020-05-07 23:15:00,57.74,50.552,43.409,29.775 -2020-05-07 23:30:00,55.82,49.86600000000001,43.409,29.775 -2020-05-07 23:45:00,54.77,49.93,43.409,29.775 -2020-05-08 00:00:00,53.24,46.693000000000005,39.884,29.775 -2020-05-08 00:15:00,54.19,47.361999999999995,39.884,29.775 -2020-05-08 00:30:00,54.32,46.278999999999996,39.884,29.775 -2020-05-08 00:45:00,55.58,45.352,39.884,29.775 -2020-05-08 01:00:00,52.76,44.941,37.658,29.775 -2020-05-08 01:15:00,53.95,44.012,37.658,29.775 -2020-05-08 01:30:00,54.34,42.949,37.658,29.775 -2020-05-08 01:45:00,53.53,42.105,37.658,29.775 -2020-05-08 02:00:00,52.96,43.25,36.707,29.775 -2020-05-08 02:15:00,54.39,42.055,36.707,29.775 -2020-05-08 02:30:00,54.97,45.31399999999999,36.707,29.775 -2020-05-08 02:45:00,54.2,45.437,36.707,29.775 -2020-05-08 03:00:00,55.31,48.655,37.025,29.775 -2020-05-08 03:15:00,56.15,47.978,37.025,29.775 -2020-05-08 03:30:00,57.39,47.113,37.025,29.775 -2020-05-08 03:45:00,59.16,47.886,37.025,29.775 -2020-05-08 04:00:00,61.87,59.01,38.349000000000004,29.775 -2020-05-08 04:15:00,63.45,68.811,38.349000000000004,29.775 -2020-05-08 04:30:00,66.08,68.631,38.349000000000004,29.775 -2020-05-08 04:45:00,68.24,68.958,38.349000000000004,29.775 -2020-05-08 05:00:00,72.24,95.93,41.565,29.775 -2020-05-08 05:15:00,73.47,122.565,41.565,29.775 -2020-05-08 05:30:00,76.45,112.135,41.565,29.775 -2020-05-08 05:45:00,79.96,103.76799999999999,41.565,29.775 -2020-05-08 06:00:00,84.65,105.696,53.861000000000004,29.775 -2020-05-08 06:15:00,86.18,108.787,53.861000000000004,29.775 -2020-05-08 06:30:00,88.73,105.376,53.861000000000004,29.775 -2020-05-08 06:45:00,89.53,105.786,53.861000000000004,29.775 -2020-05-08 07:00:00,93.58,107.083,63.497,29.775 -2020-05-08 07:15:00,92.57,107.986,63.497,29.775 -2020-05-08 07:30:00,92.28,103.915,63.497,29.775 -2020-05-08 07:45:00,92.57,100.859,63.497,29.775 -2020-05-08 08:00:00,92.12,96.67299999999999,55.43899999999999,29.775 -2020-05-08 08:15:00,93.47,96.991,55.43899999999999,29.775 -2020-05-08 08:30:00,95.97,94.354,55.43899999999999,29.775 -2020-05-08 08:45:00,98.27,93.648,55.43899999999999,29.775 -2020-05-08 09:00:00,97.16,85.48,52.132,29.775 -2020-05-08 09:15:00,97.44,85.31700000000001,52.132,29.775 -2020-05-08 09:30:00,96.52,87.052,52.132,29.775 -2020-05-08 09:45:00,93.66,87.565,52.132,29.775 -2020-05-08 10:00:00,90.57,82.54700000000001,49.881,29.775 -2020-05-08 10:15:00,90.43,83.43799999999999,49.881,29.775 -2020-05-08 10:30:00,92.89,83.205,49.881,29.775 -2020-05-08 10:45:00,92.64,83.412,49.881,29.775 -2020-05-08 11:00:00,87.14,79.635,49.396,29.775 -2020-05-08 11:15:00,84.45,79.61,49.396,29.775 -2020-05-08 11:30:00,82.3,81.271,49.396,29.775 -2020-05-08 11:45:00,84.56,80.74600000000001,49.396,29.775 -2020-05-08 12:00:00,85.34,78.204,46.7,29.775 -2020-05-08 12:15:00,87.59,77.72800000000001,46.7,29.775 -2020-05-08 12:30:00,82.79,76.611,46.7,29.775 -2020-05-08 12:45:00,84.03,77.154,46.7,29.775 -2020-05-08 13:00:00,86.69,79.491,44.05,29.775 -2020-05-08 13:15:00,90.34,79.928,44.05,29.775 -2020-05-08 13:30:00,92.17,78.503,44.05,29.775 -2020-05-08 13:45:00,91.91,77.16,44.05,29.775 -2020-05-08 14:00:00,91.0,78.111,42.805,29.775 -2020-05-08 14:15:00,87.13,77.55199999999999,42.805,29.775 -2020-05-08 14:30:00,85.29,78.155,42.805,29.775 -2020-05-08 14:45:00,85.31,78.226,42.805,29.775 -2020-05-08 15:00:00,81.82,79.128,44.36600000000001,29.775 -2020-05-08 15:15:00,80.11,76.816,44.36600000000001,29.775 -2020-05-08 15:30:00,79.41,74.672,44.36600000000001,29.775 -2020-05-08 15:45:00,79.63,74.131,44.36600000000001,29.775 -2020-05-08 16:00:00,80.31,73.779,46.928999999999995,29.775 -2020-05-08 16:15:00,83.58,73.572,46.928999999999995,29.775 -2020-05-08 16:30:00,85.81,73.242,46.928999999999995,29.775 -2020-05-08 16:45:00,88.21,69.52600000000001,46.928999999999995,29.775 -2020-05-08 17:00:00,90.9,71.516,51.468,29.775 -2020-05-08 17:15:00,90.29,72.729,51.468,29.775 -2020-05-08 17:30:00,91.83,73.315,51.468,29.775 -2020-05-08 17:45:00,93.13,73.54899999999999,51.468,29.775 -2020-05-08 18:00:00,95.38,73.646,52.58,29.775 -2020-05-08 18:15:00,93.36,74.184,52.58,29.775 -2020-05-08 18:30:00,94.59,72.903,52.58,29.775 -2020-05-08 18:45:00,91.5,78.64,52.58,29.775 -2020-05-08 19:00:00,88.14,76.949,52.183,29.775 -2020-05-08 19:15:00,85.24,77.32600000000001,52.183,29.775 -2020-05-08 19:30:00,84.68,77.257,52.183,29.775 -2020-05-08 19:45:00,85.55,77.033,52.183,29.775 -2020-05-08 20:00:00,86.18,75.357,58.497,29.775 -2020-05-08 20:15:00,86.8,74.078,58.497,29.775 -2020-05-08 20:30:00,85.42,72.279,58.497,29.775 -2020-05-08 20:45:00,84.39,71.708,58.497,29.775 -2020-05-08 21:00:00,79.14,70.98,54.731,29.775 -2020-05-08 21:15:00,78.08,73.17699999999999,54.731,29.775 -2020-05-08 21:30:00,75.44,73.305,54.731,29.775 -2020-05-08 21:45:00,73.8,74.07300000000001,54.731,29.775 -2020-05-08 22:00:00,69.96,72.279,51.386,29.775 -2020-05-08 22:15:00,70.53,71.528,51.386,29.775 -2020-05-08 22:30:00,67.66,68.579,51.386,29.775 -2020-05-08 22:45:00,66.61,66.559,51.386,29.775 -2020-05-08 23:00:00,62.1,61.192,44.463,29.775 -2020-05-08 23:15:00,61.96,57.783,44.463,29.775 -2020-05-08 23:30:00,61.24,55.128,44.463,29.775 -2020-05-08 23:45:00,61.9,54.85,44.463,29.775 -2020-05-09 00:00:00,59.19,46.31399999999999,42.833999999999996,29.662 -2020-05-09 00:15:00,59.48,44.915,42.833999999999996,29.662 -2020-05-09 00:30:00,58.27,43.941,42.833999999999996,29.662 -2020-05-09 00:45:00,58.41,42.702,42.833999999999996,29.662 -2020-05-09 01:00:00,56.44,42.744,37.859,29.662 -2020-05-09 01:15:00,57.02,41.898999999999994,37.859,29.662 -2020-05-09 01:30:00,54.39,39.994,37.859,29.662 -2020-05-09 01:45:00,57.37,40.054,37.859,29.662 -2020-05-09 02:00:00,56.57,40.662,35.327,29.662 -2020-05-09 02:15:00,57.32,38.695,35.327,29.662 -2020-05-09 02:30:00,56.09,40.938,35.327,29.662 -2020-05-09 02:45:00,56.5,41.725,35.327,29.662 -2020-05-09 03:00:00,56.74,44.101000000000006,34.908,29.662 -2020-05-09 03:15:00,57.04,42.413999999999994,34.908,29.662 -2020-05-09 03:30:00,57.73,41.23,34.908,29.662 -2020-05-09 03:45:00,58.65,43.236000000000004,34.908,29.662 -2020-05-09 04:00:00,56.1,51.325,34.84,29.662 -2020-05-09 04:15:00,58.39,59.455,34.84,29.662 -2020-05-09 04:30:00,58.62,57.167,34.84,29.662 -2020-05-09 04:45:00,58.93,57.523,34.84,29.662 -2020-05-09 05:00:00,59.13,72.622,34.222,29.662 -2020-05-09 05:15:00,58.29,83.84100000000001,34.222,29.662 -2020-05-09 05:30:00,61.65,74.814,34.222,29.662 -2020-05-09 05:45:00,64.49,71.96600000000001,34.222,29.662 -2020-05-09 06:00:00,67.16,89.139,35.515,29.662 -2020-05-09 06:15:00,68.9,104.09200000000001,35.515,29.662 -2020-05-09 06:30:00,67.17,96.552,35.515,29.662 -2020-05-09 06:45:00,72.7,91.306,35.515,29.662 -2020-05-09 07:00:00,71.59,89.74700000000001,39.687,29.662 -2020-05-09 07:15:00,75.04,89.178,39.687,29.662 -2020-05-09 07:30:00,77.56,87.242,39.687,29.662 -2020-05-09 07:45:00,81.67,86.36399999999999,39.687,29.662 -2020-05-09 08:00:00,83.62,83.985,44.9,29.662 -2020-05-09 08:15:00,84.5,85.53399999999999,44.9,29.662 -2020-05-09 08:30:00,84.0,83.471,44.9,29.662 -2020-05-09 08:45:00,81.6,84.695,44.9,29.662 -2020-05-09 09:00:00,79.98,79.499,45.724,29.662 -2020-05-09 09:15:00,82.69,79.999,45.724,29.662 -2020-05-09 09:30:00,76.88,82.469,45.724,29.662 -2020-05-09 09:45:00,80.32,82.73200000000001,45.724,29.662 -2020-05-09 10:00:00,74.68,78.143,43.123999999999995,29.662 -2020-05-09 10:15:00,75.87,79.402,43.123999999999995,29.662 -2020-05-09 10:30:00,75.83,78.999,43.123999999999995,29.662 -2020-05-09 10:45:00,76.14,79.484,43.123999999999995,29.662 -2020-05-09 11:00:00,72.08,75.645,40.255,29.662 -2020-05-09 11:15:00,70.62,75.997,40.255,29.662 -2020-05-09 11:30:00,73.05,77.406,40.255,29.662 -2020-05-09 11:45:00,75.89,76.999,40.255,29.662 -2020-05-09 12:00:00,76.78,74.229,38.582,29.662 -2020-05-09 12:15:00,71.51,74.643,38.582,29.662 -2020-05-09 12:30:00,73.06,73.529,38.582,29.662 -2020-05-09 12:45:00,72.57,74.273,38.582,29.662 -2020-05-09 13:00:00,70.0,75.922,36.043,29.662 -2020-05-09 13:15:00,68.54,75.10600000000001,36.043,29.662 -2020-05-09 13:30:00,62.42,73.637,36.043,29.662 -2020-05-09 13:45:00,66.38,71.518,36.043,29.662 -2020-05-09 14:00:00,70.51,73.04899999999999,35.216,29.662 -2020-05-09 14:15:00,77.12,71.347,35.216,29.662 -2020-05-09 14:30:00,70.75,70.837,35.216,29.662 -2020-05-09 14:45:00,67.87,71.348,35.216,29.662 -2020-05-09 15:00:00,69.68,72.88600000000001,36.759,29.662 -2020-05-09 15:15:00,71.0,71.407,36.759,29.662 -2020-05-09 15:30:00,71.64,70.027,36.759,29.662 -2020-05-09 15:45:00,74.81,68.809,36.759,29.662 -2020-05-09 16:00:00,75.54,69.814,40.086,29.662 -2020-05-09 16:15:00,71.72,69.282,40.086,29.662 -2020-05-09 16:30:00,75.34,69.111,40.086,29.662 -2020-05-09 16:45:00,76.17,65.67,40.086,29.662 -2020-05-09 17:00:00,77.72,66.625,44.876999999999995,29.662 -2020-05-09 17:15:00,79.16,66.865,44.876999999999995,29.662 -2020-05-09 17:30:00,79.96,67.304,44.876999999999995,29.662 -2020-05-09 17:45:00,80.99,67.714,44.876999999999995,29.662 -2020-05-09 18:00:00,80.0,68.844,47.056000000000004,29.662 -2020-05-09 18:15:00,82.8,71.342,47.056000000000004,29.662 -2020-05-09 18:30:00,81.66,71.592,47.056000000000004,29.662 -2020-05-09 18:45:00,82.41,73.399,47.056000000000004,29.662 -2020-05-09 19:00:00,80.39,70.936,45.57,29.662 -2020-05-09 19:15:00,77.08,70.343,45.57,29.662 -2020-05-09 19:30:00,74.71,71.15100000000001,45.57,29.662 -2020-05-09 19:45:00,78.23,72.253,45.57,29.662 -2020-05-09 20:00:00,78.77,71.929,41.685,29.662 -2020-05-09 20:15:00,79.72,70.954,41.685,29.662 -2020-05-09 20:30:00,78.87,68.374,41.685,29.662 -2020-05-09 20:45:00,78.23,69.122,41.685,29.662 -2020-05-09 21:00:00,76.02,68.109,39.576,29.662 -2020-05-09 21:15:00,74.79,70.21300000000001,39.576,29.662 -2020-05-09 21:30:00,70.34,70.922,39.576,29.662 -2020-05-09 21:45:00,72.16,71.14399999999999,39.576,29.662 -2020-05-09 22:00:00,69.44,69.848,39.068000000000005,29.662 -2020-05-09 22:15:00,66.81,70.223,39.068000000000005,29.662 -2020-05-09 22:30:00,65.84,69.253,39.068000000000005,29.662 -2020-05-09 22:45:00,65.64,68.2,39.068000000000005,29.662 -2020-05-09 23:00:00,58.1,63.147,32.06,29.662 -2020-05-09 23:15:00,61.56,59.51,32.06,29.662 -2020-05-09 23:30:00,59.59,58.13,32.06,29.662 -2020-05-09 23:45:00,61.11,57.242,32.06,29.662 -2020-05-10 00:00:00,56.26,47.231,28.825,29.662 -2020-05-10 00:15:00,56.19,44.821000000000005,28.825,29.662 -2020-05-10 00:30:00,56.2,43.593999999999994,28.825,29.662 -2020-05-10 00:45:00,56.93,42.525,28.825,29.662 -2020-05-10 01:00:00,54.42,42.729,25.995,29.662 -2020-05-10 01:15:00,55.57,42.137,25.995,29.662 -2020-05-10 01:30:00,54.8,40.296,25.995,29.662 -2020-05-10 01:45:00,54.72,39.944,25.995,29.662 -2020-05-10 02:00:00,53.8,40.336,24.394000000000002,29.662 -2020-05-10 02:15:00,53.93,38.604,24.394000000000002,29.662 -2020-05-10 02:30:00,54.27,41.316,24.394000000000002,29.662 -2020-05-10 02:45:00,53.34,42.047,24.394000000000002,29.662 -2020-05-10 03:00:00,53.35,45.059,22.916999999999998,29.662 -2020-05-10 03:15:00,53.77,43.37,22.916999999999998,29.662 -2020-05-10 03:30:00,55.05,42.111999999999995,22.916999999999998,29.662 -2020-05-10 03:45:00,55.75,43.489,22.916999999999998,29.662 -2020-05-10 04:00:00,53.43,51.42,23.576999999999998,29.662 -2020-05-10 04:15:00,53.2,58.799,23.576999999999998,29.662 -2020-05-10 04:30:00,52.95,57.58,23.576999999999998,29.662 -2020-05-10 04:45:00,53.86,57.659,23.576999999999998,29.662 -2020-05-10 05:00:00,53.48,71.578,22.730999999999998,29.662 -2020-05-10 05:15:00,54.32,81.235,22.730999999999998,29.662 -2020-05-10 05:30:00,53.89,71.875,22.730999999999998,29.662 -2020-05-10 05:45:00,54.74,68.944,22.730999999999998,29.662 -2020-05-10 06:00:00,56.84,84.185,22.34,29.662 -2020-05-10 06:15:00,56.42,99.195,22.34,29.662 -2020-05-10 06:30:00,57.71,90.72200000000001,22.34,29.662 -2020-05-10 06:45:00,58.61,84.32700000000001,22.34,29.662 -2020-05-10 07:00:00,59.27,83.804,24.691999999999997,29.662 -2020-05-10 07:15:00,59.67,81.642,24.691999999999997,29.662 -2020-05-10 07:30:00,59.68,80.24,24.691999999999997,29.662 -2020-05-10 07:45:00,60.06,79.082,24.691999999999997,29.662 -2020-05-10 08:00:00,59.38,77.918,29.340999999999998,29.662 -2020-05-10 08:15:00,59.29,80.388,29.340999999999998,29.662 -2020-05-10 08:30:00,59.52,79.59,29.340999999999998,29.662 -2020-05-10 08:45:00,58.7,81.51899999999999,29.340999999999998,29.662 -2020-05-10 09:00:00,56.6,76.072,30.788,29.662 -2020-05-10 09:15:00,57.08,76.41,30.788,29.662 -2020-05-10 09:30:00,57.77,79.16199999999999,30.788,29.662 -2020-05-10 09:45:00,60.07,80.18,30.788,29.662 -2020-05-10 10:00:00,59.67,76.94800000000001,30.158,29.662 -2020-05-10 10:15:00,60.32,78.554,30.158,29.662 -2020-05-10 10:30:00,60.97,78.569,30.158,29.662 -2020-05-10 10:45:00,61.22,79.084,30.158,29.662 -2020-05-10 11:00:00,58.46,75.331,32.056,29.662 -2020-05-10 11:15:00,58.18,75.423,32.056,29.662 -2020-05-10 11:30:00,55.24,76.867,32.056,29.662 -2020-05-10 11:45:00,55.54,76.905,32.056,29.662 -2020-05-10 12:00:00,53.33,74.672,28.671999999999997,29.662 -2020-05-10 12:15:00,54.51,75.421,28.671999999999997,29.662 -2020-05-10 12:30:00,50.53,73.825,28.671999999999997,29.662 -2020-05-10 12:45:00,50.54,73.71300000000001,28.671999999999997,29.662 -2020-05-10 13:00:00,47.4,74.889,23.171,29.662 -2020-05-10 13:15:00,45.4,74.87899999999999,23.171,29.662 -2020-05-10 13:30:00,46.65,72.51899999999999,23.171,29.662 -2020-05-10 13:45:00,48.47,70.95,23.171,29.662 -2020-05-10 14:00:00,49.27,73.501,19.11,29.662 -2020-05-10 14:15:00,48.56,72.59100000000001,19.11,29.662 -2020-05-10 14:30:00,47.46,71.642,19.11,29.662 -2020-05-10 14:45:00,47.02,71.206,19.11,29.662 -2020-05-10 15:00:00,46.47,72.26899999999999,19.689,29.662 -2020-05-10 15:15:00,45.86,70.492,19.689,29.662 -2020-05-10 15:30:00,47.51,69.187,19.689,29.662 -2020-05-10 15:45:00,49.12,68.445,19.689,29.662 -2020-05-10 16:00:00,54.26,68.936,22.875,29.662 -2020-05-10 16:15:00,59.43,68.202,22.875,29.662 -2020-05-10 16:30:00,61.12,68.893,22.875,29.662 -2020-05-10 16:45:00,64.01,65.495,22.875,29.662 -2020-05-10 17:00:00,66.15,66.755,33.884,29.662 -2020-05-10 17:15:00,65.55,67.977,33.884,29.662 -2020-05-10 17:30:00,68.01,69.122,33.884,29.662 -2020-05-10 17:45:00,69.14,70.696,33.884,29.662 -2020-05-10 18:00:00,71.22,72.01899999999999,38.453,29.662 -2020-05-10 18:15:00,71.38,74.692,38.453,29.662 -2020-05-10 18:30:00,71.35,73.967,38.453,29.662 -2020-05-10 18:45:00,72.02,76.55,38.453,29.662 -2020-05-10 19:00:00,71.05,75.596,39.221,29.662 -2020-05-10 19:15:00,71.32,74.34100000000001,39.221,29.662 -2020-05-10 19:30:00,76.1,74.89699999999999,39.221,29.662 -2020-05-10 19:45:00,79.65,76.161,39.221,29.662 -2020-05-10 20:00:00,78.39,75.993,37.871,29.662 -2020-05-10 20:15:00,77.6,75.27199999999999,37.871,29.662 -2020-05-10 20:30:00,79.29,73.798,37.871,29.662 -2020-05-10 20:45:00,79.19,72.845,37.871,29.662 -2020-05-10 21:00:00,79.14,70.771,36.465,29.662 -2020-05-10 21:15:00,79.16,72.43,36.465,29.662 -2020-05-10 21:30:00,76.86,72.72,36.465,29.662 -2020-05-10 21:45:00,77.71,73.296,36.465,29.662 -2020-05-10 22:00:00,72.23,73.175,36.092,29.662 -2020-05-10 22:15:00,73.28,71.986,36.092,29.662 -2020-05-10 22:30:00,71.29,69.47399999999999,36.092,29.662 -2020-05-10 22:45:00,72.19,67.111,36.092,29.662 -2020-05-10 23:00:00,66.3,60.795,31.013,29.662 -2020-05-10 23:15:00,67.7,58.853,31.013,29.662 -2020-05-10 23:30:00,66.26,57.341,31.013,29.662 -2020-05-10 23:45:00,66.87,56.893,31.013,29.662 -2020-05-11 00:00:00,68.36,49.586999999999996,31.174,29.775 -2020-05-11 00:15:00,71.71,48.857,31.174,29.775 -2020-05-11 00:30:00,71.93,47.37,31.174,29.775 -2020-05-11 00:45:00,68.14,45.806999999999995,31.174,29.775 -2020-05-11 01:00:00,62.71,46.325,29.663,29.775 -2020-05-11 01:15:00,63.59,45.534,29.663,29.775 -2020-05-11 01:30:00,63.09,43.979,29.663,29.775 -2020-05-11 01:45:00,66.91,43.59,29.663,29.775 -2020-05-11 02:00:00,71.05,44.325,28.793000000000003,29.775 -2020-05-11 02:15:00,71.22,42.299,28.793000000000003,29.775 -2020-05-11 02:30:00,67.29,45.268,28.793000000000003,29.775 -2020-05-11 02:45:00,64.47,45.673,28.793000000000003,29.775 -2020-05-11 03:00:00,65.55,49.538999999999994,27.728,29.775 -2020-05-11 03:15:00,66.79,48.979,27.728,29.775 -2020-05-11 03:30:00,67.95,48.162,27.728,29.775 -2020-05-11 03:45:00,74.12,49.006,27.728,29.775 -2020-05-11 04:00:00,80.04,60.865,29.266,29.775 -2020-05-11 04:15:00,81.67,71.962,29.266,29.775 -2020-05-11 04:30:00,80.33,71.131,29.266,29.775 -2020-05-11 04:45:00,81.18,71.55199999999999,29.266,29.775 -2020-05-11 05:00:00,89.03,96.06299999999999,37.889,29.775 -2020-05-11 05:15:00,93.69,120.575,37.889,29.775 -2020-05-11 05:30:00,95.84,110.10700000000001,37.889,29.775 -2020-05-11 05:45:00,103.12,102.885,37.889,29.775 -2020-05-11 06:00:00,109.88,104.07799999999999,55.485,29.775 -2020-05-11 06:15:00,110.72,106.779,55.485,29.775 -2020-05-11 06:30:00,107.23,104.19200000000001,55.485,29.775 -2020-05-11 06:45:00,105.05,105.00299999999999,55.485,29.775 -2020-05-11 07:00:00,107.77,106.0,65.765,29.775 -2020-05-11 07:15:00,109.31,106.066,65.765,29.775 -2020-05-11 07:30:00,106.64,103.68700000000001,65.765,29.775 -2020-05-11 07:45:00,108.08,101.87200000000001,65.765,29.775 -2020-05-11 08:00:00,112.88,97.49700000000001,56.745,29.775 -2020-05-11 08:15:00,110.39,98.29700000000001,56.745,29.775 -2020-05-11 08:30:00,108.45,95.244,56.745,29.775 -2020-05-11 08:45:00,105.26,96.137,56.745,29.775 -2020-05-11 09:00:00,105.56,89.617,53.321999999999996,29.775 -2020-05-11 09:15:00,103.65,87.292,53.321999999999996,29.775 -2020-05-11 09:30:00,103.56,88.97200000000001,53.321999999999996,29.775 -2020-05-11 09:45:00,106.22,88.381,53.321999999999996,29.775 -2020-05-11 10:00:00,102.45,85.273,51.309,29.775 -2020-05-11 10:15:00,103.67,86.678,51.309,29.775 -2020-05-11 10:30:00,103.32,86.024,51.309,29.775 -2020-05-11 10:45:00,101.26,85.666,51.309,29.775 -2020-05-11 11:00:00,99.43,81.113,50.415,29.775 -2020-05-11 11:15:00,98.15,82.15100000000001,50.415,29.775 -2020-05-11 11:30:00,98.64,84.656,50.415,29.775 -2020-05-11 11:45:00,96.79,84.916,50.415,29.775 -2020-05-11 12:00:00,96.52,82.369,48.273,29.775 -2020-05-11 12:15:00,96.68,83.20299999999999,48.273,29.775 -2020-05-11 12:30:00,95.66,80.89699999999999,48.273,29.775 -2020-05-11 12:45:00,95.11,81.42,48.273,29.775 -2020-05-11 13:00:00,94.53,83.427,48.452,29.775 -2020-05-11 13:15:00,101.79,82.205,48.452,29.775 -2020-05-11 13:30:00,101.81,79.762,48.452,29.775 -2020-05-11 13:45:00,100.1,78.861,48.452,29.775 -2020-05-11 14:00:00,93.14,80.579,48.35,29.775 -2020-05-11 14:15:00,93.8,79.82300000000001,48.35,29.775 -2020-05-11 14:30:00,92.72,78.492,48.35,29.775 -2020-05-11 14:45:00,95.44,79.55,48.35,29.775 -2020-05-11 15:00:00,93.18,81.13600000000001,48.838,29.775 -2020-05-11 15:15:00,91.76,78.321,48.838,29.775 -2020-05-11 15:30:00,93.36,77.204,48.838,29.775 -2020-05-11 15:45:00,94.72,75.90899999999999,48.838,29.775 -2020-05-11 16:00:00,95.88,77.11399999999999,50.873000000000005,29.775 -2020-05-11 16:15:00,96.96,76.11,50.873000000000005,29.775 -2020-05-11 16:30:00,99.03,75.937,50.873000000000005,29.775 -2020-05-11 16:45:00,101.19,72.03,50.873000000000005,29.775 -2020-05-11 17:00:00,103.98,72.402,56.637,29.775 -2020-05-11 17:15:00,104.09,73.51,56.637,29.775 -2020-05-11 17:30:00,105.88,74.139,56.637,29.775 -2020-05-11 17:45:00,105.66,74.80199999999999,56.637,29.775 -2020-05-11 18:00:00,106.65,75.36399999999999,56.35,29.775 -2020-05-11 18:15:00,105.32,75.806,56.35,29.775 -2020-05-11 18:30:00,112.4,74.733,56.35,29.775 -2020-05-11 18:45:00,113.9,80.008,56.35,29.775 -2020-05-11 19:00:00,103.24,78.153,56.023,29.775 -2020-05-11 19:15:00,99.4,77.52,56.023,29.775 -2020-05-11 19:30:00,100.03,77.965,56.023,29.775 -2020-05-11 19:45:00,99.47,78.46300000000001,56.023,29.775 -2020-05-11 20:00:00,100.76,76.457,62.372,29.775 -2020-05-11 20:15:00,98.44,75.921,62.372,29.775 -2020-05-11 20:30:00,97.74,74.20100000000001,62.372,29.775 -2020-05-11 20:45:00,96.87,74.044,62.372,29.775 -2020-05-11 21:00:00,95.03,71.687,57.516999999999996,29.775 -2020-05-11 21:15:00,93.92,73.28399999999999,57.516999999999996,29.775 -2020-05-11 21:30:00,95.99,73.58,57.516999999999996,29.775 -2020-05-11 21:45:00,94.93,73.828,57.516999999999996,29.775 -2020-05-11 22:00:00,89.61,71.101,51.823,29.775 -2020-05-11 22:15:00,82.35,71.01100000000001,51.823,29.775 -2020-05-11 22:30:00,85.45,61.678999999999995,51.823,29.775 -2020-05-11 22:45:00,85.91,57.555,51.823,29.775 -2020-05-11 23:00:00,82.2,51.543,43.832,29.775 -2020-05-11 23:15:00,78.11,49.298,43.832,29.775 -2020-05-11 23:30:00,80.04,48.743,43.832,29.775 -2020-05-11 23:45:00,81.09,48.815,43.832,29.775 -2020-05-12 00:00:00,78.75,47.239,42.371,29.775 -2020-05-12 00:15:00,74.73,47.705,42.371,29.775 -2020-05-12 00:30:00,75.86,46.446000000000005,42.371,29.775 -2020-05-12 00:45:00,79.25,45.168,42.371,29.775 -2020-05-12 01:00:00,77.81,45.195,39.597,29.775 -2020-05-12 01:15:00,76.91,44.31100000000001,39.597,29.775 -2020-05-12 01:30:00,71.67,42.696999999999996,39.597,29.775 -2020-05-12 01:45:00,78.35,42.011,39.597,29.775 -2020-05-12 02:00:00,78.21,42.357,38.298,29.775 -2020-05-12 02:15:00,77.57,41.191,38.298,29.775 -2020-05-12 02:30:00,70.31,43.653,38.298,29.775 -2020-05-12 02:45:00,70.29,44.328,38.298,29.775 -2020-05-12 03:00:00,71.96,47.341,37.884,29.775 -2020-05-12 03:15:00,73.12,47.2,37.884,29.775 -2020-05-12 03:30:00,74.65,46.527,37.884,29.775 -2020-05-12 03:45:00,76.43,46.586000000000006,37.884,29.775 -2020-05-12 04:00:00,82.71,57.331,39.442,29.775 -2020-05-12 04:15:00,87.45,68.267,39.442,29.775 -2020-05-12 04:30:00,90.35,67.226,39.442,29.775 -2020-05-12 04:45:00,87.36,68.453,39.442,29.775 -2020-05-12 05:00:00,92.61,95.80799999999999,43.608000000000004,29.775 -2020-05-12 05:15:00,95.01,120.62100000000001,43.608000000000004,29.775 -2020-05-12 05:30:00,97.1,109.846,43.608000000000004,29.775 -2020-05-12 05:45:00,99.06,102.089,43.608000000000004,29.775 -2020-05-12 06:00:00,104.0,103.76,54.99100000000001,29.775 -2020-05-12 06:15:00,104.83,107.14399999999999,54.99100000000001,29.775 -2020-05-12 06:30:00,107.38,104.08200000000001,54.99100000000001,29.775 -2020-05-12 06:45:00,107.97,104.059,54.99100000000001,29.775 -2020-05-12 07:00:00,108.87,105.101,66.217,29.775 -2020-05-12 07:15:00,110.38,104.91,66.217,29.775 -2020-05-12 07:30:00,109.05,102.322,66.217,29.775 -2020-05-12 07:45:00,113.65,99.833,66.217,29.775 -2020-05-12 08:00:00,108.2,95.444,60.151,29.775 -2020-05-12 08:15:00,103.72,95.56299999999999,60.151,29.775 -2020-05-12 08:30:00,103.77,92.559,60.151,29.775 -2020-05-12 08:45:00,104.97,92.65899999999999,60.151,29.775 -2020-05-12 09:00:00,109.3,86.09100000000001,53.873000000000005,29.775 -2020-05-12 09:15:00,106.31,84.264,53.873000000000005,29.775 -2020-05-12 09:30:00,107.3,86.738,53.873000000000005,29.775 -2020-05-12 09:45:00,105.8,87.12700000000001,53.873000000000005,29.775 -2020-05-12 10:00:00,103.34,82.756,51.417,29.775 -2020-05-12 10:15:00,109.67,83.676,51.417,29.775 -2020-05-12 10:30:00,105.75,83.12700000000001,51.417,29.775 -2020-05-12 10:45:00,108.2,83.65,51.417,29.775 -2020-05-12 11:00:00,106.71,79.682,50.43600000000001,29.775 -2020-05-12 11:15:00,107.35,80.905,50.43600000000001,29.775 -2020-05-12 11:30:00,110.29,82.086,50.43600000000001,29.775 -2020-05-12 11:45:00,120.56,82.305,50.43600000000001,29.775 -2020-05-12 12:00:00,127.19,79.093,47.468,29.775 -2020-05-12 12:15:00,125.28,80.00399999999999,47.468,29.775 -2020-05-12 12:30:00,115.59,78.583,47.468,29.775 -2020-05-12 12:45:00,116.61,79.493,47.468,29.775 -2020-05-12 13:00:00,118.57,81.10600000000001,48.453,29.775 -2020-05-12 13:15:00,118.1,81.064,48.453,29.775 -2020-05-12 13:30:00,109.12,79.085,48.453,29.775 -2020-05-12 13:45:00,111.48,77.554,48.453,29.775 -2020-05-12 14:00:00,124.56,79.757,48.435,29.775 -2020-05-12 14:15:00,123.29,78.90899999999999,48.435,29.775 -2020-05-12 14:30:00,118.12,78.03399999999999,48.435,29.775 -2020-05-12 14:45:00,118.02,78.485,48.435,29.775 -2020-05-12 15:00:00,121.42,79.819,49.966,29.775 -2020-05-12 15:15:00,120.43,77.8,49.966,29.775 -2020-05-12 15:30:00,116.21,76.589,49.966,29.775 -2020-05-12 15:45:00,114.12,75.368,49.966,29.775 -2020-05-12 16:00:00,113.27,76.267,51.184,29.775 -2020-05-12 16:15:00,110.03,75.498,51.184,29.775 -2020-05-12 16:30:00,113.31,75.309,51.184,29.775 -2020-05-12 16:45:00,110.3,71.995,51.184,29.775 -2020-05-12 17:00:00,113.37,72.785,56.138999999999996,29.775 -2020-05-12 17:15:00,113.49,74.193,56.138999999999996,29.775 -2020-05-12 17:30:00,116.69,74.74,56.138999999999996,29.775 -2020-05-12 17:45:00,114.28,75.071,56.138999999999996,29.775 -2020-05-12 18:00:00,119.23,74.863,57.038000000000004,29.775 -2020-05-12 18:15:00,118.62,76.265,57.038000000000004,29.775 -2020-05-12 18:30:00,115.63,74.863,57.038000000000004,29.775 -2020-05-12 18:45:00,111.37,80.283,57.038000000000004,29.775 -2020-05-12 19:00:00,112.68,77.525,56.492,29.775 -2020-05-12 19:15:00,112.7,76.933,56.492,29.775 -2020-05-12 19:30:00,108.78,76.973,56.492,29.775 -2020-05-12 19:45:00,103.96,77.756,56.492,29.775 -2020-05-12 20:00:00,102.83,76.097,62.534,29.775 -2020-05-12 20:15:00,99.27,74.208,62.534,29.775 -2020-05-12 20:30:00,100.18,72.914,62.534,29.775 -2020-05-12 20:45:00,106.01,72.91199999999999,62.534,29.775 -2020-05-12 21:00:00,98.91,70.986,55.506,29.775 -2020-05-12 21:15:00,97.97,71.77199999999999,55.506,29.775 -2020-05-12 21:30:00,90.65,71.925,55.506,29.775 -2020-05-12 21:45:00,87.9,72.434,55.506,29.775 -2020-05-12 22:00:00,89.08,70.472,51.472,29.775 -2020-05-12 22:15:00,88.65,70.043,51.472,29.775 -2020-05-12 22:30:00,85.02,60.961000000000006,51.472,29.775 -2020-05-12 22:45:00,79.11,56.92100000000001,51.472,29.775 -2020-05-12 23:00:00,74.3,50.28,44.593,29.775 -2020-05-12 23:15:00,77.56,48.982,44.593,29.775 -2020-05-12 23:30:00,74.7,48.299,44.593,29.775 -2020-05-12 23:45:00,74.02,48.355,44.593,29.775 -2020-05-13 00:00:00,77.65,46.93,41.978,29.775 -2020-05-13 00:15:00,79.78,47.406000000000006,41.978,29.775 -2020-05-13 00:30:00,79.02,46.145,41.978,29.775 -2020-05-13 00:45:00,74.67,44.873000000000005,41.978,29.775 -2020-05-13 01:00:00,70.45,44.906000000000006,38.59,29.775 -2020-05-13 01:15:00,75.07,43.998999999999995,38.59,29.775 -2020-05-13 01:30:00,76.59,42.369,38.59,29.775 -2020-05-13 01:45:00,78.3,41.681000000000004,38.59,29.775 -2020-05-13 02:00:00,73.41,42.021,36.23,29.775 -2020-05-13 02:15:00,70.67,40.841,36.23,29.775 -2020-05-13 02:30:00,69.26,43.31399999999999,36.23,29.775 -2020-05-13 02:45:00,70.7,43.997,36.23,29.775 -2020-05-13 03:00:00,72.64,47.019,35.867,29.775 -2020-05-13 03:15:00,72.13,46.858999999999995,35.867,29.775 -2020-05-13 03:30:00,73.68,46.188,35.867,29.775 -2020-05-13 03:45:00,77.49,46.271,35.867,29.775 -2020-05-13 04:00:00,80.21,56.97,36.75,29.775 -2020-05-13 04:15:00,87.8,67.86,36.75,29.775 -2020-05-13 04:30:00,91.75,66.811,36.75,29.775 -2020-05-13 04:45:00,96.22,68.03,36.75,29.775 -2020-05-13 05:00:00,96.43,95.272,40.461,29.775 -2020-05-13 05:15:00,98.98,119.954,40.461,29.775 -2020-05-13 05:30:00,102.79,109.227,40.461,29.775 -2020-05-13 05:45:00,105.22,101.527,40.461,29.775 -2020-05-13 06:00:00,111.07,103.219,55.481,29.775 -2020-05-13 06:15:00,111.37,106.58,55.481,29.775 -2020-05-13 06:30:00,115.97,103.52,55.481,29.775 -2020-05-13 06:45:00,119.83,103.505,55.481,29.775 -2020-05-13 07:00:00,123.06,104.54,68.45,29.775 -2020-05-13 07:15:00,124.82,104.348,68.45,29.775 -2020-05-13 07:30:00,124.87,101.734,68.45,29.775 -2020-05-13 07:45:00,126.18,99.264,68.45,29.775 -2020-05-13 08:00:00,124.84,94.87200000000001,60.885,29.775 -2020-05-13 08:15:00,125.61,95.03299999999999,60.885,29.775 -2020-05-13 08:30:00,127.05,92.01299999999999,60.885,29.775 -2020-05-13 08:45:00,130.24,92.135,60.885,29.775 -2020-05-13 09:00:00,129.22,85.56700000000001,56.887,29.775 -2020-05-13 09:15:00,132.69,83.745,56.887,29.775 -2020-05-13 09:30:00,132.0,86.23,56.887,29.775 -2020-05-13 09:45:00,133.26,86.65,56.887,29.775 -2020-05-13 10:00:00,132.15,82.288,54.401,29.775 -2020-05-13 10:15:00,133.61,83.245,54.401,29.775 -2020-05-13 10:30:00,132.7,82.713,54.401,29.775 -2020-05-13 10:45:00,129.86,83.25200000000001,54.401,29.775 -2020-05-13 11:00:00,125.77,79.27600000000001,53.678000000000004,29.775 -2020-05-13 11:15:00,124.22,80.518,53.678000000000004,29.775 -2020-05-13 11:30:00,132.15,81.693,53.678000000000004,29.775 -2020-05-13 11:45:00,134.23,81.925,53.678000000000004,29.775 -2020-05-13 12:00:00,133.92,78.745,51.68,29.775 -2020-05-13 12:15:00,126.49,79.66,51.68,29.775 -2020-05-13 12:30:00,126.47,78.203,51.68,29.775 -2020-05-13 12:45:00,126.1,79.117,51.68,29.775 -2020-05-13 13:00:00,122.35,80.75399999999999,51.263000000000005,29.775 -2020-05-13 13:15:00,121.6,80.712,51.263000000000005,29.775 -2020-05-13 13:30:00,115.7,78.74,51.263000000000005,29.775 -2020-05-13 13:45:00,116.2,77.212,51.263000000000005,29.775 -2020-05-13 14:00:00,122.46,79.46,51.107,29.775 -2020-05-13 14:15:00,118.59,78.6,51.107,29.775 -2020-05-13 14:30:00,107.28,77.684,51.107,29.775 -2020-05-13 14:45:00,98.18,78.138,51.107,29.775 -2020-05-13 15:00:00,97.87,79.513,51.498000000000005,29.775 -2020-05-13 15:15:00,105.19,77.479,51.498000000000005,29.775 -2020-05-13 15:30:00,108.25,76.236,51.498000000000005,29.775 -2020-05-13 15:45:00,112.75,74.999,51.498000000000005,29.775 -2020-05-13 16:00:00,115.28,75.942,53.376999999999995,29.775 -2020-05-13 16:15:00,121.08,75.155,53.376999999999995,29.775 -2020-05-13 16:30:00,127.19,74.976,53.376999999999995,29.775 -2020-05-13 16:45:00,124.65,71.605,53.376999999999995,29.775 -2020-05-13 17:00:00,116.0,72.441,56.965,29.775 -2020-05-13 17:15:00,118.64,73.824,56.965,29.775 -2020-05-13 17:30:00,119.77,74.36,56.965,29.775 -2020-05-13 17:45:00,121.03,74.661,56.965,29.775 -2020-05-13 18:00:00,116.2,74.469,58.231,29.775 -2020-05-13 18:15:00,110.61,75.866,58.231,29.775 -2020-05-13 18:30:00,111.68,74.452,58.231,29.775 -2020-05-13 18:45:00,115.74,79.872,58.231,29.775 -2020-05-13 19:00:00,115.72,77.115,58.865,29.775 -2020-05-13 19:15:00,108.85,76.52199999999999,58.865,29.775 -2020-05-13 19:30:00,105.3,76.563,58.865,29.775 -2020-05-13 19:45:00,108.65,77.355,58.865,29.775 -2020-05-13 20:00:00,108.52,75.672,65.605,29.775 -2020-05-13 20:15:00,107.64,73.78399999999999,65.605,29.775 -2020-05-13 20:30:00,103.12,72.518,65.605,29.775 -2020-05-13 20:45:00,101.58,72.55199999999999,65.605,29.775 -2020-05-13 21:00:00,99.72,70.63,58.083999999999996,29.775 -2020-05-13 21:15:00,98.92,71.432,58.083999999999996,29.775 -2020-05-13 21:30:00,96.57,71.565,58.083999999999996,29.775 -2020-05-13 21:45:00,89.44,72.09899999999999,58.083999999999996,29.775 -2020-05-13 22:00:00,85.38,70.156,53.243,29.775 -2020-05-13 22:15:00,89.96,69.749,53.243,29.775 -2020-05-13 22:30:00,86.78,60.653,53.243,29.775 -2020-05-13 22:45:00,85.75,56.601000000000006,53.243,29.775 -2020-05-13 23:00:00,75.39,49.934,44.283,29.775 -2020-05-13 23:15:00,74.31,48.676,44.283,29.775 -2020-05-13 23:30:00,74.53,47.994,44.283,29.775 -2020-05-13 23:45:00,72.86,48.048,44.283,29.775 -2020-05-14 00:00:00,69.32,46.623000000000005,40.219,29.775 -2020-05-14 00:15:00,70.84,47.11,40.219,29.775 -2020-05-14 00:30:00,69.79,45.847,40.219,29.775 -2020-05-14 00:45:00,70.68,44.582,40.219,29.775 -2020-05-14 01:00:00,70.16,44.619,37.959,29.775 -2020-05-14 01:15:00,69.63,43.68899999999999,37.959,29.775 -2020-05-14 01:30:00,70.15,42.043,37.959,29.775 -2020-05-14 01:45:00,70.69,41.355,37.959,29.775 -2020-05-14 02:00:00,69.83,41.687,36.113,29.775 -2020-05-14 02:15:00,70.46,40.495,36.113,29.775 -2020-05-14 02:30:00,71.03,42.979,36.113,29.775 -2020-05-14 02:45:00,70.72,43.668,36.113,29.775 -2020-05-14 03:00:00,71.24,46.7,35.546,29.775 -2020-05-14 03:15:00,72.5,46.523999999999994,35.546,29.775 -2020-05-14 03:30:00,73.71,45.853,35.546,29.775 -2020-05-14 03:45:00,75.65,45.958999999999996,35.546,29.775 -2020-05-14 04:00:00,79.12,56.613,37.169000000000004,29.775 -2020-05-14 04:15:00,79.46,67.456,37.169000000000004,29.775 -2020-05-14 04:30:00,82.89,66.40100000000001,37.169000000000004,29.775 -2020-05-14 04:45:00,85.82,67.613,37.169000000000004,29.775 -2020-05-14 05:00:00,95.94,94.743,41.233000000000004,29.775 -2020-05-14 05:15:00,98.55,119.294,41.233000000000004,29.775 -2020-05-14 05:30:00,99.72,108.61399999999999,41.233000000000004,29.775 -2020-05-14 05:45:00,102.92,100.969,41.233000000000004,29.775 -2020-05-14 06:00:00,110.05,102.68299999999999,52.57,29.775 -2020-05-14 06:15:00,112.59,106.021,52.57,29.775 -2020-05-14 06:30:00,116.11,102.962,52.57,29.775 -2020-05-14 06:45:00,118.57,102.956,52.57,29.775 -2020-05-14 07:00:00,121.86,103.98200000000001,64.53,29.775 -2020-05-14 07:15:00,122.71,103.792,64.53,29.775 -2020-05-14 07:30:00,123.27,101.15100000000001,64.53,29.775 -2020-05-14 07:45:00,125.08,98.7,64.53,29.775 -2020-05-14 08:00:00,124.66,94.306,55.911,29.775 -2020-05-14 08:15:00,124.04,94.51,55.911,29.775 -2020-05-14 08:30:00,126.3,91.473,55.911,29.775 -2020-05-14 08:45:00,127.75,91.618,55.911,29.775 -2020-05-14 09:00:00,125.81,85.04799999999999,50.949,29.775 -2020-05-14 09:15:00,127.78,83.234,50.949,29.775 -2020-05-14 09:30:00,127.51,85.727,50.949,29.775 -2020-05-14 09:45:00,129.58,86.179,50.949,29.775 -2020-05-14 10:00:00,129.11,81.82600000000001,48.136,29.775 -2020-05-14 10:15:00,129.29,82.819,48.136,29.775 -2020-05-14 10:30:00,129.5,82.303,48.136,29.775 -2020-05-14 10:45:00,130.85,82.85600000000001,48.136,29.775 -2020-05-14 11:00:00,127.65,78.875,46.643,29.775 -2020-05-14 11:15:00,126.84,80.135,46.643,29.775 -2020-05-14 11:30:00,124.7,81.304,46.643,29.775 -2020-05-14 11:45:00,126.76,81.54899999999999,46.643,29.775 -2020-05-14 12:00:00,124.85,78.4,44.098,29.775 -2020-05-14 12:15:00,122.87,79.321,44.098,29.775 -2020-05-14 12:30:00,122.4,77.82600000000001,44.098,29.775 -2020-05-14 12:45:00,118.93,78.745,44.098,29.775 -2020-05-14 13:00:00,117.21,80.405,43.717,29.775 -2020-05-14 13:15:00,120.11,80.363,43.717,29.775 -2020-05-14 13:30:00,119.68,78.4,43.717,29.775 -2020-05-14 13:45:00,123.45,76.874,43.717,29.775 -2020-05-14 14:00:00,125.18,79.166,44.218999999999994,29.775 -2020-05-14 14:15:00,124.9,78.294,44.218999999999994,29.775 -2020-05-14 14:30:00,118.8,77.33800000000001,44.218999999999994,29.775 -2020-05-14 14:45:00,111.7,77.794,44.218999999999994,29.775 -2020-05-14 15:00:00,111.77,79.21,46.159,29.775 -2020-05-14 15:15:00,115.84,77.15899999999999,46.159,29.775 -2020-05-14 15:30:00,116.18,75.887,46.159,29.775 -2020-05-14 15:45:00,115.11,74.634,46.159,29.775 -2020-05-14 16:00:00,111.0,75.619,47.115,29.775 -2020-05-14 16:15:00,111.54,74.814,47.115,29.775 -2020-05-14 16:30:00,116.27,74.64699999999999,47.115,29.775 -2020-05-14 16:45:00,116.72,71.22,47.115,29.775 -2020-05-14 17:00:00,114.96,72.101,50.827,29.775 -2020-05-14 17:15:00,108.11,73.459,50.827,29.775 -2020-05-14 17:30:00,108.02,73.984,50.827,29.775 -2020-05-14 17:45:00,108.53,74.25399999999999,50.827,29.775 -2020-05-14 18:00:00,109.63,74.078,52.586000000000006,29.775 -2020-05-14 18:15:00,108.24,75.471,52.586000000000006,29.775 -2020-05-14 18:30:00,113.52,74.045,52.586000000000006,29.775 -2020-05-14 18:45:00,109.72,79.467,52.586000000000006,29.775 -2020-05-14 19:00:00,112.3,76.709,51.886,29.775 -2020-05-14 19:15:00,106.96,76.115,51.886,29.775 -2020-05-14 19:30:00,106.72,76.158,51.886,29.775 -2020-05-14 19:45:00,107.78,76.959,51.886,29.775 -2020-05-14 20:00:00,105.58,75.251,56.162,29.775 -2020-05-14 20:15:00,99.35,73.365,56.162,29.775 -2020-05-14 20:30:00,102.93,72.126,56.162,29.775 -2020-05-14 20:45:00,105.28,72.195,56.162,29.775 -2020-05-14 21:00:00,103.12,70.278,53.023,29.775 -2020-05-14 21:15:00,99.97,71.095,53.023,29.775 -2020-05-14 21:30:00,93.22,71.209,53.023,29.775 -2020-05-14 21:45:00,93.97,71.766,53.023,29.775 -2020-05-14 22:00:00,90.52,69.846,49.303999999999995,29.775 -2020-05-14 22:15:00,90.9,69.455,49.303999999999995,29.775 -2020-05-14 22:30:00,84.63,60.348,49.303999999999995,29.775 -2020-05-14 22:45:00,88.26,56.285,49.303999999999995,29.775 -2020-05-14 23:00:00,82.53,49.589,43.409,29.775 -2020-05-14 23:15:00,81.52,48.373000000000005,43.409,29.775 -2020-05-14 23:30:00,75.59,47.691,43.409,29.775 -2020-05-14 23:45:00,81.48,47.743,43.409,29.775 -2020-05-15 00:00:00,78.17,44.512,39.884,29.775 -2020-05-15 00:15:00,78.44,45.251999999999995,39.884,29.775 -2020-05-15 00:30:00,73.49,44.153,39.884,29.775 -2020-05-15 00:45:00,78.17,43.27,39.884,29.775 -2020-05-15 01:00:00,78.18,42.896,37.658,29.775 -2020-05-15 01:15:00,78.15,41.803000000000004,37.658,29.775 -2020-05-15 01:30:00,71.38,40.624,37.658,29.775 -2020-05-15 01:45:00,77.87,39.775999999999996,37.658,29.775 -2020-05-15 02:00:00,77.41,40.869,36.707,29.775 -2020-05-15 02:15:00,78.35,39.583,36.707,29.775 -2020-05-15 02:30:00,73.66,42.92,36.707,29.775 -2020-05-15 02:45:00,70.89,43.092,36.707,29.775 -2020-05-15 03:00:00,72.47,46.383,37.025,29.775 -2020-05-15 03:15:00,72.41,45.57899999999999,37.025,29.775 -2020-05-15 03:30:00,73.92,44.714,37.025,29.775 -2020-05-15 03:45:00,77.06,45.655,37.025,29.775 -2020-05-15 04:00:00,81.51,56.457,38.349000000000004,29.775 -2020-05-15 04:15:00,79.37,65.932,38.349000000000004,29.775 -2020-05-15 04:30:00,82.59,65.705,38.349000000000004,29.775 -2020-05-15 04:45:00,85.66,65.982,38.349000000000004,29.775 -2020-05-15 05:00:00,95.76,92.154,41.565,29.775 -2020-05-15 05:15:00,97.85,117.869,41.565,29.775 -2020-05-15 05:30:00,101.38,107.76899999999999,41.565,29.775 -2020-05-15 05:45:00,103.44,99.796,41.565,29.775 -2020-05-15 06:00:00,108.63,101.87700000000001,53.861000000000004,29.775 -2020-05-15 06:15:00,111.05,104.807,53.861000000000004,29.775 -2020-05-15 06:30:00,114.06,101.404,53.861000000000004,29.775 -2020-05-15 06:45:00,116.5,101.876,53.861000000000004,29.775 -2020-05-15 07:00:00,118.58,103.118,63.497,29.775 -2020-05-15 07:15:00,119.62,104.01899999999999,63.497,29.775 -2020-05-15 07:30:00,123.22,99.75399999999999,63.497,29.775 -2020-05-15 07:45:00,121.51,96.83,63.497,29.775 -2020-05-15 08:00:00,120.07,92.62799999999999,55.43899999999999,29.775 -2020-05-15 08:15:00,120.52,93.241,55.43899999999999,29.775 -2020-05-15 08:30:00,121.85,90.48700000000001,55.43899999999999,29.775 -2020-05-15 08:45:00,123.22,89.939,55.43899999999999,29.775 -2020-05-15 09:00:00,120.17,81.768,52.132,29.775 -2020-05-15 09:15:00,120.79,81.646,52.132,29.775 -2020-05-15 09:30:00,120.3,83.456,52.132,29.775 -2020-05-15 09:45:00,121.98,84.19,52.132,29.775 -2020-05-15 10:00:00,122.01,79.236,49.881,29.775 -2020-05-15 10:15:00,122.16,80.387,49.881,29.775 -2020-05-15 10:30:00,125.08,80.267,49.881,29.775 -2020-05-15 10:45:00,121.4,80.583,49.881,29.775 -2020-05-15 11:00:00,118.56,76.764,49.396,29.775 -2020-05-15 11:15:00,117.53,76.866,49.396,29.775 -2020-05-15 11:30:00,115.03,78.49,49.396,29.775 -2020-05-15 11:45:00,117.11,78.059,49.396,29.775 -2020-05-15 12:00:00,113.62,75.738,46.7,29.775 -2020-05-15 12:15:00,112.54,75.296,46.7,29.775 -2020-05-15 12:30:00,109.67,73.919,46.7,29.775 -2020-05-15 12:45:00,110.33,74.495,46.7,29.775 -2020-05-15 13:00:00,105.34,76.998,44.05,29.775 -2020-05-15 13:15:00,104.26,77.437,44.05,29.775 -2020-05-15 13:30:00,101.89,76.069,44.05,29.775 -2020-05-15 13:45:00,103.1,74.741,44.05,29.775 -2020-05-15 14:00:00,99.61,76.006,42.805,29.775 -2020-05-15 14:15:00,96.97,75.365,42.805,29.775 -2020-05-15 14:30:00,94.75,75.683,42.805,29.775 -2020-05-15 14:45:00,93.73,75.768,42.805,29.775 -2020-05-15 15:00:00,96.11,76.961,44.36600000000001,29.775 -2020-05-15 15:15:00,94.15,74.533,44.36600000000001,29.775 -2020-05-15 15:30:00,94.02,72.171,44.36600000000001,29.775 -2020-05-15 15:45:00,94.72,71.51899999999999,44.36600000000001,29.775 -2020-05-15 16:00:00,96.97,71.47399999999999,46.928999999999995,29.775 -2020-05-15 16:15:00,96.61,71.137,46.928999999999995,29.775 -2020-05-15 16:30:00,99.35,70.885,46.928999999999995,29.775 -2020-05-15 16:45:00,100.89,66.767,46.928999999999995,29.775 -2020-05-15 17:00:00,107.94,69.078,51.468,29.775 -2020-05-15 17:15:00,103.98,70.115,51.468,29.775 -2020-05-15 17:30:00,103.89,70.623,51.468,29.775 -2020-05-15 17:45:00,105.25,70.645,51.468,29.775 -2020-05-15 18:00:00,106.7,70.852,52.58,29.775 -2020-05-15 18:15:00,104.83,71.363,52.58,29.775 -2020-05-15 18:30:00,111.99,69.997,52.58,29.775 -2020-05-15 18:45:00,112.9,75.74,52.58,29.775 -2020-05-15 19:00:00,111.75,74.04899999999999,52.183,29.775 -2020-05-15 19:15:00,103.98,74.419,52.183,29.775 -2020-05-15 19:30:00,101.92,74.359,52.183,29.775 -2020-05-15 19:45:00,98.71,74.2,52.183,29.775 -2020-05-15 20:00:00,97.62,72.35,58.497,29.775 -2020-05-15 20:15:00,103.82,71.09,58.497,29.775 -2020-05-15 20:30:00,102.75,69.482,58.497,29.775 -2020-05-15 20:45:00,100.04,69.16199999999999,58.497,29.775 -2020-05-15 21:00:00,88.18,68.469,54.731,29.775 -2020-05-15 21:15:00,88.72,70.771,54.731,29.775 -2020-05-15 21:30:00,85.61,70.765,54.731,29.775 -2020-05-15 21:45:00,83.9,71.708,54.731,29.775 -2020-05-15 22:00:00,82.78,70.062,51.386,29.775 -2020-05-15 22:15:00,84.75,69.445,51.386,29.775 -2020-05-15 22:30:00,82.82,66.408,51.386,29.775 -2020-05-15 22:45:00,80.16,64.311,51.386,29.775 -2020-05-15 23:00:00,71.33,58.746,44.463,29.775 -2020-05-15 23:15:00,70.6,55.622,44.463,29.775 -2020-05-15 23:30:00,69.36,52.972,44.463,29.775 -2020-05-15 23:45:00,70.97,52.681000000000004,44.463,29.775 -2020-05-16 00:00:00,74.15,36.794000000000004,42.833999999999996,29.662 -2020-05-16 00:15:00,73.98,35.758,42.833999999999996,29.662 -2020-05-16 00:30:00,71.46,34.895,42.833999999999996,29.662 -2020-05-16 00:45:00,69.22,33.669000000000004,42.833999999999996,29.662 -2020-05-16 01:00:00,72.75,33.125,37.859,29.662 -2020-05-16 01:15:00,75.46,32.548,37.859,29.662 -2020-05-16 01:30:00,69.76,30.769000000000002,37.859,29.662 -2020-05-16 01:45:00,65.29,30.721,37.859,29.662 -2020-05-16 02:00:00,67.91,30.855,35.327,29.662 -2020-05-16 02:15:00,70.65,28.666,35.327,29.662 -2020-05-16 02:30:00,70.38,30.735,35.327,29.662 -2020-05-16 02:45:00,64.65,31.566,35.327,29.662 -2020-05-16 03:00:00,63.24,33.628,34.908,29.662 -2020-05-16 03:15:00,64.63,30.669,34.908,29.662 -2020-05-16 03:30:00,65.46,29.659000000000002,34.908,29.662 -2020-05-16 03:45:00,65.23,31.218000000000004,34.908,29.662 -2020-05-16 04:00:00,62.15,39.24,34.84,29.662 -2020-05-16 04:15:00,61.52,47.104,34.84,29.662 -2020-05-16 04:30:00,61.86,43.983999999999995,34.84,29.662 -2020-05-16 04:45:00,62.99,44.071999999999996,34.84,29.662 -2020-05-16 05:00:00,64.2,57.34,34.222,29.662 -2020-05-16 05:15:00,64.4,64.827,34.222,29.662 -2020-05-16 05:30:00,63.92,55.413999999999994,34.222,29.662 -2020-05-16 05:45:00,66.28,53.871,34.222,29.662 -2020-05-16 06:00:00,69.14,69.734,35.515,29.662 -2020-05-16 06:15:00,69.45,82.39299999999999,35.515,29.662 -2020-05-16 06:30:00,71.81,75.32300000000001,35.515,29.662 -2020-05-16 06:45:00,72.47,70.584,35.515,29.662 -2020-05-16 07:00:00,73.5,68.16199999999999,39.687,29.662 -2020-05-16 07:15:00,74.27,67.108,39.687,29.662 -2020-05-16 07:30:00,75.61,64.484,39.687,29.662 -2020-05-16 07:45:00,78.65,63.838,39.687,29.662 -2020-05-16 08:00:00,77.98,59.902,44.9,29.662 -2020-05-16 08:15:00,77.32,62.077,44.9,29.662 -2020-05-16 08:30:00,76.53,61.327,44.9,29.662 -2020-05-16 08:45:00,77.34,63.946000000000005,44.9,29.662 -2020-05-16 09:00:00,78.57,61.705,45.724,29.662 -2020-05-16 09:15:00,78.98,62.68899999999999,45.724,29.662 -2020-05-16 09:30:00,74.87,65.62899999999999,45.724,29.662 -2020-05-16 09:45:00,76.27,66.71600000000001,45.724,29.662 -2020-05-16 10:00:00,78.95,63.416000000000004,43.123999999999995,29.662 -2020-05-16 10:15:00,81.25,64.873,43.123999999999995,29.662 -2020-05-16 10:30:00,84.8,64.884,43.123999999999995,29.662 -2020-05-16 10:45:00,82.3,65.693,43.123999999999995,29.662 -2020-05-16 11:00:00,82.79,61.979,40.255,29.662 -2020-05-16 11:15:00,83.53,62.608000000000004,40.255,29.662 -2020-05-16 11:30:00,79.48,64.295,40.255,29.662 -2020-05-16 11:45:00,74.97,64.98,40.255,29.662 -2020-05-16 12:00:00,68.37,60.761,38.582,29.662 -2020-05-16 12:15:00,68.21,60.949,38.582,29.662 -2020-05-16 12:30:00,66.19,60.332,38.582,29.662 -2020-05-16 12:45:00,65.99,61.598,38.582,29.662 -2020-05-16 13:00:00,65.29,62.047,36.043,29.662 -2020-05-16 13:15:00,66.66,62.114,36.043,29.662 -2020-05-16 13:30:00,65.2,60.93,36.043,29.662 -2020-05-16 13:45:00,65.57,58.292,36.043,29.662 -2020-05-16 14:00:00,65.77,58.663999999999994,35.216,29.662 -2020-05-16 14:15:00,65.43,56.394,35.216,29.662 -2020-05-16 14:30:00,67.17,56.395,35.216,29.662 -2020-05-16 14:45:00,69.52,56.943000000000005,35.216,29.662 -2020-05-16 15:00:00,68.76,57.736000000000004,36.759,29.662 -2020-05-16 15:15:00,70.34,55.881,36.759,29.662 -2020-05-16 15:30:00,68.44,53.98,36.759,29.662 -2020-05-16 15:45:00,69.99,51.562,36.759,29.662 -2020-05-16 16:00:00,72.09,54.672,40.086,29.662 -2020-05-16 16:15:00,70.51,54.178000000000004,40.086,29.662 -2020-05-16 16:30:00,72.95,53.538000000000004,40.086,29.662 -2020-05-16 16:45:00,72.75,49.865,40.086,29.662 -2020-05-16 17:00:00,75.24,52.239,44.876999999999995,29.662 -2020-05-16 17:15:00,76.28,51.114,44.876999999999995,29.662 -2020-05-16 17:30:00,78.78,50.931000000000004,44.876999999999995,29.662 -2020-05-16 17:45:00,80.15,50.091,44.876999999999995,29.662 -2020-05-16 18:00:00,84.39,54.299,47.056000000000004,29.662 -2020-05-16 18:15:00,81.11,56.321000000000005,47.056000000000004,29.662 -2020-05-16 18:30:00,81.76,55.763000000000005,47.056000000000004,29.662 -2020-05-16 18:45:00,84.97,57.4,47.056000000000004,29.662 -2020-05-16 19:00:00,81.21,57.782,45.57,29.662 -2020-05-16 19:15:00,78.11,56.823,45.57,29.662 -2020-05-16 19:30:00,77.61,57.25899999999999,45.57,29.662 -2020-05-16 19:45:00,76.99,58.424,45.57,29.662 -2020-05-16 20:00:00,76.78,57.797,41.685,29.662 -2020-05-16 20:15:00,77.96,57.528,41.685,29.662 -2020-05-16 20:30:00,77.7,55.493,41.685,29.662 -2020-05-16 20:45:00,79.72,56.248000000000005,41.685,29.662 -2020-05-16 21:00:00,74.4,54.181999999999995,39.576,29.662 -2020-05-16 21:15:00,73.65,57.01,39.576,29.662 -2020-05-16 21:30:00,70.78,58.263000000000005,39.576,29.662 -2020-05-16 21:45:00,71.05,58.636,39.576,29.662 -2020-05-16 22:00:00,66.23,56.073,39.068000000000005,29.662 -2020-05-16 22:15:00,66.59,57.24100000000001,39.068000000000005,29.662 -2020-05-16 22:30:00,64.33,56.806000000000004,39.068000000000005,29.662 -2020-05-16 22:45:00,63.23,55.815,39.068000000000005,29.662 -2020-05-16 23:00:00,59.53,51.784,32.06,29.662 -2020-05-16 23:15:00,59.33,47.72,32.06,29.662 -2020-05-16 23:30:00,58.61,46.766000000000005,32.06,29.662 -2020-05-16 23:45:00,58.55,45.958,32.06,29.662 -2020-05-17 00:00:00,55.99,37.921,28.825,29.662 -2020-05-17 00:15:00,56.4,35.736999999999995,28.825,29.662 -2020-05-17 00:30:00,55.85,34.657,28.825,29.662 -2020-05-17 00:45:00,55.56,33.474000000000004,28.825,29.662 -2020-05-17 01:00:00,53.93,33.169000000000004,25.995,29.662 -2020-05-17 01:15:00,54.71,32.666,25.995,29.662 -2020-05-17 01:30:00,54.49,30.851,25.995,29.662 -2020-05-17 01:45:00,53.85,30.375,25.995,29.662 -2020-05-17 02:00:00,53.53,30.426,24.394000000000002,29.662 -2020-05-17 02:15:00,54.42,28.711,24.394000000000002,29.662 -2020-05-17 02:30:00,54.27,31.146,24.394000000000002,29.662 -2020-05-17 02:45:00,53.9,31.805999999999997,24.394000000000002,29.662 -2020-05-17 03:00:00,54.33,34.554,22.916999999999998,29.662 -2020-05-17 03:15:00,54.73,31.721,22.916999999999998,29.662 -2020-05-17 03:30:00,56.27,30.311,22.916999999999998,29.662 -2020-05-17 03:45:00,54.77,31.133000000000003,22.916999999999998,29.662 -2020-05-17 04:00:00,51.22,39.014,23.576999999999998,29.662 -2020-05-17 04:15:00,51.74,46.203,23.576999999999998,29.662 -2020-05-17 04:30:00,50.38,44.345,23.576999999999998,29.662 -2020-05-17 04:45:00,50.63,44.044,23.576999999999998,29.662 -2020-05-17 05:00:00,49.16,56.647,22.730999999999998,29.662 -2020-05-17 05:15:00,49.68,62.77,22.730999999999998,29.662 -2020-05-17 05:30:00,49.0,53.008,22.730999999999998,29.662 -2020-05-17 05:45:00,49.52,51.318999999999996,22.730999999999998,29.662 -2020-05-17 06:00:00,50.22,64.92699999999999,22.34,29.662 -2020-05-17 06:15:00,50.56,78.017,22.34,29.662 -2020-05-17 06:30:00,50.65,70.086,22.34,29.662 -2020-05-17 06:45:00,51.26,64.217,22.34,29.662 -2020-05-17 07:00:00,51.72,62.525,24.691999999999997,29.662 -2020-05-17 07:15:00,52.05,59.75899999999999,24.691999999999997,29.662 -2020-05-17 07:30:00,52.11,58.062,24.691999999999997,29.662 -2020-05-17 07:45:00,51.69,57.25899999999999,24.691999999999997,29.662 -2020-05-17 08:00:00,52.0,54.4,29.340999999999998,29.662 -2020-05-17 08:15:00,51.74,57.711000000000006,29.340999999999998,29.662 -2020-05-17 08:30:00,50.65,58.111000000000004,29.340999999999998,29.662 -2020-05-17 08:45:00,50.24,61.091,29.340999999999998,29.662 -2020-05-17 09:00:00,49.9,58.648999999999994,30.788,29.662 -2020-05-17 09:15:00,50.08,59.297,30.788,29.662 -2020-05-17 09:30:00,49.35,62.619,30.788,29.662 -2020-05-17 09:45:00,49.73,64.649,30.788,29.662 -2020-05-17 10:00:00,51.82,62.388000000000005,30.158,29.662 -2020-05-17 10:15:00,54.08,64.119,30.158,29.662 -2020-05-17 10:30:00,54.15,64.479,30.158,29.662 -2020-05-17 10:45:00,54.12,65.82300000000001,30.158,29.662 -2020-05-17 11:00:00,50.69,61.978,32.056,29.662 -2020-05-17 11:15:00,49.03,62.24,32.056,29.662 -2020-05-17 11:30:00,44.51,64.207,32.056,29.662 -2020-05-17 11:45:00,46.44,65.27199999999999,32.056,29.662 -2020-05-17 12:00:00,43.04,61.916000000000004,28.671999999999997,29.662 -2020-05-17 12:15:00,42.65,61.971000000000004,28.671999999999997,29.662 -2020-05-17 12:30:00,41.87,61.183,28.671999999999997,29.662 -2020-05-17 12:45:00,41.91,61.653999999999996,28.671999999999997,29.662 -2020-05-17 13:00:00,41.33,61.688,23.171,29.662 -2020-05-17 13:15:00,41.6,61.905,23.171,29.662 -2020-05-17 13:30:00,42.2,59.663000000000004,23.171,29.662 -2020-05-17 13:45:00,41.31,57.903999999999996,23.171,29.662 -2020-05-17 14:00:00,42.39,59.47,19.11,29.662 -2020-05-17 14:15:00,42.99,57.854,19.11,29.662 -2020-05-17 14:30:00,43.09,56.96,19.11,29.662 -2020-05-17 14:45:00,43.63,56.438,19.11,29.662 -2020-05-17 15:00:00,44.32,57.077,19.689,29.662 -2020-05-17 15:15:00,44.44,54.631,19.689,29.662 -2020-05-17 15:30:00,44.86,52.67100000000001,19.689,29.662 -2020-05-17 15:45:00,47.06,50.652,19.689,29.662 -2020-05-17 16:00:00,49.45,52.661,22.875,29.662 -2020-05-17 16:15:00,50.16,52.172,22.875,29.662 -2020-05-17 16:30:00,52.1,52.534,22.875,29.662 -2020-05-17 16:45:00,55.36,48.893,22.875,29.662 -2020-05-17 17:00:00,60.0,51.653,33.884,29.662 -2020-05-17 17:15:00,60.6,51.835,33.884,29.662 -2020-05-17 17:30:00,65.61,52.448,33.884,29.662 -2020-05-17 17:45:00,64.53,52.451,33.884,29.662 -2020-05-17 18:00:00,68.54,57.073,38.453,29.662 -2020-05-17 18:15:00,66.66,58.964,38.453,29.662 -2020-05-17 18:30:00,69.58,57.735,38.453,29.662 -2020-05-17 18:45:00,66.98,59.857,38.453,29.662 -2020-05-17 19:00:00,68.34,62.187,39.221,29.662 -2020-05-17 19:15:00,67.78,60.285,39.221,29.662 -2020-05-17 19:30:00,66.98,60.448,39.221,29.662 -2020-05-17 19:45:00,67.29,61.458999999999996,39.221,29.662 -2020-05-17 20:00:00,68.66,61.008,37.871,29.662 -2020-05-17 20:15:00,68.42,60.806000000000004,37.871,29.662 -2020-05-17 20:30:00,71.45,59.808,37.871,29.662 -2020-05-17 20:45:00,70.32,58.785,37.871,29.662 -2020-05-17 21:00:00,68.35,56.067,36.465,29.662 -2020-05-17 21:15:00,67.62,58.501999999999995,36.465,29.662 -2020-05-17 21:30:00,65.42,59.177,36.465,29.662 -2020-05-17 21:45:00,64.94,59.942,36.465,29.662 -2020-05-17 22:00:00,61.02,59.086000000000006,36.092,29.662 -2020-05-17 22:15:00,61.21,58.535,36.092,29.662 -2020-05-17 22:30:00,59.51,56.996,36.092,29.662 -2020-05-17 22:45:00,58.72,54.622,36.092,29.662 -2020-05-17 23:00:00,70.52,49.718,31.013,29.662 -2020-05-17 23:15:00,70.67,47.265,31.013,29.662 -2020-05-17 23:30:00,71.07,45.965,31.013,29.662 -2020-05-17 23:45:00,71.64,45.471000000000004,31.013,29.662 -2020-05-18 00:00:00,69.55,39.926,31.174,29.775 -2020-05-18 00:15:00,70.53,39.082,31.174,29.775 -2020-05-18 00:30:00,69.8,37.664,31.174,29.775 -2020-05-18 00:45:00,70.41,36.007,31.174,29.775 -2020-05-18 01:00:00,68.21,36.088,29.663,29.775 -2020-05-18 01:15:00,68.54,35.468,29.663,29.775 -2020-05-18 01:30:00,70.13,33.989000000000004,29.663,29.775 -2020-05-18 01:45:00,71.47,33.439,29.663,29.775 -2020-05-18 02:00:00,71.14,33.912,28.793000000000003,29.775 -2020-05-18 02:15:00,70.93,31.521,28.793000000000003,29.775 -2020-05-18 02:30:00,70.27,34.186,28.793000000000003,29.775 -2020-05-18 02:45:00,71.97,34.594,28.793000000000003,29.775 -2020-05-18 03:00:00,71.3,38.082,27.728,29.775 -2020-05-18 03:15:00,70.0,36.243,27.728,29.775 -2020-05-18 03:30:00,70.97,35.424,27.728,29.775 -2020-05-18 03:45:00,73.44,35.727,27.728,29.775 -2020-05-18 04:00:00,77.39,47.346000000000004,29.266,29.775 -2020-05-18 04:15:00,78.56,58.068999999999996,29.266,29.775 -2020-05-18 04:30:00,79.66,56.18600000000001,29.266,29.775 -2020-05-18 04:45:00,83.67,56.26,29.266,29.775 -2020-05-18 05:00:00,91.78,78.305,37.889,29.775 -2020-05-18 05:15:00,95.73,97.86399999999999,37.889,29.775 -2020-05-18 05:30:00,97.75,86.74700000000001,37.889,29.775 -2020-05-18 05:45:00,99.8,81.139,37.889,29.775 -2020-05-18 06:00:00,105.24,81.69,55.485,29.775 -2020-05-18 06:15:00,107.97,83.287,55.485,29.775 -2020-05-18 06:30:00,108.48,80.672,55.485,29.775 -2020-05-18 06:45:00,108.17,81.521,55.485,29.775 -2020-05-18 07:00:00,108.28,80.867,65.765,29.775 -2020-05-18 07:15:00,108.08,80.506,65.765,29.775 -2020-05-18 07:30:00,108.87,77.82,65.765,29.775 -2020-05-18 07:45:00,109.39,76.815,65.765,29.775 -2020-05-18 08:00:00,109.39,71.055,56.745,29.775 -2020-05-18 08:15:00,107.51,72.851,56.745,29.775 -2020-05-18 08:30:00,106.6,71.499,56.745,29.775 -2020-05-18 08:45:00,105.11,74.01100000000001,56.745,29.775 -2020-05-18 09:00:00,104.7,70.43,53.321999999999996,29.775 -2020-05-18 09:15:00,104.4,68.692,53.321999999999996,29.775 -2020-05-18 09:30:00,104.55,70.94800000000001,53.321999999999996,29.775 -2020-05-18 09:45:00,105.86,70.934,53.321999999999996,29.775 -2020-05-18 10:00:00,104.56,68.959,51.309,29.775 -2020-05-18 10:15:00,105.63,70.516,51.309,29.775 -2020-05-18 10:30:00,106.46,70.271,51.309,29.775 -2020-05-18 10:45:00,105.73,70.328,51.309,29.775 -2020-05-18 11:00:00,103.62,66.137,50.415,29.775 -2020-05-18 11:15:00,102.43,67.071,50.415,29.775 -2020-05-18 11:30:00,102.2,69.97800000000001,50.415,29.775 -2020-05-18 11:45:00,100.97,71.42699999999999,50.415,29.775 -2020-05-18 12:00:00,99.99,67.039,48.273,29.775 -2020-05-18 12:15:00,99.53,67.203,48.273,29.775 -2020-05-18 12:30:00,97.76,65.457,48.273,29.775 -2020-05-18 12:45:00,98.7,66.28,48.273,29.775 -2020-05-18 13:00:00,96.87,67.222,48.452,29.775 -2020-05-18 13:15:00,99.09,66.321,48.452,29.775 -2020-05-18 13:30:00,97.98,64.128,48.452,29.775 -2020-05-18 13:45:00,98.34,63.196999999999996,48.452,29.775 -2020-05-18 14:00:00,96.84,63.848,48.35,29.775 -2020-05-18 14:15:00,97.88,62.621,48.35,29.775 -2020-05-18 14:30:00,97.61,61.413999999999994,48.35,29.775 -2020-05-18 14:45:00,97.53,62.787,48.35,29.775 -2020-05-18 15:00:00,95.98,63.553000000000004,48.838,29.775 -2020-05-18 15:15:00,94.89,60.226000000000006,48.838,29.775 -2020-05-18 15:30:00,96.64,58.75899999999999,48.838,29.775 -2020-05-18 15:45:00,97.73,56.178000000000004,48.838,29.775 -2020-05-18 16:00:00,99.47,59.169,50.873000000000005,29.775 -2020-05-18 16:15:00,100.16,58.563,50.873000000000005,29.775 -2020-05-18 16:30:00,103.58,58.114,50.873000000000005,29.775 -2020-05-18 16:45:00,103.44,54.181999999999995,50.873000000000005,29.775 -2020-05-18 17:00:00,105.33,55.906000000000006,56.637,29.775 -2020-05-18 17:15:00,106.15,56.206,56.637,29.775 -2020-05-18 17:30:00,106.58,56.32899999999999,56.637,29.775 -2020-05-18 17:45:00,107.9,55.603,56.637,29.775 -2020-05-18 18:00:00,107.24,59.253,56.35,29.775 -2020-05-18 18:15:00,106.38,58.951,56.35,29.775 -2020-05-18 18:30:00,111.23,57.136,56.35,29.775 -2020-05-18 18:45:00,112.74,62.372,56.35,29.775 -2020-05-18 19:00:00,107.03,64.04899999999999,56.023,29.775 -2020-05-18 19:15:00,99.04,63.17100000000001,56.023,29.775 -2020-05-18 19:30:00,100.51,63.076,56.023,29.775 -2020-05-18 19:45:00,98.68,63.343999999999994,56.023,29.775 -2020-05-18 20:00:00,97.75,61.221000000000004,62.372,29.775 -2020-05-18 20:15:00,100.39,61.84,62.372,29.775 -2020-05-18 20:30:00,101.96,60.983000000000004,62.372,29.775 -2020-05-18 20:45:00,101.2,60.535,62.372,29.775 -2020-05-18 21:00:00,94.45,57.338,57.516999999999996,29.775 -2020-05-18 21:15:00,93.17,59.972,57.516999999999996,29.775 -2020-05-18 21:30:00,92.14,60.852,57.516999999999996,29.775 -2020-05-18 21:45:00,93.04,61.336000000000006,57.516999999999996,29.775 -2020-05-18 22:00:00,87.62,58.005,51.823,29.775 -2020-05-18 22:15:00,84.34,59.095,51.823,29.775 -2020-05-18 22:30:00,84.14,51.568999999999996,51.823,29.775 -2020-05-18 22:45:00,84.14,48.211000000000006,51.823,29.775 -2020-05-18 23:00:00,65.96,43.48,43.832,29.775 -2020-05-18 23:15:00,68.87,40.029,43.832,29.775 -2020-05-18 23:30:00,68.41,39.236,43.832,29.775 -2020-05-18 23:45:00,64.7,38.741,43.832,29.775 -2020-05-19 00:00:00,62.8,37.38,42.371,29.775 -2020-05-19 00:15:00,66.9,37.645,42.371,29.775 -2020-05-19 00:30:00,66.25,36.715,42.371,29.775 -2020-05-19 00:45:00,63.82,35.619,42.371,29.775 -2020-05-19 01:00:00,61.03,35.165,39.597,29.775 -2020-05-19 01:15:00,66.87,34.537,39.597,29.775 -2020-05-19 01:30:00,67.1,32.953,39.597,29.775 -2020-05-19 01:45:00,66.73,31.971999999999998,39.597,29.775 -2020-05-19 02:00:00,63.92,31.985,38.298,29.775 -2020-05-19 02:15:00,67.6,30.656,38.298,29.775 -2020-05-19 02:30:00,66.14,32.842,38.298,29.775 -2020-05-19 02:45:00,65.87,33.571999999999996,38.298,29.775 -2020-05-19 03:00:00,63.27,36.298,37.884,29.775 -2020-05-19 03:15:00,69.81,35.164,37.884,29.775 -2020-05-19 03:30:00,72.41,34.414,37.884,29.775 -2020-05-19 03:45:00,66.55,33.728,37.884,29.775 -2020-05-19 04:00:00,67.08,44.04600000000001,39.442,29.775 -2020-05-19 04:15:00,68.59,54.656000000000006,39.442,29.775 -2020-05-19 04:30:00,73.0,52.583,39.442,29.775 -2020-05-19 04:45:00,76.72,53.357,39.442,29.775 -2020-05-19 05:00:00,85.35,77.723,43.608000000000004,29.775 -2020-05-19 05:15:00,88.34,97.675,43.608000000000004,29.775 -2020-05-19 05:30:00,91.29,86.52,43.608000000000004,29.775 -2020-05-19 05:45:00,94.03,80.278,43.608000000000004,29.775 -2020-05-19 06:00:00,99.55,81.65100000000001,54.99100000000001,29.775 -2020-05-19 06:15:00,99.82,83.715,54.99100000000001,29.775 -2020-05-19 06:30:00,100.65,80.687,54.99100000000001,29.775 -2020-05-19 06:45:00,100.88,80.633,54.99100000000001,29.775 -2020-05-19 07:00:00,103.61,80.07600000000001,66.217,29.775 -2020-05-19 07:15:00,103.66,79.44800000000001,66.217,29.775 -2020-05-19 07:30:00,103.58,76.64399999999999,66.217,29.775 -2020-05-19 07:45:00,101.83,74.78399999999999,66.217,29.775 -2020-05-19 08:00:00,102.87,68.985,60.151,29.775 -2020-05-19 08:15:00,101.22,70.19800000000001,60.151,29.775 -2020-05-19 08:30:00,101.29,68.957,60.151,29.775 -2020-05-19 08:45:00,100.42,70.559,60.151,29.775 -2020-05-19 09:00:00,103.99,67.12100000000001,53.873000000000005,29.775 -2020-05-19 09:15:00,100.42,65.592,53.873000000000005,29.775 -2020-05-19 09:30:00,100.05,68.646,53.873000000000005,29.775 -2020-05-19 09:45:00,101.34,69.88,53.873000000000005,29.775 -2020-05-19 10:00:00,100.74,66.505,51.417,29.775 -2020-05-19 10:15:00,102.9,67.732,51.417,29.775 -2020-05-19 10:30:00,103.08,67.561,51.417,29.775 -2020-05-19 10:45:00,102.7,68.632,51.417,29.775 -2020-05-19 11:00:00,103.54,64.76,50.43600000000001,29.775 -2020-05-19 11:15:00,99.89,66.014,50.43600000000001,29.775 -2020-05-19 11:30:00,99.47,67.587,50.43600000000001,29.775 -2020-05-19 11:45:00,100.32,68.785,50.43600000000001,29.775 -2020-05-19 12:00:00,97.16,63.95399999999999,47.468,29.775 -2020-05-19 12:15:00,97.84,64.34,47.468,29.775 -2020-05-19 12:30:00,97.67,63.512,47.468,29.775 -2020-05-19 12:45:00,98.46,64.913,47.468,29.775 -2020-05-19 13:00:00,96.91,65.453,48.453,29.775 -2020-05-19 13:15:00,99.44,66.146,48.453,29.775 -2020-05-19 13:30:00,97.7,64.196,48.453,29.775 -2020-05-19 13:45:00,98.33,62.413000000000004,48.453,29.775 -2020-05-19 14:00:00,97.24,63.581,48.435,29.775 -2020-05-19 14:15:00,95.83,62.196000000000005,48.435,29.775 -2020-05-19 14:30:00,96.03,61.394,48.435,29.775 -2020-05-19 14:45:00,95.48,62.022,48.435,29.775 -2020-05-19 15:00:00,94.77,62.601000000000006,49.966,29.775 -2020-05-19 15:15:00,94.41,60.18600000000001,49.966,29.775 -2020-05-19 15:30:00,96.22,58.551,49.966,29.775 -2020-05-19 15:45:00,94.47,56.185,49.966,29.775 -2020-05-19 16:00:00,94.85,58.66,51.184,29.775 -2020-05-19 16:15:00,96.76,58.223,51.184,29.775 -2020-05-19 16:30:00,99.86,57.58,51.184,29.775 -2020-05-19 16:45:00,104.67,54.323,51.184,29.775 -2020-05-19 17:00:00,103.7,56.407,56.138999999999996,29.775 -2020-05-19 17:15:00,102.17,57.08,56.138999999999996,29.775 -2020-05-19 17:30:00,106.22,56.923,56.138999999999996,29.775 -2020-05-19 17:45:00,104.6,55.821999999999996,56.138999999999996,29.775 -2020-05-19 18:00:00,106.74,58.563,57.038000000000004,29.775 -2020-05-19 18:15:00,105.62,59.54600000000001,57.038000000000004,29.775 -2020-05-19 18:30:00,113.35,57.408,57.038000000000004,29.775 -2020-05-19 18:45:00,112.87,62.621,57.038000000000004,29.775 -2020-05-19 19:00:00,108.17,63.18899999999999,56.492,29.775 -2020-05-19 19:15:00,98.13,62.423,56.492,29.775 -2020-05-19 19:30:00,97.16,61.989,56.492,29.775 -2020-05-19 19:45:00,103.23,62.583999999999996,56.492,29.775 -2020-05-19 20:00:00,95.97,60.846000000000004,62.534,29.775 -2020-05-19 20:15:00,100.72,59.967,62.534,29.775 -2020-05-19 20:30:00,104.92,59.372,62.534,29.775 -2020-05-19 20:45:00,104.57,59.255,62.534,29.775 -2020-05-19 21:00:00,94.39,56.748999999999995,55.506,29.775 -2020-05-19 21:15:00,97.34,58.178999999999995,55.506,29.775 -2020-05-19 21:30:00,94.26,59.06,55.506,29.775 -2020-05-19 21:45:00,93.63,59.801,55.506,29.775 -2020-05-19 22:00:00,84.19,56.99,51.472,29.775 -2020-05-19 22:15:00,82.79,57.728,51.472,29.775 -2020-05-19 22:30:00,80.26,50.5,51.472,29.775 -2020-05-19 22:45:00,86.14,47.177,51.472,29.775 -2020-05-19 23:00:00,84.68,41.677,44.593,29.775 -2020-05-19 23:15:00,80.74,39.588,44.593,29.775 -2020-05-19 23:30:00,76.55,38.729,44.593,29.775 -2020-05-19 23:45:00,76.25,38.312,44.593,29.775 -2020-05-20 00:00:00,78.51,37.128,41.978,29.775 -2020-05-20 00:15:00,80.48,37.399,41.978,29.775 -2020-05-20 00:30:00,78.67,36.468,41.978,29.775 -2020-05-20 00:45:00,72.51,35.376,41.978,29.775 -2020-05-20 01:00:00,71.79,34.94,38.59,29.775 -2020-05-20 01:15:00,77.65,34.286,38.59,29.775 -2020-05-20 01:30:00,78.37,32.689,38.59,29.775 -2020-05-20 01:45:00,79.71,31.704,38.59,29.775 -2020-05-20 02:00:00,73.24,31.713,36.23,29.775 -2020-05-20 02:15:00,78.37,30.373,36.23,29.775 -2020-05-20 02:30:00,79.12,32.566,36.23,29.775 -2020-05-20 02:45:00,77.08,33.304,36.23,29.775 -2020-05-20 03:00:00,75.21,36.037,35.867,29.775 -2020-05-20 03:15:00,78.88,34.891999999999996,35.867,29.775 -2020-05-20 03:30:00,81.38,34.144,35.867,29.775 -2020-05-20 03:45:00,80.54,33.484,35.867,29.775 -2020-05-20 04:00:00,75.73,43.74100000000001,36.75,29.775 -2020-05-20 04:15:00,76.72,54.297,36.75,29.775 -2020-05-20 04:30:00,80.9,52.211000000000006,36.75,29.775 -2020-05-20 04:45:00,86.16,52.978,36.75,29.775 -2020-05-20 05:00:00,93.21,77.21,40.461,29.775 -2020-05-20 05:15:00,95.42,96.999,40.461,29.775 -2020-05-20 05:30:00,99.77,85.91,40.461,29.775 -2020-05-20 05:45:00,101.34,79.735,40.461,29.775 -2020-05-20 06:00:00,101.76,81.133,55.481,29.775 -2020-05-20 06:15:00,101.28,83.171,55.481,29.775 -2020-05-20 06:30:00,104.28,80.161,55.481,29.775 -2020-05-20 06:45:00,105.05,80.12899999999999,55.481,29.775 -2020-05-20 07:00:00,108.2,79.56,68.45,29.775 -2020-05-20 07:15:00,107.22,78.939,68.45,29.775 -2020-05-20 07:30:00,107.19,76.111,68.45,29.775 -2020-05-20 07:45:00,106.77,74.281,68.45,29.775 -2020-05-20 08:00:00,105.12,68.487,60.885,29.775 -2020-05-20 08:15:00,105.19,69.749,60.885,29.775 -2020-05-20 08:30:00,106.79,68.49600000000001,60.885,29.775 -2020-05-20 08:45:00,108.41,70.115,60.885,29.775 -2020-05-20 09:00:00,104.53,66.671,56.887,29.775 -2020-05-20 09:15:00,106.56,65.149,56.887,29.775 -2020-05-20 09:30:00,106.98,68.21300000000001,56.887,29.775 -2020-05-20 09:45:00,110.81,69.48,56.887,29.775 -2020-05-20 10:00:00,109.87,66.115,54.401,29.775 -2020-05-20 10:15:00,111.92,67.375,54.401,29.775 -2020-05-20 10:30:00,110.2,67.215,54.401,29.775 -2020-05-20 10:45:00,106.45,68.29899999999999,54.401,29.775 -2020-05-20 11:00:00,111.94,64.417,53.678000000000004,29.775 -2020-05-20 11:15:00,116.47,65.687,53.678000000000004,29.775 -2020-05-20 11:30:00,120.29,67.253,53.678000000000004,29.775 -2020-05-20 11:45:00,127.5,68.46,53.678000000000004,29.775 -2020-05-20 12:00:00,130.45,63.667,51.68,29.775 -2020-05-20 12:15:00,131.65,64.06,51.68,29.775 -2020-05-20 12:30:00,128.54,63.196000000000005,51.68,29.775 -2020-05-20 12:45:00,128.89,64.604,51.68,29.775 -2020-05-20 13:00:00,124.02,65.156,51.263000000000005,29.775 -2020-05-20 13:15:00,125.19,65.852,51.263000000000005,29.775 -2020-05-20 13:30:00,125.37,63.912,51.263000000000005,29.775 -2020-05-20 13:45:00,129.7,62.131,51.263000000000005,29.775 -2020-05-20 14:00:00,127.75,63.336000000000006,51.107,29.775 -2020-05-20 14:15:00,134.43,61.942,51.107,29.775 -2020-05-20 14:30:00,131.56,61.101000000000006,51.107,29.775 -2020-05-20 14:45:00,129.26,61.735,51.107,29.775 -2020-05-20 15:00:00,125.84,62.364,51.498000000000005,29.775 -2020-05-20 15:15:00,123.48,59.934,51.498000000000005,29.775 -2020-05-20 15:30:00,119.44,58.276,51.498000000000005,29.775 -2020-05-20 15:45:00,121.38,55.893,51.498000000000005,29.775 -2020-05-20 16:00:00,119.62,58.416000000000004,53.376999999999995,29.775 -2020-05-20 16:15:00,114.06,57.964,53.376999999999995,29.775 -2020-05-20 16:30:00,110.26,57.338,53.376999999999995,29.775 -2020-05-20 16:45:00,113.71,54.028,53.376999999999995,29.775 -2020-05-20 17:00:00,121.74,56.156000000000006,56.965,29.775 -2020-05-20 17:15:00,122.25,56.806999999999995,56.965,29.775 -2020-05-20 17:30:00,123.94,56.637,56.965,29.775 -2020-05-20 17:45:00,119.22,55.5,56.965,29.775 -2020-05-20 18:00:00,119.16,58.25899999999999,58.231,29.775 -2020-05-20 18:15:00,116.54,59.221000000000004,58.231,29.775 -2020-05-20 18:30:00,114.14,57.071000000000005,58.231,29.775 -2020-05-20 18:45:00,115.86,62.284,58.231,29.775 -2020-05-20 19:00:00,116.25,62.854,58.865,29.775 -2020-05-20 19:15:00,109.47,62.081,58.865,29.775 -2020-05-20 19:30:00,102.19,61.643,58.865,29.775 -2020-05-20 19:45:00,100.8,62.236000000000004,58.865,29.775 -2020-05-20 20:00:00,104.57,60.473,65.605,29.775 -2020-05-20 20:15:00,101.8,59.593999999999994,65.605,29.775 -2020-05-20 20:30:00,104.21,59.021,65.605,29.775 -2020-05-20 20:45:00,107.81,58.949,65.605,29.775 -2020-05-20 21:00:00,100.78,56.446999999999996,58.083999999999996,29.775 -2020-05-20 21:15:00,99.92,57.891999999999996,58.083999999999996,29.775 -2020-05-20 21:30:00,96.61,58.748000000000005,58.083999999999996,29.775 -2020-05-20 21:45:00,94.94,59.515,58.083999999999996,29.775 -2020-05-20 22:00:00,89.31,56.731,53.243,29.775 -2020-05-20 22:15:00,87.09,57.488,53.243,29.775 -2020-05-20 22:30:00,87.82,50.25899999999999,53.243,29.775 -2020-05-20 22:45:00,88.26,46.924,53.243,29.775 -2020-05-20 23:00:00,63.05,41.388999999999996,44.283,29.775 -2020-05-20 23:15:00,62.09,39.345,44.283,29.775 -2020-05-20 23:30:00,60.51,38.489000000000004,44.283,29.775 -2020-05-20 23:45:00,60.15,38.064,44.283,29.775 -2020-05-21 00:00:00,58.01,36.91,18.527,29.662 -2020-05-21 00:15:00,57.28,34.745,18.527,29.662 -2020-05-21 00:30:00,56.86,33.663000000000004,18.527,29.662 -2020-05-21 00:45:00,56.38,32.498000000000005,18.527,29.662 -2020-05-21 01:00:00,54.83,32.263000000000005,16.348,29.662 -2020-05-21 01:15:00,54.78,31.66,16.348,29.662 -2020-05-21 01:30:00,54.4,29.784000000000002,16.348,29.662 -2020-05-21 01:45:00,54.27,29.289,16.348,29.662 -2020-05-21 02:00:00,53.37,29.329,12.581,29.662 -2020-05-21 02:15:00,53.99,27.565,12.581,29.662 -2020-05-21 02:30:00,52.98,30.034000000000002,12.581,29.662 -2020-05-21 02:45:00,55.47,30.726999999999997,12.581,29.662 -2020-05-21 03:00:00,53.56,33.503,10.712,29.662 -2020-05-21 03:15:00,54.49,30.621,10.712,29.662 -2020-05-21 03:30:00,57.11,29.22,10.712,29.662 -2020-05-21 03:45:00,55.48,30.15,10.712,29.662 -2020-05-21 04:00:00,58.06,37.786,9.084,29.662 -2020-05-21 04:15:00,53.75,44.758,9.084,29.662 -2020-05-21 04:30:00,52.67,42.848,9.084,29.662 -2020-05-21 04:45:00,55.12,42.523,9.084,29.662 -2020-05-21 05:00:00,51.91,54.581,9.388,29.662 -2020-05-21 05:15:00,52.73,60.053000000000004,9.388,29.662 -2020-05-21 05:30:00,55.46,50.553999999999995,9.388,29.662 -2020-05-21 05:45:00,54.71,49.13399999999999,9.388,29.662 -2020-05-21 06:00:00,55.97,62.847,11.109000000000002,29.662 -2020-05-21 06:15:00,58.32,75.834,11.109000000000002,29.662 -2020-05-21 06:30:00,57.09,67.965,11.109000000000002,29.662 -2020-05-21 06:45:00,58.87,62.187,11.109000000000002,29.662 -2020-05-21 07:00:00,60.26,60.449,13.77,29.662 -2020-05-21 07:15:00,63.31,57.713,13.77,29.662 -2020-05-21 07:30:00,59.48,55.919,13.77,29.662 -2020-05-21 07:45:00,59.47,55.233000000000004,13.77,29.662 -2020-05-21 08:00:00,59.73,52.391999999999996,12.868,29.662 -2020-05-21 08:15:00,58.67,55.902,12.868,29.662 -2020-05-21 08:30:00,63.81,56.254,12.868,29.662 -2020-05-21 08:45:00,58.75,59.305,12.868,29.662 -2020-05-21 09:00:00,58.3,56.836000000000006,12.804,29.662 -2020-05-21 09:15:00,58.24,57.516999999999996,12.804,29.662 -2020-05-21 09:30:00,57.56,60.875,12.804,29.662 -2020-05-21 09:45:00,61.45,63.036,12.804,29.662 -2020-05-21 10:00:00,57.46,60.818000000000005,11.029000000000002,29.662 -2020-05-21 10:15:00,57.46,62.676,11.029000000000002,29.662 -2020-05-21 10:30:00,60.42,63.08,11.029000000000002,29.662 -2020-05-21 10:45:00,58.0,64.479,11.029000000000002,29.662 -2020-05-21 11:00:00,58.01,60.6,11.681,29.662 -2020-05-21 11:15:00,54.19,60.924,11.681,29.662 -2020-05-21 11:30:00,54.54,62.856,11.681,29.662 -2020-05-21 11:45:00,55.05,63.966,11.681,29.662 -2020-05-21 12:00:00,55.16,60.76,8.915,29.662 -2020-05-21 12:15:00,53.37,60.842,8.915,29.662 -2020-05-21 12:30:00,51.32,59.909,8.915,29.662 -2020-05-21 12:45:00,50.61,60.408,8.915,29.662 -2020-05-21 13:00:00,50.98,60.489,5.4639999999999995,29.662 -2020-05-21 13:15:00,52.85,60.721000000000004,5.4639999999999995,29.662 -2020-05-21 13:30:00,48.58,58.523,5.4639999999999995,29.662 -2020-05-21 13:45:00,49.76,56.766000000000005,5.4639999999999995,29.662 -2020-05-21 14:00:00,51.52,58.485,3.2939999999999996,29.662 -2020-05-21 14:15:00,57.07,56.833999999999996,3.2939999999999996,29.662 -2020-05-21 14:30:00,64.25,55.785,3.2939999999999996,29.662 -2020-05-21 14:45:00,65.85,55.278999999999996,3.2939999999999996,29.662 -2020-05-21 15:00:00,66.73,56.126999999999995,4.689,29.662 -2020-05-21 15:15:00,60.8,53.618,4.689,29.662 -2020-05-21 15:30:00,61.9,51.562,4.689,29.662 -2020-05-21 15:45:00,64.07,49.477,4.689,29.662 -2020-05-21 16:00:00,65.13,51.675,7.732,29.662 -2020-05-21 16:15:00,66.81,51.131,7.732,29.662 -2020-05-21 16:30:00,64.74,51.558,7.732,29.662 -2020-05-21 16:45:00,64.19,47.702,7.732,29.662 -2020-05-21 17:00:00,65.06,50.641999999999996,17.558,29.662 -2020-05-21 17:15:00,66.43,50.731,17.558,29.662 -2020-05-21 17:30:00,69.39,51.294,17.558,29.662 -2020-05-21 17:45:00,71.08,51.156000000000006,17.558,29.662 -2020-05-21 18:00:00,71.37,55.849,24.763,29.662 -2020-05-21 18:15:00,70.43,57.653999999999996,24.763,29.662 -2020-05-21 18:30:00,73.17,56.379,24.763,29.662 -2020-05-21 18:45:00,70.48,58.501000000000005,24.763,29.662 -2020-05-21 19:00:00,69.9,60.832,29.633000000000003,29.662 -2020-05-21 19:15:00,68.76,58.906000000000006,29.633000000000003,29.662 -2020-05-21 19:30:00,69.06,59.05,29.633000000000003,29.662 -2020-05-21 19:45:00,71.41,60.059,29.633000000000003,29.662 -2020-05-21 20:00:00,69.15,59.508,38.826,29.662 -2020-05-21 20:15:00,71.39,59.303999999999995,38.826,29.662 -2020-05-21 20:30:00,74.95,58.396,38.826,29.662 -2020-05-21 20:45:00,71.98,57.552,38.826,29.662 -2020-05-21 21:00:00,71.11,54.851000000000006,37.751,29.662 -2020-05-21 21:15:00,67.66,57.343999999999994,37.751,29.662 -2020-05-21 21:30:00,65.62,57.926,37.751,29.662 -2020-05-21 21:45:00,65.41,58.792,37.751,29.662 -2020-05-21 22:00:00,62.1,58.045,39.799,29.662 -2020-05-21 22:15:00,64.81,57.568999999999996,39.799,29.662 -2020-05-21 22:30:00,60.51,56.025,39.799,29.662 -2020-05-21 22:45:00,60.78,53.603,39.799,29.662 -2020-05-21 23:00:00,83.53,48.56,33.686,29.662 -2020-05-21 23:15:00,79.71,46.288999999999994,33.686,29.662 -2020-05-21 23:30:00,76.64,45.003,33.686,29.662 -2020-05-21 23:45:00,81.28,44.475,33.686,29.662 -2020-05-22 00:00:00,78.78,34.703,39.884,29.662 -2020-05-22 00:15:00,78.38,35.232,39.884,29.662 -2020-05-22 00:30:00,75.8,34.53,39.884,29.662 -2020-05-22 00:45:00,78.88,33.875,39.884,29.662 -2020-05-22 01:00:00,78.43,33.044000000000004,37.658,29.662 -2020-05-22 01:15:00,76.96,31.963,37.658,29.662 -2020-05-22 01:30:00,72.09,30.969,37.658,29.662 -2020-05-22 01:45:00,77.86,29.756999999999998,37.658,29.662 -2020-05-22 02:00:00,78.9,30.668000000000003,36.707,29.662 -2020-05-22 02:15:00,79.75,29.229,36.707,29.662 -2020-05-22 02:30:00,75.51,32.335,36.707,29.662 -2020-05-22 02:45:00,79.05,32.449,36.707,29.662 -2020-05-22 03:00:00,80.35,35.719,37.025,29.662 -2020-05-22 03:15:00,80.81,33.605,37.025,29.662 -2020-05-22 03:30:00,76.91,32.632,37.025,29.662 -2020-05-22 03:45:00,75.74,32.927,37.025,29.662 -2020-05-22 04:00:00,76.89,43.244,38.349000000000004,29.662 -2020-05-22 04:15:00,77.58,52.16,38.349000000000004,29.662 -2020-05-22 04:30:00,80.22,51.0,38.349000000000004,29.662 -2020-05-22 04:45:00,84.77,50.898,38.349000000000004,29.662 -2020-05-22 05:00:00,93.82,74.133,41.565,29.662 -2020-05-22 05:15:00,95.9,94.82,41.565,29.662 -2020-05-22 05:30:00,97.7,84.27,41.565,29.662 -2020-05-22 05:45:00,97.74,77.786,41.565,29.662 -2020-05-22 06:00:00,104.46,79.546,53.861000000000004,29.662 -2020-05-22 06:15:00,105.11,81.39,53.861000000000004,29.662 -2020-05-22 06:30:00,106.02,78.183,53.861000000000004,29.662 -2020-05-22 06:45:00,108.42,78.407,53.861000000000004,29.662 -2020-05-22 07:00:00,109.92,78.263,63.497,29.662 -2020-05-22 07:15:00,110.85,78.738,63.497,29.662 -2020-05-22 07:30:00,113.49,73.98100000000001,63.497,29.662 -2020-05-22 07:45:00,111.86,71.819,63.497,29.662 -2020-05-22 08:00:00,107.87,66.528,55.43899999999999,29.662 -2020-05-22 08:15:00,106.09,68.429,55.43899999999999,29.662 -2020-05-22 08:30:00,104.97,67.28,55.43899999999999,29.662 -2020-05-22 08:45:00,109.12,68.445,55.43899999999999,29.662 -2020-05-22 09:00:00,104.41,62.928000000000004,52.132,29.662 -2020-05-22 09:15:00,102.77,63.343,52.132,29.662 -2020-05-22 09:30:00,103.2,65.684,52.132,29.662 -2020-05-22 09:45:00,105.1,67.34899999999999,52.132,29.662 -2020-05-22 10:00:00,108.85,63.57,49.881,29.662 -2020-05-22 10:15:00,114.42,64.851,49.881,29.662 -2020-05-22 10:30:00,116.06,65.205,49.881,29.662 -2020-05-22 10:45:00,112.9,66.123,49.881,29.662 -2020-05-22 11:00:00,116.37,62.451,49.396,29.662 -2020-05-22 11:15:00,116.42,62.535,49.396,29.662 -2020-05-22 11:30:00,115.65,64.168,49.396,29.662 -2020-05-22 11:45:00,114.92,64.525,49.396,29.662 -2020-05-22 12:00:00,111.62,60.479,46.7,29.662 -2020-05-22 12:15:00,106.84,59.778,46.7,29.662 -2020-05-22 12:30:00,102.56,58.99100000000001,46.7,29.662 -2020-05-22 12:45:00,98.4,59.821000000000005,46.7,29.662 -2020-05-22 13:00:00,94.96,61.16,44.05,29.662 -2020-05-22 13:15:00,96.61,62.23,44.05,29.662 -2020-05-22 13:30:00,93.79,61.045,44.05,29.662 -2020-05-22 13:45:00,92.84,59.531000000000006,44.05,29.662 -2020-05-22 14:00:00,91.24,59.794,42.805,29.662 -2020-05-22 14:15:00,93.29,58.739,42.805,29.662 -2020-05-22 14:30:00,91.88,59.32899999999999,42.805,29.662 -2020-05-22 14:45:00,93.09,59.412,42.805,29.662 -2020-05-22 15:00:00,93.45,59.95399999999999,44.36600000000001,29.662 -2020-05-22 15:15:00,95.82,57.167,44.36600000000001,29.662 -2020-05-22 15:30:00,97.12,54.571000000000005,44.36600000000001,29.662 -2020-05-22 15:45:00,97.8,52.891000000000005,44.36600000000001,29.662 -2020-05-22 16:00:00,96.53,54.466,46.928999999999995,29.662 -2020-05-22 16:15:00,103.21,54.513999999999996,46.928999999999995,29.662 -2020-05-22 16:30:00,104.3,53.775,46.928999999999995,29.662 -2020-05-22 16:45:00,104.51,49.604,46.928999999999995,29.662 -2020-05-22 17:00:00,103.54,53.474,51.468,29.662 -2020-05-22 17:15:00,101.79,53.823,51.468,29.662 -2020-05-22 17:30:00,102.58,53.708,51.468,29.662 -2020-05-22 17:45:00,104.14,52.295,51.468,29.662 -2020-05-22 18:00:00,106.35,55.326,52.58,29.662 -2020-05-22 18:15:00,103.1,55.269,52.58,29.662 -2020-05-22 18:30:00,102.56,53.086000000000006,52.58,29.662 -2020-05-22 18:45:00,105.35,58.692,52.58,29.662 -2020-05-22 19:00:00,107.38,60.31399999999999,52.183,29.662 -2020-05-22 19:15:00,103.6,60.376000000000005,52.183,29.662 -2020-05-22 19:30:00,98.21,59.898999999999994,52.183,29.662 -2020-05-22 19:45:00,98.34,59.427,52.183,29.662 -2020-05-22 20:00:00,94.72,57.468999999999994,58.497,29.662 -2020-05-22 20:15:00,95.77,57.34,58.497,29.662 -2020-05-22 20:30:00,98.16,56.342,58.497,29.662 -2020-05-22 20:45:00,96.69,55.707,58.497,29.662 -2020-05-22 21:00:00,97.63,54.568000000000005,54.731,29.662 -2020-05-22 21:15:00,97.68,57.72,54.731,29.662 -2020-05-22 21:30:00,90.83,58.396,54.731,29.662 -2020-05-22 21:45:00,86.47,59.52,54.731,29.662 -2020-05-22 22:00:00,80.55,56.864,51.386,29.662 -2020-05-22 22:15:00,84.38,57.391000000000005,51.386,29.662 -2020-05-22 22:30:00,86.27,56.013000000000005,51.386,29.662 -2020-05-22 22:45:00,84.62,54.208999999999996,51.386,29.662 -2020-05-22 23:00:00,74.96,50.117,44.463,29.662 -2020-05-22 23:15:00,74.35,46.266000000000005,44.463,29.662 -2020-05-22 23:30:00,71.21,43.38,44.463,29.662 -2020-05-22 23:45:00,76.73,42.683,44.463,29.662 -2020-05-23 00:00:00,75.7,35.035,42.833999999999996,29.662 -2020-05-23 00:15:00,76.89,34.034,42.833999999999996,29.662 -2020-05-23 00:30:00,71.1,33.168,42.833999999999996,29.662 -2020-05-23 00:45:00,75.4,31.974,42.833999999999996,29.662 -2020-05-23 01:00:00,73.52,31.552,37.859,29.662 -2020-05-23 01:15:00,74.62,30.799,37.859,29.662 -2020-05-23 01:30:00,68.8,28.918000000000003,37.859,29.662 -2020-05-23 01:45:00,73.9,28.836,37.859,29.662 -2020-05-23 02:00:00,72.13,28.951,35.327,29.662 -2020-05-23 02:15:00,73.48,26.677,35.327,29.662 -2020-05-23 02:30:00,66.65,28.805,35.327,29.662 -2020-05-23 02:45:00,65.13,29.693,35.327,29.662 -2020-05-23 03:00:00,66.59,31.802,34.908,29.662 -2020-05-23 03:15:00,64.97,28.761,34.908,29.662 -2020-05-23 03:30:00,65.86,27.765,34.908,29.662 -2020-05-23 03:45:00,65.45,29.511999999999997,34.908,29.662 -2020-05-23 04:00:00,63.35,37.108000000000004,34.84,29.662 -2020-05-23 04:15:00,63.76,44.592,34.84,29.662 -2020-05-23 04:30:00,62.96,41.382,34.84,29.662 -2020-05-23 04:45:00,64.33,41.43,34.84,29.662 -2020-05-23 05:00:00,67.52,53.748000000000005,34.222,29.662 -2020-05-23 05:15:00,67.85,60.097,34.222,29.662 -2020-05-23 05:30:00,68.35,51.146,34.222,29.662 -2020-05-23 05:45:00,68.82,50.068999999999996,34.222,29.662 -2020-05-23 06:00:00,72.79,66.115,35.515,29.662 -2020-05-23 06:15:00,71.8,78.594,35.515,29.662 -2020-05-23 06:30:00,73.05,71.634,35.515,29.662 -2020-05-23 06:45:00,75.09,67.053,35.515,29.662 -2020-05-23 07:00:00,76.2,64.55199999999999,39.687,29.662 -2020-05-23 07:15:00,76.2,63.553000000000004,39.687,29.662 -2020-05-23 07:30:00,77.52,60.75899999999999,39.687,29.662 -2020-05-23 07:45:00,79.58,60.32,39.687,29.662 -2020-05-23 08:00:00,78.67,56.416000000000004,44.9,29.662 -2020-05-23 08:15:00,78.07,58.93600000000001,44.9,29.662 -2020-05-23 08:30:00,78.21,58.104,44.9,29.662 -2020-05-23 08:45:00,78.09,60.843,44.9,29.662 -2020-05-23 09:00:00,76.63,58.558,45.724,29.662 -2020-05-23 09:15:00,76.47,59.595,45.724,29.662 -2020-05-23 09:30:00,75.4,62.603,45.724,29.662 -2020-05-23 09:45:00,75.15,63.916000000000004,45.724,29.662 -2020-05-23 10:00:00,75.82,60.69,43.123999999999995,29.662 -2020-05-23 10:15:00,76.15,62.368,43.123999999999995,29.662 -2020-05-23 10:30:00,76.53,62.45399999999999,43.123999999999995,29.662 -2020-05-23 10:45:00,75.37,63.36,43.123999999999995,29.662 -2020-05-23 11:00:00,75.31,59.586999999999996,40.255,29.662 -2020-05-23 11:15:00,77.04,60.324,40.255,29.662 -2020-05-23 11:30:00,77.44,61.949,40.255,29.662 -2020-05-23 11:45:00,74.8,62.708999999999996,40.255,29.662 -2020-05-23 12:00:00,69.86,58.754,38.582,29.662 -2020-05-23 12:15:00,69.32,58.985,38.582,29.662 -2020-05-23 12:30:00,71.91,58.121,38.582,29.662 -2020-05-23 12:45:00,70.49,59.434,38.582,29.662 -2020-05-23 13:00:00,69.63,59.963,36.043,29.662 -2020-05-23 13:15:00,73.6,60.055,36.043,29.662 -2020-05-23 13:30:00,72.67,58.946999999999996,36.043,29.662 -2020-05-23 13:45:00,69.34,56.317,36.043,29.662 -2020-05-23 14:00:00,67.9,56.952,35.216,29.662 -2020-05-23 14:15:00,68.87,54.621,35.216,29.662 -2020-05-23 14:30:00,69.82,54.353,35.216,29.662 -2020-05-23 14:45:00,66.18,54.928999999999995,35.216,29.662 -2020-05-23 15:00:00,71.76,56.085,36.759,29.662 -2020-05-23 15:15:00,72.55,54.121,36.759,29.662 -2020-05-23 15:30:00,72.31,52.053000000000004,36.759,29.662 -2020-05-23 15:45:00,71.95,49.521,36.759,29.662 -2020-05-23 16:00:00,71.37,52.958,40.086,29.662 -2020-05-23 16:15:00,71.22,52.37,40.086,29.662 -2020-05-23 16:30:00,71.77,51.843999999999994,40.086,29.662 -2020-05-23 16:45:00,72.93,47.797,40.086,29.662 -2020-05-23 17:00:00,75.81,50.486000000000004,44.876999999999995,29.662 -2020-05-23 17:15:00,77.15,49.199,44.876999999999995,29.662 -2020-05-23 17:30:00,81.73,48.926,44.876999999999995,29.662 -2020-05-23 17:45:00,83.0,47.845,44.876999999999995,29.662 -2020-05-23 18:00:00,82.3,52.175,47.056000000000004,29.662 -2020-05-23 18:15:00,80.93,54.047,47.056000000000004,29.662 -2020-05-23 18:30:00,81.66,53.409,47.056000000000004,29.662 -2020-05-23 18:45:00,81.46,55.044,47.056000000000004,29.662 -2020-05-23 19:00:00,79.51,55.43,45.57,29.662 -2020-05-23 19:15:00,76.63,54.428999999999995,45.57,29.662 -2020-05-23 19:30:00,75.8,54.83,45.57,29.662 -2020-05-23 19:45:00,75.75,55.99,45.57,29.662 -2020-05-23 20:00:00,75.77,55.193000000000005,41.685,29.662 -2020-05-23 20:15:00,75.2,54.916000000000004,41.685,29.662 -2020-05-23 20:30:00,78.48,53.038999999999994,41.685,29.662 -2020-05-23 20:45:00,77.87,54.102,41.685,29.662 -2020-05-23 21:00:00,75.6,52.068999999999996,39.576,29.662 -2020-05-23 21:15:00,75.41,54.998999999999995,39.576,29.662 -2020-05-23 21:30:00,72.51,56.086999999999996,39.576,29.662 -2020-05-23 21:45:00,71.96,56.635,39.576,29.662 -2020-05-23 22:00:00,68.93,54.263000000000005,39.068000000000005,29.662 -2020-05-23 22:15:00,68.33,55.56,39.068000000000005,29.662 -2020-05-23 22:30:00,65.26,55.118,39.068000000000005,29.662 -2020-05-23 22:45:00,65.06,54.041000000000004,39.068000000000005,29.662 -2020-05-23 23:00:00,61.85,49.77,32.06,29.662 -2020-05-23 23:15:00,61.41,46.022,32.06,29.662 -2020-05-23 23:30:00,61.34,45.095,32.06,29.662 -2020-05-23 23:45:00,59.43,44.225,32.06,29.662 -2020-05-24 00:00:00,57.13,36.184,28.825,29.662 -2020-05-24 00:15:00,57.97,34.035,28.825,29.662 -2020-05-24 00:30:00,57.26,32.953,28.825,29.662 -2020-05-24 00:45:00,57.6,31.803,28.825,29.662 -2020-05-24 01:00:00,55.3,31.62,25.995,29.662 -2020-05-24 01:15:00,56.86,30.941999999999997,25.995,29.662 -2020-05-24 01:30:00,56.68,29.026999999999997,25.995,29.662 -2020-05-24 01:45:00,56.48,28.518,25.995,29.662 -2020-05-24 02:00:00,54.98,28.549,24.394000000000002,29.662 -2020-05-24 02:15:00,55.67,26.753,24.394000000000002,29.662 -2020-05-24 02:30:00,55.71,29.241999999999997,24.394000000000002,29.662 -2020-05-24 02:45:00,54.51,29.96,24.394000000000002,29.662 -2020-05-24 03:00:00,54.2,32.754,22.916999999999998,29.662 -2020-05-24 03:15:00,54.33,29.838,22.916999999999998,29.662 -2020-05-24 03:30:00,55.39,28.445999999999998,22.916999999999998,29.662 -2020-05-24 03:45:00,53.97,29.454,22.916999999999998,29.662 -2020-05-24 04:00:00,51.59,36.912,23.576999999999998,29.662 -2020-05-24 04:15:00,52.43,43.722,23.576999999999998,29.662 -2020-05-24 04:30:00,52.07,41.773999999999994,23.576999999999998,29.662 -2020-05-24 04:45:00,52.18,41.433,23.576999999999998,29.662 -2020-05-24 05:00:00,52.38,53.093999999999994,22.730999999999998,29.662 -2020-05-24 05:15:00,51.01,58.086999999999996,22.730999999999998,29.662 -2020-05-24 05:30:00,51.44,48.788000000000004,22.730999999999998,29.662 -2020-05-24 05:45:00,50.26,47.56100000000001,22.730999999999998,29.662 -2020-05-24 06:00:00,55.4,61.348,22.34,29.662 -2020-05-24 06:15:00,56.11,74.26,22.34,29.662 -2020-05-24 06:30:00,56.5,66.44,22.34,29.662 -2020-05-24 06:45:00,58.7,60.73,22.34,29.662 -2020-05-24 07:00:00,60.0,58.957,24.691999999999997,29.662 -2020-05-24 07:15:00,62.28,56.248999999999995,24.691999999999997,29.662 -2020-05-24 07:30:00,63.86,54.387,24.691999999999997,29.662 -2020-05-24 07:45:00,66.28,53.792,24.691999999999997,29.662 -2020-05-24 08:00:00,68.02,50.965,29.340999999999998,29.662 -2020-05-24 08:15:00,69.85,54.619,29.340999999999998,29.662 -2020-05-24 08:30:00,70.34,54.93600000000001,29.340999999999998,29.662 -2020-05-24 08:45:00,69.19,58.036,29.340999999999998,29.662 -2020-05-24 09:00:00,64.38,55.551,30.788,29.662 -2020-05-24 09:15:00,64.98,56.251999999999995,30.788,29.662 -2020-05-24 09:30:00,61.61,59.635,30.788,29.662 -2020-05-24 09:45:00,58.24,61.888999999999996,30.788,29.662 -2020-05-24 10:00:00,59.37,59.70399999999999,30.158,29.662 -2020-05-24 10:15:00,60.65,61.652,30.158,29.662 -2020-05-24 10:30:00,62.09,62.085,30.158,29.662 -2020-05-24 10:45:00,62.05,63.525,30.158,29.662 -2020-05-24 11:00:00,62.17,59.622,32.056,29.662 -2020-05-24 11:15:00,59.81,59.99100000000001,32.056,29.662 -2020-05-24 11:30:00,55.31,61.896,32.056,29.662 -2020-05-24 11:45:00,55.01,63.036,32.056,29.662 -2020-05-24 12:00:00,56.12,59.94,28.671999999999997,29.662 -2020-05-24 12:15:00,58.46,60.038000000000004,28.671999999999997,29.662 -2020-05-24 12:30:00,58.98,59.003,28.671999999999997,29.662 -2020-05-24 12:45:00,61.98,59.52,28.671999999999997,29.662 -2020-05-24 13:00:00,60.62,59.632,23.171,29.662 -2020-05-24 13:15:00,61.12,59.873000000000005,23.171,29.662 -2020-05-24 13:30:00,56.46,57.708999999999996,23.171,29.662 -2020-05-24 13:45:00,56.65,55.956,23.171,29.662 -2020-05-24 14:00:00,56.1,57.781000000000006,19.11,29.662 -2020-05-24 14:15:00,58.99,56.107,19.11,29.662 -2020-05-24 14:30:00,64.84,54.945,19.11,29.662 -2020-05-24 14:45:00,70.09,54.452,19.11,29.662 -2020-05-24 15:00:00,72.58,55.449,19.689,29.662 -2020-05-24 15:15:00,73.17,52.895,19.689,29.662 -2020-05-24 15:30:00,74.8,50.771,19.689,29.662 -2020-05-24 15:45:00,76.05,48.638999999999996,19.689,29.662 -2020-05-24 16:00:00,84.67,50.972,22.875,29.662 -2020-05-24 16:15:00,87.35,50.39,22.875,29.662 -2020-05-24 16:30:00,87.69,50.867,22.875,29.662 -2020-05-24 16:45:00,83.03,46.857,22.875,29.662 -2020-05-24 17:00:00,83.2,49.927,33.884,29.662 -2020-05-24 17:15:00,80.98,49.951,33.884,29.662 -2020-05-24 17:30:00,82.32,50.475,33.884,29.662 -2020-05-24 17:45:00,86.46,50.239,33.884,29.662 -2020-05-24 18:00:00,88.05,54.983000000000004,38.453,29.662 -2020-05-24 18:15:00,84.96,56.722,38.453,29.662 -2020-05-24 18:30:00,81.33,55.413999999999994,38.453,29.662 -2020-05-24 18:45:00,80.49,57.535,38.453,29.662 -2020-05-24 19:00:00,83.33,59.87,39.221,29.662 -2020-05-24 19:15:00,81.36,57.926,39.221,29.662 -2020-05-24 19:30:00,79.76,58.052,39.221,29.662 -2020-05-24 19:45:00,80.78,59.06,39.221,29.662 -2020-05-24 20:00:00,82.2,58.438,37.871,29.662 -2020-05-24 20:15:00,82.3,58.229,37.871,29.662 -2020-05-24 20:30:00,83.51,57.387,37.871,29.662 -2020-05-24 20:45:00,83.49,56.669,37.871,29.662 -2020-05-24 21:00:00,81.41,53.983999999999995,36.465,29.662 -2020-05-24 21:15:00,82.26,56.519,36.465,29.662 -2020-05-24 21:30:00,75.32,57.03,36.465,29.662 -2020-05-24 21:45:00,76.43,57.966,36.465,29.662 -2020-05-24 22:00:00,74.5,57.299,36.092,29.662 -2020-05-24 22:15:00,78.91,56.873999999999995,36.092,29.662 -2020-05-24 22:30:00,80.16,55.325,36.092,29.662 -2020-05-24 22:45:00,79.85,52.865,36.092,29.662 -2020-05-24 23:00:00,71.49,47.725,31.013,29.662 -2020-05-24 23:15:00,69.19,45.586999999999996,31.013,29.662 -2020-05-24 23:30:00,73.1,44.315,31.013,29.662 -2020-05-24 23:45:00,75.16,43.761,31.013,29.662 -2020-05-25 00:00:00,73.04,38.211999999999996,31.174,29.775 -2020-05-25 00:15:00,71.45,37.404,31.174,29.775 -2020-05-25 00:30:00,65.82,35.985,31.174,29.775 -2020-05-25 00:45:00,65.94,34.361,31.174,29.775 -2020-05-25 01:00:00,68.21,34.563,29.663,29.775 -2020-05-25 01:15:00,73.16,33.77,29.663,29.775 -2020-05-25 01:30:00,73.49,32.193000000000005,29.663,29.775 -2020-05-25 01:45:00,70.92,31.61,29.663,29.775 -2020-05-25 02:00:00,67.31,32.065,28.793000000000003,29.775 -2020-05-25 02:15:00,72.78,29.594,28.793000000000003,29.775 -2020-05-25 02:30:00,74.32,32.31,28.793000000000003,29.775 -2020-05-25 02:45:00,74.35,32.775999999999996,28.793000000000003,29.775 -2020-05-25 03:00:00,69.78,36.306999999999995,27.728,29.775 -2020-05-25 03:15:00,68.47,34.389,27.728,29.775 -2020-05-25 03:30:00,69.86,33.588,27.728,29.775 -2020-05-25 03:45:00,70.1,34.076,27.728,29.775 -2020-05-25 04:00:00,73.53,45.273999999999994,29.266,29.775 -2020-05-25 04:15:00,73.54,55.622,29.266,29.775 -2020-05-25 04:30:00,77.68,53.648,29.266,29.775 -2020-05-25 04:45:00,81.3,53.683,29.266,29.775 -2020-05-25 05:00:00,89.03,74.794,37.889,29.775 -2020-05-25 05:15:00,93.31,93.23,37.889,29.775 -2020-05-25 05:30:00,95.72,82.57700000000001,37.889,29.775 -2020-05-25 05:45:00,98.49,77.42699999999999,37.889,29.775 -2020-05-25 06:00:00,99.46,78.153,55.485,29.775 -2020-05-25 06:15:00,104.22,79.572,55.485,29.775 -2020-05-25 06:30:00,105.6,77.07,55.485,29.775 -2020-05-25 06:45:00,106.6,78.078,55.485,29.775 -2020-05-25 07:00:00,108.35,77.343,65.765,29.775 -2020-05-25 07:15:00,107.73,77.042,65.765,29.775 -2020-05-25 07:30:00,107.92,74.193,65.765,29.775 -2020-05-25 07:45:00,108.55,73.4,65.765,29.775 -2020-05-25 08:00:00,106.16,67.673,56.745,29.775 -2020-05-25 08:15:00,106.57,69.807,56.745,29.775 -2020-05-25 08:30:00,107.31,68.376,56.745,29.775 -2020-05-25 08:45:00,106.93,71.00399999999999,56.745,29.775 -2020-05-25 09:00:00,106.44,67.383,53.321999999999996,29.775 -2020-05-25 09:15:00,106.7,65.695,53.321999999999996,29.775 -2020-05-25 09:30:00,105.96,68.01100000000001,53.321999999999996,29.775 -2020-05-25 09:45:00,105.74,68.217,53.321999999999996,29.775 -2020-05-25 10:00:00,106.53,66.318,51.309,29.775 -2020-05-25 10:15:00,109.6,68.087,51.309,29.775 -2020-05-25 10:30:00,108.78,67.915,51.309,29.775 -2020-05-25 10:45:00,107.84,68.066,51.309,29.775 -2020-05-25 11:00:00,105.05,63.818000000000005,50.415,29.775 -2020-05-25 11:15:00,103.48,64.858,50.415,29.775 -2020-05-25 11:30:00,101.34,67.70100000000001,50.415,29.775 -2020-05-25 11:45:00,101.35,69.223,50.415,29.775 -2020-05-25 12:00:00,100.54,65.095,48.273,29.775 -2020-05-25 12:15:00,101.38,65.3,48.273,29.775 -2020-05-25 12:30:00,99.35,63.31,48.273,29.775 -2020-05-25 12:45:00,101.48,64.178,48.273,29.775 -2020-05-25 13:00:00,100.53,65.195,48.452,29.775 -2020-05-25 13:15:00,101.37,64.317,48.452,29.775 -2020-05-25 13:30:00,99.07,62.199,48.452,29.775 -2020-05-25 13:45:00,100.17,61.278999999999996,48.452,29.775 -2020-05-25 14:00:00,99.77,62.183,48.35,29.775 -2020-05-25 14:15:00,102.24,60.898999999999994,48.35,29.775 -2020-05-25 14:30:00,103.48,59.428000000000004,48.35,29.775 -2020-05-25 14:45:00,101.43,60.82899999999999,48.35,29.775 -2020-05-25 15:00:00,101.0,61.948,48.838,29.775 -2020-05-25 15:15:00,97.03,58.513000000000005,48.838,29.775 -2020-05-25 15:30:00,98.64,56.886,48.838,29.775 -2020-05-25 15:45:00,101.34,54.193999999999996,48.838,29.775 -2020-05-25 16:00:00,101.37,57.505,50.873000000000005,29.775 -2020-05-25 16:15:00,103.25,56.806999999999995,50.873000000000005,29.775 -2020-05-25 16:30:00,106.69,56.475,50.873000000000005,29.775 -2020-05-25 16:45:00,106.5,52.178000000000004,50.873000000000005,29.775 -2020-05-25 17:00:00,107.1,54.208999999999996,56.637,29.775 -2020-05-25 17:15:00,111.13,54.353,56.637,29.775 -2020-05-25 17:30:00,110.99,54.388000000000005,56.637,29.775 -2020-05-25 17:45:00,111.14,53.427,56.637,29.775 -2020-05-25 18:00:00,108.96,57.196999999999996,56.35,29.775 -2020-05-25 18:15:00,105.76,56.744,56.35,29.775 -2020-05-25 18:30:00,113.14,54.85,56.35,29.775 -2020-05-25 18:45:00,116.77,60.083,56.35,29.775 -2020-05-25 19:00:00,108.64,61.768,56.023,29.775 -2020-05-25 19:15:00,98.73,60.848,56.023,29.775 -2020-05-25 19:30:00,103.11,60.716,56.023,29.775 -2020-05-25 19:45:00,99.07,60.978,56.023,29.775 -2020-05-25 20:00:00,96.44,58.687,62.372,29.775 -2020-05-25 20:15:00,103.82,59.299,62.372,29.775 -2020-05-25 20:30:00,106.27,58.593999999999994,62.372,29.775 -2020-05-25 20:45:00,103.64,58.45,62.372,29.775 -2020-05-25 21:00:00,96.06,55.285,57.516999999999996,29.775 -2020-05-25 21:15:00,96.57,58.018,57.516999999999996,29.775 -2020-05-25 21:30:00,96.34,58.732,57.516999999999996,29.775 -2020-05-25 21:45:00,96.09,59.38399999999999,57.516999999999996,29.775 -2020-05-25 22:00:00,89.25,56.24,51.823,29.775 -2020-05-25 22:15:00,83.2,57.45399999999999,51.823,29.775 -2020-05-25 22:30:00,87.35,49.917,51.823,29.775 -2020-05-25 22:45:00,89.63,46.472,51.823,29.775 -2020-05-25 23:00:00,82.93,41.511,43.832,29.775 -2020-05-25 23:15:00,75.55,38.372,43.832,29.775 -2020-05-25 23:30:00,79.33,37.609,43.832,29.775 -2020-05-25 23:45:00,82.15,37.052,43.832,29.775 -2020-05-26 00:00:00,77.88,35.689,42.371,29.775 -2020-05-26 00:15:00,75.1,35.991,42.371,29.775 -2020-05-26 00:30:00,78.04,35.06,42.371,29.775 -2020-05-26 00:45:00,78.89,33.999,42.371,29.775 -2020-05-26 01:00:00,77.82,33.665,39.597,29.775 -2020-05-26 01:15:00,76.09,32.865,39.597,29.775 -2020-05-26 01:30:00,78.26,31.186,39.597,29.775 -2020-05-26 01:45:00,79.73,30.171999999999997,39.597,29.775 -2020-05-26 02:00:00,79.78,30.166999999999998,38.298,29.775 -2020-05-26 02:15:00,76.12,28.761999999999997,38.298,29.775 -2020-05-26 02:30:00,79.45,30.995,38.298,29.775 -2020-05-26 02:45:00,81.63,31.781999999999996,38.298,29.775 -2020-05-26 03:00:00,78.81,34.551,37.884,29.775 -2020-05-26 03:15:00,75.75,33.34,37.884,29.775 -2020-05-26 03:30:00,80.4,32.609,37.884,29.775 -2020-05-26 03:45:00,82.09,32.106,37.884,29.775 -2020-05-26 04:00:00,81.49,42.005,39.442,29.775 -2020-05-26 04:15:00,78.42,52.242,39.442,29.775 -2020-05-26 04:30:00,81.46,50.07899999999999,39.442,29.775 -2020-05-26 04:45:00,86.44,50.81399999999999,39.442,29.775 -2020-05-26 05:00:00,92.93,74.25399999999999,43.608000000000004,29.775 -2020-05-26 05:15:00,95.98,93.09100000000001,43.608000000000004,29.775 -2020-05-26 05:30:00,97.87,82.4,43.608000000000004,29.775 -2020-05-26 05:45:00,100.97,76.611,43.608000000000004,29.775 -2020-05-26 06:00:00,104.23,78.155,54.99100000000001,29.775 -2020-05-26 06:15:00,106.17,80.044,54.99100000000001,29.775 -2020-05-26 06:30:00,107.29,77.12899999999999,54.99100000000001,29.775 -2020-05-26 06:45:00,108.62,77.236,54.99100000000001,29.775 -2020-05-26 07:00:00,111.63,76.596,66.217,29.775 -2020-05-26 07:15:00,109.37,76.031,66.217,29.775 -2020-05-26 07:30:00,108.15,73.069,66.217,29.775 -2020-05-26 07:45:00,107.28,71.422,66.217,29.775 -2020-05-26 08:00:00,105.84,65.657,60.151,29.775 -2020-05-26 08:15:00,107.1,67.205,60.151,29.775 -2020-05-26 08:30:00,107.63,65.885,60.151,29.775 -2020-05-26 08:45:00,105.85,67.601,60.151,29.775 -2020-05-26 09:00:00,106.2,64.122,53.873000000000005,29.775 -2020-05-26 09:15:00,106.76,62.643,53.873000000000005,29.775 -2020-05-26 09:30:00,106.29,65.755,53.873000000000005,29.775 -2020-05-26 09:45:00,107.29,67.207,53.873000000000005,29.775 -2020-05-26 10:00:00,107.68,63.907,51.417,29.775 -2020-05-26 10:15:00,108.29,65.344,51.417,29.775 -2020-05-26 10:30:00,107.02,65.244,51.417,29.775 -2020-05-26 10:45:00,106.79,66.407,51.417,29.775 -2020-05-26 11:00:00,103.4,62.48,50.43600000000001,29.775 -2020-05-26 11:15:00,103.94,63.838,50.43600000000001,29.775 -2020-05-26 11:30:00,107.61,65.347,50.43600000000001,29.775 -2020-05-26 11:45:00,110.51,66.61399999999999,50.43600000000001,29.775 -2020-05-26 12:00:00,108.4,62.042,47.468,29.775 -2020-05-26 12:15:00,103.09,62.467,47.468,29.775 -2020-05-26 12:30:00,104.04,61.398,47.468,29.775 -2020-05-26 12:45:00,101.49,62.843,47.468,29.775 -2020-05-26 13:00:00,102.78,63.45399999999999,48.453,29.775 -2020-05-26 13:15:00,107.92,64.171,48.453,29.775 -2020-05-26 13:30:00,112.42,62.297,48.453,29.775 -2020-05-26 13:45:00,112.14,60.523999999999994,48.453,29.775 -2020-05-26 14:00:00,110.26,61.941,48.435,29.775 -2020-05-26 14:15:00,105.71,60.5,48.435,29.775 -2020-05-26 14:30:00,111.81,59.43600000000001,48.435,29.775 -2020-05-26 14:45:00,112.96,60.092,48.435,29.775 -2020-05-26 15:00:00,112.99,61.018,49.966,29.775 -2020-05-26 15:15:00,110.34,58.498000000000005,49.966,29.775 -2020-05-26 15:30:00,113.39,56.705,49.966,29.775 -2020-05-26 15:45:00,116.68,54.229,49.966,29.775 -2020-05-26 16:00:00,118.92,57.022,51.184,29.775 -2020-05-26 16:15:00,115.17,56.494,51.184,29.775 -2020-05-26 16:30:00,114.9,55.968,51.184,29.775 -2020-05-26 16:45:00,120.29,52.354,51.184,29.775 -2020-05-26 17:00:00,120.37,54.74,56.138999999999996,29.775 -2020-05-26 17:15:00,116.21,55.26,56.138999999999996,29.775 -2020-05-26 17:30:00,111.59,55.015,56.138999999999996,29.775 -2020-05-26 17:45:00,120.21,53.681000000000004,56.138999999999996,29.775 -2020-05-26 18:00:00,122.33,56.542,57.038000000000004,29.775 -2020-05-26 18:15:00,115.55,57.373000000000005,57.038000000000004,29.775 -2020-05-26 18:30:00,110.43,55.159,57.038000000000004,29.775 -2020-05-26 18:45:00,113.6,60.367,57.038000000000004,29.775 -2020-05-26 19:00:00,113.25,60.945,56.492,29.775 -2020-05-26 19:15:00,105.86,60.135,56.492,29.775 -2020-05-26 19:30:00,105.79,59.665,56.492,29.775 -2020-05-26 19:45:00,100.27,60.254,56.492,29.775 -2020-05-26 20:00:00,101.3,58.349,62.534,29.775 -2020-05-26 20:15:00,105.86,57.463,62.534,29.775 -2020-05-26 20:30:00,107.47,57.018,62.534,29.775 -2020-05-26 20:45:00,104.42,57.199,62.534,29.775 -2020-05-26 21:00:00,97.23,54.727,55.506,29.775 -2020-05-26 21:15:00,102.65,56.256,55.506,29.775 -2020-05-26 21:30:00,99.18,56.97,55.506,29.775 -2020-05-26 21:45:00,96.09,57.873999999999995,55.506,29.775 -2020-05-26 22:00:00,85.21,55.248000000000005,51.472,29.775 -2020-05-26 22:15:00,90.23,56.108999999999995,51.472,29.775 -2020-05-26 22:30:00,91.74,48.865,51.472,29.775 -2020-05-26 22:45:00,88.76,45.456,51.472,29.775 -2020-05-26 23:00:00,79.55,39.731,44.593,29.775 -2020-05-26 23:15:00,79.85,37.953,44.593,29.775 -2020-05-26 23:30:00,86.2,37.123000000000005,44.593,29.775 -2020-05-26 23:45:00,87.91,36.645,44.593,29.775 -2020-05-27 00:00:00,78.67,35.461999999999996,41.978,29.775 -2020-05-27 00:15:00,76.21,35.769,41.978,29.775 -2020-05-27 00:30:00,74.83,34.839,41.978,29.775 -2020-05-27 00:45:00,82.21,33.784,41.978,29.775 -2020-05-27 01:00:00,79.98,33.466,38.59,29.775 -2020-05-27 01:15:00,79.27,32.641999999999996,38.59,29.775 -2020-05-27 01:30:00,75.92,30.951,38.59,29.775 -2020-05-27 01:45:00,75.33,29.930999999999997,38.59,29.775 -2020-05-27 02:00:00,79.93,29.924,36.23,29.775 -2020-05-27 02:15:00,81.78,28.51,36.23,29.775 -2020-05-27 02:30:00,78.49,30.748,36.23,29.775 -2020-05-27 02:45:00,76.39,31.541999999999998,36.23,29.775 -2020-05-27 03:00:00,74.78,34.317,35.867,29.775 -2020-05-27 03:15:00,81.81,33.095,35.867,29.775 -2020-05-27 03:30:00,83.72,32.368,35.867,29.775 -2020-05-27 03:45:00,84.2,31.891,35.867,29.775 -2020-05-27 04:00:00,80.31,41.731,36.75,29.775 -2020-05-27 04:15:00,79.3,51.916000000000004,36.75,29.775 -2020-05-27 04:30:00,80.69,49.742,36.75,29.775 -2020-05-27 04:45:00,85.87,50.47,36.75,29.775 -2020-05-27 05:00:00,92.63,73.78399999999999,40.461,29.775 -2020-05-27 05:15:00,97.73,92.46600000000001,40.461,29.775 -2020-05-27 05:30:00,101.04,81.842,40.461,29.775 -2020-05-27 05:45:00,102.83,76.115,40.461,29.775 -2020-05-27 06:00:00,108.55,77.681,55.481,29.775 -2020-05-27 06:15:00,111.03,79.547,55.481,29.775 -2020-05-27 06:30:00,114.05,76.648,55.481,29.775 -2020-05-27 06:45:00,116.14,76.777,55.481,29.775 -2020-05-27 07:00:00,118.55,76.126,68.45,29.775 -2020-05-27 07:15:00,117.45,75.571,68.45,29.775 -2020-05-27 07:30:00,116.41,72.589,68.45,29.775 -2020-05-27 07:45:00,115.84,70.972,68.45,29.775 -2020-05-27 08:00:00,115.67,65.21300000000001,60.885,29.775 -2020-05-27 08:15:00,117.03,66.808,60.885,29.775 -2020-05-27 08:30:00,117.65,65.476,60.885,29.775 -2020-05-27 08:45:00,116.56,67.208,60.885,29.775 -2020-05-27 09:00:00,113.89,63.724,56.887,29.775 -2020-05-27 09:15:00,113.51,62.251000000000005,56.887,29.775 -2020-05-27 09:30:00,110.46,65.37,56.887,29.775 -2020-05-27 09:45:00,113.38,66.851,56.887,29.775 -2020-05-27 10:00:00,111.51,63.56100000000001,54.401,29.775 -2020-05-27 10:15:00,109.58,65.025,54.401,29.775 -2020-05-27 10:30:00,110.08,64.936,54.401,29.775 -2020-05-27 10:45:00,111.45,66.111,54.401,29.775 -2020-05-27 11:00:00,116.34,62.177,53.678000000000004,29.775 -2020-05-27 11:15:00,116.28,63.548,53.678000000000004,29.775 -2020-05-27 11:30:00,120.85,65.04899999999999,53.678000000000004,29.775 -2020-05-27 11:45:00,119.2,66.324,53.678000000000004,29.775 -2020-05-27 12:00:00,113.31,61.788000000000004,51.68,29.775 -2020-05-27 12:15:00,114.74,62.218,51.68,29.775 -2020-05-27 12:30:00,110.65,61.11600000000001,51.68,29.775 -2020-05-27 12:45:00,108.47,62.567,51.68,29.775 -2020-05-27 13:00:00,109.49,63.18600000000001,51.263000000000005,29.775 -2020-05-27 13:15:00,113.95,63.903999999999996,51.263000000000005,29.775 -2020-05-27 13:30:00,118.69,62.041000000000004,51.263000000000005,29.775 -2020-05-27 13:45:00,116.61,60.272,51.263000000000005,29.775 -2020-05-27 14:00:00,106.92,61.72,51.107,29.775 -2020-05-27 14:15:00,103.29,60.273,51.107,29.775 -2020-05-27 14:30:00,103.58,59.173,51.107,29.775 -2020-05-27 14:45:00,101.55,59.833999999999996,51.107,29.775 -2020-05-27 15:00:00,101.02,60.805,51.498000000000005,29.775 -2020-05-27 15:15:00,106.26,58.272,51.498000000000005,29.775 -2020-05-27 15:30:00,108.1,56.458,51.498000000000005,29.775 -2020-05-27 15:45:00,106.97,53.967,51.498000000000005,29.775 -2020-05-27 16:00:00,104.58,56.803000000000004,53.376999999999995,29.775 -2020-05-27 16:15:00,112.71,56.263000000000005,53.376999999999995,29.775 -2020-05-27 16:30:00,115.07,55.753,53.376999999999995,29.775 -2020-05-27 16:45:00,117.58,52.091,53.376999999999995,29.775 -2020-05-27 17:00:00,113.92,54.519,56.965,29.775 -2020-05-27 17:15:00,109.55,55.019,56.965,29.775 -2020-05-27 17:30:00,110.79,54.761,56.965,29.775 -2020-05-27 17:45:00,117.21,53.397,56.965,29.775 -2020-05-27 18:00:00,119.07,56.273,58.231,29.775 -2020-05-27 18:15:00,116.37,57.083,58.231,29.775 -2020-05-27 18:30:00,110.78,54.858000000000004,58.231,29.775 -2020-05-27 18:45:00,111.25,60.065,58.231,29.775 -2020-05-27 19:00:00,112.51,60.646,58.865,29.775 -2020-05-27 19:15:00,110.24,59.83,58.865,29.775 -2020-05-27 19:30:00,106.93,59.354,58.865,29.775 -2020-05-27 19:45:00,104.44,59.942,58.865,29.775 -2020-05-27 20:00:00,106.22,58.015,65.605,29.775 -2020-05-27 20:15:00,109.79,57.128,65.605,29.775 -2020-05-27 20:30:00,105.72,56.702,65.605,29.775 -2020-05-27 20:45:00,106.65,56.923,65.605,29.775 -2020-05-27 21:00:00,103.35,54.456,58.083999999999996,29.775 -2020-05-27 21:15:00,102.42,55.998999999999995,58.083999999999996,29.775 -2020-05-27 21:30:00,96.73,56.688,58.083999999999996,29.775 -2020-05-27 21:45:00,90.01,57.614,58.083999999999996,29.775 -2020-05-27 22:00:00,84.55,55.013999999999996,53.243,29.775 -2020-05-27 22:15:00,92.14,55.89,53.243,29.775 -2020-05-27 22:30:00,90.68,48.641999999999996,53.243,29.775 -2020-05-27 22:45:00,91.0,45.221000000000004,53.243,29.775 -2020-05-27 23:00:00,84.6,39.466,44.283,29.775 -2020-05-27 23:15:00,85.5,37.732,44.283,29.775 -2020-05-27 23:30:00,85.54,36.907,44.283,29.775 -2020-05-27 23:45:00,79.34,36.42,44.283,29.775 -2020-05-28 00:00:00,77.91,35.239000000000004,40.219,29.775 -2020-05-28 00:15:00,80.31,35.55,40.219,29.775 -2020-05-28 00:30:00,82.35,34.621,40.219,29.775 -2020-05-28 00:45:00,82.17,33.571,40.219,29.775 -2020-05-28 01:00:00,73.42,33.269,37.959,29.775 -2020-05-28 01:15:00,73.91,32.423,37.959,29.775 -2020-05-28 01:30:00,80.25,30.72,37.959,29.775 -2020-05-28 01:45:00,80.75,29.695,37.959,29.775 -2020-05-28 02:00:00,78.89,29.686,36.113,29.775 -2020-05-28 02:15:00,76.48,28.261999999999997,36.113,29.775 -2020-05-28 02:30:00,73.17,30.505,36.113,29.775 -2020-05-28 02:45:00,71.76,31.307,36.113,29.775 -2020-05-28 03:00:00,82.48,34.086,35.546,29.775 -2020-05-28 03:15:00,81.95,32.857,35.546,29.775 -2020-05-28 03:30:00,83.28,32.132,35.546,29.775 -2020-05-28 03:45:00,79.02,31.68,35.546,29.775 -2020-05-28 04:00:00,76.6,41.464,37.169000000000004,29.775 -2020-05-28 04:15:00,77.69,51.595,37.169000000000004,29.775 -2020-05-28 04:30:00,81.45,49.409,37.169000000000004,29.775 -2020-05-28 04:45:00,85.59,50.132,37.169000000000004,29.775 -2020-05-28 05:00:00,91.9,73.321,41.233000000000004,29.775 -2020-05-28 05:15:00,94.79,91.84899999999999,41.233000000000004,29.775 -2020-05-28 05:30:00,97.76,81.291,41.233000000000004,29.775 -2020-05-28 05:45:00,101.42,75.626,41.233000000000004,29.775 -2020-05-28 06:00:00,106.7,77.21300000000001,52.57,29.775 -2020-05-28 06:15:00,107.83,79.056,52.57,29.775 -2020-05-28 06:30:00,107.22,76.17399999999999,52.57,29.775 -2020-05-28 06:45:00,108.72,76.32600000000001,52.57,29.775 -2020-05-28 07:00:00,112.97,75.663,64.53,29.775 -2020-05-28 07:15:00,113.42,75.118,64.53,29.775 -2020-05-28 07:30:00,111.88,72.117,64.53,29.775 -2020-05-28 07:45:00,110.47,70.532,64.53,29.775 -2020-05-28 08:00:00,108.25,64.778,55.911,29.775 -2020-05-28 08:15:00,109.2,66.418,55.911,29.775 -2020-05-28 08:30:00,108.89,65.07600000000001,55.911,29.775 -2020-05-28 08:45:00,108.36,66.82300000000001,55.911,29.775 -2020-05-28 09:00:00,108.84,63.333999999999996,50.949,29.775 -2020-05-28 09:15:00,109.6,61.867,50.949,29.775 -2020-05-28 09:30:00,109.68,64.991,50.949,29.775 -2020-05-28 09:45:00,109.89,66.501,50.949,29.775 -2020-05-28 10:00:00,109.06,63.222,48.136,29.775 -2020-05-28 10:15:00,109.02,64.71300000000001,48.136,29.775 -2020-05-28 10:30:00,108.21,64.633,48.136,29.775 -2020-05-28 10:45:00,110.76,65.82,48.136,29.775 -2020-05-28 11:00:00,110.74,61.88,46.643,29.775 -2020-05-28 11:15:00,114.54,63.265,46.643,29.775 -2020-05-28 11:30:00,116.21,64.755,46.643,29.775 -2020-05-28 11:45:00,112.65,66.04,46.643,29.775 -2020-05-28 12:00:00,108.35,61.538999999999994,44.098,29.775 -2020-05-28 12:15:00,107.22,61.972,44.098,29.775 -2020-05-28 12:30:00,101.85,60.839,44.098,29.775 -2020-05-28 12:45:00,104.76,62.294,44.098,29.775 -2020-05-28 13:00:00,104.61,62.92100000000001,43.717,29.775 -2020-05-28 13:15:00,106.42,63.643,43.717,29.775 -2020-05-28 13:30:00,104.67,61.792,43.717,29.775 -2020-05-28 13:45:00,106.86,60.025,43.717,29.775 -2020-05-28 14:00:00,109.05,61.505,44.218999999999994,29.775 -2020-05-28 14:15:00,109.96,60.051,44.218999999999994,29.775 -2020-05-28 14:30:00,111.04,58.915,44.218999999999994,29.775 -2020-05-28 14:45:00,111.45,59.58,44.218999999999994,29.775 -2020-05-28 15:00:00,114.19,60.596000000000004,46.159,29.775 -2020-05-28 15:15:00,116.88,58.05,46.159,29.775 -2020-05-28 15:30:00,117.36,56.215,46.159,29.775 -2020-05-28 15:45:00,118.28,53.708999999999996,46.159,29.775 -2020-05-28 16:00:00,120.09,56.588,47.115,29.775 -2020-05-28 16:15:00,117.02,56.037,47.115,29.775 -2020-05-28 16:30:00,117.12,55.543,47.115,29.775 -2020-05-28 16:45:00,115.03,51.835,47.115,29.775 -2020-05-28 17:00:00,115.21,54.302,50.827,29.775 -2020-05-28 17:15:00,114.26,54.784,50.827,29.775 -2020-05-28 17:30:00,113.32,54.513999999999996,50.827,29.775 -2020-05-28 17:45:00,113.31,53.118,50.827,29.775 -2020-05-28 18:00:00,114.29,56.00899999999999,52.586000000000006,29.775 -2020-05-28 18:15:00,111.81,56.798,52.586000000000006,29.775 -2020-05-28 18:30:00,113.58,54.565,52.586000000000006,29.775 -2020-05-28 18:45:00,111.62,59.769,52.586000000000006,29.775 -2020-05-28 19:00:00,107.16,60.352,51.886,29.775 -2020-05-28 19:15:00,104.15,59.531000000000006,51.886,29.775 -2020-05-28 19:30:00,103.5,59.049,51.886,29.775 -2020-05-28 19:45:00,102.04,59.635,51.886,29.775 -2020-05-28 20:00:00,100.18,57.68600000000001,56.162,29.775 -2020-05-28 20:15:00,101.47,56.798,56.162,29.775 -2020-05-28 20:30:00,103.36,56.391000000000005,56.162,29.775 -2020-05-28 20:45:00,99.21,56.652,56.162,29.775 -2020-05-28 21:00:00,100.34,54.191,53.023,29.775 -2020-05-28 21:15:00,99.7,55.746,53.023,29.775 -2020-05-28 21:30:00,97.4,56.413000000000004,53.023,29.775 -2020-05-28 21:45:00,98.85,57.358000000000004,53.023,29.775 -2020-05-28 22:00:00,89.45,54.783,49.303999999999995,29.775 -2020-05-28 22:15:00,90.82,55.674,49.303999999999995,29.775 -2020-05-28 22:30:00,91.85,48.423,49.303999999999995,29.775 -2020-05-28 22:45:00,92.06,44.99,49.303999999999995,29.775 -2020-05-28 23:00:00,82.98,39.205999999999996,43.409,29.775 -2020-05-28 23:15:00,78.65,37.515,43.409,29.775 -2020-05-28 23:30:00,84.66,36.694,43.409,29.775 -2020-05-28 23:45:00,83.7,36.199,43.409,29.775 -2020-05-29 00:00:00,82.83,33.088,39.884,29.775 -2020-05-29 00:15:00,79.0,33.650999999999996,39.884,29.775 -2020-05-29 00:30:00,76.64,32.953,39.884,29.775 -2020-05-29 00:45:00,75.71,32.335,39.884,29.775 -2020-05-29 01:00:00,75.93,31.62,37.658,29.775 -2020-05-29 01:15:00,75.57,30.374000000000002,37.658,29.775 -2020-05-29 01:30:00,74.04,29.29,37.658,29.775 -2020-05-29 01:45:00,79.8,28.044,37.658,29.775 -2020-05-29 02:00:00,80.93,28.939,36.707,29.775 -2020-05-29 02:15:00,81.07,27.433000000000003,36.707,29.775 -2020-05-29 02:30:00,74.85,30.576,36.707,29.775 -2020-05-29 02:45:00,77.58,30.746,36.707,29.775 -2020-05-29 03:00:00,83.0,34.054,37.025,29.775 -2020-05-29 03:15:00,79.28,31.87,37.025,29.775 -2020-05-29 03:30:00,78.44,30.916999999999998,37.025,29.775 -2020-05-29 03:45:00,80.42,31.393,37.025,29.775 -2020-05-29 04:00:00,86.4,41.3,38.349000000000004,29.775 -2020-05-29 04:15:00,88.81,49.849,38.349000000000004,29.775 -2020-05-29 04:30:00,85.87,48.602,38.349000000000004,29.775 -2020-05-29 04:45:00,86.7,48.461000000000006,38.349000000000004,29.775 -2020-05-29 05:00:00,92.38,70.797,41.565,29.775 -2020-05-29 05:15:00,96.58,90.395,41.565,29.775 -2020-05-29 05:30:00,99.6,80.309,41.565,29.775 -2020-05-29 05:45:00,103.24,74.263,41.565,29.775 -2020-05-29 06:00:00,109.02,76.183,53.861000000000004,29.775 -2020-05-29 06:15:00,107.63,77.858,53.861000000000004,29.775 -2020-05-29 06:30:00,108.54,74.766,53.861000000000004,29.775 -2020-05-29 06:45:00,109.06,75.15100000000001,53.861000000000004,29.775 -2020-05-29 07:00:00,110.54,74.923,63.497,29.775 -2020-05-29 07:15:00,108.76,75.47,63.497,29.775 -2020-05-29 07:30:00,108.91,70.567,63.497,29.775 -2020-05-29 07:45:00,107.71,68.62,63.497,29.775 -2020-05-29 08:00:00,107.34,63.365,55.43899999999999,29.775 -2020-05-29 08:15:00,109.81,65.593,55.43899999999999,29.775 -2020-05-29 08:30:00,113.11,64.368,55.43899999999999,29.775 -2020-05-29 08:45:00,111.74,65.642,55.43899999999999,29.775 -2020-05-29 09:00:00,109.3,60.086999999999996,52.132,29.775 -2020-05-29 09:15:00,107.72,60.547,52.132,29.775 -2020-05-29 09:30:00,107.65,62.937,52.132,29.775 -2020-05-29 09:45:00,106.88,64.81,52.132,29.775 -2020-05-29 10:00:00,108.37,61.106,49.881,29.775 -2020-05-29 10:15:00,108.93,62.585,49.881,29.775 -2020-05-29 10:30:00,106.62,63.006,49.881,29.775 -2020-05-29 10:45:00,106.59,64.012,49.881,29.775 -2020-05-29 11:00:00,105.28,60.29,49.396,29.775 -2020-05-29 11:15:00,105.09,60.472,49.396,29.775 -2020-05-29 11:30:00,105.21,62.038999999999994,49.396,29.775 -2020-05-29 11:45:00,107.75,62.458999999999996,49.396,29.775 -2020-05-29 12:00:00,107.33,58.667,46.7,29.775 -2020-05-29 12:15:00,107.68,57.998999999999995,46.7,29.775 -2020-05-29 12:30:00,104.99,56.981,46.7,29.775 -2020-05-29 12:45:00,108.45,57.851000000000006,46.7,29.775 -2020-05-29 13:00:00,103.8,59.25,44.05,29.775 -2020-05-29 13:15:00,104.04,60.341,44.05,29.775 -2020-05-29 13:30:00,98.47,59.233000000000004,44.05,29.775 -2020-05-29 13:45:00,98.7,57.733999999999995,44.05,29.775 -2020-05-29 14:00:00,101.02,58.231,42.805,29.775 -2020-05-29 14:15:00,107.75,57.125,42.805,29.775 -2020-05-29 14:30:00,112.27,57.461000000000006,42.805,29.775 -2020-05-29 14:45:00,112.96,57.571000000000005,42.805,29.775 -2020-05-29 15:00:00,112.92,58.443000000000005,44.36600000000001,29.775 -2020-05-29 15:15:00,113.34,55.558,44.36600000000001,29.775 -2020-05-29 15:30:00,113.29,52.812,44.36600000000001,29.775 -2020-05-29 15:45:00,113.64,51.027,44.36600000000001,29.775 -2020-05-29 16:00:00,113.52,52.907,46.928999999999995,29.775 -2020-05-29 16:15:00,114.14,52.87,46.928999999999995,29.775 -2020-05-29 16:30:00,113.45,52.247,46.928999999999995,29.775 -2020-05-29 16:45:00,114.36,47.736000000000004,46.928999999999995,29.775 -2020-05-29 17:00:00,114.25,51.898,51.468,29.775 -2020-05-29 17:15:00,112.86,52.104,51.468,29.775 -2020-05-29 17:30:00,111.56,51.902,51.468,29.775 -2020-05-29 17:45:00,111.54,50.268,51.468,29.775 -2020-05-29 18:00:00,112.16,53.412,52.58,29.775 -2020-05-29 18:15:00,107.98,53.203,52.58,29.775 -2020-05-29 18:30:00,107.39,50.948,52.58,29.775 -2020-05-29 18:45:00,106.01,56.54600000000001,52.58,29.775 -2020-05-29 19:00:00,102.16,58.183,52.183,29.775 -2020-05-29 19:15:00,99.93,58.201,52.183,29.775 -2020-05-29 19:30:00,97.8,57.68600000000001,52.183,29.775 -2020-05-29 19:45:00,97.65,57.207,52.183,29.775 -2020-05-29 20:00:00,95.98,55.088,58.497,29.775 -2020-05-29 20:15:00,94.76,54.951,58.497,29.775 -2020-05-29 20:30:00,96.05,54.095,58.497,29.775 -2020-05-29 20:45:00,94.03,53.744,58.497,29.775 -2020-05-29 21:00:00,89.57,52.641999999999996,54.731,29.775 -2020-05-29 21:15:00,90.47,55.89,54.731,29.775 -2020-05-29 21:30:00,93.37,56.398999999999994,54.731,29.775 -2020-05-29 21:45:00,92.72,57.673,54.731,29.775 -2020-05-29 22:00:00,88.91,55.195,51.386,29.775 -2020-05-29 22:15:00,88.07,55.836999999999996,51.386,29.775 -2020-05-29 22:30:00,87.69,54.43600000000001,51.386,29.775 -2020-05-29 22:45:00,85.16,52.544,51.386,29.775 -2020-05-29 23:00:00,77.14,48.243,44.463,29.775 -2020-05-29 23:15:00,77.25,44.698,44.463,29.775 -2020-05-29 23:30:00,80.76,41.843999999999994,44.463,29.775 -2020-05-29 23:45:00,79.05,41.086000000000006,44.463,29.775 -2020-05-30 00:00:00,76.42,33.446,42.833999999999996,29.662 -2020-05-30 00:15:00,70.78,32.48,42.833999999999996,29.662 -2020-05-30 00:30:00,68.05,31.618000000000002,42.833999999999996,29.662 -2020-05-30 00:45:00,68.33,30.462,42.833999999999996,29.662 -2020-05-30 01:00:00,68.06,30.156,37.859,29.662 -2020-05-30 01:15:00,67.11,29.238000000000003,37.859,29.662 -2020-05-30 01:30:00,65.83,27.27,37.859,29.662 -2020-05-30 01:45:00,66.87,27.154,37.859,29.662 -2020-05-30 02:00:00,69.9,27.255,35.327,29.662 -2020-05-30 02:15:00,73.43,24.915,35.327,29.662 -2020-05-30 02:30:00,73.36,27.076999999999998,35.327,29.662 -2020-05-30 02:45:00,69.26,28.019000000000002,35.327,29.662 -2020-05-30 03:00:00,65.65,30.165,34.908,29.662 -2020-05-30 03:15:00,65.73,27.055,34.908,29.662 -2020-05-30 03:30:00,66.22,26.081999999999997,34.908,29.662 -2020-05-30 03:45:00,64.96,28.008000000000003,34.908,29.662 -2020-05-30 04:00:00,63.16,35.196999999999996,34.84,29.662 -2020-05-30 04:15:00,63.06,42.316,34.84,29.662 -2020-05-30 04:30:00,60.91,39.02,34.84,29.662 -2020-05-30 04:45:00,65.06,39.03,34.84,29.662 -2020-05-30 05:00:00,64.16,50.458,34.222,29.662 -2020-05-30 05:15:00,65.68,55.727,34.222,29.662 -2020-05-30 05:30:00,66.81,47.24,34.222,29.662 -2020-05-30 05:45:00,68.98,46.597,34.222,29.662 -2020-05-30 06:00:00,71.04,62.797,35.515,29.662 -2020-05-30 06:15:00,72.65,75.111,35.515,29.662 -2020-05-30 06:30:00,72.85,68.265,35.515,29.662 -2020-05-30 06:45:00,75.19,63.847,35.515,29.662 -2020-05-30 07:00:00,78.59,61.262,39.687,29.662 -2020-05-30 07:15:00,80.62,60.335,39.687,29.662 -2020-05-30 07:30:00,84.57,57.398999999999994,39.687,29.662 -2020-05-30 07:45:00,86.6,57.178000000000004,39.687,29.662 -2020-05-30 08:00:00,86.67,53.312,44.9,29.662 -2020-05-30 08:15:00,86.66,56.153999999999996,44.9,29.662 -2020-05-30 08:30:00,87.74,55.246,44.9,29.662 -2020-05-30 08:45:00,88.34,58.092,44.9,29.662 -2020-05-30 09:00:00,88.31,55.77,45.724,29.662 -2020-05-30 09:15:00,89.73,56.852,45.724,29.662 -2020-05-30 09:30:00,90.73,59.906000000000006,45.724,29.662 -2020-05-30 09:45:00,90.92,61.423,45.724,29.662 -2020-05-30 10:00:00,90.8,58.273999999999994,43.123999999999995,29.662 -2020-05-30 10:15:00,91.47,60.143,43.123999999999995,29.662 -2020-05-30 10:30:00,90.99,60.29600000000001,43.123999999999995,29.662 -2020-05-30 10:45:00,93.34,61.288000000000004,43.123999999999995,29.662 -2020-05-30 11:00:00,92.41,57.467,40.255,29.662 -2020-05-30 11:15:00,90.73,58.301,40.255,29.662 -2020-05-30 11:30:00,88.51,59.858999999999995,40.255,29.662 -2020-05-30 11:45:00,87.44,60.681000000000004,40.255,29.662 -2020-05-30 12:00:00,84.12,56.977,38.582,29.662 -2020-05-30 12:15:00,84.24,57.239,38.582,29.662 -2020-05-30 12:30:00,83.18,56.147,38.582,29.662 -2020-05-30 12:45:00,81.0,57.498999999999995,38.582,29.662 -2020-05-30 13:00:00,79.85,58.083999999999996,36.043,29.662 -2020-05-30 13:15:00,84.69,58.198,36.043,29.662 -2020-05-30 13:30:00,83.22,57.165,36.043,29.662 -2020-05-30 13:45:00,79.43,54.551,36.043,29.662 -2020-05-30 14:00:00,77.05,55.413999999999994,35.216,29.662 -2020-05-30 14:15:00,77.16,53.035,35.216,29.662 -2020-05-30 14:30:00,76.32,52.515,35.216,29.662 -2020-05-30 14:45:00,76.2,53.118,35.216,29.662 -2020-05-30 15:00:00,77.38,54.601000000000006,36.759,29.662 -2020-05-30 15:15:00,76.91,52.538000000000004,36.759,29.662 -2020-05-30 15:30:00,75.07,50.324,36.759,29.662 -2020-05-30 15:45:00,72.67,47.688,36.759,29.662 -2020-05-30 16:00:00,73.75,51.426,40.086,29.662 -2020-05-30 16:15:00,77.63,50.755,40.086,29.662 -2020-05-30 16:30:00,77.61,50.346000000000004,40.086,29.662 -2020-05-30 16:45:00,79.19,45.966,40.086,29.662 -2020-05-30 17:00:00,80.06,48.941,44.876999999999995,29.662 -2020-05-30 17:15:00,80.99,47.513999999999996,44.876999999999995,29.662 -2020-05-30 17:30:00,81.71,47.153999999999996,44.876999999999995,29.662 -2020-05-30 17:45:00,82.2,45.856,44.876999999999995,29.662 -2020-05-30 18:00:00,84.63,50.298,47.056000000000004,29.662 -2020-05-30 18:15:00,85.41,52.016999999999996,47.056000000000004,29.662 -2020-05-30 18:30:00,82.68,51.31,47.056000000000004,29.662 -2020-05-30 18:45:00,82.12,52.935,47.056000000000004,29.662 -2020-05-30 19:00:00,81.55,53.339,45.57,29.662 -2020-05-30 19:15:00,78.78,52.294,45.57,29.662 -2020-05-30 19:30:00,77.84,52.655,45.57,29.662 -2020-05-30 19:45:00,77.76,53.809,45.57,29.662 -2020-05-30 20:00:00,73.8,52.851000000000006,41.685,29.662 -2020-05-30 20:15:00,77.24,52.566,41.685,29.662 -2020-05-30 20:30:00,78.75,50.82899999999999,41.685,29.662 -2020-05-30 20:45:00,79.95,52.173,41.685,29.662 -2020-05-30 21:00:00,76.15,50.175,39.576,29.662 -2020-05-30 21:15:00,75.58,53.202,39.576,29.662 -2020-05-30 21:30:00,73.83,54.123000000000005,39.576,29.662 -2020-05-30 21:45:00,73.25,54.816,39.576,29.662 -2020-05-30 22:00:00,69.37,52.62,39.068000000000005,29.662 -2020-05-30 22:15:00,70.64,54.028999999999996,39.068000000000005,29.662 -2020-05-30 22:30:00,66.88,53.56100000000001,39.068000000000005,29.662 -2020-05-30 22:45:00,65.37,52.398999999999994,39.068000000000005,29.662 -2020-05-30 23:00:00,61.82,47.92100000000001,32.06,29.662 -2020-05-30 23:15:00,61.6,44.477,32.06,29.662 -2020-05-30 23:30:00,60.72,43.583999999999996,32.06,29.662 -2020-05-30 23:45:00,59.52,42.652,32.06,29.662 -2020-05-31 00:00:00,58.29,34.622,28.825,29.662 -2020-05-31 00:15:00,58.97,32.507,28.825,29.662 -2020-05-31 00:30:00,58.75,31.43,28.825,29.662 -2020-05-31 00:45:00,58.69,30.32,28.825,29.662 -2020-05-31 01:00:00,56.94,30.25,25.995,29.662 -2020-05-31 01:15:00,57.91,29.410999999999998,25.995,29.662 -2020-05-31 01:30:00,54.43,27.41,25.995,29.662 -2020-05-31 01:45:00,57.82,26.866,25.995,29.662 -2020-05-31 02:00:00,57.05,26.883000000000003,24.394000000000002,29.662 -2020-05-31 02:15:00,57.16,25.024,24.394000000000002,29.662 -2020-05-31 02:30:00,57.04,27.545,24.394000000000002,29.662 -2020-05-31 02:45:00,57.15,28.318,24.394000000000002,29.662 -2020-05-31 03:00:00,56.31,31.146,22.916999999999998,29.662 -2020-05-31 03:15:00,56.76,28.165,22.916999999999998,29.662 -2020-05-31 03:30:00,57.63,26.795,22.916999999999998,29.662 -2020-05-31 03:45:00,57.02,27.980999999999998,22.916999999999998,29.662 -2020-05-31 04:00:00,54.07,35.034,23.576999999999998,29.662 -2020-05-31 04:15:00,53.44,41.483000000000004,23.576999999999998,29.662 -2020-05-31 04:30:00,52.41,39.448,23.576999999999998,29.662 -2020-05-31 04:45:00,52.3,39.071999999999996,23.576999999999998,29.662 -2020-05-31 05:00:00,51.54,49.851000000000006,22.730999999999998,29.662 -2020-05-31 05:15:00,50.75,53.773999999999994,22.730999999999998,29.662 -2020-05-31 05:30:00,50.89,44.93899999999999,22.730999999999998,29.662 -2020-05-31 05:45:00,52.44,44.14,22.730999999999998,29.662 -2020-05-31 06:00:00,54.87,58.076,22.34,29.662 -2020-05-31 06:15:00,54.95,70.82600000000001,22.34,29.662 -2020-05-31 06:30:00,56.67,63.121,22.34,29.662 -2020-05-31 06:45:00,59.11,57.573,22.34,29.662 -2020-05-31 07:00:00,60.32,55.717,24.691999999999997,29.662 -2020-05-31 07:15:00,62.84,53.083,24.691999999999997,29.662 -2020-05-31 07:30:00,62.65,51.083999999999996,24.691999999999997,29.662 -2020-05-31 07:45:00,62.23,50.706,24.691999999999997,29.662 -2020-05-31 08:00:00,63.06,47.919,29.340999999999998,29.662 -2020-05-31 08:15:00,62.23,51.89,29.340999999999998,29.662 -2020-05-31 08:30:00,64.21,52.13399999999999,29.340999999999998,29.662 -2020-05-31 08:45:00,65.4,55.338,29.340999999999998,29.662 -2020-05-31 09:00:00,62.96,52.817,30.788,29.662 -2020-05-31 09:15:00,61.99,53.562,30.788,29.662 -2020-05-31 09:30:00,59.44,56.989,30.788,29.662 -2020-05-31 09:45:00,62.19,59.445,30.788,29.662 -2020-05-31 10:00:00,62.46,57.333999999999996,30.158,29.662 -2020-05-31 10:15:00,62.53,59.471000000000004,30.158,29.662 -2020-05-31 10:30:00,63.05,59.968999999999994,30.158,29.662 -2020-05-31 10:45:00,65.84,61.492,30.158,29.662 -2020-05-31 11:00:00,61.04,57.544,32.056,29.662 -2020-05-31 11:15:00,61.5,58.008,32.056,29.662 -2020-05-31 11:30:00,63.18,59.845,32.056,29.662 -2020-05-31 11:45:00,61.81,61.044,32.056,29.662 -2020-05-31 12:00:00,59.18,58.198,28.671999999999997,29.662 -2020-05-31 12:15:00,59.97,58.324,28.671999999999997,29.662 -2020-05-31 12:30:00,55.08,57.066,28.671999999999997,29.662 -2020-05-31 12:45:00,55.72,57.618,28.671999999999997,29.662 -2020-05-31 13:00:00,57.1,57.784,23.171,29.662 -2020-05-31 13:15:00,57.89,58.04600000000001,23.171,29.662 -2020-05-31 13:30:00,60.84,55.958,23.171,29.662 -2020-05-31 13:45:00,58.45,54.222,23.171,29.662 -2020-05-31 14:00:00,55.33,56.27,19.11,29.662 -2020-05-31 14:15:00,57.48,54.55,19.11,29.662 -2020-05-31 14:30:00,55.43,53.14,19.11,29.662 -2020-05-31 14:45:00,56.24,52.672,19.11,29.662 -2020-05-31 15:00:00,56.35,53.988,19.689,29.662 -2020-05-31 15:15:00,59.22,51.339,19.689,29.662 -2020-05-31 15:30:00,57.36,49.071999999999996,19.689,29.662 -2020-05-31 15:45:00,59.35,46.838,19.689,29.662 -2020-05-31 16:00:00,63.4,49.468,22.875,29.662 -2020-05-31 16:15:00,64.4,48.803999999999995,22.875,29.662 -2020-05-31 16:30:00,66.23,49.398999999999994,22.875,29.662 -2020-05-31 16:45:00,67.91,45.062,22.875,29.662 -2020-05-31 17:00:00,72.6,48.413999999999994,33.884,29.662 -2020-05-31 17:15:00,71.61,48.3,33.884,29.662 -2020-05-31 17:30:00,75.13,48.739,33.884,29.662 -2020-05-31 17:45:00,76.52,48.29,33.884,29.662 -2020-05-31 18:00:00,79.29,53.143,38.453,29.662 -2020-05-31 18:15:00,79.24,54.731,38.453,29.662 -2020-05-31 18:30:00,79.41,53.353,38.453,29.662 -2020-05-31 18:45:00,83.37,55.465,38.453,29.662 -2020-05-31 19:00:00,80.11,57.818000000000005,39.221,29.662 -2020-05-31 19:15:00,85.23,55.82899999999999,39.221,29.662 -2020-05-31 19:30:00,84.73,55.917,39.221,29.662 -2020-05-31 19:45:00,86.55,56.916000000000004,39.221,29.662 -2020-05-31 20:00:00,86.5,56.137,37.871,29.662 -2020-05-31 20:15:00,86.56,55.92,37.871,29.662 -2020-05-31 20:30:00,87.79,55.214,37.871,29.662 -2020-05-31 20:45:00,86.92,54.773999999999994,37.871,29.662 -2020-05-31 21:00:00,81.05,52.123999999999995,36.465,29.662 -2020-05-31 21:15:00,84.64,54.754,36.465,29.662 -2020-05-31 21:30:00,78.67,55.098,36.465,29.662 -2020-05-31 21:45:00,78.8,56.174,36.465,29.662 -2020-05-31 22:00:00,77.15,55.681999999999995,36.092,29.662 -2020-05-31 22:15:00,82.27,55.36600000000001,36.092,29.662 -2020-05-31 22:30:00,81.25,53.788000000000004,36.092,29.662 -2020-05-31 22:45:00,78.71,51.242,36.092,29.662 -2020-05-31 23:00:00,54.1,45.903999999999996,31.013,29.662 -2020-05-31 23:15:00,55.38,44.067,31.013,29.662 -2020-05-31 23:30:00,54.63,42.828,31.013,29.662 -2020-05-31 23:45:00,53.74,42.213,31.013,29.662 -2020-06-01 00:00:00,52.35,28.079,19.295,29.17 -2020-06-01 00:15:00,52.65,26.704,19.295,29.17 -2020-06-01 00:30:00,52.03,25.386999999999997,19.295,29.17 -2020-06-01 00:45:00,52.09,24.485,19.295,29.17 -2020-06-01 01:00:00,50.91,24.236,15.365,29.17 -2020-06-01 01:15:00,52.12,23.596,15.365,29.17 -2020-06-01 01:30:00,50.95,21.804000000000002,15.365,29.17 -2020-06-01 01:45:00,51.01,21.910999999999998,15.365,29.17 -2020-06-01 02:00:00,50.17,21.433000000000003,13.03,29.17 -2020-06-01 02:15:00,50.81,19.726,13.03,29.17 -2020-06-01 02:30:00,50.97,22.149,13.03,29.17 -2020-06-01 02:45:00,51.9,22.715999999999998,13.03,29.17 -2020-06-01 03:00:00,51.98,24.44,13.46,29.17 -2020-06-01 03:15:00,52.07,21.471,13.46,29.17 -2020-06-01 03:30:00,52.7,20.089000000000002,13.46,29.17 -2020-06-01 03:45:00,49.25,20.781,13.46,29.17 -2020-06-01 04:00:00,51.66,26.259,13.305,29.17 -2020-06-01 04:15:00,51.17,32.0,13.305,29.17 -2020-06-01 04:30:00,48.48,29.869,13.305,29.17 -2020-06-01 04:45:00,50.55,29.331999999999997,13.305,29.17 -2020-06-01 05:00:00,48.78,38.022,13.482000000000001,29.17 -2020-06-01 05:15:00,47.36,39.586,13.482000000000001,29.17 -2020-06-01 05:30:00,50.13,31.343000000000004,13.482000000000001,29.17 -2020-06-01 05:45:00,50.0,31.281,13.482000000000001,29.17 -2020-06-01 06:00:00,51.06,43.007,14.677999999999999,29.17 -2020-06-01 06:15:00,50.92,53.478,14.677999999999999,29.17 -2020-06-01 06:30:00,51.78,46.945,14.677999999999999,29.17 -2020-06-01 06:45:00,52.65,42.336999999999996,14.677999999999999,29.17 -2020-06-01 07:00:00,54.68,42.636,18.473,29.17 -2020-06-01 07:15:00,53.98,40.101,18.473,29.17 -2020-06-01 07:30:00,53.67,38.333,18.473,29.17 -2020-06-01 07:45:00,54.26,38.079,18.473,29.17 -2020-06-01 08:00:00,54.22,38.016,18.142,29.17 -2020-06-01 08:15:00,53.58,41.693999999999996,18.142,29.17 -2020-06-01 08:30:00,52.87,42.685,18.142,29.17 -2020-06-01 08:45:00,53.28,45.778999999999996,18.142,29.17 -2020-06-01 09:00:00,50.94,42.258,19.148,29.17 -2020-06-01 09:15:00,51.9,43.126999999999995,19.148,29.17 -2020-06-01 09:30:00,47.84,46.728,19.148,29.17 -2020-06-01 09:45:00,51.24,49.751999999999995,19.148,29.17 -2020-06-01 10:00:00,53.22,45.975,17.139,29.17 -2020-06-01 10:15:00,55.99,47.87,17.139,29.17 -2020-06-01 10:30:00,57.89,48.418,17.139,29.17 -2020-06-01 10:45:00,57.81,50.29600000000001,17.139,29.17 -2020-06-01 11:00:00,53.25,45.78,18.037,29.17 -2020-06-01 11:15:00,51.93,46.23,18.037,29.17 -2020-06-01 11:30:00,49.85,48.203,18.037,29.17 -2020-06-01 11:45:00,49.1,49.861000000000004,18.037,29.17 -2020-06-01 12:00:00,46.61,47.415,16.559,29.17 -2020-06-01 12:15:00,46.04,47.076,16.559,29.17 -2020-06-01 12:30:00,44.13,46.448,16.559,29.17 -2020-06-01 12:45:00,44.13,47.167,16.559,29.17 -2020-06-01 13:00:00,43.0,47.331,13.697000000000001,29.17 -2020-06-01 13:15:00,42.54,47.643,13.697000000000001,29.17 -2020-06-01 13:30:00,42.78,45.68899999999999,13.697000000000001,29.17 -2020-06-01 13:45:00,44.17,44.308,13.697000000000001,29.17 -2020-06-01 14:00:00,44.11,46.63399999999999,12.578,29.17 -2020-06-01 14:15:00,43.14,44.994,12.578,29.17 -2020-06-01 14:30:00,44.66,43.99,12.578,29.17 -2020-06-01 14:45:00,44.56,43.341,12.578,29.17 -2020-06-01 15:00:00,47.23,44.93899999999999,14.425999999999998,29.17 -2020-06-01 15:15:00,46.69,42.405,14.425999999999998,29.17 -2020-06-01 15:30:00,50.96,40.277,14.425999999999998,29.17 -2020-06-01 15:45:00,50.68,38.414,14.425999999999998,29.17 -2020-06-01 16:00:00,52.39,40.319,18.287,29.17 -2020-06-01 16:15:00,55.05,39.81,18.287,29.17 -2020-06-01 16:30:00,57.45,39.958,18.287,29.17 -2020-06-01 16:45:00,61.38,36.124,18.287,29.17 -2020-06-01 17:00:00,65.43,39.303000000000004,24.461,29.17 -2020-06-01 17:15:00,66.16,38.798,24.461,29.17 -2020-06-01 17:30:00,68.01,38.955999999999996,24.461,29.17 -2020-06-01 17:45:00,69.86,38.388000000000005,24.461,29.17 -2020-06-01 18:00:00,71.38,42.556000000000004,31.44,29.17 -2020-06-01 18:15:00,71.03,43.601000000000006,31.44,29.17 -2020-06-01 18:30:00,73.56,42.254,31.44,29.17 -2020-06-01 18:45:00,79.8,43.708999999999996,31.44,29.17 -2020-06-01 19:00:00,80.73,46.256,34.859,29.17 -2020-06-01 19:15:00,73.32,44.074,34.859,29.17 -2020-06-01 19:30:00,73.38,43.883,34.859,29.17 -2020-06-01 19:45:00,70.97,44.55,34.859,29.17 -2020-06-01 20:00:00,73.37,44.213,42.937,29.17 -2020-06-01 20:15:00,81.13,44.318999999999996,42.937,29.17 -2020-06-01 20:30:00,83.24,43.968,42.937,29.17 -2020-06-01 20:45:00,81.93,43.483999999999995,42.937,29.17 -2020-06-01 21:00:00,74.91,41.38399999999999,39.795,29.17 -2020-06-01 21:15:00,74.14,44.074,39.795,29.17 -2020-06-01 21:30:00,74.04,44.778999999999996,39.795,29.17 -2020-06-01 21:45:00,70.96,45.748999999999995,39.795,29.17 -2020-06-01 22:00:00,72.4,44.92100000000001,41.108000000000004,29.17 -2020-06-01 22:15:00,76.24,45.008,41.108000000000004,29.17 -2020-06-01 22:30:00,72.97,43.971000000000004,41.108000000000004,29.17 -2020-06-01 22:45:00,68.25,41.247,41.108000000000004,29.17 -2020-06-01 23:00:00,75.51,37.607,33.82,29.17 -2020-06-01 23:15:00,76.08,35.554,33.82,29.17 -2020-06-01 23:30:00,75.77,34.259,33.82,29.17 -2020-06-01 23:45:00,75.39,33.885,33.82,29.17 -2020-06-02 00:00:00,72.23,27.486,44.625,29.28 -2020-06-02 00:15:00,72.85,28.073,44.625,29.28 -2020-06-02 00:30:00,72.81,27.004,44.625,29.28 -2020-06-02 00:45:00,72.9,26.359,44.625,29.28 -2020-06-02 01:00:00,72.66,25.963,41.733000000000004,29.28 -2020-06-02 01:15:00,73.01,25.336,41.733000000000004,29.28 -2020-06-02 01:30:00,71.48,23.773000000000003,41.733000000000004,29.28 -2020-06-02 01:45:00,71.71,23.325,41.733000000000004,29.28 -2020-06-02 02:00:00,71.14,22.816,39.872,29.28 -2020-06-02 02:15:00,72.89,21.364,39.872,29.28 -2020-06-02 02:30:00,72.11,23.55,39.872,29.28 -2020-06-02 02:45:00,72.88,24.247,39.872,29.28 -2020-06-02 03:00:00,72.62,25.929000000000002,38.711,29.28 -2020-06-02 03:15:00,73.57,24.605999999999998,38.711,29.28 -2020-06-02 03:30:00,74.97,23.865,38.711,29.28 -2020-06-02 03:45:00,74.58,23.045,38.711,29.28 -2020-06-02 04:00:00,76.34,30.565,39.823,29.28 -2020-06-02 04:15:00,80.5,39.384,39.823,29.28 -2020-06-02 04:30:00,80.66,36.830999999999996,39.823,29.28 -2020-06-02 04:45:00,85.79,37.228,39.823,29.28 -2020-06-02 05:00:00,99.7,55.825,43.228,29.28 -2020-06-02 05:15:00,105.56,69.34,43.228,29.28 -2020-06-02 05:30:00,108.46,59.775,43.228,29.28 -2020-06-02 05:45:00,108.08,55.667,43.228,29.28 -2020-06-02 06:00:00,115.07,57.045,54.316,29.28 -2020-06-02 06:15:00,119.43,57.893,54.316,29.28 -2020-06-02 06:30:00,122.79,55.489,54.316,29.28 -2020-06-02 06:45:00,121.32,55.802,54.316,29.28 -2020-06-02 07:00:00,119.92,56.843,65.758,29.28 -2020-06-02 07:15:00,127.93,56.339,65.758,29.28 -2020-06-02 07:30:00,129.28,53.638999999999996,65.758,29.28 -2020-06-02 07:45:00,129.07,52.556999999999995,65.758,29.28 -2020-06-02 08:00:00,127.22,49.989,57.983000000000004,29.28 -2020-06-02 08:15:00,131.77,51.858999999999995,57.983000000000004,29.28 -2020-06-02 08:30:00,134.36,51.706,57.983000000000004,29.28 -2020-06-02 08:45:00,133.94,53.798,57.983000000000004,29.28 -2020-06-02 09:00:00,128.31,49.465,52.653,29.28 -2020-06-02 09:15:00,126.41,48.339,52.653,29.28 -2020-06-02 09:30:00,129.84,51.693999999999996,52.653,29.28 -2020-06-02 09:45:00,130.52,53.861000000000004,52.653,29.28 -2020-06-02 10:00:00,129.02,49.056999999999995,51.408,29.28 -2020-06-02 10:15:00,128.01,50.575,51.408,29.28 -2020-06-02 10:30:00,124.54,50.652,51.408,29.28 -2020-06-02 10:45:00,130.5,52.098,51.408,29.28 -2020-06-02 11:00:00,130.58,47.683,51.913000000000004,29.28 -2020-06-02 11:15:00,129.85,48.934,51.913000000000004,29.28 -2020-06-02 11:30:00,124.12,50.481,51.913000000000004,29.28 -2020-06-02 11:45:00,123.31,52.216,51.913000000000004,29.28 -2020-06-02 12:00:00,125.14,48.06100000000001,49.508,29.28 -2020-06-02 12:15:00,122.36,48.11600000000001,49.508,29.28 -2020-06-02 12:30:00,118.37,47.354,49.508,29.28 -2020-06-02 12:45:00,115.76,48.853,49.508,29.28 -2020-06-02 13:00:00,120.83,49.547,50.007,29.28 -2020-06-02 13:15:00,121.9,50.588,50.007,29.28 -2020-06-02 13:30:00,116.48,48.832,50.007,29.28 -2020-06-02 13:45:00,110.09,47.396,50.007,29.28 -2020-06-02 14:00:00,109.79,49.284,49.778999999999996,29.28 -2020-06-02 14:15:00,111.56,47.966,49.778999999999996,29.28 -2020-06-02 14:30:00,112.09,47.093999999999994,49.778999999999996,29.28 -2020-06-02 14:45:00,114.29,47.645,49.778999999999996,29.28 -2020-06-02 15:00:00,110.48,48.917,51.559,29.28 -2020-06-02 15:15:00,103.02,46.593999999999994,51.559,29.28 -2020-06-02 15:30:00,100.93,44.923,51.559,29.28 -2020-06-02 15:45:00,105.33,42.842,51.559,29.28 -2020-06-02 16:00:00,105.0,45.167,53.531000000000006,29.28 -2020-06-02 16:15:00,105.3,44.769,53.531000000000006,29.28 -2020-06-02 16:30:00,106.76,43.912,53.531000000000006,29.28 -2020-06-02 16:45:00,111.6,40.665,53.531000000000006,29.28 -2020-06-02 17:00:00,113.3,43.038999999999994,59.497,29.28 -2020-06-02 17:15:00,109.25,43.193000000000005,59.497,29.28 -2020-06-02 17:30:00,106.26,42.56,59.497,29.28 -2020-06-02 17:45:00,108.2,41.12,59.497,29.28 -2020-06-02 18:00:00,110.8,43.38,59.861999999999995,29.28 -2020-06-02 18:15:00,114.43,43.85,59.861999999999995,29.28 -2020-06-02 18:30:00,111.78,41.553999999999995,59.861999999999995,29.28 -2020-06-02 18:45:00,107.85,45.961000000000006,59.861999999999995,29.28 -2020-06-02 19:00:00,102.8,46.961999999999996,60.989,29.28 -2020-06-02 19:15:00,105.39,46.086000000000006,60.989,29.28 -2020-06-02 19:30:00,98.65,45.317,60.989,29.28 -2020-06-02 19:45:00,98.03,45.648,60.989,29.28 -2020-06-02 20:00:00,95.94,44.276,68.35600000000001,29.28 -2020-06-02 20:15:00,101.74,44.063,68.35600000000001,29.28 -2020-06-02 20:30:00,101.49,44.187,68.35600000000001,29.28 -2020-06-02 20:45:00,102.03,44.443000000000005,68.35600000000001,29.28 -2020-06-02 21:00:00,92.62,42.57,59.251000000000005,29.28 -2020-06-02 21:15:00,92.31,44.255,59.251000000000005,29.28 -2020-06-02 21:30:00,87.58,45.371,59.251000000000005,29.28 -2020-06-02 21:45:00,86.58,46.31399999999999,59.251000000000005,29.28 -2020-06-02 22:00:00,81.88,43.613,54.736999999999995,29.28 -2020-06-02 22:15:00,80.55,45.158,54.736999999999995,29.28 -2020-06-02 22:30:00,78.5,39.465,54.736999999999995,29.28 -2020-06-02 22:45:00,77.43,36.318000000000005,54.736999999999995,29.28 -2020-06-02 23:00:00,74.39,32.014,46.806999999999995,29.28 -2020-06-02 23:15:00,74.81,30.081,46.806999999999995,29.28 -2020-06-02 23:30:00,74.8,28.956999999999997,46.806999999999995,29.28 -2020-06-02 23:45:00,73.41,28.416999999999998,46.806999999999995,29.28 -2020-06-03 00:00:00,70.04,27.325,43.824,29.28 -2020-06-03 00:15:00,71.56,27.914,43.824,29.28 -2020-06-03 00:30:00,68.67,26.846,43.824,29.28 -2020-06-03 00:45:00,70.95,26.205,43.824,29.28 -2020-06-03 01:00:00,71.49,25.829,39.86,29.28 -2020-06-03 01:15:00,71.44,25.180999999999997,39.86,29.28 -2020-06-03 01:30:00,70.05,23.608,39.86,29.28 -2020-06-03 01:45:00,70.5,23.154,39.86,29.28 -2020-06-03 02:00:00,70.07,22.645,37.931999999999995,29.28 -2020-06-03 02:15:00,71.93,21.186999999999998,37.931999999999995,29.28 -2020-06-03 02:30:00,70.28,23.375,37.931999999999995,29.28 -2020-06-03 02:45:00,70.45,24.079,37.931999999999995,29.28 -2020-06-03 03:00:00,71.64,25.763,37.579,29.28 -2020-06-03 03:15:00,72.7,24.436999999999998,37.579,29.28 -2020-06-03 03:30:00,72.09,23.701,37.579,29.28 -2020-06-03 03:45:00,72.31,22.904,37.579,29.28 -2020-06-03 04:00:00,76.85,30.365,37.931999999999995,29.28 -2020-06-03 04:15:00,80.4,39.134,37.931999999999995,29.28 -2020-06-03 04:30:00,88.31,36.568000000000005,37.931999999999995,29.28 -2020-06-03 04:45:00,93.4,36.96,37.931999999999995,29.28 -2020-06-03 05:00:00,94.29,55.43600000000001,40.942,29.28 -2020-06-03 05:15:00,98.54,68.80199999999999,40.942,29.28 -2020-06-03 05:30:00,101.92,59.305,40.942,29.28 -2020-06-03 05:45:00,106.14,55.255,40.942,29.28 -2020-06-03 06:00:00,113.26,56.653,56.516999999999996,29.28 -2020-06-03 06:15:00,112.2,57.481,56.516999999999996,29.28 -2020-06-03 06:30:00,106.99,55.099,56.516999999999996,29.28 -2020-06-03 06:45:00,110.19,55.44,56.516999999999996,29.28 -2020-06-03 07:00:00,113.12,56.468999999999994,71.707,29.28 -2020-06-03 07:15:00,116.27,55.978,71.707,29.28 -2020-06-03 07:30:00,115.88,53.263000000000005,71.707,29.28 -2020-06-03 07:45:00,113.15,52.215,71.707,29.28 -2020-06-03 08:00:00,113.28,49.655,61.17,29.28 -2020-06-03 08:15:00,115.37,51.568000000000005,61.17,29.28 -2020-06-03 08:30:00,116.39,51.406000000000006,61.17,29.28 -2020-06-03 08:45:00,112.02,53.507,61.17,29.28 -2020-06-03 09:00:00,110.21,49.167,57.282,29.28 -2020-06-03 09:15:00,113.3,48.047,57.282,29.28 -2020-06-03 09:30:00,110.77,51.406000000000006,57.282,29.28 -2020-06-03 09:45:00,112.52,53.597,57.282,29.28 -2020-06-03 10:00:00,114.37,48.805,54.026,29.28 -2020-06-03 10:15:00,114.82,50.343,54.026,29.28 -2020-06-03 10:30:00,116.42,50.426,54.026,29.28 -2020-06-03 10:45:00,116.13,51.88,54.026,29.28 -2020-06-03 11:00:00,109.87,47.458,54.277,29.28 -2020-06-03 11:15:00,104.76,48.72,54.277,29.28 -2020-06-03 11:30:00,108.82,50.256,54.277,29.28 -2020-06-03 11:45:00,108.87,51.997,54.277,29.28 -2020-06-03 12:00:00,106.62,47.876000000000005,52.552,29.28 -2020-06-03 12:15:00,102.67,47.937,52.552,29.28 -2020-06-03 12:30:00,98.68,47.147,52.552,29.28 -2020-06-03 12:45:00,105.85,48.65,52.552,29.28 -2020-06-03 13:00:00,107.79,49.343999999999994,52.111999999999995,29.28 -2020-06-03 13:15:00,108.61,50.388000000000005,52.111999999999995,29.28 -2020-06-03 13:30:00,102.26,48.643,52.111999999999995,29.28 -2020-06-03 13:45:00,104.72,47.208,52.111999999999995,29.28 -2020-06-03 14:00:00,111.64,49.123000000000005,52.066,29.28 -2020-06-03 14:15:00,108.26,47.799,52.066,29.28 -2020-06-03 14:30:00,108.8,46.897,52.066,29.28 -2020-06-03 14:45:00,103.39,47.453,52.066,29.28 -2020-06-03 15:00:00,103.63,48.77,52.523999999999994,29.28 -2020-06-03 15:15:00,105.44,46.435,52.523999999999994,29.28 -2020-06-03 15:30:00,104.72,44.748999999999995,52.523999999999994,29.28 -2020-06-03 15:45:00,100.41,42.653999999999996,52.523999999999994,29.28 -2020-06-03 16:00:00,100.42,45.018,54.101000000000006,29.28 -2020-06-03 16:15:00,110.51,44.611000000000004,54.101000000000006,29.28 -2020-06-03 16:30:00,111.07,43.773999999999994,54.101000000000006,29.28 -2020-06-03 16:45:00,109.26,40.488,54.101000000000006,29.28 -2020-06-03 17:00:00,104.98,42.896,58.155,29.28 -2020-06-03 17:15:00,111.12,43.037,58.155,29.28 -2020-06-03 17:30:00,114.79,42.395,58.155,29.28 -2020-06-03 17:45:00,115.69,40.927,58.155,29.28 -2020-06-03 18:00:00,108.75,43.202,60.205,29.28 -2020-06-03 18:15:00,112.12,43.644,60.205,29.28 -2020-06-03 18:30:00,114.87,41.341,60.205,29.28 -2020-06-03 18:45:00,113.03,45.747,60.205,29.28 -2020-06-03 19:00:00,104.74,46.748999999999995,61.568999999999996,29.28 -2020-06-03 19:15:00,99.58,45.865,61.568999999999996,29.28 -2020-06-03 19:30:00,104.58,45.088,61.568999999999996,29.28 -2020-06-03 19:45:00,104.49,45.413999999999994,61.568999999999996,29.28 -2020-06-03 20:00:00,102.46,44.022,68.145,29.28 -2020-06-03 20:15:00,95.88,43.805,68.145,29.28 -2020-06-03 20:30:00,95.33,43.943999999999996,68.145,29.28 -2020-06-03 20:45:00,95.16,44.238,68.145,29.28 -2020-06-03 21:00:00,92.82,42.37,59.696000000000005,29.28 -2020-06-03 21:15:00,92.05,44.066,59.696000000000005,29.28 -2020-06-03 21:30:00,89.64,45.158,59.696000000000005,29.28 -2020-06-03 21:45:00,88.21,46.118,59.696000000000005,29.28 -2020-06-03 22:00:00,83.59,43.44,54.861999999999995,29.28 -2020-06-03 22:15:00,82.97,44.998000000000005,54.861999999999995,29.28 -2020-06-03 22:30:00,81.26,39.305,54.861999999999995,29.28 -2020-06-03 22:45:00,80.33,36.147,54.861999999999995,29.28 -2020-06-03 23:00:00,75.58,31.816999999999997,45.568000000000005,29.28 -2020-06-03 23:15:00,75.33,29.924,45.568000000000005,29.28 -2020-06-03 23:30:00,74.44,28.808000000000003,45.568000000000005,29.28 -2020-06-03 23:45:00,73.46,28.256999999999998,45.568000000000005,29.28 -2020-06-04 00:00:00,73.36,27.168000000000003,40.181,29.28 -2020-06-04 00:15:00,73.36,27.758000000000003,40.181,29.28 -2020-06-04 00:30:00,72.26,26.691999999999997,40.181,29.28 -2020-06-04 00:45:00,74.73,26.054000000000002,40.181,29.28 -2020-06-04 01:00:00,71.88,25.698,38.296,29.28 -2020-06-04 01:15:00,72.5,25.03,38.296,29.28 -2020-06-04 01:30:00,71.31,23.448,38.296,29.28 -2020-06-04 01:45:00,71.5,22.986,38.296,29.28 -2020-06-04 02:00:00,71.83,22.479,36.575,29.28 -2020-06-04 02:15:00,71.45,21.015,36.575,29.28 -2020-06-04 02:30:00,71.1,23.204,36.575,29.28 -2020-06-04 02:45:00,71.42,23.915,36.575,29.28 -2020-06-04 03:00:00,73.16,25.601,36.394,29.28 -2020-06-04 03:15:00,72.99,24.271,36.394,29.28 -2020-06-04 03:30:00,71.02,23.54,36.394,29.28 -2020-06-04 03:45:00,73.01,22.767,36.394,29.28 -2020-06-04 04:00:00,77.6,30.171,37.207,29.28 -2020-06-04 04:15:00,77.75,38.889,37.207,29.28 -2020-06-04 04:30:00,88.08,36.309,37.207,29.28 -2020-06-04 04:45:00,93.23,36.698,37.207,29.28 -2020-06-04 05:00:00,94.94,55.053999999999995,40.713,29.28 -2020-06-04 05:15:00,96.67,68.271,40.713,29.28 -2020-06-04 05:30:00,104.59,58.843999999999994,40.713,29.28 -2020-06-04 05:45:00,107.94,54.852,40.713,29.28 -2020-06-04 06:00:00,112.46,56.268,50.952,29.28 -2020-06-04 06:15:00,109.5,57.075,50.952,29.28 -2020-06-04 06:30:00,108.67,54.715,50.952,29.28 -2020-06-04 06:45:00,109.33,55.085,50.952,29.28 -2020-06-04 07:00:00,110.44,56.102,64.88,29.28 -2020-06-04 07:15:00,109.83,55.625,64.88,29.28 -2020-06-04 07:30:00,113.96,52.897,64.88,29.28 -2020-06-04 07:45:00,116.37,51.881,64.88,29.28 -2020-06-04 08:00:00,114.22,49.33,55.133,29.28 -2020-06-04 08:15:00,107.51,51.283,55.133,29.28 -2020-06-04 08:30:00,112.36,51.113,55.133,29.28 -2020-06-04 08:45:00,109.8,53.223,55.133,29.28 -2020-06-04 09:00:00,114.08,48.876000000000005,48.912,29.28 -2020-06-04 09:15:00,119.39,47.761,48.912,29.28 -2020-06-04 09:30:00,116.25,51.125,48.912,29.28 -2020-06-04 09:45:00,110.92,53.34,48.912,29.28 -2020-06-04 10:00:00,107.25,48.559,45.968999999999994,29.28 -2020-06-04 10:15:00,107.81,50.117,45.968999999999994,29.28 -2020-06-04 10:30:00,113.72,50.20399999999999,45.968999999999994,29.28 -2020-06-04 10:45:00,116.8,51.668,45.968999999999994,29.28 -2020-06-04 11:00:00,117.17,47.239,44.067,29.28 -2020-06-04 11:15:00,109.05,48.511,44.067,29.28 -2020-06-04 11:30:00,110.66,50.037,44.067,29.28 -2020-06-04 11:45:00,105.85,51.783,44.067,29.28 -2020-06-04 12:00:00,111.23,47.696999999999996,41.501000000000005,29.28 -2020-06-04 12:15:00,113.61,47.762,41.501000000000005,29.28 -2020-06-04 12:30:00,110.92,46.945,41.501000000000005,29.28 -2020-06-04 12:45:00,102.91,48.453,41.501000000000005,29.28 -2020-06-04 13:00:00,102.85,49.145,41.117,29.28 -2020-06-04 13:15:00,107.65,50.192,41.117,29.28 -2020-06-04 13:30:00,107.87,48.458,41.117,29.28 -2020-06-04 13:45:00,112.9,47.026,41.117,29.28 -2020-06-04 14:00:00,99.93,48.964,41.492,29.28 -2020-06-04 14:15:00,105.4,47.635,41.492,29.28 -2020-06-04 14:30:00,103.85,46.70399999999999,41.492,29.28 -2020-06-04 14:45:00,104.92,47.266000000000005,41.492,29.28 -2020-06-04 15:00:00,109.26,48.626000000000005,43.711999999999996,29.28 -2020-06-04 15:15:00,110.94,46.278,43.711999999999996,29.28 -2020-06-04 15:30:00,109.79,44.57899999999999,43.711999999999996,29.28 -2020-06-04 15:45:00,112.69,42.47,43.711999999999996,29.28 -2020-06-04 16:00:00,114.07,44.871,45.446000000000005,29.28 -2020-06-04 16:15:00,109.19,44.458,45.446000000000005,29.28 -2020-06-04 16:30:00,114.59,43.638999999999996,45.446000000000005,29.28 -2020-06-04 16:45:00,116.07,40.316,45.446000000000005,29.28 -2020-06-04 17:00:00,115.34,42.75899999999999,48.803000000000004,29.28 -2020-06-04 17:15:00,110.62,42.886,48.803000000000004,29.28 -2020-06-04 17:30:00,108.75,42.233999999999995,48.803000000000004,29.28 -2020-06-04 17:45:00,116.09,40.74,48.803000000000004,29.28 -2020-06-04 18:00:00,113.07,43.028,51.167,29.28 -2020-06-04 18:15:00,111.11,43.445,51.167,29.28 -2020-06-04 18:30:00,108.92,41.133,51.167,29.28 -2020-06-04 18:45:00,115.29,45.537,51.167,29.28 -2020-06-04 19:00:00,113.15,46.541000000000004,52.486000000000004,29.28 -2020-06-04 19:15:00,104.97,45.648999999999994,52.486000000000004,29.28 -2020-06-04 19:30:00,105.27,44.864,52.486000000000004,29.28 -2020-06-04 19:45:00,105.77,45.185,52.486000000000004,29.28 -2020-06-04 20:00:00,104.54,43.773999999999994,59.635,29.28 -2020-06-04 20:15:00,99.31,43.553999999999995,59.635,29.28 -2020-06-04 20:30:00,96.07,43.706,59.635,29.28 -2020-06-04 20:45:00,96.72,44.038000000000004,59.635,29.28 -2020-06-04 21:00:00,93.51,42.174,54.353,29.28 -2020-06-04 21:15:00,91.29,43.881,54.353,29.28 -2020-06-04 21:30:00,89.16,44.949,54.353,29.28 -2020-06-04 21:45:00,87.45,45.925,54.353,29.28 -2020-06-04 22:00:00,83.88,43.271,49.431999999999995,29.28 -2020-06-04 22:15:00,83.67,44.842,49.431999999999995,29.28 -2020-06-04 22:30:00,80.79,39.148,49.431999999999995,29.28 -2020-06-04 22:45:00,82.95,35.979,49.431999999999995,29.28 -2020-06-04 23:00:00,77.09,31.623,42.872,29.28 -2020-06-04 23:15:00,76.4,29.77,42.872,29.28 -2020-06-04 23:30:00,76.03,28.66,42.872,29.28 -2020-06-04 23:45:00,75.13,28.1,42.872,29.28 -2020-06-05 00:00:00,72.68,25.186999999999998,39.819,29.28 -2020-06-05 00:15:00,74.39,26.006,39.819,29.28 -2020-06-05 00:30:00,73.39,25.189,39.819,29.28 -2020-06-05 00:45:00,72.85,24.973000000000003,39.819,29.28 -2020-06-05 01:00:00,73.16,24.238000000000003,37.797,29.28 -2020-06-05 01:15:00,73.41,23.063000000000002,37.797,29.28 -2020-06-05 01:30:00,71.51,22.136999999999997,37.797,29.28 -2020-06-05 01:45:00,72.07,21.439,37.797,29.28 -2020-06-05 02:00:00,71.82,21.836,36.905,29.28 -2020-06-05 02:15:00,72.76,20.305,36.905,29.28 -2020-06-05 02:30:00,72.71,23.339000000000002,36.905,29.28 -2020-06-05 02:45:00,73.05,23.405,36.905,29.28 -2020-06-05 03:00:00,73.18,25.739,37.1,29.28 -2020-06-05 03:15:00,74.33,23.340999999999998,37.1,29.28 -2020-06-05 03:30:00,73.58,22.383000000000003,37.1,29.28 -2020-06-05 03:45:00,74.46,22.509,37.1,29.28 -2020-06-05 04:00:00,78.21,30.009,37.882,29.28 -2020-06-05 04:15:00,84.39,37.158,37.882,29.28 -2020-06-05 04:30:00,85.9,35.499,37.882,29.28 -2020-06-05 04:45:00,92.12,35.150999999999996,37.882,29.28 -2020-06-05 05:00:00,94.15,52.801,40.777,29.28 -2020-06-05 05:15:00,96.53,66.937,40.777,29.28 -2020-06-05 05:30:00,98.78,57.873000000000005,40.777,29.28 -2020-06-05 05:45:00,105.4,53.503,40.777,29.28 -2020-06-05 06:00:00,110.45,55.2,55.528,29.28 -2020-06-05 06:15:00,115.64,55.998999999999995,55.528,29.28 -2020-06-05 06:30:00,113.37,53.525,55.528,29.28 -2020-06-05 06:45:00,113.78,53.961000000000006,55.528,29.28 -2020-06-05 07:00:00,117.75,55.501999999999995,67.749,29.28 -2020-06-05 07:15:00,116.94,56.013000000000005,67.749,29.28 -2020-06-05 07:30:00,113.93,51.391999999999996,67.749,29.28 -2020-06-05 07:45:00,110.35,50.114,67.749,29.28 -2020-06-05 08:00:00,109.2,48.199,57.55,29.28 -2020-06-05 08:15:00,110.52,50.788000000000004,57.55,29.28 -2020-06-05 08:30:00,122.31,50.622,57.55,29.28 -2020-06-05 08:45:00,120.12,52.435,57.55,29.28 -2020-06-05 09:00:00,121.23,45.938,52.588,29.28 -2020-06-05 09:15:00,117.3,46.71,52.588,29.28 -2020-06-05 09:30:00,116.81,49.378,52.588,29.28 -2020-06-05 09:45:00,121.98,51.972,52.588,29.28 -2020-06-05 10:00:00,119.87,46.911,49.772,29.28 -2020-06-05 10:15:00,116.15,48.356,49.772,29.28 -2020-06-05 10:30:00,115.85,48.961999999999996,49.772,29.28 -2020-06-05 10:45:00,117.93,50.29600000000001,49.772,29.28 -2020-06-05 11:00:00,114.69,46.1,49.226000000000006,29.28 -2020-06-05 11:15:00,113.17,46.253,49.226000000000006,29.28 -2020-06-05 11:30:00,105.0,47.621,49.226000000000006,29.28 -2020-06-05 11:45:00,103.88,48.471000000000004,49.226000000000006,29.28 -2020-06-05 12:00:00,100.79,44.951,45.705,29.28 -2020-06-05 12:15:00,102.92,44.177,45.705,29.28 -2020-06-05 12:30:00,99.51,43.463,45.705,29.28 -2020-06-05 12:45:00,100.37,44.29,45.705,29.28 -2020-06-05 13:00:00,99.89,45.646,43.133,29.28 -2020-06-05 13:15:00,99.79,46.958,43.133,29.28 -2020-06-05 13:30:00,100.88,45.986999999999995,43.133,29.28 -2020-06-05 13:45:00,99.87,44.84,43.133,29.28 -2020-06-05 14:00:00,99.86,45.912,41.989,29.28 -2020-06-05 14:15:00,97.26,44.972,41.989,29.28 -2020-06-05 14:30:00,96.34,45.495,41.989,29.28 -2020-06-05 14:45:00,94.99,45.443999999999996,41.989,29.28 -2020-06-05 15:00:00,95.46,46.718,43.728,29.28 -2020-06-05 15:15:00,98.1,44.083999999999996,43.728,29.28 -2020-06-05 15:30:00,95.79,41.667,43.728,29.28 -2020-06-05 15:45:00,95.92,40.283,43.728,29.28 -2020-06-05 16:00:00,97.42,41.795,45.93899999999999,29.28 -2020-06-05 16:15:00,100.06,41.875,45.93899999999999,29.28 -2020-06-05 16:30:00,99.13,40.913000000000004,45.93899999999999,29.28 -2020-06-05 16:45:00,101.84,36.795,45.93899999999999,29.28 -2020-06-05 17:00:00,103.81,40.939,50.488,29.28 -2020-06-05 17:15:00,103.47,40.846,50.488,29.28 -2020-06-05 17:30:00,103.83,40.308,50.488,29.28 -2020-06-05 17:45:00,104.89,38.606,50.488,29.28 -2020-06-05 18:00:00,103.73,41.031000000000006,52.408,29.28 -2020-06-05 18:15:00,103.19,40.478,52.408,29.28 -2020-06-05 18:30:00,102.75,38.1,52.408,29.28 -2020-06-05 18:45:00,103.29,42.903999999999996,52.408,29.28 -2020-06-05 19:00:00,98.83,44.846000000000004,52.736000000000004,29.28 -2020-06-05 19:15:00,95.61,44.645,52.736000000000004,29.28 -2020-06-05 19:30:00,93.94,43.871,52.736000000000004,29.28 -2020-06-05 19:45:00,93.79,43.167,52.736000000000004,29.28 -2020-06-05 20:00:00,92.15,41.589,59.68,29.28 -2020-06-05 20:15:00,92.31,42.13,59.68,29.28 -2020-06-05 20:30:00,91.82,41.824,59.68,29.28 -2020-06-05 20:45:00,92.41,41.468999999999994,59.68,29.28 -2020-06-05 21:00:00,89.29,40.918,54.343999999999994,29.28 -2020-06-05 21:15:00,87.29,44.281000000000006,54.343999999999994,29.28 -2020-06-05 21:30:00,84.11,45.185,54.343999999999994,29.28 -2020-06-05 21:45:00,83.05,46.424,54.343999999999994,29.28 -2020-06-05 22:00:00,79.44,43.754,49.672,29.28 -2020-06-05 22:15:00,78.6,45.085,49.672,29.28 -2020-06-05 22:30:00,77.06,44.566,49.672,29.28 -2020-06-05 22:45:00,75.8,42.548,49.672,29.28 -2020-06-05 23:00:00,72.07,39.743,42.065,29.28 -2020-06-05 23:15:00,72.18,36.24,42.065,29.28 -2020-06-05 23:30:00,71.31,33.249,42.065,29.28 -2020-06-05 23:45:00,70.36,32.497,42.065,29.28 -2020-06-06 00:00:00,67.51,25.941999999999997,38.829,29.17 -2020-06-06 00:15:00,67.86,25.7,38.829,29.17 -2020-06-06 00:30:00,67.41,24.565,38.829,29.17 -2020-06-06 00:45:00,67.72,23.725,38.829,29.17 -2020-06-06 01:00:00,66.82,23.335,34.63,29.17 -2020-06-06 01:15:00,67.25,22.605999999999998,34.63,29.17 -2020-06-06 01:30:00,65.85,20.85,34.63,29.17 -2020-06-06 01:45:00,66.09,21.315,34.63,29.17 -2020-06-06 02:00:00,64.68,20.839000000000002,32.465,29.17 -2020-06-06 02:15:00,64.57,18.511,32.465,29.17 -2020-06-06 02:30:00,64.68,20.68,32.465,29.17 -2020-06-06 02:45:00,64.25,21.514,32.465,29.17 -2020-06-06 03:00:00,64.14,22.599,31.925,29.17 -2020-06-06 03:15:00,65.0,19.414,31.925,29.17 -2020-06-06 03:30:00,65.09,18.62,31.925,29.17 -2020-06-06 03:45:00,67.6,20.195,31.925,29.17 -2020-06-06 04:00:00,70.16,25.434,31.309,29.17 -2020-06-06 04:15:00,70.62,31.435,31.309,29.17 -2020-06-06 04:30:00,63.11,27.958000000000002,31.309,29.17 -2020-06-06 04:45:00,65.22,27.819000000000003,31.309,29.17 -2020-06-06 05:00:00,67.91,36.066,30.323,29.17 -2020-06-06 05:15:00,67.72,37.842,30.323,29.17 -2020-06-06 05:30:00,69.31,30.338,30.323,29.17 -2020-06-06 05:45:00,71.04,30.802,30.323,29.17 -2020-06-06 06:00:00,75.65,44.887,31.438000000000002,29.17 -2020-06-06 06:15:00,80.98,54.621,31.438000000000002,29.17 -2020-06-06 06:30:00,84.8,48.958999999999996,31.438000000000002,29.17 -2020-06-06 06:45:00,83.47,45.533,31.438000000000002,29.17 -2020-06-06 07:00:00,78.85,45.275,34.891999999999996,29.17 -2020-06-06 07:15:00,78.38,44.446999999999996,34.891999999999996,29.17 -2020-06-06 07:30:00,79.02,41.507,34.891999999999996,29.17 -2020-06-06 07:45:00,80.34,41.519,34.891999999999996,29.17 -2020-06-06 08:00:00,81.61,40.611999999999995,39.608000000000004,29.17 -2020-06-06 08:15:00,82.54,43.39,39.608000000000004,29.17 -2020-06-06 08:30:00,81.42,43.358000000000004,39.608000000000004,29.17 -2020-06-06 08:45:00,81.09,46.385,39.608000000000004,29.17 -2020-06-06 09:00:00,79.87,42.973,40.894,29.17 -2020-06-06 09:15:00,87.56,44.285,40.894,29.17 -2020-06-06 09:30:00,87.68,47.515,40.894,29.17 -2020-06-06 09:45:00,83.66,49.722,40.894,29.17 -2020-06-06 10:00:00,80.88,45.253,39.525,29.17 -2020-06-06 10:15:00,87.29,47.07,39.525,29.17 -2020-06-06 10:30:00,83.73,47.373999999999995,39.525,29.17 -2020-06-06 10:45:00,83.72,48.521,39.525,29.17 -2020-06-06 11:00:00,84.12,44.211000000000006,36.718,29.17 -2020-06-06 11:15:00,90.83,45.122,36.718,29.17 -2020-06-06 11:30:00,94.67,46.63,36.718,29.17 -2020-06-06 11:45:00,94.56,48.016999999999996,36.718,29.17 -2020-06-06 12:00:00,91.99,44.79600000000001,35.688,29.17 -2020-06-06 12:15:00,90.46,44.895,35.688,29.17 -2020-06-06 12:30:00,88.8,44.06399999999999,35.688,29.17 -2020-06-06 12:45:00,87.54,45.498999999999995,35.688,29.17 -2020-06-06 13:00:00,85.74,46.00899999999999,32.858000000000004,29.17 -2020-06-06 13:15:00,87.07,46.6,32.858000000000004,29.17 -2020-06-06 13:30:00,87.33,45.77,32.858000000000004,29.17 -2020-06-06 13:45:00,86.83,43.407,32.858000000000004,29.17 -2020-06-06 14:00:00,86.15,44.693999999999996,31.738000000000003,29.17 -2020-06-06 14:15:00,84.56,42.516000000000005,31.738000000000003,29.17 -2020-06-06 14:30:00,81.7,42.419,31.738000000000003,29.17 -2020-06-06 14:45:00,80.31,42.846000000000004,31.738000000000003,29.17 -2020-06-06 15:00:00,80.41,44.662,34.35,29.17 -2020-06-06 15:15:00,79.49,42.766000000000005,34.35,29.17 -2020-06-06 15:30:00,79.47,40.687,34.35,29.17 -2020-06-06 15:45:00,79.76,38.425,34.35,29.17 -2020-06-06 16:00:00,80.07,41.907,37.522,29.17 -2020-06-06 16:15:00,79.03,41.225,37.522,29.17 -2020-06-06 16:30:00,80.08,40.488,37.522,29.17 -2020-06-06 16:45:00,81.23,36.4,37.522,29.17 -2020-06-06 17:00:00,82.36,39.404,42.498000000000005,29.17 -2020-06-06 17:15:00,81.67,37.425,42.498000000000005,29.17 -2020-06-06 17:30:00,81.82,36.743,42.498000000000005,29.17 -2020-06-06 17:45:00,84.04,35.443000000000005,42.498000000000005,29.17 -2020-06-06 18:00:00,84.14,39.175,44.701,29.17 -2020-06-06 18:15:00,83.83,40.357,44.701,29.17 -2020-06-06 18:30:00,84.61,39.39,44.701,29.17 -2020-06-06 18:45:00,84.06,40.567,44.701,29.17 -2020-06-06 19:00:00,81.29,41.091,45.727,29.17 -2020-06-06 19:15:00,78.62,39.875,45.727,29.17 -2020-06-06 19:30:00,76.78,39.896,45.727,29.17 -2020-06-06 19:45:00,75.72,40.85,45.727,29.17 -2020-06-06 20:00:00,74.89,40.242,43.391000000000005,29.17 -2020-06-06 20:15:00,74.17,40.381,43.391000000000005,29.17 -2020-06-06 20:30:00,74.7,39.22,43.391000000000005,29.17 -2020-06-06 20:45:00,74.33,40.624,43.391000000000005,29.17 -2020-06-06 21:00:00,71.59,38.898,41.231,29.17 -2020-06-06 21:15:00,69.93,41.982,41.231,29.17 -2020-06-06 21:30:00,68.91,43.161,41.231,29.17 -2020-06-06 21:45:00,68.03,43.858999999999995,41.231,29.17 -2020-06-06 22:00:00,64.54,41.316,40.798,29.17 -2020-06-06 22:15:00,64.33,43.133,40.798,29.17 -2020-06-06 22:30:00,62.24,42.849,40.798,29.17 -2020-06-06 22:45:00,62.62,41.352,40.798,29.17 -2020-06-06 23:00:00,58.66,38.109,34.402,29.17 -2020-06-06 23:15:00,58.11,34.889,34.402,29.17 -2020-06-06 23:30:00,56.97,34.075,34.402,29.17 -2020-06-06 23:45:00,56.18,33.421,34.402,29.17 -2020-06-07 00:00:00,54.78,27.147,30.171,29.17 -2020-06-07 00:15:00,55.23,25.78,30.171,29.17 -2020-06-07 00:30:00,54.07,24.471999999999998,30.171,29.17 -2020-06-07 00:45:00,54.14,23.596999999999998,30.171,29.17 -2020-06-07 01:00:00,52.67,23.464000000000002,27.15,29.17 -2020-06-07 01:15:00,52.85,22.699,27.15,29.17 -2020-06-07 01:30:00,52.59,20.854,27.15,29.17 -2020-06-07 01:45:00,52.21,20.92,27.15,29.17 -2020-06-07 02:00:00,50.98,20.448,25.403000000000002,29.17 -2020-06-07 02:15:00,51.38,18.707,25.403000000000002,29.17 -2020-06-07 02:30:00,50.88,21.136,25.403000000000002,29.17 -2020-06-07 02:45:00,51.13,21.745,25.403000000000002,29.17 -2020-06-07 03:00:00,50.84,23.48,23.386999999999997,29.17 -2020-06-07 03:15:00,51.71,20.49,23.386999999999997,29.17 -2020-06-07 03:30:00,52.75,19.136,23.386999999999997,29.17 -2020-06-07 03:45:00,50.92,19.973,23.386999999999997,29.17 -2020-06-07 04:00:00,50.55,25.101,23.941999999999997,29.17 -2020-06-07 04:15:00,51.55,30.546,23.941999999999997,29.17 -2020-06-07 04:30:00,51.6,28.333000000000002,23.941999999999997,29.17 -2020-06-07 04:45:00,52.09,27.774,23.941999999999997,29.17 -2020-06-07 05:00:00,53.72,35.75,23.026,29.17 -2020-06-07 05:15:00,54.12,36.433,23.026,29.17 -2020-06-07 05:30:00,55.59,28.6,23.026,29.17 -2020-06-07 05:45:00,57.85,28.881,23.026,29.17 -2020-06-07 06:00:00,59.26,40.721,23.223000000000003,29.17 -2020-06-07 06:15:00,60.63,51.071999999999996,23.223000000000003,29.17 -2020-06-07 06:30:00,62.83,44.669,23.223000000000003,29.17 -2020-06-07 06:45:00,64.38,40.23,23.223000000000003,29.17 -2020-06-07 07:00:00,65.6,40.458,24.968000000000004,29.17 -2020-06-07 07:15:00,66.44,38.01,24.968000000000004,29.17 -2020-06-07 07:30:00,68.26,36.156,24.968000000000004,29.17 -2020-06-07 07:45:00,68.77,36.098,24.968000000000004,29.17 -2020-06-07 08:00:00,69.95,36.086,29.131,29.17 -2020-06-07 08:15:00,69.53,40.012,29.131,29.17 -2020-06-07 08:30:00,69.54,40.953,29.131,29.17 -2020-06-07 08:45:00,69.78,44.098,29.131,29.17 -2020-06-07 09:00:00,70.2,40.535,29.904,29.17 -2020-06-07 09:15:00,72.11,41.438,29.904,29.17 -2020-06-07 09:30:00,74.06,45.06399999999999,29.904,29.17 -2020-06-07 09:45:00,74.87,48.231,29.904,29.17 -2020-06-07 10:00:00,77.19,44.516000000000005,28.943,29.17 -2020-06-07 10:15:00,78.39,46.53,28.943,29.17 -2020-06-07 10:30:00,77.62,47.106,28.943,29.17 -2020-06-07 10:45:00,75.28,49.04,28.943,29.17 -2020-06-07 11:00:00,71.31,44.483999999999995,31.682,29.17 -2020-06-07 11:15:00,70.31,44.994,31.682,29.17 -2020-06-07 11:30:00,70.0,46.903,31.682,29.17 -2020-06-07 11:45:00,67.85,48.593,31.682,29.17 -2020-06-07 12:00:00,64.35,46.353,27.315,29.17 -2020-06-07 12:15:00,61.2,46.038000000000004,27.315,29.17 -2020-06-07 12:30:00,57.59,45.248999999999995,27.315,29.17 -2020-06-07 12:45:00,59.18,45.997,27.315,29.17 -2020-06-07 13:00:00,59.01,46.153,23.894000000000002,29.17 -2020-06-07 13:15:00,55.2,46.481,23.894000000000002,29.17 -2020-06-07 13:30:00,57.81,44.589,23.894000000000002,29.17 -2020-06-07 13:45:00,60.26,43.223,23.894000000000002,29.17 -2020-06-07 14:00:00,57.88,45.69,21.148000000000003,29.17 -2020-06-07 14:15:00,64.49,44.026,21.148000000000003,29.17 -2020-06-07 14:30:00,66.8,42.846000000000004,21.148000000000003,29.17 -2020-06-07 14:45:00,67.65,42.228,21.148000000000003,29.17 -2020-06-07 15:00:00,67.87,44.083,21.229,29.17 -2020-06-07 15:15:00,65.72,41.477,21.229,29.17 -2020-06-07 15:30:00,59.62,39.268,21.229,29.17 -2020-06-07 15:45:00,60.76,37.325,21.229,29.17 -2020-06-07 16:00:00,62.45,39.453,25.037,29.17 -2020-06-07 16:15:00,63.37,38.900999999999996,25.037,29.17 -2020-06-07 16:30:00,66.59,39.158,25.037,29.17 -2020-06-07 16:45:00,70.11,35.108000000000004,25.037,29.17 -2020-06-07 17:00:00,72.96,38.486,37.11,29.17 -2020-06-07 17:15:00,76.34,37.903,37.11,29.17 -2020-06-07 17:30:00,79.24,38.003,37.11,29.17 -2020-06-07 17:45:00,81.88,37.28,37.11,29.17 -2020-06-07 18:00:00,83.72,41.528999999999996,42.215,29.17 -2020-06-07 18:15:00,79.56,42.415,42.215,29.17 -2020-06-07 18:30:00,76.0,41.022,42.215,29.17 -2020-06-07 18:45:00,78.65,42.47,42.215,29.17 -2020-06-07 19:00:00,79.38,45.026,44.383,29.17 -2020-06-07 19:15:00,79.12,42.795,44.383,29.17 -2020-06-07 19:30:00,79.25,42.556999999999995,44.383,29.17 -2020-06-07 19:45:00,78.61,43.195,44.383,29.17 -2020-06-07 20:00:00,79.46,42.74,43.426,29.17 -2020-06-07 20:15:00,79.03,42.831,43.426,29.17 -2020-06-07 20:30:00,79.41,42.56100000000001,43.426,29.17 -2020-06-07 20:45:00,81.82,42.298,43.426,29.17 -2020-06-07 21:00:00,81.38,40.224000000000004,42.265,29.17 -2020-06-07 21:15:00,80.43,42.977,42.265,29.17 -2020-06-07 21:30:00,78.77,43.54,42.265,29.17 -2020-06-07 21:45:00,77.29,44.605,42.265,29.17 -2020-06-07 22:00:00,73.8,43.917,42.26,29.17 -2020-06-07 22:15:00,72.46,44.08,42.26,29.17 -2020-06-07 22:30:00,70.72,43.038999999999994,42.26,29.17 -2020-06-07 22:45:00,70.73,40.247,42.26,29.17 -2020-06-07 23:00:00,68.68,36.454,36.609,29.17 -2020-06-07 23:15:00,69.16,34.64,36.609,29.17 -2020-06-07 23:30:00,67.02,33.389,36.609,29.17 -2020-06-07 23:45:00,67.56,32.952,36.609,29.17 -2020-06-08 00:00:00,65.18,28.81,34.611,29.28 -2020-06-08 00:15:00,65.79,28.454,34.611,29.28 -2020-06-08 00:30:00,66.1,26.794,34.611,29.28 -2020-06-08 00:45:00,66.13,25.502,34.611,29.28 -2020-06-08 01:00:00,64.79,25.755,33.552,29.28 -2020-06-08 01:15:00,65.06,24.938000000000002,33.552,29.28 -2020-06-08 01:30:00,65.24,23.433000000000003,33.552,29.28 -2020-06-08 01:45:00,65.35,23.410999999999998,33.552,29.28 -2020-06-08 02:00:00,65.58,23.37,32.351,29.28 -2020-06-08 02:15:00,67.8,20.785999999999998,32.351,29.28 -2020-06-08 02:30:00,73.54,23.4,32.351,29.28 -2020-06-08 02:45:00,74.8,23.823,32.351,29.28 -2020-06-08 03:00:00,71.75,26.151999999999997,30.793000000000003,29.28 -2020-06-08 03:15:00,68.21,23.986,30.793000000000003,29.28 -2020-06-08 03:30:00,68.19,23.261999999999997,30.793000000000003,29.28 -2020-06-08 03:45:00,75.1,23.634,30.793000000000003,29.28 -2020-06-08 04:00:00,76.21,32.04,31.274,29.28 -2020-06-08 04:15:00,81.8,40.582,31.274,29.28 -2020-06-08 04:30:00,81.13,38.092,31.274,29.28 -2020-06-08 04:45:00,84.34,37.895,31.274,29.28 -2020-06-08 05:00:00,91.59,53.857,37.75,29.28 -2020-06-08 05:15:00,94.96,65.925,37.75,29.28 -2020-06-08 05:30:00,96.35,56.716,37.75,29.28 -2020-06-08 05:45:00,103.22,53.645,37.75,29.28 -2020-06-08 06:00:00,115.44,54.214,55.36,29.28 -2020-06-08 06:15:00,117.66,54.621,55.36,29.28 -2020-06-08 06:30:00,119.09,52.708999999999996,55.36,29.28 -2020-06-08 06:45:00,118.2,54.078,55.36,29.28 -2020-06-08 07:00:00,122.59,54.916000000000004,65.87,29.28 -2020-06-08 07:15:00,126.53,54.75899999999999,65.87,29.28 -2020-06-08 07:30:00,126.58,52.008,65.87,29.28 -2020-06-08 07:45:00,117.45,52.041000000000004,65.87,29.28 -2020-06-08 08:00:00,116.82,49.583,55.695,29.28 -2020-06-08 08:15:00,124.15,52.214,55.695,29.28 -2020-06-08 08:30:00,127.09,51.86600000000001,55.695,29.28 -2020-06-08 08:45:00,129.96,54.918,55.695,29.28 -2020-06-08 09:00:00,127.9,50.291000000000004,50.881,29.28 -2020-06-08 09:15:00,127.73,49.188,50.881,29.28 -2020-06-08 09:30:00,129.88,51.846000000000004,50.881,29.28 -2020-06-08 09:45:00,128.32,52.891000000000005,50.881,29.28 -2020-06-08 10:00:00,125.16,49.513999999999996,49.138000000000005,29.28 -2020-06-08 10:15:00,119.84,51.376000000000005,49.138000000000005,29.28 -2020-06-08 10:30:00,116.84,51.43899999999999,49.138000000000005,29.28 -2020-06-08 10:45:00,124.11,51.953,49.138000000000005,29.28 -2020-06-08 11:00:00,126.44,47.358000000000004,49.178000000000004,29.28 -2020-06-08 11:15:00,121.8,48.31100000000001,49.178000000000004,29.28 -2020-06-08 11:30:00,117.6,50.997,49.178000000000004,29.28 -2020-06-08 11:45:00,113.89,53.128,49.178000000000004,29.28 -2020-06-08 12:00:00,106.1,49.479,47.698,29.28 -2020-06-08 12:15:00,110.89,49.273999999999994,47.698,29.28 -2020-06-08 12:30:00,105.06,47.468999999999994,47.698,29.28 -2020-06-08 12:45:00,103.24,48.36,47.698,29.28 -2020-06-08 13:00:00,104.28,49.425,48.104,29.28 -2020-06-08 13:15:00,105.35,48.79,48.104,29.28 -2020-06-08 13:30:00,102.99,47.021,48.104,29.28 -2020-06-08 13:45:00,106.17,46.507,48.104,29.28 -2020-06-08 14:00:00,104.77,48.076,48.53,29.28 -2020-06-08 14:15:00,99.17,46.91,48.53,29.28 -2020-06-08 14:30:00,100.05,45.497,48.53,29.28 -2020-06-08 14:45:00,101.1,46.841,48.53,29.28 -2020-06-08 15:00:00,100.5,48.551,49.351000000000006,29.28 -2020-06-08 15:15:00,97.86,45.243,49.351000000000006,29.28 -2020-06-08 15:30:00,98.11,43.674,49.351000000000006,29.28 -2020-06-08 15:45:00,102.17,41.217,49.351000000000006,29.28 -2020-06-08 16:00:00,104.3,44.405,51.44,29.28 -2020-06-08 16:15:00,106.98,43.84,51.44,29.28 -2020-06-08 16:30:00,105.78,43.396,51.44,29.28 -2020-06-08 16:45:00,107.49,39.226,51.44,29.28 -2020-06-08 17:00:00,108.2,41.544,56.868,29.28 -2020-06-08 17:15:00,108.56,41.22,56.868,29.28 -2020-06-08 17:30:00,108.83,40.895,56.868,29.28 -2020-06-08 17:45:00,109.18,39.635,56.868,29.28 -2020-06-08 18:00:00,108.41,42.896,57.229,29.28 -2020-06-08 18:15:00,107.9,41.818999999999996,57.229,29.28 -2020-06-08 18:30:00,107.3,39.758,57.229,29.28 -2020-06-08 18:45:00,105.82,44.278,57.229,29.28 -2020-06-08 19:00:00,103.74,46.408,57.744,29.28 -2020-06-08 19:15:00,100.22,45.333999999999996,57.744,29.28 -2020-06-08 19:30:00,98.33,44.78,57.744,29.28 -2020-06-08 19:45:00,97.62,44.755,57.744,29.28 -2020-06-08 20:00:00,95.68,42.876000000000005,66.05199999999999,29.28 -2020-06-08 20:15:00,95.32,44.086999999999996,66.05199999999999,29.28 -2020-06-08 20:30:00,96.73,44.168,66.05199999999999,29.28 -2020-06-08 20:45:00,96.53,44.285,66.05199999999999,29.28 -2020-06-08 21:00:00,91.82,41.66,59.396,29.28 -2020-06-08 21:15:00,91.66,44.745,59.396,29.28 -2020-06-08 21:30:00,89.92,45.61,59.396,29.28 -2020-06-08 21:45:00,88.61,46.43600000000001,59.396,29.28 -2020-06-08 22:00:00,83.94,43.571000000000005,53.06,29.28 -2020-06-08 22:15:00,83.74,45.533,53.06,29.28 -2020-06-08 22:30:00,81.28,39.543,53.06,29.28 -2020-06-08 22:45:00,80.11,36.312,53.06,29.28 -2020-06-08 23:00:00,74.81,32.603,46.148,29.28 -2020-06-08 23:15:00,76.9,29.473000000000003,46.148,29.28 -2020-06-08 23:30:00,73.41,28.424,46.148,29.28 -2020-06-08 23:45:00,75.06,27.683000000000003,46.148,29.28 -2020-06-09 00:00:00,71.41,26.438000000000002,44.625,29.28 -2020-06-09 00:15:00,73.02,27.034000000000002,44.625,29.28 -2020-06-09 00:30:00,70.5,25.976999999999997,44.625,29.28 -2020-06-09 00:45:00,73.38,25.364,44.625,29.28 -2020-06-09 01:00:00,72.8,25.099,41.733000000000004,29.28 -2020-06-09 01:15:00,73.38,24.331999999999997,41.733000000000004,29.28 -2020-06-09 01:30:00,72.4,22.71,41.733000000000004,29.28 -2020-06-09 01:45:00,72.79,22.215,41.733000000000004,29.28 -2020-06-09 02:00:00,70.58,21.713,39.872,29.28 -2020-06-09 02:15:00,72.27,20.226,39.872,29.28 -2020-06-09 02:30:00,80.49,22.413,39.872,29.28 -2020-06-09 02:45:00,80.75,23.159000000000002,39.872,29.28 -2020-06-09 03:00:00,78.25,24.851,38.711,29.28 -2020-06-09 03:15:00,73.94,23.508000000000003,38.711,29.28 -2020-06-09 03:30:00,77.01,22.8,38.711,29.28 -2020-06-09 03:45:00,74.49,22.145,38.711,29.28 -2020-06-09 04:00:00,77.74,29.266,39.823,29.28 -2020-06-09 04:15:00,79.76,37.743,39.823,29.28 -2020-06-09 04:30:00,82.3,35.097,39.823,29.28 -2020-06-09 04:45:00,87.15,35.468,39.823,29.28 -2020-06-09 05:00:00,96.24,53.251000000000005,43.228,29.28 -2020-06-09 05:15:00,100.0,65.757,43.228,29.28 -2020-06-09 05:30:00,103.94,56.667,43.228,29.28 -2020-06-09 05:45:00,105.25,52.95,43.228,29.28 -2020-06-09 06:00:00,114.5,54.452,54.316,29.28 -2020-06-09 06:15:00,116.55,55.166000000000004,54.316,29.28 -2020-06-09 06:30:00,110.99,52.913000000000004,54.316,29.28 -2020-06-09 06:45:00,112.3,53.423,54.316,29.28 -2020-06-09 07:00:00,121.18,54.379,65.758,29.28 -2020-06-09 07:15:00,120.41,53.98,65.758,29.28 -2020-06-09 07:30:00,115.9,51.185,65.758,29.28 -2020-06-09 07:45:00,116.85,50.333999999999996,65.758,29.28 -2020-06-09 08:00:00,118.19,47.826,57.983000000000004,29.28 -2020-06-09 08:15:00,119.82,49.976000000000006,57.983000000000004,29.28 -2020-06-09 08:30:00,115.93,49.765,57.983000000000004,29.28 -2020-06-09 08:45:00,114.08,51.917,57.983000000000004,29.28 -2020-06-09 09:00:00,112.79,47.534,52.653,29.28 -2020-06-09 09:15:00,113.07,46.446999999999996,52.653,29.28 -2020-06-09 09:30:00,113.8,49.825,52.653,29.28 -2020-06-09 09:45:00,113.95,52.155,52.653,29.28 -2020-06-09 10:00:00,112.65,47.425,51.408,29.28 -2020-06-09 10:15:00,115.45,49.073,51.408,29.28 -2020-06-09 10:30:00,121.75,49.181999999999995,51.408,29.28 -2020-06-09 10:45:00,124.45,50.68899999999999,51.408,29.28 -2020-06-09 11:00:00,118.76,46.229,51.913000000000004,29.28 -2020-06-09 11:15:00,118.38,47.549,51.913000000000004,29.28 -2020-06-09 11:30:00,118.73,49.02,51.913000000000004,29.28 -2020-06-09 11:45:00,120.94,50.788999999999994,51.913000000000004,29.28 -2020-06-09 12:00:00,115.6,46.87,49.508,29.28 -2020-06-09 12:15:00,114.07,46.952,49.508,29.28 -2020-06-09 12:30:00,115.92,46.006,49.508,29.28 -2020-06-09 12:45:00,116.36,47.536,49.508,29.28 -2020-06-09 13:00:00,114.4,48.217,50.007,29.28 -2020-06-09 13:15:00,114.3,49.275,50.007,29.28 -2020-06-09 13:30:00,114.18,47.592,50.007,29.28 -2020-06-09 13:45:00,114.22,46.174,50.007,29.28 -2020-06-09 14:00:00,109.09,48.222,49.778999999999996,29.28 -2020-06-09 14:15:00,106.54,46.875,49.778999999999996,29.28 -2020-06-09 14:30:00,113.2,45.803999999999995,49.778999999999996,29.28 -2020-06-09 14:45:00,112.19,46.391000000000005,49.778999999999996,29.28 -2020-06-09 15:00:00,108.09,47.952,51.559,29.28 -2020-06-09 15:15:00,101.97,45.549,51.559,29.28 -2020-06-09 15:30:00,104.39,43.786,51.559,29.28 -2020-06-09 15:45:00,107.82,41.613,51.559,29.28 -2020-06-09 16:00:00,106.35,44.193000000000005,53.531000000000006,29.28 -2020-06-09 16:15:00,101.43,43.746,53.531000000000006,29.28 -2020-06-09 16:30:00,105.03,43.018,53.531000000000006,29.28 -2020-06-09 16:45:00,106.03,39.527,53.531000000000006,29.28 -2020-06-09 17:00:00,112.45,42.126999999999995,59.497,29.28 -2020-06-09 17:15:00,115.83,42.196000000000005,59.497,29.28 -2020-06-09 17:30:00,116.3,41.498000000000005,59.497,29.28 -2020-06-09 17:45:00,110.26,39.883,59.497,29.28 -2020-06-09 18:00:00,106.4,42.235,59.861999999999995,29.28 -2020-06-09 18:15:00,108.44,42.52,59.861999999999995,29.28 -2020-06-09 18:30:00,112.57,40.175,59.861999999999995,29.28 -2020-06-09 18:45:00,113.13,44.573,59.861999999999995,29.28 -2020-06-09 19:00:00,106.86,45.585,60.989,29.28 -2020-06-09 19:15:00,103.42,44.653,60.989,29.28 -2020-06-09 19:30:00,98.81,43.82899999999999,60.989,29.28 -2020-06-09 19:45:00,99.4,44.125,60.989,29.28 -2020-06-09 20:00:00,95.84,42.619,68.35600000000001,29.28 -2020-06-09 20:15:00,94.92,42.388000000000005,68.35600000000001,29.28 -2020-06-09 20:30:00,94.19,42.602,68.35600000000001,29.28 -2020-06-09 20:45:00,94.35,43.11,68.35600000000001,29.28 -2020-06-09 21:00:00,90.98,41.266999999999996,59.251000000000005,29.28 -2020-06-09 21:15:00,89.1,43.023999999999994,59.251000000000005,29.28 -2020-06-09 21:30:00,86.25,43.975,59.251000000000005,29.28 -2020-06-09 21:45:00,86.55,45.022,59.251000000000005,29.28 -2020-06-09 22:00:00,82.28,42.479,54.736999999999995,29.28 -2020-06-09 22:15:00,80.43,44.108999999999995,54.736999999999995,29.28 -2020-06-09 22:30:00,78.68,38.405,54.736999999999995,29.28 -2020-06-09 22:45:00,78.71,35.179,54.736999999999995,29.28 -2020-06-09 23:00:00,74.03,30.709,46.806999999999995,29.28 -2020-06-09 23:15:00,74.01,29.048000000000002,46.806999999999995,29.28 -2020-06-09 23:30:00,74.25,27.978,46.806999999999995,29.28 -2020-06-09 23:45:00,73.65,27.365,46.806999999999995,29.28 -2020-06-10 00:00:00,69.28,26.302,43.824,29.28 -2020-06-10 00:15:00,71.2,26.901,43.824,29.28 -2020-06-10 00:30:00,66.99,25.846,43.824,29.28 -2020-06-10 00:45:00,71.61,25.239,43.824,29.28 -2020-06-10 01:00:00,69.8,24.991,39.86,29.28 -2020-06-10 01:15:00,71.05,24.204,39.86,29.28 -2020-06-10 01:30:00,70.29,22.575,39.86,29.28 -2020-06-10 01:45:00,71.08,22.073,39.86,29.28 -2020-06-10 02:00:00,69.28,21.573,37.931999999999995,29.28 -2020-06-10 02:15:00,70.6,20.082,37.931999999999995,29.28 -2020-06-10 02:30:00,77.94,22.269000000000002,37.931999999999995,29.28 -2020-06-10 02:45:00,78.48,23.021,37.931999999999995,29.28 -2020-06-10 03:00:00,76.46,24.714000000000002,37.579,29.28 -2020-06-10 03:15:00,74.28,23.369,37.579,29.28 -2020-06-10 03:30:00,73.07,22.666,37.579,29.28 -2020-06-10 03:45:00,74.4,22.034000000000002,37.579,29.28 -2020-06-10 04:00:00,78.61,29.1,37.931999999999995,29.28 -2020-06-10 04:15:00,78.12,37.53,37.931999999999995,29.28 -2020-06-10 04:30:00,82.15,34.872,37.931999999999995,29.28 -2020-06-10 04:45:00,86.72,35.239000000000004,37.931999999999995,29.28 -2020-06-10 05:00:00,95.11,52.913000000000004,40.942,29.28 -2020-06-10 05:15:00,101.05,65.282,40.942,29.28 -2020-06-10 05:30:00,107.89,56.26,40.942,29.28 -2020-06-10 05:45:00,112.89,52.593,40.942,29.28 -2020-06-10 06:00:00,115.77,54.111999999999995,56.516999999999996,29.28 -2020-06-10 06:15:00,116.7,54.808,56.516999999999996,29.28 -2020-06-10 06:30:00,120.98,52.577,56.516999999999996,29.28 -2020-06-10 06:45:00,121.83,53.114,56.516999999999996,29.28 -2020-06-10 07:00:00,121.07,54.059,71.707,29.28 -2020-06-10 07:15:00,116.29,53.674,71.707,29.28 -2020-06-10 07:30:00,121.13,50.87,71.707,29.28 -2020-06-10 07:45:00,119.6,50.05,71.707,29.28 -2020-06-10 08:00:00,120.93,47.552,61.17,29.28 -2020-06-10 08:15:00,117.32,49.738,61.17,29.28 -2020-06-10 08:30:00,118.76,49.519,61.17,29.28 -2020-06-10 08:45:00,116.95,51.678000000000004,61.17,29.28 -2020-06-10 09:00:00,124.86,47.288999999999994,57.282,29.28 -2020-06-10 09:15:00,121.0,46.206,57.282,29.28 -2020-06-10 09:30:00,126.65,49.588,57.282,29.28 -2020-06-10 09:45:00,128.01,51.937,57.282,29.28 -2020-06-10 10:00:00,127.44,47.218,54.026,29.28 -2020-06-10 10:15:00,119.72,48.883,54.026,29.28 -2020-06-10 10:30:00,121.47,48.994,54.026,29.28 -2020-06-10 10:45:00,128.3,50.51,54.026,29.28 -2020-06-10 11:00:00,132.15,46.044,54.277,29.28 -2020-06-10 11:15:00,127.14,47.372,54.277,29.28 -2020-06-10 11:30:00,117.1,48.833999999999996,54.277,29.28 -2020-06-10 11:45:00,114.36,50.605,54.277,29.28 -2020-06-10 12:00:00,120.41,46.718999999999994,52.552,29.28 -2020-06-10 12:15:00,123.17,46.803999999999995,52.552,29.28 -2020-06-10 12:30:00,122.08,45.833999999999996,52.552,29.28 -2020-06-10 12:45:00,120.6,47.368,52.552,29.28 -2020-06-10 13:00:00,118.16,48.044,52.111999999999995,29.28 -2020-06-10 13:15:00,119.23,49.104,52.111999999999995,29.28 -2020-06-10 13:30:00,117.97,47.431000000000004,52.111999999999995,29.28 -2020-06-10 13:45:00,113.38,46.016999999999996,52.111999999999995,29.28 -2020-06-10 14:00:00,112.33,48.085,52.066,29.28 -2020-06-10 14:15:00,108.78,46.735,52.066,29.28 -2020-06-10 14:30:00,112.6,45.636,52.066,29.28 -2020-06-10 14:45:00,111.92,46.229,52.066,29.28 -2020-06-10 15:00:00,110.0,47.827,52.523999999999994,29.28 -2020-06-10 15:15:00,106.57,45.413999999999994,52.523999999999994,29.28 -2020-06-10 15:30:00,108.24,43.638999999999996,52.523999999999994,29.28 -2020-06-10 15:45:00,110.6,41.455,52.523999999999994,29.28 -2020-06-10 16:00:00,112.33,44.068000000000005,54.101000000000006,29.28 -2020-06-10 16:15:00,110.81,43.615,54.101000000000006,29.28 -2020-06-10 16:30:00,111.57,42.906000000000006,54.101000000000006,29.28 -2020-06-10 16:45:00,118.23,39.384,54.101000000000006,29.28 -2020-06-10 17:00:00,119.5,42.012,58.155,29.28 -2020-06-10 17:15:00,116.37,42.07,58.155,29.28 -2020-06-10 17:30:00,117.51,41.364,58.155,29.28 -2020-06-10 17:45:00,114.99,39.728,58.155,29.28 -2020-06-10 18:00:00,121.59,42.092,60.205,29.28 -2020-06-10 18:15:00,119.08,42.352,60.205,29.28 -2020-06-10 18:30:00,115.11,40.0,60.205,29.28 -2020-06-10 18:45:00,110.65,44.397,60.205,29.28 -2020-06-10 19:00:00,108.16,45.411,61.568999999999996,29.28 -2020-06-10 19:15:00,108.06,44.47,61.568999999999996,29.28 -2020-06-10 19:30:00,106.76,43.638999999999996,61.568999999999996,29.28 -2020-06-10 19:45:00,103.49,43.93,61.568999999999996,29.28 -2020-06-10 20:00:00,96.17,42.407,68.145,29.28 -2020-06-10 20:15:00,96.64,42.173,68.145,29.28 -2020-06-10 20:30:00,95.52,42.398999999999994,68.145,29.28 -2020-06-10 20:45:00,94.92,42.938,68.145,29.28 -2020-06-10 21:00:00,90.77,41.101000000000006,59.696000000000005,29.28 -2020-06-10 21:15:00,90.39,42.868,59.696000000000005,29.28 -2020-06-10 21:30:00,86.63,43.795,59.696000000000005,29.28 -2020-06-10 21:45:00,85.83,44.854,59.696000000000005,29.28 -2020-06-10 22:00:00,81.02,42.332,54.861999999999995,29.28 -2020-06-10 22:15:00,79.98,43.971000000000004,54.861999999999995,29.28 -2020-06-10 22:30:00,77.22,38.265,54.861999999999995,29.28 -2020-06-10 22:45:00,76.83,35.029,54.861999999999995,29.28 -2020-06-10 23:00:00,73.12,30.535999999999998,45.568000000000005,29.28 -2020-06-10 23:15:00,75.38,28.914,45.568000000000005,29.28 -2020-06-10 23:30:00,74.71,27.851999999999997,45.568000000000005,29.28 -2020-06-10 23:45:00,75.2,27.229,45.568000000000005,29.28 -2020-06-11 00:00:00,70.41,26.171999999999997,40.181,29.28 -2020-06-11 00:15:00,70.82,26.772,40.181,29.28 -2020-06-11 00:30:00,70.59,25.719,40.181,29.28 -2020-06-11 00:45:00,70.45,25.116999999999997,40.181,29.28 -2020-06-11 01:00:00,69.57,24.886,38.296,29.28 -2020-06-11 01:15:00,70.14,24.081,38.296,29.28 -2020-06-11 01:30:00,68.96,22.445,38.296,29.28 -2020-06-11 01:45:00,69.74,21.936999999999998,38.296,29.28 -2020-06-11 02:00:00,69.81,21.438000000000002,36.575,29.28 -2020-06-11 02:15:00,69.88,19.944000000000003,36.575,29.28 -2020-06-11 02:30:00,70.09,22.129,36.575,29.28 -2020-06-11 02:45:00,76.04,22.886999999999997,36.575,29.28 -2020-06-11 03:00:00,79.84,24.581,36.394,29.28 -2020-06-11 03:15:00,79.7,23.234,36.394,29.28 -2020-06-11 03:30:00,74.17,22.535,36.394,29.28 -2020-06-11 03:45:00,76.38,21.927,36.394,29.28 -2020-06-11 04:00:00,75.88,28.939,37.207,29.28 -2020-06-11 04:15:00,78.09,37.323,37.207,29.28 -2020-06-11 04:30:00,82.26,34.653,37.207,29.28 -2020-06-11 04:45:00,86.97,35.016,37.207,29.28 -2020-06-11 05:00:00,94.99,52.582,40.713,29.28 -2020-06-11 05:15:00,100.23,64.817,40.713,29.28 -2020-06-11 05:30:00,106.76,55.861000000000004,40.713,29.28 -2020-06-11 05:45:00,111.82,52.246,40.713,29.28 -2020-06-11 06:00:00,116.83,53.778999999999996,50.952,29.28 -2020-06-11 06:15:00,112.43,54.458,50.952,29.28 -2020-06-11 06:30:00,113.1,52.248000000000005,50.952,29.28 -2020-06-11 06:45:00,118.36,52.81399999999999,50.952,29.28 -2020-06-11 07:00:00,124.46,53.745,64.88,29.28 -2020-06-11 07:15:00,126.91,53.378,64.88,29.28 -2020-06-11 07:30:00,121.71,50.56100000000001,64.88,29.28 -2020-06-11 07:45:00,118.85,49.775,64.88,29.28 -2020-06-11 08:00:00,123.62,47.285,55.133,29.28 -2020-06-11 08:15:00,123.22,49.50899999999999,55.133,29.28 -2020-06-11 08:30:00,122.66,49.281000000000006,55.133,29.28 -2020-06-11 08:45:00,116.59,51.448,55.133,29.28 -2020-06-11 09:00:00,119.41,47.053000000000004,48.912,29.28 -2020-06-11 09:15:00,123.06,45.975,48.912,29.28 -2020-06-11 09:30:00,124.81,49.357,48.912,29.28 -2020-06-11 09:45:00,122.25,51.726000000000006,48.912,29.28 -2020-06-11 10:00:00,123.12,47.016999999999996,45.968999999999994,29.28 -2020-06-11 10:15:00,122.31,48.696999999999996,45.968999999999994,29.28 -2020-06-11 10:30:00,124.76,48.813,45.968999999999994,29.28 -2020-06-11 10:45:00,121.0,50.336999999999996,45.968999999999994,29.28 -2020-06-11 11:00:00,114.05,45.86600000000001,44.067,29.28 -2020-06-11 11:15:00,112.2,47.202,44.067,29.28 -2020-06-11 11:30:00,108.33,48.652,44.067,29.28 -2020-06-11 11:45:00,108.51,50.427,44.067,29.28 -2020-06-11 12:00:00,109.52,46.573,41.501000000000005,29.28 -2020-06-11 12:15:00,106.82,46.66,41.501000000000005,29.28 -2020-06-11 12:30:00,106.48,45.667,41.501000000000005,29.28 -2020-06-11 12:45:00,100.88,47.20399999999999,41.501000000000005,29.28 -2020-06-11 13:00:00,95.77,47.875,41.117,29.28 -2020-06-11 13:15:00,95.82,48.938,41.117,29.28 -2020-06-11 13:30:00,100.99,47.275,41.117,29.28 -2020-06-11 13:45:00,104.58,45.864,41.117,29.28 -2020-06-11 14:00:00,103.94,47.951,41.492,29.28 -2020-06-11 14:15:00,100.72,46.599,41.492,29.28 -2020-06-11 14:30:00,97.94,45.475,41.492,29.28 -2020-06-11 14:45:00,99.64,46.071000000000005,41.492,29.28 -2020-06-11 15:00:00,99.53,47.706,43.711999999999996,29.28 -2020-06-11 15:15:00,97.45,45.283,43.711999999999996,29.28 -2020-06-11 15:30:00,94.25,43.497,43.711999999999996,29.28 -2020-06-11 15:45:00,100.3,41.3,43.711999999999996,29.28 -2020-06-11 16:00:00,103.4,43.946000000000005,45.446000000000005,29.28 -2020-06-11 16:15:00,100.4,43.488,45.446000000000005,29.28 -2020-06-11 16:30:00,100.04,42.797,45.446000000000005,29.28 -2020-06-11 16:45:00,101.12,39.244,45.446000000000005,29.28 -2020-06-11 17:00:00,110.22,41.901,48.803000000000004,29.28 -2020-06-11 17:15:00,110.07,41.951,48.803000000000004,29.28 -2020-06-11 17:30:00,113.51,41.235,48.803000000000004,29.28 -2020-06-11 17:45:00,106.59,39.578,48.803000000000004,29.28 -2020-06-11 18:00:00,104.74,41.95399999999999,51.167,29.28 -2020-06-11 18:15:00,106.72,42.18899999999999,51.167,29.28 -2020-06-11 18:30:00,107.72,39.83,51.167,29.28 -2020-06-11 18:45:00,102.99,44.225,51.167,29.28 -2020-06-11 19:00:00,99.51,45.243,52.486000000000004,29.28 -2020-06-11 19:15:00,103.25,44.294,52.486000000000004,29.28 -2020-06-11 19:30:00,105.06,43.455,52.486000000000004,29.28 -2020-06-11 19:45:00,97.36,43.74100000000001,52.486000000000004,29.28 -2020-06-11 20:00:00,101.57,42.2,59.635,29.28 -2020-06-11 20:15:00,101.54,41.964,59.635,29.28 -2020-06-11 20:30:00,100.96,42.202,59.635,29.28 -2020-06-11 20:45:00,96.25,42.772,59.635,29.28 -2020-06-11 21:00:00,92.74,40.939,54.353,29.28 -2020-06-11 21:15:00,89.61,42.716,54.353,29.28 -2020-06-11 21:30:00,87.7,43.619,54.353,29.28 -2020-06-11 21:45:00,94.37,44.69,54.353,29.28 -2020-06-11 22:00:00,89.15,42.188,49.431999999999995,29.28 -2020-06-11 22:15:00,88.41,43.838,49.431999999999995,29.28 -2020-06-11 22:30:00,80.58,38.126999999999995,49.431999999999995,29.28 -2020-06-11 22:45:00,80.98,34.881,49.431999999999995,29.28 -2020-06-11 23:00:00,82.25,30.368000000000002,42.872,29.28 -2020-06-11 23:15:00,83.85,28.783,42.872,29.28 -2020-06-11 23:30:00,79.67,27.729,42.872,29.28 -2020-06-11 23:45:00,74.85,27.095,42.872,29.28 -2020-06-12 00:00:00,71.36,24.219,39.819,29.28 -2020-06-12 00:15:00,72.66,25.046999999999997,39.819,29.28 -2020-06-12 00:30:00,77.04,24.245,39.819,29.28 -2020-06-12 00:45:00,78.71,24.064,39.819,29.28 -2020-06-12 01:00:00,74.65,23.453000000000003,37.797,29.28 -2020-06-12 01:15:00,72.83,22.143,37.797,29.28 -2020-06-12 01:30:00,71.34,21.164,37.797,29.28 -2020-06-12 01:45:00,78.24,20.419,37.797,29.28 -2020-06-12 02:00:00,77.22,20.825,36.905,29.28 -2020-06-12 02:15:00,75.15,19.269000000000002,36.905,29.28 -2020-06-12 02:30:00,76.1,22.295,36.905,29.28 -2020-06-12 02:45:00,72.39,22.408,36.905,29.28 -2020-06-12 03:00:00,77.81,24.747,37.1,29.28 -2020-06-12 03:15:00,79.33,22.335,37.1,29.28 -2020-06-12 03:30:00,78.33,21.410999999999998,37.1,29.28 -2020-06-12 03:45:00,74.64,21.698,37.1,29.28 -2020-06-12 04:00:00,77.34,28.813000000000002,37.882,29.28 -2020-06-12 04:15:00,84.37,35.63,37.882,29.28 -2020-06-12 04:30:00,86.71,33.882,37.882,29.28 -2020-06-12 04:45:00,88.82,33.51,37.882,29.28 -2020-06-12 05:00:00,89.4,50.383,40.777,29.28 -2020-06-12 05:15:00,93.53,63.548,40.777,29.28 -2020-06-12 05:30:00,98.42,54.953,40.777,29.28 -2020-06-12 05:45:00,98.91,50.955,40.777,29.28 -2020-06-12 06:00:00,102.82,52.763000000000005,55.528,29.28 -2020-06-12 06:15:00,102.95,53.437,55.528,29.28 -2020-06-12 06:30:00,110.8,51.111999999999995,55.528,29.28 -2020-06-12 06:45:00,112.79,51.744,55.528,29.28 -2020-06-12 07:00:00,116.91,53.201,67.749,29.28 -2020-06-12 07:15:00,107.11,53.821000000000005,67.749,29.28 -2020-06-12 07:30:00,104.05,49.117,67.749,29.28 -2020-06-12 07:45:00,103.9,48.068999999999996,67.749,29.28 -2020-06-12 08:00:00,106.28,46.215,57.55,29.28 -2020-06-12 08:15:00,102.7,49.07,57.55,29.28 -2020-06-12 08:30:00,109.84,48.847,57.55,29.28 -2020-06-12 08:45:00,110.15,50.713,57.55,29.28 -2020-06-12 09:00:00,108.4,44.169,52.588,29.28 -2020-06-12 09:15:00,103.04,44.977,52.588,29.28 -2020-06-12 09:30:00,109.97,47.66,52.588,29.28 -2020-06-12 09:45:00,108.51,50.403999999999996,52.588,29.28 -2020-06-12 10:00:00,107.23,45.416000000000004,49.772,29.28 -2020-06-12 10:15:00,105.95,46.98,49.772,29.28 -2020-06-12 10:30:00,114.2,47.611999999999995,49.772,29.28 -2020-06-12 10:45:00,115.09,49.004,49.772,29.28 -2020-06-12 11:00:00,112.1,44.768,49.226000000000006,29.28 -2020-06-12 11:15:00,107.96,44.983999999999995,49.226000000000006,29.28 -2020-06-12 11:30:00,105.26,46.276,49.226000000000006,29.28 -2020-06-12 11:45:00,103.17,47.152,49.226000000000006,29.28 -2020-06-12 12:00:00,101.7,43.861000000000004,45.705,29.28 -2020-06-12 12:15:00,103.75,43.108999999999995,45.705,29.28 -2020-06-12 12:30:00,102.31,42.221000000000004,45.705,29.28 -2020-06-12 12:45:00,102.28,43.075,45.705,29.28 -2020-06-12 13:00:00,98.03,44.407,43.133,29.28 -2020-06-12 13:15:00,98.2,45.735,43.133,29.28 -2020-06-12 13:30:00,97.7,44.833999999999996,43.133,29.28 -2020-06-12 13:45:00,99.86,43.71,43.133,29.28 -2020-06-12 14:00:00,100.43,44.925,41.989,29.28 -2020-06-12 14:15:00,103.53,43.963,41.989,29.28 -2020-06-12 14:30:00,105.57,44.29600000000001,41.989,29.28 -2020-06-12 14:45:00,108.6,44.278999999999996,41.989,29.28 -2020-06-12 15:00:00,107.88,45.821999999999996,43.728,29.28 -2020-06-12 15:15:00,108.67,43.114,43.728,29.28 -2020-06-12 15:30:00,108.56,40.613,43.728,29.28 -2020-06-12 15:45:00,108.51,39.143,43.728,29.28 -2020-06-12 16:00:00,107.02,40.894,45.93899999999999,29.28 -2020-06-12 16:15:00,107.22,40.931,45.93899999999999,29.28 -2020-06-12 16:30:00,106.33,40.096,45.93899999999999,29.28 -2020-06-12 16:45:00,105.53,35.755,45.93899999999999,29.28 -2020-06-12 17:00:00,105.78,40.11,50.488,29.28 -2020-06-12 17:15:00,104.46,39.942,50.488,29.28 -2020-06-12 17:30:00,104.33,39.343,50.488,29.28 -2020-06-12 17:45:00,106.02,37.482,50.488,29.28 -2020-06-12 18:00:00,104.89,39.993,52.408,29.28 -2020-06-12 18:15:00,103.14,39.260999999999996,52.408,29.28 -2020-06-12 18:30:00,102.14,36.836999999999996,52.408,29.28 -2020-06-12 18:45:00,101.78,41.63,52.408,29.28 -2020-06-12 19:00:00,98.46,43.586999999999996,52.736000000000004,29.28 -2020-06-12 19:15:00,94.32,43.331,52.736000000000004,29.28 -2020-06-12 19:30:00,94.61,42.503,52.736000000000004,29.28 -2020-06-12 19:45:00,91.52,41.763999999999996,52.736000000000004,29.28 -2020-06-12 20:00:00,89.27,40.058,59.68,29.28 -2020-06-12 20:15:00,91.99,40.582,59.68,29.28 -2020-06-12 20:30:00,88.8,40.359,59.68,29.28 -2020-06-12 20:45:00,88.94,40.238,59.68,29.28 -2020-06-12 21:00:00,86.4,39.718,54.343999999999994,29.28 -2020-06-12 21:15:00,85.82,43.148,54.343999999999994,29.28 -2020-06-12 21:30:00,83.13,43.888999999999996,54.343999999999994,29.28 -2020-06-12 21:45:00,84.05,45.218,54.343999999999994,29.28 -2020-06-12 22:00:00,77.72,42.698,49.672,29.28 -2020-06-12 22:15:00,77.94,44.105,49.672,29.28 -2020-06-12 22:30:00,74.99,43.566,49.672,29.28 -2020-06-12 22:45:00,76.11,41.47,49.672,29.28 -2020-06-12 23:00:00,70.24,38.516,42.065,29.28 -2020-06-12 23:15:00,69.98,35.275999999999996,42.065,29.28 -2020-06-12 23:30:00,69.72,32.342,42.065,29.28 -2020-06-12 23:45:00,69.07,31.518,42.065,29.28 -2020-06-13 00:00:00,64.84,25.0,38.829,29.17 -2020-06-13 00:15:00,66.33,24.767,38.829,29.17 -2020-06-13 00:30:00,65.93,23.649,38.829,29.17 -2020-06-13 00:45:00,64.97,22.844,38.829,29.17 -2020-06-13 01:00:00,64.06,22.574,34.63,29.17 -2020-06-13 01:15:00,64.21,21.715,34.63,29.17 -2020-06-13 01:30:00,62.98,19.908,34.63,29.17 -2020-06-13 01:45:00,63.23,20.326,34.63,29.17 -2020-06-13 02:00:00,62.58,19.86,32.465,29.17 -2020-06-13 02:15:00,62.35,17.509,32.465,29.17 -2020-06-13 02:30:00,62.98,19.667,32.465,29.17 -2020-06-13 02:45:00,62.07,20.549,32.465,29.17 -2020-06-13 03:00:00,63.84,21.636999999999997,31.925,29.17 -2020-06-13 03:15:00,70.97,18.439,31.925,29.17 -2020-06-13 03:30:00,65.1,17.679000000000002,31.925,29.17 -2020-06-13 03:45:00,63.54,19.415,31.925,29.17 -2020-06-13 04:00:00,61.45,24.273000000000003,31.309,29.17 -2020-06-13 04:15:00,69.01,29.947,31.309,29.17 -2020-06-13 04:30:00,69.89,26.381,31.309,29.17 -2020-06-13 04:45:00,70.47,26.219,31.309,29.17 -2020-06-13 05:00:00,64.5,33.701,30.323,29.17 -2020-06-13 05:15:00,66.69,34.52,30.323,29.17 -2020-06-13 05:30:00,66.77,27.484,30.323,29.17 -2020-06-13 05:45:00,69.87,28.311999999999998,30.323,29.17 -2020-06-13 06:00:00,75.11,42.504,31.438000000000002,29.17 -2020-06-13 06:15:00,75.75,52.11600000000001,31.438000000000002,29.17 -2020-06-13 06:30:00,84.06,46.602,31.438000000000002,29.17 -2020-06-13 06:45:00,87.29,43.371,31.438000000000002,29.17 -2020-06-13 07:00:00,86.33,43.028,34.891999999999996,29.17 -2020-06-13 07:15:00,87.6,42.312,34.891999999999996,29.17 -2020-06-13 07:30:00,90.91,39.294000000000004,34.891999999999996,29.17 -2020-06-13 07:45:00,93.57,39.534,34.891999999999996,29.17 -2020-06-13 08:00:00,94.56,38.69,39.608000000000004,29.17 -2020-06-13 08:15:00,92.12,41.727,39.608000000000004,29.17 -2020-06-13 08:30:00,96.32,41.638999999999996,39.608000000000004,29.17 -2020-06-13 08:45:00,97.62,44.715,39.608000000000004,29.17 -2020-06-13 09:00:00,91.24,41.26,40.894,29.17 -2020-06-13 09:15:00,87.74,42.605,40.894,29.17 -2020-06-13 09:30:00,87.68,45.849,40.894,29.17 -2020-06-13 09:45:00,84.6,48.202,40.894,29.17 -2020-06-13 10:00:00,79.46,43.803999999999995,39.525,29.17 -2020-06-13 10:15:00,84.16,45.736000000000004,39.525,29.17 -2020-06-13 10:30:00,89.39,46.066,39.525,29.17 -2020-06-13 10:45:00,89.77,47.268,39.525,29.17 -2020-06-13 11:00:00,91.12,42.92,36.718,29.17 -2020-06-13 11:15:00,93.16,43.891999999999996,36.718,29.17 -2020-06-13 11:30:00,86.51,45.324,36.718,29.17 -2020-06-13 11:45:00,84.34,46.735,36.718,29.17 -2020-06-13 12:00:00,79.65,43.74,35.688,29.17 -2020-06-13 12:15:00,73.18,43.858000000000004,35.688,29.17 -2020-06-13 12:30:00,70.87,42.857,35.688,29.17 -2020-06-13 12:45:00,71.71,44.318999999999996,35.688,29.17 -2020-06-13 13:00:00,72.51,44.803000000000004,32.858000000000004,29.17 -2020-06-13 13:15:00,73.91,45.407,32.858000000000004,29.17 -2020-06-13 13:30:00,70.81,44.647,32.858000000000004,29.17 -2020-06-13 13:45:00,71.46,42.306000000000004,32.858000000000004,29.17 -2020-06-13 14:00:00,65.58,43.733000000000004,31.738000000000003,29.17 -2020-06-13 14:15:00,67.67,41.534,31.738000000000003,29.17 -2020-06-13 14:30:00,66.02,41.251000000000005,31.738000000000003,29.17 -2020-06-13 14:45:00,69.44,41.713,31.738000000000003,29.17 -2020-06-13 15:00:00,69.59,43.79,34.35,29.17 -2020-06-13 15:15:00,68.57,41.821000000000005,34.35,29.17 -2020-06-13 15:30:00,70.63,39.661,34.35,29.17 -2020-06-13 15:45:00,70.47,37.313,34.35,29.17 -2020-06-13 16:00:00,70.88,41.03,37.522,29.17 -2020-06-13 16:15:00,69.13,40.306999999999995,37.522,29.17 -2020-06-13 16:30:00,70.38,39.698,37.522,29.17 -2020-06-13 16:45:00,72.18,35.393,37.522,29.17 -2020-06-13 17:00:00,75.53,38.603,42.498000000000005,29.17 -2020-06-13 17:15:00,75.43,36.553000000000004,42.498000000000005,29.17 -2020-06-13 17:30:00,76.48,35.812,42.498000000000005,29.17 -2020-06-13 17:45:00,78.84,34.356,42.498000000000005,29.17 -2020-06-13 18:00:00,80.38,38.174,44.701,29.17 -2020-06-13 18:15:00,80.49,39.178000000000004,44.701,29.17 -2020-06-13 18:30:00,83.97,38.166,44.701,29.17 -2020-06-13 18:45:00,81.48,39.333,44.701,29.17 -2020-06-13 19:00:00,81.9,39.872,45.727,29.17 -2020-06-13 19:15:00,76.64,38.601,45.727,29.17 -2020-06-13 19:30:00,75.82,38.568000000000005,45.727,29.17 -2020-06-13 19:45:00,78.53,39.488,45.727,29.17 -2020-06-13 20:00:00,74.72,38.754,43.391000000000005,29.17 -2020-06-13 20:15:00,76.21,38.875,43.391000000000005,29.17 -2020-06-13 20:30:00,72.61,37.795,43.391000000000005,29.17 -2020-06-13 20:45:00,72.49,39.428000000000004,43.391000000000005,29.17 -2020-06-13 21:00:00,71.31,37.734,41.231,29.17 -2020-06-13 21:15:00,69.62,40.884,41.231,29.17 -2020-06-13 21:30:00,68.99,41.898999999999994,41.231,29.17 -2020-06-13 21:45:00,65.8,42.681000000000004,41.231,29.17 -2020-06-13 22:00:00,61.94,40.286,40.798,29.17 -2020-06-13 22:15:00,62.44,42.176,40.798,29.17 -2020-06-13 22:30:00,60.41,41.869,40.798,29.17 -2020-06-13 22:45:00,59.47,40.296,40.798,29.17 -2020-06-13 23:00:00,56.53,36.909,34.402,29.17 -2020-06-13 23:15:00,56.79,33.949,34.402,29.17 -2020-06-13 23:30:00,56.94,33.191,34.402,29.17 -2020-06-13 23:45:00,56.32,32.468,34.402,29.17 -2020-06-14 00:00:00,52.28,26.233,30.171,29.17 -2020-06-14 00:15:00,53.6,24.875,30.171,29.17 -2020-06-14 00:30:00,49.61,23.584,30.171,29.17 -2020-06-14 00:45:00,51.87,22.745,30.171,29.17 -2020-06-14 01:00:00,50.13,22.730999999999998,27.15,29.17 -2020-06-14 01:15:00,51.9,21.837,27.15,29.17 -2020-06-14 01:30:00,50.98,19.942999999999998,27.15,29.17 -2020-06-14 01:45:00,51.2,19.963,27.15,29.17 -2020-06-14 02:00:00,49.19,19.500999999999998,25.403000000000002,29.17 -2020-06-14 02:15:00,49.45,17.74,25.403000000000002,29.17 -2020-06-14 02:30:00,46.81,20.155,25.403000000000002,29.17 -2020-06-14 02:45:00,49.86,20.811,25.403000000000002,29.17 -2020-06-14 03:00:00,48.72,22.546999999999997,23.386999999999997,29.17 -2020-06-14 03:15:00,49.41,19.547,23.386999999999997,29.17 -2020-06-14 03:30:00,49.29,18.229,23.386999999999997,29.17 -2020-06-14 03:45:00,48.21,19.223,23.386999999999997,29.17 -2020-06-14 04:00:00,46.3,23.975,23.941999999999997,29.17 -2020-06-14 04:15:00,49.49,29.096999999999998,23.941999999999997,29.17 -2020-06-14 04:30:00,49.02,26.796999999999997,23.941999999999997,29.17 -2020-06-14 04:45:00,51.15,26.214000000000002,23.941999999999997,29.17 -2020-06-14 05:00:00,52.15,33.439,23.026,29.17 -2020-06-14 05:15:00,52.37,33.179,23.026,29.17 -2020-06-14 05:30:00,51.73,25.811,23.026,29.17 -2020-06-14 05:45:00,51.3,26.45,23.026,29.17 -2020-06-14 06:00:00,55.97,38.391,23.223000000000003,29.17 -2020-06-14 06:15:00,56.52,48.623999999999995,23.223000000000003,29.17 -2020-06-14 06:30:00,57.52,42.369,23.223000000000003,29.17 -2020-06-14 06:45:00,56.72,38.123000000000005,23.223000000000003,29.17 -2020-06-14 07:00:00,58.51,38.266999999999996,24.968000000000004,29.17 -2020-06-14 07:15:00,64.2,35.931999999999995,24.968000000000004,29.17 -2020-06-14 07:30:00,64.55,34.004,24.968000000000004,29.17 -2020-06-14 07:45:00,59.52,34.175,24.968000000000004,29.17 -2020-06-14 08:00:00,60.05,34.224000000000004,29.131,29.17 -2020-06-14 08:15:00,60.54,38.406,29.131,29.17 -2020-06-14 08:30:00,59.86,39.29,29.131,29.17 -2020-06-14 08:45:00,59.75,42.483999999999995,29.131,29.17 -2020-06-14 09:00:00,55.4,38.879,29.904,29.17 -2020-06-14 09:15:00,59.64,39.813,29.904,29.17 -2020-06-14 09:30:00,58.18,43.45,29.904,29.17 -2020-06-14 09:45:00,60.74,46.76,29.904,29.17 -2020-06-14 10:00:00,65.21,43.11600000000001,28.943,29.17 -2020-06-14 10:15:00,69.62,45.239,28.943,29.17 -2020-06-14 10:30:00,76.14,45.839,28.943,29.17 -2020-06-14 10:45:00,76.23,47.826,28.943,29.17 -2020-06-14 11:00:00,72.62,43.235,31.682,29.17 -2020-06-14 11:15:00,66.42,43.803999999999995,31.682,29.17 -2020-06-14 11:30:00,63.36,45.636,31.682,29.17 -2020-06-14 11:45:00,64.54,47.348,31.682,29.17 -2020-06-14 12:00:00,58.01,45.331,27.315,29.17 -2020-06-14 12:15:00,62.22,45.035,27.315,29.17 -2020-06-14 12:30:00,67.96,44.08,27.315,29.17 -2020-06-14 12:45:00,72.55,44.851000000000006,27.315,29.17 -2020-06-14 13:00:00,70.76,44.978,23.894000000000002,29.17 -2020-06-14 13:15:00,65.86,45.318000000000005,23.894000000000002,29.17 -2020-06-14 13:30:00,52.16,43.497,23.894000000000002,29.17 -2020-06-14 13:45:00,56.73,42.155,23.894000000000002,29.17 -2020-06-14 14:00:00,56.02,44.756,21.148000000000003,29.17 -2020-06-14 14:15:00,58.16,43.071999999999996,21.148000000000003,29.17 -2020-06-14 14:30:00,59.65,41.708999999999996,21.148000000000003,29.17 -2020-06-14 14:45:00,66.92,41.126999999999995,21.148000000000003,29.17 -2020-06-14 15:00:00,72.76,43.235,21.229,29.17 -2020-06-14 15:15:00,75.07,40.558,21.229,29.17 -2020-06-14 15:30:00,73.16,38.271,21.229,29.17 -2020-06-14 15:45:00,75.74,36.243,21.229,29.17 -2020-06-14 16:00:00,74.37,38.602,25.037,29.17 -2020-06-14 16:15:00,75.85,38.010999999999996,25.037,29.17 -2020-06-14 16:30:00,76.68,38.395,25.037,29.17 -2020-06-14 16:45:00,79.86,34.135,25.037,29.17 -2020-06-14 17:00:00,80.06,37.714,37.11,29.17 -2020-06-14 17:15:00,75.98,37.064,37.11,29.17 -2020-06-14 17:30:00,76.02,37.105,37.11,29.17 -2020-06-14 17:45:00,75.92,36.233000000000004,37.11,29.17 -2020-06-14 18:00:00,79.59,40.564,42.215,29.17 -2020-06-14 18:15:00,78.09,41.273999999999994,42.215,29.17 -2020-06-14 18:30:00,79.69,39.839,42.215,29.17 -2020-06-14 18:45:00,80.19,41.276,42.215,29.17 -2020-06-14 19:00:00,81.77,43.848,44.383,29.17 -2020-06-14 19:15:00,78.54,41.562,44.383,29.17 -2020-06-14 19:30:00,77.06,41.271,44.383,29.17 -2020-06-14 19:45:00,76.9,41.873999999999995,44.383,29.17 -2020-06-14 20:00:00,77.81,41.295,43.426,29.17 -2020-06-14 20:15:00,77.47,41.37,43.426,29.17 -2020-06-14 20:30:00,78.68,41.177,43.426,29.17 -2020-06-14 20:45:00,80.51,41.136,43.426,29.17 -2020-06-14 21:00:00,78.65,39.095,42.265,29.17 -2020-06-14 21:15:00,80.2,41.913999999999994,42.265,29.17 -2020-06-14 21:30:00,77.65,42.313,42.265,29.17 -2020-06-14 21:45:00,77.23,43.458,42.265,29.17 -2020-06-14 22:00:00,72.55,42.913999999999994,42.26,29.17 -2020-06-14 22:15:00,73.29,43.146,42.26,29.17 -2020-06-14 22:30:00,72.39,42.07899999999999,42.26,29.17 -2020-06-14 22:45:00,72.99,39.211,42.26,29.17 -2020-06-14 23:00:00,66.65,35.281,36.609,29.17 -2020-06-14 23:15:00,66.5,33.724000000000004,36.609,29.17 -2020-06-14 23:30:00,66.47,32.531,36.609,29.17 -2020-06-14 23:45:00,66.58,32.023,36.609,29.17 -2020-06-15 00:00:00,63.68,27.921999999999997,34.611,29.28 -2020-06-15 00:15:00,66.57,27.576,34.611,29.28 -2020-06-15 00:30:00,66.22,25.934,34.611,29.28 -2020-06-15 00:45:00,67.82,24.68,34.611,29.28 -2020-06-15 01:00:00,66.38,25.049,33.552,29.28 -2020-06-15 01:15:00,63.83,24.105,33.552,29.28 -2020-06-15 01:30:00,65.85,22.554000000000002,33.552,29.28 -2020-06-15 01:45:00,63.4,22.485,33.552,29.28 -2020-06-15 02:00:00,64.21,22.454,32.351,29.28 -2020-06-15 02:15:00,65.04,19.855,32.351,29.28 -2020-06-15 02:30:00,65.8,22.451,32.351,29.28 -2020-06-15 02:45:00,71.81,22.92,32.351,29.28 -2020-06-15 03:00:00,71.74,25.247,30.793000000000003,29.28 -2020-06-15 03:15:00,73.07,23.074,30.793000000000003,29.28 -2020-06-15 03:30:00,67.77,22.386999999999997,30.793000000000003,29.28 -2020-06-15 03:45:00,68.94,22.915,30.793000000000003,29.28 -2020-06-15 04:00:00,70.83,30.949,31.274,29.28 -2020-06-15 04:15:00,72.74,39.173,31.274,29.28 -2020-06-15 04:30:00,76.76,36.596,31.274,29.28 -2020-06-15 04:45:00,82.08,36.376999999999995,31.274,29.28 -2020-06-15 05:00:00,88.23,51.6,37.75,29.28 -2020-06-15 05:15:00,92.27,62.74100000000001,37.75,29.28 -2020-06-15 05:30:00,94.48,53.993,37.75,29.28 -2020-06-15 05:45:00,97.09,51.272,37.75,29.28 -2020-06-15 06:00:00,102.24,51.938,55.36,29.28 -2020-06-15 06:15:00,111.18,52.23,55.36,29.28 -2020-06-15 06:30:00,115.52,50.466,55.36,29.28 -2020-06-15 06:45:00,117.34,52.028,55.36,29.28 -2020-06-15 07:00:00,115.83,52.782,65.87,29.28 -2020-06-15 07:15:00,114.27,52.74100000000001,65.87,29.28 -2020-06-15 07:30:00,122.7,49.918,65.87,29.28 -2020-06-15 07:45:00,120.92,50.178999999999995,65.87,29.28 -2020-06-15 08:00:00,119.84,47.783,55.695,29.28 -2020-06-15 08:15:00,111.36,50.665,55.695,29.28 -2020-06-15 08:30:00,110.75,50.261,55.695,29.28 -2020-06-15 08:45:00,113.47,53.36,55.695,29.28 -2020-06-15 09:00:00,117.2,48.68899999999999,50.881,29.28 -2020-06-15 09:15:00,116.73,47.618,50.881,29.28 -2020-06-15 09:30:00,110.25,50.285,50.881,29.28 -2020-06-15 09:45:00,118.14,51.467,50.881,29.28 -2020-06-15 10:00:00,117.56,48.161,49.138000000000005,29.28 -2020-06-15 10:15:00,114.82,50.128,49.138000000000005,29.28 -2020-06-15 10:30:00,113.15,50.214,49.138000000000005,29.28 -2020-06-15 10:45:00,110.62,50.78,49.138000000000005,29.28 -2020-06-15 11:00:00,110.55,46.151,49.178000000000004,29.28 -2020-06-15 11:15:00,113.2,47.161,49.178000000000004,29.28 -2020-06-15 11:30:00,114.7,49.771,49.178000000000004,29.28 -2020-06-15 11:45:00,107.65,51.922,49.178000000000004,29.28 -2020-06-15 12:00:00,103.58,48.492,47.698,29.28 -2020-06-15 12:15:00,100.76,48.303000000000004,47.698,29.28 -2020-06-15 12:30:00,98.31,46.336000000000006,47.698,29.28 -2020-06-15 12:45:00,100.35,47.248000000000005,47.698,29.28 -2020-06-15 13:00:00,98.72,48.282,48.104,29.28 -2020-06-15 13:15:00,97.1,47.658,48.104,29.28 -2020-06-15 13:30:00,98.57,45.958,48.104,29.28 -2020-06-15 13:45:00,100.45,45.47,48.104,29.28 -2020-06-15 14:00:00,109.23,47.168,48.53,29.28 -2020-06-15 14:15:00,107.1,45.983999999999995,48.53,29.28 -2020-06-15 14:30:00,105.45,44.391000000000005,48.53,29.28 -2020-06-15 14:45:00,107.37,45.77,48.53,29.28 -2020-06-15 15:00:00,105.82,47.726000000000006,49.351000000000006,29.28 -2020-06-15 15:15:00,101.42,44.35,49.351000000000006,29.28 -2020-06-15 15:30:00,93.89,42.706,49.351000000000006,29.28 -2020-06-15 15:45:00,102.39,40.167,49.351000000000006,29.28 -2020-06-15 16:00:00,103.23,43.58,51.44,29.28 -2020-06-15 16:15:00,103.03,42.976000000000006,51.44,29.28 -2020-06-15 16:30:00,103.38,42.66,51.44,29.28 -2020-06-15 16:45:00,105.31,38.287,51.44,29.28 -2020-06-15 17:00:00,110.64,40.8,56.868,29.28 -2020-06-15 17:15:00,108.12,40.414,56.868,29.28 -2020-06-15 17:30:00,107.45,40.03,56.868,29.28 -2020-06-15 17:45:00,108.12,38.626,56.868,29.28 -2020-06-15 18:00:00,107.0,41.968,57.229,29.28 -2020-06-15 18:15:00,105.19,40.717,57.229,29.28 -2020-06-15 18:30:00,103.36,38.615,57.229,29.28 -2020-06-15 18:45:00,103.93,43.123999999999995,57.229,29.28 -2020-06-15 19:00:00,101.36,45.273,57.744,29.28 -2020-06-15 19:15:00,97.27,44.143,57.744,29.28 -2020-06-15 19:30:00,94.43,43.535,57.744,29.28 -2020-06-15 19:45:00,94.45,43.476000000000006,57.744,29.28 -2020-06-15 20:00:00,93.01,41.475,66.05199999999999,29.28 -2020-06-15 20:15:00,93.07,42.67,66.05199999999999,29.28 -2020-06-15 20:30:00,92.72,42.825,66.05199999999999,29.28 -2020-06-15 20:45:00,92.0,43.159,66.05199999999999,29.28 -2020-06-15 21:00:00,90.53,40.566,59.396,29.28 -2020-06-15 21:15:00,89.26,43.715,59.396,29.28 -2020-06-15 21:30:00,86.15,44.418,59.396,29.28 -2020-06-15 21:45:00,83.94,45.318999999999996,59.396,29.28 -2020-06-15 22:00:00,79.53,42.593999999999994,53.06,29.28 -2020-06-15 22:15:00,79.12,44.623999999999995,53.06,29.28 -2020-06-15 22:30:00,76.82,38.604,53.06,29.28 -2020-06-15 22:45:00,77.6,35.296,53.06,29.28 -2020-06-15 23:00:00,71.23,31.458000000000002,46.148,29.28 -2020-06-15 23:15:00,71.79,28.58,46.148,29.28 -2020-06-15 23:30:00,73.2,27.590999999999998,46.148,29.28 -2020-06-15 23:45:00,71.26,26.781,46.148,29.28 -2020-06-16 00:00:00,68.96,24.359,44.625,29.28 -2020-06-16 00:15:00,69.29,25.055999999999997,44.625,29.28 -2020-06-16 00:30:00,68.44,23.929000000000002,44.625,29.28 -2020-06-16 00:45:00,68.79,23.45,44.625,29.28 -2020-06-16 01:00:00,66.84,23.256,41.733000000000004,29.28 -2020-06-16 01:15:00,68.54,22.443,41.733000000000004,29.28 -2020-06-16 01:30:00,67.64,20.79,41.733000000000004,29.28 -2020-06-16 01:45:00,68.65,20.428,41.733000000000004,29.28 -2020-06-16 02:00:00,68.14,19.965999999999998,39.872,29.28 -2020-06-16 02:15:00,67.92,18.408,39.872,29.28 -2020-06-16 02:30:00,67.69,20.671999999999997,39.872,29.28 -2020-06-16 02:45:00,68.5,21.430999999999997,39.872,29.28 -2020-06-16 03:00:00,68.84,23.045,38.711,29.28 -2020-06-16 03:15:00,70.43,21.794,38.711,29.28 -2020-06-16 03:30:00,71.46,21.061999999999998,38.711,29.28 -2020-06-16 03:45:00,71.24,20.377,38.711,29.28 -2020-06-16 04:00:00,78.78,26.963,39.823,29.28 -2020-06-16 04:15:00,83.37,34.927,39.823,29.28 -2020-06-16 04:30:00,86.33,32.236999999999995,39.823,29.28 -2020-06-16 04:45:00,84.83,32.514,39.823,29.28 -2020-06-16 05:00:00,89.13,48.638999999999996,43.228,29.28 -2020-06-16 05:15:00,95.5,59.591,43.228,29.28 -2020-06-16 05:30:00,102.96,51.088,43.228,29.28 -2020-06-16 05:45:00,106.21,47.806999999999995,43.228,29.28 -2020-06-16 06:00:00,110.04,49.923,54.316,29.28 -2020-06-16 06:15:00,106.31,50.313,54.316,29.28 -2020-06-16 06:30:00,112.22,48.407,54.316,29.28 -2020-06-16 06:45:00,116.7,49.299,54.316,29.28 -2020-06-16 07:00:00,118.4,50.067,65.758,29.28 -2020-06-16 07:15:00,114.45,49.853,65.758,29.28 -2020-06-16 07:30:00,120.01,47.098,65.758,29.28 -2020-06-16 07:45:00,121.47,46.503,65.758,29.28 -2020-06-16 08:00:00,122.52,43.562,57.983000000000004,29.28 -2020-06-16 08:15:00,120.87,46.042,57.983000000000004,29.28 -2020-06-16 08:30:00,119.62,46.19,57.983000000000004,29.28 -2020-06-16 08:45:00,120.59,48.538000000000004,57.983000000000004,29.28 -2020-06-16 09:00:00,119.48,43.669,52.653,29.28 -2020-06-16 09:15:00,115.43,42.684,52.653,29.28 -2020-06-16 09:30:00,115.25,46.261,52.653,29.28 -2020-06-16 09:45:00,119.15,48.981,52.653,29.28 -2020-06-16 10:00:00,124.46,43.623000000000005,51.408,29.28 -2020-06-16 10:15:00,124.81,45.39,51.408,29.28 -2020-06-16 10:30:00,116.98,45.55,51.408,29.28 -2020-06-16 10:45:00,119.25,47.229,51.408,29.28 -2020-06-16 11:00:00,119.52,43.428000000000004,51.913000000000004,29.28 -2020-06-16 11:15:00,106.77,44.788000000000004,51.913000000000004,29.28 -2020-06-16 11:30:00,105.75,46.257,51.913000000000004,29.28 -2020-06-16 11:45:00,111.19,48.073,51.913000000000004,29.28 -2020-06-16 12:00:00,110.92,44.413000000000004,49.508,29.28 -2020-06-16 12:15:00,110.35,44.346000000000004,49.508,29.28 -2020-06-16 12:30:00,105.59,43.29,49.508,29.28 -2020-06-16 12:45:00,115.07,44.753,49.508,29.28 -2020-06-16 13:00:00,112.54,45.398,50.007,29.28 -2020-06-16 13:15:00,111.53,46.568999999999996,50.007,29.28 -2020-06-16 13:30:00,107.86,44.854,50.007,29.28 -2020-06-16 13:45:00,113.1,43.668,50.007,29.28 -2020-06-16 14:00:00,111.48,45.714,49.778999999999996,29.28 -2020-06-16 14:15:00,108.96,44.407,49.778999999999996,29.28 -2020-06-16 14:30:00,105.74,43.292,49.778999999999996,29.28 -2020-06-16 14:45:00,106.26,43.869,49.778999999999996,29.28 -2020-06-16 15:00:00,102.33,45.617,51.559,29.28 -2020-06-16 15:15:00,103.42,43.174,51.559,29.28 -2020-06-16 15:30:00,111.85,41.39,51.559,29.28 -2020-06-16 15:45:00,116.04,39.3,51.559,29.28 -2020-06-16 16:00:00,111.53,42.037,53.531000000000006,29.28 -2020-06-16 16:15:00,109.87,41.598,53.531000000000006,29.28 -2020-06-16 16:30:00,107.38,40.802,53.531000000000006,29.28 -2020-06-16 16:45:00,111.98,37.24,53.531000000000006,29.28 -2020-06-16 17:00:00,113.93,40.24,59.497,29.28 -2020-06-16 17:15:00,112.65,40.22,59.497,29.28 -2020-06-16 17:30:00,108.82,39.359,59.497,29.28 -2020-06-16 17:45:00,114.35,37.766,59.497,29.28 -2020-06-16 18:00:00,115.88,40.343,59.861999999999995,29.28 -2020-06-16 18:15:00,116.48,40.538000000000004,59.861999999999995,29.28 -2020-06-16 18:30:00,107.47,38.215,59.861999999999995,29.28 -2020-06-16 18:45:00,107.61,42.318000000000005,59.861999999999995,29.28 -2020-06-16 19:00:00,111.62,42.979,60.989,29.28 -2020-06-16 19:15:00,106.07,42.001000000000005,60.989,29.28 -2020-06-16 19:30:00,102.41,41.065,60.989,29.28 -2020-06-16 19:45:00,95.32,41.147,60.989,29.28 -2020-06-16 20:00:00,92.13,38.826,68.35600000000001,29.28 -2020-06-16 20:15:00,94.97,38.798,68.35600000000001,29.28 -2020-06-16 20:30:00,91.06,39.229,68.35600000000001,29.28 -2020-06-16 20:45:00,94.21,39.921,68.35600000000001,29.28 -2020-06-16 21:00:00,89.7,38.23,59.251000000000005,29.28 -2020-06-16 21:15:00,89.12,40.126999999999995,59.251000000000005,29.28 -2020-06-16 21:30:00,86.59,41.136,59.251000000000005,29.28 -2020-06-16 21:45:00,85.8,42.263999999999996,59.251000000000005,29.28 -2020-06-16 22:00:00,81.71,39.696,54.736999999999995,29.28 -2020-06-16 22:15:00,82.26,41.648,54.736999999999995,29.28 -2020-06-16 22:30:00,79.77,36.277,54.736999999999995,29.28 -2020-06-16 22:45:00,81.67,32.983000000000004,54.736999999999995,29.28 -2020-06-16 23:00:00,74.51,28.76,46.806999999999995,29.28 -2020-06-16 23:15:00,74.79,27.148000000000003,46.806999999999995,29.28 -2020-06-16 23:30:00,82.15,25.996,46.806999999999995,29.28 -2020-06-16 23:45:00,74.96,25.283,46.806999999999995,29.28 -2020-06-17 00:00:00,70.38,24.259,43.824,29.28 -2020-06-17 00:15:00,71.92,24.956,43.824,29.28 -2020-06-17 00:30:00,70.72,23.831999999999997,43.824,29.28 -2020-06-17 00:45:00,71.52,23.358,43.824,29.28 -2020-06-17 01:00:00,69.75,23.180999999999997,39.86,29.28 -2020-06-17 01:15:00,70.66,22.351999999999997,39.86,29.28 -2020-06-17 01:30:00,70.25,20.694000000000003,39.86,29.28 -2020-06-17 01:45:00,70.36,20.323,39.86,29.28 -2020-06-17 02:00:00,69.85,19.865,37.931999999999995,29.28 -2020-06-17 02:15:00,69.97,18.305999999999997,37.931999999999995,29.28 -2020-06-17 02:30:00,77.19,20.566,37.931999999999995,29.28 -2020-06-17 02:45:00,79.52,21.33,37.931999999999995,29.28 -2020-06-17 03:00:00,75.47,22.943,37.579,29.28 -2020-06-17 03:15:00,71.44,21.693,37.579,29.28 -2020-06-17 03:30:00,73.65,20.968000000000004,37.579,29.28 -2020-06-17 03:45:00,74.61,20.305,37.579,29.28 -2020-06-17 04:00:00,75.19,26.839000000000002,37.931999999999995,29.28 -2020-06-17 04:15:00,77.28,34.758,37.931999999999995,29.28 -2020-06-17 04:30:00,80.23,32.056,37.931999999999995,29.28 -2020-06-17 04:45:00,86.39,32.33,37.931999999999995,29.28 -2020-06-17 05:00:00,92.75,48.354,40.942,29.28 -2020-06-17 05:15:00,94.78,59.178000000000004,40.942,29.28 -2020-06-17 05:30:00,102.45,50.743,40.942,29.28 -2020-06-17 05:45:00,103.25,47.508,40.942,29.28 -2020-06-17 06:00:00,109.39,49.636,56.516999999999996,29.28 -2020-06-17 06:15:00,108.76,50.011,56.516999999999996,29.28 -2020-06-17 06:30:00,106.58,48.129,56.516999999999996,29.28 -2020-06-17 06:45:00,115.87,49.049,56.516999999999996,29.28 -2020-06-17 07:00:00,120.05,49.805,71.707,29.28 -2020-06-17 07:15:00,118.84,49.608999999999995,71.707,29.28 -2020-06-17 07:30:00,112.36,46.847,71.707,29.28 -2020-06-17 07:45:00,116.82,46.285,71.707,29.28 -2020-06-17 08:00:00,118.62,43.354,61.17,29.28 -2020-06-17 08:15:00,120.54,45.867,61.17,29.28 -2020-06-17 08:30:00,112.25,46.007,61.17,29.28 -2020-06-17 08:45:00,110.41,48.36,61.17,29.28 -2020-06-17 09:00:00,112.65,43.483999999999995,57.282,29.28 -2020-06-17 09:15:00,116.01,42.503,57.282,29.28 -2020-06-17 09:30:00,115.28,46.07899999999999,57.282,29.28 -2020-06-17 09:45:00,117.39,48.817,57.282,29.28 -2020-06-17 10:00:00,119.51,43.468,54.026,29.28 -2020-06-17 10:15:00,118.12,45.247,54.026,29.28 -2020-06-17 10:30:00,115.99,45.409,54.026,29.28 -2020-06-17 10:45:00,112.31,47.093999999999994,54.026,29.28 -2020-06-17 11:00:00,117.86,43.288999999999994,54.277,29.28 -2020-06-17 11:15:00,118.81,44.655,54.277,29.28 -2020-06-17 11:30:00,112.48,46.113,54.277,29.28 -2020-06-17 11:45:00,106.68,47.93,54.277,29.28 -2020-06-17 12:00:00,107.8,44.3,52.552,29.28 -2020-06-17 12:15:00,109.75,44.235,52.552,29.28 -2020-06-17 12:30:00,109.97,43.158,52.552,29.28 -2020-06-17 12:45:00,106.0,44.623000000000005,52.552,29.28 -2020-06-17 13:00:00,99.76,45.25899999999999,52.111999999999995,29.28 -2020-06-17 13:15:00,101.66,46.431999999999995,52.111999999999995,29.28 -2020-06-17 13:30:00,106.4,44.727,52.111999999999995,29.28 -2020-06-17 13:45:00,109.32,43.545,52.111999999999995,29.28 -2020-06-17 14:00:00,108.0,45.607,52.066,29.28 -2020-06-17 14:15:00,102.64,44.297,52.066,29.28 -2020-06-17 14:30:00,105.38,43.159,52.066,29.28 -2020-06-17 14:45:00,107.65,43.742,52.066,29.28 -2020-06-17 15:00:00,106.16,45.522,52.523999999999994,29.28 -2020-06-17 15:15:00,98.78,43.071000000000005,52.523999999999994,29.28 -2020-06-17 15:30:00,97.44,41.277,52.523999999999994,29.28 -2020-06-17 15:45:00,102.09,39.176,52.523999999999994,29.28 -2020-06-17 16:00:00,102.74,41.942,54.101000000000006,29.28 -2020-06-17 16:15:00,111.61,41.501000000000005,54.101000000000006,29.28 -2020-06-17 16:30:00,112.07,40.723,54.101000000000006,29.28 -2020-06-17 16:45:00,111.13,37.138000000000005,54.101000000000006,29.28 -2020-06-17 17:00:00,107.3,40.161,58.155,29.28 -2020-06-17 17:15:00,106.07,40.135,58.155,29.28 -2020-06-17 17:30:00,109.67,39.269,58.155,29.28 -2020-06-17 17:45:00,109.22,37.658,58.155,29.28 -2020-06-17 18:00:00,115.64,40.247,60.205,29.28 -2020-06-17 18:15:00,115.65,40.415,60.205,29.28 -2020-06-17 18:30:00,112.11,38.086999999999996,60.205,29.28 -2020-06-17 18:45:00,108.54,42.19,60.205,29.28 -2020-06-17 19:00:00,111.91,42.853,61.568999999999996,29.28 -2020-06-17 19:15:00,107.65,41.867,61.568999999999996,29.28 -2020-06-17 19:30:00,104.81,40.923,61.568999999999996,29.28 -2020-06-17 19:45:00,100.79,41.0,61.568999999999996,29.28 -2020-06-17 20:00:00,95.94,38.662,68.145,29.28 -2020-06-17 20:15:00,95.76,38.63,68.145,29.28 -2020-06-17 20:30:00,95.13,39.07,68.145,29.28 -2020-06-17 20:45:00,95.74,39.789,68.145,29.28 -2020-06-17 21:00:00,93.35,38.104,59.696000000000005,29.28 -2020-06-17 21:15:00,92.86,40.008,59.696000000000005,29.28 -2020-06-17 21:30:00,90.82,40.994,59.696000000000005,29.28 -2020-06-17 21:45:00,88.68,42.13,59.696000000000005,29.28 -2020-06-17 22:00:00,84.2,39.58,54.861999999999995,29.28 -2020-06-17 22:15:00,84.82,41.541000000000004,54.861999999999995,29.28 -2020-06-17 22:30:00,81.78,36.164,54.861999999999995,29.28 -2020-06-17 22:45:00,80.67,32.86,54.861999999999995,29.28 -2020-06-17 23:00:00,74.94,28.62,45.568000000000005,29.28 -2020-06-17 23:15:00,76.51,27.043000000000003,45.568000000000005,29.28 -2020-06-17 23:30:00,75.84,25.903000000000002,45.568000000000005,29.28 -2020-06-17 23:45:00,75.6,25.178,45.568000000000005,29.28 -2020-06-18 00:00:00,73.4,24.163,40.181,29.28 -2020-06-18 00:15:00,72.87,24.86,40.181,29.28 -2020-06-18 00:30:00,72.34,23.739,40.181,29.28 -2020-06-18 00:45:00,73.84,23.271,40.181,29.28 -2020-06-18 01:00:00,71.31,23.109,38.296,29.28 -2020-06-18 01:15:00,72.98,22.264,38.296,29.28 -2020-06-18 01:30:00,73.12,20.601999999999997,38.296,29.28 -2020-06-18 01:45:00,72.86,20.224,38.296,29.28 -2020-06-18 02:00:00,72.69,19.769000000000002,36.575,29.28 -2020-06-18 02:15:00,73.69,18.209,36.575,29.28 -2020-06-18 02:30:00,80.7,20.464000000000002,36.575,29.28 -2020-06-18 02:45:00,81.54,21.235,36.575,29.28 -2020-06-18 03:00:00,76.54,22.845,36.394,29.28 -2020-06-18 03:15:00,74.22,21.598000000000003,36.394,29.28 -2020-06-18 03:30:00,74.27,20.877,36.394,29.28 -2020-06-18 03:45:00,75.37,20.236,36.394,29.28 -2020-06-18 04:00:00,78.58,26.719,37.207,29.28 -2020-06-18 04:15:00,77.79,34.595,37.207,29.28 -2020-06-18 04:30:00,81.29,31.881,37.207,29.28 -2020-06-18 04:45:00,87.9,32.150999999999996,37.207,29.28 -2020-06-18 05:00:00,91.79,48.077,40.713,29.28 -2020-06-18 05:15:00,94.18,58.776,40.713,29.28 -2020-06-18 05:30:00,102.83,50.407,40.713,29.28 -2020-06-18 05:45:00,105.92,47.218999999999994,40.713,29.28 -2020-06-18 06:00:00,112.75,49.357,50.952,29.28 -2020-06-18 06:15:00,110.24,49.718999999999994,50.952,29.28 -2020-06-18 06:30:00,106.85,47.858000000000004,50.952,29.28 -2020-06-18 06:45:00,108.84,48.808,50.952,29.28 -2020-06-18 07:00:00,116.96,49.552,64.88,29.28 -2020-06-18 07:15:00,117.84,49.375,64.88,29.28 -2020-06-18 07:30:00,118.78,46.605,64.88,29.28 -2020-06-18 07:45:00,110.21,46.077,64.88,29.28 -2020-06-18 08:00:00,112.37,43.156000000000006,55.133,29.28 -2020-06-18 08:15:00,120.59,45.7,55.133,29.28 -2020-06-18 08:30:00,116.62,45.832,55.133,29.28 -2020-06-18 08:45:00,113.88,48.188,55.133,29.28 -2020-06-18 09:00:00,111.01,43.306000000000004,48.912,29.28 -2020-06-18 09:15:00,116.08,42.32899999999999,48.912,29.28 -2020-06-18 09:30:00,116.1,45.903999999999996,48.912,29.28 -2020-06-18 09:45:00,110.88,48.659,48.912,29.28 -2020-06-18 10:00:00,111.72,43.321000000000005,45.968999999999994,29.28 -2020-06-18 10:15:00,116.87,45.11,45.968999999999994,29.28 -2020-06-18 10:30:00,119.29,45.273999999999994,45.968999999999994,29.28 -2020-06-18 10:45:00,117.36,46.965,45.968999999999994,29.28 -2020-06-18 11:00:00,115.58,43.156000000000006,44.067,29.28 -2020-06-18 11:15:00,108.97,44.528,44.067,29.28 -2020-06-18 11:30:00,104.55,45.975,44.067,29.28 -2020-06-18 11:45:00,104.96,47.792,44.067,29.28 -2020-06-18 12:00:00,117.07,44.193000000000005,41.501000000000005,29.28 -2020-06-18 12:15:00,118.98,44.129,41.501000000000005,29.28 -2020-06-18 12:30:00,111.45,43.032,41.501000000000005,29.28 -2020-06-18 12:45:00,105.65,44.498999999999995,41.501000000000005,29.28 -2020-06-18 13:00:00,107.61,45.126000000000005,41.117,29.28 -2020-06-18 13:15:00,111.84,46.298,41.117,29.28 -2020-06-18 13:30:00,112.94,44.604,41.117,29.28 -2020-06-18 13:45:00,114.13,43.426,41.117,29.28 -2020-06-18 14:00:00,110.3,45.501999999999995,41.492,29.28 -2020-06-18 14:15:00,112.76,44.191,41.492,29.28 -2020-06-18 14:30:00,115.17,43.031000000000006,41.492,29.28 -2020-06-18 14:45:00,117.92,43.619,41.492,29.28 -2020-06-18 15:00:00,115.32,45.431000000000004,43.711999999999996,29.28 -2020-06-18 15:15:00,123.12,42.968999999999994,43.711999999999996,29.28 -2020-06-18 15:30:00,122.03,41.169,43.711999999999996,29.28 -2020-06-18 15:45:00,118.04,39.056999999999995,43.711999999999996,29.28 -2020-06-18 16:00:00,118.78,41.851000000000006,45.446000000000005,29.28 -2020-06-18 16:15:00,112.38,41.406000000000006,45.446000000000005,29.28 -2020-06-18 16:30:00,118.25,40.647,45.446000000000005,29.28 -2020-06-18 16:45:00,122.87,37.039,45.446000000000005,29.28 -2020-06-18 17:00:00,115.45,40.086999999999996,48.803000000000004,29.28 -2020-06-18 17:15:00,111.22,40.056,48.803000000000004,29.28 -2020-06-18 17:30:00,107.59,39.183,48.803000000000004,29.28 -2020-06-18 17:45:00,109.13,37.556999999999995,48.803000000000004,29.28 -2020-06-18 18:00:00,114.28,40.154,51.167,29.28 -2020-06-18 18:15:00,108.94,40.298,51.167,29.28 -2020-06-18 18:30:00,104.66,37.966,51.167,29.28 -2020-06-18 18:45:00,108.51,42.067,51.167,29.28 -2020-06-18 19:00:00,107.19,42.733000000000004,52.486000000000004,29.28 -2020-06-18 19:15:00,97.5,41.736999999999995,52.486000000000004,29.28 -2020-06-18 19:30:00,95.56,40.787,52.486000000000004,29.28 -2020-06-18 19:45:00,94.72,40.857,52.486000000000004,29.28 -2020-06-18 20:00:00,92.52,38.503,59.635,29.28 -2020-06-18 20:15:00,92.24,38.47,59.635,29.28 -2020-06-18 20:30:00,92.21,38.916,59.635,29.28 -2020-06-18 20:45:00,92.12,39.663000000000004,59.635,29.28 -2020-06-18 21:00:00,90.57,37.983000000000004,54.353,29.28 -2020-06-18 21:15:00,89.89,39.894,54.353,29.28 -2020-06-18 21:30:00,85.01,40.857,54.353,29.28 -2020-06-18 21:45:00,85.71,42.0,54.353,29.28 -2020-06-18 22:00:00,80.59,39.469,49.431999999999995,29.28 -2020-06-18 22:15:00,80.6,41.437,49.431999999999995,29.28 -2020-06-18 22:30:00,78.43,36.054,49.431999999999995,29.28 -2020-06-18 22:45:00,78.92,32.741,49.431999999999995,29.28 -2020-06-18 23:00:00,74.17,28.486,42.872,29.28 -2020-06-18 23:15:00,75.08,26.943,42.872,29.28 -2020-06-18 23:30:00,71.55,25.811,42.872,29.28 -2020-06-18 23:45:00,74.47,25.078000000000003,42.872,29.28 -2020-06-19 00:00:00,71.17,22.217,39.819,29.28 -2020-06-19 00:15:00,70.01,23.143,39.819,29.28 -2020-06-19 00:30:00,67.45,22.285999999999998,39.819,29.28 -2020-06-19 00:45:00,69.33,22.254,39.819,29.28 -2020-06-19 01:00:00,66.99,21.705,37.797,29.28 -2020-06-19 01:15:00,71.13,20.305,37.797,29.28 -2020-06-19 01:30:00,76.08,19.335,37.797,29.28 -2020-06-19 01:45:00,76.62,18.707,37.797,29.28 -2020-06-19 02:00:00,72.72,19.19,36.905,29.28 -2020-06-19 02:15:00,76.55,17.572,36.905,29.28 -2020-06-19 02:30:00,75.24,20.678,36.905,29.28 -2020-06-19 02:45:00,72.74,20.775,36.905,29.28 -2020-06-19 03:00:00,70.27,23.093000000000004,37.1,29.28 -2020-06-19 03:15:00,72.1,20.705,37.1,29.28 -2020-06-19 03:30:00,71.55,19.752,37.1,29.28 -2020-06-19 03:45:00,73.4,20.027,37.1,29.28 -2020-06-19 04:00:00,75.24,26.611,37.882,29.28 -2020-06-19 04:15:00,77.24,32.875,37.882,29.28 -2020-06-19 04:30:00,79.93,31.11,37.882,29.28 -2020-06-19 04:45:00,81.23,30.662,37.882,29.28 -2020-06-19 05:00:00,90.71,45.931000000000004,40.777,29.28 -2020-06-19 05:15:00,98.92,57.556000000000004,40.777,29.28 -2020-06-19 05:30:00,104.38,49.518,40.777,29.28 -2020-06-19 05:45:00,106.32,45.928999999999995,40.777,29.28 -2020-06-19 06:00:00,107.95,48.331,55.528,29.28 -2020-06-19 06:15:00,110.49,48.743,55.528,29.28 -2020-06-19 06:30:00,109.88,46.794,55.528,29.28 -2020-06-19 06:45:00,113.13,47.756,55.528,29.28 -2020-06-19 07:00:00,124.83,49.074,67.749,29.28 -2020-06-19 07:15:00,124.36,49.885,67.749,29.28 -2020-06-19 07:30:00,123.58,45.167,67.749,29.28 -2020-06-19 07:45:00,117.62,44.401,67.749,29.28 -2020-06-19 08:00:00,115.83,42.183,57.55,29.28 -2020-06-19 08:15:00,118.6,45.393,57.55,29.28 -2020-06-19 08:30:00,123.82,45.49100000000001,57.55,29.28 -2020-06-19 08:45:00,128.81,47.595,57.55,29.28 -2020-06-19 09:00:00,126.71,40.461999999999996,52.588,29.28 -2020-06-19 09:15:00,114.96,41.42100000000001,52.588,29.28 -2020-06-19 09:30:00,122.02,44.286,52.588,29.28 -2020-06-19 09:45:00,127.81,47.428999999999995,52.588,29.28 -2020-06-19 10:00:00,122.67,41.851000000000006,49.772,29.28 -2020-06-19 10:15:00,122.54,43.483000000000004,49.772,29.28 -2020-06-19 10:30:00,122.79,44.187,49.772,29.28 -2020-06-19 10:45:00,121.18,45.756,49.772,29.28 -2020-06-19 11:00:00,122.52,42.196999999999996,49.226000000000006,29.28 -2020-06-19 11:15:00,124.31,42.438,49.226000000000006,29.28 -2020-06-19 11:30:00,125.71,43.64,49.226000000000006,29.28 -2020-06-19 11:45:00,123.32,44.515,49.226000000000006,29.28 -2020-06-19 12:00:00,114.63,41.446999999999996,45.705,29.28 -2020-06-19 12:15:00,122.04,40.604,45.705,29.28 -2020-06-19 12:30:00,117.45,39.614000000000004,45.705,29.28 -2020-06-19 12:45:00,117.38,40.341,45.705,29.28 -2020-06-19 13:00:00,106.5,41.613,43.133,29.28 -2020-06-19 13:15:00,107.87,43.023,43.133,29.28 -2020-06-19 13:30:00,118.64,42.123999999999995,43.133,29.28 -2020-06-19 13:45:00,120.24,41.25,43.133,29.28 -2020-06-19 14:00:00,114.0,42.461999999999996,41.989,29.28 -2020-06-19 14:15:00,101.38,41.568000000000005,41.989,29.28 -2020-06-19 14:30:00,104.98,41.913000000000004,41.989,29.28 -2020-06-19 14:45:00,100.54,41.845,41.989,29.28 -2020-06-19 15:00:00,102.98,43.575,43.728,29.28 -2020-06-19 15:15:00,95.22,40.839,43.728,29.28 -2020-06-19 15:30:00,100.15,38.366,43.728,29.28 -2020-06-19 15:45:00,103.04,37.007,43.728,29.28 -2020-06-19 16:00:00,102.24,38.91,45.93899999999999,29.28 -2020-06-19 16:15:00,97.18,38.972,45.93899999999999,29.28 -2020-06-19 16:30:00,99.11,38.058,45.93899999999999,29.28 -2020-06-19 16:45:00,103.2,33.641999999999996,45.93899999999999,29.28 -2020-06-19 17:00:00,108.34,38.445,50.488,29.28 -2020-06-19 17:15:00,102.87,38.21,50.488,29.28 -2020-06-19 17:30:00,103.68,37.475,50.488,29.28 -2020-06-19 17:45:00,106.28,35.653,50.488,29.28 -2020-06-19 18:00:00,106.78,38.35,52.408,29.28 -2020-06-19 18:15:00,100.21,37.504,52.408,29.28 -2020-06-19 18:30:00,105.97,35.091,52.408,29.28 -2020-06-19 18:45:00,105.56,39.607,52.408,29.28 -2020-06-19 19:00:00,102.94,41.207,52.736000000000004,29.28 -2020-06-19 19:15:00,96.06,40.876999999999995,52.736000000000004,29.28 -2020-06-19 19:30:00,98.92,39.952,52.736000000000004,29.28 -2020-06-19 19:45:00,97.55,38.978,52.736000000000004,29.28 -2020-06-19 20:00:00,95.78,36.455999999999996,59.68,29.28 -2020-06-19 20:15:00,97.37,37.21,59.68,29.28 -2020-06-19 20:30:00,92.83,37.177,59.68,29.28 -2020-06-19 20:45:00,91.07,37.179,59.68,29.28 -2020-06-19 21:00:00,94.6,36.84,54.343999999999994,29.28 -2020-06-19 21:15:00,92.39,40.45,54.343999999999994,29.28 -2020-06-19 21:30:00,85.76,41.243,54.343999999999994,29.28 -2020-06-19 21:45:00,83.91,42.629,54.343999999999994,29.28 -2020-06-19 22:00:00,82.56,40.035,49.672,29.28 -2020-06-19 22:15:00,84.43,41.754,49.672,29.28 -2020-06-19 22:30:00,80.48,41.489,49.672,29.28 -2020-06-19 22:45:00,74.58,39.236,49.672,29.28 -2020-06-19 23:00:00,68.57,36.621,42.065,29.28 -2020-06-19 23:15:00,75.75,33.433,42.065,29.28 -2020-06-19 23:30:00,75.04,30.408,42.065,29.28 -2020-06-19 23:45:00,71.81,29.502,42.065,29.28 -2020-06-20 00:00:00,66.09,23.177,38.829,29.17 -2020-06-20 00:15:00,61.95,23.165,38.829,29.17 -2020-06-20 00:30:00,69.46,21.93,38.829,29.17 -2020-06-20 00:45:00,70.54,21.221,38.829,29.17 -2020-06-20 01:00:00,69.8,21.002,34.63,29.17 -2020-06-20 01:15:00,64.85,20.109,34.63,29.17 -2020-06-20 01:30:00,62.76,18.302,34.63,29.17 -2020-06-20 01:45:00,68.84,18.891,34.63,29.17 -2020-06-20 02:00:00,66.79,18.442,32.465,29.17 -2020-06-20 02:15:00,62.57,16.019000000000002,32.465,29.17 -2020-06-20 02:30:00,62.89,18.262,32.465,29.17 -2020-06-20 02:45:00,62.21,19.153,32.465,29.17 -2020-06-20 03:00:00,63.7,20.147000000000002,31.925,29.17 -2020-06-20 03:15:00,68.4,16.994,31.925,29.17 -2020-06-20 03:30:00,69.28,16.274,31.925,29.17 -2020-06-20 03:45:00,66.18,18.046,31.925,29.17 -2020-06-20 04:00:00,64.28,22.455,31.309,29.17 -2020-06-20 04:15:00,67.18,27.634,31.309,29.17 -2020-06-20 04:30:00,67.69,24.068,31.309,29.17 -2020-06-20 04:45:00,65.96,23.858,31.309,29.17 -2020-06-20 05:00:00,64.27,29.932,30.323,29.17 -2020-06-20 05:15:00,63.38,29.461,30.323,29.17 -2020-06-20 05:30:00,66.59,23.021,30.323,29.17 -2020-06-20 05:45:00,73.18,24.226,30.323,29.17 -2020-06-20 06:00:00,77.68,38.798,31.438000000000002,29.17 -2020-06-20 06:15:00,75.5,47.883,31.438000000000002,29.17 -2020-06-20 06:30:00,73.46,42.826,31.438000000000002,29.17 -2020-06-20 06:45:00,70.8,40.108000000000004,31.438000000000002,29.17 -2020-06-20 07:00:00,78.74,39.754,34.891999999999996,29.17 -2020-06-20 07:15:00,82.01,39.227,34.891999999999996,29.17 -2020-06-20 07:30:00,82.18,36.159,34.891999999999996,29.17 -2020-06-20 07:45:00,82.18,36.577,34.891999999999996,29.17 -2020-06-20 08:00:00,80.59,35.275,39.608000000000004,29.17 -2020-06-20 08:15:00,79.69,38.524,39.608000000000004,29.17 -2020-06-20 08:30:00,85.25,38.698,39.608000000000004,29.17 -2020-06-20 08:45:00,92.26,41.931000000000004,39.608000000000004,29.17 -2020-06-20 09:00:00,94.83,37.946,40.894,29.17 -2020-06-20 09:15:00,91.28,39.433,40.894,29.17 -2020-06-20 09:30:00,90.28,42.839,40.894,29.17 -2020-06-20 09:45:00,90.39,45.566,40.894,29.17 -2020-06-20 10:00:00,96.46,40.611,39.525,29.17 -2020-06-20 10:15:00,102.27,42.614,39.525,29.17 -2020-06-20 10:30:00,103.51,42.993,39.525,29.17 -2020-06-20 10:45:00,99.42,44.306000000000004,39.525,29.17 -2020-06-20 11:00:00,94.12,40.628,36.718,29.17 -2020-06-20 11:15:00,91.99,41.68600000000001,36.718,29.17 -2020-06-20 11:30:00,90.43,43.083999999999996,36.718,29.17 -2020-06-20 11:45:00,91.18,44.558,36.718,29.17 -2020-06-20 12:00:00,88.9,41.86600000000001,35.688,29.17 -2020-06-20 12:15:00,87.4,41.9,35.688,29.17 -2020-06-20 12:30:00,86.31,40.782,35.688,29.17 -2020-06-20 12:45:00,85.13,42.178999999999995,35.688,29.17 -2020-06-20 13:00:00,84.77,42.562,32.858000000000004,29.17 -2020-06-20 13:15:00,85.22,43.309,32.858000000000004,29.17 -2020-06-20 13:30:00,84.09,42.575,32.858000000000004,29.17 -2020-06-20 13:45:00,82.67,40.41,32.858000000000004,29.17 -2020-06-20 14:00:00,81.43,41.786,31.738000000000003,29.17 -2020-06-20 14:15:00,81.85,39.626,31.738000000000003,29.17 -2020-06-20 14:30:00,81.06,39.414,31.738000000000003,29.17 -2020-06-20 14:45:00,82.67,39.839,31.738000000000003,29.17 -2020-06-20 15:00:00,82.96,42.092,34.35,29.17 -2020-06-20 15:15:00,80.52,40.094,34.35,29.17 -2020-06-20 15:30:00,81.22,37.909,34.35,29.17 -2020-06-20 15:45:00,82.66,35.633,34.35,29.17 -2020-06-20 16:00:00,85.43,39.605,37.522,29.17 -2020-06-20 16:15:00,84.06,38.836999999999996,37.522,29.17 -2020-06-20 16:30:00,83.74,38.161,37.522,29.17 -2020-06-20 16:45:00,84.53,33.751999999999995,37.522,29.17 -2020-06-20 17:00:00,86.97,37.381,42.498000000000005,29.17 -2020-06-20 17:15:00,87.28,35.118,42.498000000000005,29.17 -2020-06-20 17:30:00,88.27,34.24,42.498000000000005,29.17 -2020-06-20 17:45:00,87.85,32.866,42.498000000000005,29.17 -2020-06-20 18:00:00,88.24,36.915,44.701,29.17 -2020-06-20 18:15:00,86.6,37.805,44.701,29.17 -2020-06-20 18:30:00,88.94,36.808,44.701,29.17 -2020-06-20 18:45:00,84.9,37.689,44.701,29.17 -2020-06-20 19:00:00,81.63,37.759,45.727,29.17 -2020-06-20 19:15:00,76.92,36.394,45.727,29.17 -2020-06-20 19:30:00,78.37,36.266999999999996,45.727,29.17 -2020-06-20 19:45:00,75.8,37.018,45.727,29.17 -2020-06-20 20:00:00,74.14,35.438,43.391000000000005,29.17 -2020-06-20 20:15:00,71.76,35.691,43.391000000000005,29.17 -2020-06-20 20:30:00,72.4,34.777,43.391000000000005,29.17 -2020-06-20 20:45:00,71.24,36.609,43.391000000000005,29.17 -2020-06-20 21:00:00,70.11,34.961,41.231,29.17 -2020-06-20 21:15:00,68.91,38.260999999999996,41.231,29.17 -2020-06-20 21:30:00,66.55,39.293,41.231,29.17 -2020-06-20 21:45:00,64.82,40.121,41.231,29.17 -2020-06-20 22:00:00,64.11,37.602,40.798,29.17 -2020-06-20 22:15:00,62.69,39.724000000000004,40.798,29.17 -2020-06-20 22:30:00,62.16,39.457,40.798,29.17 -2020-06-20 22:45:00,58.77,37.674,40.798,29.17 -2020-06-20 23:00:00,54.05,34.529,34.402,29.17 -2020-06-20 23:15:00,54.66,31.689,34.402,29.17 -2020-06-20 23:30:00,53.59,30.991,34.402,29.17 -2020-06-20 23:45:00,53.51,30.281999999999996,34.402,29.17 -2020-06-21 00:00:00,51.37,24.479,30.171,29.17 -2020-06-21 00:15:00,51.6,23.31,30.171,29.17 -2020-06-21 00:30:00,51.28,21.910999999999998,30.171,29.17 -2020-06-21 00:45:00,50.78,21.142,30.171,29.17 -2020-06-21 01:00:00,47.84,21.191,27.15,29.17 -2020-06-21 01:15:00,50.1,20.225,27.15,29.17 -2020-06-21 01:30:00,49.76,18.312,27.15,29.17 -2020-06-21 01:45:00,49.33,18.498,27.15,29.17 -2020-06-21 02:00:00,48.37,18.084,25.403000000000002,29.17 -2020-06-21 02:15:00,48.73,16.308,25.403000000000002,29.17 -2020-06-21 02:30:00,48.89,18.782,25.403000000000002,29.17 -2020-06-21 02:45:00,48.92,19.419,25.403000000000002,29.17 -2020-06-21 03:00:00,48.71,21.072,23.386999999999997,29.17 -2020-06-21 03:15:00,49.4,18.149,23.386999999999997,29.17 -2020-06-21 03:30:00,50.71,16.795,23.386999999999997,29.17 -2020-06-21 03:45:00,50.44,17.798,23.386999999999997,29.17 -2020-06-21 04:00:00,49.72,22.112,23.941999999999997,29.17 -2020-06-21 04:15:00,50.86,26.76,23.941999999999997,29.17 -2020-06-21 04:30:00,51.2,24.508000000000003,23.941999999999997,29.17 -2020-06-21 04:45:00,52.17,23.851999999999997,23.941999999999997,29.17 -2020-06-21 05:00:00,52.67,29.799,23.026,29.17 -2020-06-21 05:15:00,53.27,28.311,23.026,29.17 -2020-06-21 05:30:00,54.64,21.53,23.026,29.17 -2020-06-21 05:45:00,56.86,22.523000000000003,23.026,29.17 -2020-06-21 06:00:00,58.47,34.766999999999996,23.223000000000003,29.17 -2020-06-21 06:15:00,59.86,44.56100000000001,23.223000000000003,29.17 -2020-06-21 06:30:00,61.63,38.775,23.223000000000003,29.17 -2020-06-21 06:45:00,63.3,35.046,23.223000000000003,29.17 -2020-06-21 07:00:00,66.04,35.114000000000004,24.968000000000004,29.17 -2020-06-21 07:15:00,66.82,32.942,24.968000000000004,29.17 -2020-06-21 07:30:00,67.37,31.054000000000002,24.968000000000004,29.17 -2020-06-21 07:45:00,67.58,31.428,24.968000000000004,29.17 -2020-06-21 08:00:00,66.37,30.988000000000003,29.131,29.17 -2020-06-21 08:15:00,68.59,35.423,29.131,29.17 -2020-06-21 08:30:00,65.93,36.542,29.131,29.17 -2020-06-21 08:45:00,64.07,39.815,29.131,29.17 -2020-06-21 09:00:00,66.26,35.691,29.904,29.17 -2020-06-21 09:15:00,65.59,36.727,29.904,29.17 -2020-06-21 09:30:00,63.46,40.547,29.904,29.17 -2020-06-21 09:45:00,64.99,44.266000000000005,29.904,29.17 -2020-06-21 10:00:00,69.22,39.995,28.943,29.17 -2020-06-21 10:15:00,70.76,42.17,28.943,29.17 -2020-06-21 10:30:00,72.73,42.803000000000004,28.943,29.17 -2020-06-21 10:45:00,73.48,45.013999999999996,28.943,29.17 -2020-06-21 11:00:00,70.43,41.044,31.682,29.17 -2020-06-21 11:15:00,67.81,41.674,31.682,29.17 -2020-06-21 11:30:00,68.94,43.528999999999996,31.682,29.17 -2020-06-21 11:45:00,70.2,45.287,31.682,29.17 -2020-06-21 12:00:00,72.15,43.645,27.315,29.17 -2020-06-21 12:15:00,71.91,43.158,27.315,29.17 -2020-06-21 12:30:00,70.77,42.159,27.315,29.17 -2020-06-21 12:45:00,71.96,42.878,27.315,29.17 -2020-06-21 13:00:00,67.38,42.913000000000004,23.894000000000002,29.17 -2020-06-21 13:15:00,68.77,43.246,23.894000000000002,29.17 -2020-06-21 13:30:00,67.21,41.412,23.894000000000002,29.17 -2020-06-21 13:45:00,64.71,40.32,23.894000000000002,29.17 -2020-06-21 14:00:00,59.95,42.907,21.148000000000003,29.17 -2020-06-21 14:15:00,61.83,41.231,21.148000000000003,29.17 -2020-06-21 14:30:00,64.05,39.84,21.148000000000003,29.17 -2020-06-21 14:45:00,66.51,39.191,21.148000000000003,29.17 -2020-06-21 15:00:00,65.17,41.54,21.229,29.17 -2020-06-21 15:15:00,65.5,38.769,21.229,29.17 -2020-06-21 15:30:00,67.43,36.427,21.229,29.17 -2020-06-21 15:45:00,68.84,34.457,21.229,29.17 -2020-06-21 16:00:00,69.3,36.935,25.037,29.17 -2020-06-21 16:15:00,70.82,36.345,25.037,29.17 -2020-06-21 16:30:00,76.2,36.694,25.037,29.17 -2020-06-21 16:45:00,75.99,32.335,25.037,29.17 -2020-06-21 17:00:00,79.94,36.341,37.11,29.17 -2020-06-21 17:15:00,79.54,35.556,37.11,29.17 -2020-06-21 17:30:00,79.33,35.486,37.11,29.17 -2020-06-21 17:45:00,79.29,34.625,37.11,29.17 -2020-06-21 18:00:00,82.67,39.243,42.215,29.17 -2020-06-21 18:15:00,79.05,39.774,42.215,29.17 -2020-06-21 18:30:00,78.46,38.423,42.215,29.17 -2020-06-21 18:45:00,78.31,39.51,42.215,29.17 -2020-06-21 19:00:00,79.89,41.713,44.383,29.17 -2020-06-21 19:15:00,78.41,39.275,44.383,29.17 -2020-06-21 19:30:00,76.51,38.885999999999996,44.383,29.17 -2020-06-21 19:45:00,76.7,39.255,44.383,29.17 -2020-06-21 20:00:00,77.36,37.827,43.426,29.17 -2020-06-21 20:15:00,76.94,37.992,43.426,29.17 -2020-06-21 20:30:00,78.36,37.949,43.426,29.17 -2020-06-21 20:45:00,78.33,38.084,43.426,29.17 -2020-06-21 21:00:00,77.45,36.18,42.265,29.17 -2020-06-21 21:15:00,76.37,39.157,42.265,29.17 -2020-06-21 21:30:00,74.61,39.539,42.265,29.17 -2020-06-21 21:45:00,74.02,40.735,42.265,29.17 -2020-06-21 22:00:00,72.94,40.181,42.26,29.17 -2020-06-21 22:15:00,71.13,40.609,42.26,29.17 -2020-06-21 22:30:00,69.5,39.675,42.26,29.17 -2020-06-21 22:45:00,73.05,36.58,42.26,29.17 -2020-06-21 23:00:00,65.27,32.983000000000004,36.609,29.17 -2020-06-21 23:15:00,66.55,31.525,36.609,29.17 -2020-06-21 23:30:00,65.95,30.346,36.609,29.17 -2020-06-21 23:45:00,64.51,29.828000000000003,36.609,29.17 -2020-06-22 00:00:00,62.06,26.113000000000003,34.611,29.28 -2020-06-22 00:15:00,63.43,25.878,34.611,29.28 -2020-06-22 00:30:00,63.24,24.111,34.611,29.28 -2020-06-22 00:45:00,62.85,22.93,34.611,29.28 -2020-06-22 01:00:00,61.06,23.374000000000002,33.552,29.28 -2020-06-22 01:15:00,62.06,22.379,33.552,29.28 -2020-06-22 01:30:00,62.07,20.820999999999998,33.552,29.28 -2020-06-22 01:45:00,62.34,20.910999999999998,33.552,29.28 -2020-06-22 02:00:00,61.38,20.945,32.351,29.28 -2020-06-22 02:15:00,62.36,18.246,32.351,29.28 -2020-06-22 02:30:00,62.99,20.894000000000002,32.351,29.28 -2020-06-22 02:45:00,69.91,21.36,32.351,29.28 -2020-06-22 03:00:00,71.83,23.574,30.793000000000003,29.28 -2020-06-22 03:15:00,74.38,21.448,30.793000000000003,29.28 -2020-06-22 03:30:00,68.44,20.76,30.793000000000003,29.28 -2020-06-22 03:45:00,72.17,21.3,30.793000000000003,29.28 -2020-06-22 04:00:00,73.67,28.855999999999998,31.274,29.28 -2020-06-22 04:15:00,81.48,36.568000000000005,31.274,29.28 -2020-06-22 04:30:00,85.31,33.944,31.274,29.28 -2020-06-22 04:45:00,87.89,33.66,31.274,29.28 -2020-06-22 05:00:00,90.15,47.393,37.75,29.28 -2020-06-22 05:15:00,92.28,57.013000000000005,37.75,29.28 -2020-06-22 05:30:00,98.06,48.794,37.75,29.28 -2020-06-22 05:45:00,104.24,46.501000000000005,37.75,29.28 -2020-06-22 06:00:00,114.07,47.684,55.36,29.28 -2020-06-22 06:15:00,113.59,47.718999999999994,55.36,29.28 -2020-06-22 06:30:00,120.64,46.295,55.36,29.28 -2020-06-22 06:45:00,117.35,48.265,55.36,29.28 -2020-06-22 07:00:00,125.32,48.82,65.87,29.28 -2020-06-22 07:15:00,124.38,48.982,65.87,29.28 -2020-06-22 07:30:00,123.8,46.201,65.87,29.28 -2020-06-22 07:45:00,120.42,46.763999999999996,65.87,29.28 -2020-06-22 08:00:00,127.99,43.946999999999996,55.695,29.28 -2020-06-22 08:15:00,124.81,47.11,55.695,29.28 -2020-06-22 08:30:00,122.97,47.053000000000004,55.695,29.28 -2020-06-22 08:45:00,126.89,50.353,55.695,29.28 -2020-06-22 09:00:00,125.76,45.153999999999996,50.881,29.28 -2020-06-22 09:15:00,128.65,44.243,50.881,29.28 -2020-06-22 09:30:00,129.68,47.093,50.881,29.28 -2020-06-22 09:45:00,120.54,48.583999999999996,50.881,29.28 -2020-06-22 10:00:00,114.39,44.678999999999995,49.138000000000005,29.28 -2020-06-22 10:15:00,122.56,46.699,49.138000000000005,29.28 -2020-06-22 10:30:00,122.96,46.832,49.138000000000005,29.28 -2020-06-22 10:45:00,125.29,47.528,49.138000000000005,29.28 -2020-06-22 11:00:00,120.65,43.623000000000005,49.178000000000004,29.28 -2020-06-22 11:15:00,114.96,44.631,49.178000000000004,29.28 -2020-06-22 11:30:00,112.14,47.236000000000004,49.178000000000004,29.28 -2020-06-22 11:45:00,104.36,49.468999999999994,49.178000000000004,29.28 -2020-06-22 12:00:00,104.38,46.236000000000004,47.698,29.28 -2020-06-22 12:15:00,103.64,45.858000000000004,47.698,29.28 -2020-06-22 12:30:00,103.64,43.793,47.698,29.28 -2020-06-22 12:45:00,102.45,44.586999999999996,47.698,29.28 -2020-06-22 13:00:00,96.63,45.56100000000001,48.104,29.28 -2020-06-22 13:15:00,99.66,44.949,48.104,29.28 -2020-06-22 13:30:00,103.52,43.265,48.104,29.28 -2020-06-22 13:45:00,107.28,43.06399999999999,48.104,29.28 -2020-06-22 14:00:00,103.1,44.723,48.53,29.28 -2020-06-22 14:15:00,98.24,43.601000000000006,48.53,29.28 -2020-06-22 14:30:00,97.96,41.998999999999995,48.53,29.28 -2020-06-22 14:45:00,98.66,43.403,48.53,29.28 -2020-06-22 15:00:00,96.96,45.501000000000005,49.351000000000006,29.28 -2020-06-22 15:15:00,95.96,42.066,49.351000000000006,29.28 -2020-06-22 15:30:00,103.76,40.439,49.351000000000006,29.28 -2020-06-22 15:45:00,101.27,37.955999999999996,49.351000000000006,29.28 -2020-06-22 16:00:00,100.46,41.551,51.44,29.28 -2020-06-22 16:15:00,102.13,40.983999999999995,51.44,29.28 -2020-06-22 16:30:00,101.46,40.643,51.44,29.28 -2020-06-22 16:45:00,101.72,36.227,51.44,29.28 -2020-06-22 17:00:00,104.77,39.12,56.868,29.28 -2020-06-22 17:15:00,101.5,38.658,56.868,29.28 -2020-06-22 17:30:00,104.47,38.171,56.868,29.28 -2020-06-22 17:45:00,106.24,36.828,56.868,29.28 -2020-06-22 18:00:00,107.49,40.416,57.229,29.28 -2020-06-22 18:15:00,104.01,38.998000000000005,57.229,29.28 -2020-06-22 18:30:00,102.52,36.928000000000004,57.229,29.28 -2020-06-22 18:45:00,102.38,41.181000000000004,57.229,29.28 -2020-06-22 19:00:00,100.87,43.022,57.744,29.28 -2020-06-22 19:15:00,100.34,41.828,57.744,29.28 -2020-06-22 19:30:00,96.04,41.091,57.744,29.28 -2020-06-22 19:45:00,97.08,40.805,57.744,29.28 -2020-06-22 20:00:00,93.07,37.99,66.05199999999999,29.28 -2020-06-22 20:15:00,93.95,39.416,66.05199999999999,29.28 -2020-06-22 20:30:00,95.77,39.804,66.05199999999999,29.28 -2020-06-22 20:45:00,99.45,40.259,66.05199999999999,29.28 -2020-06-22 21:00:00,94.07,37.760999999999996,59.396,29.28 -2020-06-22 21:15:00,93.7,41.123999999999995,59.396,29.28 -2020-06-22 21:30:00,90.26,41.853,59.396,29.28 -2020-06-22 21:45:00,88.36,42.81,59.396,29.28 -2020-06-22 22:00:00,84.6,40.099000000000004,53.06,29.28 -2020-06-22 22:15:00,85.31,42.441,53.06,29.28 -2020-06-22 22:30:00,82.83,36.732,53.06,29.28 -2020-06-22 22:45:00,81.6,33.37,53.06,29.28 -2020-06-22 23:00:00,76.8,29.838,46.148,29.28 -2020-06-22 23:15:00,77.46,26.904,46.148,29.28 -2020-06-22 23:30:00,78.9,25.831999999999997,46.148,29.28 -2020-06-22 23:45:00,78.36,24.899,46.148,29.28 -2020-06-23 00:00:00,73.53,23.746,44.625,29.28 -2020-06-23 00:15:00,75.33,24.444000000000003,44.625,29.28 -2020-06-23 00:30:00,74.92,23.34,44.625,29.28 -2020-06-23 00:45:00,74.6,22.901,44.625,29.28 -2020-06-23 01:00:00,73.27,22.811999999999998,41.733000000000004,29.28 -2020-06-23 01:15:00,75.07,21.89,41.733000000000004,29.28 -2020-06-23 01:30:00,73.73,20.209,41.733000000000004,29.28 -2020-06-23 01:45:00,74.21,19.797,41.733000000000004,29.28 -2020-06-23 02:00:00,73.89,19.355999999999998,39.872,29.28 -2020-06-23 02:15:00,73.43,17.804000000000002,39.872,29.28 -2020-06-23 02:30:00,80.85,20.026,39.872,29.28 -2020-06-23 02:45:00,82.26,20.824,39.872,29.28 -2020-06-23 03:00:00,78.93,22.423000000000002,38.711,29.28 -2020-06-23 03:15:00,75.46,21.189,38.711,29.28 -2020-06-23 03:30:00,78.81,20.5,38.711,29.28 -2020-06-23 03:45:00,76.4,19.961,38.711,29.28 -2020-06-23 04:00:00,84.82,26.201,39.823,29.28 -2020-06-23 04:15:00,90.66,33.869,39.823,29.28 -2020-06-23 04:30:00,92.85,31.098000000000003,39.823,29.28 -2020-06-23 04:45:00,92.53,31.357,39.823,29.28 -2020-06-23 05:00:00,96.07,46.823,43.228,29.28 -2020-06-23 05:15:00,98.92,56.934,43.228,29.28 -2020-06-23 05:30:00,106.98,48.886,43.228,29.28 -2020-06-23 05:45:00,113.0,45.909,43.228,29.28 -2020-06-23 06:00:00,118.57,48.091,54.316,29.28 -2020-06-23 06:15:00,120.37,48.391999999999996,54.316,29.28 -2020-06-23 06:30:00,119.5,46.638999999999996,54.316,29.28 -2020-06-23 06:45:00,123.82,47.731,54.316,29.28 -2020-06-23 07:00:00,124.88,48.418,65.758,29.28 -2020-06-23 07:15:00,122.94,48.336000000000006,65.758,29.28 -2020-06-23 07:30:00,120.91,45.54,65.758,29.28 -2020-06-23 07:45:00,125.1,45.174,65.758,29.28 -2020-06-23 08:00:00,122.4,42.302,57.983000000000004,29.28 -2020-06-23 08:15:00,118.74,44.994,57.983000000000004,29.28 -2020-06-23 08:30:00,118.28,45.086000000000006,57.983000000000004,29.28 -2020-06-23 08:45:00,122.7,47.455,57.983000000000004,29.28 -2020-06-23 09:00:00,122.76,42.547,52.653,29.28 -2020-06-23 09:15:00,122.01,41.583999999999996,52.653,29.28 -2020-06-23 09:30:00,120.53,45.15,52.653,29.28 -2020-06-23 09:45:00,118.95,47.977,52.653,29.28 -2020-06-23 10:00:00,130.1,42.687,51.408,29.28 -2020-06-23 10:15:00,123.77,44.523,51.408,29.28 -2020-06-23 10:30:00,123.38,44.691,51.408,29.28 -2020-06-23 10:45:00,116.88,46.407,51.408,29.28 -2020-06-23 11:00:00,116.32,42.581,51.913000000000004,29.28 -2020-06-23 11:15:00,111.31,43.983000000000004,51.913000000000004,29.28 -2020-06-23 11:30:00,115.49,45.371,51.913000000000004,29.28 -2020-06-23 11:45:00,107.36,47.188,51.913000000000004,29.28 -2020-06-23 12:00:00,107.78,43.729,49.508,29.28 -2020-06-23 12:15:00,112.97,43.67,49.508,29.28 -2020-06-23 12:30:00,115.46,42.479,49.508,29.28 -2020-06-23 12:45:00,111.06,43.953,49.508,29.28 -2020-06-23 13:00:00,110.37,44.533,50.007,29.28 -2020-06-23 13:15:00,106.59,45.705,50.007,29.28 -2020-06-23 13:30:00,108.56,44.053999999999995,50.007,29.28 -2020-06-23 13:45:00,117.29,42.903,50.007,29.28 -2020-06-23 14:00:00,107.17,45.04,49.778999999999996,29.28 -2020-06-23 14:15:00,109.31,43.724,49.778999999999996,29.28 -2020-06-23 14:30:00,107.16,42.458999999999996,49.778999999999996,29.28 -2020-06-23 14:45:00,97.6,43.075,49.778999999999996,29.28 -2020-06-23 15:00:00,108.88,45.023,51.559,29.28 -2020-06-23 15:15:00,109.4,42.522,51.559,29.28 -2020-06-23 15:30:00,106.06,40.688,51.559,29.28 -2020-06-23 15:45:00,108.71,38.524,51.559,29.28 -2020-06-23 16:00:00,110.94,41.452,53.531000000000006,29.28 -2020-06-23 16:15:00,110.74,40.99100000000001,53.531000000000006,29.28 -2020-06-23 16:30:00,113.66,40.326,53.531000000000006,29.28 -2020-06-23 16:45:00,109.97,36.621,53.531000000000006,29.28 -2020-06-23 17:00:00,116.03,39.773,59.497,29.28 -2020-06-23 17:15:00,115.14,39.727,59.497,29.28 -2020-06-23 17:30:00,117.22,38.826,59.497,29.28 -2020-06-23 17:45:00,112.66,37.13,59.497,29.28 -2020-06-23 18:00:00,114.14,39.775,59.861999999999995,29.28 -2020-06-23 18:15:00,114.54,39.8,59.861999999999995,29.28 -2020-06-23 18:30:00,116.69,37.45,59.861999999999995,29.28 -2020-06-23 18:45:00,112.08,41.543,59.861999999999995,29.28 -2020-06-23 19:00:00,104.05,42.224,60.989,29.28 -2020-06-23 19:15:00,100.72,41.187,60.989,29.28 -2020-06-23 19:30:00,98.01,40.196999999999996,60.989,29.28 -2020-06-23 19:45:00,97.06,40.241,60.989,29.28 -2020-06-23 20:00:00,96.0,37.812,68.35600000000001,29.28 -2020-06-23 20:15:00,96.1,37.766,68.35600000000001,29.28 -2020-06-23 20:30:00,95.0,38.244,68.35600000000001,29.28 -2020-06-23 20:45:00,98.48,39.116,68.35600000000001,29.28 -2020-06-23 21:00:00,95.1,37.458,59.251000000000005,29.28 -2020-06-23 21:15:00,93.78,39.403,59.251000000000005,29.28 -2020-06-23 21:30:00,89.52,40.253,59.251000000000005,29.28 -2020-06-23 21:45:00,88.7,41.42,59.251000000000005,29.28 -2020-06-23 22:00:00,83.01,38.971,54.736999999999995,29.28 -2020-06-23 22:15:00,84.33,40.971000000000004,54.736999999999995,29.28 -2020-06-23 22:30:00,82.74,35.55,54.736999999999995,29.28 -2020-06-23 22:45:00,81.15,32.186,54.736999999999995,29.28 -2020-06-23 23:00:00,78.29,27.874000000000002,46.806999999999995,29.28 -2020-06-23 23:15:00,77.68,26.494,46.806999999999995,29.28 -2020-06-23 23:30:00,76.77,25.414,46.806999999999995,29.28 -2020-06-23 23:45:00,75.36,24.633000000000003,46.806999999999995,29.28 -2020-06-24 00:00:00,72.75,23.675,43.824,29.28 -2020-06-24 00:15:00,73.59,24.374000000000002,43.824,29.28 -2020-06-24 00:30:00,73.6,23.273000000000003,43.824,29.28 -2020-06-24 00:45:00,73.92,22.840999999999998,43.824,29.28 -2020-06-24 01:00:00,72.73,22.764,39.86,29.28 -2020-06-24 01:15:00,73.28,21.828000000000003,39.86,29.28 -2020-06-24 01:30:00,72.51,20.145,39.86,29.28 -2020-06-24 01:45:00,73.33,19.726,39.86,29.28 -2020-06-24 02:00:00,77.46,19.287,37.931999999999995,29.28 -2020-06-24 02:15:00,82.15,17.739,37.931999999999995,29.28 -2020-06-24 02:30:00,81.13,19.952,37.931999999999995,29.28 -2020-06-24 02:45:00,74.48,20.756999999999998,37.931999999999995,29.28 -2020-06-24 03:00:00,77.13,22.351999999999997,37.579,29.28 -2020-06-24 03:15:00,76.09,21.122,37.579,29.28 -2020-06-24 03:30:00,76.54,20.439,37.579,29.28 -2020-06-24 03:45:00,76.21,19.92,37.579,29.28 -2020-06-24 04:00:00,84.58,26.114,37.931999999999995,29.28 -2020-06-24 04:15:00,88.31,33.742,37.931999999999995,29.28 -2020-06-24 04:30:00,92.93,30.961,37.931999999999995,29.28 -2020-06-24 04:45:00,91.92,31.218000000000004,37.931999999999995,29.28 -2020-06-24 05:00:00,95.34,46.599,40.942,29.28 -2020-06-24 05:15:00,98.24,56.599,40.942,29.28 -2020-06-24 05:30:00,101.16,48.613,40.942,29.28 -2020-06-24 05:45:00,103.36,45.676,40.942,29.28 -2020-06-24 06:00:00,111.68,47.864,56.516999999999996,29.28 -2020-06-24 06:15:00,116.87,48.153999999999996,56.516999999999996,29.28 -2020-06-24 06:30:00,116.87,46.42100000000001,56.516999999999996,29.28 -2020-06-24 06:45:00,117.03,47.542,56.516999999999996,29.28 -2020-06-24 07:00:00,116.69,48.217,71.707,29.28 -2020-06-24 07:15:00,116.48,48.155,71.707,29.28 -2020-06-24 07:30:00,116.84,45.356,71.707,29.28 -2020-06-24 07:45:00,112.07,45.023,71.707,29.28 -2020-06-24 08:00:00,109.37,42.161,61.17,29.28 -2020-06-24 08:15:00,108.4,44.878,61.17,29.28 -2020-06-24 08:30:00,114.26,44.963,61.17,29.28 -2020-06-24 08:45:00,114.72,47.333,61.17,29.28 -2020-06-24 09:00:00,112.86,42.42,57.282,29.28 -2020-06-24 09:15:00,108.55,41.458999999999996,57.282,29.28 -2020-06-24 09:30:00,118.09,45.023,57.282,29.28 -2020-06-24 09:45:00,123.97,47.863,57.282,29.28 -2020-06-24 10:00:00,122.12,42.582,54.026,29.28 -2020-06-24 10:15:00,117.01,44.424,54.026,29.28 -2020-06-24 10:30:00,123.16,44.593,54.026,29.28 -2020-06-24 10:45:00,121.18,46.31399999999999,54.026,29.28 -2020-06-24 11:00:00,123.52,42.483999999999995,54.277,29.28 -2020-06-24 11:15:00,117.69,43.891000000000005,54.277,29.28 -2020-06-24 11:30:00,115.43,45.269,54.277,29.28 -2020-06-24 11:45:00,119.14,47.083999999999996,54.277,29.28 -2020-06-24 12:00:00,124.99,43.653,52.552,29.28 -2020-06-24 12:15:00,127.32,43.593,52.552,29.28 -2020-06-24 12:30:00,126.8,42.385,52.552,29.28 -2020-06-24 12:45:00,117.73,43.86,52.552,29.28 -2020-06-24 13:00:00,101.49,44.428999999999995,52.111999999999995,29.28 -2020-06-24 13:15:00,105.29,45.601000000000006,52.111999999999995,29.28 -2020-06-24 13:30:00,116.19,43.958,52.111999999999995,29.28 -2020-06-24 13:45:00,110.54,42.81100000000001,52.111999999999995,29.28 -2020-06-24 14:00:00,99.9,44.958999999999996,52.066,29.28 -2020-06-24 14:15:00,102.05,43.643,52.066,29.28 -2020-06-24 14:30:00,110.45,42.358000000000004,52.066,29.28 -2020-06-24 14:45:00,109.15,42.979,52.066,29.28 -2020-06-24 15:00:00,108.94,44.952,52.523999999999994,29.28 -2020-06-24 15:15:00,101.7,42.445,52.523999999999994,29.28 -2020-06-24 15:30:00,107.34,40.605,52.523999999999994,29.28 -2020-06-24 15:45:00,106.1,38.431999999999995,52.523999999999994,29.28 -2020-06-24 16:00:00,104.67,41.382,54.101000000000006,29.28 -2020-06-24 16:15:00,105.58,40.921,54.101000000000006,29.28 -2020-06-24 16:30:00,112.42,40.273,54.101000000000006,29.28 -2020-06-24 16:45:00,110.04,36.552,54.101000000000006,29.28 -2020-06-24 17:00:00,111.19,39.722,58.155,29.28 -2020-06-24 17:15:00,108.5,39.675,58.155,29.28 -2020-06-24 17:30:00,112.41,38.77,58.155,29.28 -2020-06-24 17:45:00,117.25,37.063,58.155,29.28 -2020-06-24 18:00:00,116.0,39.716,60.205,29.28 -2020-06-24 18:15:00,111.3,39.717,60.205,29.28 -2020-06-24 18:30:00,108.34,37.365,60.205,29.28 -2020-06-24 18:45:00,107.8,41.457,60.205,29.28 -2020-06-24 19:00:00,103.84,42.141000000000005,61.568999999999996,29.28 -2020-06-24 19:15:00,101.09,41.096000000000004,61.568999999999996,29.28 -2020-06-24 19:30:00,102.04,40.098,61.568999999999996,29.28 -2020-06-24 19:45:00,97.68,40.138000000000005,61.568999999999996,29.28 -2020-06-24 20:00:00,95.61,37.694,68.145,29.28 -2020-06-24 20:15:00,94.76,37.645,68.145,29.28 -2020-06-24 20:30:00,94.42,38.129,68.145,29.28 -2020-06-24 20:45:00,94.83,39.023,68.145,29.28 -2020-06-24 21:00:00,96.89,37.37,59.696000000000005,29.28 -2020-06-24 21:15:00,94.06,39.32,59.696000000000005,29.28 -2020-06-24 21:30:00,90.13,40.148,59.696000000000005,29.28 -2020-06-24 21:45:00,88.79,41.318000000000005,59.696000000000005,29.28 -2020-06-24 22:00:00,84.68,38.884,54.861999999999995,29.28 -2020-06-24 22:15:00,87.11,40.889,54.861999999999995,29.28 -2020-06-24 22:30:00,84.09,35.459,54.861999999999995,29.28 -2020-06-24 22:45:00,84.16,32.086,54.861999999999995,29.28 -2020-06-24 23:00:00,79.54,27.765,45.568000000000005,29.28 -2020-06-24 23:15:00,76.73,26.414,45.568000000000005,29.28 -2020-06-24 23:30:00,75.86,25.346,45.568000000000005,29.28 -2020-06-24 23:45:00,78.52,24.555999999999997,45.568000000000005,29.28 -2020-06-25 00:00:00,75.55,23.608,40.181,29.28 -2020-06-25 00:15:00,75.77,24.307,40.181,29.28 -2020-06-25 00:30:00,72.44,23.21,40.181,29.28 -2020-06-25 00:45:00,75.39,22.784000000000002,40.181,29.28 -2020-06-25 01:00:00,72.29,22.72,38.296,29.28 -2020-06-25 01:15:00,74.01,21.771,38.296,29.28 -2020-06-25 01:30:00,72.67,20.085,38.296,29.28 -2020-06-25 01:45:00,72.74,19.66,38.296,29.28 -2020-06-25 02:00:00,73.25,19.224,36.575,29.28 -2020-06-25 02:15:00,74.48,17.678,36.575,29.28 -2020-06-25 02:30:00,74.61,19.884,36.575,29.28 -2020-06-25 02:45:00,74.51,20.693,36.575,29.28 -2020-06-25 03:00:00,75.05,22.285,36.394,29.28 -2020-06-25 03:15:00,76.2,21.059,36.394,29.28 -2020-06-25 03:30:00,83.01,20.384,36.394,29.28 -2020-06-25 03:45:00,85.84,19.883,36.394,29.28 -2020-06-25 04:00:00,80.84,26.031999999999996,37.207,29.28 -2020-06-25 04:15:00,81.65,33.622,37.207,29.28 -2020-06-25 04:30:00,84.58,30.829,37.207,29.28 -2020-06-25 04:45:00,88.67,31.086,37.207,29.28 -2020-06-25 05:00:00,97.63,46.383,40.713,29.28 -2020-06-25 05:15:00,98.52,56.276,40.713,29.28 -2020-06-25 05:30:00,107.02,48.352,40.713,29.28 -2020-06-25 05:45:00,107.47,45.452,40.713,29.28 -2020-06-25 06:00:00,115.01,47.646,50.952,29.28 -2020-06-25 06:15:00,111.77,47.926,50.952,29.28 -2020-06-25 06:30:00,117.15,46.214,50.952,29.28 -2020-06-25 06:45:00,118.68,47.363,50.952,29.28 -2020-06-25 07:00:00,115.38,48.026,64.88,29.28 -2020-06-25 07:15:00,114.08,47.983000000000004,64.88,29.28 -2020-06-25 07:30:00,113.71,45.181999999999995,64.88,29.28 -2020-06-25 07:45:00,114.99,44.88,64.88,29.28 -2020-06-25 08:00:00,113.05,42.027,55.133,29.28 -2020-06-25 08:15:00,112.54,44.771,55.133,29.28 -2020-06-25 08:30:00,110.54,44.848,55.133,29.28 -2020-06-25 08:45:00,116.65,47.22,55.133,29.28 -2020-06-25 09:00:00,118.67,42.302,48.912,29.28 -2020-06-25 09:15:00,119.01,41.343,48.912,29.28 -2020-06-25 09:30:00,116.77,44.903999999999996,48.912,29.28 -2020-06-25 09:45:00,122.25,47.756,48.912,29.28 -2020-06-25 10:00:00,123.53,42.483000000000004,45.968999999999994,29.28 -2020-06-25 10:15:00,129.33,44.333,45.968999999999994,29.28 -2020-06-25 10:30:00,122.87,44.501000000000005,45.968999999999994,29.28 -2020-06-25 10:45:00,119.98,46.227,45.968999999999994,29.28 -2020-06-25 11:00:00,120.33,42.395,44.067,29.28 -2020-06-25 11:15:00,118.71,43.806000000000004,44.067,29.28 -2020-06-25 11:30:00,112.17,45.172,44.067,29.28 -2020-06-25 11:45:00,113.06,46.986999999999995,44.067,29.28 -2020-06-25 12:00:00,123.41,43.581,41.501000000000005,29.28 -2020-06-25 12:15:00,127.98,43.521,41.501000000000005,29.28 -2020-06-25 12:30:00,129.51,42.29600000000001,41.501000000000005,29.28 -2020-06-25 12:45:00,125.72,43.772,41.501000000000005,29.28 -2020-06-25 13:00:00,119.23,44.33,41.117,29.28 -2020-06-25 13:15:00,120.12,45.5,41.117,29.28 -2020-06-25 13:30:00,125.64,43.867,41.117,29.28 -2020-06-25 13:45:00,128.35,42.726000000000006,41.117,29.28 -2020-06-25 14:00:00,128.81,44.882,41.492,29.28 -2020-06-25 14:15:00,125.95,43.567,41.492,29.28 -2020-06-25 14:30:00,120.48,42.263999999999996,41.492,29.28 -2020-06-25 14:45:00,120.88,42.89,41.492,29.28 -2020-06-25 15:00:00,123.63,44.88399999999999,43.711999999999996,29.28 -2020-06-25 15:15:00,121.05,42.37,43.711999999999996,29.28 -2020-06-25 15:30:00,122.08,40.525999999999996,43.711999999999996,29.28 -2020-06-25 15:45:00,120.83,38.344,43.711999999999996,29.28 -2020-06-25 16:00:00,127.83,41.317,45.446000000000005,29.28 -2020-06-25 16:15:00,122.67,40.853,45.446000000000005,29.28 -2020-06-25 16:30:00,122.29,40.224000000000004,45.446000000000005,29.28 -2020-06-25 16:45:00,126.06,36.488,45.446000000000005,29.28 -2020-06-25 17:00:00,123.94,39.675,48.803000000000004,29.28 -2020-06-25 17:15:00,123.31,39.628,48.803000000000004,29.28 -2020-06-25 17:30:00,120.34,38.719,48.803000000000004,29.28 -2020-06-25 17:45:00,114.21,37.001,48.803000000000004,29.28 -2020-06-25 18:00:00,118.99,39.661,51.167,29.28 -2020-06-25 18:15:00,118.56,39.641,51.167,29.28 -2020-06-25 18:30:00,119.87,37.286,51.167,29.28 -2020-06-25 18:45:00,112.47,41.376000000000005,51.167,29.28 -2020-06-25 19:00:00,107.94,42.06399999999999,52.486000000000004,29.28 -2020-06-25 19:15:00,104.55,41.011,52.486000000000004,29.28 -2020-06-25 19:30:00,102.42,40.006,52.486000000000004,29.28 -2020-06-25 19:45:00,103.35,40.04,52.486000000000004,29.28 -2020-06-25 20:00:00,97.55,37.582,59.635,29.28 -2020-06-25 20:15:00,97.58,37.531,59.635,29.28 -2020-06-25 20:30:00,98.76,38.021,59.635,29.28 -2020-06-25 20:45:00,98.12,38.935,59.635,29.28 -2020-06-25 21:00:00,96.92,37.287,54.353,29.28 -2020-06-25 21:15:00,92.89,39.243,54.353,29.28 -2020-06-25 21:30:00,90.17,40.049,54.353,29.28 -2020-06-25 21:45:00,88.89,41.221000000000004,54.353,29.28 -2020-06-25 22:00:00,85.17,38.8,49.431999999999995,29.28 -2020-06-25 22:15:00,84.56,40.81,49.431999999999995,29.28 -2020-06-25 22:30:00,81.54,35.371,49.431999999999995,29.28 -2020-06-25 22:45:00,80.23,31.987,49.431999999999995,29.28 -2020-06-25 23:00:00,78.24,27.659000000000002,42.872,29.28 -2020-06-25 23:15:00,77.2,26.339000000000002,42.872,29.28 -2020-06-25 23:30:00,76.09,25.281999999999996,42.872,29.28 -2020-06-25 23:45:00,74.95,24.483,42.872,29.28 -2020-06-26 00:00:00,73.91,21.691,39.819,29.28 -2020-06-26 00:15:00,73.66,22.618000000000002,39.819,29.28 -2020-06-26 00:30:00,73.5,21.787,39.819,29.28 -2020-06-26 00:45:00,73.41,21.796999999999997,39.819,29.28 -2020-06-26 01:00:00,72.61,21.344,37.797,29.28 -2020-06-26 01:15:00,73.69,19.842,37.797,29.28 -2020-06-26 01:30:00,73.2,18.852,37.797,29.28 -2020-06-26 01:45:00,73.02,18.176,37.797,29.28 -2020-06-26 02:00:00,77.1,18.678,36.905,29.28 -2020-06-26 02:15:00,81.05,17.077,36.905,29.28 -2020-06-26 02:30:00,79.47,20.13,36.905,29.28 -2020-06-26 02:45:00,73.65,20.267,36.905,29.28 -2020-06-26 03:00:00,76.43,22.564,37.1,29.28 -2020-06-26 03:15:00,75.3,20.2,37.1,29.28 -2020-06-26 03:30:00,76.44,19.292,37.1,29.28 -2020-06-26 03:45:00,77.02,19.705,37.1,29.28 -2020-06-26 04:00:00,77.12,25.962,37.882,29.28 -2020-06-26 04:15:00,84.19,31.944000000000003,37.882,29.28 -2020-06-26 04:30:00,91.07,30.104,37.882,29.28 -2020-06-26 04:45:00,88.15,29.641,37.882,29.28 -2020-06-26 05:00:00,93.96,44.298,40.777,29.28 -2020-06-26 05:15:00,96.44,55.133,40.777,29.28 -2020-06-26 05:30:00,99.55,47.538000000000004,40.777,29.28 -2020-06-26 05:45:00,102.21,44.23,40.777,29.28 -2020-06-26 06:00:00,105.49,46.681999999999995,55.528,29.28 -2020-06-26 06:15:00,106.56,47.016000000000005,55.528,29.28 -2020-06-26 06:30:00,111.38,45.214,55.528,29.28 -2020-06-26 06:45:00,113.94,46.373000000000005,55.528,29.28 -2020-06-26 07:00:00,119.24,47.61,67.749,29.28 -2020-06-26 07:15:00,120.18,48.556999999999995,67.749,29.28 -2020-06-26 07:30:00,118.22,43.81100000000001,67.749,29.28 -2020-06-26 07:45:00,111.2,43.27,67.749,29.28 -2020-06-26 08:00:00,111.58,41.121,57.55,29.28 -2020-06-26 08:15:00,114.37,44.523999999999994,57.55,29.28 -2020-06-26 08:30:00,114.99,44.568000000000005,57.55,29.28 -2020-06-26 08:45:00,113.74,46.685,57.55,29.28 -2020-06-26 09:00:00,109.16,39.516999999999996,52.588,29.28 -2020-06-26 09:15:00,113.26,40.493,52.588,29.28 -2020-06-26 09:30:00,112.71,43.34,52.588,29.28 -2020-06-26 09:45:00,110.41,46.576,52.588,29.28 -2020-06-26 10:00:00,105.72,41.063,49.772,29.28 -2020-06-26 10:15:00,112.85,42.751000000000005,49.772,29.28 -2020-06-26 10:30:00,112.6,43.458,49.772,29.28 -2020-06-26 10:45:00,111.95,45.062,49.772,29.28 -2020-06-26 11:00:00,106.15,41.48,49.226000000000006,29.28 -2020-06-26 11:15:00,102.95,41.75899999999999,49.226000000000006,29.28 -2020-06-26 11:30:00,108.99,42.881,49.226000000000006,29.28 -2020-06-26 11:45:00,107.29,43.751000000000005,49.226000000000006,29.28 -2020-06-26 12:00:00,102.93,40.873000000000005,45.705,29.28 -2020-06-26 12:15:00,97.81,40.031,45.705,29.28 -2020-06-26 12:30:00,99.61,38.917,45.705,29.28 -2020-06-26 12:45:00,101.18,39.65,45.705,29.28 -2020-06-26 13:00:00,99.27,40.851,43.133,29.28 -2020-06-26 13:15:00,96.97,42.258,43.133,29.28 -2020-06-26 13:30:00,91.8,41.42,43.133,29.28 -2020-06-26 13:45:00,90.58,40.583,43.133,29.28 -2020-06-26 14:00:00,89.92,41.871,41.989,29.28 -2020-06-26 14:15:00,91.04,40.973,41.989,29.28 -2020-06-26 14:30:00,96.86,41.178999999999995,41.989,29.28 -2020-06-26 14:45:00,96.54,41.148999999999994,41.989,29.28 -2020-06-26 15:00:00,96.12,43.053999999999995,43.728,29.28 -2020-06-26 15:15:00,92.58,40.266,43.728,29.28 -2020-06-26 15:30:00,92.76,37.751999999999995,43.728,29.28 -2020-06-26 15:45:00,99.14,36.326,43.728,29.28 -2020-06-26 16:00:00,102.55,38.402,45.93899999999999,29.28 -2020-06-26 16:15:00,102.02,38.446,45.93899999999999,29.28 -2020-06-26 16:30:00,97.83,37.661,45.93899999999999,29.28 -2020-06-26 16:45:00,104.14,33.126,45.93899999999999,29.28 -2020-06-26 17:00:00,107.5,38.062,50.488,29.28 -2020-06-26 17:15:00,108.79,37.815,50.488,29.28 -2020-06-26 17:30:00,102.7,37.045,50.488,29.28 -2020-06-26 17:45:00,108.93,35.137,50.488,29.28 -2020-06-26 18:00:00,110.72,37.895,52.408,29.28 -2020-06-26 18:15:00,106.92,36.888000000000005,52.408,29.28 -2020-06-26 18:30:00,100.88,34.454,52.408,29.28 -2020-06-26 18:45:00,99.23,38.957,52.408,29.28 -2020-06-26 19:00:00,92.97,40.580999999999996,52.736000000000004,29.28 -2020-06-26 19:15:00,93.69,40.194,52.736000000000004,29.28 -2020-06-26 19:30:00,98.71,39.216,52.736000000000004,29.28 -2020-06-26 19:45:00,97.1,38.205,52.736000000000004,29.28 -2020-06-26 20:00:00,93.03,35.582,59.68,29.28 -2020-06-26 20:15:00,87.14,36.32,59.68,29.28 -2020-06-26 20:30:00,89.87,36.326,59.68,29.28 -2020-06-26 20:45:00,87.72,36.488,59.68,29.28 -2020-06-26 21:00:00,86.85,36.183,54.343999999999994,29.28 -2020-06-26 21:15:00,86.7,39.836,54.343999999999994,29.28 -2020-06-26 21:30:00,87.8,40.472,54.343999999999994,29.28 -2020-06-26 21:45:00,87.75,41.882,54.343999999999994,29.28 -2020-06-26 22:00:00,84.36,39.395,49.672,29.28 -2020-06-26 22:15:00,81.7,41.153,49.672,29.28 -2020-06-26 22:30:00,76.68,40.829,49.672,29.28 -2020-06-26 22:45:00,74.71,38.507,49.672,29.28 -2020-06-26 23:00:00,71.08,35.825,42.065,29.28 -2020-06-26 23:15:00,71.04,32.855,42.065,29.28 -2020-06-26 23:30:00,66.37,29.904,42.065,29.28 -2020-06-26 23:45:00,69.55,28.935,42.065,29.28 -2020-06-27 00:00:00,65.06,22.680999999999997,38.829,29.17 -2020-06-27 00:15:00,66.26,22.67,38.829,29.17 -2020-06-27 00:30:00,67.1,21.462,38.829,29.17 -2020-06-27 00:45:00,66.18,20.795,38.829,29.17 -2020-06-27 01:00:00,63.32,20.668000000000003,34.63,29.17 -2020-06-27 01:15:00,71.54,19.678,34.63,29.17 -2020-06-27 01:30:00,69.12,17.852,34.63,29.17 -2020-06-27 01:45:00,70.08,18.394000000000002,34.63,29.17 -2020-06-27 02:00:00,62.8,17.964000000000002,32.465,29.17 -2020-06-27 02:15:00,62.11,15.561,32.465,29.17 -2020-06-27 02:30:00,65.37,17.749000000000002,32.465,29.17 -2020-06-27 02:45:00,68.43,18.676,32.465,29.17 -2020-06-27 03:00:00,68.92,19.649,31.925,29.17 -2020-06-27 03:15:00,68.83,16.522000000000002,31.925,29.17 -2020-06-27 03:30:00,65.01,15.847999999999999,31.925,29.17 -2020-06-27 03:45:00,60.82,17.756,31.925,29.17 -2020-06-27 04:00:00,61.0,21.845,31.309,29.17 -2020-06-27 04:15:00,68.68,26.747,31.309,29.17 -2020-06-27 04:30:00,68.89,23.107,31.309,29.17 -2020-06-27 04:45:00,67.89,22.884,31.309,29.17 -2020-06-27 05:00:00,63.25,28.361,30.323,29.17 -2020-06-27 05:15:00,62.79,27.12,30.323,29.17 -2020-06-27 05:30:00,67.71,21.116,30.323,29.17 -2020-06-27 05:45:00,71.77,22.594,30.323,29.17 -2020-06-27 06:00:00,75.97,37.208,31.438000000000002,29.17 -2020-06-27 06:15:00,71.83,46.221000000000004,31.438000000000002,29.17 -2020-06-27 06:30:00,70.47,41.309,31.438000000000002,29.17 -2020-06-27 06:45:00,72.75,38.786,31.438000000000002,29.17 -2020-06-27 07:00:00,76.92,38.353,34.891999999999996,29.17 -2020-06-27 07:15:00,77.53,37.963,34.891999999999996,29.17 -2020-06-27 07:30:00,75.96,34.871,34.891999999999996,29.17 -2020-06-27 07:45:00,74.03,35.513000000000005,34.891999999999996,29.17 -2020-06-27 08:00:00,75.68,34.279,39.608000000000004,29.17 -2020-06-27 08:15:00,76.83,37.715,39.608000000000004,29.17 -2020-06-27 08:30:00,81.14,37.835,39.608000000000004,29.17 -2020-06-27 08:45:00,76.55,41.08,39.608000000000004,29.17 -2020-06-27 09:00:00,72.19,37.061,40.894,29.17 -2020-06-27 09:15:00,77.15,38.563,40.894,29.17 -2020-06-27 09:30:00,77.14,41.949,40.894,29.17 -2020-06-27 09:45:00,75.67,44.763999999999996,40.894,29.17 -2020-06-27 10:00:00,75.84,39.874,39.525,29.17 -2020-06-27 10:15:00,73.35,41.927,39.525,29.17 -2020-06-27 10:30:00,70.92,42.309,39.525,29.17 -2020-06-27 10:45:00,72.03,43.653999999999996,39.525,29.17 -2020-06-27 11:00:00,67.93,39.957,36.718,29.17 -2020-06-27 11:15:00,67.39,41.049,36.718,29.17 -2020-06-27 11:30:00,68.26,42.368,36.718,29.17 -2020-06-27 11:45:00,65.04,43.833,36.718,29.17 -2020-06-27 12:00:00,61.59,41.327,35.688,29.17 -2020-06-27 12:15:00,61.46,41.361999999999995,35.688,29.17 -2020-06-27 12:30:00,59.79,40.125,35.688,29.17 -2020-06-27 12:45:00,61.07,41.525,35.688,29.17 -2020-06-27 13:00:00,56.5,41.835,32.858000000000004,29.17 -2020-06-27 13:15:00,57.59,42.577,32.858000000000004,29.17 -2020-06-27 13:30:00,57.29,41.902,32.858000000000004,29.17 -2020-06-27 13:45:00,57.27,39.775999999999996,32.858000000000004,29.17 -2020-06-27 14:00:00,56.82,41.222,31.738000000000003,29.17 -2020-06-27 14:15:00,57.12,39.06,31.738000000000003,29.17 -2020-06-27 14:30:00,57.83,38.715,31.738000000000003,29.17 -2020-06-27 14:45:00,60.56,39.176,31.738000000000003,29.17 -2020-06-27 15:00:00,59.67,41.595,34.35,29.17 -2020-06-27 15:15:00,59.31,39.548,34.35,29.17 -2020-06-27 15:30:00,60.2,37.325,34.35,29.17 -2020-06-27 15:45:00,61.46,34.984,34.35,29.17 -2020-06-27 16:00:00,62.44,39.122,37.522,29.17 -2020-06-27 16:15:00,63.64,38.339,37.522,29.17 -2020-06-27 16:30:00,66.31,37.792,37.522,29.17 -2020-06-27 16:45:00,67.86,33.27,37.522,29.17 -2020-06-27 17:00:00,70.65,37.027,42.498000000000005,29.17 -2020-06-27 17:15:00,71.24,34.755,42.498000000000005,29.17 -2020-06-27 17:30:00,73.98,33.844,42.498000000000005,29.17 -2020-06-27 17:45:00,75.04,32.39,42.498000000000005,29.17 -2020-06-27 18:00:00,77.44,36.498000000000005,44.701,29.17 -2020-06-27 18:15:00,76.21,37.229,44.701,29.17 -2020-06-27 18:30:00,76.3,36.213,44.701,29.17 -2020-06-27 18:45:00,76.3,37.082,44.701,29.17 -2020-06-27 19:00:00,76.28,37.175,45.727,29.17 -2020-06-27 19:15:00,73.26,35.755,45.727,29.17 -2020-06-27 19:30:00,72.4,35.575,45.727,29.17 -2020-06-27 19:45:00,70.94,36.29,45.727,29.17 -2020-06-27 20:00:00,71.82,34.611,43.391000000000005,29.17 -2020-06-27 20:15:00,71.23,34.848,43.391000000000005,29.17 -2020-06-27 20:30:00,71.26,33.97,43.391000000000005,29.17 -2020-06-27 20:45:00,72.04,35.955999999999996,43.391000000000005,29.17 -2020-06-27 21:00:00,74.06,34.341,41.231,29.17 -2020-06-27 21:15:00,74.57,37.683,41.231,29.17 -2020-06-27 21:30:00,70.48,38.56,41.231,29.17 -2020-06-27 21:45:00,70.41,39.407,41.231,29.17 -2020-06-27 22:00:00,62.13,36.991,40.798,29.17 -2020-06-27 22:15:00,64.93,39.149,40.798,29.17 -2020-06-27 22:30:00,60.41,38.819,40.798,29.17 -2020-06-27 22:45:00,62.8,36.968,40.798,29.17 -2020-06-27 23:00:00,59.35,33.760999999999996,34.402,29.17 -2020-06-27 23:15:00,59.05,31.136999999999997,34.402,29.17 -2020-06-27 23:30:00,58.0,30.514,34.402,29.17 -2020-06-27 23:45:00,57.81,29.741999999999997,34.402,29.17 -2020-06-28 00:00:00,52.34,24.013,30.171,29.17 -2020-06-28 00:15:00,55.36,22.846,30.171,29.17 -2020-06-28 00:30:00,54.45,21.474,30.171,29.17 -2020-06-28 00:45:00,54.19,20.747,30.171,29.17 -2020-06-28 01:00:00,53.18,20.885,27.15,29.17 -2020-06-28 01:15:00,50.44,19.824,27.15,29.17 -2020-06-28 01:30:00,52.82,17.895,27.15,29.17 -2020-06-28 01:45:00,53.02,18.034000000000002,27.15,29.17 -2020-06-28 02:00:00,52.68,17.64,25.403000000000002,29.17 -2020-06-28 02:15:00,52.39,15.887,25.403000000000002,29.17 -2020-06-28 02:30:00,52.08,18.303,25.403000000000002,29.17 -2020-06-28 02:45:00,52.53,18.977,25.403000000000002,29.17 -2020-06-28 03:00:00,52.73,20.605999999999998,23.386999999999997,29.17 -2020-06-28 03:15:00,53.01,17.711,23.386999999999997,29.17 -2020-06-28 03:30:00,52.27,16.403,23.386999999999997,29.17 -2020-06-28 03:45:00,52.48,17.54,23.386999999999997,29.17 -2020-06-28 04:00:00,51.85,21.539,23.941999999999997,29.17 -2020-06-28 04:15:00,51.64,25.916999999999998,23.941999999999997,29.17 -2020-06-28 04:30:00,52.18,23.593000000000004,23.941999999999997,29.17 -2020-06-28 04:45:00,52.88,22.923000000000002,23.941999999999997,29.17 -2020-06-28 05:00:00,52.47,28.29,23.026,29.17 -2020-06-28 05:15:00,52.43,26.049,23.026,29.17 -2020-06-28 05:30:00,49.33,19.701,23.026,29.17 -2020-06-28 05:45:00,52.47,20.958000000000002,23.026,29.17 -2020-06-28 06:00:00,53.34,33.241,23.223000000000003,29.17 -2020-06-28 06:15:00,53.67,42.965,23.223000000000003,29.17 -2020-06-28 06:30:00,54.36,37.324,23.223000000000003,29.17 -2020-06-28 06:45:00,54.97,33.788000000000004,23.223000000000003,29.17 -2020-06-28 07:00:00,54.97,33.775,24.968000000000004,29.17 -2020-06-28 07:15:00,54.23,31.741999999999997,24.968000000000004,29.17 -2020-06-28 07:30:00,53.95,29.835,24.968000000000004,29.17 -2020-06-28 07:45:00,53.99,30.430999999999997,24.968000000000004,29.17 -2020-06-28 08:00:00,54.31,30.06,29.131,29.17 -2020-06-28 08:15:00,53.99,34.674,29.131,29.17 -2020-06-28 08:30:00,54.29,35.741,29.131,29.17 -2020-06-28 08:45:00,55.38,39.021,29.131,29.17 -2020-06-28 09:00:00,53.19,34.866,29.904,29.17 -2020-06-28 09:15:00,54.67,35.915,29.904,29.17 -2020-06-28 09:30:00,54.92,39.713,29.904,29.17 -2020-06-28 09:45:00,55.03,43.516000000000005,29.904,29.17 -2020-06-28 10:00:00,56.5,39.309,28.943,29.17 -2020-06-28 10:15:00,57.14,41.528999999999996,28.943,29.17 -2020-06-28 10:30:00,58.29,42.163000000000004,28.943,29.17 -2020-06-28 10:45:00,58.46,44.405,28.943,29.17 -2020-06-28 11:00:00,57.77,40.417,31.682,29.17 -2020-06-28 11:15:00,55.17,41.08,31.682,29.17 -2020-06-28 11:30:00,54.11,42.855,31.682,29.17 -2020-06-28 11:45:00,54.05,44.603,31.682,29.17 -2020-06-28 12:00:00,52.24,43.143,27.315,29.17 -2020-06-28 12:15:00,54.17,42.653999999999996,27.315,29.17 -2020-06-28 12:30:00,51.79,41.54,27.315,29.17 -2020-06-28 12:45:00,52.87,42.261,27.315,29.17 -2020-06-28 13:00:00,48.04,42.218999999999994,23.894000000000002,29.17 -2020-06-28 13:15:00,48.71,42.547,23.894000000000002,29.17 -2020-06-28 13:30:00,47.9,40.772,23.894000000000002,29.17 -2020-06-28 13:45:00,51.51,39.719,23.894000000000002,29.17 -2020-06-28 14:00:00,51.52,42.372,21.148000000000003,29.17 -2020-06-28 14:15:00,49.54,40.695,21.148000000000003,29.17 -2020-06-28 14:30:00,49.02,39.174,21.148000000000003,29.17 -2020-06-28 14:45:00,52.42,38.562,21.148000000000003,29.17 -2020-06-28 15:00:00,52.14,41.068000000000005,21.229,29.17 -2020-06-28 15:15:00,52.45,38.25,21.229,29.17 -2020-06-28 15:30:00,53.38,35.873000000000005,21.229,29.17 -2020-06-28 15:45:00,50.9,33.839,21.229,29.17 -2020-06-28 16:00:00,52.91,36.479,25.037,29.17 -2020-06-28 16:15:00,55.27,35.875,25.037,29.17 -2020-06-28 16:30:00,56.98,36.352,25.037,29.17 -2020-06-28 16:45:00,61.07,31.888,25.037,29.17 -2020-06-28 17:00:00,63.55,36.014,37.11,29.17 -2020-06-28 17:15:00,65.49,35.226,37.11,29.17 -2020-06-28 17:30:00,66.42,35.125,37.11,29.17 -2020-06-28 17:45:00,67.75,34.19,37.11,29.17 -2020-06-28 18:00:00,72.02,38.864000000000004,42.215,29.17 -2020-06-28 18:15:00,71.79,39.239000000000004,42.215,29.17 -2020-06-28 18:30:00,71.81,37.871,42.215,29.17 -2020-06-28 18:45:00,73.23,38.946,42.215,29.17 -2020-06-28 19:00:00,75.51,41.174,44.383,29.17 -2020-06-28 19:15:00,72.91,38.681,44.383,29.17 -2020-06-28 19:30:00,72.05,38.239000000000004,44.383,29.17 -2020-06-28 19:45:00,71.62,38.571999999999996,44.383,29.17 -2020-06-28 20:00:00,72.23,37.048,43.426,29.17 -2020-06-28 20:15:00,72.93,37.198,43.426,29.17 -2020-06-28 20:30:00,75.71,37.187,43.426,29.17 -2020-06-28 20:45:00,76.24,37.47,43.426,29.17 -2020-06-28 21:00:00,79.17,35.599000000000004,42.265,29.17 -2020-06-28 21:15:00,79.66,38.616,42.265,29.17 -2020-06-28 21:30:00,77.53,38.846,42.265,29.17 -2020-06-28 21:45:00,76.34,40.054,42.265,29.17 -2020-06-28 22:00:00,71.7,39.599000000000004,42.26,29.17 -2020-06-28 22:15:00,73.09,40.06,42.26,29.17 -2020-06-28 22:30:00,70.56,39.058,42.26,29.17 -2020-06-28 22:45:00,71.0,35.898,42.26,29.17 -2020-06-28 23:00:00,64.44,32.248000000000005,36.609,29.17 -2020-06-28 23:15:00,64.62,30.998,36.609,29.17 -2020-06-28 23:30:00,61.55,29.895,36.609,29.17 -2020-06-28 23:45:00,63.88,29.316,36.609,29.17 -2020-06-29 00:00:00,66.83,25.676,34.611,29.28 -2020-06-29 00:15:00,67.56,25.444000000000003,34.611,29.28 -2020-06-29 00:30:00,66.99,23.704,34.611,29.28 -2020-06-29 00:45:00,65.2,22.566,34.611,29.28 -2020-06-29 01:00:00,61.33,23.096,33.552,29.28 -2020-06-29 01:15:00,61.25,22.009,33.552,29.28 -2020-06-29 01:30:00,68.73,20.438,33.552,29.28 -2020-06-29 01:45:00,69.26,20.480999999999998,33.552,29.28 -2020-06-29 02:00:00,67.73,20.535999999999998,32.351,29.28 -2020-06-29 02:15:00,64.1,17.863,32.351,29.28 -2020-06-29 02:30:00,65.25,20.449,32.351,29.28 -2020-06-29 02:45:00,62.46,20.95,32.351,29.28 -2020-06-29 03:00:00,63.93,23.139,30.793000000000003,29.28 -2020-06-29 03:15:00,64.66,21.044,30.793000000000003,29.28 -2020-06-29 03:30:00,64.42,20.402,30.793000000000003,29.28 -2020-06-29 03:45:00,65.17,21.073,30.793000000000003,29.28 -2020-06-29 04:00:00,69.29,28.322,31.274,29.28 -2020-06-29 04:15:00,69.39,35.769,31.274,29.28 -2020-06-29 04:30:00,73.21,33.074,31.274,29.28 -2020-06-29 04:45:00,76.37,32.777,31.274,29.28 -2020-06-29 05:00:00,84.05,45.946999999999996,37.75,29.28 -2020-06-29 05:15:00,89.26,54.832,37.75,29.28 -2020-06-29 05:30:00,91.77,47.041000000000004,37.75,29.28 -2020-06-29 05:45:00,100.7,45.004,37.75,29.28 -2020-06-29 06:00:00,106.18,46.221000000000004,55.36,29.28 -2020-06-29 06:15:00,104.78,46.18899999999999,55.36,29.28 -2020-06-29 06:30:00,101.12,44.909,55.36,29.28 -2020-06-29 06:45:00,106.27,47.068999999999996,55.36,29.28 -2020-06-29 07:00:00,110.35,47.54600000000001,65.87,29.28 -2020-06-29 07:15:00,106.69,47.847,65.87,29.28 -2020-06-29 07:30:00,102.74,45.05,65.87,29.28 -2020-06-29 07:45:00,98.31,45.833999999999996,65.87,29.28 -2020-06-29 08:00:00,101.86,43.085,55.695,29.28 -2020-06-29 08:15:00,104.63,46.422,55.695,29.28 -2020-06-29 08:30:00,103.4,46.312,55.695,29.28 -2020-06-29 08:45:00,98.22,49.619,55.695,29.28 -2020-06-29 09:00:00,97.83,44.388000000000005,50.881,29.28 -2020-06-29 09:15:00,98.5,43.49100000000001,50.881,29.28 -2020-06-29 09:30:00,97.26,46.31399999999999,50.881,29.28 -2020-06-29 09:45:00,99.53,47.886,50.881,29.28 -2020-06-29 10:00:00,105.48,44.043,49.138000000000005,29.28 -2020-06-29 10:15:00,107.62,46.105,49.138000000000005,29.28 -2020-06-29 10:30:00,106.69,46.236999999999995,49.138000000000005,29.28 -2020-06-29 10:45:00,101.86,46.96,49.138000000000005,29.28 -2020-06-29 11:00:00,108.46,43.041000000000004,49.178000000000004,29.28 -2020-06-29 11:15:00,104.0,44.08,49.178000000000004,29.28 -2020-06-29 11:30:00,102.52,46.606,49.178000000000004,29.28 -2020-06-29 11:45:00,97.39,48.825,49.178000000000004,29.28 -2020-06-29 12:00:00,95.56,45.77,47.698,29.28 -2020-06-29 12:15:00,98.26,45.39,47.698,29.28 -2020-06-29 12:30:00,92.89,43.213,47.698,29.28 -2020-06-29 12:45:00,91.17,44.008,47.698,29.28 -2020-06-29 13:00:00,90.56,44.903,48.104,29.28 -2020-06-29 13:15:00,91.19,44.283,48.104,29.28 -2020-06-29 13:30:00,93.09,42.658,48.104,29.28 -2020-06-29 13:45:00,90.72,42.498000000000005,48.104,29.28 -2020-06-29 14:00:00,92.67,44.217,48.53,29.28 -2020-06-29 14:15:00,90.61,43.095,48.53,29.28 -2020-06-29 14:30:00,89.74,41.367,48.53,29.28 -2020-06-29 14:45:00,89.22,42.806999999999995,48.53,29.28 -2020-06-29 15:00:00,92.79,45.053999999999995,49.351000000000006,29.28 -2020-06-29 15:15:00,89.48,41.575,49.351000000000006,29.28 -2020-06-29 15:30:00,89.59,39.916,49.351000000000006,29.28 -2020-06-29 15:45:00,91.17,37.37,49.351000000000006,29.28 -2020-06-29 16:00:00,92.45,41.121,51.44,29.28 -2020-06-29 16:15:00,93.63,40.543,51.44,29.28 -2020-06-29 16:30:00,98.96,40.327,51.44,29.28 -2020-06-29 16:45:00,97.02,35.815,51.44,29.28 -2020-06-29 17:00:00,99.29,38.823,56.868,29.28 -2020-06-29 17:15:00,99.23,38.361,56.868,29.28 -2020-06-29 17:30:00,99.19,37.845,56.868,29.28 -2020-06-29 17:45:00,102.14,36.434,56.868,29.28 -2020-06-29 18:00:00,102.78,40.075,57.229,29.28 -2020-06-29 18:15:00,101.5,38.505,57.229,29.28 -2020-06-29 18:30:00,103.73,36.42,57.229,29.28 -2020-06-29 18:45:00,102.37,40.659,57.229,29.28 -2020-06-29 19:00:00,99.56,42.528,57.744,29.28 -2020-06-29 19:15:00,95.89,41.278,57.744,29.28 -2020-06-29 19:30:00,96.63,40.488,57.744,29.28 -2020-06-29 19:45:00,92.86,40.168,57.744,29.28 -2020-06-29 20:00:00,91.91,37.258,66.05199999999999,29.28 -2020-06-29 20:15:00,90.4,38.671,66.05199999999999,29.28 -2020-06-29 20:30:00,93.06,39.088,66.05199999999999,29.28 -2020-06-29 20:45:00,90.78,39.683,66.05199999999999,29.28 -2020-06-29 21:00:00,92.65,37.217,59.396,29.28 -2020-06-29 21:15:00,88.69,40.618,59.396,29.28 -2020-06-29 21:30:00,86.02,41.196000000000005,59.396,29.28 -2020-06-29 21:45:00,84.23,42.163000000000004,59.396,29.28 -2020-06-29 22:00:00,80.64,39.546,53.06,29.28 -2020-06-29 22:15:00,79.82,41.918,53.06,29.28 -2020-06-29 22:30:00,78.02,36.138000000000005,53.06,29.28 -2020-06-29 22:45:00,80.24,32.711,53.06,29.28 -2020-06-29 23:00:00,73.65,29.131,46.148,29.28 -2020-06-29 23:15:00,74.02,26.403000000000002,46.148,29.28 -2020-06-29 23:30:00,72.94,25.406999999999996,46.148,29.28 -2020-06-29 23:45:00,71.27,24.415,46.148,29.28 -2020-06-30 00:00:00,72.03,23.34,44.625,29.28 -2020-06-30 00:15:00,69.91,24.04,44.625,29.28 -2020-06-30 00:30:00,68.65,22.963,44.625,29.28 -2020-06-30 00:45:00,71.12,22.569000000000003,44.625,29.28 -2020-06-30 01:00:00,68.89,22.561,41.733000000000004,29.28 -2020-06-30 01:15:00,68.91,21.552,41.733000000000004,29.28 -2020-06-30 01:30:00,69.21,19.86,41.733000000000004,29.28 -2020-06-30 01:45:00,72.69,19.401,41.733000000000004,29.28 -2020-06-30 02:00:00,69.55,18.98,39.872,29.28 -2020-06-30 02:15:00,70.67,17.457,39.872,29.28 -2020-06-30 02:30:00,77.04,19.615,39.872,29.28 -2020-06-30 02:45:00,77.77,20.448,39.872,29.28 -2020-06-30 03:00:00,73.56,22.02,38.711,29.28 -2020-06-30 03:15:00,71.83,20.819000000000003,38.711,29.28 -2020-06-30 03:30:00,74.59,20.177,38.711,29.28 -2020-06-30 03:45:00,72.48,19.767,38.711,29.28 -2020-06-30 04:00:00,73.67,25.706,39.823,29.28 -2020-06-30 04:15:00,76.58,33.113,39.823,29.28 -2020-06-30 04:30:00,78.87,30.273000000000003,39.823,29.28 -2020-06-30 04:45:00,84.64,30.521,39.823,29.28 -2020-06-30 05:00:00,90.01,45.441,43.228,29.28 -2020-06-30 05:15:00,94.67,54.836000000000006,43.228,29.28 -2020-06-30 05:30:00,100.35,47.21,43.228,29.28 -2020-06-30 05:45:00,104.12,44.481,43.228,29.28 -2020-06-30 06:00:00,104.34,46.69,54.316,29.28 -2020-06-30 06:15:00,106.09,46.928999999999995,54.316,29.28 -2020-06-30 06:30:00,104.25,45.317,54.316,29.28 -2020-06-30 06:45:00,111.24,46.6,54.316,29.28 -2020-06-30 07:00:00,112.68,47.207,65.758,29.28 -2020-06-30 07:15:00,112.93,47.266000000000005,65.758,29.28 -2020-06-30 07:30:00,106.91,44.458999999999996,65.758,29.28 -2020-06-30 07:45:00,109.59,44.313,65.758,29.28 -2020-06-30 08:00:00,108.14,41.508,57.983000000000004,29.28 -2020-06-30 08:15:00,105.14,44.367,57.983000000000004,29.28 -2020-06-30 08:30:00,102.56,44.407,57.983000000000004,29.28 -2020-06-30 08:45:00,109.58,46.778999999999996,57.983000000000004,29.28 -2020-06-30 09:00:00,108.83,41.842,52.653,29.28 -2020-06-30 09:15:00,107.0,40.89,52.653,29.28 -2020-06-30 09:30:00,102.96,44.428000000000004,52.653,29.28 -2020-06-30 09:45:00,100.07,47.33,52.653,29.28 -2020-06-30 10:00:00,106.46,42.102,51.408,29.28 -2020-06-30 10:15:00,108.26,43.974,51.408,29.28 -2020-06-30 10:30:00,109.08,44.14,51.408,29.28 -2020-06-30 10:45:00,106.82,45.883,51.408,29.28 -2020-06-30 11:00:00,100.17,42.043,51.913000000000004,29.28 -2020-06-30 11:15:00,103.91,43.474,51.913000000000004,29.28 -2020-06-30 11:30:00,106.58,44.784,51.913000000000004,29.28 -2020-06-30 11:45:00,101.37,46.586000000000006,51.913000000000004,29.28 -2020-06-30 12:00:00,98.37,43.301,49.508,29.28 -2020-06-30 12:15:00,96.43,43.236999999999995,49.508,29.28 -2020-06-30 12:30:00,100.54,41.938,49.508,29.28 -2020-06-30 12:45:00,100.76,43.412,49.508,29.28 -2020-06-30 13:00:00,99.12,43.91,50.007,29.28 -2020-06-30 13:15:00,93.41,45.073,50.007,29.28 -2020-06-30 13:30:00,100.0,43.479,50.007,29.28 -2020-06-30 13:45:00,100.4,42.37,50.007,29.28 -2020-06-30 14:00:00,101.59,44.562,49.778999999999996,29.28 -2020-06-30 14:15:00,93.92,43.248000000000005,49.778999999999996,29.28 -2020-06-30 14:30:00,98.79,41.861000000000004,49.778999999999996,29.28 -2020-06-30 14:45:00,99.07,42.513000000000005,49.778999999999996,29.28 -2020-06-30 15:00:00,98.76,44.603,51.559,29.28 -2020-06-30 15:15:00,96.36,42.058,51.559,29.28 -2020-06-30 15:30:00,97.72,40.195,51.559,29.28 -2020-06-30 15:45:00,100.74,37.971,51.559,29.28 -2020-06-30 16:00:00,101.72,41.047,53.531000000000006,29.28 -2020-06-30 16:15:00,97.36,40.577,53.531000000000006,29.28 -2020-06-30 16:30:00,100.31,40.037,53.531000000000006,29.28 -2020-06-30 16:45:00,100.27,36.243,53.531000000000006,29.28 -2020-06-30 17:00:00,108.34,39.504,59.497,29.28 -2020-06-30 17:15:00,109.28,39.463,59.497,29.28 -2020-06-30 17:30:00,107.79,38.535,59.497,29.28 -2020-06-30 17:45:00,105.96,36.778,59.497,29.28 -2020-06-30 18:00:00,107.94,39.473,59.861999999999995,29.28 -2020-06-30 18:15:00,109.77,39.347,59.861999999999995,29.28 -2020-06-30 18:30:00,112.0,36.985,59.861999999999995,29.28 -2020-06-30 18:45:00,107.95,41.065,59.861999999999995,29.28 -2020-06-30 19:00:00,102.45,41.773,60.989,29.28 -2020-06-30 19:15:00,102.48,40.681999999999995,60.989,29.28 -2020-06-30 19:30:00,105.3,39.64,60.989,29.28 -2020-06-30 19:45:00,101.39,39.65,60.989,29.28 -2020-06-30 20:00:00,93.96,37.129,68.35600000000001,29.28 -2020-06-30 20:15:00,92.47,37.068000000000005,68.35600000000001,29.28 -2020-06-30 20:30:00,92.77,37.573,68.35600000000001,29.28 -2020-06-30 20:45:00,95.45,38.578,68.35600000000001,29.28 -2020-06-30 21:00:00,91.73,36.955,59.251000000000005,29.28 -2020-06-30 21:15:00,91.24,38.936,59.251000000000005,29.28 -2020-06-30 21:30:00,89.56,39.635,59.251000000000005,29.28 -2020-06-30 21:45:00,87.06,40.806,59.251000000000005,29.28 -2020-06-30 22:00:00,82.92,38.446999999999996,54.736999999999995,29.28 -2020-06-30 22:15:00,83.13,40.474000000000004,54.736999999999995,29.28 -2020-06-30 22:30:00,79.39,34.979,54.736999999999995,29.28 -2020-06-30 22:45:00,78.5,31.551,54.736999999999995,29.28 -2020-06-30 23:00:00,75.71,27.199,46.806999999999995,29.28 -2020-06-30 23:15:00,75.56,26.018,46.806999999999995,29.28 -2020-06-30 23:30:00,74.43,25.016,46.806999999999995,29.28 -2020-06-30 23:45:00,73.31,24.177,46.806999999999995,29.28 -2020-07-01 00:00:00,70.43,18.788,42.195,29.509 -2020-07-01 00:15:00,71.7,19.662,42.195,29.509 -2020-07-01 00:30:00,70.95,18.385,42.195,29.509 -2020-07-01 00:45:00,71.27,18.276,42.195,29.509 -2020-07-01 01:00:00,69.68,18.217,38.82,29.509 -2020-07-01 01:15:00,70.95,17.452,38.82,29.509 -2020-07-01 01:30:00,71.25,15.89,38.82,29.509 -2020-07-01 01:45:00,71.45,15.93,38.82,29.509 -2020-07-01 02:00:00,70.66,15.644,37.023,29.509 -2020-07-01 02:15:00,70.4,14.513,37.023,29.509 -2020-07-01 02:30:00,71.19,16.352999999999998,37.023,29.509 -2020-07-01 02:45:00,71.81,17.062,37.023,29.509 -2020-07-01 03:00:00,72.87,18.293,36.818000000000005,29.509 -2020-07-01 03:15:00,71.24,17.483,36.818000000000005,29.509 -2020-07-01 03:30:00,72.47,16.722,36.818000000000005,29.509 -2020-07-01 03:45:00,73.77,15.866,36.818000000000005,29.509 -2020-07-01 04:00:00,79.37,20.849,37.495,29.509 -2020-07-01 04:15:00,83.66,27.185,37.495,29.509 -2020-07-01 04:30:00,85.87,24.576,37.495,29.509 -2020-07-01 04:45:00,82.08,24.599,37.495,29.509 -2020-07-01 05:00:00,87.68,37.652,39.858000000000004,29.509 -2020-07-01 05:15:00,91.0,45.316,39.858000000000004,29.509 -2020-07-01 05:30:00,94.48,39.075,39.858000000000004,29.509 -2020-07-01 05:45:00,96.89,36.391999999999996,39.858000000000004,29.509 -2020-07-01 06:00:00,102.46,37.272,52.867,29.509 -2020-07-01 06:15:00,102.72,36.881,52.867,29.509 -2020-07-01 06:30:00,102.82,36.024,52.867,29.509 -2020-07-01 06:45:00,103.56,37.891999999999996,52.867,29.509 -2020-07-01 07:00:00,106.17,38.054,66.061,29.509 -2020-07-01 07:15:00,107.32,38.36,66.061,29.509 -2020-07-01 07:30:00,103.25,36.042,66.061,29.509 -2020-07-01 07:45:00,105.44,36.064,66.061,29.509 -2020-07-01 08:00:00,102.77,31.921,58.532,29.509 -2020-07-01 08:15:00,106.3,34.949,58.532,29.509 -2020-07-01 08:30:00,109.35,36.171,58.532,29.509 -2020-07-01 08:45:00,110.17,38.823,58.532,29.509 -2020-07-01 09:00:00,107.3,32.952,56.047,29.509 -2020-07-01 09:15:00,102.83,32.253,56.047,29.509 -2020-07-01 09:30:00,105.13,36.139,56.047,29.509 -2020-07-01 09:45:00,108.99,39.617,56.047,29.509 -2020-07-01 10:00:00,108.91,36.763000000000005,53.823,29.509 -2020-07-01 10:15:00,111.42,38.422,53.823,29.509 -2020-07-01 10:30:00,107.94,38.482,53.823,29.509 -2020-07-01 10:45:00,106.69,39.787,53.823,29.509 -2020-07-01 11:00:00,108.14,37.555,54.184,29.509 -2020-07-01 11:15:00,107.34,38.873000000000005,54.184,29.509 -2020-07-01 11:30:00,103.63,40.246,54.184,29.509 -2020-07-01 11:45:00,99.79,41.605,54.184,29.509 -2020-07-01 12:00:00,101.32,36.647,52.628,29.509 -2020-07-01 12:15:00,105.42,36.146,52.628,29.509 -2020-07-01 12:30:00,103.81,35.018,52.628,29.509 -2020-07-01 12:45:00,100.08,36.165,52.628,29.509 -2020-07-01 13:00:00,97.35,36.506,52.31,29.509 -2020-07-01 13:15:00,99.57,37.897,52.31,29.509 -2020-07-01 13:30:00,102.34,36.091,52.31,29.509 -2020-07-01 13:45:00,101.61,35.67,52.31,29.509 -2020-07-01 14:00:00,97.36,37.439,52.278999999999996,29.509 -2020-07-01 14:15:00,95.86,36.361,52.278999999999996,29.509 -2020-07-01 14:30:00,94.71,35.399,52.278999999999996,29.509 -2020-07-01 14:45:00,102.25,35.926,52.278999999999996,29.509 -2020-07-01 15:00:00,101.58,37.861999999999995,53.306999999999995,29.509 -2020-07-01 15:15:00,106.6,35.504,53.306999999999995,29.509 -2020-07-01 15:30:00,109.06,33.898,53.306999999999995,29.509 -2020-07-01 15:45:00,110.7,32.225,53.306999999999995,29.509 -2020-07-01 16:00:00,104.2,34.999,55.358999999999995,29.509 -2020-07-01 16:15:00,100.24,34.702,55.358999999999995,29.509 -2020-07-01 16:30:00,107.63,33.689,55.358999999999995,29.509 -2020-07-01 16:45:00,110.17,30.451999999999998,55.358999999999995,29.509 -2020-07-01 17:00:00,110.35,34.147,59.211999999999996,29.509 -2020-07-01 17:15:00,104.03,34.079,59.211999999999996,29.509 -2020-07-01 17:30:00,107.88,32.936,59.211999999999996,29.509 -2020-07-01 17:45:00,108.5,31.721,59.211999999999996,29.509 -2020-07-01 18:00:00,113.12,34.732,60.403999999999996,29.509 -2020-07-01 18:15:00,112.56,34.763000000000005,60.403999999999996,29.509 -2020-07-01 18:30:00,109.0,32.714,60.403999999999996,29.509 -2020-07-01 18:45:00,106.69,35.778,60.403999999999996,29.509 -2020-07-01 19:00:00,103.85,36.946999999999996,60.993,29.509 -2020-07-01 19:15:00,104.5,36.014,60.993,29.509 -2020-07-01 19:30:00,99.49,34.96,60.993,29.509 -2020-07-01 19:45:00,102.64,34.247,60.993,29.509 -2020-07-01 20:00:00,93.35,31.489,66.6,29.509 -2020-07-01 20:15:00,92.62,31.057,66.6,29.509 -2020-07-01 20:30:00,94.73,31.609,66.6,29.509 -2020-07-01 20:45:00,92.77,32.135999999999996,66.6,29.509 -2020-07-01 21:00:00,93.61,30.487,59.855,29.509 -2020-07-01 21:15:00,91.43,32.09,59.855,29.509 -2020-07-01 21:30:00,88.22,33.063,59.855,29.509 -2020-07-01 21:45:00,86.19,34.155,59.855,29.509 -2020-07-01 22:00:00,82.68,31.27,54.942,29.509 -2020-07-01 22:15:00,82.47,33.906,54.942,29.509 -2020-07-01 22:30:00,79.33,29.747,54.942,29.509 -2020-07-01 22:45:00,79.58,26.566999999999997,54.942,29.509 -2020-07-01 23:00:00,76.55,23.329,46.056000000000004,29.509 -2020-07-01 23:15:00,75.74,21.73,46.056000000000004,29.509 -2020-07-01 23:30:00,76.51,20.483,46.056000000000004,29.509 -2020-07-01 23:45:00,75.37,19.579,46.056000000000004,29.509 -2020-07-02 00:00:00,71.27,18.766,40.859,29.509 -2020-07-02 00:15:00,75.23,19.639,40.859,29.509 -2020-07-02 00:30:00,72.87,18.366,40.859,29.509 -2020-07-02 00:45:00,72.99,18.262,40.859,29.509 -2020-07-02 01:00:00,73.16,18.214000000000002,39.06,29.509 -2020-07-02 01:15:00,71.58,17.437,39.06,29.509 -2020-07-02 01:30:00,70.03,15.875,39.06,29.509 -2020-07-02 01:45:00,71.6,15.907,39.06,29.509 -2020-07-02 02:00:00,71.98,15.627,37.592,29.509 -2020-07-02 02:15:00,72.45,14.513,37.592,29.509 -2020-07-02 02:30:00,71.95,16.332,37.592,29.509 -2020-07-02 02:45:00,73.87,17.044,37.592,29.509 -2020-07-02 03:00:00,73.17,18.271,37.416,29.509 -2020-07-02 03:15:00,73.55,17.469,37.416,29.509 -2020-07-02 03:30:00,74.45,16.714000000000002,37.416,29.509 -2020-07-02 03:45:00,76.07,15.876,37.416,29.509 -2020-07-02 04:00:00,85.78,20.818,38.176,29.509 -2020-07-02 04:15:00,84.85,27.118000000000002,38.176,29.509 -2020-07-02 04:30:00,81.2,24.5,38.176,29.509 -2020-07-02 04:45:00,82.99,24.521,38.176,29.509 -2020-07-02 05:00:00,91.83,37.501999999999995,41.203,29.509 -2020-07-02 05:15:00,92.7,45.07,41.203,29.509 -2020-07-02 05:30:00,93.71,38.889,41.203,29.509 -2020-07-02 05:45:00,100.65,36.238,41.203,29.509 -2020-07-02 06:00:00,111.15,37.12,51.09,29.509 -2020-07-02 06:15:00,112.6,36.724000000000004,51.09,29.509 -2020-07-02 06:30:00,107.72,35.889,51.09,29.509 -2020-07-02 06:45:00,109.61,37.786,51.09,29.509 -2020-07-02 07:00:00,106.92,37.939,63.541000000000004,29.509 -2020-07-02 07:15:00,111.87,38.266,63.541000000000004,29.509 -2020-07-02 07:30:00,113.84,35.949,63.541000000000004,29.509 -2020-07-02 07:45:00,113.44,36.0,63.541000000000004,29.509 -2020-07-02 08:00:00,107.91,31.868000000000002,55.65,29.509 -2020-07-02 08:15:00,107.55,34.914,55.65,29.509 -2020-07-02 08:30:00,114.18,36.126,55.65,29.509 -2020-07-02 08:45:00,115.94,38.775,55.65,29.509 -2020-07-02 09:00:00,110.22,32.898,51.833999999999996,29.509 -2020-07-02 09:15:00,109.29,32.201,51.833999999999996,29.509 -2020-07-02 09:30:00,107.38,36.082,51.833999999999996,29.509 -2020-07-02 09:45:00,111.58,39.568000000000005,51.833999999999996,29.509 -2020-07-02 10:00:00,112.97,36.721,49.70399999999999,29.509 -2020-07-02 10:15:00,113.92,38.382,49.70399999999999,29.509 -2020-07-02 10:30:00,108.83,38.439,49.70399999999999,29.509 -2020-07-02 10:45:00,113.08,39.747,49.70399999999999,29.509 -2020-07-02 11:00:00,112.65,37.512,48.593999999999994,29.509 -2020-07-02 11:15:00,108.39,38.832,48.593999999999994,29.509 -2020-07-02 11:30:00,105.46,40.194,48.593999999999994,29.509 -2020-07-02 11:45:00,103.74,41.549,48.593999999999994,29.509 -2020-07-02 12:00:00,102.28,36.616,46.275,29.509 -2020-07-02 12:15:00,112.02,36.114000000000004,46.275,29.509 -2020-07-02 12:30:00,110.12,34.974000000000004,46.275,29.509 -2020-07-02 12:45:00,111.95,36.118,46.275,29.509 -2020-07-02 13:00:00,108.93,36.444,45.803000000000004,29.509 -2020-07-02 13:15:00,104.36,37.830999999999996,45.803000000000004,29.509 -2020-07-02 13:30:00,111.63,36.033,45.803000000000004,29.509 -2020-07-02 13:45:00,108.98,35.619,45.803000000000004,29.509 -2020-07-02 14:00:00,114.1,37.393,46.251999999999995,29.509 -2020-07-02 14:15:00,102.95,36.316,46.251999999999995,29.509 -2020-07-02 14:30:00,101.66,35.339,46.251999999999995,29.509 -2020-07-02 14:45:00,108.11,35.873000000000005,46.251999999999995,29.509 -2020-07-02 15:00:00,117.54,37.826,48.309,29.509 -2020-07-02 15:15:00,115.68,35.461,48.309,29.509 -2020-07-02 15:30:00,115.1,33.853,48.309,29.509 -2020-07-02 15:45:00,108.35,32.172,48.309,29.509 -2020-07-02 16:00:00,106.86,34.964,49.681999999999995,29.509 -2020-07-02 16:15:00,116.47,34.668,49.681999999999995,29.509 -2020-07-02 16:30:00,115.12,33.673,49.681999999999995,29.509 -2020-07-02 16:45:00,115.58,30.432,49.681999999999995,29.509 -2020-07-02 17:00:00,113.83,34.135999999999996,53.086000000000006,29.509 -2020-07-02 17:15:00,108.49,34.075,53.086000000000006,29.509 -2020-07-02 17:30:00,108.27,32.933,53.086000000000006,29.509 -2020-07-02 17:45:00,110.25,31.714000000000002,53.086000000000006,29.509 -2020-07-02 18:00:00,117.44,34.733000000000004,54.038999999999994,29.509 -2020-07-02 18:15:00,113.72,34.743,54.038999999999994,29.509 -2020-07-02 18:30:00,110.69,32.695,54.038999999999994,29.509 -2020-07-02 18:45:00,103.51,35.757,54.038999999999994,29.509 -2020-07-02 19:00:00,99.92,36.928000000000004,53.408,29.509 -2020-07-02 19:15:00,95.01,35.986999999999995,53.408,29.509 -2020-07-02 19:30:00,95.4,34.926,53.408,29.509 -2020-07-02 19:45:00,91.38,34.207,53.408,29.509 -2020-07-02 20:00:00,91.86,31.435,55.309,29.509 -2020-07-02 20:15:00,89.76,31.002,55.309,29.509 -2020-07-02 20:30:00,90.27,31.554000000000002,55.309,29.509 -2020-07-02 20:45:00,93.88,32.098,55.309,29.509 -2020-07-02 21:00:00,88.63,30.451999999999998,51.585,29.509 -2020-07-02 21:15:00,87.77,32.058,51.585,29.509 -2020-07-02 21:30:00,87.98,33.01,51.585,29.509 -2020-07-02 21:45:00,85.55,34.099000000000004,51.585,29.509 -2020-07-02 22:00:00,81.55,31.225,48.006,29.509 -2020-07-02 22:15:00,79.87,33.864000000000004,48.006,29.509 -2020-07-02 22:30:00,78.85,29.693,48.006,29.509 -2020-07-02 22:45:00,80.14,26.506,48.006,29.509 -2020-07-02 23:00:00,74.23,23.264,42.309,29.509 -2020-07-02 23:15:00,74.85,21.691999999999997,42.309,29.509 -2020-07-02 23:30:00,74.97,20.459,42.309,29.509 -2020-07-02 23:45:00,73.89,19.547,42.309,29.509 -2020-07-03 00:00:00,70.93,16.918,39.649,29.509 -2020-07-03 00:15:00,74.24,18.007,39.649,29.509 -2020-07-03 00:30:00,70.69,17.027,39.649,29.509 -2020-07-03 00:45:00,71.66,17.366,39.649,29.509 -2020-07-03 01:00:00,70.83,16.942,37.744,29.509 -2020-07-03 01:15:00,71.09,15.489,37.744,29.509 -2020-07-03 01:30:00,73.13,14.683,37.744,29.509 -2020-07-03 01:45:00,70.01,14.513,37.744,29.509 -2020-07-03 02:00:00,69.99,15.138,36.965,29.509 -2020-07-03 02:15:00,72.95,14.513,36.965,29.509 -2020-07-03 02:30:00,70.26,16.631,36.965,29.509 -2020-07-03 02:45:00,72.71,16.631,36.965,29.509 -2020-07-03 03:00:00,71.77,18.701,37.678000000000004,29.509 -2020-07-03 03:15:00,71.51,16.611,37.678000000000004,29.509 -2020-07-03 03:30:00,76.38,15.615,37.678000000000004,29.509 -2020-07-03 03:45:00,80.75,15.698,37.678000000000004,29.509 -2020-07-03 04:00:00,85.8,20.732,38.591,29.509 -2020-07-03 04:15:00,79.97,25.381,38.591,29.509 -2020-07-03 04:30:00,79.88,23.738000000000003,38.591,29.509 -2020-07-03 04:45:00,87.51,23.125999999999998,38.591,29.509 -2020-07-03 05:00:00,88.68,35.584,40.666,29.509 -2020-07-03 05:15:00,99.92,44.004,40.666,29.509 -2020-07-03 05:30:00,102.65,38.053000000000004,40.666,29.509 -2020-07-03 05:45:00,105.28,34.976,40.666,29.509 -2020-07-03 06:00:00,112.07,36.077,51.784,29.509 -2020-07-03 06:15:00,109.3,35.889,51.784,29.509 -2020-07-03 06:30:00,114.85,35.047,51.784,29.509 -2020-07-03 06:45:00,117.65,36.794000000000004,51.784,29.509 -2020-07-03 07:00:00,118.78,37.632,61.383,29.509 -2020-07-03 07:15:00,114.49,38.888000000000005,61.383,29.509 -2020-07-03 07:30:00,122.05,34.546,61.383,29.509 -2020-07-03 07:45:00,121.0,34.44,61.383,29.509 -2020-07-03 08:00:00,120.65,31.176,55.272,29.509 -2020-07-03 08:15:00,115.76,34.957,55.272,29.509 -2020-07-03 08:30:00,118.63,36.018,55.272,29.509 -2020-07-03 08:45:00,124.55,38.578,55.272,29.509 -2020-07-03 09:00:00,124.35,30.26,53.506,29.509 -2020-07-03 09:15:00,123.23,31.552,53.506,29.509 -2020-07-03 09:30:00,117.79,34.724000000000004,53.506,29.509 -2020-07-03 09:45:00,124.21,38.628,53.506,29.509 -2020-07-03 10:00:00,124.18,35.675,51.363,29.509 -2020-07-03 10:15:00,125.33,37.067,51.363,29.509 -2020-07-03 10:30:00,113.6,37.708,51.363,29.509 -2020-07-03 10:45:00,112.47,38.94,51.363,29.509 -2020-07-03 11:00:00,117.84,36.973,51.043,29.509 -2020-07-03 11:15:00,119.54,37.192,51.043,29.509 -2020-07-03 11:30:00,114.56,38.071,51.043,29.509 -2020-07-03 11:45:00,111.63,38.407,51.043,29.509 -2020-07-03 12:00:00,106.04,33.891999999999996,47.52,29.509 -2020-07-03 12:15:00,110.61,32.827,47.52,29.509 -2020-07-03 12:30:00,107.32,31.789,47.52,29.509 -2020-07-03 12:45:00,103.64,32.069,47.52,29.509 -2020-07-03 13:00:00,101.08,32.958,45.494,29.509 -2020-07-03 13:15:00,99.8,34.49,45.494,29.509 -2020-07-03 13:30:00,95.66,33.538000000000004,45.494,29.509 -2020-07-03 13:45:00,101.57,33.458,45.494,29.509 -2020-07-03 14:00:00,98.0,34.446999999999996,43.883,29.509 -2020-07-03 14:15:00,99.34,33.841,43.883,29.509 -2020-07-03 14:30:00,102.34,34.419000000000004,43.883,29.509 -2020-07-03 14:45:00,107.73,34.214,43.883,29.509 -2020-07-03 15:00:00,109.4,36.125,45.714,29.509 -2020-07-03 15:15:00,106.77,33.525,45.714,29.509 -2020-07-03 15:30:00,104.22,31.396,45.714,29.509 -2020-07-03 15:45:00,105.17,30.504,45.714,29.509 -2020-07-03 16:00:00,101.31,32.468,48.222,29.509 -2020-07-03 16:15:00,98.01,32.68,48.222,29.509 -2020-07-03 16:30:00,102.98,31.509,48.222,29.509 -2020-07-03 16:45:00,107.56,27.44,48.222,29.509 -2020-07-03 17:00:00,110.19,32.964,52.619,29.509 -2020-07-03 17:15:00,103.65,32.747,52.619,29.509 -2020-07-03 17:30:00,108.81,31.796,52.619,29.509 -2020-07-03 17:45:00,110.01,30.412,52.619,29.509 -2020-07-03 18:00:00,107.36,33.431,52.99,29.509 -2020-07-03 18:15:00,103.91,32.449,52.99,29.509 -2020-07-03 18:30:00,105.74,30.276,52.99,29.509 -2020-07-03 18:45:00,106.53,33.777,52.99,29.509 -2020-07-03 19:00:00,100.94,35.813,51.923,29.509 -2020-07-03 19:15:00,91.66,35.426,51.923,29.509 -2020-07-03 19:30:00,95.59,34.438,51.923,29.509 -2020-07-03 19:45:00,96.63,32.675,51.923,29.509 -2020-07-03 20:00:00,95.47,29.728,56.238,29.509 -2020-07-03 20:15:00,91.08,30.12,56.238,29.509 -2020-07-03 20:30:00,87.72,30.166,56.238,29.509 -2020-07-03 20:45:00,93.25,29.855,56.238,29.509 -2020-07-03 21:00:00,93.94,29.558000000000003,52.426,29.509 -2020-07-03 21:15:00,91.58,32.889,52.426,29.509 -2020-07-03 21:30:00,84.87,33.661,52.426,29.509 -2020-07-03 21:45:00,81.0,34.939,52.426,29.509 -2020-07-03 22:00:00,81.22,31.889,48.196000000000005,29.509 -2020-07-03 22:15:00,83.92,34.275999999999996,48.196000000000005,29.509 -2020-07-03 22:30:00,81.87,34.796,48.196000000000005,29.509 -2020-07-03 22:45:00,78.52,32.36,48.196000000000005,29.509 -2020-07-03 23:00:00,69.31,30.891,41.71,29.509 -2020-07-03 23:15:00,70.51,27.789,41.71,29.509 -2020-07-03 23:30:00,75.91,24.73,41.71,29.509 -2020-07-03 23:45:00,74.86,23.709,41.71,29.509 -2020-07-04 00:00:00,71.28,18.308,41.105,29.398000000000003 -2020-07-04 00:15:00,66.58,18.862000000000002,41.105,29.398000000000003 -2020-07-04 00:30:00,68.4,17.344,41.105,29.398000000000003 -2020-07-04 00:45:00,72.06,16.899,41.105,29.398000000000003 -2020-07-04 01:00:00,70.88,16.746,36.934,29.398000000000003 -2020-07-04 01:15:00,68.8,15.936,36.934,29.398000000000003 -2020-07-04 01:30:00,62.1,14.513,36.934,29.398000000000003 -2020-07-04 01:45:00,61.89,15.368,36.934,29.398000000000003 -2020-07-04 02:00:00,61.38,15.015999999999998,34.782,29.398000000000003 -2020-07-04 02:15:00,61.18,14.513,34.782,29.398000000000003 -2020-07-04 02:30:00,67.97,14.918,34.782,29.398000000000003 -2020-07-04 02:45:00,68.39,15.735999999999999,34.782,29.398000000000003 -2020-07-04 03:00:00,67.7,16.349,34.489000000000004,29.398000000000003 -2020-07-04 03:15:00,65.75,14.513,34.489000000000004,29.398000000000003 -2020-07-04 03:30:00,69.36,14.513,34.489000000000004,29.398000000000003 -2020-07-04 03:45:00,67.53,14.645,34.489000000000004,29.398000000000003 -2020-07-04 04:00:00,64.42,17.846,34.111,29.398000000000003 -2020-07-04 04:15:00,60.03,21.635,34.111,29.398000000000003 -2020-07-04 04:30:00,62.56,18.337,34.111,29.398000000000003 -2020-07-04 04:45:00,68.6,18.028,34.111,29.398000000000003 -2020-07-04 05:00:00,69.39,22.414,33.283,29.398000000000003 -2020-07-04 05:15:00,67.18,20.141,33.283,29.398000000000003 -2020-07-04 05:30:00,62.58,15.806,33.283,29.398000000000003 -2020-07-04 05:45:00,66.37,17.168,33.283,29.398000000000003 -2020-07-04 06:00:00,67.09,29.13,33.653,29.398000000000003 -2020-07-04 06:15:00,70.36,36.363,33.653,29.398000000000003 -2020-07-04 06:30:00,75.13,32.815,33.653,29.398000000000003 -2020-07-04 06:45:00,76.55,31.62,33.653,29.398000000000003 -2020-07-04 07:00:00,74.19,31.340999999999998,36.732,29.398000000000003 -2020-07-04 07:15:00,69.0,31.328000000000003,36.732,29.398000000000003 -2020-07-04 07:30:00,75.39,28.43,36.732,29.398000000000003 -2020-07-04 07:45:00,78.46,29.125999999999998,36.732,29.398000000000003 -2020-07-04 08:00:00,78.82,26.471,41.318999999999996,29.398000000000003 -2020-07-04 08:15:00,75.62,29.854,41.318999999999996,29.398000000000003 -2020-07-04 08:30:00,72.05,30.805,41.318999999999996,29.398000000000003 -2020-07-04 08:45:00,72.92,34.181999999999995,41.318999999999996,29.398000000000003 -2020-07-04 09:00:00,75.51,29.045,43.195,29.398000000000003 -2020-07-04 09:15:00,75.93,30.805,43.195,29.398000000000003 -2020-07-04 09:30:00,76.19,34.44,43.195,29.398000000000003 -2020-07-04 09:45:00,69.16,37.868,43.195,29.398000000000003 -2020-07-04 10:00:00,68.23,35.586999999999996,41.843999999999994,29.398000000000003 -2020-07-04 10:15:00,68.67,37.345,41.843999999999994,29.398000000000003 -2020-07-04 10:30:00,72.15,37.611999999999995,41.843999999999994,29.398000000000003 -2020-07-04 10:45:00,69.35,38.399,41.843999999999994,29.398000000000003 -2020-07-04 11:00:00,67.52,36.268,39.035,29.398000000000003 -2020-07-04 11:15:00,67.46,37.444,39.035,29.398000000000003 -2020-07-04 11:30:00,65.74,38.674,39.035,29.398000000000003 -2020-07-04 11:45:00,64.54,39.766999999999996,39.035,29.398000000000003 -2020-07-04 12:00:00,62.25,35.786,38.001,29.398000000000003 -2020-07-04 12:15:00,60.77,35.577,38.001,29.398000000000003 -2020-07-04 12:30:00,60.3,34.369,38.001,29.398000000000003 -2020-07-04 12:45:00,59.23,35.464,38.001,29.398000000000003 -2020-07-04 13:00:00,57.7,35.444,34.747,29.398000000000003 -2020-07-04 13:15:00,57.19,36.525999999999996,34.747,29.398000000000003 -2020-07-04 13:30:00,57.53,35.806,34.747,29.398000000000003 -2020-07-04 13:45:00,57.64,34.283,34.747,29.398000000000003 -2020-07-04 14:00:00,57.47,35.258,33.434,29.398000000000003 -2020-07-04 14:15:00,57.5,33.368,33.434,29.398000000000003 -2020-07-04 14:30:00,57.46,33.594,33.434,29.398000000000003 -2020-07-04 14:45:00,58.13,33.887,33.434,29.398000000000003 -2020-07-04 15:00:00,59.46,36.232,35.921,29.398000000000003 -2020-07-04 15:15:00,59.91,34.321,35.921,29.398000000000003 -2020-07-04 15:30:00,60.67,32.314,35.921,29.398000000000003 -2020-07-04 15:45:00,61.72,30.444000000000003,35.921,29.398000000000003 -2020-07-04 16:00:00,63.63,34.691,39.427,29.398000000000003 -2020-07-04 16:15:00,64.21,33.919000000000004,39.427,29.398000000000003 -2020-07-04 16:30:00,69.13,33.007,39.427,29.398000000000003 -2020-07-04 16:45:00,69.2,28.857,39.427,29.398000000000003 -2020-07-04 17:00:00,69.91,33.164,44.096000000000004,29.398000000000003 -2020-07-04 17:15:00,72.05,30.614,44.096000000000004,29.398000000000003 -2020-07-04 17:30:00,73.58,29.529,44.096000000000004,29.398000000000003 -2020-07-04 17:45:00,75.0,28.683000000000003,44.096000000000004,29.398000000000003 -2020-07-04 18:00:00,76.84,33.131,43.931000000000004,29.398000000000003 -2020-07-04 18:15:00,76.16,33.784,43.931000000000004,29.398000000000003 -2020-07-04 18:30:00,76.26,32.959,43.931000000000004,29.398000000000003 -2020-07-04 18:45:00,77.12,33.004,43.931000000000004,29.398000000000003 -2020-07-04 19:00:00,75.57,33.28,42.187,29.398000000000003 -2020-07-04 19:15:00,72.31,31.864,42.187,29.398000000000003 -2020-07-04 19:30:00,71.21,31.631999999999998,42.187,29.398000000000003 -2020-07-04 19:45:00,70.88,31.693,42.187,29.398000000000003 -2020-07-04 20:00:00,70.25,29.52,38.315,29.398000000000003 -2020-07-04 20:15:00,69.83,29.163,38.315,29.398000000000003 -2020-07-04 20:30:00,69.99,28.314,38.315,29.398000000000003 -2020-07-04 20:45:00,70.95,29.953000000000003,38.315,29.398000000000003 -2020-07-04 21:00:00,71.53,28.039,36.843,29.398000000000003 -2020-07-04 21:15:00,71.29,30.995,36.843,29.398000000000003 -2020-07-04 21:30:00,70.14,31.895,36.843,29.398000000000003 -2020-07-04 21:45:00,68.61,32.623000000000005,36.843,29.398000000000003 -2020-07-04 22:00:00,66.07,29.506999999999998,37.260999999999996,29.398000000000003 -2020-07-04 22:15:00,66.0,32.06,37.260999999999996,29.398000000000003 -2020-07-04 22:30:00,62.91,31.948,37.260999999999996,29.398000000000003 -2020-07-04 22:45:00,62.98,29.82,37.260999999999996,29.398000000000003 -2020-07-04 23:00:00,59.32,27.59,32.148,29.398000000000003 -2020-07-04 23:15:00,58.95,25.004,32.148,29.398000000000003 -2020-07-04 23:30:00,58.24,24.543000000000003,32.148,29.398000000000003 -2020-07-04 23:45:00,57.81,23.965999999999998,32.148,29.398000000000003 -2020-07-05 00:00:00,55.67,19.707,28.905,29.398000000000003 -2020-07-05 00:15:00,55.37,19.081,28.905,29.398000000000003 -2020-07-05 00:30:00,54.85,17.43,28.905,29.398000000000003 -2020-07-05 00:45:00,54.72,16.848,28.905,29.398000000000003 -2020-07-05 01:00:00,53.6,16.983,26.906999999999996,29.398000000000003 -2020-07-05 01:15:00,53.84,15.994000000000002,26.906999999999996,29.398000000000003 -2020-07-05 01:30:00,54.02,14.513,26.906999999999996,29.398000000000003 -2020-07-05 01:45:00,54.04,14.877,26.906999999999996,29.398000000000003 -2020-07-05 02:00:00,53.25,14.642000000000001,25.938000000000002,29.398000000000003 -2020-07-05 02:15:00,53.53,14.513,25.938000000000002,29.398000000000003 -2020-07-05 02:30:00,53.44,15.464,25.938000000000002,29.398000000000003 -2020-07-05 02:45:00,52.89,15.967,25.938000000000002,29.398000000000003 -2020-07-05 03:00:00,52.29,17.232,24.693,29.398000000000003 -2020-07-05 03:15:00,53.06,14.776,24.693,29.398000000000003 -2020-07-05 03:30:00,52.75,14.513,24.693,29.398000000000003 -2020-07-05 03:45:00,51.46,14.513,24.693,29.398000000000003 -2020-07-05 04:00:00,51.22,17.359,25.683000000000003,29.398000000000003 -2020-07-05 04:15:00,51.09,20.709,25.683000000000003,29.398000000000003 -2020-07-05 04:30:00,50.47,18.782,25.683000000000003,29.398000000000003 -2020-07-05 04:45:00,51.05,17.98,25.683000000000003,29.398000000000003 -2020-07-05 05:00:00,50.86,22.613000000000003,26.023000000000003,29.398000000000003 -2020-07-05 05:15:00,50.47,19.552,26.023000000000003,29.398000000000003 -2020-07-05 05:30:00,48.1,14.87,26.023000000000003,29.398000000000003 -2020-07-05 05:45:00,51.33,15.972000000000001,26.023000000000003,29.398000000000003 -2020-07-05 06:00:00,51.68,25.519000000000002,25.834,29.398000000000003 -2020-07-05 06:15:00,51.55,33.667,25.834,29.398000000000003 -2020-07-05 06:30:00,52.79,29.468000000000004,25.834,29.398000000000003 -2020-07-05 06:45:00,53.9,27.323,25.834,29.398000000000003 -2020-07-05 07:00:00,53.77,27.261999999999997,27.765,29.398000000000003 -2020-07-05 07:15:00,53.67,25.614,27.765,29.398000000000003 -2020-07-05 07:30:00,52.11,24.101,27.765,29.398000000000003 -2020-07-05 07:45:00,54.99,24.828000000000003,27.765,29.398000000000003 -2020-07-05 08:00:00,55.61,22.898000000000003,31.357,29.398000000000003 -2020-07-05 08:15:00,55.39,27.529,31.357,29.398000000000003 -2020-07-05 08:30:00,56.12,29.285999999999998,31.357,29.398000000000003 -2020-07-05 08:45:00,56.36,32.464,31.357,29.398000000000003 -2020-07-05 09:00:00,57.17,27.238000000000003,33.238,29.398000000000003 -2020-07-05 09:15:00,57.85,28.448,33.238,29.398000000000003 -2020-07-05 09:30:00,60.75,32.537,33.238,29.398000000000003 -2020-07-05 09:45:00,58.0,37.027,33.238,29.398000000000003 -2020-07-05 10:00:00,61.54,35.167,34.22,29.398000000000003 -2020-07-05 10:15:00,62.53,37.032,34.22,29.398000000000003 -2020-07-05 10:30:00,60.64,37.488,34.22,29.398000000000003 -2020-07-05 10:45:00,64.41,39.474000000000004,34.22,29.398000000000003 -2020-07-05 11:00:00,63.97,36.913000000000004,36.298,29.398000000000003 -2020-07-05 11:15:00,62.55,37.61,36.298,29.398000000000003 -2020-07-05 11:30:00,61.59,39.439,36.298,29.398000000000003 -2020-07-05 11:45:00,61.53,40.755,36.298,29.398000000000003 -2020-07-05 12:00:00,58.57,37.967,33.52,29.398000000000003 -2020-07-05 12:15:00,59.34,36.952,33.52,29.398000000000003 -2020-07-05 12:30:00,56.89,36.068000000000005,33.52,29.398000000000003 -2020-07-05 12:45:00,56.14,36.559,33.52,29.398000000000003 -2020-07-05 13:00:00,56.09,36.25,30.12,29.398000000000003 -2020-07-05 13:15:00,58.54,36.499,30.12,29.398000000000003 -2020-07-05 13:30:00,53.91,34.625,30.12,29.398000000000003 -2020-07-05 13:45:00,54.48,34.336999999999996,30.12,29.398000000000003 -2020-07-05 14:00:00,53.1,36.564,27.233,29.398000000000003 -2020-07-05 14:15:00,52.56,35.042,27.233,29.398000000000003 -2020-07-05 14:30:00,53.24,33.861999999999995,27.233,29.398000000000003 -2020-07-05 14:45:00,53.16,33.063,27.233,29.398000000000003 -2020-07-05 15:00:00,52.17,35.689,27.468000000000004,29.398000000000003 -2020-07-05 15:15:00,53.08,32.861999999999995,27.468000000000004,29.398000000000003 -2020-07-05 15:30:00,52.72,30.621,27.468000000000004,29.398000000000003 -2020-07-05 15:45:00,52.87,28.991999999999997,27.468000000000004,29.398000000000003 -2020-07-05 16:00:00,58.7,31.435,30.8,29.398000000000003 -2020-07-05 16:15:00,59.41,30.967,30.8,29.398000000000003 -2020-07-05 16:30:00,58.69,31.108,30.8,29.398000000000003 -2020-07-05 16:45:00,63.11,27.015,30.8,29.398000000000003 -2020-07-05 17:00:00,67.05,31.717,37.806,29.398000000000003 -2020-07-05 17:15:00,64.23,30.783,37.806,29.398000000000003 -2020-07-05 17:30:00,65.25,30.528000000000002,37.806,29.398000000000003 -2020-07-05 17:45:00,68.4,29.974,37.806,29.398000000000003 -2020-07-05 18:00:00,72.02,35.104,40.766,29.398000000000003 -2020-07-05 18:15:00,71.05,35.239000000000004,40.766,29.398000000000003 -2020-07-05 18:30:00,71.03,34.275999999999996,40.766,29.398000000000003 -2020-07-05 18:45:00,71.01,34.335,40.766,29.398000000000003 -2020-07-05 19:00:00,71.74,36.906,41.163000000000004,29.398000000000003 -2020-07-05 19:15:00,70.17,34.306999999999995,41.163000000000004,29.398000000000003 -2020-07-05 19:30:00,70.19,33.819,41.163000000000004,29.398000000000003 -2020-07-05 19:45:00,70.61,33.332,41.163000000000004,29.398000000000003 -2020-07-05 20:00:00,70.45,31.316,39.885999999999996,29.398000000000003 -2020-07-05 20:15:00,71.14,30.765,39.885999999999996,29.398000000000003 -2020-07-05 20:30:00,75.63,30.69,39.885999999999996,29.398000000000003 -2020-07-05 20:45:00,75.49,30.666,39.885999999999996,29.398000000000003 -2020-07-05 21:00:00,75.9,28.771,38.900999999999996,29.398000000000003 -2020-07-05 21:15:00,76.48,31.448,38.900999999999996,29.398000000000003 -2020-07-05 21:30:00,74.17,31.642,38.900999999999996,29.398000000000003 -2020-07-05 21:45:00,73.12,32.73,38.900999999999996,29.398000000000003 -2020-07-05 22:00:00,70.17,31.783,39.806999999999995,29.398000000000003 -2020-07-05 22:15:00,70.35,32.641999999999996,39.806999999999995,29.398000000000003 -2020-07-05 22:30:00,68.44,32.145,39.806999999999995,29.398000000000003 -2020-07-05 22:45:00,68.89,28.736,39.806999999999995,29.398000000000003 -2020-07-05 23:00:00,66.69,26.328000000000003,35.564,29.398000000000003 -2020-07-05 23:15:00,65.66,24.987,35.564,29.398000000000003 -2020-07-05 23:30:00,64.82,23.948,35.564,29.398000000000003 -2020-07-05 23:45:00,63.72,23.485,35.564,29.398000000000003 -2020-07-06 00:00:00,62.93,21.053,36.578,29.509 -2020-07-06 00:15:00,62.05,21.09,36.578,29.509 -2020-07-06 00:30:00,61.97,19.039,36.578,29.509 -2020-07-06 00:45:00,61.79,18.081,36.578,29.509 -2020-07-06 01:00:00,61.64,18.625,35.292,29.509 -2020-07-06 01:15:00,60.91,17.667,35.292,29.509 -2020-07-06 01:30:00,60.67,16.254,35.292,29.509 -2020-07-06 01:45:00,59.95,16.808,35.292,29.509 -2020-07-06 02:00:00,61.3,17.049,34.319,29.509 -2020-07-06 02:15:00,61.83,14.513,34.319,29.509 -2020-07-06 02:30:00,62.07,16.898,34.319,29.509 -2020-07-06 02:45:00,62.17,17.285,34.319,29.509 -2020-07-06 03:00:00,65.43,19.002,33.13,29.509 -2020-07-06 03:15:00,64.03,17.215,33.13,29.509 -2020-07-06 03:30:00,65.09,16.549,33.13,29.509 -2020-07-06 03:45:00,65.84,16.95,33.13,29.509 -2020-07-06 04:00:00,67.57,23.045,33.851,29.509 -2020-07-06 04:15:00,68.87,29.180999999999997,33.851,29.509 -2020-07-06 04:30:00,71.48,26.636999999999997,33.851,29.509 -2020-07-06 04:45:00,75.03,26.21,33.851,29.509 -2020-07-06 05:00:00,80.19,37.529,38.718,29.509 -2020-07-06 05:15:00,84.78,44.1,38.718,29.509 -2020-07-06 05:30:00,88.76,37.885,38.718,29.509 -2020-07-06 05:45:00,91.27,36.103,38.718,29.509 -2020-07-06 06:00:00,94.97,35.835,51.648999999999994,29.509 -2020-07-06 06:15:00,100.81,35.295,51.648999999999994,29.509 -2020-07-06 06:30:00,107.42,34.845,51.648999999999994,29.509 -2020-07-06 06:45:00,109.2,37.764,51.648999999999994,29.509 -2020-07-06 07:00:00,100.07,37.709,60.159,29.509 -2020-07-06 07:15:00,99.42,38.38,60.159,29.509 -2020-07-06 07:30:00,109.47,36.021,60.159,29.509 -2020-07-06 07:45:00,108.74,37.223,60.159,29.509 -2020-07-06 08:00:00,103.48,33.216,53.8,29.509 -2020-07-06 08:15:00,101.9,36.735,53.8,29.509 -2020-07-06 08:30:00,102.67,37.72,53.8,29.509 -2020-07-06 08:45:00,102.51,41.309,53.8,29.509 -2020-07-06 09:00:00,108.05,34.997,50.583,29.509 -2020-07-06 09:15:00,108.16,34.559,50.583,29.509 -2020-07-06 09:30:00,103.79,37.734,50.583,29.509 -2020-07-06 09:45:00,100.1,39.804,50.583,29.509 -2020-07-06 10:00:00,100.26,38.406,49.11600000000001,29.509 -2020-07-06 10:15:00,98.97,40.135999999999996,49.11600000000001,29.509 -2020-07-06 10:30:00,101.45,40.166,49.11600000000001,29.509 -2020-07-06 10:45:00,105.24,40.429,49.11600000000001,29.509 -2020-07-06 11:00:00,115.14,38.292,49.056000000000004,29.509 -2020-07-06 11:15:00,114.12,39.161,49.056000000000004,29.509 -2020-07-06 11:30:00,109.51,41.618,49.056000000000004,29.509 -2020-07-06 11:45:00,107.16,43.486999999999995,49.056000000000004,29.509 -2020-07-06 12:00:00,98.95,38.736,47.227,29.509 -2020-07-06 12:15:00,97.34,37.839,47.227,29.509 -2020-07-06 12:30:00,94.65,35.788000000000004,47.227,29.509 -2020-07-06 12:45:00,94.35,36.159,47.227,29.509 -2020-07-06 13:00:00,95.56,36.779,47.006,29.509 -2020-07-06 13:15:00,94.55,36.189,47.006,29.509 -2020-07-06 13:30:00,93.02,34.541,47.006,29.509 -2020-07-06 13:45:00,98.7,35.205999999999996,47.006,29.509 -2020-07-06 14:00:00,95.14,36.535,47.19,29.509 -2020-07-06 14:15:00,91.16,35.689,47.19,29.509 -2020-07-06 14:30:00,89.41,34.361999999999995,47.19,29.509 -2020-07-06 14:45:00,88.76,35.762,47.19,29.509 -2020-07-06 15:00:00,87.58,37.905,47.846000000000004,29.509 -2020-07-06 15:15:00,88.49,34.553000000000004,47.846000000000004,29.509 -2020-07-06 15:30:00,88.04,33.184,47.846000000000004,29.509 -2020-07-06 15:45:00,88.3,31.066999999999997,47.846000000000004,29.509 -2020-07-06 16:00:00,88.8,34.711999999999996,49.641000000000005,29.509 -2020-07-06 16:15:00,90.56,34.364000000000004,49.641000000000005,29.509 -2020-07-06 16:30:00,93.03,33.887,49.641000000000005,29.509 -2020-07-06 16:45:00,95.66,29.892,49.641000000000005,29.509 -2020-07-06 17:00:00,98.75,33.438,54.133,29.509 -2020-07-06 17:15:00,99.58,32.971,54.133,29.509 -2020-07-06 17:30:00,99.52,32.346,54.133,29.509 -2020-07-06 17:45:00,99.38,31.47,54.133,29.509 -2020-07-06 18:00:00,99.13,35.501999999999995,53.761,29.509 -2020-07-06 18:15:00,101.41,33.836999999999996,53.761,29.509 -2020-07-06 18:30:00,101.33,32.049,53.761,29.509 -2020-07-06 18:45:00,99.47,35.366,53.761,29.509 -2020-07-06 19:00:00,96.1,37.764,53.923,29.509 -2020-07-06 19:15:00,92.71,36.589,53.923,29.509 -2020-07-06 19:30:00,92.02,35.689,53.923,29.509 -2020-07-06 19:45:00,92.82,34.603,53.923,29.509 -2020-07-06 20:00:00,91.58,31.381,58.786,29.509 -2020-07-06 20:15:00,89.83,32.415,58.786,29.509 -2020-07-06 20:30:00,90.8,32.982,58.786,29.509 -2020-07-06 20:45:00,90.9,33.107,58.786,29.509 -2020-07-06 21:00:00,88.74,30.525,54.591,29.509 -2020-07-06 21:15:00,87.51,33.721,54.591,29.509 -2020-07-06 21:30:00,85.2,34.366,54.591,29.509 -2020-07-06 21:45:00,83.29,35.247,54.591,29.509 -2020-07-06 22:00:00,80.54,32.321999999999996,51.551,29.509 -2020-07-06 22:15:00,79.21,35.305,51.551,29.509 -2020-07-06 22:30:00,78.44,30.779,51.551,29.509 -2020-07-06 22:45:00,76.79,27.581999999999997,51.551,29.509 -2020-07-06 23:00:00,75.39,25.141,44.716,29.509 -2020-07-06 23:15:00,74.53,21.991,44.716,29.509 -2020-07-06 23:30:00,74.57,20.794,44.716,29.509 -2020-07-06 23:45:00,72.99,19.643,44.716,29.509 -2020-07-07 00:00:00,71.53,18.721,43.01,29.509 -2020-07-07 00:15:00,70.84,19.582,43.01,29.509 -2020-07-07 00:30:00,71.39,18.327,43.01,29.509 -2020-07-07 00:45:00,70.56,18.253,43.01,29.509 -2020-07-07 01:00:00,71.2,18.25,40.687,29.509 -2020-07-07 01:15:00,70.9,17.422,40.687,29.509 -2020-07-07 01:30:00,70.33,15.867,40.687,29.509 -2020-07-07 01:45:00,70.39,15.862,40.687,29.509 -2020-07-07 02:00:00,70.97,15.605,39.554,29.509 -2020-07-07 02:15:00,70.84,14.513,39.554,29.509 -2020-07-07 02:30:00,77.92,16.289,39.554,29.509 -2020-07-07 02:45:00,78.65,17.02,39.554,29.509 -2020-07-07 03:00:00,75.67,18.214000000000002,38.958,29.509 -2020-07-07 03:15:00,70.77,17.46,38.958,29.509 -2020-07-07 03:30:00,74.81,16.742,38.958,29.509 -2020-07-07 03:45:00,74.03,15.989,38.958,29.509 -2020-07-07 04:00:00,77.7,20.735,39.783,29.509 -2020-07-07 04:15:00,82.93,26.872,39.783,29.509 -2020-07-07 04:30:00,86.8,24.212,39.783,29.509 -2020-07-07 04:45:00,87.2,24.226999999999997,39.783,29.509 -2020-07-07 05:00:00,87.99,36.885999999999996,42.281000000000006,29.509 -2020-07-07 05:15:00,95.27,44.023999999999994,42.281000000000006,29.509 -2020-07-07 05:30:00,96.03,38.126999999999995,42.281000000000006,29.509 -2020-07-07 05:45:00,100.96,35.614000000000004,42.281000000000006,29.509 -2020-07-07 06:00:00,111.3,36.497,50.801,29.509 -2020-07-07 06:15:00,114.63,36.082,50.801,29.509 -2020-07-07 06:30:00,116.56,35.349000000000004,50.801,29.509 -2020-07-07 06:45:00,116.13,37.391,50.801,29.509 -2020-07-07 07:00:00,116.59,37.498000000000005,60.202,29.509 -2020-07-07 07:15:00,121.92,37.93,60.202,29.509 -2020-07-07 07:30:00,124.36,35.623000000000005,60.202,29.509 -2020-07-07 07:45:00,123.71,35.817,60.202,29.509 -2020-07-07 08:00:00,119.38,31.735,54.461000000000006,29.509 -2020-07-07 08:15:00,124.37,34.859,54.461000000000006,29.509 -2020-07-07 08:30:00,128.67,36.022,54.461000000000006,29.509 -2020-07-07 08:45:00,130.1,38.647,54.461000000000006,29.509 -2020-07-07 09:00:00,129.85,32.748000000000005,50.753,29.509 -2020-07-07 09:15:00,128.0,32.054,50.753,29.509 -2020-07-07 09:30:00,130.61,35.906,50.753,29.509 -2020-07-07 09:45:00,129.99,39.417,50.753,29.509 -2020-07-07 10:00:00,122.45,36.611999999999995,49.703,29.509 -2020-07-07 10:15:00,120.63,38.272,49.703,29.509 -2020-07-07 10:30:00,122.78,38.313,49.703,29.509 -2020-07-07 10:45:00,123.45,39.629,49.703,29.509 -2020-07-07 11:00:00,128.68,37.385,49.42100000000001,29.509 -2020-07-07 11:15:00,125.65,38.714,49.42100000000001,29.509 -2020-07-07 11:30:00,122.76,40.018,49.42100000000001,29.509 -2020-07-07 11:45:00,121.95,41.35,49.42100000000001,29.509 -2020-07-07 12:00:00,118.98,36.525999999999996,47.155,29.509 -2020-07-07 12:15:00,125.09,36.022,47.155,29.509 -2020-07-07 12:30:00,122.87,34.824,47.155,29.509 -2020-07-07 12:45:00,119.92,35.959,47.155,29.509 -2020-07-07 13:00:00,121.51,36.201,47.515,29.509 -2020-07-07 13:15:00,124.02,37.568000000000005,47.515,29.509 -2020-07-07 13:30:00,121.3,35.8,47.515,29.509 -2020-07-07 13:45:00,120.45,35.42,47.515,29.509 -2020-07-07 14:00:00,111.95,37.211,47.575,29.509 -2020-07-07 14:15:00,112.15,36.14,47.575,29.509 -2020-07-07 14:30:00,118.73,35.104,47.575,29.509 -2020-07-07 14:45:00,117.88,35.671,47.575,29.509 -2020-07-07 15:00:00,112.52,37.69,48.903,29.509 -2020-07-07 15:15:00,106.84,35.296,48.903,29.509 -2020-07-07 15:30:00,107.4,33.681999999999995,48.903,29.509 -2020-07-07 15:45:00,99.77,31.961,48.903,29.509 -2020-07-07 16:00:00,96.93,34.83,50.218999999999994,29.509 -2020-07-07 16:15:00,106.09,34.543,50.218999999999994,29.509 -2020-07-07 16:30:00,106.41,33.641,50.218999999999994,29.509 -2020-07-07 16:45:00,104.59,30.386999999999997,50.218999999999994,29.509 -2020-07-07 17:00:00,99.96,34.125,55.396,29.509 -2020-07-07 17:15:00,103.8,34.11,55.396,29.509 -2020-07-07 17:30:00,100.99,32.974000000000004,55.396,29.509 -2020-07-07 17:45:00,102.8,31.754,55.396,29.509 -2020-07-07 18:00:00,108.74,34.804,55.583999999999996,29.509 -2020-07-07 18:15:00,108.95,34.722,55.583999999999996,29.509 -2020-07-07 18:30:00,107.59,32.679,55.583999999999996,29.509 -2020-07-07 18:45:00,103.26,35.739000000000004,55.583999999999996,29.509 -2020-07-07 19:00:00,99.07,36.917,56.071000000000005,29.509 -2020-07-07 19:15:00,94.57,35.938,56.071000000000005,29.509 -2020-07-07 19:30:00,92.99,34.843,56.071000000000005,29.509 -2020-07-07 19:45:00,92.6,34.101,56.071000000000005,29.509 -2020-07-07 20:00:00,91.59,31.27,61.55,29.509 -2020-07-07 20:15:00,90.41,30.826999999999998,61.55,29.509 -2020-07-07 20:30:00,90.06,31.373,61.55,29.509 -2020-07-07 20:45:00,90.65,31.985,61.55,29.509 -2020-07-07 21:00:00,90.29,30.355999999999998,55.94,29.509 -2020-07-07 21:15:00,88.11,31.968000000000004,55.94,29.509 -2020-07-07 21:30:00,85.72,32.819,55.94,29.509 -2020-07-07 21:45:00,84.05,33.887,55.94,29.509 -2020-07-07 22:00:00,82.01,31.055,52.857,29.509 -2020-07-07 22:15:00,79.35,33.702,52.857,29.509 -2020-07-07 22:30:00,77.27,29.464000000000002,52.857,29.509 -2020-07-07 22:45:00,75.9,26.239,52.857,29.509 -2020-07-07 23:00:00,73.88,23.0,46.04,29.509 -2020-07-07 23:15:00,72.06,21.55,46.04,29.509 -2020-07-07 23:30:00,71.73,20.384,46.04,29.509 -2020-07-07 23:45:00,70.42,19.437,46.04,29.509 -2020-07-08 00:00:00,68.87,18.723,42.195,29.509 -2020-07-08 00:15:00,68.28,19.582,42.195,29.509 -2020-07-08 00:30:00,67.17,18.331,42.195,29.509 -2020-07-08 00:45:00,67.5,18.262,42.195,29.509 -2020-07-08 01:00:00,68.65,18.267,38.82,29.509 -2020-07-08 01:15:00,68.23,17.430999999999997,38.82,29.509 -2020-07-08 01:30:00,67.33,15.877,38.82,29.509 -2020-07-08 01:45:00,67.32,15.865,38.82,29.509 -2020-07-08 02:00:00,67.95,15.613,37.023,29.509 -2020-07-08 02:15:00,67.93,14.513,37.023,29.509 -2020-07-08 02:30:00,75.12,16.292,37.023,29.509 -2020-07-08 02:45:00,73.61,17.028,37.023,29.509 -2020-07-08 03:00:00,77.07,18.215,36.818000000000005,29.509 -2020-07-08 03:15:00,72.12,17.471,36.818000000000005,29.509 -2020-07-08 03:30:00,73.91,16.761,36.818000000000005,29.509 -2020-07-08 03:45:00,71.79,16.022000000000002,36.818000000000005,29.509 -2020-07-08 04:00:00,77.39,20.733,37.495,29.509 -2020-07-08 04:15:00,82.0,26.840999999999998,37.495,29.509 -2020-07-08 04:30:00,84.87,24.173000000000002,37.495,29.509 -2020-07-08 04:45:00,83.56,24.188000000000002,37.495,29.509 -2020-07-08 05:00:00,86.92,36.789,39.858000000000004,29.509 -2020-07-08 05:15:00,93.46,43.852,39.858000000000004,29.509 -2020-07-08 05:30:00,93.59,38.008,39.858000000000004,29.509 -2020-07-08 05:45:00,96.52,35.519,39.858000000000004,29.509 -2020-07-08 06:00:00,99.68,36.399,52.867,29.509 -2020-07-08 06:15:00,102.4,35.982,52.867,29.509 -2020-07-08 06:30:00,108.54,35.269,52.867,29.509 -2020-07-08 06:45:00,113.47,37.338,52.867,29.509 -2020-07-08 07:00:00,108.0,37.436,66.061,29.509 -2020-07-08 07:15:00,102.54,37.89,66.061,29.509 -2020-07-08 07:30:00,100.01,35.586,66.061,29.509 -2020-07-08 07:45:00,101.07,35.809,66.061,29.509 -2020-07-08 08:00:00,108.39,31.736,58.532,29.509 -2020-07-08 08:15:00,116.1,34.872,58.532,29.509 -2020-07-08 08:30:00,117.34,36.025,58.532,29.509 -2020-07-08 08:45:00,115.72,38.645,58.532,29.509 -2020-07-08 09:00:00,111.76,32.743,56.047,29.509 -2020-07-08 09:15:00,108.9,32.047,56.047,29.509 -2020-07-08 09:30:00,108.08,35.893,56.047,29.509 -2020-07-08 09:45:00,116.56,39.406,56.047,29.509 -2020-07-08 10:00:00,120.29,36.609,53.823,29.509 -2020-07-08 10:15:00,118.95,38.266999999999996,53.823,29.509 -2020-07-08 10:30:00,112.11,38.305,53.823,29.509 -2020-07-08 10:45:00,109.74,39.623000000000005,53.823,29.509 -2020-07-08 11:00:00,109.06,37.376999999999995,54.184,29.509 -2020-07-08 11:15:00,108.4,38.705999999999996,54.184,29.509 -2020-07-08 11:30:00,105.6,40.0,54.184,29.509 -2020-07-08 11:45:00,107.2,41.326,54.184,29.509 -2020-07-08 12:00:00,114.53,36.522,52.628,29.509 -2020-07-08 12:15:00,112.75,36.016,52.628,29.509 -2020-07-08 12:30:00,101.99,34.808,52.628,29.509 -2020-07-08 12:45:00,99.23,35.94,52.628,29.509 -2020-07-08 13:00:00,100.2,36.167,52.31,29.509 -2020-07-08 13:15:00,102.73,37.527,52.31,29.509 -2020-07-08 13:30:00,102.61,35.765,52.31,29.509 -2020-07-08 13:45:00,102.47,35.391999999999996,52.31,29.509 -2020-07-08 14:00:00,116.5,37.185,52.278999999999996,29.509 -2020-07-08 14:15:00,112.73,36.115,52.278999999999996,29.509 -2020-07-08 14:30:00,108.85,35.069,52.278999999999996,29.509 -2020-07-08 14:45:00,109.45,35.644,52.278999999999996,29.509 -2020-07-08 15:00:00,101.37,37.671,53.306999999999995,29.509 -2020-07-08 15:15:00,100.94,35.272,53.306999999999995,29.509 -2020-07-08 15:30:00,99.62,33.659,53.306999999999995,29.509 -2020-07-08 15:45:00,97.35,31.93,53.306999999999995,29.509 -2020-07-08 16:00:00,93.45,34.812,55.358999999999995,29.509 -2020-07-08 16:15:00,94.89,34.528,55.358999999999995,29.509 -2020-07-08 16:30:00,98.99,33.643,55.358999999999995,29.509 -2020-07-08 16:45:00,99.62,30.389,55.358999999999995,29.509 -2020-07-08 17:00:00,98.93,34.132,59.211999999999996,29.509 -2020-07-08 17:15:00,98.18,34.126999999999995,59.211999999999996,29.509 -2020-07-08 17:30:00,98.72,32.994,59.211999999999996,29.509 -2020-07-08 17:45:00,100.38,31.776999999999997,59.211999999999996,29.509 -2020-07-08 18:00:00,99.58,34.832,60.403999999999996,29.509 -2020-07-08 18:15:00,99.0,34.734,60.403999999999996,29.509 -2020-07-08 18:30:00,100.18,32.691,60.403999999999996,29.509 -2020-07-08 18:45:00,98.99,35.751999999999995,60.403999999999996,29.509 -2020-07-08 19:00:00,96.37,36.931999999999995,60.993,29.509 -2020-07-08 19:15:00,92.43,35.945,60.993,29.509 -2020-07-08 19:30:00,91.2,34.844,60.993,29.509 -2020-07-08 19:45:00,90.88,34.098,60.993,29.509 -2020-07-08 20:00:00,90.29,31.255,66.6,29.509 -2020-07-08 20:15:00,88.95,30.811,66.6,29.509 -2020-07-08 20:30:00,89.13,31.355999999999998,66.6,29.509 -2020-07-08 20:45:00,91.03,31.976999999999997,66.6,29.509 -2020-07-08 21:00:00,89.33,30.351999999999997,59.855,29.509 -2020-07-08 21:15:00,87.63,31.965999999999998,59.855,29.509 -2020-07-08 21:30:00,85.13,32.796,59.855,29.509 -2020-07-08 21:45:00,83.76,33.858000000000004,59.855,29.509 -2020-07-08 22:00:00,81.36,31.031999999999996,54.942,29.509 -2020-07-08 22:15:00,79.43,33.68,54.942,29.509 -2020-07-08 22:30:00,76.01,29.427,54.942,29.509 -2020-07-08 22:45:00,74.2,26.193,54.942,29.509 -2020-07-08 23:00:00,74.17,22.959,46.056000000000004,29.509 -2020-07-08 23:15:00,72.84,21.531999999999996,46.056000000000004,29.509 -2020-07-08 23:30:00,72.63,20.379,46.056000000000004,29.509 -2020-07-08 23:45:00,72.27,19.425,46.056000000000004,29.509 -2020-07-09 00:00:00,70.77,18.729,40.859,29.509 -2020-07-09 00:15:00,69.68,19.587,40.859,29.509 -2020-07-09 00:30:00,68.62,18.339000000000002,40.859,29.509 -2020-07-09 00:45:00,68.26,18.276,40.859,29.509 -2020-07-09 01:00:00,68.62,18.287,39.06,29.509 -2020-07-09 01:15:00,67.79,17.444000000000003,39.06,29.509 -2020-07-09 01:30:00,66.52,15.892000000000001,39.06,29.509 -2020-07-09 01:45:00,67.76,15.872,39.06,29.509 -2020-07-09 02:00:00,68.72,15.626,37.592,29.509 -2020-07-09 02:15:00,67.64,14.513,37.592,29.509 -2020-07-09 02:30:00,67.33,16.301,37.592,29.509 -2020-07-09 02:45:00,67.89,17.039,37.592,29.509 -2020-07-09 03:00:00,69.13,18.219,37.416,29.509 -2020-07-09 03:15:00,70.24,17.485,37.416,29.509 -2020-07-09 03:30:00,71.26,16.783,37.416,29.509 -2020-07-09 03:45:00,72.08,16.06,37.416,29.509 -2020-07-09 04:00:00,76.24,20.737,38.176,29.509 -2020-07-09 04:15:00,75.02,26.815,38.176,29.509 -2020-07-09 04:30:00,77.62,24.14,38.176,29.509 -2020-07-09 04:45:00,80.21,24.154,38.176,29.509 -2020-07-09 05:00:00,85.53,36.703,41.203,29.509 -2020-07-09 05:15:00,89.78,43.691,41.203,29.509 -2020-07-09 05:30:00,93.38,37.9,41.203,29.509 -2020-07-09 05:45:00,101.99,35.434,41.203,29.509 -2020-07-09 06:00:00,106.23,36.31,51.09,29.509 -2020-07-09 06:15:00,109.0,35.891999999999996,51.09,29.509 -2020-07-09 06:30:00,106.78,35.196999999999996,51.09,29.509 -2020-07-09 06:45:00,104.81,37.294000000000004,51.09,29.509 -2020-07-09 07:00:00,106.36,37.384,63.541000000000004,29.509 -2020-07-09 07:15:00,110.69,37.859,63.541000000000004,29.509 -2020-07-09 07:30:00,112.06,35.56,63.541000000000004,29.509 -2020-07-09 07:45:00,118.5,35.809,63.541000000000004,29.509 -2020-07-09 08:00:00,121.66,31.746,55.65,29.509 -2020-07-09 08:15:00,122.22,34.893,55.65,29.509 -2020-07-09 08:30:00,118.81,36.037,55.65,29.509 -2020-07-09 08:45:00,116.9,38.649,55.65,29.509 -2020-07-09 09:00:00,117.8,32.743,51.833999999999996,29.509 -2020-07-09 09:15:00,120.31,32.048,51.833999999999996,29.509 -2020-07-09 09:30:00,120.69,35.887,51.833999999999996,29.509 -2020-07-09 09:45:00,124.35,39.402,51.833999999999996,29.509 -2020-07-09 10:00:00,130.54,36.613,49.70399999999999,29.509 -2020-07-09 10:15:00,131.59,38.268,49.70399999999999,29.509 -2020-07-09 10:30:00,127.98,38.302,49.70399999999999,29.509 -2020-07-09 10:45:00,122.86,39.621,49.70399999999999,29.509 -2020-07-09 11:00:00,122.53,37.374,48.593999999999994,29.509 -2020-07-09 11:15:00,121.63,38.705,48.593999999999994,29.509 -2020-07-09 11:30:00,121.85,39.986,48.593999999999994,29.509 -2020-07-09 11:45:00,120.39,41.306999999999995,48.593999999999994,29.509 -2020-07-09 12:00:00,120.74,36.523,46.275,29.509 -2020-07-09 12:15:00,120.27,36.015,46.275,29.509 -2020-07-09 12:30:00,117.63,34.798,46.275,29.509 -2020-07-09 12:45:00,114.26,35.927,46.275,29.509 -2020-07-09 13:00:00,109.73,36.135999999999996,45.803000000000004,29.509 -2020-07-09 13:15:00,108.7,37.491,45.803000000000004,29.509 -2020-07-09 13:30:00,113.78,35.734,45.803000000000004,29.509 -2020-07-09 13:45:00,117.0,35.368,45.803000000000004,29.509 -2020-07-09 14:00:00,110.72,37.163000000000004,46.251999999999995,29.509 -2020-07-09 14:15:00,111.27,36.095,46.251999999999995,29.509 -2020-07-09 14:30:00,112.08,35.039,46.251999999999995,29.509 -2020-07-09 14:45:00,109.67,35.62,46.251999999999995,29.509 -2020-07-09 15:00:00,109.08,37.655,48.309,29.509 -2020-07-09 15:15:00,104.89,35.251,48.309,29.509 -2020-07-09 15:30:00,100.7,33.638000000000005,48.309,29.509 -2020-07-09 15:45:00,104.1,31.903000000000002,48.309,29.509 -2020-07-09 16:00:00,103.6,34.797,49.681999999999995,29.509 -2020-07-09 16:15:00,105.47,34.515,49.681999999999995,29.509 -2020-07-09 16:30:00,100.69,33.648,49.681999999999995,29.509 -2020-07-09 16:45:00,101.97,30.396,49.681999999999995,29.509 -2020-07-09 17:00:00,103.65,34.143,53.086000000000006,29.509 -2020-07-09 17:15:00,103.74,34.149,53.086000000000006,29.509 -2020-07-09 17:30:00,105.78,33.018,53.086000000000006,29.509 -2020-07-09 17:45:00,108.82,31.804000000000002,53.086000000000006,29.509 -2020-07-09 18:00:00,108.39,34.865,54.038999999999994,29.509 -2020-07-09 18:15:00,108.03,34.75,54.038999999999994,29.509 -2020-07-09 18:30:00,106.59,32.711,54.038999999999994,29.509 -2020-07-09 18:45:00,103.0,35.77,54.038999999999994,29.509 -2020-07-09 19:00:00,100.88,36.952,53.408,29.509 -2020-07-09 19:15:00,97.96,35.959,53.408,29.509 -2020-07-09 19:30:00,94.18,34.851,53.408,29.509 -2020-07-09 19:45:00,92.67,34.101,53.408,29.509 -2020-07-09 20:00:00,90.99,31.249000000000002,55.309,29.509 -2020-07-09 20:15:00,90.23,30.802,55.309,29.509 -2020-07-09 20:30:00,90.8,31.345,55.309,29.509 -2020-07-09 20:45:00,92.1,31.975,55.309,29.509 -2020-07-09 21:00:00,89.34,30.354,51.585,29.509 -2020-07-09 21:15:00,87.34,31.968000000000004,51.585,29.509 -2020-07-09 21:30:00,84.56,32.778,51.585,29.509 -2020-07-09 21:45:00,83.79,33.833,51.585,29.509 -2020-07-09 22:00:00,80.79,31.013,48.006,29.509 -2020-07-09 22:15:00,78.88,33.661,48.006,29.509 -2020-07-09 22:30:00,76.76,29.392,48.006,29.509 -2020-07-09 22:45:00,75.3,26.151999999999997,48.006,29.509 -2020-07-09 23:00:00,73.05,22.921,42.309,29.509 -2020-07-09 23:15:00,72.92,21.516,42.309,29.509 -2020-07-09 23:30:00,71.81,20.377,42.309,29.509 -2020-07-09 23:45:00,70.78,19.418,42.309,29.509 -2020-07-10 00:00:00,75.87,16.908,39.649,29.509 -2020-07-10 00:15:00,78.17,17.980999999999998,39.649,29.509 -2020-07-10 00:30:00,75.85,17.027,39.649,29.509 -2020-07-10 00:45:00,71.62,17.408,39.649,29.509 -2020-07-10 01:00:00,69.81,17.038,37.744,29.509 -2020-07-10 01:15:00,68.97,15.522,37.744,29.509 -2020-07-10 01:30:00,67.92,14.729000000000001,37.744,29.509 -2020-07-10 01:45:00,69.22,14.513,37.744,29.509 -2020-07-10 02:00:00,68.97,15.165999999999999,36.965,29.509 -2020-07-10 02:15:00,71.25,14.513,36.965,29.509 -2020-07-10 02:30:00,75.07,16.63,36.965,29.509 -2020-07-10 02:45:00,74.85,16.655,36.965,29.509 -2020-07-10 03:00:00,75.76,18.677,37.678000000000004,29.509 -2020-07-10 03:15:00,71.23,16.657,37.678000000000004,29.509 -2020-07-10 03:30:00,77.96,15.714,37.678000000000004,29.509 -2020-07-10 03:45:00,73.99,15.908,37.678000000000004,29.509 -2020-07-10 04:00:00,73.54,20.688000000000002,38.591,29.509 -2020-07-10 04:15:00,74.61,25.119,38.591,29.509 -2020-07-10 04:30:00,77.28,23.421,38.591,29.509 -2020-07-10 04:45:00,79.7,22.803,38.591,29.509 -2020-07-10 05:00:00,86.23,34.849000000000004,40.666,29.509 -2020-07-10 05:15:00,91.4,42.71,40.666,29.509 -2020-07-10 05:30:00,95.84,37.144,40.666,29.509 -2020-07-10 05:45:00,103.23,34.24,40.666,29.509 -2020-07-10 06:00:00,106.94,35.33,51.784,29.509 -2020-07-10 06:15:00,106.98,35.125,51.784,29.509 -2020-07-10 06:30:00,107.3,34.42,51.784,29.509 -2020-07-10 06:45:00,110.82,36.363,51.784,29.509 -2020-07-10 07:00:00,103.93,37.14,61.383,29.509 -2020-07-10 07:15:00,103.93,38.543,61.383,29.509 -2020-07-10 07:30:00,100.62,34.223,61.383,29.509 -2020-07-10 07:45:00,107.12,34.312,61.383,29.509 -2020-07-10 08:00:00,114.53,31.116999999999997,55.272,29.509 -2020-07-10 08:15:00,110.12,34.992,55.272,29.509 -2020-07-10 08:30:00,106.93,35.983000000000004,55.272,29.509 -2020-07-10 08:45:00,109.04,38.505,55.272,29.509 -2020-07-10 09:00:00,114.76,30.16,53.506,29.509 -2020-07-10 09:15:00,113.51,31.453000000000003,53.506,29.509 -2020-07-10 09:30:00,110.89,34.580999999999996,53.506,29.509 -2020-07-10 09:45:00,107.02,38.509,53.506,29.509 -2020-07-10 10:00:00,113.21,35.611,51.363,29.509 -2020-07-10 10:15:00,111.84,36.994,51.363,29.509 -2020-07-10 10:30:00,112.42,37.61,51.363,29.509 -2020-07-10 10:45:00,110.71,38.852,51.363,29.509 -2020-07-10 11:00:00,112.67,36.874,51.043,29.509 -2020-07-10 11:15:00,113.11,37.103,51.043,29.509 -2020-07-10 11:30:00,107.88,37.903,51.043,29.509 -2020-07-10 11:45:00,109.91,38.202,51.043,29.509 -2020-07-10 12:00:00,111.54,33.830999999999996,47.52,29.509 -2020-07-10 12:15:00,112.77,32.756,47.52,29.509 -2020-07-10 12:30:00,109.24,31.646,47.52,29.509 -2020-07-10 12:45:00,105.69,31.91,47.52,29.509 -2020-07-10 13:00:00,106.61,32.681,45.494,29.509 -2020-07-10 13:15:00,112.86,34.179,45.494,29.509 -2020-07-10 13:30:00,115.59,33.268,45.494,29.509 -2020-07-10 13:45:00,109.93,33.236999999999995,45.494,29.509 -2020-07-10 14:00:00,105.89,34.242,43.883,29.509 -2020-07-10 14:15:00,101.49,33.646,43.883,29.509 -2020-07-10 14:30:00,95.86,34.148,43.883,29.509 -2020-07-10 14:45:00,97.16,33.991,43.883,29.509 -2020-07-10 15:00:00,94.55,35.974000000000004,45.714,29.509 -2020-07-10 15:15:00,89.18,33.336999999999996,45.714,29.509 -2020-07-10 15:30:00,91.05,31.205,45.714,29.509 -2020-07-10 15:45:00,100.25,30.261999999999997,45.714,29.509 -2020-07-10 16:00:00,97.71,32.321999999999996,48.222,29.509 -2020-07-10 16:15:00,101.07,32.548,48.222,29.509 -2020-07-10 16:30:00,98.92,31.503,48.222,29.509 -2020-07-10 16:45:00,98.38,27.433000000000003,48.222,29.509 -2020-07-10 17:00:00,103.96,32.991,52.619,29.509 -2020-07-10 17:15:00,105.3,32.848,52.619,29.509 -2020-07-10 17:30:00,104.89,31.909000000000002,52.619,29.509 -2020-07-10 17:45:00,101.36,30.537,52.619,29.509 -2020-07-10 18:00:00,102.68,33.596,52.99,29.509 -2020-07-10 18:15:00,105.55,32.494,52.99,29.509 -2020-07-10 18:30:00,106.3,30.329,52.99,29.509 -2020-07-10 18:45:00,103.31,33.828,52.99,29.509 -2020-07-10 19:00:00,97.76,35.876,51.923,29.509 -2020-07-10 19:15:00,92.42,35.438,51.923,29.509 -2020-07-10 19:30:00,92.95,34.405,51.923,29.509 -2020-07-10 19:45:00,97.4,32.611,51.923,29.509 -2020-07-10 20:00:00,95.69,29.585,56.238,29.509 -2020-07-10 20:15:00,92.52,29.965999999999998,56.238,29.509 -2020-07-10 20:30:00,86.88,30.0,56.238,29.509 -2020-07-10 20:45:00,87.29,29.768,56.238,29.509 -2020-07-10 21:00:00,83.47,29.494,52.426,29.509 -2020-07-10 21:15:00,83.93,32.832,52.426,29.509 -2020-07-10 21:30:00,80.36,33.465,52.426,29.509 -2020-07-10 21:45:00,84.98,34.704,52.426,29.509 -2020-07-10 22:00:00,82.87,31.701999999999998,48.196000000000005,29.509 -2020-07-10 22:15:00,79.75,34.096,48.196000000000005,29.509 -2020-07-10 22:30:00,73.88,34.513000000000005,48.196000000000005,29.509 -2020-07-10 22:45:00,70.67,32.027,48.196000000000005,29.509 -2020-07-10 23:00:00,71.98,30.574,41.71,29.509 -2020-07-10 23:15:00,75.31,27.635,41.71,29.509 -2020-07-10 23:30:00,73.88,24.671999999999997,41.71,29.509 -2020-07-10 23:45:00,69.5,23.603,41.71,29.509 -2020-07-11 00:00:00,66.78,18.323,41.105,29.398000000000003 -2020-07-11 00:15:00,65.61,18.863,41.105,29.398000000000003 -2020-07-11 00:30:00,68.91,17.372,41.105,29.398000000000003 -2020-07-11 00:45:00,71.3,16.97,41.105,29.398000000000003 -2020-07-11 01:00:00,71.31,16.866,36.934,29.398000000000003 -2020-07-11 01:15:00,66.13,15.995999999999999,36.934,29.398000000000003 -2020-07-11 01:30:00,62.39,14.513,36.934,29.398000000000003 -2020-07-11 01:45:00,65.34,15.394,36.934,29.398000000000003 -2020-07-11 02:00:00,69.2,15.074000000000002,34.782,29.398000000000003 -2020-07-11 02:15:00,69.08,14.513,34.782,29.398000000000003 -2020-07-11 02:30:00,66.46,14.948,34.782,29.398000000000003 -2020-07-11 02:45:00,61.07,15.79,34.782,29.398000000000003 -2020-07-11 03:00:00,61.65,16.352,34.489000000000004,29.398000000000003 -2020-07-11 03:15:00,67.74,14.513,34.489000000000004,29.398000000000003 -2020-07-11 03:30:00,68.92,14.513,34.489000000000004,29.398000000000003 -2020-07-11 03:45:00,67.2,14.882,34.489000000000004,29.398000000000003 -2020-07-11 04:00:00,61.39,17.837,34.111,29.398000000000003 -2020-07-11 04:15:00,62.51,21.416,34.111,29.398000000000003 -2020-07-11 04:30:00,67.0,18.064,34.111,29.398000000000003 -2020-07-11 04:45:00,68.68,17.75,34.111,29.398000000000003 -2020-07-11 05:00:00,67.76,21.743000000000002,33.283,29.398000000000003 -2020-07-11 05:15:00,65.79,18.933,33.283,29.398000000000003 -2020-07-11 05:30:00,71.55,14.975999999999999,33.283,29.398000000000003 -2020-07-11 05:45:00,73.19,16.502,33.283,29.398000000000003 -2020-07-11 06:00:00,72.98,28.447,33.653,29.398000000000003 -2020-07-11 06:15:00,70.87,35.666,33.653,29.398000000000003 -2020-07-11 06:30:00,69.27,32.253,33.653,29.398000000000003 -2020-07-11 06:45:00,72.03,31.250999999999998,33.653,29.398000000000003 -2020-07-11 07:00:00,78.34,30.91,36.732,29.398000000000003 -2020-07-11 07:15:00,79.62,31.045,36.732,29.398000000000003 -2020-07-11 07:30:00,79.34,28.175,36.732,29.398000000000003 -2020-07-11 07:45:00,76.51,29.061,36.732,29.398000000000003 -2020-07-11 08:00:00,81.35,26.475,41.318999999999996,29.398000000000003 -2020-07-11 08:15:00,81.1,29.944000000000003,41.318999999999996,29.398000000000003 -2020-07-11 08:30:00,79.87,30.825,41.318999999999996,29.398000000000003 -2020-07-11 08:45:00,74.11,34.164,41.318999999999996,29.398000000000003 -2020-07-11 09:00:00,76.56,29.0,43.195,29.398000000000003 -2020-07-11 09:15:00,73.1,30.758000000000003,43.195,29.398000000000003 -2020-07-11 09:30:00,79.0,34.347,43.195,29.398000000000003 -2020-07-11 09:45:00,80.51,37.795,43.195,29.398000000000003 -2020-07-11 10:00:00,81.2,35.568000000000005,41.843999999999994,29.398000000000003 -2020-07-11 10:15:00,76.6,37.312,41.843999999999994,29.398000000000003 -2020-07-11 10:30:00,80.08,37.554,41.843999999999994,29.398000000000003 -2020-07-11 10:45:00,79.57,38.348,41.843999999999994,29.398000000000003 -2020-07-11 11:00:00,72.28,36.21,39.035,29.398000000000003 -2020-07-11 11:15:00,72.4,37.393,39.035,29.398000000000003 -2020-07-11 11:30:00,66.56,38.545,39.035,29.398000000000003 -2020-07-11 11:45:00,64.38,39.599000000000004,39.035,29.398000000000003 -2020-07-11 12:00:00,63.06,35.757,38.001,29.398000000000003 -2020-07-11 12:15:00,61.1,35.538000000000004,38.001,29.398000000000003 -2020-07-11 12:30:00,60.22,34.263000000000005,38.001,29.398000000000003 -2020-07-11 12:45:00,59.31,35.336999999999996,38.001,29.398000000000003 -2020-07-11 13:00:00,58.37,35.196999999999996,34.747,29.398000000000003 -2020-07-11 13:15:00,58.07,36.245,34.747,29.398000000000003 -2020-07-11 13:30:00,57.74,35.563,34.747,29.398000000000003 -2020-07-11 13:45:00,57.83,34.092,34.747,29.398000000000003 -2020-07-11 14:00:00,58.61,35.078,33.434,29.398000000000003 -2020-07-11 14:15:00,58.61,33.199,33.434,29.398000000000003 -2020-07-11 14:30:00,59.59,33.353,33.434,29.398000000000003 -2020-07-11 14:45:00,60.3,33.694,33.434,29.398000000000003 -2020-07-11 15:00:00,61.37,36.101,35.921,29.398000000000003 -2020-07-11 15:15:00,61.08,34.154,35.921,29.398000000000003 -2020-07-11 15:30:00,61.78,32.147,35.921,29.398000000000003 -2020-07-11 15:45:00,62.97,30.228,35.921,29.398000000000003 -2020-07-11 16:00:00,64.97,34.564,39.427,29.398000000000003 -2020-07-11 16:15:00,65.55,33.809,39.427,29.398000000000003 -2020-07-11 16:30:00,67.43,33.021,39.427,29.398000000000003 -2020-07-11 16:45:00,69.59,28.877,39.427,29.398000000000003 -2020-07-11 17:00:00,71.58,33.213,44.096000000000004,29.398000000000003 -2020-07-11 17:15:00,72.95,30.74,44.096000000000004,29.398000000000003 -2020-07-11 17:30:00,74.4,29.671,44.096000000000004,29.398000000000003 -2020-07-11 17:45:00,76.23,28.840999999999998,44.096000000000004,29.398000000000003 -2020-07-11 18:00:00,76.65,33.328,43.931000000000004,29.398000000000003 -2020-07-11 18:15:00,76.76,33.864000000000004,43.931000000000004,29.398000000000003 -2020-07-11 18:30:00,77.06,33.051,43.931000000000004,29.398000000000003 -2020-07-11 18:45:00,77.28,33.094,43.931000000000004,29.398000000000003 -2020-07-11 19:00:00,75.61,33.381,42.187,29.398000000000003 -2020-07-11 19:15:00,73.06,31.915,42.187,29.398000000000003 -2020-07-11 19:30:00,72.09,31.64,42.187,29.398000000000003 -2020-07-11 19:45:00,72.06,31.671,42.187,29.398000000000003 -2020-07-11 20:00:00,71.02,29.423000000000002,38.315,29.398000000000003 -2020-07-11 20:15:00,70.88,29.055,38.315,29.398000000000003 -2020-07-11 20:30:00,71.67,28.191999999999997,38.315,29.398000000000003 -2020-07-11 20:45:00,73.05,29.901999999999997,38.315,29.398000000000003 -2020-07-11 21:00:00,72.16,28.011999999999997,36.843,29.398000000000003 -2020-07-11 21:15:00,70.77,30.971999999999998,36.843,29.398000000000003 -2020-07-11 21:30:00,69.19,31.735,36.843,29.398000000000003 -2020-07-11 21:45:00,68.12,32.418,36.843,29.398000000000003 -2020-07-11 22:00:00,65.88,29.346,37.260999999999996,29.398000000000003 -2020-07-11 22:15:00,64.83,31.903000000000002,37.260999999999996,29.398000000000003 -2020-07-11 22:30:00,63.5,31.685,37.260999999999996,29.398000000000003 -2020-07-11 22:45:00,61.86,29.505,37.260999999999996,29.398000000000003 -2020-07-11 23:00:00,60.37,27.302,32.148,29.398000000000003 -2020-07-11 23:15:00,59.02,24.873,32.148,29.398000000000003 -2020-07-11 23:30:00,58.04,24.506999999999998,32.148,29.398000000000003 -2020-07-11 23:45:00,57.38,23.886,32.148,29.398000000000003 -2020-07-12 00:00:00,56.58,19.749000000000002,28.905,29.398000000000003 -2020-07-12 00:15:00,55.15,19.109,28.905,29.398000000000003 -2020-07-12 00:30:00,55.08,17.485,28.905,29.398000000000003 -2020-07-12 00:45:00,55.02,16.945,28.905,29.398000000000003 -2020-07-12 01:00:00,54.63,17.125999999999998,26.906999999999996,29.398000000000003 -2020-07-12 01:15:00,54.08,16.081,26.906999999999996,29.398000000000003 -2020-07-12 01:30:00,54.07,14.513,26.906999999999996,29.398000000000003 -2020-07-12 01:45:00,53.22,14.932,26.906999999999996,29.398000000000003 -2020-07-12 02:00:00,53.3,14.73,25.938000000000002,29.398000000000003 -2020-07-12 02:15:00,52.94,14.513,25.938000000000002,29.398000000000003 -2020-07-12 02:30:00,53.05,15.523,25.938000000000002,29.398000000000003 -2020-07-12 02:45:00,52.79,16.05,25.938000000000002,29.398000000000003 -2020-07-12 03:00:00,53.08,17.264,24.693,29.398000000000003 -2020-07-12 03:15:00,52.25,14.882,24.693,29.398000000000003 -2020-07-12 03:30:00,52.6,14.513,24.693,29.398000000000003 -2020-07-12 03:45:00,52.24,14.513,24.693,29.398000000000003 -2020-07-12 04:00:00,51.42,17.385,25.683000000000003,29.398000000000003 -2020-07-12 04:15:00,51.0,20.531,25.683000000000003,29.398000000000003 -2020-07-12 04:30:00,50.5,18.554000000000002,25.683000000000003,29.398000000000003 -2020-07-12 04:45:00,50.97,17.746,25.683000000000003,29.398000000000003 -2020-07-12 05:00:00,51.24,22.005,26.023000000000003,29.398000000000003 -2020-07-12 05:15:00,51.43,18.430999999999997,26.023000000000003,29.398000000000003 -2020-07-12 05:30:00,51.69,14.513,26.023000000000003,29.398000000000003 -2020-07-12 05:45:00,52.25,15.375,26.023000000000003,29.398000000000003 -2020-07-12 06:00:00,52.24,24.9,25.834,29.398000000000003 -2020-07-12 06:15:00,52.9,33.04,25.834,29.398000000000003 -2020-07-12 06:30:00,53.7,28.971,25.834,29.398000000000003 -2020-07-12 06:45:00,55.15,27.016,25.834,29.398000000000003 -2020-07-12 07:00:00,54.82,26.895,27.765,29.398000000000003 -2020-07-12 07:15:00,55.23,25.395,27.765,29.398000000000003 -2020-07-12 07:30:00,56.51,23.913,27.765,29.398000000000003 -2020-07-12 07:45:00,57.17,24.826999999999998,27.765,29.398000000000003 -2020-07-12 08:00:00,55.26,22.965,31.357,29.398000000000003 -2020-07-12 08:15:00,55.02,27.676,31.357,29.398000000000003 -2020-07-12 08:30:00,54.22,29.362,31.357,29.398000000000003 -2020-07-12 08:45:00,55.5,32.498000000000005,31.357,29.398000000000003 -2020-07-12 09:00:00,56.55,27.249000000000002,33.238,29.398000000000003 -2020-07-12 09:15:00,54.44,28.454,33.238,29.398000000000003 -2020-07-12 09:30:00,53.99,32.496,33.238,29.398000000000003 -2020-07-12 09:45:00,54.89,37.0,33.238,29.398000000000003 -2020-07-12 10:00:00,55.54,35.193000000000005,34.22,29.398000000000003 -2020-07-12 10:15:00,57.68,37.039,34.22,29.398000000000003 -2020-07-12 10:30:00,59.4,37.47,34.22,29.398000000000003 -2020-07-12 10:45:00,58.87,39.461,34.22,29.398000000000003 -2020-07-12 11:00:00,56.65,36.895,36.298,29.398000000000003 -2020-07-12 11:15:00,55.61,37.598,36.298,29.398000000000003 -2020-07-12 11:30:00,53.69,39.349000000000004,36.298,29.398000000000003 -2020-07-12 11:45:00,51.82,40.623000000000005,36.298,29.398000000000003 -2020-07-12 12:00:00,49.79,37.971,33.52,29.398000000000003 -2020-07-12 12:15:00,48.34,36.944,33.52,29.398000000000003 -2020-07-12 12:30:00,47.06,35.995,33.52,29.398000000000003 -2020-07-12 12:45:00,46.84,36.466,33.52,29.398000000000003 -2020-07-12 13:00:00,45.47,36.035,30.12,29.398000000000003 -2020-07-12 13:15:00,44.9,36.248000000000005,30.12,29.398000000000003 -2020-07-12 13:30:00,46.01,34.412,30.12,29.398000000000003 -2020-07-12 13:45:00,48.63,34.175,30.12,29.398000000000003 -2020-07-12 14:00:00,47.77,36.409,27.233,29.398000000000003 -2020-07-12 14:15:00,50.67,34.899,27.233,29.398000000000003 -2020-07-12 14:30:00,49.51,33.652,27.233,29.398000000000003 -2020-07-12 14:45:00,50.09,32.899,27.233,29.398000000000003 -2020-07-12 15:00:00,49.11,35.577,27.468000000000004,29.398000000000003 -2020-07-12 15:15:00,51.61,32.717,27.468000000000004,29.398000000000003 -2020-07-12 15:30:00,52.28,30.478,27.468000000000004,29.398000000000003 -2020-07-12 15:45:00,54.69,28.804000000000002,27.468000000000004,29.398000000000003 -2020-07-12 16:00:00,56.69,31.328000000000003,30.8,29.398000000000003 -2020-07-12 16:15:00,57.79,30.877,30.8,29.398000000000003 -2020-07-12 16:30:00,60.36,31.142,30.8,29.398000000000003 -2020-07-12 16:45:00,62.51,27.061999999999998,30.8,29.398000000000003 -2020-07-12 17:00:00,64.81,31.787,37.806,29.398000000000003 -2020-07-12 17:15:00,65.45,30.935,37.806,29.398000000000003 -2020-07-12 17:30:00,67.71,30.695999999999998,37.806,29.398000000000003 -2020-07-12 17:45:00,68.61,30.166999999999998,37.806,29.398000000000003 -2020-07-12 18:00:00,71.24,35.333,40.766,29.398000000000003 -2020-07-12 18:15:00,71.8,35.356,40.766,29.398000000000003 -2020-07-12 18:30:00,72.57,34.407,40.766,29.398000000000003 -2020-07-12 18:45:00,72.89,34.463,40.766,29.398000000000003 -2020-07-12 19:00:00,72.83,37.047,41.163000000000004,29.398000000000003 -2020-07-12 19:15:00,71.76,34.399,41.163000000000004,29.398000000000003 -2020-07-12 19:30:00,70.93,33.869,41.163000000000004,29.398000000000003 -2020-07-12 19:45:00,71.3,33.354,41.163000000000004,29.398000000000003 -2020-07-12 20:00:00,68.86,31.265,39.885999999999996,29.398000000000003 -2020-07-12 20:15:00,69.89,30.703000000000003,39.885999999999996,29.398000000000003 -2020-07-12 20:30:00,71.35,30.611,39.885999999999996,29.398000000000003 -2020-07-12 20:45:00,74.04,30.651,39.885999999999996,29.398000000000003 -2020-07-12 21:00:00,76.76,28.779,38.900999999999996,29.398000000000003 -2020-07-12 21:15:00,76.45,31.459,38.900999999999996,29.398000000000003 -2020-07-12 21:30:00,75.21,31.52,38.900999999999996,29.398000000000003 -2020-07-12 21:45:00,73.92,32.556,38.900999999999996,29.398000000000003 -2020-07-12 22:00:00,72.32,31.649,39.806999999999995,29.398000000000003 -2020-07-12 22:15:00,71.26,32.507,39.806999999999995,29.398000000000003 -2020-07-12 22:30:00,70.12,31.899,39.806999999999995,29.398000000000003 -2020-07-12 22:45:00,69.17,28.441999999999997,39.806999999999995,29.398000000000003 -2020-07-12 23:00:00,66.25,26.066999999999997,35.564,29.398000000000003 -2020-07-12 23:15:00,66.98,24.875999999999998,35.564,29.398000000000003 -2020-07-12 23:30:00,67.33,23.935,35.564,29.398000000000003 -2020-07-12 23:45:00,65.65,23.429000000000002,35.564,29.398000000000003 -2020-07-13 00:00:00,63.25,21.121,36.578,29.509 -2020-07-13 00:15:00,63.73,21.145,36.578,29.509 -2020-07-13 00:30:00,63.24,19.121,36.578,29.509 -2020-07-13 00:45:00,64.24,18.207,36.578,29.509 -2020-07-13 01:00:00,62.98,18.792,35.292,29.509 -2020-07-13 01:15:00,64.06,17.78,35.292,29.509 -2020-07-13 01:30:00,62.35,16.386,35.292,29.509 -2020-07-13 01:45:00,62.1,16.892,35.292,29.509 -2020-07-13 02:00:00,62.13,17.167,34.319,29.509 -2020-07-13 02:15:00,63.93,14.513,34.319,29.509 -2020-07-13 02:30:00,62.7,16.987000000000002,34.319,29.509 -2020-07-13 02:45:00,68.66,17.396,34.319,29.509 -2020-07-13 03:00:00,71.71,19.061,33.13,29.509 -2020-07-13 03:15:00,70.72,17.35,33.13,29.509 -2020-07-13 03:30:00,68.96,16.737000000000002,33.13,29.509 -2020-07-13 03:45:00,68.2,17.241,33.13,29.509 -2020-07-13 04:00:00,71.65,23.105999999999998,33.851,29.509 -2020-07-13 04:15:00,75.48,29.046,33.851,29.509 -2020-07-13 04:30:00,81.43,26.454,33.851,29.509 -2020-07-13 04:45:00,88.04,26.021,33.851,29.509 -2020-07-13 05:00:00,87.82,36.985,38.718,29.509 -2020-07-13 05:15:00,92.38,43.06399999999999,38.718,29.509 -2020-07-13 05:30:00,92.03,37.214,38.718,29.509 -2020-07-13 05:45:00,94.27,35.575,38.718,29.509 -2020-07-13 06:00:00,105.23,35.279,51.648999999999994,29.509 -2020-07-13 06:15:00,111.06,34.735,51.648999999999994,29.509 -2020-07-13 06:30:00,115.27,34.412,51.648999999999994,29.509 -2020-07-13 06:45:00,111.43,37.519,51.648999999999994,29.509 -2020-07-13 07:00:00,115.92,37.404,60.159,29.509 -2020-07-13 07:15:00,114.27,38.225,60.159,29.509 -2020-07-13 07:30:00,121.23,35.899,60.159,29.509 -2020-07-13 07:45:00,121.9,37.286,60.159,29.509 -2020-07-13 08:00:00,121.83,33.344,53.8,29.509 -2020-07-13 08:15:00,117.34,36.936,53.8,29.509 -2020-07-13 08:30:00,119.03,37.851,53.8,29.509 -2020-07-13 08:45:00,125.58,41.396,53.8,29.509 -2020-07-13 09:00:00,127.01,35.063,50.583,29.509 -2020-07-13 09:15:00,124.63,34.619,50.583,29.509 -2020-07-13 09:30:00,120.25,37.743,50.583,29.509 -2020-07-13 09:45:00,127.67,39.821999999999996,50.583,29.509 -2020-07-13 10:00:00,129.63,38.476,49.11600000000001,29.509 -2020-07-13 10:15:00,127.89,40.185,49.11600000000001,29.509 -2020-07-13 10:30:00,123.11,40.189,49.11600000000001,29.509 -2020-07-13 10:45:00,122.39,40.455,49.11600000000001,29.509 -2020-07-13 11:00:00,118.84,38.315,49.056000000000004,29.509 -2020-07-13 11:15:00,121.61,39.187,49.056000000000004,29.509 -2020-07-13 11:30:00,114.5,41.566,49.056000000000004,29.509 -2020-07-13 11:45:00,110.96,43.393,49.056000000000004,29.509 -2020-07-13 12:00:00,107.31,38.772,47.227,29.509 -2020-07-13 12:15:00,105.66,37.86,47.227,29.509 -2020-07-13 12:30:00,102.31,35.75,47.227,29.509 -2020-07-13 12:45:00,93.18,36.098,47.227,29.509 -2020-07-13 13:00:00,92.65,36.595,47.006,29.509 -2020-07-13 13:15:00,92.56,35.967,47.006,29.509 -2020-07-13 13:30:00,91.16,34.355,47.006,29.509 -2020-07-13 13:45:00,91.85,35.071999999999996,47.006,29.509 -2020-07-13 14:00:00,90.82,36.405,47.19,29.509 -2020-07-13 14:15:00,91.72,35.571999999999996,47.19,29.509 -2020-07-13 14:30:00,90.69,34.181999999999995,47.19,29.509 -2020-07-13 14:45:00,90.03,35.629,47.19,29.509 -2020-07-13 15:00:00,93.94,37.814,47.846000000000004,29.509 -2020-07-13 15:15:00,99.23,34.43,47.846000000000004,29.509 -2020-07-13 15:30:00,95.18,33.066,47.846000000000004,29.509 -2020-07-13 15:45:00,96.44,30.903000000000002,47.846000000000004,29.509 -2020-07-13 16:00:00,96.49,34.625,49.641000000000005,29.509 -2020-07-13 16:15:00,95.72,34.295,49.641000000000005,29.509 -2020-07-13 16:30:00,96.85,33.941,49.641000000000005,29.509 -2020-07-13 16:45:00,94.4,29.968000000000004,49.641000000000005,29.509 -2020-07-13 17:00:00,98.43,33.529,54.133,29.509 -2020-07-13 17:15:00,98.75,33.147,54.133,29.509 -2020-07-13 17:30:00,99.74,32.542,54.133,29.509 -2020-07-13 17:45:00,98.7,31.698,54.133,29.509 -2020-07-13 18:00:00,102.1,35.762,53.761,29.509 -2020-07-13 18:15:00,99.07,33.991,53.761,29.509 -2020-07-13 18:30:00,100.45,32.218,53.761,29.509 -2020-07-13 18:45:00,100.43,35.531,53.761,29.509 -2020-07-13 19:00:00,97.31,37.945,53.923,29.509 -2020-07-13 19:15:00,93.81,36.722,53.923,29.509 -2020-07-13 19:30:00,93.3,35.78,53.923,29.509 -2020-07-13 19:45:00,91.72,34.668,53.923,29.509 -2020-07-13 20:00:00,90.68,31.375,58.786,29.509 -2020-07-13 20:15:00,90.77,32.399,58.786,29.509 -2020-07-13 20:30:00,90.87,32.946,58.786,29.509 -2020-07-13 20:45:00,91.51,33.126999999999995,58.786,29.509 -2020-07-13 21:00:00,89.62,30.568,54.591,29.509 -2020-07-13 21:15:00,88.41,33.764,54.591,29.509 -2020-07-13 21:30:00,85.66,34.279,54.591,29.509 -2020-07-13 21:45:00,85.22,35.104,54.591,29.509 -2020-07-13 22:00:00,79.21,32.213,51.551,29.509 -2020-07-13 22:15:00,80.28,35.194,51.551,29.509 -2020-07-13 22:30:00,79.16,30.554000000000002,51.551,29.509 -2020-07-13 22:45:00,78.47,27.307,51.551,29.509 -2020-07-13 23:00:00,72.96,24.906999999999996,44.716,29.509 -2020-07-13 23:15:00,73.07,21.904,44.716,29.509 -2020-07-13 23:30:00,69.81,20.805,44.716,29.509 -2020-07-13 23:45:00,72.76,19.611,44.716,29.509 -2020-07-14 00:00:00,67.54,18.816,43.01,29.509 -2020-07-14 00:15:00,69.07,19.663,43.01,29.509 -2020-07-14 00:30:00,66.45,18.436,43.01,29.509 -2020-07-14 00:45:00,70.02,18.406,43.01,29.509 -2020-07-14 01:00:00,69.42,18.439,40.687,29.509 -2020-07-14 01:15:00,69.69,17.563,40.687,29.509 -2020-07-14 01:30:00,67.81,16.028,40.687,29.509 -2020-07-14 01:45:00,69.25,15.975999999999999,40.687,29.509 -2020-07-14 02:00:00,68.49,15.753,39.554,29.509 -2020-07-14 02:15:00,69.32,14.513,39.554,29.509 -2020-07-14 02:30:00,69.78,16.408,39.554,29.509 -2020-07-14 02:45:00,69.55,17.16,39.554,29.509 -2020-07-14 03:00:00,70.53,18.301,38.958,29.509 -2020-07-14 03:15:00,71.95,17.624000000000002,38.958,29.509 -2020-07-14 03:30:00,71.88,16.959,38.958,29.509 -2020-07-14 03:45:00,75.76,16.305,38.958,29.509 -2020-07-14 04:00:00,74.52,20.831999999999997,39.783,29.509 -2020-07-14 04:15:00,75.95,26.779,39.783,29.509 -2020-07-14 04:30:00,79.03,24.072,39.783,29.509 -2020-07-14 04:45:00,81.77,24.083000000000002,39.783,29.509 -2020-07-14 05:00:00,88.06,36.406,42.281000000000006,29.509 -2020-07-14 05:15:00,90.34,43.077,42.281000000000006,29.509 -2020-07-14 05:30:00,92.69,37.535,42.281000000000006,29.509 -2020-07-14 05:45:00,98.51,35.157,42.281000000000006,29.509 -2020-07-14 06:00:00,109.3,36.005,50.801,29.509 -2020-07-14 06:15:00,108.62,35.589,50.801,29.509 -2020-07-14 06:30:00,105.84,34.981,50.801,29.509 -2020-07-14 06:45:00,105.08,37.208,50.801,29.509 -2020-07-14 07:00:00,116.07,37.256,60.202,29.509 -2020-07-14 07:15:00,115.56,37.838,60.202,29.509 -2020-07-14 07:30:00,111.32,35.568000000000005,60.202,29.509 -2020-07-14 07:45:00,103.14,35.945,60.202,29.509 -2020-07-14 08:00:00,106.06,31.928,54.461000000000006,29.509 -2020-07-14 08:15:00,103.57,35.115,54.461000000000006,29.509 -2020-07-14 08:30:00,103.42,36.209,54.461000000000006,29.509 -2020-07-14 08:45:00,106.65,38.788000000000004,54.461000000000006,29.509 -2020-07-14 09:00:00,110.63,32.869,50.753,29.509 -2020-07-14 09:15:00,110.97,32.167,50.753,29.509 -2020-07-14 09:30:00,104.6,35.965,50.753,29.509 -2020-07-14 09:45:00,103.21,39.481,50.753,29.509 -2020-07-14 10:00:00,108.94,36.727,49.703,29.509 -2020-07-14 10:15:00,110.62,38.361,49.703,29.509 -2020-07-14 10:30:00,109.15,38.375,49.703,29.509 -2020-07-14 10:45:00,102.77,39.693000000000005,49.703,29.509 -2020-07-14 11:00:00,100.68,37.448,49.42100000000001,29.509 -2020-07-14 11:15:00,104.47,38.778,49.42100000000001,29.509 -2020-07-14 11:30:00,103.85,40.007,49.42100000000001,29.509 -2020-07-14 11:45:00,104.68,41.293,49.42100000000001,29.509 -2020-07-14 12:00:00,101.13,36.594,47.155,29.509 -2020-07-14 12:15:00,100.12,36.073,47.155,29.509 -2020-07-14 12:30:00,97.38,34.82,47.155,29.509 -2020-07-14 12:45:00,103.21,35.93,47.155,29.509 -2020-07-14 13:00:00,104.0,36.05,47.515,29.509 -2020-07-14 13:15:00,102.08,37.375,47.515,29.509 -2020-07-14 13:30:00,96.92,35.641,47.515,29.509 -2020-07-14 13:45:00,97.9,35.316,47.515,29.509 -2020-07-14 14:00:00,96.33,37.105,47.575,29.509 -2020-07-14 14:15:00,101.9,36.049,47.575,29.509 -2020-07-14 14:30:00,99.58,34.953,47.575,29.509 -2020-07-14 14:45:00,98.18,35.567,47.575,29.509 -2020-07-14 15:00:00,93.03,37.619,48.903,29.509 -2020-07-14 15:15:00,90.27,35.195,48.903,29.509 -2020-07-14 15:30:00,93.61,33.589,48.903,29.509 -2020-07-14 15:45:00,99.29,31.824,48.903,29.509 -2020-07-14 16:00:00,102.48,34.764,50.218999999999994,29.509 -2020-07-14 16:15:00,101.61,34.497,50.218999999999994,29.509 -2020-07-14 16:30:00,97.93,33.715,50.218999999999994,29.509 -2020-07-14 16:45:00,99.0,30.489,50.218999999999994,29.509 -2020-07-14 17:00:00,100.95,34.236999999999995,55.396,29.509 -2020-07-14 17:15:00,101.47,34.311,55.396,29.509 -2020-07-14 17:30:00,101.51,33.196999999999996,55.396,29.509 -2020-07-14 17:45:00,104.76,32.015,55.396,29.509 -2020-07-14 18:00:00,107.85,35.096,55.583999999999996,29.509 -2020-07-14 18:15:00,108.01,34.913000000000004,55.583999999999996,29.509 -2020-07-14 18:30:00,105.94,32.885999999999996,55.583999999999996,29.509 -2020-07-14 18:45:00,101.24,35.943000000000005,55.583999999999996,29.509 -2020-07-14 19:00:00,102.79,37.137,56.071000000000005,29.509 -2020-07-14 19:15:00,102.8,36.111,56.071000000000005,29.509 -2020-07-14 19:30:00,101.74,34.975,56.071000000000005,29.509 -2020-07-14 19:45:00,95.47,34.209,56.071000000000005,29.509 -2020-07-14 20:00:00,91.86,31.309,61.55,29.509 -2020-07-14 20:15:00,91.07,30.857,61.55,29.509 -2020-07-14 20:30:00,91.99,31.381,61.55,29.509 -2020-07-14 20:45:00,93.25,32.04,61.55,29.509 -2020-07-14 21:00:00,90.18,30.436,55.94,29.509 -2020-07-14 21:15:00,89.36,32.047,55.94,29.509 -2020-07-14 21:30:00,86.61,32.768,55.94,29.509 -2020-07-14 21:45:00,85.12,33.775,55.94,29.509 -2020-07-14 22:00:00,80.32,30.973000000000003,52.857,29.509 -2020-07-14 22:15:00,79.79,33.615,52.857,29.509 -2020-07-14 22:30:00,78.37,29.256999999999998,52.857,29.509 -2020-07-14 22:45:00,77.97,25.984,52.857,29.509 -2020-07-14 23:00:00,72.49,22.794,46.04,29.509 -2020-07-14 23:15:00,73.85,21.485,46.04,29.509 -2020-07-14 23:30:00,70.65,20.417,46.04,29.509 -2020-07-14 23:45:00,72.46,19.43,46.04,29.509 -2020-07-15 00:00:00,70.34,18.844,42.195,29.509 -2020-07-15 00:15:00,71.25,19.69,42.195,29.509 -2020-07-15 00:30:00,70.44,18.468,42.195,29.509 -2020-07-15 00:45:00,70.41,18.444000000000003,42.195,29.509 -2020-07-15 01:00:00,67.46,18.48,38.82,29.509 -2020-07-15 01:15:00,68.86,17.599,38.82,29.509 -2020-07-15 01:30:00,67.2,16.067999999999998,38.82,29.509 -2020-07-15 01:45:00,67.83,16.009,38.82,29.509 -2020-07-15 02:00:00,66.75,15.790999999999999,37.023,29.509 -2020-07-15 02:15:00,67.72,14.513,37.023,29.509 -2020-07-15 02:30:00,67.76,16.441,37.023,29.509 -2020-07-15 02:45:00,67.84,17.197,37.023,29.509 -2020-07-15 03:00:00,66.87,18.329,36.818000000000005,29.509 -2020-07-15 03:15:00,71.03,17.664,36.818000000000005,29.509 -2020-07-15 03:30:00,71.12,17.007,36.818000000000005,29.509 -2020-07-15 03:45:00,72.97,16.365,36.818000000000005,29.509 -2020-07-15 04:00:00,73.68,20.866,37.495,29.509 -2020-07-15 04:15:00,76.29,26.789,37.495,29.509 -2020-07-15 04:30:00,80.11,24.078000000000003,37.495,29.509 -2020-07-15 04:45:00,84.96,24.088,37.495,29.509 -2020-07-15 05:00:00,87.78,36.374,39.858000000000004,29.509 -2020-07-15 05:15:00,87.33,42.99100000000001,39.858000000000004,29.509 -2020-07-15 05:30:00,94.23,37.495,39.858000000000004,29.509 -2020-07-15 05:45:00,94.63,35.131,39.858000000000004,29.509 -2020-07-15 06:00:00,100.78,35.971,52.867,29.509 -2020-07-15 06:15:00,108.9,35.558,52.867,29.509 -2020-07-15 06:30:00,110.63,34.966,52.867,29.509 -2020-07-15 06:45:00,111.18,37.216,52.867,29.509 -2020-07-15 07:00:00,105.52,37.258,66.061,29.509 -2020-07-15 07:15:00,103.46,37.86,66.061,29.509 -2020-07-15 07:30:00,103.47,35.599000000000004,66.061,29.509 -2020-07-15 07:45:00,102.77,35.999,66.061,29.509 -2020-07-15 08:00:00,103.23,31.991,58.532,29.509 -2020-07-15 08:15:00,108.72,35.184,58.532,29.509 -2020-07-15 08:30:00,110.2,36.266999999999996,58.532,29.509 -2020-07-15 08:45:00,110.41,38.838,58.532,29.509 -2020-07-15 09:00:00,110.93,32.918,56.047,29.509 -2020-07-15 09:15:00,114.49,32.213,56.047,29.509 -2020-07-15 09:30:00,113.05,36.003,56.047,29.509 -2020-07-15 09:45:00,111.8,39.516999999999996,56.047,29.509 -2020-07-15 10:00:00,110.53,36.769,53.823,29.509 -2020-07-15 10:15:00,113.38,38.397,53.823,29.509 -2020-07-15 10:30:00,118.08,38.407,53.823,29.509 -2020-07-15 10:45:00,119.14,39.724000000000004,53.823,29.509 -2020-07-15 11:00:00,114.85,37.48,54.184,29.509 -2020-07-15 11:15:00,105.35,38.809,54.184,29.509 -2020-07-15 11:30:00,107.75,40.027,54.184,29.509 -2020-07-15 11:45:00,104.65,41.305,54.184,29.509 -2020-07-15 12:00:00,101.57,36.622,52.628,29.509 -2020-07-15 12:15:00,102.98,36.098,52.628,29.509 -2020-07-15 12:30:00,101.37,34.839,52.628,29.509 -2020-07-15 12:45:00,100.1,35.945,52.628,29.509 -2020-07-15 13:00:00,95.13,36.046,52.31,29.509 -2020-07-15 13:15:00,97.79,37.365,52.31,29.509 -2020-07-15 13:30:00,92.09,35.635,52.31,29.509 -2020-07-15 13:45:00,93.99,35.318000000000005,52.31,29.509 -2020-07-15 14:00:00,91.96,37.105,52.278999999999996,29.509 -2020-07-15 14:15:00,93.66,36.051,52.278999999999996,29.509 -2020-07-15 14:30:00,93.43,34.949,52.278999999999996,29.509 -2020-07-15 14:45:00,94.43,35.57,52.278999999999996,29.509 -2020-07-15 15:00:00,90.17,37.62,53.306999999999995,29.509 -2020-07-15 15:15:00,89.69,35.193000000000005,53.306999999999995,29.509 -2020-07-15 15:30:00,94.34,33.59,53.306999999999995,29.509 -2020-07-15 15:45:00,93.63,31.82,53.306999999999995,29.509 -2020-07-15 16:00:00,94.82,34.765,55.358999999999995,29.509 -2020-07-15 16:15:00,96.19,34.501999999999995,55.358999999999995,29.509 -2020-07-15 16:30:00,95.74,33.736999999999995,55.358999999999995,29.509 -2020-07-15 16:45:00,96.06,30.52,55.358999999999995,29.509 -2020-07-15 17:00:00,98.6,34.265,59.211999999999996,29.509 -2020-07-15 17:15:00,98.46,34.354,59.211999999999996,29.509 -2020-07-15 17:30:00,98.19,33.245,59.211999999999996,29.509 -2020-07-15 17:45:00,101.1,32.071999999999996,59.211999999999996,29.509 -2020-07-15 18:00:00,101.97,35.156,60.403999999999996,29.509 -2020-07-15 18:15:00,100.51,34.96,60.403999999999996,29.509 -2020-07-15 18:30:00,99.62,32.937,60.403999999999996,29.509 -2020-07-15 18:45:00,99.61,35.994,60.403999999999996,29.509 -2020-07-15 19:00:00,96.67,37.191,60.993,29.509 -2020-07-15 19:15:00,92.57,36.158,60.993,29.509 -2020-07-15 19:30:00,91.19,35.016999999999996,60.993,29.509 -2020-07-15 19:45:00,90.05,34.249,60.993,29.509 -2020-07-15 20:00:00,89.39,31.340999999999998,66.6,29.509 -2020-07-15 20:15:00,89.79,30.886999999999997,66.6,29.509 -2020-07-15 20:30:00,90.48,31.406999999999996,66.6,29.509 -2020-07-15 20:45:00,94.33,32.068000000000005,66.6,29.509 -2020-07-15 21:00:00,89.0,30.467,59.855,29.509 -2020-07-15 21:15:00,90.24,32.078,59.855,29.509 -2020-07-15 21:30:00,86.46,32.781,59.855,29.509 -2020-07-15 21:45:00,84.69,33.777,59.855,29.509 -2020-07-15 22:00:00,80.58,30.976999999999997,54.942,29.509 -2020-07-15 22:15:00,79.78,33.615,54.942,29.509 -2020-07-15 22:30:00,77.4,29.239,54.942,29.509 -2020-07-15 22:45:00,76.55,25.959,54.942,29.509 -2020-07-15 23:00:00,70.55,22.781,46.056000000000004,29.509 -2020-07-15 23:15:00,73.75,21.489,46.056000000000004,29.509 -2020-07-15 23:30:00,72.68,20.434,46.056000000000004,29.509 -2020-07-15 23:45:00,71.83,19.444000000000003,46.056000000000004,29.509 -2020-07-16 00:00:00,68.4,17.695999999999998,40.859,29.509 -2020-07-16 00:15:00,69.33,18.609,40.859,29.509 -2020-07-16 00:30:00,69.21,17.279,40.859,29.509 -2020-07-16 00:45:00,69.57,17.358,40.859,29.509 -2020-07-16 01:00:00,66.81,17.385,39.06,29.509 -2020-07-16 01:15:00,69.01,16.565,39.06,29.509 -2020-07-16 01:30:00,67.69,15.054,39.06,29.509 -2020-07-16 01:45:00,68.28,15.171,39.06,29.509 -2020-07-16 02:00:00,68.0,15.002,37.592,29.509 -2020-07-16 02:15:00,67.65,14.513,37.592,29.509 -2020-07-16 02:30:00,67.91,15.694,37.592,29.509 -2020-07-16 02:45:00,69.21,16.421,37.592,29.509 -2020-07-16 03:00:00,68.8,17.433,37.416,29.509 -2020-07-16 03:15:00,69.64,16.917,37.416,29.509 -2020-07-16 03:30:00,71.41,16.209,37.416,29.509 -2020-07-16 03:45:00,74.92,15.402999999999999,37.416,29.509 -2020-07-16 04:00:00,75.15,19.62,38.176,29.509 -2020-07-16 04:15:00,76.88,25.261999999999997,38.176,29.509 -2020-07-16 04:30:00,76.66,22.57,38.176,29.509 -2020-07-16 04:45:00,80.18,22.496,38.176,29.509 -2020-07-16 05:00:00,87.87,34.303000000000004,41.203,29.509 -2020-07-16 05:15:00,89.57,40.372,41.203,29.509 -2020-07-16 05:30:00,96.26,35.325,41.203,29.509 -2020-07-16 05:45:00,101.5,32.945,41.203,29.509 -2020-07-16 06:00:00,107.07,33.306,51.09,29.509 -2020-07-16 06:15:00,102.95,32.66,51.09,29.509 -2020-07-16 06:30:00,101.95,32.357,51.09,29.509 -2020-07-16 06:45:00,107.07,34.909,51.09,29.509 -2020-07-16 07:00:00,109.85,34.71,63.541000000000004,29.509 -2020-07-16 07:15:00,107.55,35.439,63.541000000000004,29.509 -2020-07-16 07:30:00,105.46,33.343,63.541000000000004,29.509 -2020-07-16 07:45:00,99.9,33.835,63.541000000000004,29.509 -2020-07-16 08:00:00,103.71,29.159000000000002,55.65,29.509 -2020-07-16 08:15:00,106.39,32.507,55.65,29.509 -2020-07-16 08:30:00,107.35,34.055,55.65,29.509 -2020-07-16 08:45:00,103.5,36.774,55.65,29.509 -2020-07-16 09:00:00,99.32,32.482,51.833999999999996,29.509 -2020-07-16 09:15:00,101.09,32.001999999999995,51.833999999999996,29.509 -2020-07-16 09:30:00,106.4,35.815,51.833999999999996,29.509 -2020-07-16 09:45:00,112.1,39.141,51.833999999999996,29.509 -2020-07-16 10:00:00,113.34,36.639,49.70399999999999,29.509 -2020-07-16 10:15:00,103.53,38.18,49.70399999999999,29.509 -2020-07-16 10:30:00,108.61,38.09,49.70399999999999,29.509 -2020-07-16 10:45:00,110.26,39.092,49.70399999999999,29.509 -2020-07-16 11:00:00,107.49,36.751999999999995,48.593999999999994,29.509 -2020-07-16 11:15:00,101.65,38.056999999999995,48.593999999999994,29.509 -2020-07-16 11:30:00,99.95,39.297,48.593999999999994,29.509 -2020-07-16 11:45:00,105.9,40.42,48.593999999999994,29.509 -2020-07-16 12:00:00,104.35,34.999,46.275,29.509 -2020-07-16 12:15:00,106.27,34.306999999999995,46.275,29.509 -2020-07-16 12:30:00,97.52,33.074,46.275,29.509 -2020-07-16 12:45:00,98.65,34.076,46.275,29.509 -2020-07-16 13:00:00,102.47,34.053000000000004,45.803000000000004,29.509 -2020-07-16 13:15:00,104.02,35.477,45.803000000000004,29.509 -2020-07-16 13:30:00,100.98,33.608000000000004,45.803000000000004,29.509 -2020-07-16 13:45:00,94.53,33.578,45.803000000000004,29.509 -2020-07-16 14:00:00,96.85,35.39,46.251999999999995,29.509 -2020-07-16 14:15:00,96.71,34.388000000000005,46.251999999999995,29.509 -2020-07-16 14:30:00,99.91,33.429,46.251999999999995,29.509 -2020-07-16 14:45:00,99.62,34.041,46.251999999999995,29.509 -2020-07-16 15:00:00,96.59,36.379,48.309,29.509 -2020-07-16 15:15:00,93.27,34.016999999999996,48.309,29.509 -2020-07-16 15:30:00,95.88,32.552,48.309,29.509 -2020-07-16 15:45:00,100.36,30.926,48.309,29.509 -2020-07-16 16:00:00,100.07,33.31,49.681999999999995,29.509 -2020-07-16 16:15:00,95.77,33.111,49.681999999999995,29.509 -2020-07-16 16:30:00,98.62,32.172,49.681999999999995,29.509 -2020-07-16 16:45:00,100.86,29.104,49.681999999999995,29.509 -2020-07-16 17:00:00,105.52,33.069,53.086000000000006,29.509 -2020-07-16 17:15:00,107.45,33.188,53.086000000000006,29.509 -2020-07-16 17:30:00,104.16,32.003,53.086000000000006,29.509 -2020-07-16 17:45:00,103.07,31.022,53.086000000000006,29.509 -2020-07-16 18:00:00,101.84,34.315,54.038999999999994,29.509 -2020-07-16 18:15:00,100.31,34.167,54.038999999999994,29.509 -2020-07-16 18:30:00,100.5,32.226,54.038999999999994,29.509 -2020-07-16 18:45:00,106.08,34.979,54.038999999999994,29.509 -2020-07-16 19:00:00,105.9,36.38,53.408,29.509 -2020-07-16 19:15:00,102.77,35.374,53.408,29.509 -2020-07-16 19:30:00,96.1,34.2,53.408,29.509 -2020-07-16 19:45:00,95.11,33.135,53.408,29.509 -2020-07-16 20:00:00,91.57,30.674,55.309,29.509 -2020-07-16 20:15:00,94.42,29.885,55.309,29.509 -2020-07-16 20:30:00,93.02,30.358,55.309,29.509 -2020-07-16 20:45:00,93.42,30.788,55.309,29.509 -2020-07-16 21:00:00,91.2,29.548000000000002,51.585,29.509 -2020-07-16 21:15:00,90.29,30.741999999999997,51.585,29.509 -2020-07-16 21:30:00,87.58,31.38,51.585,29.509 -2020-07-16 21:45:00,86.37,32.32,51.585,29.509 -2020-07-16 22:00:00,81.95,29.609,48.006,29.509 -2020-07-16 22:15:00,81.66,32.455,48.006,29.509 -2020-07-16 22:30:00,79.46,28.267,48.006,29.509 -2020-07-16 22:45:00,80.13,25.146,48.006,29.509 -2020-07-16 23:00:00,75.25,21.895,42.309,29.509 -2020-07-16 23:15:00,75.85,20.484,42.309,29.509 -2020-07-16 23:30:00,74.85,19.387,42.309,29.509 -2020-07-16 23:45:00,73.27,18.362000000000002,42.309,29.509 -2020-07-17 00:00:00,69.67,15.872,39.649,29.509 -2020-07-17 00:15:00,72.11,17.002,39.649,29.509 -2020-07-17 00:30:00,72.27,15.982999999999999,39.649,29.509 -2020-07-17 00:45:00,72.1,16.518,39.649,29.509 -2020-07-17 01:00:00,69.69,16.156,37.744,29.509 -2020-07-17 01:15:00,70.44,14.607000000000001,37.744,29.509 -2020-07-17 01:30:00,70.83,14.513,37.744,29.509 -2020-07-17 01:45:00,73.34,14.513,37.744,29.509 -2020-07-17 02:00:00,70.32,14.565999999999999,36.965,29.509 -2020-07-17 02:15:00,70.93,14.513,36.965,29.509 -2020-07-17 02:30:00,70.09,16.064,36.965,29.509 -2020-07-17 02:45:00,69.59,16.048,36.965,29.509 -2020-07-17 03:00:00,70.93,17.969,37.678000000000004,29.509 -2020-07-17 03:15:00,72.45,16.085,37.678000000000004,29.509 -2020-07-17 03:30:00,74.49,15.127,37.678000000000004,29.509 -2020-07-17 03:45:00,74.49,15.26,37.678000000000004,29.509 -2020-07-17 04:00:00,76.78,19.583,38.591,29.509 -2020-07-17 04:15:00,83.05,23.531999999999996,38.591,29.509 -2020-07-17 04:30:00,83.79,21.848000000000003,38.591,29.509 -2020-07-17 04:45:00,82.32,21.16,38.591,29.509 -2020-07-17 05:00:00,92.53,32.51,40.666,29.509 -2020-07-17 05:15:00,93.78,39.453,40.666,29.509 -2020-07-17 05:30:00,99.45,34.591,40.666,29.509 -2020-07-17 05:45:00,102.94,31.750999999999998,40.666,29.509 -2020-07-17 06:00:00,108.29,32.311,51.784,29.509 -2020-07-17 06:15:00,101.27,31.945999999999998,51.784,29.509 -2020-07-17 06:30:00,101.61,31.660999999999998,51.784,29.509 -2020-07-17 06:45:00,108.21,33.99,51.784,29.509 -2020-07-17 07:00:00,112.7,34.54,61.383,29.509 -2020-07-17 07:15:00,110.87,36.194,61.383,29.509 -2020-07-17 07:30:00,107.13,31.997,61.383,29.509 -2020-07-17 07:45:00,105.72,32.355,61.383,29.509 -2020-07-17 08:00:00,109.76,28.636,55.272,29.509 -2020-07-17 08:15:00,109.09,32.758,55.272,29.509 -2020-07-17 08:30:00,106.95,34.098,55.272,29.509 -2020-07-17 08:45:00,103.14,36.793,55.272,29.509 -2020-07-17 09:00:00,105.78,29.919,53.506,29.509 -2020-07-17 09:15:00,106.52,31.503,53.506,29.509 -2020-07-17 09:30:00,107.91,34.588,53.506,29.509 -2020-07-17 09:45:00,105.7,38.348,53.506,29.509 -2020-07-17 10:00:00,103.69,35.79,51.363,29.509 -2020-07-17 10:15:00,108.86,37.004,51.363,29.509 -2020-07-17 10:30:00,108.56,37.53,51.363,29.509 -2020-07-17 10:45:00,105.22,38.471,51.363,29.509 -2020-07-17 11:00:00,99.4,36.409,51.043,29.509 -2020-07-17 11:15:00,104.69,36.599000000000004,51.043,29.509 -2020-07-17 11:30:00,105.83,37.249,51.043,29.509 -2020-07-17 11:45:00,105.9,37.297,51.043,29.509 -2020-07-17 12:00:00,97.56,32.258,47.52,29.509 -2020-07-17 12:15:00,96.68,31.066,47.52,29.509 -2020-07-17 12:30:00,91.93,29.941999999999997,47.52,29.509 -2020-07-17 12:45:00,92.95,30.011999999999997,47.52,29.509 -2020-07-17 13:00:00,96.42,30.528000000000002,45.494,29.509 -2020-07-17 13:15:00,97.53,32.061,45.494,29.509 -2020-07-17 13:30:00,94.43,31.075,45.494,29.509 -2020-07-17 13:45:00,91.76,31.401999999999997,45.494,29.509 -2020-07-17 14:00:00,92.13,32.44,43.883,29.509 -2020-07-17 14:15:00,90.45,31.94,43.883,29.509 -2020-07-17 14:30:00,91.4,32.594,43.883,29.509 -2020-07-17 14:45:00,93.5,32.418,43.883,29.509 -2020-07-17 15:00:00,94.71,34.715,45.714,29.509 -2020-07-17 15:15:00,95.27,32.129,45.714,29.509 -2020-07-17 15:30:00,88.29,30.189,45.714,29.509 -2020-07-17 15:45:00,92.07,29.386,45.714,29.509 -2020-07-17 16:00:00,91.83,30.943,48.222,29.509 -2020-07-17 16:15:00,100.32,31.264,48.222,29.509 -2020-07-17 16:30:00,98.96,30.133000000000003,48.222,29.509 -2020-07-17 16:45:00,102.8,26.223000000000003,48.222,29.509 -2020-07-17 17:00:00,96.41,32.058,52.619,29.509 -2020-07-17 17:15:00,97.39,32.042,52.619,29.509 -2020-07-17 17:30:00,96.03,31.074,52.619,29.509 -2020-07-17 17:45:00,101.63,29.944000000000003,52.619,29.509 -2020-07-17 18:00:00,104.59,33.204,52.99,29.509 -2020-07-17 18:15:00,104.12,32.05,52.99,29.509 -2020-07-17 18:30:00,99.36,29.965999999999998,52.99,29.509 -2020-07-17 18:45:00,95.42,33.177,52.99,29.509 -2020-07-17 19:00:00,99.76,35.439,51.923,29.509 -2020-07-17 19:15:00,99.09,34.96,51.923,29.509 -2020-07-17 19:30:00,93.72,33.879,51.923,29.509 -2020-07-17 19:45:00,89.16,31.75,51.923,29.509 -2020-07-17 20:00:00,86.65,29.105999999999998,56.238,29.509 -2020-07-17 20:15:00,91.65,29.175,56.238,29.509 -2020-07-17 20:30:00,91.6,29.116999999999997,56.238,29.509 -2020-07-17 20:45:00,89.51,28.627,56.238,29.509 -2020-07-17 21:00:00,81.75,28.764,52.426,29.509 -2020-07-17 21:15:00,84.05,31.724,52.426,29.509 -2020-07-17 21:30:00,85.59,32.179,52.426,29.509 -2020-07-17 21:45:00,85.67,33.286,52.426,29.509 -2020-07-17 22:00:00,79.08,30.348000000000003,48.196000000000005,29.509 -2020-07-17 22:15:00,73.88,32.935,48.196000000000005,29.509 -2020-07-17 22:30:00,71.96,33.379,48.196000000000005,29.509 -2020-07-17 22:45:00,70.94,30.925,48.196000000000005,29.509 -2020-07-17 23:00:00,69.13,29.535999999999998,41.71,29.509 -2020-07-17 23:15:00,72.96,26.595,41.71,29.509 -2020-07-17 23:30:00,74.34,23.662,41.71,29.509 -2020-07-17 23:45:00,73.55,22.548000000000002,41.71,29.509 -2020-07-18 00:00:00,64.97,17.482,41.105,29.398000000000003 -2020-07-18 00:15:00,64.83,18.22,41.105,29.398000000000003 -2020-07-18 00:30:00,69.52,16.590999999999998,41.105,29.398000000000003 -2020-07-18 00:45:00,69.35,16.285,41.105,29.398000000000003 -2020-07-18 01:00:00,67.98,16.169,36.934,29.398000000000003 -2020-07-18 01:15:00,63.54,15.332,36.934,29.398000000000003 -2020-07-18 01:30:00,65.47,14.513,36.934,29.398000000000003 -2020-07-18 01:45:00,67.32,14.98,36.934,29.398000000000003 -2020-07-18 02:00:00,66.36,14.703,34.782,29.398000000000003 -2020-07-18 02:15:00,62.18,14.513,34.782,29.398000000000003 -2020-07-18 02:30:00,60.75,14.605,34.782,29.398000000000003 -2020-07-18 02:45:00,66.16,15.431,34.782,29.398000000000003 -2020-07-18 03:00:00,65.84,15.811,34.489000000000004,29.398000000000003 -2020-07-18 03:15:00,67.15,14.513,34.489000000000004,29.398000000000003 -2020-07-18 03:30:00,60.47,14.513,34.489000000000004,29.398000000000003 -2020-07-18 03:45:00,66.51,14.547,34.489000000000004,29.398000000000003 -2020-07-18 04:00:00,65.6,17.13,34.111,29.398000000000003 -2020-07-18 04:15:00,60.82,20.293,34.111,29.398000000000003 -2020-07-18 04:30:00,65.33,16.974,34.111,29.398000000000003 -2020-07-18 04:45:00,61.82,16.62,34.111,29.398000000000003 -2020-07-18 05:00:00,61.13,20.213,33.283,29.398000000000003 -2020-07-18 05:15:00,60.73,16.794,33.283,29.398000000000003 -2020-07-18 05:30:00,62.16,14.513,33.283,29.398000000000003 -2020-07-18 05:45:00,66.61,15.129000000000001,33.283,29.398000000000003 -2020-07-18 06:00:00,72.23,26.281,33.653,29.398000000000003 -2020-07-18 06:15:00,71.83,33.03,33.653,29.398000000000003 -2020-07-18 06:30:00,69.3,30.125,33.653,29.398000000000003 -2020-07-18 06:45:00,68.38,29.725,33.653,29.398000000000003 -2020-07-18 07:00:00,73.72,29.413,36.732,29.398000000000003 -2020-07-18 07:15:00,77.89,29.795,36.732,29.398000000000003 -2020-07-18 07:30:00,79.76,26.998,36.732,29.398000000000003 -2020-07-18 07:45:00,78.08,28.014,36.732,29.398000000000003 -2020-07-18 08:00:00,79.55,24.814,41.318999999999996,29.398000000000003 -2020-07-18 08:15:00,85.3,28.331999999999997,41.318999999999996,29.398000000000003 -2020-07-18 08:30:00,84.34,29.479,41.318999999999996,29.398000000000003 -2020-07-18 08:45:00,82.38,32.879,41.318999999999996,29.398000000000003 -2020-07-18 09:00:00,79.5,29.301,43.195,29.398000000000003 -2020-07-18 09:15:00,79.99,31.334,43.195,29.398000000000003 -2020-07-18 09:30:00,88.35,34.856,43.195,29.398000000000003 -2020-07-18 09:45:00,91.53,38.098,43.195,29.398000000000003 -2020-07-18 10:00:00,87.81,36.235,41.843999999999994,29.398000000000003 -2020-07-18 10:15:00,83.2,37.816,41.843999999999994,29.398000000000003 -2020-07-18 10:30:00,84.65,37.939,41.843999999999994,29.398000000000003 -2020-07-18 10:45:00,92.01,38.341,41.843999999999994,29.398000000000003 -2020-07-18 11:00:00,88.51,36.077,39.035,29.398000000000003 -2020-07-18 11:15:00,83.11,37.3,39.035,29.398000000000003 -2020-07-18 11:30:00,78.1,38.375,39.035,29.398000000000003 -2020-07-18 11:45:00,76.27,39.258,39.035,29.398000000000003 -2020-07-18 12:00:00,74.23,34.78,38.001,29.398000000000003 -2020-07-18 12:15:00,71.26,34.455,38.001,29.398000000000003 -2020-07-18 12:30:00,65.03,33.147,38.001,29.398000000000003 -2020-07-18 12:45:00,61.3,34.099000000000004,38.001,29.398000000000003 -2020-07-18 13:00:00,60.24,33.71,34.747,29.398000000000003 -2020-07-18 13:15:00,62.03,34.865,34.747,29.398000000000003 -2020-07-18 13:30:00,65.18,34.141,34.747,29.398000000000003 -2020-07-18 13:45:00,70.0,32.931999999999995,34.747,29.398000000000003 -2020-07-18 14:00:00,72.75,33.854,33.434,29.398000000000003 -2020-07-18 14:15:00,74.53,32.037,33.434,29.398000000000003 -2020-07-18 14:30:00,77.56,32.413000000000004,33.434,29.398000000000003 -2020-07-18 14:45:00,79.82,32.749,33.434,29.398000000000003 -2020-07-18 15:00:00,79.73,35.416,35.921,29.398000000000003 -2020-07-18 15:15:00,78.71,33.518,35.921,29.398000000000003 -2020-07-18 15:30:00,78.58,31.645,35.921,29.398000000000003 -2020-07-18 15:45:00,78.43,29.824,35.921,29.398000000000003 -2020-07-18 16:00:00,77.97,33.81,39.427,29.398000000000003 -2020-07-18 16:15:00,75.31,33.069,39.427,29.398000000000003 -2020-07-18 16:30:00,75.91,32.207,39.427,29.398000000000003 -2020-07-18 16:45:00,77.36,28.189,39.427,29.398000000000003 -2020-07-18 17:00:00,78.24,32.734,44.096000000000004,29.398000000000003 -2020-07-18 17:15:00,78.03,30.230999999999998,44.096000000000004,29.398000000000003 -2020-07-18 17:30:00,78.44,29.135,44.096000000000004,29.398000000000003 -2020-07-18 17:45:00,80.13,28.595,44.096000000000004,29.398000000000003 -2020-07-18 18:00:00,79.23,33.352,43.931000000000004,29.398000000000003 -2020-07-18 18:15:00,78.26,33.839,43.931000000000004,29.398000000000003 -2020-07-18 18:30:00,77.24,33.111,43.931000000000004,29.398000000000003 -2020-07-18 18:45:00,76.69,32.857,43.931000000000004,29.398000000000003 -2020-07-18 19:00:00,73.66,33.239000000000004,42.187,29.398000000000003 -2020-07-18 19:15:00,71.27,31.711,42.187,29.398000000000003 -2020-07-18 19:30:00,70.94,31.392,42.187,29.398000000000003 -2020-07-18 19:45:00,71.36,31.168000000000003,42.187,29.398000000000003 -2020-07-18 20:00:00,71.01,29.235,38.315,29.398000000000003 -2020-07-18 20:15:00,72.39,28.453000000000003,38.315,29.398000000000003 -2020-07-18 20:30:00,72.9,27.475,38.315,29.398000000000003 -2020-07-18 20:45:00,72.58,29.005,38.315,29.398000000000003 -2020-07-18 21:00:00,71.06,27.381,36.843,29.398000000000003 -2020-07-18 21:15:00,70.83,29.934,36.843,29.398000000000003 -2020-07-18 21:30:00,68.87,30.485,36.843,29.398000000000003 -2020-07-18 21:45:00,68.49,31.026,36.843,29.398000000000003 -2020-07-18 22:00:00,65.43,27.968000000000004,37.260999999999996,29.398000000000003 -2020-07-18 22:15:00,65.56,30.64,37.260999999999996,29.398000000000003 -2020-07-18 22:30:00,63.24,30.226,37.260999999999996,29.398000000000003 -2020-07-18 22:45:00,62.82,28.031999999999996,37.260999999999996,29.398000000000003 -2020-07-18 23:00:00,57.26,25.816999999999997,32.148,29.398000000000003 -2020-07-18 23:15:00,59.35,23.447,32.148,29.398000000000003 -2020-07-18 23:30:00,58.36,23.248,32.148,29.398000000000003 -2020-07-18 23:45:00,57.61,22.671,32.148,29.398000000000003 -2020-07-19 00:00:00,55.61,18.977,28.905,29.398000000000003 -2020-07-19 00:15:00,55.79,18.499000000000002,28.905,29.398000000000003 -2020-07-19 00:30:00,54.68,16.748,28.905,29.398000000000003 -2020-07-19 00:45:00,54.38,16.272000000000002,28.905,29.398000000000003 -2020-07-19 01:00:00,51.87,16.452,26.906999999999996,29.398000000000003 -2020-07-19 01:15:00,53.39,15.395999999999999,26.906999999999996,29.398000000000003 -2020-07-19 01:30:00,53.69,14.513,26.906999999999996,29.398000000000003 -2020-07-19 01:45:00,53.75,14.513,26.906999999999996,29.398000000000003 -2020-07-19 02:00:00,52.09,14.513,25.938000000000002,29.398000000000003 -2020-07-19 02:15:00,52.77,14.513,25.938000000000002,29.398000000000003 -2020-07-19 02:30:00,52.15,15.205,25.938000000000002,29.398000000000003 -2020-07-19 02:45:00,52.3,15.685,25.938000000000002,29.398000000000003 -2020-07-19 03:00:00,51.95,16.727999999999998,24.693,29.398000000000003 -2020-07-19 03:15:00,52.28,14.535,24.693,29.398000000000003 -2020-07-19 03:30:00,53.32,14.513,24.693,29.398000000000003 -2020-07-19 03:45:00,53.05,14.513,24.693,29.398000000000003 -2020-07-19 04:00:00,52.24,16.62,25.683000000000003,29.398000000000003 -2020-07-19 04:15:00,51.65,19.378,25.683000000000003,29.398000000000003 -2020-07-19 04:30:00,51.6,17.485,25.683000000000003,29.398000000000003 -2020-07-19 04:45:00,52.23,16.61,25.683000000000003,29.398000000000003 -2020-07-19 05:00:00,51.43,20.625,26.023000000000003,29.398000000000003 -2020-07-19 05:15:00,51.17,16.522000000000002,26.023000000000003,29.398000000000003 -2020-07-19 05:30:00,51.75,14.513,26.023000000000003,29.398000000000003 -2020-07-19 05:45:00,52.6,14.513,26.023000000000003,29.398000000000003 -2020-07-19 06:00:00,54.27,22.829,25.834,29.398000000000003 -2020-07-19 06:15:00,54.5,30.6,25.834,29.398000000000003 -2020-07-19 06:30:00,55.07,27.053,25.834,29.398000000000003 -2020-07-19 06:45:00,57.47,25.699,25.834,29.398000000000003 -2020-07-19 07:00:00,59.13,25.54,27.765,29.398000000000003 -2020-07-19 07:15:00,59.65,24.248,27.765,29.398000000000003 -2020-07-19 07:30:00,60.2,22.959,27.765,29.398000000000003 -2020-07-19 07:45:00,60.84,24.031999999999996,27.765,29.398000000000003 -2020-07-19 08:00:00,61.14,21.519000000000002,31.357,29.398000000000003 -2020-07-19 08:15:00,58.21,26.334,31.357,29.398000000000003 -2020-07-19 08:30:00,57.28,28.247,31.357,29.398000000000003 -2020-07-19 08:45:00,58.82,31.338,31.357,29.398000000000003 -2020-07-19 09:00:00,59.11,27.697,33.238,29.398000000000003 -2020-07-19 09:15:00,60.1,29.121,33.238,29.398000000000003 -2020-07-19 09:30:00,59.77,33.126,33.238,29.398000000000003 -2020-07-19 09:45:00,62.12,37.48,33.238,29.398000000000003 -2020-07-19 10:00:00,62.62,35.931,34.22,29.398000000000003 -2020-07-19 10:15:00,63.28,37.588,34.22,29.398000000000003 -2020-07-19 10:30:00,63.94,37.878,34.22,29.398000000000003 -2020-07-19 10:45:00,63.69,39.635,34.22,29.398000000000003 -2020-07-19 11:00:00,63.64,36.87,36.298,29.398000000000003 -2020-07-19 11:15:00,65.93,37.582,36.298,29.398000000000003 -2020-07-19 11:30:00,62.62,39.328,36.298,29.398000000000003 -2020-07-19 11:45:00,61.61,40.412,36.298,29.398000000000003 -2020-07-19 12:00:00,60.44,37.195,33.52,29.398000000000003 -2020-07-19 12:15:00,58.9,35.936,33.52,29.398000000000003 -2020-07-19 12:30:00,58.57,35.041,33.52,29.398000000000003 -2020-07-19 12:45:00,56.89,35.4,33.52,29.398000000000003 -2020-07-19 13:00:00,54.98,34.747,30.12,29.398000000000003 -2020-07-19 13:15:00,53.24,34.882,30.12,29.398000000000003 -2020-07-19 13:30:00,51.7,32.954,30.12,29.398000000000003 -2020-07-19 13:45:00,54.52,33.073,30.12,29.398000000000003 -2020-07-19 14:00:00,54.96,35.285,27.233,29.398000000000003 -2020-07-19 14:15:00,52.39,33.801,27.233,29.398000000000003 -2020-07-19 14:30:00,50.08,32.659,27.233,29.398000000000003 -2020-07-19 14:45:00,49.54,31.871,27.233,29.398000000000003 -2020-07-19 15:00:00,51.32,34.882,27.468000000000004,29.398000000000003 -2020-07-19 15:15:00,51.02,32.001,27.468000000000004,29.398000000000003 -2020-07-19 15:30:00,52.26,29.862,27.468000000000004,29.398000000000003 -2020-07-19 15:45:00,52.6,28.27,27.468000000000004,29.398000000000003 -2020-07-19 16:00:00,56.74,30.274,30.8,29.398000000000003 -2020-07-19 16:15:00,57.44,29.891,30.8,29.398000000000003 -2020-07-19 16:30:00,60.95,30.116999999999997,30.8,29.398000000000003 -2020-07-19 16:45:00,61.94,26.168000000000003,30.8,29.398000000000003 -2020-07-19 17:00:00,67.22,31.125999999999998,37.806,29.398000000000003 -2020-07-19 17:15:00,71.64,30.329,37.806,29.398000000000003 -2020-07-19 17:30:00,73.51,30.09,37.806,29.398000000000003 -2020-07-19 17:45:00,74.82,29.78,37.806,29.398000000000003 -2020-07-19 18:00:00,75.5,35.266,40.766,29.398000000000003 -2020-07-19 18:15:00,73.22,35.173,40.766,29.398000000000003 -2020-07-19 18:30:00,72.29,34.387,40.766,29.398000000000003 -2020-07-19 18:45:00,72.76,34.076,40.766,29.398000000000003 -2020-07-19 19:00:00,74.69,36.863,41.163000000000004,29.398000000000003 -2020-07-19 19:15:00,73.14,34.086,41.163000000000004,29.398000000000003 -2020-07-19 19:30:00,71.41,33.510999999999996,41.163000000000004,29.398000000000003 -2020-07-19 19:45:00,72.67,32.666,41.163000000000004,29.398000000000003 -2020-07-19 20:00:00,73.53,30.916,39.885999999999996,29.398000000000003 -2020-07-19 20:15:00,75.2,29.899,39.885999999999996,29.398000000000003 -2020-07-19 20:30:00,76.95,29.673000000000002,39.885999999999996,29.398000000000003 -2020-07-19 20:45:00,76.76,29.506,39.885999999999996,29.398000000000003 -2020-07-19 21:00:00,74.32,28.004,38.900999999999996,29.398000000000003 -2020-07-19 21:15:00,74.99,30.284000000000002,38.900999999999996,29.398000000000003 -2020-07-19 21:30:00,73.19,30.101999999999997,38.900999999999996,29.398000000000003 -2020-07-19 21:45:00,71.17,30.999000000000002,38.900999999999996,29.398000000000003 -2020-07-19 22:00:00,66.49,30.218000000000004,39.806999999999995,29.398000000000003 -2020-07-19 22:15:00,68.96,31.16,39.806999999999995,29.398000000000003 -2020-07-19 22:30:00,67.63,30.438000000000002,39.806999999999995,29.398000000000003 -2020-07-19 22:45:00,67.17,26.953000000000003,39.806999999999995,29.398000000000003 -2020-07-19 23:00:00,62.05,24.655,35.564,29.398000000000003 -2020-07-19 23:15:00,63.9,23.502,35.564,29.398000000000003 -2020-07-19 23:30:00,62.49,22.683000000000003,35.564,29.398000000000003 -2020-07-19 23:45:00,62.2,22.201,35.564,29.398000000000003 -2020-07-20 00:00:00,60.09,20.273,36.578,29.509 -2020-07-20 00:15:00,60.71,20.372,36.578,29.509 -2020-07-20 00:30:00,59.91,18.199,36.578,29.509 -2020-07-20 00:45:00,60.28,17.354,36.578,29.509 -2020-07-20 01:00:00,58.64,17.95,35.292,29.509 -2020-07-20 01:15:00,58.78,16.954,35.292,29.509 -2020-07-20 01:30:00,59.4,15.62,35.292,29.509 -2020-07-20 01:45:00,58.95,16.297,35.292,29.509 -2020-07-20 02:00:00,58.17,16.67,34.319,29.509 -2020-07-20 02:15:00,59.43,14.513,34.319,29.509 -2020-07-20 02:30:00,65.62,16.453,34.319,29.509 -2020-07-20 02:45:00,68.61,16.833,34.319,29.509 -2020-07-20 03:00:00,68.25,18.299,33.13,29.509 -2020-07-20 03:15:00,63.19,16.746,33.13,29.509 -2020-07-20 03:30:00,64.3,16.148,33.13,29.509 -2020-07-20 03:45:00,71.14,16.613,33.13,29.509 -2020-07-20 04:00:00,74.7,22.09,33.851,29.509 -2020-07-20 04:15:00,76.07,27.603,33.851,29.509 -2020-07-20 04:30:00,72.95,24.997,33.851,29.509 -2020-07-20 04:45:00,78.22,24.505,33.851,29.509 -2020-07-20 05:00:00,83.67,34.926,38.718,29.509 -2020-07-20 05:15:00,86.56,40.138000000000005,38.718,29.509 -2020-07-20 05:30:00,86.91,34.939,38.718,29.509 -2020-07-20 05:45:00,96.34,33.38,38.718,29.509 -2020-07-20 06:00:00,101.08,32.46,51.648999999999994,29.509 -2020-07-20 06:15:00,103.96,31.761999999999997,51.648999999999994,29.509 -2020-07-20 06:30:00,100.97,31.808000000000003,51.648999999999994,29.509 -2020-07-20 06:45:00,101.61,35.384,51.648999999999994,29.509 -2020-07-20 07:00:00,101.25,34.973,60.159,29.509 -2020-07-20 07:15:00,101.47,36.051,60.159,29.509 -2020-07-20 07:30:00,105.36,33.915,60.159,29.509 -2020-07-20 07:45:00,106.42,35.589,60.159,29.509 -2020-07-20 08:00:00,107.88,31.041,53.8,29.509 -2020-07-20 08:15:00,105.6,34.771,53.8,29.509 -2020-07-20 08:30:00,101.05,36.065,53.8,29.509 -2020-07-20 08:45:00,107.78,39.739000000000004,53.8,29.509 -2020-07-20 09:00:00,106.48,34.959,50.583,29.509 -2020-07-20 09:15:00,103.76,34.821,50.583,29.509 -2020-07-20 09:30:00,100.88,37.909,50.583,29.509 -2020-07-20 09:45:00,97.4,39.689,50.583,29.509 -2020-07-20 10:00:00,103.26,38.675,49.11600000000001,29.509 -2020-07-20 10:15:00,107.19,40.2,49.11600000000001,29.509 -2020-07-20 10:30:00,105.63,40.084,49.11600000000001,29.509 -2020-07-20 10:45:00,97.62,39.985,49.11600000000001,29.509 -2020-07-20 11:00:00,97.57,37.84,49.056000000000004,29.509 -2020-07-20 11:15:00,103.01,38.643,49.056000000000004,29.509 -2020-07-20 11:30:00,102.55,40.983000000000004,49.056000000000004,29.509 -2020-07-20 11:45:00,105.27,42.663999999999994,49.056000000000004,29.509 -2020-07-20 12:00:00,98.29,37.333,47.227,29.509 -2020-07-20 12:15:00,99.49,36.194,47.227,29.509 -2020-07-20 12:30:00,97.27,34.074,47.227,29.509 -2020-07-20 12:45:00,96.77,34.235,47.227,29.509 -2020-07-20 13:00:00,95.98,34.484,47.006,29.509 -2020-07-20 13:15:00,91.51,33.8,47.006,29.509 -2020-07-20 13:30:00,91.54,32.129,47.006,29.509 -2020-07-20 13:45:00,91.45,33.25,47.006,29.509 -2020-07-20 14:00:00,91.7,34.586,47.19,29.509 -2020-07-20 14:15:00,91.78,33.838,47.19,29.509 -2020-07-20 14:30:00,88.92,32.577,47.19,29.509 -2020-07-20 14:45:00,90.77,34.091,47.19,29.509 -2020-07-20 15:00:00,87.71,36.536,47.846000000000004,29.509 -2020-07-20 15:15:00,88.74,33.169000000000004,47.846000000000004,29.509 -2020-07-20 15:30:00,89.17,31.98,47.846000000000004,29.509 -2020-07-20 15:45:00,88.84,29.899,47.846000000000004,29.509 -2020-07-20 16:00:00,89.26,33.132,49.641000000000005,29.509 -2020-07-20 16:15:00,89.55,32.911,49.641000000000005,29.509 -2020-07-20 16:30:00,91.94,32.528,49.641000000000005,29.509 -2020-07-20 16:45:00,94.53,28.75,49.641000000000005,29.509 -2020-07-20 17:00:00,95.39,32.519,54.133,29.509 -2020-07-20 17:15:00,94.5,32.255,54.133,29.509 -2020-07-20 17:30:00,96.79,31.66,54.133,29.509 -2020-07-20 17:45:00,98.25,31.088,54.133,29.509 -2020-07-20 18:00:00,98.64,35.414,53.761,29.509 -2020-07-20 18:15:00,97.32,33.545,53.761,29.509 -2020-07-20 18:30:00,97.45,31.878,53.761,29.509 -2020-07-20 18:45:00,96.82,34.93,53.761,29.509 -2020-07-20 19:00:00,93.5,37.611,53.923,29.509 -2020-07-20 19:15:00,90.15,36.361,53.923,29.509 -2020-07-20 19:30:00,89.51,35.339,53.923,29.509 -2020-07-20 19:45:00,89.22,33.908,53.923,29.509 -2020-07-20 20:00:00,86.74,31.003,58.786,29.509 -2020-07-20 20:15:00,87.84,31.72,58.786,29.509 -2020-07-20 20:30:00,88.62,32.219,58.786,29.509 -2020-07-20 20:45:00,88.28,32.134,58.786,29.509 -2020-07-20 21:00:00,86.92,29.896,54.591,29.509 -2020-07-20 21:15:00,85.22,32.747,54.591,29.509 -2020-07-20 21:30:00,80.97,33.064,54.591,29.509 -2020-07-20 21:45:00,80.99,33.756,54.591,29.509 -2020-07-20 22:00:00,74.55,31.004,51.551,29.509 -2020-07-20 22:15:00,75.32,34.176,51.551,29.509 -2020-07-20 22:30:00,74.29,29.589000000000002,51.551,29.509 -2020-07-20 22:45:00,72.88,26.478,51.551,29.509 -2020-07-20 23:00:00,68.65,24.109,44.716,29.509 -2020-07-20 23:15:00,69.81,20.999000000000002,44.716,29.509 -2020-07-20 23:30:00,68.67,19.935,44.716,29.509 -2020-07-20 23:45:00,67.93,18.665,44.716,29.509 -2020-07-21 00:00:00,63.82,17.929000000000002,43.01,29.509 -2020-07-21 00:15:00,65.39,18.83,43.01,29.509 -2020-07-21 00:30:00,64.02,17.522000000000002,43.01,29.509 -2020-07-21 00:45:00,64.2,17.633,43.01,29.509 -2020-07-21 01:00:00,64.16,17.659000000000002,40.687,29.509 -2020-07-21 01:15:00,65.18,16.822,40.687,29.509 -2020-07-21 01:30:00,64.43,15.337,40.687,29.509 -2020-07-21 01:45:00,65.07,15.425,40.687,29.509 -2020-07-21 02:00:00,63.66,15.28,39.554,29.509 -2020-07-21 02:15:00,65.32,14.513,39.554,29.509 -2020-07-21 02:30:00,72.4,15.956,39.554,29.509 -2020-07-21 02:45:00,73.53,16.692,39.554,29.509 -2020-07-21 03:00:00,68.84,17.657,38.958,29.509 -2020-07-21 03:15:00,66.73,17.21,38.958,29.509 -2020-07-21 03:30:00,70.8,16.541,38.958,29.509 -2020-07-21 03:45:00,75.24,15.792,38.958,29.509 -2020-07-21 04:00:00,77.83,19.896,39.783,29.509 -2020-07-21 04:15:00,79.77,25.436,39.783,29.509 -2020-07-21 04:30:00,76.2,22.725,39.783,29.509 -2020-07-21 04:45:00,78.11,22.65,39.783,29.509 -2020-07-21 05:00:00,86.55,34.32,42.281000000000006,29.509 -2020-07-21 05:15:00,88.52,40.176,42.281000000000006,29.509 -2020-07-21 05:30:00,90.48,35.345,42.281000000000006,29.509 -2020-07-21 05:45:00,99.68,33.008,42.281000000000006,29.509 -2020-07-21 06:00:00,105.81,33.314,50.801,29.509 -2020-07-21 06:15:00,105.55,32.696,50.801,29.509 -2020-07-21 06:30:00,99.73,32.465,50.801,29.509 -2020-07-21 06:45:00,103.62,35.137,50.801,29.509 -2020-07-21 07:00:00,102.74,34.907,60.202,29.509 -2020-07-21 07:15:00,100.9,35.742,60.202,29.509 -2020-07-21 07:30:00,99.85,33.693000000000005,60.202,29.509 -2020-07-21 07:45:00,99.38,34.294000000000004,60.202,29.509 -2020-07-21 08:00:00,99.42,29.659000000000002,54.461000000000006,29.509 -2020-07-21 08:15:00,99.3,33.006,54.461000000000006,29.509 -2020-07-21 08:30:00,105.13,34.493,54.461000000000006,29.509 -2020-07-21 08:45:00,106.32,37.162,54.461000000000006,29.509 -2020-07-21 09:00:00,105.96,32.863,50.753,29.509 -2020-07-21 09:15:00,101.04,32.369,50.753,29.509 -2020-07-21 09:30:00,99.71,36.133,50.753,29.509 -2020-07-21 09:45:00,98.06,39.431999999999995,50.753,29.509 -2020-07-21 10:00:00,97.74,36.961999999999996,49.703,29.509 -2020-07-21 10:15:00,100.93,38.46,49.703,29.509 -2020-07-21 10:30:00,104.39,38.344,49.703,29.509 -2020-07-21 10:45:00,104.51,39.336999999999996,49.703,29.509 -2020-07-21 11:00:00,102.28,37.006,49.42100000000001,29.509 -2020-07-21 11:15:00,99.28,38.304,49.42100000000001,29.509 -2020-07-21 11:30:00,95.02,39.493,49.42100000000001,29.509 -2020-07-21 11:45:00,101.89,40.571,49.42100000000001,29.509 -2020-07-21 12:00:00,99.86,35.217,47.155,29.509 -2020-07-21 12:15:00,95.66,34.506,47.155,29.509 -2020-07-21 12:30:00,93.76,33.255,47.155,29.509 -2020-07-21 12:45:00,93.59,34.227,47.155,29.509 -2020-07-21 13:00:00,92.33,34.104,47.515,29.509 -2020-07-21 13:15:00,91.18,35.485,47.515,29.509 -2020-07-21 13:30:00,89.86,33.629,47.515,29.509 -2020-07-21 13:45:00,90.0,33.645,47.515,29.509 -2020-07-21 14:00:00,90.09,35.437,47.575,29.509 -2020-07-21 14:15:00,92.68,34.449,47.575,29.509 -2020-07-21 14:30:00,90.15,33.472,47.575,29.509 -2020-07-21 14:45:00,88.28,34.118,47.575,29.509 -2020-07-21 15:00:00,88.62,36.428000000000004,48.903,29.509 -2020-07-21 15:15:00,88.99,34.052,48.903,29.509 -2020-07-21 15:30:00,89.61,32.603,48.903,29.509 -2020-07-21 15:45:00,91.36,30.955,48.903,29.509 -2020-07-21 16:00:00,93.5,33.351,50.218999999999994,29.509 -2020-07-21 16:15:00,91.9,33.176,50.218999999999994,29.509 -2020-07-21 16:30:00,94.2,32.317,50.218999999999994,29.509 -2020-07-21 16:45:00,95.32,29.316,50.218999999999994,29.509 -2020-07-21 17:00:00,95.98,33.25,55.396,29.509 -2020-07-21 17:15:00,97.01,33.464,55.396,29.509 -2020-07-21 17:30:00,98.47,32.316,55.396,29.509 -2020-07-21 17:45:00,99.18,31.405,55.396,29.509 -2020-07-21 18:00:00,98.99,34.709,55.583999999999996,29.509 -2020-07-21 18:15:00,99.15,34.52,55.583999999999996,29.509 -2020-07-21 18:30:00,101.85,32.603,55.583999999999996,29.509 -2020-07-21 18:45:00,98.64,35.358000000000004,55.583999999999996,29.509 -2020-07-21 19:00:00,97.79,36.768,56.071000000000005,29.509 -2020-07-21 19:15:00,91.91,35.734,56.071000000000005,29.509 -2020-07-21 19:30:00,90.5,34.538000000000004,56.071000000000005,29.509 -2020-07-21 19:45:00,89.8,33.467,56.071000000000005,29.509 -2020-07-21 20:00:00,87.79,30.967,61.55,29.509 -2020-07-21 20:15:00,91.21,30.176,61.55,29.509 -2020-07-21 20:30:00,90.1,30.614,61.55,29.509 -2020-07-21 20:45:00,90.93,31.037,61.55,29.509 -2020-07-21 21:00:00,86.27,29.811999999999998,55.94,29.509 -2020-07-21 21:15:00,86.42,30.99,55.94,29.509 -2020-07-21 21:30:00,83.72,31.548000000000002,55.94,29.509 -2020-07-21 21:45:00,82.77,32.415,55.94,29.509 -2020-07-21 22:00:00,76.63,29.698,52.857,29.509 -2020-07-21 22:15:00,77.71,32.525999999999996,52.857,29.509 -2020-07-21 22:30:00,75.72,28.224,52.857,29.509 -2020-07-21 22:45:00,76.32,25.078000000000003,52.857,29.509 -2020-07-21 23:00:00,71.02,21.905,46.04,29.509 -2020-07-21 23:15:00,71.7,20.565,46.04,29.509 -2020-07-21 23:30:00,70.36,19.544,46.04,29.509 -2020-07-21 23:45:00,70.53,18.503,46.04,29.509 -2020-07-22 00:00:00,67.59,17.987000000000002,42.195,29.509 -2020-07-22 00:15:00,68.74,18.885,42.195,29.509 -2020-07-22 00:30:00,66.81,17.581,42.195,29.509 -2020-07-22 00:45:00,67.64,17.7,42.195,29.509 -2020-07-22 01:00:00,66.47,17.722,38.82,29.509 -2020-07-22 01:15:00,67.91,16.884,38.82,29.509 -2020-07-22 01:30:00,65.56,15.405999999999999,38.82,29.509 -2020-07-22 01:45:00,66.27,15.485999999999999,38.82,29.509 -2020-07-22 02:00:00,64.97,15.347999999999999,37.023,29.509 -2020-07-22 02:15:00,72.4,14.513,37.023,29.509 -2020-07-22 02:30:00,74.12,16.021,37.023,29.509 -2020-07-22 02:45:00,73.22,16.758,37.023,29.509 -2020-07-22 03:00:00,66.46,17.713,36.818000000000005,29.509 -2020-07-22 03:15:00,75.15,17.28,36.818000000000005,29.509 -2020-07-22 03:30:00,76.71,16.618,36.818000000000005,29.509 -2020-07-22 03:45:00,78.64,15.88,36.818000000000005,29.509 -2020-07-22 04:00:00,77.87,19.965,37.495,29.509 -2020-07-22 04:15:00,72.77,25.488000000000003,37.495,29.509 -2020-07-22 04:30:00,76.27,22.774,37.495,29.509 -2020-07-22 04:45:00,81.51,22.7,37.495,29.509 -2020-07-22 05:00:00,85.93,34.351,39.858000000000004,29.509 -2020-07-22 05:15:00,89.79,40.176,39.858000000000004,29.509 -2020-07-22 05:30:00,98.41,35.385,39.858000000000004,29.509 -2020-07-22 05:45:00,102.38,33.051,39.858000000000004,29.509 -2020-07-22 06:00:00,107.43,33.343,52.867,29.509 -2020-07-22 06:15:00,101.45,32.733000000000004,52.867,29.509 -2020-07-22 06:30:00,105.73,32.514,52.867,29.509 -2020-07-22 06:45:00,102.48,35.209,52.867,29.509 -2020-07-22 07:00:00,105.43,34.973,66.061,29.509 -2020-07-22 07:15:00,109.7,35.829,66.061,29.509 -2020-07-22 07:30:00,109.95,33.79,66.061,29.509 -2020-07-22 07:45:00,108.67,34.412,66.061,29.509 -2020-07-22 08:00:00,103.21,29.785,58.532,29.509 -2020-07-22 08:15:00,103.06,33.128,58.532,29.509 -2020-07-22 08:30:00,103.46,34.603,58.532,29.509 -2020-07-22 08:45:00,102.16,37.262,58.532,29.509 -2020-07-22 09:00:00,103.55,32.961999999999996,56.047,29.509 -2020-07-22 09:15:00,112.12,32.464,56.047,29.509 -2020-07-22 09:30:00,113.73,36.217,56.047,29.509 -2020-07-22 09:45:00,114.83,39.509,56.047,29.509 -2020-07-22 10:00:00,106.75,37.044000000000004,53.823,29.509 -2020-07-22 10:15:00,110.2,38.532,53.823,29.509 -2020-07-22 10:30:00,111.64,38.41,53.823,29.509 -2020-07-22 10:45:00,111.39,39.402,53.823,29.509 -2020-07-22 11:00:00,107.68,37.074,54.184,29.509 -2020-07-22 11:15:00,106.1,38.369,54.184,29.509 -2020-07-22 11:30:00,109.13,39.548,54.184,29.509 -2020-07-22 11:45:00,107.03,40.616,54.184,29.509 -2020-07-22 12:00:00,105.01,35.274,52.628,29.509 -2020-07-22 12:15:00,102.07,34.558,52.628,29.509 -2020-07-22 12:30:00,96.47,33.306,52.628,29.509 -2020-07-22 12:45:00,97.11,34.27,52.628,29.509 -2020-07-22 13:00:00,97.9,34.126999999999995,52.31,29.509 -2020-07-22 13:15:00,101.75,35.499,52.31,29.509 -2020-07-22 13:30:00,97.86,33.644,52.31,29.509 -2020-07-22 13:45:00,95.02,33.67,52.31,29.509 -2020-07-22 14:00:00,94.83,35.455999999999996,52.278999999999996,29.509 -2020-07-22 14:15:00,98.49,34.472,52.278999999999996,29.509 -2020-07-22 14:30:00,95.78,33.493,52.278999999999996,29.509 -2020-07-22 14:45:00,94.64,34.146,52.278999999999996,29.509 -2020-07-22 15:00:00,92.12,36.445,53.306999999999995,29.509 -2020-07-22 15:15:00,93.44,34.067,53.306999999999995,29.509 -2020-07-22 15:30:00,92.66,32.624,53.306999999999995,29.509 -2020-07-22 15:45:00,95.61,30.971999999999998,53.306999999999995,29.509 -2020-07-22 16:00:00,95.06,33.367,55.358999999999995,29.509 -2020-07-22 16:15:00,96.05,33.196,55.358999999999995,29.509 -2020-07-22 16:30:00,96.79,32.354,55.358999999999995,29.509 -2020-07-22 16:45:00,97.88,29.37,55.358999999999995,29.509 -2020-07-22 17:00:00,99.85,33.293,59.211999999999996,29.509 -2020-07-22 17:15:00,99.72,33.529,59.211999999999996,29.509 -2020-07-22 17:30:00,100.86,32.388000000000005,59.211999999999996,29.509 -2020-07-22 17:45:00,101.07,31.495,59.211999999999996,29.509 -2020-07-22 18:00:00,101.72,34.8,60.403999999999996,29.509 -2020-07-22 18:15:00,102.59,34.605,60.403999999999996,29.509 -2020-07-22 18:30:00,100.5,32.693000000000005,60.403999999999996,29.509 -2020-07-22 18:45:00,100.9,35.45,60.403999999999996,29.509 -2020-07-22 19:00:00,97.19,36.861,60.993,29.509 -2020-07-22 19:15:00,97.17,35.821999999999996,60.993,29.509 -2020-07-22 19:30:00,92.14,34.623000000000005,60.993,29.509 -2020-07-22 19:45:00,91.77,33.551,60.993,29.509 -2020-07-22 20:00:00,90.35,31.044,66.6,29.509 -2020-07-22 20:15:00,91.78,30.253,66.6,29.509 -2020-07-22 20:30:00,95.13,30.684,66.6,29.509 -2020-07-22 20:45:00,92.78,31.101999999999997,66.6,29.509 -2020-07-22 21:00:00,90.14,29.88,59.855,29.509 -2020-07-22 21:15:00,88.39,31.054000000000002,59.855,29.509 -2020-07-22 21:30:00,85.36,31.596,59.855,29.509 -2020-07-22 21:45:00,84.32,32.446,59.855,29.509 -2020-07-22 22:00:00,79.02,29.726,54.942,29.509 -2020-07-22 22:15:00,78.99,32.55,54.942,29.509 -2020-07-22 22:30:00,76.96,28.224,54.942,29.509 -2020-07-22 22:45:00,79.38,25.072,54.942,29.509 -2020-07-22 23:00:00,71.81,21.918000000000003,46.056000000000004,29.509 -2020-07-22 23:15:00,73.46,20.590999999999998,46.056000000000004,29.509 -2020-07-22 23:30:00,72.52,19.585,46.056000000000004,29.509 -2020-07-22 23:45:00,71.69,18.542,46.056000000000004,29.509 -2020-07-23 00:00:00,68.3,18.047,40.859,29.509 -2020-07-23 00:15:00,70.02,18.944000000000003,40.859,29.509 -2020-07-23 00:30:00,68.78,17.644000000000002,40.859,29.509 -2020-07-23 00:45:00,69.1,17.771,40.859,29.509 -2020-07-23 01:00:00,68.25,17.789,39.06,29.509 -2020-07-23 01:15:00,68.94,16.949,39.06,29.509 -2020-07-23 01:30:00,67.5,15.479000000000001,39.06,29.509 -2020-07-23 01:45:00,67.58,15.552999999999999,39.06,29.509 -2020-07-23 02:00:00,66.85,15.42,37.592,29.509 -2020-07-23 02:15:00,71.03,14.513,37.592,29.509 -2020-07-23 02:30:00,76.38,16.089000000000002,37.592,29.509 -2020-07-23 02:45:00,76.3,16.827,37.592,29.509 -2020-07-23 03:00:00,71.84,17.772000000000002,37.416,29.509 -2020-07-23 03:15:00,72.28,17.354,37.416,29.509 -2020-07-23 03:30:00,79.96,16.701,37.416,29.509 -2020-07-23 03:45:00,81.25,15.972000000000001,37.416,29.509 -2020-07-23 04:00:00,80.97,20.04,38.176,29.509 -2020-07-23 04:15:00,74.82,25.546,38.176,29.509 -2020-07-23 04:30:00,77.22,22.83,38.176,29.509 -2020-07-23 04:45:00,84.81,22.756999999999998,38.176,29.509 -2020-07-23 05:00:00,87.82,34.391,41.203,29.509 -2020-07-23 05:15:00,95.47,40.189,41.203,29.509 -2020-07-23 05:30:00,100.21,35.435,41.203,29.509 -2020-07-23 05:45:00,104.43,33.103,41.203,29.509 -2020-07-23 06:00:00,105.17,33.383,51.09,29.509 -2020-07-23 06:15:00,102.39,32.779,51.09,29.509 -2020-07-23 06:30:00,107.01,32.573,51.09,29.509 -2020-07-23 06:45:00,109.32,35.289,51.09,29.509 -2020-07-23 07:00:00,113.51,35.048,63.541000000000004,29.509 -2020-07-23 07:15:00,107.64,35.925,63.541000000000004,29.509 -2020-07-23 07:30:00,106.21,33.898,63.541000000000004,29.509 -2020-07-23 07:45:00,107.98,34.539,63.541000000000004,29.509 -2020-07-23 08:00:00,109.58,29.919,55.65,29.509 -2020-07-23 08:15:00,109.46,33.257,55.65,29.509 -2020-07-23 08:30:00,109.2,34.72,55.65,29.509 -2020-07-23 08:45:00,111.06,37.368,55.65,29.509 -2020-07-23 09:00:00,111.53,33.069,51.833999999999996,29.509 -2020-07-23 09:15:00,109.8,32.566,51.833999999999996,29.509 -2020-07-23 09:30:00,107.6,36.308,51.833999999999996,29.509 -2020-07-23 09:45:00,106.21,39.592,51.833999999999996,29.509 -2020-07-23 10:00:00,111.27,37.133,49.70399999999999,29.509 -2020-07-23 10:15:00,114.48,38.61,49.70399999999999,29.509 -2020-07-23 10:30:00,116.26,38.482,49.70399999999999,29.509 -2020-07-23 10:45:00,112.02,39.472,49.70399999999999,29.509 -2020-07-23 11:00:00,112.26,37.147,48.593999999999994,29.509 -2020-07-23 11:15:00,110.11,38.439,48.593999999999994,29.509 -2020-07-23 11:30:00,107.88,39.608000000000004,48.593999999999994,29.509 -2020-07-23 11:45:00,103.41,40.667,48.593999999999994,29.509 -2020-07-23 12:00:00,100.19,35.335,46.275,29.509 -2020-07-23 12:15:00,101.53,34.614000000000004,46.275,29.509 -2020-07-23 12:30:00,98.55,33.36,46.275,29.509 -2020-07-23 12:45:00,97.18,34.318000000000005,46.275,29.509 -2020-07-23 13:00:00,95.62,34.154,45.803000000000004,29.509 -2020-07-23 13:15:00,94.73,35.516,45.803000000000004,29.509 -2020-07-23 13:30:00,96.45,33.664,45.803000000000004,29.509 -2020-07-23 13:45:00,95.72,33.7,45.803000000000004,29.509 -2020-07-23 14:00:00,95.01,35.48,46.251999999999995,29.509 -2020-07-23 14:15:00,93.53,34.498000000000005,46.251999999999995,29.509 -2020-07-23 14:30:00,92.34,33.518,46.251999999999995,29.509 -2020-07-23 14:45:00,91.59,34.177,46.251999999999995,29.509 -2020-07-23 15:00:00,90.99,36.466,48.309,29.509 -2020-07-23 15:15:00,91.91,34.086,48.309,29.509 -2020-07-23 15:30:00,92.29,32.647,48.309,29.509 -2020-07-23 15:45:00,98.7,30.991999999999997,48.309,29.509 -2020-07-23 16:00:00,94.54,33.385,49.681999999999995,29.509 -2020-07-23 16:15:00,95.86,33.22,49.681999999999995,29.509 -2020-07-23 16:30:00,97.35,32.391999999999996,49.681999999999995,29.509 -2020-07-23 16:45:00,99.16,29.426,49.681999999999995,29.509 -2020-07-23 17:00:00,99.71,33.339,53.086000000000006,29.509 -2020-07-23 17:15:00,100.72,33.598,53.086000000000006,29.509 -2020-07-23 17:30:00,104.23,32.466,53.086000000000006,29.509 -2020-07-23 17:45:00,102.39,31.589000000000002,53.086000000000006,29.509 -2020-07-23 18:00:00,103.82,34.895,54.038999999999994,29.509 -2020-07-23 18:15:00,102.29,34.695,54.038999999999994,29.509 -2020-07-23 18:30:00,102.15,32.79,54.038999999999994,29.509 -2020-07-23 18:45:00,101.8,35.545,54.038999999999994,29.509 -2020-07-23 19:00:00,98.33,36.96,53.408,29.509 -2020-07-23 19:15:00,94.82,35.916,53.408,29.509 -2020-07-23 19:30:00,93.96,34.714,53.408,29.509 -2020-07-23 19:45:00,92.17,33.641,53.408,29.509 -2020-07-23 20:00:00,91.92,31.128,55.309,29.509 -2020-07-23 20:15:00,92.98,30.337,55.309,29.509 -2020-07-23 20:30:00,94.08,30.759,55.309,29.509 -2020-07-23 20:45:00,96.13,31.17,55.309,29.509 -2020-07-23 21:00:00,89.56,29.951999999999998,51.585,29.509 -2020-07-23 21:15:00,88.93,31.123,51.585,29.509 -2020-07-23 21:30:00,84.47,31.65,51.585,29.509 -2020-07-23 21:45:00,83.82,32.483000000000004,51.585,29.509 -2020-07-23 22:00:00,79.54,29.758000000000003,48.006,29.509 -2020-07-23 22:15:00,79.02,32.576,48.006,29.509 -2020-07-23 22:30:00,77.0,28.225,48.006,29.509 -2020-07-23 22:45:00,76.14,25.069000000000003,48.006,29.509 -2020-07-23 23:00:00,71.58,21.935,42.309,29.509 -2020-07-23 23:15:00,73.24,20.619,42.309,29.509 -2020-07-23 23:30:00,70.94,19.628,42.309,29.509 -2020-07-23 23:45:00,70.55,18.584,42.309,29.509 -2020-07-24 00:00:00,68.03,16.248,39.649,29.509 -2020-07-24 00:15:00,69.75,17.363,39.649,29.509 -2020-07-24 00:30:00,68.38,16.374000000000002,39.649,29.509 -2020-07-24 00:45:00,68.41,16.957,39.649,29.509 -2020-07-24 01:00:00,66.25,16.582,37.744,29.509 -2020-07-24 01:15:00,67.66,15.015,37.744,29.509 -2020-07-24 01:30:00,66.78,14.513,37.744,29.509 -2020-07-24 01:45:00,67.15,14.513,37.744,29.509 -2020-07-24 02:00:00,66.84,15.012,36.965,29.509 -2020-07-24 02:15:00,67.65,14.513,36.965,29.509 -2020-07-24 02:30:00,73.02,16.486,36.965,29.509 -2020-07-24 02:45:00,75.8,16.48,36.965,29.509 -2020-07-24 03:00:00,71.52,18.334,37.678000000000004,29.509 -2020-07-24 03:15:00,70.7,16.549,37.678000000000004,29.509 -2020-07-24 03:30:00,77.5,15.645999999999999,37.678000000000004,29.509 -2020-07-24 03:45:00,79.57,15.852,37.678000000000004,29.509 -2020-07-24 04:00:00,80.0,20.035999999999998,38.591,29.509 -2020-07-24 04:15:00,75.24,23.857,38.591,29.509 -2020-07-24 04:30:00,76.43,22.151999999999997,38.591,29.509 -2020-07-24 04:45:00,79.48,21.465,38.591,29.509 -2020-07-24 05:00:00,87.6,32.664,40.666,29.509 -2020-07-24 05:15:00,90.22,39.36,40.666,29.509 -2020-07-24 05:30:00,93.14,34.783,40.666,29.509 -2020-07-24 05:45:00,91.92,31.98,40.666,29.509 -2020-07-24 06:00:00,106.2,32.453,51.784,29.509 -2020-07-24 06:15:00,107.22,32.135,51.784,29.509 -2020-07-24 06:30:00,110.0,31.943,51.784,29.509 -2020-07-24 06:45:00,107.88,34.431999999999995,51.784,29.509 -2020-07-24 07:00:00,114.31,34.941,61.383,29.509 -2020-07-24 07:15:00,115.72,36.741,61.383,29.509 -2020-07-24 07:30:00,112.69,32.617,61.383,29.509 -2020-07-24 07:45:00,107.73,33.121,61.383,29.509 -2020-07-24 08:00:00,109.58,29.456,55.272,29.509 -2020-07-24 08:15:00,115.15,33.56,55.272,29.509 -2020-07-24 08:30:00,122.28,34.815,55.272,29.509 -2020-07-24 08:45:00,123.43,37.437,55.272,29.509 -2020-07-24 09:00:00,124.99,30.558000000000003,53.506,29.509 -2020-07-24 09:15:00,124.06,32.117,53.506,29.509 -2020-07-24 09:30:00,113.17,35.128,53.506,29.509 -2020-07-24 09:45:00,102.68,38.842,53.506,29.509 -2020-07-24 10:00:00,108.12,36.326,51.363,29.509 -2020-07-24 10:15:00,109.83,37.472,51.363,29.509 -2020-07-24 10:30:00,109.0,37.959,51.363,29.509 -2020-07-24 10:45:00,104.36,38.887,51.363,29.509 -2020-07-24 11:00:00,106.38,36.842,51.043,29.509 -2020-07-24 11:15:00,106.17,37.016999999999996,51.043,29.509 -2020-07-24 11:30:00,105.41,37.599000000000004,51.043,29.509 -2020-07-24 11:45:00,102.94,37.579,51.043,29.509 -2020-07-24 12:00:00,101.2,32.623000000000005,47.52,29.509 -2020-07-24 12:15:00,101.62,31.401999999999997,47.52,29.509 -2020-07-24 12:30:00,104.8,30.261,47.52,29.509 -2020-07-24 12:45:00,107.68,30.285,47.52,29.509 -2020-07-24 13:00:00,109.01,30.66,45.494,29.509 -2020-07-24 13:15:00,111.33,32.129,45.494,29.509 -2020-07-24 13:30:00,109.44,31.159000000000002,45.494,29.509 -2020-07-24 13:45:00,103.01,31.551,45.494,29.509 -2020-07-24 14:00:00,93.71,32.553000000000004,43.883,29.509 -2020-07-24 14:15:00,95.41,32.075,43.883,29.509 -2020-07-24 14:30:00,104.39,32.713,43.883,29.509 -2020-07-24 14:45:00,101.16,32.585,43.883,29.509 -2020-07-24 15:00:00,96.2,34.821,45.714,29.509 -2020-07-24 15:15:00,94.94,32.219,45.714,29.509 -2020-07-24 15:30:00,91.29,30.305999999999997,45.714,29.509 -2020-07-24 15:45:00,89.73,29.476,45.714,29.509 -2020-07-24 16:00:00,90.1,31.035999999999998,48.222,29.509 -2020-07-24 16:15:00,94.51,31.392,48.222,29.509 -2020-07-24 16:30:00,101.04,30.37,48.222,29.509 -2020-07-24 16:45:00,100.07,26.570999999999998,48.222,29.509 -2020-07-24 17:00:00,103.96,32.347,52.619,29.509 -2020-07-24 17:15:00,101.89,32.475,52.619,29.509 -2020-07-24 17:30:00,100.32,31.561,52.619,29.509 -2020-07-24 17:45:00,99.63,30.541999999999998,52.619,29.509 -2020-07-24 18:00:00,100.58,33.814,52.99,29.509 -2020-07-24 18:15:00,97.61,32.611999999999995,52.99,29.509 -2020-07-24 18:30:00,96.16,30.566,52.99,29.509 -2020-07-24 18:45:00,95.07,33.78,52.99,29.509 -2020-07-24 19:00:00,91.05,36.055,51.923,29.509 -2020-07-24 19:15:00,89.09,35.54,51.923,29.509 -2020-07-24 19:30:00,87.63,34.434,51.923,29.509 -2020-07-24 19:45:00,86.2,32.297,51.923,29.509 -2020-07-24 20:00:00,85.56,29.603,56.238,29.509 -2020-07-24 20:15:00,85.67,29.671,56.238,29.509 -2020-07-24 20:30:00,86.67,29.561,56.238,29.509 -2020-07-24 20:45:00,85.85,29.044,56.238,29.509 -2020-07-24 21:00:00,80.66,29.201999999999998,52.426,29.509 -2020-07-24 21:15:00,79.89,32.137,52.426,29.509 -2020-07-24 21:30:00,76.38,32.484,52.426,29.509 -2020-07-24 21:45:00,77.28,33.479,52.426,29.509 -2020-07-24 22:00:00,72.52,30.523000000000003,48.196000000000005,29.509 -2020-07-24 22:15:00,73.32,33.078,48.196000000000005,29.509 -2020-07-24 22:30:00,71.32,33.355,48.196000000000005,29.509 -2020-07-24 22:45:00,70.83,30.868000000000002,48.196000000000005,29.509 -2020-07-24 23:00:00,64.51,29.603,41.71,29.509 -2020-07-24 23:15:00,66.35,26.752,41.71,29.509 -2020-07-24 23:30:00,63.12,23.924,41.71,29.509 -2020-07-24 23:45:00,64.62,22.791,41.71,29.509 -2020-07-25 00:00:00,60.52,17.883,41.105,29.398000000000003 -2020-07-25 00:15:00,63.43,18.605999999999998,41.105,29.398000000000003 -2020-07-25 00:30:00,61.73,17.007,41.105,29.398000000000003 -2020-07-25 00:45:00,60.92,16.749000000000002,41.105,29.398000000000003 -2020-07-25 01:00:00,58.7,16.616,36.934,29.398000000000003 -2020-07-25 01:15:00,60.36,15.764000000000001,36.934,29.398000000000003 -2020-07-25 01:30:00,59.34,14.513,36.934,29.398000000000003 -2020-07-25 01:45:00,59.81,15.419,36.934,29.398000000000003 -2020-07-25 02:00:00,63.49,15.175999999999998,34.782,29.398000000000003 -2020-07-25 02:15:00,67.28,14.513,34.782,29.398000000000003 -2020-07-25 02:30:00,64.51,15.055,34.782,29.398000000000003 -2020-07-25 02:45:00,58.8,15.890999999999998,34.782,29.398000000000003 -2020-07-25 03:00:00,58.48,16.201,34.489000000000004,29.398000000000003 -2020-07-25 03:15:00,60.49,14.513,34.489000000000004,29.398000000000003 -2020-07-25 03:30:00,60.97,14.513,34.489000000000004,29.398000000000003 -2020-07-25 03:45:00,67.24,15.163,34.489000000000004,29.398000000000003 -2020-07-25 04:00:00,67.49,17.617,34.111,29.398000000000003 -2020-07-25 04:15:00,64.17,20.659000000000002,34.111,29.398000000000003 -2020-07-25 04:30:00,56.16,17.320999999999998,34.111,29.398000000000003 -2020-07-25 04:45:00,60.64,16.969,34.111,29.398000000000003 -2020-07-25 05:00:00,64.22,20.432000000000002,33.283,29.398000000000003 -2020-07-25 05:15:00,62.07,16.791,33.283,29.398000000000003 -2020-07-25 05:30:00,66.1,14.513,33.283,29.398000000000003 -2020-07-25 05:45:00,71.54,15.429,33.283,29.398000000000003 -2020-07-25 06:00:00,72.53,26.487,33.653,29.398000000000003 -2020-07-25 06:15:00,71.91,33.288000000000004,33.653,29.398000000000003 -2020-07-25 06:30:00,69.01,30.473000000000003,33.653,29.398000000000003 -2020-07-25 06:45:00,69.56,30.228,33.653,29.398000000000003 -2020-07-25 07:00:00,73.11,29.875999999999998,36.732,29.398000000000003 -2020-07-25 07:15:00,76.36,30.405,36.732,29.398000000000003 -2020-07-25 07:30:00,78.06,27.684,36.732,29.398000000000003 -2020-07-25 07:45:00,80.04,28.840999999999998,36.732,29.398000000000003 -2020-07-25 08:00:00,77.09,25.694000000000003,41.318999999999996,29.398000000000003 -2020-07-25 08:15:00,79.78,29.186,41.318999999999996,29.398000000000003 -2020-07-25 08:30:00,80.24,30.247,41.318999999999996,29.398000000000003 -2020-07-25 08:45:00,80.25,33.574,41.318999999999996,29.398000000000003 -2020-07-25 09:00:00,76.95,29.991999999999997,43.195,29.398000000000003 -2020-07-25 09:15:00,80.1,31.998,43.195,29.398000000000003 -2020-07-25 09:30:00,78.95,35.445,43.195,29.398000000000003 -2020-07-25 09:45:00,77.75,38.635999999999996,43.195,29.398000000000003 -2020-07-25 10:00:00,76.41,36.813,41.843999999999994,29.398000000000003 -2020-07-25 10:15:00,81.4,38.321999999999996,41.843999999999994,29.398000000000003 -2020-07-25 10:30:00,76.14,38.407,41.843999999999994,29.398000000000003 -2020-07-25 10:45:00,74.84,38.793,41.843999999999994,29.398000000000003 -2020-07-25 11:00:00,74.35,36.548,39.035,29.398000000000003 -2020-07-25 11:15:00,74.37,37.754,39.035,29.398000000000003 -2020-07-25 11:30:00,82.18,38.760999999999996,39.035,29.398000000000003 -2020-07-25 11:45:00,79.36,39.576,39.035,29.398000000000003 -2020-07-25 12:00:00,73.47,35.176,38.001,29.398000000000003 -2020-07-25 12:15:00,71.39,34.818000000000005,38.001,29.398000000000003 -2020-07-25 12:30:00,73.92,33.497,38.001,29.398000000000003 -2020-07-25 12:45:00,73.51,34.403,38.001,29.398000000000003 -2020-07-25 13:00:00,62.75,33.873000000000005,34.747,29.398000000000003 -2020-07-25 13:15:00,60.52,34.961,34.747,29.398000000000003 -2020-07-25 13:30:00,64.66,34.251,34.747,29.398000000000003 -2020-07-25 13:45:00,71.77,33.11,34.747,29.398000000000003 -2020-07-25 14:00:00,74.39,33.992,33.434,29.398000000000003 -2020-07-25 14:15:00,80.98,32.196,33.434,29.398000000000003 -2020-07-25 14:30:00,77.88,32.56,33.434,29.398000000000003 -2020-07-25 14:45:00,74.01,32.943000000000005,33.434,29.398000000000003 -2020-07-25 15:00:00,67.72,35.54,35.921,29.398000000000003 -2020-07-25 15:15:00,63.96,33.629,35.921,29.398000000000003 -2020-07-25 15:30:00,60.29,31.785,35.921,29.398000000000003 -2020-07-25 15:45:00,70.07,29.939,35.921,29.398000000000003 -2020-07-25 16:00:00,80.46,33.92,39.427,29.398000000000003 -2020-07-25 16:15:00,81.09,33.215,39.427,29.398000000000003 -2020-07-25 16:30:00,81.48,32.46,39.427,29.398000000000003 -2020-07-25 16:45:00,81.44,28.561,39.427,29.398000000000003 -2020-07-25 17:00:00,83.25,33.041,44.096000000000004,29.398000000000003 -2020-07-25 17:15:00,81.38,30.686,44.096000000000004,29.398000000000003 -2020-07-25 17:30:00,81.62,29.645,44.096000000000004,29.398000000000003 -2020-07-25 17:45:00,80.18,29.224,44.096000000000004,29.398000000000003 -2020-07-25 18:00:00,82.87,33.99,43.931000000000004,29.398000000000003 -2020-07-25 18:15:00,82.76,34.434,43.931000000000004,29.398000000000003 -2020-07-25 18:30:00,83.27,33.746,43.931000000000004,29.398000000000003 -2020-07-25 18:45:00,81.38,33.495,43.931000000000004,29.398000000000003 -2020-07-25 19:00:00,79.1,33.891999999999996,42.187,29.398000000000003 -2020-07-25 19:15:00,73.98,32.328,42.187,29.398000000000003 -2020-07-25 19:30:00,74.69,31.985,42.187,29.398000000000003 -2020-07-25 19:45:00,73.47,31.756999999999998,42.187,29.398000000000003 -2020-07-25 20:00:00,74.38,29.776,38.315,29.398000000000003 -2020-07-25 20:15:00,74.87,28.994,38.315,29.398000000000003 -2020-07-25 20:30:00,74.53,27.961,38.315,29.398000000000003 -2020-07-25 20:45:00,73.21,29.456,38.315,29.398000000000003 -2020-07-25 21:00:00,69.25,27.854,36.843,29.398000000000003 -2020-07-25 21:15:00,69.72,30.38,36.843,29.398000000000003 -2020-07-25 21:30:00,66.85,30.825,36.843,29.398000000000003 -2020-07-25 21:45:00,66.91,31.249000000000002,36.843,29.398000000000003 -2020-07-25 22:00:00,63.87,28.166999999999998,37.260999999999996,29.398000000000003 -2020-07-25 22:15:00,65.21,30.805999999999997,37.260999999999996,29.398000000000003 -2020-07-25 22:30:00,60.75,30.22,37.260999999999996,29.398000000000003 -2020-07-25 22:45:00,62.66,27.993000000000002,37.260999999999996,29.398000000000003 -2020-07-25 23:00:00,57.37,25.910999999999998,32.148,29.398000000000003 -2020-07-25 23:15:00,58.97,23.625,32.148,29.398000000000003 -2020-07-25 23:30:00,57.29,23.531,32.148,29.398000000000003 -2020-07-25 23:45:00,57.1,22.938000000000002,32.148,29.398000000000003 -2020-07-26 00:00:00,52.63,19.403,28.905,29.398000000000003 -2020-07-26 00:15:00,55.31,18.910999999999998,28.905,29.398000000000003 -2020-07-26 00:30:00,54.02,17.189,28.905,29.398000000000003 -2020-07-26 00:45:00,54.11,16.762999999999998,28.905,29.398000000000003 -2020-07-26 01:00:00,51.5,16.92,26.906999999999996,29.398000000000003 -2020-07-26 01:15:00,52.88,15.854000000000001,26.906999999999996,29.398000000000003 -2020-07-26 01:30:00,53.34,14.513,26.906999999999996,29.398000000000003 -2020-07-26 01:45:00,53.47,14.937999999999999,26.906999999999996,29.398000000000003 -2020-07-26 02:00:00,55.77,14.847000000000001,25.938000000000002,29.398000000000003 -2020-07-26 02:15:00,52.74,14.513,25.938000000000002,29.398000000000003 -2020-07-26 02:30:00,51.94,15.683,25.938000000000002,29.398000000000003 -2020-07-26 02:45:00,52.62,16.17,25.938000000000002,29.398000000000003 -2020-07-26 03:00:00,52.69,17.144000000000002,24.693,29.398000000000003 -2020-07-26 03:15:00,52.83,15.054,24.693,29.398000000000003 -2020-07-26 03:30:00,53.82,14.513,24.693,29.398000000000003 -2020-07-26 03:45:00,53.8,14.713,24.693,29.398000000000003 -2020-07-26 04:00:00,53.27,17.141,25.683000000000003,29.398000000000003 -2020-07-26 04:15:00,52.43,19.784000000000002,25.683000000000003,29.398000000000003 -2020-07-26 04:30:00,51.66,17.875999999999998,25.683000000000003,29.398000000000003 -2020-07-26 04:45:00,52.2,17.003,25.683000000000003,29.398000000000003 -2020-07-26 05:00:00,51.2,20.91,26.023000000000003,29.398000000000003 -2020-07-26 05:15:00,51.8,16.61,26.023000000000003,29.398000000000003 -2020-07-26 05:30:00,52.03,14.513,26.023000000000003,29.398000000000003 -2020-07-26 05:45:00,52.83,14.56,26.023000000000003,29.398000000000003 -2020-07-26 06:00:00,54.33,23.101,25.834,29.398000000000003 -2020-07-26 06:15:00,54.38,30.927,25.834,29.398000000000003 -2020-07-26 06:30:00,54.62,27.465999999999998,25.834,29.398000000000003 -2020-07-26 06:45:00,55.22,26.263,25.834,29.398000000000003 -2020-07-26 07:00:00,57.6,26.066,27.765,29.398000000000003 -2020-07-26 07:15:00,56.19,24.92,27.765,29.398000000000003 -2020-07-26 07:30:00,57.26,23.711,27.765,29.398000000000003 -2020-07-26 07:45:00,57.26,24.921,27.765,29.398000000000003 -2020-07-26 08:00:00,55.54,22.46,31.357,29.398000000000003 -2020-07-26 08:15:00,55.35,27.239,31.357,29.398000000000003 -2020-07-26 08:30:00,54.77,29.068,31.357,29.398000000000003 -2020-07-26 08:45:00,57.54,32.083,31.357,29.398000000000003 -2020-07-26 09:00:00,55.42,28.44,33.238,29.398000000000003 -2020-07-26 09:15:00,54.51,29.836,33.238,29.398000000000003 -2020-07-26 09:30:00,54.13,33.762,33.238,29.398000000000003 -2020-07-26 09:45:00,56.02,38.06,33.238,29.398000000000003 -2020-07-26 10:00:00,57.58,36.551,34.22,29.398000000000003 -2020-07-26 10:15:00,58.34,38.132,34.22,29.398000000000003 -2020-07-26 10:30:00,58.17,38.382,34.22,29.398000000000003 -2020-07-26 10:45:00,57.3,40.123000000000005,34.22,29.398000000000003 -2020-07-26 11:00:00,52.42,37.379,36.298,29.398000000000003 -2020-07-26 11:15:00,55.26,38.073,36.298,29.398000000000003 -2020-07-26 11:30:00,54.51,39.75,36.298,29.398000000000003 -2020-07-26 11:45:00,58.89,40.765,36.298,29.398000000000003 -2020-07-26 12:00:00,51.91,37.62,33.52,29.398000000000003 -2020-07-26 12:15:00,53.07,36.328,33.52,29.398000000000003 -2020-07-26 12:30:00,55.05,35.425,33.52,29.398000000000003 -2020-07-26 12:45:00,57.13,35.735,33.52,29.398000000000003 -2020-07-26 13:00:00,54.79,34.94,30.12,29.398000000000003 -2020-07-26 13:15:00,57.11,35.007,30.12,29.398000000000003 -2020-07-26 13:30:00,57.12,33.091,30.12,29.398000000000003 -2020-07-26 13:45:00,53.35,33.279,30.12,29.398000000000003 -2020-07-26 14:00:00,53.46,35.445,27.233,29.398000000000003 -2020-07-26 14:15:00,50.5,33.985,27.233,29.398000000000003 -2020-07-26 14:30:00,48.85,32.835,27.233,29.398000000000003 -2020-07-26 14:45:00,48.31,32.092,27.233,29.398000000000003 -2020-07-26 15:00:00,50.34,35.024,27.468000000000004,29.398000000000003 -2020-07-26 15:15:00,51.44,32.132,27.468000000000004,29.398000000000003 -2020-07-26 15:30:00,51.5,30.025,27.468000000000004,29.398000000000003 -2020-07-26 15:45:00,56.42,28.409000000000002,27.468000000000004,29.398000000000003 -2020-07-26 16:00:00,57.3,30.403000000000002,30.8,29.398000000000003 -2020-07-26 16:15:00,58.79,30.057,30.8,29.398000000000003 -2020-07-26 16:30:00,61.59,30.386999999999997,30.8,29.398000000000003 -2020-07-26 16:45:00,60.88,26.564,30.8,29.398000000000003 -2020-07-26 17:00:00,65.37,31.451,37.806,29.398000000000003 -2020-07-26 17:15:00,66.63,30.805999999999997,37.806,29.398000000000003 -2020-07-26 17:30:00,68.97,30.625999999999998,37.806,29.398000000000003 -2020-07-26 17:45:00,71.33,30.439,37.806,29.398000000000003 -2020-07-26 18:00:00,72.0,35.931999999999995,40.766,29.398000000000003 -2020-07-26 18:15:00,74.61,35.803000000000004,40.766,29.398000000000003 -2020-07-26 18:30:00,73.46,35.058,40.766,29.398000000000003 -2020-07-26 18:45:00,72.82,34.749,40.766,29.398000000000003 -2020-07-26 19:00:00,75.82,37.553000000000004,41.163000000000004,29.398000000000003 -2020-07-26 19:15:00,72.02,34.741,41.163000000000004,29.398000000000003 -2020-07-26 19:30:00,72.59,34.144,41.163000000000004,29.398000000000003 -2020-07-26 19:45:00,73.1,33.296,41.163000000000004,29.398000000000003 -2020-07-26 20:00:00,77.43,31.500999999999998,39.885999999999996,29.398000000000003 -2020-07-26 20:15:00,75.87,30.485,39.885999999999996,29.398000000000003 -2020-07-26 20:30:00,76.9,30.201999999999998,39.885999999999996,29.398000000000003 -2020-07-26 20:45:00,76.0,29.991,39.885999999999996,29.398000000000003 -2020-07-26 21:00:00,74.18,28.511,38.900999999999996,29.398000000000003 -2020-07-26 21:15:00,74.35,30.761999999999997,38.900999999999996,29.398000000000003 -2020-07-26 21:30:00,73.05,30.476999999999997,38.900999999999996,29.398000000000003 -2020-07-26 21:45:00,74.93,31.252,38.900999999999996,29.398000000000003 -2020-07-26 22:00:00,69.15,30.444000000000003,39.806999999999995,29.398000000000003 -2020-07-26 22:15:00,68.91,31.346999999999998,39.806999999999995,29.398000000000003 -2020-07-26 22:30:00,67.21,30.45,39.806999999999995,29.398000000000003 -2020-07-26 22:45:00,66.92,26.933000000000003,39.806999999999995,29.398000000000003 -2020-07-26 23:00:00,62.44,24.776,35.564,29.398000000000003 -2020-07-26 23:15:00,64.2,23.701,35.564,29.398000000000003 -2020-07-26 23:30:00,63.75,22.988000000000003,35.564,29.398000000000003 -2020-07-26 23:45:00,63.65,22.491,35.564,29.398000000000003 -2020-07-27 00:00:00,58.08,20.721999999999998,36.578,29.509 -2020-07-27 00:15:00,59.82,20.807,36.578,29.509 -2020-07-27 00:30:00,59.44,18.666,36.578,29.509 -2020-07-27 00:45:00,59.65,17.87,36.578,29.509 -2020-07-27 01:00:00,58.51,18.438,35.292,29.509 -2020-07-27 01:15:00,59.94,17.435,35.292,29.509 -2020-07-27 01:30:00,58.16,16.151,35.292,29.509 -2020-07-27 01:45:00,59.68,16.791,35.292,29.509 -2020-07-27 02:00:00,59.36,17.198,34.319,29.509 -2020-07-27 02:15:00,60.33,14.605,34.319,29.509 -2020-07-27 02:30:00,60.15,16.959,34.319,29.509 -2020-07-27 02:45:00,68.55,17.345,34.319,29.509 -2020-07-27 03:00:00,69.23,18.741,33.13,29.509 -2020-07-27 03:15:00,68.09,17.291,33.13,29.509 -2020-07-27 03:30:00,64.78,16.747,33.13,29.509 -2020-07-27 03:45:00,67.17,17.275,33.13,29.509 -2020-07-27 04:00:00,69.51,22.643,33.851,29.509 -2020-07-27 04:15:00,71.44,28.05,33.851,29.509 -2020-07-27 04:30:00,75.51,25.430999999999997,33.851,29.509 -2020-07-27 04:45:00,75.8,24.941,33.851,29.509 -2020-07-27 05:00:00,83.51,35.275999999999996,38.718,29.509 -2020-07-27 05:15:00,93.98,40.316,38.718,29.509 -2020-07-27 05:30:00,96.18,35.374,38.718,29.509 -2020-07-27 05:45:00,97.7,33.821,38.718,29.509 -2020-07-27 06:00:00,98.3,32.796,51.648999999999994,29.509 -2020-07-27 06:15:00,101.88,32.159,51.648999999999994,29.509 -2020-07-27 06:30:00,101.45,32.286,51.648999999999994,29.509 -2020-07-27 06:45:00,102.71,36.009,51.648999999999994,29.509 -2020-07-27 07:00:00,109.88,35.56,60.159,29.509 -2020-07-27 07:15:00,110.12,36.783,60.159,29.509 -2020-07-27 07:30:00,108.64,34.733000000000004,60.159,29.509 -2020-07-27 07:45:00,103.69,36.539,60.159,29.509 -2020-07-27 08:00:00,102.39,32.04,53.8,29.509 -2020-07-27 08:15:00,101.23,35.729,53.8,29.509 -2020-07-27 08:30:00,102.32,36.937,53.8,29.509 -2020-07-27 08:45:00,101.63,40.533,53.8,29.509 -2020-07-27 09:00:00,100.95,35.754,50.583,29.509 -2020-07-27 09:15:00,100.48,35.586,50.583,29.509 -2020-07-27 09:30:00,99.92,38.594,50.583,29.509 -2020-07-27 09:45:00,102.25,40.314,50.583,29.509 -2020-07-27 10:00:00,100.85,39.336,49.11600000000001,29.509 -2020-07-27 10:15:00,105.36,40.782,49.11600000000001,29.509 -2020-07-27 10:30:00,110.58,40.625,49.11600000000001,29.509 -2020-07-27 10:45:00,104.99,40.507,49.11600000000001,29.509 -2020-07-27 11:00:00,104.14,38.387,49.056000000000004,29.509 -2020-07-27 11:15:00,99.79,39.169000000000004,49.056000000000004,29.509 -2020-07-27 11:30:00,97.15,41.443999999999996,49.056000000000004,29.509 -2020-07-27 11:45:00,97.01,43.053000000000004,49.056000000000004,29.509 -2020-07-27 12:00:00,94.59,37.788000000000004,47.227,29.509 -2020-07-27 12:15:00,96.64,36.614000000000004,47.227,29.509 -2020-07-27 12:30:00,94.46,34.49,47.227,29.509 -2020-07-27 12:45:00,96.43,34.6,47.227,29.509 -2020-07-27 13:00:00,94.41,34.707,47.006,29.509 -2020-07-27 13:15:00,98.63,33.953,47.006,29.509 -2020-07-27 13:30:00,92.15,32.293,47.006,29.509 -2020-07-27 13:45:00,92.79,33.483000000000004,47.006,29.509 -2020-07-27 14:00:00,96.24,34.769,47.19,29.509 -2020-07-27 14:15:00,94.96,34.046,47.19,29.509 -2020-07-27 14:30:00,97.53,32.781,47.19,29.509 -2020-07-27 14:45:00,94.66,34.342,47.19,29.509 -2020-07-27 15:00:00,96.06,36.696,47.846000000000004,29.509 -2020-07-27 15:15:00,96.69,33.32,47.846000000000004,29.509 -2020-07-27 15:30:00,94.08,32.165,47.846000000000004,29.509 -2020-07-27 15:45:00,95.96,30.063000000000002,47.846000000000004,29.509 -2020-07-27 16:00:00,97.15,33.278,49.641000000000005,29.509 -2020-07-27 16:15:00,97.99,33.095,49.641000000000005,29.509 -2020-07-27 16:30:00,102.46,32.815,49.641000000000005,29.509 -2020-07-27 16:45:00,101.93,29.17,49.641000000000005,29.509 -2020-07-27 17:00:00,100.45,32.861999999999995,54.133,29.509 -2020-07-27 17:15:00,98.37,32.753,54.133,29.509 -2020-07-27 17:30:00,98.03,32.219,54.133,29.509 -2020-07-27 17:45:00,97.96,31.778000000000002,54.133,29.509 -2020-07-27 18:00:00,100.96,36.108000000000004,53.761,29.509 -2020-07-27 18:15:00,102.23,34.207,53.761,29.509 -2020-07-27 18:30:00,99.94,32.584,53.761,29.509 -2020-07-27 18:45:00,102.7,35.639,53.761,29.509 -2020-07-27 19:00:00,97.52,38.338,53.923,29.509 -2020-07-27 19:15:00,93.44,37.054,53.923,29.509 -2020-07-27 19:30:00,92.1,36.010999999999996,53.923,29.509 -2020-07-27 19:45:00,92.41,34.579,53.923,29.509 -2020-07-27 20:00:00,90.77,31.631999999999998,58.786,29.509 -2020-07-27 20:15:00,91.86,32.35,58.786,29.509 -2020-07-27 20:30:00,92.73,32.789,58.786,29.509 -2020-07-27 20:45:00,92.17,32.653,58.786,29.509 -2020-07-27 21:00:00,88.4,30.436,54.591,29.509 -2020-07-27 21:15:00,86.32,33.257,54.591,29.509 -2020-07-27 21:30:00,81.87,33.473,54.591,29.509 -2020-07-27 21:45:00,81.44,34.038000000000004,54.591,29.509 -2020-07-27 22:00:00,75.29,31.253,51.551,29.509 -2020-07-27 22:15:00,76.35,34.385999999999996,51.551,29.509 -2020-07-27 22:30:00,74.74,29.62,51.551,29.509 -2020-07-27 22:45:00,78.59,26.478,51.551,29.509 -2020-07-27 23:00:00,69.51,24.256,44.716,29.509 -2020-07-27 23:15:00,70.81,21.219,44.716,29.509 -2020-07-27 23:30:00,70.12,20.26,44.716,29.509 -2020-07-27 23:45:00,69.6,18.979,44.716,29.509 -2020-07-28 00:00:00,66.06,18.403,43.01,29.509 -2020-07-28 00:15:00,67.08,19.291,43.01,29.509 -2020-07-28 00:30:00,66.41,18.014,43.01,29.509 -2020-07-28 00:45:00,67.0,18.176,43.01,29.509 -2020-07-28 01:00:00,64.96,18.167,40.687,29.509 -2020-07-28 01:15:00,66.69,17.328,40.687,29.509 -2020-07-28 01:30:00,66.57,15.895,40.687,29.509 -2020-07-28 01:45:00,67.11,15.945,40.687,29.509 -2020-07-28 02:00:00,65.29,15.835,39.554,29.509 -2020-07-28 02:15:00,66.22,14.524000000000001,39.554,29.509 -2020-07-28 02:30:00,67.12,16.489,39.554,29.509 -2020-07-28 02:45:00,74.51,17.230999999999998,39.554,29.509 -2020-07-28 03:00:00,75.51,18.124000000000002,38.958,29.509 -2020-07-28 03:15:00,71.6,17.781,38.958,29.509 -2020-07-28 03:30:00,69.58,17.167,38.958,29.509 -2020-07-28 03:45:00,71.85,16.477999999999998,38.958,29.509 -2020-07-28 04:00:00,75.9,20.482,39.783,29.509 -2020-07-28 04:15:00,74.67,25.921999999999997,39.783,29.509 -2020-07-28 04:30:00,75.33,23.201999999999998,39.783,29.509 -2020-07-28 04:45:00,79.19,23.13,39.783,29.509 -2020-07-28 05:00:00,91.86,34.734,42.281000000000006,29.509 -2020-07-28 05:15:00,95.84,40.444,42.281000000000006,29.509 -2020-07-28 05:30:00,96.24,35.861,42.281000000000006,29.509 -2020-07-28 05:45:00,94.7,33.519,42.281000000000006,29.509 -2020-07-28 06:00:00,104.47,33.715,50.801,29.509 -2020-07-28 06:15:00,108.31,33.161,50.801,29.509 -2020-07-28 06:30:00,109.99,33.008,50.801,29.509 -2020-07-28 06:45:00,108.51,35.823,50.801,29.509 -2020-07-28 07:00:00,107.45,35.556,60.202,29.509 -2020-07-28 07:15:00,105.46,36.536,60.202,29.509 -2020-07-28 07:30:00,107.66,34.574,60.202,29.509 -2020-07-28 07:45:00,111.91,35.306,60.202,29.509 -2020-07-28 08:00:00,111.65,30.717,54.461000000000006,29.509 -2020-07-28 08:15:00,111.03,34.015,54.461000000000006,29.509 -2020-07-28 08:30:00,104.82,35.417,54.461000000000006,29.509 -2020-07-28 08:45:00,105.65,38.006,54.461000000000006,29.509 -2020-07-28 09:00:00,105.34,33.71,50.753,29.509 -2020-07-28 09:15:00,104.97,33.184,50.753,29.509 -2020-07-28 09:30:00,111.63,36.865,50.753,29.509 -2020-07-28 09:45:00,111.1,40.098,50.753,29.509 -2020-07-28 10:00:00,103.92,37.664,49.703,29.509 -2020-07-28 10:15:00,108.58,39.079,49.703,29.509 -2020-07-28 10:30:00,104.47,38.921,49.703,29.509 -2020-07-28 10:45:00,102.44,39.895,49.703,29.509 -2020-07-28 11:00:00,99.91,37.591,49.42100000000001,29.509 -2020-07-28 11:15:00,101.99,38.866,49.42100000000001,29.509 -2020-07-28 11:30:00,102.94,39.99,49.42100000000001,29.509 -2020-07-28 11:45:00,104.11,40.995,49.42100000000001,29.509 -2020-07-28 12:00:00,103.58,35.702,47.155,29.509 -2020-07-28 12:15:00,108.58,34.954,47.155,29.509 -2020-07-28 12:30:00,112.24,33.703,47.155,29.509 -2020-07-28 12:45:00,113.51,34.622,47.155,29.509 -2020-07-28 13:00:00,110.53,34.356,47.515,29.509 -2020-07-28 13:15:00,113.4,35.666,47.515,29.509 -2020-07-28 13:30:00,111.67,33.818000000000005,47.515,29.509 -2020-07-28 13:45:00,108.55,33.906,47.515,29.509 -2020-07-28 14:00:00,98.05,35.644,47.575,29.509 -2020-07-28 14:15:00,94.38,34.681,47.575,29.509 -2020-07-28 14:30:00,93.99,33.705,47.575,29.509 -2020-07-28 14:45:00,93.91,34.396,47.575,29.509 -2020-07-28 15:00:00,99.21,36.606,48.903,29.509 -2020-07-28 15:15:00,101.07,34.223,48.903,29.509 -2020-07-28 15:30:00,100.87,32.81,48.903,29.509 -2020-07-28 15:45:00,100.92,31.144000000000002,48.903,29.509 -2020-07-28 16:00:00,97.59,33.515,50.218999999999994,29.509 -2020-07-28 16:15:00,98.84,33.376999999999995,50.218999999999994,29.509 -2020-07-28 16:30:00,99.43,32.621,50.218999999999994,29.509 -2020-07-28 16:45:00,99.6,29.761,50.218999999999994,29.509 -2020-07-28 17:00:00,102.62,33.61,55.396,29.509 -2020-07-28 17:15:00,101.28,33.985,55.396,29.509 -2020-07-28 17:30:00,100.85,32.898,55.396,29.509 -2020-07-28 17:45:00,102.53,32.125,55.396,29.509 -2020-07-28 18:00:00,102.54,35.43,55.583999999999996,29.509 -2020-07-28 18:15:00,100.74,35.217,55.583999999999996,29.509 -2020-07-28 18:30:00,102.02,33.344,55.583999999999996,29.509 -2020-07-28 18:45:00,102.2,36.103,55.583999999999996,29.509 -2020-07-28 19:00:00,97.76,37.53,56.071000000000005,29.509 -2020-07-28 19:15:00,95.49,36.464,56.071000000000005,29.509 -2020-07-28 19:30:00,94.9,35.249,56.071000000000005,29.509 -2020-07-28 19:45:00,93.33,34.178000000000004,56.071000000000005,29.509 -2020-07-28 20:00:00,91.91,31.64,61.55,29.509 -2020-07-28 20:15:00,93.19,30.85,61.55,29.509 -2020-07-28 20:30:00,94.82,31.225,61.55,29.509 -2020-07-28 20:45:00,93.22,31.589000000000002,61.55,29.509 -2020-07-28 21:00:00,89.1,30.386999999999997,55.94,29.509 -2020-07-28 21:15:00,87.92,31.531999999999996,55.94,29.509 -2020-07-28 21:30:00,84.69,31.991999999999997,55.94,29.509 -2020-07-28 21:45:00,84.68,32.727,55.94,29.509 -2020-07-28 22:00:00,78.94,29.971,52.857,29.509 -2020-07-28 22:15:00,85.09,32.757,52.857,29.509 -2020-07-28 22:30:00,81.12,28.272,52.857,29.509 -2020-07-28 22:45:00,81.25,25.096999999999998,52.857,29.509 -2020-07-28 23:00:00,77.11,22.079,46.04,29.509 -2020-07-28 23:15:00,78.13,20.805999999999997,46.04,29.509 -2020-07-28 23:30:00,77.41,19.891,46.04,29.509 -2020-07-28 23:45:00,77.45,18.84,46.04,29.509 -2020-07-29 00:00:00,72.28,18.484,42.195,29.509 -2020-07-29 00:15:00,73.33,19.371,42.195,29.509 -2020-07-29 00:30:00,73.27,18.098,42.195,29.509 -2020-07-29 00:45:00,72.85,18.268,42.195,29.509 -2020-07-29 01:00:00,71.33,18.252,38.82,29.509 -2020-07-29 01:15:00,72.86,17.414,38.82,29.509 -2020-07-29 01:30:00,73.45,15.99,38.82,29.509 -2020-07-29 01:45:00,73.58,16.035,38.82,29.509 -2020-07-29 02:00:00,71.67,15.93,37.023,29.509 -2020-07-29 02:15:00,72.3,14.637,37.023,29.509 -2020-07-29 02:30:00,69.76,16.581,37.023,29.509 -2020-07-29 02:45:00,79.06,17.323,37.023,29.509 -2020-07-29 03:00:00,79.64,18.204,36.818000000000005,29.509 -2020-07-29 03:15:00,77.13,17.878,36.818000000000005,29.509 -2020-07-29 03:30:00,73.32,17.271,36.818000000000005,29.509 -2020-07-29 03:45:00,77.35,16.589000000000002,36.818000000000005,29.509 -2020-07-29 04:00:00,79.17,20.584,37.495,29.509 -2020-07-29 04:15:00,78.45,26.015,37.495,29.509 -2020-07-29 04:30:00,78.67,23.294,37.495,29.509 -2020-07-29 04:45:00,81.9,23.224,37.495,29.509 -2020-07-29 05:00:00,97.1,34.83,39.858000000000004,29.509 -2020-07-29 05:15:00,99.54,40.534,39.858000000000004,29.509 -2020-07-29 05:30:00,103.49,35.981,39.858000000000004,29.509 -2020-07-29 05:45:00,98.13,33.632,39.858000000000004,29.509 -2020-07-29 06:00:00,105.66,33.81,52.867,29.509 -2020-07-29 06:15:00,110.73,33.266999999999996,52.867,29.509 -2020-07-29 06:30:00,111.3,33.122,52.867,29.509 -2020-07-29 06:45:00,112.66,35.955,52.867,29.509 -2020-07-29 07:00:00,108.85,35.685,66.061,29.509 -2020-07-29 07:15:00,105.98,36.684,66.061,29.509 -2020-07-29 07:30:00,105.83,34.736999999999995,66.061,29.509 -2020-07-29 07:45:00,104.0,35.484,66.061,29.509 -2020-07-29 08:00:00,106.11,30.903000000000002,58.532,29.509 -2020-07-29 08:15:00,104.13,34.188,58.532,29.509 -2020-07-29 08:30:00,106.01,35.577,58.532,29.509 -2020-07-29 08:45:00,106.01,38.155,58.532,29.509 -2020-07-29 09:00:00,106.42,33.86,56.047,29.509 -2020-07-29 09:15:00,105.79,33.329,56.047,29.509 -2020-07-29 09:30:00,105.45,36.997,56.047,29.509 -2020-07-29 09:45:00,103.21,40.218,56.047,29.509 -2020-07-29 10:00:00,102.4,37.788000000000004,53.823,29.509 -2020-07-29 10:15:00,106.01,39.188,53.823,29.509 -2020-07-29 10:30:00,104.96,39.025,53.823,29.509 -2020-07-29 10:45:00,104.1,39.994,53.823,29.509 -2020-07-29 11:00:00,108.14,37.696,54.184,29.509 -2020-07-29 11:15:00,108.27,38.968,54.184,29.509 -2020-07-29 11:30:00,108.1,40.082,54.184,29.509 -2020-07-29 11:45:00,102.46,41.075,54.184,29.509 -2020-07-29 12:00:00,102.99,35.788000000000004,52.628,29.509 -2020-07-29 12:15:00,100.87,35.034,52.628,29.509 -2020-07-29 12:30:00,100.15,33.786,52.628,29.509 -2020-07-29 12:45:00,98.46,34.696,52.628,29.509 -2020-07-29 13:00:00,98.77,34.41,52.31,29.509 -2020-07-29 13:15:00,97.85,35.708,52.31,29.509 -2020-07-29 13:30:00,97.02,33.861,52.31,29.509 -2020-07-29 13:45:00,97.78,33.959,52.31,29.509 -2020-07-29 14:00:00,96.14,35.687,52.278999999999996,29.509 -2020-07-29 14:15:00,95.9,34.727,52.278999999999996,29.509 -2020-07-29 14:30:00,96.19,33.756,52.278999999999996,29.509 -2020-07-29 14:45:00,96.56,34.452,52.278999999999996,29.509 -2020-07-29 15:00:00,95.81,36.641999999999996,53.306999999999995,29.509 -2020-07-29 15:15:00,92.1,34.259,53.306999999999995,29.509 -2020-07-29 15:30:00,94.45,32.853,53.306999999999995,29.509 -2020-07-29 15:45:00,96.56,31.185,53.306999999999995,29.509 -2020-07-29 16:00:00,97.51,33.548,55.358999999999995,29.509 -2020-07-29 16:15:00,98.55,33.417,55.358999999999995,29.509 -2020-07-29 16:30:00,98.44,32.673,55.358999999999995,29.509 -2020-07-29 16:45:00,98.51,29.837,55.358999999999995,29.509 -2020-07-29 17:00:00,100.98,33.671,59.211999999999996,29.509 -2020-07-29 17:15:00,101.93,34.071,59.211999999999996,29.509 -2020-07-29 17:30:00,102.63,32.995,59.211999999999996,29.509 -2020-07-29 17:45:00,103.93,32.245,59.211999999999996,29.509 -2020-07-29 18:00:00,104.7,35.549,60.403999999999996,29.509 -2020-07-29 18:15:00,103.25,35.335,60.403999999999996,29.509 -2020-07-29 18:30:00,103.93,33.47,60.403999999999996,29.509 -2020-07-29 18:45:00,103.57,36.229,60.403999999999996,29.509 -2020-07-29 19:00:00,99.94,37.659,60.993,29.509 -2020-07-29 19:15:00,95.58,36.588,60.993,29.509 -2020-07-29 19:30:00,95.02,35.373000000000005,60.993,29.509 -2020-07-29 19:45:00,95.21,34.303000000000004,60.993,29.509 -2020-07-29 20:00:00,93.06,31.76,66.6,29.509 -2020-07-29 20:15:00,95.46,30.971999999999998,66.6,29.509 -2020-07-29 20:30:00,95.47,31.337,66.6,29.509 -2020-07-29 20:45:00,94.87,31.686999999999998,66.6,29.509 -2020-07-29 21:00:00,91.12,30.487,59.855,29.509 -2020-07-29 21:15:00,90.81,31.628,59.855,29.509 -2020-07-29 21:30:00,87.11,32.074,59.855,29.509 -2020-07-29 21:45:00,85.59,32.789,59.855,29.509 -2020-07-29 22:00:00,80.94,30.025,54.942,29.509 -2020-07-29 22:15:00,79.82,32.803000000000004,54.942,29.509 -2020-07-29 22:30:00,78.83,28.29,54.942,29.509 -2020-07-29 22:45:00,78.28,25.111,54.942,29.509 -2020-07-29 23:00:00,73.71,22.119,46.056000000000004,29.509 -2020-07-29 23:15:00,75.13,20.851999999999997,46.056000000000004,29.509 -2020-07-29 23:30:00,74.23,19.952,46.056000000000004,29.509 -2020-07-29 23:45:00,72.75,18.901,46.056000000000004,29.509 -2020-07-30 00:00:00,66.91,18.569000000000003,40.859,29.509 -2020-07-30 00:15:00,70.85,19.454,40.859,29.509 -2020-07-30 00:30:00,68.48,18.186,40.859,29.509 -2020-07-30 00:45:00,71.01,18.364,40.859,29.509 -2020-07-30 01:00:00,68.53,18.339000000000002,39.06,29.509 -2020-07-30 01:15:00,69.68,17.503,39.06,29.509 -2020-07-30 01:30:00,69.53,16.087,39.06,29.509 -2020-07-30 01:45:00,69.27,16.129,39.06,29.509 -2020-07-30 02:00:00,69.25,16.028,37.592,29.509 -2020-07-30 02:15:00,69.73,14.753,37.592,29.509 -2020-07-30 02:30:00,69.73,16.677,37.592,29.509 -2020-07-30 02:45:00,70.05,17.419,37.592,29.509 -2020-07-30 03:00:00,70.14,18.289,37.416,29.509 -2020-07-30 03:15:00,67.95,17.977999999999998,37.416,29.509 -2020-07-30 03:30:00,71.64,17.379,37.416,29.509 -2020-07-30 03:45:00,74.13,16.704,37.416,29.509 -2020-07-30 04:00:00,78.12,20.691,38.176,29.509 -2020-07-30 04:15:00,77.94,26.114,38.176,29.509 -2020-07-30 04:30:00,82.51,23.393,38.176,29.509 -2020-07-30 04:45:00,88.96,23.324,38.176,29.509 -2020-07-30 05:00:00,95.56,34.935,41.203,29.509 -2020-07-30 05:15:00,96.07,40.637,41.203,29.509 -2020-07-30 05:30:00,94.6,36.111999999999995,41.203,29.509 -2020-07-30 05:45:00,97.84,33.755,41.203,29.509 -2020-07-30 06:00:00,103.74,33.912,51.09,29.509 -2020-07-30 06:15:00,106.27,33.382,51.09,29.509 -2020-07-30 06:30:00,107.84,33.246,51.09,29.509 -2020-07-30 06:45:00,108.65,36.096,51.09,29.509 -2020-07-30 07:00:00,115.44,35.821,63.541000000000004,29.509 -2020-07-30 07:15:00,115.82,36.841,63.541000000000004,29.509 -2020-07-30 07:30:00,119.1,34.909,63.541000000000004,29.509 -2020-07-30 07:45:00,116.84,35.671,63.541000000000004,29.509 -2020-07-30 08:00:00,114.55,31.096,55.65,29.509 -2020-07-30 08:15:00,110.16,34.368,55.65,29.509 -2020-07-30 08:30:00,112.03,35.745,55.65,29.509 -2020-07-30 08:45:00,108.34,38.311,55.65,29.509 -2020-07-30 09:00:00,110.22,34.018,51.833999999999996,29.509 -2020-07-30 09:15:00,117.14,33.48,51.833999999999996,29.509 -2020-07-30 09:30:00,114.83,37.135,51.833999999999996,29.509 -2020-07-30 09:45:00,112.31,40.343,51.833999999999996,29.509 -2020-07-30 10:00:00,105.58,37.918,49.70399999999999,29.509 -2020-07-30 10:15:00,106.54,39.303000000000004,49.70399999999999,29.509 -2020-07-30 10:30:00,105.24,39.134,49.70399999999999,29.509 -2020-07-30 10:45:00,108.78,40.099000000000004,49.70399999999999,29.509 -2020-07-30 11:00:00,108.28,37.806,48.593999999999994,29.509 -2020-07-30 11:15:00,108.61,39.073,48.593999999999994,29.509 -2020-07-30 11:30:00,108.06,40.179,48.593999999999994,29.509 -2020-07-30 11:45:00,105.5,41.161,48.593999999999994,29.509 -2020-07-30 12:00:00,101.8,35.878,46.275,29.509 -2020-07-30 12:15:00,100.72,35.118,46.275,29.509 -2020-07-30 12:30:00,98.24,33.873000000000005,46.275,29.509 -2020-07-30 12:45:00,98.08,34.773,46.275,29.509 -2020-07-30 13:00:00,96.54,34.467,45.803000000000004,29.509 -2020-07-30 13:15:00,96.92,35.754,45.803000000000004,29.509 -2020-07-30 13:30:00,96.23,33.906,45.803000000000004,29.509 -2020-07-30 13:45:00,97.99,34.016,45.803000000000004,29.509 -2020-07-30 14:00:00,96.53,35.733000000000004,46.251999999999995,29.509 -2020-07-30 14:15:00,96.4,34.777,46.251999999999995,29.509 -2020-07-30 14:30:00,96.01,33.809,46.251999999999995,29.509 -2020-07-30 14:45:00,97.0,34.510999999999996,46.251999999999995,29.509 -2020-07-30 15:00:00,95.45,36.68,48.309,29.509 -2020-07-30 15:15:00,95.82,34.297,48.309,29.509 -2020-07-30 15:30:00,95.51,32.898,48.309,29.509 -2020-07-30 15:45:00,96.03,31.229,48.309,29.509 -2020-07-30 16:00:00,97.23,33.583,49.681999999999995,29.509 -2020-07-30 16:15:00,98.16,33.459,49.681999999999995,29.509 -2020-07-30 16:30:00,98.84,32.728,49.681999999999995,29.509 -2020-07-30 16:45:00,101.61,29.916999999999998,49.681999999999995,29.509 -2020-07-30 17:00:00,101.98,33.736,53.086000000000006,29.509 -2020-07-30 17:15:00,101.92,34.16,53.086000000000006,29.509 -2020-07-30 17:30:00,102.4,33.095,53.086000000000006,29.509 -2020-07-30 17:45:00,104.67,32.369,53.086000000000006,29.509 -2020-07-30 18:00:00,104.01,35.671,54.038999999999994,29.509 -2020-07-30 18:15:00,102.55,35.457,54.038999999999994,29.509 -2020-07-30 18:30:00,101.69,33.601,54.038999999999994,29.509 -2020-07-30 18:45:00,101.62,36.359,54.038999999999994,29.509 -2020-07-30 19:00:00,97.79,37.793,53.408,29.509 -2020-07-30 19:15:00,95.25,36.719,53.408,29.509 -2020-07-30 19:30:00,94.88,35.501999999999995,53.408,29.509 -2020-07-30 19:45:00,94.26,34.434,53.408,29.509 -2020-07-30 20:00:00,94.38,31.886,55.309,29.509 -2020-07-30 20:15:00,95.53,31.099,55.309,29.509 -2020-07-30 20:30:00,95.23,31.453000000000003,55.309,29.509 -2020-07-30 20:45:00,94.49,31.789,55.309,29.509 -2020-07-30 21:00:00,90.29,30.593000000000004,51.585,29.509 -2020-07-30 21:15:00,90.19,31.728,51.585,29.509 -2020-07-30 21:30:00,85.83,32.162,51.585,29.509 -2020-07-30 21:45:00,84.2,32.855,51.585,29.509 -2020-07-30 22:00:00,79.37,30.081999999999997,48.006,29.509 -2020-07-30 22:15:00,79.95,32.852,48.006,29.509 -2020-07-30 22:30:00,77.7,28.309,48.006,29.509 -2020-07-30 22:45:00,78.53,25.127,48.006,29.509 -2020-07-30 23:00:00,73.03,22.162,42.309,29.509 -2020-07-30 23:15:00,73.29,20.901999999999997,42.309,29.509 -2020-07-30 23:30:00,72.79,20.016,42.309,29.509 -2020-07-30 23:45:00,72.36,18.965999999999998,42.309,29.509 -2020-07-31 00:00:00,67.66,16.794,39.649,29.509 -2020-07-31 00:15:00,67.46,17.897000000000002,39.649,29.509 -2020-07-31 00:30:00,69.3,16.939,39.649,29.509 -2020-07-31 00:45:00,69.18,17.575,39.649,29.509 -2020-07-31 01:00:00,68.54,17.151,37.744,29.509 -2020-07-31 01:15:00,69.64,15.592,37.744,29.509 -2020-07-31 01:30:00,68.26,14.98,37.744,29.509 -2020-07-31 01:45:00,68.52,14.735999999999999,37.744,29.509 -2020-07-31 02:00:00,66.7,15.647,36.965,29.509 -2020-07-31 02:15:00,69.12,14.513,36.965,29.509 -2020-07-31 02:30:00,68.98,17.101,36.965,29.509 -2020-07-31 02:45:00,68.73,17.097,36.965,29.509 -2020-07-31 03:00:00,69.73,18.875999999999998,37.678000000000004,29.509 -2020-07-31 03:15:00,70.06,17.2,37.678000000000004,29.509 -2020-07-31 03:30:00,71.02,16.35,37.678000000000004,29.509 -2020-07-31 03:45:00,73.59,16.607,37.678000000000004,29.509 -2020-07-31 04:00:00,77.51,20.721,38.591,29.509 -2020-07-31 04:15:00,84.16,24.464000000000002,38.591,29.509 -2020-07-31 04:30:00,85.18,22.758000000000003,38.591,29.509 -2020-07-31 04:45:00,83.87,22.075,38.591,29.509 -2020-07-31 05:00:00,88.03,33.272,40.666,29.509 -2020-07-31 05:15:00,93.14,39.897,40.666,29.509 -2020-07-31 05:30:00,96.11,35.541,40.666,29.509 -2020-07-31 05:45:00,103.78,32.701,40.666,29.509 -2020-07-31 06:00:00,109.56,33.047,51.784,29.509 -2020-07-31 06:15:00,108.31,32.806,51.784,29.509 -2020-07-31 06:30:00,104.13,32.679,51.784,29.509 -2020-07-31 06:45:00,103.29,35.299,51.784,29.509 -2020-07-31 07:00:00,105.64,35.775,61.383,29.509 -2020-07-31 07:15:00,107.2,37.719,61.383,29.509 -2020-07-31 07:30:00,106.76,33.693000000000005,61.383,29.509 -2020-07-31 07:45:00,108.81,34.313,61.383,29.509 -2020-07-31 08:00:00,110.94,30.691,55.272,29.509 -2020-07-31 08:15:00,111.98,34.72,55.272,29.509 -2020-07-31 08:30:00,111.09,35.89,55.272,29.509 -2020-07-31 08:45:00,105.5,38.428000000000004,55.272,29.509 -2020-07-31 09:00:00,112.21,31.557,53.506,29.509 -2020-07-31 09:15:00,112.71,33.08,53.506,29.509 -2020-07-31 09:30:00,117.26,36.003,53.506,29.509 -2020-07-31 09:45:00,117.46,39.635,53.506,29.509 -2020-07-31 10:00:00,118.86,37.153,51.363,29.509 -2020-07-31 10:15:00,109.46,38.202,51.363,29.509 -2020-07-31 10:30:00,106.07,38.647,51.363,29.509 -2020-07-31 10:45:00,104.7,39.549,51.363,29.509 -2020-07-31 11:00:00,102.28,37.538000000000004,51.043,29.509 -2020-07-31 11:15:00,99.07,37.687,51.043,29.509 -2020-07-31 11:30:00,98.78,38.205999999999996,51.043,29.509 -2020-07-31 11:45:00,102.16,38.109,51.043,29.509 -2020-07-31 12:00:00,98.03,33.196,47.52,29.509 -2020-07-31 12:15:00,98.7,31.933000000000003,47.52,29.509 -2020-07-31 12:30:00,97.7,30.805999999999997,47.52,29.509 -2020-07-31 12:45:00,101.31,30.771,47.52,29.509 -2020-07-31 13:00:00,97.21,31.002,45.494,29.509 -2020-07-31 13:15:00,94.94,32.394,45.494,29.509 -2020-07-31 13:30:00,95.09,31.428,45.494,29.509 -2020-07-31 13:45:00,99.62,31.894000000000002,45.494,29.509 -2020-07-31 14:00:00,99.4,32.83,43.883,29.509 -2020-07-31 14:15:00,102.69,32.379,43.883,29.509 -2020-07-31 14:30:00,104.82,33.031,43.883,29.509 -2020-07-31 14:45:00,108.76,32.946,43.883,29.509 -2020-07-31 15:00:00,107.19,35.053000000000004,45.714,29.509 -2020-07-31 15:15:00,107.16,32.45,45.714,29.509 -2020-07-31 15:30:00,102.02,30.58,45.714,29.509 -2020-07-31 15:45:00,98.73,29.737,45.714,29.509 -2020-07-31 16:00:00,97.24,31.250999999999998,48.222,29.509 -2020-07-31 16:15:00,96.65,31.649,48.222,29.509 -2020-07-31 16:30:00,95.0,30.721999999999998,48.222,29.509 -2020-07-31 16:45:00,98.3,27.085,48.222,29.509 -2020-07-31 17:00:00,100.32,32.76,52.619,29.509 -2020-07-31 17:15:00,100.27,33.058,52.619,29.509 -2020-07-31 17:30:00,99.43,32.214,52.619,29.509 -2020-07-31 17:45:00,99.98,31.351999999999997,52.619,29.509 -2020-07-31 18:00:00,100.2,34.617,52.99,29.509 -2020-07-31 18:15:00,101.05,33.407,52.99,29.509 -2020-07-31 18:30:00,100.24,31.410999999999998,52.99,29.509 -2020-07-31 18:45:00,97.28,34.628,52.99,29.509 -2020-07-31 19:00:00,94.41,36.923,51.923,29.509 -2020-07-31 19:15:00,93.63,36.38,51.923,29.509 -2020-07-31 19:30:00,90.61,35.259,51.923,29.509 -2020-07-31 19:45:00,91.18,33.129,51.923,29.509 -2020-07-31 20:00:00,91.95,30.405,56.238,29.509 -2020-07-31 20:15:00,91.38,30.476999999999997,56.238,29.509 -2020-07-31 20:30:00,90.78,30.296999999999997,56.238,29.509 -2020-07-31 20:45:00,90.26,29.695,56.238,29.509 -2020-07-31 21:00:00,85.04,29.877,52.426,29.509 -2020-07-31 21:15:00,83.4,32.773,52.426,29.509 -2020-07-31 21:30:00,80.13,33.031,52.426,29.509 -2020-07-31 21:45:00,79.5,33.88,52.426,29.509 -2020-07-31 22:00:00,75.94,30.871,48.196000000000005,29.509 -2020-07-31 22:15:00,75.61,33.374,48.196000000000005,29.509 -2020-07-31 22:30:00,74.04,33.457,48.196000000000005,29.509 -2020-07-31 22:45:00,74.51,30.945,48.196000000000005,29.509 -2020-07-31 23:00:00,69.29,29.855999999999998,41.71,29.509 -2020-07-31 23:15:00,67.98,27.054000000000002,41.71,29.509 -2020-07-31 23:30:00,67.78,24.331999999999997,41.71,29.509 -2020-07-31 23:45:00,68.07,23.197,41.71,29.509 -2020-08-01 00:00:00,64.84,16.840999999999998,40.227,29.423000000000002 -2020-08-01 00:15:00,65.7,17.429000000000002,40.227,29.423000000000002 -2020-08-01 00:30:00,61.29,16.066,40.227,29.423000000000002 -2020-08-01 00:45:00,63.86,15.87,40.227,29.423000000000002 -2020-08-01 01:00:00,61.89,15.71,36.303000000000004,29.423000000000002 -2020-08-01 01:15:00,63.24,14.94,36.303000000000004,29.423000000000002 -2020-08-01 01:30:00,61.9,13.640999999999998,36.303000000000004,29.423000000000002 -2020-08-01 01:45:00,62.53,14.588,36.303000000000004,29.423000000000002 -2020-08-01 02:00:00,60.34,14.395999999999999,33.849000000000004,29.423000000000002 -2020-08-01 02:15:00,60.96,13.040999999999999,33.849000000000004,29.423000000000002 -2020-08-01 02:30:00,66.99,14.253,33.849000000000004,29.423000000000002 -2020-08-01 02:45:00,68.32,15.014000000000001,33.849000000000004,29.423000000000002 -2020-08-01 03:00:00,65.65,15.258,33.149,29.423000000000002 -2020-08-01 03:15:00,61.57,13.152999999999999,33.149,29.423000000000002 -2020-08-01 03:30:00,61.95,13.040999999999999,33.149,29.423000000000002 -2020-08-01 03:45:00,64.13,14.512,33.149,29.423000000000002 -2020-08-01 04:00:00,63.58,16.715,32.501,29.423000000000002 -2020-08-01 04:15:00,62.36,19.454,32.501,29.423000000000002 -2020-08-01 04:30:00,64.17,16.437,32.501,29.423000000000002 -2020-08-01 04:45:00,69.28,16.136,32.501,29.423000000000002 -2020-08-01 05:00:00,70.79,19.305,31.648000000000003,29.423000000000002 -2020-08-01 05:15:00,66.78,16.058,31.648000000000003,29.423000000000002 -2020-08-01 05:30:00,64.36,13.495999999999999,31.648000000000003,29.423000000000002 -2020-08-01 05:45:00,65.34,14.887,31.648000000000003,29.423000000000002 -2020-08-01 06:00:00,68.08,24.894000000000002,32.552,29.423000000000002 -2020-08-01 06:15:00,71.47,31.201,32.552,29.423000000000002 -2020-08-01 06:30:00,70.7,28.622,32.552,29.423000000000002 -2020-08-01 06:45:00,73.6,28.4,32.552,29.423000000000002 -2020-08-01 07:00:00,80.26,28.034000000000002,35.181999999999995,29.423000000000002 -2020-08-01 07:15:00,81.15,28.61,35.181999999999995,29.423000000000002 -2020-08-01 07:30:00,83.99,26.254,35.181999999999995,29.423000000000002 -2020-08-01 07:45:00,78.02,27.406,35.181999999999995,29.423000000000002 -2020-08-01 08:00:00,78.36,24.79,40.35,29.423000000000002 -2020-08-01 08:15:00,78.58,27.854,40.35,29.423000000000002 -2020-08-01 08:30:00,84.13,28.653000000000002,40.35,29.423000000000002 -2020-08-01 08:45:00,88.25,31.538,40.35,29.423000000000002 -2020-08-01 09:00:00,83.81,27.921,42.292,29.423000000000002 -2020-08-01 09:15:00,77.51,29.616,42.292,29.423000000000002 -2020-08-01 09:30:00,77.4,32.631,42.292,29.423000000000002 -2020-08-01 09:45:00,81.46,35.472,42.292,29.423000000000002 -2020-08-01 10:00:00,88.03,33.78,40.084,29.423000000000002 -2020-08-01 10:15:00,87.71,35.077,40.084,29.423000000000002 -2020-08-01 10:30:00,88.54,35.137,40.084,29.423000000000002 -2020-08-01 10:45:00,81.12,35.558,40.084,29.423000000000002 -2020-08-01 11:00:00,75.49,33.609,36.966,29.423000000000002 -2020-08-01 11:15:00,77.04,34.655,36.966,29.423000000000002 -2020-08-01 11:30:00,73.65,35.507,36.966,29.423000000000002 -2020-08-01 11:45:00,73.41,36.196999999999996,36.966,29.423000000000002 -2020-08-01 12:00:00,72.34,32.466,35.19,29.423000000000002 -2020-08-01 12:15:00,72.7,32.124,35.19,29.423000000000002 -2020-08-01 12:30:00,68.3,30.954,35.19,29.423000000000002 -2020-08-01 12:45:00,72.12,31.738000000000003,35.19,29.423000000000002 -2020-08-01 13:00:00,71.85,31.165,32.277,29.423000000000002 -2020-08-01 13:15:00,73.17,32.038000000000004,32.277,29.423000000000002 -2020-08-01 13:30:00,75.14,31.421,32.277,29.423000000000002 -2020-08-01 13:45:00,78.23,30.410999999999998,32.277,29.423000000000002 -2020-08-01 14:00:00,75.78,31.076999999999998,31.436999999999998,29.423000000000002 -2020-08-01 14:15:00,76.7,29.488000000000003,31.436999999999998,29.423000000000002 -2020-08-01 14:30:00,76.4,29.755,31.436999999999998,29.423000000000002 -2020-08-01 14:45:00,71.54,30.14,31.436999999999998,29.423000000000002 -2020-08-01 15:00:00,72.15,32.229,33.493,29.423000000000002 -2020-08-01 15:15:00,79.53,30.479,33.493,29.423000000000002 -2020-08-01 15:30:00,82.37,28.81,33.493,29.423000000000002 -2020-08-01 15:45:00,82.54,27.113000000000003,33.493,29.423000000000002 -2020-08-01 16:00:00,80.64,30.905,36.593,29.423000000000002 -2020-08-01 16:15:00,78.96,30.311,36.593,29.423000000000002 -2020-08-01 16:30:00,81.42,29.761,36.593,29.423000000000002 -2020-08-01 16:45:00,77.27,26.39,36.593,29.423000000000002 -2020-08-01 17:00:00,78.24,30.271,42.049,29.423000000000002 -2020-08-01 17:15:00,80.69,28.34,42.049,29.423000000000002 -2020-08-01 17:30:00,83.53,27.478,42.049,29.423000000000002 -2020-08-01 17:45:00,81.6,27.182,42.049,29.423000000000002 -2020-08-01 18:00:00,83.2,31.39,43.755,29.423000000000002 -2020-08-01 18:15:00,82.56,31.776,43.755,29.423000000000002 -2020-08-01 18:30:00,82.19,31.188000000000002,43.755,29.423000000000002 -2020-08-01 18:45:00,81.88,31.037,43.755,29.423000000000002 -2020-08-01 19:00:00,79.58,31.397,44.492,29.423000000000002 -2020-08-01 19:15:00,76.37,29.973000000000003,44.492,29.423000000000002 -2020-08-01 19:30:00,75.74,29.653000000000002,44.492,29.423000000000002 -2020-08-01 19:45:00,76.03,29.514,44.492,29.423000000000002 -2020-08-01 20:00:00,78.67,27.618000000000002,40.896,29.423000000000002 -2020-08-01 20:15:00,78.26,27.023000000000003,40.896,29.423000000000002 -2020-08-01 20:30:00,77.78,26.051,40.896,29.423000000000002 -2020-08-01 20:45:00,79.77,27.362,40.896,29.423000000000002 -2020-08-01 21:00:00,74.49,25.883000000000003,39.056,29.423000000000002 -2020-08-01 21:15:00,73.64,28.221,39.056,29.423000000000002 -2020-08-01 21:30:00,71.31,28.568,39.056,29.423000000000002 -2020-08-01 21:45:00,70.86,28.83,39.056,29.423000000000002 -2020-08-01 22:00:00,68.57,26.011,38.478,29.423000000000002 -2020-08-01 22:15:00,67.73,28.296999999999997,38.478,29.423000000000002 -2020-08-01 22:30:00,65.12,27.611,38.478,29.423000000000002 -2020-08-01 22:45:00,65.56,25.581999999999997,38.478,29.423000000000002 -2020-08-01 23:00:00,61.72,23.868000000000002,32.953,29.423000000000002 -2020-08-01 23:15:00,60.35,21.84,32.953,29.423000000000002 -2020-08-01 23:30:00,58.32,21.82,32.953,29.423000000000002 -2020-08-01 23:45:00,57.81,21.265,32.953,29.423000000000002 -2020-08-02 00:00:00,56.36,18.218,28.584,29.423000000000002 -2020-08-02 00:15:00,57.66,17.723,28.584,29.423000000000002 -2020-08-02 00:30:00,57.1,16.248,28.584,29.423000000000002 -2020-08-02 00:45:00,57.12,15.909,28.584,29.423000000000002 -2020-08-02 01:00:00,55.38,16.003,26.419,29.423000000000002 -2020-08-02 01:15:00,55.55,15.054,26.419,29.423000000000002 -2020-08-02 01:30:00,55.72,13.6,26.419,29.423000000000002 -2020-08-02 01:45:00,55.6,14.199000000000002,26.419,29.423000000000002 -2020-08-02 02:00:00,55.11,14.134,25.335,29.423000000000002 -2020-08-02 02:15:00,55.47,13.040999999999999,25.335,29.423000000000002 -2020-08-02 02:30:00,54.77,14.844000000000001,25.335,29.423000000000002 -2020-08-02 02:45:00,55.36,15.297,25.335,29.423000000000002 -2020-08-02 03:00:00,54.8,16.134,24.805,29.423000000000002 -2020-08-02 03:15:00,55.54,14.325,24.805,29.423000000000002 -2020-08-02 03:30:00,55.72,13.217,24.805,29.423000000000002 -2020-08-02 03:45:00,55.21,14.154000000000002,24.805,29.423000000000002 -2020-08-02 04:00:00,58.15,16.34,25.772,29.423000000000002 -2020-08-02 04:15:00,56.39,18.723,25.772,29.423000000000002 -2020-08-02 04:30:00,54.95,16.98,25.772,29.423000000000002 -2020-08-02 04:45:00,55.36,16.218,25.772,29.423000000000002 -2020-08-02 05:00:00,55.17,19.77,25.971999999999998,29.423000000000002 -2020-08-02 05:15:00,54.51,15.937000000000001,25.971999999999998,29.423000000000002 -2020-08-02 05:30:00,54.25,13.040999999999999,25.971999999999998,29.423000000000002 -2020-08-02 05:45:00,55.07,14.138,25.971999999999998,29.423000000000002 -2020-08-02 06:00:00,55.41,21.9,26.026,29.423000000000002 -2020-08-02 06:15:00,55.73,29.107,26.026,29.423000000000002 -2020-08-02 06:30:00,57.85,25.941,26.026,29.423000000000002 -2020-08-02 06:45:00,59.03,24.854,26.026,29.423000000000002 -2020-08-02 07:00:00,59.2,24.644000000000002,27.396,29.423000000000002 -2020-08-02 07:15:00,60.74,23.725,27.396,29.423000000000002 -2020-08-02 07:30:00,63.57,22.7,27.396,29.423000000000002 -2020-08-02 07:45:00,64.87,23.888,27.396,29.423000000000002 -2020-08-02 08:00:00,63.57,21.895,30.791999999999998,29.423000000000002 -2020-08-02 08:15:00,66.35,26.094,30.791999999999998,29.423000000000002 -2020-08-02 08:30:00,66.99,27.593000000000004,30.791999999999998,29.423000000000002 -2020-08-02 08:45:00,68.25,30.224,30.791999999999998,29.423000000000002 -2020-08-02 09:00:00,68.6,26.549,32.482,29.423000000000002 -2020-08-02 09:15:00,67.03,27.708000000000002,32.482,29.423000000000002 -2020-08-02 09:30:00,66.69,31.144000000000002,32.482,29.423000000000002 -2020-08-02 09:45:00,67.87,34.96,32.482,29.423000000000002 -2020-08-02 10:00:00,69.2,33.574,31.951,29.423000000000002 -2020-08-02 10:15:00,67.97,34.938,31.951,29.423000000000002 -2020-08-02 10:30:00,69.5,35.152,31.951,29.423000000000002 -2020-08-02 10:45:00,73.44,36.748000000000005,31.951,29.423000000000002 -2020-08-02 11:00:00,71.52,34.372,33.619,29.423000000000002 -2020-08-02 11:15:00,64.82,34.964,33.619,29.423000000000002 -2020-08-02 11:30:00,60.86,36.402,33.619,29.423000000000002 -2020-08-02 11:45:00,66.18,37.275,33.619,29.423000000000002 -2020-08-02 12:00:00,63.81,34.646,30.975,29.423000000000002 -2020-08-02 12:15:00,60.26,33.494,30.975,29.423000000000002 -2020-08-02 12:30:00,56.84,32.681,30.975,29.423000000000002 -2020-08-02 12:45:00,55.4,32.926,30.975,29.423000000000002 -2020-08-02 13:00:00,53.87,32.108000000000004,27.956999999999997,29.423000000000002 -2020-08-02 13:15:00,58.19,32.109,27.956999999999997,29.423000000000002 -2020-08-02 13:30:00,55.86,30.419,27.956999999999997,29.423000000000002 -2020-08-02 13:45:00,57.45,30.579,27.956999999999997,29.423000000000002 -2020-08-02 14:00:00,56.54,32.385999999999996,25.555999999999997,29.423000000000002 -2020-08-02 14:15:00,55.69,31.107,25.555999999999997,29.423000000000002 -2020-08-02 14:30:00,56.82,30.05,25.555999999999997,29.423000000000002 -2020-08-02 14:45:00,56.34,29.430999999999997,25.555999999999997,29.423000000000002 -2020-08-02 15:00:00,56.78,31.79,26.271,29.423000000000002 -2020-08-02 15:15:00,54.95,29.179000000000002,26.271,29.423000000000002 -2020-08-02 15:30:00,54.84,27.285,26.271,29.423000000000002 -2020-08-02 15:45:00,56.41,25.801,26.271,29.423000000000002 -2020-08-02 16:00:00,59.72,27.843000000000004,30.369,29.423000000000002 -2020-08-02 16:15:00,59.6,27.56,30.369,29.423000000000002 -2020-08-02 16:30:00,61.23,27.973000000000003,30.369,29.423000000000002 -2020-08-02 16:45:00,63.53,24.678,30.369,29.423000000000002 -2020-08-02 17:00:00,67.12,28.910999999999998,38.787,29.423000000000002 -2020-08-02 17:15:00,68.98,28.498,38.787,29.423000000000002 -2020-08-02 17:30:00,70.71,28.406,38.787,29.423000000000002 -2020-08-02 17:45:00,74.99,28.346999999999998,38.787,29.423000000000002 -2020-08-02 18:00:00,74.67,33.192,41.886,29.423000000000002 -2020-08-02 18:15:00,73.6,33.085,41.886,29.423000000000002 -2020-08-02 18:30:00,76.34,32.428000000000004,41.886,29.423000000000002 -2020-08-02 18:45:00,74.12,32.243,41.886,29.423000000000002 -2020-08-02 19:00:00,75.09,34.739000000000004,42.91,29.423000000000002 -2020-08-02 19:15:00,72.97,32.211999999999996,42.91,29.423000000000002 -2020-08-02 19:30:00,73.09,31.666,42.91,29.423000000000002 -2020-08-02 19:45:00,73.57,30.991,42.91,29.423000000000002 -2020-08-02 20:00:00,78.39,29.259,42.148999999999994,29.423000000000002 -2020-08-02 20:15:00,77.61,28.465,42.148999999999994,29.423000000000002 -2020-08-02 20:30:00,77.86,28.169,42.148999999999994,29.423000000000002 -2020-08-02 20:45:00,80.93,27.945999999999998,42.148999999999994,29.423000000000002 -2020-08-02 21:00:00,76.12,26.549,40.955999999999996,29.423000000000002 -2020-08-02 21:15:00,75.92,28.636999999999997,40.955999999999996,29.423000000000002 -2020-08-02 21:30:00,72.29,28.338,40.955999999999996,29.423000000000002 -2020-08-02 21:45:00,71.28,28.91,40.955999999999996,29.423000000000002 -2020-08-02 22:00:00,67.02,28.098000000000003,39.873000000000005,29.423000000000002 -2020-08-02 22:15:00,68.04,28.831,39.873000000000005,29.423000000000002 -2020-08-02 22:30:00,65.78,27.840999999999998,39.873000000000005,29.423000000000002 -2020-08-02 22:45:00,65.72,24.656999999999996,39.873000000000005,29.423000000000002 -2020-08-02 23:00:00,60.72,22.861,35.510999999999996,29.423000000000002 -2020-08-02 23:15:00,63.05,21.92,35.510999999999996,29.423000000000002 -2020-08-02 23:30:00,62.6,21.354,35.510999999999996,29.423000000000002 -2020-08-02 23:45:00,62.86,20.894000000000002,35.510999999999996,29.423000000000002 -2020-08-03 00:00:00,64.33,19.451,33.475,29.535 -2020-08-03 00:15:00,67.22,19.498,33.475,29.535 -2020-08-03 00:30:00,66.41,17.651,33.475,29.535 -2020-08-03 00:45:00,63.17,16.98,33.475,29.535 -2020-08-03 01:00:00,63.68,17.434,33.111,29.535 -2020-08-03 01:15:00,60.61,16.54,33.111,29.535 -2020-08-03 01:30:00,60.33,15.436,33.111,29.535 -2020-08-03 01:45:00,61.13,15.931,33.111,29.535 -2020-08-03 02:00:00,59.92,16.308,32.358000000000004,29.535 -2020-08-03 02:15:00,61.4,14.12,32.358000000000004,29.535 -2020-08-03 02:30:00,61.07,16.077,32.358000000000004,29.535 -2020-08-03 02:45:00,61.46,16.435,32.358000000000004,29.535 -2020-08-03 03:00:00,60.32,17.656,30.779,29.535 -2020-08-03 03:15:00,63.48,16.432000000000002,30.779,29.535 -2020-08-03 03:30:00,63.96,16.000999999999998,30.779,29.535 -2020-08-03 03:45:00,66.5,16.54,30.779,29.535 -2020-08-03 04:00:00,74.97,21.386999999999997,31.416,29.535 -2020-08-03 04:15:00,76.04,26.27,31.416,29.535 -2020-08-03 04:30:00,74.98,23.919,31.416,29.535 -2020-08-03 04:45:00,83.07,23.5,31.416,29.535 -2020-08-03 05:00:00,90.67,32.928000000000004,37.221,29.535 -2020-08-03 05:15:00,89.8,37.603,37.221,29.535 -2020-08-03 05:30:00,91.37,33.244,37.221,29.535 -2020-08-03 05:45:00,93.43,31.784000000000002,37.221,29.535 -2020-08-03 06:00:00,105.8,30.877,51.891000000000005,29.535 -2020-08-03 06:15:00,107.93,30.43,51.891000000000005,29.535 -2020-08-03 06:30:00,104.29,30.522,51.891000000000005,29.535 -2020-08-03 06:45:00,105.53,33.891,51.891000000000005,29.535 -2020-08-03 07:00:00,111.0,33.523,62.282,29.535 -2020-08-03 07:15:00,108.3,34.719,62.282,29.535 -2020-08-03 07:30:00,108.02,32.942,62.282,29.535 -2020-08-03 07:45:00,103.2,34.629,62.282,29.535 -2020-08-03 08:00:00,104.16,30.793000000000003,54.102,29.535 -2020-08-03 08:15:00,105.8,33.993,54.102,29.535 -2020-08-03 08:30:00,107.65,34.896,54.102,29.535 -2020-08-03 08:45:00,105.3,38.001999999999995,54.102,29.535 -2020-08-03 09:00:00,103.61,33.321999999999996,50.917,29.535 -2020-08-03 09:15:00,107.08,33.051,50.917,29.535 -2020-08-03 09:30:00,106.27,35.66,50.917,29.535 -2020-08-03 09:45:00,106.17,37.192,50.917,29.535 -2020-08-03 10:00:00,102.23,36.264,49.718999999999994,29.535 -2020-08-03 10:15:00,106.93,37.5,49.718999999999994,29.535 -2020-08-03 10:30:00,107.96,37.342,49.718999999999994,29.535 -2020-08-03 10:45:00,107.77,37.3,49.718999999999994,29.535 -2020-08-03 11:00:00,100.64,35.437,49.833999999999996,29.535 -2020-08-03 11:15:00,97.07,36.126,49.833999999999996,29.535 -2020-08-03 11:30:00,95.48,38.11,49.833999999999996,29.535 -2020-08-03 11:45:00,95.33,39.504,49.833999999999996,29.535 -2020-08-03 12:00:00,98.79,35.001,47.832,29.535 -2020-08-03 12:15:00,96.36,33.953,47.832,29.535 -2020-08-03 12:30:00,97.53,32.065,47.832,29.535 -2020-08-03 12:45:00,95.9,32.147,47.832,29.535 -2020-08-03 13:00:00,100.77,32.144,48.03,29.535 -2020-08-03 13:15:00,99.22,31.401,48.03,29.535 -2020-08-03 13:30:00,102.37,29.932,48.03,29.535 -2020-08-03 13:45:00,97.29,30.98,48.03,29.535 -2020-08-03 14:00:00,94.86,31.984,48.157,29.535 -2020-08-03 14:15:00,93.05,31.354,48.157,29.535 -2020-08-03 14:30:00,92.91,30.191999999999997,48.157,29.535 -2020-08-03 14:45:00,92.65,31.615,48.157,29.535 -2020-08-03 15:00:00,92.04,33.464,48.897,29.535 -2020-08-03 15:15:00,91.52,30.410999999999998,48.897,29.535 -2020-08-03 15:30:00,90.89,29.355999999999998,48.897,29.535 -2020-08-03 15:45:00,92.82,27.436999999999998,48.897,29.535 -2020-08-03 16:00:00,94.14,30.561,51.446000000000005,29.535 -2020-08-03 16:15:00,94.64,30.415,51.446000000000005,29.535 -2020-08-03 16:30:00,96.56,30.275,51.446000000000005,29.535 -2020-08-03 16:45:00,98.01,27.131999999999998,51.446000000000005,29.535 -2020-08-03 17:00:00,98.65,30.291,57.507,29.535 -2020-08-03 17:15:00,100.17,30.348000000000003,57.507,29.535 -2020-08-03 17:30:00,101.49,29.936999999999998,57.507,29.535 -2020-08-03 17:45:00,102.04,29.642,57.507,29.535 -2020-08-03 18:00:00,103.4,33.454,57.896,29.535 -2020-08-03 18:15:00,101.83,31.758000000000003,57.896,29.535 -2020-08-03 18:30:00,102.94,30.328000000000003,57.896,29.535 -2020-08-03 18:45:00,102.54,33.139,57.896,29.535 -2020-08-03 19:00:00,99.56,35.525,57.891999999999996,29.535 -2020-08-03 19:15:00,96.15,34.346,57.891999999999996,29.535 -2020-08-03 19:30:00,94.72,33.41,57.891999999999996,29.535 -2020-08-03 19:45:00,96.09,32.209,57.891999999999996,29.535 -2020-08-03 20:00:00,96.55,29.432,64.57300000000001,29.535 -2020-08-03 20:15:00,103.85,30.159000000000002,64.57300000000001,29.535 -2020-08-03 20:30:00,102.7,30.487,64.57300000000001,29.535 -2020-08-03 20:45:00,96.84,30.337,64.57300000000001,29.535 -2020-08-03 21:00:00,90.83,28.291999999999998,59.431999999999995,29.535 -2020-08-03 21:15:00,90.21,30.875999999999998,59.431999999999995,29.535 -2020-08-03 21:30:00,85.03,31.019000000000002,59.431999999999995,29.535 -2020-08-03 21:45:00,84.99,31.394000000000002,59.431999999999995,29.535 -2020-08-03 22:00:00,79.53,28.799,51.519,29.535 -2020-08-03 22:15:00,80.49,31.503,51.519,29.535 -2020-08-03 22:30:00,79.92,26.988000000000003,51.519,29.535 -2020-08-03 22:45:00,78.23,24.1,51.519,29.535 -2020-08-03 23:00:00,73.74,22.268,44.501000000000005,29.535 -2020-08-03 23:15:00,72.71,19.594,44.501000000000005,29.535 -2020-08-03 23:30:00,74.86,18.83,44.501000000000005,29.535 -2020-08-03 23:45:00,80.0,17.692,44.501000000000005,29.535 -2020-08-04 00:00:00,77.12,17.405,44.522,29.535 -2020-08-04 00:15:00,74.58,18.18,44.522,29.535 -2020-08-04 00:30:00,69.85,17.092,44.522,29.535 -2020-08-04 00:45:00,70.87,17.262999999999998,44.522,29.535 -2020-08-04 01:00:00,70.62,17.199,41.441,29.535 -2020-08-04 01:15:00,70.7,16.449,41.441,29.535 -2020-08-04 01:30:00,73.19,15.217,41.441,29.535 -2020-08-04 01:45:00,78.29,15.190999999999999,41.441,29.535 -2020-08-04 02:00:00,77.5,15.107999999999999,40.203,29.535 -2020-08-04 02:15:00,71.54,14.061,40.203,29.535 -2020-08-04 02:30:00,71.46,15.665999999999999,40.203,29.535 -2020-08-04 02:45:00,76.65,16.338,40.203,29.535 -2020-08-04 03:00:00,78.03,17.101,39.536,29.535 -2020-08-04 03:15:00,78.15,16.855,39.536,29.535 -2020-08-04 03:30:00,71.82,16.365,39.536,29.535 -2020-08-04 03:45:00,79.75,15.821,39.536,29.535 -2020-08-04 04:00:00,84.05,19.462,40.759,29.535 -2020-08-04 04:15:00,86.03,24.38,40.759,29.535 -2020-08-04 04:30:00,85.13,21.936,40.759,29.535 -2020-08-04 04:45:00,89.94,21.901999999999997,40.759,29.535 -2020-08-04 05:00:00,95.49,32.521,43.623999999999995,29.535 -2020-08-04 05:15:00,96.01,37.814,43.623999999999995,29.535 -2020-08-04 05:30:00,98.12,33.751,43.623999999999995,29.535 -2020-08-04 05:45:00,99.46,31.581,43.623999999999995,29.535 -2020-08-04 06:00:00,100.86,31.744,52.684,29.535 -2020-08-04 06:15:00,104.1,31.389,52.684,29.535 -2020-08-04 06:30:00,107.85,31.221999999999998,52.684,29.535 -2020-08-04 06:45:00,112.25,33.775999999999996,52.684,29.535 -2020-08-04 07:00:00,112.11,33.569,62.676,29.535 -2020-08-04 07:15:00,113.72,34.546,62.676,29.535 -2020-08-04 07:30:00,109.77,32.847,62.676,29.535 -2020-08-04 07:45:00,107.83,33.577,62.676,29.535 -2020-08-04 08:00:00,112.52,29.664,56.161,29.535 -2020-08-04 08:15:00,112.46,32.497,56.161,29.535 -2020-08-04 08:30:00,112.9,33.57,56.161,29.535 -2020-08-04 08:45:00,111.06,35.781,56.161,29.535 -2020-08-04 09:00:00,115.35,31.52,52.132,29.535 -2020-08-04 09:15:00,114.68,30.95,52.132,29.535 -2020-08-04 09:30:00,112.07,34.161,52.132,29.535 -2020-08-04 09:45:00,109.95,37.027,52.132,29.535 -2020-08-04 10:00:00,113.26,34.8,51.032,29.535 -2020-08-04 10:15:00,111.01,35.992,51.032,29.535 -2020-08-04 10:30:00,108.81,35.836,51.032,29.535 -2020-08-04 10:45:00,112.99,36.762,51.032,29.535 -2020-08-04 11:00:00,106.02,34.756,51.085,29.535 -2020-08-04 11:15:00,107.07,35.876999999999995,51.085,29.535 -2020-08-04 11:30:00,108.91,36.829,51.085,29.535 -2020-08-04 11:45:00,110.11,37.693000000000005,51.085,29.535 -2020-08-04 12:00:00,105.38,33.145,49.049,29.535 -2020-08-04 12:15:00,101.69,32.468,49.049,29.535 -2020-08-04 12:30:00,100.66,31.366999999999997,49.049,29.535 -2020-08-04 12:45:00,99.77,32.162,49.049,29.535 -2020-08-04 13:00:00,98.37,31.823,49.722,29.535 -2020-08-04 13:15:00,98.25,32.902,49.722,29.535 -2020-08-04 13:30:00,97.22,31.279,49.722,29.535 -2020-08-04 13:45:00,97.65,31.354,49.722,29.535 -2020-08-04 14:00:00,98.37,32.76,49.565,29.535 -2020-08-04 14:15:00,100.11,31.919,49.565,29.535 -2020-08-04 14:30:00,100.18,31.025,49.565,29.535 -2020-08-04 14:45:00,99.55,31.674,49.565,29.535 -2020-08-04 15:00:00,100.99,33.382,51.108999999999995,29.535 -2020-08-04 15:15:00,98.14,31.217,51.108999999999995,29.535 -2020-08-04 15:30:00,97.4,29.936999999999998,51.108999999999995,29.535 -2020-08-04 15:45:00,98.43,28.403000000000002,51.108999999999995,29.535 -2020-08-04 16:00:00,102.05,30.775,52.725,29.535 -2020-08-04 16:15:00,104.62,30.676,52.725,29.535 -2020-08-04 16:30:00,102.53,30.116999999999997,52.725,29.535 -2020-08-04 16:45:00,103.42,27.68,52.725,29.535 -2020-08-04 17:00:00,104.82,30.979,58.031000000000006,29.535 -2020-08-04 17:15:00,106.88,31.469,58.031000000000006,29.535 -2020-08-04 17:30:00,106.52,30.576,58.031000000000006,29.535 -2020-08-04 17:45:00,106.89,29.989,58.031000000000006,29.535 -2020-08-04 18:00:00,107.37,32.888000000000005,58.338,29.535 -2020-08-04 18:15:00,105.89,32.689,58.338,29.535 -2020-08-04 18:30:00,105.26,31.037,58.338,29.535 -2020-08-04 18:45:00,104.69,33.591,58.338,29.535 -2020-08-04 19:00:00,102.74,34.849000000000004,58.464,29.535 -2020-08-04 19:15:00,99.6,33.861,58.464,29.535 -2020-08-04 19:30:00,98.69,32.769,58.464,29.535 -2020-08-04 19:45:00,99.54,31.89,58.464,29.535 -2020-08-04 20:00:00,98.28,29.48,63.708,29.535 -2020-08-04 20:15:00,104.76,28.862,63.708,29.535 -2020-08-04 20:30:00,104.72,29.14,63.708,29.535 -2020-08-04 20:45:00,102.08,29.421999999999997,63.708,29.535 -2020-08-04 21:00:00,93.21,28.273000000000003,57.06399999999999,29.535 -2020-08-04 21:15:00,91.37,29.372,57.06399999999999,29.535 -2020-08-04 21:30:00,88.94,29.728,57.06399999999999,29.535 -2020-08-04 21:45:00,87.22,30.253,57.06399999999999,29.535 -2020-08-04 22:00:00,81.39,27.693,52.831,29.535 -2020-08-04 22:15:00,90.48,30.084,52.831,29.535 -2020-08-04 22:30:00,86.19,25.816999999999997,52.831,29.535 -2020-08-04 22:45:00,84.07,22.903000000000002,52.831,29.535 -2020-08-04 23:00:00,76.58,20.366,44.717,29.535 -2020-08-04 23:15:00,82.38,19.252,44.717,29.535 -2020-08-04 23:30:00,81.93,18.522000000000002,44.717,29.535 -2020-08-04 23:45:00,82.08,17.589000000000002,44.717,29.535 -2020-08-05 00:00:00,74.43,17.499000000000002,41.263000000000005,29.535 -2020-08-05 00:15:00,72.81,18.273,41.263000000000005,29.535 -2020-08-05 00:30:00,73.78,17.189,41.263000000000005,29.535 -2020-08-05 00:45:00,79.92,17.368,41.263000000000005,29.535 -2020-08-05 01:00:00,79.4,17.293,38.448,29.535 -2020-08-05 01:15:00,79.23,16.547,38.448,29.535 -2020-08-05 01:30:00,73.16,15.324000000000002,38.448,29.535 -2020-08-05 01:45:00,74.56,15.296,38.448,29.535 -2020-08-05 02:00:00,71.2,15.217,36.471,29.535 -2020-08-05 02:15:00,72.53,14.187999999999999,36.471,29.535 -2020-08-05 02:30:00,76.7,15.772,36.471,29.535 -2020-08-05 02:45:00,78.9,16.442999999999998,36.471,29.535 -2020-08-05 03:00:00,76.32,17.195,36.042,29.535 -2020-08-05 03:15:00,71.19,16.965,36.042,29.535 -2020-08-05 03:30:00,73.06,16.482,36.042,29.535 -2020-08-05 03:45:00,74.45,15.94,36.042,29.535 -2020-08-05 04:00:00,76.22,19.583,36.705,29.535 -2020-08-05 04:15:00,79.11,24.499000000000002,36.705,29.535 -2020-08-05 04:30:00,80.91,22.057,36.705,29.535 -2020-08-05 04:45:00,83.59,22.023000000000003,36.705,29.535 -2020-08-05 05:00:00,89.3,32.665,39.716,29.535 -2020-08-05 05:15:00,93.87,37.975,39.716,29.535 -2020-08-05 05:30:00,98.13,33.93,39.716,29.535 -2020-08-05 05:45:00,102.24,31.743000000000002,39.716,29.535 -2020-08-05 06:00:00,110.74,31.885,52.756,29.535 -2020-08-05 06:15:00,113.42,31.543000000000003,52.756,29.535 -2020-08-05 06:30:00,114.26,31.381999999999998,52.756,29.535 -2020-08-05 06:45:00,111.89,33.946999999999996,52.756,29.535 -2020-08-05 07:00:00,112.05,33.738,65.977,29.535 -2020-08-05 07:15:00,111.49,34.732,65.977,29.535 -2020-08-05 07:30:00,110.79,33.048,65.977,29.535 -2020-08-05 07:45:00,115.87,33.79,65.977,29.535 -2020-08-05 08:00:00,116.83,29.88,57.927,29.535 -2020-08-05 08:15:00,115.09,32.696999999999996,57.927,29.535 -2020-08-05 08:30:00,115.44,33.76,57.927,29.535 -2020-08-05 08:45:00,116.68,35.958,57.927,29.535 -2020-08-05 09:00:00,119.16,31.701,54.86,29.535 -2020-08-05 09:15:00,115.56,31.124000000000002,54.86,29.535 -2020-08-05 09:30:00,112.66,34.321999999999996,54.86,29.535 -2020-08-05 09:45:00,127.72,37.173,54.86,29.535 -2020-08-05 10:00:00,125.46,34.949,52.818000000000005,29.535 -2020-08-05 10:15:00,122.4,36.124,52.818000000000005,29.535 -2020-08-05 10:30:00,115.0,35.961999999999996,52.818000000000005,29.535 -2020-08-05 10:45:00,114.33,36.883,52.818000000000005,29.535 -2020-08-05 11:00:00,111.89,34.884,52.937,29.535 -2020-08-05 11:15:00,111.04,36.0,52.937,29.535 -2020-08-05 11:30:00,107.75,36.945,52.937,29.535 -2020-08-05 11:45:00,105.24,37.798,52.937,29.535 -2020-08-05 12:00:00,104.45,33.249,50.826,29.535 -2020-08-05 12:15:00,103.52,32.566,50.826,29.535 -2020-08-05 12:30:00,103.67,31.47,50.826,29.535 -2020-08-05 12:45:00,102.21,32.255,50.826,29.535 -2020-08-05 13:00:00,100.82,31.898000000000003,50.556000000000004,29.535 -2020-08-05 13:15:00,100.96,32.968,50.556000000000004,29.535 -2020-08-05 13:30:00,105.71,31.343000000000004,50.556000000000004,29.535 -2020-08-05 13:45:00,102.53,31.427,50.556000000000004,29.535 -2020-08-05 14:00:00,104.32,32.821,51.188,29.535 -2020-08-05 14:15:00,112.39,31.984,51.188,29.535 -2020-08-05 14:30:00,116.08,31.096999999999998,51.188,29.535 -2020-08-05 14:45:00,116.4,31.75,51.188,29.535 -2020-08-05 15:00:00,114.44,33.433,52.976000000000006,29.535 -2020-08-05 15:15:00,114.65,31.269000000000002,52.976000000000006,29.535 -2020-08-05 15:30:00,115.6,29.997,52.976000000000006,29.535 -2020-08-05 15:45:00,115.27,28.464000000000002,52.976000000000006,29.535 -2020-08-05 16:00:00,113.47,30.823,55.463,29.535 -2020-08-05 16:15:00,110.57,30.73,55.463,29.535 -2020-08-05 16:30:00,109.52,30.18,55.463,29.535 -2020-08-05 16:45:00,111.03,27.772,55.463,29.535 -2020-08-05 17:00:00,109.65,31.051,59.435,29.535 -2020-08-05 17:15:00,106.85,31.566,59.435,29.535 -2020-08-05 17:30:00,106.49,30.684,59.435,29.535 -2020-08-05 17:45:00,109.02,30.124000000000002,59.435,29.535 -2020-08-05 18:00:00,109.21,33.019,61.387,29.535 -2020-08-05 18:15:00,107.26,32.824,61.387,29.535 -2020-08-05 18:30:00,107.93,31.18,61.387,29.535 -2020-08-05 18:45:00,105.44,33.734,61.387,29.535 -2020-08-05 19:00:00,102.48,34.996,63.323,29.535 -2020-08-05 19:15:00,99.33,34.006,63.323,29.535 -2020-08-05 19:30:00,98.06,32.913000000000004,63.323,29.535 -2020-08-05 19:45:00,97.58,32.037,63.323,29.535 -2020-08-05 20:00:00,98.82,29.625999999999998,69.083,29.535 -2020-08-05 20:15:00,99.21,29.009,69.083,29.535 -2020-08-05 20:30:00,100.79,29.276,69.083,29.535 -2020-08-05 20:45:00,97.25,29.539,69.083,29.535 -2020-08-05 21:00:00,94.59,28.393,59.957,29.535 -2020-08-05 21:15:00,93.52,29.486,59.957,29.535 -2020-08-05 21:30:00,89.59,29.833000000000002,59.957,29.535 -2020-08-05 21:45:00,86.86,30.335,59.957,29.535 -2020-08-05 22:00:00,81.98,27.763,53.821000000000005,29.535 -2020-08-05 22:15:00,82.56,30.144000000000002,53.821000000000005,29.535 -2020-08-05 22:30:00,80.52,25.849,53.821000000000005,29.535 -2020-08-05 22:45:00,83.39,22.934,53.821000000000005,29.535 -2020-08-05 23:00:00,76.4,20.426,45.458,29.535 -2020-08-05 23:15:00,76.83,19.312,45.458,29.535 -2020-08-05 23:30:00,76.33,18.596,45.458,29.535 -2020-08-05 23:45:00,75.85,17.664,45.458,29.535 -2020-08-06 00:00:00,71.94,17.595,40.36,29.535 -2020-08-06 00:15:00,72.79,18.368,40.36,29.535 -2020-08-06 00:30:00,71.6,17.289,40.36,29.535 -2020-08-06 00:45:00,72.0,17.474,40.36,29.535 -2020-08-06 01:00:00,70.14,17.387999999999998,38.552,29.535 -2020-08-06 01:15:00,71.15,16.648,38.552,29.535 -2020-08-06 01:30:00,70.1,15.435,38.552,29.535 -2020-08-06 01:45:00,69.93,15.404000000000002,38.552,29.535 -2020-08-06 02:00:00,76.01,15.328,36.895,29.535 -2020-08-06 02:15:00,77.91,14.318,36.895,29.535 -2020-08-06 02:30:00,75.22,15.882,36.895,29.535 -2020-08-06 02:45:00,72.63,16.552,36.895,29.535 -2020-08-06 03:00:00,72.82,17.294,36.565,29.535 -2020-08-06 03:15:00,72.3,17.077,36.565,29.535 -2020-08-06 03:30:00,74.58,16.601,36.565,29.535 -2020-08-06 03:45:00,75.09,16.062,36.565,29.535 -2020-08-06 04:00:00,79.6,19.707,37.263000000000005,29.535 -2020-08-06 04:15:00,86.77,24.622,37.263000000000005,29.535 -2020-08-06 04:30:00,88.17,22.183000000000003,37.263000000000005,29.535 -2020-08-06 04:45:00,90.68,22.151,37.263000000000005,29.535 -2020-08-06 05:00:00,90.92,32.816,40.412,29.535 -2020-08-06 05:15:00,94.47,38.146,40.412,29.535 -2020-08-06 05:30:00,102.91,34.119,40.412,29.535 -2020-08-06 05:45:00,107.32,31.916,40.412,29.535 -2020-08-06 06:00:00,110.3,32.035,49.825,29.535 -2020-08-06 06:15:00,109.16,31.708000000000002,49.825,29.535 -2020-08-06 06:30:00,113.88,31.549,49.825,29.535 -2020-08-06 06:45:00,115.25,34.126,49.825,29.535 -2020-08-06 07:00:00,118.34,33.914,61.082,29.535 -2020-08-06 07:15:00,113.87,34.925,61.082,29.535 -2020-08-06 07:30:00,116.05,33.259,61.082,29.535 -2020-08-06 07:45:00,116.6,34.01,61.082,29.535 -2020-08-06 08:00:00,115.41,30.105,53.961999999999996,29.535 -2020-08-06 08:15:00,108.89,32.903,53.961999999999996,29.535 -2020-08-06 08:30:00,114.46,33.955,53.961999999999996,29.535 -2020-08-06 08:45:00,116.44,36.141999999999996,53.961999999999996,29.535 -2020-08-06 09:00:00,117.7,31.888,50.06100000000001,29.535 -2020-08-06 09:15:00,113.69,31.304000000000002,50.06100000000001,29.535 -2020-08-06 09:30:00,109.27,34.489000000000004,50.06100000000001,29.535 -2020-08-06 09:45:00,108.39,37.323,50.06100000000001,29.535 -2020-08-06 10:00:00,109.59,35.102,47.68,29.535 -2020-08-06 10:15:00,109.22,36.260999999999996,47.68,29.535 -2020-08-06 10:30:00,115.31,36.093,47.68,29.535 -2020-08-06 10:45:00,123.63,37.009,47.68,29.535 -2020-08-06 11:00:00,120.81,35.016999999999996,45.93899999999999,29.535 -2020-08-06 11:15:00,120.53,36.128,45.93899999999999,29.535 -2020-08-06 11:30:00,115.4,37.066,45.93899999999999,29.535 -2020-08-06 11:45:00,114.1,37.907,45.93899999999999,29.535 -2020-08-06 12:00:00,108.48,33.357,43.648999999999994,29.535 -2020-08-06 12:15:00,115.79,32.666,43.648999999999994,29.535 -2020-08-06 12:30:00,114.88,31.578000000000003,43.648999999999994,29.535 -2020-08-06 12:45:00,109.1,32.354,43.648999999999994,29.535 -2020-08-06 13:00:00,105.16,31.976999999999997,42.801,29.535 -2020-08-06 13:15:00,109.12,33.035,42.801,29.535 -2020-08-06 13:30:00,109.71,31.41,42.801,29.535 -2020-08-06 13:45:00,108.45,31.504,42.801,29.535 -2020-08-06 14:00:00,105.05,32.884,43.24,29.535 -2020-08-06 14:15:00,106.1,32.052,43.24,29.535 -2020-08-06 14:30:00,113.49,31.171,43.24,29.535 -2020-08-06 14:45:00,112.04,31.83,43.24,29.535 -2020-08-06 15:00:00,106.97,33.484,45.04600000000001,29.535 -2020-08-06 15:15:00,105.8,31.323,45.04600000000001,29.535 -2020-08-06 15:30:00,104.44,30.059,45.04600000000001,29.535 -2020-08-06 15:45:00,107.22,28.526999999999997,45.04600000000001,29.535 -2020-08-06 16:00:00,109.97,30.872,46.568000000000005,29.535 -2020-08-06 16:15:00,108.43,30.785999999999998,46.568000000000005,29.535 -2020-08-06 16:30:00,109.72,30.246,46.568000000000005,29.535 -2020-08-06 16:45:00,114.1,27.866999999999997,46.568000000000005,29.535 -2020-08-06 17:00:00,114.18,31.127,50.618,29.535 -2020-08-06 17:15:00,107.45,31.666,50.618,29.535 -2020-08-06 17:30:00,110.68,30.795,50.618,29.535 -2020-08-06 17:45:00,113.82,30.261,50.618,29.535 -2020-08-06 18:00:00,115.22,33.153,52.806999999999995,29.535 -2020-08-06 18:15:00,116.03,32.963,52.806999999999995,29.535 -2020-08-06 18:30:00,115.79,31.326999999999998,52.806999999999995,29.535 -2020-08-06 18:45:00,111.98,33.882,52.806999999999995,29.535 -2020-08-06 19:00:00,104.96,35.147,53.464,29.535 -2020-08-06 19:15:00,99.09,34.155,53.464,29.535 -2020-08-06 19:30:00,97.96,33.061,53.464,29.535 -2020-08-06 19:45:00,99.34,32.188,53.464,29.535 -2020-08-06 20:00:00,102.66,29.776999999999997,56.753,29.535 -2020-08-06 20:15:00,99.01,29.160999999999998,56.753,29.535 -2020-08-06 20:30:00,98.36,29.416999999999998,56.753,29.535 -2020-08-06 20:45:00,96.53,29.66,56.753,29.535 -2020-08-06 21:00:00,92.34,28.517,52.506,29.535 -2020-08-06 21:15:00,91.64,29.603,52.506,29.535 -2020-08-06 21:30:00,87.55,29.941999999999997,52.506,29.535 -2020-08-06 21:45:00,86.87,30.42,52.506,29.535 -2020-08-06 22:00:00,82.17,27.836,48.163000000000004,29.535 -2020-08-06 22:15:00,83.32,30.206999999999997,48.163000000000004,29.535 -2020-08-06 22:30:00,79.81,25.884,48.163000000000004,29.535 -2020-08-06 22:45:00,79.96,22.967,48.163000000000004,29.535 -2020-08-06 23:00:00,73.69,20.49,42.379,29.535 -2020-08-06 23:15:00,75.01,19.374000000000002,42.379,29.535 -2020-08-06 23:30:00,75.72,18.672,42.379,29.535 -2020-08-06 23:45:00,76.69,17.742,42.379,29.535 -2020-08-07 00:00:00,71.41,16.029,38.505,29.535 -2020-08-07 00:15:00,70.97,16.997,38.505,29.535 -2020-08-07 00:30:00,66.19,16.194000000000003,38.505,29.535 -2020-08-07 00:45:00,68.12,16.788,38.505,29.535 -2020-08-07 01:00:00,69.02,16.339000000000002,37.004,29.535 -2020-08-07 01:15:00,70.25,14.969000000000001,37.004,29.535 -2020-08-07 01:30:00,70.41,14.469000000000001,37.004,29.535 -2020-08-07 01:45:00,70.59,14.186,37.004,29.535 -2020-08-07 02:00:00,76.79,15.011,36.098,29.535 -2020-08-07 02:15:00,79.54,13.975999999999999,36.098,29.535 -2020-08-07 02:30:00,74.87,16.284000000000002,36.098,29.535 -2020-08-07 02:45:00,70.69,16.291,36.098,29.535 -2020-08-07 03:00:00,70.91,17.83,36.561,29.535 -2020-08-07 03:15:00,70.99,16.409000000000002,36.561,29.535 -2020-08-07 03:30:00,73.56,15.71,36.561,29.535 -2020-08-07 03:45:00,75.0,15.999,36.561,29.535 -2020-08-07 04:00:00,77.55,19.767,37.355,29.535 -2020-08-07 04:15:00,85.83,23.194000000000003,37.355,29.535 -2020-08-07 04:30:00,87.24,21.662,37.355,29.535 -2020-08-07 04:45:00,88.67,21.076,37.355,29.535 -2020-08-07 05:00:00,92.85,31.379,40.285,29.535 -2020-08-07 05:15:00,98.87,37.567,40.285,29.535 -2020-08-07 05:30:00,96.26,33.688,40.285,29.535 -2020-08-07 05:45:00,100.51,31.046999999999997,40.285,29.535 -2020-08-07 06:00:00,106.1,31.334,52.378,29.535 -2020-08-07 06:15:00,108.88,31.254,52.378,29.535 -2020-08-07 06:30:00,111.1,31.092,52.378,29.535 -2020-08-07 06:45:00,113.24,33.475,52.378,29.535 -2020-08-07 07:00:00,114.37,33.924,60.891999999999996,29.535 -2020-08-07 07:15:00,113.51,35.764,60.891999999999996,29.535 -2020-08-07 07:30:00,113.78,32.241,60.891999999999996,29.535 -2020-08-07 07:45:00,114.69,32.854,60.891999999999996,29.535 -2020-08-07 08:00:00,115.11,29.781,53.652,29.535 -2020-08-07 08:15:00,119.6,33.239000000000004,53.652,29.535 -2020-08-07 08:30:00,121.84,34.12,53.652,29.535 -2020-08-07 08:45:00,120.12,36.262,53.652,29.535 -2020-08-07 09:00:00,124.38,29.730999999999998,51.456,29.535 -2020-08-07 09:15:00,127.6,30.976999999999997,51.456,29.535 -2020-08-07 09:30:00,130.39,33.506,51.456,29.535 -2020-08-07 09:45:00,126.25,36.711,51.456,29.535 -2020-08-07 10:00:00,120.83,34.423,49.4,29.535 -2020-08-07 10:15:00,127.56,35.29,49.4,29.535 -2020-08-07 10:30:00,127.41,35.665,49.4,29.535 -2020-08-07 10:45:00,126.26,36.516999999999996,49.4,29.535 -2020-08-07 11:00:00,118.94,34.779,48.773,29.535 -2020-08-07 11:15:00,114.75,34.885,48.773,29.535 -2020-08-07 11:30:00,117.42,35.325,48.773,29.535 -2020-08-07 11:45:00,115.42,35.209,48.773,29.535 -2020-08-07 12:00:00,110.13,30.995,46.033,29.535 -2020-08-07 12:15:00,110.2,29.831999999999997,46.033,29.535 -2020-08-07 12:30:00,109.71,28.854,46.033,29.535 -2020-08-07 12:45:00,110.98,28.804000000000002,46.033,29.535 -2020-08-07 13:00:00,108.18,28.916999999999998,44.38399999999999,29.535 -2020-08-07 13:15:00,108.46,30.076999999999998,44.38399999999999,29.535 -2020-08-07 13:30:00,109.25,29.230999999999998,44.38399999999999,29.535 -2020-08-07 13:45:00,111.29,29.642,44.38399999999999,29.535 -2020-08-07 14:00:00,99.65,30.31,43.162,29.535 -2020-08-07 14:15:00,103.35,29.925,43.162,29.535 -2020-08-07 14:30:00,100.75,30.491,43.162,29.535 -2020-08-07 14:45:00,103.99,30.454,43.162,29.535 -2020-08-07 15:00:00,92.04,32.04,44.91,29.535 -2020-08-07 15:15:00,93.49,29.68,44.91,29.535 -2020-08-07 15:30:00,91.17,27.985,44.91,29.535 -2020-08-07 15:45:00,93.1,27.19,44.91,29.535 -2020-08-07 16:00:00,99.66,28.771,47.489,29.535 -2020-08-07 16:15:00,101.19,29.153000000000002,47.489,29.535 -2020-08-07 16:30:00,102.13,28.436999999999998,47.489,29.535 -2020-08-07 16:45:00,100.1,25.329,47.489,29.535 -2020-08-07 17:00:00,107.59,30.236,52.047,29.535 -2020-08-07 17:15:00,107.4,30.662,52.047,29.535 -2020-08-07 17:30:00,106.27,29.985,52.047,29.535 -2020-08-07 17:45:00,100.88,29.333000000000002,52.047,29.535 -2020-08-07 18:00:00,106.56,32.196,53.306000000000004,29.535 -2020-08-07 18:15:00,103.38,31.123,53.306000000000004,29.535 -2020-08-07 18:30:00,105.2,29.368000000000002,53.306000000000004,29.535 -2020-08-07 18:45:00,105.43,32.33,53.306000000000004,29.535 -2020-08-07 19:00:00,100.82,34.372,53.516000000000005,29.535 -2020-08-07 19:15:00,99.6,33.865,53.516000000000005,29.535 -2020-08-07 19:30:00,101.51,32.855,53.516000000000005,29.535 -2020-08-07 19:45:00,97.37,31.035,53.516000000000005,29.535 -2020-08-07 20:00:00,96.5,28.47,57.88,29.535 -2020-08-07 20:15:00,97.87,28.62,57.88,29.535 -2020-08-07 20:30:00,97.05,28.397,57.88,29.535 -2020-08-07 20:45:00,91.55,27.805,57.88,29.535 -2020-08-07 21:00:00,87.14,27.892,53.32,29.535 -2020-08-07 21:15:00,88.39,30.548000000000002,53.32,29.535 -2020-08-07 21:30:00,86.76,30.733,53.32,29.535 -2020-08-07 21:45:00,86.09,31.351,53.32,29.535 -2020-08-07 22:00:00,78.09,28.561,48.074,29.535 -2020-08-07 22:15:00,76.09,30.691,48.074,29.535 -2020-08-07 22:30:00,77.97,30.533,48.074,29.535 -2020-08-07 22:45:00,79.31,28.241999999999997,48.074,29.535 -2020-08-07 23:00:00,74.19,27.438000000000002,41.306999999999995,29.535 -2020-08-07 23:15:00,68.5,24.93,41.306999999999995,29.535 -2020-08-07 23:30:00,66.0,22.58,41.306999999999995,29.535 -2020-08-07 23:45:00,65.38,21.570999999999998,41.306999999999995,29.535 -2020-08-08 00:00:00,66.74,17.496,40.227,29.423000000000002 -2020-08-08 00:15:00,69.29,18.078,40.227,29.423000000000002 -2020-08-08 00:30:00,68.8,16.746,40.227,29.423000000000002 -2020-08-08 00:45:00,63.95,16.6,40.227,29.423000000000002 -2020-08-08 01:00:00,66.72,16.363,36.303000000000004,29.423000000000002 -2020-08-08 01:15:00,67.67,15.626,36.303000000000004,29.423000000000002 -2020-08-08 01:30:00,68.13,14.392999999999999,36.303000000000004,29.423000000000002 -2020-08-08 01:45:00,65.03,15.319,36.303000000000004,29.423000000000002 -2020-08-08 02:00:00,64.36,15.154000000000002,33.849000000000004,29.423000000000002 -2020-08-08 02:15:00,66.91,13.402000000000001,33.849000000000004,29.423000000000002 -2020-08-08 02:30:00,66.76,14.997,33.849000000000004,29.423000000000002 -2020-08-08 02:45:00,63.89,15.75,33.849000000000004,29.423000000000002 -2020-08-08 03:00:00,58.71,15.923,33.149,29.423000000000002 -2020-08-08 03:15:00,64.73,13.921,33.149,29.423000000000002 -2020-08-08 03:30:00,67.22,13.64,33.149,29.423000000000002 -2020-08-08 03:45:00,67.03,15.345,33.149,29.423000000000002 -2020-08-08 04:00:00,65.26,17.557000000000002,32.501,29.423000000000002 -2020-08-08 04:15:00,60.78,20.284000000000002,32.501,29.423000000000002 -2020-08-08 04:30:00,63.06,17.284000000000002,32.501,29.423000000000002 -2020-08-08 04:45:00,63.74,16.992,32.501,29.423000000000002 -2020-08-08 05:00:00,68.08,20.31,31.648000000000003,29.423000000000002 -2020-08-08 05:15:00,70.17,17.18,31.648000000000003,29.423000000000002 -2020-08-08 05:30:00,69.63,14.745999999999999,31.648000000000003,29.423000000000002 -2020-08-08 05:45:00,69.72,16.029,31.648000000000003,29.423000000000002 -2020-08-08 06:00:00,74.01,25.884,32.552,29.423000000000002 -2020-08-08 06:15:00,76.69,32.289,32.552,29.423000000000002 -2020-08-08 06:30:00,74.92,29.739,32.552,29.423000000000002 -2020-08-08 06:45:00,75.68,29.601,32.552,29.423000000000002 -2020-08-08 07:00:00,78.07,29.214000000000002,35.181999999999995,29.423000000000002 -2020-08-08 07:15:00,77.53,29.912,35.181999999999995,29.423000000000002 -2020-08-08 07:30:00,79.7,27.668000000000003,35.181999999999995,29.423000000000002 -2020-08-08 07:45:00,85.61,28.895,35.181999999999995,29.423000000000002 -2020-08-08 08:00:00,89.39,26.308000000000003,40.35,29.423000000000002 -2020-08-08 08:15:00,89.11,29.252,40.35,29.423000000000002 -2020-08-08 08:30:00,80.69,29.98,40.35,29.423000000000002 -2020-08-08 08:45:00,79.94,32.78,40.35,29.423000000000002 -2020-08-08 09:00:00,81.67,29.186,42.292,29.423000000000002 -2020-08-08 09:15:00,85.15,30.836,42.292,29.423000000000002 -2020-08-08 09:30:00,86.84,33.758,42.292,29.423000000000002 -2020-08-08 09:45:00,85.72,36.493,42.292,29.423000000000002 -2020-08-08 10:00:00,78.03,34.819,40.084,29.423000000000002 -2020-08-08 10:15:00,80.44,36.003,40.084,29.423000000000002 -2020-08-08 10:30:00,87.73,36.022,40.084,29.423000000000002 -2020-08-08 10:45:00,76.01,36.408,40.084,29.423000000000002 -2020-08-08 11:00:00,75.02,34.505,36.966,29.423000000000002 -2020-08-08 11:15:00,71.3,35.515,36.966,29.423000000000002 -2020-08-08 11:30:00,71.44,36.321,36.966,29.423000000000002 -2020-08-08 11:45:00,83.52,36.929,36.966,29.423000000000002 -2020-08-08 12:00:00,78.82,33.196,35.19,29.423000000000002 -2020-08-08 12:15:00,77.58,32.803000000000004,35.19,29.423000000000002 -2020-08-08 12:30:00,68.36,31.677,35.19,29.423000000000002 -2020-08-08 12:45:00,71.92,32.397,35.19,29.423000000000002 -2020-08-08 13:00:00,65.67,31.694000000000003,32.277,29.423000000000002 -2020-08-08 13:15:00,71.96,32.49,32.277,29.423000000000002 -2020-08-08 13:30:00,63.1,31.866,32.277,29.423000000000002 -2020-08-08 13:45:00,64.32,30.926,32.277,29.423000000000002 -2020-08-08 14:00:00,72.95,31.502,31.436999999999998,29.423000000000002 -2020-08-08 14:15:00,65.23,29.944000000000003,31.436999999999998,29.423000000000002 -2020-08-08 14:30:00,63.29,30.256,31.436999999999998,29.423000000000002 -2020-08-08 14:45:00,69.43,30.671999999999997,31.436999999999998,29.423000000000002 -2020-08-08 15:00:00,64.98,32.576,33.493,29.423000000000002 -2020-08-08 15:15:00,62.48,30.843000000000004,33.493,29.423000000000002 -2020-08-08 15:30:00,61.84,29.228,33.493,29.423000000000002 -2020-08-08 15:45:00,64.18,27.535999999999998,33.493,29.423000000000002 -2020-08-08 16:00:00,64.57,31.239,36.593,29.423000000000002 -2020-08-08 16:15:00,66.55,30.691,36.593,29.423000000000002 -2020-08-08 16:30:00,72.26,30.206999999999997,36.593,29.423000000000002 -2020-08-08 16:45:00,72.92,27.033,36.593,29.423000000000002 -2020-08-08 17:00:00,74.06,30.78,42.049,29.423000000000002 -2020-08-08 17:15:00,73.2,29.022,42.049,29.423000000000002 -2020-08-08 17:30:00,75.04,28.235,42.049,29.423000000000002 -2020-08-08 17:45:00,77.03,28.121,42.049,29.423000000000002 -2020-08-08 18:00:00,78.39,32.303000000000004,43.755,29.423000000000002 -2020-08-08 18:15:00,76.69,32.72,43.755,29.423000000000002 -2020-08-08 18:30:00,76.27,32.189,43.755,29.423000000000002 -2020-08-08 18:45:00,76.64,32.038000000000004,43.755,29.423000000000002 -2020-08-08 19:00:00,75.57,32.424,44.492,29.423000000000002 -2020-08-08 19:15:00,72.67,30.984,44.492,29.423000000000002 -2020-08-08 19:30:00,72.39,30.662,44.492,29.423000000000002 -2020-08-08 19:45:00,73.31,30.538,44.492,29.423000000000002 -2020-08-08 20:00:00,75.0,28.638,40.896,29.423000000000002 -2020-08-08 20:15:00,74.88,28.051,40.896,29.423000000000002 -2020-08-08 20:30:00,74.76,27.0,40.896,29.423000000000002 -2020-08-08 20:45:00,74.69,28.18,40.896,29.423000000000002 -2020-08-08 21:00:00,71.99,26.721999999999998,39.056,29.423000000000002 -2020-08-08 21:15:00,71.04,29.015,39.056,29.423000000000002 -2020-08-08 21:30:00,67.87,29.302,39.056,29.423000000000002 -2020-08-08 21:45:00,67.58,29.401,39.056,29.423000000000002 -2020-08-08 22:00:00,63.3,26.502,38.478,29.423000000000002 -2020-08-08 22:15:00,64.97,28.721,38.478,29.423000000000002 -2020-08-08 22:30:00,62.91,27.836,38.478,29.423000000000002 -2020-08-08 22:45:00,62.35,25.795,38.478,29.423000000000002 -2020-08-08 23:00:00,57.64,24.291,32.953,29.423000000000002 -2020-08-08 23:15:00,57.7,22.261999999999997,32.953,29.423000000000002 -2020-08-08 23:30:00,58.28,22.333000000000002,32.953,29.423000000000002 -2020-08-08 23:45:00,58.51,21.79,32.953,29.423000000000002 -2020-08-09 00:00:00,54.08,18.893,28.584,29.423000000000002 -2020-08-09 00:15:00,54.76,18.393,28.584,29.423000000000002 -2020-08-09 00:30:00,54.22,16.949,28.584,29.423000000000002 -2020-08-09 00:45:00,55.07,16.660999999999998,28.584,29.423000000000002 -2020-08-09 01:00:00,51.21,16.673,26.419,29.423000000000002 -2020-08-09 01:15:00,53.23,15.76,26.419,29.423000000000002 -2020-08-09 01:30:00,50.21,14.374,26.419,29.423000000000002 -2020-08-09 01:45:00,52.88,14.954,26.419,29.423000000000002 -2020-08-09 02:00:00,51.33,14.915,25.335,29.423000000000002 -2020-08-09 02:15:00,52.26,13.91,25.335,29.423000000000002 -2020-08-09 02:30:00,51.66,15.61,25.335,29.423000000000002 -2020-08-09 02:45:00,51.22,16.055999999999997,25.335,29.423000000000002 -2020-08-09 03:00:00,51.33,16.819000000000003,24.805,29.423000000000002 -2020-08-09 03:15:00,52.88,15.114,24.805,29.423000000000002 -2020-08-09 03:30:00,52.66,14.052,24.805,29.423000000000002 -2020-08-09 03:45:00,53.13,15.005999999999998,24.805,29.423000000000002 -2020-08-09 04:00:00,53.45,17.209,25.772,29.423000000000002 -2020-08-09 04:15:00,54.38,19.586,25.772,29.423000000000002 -2020-08-09 04:30:00,52.71,17.863,25.772,29.423000000000002 -2020-08-09 04:45:00,52.22,17.111,25.772,29.423000000000002 -2020-08-09 05:00:00,51.09,20.83,25.971999999999998,29.423000000000002 -2020-08-09 05:15:00,51.86,17.137999999999998,25.971999999999998,29.423000000000002 -2020-08-09 05:30:00,51.9,14.347999999999999,25.971999999999998,29.423000000000002 -2020-08-09 05:45:00,52.64,15.338,25.971999999999998,29.423000000000002 -2020-08-09 06:00:00,52.67,22.945,26.026,29.423000000000002 -2020-08-09 06:15:00,53.6,30.255,26.026,29.423000000000002 -2020-08-09 06:30:00,54.27,27.113000000000003,26.026,29.423000000000002 -2020-08-09 06:45:00,55.49,26.105999999999998,26.026,29.423000000000002 -2020-08-09 07:00:00,57.23,25.877,27.396,29.423000000000002 -2020-08-09 07:15:00,57.15,25.078000000000003,27.396,29.423000000000002 -2020-08-09 07:30:00,56.53,24.169,27.396,29.423000000000002 -2020-08-09 07:45:00,56.43,25.428,27.396,29.423000000000002 -2020-08-09 08:00:00,56.12,23.463,30.791999999999998,29.423000000000002 -2020-08-09 08:15:00,57.24,27.535,30.791999999999998,29.423000000000002 -2020-08-09 08:30:00,56.64,28.962,30.791999999999998,29.423000000000002 -2020-08-09 08:45:00,57.8,31.506999999999998,30.791999999999998,29.423000000000002 -2020-08-09 09:00:00,57.9,27.857,32.482,29.423000000000002 -2020-08-09 09:15:00,57.48,28.969,32.482,29.423000000000002 -2020-08-09 09:30:00,53.71,32.31,32.482,29.423000000000002 -2020-08-09 09:45:00,57.49,36.016999999999996,32.482,29.423000000000002 -2020-08-09 10:00:00,58.06,34.648,31.951,29.423000000000002 -2020-08-09 10:15:00,59.54,35.896,31.951,29.423000000000002 -2020-08-09 10:30:00,61.42,36.068000000000005,31.951,29.423000000000002 -2020-08-09 10:45:00,60.84,37.628,31.951,29.423000000000002 -2020-08-09 11:00:00,60.37,35.299,33.619,29.423000000000002 -2020-08-09 11:15:00,61.49,35.853,33.619,29.423000000000002 -2020-08-09 11:30:00,57.13,37.247,33.619,29.423000000000002 -2020-08-09 11:45:00,57.85,38.035,33.619,29.423000000000002 -2020-08-09 12:00:00,53.65,35.400999999999996,30.975,29.423000000000002 -2020-08-09 12:15:00,52.86,34.196999999999996,30.975,29.423000000000002 -2020-08-09 12:30:00,50.62,33.433,30.975,29.423000000000002 -2020-08-09 12:45:00,50.63,33.611999999999995,30.975,29.423000000000002 -2020-08-09 13:00:00,49.19,32.663000000000004,27.956999999999997,29.423000000000002 -2020-08-09 13:15:00,48.43,32.585,27.956999999999997,29.423000000000002 -2020-08-09 13:30:00,48.61,30.886999999999997,27.956999999999997,29.423000000000002 -2020-08-09 13:45:00,49.71,31.119,27.956999999999997,29.423000000000002 -2020-08-09 14:00:00,49.4,32.830999999999996,25.555999999999997,29.423000000000002 -2020-08-09 14:15:00,50.46,31.583000000000002,25.555999999999997,29.423000000000002 -2020-08-09 14:30:00,49.99,30.576,25.555999999999997,29.423000000000002 -2020-08-09 14:45:00,51.47,29.987,25.555999999999997,29.423000000000002 -2020-08-09 15:00:00,52.05,32.154,26.271,29.423000000000002 -2020-08-09 15:15:00,51.23,29.56,26.271,29.423000000000002 -2020-08-09 15:30:00,52.35,27.721999999999998,26.271,29.423000000000002 -2020-08-09 15:45:00,54.8,26.245,26.271,29.423000000000002 -2020-08-09 16:00:00,58.39,28.193,30.369,29.423000000000002 -2020-08-09 16:15:00,59.37,27.954,30.369,29.423000000000002 -2020-08-09 16:30:00,61.94,28.434,30.369,29.423000000000002 -2020-08-09 16:45:00,64.25,25.340999999999998,30.369,29.423000000000002 -2020-08-09 17:00:00,67.55,29.435,38.787,29.423000000000002 -2020-08-09 17:15:00,69.77,29.199,38.787,29.423000000000002 -2020-08-09 17:30:00,74.47,29.183000000000003,38.787,29.423000000000002 -2020-08-09 17:45:00,73.06,29.31,38.787,29.423000000000002 -2020-08-09 18:00:00,76.18,34.126999999999995,41.886,29.423000000000002 -2020-08-09 18:15:00,75.37,34.056,41.886,29.423000000000002 -2020-08-09 18:30:00,78.04,33.457,41.886,29.423000000000002 -2020-08-09 18:45:00,75.37,33.274,41.886,29.423000000000002 -2020-08-09 19:00:00,77.91,35.796,42.91,29.423000000000002 -2020-08-09 19:15:00,76.23,33.253,42.91,29.423000000000002 -2020-08-09 19:30:00,76.04,32.707,42.91,29.423000000000002 -2020-08-09 19:45:00,78.63,32.049,42.91,29.423000000000002 -2020-08-09 20:00:00,79.32,30.315,42.148999999999994,29.423000000000002 -2020-08-09 20:15:00,78.88,29.53,42.148999999999994,29.423000000000002 -2020-08-09 20:30:00,78.51,29.153000000000002,42.148999999999994,29.423000000000002 -2020-08-09 20:45:00,77.97,28.791,42.148999999999994,29.423000000000002 -2020-08-09 21:00:00,76.29,27.416,40.955999999999996,29.423000000000002 -2020-08-09 21:15:00,76.26,29.456999999999997,40.955999999999996,29.423000000000002 -2020-08-09 21:30:00,74.21,29.101,40.955999999999996,29.423000000000002 -2020-08-09 21:45:00,72.91,29.506,40.955999999999996,29.423000000000002 -2020-08-09 22:00:00,68.37,28.61,39.873000000000005,29.423000000000002 -2020-08-09 22:15:00,70.97,29.274,39.873000000000005,29.423000000000002 -2020-08-09 22:30:00,68.0,28.081,39.873000000000005,29.423000000000002 -2020-08-09 22:45:00,68.68,24.886999999999997,39.873000000000005,29.423000000000002 -2020-08-09 23:00:00,64.15,23.305999999999997,35.510999999999996,29.423000000000002 -2020-08-09 23:15:00,65.15,22.361,35.510999999999996,29.423000000000002 -2020-08-09 23:30:00,65.42,21.884,35.510999999999996,29.423000000000002 -2020-08-09 23:45:00,64.78,21.439,35.510999999999996,29.423000000000002 -2020-08-10 00:00:00,64.59,20.146,33.475,29.535 -2020-08-10 00:15:00,67.7,20.188,33.475,29.535 -2020-08-10 00:30:00,67.92,18.373,33.475,29.535 -2020-08-10 00:45:00,66.94,17.753,33.475,29.535 -2020-08-10 01:00:00,60.75,18.12,33.111,29.535 -2020-08-10 01:15:00,62.05,17.266,33.111,29.535 -2020-08-10 01:30:00,61.91,16.230999999999998,33.111,29.535 -2020-08-10 01:45:00,61.67,16.708,33.111,29.535 -2020-08-10 02:00:00,62.03,17.11,32.358000000000004,29.535 -2020-08-10 02:15:00,66.27,15.054,32.358000000000004,29.535 -2020-08-10 02:30:00,69.88,16.866,32.358000000000004,29.535 -2020-08-10 02:45:00,70.55,17.215,32.358000000000004,29.535 -2020-08-10 03:00:00,67.73,18.362000000000002,30.779,29.535 -2020-08-10 03:15:00,64.33,17.243,30.779,29.535 -2020-08-10 03:30:00,68.6,16.858,30.779,29.535 -2020-08-10 03:45:00,68.36,17.41,30.779,29.535 -2020-08-10 04:00:00,72.46,22.284000000000002,31.416,29.535 -2020-08-10 04:15:00,75.4,27.168000000000003,31.416,29.535 -2020-08-10 04:30:00,77.15,24.838,31.416,29.535 -2020-08-10 04:45:00,82.65,24.43,31.416,29.535 -2020-08-10 05:00:00,87.89,34.043,37.221,29.535 -2020-08-10 05:15:00,93.43,38.881,37.221,29.535 -2020-08-10 05:30:00,98.76,34.632,37.221,29.535 -2020-08-10 05:45:00,106.75,33.045,37.221,29.535 -2020-08-10 06:00:00,109.61,31.978,51.891000000000005,29.535 -2020-08-10 06:15:00,110.17,31.636,51.891000000000005,29.535 -2020-08-10 06:30:00,106.16,31.749000000000002,51.891000000000005,29.535 -2020-08-10 06:45:00,105.44,35.194,51.891000000000005,29.535 -2020-08-10 07:00:00,112.69,34.809,62.282,29.535 -2020-08-10 07:15:00,114.13,36.123000000000005,62.282,29.535 -2020-08-10 07:30:00,114.44,34.466,62.282,29.535 -2020-08-10 07:45:00,111.46,36.219,62.282,29.535 -2020-08-10 08:00:00,109.2,32.409,54.102,29.535 -2020-08-10 08:15:00,114.03,35.476,54.102,29.535 -2020-08-10 08:30:00,115.94,36.306999999999995,54.102,29.535 -2020-08-10 08:45:00,113.48,39.326,54.102,29.535 -2020-08-10 09:00:00,109.67,34.673,50.917,29.535 -2020-08-10 09:15:00,113.16,34.354,50.917,29.535 -2020-08-10 09:30:00,114.97,36.866,50.917,29.535 -2020-08-10 09:45:00,114.23,38.284,50.917,29.535 -2020-08-10 10:00:00,110.17,37.372,49.718999999999994,29.535 -2020-08-10 10:15:00,112.96,38.489000000000004,49.718999999999994,29.535 -2020-08-10 10:30:00,113.44,38.289,49.718999999999994,29.535 -2020-08-10 10:45:00,112.68,38.208,49.718999999999994,29.535 -2020-08-10 11:00:00,106.81,36.395,49.833999999999996,29.535 -2020-08-10 11:15:00,101.75,37.046,49.833999999999996,29.535 -2020-08-10 11:30:00,108.76,38.986,49.833999999999996,29.535 -2020-08-10 11:45:00,115.96,40.294000000000004,49.833999999999996,29.535 -2020-08-10 12:00:00,116.04,35.781,47.832,29.535 -2020-08-10 12:15:00,108.93,34.679,47.832,29.535 -2020-08-10 12:30:00,105.17,32.842,47.832,29.535 -2020-08-10 12:45:00,109.13,32.858000000000004,47.832,29.535 -2020-08-10 13:00:00,105.35,32.725,48.03,29.535 -2020-08-10 13:15:00,106.96,31.901999999999997,48.03,29.535 -2020-08-10 13:30:00,112.55,30.421999999999997,48.03,29.535 -2020-08-10 13:45:00,110.89,31.543000000000003,48.03,29.535 -2020-08-10 14:00:00,108.24,32.449,48.157,29.535 -2020-08-10 14:15:00,116.67,31.851,48.157,29.535 -2020-08-10 14:30:00,109.93,30.743000000000002,48.157,29.535 -2020-08-10 14:45:00,100.75,32.195,48.157,29.535 -2020-08-10 15:00:00,102.13,33.843,48.897,29.535 -2020-08-10 15:15:00,101.58,30.81,48.897,29.535 -2020-08-10 15:30:00,103.82,29.811999999999998,48.897,29.535 -2020-08-10 15:45:00,112.32,27.903000000000002,48.897,29.535 -2020-08-10 16:00:00,107.56,30.926,51.446000000000005,29.535 -2020-08-10 16:15:00,110.02,30.826,51.446000000000005,29.535 -2020-08-10 16:30:00,111.09,30.749000000000002,51.446000000000005,29.535 -2020-08-10 16:45:00,111.76,27.815,51.446000000000005,29.535 -2020-08-10 17:00:00,116.67,30.83,57.507,29.535 -2020-08-10 17:15:00,115.62,31.066999999999997,57.507,29.535 -2020-08-10 17:30:00,119.59,30.733,57.507,29.535 -2020-08-10 17:45:00,117.62,30.63,57.507,29.535 -2020-08-10 18:00:00,112.36,34.413000000000004,57.896,29.535 -2020-08-10 18:15:00,111.68,32.756,57.896,29.535 -2020-08-10 18:30:00,111.01,31.386,57.896,29.535 -2020-08-10 18:45:00,118.2,34.199,57.896,29.535 -2020-08-10 19:00:00,115.05,36.61,57.891999999999996,29.535 -2020-08-10 19:15:00,108.14,35.417,57.891999999999996,29.535 -2020-08-10 19:30:00,100.95,34.482,57.891999999999996,29.535 -2020-08-10 19:45:00,107.15,33.301,57.891999999999996,29.535 -2020-08-10 20:00:00,107.38,30.524,64.57300000000001,29.535 -2020-08-10 20:15:00,106.34,31.261,64.57300000000001,29.535 -2020-08-10 20:30:00,101.37,31.505,64.57300000000001,29.535 -2020-08-10 20:45:00,96.58,31.21,64.57300000000001,29.535 -2020-08-10 21:00:00,92.54,29.186,59.431999999999995,29.535 -2020-08-10 21:15:00,98.18,31.721999999999998,59.431999999999995,29.535 -2020-08-10 21:30:00,94.61,31.811,59.431999999999995,29.535 -2020-08-10 21:45:00,89.3,32.016,59.431999999999995,29.535 -2020-08-10 22:00:00,85.65,29.331999999999997,51.519,29.535 -2020-08-10 22:15:00,83.02,31.964000000000002,51.519,29.535 -2020-08-10 22:30:00,84.02,27.245,51.519,29.535 -2020-08-10 22:45:00,86.92,24.348000000000003,51.519,29.535 -2020-08-10 23:00:00,83.27,22.737,44.501000000000005,29.535 -2020-08-10 23:15:00,79.13,20.052,44.501000000000005,29.535 -2020-08-10 23:30:00,77.81,19.378,44.501000000000005,29.535 -2020-08-10 23:45:00,76.11,18.256,44.501000000000005,29.535 -2020-08-11 00:00:00,80.82,18.12,44.522,29.535 -2020-08-11 00:15:00,81.42,18.89,44.522,29.535 -2020-08-11 00:30:00,79.26,17.834,44.522,29.535 -2020-08-11 00:45:00,77.23,18.055999999999997,44.522,29.535 -2020-08-11 01:00:00,77.88,17.902,41.441,29.535 -2020-08-11 01:15:00,81.19,17.195,41.441,29.535 -2020-08-11 01:30:00,80.87,16.033,41.441,29.535 -2020-08-11 01:45:00,77.52,15.99,41.441,29.535 -2020-08-11 02:00:00,76.14,15.933,40.203,29.535 -2020-08-11 02:15:00,80.33,15.019,40.203,29.535 -2020-08-11 02:30:00,80.22,16.476,40.203,29.535 -2020-08-11 02:45:00,76.52,17.139,40.203,29.535 -2020-08-11 03:00:00,76.12,17.828,39.536,29.535 -2020-08-11 03:15:00,81.42,17.687,39.536,29.535 -2020-08-11 03:30:00,82.68,17.243,39.536,29.535 -2020-08-11 03:45:00,81.38,16.709,39.536,29.535 -2020-08-11 04:00:00,84.21,20.386,40.759,29.535 -2020-08-11 04:15:00,83.25,25.311,40.759,29.535 -2020-08-11 04:30:00,87.0,22.891,40.759,29.535 -2020-08-11 04:45:00,96.1,22.866999999999997,40.759,29.535 -2020-08-11 05:00:00,103.35,33.692,43.623999999999995,29.535 -2020-08-11 05:15:00,104.96,39.169000000000004,43.623999999999995,29.535 -2020-08-11 05:30:00,101.93,35.208,43.623999999999995,29.535 -2020-08-11 05:45:00,103.66,32.900999999999996,43.623999999999995,29.535 -2020-08-11 06:00:00,107.81,32.9,52.684,29.535 -2020-08-11 06:15:00,115.41,32.653,52.684,29.535 -2020-08-11 06:30:00,119.45,32.504,52.684,29.535 -2020-08-11 06:45:00,121.63,35.13,52.684,29.535 -2020-08-11 07:00:00,117.33,34.907,62.676,29.535 -2020-08-11 07:15:00,117.63,36.001,62.676,29.535 -2020-08-11 07:30:00,120.47,34.424,62.676,29.535 -2020-08-11 07:45:00,121.97,35.217,62.676,29.535 -2020-08-11 08:00:00,118.67,31.328000000000003,56.161,29.535 -2020-08-11 08:15:00,115.82,34.021,56.161,29.535 -2020-08-11 08:30:00,109.61,35.024,56.161,29.535 -2020-08-11 08:45:00,108.39,37.145,56.161,29.535 -2020-08-11 09:00:00,118.63,32.913000000000004,52.132,29.535 -2020-08-11 09:15:00,124.0,32.294000000000004,52.132,29.535 -2020-08-11 09:30:00,120.89,35.407,52.132,29.535 -2020-08-11 09:45:00,113.74,38.154,52.132,29.535 -2020-08-11 10:00:00,115.04,35.943000000000005,51.032,29.535 -2020-08-11 10:15:00,111.74,37.010999999999996,51.032,29.535 -2020-08-11 10:30:00,117.81,36.813,51.032,29.535 -2020-08-11 10:45:00,120.03,37.7,51.032,29.535 -2020-08-11 11:00:00,116.21,35.745,51.085,29.535 -2020-08-11 11:15:00,109.85,36.827,51.085,29.535 -2020-08-11 11:30:00,112.41,37.735,51.085,29.535 -2020-08-11 11:45:00,117.13,38.514,51.085,29.535 -2020-08-11 12:00:00,121.63,33.95,49.049,29.535 -2020-08-11 12:15:00,122.61,33.218,49.049,29.535 -2020-08-11 12:30:00,116.41,32.172,49.049,29.535 -2020-08-11 12:45:00,106.88,32.898,49.049,29.535 -2020-08-11 13:00:00,103.94,32.428000000000004,49.722,29.535 -2020-08-11 13:15:00,106.12,33.428000000000004,49.722,29.535 -2020-08-11 13:30:00,107.33,31.793000000000003,49.722,29.535 -2020-08-11 13:45:00,105.9,31.94,49.722,29.535 -2020-08-11 14:00:00,104.55,33.245,49.565,29.535 -2020-08-11 14:15:00,111.12,32.436,49.565,29.535 -2020-08-11 14:30:00,106.63,31.599,49.565,29.535 -2020-08-11 14:45:00,112.83,32.278,49.565,29.535 -2020-08-11 15:00:00,106.44,33.777,51.108999999999995,29.535 -2020-08-11 15:15:00,107.53,31.633000000000003,51.108999999999995,29.535 -2020-08-11 15:30:00,111.68,30.412,51.108999999999995,29.535 -2020-08-11 15:45:00,112.97,28.89,51.108999999999995,29.535 -2020-08-11 16:00:00,110.35,31.155,52.725,29.535 -2020-08-11 16:15:00,106.75,31.103,52.725,29.535 -2020-08-11 16:30:00,107.28,30.604,52.725,29.535 -2020-08-11 16:45:00,113.29,28.383000000000003,52.725,29.535 -2020-08-11 17:00:00,120.26,31.531999999999996,58.031000000000006,29.535 -2020-08-11 17:15:00,116.61,32.205,58.031000000000006,29.535 -2020-08-11 17:30:00,113.1,31.391,58.031000000000006,29.535 -2020-08-11 17:45:00,115.09,31.000999999999998,58.031000000000006,29.535 -2020-08-11 18:00:00,111.56,33.868,58.338,29.535 -2020-08-11 18:15:00,114.45,33.714,58.338,29.535 -2020-08-11 18:30:00,115.07,32.123000000000005,58.338,29.535 -2020-08-11 18:45:00,115.29,34.679,58.338,29.535 -2020-08-11 19:00:00,107.15,35.964,58.464,29.535 -2020-08-11 19:15:00,108.56,34.964,58.464,29.535 -2020-08-11 19:30:00,109.56,33.873000000000005,58.464,29.535 -2020-08-11 19:45:00,111.07,33.014,58.464,29.535 -2020-08-11 20:00:00,104.95,30.608,63.708,29.535 -2020-08-11 20:15:00,102.1,30.0,63.708,29.535 -2020-08-11 20:30:00,97.33,30.191999999999997,63.708,29.535 -2020-08-11 20:45:00,97.81,30.324,63.708,29.535 -2020-08-11 21:00:00,95.02,29.195,57.06399999999999,29.535 -2020-08-11 21:15:00,98.79,30.244,57.06399999999999,29.535 -2020-08-11 21:30:00,96.16,30.55,57.06399999999999,29.535 -2020-08-11 21:45:00,94.75,30.9,57.06399999999999,29.535 -2020-08-11 22:00:00,85.73,28.247,52.831,29.535 -2020-08-11 22:15:00,90.12,30.564,52.831,29.535 -2020-08-11 22:30:00,87.77,26.09,52.831,29.535 -2020-08-11 22:45:00,86.92,23.166999999999998,52.831,29.535 -2020-08-11 23:00:00,83.18,20.858,44.717,29.535 -2020-08-11 23:15:00,84.73,19.727,44.717,29.535 -2020-08-11 23:30:00,83.67,19.087,44.717,29.535 -2020-08-11 23:45:00,76.9,18.173,44.717,29.535 -2020-08-12 00:00:00,72.5,18.233,41.263000000000005,29.535 -2020-08-12 00:15:00,78.82,19.003,41.263000000000005,29.535 -2020-08-12 00:30:00,80.76,17.951,41.263000000000005,29.535 -2020-08-12 00:45:00,81.39,18.18,41.263000000000005,29.535 -2020-08-12 01:00:00,78.93,18.011,38.448,29.535 -2020-08-12 01:15:00,81.15,17.312,38.448,29.535 -2020-08-12 01:30:00,80.17,16.162,38.448,29.535 -2020-08-12 01:45:00,77.33,16.117,38.448,29.535 -2020-08-12 02:00:00,73.77,16.063,36.471,29.535 -2020-08-12 02:15:00,73.65,15.169,36.471,29.535 -2020-08-12 02:30:00,80.17,16.605,36.471,29.535 -2020-08-12 02:45:00,80.73,17.266,36.471,29.535 -2020-08-12 03:00:00,81.84,17.944000000000003,36.042,29.535 -2020-08-12 03:15:00,75.9,17.819000000000003,36.042,29.535 -2020-08-12 03:30:00,82.63,17.381,36.042,29.535 -2020-08-12 03:45:00,85.64,16.847,36.042,29.535 -2020-08-12 04:00:00,87.96,20.533,36.705,29.535 -2020-08-12 04:15:00,83.3,25.464000000000002,36.705,29.535 -2020-08-12 04:30:00,94.27,23.048000000000002,36.705,29.535 -2020-08-12 04:45:00,96.6,23.026,36.705,29.535 -2020-08-12 05:00:00,101.67,33.89,39.716,29.535 -2020-08-12 05:15:00,102.94,39.406,39.716,29.535 -2020-08-12 05:30:00,106.43,35.455,39.716,29.535 -2020-08-12 05:45:00,111.7,33.123000000000005,39.716,29.535 -2020-08-12 06:00:00,115.63,33.095,52.756,29.535 -2020-08-12 06:15:00,114.21,32.867,52.756,29.535 -2020-08-12 06:30:00,115.31,32.718,52.756,29.535 -2020-08-12 06:45:00,119.39,35.351,52.756,29.535 -2020-08-12 07:00:00,120.77,35.128,65.977,29.535 -2020-08-12 07:15:00,115.6,36.236999999999995,65.977,29.535 -2020-08-12 07:30:00,114.05,34.679,65.977,29.535 -2020-08-12 07:45:00,117.2,35.479,65.977,29.535 -2020-08-12 08:00:00,118.01,31.593000000000004,57.927,29.535 -2020-08-12 08:15:00,113.31,34.262,57.927,29.535 -2020-08-12 08:30:00,111.75,35.255,57.927,29.535 -2020-08-12 08:45:00,110.43,37.361999999999995,57.927,29.535 -2020-08-12 09:00:00,116.17,33.135,54.86,29.535 -2020-08-12 09:15:00,116.01,32.508,54.86,29.535 -2020-08-12 09:30:00,116.37,35.607,54.86,29.535 -2020-08-12 09:45:00,112.57,38.335,54.86,29.535 -2020-08-12 10:00:00,113.53,36.125,52.818000000000005,29.535 -2020-08-12 10:15:00,118.87,37.175,52.818000000000005,29.535 -2020-08-12 10:30:00,114.46,36.969,52.818000000000005,29.535 -2020-08-12 10:45:00,110.49,37.849000000000004,52.818000000000005,29.535 -2020-08-12 11:00:00,116.41,35.904,52.937,29.535 -2020-08-12 11:15:00,112.99,36.979,52.937,29.535 -2020-08-12 11:30:00,106.78,37.882,52.937,29.535 -2020-08-12 11:45:00,114.37,38.647,52.937,29.535 -2020-08-12 12:00:00,116.44,34.079,50.826,29.535 -2020-08-12 12:15:00,114.58,33.338,50.826,29.535 -2020-08-12 12:30:00,119.4,32.302,50.826,29.535 -2020-08-12 12:45:00,114.81,33.016999999999996,50.826,29.535 -2020-08-12 13:00:00,111.49,32.529,50.556000000000004,29.535 -2020-08-12 13:15:00,104.65,33.516999999999996,50.556000000000004,29.535 -2020-08-12 13:30:00,100.7,31.879,50.556000000000004,29.535 -2020-08-12 13:45:00,102.39,32.037,50.556000000000004,29.535 -2020-08-12 14:00:00,103.75,33.325,51.188,29.535 -2020-08-12 14:15:00,103.76,32.522,51.188,29.535 -2020-08-12 14:30:00,107.42,31.695999999999998,51.188,29.535 -2020-08-12 14:45:00,105.01,32.378,51.188,29.535 -2020-08-12 15:00:00,100.4,33.842,52.976000000000006,29.535 -2020-08-12 15:15:00,103.59,31.701999999999998,52.976000000000006,29.535 -2020-08-12 15:30:00,101.61,30.491,52.976000000000006,29.535 -2020-08-12 15:45:00,103.92,28.971999999999998,52.976000000000006,29.535 -2020-08-12 16:00:00,102.77,31.217,55.463,29.535 -2020-08-12 16:15:00,107.41,31.173000000000002,55.463,29.535 -2020-08-12 16:30:00,108.87,30.682,55.463,29.535 -2020-08-12 16:45:00,109.71,28.494,55.463,29.535 -2020-08-12 17:00:00,108.65,31.62,59.435,29.535 -2020-08-12 17:15:00,108.44,32.32,59.435,29.535 -2020-08-12 17:30:00,108.62,31.518,59.435,29.535 -2020-08-12 17:45:00,109.31,31.159000000000002,59.435,29.535 -2020-08-12 18:00:00,109.78,34.02,61.387,29.535 -2020-08-12 18:15:00,107.43,33.876,61.387,29.535 -2020-08-12 18:30:00,107.41,32.295,61.387,29.535 -2020-08-12 18:45:00,106.93,34.85,61.387,29.535 -2020-08-12 19:00:00,105.24,36.139,63.323,29.535 -2020-08-12 19:15:00,101.29,35.138000000000005,63.323,29.535 -2020-08-12 19:30:00,101.31,34.048,63.323,29.535 -2020-08-12 19:45:00,105.17,33.194,63.323,29.535 -2020-08-12 20:00:00,102.58,30.789,69.083,29.535 -2020-08-12 20:15:00,100.04,30.183000000000003,69.083,29.535 -2020-08-12 20:30:00,99.46,30.362,69.083,29.535 -2020-08-12 20:45:00,97.18,30.468000000000004,69.083,29.535 -2020-08-12 21:00:00,93.2,29.343000000000004,59.957,29.535 -2020-08-12 21:15:00,90.83,30.383000000000003,59.957,29.535 -2020-08-12 21:30:00,86.78,30.683000000000003,59.957,29.535 -2020-08-12 21:45:00,87.17,31.006,59.957,29.535 -2020-08-12 22:00:00,81.89,28.338,53.821000000000005,29.535 -2020-08-12 22:15:00,83.57,30.643,53.821000000000005,29.535 -2020-08-12 22:30:00,80.19,26.136999999999997,53.821000000000005,29.535 -2020-08-12 22:45:00,79.2,23.215,53.821000000000005,29.535 -2020-08-12 23:00:00,76.03,20.941,45.458,29.535 -2020-08-12 23:15:00,75.7,19.805,45.458,29.535 -2020-08-12 23:30:00,76.14,19.178,45.458,29.535 -2020-08-12 23:45:00,76.61,18.267,45.458,29.535 -2020-08-13 00:00:00,72.42,18.349,40.36,29.535 -2020-08-13 00:15:00,73.49,19.119,40.36,29.535 -2020-08-13 00:30:00,73.4,18.072,40.36,29.535 -2020-08-13 00:45:00,73.58,18.308,40.36,29.535 -2020-08-13 01:00:00,73.1,18.123,38.552,29.535 -2020-08-13 01:15:00,73.24,17.432000000000002,38.552,29.535 -2020-08-13 01:30:00,72.48,16.293,38.552,29.535 -2020-08-13 01:45:00,73.22,16.247,38.552,29.535 -2020-08-13 02:00:00,71.87,16.195999999999998,36.895,29.535 -2020-08-13 02:15:00,75.03,15.322000000000001,36.895,29.535 -2020-08-13 02:30:00,78.27,16.736,36.895,29.535 -2020-08-13 02:45:00,80.01,17.394000000000002,36.895,29.535 -2020-08-13 03:00:00,77.38,18.062,36.565,29.535 -2020-08-13 03:15:00,72.6,17.952,36.565,29.535 -2020-08-13 03:30:00,74.33,17.521,36.565,29.535 -2020-08-13 03:45:00,79.52,16.986,36.565,29.535 -2020-08-13 04:00:00,85.0,20.684,37.263000000000005,29.535 -2020-08-13 04:15:00,91.08,25.62,37.263000000000005,29.535 -2020-08-13 04:30:00,94.95,23.21,37.263000000000005,29.535 -2020-08-13 04:45:00,93.59,23.19,37.263000000000005,29.535 -2020-08-13 05:00:00,98.02,34.096,40.412,29.535 -2020-08-13 05:15:00,100.02,39.655,40.412,29.535 -2020-08-13 05:30:00,106.59,35.711,40.412,29.535 -2020-08-13 05:45:00,112.6,33.354,40.412,29.535 -2020-08-13 06:00:00,116.48,33.299,49.825,29.535 -2020-08-13 06:15:00,113.4,33.088,49.825,29.535 -2020-08-13 06:30:00,111.91,32.939,49.825,29.535 -2020-08-13 06:45:00,111.96,35.58,49.825,29.535 -2020-08-13 07:00:00,114.86,35.355,61.082,29.535 -2020-08-13 07:15:00,116.59,36.48,61.082,29.535 -2020-08-13 07:30:00,119.63,34.942,61.082,29.535 -2020-08-13 07:45:00,118.33,35.748000000000005,61.082,29.535 -2020-08-13 08:00:00,111.91,31.864,53.961999999999996,29.535 -2020-08-13 08:15:00,110.29,34.508,53.961999999999996,29.535 -2020-08-13 08:30:00,109.63,35.491,53.961999999999996,29.535 -2020-08-13 08:45:00,114.21,37.586,53.961999999999996,29.535 -2020-08-13 09:00:00,116.43,33.364000000000004,50.06100000000001,29.535 -2020-08-13 09:15:00,117.8,32.728,50.06100000000001,29.535 -2020-08-13 09:30:00,114.9,35.812,50.06100000000001,29.535 -2020-08-13 09:45:00,115.89,38.521,50.06100000000001,29.535 -2020-08-13 10:00:00,115.79,36.312,47.68,29.535 -2020-08-13 10:15:00,109.66,37.342,47.68,29.535 -2020-08-13 10:30:00,115.53,37.13,47.68,29.535 -2020-08-13 10:45:00,114.31,38.004,47.68,29.535 -2020-08-13 11:00:00,112.62,36.067,45.93899999999999,29.535 -2020-08-13 11:15:00,112.44,37.135999999999996,45.93899999999999,29.535 -2020-08-13 11:30:00,112.77,38.033,45.93899999999999,29.535 -2020-08-13 11:45:00,109.6,38.785,45.93899999999999,29.535 -2020-08-13 12:00:00,110.46,34.21,43.648999999999994,29.535 -2020-08-13 12:15:00,110.53,33.461,43.648999999999994,29.535 -2020-08-13 12:30:00,110.63,32.435,43.648999999999994,29.535 -2020-08-13 12:45:00,107.32,33.141,43.648999999999994,29.535 -2020-08-13 13:00:00,111.0,32.635,42.801,29.535 -2020-08-13 13:15:00,110.62,33.609,42.801,29.535 -2020-08-13 13:30:00,110.8,31.969,42.801,29.535 -2020-08-13 13:45:00,106.12,32.137,42.801,29.535 -2020-08-13 14:00:00,106.62,33.408,43.24,29.535 -2020-08-13 14:15:00,110.75,32.61,43.24,29.535 -2020-08-13 14:30:00,108.44,31.795,43.24,29.535 -2020-08-13 14:45:00,108.43,32.482,43.24,29.535 -2020-08-13 15:00:00,100.47,33.91,45.04600000000001,29.535 -2020-08-13 15:15:00,100.61,31.774,45.04600000000001,29.535 -2020-08-13 15:30:00,104.49,30.573,45.04600000000001,29.535 -2020-08-13 15:45:00,105.82,29.055999999999997,45.04600000000001,29.535 -2020-08-13 16:00:00,111.96,31.281999999999996,46.568000000000005,29.535 -2020-08-13 16:15:00,112.43,31.245,46.568000000000005,29.535 -2020-08-13 16:30:00,113.32,30.761,46.568000000000005,29.535 -2020-08-13 16:45:00,111.17,28.608,46.568000000000005,29.535 -2020-08-13 17:00:00,120.16,31.709,50.618,29.535 -2020-08-13 17:15:00,119.4,32.437,50.618,29.535 -2020-08-13 17:30:00,121.21,31.648000000000003,50.618,29.535 -2020-08-13 17:45:00,113.4,31.320999999999998,50.618,29.535 -2020-08-13 18:00:00,115.18,34.176,52.806999999999995,29.535 -2020-08-13 18:15:00,116.91,34.041,52.806999999999995,29.535 -2020-08-13 18:30:00,119.58,32.469,52.806999999999995,29.535 -2020-08-13 18:45:00,114.31,35.025,52.806999999999995,29.535 -2020-08-13 19:00:00,107.96,36.318000000000005,53.464,29.535 -2020-08-13 19:15:00,104.43,35.316,53.464,29.535 -2020-08-13 19:30:00,104.41,34.227,53.464,29.535 -2020-08-13 19:45:00,110.75,33.378,53.464,29.535 -2020-08-13 20:00:00,110.35,30.975,56.753,29.535 -2020-08-13 20:15:00,104.81,30.371,56.753,29.535 -2020-08-13 20:30:00,100.88,30.537,56.753,29.535 -2020-08-13 20:45:00,98.91,30.616,56.753,29.535 -2020-08-13 21:00:00,94.78,29.493000000000002,52.506,29.535 -2020-08-13 21:15:00,93.57,30.526,52.506,29.535 -2020-08-13 21:30:00,89.55,30.82,52.506,29.535 -2020-08-13 21:45:00,89.48,31.116999999999997,52.506,29.535 -2020-08-13 22:00:00,84.47,28.432,48.163000000000004,29.535 -2020-08-13 22:15:00,85.66,30.724,48.163000000000004,29.535 -2020-08-13 22:30:00,83.52,26.188000000000002,48.163000000000004,29.535 -2020-08-13 22:45:00,82.68,23.265,48.163000000000004,29.535 -2020-08-13 23:00:00,78.63,21.028000000000002,42.379,29.535 -2020-08-13 23:15:00,79.67,19.885,42.379,29.535 -2020-08-13 23:30:00,78.45,19.271,42.379,29.535 -2020-08-13 23:45:00,79.55,18.363,42.379,29.535 -2020-08-14 00:00:00,77.38,16.802,38.505,29.535 -2020-08-14 00:15:00,77.2,17.767,38.505,29.535 -2020-08-14 00:30:00,76.42,16.995,38.505,29.535 -2020-08-14 00:45:00,77.01,17.641,38.505,29.535 -2020-08-14 01:00:00,75.04,17.09,37.004,29.535 -2020-08-14 01:15:00,76.59,15.772,37.004,29.535 -2020-08-14 01:30:00,76.72,15.347000000000001,37.004,29.535 -2020-08-14 01:45:00,76.98,15.050999999999998,37.004,29.535 -2020-08-14 02:00:00,75.7,15.899000000000001,36.098,29.535 -2020-08-14 02:15:00,76.98,15.003,36.098,29.535 -2020-08-14 02:30:00,76.36,17.160999999999998,36.098,29.535 -2020-08-14 02:45:00,77.05,17.155,36.098,29.535 -2020-08-14 03:00:00,76.85,18.619,36.561,29.535 -2020-08-14 03:15:00,76.89,17.305,36.561,29.535 -2020-08-14 03:30:00,77.81,16.651,36.561,29.535 -2020-08-14 03:45:00,81.89,16.94,36.561,29.535 -2020-08-14 04:00:00,84.07,20.77,37.355,29.535 -2020-08-14 04:15:00,91.01,24.226,37.355,29.535 -2020-08-14 04:30:00,96.53,22.723000000000003,37.355,29.535 -2020-08-14 04:45:00,98.47,22.149,37.355,29.535 -2020-08-14 05:00:00,97.75,32.713,40.285,29.535 -2020-08-14 05:15:00,106.42,39.152,40.285,29.535 -2020-08-14 05:30:00,111.47,35.349000000000004,40.285,29.535 -2020-08-14 05:45:00,114.34,32.543,40.285,29.535 -2020-08-14 06:00:00,117.13,32.652,52.378,29.535 -2020-08-14 06:15:00,115.21,32.692,52.378,29.535 -2020-08-14 06:30:00,119.17,32.536,52.378,29.535 -2020-08-14 06:45:00,121.31,34.979,52.378,29.535 -2020-08-14 07:00:00,122.16,35.415,60.891999999999996,29.535 -2020-08-14 07:15:00,114.97,37.369,60.891999999999996,29.535 -2020-08-14 07:30:00,119.05,33.976,60.891999999999996,29.535 -2020-08-14 07:45:00,119.76,34.639,60.891999999999996,29.535 -2020-08-14 08:00:00,121.92,31.587,53.652,29.535 -2020-08-14 08:15:00,117.57,34.885,53.652,29.535 -2020-08-14 08:30:00,116.45,35.696,53.652,29.535 -2020-08-14 08:45:00,119.87,37.746,53.652,29.535 -2020-08-14 09:00:00,122.22,31.248,51.456,29.535 -2020-08-14 09:15:00,121.31,32.442,51.456,29.535 -2020-08-14 09:30:00,117.74,34.867,51.456,29.535 -2020-08-14 09:45:00,114.72,37.943000000000005,51.456,29.535 -2020-08-14 10:00:00,116.88,35.665,49.4,29.535 -2020-08-14 10:15:00,122.22,36.4,49.4,29.535 -2020-08-14 10:30:00,123.37,36.731,49.4,29.535 -2020-08-14 10:45:00,118.57,37.541,49.4,29.535 -2020-08-14 11:00:00,116.16,35.859,48.773,29.535 -2020-08-14 11:15:00,114.64,35.923,48.773,29.535 -2020-08-14 11:30:00,114.36,36.323,48.773,29.535 -2020-08-14 11:45:00,109.72,36.116,48.773,29.535 -2020-08-14 12:00:00,109.46,31.871,46.033,29.535 -2020-08-14 12:15:00,106.86,30.65,46.033,29.535 -2020-08-14 12:30:00,104.86,29.738000000000003,46.033,29.535 -2020-08-14 12:45:00,104.8,29.616,46.033,29.535 -2020-08-14 13:00:00,101.33,29.599,44.38399999999999,29.535 -2020-08-14 13:15:00,101.51,30.674,44.38399999999999,29.535 -2020-08-14 13:30:00,101.84,29.811999999999998,44.38399999999999,29.535 -2020-08-14 13:45:00,104.46,30.298000000000002,44.38399999999999,29.535 -2020-08-14 14:00:00,98.79,30.854,43.162,29.535 -2020-08-14 14:15:00,100.52,30.504,43.162,29.535 -2020-08-14 14:30:00,104.56,31.139,43.162,29.535 -2020-08-14 14:45:00,107.41,31.128,43.162,29.535 -2020-08-14 15:00:00,107.75,32.481,44.91,29.535 -2020-08-14 15:15:00,107.7,30.148000000000003,44.91,29.535 -2020-08-14 15:30:00,103.14,28.518,44.91,29.535 -2020-08-14 15:45:00,104.08,27.74,44.91,29.535 -2020-08-14 16:00:00,108.58,29.195,47.489,29.535 -2020-08-14 16:15:00,109.61,29.625999999999998,47.489,29.535 -2020-08-14 16:30:00,105.67,28.965999999999998,47.489,29.535 -2020-08-14 16:45:00,106.48,26.09,47.489,29.535 -2020-08-14 17:00:00,106.27,30.831999999999997,52.047,29.535 -2020-08-14 17:15:00,112.92,31.45,52.047,29.535 -2020-08-14 17:30:00,117.17,30.857,52.047,29.535 -2020-08-14 17:45:00,114.87,30.416999999999998,52.047,29.535 -2020-08-14 18:00:00,113.07,33.241,53.306000000000004,29.535 -2020-08-14 18:15:00,108.05,32.227,53.306000000000004,29.535 -2020-08-14 18:30:00,110.51,30.538,53.306000000000004,29.535 -2020-08-14 18:45:00,114.14,33.5,53.306000000000004,29.535 -2020-08-14 19:00:00,111.52,35.571,53.516000000000005,29.535 -2020-08-14 19:15:00,103.77,35.055,53.516000000000005,29.535 -2020-08-14 19:30:00,100.59,34.052,53.516000000000005,29.535 -2020-08-14 19:45:00,102.62,32.256,53.516000000000005,29.535 -2020-08-14 20:00:00,101.7,29.701999999999998,57.88,29.535 -2020-08-14 20:15:00,100.14,29.865,57.88,29.535 -2020-08-14 20:30:00,104.22,29.55,57.88,29.535 -2020-08-14 20:45:00,102.33,28.785999999999998,57.88,29.535 -2020-08-14 21:00:00,93.47,28.896,53.32,29.535 -2020-08-14 21:15:00,89.73,31.496,53.32,29.535 -2020-08-14 21:30:00,84.2,31.639,53.32,29.535 -2020-08-14 21:45:00,90.61,32.071999999999996,53.32,29.535 -2020-08-14 22:00:00,87.69,29.177,48.074,29.535 -2020-08-14 22:15:00,85.9,31.226999999999997,48.074,29.535 -2020-08-14 22:30:00,80.19,30.853,48.074,29.535 -2020-08-14 22:45:00,79.07,28.558000000000003,48.074,29.535 -2020-08-14 23:00:00,72.75,27.998,41.306999999999995,29.535 -2020-08-14 23:15:00,73.35,25.458000000000002,41.306999999999995,29.535 -2020-08-14 23:30:00,77.1,23.195,41.306999999999995,29.535 -2020-08-14 23:45:00,80.37,22.21,41.306999999999995,29.535 -2020-08-15 00:00:00,78.66,18.289,40.227,29.423000000000002 -2020-08-15 00:15:00,78.01,18.867,40.227,29.423000000000002 -2020-08-15 00:30:00,77.59,17.567,40.227,29.423000000000002 -2020-08-15 00:45:00,78.28,17.474,40.227,29.423000000000002 -2020-08-15 01:00:00,75.62,17.129,36.303000000000004,29.423000000000002 -2020-08-15 01:15:00,73.05,16.448,36.303000000000004,29.423000000000002 -2020-08-15 01:30:00,72.17,15.292,36.303000000000004,29.423000000000002 -2020-08-15 01:45:00,76.06,16.205,36.303000000000004,29.423000000000002 -2020-08-15 02:00:00,74.7,16.063,33.849000000000004,29.423000000000002 -2020-08-15 02:15:00,74.19,14.452,33.849000000000004,29.423000000000002 -2020-08-15 02:30:00,72.6,15.894,33.849000000000004,29.423000000000002 -2020-08-15 02:45:00,74.73,16.635,33.849000000000004,29.423000000000002 -2020-08-15 03:00:00,74.32,16.73,33.149,29.423000000000002 -2020-08-15 03:15:00,71.83,14.837,33.149,29.423000000000002 -2020-08-15 03:30:00,66.95,14.600999999999999,33.149,29.423000000000002 -2020-08-15 03:45:00,67.51,16.303,33.149,29.423000000000002 -2020-08-15 04:00:00,68.28,18.586,32.501,29.423000000000002 -2020-08-15 04:15:00,68.18,21.346999999999998,32.501,29.423000000000002 -2020-08-15 04:30:00,67.99,18.38,32.501,29.423000000000002 -2020-08-15 04:45:00,69.92,18.101,32.501,29.423000000000002 -2020-08-15 05:00:00,68.61,21.697,31.648000000000003,29.423000000000002 -2020-08-15 05:15:00,71.82,18.840999999999998,31.648000000000003,29.423000000000002 -2020-08-15 05:30:00,69.58,16.473,31.648000000000003,29.423000000000002 -2020-08-15 05:45:00,70.22,17.582,31.648000000000003,29.423000000000002 -2020-08-15 06:00:00,73.17,27.255,32.552,29.423000000000002 -2020-08-15 06:15:00,75.29,33.784,32.552,29.423000000000002 -2020-08-15 06:30:00,78.84,31.236,32.552,29.423000000000002 -2020-08-15 06:45:00,86.96,31.153000000000002,32.552,29.423000000000002 -2020-08-15 07:00:00,88.59,30.756,35.181999999999995,29.423000000000002 -2020-08-15 07:15:00,88.61,31.564,35.181999999999995,29.423000000000002 -2020-08-15 07:30:00,88.1,29.455,35.181999999999995,29.423000000000002 -2020-08-15 07:45:00,94.77,30.729,35.181999999999995,29.423000000000002 -2020-08-15 08:00:00,96.75,28.160999999999998,40.35,29.423000000000002 -2020-08-15 08:15:00,93.18,30.936999999999998,40.35,29.423000000000002 -2020-08-15 08:30:00,93.98,31.596,40.35,29.423000000000002 -2020-08-15 08:45:00,95.48,34.302,40.35,29.423000000000002 -2020-08-15 09:00:00,93.38,30.744,42.292,29.423000000000002 -2020-08-15 09:15:00,91.59,32.339,42.292,29.423000000000002 -2020-08-15 09:30:00,92.73,35.157,42.292,29.423000000000002 -2020-08-15 09:45:00,96.95,37.759,42.292,29.423000000000002 -2020-08-15 10:00:00,100.16,36.094,40.084,29.423000000000002 -2020-08-15 10:15:00,101.04,37.143,40.084,29.423000000000002 -2020-08-15 10:30:00,99.67,37.117,40.084,29.423000000000002 -2020-08-15 10:45:00,99.38,37.459,40.084,29.423000000000002 -2020-08-15 11:00:00,95.9,35.616,36.966,29.423000000000002 -2020-08-15 11:15:00,93.51,36.58,36.966,29.423000000000002 -2020-08-15 11:30:00,89.93,37.348,36.966,29.423000000000002 -2020-08-15 11:45:00,86.8,37.865,36.966,29.423000000000002 -2020-08-15 12:00:00,82.81,34.096,35.19,29.423000000000002 -2020-08-15 12:15:00,81.87,33.643,35.19,29.423000000000002 -2020-08-15 12:30:00,80.39,32.586999999999996,35.19,29.423000000000002 -2020-08-15 12:45:00,72.88,33.234,35.19,29.423000000000002 -2020-08-15 13:00:00,68.23,32.400999999999996,32.277,29.423000000000002 -2020-08-15 13:15:00,68.49,33.111999999999995,32.277,29.423000000000002 -2020-08-15 13:30:00,72.94,32.469,32.277,29.423000000000002 -2020-08-15 13:45:00,73.41,31.604,32.277,29.423000000000002 -2020-08-15 14:00:00,67.3,32.065,31.436999999999998,29.423000000000002 -2020-08-15 14:15:00,71.1,30.541999999999998,31.436999999999998,29.423000000000002 -2020-08-15 14:30:00,67.23,30.926,31.436999999999998,29.423000000000002 -2020-08-15 14:45:00,66.15,31.37,31.436999999999998,29.423000000000002 -2020-08-15 15:00:00,64.93,33.033,33.493,29.423000000000002 -2020-08-15 15:15:00,67.48,31.328000000000003,33.493,29.423000000000002 -2020-08-15 15:30:00,66.52,29.779,33.493,29.423000000000002 -2020-08-15 15:45:00,68.11,28.105999999999998,33.493,29.423000000000002 -2020-08-15 16:00:00,71.24,31.679000000000002,36.593,29.423000000000002 -2020-08-15 16:15:00,71.7,31.18,36.593,29.423000000000002 -2020-08-15 16:30:00,73.28,30.749000000000002,36.593,29.423000000000002 -2020-08-15 16:45:00,76.09,27.814,36.593,29.423000000000002 -2020-08-15 17:00:00,77.79,31.39,42.049,29.423000000000002 -2020-08-15 17:15:00,78.28,29.826,42.049,29.423000000000002 -2020-08-15 17:30:00,80.48,29.125999999999998,42.049,29.423000000000002 -2020-08-15 17:45:00,81.56,29.226,42.049,29.423000000000002 -2020-08-15 18:00:00,82.67,33.369,43.755,29.423000000000002 -2020-08-15 18:15:00,81.18,33.85,43.755,29.423000000000002 -2020-08-15 18:30:00,80.1,33.385,43.755,29.423000000000002 -2020-08-15 18:45:00,80.79,33.236,43.755,29.423000000000002 -2020-08-15 19:00:00,78.86,33.652,44.492,29.423000000000002 -2020-08-15 19:15:00,76.55,32.202,44.492,29.423000000000002 -2020-08-15 19:30:00,77.59,31.889,44.492,29.423000000000002 -2020-08-15 19:45:00,80.08,31.791,44.492,29.423000000000002 -2020-08-15 20:00:00,79.74,29.905,40.896,29.423000000000002 -2020-08-15 20:15:00,80.99,29.331,40.896,29.423000000000002 -2020-08-15 20:30:00,76.08,28.186999999999998,40.896,29.423000000000002 -2020-08-15 20:45:00,75.31,29.188000000000002,40.896,29.423000000000002 -2020-08-15 21:00:00,72.65,27.752,39.056,29.423000000000002 -2020-08-15 21:15:00,72.67,29.988000000000003,39.056,29.423000000000002 -2020-08-15 21:30:00,70.17,30.236,39.056,29.423000000000002 -2020-08-15 21:45:00,69.86,30.145,39.056,29.423000000000002 -2020-08-15 22:00:00,66.08,27.138,38.478,29.423000000000002 -2020-08-15 22:15:00,67.02,29.274,38.478,29.423000000000002 -2020-08-15 22:30:00,64.4,28.171999999999997,38.478,29.423000000000002 -2020-08-15 22:45:00,64.41,26.127,38.478,29.423000000000002 -2020-08-15 23:00:00,59.88,24.873,32.953,29.423000000000002 -2020-08-15 23:15:00,61.24,22.807,32.953,29.423000000000002 -2020-08-15 23:30:00,60.61,22.965,32.953,29.423000000000002 -2020-08-15 23:45:00,59.27,22.448,32.953,29.423000000000002 -2020-08-16 00:00:00,56.4,20.963,28.584,29.423000000000002 -2020-08-16 00:15:00,57.15,20.25,28.584,29.423000000000002 -2020-08-16 00:30:00,56.4,19.059,28.584,29.423000000000002 -2020-08-16 00:45:00,56.75,18.803,28.584,29.423000000000002 -2020-08-16 01:00:00,54.08,18.761,26.419,29.423000000000002 -2020-08-16 01:15:00,55.73,17.887999999999998,26.419,29.423000000000002 -2020-08-16 01:30:00,54.82,16.56,26.419,29.423000000000002 -2020-08-16 01:45:00,55.19,16.852,26.419,29.423000000000002 -2020-08-16 02:00:00,53.79,16.772000000000002,25.335,29.423000000000002 -2020-08-16 02:15:00,54.64,15.895999999999999,25.335,29.423000000000002 -2020-08-16 02:30:00,54.21,17.325,25.335,29.423000000000002 -2020-08-16 02:45:00,53.98,17.839000000000002,25.335,29.423000000000002 -2020-08-16 03:00:00,53.46,18.679000000000002,24.805,29.423000000000002 -2020-08-16 03:15:00,54.06,16.922,24.805,29.423000000000002 -2020-08-16 03:30:00,54.48,15.995999999999999,24.805,29.423000000000002 -2020-08-16 03:45:00,55.19,17.086,24.805,29.423000000000002 -2020-08-16 04:00:00,55.99,19.613,25.772,29.423000000000002 -2020-08-16 04:15:00,57.26,22.346,25.772,29.423000000000002 -2020-08-16 04:30:00,54.82,20.494,25.772,29.423000000000002 -2020-08-16 04:45:00,56.06,19.851,25.772,29.423000000000002 -2020-08-16 05:00:00,55.21,23.886,25.971999999999998,29.423000000000002 -2020-08-16 05:15:00,55.74,20.704,25.971999999999998,29.423000000000002 -2020-08-16 05:30:00,55.42,17.433,25.971999999999998,29.423000000000002 -2020-08-16 05:45:00,56.8,18.361,25.971999999999998,29.423000000000002 -2020-08-16 06:00:00,55.15,26.846999999999998,26.026,29.423000000000002 -2020-08-16 06:15:00,58.89,34.753,26.026,29.423000000000002 -2020-08-16 06:30:00,59.5,31.089000000000002,26.026,29.423000000000002 -2020-08-16 06:45:00,60.53,29.549,26.026,29.423000000000002 -2020-08-16 07:00:00,61.9,29.266,27.396,29.423000000000002 -2020-08-16 07:15:00,61.93,28.503,27.396,29.423000000000002 -2020-08-16 07:30:00,61.72,27.575,27.396,29.423000000000002 -2020-08-16 07:45:00,59.55,28.915,27.396,29.423000000000002 -2020-08-16 08:00:00,62.74,27.895,30.791999999999998,29.423000000000002 -2020-08-16 08:15:00,61.33,31.752,30.791999999999998,29.423000000000002 -2020-08-16 08:30:00,60.91,32.825,30.791999999999998,29.423000000000002 -2020-08-16 08:45:00,60.73,35.29,30.791999999999998,29.423000000000002 -2020-08-16 09:00:00,60.52,30.888,32.482,29.423000000000002 -2020-08-16 09:15:00,61.23,31.75,32.482,29.423000000000002 -2020-08-16 09:30:00,61.34,34.828,32.482,29.423000000000002 -2020-08-16 09:45:00,61.67,38.303000000000004,32.482,29.423000000000002 -2020-08-16 10:00:00,59.03,35.681,31.951,29.423000000000002 -2020-08-16 10:15:00,63.77,36.964,31.951,29.423000000000002 -2020-08-16 10:30:00,64.3,37.236,31.951,29.423000000000002 -2020-08-16 10:45:00,64.55,39.031,31.951,29.423000000000002 -2020-08-16 11:00:00,62.19,37.029,33.619,29.423000000000002 -2020-08-16 11:15:00,61.81,37.51,33.619,29.423000000000002 -2020-08-16 11:30:00,60.1,38.786,33.619,29.423000000000002 -2020-08-16 11:45:00,59.33,39.66,33.619,29.423000000000002 -2020-08-16 12:00:00,56.59,37.803000000000004,30.975,29.423000000000002 -2020-08-16 12:15:00,56.52,36.787,30.975,29.423000000000002 -2020-08-16 12:30:00,56.09,36.008,30.975,29.423000000000002 -2020-08-16 12:45:00,54.79,36.227,30.975,29.423000000000002 -2020-08-16 13:00:00,51.28,35.288000000000004,27.956999999999997,29.423000000000002 -2020-08-16 13:15:00,55.09,35.214,27.956999999999997,29.423000000000002 -2020-08-16 13:30:00,52.21,33.672,27.956999999999997,29.423000000000002 -2020-08-16 13:45:00,54.84,33.584,27.956999999999997,29.423000000000002 -2020-08-16 14:00:00,55.31,35.247,25.555999999999997,29.423000000000002 -2020-08-16 14:15:00,56.62,34.028,25.555999999999997,29.423000000000002 -2020-08-16 14:30:00,54.58,32.937,25.555999999999997,29.423000000000002 -2020-08-16 14:45:00,56.86,32.461,25.555999999999997,29.423000000000002 -2020-08-16 15:00:00,58.29,34.168,26.271,29.423000000000002 -2020-08-16 15:15:00,58.87,31.699,26.271,29.423000000000002 -2020-08-16 15:30:00,54.96,29.926,26.271,29.423000000000002 -2020-08-16 15:45:00,59.3,28.335,26.271,29.423000000000002 -2020-08-16 16:00:00,63.28,30.392,30.369,29.423000000000002 -2020-08-16 16:15:00,64.29,30.148000000000003,30.369,29.423000000000002 -2020-08-16 16:30:00,66.51,30.853,30.369,29.423000000000002 -2020-08-16 16:45:00,70.7,27.897,30.369,29.423000000000002 -2020-08-16 17:00:00,74.15,31.488000000000003,38.787,29.423000000000002 -2020-08-16 17:15:00,75.05,31.449,38.787,29.423000000000002 -2020-08-16 17:30:00,76.55,31.503,38.787,29.423000000000002 -2020-08-16 17:45:00,77.73,31.592,38.787,29.423000000000002 -2020-08-16 18:00:00,80.1,35.961,41.886,29.423000000000002 -2020-08-16 18:15:00,79.15,35.951,41.886,29.423000000000002 -2020-08-16 18:30:00,78.57,35.23,41.886,29.423000000000002 -2020-08-16 18:45:00,78.39,35.485,41.886,29.423000000000002 -2020-08-16 19:00:00,77.11,37.847,42.91,29.423000000000002 -2020-08-16 19:15:00,78.29,35.399,42.91,29.423000000000002 -2020-08-16 19:30:00,80.1,34.857,42.91,29.423000000000002 -2020-08-16 19:45:00,82.78,34.594,42.91,29.423000000000002 -2020-08-16 20:00:00,82.35,32.407,42.148999999999994,29.423000000000002 -2020-08-16 20:15:00,81.74,32.199,42.148999999999994,29.423000000000002 -2020-08-16 20:30:00,81.4,31.858,42.148999999999994,29.423000000000002 -2020-08-16 20:45:00,81.03,31.601,42.148999999999994,29.423000000000002 -2020-08-16 21:00:00,79.1,29.82,40.955999999999996,29.423000000000002 -2020-08-16 21:15:00,80.28,32.325,40.955999999999996,29.423000000000002 -2020-08-16 21:30:00,78.42,32.105,40.955999999999996,29.423000000000002 -2020-08-16 21:45:00,77.85,32.394,40.955999999999996,29.423000000000002 -2020-08-16 22:00:00,73.71,31.552,39.873000000000005,29.423000000000002 -2020-08-16 22:15:00,74.13,31.956,39.873000000000005,29.423000000000002 -2020-08-16 22:30:00,70.68,30.46,39.873000000000005,29.423000000000002 -2020-08-16 22:45:00,73.48,27.304000000000002,39.873000000000005,29.423000000000002 -2020-08-16 23:00:00,66.63,25.803,35.510999999999996,29.423000000000002 -2020-08-16 23:15:00,68.8,24.945999999999998,35.510999999999996,29.423000000000002 -2020-08-16 23:30:00,67.36,24.513,35.510999999999996,29.423000000000002 -2020-08-16 23:45:00,68.31,24.014,35.510999999999996,29.423000000000002 -2020-08-17 00:00:00,67.08,22.375,33.475,29.535 -2020-08-17 00:15:00,66.94,22.315,33.475,29.535 -2020-08-17 00:30:00,66.43,20.779,33.475,29.535 -2020-08-17 00:45:00,66.96,20.184,33.475,29.535 -2020-08-17 01:00:00,64.35,20.474,33.111,29.535 -2020-08-17 01:15:00,65.92,19.635,33.111,29.535 -2020-08-17 01:30:00,65.15,18.643,33.111,29.535 -2020-08-17 01:45:00,64.74,18.845,33.111,29.535 -2020-08-17 02:00:00,65.98,19.177,32.358000000000004,29.535 -2020-08-17 02:15:00,72.77,17.374000000000002,32.358000000000004,29.535 -2020-08-17 02:30:00,73.53,18.923,32.358000000000004,29.535 -2020-08-17 02:45:00,69.28,19.314,32.358000000000004,29.535 -2020-08-17 03:00:00,66.89,20.573,30.779,29.535 -2020-08-17 03:15:00,67.8,19.445999999999998,30.779,29.535 -2020-08-17 03:30:00,71.43,19.149,30.779,29.535 -2020-08-17 03:45:00,74.75,19.83,30.779,29.535 -2020-08-17 04:00:00,80.78,25.086,31.416,29.535 -2020-08-17 04:15:00,87.43,30.389,31.416,29.535 -2020-08-17 04:30:00,90.67,28.061,31.416,29.535 -2020-08-17 04:45:00,90.09,27.750999999999998,31.416,29.535 -2020-08-17 05:00:00,93.62,38.103,37.221,29.535 -2020-08-17 05:15:00,96.83,43.934,37.221,29.535 -2020-08-17 05:30:00,105.37,39.284,37.221,29.535 -2020-08-17 05:45:00,108.9,37.507,37.221,29.535 -2020-08-17 06:00:00,113.83,36.976,51.891000000000005,29.535 -2020-08-17 06:15:00,111.93,36.961,51.891000000000005,29.535 -2020-08-17 06:30:00,116.33,36.743,51.891000000000005,29.535 -2020-08-17 06:45:00,116.15,39.816,51.891000000000005,29.535 -2020-08-17 07:00:00,119.7,39.711999999999996,62.282,29.535 -2020-08-17 07:15:00,114.94,40.998000000000005,62.282,29.535 -2020-08-17 07:30:00,113.97,39.330999999999996,62.282,29.535 -2020-08-17 07:45:00,116.01,40.996,62.282,29.535 -2020-08-17 08:00:00,116.0,38.075,54.102,29.535 -2020-08-17 08:15:00,113.19,40.864000000000004,54.102,29.535 -2020-08-17 08:30:00,108.1,41.148,54.102,29.535 -2020-08-17 08:45:00,111.95,43.861000000000004,54.102,29.535 -2020-08-17 09:00:00,116.11,38.529,50.917,29.535 -2020-08-17 09:15:00,113.89,37.845,50.917,29.535 -2020-08-17 09:30:00,111.12,40.088,50.917,29.535 -2020-08-17 09:45:00,106.99,41.456,50.917,29.535 -2020-08-17 10:00:00,109.37,39.195,49.718999999999994,29.535 -2020-08-17 10:15:00,108.32,40.332,49.718999999999994,29.535 -2020-08-17 10:30:00,115.64,40.201,49.718999999999994,29.535 -2020-08-17 10:45:00,113.32,40.523,49.718999999999994,29.535 -2020-08-17 11:00:00,109.23,38.788000000000004,49.833999999999996,29.535 -2020-08-17 11:15:00,106.15,39.465,49.833999999999996,29.535 -2020-08-17 11:30:00,106.16,41.331,49.833999999999996,29.535 -2020-08-17 11:45:00,112.52,42.663999999999994,49.833999999999996,29.535 -2020-08-17 12:00:00,113.63,39.105,47.832,29.535 -2020-08-17 12:15:00,106.33,38.181,47.832,29.535 -2020-08-17 12:30:00,107.11,36.42,47.832,29.535 -2020-08-17 12:45:00,107.04,36.57,47.832,29.535 -2020-08-17 13:00:00,103.37,36.479,48.03,29.535 -2020-08-17 13:15:00,102.48,35.629,48.03,29.535 -2020-08-17 13:30:00,104.21,34.259,48.03,29.535 -2020-08-17 13:45:00,102.22,35.001,48.03,29.535 -2020-08-17 14:00:00,103.51,35.816,48.157,29.535 -2020-08-17 14:15:00,99.38,35.17,48.157,29.535 -2020-08-17 14:30:00,99.26,33.96,48.157,29.535 -2020-08-17 14:45:00,99.3,35.389,48.157,29.535 -2020-08-17 15:00:00,102.77,36.656,48.897,29.535 -2020-08-17 15:15:00,101.97,33.701,48.897,29.535 -2020-08-17 15:30:00,103.47,32.678000000000004,48.897,29.535 -2020-08-17 15:45:00,104.76,30.66,48.897,29.535 -2020-08-17 16:00:00,106.74,33.74,51.446000000000005,29.535 -2020-08-17 16:15:00,109.18,33.586,51.446000000000005,29.535 -2020-08-17 16:30:00,111.89,33.715,51.446000000000005,29.535 -2020-08-17 16:45:00,110.39,30.851,51.446000000000005,29.535 -2020-08-17 17:00:00,109.89,33.382,57.507,29.535 -2020-08-17 17:15:00,108.72,33.742,57.507,29.535 -2020-08-17 17:30:00,109.15,33.471,57.507,29.535 -2020-08-17 17:45:00,108.54,33.272,57.507,29.535 -2020-08-17 18:00:00,108.29,36.677,57.896,29.535 -2020-08-17 18:15:00,107.32,35.067,57.896,29.535 -2020-08-17 18:30:00,107.79,33.650999999999996,57.896,29.535 -2020-08-17 18:45:00,106.79,36.765,57.896,29.535 -2020-08-17 19:00:00,113.59,38.936,57.891999999999996,29.535 -2020-08-17 19:15:00,107.67,37.709,57.891999999999996,29.535 -2020-08-17 19:30:00,110.58,36.824,57.891999999999996,29.535 -2020-08-17 19:45:00,109.62,36.025999999999996,57.891999999999996,29.535 -2020-08-17 20:00:00,102.56,32.74,64.57300000000001,29.535 -2020-08-17 20:15:00,100.21,33.866,64.57300000000001,29.535 -2020-08-17 20:30:00,98.27,34.029,64.57300000000001,29.535 -2020-08-17 20:45:00,99.3,33.9,64.57300000000001,29.535 -2020-08-17 21:00:00,95.28,31.535,59.431999999999995,29.535 -2020-08-17 21:15:00,96.5,34.459,59.431999999999995,29.535 -2020-08-17 21:30:00,93.09,34.631,59.431999999999995,29.535 -2020-08-17 21:45:00,92.08,34.704,59.431999999999995,29.535 -2020-08-17 22:00:00,89.32,32.049,51.519,29.535 -2020-08-17 22:15:00,87.02,34.274,51.519,29.535 -2020-08-17 22:30:00,82.96,29.033,51.519,29.535 -2020-08-17 22:45:00,87.26,25.968000000000004,51.519,29.535 -2020-08-17 23:00:00,83.17,24.506999999999998,44.501000000000005,29.535 -2020-08-17 23:15:00,84.93,22.086,44.501000000000005,29.535 -2020-08-17 23:30:00,79.26,21.566999999999997,44.501000000000005,29.535 -2020-08-17 23:45:00,75.42,20.524,44.501000000000005,29.535 -2020-08-18 00:00:00,80.51,20.454,44.522,29.535 -2020-08-18 00:15:00,81.43,21.15,44.522,29.535 -2020-08-18 00:30:00,80.49,20.289,44.522,29.535 -2020-08-18 00:45:00,75.85,20.444000000000003,44.522,29.535 -2020-08-18 01:00:00,72.88,20.229,41.441,29.535 -2020-08-18 01:15:00,73.3,19.511,41.441,29.535 -2020-08-18 01:30:00,75.87,18.41,41.441,29.535 -2020-08-18 01:45:00,80.87,18.136,41.441,29.535 -2020-08-18 02:00:00,81.98,18.032,40.203,29.535 -2020-08-18 02:15:00,79.19,17.307000000000002,40.203,29.535 -2020-08-18 02:30:00,76.72,18.492,40.203,29.535 -2020-08-18 02:45:00,82.03,19.179000000000002,40.203,29.535 -2020-08-18 03:00:00,82.91,19.949,39.536,29.535 -2020-08-18 03:15:00,82.37,19.711,39.536,29.535 -2020-08-18 03:30:00,78.01,19.379,39.536,29.535 -2020-08-18 03:45:00,83.18,19.037,39.536,29.535 -2020-08-18 04:00:00,90.43,23.16,40.759,29.535 -2020-08-18 04:15:00,92.1,28.493000000000002,40.759,29.535 -2020-08-18 04:30:00,89.48,26.066999999999997,40.759,29.535 -2020-08-18 04:45:00,92.67,26.177,40.759,29.535 -2020-08-18 05:00:00,97.83,37.921,43.623999999999995,29.535 -2020-08-18 05:15:00,105.65,44.367,43.623999999999995,29.535 -2020-08-18 05:30:00,111.86,39.913000000000004,43.623999999999995,29.535 -2020-08-18 05:45:00,115.65,37.446,43.623999999999995,29.535 -2020-08-18 06:00:00,116.29,37.865,52.684,29.535 -2020-08-18 06:15:00,117.43,38.018,52.684,29.535 -2020-08-18 06:30:00,123.82,37.52,52.684,29.535 -2020-08-18 06:45:00,129.27,39.798,52.684,29.535 -2020-08-18 07:00:00,135.91,39.836,62.676,29.535 -2020-08-18 07:15:00,126.32,40.907,62.676,29.535 -2020-08-18 07:30:00,127.7,39.289,62.676,29.535 -2020-08-18 07:45:00,125.98,40.064,62.676,29.535 -2020-08-18 08:00:00,127.08,37.076,56.161,29.535 -2020-08-18 08:15:00,131.88,39.455,56.161,29.535 -2020-08-18 08:30:00,134.91,39.891,56.161,29.535 -2020-08-18 08:45:00,131.78,41.751000000000005,56.161,29.535 -2020-08-18 09:00:00,132.31,36.759,52.132,29.535 -2020-08-18 09:15:00,134.14,35.897,52.132,29.535 -2020-08-18 09:30:00,140.01,38.738,52.132,29.535 -2020-08-18 09:45:00,140.34,41.32,52.132,29.535 -2020-08-18 10:00:00,137.81,37.816,51.032,29.535 -2020-08-18 10:15:00,133.5,38.835,51.032,29.535 -2020-08-18 10:30:00,134.66,38.716,51.032,29.535 -2020-08-18 10:45:00,131.74,39.949,51.032,29.535 -2020-08-18 11:00:00,125.16,38.181,51.085,29.535 -2020-08-18 11:15:00,124.46,39.236,51.085,29.535 -2020-08-18 11:30:00,124.13,40.074,51.085,29.535 -2020-08-18 11:45:00,115.02,40.953,51.085,29.535 -2020-08-18 12:00:00,109.2,37.262,49.049,29.535 -2020-08-18 12:15:00,108.88,36.66,49.049,29.535 -2020-08-18 12:30:00,112.98,35.681999999999995,49.049,29.535 -2020-08-18 12:45:00,115.32,36.479,49.049,29.535 -2020-08-18 13:00:00,108.39,36.041,49.722,29.535 -2020-08-18 13:15:00,111.75,36.867,49.722,29.535 -2020-08-18 13:30:00,111.55,35.42,49.722,29.535 -2020-08-18 13:45:00,107.18,35.269,49.722,29.535 -2020-08-18 14:00:00,114.92,36.473,49.565,29.535 -2020-08-18 14:15:00,117.53,35.641999999999996,49.565,29.535 -2020-08-18 14:30:00,117.41,34.726,49.565,29.535 -2020-08-18 14:45:00,113.97,35.425,49.565,29.535 -2020-08-18 15:00:00,104.47,36.525,51.108999999999995,29.535 -2020-08-18 15:15:00,107.86,34.426,51.108999999999995,29.535 -2020-08-18 15:30:00,116.75,33.21,51.108999999999995,29.535 -2020-08-18 15:45:00,114.65,31.535999999999998,51.108999999999995,29.535 -2020-08-18 16:00:00,112.4,33.915,52.725,29.535 -2020-08-18 16:15:00,114.87,33.833,52.725,29.535 -2020-08-18 16:30:00,114.02,33.6,52.725,29.535 -2020-08-18 16:45:00,116.97,31.426,52.725,29.535 -2020-08-18 17:00:00,113.73,34.107,58.031000000000006,29.535 -2020-08-18 17:15:00,110.05,34.88,58.031000000000006,29.535 -2020-08-18 17:30:00,118.5,34.19,58.031000000000006,29.535 -2020-08-18 17:45:00,119.33,33.718,58.031000000000006,29.535 -2020-08-18 18:00:00,122.4,36.251999999999995,58.338,29.535 -2020-08-18 18:15:00,114.26,36.035,58.338,29.535 -2020-08-18 18:30:00,115.94,34.396,58.338,29.535 -2020-08-18 18:45:00,118.08,37.306999999999995,58.338,29.535 -2020-08-18 19:00:00,107.79,38.418,58.464,29.535 -2020-08-18 19:15:00,104.16,37.361,58.464,29.535 -2020-08-18 19:30:00,111.36,36.297,58.464,29.535 -2020-08-18 19:45:00,113.13,35.806,58.464,29.535 -2020-08-18 20:00:00,106.38,32.879,63.708,29.535 -2020-08-18 20:15:00,103.17,32.701,63.708,29.535 -2020-08-18 20:30:00,98.53,32.858000000000004,63.708,29.535 -2020-08-18 20:45:00,97.08,33.101,63.708,29.535 -2020-08-18 21:00:00,89.74,31.555,57.06399999999999,29.535 -2020-08-18 21:15:00,91.57,33.103,57.06399999999999,29.535 -2020-08-18 21:30:00,86.58,33.452,57.06399999999999,29.535 -2020-08-18 21:45:00,86.69,33.669000000000004,57.06399999999999,29.535 -2020-08-18 22:00:00,80.92,31.107,52.831,29.535 -2020-08-18 22:15:00,81.51,33.018,52.831,29.535 -2020-08-18 22:30:00,80.13,28.011999999999997,52.831,29.535 -2020-08-18 22:45:00,79.13,24.936999999999998,52.831,29.535 -2020-08-18 23:00:00,72.87,22.809,44.717,29.535 -2020-08-18 23:15:00,76.22,21.831999999999997,44.717,29.535 -2020-08-18 23:30:00,77.08,21.331,44.717,29.535 -2020-08-18 23:45:00,78.95,20.47,44.717,29.535 -2020-08-19 00:00:00,75.55,20.588,41.263000000000005,29.535 -2020-08-19 00:15:00,74.25,21.284000000000002,41.263000000000005,29.535 -2020-08-19 00:30:00,73.14,20.428,41.263000000000005,29.535 -2020-08-19 00:45:00,73.85,20.59,41.263000000000005,29.535 -2020-08-19 01:00:00,72.97,20.358,38.448,29.535 -2020-08-19 01:15:00,75.56,19.652,38.448,29.535 -2020-08-19 01:30:00,70.9,18.563,38.448,29.535 -2020-08-19 01:45:00,73.95,18.288,38.448,29.535 -2020-08-19 02:00:00,73.83,18.187,36.471,29.535 -2020-08-19 02:15:00,75.11,17.482,36.471,29.535 -2020-08-19 02:30:00,74.21,18.646,36.471,29.535 -2020-08-19 02:45:00,74.47,19.331,36.471,29.535 -2020-08-19 03:00:00,76.06,20.089000000000002,36.042,29.535 -2020-08-19 03:15:00,77.07,19.866,36.042,29.535 -2020-08-19 03:30:00,74.69,19.54,36.042,29.535 -2020-08-19 03:45:00,84.15,19.194000000000003,36.042,29.535 -2020-08-19 04:00:00,90.54,23.335,36.705,29.535 -2020-08-19 04:15:00,91.52,28.679000000000002,36.705,29.535 -2020-08-19 04:30:00,88.4,26.26,36.705,29.535 -2020-08-19 04:45:00,91.16,26.372,36.705,29.535 -2020-08-19 05:00:00,95.33,38.171,39.716,29.535 -2020-08-19 05:15:00,104.66,44.675,39.716,29.535 -2020-08-19 05:30:00,106.17,40.223,39.716,29.535 -2020-08-19 05:45:00,113.8,37.723,39.716,29.535 -2020-08-19 06:00:00,118.61,38.113,52.756,29.535 -2020-08-19 06:15:00,117.99,38.286,52.756,29.535 -2020-08-19 06:30:00,114.51,37.786,52.756,29.535 -2020-08-19 06:45:00,111.03,40.067,52.756,29.535 -2020-08-19 07:00:00,120.0,40.104,65.977,29.535 -2020-08-19 07:15:00,121.82,41.191,65.977,29.535 -2020-08-19 07:30:00,118.1,39.595,65.977,29.535 -2020-08-19 07:45:00,117.26,40.374,65.977,29.535 -2020-08-19 08:00:00,114.13,37.389,57.927,29.535 -2020-08-19 08:15:00,117.26,39.741,57.927,29.535 -2020-08-19 08:30:00,120.28,40.171,57.927,29.535 -2020-08-19 08:45:00,118.51,42.016999999999996,57.927,29.535 -2020-08-19 09:00:00,117.01,37.033,54.86,29.535 -2020-08-19 09:15:00,114.75,36.162,54.86,29.535 -2020-08-19 09:30:00,117.13,38.986999999999995,54.86,29.535 -2020-08-19 09:45:00,112.52,41.547,54.86,29.535 -2020-08-19 10:00:00,114.95,38.043,52.818000000000005,29.535 -2020-08-19 10:15:00,116.13,39.04,52.818000000000005,29.535 -2020-08-19 10:30:00,116.65,38.913000000000004,52.818000000000005,29.535 -2020-08-19 10:45:00,111.6,40.138000000000005,52.818000000000005,29.535 -2020-08-19 11:00:00,110.58,38.38,52.937,29.535 -2020-08-19 11:15:00,112.5,39.426,52.937,29.535 -2020-08-19 11:30:00,112.13,40.26,52.937,29.535 -2020-08-19 11:45:00,111.52,41.125,52.937,29.535 -2020-08-19 12:00:00,106.13,37.423,50.826,29.535 -2020-08-19 12:15:00,106.08,36.811,50.826,29.535 -2020-08-19 12:30:00,106.22,35.847,50.826,29.535 -2020-08-19 12:45:00,115.53,36.633,50.826,29.535 -2020-08-19 13:00:00,113.75,36.177,50.556000000000004,29.535 -2020-08-19 13:15:00,112.48,36.993,50.556000000000004,29.535 -2020-08-19 13:30:00,105.73,35.543,50.556000000000004,29.535 -2020-08-19 13:45:00,105.62,35.400999999999996,50.556000000000004,29.535 -2020-08-19 14:00:00,109.57,36.584,51.188,29.535 -2020-08-19 14:15:00,112.24,35.759,51.188,29.535 -2020-08-19 14:30:00,108.23,34.856,51.188,29.535 -2020-08-19 14:45:00,107.5,35.559,51.188,29.535 -2020-08-19 15:00:00,109.94,36.618,52.976000000000006,29.535 -2020-08-19 15:15:00,110.19,34.525999999999996,52.976000000000006,29.535 -2020-08-19 15:30:00,105.86,33.321999999999996,52.976000000000006,29.535 -2020-08-19 15:45:00,109.9,31.653000000000002,52.976000000000006,29.535 -2020-08-19 16:00:00,110.95,34.01,55.463,29.535 -2020-08-19 16:15:00,112.44,33.936,55.463,29.535 -2020-08-19 16:30:00,109.15,33.707,55.463,29.535 -2020-08-19 16:45:00,107.57,31.572,55.463,29.535 -2020-08-19 17:00:00,115.26,34.226,59.435,29.535 -2020-08-19 17:15:00,118.06,35.025,59.435,29.535 -2020-08-19 17:30:00,118.86,34.346,59.435,29.535 -2020-08-19 17:45:00,116.17,33.907,59.435,29.535 -2020-08-19 18:00:00,112.79,36.433,61.387,29.535 -2020-08-19 18:15:00,115.1,36.225,61.387,29.535 -2020-08-19 18:30:00,118.21,34.598,61.387,29.535 -2020-08-19 18:45:00,118.6,37.507,61.387,29.535 -2020-08-19 19:00:00,113.59,38.624,63.323,29.535 -2020-08-19 19:15:00,106.04,37.567,63.323,29.535 -2020-08-19 19:30:00,112.3,36.503,63.323,29.535 -2020-08-19 19:45:00,112.18,36.016,63.323,29.535 -2020-08-19 20:00:00,107.02,33.094,69.083,29.535 -2020-08-19 20:15:00,107.02,32.918,69.083,29.535 -2020-08-19 20:30:00,100.41,33.061,69.083,29.535 -2020-08-19 20:45:00,98.9,33.274,69.083,29.535 -2020-08-19 21:00:00,94.63,31.729,59.957,29.535 -2020-08-19 21:15:00,94.3,33.27,59.957,29.535 -2020-08-19 21:30:00,91.09,33.616,59.957,29.535 -2020-08-19 21:45:00,88.92,33.804,59.957,29.535 -2020-08-19 22:00:00,81.63,31.223000000000003,53.821000000000005,29.535 -2020-08-19 22:15:00,84.95,33.12,53.821000000000005,29.535 -2020-08-19 22:30:00,83.56,28.084,53.821000000000005,29.535 -2020-08-19 22:45:00,83.98,25.009,53.821000000000005,29.535 -2020-08-19 23:00:00,74.72,22.921,45.458,29.535 -2020-08-19 23:15:00,78.64,21.933000000000003,45.458,29.535 -2020-08-19 23:30:00,74.14,21.443,45.458,29.535 -2020-08-19 23:45:00,79.63,20.586,45.458,29.535 -2020-08-20 00:00:00,73.64,20.724,40.36,29.535 -2020-08-20 00:15:00,75.13,21.421,40.36,29.535 -2020-08-20 00:30:00,70.59,20.57,40.36,29.535 -2020-08-20 00:45:00,74.45,20.739,40.36,29.535 -2020-08-20 01:00:00,72.22,20.49,38.552,29.535 -2020-08-20 01:15:00,74.16,19.795,38.552,29.535 -2020-08-20 01:30:00,75.17,18.719,38.552,29.535 -2020-08-20 01:45:00,81.58,18.444000000000003,38.552,29.535 -2020-08-20 02:00:00,80.48,18.346,36.895,29.535 -2020-08-20 02:15:00,77.16,17.660999999999998,36.895,29.535 -2020-08-20 02:30:00,74.84,18.802,36.895,29.535 -2020-08-20 02:45:00,76.18,19.485,36.895,29.535 -2020-08-20 03:00:00,77.16,20.230999999999998,36.565,29.535 -2020-08-20 03:15:00,76.8,20.024,36.565,29.535 -2020-08-20 03:30:00,77.13,19.705,36.565,29.535 -2020-08-20 03:45:00,81.08,19.352999999999998,36.565,29.535 -2020-08-20 04:00:00,82.53,23.513,37.263000000000005,29.535 -2020-08-20 04:15:00,85.3,28.87,37.263000000000005,29.535 -2020-08-20 04:30:00,89.78,26.456,37.263000000000005,29.535 -2020-08-20 04:45:00,92.89,26.572,37.263000000000005,29.535 -2020-08-20 05:00:00,100.42,38.428000000000004,40.412,29.535 -2020-08-20 05:15:00,97.56,44.992,40.412,29.535 -2020-08-20 05:30:00,104.41,40.54,40.412,29.535 -2020-08-20 05:45:00,114.19,38.008,40.412,29.535 -2020-08-20 06:00:00,118.96,38.368,49.825,29.535 -2020-08-20 06:15:00,118.57,38.561,49.825,29.535 -2020-08-20 06:30:00,115.56,38.058,49.825,29.535 -2020-08-20 06:45:00,116.37,40.343,49.825,29.535 -2020-08-20 07:00:00,117.94,40.379,61.082,29.535 -2020-08-20 07:15:00,121.74,41.48,61.082,29.535 -2020-08-20 07:30:00,117.74,39.907,61.082,29.535 -2020-08-20 07:45:00,117.37,40.691,61.082,29.535 -2020-08-20 08:00:00,119.5,37.708,53.961999999999996,29.535 -2020-08-20 08:15:00,119.23,40.032,53.961999999999996,29.535 -2020-08-20 08:30:00,117.61,40.457,53.961999999999996,29.535 -2020-08-20 08:45:00,119.25,42.288999999999994,53.961999999999996,29.535 -2020-08-20 09:00:00,121.78,37.311,50.06100000000001,29.535 -2020-08-20 09:15:00,121.69,36.431999999999995,50.06100000000001,29.535 -2020-08-20 09:30:00,113.46,39.24,50.06100000000001,29.535 -2020-08-20 09:45:00,113.9,41.778,50.06100000000001,29.535 -2020-08-20 10:00:00,119.89,38.274,47.68,29.535 -2020-08-20 10:15:00,119.66,39.247,47.68,29.535 -2020-08-20 10:30:00,118.06,39.114000000000004,47.68,29.535 -2020-08-20 10:45:00,111.47,40.330999999999996,47.68,29.535 -2020-08-20 11:00:00,110.59,38.582,45.93899999999999,29.535 -2020-08-20 11:15:00,113.71,39.62,45.93899999999999,29.535 -2020-08-20 11:30:00,113.77,40.45,45.93899999999999,29.535 -2020-08-20 11:45:00,113.94,41.301,45.93899999999999,29.535 -2020-08-20 12:00:00,110.52,37.588,43.648999999999994,29.535 -2020-08-20 12:15:00,113.88,36.965,43.648999999999994,29.535 -2020-08-20 12:30:00,114.93,36.016,43.648999999999994,29.535 -2020-08-20 12:45:00,114.05,36.791,43.648999999999994,29.535 -2020-08-20 13:00:00,113.61,36.317,42.801,29.535 -2020-08-20 13:15:00,115.95,37.122,42.801,29.535 -2020-08-20 13:30:00,127.37,35.669000000000004,42.801,29.535 -2020-08-20 13:45:00,125.7,35.538000000000004,42.801,29.535 -2020-08-20 14:00:00,120.24,36.696999999999996,43.24,29.535 -2020-08-20 14:15:00,116.22,35.878,43.24,29.535 -2020-08-20 14:30:00,117.36,34.991,43.24,29.535 -2020-08-20 14:45:00,120.32,35.696,43.24,29.535 -2020-08-20 15:00:00,118.94,36.713,45.04600000000001,29.535 -2020-08-20 15:15:00,113.51,34.626999999999995,45.04600000000001,29.535 -2020-08-20 15:30:00,112.59,33.437,45.04600000000001,29.535 -2020-08-20 15:45:00,117.13,31.774,45.04600000000001,29.535 -2020-08-20 16:00:00,112.87,34.106,46.568000000000005,29.535 -2020-08-20 16:15:00,107.6,34.041,46.568000000000005,29.535 -2020-08-20 16:30:00,113.61,33.817,46.568000000000005,29.535 -2020-08-20 16:45:00,113.75,31.721,46.568000000000005,29.535 -2020-08-20 17:00:00,116.5,34.345,50.618,29.535 -2020-08-20 17:15:00,117.63,35.172,50.618,29.535 -2020-08-20 17:30:00,118.34,34.506,50.618,29.535 -2020-08-20 17:45:00,121.4,34.099000000000004,50.618,29.535 -2020-08-20 18:00:00,121.03,36.616,52.806999999999995,29.535 -2020-08-20 18:15:00,118.78,36.42,52.806999999999995,29.535 -2020-08-20 18:30:00,118.33,34.802,52.806999999999995,29.535 -2020-08-20 18:45:00,113.93,37.711,52.806999999999995,29.535 -2020-08-20 19:00:00,110.67,38.834,53.464,29.535 -2020-08-20 19:15:00,108.18,37.775999999999996,53.464,29.535 -2020-08-20 19:30:00,108.15,36.714,53.464,29.535 -2020-08-20 19:45:00,108.46,36.23,53.464,29.535 -2020-08-20 20:00:00,104.07,33.314,56.753,29.535 -2020-08-20 20:15:00,102.81,33.14,56.753,29.535 -2020-08-20 20:30:00,101.22,33.266999999999996,56.753,29.535 -2020-08-20 20:45:00,100.86,33.449,56.753,29.535 -2020-08-20 21:00:00,93.78,31.909000000000002,52.506,29.535 -2020-08-20 21:15:00,95.92,33.44,52.506,29.535 -2020-08-20 21:30:00,88.69,33.784,52.506,29.535 -2020-08-20 21:45:00,90.87,33.941,52.506,29.535 -2020-08-20 22:00:00,83.08,31.343000000000004,48.163000000000004,29.535 -2020-08-20 22:15:00,86.43,33.224000000000004,48.163000000000004,29.535 -2020-08-20 22:30:00,83.99,28.158,48.163000000000004,29.535 -2020-08-20 22:45:00,83.77,25.084,48.163000000000004,29.535 -2020-08-20 23:00:00,75.0,23.035,42.379,29.535 -2020-08-20 23:15:00,77.96,22.035,42.379,29.535 -2020-08-20 23:30:00,75.78,21.557,42.379,29.535 -2020-08-20 23:45:00,78.91,20.704,42.379,29.535 -2020-08-21 00:00:00,75.42,19.239,38.505,29.535 -2020-08-21 00:15:00,75.15,20.131,38.505,29.535 -2020-08-21 00:30:00,73.55,19.534000000000002,38.505,29.535 -2020-08-21 00:45:00,74.33,20.096,38.505,29.535 -2020-08-21 01:00:00,73.08,19.484,37.004,29.535 -2020-08-21 01:15:00,73.78,18.242,37.004,29.535 -2020-08-21 01:30:00,73.77,17.834,37.004,29.535 -2020-08-21 01:45:00,80.71,17.328,37.004,29.535 -2020-08-21 02:00:00,81.21,18.083,36.098,29.535 -2020-08-21 02:15:00,77.73,17.374000000000002,36.098,29.535 -2020-08-21 02:30:00,74.39,19.24,36.098,29.535 -2020-08-21 02:45:00,75.1,19.294,36.098,29.535 -2020-08-21 03:00:00,77.31,20.749000000000002,36.561,29.535 -2020-08-21 03:15:00,77.03,19.445,36.561,29.535 -2020-08-21 03:30:00,76.96,18.914,36.561,29.535 -2020-08-21 03:45:00,83.38,19.355,36.561,29.535 -2020-08-21 04:00:00,88.24,23.659000000000002,37.355,29.535 -2020-08-21 04:15:00,91.71,27.607,37.355,29.535 -2020-08-21 04:30:00,90.26,26.066,37.355,29.535 -2020-08-21 04:45:00,91.87,25.607,37.355,29.535 -2020-08-21 05:00:00,99.2,37.098,40.285,29.535 -2020-08-21 05:15:00,102.18,44.583,40.285,29.535 -2020-08-21 05:30:00,106.7,40.306,40.285,29.535 -2020-08-21 05:45:00,115.67,37.338,40.285,29.535 -2020-08-21 06:00:00,123.11,37.869,52.378,29.535 -2020-08-21 06:15:00,120.91,38.234,52.378,29.535 -2020-08-21 06:30:00,123.41,37.684,52.378,29.535 -2020-08-21 06:45:00,127.16,39.852,52.378,29.535 -2020-08-21 07:00:00,129.29,40.475,60.891999999999996,29.535 -2020-08-21 07:15:00,124.88,42.409,60.891999999999996,29.535 -2020-08-21 07:30:00,117.88,39.092,60.891999999999996,29.535 -2020-08-21 07:45:00,116.37,39.691,60.891999999999996,29.535 -2020-08-21 08:00:00,118.09,37.425,53.652,29.535 -2020-08-21 08:15:00,119.83,40.330999999999996,53.652,29.535 -2020-08-21 08:30:00,122.73,40.656,53.652,29.535 -2020-08-21 08:45:00,121.5,42.353,53.652,29.535 -2020-08-21 09:00:00,118.6,35.285,51.456,29.535 -2020-08-21 09:15:00,120.12,36.135999999999996,51.456,29.535 -2020-08-21 09:30:00,126.02,38.305,51.456,29.535 -2020-08-21 09:45:00,122.6,41.172,51.456,29.535 -2020-08-21 10:00:00,120.93,37.529,49.4,29.535 -2020-08-21 10:15:00,121.21,38.27,49.4,29.535 -2020-08-21 10:30:00,124.91,38.634,49.4,29.535 -2020-08-21 10:45:00,126.82,39.759,49.4,29.535 -2020-08-21 11:00:00,123.43,38.259,48.773,29.535 -2020-08-21 11:15:00,121.31,38.306,48.773,29.535 -2020-08-21 11:30:00,114.97,38.778,48.773,29.535 -2020-08-21 11:45:00,117.42,38.736,48.773,29.535 -2020-08-21 12:00:00,122.33,35.384,46.033,29.535 -2020-08-21 12:15:00,112.57,34.199,46.033,29.535 -2020-08-21 12:30:00,103.79,33.37,46.033,29.535 -2020-08-21 12:45:00,111.23,33.400999999999996,46.033,29.535 -2020-08-21 13:00:00,109.6,33.441,44.38399999999999,29.535 -2020-08-21 13:15:00,108.67,34.389,44.38399999999999,29.535 -2020-08-21 13:30:00,105.43,33.662,44.38399999999999,29.535 -2020-08-21 13:45:00,105.72,33.823,44.38399999999999,29.535 -2020-08-21 14:00:00,109.3,34.238,43.162,29.535 -2020-08-21 14:15:00,119.63,33.829,43.162,29.535 -2020-08-21 14:30:00,116.81,34.333,43.162,29.535 -2020-08-21 14:45:00,115.99,34.4,43.162,29.535 -2020-08-21 15:00:00,108.46,35.311,44.91,29.535 -2020-08-21 15:15:00,102.35,33.022,44.91,29.535 -2020-08-21 15:30:00,102.98,31.35,44.91,29.535 -2020-08-21 15:45:00,109.7,30.392,44.91,29.535 -2020-08-21 16:00:00,115.52,31.933000000000003,47.489,29.535 -2020-08-21 16:15:00,117.53,32.324,47.489,29.535 -2020-08-21 16:30:00,114.19,31.936,47.489,29.535 -2020-08-21 16:45:00,114.63,29.162,47.489,29.535 -2020-08-21 17:00:00,114.87,33.342,52.047,29.535 -2020-08-21 17:15:00,112.6,34.046,52.047,29.535 -2020-08-21 17:30:00,111.56,33.547,52.047,29.535 -2020-08-21 17:45:00,119.81,33.025999999999996,52.047,29.535 -2020-08-21 18:00:00,118.61,35.549,53.306000000000004,29.535 -2020-08-21 18:15:00,111.71,34.506,53.306000000000004,29.535 -2020-08-21 18:30:00,109.02,32.798,53.306000000000004,29.535 -2020-08-21 18:45:00,109.32,36.089,53.306000000000004,29.535 -2020-08-21 19:00:00,104.75,37.998000000000005,53.516000000000005,29.535 -2020-08-21 19:15:00,103.22,37.463,53.516000000000005,29.535 -2020-08-21 19:30:00,105.19,36.463,53.516000000000005,29.535 -2020-08-21 19:45:00,109.91,35.064,53.516000000000005,29.535 -2020-08-21 20:00:00,108.19,32.015,57.88,29.535 -2020-08-21 20:15:00,105.08,32.567,57.88,29.535 -2020-08-21 20:30:00,99.01,32.235,57.88,29.535 -2020-08-21 20:45:00,95.41,31.636,57.88,29.535 -2020-08-21 21:00:00,90.21,31.29,53.32,29.535 -2020-08-21 21:15:00,89.55,34.330999999999996,53.32,29.535 -2020-08-21 21:30:00,92.66,34.534,53.32,29.535 -2020-08-21 21:45:00,91.64,34.841,53.32,29.535 -2020-08-21 22:00:00,87.68,32.082,48.074,29.535 -2020-08-21 22:15:00,84.08,33.724000000000004,48.074,29.535 -2020-08-21 22:30:00,86.02,32.883,48.074,29.535 -2020-08-21 22:45:00,85.3,30.55,48.074,29.535 -2020-08-21 23:00:00,77.83,30.086,41.306999999999995,29.535 -2020-08-21 23:15:00,71.95,27.67,41.306999999999995,29.535 -2020-08-21 23:30:00,73.74,25.559,41.306999999999995,29.535 -2020-08-21 23:45:00,71.29,24.608,41.306999999999995,29.535 -2020-08-22 00:00:00,69.81,20.534000000000002,40.227,29.423000000000002 -2020-08-22 00:15:00,69.02,20.857,40.227,29.423000000000002 -2020-08-22 00:30:00,67.89,19.826,40.227,29.423000000000002 -2020-08-22 00:45:00,72.34,19.723,40.227,29.423000000000002 -2020-08-22 01:00:00,75.79,19.339000000000002,36.303000000000004,29.423000000000002 -2020-08-22 01:15:00,76.02,18.655,36.303000000000004,29.423000000000002 -2020-08-22 01:30:00,75.61,17.533,36.303000000000004,29.423000000000002 -2020-08-22 01:45:00,73.35,18.16,36.303000000000004,29.423000000000002 -2020-08-22 02:00:00,74.97,18.018,33.849000000000004,29.423000000000002 -2020-08-22 02:15:00,75.4,16.616,33.849000000000004,29.423000000000002 -2020-08-22 02:30:00,69.28,17.753,33.849000000000004,29.423000000000002 -2020-08-22 02:45:00,66.05,18.517,33.849000000000004,29.423000000000002 -2020-08-22 03:00:00,72.84,18.707,33.149,29.423000000000002 -2020-08-22 03:15:00,73.03,16.8,33.149,29.423000000000002 -2020-08-22 03:30:00,67.68,16.592,33.149,29.423000000000002 -2020-08-22 03:45:00,66.88,18.373,33.149,29.423000000000002 -2020-08-22 04:00:00,69.12,21.037,32.501,29.423000000000002 -2020-08-22 04:15:00,67.9,24.219,32.501,29.423000000000002 -2020-08-22 04:30:00,68.89,21.195999999999998,32.501,29.423000000000002 -2020-08-22 04:45:00,75.53,20.994,32.501,29.423000000000002 -2020-08-22 05:00:00,76.39,25.175,31.648000000000003,29.423000000000002 -2020-08-22 05:15:00,77.34,23.013,31.648000000000003,29.423000000000002 -2020-08-22 05:30:00,70.85,20.104,31.648000000000003,29.423000000000002 -2020-08-22 05:45:00,75.88,21.084,31.648000000000003,29.423000000000002 -2020-08-22 06:00:00,79.9,31.51,32.552,29.423000000000002 -2020-08-22 06:15:00,81.93,38.769,32.552,29.423000000000002 -2020-08-22 06:30:00,78.71,35.705999999999996,32.552,29.423000000000002 -2020-08-22 06:45:00,78.42,35.068000000000005,32.552,29.423000000000002 -2020-08-22 07:00:00,81.49,34.53,35.181999999999995,29.423000000000002 -2020-08-22 07:15:00,83.52,35.321999999999996,35.181999999999995,29.423000000000002 -2020-08-22 07:30:00,88.71,33.359,35.181999999999995,29.423000000000002 -2020-08-22 07:45:00,83.28,34.743,35.181999999999995,29.423000000000002 -2020-08-22 08:00:00,87.36,33.074,40.35,29.423000000000002 -2020-08-22 08:15:00,83.56,35.699,40.35,29.423000000000002 -2020-08-22 08:30:00,83.76,35.982,40.35,29.423000000000002 -2020-08-22 08:45:00,86.8,38.474000000000004,40.35,29.423000000000002 -2020-08-22 09:00:00,91.83,34.203,42.292,29.423000000000002 -2020-08-22 09:15:00,94.77,35.471,42.292,29.423000000000002 -2020-08-22 09:30:00,93.76,38.058,42.292,29.423000000000002 -2020-08-22 09:45:00,86.84,40.493,42.292,29.423000000000002 -2020-08-22 10:00:00,92.6,37.429,40.084,29.423000000000002 -2020-08-22 10:15:00,82.43,38.467,40.084,29.423000000000002 -2020-08-22 10:30:00,84.17,38.509,40.084,29.423000000000002 -2020-08-22 10:45:00,83.75,39.282,40.084,29.423000000000002 -2020-08-22 11:00:00,82.45,37.674,36.966,29.423000000000002 -2020-08-22 11:15:00,78.36,38.518,36.966,29.423000000000002 -2020-08-22 11:30:00,80.55,39.266,36.966,29.423000000000002 -2020-08-22 11:45:00,84.46,39.841,36.966,29.423000000000002 -2020-08-22 12:00:00,80.83,36.912,35.19,29.423000000000002 -2020-08-22 12:15:00,81.73,36.481,35.19,29.423000000000002 -2020-08-22 12:30:00,83.66,35.539,35.19,29.423000000000002 -2020-08-22 12:45:00,83.6,36.244,35.19,29.423000000000002 -2020-08-22 13:00:00,80.22,35.459,32.277,29.423000000000002 -2020-08-22 13:15:00,81.83,35.946,32.277,29.423000000000002 -2020-08-22 13:30:00,79.68,35.398,32.277,29.423000000000002 -2020-08-22 13:45:00,76.05,34.327,32.277,29.423000000000002 -2020-08-22 14:00:00,76.01,34.762,31.436999999999998,29.423000000000002 -2020-08-22 14:15:00,76.05,33.229,31.436999999999998,29.423000000000002 -2020-08-22 14:30:00,73.17,33.400999999999996,31.436999999999998,29.423000000000002 -2020-08-22 14:45:00,73.18,33.904,31.436999999999998,29.423000000000002 -2020-08-22 15:00:00,73.62,35.177,33.493,29.423000000000002 -2020-08-22 15:15:00,75.64,33.522,33.493,29.423000000000002 -2020-08-22 15:30:00,75.93,32.013000000000005,33.493,29.423000000000002 -2020-08-22 15:45:00,75.66,30.217,33.493,29.423000000000002 -2020-08-22 16:00:00,74.81,33.667,36.593,29.423000000000002 -2020-08-22 16:15:00,75.25,33.234,36.593,29.423000000000002 -2020-08-22 16:30:00,75.96,33.058,36.593,29.423000000000002 -2020-08-22 16:45:00,78.59,30.281,36.593,29.423000000000002 -2020-08-22 17:00:00,80.01,33.37,42.049,29.423000000000002 -2020-08-22 17:15:00,81.17,32.102,42.049,29.423000000000002 -2020-08-22 17:30:00,80.84,31.496,42.049,29.423000000000002 -2020-08-22 17:45:00,79.99,31.468000000000004,42.049,29.423000000000002 -2020-08-22 18:00:00,83.24,35.213,43.755,29.423000000000002 -2020-08-22 18:15:00,78.95,35.67,43.755,29.423000000000002 -2020-08-22 18:30:00,78.74,35.183,43.755,29.423000000000002 -2020-08-22 18:45:00,79.68,35.374,43.755,29.423000000000002 -2020-08-22 19:00:00,79.36,35.782,44.492,29.423000000000002 -2020-08-22 19:15:00,78.0,34.344,44.492,29.423000000000002 -2020-08-22 19:30:00,78.76,34.03,44.492,29.423000000000002 -2020-08-22 19:45:00,78.12,34.227,44.492,29.423000000000002 -2020-08-22 20:00:00,75.97,31.935,40.896,29.423000000000002 -2020-08-22 20:15:00,75.27,31.884,40.896,29.423000000000002 -2020-08-22 20:30:00,77.26,30.746,40.896,29.423000000000002 -2020-08-22 20:45:00,74.04,31.799,40.896,29.423000000000002 -2020-08-22 21:00:00,72.99,30.090999999999998,39.056,29.423000000000002 -2020-08-22 21:15:00,69.37,32.803000000000004,39.056,29.423000000000002 -2020-08-22 21:30:00,65.99,33.161,39.056,29.423000000000002 -2020-08-22 21:45:00,65.64,32.949,39.056,29.423000000000002 -2020-08-22 22:00:00,62.66,30.133000000000003,38.478,29.423000000000002 -2020-08-22 22:15:00,62.96,31.954,38.478,29.423000000000002 -2020-08-22 22:30:00,60.65,30.666999999999998,38.478,29.423000000000002 -2020-08-22 22:45:00,60.45,28.648000000000003,38.478,29.423000000000002 -2020-08-22 23:00:00,57.7,27.596,32.953,29.423000000000002 -2020-08-22 23:15:00,56.86,25.565,32.953,29.423000000000002 -2020-08-22 23:30:00,56.41,25.7,32.953,29.423000000000002 -2020-08-22 23:45:00,56.12,25.105,32.953,29.423000000000002 -2020-08-23 00:00:00,54.05,21.918000000000003,28.584,29.423000000000002 -2020-08-23 00:15:00,54.14,21.205,28.584,29.423000000000002 -2020-08-23 00:30:00,51.82,20.051,28.584,29.423000000000002 -2020-08-23 00:45:00,52.75,19.846,28.584,29.423000000000002 -2020-08-23 01:00:00,50.79,19.685,26.419,29.423000000000002 -2020-08-23 01:15:00,51.91,18.892,26.419,29.423000000000002 -2020-08-23 01:30:00,52.0,17.651,26.419,29.423000000000002 -2020-08-23 01:45:00,51.21,17.939,26.419,29.423000000000002 -2020-08-23 02:00:00,49.98,17.88,25.335,29.423000000000002 -2020-08-23 02:15:00,49.28,17.148,25.335,29.423000000000002 -2020-08-23 02:30:00,49.62,18.421,25.335,29.423000000000002 -2020-08-23 02:45:00,50.01,18.914,25.335,29.423000000000002 -2020-08-23 03:00:00,49.63,19.677,24.805,29.423000000000002 -2020-08-23 03:15:00,50.61,18.027,24.805,29.423000000000002 -2020-08-23 03:30:00,51.05,17.143,24.805,29.423000000000002 -2020-08-23 03:45:00,51.04,18.202,24.805,29.423000000000002 -2020-08-23 04:00:00,52.83,20.862,25.772,29.423000000000002 -2020-08-23 04:15:00,53.3,23.68,25.772,29.423000000000002 -2020-08-23 04:30:00,53.83,21.873,25.772,29.423000000000002 -2020-08-23 04:45:00,55.23,21.249000000000002,25.772,29.423000000000002 -2020-08-23 05:00:00,53.76,25.686999999999998,25.971999999999998,29.423000000000002 -2020-08-23 05:15:00,53.31,22.923000000000002,25.971999999999998,29.423000000000002 -2020-08-23 05:30:00,52.98,19.655,25.971999999999998,29.423000000000002 -2020-08-23 05:45:00,54.2,20.352,25.971999999999998,29.423000000000002 -2020-08-23 06:00:00,55.52,28.631999999999998,26.026,29.423000000000002 -2020-08-23 06:15:00,56.17,36.676,26.026,29.423000000000002 -2020-08-23 06:30:00,57.6,32.996,26.026,29.423000000000002 -2020-08-23 06:45:00,58.37,31.48,26.026,29.423000000000002 -2020-08-23 07:00:00,60.01,31.19,27.396,29.423000000000002 -2020-08-23 07:15:00,59.93,30.531,27.396,29.423000000000002 -2020-08-23 07:30:00,61.88,29.76,27.396,29.423000000000002 -2020-08-23 07:45:00,62.61,31.128,27.396,29.423000000000002 -2020-08-23 08:00:00,63.28,30.125999999999998,30.791999999999998,29.423000000000002 -2020-08-23 08:15:00,61.72,33.788000000000004,30.791999999999998,29.423000000000002 -2020-08-23 08:30:00,60.05,34.823,30.791999999999998,29.423000000000002 -2020-08-23 08:45:00,60.37,37.19,30.791999999999998,29.423000000000002 -2020-08-23 09:00:00,57.88,32.838,32.482,29.423000000000002 -2020-08-23 09:15:00,58.54,33.64,32.482,29.423000000000002 -2020-08-23 09:30:00,61.36,36.603,32.482,29.423000000000002 -2020-08-23 09:45:00,64.5,39.924,32.482,29.423000000000002 -2020-08-23 10:00:00,65.45,37.297,31.951,29.423000000000002 -2020-08-23 10:15:00,63.8,38.42,31.951,29.423000000000002 -2020-08-23 10:30:00,69.36,38.641,31.951,29.423000000000002 -2020-08-23 10:45:00,69.5,40.378,31.951,29.423000000000002 -2020-08-23 11:00:00,68.42,38.444,33.619,29.423000000000002 -2020-08-23 11:15:00,68.97,38.867,33.619,29.423000000000002 -2020-08-23 11:30:00,65.45,40.113,33.619,29.423000000000002 -2020-08-23 11:45:00,65.73,40.89,33.619,29.423000000000002 -2020-08-23 12:00:00,62.07,38.953,30.975,29.423000000000002 -2020-08-23 12:15:00,60.77,37.865,30.975,29.423000000000002 -2020-08-23 12:30:00,57.24,37.19,30.975,29.423000000000002 -2020-08-23 12:45:00,59.1,37.333,30.975,29.423000000000002 -2020-08-23 13:00:00,54.41,36.266999999999996,27.956999999999997,29.423000000000002 -2020-08-23 13:15:00,56.3,36.116,27.956999999999997,29.423000000000002 -2020-08-23 13:30:00,54.14,34.552,27.956999999999997,29.423000000000002 -2020-08-23 13:45:00,57.34,34.536,27.956999999999997,29.423000000000002 -2020-08-23 14:00:00,54.95,36.039,25.555999999999997,29.423000000000002 -2020-08-23 14:15:00,55.93,34.866,25.555999999999997,29.423000000000002 -2020-08-23 14:30:00,53.34,33.878,25.555999999999997,29.423000000000002 -2020-08-23 14:45:00,52.83,33.415,25.555999999999997,29.423000000000002 -2020-08-23 15:00:00,52.56,34.832,26.271,29.423000000000002 -2020-08-23 15:15:00,52.26,32.414,26.271,29.423000000000002 -2020-08-23 15:30:00,53.99,30.730999999999998,26.271,29.423000000000002 -2020-08-23 15:45:00,55.72,29.178,26.271,29.423000000000002 -2020-08-23 16:00:00,60.31,31.070999999999998,30.369,29.423000000000002 -2020-08-23 16:15:00,60.83,30.884,30.369,29.423000000000002 -2020-08-23 16:30:00,63.75,31.62,30.369,29.423000000000002 -2020-08-23 16:45:00,67.7,28.939,30.369,29.423000000000002 -2020-08-23 17:00:00,71.4,32.325,38.787,29.423000000000002 -2020-08-23 17:15:00,73.13,32.479,38.787,29.423000000000002 -2020-08-23 17:30:00,74.84,32.615,38.787,29.423000000000002 -2020-08-23 17:45:00,76.19,32.934,38.787,29.423000000000002 -2020-08-23 18:00:00,78.8,37.244,41.886,29.423000000000002 -2020-08-23 18:15:00,78.18,37.311,41.886,29.423000000000002 -2020-08-23 18:30:00,76.17,36.661,41.886,29.423000000000002 -2020-08-23 18:45:00,77.31,36.91,41.886,29.423000000000002 -2020-08-23 19:00:00,79.28,39.316,42.91,29.423000000000002 -2020-08-23 19:15:00,79.78,36.865,42.91,29.423000000000002 -2020-08-23 19:30:00,81.08,36.332,42.91,29.423000000000002 -2020-08-23 19:45:00,80.3,36.093,42.91,29.423000000000002 -2020-08-23 20:00:00,79.94,33.946999999999996,42.148999999999994,29.423000000000002 -2020-08-23 20:15:00,79.46,33.751,42.148999999999994,29.423000000000002 -2020-08-23 20:30:00,78.65,33.303000000000004,42.148999999999994,29.423000000000002 -2020-08-23 20:45:00,78.43,32.828,42.148999999999994,29.423000000000002 -2020-08-23 21:00:00,77.51,31.070999999999998,40.955999999999996,29.423000000000002 -2020-08-23 21:15:00,78.17,33.514,40.955999999999996,29.423000000000002 -2020-08-23 21:30:00,74.81,33.278,40.955999999999996,29.423000000000002 -2020-08-23 21:45:00,74.82,33.356,40.955999999999996,29.423000000000002 -2020-08-23 22:00:00,70.62,32.385999999999996,39.873000000000005,29.423000000000002 -2020-08-23 22:15:00,71.54,32.687,39.873000000000005,29.423000000000002 -2020-08-23 22:30:00,70.08,30.976999999999997,39.873000000000005,29.423000000000002 -2020-08-23 22:45:00,70.5,27.828000000000003,39.873000000000005,29.423000000000002 -2020-08-23 23:00:00,65.77,26.604,35.510999999999996,29.423000000000002 -2020-08-23 23:15:00,68.03,25.668000000000003,35.510999999999996,29.423000000000002 -2020-08-23 23:30:00,68.09,25.31,35.510999999999996,29.423000000000002 -2020-08-23 23:45:00,67.17,24.844,35.510999999999996,29.423000000000002 -2020-08-24 00:00:00,61.92,23.346999999999998,33.475,29.535 -2020-08-24 00:15:00,61.51,23.287,33.475,29.535 -2020-08-24 00:30:00,62.23,21.79,33.475,29.535 -2020-08-24 00:45:00,64.91,21.246,33.475,29.535 -2020-08-24 01:00:00,63.88,21.413,33.111,29.535 -2020-08-24 01:15:00,64.28,20.656,33.111,29.535 -2020-08-24 01:30:00,63.48,19.754,33.111,29.535 -2020-08-24 01:45:00,64.2,19.951,33.111,29.535 -2020-08-24 02:00:00,63.59,20.304000000000002,32.358000000000004,29.535 -2020-08-24 02:15:00,65.17,18.646,32.358000000000004,29.535 -2020-08-24 02:30:00,66.78,20.039,32.358000000000004,29.535 -2020-08-24 02:45:00,69.86,20.409000000000002,32.358000000000004,29.535 -2020-08-24 03:00:00,74.42,21.589000000000002,30.779,29.535 -2020-08-24 03:15:00,75.12,20.570999999999998,30.779,29.535 -2020-08-24 03:30:00,69.75,20.315,30.779,29.535 -2020-08-24 03:45:00,71.44,20.962,30.779,29.535 -2020-08-24 04:00:00,77.38,26.358,31.416,29.535 -2020-08-24 04:15:00,84.0,31.753,31.416,29.535 -2020-08-24 04:30:00,88.97,29.471,31.416,29.535 -2020-08-24 04:45:00,90.63,29.182,31.416,29.535 -2020-08-24 05:00:00,92.59,39.953,37.221,29.535 -2020-08-24 05:15:00,98.51,46.222,37.221,29.535 -2020-08-24 05:30:00,97.7,41.567,37.221,29.535 -2020-08-24 05:45:00,106.99,39.55,37.221,29.535 -2020-08-24 06:00:00,114.75,38.809,51.891000000000005,29.535 -2020-08-24 06:15:00,115.62,38.935,51.891000000000005,29.535 -2020-08-24 06:30:00,114.57,38.699,51.891000000000005,29.535 -2020-08-24 06:45:00,111.02,41.791000000000004,51.891000000000005,29.535 -2020-08-24 07:00:00,116.17,41.681999999999995,62.282,29.535 -2020-08-24 07:15:00,117.57,43.071000000000005,62.282,29.535 -2020-08-24 07:30:00,116.26,41.563,62.282,29.535 -2020-08-24 07:45:00,111.86,43.251999999999995,62.282,29.535 -2020-08-24 08:00:00,112.24,40.347,54.102,29.535 -2020-08-24 08:15:00,110.91,42.93600000000001,54.102,29.535 -2020-08-24 08:30:00,107.9,43.183,54.102,29.535 -2020-08-24 08:45:00,106.7,45.798,54.102,29.535 -2020-08-24 09:00:00,103.97,40.516999999999996,50.917,29.535 -2020-08-24 09:15:00,112.8,39.77,50.917,29.535 -2020-08-24 09:30:00,109.89,41.897,50.917,29.535 -2020-08-24 09:45:00,115.07,43.108000000000004,50.917,29.535 -2020-08-24 10:00:00,107.02,40.842,49.718999999999994,29.535 -2020-08-24 10:15:00,116.52,41.815,49.718999999999994,29.535 -2020-08-24 10:30:00,119.9,41.635,49.718999999999994,29.535 -2020-08-24 10:45:00,119.31,41.896,49.718999999999994,29.535 -2020-08-24 11:00:00,116.15,40.231,49.833999999999996,29.535 -2020-08-24 11:15:00,121.04,40.848,49.833999999999996,29.535 -2020-08-24 11:30:00,121.33,42.687,49.833999999999996,29.535 -2020-08-24 11:45:00,120.83,43.92,49.833999999999996,29.535 -2020-08-24 12:00:00,115.38,40.277,47.832,29.535 -2020-08-24 12:15:00,118.08,39.281,47.832,29.535 -2020-08-24 12:30:00,110.62,37.626999999999995,47.832,29.535 -2020-08-24 12:45:00,109.16,37.7,47.832,29.535 -2020-08-24 13:00:00,106.38,37.482,48.03,29.535 -2020-08-24 13:15:00,100.21,36.554,48.03,29.535 -2020-08-24 13:30:00,103.29,35.159,48.03,29.535 -2020-08-24 13:45:00,105.95,35.975,48.03,29.535 -2020-08-24 14:00:00,103.3,36.626999999999995,48.157,29.535 -2020-08-24 14:15:00,99.66,36.027,48.157,29.535 -2020-08-24 14:30:00,103.17,34.923,48.157,29.535 -2020-08-24 14:45:00,110.25,36.366,48.157,29.535 -2020-08-24 15:00:00,115.96,37.335,48.897,29.535 -2020-08-24 15:15:00,112.33,34.433,48.897,29.535 -2020-08-24 15:30:00,104.1,33.501,48.897,29.535 -2020-08-24 15:45:00,99.37,31.523000000000003,48.897,29.535 -2020-08-24 16:00:00,108.59,34.434,51.446000000000005,29.535 -2020-08-24 16:15:00,110.63,34.338,51.446000000000005,29.535 -2020-08-24 16:30:00,113.83,34.496,51.446000000000005,29.535 -2020-08-24 16:45:00,113.22,31.912,51.446000000000005,29.535 -2020-08-24 17:00:00,108.34,34.234,57.507,29.535 -2020-08-24 17:15:00,106.39,34.789,57.507,29.535 -2020-08-24 17:30:00,107.38,34.6,57.507,29.535 -2020-08-24 17:45:00,112.93,34.635999999999996,57.507,29.535 -2020-08-24 18:00:00,113.44,37.981,57.896,29.535 -2020-08-24 18:15:00,110.9,36.451,57.896,29.535 -2020-08-24 18:30:00,108.43,35.108000000000004,57.896,29.535 -2020-08-24 18:45:00,106.18,38.216,57.896,29.535 -2020-08-24 19:00:00,105.21,40.431,57.891999999999996,29.535 -2020-08-24 19:15:00,109.56,39.202,57.891999999999996,29.535 -2020-08-24 19:30:00,106.94,38.328,57.891999999999996,29.535 -2020-08-24 19:45:00,101.03,37.554,57.891999999999996,29.535 -2020-08-24 20:00:00,100.29,34.311,64.57300000000001,29.535 -2020-08-24 20:15:00,98.55,35.45,64.57300000000001,29.535 -2020-08-24 20:30:00,97.25,35.505,64.57300000000001,29.535 -2020-08-24 20:45:00,101.29,35.153,64.57300000000001,29.535 -2020-08-24 21:00:00,99.14,32.809,59.431999999999995,29.535 -2020-08-24 21:15:00,93.54,35.671,59.431999999999995,29.535 -2020-08-24 21:30:00,84.27,35.83,59.431999999999995,29.535 -2020-08-24 21:45:00,83.92,35.689,59.431999999999995,29.535 -2020-08-24 22:00:00,75.13,32.900999999999996,51.519,29.535 -2020-08-24 22:15:00,79.97,35.023,51.519,29.535 -2020-08-24 22:30:00,74.93,29.565,51.519,29.535 -2020-08-24 22:45:00,75.97,26.509,51.519,29.535 -2020-08-24 23:00:00,72.01,25.331,44.501000000000005,29.535 -2020-08-24 23:15:00,73.98,22.824,44.501000000000005,29.535 -2020-08-24 23:30:00,73.76,22.38,44.501000000000005,29.535 -2020-08-24 23:45:00,72.64,21.371,44.501000000000005,29.535 -2020-08-25 00:00:00,71.72,21.445,44.522,29.535 -2020-08-25 00:15:00,71.62,22.142,44.522,29.535 -2020-08-25 00:30:00,70.03,21.316999999999997,44.522,29.535 -2020-08-25 00:45:00,71.88,21.523000000000003,44.522,29.535 -2020-08-25 01:00:00,71.19,21.183000000000003,41.441,29.535 -2020-08-25 01:15:00,72.36,20.55,41.441,29.535 -2020-08-25 01:30:00,70.95,19.538,41.441,29.535 -2020-08-25 01:45:00,71.76,19.262,41.441,29.535 -2020-08-25 02:00:00,70.7,19.179000000000002,40.203,29.535 -2020-08-25 02:15:00,69.84,18.6,40.203,29.535 -2020-08-25 02:30:00,70.82,19.628,40.203,29.535 -2020-08-25 02:45:00,70.92,20.293,40.203,29.535 -2020-08-25 03:00:00,72.33,20.983,39.536,29.535 -2020-08-25 03:15:00,72.62,20.854,39.536,29.535 -2020-08-25 03:30:00,75.29,20.564,39.536,29.535 -2020-08-25 03:45:00,77.05,20.184,39.536,29.535 -2020-08-25 04:00:00,84.41,24.456,40.759,29.535 -2020-08-25 04:15:00,89.97,29.886,40.759,29.535 -2020-08-25 04:30:00,92.14,27.509,40.759,29.535 -2020-08-25 04:45:00,92.03,27.641,40.759,29.535 -2020-08-25 05:00:00,99.0,39.819,43.623999999999995,29.535 -2020-08-25 05:15:00,107.55,46.723,43.623999999999995,29.535 -2020-08-25 05:30:00,109.53,42.256,43.623999999999995,29.535 -2020-08-25 05:45:00,114.17,39.54,43.623999999999995,29.535 -2020-08-25 06:00:00,113.12,39.746,52.684,29.535 -2020-08-25 06:15:00,114.58,40.044000000000004,52.684,29.535 -2020-08-25 06:30:00,116.2,39.524,52.684,29.535 -2020-08-25 06:45:00,116.67,41.817,52.684,29.535 -2020-08-25 07:00:00,123.26,41.85,62.676,29.535 -2020-08-25 07:15:00,127.76,43.023,62.676,29.535 -2020-08-25 07:30:00,128.42,41.568000000000005,62.676,29.535 -2020-08-25 07:45:00,121.39,42.361999999999995,62.676,29.535 -2020-08-25 08:00:00,117.12,39.391,56.161,29.535 -2020-08-25 08:15:00,112.74,41.563,56.161,29.535 -2020-08-25 08:30:00,113.5,41.963,56.161,29.535 -2020-08-25 08:45:00,116.16,43.723,56.161,29.535 -2020-08-25 09:00:00,118.01,38.784,52.132,29.535 -2020-08-25 09:15:00,126.14,37.858000000000004,52.132,29.535 -2020-08-25 09:30:00,124.08,40.582,52.132,29.535 -2020-08-25 09:45:00,118.84,43.004,52.132,29.535 -2020-08-25 10:00:00,117.53,39.493,51.032,29.535 -2020-08-25 10:15:00,121.68,40.346,51.032,29.535 -2020-08-25 10:30:00,120.94,40.177,51.032,29.535 -2020-08-25 10:45:00,113.26,41.349,51.032,29.535 -2020-08-25 11:00:00,109.61,39.652,51.085,29.535 -2020-08-25 11:15:00,117.1,40.645,51.085,29.535 -2020-08-25 11:30:00,122.66,41.457,51.085,29.535 -2020-08-25 11:45:00,124.89,42.236000000000004,51.085,29.535 -2020-08-25 12:00:00,115.6,38.455,49.049,29.535 -2020-08-25 12:15:00,113.66,37.78,49.049,29.535 -2020-08-25 12:30:00,120.05,36.912,49.049,29.535 -2020-08-25 12:45:00,118.16,37.631,49.049,29.535 -2020-08-25 13:00:00,117.18,37.067,49.722,29.535 -2020-08-25 13:15:00,108.95,37.815,49.722,29.535 -2020-08-25 13:30:00,104.14,36.343,49.722,29.535 -2020-08-25 13:45:00,104.85,36.264,49.722,29.535 -2020-08-25 14:00:00,109.46,37.303000000000004,49.565,29.535 -2020-08-25 14:15:00,108.21,36.518,49.565,29.535 -2020-08-25 14:30:00,106.98,35.711,49.565,29.535 -2020-08-25 14:45:00,101.38,36.424,49.565,29.535 -2020-08-25 15:00:00,98.49,37.22,51.108999999999995,29.535 -2020-08-25 15:15:00,103.6,35.174,51.108999999999995,29.535 -2020-08-25 15:30:00,106.69,34.051,51.108999999999995,29.535 -2020-08-25 15:45:00,107.37,32.419000000000004,51.108999999999995,29.535 -2020-08-25 16:00:00,106.02,34.625,52.725,29.535 -2020-08-25 16:15:00,105.04,34.601,52.725,29.535 -2020-08-25 16:30:00,111.46,34.395,52.725,29.535 -2020-08-25 16:45:00,113.59,32.505,52.725,29.535 -2020-08-25 17:00:00,115.04,34.974000000000004,58.031000000000006,29.535 -2020-08-25 17:15:00,109.32,35.943000000000005,58.031000000000006,29.535 -2020-08-25 17:30:00,115.28,35.338,58.031000000000006,29.535 -2020-08-25 17:45:00,115.65,35.104,58.031000000000006,29.535 -2020-08-25 18:00:00,116.18,37.575,58.338,29.535 -2020-08-25 18:15:00,111.28,37.442,58.338,29.535 -2020-08-25 18:30:00,110.08,35.876999999999995,58.338,29.535 -2020-08-25 18:45:00,108.95,38.782,58.338,29.535 -2020-08-25 19:00:00,112.98,39.938,58.464,29.535 -2020-08-25 19:15:00,115.45,38.88,58.464,29.535 -2020-08-25 19:30:00,112.47,37.827,58.464,29.535 -2020-08-25 19:45:00,107.22,37.361999999999995,58.464,29.535 -2020-08-25 20:00:00,101.92,34.48,63.708,29.535 -2020-08-25 20:15:00,107.45,34.316,63.708,29.535 -2020-08-25 20:30:00,106.27,34.363,63.708,29.535 -2020-08-25 20:45:00,103.99,34.378,63.708,29.535 -2020-08-25 21:00:00,94.4,32.854,57.06399999999999,29.535 -2020-08-25 21:15:00,95.17,34.338,57.06399999999999,29.535 -2020-08-25 21:30:00,89.29,34.676,57.06399999999999,29.535 -2020-08-25 21:45:00,87.19,34.677,57.06399999999999,29.535 -2020-08-25 22:00:00,78.39,31.979,52.831,29.535 -2020-08-25 22:15:00,81.0,33.783,52.831,29.535 -2020-08-25 22:30:00,76.91,28.561,52.831,29.535 -2020-08-25 22:45:00,80.86,25.493000000000002,52.831,29.535 -2020-08-25 23:00:00,74.5,23.654,44.717,29.535 -2020-08-25 23:15:00,72.88,22.587,44.717,29.535 -2020-08-25 23:30:00,72.05,22.16,44.717,29.535 -2020-08-25 23:45:00,72.32,21.335,44.717,29.535 -2020-08-26 00:00:00,71.27,21.596,41.263000000000005,29.535 -2020-08-26 00:15:00,72.44,22.294,41.263000000000005,29.535 -2020-08-26 00:30:00,72.68,21.475,41.263000000000005,29.535 -2020-08-26 00:45:00,73.43,21.688000000000002,41.263000000000005,29.535 -2020-08-26 01:00:00,69.7,21.326,38.448,29.535 -2020-08-26 01:15:00,71.13,20.708000000000002,38.448,29.535 -2020-08-26 01:30:00,67.88,19.711,38.448,29.535 -2020-08-26 01:45:00,71.52,19.434,38.448,29.535 -2020-08-26 02:00:00,69.26,19.352999999999998,36.471,29.535 -2020-08-26 02:15:00,69.62,18.797,36.471,29.535 -2020-08-26 02:30:00,70.17,19.801,36.471,29.535 -2020-08-26 02:45:00,71.2,20.463,36.471,29.535 -2020-08-26 03:00:00,72.48,21.142,36.042,29.535 -2020-08-26 03:15:00,72.93,21.028000000000002,36.042,29.535 -2020-08-26 03:30:00,74.6,20.743000000000002,36.042,29.535 -2020-08-26 03:45:00,77.43,20.357,36.042,29.535 -2020-08-26 04:00:00,82.27,24.653000000000002,36.705,29.535 -2020-08-26 04:15:00,88.85,30.103,36.705,29.535 -2020-08-26 04:30:00,91.76,27.734,36.705,29.535 -2020-08-26 04:45:00,93.35,27.868000000000002,36.705,29.535 -2020-08-26 05:00:00,101.37,40.118,39.716,29.535 -2020-08-26 05:15:00,108.33,47.097,39.716,29.535 -2020-08-26 05:30:00,110.12,42.623000000000005,39.716,29.535 -2020-08-26 05:45:00,111.53,39.868,39.716,29.535 -2020-08-26 06:00:00,112.68,40.042,52.756,29.535 -2020-08-26 06:15:00,116.55,40.361999999999995,52.756,29.535 -2020-08-26 06:30:00,120.01,39.836999999999996,52.756,29.535 -2020-08-26 06:45:00,121.27,42.13,52.756,29.535 -2020-08-26 07:00:00,119.57,42.163000000000004,65.977,29.535 -2020-08-26 07:15:00,114.72,43.35,65.977,29.535 -2020-08-26 07:30:00,121.57,41.919,65.977,29.535 -2020-08-26 07:45:00,118.18,42.715,65.977,29.535 -2020-08-26 08:00:00,118.81,39.744,57.927,29.535 -2020-08-26 08:15:00,116.2,41.883,57.927,29.535 -2020-08-26 08:30:00,118.47,42.278,57.927,29.535 -2020-08-26 08:45:00,117.16,44.023999999999994,57.927,29.535 -2020-08-26 09:00:00,113.77,39.093,54.86,29.535 -2020-08-26 09:15:00,109.86,38.158,54.86,29.535 -2020-08-26 09:30:00,114.74,40.864000000000004,54.86,29.535 -2020-08-26 09:45:00,115.54,43.261,54.86,29.535 -2020-08-26 10:00:00,115.19,39.749,52.818000000000005,29.535 -2020-08-26 10:15:00,109.01,40.578,52.818000000000005,29.535 -2020-08-26 10:30:00,111.28,40.4,52.818000000000005,29.535 -2020-08-26 10:45:00,114.2,41.563,52.818000000000005,29.535 -2020-08-26 11:00:00,112.86,39.876,52.937,29.535 -2020-08-26 11:15:00,109.67,40.861,52.937,29.535 -2020-08-26 11:30:00,106.25,41.67,52.937,29.535 -2020-08-26 11:45:00,111.83,42.434,52.937,29.535 -2020-08-26 12:00:00,109.68,38.638000000000005,50.826,29.535 -2020-08-26 12:15:00,109.43,37.951,50.826,29.535 -2020-08-26 12:30:00,102.87,37.102,50.826,29.535 -2020-08-26 12:45:00,108.4,37.808,50.826,29.535 -2020-08-26 13:00:00,105.9,37.226,50.556000000000004,29.535 -2020-08-26 13:15:00,105.95,37.961999999999996,50.556000000000004,29.535 -2020-08-26 13:30:00,101.59,36.486999999999995,50.556000000000004,29.535 -2020-08-26 13:45:00,99.54,36.418,50.556000000000004,29.535 -2020-08-26 14:00:00,100.94,37.431999999999995,51.188,29.535 -2020-08-26 14:15:00,105.78,36.654,51.188,29.535 -2020-08-26 14:30:00,108.59,35.864000000000004,51.188,29.535 -2020-08-26 14:45:00,109.21,36.579,51.188,29.535 -2020-08-26 15:00:00,104.05,37.327,52.976000000000006,29.535 -2020-08-26 15:15:00,107.09,35.291,52.976000000000006,29.535 -2020-08-26 15:30:00,108.02,34.181,52.976000000000006,29.535 -2020-08-26 15:45:00,108.26,32.556,52.976000000000006,29.535 -2020-08-26 16:00:00,107.58,34.734,55.463,29.535 -2020-08-26 16:15:00,106.64,34.72,55.463,29.535 -2020-08-26 16:30:00,106.57,34.516,55.463,29.535 -2020-08-26 16:45:00,108.55,32.669000000000004,55.463,29.535 -2020-08-26 17:00:00,113.88,35.105,59.435,29.535 -2020-08-26 17:15:00,118.13,36.104,59.435,29.535 -2020-08-26 17:30:00,118.87,35.510999999999996,59.435,29.535 -2020-08-26 17:45:00,113.11,35.313,59.435,29.535 -2020-08-26 18:00:00,113.43,37.775,61.387,29.535 -2020-08-26 18:15:00,112.92,37.656,61.387,29.535 -2020-08-26 18:30:00,117.05,36.104,61.387,29.535 -2020-08-26 18:45:00,113.66,39.007,61.387,29.535 -2020-08-26 19:00:00,116.87,40.169000000000004,63.323,29.535 -2020-08-26 19:15:00,114.49,39.111,63.323,29.535 -2020-08-26 19:30:00,115.09,38.061,63.323,29.535 -2020-08-26 19:45:00,114.82,37.6,63.323,29.535 -2020-08-26 20:00:00,107.23,34.726,69.083,29.535 -2020-08-26 20:15:00,102.55,34.564,69.083,29.535 -2020-08-26 20:30:00,99.74,34.594,69.083,29.535 -2020-08-26 20:45:00,99.86,34.573,69.083,29.535 -2020-08-26 21:00:00,92.47,33.052,59.957,29.535 -2020-08-26 21:15:00,89.95,34.527,59.957,29.535 -2020-08-26 21:30:00,86.5,34.865,59.957,29.535 -2020-08-26 21:45:00,86.61,34.833,59.957,29.535 -2020-08-26 22:00:00,81.97,32.114000000000004,53.821000000000005,29.535 -2020-08-26 22:15:00,83.28,33.902,53.821000000000005,29.535 -2020-08-26 22:30:00,79.53,28.649,53.821000000000005,29.535 -2020-08-26 22:45:00,84.8,25.583000000000002,53.821000000000005,29.535 -2020-08-26 23:00:00,81.23,23.787,45.458,29.535 -2020-08-26 23:15:00,79.13,22.703000000000003,45.458,29.535 -2020-08-26 23:30:00,78.99,22.287,45.458,29.535 -2020-08-26 23:45:00,78.9,21.468000000000004,45.458,29.535 -2020-08-27 00:00:00,71.33,21.749000000000002,40.36,29.535 -2020-08-27 00:15:00,73.36,22.448,40.36,29.535 -2020-08-27 00:30:00,73.38,21.634,40.36,29.535 -2020-08-27 00:45:00,73.37,21.854,40.36,29.535 -2020-08-27 01:00:00,70.34,21.473000000000003,38.552,29.535 -2020-08-27 01:15:00,71.82,20.868000000000002,38.552,29.535 -2020-08-27 01:30:00,71.13,19.885,38.552,29.535 -2020-08-27 01:45:00,71.82,19.609,38.552,29.535 -2020-08-27 02:00:00,69.67,19.53,36.895,29.535 -2020-08-27 02:15:00,71.68,18.995,36.895,29.535 -2020-08-27 02:30:00,71.41,19.977,36.895,29.535 -2020-08-27 02:45:00,72.88,20.635,36.895,29.535 -2020-08-27 03:00:00,74.73,21.303,36.565,29.535 -2020-08-27 03:15:00,73.5,21.204,36.565,29.535 -2020-08-27 03:30:00,74.7,20.924,36.565,29.535 -2020-08-27 03:45:00,77.44,20.531,36.565,29.535 -2020-08-27 04:00:00,80.24,24.855,37.263000000000005,29.535 -2020-08-27 04:15:00,84.26,30.322,37.263000000000005,29.535 -2020-08-27 04:30:00,88.93,27.961,37.263000000000005,29.535 -2020-08-27 04:45:00,92.35,28.099,37.263000000000005,29.535 -2020-08-27 05:00:00,100.04,40.422,40.412,29.535 -2020-08-27 05:15:00,101.99,47.481,40.412,29.535 -2020-08-27 05:30:00,103.56,43.0,40.412,29.535 -2020-08-27 05:45:00,108.21,40.204,40.412,29.535 -2020-08-27 06:00:00,111.6,40.345,49.825,29.535 -2020-08-27 06:15:00,115.35,40.686,49.825,29.535 -2020-08-27 06:30:00,124.04,40.156,49.825,29.535 -2020-08-27 06:45:00,126.88,42.449,49.825,29.535 -2020-08-27 07:00:00,129.89,42.482,61.082,29.535 -2020-08-27 07:15:00,127.93,43.683,61.082,29.535 -2020-08-27 07:30:00,132.59,42.276,61.082,29.535 -2020-08-27 07:45:00,134.35,43.071999999999996,61.082,29.535 -2020-08-27 08:00:00,130.7,40.103,53.961999999999996,29.535 -2020-08-27 08:15:00,129.47,42.208999999999996,53.961999999999996,29.535 -2020-08-27 08:30:00,135.31,42.599,53.961999999999996,29.535 -2020-08-27 08:45:00,130.08,44.33,53.961999999999996,29.535 -2020-08-27 09:00:00,132.01,39.407,50.06100000000001,29.535 -2020-08-27 09:15:00,137.15,38.461999999999996,50.06100000000001,29.535 -2020-08-27 09:30:00,136.56,41.151,50.06100000000001,29.535 -2020-08-27 09:45:00,138.15,43.523999999999994,50.06100000000001,29.535 -2020-08-27 10:00:00,140.11,40.01,47.68,29.535 -2020-08-27 10:15:00,137.51,40.811,47.68,29.535 -2020-08-27 10:30:00,134.24,40.628,47.68,29.535 -2020-08-27 10:45:00,138.17,41.781000000000006,47.68,29.535 -2020-08-27 11:00:00,132.89,40.105,45.93899999999999,29.535 -2020-08-27 11:15:00,126.1,41.08,45.93899999999999,29.535 -2020-08-27 11:30:00,131.52,41.886,45.93899999999999,29.535 -2020-08-27 11:45:00,134.76,42.635,45.93899999999999,29.535 -2020-08-27 12:00:00,133.16,38.823,43.648999999999994,29.535 -2020-08-27 12:15:00,130.96,38.125,43.648999999999994,29.535 -2020-08-27 12:30:00,125.67,37.294000000000004,43.648999999999994,29.535 -2020-08-27 12:45:00,134.33,37.988,43.648999999999994,29.535 -2020-08-27 13:00:00,127.85,37.389,42.801,29.535 -2020-08-27 13:15:00,124.91,38.113,42.801,29.535 -2020-08-27 13:30:00,127.6,36.634,42.801,29.535 -2020-08-27 13:45:00,132.81,36.576,42.801,29.535 -2020-08-27 14:00:00,129.82,37.563,43.24,29.535 -2020-08-27 14:15:00,126.07,36.792,43.24,29.535 -2020-08-27 14:30:00,121.33,36.021,43.24,29.535 -2020-08-27 14:45:00,126.96,36.736999999999995,43.24,29.535 -2020-08-27 15:00:00,125.31,37.437,45.04600000000001,29.535 -2020-08-27 15:15:00,123.64,35.409,45.04600000000001,29.535 -2020-08-27 15:30:00,117.09,34.314,45.04600000000001,29.535 -2020-08-27 15:45:00,113.24,32.696,45.04600000000001,29.535 -2020-08-27 16:00:00,115.1,34.846,46.568000000000005,29.535 -2020-08-27 16:15:00,117.47,34.841,46.568000000000005,29.535 -2020-08-27 16:30:00,113.23,34.639,46.568000000000005,29.535 -2020-08-27 16:45:00,113.72,32.836999999999996,46.568000000000005,29.535 -2020-08-27 17:00:00,116.96,35.239000000000004,50.618,29.535 -2020-08-27 17:15:00,113.6,36.266999999999996,50.618,29.535 -2020-08-27 17:30:00,111.58,35.686,50.618,29.535 -2020-08-27 17:45:00,112.19,35.525999999999996,50.618,29.535 -2020-08-27 18:00:00,111.35,37.977,52.806999999999995,29.535 -2020-08-27 18:15:00,110.44,37.874,52.806999999999995,29.535 -2020-08-27 18:30:00,108.59,36.330999999999996,52.806999999999995,29.535 -2020-08-27 18:45:00,108.94,39.234,52.806999999999995,29.535 -2020-08-27 19:00:00,111.09,40.403,53.464,29.535 -2020-08-27 19:15:00,108.1,39.346,53.464,29.535 -2020-08-27 19:30:00,108.89,38.298,53.464,29.535 -2020-08-27 19:45:00,104.8,37.842,53.464,29.535 -2020-08-27 20:00:00,99.49,34.976,56.753,29.535 -2020-08-27 20:15:00,100.21,34.816,56.753,29.535 -2020-08-27 20:30:00,96.74,34.83,56.753,29.535 -2020-08-27 20:45:00,94.89,34.772,56.753,29.535 -2020-08-27 21:00:00,91.41,33.254,52.506,29.535 -2020-08-27 21:15:00,90.08,34.719,52.506,29.535 -2020-08-27 21:30:00,85.55,35.056999999999995,52.506,29.535 -2020-08-27 21:45:00,85.17,34.993,52.506,29.535 -2020-08-27 22:00:00,80.57,32.251999999999995,48.163000000000004,29.535 -2020-08-27 22:15:00,82.4,34.023,48.163000000000004,29.535 -2020-08-27 22:30:00,79.2,28.738000000000003,48.163000000000004,29.535 -2020-08-27 22:45:00,79.7,25.674,48.163000000000004,29.535 -2020-08-27 23:00:00,75.57,23.923000000000002,42.379,29.535 -2020-08-27 23:15:00,76.68,22.822,42.379,29.535 -2020-08-27 23:30:00,75.21,22.416,42.379,29.535 -2020-08-27 23:45:00,74.58,21.603,42.379,29.535 -2020-08-28 00:00:00,72.43,20.281,38.505,29.535 -2020-08-28 00:15:00,71.75,21.175,38.505,29.535 -2020-08-28 00:30:00,72.78,20.615,38.505,29.535 -2020-08-28 00:45:00,72.99,21.226999999999997,38.505,29.535 -2020-08-28 01:00:00,70.23,20.479,37.004,29.535 -2020-08-28 01:15:00,72.14,19.332,37.004,29.535 -2020-08-28 01:30:00,71.17,19.017,37.004,29.535 -2020-08-28 01:45:00,72.62,18.511,37.004,29.535 -2020-08-28 02:00:00,71.55,19.285,36.098,29.535 -2020-08-28 02:15:00,72.3,18.726,36.098,29.535 -2020-08-28 02:30:00,72.07,20.434,36.098,29.535 -2020-08-28 02:45:00,72.86,20.463,36.098,29.535 -2020-08-28 03:00:00,73.05,21.838,36.561,29.535 -2020-08-28 03:15:00,75.07,20.644000000000002,36.561,29.535 -2020-08-28 03:30:00,74.45,20.151,36.561,29.535 -2020-08-28 03:45:00,79.36,20.548000000000002,36.561,29.535 -2020-08-28 04:00:00,87.62,25.022,37.355,29.535 -2020-08-28 04:15:00,89.23,29.087,37.355,29.535 -2020-08-28 04:30:00,93.88,27.601,37.355,29.535 -2020-08-28 04:45:00,94.42,27.164,37.355,29.535 -2020-08-28 05:00:00,99.29,39.138000000000005,40.285,29.535 -2020-08-28 05:15:00,100.91,47.137,40.285,29.535 -2020-08-28 05:30:00,109.34,42.821999999999996,40.285,29.535 -2020-08-28 05:45:00,112.16,39.583,40.285,29.535 -2020-08-28 06:00:00,116.49,39.891999999999996,52.378,29.535 -2020-08-28 06:15:00,113.8,40.41,52.378,29.535 -2020-08-28 06:30:00,110.25,39.828,52.378,29.535 -2020-08-28 06:45:00,110.75,42.0,52.378,29.535 -2020-08-28 07:00:00,116.02,42.622,60.891999999999996,29.535 -2020-08-28 07:15:00,113.76,44.653,60.891999999999996,29.535 -2020-08-28 07:30:00,113.32,41.505,60.891999999999996,29.535 -2020-08-28 07:45:00,116.11,42.113,60.891999999999996,29.535 -2020-08-28 08:00:00,110.53,39.859,53.652,29.535 -2020-08-28 08:15:00,110.47,42.542,53.652,29.535 -2020-08-28 08:30:00,118.74,42.833,53.652,29.535 -2020-08-28 08:45:00,118.78,44.428000000000004,53.652,29.535 -2020-08-28 09:00:00,114.6,37.416,51.456,29.535 -2020-08-28 09:15:00,109.6,38.2,51.456,29.535 -2020-08-28 09:30:00,106.67,40.25,51.456,29.535 -2020-08-28 09:45:00,107.3,42.948,51.456,29.535 -2020-08-28 10:00:00,110.6,39.293,49.4,29.535 -2020-08-28 10:15:00,108.84,39.86,49.4,29.535 -2020-08-28 10:30:00,109.67,40.172,49.4,29.535 -2020-08-28 10:45:00,104.36,41.235,49.4,29.535 -2020-08-28 11:00:00,105.31,39.806999999999995,48.773,29.535 -2020-08-28 11:15:00,104.1,39.791,48.773,29.535 -2020-08-28 11:30:00,102.59,40.239000000000004,48.773,29.535 -2020-08-28 11:45:00,100.47,40.095,48.773,29.535 -2020-08-28 12:00:00,97.94,36.64,46.033,29.535 -2020-08-28 12:15:00,100.85,35.379,46.033,29.535 -2020-08-28 12:30:00,98.11,34.671,46.033,29.535 -2020-08-28 12:45:00,98.83,34.62,46.033,29.535 -2020-08-28 13:00:00,96.79,34.536,44.38399999999999,29.535 -2020-08-28 13:15:00,98.89,35.403,44.38399999999999,29.535 -2020-08-28 13:30:00,109.7,34.647,44.38399999999999,29.535 -2020-08-28 13:45:00,107.01,34.881,44.38399999999999,29.535 -2020-08-28 14:00:00,107.86,35.121,43.162,29.535 -2020-08-28 14:15:00,106.56,34.760999999999996,43.162,29.535 -2020-08-28 14:30:00,110.38,35.385,43.162,29.535 -2020-08-28 14:45:00,113.12,35.463,43.162,29.535 -2020-08-28 15:00:00,113.59,36.05,44.91,29.535 -2020-08-28 15:15:00,109.31,33.82,44.91,29.535 -2020-08-28 15:30:00,109.09,32.246,44.91,29.535 -2020-08-28 15:45:00,113.5,31.334,44.91,29.535 -2020-08-28 16:00:00,117.73,32.688,47.489,29.535 -2020-08-28 16:15:00,114.05,33.139,47.489,29.535 -2020-08-28 16:30:00,111.88,32.772,47.489,29.535 -2020-08-28 16:45:00,110.29,30.296,47.489,29.535 -2020-08-28 17:00:00,116.92,34.25,52.047,29.535 -2020-08-28 17:15:00,116.73,35.156,52.047,29.535 -2020-08-28 17:30:00,116.42,34.746,52.047,29.535 -2020-08-28 17:45:00,110.79,34.473,52.047,29.535 -2020-08-28 18:00:00,112.92,36.928000000000004,53.306000000000004,29.535 -2020-08-28 18:15:00,115.02,35.982,53.306000000000004,29.535 -2020-08-28 18:30:00,113.25,34.352,53.306000000000004,29.535 -2020-08-28 18:45:00,112.51,37.635,53.306000000000004,29.535 -2020-08-28 19:00:00,107.58,39.59,53.516000000000005,29.535 -2020-08-28 19:15:00,106.68,39.058,53.516000000000005,29.535 -2020-08-28 19:30:00,106.55,38.073,53.516000000000005,29.535 -2020-08-28 19:45:00,110.05,36.702,53.516000000000005,29.535 -2020-08-28 20:00:00,105.55,33.705999999999996,57.88,29.535 -2020-08-28 20:15:00,103.72,34.273,57.88,29.535 -2020-08-28 20:30:00,95.59,33.826,57.88,29.535 -2020-08-28 20:45:00,90.62,32.982,57.88,29.535 -2020-08-28 21:00:00,84.68,32.658,53.32,29.535 -2020-08-28 21:15:00,83.65,35.631,53.32,29.535 -2020-08-28 21:30:00,80.52,35.832,53.32,29.535 -2020-08-28 21:45:00,82.9,35.915,53.32,29.535 -2020-08-28 22:00:00,83.63,33.01,48.074,29.535 -2020-08-28 22:15:00,83.65,34.539,48.074,29.535 -2020-08-28 22:30:00,78.8,33.479,48.074,29.535 -2020-08-28 22:45:00,76.63,31.156999999999996,48.074,29.535 -2020-08-28 23:00:00,75.72,30.994,41.306999999999995,29.535 -2020-08-28 23:15:00,75.8,28.473000000000003,41.306999999999995,29.535 -2020-08-28 23:30:00,73.3,26.433000000000003,41.306999999999995,29.535 -2020-08-28 23:45:00,66.16,25.524,41.306999999999995,29.535 -2020-08-29 00:00:00,64.44,21.593000000000004,40.227,29.423000000000002 -2020-08-29 00:15:00,71.06,21.918000000000003,40.227,29.423000000000002 -2020-08-29 00:30:00,70.31,20.924,40.227,29.423000000000002 -2020-08-29 00:45:00,70.07,20.871,40.227,29.423000000000002 -2020-08-29 01:00:00,63.31,20.348,36.303000000000004,29.423000000000002 -2020-08-29 01:15:00,64.56,19.761,36.303000000000004,29.423000000000002 -2020-08-29 01:30:00,62.06,18.733,36.303000000000004,29.423000000000002 -2020-08-29 01:45:00,61.97,19.362000000000002,36.303000000000004,29.423000000000002 -2020-08-29 02:00:00,61.2,19.239,33.849000000000004,29.423000000000002 -2020-08-29 02:15:00,65.22,17.988,33.849000000000004,29.423000000000002 -2020-08-29 02:30:00,67.93,18.965,33.849000000000004,29.423000000000002 -2020-08-29 02:45:00,67.63,19.703,33.849000000000004,29.423000000000002 -2020-08-29 03:00:00,62.88,19.814,33.149,29.423000000000002 -2020-08-29 03:15:00,60.75,18.016,33.149,29.423000000000002 -2020-08-29 03:30:00,64.25,17.847,33.149,29.423000000000002 -2020-08-29 03:45:00,69.62,19.58,33.149,29.423000000000002 -2020-08-29 04:00:00,70.43,22.421999999999997,32.501,29.423000000000002 -2020-08-29 04:15:00,67.71,25.729,32.501,29.423000000000002 -2020-08-29 04:30:00,64.26,22.76,32.501,29.423000000000002 -2020-08-29 04:45:00,66.86,22.581,32.501,29.423000000000002 -2020-08-29 05:00:00,72.03,27.261,31.648000000000003,29.423000000000002 -2020-08-29 05:15:00,74.87,25.631,31.648000000000003,29.423000000000002 -2020-08-29 05:30:00,74.46,22.678,31.648000000000003,29.423000000000002 -2020-08-29 05:45:00,72.84,23.379,31.648000000000003,29.423000000000002 -2020-08-29 06:00:00,73.13,33.579,32.552,29.423000000000002 -2020-08-29 06:15:00,77.89,40.993,32.552,29.423000000000002 -2020-08-29 06:30:00,78.78,37.895,32.552,29.423000000000002 -2020-08-29 06:45:00,77.89,37.258,32.552,29.423000000000002 -2020-08-29 07:00:00,77.07,36.72,35.181999999999995,29.423000000000002 -2020-08-29 07:15:00,78.08,37.609,35.181999999999995,29.423000000000002 -2020-08-29 07:30:00,79.75,35.816,35.181999999999995,29.423000000000002 -2020-08-29 07:45:00,82.27,37.204,35.181999999999995,29.423000000000002 -2020-08-29 08:00:00,84.1,35.546,40.35,29.423000000000002 -2020-08-29 08:15:00,84.31,37.943000000000005,40.35,29.423000000000002 -2020-08-29 08:30:00,85.25,38.193000000000005,40.35,29.423000000000002 -2020-08-29 08:45:00,85.89,40.580999999999996,40.35,29.423000000000002 -2020-08-29 09:00:00,89.71,36.368,42.292,29.423000000000002 -2020-08-29 09:15:00,89.66,37.569,42.292,29.423000000000002 -2020-08-29 09:30:00,85.99,40.035,42.292,29.423000000000002 -2020-08-29 09:45:00,80.14,42.298,42.292,29.423000000000002 -2020-08-29 10:00:00,80.79,39.221,40.084,29.423000000000002 -2020-08-29 10:15:00,88.58,40.082,40.084,29.423000000000002 -2020-08-29 10:30:00,90.14,40.073,40.084,29.423000000000002 -2020-08-29 10:45:00,90.37,40.782,40.084,29.423000000000002 -2020-08-29 11:00:00,89.97,39.248000000000005,36.966,29.423000000000002 -2020-08-29 11:15:00,91.84,40.025999999999996,36.966,29.423000000000002 -2020-08-29 11:30:00,89.82,40.753,36.966,29.423000000000002 -2020-08-29 11:45:00,88.72,41.224,36.966,29.423000000000002 -2020-08-29 12:00:00,86.32,38.188,35.19,29.423000000000002 -2020-08-29 12:15:00,84.51,37.68,35.19,29.423000000000002 -2020-08-29 12:30:00,82.92,36.863,35.19,29.423000000000002 -2020-08-29 12:45:00,85.5,37.486,35.19,29.423000000000002 -2020-08-29 13:00:00,81.37,36.576,32.277,29.423000000000002 -2020-08-29 13:15:00,82.13,36.982,32.277,29.423000000000002 -2020-08-29 13:30:00,81.68,36.403,32.277,29.423000000000002 -2020-08-29 13:45:00,81.28,35.406,32.277,29.423000000000002 -2020-08-29 14:00:00,79.6,35.664,31.436999999999998,29.423000000000002 -2020-08-29 14:15:00,79.02,34.179,31.436999999999998,29.423000000000002 -2020-08-29 14:30:00,76.91,34.474000000000004,31.436999999999998,29.423000000000002 -2020-08-29 14:45:00,76.5,34.986999999999995,31.436999999999998,29.423000000000002 -2020-08-29 15:00:00,76.4,35.93,33.493,29.423000000000002 -2020-08-29 15:15:00,77.73,34.336,33.493,29.423000000000002 -2020-08-29 15:30:00,78.14,32.926,33.493,29.423000000000002 -2020-08-29 15:45:00,77.73,31.178,33.493,29.423000000000002 -2020-08-29 16:00:00,78.32,34.436,36.593,29.423000000000002 -2020-08-29 16:15:00,79.7,34.065,36.593,29.423000000000002 -2020-08-29 16:30:00,80.81,33.906,36.593,29.423000000000002 -2020-08-29 16:45:00,82.06,31.432,36.593,29.423000000000002 -2020-08-29 17:00:00,84.59,34.291,42.049,29.423000000000002 -2020-08-29 17:15:00,86.73,33.227,42.049,29.423000000000002 -2020-08-29 17:30:00,85.87,32.711,42.049,29.423000000000002 -2020-08-29 17:45:00,83.17,32.935,42.049,29.423000000000002 -2020-08-29 18:00:00,84.18,36.61,43.755,29.423000000000002 -2020-08-29 18:15:00,83.2,37.168,43.755,29.423000000000002 -2020-08-29 18:30:00,83.27,36.759,43.755,29.423000000000002 -2020-08-29 18:45:00,84.31,36.943000000000005,43.755,29.423000000000002 -2020-08-29 19:00:00,89.05,37.398,44.492,29.423000000000002 -2020-08-29 19:15:00,84.66,35.963,44.492,29.423000000000002 -2020-08-29 19:30:00,86.21,35.665,44.492,29.423000000000002 -2020-08-29 19:45:00,81.85,35.891999999999996,44.492,29.423000000000002 -2020-08-29 20:00:00,77.43,33.655,40.896,29.423000000000002 -2020-08-29 20:15:00,77.44,33.62,40.896,29.423000000000002 -2020-08-29 20:30:00,75.73,32.365,40.896,29.423000000000002 -2020-08-29 20:45:00,76.44,33.168,40.896,29.423000000000002 -2020-08-29 21:00:00,73.3,31.482,39.056,29.423000000000002 -2020-08-29 21:15:00,72.72,34.125,39.056,29.423000000000002 -2020-08-29 21:30:00,70.23,34.483000000000004,39.056,29.423000000000002 -2020-08-29 21:45:00,69.8,34.045,39.056,29.423000000000002 -2020-08-29 22:00:00,66.22,31.079,38.478,29.423000000000002 -2020-08-29 22:15:00,65.01,32.786,38.478,29.423000000000002 -2020-08-29 22:30:00,63.02,31.276999999999997,38.478,29.423000000000002 -2020-08-29 22:45:00,61.99,29.272,38.478,29.423000000000002 -2020-08-29 23:00:00,58.73,28.525,32.953,29.423000000000002 -2020-08-29 23:15:00,57.9,26.384,32.953,29.423000000000002 -2020-08-29 23:30:00,57.04,26.589000000000002,32.953,29.423000000000002 -2020-08-29 23:45:00,56.91,26.037,32.953,29.423000000000002 -2020-08-30 00:00:00,54.27,22.994,28.584,29.423000000000002 -2020-08-30 00:15:00,55.13,22.281999999999996,28.584,29.423000000000002 -2020-08-30 00:30:00,50.94,21.165,28.584,29.423000000000002 -2020-08-30 00:45:00,53.35,21.011,28.584,29.423000000000002 -2020-08-30 01:00:00,51.34,20.706999999999997,26.419,29.423000000000002 -2020-08-30 01:15:00,52.9,20.012999999999998,26.419,29.423000000000002 -2020-08-30 01:30:00,52.18,18.868,26.419,29.423000000000002 -2020-08-30 01:45:00,51.93,19.159000000000002,26.419,29.423000000000002 -2020-08-30 02:00:00,52.18,19.118,25.335,29.423000000000002 -2020-08-30 02:15:00,51.62,18.54,25.335,29.423000000000002 -2020-08-30 02:30:00,51.01,19.651,25.335,29.423000000000002 -2020-08-30 02:45:00,50.76,20.117,25.335,29.423000000000002 -2020-08-30 03:00:00,51.01,20.8,24.805,29.423000000000002 -2020-08-30 03:15:00,51.32,19.261,24.805,29.423000000000002 -2020-08-30 03:30:00,52.27,18.414,24.805,29.423000000000002 -2020-08-30 03:45:00,53.2,19.423,24.805,29.423000000000002 -2020-08-30 04:00:00,53.28,22.268,25.772,29.423000000000002 -2020-08-30 04:15:00,53.79,25.218000000000004,25.772,29.423000000000002 -2020-08-30 04:30:00,54.34,23.467,25.772,29.423000000000002 -2020-08-30 04:45:00,55.42,22.866,25.772,29.423000000000002 -2020-08-30 05:00:00,56.16,27.819000000000003,25.971999999999998,29.423000000000002 -2020-08-30 05:15:00,56.99,25.605999999999998,25.971999999999998,29.423000000000002 -2020-08-30 05:30:00,56.61,22.284000000000002,25.971999999999998,29.423000000000002 -2020-08-30 05:45:00,57.04,22.694000000000003,25.971999999999998,29.423000000000002 -2020-08-30 06:00:00,58.54,30.746,26.026,29.423000000000002 -2020-08-30 06:15:00,59.88,38.948,26.026,29.423000000000002 -2020-08-30 06:30:00,61.66,35.231,26.026,29.423000000000002 -2020-08-30 06:45:00,63.97,33.71,26.026,29.423000000000002 -2020-08-30 07:00:00,66.86,33.422,27.396,29.423000000000002 -2020-08-30 07:15:00,68.13,32.858000000000004,27.396,29.423000000000002 -2020-08-30 07:30:00,70.07,32.260999999999996,27.396,29.423000000000002 -2020-08-30 07:45:00,71.87,33.628,27.396,29.423000000000002 -2020-08-30 08:00:00,74.54,32.635,30.791999999999998,29.423000000000002 -2020-08-30 08:15:00,76.67,36.064,30.791999999999998,29.423000000000002 -2020-08-30 08:30:00,77.37,37.067,30.791999999999998,29.423000000000002 -2020-08-30 08:45:00,78.29,39.330999999999996,30.791999999999998,29.423000000000002 -2020-08-30 09:00:00,78.71,35.037,32.482,29.423000000000002 -2020-08-30 09:15:00,79.95,35.771,32.482,29.423000000000002 -2020-08-30 09:30:00,80.79,38.611999999999995,32.482,29.423000000000002 -2020-08-30 09:45:00,83.14,41.757,32.482,29.423000000000002 -2020-08-30 10:00:00,84.85,39.117,31.951,29.423000000000002 -2020-08-30 10:15:00,87.31,40.061,31.951,29.423000000000002 -2020-08-30 10:30:00,88.3,40.231,31.951,29.423000000000002 -2020-08-30 10:45:00,87.52,41.902,31.951,29.423000000000002 -2020-08-30 11:00:00,84.25,40.043,33.619,29.423000000000002 -2020-08-30 11:15:00,83.28,40.399,33.619,29.423000000000002 -2020-08-30 11:30:00,81.94,41.626000000000005,33.619,29.423000000000002 -2020-08-30 11:45:00,80.98,42.297,33.619,29.423000000000002 -2020-08-30 12:00:00,78.49,40.249,30.975,29.423000000000002 -2020-08-30 12:15:00,77.41,39.084,30.975,29.423000000000002 -2020-08-30 12:30:00,75.82,38.535,30.975,29.423000000000002 -2020-08-30 12:45:00,78.16,38.595,30.975,29.423000000000002 -2020-08-30 13:00:00,72.08,37.405,27.956999999999997,29.423000000000002 -2020-08-30 13:15:00,72.23,37.171,27.956999999999997,29.423000000000002 -2020-08-30 13:30:00,71.97,35.578,27.956999999999997,29.423000000000002 -2020-08-30 13:45:00,72.4,35.635,27.956999999999997,29.423000000000002 -2020-08-30 14:00:00,70.92,36.957,25.555999999999997,29.423000000000002 -2020-08-30 14:15:00,70.88,35.834,25.555999999999997,29.423000000000002 -2020-08-30 14:30:00,70.54,34.972,25.555999999999997,29.423000000000002 -2020-08-30 14:45:00,70.65,34.519,25.555999999999997,29.423000000000002 -2020-08-30 15:00:00,71.32,35.598,26.271,29.423000000000002 -2020-08-30 15:15:00,71.25,33.243,26.271,29.423000000000002 -2020-08-30 15:30:00,71.19,31.660999999999998,26.271,29.423000000000002 -2020-08-30 15:45:00,71.97,30.158,26.271,29.423000000000002 -2020-08-30 16:00:00,73.4,31.854,30.369,29.423000000000002 -2020-08-30 16:15:00,74.06,31.729,30.369,29.423000000000002 -2020-08-30 16:30:00,75.86,32.481,30.369,29.423000000000002 -2020-08-30 16:45:00,78.3,30.105999999999998,30.369,29.423000000000002 -2020-08-30 17:00:00,79.85,33.259,38.787,29.423000000000002 -2020-08-30 17:15:00,80.1,33.619,38.787,29.423000000000002 -2020-08-30 17:30:00,81.58,33.845,38.787,29.423000000000002 -2020-08-30 17:45:00,80.78,34.421,38.787,29.423000000000002 -2020-08-30 18:00:00,83.14,38.659,41.886,29.423000000000002 -2020-08-30 18:15:00,80.9,38.830999999999996,41.886,29.423000000000002 -2020-08-30 18:30:00,80.98,38.26,41.886,29.423000000000002 -2020-08-30 18:45:00,80.43,38.503,41.886,29.423000000000002 -2020-08-30 19:00:00,86.05,40.954,42.91,29.423000000000002 -2020-08-30 19:15:00,82.33,38.507,42.91,29.423000000000002 -2020-08-30 19:30:00,83.91,37.993,42.91,29.423000000000002 -2020-08-30 19:45:00,80.57,37.784,42.91,29.423000000000002 -2020-08-30 20:00:00,78.83,35.695,42.148999999999994,29.423000000000002 -2020-08-30 20:15:00,79.05,35.516,42.148999999999994,29.423000000000002 -2020-08-30 20:30:00,78.41,34.949,42.148999999999994,29.423000000000002 -2020-08-30 20:45:00,79.19,34.219,42.148999999999994,29.423000000000002 -2020-08-30 21:00:00,77.65,32.483000000000004,40.955999999999996,29.423000000000002 -2020-08-30 21:15:00,78.79,34.856,40.955999999999996,29.423000000000002 -2020-08-30 21:30:00,75.78,34.623000000000005,40.955999999999996,29.423000000000002 -2020-08-30 21:45:00,75.79,34.472,40.955999999999996,29.423000000000002 -2020-08-30 22:00:00,72.02,33.349000000000004,39.873000000000005,29.423000000000002 -2020-08-30 22:15:00,72.93,33.534,39.873000000000005,29.423000000000002 -2020-08-30 22:30:00,71.7,31.603,39.873000000000005,29.423000000000002 -2020-08-30 22:45:00,70.88,28.467,39.873000000000005,29.423000000000002 -2020-08-30 23:00:00,66.13,27.553,35.510999999999996,29.423000000000002 -2020-08-30 23:15:00,67.65,26.502,35.510999999999996,29.423000000000002 -2020-08-30 23:30:00,68.19,26.213,35.510999999999996,29.423000000000002 -2020-08-30 23:45:00,66.64,25.791,35.510999999999996,29.423000000000002 -2020-08-31 00:00:00,63.09,24.439,33.475,29.535 -2020-08-31 00:15:00,65.41,24.381,33.475,29.535 -2020-08-31 00:30:00,65.2,22.921,33.475,29.535 -2020-08-31 00:45:00,65.56,22.427,33.475,29.535 -2020-08-31 01:00:00,62.92,22.448,33.111,29.535 -2020-08-31 01:15:00,64.41,21.793000000000003,33.111,29.535 -2020-08-31 01:30:00,64.04,20.988000000000003,33.111,29.535 -2020-08-31 01:45:00,65.01,21.19,33.111,29.535 -2020-08-31 02:00:00,62.9,21.559,32.358000000000004,29.535 -2020-08-31 02:15:00,64.34,20.055999999999997,32.358000000000004,29.535 -2020-08-31 02:30:00,64.2,21.287,32.358000000000004,29.535 -2020-08-31 02:45:00,64.04,21.629,32.358000000000004,29.535 -2020-08-31 03:00:00,65.64,22.729,30.779,29.535 -2020-08-31 03:15:00,66.35,21.822,30.779,29.535 -2020-08-31 03:30:00,72.4,21.601999999999997,30.779,29.535 -2020-08-31 03:45:00,75.77,22.195,30.779,29.535 -2020-08-31 04:00:00,80.59,27.787,31.416,29.535 -2020-08-31 04:15:00,79.36,33.317,31.416,29.535 -2020-08-31 04:30:00,88.89,31.094,31.416,29.535 -2020-08-31 04:45:00,94.27,30.829,31.416,29.535 -2020-08-31 05:00:00,100.31,42.129,37.221,29.535 -2020-08-31 05:15:00,101.92,48.968,37.221,29.535 -2020-08-31 05:30:00,101.22,44.25,37.221,29.535 -2020-08-31 05:45:00,104.81,41.94,37.221,29.535 -2020-08-31 06:00:00,108.02,40.968,51.891000000000005,29.535 -2020-08-31 06:15:00,111.44,41.255,51.891000000000005,29.535 -2020-08-31 06:30:00,112.93,40.977,51.891000000000005,29.535 -2020-08-31 06:45:00,115.62,44.062,51.891000000000005,29.535 -2020-08-31 07:00:00,119.0,43.956,62.282,29.535 -2020-08-31 07:15:00,119.27,45.438,62.282,29.535 -2020-08-31 07:30:00,120.08,44.106,62.282,29.535 -2020-08-31 07:45:00,119.77,45.791000000000004,62.282,29.535 -2020-08-31 08:00:00,121.76,42.894,54.102,29.535 -2020-08-31 08:15:00,123.35,45.245,54.102,29.535 -2020-08-31 08:30:00,124.12,45.458999999999996,54.102,29.535 -2020-08-31 08:45:00,125.71,47.97,54.102,29.535 -2020-08-31 09:00:00,129.33,42.748999999999995,50.917,29.535 -2020-08-31 09:15:00,126.56,41.933,50.917,29.535 -2020-08-31 09:30:00,126.46,43.938,50.917,29.535 -2020-08-31 09:45:00,127.36,44.971000000000004,50.917,29.535 -2020-08-31 10:00:00,128.24,42.68899999999999,49.718999999999994,29.535 -2020-08-31 10:15:00,131.55,43.481,49.718999999999994,29.535 -2020-08-31 10:30:00,130.34,43.248000000000005,49.718999999999994,29.535 -2020-08-31 10:45:00,123.89,43.443000000000005,49.718999999999994,29.535 -2020-08-31 11:00:00,124.43,41.855,49.833999999999996,29.535 -2020-08-31 11:15:00,124.83,42.403,49.833999999999996,29.535 -2020-08-31 11:30:00,129.95,44.224,49.833999999999996,29.535 -2020-08-31 11:45:00,125.36,45.352,49.833999999999996,29.535 -2020-08-31 12:00:00,121.07,41.592,47.832,29.535 -2020-08-31 12:15:00,122.98,40.518,47.832,29.535 -2020-08-31 12:30:00,117.12,38.993,47.832,29.535 -2020-08-31 12:45:00,116.46,38.983000000000004,47.832,29.535 -2020-08-31 13:00:00,120.35,38.641,48.03,29.535 -2020-08-31 13:15:00,120.22,37.631,48.03,29.535 -2020-08-31 13:30:00,118.44,36.205,48.03,29.535 -2020-08-31 13:45:00,111.87,37.094,48.03,29.535 -2020-08-31 14:00:00,111.57,37.562,48.157,29.535 -2020-08-31 14:15:00,114.04,37.012,48.157,29.535 -2020-08-31 14:30:00,113.71,36.038000000000004,48.157,29.535 -2020-08-31 14:45:00,116.13,37.49,48.157,29.535 -2020-08-31 15:00:00,113.44,38.115,48.897,29.535 -2020-08-31 15:15:00,111.84,35.278,48.897,29.535 -2020-08-31 15:30:00,110.24,34.448,48.897,29.535 -2020-08-31 15:45:00,108.51,32.522,48.897,29.535 -2020-08-31 16:00:00,104.99,35.231,51.446000000000005,29.535 -2020-08-31 16:15:00,104.55,35.198,51.446000000000005,29.535 -2020-08-31 16:30:00,108.33,35.369,51.446000000000005,29.535 -2020-08-31 16:45:00,108.42,33.097,51.446000000000005,29.535 -2020-08-31 17:00:00,109.57,35.18,57.507,29.535 -2020-08-31 17:15:00,108.31,35.944,57.507,29.535 -2020-08-31 17:30:00,111.74,35.846,57.507,29.535 -2020-08-31 17:45:00,110.77,36.141999999999996,57.507,29.535 -2020-08-31 18:00:00,113.05,39.413000000000004,57.896,29.535 -2020-08-31 18:15:00,109.66,37.992,57.896,29.535 -2020-08-31 18:30:00,110.56,36.729,57.896,29.535 -2020-08-31 18:45:00,108.24,39.83,57.896,29.535 -2020-08-31 19:00:00,109.87,42.091,57.891999999999996,29.535 -2020-08-31 19:15:00,105.78,40.868,57.891999999999996,29.535 -2020-08-31 19:30:00,103.3,40.012,57.891999999999996,29.535 -2020-08-31 19:45:00,105.3,39.27,57.891999999999996,29.535 -2020-08-31 20:00:00,98.65,36.086999999999996,64.57300000000001,29.535 -2020-08-31 20:15:00,99.73,37.242,64.57300000000001,29.535 -2020-08-31 20:30:00,98.19,37.177,64.57300000000001,29.535 -2020-08-31 20:45:00,98.28,36.565,64.57300000000001,29.535 -2020-08-31 21:00:00,93.39,34.244,59.431999999999995,29.535 -2020-08-31 21:15:00,93.97,37.034,59.431999999999995,29.535 -2020-08-31 21:30:00,89.46,37.196999999999996,59.431999999999995,29.535 -2020-08-31 21:45:00,86.99,36.826,59.431999999999995,29.535 -2020-08-31 22:00:00,86.09,33.882,51.519,29.535 -2020-08-31 22:15:00,89.99,35.885999999999996,51.519,29.535 -2020-08-31 22:30:00,88.27,30.206,51.519,29.535 -2020-08-31 22:45:00,83.31,27.164,51.519,29.535 -2020-08-31 23:00:00,75.5,26.299,44.501000000000005,29.535 -2020-08-31 23:15:00,75.51,23.673000000000002,44.501000000000005,29.535 -2020-08-31 23:30:00,75.27,23.296999999999997,44.501000000000005,29.535 -2020-08-31 23:45:00,75.09,22.335,44.501000000000005,29.535 -2020-09-01 00:00:00,73.47,29.916999999999998,44.438,29.93 -2020-09-01 00:15:00,81.26,30.489,44.438,29.93 -2020-09-01 00:30:00,72.65,30.061,44.438,29.93 -2020-09-01 00:45:00,74.24,30.002,44.438,29.93 -2020-09-01 01:00:00,75.98,29.759,41.468999999999994,29.93 -2020-09-01 01:15:00,79.49,28.94,41.468999999999994,29.93 -2020-09-01 01:30:00,80.77,27.840999999999998,41.468999999999994,29.93 -2020-09-01 01:45:00,80.77,26.919,41.468999999999994,29.93 -2020-09-01 02:00:00,76.57,26.625,39.708,29.93 -2020-09-01 02:15:00,74.41,26.226,39.708,29.93 -2020-09-01 02:30:00,73.57,26.895,39.708,29.93 -2020-09-01 02:45:00,75.06,27.743000000000002,39.708,29.93 -2020-09-01 03:00:00,75.97,28.845,38.919000000000004,29.93 -2020-09-01 03:15:00,76.67,28.206,38.919000000000004,29.93 -2020-09-01 03:30:00,77.0,28.11,38.919000000000004,29.93 -2020-09-01 03:45:00,80.42,28.19,38.919000000000004,29.93 -2020-09-01 04:00:00,83.6,34.74,40.092,29.93 -2020-09-01 04:15:00,85.52,41.981,40.092,29.93 -2020-09-01 04:30:00,93.85,39.315,40.092,29.93 -2020-09-01 04:45:00,102.83,39.861999999999995,40.092,29.93 -2020-09-01 05:00:00,110.21,57.162,43.713,29.93 -2020-09-01 05:15:00,108.06,68.223,43.713,29.93 -2020-09-01 05:30:00,106.25,62.226000000000006,43.713,29.93 -2020-09-01 05:45:00,108.65,58.465,43.713,29.93 -2020-09-01 06:00:00,117.46,58.838,56.033,29.93 -2020-09-01 06:15:00,114.57,60.111000000000004,56.033,29.93 -2020-09-01 06:30:00,114.76,58.723,56.033,29.93 -2020-09-01 06:45:00,117.5,60.365,56.033,29.93 -2020-09-01 07:00:00,120.97,58.69,66.003,29.93 -2020-09-01 07:15:00,121.58,59.825,66.003,29.93 -2020-09-01 07:30:00,122.12,58.056000000000004,66.003,29.93 -2020-09-01 07:45:00,122.42,58.925,66.003,29.93 -2020-09-01 08:00:00,123.81,57.608999999999995,57.474,29.93 -2020-09-01 08:15:00,119.75,59.684,57.474,29.93 -2020-09-01 08:30:00,121.34,58.928999999999995,57.474,29.93 -2020-09-01 08:45:00,119.7,60.385,57.474,29.93 -2020-09-01 09:00:00,119.63,56.747,51.928000000000004,29.93 -2020-09-01 09:15:00,114.95,55.32899999999999,51.928000000000004,29.93 -2020-09-01 09:30:00,111.22,57.449,51.928000000000004,29.93 -2020-09-01 09:45:00,112.13,59.055,51.928000000000004,29.93 -2020-09-01 10:00:00,110.13,55.532,49.46,29.93 -2020-09-01 10:15:00,111.52,56.278999999999996,49.46,29.93 -2020-09-01 10:30:00,112.58,56.043,49.46,29.93 -2020-09-01 10:45:00,113.39,57.13399999999999,49.46,29.93 -2020-09-01 11:00:00,106.2,53.576,48.206,29.93 -2020-09-01 11:15:00,104.73,54.661,48.206,29.93 -2020-09-01 11:30:00,106.63,55.326,48.206,29.93 -2020-09-01 11:45:00,108.22,56.138000000000005,48.206,29.93 -2020-09-01 12:00:00,105.4,52.413999999999994,46.285,29.93 -2020-09-01 12:15:00,110.05,52.071000000000005,46.285,29.93 -2020-09-01 12:30:00,108.08,51.176,46.285,29.93 -2020-09-01 12:45:00,105.77,52.277,46.285,29.93 -2020-09-01 13:00:00,110.03,51.369,46.861999999999995,29.93 -2020-09-01 13:15:00,106.07,51.993,46.861999999999995,29.93 -2020-09-01 13:30:00,107.01,50.818000000000005,46.861999999999995,29.93 -2020-09-01 13:45:00,108.05,49.903999999999996,46.861999999999995,29.93 -2020-09-01 14:00:00,104.78,51.108000000000004,46.488,29.93 -2020-09-01 14:15:00,107.92,50.152,46.488,29.93 -2020-09-01 14:30:00,103.2,48.902,46.488,29.93 -2020-09-01 14:45:00,103.58,49.669,46.488,29.93 -2020-09-01 15:00:00,101.89,50.393,48.442,29.93 -2020-09-01 15:15:00,103.13,48.303000000000004,48.442,29.93 -2020-09-01 15:30:00,103.38,47.102,48.442,29.93 -2020-09-01 15:45:00,102.77,44.833999999999996,48.442,29.93 -2020-09-01 16:00:00,102.66,47.282,50.397,29.93 -2020-09-01 16:15:00,105.53,47.086000000000006,50.397,29.93 -2020-09-01 16:30:00,106.51,47.457,50.397,29.93 -2020-09-01 16:45:00,108.89,45.013999999999996,50.397,29.93 -2020-09-01 17:00:00,109.84,46.915,56.668,29.93 -2020-09-01 17:15:00,109.18,47.858000000000004,56.668,29.93 -2020-09-01 17:30:00,109.98,47.298,56.668,29.93 -2020-09-01 17:45:00,112.11,46.278999999999996,56.668,29.93 -2020-09-01 18:00:00,113.7,48.562,57.957,29.93 -2020-09-01 18:15:00,109.54,48.016999999999996,57.957,29.93 -2020-09-01 18:30:00,111.45,46.04,57.957,29.93 -2020-09-01 18:45:00,109.99,50.2,57.957,29.93 -2020-09-01 19:00:00,111.38,52.652,57.056000000000004,29.93 -2020-09-01 19:15:00,107.84,51.383,57.056000000000004,29.93 -2020-09-01 19:30:00,107.01,50.34,57.056000000000004,29.93 -2020-09-01 19:45:00,106.18,50.461999999999996,57.056000000000004,29.93 -2020-09-01 20:00:00,100.69,48.705,64.156,29.93 -2020-09-01 20:15:00,100.76,48.184,64.156,29.93 -2020-09-01 20:30:00,98.36,47.782,64.156,29.93 -2020-09-01 20:45:00,97.15,47.833,64.156,29.93 -2020-09-01 21:00:00,91.14,46.413000000000004,56.507,29.93 -2020-09-01 21:15:00,91.05,48.013000000000005,56.507,29.93 -2020-09-01 21:30:00,87.36,47.721000000000004,56.507,29.93 -2020-09-01 21:45:00,85.97,47.453,56.507,29.93 -2020-09-01 22:00:00,81.23,45.082,50.728,29.93 -2020-09-01 22:15:00,80.37,45.82899999999999,50.728,29.93 -2020-09-01 22:30:00,79.33,38.537,50.728,29.93 -2020-09-01 22:45:00,78.66,35.022,50.728,29.93 -2020-09-01 23:00:00,74.04,31.743000000000002,43.556999999999995,29.93 -2020-09-01 23:15:00,74.63,31.009,43.556999999999995,29.93 -2020-09-01 23:30:00,74.95,30.879,43.556999999999995,29.93 -2020-09-01 23:45:00,73.76,30.048000000000002,43.556999999999995,29.93 -2020-09-02 00:00:00,70.09,30.121,41.151,29.93 -2020-09-02 00:15:00,71.22,30.691,41.151,29.93 -2020-09-02 00:30:00,70.52,30.272,41.151,29.93 -2020-09-02 00:45:00,70.04,30.22,41.151,29.93 -2020-09-02 01:00:00,70.4,29.961,37.763000000000005,29.93 -2020-09-02 01:15:00,71.55,29.159000000000002,37.763000000000005,29.93 -2020-09-02 01:30:00,70.54,28.076999999999998,37.763000000000005,29.93 -2020-09-02 01:45:00,71.31,27.156,37.763000000000005,29.93 -2020-09-02 02:00:00,70.05,26.865,35.615,29.93 -2020-09-02 02:15:00,70.33,26.489,35.615,29.93 -2020-09-02 02:30:00,70.42,27.131999999999998,35.615,29.93 -2020-09-02 02:45:00,71.01,27.975,35.615,29.93 -2020-09-02 03:00:00,71.66,29.066,35.153,29.93 -2020-09-02 03:15:00,73.29,28.445,35.153,29.93 -2020-09-02 03:30:00,74.2,28.353,35.153,29.93 -2020-09-02 03:45:00,76.12,28.423000000000002,35.153,29.93 -2020-09-02 04:00:00,79.71,35.003,36.203,29.93 -2020-09-02 04:15:00,83.25,42.268,36.203,29.93 -2020-09-02 04:30:00,93.94,39.609,36.203,29.93 -2020-09-02 04:45:00,98.95,40.161,36.203,29.93 -2020-09-02 05:00:00,102.22,57.54600000000001,39.922,29.93 -2020-09-02 05:15:00,99.73,68.696,39.922,29.93 -2020-09-02 05:30:00,104.42,62.69,39.922,29.93 -2020-09-02 05:45:00,106.12,58.883,39.922,29.93 -2020-09-02 06:00:00,110.69,59.221000000000004,56.443999999999996,29.93 -2020-09-02 06:15:00,110.55,60.515,56.443999999999996,29.93 -2020-09-02 06:30:00,112.02,59.126999999999995,56.443999999999996,29.93 -2020-09-02 06:45:00,111.67,60.766000000000005,56.443999999999996,29.93 -2020-09-02 07:00:00,114.69,59.091,68.683,29.93 -2020-09-02 07:15:00,114.16,60.243,68.683,29.93 -2020-09-02 07:30:00,112.53,58.504,68.683,29.93 -2020-09-02 07:45:00,112.99,59.375,68.683,29.93 -2020-09-02 08:00:00,112.66,58.06399999999999,59.003,29.93 -2020-09-02 08:15:00,112.55,60.106,59.003,29.93 -2020-09-02 08:30:00,111.76,59.354,59.003,29.93 -2020-09-02 08:45:00,108.42,60.795,59.003,29.93 -2020-09-02 09:00:00,107.65,57.165,56.21,29.93 -2020-09-02 09:15:00,106.69,55.736000000000004,56.21,29.93 -2020-09-02 09:30:00,106.79,57.836000000000006,56.21,29.93 -2020-09-02 09:45:00,106.64,59.413999999999994,56.21,29.93 -2020-09-02 10:00:00,106.81,55.888999999999996,52.358999999999995,29.93 -2020-09-02 10:15:00,107.49,56.603,52.358999999999995,29.93 -2020-09-02 10:30:00,104.67,56.357,52.358999999999995,29.93 -2020-09-02 10:45:00,104.68,57.435,52.358999999999995,29.93 -2020-09-02 11:00:00,102.67,53.89,51.161,29.93 -2020-09-02 11:15:00,101.54,54.961000000000006,51.161,29.93 -2020-09-02 11:30:00,105.83,55.623999999999995,51.161,29.93 -2020-09-02 11:45:00,103.98,56.419,51.161,29.93 -2020-09-02 12:00:00,99.78,52.676,49.119,29.93 -2020-09-02 12:15:00,101.12,52.318999999999996,49.119,29.93 -2020-09-02 12:30:00,101.87,51.45,49.119,29.93 -2020-09-02 12:45:00,104.35,52.54,49.119,29.93 -2020-09-02 13:00:00,100.22,51.608000000000004,49.187,29.93 -2020-09-02 13:15:00,101.85,52.223,49.187,29.93 -2020-09-02 13:30:00,99.0,51.04600000000001,49.187,29.93 -2020-09-02 13:45:00,102.96,50.141999999999996,49.187,29.93 -2020-09-02 14:00:00,102.01,51.306999999999995,49.787,29.93 -2020-09-02 14:15:00,102.18,50.363,49.787,29.93 -2020-09-02 14:30:00,100.6,49.137,49.787,29.93 -2020-09-02 14:45:00,103.38,49.903,49.787,29.93 -2020-09-02 15:00:00,106.74,50.576,51.458999999999996,29.93 -2020-09-02 15:15:00,101.18,48.5,51.458999999999996,29.93 -2020-09-02 15:30:00,102.11,47.323,51.458999999999996,29.93 -2020-09-02 15:45:00,100.59,45.065,51.458999999999996,29.93 -2020-09-02 16:00:00,102.98,47.48,53.663000000000004,29.93 -2020-09-02 16:15:00,103.91,47.295,53.663000000000004,29.93 -2020-09-02 16:30:00,108.38,47.666000000000004,53.663000000000004,29.93 -2020-09-02 16:45:00,107.21,45.275,53.663000000000004,29.93 -2020-09-02 17:00:00,108.81,47.14,58.183,29.93 -2020-09-02 17:15:00,109.2,48.111000000000004,58.183,29.93 -2020-09-02 17:30:00,109.84,47.558,58.183,29.93 -2020-09-02 17:45:00,111.27,46.574,58.183,29.93 -2020-09-02 18:00:00,113.28,48.843,60.141000000000005,29.93 -2020-09-02 18:15:00,110.36,48.303999999999995,60.141000000000005,29.93 -2020-09-02 18:30:00,108.48,46.339,60.141000000000005,29.93 -2020-09-02 18:45:00,112.25,50.495,60.141000000000005,29.93 -2020-09-02 19:00:00,116.4,52.958999999999996,60.582,29.93 -2020-09-02 19:15:00,114.79,51.688,60.582,29.93 -2020-09-02 19:30:00,113.75,50.644,60.582,29.93 -2020-09-02 19:45:00,114.25,50.766000000000005,60.582,29.93 -2020-09-02 20:00:00,110.94,49.023,66.61,29.93 -2020-09-02 20:15:00,106.94,48.503,66.61,29.93 -2020-09-02 20:30:00,102.41,48.07899999999999,66.61,29.93 -2020-09-02 20:45:00,98.12,48.092,66.61,29.93 -2020-09-02 21:00:00,91.53,46.675,57.658,29.93 -2020-09-02 21:15:00,91.25,48.266000000000005,57.658,29.93 -2020-09-02 21:30:00,86.71,47.976000000000006,57.658,29.93 -2020-09-02 21:45:00,86.21,47.672,57.658,29.93 -2020-09-02 22:00:00,80.75,45.28,51.81,29.93 -2020-09-02 22:15:00,81.19,46.005,51.81,29.93 -2020-09-02 22:30:00,80.24,38.691,51.81,29.93 -2020-09-02 22:45:00,83.69,35.178000000000004,51.81,29.93 -2020-09-02 23:00:00,82.31,31.945,42.93600000000001,29.93 -2020-09-02 23:15:00,84.53,31.188000000000002,42.93600000000001,29.93 -2020-09-02 23:30:00,80.72,31.066,42.93600000000001,29.93 -2020-09-02 23:45:00,74.93,30.238000000000003,42.93600000000001,29.93 -2020-09-03 00:00:00,78.78,30.328000000000003,39.211,29.93 -2020-09-03 00:15:00,80.39,30.897,39.211,29.93 -2020-09-03 00:30:00,80.32,30.485,39.211,29.93 -2020-09-03 00:45:00,73.95,30.44,39.211,29.93 -2020-09-03 01:00:00,73.78,30.165,37.607,29.93 -2020-09-03 01:15:00,79.05,29.381,37.607,29.93 -2020-09-03 01:30:00,79.16,28.316,37.607,29.93 -2020-09-03 01:45:00,76.51,27.395,37.607,29.93 -2020-09-03 02:00:00,74.11,27.108,36.44,29.93 -2020-09-03 02:15:00,80.14,26.754,36.44,29.93 -2020-09-03 02:30:00,79.59,27.372,36.44,29.93 -2020-09-03 02:45:00,78.57,28.211,36.44,29.93 -2020-09-03 03:00:00,73.82,29.289,36.116,29.93 -2020-09-03 03:15:00,72.93,28.685,36.116,29.93 -2020-09-03 03:30:00,74.34,28.599,36.116,29.93 -2020-09-03 03:45:00,77.3,28.656999999999996,36.116,29.93 -2020-09-03 04:00:00,81.78,35.27,37.398,29.93 -2020-09-03 04:15:00,82.8,42.558,37.398,29.93 -2020-09-03 04:30:00,91.14,39.907,37.398,29.93 -2020-09-03 04:45:00,98.67,40.465,37.398,29.93 -2020-09-03 05:00:00,108.57,57.937,41.776,29.93 -2020-09-03 05:15:00,103.7,69.179,41.776,29.93 -2020-09-03 05:30:00,104.33,63.162,41.776,29.93 -2020-09-03 05:45:00,105.67,59.306999999999995,41.776,29.93 -2020-09-03 06:00:00,110.59,59.608999999999995,55.61,29.93 -2020-09-03 06:15:00,111.02,60.928000000000004,55.61,29.93 -2020-09-03 06:30:00,112.63,59.537,55.61,29.93 -2020-09-03 06:45:00,113.01,61.175,55.61,29.93 -2020-09-03 07:00:00,115.07,59.497,67.13600000000001,29.93 -2020-09-03 07:15:00,114.55,60.665,67.13600000000001,29.93 -2020-09-03 07:30:00,112.42,58.958,67.13600000000001,29.93 -2020-09-03 07:45:00,112.27,59.832,67.13600000000001,29.93 -2020-09-03 08:00:00,109.14,58.523999999999994,57.55,29.93 -2020-09-03 08:15:00,107.8,60.532,57.55,29.93 -2020-09-03 08:30:00,111.96,59.786,57.55,29.93 -2020-09-03 08:45:00,108.99,61.208,57.55,29.93 -2020-09-03 09:00:00,108.66,57.586000000000006,52.931999999999995,29.93 -2020-09-03 09:15:00,108.25,56.148999999999994,52.931999999999995,29.93 -2020-09-03 09:30:00,107.97,58.228,52.931999999999995,29.93 -2020-09-03 09:45:00,106.96,59.778,52.931999999999995,29.93 -2020-09-03 10:00:00,107.82,56.248999999999995,50.36600000000001,29.93 -2020-09-03 10:15:00,109.16,56.93,50.36600000000001,29.93 -2020-09-03 10:30:00,108.59,56.674,50.36600000000001,29.93 -2020-09-03 10:45:00,107.7,57.74,50.36600000000001,29.93 -2020-09-03 11:00:00,107.03,54.208,47.893,29.93 -2020-09-03 11:15:00,107.11,55.266000000000005,47.893,29.93 -2020-09-03 11:30:00,109.86,55.926,47.893,29.93 -2020-09-03 11:45:00,106.11,56.703,47.893,29.93 -2020-09-03 12:00:00,104.22,52.94,45.271,29.93 -2020-09-03 12:15:00,106.28,52.57,45.271,29.93 -2020-09-03 12:30:00,105.6,51.727,45.271,29.93 -2020-09-03 12:45:00,104.71,52.805,45.271,29.93 -2020-09-03 13:00:00,106.15,51.85,44.351000000000006,29.93 -2020-09-03 13:15:00,107.6,52.457,44.351000000000006,29.93 -2020-09-03 13:30:00,113.46,51.277,44.351000000000006,29.93 -2020-09-03 13:45:00,112.08,50.383,44.351000000000006,29.93 -2020-09-03 14:00:00,113.14,51.511,44.99,29.93 -2020-09-03 14:15:00,108.1,50.577,44.99,29.93 -2020-09-03 14:30:00,104.21,49.376000000000005,44.99,29.93 -2020-09-03 14:45:00,103.23,50.141000000000005,44.99,29.93 -2020-09-03 15:00:00,107.19,50.763000000000005,46.869,29.93 -2020-09-03 15:15:00,106.83,48.701,46.869,29.93 -2020-09-03 15:30:00,105.79,47.54600000000001,46.869,29.93 -2020-09-03 15:45:00,104.72,45.301,46.869,29.93 -2020-09-03 16:00:00,105.33,47.681000000000004,48.902,29.93 -2020-09-03 16:15:00,105.72,47.508,48.902,29.93 -2020-09-03 16:30:00,107.33,47.879,48.902,29.93 -2020-09-03 16:45:00,110.5,45.538999999999994,48.902,29.93 -2020-09-03 17:00:00,112.54,47.36600000000001,53.244,29.93 -2020-09-03 17:15:00,111.29,48.365,53.244,29.93 -2020-09-03 17:30:00,110.8,47.821999999999996,53.244,29.93 -2020-09-03 17:45:00,111.93,46.872,53.244,29.93 -2020-09-03 18:00:00,112.15,49.126999999999995,54.343999999999994,29.93 -2020-09-03 18:15:00,110.62,48.596000000000004,54.343999999999994,29.93 -2020-09-03 18:30:00,112.94,46.643,54.343999999999994,29.93 -2020-09-03 18:45:00,111.47,50.794,54.343999999999994,29.93 -2020-09-03 19:00:00,112.83,53.269,54.332,29.93 -2020-09-03 19:15:00,109.18,51.998000000000005,54.332,29.93 -2020-09-03 19:30:00,109.17,50.953,54.332,29.93 -2020-09-03 19:45:00,112.48,51.073,54.332,29.93 -2020-09-03 20:00:00,107.89,49.346000000000004,58.06,29.93 -2020-09-03 20:15:00,107.07,48.826,58.06,29.93 -2020-09-03 20:30:00,101.06,48.38,58.06,29.93 -2020-09-03 20:45:00,99.03,48.354,58.06,29.93 -2020-09-03 21:00:00,91.24,46.941,52.411,29.93 -2020-09-03 21:15:00,98.02,48.522,52.411,29.93 -2020-09-03 21:30:00,96.57,48.235,52.411,29.93 -2020-09-03 21:45:00,96.41,47.895,52.411,29.93 -2020-09-03 22:00:00,88.52,45.481,47.148999999999994,29.93 -2020-09-03 22:15:00,84.04,46.18600000000001,47.148999999999994,29.93 -2020-09-03 22:30:00,85.59,38.847,47.148999999999994,29.93 -2020-09-03 22:45:00,88.8,35.336999999999996,47.148999999999994,29.93 -2020-09-03 23:00:00,84.03,32.149,40.814,29.93 -2020-09-03 23:15:00,81.23,31.37,40.814,29.93 -2020-09-03 23:30:00,78.97,31.255,40.814,29.93 -2020-09-03 23:45:00,84.43,30.430999999999997,40.814,29.93 -2020-09-04 00:00:00,79.88,28.809,39.153,29.93 -2020-09-04 00:15:00,81.93,29.595,39.153,29.93 -2020-09-04 00:30:00,76.83,29.414,39.153,29.93 -2020-09-04 00:45:00,81.19,29.766,39.153,29.93 -2020-09-04 01:00:00,78.59,29.094,37.228,29.93 -2020-09-04 01:15:00,75.78,27.914,37.228,29.93 -2020-09-04 01:30:00,74.53,27.471,37.228,29.93 -2020-09-04 01:45:00,76.53,26.340999999999998,37.228,29.93 -2020-09-04 02:00:00,79.34,26.897,35.851,29.93 -2020-09-04 02:15:00,80.68,26.505,35.851,29.93 -2020-09-04 02:30:00,79.68,27.898000000000003,35.851,29.93 -2020-09-04 02:45:00,72.95,28.131,35.851,29.93 -2020-09-04 03:00:00,71.7,29.756,36.54,29.93 -2020-09-04 03:15:00,72.28,28.221,36.54,29.93 -2020-09-04 03:30:00,76.49,27.927,36.54,29.93 -2020-09-04 03:45:00,83.91,28.791999999999998,36.54,29.93 -2020-09-04 04:00:00,87.81,35.59,37.578,29.93 -2020-09-04 04:15:00,87.55,41.49100000000001,37.578,29.93 -2020-09-04 04:30:00,92.55,39.719,37.578,29.93 -2020-09-04 04:45:00,98.47,39.561,37.578,29.93 -2020-09-04 05:00:00,103.34,56.532,40.387,29.93 -2020-09-04 05:15:00,107.52,68.905,40.387,29.93 -2020-09-04 05:30:00,105.74,63.184,40.387,29.93 -2020-09-04 05:45:00,109.74,58.878,40.387,29.93 -2020-09-04 06:00:00,115.98,59.403999999999996,54.668,29.93 -2020-09-04 06:15:00,115.47,60.705,54.668,29.93 -2020-09-04 06:30:00,117.38,59.156000000000006,54.668,29.93 -2020-09-04 06:45:00,119.84,60.883,54.668,29.93 -2020-09-04 07:00:00,121.76,59.673,63.971000000000004,29.93 -2020-09-04 07:15:00,124.37,61.793,63.971000000000004,29.93 -2020-09-04 07:30:00,124.71,58.375,63.971000000000004,29.93 -2020-09-04 07:45:00,124.2,58.946000000000005,63.971000000000004,29.93 -2020-09-04 08:00:00,125.37,58.18,56.042,29.93 -2020-09-04 08:15:00,124.6,60.688,56.042,29.93 -2020-09-04 08:30:00,128.49,59.994,56.042,29.93 -2020-09-04 08:45:00,128.74,61.058,56.042,29.93 -2020-09-04 09:00:00,130.79,55.483000000000004,52.832,29.93 -2020-09-04 09:15:00,128.22,55.792,52.832,29.93 -2020-09-04 09:30:00,125.85,57.18899999999999,52.832,29.93 -2020-09-04 09:45:00,122.8,59.033,52.832,29.93 -2020-09-04 10:00:00,123.79,55.185,50.044,29.93 -2020-09-04 10:15:00,126.55,55.742,50.044,29.93 -2020-09-04 10:30:00,128.28,55.945,50.044,29.93 -2020-09-04 10:45:00,124.38,56.851000000000006,50.044,29.93 -2020-09-04 11:00:00,124.72,53.549,49.06100000000001,29.93 -2020-09-04 11:15:00,126.1,53.519,49.06100000000001,29.93 -2020-09-04 11:30:00,129.93,54.115,49.06100000000001,29.93 -2020-09-04 11:45:00,120.43,54.05,49.06100000000001,29.93 -2020-09-04 12:00:00,116.34,50.803999999999995,45.595,29.93 -2020-09-04 12:15:00,122.71,49.553999999999995,45.595,29.93 -2020-09-04 12:30:00,109.57,48.861999999999995,45.595,29.93 -2020-09-04 12:45:00,103.66,49.325,45.595,29.93 -2020-09-04 13:00:00,100.92,48.995,43.218,29.93 -2020-09-04 13:15:00,101.16,49.873000000000005,43.218,29.93 -2020-09-04 13:30:00,100.93,49.382,43.218,29.93 -2020-09-04 13:45:00,100.61,48.751999999999995,43.218,29.93 -2020-09-04 14:00:00,99.88,48.977,41.926,29.93 -2020-09-04 14:15:00,100.39,48.406000000000006,41.926,29.93 -2020-09-04 14:30:00,103.67,48.613,41.926,29.93 -2020-09-04 14:45:00,105.03,48.823,41.926,29.93 -2020-09-04 15:00:00,106.17,49.254,43.79,29.93 -2020-09-04 15:15:00,101.95,46.93600000000001,43.79,29.93 -2020-09-04 15:30:00,95.93,45.091,43.79,29.93 -2020-09-04 15:45:00,97.23,43.538999999999994,43.79,29.93 -2020-09-04 16:00:00,99.92,44.988,45.895,29.93 -2020-09-04 16:15:00,100.88,45.3,45.895,29.93 -2020-09-04 16:30:00,104.55,45.525,45.895,29.93 -2020-09-04 16:45:00,106.79,42.534,45.895,29.93 -2020-09-04 17:00:00,106.67,45.867,51.36,29.93 -2020-09-04 17:15:00,105.13,46.685,51.36,29.93 -2020-09-04 17:30:00,106.1,46.251999999999995,51.36,29.93 -2020-09-04 17:45:00,108.53,45.159,51.36,29.93 -2020-09-04 18:00:00,108.52,47.55,52.985,29.93 -2020-09-04 18:15:00,105.88,46.141999999999996,52.985,29.93 -2020-09-04 18:30:00,108.88,44.162,52.985,29.93 -2020-09-04 18:45:00,110.64,48.68,52.985,29.93 -2020-09-04 19:00:00,111.33,52.07,52.602,29.93 -2020-09-04 19:15:00,112.44,51.495,52.602,29.93 -2020-09-04 19:30:00,112.3,50.453,52.602,29.93 -2020-09-04 19:45:00,106.77,49.611999999999995,52.602,29.93 -2020-09-04 20:00:00,98.61,47.763000000000005,58.063,29.93 -2020-09-04 20:15:00,99.3,47.946999999999996,58.063,29.93 -2020-09-04 20:30:00,95.69,47.044,58.063,29.93 -2020-09-04 20:45:00,93.25,46.33,58.063,29.93 -2020-09-04 21:00:00,88.17,46.15,50.135,29.93 -2020-09-04 21:15:00,88.23,49.258,50.135,29.93 -2020-09-04 21:30:00,85.09,48.846000000000004,50.135,29.93 -2020-09-04 21:45:00,85.78,48.718999999999994,50.135,29.93 -2020-09-04 22:00:00,82.3,46.283,45.165,29.93 -2020-09-04 22:15:00,81.47,46.729,45.165,29.93 -2020-09-04 22:30:00,77.2,44.387,45.165,29.93 -2020-09-04 22:45:00,79.3,42.086000000000006,45.165,29.93 -2020-09-04 23:00:00,74.88,40.385,39.121,29.93 -2020-09-04 23:15:00,70.48,37.946999999999996,39.121,29.93 -2020-09-04 23:30:00,72.77,36.041,39.121,29.93 -2020-09-04 23:45:00,73.16,35.028,39.121,29.93 -2020-09-05 00:00:00,69.6,29.750999999999998,38.49,29.816 -2020-09-05 00:15:00,65.9,29.398000000000003,38.49,29.816 -2020-09-05 00:30:00,61.59,28.978,38.49,29.816 -2020-09-05 00:45:00,64.02,28.78,38.49,29.816 -2020-09-05 01:00:00,62.98,28.416999999999998,34.5,29.816 -2020-09-05 01:15:00,69.08,27.651,34.5,29.816 -2020-09-05 01:30:00,69.9,26.445999999999998,34.5,29.816 -2020-09-05 01:45:00,65.62,26.391,34.5,29.816 -2020-09-05 02:00:00,59.22,26.165,32.236,29.816 -2020-09-05 02:15:00,61.62,25.051,32.236,29.816 -2020-09-05 02:30:00,60.74,25.576999999999998,32.236,29.816 -2020-09-05 02:45:00,61.81,26.514,32.236,29.816 -2020-09-05 03:00:00,59.94,26.993000000000002,32.067,29.816 -2020-09-05 03:15:00,61.24,24.705,32.067,29.816 -2020-09-05 03:30:00,61.65,24.504,32.067,29.816 -2020-09-05 03:45:00,61.59,26.679000000000002,32.067,29.816 -2020-09-05 04:00:00,61.57,31.316999999999997,33.071,29.816 -2020-09-05 04:15:00,59.69,36.134,33.071,29.816 -2020-09-05 04:30:00,62.02,32.619,33.071,29.816 -2020-09-05 04:45:00,63.77,32.647,33.071,29.816 -2020-09-05 05:00:00,64.24,40.606,33.014,29.816 -2020-09-05 05:15:00,65.87,41.105,33.014,29.816 -2020-09-05 05:30:00,65.26,36.77,33.014,29.816 -2020-09-05 05:45:00,64.15,37.029,33.014,29.816 -2020-09-05 06:00:00,67.9,49.608999999999995,34.628,29.816 -2020-09-05 06:15:00,70.61,59.795,34.628,29.816 -2020-09-05 06:30:00,71.88,55.083,34.628,29.816 -2020-09-05 06:45:00,72.73,52.895,34.628,29.816 -2020-09-05 07:00:00,75.06,49.843999999999994,38.871,29.816 -2020-09-05 07:15:00,73.03,50.681999999999995,38.871,29.816 -2020-09-05 07:30:00,77.02,48.961999999999996,38.871,29.816 -2020-09-05 07:45:00,79.22,50.842,38.871,29.816 -2020-09-05 08:00:00,77.29,51.131,43.293,29.816 -2020-09-05 08:15:00,78.24,53.879,43.293,29.816 -2020-09-05 08:30:00,76.23,53.385,43.293,29.816 -2020-09-05 08:45:00,75.72,55.662,43.293,29.816 -2020-09-05 09:00:00,74.97,53.013000000000005,44.559,29.816 -2020-09-05 09:15:00,72.64,53.833,44.559,29.816 -2020-09-05 09:30:00,73.09,55.76,44.559,29.816 -2020-09-05 09:45:00,75.25,57.203,44.559,29.816 -2020-09-05 10:00:00,74.29,53.878,42.091,29.816 -2020-09-05 10:15:00,73.61,54.733000000000004,42.091,29.816 -2020-09-05 10:30:00,73.64,54.651,42.091,29.816 -2020-09-05 10:45:00,71.59,55.418,42.091,29.816 -2020-09-05 11:00:00,70.86,52.035,38.505,29.816 -2020-09-05 11:15:00,67.38,52.648,38.505,29.816 -2020-09-05 11:30:00,67.78,53.333999999999996,38.505,29.816 -2020-09-05 11:45:00,67.79,53.699,38.505,29.816 -2020-09-05 12:00:00,64.86,50.608999999999995,35.388000000000005,29.816 -2020-09-05 12:15:00,64.27,50.17,35.388000000000005,29.816 -2020-09-05 12:30:00,61.73,49.437,35.388000000000005,29.816 -2020-09-05 12:45:00,61.93,50.407,35.388000000000005,29.816 -2020-09-05 13:00:00,57.25,49.278999999999996,31.355999999999998,29.816 -2020-09-05 13:15:00,60.01,49.393,31.355999999999998,29.816 -2020-09-05 13:30:00,60.1,48.998000000000005,31.355999999999998,29.816 -2020-09-05 13:45:00,58.58,47.281000000000006,31.355999999999998,29.816 -2020-09-05 14:00:00,62.4,47.692,30.522,29.816 -2020-09-05 14:15:00,62.32,45.977,30.522,29.816 -2020-09-05 14:30:00,69.06,45.586000000000006,30.522,29.816 -2020-09-05 14:45:00,67.31,46.24,30.522,29.816 -2020-09-05 15:00:00,64.95,47.103,34.36,29.816 -2020-09-05 15:15:00,68.02,45.523,34.36,29.816 -2020-09-05 15:30:00,73.3,44.092,34.36,29.816 -2020-09-05 15:45:00,66.23,41.758,34.36,29.816 -2020-09-05 16:00:00,70.0,44.927,39.507,29.816 -2020-09-05 16:15:00,70.69,44.593999999999994,39.507,29.816 -2020-09-05 16:30:00,72.97,45.01,39.507,29.816 -2020-09-05 16:45:00,75.43,42.166000000000004,39.507,29.816 -2020-09-05 17:00:00,78.45,44.356,47.151,29.816 -2020-09-05 17:15:00,79.25,43.55,47.151,29.816 -2020-09-05 17:30:00,80.34,42.998000000000005,47.151,29.816 -2020-09-05 17:45:00,81.44,42.316,47.151,29.816 -2020-09-05 18:00:00,84.42,45.887,50.303999999999995,29.816 -2020-09-05 18:15:00,82.25,46.185,50.303999999999995,29.816 -2020-09-05 18:30:00,82.55,45.576,50.303999999999995,29.816 -2020-09-05 18:45:00,85.27,46.61600000000001,50.303999999999995,29.816 -2020-09-05 19:00:00,83.52,48.75899999999999,50.622,29.816 -2020-09-05 19:15:00,83.04,47.233000000000004,50.622,29.816 -2020-09-05 19:30:00,81.56,46.961999999999996,50.622,29.816 -2020-09-05 19:45:00,80.53,47.653,50.622,29.816 -2020-09-05 20:00:00,77.83,46.8,45.391000000000005,29.816 -2020-09-05 20:15:00,76.29,46.696000000000005,45.391000000000005,29.816 -2020-09-05 20:30:00,75.74,44.957,45.391000000000005,29.816 -2020-09-05 20:45:00,75.02,45.777,45.391000000000005,29.816 -2020-09-05 21:00:00,71.28,44.6,39.98,29.816 -2020-09-05 21:15:00,70.5,47.446000000000005,39.98,29.816 -2020-09-05 21:30:00,67.18,47.358999999999995,39.98,29.816 -2020-09-05 21:45:00,67.05,46.666000000000004,39.98,29.816 -2020-09-05 22:00:00,64.24,44.354,37.53,29.816 -2020-09-05 22:15:00,62.85,45.309,37.53,29.816 -2020-09-05 22:30:00,60.44,43.391000000000005,37.53,29.816 -2020-09-05 22:45:00,60.44,41.656000000000006,37.53,29.816 -2020-09-05 23:00:00,55.7,39.704,30.97,29.816 -2020-09-05 23:15:00,55.88,37.408,30.97,29.816 -2020-09-05 23:30:00,55.87,37.45,30.97,29.816 -2020-09-05 23:45:00,54.85,36.455,30.97,29.816 -2020-09-06 00:00:00,51.94,31.230999999999998,27.24,29.816 -2020-09-06 00:15:00,52.98,29.822,27.24,29.816 -2020-09-06 00:30:00,51.93,29.235,27.24,29.816 -2020-09-06 00:45:00,52.47,29.033,27.24,29.816 -2020-09-06 01:00:00,50.9,28.868000000000002,25.662,29.816 -2020-09-06 01:15:00,51.69,28.144000000000002,25.662,29.816 -2020-09-06 01:30:00,51.06,26.9,25.662,29.816 -2020-09-06 01:45:00,51.75,26.471999999999998,25.662,29.816 -2020-09-06 02:00:00,50.22,26.226,25.67,29.816 -2020-09-06 02:15:00,50.83,25.649,25.67,29.816 -2020-09-06 02:30:00,50.93,26.423000000000002,25.67,29.816 -2020-09-06 02:45:00,51.01,27.158,25.67,29.816 -2020-09-06 03:00:00,50.72,28.236,24.258000000000003,29.816 -2020-09-06 03:15:00,51.86,26.129,24.258000000000003,29.816 -2020-09-06 03:30:00,51.93,25.463,24.258000000000003,29.816 -2020-09-06 03:45:00,52.05,26.918000000000003,24.258000000000003,29.816 -2020-09-06 04:00:00,52.55,31.531,25.051,29.816 -2020-09-06 04:15:00,52.95,35.867,25.051,29.816 -2020-09-06 04:30:00,53.59,33.545,25.051,29.816 -2020-09-06 04:45:00,54.33,33.202,25.051,29.816 -2020-09-06 05:00:00,56.46,40.988,25.145,29.816 -2020-09-06 05:15:00,55.34,40.624,25.145,29.816 -2020-09-06 05:30:00,53.86,35.900999999999996,25.145,29.816 -2020-09-06 05:45:00,55.65,35.897,25.145,29.816 -2020-09-06 06:00:00,56.53,46.333999999999996,26.371,29.816 -2020-09-06 06:15:00,57.41,57.077,26.371,29.816 -2020-09-06 06:30:00,58.46,51.619,26.371,29.816 -2020-09-06 06:45:00,60.16,48.426,26.371,29.816 -2020-09-06 07:00:00,60.1,45.915,28.756999999999998,29.816 -2020-09-06 07:15:00,60.26,45.225,28.756999999999998,29.816 -2020-09-06 07:30:00,65.45,44.508,28.756999999999998,29.816 -2020-09-06 07:45:00,66.55,46.265,28.756999999999998,29.816 -2020-09-06 08:00:00,66.48,47.442,32.82,29.816 -2020-09-06 08:15:00,65.64,51.174,32.82,29.816 -2020-09-06 08:30:00,58.67,51.653,32.82,29.816 -2020-09-06 08:45:00,61.25,54.093,32.82,29.816 -2020-09-06 09:00:00,63.13,51.301,35.534,29.816 -2020-09-06 09:15:00,57.61,51.75,35.534,29.816 -2020-09-06 09:30:00,58.16,54.012,35.534,29.816 -2020-09-06 09:45:00,60.38,56.286,35.534,29.816 -2020-09-06 10:00:00,61.07,53.742,35.925,29.816 -2020-09-06 10:15:00,60.78,54.75,35.925,29.816 -2020-09-06 10:30:00,63.8,54.928000000000004,35.925,29.816 -2020-09-06 10:45:00,61.65,56.318000000000005,35.925,29.816 -2020-09-06 11:00:00,61.14,52.763999999999996,37.056,29.816 -2020-09-06 11:15:00,57.92,52.993,37.056,29.816 -2020-09-06 11:30:00,60.04,54.018,37.056,29.816 -2020-09-06 11:45:00,59.7,54.666000000000004,37.056,29.816 -2020-09-06 12:00:00,59.47,52.391999999999996,33.124,29.816 -2020-09-06 12:15:00,59.46,51.644,33.124,29.816 -2020-09-06 12:30:00,57.73,50.933,33.124,29.816 -2020-09-06 12:45:00,56.0,51.214,33.124,29.816 -2020-09-06 13:00:00,53.2,49.711000000000006,29.874000000000002,29.816 -2020-09-06 13:15:00,54.82,49.708,29.874000000000002,29.816 -2020-09-06 13:30:00,54.67,48.316,29.874000000000002,29.816 -2020-09-06 13:45:00,53.78,47.49100000000001,29.874000000000002,29.816 -2020-09-06 14:00:00,52.66,48.938,27.302,29.816 -2020-09-06 14:15:00,55.42,47.761,27.302,29.816 -2020-09-06 14:30:00,57.66,46.483000000000004,27.302,29.816 -2020-09-06 14:45:00,57.04,46.153,27.302,29.816 -2020-09-06 15:00:00,57.93,46.895,27.642,29.816 -2020-09-06 15:15:00,56.35,44.723,27.642,29.816 -2020-09-06 15:30:00,55.04,43.233000000000004,27.642,29.816 -2020-09-06 15:45:00,55.71,41.247,27.642,29.816 -2020-09-06 16:00:00,59.68,43.17,31.945999999999998,29.816 -2020-09-06 16:15:00,60.8,42.937,31.945999999999998,29.816 -2020-09-06 16:30:00,66.16,44.26,31.945999999999998,29.816 -2020-09-06 16:45:00,71.81,41.54,31.945999999999998,29.816 -2020-09-06 17:00:00,76.25,44.00899999999999,40.387,29.816 -2020-09-06 17:15:00,77.49,44.511,40.387,29.816 -2020-09-06 17:30:00,78.34,44.708,40.387,29.816 -2020-09-06 17:45:00,74.93,44.711000000000006,40.387,29.816 -2020-09-06 18:00:00,79.12,48.699,44.575,29.816 -2020-09-06 18:15:00,78.19,48.806999999999995,44.575,29.816 -2020-09-06 18:30:00,80.41,47.744,44.575,29.816 -2020-09-06 18:45:00,80.48,49.102,44.575,29.816 -2020-09-06 19:00:00,92.24,53.111000000000004,45.623999999999995,29.816 -2020-09-06 19:15:00,91.93,50.673,45.623999999999995,29.816 -2020-09-06 19:30:00,89.23,50.163999999999994,45.623999999999995,29.816 -2020-09-06 19:45:00,85.71,50.617,45.623999999999995,29.816 -2020-09-06 20:00:00,81.21,49.949,44.583999999999996,29.816 -2020-09-06 20:15:00,87.39,49.841,44.583999999999996,29.816 -2020-09-06 20:30:00,86.81,48.938,44.583999999999996,29.816 -2020-09-06 20:45:00,86.6,48.099,44.583999999999996,29.816 -2020-09-06 21:00:00,78.52,46.498999999999995,39.732,29.816 -2020-09-06 21:15:00,79.0,48.997,39.732,29.816 -2020-09-06 21:30:00,80.14,48.376999999999995,39.732,29.816 -2020-09-06 21:45:00,77.29,47.978,39.732,29.816 -2020-09-06 22:00:00,73.53,47.293,38.571,29.816 -2020-09-06 22:15:00,73.0,46.657,38.571,29.816 -2020-09-06 22:30:00,71.65,43.901,38.571,29.816 -2020-09-06 22:45:00,71.15,40.95,38.571,29.816 -2020-09-06 23:00:00,65.54,38.457,33.121,29.816 -2020-09-06 23:15:00,66.93,37.468,33.121,29.816 -2020-09-06 23:30:00,66.52,37.135999999999996,33.121,29.816 -2020-09-06 23:45:00,66.55,36.387,33.121,29.816 -2020-09-07 00:00:00,64.09,33.271,32.506,29.93 -2020-09-07 00:15:00,66.1,32.906,32.506,29.93 -2020-09-07 00:30:00,65.16,32.003,32.506,29.93 -2020-09-07 00:45:00,72.59,31.397,32.506,29.93 -2020-09-07 01:00:00,71.19,31.555999999999997,31.121,29.93 -2020-09-07 01:15:00,72.55,30.802,31.121,29.93 -2020-09-07 01:30:00,70.95,29.899,31.121,29.93 -2020-09-07 01:45:00,71.59,29.4,31.121,29.93 -2020-09-07 02:00:00,71.83,29.554000000000002,29.605999999999998,29.93 -2020-09-07 02:15:00,71.29,28.28,29.605999999999998,29.93 -2020-09-07 02:30:00,65.32,29.213,29.605999999999998,29.93 -2020-09-07 02:45:00,68.55,29.741,29.605999999999998,29.93 -2020-09-07 03:00:00,73.65,31.401,28.124000000000002,29.93 -2020-09-07 03:15:00,75.4,30.138,28.124000000000002,29.93 -2020-09-07 03:30:00,77.07,30.045,28.124000000000002,29.93 -2020-09-07 03:45:00,72.74,31.016,28.124000000000002,29.93 -2020-09-07 04:00:00,78.77,38.909,29.743000000000002,29.93 -2020-09-07 04:15:00,78.27,46.333999999999996,29.743000000000002,29.93 -2020-09-07 04:30:00,81.92,43.856,29.743000000000002,29.93 -2020-09-07 04:45:00,87.93,43.864,29.743000000000002,29.93 -2020-09-07 05:00:00,95.33,59.738,36.191,29.93 -2020-09-07 05:15:00,99.43,70.842,36.191,29.93 -2020-09-07 05:30:00,100.39,64.77199999999999,36.191,29.93 -2020-09-07 05:45:00,103.16,61.369,36.191,29.93 -2020-09-07 06:00:00,107.79,60.716,55.277,29.93 -2020-09-07 06:15:00,107.35,61.775,55.277,29.93 -2020-09-07 06:30:00,111.41,60.729,55.277,29.93 -2020-09-07 06:45:00,110.88,63.191,55.277,29.93 -2020-09-07 07:00:00,111.5,61.396,65.697,29.93 -2020-09-07 07:15:00,111.03,62.872,65.697,29.93 -2020-09-07 07:30:00,108.26,61.34,65.697,29.93 -2020-09-07 07:45:00,107.84,63.056999999999995,65.697,29.93 -2020-09-07 08:00:00,106.73,61.815,57.028,29.93 -2020-09-07 08:15:00,106.0,64.19800000000001,57.028,29.93 -2020-09-07 08:30:00,105.72,63.345,57.028,29.93 -2020-09-07 08:45:00,105.41,65.547,57.028,29.93 -2020-09-07 09:00:00,105.06,61.758,52.633,29.93 -2020-09-07 09:15:00,103.6,60.206,52.633,29.93 -2020-09-07 09:30:00,104.14,61.515,52.633,29.93 -2020-09-07 09:45:00,108.19,61.792,52.633,29.93 -2020-09-07 10:00:00,105.88,59.531000000000006,50.647,29.93 -2020-09-07 10:15:00,107.1,60.333999999999996,50.647,29.93 -2020-09-07 10:30:00,106.5,59.99,50.647,29.93 -2020-09-07 10:45:00,110.53,60.085,50.647,29.93 -2020-09-07 11:00:00,112.98,56.413000000000004,50.245,29.93 -2020-09-07 11:15:00,107.64,57.105,50.245,29.93 -2020-09-07 11:30:00,101.36,58.91,50.245,29.93 -2020-09-07 11:45:00,103.67,59.924,50.245,29.93 -2020-09-07 12:00:00,108.34,56.413000000000004,46.956,29.93 -2020-09-07 12:15:00,106.87,55.748000000000005,46.956,29.93 -2020-09-07 12:30:00,97.46,54.169,46.956,29.93 -2020-09-07 12:45:00,99.84,54.635,46.956,29.93 -2020-09-07 13:00:00,97.74,53.949,47.383,29.93 -2020-09-07 13:15:00,100.52,52.99100000000001,47.383,29.93 -2020-09-07 13:30:00,97.51,51.676,47.383,29.93 -2020-09-07 13:45:00,96.25,51.64,47.383,29.93 -2020-09-07 14:00:00,98.27,52.184,47.1,29.93 -2020-09-07 14:15:00,99.93,51.449,47.1,29.93 -2020-09-07 14:30:00,104.1,49.985,47.1,29.93 -2020-09-07 14:45:00,107.04,51.446999999999996,47.1,29.93 -2020-09-07 15:00:00,106.36,52.048,49.355,29.93 -2020-09-07 15:15:00,104.11,49.193999999999996,49.355,29.93 -2020-09-07 15:30:00,105.03,48.29,49.355,29.93 -2020-09-07 15:45:00,108.61,45.835,49.355,29.93 -2020-09-07 16:00:00,109.53,48.643,52.14,29.93 -2020-09-07 16:15:00,109.89,48.38399999999999,52.14,29.93 -2020-09-07 16:30:00,113.1,49.004,52.14,29.93 -2020-09-07 16:45:00,113.41,46.208,52.14,29.93 -2020-09-07 17:00:00,113.76,47.629,58.705,29.93 -2020-09-07 17:15:00,113.0,48.368,58.705,29.93 -2020-09-07 17:30:00,115.63,48.17,58.705,29.93 -2020-09-07 17:45:00,114.27,47.673,58.705,29.93 -2020-09-07 18:00:00,115.1,50.723,59.153,29.93 -2020-09-07 18:15:00,112.76,48.966,59.153,29.93 -2020-09-07 18:30:00,113.45,47.331,59.153,29.93 -2020-09-07 18:45:00,118.05,51.548,59.153,29.93 -2020-09-07 19:00:00,114.98,55.098,61.483000000000004,29.93 -2020-09-07 19:15:00,112.15,53.696999999999996,61.483000000000004,29.93 -2020-09-07 19:30:00,113.66,52.918,61.483000000000004,29.93 -2020-09-07 19:45:00,113.35,52.733000000000004,61.483000000000004,29.93 -2020-09-07 20:00:00,109.69,50.699,67.55,29.93 -2020-09-07 20:15:00,107.25,51.534,67.55,29.93 -2020-09-07 20:30:00,106.23,50.851000000000006,67.55,29.93 -2020-09-07 20:45:00,103.57,50.35,67.55,29.93 -2020-09-07 21:00:00,96.79,48.262,60.026,29.93 -2020-09-07 21:15:00,92.0,51.003,60.026,29.93 -2020-09-07 21:30:00,87.06,50.658,60.026,29.93 -2020-09-07 21:45:00,91.31,49.968999999999994,60.026,29.93 -2020-09-07 22:00:00,88.7,47.123000000000005,52.736999999999995,29.93 -2020-09-07 22:15:00,87.47,48.068999999999996,52.736999999999995,29.93 -2020-09-07 22:30:00,82.02,40.363,52.736999999999995,29.93 -2020-09-07 22:45:00,84.25,36.84,52.736999999999995,29.93 -2020-09-07 23:00:00,81.67,34.525999999999996,44.408,29.93 -2020-09-07 23:15:00,82.18,32.357,44.408,29.93 -2020-09-07 23:30:00,79.67,32.315,44.408,29.93 -2020-09-07 23:45:00,80.93,31.396,44.408,29.93 -2020-09-08 00:00:00,78.69,31.4,44.438,29.93 -2020-09-08 00:15:00,78.24,31.965,44.438,29.93 -2020-09-08 00:30:00,74.74,31.59,44.438,29.93 -2020-09-08 00:45:00,78.91,31.578000000000003,44.438,29.93 -2020-09-08 01:00:00,78.69,31.218000000000004,41.468999999999994,29.93 -2020-09-08 01:15:00,79.73,30.53,41.468999999999994,29.93 -2020-09-08 01:30:00,74.2,29.549,41.468999999999994,29.93 -2020-09-08 01:45:00,79.22,28.631999999999998,41.468999999999994,29.93 -2020-09-08 02:00:00,78.78,28.364,39.708,29.93 -2020-09-08 02:15:00,79.67,28.124000000000002,39.708,29.93 -2020-09-08 02:30:00,78.51,28.616999999999997,39.708,29.93 -2020-09-08 02:45:00,78.4,29.432,39.708,29.93 -2020-09-08 03:00:00,81.3,30.445999999999998,38.919000000000004,29.93 -2020-09-08 03:15:00,79.9,29.929000000000002,38.919000000000004,29.93 -2020-09-08 03:30:00,77.62,29.871,38.919000000000004,29.93 -2020-09-08 03:45:00,81.23,29.865,38.919000000000004,29.93 -2020-09-08 04:00:00,88.53,36.65,40.092,29.93 -2020-09-08 04:15:00,91.69,44.075,40.092,29.93 -2020-09-08 04:30:00,94.06,41.461999999999996,40.092,29.93 -2020-09-08 04:45:00,99.19,42.047,40.092,29.93 -2020-09-08 05:00:00,107.52,59.983000000000004,43.713,29.93 -2020-09-08 05:15:00,111.02,71.717,43.713,29.93 -2020-09-08 05:30:00,106.44,65.628,43.713,29.93 -2020-09-08 05:45:00,110.66,61.523,43.713,29.93 -2020-09-08 06:00:00,113.9,61.646,56.033,29.93 -2020-09-08 06:15:00,116.71,63.085,56.033,29.93 -2020-09-08 06:30:00,119.58,61.678000000000004,56.033,29.93 -2020-09-08 06:45:00,119.15,63.302,56.033,29.93 -2020-09-08 07:00:00,121.33,61.621,66.003,29.93 -2020-09-08 07:15:00,120.99,62.865,66.003,29.93 -2020-09-08 07:30:00,121.51,61.317,66.003,29.93 -2020-09-08 07:45:00,119.76,62.193999999999996,66.003,29.93 -2020-09-08 08:00:00,113.06,60.905,57.474,29.93 -2020-09-08 08:15:00,112.42,62.732,57.474,29.93 -2020-09-08 08:30:00,112.09,62.012,57.474,29.93 -2020-09-08 08:45:00,114.08,63.347,57.474,29.93 -2020-09-08 09:00:00,111.56,59.766999999999996,51.928000000000004,29.93 -2020-09-08 09:15:00,113.34,58.276,51.928000000000004,29.93 -2020-09-08 09:30:00,112.99,60.253,51.928000000000004,29.93 -2020-09-08 09:45:00,116.22,61.656000000000006,51.928000000000004,29.93 -2020-09-08 10:00:00,117.84,58.111000000000004,49.46,29.93 -2020-09-08 10:15:00,109.46,58.625,49.46,29.93 -2020-09-08 10:30:00,109.25,58.316,49.46,29.93 -2020-09-08 10:45:00,109.93,59.315,49.46,29.93 -2020-09-08 11:00:00,105.93,55.848,48.206,29.93 -2020-09-08 11:15:00,109.06,56.84,48.206,29.93 -2020-09-08 11:30:00,107.95,57.488,48.206,29.93 -2020-09-08 11:45:00,108.6,58.178000000000004,48.206,29.93 -2020-09-08 12:00:00,108.71,54.305,46.285,29.93 -2020-09-08 12:15:00,108.48,53.868,46.285,29.93 -2020-09-08 12:30:00,108.83,53.159,46.285,29.93 -2020-09-08 12:45:00,105.3,54.181999999999995,46.285,29.93 -2020-09-08 13:00:00,104.99,53.108000000000004,46.861999999999995,29.93 -2020-09-08 13:15:00,107.88,53.677,46.861999999999995,29.93 -2020-09-08 13:30:00,110.14,52.479,46.861999999999995,29.93 -2020-09-08 13:45:00,107.86,51.63,46.861999999999995,29.93 -2020-09-08 14:00:00,108.5,52.566,46.488,29.93 -2020-09-08 14:15:00,109.29,51.687,46.488,29.93 -2020-09-08 14:30:00,103.98,50.617,46.488,29.93 -2020-09-08 14:45:00,103.36,51.375,46.488,29.93 -2020-09-08 15:00:00,104.44,51.73,48.442,29.93 -2020-09-08 15:15:00,102.88,49.745,48.442,29.93 -2020-09-08 15:30:00,100.95,48.707,48.442,29.93 -2020-09-08 15:45:00,101.93,46.519,48.442,29.93 -2020-09-08 16:00:00,106.74,48.718999999999994,50.397,29.93 -2020-09-08 16:15:00,109.62,48.608000000000004,50.397,29.93 -2020-09-08 16:30:00,111.41,48.976000000000006,50.397,29.93 -2020-09-08 16:45:00,111.89,46.9,50.397,29.93 -2020-09-08 17:00:00,114.63,48.536,56.668,29.93 -2020-09-08 17:15:00,112.41,49.675,56.668,29.93 -2020-09-08 17:30:00,113.49,49.18,56.668,29.93 -2020-09-08 17:45:00,115.51,48.411,56.668,29.93 -2020-09-08 18:00:00,115.96,50.586999999999996,57.957,29.93 -2020-09-08 18:15:00,112.95,50.101000000000006,57.957,29.93 -2020-09-08 18:30:00,113.99,48.21,57.957,29.93 -2020-09-08 18:45:00,116.35,52.343,57.957,29.93 -2020-09-08 19:00:00,120.65,54.869,57.056000000000004,29.93 -2020-09-08 19:15:00,118.5,53.596000000000004,57.056000000000004,29.93 -2020-09-08 19:30:00,120.53,52.551,57.056000000000004,29.93 -2020-09-08 19:45:00,111.59,52.668,57.056000000000004,29.93 -2020-09-08 20:00:00,103.31,51.019,64.156,29.93 -2020-09-08 20:15:00,102.93,50.501000000000005,64.156,29.93 -2020-09-08 20:30:00,100.46,49.946999999999996,64.156,29.93 -2020-09-08 20:45:00,102.55,49.711000000000006,64.156,29.93 -2020-09-08 21:00:00,101.45,48.318000000000005,56.507,29.93 -2020-09-08 21:15:00,102.7,49.847,56.507,29.93 -2020-09-08 21:30:00,97.23,49.58,56.507,29.93 -2020-09-08 21:45:00,96.21,49.055,56.507,29.93 -2020-09-08 22:00:00,91.16,46.526,50.728,29.93 -2020-09-08 22:15:00,91.57,47.123000000000005,50.728,29.93 -2020-09-08 22:30:00,84.85,39.665,50.728,29.93 -2020-09-08 22:45:00,86.91,36.171,50.728,29.93 -2020-09-08 23:00:00,84.69,33.22,43.556999999999995,29.93 -2020-09-08 23:15:00,85.78,32.317,43.556999999999995,29.93 -2020-09-08 23:30:00,82.52,32.239000000000004,43.556999999999995,29.93 -2020-09-08 23:45:00,85.59,31.436999999999998,43.556999999999995,29.93 -2020-09-09 00:00:00,82.31,31.622,41.151,29.93 -2020-09-09 00:15:00,83.13,32.187,41.151,29.93 -2020-09-09 00:30:00,79.51,31.819000000000003,41.151,29.93 -2020-09-09 00:45:00,77.56,31.813000000000002,41.151,29.93 -2020-09-09 01:00:00,79.6,31.435,37.763000000000005,29.93 -2020-09-09 01:15:00,81.91,30.766,37.763000000000005,29.93 -2020-09-09 01:30:00,82.15,29.803,37.763000000000005,29.93 -2020-09-09 01:45:00,79.99,28.886999999999997,37.763000000000005,29.93 -2020-09-09 02:00:00,74.52,28.623,35.615,29.93 -2020-09-09 02:15:00,74.8,28.405,35.615,29.93 -2020-09-09 02:30:00,81.69,28.874000000000002,35.615,29.93 -2020-09-09 02:45:00,82.63,29.683000000000003,35.615,29.93 -2020-09-09 03:00:00,83.44,30.685,35.153,29.93 -2020-09-09 03:15:00,78.79,30.186,35.153,29.93 -2020-09-09 03:30:00,84.28,30.133000000000003,35.153,29.93 -2020-09-09 03:45:00,86.29,30.113000000000003,35.153,29.93 -2020-09-09 04:00:00,89.48,36.935,36.203,29.93 -2020-09-09 04:15:00,89.42,44.39,36.203,29.93 -2020-09-09 04:30:00,94.57,41.785,36.203,29.93 -2020-09-09 04:45:00,102.54,42.376000000000005,36.203,29.93 -2020-09-09 05:00:00,108.61,60.409,39.922,29.93 -2020-09-09 05:15:00,107.95,72.247,39.922,29.93 -2020-09-09 05:30:00,111.12,66.142,39.922,29.93 -2020-09-09 05:45:00,111.45,61.985,39.922,29.93 -2020-09-09 06:00:00,113.78,62.07,56.443999999999996,29.93 -2020-09-09 06:15:00,116.37,63.535,56.443999999999996,29.93 -2020-09-09 06:30:00,119.03,62.123000000000005,56.443999999999996,29.93 -2020-09-09 06:45:00,118.81,63.743,56.443999999999996,29.93 -2020-09-09 07:00:00,118.73,62.062,68.683,29.93 -2020-09-09 07:15:00,118.39,63.321000000000005,68.683,29.93 -2020-09-09 07:30:00,116.09,61.806000000000004,68.683,29.93 -2020-09-09 07:45:00,114.53,62.681999999999995,68.683,29.93 -2020-09-09 08:00:00,113.91,61.396,59.003,29.93 -2020-09-09 08:15:00,112.48,63.184,59.003,29.93 -2020-09-09 08:30:00,113.23,62.471000000000004,59.003,29.93 -2020-09-09 08:45:00,114.44,63.788000000000004,59.003,29.93 -2020-09-09 09:00:00,111.57,60.216,56.21,29.93 -2020-09-09 09:15:00,109.34,58.714,56.21,29.93 -2020-09-09 09:30:00,109.66,60.67100000000001,56.21,29.93 -2020-09-09 09:45:00,109.6,62.044,56.21,29.93 -2020-09-09 10:00:00,108.97,58.495,52.358999999999995,29.93 -2020-09-09 10:15:00,108.59,58.974,52.358999999999995,29.93 -2020-09-09 10:30:00,107.81,58.653,52.358999999999995,29.93 -2020-09-09 10:45:00,107.09,59.638999999999996,52.358999999999995,29.93 -2020-09-09 11:00:00,105.96,56.187,51.161,29.93 -2020-09-09 11:15:00,104.7,57.163999999999994,51.161,29.93 -2020-09-09 11:30:00,105.82,57.81,51.161,29.93 -2020-09-09 11:45:00,102.87,58.482,51.161,29.93 -2020-09-09 12:00:00,103.56,54.586000000000006,49.119,29.93 -2020-09-09 12:15:00,100.18,54.136,49.119,29.93 -2020-09-09 12:30:00,98.72,53.456,49.119,29.93 -2020-09-09 12:45:00,100.89,54.467,49.119,29.93 -2020-09-09 13:00:00,101.22,53.369,49.187,29.93 -2020-09-09 13:15:00,101.0,53.93,49.187,29.93 -2020-09-09 13:30:00,103.88,52.728,49.187,29.93 -2020-09-09 13:45:00,102.94,51.888000000000005,49.187,29.93 -2020-09-09 14:00:00,103.48,52.784,49.787,29.93 -2020-09-09 14:15:00,104.49,51.917,49.787,29.93 -2020-09-09 14:30:00,101.7,50.873999999999995,49.787,29.93 -2020-09-09 14:45:00,101.43,51.63,49.787,29.93 -2020-09-09 15:00:00,101.46,51.931000000000004,51.458999999999996,29.93 -2020-09-09 15:15:00,101.99,49.961000000000006,51.458999999999996,29.93 -2020-09-09 15:30:00,102.58,48.946999999999996,51.458999999999996,29.93 -2020-09-09 15:45:00,104.51,46.772,51.458999999999996,29.93 -2020-09-09 16:00:00,104.61,48.935,53.663000000000004,29.93 -2020-09-09 16:15:00,109.4,48.835,53.663000000000004,29.93 -2020-09-09 16:30:00,108.58,49.201,53.663000000000004,29.93 -2020-09-09 16:45:00,109.96,47.181000000000004,53.663000000000004,29.93 -2020-09-09 17:00:00,112.76,48.776,58.183,29.93 -2020-09-09 17:15:00,112.11,49.945,58.183,29.93 -2020-09-09 17:30:00,115.42,49.458999999999996,58.183,29.93 -2020-09-09 17:45:00,113.9,48.728,58.183,29.93 -2020-09-09 18:00:00,112.53,50.887,60.141000000000005,29.93 -2020-09-09 18:15:00,111.03,50.411,60.141000000000005,29.93 -2020-09-09 18:30:00,112.22,48.534,60.141000000000005,29.93 -2020-09-09 18:45:00,115.87,52.662,60.141000000000005,29.93 -2020-09-09 19:00:00,114.45,55.199,60.582,29.93 -2020-09-09 19:15:00,114.54,53.925,60.582,29.93 -2020-09-09 19:30:00,114.12,52.881,60.582,29.93 -2020-09-09 19:45:00,114.17,52.997,60.582,29.93 -2020-09-09 20:00:00,107.24,51.36600000000001,66.61,29.93 -2020-09-09 20:15:00,100.92,50.848,66.61,29.93 -2020-09-09 20:30:00,105.27,50.271,66.61,29.93 -2020-09-09 20:45:00,105.34,49.992,66.61,29.93 -2020-09-09 21:00:00,100.98,48.603,57.658,29.93 -2020-09-09 21:15:00,97.14,50.12,57.658,29.93 -2020-09-09 21:30:00,95.75,49.858999999999995,57.658,29.93 -2020-09-09 21:45:00,95.25,49.29600000000001,57.658,29.93 -2020-09-09 22:00:00,92.5,46.744,51.81,29.93 -2020-09-09 22:15:00,86.93,47.318000000000005,51.81,29.93 -2020-09-09 22:30:00,87.86,39.836,51.81,29.93 -2020-09-09 22:45:00,86.09,36.346,51.81,29.93 -2020-09-09 23:00:00,84.21,33.443000000000005,42.93600000000001,29.93 -2020-09-09 23:15:00,82.99,32.514,42.93600000000001,29.93 -2020-09-09 23:30:00,83.34,32.443000000000005,42.93600000000001,29.93 -2020-09-09 23:45:00,84.51,31.645,42.93600000000001,29.93 -2020-09-10 00:00:00,80.33,31.846,39.211,29.93 -2020-09-10 00:15:00,77.06,32.409,39.211,29.93 -2020-09-10 00:30:00,80.13,32.05,39.211,29.93 -2020-09-10 00:45:00,81.28,32.049,39.211,29.93 -2020-09-10 01:00:00,81.02,31.653000000000002,37.607,29.93 -2020-09-10 01:15:00,75.14,31.005,37.607,29.93 -2020-09-10 01:30:00,73.91,30.059,37.607,29.93 -2020-09-10 01:45:00,77.05,29.145,37.607,29.93 -2020-09-10 02:00:00,80.23,28.884,36.44,29.93 -2020-09-10 02:15:00,81.87,28.69,36.44,29.93 -2020-09-10 02:30:00,73.49,29.133000000000003,36.44,29.93 -2020-09-10 02:45:00,74.47,29.936999999999998,36.44,29.93 -2020-09-10 03:00:00,77.1,30.927,36.116,29.93 -2020-09-10 03:15:00,80.63,30.445,36.116,29.93 -2020-09-10 03:30:00,83.84,30.397,36.116,29.93 -2020-09-10 03:45:00,83.29,30.363000000000003,36.116,29.93 -2020-09-10 04:00:00,83.53,37.223,37.398,29.93 -2020-09-10 04:15:00,89.58,44.708,37.398,29.93 -2020-09-10 04:30:00,94.75,42.11,37.398,29.93 -2020-09-10 04:45:00,100.81,42.708999999999996,37.398,29.93 -2020-09-10 05:00:00,101.58,60.841,41.776,29.93 -2020-09-10 05:15:00,104.3,72.78699999999999,41.776,29.93 -2020-09-10 05:30:00,107.36,66.663,41.776,29.93 -2020-09-10 05:45:00,107.9,62.453,41.776,29.93 -2020-09-10 06:00:00,114.18,62.5,55.61,29.93 -2020-09-10 06:15:00,116.19,63.99,55.61,29.93 -2020-09-10 06:30:00,117.06,62.575,55.61,29.93 -2020-09-10 06:45:00,118.62,64.19,55.61,29.93 -2020-09-10 07:00:00,119.66,62.50899999999999,67.13600000000001,29.93 -2020-09-10 07:15:00,118.33,63.782,67.13600000000001,29.93 -2020-09-10 07:30:00,118.98,62.299,67.13600000000001,29.93 -2020-09-10 07:45:00,118.75,63.175,67.13600000000001,29.93 -2020-09-10 08:00:00,117.68,61.891999999999996,57.55,29.93 -2020-09-10 08:15:00,118.15,63.641000000000005,57.55,29.93 -2020-09-10 08:30:00,118.58,62.933,57.55,29.93 -2020-09-10 08:45:00,119.2,64.232,57.55,29.93 -2020-09-10 09:00:00,121.14,60.669,52.931999999999995,29.93 -2020-09-10 09:15:00,118.91,59.157,52.931999999999995,29.93 -2020-09-10 09:30:00,120.24,61.093,52.931999999999995,29.93 -2020-09-10 09:45:00,124.85,62.435,52.931999999999995,29.93 -2020-09-10 10:00:00,125.58,58.882,50.36600000000001,29.93 -2020-09-10 10:15:00,124.15,59.327,50.36600000000001,29.93 -2020-09-10 10:30:00,125.44,58.995,50.36600000000001,29.93 -2020-09-10 10:45:00,121.44,59.967,50.36600000000001,29.93 -2020-09-10 11:00:00,119.2,56.528,47.893,29.93 -2020-09-10 11:15:00,121.84,57.49100000000001,47.893,29.93 -2020-09-10 11:30:00,126.86,58.136,47.893,29.93 -2020-09-10 11:45:00,126.23,58.79,47.893,29.93 -2020-09-10 12:00:00,125.52,54.871,45.271,29.93 -2020-09-10 12:15:00,125.28,54.406000000000006,45.271,29.93 -2020-09-10 12:30:00,120.23,53.754,45.271,29.93 -2020-09-10 12:45:00,121.25,54.754,45.271,29.93 -2020-09-10 13:00:00,119.37,53.633,44.351000000000006,29.93 -2020-09-10 13:15:00,120.66,54.18600000000001,44.351000000000006,29.93 -2020-09-10 13:30:00,119.3,52.98,44.351000000000006,29.93 -2020-09-10 13:45:00,117.09,52.148,44.351000000000006,29.93 -2020-09-10 14:00:00,117.58,53.005,44.99,29.93 -2020-09-10 14:15:00,117.93,52.148999999999994,44.99,29.93 -2020-09-10 14:30:00,115.71,51.13399999999999,44.99,29.93 -2020-09-10 14:45:00,114.86,51.888000000000005,44.99,29.93 -2020-09-10 15:00:00,113.58,52.13399999999999,46.869,29.93 -2020-09-10 15:15:00,112.65,50.178999999999995,46.869,29.93 -2020-09-10 15:30:00,113.09,49.191,46.869,29.93 -2020-09-10 15:45:00,113.8,47.027,46.869,29.93 -2020-09-10 16:00:00,114.36,49.151,48.902,29.93 -2020-09-10 16:15:00,116.87,49.066,48.902,29.93 -2020-09-10 16:30:00,117.85,49.43,48.902,29.93 -2020-09-10 16:45:00,119.64,47.464,48.902,29.93 -2020-09-10 17:00:00,120.81,49.018,53.244,29.93 -2020-09-10 17:15:00,118.06,50.216,53.244,29.93 -2020-09-10 17:30:00,118.85,49.74100000000001,53.244,29.93 -2020-09-10 17:45:00,118.18,49.047,53.244,29.93 -2020-09-10 18:00:00,119.17,51.18899999999999,54.343999999999994,29.93 -2020-09-10 18:15:00,118.52,50.725,54.343999999999994,29.93 -2020-09-10 18:30:00,117.98,48.86,54.343999999999994,29.93 -2020-09-10 18:45:00,117.71,52.985,54.343999999999994,29.93 -2020-09-10 19:00:00,118.08,55.531000000000006,54.332,29.93 -2020-09-10 19:15:00,117.58,54.257,54.332,29.93 -2020-09-10 19:30:00,115.86,53.214,54.332,29.93 -2020-09-10 19:45:00,111.56,53.33,54.332,29.93 -2020-09-10 20:00:00,103.85,51.715,58.06,29.93 -2020-09-10 20:15:00,104.58,51.196999999999996,58.06,29.93 -2020-09-10 20:30:00,99.86,50.598,58.06,29.93 -2020-09-10 20:45:00,103.55,50.276,58.06,29.93 -2020-09-10 21:00:00,99.44,48.89,52.411,29.93 -2020-09-10 21:15:00,100.62,50.397,52.411,29.93 -2020-09-10 21:30:00,92.72,50.141000000000005,52.411,29.93 -2020-09-10 21:45:00,88.73,49.54,52.411,29.93 -2020-09-10 22:00:00,87.31,46.963,47.148999999999994,29.93 -2020-09-10 22:15:00,88.58,47.515,47.148999999999994,29.93 -2020-09-10 22:30:00,84.17,40.009,47.148999999999994,29.93 -2020-09-10 22:45:00,83.22,36.524,47.148999999999994,29.93 -2020-09-10 23:00:00,80.89,33.669000000000004,40.814,29.93 -2020-09-10 23:15:00,82.71,32.713,40.814,29.93 -2020-09-10 23:30:00,78.26,32.649,40.814,29.93 -2020-09-10 23:45:00,82.96,31.855999999999998,40.814,29.93 -2020-09-11 00:00:00,75.6,30.344,39.153,29.93 -2020-09-11 00:15:00,80.38,31.125,39.153,29.93 -2020-09-11 00:30:00,80.19,30.996,39.153,29.93 -2020-09-11 00:45:00,79.37,31.391,39.153,29.93 -2020-09-11 01:00:00,72.6,30.596999999999998,37.228,29.93 -2020-09-11 01:15:00,79.38,29.555,37.228,29.93 -2020-09-11 01:30:00,77.58,29.232,37.228,29.93 -2020-09-11 01:45:00,80.0,28.109,37.228,29.93 -2020-09-11 02:00:00,76.16,28.691,35.851,29.93 -2020-09-11 02:15:00,80.66,28.458000000000002,35.851,29.93 -2020-09-11 02:30:00,81.2,29.677,35.851,29.93 -2020-09-11 02:45:00,80.38,29.875999999999998,35.851,29.93 -2020-09-11 03:00:00,76.4,31.41,36.54,29.93 -2020-09-11 03:15:00,80.73,29.999000000000002,36.54,29.93 -2020-09-11 03:30:00,84.41,29.741999999999997,36.54,29.93 -2020-09-11 03:45:00,81.82,30.514,36.54,29.93 -2020-09-11 04:00:00,85.33,37.564,37.578,29.93 -2020-09-11 04:15:00,91.54,43.666000000000004,37.578,29.93 -2020-09-11 04:30:00,94.67,41.95,37.578,29.93 -2020-09-11 04:45:00,96.14,41.833,37.578,29.93 -2020-09-11 05:00:00,101.04,59.476000000000006,40.387,29.93 -2020-09-11 05:15:00,102.73,72.569,40.387,29.93 -2020-09-11 05:30:00,108.87,66.734,40.387,29.93 -2020-09-11 05:45:00,111.53,62.067,40.387,29.93 -2020-09-11 06:00:00,116.13,62.336000000000006,54.668,29.93 -2020-09-11 06:15:00,118.68,63.81100000000001,54.668,29.93 -2020-09-11 06:30:00,120.17,62.233999999999995,54.668,29.93 -2020-09-11 06:45:00,123.12,63.937,54.668,29.93 -2020-09-11 07:00:00,125.11,62.724,63.971000000000004,29.93 -2020-09-11 07:15:00,125.24,64.94800000000001,63.971000000000004,29.93 -2020-09-11 07:30:00,125.46,61.755,63.971000000000004,29.93 -2020-09-11 07:45:00,125.76,62.323,63.971000000000004,29.93 -2020-09-11 08:00:00,124.69,61.582,56.042,29.93 -2020-09-11 08:15:00,125.79,63.826,56.042,29.93 -2020-09-11 08:30:00,127.99,63.174,56.042,29.93 -2020-09-11 08:45:00,128.03,64.111,56.042,29.93 -2020-09-11 09:00:00,127.4,58.596000000000004,52.832,29.93 -2020-09-11 09:15:00,127.15,58.831,52.832,29.93 -2020-09-11 09:30:00,126.24,60.083999999999996,52.832,29.93 -2020-09-11 09:45:00,126.73,61.718,52.832,29.93 -2020-09-11 10:00:00,123.95,57.843999999999994,50.044,29.93 -2020-09-11 10:15:00,126.91,58.161,50.044,29.93 -2020-09-11 10:30:00,126.92,58.288000000000004,50.044,29.93 -2020-09-11 10:45:00,126.94,59.099,50.044,29.93 -2020-09-11 11:00:00,124.25,55.891999999999996,49.06100000000001,29.93 -2020-09-11 11:15:00,122.43,55.766000000000005,49.06100000000001,29.93 -2020-09-11 11:30:00,123.76,56.348,49.06100000000001,29.93 -2020-09-11 11:45:00,121.22,56.158,49.06100000000001,29.93 -2020-09-11 12:00:00,117.99,52.754,45.595,29.93 -2020-09-11 12:15:00,118.71,51.409,45.595,29.93 -2020-09-11 12:30:00,115.33,50.912,45.595,29.93 -2020-09-11 12:45:00,114.1,51.29600000000001,45.595,29.93 -2020-09-11 13:00:00,113.23,50.799,43.218,29.93 -2020-09-11 13:15:00,112.25,51.623000000000005,43.218,29.93 -2020-09-11 13:30:00,110.79,51.105,43.218,29.93 -2020-09-11 13:45:00,111.31,50.538000000000004,43.218,29.93 -2020-09-11 14:00:00,110.24,50.486999999999995,41.926,29.93 -2020-09-11 14:15:00,112.95,49.996,41.926,29.93 -2020-09-11 14:30:00,108.86,50.391999999999996,41.926,29.93 -2020-09-11 14:45:00,112.25,50.591,41.926,29.93 -2020-09-11 15:00:00,111.4,50.64,43.79,29.93 -2020-09-11 15:15:00,111.65,48.431000000000004,43.79,29.93 -2020-09-11 15:30:00,110.79,46.753,43.79,29.93 -2020-09-11 15:45:00,107.67,45.287,43.79,29.93 -2020-09-11 16:00:00,107.07,46.474,45.895,29.93 -2020-09-11 16:15:00,108.07,46.873999999999995,45.895,29.93 -2020-09-11 16:30:00,106.95,47.09,45.895,29.93 -2020-09-11 16:45:00,105.52,44.477,45.895,29.93 -2020-09-11 17:00:00,109.06,47.535,51.36,29.93 -2020-09-11 17:15:00,109.74,48.553000000000004,51.36,29.93 -2020-09-11 17:30:00,110.3,48.187,51.36,29.93 -2020-09-11 17:45:00,111.89,47.353,51.36,29.93 -2020-09-11 18:00:00,111.62,49.632,52.985,29.93 -2020-09-11 18:15:00,109.54,48.294,52.985,29.93 -2020-09-11 18:30:00,111.46,46.401,52.985,29.93 -2020-09-11 18:45:00,114.19,50.891999999999996,52.985,29.93 -2020-09-11 19:00:00,112.04,54.356,52.602,29.93 -2020-09-11 19:15:00,108.7,53.778,52.602,29.93 -2020-09-11 19:30:00,105.58,52.738,52.602,29.93 -2020-09-11 19:45:00,103.63,51.891999999999996,52.602,29.93 -2020-09-11 20:00:00,98.96,50.158,58.063,29.93 -2020-09-11 20:15:00,100.7,50.345,58.063,29.93 -2020-09-11 20:30:00,97.79,49.286,58.063,29.93 -2020-09-11 20:45:00,98.82,48.273999999999994,58.063,29.93 -2020-09-11 21:00:00,92.16,48.119,50.135,29.93 -2020-09-11 21:15:00,87.15,51.151,50.135,29.93 -2020-09-11 21:30:00,82.01,50.773999999999994,50.135,29.93 -2020-09-11 21:45:00,83.79,50.385,50.135,29.93 -2020-09-11 22:00:00,84.45,47.785,45.165,29.93 -2020-09-11 22:15:00,82.84,48.075,45.165,29.93 -2020-09-11 22:30:00,74.05,45.567,45.165,29.93 -2020-09-11 22:45:00,75.88,43.292,45.165,29.93 -2020-09-11 23:00:00,67.88,41.926,39.121,29.93 -2020-09-11 23:15:00,66.06,39.306999999999995,39.121,29.93 -2020-09-11 23:30:00,65.9,37.449,39.121,29.93 -2020-09-11 23:45:00,73.95,36.47,39.121,29.93 -2020-09-12 00:00:00,72.49,31.303,38.49,29.816 -2020-09-12 00:15:00,70.54,30.943,38.49,29.816 -2020-09-12 00:30:00,63.74,30.576,38.49,29.816 -2020-09-12 00:45:00,64.17,30.421999999999997,38.49,29.816 -2020-09-12 01:00:00,63.03,29.933000000000003,34.5,29.816 -2020-09-12 01:15:00,61.92,29.307,34.5,29.816 -2020-09-12 01:30:00,61.64,28.223000000000003,34.5,29.816 -2020-09-12 01:45:00,61.96,28.176,34.5,29.816 -2020-09-12 02:00:00,62.24,27.976,32.236,29.816 -2020-09-12 02:15:00,67.5,27.022,32.236,29.816 -2020-09-12 02:30:00,67.15,27.374000000000002,32.236,29.816 -2020-09-12 02:45:00,66.48,28.275,32.236,29.816 -2020-09-12 03:00:00,65.68,28.665,32.067,29.816 -2020-09-12 03:15:00,61.69,26.500999999999998,32.067,29.816 -2020-09-12 03:30:00,61.21,26.336,32.067,29.816 -2020-09-12 03:45:00,61.06,28.415,32.067,29.816 -2020-09-12 04:00:00,62.25,33.311,33.071,29.816 -2020-09-12 04:15:00,61.64,38.334,33.071,29.816 -2020-09-12 04:30:00,61.43,34.878,33.071,29.816 -2020-09-12 04:45:00,64.14,34.945,33.071,29.816 -2020-09-12 05:00:00,66.46,43.589,33.014,29.816 -2020-09-12 05:15:00,68.23,44.825,33.014,29.816 -2020-09-12 05:30:00,66.76,40.367,33.014,29.816 -2020-09-12 05:45:00,68.02,40.258,33.014,29.816 -2020-09-12 06:00:00,70.3,52.581,34.628,29.816 -2020-09-12 06:15:00,72.97,62.941,34.628,29.816 -2020-09-12 06:30:00,74.15,58.201,34.628,29.816 -2020-09-12 06:45:00,76.25,55.983999999999995,34.628,29.816 -2020-09-12 07:00:00,77.74,52.931999999999995,38.871,29.816 -2020-09-12 07:15:00,80.76,53.872,38.871,29.816 -2020-09-12 07:30:00,82.04,52.38,38.871,29.816 -2020-09-12 07:45:00,79.99,54.251999999999995,38.871,29.816 -2020-09-12 08:00:00,80.65,54.566,43.293,29.816 -2020-09-12 08:15:00,84.33,57.047,43.293,29.816 -2020-09-12 08:30:00,80.92,56.593,43.293,29.816 -2020-09-12 08:45:00,82.18,58.745,43.293,29.816 -2020-09-12 09:00:00,73.56,56.155,44.559,29.816 -2020-09-12 09:15:00,72.42,56.9,44.559,29.816 -2020-09-12 09:30:00,72.7,58.683,44.559,29.816 -2020-09-12 09:45:00,77.44,59.915,44.559,29.816 -2020-09-12 10:00:00,76.92,56.563,42.091,29.816 -2020-09-12 10:15:00,82.93,57.177,42.091,29.816 -2020-09-12 10:30:00,75.13,57.018,42.091,29.816 -2020-09-12 10:45:00,78.26,57.68899999999999,42.091,29.816 -2020-09-12 11:00:00,75.56,54.4,38.505,29.816 -2020-09-12 11:15:00,72.28,54.917,38.505,29.816 -2020-09-12 11:30:00,70.41,55.589,38.505,29.816 -2020-09-12 11:45:00,69.55,55.831,38.505,29.816 -2020-09-12 12:00:00,69.43,52.578,35.388000000000005,29.816 -2020-09-12 12:15:00,67.6,52.043,35.388000000000005,29.816 -2020-09-12 12:30:00,66.35,51.507,35.388000000000005,29.816 -2020-09-12 12:45:00,66.07,52.397,35.388000000000005,29.816 -2020-09-12 13:00:00,63.61,51.104,31.355999999999998,29.816 -2020-09-12 13:15:00,68.82,51.163999999999994,31.355999999999998,29.816 -2020-09-12 13:30:00,66.78,50.739,31.355999999999998,29.816 -2020-09-12 13:45:00,66.36,49.083999999999996,31.355999999999998,29.816 -2020-09-12 14:00:00,68.36,49.22,30.522,29.816 -2020-09-12 14:15:00,69.67,47.583999999999996,30.522,29.816 -2020-09-12 14:30:00,66.25,47.385,30.522,29.816 -2020-09-12 14:45:00,67.5,48.027,30.522,29.816 -2020-09-12 15:00:00,68.44,48.504,34.36,29.816 -2020-09-12 15:15:00,69.12,47.034,34.36,29.816 -2020-09-12 15:30:00,69.44,45.773,34.36,29.816 -2020-09-12 15:45:00,70.61,43.525,34.36,29.816 -2020-09-12 16:00:00,73.84,46.43,39.507,29.816 -2020-09-12 16:15:00,75.23,46.185,39.507,29.816 -2020-09-12 16:30:00,75.45,46.589,39.507,29.816 -2020-09-12 16:45:00,77.86,44.126999999999995,39.507,29.816 -2020-09-12 17:00:00,81.34,46.038000000000004,47.151,29.816 -2020-09-12 17:15:00,84.36,45.433,47.151,29.816 -2020-09-12 17:30:00,82.72,44.951,47.151,29.816 -2020-09-12 17:45:00,83.58,44.531000000000006,47.151,29.816 -2020-09-12 18:00:00,85.61,47.988,50.303999999999995,29.816 -2020-09-12 18:15:00,84.62,48.356,50.303999999999995,29.816 -2020-09-12 18:30:00,89.46,47.836999999999996,50.303999999999995,29.816 -2020-09-12 18:45:00,89.86,48.85,50.303999999999995,29.816 -2020-09-12 19:00:00,88.15,51.066,50.622,29.816 -2020-09-12 19:15:00,83.92,49.538000000000004,50.622,29.816 -2020-09-12 19:30:00,83.24,49.269,50.622,29.816 -2020-09-12 19:45:00,82.65,49.957,50.622,29.816 -2020-09-12 20:00:00,80.31,49.22,45.391000000000005,29.816 -2020-09-12 20:15:00,77.48,49.12,45.391000000000005,29.816 -2020-09-12 20:30:00,76.12,47.223,45.391000000000005,29.816 -2020-09-12 20:45:00,75.87,47.74100000000001,45.391000000000005,29.816 -2020-09-12 21:00:00,71.68,46.589,39.98,29.816 -2020-09-12 21:15:00,70.68,49.358999999999995,39.98,29.816 -2020-09-12 21:30:00,68.86,49.309,39.98,29.816 -2020-09-12 21:45:00,68.45,48.352,39.98,29.816 -2020-09-12 22:00:00,64.74,45.873999999999995,37.53,29.816 -2020-09-12 22:15:00,65.48,46.672,37.53,29.816 -2020-09-12 22:30:00,62.36,44.59,37.53,29.816 -2020-09-12 22:45:00,62.8,42.88,37.53,29.816 -2020-09-12 23:00:00,57.88,41.266999999999996,30.97,29.816 -2020-09-12 23:15:00,56.65,38.785,30.97,29.816 -2020-09-12 23:30:00,58.13,38.875,30.97,29.816 -2020-09-12 23:45:00,57.91,37.913000000000004,30.97,29.816 -2020-09-13 00:00:00,53.28,32.799,27.24,29.816 -2020-09-13 00:15:00,53.54,31.384,27.24,29.816 -2020-09-13 00:30:00,53.48,30.849,27.24,29.816 -2020-09-13 00:45:00,53.88,30.689,27.24,29.816 -2020-09-13 01:00:00,51.24,30.398000000000003,25.662,29.816 -2020-09-13 01:15:00,52.62,29.816,25.662,29.816 -2020-09-13 01:30:00,52.7,28.694000000000003,25.662,29.816 -2020-09-13 01:45:00,52.44,28.274,25.662,29.816 -2020-09-13 02:00:00,53.25,28.054000000000002,25.67,29.816 -2020-09-13 02:15:00,52.44,27.636999999999997,25.67,29.816 -2020-09-13 02:30:00,51.82,28.238000000000003,25.67,29.816 -2020-09-13 02:45:00,51.81,28.935,25.67,29.816 -2020-09-13 03:00:00,50.96,29.924,24.258000000000003,29.816 -2020-09-13 03:15:00,52.6,27.941,24.258000000000003,29.816 -2020-09-13 03:30:00,52.63,27.311,24.258000000000003,29.816 -2020-09-13 03:45:00,52.9,28.666999999999998,24.258000000000003,29.816 -2020-09-13 04:00:00,53.7,33.546,25.051,29.816 -2020-09-13 04:15:00,54.59,38.092,25.051,29.816 -2020-09-13 04:30:00,54.4,35.829,25.051,29.816 -2020-09-13 04:45:00,55.85,35.528,25.051,29.816 -2020-09-13 05:00:00,57.42,44.011,25.145,29.816 -2020-09-13 05:15:00,58.05,44.397,25.145,29.816 -2020-09-13 05:30:00,55.99,39.544000000000004,25.145,29.816 -2020-09-13 05:45:00,56.86,39.168,25.145,29.816 -2020-09-13 06:00:00,57.81,49.345,26.371,29.816 -2020-09-13 06:15:00,58.65,60.265,26.371,29.816 -2020-09-13 06:30:00,58.81,54.775,26.371,29.816 -2020-09-13 06:45:00,60.74,51.551,26.371,29.816 -2020-09-13 07:00:00,61.74,49.041000000000004,28.756999999999998,29.816 -2020-09-13 07:15:00,61.61,48.45,28.756999999999998,29.816 -2020-09-13 07:30:00,61.01,47.961999999999996,28.756999999999998,29.816 -2020-09-13 07:45:00,61.51,49.708999999999996,28.756999999999998,29.816 -2020-09-13 08:00:00,61.6,50.91,32.82,29.816 -2020-09-13 08:15:00,63.67,54.369,32.82,29.816 -2020-09-13 08:30:00,61.43,54.89,32.82,29.816 -2020-09-13 08:45:00,60.33,57.203,32.82,29.816 -2020-09-13 09:00:00,58.83,54.472,35.534,29.816 -2020-09-13 09:15:00,60.35,54.845,35.534,29.816 -2020-09-13 09:30:00,60.42,56.964,35.534,29.816 -2020-09-13 09:45:00,60.83,59.023999999999994,35.534,29.816 -2020-09-13 10:00:00,61.52,56.451,35.925,29.816 -2020-09-13 10:15:00,63.03,57.215,35.925,29.816 -2020-09-13 10:30:00,64.13,57.315,35.925,29.816 -2020-09-13 10:45:00,64.9,58.61,35.925,29.816 -2020-09-13 11:00:00,61.16,55.151,37.056,29.816 -2020-09-13 11:15:00,60.3,55.281000000000006,37.056,29.816 -2020-09-13 11:30:00,56.6,56.294,37.056,29.816 -2020-09-13 11:45:00,58.49,56.818000000000005,37.056,29.816 -2020-09-13 12:00:00,52.37,54.376999999999995,33.124,29.816 -2020-09-13 12:15:00,54.75,53.535,33.124,29.816 -2020-09-13 12:30:00,50.06,53.025,33.124,29.816 -2020-09-13 12:45:00,50.17,53.224,33.124,29.816 -2020-09-13 13:00:00,49.32,51.556999999999995,29.874000000000002,29.816 -2020-09-13 13:15:00,49.67,51.497,29.874000000000002,29.816 -2020-09-13 13:30:00,48.44,50.077,29.874000000000002,29.816 -2020-09-13 13:45:00,49.63,49.31399999999999,29.874000000000002,29.816 -2020-09-13 14:00:00,49.39,50.481,27.302,29.816 -2020-09-13 14:15:00,50.02,49.385,27.302,29.816 -2020-09-13 14:30:00,50.96,48.302,27.302,29.816 -2020-09-13 14:45:00,52.31,47.96,27.302,29.816 -2020-09-13 15:00:00,52.47,48.31100000000001,27.642,29.816 -2020-09-13 15:15:00,56.04,46.251000000000005,27.642,29.816 -2020-09-13 15:30:00,55.91,44.931000000000004,27.642,29.816 -2020-09-13 15:45:00,58.62,43.033,27.642,29.816 -2020-09-13 16:00:00,63.63,44.687,31.945999999999998,29.816 -2020-09-13 16:15:00,63.63,44.543,31.945999999999998,29.816 -2020-09-13 16:30:00,67.25,45.853,31.945999999999998,29.816 -2020-09-13 16:45:00,69.54,43.519,31.945999999999998,29.816 -2020-09-13 17:00:00,73.29,45.705,40.387,29.816 -2020-09-13 17:15:00,75.48,46.41,40.387,29.816 -2020-09-13 17:30:00,76.49,46.678000000000004,40.387,29.816 -2020-09-13 17:45:00,80.06,46.943999999999996,40.387,29.816 -2020-09-13 18:00:00,80.24,50.817,44.575,29.816 -2020-09-13 18:15:00,80.31,50.998000000000005,44.575,29.816 -2020-09-13 18:30:00,86.23,50.026,44.575,29.816 -2020-09-13 18:45:00,84.95,51.357,44.575,29.816 -2020-09-13 19:00:00,83.91,55.43899999999999,45.623999999999995,29.816 -2020-09-13 19:15:00,82.55,53.001000000000005,45.623999999999995,29.816 -2020-09-13 19:30:00,81.33,52.494,45.623999999999995,29.816 -2020-09-13 19:45:00,82.57,52.943999999999996,45.623999999999995,29.816 -2020-09-13 20:00:00,82.59,52.394,44.583999999999996,29.816 -2020-09-13 20:15:00,85.52,52.29,44.583999999999996,29.816 -2020-09-13 20:30:00,81.65,51.228,44.583999999999996,29.816 -2020-09-13 20:45:00,80.24,50.083999999999996,44.583999999999996,29.816 -2020-09-13 21:00:00,74.96,48.508,39.732,29.816 -2020-09-13 21:15:00,80.14,50.928000000000004,39.732,29.816 -2020-09-13 21:30:00,82.33,50.347,39.732,29.816 -2020-09-13 21:45:00,81.42,49.684,39.732,29.816 -2020-09-13 22:00:00,76.5,48.83,38.571,29.816 -2020-09-13 22:15:00,71.7,48.037,38.571,29.816 -2020-09-13 22:30:00,69.2,45.117,38.571,29.816 -2020-09-13 22:45:00,69.21,42.193000000000005,38.571,29.816 -2020-09-13 23:00:00,66.31,40.039,33.121,29.816 -2020-09-13 23:15:00,68.89,38.861,33.121,29.816 -2020-09-13 23:30:00,67.54,38.577,33.121,29.816 -2020-09-13 23:45:00,72.63,37.861999999999995,33.121,29.816 -2020-09-14 00:00:00,70.49,34.855,32.506,29.93 -2020-09-14 00:15:00,71.68,34.483000000000004,32.506,29.93 -2020-09-14 00:30:00,66.33,33.631,32.506,29.93 -2020-09-14 00:45:00,63.56,33.067,32.506,29.93 -2020-09-14 01:00:00,65.34,33.099000000000004,31.121,29.93 -2020-09-14 01:15:00,71.25,32.488,31.121,29.93 -2020-09-14 01:30:00,71.36,31.708000000000002,31.121,29.93 -2020-09-14 01:45:00,70.39,31.218000000000004,31.121,29.93 -2020-09-14 02:00:00,67.0,31.398000000000003,29.605999999999998,29.93 -2020-09-14 02:15:00,72.09,30.285,29.605999999999998,29.93 -2020-09-14 02:30:00,73.13,31.045,29.605999999999998,29.93 -2020-09-14 02:45:00,72.63,31.535,29.605999999999998,29.93 -2020-09-14 03:00:00,70.81,33.106,28.124000000000002,29.93 -2020-09-14 03:15:00,74.52,31.967,28.124000000000002,29.93 -2020-09-14 03:30:00,76.44,31.909000000000002,28.124000000000002,29.93 -2020-09-14 03:45:00,75.06,32.779,28.124000000000002,29.93 -2020-09-14 04:00:00,77.33,40.942,29.743000000000002,29.93 -2020-09-14 04:15:00,83.88,48.583999999999996,29.743000000000002,29.93 -2020-09-14 04:30:00,89.01,46.166000000000004,29.743000000000002,29.93 -2020-09-14 04:45:00,92.93,46.215,29.743000000000002,29.93 -2020-09-14 05:00:00,94.93,62.798,36.191,29.93 -2020-09-14 05:15:00,104.51,74.667,36.191,29.93 -2020-09-14 05:30:00,102.86,68.46,36.191,29.93 -2020-09-14 05:45:00,103.19,64.678,36.191,29.93 -2020-09-14 06:00:00,108.82,63.763999999999996,55.277,29.93 -2020-09-14 06:15:00,111.8,65.002,55.277,29.93 -2020-09-14 06:30:00,112.82,63.923,55.277,29.93 -2020-09-14 06:45:00,111.4,66.351,55.277,29.93 -2020-09-14 07:00:00,115.98,64.559,65.697,29.93 -2020-09-14 07:15:00,110.78,66.132,65.697,29.93 -2020-09-14 07:30:00,110.77,64.83,65.697,29.93 -2020-09-14 07:45:00,108.81,66.53399999999999,65.697,29.93 -2020-09-14 08:00:00,107.66,65.313,57.028,29.93 -2020-09-14 08:15:00,106.67,67.42,57.028,29.93 -2020-09-14 08:30:00,111.75,66.61,57.028,29.93 -2020-09-14 08:45:00,114.96,68.684,57.028,29.93 -2020-09-14 09:00:00,107.61,64.957,52.633,29.93 -2020-09-14 09:15:00,104.76,63.328,52.633,29.93 -2020-09-14 09:30:00,104.17,64.493,52.633,29.93 -2020-09-14 09:45:00,103.52,64.554,52.633,29.93 -2020-09-14 10:00:00,104.08,62.263000000000005,50.647,29.93 -2020-09-14 10:15:00,103.09,62.821000000000005,50.647,29.93 -2020-09-14 10:30:00,103.8,62.4,50.647,29.93 -2020-09-14 10:45:00,103.53,62.397,50.647,29.93 -2020-09-14 11:00:00,102.76,58.821999999999996,50.245,29.93 -2020-09-14 11:15:00,103.69,59.413000000000004,50.245,29.93 -2020-09-14 11:30:00,102.69,61.208,50.245,29.93 -2020-09-14 11:45:00,105.2,62.097,50.245,29.93 -2020-09-14 12:00:00,101.81,58.416000000000004,46.956,29.93 -2020-09-14 12:15:00,103.71,57.656000000000006,46.956,29.93 -2020-09-14 12:30:00,101.04,56.28,46.956,29.93 -2020-09-14 12:45:00,103.68,56.665,46.956,29.93 -2020-09-14 13:00:00,99.52,55.81399999999999,47.383,29.93 -2020-09-14 13:15:00,98.82,54.8,47.383,29.93 -2020-09-14 13:30:00,100.67,53.45399999999999,47.383,29.93 -2020-09-14 13:45:00,102.23,53.481,47.383,29.93 -2020-09-14 14:00:00,102.81,53.742,47.1,29.93 -2020-09-14 14:15:00,100.22,53.089,47.1,29.93 -2020-09-14 14:30:00,100.3,51.821999999999996,47.1,29.93 -2020-09-14 14:45:00,98.73,53.272,47.1,29.93 -2020-09-14 15:00:00,99.67,53.479,49.355,29.93 -2020-09-14 15:15:00,101.22,50.736999999999995,49.355,29.93 -2020-09-14 15:30:00,103.35,50.004,49.355,29.93 -2020-09-14 15:45:00,102.28,47.64,49.355,29.93 -2020-09-14 16:00:00,103.7,50.174,52.14,29.93 -2020-09-14 16:15:00,104.69,50.006,52.14,29.93 -2020-09-14 16:30:00,107.28,50.61,52.14,29.93 -2020-09-14 16:45:00,109.54,48.20399999999999,52.14,29.93 -2020-09-14 17:00:00,111.52,49.339,58.705,29.93 -2020-09-14 17:15:00,109.57,50.282,58.705,29.93 -2020-09-14 17:30:00,112.39,50.155,58.705,29.93 -2020-09-14 17:45:00,111.73,49.925,58.705,29.93 -2020-09-14 18:00:00,113.13,52.857,59.153,29.93 -2020-09-14 18:15:00,111.59,51.177,59.153,29.93 -2020-09-14 18:30:00,117.13,49.633,59.153,29.93 -2020-09-14 18:45:00,115.67,53.824,59.153,29.93 -2020-09-14 19:00:00,112.01,57.446000000000005,61.483000000000004,29.93 -2020-09-14 19:15:00,107.89,56.045,61.483000000000004,29.93 -2020-09-14 19:30:00,105.2,55.27,61.483000000000004,29.93 -2020-09-14 19:45:00,108.61,55.082,61.483000000000004,29.93 -2020-09-14 20:00:00,105.34,53.168,67.55,29.93 -2020-09-14 20:15:00,106.11,54.007,67.55,29.93 -2020-09-14 20:30:00,103.61,53.163000000000004,67.55,29.93 -2020-09-14 20:45:00,94.43,52.355,67.55,29.93 -2020-09-14 21:00:00,92.43,50.291000000000004,60.026,29.93 -2020-09-14 21:15:00,89.42,52.953,60.026,29.93 -2020-09-14 21:30:00,85.77,52.648999999999994,60.026,29.93 -2020-09-14 21:45:00,91.92,51.695,60.026,29.93 -2020-09-14 22:00:00,88.36,48.677,52.736999999999995,29.93 -2020-09-14 22:15:00,88.46,49.465,52.736999999999995,29.93 -2020-09-14 22:30:00,82.45,41.596000000000004,52.736999999999995,29.93 -2020-09-14 22:45:00,82.32,38.1,52.736999999999995,29.93 -2020-09-14 23:00:00,80.56,36.128,44.408,29.93 -2020-09-14 23:15:00,81.23,33.766,44.408,29.93 -2020-09-14 23:30:00,76.93,33.77,44.408,29.93 -2020-09-14 23:45:00,75.97,32.887,44.408,29.93 -2020-09-15 00:00:00,76.99,32.999,44.438,29.93 -2020-09-15 00:15:00,79.26,33.558,44.438,29.93 -2020-09-15 00:30:00,78.82,33.234,44.438,29.93 -2020-09-15 00:45:00,74.27,33.263000000000005,44.438,29.93 -2020-09-15 01:00:00,70.44,32.773,41.468999999999994,29.93 -2020-09-15 01:15:00,74.86,32.23,41.468999999999994,29.93 -2020-09-15 01:30:00,77.73,31.374000000000002,41.468999999999994,29.93 -2020-09-15 01:45:00,78.5,30.465999999999998,41.468999999999994,29.93 -2020-09-15 02:00:00,77.58,30.223000000000003,39.708,29.93 -2020-09-15 02:15:00,76.77,30.145,39.708,29.93 -2020-09-15 02:30:00,78.83,30.465,39.708,29.93 -2020-09-15 02:45:00,78.93,31.241,39.708,29.93 -2020-09-15 03:00:00,79.89,32.166,38.919000000000004,29.93 -2020-09-15 03:15:00,80.14,31.774,38.919000000000004,29.93 -2020-09-15 03:30:00,82.91,31.75,38.919000000000004,29.93 -2020-09-15 03:45:00,83.91,31.641,38.919000000000004,29.93 -2020-09-15 04:00:00,85.18,38.702,40.092,29.93 -2020-09-15 04:15:00,89.49,46.347,40.092,29.93 -2020-09-15 04:30:00,94.24,43.79600000000001,40.092,29.93 -2020-09-15 04:45:00,96.41,44.424,40.092,29.93 -2020-09-15 05:00:00,99.04,63.07899999999999,43.713,29.93 -2020-09-15 05:15:00,103.98,75.593,43.713,29.93 -2020-09-15 05:30:00,107.4,69.36,43.713,29.93 -2020-09-15 05:45:00,109.39,64.872,43.713,29.93 -2020-09-15 06:00:00,117.19,64.73100000000001,56.033,29.93 -2020-09-15 06:15:00,114.09,66.351,56.033,29.93 -2020-09-15 06:30:00,115.14,64.90899999999999,56.033,29.93 -2020-09-15 06:45:00,116.49,66.49600000000001,56.033,29.93 -2020-09-15 07:00:00,118.41,64.818,66.003,29.93 -2020-09-15 07:15:00,117.26,66.15899999999999,66.003,29.93 -2020-09-15 07:30:00,117.65,64.84100000000001,66.003,29.93 -2020-09-15 07:45:00,116.28,65.70100000000001,66.003,29.93 -2020-09-15 08:00:00,113.85,64.434,57.474,29.93 -2020-09-15 08:15:00,110.41,65.98100000000001,57.474,29.93 -2020-09-15 08:30:00,110.67,65.303,57.474,29.93 -2020-09-15 08:45:00,110.98,66.51,57.474,29.93 -2020-09-15 09:00:00,110.26,62.99100000000001,51.928000000000004,29.93 -2020-09-15 09:15:00,109.79,61.425,51.928000000000004,29.93 -2020-09-15 09:30:00,108.62,63.258,51.928000000000004,29.93 -2020-09-15 09:45:00,108.66,64.442,51.928000000000004,29.93 -2020-09-15 10:00:00,108.71,60.86600000000001,49.46,29.93 -2020-09-15 10:15:00,108.89,61.133,49.46,29.93 -2020-09-15 10:30:00,108.34,60.746,49.46,29.93 -2020-09-15 10:45:00,108.25,61.647,49.46,29.93 -2020-09-15 11:00:00,106.43,58.276,48.206,29.93 -2020-09-15 11:15:00,104.48,59.166000000000004,48.206,29.93 -2020-09-15 11:30:00,106.87,59.805,48.206,29.93 -2020-09-15 11:45:00,104.72,60.371,48.206,29.93 -2020-09-15 12:00:00,102.62,56.325,46.285,29.93 -2020-09-15 12:15:00,101.2,55.792,46.285,29.93 -2020-09-15 12:30:00,102.37,55.288999999999994,46.285,29.93 -2020-09-15 12:45:00,101.53,56.23,46.285,29.93 -2020-09-15 13:00:00,101.21,54.992,46.861999999999995,29.93 -2020-09-15 13:15:00,100.74,55.506,46.861999999999995,29.93 -2020-09-15 13:30:00,100.23,54.276,46.861999999999995,29.93 -2020-09-15 13:45:00,102.07,53.488,46.861999999999995,29.93 -2020-09-15 14:00:00,100.98,54.14,46.488,29.93 -2020-09-15 14:15:00,101.37,53.342,46.488,29.93 -2020-09-15 14:30:00,102.21,52.472,46.488,29.93 -2020-09-15 14:45:00,104.2,53.218,46.488,29.93 -2020-09-15 15:00:00,103.31,53.176,48.442,29.93 -2020-09-15 15:15:00,105.66,51.303999999999995,48.442,29.93 -2020-09-15 15:30:00,104.22,50.43899999999999,48.442,29.93 -2020-09-15 15:45:00,106.45,48.342,48.442,29.93 -2020-09-15 16:00:00,107.2,50.266000000000005,50.397,29.93 -2020-09-15 16:15:00,108.47,50.245,50.397,29.93 -2020-09-15 16:30:00,111.24,50.597,50.397,29.93 -2020-09-15 16:45:00,112.38,48.913000000000004,50.397,29.93 -2020-09-15 17:00:00,118.39,50.25899999999999,56.668,29.93 -2020-09-15 17:15:00,113.96,51.603,56.668,29.93 -2020-09-15 17:30:00,113.72,51.18,56.668,29.93 -2020-09-15 17:45:00,114.84,50.68,56.668,29.93 -2020-09-15 18:00:00,117.58,52.738,57.957,29.93 -2020-09-15 18:15:00,116.18,52.331,57.957,29.93 -2020-09-15 18:30:00,120.29,50.532,57.957,29.93 -2020-09-15 18:45:00,119.15,54.638999999999996,57.957,29.93 -2020-09-15 19:00:00,120.04,57.236000000000004,57.056000000000004,29.93 -2020-09-15 19:15:00,118.63,55.964,57.056000000000004,29.93 -2020-09-15 19:30:00,113.73,54.924,57.056000000000004,29.93 -2020-09-15 19:45:00,113.54,55.038000000000004,57.056000000000004,29.93 -2020-09-15 20:00:00,105.26,53.512,64.156,29.93 -2020-09-15 20:15:00,101.39,52.998000000000005,64.156,29.93 -2020-09-15 20:30:00,98.17,52.282,64.156,29.93 -2020-09-15 20:45:00,97.7,51.735,64.156,29.93 -2020-09-15 21:00:00,98.43,50.36600000000001,56.507,29.93 -2020-09-15 21:15:00,100.0,51.815,56.507,29.93 -2020-09-15 21:30:00,94.23,51.591,56.507,29.93 -2020-09-15 21:45:00,89.6,50.799,56.507,29.93 -2020-09-15 22:00:00,85.63,48.097,50.728,29.93 -2020-09-15 22:15:00,88.34,48.534,50.728,29.93 -2020-09-15 22:30:00,87.42,40.913000000000004,50.728,29.93 -2020-09-15 22:45:00,85.3,37.449,50.728,29.93 -2020-09-15 23:00:00,78.95,34.841,43.556999999999995,29.93 -2020-09-15 23:15:00,83.3,33.741,43.556999999999995,29.93 -2020-09-15 23:30:00,83.87,33.709,43.556999999999995,29.93 -2020-09-15 23:45:00,78.6,32.943000000000005,43.556999999999995,29.93 -2020-09-16 00:00:00,73.44,39.084,41.151,29.93 -2020-09-16 00:15:00,73.98,39.830999999999996,41.151,29.93 -2020-09-16 00:30:00,78.99,39.635,41.151,29.93 -2020-09-16 00:45:00,80.82,39.476,41.151,29.93 -2020-09-16 01:00:00,76.35,39.616,37.763000000000005,29.93 -2020-09-16 01:15:00,73.59,38.859,37.763000000000005,29.93 -2020-09-16 01:30:00,72.26,37.896,37.763000000000005,29.93 -2020-09-16 01:45:00,78.96,37.031,37.763000000000005,29.93 -2020-09-16 02:00:00,78.44,37.302,35.615,29.93 -2020-09-16 02:15:00,78.12,37.166,35.615,29.93 -2020-09-16 02:30:00,72.51,37.736,35.615,29.93 -2020-09-16 02:45:00,78.41,38.519,35.615,29.93 -2020-09-16 03:00:00,82.31,40.568000000000005,35.153,29.93 -2020-09-16 03:15:00,81.48,40.628,35.153,29.93 -2020-09-16 03:30:00,78.89,40.606,35.153,29.93 -2020-09-16 03:45:00,80.9,41.105,35.153,29.93 -2020-09-16 04:00:00,85.15,49.181000000000004,36.203,29.93 -2020-09-16 04:15:00,89.89,57.271,36.203,29.93 -2020-09-16 04:30:00,92.92,55.732,36.203,29.93 -2020-09-16 04:45:00,92.18,56.861000000000004,36.203,29.93 -2020-09-16 05:00:00,99.03,78.215,39.922,29.93 -2020-09-16 05:15:00,104.76,95.447,39.922,29.93 -2020-09-16 05:30:00,108.0,89.35799999999999,39.922,29.93 -2020-09-16 05:45:00,109.51,83.431,39.922,29.93 -2020-09-16 06:00:00,113.65,83.573,56.443999999999996,29.93 -2020-09-16 06:15:00,114.5,86.17,56.443999999999996,29.93 -2020-09-16 06:30:00,115.4,84.446,56.443999999999996,29.93 -2020-09-16 06:45:00,117.08,85.50399999999999,56.443999999999996,29.93 -2020-09-16 07:00:00,119.77,85.01899999999999,68.683,29.93 -2020-09-16 07:15:00,119.16,86.70100000000001,68.683,29.93 -2020-09-16 07:30:00,124.41,85.609,68.683,29.93 -2020-09-16 07:45:00,120.27,86.053,68.683,29.93 -2020-09-16 08:00:00,119.32,82.478,59.003,29.93 -2020-09-16 08:15:00,118.97,83.669,59.003,29.93 -2020-09-16 08:30:00,117.59,81.854,59.003,29.93 -2020-09-16 08:45:00,117.4,82.26899999999999,59.003,29.93 -2020-09-16 09:00:00,118.31,76.666,56.21,29.93 -2020-09-16 09:15:00,120.11,74.795,56.21,29.93 -2020-09-16 09:30:00,122.52,75.825,56.21,29.93 -2020-09-16 09:45:00,124.42,76.199,56.21,29.93 -2020-09-16 10:00:00,126.1,73.703,52.358999999999995,29.93 -2020-09-16 10:15:00,126.69,73.807,52.358999999999995,29.93 -2020-09-16 10:30:00,121.23,73.306,52.358999999999995,29.93 -2020-09-16 10:45:00,118.65,73.608,52.358999999999995,29.93 -2020-09-16 11:00:00,109.55,71.77600000000001,51.161,29.93 -2020-09-16 11:15:00,111.88,72.722,51.161,29.93 -2020-09-16 11:30:00,117.41,73.042,51.161,29.93 -2020-09-16 11:45:00,116.4,72.767,51.161,29.93 -2020-09-16 12:00:00,107.59,70.317,49.119,29.93 -2020-09-16 12:15:00,105.17,70.104,49.119,29.93 -2020-09-16 12:30:00,102.46,69.05199999999999,49.119,29.93 -2020-09-16 12:45:00,103.14,69.616,49.119,29.93 -2020-09-16 13:00:00,101.75,69.403,49.187,29.93 -2020-09-16 13:15:00,101.95,69.571,49.187,29.93 -2020-09-16 13:30:00,104.52,68.53399999999999,49.187,29.93 -2020-09-16 13:45:00,101.25,67.885,49.187,29.93 -2020-09-16 14:00:00,102.1,68.471,49.787,29.93 -2020-09-16 14:15:00,103.0,68.127,49.787,29.93 -2020-09-16 14:30:00,103.44,66.956,49.787,29.93 -2020-09-16 14:45:00,104.37,67.27600000000001,49.787,29.93 -2020-09-16 15:00:00,102.18,67.498,51.458999999999996,29.93 -2020-09-16 15:15:00,104.63,66.195,51.458999999999996,29.93 -2020-09-16 15:30:00,104.49,65.661,51.458999999999996,29.93 -2020-09-16 15:45:00,106.34,64.396,51.458999999999996,29.93 -2020-09-16 16:00:00,111.51,65.649,53.663000000000004,29.93 -2020-09-16 16:15:00,108.92,65.133,53.663000000000004,29.93 -2020-09-16 16:30:00,110.62,65.763,53.663000000000004,29.93 -2020-09-16 16:45:00,111.54,63.458999999999996,53.663000000000004,29.93 -2020-09-16 17:00:00,116.11,64.80199999999999,58.183,29.93 -2020-09-16 17:15:00,114.55,65.96,58.183,29.93 -2020-09-16 17:30:00,114.66,65.595,58.183,29.93 -2020-09-16 17:45:00,115.07,65.561,58.183,29.93 -2020-09-16 18:00:00,117.2,65.617,60.141000000000005,29.93 -2020-09-16 18:15:00,116.17,65.419,60.141000000000005,29.93 -2020-09-16 18:30:00,119.01,64.142,60.141000000000005,29.93 -2020-09-16 18:45:00,118.27,68.15,60.141000000000005,29.93 -2020-09-16 19:00:00,116.67,67.969,60.582,29.93 -2020-09-16 19:15:00,116.96,66.834,60.582,29.93 -2020-09-16 19:30:00,113.33,66.178,60.582,29.93 -2020-09-16 19:45:00,115.63,66.4,60.582,29.93 -2020-09-16 20:00:00,104.66,64.4,66.61,29.93 -2020-09-16 20:15:00,107.17,62.825,66.61,29.93 -2020-09-16 20:30:00,104.72,61.527,66.61,29.93 -2020-09-16 20:45:00,104.85,61.146,66.61,29.93 -2020-09-16 21:00:00,98.44,59.425,57.658,29.93 -2020-09-16 21:15:00,92.27,60.768,57.658,29.93 -2020-09-16 21:30:00,95.86,59.723,57.658,29.93 -2020-09-16 21:45:00,94.6,58.926,57.658,29.93 -2020-09-16 22:00:00,90.23,57.67,51.81,29.93 -2020-09-16 22:15:00,84.5,56.826,51.81,29.93 -2020-09-16 22:30:00,82.73,48.071000000000005,51.81,29.93 -2020-09-16 22:45:00,80.31,44.38,51.81,29.93 -2020-09-16 23:00:00,82.5,40.444,42.93600000000001,29.93 -2020-09-16 23:15:00,82.69,40.001999999999995,42.93600000000001,29.93 -2020-09-16 23:30:00,82.91,40.023,42.93600000000001,29.93 -2020-09-16 23:45:00,78.13,39.402,42.93600000000001,29.93 -2020-09-17 00:00:00,79.44,39.347,39.211,29.93 -2020-09-17 00:15:00,80.59,40.09,39.211,29.93 -2020-09-17 00:30:00,78.36,39.903,39.211,29.93 -2020-09-17 00:45:00,75.57,39.747,39.211,29.93 -2020-09-17 01:00:00,79.12,39.88,37.607,29.93 -2020-09-17 01:15:00,78.2,39.145,37.607,29.93 -2020-09-17 01:30:00,77.23,38.2,37.607,29.93 -2020-09-17 01:45:00,74.58,37.333,37.607,29.93 -2020-09-17 02:00:00,78.04,37.611,36.44,29.93 -2020-09-17 02:15:00,79.78,37.494,36.44,29.93 -2020-09-17 02:30:00,76.24,38.041,36.44,29.93 -2020-09-17 02:45:00,75.01,38.82,36.44,29.93 -2020-09-17 03:00:00,77.9,40.855,36.116,29.93 -2020-09-17 03:15:00,79.65,40.934,36.116,29.93 -2020-09-17 03:30:00,81.81,40.917,36.116,29.93 -2020-09-17 03:45:00,78.48,41.401,36.116,29.93 -2020-09-17 04:00:00,87.76,49.50899999999999,37.398,29.93 -2020-09-17 04:15:00,91.21,57.629,37.398,29.93 -2020-09-17 04:30:00,95.34,56.093999999999994,37.398,29.93 -2020-09-17 04:45:00,94.05,57.232,37.398,29.93 -2020-09-17 05:00:00,98.21,78.67699999999999,41.776,29.93 -2020-09-17 05:15:00,104.48,96.00299999999999,41.776,29.93 -2020-09-17 05:30:00,108.42,89.899,41.776,29.93 -2020-09-17 05:45:00,108.7,83.925,41.776,29.93 -2020-09-17 06:00:00,114.23,84.03399999999999,55.61,29.93 -2020-09-17 06:15:00,113.86,86.65299999999999,55.61,29.93 -2020-09-17 06:30:00,116.05,84.93299999999999,55.61,29.93 -2020-09-17 06:45:00,116.34,85.98899999999999,55.61,29.93 -2020-09-17 07:00:00,117.73,85.50200000000001,67.13600000000001,29.93 -2020-09-17 07:15:00,117.01,87.2,67.13600000000001,29.93 -2020-09-17 07:30:00,115.49,86.14299999999999,67.13600000000001,29.93 -2020-09-17 07:45:00,114.64,86.59,67.13600000000001,29.93 -2020-09-17 08:00:00,113.27,83.023,57.55,29.93 -2020-09-17 08:15:00,113.0,84.181,57.55,29.93 -2020-09-17 08:30:00,112.72,82.383,57.55,29.93 -2020-09-17 08:45:00,115.23,82.779,57.55,29.93 -2020-09-17 09:00:00,116.21,77.181,52.931999999999995,29.93 -2020-09-17 09:15:00,118.42,75.3,52.931999999999995,29.93 -2020-09-17 09:30:00,115.37,76.311,52.931999999999995,29.93 -2020-09-17 09:45:00,110.86,76.655,52.931999999999995,29.93 -2020-09-17 10:00:00,114.03,74.156,50.36600000000001,29.93 -2020-09-17 10:15:00,117.04,74.223,50.36600000000001,29.93 -2020-09-17 10:30:00,119.88,73.707,50.36600000000001,29.93 -2020-09-17 10:45:00,114.08,73.994,50.36600000000001,29.93 -2020-09-17 11:00:00,112.85,72.17399999999999,47.893,29.93 -2020-09-17 11:15:00,111.3,73.104,47.893,29.93 -2020-09-17 11:30:00,112.41,73.421,47.893,29.93 -2020-09-17 11:45:00,109.95,73.13,47.893,29.93 -2020-09-17 12:00:00,109.44,70.656,45.271,29.93 -2020-09-17 12:15:00,107.34,70.431,45.271,29.93 -2020-09-17 12:30:00,105.72,69.41,45.271,29.93 -2020-09-17 12:45:00,114.51,69.967,45.271,29.93 -2020-09-17 13:00:00,108.61,69.725,44.351000000000006,29.93 -2020-09-17 13:15:00,107.32,69.892,44.351000000000006,29.93 -2020-09-17 13:30:00,104.8,68.85300000000001,44.351000000000006,29.93 -2020-09-17 13:45:00,105.93,68.209,44.351000000000006,29.93 -2020-09-17 14:00:00,105.83,68.747,44.99,29.93 -2020-09-17 14:15:00,108.76,68.418,44.99,29.93 -2020-09-17 14:30:00,104.88,67.279,44.99,29.93 -2020-09-17 14:45:00,103.7,67.595,44.99,29.93 -2020-09-17 15:00:00,105.59,67.77199999999999,46.869,29.93 -2020-09-17 15:15:00,105.16,66.488,46.869,29.93 -2020-09-17 15:30:00,103.15,65.985,46.869,29.93 -2020-09-17 15:45:00,107.04,64.735,46.869,29.93 -2020-09-17 16:00:00,107.68,65.949,48.902,29.93 -2020-09-17 16:15:00,110.33,65.449,48.902,29.93 -2020-09-17 16:30:00,109.86,66.07600000000001,48.902,29.93 -2020-09-17 16:45:00,111.84,63.827,48.902,29.93 -2020-09-17 17:00:00,114.46,65.133,53.244,29.93 -2020-09-17 17:15:00,113.14,66.313,53.244,29.93 -2020-09-17 17:30:00,113.05,65.952,53.244,29.93 -2020-09-17 17:45:00,114.69,65.947,53.244,29.93 -2020-09-17 18:00:00,115.77,65.985,54.343999999999994,29.93 -2020-09-17 18:15:00,118.59,65.783,54.343999999999994,29.93 -2020-09-17 18:30:00,118.36,64.518,54.343999999999994,29.93 -2020-09-17 18:45:00,120.85,68.518,54.343999999999994,29.93 -2020-09-17 19:00:00,115.71,68.35,54.332,29.93 -2020-09-17 19:15:00,116.59,67.211,54.332,29.93 -2020-09-17 19:30:00,109.14,66.551,54.332,29.93 -2020-09-17 19:45:00,110.22,66.765,54.332,29.93 -2020-09-17 20:00:00,101.81,64.786,58.06,29.93 -2020-09-17 20:15:00,102.55,63.208,58.06,29.93 -2020-09-17 20:30:00,100.73,61.886,58.06,29.93 -2020-09-17 20:45:00,100.68,61.467,58.06,29.93 -2020-09-17 21:00:00,95.01,59.748999999999995,52.411,29.93 -2020-09-17 21:15:00,100.41,61.083,52.411,29.93 -2020-09-17 21:30:00,97.41,60.045,52.411,29.93 -2020-09-17 21:45:00,94.7,59.213,52.411,29.93 -2020-09-17 22:00:00,85.44,57.93899999999999,47.148999999999994,29.93 -2020-09-17 22:15:00,89.95,57.071000000000005,47.148999999999994,29.93 -2020-09-17 22:30:00,89.79,48.309,47.148999999999994,29.93 -2020-09-17 22:45:00,89.53,44.622,47.148999999999994,29.93 -2020-09-17 23:00:00,81.01,40.726,40.814,29.93 -2020-09-17 23:15:00,79.55,40.256,40.814,29.93 -2020-09-17 23:30:00,83.76,40.281,40.814,29.93 -2020-09-17 23:45:00,83.95,39.659,40.814,29.93 -2020-09-18 00:00:00,80.29,38.004,39.153,29.93 -2020-09-18 00:15:00,74.72,38.957,39.153,29.93 -2020-09-18 00:30:00,73.6,38.939,39.153,29.93 -2020-09-18 00:45:00,72.77,39.126,39.153,29.93 -2020-09-18 01:00:00,71.06,38.888000000000005,37.228,29.93 -2020-09-18 01:15:00,72.86,37.991,37.228,29.93 -2020-09-18 01:30:00,71.99,37.521,37.228,29.93 -2020-09-18 01:45:00,72.65,36.501999999999995,37.228,29.93 -2020-09-18 02:00:00,72.83,37.49,35.851,29.93 -2020-09-18 02:15:00,78.45,37.321999999999996,35.851,29.93 -2020-09-18 02:30:00,72.91,38.597,35.851,29.93 -2020-09-18 02:45:00,73.98,38.884,35.851,29.93 -2020-09-18 03:00:00,74.75,41.191,36.54,29.93 -2020-09-18 03:15:00,77.42,40.675,36.54,29.93 -2020-09-18 03:30:00,77.21,40.485,36.54,29.93 -2020-09-18 03:45:00,81.51,41.681000000000004,36.54,29.93 -2020-09-18 04:00:00,90.84,49.988,37.578,29.93 -2020-09-18 04:15:00,91.02,56.937,37.578,29.93 -2020-09-18 04:30:00,86.07,56.161,37.578,29.93 -2020-09-18 04:45:00,92.31,56.513999999999996,37.578,29.93 -2020-09-18 05:00:00,108.17,77.343,40.387,29.93 -2020-09-18 05:15:00,112.85,95.874,40.387,29.93 -2020-09-18 05:30:00,108.76,90.18299999999999,40.387,29.93 -2020-09-18 05:45:00,109.88,83.81,40.387,29.93 -2020-09-18 06:00:00,117.15,84.181,54.668,29.93 -2020-09-18 06:15:00,115.51,86.54899999999999,54.668,29.93 -2020-09-18 06:30:00,115.18,84.565,54.668,29.93 -2020-09-18 06:45:00,116.95,85.958,54.668,29.93 -2020-09-18 07:00:00,118.68,85.72399999999999,63.971000000000004,29.93 -2020-09-18 07:15:00,118.95,88.38600000000001,63.971000000000004,29.93 -2020-09-18 07:30:00,116.43,85.897,63.971000000000004,29.93 -2020-09-18 07:45:00,116.62,85.943,63.971000000000004,29.93 -2020-09-18 08:00:00,114.28,82.63,56.042,29.93 -2020-09-18 08:15:00,112.83,84.12299999999999,56.042,29.93 -2020-09-18 08:30:00,114.36,82.556,56.042,29.93 -2020-09-18 08:45:00,115.55,82.37,56.042,29.93 -2020-09-18 09:00:00,114.17,75.26,52.832,29.93 -2020-09-18 09:15:00,111.77,74.903,52.832,29.93 -2020-09-18 09:30:00,111.15,75.277,52.832,29.93 -2020-09-18 09:45:00,111.61,75.834,52.832,29.93 -2020-09-18 10:00:00,107.89,72.842,50.044,29.93 -2020-09-18 10:15:00,109.25,72.935,50.044,29.93 -2020-09-18 10:30:00,109.02,72.765,50.044,29.93 -2020-09-18 10:45:00,107.6,72.832,50.044,29.93 -2020-09-18 11:00:00,107.22,71.185,49.06100000000001,29.93 -2020-09-18 11:15:00,115.43,71.064,49.06100000000001,29.93 -2020-09-18 11:30:00,114.09,71.685,49.06100000000001,29.93 -2020-09-18 11:45:00,112.5,70.729,49.06100000000001,29.93 -2020-09-18 12:00:00,109.27,68.899,45.595,29.93 -2020-09-18 12:15:00,107.26,67.53399999999999,45.595,29.93 -2020-09-18 12:30:00,111.87,66.678,45.595,29.93 -2020-09-18 12:45:00,112.45,66.866,45.595,29.93 -2020-09-18 13:00:00,107.19,67.291,43.218,29.93 -2020-09-18 13:15:00,111.3,67.846,43.218,29.93 -2020-09-18 13:30:00,109.47,67.354,43.218,29.93 -2020-09-18 13:45:00,112.2,66.903,43.218,29.93 -2020-09-18 14:00:00,112.75,66.477,41.926,29.93 -2020-09-18 14:15:00,113.18,66.402,41.926,29.93 -2020-09-18 14:30:00,113.34,66.488,41.926,29.93 -2020-09-18 14:45:00,112.16,66.43,41.926,29.93 -2020-09-18 15:00:00,107.96,66.34899999999999,43.79,29.93 -2020-09-18 15:15:00,108.95,64.778,43.79,29.93 -2020-09-18 15:30:00,111.25,63.426,43.79,29.93 -2020-09-18 15:45:00,111.89,62.758,43.79,29.93 -2020-09-18 16:00:00,114.19,63.008,45.895,29.93 -2020-09-18 16:15:00,112.94,62.958,45.895,29.93 -2020-09-18 16:30:00,113.82,63.486000000000004,45.895,29.93 -2020-09-18 16:45:00,116.38,60.707,45.895,29.93 -2020-09-18 17:00:00,117.22,63.245,51.36,29.93 -2020-09-18 17:15:00,116.42,64.20100000000001,51.36,29.93 -2020-09-18 17:30:00,115.99,63.861000000000004,51.36,29.93 -2020-09-18 17:45:00,115.41,63.695,51.36,29.93 -2020-09-18 18:00:00,117.73,64.012,52.985,29.93 -2020-09-18 18:15:00,116.7,63.016000000000005,52.985,29.93 -2020-09-18 18:30:00,116.11,61.803000000000004,52.985,29.93 -2020-09-18 18:45:00,115.8,66.096,52.985,29.93 -2020-09-18 19:00:00,113.5,66.872,52.602,29.93 -2020-09-18 19:15:00,108.61,66.538,52.602,29.93 -2020-09-18 19:30:00,106.11,65.805,52.602,29.93 -2020-09-18 19:45:00,104.55,65.138,52.602,29.93 -2020-09-18 20:00:00,95.47,63.067,58.063,29.93 -2020-09-18 20:15:00,94.24,62.063,58.063,29.93 -2020-09-18 20:30:00,92.29,60.351000000000006,58.063,29.93 -2020-09-18 20:45:00,95.23,59.466,58.063,29.93 -2020-09-18 21:00:00,87.91,58.849,50.135,29.93 -2020-09-18 21:15:00,85.57,61.511,50.135,29.93 -2020-09-18 21:30:00,78.26,60.382,50.135,29.93 -2020-09-18 21:45:00,84.38,59.817,50.135,29.93 -2020-09-18 22:00:00,79.5,58.698,45.165,29.93 -2020-09-18 22:15:00,81.75,57.588,45.165,29.93 -2020-09-18 22:30:00,74.47,54.06,45.165,29.93 -2020-09-18 22:45:00,77.94,51.985,45.165,29.93 -2020-09-18 23:00:00,76.49,49.233000000000004,39.121,29.93 -2020-09-18 23:15:00,74.79,47.049,39.121,29.93 -2020-09-18 23:30:00,72.85,45.343999999999994,39.121,29.93 -2020-09-18 23:45:00,71.53,44.448,39.121,29.93 -2020-09-19 00:00:00,71.22,38.38,38.49,29.816 -2020-09-19 00:15:00,70.98,37.655,38.49,29.816 -2020-09-19 00:30:00,62.35,37.674,38.49,29.816 -2020-09-19 00:45:00,70.07,37.529,38.49,29.816 -2020-09-19 01:00:00,68.84,37.658,34.5,29.816 -2020-09-19 01:15:00,66.83,36.938,34.5,29.816 -2020-09-19 01:30:00,59.53,35.755,34.5,29.816 -2020-09-19 01:45:00,60.48,35.583,34.5,29.816 -2020-09-19 02:00:00,61.5,36.045,32.236,29.816 -2020-09-19 02:15:00,67.13,35.221,32.236,29.816 -2020-09-19 02:30:00,59.26,35.586999999999996,32.236,29.816 -2020-09-19 02:45:00,59.5,36.468,32.236,29.816 -2020-09-19 03:00:00,58.35,37.939,32.067,29.816 -2020-09-19 03:15:00,60.56,36.596,32.067,29.816 -2020-09-19 03:30:00,58.82,36.205,32.067,29.816 -2020-09-19 03:45:00,60.46,38.491,32.067,29.816 -2020-09-19 04:00:00,61.29,44.302,33.071,29.816 -2020-09-19 04:15:00,60.56,49.925,33.071,29.816 -2020-09-19 04:30:00,57.39,47.338,33.071,29.816 -2020-09-19 04:45:00,61.32,47.757,33.071,29.816 -2020-09-19 05:00:00,63.99,58.761,33.014,29.816 -2020-09-19 05:15:00,64.16,64.37100000000001,33.014,29.816 -2020-09-19 05:30:00,65.42,59.894,33.014,29.816 -2020-09-19 05:45:00,62.16,58.2,33.014,29.816 -2020-09-19 06:00:00,67.73,71.545,34.628,29.816 -2020-09-19 06:15:00,67.75,83.959,34.628,29.816 -2020-09-19 06:30:00,71.21,78.477,34.628,29.816 -2020-09-19 06:45:00,72.05,75.16,34.628,29.816 -2020-09-19 07:00:00,74.14,72.542,38.871,29.816 -2020-09-19 07:15:00,71.97,73.934,38.871,29.816 -2020-09-19 07:30:00,76.89,73.319,38.871,29.816 -2020-09-19 07:45:00,80.01,75.127,38.871,29.816 -2020-09-19 08:00:00,80.53,73.286,43.293,29.816 -2020-09-19 08:15:00,82.15,75.622,43.293,29.816 -2020-09-19 08:30:00,81.09,74.517,43.293,29.816 -2020-09-19 08:45:00,76.52,75.882,43.293,29.816 -2020-09-19 09:00:00,75.61,71.438,44.559,29.816 -2020-09-19 09:15:00,74.54,71.634,44.559,29.816 -2020-09-19 09:30:00,74.81,72.601,44.559,29.816 -2020-09-19 09:45:00,76.44,72.857,44.559,29.816 -2020-09-19 10:00:00,76.74,70.247,42.091,29.816 -2020-09-19 10:15:00,77.89,70.598,42.091,29.816 -2020-09-19 10:30:00,82.26,70.22800000000001,42.091,29.816 -2020-09-19 10:45:00,79.12,70.435,42.091,29.816 -2020-09-19 11:00:00,78.34,68.741,38.505,29.816 -2020-09-19 11:15:00,81.3,68.98899999999999,38.505,29.816 -2020-09-19 11:30:00,77.75,69.456,38.505,29.816 -2020-09-19 11:45:00,76.77,68.646,38.505,29.816 -2020-09-19 12:00:00,74.59,66.622,35.388000000000005,29.816 -2020-09-19 12:15:00,77.13,66.02600000000001,35.388000000000005,29.816 -2020-09-19 12:30:00,69.95,65.217,35.388000000000005,29.816 -2020-09-19 12:45:00,65.4,65.63600000000001,35.388000000000005,29.816 -2020-09-19 13:00:00,66.49,65.413,31.355999999999998,29.816 -2020-09-19 13:15:00,69.13,64.945,31.355999999999998,29.816 -2020-09-19 13:30:00,67.92,64.436,31.355999999999998,29.816 -2020-09-19 13:45:00,71.32,63.224,31.355999999999998,29.816 -2020-09-19 14:00:00,68.7,63.16,30.522,29.816 -2020-09-19 14:15:00,68.61,62.07899999999999,30.522,29.816 -2020-09-19 14:30:00,70.26,61.327,30.522,29.816 -2020-09-19 14:45:00,70.21,61.659,30.522,29.816 -2020-09-19 15:00:00,71.68,62.038999999999994,34.36,29.816 -2020-09-19 15:15:00,71.93,61.226000000000006,34.36,29.816 -2020-09-19 15:30:00,73.29,60.536,34.36,29.816 -2020-09-19 15:45:00,75.49,59.263999999999996,34.36,29.816 -2020-09-19 16:00:00,78.43,60.763999999999996,39.507,29.816 -2020-09-19 16:15:00,79.18,60.381,39.507,29.816 -2020-09-19 16:30:00,79.14,61.047,39.507,29.816 -2020-09-19 16:45:00,81.7,58.574,39.507,29.816 -2020-09-19 17:00:00,83.48,60.071999999999996,47.151,29.816 -2020-09-19 17:15:00,84.27,60.06100000000001,47.151,29.816 -2020-09-19 17:30:00,85.58,59.604,47.151,29.816 -2020-09-19 17:45:00,85.8,59.68600000000001,47.151,29.816 -2020-09-19 18:00:00,89.34,60.965,50.303999999999995,29.816 -2020-09-19 18:15:00,89.84,61.67100000000001,50.303999999999995,29.816 -2020-09-19 18:30:00,90.7,61.82,50.303999999999995,29.816 -2020-09-19 18:45:00,89.05,62.67,50.303999999999995,29.816 -2020-09-19 19:00:00,87.56,62.681000000000004,50.622,29.816 -2020-09-19 19:15:00,84.6,61.475,50.622,29.816 -2020-09-19 19:30:00,82.62,61.503,50.622,29.816 -2020-09-19 19:45:00,81.68,62.065,50.622,29.816 -2020-09-19 20:00:00,77.1,61.138999999999996,45.391000000000005,29.816 -2020-09-19 20:15:00,73.8,60.276,45.391000000000005,29.816 -2020-09-19 20:30:00,72.52,57.81,45.391000000000005,29.816 -2020-09-19 20:45:00,74.56,58.104,45.391000000000005,29.816 -2020-09-19 21:00:00,70.72,57.077,39.98,29.816 -2020-09-19 21:15:00,71.27,59.6,39.98,29.816 -2020-09-19 21:30:00,68.48,58.958,39.98,29.816 -2020-09-19 21:45:00,68.12,57.851000000000006,39.98,29.816 -2020-09-19 22:00:00,63.93,57.068999999999996,37.53,29.816 -2020-09-19 22:15:00,68.03,56.803000000000004,37.53,29.816 -2020-09-19 22:30:00,62.43,54.72,37.53,29.816 -2020-09-19 22:45:00,64.15,53.43600000000001,37.53,29.816 -2020-09-19 23:00:00,60.71,50.88399999999999,30.97,29.816 -2020-09-19 23:15:00,60.35,48.526,30.97,29.816 -2020-09-19 23:30:00,59.11,48.121,30.97,29.816 -2020-09-19 23:45:00,56.8,46.816,30.97,29.816 -2020-09-20 00:00:00,55.45,39.763000000000005,27.24,29.816 -2020-09-20 00:15:00,55.34,38.111999999999995,27.24,29.816 -2020-09-20 00:30:00,53.87,37.93,27.24,29.816 -2020-09-20 00:45:00,54.21,37.898,27.24,29.816 -2020-09-20 01:00:00,52.57,38.174,25.662,29.816 -2020-09-20 01:15:00,53.16,37.672,25.662,29.816 -2020-09-20 01:30:00,53.67,36.552,25.662,29.816 -2020-09-20 01:45:00,54.29,36.016999999999996,25.662,29.816 -2020-09-20 02:00:00,51.96,36.338,25.67,29.816 -2020-09-20 02:15:00,53.19,35.809,25.67,29.816 -2020-09-20 02:30:00,53.0,36.524,25.67,29.816 -2020-09-20 02:45:00,53.05,37.319,25.67,29.816 -2020-09-20 03:00:00,52.73,39.335,24.258000000000003,29.816 -2020-09-20 03:15:00,53.33,38.053000000000004,24.258000000000003,29.816 -2020-09-20 03:30:00,52.77,37.523,24.258000000000003,29.816 -2020-09-20 03:45:00,53.76,39.198,24.258000000000003,29.816 -2020-09-20 04:00:00,54.65,44.95399999999999,25.051,29.816 -2020-09-20 04:15:00,54.55,50.015,25.051,29.816 -2020-09-20 04:30:00,55.06,48.416000000000004,25.051,29.816 -2020-09-20 04:45:00,55.46,48.582,25.051,29.816 -2020-09-20 05:00:00,56.45,58.891999999999996,25.145,29.816 -2020-09-20 05:15:00,57.48,63.419,25.145,29.816 -2020-09-20 05:30:00,58.07,58.583,25.145,29.816 -2020-09-20 05:45:00,58.21,56.698,25.145,29.816 -2020-09-20 06:00:00,59.41,68.219,26.371,29.816 -2020-09-20 06:15:00,60.9,80.828,26.371,29.816 -2020-09-20 06:30:00,62.42,74.55,26.371,29.816 -2020-09-20 06:45:00,64.42,70.22399999999999,26.371,29.816 -2020-09-20 07:00:00,66.46,68.428,28.756999999999998,29.816 -2020-09-20 07:15:00,67.42,68.416,28.756999999999998,29.816 -2020-09-20 07:30:00,69.0,68.42699999999999,28.756999999999998,29.816 -2020-09-20 07:45:00,71.83,70.008,28.756999999999998,29.816 -2020-09-20 08:00:00,72.34,69.197,32.82,29.816 -2020-09-20 08:15:00,74.03,72.319,32.82,29.816 -2020-09-20 08:30:00,74.41,72.318,32.82,29.816 -2020-09-20 08:45:00,74.79,74.168,32.82,29.816 -2020-09-20 09:00:00,76.91,69.527,35.534,29.816 -2020-09-20 09:15:00,76.22,69.52,35.534,29.816 -2020-09-20 09:30:00,78.51,70.729,35.534,29.816 -2020-09-20 09:45:00,78.9,71.635,35.534,29.816 -2020-09-20 10:00:00,78.27,70.111,35.925,29.816 -2020-09-20 10:15:00,84.18,70.676,35.925,29.816 -2020-09-20 10:30:00,87.27,70.625,35.925,29.816 -2020-09-20 10:45:00,86.96,70.958,35.925,29.816 -2020-09-20 11:00:00,84.99,69.303,37.056,29.816 -2020-09-20 11:15:00,85.58,69.262,37.056,29.816 -2020-09-20 11:30:00,85.55,69.827,37.056,29.816 -2020-09-20 11:45:00,82.98,69.359,37.056,29.816 -2020-09-20 12:00:00,78.15,67.827,33.124,29.816 -2020-09-20 12:15:00,79.24,67.391,33.124,29.816 -2020-09-20 12:30:00,78.19,66.303,33.124,29.816 -2020-09-20 12:45:00,78.17,65.979,33.124,29.816 -2020-09-20 13:00:00,77.34,65.316,29.874000000000002,29.816 -2020-09-20 13:15:00,74.05,65.38,29.874000000000002,29.816 -2020-09-20 13:30:00,74.77,64.042,29.874000000000002,29.816 -2020-09-20 13:45:00,77.63,63.398,29.874000000000002,29.816 -2020-09-20 14:00:00,79.84,64.18,27.302,29.816 -2020-09-20 14:15:00,80.59,63.778999999999996,27.302,29.816 -2020-09-20 14:30:00,80.69,62.596000000000004,27.302,29.816 -2020-09-20 14:45:00,79.0,62.066,27.302,29.816 -2020-09-20 15:00:00,76.77,62.028,27.642,29.816 -2020-09-20 15:15:00,78.69,60.915,27.642,29.816 -2020-09-20 15:30:00,79.78,60.31399999999999,27.642,29.816 -2020-09-20 15:45:00,80.99,59.467,27.642,29.816 -2020-09-20 16:00:00,82.95,60.283,31.945999999999998,29.816 -2020-09-20 16:15:00,83.28,59.808,31.945999999999998,29.816 -2020-09-20 16:30:00,84.6,61.248999999999995,31.945999999999998,29.816 -2020-09-20 16:45:00,89.69,58.915,31.945999999999998,29.816 -2020-09-20 17:00:00,94.49,60.631,40.387,29.816 -2020-09-20 17:15:00,95.66,61.602,40.387,29.816 -2020-09-20 17:30:00,95.71,61.797,40.387,29.816 -2020-09-20 17:45:00,97.43,62.872,40.387,29.816 -2020-09-20 18:00:00,98.66,64.33,44.575,29.816 -2020-09-20 18:15:00,95.42,65.113,44.575,29.816 -2020-09-20 18:30:00,96.3,64.515,44.575,29.816 -2020-09-20 18:45:00,92.61,65.957,44.575,29.816 -2020-09-20 19:00:00,91.79,67.405,45.623999999999995,29.816 -2020-09-20 19:15:00,93.46,65.542,45.623999999999995,29.816 -2020-09-20 19:30:00,95.78,65.34,45.623999999999995,29.816 -2020-09-20 19:45:00,94.24,65.939,45.623999999999995,29.816 -2020-09-20 20:00:00,87.59,65.218,44.583999999999996,29.816 -2020-09-20 20:15:00,90.25,64.521,44.583999999999996,29.816 -2020-09-20 20:30:00,92.85,62.952,44.583999999999996,29.816 -2020-09-20 20:45:00,92.59,61.67100000000001,44.583999999999996,29.816 -2020-09-20 21:00:00,85.14,59.825,39.732,29.816 -2020-09-20 21:15:00,82.41,61.951,39.732,29.816 -2020-09-20 21:30:00,79.04,60.927,39.732,29.816 -2020-09-20 21:45:00,80.63,60.081,39.732,29.816 -2020-09-20 22:00:00,78.14,60.426,38.571,29.816 -2020-09-20 22:15:00,79.55,58.713,38.571,29.816 -2020-09-20 22:30:00,75.33,55.406000000000006,38.571,29.816 -2020-09-20 22:45:00,75.99,52.976000000000006,38.571,29.816 -2020-09-20 23:00:00,74.75,49.496,33.121,29.816 -2020-09-20 23:15:00,77.14,48.525,33.121,29.816 -2020-09-20 23:30:00,75.39,47.949,33.121,29.816 -2020-09-20 23:45:00,71.17,46.997,33.121,29.816 -2020-09-21 00:00:00,63.72,42.257,32.506,29.93 -2020-09-21 00:15:00,71.94,41.973,32.506,29.93 -2020-09-21 00:30:00,71.02,41.553999999999995,32.506,29.93 -2020-09-21 00:45:00,73.89,41.096000000000004,32.506,29.93 -2020-09-21 01:00:00,68.98,41.651,31.121,29.93 -2020-09-21 01:15:00,69.62,41.038000000000004,31.121,29.93 -2020-09-21 01:30:00,73.22,40.213,31.121,29.93 -2020-09-21 01:45:00,73.73,39.638000000000005,31.121,29.93 -2020-09-21 02:00:00,72.86,40.29,29.605999999999998,29.93 -2020-09-21 02:15:00,70.41,39.441,29.605999999999998,29.93 -2020-09-21 02:30:00,75.57,40.343,29.605999999999998,29.93 -2020-09-21 02:45:00,75.89,40.859,29.605999999999998,29.93 -2020-09-21 03:00:00,73.84,43.576,28.124000000000002,29.93 -2020-09-21 03:15:00,72.28,43.278,28.124000000000002,29.93 -2020-09-21 03:30:00,78.86,43.173,28.124000000000002,29.93 -2020-09-21 03:45:00,81.91,44.346000000000004,28.124000000000002,29.93 -2020-09-21 04:00:00,83.6,53.56399999999999,29.743000000000002,29.93 -2020-09-21 04:15:00,81.79,61.896,29.743000000000002,29.93 -2020-09-21 04:30:00,85.13,60.549,29.743000000000002,29.93 -2020-09-21 04:45:00,90.52,61.034,29.743000000000002,29.93 -2020-09-21 05:00:00,99.97,80.40899999999999,36.191,29.93 -2020-09-21 05:15:00,106.86,97.719,36.191,29.93 -2020-09-21 05:30:00,113.92,91.788,36.191,29.93 -2020-09-21 05:45:00,121.46,86.161,36.191,29.93 -2020-09-21 06:00:00,123.22,85.652,55.277,29.93 -2020-09-21 06:15:00,121.57,87.79899999999999,55.277,29.93 -2020-09-21 06:30:00,121.97,86.494,55.277,29.93 -2020-09-21 06:45:00,124.18,88.302,55.277,29.93 -2020-09-21 07:00:00,126.05,87.74700000000001,65.697,29.93 -2020-09-21 07:15:00,123.56,89.73700000000001,65.697,29.93 -2020-09-21 07:30:00,119.15,88.95200000000001,65.697,29.93 -2020-09-21 07:45:00,121.25,90.06200000000001,65.697,29.93 -2020-09-21 08:00:00,122.84,86.54299999999999,57.028,29.93 -2020-09-21 08:15:00,123.58,88.176,57.028,29.93 -2020-09-21 08:30:00,124.68,86.37299999999999,57.028,29.93 -2020-09-21 08:45:00,123.86,87.43,57.028,29.93 -2020-09-21 09:00:00,122.73,81.84100000000001,52.633,29.93 -2020-09-21 09:15:00,122.01,79.57300000000001,52.633,29.93 -2020-09-21 09:30:00,121.3,79.82,52.633,29.93 -2020-09-21 09:45:00,123.09,79.15100000000001,52.633,29.93 -2020-09-21 10:00:00,127.26,77.78,50.647,29.93 -2020-09-21 10:15:00,126.28,78.107,50.647,29.93 -2020-09-21 10:30:00,120.76,77.462,50.647,29.93 -2020-09-21 10:45:00,121.89,76.899,50.647,29.93 -2020-09-21 11:00:00,118.55,74.681,50.245,29.93 -2020-09-21 11:15:00,120.27,75.36399999999999,50.245,29.93 -2020-09-21 11:30:00,121.52,76.82600000000001,50.245,29.93 -2020-09-21 11:45:00,118.95,76.563,50.245,29.93 -2020-09-21 12:00:00,109.74,74.55,46.956,29.93 -2020-09-21 12:15:00,113.88,74.17699999999999,46.956,29.93 -2020-09-21 12:30:00,105.64,72.477,46.956,29.93 -2020-09-21 12:45:00,114.96,72.624,46.956,29.93 -2020-09-21 13:00:00,114.37,72.619,47.383,29.93 -2020-09-21 13:15:00,107.54,71.649,47.383,29.93 -2020-09-21 13:30:00,102.4,70.263,47.383,29.93 -2020-09-21 13:45:00,107.14,70.24600000000001,47.383,29.93 -2020-09-21 14:00:00,122.16,70.2,47.1,29.93 -2020-09-21 14:15:00,123.25,70.017,47.1,29.93 -2020-09-21 14:30:00,111.73,68.586,47.1,29.93 -2020-09-21 14:45:00,113.21,69.455,47.1,29.93 -2020-09-21 15:00:00,114.42,69.688,49.355,29.93 -2020-09-21 15:15:00,113.68,67.744,49.355,29.93 -2020-09-21 15:30:00,115.25,67.443,49.355,29.93 -2020-09-21 15:45:00,112.83,66.139,49.355,29.93 -2020-09-21 16:00:00,116.9,67.553,52.14,29.93 -2020-09-21 16:15:00,113.27,66.914,52.14,29.93 -2020-09-21 16:30:00,113.73,67.6,52.14,29.93 -2020-09-21 16:45:00,116.11,64.986,52.14,29.93 -2020-09-21 17:00:00,119.45,65.829,58.705,29.93 -2020-09-21 17:15:00,118.12,66.80199999999999,58.705,29.93 -2020-09-21 17:30:00,117.64,66.57,58.705,29.93 -2020-09-21 17:45:00,115.33,66.952,58.705,29.93 -2020-09-21 18:00:00,117.55,67.64,59.153,29.93 -2020-09-21 18:15:00,117.76,66.494,59.153,29.93 -2020-09-21 18:30:00,118.86,65.55,59.153,29.93 -2020-09-21 18:45:00,118.16,69.444,59.153,29.93 -2020-09-21 19:00:00,113.92,70.17,61.483000000000004,29.93 -2020-09-21 19:15:00,109.99,68.964,61.483000000000004,29.93 -2020-09-21 19:30:00,107.98,68.619,61.483000000000004,29.93 -2020-09-21 19:45:00,105.49,68.542,61.483000000000004,29.93 -2020-09-21 20:00:00,105.98,66.316,67.55,29.93 -2020-09-21 20:15:00,107.86,65.946,67.55,29.93 -2020-09-21 20:30:00,105.11,64.217,67.55,29.93 -2020-09-21 20:45:00,95.78,63.5,67.55,29.93 -2020-09-21 21:00:00,94.14,61.354,60.026,29.93 -2020-09-21 21:15:00,93.33,63.472,60.026,29.93 -2020-09-21 21:30:00,94.41,62.532,60.026,29.93 -2020-09-21 21:45:00,94.3,61.356,60.026,29.93 -2020-09-21 22:00:00,89.01,59.428000000000004,52.736999999999995,29.93 -2020-09-21 22:15:00,84.04,58.778999999999996,52.736999999999995,29.93 -2020-09-21 22:30:00,83.53,49.756,52.736999999999995,29.93 -2020-09-21 22:45:00,85.82,46.015,52.736999999999995,29.93 -2020-09-21 23:00:00,83.32,42.842,44.408,29.93 -2020-09-21 23:15:00,76.73,41.349,44.408,29.93 -2020-09-21 23:30:00,74.23,41.486000000000004,44.408,29.93 -2020-09-21 23:45:00,81.41,40.847,44.408,29.93 -2020-09-22 00:00:00,79.25,40.692,44.438,29.93 -2020-09-22 00:15:00,79.02,41.415,44.438,29.93 -2020-09-22 00:30:00,71.81,41.271,44.438,29.93 -2020-09-22 00:45:00,70.35,41.129,44.438,29.93 -2020-09-22 01:00:00,78.04,41.229,41.468999999999994,29.93 -2020-09-22 01:15:00,79.59,40.599000000000004,41.468999999999994,29.93 -2020-09-22 01:30:00,77.85,39.745,41.468999999999994,29.93 -2020-09-22 01:45:00,74.14,38.878,41.468999999999994,29.93 -2020-09-22 02:00:00,76.1,39.185,39.708,29.93 -2020-09-22 02:15:00,78.9,39.167,39.708,29.93 -2020-09-22 02:30:00,78.9,39.602,39.708,29.93 -2020-09-22 02:45:00,74.95,40.354,39.708,29.93 -2020-09-22 03:00:00,72.1,42.326,38.919000000000004,29.93 -2020-09-22 03:15:00,77.33,42.497,38.919000000000004,29.93 -2020-09-22 03:30:00,75.89,42.504,38.919000000000004,29.93 -2020-09-22 03:45:00,80.08,42.905,38.919000000000004,29.93 -2020-09-22 04:00:00,83.28,51.184,40.092,29.93 -2020-09-22 04:15:00,83.18,59.461999999999996,40.092,29.93 -2020-09-22 04:30:00,88.24,57.948,40.092,29.93 -2020-09-22 04:45:00,92.26,59.123999999999995,40.092,29.93 -2020-09-22 05:00:00,101.36,81.043,43.713,29.93 -2020-09-22 05:15:00,112.59,98.867,43.713,29.93 -2020-09-22 05:30:00,117.29,92.676,43.713,29.93 -2020-09-22 05:45:00,118.66,86.45299999999999,43.713,29.93 -2020-09-22 06:00:00,117.1,86.402,56.033,29.93 -2020-09-22 06:15:00,118.12,89.12799999999999,56.033,29.93 -2020-09-22 06:30:00,117.39,87.425,56.033,29.93 -2020-09-22 06:45:00,118.72,88.47200000000001,56.033,29.93 -2020-09-22 07:00:00,120.28,87.978,66.003,29.93 -2020-09-22 07:15:00,118.56,89.75299999999999,66.003,29.93 -2020-09-22 07:30:00,117.78,88.869,66.003,29.93 -2020-09-22 07:45:00,117.36,89.32600000000001,66.003,29.93 -2020-09-22 08:00:00,115.63,85.794,57.474,29.93 -2020-09-22 08:15:00,115.97,86.789,57.474,29.93 -2020-09-22 08:30:00,116.15,85.074,57.474,29.93 -2020-09-22 08:45:00,116.14,85.37,57.474,29.93 -2020-09-22 09:00:00,113.46,79.796,51.928000000000004,29.93 -2020-09-22 09:15:00,114.86,77.87100000000001,51.928000000000004,29.93 -2020-09-22 09:30:00,116.65,78.78399999999999,51.928000000000004,29.93 -2020-09-22 09:45:00,117.21,78.98100000000001,51.928000000000004,29.93 -2020-09-22 10:00:00,116.31,76.461,49.46,29.93 -2020-09-22 10:15:00,112.61,76.334,49.46,29.93 -2020-09-22 10:30:00,112.62,75.747,49.46,29.93 -2020-09-22 10:45:00,113.46,75.955,49.46,29.93 -2020-09-22 11:00:00,111.98,74.19800000000001,48.206,29.93 -2020-09-22 11:15:00,107.81,75.045,48.206,29.93 -2020-09-22 11:30:00,113.38,75.354,48.206,29.93 -2020-09-22 11:45:00,106.2,74.97399999999999,48.206,29.93 -2020-09-22 12:00:00,105.03,72.382,46.285,29.93 -2020-09-22 12:15:00,104.5,72.095,46.285,29.93 -2020-09-22 12:30:00,103.2,71.24,46.285,29.93 -2020-09-22 12:45:00,99.21,71.758,46.285,29.93 -2020-09-22 13:00:00,102.59,71.366,46.861999999999995,29.93 -2020-09-22 13:15:00,108.02,71.529,46.861999999999995,29.93 -2020-09-22 13:30:00,103.9,70.479,46.861999999999995,29.93 -2020-09-22 13:45:00,102.03,69.86,46.861999999999995,29.93 -2020-09-22 14:00:00,106.58,70.15899999999999,46.488,29.93 -2020-09-22 14:15:00,105.28,69.903,46.488,29.93 -2020-09-22 14:30:00,101.09,68.925,46.488,29.93 -2020-09-22 14:45:00,102.61,69.22399999999999,46.488,29.93 -2020-09-22 15:00:00,105.33,69.167,48.442,29.93 -2020-09-22 15:15:00,104.23,67.98,48.442,29.93 -2020-09-22 15:30:00,103.53,67.63600000000001,48.442,29.93 -2020-09-22 15:45:00,105.75,66.461,48.442,29.93 -2020-09-22 16:00:00,108.11,67.479,50.397,29.93 -2020-09-22 16:15:00,108.64,67.058,50.397,29.93 -2020-09-22 16:30:00,110.45,67.669,50.397,29.93 -2020-09-22 16:45:00,113.26,65.699,50.397,29.93 -2020-09-22 17:00:00,114.67,66.813,56.668,29.93 -2020-09-22 17:15:00,114.65,68.11,56.668,29.93 -2020-09-22 17:30:00,116.47,67.773,56.668,29.93 -2020-09-22 17:45:00,117.39,67.911,56.668,29.93 -2020-09-22 18:00:00,121.5,67.854,57.957,29.93 -2020-09-22 18:15:00,119.7,67.63600000000001,57.957,29.93 -2020-09-22 18:30:00,121.25,66.432,57.957,29.93 -2020-09-22 18:45:00,119.41,70.4,57.957,29.93 -2020-09-22 19:00:00,117.27,70.289,57.056000000000004,29.93 -2020-09-22 19:15:00,114.03,69.138,57.056000000000004,29.93 -2020-09-22 19:30:00,113.68,68.457,57.056000000000004,29.93 -2020-09-22 19:45:00,108.16,68.626,57.056000000000004,29.93 -2020-09-22 20:00:00,106.04,66.758,64.156,29.93 -2020-09-22 20:15:00,110.06,65.166,64.156,29.93 -2020-09-22 20:30:00,108.07,63.717,64.156,29.93 -2020-09-22 20:45:00,100.9,63.108999999999995,64.156,29.93 -2020-09-22 21:00:00,93.55,61.407,56.507,29.93 -2020-09-22 21:15:00,93.78,62.69,56.507,29.93 -2020-09-22 21:30:00,90.09,61.68899999999999,56.507,29.93 -2020-09-22 21:45:00,92.81,60.684,56.507,29.93 -2020-09-22 22:00:00,88.4,59.316,50.728,29.93 -2020-09-22 22:15:00,90.76,58.328,50.728,29.93 -2020-09-22 22:30:00,83.74,49.538000000000004,50.728,29.93 -2020-09-22 22:45:00,79.35,45.873000000000005,50.728,29.93 -2020-09-22 23:00:00,73.73,42.176,43.556999999999995,29.93 -2020-09-22 23:15:00,74.31,41.556999999999995,43.556999999999995,29.93 -2020-09-22 23:30:00,72.56,41.6,43.556999999999995,29.93 -2020-09-22 23:45:00,73.7,40.976000000000006,43.556999999999995,29.93 -2020-09-23 00:00:00,74.14,40.966,41.151,29.93 -2020-09-23 00:15:00,80.43,41.68600000000001,41.151,29.93 -2020-09-23 00:30:00,80.03,41.549,41.151,29.93 -2020-09-23 00:45:00,79.17,41.41,41.151,29.93 -2020-09-23 01:00:00,72.52,41.504,37.763000000000005,29.93 -2020-09-23 01:15:00,76.54,40.896,37.763000000000005,29.93 -2020-09-23 01:30:00,79.93,40.059,37.763000000000005,29.93 -2020-09-23 01:45:00,81.24,39.193000000000005,37.763000000000005,29.93 -2020-09-23 02:00:00,78.02,39.505,35.615,29.93 -2020-09-23 02:15:00,77.07,39.507,35.615,29.93 -2020-09-23 02:30:00,81.33,39.919000000000004,35.615,29.93 -2020-09-23 02:45:00,81.34,40.667,35.615,29.93 -2020-09-23 03:00:00,77.9,42.626000000000005,35.153,29.93 -2020-09-23 03:15:00,76.25,42.815,35.153,29.93 -2020-09-23 03:30:00,84.12,42.827,35.153,29.93 -2020-09-23 03:45:00,87.89,43.21,35.153,29.93 -2020-09-23 04:00:00,91.84,51.525,36.203,29.93 -2020-09-23 04:15:00,87.47,59.836999999999996,36.203,29.93 -2020-09-23 04:30:00,95.22,58.328,36.203,29.93 -2020-09-23 04:45:00,101.67,59.511,36.203,29.93 -2020-09-23 05:00:00,111.18,81.527,39.922,29.93 -2020-09-23 05:15:00,112.35,99.454,39.922,29.93 -2020-09-23 05:30:00,111.69,93.244,39.922,29.93 -2020-09-23 05:45:00,116.38,86.97,39.922,29.93 -2020-09-23 06:00:00,119.88,86.88600000000001,56.443999999999996,29.93 -2020-09-23 06:15:00,119.76,89.635,56.443999999999996,29.93 -2020-09-23 06:30:00,120.0,87.935,56.443999999999996,29.93 -2020-09-23 06:45:00,121.7,88.979,56.443999999999996,29.93 -2020-09-23 07:00:00,124.88,88.484,68.683,29.93 -2020-09-23 07:15:00,120.09,90.274,68.683,29.93 -2020-09-23 07:30:00,121.21,89.425,68.683,29.93 -2020-09-23 07:45:00,123.12,89.883,68.683,29.93 -2020-09-23 08:00:00,116.84,86.35799999999999,59.003,29.93 -2020-09-23 08:15:00,115.12,87.31700000000001,59.003,29.93 -2020-09-23 08:30:00,115.48,85.62,59.003,29.93 -2020-09-23 08:45:00,115.51,85.897,59.003,29.93 -2020-09-23 09:00:00,114.02,80.328,56.21,29.93 -2020-09-23 09:15:00,114.6,78.39399999999999,56.21,29.93 -2020-09-23 09:30:00,110.5,79.286,56.21,29.93 -2020-09-23 09:45:00,108.97,79.453,56.21,29.93 -2020-09-23 10:00:00,108.11,76.928,52.358999999999995,29.93 -2020-09-23 10:15:00,109.82,76.764,52.358999999999995,29.93 -2020-09-23 10:30:00,110.15,76.161,52.358999999999995,29.93 -2020-09-23 10:45:00,105.93,76.354,52.358999999999995,29.93 -2020-09-23 11:00:00,105.59,74.609,51.161,29.93 -2020-09-23 11:15:00,106.75,75.438,51.161,29.93 -2020-09-23 11:30:00,104.38,75.747,51.161,29.93 -2020-09-23 11:45:00,103.61,75.348,51.161,29.93 -2020-09-23 12:00:00,100.04,72.733,49.119,29.93 -2020-09-23 12:15:00,106.82,72.434,49.119,29.93 -2020-09-23 12:30:00,109.78,71.612,49.119,29.93 -2020-09-23 12:45:00,102.31,72.122,49.119,29.93 -2020-09-23 13:00:00,103.09,71.70100000000001,49.187,29.93 -2020-09-23 13:15:00,103.29,71.862,49.187,29.93 -2020-09-23 13:30:00,102.62,70.81,49.187,29.93 -2020-09-23 13:45:00,105.34,70.196,49.187,29.93 -2020-09-23 14:00:00,107.99,70.447,49.787,29.93 -2020-09-23 14:15:00,105.73,70.204,49.787,29.93 -2020-09-23 14:30:00,107.7,69.259,49.787,29.93 -2020-09-23 14:45:00,109.29,69.555,49.787,29.93 -2020-09-23 15:00:00,108.43,69.45100000000001,51.458999999999996,29.93 -2020-09-23 15:15:00,104.72,68.285,51.458999999999996,29.93 -2020-09-23 15:30:00,107.19,67.971,51.458999999999996,29.93 -2020-09-23 15:45:00,107.85,66.812,51.458999999999996,29.93 -2020-09-23 16:00:00,109.45,67.789,53.663000000000004,29.93 -2020-09-23 16:15:00,108.62,67.385,53.663000000000004,29.93 -2020-09-23 16:30:00,110.5,67.992,53.663000000000004,29.93 -2020-09-23 16:45:00,113.22,66.078,53.663000000000004,29.93 -2020-09-23 17:00:00,116.47,67.153,58.183,29.93 -2020-09-23 17:15:00,114.39,68.475,58.183,29.93 -2020-09-23 17:30:00,116.38,68.142,58.183,29.93 -2020-09-23 17:45:00,117.99,68.31,58.183,29.93 -2020-09-23 18:00:00,119.46,68.234,60.141000000000005,29.93 -2020-09-23 18:15:00,119.27,68.014,60.141000000000005,29.93 -2020-09-23 18:30:00,119.58,66.82,60.141000000000005,29.93 -2020-09-23 18:45:00,119.51,70.78399999999999,60.141000000000005,29.93 -2020-09-23 19:00:00,115.57,70.683,60.582,29.93 -2020-09-23 19:15:00,111.65,69.531,60.582,29.93 -2020-09-23 19:30:00,110.06,68.845,60.582,29.93 -2020-09-23 19:45:00,107.79,69.005,60.582,29.93 -2020-09-23 20:00:00,103.36,67.161,66.61,29.93 -2020-09-23 20:15:00,99.89,65.566,66.61,29.93 -2020-09-23 20:30:00,99.32,64.09,66.61,29.93 -2020-09-23 20:45:00,101.02,63.443000000000005,66.61,29.93 -2020-09-23 21:00:00,98.0,61.744,57.658,29.93 -2020-09-23 21:15:00,99.6,63.016999999999996,57.658,29.93 -2020-09-23 21:30:00,86.71,62.025,57.658,29.93 -2020-09-23 21:45:00,90.43,60.985,57.658,29.93 -2020-09-23 22:00:00,84.37,59.597,51.81,29.93 -2020-09-23 22:15:00,91.09,58.586000000000006,51.81,29.93 -2020-09-23 22:30:00,88.12,49.79,51.81,29.93 -2020-09-23 22:45:00,83.92,46.13,51.81,29.93 -2020-09-23 23:00:00,76.12,42.474,42.93600000000001,29.93 -2020-09-23 23:15:00,75.88,41.823,42.93600000000001,29.93 -2020-09-23 23:30:00,79.32,41.87,42.93600000000001,29.93 -2020-09-23 23:45:00,82.59,41.246,42.93600000000001,29.93 -2020-09-24 00:00:00,79.44,41.243,39.211,29.93 -2020-09-24 00:15:00,76.44,41.958999999999996,39.211,29.93 -2020-09-24 00:30:00,75.48,41.83,39.211,29.93 -2020-09-24 00:45:00,79.62,41.693999999999996,39.211,29.93 -2020-09-24 01:00:00,76.77,41.78,37.607,29.93 -2020-09-24 01:15:00,80.51,41.193000000000005,37.607,29.93 -2020-09-24 01:30:00,72.87,40.375,37.607,29.93 -2020-09-24 01:45:00,79.28,39.509,37.607,29.93 -2020-09-24 02:00:00,78.34,39.827,36.44,29.93 -2020-09-24 02:15:00,78.78,39.849000000000004,36.44,29.93 -2020-09-24 02:30:00,74.7,40.239000000000004,36.44,29.93 -2020-09-24 02:45:00,82.77,40.981,36.44,29.93 -2020-09-24 03:00:00,81.53,42.928000000000004,36.116,29.93 -2020-09-24 03:15:00,79.01,43.135,36.116,29.93 -2020-09-24 03:30:00,78.21,43.151,36.116,29.93 -2020-09-24 03:45:00,82.11,43.516999999999996,36.116,29.93 -2020-09-24 04:00:00,87.36,51.869,37.398,29.93 -2020-09-24 04:15:00,93.6,60.213,37.398,29.93 -2020-09-24 04:30:00,97.83,58.708999999999996,37.398,29.93 -2020-09-24 04:45:00,99.6,59.9,37.398,29.93 -2020-09-24 05:00:00,109.7,82.016,41.776,29.93 -2020-09-24 05:15:00,108.24,100.046,41.776,29.93 -2020-09-24 05:30:00,110.1,93.816,41.776,29.93 -2020-09-24 05:45:00,112.5,87.49,41.776,29.93 -2020-09-24 06:00:00,118.92,87.375,55.61,29.93 -2020-09-24 06:15:00,120.61,90.146,55.61,29.93 -2020-09-24 06:30:00,123.08,88.449,55.61,29.93 -2020-09-24 06:45:00,124.17,89.49,55.61,29.93 -2020-09-24 07:00:00,126.37,88.994,67.13600000000001,29.93 -2020-09-24 07:15:00,125.34,90.79899999999999,67.13600000000001,29.93 -2020-09-24 07:30:00,127.26,89.985,67.13600000000001,29.93 -2020-09-24 07:45:00,127.08,90.443,67.13600000000001,29.93 -2020-09-24 08:00:00,125.35,86.925,57.55,29.93 -2020-09-24 08:15:00,126.55,87.84899999999999,57.55,29.93 -2020-09-24 08:30:00,129.1,86.16799999999999,57.55,29.93 -2020-09-24 08:45:00,130.06,86.425,57.55,29.93 -2020-09-24 09:00:00,128.57,80.861,52.931999999999995,29.93 -2020-09-24 09:15:00,126.84,78.918,52.931999999999995,29.93 -2020-09-24 09:30:00,123.67,79.79,52.931999999999995,29.93 -2020-09-24 09:45:00,127.27,79.92699999999999,52.931999999999995,29.93 -2020-09-24 10:00:00,125.65,77.398,50.36600000000001,29.93 -2020-09-24 10:15:00,124.59,77.194,50.36600000000001,29.93 -2020-09-24 10:30:00,120.94,76.577,50.36600000000001,29.93 -2020-09-24 10:45:00,116.89,76.75399999999999,50.36600000000001,29.93 -2020-09-24 11:00:00,110.52,75.02,47.893,29.93 -2020-09-24 11:15:00,109.17,75.833,47.893,29.93 -2020-09-24 11:30:00,110.97,76.141,47.893,29.93 -2020-09-24 11:45:00,106.21,75.725,47.893,29.93 -2020-09-24 12:00:00,102.53,73.084,45.271,29.93 -2020-09-24 12:15:00,102.03,72.77199999999999,45.271,29.93 -2020-09-24 12:30:00,99.75,71.986,45.271,29.93 -2020-09-24 12:45:00,99.48,72.488,45.271,29.93 -2020-09-24 13:00:00,98.27,72.03699999999999,44.351000000000006,29.93 -2020-09-24 13:15:00,99.53,72.196,44.351000000000006,29.93 -2020-09-24 13:30:00,100.61,71.143,44.351000000000006,29.93 -2020-09-24 13:45:00,102.58,70.532,44.351000000000006,29.93 -2020-09-24 14:00:00,103.51,70.735,44.99,29.93 -2020-09-24 14:15:00,103.08,70.508,44.99,29.93 -2020-09-24 14:30:00,101.16,69.596,44.99,29.93 -2020-09-24 14:45:00,101.81,69.888,44.99,29.93 -2020-09-24 15:00:00,101.94,69.737,46.869,29.93 -2020-09-24 15:15:00,103.21,68.59,46.869,29.93 -2020-09-24 15:30:00,103.97,68.308,46.869,29.93 -2020-09-24 15:45:00,104.84,67.164,46.869,29.93 -2020-09-24 16:00:00,108.09,68.101,48.902,29.93 -2020-09-24 16:15:00,109.23,67.714,48.902,29.93 -2020-09-24 16:30:00,110.09,68.317,48.902,29.93 -2020-09-24 16:45:00,113.09,66.46,48.902,29.93 -2020-09-24 17:00:00,116.14,67.495,53.244,29.93 -2020-09-24 17:15:00,116.08,68.84100000000001,53.244,29.93 -2020-09-24 17:30:00,117.28,68.513,53.244,29.93 -2020-09-24 17:45:00,116.1,68.711,53.244,29.93 -2020-09-24 18:00:00,120.4,68.615,54.343999999999994,29.93 -2020-09-24 18:15:00,120.5,68.393,54.343999999999994,29.93 -2020-09-24 18:30:00,123.4,67.212,54.343999999999994,29.93 -2020-09-24 18:45:00,118.83,71.17,54.343999999999994,29.93 -2020-09-24 19:00:00,115.06,71.08,54.332,29.93 -2020-09-24 19:15:00,112.24,69.925,54.332,29.93 -2020-09-24 19:30:00,114.88,69.235,54.332,29.93 -2020-09-24 19:45:00,110.88,69.387,54.332,29.93 -2020-09-24 20:00:00,107.62,67.565,58.06,29.93 -2020-09-24 20:15:00,108.78,65.968,58.06,29.93 -2020-09-24 20:30:00,104.84,64.46600000000001,58.06,29.93 -2020-09-24 20:45:00,96.86,63.781000000000006,58.06,29.93 -2020-09-24 21:00:00,94.23,62.083999999999996,52.411,29.93 -2020-09-24 21:15:00,89.92,63.346000000000004,52.411,29.93 -2020-09-24 21:30:00,87.79,62.361000000000004,52.411,29.93 -2020-09-24 21:45:00,89.98,61.287,52.411,29.93 -2020-09-24 22:00:00,88.04,59.88,47.148999999999994,29.93 -2020-09-24 22:15:00,89.31,58.845,47.148999999999994,29.93 -2020-09-24 22:30:00,80.71,50.044,47.148999999999994,29.93 -2020-09-24 22:45:00,79.84,46.388000000000005,47.148999999999994,29.93 -2020-09-24 23:00:00,71.75,42.773,40.814,29.93 -2020-09-24 23:15:00,77.77,42.091,40.814,29.93 -2020-09-24 23:30:00,81.26,42.141000000000005,40.814,29.93 -2020-09-24 23:45:00,80.31,41.516999999999996,40.814,29.93 -2020-09-25 00:00:00,74.0,39.914,39.153,29.93 -2020-09-25 00:15:00,72.28,40.838,39.153,29.93 -2020-09-25 00:30:00,77.46,40.879,39.153,29.93 -2020-09-25 00:45:00,78.16,41.085,39.153,29.93 -2020-09-25 01:00:00,76.04,40.797,37.228,29.93 -2020-09-25 01:15:00,73.4,40.051,37.228,29.93 -2020-09-25 01:30:00,76.94,39.709,37.228,29.93 -2020-09-25 01:45:00,79.03,38.69,37.228,29.93 -2020-09-25 02:00:00,75.49,39.719,35.851,29.93 -2020-09-25 02:15:00,75.88,39.689,35.851,29.93 -2020-09-25 02:30:00,78.09,40.806,35.851,29.93 -2020-09-25 02:45:00,74.34,41.058,35.851,29.93 -2020-09-25 03:00:00,73.16,43.275,36.54,29.93 -2020-09-25 03:15:00,77.62,42.89,36.54,29.93 -2020-09-25 03:30:00,83.89,42.731,36.54,29.93 -2020-09-25 03:45:00,88.6,43.809,36.54,29.93 -2020-09-25 04:00:00,88.15,52.363,37.578,29.93 -2020-09-25 04:15:00,90.7,59.54,37.578,29.93 -2020-09-25 04:30:00,93.93,58.795,37.578,29.93 -2020-09-25 04:45:00,97.88,59.202,37.578,29.93 -2020-09-25 05:00:00,100.27,80.708,40.387,29.93 -2020-09-25 05:15:00,109.18,99.95299999999999,40.387,29.93 -2020-09-25 05:30:00,109.6,94.13,40.387,29.93 -2020-09-25 05:45:00,110.57,87.402,40.387,29.93 -2020-09-25 06:00:00,115.32,87.54799999999999,54.668,29.93 -2020-09-25 06:15:00,115.66,90.071,54.668,29.93 -2020-09-25 06:30:00,118.26,88.10700000000001,54.668,29.93 -2020-09-25 06:45:00,117.52,89.484,54.668,29.93 -2020-09-25 07:00:00,120.63,89.242,63.971000000000004,29.93 -2020-09-25 07:15:00,121.05,92.009,63.971000000000004,29.93 -2020-09-25 07:30:00,117.32,89.76299999999999,63.971000000000004,29.93 -2020-09-25 07:45:00,117.02,89.816,63.971000000000004,29.93 -2020-09-25 08:00:00,113.23,86.553,56.042,29.93 -2020-09-25 08:15:00,112.83,87.81,56.042,29.93 -2020-09-25 08:30:00,112.79,86.36,56.042,29.93 -2020-09-25 08:45:00,114.62,86.03399999999999,56.042,29.93 -2020-09-25 09:00:00,110.41,78.957,52.832,29.93 -2020-09-25 09:15:00,113.15,78.538,52.832,29.93 -2020-09-25 09:30:00,115.51,78.775,52.832,29.93 -2020-09-25 09:45:00,121.32,79.123,52.832,29.93 -2020-09-25 10:00:00,122.11,76.09899999999999,50.044,29.93 -2020-09-25 10:15:00,119.95,75.921,50.044,29.93 -2020-09-25 10:30:00,119.89,75.65,50.044,29.93 -2020-09-25 10:45:00,123.44,75.60600000000001,50.044,29.93 -2020-09-25 11:00:00,126.14,74.045,49.06100000000001,29.93 -2020-09-25 11:15:00,126.84,73.806,49.06100000000001,29.93 -2020-09-25 11:30:00,127.73,74.418,49.06100000000001,29.93 -2020-09-25 11:45:00,125.85,73.337,49.06100000000001,29.93 -2020-09-25 12:00:00,124.09,71.339,45.595,29.93 -2020-09-25 12:15:00,125.3,69.887,45.595,29.93 -2020-09-25 12:30:00,123.99,69.266,45.595,29.93 -2020-09-25 12:45:00,122.75,69.4,45.595,29.93 -2020-09-25 13:00:00,119.15,69.617,43.218,29.93 -2020-09-25 13:15:00,119.21,70.164,43.218,29.93 -2020-09-25 13:30:00,117.01,69.656,43.218,29.93 -2020-09-25 13:45:00,117.78,69.24,43.218,29.93 -2020-09-25 14:00:00,116.77,68.476,41.926,29.93 -2020-09-25 14:15:00,115.25,68.501,41.926,29.93 -2020-09-25 14:30:00,113.74,68.818,41.926,29.93 -2020-09-25 14:45:00,110.79,68.735,41.926,29.93 -2020-09-25 15:00:00,110.83,68.325,43.79,29.93 -2020-09-25 15:15:00,108.35,66.892,43.79,29.93 -2020-09-25 15:30:00,109.43,65.762,43.79,29.93 -2020-09-25 15:45:00,113.35,65.20100000000001,43.79,29.93 -2020-09-25 16:00:00,112.61,65.172,45.895,29.93 -2020-09-25 16:15:00,111.78,65.234,45.895,29.93 -2020-09-25 16:30:00,114.66,65.737,45.895,29.93 -2020-09-25 16:45:00,114.16,63.353,45.895,29.93 -2020-09-25 17:00:00,116.51,65.618,51.36,29.93 -2020-09-25 17:15:00,117.46,66.74,51.36,29.93 -2020-09-25 17:30:00,116.53,66.434,51.36,29.93 -2020-09-25 17:45:00,119.69,66.47399999999999,51.36,29.93 -2020-09-25 18:00:00,124.62,66.657,52.985,29.93 -2020-09-25 18:15:00,119.12,65.642,52.985,29.93 -2020-09-25 18:30:00,119.12,64.514,52.985,29.93 -2020-09-25 18:45:00,114.05,68.764,52.985,29.93 -2020-09-25 19:00:00,110.75,69.617,52.602,29.93 -2020-09-25 19:15:00,109.43,69.267,52.602,29.93 -2020-09-25 19:30:00,107.76,68.505,52.602,29.93 -2020-09-25 19:45:00,104.87,67.777,52.602,29.93 -2020-09-25 20:00:00,100.97,65.862,58.063,29.93 -2020-09-25 20:15:00,102.74,64.84,58.063,29.93 -2020-09-25 20:30:00,100.5,62.946999999999996,58.063,29.93 -2020-09-25 20:45:00,93.97,61.794,58.063,29.93 -2020-09-25 21:00:00,88.38,61.198,50.135,29.93 -2020-09-25 21:15:00,87.81,63.786,50.135,29.93 -2020-09-25 21:30:00,85.24,62.715,50.135,29.93 -2020-09-25 21:45:00,86.18,61.906000000000006,50.135,29.93 -2020-09-25 22:00:00,83.96,60.652,45.165,29.93 -2020-09-25 22:15:00,79.17,59.376000000000005,45.165,29.93 -2020-09-25 22:30:00,75.1,55.809,45.165,29.93 -2020-09-25 22:45:00,77.09,53.768,45.165,29.93 -2020-09-25 23:00:00,79.16,51.29600000000001,39.121,29.93 -2020-09-25 23:15:00,75.29,48.898,39.121,29.93 -2020-09-25 23:30:00,70.25,47.217,39.121,29.93 -2020-09-25 23:45:00,67.44,46.32,39.121,29.93 -2020-09-26 00:00:00,64.11,40.303000000000004,38.49,29.816 -2020-09-26 00:15:00,64.22,39.549,38.49,29.816 -2020-09-26 00:30:00,71.12,39.624,38.49,29.816 -2020-09-26 00:45:00,71.01,39.498000000000005,38.49,29.816 -2020-09-26 01:00:00,69.95,39.577,34.5,29.816 -2020-09-26 01:15:00,63.99,39.008,34.5,29.816 -2020-09-26 01:30:00,69.4,37.955,34.5,29.816 -2020-09-26 01:45:00,69.93,37.782,34.5,29.816 -2020-09-26 02:00:00,68.96,38.286,32.236,29.816 -2020-09-26 02:15:00,64.21,37.6,32.236,29.816 -2020-09-26 02:30:00,68.74,37.809,32.236,29.816 -2020-09-26 02:45:00,67.86,38.655,32.236,29.816 -2020-09-26 03:00:00,62.1,40.036,32.067,29.816 -2020-09-26 03:15:00,61.48,38.824,32.067,29.816 -2020-09-26 03:30:00,62.24,38.464,32.067,29.816 -2020-09-26 03:45:00,62.77,40.629,32.067,29.816 -2020-09-26 04:00:00,63.44,46.69,33.071,29.816 -2020-09-26 04:15:00,63.08,52.544,33.071,29.816 -2020-09-26 04:30:00,63.34,49.988,33.071,29.816 -2020-09-26 04:45:00,65.2,50.463,33.071,29.816 -2020-09-26 05:00:00,67.69,62.15,33.014,29.816 -2020-09-26 05:15:00,69.25,68.483,33.014,29.816 -2020-09-26 05:30:00,71.19,63.869,33.014,29.816 -2020-09-26 05:45:00,70.98,61.817,33.014,29.816 -2020-09-26 06:00:00,72.82,74.937,34.628,29.816 -2020-09-26 06:15:00,73.93,87.507,34.628,29.816 -2020-09-26 06:30:00,75.0,82.045,34.628,29.816 -2020-09-26 06:45:00,77.15,78.709,34.628,29.816 -2020-09-26 07:00:00,78.72,76.085,38.871,29.816 -2020-09-26 07:15:00,78.7,77.581,38.871,29.816 -2020-09-26 07:30:00,79.43,77.208,38.871,29.816 -2020-09-26 07:45:00,81.14,79.021,38.871,29.816 -2020-09-26 08:00:00,82.04,77.229,43.293,29.816 -2020-09-26 08:15:00,81.29,79.325,43.293,29.816 -2020-09-26 08:30:00,79.31,78.339,43.293,29.816 -2020-09-26 08:45:00,78.64,79.563,43.293,29.816 -2020-09-26 09:00:00,77.73,75.152,44.559,29.816 -2020-09-26 09:15:00,77.3,75.286,44.559,29.816 -2020-09-26 09:30:00,76.01,76.116,44.559,29.816 -2020-09-26 09:45:00,76.69,76.161,44.559,29.816 -2020-09-26 10:00:00,77.09,73.52,42.091,29.816 -2020-09-26 10:15:00,78.02,73.59899999999999,42.091,29.816 -2020-09-26 10:30:00,79.27,73.126,42.091,29.816 -2020-09-26 10:45:00,75.73,73.221,42.091,29.816 -2020-09-26 11:00:00,74.16,71.61399999999999,38.505,29.816 -2020-09-26 11:15:00,73.22,71.741,38.505,29.816 -2020-09-26 11:30:00,74.17,72.20100000000001,38.505,29.816 -2020-09-26 11:45:00,72.12,71.267,38.505,29.816 -2020-09-26 12:00:00,70.46,69.072,35.388000000000005,29.816 -2020-09-26 12:15:00,67.44,68.39,35.388000000000005,29.816 -2020-09-26 12:30:00,71.03,67.819,35.388000000000005,29.816 -2020-09-26 12:45:00,69.48,68.183,35.388000000000005,29.816 -2020-09-26 13:00:00,63.24,67.752,31.355999999999998,29.816 -2020-09-26 13:15:00,64.45,67.275,31.355999999999998,29.816 -2020-09-26 13:30:00,64.55,66.75,31.355999999999998,29.816 -2020-09-26 13:45:00,63.96,65.571,31.355999999999998,29.816 -2020-09-26 14:00:00,64.09,65.168,30.522,29.816 -2020-09-26 14:15:00,67.34,64.189,30.522,29.816 -2020-09-26 14:30:00,65.35,63.668,30.522,29.816 -2020-09-26 14:45:00,66.06,63.978,30.522,29.816 -2020-09-26 15:00:00,67.79,64.027,34.36,29.816 -2020-09-26 15:15:00,66.51,63.351000000000006,34.36,29.816 -2020-09-26 15:30:00,68.16,62.885,34.36,29.816 -2020-09-26 15:45:00,72.59,61.718999999999994,34.36,29.816 -2020-09-26 16:00:00,75.45,62.938,39.507,29.816 -2020-09-26 16:15:00,77.55,62.669,39.507,29.816 -2020-09-26 16:30:00,79.63,63.308,39.507,29.816 -2020-09-26 16:45:00,83.05,61.232,39.507,29.816 -2020-09-26 17:00:00,85.81,62.456,47.151,29.816 -2020-09-26 17:15:00,86.82,62.61,47.151,29.816 -2020-09-26 17:30:00,88.1,62.18899999999999,47.151,29.816 -2020-09-26 17:45:00,90.04,62.478,47.151,29.816 -2020-09-26 18:00:00,95.33,63.623000000000005,50.303999999999995,29.816 -2020-09-26 18:15:00,94.65,64.312,50.303999999999995,29.816 -2020-09-26 18:30:00,97.77,64.545,50.303999999999995,29.816 -2020-09-26 18:45:00,95.72,65.35300000000001,50.303999999999995,29.816 -2020-09-26 19:00:00,90.07,65.441,50.622,29.816 -2020-09-26 19:15:00,86.83,64.219,50.622,29.816 -2020-09-26 19:30:00,85.33,64.218,50.622,29.816 -2020-09-26 19:45:00,84.12,64.718,50.622,29.816 -2020-09-26 20:00:00,79.3,63.951,45.391000000000005,29.816 -2020-09-26 20:15:00,80.25,63.068000000000005,45.391000000000005,29.816 -2020-09-26 20:30:00,78.57,60.422,45.391000000000005,29.816 -2020-09-26 20:45:00,77.53,60.446000000000005,45.391000000000005,29.816 -2020-09-26 21:00:00,73.77,59.438,39.98,29.816 -2020-09-26 21:15:00,75.16,61.888000000000005,39.98,29.816 -2020-09-26 21:30:00,69.54,61.303999999999995,39.98,29.816 -2020-09-26 21:45:00,69.49,59.95399999999999,39.98,29.816 -2020-09-26 22:00:00,66.64,59.036,37.53,29.816 -2020-09-26 22:15:00,67.12,58.604,37.53,29.816 -2020-09-26 22:30:00,63.4,56.483000000000004,37.53,29.816 -2020-09-26 22:45:00,63.85,55.235,37.53,29.816 -2020-09-26 23:00:00,58.44,52.963,30.97,29.816 -2020-09-26 23:15:00,57.4,50.388000000000005,30.97,29.816 -2020-09-26 23:30:00,57.88,50.007,30.97,29.816 -2020-09-26 23:45:00,57.35,48.701,30.97,29.816 -2020-09-27 00:00:00,58.71,41.699,27.24,29.816 -2020-09-27 00:15:00,55.74,40.016999999999996,27.24,29.816 -2020-09-27 00:30:00,54.56,39.891,27.24,29.816 -2020-09-27 00:45:00,54.16,39.876999999999995,27.24,29.816 -2020-09-27 01:00:00,52.05,40.103,25.662,29.816 -2020-09-27 01:15:00,53.71,39.753,25.662,29.816 -2020-09-27 01:30:00,52.61,38.762,25.662,29.816 -2020-09-27 01:45:00,52.16,38.228,25.662,29.816 -2020-09-27 02:00:00,52.19,38.591,25.67,29.816 -2020-09-27 02:15:00,52.28,38.199,25.67,29.816 -2020-09-27 02:30:00,52.04,38.759,25.67,29.816 -2020-09-27 02:45:00,51.99,39.516,25.67,29.816 -2020-09-27 03:00:00,52.66,41.443000000000005,24.258000000000003,29.816 -2020-09-27 03:15:00,52.96,40.293,24.258000000000003,29.816 -2020-09-27 03:30:00,54.26,39.794000000000004,24.258000000000003,29.816 -2020-09-27 03:45:00,54.89,41.345,24.258000000000003,29.816 -2020-09-27 04:00:00,55.49,47.354,25.051,29.816 -2020-09-27 04:15:00,55.87,52.648999999999994,25.051,29.816 -2020-09-27 04:30:00,55.54,51.083999999999996,25.051,29.816 -2020-09-27 04:45:00,56.78,51.303999999999995,25.051,29.816 -2020-09-27 05:00:00,58.11,62.303999999999995,25.145,29.816 -2020-09-27 05:15:00,58.1,67.563,25.145,29.816 -2020-09-27 05:30:00,58.65,62.586000000000006,25.145,29.816 -2020-09-27 05:45:00,58.18,60.34,25.145,29.816 -2020-09-27 06:00:00,59.68,71.635,26.371,29.816 -2020-09-27 06:15:00,60.43,84.40100000000001,26.371,29.816 -2020-09-27 06:30:00,61.96,78.143,26.371,29.816 -2020-09-27 06:45:00,63.71,73.797,26.371,29.816 -2020-09-27 07:00:00,64.31,71.99600000000001,28.756999999999998,29.816 -2020-09-27 07:15:00,65.55,72.084,28.756999999999998,29.816 -2020-09-27 07:30:00,66.0,72.34,28.756999999999998,29.816 -2020-09-27 07:45:00,66.81,73.921,28.756999999999998,29.816 -2020-09-27 08:00:00,66.27,73.15899999999999,32.82,29.816 -2020-09-27 08:15:00,65.26,76.039,32.82,29.816 -2020-09-27 08:30:00,64.16,76.156,32.82,29.816 -2020-09-27 08:45:00,63.97,77.865,32.82,29.816 -2020-09-27 09:00:00,62.41,73.256,35.534,29.816 -2020-09-27 09:15:00,62.07,73.188,35.534,29.816 -2020-09-27 09:30:00,61.44,74.26,35.534,29.816 -2020-09-27 09:45:00,62.57,74.954,35.534,29.816 -2020-09-27 10:00:00,63.81,73.396,35.925,29.816 -2020-09-27 10:15:00,64.87,73.69,35.925,29.816 -2020-09-27 10:30:00,65.54,73.535,35.925,29.816 -2020-09-27 10:45:00,66.79,73.756,35.925,29.816 -2020-09-27 11:00:00,64.87,72.186,37.056,29.816 -2020-09-27 11:15:00,63.25,72.028,37.056,29.816 -2020-09-27 11:30:00,60.2,72.585,37.056,29.816 -2020-09-27 11:45:00,59.31,71.992,37.056,29.816 -2020-09-27 12:00:00,54.52,70.286,33.124,29.816 -2020-09-27 12:15:00,56.11,69.765,33.124,29.816 -2020-09-27 12:30:00,55.91,68.917,33.124,29.816 -2020-09-27 12:45:00,58.63,68.538,33.124,29.816 -2020-09-27 13:00:00,58.5,67.667,29.874000000000002,29.816 -2020-09-27 13:15:00,55.01,67.723,29.874000000000002,29.816 -2020-09-27 13:30:00,55.43,66.368,29.874000000000002,29.816 -2020-09-27 13:45:00,55.98,65.756,29.874000000000002,29.816 -2020-09-27 14:00:00,57.32,66.19800000000001,27.302,29.816 -2020-09-27 14:15:00,57.14,65.899,27.302,29.816 -2020-09-27 14:30:00,57.98,64.95,27.302,29.816 -2020-09-27 14:45:00,60.21,64.396,27.302,29.816 -2020-09-27 15:00:00,61.57,64.02600000000001,27.642,29.816 -2020-09-27 15:15:00,62.18,63.051,27.642,29.816 -2020-09-27 15:30:00,64.05,62.675,27.642,29.816 -2020-09-27 15:45:00,67.12,61.934,27.642,29.816 -2020-09-27 16:00:00,70.42,62.468,31.945999999999998,29.816 -2020-09-27 16:15:00,73.68,62.107,31.945999999999998,29.816 -2020-09-27 16:30:00,76.58,63.519,31.945999999999998,29.816 -2020-09-27 16:45:00,79.25,61.583999999999996,31.945999999999998,29.816 -2020-09-27 17:00:00,82.98,63.023,40.387,29.816 -2020-09-27 17:15:00,82.75,64.161,40.387,29.816 -2020-09-27 17:30:00,87.05,64.39399999999999,40.387,29.816 -2020-09-27 17:45:00,86.61,65.675,40.387,29.816 -2020-09-27 18:00:00,90.18,67.0,44.575,29.816 -2020-09-27 18:15:00,89.39,67.767,44.575,29.816 -2020-09-27 18:30:00,89.29,67.25399999999999,44.575,29.816 -2020-09-27 18:45:00,87.47,68.656,44.575,29.816 -2020-09-27 19:00:00,91.61,70.178,45.623999999999995,29.816 -2020-09-27 19:15:00,93.17,68.29899999999999,45.623999999999995,29.816 -2020-09-27 19:30:00,92.79,68.07,45.623999999999995,29.816 -2020-09-27 19:45:00,86.34,68.607,45.623999999999995,29.816 -2020-09-27 20:00:00,78.55,68.045,44.583999999999996,29.816 -2020-09-27 20:15:00,81.4,67.33,44.583999999999996,29.816 -2020-09-27 20:30:00,83.71,65.579,44.583999999999996,29.816 -2020-09-27 20:45:00,80.58,64.027,44.583999999999996,29.816 -2020-09-27 21:00:00,84.36,62.199,39.732,29.816 -2020-09-27 21:15:00,89.77,64.25,39.732,29.816 -2020-09-27 21:30:00,86.37,63.286,39.732,29.816 -2020-09-27 21:45:00,80.3,62.198,39.732,29.816 -2020-09-27 22:00:00,75.34,62.406000000000006,38.571,29.816 -2020-09-27 22:15:00,77.9,60.526,38.571,29.816 -2020-09-27 22:30:00,78.22,57.183,38.571,29.816 -2020-09-27 22:45:00,80.64,54.788999999999994,38.571,29.816 -2020-09-27 23:00:00,76.15,51.59,33.121,29.816 -2020-09-27 23:15:00,70.67,50.4,33.121,29.816 -2020-09-27 23:30:00,68.9,49.846000000000004,33.121,29.816 -2020-09-27 23:45:00,69.98,48.894,33.121,29.816 -2020-09-28 00:00:00,72.56,44.205,32.506,29.93 -2020-09-28 00:15:00,73.53,43.888999999999996,32.506,29.93 -2020-09-28 00:30:00,72.62,43.526,32.506,29.93 -2020-09-28 00:45:00,68.54,43.083999999999996,32.506,29.93 -2020-09-28 01:00:00,65.59,43.588,31.121,29.93 -2020-09-28 01:15:00,65.31,43.13,31.121,29.93 -2020-09-28 01:30:00,66.33,42.431999999999995,31.121,29.93 -2020-09-28 01:45:00,66.87,41.858999999999995,31.121,29.93 -2020-09-28 02:00:00,64.01,42.553999999999995,29.605999999999998,29.93 -2020-09-28 02:15:00,69.97,41.842,29.605999999999998,29.93 -2020-09-28 02:30:00,74.06,42.589,29.605999999999998,29.93 -2020-09-28 02:45:00,75.8,43.067,29.605999999999998,29.93 -2020-09-28 03:00:00,72.48,45.693999999999996,28.124000000000002,29.93 -2020-09-28 03:15:00,69.89,45.528999999999996,28.124000000000002,29.93 -2020-09-28 03:30:00,74.11,45.453,28.124000000000002,29.93 -2020-09-28 03:45:00,79.39,46.504,28.124000000000002,29.93 -2020-09-28 04:00:00,83.04,55.977,29.743000000000002,29.93 -2020-09-28 04:15:00,81.76,64.546,29.743000000000002,29.93 -2020-09-28 04:30:00,89.53,63.233000000000004,29.743000000000002,29.93 -2020-09-28 04:45:00,96.21,63.772,29.743000000000002,29.93 -2020-09-28 05:00:00,105.6,83.844,36.191,29.93 -2020-09-28 05:15:00,107.36,101.895,36.191,29.93 -2020-09-28 05:30:00,107.75,95.816,36.191,29.93 -2020-09-28 05:45:00,111.23,89.825,36.191,29.93 -2020-09-28 06:00:00,115.68,89.09299999999999,55.277,29.93 -2020-09-28 06:15:00,115.66,91.397,55.277,29.93 -2020-09-28 06:30:00,117.81,90.111,55.277,29.93 -2020-09-28 06:45:00,119.85,91.897,55.277,29.93 -2020-09-28 07:00:00,122.15,91.338,65.697,29.93 -2020-09-28 07:15:00,122.14,93.427,65.697,29.93 -2020-09-28 07:30:00,121.34,92.88600000000001,65.697,29.93 -2020-09-28 07:45:00,116.32,93.995,65.697,29.93 -2020-09-28 08:00:00,117.15,90.522,57.028,29.93 -2020-09-28 08:15:00,115.61,91.912,57.028,29.93 -2020-09-28 08:30:00,119.14,90.226,57.028,29.93 -2020-09-28 08:45:00,116.91,91.141,57.028,29.93 -2020-09-28 09:00:00,116.01,85.585,52.633,29.93 -2020-09-28 09:15:00,119.67,83.25399999999999,52.633,29.93 -2020-09-28 09:30:00,118.68,83.365,52.633,29.93 -2020-09-28 09:45:00,122.1,82.484,52.633,29.93 -2020-09-28 10:00:00,117.58,81.078,50.647,29.93 -2020-09-28 10:15:00,119.15,81.132,50.647,29.93 -2020-09-28 10:30:00,116.21,80.383,50.647,29.93 -2020-09-28 10:45:00,116.73,79.708,50.647,29.93 -2020-09-28 11:00:00,119.42,77.577,50.245,29.93 -2020-09-28 11:15:00,116.62,78.139,50.245,29.93 -2020-09-28 11:30:00,121.85,79.594,50.245,29.93 -2020-09-28 11:45:00,117.42,79.208,50.245,29.93 -2020-09-28 12:00:00,113.11,77.02,46.956,29.93 -2020-09-28 12:15:00,116.0,76.562,46.956,29.93 -2020-09-28 12:30:00,113.7,75.102,46.956,29.93 -2020-09-28 12:45:00,110.78,75.194,46.956,29.93 -2020-09-28 13:00:00,109.56,74.983,47.383,29.93 -2020-09-28 13:15:00,112.19,74.00399999999999,47.383,29.93 -2020-09-28 13:30:00,112.32,72.598,47.383,29.93 -2020-09-28 13:45:00,113.59,72.613,47.383,29.93 -2020-09-28 14:00:00,115.79,72.227,47.1,29.93 -2020-09-28 14:15:00,115.49,72.146,47.1,29.93 -2020-09-28 14:30:00,114.12,70.952,47.1,29.93 -2020-09-28 14:45:00,114.56,71.797,47.1,29.93 -2020-09-28 15:00:00,117.46,71.695,49.355,29.93 -2020-09-28 15:15:00,116.47,69.889,49.355,29.93 -2020-09-28 15:30:00,114.37,69.814,49.355,29.93 -2020-09-28 15:45:00,114.24,68.618,49.355,29.93 -2020-09-28 16:00:00,115.27,69.747,52.14,29.93 -2020-09-28 16:15:00,117.99,69.223,52.14,29.93 -2020-09-28 16:30:00,117.86,69.88,52.14,29.93 -2020-09-28 16:45:00,120.05,67.665,52.14,29.93 -2020-09-28 17:00:00,122.78,68.229,58.705,29.93 -2020-09-28 17:15:00,121.13,69.37100000000001,58.705,29.93 -2020-09-28 17:30:00,122.96,69.17699999999999,58.705,29.93 -2020-09-28 17:45:00,120.89,69.768,58.705,29.93 -2020-09-28 18:00:00,124.3,70.32,59.153,29.93 -2020-09-28 18:15:00,121.62,69.16,59.153,29.93 -2020-09-28 18:30:00,123.48,68.303,59.153,29.93 -2020-09-28 18:45:00,119.09,72.157,59.153,29.93 -2020-09-28 19:00:00,115.15,72.955,61.483000000000004,29.93 -2020-09-28 19:15:00,109.93,71.735,61.483000000000004,29.93 -2020-09-28 19:30:00,107.66,71.362,61.483000000000004,29.93 -2020-09-28 19:45:00,110.06,71.22399999999999,61.483000000000004,29.93 -2020-09-28 20:00:00,99.87,69.15899999999999,67.55,29.93 -2020-09-28 20:15:00,98.53,68.77,67.55,29.93 -2020-09-28 20:30:00,102.19,66.858,67.55,29.93 -2020-09-28 20:45:00,103.9,65.867,67.55,29.93 -2020-09-28 21:00:00,96.81,63.74,60.026,29.93 -2020-09-28 21:15:00,91.9,65.782,60.026,29.93 -2020-09-28 21:30:00,86.66,64.904,60.026,29.93 -2020-09-28 21:45:00,91.92,63.486999999999995,60.026,29.93 -2020-09-28 22:00:00,89.43,61.42,52.736999999999995,29.93 -2020-09-28 22:15:00,89.06,60.604,52.736999999999995,29.93 -2020-09-28 22:30:00,81.86,51.547,52.736999999999995,29.93 -2020-09-28 22:45:00,86.05,47.843999999999994,52.736999999999995,29.93 -2020-09-28 23:00:00,82.52,44.95,44.408,29.93 -2020-09-28 23:15:00,80.8,43.236000000000004,44.408,29.93 -2020-09-28 23:30:00,76.73,43.395,44.408,29.93 -2020-09-28 23:45:00,79.66,42.756,44.408,29.93 -2020-09-29 00:00:00,77.73,42.65,44.438,29.93 -2020-09-29 00:15:00,76.61,43.342,44.438,29.93 -2020-09-29 00:30:00,76.57,43.253,44.438,29.93 -2020-09-29 00:45:00,78.99,43.126999999999995,44.438,29.93 -2020-09-29 01:00:00,77.92,43.176,41.468999999999994,29.93 -2020-09-29 01:15:00,78.18,42.699,41.468999999999994,29.93 -2020-09-29 01:30:00,74.21,41.974,41.468999999999994,29.93 -2020-09-29 01:45:00,77.34,41.108999999999995,41.468999999999994,29.93 -2020-09-29 02:00:00,78.32,41.458,39.708,29.93 -2020-09-29 02:15:00,77.97,41.577,39.708,29.93 -2020-09-29 02:30:00,78.04,41.858000000000004,39.708,29.93 -2020-09-29 02:45:00,80.85,42.573,39.708,29.93 -2020-09-29 03:00:00,79.93,44.456,38.919000000000004,29.93 -2020-09-29 03:15:00,76.51,44.75899999999999,38.919000000000004,29.93 -2020-09-29 03:30:00,79.49,44.794,38.919000000000004,29.93 -2020-09-29 03:45:00,80.02,45.07,38.919000000000004,29.93 -2020-09-29 04:00:00,89.45,53.608999999999995,40.092,29.93 -2020-09-29 04:15:00,93.05,62.126999999999995,40.092,29.93 -2020-09-29 04:30:00,95.48,60.648,40.092,29.93 -2020-09-29 04:45:00,95.39,61.878,40.092,29.93 -2020-09-29 05:00:00,103.88,84.5,43.713,29.93 -2020-09-29 05:15:00,108.14,103.072,43.713,29.93 -2020-09-29 05:30:00,110.41,96.729,43.713,29.93 -2020-09-29 05:45:00,114.39,90.139,43.713,29.93 -2020-09-29 06:00:00,118.77,89.865,56.033,29.93 -2020-09-29 06:15:00,120.88,92.749,56.033,29.93 -2020-09-29 06:30:00,121.79,91.06299999999999,56.033,29.93 -2020-09-29 06:45:00,123.75,92.088,56.033,29.93 -2020-09-29 07:00:00,126.35,91.59100000000001,66.003,29.93 -2020-09-29 07:15:00,126.55,93.463,66.003,29.93 -2020-09-29 07:30:00,125.28,92.823,66.003,29.93 -2020-09-29 07:45:00,123.7,93.275,66.003,29.93 -2020-09-29 08:00:00,120.71,89.791,57.474,29.93 -2020-09-29 08:15:00,117.0,90.537,57.474,29.93 -2020-09-29 08:30:00,116.25,88.941,57.474,29.93 -2020-09-29 08:45:00,114.5,89.095,57.474,29.93 -2020-09-29 09:00:00,115.58,83.553,51.928000000000004,29.93 -2020-09-29 09:15:00,120.06,81.567,51.928000000000004,29.93 -2020-09-29 09:30:00,116.6,82.34299999999999,51.928000000000004,29.93 -2020-09-29 09:45:00,112.53,82.32700000000001,51.928000000000004,29.93 -2020-09-29 10:00:00,117.82,79.771,49.46,29.93 -2020-09-29 10:15:00,117.6,79.37100000000001,49.46,29.93 -2020-09-29 10:30:00,115.38,78.68,49.46,29.93 -2020-09-29 10:45:00,115.03,78.775,49.46,29.93 -2020-09-29 11:00:00,112.32,77.102,48.206,29.93 -2020-09-29 11:15:00,114.51,77.828,48.206,29.93 -2020-09-29 11:30:00,115.85,78.133,48.206,29.93 -2020-09-29 11:45:00,122.55,77.62899999999999,48.206,29.93 -2020-09-29 12:00:00,121.21,74.861,46.285,29.93 -2020-09-29 12:15:00,123.65,74.48899999999999,46.285,29.93 -2020-09-29 12:30:00,119.98,73.876,46.285,29.93 -2020-09-29 12:45:00,119.13,74.34,46.285,29.93 -2020-09-29 13:00:00,117.91,73.741,46.861999999999995,29.93 -2020-09-29 13:15:00,116.67,73.89399999999999,46.861999999999995,29.93 -2020-09-29 13:30:00,117.65,72.825,46.861999999999995,29.93 -2020-09-29 13:45:00,118.25,72.237,46.861999999999995,29.93 -2020-09-29 14:00:00,115.97,72.195,46.488,29.93 -2020-09-29 14:15:00,115.47,72.04,46.488,29.93 -2020-09-29 14:30:00,114.39,71.3,46.488,29.93 -2020-09-29 14:45:00,114.29,71.57600000000001,46.488,29.93 -2020-09-29 15:00:00,110.86,71.184,48.442,29.93 -2020-09-29 15:15:00,112.31,70.13600000000001,48.442,29.93 -2020-09-29 15:30:00,111.91,70.016,48.442,29.93 -2020-09-29 15:45:00,114.05,68.95,48.442,29.93 -2020-09-29 16:00:00,116.1,69.681,50.397,29.93 -2020-09-29 16:15:00,115.08,69.375,50.397,29.93 -2020-09-29 16:30:00,117.86,69.956,50.397,29.93 -2020-09-29 16:45:00,119.27,68.388,50.397,29.93 -2020-09-29 17:00:00,122.25,69.221,56.668,29.93 -2020-09-29 17:15:00,118.99,70.688,56.668,29.93 -2020-09-29 17:30:00,119.0,70.389,56.668,29.93 -2020-09-29 17:45:00,121.21,70.738,56.668,29.93 -2020-09-29 18:00:00,124.72,70.545,57.957,29.93 -2020-09-29 18:15:00,120.93,70.315,57.957,29.93 -2020-09-29 18:30:00,122.34,69.196,57.957,29.93 -2020-09-29 18:45:00,118.19,73.126,57.957,29.93 -2020-09-29 19:00:00,111.46,73.086,57.056000000000004,29.93 -2020-09-29 19:15:00,107.02,71.922,57.056000000000004,29.93 -2020-09-29 19:30:00,105.82,71.21300000000001,57.056000000000004,29.93 -2020-09-29 19:45:00,104.05,71.321,57.056000000000004,29.93 -2020-09-29 20:00:00,99.51,69.616,64.156,29.93 -2020-09-29 20:15:00,95.64,68.005,64.156,29.93 -2020-09-29 20:30:00,91.07,66.372,64.156,29.93 -2020-09-29 20:45:00,88.43,65.49,64.156,29.93 -2020-09-29 21:00:00,82.9,63.803999999999995,56.507,29.93 -2020-09-29 21:15:00,78.99,65.01100000000001,56.507,29.93 -2020-09-29 21:30:00,76.78,64.074,56.507,29.93 -2020-09-29 21:45:00,75.2,62.826,56.507,29.93 -2020-09-29 22:00:00,71.1,61.318999999999996,50.728,29.93 -2020-09-29 22:15:00,70.7,60.165,50.728,29.93 -2020-09-29 22:30:00,67.54,51.342,50.728,29.93 -2020-09-29 22:45:00,65.87,47.714,50.728,29.93 -2020-09-29 23:00:00,77.01,44.299,43.556999999999995,29.93 -2020-09-29 23:15:00,80.01,43.45399999999999,43.556999999999995,29.93 -2020-09-29 23:30:00,80.68,43.519,43.556999999999995,29.93 -2020-09-29 23:45:00,80.11,42.897,43.556999999999995,29.93 -2020-09-30 00:00:00,75.35,42.93600000000001,41.151,29.93 -2020-09-30 00:15:00,78.11,43.623000000000005,41.151,29.93 -2020-09-30 00:30:00,79.8,43.541000000000004,41.151,29.93 -2020-09-30 00:45:00,81.56,43.416000000000004,41.151,29.93 -2020-09-30 01:00:00,76.14,43.457,37.763000000000005,29.93 -2020-09-30 01:15:00,75.09,43.004,37.763000000000005,29.93 -2020-09-30 01:30:00,75.45,42.298,37.763000000000005,29.93 -2020-09-30 01:45:00,80.02,41.433,37.763000000000005,29.93 -2020-09-30 02:00:00,79.63,41.788000000000004,35.615,29.93 -2020-09-30 02:15:00,78.9,41.925,35.615,29.93 -2020-09-30 02:30:00,76.86,42.18600000000001,35.615,29.93 -2020-09-30 02:45:00,78.72,42.895,35.615,29.93 -2020-09-30 03:00:00,81.85,44.765,35.153,29.93 -2020-09-30 03:15:00,81.74,45.086999999999996,35.153,29.93 -2020-09-30 03:30:00,82.99,45.126000000000005,35.153,29.93 -2020-09-30 03:45:00,83.55,45.383,35.153,29.93 -2020-09-30 04:00:00,88.62,53.961000000000006,36.203,29.93 -2020-09-30 04:15:00,91.38,62.516000000000005,36.203,29.93 -2020-09-30 04:30:00,94.94,61.042,36.203,29.93 -2020-09-30 04:45:00,97.42,62.28,36.203,29.93 -2020-09-30 05:00:00,103.8,85.005,39.922,29.93 -2020-09-30 05:15:00,107.95,103.689,39.922,29.93 -2020-09-30 05:30:00,110.12,97.321,39.922,29.93 -2020-09-30 05:45:00,113.38,90.678,39.922,29.93 -2020-09-30 06:00:00,119.1,90.37100000000001,56.443999999999996,29.93 -2020-09-30 06:15:00,117.38,93.279,56.443999999999996,29.93 -2020-09-30 06:30:00,119.12,91.594,56.443999999999996,29.93 -2020-09-30 06:45:00,120.5,92.615,56.443999999999996,29.93 -2020-09-30 07:00:00,123.29,92.118,68.683,29.93 -2020-09-30 07:15:00,121.03,94.00399999999999,68.683,29.93 -2020-09-30 07:30:00,122.13,93.398,68.683,29.93 -2020-09-30 07:45:00,120.93,93.84700000000001,68.683,29.93 -2020-09-30 08:00:00,117.66,90.37,59.003,29.93 -2020-09-30 08:15:00,117.3,91.08,59.003,29.93 -2020-09-30 08:30:00,116.66,89.5,59.003,29.93 -2020-09-30 08:45:00,116.56,89.633,59.003,29.93 -2020-09-30 09:00:00,115.38,84.096,56.21,29.93 -2020-09-30 09:15:00,115.67,82.101,56.21,29.93 -2020-09-30 09:30:00,115.08,82.85799999999999,56.21,29.93 -2020-09-30 09:45:00,114.96,82.81,56.21,29.93 -2020-09-30 10:00:00,115.1,80.25,52.358999999999995,29.93 -2020-09-30 10:15:00,116.5,79.81,52.358999999999995,29.93 -2020-09-30 10:30:00,113.95,79.10300000000001,52.358999999999995,29.93 -2020-09-30 10:45:00,112.23,79.183,52.358999999999995,29.93 -2020-09-30 11:00:00,108.77,77.523,51.161,29.93 -2020-09-30 11:15:00,108.69,78.23100000000001,51.161,29.93 -2020-09-30 11:30:00,109.39,78.536,51.161,29.93 -2020-09-30 11:45:00,108.48,78.013,51.161,29.93 -2020-09-30 12:00:00,104.28,75.219,49.119,29.93 -2020-09-30 12:15:00,106.05,74.836,49.119,29.93 -2020-09-30 12:30:00,104.62,74.258,49.119,29.93 -2020-09-30 12:45:00,103.69,74.71300000000001,49.119,29.93 -2020-09-30 13:00:00,102.95,74.086,49.187,29.93 -2020-09-30 13:15:00,102.97,74.237,49.187,29.93 -2020-09-30 13:30:00,103.28,73.165,49.187,29.93 -2020-09-30 13:45:00,103.85,72.581,49.187,29.93 -2020-09-30 14:00:00,104.84,72.49,49.787,29.93 -2020-09-30 14:15:00,104.58,72.35,49.787,29.93 -2020-09-30 14:30:00,105.13,71.645,49.787,29.93 -2020-09-30 14:45:00,105.67,71.917,49.787,29.93 -2020-09-30 15:00:00,105.44,71.477,51.458999999999996,29.93 -2020-09-30 15:15:00,106.06,70.44800000000001,51.458999999999996,29.93 -2020-09-30 15:30:00,106.29,70.362,51.458999999999996,29.93 -2020-09-30 15:45:00,107.58,69.31,51.458999999999996,29.93 -2020-09-30 16:00:00,110.43,70.0,53.663000000000004,29.93 -2020-09-30 16:15:00,109.91,69.711,53.663000000000004,29.93 -2020-09-30 16:30:00,111.8,70.28699999999999,53.663000000000004,29.93 -2020-09-30 16:45:00,113.95,68.777,53.663000000000004,29.93 -2020-09-30 17:00:00,117.7,69.568,58.183,29.93 -2020-09-30 17:15:00,116.19,71.06,58.183,29.93 -2020-09-30 17:30:00,119.57,70.767,58.183,29.93 -2020-09-30 17:45:00,120.66,71.14699999999999,58.183,29.93 -2020-09-30 18:00:00,122.31,70.935,60.141000000000005,29.93 -2020-09-30 18:15:00,120.45,70.704,60.141000000000005,29.93 -2020-09-30 18:30:00,121.29,69.597,60.141000000000005,29.93 -2020-09-30 18:45:00,119.4,73.52199999999999,60.141000000000005,29.93 -2020-09-30 19:00:00,116.88,73.491,60.582,29.93 -2020-09-30 19:15:00,115.1,72.325,60.582,29.93 -2020-09-30 19:30:00,111.94,71.613,60.582,29.93 -2020-09-30 19:45:00,110.07,71.71300000000001,60.582,29.93 -2020-09-30 20:00:00,104.26,70.03,66.61,29.93 -2020-09-30 20:15:00,105.15,68.417,66.61,29.93 -2020-09-30 20:30:00,101.51,66.757,66.61,29.93 -2020-09-30 20:45:00,102.47,65.836,66.61,29.93 -2020-09-30 21:00:00,96.11,64.15100000000001,57.658,29.93 -2020-09-30 21:15:00,94.9,65.348,57.658,29.93 -2020-09-30 21:30:00,93.22,64.42,57.658,29.93 -2020-09-30 21:45:00,94.05,63.138000000000005,57.658,29.93 -2020-09-30 22:00:00,89.15,61.61,51.81,29.93 -2020-09-30 22:15:00,86.92,60.433,51.81,29.93 -2020-09-30 22:30:00,83.39,51.606,51.81,29.93 -2020-09-30 22:45:00,81.96,47.983999999999995,51.81,29.93 -2020-09-30 23:00:00,72.81,44.608999999999995,42.93600000000001,29.93 -2020-09-30 23:15:00,74.96,43.731,42.93600000000001,29.93 -2020-09-30 23:30:00,74.25,43.799,42.93600000000001,29.93 -2020-09-30 23:45:00,77.92,43.177,42.93600000000001,29.93 -2020-10-01 00:00:00,74.36,48.013000000000005,42.746,31.349 -2020-10-01 00:15:00,73.35,49.218999999999994,42.746,31.349 -2020-10-01 00:30:00,71.89,48.891000000000005,42.746,31.349 -2020-10-01 00:45:00,74.82,48.773,42.746,31.349 -2020-10-01 01:00:00,74.95,49.074,40.025999999999996,31.349 -2020-10-01 01:15:00,73.4,48.443000000000005,40.025999999999996,31.349 -2020-10-01 01:30:00,70.75,47.641000000000005,40.025999999999996,31.349 -2020-10-01 01:45:00,74.21,47.16,40.025999999999996,31.349 -2020-10-01 02:00:00,74.54,47.581,38.154,31.349 -2020-10-01 02:15:00,75.11,47.598,38.154,31.349 -2020-10-01 02:30:00,70.83,48.211000000000006,38.154,31.349 -2020-10-01 02:45:00,74.98,48.898999999999994,38.154,31.349 -2020-10-01 03:00:00,76.99,51.108999999999995,37.575,31.349 -2020-10-01 03:15:00,78.83,51.843,37.575,31.349 -2020-10-01 03:30:00,75.95,51.895,37.575,31.349 -2020-10-01 03:45:00,81.07,52.646,37.575,31.349 -2020-10-01 04:00:00,86.24,61.338,39.154,31.349 -2020-10-01 04:15:00,88.04,69.964,39.154,31.349 -2020-10-01 04:30:00,85.63,69.25,39.154,31.349 -2020-10-01 04:45:00,92.33,70.865,39.154,31.349 -2020-10-01 05:00:00,98.87,96.057,44.085,31.349 -2020-10-01 05:15:00,108.96,118.06,44.085,31.349 -2020-10-01 05:30:00,116.11,112.14200000000001,44.085,31.349 -2020-10-01 05:45:00,112.83,104.488,44.085,31.349 -2020-10-01 06:00:00,118.1,104.522,57.49,31.349 -2020-10-01 06:15:00,117.62,108.09299999999999,57.49,31.349 -2020-10-01 06:30:00,119.15,106.42200000000001,57.49,31.349 -2020-10-01 06:45:00,121.05,107.11200000000001,57.49,31.349 -2020-10-01 07:00:00,122.34,107.375,73.617,31.349 -2020-10-01 07:15:00,120.54,109.579,73.617,31.349 -2020-10-01 07:30:00,118.69,109.189,73.617,31.349 -2020-10-01 07:45:00,117.8,109.36,73.617,31.349 -2020-10-01 08:00:00,115.95,107.73899999999999,69.281,31.349 -2020-10-01 08:15:00,115.95,108.02799999999999,69.281,31.349 -2020-10-01 08:30:00,122.25,105.654,69.281,31.349 -2020-10-01 08:45:00,124.29,104.889,69.281,31.349 -2020-10-01 09:00:00,122.13,100.23700000000001,63.926,31.349 -2020-10-01 09:15:00,121.05,98.084,63.926,31.349 -2020-10-01 09:30:00,116.55,98.544,63.926,31.349 -2020-10-01 09:45:00,115.32,98.477,63.926,31.349 -2020-10-01 10:00:00,116.93,95.274,59.442,31.349 -2020-10-01 10:15:00,119.48,94.848,59.442,31.349 -2020-10-01 10:30:00,116.82,93.89399999999999,59.442,31.349 -2020-10-01 10:45:00,115.21,93.82700000000001,59.442,31.349 -2020-10-01 11:00:00,110.64,90.531,56.771,31.349 -2020-10-01 11:15:00,110.57,91.235,56.771,31.349 -2020-10-01 11:30:00,114.18,91.44200000000001,56.771,31.349 -2020-10-01 11:45:00,110.72,91.22200000000001,56.771,31.349 -2020-10-01 12:00:00,110.27,87.663,53.701,31.349 -2020-10-01 12:15:00,110.53,87.29899999999999,53.701,31.349 -2020-10-01 12:30:00,110.68,86.45700000000001,53.701,31.349 -2020-10-01 12:45:00,110.33,86.771,53.701,31.349 -2020-10-01 13:00:00,109.27,86.516,52.364,31.349 -2020-10-01 13:15:00,108.61,86.375,52.364,31.349 -2020-10-01 13:30:00,106.82,85.52,52.364,31.349 -2020-10-01 13:45:00,108.3,85.07799999999999,52.364,31.349 -2020-10-01 14:00:00,106.34,84.788,53.419,31.349 -2020-10-01 14:15:00,103.21,85.045,53.419,31.349 -2020-10-01 14:30:00,104.24,84.14200000000001,53.419,31.349 -2020-10-01 14:45:00,101.78,84.021,53.419,31.349 -2020-10-01 15:00:00,102.18,84.177,56.744,31.349 -2020-10-01 15:15:00,105.7,83.725,56.744,31.349 -2020-10-01 15:30:00,109.3,83.95100000000001,56.744,31.349 -2020-10-01 15:45:00,112.38,83.89299999999999,56.744,31.349 -2020-10-01 16:00:00,110.92,83.941,60.458,31.349 -2020-10-01 16:15:00,110.81,83.338,60.458,31.349 -2020-10-01 16:30:00,115.07,84.031,60.458,31.349 -2020-10-01 16:45:00,114.72,82.064,60.458,31.349 -2020-10-01 17:00:00,119.03,82.493,66.295,31.349 -2020-10-01 17:15:00,116.61,83.67299999999999,66.295,31.349 -2020-10-01 17:30:00,118.16,83.4,66.295,31.349 -2020-10-01 17:45:00,121.44,84.021,66.295,31.349 -2020-10-01 18:00:00,125.72,83.735,68.468,31.349 -2020-10-01 18:15:00,122.88,83.59700000000001,68.468,31.349 -2020-10-01 18:30:00,124.48,82.62799999999999,68.468,31.349 -2020-10-01 18:45:00,119.47,86.285,68.468,31.349 -2020-10-01 19:00:00,116.32,86.665,66.39399999999999,31.349 -2020-10-01 19:15:00,113.44,85.539,66.39399999999999,31.349 -2020-10-01 19:30:00,108.44,85.01100000000001,66.39399999999999,31.349 -2020-10-01 19:45:00,109.78,85.054,66.39399999999999,31.349 -2020-10-01 20:00:00,106.35,84.295,63.183,31.349 -2020-10-01 20:15:00,108.46,82.07799999999999,63.183,31.349 -2020-10-01 20:30:00,106.1,81.286,63.183,31.349 -2020-10-01 20:45:00,101.39,80.277,63.183,31.349 -2020-10-01 21:00:00,93.83,78.27,55.133,31.349 -2020-10-01 21:15:00,92.88,78.835,55.133,31.349 -2020-10-01 21:30:00,91.65,77.945,55.133,31.349 -2020-10-01 21:45:00,92.82,76.352,55.133,31.349 -2020-10-01 22:00:00,85.0,73.477,50.111999999999995,31.349 -2020-10-01 22:15:00,85.89,71.318,50.111999999999995,31.349 -2020-10-01 22:30:00,82.6,60.872,50.111999999999995,31.349 -2020-10-01 22:45:00,86.0,55.68899999999999,50.111999999999995,31.349 -2020-10-01 23:00:00,78.43,50.915,44.536,31.349 -2020-10-01 23:15:00,77.59,50.651,44.536,31.349 -2020-10-01 23:30:00,77.83,50.118,44.536,31.349 -2020-10-01 23:45:00,79.72,50.093999999999994,44.536,31.349 -2020-10-02 00:00:00,74.84,46.853,42.291000000000004,31.349 -2020-10-02 00:15:00,76.22,48.257,42.291000000000004,31.349 -2020-10-02 00:30:00,70.57,48.04,42.291000000000004,31.349 -2020-10-02 00:45:00,72.88,48.211000000000006,42.291000000000004,31.349 -2020-10-02 01:00:00,69.56,48.17,41.008,31.349 -2020-10-02 01:15:00,70.58,47.573,41.008,31.349 -2020-10-02 01:30:00,77.02,47.115,41.008,31.349 -2020-10-02 01:45:00,78.7,46.528999999999996,41.008,31.349 -2020-10-02 02:00:00,78.05,47.533,39.521,31.349 -2020-10-02 02:15:00,73.35,47.49,39.521,31.349 -2020-10-02 02:30:00,78.63,48.773,39.521,31.349 -2020-10-02 02:45:00,79.6,49.075,39.521,31.349 -2020-10-02 03:00:00,74.8,51.332,39.812,31.349 -2020-10-02 03:15:00,75.56,51.751000000000005,39.812,31.349 -2020-10-02 03:30:00,79.48,51.661,39.812,31.349 -2020-10-02 03:45:00,85.82,53.034,39.812,31.349 -2020-10-02 04:00:00,90.92,61.923,41.22,31.349 -2020-10-02 04:15:00,87.14,69.571,41.22,31.349 -2020-10-02 04:30:00,91.86,69.50399999999999,41.22,31.349 -2020-10-02 04:45:00,98.29,70.303,41.22,31.349 -2020-10-02 05:00:00,108.33,94.79799999999999,45.115,31.349 -2020-10-02 05:15:00,107.36,118.024,45.115,31.349 -2020-10-02 05:30:00,109.91,112.62,45.115,31.349 -2020-10-02 05:45:00,112.72,104.635,45.115,31.349 -2020-10-02 06:00:00,119.78,104.962,59.06100000000001,31.349 -2020-10-02 06:15:00,119.47,108.075,59.06100000000001,31.349 -2020-10-02 06:30:00,123.95,106.051,59.06100000000001,31.349 -2020-10-02 06:45:00,121.8,107.29899999999999,59.06100000000001,31.349 -2020-10-02 07:00:00,122.66,107.613,71.874,31.349 -2020-10-02 07:15:00,121.44,110.766,71.874,31.349 -2020-10-02 07:30:00,120.73,109.22200000000001,71.874,31.349 -2020-10-02 07:45:00,118.08,108.91799999999999,71.874,31.349 -2020-10-02 08:00:00,116.05,107.29700000000001,68.439,31.349 -2020-10-02 08:15:00,115.68,107.771,68.439,31.349 -2020-10-02 08:30:00,115.03,105.775,68.439,31.349 -2020-10-02 08:45:00,114.17,104.251,68.439,31.349 -2020-10-02 09:00:00,111.59,98.507,65.523,31.349 -2020-10-02 09:15:00,110.49,97.64299999999999,65.523,31.349 -2020-10-02 09:30:00,110.15,97.525,65.523,31.349 -2020-10-02 09:45:00,108.89,97.596,65.523,31.349 -2020-10-02 10:00:00,107.9,93.774,62.005,31.349 -2020-10-02 10:15:00,108.37,93.501,62.005,31.349 -2020-10-02 10:30:00,108.22,92.79,62.005,31.349 -2020-10-02 10:45:00,109.74,92.461,62.005,31.349 -2020-10-02 11:00:00,105.01,89.28200000000001,60.351000000000006,31.349 -2020-10-02 11:15:00,103.83,88.994,60.351000000000006,31.349 -2020-10-02 11:30:00,107.03,89.801,60.351000000000006,31.349 -2020-10-02 11:45:00,102.87,89.085,60.351000000000006,31.349 -2020-10-02 12:00:00,100.19,86.24,55.331,31.349 -2020-10-02 12:15:00,99.6,84.566,55.331,31.349 -2020-10-02 12:30:00,97.98,83.89200000000001,55.331,31.349 -2020-10-02 12:45:00,96.69,84.041,55.331,31.349 -2020-10-02 13:00:00,95.31,84.48899999999999,53.361999999999995,31.349 -2020-10-02 13:15:00,96.32,84.825,53.361999999999995,31.349 -2020-10-02 13:30:00,95.77,84.383,53.361999999999995,31.349 -2020-10-02 13:45:00,96.42,84.069,53.361999999999995,31.349 -2020-10-02 14:00:00,97.78,82.788,51.708,31.349 -2020-10-02 14:15:00,97.81,83.20100000000001,51.708,31.349 -2020-10-02 14:30:00,97.63,83.339,51.708,31.349 -2020-10-02 14:45:00,99.23,83.00299999999999,51.708,31.349 -2020-10-02 15:00:00,99.88,82.859,54.571000000000005,31.349 -2020-10-02 15:15:00,98.98,82.102,54.571000000000005,31.349 -2020-10-02 15:30:00,100.68,81.37100000000001,54.571000000000005,31.349 -2020-10-02 15:45:00,102.59,81.78399999999999,54.571000000000005,31.349 -2020-10-02 16:00:00,107.25,80.862,58.662,31.349 -2020-10-02 16:15:00,107.43,80.667,58.662,31.349 -2020-10-02 16:30:00,108.62,81.304,58.662,31.349 -2020-10-02 16:45:00,109.89,78.92399999999999,58.662,31.349 -2020-10-02 17:00:00,114.07,80.343,65.941,31.349 -2020-10-02 17:15:00,111.49,81.27,65.941,31.349 -2020-10-02 17:30:00,115.35,80.94800000000001,65.941,31.349 -2020-10-02 17:45:00,115.86,81.39699999999999,65.941,31.349 -2020-10-02 18:00:00,122.12,81.46300000000001,65.628,31.349 -2020-10-02 18:15:00,118.93,80.623,65.628,31.349 -2020-10-02 18:30:00,117.78,79.774,65.628,31.349 -2020-10-02 18:45:00,115.24,83.65299999999999,65.628,31.349 -2020-10-02 19:00:00,111.24,84.94,63.662,31.349 -2020-10-02 19:15:00,106.67,84.70299999999999,63.662,31.349 -2020-10-02 19:30:00,105.91,84.031,63.662,31.349 -2020-10-02 19:45:00,102.96,83.294,63.662,31.349 -2020-10-02 20:00:00,92.61,82.48899999999999,61.945,31.349 -2020-10-02 20:15:00,99.16,80.71600000000001,61.945,31.349 -2020-10-02 20:30:00,98.55,79.605,61.945,31.349 -2020-10-02 20:45:00,96.27,78.343,61.945,31.349 -2020-10-02 21:00:00,88.3,77.288,53.903,31.349 -2020-10-02 21:15:00,85.55,78.967,53.903,31.349 -2020-10-02 21:30:00,80.59,78.02,53.903,31.349 -2020-10-02 21:45:00,84.21,76.735,53.903,31.349 -2020-10-02 22:00:00,80.6,74.171,48.403999999999996,31.349 -2020-10-02 22:15:00,79.32,71.794,48.403999999999996,31.349 -2020-10-02 22:30:00,71.59,66.65899999999999,48.403999999999996,31.349 -2020-10-02 22:45:00,70.6,63.413000000000004,48.403999999999996,31.349 -2020-10-02 23:00:00,62.12,59.43899999999999,41.07,31.349 -2020-10-02 23:15:00,61.6,57.461999999999996,41.07,31.349 -2020-10-02 23:30:00,61.56,55.302,41.07,31.349 -2020-10-02 23:45:00,59.87,54.931000000000004,41.07,31.349 -2020-10-03 00:00:00,57.65,47.711000000000006,11.117,31.177 -2020-10-03 00:15:00,57.59,46.246,11.117,31.177 -2020-10-03 00:30:00,56.67,46.058,11.117,31.177 -2020-10-03 00:45:00,56.96,46.294,11.117,31.177 -2020-10-03 01:00:00,56.34,46.769,10.685,31.177 -2020-10-03 01:15:00,56.06,46.481,10.685,31.177 -2020-10-03 01:30:00,55.87,45.493,10.685,31.177 -2020-10-03 01:45:00,55.71,45.202,10.685,31.177 -2020-10-03 02:00:00,55.96,45.67,7.925,31.177 -2020-10-03 02:15:00,56.34,45.118,7.925,31.177 -2020-10-03 02:30:00,56.14,45.926,7.925,31.177 -2020-10-03 02:45:00,56.27,46.731,7.925,31.177 -2020-10-03 03:00:00,55.64,48.928000000000004,7.627999999999999,31.177 -2020-10-03 03:15:00,57.67,48.434,7.627999999999999,31.177 -2020-10-03 03:30:00,57.91,48.034,7.627999999999999,31.177 -2020-10-03 03:45:00,58.8,49.805,7.627999999999999,31.177 -2020-10-03 04:00:00,60.47,55.907,7.986000000000001,31.177 -2020-10-03 04:15:00,59.42,61.435,7.986000000000001,31.177 -2020-10-03 04:30:00,59.0,60.357,7.986000000000001,31.177 -2020-10-03 04:45:00,58.38,60.965,7.986000000000001,31.177 -2020-10-03 05:00:00,59.9,73.63,9.039,31.177 -2020-10-03 05:15:00,59.86,81.95299999999999,9.039,31.177 -2020-10-03 05:30:00,60.28,77.28,9.039,31.177 -2020-10-03 05:45:00,61.97,73.884,9.039,31.177 -2020-10-03 06:00:00,62.0,86.214,10.683,31.177 -2020-10-03 06:15:00,62.94,100.009,10.683,31.177 -2020-10-03 06:30:00,64.88,93.45,10.683,31.177 -2020-10-03 06:45:00,68.16,88.402,10.683,31.177 -2020-10-03 07:00:00,74.5,87.154,14.055,31.177 -2020-10-03 07:15:00,72.95,87.805,14.055,31.177 -2020-10-03 07:30:00,76.05,88.484,14.055,31.177 -2020-10-03 07:45:00,75.81,89.97200000000001,14.055,31.177 -2020-10-03 08:00:00,77.66,91.262,17.652,31.177 -2020-10-03 08:15:00,78.36,93.70299999999999,17.652,31.177 -2020-10-03 08:30:00,76.08,93.544,17.652,31.177 -2020-10-03 08:45:00,77.02,94.595,17.652,31.177 -2020-10-03 09:00:00,76.35,90.97,21.353,31.177 -2020-10-03 09:15:00,79.16,90.64,21.353,31.177 -2020-10-03 09:30:00,78.99,91.32799999999999,21.353,31.177 -2020-10-03 09:45:00,81.23,91.696,21.353,31.177 -2020-10-03 10:00:00,83.68,89.508,23.467,31.177 -2020-10-03 10:15:00,86.8,89.756,23.467,31.177 -2020-10-03 10:30:00,87.14,89.29700000000001,23.467,31.177 -2020-10-03 10:45:00,87.03,89.06,23.467,31.177 -2020-10-03 11:00:00,82.41,86.065,24.539,31.177 -2020-10-03 11:15:00,82.83,85.729,24.539,31.177 -2020-10-03 11:30:00,76.93,86.07799999999999,24.539,31.177 -2020-10-03 11:45:00,78.79,85.66799999999999,24.539,31.177 -2020-10-03 12:00:00,74.79,82.721,21.488000000000003,31.177 -2020-10-03 12:15:00,72.19,82.29799999999999,21.488000000000003,31.177 -2020-10-03 12:30:00,59.91,81.195,21.488000000000003,31.177 -2020-10-03 12:45:00,62.91,80.593,21.488000000000003,31.177 -2020-10-03 13:00:00,61.74,79.922,18.776,31.177 -2020-10-03 13:15:00,59.78,80.102,18.776,31.177 -2020-10-03 13:30:00,66.56,78.882,18.776,31.177 -2020-10-03 13:45:00,70.81,78.37899999999999,18.776,31.177 -2020-10-03 14:00:00,68.47,78.347,17.301,31.177 -2020-10-03 14:15:00,68.4,78.655,17.301,31.177 -2020-10-03 14:30:00,67.72,77.69800000000001,17.301,31.177 -2020-10-03 14:45:00,64.03,76.969,17.301,31.177 -2020-10-03 15:00:00,66.02,76.7,21.236,31.177 -2020-10-03 15:15:00,68.96,76.618,21.236,31.177 -2020-10-03 15:30:00,70.76,76.907,21.236,31.177 -2020-10-03 15:45:00,72.25,77.335,21.236,31.177 -2020-10-03 16:00:00,74.29,77.045,28.31,31.177 -2020-10-03 16:15:00,75.52,76.52199999999999,28.31,31.177 -2020-10-03 16:30:00,78.34,77.89699999999999,28.31,31.177 -2020-10-03 16:45:00,82.8,76.039,28.31,31.177 -2020-10-03 17:00:00,87.35,76.783,41.687,31.177 -2020-10-03 17:15:00,87.12,77.97,41.687,31.177 -2020-10-03 17:30:00,88.97,78.093,41.687,31.177 -2020-10-03 17:45:00,90.28,79.843,41.687,31.177 -2020-10-03 18:00:00,94.76,80.597,49.201,31.177 -2020-10-03 18:15:00,92.25,81.734,49.201,31.177 -2020-10-03 18:30:00,94.32,81.203,49.201,31.177 -2020-10-03 18:45:00,91.48,82.59100000000001,49.201,31.177 -2020-10-03 19:00:00,92.73,84.505,51.937,31.177 -2020-10-03 19:15:00,98.28,83.11,51.937,31.177 -2020-10-03 19:30:00,96.89,82.96,51.937,31.177 -2020-10-03 19:45:00,88.92,83.443,51.937,31.177 -2020-10-03 20:00:00,85.03,84.101,52.617,31.177 -2020-10-03 20:15:00,85.02,83.164,52.617,31.177 -2020-10-03 20:30:00,90.9,82.34700000000001,52.617,31.177 -2020-10-03 20:45:00,91.31,80.497,52.617,31.177 -2020-10-03 21:00:00,85.41,78.417,46.238,31.177 -2020-10-03 21:15:00,84.03,79.649,46.238,31.177 -2020-10-03 21:30:00,86.88,79.069,46.238,31.177 -2020-10-03 21:45:00,86.4,77.541,46.238,31.177 -2020-10-03 22:00:00,81.78,76.17,48.275,31.177 -2020-10-03 22:15:00,78.99,73.67,48.275,31.177 -2020-10-03 22:30:00,80.17,69.319,48.275,31.177 -2020-10-03 22:45:00,79.39,65.98899999999999,48.275,31.177 -2020-10-03 23:00:00,55.75,61.363,38.071999999999996,31.177 -2020-10-03 23:15:00,57.09,60.38399999999999,38.071999999999996,31.177 -2020-10-03 23:30:00,53.87,58.913999999999994,38.071999999999996,31.177 -2020-10-03 23:45:00,56.4,58.196000000000005,38.071999999999996,31.177 -2020-10-04 00:00:00,53.94,48.018,28.229,31.177 -2020-10-04 00:15:00,54.47,46.545,28.229,31.177 -2020-10-04 00:30:00,52.62,46.365,28.229,31.177 -2020-10-04 00:45:00,53.51,46.6,28.229,31.177 -2020-10-04 01:00:00,50.25,47.07899999999999,25.669,31.177 -2020-10-04 01:15:00,52.39,46.812,25.669,31.177 -2020-10-04 01:30:00,51.82,45.841,25.669,31.177 -2020-10-04 01:45:00,51.72,45.548,25.669,31.177 -2020-10-04 02:00:00,51.1,46.023999999999994,24.948,31.177 -2020-10-04 02:15:00,52.15,45.488,24.948,31.177 -2020-10-04 02:30:00,51.41,46.276,24.948,31.177 -2020-10-04 02:45:00,50.99,47.077,24.948,31.177 -2020-10-04 03:00:00,52.2,49.261,24.445,31.177 -2020-10-04 03:15:00,51.81,48.788000000000004,24.445,31.177 -2020-10-04 03:30:00,52.33,48.39,24.445,31.177 -2020-10-04 03:45:00,53.15,50.145,24.445,31.177 -2020-10-04 04:00:00,53.33,56.275,25.839000000000002,31.177 -2020-10-04 04:15:00,53.92,61.833,25.839000000000002,31.177 -2020-10-04 04:30:00,54.43,60.755,25.839000000000002,31.177 -2020-10-04 04:45:00,54.79,61.372,25.839000000000002,31.177 -2020-10-04 05:00:00,57.04,74.122,26.803,31.177 -2020-10-04 05:15:00,56.99,82.527,26.803,31.177 -2020-10-04 05:30:00,58.51,77.84100000000001,26.803,31.177 -2020-10-04 05:45:00,59.57,74.402,26.803,31.177 -2020-10-04 06:00:00,60.65,86.709,28.147,31.177 -2020-10-04 06:15:00,59.54,100.521,28.147,31.177 -2020-10-04 06:30:00,61.12,93.97399999999999,28.147,31.177 -2020-10-04 06:45:00,61.43,88.93,28.147,31.177 -2020-10-04 07:00:00,66.21,87.679,31.116,31.177 -2020-10-04 07:15:00,65.25,88.34700000000001,31.116,31.177 -2020-10-04 07:30:00,64.85,89.059,31.116,31.177 -2020-10-04 07:45:00,65.24,90.551,31.116,31.177 -2020-10-04 08:00:00,63.88,91.851,35.739000000000004,31.177 -2020-10-04 08:15:00,66.44,94.265,35.739000000000004,31.177 -2020-10-04 08:30:00,70.93,94.132,35.739000000000004,31.177 -2020-10-04 08:45:00,67.85,95.15899999999999,35.739000000000004,31.177 -2020-10-04 09:00:00,66.06,91.535,39.455999999999996,31.177 -2020-10-04 09:15:00,67.28,91.199,39.455999999999996,31.177 -2020-10-04 09:30:00,70.83,91.869,39.455999999999996,31.177 -2020-10-04 09:45:00,77.44,92.211,39.455999999999996,31.177 -2020-10-04 10:00:00,78.19,90.01700000000001,41.343999999999994,31.177 -2020-10-04 10:15:00,81.74,90.225,41.343999999999994,31.177 -2020-10-04 10:30:00,83.73,89.74799999999999,41.343999999999994,31.177 -2020-10-04 10:45:00,84.45,89.494,41.343999999999994,31.177 -2020-10-04 11:00:00,83.31,86.509,43.645,31.177 -2020-10-04 11:15:00,81.0,86.15299999999999,43.645,31.177 -2020-10-04 11:30:00,79.77,86.50200000000001,43.645,31.177 -2020-10-04 11:45:00,79.1,86.075,43.645,31.177 -2020-10-04 12:00:00,75.39,83.105,39.796,31.177 -2020-10-04 12:15:00,75.42,82.67200000000001,39.796,31.177 -2020-10-04 12:30:00,74.51,81.604,39.796,31.177 -2020-10-04 12:45:00,74.31,80.998,39.796,31.177 -2020-10-04 13:00:00,73.03,80.295,36.343,31.177 -2020-10-04 13:15:00,72.61,80.479,36.343,31.177 -2020-10-04 13:30:00,73.42,79.259,36.343,31.177 -2020-10-04 13:45:00,73.96,78.757,36.343,31.177 -2020-10-04 14:00:00,72.48,78.671,33.162,31.177 -2020-10-04 14:15:00,72.19,78.99600000000001,33.162,31.177 -2020-10-04 14:30:00,73.52,78.075,33.162,31.177 -2020-10-04 14:45:00,73.96,77.342,33.162,31.177 -2020-10-04 15:00:00,74.74,77.039,33.215,31.177 -2020-10-04 15:15:00,74.08,76.976,33.215,31.177 -2020-10-04 15:30:00,74.78,77.30199999999999,33.215,31.177 -2020-10-04 15:45:00,76.21,77.745,33.215,31.177 -2020-10-04 16:00:00,78.09,77.42,37.385999999999996,31.177 -2020-10-04 16:15:00,79.1,76.916,37.385999999999996,31.177 -2020-10-04 16:30:00,80.57,78.28699999999999,37.385999999999996,31.177 -2020-10-04 16:45:00,82.81,76.484,37.385999999999996,31.177 -2020-10-04 17:00:00,87.51,77.188,46.618,31.177 -2020-10-04 17:15:00,86.86,78.395,46.618,31.177 -2020-10-04 17:30:00,92.1,78.51899999999999,46.618,31.177 -2020-10-04 17:45:00,89.55,80.29,46.618,31.177 -2020-10-04 18:00:00,95.29,81.03,50.111000000000004,31.177 -2020-10-04 18:15:00,88.95,82.15100000000001,50.111000000000004,31.177 -2020-10-04 18:30:00,91.01,81.631,50.111000000000004,31.177 -2020-10-04 18:45:00,87.31,83.012,50.111000000000004,31.177 -2020-10-04 19:00:00,86.3,84.939,50.25,31.177 -2020-10-04 19:15:00,84.42,83.538,50.25,31.177 -2020-10-04 19:30:00,83.14,83.38,50.25,31.177 -2020-10-04 19:45:00,81.46,83.845,50.25,31.177 -2020-10-04 20:00:00,77.95,84.52600000000001,44.265,31.177 -2020-10-04 20:15:00,79.6,83.583,44.265,31.177 -2020-10-04 20:30:00,77.92,82.73899999999999,44.265,31.177 -2020-10-04 20:45:00,78.85,80.859,44.265,31.177 -2020-10-04 21:00:00,77.61,78.781,39.717,31.177 -2020-10-04 21:15:00,77.95,80.00399999999999,39.717,31.177 -2020-10-04 21:30:00,75.4,79.433,39.717,31.177 -2020-10-04 21:45:00,75.62,77.875,39.717,31.177 -2020-10-04 22:00:00,72.93,76.492,39.224000000000004,31.177 -2020-10-04 22:15:00,72.3,73.969,39.224000000000004,31.177 -2020-10-04 22:30:00,69.94,69.63,39.224000000000004,31.177 -2020-10-04 22:45:00,69.71,66.307,39.224000000000004,31.177 -2020-10-04 23:00:00,65.21,61.708999999999996,33.518,31.177 -2020-10-04 23:15:00,66.43,60.7,33.518,31.177 -2020-10-04 23:30:00,65.32,59.232,33.518,31.177 -2020-10-04 23:45:00,65.1,58.507,33.518,31.177 -2020-10-05 00:00:00,66.33,50.821999999999996,34.301,31.349 -2020-10-05 00:15:00,64.68,50.945,34.301,31.349 -2020-10-05 00:30:00,63.44,50.6,34.301,31.349 -2020-10-05 00:45:00,65.02,50.398,34.301,31.349 -2020-10-05 01:00:00,64.78,51.103,34.143,31.349 -2020-10-05 01:15:00,64.46,50.661,34.143,31.349 -2020-10-05 01:30:00,64.07,49.93899999999999,34.143,31.349 -2020-10-05 01:45:00,64.5,49.631,34.143,31.349 -2020-10-05 02:00:00,63.51,50.364,33.650999999999996,31.349 -2020-10-05 02:15:00,65.03,49.815,33.650999999999996,31.349 -2020-10-05 02:30:00,65.36,50.809,33.650999999999996,31.349 -2020-10-05 02:45:00,66.21,51.282,33.650999999999996,31.349 -2020-10-05 03:00:00,69.64,54.224,32.599000000000004,31.349 -2020-10-05 03:15:00,74.23,54.818999999999996,32.599000000000004,31.349 -2020-10-05 03:30:00,75.31,54.718,32.599000000000004,31.349 -2020-10-05 03:45:00,73.63,55.974,32.599000000000004,31.349 -2020-10-05 04:00:00,76.43,65.589,33.785,31.349 -2020-10-05 04:15:00,77.8,74.464,33.785,31.349 -2020-10-05 04:30:00,89.37,73.941,33.785,31.349 -2020-10-05 04:45:00,95.43,74.844,33.785,31.349 -2020-10-05 05:00:00,102.6,97.568,41.285,31.349 -2020-10-05 05:15:00,101.26,119.60700000000001,41.285,31.349 -2020-10-05 05:30:00,105.1,114.088,41.285,31.349 -2020-10-05 05:45:00,112.77,106.70100000000001,41.285,31.349 -2020-10-05 06:00:00,116.26,106.459,60.486000000000004,31.349 -2020-10-05 06:15:00,116.2,109.37,60.486000000000004,31.349 -2020-10-05 06:30:00,117.92,108.182,60.486000000000004,31.349 -2020-10-05 06:45:00,120.49,109.552,60.486000000000004,31.349 -2020-10-05 07:00:00,124.95,109.79,74.012,31.349 -2020-10-05 07:15:00,123.49,112.266,74.012,31.349 -2020-10-05 07:30:00,122.31,112.215,74.012,31.349 -2020-10-05 07:45:00,122.8,112.87799999999999,74.012,31.349 -2020-10-05 08:00:00,122.15,111.291,69.569,31.349 -2020-10-05 08:15:00,123.81,112.141,69.569,31.349 -2020-10-05 08:30:00,125.38,109.84100000000001,69.569,31.349 -2020-10-05 08:45:00,126.19,109.616,69.569,31.349 -2020-10-05 09:00:00,123.98,105.119,66.152,31.349 -2020-10-05 09:15:00,123.63,102.352,66.152,31.349 -2020-10-05 09:30:00,123.4,102.083,66.152,31.349 -2020-10-05 09:45:00,125.12,101.262,66.152,31.349 -2020-10-05 10:00:00,124.46,99.031,62.923,31.349 -2020-10-05 10:15:00,127.71,98.98,62.923,31.349 -2020-10-05 10:30:00,127.77,97.86399999999999,62.923,31.349 -2020-10-05 10:45:00,125.66,97.073,62.923,31.349 -2020-10-05 11:00:00,120.69,93.165,61.522,31.349 -2020-10-05 11:15:00,117.37,93.73,61.522,31.349 -2020-10-05 11:30:00,112.63,95.04799999999999,61.522,31.349 -2020-10-05 11:45:00,108.66,94.689,61.522,31.349 -2020-10-05 12:00:00,102.98,91.74600000000001,58.632,31.349 -2020-10-05 12:15:00,103.41,91.36,58.632,31.349 -2020-10-05 12:30:00,100.84,89.89299999999999,58.632,31.349 -2020-10-05 12:45:00,101.8,89.97,58.632,31.349 -2020-10-05 13:00:00,100.62,89.954,59.06,31.349 -2020-10-05 13:15:00,100.28,89.065,59.06,31.349 -2020-10-05 13:30:00,100.19,87.693,59.06,31.349 -2020-10-05 13:45:00,100.01,87.664,59.06,31.349 -2020-10-05 14:00:00,100.8,86.809,59.791000000000004,31.349 -2020-10-05 14:15:00,101.96,87.161,59.791000000000004,31.349 -2020-10-05 14:30:00,103.14,85.947,59.791000000000004,31.349 -2020-10-05 14:45:00,104.3,86.251,59.791000000000004,31.349 -2020-10-05 15:00:00,107.02,86.544,61.148,31.349 -2020-10-05 15:15:00,104.62,85.551,61.148,31.349 -2020-10-05 15:30:00,105.02,85.93,61.148,31.349 -2020-10-05 15:45:00,104.28,85.939,61.148,31.349 -2020-10-05 16:00:00,105.06,86.02600000000001,66.009,31.349 -2020-10-05 16:15:00,106.92,85.244,66.009,31.349 -2020-10-05 16:30:00,109.95,85.84,66.009,31.349 -2020-10-05 16:45:00,111.32,83.586,66.009,31.349 -2020-10-05 17:00:00,116.22,83.50299999999999,73.683,31.349 -2020-10-05 17:15:00,114.24,84.52600000000001,73.683,31.349 -2020-10-05 17:30:00,117.49,84.211,73.683,31.349 -2020-10-05 17:45:00,122.2,85.152,73.683,31.349 -2020-10-05 18:00:00,125.09,85.385,72.848,31.349 -2020-10-05 18:15:00,121.39,84.57,72.848,31.349 -2020-10-05 18:30:00,120.7,83.915,72.848,31.349 -2020-10-05 18:45:00,118.89,87.323,72.848,31.349 -2020-10-05 19:00:00,119.68,88.385,71.139,31.349 -2020-10-05 19:15:00,117.11,87.249,71.139,31.349 -2020-10-05 19:30:00,112.56,87.075,71.139,31.349 -2020-10-05 19:45:00,108.13,86.844,71.139,31.349 -2020-10-05 20:00:00,101.98,85.89200000000001,69.667,31.349 -2020-10-05 20:15:00,101.73,84.719,69.667,31.349 -2020-10-05 20:30:00,97.75,83.385,69.667,31.349 -2020-10-05 20:45:00,97.25,82.257,69.667,31.349 -2020-10-05 21:00:00,93.19,80.05,61.166000000000004,31.349 -2020-10-05 21:15:00,97.6,81.041,61.166000000000004,31.349 -2020-10-05 21:30:00,94.2,80.37899999999999,61.166000000000004,31.349 -2020-10-05 21:45:00,93.44,78.468,61.166000000000004,31.349 -2020-10-05 22:00:00,85.58,74.764,52.772,31.349 -2020-10-05 22:15:00,82.71,72.807,52.772,31.349 -2020-10-05 22:30:00,81.05,62.21,52.772,31.349 -2020-10-05 22:45:00,84.7,56.94,52.772,31.349 -2020-10-05 23:00:00,82.54,52.706,45.136,31.349 -2020-10-05 23:15:00,82.36,51.802,45.136,31.349 -2020-10-05 23:30:00,74.67,51.42,45.136,31.349 -2020-10-05 23:45:00,79.64,51.446999999999996,45.136,31.349 -2020-10-06 00:00:00,78.11,49.55,47.35,31.349 -2020-10-06 00:15:00,81.09,50.716,47.35,31.349 -2020-10-06 00:30:00,75.23,50.428000000000004,47.35,31.349 -2020-10-06 00:45:00,74.03,50.303999999999995,47.35,31.349 -2020-10-06 01:00:00,70.95,50.623999999999995,43.424,31.349 -2020-10-06 01:15:00,79.65,50.096000000000004,43.424,31.349 -2020-10-06 01:30:00,79.59,49.382,43.424,31.349 -2020-10-06 01:45:00,78.21,48.891999999999996,43.424,31.349 -2020-10-06 02:00:00,72.09,49.352,41.778999999999996,31.349 -2020-10-06 02:15:00,78.09,49.449,41.778999999999996,31.349 -2020-10-06 02:30:00,79.6,49.968999999999994,41.778999999999996,31.349 -2020-10-06 02:45:00,79.98,50.633,41.778999999999996,31.349 -2020-10-06 03:00:00,79.25,52.776,40.771,31.349 -2020-10-06 03:15:00,82.62,53.61,40.771,31.349 -2020-10-06 03:30:00,83.6,53.68,40.771,31.349 -2020-10-06 03:45:00,83.88,54.345,40.771,31.349 -2020-10-06 04:00:00,83.35,63.176,41.816,31.349 -2020-10-06 04:15:00,89.08,71.957,41.816,31.349 -2020-10-06 04:30:00,94.83,71.24600000000001,41.816,31.349 -2020-10-06 04:45:00,100.98,72.905,41.816,31.349 -2020-10-06 05:00:00,99.83,98.514,45.842,31.349 -2020-10-06 05:15:00,101.8,120.931,45.842,31.349 -2020-10-06 05:30:00,108.7,114.943,45.842,31.349 -2020-10-06 05:45:00,110.57,107.079,45.842,31.349 -2020-10-06 06:00:00,117.91,106.995,59.12,31.349 -2020-10-06 06:15:00,116.24,110.652,59.12,31.349 -2020-10-06 06:30:00,118.48,109.045,59.12,31.349 -2020-10-06 06:45:00,119.02,109.751,59.12,31.349 -2020-10-06 07:00:00,123.59,110.00200000000001,70.33,31.349 -2020-10-06 07:15:00,121.19,112.28299999999999,70.33,31.349 -2020-10-06 07:30:00,117.92,112.06200000000001,70.33,31.349 -2020-10-06 07:45:00,117.6,112.25200000000001,70.33,31.349 -2020-10-06 08:00:00,114.16,110.685,67.788,31.349 -2020-10-06 08:15:00,114.34,110.84,67.788,31.349 -2020-10-06 08:30:00,118.79,108.587,67.788,31.349 -2020-10-06 08:45:00,121.22,107.713,67.788,31.349 -2020-10-06 09:00:00,113.61,103.059,62.622,31.349 -2020-10-06 09:15:00,114.17,100.876,62.622,31.349 -2020-10-06 09:30:00,117.63,101.25,62.622,31.349 -2020-10-06 09:45:00,116.96,101.04799999999999,62.622,31.349 -2020-10-06 10:00:00,111.74,97.815,60.887,31.349 -2020-10-06 10:15:00,115.0,97.189,60.887,31.349 -2020-10-06 10:30:00,113.09,96.147,60.887,31.349 -2020-10-06 10:45:00,113.9,95.99799999999999,60.887,31.349 -2020-10-06 11:00:00,115.53,92.74600000000001,59.812,31.349 -2020-10-06 11:15:00,116.95,93.359,59.812,31.349 -2020-10-06 11:30:00,115.24,93.559,59.812,31.349 -2020-10-06 11:45:00,109.97,93.256,59.812,31.349 -2020-10-06 12:00:00,105.42,89.58,56.614,31.349 -2020-10-06 12:15:00,102.97,89.16799999999999,56.614,31.349 -2020-10-06 12:30:00,103.1,88.50299999999999,56.614,31.349 -2020-10-06 12:45:00,106.67,88.796,56.614,31.349 -2020-10-06 13:00:00,103.21,88.37899999999999,56.824,31.349 -2020-10-06 13:15:00,101.73,88.26,56.824,31.349 -2020-10-06 13:30:00,102.74,87.40100000000001,56.824,31.349 -2020-10-06 13:45:00,104.36,86.963,56.824,31.349 -2020-10-06 14:00:00,104.47,86.411,57.623999999999995,31.349 -2020-10-06 14:15:00,105.34,86.749,57.623999999999995,31.349 -2020-10-06 14:30:00,104.12,86.022,57.623999999999995,31.349 -2020-10-06 14:45:00,104.25,85.884,57.623999999999995,31.349 -2020-10-06 15:00:00,104.24,85.866,59.724,31.349 -2020-10-06 15:15:00,105.47,85.515,59.724,31.349 -2020-10-06 15:30:00,105.21,85.925,59.724,31.349 -2020-10-06 15:45:00,107.87,85.944,59.724,31.349 -2020-10-06 16:00:00,110.19,85.816,61.64,31.349 -2020-10-06 16:15:00,113.18,85.307,61.64,31.349 -2020-10-06 16:30:00,112.84,85.98299999999999,61.64,31.349 -2020-10-06 16:45:00,115.02,84.288,61.64,31.349 -2020-10-06 17:00:00,119.5,84.516,68.962,31.349 -2020-10-06 17:15:00,116.33,85.795,68.962,31.349 -2020-10-06 17:30:00,121.99,85.531,68.962,31.349 -2020-10-06 17:45:00,123.39,86.258,68.962,31.349 -2020-10-06 18:00:00,127.05,85.90100000000001,69.149,31.349 -2020-10-06 18:15:00,122.1,85.682,69.149,31.349 -2020-10-06 18:30:00,121.85,84.766,69.149,31.349 -2020-10-06 18:45:00,120.37,88.39,69.149,31.349 -2020-10-06 19:00:00,118.45,88.83200000000001,68.832,31.349 -2020-10-06 19:15:00,118.71,87.679,68.832,31.349 -2020-10-06 19:30:00,115.61,87.10600000000001,68.832,31.349 -2020-10-06 19:45:00,112.12,87.06,68.832,31.349 -2020-10-06 20:00:00,100.95,86.41799999999999,66.403,31.349 -2020-10-06 20:15:00,100.94,84.17299999999999,66.403,31.349 -2020-10-06 20:30:00,98.55,83.243,66.403,31.349 -2020-10-06 20:45:00,98.12,82.089,66.403,31.349 -2020-10-06 21:00:00,91.59,80.08800000000001,57.352,31.349 -2020-10-06 21:15:00,90.82,80.605,57.352,31.349 -2020-10-06 21:30:00,87.87,79.758,57.352,31.349 -2020-10-06 21:45:00,90.21,78.021,57.352,31.349 -2020-10-06 22:00:00,90.01,75.087,51.148999999999994,31.349 -2020-10-06 22:15:00,89.32,72.814,51.148999999999994,31.349 -2020-10-06 22:30:00,85.07,62.431999999999995,51.148999999999994,31.349 -2020-10-06 22:45:00,83.99,57.276,51.148999999999994,31.349 -2020-10-06 23:00:00,78.1,52.643,41.8,31.349 -2020-10-06 23:15:00,82.01,52.229,41.8,31.349 -2020-10-06 23:30:00,84.4,51.70399999999999,41.8,31.349 -2020-10-06 23:45:00,87.64,51.651,41.8,31.349 -2020-10-07 00:00:00,80.25,49.86,42.269,31.349 -2020-10-07 00:15:00,80.19,51.019,42.269,31.349 -2020-10-07 00:30:00,81.39,50.736999999999995,42.269,31.349 -2020-10-07 00:45:00,86.62,50.611999999999995,42.269,31.349 -2020-10-07 01:00:00,82.23,50.937,38.527,31.349 -2020-10-07 01:15:00,79.29,50.428999999999995,38.527,31.349 -2020-10-07 01:30:00,81.05,49.733000000000004,38.527,31.349 -2020-10-07 01:45:00,83.23,49.24100000000001,38.527,31.349 -2020-10-07 02:00:00,83.52,49.708,36.393,31.349 -2020-10-07 02:15:00,82.09,49.821000000000005,36.393,31.349 -2020-10-07 02:30:00,79.19,50.321999999999996,36.393,31.349 -2020-10-07 02:45:00,79.81,50.982,36.393,31.349 -2020-10-07 03:00:00,86.92,53.113,36.167,31.349 -2020-10-07 03:15:00,87.2,53.966,36.167,31.349 -2020-10-07 03:30:00,88.26,54.038999999999994,36.167,31.349 -2020-10-07 03:45:00,86.31,54.68600000000001,36.167,31.349 -2020-10-07 04:00:00,94.04,63.54600000000001,38.092,31.349 -2020-10-07 04:15:00,97.41,72.358,38.092,31.349 -2020-10-07 04:30:00,98.81,71.648,38.092,31.349 -2020-10-07 04:45:00,98.37,73.316,38.092,31.349 -2020-10-07 05:00:00,104.2,99.01,42.268,31.349 -2020-10-07 05:15:00,110.49,121.51100000000001,42.268,31.349 -2020-10-07 05:30:00,112.99,115.508,42.268,31.349 -2020-10-07 05:45:00,120.34,107.602,42.268,31.349 -2020-10-07 06:00:00,123.12,107.494,60.158,31.349 -2020-10-07 06:15:00,115.11,111.17,60.158,31.349 -2020-10-07 06:30:00,121.4,109.574,60.158,31.349 -2020-10-07 06:45:00,121.67,110.28299999999999,60.158,31.349 -2020-10-07 07:00:00,123.41,110.53299999999999,74.792,31.349 -2020-10-07 07:15:00,121.71,112.82700000000001,74.792,31.349 -2020-10-07 07:30:00,121.33,112.64,74.792,31.349 -2020-10-07 07:45:00,119.53,112.833,74.792,31.349 -2020-10-07 08:00:00,115.82,111.277,70.499,31.349 -2020-10-07 08:15:00,116.66,111.404,70.499,31.349 -2020-10-07 08:30:00,116.5,109.17399999999999,70.499,31.349 -2020-10-07 08:45:00,116.09,108.279,70.499,31.349 -2020-10-07 09:00:00,113.61,103.625,68.892,31.349 -2020-10-07 09:15:00,113.03,101.435,68.892,31.349 -2020-10-07 09:30:00,112.32,101.794,68.892,31.349 -2020-10-07 09:45:00,113.24,101.564,68.892,31.349 -2020-10-07 10:00:00,113.52,98.324,66.88600000000001,31.349 -2020-10-07 10:15:00,116.01,97.65799999999999,66.88600000000001,31.349 -2020-10-07 10:30:00,112.9,96.59899999999999,66.88600000000001,31.349 -2020-10-07 10:45:00,113.01,96.432,66.88600000000001,31.349 -2020-10-07 11:00:00,110.74,93.19,66.187,31.349 -2020-10-07 11:15:00,107.17,93.78399999999999,66.187,31.349 -2020-10-07 11:30:00,106.12,93.98299999999999,66.187,31.349 -2020-10-07 11:45:00,106.28,93.664,66.187,31.349 -2020-10-07 12:00:00,103.85,89.963,62.18,31.349 -2020-10-07 12:15:00,104.47,89.54299999999999,62.18,31.349 -2020-10-07 12:30:00,103.29,88.913,62.18,31.349 -2020-10-07 12:45:00,104.27,89.20200000000001,62.18,31.349 -2020-10-07 13:00:00,103.01,88.75399999999999,62.23,31.349 -2020-10-07 13:15:00,102.19,88.63799999999999,62.23,31.349 -2020-10-07 13:30:00,102.91,87.77799999999999,62.23,31.349 -2020-10-07 13:45:00,103.83,87.34,62.23,31.349 -2020-10-07 14:00:00,104.04,86.73700000000001,63.721000000000004,31.349 -2020-10-07 14:15:00,103.67,87.09,63.721000000000004,31.349 -2020-10-07 14:30:00,102.78,86.399,63.721000000000004,31.349 -2020-10-07 14:45:00,102.93,86.258,63.721000000000004,31.349 -2020-10-07 15:00:00,104.88,86.205,66.523,31.349 -2020-10-07 15:15:00,108.17,85.874,66.523,31.349 -2020-10-07 15:30:00,106.16,86.321,66.523,31.349 -2020-10-07 15:45:00,106.9,86.355,66.523,31.349 -2020-10-07 16:00:00,110.44,86.19200000000001,69.679,31.349 -2020-10-07 16:15:00,110.4,85.70200000000001,69.679,31.349 -2020-10-07 16:30:00,112.36,86.374,69.679,31.349 -2020-10-07 16:45:00,114.77,84.734,69.679,31.349 -2020-10-07 17:00:00,117.12,84.92200000000001,75.04,31.349 -2020-10-07 17:15:00,116.68,86.22,75.04,31.349 -2020-10-07 17:30:00,121.96,85.959,75.04,31.349 -2020-10-07 17:45:00,123.54,86.70700000000001,75.04,31.349 -2020-10-07 18:00:00,125.9,86.336,75.915,31.349 -2020-10-07 18:15:00,122.45,86.101,75.915,31.349 -2020-10-07 18:30:00,122.89,85.197,75.915,31.349 -2020-10-07 18:45:00,119.42,88.81299999999999,75.915,31.349 -2020-10-07 19:00:00,114.78,89.26799999999999,74.66,31.349 -2020-10-07 19:15:00,112.06,88.11,74.66,31.349 -2020-10-07 19:30:00,107.91,87.527,74.66,31.349 -2020-10-07 19:45:00,108.91,87.464,74.66,31.349 -2020-10-07 20:00:00,101.7,86.845,71.204,31.349 -2020-10-07 20:15:00,104.07,84.595,71.204,31.349 -2020-10-07 20:30:00,101.57,83.637,71.204,31.349 -2020-10-07 20:45:00,99.17,82.454,71.204,31.349 -2020-10-07 21:00:00,94.53,80.454,61.052,31.349 -2020-10-07 21:15:00,94.88,80.96,61.052,31.349 -2020-10-07 21:30:00,96.49,80.123,61.052,31.349 -2020-10-07 21:45:00,95.82,78.357,61.052,31.349 -2020-10-07 22:00:00,91.81,75.411,54.691,31.349 -2020-10-07 22:15:00,85.9,73.115,54.691,31.349 -2020-10-07 22:30:00,83.52,62.748000000000005,54.691,31.349 -2020-10-07 22:45:00,79.93,57.597,54.691,31.349 -2020-10-07 23:00:00,76.11,52.993,45.18,31.349 -2020-10-07 23:15:00,76.08,52.548,45.18,31.349 -2020-10-07 23:30:00,75.38,52.023999999999994,45.18,31.349 -2020-10-07 23:45:00,79.99,51.966,45.18,31.349 -2020-10-08 00:00:00,79.69,50.172,42.746,31.349 -2020-10-08 00:15:00,79.41,51.321000000000005,42.746,31.349 -2020-10-08 00:30:00,77.72,51.048,42.746,31.349 -2020-10-08 00:45:00,77.1,50.92,42.746,31.349 -2020-10-08 01:00:00,77.16,51.248999999999995,40.025999999999996,31.349 -2020-10-08 01:15:00,79.65,50.761,40.025999999999996,31.349 -2020-10-08 01:30:00,78.99,50.083999999999996,40.025999999999996,31.349 -2020-10-08 01:45:00,73.79,49.59,40.025999999999996,31.349 -2020-10-08 02:00:00,79.29,50.065,38.154,31.349 -2020-10-08 02:15:00,80.26,50.193999999999996,38.154,31.349 -2020-10-08 02:30:00,78.95,50.676,38.154,31.349 -2020-10-08 02:45:00,76.08,51.33,38.154,31.349 -2020-10-08 03:00:00,75.33,53.449,37.575,31.349 -2020-10-08 03:15:00,82.42,54.321999999999996,37.575,31.349 -2020-10-08 03:30:00,84.01,54.4,37.575,31.349 -2020-10-08 03:45:00,86.07,55.028,37.575,31.349 -2020-10-08 04:00:00,84.0,63.916000000000004,39.154,31.349 -2020-10-08 04:15:00,90.23,72.76100000000001,39.154,31.349 -2020-10-08 04:30:00,94.69,72.051,39.154,31.349 -2020-10-08 04:45:00,97.5,73.727,39.154,31.349 -2020-10-08 05:00:00,97.92,99.507,44.085,31.349 -2020-10-08 05:15:00,101.71,122.09299999999999,44.085,31.349 -2020-10-08 05:30:00,107.92,116.074,44.085,31.349 -2020-10-08 05:45:00,111.57,108.126,44.085,31.349 -2020-10-08 06:00:00,116.78,107.995,57.49,31.349 -2020-10-08 06:15:00,114.86,111.68700000000001,57.49,31.349 -2020-10-08 06:30:00,114.18,110.105,57.49,31.349 -2020-10-08 06:45:00,118.63,110.81700000000001,57.49,31.349 -2020-10-08 07:00:00,120.92,111.065,73.617,31.349 -2020-10-08 07:15:00,119.12,113.374,73.617,31.349 -2020-10-08 07:30:00,117.33,113.219,73.617,31.349 -2020-10-08 07:45:00,116.58,113.415,73.617,31.349 -2020-10-08 08:00:00,115.2,111.869,69.281,31.349 -2020-10-08 08:15:00,114.49,111.969,69.281,31.349 -2020-10-08 08:30:00,113.74,109.76299999999999,69.281,31.349 -2020-10-08 08:45:00,113.82,108.845,69.281,31.349 -2020-10-08 09:00:00,111.25,104.189,63.926,31.349 -2020-10-08 09:15:00,110.51,101.995,63.926,31.349 -2020-10-08 09:30:00,110.18,102.336,63.926,31.349 -2020-10-08 09:45:00,110.35,102.079,63.926,31.349 -2020-10-08 10:00:00,108.45,98.833,59.442,31.349 -2020-10-08 10:15:00,109.67,98.12799999999999,59.442,31.349 -2020-10-08 10:30:00,108.62,97.05,59.442,31.349 -2020-10-08 10:45:00,107.19,96.867,59.442,31.349 -2020-10-08 11:00:00,105.31,93.634,56.771,31.349 -2020-10-08 11:15:00,106.63,94.209,56.771,31.349 -2020-10-08 11:30:00,106.21,94.40700000000001,56.771,31.349 -2020-10-08 11:45:00,108.79,94.072,56.771,31.349 -2020-10-08 12:00:00,101.41,90.34700000000001,53.701,31.349 -2020-10-08 12:15:00,101.81,89.917,53.701,31.349 -2020-10-08 12:30:00,99.91,89.323,53.701,31.349 -2020-10-08 12:45:00,100.67,89.609,53.701,31.349 -2020-10-08 13:00:00,99.91,89.12799999999999,52.364,31.349 -2020-10-08 13:15:00,100.85,89.016,52.364,31.349 -2020-10-08 13:30:00,99.77,88.156,52.364,31.349 -2020-10-08 13:45:00,102.06,87.71799999999999,52.364,31.349 -2020-10-08 14:00:00,101.66,87.06200000000001,53.419,31.349 -2020-10-08 14:15:00,102.04,87.431,53.419,31.349 -2020-10-08 14:30:00,101.37,86.77600000000001,53.419,31.349 -2020-10-08 14:45:00,102.03,86.632,53.419,31.349 -2020-10-08 15:00:00,102.38,86.545,56.744,31.349 -2020-10-08 15:15:00,106.15,86.235,56.744,31.349 -2020-10-08 15:30:00,105.14,86.71700000000001,56.744,31.349 -2020-10-08 15:45:00,106.58,86.766,56.744,31.349 -2020-10-08 16:00:00,108.41,86.568,60.458,31.349 -2020-10-08 16:15:00,109.23,86.09700000000001,60.458,31.349 -2020-10-08 16:30:00,111.33,86.765,60.458,31.349 -2020-10-08 16:45:00,112.69,85.179,60.458,31.349 -2020-10-08 17:00:00,117.22,85.32600000000001,66.295,31.349 -2020-10-08 17:15:00,114.98,86.646,66.295,31.349 -2020-10-08 17:30:00,121.72,86.387,66.295,31.349 -2020-10-08 17:45:00,122.79,87.156,66.295,31.349 -2020-10-08 18:00:00,124.63,86.772,68.468,31.349 -2020-10-08 18:15:00,124.71,86.521,68.468,31.349 -2020-10-08 18:30:00,121.3,85.62799999999999,68.468,31.349 -2020-10-08 18:45:00,120.86,89.238,68.468,31.349 -2020-10-08 19:00:00,121.84,89.704,66.39399999999999,31.349 -2020-10-08 19:15:00,118.99,88.54,66.39399999999999,31.349 -2020-10-08 19:30:00,110.25,87.949,66.39399999999999,31.349 -2020-10-08 19:45:00,112.82,87.868,66.39399999999999,31.349 -2020-10-08 20:00:00,104.39,87.273,63.183,31.349 -2020-10-08 20:15:00,104.11,85.01700000000001,63.183,31.349 -2020-10-08 20:30:00,96.44,84.031,63.183,31.349 -2020-10-08 20:45:00,97.74,82.82,63.183,31.349 -2020-10-08 21:00:00,95.47,80.82,55.133,31.349 -2020-10-08 21:15:00,94.29,81.316,55.133,31.349 -2020-10-08 21:30:00,88.11,80.48899999999999,55.133,31.349 -2020-10-08 21:45:00,94.31,78.695,55.133,31.349 -2020-10-08 22:00:00,86.54,75.735,50.111999999999995,31.349 -2020-10-08 22:15:00,84.64,73.417,50.111999999999995,31.349 -2020-10-08 22:30:00,84.73,63.065,50.111999999999995,31.349 -2020-10-08 22:45:00,80.12,57.919,50.111999999999995,31.349 -2020-10-08 23:00:00,82.03,53.343,44.536,31.349 -2020-10-08 23:15:00,83.35,52.867,44.536,31.349 -2020-10-08 23:30:00,82.16,52.345,44.536,31.349 -2020-10-08 23:45:00,78.69,52.281000000000006,44.536,31.349 -2020-10-09 00:00:00,78.1,49.02,42.291000000000004,31.349 -2020-10-09 00:15:00,79.85,50.364,42.291000000000004,31.349 -2020-10-09 00:30:00,79.77,50.202,42.291000000000004,31.349 -2020-10-09 00:45:00,76.69,50.361999999999995,42.291000000000004,31.349 -2020-10-09 01:00:00,73.43,50.349,41.008,31.349 -2020-10-09 01:15:00,80.07,49.895,41.008,31.349 -2020-10-09 01:30:00,78.35,49.56100000000001,41.008,31.349 -2020-10-09 01:45:00,75.18,48.961999999999996,41.008,31.349 -2020-10-09 02:00:00,76.68,50.022,39.521,31.349 -2020-10-09 02:15:00,74.18,50.09,39.521,31.349 -2020-10-09 02:30:00,79.65,51.242,39.521,31.349 -2020-10-09 02:45:00,79.83,51.511,39.521,31.349 -2020-10-09 03:00:00,80.63,53.677,39.812,31.349 -2020-10-09 03:15:00,76.92,54.235,39.812,31.349 -2020-10-09 03:30:00,78.86,54.17,39.812,31.349 -2020-10-09 03:45:00,81.09,55.42,39.812,31.349 -2020-10-09 04:00:00,87.69,64.506,41.22,31.349 -2020-10-09 04:15:00,90.33,72.376,41.22,31.349 -2020-10-09 04:30:00,89.24,72.312,41.22,31.349 -2020-10-09 04:45:00,95.09,73.171,41.22,31.349 -2020-10-09 05:00:00,107.98,98.257,45.115,31.349 -2020-10-09 05:15:00,110.72,122.072,45.115,31.349 -2020-10-09 05:30:00,111.56,116.56299999999999,45.115,31.349 -2020-10-09 05:45:00,111.91,108.28200000000001,45.115,31.349 -2020-10-09 06:00:00,119.82,108.445,59.06100000000001,31.349 -2020-10-09 06:15:00,118.17,111.68,59.06100000000001,31.349 -2020-10-09 06:30:00,118.58,109.745,59.06100000000001,31.349 -2020-10-09 06:45:00,120.01,111.014,59.06100000000001,31.349 -2020-10-09 07:00:00,122.14,111.315,71.874,31.349 -2020-10-09 07:15:00,120.54,114.571,71.874,31.349 -2020-10-09 07:30:00,118.81,113.26100000000001,71.874,31.349 -2020-10-09 07:45:00,118.12,112.98,71.874,31.349 -2020-10-09 08:00:00,116.07,111.43299999999999,68.439,31.349 -2020-10-09 08:15:00,113.32,111.71600000000001,68.439,31.349 -2020-10-09 08:30:00,113.35,109.887,68.439,31.349 -2020-10-09 08:45:00,116.44,108.208,68.439,31.349 -2020-10-09 09:00:00,120.09,102.461,65.523,31.349 -2020-10-09 09:15:00,119.56,101.556,65.523,31.349 -2020-10-09 09:30:00,119.2,101.32,65.523,31.349 -2020-10-09 09:45:00,120.05,101.2,65.523,31.349 -2020-10-09 10:00:00,118.05,97.335,62.005,31.349 -2020-10-09 10:15:00,120.4,96.78399999999999,62.005,31.349 -2020-10-09 10:30:00,117.17,95.949,62.005,31.349 -2020-10-09 10:45:00,113.01,95.501,62.005,31.349 -2020-10-09 11:00:00,109.41,92.38600000000001,60.351000000000006,31.349 -2020-10-09 11:15:00,112.78,91.969,60.351000000000006,31.349 -2020-10-09 11:30:00,116.56,92.76799999999999,60.351000000000006,31.349 -2020-10-09 11:45:00,115.51,91.935,60.351000000000006,31.349 -2020-10-09 12:00:00,112.96,88.92399999999999,55.331,31.349 -2020-10-09 12:15:00,112.0,87.18700000000001,55.331,31.349 -2020-10-09 12:30:00,109.13,86.762,55.331,31.349 -2020-10-09 12:45:00,106.59,86.881,55.331,31.349 -2020-10-09 13:00:00,98.34,87.105,53.361999999999995,31.349 -2020-10-09 13:15:00,98.84,87.469,53.361999999999995,31.349 -2020-10-09 13:30:00,99.07,87.02,53.361999999999995,31.349 -2020-10-09 13:45:00,99.93,86.71,53.361999999999995,31.349 -2020-10-09 14:00:00,99.24,85.06299999999999,51.708,31.349 -2020-10-09 14:15:00,99.81,85.588,51.708,31.349 -2020-10-09 14:30:00,100.03,85.976,51.708,31.349 -2020-10-09 14:45:00,101.58,85.617,51.708,31.349 -2020-10-09 15:00:00,101.74,85.23100000000001,54.571000000000005,31.349 -2020-10-09 15:15:00,104.75,84.615,54.571000000000005,31.349 -2020-10-09 15:30:00,101.76,84.139,54.571000000000005,31.349 -2020-10-09 15:45:00,104.43,84.66,54.571000000000005,31.349 -2020-10-09 16:00:00,106.95,83.49,58.662,31.349 -2020-10-09 16:15:00,108.48,83.429,58.662,31.349 -2020-10-09 16:30:00,109.76,84.04,58.662,31.349 -2020-10-09 16:45:00,111.36,82.041,58.662,31.349 -2020-10-09 17:00:00,113.98,83.177,65.941,31.349 -2020-10-09 17:15:00,116.0,84.243,65.941,31.349 -2020-10-09 17:30:00,116.08,83.93700000000001,65.941,31.349 -2020-10-09 17:45:00,120.05,84.536,65.941,31.349 -2020-10-09 18:00:00,122.43,84.505,65.628,31.349 -2020-10-09 18:15:00,118.95,83.552,65.628,31.349 -2020-10-09 18:30:00,120.14,82.779,65.628,31.349 -2020-10-09 18:45:00,116.46,86.61200000000001,65.628,31.349 -2020-10-09 19:00:00,110.08,87.984,63.662,31.349 -2020-10-09 19:15:00,107.3,87.711,63.662,31.349 -2020-10-09 19:30:00,102.67,86.975,63.662,31.349 -2020-10-09 19:45:00,102.52,86.115,63.662,31.349 -2020-10-09 20:00:00,94.57,85.47399999999999,61.945,31.349 -2020-10-09 20:15:00,92.41,83.66,61.945,31.349 -2020-10-09 20:30:00,93.56,82.355,61.945,31.349 -2020-10-09 20:45:00,88.21,80.892,61.945,31.349 -2020-10-09 21:00:00,83.31,79.843,53.903,31.349 -2020-10-09 21:15:00,83.13,81.453,53.903,31.349 -2020-10-09 21:30:00,83.95,80.569,53.903,31.349 -2020-10-09 21:45:00,86.18,79.085,53.903,31.349 -2020-10-09 22:00:00,80.94,76.436,48.403999999999996,31.349 -2020-10-09 22:15:00,79.92,73.9,48.403999999999996,31.349 -2020-10-09 22:30:00,73.84,68.861,48.403999999999996,31.349 -2020-10-09 22:45:00,69.3,65.65100000000001,48.403999999999996,31.349 -2020-10-09 23:00:00,66.03,61.873999999999995,41.07,31.349 -2020-10-09 23:15:00,66.63,59.684,41.07,31.349 -2020-10-09 23:30:00,71.33,57.535,41.07,31.349 -2020-10-09 23:45:00,72.04,57.123999999999995,41.07,31.349 -2020-10-10 00:00:00,68.05,48.924,38.989000000000004,31.177 -2020-10-10 00:15:00,66.03,48.18899999999999,38.989000000000004,31.177 -2020-10-10 00:30:00,63.47,48.287,38.989000000000004,31.177 -2020-10-10 00:45:00,70.67,48.302,38.989000000000004,31.177 -2020-10-10 01:00:00,68.97,48.708,35.275,31.177 -2020-10-10 01:15:00,68.98,48.229,35.275,31.177 -2020-10-10 01:30:00,62.64,47.24100000000001,35.275,31.177 -2020-10-10 01:45:00,63.19,47.28,35.275,31.177 -2020-10-10 02:00:00,60.49,48.051,32.838,31.177 -2020-10-10 02:15:00,67.91,47.53,32.838,31.177 -2020-10-10 02:30:00,68.09,47.768,32.838,31.177 -2020-10-10 02:45:00,61.18,48.528999999999996,32.838,31.177 -2020-10-10 03:00:00,60.57,50.135,32.418,31.177 -2020-10-10 03:15:00,60.96,49.832,32.418,31.177 -2020-10-10 03:30:00,60.75,49.342,32.418,31.177 -2020-10-10 03:45:00,61.33,51.481,32.418,31.177 -2020-10-10 04:00:00,63.26,57.9,32.099000000000004,31.177 -2020-10-10 04:15:00,62.9,64.293,32.099000000000004,31.177 -2020-10-10 04:30:00,62.36,62.413000000000004,32.099000000000004,31.177 -2020-10-10 04:45:00,64.21,63.248000000000005,32.099000000000004,31.177 -2020-10-10 05:00:00,67.64,77.72,32.926,31.177 -2020-10-10 05:15:00,68.34,87.98899999999999,32.926,31.177 -2020-10-10 05:30:00,67.96,83.508,32.926,31.177 -2020-10-10 05:45:00,69.98,79.889,32.926,31.177 -2020-10-10 06:00:00,70.7,93.516,35.069,31.177 -2020-10-10 06:15:00,73.22,107.594,35.069,31.177 -2020-10-10 06:30:00,74.31,101.949,35.069,31.177 -2020-10-10 06:45:00,77.52,97.90799999999999,35.069,31.177 -2020-10-10 07:00:00,79.57,95.572,40.906,31.177 -2020-10-10 07:15:00,80.64,97.601,40.906,31.177 -2020-10-10 07:30:00,80.5,98.264,40.906,31.177 -2020-10-10 07:45:00,81.73,100.09,40.906,31.177 -2020-10-10 08:00:00,80.79,100.335,46.603,31.177 -2020-10-10 08:15:00,80.92,101.959,46.603,31.177 -2020-10-10 08:30:00,80.69,100.809,46.603,31.177 -2020-10-10 08:45:00,79.3,100.93,46.603,31.177 -2020-10-10 09:00:00,80.93,97.544,49.935,31.177 -2020-10-10 09:15:00,78.7,97.215,49.935,31.177 -2020-10-10 09:30:00,77.39,97.61399999999999,49.935,31.177 -2020-10-10 09:45:00,77.01,97.289,49.935,31.177 -2020-10-10 10:00:00,75.49,93.726,47.585,31.177 -2020-10-10 10:15:00,76.27,93.39399999999999,47.585,31.177 -2020-10-10 10:30:00,75.48,92.43299999999999,47.585,31.177 -2020-10-10 10:45:00,75.32,92.353,47.585,31.177 -2020-10-10 11:00:00,73.13,89.22200000000001,43.376999999999995,31.177 -2020-10-10 11:15:00,72.55,88.932,43.376999999999995,31.177 -2020-10-10 11:30:00,69.49,89.382,43.376999999999995,31.177 -2020-10-10 11:45:00,71.73,88.456,43.376999999999995,31.177 -2020-10-10 12:00:00,67.21,85.07799999999999,40.855,31.177 -2020-10-10 12:15:00,65.23,84.057,40.855,31.177 -2020-10-10 12:30:00,64.52,83.74700000000001,40.855,31.177 -2020-10-10 12:45:00,64.43,83.87899999999999,40.855,31.177 -2020-10-10 13:00:00,62.06,83.459,37.251,31.177 -2020-10-10 13:15:00,63.54,82.611,37.251,31.177 -2020-10-10 13:30:00,64.24,82.051,37.251,31.177 -2020-10-10 13:45:00,65.9,81.27199999999999,37.251,31.177 -2020-10-10 14:00:00,65.04,80.149,38.548,31.177 -2020-10-10 14:15:00,65.98,79.81,38.548,31.177 -2020-10-10 14:30:00,67.11,79.18,38.548,31.177 -2020-10-10 14:45:00,68.17,79.163,38.548,31.177 -2020-10-10 15:00:00,68.93,79.266,42.883,31.177 -2020-10-10 15:15:00,68.68,79.398,42.883,31.177 -2020-10-10 15:30:00,70.26,79.771,42.883,31.177 -2020-10-10 15:45:00,73.92,79.848,42.883,31.177 -2020-10-10 16:00:00,76.34,79.433,48.143,31.177 -2020-10-10 16:15:00,80.43,79.315,48.143,31.177 -2020-10-10 16:30:00,82.57,80.018,48.143,31.177 -2020-10-10 16:45:00,81.96,78.45,48.143,31.177 -2020-10-10 17:00:00,87.07,78.757,55.25,31.177 -2020-10-10 17:15:00,85.61,79.41,55.25,31.177 -2020-10-10 17:30:00,93.1,78.993,55.25,31.177 -2020-10-10 17:45:00,93.78,79.703,55.25,31.177 -2020-10-10 18:00:00,94.81,80.311,57.506,31.177 -2020-10-10 18:15:00,93.4,81.016,57.506,31.177 -2020-10-10 18:30:00,95.37,81.557,57.506,31.177 -2020-10-10 18:45:00,92.73,82.074,57.506,31.177 -2020-10-10 19:00:00,86.72,83.03299999999999,55.528999999999996,31.177 -2020-10-10 19:15:00,83.84,81.98700000000001,55.528999999999996,31.177 -2020-10-10 19:30:00,81.98,81.98100000000001,55.528999999999996,31.177 -2020-10-10 19:45:00,80.54,82.01799999999999,55.528999999999996,31.177 -2020-10-10 20:00:00,77.04,82.738,46.166000000000004,31.177 -2020-10-10 20:15:00,76.13,81.439,46.166000000000004,31.177 -2020-10-10 20:30:00,75.51,79.47399999999999,46.166000000000004,31.177 -2020-10-10 20:45:00,73.93,78.848,46.166000000000004,31.177 -2020-10-10 21:00:00,71.35,77.916,40.406,31.177 -2020-10-10 21:15:00,70.87,79.498,40.406,31.177 -2020-10-10 21:30:00,66.41,79.23100000000001,40.406,31.177 -2020-10-10 21:45:00,65.71,77.248,40.406,31.177 -2020-10-10 22:00:00,61.42,75.122,39.616,31.177 -2020-10-10 22:15:00,62.07,73.71600000000001,39.616,31.177 -2020-10-10 22:30:00,59.56,71.013,39.616,31.177 -2020-10-10 22:45:00,58.47,68.78,39.616,31.177 -2020-10-10 23:00:00,54.74,65.638,32.205,31.177 -2020-10-10 23:15:00,54.08,62.986999999999995,32.205,31.177 -2020-10-10 23:30:00,55.19,61.508,32.205,31.177 -2020-10-10 23:45:00,53.38,60.303999999999995,32.205,31.177 -2020-10-11 00:00:00,50.93,50.198,28.229,31.177 -2020-10-11 00:15:00,51.94,48.662,28.229,31.177 -2020-10-11 00:30:00,50.97,48.536,28.229,31.177 -2020-10-11 00:45:00,51.87,48.757,28.229,31.177 -2020-10-11 01:00:00,49.67,49.263999999999996,25.669,31.177 -2020-10-11 01:15:00,51.23,49.141000000000005,25.669,31.177 -2020-10-11 01:30:00,50.66,48.294,25.669,31.177 -2020-10-11 01:45:00,51.05,47.988,25.669,31.177 -2020-10-11 02:00:00,51.26,48.52,24.948,31.177 -2020-10-11 02:15:00,50.24,48.092,24.948,31.177 -2020-10-11 02:30:00,49.75,48.754,24.948,31.177 -2020-10-11 02:45:00,49.74,49.521,24.948,31.177 -2020-10-11 03:00:00,49.25,51.614,24.445,31.177 -2020-10-11 03:15:00,50.7,51.281000000000006,24.445,31.177 -2020-10-11 03:30:00,51.34,50.907,24.445,31.177 -2020-10-11 03:45:00,51.61,52.536,24.445,31.177 -2020-10-11 04:00:00,52.83,58.86600000000001,25.839000000000002,31.177 -2020-10-11 04:15:00,53.93,64.65,25.839000000000002,31.177 -2020-10-11 04:30:00,53.84,63.577,25.839000000000002,31.177 -2020-10-11 04:45:00,54.0,64.253,25.839000000000002,31.177 -2020-10-11 05:00:00,56.26,77.597,26.803,31.177 -2020-10-11 05:15:00,56.25,86.6,26.803,31.177 -2020-10-11 05:30:00,56.59,81.8,26.803,31.177 -2020-10-11 05:45:00,54.83,78.065,26.803,31.177 -2020-10-11 06:00:00,60.32,90.211,28.147,31.177 -2020-10-11 06:15:00,59.82,104.145,28.147,31.177 -2020-10-11 06:30:00,62.29,97.684,28.147,31.177 -2020-10-11 06:45:00,63.34,92.662,28.147,31.177 -2020-10-11 07:00:00,65.28,91.40100000000001,31.116,31.177 -2020-10-11 07:15:00,66.84,92.166,31.116,31.177 -2020-10-11 07:30:00,67.06,93.11200000000001,31.116,31.177 -2020-10-11 07:45:00,67.82,94.62100000000001,31.116,31.177 -2020-10-11 08:00:00,66.65,95.993,35.739000000000004,31.177 -2020-10-11 08:15:00,66.05,98.213,35.739000000000004,31.177 -2020-10-11 08:30:00,64.97,98.24600000000001,35.739000000000004,31.177 -2020-10-11 08:45:00,64.71,99.119,35.739000000000004,31.177 -2020-10-11 09:00:00,62.3,95.48899999999999,39.455999999999996,31.177 -2020-10-11 09:15:00,61.87,95.111,39.455999999999996,31.177 -2020-10-11 09:30:00,61.94,95.667,39.455999999999996,31.177 -2020-10-11 09:45:00,63.26,95.81700000000001,39.455999999999996,31.177 -2020-10-11 10:00:00,63.21,93.579,41.343999999999994,31.177 -2020-10-11 10:15:00,63.71,93.508,41.343999999999994,31.177 -2020-10-11 10:30:00,64.26,92.906,41.343999999999994,31.177 -2020-10-11 10:45:00,63.77,92.535,41.343999999999994,31.177 -2020-10-11 11:00:00,60.7,89.61200000000001,43.645,31.177 -2020-10-11 11:15:00,59.98,89.12700000000001,43.645,31.177 -2020-10-11 11:30:00,57.95,89.469,43.645,31.177 -2020-10-11 11:45:00,60.21,88.926,43.645,31.177 -2020-10-11 12:00:00,56.47,85.79,39.796,31.177 -2020-10-11 12:15:00,53.94,85.294,39.796,31.177 -2020-10-11 12:30:00,52.59,84.475,39.796,31.177 -2020-10-11 12:45:00,54.32,83.84200000000001,39.796,31.177 -2020-10-11 13:00:00,49.62,82.914,36.343,31.177 -2020-10-11 13:15:00,51.08,83.12700000000001,36.343,31.177 -2020-10-11 13:30:00,51.24,81.898,36.343,31.177 -2020-10-11 13:45:00,52.47,81.398,36.343,31.177 -2020-10-11 14:00:00,52.86,80.949,33.162,31.177 -2020-10-11 14:15:00,54.18,81.385,33.162,31.177 -2020-10-11 14:30:00,54.72,80.714,33.162,31.177 -2020-10-11 14:45:00,57.08,79.959,33.162,31.177 -2020-10-11 15:00:00,61.5,79.414,33.215,31.177 -2020-10-11 15:15:00,61.01,79.492,33.215,31.177 -2020-10-11 15:30:00,61.52,80.07300000000001,33.215,31.177 -2020-10-11 15:45:00,64.89,80.623,33.215,31.177 -2020-10-11 16:00:00,69.86,80.05,37.385999999999996,31.177 -2020-10-11 16:15:00,73.5,79.68,37.385999999999996,31.177 -2020-10-11 16:30:00,72.54,81.023,37.385999999999996,31.177 -2020-10-11 16:45:00,77.12,79.60300000000001,37.385999999999996,31.177 -2020-10-11 17:00:00,81.3,80.021,46.618,31.177 -2020-10-11 17:15:00,82.78,81.369,46.618,31.177 -2020-10-11 17:30:00,89.15,81.513,46.618,31.177 -2020-10-11 17:45:00,88.39,83.434,46.618,31.177 -2020-10-11 18:00:00,91.86,84.07700000000001,50.111000000000004,31.177 -2020-10-11 18:15:00,89.54,85.089,50.111000000000004,31.177 -2020-10-11 18:30:00,91.48,84.645,50.111000000000004,31.177 -2020-10-11 18:45:00,86.46,85.98299999999999,50.111000000000004,31.177 -2020-10-11 19:00:00,81.58,87.99,50.25,31.177 -2020-10-11 19:15:00,87.47,86.554,50.25,31.177 -2020-10-11 19:30:00,87.65,86.333,50.25,31.177 -2020-10-11 19:45:00,84.98,86.675,50.25,31.177 -2020-10-11 20:00:00,82.24,87.521,44.265,31.177 -2020-10-11 20:15:00,88.55,86.537,44.265,31.177 -2020-10-11 20:30:00,87.01,85.499,44.265,31.177 -2020-10-11 20:45:00,88.49,83.41799999999999,44.265,31.177 -2020-10-11 21:00:00,79.77,81.342,39.717,31.177 -2020-10-11 21:15:00,77.18,82.494,39.717,31.177 -2020-10-11 21:30:00,75.52,81.98899999999999,39.717,31.177 -2020-10-11 21:45:00,74.79,80.236,39.717,31.177 -2020-10-11 22:00:00,70.87,78.767,39.224000000000004,31.177 -2020-10-11 22:15:00,77.05,76.087,39.224000000000004,31.177 -2020-10-11 22:30:00,76.79,71.846,39.224000000000004,31.177 -2020-10-11 22:45:00,77.47,68.562,39.224000000000004,31.177 -2020-10-11 23:00:00,67.29,64.157,33.518,31.177 -2020-10-11 23:15:00,67.19,62.933,33.518,31.177 -2020-10-11 23:30:00,72.95,61.476000000000006,33.518,31.177 -2020-10-11 23:45:00,73.21,60.711999999999996,33.518,31.177 -2020-10-12 00:00:00,70.94,53.006,34.301,31.349 -2020-10-12 00:15:00,68.42,53.067,34.301,31.349 -2020-10-12 00:30:00,68.11,52.773999999999994,34.301,31.349 -2020-10-12 00:45:00,72.11,52.558,34.301,31.349 -2020-10-12 01:00:00,70.36,53.288999999999994,34.143,31.349 -2020-10-12 01:15:00,68.12,52.99100000000001,34.143,31.349 -2020-10-12 01:30:00,65.37,52.393,34.143,31.349 -2020-10-12 01:45:00,69.82,52.073,34.143,31.349 -2020-10-12 02:00:00,69.15,52.861999999999995,33.650999999999996,31.349 -2020-10-12 02:15:00,70.12,52.422,33.650999999999996,31.349 -2020-10-12 02:30:00,66.23,53.29,33.650999999999996,31.349 -2020-10-12 02:45:00,70.36,53.729,33.650999999999996,31.349 -2020-10-12 03:00:00,72.15,56.57899999999999,32.599000000000004,31.349 -2020-10-12 03:15:00,72.16,57.313,32.599000000000004,31.349 -2020-10-12 03:30:00,69.17,57.236000000000004,32.599000000000004,31.349 -2020-10-12 03:45:00,76.19,58.367,32.599000000000004,31.349 -2020-10-12 04:00:00,82.36,68.183,33.785,31.349 -2020-10-12 04:15:00,85.74,77.285,33.785,31.349 -2020-10-12 04:30:00,84.49,76.767,33.785,31.349 -2020-10-12 04:45:00,89.3,77.73,33.785,31.349 -2020-10-12 05:00:00,101.52,101.051,41.285,31.349 -2020-10-12 05:15:00,106.64,123.691,41.285,31.349 -2020-10-12 05:30:00,106.95,118.056,41.285,31.349 -2020-10-12 05:45:00,106.85,110.37,41.285,31.349 -2020-10-12 06:00:00,115.15,109.96799999999999,60.486000000000004,31.349 -2020-10-12 06:15:00,115.95,113.00200000000001,60.486000000000004,31.349 -2020-10-12 06:30:00,115.89,111.90100000000001,60.486000000000004,31.349 -2020-10-12 06:45:00,118.2,113.29,60.486000000000004,31.349 -2020-10-12 07:00:00,121.53,113.51899999999999,74.012,31.349 -2020-10-12 07:15:00,122.2,116.09200000000001,74.012,31.349 -2020-10-12 07:30:00,118.03,116.274,74.012,31.349 -2020-10-12 07:45:00,116.52,116.95,74.012,31.349 -2020-10-12 08:00:00,114.57,115.436,69.569,31.349 -2020-10-12 08:15:00,114.1,116.088,69.569,31.349 -2020-10-12 08:30:00,114.31,113.955,69.569,31.349 -2020-10-12 08:45:00,116.52,113.573,69.569,31.349 -2020-10-12 09:00:00,111.0,109.07,66.152,31.349 -2020-10-12 09:15:00,110.6,106.264,66.152,31.349 -2020-10-12 09:30:00,110.4,105.881,66.152,31.349 -2020-10-12 09:45:00,110.21,104.867,66.152,31.349 -2020-10-12 10:00:00,109.5,102.59200000000001,62.923,31.349 -2020-10-12 10:15:00,116.29,102.26299999999999,62.923,31.349 -2020-10-12 10:30:00,112.61,101.021,62.923,31.349 -2020-10-12 10:45:00,114.5,100.115,62.923,31.349 -2020-10-12 11:00:00,111.52,96.26700000000001,61.522,31.349 -2020-10-12 11:15:00,115.75,96.70100000000001,61.522,31.349 -2020-10-12 11:30:00,110.56,98.014,61.522,31.349 -2020-10-12 11:45:00,105.29,97.54,61.522,31.349 -2020-10-12 12:00:00,103.06,94.429,58.632,31.349 -2020-10-12 12:15:00,103.27,93.98200000000001,58.632,31.349 -2020-10-12 12:30:00,99.5,92.764,58.632,31.349 -2020-10-12 12:45:00,102.59,92.81299999999999,58.632,31.349 -2020-10-12 13:00:00,103.02,92.575,59.06,31.349 -2020-10-12 13:15:00,104.16,91.713,59.06,31.349 -2020-10-12 13:30:00,101.81,90.331,59.06,31.349 -2020-10-12 13:45:00,102.12,90.304,59.06,31.349 -2020-10-12 14:00:00,104.85,89.085,59.791000000000004,31.349 -2020-10-12 14:15:00,111.72,89.54899999999999,59.791000000000004,31.349 -2020-10-12 14:30:00,108.5,88.586,59.791000000000004,31.349 -2020-10-12 14:45:00,107.1,88.87,59.791000000000004,31.349 -2020-10-12 15:00:00,107.33,88.921,61.148,31.349 -2020-10-12 15:15:00,107.86,88.06700000000001,61.148,31.349 -2020-10-12 15:30:00,108.23,88.7,61.148,31.349 -2020-10-12 15:45:00,110.3,88.81700000000001,61.148,31.349 -2020-10-12 16:00:00,111.96,88.654,66.009,31.349 -2020-10-12 16:15:00,114.55,88.006,66.009,31.349 -2020-10-12 16:30:00,114.39,88.575,66.009,31.349 -2020-10-12 16:45:00,116.45,86.70299999999999,66.009,31.349 -2020-10-12 17:00:00,117.5,86.335,73.683,31.349 -2020-10-12 17:15:00,116.77,87.499,73.683,31.349 -2020-10-12 17:30:00,118.94,87.204,73.683,31.349 -2020-10-12 17:45:00,119.82,88.29700000000001,73.683,31.349 -2020-10-12 18:00:00,121.77,88.435,72.848,31.349 -2020-10-12 18:15:00,121.95,87.51100000000001,72.848,31.349 -2020-10-12 18:30:00,119.5,86.93299999999999,72.848,31.349 -2020-10-12 18:45:00,116.12,90.29799999999999,72.848,31.349 -2020-10-12 19:00:00,110.92,91.43799999999999,71.139,31.349 -2020-10-12 19:15:00,106.85,90.26799999999999,71.139,31.349 -2020-10-12 19:30:00,103.85,90.03200000000001,71.139,31.349 -2020-10-12 19:45:00,107.99,89.679,71.139,31.349 -2020-10-12 20:00:00,105.84,88.89,69.667,31.349 -2020-10-12 20:15:00,105.89,87.679,69.667,31.349 -2020-10-12 20:30:00,98.58,86.147,69.667,31.349 -2020-10-12 20:45:00,96.33,84.819,69.667,31.349 -2020-10-12 21:00:00,91.25,82.61399999999999,61.166000000000004,31.349 -2020-10-12 21:15:00,90.87,83.53299999999999,61.166000000000004,31.349 -2020-10-12 21:30:00,91.81,82.939,61.166000000000004,31.349 -2020-10-12 21:45:00,92.26,80.832,61.166000000000004,31.349 -2020-10-12 22:00:00,88.19,77.041,52.772,31.349 -2020-10-12 22:15:00,87.73,74.929,52.772,31.349 -2020-10-12 22:30:00,87.11,64.431,52.772,31.349 -2020-10-12 22:45:00,87.25,59.202,52.772,31.349 -2020-10-12 23:00:00,79.67,55.16,45.136,31.349 -2020-10-12 23:15:00,78.21,54.04,45.136,31.349 -2020-10-12 23:30:00,80.76,53.67,45.136,31.349 -2020-10-12 23:45:00,81.41,53.657,45.136,31.349 -2020-10-13 00:00:00,73.81,51.739,47.35,31.349 -2020-10-13 00:15:00,72.3,52.842,47.35,31.349 -2020-10-13 00:30:00,74.12,52.604,47.35,31.349 -2020-10-13 00:45:00,77.97,52.465,47.35,31.349 -2020-10-13 01:00:00,75.1,52.812,43.424,31.349 -2020-10-13 01:15:00,74.15,52.428000000000004,43.424,31.349 -2020-10-13 01:30:00,75.85,51.838,43.424,31.349 -2020-10-13 01:45:00,77.72,51.335,43.424,31.349 -2020-10-13 02:00:00,77.58,51.851000000000006,41.778999999999996,31.349 -2020-10-13 02:15:00,75.84,52.056000000000004,41.778999999999996,31.349 -2020-10-13 02:30:00,75.85,52.452,41.778999999999996,31.349 -2020-10-13 02:45:00,75.3,53.08,41.778999999999996,31.349 -2020-10-13 03:00:00,79.74,55.13399999999999,40.771,31.349 -2020-10-13 03:15:00,78.15,56.108000000000004,40.771,31.349 -2020-10-13 03:30:00,75.52,56.201,40.771,31.349 -2020-10-13 03:45:00,75.48,56.739,40.771,31.349 -2020-10-13 04:00:00,77.47,65.77199999999999,41.816,31.349 -2020-10-13 04:15:00,82.06,74.781,41.816,31.349 -2020-10-13 04:30:00,90.51,74.07600000000001,41.816,31.349 -2020-10-13 04:45:00,97.73,75.794,41.816,31.349 -2020-10-13 05:00:00,105.85,102.00200000000001,45.842,31.349 -2020-10-13 05:15:00,102.75,125.023,45.842,31.349 -2020-10-13 05:30:00,106.37,118.916,45.842,31.349 -2020-10-13 05:45:00,110.98,110.75399999999999,45.842,31.349 -2020-10-13 06:00:00,119.85,110.51,59.12,31.349 -2020-10-13 06:15:00,119.21,114.292,59.12,31.349 -2020-10-13 06:30:00,118.8,112.76899999999999,59.12,31.349 -2020-10-13 06:45:00,121.82,113.495,59.12,31.349 -2020-10-13 07:00:00,123.79,113.73899999999999,70.33,31.349 -2020-10-13 07:15:00,123.79,116.11399999999999,70.33,31.349 -2020-10-13 07:30:00,123.33,116.124,70.33,31.349 -2020-10-13 07:45:00,123.65,116.325,70.33,31.349 -2020-10-13 08:00:00,123.2,114.829,67.788,31.349 -2020-10-13 08:15:00,123.26,114.786,67.788,31.349 -2020-10-13 08:30:00,124.04,112.699,67.788,31.349 -2020-10-13 08:45:00,124.33,111.66799999999999,67.788,31.349 -2020-10-13 09:00:00,122.91,107.008,62.622,31.349 -2020-10-13 09:15:00,121.43,104.785,62.622,31.349 -2020-10-13 09:30:00,119.64,105.04799999999999,62.622,31.349 -2020-10-13 09:45:00,119.0,104.652,62.622,31.349 -2020-10-13 10:00:00,118.75,101.374,60.887,31.349 -2020-10-13 10:15:00,118.55,100.47,60.887,31.349 -2020-10-13 10:30:00,116.94,99.303,60.887,31.349 -2020-10-13 10:45:00,119.41,99.036,60.887,31.349 -2020-10-13 11:00:00,113.99,95.845,59.812,31.349 -2020-10-13 11:15:00,112.18,96.32600000000001,59.812,31.349 -2020-10-13 11:30:00,114.67,96.523,59.812,31.349 -2020-10-13 11:45:00,109.79,96.10600000000001,59.812,31.349 -2020-10-13 12:00:00,105.65,92.26,56.614,31.349 -2020-10-13 12:15:00,104.47,91.789,56.614,31.349 -2020-10-13 12:30:00,103.49,91.37299999999999,56.614,31.349 -2020-10-13 12:45:00,102.45,91.63799999999999,56.614,31.349 -2020-10-13 13:00:00,102.46,91.0,56.824,31.349 -2020-10-13 13:15:00,105.71,90.90700000000001,56.824,31.349 -2020-10-13 13:30:00,106.9,90.038,56.824,31.349 -2020-10-13 13:45:00,109.76,89.601,56.824,31.349 -2020-10-13 14:00:00,110.65,88.68700000000001,57.623999999999995,31.349 -2020-10-13 14:15:00,111.37,89.135,57.623999999999995,31.349 -2020-10-13 14:30:00,111.33,88.661,57.623999999999995,31.349 -2020-10-13 14:45:00,111.96,88.50200000000001,57.623999999999995,31.349 -2020-10-13 15:00:00,111.31,88.243,59.724,31.349 -2020-10-13 15:15:00,107.85,88.03,59.724,31.349 -2020-10-13 15:30:00,108.03,88.694,59.724,31.349 -2020-10-13 15:45:00,111.58,88.821,59.724,31.349 -2020-10-13 16:00:00,111.54,88.443,61.64,31.349 -2020-10-13 16:15:00,111.16,88.069,61.64,31.349 -2020-10-13 16:30:00,113.67,88.71700000000001,61.64,31.349 -2020-10-13 16:45:00,116.95,87.404,61.64,31.349 -2020-10-13 17:00:00,118.92,87.344,68.962,31.349 -2020-10-13 17:15:00,119.99,88.766,68.962,31.349 -2020-10-13 17:30:00,122.98,88.524,68.962,31.349 -2020-10-13 17:45:00,123.02,89.40299999999999,68.962,31.349 -2020-10-13 18:00:00,123.95,88.95,69.149,31.349 -2020-10-13 18:15:00,123.41,88.626,69.149,31.349 -2020-10-13 18:30:00,120.16,87.787,69.149,31.349 -2020-10-13 18:45:00,119.43,91.367,69.149,31.349 -2020-10-13 19:00:00,114.42,91.887,68.832,31.349 -2020-10-13 19:15:00,112.4,90.699,68.832,31.349 -2020-10-13 19:30:00,115.3,90.065,68.832,31.349 -2020-10-13 19:45:00,110.17,89.897,68.832,31.349 -2020-10-13 20:00:00,100.53,89.419,66.403,31.349 -2020-10-13 20:15:00,101.98,87.134,66.403,31.349 -2020-10-13 20:30:00,100.14,86.008,66.403,31.349 -2020-10-13 20:45:00,100.17,84.654,66.403,31.349 -2020-10-13 21:00:00,98.31,82.654,57.352,31.349 -2020-10-13 21:15:00,99.67,83.098,57.352,31.349 -2020-10-13 21:30:00,94.62,82.321,57.352,31.349 -2020-10-13 21:45:00,94.12,80.389,57.352,31.349 -2020-10-13 22:00:00,86.01,77.366,51.148999999999994,31.349 -2020-10-13 22:15:00,88.4,74.939,51.148999999999994,31.349 -2020-10-13 22:30:00,85.44,64.658,51.148999999999994,31.349 -2020-10-13 22:45:00,86.13,59.544,51.148999999999994,31.349 -2020-10-13 23:00:00,77.28,55.103,41.8,31.349 -2020-10-13 23:15:00,80.43,54.472,41.8,31.349 -2020-10-13 23:30:00,81.88,53.957,41.8,31.349 -2020-10-13 23:45:00,80.49,53.865,41.8,31.349 -2020-10-14 00:00:00,73.56,52.053000000000004,42.269,31.349 -2020-10-14 00:15:00,77.84,53.146,42.269,31.349 -2020-10-14 00:30:00,78.77,52.915,42.269,31.349 -2020-10-14 00:45:00,79.64,52.773999999999994,42.269,31.349 -2020-10-14 01:00:00,73.44,53.123999999999995,38.527,31.349 -2020-10-14 01:15:00,70.94,52.76,38.527,31.349 -2020-10-14 01:30:00,68.57,52.188,38.527,31.349 -2020-10-14 01:45:00,76.68,51.684,38.527,31.349 -2020-10-14 02:00:00,78.54,52.208,36.393,31.349 -2020-10-14 02:15:00,78.33,52.428000000000004,36.393,31.349 -2020-10-14 02:30:00,74.42,52.806000000000004,36.393,31.349 -2020-10-14 02:45:00,75.3,53.431000000000004,36.393,31.349 -2020-10-14 03:00:00,80.29,55.471000000000004,36.167,31.349 -2020-10-14 03:15:00,82.02,56.465,36.167,31.349 -2020-10-14 03:30:00,80.68,56.562,36.167,31.349 -2020-10-14 03:45:00,79.09,57.08,36.167,31.349 -2020-10-14 04:00:00,83.44,66.143,38.092,31.349 -2020-10-14 04:15:00,90.73,75.185,38.092,31.349 -2020-10-14 04:30:00,92.79,74.482,38.092,31.349 -2020-10-14 04:45:00,97.44,76.208,38.092,31.349 -2020-10-14 05:00:00,100.81,102.50299999999999,42.268,31.349 -2020-10-14 05:15:00,104.2,125.611,42.268,31.349 -2020-10-14 05:30:00,107.8,119.486,42.268,31.349 -2020-10-14 05:45:00,107.29,111.281,42.268,31.349 -2020-10-14 06:00:00,119.76,111.015,60.158,31.349 -2020-10-14 06:15:00,120.14,114.814,60.158,31.349 -2020-10-14 06:30:00,119.69,113.304,60.158,31.349 -2020-10-14 06:45:00,121.56,114.03200000000001,60.158,31.349 -2020-10-14 07:00:00,125.08,114.275,74.792,31.349 -2020-10-14 07:15:00,123.17,116.663,74.792,31.349 -2020-10-14 07:30:00,121.58,116.706,74.792,31.349 -2020-10-14 07:45:00,120.8,116.906,74.792,31.349 -2020-10-14 08:00:00,118.68,115.421,70.499,31.349 -2020-10-14 08:15:00,118.45,115.34899999999999,70.499,31.349 -2020-10-14 08:30:00,117.08,113.28399999999999,70.499,31.349 -2020-10-14 08:45:00,117.25,112.23,70.499,31.349 -2020-10-14 09:00:00,115.59,107.568,68.892,31.349 -2020-10-14 09:15:00,114.95,105.34,68.892,31.349 -2020-10-14 09:30:00,114.97,105.587,68.892,31.349 -2020-10-14 09:45:00,113.19,105.165,68.892,31.349 -2020-10-14 10:00:00,112.69,101.88,66.88600000000001,31.349 -2020-10-14 10:15:00,113.46,100.93700000000001,66.88600000000001,31.349 -2020-10-14 10:30:00,111.52,99.751,66.88600000000001,31.349 -2020-10-14 10:45:00,111.35,99.469,66.88600000000001,31.349 -2020-10-14 11:00:00,109.19,96.285,66.187,31.349 -2020-10-14 11:15:00,108.97,96.74799999999999,66.187,31.349 -2020-10-14 11:30:00,109.66,96.945,66.187,31.349 -2020-10-14 11:45:00,107.61,96.51100000000001,66.187,31.349 -2020-10-14 12:00:00,105.62,92.641,62.18,31.349 -2020-10-14 12:15:00,107.74,92.162,62.18,31.349 -2020-10-14 12:30:00,102.93,91.781,62.18,31.349 -2020-10-14 12:45:00,105.05,92.044,62.18,31.349 -2020-10-14 13:00:00,103.94,91.37299999999999,62.23,31.349 -2020-10-14 13:15:00,104.45,91.285,62.23,31.349 -2020-10-14 13:30:00,103.05,90.414,62.23,31.349 -2020-10-14 13:45:00,104.96,89.976,62.23,31.349 -2020-10-14 14:00:00,105.25,89.01100000000001,63.721000000000004,31.349 -2020-10-14 14:15:00,105.9,89.475,63.721000000000004,31.349 -2020-10-14 14:30:00,106.58,89.037,63.721000000000004,31.349 -2020-10-14 14:45:00,106.09,88.876,63.721000000000004,31.349 -2020-10-14 15:00:00,106.28,88.58200000000001,66.523,31.349 -2020-10-14 15:15:00,106.7,88.389,66.523,31.349 -2020-10-14 15:30:00,107.34,89.088,66.523,31.349 -2020-10-14 15:45:00,109.23,89.23,66.523,31.349 -2020-10-14 16:00:00,110.31,88.816,69.679,31.349 -2020-10-14 16:15:00,111.71,88.462,69.679,31.349 -2020-10-14 16:30:00,113.73,89.105,69.679,31.349 -2020-10-14 16:45:00,116.27,87.84700000000001,69.679,31.349 -2020-10-14 17:00:00,121.1,87.74600000000001,75.04,31.349 -2020-10-14 17:15:00,119.15,89.189,75.04,31.349 -2020-10-14 17:30:00,125.29,88.95,75.04,31.349 -2020-10-14 17:45:00,126.05,89.851,75.04,31.349 -2020-10-14 18:00:00,126.68,89.385,75.915,31.349 -2020-10-14 18:15:00,123.93,89.046,75.915,31.349 -2020-10-14 18:30:00,123.69,88.21799999999999,75.915,31.349 -2020-10-14 18:45:00,120.41,91.794,75.915,31.349 -2020-10-14 19:00:00,120.5,92.323,74.66,31.349 -2020-10-14 19:15:00,119.72,91.131,74.66,31.349 -2020-10-14 19:30:00,116.45,90.488,74.66,31.349 -2020-10-14 19:45:00,107.96,90.303,74.66,31.349 -2020-10-14 20:00:00,107.34,89.848,71.204,31.349 -2020-10-14 20:15:00,109.49,87.55799999999999,71.204,31.349 -2020-10-14 20:30:00,100.41,86.404,71.204,31.349 -2020-10-14 20:45:00,104.83,85.022,71.204,31.349 -2020-10-14 21:00:00,93.46,83.021,61.052,31.349 -2020-10-14 21:15:00,92.85,83.454,61.052,31.349 -2020-10-14 21:30:00,93.83,82.68700000000001,61.052,31.349 -2020-10-14 21:45:00,95.35,80.72800000000001,61.052,31.349 -2020-10-14 22:00:00,90.21,77.693,54.691,31.349 -2020-10-14 22:15:00,86.19,75.244,54.691,31.349 -2020-10-14 22:30:00,79.18,64.979,54.691,31.349 -2020-10-14 22:45:00,83.68,59.871,54.691,31.349 -2020-10-14 23:00:00,81.26,55.456,45.18,31.349 -2020-10-14 23:15:00,82.45,54.794,45.18,31.349 -2020-10-14 23:30:00,76.29,54.28,45.18,31.349 -2020-10-14 23:45:00,79.49,54.183,45.18,31.349 -2020-10-15 00:00:00,78.17,52.367,42.746,31.349 -2020-10-15 00:15:00,79.69,53.452,42.746,31.349 -2020-10-15 00:30:00,76.7,53.227,42.746,31.349 -2020-10-15 00:45:00,80.0,53.082,42.746,31.349 -2020-10-15 01:00:00,77.04,53.437,40.025999999999996,31.349 -2020-10-15 01:15:00,76.62,53.093999999999994,40.025999999999996,31.349 -2020-10-15 01:30:00,72.38,52.538999999999994,40.025999999999996,31.349 -2020-10-15 01:45:00,72.39,52.033,40.025999999999996,31.349 -2020-10-15 02:00:00,75.25,52.565,38.154,31.349 -2020-10-15 02:15:00,79.25,52.799,38.154,31.349 -2020-10-15 02:30:00,80.42,53.161,38.154,31.349 -2020-10-15 02:45:00,76.0,53.781000000000006,38.154,31.349 -2020-10-15 03:00:00,78.42,55.809,37.575,31.349 -2020-10-15 03:15:00,81.16,56.821000000000005,37.575,31.349 -2020-10-15 03:30:00,82.38,56.92100000000001,37.575,31.349 -2020-10-15 03:45:00,80.84,57.42100000000001,37.575,31.349 -2020-10-15 04:00:00,86.29,66.514,39.154,31.349 -2020-10-15 04:15:00,91.49,75.59,39.154,31.349 -2020-10-15 04:30:00,94.3,74.887,39.154,31.349 -2020-10-15 04:45:00,97.08,76.622,39.154,31.349 -2020-10-15 05:00:00,98.74,103.00299999999999,44.085,31.349 -2020-10-15 05:15:00,105.55,126.2,44.085,31.349 -2020-10-15 05:30:00,107.13,120.055,44.085,31.349 -2020-10-15 05:45:00,111.73,111.80799999999999,44.085,31.349 -2020-10-15 06:00:00,119.93,111.52,57.49,31.349 -2020-10-15 06:15:00,118.79,115.337,57.49,31.349 -2020-10-15 06:30:00,120.03,113.837,57.49,31.349 -2020-10-15 06:45:00,122.37,114.57,57.49,31.349 -2020-10-15 07:00:00,124.75,114.81200000000001,73.617,31.349 -2020-10-15 07:15:00,124.91,117.212,73.617,31.349 -2020-10-15 07:30:00,125.12,117.286,73.617,31.349 -2020-10-15 07:45:00,121.11,117.48700000000001,73.617,31.349 -2020-10-15 08:00:00,118.19,116.01,69.281,31.349 -2020-10-15 08:15:00,117.96,115.90899999999999,69.281,31.349 -2020-10-15 08:30:00,122.14,113.867,69.281,31.349 -2020-10-15 08:45:00,122.14,112.792,69.281,31.349 -2020-10-15 09:00:00,120.75,108.12799999999999,63.926,31.349 -2020-10-15 09:15:00,121.83,105.895,63.926,31.349 -2020-10-15 09:30:00,123.0,106.12700000000001,63.926,31.349 -2020-10-15 09:45:00,123.77,105.677,63.926,31.349 -2020-10-15 10:00:00,117.9,102.385,59.442,31.349 -2020-10-15 10:15:00,119.2,101.40299999999999,59.442,31.349 -2020-10-15 10:30:00,115.6,100.199,59.442,31.349 -2020-10-15 10:45:00,114.68,99.9,59.442,31.349 -2020-10-15 11:00:00,111.42,96.72399999999999,56.771,31.349 -2020-10-15 11:15:00,110.71,97.169,56.771,31.349 -2020-10-15 11:30:00,111.45,97.36399999999999,56.771,31.349 -2020-10-15 11:45:00,110.73,96.916,56.771,31.349 -2020-10-15 12:00:00,108.08,93.021,53.701,31.349 -2020-10-15 12:15:00,108.8,92.53399999999999,53.701,31.349 -2020-10-15 12:30:00,106.76,92.189,53.701,31.349 -2020-10-15 12:45:00,106.52,92.447,53.701,31.349 -2020-10-15 13:00:00,106.01,91.74600000000001,52.364,31.349 -2020-10-15 13:15:00,105.75,91.661,52.364,31.349 -2020-10-15 13:30:00,107.45,90.788,52.364,31.349 -2020-10-15 13:45:00,110.96,90.34899999999999,52.364,31.349 -2020-10-15 14:00:00,110.78,89.334,53.419,31.349 -2020-10-15 14:15:00,109.56,89.81299999999999,53.419,31.349 -2020-10-15 14:30:00,111.28,89.411,53.419,31.349 -2020-10-15 14:45:00,109.87,89.24700000000001,53.419,31.349 -2020-10-15 15:00:00,111.22,88.921,56.744,31.349 -2020-10-15 15:15:00,111.54,88.74600000000001,56.744,31.349 -2020-10-15 15:30:00,111.77,89.48200000000001,56.744,31.349 -2020-10-15 15:45:00,113.05,89.63799999999999,56.744,31.349 -2020-10-15 16:00:00,116.56,89.189,60.458,31.349 -2020-10-15 16:15:00,120.2,88.854,60.458,31.349 -2020-10-15 16:30:00,122.91,89.492,60.458,31.349 -2020-10-15 16:45:00,122.73,88.289,60.458,31.349 -2020-10-15 17:00:00,126.75,88.146,66.295,31.349 -2020-10-15 17:15:00,126.46,89.611,66.295,31.349 -2020-10-15 17:30:00,128.48,89.376,66.295,31.349 -2020-10-15 17:45:00,124.9,90.3,66.295,31.349 -2020-10-15 18:00:00,127.14,89.82,68.468,31.349 -2020-10-15 18:15:00,123.0,89.46700000000001,68.468,31.349 -2020-10-15 18:30:00,124.51,88.649,68.468,31.349 -2020-10-15 18:45:00,120.83,92.22,68.468,31.349 -2020-10-15 19:00:00,114.09,92.759,66.39399999999999,31.349 -2020-10-15 19:15:00,112.02,91.56299999999999,66.39399999999999,31.349 -2020-10-15 19:30:00,107.14,90.911,66.39399999999999,31.349 -2020-10-15 19:45:00,113.12,90.709,66.39399999999999,31.349 -2020-10-15 20:00:00,108.37,90.277,63.183,31.349 -2020-10-15 20:15:00,108.7,87.98200000000001,63.183,31.349 -2020-10-15 20:30:00,99.66,86.8,63.183,31.349 -2020-10-15 20:45:00,101.56,85.389,63.183,31.349 -2020-10-15 21:00:00,93.27,83.387,55.133,31.349 -2020-10-15 21:15:00,93.41,83.809,55.133,31.349 -2020-10-15 21:30:00,93.15,83.053,55.133,31.349 -2020-10-15 21:45:00,94.83,81.067,55.133,31.349 -2020-10-15 22:00:00,90.93,78.02,50.111999999999995,31.349 -2020-10-15 22:15:00,84.87,75.54899999999999,50.111999999999995,31.349 -2020-10-15 22:30:00,80.83,65.3,50.111999999999995,31.349 -2020-10-15 22:45:00,85.9,60.198,50.111999999999995,31.349 -2020-10-15 23:00:00,83.4,55.809,44.536,31.349 -2020-10-15 23:15:00,83.04,55.11600000000001,44.536,31.349 -2020-10-15 23:30:00,78.5,54.603,44.536,31.349 -2020-10-15 23:45:00,78.94,54.5,44.536,31.349 -2020-10-16 00:00:00,78.53,56.543,42.291000000000004,31.349 -2020-10-16 00:15:00,80.34,58.228,42.291000000000004,31.349 -2020-10-16 00:30:00,75.11,57.871,42.291000000000004,31.349 -2020-10-16 00:45:00,78.3,57.95399999999999,42.291000000000004,31.349 -2020-10-16 01:00:00,77.43,58.34,41.008,31.349 -2020-10-16 01:15:00,79.28,57.986000000000004,41.008,31.349 -2020-10-16 01:30:00,77.29,57.566,41.008,31.349 -2020-10-16 01:45:00,76.13,57.258,41.008,31.349 -2020-10-16 02:00:00,78.43,58.57,39.521,31.349 -2020-10-16 02:15:00,78.48,58.61600000000001,39.521,31.349 -2020-10-16 02:30:00,76.3,59.912,39.521,31.349 -2020-10-16 02:45:00,72.93,60.25899999999999,39.521,31.349 -2020-10-16 03:00:00,75.99,62.902,39.812,31.349 -2020-10-16 03:15:00,78.35,63.992,39.812,31.349 -2020-10-16 03:30:00,83.32,64.1,39.812,31.349 -2020-10-16 03:45:00,87.67,65.527,39.812,31.349 -2020-10-16 04:00:00,86.71,75.56,41.22,31.349 -2020-10-16 04:15:00,87.83,84.14,41.22,31.349 -2020-10-16 04:30:00,93.37,84.585,41.22,31.349 -2020-10-16 04:45:00,97.81,85.63799999999999,41.22,31.349 -2020-10-16 05:00:00,105.43,113.375,45.115,31.349 -2020-10-16 05:15:00,108.97,139.797,45.115,31.349 -2020-10-16 05:30:00,110.44,134.631,45.115,31.349 -2020-10-16 05:45:00,116.33,125.77,45.115,31.349 -2020-10-16 06:00:00,121.67,125.60799999999999,59.06100000000001,31.349 -2020-10-16 06:15:00,121.67,129.158,59.06100000000001,31.349 -2020-10-16 06:30:00,123.2,127.425,59.06100000000001,31.349 -2020-10-16 06:45:00,125.24,128.667,59.06100000000001,31.349 -2020-10-16 07:00:00,129.04,129.357,71.874,31.349 -2020-10-16 07:15:00,129.43,132.971,71.874,31.349 -2020-10-16 07:30:00,129.86,132.234,71.874,31.349 -2020-10-16 07:45:00,129.09,131.789,71.874,31.349 -2020-10-16 08:00:00,129.38,131.43,68.439,31.349 -2020-10-16 08:15:00,130.59,131.24200000000002,68.439,31.349 -2020-10-16 08:30:00,135.1,129.144,68.439,31.349 -2020-10-16 08:45:00,135.29,126.51,68.439,31.349 -2020-10-16 09:00:00,133.1,121.61399999999999,65.523,31.349 -2020-10-16 09:15:00,134.37,120.37200000000001,65.523,31.349 -2020-10-16 09:30:00,133.1,119.85799999999999,65.523,31.349 -2020-10-16 09:45:00,136.75,119.541,65.523,31.349 -2020-10-16 10:00:00,133.64,115.68799999999999,62.005,31.349 -2020-10-16 10:15:00,134.38,115.005,62.005,31.349 -2020-10-16 10:30:00,133.95,113.708,62.005,31.349 -2020-10-16 10:45:00,134.02,113.04,62.005,31.349 -2020-10-16 11:00:00,132.18,109.76799999999999,60.351000000000006,31.349 -2020-10-16 11:15:00,131.6,109.25299999999999,60.351000000000006,31.349 -2020-10-16 11:30:00,132.82,110.32799999999999,60.351000000000006,31.349 -2020-10-16 11:45:00,132.96,110.382,60.351000000000006,31.349 -2020-10-16 12:00:00,127.23,107.589,55.331,31.349 -2020-10-16 12:15:00,125.71,105.316,55.331,31.349 -2020-10-16 12:30:00,122.7,105.105,55.331,31.349 -2020-10-16 12:45:00,120.88,105.475,55.331,31.349 -2020-10-16 13:00:00,119.13,106.29700000000001,53.361999999999995,31.349 -2020-10-16 13:15:00,118.48,106.384,53.361999999999995,31.349 -2020-10-16 13:30:00,117.76,105.999,53.361999999999995,31.349 -2020-10-16 13:45:00,117.81,105.741,53.361999999999995,31.349 -2020-10-16 14:00:00,116.71,104.645,51.708,31.349 -2020-10-16 14:15:00,115.79,104.56,51.708,31.349 -2020-10-16 14:30:00,116.75,104.74799999999999,51.708,31.349 -2020-10-16 14:45:00,114.09,104.55,51.708,31.349 -2020-10-16 15:00:00,111.63,103.62700000000001,54.571000000000005,31.349 -2020-10-16 15:15:00,111.74,103.186,54.571000000000005,31.349 -2020-10-16 15:30:00,111.8,102.095,54.571000000000005,31.349 -2020-10-16 15:45:00,112.85,102.43700000000001,54.571000000000005,31.349 -2020-10-16 16:00:00,116.73,102.41799999999999,58.662,31.349 -2020-10-16 16:15:00,115.35,103.176,58.662,31.349 -2020-10-16 16:30:00,120.57,103.56,58.662,31.349 -2020-10-16 16:45:00,121.41,101.693,58.662,31.349 -2020-10-16 17:00:00,125.43,103.272,65.941,31.349 -2020-10-16 17:15:00,124.15,103.835,65.941,31.349 -2020-10-16 17:30:00,126.8,103.678,65.941,31.349 -2020-10-16 17:45:00,124.08,103.572,65.941,31.349 -2020-10-16 18:00:00,125.15,105.102,65.628,31.349 -2020-10-16 18:15:00,120.47,104.225,65.628,31.349 -2020-10-16 18:30:00,119.06,103.037,65.628,31.349 -2020-10-16 18:45:00,118.12,106.52799999999999,65.628,31.349 -2020-10-16 19:00:00,111.76,108.321,63.662,31.349 -2020-10-16 19:15:00,108.6,107.913,63.662,31.349 -2020-10-16 19:30:00,107.6,106.984,63.662,31.349 -2020-10-16 19:45:00,105.8,105.885,63.662,31.349 -2020-10-16 20:00:00,105.65,102.874,61.945,31.349 -2020-10-16 20:15:00,104.08,100.425,61.945,31.349 -2020-10-16 20:30:00,97.94,99.93700000000001,61.945,31.349 -2020-10-16 20:45:00,91.95,98.415,61.945,31.349 -2020-10-16 21:00:00,89.11,95.71700000000001,53.903,31.349 -2020-10-16 21:15:00,85.85,96.61399999999999,53.903,31.349 -2020-10-16 21:30:00,82.94,95.831,53.903,31.349 -2020-10-16 21:45:00,80.74,93.97200000000001,53.903,31.349 -2020-10-16 22:00:00,77.43,89.986,48.403999999999996,31.349 -2020-10-16 22:15:00,76.58,86.50399999999999,48.403999999999996,31.349 -2020-10-16 22:30:00,73.82,80.395,48.403999999999996,31.349 -2020-10-16 22:45:00,73.45,76.143,48.403999999999996,31.349 -2020-10-16 23:00:00,75.53,70.91199999999999,41.07,31.349 -2020-10-16 23:15:00,75.35,69.029,41.07,31.349 -2020-10-16 23:30:00,75.46,66.437,41.07,31.349 -2020-10-16 23:45:00,67.11,66.324,41.07,31.349 -2020-10-17 00:00:00,64.47,56.047,38.989000000000004,31.177 -2020-10-17 00:15:00,67.85,55.284,38.989000000000004,31.177 -2020-10-17 00:30:00,69.98,55.373000000000005,38.989000000000004,31.177 -2020-10-17 00:45:00,71.35,55.461000000000006,38.989000000000004,31.177 -2020-10-17 01:00:00,65.24,56.316,35.275,31.177 -2020-10-17 01:15:00,64.34,55.773999999999994,35.275,31.177 -2020-10-17 01:30:00,64.87,54.731,35.275,31.177 -2020-10-17 01:45:00,69.92,54.907,35.275,31.177 -2020-10-17 02:00:00,67.08,56.114,32.838,31.177 -2020-10-17 02:15:00,64.62,55.611999999999995,32.838,31.177 -2020-10-17 02:30:00,62.67,55.968,32.838,31.177 -2020-10-17 02:45:00,62.07,56.736999999999995,32.838,31.177 -2020-10-17 03:00:00,61.8,59.028999999999996,32.418,31.177 -2020-10-17 03:15:00,61.68,59.211999999999996,32.418,31.177 -2020-10-17 03:30:00,62.25,58.705,32.418,31.177 -2020-10-17 03:45:00,62.17,60.88399999999999,32.418,31.177 -2020-10-17 04:00:00,63.27,68.046,32.099000000000004,31.177 -2020-10-17 04:15:00,63.3,74.992,32.099000000000004,31.177 -2020-10-17 04:30:00,63.92,73.575,32.099000000000004,31.177 -2020-10-17 04:45:00,65.08,74.53,32.099000000000004,31.177 -2020-10-17 05:00:00,68.03,90.771,32.926,31.177 -2020-10-17 05:15:00,68.43,102.831,32.926,31.177 -2020-10-17 05:30:00,69.72,98.57600000000001,32.926,31.177 -2020-10-17 05:45:00,72.43,94.47,32.926,31.177 -2020-10-17 06:00:00,75.18,108.464,35.069,31.177 -2020-10-17 06:15:00,75.93,123.736,35.069,31.177 -2020-10-17 06:30:00,77.23,118.042,35.069,31.177 -2020-10-17 06:45:00,80.22,113.37700000000001,35.069,31.177 -2020-10-17 07:00:00,83.39,111.169,40.906,31.177 -2020-10-17 07:15:00,84.09,113.566,40.906,31.177 -2020-10-17 07:30:00,85.78,114.92299999999999,40.906,31.177 -2020-10-17 07:45:00,87.06,116.913,40.906,31.177 -2020-10-17 08:00:00,87.85,118.645,46.603,31.177 -2020-10-17 08:15:00,88.48,120.234,46.603,31.177 -2020-10-17 08:30:00,88.17,118.999,46.603,31.177 -2020-10-17 08:45:00,89.51,118.406,46.603,31.177 -2020-10-17 09:00:00,88.81,115.679,49.935,31.177 -2020-10-17 09:15:00,91.01,115.045,49.935,31.177 -2020-10-17 09:30:00,92.06,115.214,49.935,31.177 -2020-10-17 09:45:00,91.72,114.764,49.935,31.177 -2020-10-17 10:00:00,86.24,111.156,47.585,31.177 -2020-10-17 10:15:00,83.34,110.669,47.585,31.177 -2020-10-17 10:30:00,82.45,109.304,47.585,31.177 -2020-10-17 10:45:00,81.32,109.196,47.585,31.177 -2020-10-17 11:00:00,79.02,105.931,43.376999999999995,31.177 -2020-10-17 11:15:00,79.49,105.35600000000001,43.376999999999995,31.177 -2020-10-17 11:30:00,76.81,105.913,43.376999999999995,31.177 -2020-10-17 11:45:00,81.02,105.679,43.376999999999995,31.177 -2020-10-17 12:00:00,75.69,102.375,40.855,31.177 -2020-10-17 12:15:00,78.67,100.795,40.855,31.177 -2020-10-17 12:30:00,77.84,100.75200000000001,40.855,31.177 -2020-10-17 12:45:00,75.43,100.959,40.855,31.177 -2020-10-17 13:00:00,74.06,101.12700000000001,37.251,31.177 -2020-10-17 13:15:00,75.51,99.822,37.251,31.177 -2020-10-17 13:30:00,76.5,99.24700000000001,37.251,31.177 -2020-10-17 13:45:00,76.49,98.74799999999999,37.251,31.177 -2020-10-17 14:00:00,73.63,98.32,38.548,31.177 -2020-10-17 14:15:00,76.38,97.464,38.548,31.177 -2020-10-17 14:30:00,77.84,96.46700000000001,38.548,31.177 -2020-10-17 14:45:00,80.65,96.57700000000001,38.548,31.177 -2020-10-17 15:00:00,81.45,96.179,42.883,31.177 -2020-10-17 15:15:00,81.41,96.49600000000001,42.883,31.177 -2020-10-17 15:30:00,83.3,96.42,42.883,31.177 -2020-10-17 15:45:00,84.57,96.435,42.883,31.177 -2020-10-17 16:00:00,85.25,96.794,48.143,31.177 -2020-10-17 16:15:00,84.79,97.71600000000001,48.143,31.177 -2020-10-17 16:30:00,86.19,98.15799999999999,48.143,31.177 -2020-10-17 16:45:00,89.26,96.83,48.143,31.177 -2020-10-17 17:00:00,95.62,97.73299999999999,55.25,31.177 -2020-10-17 17:15:00,96.78,98.31700000000001,55.25,31.177 -2020-10-17 17:30:00,101.35,98.053,55.25,31.177 -2020-10-17 17:45:00,99.12,97.947,55.25,31.177 -2020-10-17 18:00:00,101.19,99.876,57.506,31.177 -2020-10-17 18:15:00,98.31,100.652,57.506,31.177 -2020-10-17 18:30:00,99.49,100.771,57.506,31.177 -2020-10-17 18:45:00,95.84,100.971,57.506,31.177 -2020-10-17 19:00:00,89.41,102.62799999999999,55.528999999999996,31.177 -2020-10-17 19:15:00,87.46,101.51,55.528999999999996,31.177 -2020-10-17 19:30:00,85.9,101.3,55.528999999999996,31.177 -2020-10-17 19:45:00,84.72,100.851,55.528999999999996,31.177 -2020-10-17 20:00:00,80.41,99.40299999999999,46.166000000000004,31.177 -2020-10-17 20:15:00,80.37,97.781,46.166000000000004,31.177 -2020-10-17 20:30:00,79.26,96.693,46.166000000000004,31.177 -2020-10-17 20:45:00,77.14,95.757,46.166000000000004,31.177 -2020-10-17 21:00:00,73.18,93.601,40.406,31.177 -2020-10-17 21:15:00,72.29,94.561,40.406,31.177 -2020-10-17 21:30:00,70.67,94.51,40.406,31.177 -2020-10-17 21:45:00,69.52,92.175,40.406,31.177 -2020-10-17 22:00:00,66.37,88.87700000000001,39.616,31.177 -2020-10-17 22:15:00,65.35,86.779,39.616,31.177 -2020-10-17 22:30:00,63.44,83.772,39.616,31.177 -2020-10-17 22:45:00,62.35,80.665,39.616,31.177 -2020-10-17 23:00:00,58.53,76.434,32.205,31.177 -2020-10-17 23:15:00,56.67,73.855,32.205,31.177 -2020-10-17 23:30:00,56.68,71.44,32.205,31.177 -2020-10-17 23:45:00,56.38,70.208,32.205,31.177 -2020-10-18 00:00:00,52.9,57.243,28.229,31.177 -2020-10-18 00:15:00,53.22,55.765,28.229,31.177 -2020-10-18 00:30:00,52.88,55.606,28.229,31.177 -2020-10-18 00:45:00,53.61,55.98,28.229,31.177 -2020-10-18 01:00:00,51.32,56.903999999999996,25.669,31.177 -2020-10-18 01:15:00,52.64,56.831,25.669,31.177 -2020-10-18 01:30:00,51.62,55.996,25.669,31.177 -2020-10-18 01:45:00,51.85,55.832,25.669,31.177 -2020-10-18 02:00:00,51.91,56.717,24.948,31.177 -2020-10-18 02:15:00,52.54,56.151,24.948,31.177 -2020-10-18 02:30:00,52.65,56.998000000000005,24.948,31.177 -2020-10-18 02:45:00,52.06,57.849,24.948,31.177 -2020-10-18 03:00:00,51.53,60.59,24.445,31.177 -2020-10-18 03:15:00,52.25,60.667,24.445,31.177 -2020-10-18 03:30:00,52.4,60.486999999999995,24.445,31.177 -2020-10-18 03:45:00,52.91,62.228,24.445,31.177 -2020-10-18 04:00:00,54.69,69.267,25.839000000000002,31.177 -2020-10-18 04:15:00,55.62,75.545,25.839000000000002,31.177 -2020-10-18 04:30:00,56.31,74.807,25.839000000000002,31.177 -2020-10-18 04:45:00,56.9,75.675,25.839000000000002,31.177 -2020-10-18 05:00:00,58.65,90.402,26.803,31.177 -2020-10-18 05:15:00,58.63,101.00399999999999,26.803,31.177 -2020-10-18 05:30:00,59.14,96.463,26.803,31.177 -2020-10-18 05:45:00,57.88,92.30799999999999,26.803,31.177 -2020-10-18 06:00:00,62.76,105.06200000000001,28.147,31.177 -2020-10-18 06:15:00,63.62,119.914,28.147,31.177 -2020-10-18 06:30:00,63.69,113.374,28.147,31.177 -2020-10-18 06:45:00,65.7,107.73,28.147,31.177 -2020-10-18 07:00:00,70.03,106.82,31.116,31.177 -2020-10-18 07:15:00,71.46,108.045,31.116,31.177 -2020-10-18 07:30:00,71.58,109.411,31.116,31.177 -2020-10-18 07:45:00,72.74,111.01,31.116,31.177 -2020-10-18 08:00:00,71.27,113.97399999999999,35.739000000000004,31.177 -2020-10-18 08:15:00,71.83,116.021,35.739000000000004,31.177 -2020-10-18 08:30:00,71.64,116.059,35.739000000000004,31.177 -2020-10-18 08:45:00,73.24,116.448,35.739000000000004,31.177 -2020-10-18 09:00:00,73.13,113.434,39.455999999999996,31.177 -2020-10-18 09:15:00,75.2,112.875,39.455999999999996,31.177 -2020-10-18 09:30:00,75.81,113.135,39.455999999999996,31.177 -2020-10-18 09:45:00,78.43,113.03299999999999,39.455999999999996,31.177 -2020-10-18 10:00:00,74.81,110.97200000000001,41.343999999999994,31.177 -2020-10-18 10:15:00,75.06,110.79299999999999,41.343999999999994,31.177 -2020-10-18 10:30:00,75.48,109.82700000000001,41.343999999999994,31.177 -2020-10-18 10:45:00,76.1,109.087,41.343999999999994,31.177 -2020-10-18 11:00:00,75.4,106.171,43.645,31.177 -2020-10-18 11:15:00,70.54,105.47,43.645,31.177 -2020-10-18 11:30:00,68.91,105.75200000000001,43.645,31.177 -2020-10-18 11:45:00,69.28,105.943,43.645,31.177 -2020-10-18 12:00:00,69.75,102.689,39.796,31.177 -2020-10-18 12:15:00,67.11,101.93700000000001,39.796,31.177 -2020-10-18 12:30:00,67.36,101.18799999999999,39.796,31.177 -2020-10-18 12:45:00,69.13,100.594,39.796,31.177 -2020-10-18 13:00:00,64.24,100.186,36.343,31.177 -2020-10-18 13:15:00,62.42,100.39,36.343,31.177 -2020-10-18 13:30:00,63.26,99.26299999999999,36.343,31.177 -2020-10-18 13:45:00,63.85,98.81700000000001,36.343,31.177 -2020-10-18 14:00:00,61.41,98.94,33.162,31.177 -2020-10-18 14:15:00,61.03,98.955,33.162,31.177 -2020-10-18 14:30:00,62.69,98.22399999999999,33.162,31.177 -2020-10-18 14:45:00,64.11,97.681,33.162,31.177 -2020-10-18 15:00:00,66.4,96.44,33.215,31.177 -2020-10-18 15:15:00,66.52,96.898,33.215,31.177 -2020-10-18 15:30:00,68.51,97.12799999999999,33.215,31.177 -2020-10-18 15:45:00,71.34,97.664,33.215,31.177 -2020-10-18 16:00:00,75.08,98.287,37.385999999999996,31.177 -2020-10-18 16:15:00,76.81,98.823,37.385999999999996,31.177 -2020-10-18 16:30:00,79.34,99.815,37.385999999999996,31.177 -2020-10-18 16:45:00,83.18,98.639,37.385999999999996,31.177 -2020-10-18 17:00:00,89.98,99.574,46.618,31.177 -2020-10-18 17:15:00,92.24,100.639,46.618,31.177 -2020-10-18 17:30:00,94.57,100.874,46.618,31.177 -2020-10-18 17:45:00,94.81,102.181,46.618,31.177 -2020-10-18 18:00:00,98.0,104.02799999999999,50.111000000000004,31.177 -2020-10-18 18:15:00,94.97,105.304,50.111000000000004,31.177 -2020-10-18 18:30:00,93.21,104.22200000000001,50.111000000000004,31.177 -2020-10-18 18:45:00,91.12,105.446,50.111000000000004,31.177 -2020-10-18 19:00:00,88.83,107.86,50.25,31.177 -2020-10-18 19:15:00,92.4,106.557,50.25,31.177 -2020-10-18 19:30:00,93.45,106.14,50.25,31.177 -2020-10-18 19:45:00,92.2,106.221,50.25,31.177 -2020-10-18 20:00:00,82.79,104.83200000000001,44.265,31.177 -2020-10-18 20:15:00,85.44,103.654,44.265,31.177 -2020-10-18 20:30:00,87.59,103.538,44.265,31.177 -2020-10-18 20:45:00,91.02,101.215,44.265,31.177 -2020-10-18 21:00:00,88.07,97.631,39.717,31.177 -2020-10-18 21:15:00,83.42,98.124,39.717,31.177 -2020-10-18 21:30:00,80.07,97.94200000000001,39.717,31.177 -2020-10-18 21:45:00,83.72,95.816,39.717,31.177 -2020-10-18 22:00:00,81.94,92.81299999999999,39.224000000000004,31.177 -2020-10-18 22:15:00,82.38,89.552,39.224000000000004,31.177 -2020-10-18 22:30:00,77.0,84.719,39.224000000000004,31.177 -2020-10-18 22:45:00,77.52,80.611,39.224000000000004,31.177 -2020-10-18 23:00:00,75.69,74.82,33.518,31.177 -2020-10-18 23:15:00,75.04,73.737,33.518,31.177 -2020-10-18 23:30:00,72.22,71.502,33.518,31.177 -2020-10-18 23:45:00,70.95,70.789,33.518,31.177 -2020-10-19 00:00:00,72.57,60.35,34.301,31.349 -2020-10-19 00:15:00,72.05,60.687,34.301,31.349 -2020-10-19 00:30:00,71.7,60.415,34.301,31.349 -2020-10-19 00:45:00,68.05,60.333999999999996,34.301,31.349 -2020-10-19 01:00:00,63.81,61.445,34.143,31.349 -2020-10-19 01:15:00,70.57,61.14,34.143,31.349 -2020-10-19 01:30:00,73.46,60.521,34.143,31.349 -2020-10-19 01:45:00,73.96,60.363,34.143,31.349 -2020-10-19 02:00:00,69.68,61.452,33.650999999999996,31.349 -2020-10-19 02:15:00,71.16,61.118,33.650999999999996,31.349 -2020-10-19 02:30:00,74.31,62.191,33.650999999999996,31.349 -2020-10-19 02:45:00,75.73,62.668,33.650999999999996,31.349 -2020-10-19 03:00:00,74.19,66.23100000000001,32.599000000000004,31.349 -2020-10-19 03:15:00,76.4,67.46600000000001,32.599000000000004,31.349 -2020-10-19 03:30:00,80.44,67.487,32.599000000000004,31.349 -2020-10-19 03:45:00,82.75,68.72399999999999,32.599000000000004,31.349 -2020-10-19 04:00:00,83.01,79.33800000000001,33.785,31.349 -2020-10-19 04:15:00,86.46,89.04,33.785,31.349 -2020-10-19 04:30:00,93.16,89.11200000000001,33.785,31.349 -2020-10-19 04:45:00,99.61,90.245,33.785,31.349 -2020-10-19 05:00:00,103.97,115.891,41.285,31.349 -2020-10-19 05:15:00,106.86,141.102,41.285,31.349 -2020-10-19 05:30:00,111.68,135.92700000000002,41.285,31.349 -2020-10-19 05:45:00,123.4,127.571,41.285,31.349 -2020-10-19 06:00:00,130.7,127.085,60.486000000000004,31.349 -2020-10-19 06:15:00,123.9,130.441,60.486000000000004,31.349 -2020-10-19 06:30:00,124.3,129.692,60.486000000000004,31.349 -2020-10-19 06:45:00,125.37,130.83,60.486000000000004,31.349 -2020-10-19 07:00:00,128.39,131.643,74.012,31.349 -2020-10-19 07:15:00,126.92,134.558,74.012,31.349 -2020-10-19 07:30:00,124.7,135.17,74.012,31.349 -2020-10-19 07:45:00,123.89,135.63299999999998,74.012,31.349 -2020-10-19 08:00:00,122.1,135.503,69.569,31.349 -2020-10-19 08:15:00,120.91,135.888,69.569,31.349 -2020-10-19 08:30:00,123.72,133.418,69.569,31.349 -2020-10-19 08:45:00,126.6,132.155,69.569,31.349 -2020-10-19 09:00:00,123.28,128.308,66.152,31.349 -2020-10-19 09:15:00,122.26,125.132,66.152,31.349 -2020-10-19 09:30:00,120.9,124.45,66.152,31.349 -2020-10-19 09:45:00,122.7,123.491,66.152,31.349 -2020-10-19 10:00:00,123.26,121.242,62.923,31.349 -2020-10-19 10:15:00,123.92,120.78200000000001,62.923,31.349 -2020-10-19 10:30:00,124.65,119.12799999999999,62.923,31.349 -2020-10-19 10:45:00,121.74,118.13,62.923,31.349 -2020-10-19 11:00:00,111.45,113.977,61.522,31.349 -2020-10-19 11:15:00,111.72,114.376,61.522,31.349 -2020-10-19 11:30:00,111.64,115.708,61.522,31.349 -2020-10-19 11:45:00,114.51,115.85799999999999,61.522,31.349 -2020-10-19 12:00:00,109.18,113.042,58.632,31.349 -2020-10-19 12:15:00,109.27,112.32799999999999,58.632,31.349 -2020-10-19 12:30:00,119.09,111.34,58.632,31.349 -2020-10-19 12:45:00,120.49,111.61399999999999,58.632,31.349 -2020-10-19 13:00:00,121.81,111.93299999999999,59.06,31.349 -2020-10-19 13:15:00,123.36,111.008,59.06,31.349 -2020-10-19 13:30:00,124.18,109.641,59.06,31.349 -2020-10-19 13:45:00,123.73,109.555,59.06,31.349 -2020-10-19 14:00:00,128.78,108.941,59.791000000000004,31.349 -2020-10-19 14:15:00,127.05,108.83,59.791000000000004,31.349 -2020-10-19 14:30:00,122.62,107.762,59.791000000000004,31.349 -2020-10-19 14:45:00,122.32,107.991,59.791000000000004,31.349 -2020-10-19 15:00:00,121.8,107.624,61.148,31.349 -2020-10-19 15:15:00,124.23,107.045,61.148,31.349 -2020-10-19 15:30:00,120.17,107.131,61.148,31.349 -2020-10-19 15:45:00,122.07,107.24,61.148,31.349 -2020-10-19 16:00:00,125.4,108.13600000000001,66.009,31.349 -2020-10-19 16:15:00,122.23,108.296,66.009,31.349 -2020-10-19 16:30:00,123.96,108.48,66.009,31.349 -2020-10-19 16:45:00,124.66,106.705,66.009,31.349 -2020-10-19 17:00:00,132.19,106.90799999999999,73.683,31.349 -2020-10-19 17:15:00,128.75,107.635,73.683,31.349 -2020-10-19 17:30:00,131.94,107.412,73.683,31.349 -2020-10-19 17:45:00,128.27,107.76,73.683,31.349 -2020-10-19 18:00:00,132.72,109.30799999999999,72.848,31.349 -2020-10-19 18:15:00,126.09,108.596,72.848,31.349 -2020-10-19 18:30:00,127.58,107.54700000000001,72.848,31.349 -2020-10-19 18:45:00,123.85,110.50200000000001,72.848,31.349 -2020-10-19 19:00:00,120.43,111.916,71.139,31.349 -2020-10-19 19:15:00,112.13,110.569,71.139,31.349 -2020-10-19 19:30:00,108.57,110.238,71.139,31.349 -2020-10-19 19:45:00,108.74,109.59,71.139,31.349 -2020-10-19 20:00:00,106.01,106.426,69.667,31.349 -2020-10-19 20:15:00,109.28,104.56700000000001,69.667,31.349 -2020-10-19 20:30:00,106.18,103.68299999999999,69.667,31.349 -2020-10-19 20:45:00,100.16,102.28200000000001,69.667,31.349 -2020-10-19 21:00:00,94.21,98.705,61.166000000000004,31.349 -2020-10-19 21:15:00,95.43,98.78,61.166000000000004,31.349 -2020-10-19 21:30:00,96.14,98.366,61.166000000000004,31.349 -2020-10-19 21:45:00,94.59,95.859,61.166000000000004,31.349 -2020-10-19 22:00:00,89.3,90.444,52.772,31.349 -2020-10-19 22:15:00,84.72,87.365,52.772,31.349 -2020-10-19 22:30:00,86.82,75.70100000000001,52.772,31.349 -2020-10-19 22:45:00,86.53,69.094,52.772,31.349 -2020-10-19 23:00:00,81.21,63.718,45.136,31.349 -2020-10-19 23:15:00,79.44,63.248999999999995,45.136,31.349 -2020-10-19 23:30:00,81.27,62.427,45.136,31.349 -2020-10-19 23:45:00,78.67,62.832,45.136,31.349 -2020-10-20 00:00:00,75.94,59.277,47.35,31.349 -2020-10-20 00:15:00,72.26,60.708999999999996,47.35,31.349 -2020-10-20 00:30:00,77.13,60.317,47.35,31.349 -2020-10-20 00:45:00,79.34,60.123999999999995,47.35,31.349 -2020-10-20 01:00:00,76.83,60.9,43.424,31.349 -2020-10-20 01:15:00,74.74,60.449,43.424,31.349 -2020-10-20 01:30:00,77.46,59.869,43.424,31.349 -2020-10-20 01:45:00,79.29,59.611999999999995,43.424,31.349 -2020-10-20 02:00:00,74.21,60.479,41.778999999999996,31.349 -2020-10-20 02:15:00,72.87,60.657,41.778999999999996,31.349 -2020-10-20 02:30:00,80.67,61.238,41.778999999999996,31.349 -2020-10-20 02:45:00,80.69,61.875,41.778999999999996,31.349 -2020-10-20 03:00:00,84.16,64.58,40.771,31.349 -2020-10-20 03:15:00,76.84,65.876,40.771,31.349 -2020-10-20 03:30:00,81.98,66.119,40.771,31.349 -2020-10-20 03:45:00,85.52,66.896,40.771,31.349 -2020-10-20 04:00:00,88.83,76.83,41.816,31.349 -2020-10-20 04:15:00,86.61,86.4,41.816,31.349 -2020-10-20 04:30:00,89.0,86.262,41.816,31.349 -2020-10-20 04:45:00,93.57,88.22,41.816,31.349 -2020-10-20 05:00:00,103.8,117.08200000000001,45.842,31.349 -2020-10-20 05:15:00,114.96,142.561,45.842,31.349 -2020-10-20 05:30:00,119.33,136.741,45.842,31.349 -2020-10-20 05:45:00,118.64,127.999,45.842,31.349 -2020-10-20 06:00:00,120.99,127.436,59.12,31.349 -2020-10-20 06:15:00,123.0,131.688,59.12,31.349 -2020-10-20 06:30:00,124.14,130.493,59.12,31.349 -2020-10-20 06:45:00,126.4,131.034,59.12,31.349 -2020-10-20 07:00:00,128.62,131.822,70.33,31.349 -2020-10-20 07:15:00,126.71,134.554,70.33,31.349 -2020-10-20 07:30:00,126.14,134.931,70.33,31.349 -2020-10-20 07:45:00,125.14,135.055,70.33,31.349 -2020-10-20 08:00:00,124.17,134.97,67.788,31.349 -2020-10-20 08:15:00,128.23,134.60299999999998,67.788,31.349 -2020-10-20 08:30:00,129.26,132.143,67.788,31.349 -2020-10-20 08:45:00,129.38,130.308,67.788,31.349 -2020-10-20 09:00:00,127.86,126.161,62.622,31.349 -2020-10-20 09:15:00,129.76,123.774,62.622,31.349 -2020-10-20 09:30:00,128.95,123.736,62.622,31.349 -2020-10-20 09:45:00,129.62,123.212,62.622,31.349 -2020-10-20 10:00:00,130.0,120.056,60.887,31.349 -2020-10-20 10:15:00,130.9,118.914,60.887,31.349 -2020-10-20 10:30:00,130.71,117.34899999999999,60.887,31.349 -2020-10-20 10:45:00,129.56,116.9,60.887,31.349 -2020-10-20 11:00:00,126.57,113.57799999999999,59.812,31.349 -2020-10-20 11:15:00,127.65,113.931,59.812,31.349 -2020-10-20 11:30:00,129.33,114.149,59.812,31.349 -2020-10-20 11:45:00,128.71,114.49700000000001,59.812,31.349 -2020-10-20 12:00:00,126.55,110.807,56.614,31.349 -2020-10-20 12:15:00,128.41,109.98,56.614,31.349 -2020-10-20 12:30:00,126.5,109.773,56.614,31.349 -2020-10-20 12:45:00,126.79,110.145,56.614,31.349 -2020-10-20 13:00:00,125.27,110.044,56.824,31.349 -2020-10-20 13:15:00,127.5,109.61200000000001,56.824,31.349 -2020-10-20 13:30:00,127.16,108.911,56.824,31.349 -2020-10-20 13:45:00,125.66,108.56200000000001,56.824,31.349 -2020-10-20 14:00:00,126.58,108.228,57.623999999999995,31.349 -2020-10-20 14:15:00,125.58,108.148,57.623999999999995,31.349 -2020-10-20 14:30:00,125.54,107.60600000000001,57.623999999999995,31.349 -2020-10-20 14:45:00,124.61,107.48700000000001,57.623999999999995,31.349 -2020-10-20 15:00:00,124.48,106.787,59.724,31.349 -2020-10-20 15:15:00,125.53,106.771,59.724,31.349 -2020-10-20 15:30:00,122.51,106.945,59.724,31.349 -2020-10-20 15:45:00,123.47,106.96700000000001,59.724,31.349 -2020-10-20 16:00:00,124.86,107.792,61.64,31.349 -2020-10-20 16:15:00,126.88,108.274,61.64,31.349 -2020-10-20 16:30:00,127.79,108.666,61.64,31.349 -2020-10-20 16:45:00,130.62,107.39299999999999,61.64,31.349 -2020-10-20 17:00:00,132.63,107.946,68.962,31.349 -2020-10-20 17:15:00,134.24,108.882,68.962,31.349 -2020-10-20 17:30:00,132.03,108.838,68.962,31.349 -2020-10-20 17:45:00,129.1,108.991,68.962,31.349 -2020-10-20 18:00:00,128.24,110.06,69.149,31.349 -2020-10-20 18:15:00,127.87,109.694,69.149,31.349 -2020-10-20 18:30:00,123.22,108.37899999999999,69.149,31.349 -2020-10-20 18:45:00,124.82,111.66799999999999,69.149,31.349 -2020-10-20 19:00:00,115.2,112.62100000000001,68.832,31.349 -2020-10-20 19:15:00,110.22,111.197,68.832,31.349 -2020-10-20 19:30:00,108.41,110.40899999999999,68.832,31.349 -2020-10-20 19:45:00,104.68,109.90299999999999,68.832,31.349 -2020-10-20 20:00:00,101.56,107.01299999999999,66.403,31.349 -2020-10-20 20:15:00,101.65,104.178,66.403,31.349 -2020-10-20 20:30:00,100.36,103.81,66.403,31.349 -2020-10-20 20:45:00,100.6,102.27600000000001,66.403,31.349 -2020-10-20 21:00:00,91.23,98.71700000000001,57.352,31.349 -2020-10-20 21:15:00,90.92,98.59700000000001,57.352,31.349 -2020-10-20 21:30:00,88.27,97.89299999999999,57.352,31.349 -2020-10-20 21:45:00,86.17,95.569,57.352,31.349 -2020-10-20 22:00:00,81.6,91.111,51.148999999999994,31.349 -2020-10-20 22:15:00,80.06,87.729,51.148999999999994,31.349 -2020-10-20 22:30:00,78.33,76.27,51.148999999999994,31.349 -2020-10-20 22:45:00,77.72,69.81,51.148999999999994,31.349 -2020-10-20 23:00:00,75.14,64.126,41.8,31.349 -2020-10-20 23:15:00,72.31,63.852,41.8,31.349 -2020-10-20 23:30:00,72.2,62.843,41.8,31.349 -2020-10-20 23:45:00,70.69,63.091,41.8,31.349 -2020-10-21 00:00:00,69.76,59.614,42.269,31.349 -2020-10-21 00:15:00,70.51,61.033,42.269,31.349 -2020-10-21 00:30:00,68.91,60.648,42.269,31.349 -2020-10-21 00:45:00,70.39,60.449,42.269,31.349 -2020-10-21 01:00:00,68.96,61.236999999999995,38.527,31.349 -2020-10-21 01:15:00,69.7,60.805,38.527,31.349 -2020-10-21 01:30:00,66.77,60.242,38.527,31.349 -2020-10-21 01:45:00,69.61,59.981,38.527,31.349 -2020-10-21 02:00:00,69.44,60.858000000000004,36.393,31.349 -2020-10-21 02:15:00,70.57,61.048,36.393,31.349 -2020-10-21 02:30:00,70.25,61.61600000000001,36.393,31.349 -2020-10-21 02:45:00,70.99,62.246,36.393,31.349 -2020-10-21 03:00:00,72.78,64.939,36.167,31.349 -2020-10-21 03:15:00,73.7,66.257,36.167,31.349 -2020-10-21 03:30:00,74.4,66.502,36.167,31.349 -2020-10-21 03:45:00,74.06,67.263,36.167,31.349 -2020-10-21 04:00:00,80.73,77.21600000000001,38.092,31.349 -2020-10-21 04:15:00,83.23,86.816,38.092,31.349 -2020-10-21 04:30:00,94.46,86.675,38.092,31.349 -2020-10-21 04:45:00,98.59,88.64200000000001,38.092,31.349 -2020-10-21 05:00:00,105.32,117.571,42.268,31.349 -2020-10-21 05:15:00,106.7,143.113,42.268,31.349 -2020-10-21 05:30:00,113.14,137.283,42.268,31.349 -2020-10-21 05:45:00,116.18,128.509,42.268,31.349 -2020-10-21 06:00:00,122.3,127.931,60.158,31.349 -2020-10-21 06:15:00,125.24,132.197,60.158,31.349 -2020-10-21 06:30:00,128.18,131.024,60.158,31.349 -2020-10-21 06:45:00,129.46,131.575,60.158,31.349 -2020-10-21 07:00:00,132.09,132.361,74.792,31.349 -2020-10-21 07:15:00,133.19,135.106,74.792,31.349 -2020-10-21 07:30:00,135.44,135.514,74.792,31.349 -2020-10-21 07:45:00,133.65,135.642,74.792,31.349 -2020-10-21 08:00:00,132.41,135.57,70.499,31.349 -2020-10-21 08:15:00,134.22,135.18200000000002,70.499,31.349 -2020-10-21 08:30:00,134.75,132.749,70.499,31.349 -2020-10-21 08:45:00,134.77,130.888,70.499,31.349 -2020-10-21 09:00:00,135.22,126.735,68.892,31.349 -2020-10-21 09:15:00,136.3,124.346,68.892,31.349 -2020-10-21 09:30:00,138.43,124.294,68.892,31.349 -2020-10-21 09:45:00,138.85,123.74600000000001,68.892,31.349 -2020-10-21 10:00:00,138.15,120.583,66.88600000000001,31.349 -2020-10-21 10:15:00,138.15,119.40100000000001,66.88600000000001,31.349 -2020-10-21 10:30:00,137.96,117.815,66.88600000000001,31.349 -2020-10-21 10:45:00,135.02,117.351,66.88600000000001,31.349 -2020-10-21 11:00:00,131.3,114.03200000000001,66.187,31.349 -2020-10-21 11:15:00,131.26,114.366,66.187,31.349 -2020-10-21 11:30:00,131.11,114.583,66.187,31.349 -2020-10-21 11:45:00,132.59,114.916,66.187,31.349 -2020-10-21 12:00:00,129.61,111.206,62.18,31.349 -2020-10-21 12:15:00,129.87,110.37299999999999,62.18,31.349 -2020-10-21 12:30:00,128.91,110.20200000000001,62.18,31.349 -2020-10-21 12:45:00,128.14,110.572,62.18,31.349 -2020-10-21 13:00:00,124.86,110.43700000000001,62.23,31.349 -2020-10-21 13:15:00,121.86,110.012,62.23,31.349 -2020-10-21 13:30:00,123.13,109.31,62.23,31.349 -2020-10-21 13:45:00,120.42,108.958,62.23,31.349 -2020-10-21 14:00:00,118.39,108.573,63.721000000000004,31.349 -2020-10-21 14:15:00,120.77,108.508,63.721000000000004,31.349 -2020-10-21 14:30:00,121.85,108.00299999999999,63.721000000000004,31.349 -2020-10-21 14:45:00,121.34,107.882,63.721000000000004,31.349 -2020-10-21 15:00:00,118.71,107.161,66.523,31.349 -2020-10-21 15:15:00,118.61,107.163,66.523,31.349 -2020-10-21 15:30:00,118.37,107.37700000000001,66.523,31.349 -2020-10-21 15:45:00,117.66,107.412,66.523,31.349 -2020-10-21 16:00:00,117.97,108.20700000000001,69.679,31.349 -2020-10-21 16:15:00,119.99,108.711,69.679,31.349 -2020-10-21 16:30:00,122.09,109.101,69.679,31.349 -2020-10-21 16:45:00,123.57,107.87899999999999,69.679,31.349 -2020-10-21 17:00:00,130.15,108.39,75.04,31.349 -2020-10-21 17:15:00,128.12,109.345,75.04,31.349 -2020-10-21 17:30:00,132.36,109.304,75.04,31.349 -2020-10-21 17:45:00,129.08,109.47200000000001,75.04,31.349 -2020-10-21 18:00:00,130.24,110.53299999999999,75.915,31.349 -2020-10-21 18:15:00,127.7,110.141,75.915,31.349 -2020-10-21 18:30:00,127.89,108.836,75.915,31.349 -2020-10-21 18:45:00,123.32,112.119,75.915,31.349 -2020-10-21 19:00:00,118.23,113.083,74.66,31.349 -2020-10-21 19:15:00,113.94,111.652,74.66,31.349 -2020-10-21 19:30:00,110.24,110.85,74.66,31.349 -2020-10-21 19:45:00,109.15,110.32,74.66,31.349 -2020-10-21 20:00:00,103.44,107.45299999999999,71.204,31.349 -2020-10-21 20:15:00,104.11,104.609,71.204,31.349 -2020-10-21 20:30:00,104.78,104.212,71.204,31.349 -2020-10-21 20:45:00,105.75,102.661,71.204,31.349 -2020-10-21 21:00:00,100.72,99.09899999999999,61.052,31.349 -2020-10-21 21:15:00,95.64,98.96700000000001,61.052,31.349 -2020-10-21 21:30:00,91.76,98.273,61.052,31.349 -2020-10-21 21:45:00,95.11,95.929,61.052,31.349 -2020-10-21 22:00:00,91.39,91.464,54.691,31.349 -2020-10-21 22:15:00,87.48,88.06200000000001,54.691,31.349 -2020-10-21 22:30:00,84.33,76.632,54.691,31.349 -2020-10-21 22:45:00,85.24,70.179,54.691,31.349 -2020-10-21 23:00:00,81.86,64.51,45.18,31.349 -2020-10-21 23:15:00,83.34,64.208,45.18,31.349 -2020-10-21 23:30:00,79.08,63.201,45.18,31.349 -2020-10-21 23:45:00,78.07,63.438,45.18,31.349 -2020-10-22 00:00:00,78.96,59.951,42.746,31.349 -2020-10-22 00:15:00,80.34,61.357,42.746,31.349 -2020-10-22 00:30:00,76.75,60.979,42.746,31.349 -2020-10-22 00:45:00,74.47,60.773,42.746,31.349 -2020-10-22 01:00:00,74.08,61.575,40.025999999999996,31.349 -2020-10-22 01:15:00,78.96,61.161,40.025999999999996,31.349 -2020-10-22 01:30:00,78.92,60.614,40.025999999999996,31.349 -2020-10-22 01:45:00,72.66,60.349,40.025999999999996,31.349 -2020-10-22 02:00:00,72.85,61.236000000000004,38.154,31.349 -2020-10-22 02:15:00,72.18,61.438,38.154,31.349 -2020-10-22 02:30:00,80.12,61.99100000000001,38.154,31.349 -2020-10-22 02:45:00,80.24,62.618,38.154,31.349 -2020-10-22 03:00:00,77.98,65.297,37.575,31.349 -2020-10-22 03:15:00,77.21,66.63600000000001,37.575,31.349 -2020-10-22 03:30:00,76.58,66.88600000000001,37.575,31.349 -2020-10-22 03:45:00,81.48,67.62899999999999,37.575,31.349 -2020-10-22 04:00:00,90.01,77.601,39.154,31.349 -2020-10-22 04:15:00,92.44,87.23,39.154,31.349 -2020-10-22 04:30:00,87.34,87.086,39.154,31.349 -2020-10-22 04:45:00,93.79,89.06200000000001,39.154,31.349 -2020-10-22 05:00:00,105.64,118.059,44.085,31.349 -2020-10-22 05:15:00,114.87,143.665,44.085,31.349 -2020-10-22 05:30:00,114.7,137.82399999999998,44.085,31.349 -2020-10-22 05:45:00,113.54,129.018,44.085,31.349 -2020-10-22 06:00:00,122.12,128.42600000000002,57.49,31.349 -2020-10-22 06:15:00,124.14,132.705,57.49,31.349 -2020-10-22 06:30:00,127.55,131.553,57.49,31.349 -2020-10-22 06:45:00,128.59,132.114,57.49,31.349 -2020-10-22 07:00:00,132.08,132.9,73.617,31.349 -2020-10-22 07:15:00,132.26,135.658,73.617,31.349 -2020-10-22 07:30:00,133.23,136.096,73.617,31.349 -2020-10-22 07:45:00,133.61,136.227,73.617,31.349 -2020-10-22 08:00:00,133.59,136.168,69.281,31.349 -2020-10-22 08:15:00,133.94,135.75799999999998,69.281,31.349 -2020-10-22 08:30:00,133.56,133.351,69.281,31.349 -2020-10-22 08:45:00,134.29,131.466,69.281,31.349 -2020-10-22 09:00:00,132.11,127.307,63.926,31.349 -2020-10-22 09:15:00,134.27,124.915,63.926,31.349 -2020-10-22 09:30:00,133.85,124.851,63.926,31.349 -2020-10-22 09:45:00,133.96,124.27799999999999,63.926,31.349 -2020-10-22 10:00:00,131.38,121.10799999999999,59.442,31.349 -2020-10-22 10:15:00,133.21,119.88600000000001,59.442,31.349 -2020-10-22 10:30:00,132.9,118.28,59.442,31.349 -2020-10-22 10:45:00,132.11,117.79899999999999,59.442,31.349 -2020-10-22 11:00:00,127.4,114.485,56.771,31.349 -2020-10-22 11:15:00,129.2,114.801,56.771,31.349 -2020-10-22 11:30:00,127.65,115.016,56.771,31.349 -2020-10-22 11:45:00,125.62,115.334,56.771,31.349 -2020-10-22 12:00:00,123.46,111.602,53.701,31.349 -2020-10-22 12:15:00,124.88,110.765,53.701,31.349 -2020-10-22 12:30:00,119.09,110.62799999999999,53.701,31.349 -2020-10-22 12:45:00,120.14,110.99799999999999,53.701,31.349 -2020-10-22 13:00:00,122.1,110.82799999999999,52.364,31.349 -2020-10-22 13:15:00,123.26,110.411,52.364,31.349 -2020-10-22 13:30:00,124.34,109.709,52.364,31.349 -2020-10-22 13:45:00,124.93,109.353,52.364,31.349 -2020-10-22 14:00:00,128.1,108.916,53.419,31.349 -2020-10-22 14:15:00,124.78,108.868,53.419,31.349 -2020-10-22 14:30:00,123.07,108.398,53.419,31.349 -2020-10-22 14:45:00,119.7,108.27600000000001,53.419,31.349 -2020-10-22 15:00:00,121.74,107.53399999999999,56.744,31.349 -2020-10-22 15:15:00,119.68,107.554,56.744,31.349 -2020-10-22 15:30:00,119.77,107.806,56.744,31.349 -2020-10-22 15:45:00,118.01,107.85600000000001,56.744,31.349 -2020-10-22 16:00:00,123.58,108.62,60.458,31.349 -2020-10-22 16:15:00,123.95,109.146,60.458,31.349 -2020-10-22 16:30:00,124.24,109.53299999999999,60.458,31.349 -2020-10-22 16:45:00,126.61,108.363,60.458,31.349 -2020-10-22 17:00:00,132.29,108.83200000000001,66.295,31.349 -2020-10-22 17:15:00,131.4,109.807,66.295,31.349 -2020-10-22 17:30:00,130.67,109.76799999999999,66.295,31.349 -2020-10-22 17:45:00,130.55,109.95200000000001,66.295,31.349 -2020-10-22 18:00:00,130.07,111.006,68.468,31.349 -2020-10-22 18:15:00,127.24,110.588,68.468,31.349 -2020-10-22 18:30:00,126.24,109.292,68.468,31.349 -2020-10-22 18:45:00,124.94,112.571,68.468,31.349 -2020-10-22 19:00:00,116.69,113.544,66.39399999999999,31.349 -2020-10-22 19:15:00,113.13,112.10600000000001,66.39399999999999,31.349 -2020-10-22 19:30:00,109.94,111.291,66.39399999999999,31.349 -2020-10-22 19:45:00,109.01,110.73700000000001,66.39399999999999,31.349 -2020-10-22 20:00:00,106.55,107.891,63.183,31.349 -2020-10-22 20:15:00,110.78,105.039,63.183,31.349 -2020-10-22 20:30:00,109.45,104.61399999999999,63.183,31.349 -2020-10-22 20:45:00,102.93,103.04299999999999,63.183,31.349 -2020-10-22 21:00:00,95.75,99.479,55.133,31.349 -2020-10-22 21:15:00,94.53,99.337,55.133,31.349 -2020-10-22 21:30:00,91.45,98.652,55.133,31.349 -2020-10-22 21:45:00,96.05,96.286,55.133,31.349 -2020-10-22 22:00:00,89.93,91.816,50.111999999999995,31.349 -2020-10-22 22:15:00,88.13,88.395,50.111999999999995,31.349 -2020-10-22 22:30:00,89.13,76.995,50.111999999999995,31.349 -2020-10-22 22:45:00,89.9,70.547,50.111999999999995,31.349 -2020-10-22 23:00:00,83.75,64.893,44.536,31.349 -2020-10-22 23:15:00,78.07,64.564,44.536,31.349 -2020-10-22 23:30:00,80.56,63.558,44.536,31.349 -2020-10-22 23:45:00,82.63,63.785,44.536,31.349 -2020-10-23 00:00:00,78.83,58.907,42.291000000000004,31.349 -2020-10-23 00:15:00,76.14,60.5,42.291000000000004,31.349 -2020-10-23 00:30:00,70.38,60.188,42.291000000000004,31.349 -2020-10-23 00:45:00,78.33,60.231,42.291000000000004,31.349 -2020-10-23 01:00:00,77.49,60.708999999999996,41.008,31.349 -2020-10-23 01:15:00,78.38,60.483000000000004,41.008,31.349 -2020-10-23 01:30:00,72.93,60.181999999999995,41.008,31.349 -2020-10-23 01:45:00,73.02,59.847,41.008,31.349 -2020-10-23 02:00:00,71.03,61.23,39.521,31.349 -2020-10-23 02:15:00,73.65,61.361000000000004,39.521,31.349 -2020-10-23 02:30:00,79.52,62.555,39.521,31.349 -2020-10-23 02:45:00,80.33,62.872,39.521,31.349 -2020-10-23 03:00:00,79.49,65.42,39.812,31.349 -2020-10-23 03:15:00,77.2,66.66199999999999,39.812,31.349 -2020-10-23 03:30:00,77.66,66.79,39.812,31.349 -2020-10-23 03:45:00,80.08,68.09899999999999,39.812,31.349 -2020-10-23 04:00:00,93.02,78.267,41.22,31.349 -2020-10-23 04:15:00,92.04,87.04899999999999,41.22,31.349 -2020-10-23 04:30:00,95.88,87.476,41.22,31.349 -2020-10-23 04:45:00,103.64,88.589,41.22,31.349 -2020-10-23 05:00:00,110.56,116.802,45.115,31.349 -2020-10-23 05:15:00,109.64,143.666,45.115,31.349 -2020-10-23 05:30:00,112.58,138.431,45.115,31.349 -2020-10-23 05:45:00,115.81,129.345,45.115,31.349 -2020-10-23 06:00:00,127.62,129.082,59.06100000000001,31.349 -2020-10-23 06:15:00,125.7,132.726,59.06100000000001,31.349 -2020-10-23 06:30:00,129.36,131.142,59.06100000000001,31.349 -2020-10-23 06:45:00,130.72,132.45600000000002,59.06100000000001,31.349 -2020-10-23 07:00:00,132.87,133.134,71.874,31.349 -2020-10-23 07:15:00,132.91,136.845,71.874,31.349 -2020-10-23 07:30:00,132.75,136.32399999999998,71.874,31.349 -2020-10-23 07:45:00,132.39,135.908,71.874,31.349 -2020-10-23 08:00:00,135.33,135.643,68.439,31.349 -2020-10-23 08:15:00,133.18,135.3,68.439,31.349 -2020-10-23 08:30:00,131.71,133.393,68.439,31.349 -2020-10-23 08:45:00,132.74,130.588,68.439,31.349 -2020-10-23 09:00:00,130.86,125.649,65.523,31.349 -2020-10-23 09:15:00,129.44,124.387,65.523,31.349 -2020-10-23 09:30:00,130.21,123.78299999999999,65.523,31.349 -2020-10-23 09:45:00,126.99,123.292,65.523,31.349 -2020-10-23 10:00:00,126.29,119.389,62.005,31.349 -2020-10-23 10:15:00,126.97,118.429,62.005,31.349 -2020-10-23 10:30:00,127.62,116.988,62.005,31.349 -2020-10-23 10:45:00,128.81,116.204,62.005,31.349 -2020-10-23 11:00:00,125.12,112.964,60.351000000000006,31.349 -2020-10-23 11:15:00,124.04,112.315,60.351000000000006,31.349 -2020-10-23 11:30:00,124.64,113.381,60.351000000000006,31.349 -2020-10-23 11:45:00,128.31,113.32799999999999,60.351000000000006,31.349 -2020-10-23 12:00:00,121.36,110.387,55.331,31.349 -2020-10-23 12:15:00,123.71,108.07700000000001,55.331,31.349 -2020-10-23 12:30:00,121.4,108.113,55.331,31.349 -2020-10-23 12:45:00,120.71,108.477,55.331,31.349 -2020-10-23 13:00:00,114.05,109.057,53.361999999999995,31.349 -2020-10-23 13:15:00,112.96,109.195,53.361999999999995,31.349 -2020-10-23 13:30:00,114.06,108.807,53.361999999999995,31.349 -2020-10-23 13:45:00,109.64,108.527,53.361999999999995,31.349 -2020-10-23 14:00:00,109.29,107.06299999999999,51.708,31.349 -2020-10-23 14:15:00,107.91,107.09200000000001,51.708,31.349 -2020-10-23 14:30:00,109.64,107.539,51.708,31.349 -2020-10-23 14:45:00,108.55,107.329,51.708,31.349 -2020-10-23 15:00:00,108.97,106.251,54.571000000000005,31.349 -2020-10-23 15:15:00,110.24,105.94,54.571000000000005,31.349 -2020-10-23 15:30:00,110.25,105.12200000000001,54.571000000000005,31.349 -2020-10-23 15:45:00,111.4,105.56299999999999,54.571000000000005,31.349 -2020-10-23 16:00:00,111.51,105.333,58.662,31.349 -2020-10-23 16:15:00,113.23,106.244,58.662,31.349 -2020-10-23 16:30:00,114.42,106.60799999999999,58.662,31.349 -2020-10-23 16:45:00,118.53,105.10600000000001,58.662,31.349 -2020-10-23 17:00:00,122.49,106.39399999999999,65.941,31.349 -2020-10-23 17:15:00,122.56,107.088,65.941,31.349 -2020-10-23 17:30:00,123.72,106.946,65.941,31.349 -2020-10-23 17:45:00,123.59,106.948,65.941,31.349 -2020-10-23 18:00:00,121.84,108.42299999999999,65.628,31.349 -2020-10-23 18:15:00,121.3,107.361,65.628,31.349 -2020-10-23 18:30:00,117.05,106.244,65.628,31.349 -2020-10-23 18:45:00,113.81,109.695,65.628,31.349 -2020-10-23 19:00:00,107.92,111.56299999999999,63.662,31.349 -2020-10-23 19:15:00,105.21,111.102,63.662,31.349 -2020-10-23 19:30:00,103.36,110.083,63.662,31.349 -2020-10-23 19:45:00,100.53,108.814,63.662,31.349 -2020-10-23 20:00:00,96.19,105.954,61.945,31.349 -2020-10-23 20:15:00,96.91,103.45,61.945,31.349 -2020-10-23 20:30:00,98.96,102.758,61.945,31.349 -2020-10-23 20:45:00,98.21,101.104,61.945,31.349 -2020-10-23 21:00:00,88.95,98.39200000000001,53.903,31.349 -2020-10-23 21:15:00,85.03,99.21700000000001,53.903,31.349 -2020-10-23 21:30:00,86.27,98.49700000000001,53.903,31.349 -2020-10-23 21:45:00,86.36,96.485,53.903,31.349 -2020-10-23 22:00:00,82.11,92.458,48.403999999999996,31.349 -2020-10-23 22:15:00,79.76,88.836,48.403999999999996,31.349 -2020-10-23 22:30:00,78.87,82.929,48.403999999999996,31.349 -2020-10-23 22:45:00,77.66,78.721,48.403999999999996,31.349 -2020-10-23 23:00:00,72.45,73.60300000000001,41.07,31.349 -2020-10-23 23:15:00,66.27,71.525,41.07,31.349 -2020-10-23 23:30:00,71.85,68.943,41.07,31.349 -2020-10-23 23:45:00,72.89,68.757,41.07,31.349 -2020-10-24 00:00:00,69.15,58.409,38.989000000000004,31.177 -2020-10-24 00:15:00,66.21,57.553999999999995,38.989000000000004,31.177 -2020-10-24 00:30:00,66.17,57.687,38.989000000000004,31.177 -2020-10-24 00:45:00,68.56,57.732,38.989000000000004,31.177 -2020-10-24 01:00:00,67.79,58.681000000000004,35.275,31.177 -2020-10-24 01:15:00,63.68,58.266000000000005,35.275,31.177 -2020-10-24 01:30:00,65.18,57.342,35.275,31.177 -2020-10-24 01:45:00,67.86,57.486999999999995,35.275,31.177 -2020-10-24 02:00:00,66.58,58.766999999999996,32.838,31.177 -2020-10-24 02:15:00,64.36,58.35,32.838,31.177 -2020-10-24 02:30:00,61.86,58.606,32.838,31.177 -2020-10-24 02:45:00,66.88,59.343999999999994,32.838,31.177 -2020-10-24 03:00:00,65.58,61.542,32.418,31.177 -2020-10-24 03:15:00,60.76,61.876000000000005,32.418,31.177 -2020-10-24 03:30:00,61.45,61.39,32.418,31.177 -2020-10-24 03:45:00,60.33,63.449,32.418,31.177 -2020-10-24 04:00:00,61.63,70.747,32.099000000000004,31.177 -2020-10-24 04:15:00,61.62,77.895,32.099000000000004,31.177 -2020-10-24 04:30:00,61.95,76.46,32.099000000000004,31.177 -2020-10-24 04:45:00,64.96,77.475,32.099000000000004,31.177 -2020-10-24 05:00:00,66.95,94.19200000000001,32.926,31.177 -2020-10-24 05:15:00,68.94,106.695,32.926,31.177 -2020-10-24 05:30:00,70.32,102.368,32.926,31.177 -2020-10-24 05:45:00,72.01,98.037,32.926,31.177 -2020-10-24 06:00:00,75.35,111.932,35.069,31.177 -2020-10-24 06:15:00,77.38,127.29899999999999,35.069,31.177 -2020-10-24 06:30:00,80.27,121.75399999999999,35.069,31.177 -2020-10-24 06:45:00,82.52,117.161,35.069,31.177 -2020-10-24 07:00:00,84.52,114.94,40.906,31.177 -2020-10-24 07:15:00,86.47,117.434,40.906,31.177 -2020-10-24 07:30:00,88.55,119.00399999999999,40.906,31.177 -2020-10-24 07:45:00,90.46,121.02,40.906,31.177 -2020-10-24 08:00:00,93.09,122.844,46.603,31.177 -2020-10-24 08:15:00,92.39,124.279,46.603,31.177 -2020-10-24 08:30:00,92.69,123.23299999999999,46.603,31.177 -2020-10-24 08:45:00,92.18,122.46799999999999,46.603,31.177 -2020-10-24 09:00:00,92.61,119.697,49.935,31.177 -2020-10-24 09:15:00,95.38,119.044,49.935,31.177 -2020-10-24 09:30:00,92.79,119.12299999999999,49.935,31.177 -2020-10-24 09:45:00,98.37,118.501,49.935,31.177 -2020-10-24 10:00:00,95.5,114.84299999999999,47.585,31.177 -2020-10-24 10:15:00,96.58,114.081,47.585,31.177 -2020-10-24 10:30:00,97.94,112.571,47.585,31.177 -2020-10-24 10:45:00,98.26,112.348,47.585,31.177 -2020-10-24 11:00:00,98.27,109.11399999999999,43.376999999999995,31.177 -2020-10-24 11:15:00,98.66,108.404,43.376999999999995,31.177 -2020-10-24 11:30:00,99.06,108.95299999999999,43.376999999999995,31.177 -2020-10-24 11:45:00,96.15,108.61399999999999,43.376999999999995,31.177 -2020-10-24 12:00:00,91.6,105.161,40.855,31.177 -2020-10-24 12:15:00,91.87,103.545,40.855,31.177 -2020-10-24 12:30:00,93.38,103.74799999999999,40.855,31.177 -2020-10-24 12:45:00,90.21,103.949,40.855,31.177 -2020-10-24 13:00:00,86.6,103.87899999999999,37.251,31.177 -2020-10-24 13:15:00,86.51,102.62299999999999,37.251,31.177 -2020-10-24 13:30:00,85.95,102.045,37.251,31.177 -2020-10-24 13:45:00,85.82,101.522,37.251,31.177 -2020-10-24 14:00:00,86.06,100.728,38.548,31.177 -2020-10-24 14:15:00,86.43,99.986,38.548,31.177 -2020-10-24 14:30:00,86.03,99.24700000000001,38.548,31.177 -2020-10-24 14:45:00,85.75,99.345,38.548,31.177 -2020-10-24 15:00:00,84.72,98.795,42.883,31.177 -2020-10-24 15:15:00,85.56,99.241,42.883,31.177 -2020-10-24 15:30:00,85.6,99.435,42.883,31.177 -2020-10-24 15:45:00,86.3,99.54899999999999,42.883,31.177 -2020-10-24 16:00:00,89.01,99.697,48.143,31.177 -2020-10-24 16:15:00,89.51,100.771,48.143,31.177 -2020-10-24 16:30:00,92.55,101.194,48.143,31.177 -2020-10-24 16:45:00,96.25,100.23100000000001,48.143,31.177 -2020-10-24 17:00:00,102.45,100.84299999999999,55.25,31.177 -2020-10-24 17:15:00,102.68,101.559,55.25,31.177 -2020-10-24 17:30:00,105.3,101.311,55.25,31.177 -2020-10-24 17:45:00,102.74,101.31299999999999,55.25,31.177 -2020-10-24 18:00:00,102.08,103.189,57.506,31.177 -2020-10-24 18:15:00,99.7,103.78200000000001,57.506,31.177 -2020-10-24 18:30:00,98.42,103.971,57.506,31.177 -2020-10-24 18:45:00,96.26,104.133,57.506,31.177 -2020-10-24 19:00:00,91.99,105.861,55.528999999999996,31.177 -2020-10-24 19:15:00,88.78,104.691,55.528999999999996,31.177 -2020-10-24 19:30:00,86.53,104.39200000000001,55.528999999999996,31.177 -2020-10-24 19:45:00,85.01,103.775,55.528999999999996,31.177 -2020-10-24 20:00:00,81.07,102.477,46.166000000000004,31.177 -2020-10-24 20:15:00,81.19,100.8,46.166000000000004,31.177 -2020-10-24 20:30:00,79.06,99.508,46.166000000000004,31.177 -2020-10-24 20:45:00,78.71,98.44,46.166000000000004,31.177 -2020-10-24 21:00:00,74.27,96.27,40.406,31.177 -2020-10-24 21:15:00,73.82,97.156,40.406,31.177 -2020-10-24 21:30:00,69.74,97.17,40.406,31.177 -2020-10-24 21:45:00,70.03,94.685,40.406,31.177 -2020-10-24 22:00:00,66.57,91.34200000000001,39.616,31.177 -2020-10-24 22:15:00,66.52,89.10799999999999,39.616,31.177 -2020-10-24 22:30:00,63.76,86.304,39.616,31.177 -2020-10-24 22:45:00,63.35,83.242,39.616,31.177 -2020-10-24 23:00:00,59.13,79.12100000000001,32.205,31.177 -2020-10-24 23:15:00,57.91,76.347,32.205,31.177 -2020-10-24 23:30:00,58.49,73.942,32.205,31.177 -2020-10-24 23:45:00,57.43,72.638,32.205,31.177 -2020-10-25 00:00:00,54.37,59.602,28.229,31.177 -2020-10-25 00:15:00,54.85,58.032,28.229,31.177 -2020-10-25 00:30:00,53.15,57.915,28.229,31.177 -2020-10-25 00:45:00,53.46,58.245,28.229,31.177 -2020-10-25 01:00:00,52.45,59.261,25.669,31.177 -2020-10-25 01:15:00,53.12,59.316,25.669,31.177 -2020-10-25 01:30:00,52.62,58.598,25.669,31.177 -2020-10-25 01:45:00,53.42,58.406000000000006,25.669,31.177 -2020-10-25 02:00:00,53.47,59.361999999999995,24.948,31.177 -2020-10-25 02:15:00,52.07,58.88,24.948,31.177 -2020-10-25 02:30:00,53.29,59.628,24.948,31.177 -2020-10-25 02:45:00,50.45,60.449,24.948,31.177 -2020-10-25 02:00:00,53.47,59.361999999999995,24.948,31.177 -2020-10-25 02:15:00,52.07,58.88,24.948,31.177 -2020-10-25 02:30:00,53.29,59.628,24.948,31.177 -2020-10-25 02:45:00,50.45,60.449,24.948,31.177 -2020-10-25 03:00:00,51.47,63.097,25.839000000000002,31.177 -2020-10-25 03:15:00,53.26,63.324,25.839000000000002,31.177 -2020-10-25 03:30:00,53.34,63.165,25.839000000000002,31.177 -2020-10-25 03:45:00,54.51,64.78699999999999,25.839000000000002,31.177 -2020-10-25 04:00:00,53.29,71.962,26.803,31.177 -2020-10-25 04:15:00,53.34,78.443,26.803,31.177 -2020-10-25 04:30:00,53.8,77.687,26.803,31.177 -2020-10-25 04:45:00,54.16,78.613,26.803,31.177 -2020-10-25 05:00:00,55.1,93.816,28.147,31.177 -2020-10-25 05:15:00,57.45,104.86200000000001,28.147,31.177 -2020-10-25 05:30:00,57.71,100.24700000000001,28.147,31.177 -2020-10-25 05:45:00,58.69,95.866,28.147,31.177 -2020-10-25 06:00:00,62.27,108.524,31.116,31.177 -2020-10-25 06:15:00,63.1,123.47,31.116,31.177 -2020-10-25 06:30:00,62.46,117.07799999999999,31.116,31.177 -2020-10-25 06:45:00,62.7,111.507,31.116,31.177 -2020-10-25 07:00:00,63.83,110.587,35.739000000000004,31.177 -2020-10-25 07:15:00,68.03,111.905,35.739000000000004,31.177 -2020-10-25 07:30:00,70.45,113.48200000000001,35.739000000000004,31.177 -2020-10-25 07:45:00,73.32,115.104,35.739000000000004,31.177 -2020-10-25 08:00:00,74.3,118.15899999999999,39.455999999999996,31.177 -2020-10-25 08:15:00,75.67,120.051,39.455999999999996,31.177 -2020-10-25 08:30:00,77.85,120.27600000000001,39.455999999999996,31.177 -2020-10-25 08:45:00,79.31,120.493,39.455999999999996,31.177 -2020-10-25 09:00:00,79.61,117.434,41.343999999999994,31.177 -2020-10-25 09:15:00,84.58,116.85700000000001,41.343999999999994,31.177 -2020-10-25 09:30:00,85.64,117.029,41.343999999999994,31.177 -2020-10-25 09:45:00,82.94,116.75399999999999,41.343999999999994,31.177 -2020-10-25 10:00:00,85.77,114.64299999999999,43.645,31.177 -2020-10-25 10:15:00,85.99,114.189,43.645,31.177 -2020-10-25 10:30:00,87.54,113.08,43.645,31.177 -2020-10-25 10:45:00,90.19,112.226,43.645,31.177 -2020-10-25 11:00:00,91.19,109.338,39.796,31.177 -2020-10-25 11:15:00,94.15,108.50299999999999,39.796,31.177 -2020-10-25 11:30:00,95.37,108.779,39.796,31.177 -2020-10-25 11:45:00,98.86,108.865,39.796,31.177 -2020-10-25 12:00:00,91.98,105.462,36.343,31.177 -2020-10-25 12:15:00,91.68,104.677,36.343,31.177 -2020-10-25 12:30:00,88.9,104.17200000000001,36.343,31.177 -2020-10-25 12:45:00,90.13,103.572,36.343,31.177 -2020-10-25 13:00:00,82.09,102.927,33.162,31.177 -2020-10-25 13:15:00,81.4,103.18,33.162,31.177 -2020-10-25 13:30:00,81.38,102.04700000000001,33.162,31.177 -2020-10-25 13:45:00,81.84,101.579,33.162,31.177 -2020-10-25 14:00:00,81.77,101.337,33.215,31.177 -2020-10-25 14:15:00,79.73,101.465,33.215,31.177 -2020-10-25 14:30:00,79.1,100.992,33.215,31.177 -2020-10-25 14:45:00,79.01,100.43799999999999,33.215,31.177 -2020-10-25 15:00:00,79.17,99.04700000000001,37.385999999999996,31.177 -2020-10-25 15:15:00,80.91,99.631,37.385999999999996,31.177 -2020-10-25 15:30:00,81.52,100.13,37.385999999999996,31.177 -2020-10-25 15:45:00,82.75,100.765,37.385999999999996,31.177 -2020-10-25 16:00:00,82.73,101.178,46.618,31.177 -2020-10-25 16:15:00,84.13,101.86399999999999,46.618,31.177 -2020-10-25 16:30:00,88.98,102.837,46.618,31.177 -2020-10-25 16:45:00,90.57,102.025,46.618,31.177 -2020-10-25 17:00:00,95.12,102.66799999999999,50.111000000000004,31.177 -2020-10-25 17:15:00,97.69,103.867,50.111000000000004,31.177 -2020-10-25 17:30:00,96.42,104.118,50.111000000000004,31.177 -2020-10-25 17:45:00,98.46,105.537,50.111000000000004,31.177 -2020-10-25 18:00:00,103.58,107.33200000000001,50.25,31.177 -2020-10-25 18:15:00,101.93,108.426,50.25,31.177 -2020-10-25 18:30:00,102.54,107.414,50.25,31.177 -2020-10-25 18:45:00,100.8,108.6,50.25,31.177 -2020-10-25 19:00:00,96.36,111.086,44.265,31.177 -2020-10-25 19:15:00,94.85,109.73100000000001,44.265,31.177 -2020-10-25 19:30:00,95.47,109.225,44.265,31.177 -2020-10-25 19:45:00,92.72,109.13799999999999,44.265,31.177 -2020-10-25 20:00:00,95.91,107.9,39.717,31.177 -2020-10-25 20:15:00,98.51,106.665,39.717,31.177 -2020-10-25 20:30:00,93.22,106.344,39.717,31.177 -2020-10-25 20:45:00,87.29,103.89200000000001,39.717,31.177 -2020-10-25 21:00:00,89.17,100.29299999999999,39.224000000000004,31.177 -2020-10-25 21:15:00,87.17,100.712,39.224000000000004,31.177 -2020-10-25 21:30:00,90.1,100.595,39.224000000000004,31.177 -2020-10-25 21:45:00,88.38,98.32,39.224000000000004,31.177 -2020-10-25 22:00:00,85.43,95.274,33.518,31.177 -2020-10-25 22:15:00,83.95,91.87799999999999,33.518,31.177 -2020-10-25 22:30:00,78.09,87.24700000000001,33.518,31.177 -2020-10-25 22:45:00,78.18,83.186,33.518,31.177 -2020-10-25 23:00:00,78.0,77.503,33.518,31.177 -2020-10-25 23:15:00,83.24,76.22399999999999,33.518,31.177 -2020-10-25 23:30:00,81.26,73.999,33.518,31.177 -2020-10-25 23:45:00,79.19,73.21600000000001,33.518,31.177 -2020-10-26 00:00:00,70.49,62.706,34.301,31.349 -2020-10-26 00:15:00,74.87,62.949,34.301,31.349 -2020-10-26 00:30:00,78.69,62.718999999999994,34.301,31.349 -2020-10-26 00:45:00,81.84,62.592,34.301,31.349 -2020-10-26 01:00:00,75.6,63.79600000000001,34.143,31.349 -2020-10-26 01:15:00,71.72,63.619,34.143,31.349 -2020-10-26 01:30:00,75.5,63.11600000000001,34.143,31.349 -2020-10-26 01:45:00,77.08,62.928999999999995,34.143,31.349 -2020-10-26 02:00:00,76.26,64.08800000000001,33.650999999999996,31.349 -2020-10-26 02:15:00,72.33,63.838,33.650999999999996,31.349 -2020-10-26 02:30:00,72.88,64.815,33.650999999999996,31.349 -2020-10-26 02:45:00,75.86,65.26100000000001,33.650999999999996,31.349 -2020-10-26 03:00:00,73.71,68.73,32.599000000000004,31.349 -2020-10-26 03:15:00,76.24,70.116,32.599000000000004,31.349 -2020-10-26 03:30:00,71.02,70.158,32.599000000000004,31.349 -2020-10-26 03:45:00,78.2,71.275,32.599000000000004,31.349 -2020-10-26 04:00:00,79.63,82.025,33.785,31.349 -2020-10-26 04:15:00,78.32,91.931,33.785,31.349 -2020-10-26 04:30:00,77.2,91.984,33.785,31.349 -2020-10-26 04:45:00,76.44,93.176,33.785,31.349 -2020-10-26 05:00:00,81.4,119.296,41.285,31.349 -2020-10-26 05:15:00,86.72,144.952,41.285,31.349 -2020-10-26 05:30:00,94.41,139.7,41.285,31.349 -2020-10-26 05:45:00,100.77,131.119,41.285,31.349 -2020-10-26 06:00:00,111.1,130.539,60.486000000000004,31.349 -2020-10-26 06:15:00,109.54,133.99,60.486000000000004,31.349 -2020-10-26 06:30:00,116.42,133.387,60.486000000000004,31.349 -2020-10-26 06:45:00,125.54,134.597,60.486000000000004,31.349 -2020-10-26 07:00:00,128.36,135.40200000000002,74.012,31.349 -2020-10-26 07:15:00,127.02,138.408,74.012,31.349 -2020-10-26 07:30:00,129.02,139.22899999999998,74.012,31.349 -2020-10-26 07:45:00,130.69,139.71200000000002,74.012,31.349 -2020-10-26 08:00:00,133.0,139.673,69.569,31.349 -2020-10-26 08:15:00,130.26,139.901,69.569,31.349 -2020-10-26 08:30:00,131.6,137.61700000000002,69.569,31.349 -2020-10-26 08:45:00,131.65,136.181,69.569,31.349 -2020-10-26 09:00:00,132.1,132.289,66.152,31.349 -2020-10-26 09:15:00,133.34,129.095,66.152,31.349 -2020-10-26 09:30:00,133.68,128.327,66.152,31.349 -2020-10-26 09:45:00,134.8,127.196,66.152,31.349 -2020-10-26 10:00:00,135.34,124.896,62.923,31.349 -2020-10-26 10:15:00,134.92,124.164,62.923,31.349 -2020-10-26 10:30:00,135.66,122.366,62.923,31.349 -2020-10-26 10:45:00,136.84,121.25299999999999,62.923,31.349 -2020-10-26 11:00:00,136.57,117.12899999999999,61.522,31.349 -2020-10-26 11:15:00,138.31,117.39399999999999,61.522,31.349 -2020-10-26 11:30:00,137.5,118.719,61.522,31.349 -2020-10-26 11:45:00,136.55,118.765,61.522,31.349 -2020-10-26 12:00:00,132.39,115.801,58.632,31.349 -2020-10-26 12:15:00,133.44,115.056,58.632,31.349 -2020-10-26 12:30:00,136.05,114.31200000000001,58.632,31.349 -2020-10-26 12:45:00,136.12,114.579,58.632,31.349 -2020-10-26 13:00:00,131.73,114.663,59.06,31.349 -2020-10-26 13:15:00,133.41,113.785,59.06,31.349 -2020-10-26 13:30:00,132.28,112.412,59.06,31.349 -2020-10-26 13:45:00,129.34,112.303,59.06,31.349 -2020-10-26 14:00:00,128.88,111.329,59.791000000000004,31.349 -2020-10-26 14:15:00,130.35,111.32799999999999,59.791000000000004,31.349 -2020-10-26 14:30:00,130.63,110.51799999999999,59.791000000000004,31.349 -2020-10-26 14:45:00,129.71,110.73700000000001,59.791000000000004,31.349 -2020-10-26 15:00:00,133.57,110.221,61.148,31.349 -2020-10-26 15:15:00,130.84,109.76799999999999,61.148,31.349 -2020-10-26 15:30:00,129.48,110.12,61.148,31.349 -2020-10-26 15:45:00,127.89,110.32600000000001,61.148,31.349 -2020-10-26 16:00:00,129.63,111.01299999999999,66.009,31.349 -2020-10-26 16:15:00,130.73,111.325,66.009,31.349 -2020-10-26 16:30:00,128.87,111.488,66.009,31.349 -2020-10-26 16:45:00,131.75,110.07600000000001,66.009,31.349 -2020-10-26 17:00:00,135.6,109.988,73.683,31.349 -2020-10-26 17:15:00,135.29,110.84899999999999,73.683,31.349 -2020-10-26 17:30:00,138.5,110.64399999999999,73.683,31.349 -2020-10-26 17:45:00,138.2,111.103,73.683,31.349 -2020-10-26 18:00:00,139.43,112.59899999999999,72.848,31.349 -2020-10-26 18:15:00,137.29,111.71,72.848,31.349 -2020-10-26 18:30:00,135.93,110.73100000000001,72.848,31.349 -2020-10-26 18:45:00,133.94,113.649,72.848,31.349 -2020-10-26 19:00:00,131.63,115.131,71.139,31.349 -2020-10-26 19:15:00,128.86,113.73299999999999,71.139,31.349 -2020-10-26 19:30:00,126.98,113.314,71.139,31.349 -2020-10-26 19:45:00,125.67,112.5,71.139,31.349 -2020-10-26 20:00:00,117.03,109.485,69.667,31.349 -2020-10-26 20:15:00,114.91,107.57,69.667,31.349 -2020-10-26 20:30:00,114.37,106.48299999999999,69.667,31.349 -2020-10-26 20:45:00,109.01,104.954,69.667,31.349 -2020-10-26 21:00:00,112.26,101.359,61.166000000000004,31.349 -2020-10-26 21:15:00,112.02,101.35799999999999,61.166000000000004,31.349 -2020-10-26 21:30:00,107.75,101.01100000000001,61.166000000000004,31.349 -2020-10-26 21:45:00,102.71,98.35700000000001,61.166000000000004,31.349 -2020-10-26 22:00:00,96.59,92.899,52.772,31.349 -2020-10-26 22:15:00,95.82,89.686,52.772,31.349 -2020-10-26 22:30:00,95.29,78.227,52.772,31.349 -2020-10-26 22:45:00,98.09,71.666,52.772,31.349 -2020-10-26 23:00:00,93.54,66.396,45.136,31.349 -2020-10-26 23:15:00,86.6,65.73100000000001,45.136,31.349 -2020-10-26 23:30:00,84.41,64.919,45.136,31.349 -2020-10-26 23:45:00,88.99,65.25399999999999,45.136,31.349 -2020-10-27 00:00:00,81.74,61.629,47.35,31.349 -2020-10-27 00:15:00,82.4,62.966,47.35,31.349 -2020-10-27 00:30:00,77.99,62.614,47.35,31.349 -2020-10-27 00:45:00,77.23,62.376000000000005,47.35,31.349 -2020-10-27 01:00:00,80.95,63.243,43.424,31.349 -2020-10-27 01:15:00,82.6,62.918,43.424,31.349 -2020-10-27 01:30:00,79.87,62.45399999999999,43.424,31.349 -2020-10-27 01:45:00,72.99,62.169,43.424,31.349 -2020-10-27 02:00:00,72.38,63.107,41.778999999999996,31.349 -2020-10-27 02:15:00,71.66,63.367,41.778999999999996,31.349 -2020-10-27 02:30:00,79.74,63.854,41.778999999999996,31.349 -2020-10-27 02:45:00,81.55,64.459,41.778999999999996,31.349 -2020-10-27 03:00:00,82.6,67.071,40.771,31.349 -2020-10-27 03:15:00,75.83,68.518,40.771,31.349 -2020-10-27 03:30:00,74.84,68.78,40.771,31.349 -2020-10-27 03:45:00,75.64,69.439,40.771,31.349 -2020-10-27 04:00:00,78.97,79.508,41.816,31.349 -2020-10-27 04:15:00,82.85,89.28299999999999,41.816,31.349 -2020-10-27 04:30:00,84.37,89.12799999999999,41.816,31.349 -2020-10-27 04:45:00,85.9,91.14399999999999,41.816,31.349 -2020-10-27 05:00:00,85.7,120.477,45.842,31.349 -2020-10-27 05:15:00,86.54,146.40200000000002,45.842,31.349 -2020-10-27 05:30:00,89.88,140.503,45.842,31.349 -2020-10-27 05:45:00,92.99,131.537,45.842,31.349 -2020-10-27 06:00:00,102.93,130.881,59.12,31.349 -2020-10-27 06:15:00,108.45,135.22899999999998,59.12,31.349 -2020-10-27 06:30:00,113.1,134.179,59.12,31.349 -2020-10-27 06:45:00,115.04,134.792,59.12,31.349 -2020-10-27 07:00:00,122.18,135.57399999999998,70.33,31.349 -2020-10-27 07:15:00,119.35,138.393,70.33,31.349 -2020-10-27 07:30:00,121.17,138.977,70.33,31.349 -2020-10-27 07:45:00,122.32,139.118,70.33,31.349 -2020-10-27 08:00:00,123.86,139.123,67.788,31.349 -2020-10-27 08:15:00,121.9,138.599,67.788,31.349 -2020-10-27 08:30:00,120.05,136.322,67.788,31.349 -2020-10-27 08:45:00,119.09,134.313,67.788,31.349 -2020-10-27 09:00:00,117.83,130.122,62.622,31.349 -2020-10-27 09:15:00,117.56,127.71700000000001,62.622,31.349 -2020-10-27 09:30:00,117.58,127.595,62.622,31.349 -2020-10-27 09:45:00,116.75,126.899,62.622,31.349 -2020-10-27 10:00:00,115.99,123.69200000000001,60.887,31.349 -2020-10-27 10:15:00,115.96,122.279,60.887,31.349 -2020-10-27 10:30:00,114.65,120.57,60.887,31.349 -2020-10-27 10:45:00,115.67,120.009,60.887,31.349 -2020-10-27 11:00:00,114.34,116.712,59.812,31.349 -2020-10-27 11:15:00,114.91,116.93299999999999,59.812,31.349 -2020-10-27 11:30:00,116.28,117.145,59.812,31.349 -2020-10-27 11:45:00,115.75,117.391,59.812,31.349 -2020-10-27 12:00:00,113.14,113.553,56.614,31.349 -2020-10-27 12:15:00,114.82,112.695,56.614,31.349 -2020-10-27 12:30:00,122.4,112.73200000000001,56.614,31.349 -2020-10-27 12:45:00,118.13,113.09700000000001,56.614,31.349 -2020-10-27 13:00:00,120.98,112.76,56.824,31.349 -2020-10-27 13:15:00,123.1,112.37700000000001,56.824,31.349 -2020-10-27 13:30:00,120.06,111.669,56.824,31.349 -2020-10-27 13:45:00,116.95,111.295,56.824,31.349 -2020-10-27 14:00:00,111.93,110.604,57.623999999999995,31.349 -2020-10-27 14:15:00,110.15,110.633,57.623999999999995,31.349 -2020-10-27 14:30:00,114.67,110.348,57.623999999999995,31.349 -2020-10-27 14:45:00,116.3,110.22,57.623999999999995,31.349 -2020-10-27 15:00:00,118.01,109.37299999999999,59.724,31.349 -2020-10-27 15:15:00,119.23,109.48100000000001,59.724,31.349 -2020-10-27 15:30:00,118.98,109.921,59.724,31.349 -2020-10-27 15:45:00,123.51,110.039,59.724,31.349 -2020-10-27 16:00:00,124.34,110.655,61.64,31.349 -2020-10-27 16:15:00,123.37,111.288,61.64,31.349 -2020-10-27 16:30:00,126.21,111.66,61.64,31.349 -2020-10-27 16:45:00,127.32,110.74799999999999,61.64,31.349 -2020-10-27 17:00:00,132.95,111.009,68.962,31.349 -2020-10-27 17:15:00,130.54,112.081,68.962,31.349 -2020-10-27 17:30:00,133.6,112.05799999999999,68.962,31.349 -2020-10-27 17:45:00,132.78,112.322,68.962,31.349 -2020-10-27 18:00:00,133.9,113.34,69.149,31.349 -2020-10-27 18:15:00,131.39,112.79799999999999,69.149,31.349 -2020-10-27 18:30:00,130.49,111.552,69.149,31.349 -2020-10-27 18:45:00,129.9,114.807,69.149,31.349 -2020-10-27 19:00:00,126.61,115.82600000000001,68.832,31.349 -2020-10-27 19:15:00,124.87,114.351,68.832,31.349 -2020-10-27 19:30:00,122.2,113.476,68.832,31.349 -2020-10-27 19:45:00,121.07,112.803,68.832,31.349 -2020-10-27 20:00:00,114.52,110.06200000000001,66.403,31.349 -2020-10-27 20:15:00,112.08,107.171,66.403,31.349 -2020-10-27 20:30:00,108.46,106.601,66.403,31.349 -2020-10-27 20:45:00,105.79,104.941,66.403,31.349 -2020-10-27 21:00:00,98.15,101.363,57.352,31.349 -2020-10-27 21:15:00,94.41,101.166,57.352,31.349 -2020-10-27 21:30:00,92.13,100.529,57.352,31.349 -2020-10-27 21:45:00,91.04,98.061,57.352,31.349 -2020-10-27 22:00:00,84.86,93.559,51.148999999999994,31.349 -2020-10-27 22:15:00,81.56,90.044,51.148999999999994,31.349 -2020-10-27 22:30:00,78.97,78.791,51.148999999999994,31.349 -2020-10-27 22:45:00,75.04,72.378,51.148999999999994,31.349 -2020-10-27 23:00:00,69.33,66.797,41.8,31.349 -2020-10-27 23:15:00,72.04,66.328,41.8,31.349 -2020-10-27 23:30:00,67.62,65.33,41.8,31.349 -2020-10-27 23:45:00,70.34,65.508,41.8,31.349 -2020-10-28 00:00:00,66.64,61.961999999999996,42.269,31.349 -2020-10-28 00:15:00,63.47,63.285,42.269,31.349 -2020-10-28 00:30:00,62.17,62.938,42.269,31.349 -2020-10-28 00:45:00,59.22,62.693000000000005,42.269,31.349 -2020-10-28 01:00:00,59.46,63.573,38.527,31.349 -2020-10-28 01:15:00,59.73,63.266000000000005,38.527,31.349 -2020-10-28 01:30:00,57.69,62.818000000000005,38.527,31.349 -2020-10-28 01:45:00,59.75,62.528,38.527,31.349 -2020-10-28 02:00:00,58.9,63.477,36.393,31.349 -2020-10-28 02:15:00,57.99,63.747,36.393,31.349 -2020-10-28 02:30:00,58.66,64.222,36.393,31.349 -2020-10-28 02:45:00,59.76,64.822,36.393,31.349 -2020-10-28 03:00:00,58.64,67.422,36.167,31.349 -2020-10-28 03:15:00,59.18,68.89,36.167,31.349 -2020-10-28 03:30:00,59.1,69.155,36.167,31.349 -2020-10-28 03:45:00,59.8,69.797,36.167,31.349 -2020-10-28 04:00:00,57.35,79.88600000000001,38.092,31.349 -2020-10-28 04:15:00,60.36,89.689,38.092,31.349 -2020-10-28 04:30:00,61.45,89.531,38.092,31.349 -2020-10-28 04:45:00,62.49,91.555,38.092,31.349 -2020-10-28 05:00:00,64.56,120.95700000000001,42.268,31.349 -2020-10-28 05:15:00,64.41,146.945,42.268,31.349 -2020-10-28 05:30:00,63.43,141.032,42.268,31.349 -2020-10-28 05:45:00,60.47,132.036,42.268,31.349 -2020-10-28 06:00:00,64.37,131.36700000000002,60.158,31.349 -2020-10-28 06:15:00,65.72,135.72799999999998,60.158,31.349 -2020-10-28 06:30:00,65.45,134.7,60.158,31.349 -2020-10-28 06:45:00,65.86,135.322,60.158,31.349 -2020-10-28 07:00:00,68.96,136.10399999999998,74.792,31.349 -2020-10-28 07:15:00,70.01,138.934,74.792,31.349 -2020-10-28 07:30:00,71.67,139.547,74.792,31.349 -2020-10-28 07:45:00,73.31,139.689,74.792,31.349 -2020-10-28 08:00:00,76.29,139.70600000000002,70.499,31.349 -2020-10-28 08:15:00,75.92,139.159,70.499,31.349 -2020-10-28 08:30:00,76.39,136.907,70.499,31.349 -2020-10-28 08:45:00,76.44,134.873,70.499,31.349 -2020-10-28 09:00:00,75.16,130.675,68.892,31.349 -2020-10-28 09:15:00,75.44,128.268,68.892,31.349 -2020-10-28 09:30:00,72.53,128.134,68.892,31.349 -2020-10-28 09:45:00,73.66,127.415,68.892,31.349 -2020-10-28 10:00:00,72.88,124.2,66.88600000000001,31.349 -2020-10-28 10:15:00,69.24,122.749,66.88600000000001,31.349 -2020-10-28 10:30:00,71.4,121.02,66.88600000000001,31.349 -2020-10-28 10:45:00,72.71,120.443,66.88600000000001,31.349 -2020-10-28 11:00:00,72.82,117.149,66.187,31.349 -2020-10-28 11:15:00,74.56,117.351,66.187,31.349 -2020-10-28 11:30:00,75.37,117.56200000000001,66.187,31.349 -2020-10-28 11:45:00,76.16,117.794,66.187,31.349 -2020-10-28 12:00:00,73.91,113.936,62.18,31.349 -2020-10-28 12:15:00,72.6,113.075,62.18,31.349 -2020-10-28 12:30:00,71.55,113.145,62.18,31.349 -2020-10-28 12:45:00,70.09,113.51,62.18,31.349 -2020-10-28 13:00:00,70.36,113.141,62.23,31.349 -2020-10-28 13:15:00,67.51,112.76299999999999,62.23,31.349 -2020-10-28 13:30:00,65.99,112.054,62.23,31.349 -2020-10-28 13:45:00,66.82,111.676,62.23,31.349 -2020-10-28 14:00:00,67.12,110.936,63.721000000000004,31.349 -2020-10-28 14:15:00,67.37,110.98,63.721000000000004,31.349 -2020-10-28 14:30:00,69.07,110.73200000000001,63.721000000000004,31.349 -2020-10-28 14:45:00,72.24,110.602,63.721000000000004,31.349 -2020-10-28 15:00:00,74.87,109.736,66.523,31.349 -2020-10-28 15:15:00,75.32,109.86,66.523,31.349 -2020-10-28 15:30:00,79.14,110.337,66.523,31.349 -2020-10-28 15:45:00,80.15,110.46799999999999,66.523,31.349 -2020-10-28 16:00:00,83.7,111.055,69.679,31.349 -2020-10-28 16:15:00,83.65,111.71,69.679,31.349 -2020-10-28 16:30:00,85.79,112.07799999999999,69.679,31.349 -2020-10-28 16:45:00,87.82,111.21600000000001,69.679,31.349 -2020-10-28 17:00:00,95.0,111.43700000000001,75.04,31.349 -2020-10-28 17:15:00,96.91,112.52799999999999,75.04,31.349 -2020-10-28 17:30:00,100.33,112.508,75.04,31.349 -2020-10-28 17:45:00,101.95,112.79,75.04,31.349 -2020-10-28 18:00:00,102.58,113.801,75.915,31.349 -2020-10-28 18:15:00,101.4,113.235,75.915,31.349 -2020-10-28 18:30:00,100.78,111.999,75.915,31.349 -2020-10-28 18:45:00,99.5,115.25,75.915,31.349 -2020-10-28 19:00:00,96.55,116.27600000000001,74.66,31.349 -2020-10-28 19:15:00,96.95,114.795,74.66,31.349 -2020-10-28 19:30:00,94.44,113.90700000000001,74.66,31.349 -2020-10-28 19:45:00,93.81,113.212,74.66,31.349 -2020-10-28 20:00:00,97.85,110.491,71.204,31.349 -2020-10-28 20:15:00,98.96,107.59299999999999,71.204,31.349 -2020-10-28 20:30:00,93.72,106.994,71.204,31.349 -2020-10-28 20:45:00,96.99,105.316,71.204,31.349 -2020-10-28 21:00:00,88.01,101.735,61.052,31.349 -2020-10-28 21:15:00,91.89,101.527,61.052,31.349 -2020-10-28 21:30:00,86.26,100.899,61.052,31.349 -2020-10-28 21:45:00,85.22,98.413,61.052,31.349 -2020-10-28 22:00:00,82.78,93.904,54.691,31.349 -2020-10-28 22:15:00,84.41,90.37200000000001,54.691,31.349 -2020-10-28 22:30:00,80.28,79.148,54.691,31.349 -2020-10-28 22:45:00,82.44,72.742,54.691,31.349 -2020-10-28 23:00:00,82.46,67.175,45.18,31.349 -2020-10-28 23:15:00,86.27,66.678,45.18,31.349 -2020-10-28 23:30:00,83.78,65.682,45.18,31.349 -2020-10-28 23:45:00,82.95,65.851,45.18,31.349 -2020-10-29 00:00:00,77.65,62.294,42.746,31.349 -2020-10-29 00:15:00,79.71,63.602,42.746,31.349 -2020-10-29 00:30:00,79.87,63.261,42.746,31.349 -2020-10-29 00:45:00,70.04,63.008,42.746,31.349 -2020-10-29 01:00:00,74.52,63.9,40.025999999999996,31.349 -2020-10-29 01:15:00,77.14,63.611000000000004,40.025999999999996,31.349 -2020-10-29 01:30:00,78.1,63.178999999999995,40.025999999999996,31.349 -2020-10-29 01:45:00,74.52,62.885,40.025999999999996,31.349 -2020-10-29 02:00:00,74.6,63.843999999999994,38.154,31.349 -2020-10-29 02:15:00,78.3,64.126,38.154,31.349 -2020-10-29 02:30:00,77.12,64.58800000000001,38.154,31.349 -2020-10-29 02:45:00,73.83,65.185,38.154,31.349 -2020-10-29 03:00:00,77.13,67.771,37.575,31.349 -2020-10-29 03:15:00,78.81,69.26100000000001,37.575,31.349 -2020-10-29 03:30:00,79.06,69.528,37.575,31.349 -2020-10-29 03:45:00,73.2,70.152,37.575,31.349 -2020-10-29 04:00:00,76.0,80.26,39.154,31.349 -2020-10-29 04:15:00,81.59,90.094,39.154,31.349 -2020-10-29 04:30:00,84.79,89.934,39.154,31.349 -2020-10-29 04:45:00,83.0,91.965,39.154,31.349 -2020-10-29 05:00:00,83.0,121.43299999999999,44.085,31.349 -2020-10-29 05:15:00,86.43,147.484,44.085,31.349 -2020-10-29 05:30:00,90.66,141.559,44.085,31.349 -2020-10-29 05:45:00,100.19,132.532,44.085,31.349 -2020-10-29 06:00:00,111.83,131.852,57.49,31.349 -2020-10-29 06:15:00,117.74,136.226,57.49,31.349 -2020-10-29 06:30:00,119.59,135.217,57.49,31.349 -2020-10-29 06:45:00,115.34,135.85,57.49,31.349 -2020-10-29 07:00:00,122.6,136.632,73.617,31.349 -2020-10-29 07:15:00,123.1,139.474,73.617,31.349 -2020-10-29 07:30:00,124.09,140.114,73.617,31.349 -2020-10-29 07:45:00,126.48,140.257,73.617,31.349 -2020-10-29 08:00:00,126.46,140.285,69.281,31.349 -2020-10-29 08:15:00,123.29,139.715,69.281,31.349 -2020-10-29 08:30:00,119.21,137.487,69.281,31.349 -2020-10-29 08:45:00,118.47,135.429,69.281,31.349 -2020-10-29 09:00:00,118.0,131.224,63.926,31.349 -2020-10-29 09:15:00,122.76,128.815,63.926,31.349 -2020-10-29 09:30:00,119.63,128.67,63.926,31.349 -2020-10-29 09:45:00,122.87,127.926,63.926,31.349 -2020-10-29 10:00:00,122.23,124.705,59.442,31.349 -2020-10-29 10:15:00,119.89,123.21600000000001,59.442,31.349 -2020-10-29 10:30:00,124.6,121.46700000000001,59.442,31.349 -2020-10-29 10:45:00,131.0,120.874,59.442,31.349 -2020-10-29 11:00:00,127.83,117.583,56.771,31.349 -2020-10-29 11:15:00,128.86,117.766,56.771,31.349 -2020-10-29 11:30:00,130.16,117.978,56.771,31.349 -2020-10-29 11:45:00,131.01,118.196,56.771,31.349 -2020-10-29 12:00:00,130.57,114.31700000000001,53.701,31.349 -2020-10-29 12:15:00,130.91,113.45200000000001,53.701,31.349 -2020-10-29 12:30:00,133.3,113.556,53.701,31.349 -2020-10-29 12:45:00,130.48,113.921,53.701,31.349 -2020-10-29 13:00:00,133.45,113.51899999999999,52.364,31.349 -2020-10-29 13:15:00,130.71,113.147,52.364,31.349 -2020-10-29 13:30:00,128.95,112.436,52.364,31.349 -2020-10-29 13:45:00,129.8,112.055,52.364,31.349 -2020-10-29 14:00:00,130.29,111.265,53.419,31.349 -2020-10-29 14:15:00,129.67,111.324,53.419,31.349 -2020-10-29 14:30:00,128.02,111.11200000000001,53.419,31.349 -2020-10-29 14:45:00,130.31,110.98299999999999,53.419,31.349 -2020-10-29 15:00:00,127.71,110.096,56.744,31.349 -2020-10-29 15:15:00,126.9,110.236,56.744,31.349 -2020-10-29 15:30:00,126.36,110.75,56.744,31.349 -2020-10-29 15:45:00,125.0,110.89399999999999,56.744,31.349 -2020-10-29 16:00:00,125.03,111.45200000000001,60.458,31.349 -2020-10-29 16:15:00,124.56,112.12799999999999,60.458,31.349 -2020-10-29 16:30:00,124.0,112.494,60.458,31.349 -2020-10-29 16:45:00,128.25,111.682,60.458,31.349 -2020-10-29 17:00:00,134.32,111.861,66.295,31.349 -2020-10-29 17:15:00,137.45,112.973,66.295,31.349 -2020-10-29 17:30:00,135.07,112.95700000000001,66.295,31.349 -2020-10-29 17:45:00,135.2,113.255,66.295,31.349 -2020-10-29 18:00:00,134.97,114.259,68.468,31.349 -2020-10-29 18:15:00,133.97,113.67,68.468,31.349 -2020-10-29 18:30:00,134.41,112.444,68.468,31.349 -2020-10-29 18:45:00,130.82,115.691,68.468,31.349 -2020-10-29 19:00:00,127.46,116.72399999999999,66.39399999999999,31.349 -2020-10-29 19:15:00,126.41,115.23700000000001,66.39399999999999,31.349 -2020-10-29 19:30:00,124.92,114.338,66.39399999999999,31.349 -2020-10-29 19:45:00,123.49,113.619,66.39399999999999,31.349 -2020-10-29 20:00:00,123.79,110.91799999999999,63.183,31.349 -2020-10-29 20:15:00,121.13,108.01299999999999,63.183,31.349 -2020-10-29 20:30:00,115.55,107.385,63.183,31.349 -2020-10-29 20:45:00,110.22,105.69,63.183,31.349 -2020-10-29 21:00:00,106.12,102.105,55.133,31.349 -2020-10-29 21:15:00,108.93,101.885,55.133,31.349 -2020-10-29 21:30:00,108.46,101.26799999999999,55.133,31.349 -2020-10-29 21:45:00,104.99,98.762,55.133,31.349 -2020-10-29 22:00:00,95.45,94.24700000000001,50.111999999999995,31.349 -2020-10-29 22:15:00,94.84,90.696,50.111999999999995,31.349 -2020-10-29 22:30:00,95.34,79.503,50.111999999999995,31.349 -2020-10-29 22:45:00,96.16,73.104,50.111999999999995,31.349 -2020-10-29 23:00:00,90.51,67.551,44.536,31.349 -2020-10-29 23:15:00,87.23,67.025,44.536,31.349 -2020-10-29 23:30:00,86.68,66.032,44.536,31.349 -2020-10-29 23:45:00,87.36,66.191,44.536,31.349 -2020-10-30 00:00:00,85.77,61.242,42.291000000000004,31.349 -2020-10-30 00:15:00,82.09,62.739,42.291000000000004,31.349 -2020-10-30 00:30:00,77.43,62.463,42.291000000000004,31.349 -2020-10-30 00:45:00,82.66,62.458,42.291000000000004,31.349 -2020-10-30 01:00:00,80.41,63.026,41.008,31.349 -2020-10-30 01:15:00,82.12,62.924,41.008,31.349 -2020-10-30 01:30:00,75.47,62.736000000000004,41.008,31.349 -2020-10-30 01:45:00,77.24,62.371,41.008,31.349 -2020-10-30 02:00:00,74.28,63.827,39.521,31.349 -2020-10-30 02:15:00,74.78,64.03699999999999,39.521,31.349 -2020-10-30 02:30:00,79.52,65.142,39.521,31.349 -2020-10-30 02:45:00,81.93,65.428,39.521,31.349 -2020-10-30 03:00:00,80.63,67.885,39.812,31.349 -2020-10-30 03:15:00,75.97,69.27600000000001,39.812,31.349 -2020-10-30 03:30:00,78.26,69.422,39.812,31.349 -2020-10-30 03:45:00,82.39,70.611,39.812,31.349 -2020-10-30 04:00:00,82.18,80.916,41.22,31.349 -2020-10-30 04:15:00,78.3,89.902,41.22,31.349 -2020-10-30 04:30:00,84.06,90.31200000000001,41.22,31.349 -2020-10-30 04:45:00,88.56,91.48200000000001,41.22,31.349 -2020-10-30 05:00:00,92.62,120.163,45.115,31.349 -2020-10-30 05:15:00,89.69,147.474,45.115,31.349 -2020-10-30 05:30:00,95.75,142.15200000000002,45.115,31.349 -2020-10-30 05:45:00,102.23,132.844,45.115,31.349 -2020-10-30 06:00:00,113.59,132.496,59.06100000000001,31.349 -2020-10-30 06:15:00,111.04,136.235,59.06100000000001,31.349 -2020-10-30 06:30:00,113.71,134.79399999999998,59.06100000000001,31.349 -2020-10-30 06:45:00,116.04,136.179,59.06100000000001,31.349 -2020-10-30 07:00:00,119.06,136.855,71.874,31.349 -2020-10-30 07:15:00,120.84,140.64700000000002,71.874,31.349 -2020-10-30 07:30:00,122.72,140.326,71.874,31.349 -2020-10-30 07:45:00,122.52,139.918,71.874,31.349 -2020-10-30 08:00:00,125.5,139.739,68.439,31.349 -2020-10-30 08:15:00,124.47,139.237,68.439,31.349 -2020-10-30 08:30:00,127.07,137.506,68.439,31.349 -2020-10-30 08:45:00,127.1,134.52700000000002,68.439,31.349 -2020-10-30 09:00:00,128.83,129.542,65.523,31.349 -2020-10-30 09:15:00,129.4,128.264,65.523,31.349 -2020-10-30 09:30:00,128.5,127.581,65.523,31.349 -2020-10-30 09:45:00,127.31,126.919,65.523,31.349 -2020-10-30 10:00:00,126.26,122.965,62.005,31.349 -2020-10-30 10:15:00,124.62,121.74,62.005,31.349 -2020-10-30 10:30:00,123.49,120.156,62.005,31.349 -2020-10-30 10:45:00,124.03,119.26,62.005,31.349 -2020-10-30 11:00:00,124.17,116.04299999999999,60.351000000000006,31.349 -2020-10-30 11:15:00,126.28,115.26100000000001,60.351000000000006,31.349 -2020-10-30 11:30:00,123.52,116.325,60.351000000000006,31.349 -2020-10-30 11:45:00,123.86,116.17299999999999,60.351000000000006,31.349 -2020-10-30 12:00:00,121.54,113.084,55.331,31.349 -2020-10-30 12:15:00,125.28,110.75,55.331,31.349 -2020-10-30 12:30:00,124.06,111.024,55.331,31.349 -2020-10-30 12:45:00,121.96,111.383,55.331,31.349 -2020-10-30 13:00:00,118.71,111.73299999999999,53.361999999999995,31.349 -2020-10-30 13:15:00,120.56,111.917,53.361999999999995,31.349 -2020-10-30 13:30:00,118.3,111.51799999999999,53.361999999999995,31.349 -2020-10-30 13:45:00,120.01,111.213,53.361999999999995,31.349 -2020-10-30 14:00:00,117.73,109.399,51.708,31.349 -2020-10-30 14:15:00,118.43,109.535,51.708,31.349 -2020-10-30 14:30:00,118.52,110.236,51.708,31.349 -2020-10-30 14:45:00,119.61,110.02,51.708,31.349 -2020-10-30 15:00:00,120.23,108.8,54.571000000000005,31.349 -2020-10-30 15:15:00,118.83,108.60799999999999,54.571000000000005,31.349 -2020-10-30 15:30:00,118.31,108.04899999999999,54.571000000000005,31.349 -2020-10-30 15:45:00,119.66,108.585,54.571000000000005,31.349 -2020-10-30 16:00:00,120.97,108.147,58.662,31.349 -2020-10-30 16:15:00,120.45,109.209,58.662,31.349 -2020-10-30 16:30:00,122.73,109.552,58.662,31.349 -2020-10-30 16:45:00,126.37,108.40799999999999,58.662,31.349 -2020-10-30 17:00:00,132.24,109.404,65.941,31.349 -2020-10-30 17:15:00,131.66,110.236,65.941,31.349 -2020-10-30 17:30:00,135.32,110.119,65.941,31.349 -2020-10-30 17:45:00,131.92,110.234,65.941,31.349 -2020-10-30 18:00:00,131.8,111.662,65.628,31.349 -2020-10-30 18:15:00,129.88,110.432,65.628,31.349 -2020-10-30 18:30:00,128.86,109.383,65.628,31.349 -2020-10-30 18:45:00,128.89,112.803,65.628,31.349 -2020-10-30 19:00:00,127.22,114.73,63.662,31.349 -2020-10-30 19:15:00,123.33,114.22,63.662,31.349 -2020-10-30 19:30:00,121.1,113.117,63.662,31.349 -2020-10-30 19:45:00,121.23,111.685,63.662,31.349 -2020-10-30 20:00:00,115.04,108.969,61.945,31.349 -2020-10-30 20:15:00,110.74,106.413,61.945,31.349 -2020-10-30 20:30:00,109.01,105.51799999999999,61.945,31.349 -2020-10-30 20:45:00,106.5,103.742,61.945,31.349 -2020-10-30 21:00:00,100.01,101.007,53.903,31.349 -2020-10-30 21:15:00,102.44,101.75200000000001,53.903,31.349 -2020-10-30 21:30:00,100.98,101.102,53.903,31.349 -2020-10-30 21:45:00,94.15,98.95299999999999,53.903,31.349 -2020-10-30 22:00:00,87.77,94.88,48.403999999999996,31.349 -2020-10-30 22:15:00,85.26,91.131,48.403999999999996,31.349 -2020-10-30 22:30:00,84.58,85.432,48.403999999999996,31.349 -2020-10-30 22:45:00,83.7,81.27199999999999,48.403999999999996,31.349 -2020-10-30 23:00:00,82.16,76.253,41.07,31.349 -2020-10-30 23:15:00,81.37,73.979,41.07,31.349 -2020-10-30 23:30:00,73.71,71.40899999999999,41.07,31.349 -2020-10-30 23:45:00,71.13,71.156,41.07,31.349 -2020-10-31 00:00:00,72.45,61.597,38.989000000000004,31.177 -2020-10-31 00:15:00,73.37,59.941,38.989000000000004,31.177 -2020-10-31 00:30:00,73.61,59.854,38.989000000000004,31.177 -2020-10-31 00:45:00,71.83,60.141000000000005,38.989000000000004,31.177 -2020-10-31 01:00:00,68.78,61.233000000000004,35.275,31.177 -2020-10-31 01:15:00,71.23,61.393,35.275,31.177 -2020-10-31 01:30:00,69.97,60.773,35.275,31.177 -2020-10-31 01:45:00,65.93,60.555,35.275,31.177 -2020-10-31 02:00:00,64.99,61.573,32.838,31.177 -2020-10-31 02:15:00,69.96,61.156000000000006,32.838,31.177 -2020-10-31 02:30:00,68.68,61.831,32.838,31.177 -2020-10-31 02:45:00,66.64,62.625,32.838,31.177 -2020-10-31 03:00:00,65.79,65.196,32.418,31.177 -2020-10-31 03:15:00,67.38,65.55199999999999,32.418,31.177 -2020-10-31 03:30:00,67.91,65.406,32.418,31.177 -2020-10-31 03:45:00,63.45,66.92699999999999,32.418,31.177 -2020-10-31 04:00:00,60.12,74.21600000000001,32.099000000000004,31.177 -2020-10-31 04:15:00,61.24,80.874,32.099000000000004,31.177 -2020-10-31 04:30:00,62.55,80.10300000000001,32.099000000000004,31.177 -2020-10-31 04:45:00,62.68,81.078,32.099000000000004,31.177 -2020-10-31 05:00:00,63.12,96.679,32.926,31.177 -2020-10-31 05:15:00,62.57,108.109,32.926,31.177 -2020-10-31 05:30:00,63.2,103.416,32.926,31.177 -2020-10-31 05:45:00,65.37,98.848,32.926,31.177 -2020-10-31 06:00:00,68.19,111.434,35.069,31.177 -2020-10-31 06:15:00,69.79,126.461,35.069,31.177 -2020-10-31 06:30:00,71.11,120.191,35.069,31.177 -2020-10-31 06:45:00,73.82,114.679,35.069,31.177 -2020-10-31 07:00:00,75.79,113.76,40.906,31.177 -2020-10-31 07:15:00,77.22,115.145,40.906,31.177 -2020-10-31 07:30:00,78.86,116.89,40.906,31.177 -2020-10-31 07:45:00,81.1,118.516,40.906,31.177 -2020-10-31 08:00:00,83.23,121.64399999999999,46.603,31.177 -2020-10-31 08:15:00,84.49,123.396,46.603,31.177 -2020-10-31 08:30:00,86.25,123.77,46.603,31.177 -2020-10-31 08:45:00,88.76,123.838,46.603,31.177 -2020-10-31 09:00:00,92.45,120.73899999999999,49.935,31.177 -2020-10-31 09:15:00,93.68,120.148,49.935,31.177 -2020-10-31 09:30:00,94.02,120.255,49.935,31.177 -2020-10-31 09:45:00,94.02,119.835,49.935,31.177 -2020-10-31 10:00:00,92.45,117.68,47.585,31.177 -2020-10-31 10:15:00,93.24,117.001,47.585,31.177 -2020-10-31 10:30:00,92.64,115.77,47.585,31.177 -2020-10-31 10:45:00,92.15,114.821,47.585,31.177 -2020-10-31 11:00:00,93.34,111.95100000000001,43.376999999999995,31.177 -2020-10-31 11:15:00,96.08,111.00299999999999,43.376999999999995,31.177 -2020-10-31 11:30:00,95.83,111.277,43.376999999999995,31.177 -2020-10-31 11:45:00,96.13,111.28,43.376999999999995,31.177 -2020-10-31 12:00:00,95.48,107.751,40.855,31.177 -2020-10-31 12:15:00,95.57,106.947,40.855,31.177 -2020-10-31 12:30:00,92.12,106.645,40.855,31.177 -2020-10-31 12:45:00,91.78,106.042,40.855,31.177 -2020-10-31 13:00:00,88.92,105.20100000000001,37.251,31.177 -2020-10-31 13:15:00,89.25,105.492,37.251,31.177 -2020-10-31 13:30:00,88.99,104.34899999999999,37.251,31.177 -2020-10-31 13:45:00,89.08,103.85700000000001,37.251,31.177 -2020-10-31 14:00:00,86.2,103.321,38.548,31.177 -2020-10-31 14:15:00,87.18,103.538,38.548,31.177 -2020-10-31 14:30:00,86.99,103.285,38.548,31.177 -2020-10-31 14:45:00,87.31,102.726,38.548,31.177 -2020-10-31 15:00:00,87.48,101.215,42.883,31.177 -2020-10-31 15:15:00,87.93,101.897,42.883,31.177 -2020-10-31 15:30:00,87.72,102.617,42.883,31.177 -2020-10-31 15:45:00,88.54,103.33200000000001,42.883,31.177 -2020-10-31 16:00:00,90.83,103.56700000000001,48.143,31.177 -2020-10-31 16:15:00,90.48,104.383,48.143,31.177 -2020-10-31 16:30:00,92.56,105.337,48.143,31.177 -2020-10-31 16:45:00,97.53,104.829,48.143,31.177 -2020-10-31 17:00:00,102.04,105.22399999999999,55.25,31.177 -2020-10-31 17:15:00,100.68,106.541,55.25,31.177 -2020-10-31 17:30:00,102.51,106.81700000000001,55.25,31.177 -2020-10-31 17:45:00,102.66,108.331,55.25,31.177 -2020-10-31 18:00:00,104.1,110.088,57.506,31.177 -2020-10-31 18:15:00,107.0,111.04,57.506,31.177 -2020-10-31 18:30:00,103.91,110.087,57.506,31.177 -2020-10-31 18:45:00,104.55,111.25,57.506,31.177 -2020-10-31 19:00:00,100.55,113.781,55.528999999999996,31.177 -2020-10-31 19:15:00,99.24,112.38600000000001,55.528999999999996,31.177 -2020-10-31 19:30:00,97.68,111.81,55.528999999999996,31.177 -2020-10-31 19:45:00,96.75,111.583,55.528999999999996,31.177 -2020-10-31 20:00:00,91.74,110.46700000000001,46.166000000000004,31.177 -2020-10-31 20:15:00,88.35,109.18799999999999,46.166000000000004,31.177 -2020-10-31 20:30:00,86.31,108.696,46.166000000000004,31.177 -2020-10-31 20:45:00,83.91,106.139,46.166000000000004,31.177 -2020-10-31 21:00:00,80.56,102.51899999999999,40.406,31.177 -2020-10-31 21:15:00,80.4,102.87,40.406,31.177 -2020-10-31 21:30:00,79.05,102.81200000000001,40.406,31.177 -2020-10-31 21:45:00,77.96,100.42200000000001,40.406,31.177 -2020-10-31 22:00:00,75.29,97.337,39.616,31.177 -2020-10-31 22:15:00,74.6,93.835,39.616,31.177 -2020-10-31 22:30:00,71.03,89.384,39.616,31.177 -2020-10-31 22:45:00,70.49,85.363,39.616,31.177 -2020-10-31 23:00:00,67.0,79.763,32.205,31.177 -2020-10-31 23:15:00,66.8,78.317,32.205,31.177 -2020-10-31 23:30:00,64.91,76.102,32.205,31.177 -2020-10-31 23:45:00,63.18,75.262,32.205,31.177 -2020-11-01 00:00:00,58.91,73.844,36.376,32.047 -2020-11-01 00:15:00,57.64,71.598,36.376,32.047 -2020-11-01 00:30:00,58.09,71.771,36.376,32.047 -2020-11-01 00:45:00,56.8,72.655,36.376,32.047 -2020-11-01 01:00:00,53.61,74.10300000000001,32.992,32.047 -2020-11-01 01:15:00,55.48,74.691,32.992,32.047 -2020-11-01 01:30:00,54.86,74.157,32.992,32.047 -2020-11-01 01:45:00,55.56,74.169,32.992,32.047 -2020-11-01 02:00:00,53.33,75.35600000000001,32.327,32.047 -2020-11-01 02:15:00,53.19,74.95,32.327,32.047 -2020-11-01 02:30:00,53.38,75.499,32.327,32.047 -2020-11-01 02:45:00,53.54,76.763,32.327,32.047 -2020-11-01 03:00:00,52.29,79.542,31.169,32.047 -2020-11-01 03:15:00,53.27,79.608,31.169,32.047 -2020-11-01 03:30:00,53.38,80.009,31.169,32.047 -2020-11-01 03:45:00,53.67,81.561,31.169,32.047 -2020-11-01 04:00:00,53.04,90.10600000000001,30.796,32.047 -2020-11-01 04:15:00,53.69,97.82600000000001,30.796,32.047 -2020-11-01 04:30:00,54.71,97.304,30.796,32.047 -2020-11-01 04:45:00,54.7,98.21600000000001,30.796,32.047 -2020-11-01 05:00:00,56.28,114.69200000000001,30.848000000000003,32.047 -2020-11-01 05:15:00,56.84,126.919,30.848000000000003,32.047 -2020-11-01 05:30:00,56.12,122.334,30.848000000000003,32.047 -2020-11-01 05:45:00,57.2,117.79299999999999,30.848000000000003,32.047 -2020-11-01 06:00:00,58.93,131.375,31.166,32.047 -2020-11-01 06:15:00,60.16,148.164,31.166,32.047 -2020-11-01 06:30:00,60.09,141.499,31.166,32.047 -2020-11-01 06:45:00,61.33,135.033,31.166,32.047 -2020-11-01 07:00:00,62.62,134.209,33.527,32.047 -2020-11-01 07:15:00,64.53,135.998,33.527,32.047 -2020-11-01 07:30:00,66.19,138.07299999999998,33.527,32.047 -2020-11-01 07:45:00,69.32,140.029,33.527,32.047 -2020-11-01 08:00:00,72.18,143.476,36.616,32.047 -2020-11-01 08:15:00,73.05,145.618,36.616,32.047 -2020-11-01 08:30:00,75.6,146.314,36.616,32.047 -2020-11-01 08:45:00,76.81,146.583,36.616,32.047 -2020-11-01 09:00:00,78.39,143.313,37.857,32.047 -2020-11-01 09:15:00,79.58,142.33100000000002,37.857,32.047 -2020-11-01 09:30:00,79.36,141.849,37.857,32.047 -2020-11-01 09:45:00,80.15,141.034,37.857,32.047 -2020-11-01 10:00:00,78.79,139.36,36.319,32.047 -2020-11-01 10:15:00,81.18,138.553,36.319,32.047 -2020-11-01 10:30:00,82.06,137.161,36.319,32.047 -2020-11-01 10:45:00,84.37,135.914,36.319,32.047 -2020-11-01 11:00:00,87.44,132.654,37.236999999999995,32.047 -2020-11-01 11:15:00,90.16,131.516,37.236999999999995,32.047 -2020-11-01 11:30:00,92.79,131.694,37.236999999999995,32.047 -2020-11-01 11:45:00,94.47,132.332,37.236999999999995,32.047 -2020-11-01 12:00:00,93.72,128.334,34.871,32.047 -2020-11-01 12:15:00,91.05,127.323,34.871,32.047 -2020-11-01 12:30:00,88.73,126.853,34.871,32.047 -2020-11-01 12:45:00,87.98,126.25399999999999,34.871,32.047 -2020-11-01 13:00:00,81.86,125.57600000000001,29.738000000000003,32.047 -2020-11-01 13:15:00,79.48,125.912,29.738000000000003,32.047 -2020-11-01 13:30:00,78.24,124.734,29.738000000000003,32.047 -2020-11-01 13:45:00,77.77,124.292,29.738000000000003,32.047 -2020-11-01 14:00:00,78.82,124.43,27.333000000000002,32.047 -2020-11-01 14:15:00,77.97,124.331,27.333000000000002,32.047 -2020-11-01 14:30:00,78.18,123.743,27.333000000000002,32.047 -2020-11-01 14:45:00,79.25,123.29,27.333000000000002,32.047 -2020-11-01 15:00:00,79.18,121.579,28.232,32.047 -2020-11-01 15:15:00,79.37,122.45100000000001,28.232,32.047 -2020-11-01 15:30:00,79.55,122.805,28.232,32.047 -2020-11-01 15:45:00,81.75,123.443,28.232,32.047 -2020-11-01 16:00:00,83.82,125.545,32.815,32.047 -2020-11-01 16:15:00,83.89,127.041,32.815,32.047 -2020-11-01 16:30:00,84.8,127.854,32.815,32.047 -2020-11-01 16:45:00,89.87,127.169,32.815,32.047 -2020-11-01 17:00:00,96.6,129.106,43.068999999999996,32.047 -2020-11-01 17:15:00,96.91,130.164,43.068999999999996,32.047 -2020-11-01 17:30:00,99.73,130.485,43.068999999999996,32.047 -2020-11-01 17:45:00,101.95,131.416,43.068999999999996,32.047 -2020-11-01 18:00:00,100.66,134.56799999999998,50.498999999999995,32.047 -2020-11-01 18:15:00,100.99,135.72299999999998,50.498999999999995,32.047 -2020-11-01 18:30:00,97.5,134.084,50.498999999999995,32.047 -2020-11-01 18:45:00,96.13,135.263,50.498999999999995,32.047 -2020-11-01 19:00:00,94.03,137.83700000000002,53.481,32.047 -2020-11-01 19:15:00,94.0,136.382,53.481,32.047 -2020-11-01 19:30:00,92.13,135.606,53.481,32.047 -2020-11-01 19:45:00,92.89,135.071,53.481,32.047 -2020-11-01 20:00:00,90.69,132.186,51.687,32.047 -2020-11-01 20:15:00,94.58,130.61700000000002,51.687,32.047 -2020-11-01 20:30:00,92.24,130.408,51.687,32.047 -2020-11-01 20:45:00,86.66,127.39,51.687,32.047 -2020-11-01 21:00:00,83.37,123.02,47.674,32.047 -2020-11-01 21:15:00,84.21,122.93,47.674,32.047 -2020-11-01 21:30:00,88.7,122.751,47.674,32.047 -2020-11-01 21:45:00,90.47,120.36399999999999,47.674,32.047 -2020-11-01 22:00:00,90.97,116.14,48.178000000000004,32.047 -2020-11-01 22:15:00,86.98,112.27799999999999,48.178000000000004,32.047 -2020-11-01 22:30:00,83.62,106.661,48.178000000000004,32.047 -2020-11-01 22:45:00,82.66,102.23,48.178000000000004,32.047 -2020-11-01 23:00:00,81.93,96.52,42.553999999999995,32.047 -2020-11-01 23:15:00,85.7,94.42200000000001,42.553999999999995,32.047 -2020-11-01 23:30:00,82.06,92.15700000000001,42.553999999999995,32.047 -2020-11-01 23:45:00,78.61,90.99799999999999,42.553999999999995,32.047 -2020-11-02 00:00:00,77.71,77.483,37.177,32.225 -2020-11-02 00:15:00,78.04,77.413,37.177,32.225 -2020-11-02 00:30:00,75.0,77.514,37.177,32.225 -2020-11-02 00:45:00,71.86,77.888,37.177,32.225 -2020-11-02 01:00:00,73.26,79.508,35.358000000000004,32.225 -2020-11-02 01:15:00,75.19,79.788,35.358000000000004,32.225 -2020-11-02 01:30:00,74.29,79.453,35.358000000000004,32.225 -2020-11-02 01:45:00,71.36,79.48899999999999,35.358000000000004,32.225 -2020-11-02 02:00:00,74.46,80.84899999999999,35.03,32.225 -2020-11-02 02:15:00,76.19,80.941,35.03,32.225 -2020-11-02 02:30:00,73.58,81.756,35.03,32.225 -2020-11-02 02:45:00,70.49,82.568,35.03,32.225 -2020-11-02 03:00:00,71.48,86.30799999999999,34.394,32.225 -2020-11-02 03:15:00,76.54,87.72,34.394,32.225 -2020-11-02 03:30:00,78.06,88.242,34.394,32.225 -2020-11-02 03:45:00,72.8,89.243,34.394,32.225 -2020-11-02 04:00:00,77.28,101.759,34.421,32.225 -2020-11-02 04:15:00,80.21,113.29299999999999,34.421,32.225 -2020-11-02 04:30:00,81.33,113.90799999999999,34.421,32.225 -2020-11-02 04:45:00,78.06,115.085,34.421,32.225 -2020-11-02 05:00:00,85.36,144.252,39.435,32.225 -2020-11-02 05:15:00,91.98,173.24200000000002,39.435,32.225 -2020-11-02 05:30:00,96.04,168.166,39.435,32.225 -2020-11-02 05:45:00,95.92,158.836,39.435,32.225 -2020-11-02 06:00:00,104.79,157.326,55.685,32.225 -2020-11-02 06:15:00,114.4,161.115,55.685,32.225 -2020-11-02 06:30:00,121.39,161.122,55.685,32.225 -2020-11-02 06:45:00,125.87,162.344,55.685,32.225 -2020-11-02 07:00:00,126.76,163.525,66.837,32.225 -2020-11-02 07:15:00,126.88,167.03400000000002,66.837,32.225 -2020-11-02 07:30:00,127.09,168.299,66.837,32.225 -2020-11-02 07:45:00,125.37,168.74099999999999,66.837,32.225 -2020-11-02 08:00:00,127.99,168.55700000000002,72.217,32.225 -2020-11-02 08:15:00,126.32,168.81599999999997,72.217,32.225 -2020-11-02 08:30:00,126.89,166.49400000000003,72.217,32.225 -2020-11-02 08:45:00,124.03,164.62900000000002,72.217,32.225 -2020-11-02 09:00:00,123.27,160.44799999999998,66.117,32.225 -2020-11-02 09:15:00,123.92,156.477,66.117,32.225 -2020-11-02 09:30:00,125.15,154.977,66.117,32.225 -2020-11-02 09:45:00,128.93,153.493,66.117,32.225 -2020-11-02 10:00:00,124.84,151.434,62.1,32.225 -2020-11-02 10:15:00,123.92,150.312,62.1,32.225 -2020-11-02 10:30:00,124.83,148.13299999999998,62.1,32.225 -2020-11-02 10:45:00,125.47,146.829,62.1,32.225 -2020-11-02 11:00:00,125.02,141.958,60.021,32.225 -2020-11-02 11:15:00,116.55,142.15,60.021,32.225 -2020-11-02 11:30:00,119.3,143.525,60.021,32.225 -2020-11-02 11:45:00,122.68,144.034,60.021,32.225 -2020-11-02 12:00:00,116.26,140.8,56.75899999999999,32.225 -2020-11-02 12:15:00,116.59,139.82399999999998,56.75899999999999,32.225 -2020-11-02 12:30:00,124.27,139.215,56.75899999999999,32.225 -2020-11-02 12:45:00,119.06,139.694,56.75899999999999,32.225 -2020-11-02 13:00:00,118.02,139.783,56.04600000000001,32.225 -2020-11-02 13:15:00,119.4,138.85399999999998,56.04600000000001,32.225 -2020-11-02 13:30:00,122.02,137.35399999999998,56.04600000000001,32.225 -2020-11-02 13:45:00,119.1,137.218,56.04600000000001,32.225 -2020-11-02 14:00:00,122.77,136.59,55.475,32.225 -2020-11-02 14:15:00,120.69,136.239,55.475,32.225 -2020-11-02 14:30:00,120.41,135.251,55.475,32.225 -2020-11-02 14:45:00,124.04,135.44,55.475,32.225 -2020-11-02 15:00:00,120.94,134.887,57.048,32.225 -2020-11-02 15:15:00,120.7,134.558,57.048,32.225 -2020-11-02 15:30:00,120.66,134.607,57.048,32.225 -2020-11-02 15:45:00,121.41,134.784,57.048,32.225 -2020-11-02 16:00:00,122.05,137.142,59.06,32.225 -2020-11-02 16:15:00,124.08,138.155,59.06,32.225 -2020-11-02 16:30:00,124.36,138.067,59.06,32.225 -2020-11-02 16:45:00,129.23,136.614,59.06,32.225 -2020-11-02 17:00:00,134.05,137.868,65.419,32.225 -2020-11-02 17:15:00,132.55,138.431,65.419,32.225 -2020-11-02 17:30:00,137.22,138.244,65.419,32.225 -2020-11-02 17:45:00,135.22,138.03,65.419,32.225 -2020-11-02 18:00:00,135.07,141.039,69.345,32.225 -2020-11-02 18:15:00,132.86,140.007,69.345,32.225 -2020-11-02 18:30:00,131.02,138.54399999999998,69.345,32.225 -2020-11-02 18:45:00,131.86,141.345,69.345,32.225 -2020-11-02 19:00:00,128.07,142.715,73.825,32.225 -2020-11-02 19:15:00,126.32,140.951,73.825,32.225 -2020-11-02 19:30:00,127.01,140.357,73.825,32.225 -2020-11-02 19:45:00,122.74,139.009,73.825,32.225 -2020-11-02 20:00:00,119.02,134.085,64.027,32.225 -2020-11-02 20:15:00,113.3,131.38299999999998,64.027,32.225 -2020-11-02 20:30:00,108.72,130.102,64.027,32.225 -2020-11-02 20:45:00,106.21,128.234,64.027,32.225 -2020-11-02 21:00:00,107.92,123.986,57.952,32.225 -2020-11-02 21:15:00,109.71,123.275,57.952,32.225 -2020-11-02 21:30:00,108.09,122.71700000000001,57.952,32.225 -2020-11-02 21:45:00,100.41,119.898,57.952,32.225 -2020-11-02 22:00:00,93.26,112.988,53.031000000000006,32.225 -2020-11-02 22:15:00,93.34,108.98299999999999,53.031000000000006,32.225 -2020-11-02 22:30:00,98.42,95.46700000000001,53.031000000000006,32.225 -2020-11-02 22:45:00,95.12,87.84,53.031000000000006,32.225 -2020-11-02 23:00:00,89.39,82.635,45.085,32.225 -2020-11-02 23:15:00,85.21,81.652,45.085,32.225 -2020-11-02 23:30:00,86.99,81.206,45.085,32.225 -2020-11-02 23:45:00,87.54,81.581,45.085,32.225 -2020-11-03 00:00:00,84.2,76.499,42.843,32.225 -2020-11-03 00:15:00,78.55,77.664,42.843,32.225 -2020-11-03 00:30:00,76.57,77.46300000000001,42.843,32.225 -2020-11-03 00:45:00,82.07,77.531,42.843,32.225 -2020-11-03 01:00:00,80.46,78.831,41.542,32.225 -2020-11-03 01:15:00,79.44,78.893,41.542,32.225 -2020-11-03 01:30:00,74.87,78.63,41.542,32.225 -2020-11-03 01:45:00,72.5,78.641,41.542,32.225 -2020-11-03 02:00:00,78.57,79.813,40.19,32.225 -2020-11-03 02:15:00,80.48,80.32300000000001,40.19,32.225 -2020-11-03 02:30:00,80.8,80.593,40.19,32.225 -2020-11-03 02:45:00,74.67,81.544,40.19,32.225 -2020-11-03 03:00:00,71.93,84.294,39.626,32.225 -2020-11-03 03:15:00,79.33,85.59299999999999,39.626,32.225 -2020-11-03 03:30:00,82.08,86.404,39.626,32.225 -2020-11-03 03:45:00,82.13,87.045,39.626,32.225 -2020-11-03 04:00:00,77.08,98.926,40.196999999999996,32.225 -2020-11-03 04:15:00,77.12,110.274,40.196999999999996,32.225 -2020-11-03 04:30:00,82.35,110.641,40.196999999999996,32.225 -2020-11-03 04:45:00,87.06,112.781,40.196999999999996,32.225 -2020-11-03 05:00:00,91.11,145.749,43.378,32.225 -2020-11-03 05:15:00,88.11,174.916,43.378,32.225 -2020-11-03 05:30:00,88.09,168.97,43.378,32.225 -2020-11-03 05:45:00,96.86,159.315,43.378,32.225 -2020-11-03 06:00:00,106.9,157.497,55.691,32.225 -2020-11-03 06:15:00,109.83,162.39700000000002,55.691,32.225 -2020-11-03 06:30:00,115.64,161.89700000000002,55.691,32.225 -2020-11-03 06:45:00,121.32,162.537,55.691,32.225 -2020-11-03 07:00:00,121.08,163.66,65.567,32.225 -2020-11-03 07:15:00,122.41,166.97799999999998,65.567,32.225 -2020-11-03 07:30:00,122.07,167.926,65.567,32.225 -2020-11-03 07:45:00,122.18,168.12400000000002,65.567,32.225 -2020-11-03 08:00:00,123.78,168.005,73.001,32.225 -2020-11-03 08:15:00,123.3,167.40099999999998,73.001,32.225 -2020-11-03 08:30:00,121.01,165.054,73.001,32.225 -2020-11-03 08:45:00,120.18,162.636,73.001,32.225 -2020-11-03 09:00:00,119.27,158.009,67.08800000000001,32.225 -2020-11-03 09:15:00,120.13,155.065,67.08800000000001,32.225 -2020-11-03 09:30:00,119.45,154.263,67.08800000000001,32.225 -2020-11-03 09:45:00,119.38,153.096,67.08800000000001,32.225 -2020-11-03 10:00:00,117.17,150.135,62.803000000000004,32.225 -2020-11-03 10:15:00,116.37,148.19,62.803000000000004,32.225 -2020-11-03 10:30:00,114.86,146.122,62.803000000000004,32.225 -2020-11-03 10:45:00,114.46,145.343,62.803000000000004,32.225 -2020-11-03 11:00:00,115.53,141.503,60.155,32.225 -2020-11-03 11:15:00,116.0,141.577,60.155,32.225 -2020-11-03 11:30:00,114.96,141.749,60.155,32.225 -2020-11-03 11:45:00,115.15,142.586,60.155,32.225 -2020-11-03 12:00:00,114.54,138.3,56.845,32.225 -2020-11-03 12:15:00,113.41,137.136,56.845,32.225 -2020-11-03 12:30:00,116.09,137.35399999999998,56.845,32.225 -2020-11-03 12:45:00,113.87,137.849,56.845,32.225 -2020-11-03 13:00:00,112.05,137.47799999999998,56.163000000000004,32.225 -2020-11-03 13:15:00,113.4,136.88,56.163000000000004,32.225 -2020-11-03 13:30:00,111.98,136.209,56.163000000000004,32.225 -2020-11-03 13:45:00,112.78,135.895,56.163000000000004,32.225 -2020-11-03 14:00:00,114.01,135.56,55.934,32.225 -2020-11-03 14:15:00,114.62,135.273,55.934,32.225 -2020-11-03 14:30:00,114.22,134.879,55.934,32.225 -2020-11-03 14:45:00,116.32,134.763,55.934,32.225 -2020-11-03 15:00:00,118.75,133.835,57.43899999999999,32.225 -2020-11-03 15:15:00,121.12,134.054,57.43899999999999,32.225 -2020-11-03 15:30:00,118.99,134.238,57.43899999999999,32.225 -2020-11-03 15:45:00,120.3,134.248,57.43899999999999,32.225 -2020-11-03 16:00:00,121.23,136.631,59.968999999999994,32.225 -2020-11-03 16:15:00,121.12,138.031,59.968999999999994,32.225 -2020-11-03 16:30:00,124.29,138.269,59.968999999999994,32.225 -2020-11-03 16:45:00,128.38,137.31,59.968999999999994,32.225 -2020-11-03 17:00:00,134.36,138.976,67.428,32.225 -2020-11-03 17:15:00,135.38,139.727,67.428,32.225 -2020-11-03 17:30:00,136.98,139.846,67.428,32.225 -2020-11-03 17:45:00,136.7,139.438,67.428,32.225 -2020-11-03 18:00:00,135.03,142.02700000000002,71.533,32.225 -2020-11-03 18:15:00,133.99,141.156,71.533,32.225 -2020-11-03 18:30:00,132.72,139.4,71.533,32.225 -2020-11-03 18:45:00,132.44,142.667,71.533,32.225 -2020-11-03 19:00:00,129.7,143.67,73.32300000000001,32.225 -2020-11-03 19:15:00,127.73,141.774,73.32300000000001,32.225 -2020-11-03 19:30:00,125.23,140.636,73.32300000000001,32.225 -2020-11-03 19:45:00,124.06,139.406,73.32300000000001,32.225 -2020-11-03 20:00:00,116.4,134.749,64.166,32.225 -2020-11-03 20:15:00,113.06,131.075,64.166,32.225 -2020-11-03 20:30:00,111.89,130.452,64.166,32.225 -2020-11-03 20:45:00,108.23,128.349,64.166,32.225 -2020-11-03 21:00:00,111.71,123.95299999999999,57.891999999999996,32.225 -2020-11-03 21:15:00,112.07,123.279,57.891999999999996,32.225 -2020-11-03 21:30:00,109.48,122.31299999999999,57.891999999999996,32.225 -2020-11-03 21:45:00,100.8,119.70200000000001,57.891999999999996,32.225 -2020-11-03 22:00:00,96.06,113.992,53.242,32.225 -2020-11-03 22:15:00,98.84,109.67299999999999,53.242,32.225 -2020-11-03 22:30:00,98.89,96.368,53.242,32.225 -2020-11-03 22:45:00,97.23,88.929,53.242,32.225 -2020-11-03 23:00:00,87.29,83.465,46.665,32.225 -2020-11-03 23:15:00,90.27,82.43700000000001,46.665,32.225 -2020-11-03 23:30:00,89.3,81.75399999999999,46.665,32.225 -2020-11-03 23:45:00,90.61,81.89,46.665,32.225 -2020-11-04 00:00:00,81.4,76.873,43.16,32.225 -2020-11-04 00:15:00,84.07,78.01899999999999,43.16,32.225 -2020-11-04 00:30:00,85.61,77.822,43.16,32.225 -2020-11-04 00:45:00,83.69,77.87899999999999,43.16,32.225 -2020-11-04 01:00:00,75.16,79.202,40.972,32.225 -2020-11-04 01:15:00,82.28,79.28,40.972,32.225 -2020-11-04 01:30:00,82.05,79.033,40.972,32.225 -2020-11-04 01:45:00,82.13,79.03699999999999,40.972,32.225 -2020-11-04 02:00:00,77.14,80.223,39.749,32.225 -2020-11-04 02:15:00,83.18,80.742,39.749,32.225 -2020-11-04 02:30:00,81.85,81.001,39.749,32.225 -2020-11-04 02:45:00,79.64,81.949,39.749,32.225 -2020-11-04 03:00:00,77.05,84.684,39.422,32.225 -2020-11-04 03:15:00,82.52,86.007,39.422,32.225 -2020-11-04 03:30:00,83.16,86.821,39.422,32.225 -2020-11-04 03:45:00,81.55,87.447,39.422,32.225 -2020-11-04 04:00:00,77.98,99.337,40.505,32.225 -2020-11-04 04:15:00,84.43,110.712,40.505,32.225 -2020-11-04 04:30:00,87.82,111.073,40.505,32.225 -2020-11-04 04:45:00,88.51,113.22,40.505,32.225 -2020-11-04 05:00:00,88.07,146.239,43.397,32.225 -2020-11-04 05:15:00,92.6,175.447,43.397,32.225 -2020-11-04 05:30:00,99.67,169.497,43.397,32.225 -2020-11-04 05:45:00,101.58,159.82,43.397,32.225 -2020-11-04 06:00:00,103.46,157.997,55.218,32.225 -2020-11-04 06:15:00,112.23,162.908,55.218,32.225 -2020-11-04 06:30:00,114.12,162.44,55.218,32.225 -2020-11-04 06:45:00,117.78,163.09799999999998,55.218,32.225 -2020-11-04 07:00:00,121.0,164.22,67.39,32.225 -2020-11-04 07:15:00,123.07,167.551,67.39,32.225 -2020-11-04 07:30:00,121.54,168.52599999999998,67.39,32.225 -2020-11-04 07:45:00,123.46,168.72799999999998,67.39,32.225 -2020-11-04 08:00:00,126.53,168.62400000000002,74.345,32.225 -2020-11-04 08:15:00,124.13,168.00099999999998,74.345,32.225 -2020-11-04 08:30:00,124.39,165.68,74.345,32.225 -2020-11-04 08:45:00,125.06,163.233,74.345,32.225 -2020-11-04 09:00:00,120.85,158.594,69.336,32.225 -2020-11-04 09:15:00,122.69,155.651,69.336,32.225 -2020-11-04 09:30:00,127.17,154.84,69.336,32.225 -2020-11-04 09:45:00,119.89,153.649,69.336,32.225 -2020-11-04 10:00:00,117.85,150.679,64.291,32.225 -2020-11-04 10:15:00,119.47,148.695,64.291,32.225 -2020-11-04 10:30:00,118.23,146.60299999999998,64.291,32.225 -2020-11-04 10:45:00,117.75,145.809,64.291,32.225 -2020-11-04 11:00:00,117.87,141.967,62.20399999999999,32.225 -2020-11-04 11:15:00,117.63,142.02100000000002,62.20399999999999,32.225 -2020-11-04 11:30:00,117.7,142.192,62.20399999999999,32.225 -2020-11-04 11:45:00,119.73,143.015,62.20399999999999,32.225 -2020-11-04 12:00:00,117.89,138.71,59.042,32.225 -2020-11-04 12:15:00,116.62,137.545,59.042,32.225 -2020-11-04 12:30:00,119.41,137.797,59.042,32.225 -2020-11-04 12:45:00,114.95,138.29399999999998,59.042,32.225 -2020-11-04 13:00:00,118.41,137.885,57.907,32.225 -2020-11-04 13:15:00,117.16,137.295,57.907,32.225 -2020-11-04 13:30:00,115.27,136.624,57.907,32.225 -2020-11-04 13:45:00,116.77,136.303,57.907,32.225 -2020-11-04 14:00:00,114.41,135.916,58.358000000000004,32.225 -2020-11-04 14:15:00,113.25,135.64700000000002,58.358000000000004,32.225 -2020-11-04 14:30:00,113.2,135.291,58.358000000000004,32.225 -2020-11-04 14:45:00,116.71,135.17600000000002,58.358000000000004,32.225 -2020-11-04 15:00:00,118.53,134.237,59.348,32.225 -2020-11-04 15:15:00,117.22,134.471,59.348,32.225 -2020-11-04 15:30:00,115.9,134.694,59.348,32.225 -2020-11-04 15:45:00,117.62,134.718,59.348,32.225 -2020-11-04 16:00:00,119.43,137.075,61.413999999999994,32.225 -2020-11-04 16:15:00,119.61,138.499,61.413999999999994,32.225 -2020-11-04 16:30:00,124.61,138.737,61.413999999999994,32.225 -2020-11-04 16:45:00,127.54,137.829,61.413999999999994,32.225 -2020-11-04 17:00:00,135.41,139.454,67.107,32.225 -2020-11-04 17:15:00,134.88,140.226,67.107,32.225 -2020-11-04 17:30:00,134.97,140.349,67.107,32.225 -2020-11-04 17:45:00,134.76,139.954,67.107,32.225 -2020-11-04 18:00:00,134.73,142.542,71.92,32.225 -2020-11-04 18:15:00,133.15,141.637,71.92,32.225 -2020-11-04 18:30:00,132.19,139.892,71.92,32.225 -2020-11-04 18:45:00,131.93,143.155,71.92,32.225 -2020-11-04 19:00:00,128.13,144.165,75.09,32.225 -2020-11-04 19:15:00,126.83,142.259,75.09,32.225 -2020-11-04 19:30:00,125.21,141.106,75.09,32.225 -2020-11-04 19:45:00,126.42,139.845,75.09,32.225 -2020-11-04 20:00:00,120.08,135.208,65.977,32.225 -2020-11-04 20:15:00,113.28,131.525,65.977,32.225 -2020-11-04 20:30:00,111.02,130.87,65.977,32.225 -2020-11-04 20:45:00,112.13,128.757,65.977,32.225 -2020-11-04 21:00:00,108.11,124.35700000000001,58.798,32.225 -2020-11-04 21:15:00,107.66,123.671,58.798,32.225 -2020-11-04 21:30:00,109.28,122.713,58.798,32.225 -2020-11-04 21:45:00,103.93,120.087,58.798,32.225 -2020-11-04 22:00:00,97.92,114.374,54.486000000000004,32.225 -2020-11-04 22:15:00,93.55,110.039,54.486000000000004,32.225 -2020-11-04 22:30:00,95.7,96.777,54.486000000000004,32.225 -2020-11-04 22:45:00,98.22,89.345,54.486000000000004,32.225 -2020-11-04 23:00:00,93.29,83.88600000000001,47.783,32.225 -2020-11-04 23:15:00,92.59,82.83200000000001,47.783,32.225 -2020-11-04 23:30:00,89.1,82.152,47.783,32.225 -2020-11-04 23:45:00,89.25,82.274,47.783,32.225 -2020-11-05 00:00:00,85.13,77.245,43.88,32.225 -2020-11-05 00:15:00,81.55,78.372,43.88,32.225 -2020-11-05 00:30:00,84.52,78.18,43.88,32.225 -2020-11-05 00:45:00,83.95,78.225,43.88,32.225 -2020-11-05 01:00:00,78.92,79.571,42.242,32.225 -2020-11-05 01:15:00,78.1,79.663,42.242,32.225 -2020-11-05 01:30:00,81.46,79.433,42.242,32.225 -2020-11-05 01:45:00,82.78,79.43,42.242,32.225 -2020-11-05 02:00:00,79.05,80.63,40.918,32.225 -2020-11-05 02:15:00,78.82,81.15899999999999,40.918,32.225 -2020-11-05 02:30:00,81.15,81.407,40.918,32.225 -2020-11-05 02:45:00,85.55,82.351,40.918,32.225 -2020-11-05 03:00:00,79.23,85.072,40.411,32.225 -2020-11-05 03:15:00,74.81,86.42,40.411,32.225 -2020-11-05 03:30:00,82.28,87.23700000000001,40.411,32.225 -2020-11-05 03:45:00,82.7,87.84700000000001,40.411,32.225 -2020-11-05 04:00:00,84.49,99.74700000000001,41.246,32.225 -2020-11-05 04:15:00,80.91,111.145,41.246,32.225 -2020-11-05 04:30:00,84.02,111.501,41.246,32.225 -2020-11-05 04:45:00,88.63,113.65700000000001,41.246,32.225 -2020-11-05 05:00:00,89.89,146.725,44.533,32.225 -2020-11-05 05:15:00,85.19,175.975,44.533,32.225 -2020-11-05 05:30:00,88.1,170.021,44.533,32.225 -2020-11-05 05:45:00,93.43,160.321,44.533,32.225 -2020-11-05 06:00:00,102.82,158.494,55.005,32.225 -2020-11-05 06:15:00,110.75,163.417,55.005,32.225 -2020-11-05 06:30:00,115.53,162.97799999999998,55.005,32.225 -2020-11-05 06:45:00,126.41,163.656,55.005,32.225 -2020-11-05 07:00:00,128.69,164.77700000000002,64.597,32.225 -2020-11-05 07:15:00,123.86,168.12,64.597,32.225 -2020-11-05 07:30:00,127.33,169.122,64.597,32.225 -2020-11-05 07:45:00,126.84,169.326,64.597,32.225 -2020-11-05 08:00:00,128.0,169.237,71.71600000000001,32.225 -2020-11-05 08:15:00,129.17,168.595,71.71600000000001,32.225 -2020-11-05 08:30:00,128.65,166.301,71.71600000000001,32.225 -2020-11-05 08:45:00,128.37,163.826,71.71600000000001,32.225 -2020-11-05 09:00:00,127.46,159.174,66.51899999999999,32.225 -2020-11-05 09:15:00,128.89,156.231,66.51899999999999,32.225 -2020-11-05 09:30:00,127.36,155.411,66.51899999999999,32.225 -2020-11-05 09:45:00,127.69,154.197,66.51899999999999,32.225 -2020-11-05 10:00:00,124.33,151.218,63.04,32.225 -2020-11-05 10:15:00,121.71,149.195,63.04,32.225 -2020-11-05 10:30:00,118.24,147.08,63.04,32.225 -2020-11-05 10:45:00,119.63,146.269,63.04,32.225 -2020-11-05 11:00:00,119.67,142.42700000000002,60.998000000000005,32.225 -2020-11-05 11:15:00,125.95,142.46,60.998000000000005,32.225 -2020-11-05 11:30:00,128.57,142.63,60.998000000000005,32.225 -2020-11-05 11:45:00,128.14,143.44,60.998000000000005,32.225 -2020-11-05 12:00:00,125.75,139.116,58.27,32.225 -2020-11-05 12:15:00,124.99,137.951,58.27,32.225 -2020-11-05 12:30:00,126.45,138.237,58.27,32.225 -2020-11-05 12:45:00,127.2,138.736,58.27,32.225 -2020-11-05 13:00:00,125.78,138.29,57.196000000000005,32.225 -2020-11-05 13:15:00,126.04,137.708,57.196000000000005,32.225 -2020-11-05 13:30:00,123.81,137.035,57.196000000000005,32.225 -2020-11-05 13:45:00,124.92,136.709,57.196000000000005,32.225 -2020-11-05 14:00:00,127.64,136.27,57.38399999999999,32.225 -2020-11-05 14:15:00,128.62,136.016,57.38399999999999,32.225 -2020-11-05 14:30:00,130.34,135.69899999999998,57.38399999999999,32.225 -2020-11-05 14:45:00,127.29,135.585,57.38399999999999,32.225 -2020-11-05 15:00:00,129.48,134.634,58.647,32.225 -2020-11-05 15:15:00,129.34,134.884,58.647,32.225 -2020-11-05 15:30:00,127.64,135.14600000000002,58.647,32.225 -2020-11-05 15:45:00,127.96,135.18200000000002,58.647,32.225 -2020-11-05 16:00:00,131.92,137.515,60.083999999999996,32.225 -2020-11-05 16:15:00,131.35,138.964,60.083999999999996,32.225 -2020-11-05 16:30:00,131.61,139.2,60.083999999999996,32.225 -2020-11-05 16:45:00,134.19,138.344,60.083999999999996,32.225 -2020-11-05 17:00:00,137.92,139.92700000000002,65.85600000000001,32.225 -2020-11-05 17:15:00,136.96,140.72,65.85600000000001,32.225 -2020-11-05 17:30:00,137.47,140.84799999999998,65.85600000000001,32.225 -2020-11-05 17:45:00,138.29,140.466,65.85600000000001,32.225 -2020-11-05 18:00:00,139.48,143.053,69.855,32.225 -2020-11-05 18:15:00,134.15,142.115,69.855,32.225 -2020-11-05 18:30:00,133.51,140.38,69.855,32.225 -2020-11-05 18:45:00,132.8,143.64,69.855,32.225 -2020-11-05 19:00:00,131.09,144.657,74.015,32.225 -2020-11-05 19:15:00,127.89,142.741,74.015,32.225 -2020-11-05 19:30:00,128.05,141.57299999999998,74.015,32.225 -2020-11-05 19:45:00,123.88,140.283,74.015,32.225 -2020-11-05 20:00:00,116.5,135.664,65.316,32.225 -2020-11-05 20:15:00,113.48,131.972,65.316,32.225 -2020-11-05 20:30:00,112.84,131.285,65.316,32.225 -2020-11-05 20:45:00,114.31,129.164,65.316,32.225 -2020-11-05 21:00:00,103.83,124.757,58.403999999999996,32.225 -2020-11-05 21:15:00,105.69,124.059,58.403999999999996,32.225 -2020-11-05 21:30:00,103.3,123.11,58.403999999999996,32.225 -2020-11-05 21:45:00,102.93,120.469,58.403999999999996,32.225 -2020-11-05 22:00:00,97.89,114.755,54.092,32.225 -2020-11-05 22:15:00,97.8,110.404,54.092,32.225 -2020-11-05 22:30:00,92.88,97.184,54.092,32.225 -2020-11-05 22:45:00,97.46,89.76,54.092,32.225 -2020-11-05 23:00:00,92.45,84.305,48.18600000000001,32.225 -2020-11-05 23:15:00,91.31,83.226,48.18600000000001,32.225 -2020-11-05 23:30:00,83.3,82.54899999999999,48.18600000000001,32.225 -2020-11-05 23:45:00,84.68,82.65700000000001,48.18600000000001,32.225 -2020-11-06 00:00:00,84.7,76.2,45.18899999999999,32.225 -2020-11-06 00:15:00,85.23,77.523,45.18899999999999,32.225 -2020-11-06 00:30:00,83.88,77.358,45.18899999999999,32.225 -2020-11-06 00:45:00,80.5,77.637,45.18899999999999,32.225 -2020-11-06 01:00:00,80.34,78.648,43.256,32.225 -2020-11-06 01:15:00,81.37,79.095,43.256,32.225 -2020-11-06 01:30:00,77.99,79.031,43.256,32.225 -2020-11-06 01:45:00,77.75,78.985,43.256,32.225 -2020-11-06 02:00:00,79.46,80.635,42.312,32.225 -2020-11-06 02:15:00,81.87,81.078,42.312,32.225 -2020-11-06 02:30:00,75.94,81.99,42.312,32.225 -2020-11-06 02:45:00,73.82,82.675,42.312,32.225 -2020-11-06 03:00:00,72.73,85.07600000000001,41.833,32.225 -2020-11-06 03:15:00,81.36,86.537,41.833,32.225 -2020-11-06 03:30:00,82.16,87.24600000000001,41.833,32.225 -2020-11-06 03:45:00,82.46,88.412,41.833,32.225 -2020-11-06 04:00:00,78.14,100.52,42.732,32.225 -2020-11-06 04:15:00,80.31,111.135,42.732,32.225 -2020-11-06 04:30:00,83.97,112.02799999999999,42.732,32.225 -2020-11-06 04:45:00,89.09,113.204,42.732,32.225 -2020-11-06 05:00:00,92.15,145.32399999999998,46.254,32.225 -2020-11-06 05:15:00,86.95,175.96,46.254,32.225 -2020-11-06 05:30:00,90.34,170.75799999999998,46.254,32.225 -2020-11-06 05:45:00,99.94,160.809,46.254,32.225 -2020-11-06 06:00:00,111.88,159.36700000000002,56.76,32.225 -2020-11-06 06:15:00,114.2,163.43200000000002,56.76,32.225 -2020-11-06 06:30:00,122.58,162.452,56.76,32.225 -2020-11-06 06:45:00,120.37,164.13,56.76,32.225 -2020-11-06 07:00:00,125.85,164.983,66.029,32.225 -2020-11-06 07:15:00,126.83,169.363,66.029,32.225 -2020-11-06 07:30:00,126.69,169.503,66.029,32.225 -2020-11-06 07:45:00,128.85,169.049,66.029,32.225 -2020-11-06 08:00:00,132.52,168.55,73.128,32.225 -2020-11-06 08:15:00,131.81,167.88299999999998,73.128,32.225 -2020-11-06 08:30:00,131.12,166.232,73.128,32.225 -2020-11-06 08:45:00,131.33,162.619,73.128,32.225 -2020-11-06 09:00:00,132.45,157.394,68.23100000000001,32.225 -2020-11-06 09:15:00,134.02,155.541,68.23100000000001,32.225 -2020-11-06 09:30:00,135.36,154.16899999999998,68.23100000000001,32.225 -2020-11-06 09:45:00,135.72,152.999,68.23100000000001,32.225 -2020-11-06 10:00:00,135.71,149.14,64.733,32.225 -2020-11-06 10:15:00,136.19,147.489,64.733,32.225 -2020-11-06 10:30:00,134.9,145.493,64.733,32.225 -2020-11-06 10:45:00,134.66,144.32399999999998,64.733,32.225 -2020-11-06 11:00:00,134.81,140.532,62.0,32.225 -2020-11-06 11:15:00,135.06,139.54399999999998,62.0,32.225 -2020-11-06 11:30:00,134.53,140.829,62.0,32.225 -2020-11-06 11:45:00,137.14,141.335,62.0,32.225 -2020-11-06 12:00:00,134.12,137.924,57.876999999999995,32.225 -2020-11-06 12:15:00,133.37,135.04399999999998,57.876999999999995,32.225 -2020-11-06 12:30:00,132.61,135.516,57.876999999999995,32.225 -2020-11-06 12:45:00,131.3,136.129,57.876999999999995,32.225 -2020-11-06 13:00:00,128.69,136.524,55.585,32.225 -2020-11-06 13:15:00,127.97,136.601,55.585,32.225 -2020-11-06 13:30:00,124.2,136.194,55.585,32.225 -2020-11-06 13:45:00,123.92,135.91299999999998,55.585,32.225 -2020-11-06 14:00:00,118.95,134.342,54.5,32.225 -2020-11-06 14:15:00,120.9,134.113,54.5,32.225 -2020-11-06 14:30:00,121.51,134.691,54.5,32.225 -2020-11-06 14:45:00,120.58,134.575,54.5,32.225 -2020-11-06 15:00:00,122.67,133.238,55.131,32.225 -2020-11-06 15:15:00,118.7,133.108,55.131,32.225 -2020-11-06 15:30:00,121.37,132.126,55.131,32.225 -2020-11-06 15:45:00,120.79,132.526,55.131,32.225 -2020-11-06 16:00:00,122.13,133.756,56.8,32.225 -2020-11-06 16:15:00,122.5,135.602,56.8,32.225 -2020-11-06 16:30:00,127.43,135.842,56.8,32.225 -2020-11-06 16:45:00,129.4,134.688,56.8,32.225 -2020-11-06 17:00:00,133.06,137.007,63.428999999999995,32.225 -2020-11-06 17:15:00,134.26,137.476,63.428999999999995,32.225 -2020-11-06 17:30:00,132.78,137.447,63.428999999999995,32.225 -2020-11-06 17:45:00,131.33,136.857,63.428999999999995,32.225 -2020-11-06 18:00:00,134.08,139.959,67.915,32.225 -2020-11-06 18:15:00,130.5,138.378,67.915,32.225 -2020-11-06 18:30:00,129.8,136.88299999999998,67.915,32.225 -2020-11-06 18:45:00,133.03,140.289,67.915,32.225 -2020-11-06 19:00:00,126.8,142.261,69.428,32.225 -2020-11-06 19:15:00,125.27,141.47899999999998,69.428,32.225 -2020-11-06 19:30:00,122.86,140.039,69.428,32.225 -2020-11-06 19:45:00,124.09,138.034,69.428,32.225 -2020-11-06 20:00:00,114.98,133.422,60.56100000000001,32.225 -2020-11-06 20:15:00,113.0,130.02100000000002,60.56100000000001,32.225 -2020-11-06 20:30:00,109.07,129.093,60.56100000000001,32.225 -2020-11-06 20:45:00,109.58,127.03299999999999,60.56100000000001,32.225 -2020-11-06 21:00:00,102.28,123.459,55.18600000000001,32.225 -2020-11-06 21:15:00,106.27,123.67200000000001,55.18600000000001,32.225 -2020-11-06 21:30:00,104.99,122.706,55.18600000000001,32.225 -2020-11-06 21:45:00,96.99,120.491,55.18600000000001,32.225 -2020-11-06 22:00:00,88.19,115.375,51.433,32.225 -2020-11-06 22:15:00,86.68,110.825,51.433,32.225 -2020-11-06 22:30:00,82.9,103.704,51.433,32.225 -2020-11-06 22:45:00,82.11,98.971,51.433,32.225 -2020-11-06 23:00:00,77.89,93.86200000000001,46.201,32.225 -2020-11-06 23:15:00,81.36,90.863,46.201,32.225 -2020-11-06 23:30:00,75.34,88.525,46.201,32.225 -2020-11-06 23:45:00,74.67,88.12899999999999,46.201,32.225 -2020-11-07 00:00:00,72.4,75.25399999999999,42.576,32.047 -2020-11-07 00:15:00,76.3,73.567,42.576,32.047 -2020-11-07 00:30:00,76.56,74.063,42.576,32.047 -2020-11-07 00:45:00,74.82,74.486,42.576,32.047 -2020-11-07 01:00:00,70.47,76.05,39.34,32.047 -2020-11-07 01:15:00,73.1,76.13,39.34,32.047 -2020-11-07 01:30:00,72.65,75.42,39.34,32.047 -2020-11-07 01:45:00,66.71,75.745,39.34,32.047 -2020-11-07 02:00:00,66.31,77.455,37.582,32.047 -2020-11-07 02:15:00,71.52,77.346,37.582,32.047 -2020-11-07 02:30:00,68.13,77.218,37.582,32.047 -2020-11-07 02:45:00,68.12,78.291,37.582,32.047 -2020-11-07 03:00:00,64.49,80.518,36.523,32.047 -2020-11-07 03:15:00,70.44,80.949,36.523,32.047 -2020-11-07 03:30:00,71.27,80.803,36.523,32.047 -2020-11-07 03:45:00,70.23,82.641,36.523,32.047 -2020-11-07 04:00:00,62.21,91.421,36.347,32.047 -2020-11-07 04:15:00,62.81,100.102,36.347,32.047 -2020-11-07 04:30:00,63.23,98.935,36.347,32.047 -2020-11-07 04:45:00,63.56,99.926,36.347,32.047 -2020-11-07 05:00:00,64.36,118.78299999999999,36.407,32.047 -2020-11-07 05:15:00,65.17,133.07,36.407,32.047 -2020-11-07 05:30:00,64.94,128.732,36.407,32.047 -2020-11-07 05:45:00,64.25,124.027,36.407,32.047 -2020-11-07 06:00:00,70.3,138.67,38.228,32.047 -2020-11-07 06:15:00,70.95,156.255,38.228,32.047 -2020-11-07 06:30:00,68.59,150.749,38.228,32.047 -2020-11-07 06:45:00,70.43,145.471,38.228,32.047 -2020-11-07 07:00:00,75.46,143.015,41.905,32.047 -2020-11-07 07:15:00,76.33,146.082,41.905,32.047 -2020-11-07 07:30:00,78.63,148.593,41.905,32.047 -2020-11-07 07:45:00,81.45,151.06799999999998,41.905,32.047 -2020-11-07 08:00:00,83.9,153.178,46.051,32.047 -2020-11-07 08:15:00,83.65,154.811,46.051,32.047 -2020-11-07 08:30:00,84.05,154.248,46.051,32.047 -2020-11-07 08:45:00,85.96,153.055,46.051,32.047 -2020-11-07 09:00:00,87.34,150.039,46.683,32.047 -2020-11-07 09:15:00,87.81,148.872,46.683,32.047 -2020-11-07 09:30:00,86.74,148.283,46.683,32.047 -2020-11-07 09:45:00,86.17,147.033,46.683,32.047 -2020-11-07 10:00:00,86.08,143.433,44.425,32.047 -2020-11-07 10:15:00,85.25,141.98,44.425,32.047 -2020-11-07 10:30:00,85.52,139.95600000000002,44.425,32.047 -2020-11-07 10:45:00,85.84,139.55,44.425,32.047 -2020-11-07 11:00:00,86.36,135.795,42.148999999999994,32.047 -2020-11-07 11:15:00,87.42,134.597,42.148999999999994,32.047 -2020-11-07 11:30:00,87.9,135.195,42.148999999999994,32.047 -2020-11-07 11:45:00,89.42,135.241,42.148999999999994,32.047 -2020-11-07 12:00:00,86.24,131.194,39.683,32.047 -2020-11-07 12:15:00,84.54,129.049,39.683,32.047 -2020-11-07 12:30:00,83.03,129.739,39.683,32.047 -2020-11-07 12:45:00,82.28,130.041,39.683,32.047 -2020-11-07 13:00:00,80.02,129.766,37.154,32.047 -2020-11-07 13:15:00,79.68,128.201,37.154,32.047 -2020-11-07 13:30:00,78.82,127.529,37.154,32.047 -2020-11-07 13:45:00,78.57,127.15,37.154,32.047 -2020-11-07 14:00:00,77.79,126.416,36.457,32.047 -2020-11-07 14:15:00,78.58,125.419,36.457,32.047 -2020-11-07 14:30:00,78.89,124.588,36.457,32.047 -2020-11-07 14:45:00,80.87,124.78399999999999,36.457,32.047 -2020-11-07 15:00:00,80.77,124.04299999999999,38.257,32.047 -2020-11-07 15:15:00,82.29,124.734,38.257,32.047 -2020-11-07 15:30:00,83.92,124.965,38.257,32.047 -2020-11-07 15:45:00,86.04,125.09299999999999,38.257,32.047 -2020-11-07 16:00:00,87.33,126.383,41.181000000000004,32.047 -2020-11-07 16:15:00,88.36,128.577,41.181000000000004,32.047 -2020-11-07 16:30:00,90.62,128.856,41.181000000000004,32.047 -2020-11-07 16:45:00,95.89,128.364,41.181000000000004,32.047 -2020-11-07 17:00:00,101.97,130.005,46.806000000000004,32.047 -2020-11-07 17:15:00,101.76,130.86700000000002,46.806000000000004,32.047 -2020-11-07 17:30:00,104.34,130.73,46.806000000000004,32.047 -2020-11-07 17:45:00,104.83,130.05,46.806000000000004,32.047 -2020-11-07 18:00:00,105.8,133.376,52.073,32.047 -2020-11-07 18:15:00,105.2,133.58700000000002,52.073,32.047 -2020-11-07 18:30:00,103.89,133.498,52.073,32.047 -2020-11-07 18:45:00,101.96,133.372,52.073,32.047 -2020-11-07 19:00:00,100.94,135.424,53.608000000000004,32.047 -2020-11-07 19:15:00,98.9,133.929,53.608000000000004,32.047 -2020-11-07 19:30:00,97.79,133.262,53.608000000000004,32.047 -2020-11-07 19:45:00,96.56,131.755,53.608000000000004,32.047 -2020-11-07 20:00:00,91.1,128.975,50.265,32.047 -2020-11-07 20:15:00,85.93,126.74799999999999,50.265,32.047 -2020-11-07 20:30:00,85.7,125.228,50.265,32.047 -2020-11-07 20:45:00,83.77,123.581,50.265,32.047 -2020-11-07 21:00:00,79.67,120.964,45.766000000000005,32.047 -2020-11-07 21:15:00,79.69,121.325,45.766000000000005,32.047 -2020-11-07 21:30:00,78.88,121.25399999999999,45.766000000000005,32.047 -2020-11-07 21:45:00,77.61,118.55,45.766000000000005,32.047 -2020-11-07 22:00:00,75.18,114.319,45.97,32.047 -2020-11-07 22:15:00,76.21,111.5,45.97,32.047 -2020-11-07 22:30:00,73.1,108.414,45.97,32.047 -2020-11-07 22:45:00,72.11,105.071,45.97,32.047 -2020-11-07 23:00:00,68.65,101.348,40.415,32.047 -2020-11-07 23:15:00,68.16,97.39200000000001,40.415,32.047 -2020-11-07 23:30:00,64.73,94.814,40.415,32.047 -2020-11-07 23:45:00,63.74,92.92399999999999,40.415,32.047 -2020-11-08 00:00:00,56.78,76.44800000000001,36.376,32.047 -2020-11-08 00:15:00,59.59,74.069,36.376,32.047 -2020-11-08 00:30:00,59.04,74.27,36.376,32.047 -2020-11-08 00:45:00,58.74,75.078,36.376,32.047 -2020-11-08 01:00:00,56.0,76.681,32.992,32.047 -2020-11-08 01:15:00,56.15,77.38,32.992,32.047 -2020-11-08 01:30:00,53.94,76.958,32.992,32.047 -2020-11-08 01:45:00,55.96,76.921,32.992,32.047 -2020-11-08 02:00:00,53.18,78.202,32.327,32.047 -2020-11-08 02:15:00,54.85,77.861,32.327,32.047 -2020-11-08 02:30:00,53.77,78.33800000000001,32.327,32.047 -2020-11-08 02:45:00,54.28,79.574,32.327,32.047 -2020-11-08 03:00:00,54.05,82.25399999999999,31.169,32.047 -2020-11-08 03:15:00,54.15,82.494,31.169,32.047 -2020-11-08 03:30:00,54.49,82.912,31.169,32.047 -2020-11-08 03:45:00,52.1,84.354,31.169,32.047 -2020-11-08 04:00:00,54.3,92.965,30.796,32.047 -2020-11-08 04:15:00,54.34,100.863,30.796,32.047 -2020-11-08 04:30:00,55.34,100.29799999999999,30.796,32.047 -2020-11-08 04:45:00,52.7,101.26799999999999,30.796,32.047 -2020-11-08 05:00:00,55.15,118.09200000000001,30.848000000000003,32.047 -2020-11-08 05:15:00,56.75,130.61,30.848000000000003,32.047 -2020-11-08 05:30:00,56.68,125.994,30.848000000000003,32.047 -2020-11-08 05:45:00,57.37,121.301,30.848000000000003,32.047 -2020-11-08 06:00:00,58.77,134.852,31.166,32.047 -2020-11-08 06:15:00,56.79,151.719,31.166,32.047 -2020-11-08 06:30:00,59.58,145.264,31.166,32.047 -2020-11-08 06:45:00,61.46,138.931,31.166,32.047 -2020-11-08 07:00:00,63.74,138.10299999999998,33.527,32.047 -2020-11-08 07:15:00,64.26,139.97799999999998,33.527,32.047 -2020-11-08 07:30:00,66.51,142.24200000000002,33.527,32.047 -2020-11-08 07:45:00,65.6,144.219,33.527,32.047 -2020-11-08 08:00:00,71.16,147.767,36.616,32.047 -2020-11-08 08:15:00,72.66,149.776,36.616,32.047 -2020-11-08 08:30:00,73.14,150.658,36.616,32.047 -2020-11-08 08:45:00,75.05,150.73,36.616,32.047 -2020-11-08 09:00:00,75.61,147.37,37.857,32.047 -2020-11-08 09:15:00,73.91,146.389,37.857,32.047 -2020-11-08 09:30:00,74.91,145.84799999999998,37.857,32.047 -2020-11-08 09:45:00,74.9,144.869,37.857,32.047 -2020-11-08 10:00:00,75.28,143.132,36.319,32.047 -2020-11-08 10:15:00,81.36,142.055,36.319,32.047 -2020-11-08 10:30:00,83.52,140.497,36.319,32.047 -2020-11-08 10:45:00,84.81,139.138,36.319,32.047 -2020-11-08 11:00:00,89.21,135.871,37.236999999999995,32.047 -2020-11-08 11:15:00,90.66,134.592,37.236999999999995,32.047 -2020-11-08 11:30:00,90.88,134.764,37.236999999999995,32.047 -2020-11-08 11:45:00,89.95,135.306,37.236999999999995,32.047 -2020-11-08 12:00:00,87.53,131.175,34.871,32.047 -2020-11-08 12:15:00,85.32,130.161,34.871,32.047 -2020-11-08 12:30:00,83.34,129.931,34.871,32.047 -2020-11-08 12:45:00,78.63,129.341,34.871,32.047 -2020-11-08 13:00:00,69.57,128.405,29.738000000000003,32.047 -2020-11-08 13:15:00,69.16,128.797,29.738000000000003,32.047 -2020-11-08 13:30:00,66.72,127.609,29.738000000000003,32.047 -2020-11-08 13:45:00,67.62,127.12299999999999,29.738000000000003,32.047 -2020-11-08 14:00:00,66.83,126.906,27.333000000000002,32.047 -2020-11-08 14:15:00,66.82,126.916,27.333000000000002,32.047 -2020-11-08 14:30:00,67.73,126.59700000000001,27.333000000000002,32.047 -2020-11-08 14:45:00,69.57,126.15100000000001,27.333000000000002,32.047 -2020-11-08 15:00:00,71.36,124.36,28.232,32.047 -2020-11-08 15:15:00,70.85,125.339,28.232,32.047 -2020-11-08 15:30:00,72.33,125.96799999999999,28.232,32.047 -2020-11-08 15:45:00,74.18,126.693,28.232,32.047 -2020-11-08 16:00:00,77.95,128.625,32.815,32.047 -2020-11-08 16:15:00,78.12,130.292,32.815,32.047 -2020-11-08 16:30:00,81.11,131.095,32.815,32.047 -2020-11-08 16:45:00,86.31,130.768,32.815,32.047 -2020-11-08 17:00:00,91.61,132.41899999999998,43.068999999999996,32.047 -2020-11-08 17:15:00,92.71,133.623,43.068999999999996,32.047 -2020-11-08 17:30:00,94.77,133.976,43.068999999999996,32.047 -2020-11-08 17:45:00,96.36,135.0,43.068999999999996,32.047 -2020-11-08 18:00:00,97.41,138.14700000000002,50.498999999999995,32.047 -2020-11-08 18:15:00,96.11,139.069,50.498999999999995,32.047 -2020-11-08 18:30:00,96.01,137.498,50.498999999999995,32.047 -2020-11-08 18:45:00,94.28,138.656,50.498999999999995,32.047 -2020-11-08 19:00:00,92.22,141.273,53.481,32.047 -2020-11-08 19:15:00,90.52,139.755,53.481,32.047 -2020-11-08 19:30:00,89.88,138.872,53.481,32.047 -2020-11-08 19:45:00,88.47,138.131,53.481,32.047 -2020-11-08 20:00:00,86.19,135.379,51.687,32.047 -2020-11-08 20:15:00,84.49,133.74200000000002,51.687,32.047 -2020-11-08 20:30:00,83.73,133.315,51.687,32.047 -2020-11-08 20:45:00,83.27,130.233,51.687,32.047 -2020-11-08 21:00:00,79.79,125.823,47.674,32.047 -2020-11-08 21:15:00,80.23,125.645,47.674,32.047 -2020-11-08 21:30:00,79.81,125.53,47.674,32.047 -2020-11-08 21:45:00,80.44,123.037,47.674,32.047 -2020-11-08 22:00:00,78.11,118.802,48.178000000000004,32.047 -2020-11-08 22:15:00,78.45,114.829,48.178000000000004,32.047 -2020-11-08 22:30:00,76.78,109.508,48.178000000000004,32.047 -2020-11-08 22:45:00,77.39,105.12899999999999,48.178000000000004,32.047 -2020-11-08 23:00:00,74.59,99.45,42.553999999999995,32.047 -2020-11-08 23:15:00,73.28,97.175,42.553999999999995,32.047 -2020-11-08 23:30:00,72.41,94.932,42.553999999999995,32.047 -2020-11-08 23:45:00,72.06,93.675,42.553999999999995,32.047 -2020-11-09 00:00:00,68.05,80.07300000000001,37.177,32.225 -2020-11-09 00:15:00,68.1,79.87,37.177,32.225 -2020-11-09 00:30:00,68.17,79.997,37.177,32.225 -2020-11-09 00:45:00,67.31,80.294,37.177,32.225 -2020-11-09 01:00:00,65.98,82.068,35.358000000000004,32.225 -2020-11-09 01:15:00,65.25,82.456,35.358000000000004,32.225 -2020-11-09 01:30:00,64.89,82.23299999999999,35.358000000000004,32.225 -2020-11-09 01:45:00,65.37,82.22,35.358000000000004,32.225 -2020-11-09 02:00:00,63.02,83.67399999999999,35.03,32.225 -2020-11-09 02:15:00,64.11,83.83,35.03,32.225 -2020-11-09 02:30:00,64.54,84.574,35.03,32.225 -2020-11-09 02:45:00,64.76,85.359,35.03,32.225 -2020-11-09 03:00:00,64.28,89.001,34.394,32.225 -2020-11-09 03:15:00,66.01,90.586,34.394,32.225 -2020-11-09 03:30:00,66.41,91.126,34.394,32.225 -2020-11-09 03:45:00,67.37,92.016,34.394,32.225 -2020-11-09 04:00:00,69.97,104.598,34.421,32.225 -2020-11-09 04:15:00,71.09,116.309,34.421,32.225 -2020-11-09 04:30:00,71.91,116.883,34.421,32.225 -2020-11-09 04:45:00,79.32,118.117,34.421,32.225 -2020-11-09 05:00:00,82.71,147.628,39.435,32.225 -2020-11-09 05:15:00,87.16,176.90900000000002,39.435,32.225 -2020-11-09 05:30:00,87.32,171.798,39.435,32.225 -2020-11-09 05:45:00,101.72,162.31799999999998,39.435,32.225 -2020-11-09 06:00:00,110.45,160.78,55.685,32.225 -2020-11-09 06:15:00,113.86,164.645,55.685,32.225 -2020-11-09 06:30:00,115.39,164.861,55.685,32.225 -2020-11-09 06:45:00,118.64,166.217,55.685,32.225 -2020-11-09 07:00:00,122.58,167.396,66.837,32.225 -2020-11-09 07:15:00,122.82,170.988,66.837,32.225 -2020-11-09 07:30:00,126.22,172.438,66.837,32.225 -2020-11-09 07:45:00,125.54,172.898,66.837,32.225 -2020-11-09 08:00:00,127.34,172.813,72.217,32.225 -2020-11-09 08:15:00,127.18,172.938,72.217,32.225 -2020-11-09 08:30:00,126.88,170.8,72.217,32.225 -2020-11-09 08:45:00,127.51,168.736,72.217,32.225 -2020-11-09 09:00:00,128.25,164.46599999999998,66.117,32.225 -2020-11-09 09:15:00,130.74,160.498,66.117,32.225 -2020-11-09 09:30:00,135.41,158.94,66.117,32.225 -2020-11-09 09:45:00,128.44,157.295,66.117,32.225 -2020-11-09 10:00:00,121.64,155.171,62.1,32.225 -2020-11-09 10:15:00,122.64,153.78,62.1,32.225 -2020-11-09 10:30:00,121.37,151.439,62.1,32.225 -2020-11-09 10:45:00,120.12,150.023,62.1,32.225 -2020-11-09 11:00:00,119.17,145.142,60.021,32.225 -2020-11-09 11:15:00,118.05,145.196,60.021,32.225 -2020-11-09 11:30:00,120.45,146.566,60.021,32.225 -2020-11-09 11:45:00,127.24,146.98,60.021,32.225 -2020-11-09 12:00:00,123.12,143.612,56.75899999999999,32.225 -2020-11-09 12:15:00,119.35,142.637,56.75899999999999,32.225 -2020-11-09 12:30:00,119.39,142.264,56.75899999999999,32.225 -2020-11-09 12:45:00,119.84,142.754,56.75899999999999,32.225 -2020-11-09 13:00:00,116.9,142.586,56.04600000000001,32.225 -2020-11-09 13:15:00,116.79,141.71200000000002,56.04600000000001,32.225 -2020-11-09 13:30:00,113.73,140.2,56.04600000000001,32.225 -2020-11-09 13:45:00,114.61,140.019,56.04600000000001,32.225 -2020-11-09 14:00:00,115.9,139.042,55.475,32.225 -2020-11-09 14:15:00,116.58,138.798,55.475,32.225 -2020-11-09 14:30:00,120.75,138.077,55.475,32.225 -2020-11-09 14:45:00,120.18,138.275,55.475,32.225 -2020-11-09 15:00:00,123.17,137.644,57.048,32.225 -2020-11-09 15:15:00,122.33,137.42,57.048,32.225 -2020-11-09 15:30:00,124.39,137.74,57.048,32.225 -2020-11-09 15:45:00,125.72,138.004,57.048,32.225 -2020-11-09 16:00:00,126.17,140.192,59.06,32.225 -2020-11-09 16:15:00,125.85,141.375,59.06,32.225 -2020-11-09 16:30:00,127.02,141.27700000000002,59.06,32.225 -2020-11-09 16:45:00,131.92,140.181,59.06,32.225 -2020-11-09 17:00:00,136.09,141.148,65.419,32.225 -2020-11-09 17:15:00,136.48,141.859,65.419,32.225 -2020-11-09 17:30:00,137.83,141.708,65.419,32.225 -2020-11-09 17:45:00,137.56,141.585,65.419,32.225 -2020-11-09 18:00:00,135.48,144.59,69.345,32.225 -2020-11-09 18:15:00,133.71,143.328,69.345,32.225 -2020-11-09 18:30:00,132.59,141.934,69.345,32.225 -2020-11-09 18:45:00,132.3,144.716,69.345,32.225 -2020-11-09 19:00:00,127.93,146.127,73.825,32.225 -2020-11-09 19:15:00,126.34,144.3,73.825,32.225 -2020-11-09 19:30:00,123.57,143.6,73.825,32.225 -2020-11-09 19:45:00,121.6,142.05,73.825,32.225 -2020-11-09 20:00:00,115.11,137.256,64.027,32.225 -2020-11-09 20:15:00,112.04,134.487,64.027,32.225 -2020-11-09 20:30:00,108.08,132.988,64.027,32.225 -2020-11-09 20:45:00,107.81,131.05700000000002,64.027,32.225 -2020-11-09 21:00:00,107.92,126.76799999999999,57.952,32.225 -2020-11-09 21:15:00,109.06,125.969,57.952,32.225 -2020-11-09 21:30:00,106.65,125.475,57.952,32.225 -2020-11-09 21:45:00,100.41,122.554,57.952,32.225 -2020-11-09 22:00:00,92.6,115.631,53.031000000000006,32.225 -2020-11-09 22:15:00,93.12,111.51899999999999,53.031000000000006,32.225 -2020-11-09 22:30:00,95.32,98.29700000000001,53.031000000000006,32.225 -2020-11-09 22:45:00,94.64,90.72200000000001,53.031000000000006,32.225 -2020-11-09 23:00:00,90.53,85.54799999999999,45.085,32.225 -2020-11-09 23:15:00,89.52,84.38799999999999,45.085,32.225 -2020-11-09 23:30:00,88.09,83.963,45.085,32.225 -2020-11-09 23:45:00,87.29,84.242,45.085,32.225 -2020-11-10 00:00:00,79.63,79.074,42.843,32.225 -2020-11-10 00:15:00,76.93,80.104,42.843,32.225 -2020-11-10 00:30:00,82.65,79.929,42.843,32.225 -2020-11-10 00:45:00,81.56,79.919,42.843,32.225 -2020-11-10 01:00:00,80.25,81.372,41.542,32.225 -2020-11-10 01:15:00,76.77,81.541,41.542,32.225 -2020-11-10 01:30:00,79.55,81.388,41.542,32.225 -2020-11-10 01:45:00,79.66,81.351,41.542,32.225 -2020-11-10 02:00:00,78.05,82.617,40.19,32.225 -2020-11-10 02:15:00,73.84,83.189,40.19,32.225 -2020-11-10 02:30:00,77.2,83.39,40.19,32.225 -2020-11-10 02:45:00,79.47,84.315,40.19,32.225 -2020-11-10 03:00:00,79.27,86.96600000000001,39.626,32.225 -2020-11-10 03:15:00,78.58,88.43700000000001,39.626,32.225 -2020-11-10 03:30:00,80.16,89.265,39.626,32.225 -2020-11-10 03:45:00,80.07,89.79799999999999,39.626,32.225 -2020-11-10 04:00:00,78.72,101.744,40.196999999999996,32.225 -2020-11-10 04:15:00,81.21,113.26899999999999,40.196999999999996,32.225 -2020-11-10 04:30:00,83.65,113.595,40.196999999999996,32.225 -2020-11-10 04:45:00,86.52,115.79,40.196999999999996,32.225 -2020-11-10 05:00:00,87.82,149.101,43.378,32.225 -2020-11-10 05:15:00,90.74,178.55599999999998,43.378,32.225 -2020-11-10 05:30:00,99.8,172.574,43.378,32.225 -2020-11-10 05:45:00,101.83,162.769,43.378,32.225 -2020-11-10 06:00:00,103.85,160.92600000000002,55.691,32.225 -2020-11-10 06:15:00,109.52,165.903,55.691,32.225 -2020-11-10 06:30:00,115.89,165.612,55.691,32.225 -2020-11-10 06:45:00,121.67,166.38299999999998,55.691,32.225 -2020-11-10 07:00:00,125.99,167.50599999999997,65.567,32.225 -2020-11-10 07:15:00,125.23,170.90400000000002,65.567,32.225 -2020-11-10 07:30:00,125.48,172.033,65.567,32.225 -2020-11-10 07:45:00,127.15,172.24599999999998,65.567,32.225 -2020-11-10 08:00:00,130.42,172.225,73.001,32.225 -2020-11-10 08:15:00,129.33,171.486,73.001,32.225 -2020-11-10 08:30:00,128.62,169.31900000000002,73.001,32.225 -2020-11-10 08:45:00,130.72,166.703,73.001,32.225 -2020-11-10 09:00:00,130.86,161.987,67.08800000000001,32.225 -2020-11-10 09:15:00,133.46,159.045,67.08800000000001,32.225 -2020-11-10 09:30:00,134.07,158.188,67.08800000000001,32.225 -2020-11-10 09:45:00,134.92,156.86,67.08800000000001,32.225 -2020-11-10 10:00:00,138.69,153.835,62.803000000000004,32.225 -2020-11-10 10:15:00,136.4,151.625,62.803000000000004,32.225 -2020-11-10 10:30:00,138.3,149.394,62.803000000000004,32.225 -2020-11-10 10:45:00,138.45,148.505,62.803000000000004,32.225 -2020-11-10 11:00:00,138.8,144.653,60.155,32.225 -2020-11-10 11:15:00,140.26,144.589,60.155,32.225 -2020-11-10 11:30:00,138.98,144.757,60.155,32.225 -2020-11-10 11:45:00,138.44,145.502,60.155,32.225 -2020-11-10 12:00:00,136.24,141.084,56.845,32.225 -2020-11-10 12:15:00,132.78,139.921,56.845,32.225 -2020-11-10 12:30:00,132.82,140.374,56.845,32.225 -2020-11-10 12:45:00,133.55,140.88,56.845,32.225 -2020-11-10 13:00:00,133.51,140.255,56.163000000000004,32.225 -2020-11-10 13:15:00,133.18,139.71,56.163000000000004,32.225 -2020-11-10 13:30:00,132.48,139.026,56.163000000000004,32.225 -2020-11-10 13:45:00,135.29,138.667,56.163000000000004,32.225 -2020-11-10 14:00:00,132.64,137.986,55.934,32.225 -2020-11-10 14:15:00,133.0,137.806,55.934,32.225 -2020-11-10 14:30:00,130.66,137.678,55.934,32.225 -2020-11-10 14:45:00,129.66,137.57,55.934,32.225 -2020-11-10 15:00:00,129.09,136.569,57.43899999999999,32.225 -2020-11-10 15:15:00,128.92,136.889,57.43899999999999,32.225 -2020-11-10 15:30:00,125.2,137.341,57.43899999999999,32.225 -2020-11-10 15:45:00,127.27,137.435,57.43899999999999,32.225 -2020-11-10 16:00:00,129.73,139.649,59.968999999999994,32.225 -2020-11-10 16:15:00,128.3,141.219,59.968999999999994,32.225 -2020-11-10 16:30:00,129.86,141.44799999999998,59.968999999999994,32.225 -2020-11-10 16:45:00,133.6,140.843,59.968999999999994,32.225 -2020-11-10 17:00:00,138.8,142.224,67.428,32.225 -2020-11-10 17:15:00,136.91,143.123,67.428,32.225 -2020-11-10 17:30:00,136.54,143.278,67.428,32.225 -2020-11-10 17:45:00,137.56,142.963,67.428,32.225 -2020-11-10 18:00:00,136.51,145.55,71.533,32.225 -2020-11-10 18:15:00,134.26,144.453,71.533,32.225 -2020-11-10 18:30:00,133.81,142.766,71.533,32.225 -2020-11-10 18:45:00,133.71,146.015,71.533,32.225 -2020-11-10 19:00:00,131.4,147.056,73.32300000000001,32.225 -2020-11-10 19:15:00,128.61,145.097,73.32300000000001,32.225 -2020-11-10 19:30:00,126.59,143.855,73.32300000000001,32.225 -2020-11-10 19:45:00,125.18,142.424,73.32300000000001,32.225 -2020-11-10 20:00:00,121.52,137.89600000000002,64.166,32.225 -2020-11-10 20:15:00,115.1,134.157,64.166,32.225 -2020-11-10 20:30:00,113.81,133.317,64.166,32.225 -2020-11-10 20:45:00,112.32,131.153,64.166,32.225 -2020-11-10 21:00:00,107.06,126.714,57.891999999999996,32.225 -2020-11-10 21:15:00,105.3,125.95200000000001,57.891999999999996,32.225 -2020-11-10 21:30:00,102.67,125.04799999999999,57.891999999999996,32.225 -2020-11-10 21:45:00,101.94,122.339,57.891999999999996,32.225 -2020-11-10 22:00:00,98.35,116.615,53.242,32.225 -2020-11-10 22:15:00,100.16,112.191,53.242,32.225 -2020-11-10 22:30:00,98.28,99.18,53.242,32.225 -2020-11-10 22:45:00,96.68,91.795,53.242,32.225 -2020-11-10 23:00:00,88.01,86.35799999999999,46.665,32.225 -2020-11-10 23:15:00,84.04,85.154,46.665,32.225 -2020-11-10 23:30:00,84.59,84.492,46.665,32.225 -2020-11-10 23:45:00,81.05,84.535,46.665,32.225 -2020-11-11 00:00:00,83.4,79.433,43.16,32.225 -2020-11-11 00:15:00,83.26,80.444,43.16,32.225 -2020-11-11 00:30:00,84.67,80.27,43.16,32.225 -2020-11-11 00:45:00,81.96,80.248,43.16,32.225 -2020-11-11 01:00:00,78.35,81.723,40.972,32.225 -2020-11-11 01:15:00,81.15,81.906,40.972,32.225 -2020-11-11 01:30:00,81.08,81.768,40.972,32.225 -2020-11-11 01:45:00,81.16,81.72399999999999,40.972,32.225 -2020-11-11 02:00:00,78.61,83.00299999999999,39.749,32.225 -2020-11-11 02:15:00,80.73,83.584,39.749,32.225 -2020-11-11 02:30:00,81.45,83.777,39.749,32.225 -2020-11-11 02:45:00,79.88,84.698,39.749,32.225 -2020-11-11 03:00:00,77.2,87.337,39.422,32.225 -2020-11-11 03:15:00,82.68,88.831,39.422,32.225 -2020-11-11 03:30:00,82.22,89.661,39.422,32.225 -2020-11-11 03:45:00,83.11,90.179,39.422,32.225 -2020-11-11 04:00:00,83.29,102.134,40.505,32.225 -2020-11-11 04:15:00,85.37,113.684,40.505,32.225 -2020-11-11 04:30:00,86.75,114.00299999999999,40.505,32.225 -2020-11-11 04:45:00,86.34,116.20700000000001,40.505,32.225 -2020-11-11 05:00:00,93.0,149.563,43.397,32.225 -2020-11-11 05:15:00,96.5,179.05900000000003,43.397,32.225 -2020-11-11 05:30:00,96.82,173.071,43.397,32.225 -2020-11-11 05:45:00,95.16,163.246,43.397,32.225 -2020-11-11 06:00:00,104.31,161.401,55.218,32.225 -2020-11-11 06:15:00,112.72,166.389,55.218,32.225 -2020-11-11 06:30:00,122.08,166.125,55.218,32.225 -2020-11-11 06:45:00,124.33,166.916,55.218,32.225 -2020-11-11 07:00:00,126.72,168.04,67.39,32.225 -2020-11-11 07:15:00,123.92,171.447,67.39,32.225 -2020-11-11 07:30:00,127.19,172.601,67.39,32.225 -2020-11-11 07:45:00,125.8,172.813,67.39,32.225 -2020-11-11 08:00:00,126.17,172.80700000000002,74.345,32.225 -2020-11-11 08:15:00,124.97,172.047,74.345,32.225 -2020-11-11 08:30:00,126.2,169.903,74.345,32.225 -2020-11-11 08:45:00,125.23,167.25900000000001,74.345,32.225 -2020-11-11 09:00:00,124.4,162.53,69.336,32.225 -2020-11-11 09:15:00,125.54,159.589,69.336,32.225 -2020-11-11 09:30:00,123.59,158.725,69.336,32.225 -2020-11-11 09:45:00,121.93,157.375,69.336,32.225 -2020-11-11 10:00:00,121.82,154.341,64.291,32.225 -2020-11-11 10:15:00,120.73,152.095,64.291,32.225 -2020-11-11 10:30:00,118.92,149.842,64.291,32.225 -2020-11-11 10:45:00,121.37,148.938,64.291,32.225 -2020-11-11 11:00:00,120.95,145.084,62.20399999999999,32.225 -2020-11-11 11:15:00,122.46,145.001,62.20399999999999,32.225 -2020-11-11 11:30:00,121.56,145.16899999999998,62.20399999999999,32.225 -2020-11-11 11:45:00,121.47,145.9,62.20399999999999,32.225 -2020-11-11 12:00:00,120.37,141.465,59.042,32.225 -2020-11-11 12:15:00,119.79,140.303,59.042,32.225 -2020-11-11 12:30:00,119.63,140.78799999999998,59.042,32.225 -2020-11-11 12:45:00,121.96,141.296,59.042,32.225 -2020-11-11 13:00:00,120.59,140.635,57.907,32.225 -2020-11-11 13:15:00,122.95,140.09799999999998,57.907,32.225 -2020-11-11 13:30:00,118.81,139.411,57.907,32.225 -2020-11-11 13:45:00,117.0,139.046,57.907,32.225 -2020-11-11 14:00:00,116.01,138.31799999999998,58.358000000000004,32.225 -2020-11-11 14:15:00,115.36,138.151,58.358000000000004,32.225 -2020-11-11 14:30:00,116.38,138.061,58.358000000000004,32.225 -2020-11-11 14:45:00,119.55,137.955,58.358000000000004,32.225 -2020-11-11 15:00:00,123.96,136.944,59.348,32.225 -2020-11-11 15:15:00,123.92,137.278,59.348,32.225 -2020-11-11 15:30:00,125.52,137.766,59.348,32.225 -2020-11-11 15:45:00,123.32,137.872,59.348,32.225 -2020-11-11 16:00:00,125.75,140.063,61.413999999999994,32.225 -2020-11-11 16:15:00,126.76,141.656,61.413999999999994,32.225 -2020-11-11 16:30:00,128.31,141.88299999999998,61.413999999999994,32.225 -2020-11-11 16:45:00,133.13,141.327,61.413999999999994,32.225 -2020-11-11 17:00:00,138.86,142.668,67.107,32.225 -2020-11-11 17:15:00,136.54,143.589,67.107,32.225 -2020-11-11 17:30:00,137.81,143.75,67.107,32.225 -2020-11-11 17:45:00,137.09,143.44899999999998,67.107,32.225 -2020-11-11 18:00:00,136.75,146.037,71.92,32.225 -2020-11-11 18:15:00,134.87,144.91,71.92,32.225 -2020-11-11 18:30:00,134.79,143.232,71.92,32.225 -2020-11-11 18:45:00,135.01,146.47899999999998,71.92,32.225 -2020-11-11 19:00:00,130.77,147.524,75.09,32.225 -2020-11-11 19:15:00,128.61,145.55700000000002,75.09,32.225 -2020-11-11 19:30:00,126.23,144.30100000000002,75.09,32.225 -2020-11-11 19:45:00,129.72,142.842,75.09,32.225 -2020-11-11 20:00:00,120.35,138.33,65.977,32.225 -2020-11-11 20:15:00,114.91,134.583,65.977,32.225 -2020-11-11 20:30:00,111.86,133.71200000000002,65.977,32.225 -2020-11-11 20:45:00,111.47,131.542,65.977,32.225 -2020-11-11 21:00:00,106.24,127.095,58.798,32.225 -2020-11-11 21:15:00,111.59,126.32,58.798,32.225 -2020-11-11 21:30:00,109.24,125.426,58.798,32.225 -2020-11-11 21:45:00,110.46,122.704,58.798,32.225 -2020-11-11 22:00:00,100.23,116.978,54.486000000000004,32.225 -2020-11-11 22:15:00,95.43,112.539,54.486000000000004,32.225 -2020-11-11 22:30:00,93.35,99.57,54.486000000000004,32.225 -2020-11-11 22:45:00,97.79,92.194,54.486000000000004,32.225 -2020-11-11 23:00:00,94.38,86.758,47.783,32.225 -2020-11-11 23:15:00,93.93,85.53,47.783,32.225 -2020-11-11 23:30:00,85.88,84.87299999999999,47.783,32.225 -2020-11-11 23:45:00,84.95,84.90299999999999,47.783,32.225 -2020-11-12 00:00:00,85.17,79.789,43.88,32.225 -2020-11-12 00:15:00,87.34,80.78,43.88,32.225 -2020-11-12 00:30:00,85.98,80.609,43.88,32.225 -2020-11-12 00:45:00,80.22,80.575,43.88,32.225 -2020-11-12 01:00:00,83.59,82.071,42.242,32.225 -2020-11-12 01:15:00,84.86,82.26899999999999,42.242,32.225 -2020-11-12 01:30:00,83.06,82.146,42.242,32.225 -2020-11-12 01:45:00,81.74,82.094,42.242,32.225 -2020-11-12 02:00:00,83.52,83.387,40.918,32.225 -2020-11-12 02:15:00,83.31,83.976,40.918,32.225 -2020-11-12 02:30:00,78.25,84.161,40.918,32.225 -2020-11-12 02:45:00,78.19,85.07700000000001,40.918,32.225 -2020-11-12 03:00:00,75.8,87.70299999999999,40.411,32.225 -2020-11-12 03:15:00,78.38,89.221,40.411,32.225 -2020-11-12 03:30:00,82.84,90.053,40.411,32.225 -2020-11-12 03:45:00,85.06,90.555,40.411,32.225 -2020-11-12 04:00:00,83.04,102.51899999999999,41.246,32.225 -2020-11-12 04:15:00,79.36,114.094,41.246,32.225 -2020-11-12 04:30:00,80.43,114.40899999999999,41.246,32.225 -2020-11-12 04:45:00,81.3,116.619,41.246,32.225 -2020-11-12 05:00:00,85.8,150.023,44.533,32.225 -2020-11-12 05:15:00,88.26,179.558,44.533,32.225 -2020-11-12 05:30:00,92.04,173.56400000000002,44.533,32.225 -2020-11-12 05:45:00,97.64,163.719,44.533,32.225 -2020-11-12 06:00:00,113.62,161.872,55.005,32.225 -2020-11-12 06:15:00,116.6,166.87,55.005,32.225 -2020-11-12 06:30:00,120.04,166.635,55.005,32.225 -2020-11-12 06:45:00,120.88,167.44400000000002,55.005,32.225 -2020-11-12 07:00:00,127.95,168.56900000000002,64.597,32.225 -2020-11-12 07:15:00,124.59,171.987,64.597,32.225 -2020-11-12 07:30:00,125.29,173.16299999999998,64.597,32.225 -2020-11-12 07:45:00,127.13,173.375,64.597,32.225 -2020-11-12 08:00:00,129.63,173.38099999999997,71.71600000000001,32.225 -2020-11-12 08:15:00,127.3,172.602,71.71600000000001,32.225 -2020-11-12 08:30:00,127.82,170.482,71.71600000000001,32.225 -2020-11-12 08:45:00,127.07,167.80900000000003,71.71600000000001,32.225 -2020-11-12 09:00:00,123.74,163.067,66.51899999999999,32.225 -2020-11-12 09:15:00,124.7,160.127,66.51899999999999,32.225 -2020-11-12 09:30:00,127.83,159.257,66.51899999999999,32.225 -2020-11-12 09:45:00,129.16,157.885,66.51899999999999,32.225 -2020-11-12 10:00:00,129.07,154.842,63.04,32.225 -2020-11-12 10:15:00,128.54,152.56,63.04,32.225 -2020-11-12 10:30:00,127.42,150.284,63.04,32.225 -2020-11-12 10:45:00,124.95,149.365,63.04,32.225 -2020-11-12 11:00:00,124.62,145.50799999999998,60.998000000000005,32.225 -2020-11-12 11:15:00,124.2,145.406,60.998000000000005,32.225 -2020-11-12 11:30:00,123.72,145.57399999999998,60.998000000000005,32.225 -2020-11-12 11:45:00,125.51,146.29399999999998,60.998000000000005,32.225 -2020-11-12 12:00:00,126.91,141.84,58.27,32.225 -2020-11-12 12:15:00,122.96,140.68,58.27,32.225 -2020-11-12 12:30:00,121.82,141.197,58.27,32.225 -2020-11-12 12:45:00,116.29,141.707,58.27,32.225 -2020-11-12 13:00:00,117.07,141.012,57.196000000000005,32.225 -2020-11-12 13:15:00,119.85,140.481,57.196000000000005,32.225 -2020-11-12 13:30:00,119.33,139.791,57.196000000000005,32.225 -2020-11-12 13:45:00,120.26,139.41899999999998,57.196000000000005,32.225 -2020-11-12 14:00:00,118.83,138.64600000000002,57.38399999999999,32.225 -2020-11-12 14:15:00,120.08,138.493,57.38399999999999,32.225 -2020-11-12 14:30:00,121.28,138.439,57.38399999999999,32.225 -2020-11-12 14:45:00,124.13,138.335,57.38399999999999,32.225 -2020-11-12 15:00:00,125.14,137.315,58.647,32.225 -2020-11-12 15:15:00,123.4,137.661,58.647,32.225 -2020-11-12 15:30:00,122.12,138.186,58.647,32.225 -2020-11-12 15:45:00,123.51,138.30200000000002,58.647,32.225 -2020-11-12 16:00:00,126.72,140.471,60.083999999999996,32.225 -2020-11-12 16:15:00,125.87,142.08700000000002,60.083999999999996,32.225 -2020-11-12 16:30:00,129.87,142.313,60.083999999999996,32.225 -2020-11-12 16:45:00,133.87,141.805,60.083999999999996,32.225 -2020-11-12 17:00:00,137.7,143.105,65.85600000000001,32.225 -2020-11-12 17:15:00,137.13,144.049,65.85600000000001,32.225 -2020-11-12 17:30:00,139.61,144.218,65.85600000000001,32.225 -2020-11-12 17:45:00,138.78,143.93,65.85600000000001,32.225 -2020-11-12 18:00:00,137.47,146.519,69.855,32.225 -2020-11-12 18:15:00,136.57,145.361,69.855,32.225 -2020-11-12 18:30:00,135.22,143.692,69.855,32.225 -2020-11-12 18:45:00,135.89,146.94,69.855,32.225 -2020-11-12 19:00:00,131.65,147.986,74.015,32.225 -2020-11-12 19:15:00,131.85,146.012,74.015,32.225 -2020-11-12 19:30:00,129.37,144.74200000000002,74.015,32.225 -2020-11-12 19:45:00,126.94,143.257,74.015,32.225 -2020-11-12 20:00:00,119.74,138.762,65.316,32.225 -2020-11-12 20:15:00,117.13,135.005,65.316,32.225 -2020-11-12 20:30:00,115.09,134.105,65.316,32.225 -2020-11-12 20:45:00,111.94,131.92600000000002,65.316,32.225 -2020-11-12 21:00:00,107.77,127.473,58.403999999999996,32.225 -2020-11-12 21:15:00,113.2,126.684,58.403999999999996,32.225 -2020-11-12 21:30:00,112.61,125.8,58.403999999999996,32.225 -2020-11-12 21:45:00,110.3,123.066,58.403999999999996,32.225 -2020-11-12 22:00:00,100.12,117.338,54.092,32.225 -2020-11-12 22:15:00,96.26,112.885,54.092,32.225 -2020-11-12 22:30:00,99.58,99.958,54.092,32.225 -2020-11-12 22:45:00,99.58,92.59,54.092,32.225 -2020-11-12 23:00:00,93.92,87.156,48.18600000000001,32.225 -2020-11-12 23:15:00,90.66,85.905,48.18600000000001,32.225 -2020-11-12 23:30:00,82.77,85.25,48.18600000000001,32.225 -2020-11-12 23:45:00,93.39,85.26700000000001,48.18600000000001,32.225 -2020-11-13 00:00:00,88.54,78.727,45.18899999999999,32.225 -2020-11-13 00:15:00,87.54,79.914,45.18899999999999,32.225 -2020-11-13 00:30:00,83.28,79.76899999999999,45.18899999999999,32.225 -2020-11-13 00:45:00,75.68,79.967,45.18899999999999,32.225 -2020-11-13 01:00:00,83.0,81.128,43.256,32.225 -2020-11-13 01:15:00,83.41,81.67699999999999,43.256,32.225 -2020-11-13 01:30:00,80.05,81.719,43.256,32.225 -2020-11-13 01:45:00,78.52,81.625,43.256,32.225 -2020-11-13 02:00:00,74.26,83.368,42.312,32.225 -2020-11-13 02:15:00,81.05,83.87,42.312,32.225 -2020-11-13 02:30:00,84.7,84.721,42.312,32.225 -2020-11-13 02:45:00,85.03,85.37799999999999,42.312,32.225 -2020-11-13 03:00:00,79.15,87.684,41.833,32.225 -2020-11-13 03:15:00,80.84,89.316,41.833,32.225 -2020-11-13 03:30:00,82.96,90.04,41.833,32.225 -2020-11-13 03:45:00,87.43,91.09899999999999,41.833,32.225 -2020-11-13 04:00:00,85.9,103.27,42.732,32.225 -2020-11-13 04:15:00,84.02,114.059,42.732,32.225 -2020-11-13 04:30:00,88.32,114.913,42.732,32.225 -2020-11-13 04:45:00,91.69,116.14200000000001,42.732,32.225 -2020-11-13 05:00:00,90.57,148.593,46.254,32.225 -2020-11-13 05:15:00,89.24,179.515,46.254,32.225 -2020-11-13 05:30:00,100.06,174.269,46.254,32.225 -2020-11-13 05:45:00,105.63,164.176,46.254,32.225 -2020-11-13 06:00:00,111.79,162.717,56.76,32.225 -2020-11-13 06:15:00,111.89,166.858,56.76,32.225 -2020-11-13 06:30:00,118.4,166.08,56.76,32.225 -2020-11-13 06:45:00,122.39,167.889,56.76,32.225 -2020-11-13 07:00:00,129.36,168.747,66.029,32.225 -2020-11-13 07:15:00,127.21,173.19799999999998,66.029,32.225 -2020-11-13 07:30:00,127.28,173.50900000000001,66.029,32.225 -2020-11-13 07:45:00,128.94,173.06,66.029,32.225 -2020-11-13 08:00:00,131.25,172.65400000000002,73.128,32.225 -2020-11-13 08:15:00,129.89,171.84900000000002,73.128,32.225 -2020-11-13 08:30:00,128.17,170.36700000000002,73.128,32.225 -2020-11-13 08:45:00,128.29,166.558,73.128,32.225 -2020-11-13 09:00:00,123.97,161.243,68.23100000000001,32.225 -2020-11-13 09:15:00,128.09,159.393,68.23100000000001,32.225 -2020-11-13 09:30:00,127.96,157.974,68.23100000000001,32.225 -2020-11-13 09:45:00,126.9,156.645,68.23100000000001,32.225 -2020-11-13 10:00:00,123.72,152.725,64.733,32.225 -2020-11-13 10:15:00,124.12,150.817,64.733,32.225 -2020-11-13 10:30:00,121.64,148.661,64.733,32.225 -2020-11-13 10:45:00,122.3,147.386,64.733,32.225 -2020-11-13 11:00:00,119.98,143.577,62.0,32.225 -2020-11-13 11:15:00,121.51,142.455,62.0,32.225 -2020-11-13 11:30:00,120.54,143.74,62.0,32.225 -2020-11-13 11:45:00,120.13,144.156,62.0,32.225 -2020-11-13 12:00:00,119.43,140.618,57.876999999999995,32.225 -2020-11-13 12:15:00,119.97,137.744,57.876999999999995,32.225 -2020-11-13 12:30:00,120.64,138.444,57.876999999999995,32.225 -2020-11-13 12:45:00,119.97,139.069,57.876999999999995,32.225 -2020-11-13 13:00:00,117.26,139.217,55.585,32.225 -2020-11-13 13:15:00,116.98,139.343,55.585,32.225 -2020-11-13 13:30:00,114.0,138.91899999999998,55.585,32.225 -2020-11-13 13:45:00,115.51,138.592,55.585,32.225 -2020-11-13 14:00:00,113.7,136.691,54.5,32.225 -2020-11-13 14:15:00,116.76,136.561,54.5,32.225 -2020-11-13 14:30:00,115.06,137.4,54.5,32.225 -2020-11-13 14:45:00,117.33,137.297,54.5,32.225 -2020-11-13 15:00:00,118.57,135.893,55.131,32.225 -2020-11-13 15:15:00,121.83,135.856,55.131,32.225 -2020-11-13 15:30:00,118.63,135.135,55.131,32.225 -2020-11-13 15:45:00,120.04,135.612,55.131,32.225 -2020-11-13 16:00:00,123.01,136.678,56.8,32.225 -2020-11-13 16:15:00,123.05,138.69,56.8,32.225 -2020-11-13 16:30:00,127.02,138.92,56.8,32.225 -2020-11-13 16:45:00,132.91,138.112,56.8,32.225 -2020-11-13 17:00:00,136.31,140.149,63.428999999999995,32.225 -2020-11-13 17:15:00,134.14,140.769,63.428999999999995,32.225 -2020-11-13 17:30:00,135.6,140.783,63.428999999999995,32.225 -2020-11-13 17:45:00,136.85,140.28799999999998,63.428999999999995,32.225 -2020-11-13 18:00:00,132.99,143.393,67.915,32.225 -2020-11-13 18:15:00,132.55,141.596,67.915,32.225 -2020-11-13 18:30:00,131.85,140.16899999999998,67.915,32.225 -2020-11-13 18:45:00,131.77,143.563,67.915,32.225 -2020-11-13 19:00:00,127.72,145.562,69.428,32.225 -2020-11-13 19:15:00,126.33,144.722,69.428,32.225 -2020-11-13 19:30:00,123.37,143.181,69.428,32.225 -2020-11-13 19:45:00,122.12,140.983,69.428,32.225 -2020-11-13 20:00:00,116.08,136.49200000000002,60.56100000000001,32.225 -2020-11-13 20:15:00,112.51,133.029,60.56100000000001,32.225 -2020-11-13 20:30:00,109.7,131.888,60.56100000000001,32.225 -2020-11-13 20:45:00,109.08,129.773,60.56100000000001,32.225 -2020-11-13 21:00:00,102.38,126.15,55.18600000000001,32.225 -2020-11-13 21:15:00,102.35,126.273,55.18600000000001,32.225 -2020-11-13 21:30:00,103.54,125.37200000000001,55.18600000000001,32.225 -2020-11-13 21:45:00,102.4,123.066,55.18600000000001,32.225 -2020-11-13 22:00:00,94.88,117.936,51.433,32.225 -2020-11-13 22:15:00,90.57,113.288,51.433,32.225 -2020-11-13 22:30:00,83.68,106.458,51.433,32.225 -2020-11-13 22:45:00,88.07,101.781,51.433,32.225 -2020-11-13 23:00:00,87.2,96.691,46.201,32.225 -2020-11-13 23:15:00,86.93,93.52,46.201,32.225 -2020-11-13 23:30:00,81.06,91.205,46.201,32.225 -2020-11-13 23:45:00,75.58,90.72,46.201,32.225 -2020-11-14 00:00:00,73.58,77.764,42.576,32.047 -2020-11-14 00:15:00,74.26,75.94,42.576,32.047 -2020-11-14 00:30:00,76.41,76.453,42.576,32.047 -2020-11-14 00:45:00,75.17,76.795,42.576,32.047 -2020-11-14 01:00:00,67.85,78.506,39.34,32.047 -2020-11-14 01:15:00,73.52,78.689,39.34,32.047 -2020-11-14 01:30:00,72.25,78.084,39.34,32.047 -2020-11-14 01:45:00,73.31,78.359,39.34,32.047 -2020-11-14 02:00:00,65.19,80.163,37.582,32.047 -2020-11-14 02:15:00,71.47,80.111,37.582,32.047 -2020-11-14 02:30:00,72.68,79.926,37.582,32.047 -2020-11-14 02:45:00,72.1,80.97,37.582,32.047 -2020-11-14 03:00:00,65.86,83.103,36.523,32.047 -2020-11-14 03:15:00,72.14,83.70299999999999,36.523,32.047 -2020-11-14 03:30:00,71.64,83.572,36.523,32.047 -2020-11-14 03:45:00,67.64,85.304,36.523,32.047 -2020-11-14 04:00:00,64.63,94.146,36.347,32.047 -2020-11-14 04:15:00,66.11,103.001,36.347,32.047 -2020-11-14 04:30:00,66.46,101.794,36.347,32.047 -2020-11-14 04:45:00,67.22,102.839,36.347,32.047 -2020-11-14 05:00:00,66.75,122.023,36.407,32.047 -2020-11-14 05:15:00,66.43,136.593,36.407,32.047 -2020-11-14 05:30:00,66.52,132.209,36.407,32.047 -2020-11-14 05:45:00,69.84,127.36399999999999,36.407,32.047 -2020-11-14 06:00:00,68.77,141.99200000000002,38.228,32.047 -2020-11-14 06:15:00,71.68,159.651,38.228,32.047 -2020-11-14 06:30:00,74.01,154.347,38.228,32.047 -2020-11-14 06:45:00,75.87,149.19799999999998,38.228,32.047 -2020-11-14 07:00:00,77.55,146.75,41.905,32.047 -2020-11-14 07:15:00,78.66,149.886,41.905,32.047 -2020-11-14 07:30:00,84.59,152.562,41.905,32.047 -2020-11-14 07:45:00,85.21,155.03799999999998,41.905,32.047 -2020-11-14 08:00:00,88.67,157.241,46.051,32.047 -2020-11-14 08:15:00,88.43,158.736,46.051,32.047 -2020-11-14 08:30:00,88.2,158.338,46.051,32.047 -2020-11-14 08:45:00,89.95,156.94899999999998,46.051,32.047 -2020-11-14 09:00:00,90.45,153.843,46.683,32.047 -2020-11-14 09:15:00,92.7,152.679,46.683,32.047 -2020-11-14 09:30:00,89.36,152.045,46.683,32.047 -2020-11-14 09:45:00,91.08,150.639,46.683,32.047 -2020-11-14 10:00:00,92.47,146.975,44.425,32.047 -2020-11-14 10:15:00,89.41,145.27,44.425,32.047 -2020-11-14 10:30:00,89.74,143.088,44.425,32.047 -2020-11-14 10:45:00,89.93,142.576,44.425,32.047 -2020-11-14 11:00:00,88.64,138.803,42.148999999999994,32.047 -2020-11-14 11:15:00,89.78,137.474,42.148999999999994,32.047 -2020-11-14 11:30:00,87.93,138.07,42.148999999999994,32.047 -2020-11-14 11:45:00,90.55,138.03,42.148999999999994,32.047 -2020-11-14 12:00:00,88.09,133.856,39.683,32.047 -2020-11-14 12:15:00,84.91,131.719,39.683,32.047 -2020-11-14 12:30:00,85.92,132.635,39.683,32.047 -2020-11-14 12:45:00,81.63,132.94799999999998,39.683,32.047 -2020-11-14 13:00:00,83.08,132.429,37.154,32.047 -2020-11-14 13:15:00,79.91,130.911,37.154,32.047 -2020-11-14 13:30:00,78.46,130.222,37.154,32.047 -2020-11-14 13:45:00,81.0,129.796,37.154,32.047 -2020-11-14 14:00:00,78.11,128.738,36.457,32.047 -2020-11-14 14:15:00,80.05,127.838,36.457,32.047 -2020-11-14 14:30:00,79.25,127.26700000000001,36.457,32.047 -2020-11-14 14:45:00,81.47,127.476,36.457,32.047 -2020-11-14 15:00:00,82.81,126.669,38.257,32.047 -2020-11-14 15:15:00,83.68,127.45100000000001,38.257,32.047 -2020-11-14 15:30:00,86.67,127.93799999999999,38.257,32.047 -2020-11-14 15:45:00,89.46,128.145,38.257,32.047 -2020-11-14 16:00:00,93.0,129.27100000000002,41.181000000000004,32.047 -2020-11-14 16:15:00,94.17,131.631,41.181000000000004,32.047 -2020-11-14 16:30:00,97.49,131.899,41.181000000000004,32.047 -2020-11-14 16:45:00,98.74,131.75,41.181000000000004,32.047 -2020-11-14 17:00:00,103.86,133.11,46.806000000000004,32.047 -2020-11-14 17:15:00,102.08,134.124,46.806000000000004,32.047 -2020-11-14 17:30:00,106.38,134.031,46.806000000000004,32.047 -2020-11-14 17:45:00,104.65,133.447,46.806000000000004,32.047 -2020-11-14 18:00:00,105.57,136.77700000000002,52.073,32.047 -2020-11-14 18:15:00,107.67,136.77700000000002,52.073,32.047 -2020-11-14 18:30:00,107.18,136.754,52.073,32.047 -2020-11-14 18:45:00,104.68,136.619,52.073,32.047 -2020-11-14 19:00:00,106.23,138.694,53.608000000000004,32.047 -2020-11-14 19:15:00,102.11,137.142,53.608000000000004,32.047 -2020-11-14 19:30:00,101.48,136.377,53.608000000000004,32.047 -2020-11-14 19:45:00,102.58,134.679,53.608000000000004,32.047 -2020-11-14 20:00:00,93.26,132.019,50.265,32.047 -2020-11-14 20:15:00,91.95,129.72899999999998,50.265,32.047 -2020-11-14 20:30:00,89.67,127.99700000000001,50.265,32.047 -2020-11-14 20:45:00,87.38,126.29799999999999,50.265,32.047 -2020-11-14 21:00:00,82.46,123.631,45.766000000000005,32.047 -2020-11-14 21:15:00,82.49,123.90100000000001,45.766000000000005,32.047 -2020-11-14 21:30:00,81.16,123.895,45.766000000000005,32.047 -2020-11-14 21:45:00,81.79,121.103,45.766000000000005,32.047 -2020-11-14 22:00:00,76.57,116.85799999999999,45.97,32.047 -2020-11-14 22:15:00,78.47,113.943,45.97,32.047 -2020-11-14 22:30:00,73.69,111.147,45.97,32.047 -2020-11-14 22:45:00,74.23,107.86,45.97,32.047 -2020-11-14 23:00:00,71.23,104.15299999999999,40.415,32.047 -2020-11-14 23:15:00,73.31,100.02799999999999,40.415,32.047 -2020-11-14 23:30:00,67.49,97.47399999999999,40.415,32.047 -2020-11-14 23:45:00,69.47,95.495,40.415,32.047 -2020-11-15 00:00:00,64.33,78.939,36.376,32.047 -2020-11-15 00:15:00,64.14,76.423,36.376,32.047 -2020-11-15 00:30:00,62.75,76.64,36.376,32.047 -2020-11-15 00:45:00,61.39,77.367,36.376,32.047 -2020-11-15 01:00:00,59.62,79.115,32.992,32.047 -2020-11-15 01:15:00,59.48,79.914,32.992,32.047 -2020-11-15 01:30:00,59.29,79.596,32.992,32.047 -2020-11-15 01:45:00,58.73,79.51,32.992,32.047 -2020-11-15 02:00:00,57.48,80.884,32.327,32.047 -2020-11-15 02:15:00,58.06,80.6,32.327,32.047 -2020-11-15 02:30:00,56.99,81.02,32.327,32.047 -2020-11-15 02:45:00,57.41,82.229,32.327,32.047 -2020-11-15 03:00:00,56.71,84.815,31.169,32.047 -2020-11-15 03:15:00,58.22,85.223,31.169,32.047 -2020-11-15 03:30:00,57.82,85.655,31.169,32.047 -2020-11-15 03:45:00,60.95,86.992,31.169,32.047 -2020-11-15 04:00:00,58.41,95.665,30.796,32.047 -2020-11-15 04:15:00,58.67,103.736,30.796,32.047 -2020-11-15 04:30:00,59.85,103.132,30.796,32.047 -2020-11-15 04:45:00,60.47,104.154,30.796,32.047 -2020-11-15 05:00:00,61.1,121.301,30.848000000000003,32.047 -2020-11-15 05:15:00,62.05,134.10299999999998,30.848000000000003,32.047 -2020-11-15 05:30:00,61.78,129.438,30.848000000000003,32.047 -2020-11-15 05:45:00,62.1,124.605,30.848000000000003,32.047 -2020-11-15 06:00:00,63.73,138.144,31.166,32.047 -2020-11-15 06:15:00,64.63,155.086,31.166,32.047 -2020-11-15 06:30:00,63.71,148.829,31.166,32.047 -2020-11-15 06:45:00,66.0,142.626,31.166,32.047 -2020-11-15 07:00:00,69.28,141.808,33.527,32.047 -2020-11-15 07:15:00,69.23,143.747,33.527,32.047 -2020-11-15 07:30:00,70.66,146.174,33.527,32.047 -2020-11-15 07:45:00,72.74,148.149,33.527,32.047 -2020-11-15 08:00:00,76.18,151.78799999999998,36.616,32.047 -2020-11-15 08:15:00,75.92,153.657,36.616,32.047 -2020-11-15 08:30:00,76.26,154.701,36.616,32.047 -2020-11-15 08:45:00,76.69,154.578,36.616,32.047 -2020-11-15 09:00:00,75.86,151.127,37.857,32.047 -2020-11-15 09:15:00,76.01,150.151,37.857,32.047 -2020-11-15 09:30:00,76.34,149.567,37.857,32.047 -2020-11-15 09:45:00,75.23,148.433,37.857,32.047 -2020-11-15 10:00:00,74.72,146.63299999999998,36.319,32.047 -2020-11-15 10:15:00,76.47,145.306,36.319,32.047 -2020-11-15 10:30:00,77.15,143.592,36.319,32.047 -2020-11-15 10:45:00,81.23,142.128,36.319,32.047 -2020-11-15 11:00:00,80.21,138.842,37.236999999999995,32.047 -2020-11-15 11:15:00,83.25,137.43200000000002,37.236999999999995,32.047 -2020-11-15 11:30:00,84.16,137.60299999999998,37.236999999999995,32.047 -2020-11-15 11:45:00,84.67,138.061,37.236999999999995,32.047 -2020-11-15 12:00:00,81.09,133.804,34.871,32.047 -2020-11-15 12:15:00,79.37,132.8,34.871,32.047 -2020-11-15 12:30:00,75.92,132.793,34.871,32.047 -2020-11-15 12:45:00,80.89,132.215,34.871,32.047 -2020-11-15 13:00:00,77.87,131.03799999999998,29.738000000000003,32.047 -2020-11-15 13:15:00,77.45,131.476,29.738000000000003,32.047 -2020-11-15 13:30:00,80.57,130.268,29.738000000000003,32.047 -2020-11-15 13:45:00,80.75,129.736,29.738000000000003,32.047 -2020-11-15 14:00:00,77.13,129.19799999999998,27.333000000000002,32.047 -2020-11-15 14:15:00,77.57,129.305,27.333000000000002,32.047 -2020-11-15 14:30:00,76.34,129.243,27.333000000000002,32.047 -2020-11-15 14:45:00,81.67,128.811,27.333000000000002,32.047 -2020-11-15 15:00:00,80.78,126.95700000000001,28.232,32.047 -2020-11-15 15:15:00,82.26,128.025,28.232,32.047 -2020-11-15 15:30:00,82.54,128.907,28.232,32.047 -2020-11-15 15:45:00,83.8,129.707,28.232,32.047 -2020-11-15 16:00:00,84.97,131.477,32.815,32.047 -2020-11-15 16:15:00,87.01,133.31,32.815,32.047 -2020-11-15 16:30:00,90.92,134.102,32.815,32.047 -2020-11-15 16:45:00,94.28,134.114,32.815,32.047 -2020-11-15 17:00:00,99.69,135.486,43.068999999999996,32.047 -2020-11-15 17:15:00,99.2,136.842,43.068999999999996,32.047 -2020-11-15 17:30:00,101.41,137.244,43.068999999999996,32.047 -2020-11-15 17:45:00,102.52,138.362,43.068999999999996,32.047 -2020-11-15 18:00:00,103.76,141.516,50.498999999999995,32.047 -2020-11-15 18:15:00,103.31,142.23,50.498999999999995,32.047 -2020-11-15 18:30:00,103.12,140.725,50.498999999999995,32.047 -2020-11-15 18:45:00,100.94,141.876,50.498999999999995,32.047 -2020-11-15 19:00:00,98.74,144.512,53.481,32.047 -2020-11-15 19:15:00,97.83,142.938,53.481,32.047 -2020-11-15 19:30:00,95.48,141.958,53.481,32.047 -2020-11-15 19:45:00,95.27,141.03,53.481,32.047 -2020-11-15 20:00:00,97.93,138.394,51.687,32.047 -2020-11-15 20:15:00,98.82,136.695,51.687,32.047 -2020-11-15 20:30:00,95.61,136.059,51.687,32.047 -2020-11-15 20:45:00,88.72,132.92600000000002,51.687,32.047 -2020-11-15 21:00:00,85.57,128.464,47.674,32.047 -2020-11-15 21:15:00,87.2,128.194,47.674,32.047 -2020-11-15 21:30:00,86.41,128.145,47.674,32.047 -2020-11-15 21:45:00,91.7,125.568,47.674,32.047 -2020-11-15 22:00:00,90.91,121.318,48.178000000000004,32.047 -2020-11-15 22:15:00,94.56,117.251,48.178000000000004,32.047 -2020-11-15 22:30:00,86.94,112.219,48.178000000000004,32.047 -2020-11-15 22:45:00,86.52,107.897,48.178000000000004,32.047 -2020-11-15 23:00:00,86.59,102.23200000000001,42.553999999999995,32.047 -2020-11-15 23:15:00,89.06,99.788,42.553999999999995,32.047 -2020-11-15 23:30:00,87.84,97.57,42.553999999999995,32.047 -2020-11-15 23:45:00,83.38,96.226,42.553999999999995,32.047 -2020-11-16 00:00:00,77.1,99.905,37.177,32.225 -2020-11-16 00:15:00,77.33,98.662,37.177,32.225 -2020-11-16 00:30:00,82.22,99.729,37.177,32.225 -2020-11-16 00:45:00,82.47,101.445,37.177,32.225 -2020-11-16 01:00:00,81.36,102.945,35.358000000000004,32.225 -2020-11-16 01:15:00,75.91,104.265,35.358000000000004,32.225 -2020-11-16 01:30:00,77.02,104.413,35.358000000000004,32.225 -2020-11-16 01:45:00,82.03,104.87100000000001,35.358000000000004,32.225 -2020-11-16 02:00:00,81.18,106.01299999999999,35.03,32.225 -2020-11-16 02:15:00,77.98,106.837,35.03,32.225 -2020-11-16 02:30:00,75.5,107.139,35.03,32.225 -2020-11-16 02:45:00,80.51,108.699,35.03,32.225 -2020-11-16 03:00:00,81.9,112.40299999999999,34.394,32.225 -2020-11-16 03:15:00,83.19,113.646,34.394,32.225 -2020-11-16 03:30:00,82.04,115.22200000000001,34.394,32.225 -2020-11-16 03:45:00,82.89,115.84,34.394,32.225 -2020-11-16 04:00:00,84.77,130.126,34.421,32.225 -2020-11-16 04:15:00,83.73,143.125,34.421,32.225 -2020-11-16 04:30:00,79.15,144.792,34.421,32.225 -2020-11-16 04:45:00,87.58,145.659,34.421,32.225 -2020-11-16 05:00:00,93.5,175.607,39.435,32.225 -2020-11-16 05:15:00,96.13,205.88400000000001,39.435,32.225 -2020-11-16 05:30:00,95.49,202.571,39.435,32.225 -2020-11-16 05:45:00,96.86,193.637,39.435,32.225 -2020-11-16 06:00:00,106.96,190.03,55.685,32.225 -2020-11-16 06:15:00,117.57,193.713,55.685,32.225 -2020-11-16 06:30:00,124.3,195.49400000000003,55.685,32.225 -2020-11-16 06:45:00,128.75,196.998,55.685,32.225 -2020-11-16 07:00:00,127.88,198.408,66.837,32.225 -2020-11-16 07:15:00,130.09,202.535,66.837,32.225 -2020-11-16 07:30:00,131.28,204.43900000000002,66.837,32.225 -2020-11-16 07:45:00,132.13,204.878,66.837,32.225 -2020-11-16 08:00:00,134.59,203.71,72.217,32.225 -2020-11-16 08:15:00,135.13,204.238,72.217,32.225 -2020-11-16 08:30:00,134.93,202.28799999999998,72.217,32.225 -2020-11-16 08:45:00,135.48,199.72,72.217,32.225 -2020-11-16 09:00:00,137.04,194.734,66.117,32.225 -2020-11-16 09:15:00,137.72,189.67,66.117,32.225 -2020-11-16 09:30:00,139.23,186.65099999999998,66.117,32.225 -2020-11-16 09:45:00,140.89,184.546,66.117,32.225 -2020-11-16 10:00:00,140.75,182.533,62.1,32.225 -2020-11-16 10:15:00,140.27,180.21200000000002,62.1,32.225 -2020-11-16 10:30:00,139.75,178.195,62.1,32.225 -2020-11-16 10:45:00,141.87,176.75400000000002,62.1,32.225 -2020-11-16 11:00:00,139.36,173.687,60.021,32.225 -2020-11-16 11:15:00,138.76,173.46599999999998,60.021,32.225 -2020-11-16 11:30:00,136.87,174.088,60.021,32.225 -2020-11-16 11:45:00,137.88,173.037,60.021,32.225 -2020-11-16 12:00:00,136.95,168.87,56.75899999999999,32.225 -2020-11-16 12:15:00,136.17,167.93599999999998,56.75899999999999,32.225 -2020-11-16 12:30:00,138.07,167.206,56.75899999999999,32.225 -2020-11-16 12:45:00,139.07,167.998,56.75899999999999,32.225 -2020-11-16 13:00:00,135.65,167.93099999999998,56.04600000000001,32.225 -2020-11-16 13:15:00,138.73,167.377,56.04600000000001,32.225 -2020-11-16 13:30:00,134.79,165.9,56.04600000000001,32.225 -2020-11-16 13:45:00,133.83,165.859,56.04600000000001,32.225 -2020-11-16 14:00:00,130.83,165.37900000000002,55.475,32.225 -2020-11-16 14:15:00,130.65,165.18099999999998,55.475,32.225 -2020-11-16 14:30:00,129.75,164.22,55.475,32.225 -2020-11-16 14:45:00,129.95,164.2,55.475,32.225 -2020-11-16 15:00:00,130.58,164.608,57.048,32.225 -2020-11-16 15:15:00,129.71,164.543,57.048,32.225 -2020-11-16 15:30:00,129.12,165.127,57.048,32.225 -2020-11-16 15:45:00,129.23,166.125,57.048,32.225 -2020-11-16 16:00:00,130.93,169.43,59.06,32.225 -2020-11-16 16:15:00,132.04,170.313,59.06,32.225 -2020-11-16 16:30:00,136.67,171.088,59.06,32.225 -2020-11-16 16:45:00,138.56,170.58900000000003,59.06,32.225 -2020-11-16 17:00:00,139.38,173.562,65.419,32.225 -2020-11-16 17:15:00,139.96,173.8,65.419,32.225 -2020-11-16 17:30:00,140.06,173.597,65.419,32.225 -2020-11-16 17:45:00,139.57,173.247,65.419,32.225 -2020-11-16 18:00:00,137.5,175.109,69.345,32.225 -2020-11-16 18:15:00,136.2,174.597,69.345,32.225 -2020-11-16 18:30:00,135.18,173.084,69.345,32.225 -2020-11-16 18:45:00,135.88,173.261,69.345,32.225 -2020-11-16 19:00:00,133.06,174.851,73.825,32.225 -2020-11-16 19:15:00,129.71,172.138,73.825,32.225 -2020-11-16 19:30:00,128.36,171.513,73.825,32.225 -2020-11-16 19:45:00,126.9,168.456,73.825,32.225 -2020-11-16 20:00:00,121.58,164.43200000000002,64.027,32.225 -2020-11-16 20:15:00,117.62,160.344,64.027,32.225 -2020-11-16 20:30:00,113.7,156.911,64.027,32.225 -2020-11-16 20:45:00,112.48,154.009,64.027,32.225 -2020-11-16 21:00:00,108.97,152.451,57.952,32.225 -2020-11-16 21:15:00,107.75,150.361,57.952,32.225 -2020-11-16 21:30:00,110.85,148.789,57.952,32.225 -2020-11-16 21:45:00,111.19,146.843,57.952,32.225 -2020-11-16 22:00:00,106.86,138.77200000000002,53.031000000000006,32.225 -2020-11-16 22:15:00,98.64,134.02100000000002,53.031000000000006,32.225 -2020-11-16 22:30:00,94.58,118.351,53.031000000000006,32.225 -2020-11-16 22:45:00,92.11,110.148,53.031000000000006,32.225 -2020-11-16 23:00:00,89.04,105.603,45.085,32.225 -2020-11-16 23:15:00,88.11,104.111,45.085,32.225 -2020-11-16 23:30:00,84.69,104.76700000000001,45.085,32.225 -2020-11-16 23:45:00,83.57,104.964,45.085,32.225 -2020-11-17 00:00:00,84.58,99.30799999999999,42.843,32.225 -2020-11-17 00:15:00,87.16,99.41,42.843,32.225 -2020-11-17 00:30:00,88.47,99.79700000000001,42.843,32.225 -2020-11-17 00:45:00,83.12,100.8,42.843,32.225 -2020-11-17 01:00:00,78.7,102.07799999999999,41.542,32.225 -2020-11-17 01:15:00,84.09,103.046,41.542,32.225 -2020-11-17 01:30:00,84.23,103.331,41.542,32.225 -2020-11-17 01:45:00,83.62,103.946,41.542,32.225 -2020-11-17 02:00:00,76.91,105.01700000000001,40.19,32.225 -2020-11-17 02:15:00,80.46,105.959,40.19,32.225 -2020-11-17 02:30:00,83.38,105.678,40.19,32.225 -2020-11-17 02:45:00,84.48,107.306,40.19,32.225 -2020-11-17 03:00:00,82.24,109.87200000000001,39.626,32.225 -2020-11-17 03:15:00,84.6,110.59299999999999,39.626,32.225 -2020-11-17 03:30:00,86.1,112.57600000000001,39.626,32.225 -2020-11-17 03:45:00,86.14,113.147,39.626,32.225 -2020-11-17 04:00:00,82.44,127.023,40.196999999999996,32.225 -2020-11-17 04:15:00,86.13,139.739,40.196999999999996,32.225 -2020-11-17 04:30:00,89.79,141.108,40.196999999999996,32.225 -2020-11-17 04:45:00,90.47,143.09799999999998,40.196999999999996,32.225 -2020-11-17 05:00:00,88.89,177.57299999999998,43.378,32.225 -2020-11-17 05:15:00,91.86,207.77200000000002,43.378,32.225 -2020-11-17 05:30:00,94.62,203.21400000000003,43.378,32.225 -2020-11-17 05:45:00,98.85,194.155,43.378,32.225 -2020-11-17 06:00:00,105.56,189.748,55.691,32.225 -2020-11-17 06:15:00,112.04,194.85299999999998,55.691,32.225 -2020-11-17 06:30:00,118.42,196.07299999999998,55.691,32.225 -2020-11-17 06:45:00,121.99,197.13400000000001,55.691,32.225 -2020-11-17 07:00:00,126.36,198.41299999999998,65.567,32.225 -2020-11-17 07:15:00,128.77,202.37,65.567,32.225 -2020-11-17 07:30:00,129.41,203.832,65.567,32.225 -2020-11-17 07:45:00,131.36,204.28900000000002,65.567,32.225 -2020-11-17 08:00:00,134.03,203.22099999999998,73.001,32.225 -2020-11-17 08:15:00,133.22,202.782,73.001,32.225 -2020-11-17 08:30:00,137.27,200.732,73.001,32.225 -2020-11-17 08:45:00,136.45,197.748,73.001,32.225 -2020-11-17 09:00:00,137.66,192.072,67.08800000000001,32.225 -2020-11-17 09:15:00,138.23,188.37599999999998,67.08800000000001,32.225 -2020-11-17 09:30:00,141.61,186.05599999999998,67.08800000000001,32.225 -2020-11-17 09:45:00,141.87,183.963,67.08800000000001,32.225 -2020-11-17 10:00:00,142.21,181.206,62.803000000000004,32.225 -2020-11-17 10:15:00,140.92,177.903,62.803000000000004,32.225 -2020-11-17 10:30:00,143.35,176.02200000000002,62.803000000000004,32.225 -2020-11-17 10:45:00,142.09,174.975,62.803000000000004,32.225 -2020-11-17 11:00:00,140.39,173.19299999999998,60.155,32.225 -2020-11-17 11:15:00,139.74,172.726,60.155,32.225 -2020-11-17 11:30:00,140.21,172.148,60.155,32.225 -2020-11-17 11:45:00,140.97,171.627,60.155,32.225 -2020-11-17 12:00:00,139.79,166.22099999999998,56.845,32.225 -2020-11-17 12:15:00,138.99,164.982,56.845,32.225 -2020-11-17 12:30:00,143.01,165.047,56.845,32.225 -2020-11-17 12:45:00,146.71,165.69400000000002,56.845,32.225 -2020-11-17 13:00:00,144.04,165.172,56.163000000000004,32.225 -2020-11-17 13:15:00,138.69,164.601,56.163000000000004,32.225 -2020-11-17 13:30:00,137.01,164.142,56.163000000000004,32.225 -2020-11-17 13:45:00,136.33,164.106,56.163000000000004,32.225 -2020-11-17 14:00:00,136.4,163.898,55.934,32.225 -2020-11-17 14:15:00,130.93,163.82,55.934,32.225 -2020-11-17 14:30:00,127.86,163.495,55.934,32.225 -2020-11-17 14:45:00,127.46,163.289,55.934,32.225 -2020-11-17 15:00:00,129.68,163.296,57.43899999999999,32.225 -2020-11-17 15:15:00,129.91,163.672,57.43899999999999,32.225 -2020-11-17 15:30:00,130.07,164.459,57.43899999999999,32.225 -2020-11-17 15:45:00,132.74,165.16299999999998,57.43899999999999,32.225 -2020-11-17 16:00:00,135.56,168.65099999999998,59.968999999999994,32.225 -2020-11-17 16:15:00,137.0,169.99099999999999,59.968999999999994,32.225 -2020-11-17 16:30:00,137.55,171.27900000000002,59.968999999999994,32.225 -2020-11-17 16:45:00,137.39,171.187,59.968999999999994,32.225 -2020-11-17 17:00:00,139.11,174.64,67.428,32.225 -2020-11-17 17:15:00,139.71,174.99,67.428,32.225 -2020-11-17 17:30:00,139.17,175.31,67.428,32.225 -2020-11-17 17:45:00,137.63,174.801,67.428,32.225 -2020-11-17 18:00:00,135.8,176.44,71.533,32.225 -2020-11-17 18:15:00,134.06,175.666,71.533,32.225 -2020-11-17 18:30:00,133.05,173.852,71.533,32.225 -2020-11-17 18:45:00,133.67,174.7,71.533,32.225 -2020-11-17 19:00:00,130.58,176.188,73.32300000000001,32.225 -2020-11-17 19:15:00,128.02,173.24200000000002,73.32300000000001,32.225 -2020-11-17 19:30:00,125.82,171.97799999999998,73.32300000000001,32.225 -2020-11-17 19:45:00,124.09,168.967,73.32300000000001,32.225 -2020-11-17 20:00:00,119.92,165.153,64.166,32.225 -2020-11-17 20:15:00,115.35,160.27200000000002,64.166,32.225 -2020-11-17 20:30:00,112.08,157.69899999999998,64.166,32.225 -2020-11-17 20:45:00,109.65,154.369,64.166,32.225 -2020-11-17 21:00:00,106.35,152.325,57.891999999999996,32.225 -2020-11-17 21:15:00,101.33,150.787,57.891999999999996,32.225 -2020-11-17 21:30:00,97.94,148.607,57.891999999999996,32.225 -2020-11-17 21:45:00,94.87,146.888,57.891999999999996,32.225 -2020-11-17 22:00:00,90.4,140.35,53.242,32.225 -2020-11-17 22:15:00,87.25,135.31,53.242,32.225 -2020-11-17 22:30:00,83.17,119.822,53.242,32.225 -2020-11-17 22:45:00,81.13,111.868,53.242,32.225 -2020-11-17 23:00:00,77.54,107.215,46.665,32.225 -2020-11-17 23:15:00,75.31,105.156,46.665,32.225 -2020-11-17 23:30:00,73.28,105.50200000000001,46.665,32.225 -2020-11-17 23:45:00,71.49,105.323,46.665,32.225 -2020-11-18 00:00:00,68.06,96.84,43.16,32.225 -2020-11-18 00:15:00,66.65,92.867,43.16,32.225 -2020-11-18 00:30:00,66.05,93.897,43.16,32.225 -2020-11-18 00:45:00,65.38,96.11,43.16,32.225 -2020-11-18 01:00:00,63.81,97.618,40.972,32.225 -2020-11-18 01:15:00,65.53,99.4,40.972,32.225 -2020-11-18 01:30:00,61.92,99.46,40.972,32.225 -2020-11-18 01:45:00,62.1,99.815,40.972,32.225 -2020-11-18 02:00:00,61.67,100.939,39.749,32.225 -2020-11-18 02:15:00,62.09,100.72399999999999,39.749,32.225 -2020-11-18 02:30:00,61.4,100.70299999999999,39.749,32.225 -2020-11-18 02:45:00,61.82,102.81200000000001,39.749,32.225 -2020-11-18 03:00:00,61.2,105.334,39.422,32.225 -2020-11-18 03:15:00,61.6,105.109,39.422,32.225 -2020-11-18 03:30:00,61.32,106.791,39.422,32.225 -2020-11-18 03:45:00,62.03,107.945,39.422,32.225 -2020-11-18 04:00:00,62.95,117.961,40.505,32.225 -2020-11-18 04:15:00,62.82,126.931,40.505,32.225 -2020-11-18 04:30:00,63.09,126.81700000000001,40.505,32.225 -2020-11-18 04:45:00,63.58,127.492,40.505,32.225 -2020-11-18 05:00:00,64.61,142.859,43.397,32.225 -2020-11-18 05:15:00,64.56,154.186,43.397,32.225 -2020-11-18 05:30:00,64.42,150.976,43.397,32.225 -2020-11-18 05:45:00,65.18,147.387,43.397,32.225 -2020-11-18 06:00:00,65.99,160.76,55.218,32.225 -2020-11-18 06:15:00,66.23,178.679,55.218,32.225 -2020-11-18 06:30:00,67.7,173.043,55.218,32.225 -2020-11-18 06:45:00,69.63,166.206,55.218,32.225 -2020-11-18 07:00:00,72.15,165.429,67.39,32.225 -2020-11-18 07:15:00,73.16,168.10299999999998,67.39,32.225 -2020-11-18 07:30:00,73.58,170.863,67.39,32.225 -2020-11-18 07:45:00,76.05,173.437,67.39,32.225 -2020-11-18 08:00:00,79.97,176.707,74.345,32.225 -2020-11-18 08:15:00,81.61,179.26,74.345,32.225 -2020-11-18 08:30:00,82.38,181.017,74.345,32.225 -2020-11-18 08:45:00,84.44,181.19799999999998,74.345,32.225 -2020-11-18 09:00:00,86.73,177.138,69.336,32.225 -2020-11-18 09:15:00,88.15,175.387,69.336,32.225 -2020-11-18 09:30:00,88.01,173.38099999999997,69.336,32.225 -2020-11-18 09:45:00,88.77,171.382,69.336,32.225 -2020-11-18 10:00:00,88.6,170.173,64.291,32.225 -2020-11-18 10:15:00,89.69,168.09,64.291,32.225 -2020-11-18 10:30:00,91.83,166.854,64.291,32.225 -2020-11-18 10:45:00,93.65,165.011,64.291,32.225 -2020-11-18 11:00:00,93.82,164.09099999999998,62.20399999999999,32.225 -2020-11-18 11:15:00,96.88,162.225,62.20399999999999,32.225 -2020-11-18 11:30:00,97.62,161.532,62.20399999999999,32.225 -2020-11-18 11:45:00,98.81,160.72899999999998,62.20399999999999,32.225 -2020-11-18 12:00:00,98.43,155.30100000000002,59.042,32.225 -2020-11-18 12:15:00,96.83,154.36,59.042,32.225 -2020-11-18 12:30:00,94.69,153.64600000000002,59.042,32.225 -2020-11-18 12:45:00,92.52,153.125,59.042,32.225 -2020-11-18 13:00:00,91.17,152.308,57.907,32.225 -2020-11-18 13:15:00,90.28,153.11700000000002,57.907,32.225 -2020-11-18 13:30:00,89.49,152.067,57.907,32.225 -2020-11-18 13:45:00,89.41,151.834,57.907,32.225 -2020-11-18 14:00:00,88.17,151.933,58.358000000000004,32.225 -2020-11-18 14:15:00,87.97,152.22299999999998,58.358000000000004,32.225 -2020-11-18 14:30:00,88.15,151.829,58.358000000000004,32.225 -2020-11-18 14:45:00,89.22,151.511,58.358000000000004,32.225 -2020-11-18 15:00:00,91.04,150.407,59.348,32.225 -2020-11-18 15:15:00,90.98,151.707,59.348,32.225 -2020-11-18 15:30:00,90.33,152.963,59.348,32.225 -2020-11-18 15:45:00,91.75,154.442,59.348,32.225 -2020-11-18 16:00:00,93.83,157.414,61.413999999999994,32.225 -2020-11-18 16:15:00,96.36,159.002,61.413999999999994,32.225 -2020-11-18 16:30:00,97.49,160.732,61.413999999999994,32.225 -2020-11-18 16:45:00,98.72,161.352,61.413999999999994,32.225 -2020-11-18 17:00:00,101.46,164.60299999999998,67.107,32.225 -2020-11-18 17:15:00,103.06,165.655,67.107,32.225 -2020-11-18 17:30:00,107.61,166.00599999999997,67.107,32.225 -2020-11-18 17:45:00,105.53,167.03400000000002,67.107,32.225 -2020-11-18 18:00:00,105.8,168.706,71.92,32.225 -2020-11-18 18:15:00,104.59,170.32,71.92,32.225 -2020-11-18 18:30:00,103.28,168.372,71.92,32.225 -2020-11-18 18:45:00,104.04,167.43,71.92,32.225 -2020-11-18 19:00:00,102.53,170.468,75.09,32.225 -2020-11-18 19:15:00,100.99,168.55,75.09,32.225 -2020-11-18 19:30:00,99.54,167.50900000000001,75.09,32.225 -2020-11-18 19:45:00,98.32,165.204,75.09,32.225 -2020-11-18 20:00:00,101.42,163.47799999999998,65.977,32.225 -2020-11-18 20:15:00,101.77,161.30200000000002,65.977,32.225 -2020-11-18 20:30:00,98.91,159.345,65.977,32.225 -2020-11-18 20:45:00,94.0,155.009,65.977,32.225 -2020-11-18 21:00:00,91.32,153.06799999999998,58.798,32.225 -2020-11-18 21:15:00,94.27,151.904,58.798,32.225 -2020-11-18 21:30:00,94.17,150.993,58.798,32.225 -2020-11-18 21:45:00,90.94,149.516,58.798,32.225 -2020-11-18 22:00:00,87.11,144.303,54.486000000000004,32.225 -2020-11-18 22:15:00,86.53,140.368,54.486000000000004,32.225 -2020-11-18 22:30:00,87.31,133.83,54.486000000000004,32.225 -2020-11-18 22:45:00,83.89,129.859,54.486000000000004,32.225 -2020-11-18 23:00:00,79.96,124.615,47.783,32.225 -2020-11-18 23:15:00,80.17,121.041,47.783,32.225 -2020-11-18 23:30:00,78.39,119.30799999999999,47.783,32.225 -2020-11-18 23:45:00,77.89,117.245,47.783,32.225 -2020-11-19 00:00:00,74.83,100.085,43.88,32.225 -2020-11-19 00:15:00,74.27,100.132,43.88,32.225 -2020-11-19 00:30:00,72.66,100.522,43.88,32.225 -2020-11-19 00:45:00,72.09,101.492,43.88,32.225 -2020-11-19 01:00:00,71.02,102.845,42.242,32.225 -2020-11-19 01:15:00,71.4,103.829,42.242,32.225 -2020-11-19 01:30:00,70.62,104.14,42.242,32.225 -2020-11-19 01:45:00,71.65,104.73,42.242,32.225 -2020-11-19 02:00:00,74.0,105.839,40.918,32.225 -2020-11-19 02:15:00,71.51,106.79,40.918,32.225 -2020-11-19 02:30:00,71.25,106.499,40.918,32.225 -2020-11-19 02:45:00,72.0,108.12299999999999,40.918,32.225 -2020-11-19 03:00:00,71.53,110.661,40.411,32.225 -2020-11-19 03:15:00,71.33,111.44,40.411,32.225 -2020-11-19 03:30:00,71.54,113.429,40.411,32.225 -2020-11-19 03:45:00,73.18,113.98200000000001,40.411,32.225 -2020-11-19 04:00:00,74.7,127.82600000000001,41.246,32.225 -2020-11-19 04:15:00,75.98,140.563,41.246,32.225 -2020-11-19 04:30:00,77.0,141.905,41.246,32.225 -2020-11-19 04:45:00,79.12,143.909,41.246,32.225 -2020-11-19 05:00:00,84.14,178.382,44.533,32.225 -2020-11-19 05:15:00,87.47,208.544,44.533,32.225 -2020-11-19 05:30:00,90.75,204.02200000000002,44.533,32.225 -2020-11-19 05:45:00,96.44,194.972,44.533,32.225 -2020-11-19 06:00:00,104.01,190.593,55.005,32.225 -2020-11-19 06:15:00,109.76,195.706,55.005,32.225 -2020-11-19 06:30:00,116.54,197.018,55.005,32.225 -2020-11-19 06:45:00,120.52,198.15200000000002,55.005,32.225 -2020-11-19 07:00:00,124.39,199.429,64.597,32.225 -2020-11-19 07:15:00,126.36,203.41299999999998,64.597,32.225 -2020-11-19 07:30:00,127.26,204.91,64.597,32.225 -2020-11-19 07:45:00,130.79,205.38299999999998,64.597,32.225 -2020-11-19 08:00:00,133.32,204.34599999999998,71.71600000000001,32.225 -2020-11-19 08:15:00,133.27,203.889,71.71600000000001,32.225 -2020-11-19 08:30:00,133.0,201.88400000000001,71.71600000000001,32.225 -2020-11-19 08:45:00,133.02,198.83599999999998,71.71600000000001,32.225 -2020-11-19 09:00:00,137.0,193.114,66.51899999999999,32.225 -2020-11-19 09:15:00,138.81,189.428,66.51899999999999,32.225 -2020-11-19 09:30:00,139.87,187.102,66.51899999999999,32.225 -2020-11-19 09:45:00,136.56,184.975,66.51899999999999,32.225 -2020-11-19 10:00:00,136.84,182.19400000000002,63.04,32.225 -2020-11-19 10:15:00,136.24,178.824,63.04,32.225 -2020-11-19 10:30:00,136.58,176.892,63.04,32.225 -2020-11-19 10:45:00,138.2,175.81799999999998,63.04,32.225 -2020-11-19 11:00:00,143.48,174.016,60.998000000000005,32.225 -2020-11-19 11:15:00,145.08,173.512,60.998000000000005,32.225 -2020-11-19 11:30:00,141.99,172.93099999999998,60.998000000000005,32.225 -2020-11-19 11:45:00,139.15,172.388,60.998000000000005,32.225 -2020-11-19 12:00:00,137.73,166.959,58.27,32.225 -2020-11-19 12:15:00,136.88,165.732,58.27,32.225 -2020-11-19 12:30:00,137.43,165.851,58.27,32.225 -2020-11-19 12:45:00,136.49,166.507,58.27,32.225 -2020-11-19 13:00:00,134.8,165.90599999999998,57.196000000000005,32.225 -2020-11-19 13:15:00,134.79,165.35,57.196000000000005,32.225 -2020-11-19 13:30:00,132.52,164.889,57.196000000000005,32.225 -2020-11-19 13:45:00,131.77,164.835,57.196000000000005,32.225 -2020-11-19 14:00:00,133.0,164.542,57.38399999999999,32.225 -2020-11-19 14:15:00,133.06,164.49,57.38399999999999,32.225 -2020-11-19 14:30:00,131.21,164.235,57.38399999999999,32.225 -2020-11-19 14:45:00,131.57,164.03900000000002,57.38399999999999,32.225 -2020-11-19 15:00:00,132.6,164.05700000000002,58.647,32.225 -2020-11-19 15:15:00,131.83,164.451,58.647,32.225 -2020-11-19 15:30:00,132.4,165.30900000000003,58.647,32.225 -2020-11-19 15:45:00,133.34,166.02900000000002,58.647,32.225 -2020-11-19 16:00:00,136.61,169.5,60.083999999999996,32.225 -2020-11-19 16:15:00,135.17,170.892,60.083999999999996,32.225 -2020-11-19 16:30:00,137.53,172.18599999999998,60.083999999999996,32.225 -2020-11-19 16:45:00,137.22,172.18200000000002,60.083999999999996,32.225 -2020-11-19 17:00:00,138.52,175.574,65.85600000000001,32.225 -2020-11-19 17:15:00,138.89,175.96900000000002,65.85600000000001,32.225 -2020-11-19 17:30:00,140.31,176.305,65.85600000000001,32.225 -2020-11-19 17:45:00,138.58,175.81,65.85600000000001,32.225 -2020-11-19 18:00:00,136.88,177.47400000000002,69.855,32.225 -2020-11-19 18:15:00,135.08,176.61,69.855,32.225 -2020-11-19 18:30:00,134.94,174.812,69.855,32.225 -2020-11-19 18:45:00,135.05,175.66299999999998,69.855,32.225 -2020-11-19 19:00:00,132.41,177.15200000000002,74.015,32.225 -2020-11-19 19:15:00,129.97,174.18200000000002,74.015,32.225 -2020-11-19 19:30:00,128.17,172.87900000000002,74.015,32.225 -2020-11-19 19:45:00,126.77,169.799,74.015,32.225 -2020-11-19 20:00:00,121.56,166.00799999999998,65.316,32.225 -2020-11-19 20:15:00,116.74,161.10299999999998,65.316,32.225 -2020-11-19 20:30:00,113.64,158.468,65.316,32.225 -2020-11-19 20:45:00,117.52,155.156,65.316,32.225 -2020-11-19 21:00:00,113.74,153.093,58.403999999999996,32.225 -2020-11-19 21:15:00,113.82,151.528,58.403999999999996,32.225 -2020-11-19 21:30:00,108.94,149.357,58.403999999999996,32.225 -2020-11-19 21:45:00,105.09,147.631,58.403999999999996,32.225 -2020-11-19 22:00:00,99.66,141.107,54.092,32.225 -2020-11-19 22:15:00,102.85,136.05100000000002,54.092,32.225 -2020-11-19 22:30:00,100.78,120.677,54.092,32.225 -2020-11-19 22:45:00,99.23,112.73700000000001,54.092,32.225 -2020-11-19 23:00:00,91.21,108.055,48.18600000000001,32.225 -2020-11-19 23:15:00,88.51,105.96700000000001,48.18600000000001,32.225 -2020-11-19 23:30:00,92.47,106.329,48.18600000000001,32.225 -2020-11-19 23:45:00,91.07,106.109,48.18600000000001,32.225 -2020-11-20 00:00:00,87.75,99.23,45.18899999999999,32.225 -2020-11-20 00:15:00,81.05,99.459,45.18899999999999,32.225 -2020-11-20 00:30:00,85.97,99.78200000000001,45.18899999999999,32.225 -2020-11-20 00:45:00,85.53,100.905,45.18899999999999,32.225 -2020-11-20 01:00:00,83.27,101.958,43.256,32.225 -2020-11-20 01:15:00,80.61,103.631,43.256,32.225 -2020-11-20 01:30:00,82.75,103.89,43.256,32.225 -2020-11-20 01:45:00,83.03,104.514,43.256,32.225 -2020-11-20 02:00:00,78.63,105.882,42.312,32.225 -2020-11-20 02:15:00,77.14,106.729,42.312,32.225 -2020-11-20 02:30:00,77.65,107.031,42.312,32.225 -2020-11-20 02:45:00,82.12,108.568,42.312,32.225 -2020-11-20 03:00:00,83.0,110.37899999999999,41.833,32.225 -2020-11-20 03:15:00,81.44,111.771,41.833,32.225 -2020-11-20 03:30:00,78.04,113.704,41.833,32.225 -2020-11-20 03:45:00,81.83,114.689,41.833,32.225 -2020-11-20 04:00:00,85.52,128.736,42.732,32.225 -2020-11-20 04:15:00,86.71,140.985,42.732,32.225 -2020-11-20 04:30:00,82.74,142.685,42.732,32.225 -2020-11-20 04:45:00,85.24,143.602,42.732,32.225 -2020-11-20 05:00:00,87.34,176.903,46.254,32.225 -2020-11-20 05:15:00,95.1,208.50799999999998,46.254,32.225 -2020-11-20 05:30:00,100.38,204.958,46.254,32.225 -2020-11-20 05:45:00,104.5,195.774,46.254,32.225 -2020-11-20 06:00:00,105.66,191.827,56.76,32.225 -2020-11-20 06:15:00,110.32,195.717,56.76,32.225 -2020-11-20 06:30:00,114.29,196.326,56.76,32.225 -2020-11-20 06:45:00,119.4,198.864,56.76,32.225 -2020-11-20 07:00:00,126.14,199.542,66.029,32.225 -2020-11-20 07:15:00,127.73,204.579,66.029,32.225 -2020-11-20 07:30:00,127.93,205.59099999999998,66.029,32.225 -2020-11-20 07:45:00,128.79,205.271,66.029,32.225 -2020-11-20 08:00:00,131.62,203.435,73.128,32.225 -2020-11-20 08:15:00,131.62,202.748,73.128,32.225 -2020-11-20 08:30:00,131.81,201.59400000000002,73.128,32.225 -2020-11-20 08:45:00,132.09,197.12599999999998,73.128,32.225 -2020-11-20 09:00:00,132.08,191.378,68.23100000000001,32.225 -2020-11-20 09:15:00,132.7,188.517,68.23100000000001,32.225 -2020-11-20 09:30:00,130.51,185.7,68.23100000000001,32.225 -2020-11-20 09:45:00,130.25,183.52700000000002,68.23100000000001,32.225 -2020-11-20 10:00:00,127.82,179.71400000000003,64.733,32.225 -2020-11-20 10:15:00,126.85,176.882,64.733,32.225 -2020-11-20 10:30:00,125.62,174.955,64.733,32.225 -2020-11-20 10:45:00,125.03,173.46599999999998,64.733,32.225 -2020-11-20 11:00:00,126.8,171.67,62.0,32.225 -2020-11-20 11:15:00,126.91,170.18599999999998,62.0,32.225 -2020-11-20 11:30:00,124.26,171.074,62.0,32.225 -2020-11-20 11:45:00,122.85,170.40400000000002,62.0,32.225 -2020-11-20 12:00:00,121.61,165.986,57.876999999999995,32.225 -2020-11-20 12:15:00,121.21,162.827,57.876999999999995,32.225 -2020-11-20 12:30:00,121.35,163.131,57.876999999999995,32.225 -2020-11-20 12:45:00,122.36,164.113,57.876999999999995,32.225 -2020-11-20 13:00:00,119.72,164.393,55.585,32.225 -2020-11-20 13:15:00,118.77,164.592,55.585,32.225 -2020-11-20 13:30:00,118.89,164.27200000000002,55.585,32.225 -2020-11-20 13:45:00,117.82,164.204,55.585,32.225 -2020-11-20 14:00:00,119.04,162.74,54.5,32.225 -2020-11-20 14:15:00,118.42,162.615,54.5,32.225 -2020-11-20 14:30:00,118.28,163.089,54.5,32.225 -2020-11-20 14:45:00,119.7,163.05,54.5,32.225 -2020-11-20 15:00:00,119.39,162.639,55.131,32.225 -2020-11-20 15:15:00,120.02,162.61700000000002,55.131,32.225 -2020-11-20 15:30:00,120.71,162.084,55.131,32.225 -2020-11-20 15:45:00,122.19,163.062,55.131,32.225 -2020-11-20 16:00:00,124.56,165.365,56.8,32.225 -2020-11-20 16:15:00,127.81,167.11599999999999,56.8,32.225 -2020-11-20 16:30:00,132.47,168.468,56.8,32.225 -2020-11-20 16:45:00,134.58,168.27900000000002,56.8,32.225 -2020-11-20 17:00:00,135.53,172.09799999999998,63.428999999999995,32.225 -2020-11-20 17:15:00,135.41,172.127,63.428999999999995,32.225 -2020-11-20 17:30:00,135.86,172.21599999999998,63.428999999999995,32.225 -2020-11-20 17:45:00,135.03,171.495,63.428999999999995,32.225 -2020-11-20 18:00:00,133.0,173.782,67.915,32.225 -2020-11-20 18:15:00,132.6,172.382,67.915,32.225 -2020-11-20 18:30:00,132.16,170.923,67.915,32.225 -2020-11-20 18:45:00,133.09,171.83599999999998,67.915,32.225 -2020-11-20 19:00:00,129.97,174.26,69.428,32.225 -2020-11-20 19:15:00,127.95,172.574,69.428,32.225 -2020-11-20 19:30:00,125.39,170.898,69.428,32.225 -2020-11-20 19:45:00,122.11,167.21900000000002,69.428,32.225 -2020-11-20 20:00:00,117.37,163.468,60.56100000000001,32.225 -2020-11-20 20:15:00,113.89,158.686,60.56100000000001,32.225 -2020-11-20 20:30:00,110.74,155.905,60.56100000000001,32.225 -2020-11-20 20:45:00,109.52,152.961,60.56100000000001,32.225 -2020-11-20 21:00:00,106.9,151.546,55.18600000000001,32.225 -2020-11-20 21:15:00,105.87,150.619,55.18600000000001,32.225 -2020-11-20 21:30:00,106.31,148.468,55.18600000000001,32.225 -2020-11-20 21:45:00,103.66,147.249,55.18600000000001,32.225 -2020-11-20 22:00:00,92.56,141.57,51.433,32.225 -2020-11-20 22:15:00,90.8,136.34799999999998,51.433,32.225 -2020-11-20 22:30:00,84.72,127.389,51.433,32.225 -2020-11-20 22:45:00,86.32,122.689,51.433,32.225 -2020-11-20 23:00:00,87.62,117.875,46.201,32.225 -2020-11-20 23:15:00,86.93,113.809,46.201,32.225 -2020-11-20 23:30:00,82.7,112.605,46.201,32.225 -2020-11-20 23:45:00,78.82,111.764,46.201,32.225 -2020-11-21 00:00:00,79.22,97.382,42.576,32.047 -2020-11-21 00:15:00,78.96,93.809,42.576,32.047 -2020-11-21 00:30:00,73.94,95.18799999999999,42.576,32.047 -2020-11-21 00:45:00,70.84,96.777,42.576,32.047 -2020-11-21 01:00:00,71.67,98.469,39.34,32.047 -2020-11-21 01:15:00,74.73,99.413,39.34,32.047 -2020-11-21 01:30:00,74.22,99.094,39.34,32.047 -2020-11-21 01:45:00,71.33,99.74799999999999,39.34,32.047 -2020-11-21 02:00:00,71.97,101.554,37.582,32.047 -2020-11-21 02:15:00,73.0,101.944,37.582,32.047 -2020-11-21 02:30:00,71.44,101.148,37.582,32.047 -2020-11-21 02:45:00,69.8,102.914,37.582,32.047 -2020-11-21 03:00:00,70.35,105.005,36.523,32.047 -2020-11-21 03:15:00,71.12,105.26100000000001,36.523,32.047 -2020-11-21 03:30:00,68.69,105.90799999999999,36.523,32.047 -2020-11-21 03:45:00,65.43,107.25299999999999,36.523,32.047 -2020-11-21 04:00:00,63.81,117.439,36.347,32.047 -2020-11-21 04:15:00,64.05,127.375,36.347,32.047 -2020-11-21 04:30:00,65.13,126.90899999999999,36.347,32.047 -2020-11-21 04:45:00,65.92,127.463,36.347,32.047 -2020-11-21 05:00:00,66.64,145.74,36.407,32.047 -2020-11-21 05:15:00,66.61,159.19,36.407,32.047 -2020-11-21 05:30:00,66.7,156.248,36.407,32.047 -2020-11-21 05:45:00,68.33,152.523,36.407,32.047 -2020-11-21 06:00:00,70.41,166.495,38.228,32.047 -2020-11-21 06:15:00,71.52,185.717,38.228,32.047 -2020-11-21 06:30:00,73.49,181.28099999999998,38.228,32.047 -2020-11-21 06:45:00,75.76,175.635,38.228,32.047 -2020-11-21 07:00:00,79.69,172.74200000000002,41.905,32.047 -2020-11-21 07:15:00,81.93,176.484,41.905,32.047 -2020-11-21 07:30:00,82.59,180.097,41.905,32.047 -2020-11-21 07:45:00,86.02,183.34099999999998,41.905,32.047 -2020-11-21 08:00:00,89.63,184.977,46.051,32.047 -2020-11-21 08:15:00,90.83,187.362,46.051,32.047 -2020-11-21 08:30:00,93.03,187.613,46.051,32.047 -2020-11-21 08:45:00,96.12,185.99200000000002,46.051,32.047 -2020-11-21 09:00:00,100.89,182.237,46.683,32.047 -2020-11-21 09:15:00,98.64,180.114,46.683,32.047 -2020-11-21 09:30:00,100.25,178.16,46.683,32.047 -2020-11-21 09:45:00,100.87,176.032,46.683,32.047 -2020-11-21 10:00:00,100.67,172.535,44.425,32.047 -2020-11-21 10:15:00,99.74,169.87400000000002,44.425,32.047 -2020-11-21 10:30:00,99.32,168.007,44.425,32.047 -2020-11-21 10:45:00,99.73,167.572,44.425,32.047 -2020-11-21 11:00:00,100.59,165.90200000000002,42.148999999999994,32.047 -2020-11-21 11:15:00,101.81,163.94400000000002,42.148999999999994,32.047 -2020-11-21 11:30:00,104.19,163.908,42.148999999999994,32.047 -2020-11-21 11:45:00,103.4,162.507,42.148999999999994,32.047 -2020-11-21 12:00:00,103.31,157.35299999999998,39.683,32.047 -2020-11-21 12:15:00,103.72,154.89600000000002,39.683,32.047 -2020-11-21 12:30:00,101.44,155.484,39.683,32.047 -2020-11-21 12:45:00,101.91,155.91899999999998,39.683,32.047 -2020-11-21 13:00:00,97.83,155.659,37.154,32.047 -2020-11-21 13:15:00,97.45,153.987,37.154,32.047 -2020-11-21 13:30:00,97.31,153.30200000000002,37.154,32.047 -2020-11-21 13:45:00,96.77,153.42,37.154,32.047 -2020-11-21 14:00:00,94.23,152.981,36.457,32.047 -2020-11-21 14:15:00,92.82,152.19899999999998,36.457,32.047 -2020-11-21 14:30:00,91.21,151.05100000000002,36.457,32.047 -2020-11-21 14:45:00,90.74,151.286,36.457,32.047 -2020-11-21 15:00:00,89.08,151.498,38.257,32.047 -2020-11-21 15:15:00,90.29,152.30200000000002,38.257,32.047 -2020-11-21 15:30:00,90.87,153.183,38.257,32.047 -2020-11-21 15:45:00,91.79,154.035,38.257,32.047 -2020-11-21 16:00:00,94.22,155.641,41.181000000000004,32.047 -2020-11-21 16:15:00,97.63,158.055,41.181000000000004,32.047 -2020-11-21 16:30:00,100.73,159.40200000000002,41.181000000000004,32.047 -2020-11-21 16:45:00,102.19,160.024,41.181000000000004,32.047 -2020-11-21 17:00:00,104.89,163.127,46.806000000000004,32.047 -2020-11-21 17:15:00,108.34,164.282,46.806000000000004,32.047 -2020-11-21 17:30:00,107.18,164.275,46.806000000000004,32.047 -2020-11-21 17:45:00,107.64,163.287,46.806000000000004,32.047 -2020-11-21 18:00:00,107.72,165.36599999999999,52.073,32.047 -2020-11-21 18:15:00,107.31,165.763,52.073,32.047 -2020-11-21 18:30:00,105.98,165.696,52.073,32.047 -2020-11-21 18:45:00,105.32,163.127,52.073,32.047 -2020-11-21 19:00:00,103.23,166.097,53.608000000000004,32.047 -2020-11-21 19:15:00,101.82,163.806,53.608000000000004,32.047 -2020-11-21 19:30:00,100.14,162.893,53.608000000000004,32.047 -2020-11-21 19:45:00,98.85,159.30200000000002,53.608000000000004,32.047 -2020-11-21 20:00:00,94.8,157.629,50.265,32.047 -2020-11-21 20:15:00,91.04,154.589,50.265,32.047 -2020-11-21 20:30:00,87.39,151.33,50.265,32.047 -2020-11-21 20:45:00,85.88,148.344,50.265,32.047 -2020-11-21 21:00:00,83.83,148.64700000000002,45.766000000000005,32.047 -2020-11-21 21:15:00,82.44,148.033,45.766000000000005,32.047 -2020-11-21 21:30:00,81.16,146.989,45.766000000000005,32.047 -2020-11-21 21:45:00,82.28,145.325,45.766000000000005,32.047 -2020-11-21 22:00:00,77.41,140.82399999999998,45.97,32.047 -2020-11-21 22:15:00,75.61,137.806,45.97,32.047 -2020-11-21 22:30:00,72.97,134.268,45.97,32.047 -2020-11-21 22:45:00,72.35,131.268,45.97,32.047 -2020-11-21 23:00:00,69.85,128.459,40.415,32.047 -2020-11-21 23:15:00,68.87,123.02,40.415,32.047 -2020-11-21 23:30:00,66.45,120.694,40.415,32.047 -2020-11-21 23:45:00,64.91,117.774,40.415,32.047 -2020-11-22 00:00:00,62.39,98.36399999999999,36.376,32.047 -2020-11-22 00:15:00,61.59,94.28299999999999,36.376,32.047 -2020-11-22 00:30:00,60.39,95.315,36.376,32.047 -2020-11-22 00:45:00,59.43,97.46,36.376,32.047 -2020-11-22 01:00:00,60.22,99.115,32.992,32.047 -2020-11-22 01:15:00,57.85,100.927,32.992,32.047 -2020-11-22 01:30:00,56.88,101.039,32.992,32.047 -2020-11-22 01:45:00,56.3,101.34299999999999,32.992,32.047 -2020-11-22 02:00:00,55.45,102.54299999999999,32.327,32.047 -2020-11-22 02:15:00,55.69,102.346,32.327,32.047 -2020-11-22 02:30:00,55.72,102.307,32.327,32.047 -2020-11-22 02:45:00,55.6,104.40899999999999,32.327,32.047 -2020-11-22 03:00:00,55.22,106.87299999999999,31.169,32.047 -2020-11-22 03:15:00,55.28,106.764,31.169,32.047 -2020-11-22 03:30:00,55.5,108.458,31.169,32.047 -2020-11-22 03:45:00,55.87,109.579,31.169,32.047 -2020-11-22 04:00:00,55.68,119.53,30.796,32.047 -2020-11-22 04:15:00,56.25,128.539,30.796,32.047 -2020-11-22 04:30:00,56.7,128.373,30.796,32.047 -2020-11-22 04:45:00,57.4,129.07399999999998,30.796,32.047 -2020-11-22 05:00:00,57.89,144.434,30.848000000000003,32.047 -2020-11-22 05:15:00,58.33,155.689,30.848000000000003,32.047 -2020-11-22 05:30:00,58.4,152.545,30.848000000000003,32.047 -2020-11-22 05:45:00,58.91,148.976,30.848000000000003,32.047 -2020-11-22 06:00:00,60.42,162.407,31.166,32.047 -2020-11-22 06:15:00,61.02,180.342,31.166,32.047 -2020-11-22 06:30:00,61.68,174.887,31.166,32.047 -2020-11-22 06:45:00,63.19,168.197,31.166,32.047 -2020-11-22 07:00:00,66.58,167.417,33.527,32.047 -2020-11-22 07:15:00,67.92,170.138,33.527,32.047 -2020-11-22 07:30:00,68.4,172.968,33.527,32.047 -2020-11-22 07:45:00,70.27,175.56599999999997,33.527,32.047 -2020-11-22 08:00:00,73.27,178.896,36.616,32.047 -2020-11-22 08:15:00,74.74,181.408,36.616,32.047 -2020-11-22 08:30:00,76.33,183.252,36.616,32.047 -2020-11-22 08:45:00,76.44,183.30700000000002,36.616,32.047 -2020-11-22 09:00:00,77.09,179.15400000000002,37.857,32.047 -2020-11-22 09:15:00,78.76,177.422,37.857,32.047 -2020-11-22 09:30:00,79.57,175.412,37.857,32.047 -2020-11-22 09:45:00,79.12,173.343,37.857,32.047 -2020-11-22 10:00:00,78.1,172.08700000000002,36.319,32.047 -2020-11-22 10:15:00,80.56,169.87400000000002,36.319,32.047 -2020-11-22 10:30:00,83.15,168.53900000000002,36.319,32.047 -2020-11-22 10:45:00,84.7,166.643,36.319,32.047 -2020-11-22 11:00:00,85.59,165.68099999999998,37.236999999999995,32.047 -2020-11-22 11:15:00,87.48,163.744,37.236999999999995,32.047 -2020-11-22 11:30:00,89.9,163.045,37.236999999999995,32.047 -2020-11-22 11:45:00,94.79,162.2,37.236999999999995,32.047 -2020-11-22 12:00:00,90.94,156.726,34.871,32.047 -2020-11-22 12:15:00,89.01,155.812,34.871,32.047 -2020-11-22 12:30:00,86.85,155.20600000000002,34.871,32.047 -2020-11-22 12:45:00,85.53,154.701,34.871,32.047 -2020-11-22 13:00:00,81.64,153.731,29.738000000000003,32.047 -2020-11-22 13:15:00,78.73,154.57,29.738000000000003,32.047 -2020-11-22 13:30:00,78.52,153.511,29.738000000000003,32.047 -2020-11-22 13:45:00,76.68,153.241,29.738000000000003,32.047 -2020-11-22 14:00:00,76.68,153.178,27.333000000000002,32.047 -2020-11-22 14:15:00,75.93,153.518,27.333000000000002,32.047 -2020-11-22 14:30:00,75.96,153.26,27.333000000000002,32.047 -2020-11-22 14:45:00,75.98,152.963,27.333000000000002,32.047 -2020-11-22 15:00:00,80.48,151.885,28.232,32.047 -2020-11-22 15:15:00,77.4,153.216,28.232,32.047 -2020-11-22 15:30:00,79.06,154.611,28.232,32.047 -2020-11-22 15:45:00,81.3,156.118,28.232,32.047 -2020-11-22 16:00:00,84.0,159.056,32.815,32.047 -2020-11-22 16:15:00,86.71,160.746,32.815,32.047 -2020-11-22 16:30:00,91.42,162.487,32.815,32.047 -2020-11-22 16:45:00,94.2,163.279,32.815,32.047 -2020-11-22 17:00:00,97.01,166.412,43.068999999999996,32.047 -2020-11-22 17:15:00,99.13,167.55599999999998,43.068999999999996,32.047 -2020-11-22 17:30:00,106.82,167.94099999999997,43.068999999999996,32.047 -2020-11-22 17:45:00,108.43,168.998,43.068999999999996,32.047 -2020-11-22 18:00:00,106.98,170.72,50.498999999999995,32.047 -2020-11-22 18:15:00,102.7,172.16400000000002,50.498999999999995,32.047 -2020-11-22 18:30:00,103.87,170.24900000000002,50.498999999999995,32.047 -2020-11-22 18:45:00,101.15,169.312,50.498999999999995,32.047 -2020-11-22 19:00:00,101.38,172.34799999999998,53.481,32.047 -2020-11-22 19:15:00,97.91,170.38400000000001,53.481,32.047 -2020-11-22 19:30:00,97.08,169.269,53.481,32.047 -2020-11-22 19:45:00,94.87,166.83,53.481,32.047 -2020-11-22 20:00:00,100.26,165.14700000000002,51.687,32.047 -2020-11-22 20:15:00,100.16,162.92600000000002,51.687,32.047 -2020-11-22 20:30:00,97.79,160.846,51.687,32.047 -2020-11-22 20:45:00,89.12,156.549,51.687,32.047 -2020-11-22 21:00:00,89.33,154.567,47.674,32.047 -2020-11-22 21:15:00,90.04,153.346,47.674,32.047 -2020-11-22 21:30:00,87.6,152.453,47.674,32.047 -2020-11-22 21:45:00,87.83,150.965,47.674,32.047 -2020-11-22 22:00:00,86.54,145.78,48.178000000000004,32.047 -2020-11-22 22:15:00,87.22,141.814,48.178000000000004,32.047 -2020-11-22 22:30:00,85.35,135.501,48.178000000000004,32.047 -2020-11-22 22:45:00,86.7,131.559,48.178000000000004,32.047 -2020-11-22 23:00:00,87.2,126.255,42.553999999999995,32.047 -2020-11-22 23:15:00,88.4,122.626,42.553999999999995,32.047 -2020-11-22 23:30:00,84.48,120.92200000000001,42.553999999999995,32.047 -2020-11-22 23:45:00,81.66,118.785,42.553999999999995,32.047 -2020-11-23 00:00:00,81.01,102.585,37.177,32.225 -2020-11-23 00:15:00,80.79,101.152,37.177,32.225 -2020-11-23 00:30:00,79.11,102.22200000000001,37.177,32.225 -2020-11-23 00:45:00,74.45,103.821,37.177,32.225 -2020-11-23 01:00:00,77.84,105.58,35.358000000000004,32.225 -2020-11-23 01:15:00,77.83,106.954,35.358000000000004,32.225 -2020-11-23 01:30:00,77.76,107.191,35.358000000000004,32.225 -2020-11-23 01:45:00,72.18,107.56299999999999,35.358000000000004,32.225 -2020-11-23 02:00:00,77.53,108.836,35.03,32.225 -2020-11-23 02:15:00,77.99,109.693,35.03,32.225 -2020-11-23 02:30:00,77.7,109.962,35.03,32.225 -2020-11-23 02:45:00,71.51,111.509,35.03,32.225 -2020-11-23 03:00:00,77.86,115.11200000000001,34.394,32.225 -2020-11-23 03:15:00,77.98,116.559,34.394,32.225 -2020-11-23 03:30:00,79.9,118.154,34.394,32.225 -2020-11-23 03:45:00,76.4,118.714,34.394,32.225 -2020-11-23 04:00:00,75.65,132.888,34.421,32.225 -2020-11-23 04:15:00,75.21,145.955,34.421,32.225 -2020-11-23 04:30:00,80.58,147.53,34.421,32.225 -2020-11-23 04:45:00,86.22,148.444,34.421,32.225 -2020-11-23 05:00:00,89.91,178.38099999999997,39.435,32.225 -2020-11-23 05:15:00,93.08,208.532,39.435,32.225 -2020-11-23 05:30:00,91.38,205.338,39.435,32.225 -2020-11-23 05:45:00,101.04,196.43599999999998,39.435,32.225 -2020-11-23 06:00:00,111.88,192.929,55.685,32.225 -2020-11-23 06:15:00,119.66,196.643,55.685,32.225 -2020-11-23 06:30:00,117.56,198.74,55.685,32.225 -2020-11-23 06:45:00,126.52,200.50099999999998,55.685,32.225 -2020-11-23 07:00:00,127.0,201.90599999999998,66.837,32.225 -2020-11-23 07:15:00,130.33,206.11599999999999,66.837,32.225 -2020-11-23 07:30:00,133.16,208.145,66.837,32.225 -2020-11-23 07:45:00,134.79,208.628,66.837,32.225 -2020-11-23 08:00:00,135.1,207.56599999999997,72.217,32.225 -2020-11-23 08:15:00,134.95,208.025,72.217,32.225 -2020-11-23 08:30:00,135.26,206.227,72.217,32.225 -2020-11-23 08:45:00,135.33,203.44,72.217,32.225 -2020-11-23 09:00:00,137.24,198.29,66.117,32.225 -2020-11-23 09:15:00,141.14,193.26,66.117,32.225 -2020-11-23 09:30:00,142.69,190.23,66.117,32.225 -2020-11-23 09:45:00,142.86,188.003,66.117,32.225 -2020-11-23 10:00:00,143.04,185.90900000000002,62.1,32.225 -2020-11-23 10:15:00,143.04,183.359,62.1,32.225 -2020-11-23 10:30:00,143.62,181.167,62.1,32.225 -2020-11-23 10:45:00,139.51,179.63299999999998,62.1,32.225 -2020-11-23 11:00:00,144.08,176.495,60.021,32.225 -2020-11-23 11:15:00,159.0,176.148,60.021,32.225 -2020-11-23 11:30:00,160.47,176.75799999999998,60.021,32.225 -2020-11-23 11:45:00,160.25,175.63299999999998,60.021,32.225 -2020-11-23 12:00:00,147.32,171.385,56.75899999999999,32.225 -2020-11-23 12:15:00,145.08,170.497,56.75899999999999,32.225 -2020-11-23 12:30:00,148.09,169.957,56.75899999999999,32.225 -2020-11-23 12:45:00,145.6,170.778,56.75899999999999,32.225 -2020-11-23 13:00:00,143.46,170.442,56.04600000000001,32.225 -2020-11-23 13:15:00,142.41,169.93900000000002,56.04600000000001,32.225 -2020-11-23 13:30:00,140.37,168.447,56.04600000000001,32.225 -2020-11-23 13:45:00,135.08,168.343,56.04600000000001,32.225 -2020-11-23 14:00:00,135.61,167.577,55.475,32.225 -2020-11-23 14:15:00,135.86,167.467,55.475,32.225 -2020-11-23 14:30:00,135.91,166.745,55.475,32.225 -2020-11-23 14:45:00,136.54,166.763,55.475,32.225 -2020-11-23 15:00:00,136.12,167.21400000000003,57.048,32.225 -2020-11-23 15:15:00,135.68,167.205,57.048,32.225 -2020-11-23 15:30:00,135.08,168.033,57.048,32.225 -2020-11-23 15:45:00,135.81,169.081,57.048,32.225 -2020-11-23 16:00:00,136.9,172.327,59.06,32.225 -2020-11-23 16:15:00,137.68,173.388,59.06,32.225 -2020-11-23 16:30:00,139.13,174.18400000000003,59.06,32.225 -2020-11-23 16:45:00,141.15,173.987,59.06,32.225 -2020-11-23 17:00:00,155.46,176.75400000000002,65.419,32.225 -2020-11-23 17:15:00,157.73,177.153,65.419,32.225 -2020-11-23 17:30:00,158.42,177.005,65.419,32.225 -2020-11-23 17:45:00,145.53,176.706,65.419,32.225 -2020-11-23 18:00:00,137.98,178.655,69.345,32.225 -2020-11-23 18:15:00,139.34,177.84,69.345,32.225 -2020-11-23 18:30:00,138.47,176.386,69.345,32.225 -2020-11-23 18:45:00,139.68,176.57299999999998,69.345,32.225 -2020-11-23 19:00:00,137.89,178.16,73.825,32.225 -2020-11-23 19:15:00,134.19,175.365,73.825,32.225 -2020-11-23 19:30:00,131.82,174.611,73.825,32.225 -2020-11-23 19:45:00,129.59,171.31599999999997,73.825,32.225 -2020-11-23 20:00:00,125.6,167.36900000000003,64.027,32.225 -2020-11-23 20:15:00,119.28,163.202,64.027,32.225 -2020-11-23 20:30:00,122.12,159.554,64.027,32.225 -2020-11-23 20:45:00,121.81,156.717,64.027,32.225 -2020-11-23 21:00:00,118.81,155.09,57.952,32.225 -2020-11-23 21:15:00,109.73,152.90200000000002,57.952,32.225 -2020-11-23 21:30:00,108.05,151.36,57.952,32.225 -2020-11-23 21:45:00,106.57,149.395,57.952,32.225 -2020-11-23 22:00:00,105.12,141.372,53.031000000000006,32.225 -2020-11-23 22:15:00,106.96,136.566,53.031000000000006,32.225 -2020-11-23 22:30:00,104.93,121.29,53.031000000000006,32.225 -2020-11-23 22:45:00,101.2,113.13799999999999,53.031000000000006,32.225 -2020-11-23 23:00:00,92.0,108.491,45.085,32.225 -2020-11-23 23:15:00,94.85,106.9,45.085,32.225 -2020-11-23 23:30:00,94.19,107.60799999999999,45.085,32.225 -2020-11-23 23:45:00,91.56,107.67299999999999,45.085,32.225 -2020-11-24 00:00:00,86.66,101.96,42.843,32.225 -2020-11-24 00:15:00,81.99,101.87299999999999,42.843,32.225 -2020-11-24 00:30:00,80.81,102.262,42.843,32.225 -2020-11-24 00:45:00,83.16,103.147,42.843,32.225 -2020-11-24 01:00:00,86.21,104.68,41.542,32.225 -2020-11-24 01:15:00,86.93,105.70100000000001,41.542,32.225 -2020-11-24 01:30:00,83.03,106.073,41.542,32.225 -2020-11-24 01:45:00,80.91,106.603,41.542,32.225 -2020-11-24 02:00:00,80.28,107.804,40.19,32.225 -2020-11-24 02:15:00,85.04,108.777,40.19,32.225 -2020-11-24 02:30:00,80.94,108.46600000000001,40.19,32.225 -2020-11-24 02:45:00,83.11,110.08,40.19,32.225 -2020-11-24 03:00:00,82.33,112.54700000000001,39.626,32.225 -2020-11-24 03:15:00,81.11,113.471,39.626,32.225 -2020-11-24 03:30:00,80.6,115.473,39.626,32.225 -2020-11-24 03:45:00,81.65,115.988,39.626,32.225 -2020-11-24 04:00:00,89.16,129.751,40.196999999999996,32.225 -2020-11-24 04:15:00,89.98,142.534,40.196999999999996,32.225 -2020-11-24 04:30:00,91.48,143.811,40.196999999999996,32.225 -2020-11-24 04:45:00,88.32,145.847,40.196999999999996,32.225 -2020-11-24 05:00:00,95.04,180.31,43.378,32.225 -2020-11-24 05:15:00,100.05,210.38299999999998,43.378,32.225 -2020-11-24 05:30:00,105.49,205.94099999999997,43.378,32.225 -2020-11-24 05:45:00,104.35,196.915,43.378,32.225 -2020-11-24 06:00:00,111.75,192.609,55.691,32.225 -2020-11-24 06:15:00,115.57,197.745,55.691,32.225 -2020-11-24 06:30:00,120.7,199.278,55.691,32.225 -2020-11-24 06:45:00,125.63,200.59400000000002,55.691,32.225 -2020-11-24 07:00:00,131.53,201.87,65.567,32.225 -2020-11-24 07:15:00,134.38,205.908,65.567,32.225 -2020-11-24 07:30:00,135.59,207.489,65.567,32.225 -2020-11-24 07:45:00,136.05,207.987,65.567,32.225 -2020-11-24 08:00:00,133.21,207.02200000000002,73.001,32.225 -2020-11-24 08:15:00,132.47,206.514,73.001,32.225 -2020-11-24 08:30:00,131.85,204.61,73.001,32.225 -2020-11-24 08:45:00,132.0,201.40599999999998,73.001,32.225 -2020-11-24 09:00:00,133.22,195.56900000000002,67.08800000000001,32.225 -2020-11-24 09:15:00,134.13,191.907,67.08800000000001,32.225 -2020-11-24 09:30:00,136.8,189.577,67.08800000000001,32.225 -2020-11-24 09:45:00,138.03,187.364,67.08800000000001,32.225 -2020-11-24 10:00:00,143.53,184.52599999999998,62.803000000000004,32.225 -2020-11-24 10:15:00,143.26,181.0,62.803000000000004,32.225 -2020-11-24 10:30:00,144.11,178.94400000000002,62.803000000000004,32.225 -2020-11-24 10:45:00,142.0,177.80599999999998,62.803000000000004,32.225 -2020-11-24 11:00:00,145.32,175.952,60.155,32.225 -2020-11-24 11:15:00,159.14,175.36,60.155,32.225 -2020-11-24 11:30:00,159.74,174.77200000000002,60.155,32.225 -2020-11-24 11:45:00,158.94,174.179,60.155,32.225 -2020-11-24 12:00:00,144.91,168.69400000000002,56.845,32.225 -2020-11-24 12:15:00,143.79,167.502,56.845,32.225 -2020-11-24 12:30:00,144.9,167.753,56.845,32.225 -2020-11-24 12:45:00,148.31,168.429,56.845,32.225 -2020-11-24 13:00:00,143.29,167.641,56.163000000000004,32.225 -2020-11-24 13:15:00,142.71,167.11900000000003,56.163000000000004,32.225 -2020-11-24 13:30:00,138.34,166.644,56.163000000000004,32.225 -2020-11-24 13:45:00,137.62,166.546,56.163000000000004,32.225 -2020-11-24 14:00:00,137.28,166.058,55.934,32.225 -2020-11-24 14:15:00,136.5,166.065,55.934,32.225 -2020-11-24 14:30:00,136.48,165.97799999999998,55.934,32.225 -2020-11-24 14:45:00,135.39,165.81,55.934,32.225 -2020-11-24 15:00:00,134.87,165.862,57.43899999999999,32.225 -2020-11-24 15:15:00,134.85,166.291,57.43899999999999,32.225 -2020-11-24 15:30:00,135.23,167.31599999999997,57.43899999999999,32.225 -2020-11-24 15:45:00,134.55,168.06900000000002,57.43899999999999,32.225 -2020-11-24 16:00:00,136.4,171.49900000000002,59.968999999999994,32.225 -2020-11-24 16:15:00,141.88,173.015,59.968999999999994,32.225 -2020-11-24 16:30:00,145.09,174.326,59.968999999999994,32.225 -2020-11-24 16:45:00,146.27,174.53099999999998,59.968999999999994,32.225 -2020-11-24 17:00:00,147.1,177.77900000000002,67.428,32.225 -2020-11-24 17:15:00,160.92,178.29,67.428,32.225 -2020-11-24 17:30:00,161.04,178.67,67.428,32.225 -2020-11-24 17:45:00,161.02,178.213,67.428,32.225 -2020-11-24 18:00:00,147.32,179.94099999999997,71.533,32.225 -2020-11-24 18:15:00,138.99,178.87099999999998,71.533,32.225 -2020-11-24 18:30:00,137.9,177.114,71.533,32.225 -2020-11-24 18:45:00,141.41,177.975,71.533,32.225 -2020-11-24 19:00:00,139.01,179.456,73.32300000000001,32.225 -2020-11-24 19:15:00,136.46,176.43,73.32300000000001,32.225 -2020-11-24 19:30:00,133.33,175.03799999999998,73.32300000000001,32.225 -2020-11-24 19:45:00,132.31,171.794,73.32300000000001,32.225 -2020-11-24 20:00:00,128.54,168.054,64.166,32.225 -2020-11-24 20:15:00,122.92,163.094,64.166,32.225 -2020-11-24 20:30:00,122.74,160.308,64.166,32.225 -2020-11-24 20:45:00,124.43,157.046,64.166,32.225 -2020-11-24 21:00:00,120.21,154.929,57.891999999999996,32.225 -2020-11-24 21:15:00,116.54,153.293,57.891999999999996,32.225 -2020-11-24 21:30:00,111.79,151.145,57.891999999999996,32.225 -2020-11-24 21:45:00,110.12,149.408,57.891999999999996,32.225 -2020-11-24 22:00:00,104.81,142.917,53.242,32.225 -2020-11-24 22:15:00,106.93,137.826,53.242,32.225 -2020-11-24 22:30:00,109.19,122.727,53.242,32.225 -2020-11-24 22:45:00,106.58,114.824,53.242,32.225 -2020-11-24 23:00:00,99.59,110.068,46.665,32.225 -2020-11-24 23:15:00,93.79,107.912,46.665,32.225 -2020-11-24 23:30:00,97.04,108.311,46.665,32.225 -2020-11-24 23:45:00,95.69,108.001,46.665,32.225 -2020-11-25 00:00:00,94.25,102.323,43.16,32.225 -2020-11-25 00:15:00,89.05,102.209,43.16,32.225 -2020-11-25 00:30:00,88.28,102.59700000000001,43.16,32.225 -2020-11-25 00:45:00,90.4,103.465,43.16,32.225 -2020-11-25 01:00:00,88.67,105.03299999999999,40.972,32.225 -2020-11-25 01:15:00,83.25,106.059,40.972,32.225 -2020-11-25 01:30:00,82.33,106.444,40.972,32.225 -2020-11-25 01:45:00,79.08,106.96,40.972,32.225 -2020-11-25 02:00:00,81.52,108.182,39.749,32.225 -2020-11-25 02:15:00,83.98,109.15799999999999,39.749,32.225 -2020-11-25 02:30:00,87.31,108.84299999999999,39.749,32.225 -2020-11-25 02:45:00,88.71,110.45700000000001,39.749,32.225 -2020-11-25 03:00:00,85.46,112.91,39.422,32.225 -2020-11-25 03:15:00,84.79,113.861,39.422,32.225 -2020-11-25 03:30:00,89.21,115.866,39.422,32.225 -2020-11-25 03:45:00,90.74,116.37200000000001,39.422,32.225 -2020-11-25 04:00:00,92.44,130.12,40.505,32.225 -2020-11-25 04:15:00,89.59,142.911,40.505,32.225 -2020-11-25 04:30:00,92.56,144.178,40.505,32.225 -2020-11-25 04:45:00,96.12,146.219,40.505,32.225 -2020-11-25 05:00:00,97.39,180.678,43.397,32.225 -2020-11-25 05:15:00,101.36,210.732,43.397,32.225 -2020-11-25 05:30:00,106.35,206.30599999999998,43.397,32.225 -2020-11-25 05:45:00,111.83,197.285,43.397,32.225 -2020-11-25 06:00:00,112.45,192.995,55.218,32.225 -2020-11-25 06:15:00,119.29,198.136,55.218,32.225 -2020-11-25 06:30:00,122.62,199.71200000000002,55.218,32.225 -2020-11-25 06:45:00,127.32,201.063,55.218,32.225 -2020-11-25 07:00:00,129.42,202.33900000000003,67.39,32.225 -2020-11-25 07:15:00,133.53,206.388,67.39,32.225 -2020-11-25 07:30:00,133.46,207.983,67.39,32.225 -2020-11-25 07:45:00,133.22,208.485,67.39,32.225 -2020-11-25 08:00:00,134.72,207.533,74.345,32.225 -2020-11-25 08:15:00,134.93,207.014,74.345,32.225 -2020-11-25 08:30:00,133.18,205.128,74.345,32.225 -2020-11-25 08:45:00,136.06,201.892,74.345,32.225 -2020-11-25 09:00:00,136.12,196.03400000000002,69.336,32.225 -2020-11-25 09:15:00,136.89,192.37599999999998,69.336,32.225 -2020-11-25 09:30:00,137.83,190.047,69.336,32.225 -2020-11-25 09:45:00,136.21,187.817,69.336,32.225 -2020-11-25 10:00:00,134.1,184.968,64.291,32.225 -2020-11-25 10:15:00,134.21,181.41099999999997,64.291,32.225 -2020-11-25 10:30:00,132.81,179.333,64.291,32.225 -2020-11-25 10:45:00,132.27,178.18200000000002,64.291,32.225 -2020-11-25 11:00:00,130.7,176.317,62.20399999999999,32.225 -2020-11-25 11:15:00,130.65,175.709,62.20399999999999,32.225 -2020-11-25 11:30:00,129.63,175.11900000000003,62.20399999999999,32.225 -2020-11-25 11:45:00,128.59,174.517,62.20399999999999,32.225 -2020-11-25 12:00:00,127.98,169.023,59.042,32.225 -2020-11-25 12:15:00,127.46,167.83700000000002,59.042,32.225 -2020-11-25 12:30:00,129.41,168.113,59.042,32.225 -2020-11-25 12:45:00,127.0,168.794,59.042,32.225 -2020-11-25 13:00:00,126.51,167.97,57.907,32.225 -2020-11-25 13:15:00,126.11,167.454,57.907,32.225 -2020-11-25 13:30:00,126.17,166.975,57.907,32.225 -2020-11-25 13:45:00,126.16,166.868,57.907,32.225 -2020-11-25 14:00:00,128.27,166.345,58.358000000000004,32.225 -2020-11-25 14:15:00,129.25,166.362,58.358000000000004,32.225 -2020-11-25 14:30:00,129.64,166.30700000000002,58.358000000000004,32.225 -2020-11-25 14:45:00,130.02,166.146,58.358000000000004,32.225 -2020-11-25 15:00:00,131.43,166.205,59.348,32.225 -2020-11-25 15:15:00,133.51,166.64,59.348,32.225 -2020-11-25 15:30:00,133.44,167.696,59.348,32.225 -2020-11-25 15:45:00,134.78,168.456,59.348,32.225 -2020-11-25 16:00:00,136.6,171.878,61.413999999999994,32.225 -2020-11-25 16:15:00,139.28,173.418,61.413999999999994,32.225 -2020-11-25 16:30:00,143.76,174.731,61.413999999999994,32.225 -2020-11-25 16:45:00,145.53,174.976,61.413999999999994,32.225 -2020-11-25 17:00:00,146.91,178.197,67.107,32.225 -2020-11-25 17:15:00,147.41,178.731,67.107,32.225 -2020-11-25 17:30:00,147.84,179.12099999999998,67.107,32.225 -2020-11-25 17:45:00,147.01,178.673,67.107,32.225 -2020-11-25 18:00:00,144.4,180.41400000000002,71.92,32.225 -2020-11-25 18:15:00,143.14,179.304,71.92,32.225 -2020-11-25 18:30:00,141.88,177.55700000000002,71.92,32.225 -2020-11-25 18:45:00,142.19,178.42,71.92,32.225 -2020-11-25 19:00:00,141.09,179.89700000000002,75.09,32.225 -2020-11-25 19:15:00,137.11,176.861,75.09,32.225 -2020-11-25 19:30:00,134.84,175.452,75.09,32.225 -2020-11-25 19:45:00,133.69,172.17700000000002,75.09,32.225 -2020-11-25 20:00:00,130.33,168.446,65.977,32.225 -2020-11-25 20:15:00,122.19,163.476,65.977,32.225 -2020-11-25 20:30:00,123.02,160.661,65.977,32.225 -2020-11-25 20:45:00,124.89,157.409,65.977,32.225 -2020-11-25 21:00:00,121.48,155.282,58.798,32.225 -2020-11-25 21:15:00,116.62,153.631,58.798,32.225 -2020-11-25 21:30:00,108.88,151.487,58.798,32.225 -2020-11-25 21:45:00,109.31,149.749,58.798,32.225 -2020-11-25 22:00:00,105.77,143.263,54.486000000000004,32.225 -2020-11-25 22:15:00,109.26,138.168,54.486000000000004,32.225 -2020-11-25 22:30:00,106.25,123.12200000000001,54.486000000000004,32.225 -2020-11-25 22:45:00,104.95,115.227,54.486000000000004,32.225 -2020-11-25 23:00:00,95.13,110.454,47.783,32.225 -2020-11-25 23:15:00,98.94,108.286,47.783,32.225 -2020-11-25 23:30:00,97.56,108.693,47.783,32.225 -2020-11-25 23:45:00,94.42,108.366,47.783,32.225 -2020-11-26 00:00:00,86.96,102.682,43.88,32.225 -2020-11-26 00:15:00,83.82,102.54,43.88,32.225 -2020-11-26 00:30:00,85.5,102.928,43.88,32.225 -2020-11-26 00:45:00,88.72,103.77799999999999,43.88,32.225 -2020-11-26 01:00:00,88.89,105.38,42.242,32.225 -2020-11-26 01:15:00,85.06,106.413,42.242,32.225 -2020-11-26 01:30:00,83.57,106.809,42.242,32.225 -2020-11-26 01:45:00,85.67,107.31299999999999,42.242,32.225 -2020-11-26 02:00:00,86.12,108.552,40.918,32.225 -2020-11-26 02:15:00,83.43,109.53299999999999,40.918,32.225 -2020-11-26 02:30:00,83.05,109.21600000000001,40.918,32.225 -2020-11-26 02:45:00,85.0,110.82700000000001,40.918,32.225 -2020-11-26 03:00:00,85.05,113.26700000000001,40.411,32.225 -2020-11-26 03:15:00,83.54,114.24600000000001,40.411,32.225 -2020-11-26 03:30:00,84.59,116.25299999999999,40.411,32.225 -2020-11-26 03:45:00,88.11,116.75299999999999,40.411,32.225 -2020-11-26 04:00:00,89.54,130.483,41.246,32.225 -2020-11-26 04:15:00,86.6,143.284,41.246,32.225 -2020-11-26 04:30:00,88.88,144.537,41.246,32.225 -2020-11-26 04:45:00,93.86,146.585,41.246,32.225 -2020-11-26 05:00:00,97.17,181.04,44.533,32.225 -2020-11-26 05:15:00,98.12,211.076,44.533,32.225 -2020-11-26 05:30:00,99.16,206.66400000000002,44.533,32.225 -2020-11-26 05:45:00,105.0,197.649,44.533,32.225 -2020-11-26 06:00:00,117.69,193.375,55.005,32.225 -2020-11-26 06:15:00,124.77,198.52,55.005,32.225 -2020-11-26 06:30:00,126.33,200.138,55.005,32.225 -2020-11-26 06:45:00,125.77,201.525,55.005,32.225 -2020-11-26 07:00:00,130.95,202.803,64.597,32.225 -2020-11-26 07:15:00,134.46,206.86,64.597,32.225 -2020-11-26 07:30:00,133.78,208.47,64.597,32.225 -2020-11-26 07:45:00,135.55,208.97400000000002,64.597,32.225 -2020-11-26 08:00:00,136.97,208.035,71.71600000000001,32.225 -2020-11-26 08:15:00,136.62,207.50400000000002,71.71600000000001,32.225 -2020-11-26 08:30:00,135.96,205.636,71.71600000000001,32.225 -2020-11-26 08:45:00,134.99,202.37,71.71600000000001,32.225 -2020-11-26 09:00:00,134.73,196.488,66.51899999999999,32.225 -2020-11-26 09:15:00,134.01,192.83599999999998,66.51899999999999,32.225 -2020-11-26 09:30:00,134.51,190.507,66.51899999999999,32.225 -2020-11-26 09:45:00,133.76,188.261,66.51899999999999,32.225 -2020-11-26 10:00:00,133.11,185.40099999999998,63.04,32.225 -2020-11-26 10:15:00,133.63,181.81599999999997,63.04,32.225 -2020-11-26 10:30:00,132.09,179.71400000000003,63.04,32.225 -2020-11-26 10:45:00,131.91,178.551,63.04,32.225 -2020-11-26 11:00:00,130.31,176.675,60.998000000000005,32.225 -2020-11-26 11:15:00,129.82,176.05,60.998000000000005,32.225 -2020-11-26 11:30:00,129.38,175.459,60.998000000000005,32.225 -2020-11-26 11:45:00,129.19,174.84900000000002,60.998000000000005,32.225 -2020-11-26 12:00:00,129.43,169.345,58.27,32.225 -2020-11-26 12:15:00,129.92,168.167,58.27,32.225 -2020-11-26 12:30:00,130.77,168.467,58.27,32.225 -2020-11-26 12:45:00,128.99,169.15200000000002,58.27,32.225 -2020-11-26 13:00:00,127.51,168.292,57.196000000000005,32.225 -2020-11-26 13:15:00,127.43,167.783,57.196000000000005,32.225 -2020-11-26 13:30:00,128.14,167.3,57.196000000000005,32.225 -2020-11-26 13:45:00,128.96,167.18400000000003,57.196000000000005,32.225 -2020-11-26 14:00:00,131.54,166.625,57.38399999999999,32.225 -2020-11-26 14:15:00,132.48,166.65400000000002,57.38399999999999,32.225 -2020-11-26 14:30:00,133.07,166.63,57.38399999999999,32.225 -2020-11-26 14:45:00,134.6,166.475,57.38399999999999,32.225 -2020-11-26 15:00:00,135.57,166.542,58.647,32.225 -2020-11-26 15:15:00,134.79,166.982,58.647,32.225 -2020-11-26 15:30:00,133.95,168.06900000000002,58.647,32.225 -2020-11-26 15:45:00,134.77,168.833,58.647,32.225 -2020-11-26 16:00:00,135.09,172.248,60.083999999999996,32.225 -2020-11-26 16:15:00,139.08,173.813,60.083999999999996,32.225 -2020-11-26 16:30:00,142.52,175.12900000000002,60.083999999999996,32.225 -2020-11-26 16:45:00,144.11,175.41400000000002,60.083999999999996,32.225 -2020-11-26 17:00:00,145.11,178.605,65.85600000000001,32.225 -2020-11-26 17:15:00,145.39,179.16299999999998,65.85600000000001,32.225 -2020-11-26 17:30:00,144.99,179.56400000000002,65.85600000000001,32.225 -2020-11-26 17:45:00,144.73,179.125,65.85600000000001,32.225 -2020-11-26 18:00:00,142.36,180.87900000000002,69.855,32.225 -2020-11-26 18:15:00,139.12,179.732,69.855,32.225 -2020-11-26 18:30:00,138.76,177.99200000000002,69.855,32.225 -2020-11-26 18:45:00,139.25,178.859,69.855,32.225 -2020-11-26 19:00:00,138.59,180.332,74.015,32.225 -2020-11-26 19:15:00,135.27,177.285,74.015,32.225 -2020-11-26 19:30:00,133.83,175.861,74.015,32.225 -2020-11-26 19:45:00,132.18,172.55599999999998,74.015,32.225 -2020-11-26 20:00:00,127.04,168.832,65.316,32.225 -2020-11-26 20:15:00,124.0,163.852,65.316,32.225 -2020-11-26 20:30:00,120.42,161.009,65.316,32.225 -2020-11-26 20:45:00,117.9,157.766,65.316,32.225 -2020-11-26 21:00:00,116.75,155.628,58.403999999999996,32.225 -2020-11-26 21:15:00,114.04,153.963,58.403999999999996,32.225 -2020-11-26 21:30:00,110.09,151.82299999999998,58.403999999999996,32.225 -2020-11-26 21:45:00,106.75,150.084,58.403999999999996,32.225 -2020-11-26 22:00:00,103.22,143.605,54.092,32.225 -2020-11-26 22:15:00,99.89,138.503,54.092,32.225 -2020-11-26 22:30:00,96.72,123.51100000000001,54.092,32.225 -2020-11-26 22:45:00,95.07,115.624,54.092,32.225 -2020-11-26 23:00:00,93.68,110.834,48.18600000000001,32.225 -2020-11-26 23:15:00,90.11,108.655,48.18600000000001,32.225 -2020-11-26 23:30:00,88.8,109.069,48.18600000000001,32.225 -2020-11-26 23:45:00,89.14,108.726,48.18600000000001,32.225 -2020-11-27 00:00:00,84.43,101.79899999999999,45.18899999999999,32.225 -2020-11-27 00:15:00,89.72,101.839,45.18899999999999,32.225 -2020-11-27 00:30:00,89.24,102.156,45.18899999999999,32.225 -2020-11-27 00:45:00,87.86,103.16,45.18899999999999,32.225 -2020-11-27 01:00:00,81.04,104.459,43.256,32.225 -2020-11-27 01:15:00,79.47,106.179,43.256,32.225 -2020-11-27 01:30:00,83.22,106.521,43.256,32.225 -2020-11-27 01:45:00,85.75,107.06,43.256,32.225 -2020-11-27 02:00:00,86.44,108.556,42.312,32.225 -2020-11-27 02:15:00,83.54,109.431,42.312,32.225 -2020-11-27 02:30:00,79.41,109.71,42.312,32.225 -2020-11-27 02:45:00,79.4,111.234,42.312,32.225 -2020-11-27 03:00:00,80.77,112.949,41.833,32.225 -2020-11-27 03:15:00,88.63,114.539,41.833,32.225 -2020-11-27 03:30:00,90.12,116.492,41.833,32.225 -2020-11-27 03:45:00,88.67,117.42200000000001,41.833,32.225 -2020-11-27 04:00:00,85.04,131.356,42.732,32.225 -2020-11-27 04:15:00,84.49,143.668,42.732,32.225 -2020-11-27 04:30:00,91.48,145.282,42.732,32.225 -2020-11-27 04:45:00,95.37,146.24,42.732,32.225 -2020-11-27 05:00:00,98.6,179.519,46.254,32.225 -2020-11-27 05:15:00,98.74,211.0,46.254,32.225 -2020-11-27 05:30:00,104.82,207.55599999999998,46.254,32.225 -2020-11-27 05:45:00,107.0,198.40900000000002,46.254,32.225 -2020-11-27 06:00:00,109.72,194.56900000000002,56.76,32.225 -2020-11-27 06:15:00,114.28,198.49099999999999,56.76,32.225 -2020-11-27 06:30:00,121.1,199.40200000000002,56.76,32.225 -2020-11-27 06:45:00,125.73,202.19099999999997,56.76,32.225 -2020-11-27 07:00:00,132.85,202.873,66.029,32.225 -2020-11-27 07:15:00,137.07,207.98,66.029,32.225 -2020-11-27 07:30:00,138.18,209.1,66.029,32.225 -2020-11-27 07:45:00,139.13,208.80700000000002,66.029,32.225 -2020-11-27 08:00:00,140.02,207.06799999999998,73.128,32.225 -2020-11-27 08:15:00,141.53,206.30599999999998,73.128,32.225 -2020-11-27 08:30:00,141.95,205.28,73.128,32.225 -2020-11-27 08:45:00,141.74,200.595,73.128,32.225 -2020-11-27 09:00:00,143.12,194.68900000000002,68.23100000000001,32.225 -2020-11-27 09:15:00,144.97,191.862,68.23100000000001,32.225 -2020-11-27 09:30:00,146.38,189.045,68.23100000000001,32.225 -2020-11-27 09:45:00,148.07,186.753,68.23100000000001,32.225 -2020-11-27 10:00:00,148.43,182.864,64.733,32.225 -2020-11-27 10:15:00,149.35,179.82,64.733,32.225 -2020-11-27 10:30:00,148.75,177.725,64.733,32.225 -2020-11-27 10:45:00,149.72,176.15099999999998,64.733,32.225 -2020-11-27 11:00:00,148.49,174.278,62.0,32.225 -2020-11-27 11:15:00,148.43,172.675,62.0,32.225 -2020-11-27 11:30:00,148.91,173.55599999999998,62.0,32.225 -2020-11-27 11:45:00,148.96,172.81900000000002,62.0,32.225 -2020-11-27 12:00:00,148.12,168.327,57.876999999999995,32.225 -2020-11-27 12:15:00,148.75,165.21900000000002,57.876999999999995,32.225 -2020-11-27 12:30:00,151.8,165.7,57.876999999999995,32.225 -2020-11-27 12:45:00,148.18,166.71200000000002,57.876999999999995,32.225 -2020-11-27 13:00:00,147.61,166.737,55.585,32.225 -2020-11-27 13:15:00,145.8,166.979,55.585,32.225 -2020-11-27 13:30:00,143.06,166.637,55.585,32.225 -2020-11-27 13:45:00,142.49,166.505,55.585,32.225 -2020-11-27 14:00:00,140.86,164.783,54.5,32.225 -2020-11-27 14:15:00,140.22,164.737,54.5,32.225 -2020-11-27 14:30:00,139.38,165.44,54.5,32.225 -2020-11-27 14:45:00,139.36,165.44400000000002,54.5,32.225 -2020-11-27 15:00:00,138.46,165.082,55.131,32.225 -2020-11-27 15:15:00,138.42,165.104,55.131,32.225 -2020-11-27 15:30:00,138.56,164.794,55.131,32.225 -2020-11-27 15:45:00,138.86,165.815,55.131,32.225 -2020-11-27 16:00:00,140.34,168.062,56.8,32.225 -2020-11-27 16:15:00,142.71,169.984,56.8,32.225 -2020-11-27 16:30:00,143.85,171.358,56.8,32.225 -2020-11-27 16:45:00,144.15,171.454,56.8,32.225 -2020-11-27 17:00:00,144.16,175.07299999999998,63.428999999999995,32.225 -2020-11-27 17:15:00,144.21,175.265,63.428999999999995,32.225 -2020-11-27 17:30:00,143.91,175.423,63.428999999999995,32.225 -2020-11-27 17:45:00,143.48,174.76,63.428999999999995,32.225 -2020-11-27 18:00:00,141.75,177.138,67.915,32.225 -2020-11-27 18:15:00,141.19,175.46099999999998,67.915,32.225 -2020-11-27 18:30:00,140.31,174.05900000000003,67.915,32.225 -2020-11-27 18:45:00,140.84,174.99200000000002,67.915,32.225 -2020-11-27 19:00:00,139.02,177.395,69.428,32.225 -2020-11-27 19:15:00,136.92,175.63299999999998,69.428,32.225 -2020-11-27 19:30:00,133.97,173.84,69.428,32.225 -2020-11-27 19:45:00,132.93,169.93900000000002,69.428,32.225 -2020-11-27 20:00:00,127.08,166.252,60.56100000000001,32.225 -2020-11-27 20:15:00,123.22,161.39700000000002,60.56100000000001,32.225 -2020-11-27 20:30:00,120.97,158.409,60.56100000000001,32.225 -2020-11-27 20:45:00,121.98,155.536,60.56100000000001,32.225 -2020-11-27 21:00:00,120.28,154.04399999999998,55.18600000000001,32.225 -2020-11-27 21:15:00,115.06,153.016,55.18600000000001,32.225 -2020-11-27 21:30:00,106.27,150.89700000000002,55.18600000000001,32.225 -2020-11-27 21:45:00,104.92,149.668,55.18600000000001,32.225 -2020-11-27 22:00:00,98.21,144.031,51.433,32.225 -2020-11-27 22:15:00,95.41,138.768,51.433,32.225 -2020-11-27 22:30:00,92.29,130.185,51.433,32.225 -2020-11-27 22:45:00,89.5,125.54,51.433,32.225 -2020-11-27 23:00:00,88.76,120.616,46.201,32.225 -2020-11-27 23:15:00,90.76,116.462,46.201,32.225 -2020-11-27 23:30:00,88.25,115.31200000000001,46.201,32.225 -2020-11-27 23:45:00,84.02,114.34899999999999,46.201,32.225 -2020-11-28 00:00:00,77.33,99.921,42.576,32.047 -2020-11-28 00:15:00,75.71,96.16,42.576,32.047 -2020-11-28 00:30:00,79.64,97.53200000000001,42.576,32.047 -2020-11-28 00:45:00,79.58,99.0,42.576,32.047 -2020-11-28 01:00:00,78.45,100.935,39.34,32.047 -2020-11-28 01:15:00,69.57,101.92299999999999,39.34,32.047 -2020-11-28 01:30:00,71.73,101.686,39.34,32.047 -2020-11-28 01:45:00,75.49,102.255,39.34,32.047 -2020-11-28 02:00:00,75.22,104.189,37.582,32.047 -2020-11-28 02:15:00,73.99,104.60799999999999,37.582,32.047 -2020-11-28 02:30:00,67.25,103.791,37.582,32.047 -2020-11-28 02:45:00,71.5,105.54299999999999,37.582,32.047 -2020-11-28 03:00:00,74.56,107.538,36.523,32.047 -2020-11-28 03:15:00,74.51,107.991,36.523,32.047 -2020-11-28 03:30:00,72.04,108.655,36.523,32.047 -2020-11-28 03:45:00,70.18,109.949,36.523,32.047 -2020-11-28 04:00:00,74.75,120.022,36.347,32.047 -2020-11-28 04:15:00,70.48,130.019,36.347,32.047 -2020-11-28 04:30:00,69.82,129.468,36.347,32.047 -2020-11-28 04:45:00,68.36,130.064,36.347,32.047 -2020-11-28 05:00:00,68.09,148.316,36.407,32.047 -2020-11-28 05:15:00,67.78,161.641,36.407,32.047 -2020-11-28 05:30:00,68.17,158.80200000000002,36.407,32.047 -2020-11-28 05:45:00,70.47,155.115,36.407,32.047 -2020-11-28 06:00:00,72.18,169.19400000000002,38.228,32.047 -2020-11-28 06:15:00,75.04,188.45,38.228,32.047 -2020-11-28 06:30:00,76.95,184.31099999999998,38.228,32.047 -2020-11-28 06:45:00,78.95,178.916,38.228,32.047 -2020-11-28 07:00:00,81.61,176.02900000000002,41.905,32.047 -2020-11-28 07:15:00,85.49,179.83700000000002,41.905,32.047 -2020-11-28 07:30:00,86.76,183.553,41.905,32.047 -2020-11-28 07:45:00,89.52,186.821,41.905,32.047 -2020-11-28 08:00:00,92.6,188.55,46.051,32.047 -2020-11-28 08:15:00,93.47,190.86,46.051,32.047 -2020-11-28 08:30:00,94.42,191.234,46.051,32.047 -2020-11-28 08:45:00,96.07,189.398,46.051,32.047 -2020-11-28 09:00:00,97.99,185.484,46.683,32.047 -2020-11-28 09:15:00,98.87,183.395,46.683,32.047 -2020-11-28 09:30:00,100.11,181.44400000000002,46.683,32.047 -2020-11-28 09:45:00,100.75,179.19799999999998,46.683,32.047 -2020-11-28 10:00:00,100.87,175.627,44.425,32.047 -2020-11-28 10:15:00,100.95,172.757,44.425,32.047 -2020-11-28 10:30:00,101.61,170.725,44.425,32.047 -2020-11-28 10:45:00,102.02,170.206,44.425,32.047 -2020-11-28 11:00:00,102.71,168.458,42.148999999999994,32.047 -2020-11-28 11:15:00,102.34,166.38299999999998,42.148999999999994,32.047 -2020-11-28 11:30:00,102.7,166.33900000000003,42.148999999999994,32.047 -2020-11-28 11:45:00,102.53,164.873,42.148999999999994,32.047 -2020-11-28 12:00:00,101.89,159.649,39.683,32.047 -2020-11-28 12:15:00,99.63,157.246,39.683,32.047 -2020-11-28 12:30:00,98.83,158.006,39.683,32.047 -2020-11-28 12:45:00,95.47,158.469,39.683,32.047 -2020-11-28 13:00:00,91.62,157.96,37.154,32.047 -2020-11-28 13:15:00,89.78,156.328,37.154,32.047 -2020-11-28 13:30:00,88.89,155.621,37.154,32.047 -2020-11-28 13:45:00,89.56,155.675,37.154,32.047 -2020-11-28 14:00:00,88.24,154.984,36.457,32.047 -2020-11-28 14:15:00,89.2,154.278,36.457,32.047 -2020-11-28 14:30:00,90.17,153.356,36.457,32.047 -2020-11-28 14:45:00,90.93,153.635,36.457,32.047 -2020-11-28 15:00:00,91.29,153.899,38.257,32.047 -2020-11-28 15:15:00,91.79,154.743,38.257,32.047 -2020-11-28 15:30:00,92.89,155.843,38.257,32.047 -2020-11-28 15:45:00,94.51,156.736,38.257,32.047 -2020-11-28 16:00:00,96.87,158.286,41.181000000000004,32.047 -2020-11-28 16:15:00,100.02,160.869,41.181000000000004,32.047 -2020-11-28 16:30:00,104.43,162.238,41.181000000000004,32.047 -2020-11-28 16:45:00,105.58,163.142,41.181000000000004,32.047 -2020-11-28 17:00:00,107.55,166.046,46.806000000000004,32.047 -2020-11-28 17:15:00,108.65,167.36599999999999,46.806000000000004,32.047 -2020-11-28 17:30:00,109.6,167.429,46.806000000000004,32.047 -2020-11-28 17:45:00,109.96,166.5,46.806000000000004,32.047 -2020-11-28 18:00:00,110.46,168.673,52.073,32.047 -2020-11-28 18:15:00,109.84,168.799,52.073,32.047 -2020-11-28 18:30:00,109.58,168.78900000000002,52.073,32.047 -2020-11-28 18:45:00,109.05,166.24200000000002,52.073,32.047 -2020-11-28 19:00:00,106.97,169.187,53.608000000000004,32.047 -2020-11-28 19:15:00,105.88,166.82299999999998,53.608000000000004,32.047 -2020-11-28 19:30:00,104.51,165.793,53.608000000000004,32.047 -2020-11-28 19:45:00,103.77,161.985,53.608000000000004,32.047 -2020-11-28 20:00:00,99.85,160.374,50.265,32.047 -2020-11-28 20:15:00,95.69,157.26,50.265,32.047 -2020-11-28 20:30:00,93.1,153.798,50.265,32.047 -2020-11-28 20:45:00,91.53,150.88299999999998,50.265,32.047 -2020-11-28 21:00:00,89.7,151.108,45.766000000000005,32.047 -2020-11-28 21:15:00,87.06,150.393,45.766000000000005,32.047 -2020-11-28 21:30:00,85.08,149.381,45.766000000000005,32.047 -2020-11-28 21:45:00,84.12,147.709,45.766000000000005,32.047 -2020-11-28 22:00:00,82.85,143.25,45.97,32.047 -2020-11-28 22:15:00,80.31,140.19299999999998,45.97,32.047 -2020-11-28 22:30:00,77.52,137.02700000000002,45.97,32.047 -2020-11-28 22:45:00,75.91,134.08100000000002,45.97,32.047 -2020-11-28 23:00:00,73.58,131.162,40.415,32.047 -2020-11-28 23:15:00,72.14,125.635,40.415,32.047 -2020-11-28 23:30:00,69.33,123.365,40.415,32.047 -2020-11-28 23:45:00,66.87,120.325,40.415,32.047 -2020-11-29 00:00:00,64.94,100.87299999999999,36.376,32.047 -2020-11-29 00:15:00,62.98,96.604,36.376,32.047 -2020-11-29 00:30:00,61.95,97.626,36.376,32.047 -2020-11-29 00:45:00,60.96,99.65100000000001,36.376,32.047 -2020-11-29 01:00:00,59.8,101.545,32.992,32.047 -2020-11-29 01:15:00,59.26,103.4,32.992,32.047 -2020-11-29 01:30:00,58.37,103.59,32.992,32.047 -2020-11-29 01:45:00,57.51,103.811,32.992,32.047 -2020-11-29 02:00:00,57.27,105.13799999999999,32.327,32.047 -2020-11-29 02:15:00,56.22,104.96799999999999,32.327,32.047 -2020-11-29 02:30:00,55.73,104.90899999999999,32.327,32.047 -2020-11-29 02:45:00,55.26,106.99799999999999,32.327,32.047 -2020-11-29 03:00:00,54.75,109.369,31.169,32.047 -2020-11-29 03:15:00,55.27,109.456,31.169,32.047 -2020-11-29 03:30:00,55.26,111.166,31.169,32.047 -2020-11-29 03:45:00,55.47,112.23700000000001,31.169,32.047 -2020-11-29 04:00:00,56.47,122.075,30.796,32.047 -2020-11-29 04:15:00,56.6,131.143,30.796,32.047 -2020-11-29 04:30:00,56.82,130.894,30.796,32.047 -2020-11-29 04:45:00,57.66,131.634,30.796,32.047 -2020-11-29 05:00:00,58.4,146.967,30.848000000000003,32.047 -2020-11-29 05:15:00,59.09,158.09799999999998,30.848000000000003,32.047 -2020-11-29 05:30:00,59.43,155.056,30.848000000000003,32.047 -2020-11-29 05:45:00,60.08,151.525,30.848000000000003,32.047 -2020-11-29 06:00:00,61.05,165.06400000000002,31.166,32.047 -2020-11-29 06:15:00,61.8,183.03400000000002,31.166,32.047 -2020-11-29 06:30:00,62.33,177.872,31.166,32.047 -2020-11-29 06:45:00,64.49,171.43099999999998,31.166,32.047 -2020-11-29 07:00:00,66.6,170.65900000000002,33.527,32.047 -2020-11-29 07:15:00,68.82,173.44400000000002,33.527,32.047 -2020-11-29 07:30:00,70.6,176.37099999999998,33.527,32.047 -2020-11-29 07:45:00,72.8,178.989,33.527,32.047 -2020-11-29 08:00:00,75.93,182.40900000000002,36.616,32.047 -2020-11-29 08:15:00,78.15,184.84400000000002,36.616,32.047 -2020-11-29 08:30:00,79.59,186.805,36.616,32.047 -2020-11-29 08:45:00,81.98,186.64700000000002,36.616,32.047 -2020-11-29 09:00:00,83.56,182.33700000000002,37.857,32.047 -2020-11-29 09:15:00,85.67,180.64,37.857,32.047 -2020-11-29 09:30:00,86.37,178.63299999999998,37.857,32.047 -2020-11-29 09:45:00,87.65,176.449,37.857,32.047 -2020-11-29 10:00:00,88.71,175.118,36.319,32.047 -2020-11-29 10:15:00,89.33,172.702,36.319,32.047 -2020-11-29 10:30:00,89.75,171.203,36.319,32.047 -2020-11-29 10:45:00,91.28,169.226,36.319,32.047 -2020-11-29 11:00:00,94.21,168.18400000000003,37.236999999999995,32.047 -2020-11-29 11:15:00,98.71,166.132,37.236999999999995,32.047 -2020-11-29 11:30:00,100.96,165.42700000000002,37.236999999999995,32.047 -2020-11-29 11:45:00,103.01,164.52,37.236999999999995,32.047 -2020-11-29 12:00:00,100.88,158.976,34.871,32.047 -2020-11-29 12:15:00,98.94,158.11700000000002,34.871,32.047 -2020-11-29 12:30:00,96.38,157.68,34.871,32.047 -2020-11-29 12:45:00,94.52,157.204,34.871,32.047 -2020-11-29 13:00:00,94.12,155.987,29.738000000000003,32.047 -2020-11-29 13:15:00,92.16,156.864,29.738000000000003,32.047 -2020-11-29 13:30:00,90.98,155.782,29.738000000000003,32.047 -2020-11-29 13:45:00,89.74,155.44899999999998,29.738000000000003,32.047 -2020-11-29 14:00:00,89.32,155.141,27.333000000000002,32.047 -2020-11-29 14:15:00,88.36,155.555,27.333000000000002,32.047 -2020-11-29 14:30:00,90.19,155.519,27.333000000000002,32.047 -2020-11-29 14:45:00,90.43,155.267,27.333000000000002,32.047 -2020-11-29 15:00:00,89.89,154.24200000000002,28.232,32.047 -2020-11-29 15:15:00,90.56,155.61,28.232,32.047 -2020-11-29 15:30:00,91.22,157.219,28.232,32.047 -2020-11-29 15:45:00,92.39,158.764,28.232,32.047 -2020-11-29 16:00:00,95.42,161.649,32.815,32.047 -2020-11-29 16:15:00,96.9,163.506,32.815,32.047 -2020-11-29 16:30:00,98.92,165.269,32.815,32.047 -2020-11-29 16:45:00,101.02,166.33900000000003,32.815,32.047 -2020-11-29 17:00:00,102.53,169.273,43.068999999999996,32.047 -2020-11-29 17:15:00,104.58,170.584,43.068999999999996,32.047 -2020-11-29 17:30:00,105.89,171.041,43.068999999999996,32.047 -2020-11-29 17:45:00,107.7,172.16,43.068999999999996,32.047 -2020-11-29 18:00:00,106.71,173.975,50.498999999999995,32.047 -2020-11-29 18:15:00,105.17,175.155,50.498999999999995,32.047 -2020-11-29 18:30:00,103.9,173.296,50.498999999999995,32.047 -2020-11-29 18:45:00,102.8,172.385,50.498999999999995,32.047 -2020-11-29 19:00:00,101.44,175.391,53.481,32.047 -2020-11-29 19:15:00,99.87,173.355,53.481,32.047 -2020-11-29 19:30:00,98.61,172.127,53.481,32.047 -2020-11-29 19:45:00,96.7,169.475,53.481,32.047 -2020-11-29 20:00:00,95.81,167.84900000000002,51.687,32.047 -2020-11-29 20:15:00,93.42,165.55900000000003,51.687,32.047 -2020-11-29 20:30:00,91.99,163.27700000000002,51.687,32.047 -2020-11-29 20:45:00,90.53,159.05200000000002,51.687,32.047 -2020-11-29 21:00:00,89.47,156.99200000000002,47.674,32.047 -2020-11-29 21:15:00,87.86,155.668,47.674,32.047 -2020-11-29 21:30:00,87.32,154.80700000000002,47.674,32.047 -2020-11-29 21:45:00,87.42,153.313,47.674,32.047 -2020-11-29 22:00:00,87.27,148.168,48.178000000000004,32.047 -2020-11-29 22:15:00,87.13,144.168,48.178000000000004,32.047 -2020-11-29 22:30:00,85.14,138.221,48.178000000000004,32.047 -2020-11-29 22:45:00,83.37,134.334,48.178000000000004,32.047 -2020-11-29 23:00:00,80.39,128.91899999999998,42.553999999999995,32.047 -2020-11-29 23:15:00,78.24,125.204,42.553999999999995,32.047 -2020-11-29 23:30:00,76.63,123.557,42.553999999999995,32.047 -2020-11-29 23:45:00,74.94,121.303,42.553999999999995,32.047 -2020-11-30 00:00:00,70.81,105.06200000000001,37.177,32.225 -2020-11-30 00:15:00,71.25,103.441,37.177,32.225 -2020-11-30 00:30:00,71.29,104.5,37.177,32.225 -2020-11-30 00:45:00,72.06,105.978,37.177,32.225 -2020-11-30 01:00:00,67.48,107.973,35.358000000000004,32.225 -2020-11-30 01:15:00,69.08,109.38799999999999,35.358000000000004,32.225 -2020-11-30 01:30:00,68.47,109.70200000000001,35.358000000000004,32.225 -2020-11-30 01:45:00,68.71,109.99,35.358000000000004,32.225 -2020-11-30 02:00:00,66.0,111.391,35.03,32.225 -2020-11-30 02:15:00,67.54,112.273,35.03,32.225 -2020-11-30 02:30:00,67.4,112.52600000000001,35.03,32.225 -2020-11-30 02:45:00,68.06,114.06,35.03,32.225 -2020-11-30 03:00:00,66.68,117.569,34.394,32.225 -2020-11-30 03:15:00,67.69,119.211,34.394,32.225 -2020-11-30 03:30:00,68.14,120.822,34.394,32.225 -2020-11-30 03:45:00,69.36,121.333,34.394,32.225 -2020-11-30 04:00:00,71.04,135.393,34.421,32.225 -2020-11-30 04:15:00,71.36,148.519,34.421,32.225 -2020-11-30 04:30:00,80.36,150.013,34.421,32.225 -2020-11-30 04:45:00,83.58,150.964,34.421,32.225 -2020-11-30 05:00:00,84.07,180.87099999999998,39.435,32.225 -2020-11-30 05:15:00,83.14,210.899,39.435,32.225 -2020-11-30 05:30:00,93.56,207.80200000000002,39.435,32.225 -2020-11-30 05:45:00,101.91,198.94,39.435,32.225 -2020-11-30 06:00:00,110.42,195.543,55.685,32.225 -2020-11-30 06:15:00,109.5,199.292,55.685,32.225 -2020-11-30 06:30:00,112.78,201.679,55.685,32.225 -2020-11-30 06:45:00,120.16,203.687,55.685,32.225 -2020-11-30 07:00:00,123.28,205.102,66.837,32.225 -2020-11-30 07:15:00,128.77,209.372,66.837,32.225 -2020-11-30 07:30:00,128.06,211.49400000000003,66.837,32.225 -2020-11-30 07:45:00,128.21,211.993,66.837,32.225 -2020-11-30 08:00:00,135.05,211.018,72.217,32.225 -2020-11-30 08:15:00,132.17,211.399,72.217,32.225 -2020-11-30 08:30:00,130.69,209.71200000000002,72.217,32.225 -2020-11-30 08:45:00,130.49,206.71200000000002,72.217,32.225 -2020-11-30 09:00:00,131.71,201.407,66.117,32.225 -2020-11-30 09:15:00,132.93,196.41099999999997,66.117,32.225 -2020-11-30 09:30:00,133.88,193.389,66.117,32.225 -2020-11-30 09:45:00,136.35,191.048,66.117,32.225 -2020-11-30 10:00:00,133.09,188.88,62.1,32.225 -2020-11-30 10:15:00,133.18,186.13,62.1,32.225 -2020-11-30 10:30:00,131.07,183.77599999999998,62.1,32.225 -2020-11-30 10:45:00,130.86,182.16299999999998,62.1,32.225 -2020-11-30 11:00:00,129.58,178.945,60.021,32.225 -2020-11-30 11:15:00,127.7,178.484,60.021,32.225 -2020-11-30 11:30:00,128.06,179.08900000000003,60.021,32.225 -2020-11-30 11:45:00,127.63,177.903,60.021,32.225 -2020-11-30 12:00:00,127.01,173.58900000000003,56.75899999999999,32.225 -2020-11-30 12:15:00,129.44,172.75599999999997,56.75899999999999,32.225 -2020-11-30 12:30:00,126.09,172.38299999999998,56.75899999999999,32.225 -2020-11-30 12:45:00,130.5,173.231,56.75899999999999,32.225 -2020-11-30 13:00:00,127.8,172.65200000000002,56.04600000000001,32.225 -2020-11-30 13:15:00,135.15,172.187,56.04600000000001,32.225 -2020-11-30 13:30:00,133.2,170.669,56.04600000000001,32.225 -2020-11-30 13:45:00,134.94,170.502,56.04600000000001,32.225 -2020-11-30 14:00:00,132.77,169.498,55.475,32.225 -2020-11-30 14:15:00,134.22,169.459,55.475,32.225 -2020-11-30 14:30:00,134.93,168.958,55.475,32.225 -2020-11-30 14:45:00,139.22,169.021,55.475,32.225 -2020-11-30 15:00:00,139.45,169.52599999999998,57.048,32.225 -2020-11-30 15:15:00,138.24,169.551,57.048,32.225 -2020-11-30 15:30:00,137.99,170.58900000000003,57.048,32.225 -2020-11-30 15:45:00,136.61,171.674,57.048,32.225 -2020-11-30 16:00:00,139.97,174.865,59.06,32.225 -2020-11-30 16:15:00,141.95,176.092,59.06,32.225 -2020-11-30 16:30:00,140.58,176.91,59.06,32.225 -2020-11-30 16:45:00,140.15,176.986,59.06,32.225 -2020-11-30 17:00:00,142.38,179.55700000000002,65.419,32.225 -2020-11-30 17:15:00,140.03,180.122,65.419,32.225 -2020-11-30 17:30:00,141.11,180.05200000000002,65.419,32.225 -2020-11-30 17:45:00,140.16,179.81400000000002,65.419,32.225 -2020-11-30 18:00:00,138.78,181.859,69.345,32.225 -2020-11-30 18:15:00,137.07,180.787,69.345,32.225 -2020-11-30 18:30:00,136.41,179.389,69.345,32.225 -2020-11-30 18:45:00,137.66,179.602,69.345,32.225 -2020-11-30 19:00:00,134.46,181.15599999999998,73.825,32.225 -2020-11-30 19:15:00,133.63,178.29,73.825,32.225 -2020-11-30 19:30:00,137.08,177.426,73.825,32.225 -2020-11-30 19:45:00,135.8,173.923,73.825,32.225 -2020-11-30 20:00:00,129.29,170.03,64.027,32.225 -2020-11-30 20:15:00,123.1,165.794,64.027,32.225 -2020-11-30 20:30:00,117.76,161.946,64.027,32.225 -2020-11-30 20:45:00,115.12,159.184,64.027,32.225 -2020-11-30 21:00:00,116.06,157.475,57.952,32.225 -2020-11-30 21:15:00,113.82,155.185,57.952,32.225 -2020-11-30 21:30:00,112.58,153.67600000000002,57.952,32.225 -2020-11-30 21:45:00,105.49,151.708,57.952,32.225 -2020-11-30 22:00:00,101.57,143.72299999999998,53.031000000000006,32.225 -2020-11-30 22:15:00,95.52,138.885,53.031000000000006,32.225 -2020-11-30 22:30:00,97.58,123.971,53.031000000000006,32.225 -2020-11-30 22:45:00,97.01,115.875,53.031000000000006,32.225 -2020-11-30 23:00:00,92.05,111.11399999999999,45.085,32.225 -2020-11-30 23:15:00,89.71,109.441,45.085,32.225 -2020-11-30 23:30:00,83.96,110.206,45.085,32.225 -2020-11-30 23:45:00,81.23,110.156,45.085,32.225 -2020-12-01 00:00:00,77.38,114.376,43.537,32.65 -2020-12-01 00:15:00,76.24,114.65799999999999,43.537,32.65 -2020-12-01 00:30:00,76.87,115.84100000000001,43.537,32.65 -2020-12-01 00:45:00,75.04,117.59700000000001,43.537,32.65 -2020-12-01 01:00:00,73.08,119.316,41.854,32.65 -2020-12-01 01:15:00,73.82,120.051,41.854,32.65 -2020-12-01 01:30:00,71.9,120.405,41.854,32.65 -2020-12-01 01:45:00,73.54,121.234,41.854,32.65 -2020-12-01 02:00:00,71.89,122.501,40.321,32.65 -2020-12-01 02:15:00,73.96,123.98899999999999,40.321,32.65 -2020-12-01 02:30:00,71.76,124.215,40.321,32.65 -2020-12-01 02:45:00,72.0,126.095,40.321,32.65 -2020-12-01 03:00:00,71.75,128.914,39.632,32.65 -2020-12-01 03:15:00,75.34,129.286,39.632,32.65 -2020-12-01 03:30:00,74.29,131.14,39.632,32.65 -2020-12-01 03:45:00,75.41,132.171,39.632,32.65 -2020-12-01 04:00:00,76.26,145.259,40.183,32.65 -2020-12-01 04:15:00,77.11,157.393,40.183,32.65 -2020-12-01 04:30:00,79.19,160.136,40.183,32.65 -2020-12-01 04:45:00,80.44,162.73,40.183,32.65 -2020-12-01 05:00:00,84.76,197.88,43.945,32.65 -2020-12-01 05:15:00,88.06,227.541,43.945,32.65 -2020-12-01 05:30:00,90.98,222.683,43.945,32.65 -2020-12-01 05:45:00,99.38,214.511,43.945,32.65 -2020-12-01 06:00:00,107.58,210.252,56.048,32.65 -2020-12-01 06:15:00,111.62,215.68,56.048,32.65 -2020-12-01 06:30:00,119.23,217.26,56.048,32.65 -2020-12-01 06:45:00,121.67,219.46099999999998,56.048,32.65 -2020-12-01 07:00:00,129.63,218.843,65.74,32.65 -2020-12-01 07:15:00,130.84,223.71400000000003,65.74,32.65 -2020-12-01 07:30:00,137.48,226.422,65.74,32.65 -2020-12-01 07:45:00,137.37,227.78900000000002,65.74,32.65 -2020-12-01 08:00:00,140.6,226.493,72.757,32.65 -2020-12-01 08:15:00,141.1,226.658,72.757,32.65 -2020-12-01 08:30:00,140.35,224.92700000000002,72.757,32.65 -2020-12-01 08:45:00,140.13,222.5,72.757,32.65 -2020-12-01 09:00:00,141.71,216.40599999999998,67.692,32.65 -2020-12-01 09:15:00,141.8,212.893,67.692,32.65 -2020-12-01 09:30:00,142.81,210.30700000000002,67.692,32.65 -2020-12-01 09:45:00,145.16,207.66299999999998,67.692,32.65 -2020-12-01 10:00:00,142.57,203.207,63.506,32.65 -2020-12-01 10:15:00,141.13,199.245,63.506,32.65 -2020-12-01 10:30:00,139.34,197.101,63.506,32.65 -2020-12-01 10:45:00,143.54,195.78,63.506,32.65 -2020-12-01 11:00:00,143.81,194.96099999999998,60.758,32.65 -2020-12-01 11:15:00,146.07,194.06400000000002,60.758,32.65 -2020-12-01 11:30:00,145.51,192.882,60.758,32.65 -2020-12-01 11:45:00,143.54,191.393,60.758,32.65 -2020-12-01 12:00:00,141.07,185.77599999999998,57.519,32.65 -2020-12-01 12:15:00,140.56,184.696,57.519,32.65 -2020-12-01 12:30:00,136.23,184.53,57.519,32.65 -2020-12-01 12:45:00,133.91,185.405,57.519,32.65 -2020-12-01 13:00:00,132.2,184.952,56.46,32.65 -2020-12-01 13:15:00,136.5,184.533,56.46,32.65 -2020-12-01 13:30:00,133.44,184.457,56.46,32.65 -2020-12-01 13:45:00,129.4,184.488,56.46,32.65 -2020-12-01 14:00:00,127.85,183.696,56.207,32.65 -2020-12-01 14:15:00,132.64,184.13400000000001,56.207,32.65 -2020-12-01 14:30:00,135.84,184.32299999999998,56.207,32.65 -2020-12-01 14:45:00,135.32,184.10299999999998,56.207,32.65 -2020-12-01 15:00:00,135.69,184.78400000000002,57.391999999999996,32.65 -2020-12-01 15:15:00,135.61,185.36599999999999,57.391999999999996,32.65 -2020-12-01 15:30:00,133.27,187.30200000000002,57.391999999999996,32.65 -2020-12-01 15:45:00,133.91,189.111,57.391999999999996,32.65 -2020-12-01 16:00:00,137.4,190.207,59.955,32.65 -2020-12-01 16:15:00,139.6,191.11599999999999,59.955,32.65 -2020-12-01 16:30:00,142.04,193.775,59.955,32.65 -2020-12-01 16:45:00,141.31,194.75400000000002,59.955,32.65 -2020-12-01 17:00:00,144.35,197.62,67.063,32.65 -2020-12-01 17:15:00,144.05,197.739,67.063,32.65 -2020-12-01 17:30:00,146.6,198.13,67.063,32.65 -2020-12-01 17:45:00,145.09,197.708,67.063,32.65 -2020-12-01 18:00:00,144.03,198.524,71.477,32.65 -2020-12-01 18:15:00,142.72,196.93200000000002,71.477,32.65 -2020-12-01 18:30:00,142.42,195.355,71.477,32.65 -2020-12-01 18:45:00,141.42,195.035,71.477,32.65 -2020-12-01 19:00:00,139.2,196.077,74.32,32.65 -2020-12-01 19:15:00,138.58,192.56,74.32,32.65 -2020-12-01 19:30:00,136.59,190.588,74.32,32.65 -2020-12-01 19:45:00,136.62,187.38299999999998,74.32,32.65 -2020-12-01 20:00:00,127.68,183.91299999999998,66.157,32.65 -2020-12-01 20:15:00,124.64,178.15099999999998,66.157,32.65 -2020-12-01 20:30:00,120.77,174.831,66.157,32.65 -2020-12-01 20:45:00,119.59,171.986,66.157,32.65 -2020-12-01 21:00:00,113.2,170.00900000000001,59.806000000000004,32.65 -2020-12-01 21:15:00,113.24,168.122,59.806000000000004,32.65 -2020-12-01 21:30:00,115.21,166.013,59.806000000000004,32.65 -2020-12-01 21:45:00,113.56,164.245,59.806000000000004,32.65 -2020-12-01 22:00:00,109.11,157.439,54.785,32.65 -2020-12-01 22:15:00,104.46,151.769,54.785,32.65 -2020-12-01 22:30:00,97.9,136.998,54.785,32.65 -2020-12-01 22:45:00,101.22,128.881,54.785,32.65 -2020-12-01 23:00:00,96.47,123.572,47.176,32.65 -2020-12-01 23:15:00,95.75,121.62100000000001,47.176,32.65 -2020-12-01 23:30:00,88.96,121.59299999999999,47.176,32.65 -2020-12-01 23:45:00,85.14,121.363,47.176,32.65 -2020-12-02 00:00:00,84.73,114.73200000000001,43.42,32.65 -2020-12-02 00:15:00,88.25,114.98200000000001,43.42,32.65 -2020-12-02 00:30:00,88.93,116.162,43.42,32.65 -2020-12-02 00:45:00,86.11,117.9,43.42,32.65 -2020-12-02 01:00:00,82.71,119.66,40.869,32.65 -2020-12-02 01:15:00,87.22,120.396,40.869,32.65 -2020-12-02 01:30:00,85.57,120.758,40.869,32.65 -2020-12-02 01:45:00,81.75,121.573,40.869,32.65 -2020-12-02 02:00:00,83.12,122.861,39.541,32.65 -2020-12-02 02:15:00,84.79,124.352,39.541,32.65 -2020-12-02 02:30:00,82.28,124.575,39.541,32.65 -2020-12-02 02:45:00,82.21,126.456,39.541,32.65 -2020-12-02 03:00:00,84.94,129.262,39.052,32.65 -2020-12-02 03:15:00,86.09,129.664,39.052,32.65 -2020-12-02 03:30:00,83.57,131.52100000000002,39.052,32.65 -2020-12-02 03:45:00,84.34,132.55200000000002,39.052,32.65 -2020-12-02 04:00:00,87.42,145.606,40.36,32.65 -2020-12-02 04:15:00,90.24,157.736,40.36,32.65 -2020-12-02 04:30:00,90.6,160.465,40.36,32.65 -2020-12-02 04:45:00,88.51,163.063,40.36,32.65 -2020-12-02 05:00:00,94.36,198.175,43.133,32.65 -2020-12-02 05:15:00,100.52,227.77700000000002,43.133,32.65 -2020-12-02 05:30:00,105.35,222.95,43.133,32.65 -2020-12-02 05:45:00,105.35,214.801,43.133,32.65 -2020-12-02 06:00:00,109.55,210.567,54.953,32.65 -2020-12-02 06:15:00,115.06,215.998,54.953,32.65 -2020-12-02 06:30:00,123.34,217.628,54.953,32.65 -2020-12-02 06:45:00,127.78,219.878,54.953,32.65 -2020-12-02 07:00:00,134.98,219.26,66.566,32.65 -2020-12-02 07:15:00,137.75,224.141,66.566,32.65 -2020-12-02 07:30:00,136.68,226.858,66.566,32.65 -2020-12-02 07:45:00,138.02,228.231,66.566,32.65 -2020-12-02 08:00:00,141.25,226.949,72.902,32.65 -2020-12-02 08:15:00,138.63,227.109,72.902,32.65 -2020-12-02 08:30:00,137.77,225.389,72.902,32.65 -2020-12-02 08:45:00,136.96,222.929,72.902,32.65 -2020-12-02 09:00:00,134.22,216.808,68.465,32.65 -2020-12-02 09:15:00,138.14,213.30200000000002,68.465,32.65 -2020-12-02 09:30:00,138.14,210.72099999999998,68.465,32.65 -2020-12-02 09:45:00,137.02,208.063,68.465,32.65 -2020-12-02 10:00:00,134.5,203.59900000000002,63.625,32.65 -2020-12-02 10:15:00,134.21,199.612,63.625,32.65 -2020-12-02 10:30:00,134.09,197.44299999999998,63.625,32.65 -2020-12-02 10:45:00,133.17,196.112,63.625,32.65 -2020-12-02 11:00:00,128.69,195.27599999999998,61.628,32.65 -2020-12-02 11:15:00,131.51,194.36599999999999,61.628,32.65 -2020-12-02 11:30:00,130.92,193.18099999999998,61.628,32.65 -2020-12-02 11:45:00,130.63,191.68400000000003,61.628,32.65 -2020-12-02 12:00:00,128.7,186.065,58.708999999999996,32.65 -2020-12-02 12:15:00,125.44,184.99599999999998,58.708999999999996,32.65 -2020-12-02 12:30:00,126.63,184.84900000000002,58.708999999999996,32.65 -2020-12-02 12:45:00,128.48,185.729,58.708999999999996,32.65 -2020-12-02 13:00:00,126.35,185.24,57.373000000000005,32.65 -2020-12-02 13:15:00,128.95,184.826,57.373000000000005,32.65 -2020-12-02 13:30:00,127.1,184.747,57.373000000000005,32.65 -2020-12-02 13:45:00,127.31,184.768,57.373000000000005,32.65 -2020-12-02 14:00:00,125.8,183.947,57.684,32.65 -2020-12-02 14:15:00,128.72,184.392,57.684,32.65 -2020-12-02 14:30:00,128.58,184.61,57.684,32.65 -2020-12-02 14:45:00,128.73,184.4,57.684,32.65 -2020-12-02 15:00:00,132.19,185.09799999999998,58.03,32.65 -2020-12-02 15:15:00,132.73,185.68099999999998,58.03,32.65 -2020-12-02 15:30:00,132.49,187.644,58.03,32.65 -2020-12-02 15:45:00,133.74,189.456,58.03,32.65 -2020-12-02 16:00:00,137.37,190.55200000000002,59.97,32.65 -2020-12-02 16:15:00,138.66,191.484,59.97,32.65 -2020-12-02 16:30:00,141.97,194.15,59.97,32.65 -2020-12-02 16:45:00,142.93,195.16400000000002,59.97,32.65 -2020-12-02 17:00:00,147.26,198.00400000000002,65.661,32.65 -2020-12-02 17:15:00,146.69,198.15099999999998,65.661,32.65 -2020-12-02 17:30:00,147.72,198.554,65.661,32.65 -2020-12-02 17:45:00,146.61,198.139,65.661,32.65 -2020-12-02 18:00:00,144.03,198.97299999999998,70.96300000000001,32.65 -2020-12-02 18:15:00,143.55,197.338,70.96300000000001,32.65 -2020-12-02 18:30:00,141.98,195.77,70.96300000000001,32.65 -2020-12-02 18:45:00,139.97,195.455,70.96300000000001,32.65 -2020-12-02 19:00:00,141.8,196.488,74.133,32.65 -2020-12-02 19:15:00,140.75,192.959,74.133,32.65 -2020-12-02 19:30:00,137.4,190.96900000000002,74.133,32.65 -2020-12-02 19:45:00,137.36,187.734,74.133,32.65 -2020-12-02 20:00:00,130.36,184.267,65.613,32.65 -2020-12-02 20:15:00,122.11,178.49400000000003,65.613,32.65 -2020-12-02 20:30:00,123.19,175.14700000000002,65.613,32.65 -2020-12-02 20:45:00,122.73,172.321,65.613,32.65 -2020-12-02 21:00:00,117.07,170.332,58.583,32.65 -2020-12-02 21:15:00,110.96,168.428,58.583,32.65 -2020-12-02 21:30:00,110.94,166.321,58.583,32.65 -2020-12-02 21:45:00,111.13,164.558,58.583,32.65 -2020-12-02 22:00:00,105.41,157.761,54.411,32.65 -2020-12-02 22:15:00,105.31,152.091,54.411,32.65 -2020-12-02 22:30:00,102.13,137.376,54.411,32.65 -2020-12-02 22:45:00,104.64,129.267,54.411,32.65 -2020-12-02 23:00:00,96.98,123.932,47.878,32.65 -2020-12-02 23:15:00,94.32,121.976,47.878,32.65 -2020-12-02 23:30:00,94.93,121.96,47.878,32.65 -2020-12-02 23:45:00,93.72,121.713,47.878,32.65 -2020-12-03 00:00:00,87.84,115.08200000000001,44.513000000000005,32.65 -2020-12-03 00:15:00,83.11,115.303,44.513000000000005,32.65 -2020-12-03 00:30:00,84.51,116.478,44.513000000000005,32.65 -2020-12-03 00:45:00,87.95,118.196,44.513000000000005,32.65 -2020-12-03 01:00:00,84.16,119.99700000000001,43.169,32.65 -2020-12-03 01:15:00,83.09,120.73299999999999,43.169,32.65 -2020-12-03 01:30:00,85.34,121.10600000000001,43.169,32.65 -2020-12-03 01:45:00,86.49,121.904,43.169,32.65 -2020-12-03 02:00:00,82.06,123.214,41.763999999999996,32.65 -2020-12-03 02:15:00,82.6,124.706,41.763999999999996,32.65 -2020-12-03 02:30:00,85.09,124.931,41.763999999999996,32.65 -2020-12-03 02:45:00,86.07,126.811,41.763999999999996,32.65 -2020-12-03 03:00:00,80.57,129.602,41.155,32.65 -2020-12-03 03:15:00,80.92,130.036,41.155,32.65 -2020-12-03 03:30:00,83.75,131.89700000000002,41.155,32.65 -2020-12-03 03:45:00,87.62,132.925,41.155,32.65 -2020-12-03 04:00:00,89.28,145.94799999999998,41.96,32.65 -2020-12-03 04:15:00,85.37,158.075,41.96,32.65 -2020-12-03 04:30:00,83.55,160.786,41.96,32.65 -2020-12-03 04:45:00,85.67,163.389,41.96,32.65 -2020-12-03 05:00:00,90.67,198.46200000000002,45.206,32.65 -2020-12-03 05:15:00,94.78,228.007,45.206,32.65 -2020-12-03 05:30:00,95.98,223.21200000000002,45.206,32.65 -2020-12-03 05:45:00,101.33,215.085,45.206,32.65 -2020-12-03 06:00:00,109.52,210.877,55.398999999999994,32.65 -2020-12-03 06:15:00,116.92,216.308,55.398999999999994,32.65 -2020-12-03 06:30:00,120.32,217.99,55.398999999999994,32.65 -2020-12-03 06:45:00,124.73,220.28599999999997,55.398999999999994,32.65 -2020-12-03 07:00:00,131.84,219.67,64.627,32.65 -2020-12-03 07:15:00,134.4,224.56099999999998,64.627,32.65 -2020-12-03 07:30:00,137.22,227.28400000000002,64.627,32.65 -2020-12-03 07:45:00,137.69,228.66400000000002,64.627,32.65 -2020-12-03 08:00:00,141.29,227.395,70.895,32.65 -2020-12-03 08:15:00,139.35,227.55,70.895,32.65 -2020-12-03 08:30:00,139.65,225.842,70.895,32.65 -2020-12-03 08:45:00,139.71,223.34900000000002,70.895,32.65 -2020-12-03 09:00:00,139.33,217.199,66.382,32.65 -2020-12-03 09:15:00,140.17,213.701,66.382,32.65 -2020-12-03 09:30:00,140.93,211.125,66.382,32.65 -2020-12-03 09:45:00,141.66,208.455,66.382,32.65 -2020-12-03 10:00:00,143.6,203.981,62.739,32.65 -2020-12-03 10:15:00,145.07,199.96900000000002,62.739,32.65 -2020-12-03 10:30:00,144.64,197.77700000000002,62.739,32.65 -2020-12-03 10:45:00,144.41,196.437,62.739,32.65 -2020-12-03 11:00:00,143.34,195.584,60.843,32.65 -2020-12-03 11:15:00,145.71,194.658,60.843,32.65 -2020-12-03 11:30:00,144.57,193.47099999999998,60.843,32.65 -2020-12-03 11:45:00,144.85,191.968,60.843,32.65 -2020-12-03 12:00:00,142.96,186.34599999999998,58.466,32.65 -2020-12-03 12:15:00,140.5,185.28900000000002,58.466,32.65 -2020-12-03 12:30:00,139.07,185.16,58.466,32.65 -2020-12-03 12:45:00,139.98,186.046,58.466,32.65 -2020-12-03 13:00:00,137.73,185.521,56.883,32.65 -2020-12-03 13:15:00,138.78,185.111,56.883,32.65 -2020-12-03 13:30:00,136.57,185.028,56.883,32.65 -2020-12-03 13:45:00,136.43,185.03799999999998,56.883,32.65 -2020-12-03 14:00:00,135.01,184.19099999999997,56.503,32.65 -2020-12-03 14:15:00,135.23,184.644,56.503,32.65 -2020-12-03 14:30:00,135.78,184.89,56.503,32.65 -2020-12-03 14:45:00,136.83,184.68900000000002,56.503,32.65 -2020-12-03 15:00:00,136.93,185.407,57.803999999999995,32.65 -2020-12-03 15:15:00,136.54,185.989,57.803999999999995,32.65 -2020-12-03 15:30:00,135.85,187.979,57.803999999999995,32.65 -2020-12-03 15:45:00,135.94,189.792,57.803999999999995,32.65 -2020-12-03 16:00:00,139.8,190.887,59.379,32.65 -2020-12-03 16:15:00,140.97,191.84400000000002,59.379,32.65 -2020-12-03 16:30:00,142.86,194.516,59.379,32.65 -2020-12-03 16:45:00,142.14,195.565,59.379,32.65 -2020-12-03 17:00:00,143.33,198.38,64.71600000000001,32.65 -2020-12-03 17:15:00,143.46,198.551,64.71600000000001,32.65 -2020-12-03 17:30:00,145.46,198.96900000000002,64.71600000000001,32.65 -2020-12-03 17:45:00,144.91,198.55900000000003,64.71600000000001,32.65 -2020-12-03 18:00:00,141.72,199.41299999999998,68.803,32.65 -2020-12-03 18:15:00,139.73,197.738,68.803,32.65 -2020-12-03 18:30:00,138.04,196.17700000000002,68.803,32.65 -2020-12-03 18:45:00,139.53,195.868,68.803,32.65 -2020-12-03 19:00:00,136.48,196.892,72.934,32.65 -2020-12-03 19:15:00,136.16,193.352,72.934,32.65 -2020-12-03 19:30:00,132.96,191.345,72.934,32.65 -2020-12-03 19:45:00,132.54,188.079,72.934,32.65 -2020-12-03 20:00:00,124.03,184.614,65.175,32.65 -2020-12-03 20:15:00,120.74,178.831,65.175,32.65 -2020-12-03 20:30:00,117.79,175.456,65.175,32.65 -2020-12-03 20:45:00,115.66,172.65099999999998,65.175,32.65 -2020-12-03 21:00:00,113.13,170.648,58.55,32.65 -2020-12-03 21:15:00,111.13,168.72799999999998,58.55,32.65 -2020-12-03 21:30:00,107.15,166.62099999999998,58.55,32.65 -2020-12-03 21:45:00,104.82,164.864,58.55,32.65 -2020-12-03 22:00:00,98.64,158.077,55.041000000000004,32.65 -2020-12-03 22:15:00,100.35,152.408,55.041000000000004,32.65 -2020-12-03 22:30:00,99.69,137.747,55.041000000000004,32.65 -2020-12-03 22:45:00,99.18,129.645,55.041000000000004,32.65 -2020-12-03 23:00:00,96.32,124.286,48.258,32.65 -2020-12-03 23:15:00,90.21,122.32600000000001,48.258,32.65 -2020-12-03 23:30:00,91.13,122.322,48.258,32.65 -2020-12-03 23:45:00,90.28,122.055,48.258,32.65 -2020-12-04 00:00:00,88.17,114.368,45.02,32.65 -2020-12-04 00:15:00,80.56,114.755,45.02,32.65 -2020-12-04 00:30:00,74.99,115.79,45.02,32.65 -2020-12-04 00:45:00,78.87,117.596,45.02,32.65 -2020-12-04 01:00:00,80.8,119.12799999999999,42.695,32.65 -2020-12-04 01:15:00,81.08,120.788,42.695,32.65 -2020-12-04 01:30:00,79.01,120.94,42.695,32.65 -2020-12-04 01:45:00,77.31,121.831,42.695,32.65 -2020-12-04 02:00:00,74.12,123.24,41.511,32.65 -2020-12-04 02:15:00,74.97,124.619,41.511,32.65 -2020-12-04 02:30:00,79.2,125.36200000000001,41.511,32.65 -2020-12-04 02:45:00,82.28,127.291,41.511,32.65 -2020-12-04 03:00:00,79.37,129.062,41.162,32.65 -2020-12-04 03:15:00,79.26,130.482,41.162,32.65 -2020-12-04 03:30:00,82.2,132.33100000000002,41.162,32.65 -2020-12-04 03:45:00,85.02,133.678,41.162,32.65 -2020-12-04 04:00:00,84.4,146.89600000000002,42.226000000000006,32.65 -2020-12-04 04:15:00,78.87,158.79,42.226000000000006,32.65 -2020-12-04 04:30:00,75.73,161.70600000000002,42.226000000000006,32.65 -2020-12-04 04:45:00,80.98,163.172,42.226000000000006,32.65 -2020-12-04 05:00:00,85.06,196.937,45.597,32.65 -2020-12-04 05:15:00,88.99,227.93,45.597,32.65 -2020-12-04 05:30:00,89.73,224.248,45.597,32.65 -2020-12-04 05:45:00,96.09,216.083,45.597,32.65 -2020-12-04 06:00:00,108.01,212.333,56.263999999999996,32.65 -2020-12-04 06:15:00,112.74,216.285,56.263999999999996,32.65 -2020-12-04 06:30:00,117.29,217.148,56.263999999999996,32.65 -2020-12-04 06:45:00,121.66,221.128,56.263999999999996,32.65 -2020-12-04 07:00:00,130.21,219.666,66.888,32.65 -2020-12-04 07:15:00,129.15,225.585,66.888,32.65 -2020-12-04 07:30:00,132.61,228.15400000000002,66.888,32.65 -2020-12-04 07:45:00,134.61,228.649,66.888,32.65 -2020-12-04 08:00:00,137.01,226.28,73.459,32.65 -2020-12-04 08:15:00,136.94,226.03599999999997,73.459,32.65 -2020-12-04 08:30:00,137.62,225.31599999999997,73.459,32.65 -2020-12-04 08:45:00,137.29,221.213,73.459,32.65 -2020-12-04 09:00:00,138.54,215.50599999999997,69.087,32.65 -2020-12-04 09:15:00,139.96,212.579,69.087,32.65 -2020-12-04 09:30:00,139.56,209.581,69.087,32.65 -2020-12-04 09:45:00,138.84,206.78799999999998,69.087,32.65 -2020-12-04 10:00:00,136.1,201.173,65.404,32.65 -2020-12-04 10:15:00,138.43,197.835,65.404,32.65 -2020-12-04 10:30:00,137.86,195.547,65.404,32.65 -2020-12-04 10:45:00,138.39,193.75599999999997,65.404,32.65 -2020-12-04 11:00:00,136.83,192.87,63.0,32.65 -2020-12-04 11:15:00,137.96,191.03599999999997,63.0,32.65 -2020-12-04 11:30:00,138.11,191.58900000000003,63.0,32.65 -2020-12-04 11:45:00,138.08,190.125,63.0,32.65 -2020-12-04 12:00:00,136.37,185.582,59.083,32.65 -2020-12-04 12:15:00,134.96,182.451,59.083,32.65 -2020-12-04 12:30:00,134.55,182.497,59.083,32.65 -2020-12-04 12:45:00,137.65,183.898,59.083,32.65 -2020-12-04 13:00:00,132.64,184.27200000000002,56.611999999999995,32.65 -2020-12-04 13:15:00,133.9,184.679,56.611999999999995,32.65 -2020-12-04 13:30:00,132.84,184.613,56.611999999999995,32.65 -2020-12-04 13:45:00,133.43,184.551,56.611999999999995,32.65 -2020-12-04 14:00:00,130.52,182.54,55.161,32.65 -2020-12-04 14:15:00,129.92,182.826,55.161,32.65 -2020-12-04 14:30:00,127.64,183.615,55.161,32.65 -2020-12-04 14:45:00,127.22,183.72099999999998,55.161,32.65 -2020-12-04 15:00:00,125.79,183.976,55.583,32.65 -2020-12-04 15:15:00,126.08,184.122,55.583,32.65 -2020-12-04 15:30:00,126.65,184.62599999999998,55.583,32.65 -2020-12-04 15:45:00,131.71,186.58900000000003,55.583,32.65 -2020-12-04 16:00:00,133.56,186.52,57.611999999999995,32.65 -2020-12-04 16:15:00,135.6,187.78900000000002,57.611999999999995,32.65 -2020-12-04 16:30:00,138.73,190.56400000000002,57.611999999999995,32.65 -2020-12-04 16:45:00,136.33,191.52700000000002,57.611999999999995,32.65 -2020-12-04 17:00:00,138.83,194.524,64.14,32.65 -2020-12-04 17:15:00,137.41,194.31099999999998,64.14,32.65 -2020-12-04 17:30:00,138.46,194.424,64.14,32.65 -2020-12-04 17:45:00,138.3,193.78900000000002,64.14,32.65 -2020-12-04 18:00:00,135.16,195.34900000000002,68.086,32.65 -2020-12-04 18:15:00,133.86,193.245,68.086,32.65 -2020-12-04 18:30:00,134.09,192.083,68.086,32.65 -2020-12-04 18:45:00,137.38,191.77700000000002,68.086,32.65 -2020-12-04 19:00:00,132.52,193.69400000000002,69.915,32.65 -2020-12-04 19:15:00,129.24,191.503,69.915,32.65 -2020-12-04 19:30:00,128.95,189.06900000000002,69.915,32.65 -2020-12-04 19:45:00,126.18,185.313,69.915,32.65 -2020-12-04 20:00:00,118.12,181.905,61.695,32.65 -2020-12-04 20:15:00,116.18,176.11900000000003,61.695,32.65 -2020-12-04 20:30:00,113.76,172.675,61.695,32.65 -2020-12-04 20:45:00,111.07,170.445,61.695,32.65 -2020-12-04 21:00:00,107.3,168.935,56.041000000000004,32.65 -2020-12-04 21:15:00,103.46,167.43400000000003,56.041000000000004,32.65 -2020-12-04 21:30:00,100.57,165.37400000000002,56.041000000000004,32.65 -2020-12-04 21:45:00,103.14,164.165,56.041000000000004,32.65 -2020-12-04 22:00:00,98.06,158.365,51.888999999999996,32.65 -2020-12-04 22:15:00,96.92,152.564,51.888999999999996,32.65 -2020-12-04 22:30:00,84.82,144.314,51.888999999999996,32.65 -2020-12-04 22:45:00,84.39,139.736,51.888999999999996,32.65 -2020-12-04 23:00:00,84.2,133.908,45.787,32.65 -2020-12-04 23:15:00,84.93,130.002,45.787,32.65 -2020-12-04 23:30:00,82.36,128.553,45.787,32.65 -2020-12-04 23:45:00,78.57,127.60700000000001,45.787,32.65 -2020-12-05 00:00:00,72.37,111.85600000000001,41.815,32.468 -2020-12-05 00:15:00,73.35,107.964,41.815,32.468 -2020-12-05 00:30:00,73.52,110.323,41.815,32.468 -2020-12-05 00:45:00,69.58,112.825,41.815,32.468 -2020-12-05 01:00:00,65.32,115.024,38.645,32.468 -2020-12-05 01:15:00,67.07,115.698,38.645,32.468 -2020-12-05 01:30:00,70.05,115.344,38.645,32.468 -2020-12-05 01:45:00,72.67,116.0,38.645,32.468 -2020-12-05 02:00:00,67.0,118.118,36.696,32.468 -2020-12-05 02:15:00,65.94,119.133,36.696,32.468 -2020-12-05 02:30:00,62.92,118.76799999999999,36.696,32.468 -2020-12-05 02:45:00,67.89,120.795,36.696,32.468 -2020-12-05 03:00:00,68.09,123.185,35.42,32.468 -2020-12-05 03:15:00,69.91,123.42200000000001,35.42,32.468 -2020-12-05 03:30:00,67.12,123.686,35.42,32.468 -2020-12-05 03:45:00,70.54,125.135,35.42,32.468 -2020-12-05 04:00:00,70.06,134.191,35.167,32.468 -2020-12-05 04:15:00,64.46,143.546,35.167,32.468 -2020-12-05 04:30:00,62.74,144.289,35.167,32.468 -2020-12-05 04:45:00,63.37,145.262,35.167,32.468 -2020-12-05 05:00:00,63.4,163.07299999999998,35.311,32.468 -2020-12-05 05:15:00,63.89,175.081,35.311,32.468 -2020-12-05 05:30:00,64.64,171.774,35.311,32.468 -2020-12-05 05:45:00,66.98,169.051,35.311,32.468 -2020-12-05 06:00:00,68.66,184.03099999999998,37.117,32.468 -2020-12-05 06:15:00,70.86,204.283,37.117,32.468 -2020-12-05 06:30:00,72.62,199.84,37.117,32.468 -2020-12-05 06:45:00,75.93,194.908,37.117,32.468 -2020-12-05 07:00:00,78.46,189.69299999999998,40.948,32.468 -2020-12-05 07:15:00,81.06,194.37400000000002,40.948,32.468 -2020-12-05 07:30:00,83.25,199.646,40.948,32.468 -2020-12-05 07:45:00,85.28,204.113,40.948,32.468 -2020-12-05 08:00:00,88.29,205.796,44.903,32.468 -2020-12-05 08:15:00,88.3,209.168,44.903,32.468 -2020-12-05 08:30:00,89.21,210.06599999999997,44.903,32.468 -2020-12-05 08:45:00,91.96,209.077,44.903,32.468 -2020-12-05 09:00:00,93.43,205.105,46.283,32.468 -2020-12-05 09:15:00,93.03,202.935,46.283,32.468 -2020-12-05 09:30:00,95.57,200.84099999999998,46.283,32.468 -2020-12-05 09:45:00,93.63,198.196,46.283,32.468 -2020-12-05 10:00:00,95.54,192.859,44.103,32.468 -2020-12-05 10:15:00,96.38,189.66,44.103,32.468 -2020-12-05 10:30:00,98.31,187.513,44.103,32.468 -2020-12-05 10:45:00,98.64,187.00599999999997,44.103,32.468 -2020-12-05 11:00:00,101.27,186.32299999999998,42.373999999999995,32.468 -2020-12-05 11:15:00,101.54,183.795,42.373999999999995,32.468 -2020-12-05 11:30:00,99.33,183.24,42.373999999999995,32.468 -2020-12-05 11:45:00,97.85,180.82299999999998,42.373999999999995,32.468 -2020-12-05 12:00:00,99.19,175.40900000000002,39.937,32.468 -2020-12-05 12:15:00,96.78,172.928,39.937,32.468 -2020-12-05 12:30:00,95.3,173.30599999999998,39.937,32.468 -2020-12-05 12:45:00,95.84,173.953,39.937,32.468 -2020-12-05 13:00:00,90.76,173.893,37.138000000000005,32.468 -2020-12-05 13:15:00,88.94,172.267,37.138000000000005,32.468 -2020-12-05 13:30:00,88.02,171.75099999999998,37.138000000000005,32.468 -2020-12-05 13:45:00,84.59,172.145,37.138000000000005,32.468 -2020-12-05 14:00:00,86.03,171.328,36.141999999999996,32.468 -2020-12-05 14:15:00,87.73,171.08599999999998,36.141999999999996,32.468 -2020-12-05 14:30:00,88.66,170.1,36.141999999999996,32.468 -2020-12-05 14:45:00,88.57,170.43400000000003,36.141999999999996,32.468 -2020-12-05 15:00:00,90.45,171.362,37.964,32.468 -2020-12-05 15:15:00,92.3,172.308,37.964,32.468 -2020-12-05 15:30:00,95.92,174.362,37.964,32.468 -2020-12-05 15:45:00,95.54,176.33700000000002,37.964,32.468 -2020-12-05 16:00:00,99.12,175.083,40.699,32.468 -2020-12-05 16:15:00,103.2,177.25900000000001,40.699,32.468 -2020-12-05 16:30:00,101.85,179.989,40.699,32.468 -2020-12-05 16:45:00,102.17,181.863,40.699,32.468 -2020-12-05 17:00:00,105.16,184.262,46.216,32.468 -2020-12-05 17:15:00,104.75,185.71099999999998,46.216,32.468 -2020-12-05 17:30:00,109.26,185.743,46.216,32.468 -2020-12-05 17:45:00,106.26,184.71400000000003,46.216,32.468 -2020-12-05 18:00:00,107.15,185.812,51.123999999999995,32.468 -2020-12-05 18:15:00,107.16,185.459,51.123999999999995,32.468 -2020-12-05 18:30:00,106.1,185.628,51.123999999999995,32.468 -2020-12-05 18:45:00,105.22,182.0,51.123999999999995,32.468 -2020-12-05 19:00:00,103.27,184.81900000000002,52.336000000000006,32.468 -2020-12-05 19:15:00,102.54,182.122,52.336000000000006,32.468 -2020-12-05 19:30:00,101.72,180.421,52.336000000000006,32.468 -2020-12-05 19:45:00,99.84,176.465,52.336000000000006,32.468 -2020-12-05 20:00:00,95.78,175.215,48.825,32.468 -2020-12-05 20:15:00,92.88,171.52599999999998,48.825,32.468 -2020-12-05 20:30:00,90.67,167.706,48.825,32.468 -2020-12-05 20:45:00,89.27,165.101,48.825,32.468 -2020-12-05 21:00:00,84.32,165.80599999999998,43.729,32.468 -2020-12-05 21:15:00,84.83,164.725,43.729,32.468 -2020-12-05 21:30:00,83.33,163.88400000000001,43.729,32.468 -2020-12-05 21:45:00,81.56,162.278,43.729,32.468 -2020-12-05 22:00:00,77.6,157.82299999999998,44.126000000000005,32.468 -2020-12-05 22:15:00,76.48,154.488,44.126000000000005,32.468 -2020-12-05 22:30:00,73.47,152.457,44.126000000000005,32.468 -2020-12-05 22:45:00,72.33,149.74200000000002,44.126000000000005,32.468 -2020-12-05 23:00:00,67.46,146.29399999999998,38.169000000000004,32.468 -2020-12-05 23:15:00,67.5,140.766,38.169000000000004,32.468 -2020-12-05 23:30:00,65.09,137.606,38.169000000000004,32.468 -2020-12-05 23:45:00,63.28,134.237,38.169000000000004,32.468 -2020-12-06 00:00:00,59.2,112.60799999999999,35.232,32.468 -2020-12-06 00:15:00,58.21,108.366,35.232,32.468 -2020-12-06 00:30:00,58.38,110.34700000000001,35.232,32.468 -2020-12-06 00:45:00,57.76,113.522,35.232,32.468 -2020-12-06 01:00:00,53.35,115.618,31.403000000000002,32.468 -2020-12-06 01:15:00,54.31,117.323,31.403000000000002,32.468 -2020-12-06 01:30:00,53.96,117.494,31.403000000000002,32.468 -2020-12-06 01:45:00,53.89,117.824,31.403000000000002,32.468 -2020-12-06 02:00:00,52.08,119.219,30.69,32.468 -2020-12-06 02:15:00,53.48,119.389,30.69,32.468 -2020-12-06 02:30:00,52.62,119.875,30.69,32.468 -2020-12-06 02:45:00,52.89,122.36,30.69,32.468 -2020-12-06 03:00:00,50.89,125.052,29.516,32.468 -2020-12-06 03:15:00,52.01,124.8,29.516,32.468 -2020-12-06 03:30:00,49.54,126.455,29.516,32.468 -2020-12-06 03:45:00,51.95,127.82600000000001,29.516,32.468 -2020-12-06 04:00:00,51.32,136.608,29.148000000000003,32.468 -2020-12-06 04:15:00,52.22,144.953,29.148000000000003,32.468 -2020-12-06 04:30:00,53.06,145.747,29.148000000000003,32.468 -2020-12-06 04:45:00,54.12,146.994,29.148000000000003,32.468 -2020-12-06 05:00:00,54.06,161.283,28.706,32.468 -2020-12-06 05:15:00,55.45,170.83599999999998,28.706,32.468 -2020-12-06 05:30:00,55.14,167.392,28.706,32.468 -2020-12-06 05:45:00,55.66,164.93400000000003,28.706,32.468 -2020-12-06 06:00:00,57.08,179.829,28.771,32.468 -2020-12-06 06:15:00,58.52,198.37599999999998,28.771,32.468 -2020-12-06 06:30:00,60.18,192.88299999999998,28.771,32.468 -2020-12-06 06:45:00,62.27,186.947,28.771,32.468 -2020-12-06 07:00:00,64.58,184.169,31.39,32.468 -2020-12-06 07:15:00,66.66,188.005,31.39,32.468 -2020-12-06 07:30:00,70.38,192.06599999999997,31.39,32.468 -2020-12-06 07:45:00,71.0,195.782,31.39,32.468 -2020-12-06 08:00:00,72.79,199.30599999999998,34.972,32.468 -2020-12-06 08:15:00,75.41,202.59,34.972,32.468 -2020-12-06 08:30:00,74.46,205.142,34.972,32.468 -2020-12-06 08:45:00,78.84,206.12,34.972,32.468 -2020-12-06 09:00:00,80.64,201.72299999999998,36.709,32.468 -2020-12-06 09:15:00,82.63,200.10299999999998,36.709,32.468 -2020-12-06 09:30:00,84.01,197.863,36.709,32.468 -2020-12-06 09:45:00,85.27,195.09799999999998,36.709,32.468 -2020-12-06 10:00:00,87.21,192.24400000000003,35.812,32.468 -2020-12-06 10:15:00,88.37,189.549,35.812,32.468 -2020-12-06 10:30:00,89.09,187.975,35.812,32.468 -2020-12-06 10:45:00,91.13,185.59900000000002,35.812,32.468 -2020-12-06 11:00:00,92.13,185.801,36.746,32.468 -2020-12-06 11:15:00,94.24,183.386,36.746,32.468 -2020-12-06 11:30:00,96.69,181.97799999999998,36.746,32.468 -2020-12-06 11:45:00,97.52,180.15400000000002,36.746,32.468 -2020-12-06 12:00:00,95.11,174.195,35.048,32.468 -2020-12-06 12:15:00,93.73,173.592,35.048,32.468 -2020-12-06 12:30:00,90.61,172.55700000000002,35.048,32.468 -2020-12-06 12:45:00,90.11,172.25,35.048,32.468 -2020-12-06 13:00:00,86.89,171.447,29.987,32.468 -2020-12-06 13:15:00,86.08,172.782,29.987,32.468 -2020-12-06 13:30:00,85.02,172.041,29.987,32.468 -2020-12-06 13:45:00,84.86,171.791,29.987,32.468 -2020-12-06 14:00:00,83.89,171.206,27.21,32.468 -2020-12-06 14:15:00,85.55,172.157,27.21,32.468 -2020-12-06 14:30:00,85.84,172.392,27.21,32.468 -2020-12-06 14:45:00,85.27,172.317,27.21,32.468 -2020-12-06 15:00:00,86.37,171.761,27.726999999999997,32.468 -2020-12-06 15:15:00,86.71,173.438,27.726999999999997,32.468 -2020-12-06 15:30:00,87.73,176.092,27.726999999999997,32.468 -2020-12-06 15:45:00,90.08,178.752,27.726999999999997,32.468 -2020-12-06 16:00:00,91.02,179.32,32.23,32.468 -2020-12-06 16:15:00,92.78,180.62599999999998,32.23,32.468 -2020-12-06 16:30:00,94.77,183.63,32.23,32.468 -2020-12-06 16:45:00,95.94,185.667,32.23,32.468 -2020-12-06 17:00:00,99.97,188.047,42.016999999999996,32.468 -2020-12-06 17:15:00,100.43,189.213,42.016999999999996,32.468 -2020-12-06 17:30:00,101.22,189.55,42.016999999999996,32.468 -2020-12-06 17:45:00,103.06,190.769,42.016999999999996,32.468 -2020-12-06 18:00:00,104.02,191.354,49.338,32.468 -2020-12-06 18:15:00,103.43,192.268,49.338,32.468 -2020-12-06 18:30:00,103.55,190.385,49.338,32.468 -2020-12-06 18:45:00,102.55,188.59400000000002,49.338,32.468 -2020-12-06 19:00:00,101.83,191.08900000000003,52.369,32.468 -2020-12-06 19:15:00,100.33,188.959,52.369,32.468 -2020-12-06 19:30:00,99.34,187.078,52.369,32.468 -2020-12-06 19:45:00,96.53,184.515,52.369,32.468 -2020-12-06 20:00:00,95.12,183.24099999999999,50.405,32.468 -2020-12-06 20:15:00,95.13,180.514,50.405,32.468 -2020-12-06 20:30:00,94.12,177.896,50.405,32.468 -2020-12-06 20:45:00,99.95,174.113,50.405,32.468 -2020-12-06 21:00:00,96.19,172.24400000000003,46.235,32.468 -2020-12-06 21:15:00,96.46,170.52599999999998,46.235,32.468 -2020-12-06 21:30:00,87.84,169.972,46.235,32.468 -2020-12-06 21:45:00,89.67,168.50900000000001,46.235,32.468 -2020-12-06 22:00:00,87.62,162.901,46.861000000000004,32.468 -2020-12-06 22:15:00,90.69,158.804,46.861000000000004,32.468 -2020-12-06 22:30:00,89.86,153.694,46.861000000000004,32.468 -2020-12-06 22:45:00,88.9,150.136,46.861000000000004,32.468 -2020-12-06 23:00:00,78.62,143.899,41.302,32.468 -2020-12-06 23:15:00,77.12,140.21,41.302,32.468 -2020-12-06 23:30:00,79.89,137.859,41.302,32.468 -2020-12-06 23:45:00,77.11,135.356,41.302,32.468 -2020-12-07 00:00:00,72.47,117.10600000000001,37.164,32.65 -2020-12-07 00:15:00,69.76,115.786,37.164,32.65 -2020-12-07 00:30:00,69.85,117.88799999999999,37.164,32.65 -2020-12-07 00:45:00,69.38,120.509,37.164,32.65 -2020-12-07 01:00:00,67.99,122.65899999999999,34.994,32.65 -2020-12-07 01:15:00,70.59,123.839,34.994,32.65 -2020-12-07 01:30:00,73.49,124.073,34.994,32.65 -2020-12-07 01:45:00,70.94,124.50200000000001,34.994,32.65 -2020-12-07 02:00:00,66.53,125.896,34.571,32.65 -2020-12-07 02:15:00,67.94,127.52,34.571,32.65 -2020-12-07 02:30:00,66.77,128.33700000000002,34.571,32.65 -2020-12-07 02:45:00,67.8,130.205,34.571,32.65 -2020-12-07 03:00:00,68.56,134.14700000000002,33.934,32.65 -2020-12-07 03:15:00,68.92,135.562,33.934,32.65 -2020-12-07 03:30:00,72.78,136.951,33.934,32.65 -2020-12-07 03:45:00,77.58,137.773,33.934,32.65 -2020-12-07 04:00:00,78.96,150.85,34.107,32.65 -2020-12-07 04:15:00,76.3,163.312,34.107,32.65 -2020-12-07 04:30:00,75.15,166.274,34.107,32.65 -2020-12-07 04:45:00,76.91,167.683,34.107,32.65 -2020-12-07 05:00:00,80.43,197.60299999999998,39.575,32.65 -2020-12-07 05:15:00,83.47,227.14,39.575,32.65 -2020-12-07 05:30:00,88.42,224.0,39.575,32.65 -2020-12-07 05:45:00,93.74,215.947,39.575,32.65 -2020-12-07 06:00:00,104.42,213.045,56.156000000000006,32.65 -2020-12-07 06:15:00,111.84,216.856,56.156000000000006,32.65 -2020-12-07 06:30:00,115.43,219.375,56.156000000000006,32.65 -2020-12-07 06:45:00,121.15,222.22299999999998,56.156000000000006,32.65 -2020-12-07 07:00:00,126.05,221.801,67.926,32.65 -2020-12-07 07:15:00,129.31,226.882,67.926,32.65 -2020-12-07 07:30:00,132.06,230.178,67.926,32.65 -2020-12-07 07:45:00,131.74,231.34599999999998,67.926,32.65 -2020-12-07 08:00:00,134.22,230.016,72.58,32.65 -2020-12-07 08:15:00,134.5,231.165,72.58,32.65 -2020-12-07 08:30:00,134.15,229.669,72.58,32.65 -2020-12-07 08:45:00,132.35,227.305,72.58,32.65 -2020-12-07 09:00:00,135.85,221.885,66.984,32.65 -2020-12-07 09:15:00,133.39,216.815,66.984,32.65 -2020-12-07 09:30:00,137.13,213.58900000000003,66.984,32.65 -2020-12-07 09:45:00,137.46,211.10299999999998,66.984,32.65 -2020-12-07 10:00:00,137.29,207.167,63.158,32.65 -2020-12-07 10:15:00,137.05,204.125,63.158,32.65 -2020-12-07 10:30:00,137.9,201.66,63.158,32.65 -2020-12-07 10:45:00,135.05,200.014,63.158,32.65 -2020-12-07 11:00:00,137.29,197.605,61.141000000000005,32.65 -2020-12-07 11:15:00,137.89,196.96099999999998,61.141000000000005,32.65 -2020-12-07 11:30:00,136.52,196.918,61.141000000000005,32.65 -2020-12-07 11:45:00,136.73,194.68599999999998,61.141000000000005,32.65 -2020-12-07 12:00:00,135.1,190.422,57.961000000000006,32.65 -2020-12-07 12:15:00,132.49,189.833,57.961000000000006,32.65 -2020-12-07 12:30:00,133.75,189.05700000000002,57.961000000000006,32.65 -2020-12-07 12:45:00,132.35,190.261,57.961000000000006,32.65 -2020-12-07 13:00:00,131.69,190.002,56.843,32.65 -2020-12-07 13:15:00,130.03,189.96599999999998,56.843,32.65 -2020-12-07 13:30:00,129.19,188.69799999999998,56.843,32.65 -2020-12-07 13:45:00,129.62,188.476,56.843,32.65 -2020-12-07 14:00:00,131.03,187.24900000000002,55.992,32.65 -2020-12-07 14:15:00,133.01,187.574,55.992,32.65 -2020-12-07 14:30:00,132.23,187.301,55.992,32.65 -2020-12-07 14:45:00,130.84,187.215,55.992,32.65 -2020-12-07 15:00:00,132.97,188.435,57.523,32.65 -2020-12-07 15:15:00,132.34,188.688,57.523,32.65 -2020-12-07 15:30:00,133.31,190.551,57.523,32.65 -2020-12-07 15:45:00,133.8,192.769,57.523,32.65 -2020-12-07 16:00:00,136.26,193.521,59.471000000000004,32.65 -2020-12-07 16:15:00,137.17,194.09599999999998,59.471000000000004,32.65 -2020-12-07 16:30:00,133.45,196.149,59.471000000000004,32.65 -2020-12-07 16:45:00,141.66,197.045,59.471000000000004,32.65 -2020-12-07 17:00:00,155.57,199.225,65.066,32.65 -2020-12-07 17:15:00,156.59,199.476,65.066,32.65 -2020-12-07 17:30:00,158.11,199.28400000000002,65.066,32.65 -2020-12-07 17:45:00,143.66,199.03099999999998,65.066,32.65 -2020-12-07 18:00:00,137.35,200.05900000000003,69.581,32.65 -2020-12-07 18:15:00,140.33,198.747,69.581,32.65 -2020-12-07 18:30:00,138.53,197.518,69.581,32.65 -2020-12-07 18:45:00,138.89,196.44299999999998,69.581,32.65 -2020-12-07 19:00:00,138.22,197.331,73.771,32.65 -2020-12-07 19:15:00,133.86,194.03,73.771,32.65 -2020-12-07 19:30:00,132.32,192.628,73.771,32.65 -2020-12-07 19:45:00,130.15,189.213,73.771,32.65 -2020-12-07 20:00:00,125.89,185.597,65.035,32.65 -2020-12-07 20:15:00,120.27,180.40099999999998,65.035,32.65 -2020-12-07 20:30:00,116.83,175.91099999999997,65.035,32.65 -2020-12-07 20:45:00,115.19,173.76,65.035,32.65 -2020-12-07 21:00:00,113.15,172.405,58.7,32.65 -2020-12-07 21:15:00,109.2,169.50599999999997,58.7,32.65 -2020-12-07 21:30:00,106.77,168.132,58.7,32.65 -2020-12-07 21:45:00,102.99,166.173,58.7,32.65 -2020-12-07 22:00:00,100.77,157.714,53.888000000000005,32.65 -2020-12-07 22:15:00,98.17,152.30700000000002,53.888000000000005,32.65 -2020-12-07 22:30:00,96.13,137.769,53.888000000000005,32.65 -2020-12-07 22:45:00,92.55,129.42,53.888000000000005,32.65 -2020-12-07 23:00:00,91.82,123.929,45.501999999999995,32.65 -2020-12-07 23:15:00,89.24,122.87299999999999,45.501999999999995,32.65 -2020-12-07 23:30:00,86.63,123.27600000000001,45.501999999999995,32.65 -2020-12-07 23:45:00,83.86,123.38,45.501999999999995,32.65 -2020-12-08 00:00:00,81.48,116.75399999999999,43.537,32.65 -2020-12-08 00:15:00,80.94,116.822,43.537,32.65 -2020-12-08 00:30:00,78.77,117.975,43.537,32.65 -2020-12-08 00:45:00,79.27,119.594,43.537,32.65 -2020-12-08 01:00:00,79.97,121.589,41.854,32.65 -2020-12-08 01:15:00,75.56,122.323,41.854,32.65 -2020-12-08 01:30:00,74.99,122.73700000000001,41.854,32.65 -2020-12-08 01:45:00,74.98,123.461,41.854,32.65 -2020-12-08 02:00:00,75.24,124.875,40.321,32.65 -2020-12-08 02:15:00,76.79,126.37799999999999,40.321,32.65 -2020-12-08 02:30:00,77.69,126.604,40.321,32.65 -2020-12-08 02:45:00,75.39,128.481,40.321,32.65 -2020-12-08 03:00:00,75.09,131.209,39.632,32.65 -2020-12-08 03:15:00,77.11,131.793,39.632,32.65 -2020-12-08 03:30:00,77.43,133.668,39.632,32.65 -2020-12-08 03:45:00,78.5,134.694,39.632,32.65 -2020-12-08 04:00:00,80.28,147.555,40.183,32.65 -2020-12-08 04:15:00,79.41,159.66299999999998,40.183,32.65 -2020-12-08 04:30:00,80.97,162.30100000000002,40.183,32.65 -2020-12-08 04:45:00,83.79,164.922,40.183,32.65 -2020-12-08 05:00:00,87.76,199.80200000000002,43.945,32.65 -2020-12-08 05:15:00,91.47,229.07,43.945,32.65 -2020-12-08 05:30:00,96.61,224.416,43.945,32.65 -2020-12-08 05:45:00,100.78,216.40099999999998,43.945,32.65 -2020-12-08 06:00:00,109.95,212.322,56.048,32.65 -2020-12-08 06:15:00,114.88,217.763,56.048,32.65 -2020-12-08 06:30:00,120.13,219.68400000000003,56.048,32.65 -2020-12-08 06:45:00,125.43,222.21400000000003,56.048,32.65 -2020-12-08 07:00:00,131.43,221.608,65.74,32.65 -2020-12-08 07:15:00,135.23,226.53900000000002,65.74,32.65 -2020-12-08 07:30:00,137.34,229.292,65.74,32.65 -2020-12-08 07:45:00,140.61,230.69,65.74,32.65 -2020-12-08 08:00:00,140.68,229.48,72.757,32.65 -2020-12-08 08:15:00,141.83,229.604,72.757,32.65 -2020-12-08 08:30:00,140.68,227.933,72.757,32.65 -2020-12-08 08:45:00,140.89,225.28400000000002,72.757,32.65 -2020-12-08 09:00:00,137.03,218.998,67.692,32.65 -2020-12-08 09:15:00,139.39,215.53799999999998,67.692,32.65 -2020-12-08 09:30:00,139.92,212.989,67.692,32.65 -2020-12-08 09:45:00,139.63,210.25900000000001,67.692,32.65 -2020-12-08 10:00:00,141.01,205.74200000000002,63.506,32.65 -2020-12-08 10:15:00,141.07,201.618,63.506,32.65 -2020-12-08 10:30:00,142.01,199.31,63.506,32.65 -2020-12-08 10:45:00,148.52,197.928,63.506,32.65 -2020-12-08 11:00:00,159.29,196.99099999999999,60.758,32.65 -2020-12-08 11:15:00,158.54,195.997,60.758,32.65 -2020-12-08 11:30:00,159.27,194.8,60.758,32.65 -2020-12-08 11:45:00,146.55,193.266,60.758,32.65 -2020-12-08 12:00:00,143.87,187.637,57.519,32.65 -2020-12-08 12:15:00,142.59,186.643,57.519,32.65 -2020-12-08 12:30:00,142.23,186.595,57.519,32.65 -2020-12-08 12:45:00,140.71,187.507,57.519,32.65 -2020-12-08 13:00:00,136.17,186.812,56.46,32.65 -2020-12-08 13:15:00,135.08,186.417,56.46,32.65 -2020-12-08 13:30:00,133.39,186.313,56.46,32.65 -2020-12-08 13:45:00,132.97,186.27599999999998,56.46,32.65 -2020-12-08 14:00:00,133.26,185.308,56.207,32.65 -2020-12-08 14:15:00,134.5,185.797,56.207,32.65 -2020-12-08 14:30:00,134.65,186.173,56.207,32.65 -2020-12-08 14:45:00,134.64,186.024,56.207,32.65 -2020-12-08 15:00:00,135.74,186.833,57.391999999999996,32.65 -2020-12-08 15:15:00,137.12,187.408,57.391999999999996,32.65 -2020-12-08 15:30:00,138.16,189.517,57.391999999999996,32.65 -2020-12-08 15:45:00,139.22,191.333,57.391999999999996,32.65 -2020-12-08 16:00:00,144.76,192.426,59.955,32.65 -2020-12-08 16:15:00,147.35,193.497,59.955,32.65 -2020-12-08 16:30:00,147.5,196.205,59.955,32.65 -2020-12-08 16:45:00,149.58,197.412,59.955,32.65 -2020-12-08 17:00:00,161.96,200.109,67.063,32.65 -2020-12-08 17:15:00,162.06,200.412,67.063,32.65 -2020-12-08 17:30:00,163.06,200.90400000000002,67.063,32.65 -2020-12-08 17:45:00,150.37,200.532,67.063,32.65 -2020-12-08 18:00:00,142.45,201.481,71.477,32.65 -2020-12-08 18:15:00,144.58,199.622,71.477,32.65 -2020-12-08 18:30:00,143.42,198.09599999999998,71.477,32.65 -2020-12-08 18:45:00,143.59,197.825,71.477,32.65 -2020-12-08 19:00:00,141.4,198.78900000000002,74.32,32.65 -2020-12-08 19:15:00,138.68,195.19799999999998,74.32,32.65 -2020-12-08 19:30:00,136.14,193.11599999999999,74.32,32.65 -2020-12-08 19:45:00,135.11,189.708,74.32,32.65 -2020-12-08 20:00:00,130.38,186.248,66.157,32.65 -2020-12-08 20:15:00,125.0,180.418,66.157,32.65 -2020-12-08 20:30:00,121.61,176.912,66.157,32.65 -2020-12-08 20:45:00,119.36,174.207,66.157,32.65 -2020-12-08 21:00:00,116.99,172.13299999999998,59.806000000000004,32.65 -2020-12-08 21:15:00,116.17,170.135,59.806000000000004,32.65 -2020-12-08 21:30:00,118.31,168.03,59.806000000000004,32.65 -2020-12-08 21:45:00,115.49,166.30599999999998,59.806000000000004,32.65 -2020-12-08 22:00:00,106.63,159.56,54.785,32.65 -2020-12-08 22:15:00,103.62,153.899,54.785,32.65 -2020-12-08 22:30:00,104.21,139.499,54.785,32.65 -2020-12-08 22:45:00,103.55,131.433,54.785,32.65 -2020-12-08 23:00:00,100.51,125.949,47.176,32.65 -2020-12-08 23:15:00,93.05,123.975,47.176,32.65 -2020-12-08 23:30:00,89.98,124.031,47.176,32.65 -2020-12-08 23:45:00,88.91,123.681,47.176,32.65 -2020-12-09 00:00:00,91.27,117.072,43.42,32.65 -2020-12-09 00:15:00,91.07,117.11,43.42,32.65 -2020-12-09 00:30:00,87.44,118.256,43.42,32.65 -2020-12-09 00:45:00,84.26,119.85700000000001,43.42,32.65 -2020-12-09 01:00:00,86.4,121.887,40.869,32.65 -2020-12-09 01:15:00,87.29,122.62,40.869,32.65 -2020-12-09 01:30:00,87.03,123.04299999999999,40.869,32.65 -2020-12-09 01:45:00,82.74,123.751,40.869,32.65 -2020-12-09 02:00:00,80.31,125.185,39.541,32.65 -2020-12-09 02:15:00,81.19,126.69,39.541,32.65 -2020-12-09 02:30:00,86.0,126.91799999999999,39.541,32.65 -2020-12-09 02:45:00,86.44,128.79399999999998,39.541,32.65 -2020-12-09 03:00:00,85.55,131.509,39.052,32.65 -2020-12-09 03:15:00,80.17,132.123,39.052,32.65 -2020-12-09 03:30:00,84.96,134.0,39.052,32.65 -2020-12-09 03:45:00,88.74,135.02700000000002,39.052,32.65 -2020-12-09 04:00:00,90.1,147.856,40.36,32.65 -2020-12-09 04:15:00,86.33,159.96,40.36,32.65 -2020-12-09 04:30:00,85.2,162.585,40.36,32.65 -2020-12-09 04:45:00,86.88,165.207,40.36,32.65 -2020-12-09 05:00:00,93.26,200.05,43.133,32.65 -2020-12-09 05:15:00,99.1,229.263,43.133,32.65 -2020-12-09 05:30:00,104.98,224.636,43.133,32.65 -2020-12-09 05:45:00,108.07,216.644,43.133,32.65 -2020-12-09 06:00:00,110.28,212.59,54.953,32.65 -2020-12-09 06:15:00,116.51,218.035,54.953,32.65 -2020-12-09 06:30:00,119.87,220.00099999999998,54.953,32.65 -2020-12-09 06:45:00,127.56,222.575,54.953,32.65 -2020-12-09 07:00:00,130.61,221.97299999999998,66.566,32.65 -2020-12-09 07:15:00,131.47,226.91099999999997,66.566,32.65 -2020-12-09 07:30:00,133.34,229.667,66.566,32.65 -2020-12-09 07:45:00,136.34,231.06599999999997,66.566,32.65 -2020-12-09 08:00:00,138.72,229.86599999999999,72.902,32.65 -2020-12-09 08:15:00,140.05,229.984,72.902,32.65 -2020-12-09 08:30:00,140.81,228.317,72.902,32.65 -2020-12-09 08:45:00,140.14,225.637,72.902,32.65 -2020-12-09 09:00:00,141.66,219.325,68.465,32.65 -2020-12-09 09:15:00,142.55,215.872,68.465,32.65 -2020-12-09 09:30:00,144.01,213.329,68.465,32.65 -2020-12-09 09:45:00,148.03,210.588,68.465,32.65 -2020-12-09 10:00:00,148.43,206.063,63.625,32.65 -2020-12-09 10:15:00,148.69,201.92,63.625,32.65 -2020-12-09 10:30:00,147.3,199.59,63.625,32.65 -2020-12-09 10:45:00,150.11,198.201,63.625,32.65 -2020-12-09 11:00:00,165.23,197.245,61.628,32.65 -2020-12-09 11:15:00,167.64,196.24,61.628,32.65 -2020-12-09 11:30:00,171.35,195.041,61.628,32.65 -2020-12-09 11:45:00,158.93,193.50099999999998,61.628,32.65 -2020-12-09 12:00:00,150.69,187.872,58.708999999999996,32.65 -2020-12-09 12:15:00,147.05,186.891,58.708999999999996,32.65 -2020-12-09 12:30:00,147.05,186.858,58.708999999999996,32.65 -2020-12-09 12:45:00,144.87,187.774,58.708999999999996,32.65 -2020-12-09 13:00:00,143.99,187.047,57.373000000000005,32.65 -2020-12-09 13:15:00,143.5,186.655,57.373000000000005,32.65 -2020-12-09 13:30:00,140.33,186.545,57.373000000000005,32.65 -2020-12-09 13:45:00,139.48,186.498,57.373000000000005,32.65 -2020-12-09 14:00:00,139.53,185.511,57.684,32.65 -2020-12-09 14:15:00,138.98,186.005,57.684,32.65 -2020-12-09 14:30:00,138.49,186.405,57.684,32.65 -2020-12-09 14:45:00,137.14,186.267,57.684,32.65 -2020-12-09 15:00:00,135.82,187.095,58.03,32.65 -2020-12-09 15:15:00,135.39,187.666,58.03,32.65 -2020-12-09 15:30:00,138.38,189.798,58.03,32.65 -2020-12-09 15:45:00,138.54,191.613,58.03,32.65 -2020-12-09 16:00:00,139.67,192.706,59.97,32.65 -2020-12-09 16:15:00,145.31,193.799,59.97,32.65 -2020-12-09 16:30:00,147.26,196.513,59.97,32.65 -2020-12-09 16:45:00,151.24,197.75099999999998,59.97,32.65 -2020-12-09 17:00:00,169.05,200.424,65.661,32.65 -2020-12-09 17:15:00,169.17,200.75400000000002,65.661,32.65 -2020-12-09 17:30:00,170.79,201.261,65.661,32.65 -2020-12-09 17:45:00,149.97,200.899,65.661,32.65 -2020-12-09 18:00:00,141.53,201.86599999999999,70.96300000000001,32.65 -2020-12-09 18:15:00,143.4,199.97400000000002,70.96300000000001,32.65 -2020-12-09 18:30:00,142.84,198.456,70.96300000000001,32.65 -2020-12-09 18:45:00,143.61,198.19400000000002,70.96300000000001,32.65 -2020-12-09 19:00:00,142.88,199.144,74.133,32.65 -2020-12-09 19:15:00,140.01,195.545,74.133,32.65 -2020-12-09 19:30:00,138.02,193.44799999999998,74.133,32.65 -2020-12-09 19:45:00,135.99,190.014,74.133,32.65 -2020-12-09 20:00:00,129.82,186.553,65.613,32.65 -2020-12-09 20:15:00,124.78,180.71400000000003,65.613,32.65 -2020-12-09 20:30:00,122.6,177.18400000000003,65.613,32.65 -2020-12-09 20:45:00,120.04,174.5,65.613,32.65 -2020-12-09 21:00:00,116.74,172.41,58.583,32.65 -2020-12-09 21:15:00,113.73,170.396,58.583,32.65 -2020-12-09 21:30:00,111.56,168.293,58.583,32.65 -2020-12-09 21:45:00,112.32,166.574,58.583,32.65 -2020-12-09 22:00:00,111.72,159.83700000000002,54.411,32.65 -2020-12-09 22:15:00,110.88,154.179,54.411,32.65 -2020-12-09 22:30:00,104.66,139.827,54.411,32.65 -2020-12-09 22:45:00,99.54,131.77,54.411,32.65 -2020-12-09 23:00:00,93.47,126.26100000000001,47.878,32.65 -2020-12-09 23:15:00,92.23,124.285,47.878,32.65 -2020-12-09 23:30:00,90.56,124.353,47.878,32.65 -2020-12-09 23:45:00,89.81,123.98700000000001,47.878,32.65 -2020-12-10 00:00:00,92.49,117.383,44.513000000000005,32.65 -2020-12-10 00:15:00,87.7,117.391,44.513000000000005,32.65 -2020-12-10 00:30:00,90.23,118.53200000000001,44.513000000000005,32.65 -2020-12-10 00:45:00,85.04,120.113,44.513000000000005,32.65 -2020-12-10 01:00:00,82.13,122.18,43.169,32.65 -2020-12-10 01:15:00,80.96,122.911,43.169,32.65 -2020-12-10 01:30:00,85.8,123.34,43.169,32.65 -2020-12-10 01:45:00,87.1,124.03299999999999,43.169,32.65 -2020-12-10 02:00:00,87.01,125.48899999999999,41.763999999999996,32.65 -2020-12-10 02:15:00,82.06,126.995,41.763999999999996,32.65 -2020-12-10 02:30:00,85.45,127.22399999999999,41.763999999999996,32.65 -2020-12-10 02:45:00,87.43,129.1,41.763999999999996,32.65 -2020-12-10 03:00:00,87.6,131.804,41.155,32.65 -2020-12-10 03:15:00,83.72,132.446,41.155,32.65 -2020-12-10 03:30:00,87.63,134.326,41.155,32.65 -2020-12-10 03:45:00,90.33,135.352,41.155,32.65 -2020-12-10 04:00:00,91.8,148.15,41.96,32.65 -2020-12-10 04:15:00,88.57,160.25,41.96,32.65 -2020-12-10 04:30:00,87.86,162.861,41.96,32.65 -2020-12-10 04:45:00,87.24,165.486,41.96,32.65 -2020-12-10 05:00:00,94.72,200.29,45.206,32.65 -2020-12-10 05:15:00,98.25,229.452,45.206,32.65 -2020-12-10 05:30:00,98.01,224.84900000000002,45.206,32.65 -2020-12-10 05:45:00,102.63,216.87900000000002,45.206,32.65 -2020-12-10 06:00:00,109.41,212.851,55.398999999999994,32.65 -2020-12-10 06:15:00,116.05,218.298,55.398999999999994,32.65 -2020-12-10 06:30:00,120.99,220.30900000000003,55.398999999999994,32.65 -2020-12-10 06:45:00,125.66,222.928,55.398999999999994,32.65 -2020-12-10 07:00:00,129.75,222.33,64.627,32.65 -2020-12-10 07:15:00,134.39,227.273,64.627,32.65 -2020-12-10 07:30:00,136.07,230.03400000000002,64.627,32.65 -2020-12-10 07:45:00,137.41,231.43200000000002,64.627,32.65 -2020-12-10 08:00:00,138.31,230.24200000000002,70.895,32.65 -2020-12-10 08:15:00,138.27,230.352,70.895,32.65 -2020-12-10 08:30:00,139.4,228.68900000000002,70.895,32.65 -2020-12-10 08:45:00,139.75,225.979,70.895,32.65 -2020-12-10 09:00:00,140.06,219.641,66.382,32.65 -2020-12-10 09:15:00,143.71,216.195,66.382,32.65 -2020-12-10 09:30:00,145.38,213.66,66.382,32.65 -2020-12-10 09:45:00,143.55,210.907,66.382,32.65 -2020-12-10 10:00:00,144.96,206.37400000000002,62.739,32.65 -2020-12-10 10:15:00,143.67,202.21099999999998,62.739,32.65 -2020-12-10 10:30:00,142.44,199.86,62.739,32.65 -2020-12-10 10:45:00,142.02,198.46400000000003,62.739,32.65 -2020-12-10 11:00:00,140.23,197.49099999999999,60.843,32.65 -2020-12-10 11:15:00,141.63,196.47299999999998,60.843,32.65 -2020-12-10 11:30:00,140.83,195.273,60.843,32.65 -2020-12-10 11:45:00,140.68,193.729,60.843,32.65 -2020-12-10 12:00:00,140.94,188.09900000000002,58.466,32.65 -2020-12-10 12:15:00,140.46,187.13,58.466,32.65 -2020-12-10 12:30:00,138.97,187.112,58.466,32.65 -2020-12-10 12:45:00,136.01,188.033,58.466,32.65 -2020-12-10 13:00:00,137.69,187.275,56.883,32.65 -2020-12-10 13:15:00,137.48,186.88400000000001,56.883,32.65 -2020-12-10 13:30:00,132.54,186.77,56.883,32.65 -2020-12-10 13:45:00,133.09,186.713,56.883,32.65 -2020-12-10 14:00:00,135.21,185.706,56.503,32.65 -2020-12-10 14:15:00,136.53,186.206,56.503,32.65 -2020-12-10 14:30:00,137.12,186.63,56.503,32.65 -2020-12-10 14:45:00,137.05,186.502,56.503,32.65 -2020-12-10 15:00:00,137.96,187.34900000000002,57.803999999999995,32.65 -2020-12-10 15:15:00,139.18,187.918,57.803999999999995,32.65 -2020-12-10 15:30:00,136.67,190.06900000000002,57.803999999999995,32.65 -2020-12-10 15:45:00,137.64,191.88400000000001,57.803999999999995,32.65 -2020-12-10 16:00:00,138.45,192.976,59.379,32.65 -2020-12-10 16:15:00,142.96,194.09,59.379,32.65 -2020-12-10 16:30:00,146.32,196.812,59.379,32.65 -2020-12-10 16:45:00,153.38,198.078,59.379,32.65 -2020-12-10 17:00:00,168.95,200.729,64.71600000000001,32.65 -2020-12-10 17:15:00,169.75,201.08599999999998,64.71600000000001,32.65 -2020-12-10 17:30:00,169.32,201.61,64.71600000000001,32.65 -2020-12-10 17:45:00,147.93,201.25599999999997,64.71600000000001,32.65 -2020-12-10 18:00:00,141.13,202.24400000000003,68.803,32.65 -2020-12-10 18:15:00,143.26,200.32,68.803,32.65 -2020-12-10 18:30:00,143.39,198.808,68.803,32.65 -2020-12-10 18:45:00,144.16,198.555,68.803,32.65 -2020-12-10 19:00:00,142.57,199.49099999999999,72.934,32.65 -2020-12-10 19:15:00,140.71,195.882,72.934,32.65 -2020-12-10 19:30:00,137.64,193.773,72.934,32.65 -2020-12-10 19:45:00,135.04,190.31400000000002,72.934,32.65 -2020-12-10 20:00:00,132.42,186.852,65.175,32.65 -2020-12-10 20:15:00,126.79,181.005,65.175,32.65 -2020-12-10 20:30:00,122.81,177.45,65.175,32.65 -2020-12-10 20:45:00,120.88,174.785,65.175,32.65 -2020-12-10 21:00:00,117.89,172.68099999999998,58.55,32.65 -2020-12-10 21:15:00,115.42,170.65,58.55,32.65 -2020-12-10 21:30:00,118.82,168.548,58.55,32.65 -2020-12-10 21:45:00,116.05,166.838,58.55,32.65 -2020-12-10 22:00:00,113.48,160.107,55.041000000000004,32.65 -2020-12-10 22:15:00,110.17,154.452,55.041000000000004,32.65 -2020-12-10 22:30:00,99.6,140.149,55.041000000000004,32.65 -2020-12-10 22:45:00,100.62,132.09799999999998,55.041000000000004,32.65 -2020-12-10 23:00:00,94.51,126.565,48.258,32.65 -2020-12-10 23:15:00,91.88,124.586,48.258,32.65 -2020-12-10 23:30:00,89.58,124.66799999999999,48.258,32.65 -2020-12-10 23:45:00,91.27,124.288,48.258,32.65 -2020-12-11 00:00:00,91.85,116.62899999999999,45.02,32.65 -2020-12-11 00:15:00,90.83,116.805,45.02,32.65 -2020-12-11 00:30:00,85.32,117.803,45.02,32.65 -2020-12-11 00:45:00,82.24,119.473,45.02,32.65 -2020-12-11 01:00:00,79.17,121.26299999999999,42.695,32.65 -2020-12-11 01:15:00,81.0,122.91799999999999,42.695,32.65 -2020-12-11 01:30:00,77.89,123.124,42.695,32.65 -2020-12-11 01:45:00,78.18,123.911,42.695,32.65 -2020-12-11 02:00:00,81.93,125.464,41.511,32.65 -2020-12-11 02:15:00,86.0,126.85700000000001,41.511,32.65 -2020-12-11 02:30:00,86.34,127.60600000000001,41.511,32.65 -2020-12-11 02:45:00,81.69,129.531,41.511,32.65 -2020-12-11 03:00:00,81.72,131.216,41.162,32.65 -2020-12-11 03:15:00,86.02,132.842,41.162,32.65 -2020-12-11 03:30:00,88.54,134.709,41.162,32.65 -2020-12-11 03:45:00,89.73,136.055,41.162,32.65 -2020-12-11 04:00:00,84.52,149.05200000000002,42.226000000000006,32.65 -2020-12-11 04:15:00,82.83,160.917,42.226000000000006,32.65 -2020-12-11 04:30:00,84.73,163.737,42.226000000000006,32.65 -2020-12-11 04:45:00,86.99,165.222,42.226000000000006,32.65 -2020-12-11 05:00:00,89.74,198.715,45.597,32.65 -2020-12-11 05:15:00,93.03,229.33,45.597,32.65 -2020-12-11 05:30:00,96.79,225.83599999999998,45.597,32.65 -2020-12-11 05:45:00,101.33,217.828,45.597,32.65 -2020-12-11 06:00:00,110.09,214.25900000000001,56.263999999999996,32.65 -2020-12-11 06:15:00,118.24,218.227,56.263999999999996,32.65 -2020-12-11 06:30:00,122.75,219.41400000000002,56.263999999999996,32.65 -2020-12-11 06:45:00,127.63,223.713,56.263999999999996,32.65 -2020-12-11 07:00:00,132.56,222.27200000000002,66.888,32.65 -2020-12-11 07:15:00,135.59,228.239,66.888,32.65 -2020-12-11 07:30:00,138.93,230.83900000000003,66.888,32.65 -2020-12-11 07:45:00,140.11,231.35,66.888,32.65 -2020-12-11 08:00:00,140.74,229.05599999999998,73.459,32.65 -2020-12-11 08:15:00,138.35,228.766,73.459,32.65 -2020-12-11 08:30:00,138.94,228.084,73.459,32.65 -2020-12-11 08:45:00,138.68,223.765,73.459,32.65 -2020-12-11 09:00:00,138.76,217.87099999999998,69.087,32.65 -2020-12-11 09:15:00,142.43,214.997,69.087,32.65 -2020-12-11 09:30:00,145.27,212.041,69.087,32.65 -2020-12-11 09:45:00,145.82,209.167,69.087,32.65 -2020-12-11 10:00:00,146.67,203.49599999999998,65.404,32.65 -2020-12-11 10:15:00,146.04,200.011,65.404,32.65 -2020-12-11 10:30:00,146.52,197.567,65.404,32.65 -2020-12-11 10:45:00,146.39,195.722,65.404,32.65 -2020-12-11 11:00:00,144.72,194.715,63.0,32.65 -2020-12-11 11:15:00,144.28,192.791,63.0,32.65 -2020-12-11 11:30:00,145.11,193.333,63.0,32.65 -2020-12-11 11:45:00,143.67,191.829,63.0,32.65 -2020-12-11 12:00:00,143.59,187.28,59.083,32.65 -2020-12-11 12:15:00,143.45,184.239,59.083,32.65 -2020-12-11 12:30:00,142.82,184.391,59.083,32.65 -2020-12-11 12:45:00,140.7,185.827,59.083,32.65 -2020-12-11 13:00:00,139.92,185.97299999999998,56.611999999999995,32.65 -2020-12-11 13:15:00,139.16,186.396,56.611999999999995,32.65 -2020-12-11 13:30:00,136.21,186.297,56.611999999999995,32.65 -2020-12-11 13:45:00,136.82,186.169,56.611999999999995,32.65 -2020-12-11 14:00:00,134.19,184.005,55.161,32.65 -2020-12-11 14:15:00,134.68,184.335,55.161,32.65 -2020-12-11 14:30:00,134.81,185.299,55.161,32.65 -2020-12-11 14:45:00,133.75,185.48,55.161,32.65 -2020-12-11 15:00:00,133.3,185.865,55.583,32.65 -2020-12-11 15:15:00,132.89,185.993,55.583,32.65 -2020-12-11 15:30:00,133.49,186.65400000000002,55.583,32.65 -2020-12-11 15:45:00,133.98,188.61599999999999,55.583,32.65 -2020-12-11 16:00:00,135.78,188.544,57.611999999999995,32.65 -2020-12-11 16:15:00,139.22,189.968,57.611999999999995,32.65 -2020-12-11 16:30:00,141.38,192.791,57.611999999999995,32.65 -2020-12-11 16:45:00,141.75,193.968,57.611999999999995,32.65 -2020-12-11 17:00:00,142.8,196.8,64.14,32.65 -2020-12-11 17:15:00,142.37,196.774,64.14,32.65 -2020-12-11 17:30:00,142.8,196.99900000000002,64.14,32.65 -2020-12-11 17:45:00,141.37,196.421,64.14,32.65 -2020-12-11 18:00:00,139.86,198.11599999999999,68.086,32.65 -2020-12-11 18:15:00,137.98,195.77200000000002,68.086,32.65 -2020-12-11 18:30:00,137.12,194.65900000000002,68.086,32.65 -2020-12-11 18:45:00,137.09,194.41099999999997,68.086,32.65 -2020-12-11 19:00:00,135.72,196.236,69.915,32.65 -2020-12-11 19:15:00,133.39,193.97799999999998,69.915,32.65 -2020-12-11 19:30:00,131.85,191.445,69.915,32.65 -2020-12-11 19:45:00,128.74,187.503,69.915,32.65 -2020-12-11 20:00:00,123.92,184.093,61.695,32.65 -2020-12-11 20:15:00,119.13,178.245,61.695,32.65 -2020-12-11 20:30:00,116.21,174.625,61.695,32.65 -2020-12-11 20:45:00,114.15,172.53400000000002,61.695,32.65 -2020-12-11 21:00:00,114.45,170.922,56.041000000000004,32.65 -2020-12-11 21:15:00,112.95,169.31,56.041000000000004,32.65 -2020-12-11 21:30:00,111.79,167.25400000000002,56.041000000000004,32.65 -2020-12-11 21:45:00,111.52,166.093,56.041000000000004,32.65 -2020-12-11 22:00:00,100.51,160.34799999999998,51.888999999999996,32.65 -2020-12-11 22:15:00,97.98,154.564,51.888999999999996,32.65 -2020-12-11 22:30:00,92.39,146.664,51.888999999999996,32.65 -2020-12-11 22:45:00,92.97,142.139,51.888999999999996,32.65 -2020-12-11 23:00:00,94.85,136.137,45.787,32.65 -2020-12-11 23:15:00,93.72,132.213,45.787,32.65 -2020-12-11 23:30:00,88.87,130.852,45.787,32.65 -2020-12-11 23:45:00,84.74,129.795,45.787,32.65 -2020-12-12 00:00:00,84.97,114.07600000000001,41.815,32.468 -2020-12-12 00:15:00,83.87,109.97399999999999,41.815,32.468 -2020-12-12 00:30:00,79.35,112.294,41.815,32.468 -2020-12-12 00:45:00,76.4,114.661,41.815,32.468 -2020-12-12 01:00:00,78.93,117.113,38.645,32.468 -2020-12-12 01:15:00,79.22,117.779,38.645,32.468 -2020-12-12 01:30:00,74.91,117.477,38.645,32.468 -2020-12-12 01:45:00,70.19,118.03,38.645,32.468 -2020-12-12 02:00:00,69.07,120.29,36.696,32.468 -2020-12-12 02:15:00,68.62,121.31700000000001,36.696,32.468 -2020-12-12 02:30:00,76.58,120.963,36.696,32.468 -2020-12-12 02:45:00,71.16,122.986,36.696,32.468 -2020-12-12 03:00:00,72.83,125.289,35.42,32.468 -2020-12-12 03:15:00,75.73,125.73200000000001,35.42,32.468 -2020-12-12 03:30:00,75.68,126.01299999999999,35.42,32.468 -2020-12-12 03:45:00,71.18,127.463,35.42,32.468 -2020-12-12 04:00:00,69.45,136.298,35.167,32.468 -2020-12-12 04:15:00,68.25,145.624,35.167,32.468 -2020-12-12 04:30:00,69.44,146.27200000000002,35.167,32.468 -2020-12-12 04:45:00,69.89,147.263,35.167,32.468 -2020-12-12 05:00:00,71.14,164.803,35.311,32.468 -2020-12-12 05:15:00,71.23,176.438,35.311,32.468 -2020-12-12 05:30:00,71.29,173.31400000000002,35.311,32.468 -2020-12-12 05:45:00,72.91,170.747,35.311,32.468 -2020-12-12 06:00:00,74.72,185.908,37.117,32.468 -2020-12-12 06:15:00,76.39,206.178,37.117,32.468 -2020-12-12 06:30:00,78.26,202.05200000000002,37.117,32.468 -2020-12-12 06:45:00,80.93,197.43599999999998,37.117,32.468 -2020-12-12 07:00:00,84.33,192.245,40.948,32.468 -2020-12-12 07:15:00,86.73,196.97,40.948,32.468 -2020-12-12 07:30:00,90.02,202.268,40.948,32.468 -2020-12-12 07:45:00,93.49,206.74400000000003,40.948,32.468 -2020-12-12 08:00:00,95.85,208.5,44.903,32.468 -2020-12-12 08:15:00,97.55,211.82299999999998,44.903,32.468 -2020-12-12 08:30:00,99.61,212.753,44.903,32.468 -2020-12-12 08:45:00,103.07,211.549,44.903,32.468 -2020-12-12 09:00:00,104.78,207.393,46.283,32.468 -2020-12-12 09:15:00,105.94,205.275,46.283,32.468 -2020-12-12 09:30:00,106.91,203.227,46.283,32.468 -2020-12-12 09:45:00,107.66,200.5,46.283,32.468 -2020-12-12 10:00:00,108.32,195.109,44.103,32.468 -2020-12-12 10:15:00,109.17,191.769,44.103,32.468 -2020-12-12 10:30:00,109.72,189.468,44.103,32.468 -2020-12-12 10:45:00,110.26,188.91,44.103,32.468 -2020-12-12 11:00:00,111.78,188.105,42.373999999999995,32.468 -2020-12-12 11:15:00,113.31,185.49,42.373999999999995,32.468 -2020-12-12 11:30:00,113.65,184.924,42.373999999999995,32.468 -2020-12-12 11:45:00,113.32,182.46900000000002,42.373999999999995,32.468 -2020-12-12 12:00:00,111.9,177.05200000000002,39.937,32.468 -2020-12-12 12:15:00,111.01,174.662,39.937,32.468 -2020-12-12 12:30:00,108.83,175.142,39.937,32.468 -2020-12-12 12:45:00,106.63,175.82299999999998,39.937,32.468 -2020-12-12 13:00:00,103.27,175.53900000000002,37.138000000000005,32.468 -2020-12-12 13:15:00,101.79,173.928,37.138000000000005,32.468 -2020-12-12 13:30:00,101.3,173.377,37.138000000000005,32.468 -2020-12-12 13:45:00,101.1,173.704,37.138000000000005,32.468 -2020-12-12 14:00:00,100.88,172.74400000000003,36.141999999999996,32.468 -2020-12-12 14:15:00,100.41,172.542,36.141999999999996,32.468 -2020-12-12 14:30:00,100.19,171.72799999999998,36.141999999999996,32.468 -2020-12-12 14:45:00,99.5,172.138,36.141999999999996,32.468 -2020-12-12 15:00:00,98.71,173.196,37.964,32.468 -2020-12-12 15:15:00,98.98,174.12,37.964,32.468 -2020-12-12 15:30:00,99.94,176.325,37.964,32.468 -2020-12-12 15:45:00,100.8,178.297,37.964,32.468 -2020-12-12 16:00:00,104.51,177.041,40.699,32.468 -2020-12-12 16:15:00,107.74,179.368,40.699,32.468 -2020-12-12 16:30:00,108.74,182.148,40.699,32.468 -2020-12-12 16:45:00,109.29,184.231,40.699,32.468 -2020-12-12 17:00:00,110.17,186.467,46.216,32.468 -2020-12-12 17:15:00,110.88,188.104,46.216,32.468 -2020-12-12 17:30:00,111.64,188.24900000000002,46.216,32.468 -2020-12-12 17:45:00,112.51,187.28099999999998,46.216,32.468 -2020-12-12 18:00:00,112.37,188.514,51.123999999999995,32.468 -2020-12-12 18:15:00,112.02,187.929,51.123999999999995,32.468 -2020-12-12 18:30:00,111.33,188.148,51.123999999999995,32.468 -2020-12-12 18:45:00,111.02,184.58,51.123999999999995,32.468 -2020-12-12 19:00:00,110.03,187.303,52.336000000000006,32.468 -2020-12-12 19:15:00,108.32,184.541,52.336000000000006,32.468 -2020-12-12 19:30:00,107.24,182.74400000000003,52.336000000000006,32.468 -2020-12-12 19:45:00,106.61,178.607,52.336000000000006,32.468 -2020-12-12 20:00:00,102.62,177.35299999999998,48.825,32.468 -2020-12-12 20:15:00,97.95,173.604,48.825,32.468 -2020-12-12 20:30:00,96.03,169.61,48.825,32.468 -2020-12-12 20:45:00,94.62,167.145,48.825,32.468 -2020-12-12 21:00:00,92.77,167.74599999999998,43.729,32.468 -2020-12-12 21:15:00,90.0,166.553,43.729,32.468 -2020-12-12 21:30:00,87.78,165.718,43.729,32.468 -2020-12-12 21:45:00,87.21,164.16,43.729,32.468 -2020-12-12 22:00:00,86.33,159.75799999999998,44.126000000000005,32.468 -2020-12-12 22:15:00,84.41,156.444,44.126000000000005,32.468 -2020-12-12 22:30:00,82.37,154.756,44.126000000000005,32.468 -2020-12-12 22:45:00,80.33,152.092,44.126000000000005,32.468 -2020-12-12 23:00:00,76.41,148.47299999999998,38.169000000000004,32.468 -2020-12-12 23:15:00,74.99,142.928,38.169000000000004,32.468 -2020-12-12 23:30:00,72.58,139.857,38.169000000000004,32.468 -2020-12-12 23:45:00,70.05,136.382,38.169000000000004,32.468 -2020-12-13 00:00:00,67.83,114.786,35.232,32.468 -2020-12-13 00:15:00,65.77,110.337,35.232,32.468 -2020-12-13 00:30:00,64.76,112.277,35.232,32.468 -2020-12-13 00:45:00,63.83,115.316,35.232,32.468 -2020-12-13 01:00:00,62.51,117.65799999999999,31.403000000000002,32.468 -2020-12-13 01:15:00,61.48,119.354,31.403000000000002,32.468 -2020-12-13 01:30:00,61.2,119.575,31.403000000000002,32.468 -2020-12-13 01:45:00,60.78,119.804,31.403000000000002,32.468 -2020-12-13 02:00:00,59.95,121.339,30.69,32.468 -2020-12-13 02:15:00,59.57,121.521,30.69,32.468 -2020-12-13 02:30:00,59.18,122.01899999999999,30.69,32.468 -2020-12-13 02:45:00,59.27,124.5,30.69,32.468 -2020-12-13 03:00:00,59.3,127.10700000000001,29.516,32.468 -2020-12-13 03:15:00,59.76,127.05799999999999,29.516,32.468 -2020-12-13 03:30:00,60.59,128.73,29.516,32.468 -2020-12-13 03:45:00,60.29,130.10399999999998,29.516,32.468 -2020-12-13 04:00:00,59.87,138.665,29.148000000000003,32.468 -2020-12-13 04:15:00,59.82,146.981,29.148000000000003,32.468 -2020-12-13 04:30:00,60.61,147.683,29.148000000000003,32.468 -2020-12-13 04:45:00,60.83,148.946,29.148000000000003,32.468 -2020-12-13 05:00:00,61.5,162.963,28.706,32.468 -2020-12-13 05:15:00,62.25,172.148,28.706,32.468 -2020-12-13 05:30:00,63.05,168.88299999999998,28.706,32.468 -2020-12-13 05:45:00,63.76,166.579,28.706,32.468 -2020-12-13 06:00:00,64.65,181.65599999999998,28.771,32.468 -2020-12-13 06:15:00,65.56,200.222,28.771,32.468 -2020-12-13 06:30:00,65.84,195.041,28.771,32.468 -2020-12-13 06:45:00,67.8,189.417,28.771,32.468 -2020-12-13 07:00:00,69.8,186.665,31.39,32.468 -2020-12-13 07:15:00,72.1,190.542,31.39,32.468 -2020-12-13 07:30:00,74.71,194.625,31.39,32.468 -2020-12-13 07:45:00,76.75,198.34400000000002,31.39,32.468 -2020-12-13 08:00:00,78.86,201.938,34.972,32.468 -2020-12-13 08:15:00,81.37,205.171,34.972,32.468 -2020-12-13 08:30:00,82.52,207.74599999999998,34.972,32.468 -2020-12-13 08:45:00,84.69,208.512,34.972,32.468 -2020-12-13 09:00:00,85.83,203.933,36.709,32.468 -2020-12-13 09:15:00,87.39,202.365,36.709,32.468 -2020-12-13 09:30:00,88.29,200.173,36.709,32.468 -2020-12-13 09:45:00,89.37,197.327,36.709,32.468 -2020-12-13 10:00:00,91.42,194.422,35.812,32.468 -2020-12-13 10:15:00,92.02,191.58900000000003,35.812,32.468 -2020-12-13 10:30:00,93.21,189.86599999999999,35.812,32.468 -2020-12-13 10:45:00,95.56,187.44,35.812,32.468 -2020-12-13 11:00:00,97.35,187.52,36.746,32.468 -2020-12-13 11:15:00,99.58,185.02,36.746,32.468 -2020-12-13 11:30:00,99.04,183.602,36.746,32.468 -2020-12-13 11:45:00,101.44,181.74200000000002,36.746,32.468 -2020-12-13 12:00:00,102.29,175.783,35.048,32.468 -2020-12-13 12:15:00,100.68,175.27200000000002,35.048,32.468 -2020-12-13 12:30:00,97.69,174.334,35.048,32.468 -2020-12-13 12:45:00,93.5,174.06099999999998,35.048,32.468 -2020-12-13 13:00:00,93.18,173.04,29.987,32.468 -2020-12-13 13:15:00,91.25,174.38400000000001,29.987,32.468 -2020-12-13 13:30:00,90.27,173.607,29.987,32.468 -2020-12-13 13:45:00,90.93,173.293,29.987,32.468 -2020-12-13 14:00:00,91.2,172.57299999999998,27.21,32.468 -2020-12-13 14:15:00,91.53,173.56,27.21,32.468 -2020-12-13 14:30:00,92.77,173.96400000000003,27.21,32.468 -2020-12-13 14:45:00,92.68,173.96599999999998,27.21,32.468 -2020-12-13 15:00:00,92.95,173.54,27.726999999999997,32.468 -2020-12-13 15:15:00,93.32,175.192,27.726999999999997,32.468 -2020-12-13 15:30:00,93.65,177.99,27.726999999999997,32.468 -2020-12-13 15:45:00,94.82,180.645,27.726999999999997,32.468 -2020-12-13 16:00:00,97.84,181.21,32.23,32.468 -2020-12-13 16:15:00,102.34,182.666,32.23,32.468 -2020-12-13 16:30:00,104.88,185.718,32.23,32.468 -2020-12-13 16:45:00,106.38,187.96200000000002,32.23,32.468 -2020-12-13 17:00:00,108.65,190.179,42.016999999999996,32.468 -2020-12-13 17:15:00,110.19,191.53400000000002,42.016999999999996,32.468 -2020-12-13 17:30:00,112.07,191.988,42.016999999999996,32.468 -2020-12-13 17:45:00,113.2,193.269,42.016999999999996,32.468 -2020-12-13 18:00:00,112.55,193.99099999999999,49.338,32.468 -2020-12-13 18:15:00,110.56,194.68200000000002,49.338,32.468 -2020-12-13 18:30:00,110.25,192.84799999999998,49.338,32.468 -2020-12-13 18:45:00,110.1,191.11900000000003,49.338,32.468 -2020-12-13 19:00:00,108.26,193.512,52.369,32.468 -2020-12-13 19:15:00,106.2,191.321,52.369,32.468 -2020-12-13 19:30:00,105.15,189.34799999999998,52.369,32.468 -2020-12-13 19:45:00,104.42,186.611,52.369,32.468 -2020-12-13 20:00:00,108.33,185.328,50.405,32.468 -2020-12-13 20:15:00,108.05,182.543,50.405,32.468 -2020-12-13 20:30:00,103.92,179.755,50.405,32.468 -2020-12-13 20:45:00,96.6,176.112,50.405,32.468 -2020-12-13 21:00:00,98.84,174.137,46.235,32.468 -2020-12-13 21:15:00,101.2,172.30700000000002,46.235,32.468 -2020-12-13 21:30:00,100.09,171.757,46.235,32.468 -2020-12-13 21:45:00,99.54,170.34599999999998,46.235,32.468 -2020-12-13 22:00:00,91.06,164.78900000000002,46.861000000000004,32.468 -2020-12-13 22:15:00,90.94,160.716,46.861000000000004,32.468 -2020-12-13 22:30:00,85.27,155.94,46.861000000000004,32.468 -2020-12-13 22:45:00,86.11,152.435,46.861000000000004,32.468 -2020-12-13 23:00:00,87.91,146.025,41.302,32.468 -2020-12-13 23:15:00,88.21,142.32299999999998,41.302,32.468 -2020-12-13 23:30:00,84.64,140.062,41.302,32.468 -2020-12-13 23:45:00,77.19,137.455,41.302,32.468 -2020-12-14 00:00:00,73.85,119.242,37.164,32.65 -2020-12-14 00:15:00,76.66,117.71700000000001,37.164,32.65 -2020-12-14 00:30:00,78.78,119.77600000000001,37.164,32.65 -2020-12-14 00:45:00,77.4,122.26100000000001,37.164,32.65 -2020-12-14 01:00:00,68.45,124.65100000000001,34.994,32.65 -2020-12-14 01:15:00,73.71,125.82,34.994,32.65 -2020-12-14 01:30:00,75.05,126.103,34.994,32.65 -2020-12-14 01:45:00,74.17,126.43,34.994,32.65 -2020-12-14 02:00:00,71.84,127.964,34.571,32.65 -2020-12-14 02:15:00,74.01,129.599,34.571,32.65 -2020-12-14 02:30:00,74.73,130.429,34.571,32.65 -2020-12-14 02:45:00,74.65,132.29399999999998,34.571,32.65 -2020-12-14 03:00:00,70.83,136.15200000000002,33.934,32.65 -2020-12-14 03:15:00,76.74,137.769,33.934,32.65 -2020-12-14 03:30:00,78.07,139.173,33.934,32.65 -2020-12-14 03:45:00,77.08,139.999,33.934,32.65 -2020-12-14 04:00:00,73.01,152.859,34.107,32.65 -2020-12-14 04:15:00,78.81,165.28900000000002,34.107,32.65 -2020-12-14 04:30:00,80.92,168.162,34.107,32.65 -2020-12-14 04:45:00,82.24,169.58599999999998,34.107,32.65 -2020-12-14 05:00:00,82.67,199.233,39.575,32.65 -2020-12-14 05:15:00,82.48,228.407,39.575,32.65 -2020-12-14 05:30:00,87.89,225.44099999999997,39.575,32.65 -2020-12-14 05:45:00,93.59,217.542,39.575,32.65 -2020-12-14 06:00:00,104.33,214.821,56.156000000000006,32.65 -2020-12-14 06:15:00,110.84,218.653,56.156000000000006,32.65 -2020-12-14 06:30:00,116.28,221.476,56.156000000000006,32.65 -2020-12-14 06:45:00,125.61,224.635,56.156000000000006,32.65 -2020-12-14 07:00:00,128.28,224.24200000000002,67.926,32.65 -2020-12-14 07:15:00,129.58,229.359,67.926,32.65 -2020-12-14 07:30:00,133.95,232.672,67.926,32.65 -2020-12-14 07:45:00,132.49,233.838,67.926,32.65 -2020-12-14 08:00:00,136.16,232.574,72.58,32.65 -2020-12-14 08:15:00,133.51,233.67,72.58,32.65 -2020-12-14 08:30:00,136.78,232.19,72.58,32.65 -2020-12-14 08:45:00,132.47,229.61599999999999,72.58,32.65 -2020-12-14 09:00:00,134.5,224.016,66.984,32.65 -2020-12-14 09:15:00,136.6,218.998,66.984,32.65 -2020-12-14 09:30:00,141.47,215.822,66.984,32.65 -2020-12-14 09:45:00,137.79,213.25799999999998,66.984,32.65 -2020-12-14 10:00:00,135.32,209.27,63.158,32.65 -2020-12-14 10:15:00,131.67,206.097,63.158,32.65 -2020-12-14 10:30:00,131.48,203.485,63.158,32.65 -2020-12-14 10:45:00,133.72,201.791,63.158,32.65 -2020-12-14 11:00:00,129.21,199.26,61.141000000000005,32.65 -2020-12-14 11:15:00,129.73,198.532,61.141000000000005,32.65 -2020-12-14 11:30:00,131.24,198.482,61.141000000000005,32.65 -2020-12-14 11:45:00,132.76,196.21599999999998,61.141000000000005,32.65 -2020-12-14 12:00:00,129.52,191.954,57.961000000000006,32.65 -2020-12-14 12:15:00,133.74,191.457,57.961000000000006,32.65 -2020-12-14 12:30:00,133.03,190.775,57.961000000000006,32.65 -2020-12-14 12:45:00,135.84,192.012,57.961000000000006,32.65 -2020-12-14 13:00:00,131.88,191.54,56.843,32.65 -2020-12-14 13:15:00,130.95,191.511,56.843,32.65 -2020-12-14 13:30:00,128.48,190.205,56.843,32.65 -2020-12-14 13:45:00,128.95,189.919,56.843,32.65 -2020-12-14 14:00:00,123.91,188.565,55.992,32.65 -2020-12-14 14:15:00,124.92,188.925,55.992,32.65 -2020-12-14 14:30:00,124.35,188.81599999999997,55.992,32.65 -2020-12-14 14:45:00,126.22,188.80700000000002,55.992,32.65 -2020-12-14 15:00:00,127.2,190.158,57.523,32.65 -2020-12-14 15:15:00,128.82,190.382,57.523,32.65 -2020-12-14 15:30:00,126.77,192.38400000000001,57.523,32.65 -2020-12-14 15:45:00,128.99,194.595,57.523,32.65 -2020-12-14 16:00:00,133.74,195.34400000000002,59.471000000000004,32.65 -2020-12-14 16:15:00,135.18,196.06599999999997,59.471000000000004,32.65 -2020-12-14 16:30:00,136.84,198.168,59.471000000000004,32.65 -2020-12-14 16:45:00,137.31,199.264,59.471000000000004,32.65 -2020-12-14 17:00:00,139.82,201.285,65.066,32.65 -2020-12-14 17:15:00,139.13,201.725,65.066,32.65 -2020-12-14 17:30:00,139.59,201.65200000000002,65.066,32.65 -2020-12-14 17:45:00,139.83,201.465,65.066,32.65 -2020-12-14 18:00:00,137.48,202.62900000000002,69.581,32.65 -2020-12-14 18:15:00,135.23,201.102,69.581,32.65 -2020-12-14 18:30:00,134.3,199.922,69.581,32.65 -2020-12-14 18:45:00,134.53,198.91299999999998,69.581,32.65 -2020-12-14 19:00:00,132.27,199.696,73.771,32.65 -2020-12-14 19:15:00,137.65,196.335,73.771,32.65 -2020-12-14 19:30:00,137.55,194.843,73.771,32.65 -2020-12-14 19:45:00,136.21,191.26,73.771,32.65 -2020-12-14 20:00:00,121.11,187.632,65.035,32.65 -2020-12-14 20:15:00,117.82,182.38099999999997,65.035,32.65 -2020-12-14 20:30:00,115.14,177.72400000000002,65.035,32.65 -2020-12-14 20:45:00,115.32,175.71200000000002,65.035,32.65 -2020-12-14 21:00:00,109.37,174.25,58.7,32.65 -2020-12-14 21:15:00,108.96,171.238,58.7,32.65 -2020-12-14 21:30:00,104.73,169.86900000000003,58.7,32.65 -2020-12-14 21:45:00,104.04,167.965,58.7,32.65 -2020-12-14 22:00:00,99.84,159.553,53.888000000000005,32.65 -2020-12-14 22:15:00,96.41,154.173,53.888000000000005,32.65 -2020-12-14 22:30:00,96.15,139.96200000000002,53.888000000000005,32.65 -2020-12-14 22:45:00,97.48,131.666,53.888000000000005,32.65 -2020-12-14 23:00:00,92.99,126.00399999999999,45.501999999999995,32.65 -2020-12-14 23:15:00,90.86,124.936,45.501999999999995,32.65 -2020-12-14 23:30:00,86.06,125.43,45.501999999999995,32.65 -2020-12-14 23:45:00,88.58,125.434,45.501999999999995,32.65 -2020-12-15 00:00:00,83.52,115.656,43.537,32.65 -2020-12-15 00:15:00,83.19,115.315,43.537,32.65 -2020-12-15 00:30:00,81.66,116.205,43.537,32.65 -2020-12-15 00:45:00,84.41,117.449,43.537,32.65 -2020-12-15 01:00:00,80.92,119.626,41.854,32.65 -2020-12-15 01:15:00,78.66,120.473,41.854,32.65 -2020-12-15 01:30:00,78.58,120.975,41.854,32.65 -2020-12-15 01:45:00,80.79,121.521,41.854,32.65 -2020-12-15 02:00:00,78.33,123.03200000000001,40.321,32.65 -2020-12-15 02:15:00,79.13,124.414,40.321,32.65 -2020-12-15 02:30:00,79.46,124.492,40.321,32.65 -2020-12-15 02:45:00,81.65,126.29299999999999,40.321,32.65 -2020-12-15 03:00:00,80.09,128.789,39.632,32.65 -2020-12-15 03:15:00,79.26,129.722,39.632,32.65 -2020-12-15 03:30:00,80.46,131.651,39.632,32.65 -2020-12-15 03:45:00,82.17,132.52100000000002,39.632,32.65 -2020-12-15 04:00:00,82.38,145.236,40.183,32.65 -2020-12-15 04:15:00,81.57,157.416,40.183,32.65 -2020-12-15 04:30:00,85.91,159.6,40.183,32.65 -2020-12-15 04:45:00,85.18,162.086,40.183,32.65 -2020-12-15 05:00:00,85.38,196.30599999999998,43.945,32.65 -2020-12-15 05:15:00,84.75,225.167,43.945,32.65 -2020-12-15 05:30:00,94.66,220.77900000000002,43.945,32.65 -2020-12-15 05:45:00,104.6,212.74599999999998,43.945,32.65 -2020-12-15 06:00:00,114.07,208.825,56.048,32.65 -2020-12-15 06:15:00,115.06,214.197,56.048,32.65 -2020-12-15 06:30:00,115.45,216.41,56.048,32.65 -2020-12-15 06:45:00,118.13,219.02599999999998,56.048,32.65 -2020-12-15 07:00:00,127.23,218.861,65.74,32.65 -2020-12-15 07:15:00,130.09,223.575,65.74,32.65 -2020-12-15 07:30:00,130.79,226.06599999999997,65.74,32.65 -2020-12-15 07:45:00,134.25,227.21,65.74,32.65 -2020-12-15 08:00:00,137.81,226.11599999999999,72.757,32.65 -2020-12-15 08:15:00,135.91,225.97099999999998,72.757,32.65 -2020-12-15 08:30:00,136.55,224.303,72.757,32.65 -2020-12-15 08:45:00,137.31,221.18200000000002,72.757,32.65 -2020-12-15 09:00:00,136.97,214.54,67.692,32.65 -2020-12-15 09:15:00,141.95,211.025,67.692,32.65 -2020-12-15 09:30:00,142.11,208.47,67.692,32.65 -2020-12-15 09:45:00,142.8,205.74200000000002,67.692,32.65 -2020-12-15 10:00:00,141.36,201.498,63.506,32.65 -2020-12-15 10:15:00,141.94,197.08599999999998,63.506,32.65 -2020-12-15 10:30:00,141.49,194.91400000000002,63.506,32.65 -2020-12-15 10:45:00,143.1,193.605,63.506,32.65 -2020-12-15 11:00:00,141.22,192.74099999999999,60.758,32.65 -2020-12-15 11:15:00,142.97,191.713,60.758,32.65 -2020-12-15 11:30:00,142.93,190.668,60.758,32.65 -2020-12-15 11:45:00,141.87,189.172,60.758,32.65 -2020-12-15 12:00:00,141.11,183.533,57.519,32.65 -2020-12-15 12:15:00,141.16,182.61700000000002,57.519,32.65 -2020-12-15 12:30:00,140.9,182.8,57.519,32.65 -2020-12-15 12:45:00,138.4,183.675,57.519,32.65 -2020-12-15 13:00:00,133.81,182.6,56.46,32.65 -2020-12-15 13:15:00,131.95,182.195,56.46,32.65 -2020-12-15 13:30:00,129.26,181.915,56.46,32.65 -2020-12-15 13:45:00,129.67,181.796,56.46,32.65 -2020-12-15 14:00:00,123.7,180.81599999999997,56.207,32.65 -2020-12-15 14:15:00,124.77,181.243,56.207,32.65 -2020-12-15 14:30:00,124.87,181.71400000000003,56.207,32.65 -2020-12-15 14:45:00,125.31,181.653,56.207,32.65 -2020-12-15 15:00:00,127.76,182.535,57.391999999999996,32.65 -2020-12-15 15:15:00,126.63,183.02599999999998,57.391999999999996,32.65 -2020-12-15 15:30:00,126.46,185.03799999999998,57.391999999999996,32.65 -2020-12-15 15:45:00,128.96,186.55,57.391999999999996,32.65 -2020-12-15 16:00:00,131.98,188.59799999999998,59.955,32.65 -2020-12-15 16:15:00,135.15,189.975,59.955,32.65 -2020-12-15 16:30:00,137.94,192.37400000000002,59.955,32.65 -2020-12-15 16:45:00,139.15,193.635,59.955,32.65 -2020-12-15 17:00:00,141.27,196.472,67.063,32.65 -2020-12-15 17:15:00,141.7,197.14700000000002,67.063,32.65 -2020-12-15 17:30:00,143.06,197.852,67.063,32.65 -2020-12-15 17:45:00,142.57,197.611,67.063,32.65 -2020-12-15 18:00:00,140.73,198.834,71.477,32.65 -2020-12-15 18:15:00,139.06,197.072,71.477,32.65 -2020-12-15 18:30:00,137.45,195.517,71.477,32.65 -2020-12-15 18:45:00,137.98,195.32299999999998,71.477,32.65 -2020-12-15 19:00:00,135.69,196.59099999999998,74.32,32.65 -2020-12-15 19:15:00,134.51,193.03599999999997,74.32,32.65 -2020-12-15 19:30:00,132.57,191.079,74.32,32.65 -2020-12-15 19:45:00,131.33,187.28799999999998,74.32,32.65 -2020-12-15 20:00:00,123.93,183.83700000000002,66.157,32.65 -2020-12-15 20:15:00,121.36,178.165,66.157,32.65 -2020-12-15 20:30:00,114.46,174.50599999999997,66.157,32.65 -2020-12-15 20:45:00,115.6,171.75400000000002,66.157,32.65 -2020-12-15 21:00:00,115.82,169.782,59.806000000000004,32.65 -2020-12-15 21:15:00,114.44,167.699,59.806000000000004,32.65 -2020-12-15 21:30:00,113.31,165.55900000000003,59.806000000000004,32.65 -2020-12-15 21:45:00,103.61,163.97,59.806000000000004,32.65 -2020-12-15 22:00:00,102.01,157.408,54.785,32.65 -2020-12-15 22:15:00,98.37,151.947,54.785,32.65 -2020-12-15 22:30:00,92.73,137.92,54.785,32.65 -2020-12-15 22:45:00,94.84,130.05100000000002,54.785,32.65 -2020-12-15 23:00:00,93.0,124.61200000000001,47.176,32.65 -2020-12-15 23:15:00,94.63,122.538,47.176,32.65 -2020-12-15 23:30:00,87.81,122.829,47.176,32.65 -2020-12-15 23:45:00,85.28,122.35,47.176,32.65 -2020-12-16 00:00:00,80.62,115.92399999999999,43.42,32.65 -2020-12-16 00:15:00,83.74,115.554,43.42,32.65 -2020-12-16 00:30:00,81.51,116.43799999999999,43.42,32.65 -2020-12-16 00:45:00,80.18,117.663,43.42,32.65 -2020-12-16 01:00:00,69.83,119.867,40.869,32.65 -2020-12-16 01:15:00,80.0,120.713,40.869,32.65 -2020-12-16 01:30:00,80.76,121.22,40.869,32.65 -2020-12-16 01:45:00,80.19,121.75399999999999,40.869,32.65 -2020-12-16 02:00:00,74.32,123.281,39.541,32.65 -2020-12-16 02:15:00,78.32,124.664,39.541,32.65 -2020-12-16 02:30:00,77.27,124.74799999999999,39.541,32.65 -2020-12-16 02:45:00,80.86,126.546,39.541,32.65 -2020-12-16 03:00:00,75.94,129.032,39.052,32.65 -2020-12-16 03:15:00,79.31,129.991,39.052,32.65 -2020-12-16 03:30:00,79.64,131.922,39.052,32.65 -2020-12-16 03:45:00,82.22,132.792,39.052,32.65 -2020-12-16 04:00:00,79.05,145.48,40.36,32.65 -2020-12-16 04:15:00,81.08,157.658,40.36,32.65 -2020-12-16 04:30:00,85.18,159.833,40.36,32.65 -2020-12-16 04:45:00,82.1,162.319,40.36,32.65 -2020-12-16 05:00:00,83.77,196.507,43.133,32.65 -2020-12-16 05:15:00,87.36,225.327,43.133,32.65 -2020-12-16 05:30:00,91.05,220.956,43.133,32.65 -2020-12-16 05:45:00,95.36,212.94299999999998,43.133,32.65 -2020-12-16 06:00:00,105.17,209.046,54.953,32.65 -2020-12-16 06:15:00,108.16,214.421,54.953,32.65 -2020-12-16 06:30:00,114.63,216.671,54.953,32.65 -2020-12-16 06:45:00,117.72,219.325,54.953,32.65 -2020-12-16 07:00:00,127.82,219.166,66.566,32.65 -2020-12-16 07:15:00,130.45,223.882,66.566,32.65 -2020-12-16 07:30:00,131.15,226.372,66.566,32.65 -2020-12-16 07:45:00,132.85,227.511,66.566,32.65 -2020-12-16 08:00:00,137.7,226.423,72.902,32.65 -2020-12-16 08:15:00,135.94,226.269,72.902,32.65 -2020-12-16 08:30:00,136.44,224.597,72.902,32.65 -2020-12-16 08:45:00,137.62,221.44799999999998,72.902,32.65 -2020-12-16 09:00:00,137.95,214.785,68.465,32.65 -2020-12-16 09:15:00,139.43,211.27599999999998,68.465,32.65 -2020-12-16 09:30:00,140.51,208.729,68.465,32.65 -2020-12-16 09:45:00,139.57,205.99,68.465,32.65 -2020-12-16 10:00:00,137.58,201.74,63.625,32.65 -2020-12-16 10:15:00,137.97,197.31400000000002,63.625,32.65 -2020-12-16 10:30:00,140.2,195.123,63.625,32.65 -2020-12-16 10:45:00,138.04,193.81,63.625,32.65 -2020-12-16 11:00:00,138.03,192.929,61.628,32.65 -2020-12-16 11:15:00,136.32,191.89,61.628,32.65 -2020-12-16 11:30:00,136.53,190.845,61.628,32.65 -2020-12-16 11:45:00,136.0,189.34599999999998,61.628,32.65 -2020-12-16 12:00:00,135.94,183.708,58.708999999999996,32.65 -2020-12-16 12:15:00,135.61,182.80599999999998,58.708999999999996,32.65 -2020-12-16 12:30:00,133.9,182.998,58.708999999999996,32.65 -2020-12-16 12:45:00,135.77,183.877,58.708999999999996,32.65 -2020-12-16 13:00:00,133.66,182.77900000000002,57.373000000000005,32.65 -2020-12-16 13:15:00,133.97,182.372,57.373000000000005,32.65 -2020-12-16 13:30:00,134.94,182.085,57.373000000000005,32.65 -2020-12-16 13:45:00,130.4,181.957,57.373000000000005,32.65 -2020-12-16 14:00:00,129.27,180.96599999999998,57.684,32.65 -2020-12-16 14:15:00,131.13,181.395,57.684,32.65 -2020-12-16 14:30:00,131.55,181.887,57.684,32.65 -2020-12-16 14:45:00,131.65,181.83599999999998,57.684,32.65 -2020-12-16 15:00:00,133.62,182.735,58.03,32.65 -2020-12-16 15:15:00,133.02,183.22,58.03,32.65 -2020-12-16 15:30:00,131.8,185.247,58.03,32.65 -2020-12-16 15:45:00,133.43,186.75799999999998,58.03,32.65 -2020-12-16 16:00:00,137.44,188.804,59.97,32.65 -2020-12-16 16:15:00,138.97,190.2,59.97,32.65 -2020-12-16 16:30:00,140.77,192.604,59.97,32.65 -2020-12-16 16:45:00,140.66,193.891,59.97,32.65 -2020-12-16 17:00:00,143.13,196.707,65.661,32.65 -2020-12-16 17:15:00,142.28,197.408,65.661,32.65 -2020-12-16 17:30:00,143.12,198.132,65.661,32.65 -2020-12-16 17:45:00,142.9,197.90200000000002,65.661,32.65 -2020-12-16 18:00:00,139.93,199.144,70.96300000000001,32.65 -2020-12-16 18:15:00,139.4,197.36,70.96300000000001,32.65 -2020-12-16 18:30:00,138.28,195.812,70.96300000000001,32.65 -2020-12-16 18:45:00,137.34,195.62900000000002,70.96300000000001,32.65 -2020-12-16 19:00:00,135.22,196.88,74.133,32.65 -2020-12-16 19:15:00,133.45,193.31799999999998,74.133,32.65 -2020-12-16 19:30:00,130.36,191.351,74.133,32.65 -2020-12-16 19:45:00,130.37,187.542,74.133,32.65 -2020-12-16 20:00:00,125.93,184.088,65.613,32.65 -2020-12-16 20:15:00,119.64,178.40900000000002,65.613,32.65 -2020-12-16 20:30:00,118.92,174.72799999999998,65.613,32.65 -2020-12-16 20:45:00,115.48,171.995,65.613,32.65 -2020-12-16 21:00:00,115.75,170.007,58.583,32.65 -2020-12-16 21:15:00,117.64,167.907,58.583,32.65 -2020-12-16 21:30:00,113.57,165.769,58.583,32.65 -2020-12-16 21:45:00,107.19,164.188,58.583,32.65 -2020-12-16 22:00:00,102.1,157.631,54.411,32.65 -2020-12-16 22:15:00,102.82,152.17600000000002,54.411,32.65 -2020-12-16 22:30:00,100.28,138.189,54.411,32.65 -2020-12-16 22:45:00,101.94,130.327,54.411,32.65 -2020-12-16 23:00:00,95.99,124.867,47.878,32.65 -2020-12-16 23:15:00,90.51,122.79,47.878,32.65 -2020-12-16 23:30:00,87.38,123.094,47.878,32.65 -2020-12-16 23:45:00,89.89,122.604,47.878,32.65 -2020-12-17 00:00:00,84.21,116.184,44.513000000000005,32.65 -2020-12-17 00:15:00,85.18,115.789,44.513000000000005,32.65 -2020-12-17 00:30:00,74.91,116.663,44.513000000000005,32.65 -2020-12-17 00:45:00,77.85,117.87100000000001,44.513000000000005,32.65 -2020-12-17 01:00:00,72.93,120.101,43.169,32.65 -2020-12-17 01:15:00,73.81,120.945,43.169,32.65 -2020-12-17 01:30:00,79.24,121.45700000000001,43.169,32.65 -2020-12-17 01:45:00,81.74,121.978,43.169,32.65 -2020-12-17 02:00:00,79.9,123.524,41.763999999999996,32.65 -2020-12-17 02:15:00,75.28,124.90799999999999,41.763999999999996,32.65 -2020-12-17 02:30:00,79.95,124.994,41.763999999999996,32.65 -2020-12-17 02:45:00,81.25,126.792,41.763999999999996,32.65 -2020-12-17 03:00:00,79.07,129.269,41.155,32.65 -2020-12-17 03:15:00,76.1,130.252,41.155,32.65 -2020-12-17 03:30:00,80.42,132.184,41.155,32.65 -2020-12-17 03:45:00,81.44,133.056,41.155,32.65 -2020-12-17 04:00:00,76.27,145.717,41.96,32.65 -2020-12-17 04:15:00,77.05,157.893,41.96,32.65 -2020-12-17 04:30:00,82.44,160.058,41.96,32.65 -2020-12-17 04:45:00,82.35,162.546,41.96,32.65 -2020-12-17 05:00:00,84.79,196.699,45.206,32.65 -2020-12-17 05:15:00,87.77,225.481,45.206,32.65 -2020-12-17 05:30:00,92.75,221.127,45.206,32.65 -2020-12-17 05:45:00,94.22,213.13099999999997,45.206,32.65 -2020-12-17 06:00:00,101.61,209.25799999999998,55.398999999999994,32.65 -2020-12-17 06:15:00,107.73,214.637,55.398999999999994,32.65 -2020-12-17 06:30:00,111.51,216.922,55.398999999999994,32.65 -2020-12-17 06:45:00,116.77,219.614,55.398999999999994,32.65 -2020-12-17 07:00:00,125.56,219.46200000000002,64.627,32.65 -2020-12-17 07:15:00,127.94,224.178,64.627,32.65 -2020-12-17 07:30:00,129.03,226.668,64.627,32.65 -2020-12-17 07:45:00,130.58,227.801,64.627,32.65 -2020-12-17 08:00:00,131.85,226.71900000000002,70.895,32.65 -2020-12-17 08:15:00,132.24,226.554,70.895,32.65 -2020-12-17 08:30:00,132.44,224.88,70.895,32.65 -2020-12-17 08:45:00,131.91,221.704,70.895,32.65 -2020-12-17 09:00:00,135.0,215.017,66.382,32.65 -2020-12-17 09:15:00,133.95,211.517,66.382,32.65 -2020-12-17 09:30:00,134.58,208.97799999999998,66.382,32.65 -2020-12-17 09:45:00,134.76,206.22799999999998,66.382,32.65 -2020-12-17 10:00:00,134.7,201.97299999999998,62.739,32.65 -2020-12-17 10:15:00,134.75,197.532,62.739,32.65 -2020-12-17 10:30:00,131.89,195.32299999999998,62.739,32.65 -2020-12-17 10:45:00,131.47,194.005,62.739,32.65 -2020-12-17 11:00:00,130.35,193.108,60.843,32.65 -2020-12-17 11:15:00,130.67,192.05900000000003,60.843,32.65 -2020-12-17 11:30:00,128.97,191.014,60.843,32.65 -2020-12-17 11:45:00,127.33,189.512,60.843,32.65 -2020-12-17 12:00:00,121.37,183.87400000000002,58.466,32.65 -2020-12-17 12:15:00,126.04,182.985,58.466,32.65 -2020-12-17 12:30:00,128.2,183.188,58.466,32.65 -2020-12-17 12:45:00,130.0,184.072,58.466,32.65 -2020-12-17 13:00:00,126.85,182.94799999999998,56.883,32.65 -2020-12-17 13:15:00,126.09,182.54,56.883,32.65 -2020-12-17 13:30:00,125.29,182.247,56.883,32.65 -2020-12-17 13:45:00,126.95,182.11,56.883,32.65 -2020-12-17 14:00:00,126.35,181.108,56.503,32.65 -2020-12-17 14:15:00,127.72,181.54,56.503,32.65 -2020-12-17 14:30:00,124.54,182.05200000000002,56.503,32.65 -2020-12-17 14:45:00,123.37,182.012,56.503,32.65 -2020-12-17 15:00:00,124.4,182.928,57.803999999999995,32.65 -2020-12-17 15:15:00,125.58,183.40599999999998,57.803999999999995,32.65 -2020-12-17 15:30:00,125.0,185.44799999999998,57.803999999999995,32.65 -2020-12-17 15:45:00,124.76,186.956,57.803999999999995,32.65 -2020-12-17 16:00:00,128.21,189.00099999999998,59.379,32.65 -2020-12-17 16:15:00,129.34,190.415,59.379,32.65 -2020-12-17 16:30:00,129.97,192.825,59.379,32.65 -2020-12-17 16:45:00,130.22,194.135,59.379,32.65 -2020-12-17 17:00:00,132.3,196.93099999999998,64.71600000000001,32.65 -2020-12-17 17:15:00,132.66,197.66,64.71600000000001,32.65 -2020-12-17 17:30:00,133.9,198.40200000000002,64.71600000000001,32.65 -2020-12-17 17:45:00,133.14,198.18400000000003,64.71600000000001,32.65 -2020-12-17 18:00:00,130.47,199.445,68.803,32.65 -2020-12-17 18:15:00,129.63,197.641,68.803,32.65 -2020-12-17 18:30:00,128.9,196.09799999999998,68.803,32.65 -2020-12-17 18:45:00,128.16,195.926,68.803,32.65 -2020-12-17 19:00:00,126.35,197.15900000000002,72.934,32.65 -2020-12-17 19:15:00,123.77,193.59099999999998,72.934,32.65 -2020-12-17 19:30:00,122.92,191.61599999999999,72.934,32.65 -2020-12-17 19:45:00,121.49,187.78799999999998,72.934,32.65 -2020-12-17 20:00:00,114.67,184.33,65.175,32.65 -2020-12-17 20:15:00,109.95,178.645,65.175,32.65 -2020-12-17 20:30:00,106.37,174.94400000000002,65.175,32.65 -2020-12-17 20:45:00,104.71,172.22799999999998,65.175,32.65 -2020-12-17 21:00:00,100.41,170.226,58.55,32.65 -2020-12-17 21:15:00,99.67,168.11,58.55,32.65 -2020-12-17 21:30:00,97.77,165.972,58.55,32.65 -2020-12-17 21:45:00,96.78,164.40099999999998,58.55,32.65 -2020-12-17 22:00:00,90.43,157.846,55.041000000000004,32.65 -2020-12-17 22:15:00,88.71,152.398,55.041000000000004,32.65 -2020-12-17 22:30:00,84.97,138.44899999999998,55.041000000000004,32.65 -2020-12-17 22:45:00,82.32,130.597,55.041000000000004,32.65 -2020-12-17 23:00:00,77.33,125.113,48.258,32.65 -2020-12-17 23:15:00,78.58,123.036,48.258,32.65 -2020-12-17 23:30:00,74.54,123.352,48.258,32.65 -2020-12-17 23:45:00,74.17,122.852,48.258,32.65 -2020-12-18 00:00:00,69.93,115.34700000000001,45.02,32.65 -2020-12-18 00:15:00,68.18,115.12299999999999,45.02,32.65 -2020-12-18 00:30:00,67.68,115.87,45.02,32.65 -2020-12-18 00:45:00,67.22,117.18,45.02,32.65 -2020-12-18 01:00:00,62.8,119.12299999999999,42.695,32.65 -2020-12-18 01:15:00,62.98,120.82700000000001,42.695,32.65 -2020-12-18 01:30:00,62.72,121.15299999999999,42.695,32.65 -2020-12-18 01:45:00,63.95,121.755,42.695,32.65 -2020-12-18 02:00:00,61.06,123.43,41.511,32.65 -2020-12-18 02:15:00,62.5,124.70299999999999,41.511,32.65 -2020-12-18 02:30:00,62.92,125.325,41.511,32.65 -2020-12-18 02:45:00,63.6,127.141,41.511,32.65 -2020-12-18 03:00:00,61.83,128.67600000000002,41.162,32.65 -2020-12-18 03:15:00,62.77,130.548,41.162,32.65 -2020-12-18 03:30:00,63.63,132.457,41.162,32.65 -2020-12-18 03:45:00,64.24,133.673,41.162,32.65 -2020-12-18 04:00:00,64.94,146.534,42.226000000000006,32.65 -2020-12-18 04:15:00,66.41,158.416,42.226000000000006,32.65 -2020-12-18 04:30:00,68.08,160.825,42.226000000000006,32.65 -2020-12-18 04:45:00,70.29,162.19299999999998,42.226000000000006,32.65 -2020-12-18 05:00:00,72.72,195.077,45.597,32.65 -2020-12-18 05:15:00,74.61,225.30200000000002,45.597,32.65 -2020-12-18 05:30:00,78.87,222.00799999999998,45.597,32.65 -2020-12-18 05:45:00,83.71,213.953,45.597,32.65 -2020-12-18 06:00:00,92.43,210.528,56.263999999999996,32.65 -2020-12-18 06:15:00,96.81,214.503,56.263999999999996,32.65 -2020-12-18 06:30:00,100.32,215.99,56.263999999999996,32.65 -2020-12-18 06:45:00,104.38,220.27700000000002,56.263999999999996,32.65 -2020-12-18 07:00:00,110.53,219.352,66.888,32.65 -2020-12-18 07:15:00,113.01,225.084,66.888,32.65 -2020-12-18 07:30:00,116.17,227.328,66.888,32.65 -2020-12-18 07:45:00,119.09,227.59599999999998,66.888,32.65 -2020-12-18 08:00:00,121.73,225.487,73.459,32.65 -2020-12-18 08:15:00,120.52,224.96400000000003,73.459,32.65 -2020-12-18 08:30:00,121.35,224.218,73.459,32.65 -2020-12-18 08:45:00,122.72,219.497,73.459,32.65 -2020-12-18 09:00:00,124.32,213.14,69.087,32.65 -2020-12-18 09:15:00,125.38,210.268,69.087,32.65 -2020-12-18 09:30:00,126.56,207.3,69.087,32.65 -2020-12-18 09:45:00,126.98,204.45,69.087,32.65 -2020-12-18 10:00:00,127.19,199.09799999999998,65.404,32.65 -2020-12-18 10:15:00,126.67,195.303,65.404,32.65 -2020-12-18 10:30:00,127.1,193.03099999999998,65.404,32.65 -2020-12-18 10:45:00,125.45,191.282,65.404,32.65 -2020-12-18 11:00:00,123.87,190.359,63.0,32.65 -2020-12-18 11:15:00,125.98,188.398,63.0,32.65 -2020-12-18 11:30:00,126.3,189.011,63.0,32.65 -2020-12-18 11:45:00,124.8,187.51,63.0,32.65 -2020-12-18 12:00:00,123.25,182.92700000000002,59.083,32.65 -2020-12-18 12:15:00,123.46,180.024,59.083,32.65 -2020-12-18 12:30:00,122.58,180.393,59.083,32.65 -2020-12-18 12:45:00,124.82,181.736,59.083,32.65 -2020-12-18 13:00:00,120.54,181.50900000000001,56.611999999999995,32.65 -2020-12-18 13:15:00,119.45,181.887,56.611999999999995,32.65 -2020-12-18 13:30:00,117.01,181.64,56.611999999999995,32.65 -2020-12-18 13:45:00,115.3,181.44799999999998,56.611999999999995,32.65 -2020-12-18 14:00:00,111.85,179.312,55.161,32.65 -2020-12-18 14:15:00,111.13,179.59599999999998,55.161,32.65 -2020-12-18 14:30:00,110.97,180.68599999999998,55.161,32.65 -2020-12-18 14:45:00,110.48,180.91400000000002,55.161,32.65 -2020-12-18 15:00:00,111.97,181.382,55.583,32.65 -2020-12-18 15:15:00,109.45,181.424,55.583,32.65 -2020-12-18 15:30:00,108.82,182.00400000000002,55.583,32.65 -2020-12-18 15:45:00,110.05,183.68400000000003,55.583,32.65 -2020-12-18 16:00:00,114.02,184.56900000000002,57.611999999999995,32.65 -2020-12-18 16:15:00,116.24,186.3,57.611999999999995,32.65 -2020-12-18 16:30:00,116.88,188.799,57.611999999999995,32.65 -2020-12-18 16:45:00,117.32,189.99,57.611999999999995,32.65 -2020-12-18 17:00:00,119.59,193.02,64.14,32.65 -2020-12-18 17:15:00,117.88,193.373,64.14,32.65 -2020-12-18 17:30:00,118.76,193.835,64.14,32.65 -2020-12-18 17:45:00,117.85,193.396,64.14,32.65 -2020-12-18 18:00:00,116.42,195.33700000000002,68.086,32.65 -2020-12-18 18:15:00,115.7,193.104,68.086,32.65 -2020-12-18 18:30:00,115.31,191.945,68.086,32.65 -2020-12-18 18:45:00,114.72,191.793,68.086,32.65 -2020-12-18 19:00:00,112.86,193.90900000000002,69.915,32.65 -2020-12-18 19:15:00,110.63,191.672,69.915,32.65 -2020-12-18 19:30:00,107.94,189.292,69.915,32.65 -2020-12-18 19:45:00,106.94,184.96900000000002,69.915,32.65 -2020-12-18 20:00:00,101.64,181.555,61.695,32.65 -2020-12-18 20:15:00,99.78,175.899,61.695,32.65 -2020-12-18 20:30:00,94.34,172.12099999999998,61.695,32.65 -2020-12-18 20:45:00,92.41,169.93200000000002,61.695,32.65 -2020-12-18 21:00:00,88.08,168.44799999999998,56.041000000000004,32.65 -2020-12-18 21:15:00,86.79,166.792,56.041000000000004,32.65 -2020-12-18 21:30:00,85.7,164.696,56.041000000000004,32.65 -2020-12-18 21:45:00,83.61,163.665,56.041000000000004,32.65 -2020-12-18 22:00:00,79.01,158.058,51.888999999999996,32.65 -2020-12-18 22:15:00,77.65,152.47799999999998,51.888999999999996,32.65 -2020-12-18 22:30:00,75.96,144.877,51.888999999999996,32.65 -2020-12-18 22:45:00,74.55,140.467,51.888999999999996,32.65 -2020-12-18 23:00:00,68.49,134.586,45.787,32.65 -2020-12-18 23:15:00,67.5,130.575,45.787,32.65 -2020-12-18 23:30:00,65.03,129.435,45.787,32.65 -2020-12-18 23:45:00,63.68,128.28,45.787,32.65 -2020-12-19 00:00:00,58.59,112.90299999999999,41.815,32.468 -2020-12-19 00:15:00,56.86,108.542,41.815,32.468 -2020-12-19 00:30:00,56.32,110.538,41.815,32.468 -2020-12-19 00:45:00,55.88,112.488,41.815,32.468 -2020-12-19 01:00:00,53.45,115.075,38.645,32.468 -2020-12-19 01:15:00,53.4,115.85600000000001,38.645,32.468 -2020-12-19 01:30:00,53.25,115.65799999999999,38.645,32.468 -2020-12-19 01:45:00,53.53,116.088,38.645,32.468 -2020-12-19 02:00:00,51.2,118.405,36.696,32.468 -2020-12-19 02:15:00,51.19,119.29299999999999,36.696,32.468 -2020-12-19 02:30:00,51.56,118.825,36.696,32.468 -2020-12-19 02:45:00,51.39,120.76700000000001,36.696,32.468 -2020-12-19 03:00:00,49.17,122.844,35.42,32.468 -2020-12-19 03:15:00,49.88,123.54899999999999,35.42,32.468 -2020-12-19 03:30:00,50.39,123.95100000000001,35.42,32.468 -2020-12-19 03:45:00,50.38,125.331,35.42,32.468 -2020-12-19 04:00:00,50.5,134.138,35.167,32.468 -2020-12-19 04:15:00,51.79,143.55200000000002,35.167,32.468 -2020-12-19 04:30:00,52.07,143.812,35.167,32.468 -2020-12-19 04:45:00,52.89,144.718,35.167,32.468 -2020-12-19 05:00:00,55.09,161.997,35.311,32.468 -2020-12-19 05:15:00,59.51,173.59400000000002,35.311,32.468 -2020-12-19 05:30:00,56.55,170.71599999999998,35.311,32.468 -2020-12-19 05:45:00,59.21,168.06099999999998,35.311,32.468 -2020-12-19 06:00:00,62.57,183.03900000000002,37.117,32.468 -2020-12-19 06:15:00,65.09,202.952,37.117,32.468 -2020-12-19 06:30:00,65.7,199.22299999999998,37.117,32.468 -2020-12-19 06:45:00,67.91,194.838,37.117,32.468 -2020-12-19 07:00:00,72.04,190.25900000000001,40.948,32.468 -2020-12-19 07:15:00,73.9,194.74,40.948,32.468 -2020-12-19 07:30:00,76.28,199.62900000000002,40.948,32.468 -2020-12-19 07:45:00,80.12,203.72299999999998,40.948,32.468 -2020-12-19 08:00:00,84.67,205.49,44.903,32.468 -2020-12-19 08:15:00,84.63,208.417,44.903,32.468 -2020-12-19 08:30:00,85.9,209.208,44.903,32.468 -2020-12-19 08:45:00,89.47,207.513,44.903,32.468 -2020-12-19 09:00:00,91.2,202.946,46.283,32.468 -2020-12-19 09:15:00,91.66,200.81900000000002,46.283,32.468 -2020-12-19 09:30:00,92.64,198.743,46.283,32.468 -2020-12-19 09:45:00,94.43,196.018,46.283,32.468 -2020-12-19 10:00:00,95.05,190.972,44.103,32.468 -2020-12-19 10:15:00,97.92,187.33599999999998,44.103,32.468 -2020-12-19 10:30:00,97.13,185.18900000000002,44.103,32.468 -2020-12-19 10:45:00,98.68,184.658,44.103,32.468 -2020-12-19 11:00:00,98.05,183.915,42.373999999999995,32.468 -2020-12-19 11:15:00,97.08,181.329,42.373999999999995,32.468 -2020-12-19 11:30:00,99.87,180.894,42.373999999999995,32.468 -2020-12-19 11:45:00,100.27,178.51,42.373999999999995,32.468 -2020-12-19 12:00:00,97.52,173.118,39.937,32.468 -2020-12-19 12:15:00,97.76,170.877,39.937,32.468 -2020-12-19 12:30:00,98.11,171.55,39.937,32.468 -2020-12-19 12:45:00,100.55,172.199,39.937,32.468 -2020-12-19 13:00:00,99.76,171.535,37.138000000000005,32.468 -2020-12-19 13:15:00,98.38,169.935,37.138000000000005,32.468 -2020-12-19 13:30:00,95.17,169.263,37.138000000000005,32.468 -2020-12-19 13:45:00,95.3,169.452,37.138000000000005,32.468 -2020-12-19 14:00:00,93.76,168.46900000000002,36.141999999999996,32.468 -2020-12-19 14:15:00,94.4,168.18900000000002,36.141999999999996,32.468 -2020-12-19 14:30:00,93.73,167.553,36.141999999999996,32.468 -2020-12-19 14:45:00,95.27,168.02,36.141999999999996,32.468 -2020-12-19 15:00:00,95.53,169.136,37.964,32.468 -2020-12-19 15:15:00,95.12,169.968,37.964,32.468 -2020-12-19 15:30:00,94.85,172.035,37.964,32.468 -2020-12-19 15:45:00,95.87,173.68400000000003,37.964,32.468 -2020-12-19 16:00:00,102.71,173.486,40.699,32.468 -2020-12-19 16:15:00,100.85,176.049,40.699,32.468 -2020-12-19 16:30:00,103.71,178.514,40.699,32.468 -2020-12-19 16:45:00,102.26,180.572,40.699,32.468 -2020-12-19 17:00:00,105.22,182.958,46.216,32.468 -2020-12-19 17:15:00,103.82,184.842,46.216,32.468 -2020-12-19 17:30:00,107.42,185.225,46.216,32.468 -2020-12-19 17:45:00,106.86,184.43,46.216,32.468 -2020-12-19 18:00:00,106.39,185.953,51.123999999999995,32.468 -2020-12-19 18:15:00,105.15,185.488,51.123999999999995,32.468 -2020-12-19 18:30:00,104.09,185.662,51.123999999999995,32.468 -2020-12-19 18:45:00,103.35,182.18599999999998,51.123999999999995,32.468 -2020-12-19 19:00:00,101.94,185.102,52.336000000000006,32.468 -2020-12-19 19:15:00,100.64,182.34900000000002,52.336000000000006,32.468 -2020-12-19 19:30:00,101.69,180.71,52.336000000000006,32.468 -2020-12-19 19:45:00,98.12,176.26,52.336000000000006,32.468 -2020-12-19 20:00:00,92.73,174.96900000000002,48.825,32.468 -2020-12-19 20:15:00,89.48,171.326,48.825,32.468 -2020-12-19 20:30:00,86.99,167.16099999999997,48.825,32.468 -2020-12-19 20:45:00,85.88,164.67,48.825,32.468 -2020-12-19 21:00:00,82.34,165.27700000000002,43.729,32.468 -2020-12-19 21:15:00,85.85,164.014,43.729,32.468 -2020-12-19 21:30:00,81.94,163.107,43.729,32.468 -2020-12-19 21:45:00,81.18,161.67600000000002,43.729,32.468 -2020-12-19 22:00:00,76.01,157.365,44.126000000000005,32.468 -2020-12-19 22:15:00,75.43,154.187,44.126000000000005,32.468 -2020-12-19 22:30:00,71.47,152.58100000000002,44.126000000000005,32.468 -2020-12-19 22:45:00,70.06,149.986,44.126000000000005,32.468 -2020-12-19 23:00:00,66.49,146.393,38.169000000000004,32.468 -2020-12-19 23:15:00,66.77,140.828,38.169000000000004,32.468 -2020-12-19 23:30:00,64.05,138.111,38.169000000000004,32.468 -2020-12-19 23:45:00,61.94,134.629,38.169000000000004,32.468 -2020-12-20 00:00:00,57.44,113.59899999999999,35.232,32.468 -2020-12-20 00:15:00,55.83,108.861,35.232,32.468 -2020-12-20 00:30:00,55.22,110.48299999999999,35.232,32.468 -2020-12-20 00:45:00,53.65,113.07600000000001,35.232,32.468 -2020-12-20 01:00:00,50.86,115.56200000000001,31.403000000000002,32.468 -2020-12-20 01:15:00,51.31,117.324,31.403000000000002,32.468 -2020-12-20 01:30:00,50.82,117.62100000000001,31.403000000000002,32.468 -2020-12-20 01:45:00,50.65,117.726,31.403000000000002,32.468 -2020-12-20 02:00:00,48.06,119.34700000000001,30.69,32.468 -2020-12-20 02:15:00,48.95,119.454,30.69,32.468 -2020-12-20 02:30:00,49.69,119.81200000000001,30.69,32.468 -2020-12-20 02:45:00,49.72,122.182,30.69,32.468 -2020-12-20 03:00:00,52.26,124.57700000000001,29.516,32.468 -2020-12-20 03:15:00,50.67,124.82,29.516,32.468 -2020-12-20 03:30:00,50.57,126.522,29.516,32.468 -2020-12-20 03:45:00,50.51,127.795,29.516,32.468 -2020-12-20 04:00:00,50.06,136.341,29.148000000000003,32.468 -2020-12-20 04:15:00,50.44,144.77100000000002,29.148000000000003,32.468 -2020-12-20 04:30:00,51.53,145.145,29.148000000000003,32.468 -2020-12-20 04:45:00,52.44,146.28799999999998,29.148000000000003,32.468 -2020-12-20 05:00:00,53.76,160.211,28.706,32.468 -2020-12-20 05:15:00,54.72,169.44299999999998,28.706,32.468 -2020-12-20 05:30:00,54.25,166.403,28.706,32.468 -2020-12-20 05:45:00,55.03,163.986,28.706,32.468 -2020-12-20 06:00:00,57.82,178.773,28.771,32.468 -2020-12-20 06:15:00,58.08,197.097,28.771,32.468 -2020-12-20 06:30:00,59.03,192.31900000000002,28.771,32.468 -2020-12-20 06:45:00,60.79,186.922,28.771,32.468 -2020-12-20 07:00:00,62.77,184.69099999999997,31.39,32.468 -2020-12-20 07:15:00,63.39,188.282,31.39,32.468 -2020-12-20 07:30:00,65.38,192.062,31.39,32.468 -2020-12-20 07:45:00,67.0,195.424,31.39,32.468 -2020-12-20 08:00:00,72.03,198.976,34.972,32.468 -2020-12-20 08:15:00,73.09,201.861,34.972,32.468 -2020-12-20 08:30:00,75.12,204.26,34.972,32.468 -2020-12-20 08:45:00,78.34,204.452,34.972,32.468 -2020-12-20 09:00:00,79.75,199.475,36.709,32.468 -2020-12-20 09:15:00,81.63,197.854,36.709,32.468 -2020-12-20 09:30:00,81.76,195.66099999999997,36.709,32.468 -2020-12-20 09:45:00,83.87,192.864,36.709,32.468 -2020-12-20 10:00:00,85.18,190.226,35.812,32.468 -2020-12-20 10:15:00,86.79,187.08700000000002,35.812,32.468 -2020-12-20 10:30:00,87.22,185.505,35.812,32.468 -2020-12-20 10:45:00,88.97,183.232,35.812,32.468 -2020-12-20 11:00:00,87.56,183.31900000000002,36.746,32.468 -2020-12-20 11:15:00,90.47,180.827,36.746,32.468 -2020-12-20 11:30:00,91.82,179.59799999999998,36.746,32.468 -2020-12-20 11:45:00,93.1,177.797,36.746,32.468 -2020-12-20 12:00:00,92.2,171.933,35.048,32.468 -2020-12-20 12:15:00,91.24,171.46599999999998,35.048,32.468 -2020-12-20 12:30:00,91.52,170.787,35.048,32.468 -2020-12-20 12:45:00,87.78,170.493,35.048,32.468 -2020-12-20 13:00:00,84.85,169.11700000000002,29.987,32.468 -2020-12-20 13:15:00,83.06,170.32299999999998,29.987,32.468 -2020-12-20 13:30:00,80.86,169.38299999999998,29.987,32.468 -2020-12-20 13:45:00,80.54,169.005,29.987,32.468 -2020-12-20 14:00:00,80.42,168.31099999999998,27.21,32.468 -2020-12-20 14:15:00,80.53,169.185,27.21,32.468 -2020-12-20 14:30:00,83.4,169.662,27.21,32.468 -2020-12-20 14:45:00,81.31,169.695,27.21,32.468 -2020-12-20 15:00:00,85.33,169.394,27.726999999999997,32.468 -2020-12-20 15:15:00,82.97,170.889,27.726999999999997,32.468 -2020-12-20 15:30:00,84.51,173.511,27.726999999999997,32.468 -2020-12-20 15:45:00,86.95,175.827,27.726999999999997,32.468 -2020-12-20 16:00:00,90.83,177.327,32.23,32.468 -2020-12-20 16:15:00,91.19,179.05700000000002,32.23,32.468 -2020-12-20 16:30:00,92.7,181.822,32.23,32.468 -2020-12-20 16:45:00,93.89,184.03099999999998,32.23,32.468 -2020-12-20 17:00:00,96.36,186.42700000000002,42.016999999999996,32.468 -2020-12-20 17:15:00,96.89,188.093,42.016999999999996,32.468 -2020-12-20 17:30:00,101.65,188.80599999999998,42.016999999999996,32.468 -2020-12-20 17:45:00,99.7,190.2,42.016999999999996,32.468 -2020-12-20 18:00:00,99.58,191.257,49.338,32.468 -2020-12-20 18:15:00,98.86,192.024,49.338,32.468 -2020-12-20 18:30:00,99.68,190.203,49.338,32.468 -2020-12-20 18:45:00,98.16,188.51,49.338,32.468 -2020-12-20 19:00:00,96.38,191.18099999999998,52.369,32.468 -2020-12-20 19:15:00,95.5,188.947,52.369,32.468 -2020-12-20 19:30:00,94.26,187.13400000000001,52.369,32.468 -2020-12-20 19:45:00,93.64,184.032,52.369,32.468 -2020-12-20 20:00:00,92.2,182.707,50.405,32.468 -2020-12-20 20:15:00,91.3,179.99400000000003,50.405,32.468 -2020-12-20 20:30:00,89.41,177.025,50.405,32.468 -2020-12-20 20:45:00,88.72,173.33700000000002,50.405,32.468 -2020-12-20 21:00:00,82.11,171.449,46.235,32.468 -2020-12-20 21:15:00,79.31,169.55900000000003,46.235,32.468 -2020-12-20 21:30:00,78.23,168.908,46.235,32.468 -2020-12-20 21:45:00,80.71,167.632,46.235,32.468 -2020-12-20 22:00:00,76.48,162.267,46.861000000000004,32.468 -2020-12-20 22:15:00,75.69,158.30200000000002,46.861000000000004,32.468 -2020-12-20 22:30:00,72.53,153.685,46.861000000000004,32.468 -2020-12-20 22:45:00,71.4,150.233,46.861000000000004,32.468 -2020-12-20 23:00:00,67.24,143.93200000000002,41.302,32.468 -2020-12-20 23:15:00,67.09,140.191,41.302,32.468 -2020-12-20 23:30:00,65.09,138.24200000000002,41.302,32.468 -2020-12-20 23:45:00,63.54,135.611,41.302,32.468 -2020-12-21 00:00:00,59.61,117.899,37.164,32.65 -2020-12-21 00:15:00,58.76,116.00200000000001,37.164,32.65 -2020-12-21 00:30:00,55.48,117.723,37.164,32.65 -2020-12-21 00:45:00,58.12,119.76799999999999,37.164,32.65 -2020-12-21 01:00:00,54.85,122.302,34.994,32.65 -2020-12-21 01:15:00,52.98,123.559,34.994,32.65 -2020-12-21 01:30:00,50.94,123.928,34.994,32.65 -2020-12-21 01:45:00,50.8,124.125,34.994,32.65 -2020-12-21 02:00:00,52.33,125.757,34.571,32.65 -2020-12-21 02:15:00,52.41,127.214,34.571,32.65 -2020-12-21 02:30:00,52.65,127.90100000000001,34.571,32.65 -2020-12-21 02:45:00,53.11,129.673,34.571,32.65 -2020-12-21 03:00:00,51.52,133.284,33.934,32.65 -2020-12-21 03:15:00,52.71,135.15,33.934,32.65 -2020-12-21 03:30:00,52.07,136.624,33.934,32.65 -2020-12-21 03:45:00,49.61,137.35299999999998,33.934,32.65 -2020-12-21 04:00:00,53.48,150.143,34.107,32.65 -2020-12-21 04:15:00,52.93,162.64,34.107,32.65 -2020-12-21 04:30:00,53.67,165.072,34.107,32.65 -2020-12-21 04:45:00,54.53,166.38400000000001,34.107,32.65 -2020-12-21 05:00:00,56.42,195.558,39.575,32.65 -2020-12-21 05:15:00,57.68,224.365,39.575,32.65 -2020-12-21 05:30:00,58.83,221.533,39.575,32.65 -2020-12-21 05:45:00,60.34,213.628,39.575,32.65 -2020-12-21 06:00:00,62.76,210.952,56.156000000000006,32.65 -2020-12-21 06:15:00,66.38,214.78599999999997,56.156000000000006,32.65 -2020-12-21 06:30:00,67.63,217.83,56.156000000000006,32.65 -2020-12-21 06:45:00,70.42,221.06,56.156000000000006,32.65 -2020-12-21 07:00:00,74.09,221.109,67.926,32.65 -2020-12-21 07:15:00,75.56,225.983,67.926,32.65 -2020-12-21 07:30:00,78.04,228.984,67.926,32.65 -2020-12-21 07:45:00,81.33,229.912,67.926,32.65 -2020-12-21 08:00:00,85.45,228.74900000000002,72.58,32.65 -2020-12-21 08:15:00,86.9,229.53400000000002,72.58,32.65 -2020-12-21 08:30:00,89.21,228.0,72.58,32.65 -2020-12-21 08:45:00,91.43,225.005,72.58,32.65 -2020-12-21 09:00:00,94.89,219.02200000000002,66.984,32.65 -2020-12-21 09:15:00,97.25,214.018,66.984,32.65 -2020-12-21 09:30:00,101.27,210.845,66.984,32.65 -2020-12-21 09:45:00,99.72,208.222,66.984,32.65 -2020-12-21 10:00:00,101.1,204.55200000000002,63.158,32.65 -2020-12-21 10:15:00,100.36,201.084,63.158,32.65 -2020-12-21 10:30:00,101.56,198.63400000000001,63.158,32.65 -2020-12-21 10:45:00,102.86,196.997,63.158,32.65 -2020-12-21 11:00:00,100.92,194.606,61.141000000000005,32.65 -2020-12-21 11:15:00,101.74,193.829,61.141000000000005,32.65 -2020-12-21 11:30:00,103.83,193.94,61.141000000000005,32.65 -2020-12-21 11:45:00,102.91,191.77200000000002,61.141000000000005,32.65 -2020-12-21 12:00:00,101.04,187.456,57.961000000000006,32.65 -2020-12-21 12:15:00,101.4,187.00900000000001,57.961000000000006,32.65 -2020-12-21 12:30:00,98.37,186.525,57.961000000000006,32.65 -2020-12-21 12:45:00,97.4,187.678,57.961000000000006,32.65 -2020-12-21 13:00:00,94.93,186.868,56.843,32.65 -2020-12-21 13:15:00,92.49,186.717,56.843,32.65 -2020-12-21 13:30:00,90.19,185.27599999999998,56.843,32.65 -2020-12-21 13:45:00,89.94,184.963,56.843,32.65 -2020-12-21 14:00:00,86.88,183.642,55.992,32.65 -2020-12-21 14:15:00,84.1,183.937,55.992,32.65 -2020-12-21 14:30:00,81.84,183.912,55.992,32.65 -2020-12-21 14:45:00,83.32,184.023,55.992,32.65 -2020-12-21 15:00:00,83.47,185.426,57.523,32.65 -2020-12-21 15:15:00,81.79,185.524,57.523,32.65 -2020-12-21 15:30:00,81.2,187.407,57.523,32.65 -2020-12-21 15:45:00,82.37,189.275,57.523,32.65 -2020-12-21 16:00:00,85.28,191.02,59.471000000000004,32.65 -2020-12-21 16:15:00,84.75,192.044,59.471000000000004,32.65 -2020-12-21 16:30:00,86.17,193.86700000000002,59.471000000000004,32.65 -2020-12-21 16:45:00,86.8,194.97,59.471000000000004,32.65 -2020-12-21 17:00:00,89.11,197.155,65.066,32.65 -2020-12-21 17:15:00,90.27,197.953,65.066,32.65 -2020-12-21 17:30:00,92.59,198.148,65.066,32.65 -2020-12-21 17:45:00,92.01,198.112,65.066,32.65 -2020-12-21 18:00:00,92.03,199.575,69.581,32.65 -2020-12-21 18:15:00,92.38,198.145,69.581,32.65 -2020-12-21 18:30:00,91.75,196.93200000000002,69.581,32.65 -2020-12-21 18:45:00,91.35,196.043,69.581,32.65 -2020-12-21 19:00:00,87.77,197.149,73.771,32.65 -2020-12-21 19:15:00,86.86,193.826,73.771,32.65 -2020-12-21 19:30:00,85.1,192.47299999999998,73.771,32.65 -2020-12-21 19:45:00,84.71,188.53799999999998,73.771,32.65 -2020-12-21 20:00:00,81.21,184.893,65.035,32.65 -2020-12-21 20:15:00,80.86,179.843,65.035,32.65 -2020-12-21 20:30:00,79.94,175.08700000000002,65.035,32.65 -2020-12-21 20:45:00,78.91,172.983,65.035,32.65 -2020-12-21 21:00:00,76.0,171.56799999999998,58.7,32.65 -2020-12-21 21:15:00,76.31,168.549,58.7,32.65 -2020-12-21 21:30:00,74.92,167.11900000000003,58.7,32.65 -2020-12-21 21:45:00,74.61,165.36,58.7,32.65 -2020-12-21 22:00:00,72.26,157.161,53.888000000000005,32.65 -2020-12-21 22:15:00,72.03,152.0,53.888000000000005,32.65 -2020-12-21 22:30:00,70.9,138.101,53.888000000000005,32.65 -2020-12-21 22:45:00,70.7,130.013,53.888000000000005,32.65 -2020-12-21 23:00:00,67.92,124.43299999999999,45.501999999999995,32.65 -2020-12-21 23:15:00,65.32,123.19,45.501999999999995,32.65 -2020-12-21 23:30:00,63.5,123.906,45.501999999999995,32.65 -2020-12-21 23:45:00,63.09,123.789,45.501999999999995,32.65 -2020-12-22 00:00:00,77.11,117.387,43.537,32.65 -2020-12-22 00:15:00,75.55,116.86,43.537,32.65 -2020-12-22 00:30:00,74.5,117.693,43.537,32.65 -2020-12-22 00:45:00,75.85,118.81,43.537,32.65 -2020-12-22 01:00:00,74.35,121.161,41.854,32.65 -2020-12-22 01:15:00,72.04,121.991,41.854,32.65 -2020-12-22 01:30:00,70.74,122.525,41.854,32.65 -2020-12-22 01:45:00,72.79,122.985,41.854,32.65 -2020-12-22 02:00:00,71.78,124.613,40.321,32.65 -2020-12-22 02:15:00,72.48,126.00299999999999,40.321,32.65 -2020-12-22 02:30:00,72.39,126.111,40.321,32.65 -2020-12-22 02:45:00,73.21,127.904,40.321,32.65 -2020-12-22 03:00:00,72.62,130.333,39.632,32.65 -2020-12-22 03:15:00,73.58,131.439,39.632,32.65 -2020-12-22 03:30:00,75.36,133.376,39.632,32.65 -2020-12-22 03:45:00,75.07,134.254,39.632,32.65 -2020-12-22 04:00:00,74.68,146.79,40.183,32.65 -2020-12-22 04:15:00,74.32,158.94799999999998,40.183,32.65 -2020-12-22 04:30:00,75.7,161.071,40.183,32.65 -2020-12-22 04:45:00,77.44,163.56,40.183,32.65 -2020-12-22 05:00:00,80.88,197.551,43.945,32.65 -2020-12-22 05:15:00,83.17,226.143,43.945,32.65 -2020-12-22 05:30:00,88.89,221.862,43.945,32.65 -2020-12-22 05:45:00,94.9,213.957,43.945,32.65 -2020-12-22 06:00:00,103.72,210.203,56.048,32.65 -2020-12-22 06:15:00,108.11,215.607,56.048,32.65 -2020-12-22 06:30:00,112.38,218.05200000000002,56.048,32.65 -2020-12-22 06:45:00,116.06,220.926,56.048,32.65 -2020-12-22 07:00:00,122.27,220.813,65.74,32.65 -2020-12-22 07:15:00,126.37,225.525,65.74,32.65 -2020-12-22 07:30:00,128.12,227.997,65.74,32.65 -2020-12-22 07:45:00,131.48,229.09,65.74,32.65 -2020-12-22 08:00:00,133.47,228.032,72.757,32.65 -2020-12-22 08:15:00,133.0,227.81099999999998,72.757,32.65 -2020-12-22 08:30:00,133.5,226.104,72.757,32.65 -2020-12-22 08:45:00,134.21,222.799,72.757,32.65 -2020-12-22 09:00:00,133.97,216.007,67.692,32.65 -2020-12-22 09:15:00,136.46,212.53900000000002,67.692,32.65 -2020-12-22 09:30:00,137.68,210.049,67.692,32.65 -2020-12-22 09:45:00,137.1,207.25099999999998,67.692,32.65 -2020-12-22 10:00:00,137.74,202.968,63.506,32.65 -2020-12-22 10:15:00,137.9,198.468,63.506,32.65 -2020-12-22 10:30:00,137.65,196.17700000000002,63.506,32.65 -2020-12-22 10:45:00,140.41,194.83900000000003,63.506,32.65 -2020-12-22 11:00:00,142.25,193.857,60.758,32.65 -2020-12-22 11:15:00,143.49,192.766,60.758,32.65 -2020-12-22 11:30:00,143.27,191.726,60.758,32.65 -2020-12-22 11:45:00,138.82,190.21099999999998,60.758,32.65 -2020-12-22 12:00:00,137.52,184.581,57.519,32.65 -2020-12-22 12:15:00,137.54,183.763,57.519,32.65 -2020-12-22 12:30:00,137.08,184.00599999999997,57.519,32.65 -2020-12-22 12:45:00,135.61,184.908,57.519,32.65 -2020-12-22 13:00:00,132.97,183.676,56.46,32.65 -2020-12-22 13:15:00,132.28,183.25400000000002,56.46,32.65 -2020-12-22 13:30:00,130.5,182.924,56.46,32.65 -2020-12-22 13:45:00,130.46,182.74599999999998,56.46,32.65 -2020-12-22 14:00:00,129.11,181.707,56.207,32.65 -2020-12-22 14:15:00,129.2,182.144,56.207,32.65 -2020-12-22 14:30:00,127.97,182.748,56.207,32.65 -2020-12-22 14:45:00,128.67,182.766,56.207,32.65 -2020-12-22 15:00:00,128.91,183.767,57.391999999999996,32.65 -2020-12-22 15:15:00,128.69,184.206,57.391999999999996,32.65 -2020-12-22 15:30:00,128.66,186.304,57.391999999999996,32.65 -2020-12-22 15:45:00,129.67,187.796,57.391999999999996,32.65 -2020-12-22 16:00:00,133.28,189.83700000000002,59.955,32.65 -2020-12-22 16:15:00,135.66,191.335,59.955,32.65 -2020-12-22 16:30:00,137.94,193.77200000000002,59.955,32.65 -2020-12-22 16:45:00,139.17,195.19400000000002,59.955,32.65 -2020-12-22 17:00:00,143.78,197.887,67.063,32.65 -2020-12-22 17:15:00,144.03,198.753,67.063,32.65 -2020-12-22 17:30:00,145.49,199.59599999999998,67.063,32.65 -2020-12-22 17:45:00,142.58,199.43900000000002,67.063,32.65 -2020-12-22 18:00:00,139.78,200.797,71.477,32.65 -2020-12-22 18:15:00,138.93,198.907,71.477,32.65 -2020-12-22 18:30:00,137.96,197.396,71.477,32.65 -2020-12-22 18:45:00,138.52,197.283,71.477,32.65 -2020-12-22 19:00:00,134.79,198.418,74.32,32.65 -2020-12-22 19:15:00,134.11,194.825,74.32,32.65 -2020-12-22 19:30:00,134.57,192.81400000000002,74.32,32.65 -2020-12-22 19:45:00,134.19,188.90900000000002,74.32,32.65 -2020-12-22 20:00:00,126.29,185.421,66.157,32.65 -2020-12-22 20:15:00,120.72,179.71200000000002,66.157,32.65 -2020-12-22 20:30:00,116.25,175.917,66.157,32.65 -2020-12-22 20:45:00,114.97,173.28900000000002,66.157,32.65 -2020-12-22 21:00:00,111.39,171.205,59.806000000000004,32.65 -2020-12-22 21:15:00,112.49,169.00799999999998,59.806000000000004,32.65 -2020-12-22 21:30:00,112.51,166.87900000000002,59.806000000000004,32.65 -2020-12-22 21:45:00,108.75,165.354,59.806000000000004,32.65 -2020-12-22 22:00:00,100.85,158.816,54.785,32.65 -2020-12-22 22:15:00,98.46,153.403,54.785,32.65 -2020-12-22 22:30:00,93.77,139.632,54.785,32.65 -2020-12-22 22:45:00,94.39,131.819,54.785,32.65 -2020-12-22 23:00:00,92.62,126.223,47.176,32.65 -2020-12-22 23:15:00,91.43,124.146,47.176,32.65 -2020-12-22 23:30:00,86.04,124.527,47.176,32.65 -2020-12-22 23:45:00,83.32,123.98200000000001,47.176,32.65 -2020-12-23 00:00:00,83.37,117.60700000000001,43.42,32.65 -2020-12-23 00:15:00,83.59,117.055,43.42,32.65 -2020-12-23 00:30:00,83.23,117.87899999999999,43.42,32.65 -2020-12-23 00:45:00,80.94,118.979,43.42,32.65 -2020-12-23 01:00:00,76.7,121.35,40.869,32.65 -2020-12-23 01:15:00,80.48,122.177,40.869,32.65 -2020-12-23 01:30:00,81.54,122.714,40.869,32.65 -2020-12-23 01:45:00,80.75,123.163,40.869,32.65 -2020-12-23 02:00:00,77.53,124.807,39.541,32.65 -2020-12-23 02:15:00,76.71,126.197,39.541,32.65 -2020-12-23 02:30:00,79.2,126.31,39.541,32.65 -2020-12-23 02:45:00,81.57,128.10299999999998,39.541,32.65 -2020-12-23 03:00:00,79.82,130.524,39.052,32.65 -2020-12-23 03:15:00,77.81,131.65200000000002,39.052,32.65 -2020-12-23 03:30:00,79.49,133.59,39.052,32.65 -2020-12-23 03:45:00,83.44,134.471,39.052,32.65 -2020-12-23 04:00:00,82.03,146.98,40.36,32.65 -2020-12-23 04:15:00,80.22,159.136,40.36,32.65 -2020-12-23 04:30:00,80.62,161.252,40.36,32.65 -2020-12-23 04:45:00,83.03,163.74,40.36,32.65 -2020-12-23 05:00:00,87.93,197.69799999999998,43.133,32.65 -2020-12-23 05:15:00,91.08,226.25599999999997,43.133,32.65 -2020-12-23 05:30:00,94.96,221.985,43.133,32.65 -2020-12-23 05:45:00,98.6,214.09900000000002,43.133,32.65 -2020-12-23 06:00:00,105.55,210.36900000000003,54.953,32.65 -2020-12-23 06:15:00,110.85,215.77700000000002,54.953,32.65 -2020-12-23 06:30:00,115.87,218.25099999999998,54.953,32.65 -2020-12-23 06:45:00,120.4,221.162,54.953,32.65 -2020-12-23 07:00:00,126.78,221.05700000000002,66.566,32.65 -2020-12-23 07:15:00,128.65,225.765,66.566,32.65 -2020-12-23 07:30:00,130.99,228.233,66.566,32.65 -2020-12-23 07:45:00,133.38,229.315,66.566,32.65 -2020-12-23 08:00:00,135.54,228.261,72.902,32.65 -2020-12-23 08:15:00,135.89,228.02700000000002,72.902,32.65 -2020-12-23 08:30:00,136.41,226.31099999999998,72.902,32.65 -2020-12-23 08:45:00,136.61,222.982,72.902,32.65 -2020-12-23 09:00:00,137.52,216.169,68.465,32.65 -2020-12-23 09:15:00,138.65,212.708,68.465,32.65 -2020-12-23 09:30:00,139.53,210.229,68.465,32.65 -2020-12-23 09:45:00,140.58,207.421,68.465,32.65 -2020-12-23 10:00:00,139.15,203.13299999999998,63.625,32.65 -2020-12-23 10:15:00,139.36,198.62400000000002,63.625,32.65 -2020-12-23 10:30:00,139.02,196.31799999999998,63.625,32.65 -2020-12-23 10:45:00,138.24,194.97799999999998,63.625,32.65 -2020-12-23 11:00:00,141.5,193.97799999999998,61.628,32.65 -2020-12-23 11:15:00,140.02,192.88,61.628,32.65 -2020-12-23 11:30:00,142.39,191.84,61.628,32.65 -2020-12-23 11:45:00,137.8,190.325,61.628,32.65 -2020-12-23 12:00:00,134.47,184.69799999999998,58.708999999999996,32.65 -2020-12-23 12:15:00,132.86,183.894,58.708999999999996,32.65 -2020-12-23 12:30:00,132.25,184.143,58.708999999999996,32.65 -2020-12-23 12:45:00,133.37,185.047,58.708999999999996,32.65 -2020-12-23 13:00:00,133.19,183.796,57.373000000000005,32.65 -2020-12-23 13:15:00,133.97,183.37,57.373000000000005,32.65 -2020-12-23 13:30:00,132.3,183.033,57.373000000000005,32.65 -2020-12-23 13:45:00,130.97,182.84799999999998,57.373000000000005,32.65 -2020-12-23 14:00:00,130.64,181.804,57.684,32.65 -2020-12-23 14:15:00,130.79,182.24099999999999,57.684,32.65 -2020-12-23 14:30:00,130.4,182.863,57.684,32.65 -2020-12-23 14:45:00,130.0,182.893,57.684,32.65 -2020-12-23 15:00:00,130.36,183.90900000000002,58.03,32.65 -2020-12-23 15:15:00,130.22,184.33900000000003,58.03,32.65 -2020-12-23 15:30:00,129.82,186.446,58.03,32.65 -2020-12-23 15:45:00,131.13,187.935,58.03,32.65 -2020-12-23 16:00:00,133.7,189.97299999999998,59.97,32.65 -2020-12-23 16:15:00,136.31,191.487,59.97,32.65 -2020-12-23 16:30:00,139.13,193.93,59.97,32.65 -2020-12-23 16:45:00,141.42,195.372,59.97,32.65 -2020-12-23 17:00:00,147.76,198.045,65.661,32.65 -2020-12-23 17:15:00,147.93,198.938,65.661,32.65 -2020-12-23 17:30:00,148.05,199.803,65.661,32.65 -2020-12-23 17:45:00,143.23,199.658,65.661,32.65 -2020-12-23 18:00:00,140.17,201.03599999999997,70.96300000000001,32.65 -2020-12-23 18:15:00,139.83,199.13299999999998,70.96300000000001,32.65 -2020-12-23 18:30:00,138.54,197.62900000000002,70.96300000000001,32.65 -2020-12-23 18:45:00,138.83,197.52900000000002,70.96300000000001,32.65 -2020-12-23 19:00:00,136.66,198.641,74.133,32.65 -2020-12-23 19:15:00,134.75,195.046,74.133,32.65 -2020-12-23 19:30:00,133.88,193.02900000000002,74.133,32.65 -2020-12-23 19:45:00,133.25,189.11,74.133,32.65 -2020-12-23 20:00:00,126.72,185.614,65.613,32.65 -2020-12-23 20:15:00,120.6,179.90200000000002,65.613,32.65 -2020-12-23 20:30:00,117.16,176.08900000000003,65.613,32.65 -2020-12-23 20:45:00,115.94,173.47799999999998,65.613,32.65 -2020-12-23 21:00:00,113.09,171.37900000000002,58.583,32.65 -2020-12-23 21:15:00,114.76,169.165,58.583,32.65 -2020-12-23 21:30:00,112.8,167.037,58.583,32.65 -2020-12-23 21:45:00,109.3,165.523,58.583,32.65 -2020-12-23 22:00:00,103.75,158.988,54.411,32.65 -2020-12-23 22:15:00,102.73,153.583,54.411,32.65 -2020-12-23 22:30:00,99.23,139.843,54.411,32.65 -2020-12-23 22:45:00,99.37,132.03799999999998,54.411,32.65 -2020-12-23 23:00:00,94.42,126.421,47.878,32.65 -2020-12-23 23:15:00,89.7,124.345,47.878,32.65 -2020-12-23 23:30:00,86.46,124.74,47.878,32.65 -2020-12-23 23:45:00,87.03,124.18799999999999,47.878,32.65 -2020-12-24 00:00:00,57.0,117.822,44.513000000000005,32.468 -2020-12-24 00:15:00,57.34,117.244,44.513000000000005,32.468 -2020-12-24 00:30:00,56.42,118.05799999999999,44.513000000000005,32.468 -2020-12-24 00:45:00,55.53,119.14,44.513000000000005,32.468 -2020-12-24 01:00:00,52.63,121.53200000000001,43.169,32.468 -2020-12-24 01:15:00,53.35,122.355,43.169,32.468 -2020-12-24 01:30:00,52.36,122.896,43.169,32.468 -2020-12-24 01:45:00,52.48,123.33200000000001,43.169,32.468 -2020-12-24 02:00:00,51.25,124.992,41.763999999999996,32.468 -2020-12-24 02:15:00,52.09,126.383,41.763999999999996,32.468 -2020-12-24 02:30:00,51.45,126.501,41.763999999999996,32.468 -2020-12-24 02:45:00,51.82,128.293,41.763999999999996,32.468 -2020-12-24 03:00:00,51.68,130.705,41.155,32.468 -2020-12-24 03:15:00,52.17,131.857,41.155,32.468 -2020-12-24 03:30:00,52.33,133.796,41.155,32.468 -2020-12-24 03:45:00,53.49,134.678,41.155,32.468 -2020-12-24 04:00:00,54.37,147.165,41.96,32.468 -2020-12-24 04:15:00,54.98,159.316,41.96,32.468 -2020-12-24 04:30:00,55.93,161.425,41.96,32.468 -2020-12-24 04:45:00,57.9,163.912,41.96,32.468 -2020-12-24 05:00:00,61.23,197.83700000000002,45.206,32.468 -2020-12-24 05:15:00,61.7,226.36,45.206,32.468 -2020-12-24 05:30:00,63.74,222.101,45.206,32.468 -2020-12-24 05:45:00,65.72,214.232,45.206,32.468 -2020-12-24 06:00:00,71.25,210.52599999999998,55.398999999999994,32.468 -2020-12-24 06:15:00,73.82,215.94,55.398999999999994,32.468 -2020-12-24 06:30:00,75.73,218.44099999999997,55.398999999999994,32.468 -2020-12-24 06:45:00,79.76,221.386,55.398999999999994,32.468 -2020-12-24 07:00:00,84.31,221.29,64.627,32.468 -2020-12-24 07:15:00,85.26,225.99599999999998,64.627,32.468 -2020-12-24 07:30:00,86.83,228.458,64.627,32.468 -2020-12-24 07:45:00,89.99,229.53,64.627,32.468 -2020-12-24 08:00:00,92.92,228.477,70.895,32.468 -2020-12-24 08:15:00,93.1,228.232,70.895,32.468 -2020-12-24 08:30:00,94.95,226.505,70.895,32.468 -2020-12-24 08:45:00,95.57,223.15200000000002,70.895,32.468 -2020-12-24 09:00:00,99.59,216.31900000000002,66.382,32.468 -2020-12-24 09:15:00,100.3,212.865,66.382,32.468 -2020-12-24 09:30:00,100.82,210.396,66.382,32.468 -2020-12-24 09:45:00,100.23,207.579,66.382,32.468 -2020-12-24 10:00:00,101.06,203.28799999999998,62.739,32.468 -2020-12-24 10:15:00,101.79,198.771,62.739,32.468 -2020-12-24 10:30:00,102.05,196.449,62.739,32.468 -2020-12-24 10:45:00,102.54,195.107,62.739,32.468 -2020-12-24 11:00:00,100.69,194.09,60.843,32.468 -2020-12-24 11:15:00,101.48,192.986,60.843,32.468 -2020-12-24 11:30:00,100.88,191.946,60.843,32.468 -2020-12-24 11:45:00,99.0,190.43,60.843,32.468 -2020-12-24 12:00:00,94.85,184.805,58.466,32.468 -2020-12-24 12:15:00,95.31,184.016,58.466,32.468 -2020-12-24 12:30:00,93.98,184.271,58.466,32.468 -2020-12-24 12:45:00,96.03,185.178,58.466,32.468 -2020-12-24 13:00:00,94.18,183.908,56.883,32.468 -2020-12-24 13:15:00,90.44,183.48,56.883,32.468 -2020-12-24 13:30:00,90.13,183.132,56.883,32.468 -2020-12-24 13:45:00,90.54,182.94,56.883,32.468 -2020-12-24 14:00:00,89.78,181.893,56.503,32.468 -2020-12-24 14:15:00,92.43,182.33,56.503,32.468 -2020-12-24 14:30:00,90.39,182.968,56.503,32.468 -2020-12-24 14:45:00,92.14,183.01,56.503,32.468 -2020-12-24 15:00:00,92.57,184.044,57.803999999999995,32.468 -2020-12-24 15:15:00,93.64,184.463,57.803999999999995,32.468 -2020-12-24 15:30:00,94.37,186.578,57.803999999999995,32.468 -2020-12-24 15:45:00,97.44,188.062,57.803999999999995,32.468 -2020-12-24 16:00:00,103.78,190.1,59.379,32.468 -2020-12-24 16:15:00,107.92,191.628,59.379,32.468 -2020-12-24 16:30:00,106.71,194.077,59.379,32.468 -2020-12-24 16:45:00,107.58,195.53900000000002,59.379,32.468 -2020-12-24 17:00:00,111.26,198.19299999999998,64.71600000000001,32.468 -2020-12-24 17:15:00,110.82,199.113,64.71600000000001,32.468 -2020-12-24 17:30:00,112.07,200.0,64.71600000000001,32.468 -2020-12-24 17:45:00,111.35,199.86700000000002,64.71600000000001,32.468 -2020-12-24 18:00:00,110.75,201.264,68.803,32.468 -2020-12-24 18:15:00,110.62,199.34900000000002,68.803,32.468 -2020-12-24 18:30:00,110.52,197.851,68.803,32.468 -2020-12-24 18:45:00,110.48,197.765,68.803,32.468 -2020-12-24 19:00:00,111.49,198.856,72.934,32.468 -2020-12-24 19:15:00,106.52,195.25599999999997,72.934,32.468 -2020-12-24 19:30:00,104.08,193.234,72.934,32.468 -2020-12-24 19:45:00,102.68,189.304,72.934,32.468 -2020-12-24 20:00:00,96.72,185.8,65.175,32.468 -2020-12-24 20:15:00,96.77,180.084,65.175,32.468 -2020-12-24 20:30:00,91.18,176.25400000000002,65.175,32.468 -2020-12-24 20:45:00,89.83,173.66,65.175,32.468 -2020-12-24 21:00:00,86.22,171.545,58.55,32.468 -2020-12-24 21:15:00,85.58,169.31400000000002,58.55,32.468 -2020-12-24 21:30:00,83.25,167.188,58.55,32.468 -2020-12-24 21:45:00,85.54,165.68599999999998,58.55,32.468 -2020-12-24 22:00:00,76.93,159.151,55.041000000000004,32.468 -2020-12-24 22:15:00,77.34,153.755,55.041000000000004,32.468 -2020-12-24 22:30:00,73.53,140.047,55.041000000000004,32.468 -2020-12-24 22:45:00,72.69,132.249,55.041000000000004,32.468 -2020-12-24 23:00:00,68.02,126.61,48.258,32.468 -2020-12-24 23:15:00,68.11,124.535,48.258,32.468 -2020-12-24 23:30:00,64.77,124.944,48.258,32.468 -2020-12-24 23:45:00,67.67,124.384,48.258,32.468 -2020-12-25 00:00:00,57.87,114.70100000000001,32.311,32.468 -2020-12-25 00:15:00,58.21,109.835,32.311,32.468 -2020-12-25 00:30:00,57.43,111.413,32.311,32.468 -2020-12-25 00:45:00,57.71,113.917,32.311,32.468 -2020-12-25 01:00:00,54.98,116.509,25.569000000000003,32.468 -2020-12-25 01:15:00,55.19,118.25200000000001,25.569000000000003,32.468 -2020-12-25 01:30:00,54.78,118.568,25.569000000000003,32.468 -2020-12-25 01:45:00,53.55,118.613,25.569000000000003,32.468 -2020-12-25 02:00:00,50.76,120.314,21.038,32.468 -2020-12-25 02:15:00,52.54,120.425,21.038,32.468 -2020-12-25 02:30:00,49.88,120.809,21.038,32.468 -2020-12-25 02:45:00,52.68,123.17399999999999,21.038,32.468 -2020-12-25 03:00:00,51.32,125.52600000000001,19.865,32.468 -2020-12-25 03:15:00,51.45,125.884,19.865,32.468 -2020-12-25 03:30:00,52.01,127.59,19.865,32.468 -2020-12-25 03:45:00,52.84,128.874,19.865,32.468 -2020-12-25 04:00:00,52.17,137.297,19.076,32.468 -2020-12-25 04:15:00,52.24,145.708,19.076,32.468 -2020-12-25 04:30:00,53.63,146.046,19.076,32.468 -2020-12-25 04:45:00,54.09,147.187,19.076,32.468 -2020-12-25 05:00:00,53.22,160.945,20.174,32.468 -2020-12-25 05:15:00,54.53,169.99900000000002,20.174,32.468 -2020-12-25 05:30:00,53.56,167.02200000000002,20.174,32.468 -2020-12-25 05:45:00,57.2,164.69400000000002,20.174,32.468 -2020-12-25 06:00:00,57.45,179.6,19.854,32.468 -2020-12-25 06:15:00,58.55,197.949,19.854,32.468 -2020-12-25 06:30:00,59.92,193.317,19.854,32.468 -2020-12-25 06:45:00,61.05,188.095,19.854,32.468 -2020-12-25 07:00:00,63.95,185.90599999999998,23.096999999999998,32.468 -2020-12-25 07:15:00,65.26,189.485,23.096999999999998,32.468 -2020-12-25 07:30:00,66.28,193.239,23.096999999999998,32.468 -2020-12-25 07:45:00,69.33,196.55,23.096999999999998,32.468 -2020-12-25 08:00:00,71.62,200.118,30.849,32.468 -2020-12-25 08:15:00,74.42,202.94400000000002,30.849,32.468 -2020-12-25 08:30:00,76.82,205.296,30.849,32.468 -2020-12-25 08:45:00,78.68,205.364,30.849,32.468 -2020-12-25 09:00:00,81.32,200.285,30.03,32.468 -2020-12-25 09:15:00,83.82,198.69799999999998,30.03,32.468 -2020-12-25 09:30:00,85.11,196.558,30.03,32.468 -2020-12-25 09:45:00,87.16,193.717,30.03,32.468 -2020-12-25 10:00:00,87.91,191.055,27.625999999999998,32.468 -2020-12-25 10:15:00,89.23,187.868,27.625999999999998,32.468 -2020-12-25 10:30:00,91.55,186.21,27.625999999999998,32.468 -2020-12-25 10:45:00,93.17,183.923,27.625999999999998,32.468 -2020-12-25 11:00:00,94.65,183.926,29.03,32.468 -2020-12-25 11:15:00,98.58,181.39700000000002,29.03,32.468 -2020-12-25 11:30:00,100.6,180.172,29.03,32.468 -2020-12-25 11:45:00,100.05,178.365,29.03,32.468 -2020-12-25 12:00:00,95.72,172.512,25.93,32.468 -2020-12-25 12:15:00,92.54,172.11900000000003,25.93,32.468 -2020-12-25 12:30:00,88.4,171.47,25.93,32.468 -2020-12-25 12:45:00,84.51,171.19299999999998,25.93,32.468 -2020-12-25 13:00:00,81.0,169.72099999999998,16.363,32.468 -2020-12-25 13:15:00,80.85,170.907,16.363,32.468 -2020-12-25 13:30:00,80.27,169.92700000000002,16.363,32.468 -2020-12-25 13:45:00,80.22,169.51,16.363,32.468 -2020-12-25 14:00:00,77.57,168.797,14.370999999999999,32.468 -2020-12-25 14:15:00,78.09,169.671,14.370999999999999,32.468 -2020-12-25 14:30:00,77.16,170.231,14.370999999999999,32.468 -2020-12-25 14:45:00,76.87,170.32299999999998,14.370999999999999,32.468 -2020-12-25 15:00:00,77.34,170.106,19.031,32.468 -2020-12-25 15:15:00,76.88,171.554,19.031,32.468 -2020-12-25 15:30:00,76.93,174.22,19.031,32.468 -2020-12-25 15:45:00,78.2,176.516,19.031,32.468 -2020-12-25 16:00:00,81.38,178.011,24.998,32.468 -2020-12-25 16:15:00,82.28,179.81900000000002,24.998,32.468 -2020-12-25 16:30:00,86.93,182.61,24.998,32.468 -2020-12-25 16:45:00,86.17,184.921,24.998,32.468 -2020-12-25 17:00:00,89.63,187.218,35.976,32.468 -2020-12-25 17:15:00,87.98,189.021,35.976,32.468 -2020-12-25 17:30:00,89.65,189.84099999999998,35.976,32.468 -2020-12-25 17:45:00,90.91,191.3,35.976,32.468 -2020-12-25 18:00:00,91.63,192.454,41.513000000000005,32.468 -2020-12-25 18:15:00,90.25,193.15400000000002,41.513000000000005,32.468 -2020-12-25 18:30:00,91.06,191.364,41.513000000000005,32.468 -2020-12-25 18:45:00,91.59,189.735,41.513000000000005,32.468 -2020-12-25 19:00:00,90.53,192.299,45.607,32.468 -2020-12-25 19:15:00,89.44,190.046,45.607,32.468 -2020-12-25 19:30:00,88.41,188.205,45.607,32.468 -2020-12-25 19:45:00,87.49,185.03900000000002,45.607,32.468 -2020-12-25 20:00:00,85.33,183.676,43.372,32.468 -2020-12-25 20:15:00,84.64,180.94400000000002,43.372,32.468 -2020-12-25 20:30:00,83.34,177.888,43.372,32.468 -2020-12-25 20:45:00,82.68,174.28599999999997,43.372,32.468 -2020-12-25 21:00:00,80.07,172.31599999999997,39.458,32.468 -2020-12-25 21:15:00,79.28,170.345,39.458,32.468 -2020-12-25 21:30:00,78.47,169.702,39.458,32.468 -2020-12-25 21:45:00,77.69,168.477,39.458,32.468 -2020-12-25 22:00:00,74.04,163.124,40.15,32.468 -2020-12-25 22:15:00,75.14,159.202,40.15,32.468 -2020-12-25 22:30:00,71.33,154.743,40.15,32.468 -2020-12-25 22:45:00,71.18,151.332,40.15,32.468 -2020-12-25 23:00:00,67.24,144.92,33.876999999999995,32.468 -2020-12-25 23:15:00,66.92,141.184,33.876999999999995,32.468 -2020-12-25 23:30:00,63.35,139.3,33.876999999999995,32.468 -2020-12-25 23:45:00,62.32,136.632,33.876999999999995,32.468 -2020-12-26 00:00:00,57.9,114.9,32.311,32.468 -2020-12-26 00:15:00,58.19,110.01,32.311,32.468 -2020-12-26 00:30:00,57.69,111.57700000000001,32.311,32.468 -2020-12-26 00:45:00,58.18,114.064,32.311,32.468 -2020-12-26 01:00:00,54.2,116.675,25.569000000000003,32.468 -2020-12-26 01:15:00,53.83,118.415,25.569000000000003,32.468 -2020-12-26 01:30:00,54.04,118.73299999999999,25.569000000000003,32.468 -2020-12-26 01:45:00,53.35,118.766,25.569000000000003,32.468 -2020-12-26 02:00:00,50.27,120.48200000000001,21.038,32.468 -2020-12-26 02:15:00,50.84,120.59299999999999,21.038,32.468 -2020-12-26 02:30:00,50.18,120.984,21.038,32.468 -2020-12-26 02:45:00,49.92,123.348,21.038,32.468 -2020-12-26 03:00:00,50.44,125.69200000000001,19.865,32.468 -2020-12-26 03:15:00,51.13,126.073,19.865,32.468 -2020-12-26 03:30:00,51.68,127.77799999999999,19.865,32.468 -2020-12-26 03:45:00,52.19,129.064,19.865,32.468 -2020-12-26 04:00:00,52.65,137.464,19.076,32.468 -2020-12-26 04:15:00,51.74,145.872,19.076,32.468 -2020-12-26 04:30:00,52.08,146.203,19.076,32.468 -2020-12-26 04:45:00,52.91,147.342,19.076,32.468 -2020-12-26 05:00:00,52.87,161.06799999999998,20.174,32.468 -2020-12-26 05:15:00,54.46,170.08900000000003,20.174,32.468 -2020-12-26 05:30:00,54.44,167.12099999999998,20.174,32.468 -2020-12-26 05:45:00,56.77,164.81099999999998,20.174,32.468 -2020-12-26 06:00:00,56.75,179.74099999999999,19.854,32.468 -2020-12-26 06:15:00,60.38,198.097,19.854,32.468 -2020-12-26 06:30:00,58.83,193.49,19.854,32.468 -2020-12-26 06:45:00,59.86,188.3,19.854,32.468 -2020-12-26 07:00:00,62.28,186.122,23.096999999999998,32.468 -2020-12-26 07:15:00,63.68,189.696,23.096999999999998,32.468 -2020-12-26 07:30:00,64.23,193.442,23.096999999999998,32.468 -2020-12-26 07:45:00,69.19,196.74200000000002,23.096999999999998,32.468 -2020-12-26 08:00:00,69.13,200.31099999999998,28.963,32.468 -2020-12-26 08:15:00,74.4,203.125,28.963,32.468 -2020-12-26 08:30:00,73.12,205.46400000000003,28.963,32.468 -2020-12-26 08:45:00,75.14,205.50900000000001,28.963,32.468 -2020-12-26 09:00:00,78.03,200.41099999999997,28.194000000000003,32.468 -2020-12-26 09:15:00,80.29,198.832,28.194000000000003,32.468 -2020-12-26 09:30:00,83.3,196.702,28.194000000000003,32.468 -2020-12-26 09:45:00,81.36,193.852,28.194000000000003,32.468 -2020-12-26 10:00:00,86.11,191.188,25.936999999999998,32.468 -2020-12-26 10:15:00,85.18,187.99400000000003,25.936999999999998,32.468 -2020-12-26 10:30:00,89.16,186.321,25.936999999999998,32.468 -2020-12-26 10:45:00,88.48,184.033,25.936999999999998,32.468 -2020-12-26 11:00:00,92.1,184.018,27.256,32.468 -2020-12-26 11:15:00,93.15,181.483,27.256,32.468 -2020-12-26 11:30:00,95.06,180.26,27.256,32.468 -2020-12-26 11:45:00,97.74,178.452,27.256,32.468 -2020-12-26 12:00:00,92.69,172.60299999999998,24.345,32.468 -2020-12-26 12:15:00,92.99,172.22400000000002,24.345,32.468 -2020-12-26 12:30:00,85.85,171.58,24.345,32.468 -2020-12-26 12:45:00,84.15,171.30599999999998,24.345,32.468 -2020-12-26 13:00:00,84.26,169.81599999999997,15.363,32.468 -2020-12-26 13:15:00,80.45,170.998,15.363,32.468 -2020-12-26 13:30:00,78.78,170.00900000000001,15.363,32.468 -2020-12-26 13:45:00,79.31,169.584,15.363,32.468 -2020-12-26 14:00:00,76.77,168.87099999999998,13.492,32.468 -2020-12-26 14:15:00,76.41,169.743,13.492,32.468 -2020-12-26 14:30:00,76.48,170.31900000000002,13.492,32.468 -2020-12-26 14:45:00,77.52,170.423,13.492,32.468 -2020-12-26 15:00:00,78.64,170.22299999999998,17.868,32.468 -2020-12-26 15:15:00,80.39,171.66,17.868,32.468 -2020-12-26 15:30:00,77.92,174.332,17.868,32.468 -2020-12-26 15:45:00,79.11,176.623,17.868,32.468 -2020-12-26 16:00:00,86.11,178.118,23.47,32.468 -2020-12-26 16:15:00,83.6,179.93900000000002,23.47,32.468 -2020-12-26 16:30:00,87.85,182.736,23.47,32.468 -2020-12-26 16:45:00,86.53,185.065,23.47,32.468 -2020-12-26 17:00:00,89.38,187.34400000000002,33.777,32.468 -2020-12-26 17:15:00,90.4,189.174,33.777,32.468 -2020-12-26 17:30:00,94.34,190.016,33.777,32.468 -2020-12-26 17:45:00,92.08,191.488,33.777,32.468 -2020-12-26 18:00:00,93.83,192.66099999999997,38.975,32.468 -2020-12-26 18:15:00,92.14,193.35299999999998,38.975,32.468 -2020-12-26 18:30:00,92.33,191.56799999999998,38.975,32.468 -2020-12-26 18:45:00,93.19,189.954,38.975,32.468 -2020-12-26 19:00:00,91.96,192.495,42.818999999999996,32.468 -2020-12-26 19:15:00,91.71,190.238,42.818999999999996,32.468 -2020-12-26 19:30:00,90.43,188.393,42.818999999999996,32.468 -2020-12-26 19:45:00,90.65,185.217,42.818999999999996,32.468 -2020-12-26 20:00:00,86.8,183.845,43.372,32.468 -2020-12-26 20:15:00,86.59,181.109,43.372,32.468 -2020-12-26 20:30:00,84.5,178.03799999999998,43.372,32.468 -2020-12-26 20:45:00,83.84,174.453,43.372,32.468 -2020-12-26 21:00:00,79.9,172.46599999999998,39.458,32.468 -2020-12-26 21:15:00,79.31,170.479,39.458,32.468 -2020-12-26 21:30:00,79.36,169.83700000000002,39.458,32.468 -2020-12-26 21:45:00,81.72,168.62400000000002,39.458,32.468 -2020-12-26 22:00:00,74.52,163.27200000000002,40.15,32.468 -2020-12-26 22:15:00,74.0,159.359,40.15,32.468 -2020-12-26 22:30:00,72.07,154.929,40.15,32.468 -2020-12-26 22:45:00,70.15,151.52700000000002,40.15,32.468 -2020-12-26 23:00:00,66.32,145.092,33.876999999999995,32.468 -2020-12-26 23:15:00,66.62,141.359,33.876999999999995,32.468 -2020-12-26 23:30:00,63.94,139.489,33.876999999999995,32.468 -2020-12-26 23:45:00,64.95,136.814,33.876999999999995,32.468 -2020-12-27 00:00:00,56.66,115.09299999999999,35.232,32.468 -2020-12-27 00:15:00,57.29,110.179,35.232,32.468 -2020-12-27 00:30:00,56.01,111.735,35.232,32.468 -2020-12-27 00:45:00,54.37,114.205,35.232,32.468 -2020-12-27 01:00:00,52.25,116.833,31.403000000000002,32.468 -2020-12-27 01:15:00,53.32,118.569,31.403000000000002,32.468 -2020-12-27 01:30:00,52.46,118.88799999999999,31.403000000000002,32.468 -2020-12-27 01:45:00,51.48,118.911,31.403000000000002,32.468 -2020-12-27 02:00:00,49.93,120.641,30.69,32.468 -2020-12-27 02:15:00,51.57,120.75299999999999,30.69,32.468 -2020-12-27 02:30:00,50.82,121.15,30.69,32.468 -2020-12-27 02:45:00,50.77,123.514,30.69,32.468 -2020-12-27 03:00:00,49.34,125.84899999999999,29.516,32.468 -2020-12-27 03:15:00,50.62,126.25200000000001,29.516,32.468 -2020-12-27 03:30:00,51.39,127.959,29.516,32.468 -2020-12-27 03:45:00,51.75,129.248,29.516,32.468 -2020-12-27 04:00:00,50.79,137.624,29.148000000000003,32.468 -2020-12-27 04:15:00,51.03,146.02700000000002,29.148000000000003,32.468 -2020-12-27 04:30:00,51.16,146.35299999999998,29.148000000000003,32.468 -2020-12-27 04:45:00,52.11,147.49,29.148000000000003,32.468 -2020-12-27 05:00:00,52.51,161.184,28.706,32.468 -2020-12-27 05:15:00,53.68,170.172,28.706,32.468 -2020-12-27 05:30:00,52.91,167.213,28.706,32.468 -2020-12-27 05:45:00,54.55,164.921,28.706,32.468 -2020-12-27 06:00:00,55.49,179.87400000000002,28.771,32.468 -2020-12-27 06:15:00,55.76,198.235,28.771,32.468 -2020-12-27 06:30:00,56.15,193.65400000000002,28.771,32.468 -2020-12-27 06:45:00,57.92,188.49599999999998,28.771,32.468 -2020-12-27 07:00:00,60.11,186.328,31.39,32.468 -2020-12-27 07:15:00,61.04,189.898,31.39,32.468 -2020-12-27 07:30:00,65.58,193.636,31.39,32.468 -2020-12-27 07:45:00,65.21,196.923,31.39,32.468 -2020-12-27 08:00:00,66.03,200.493,34.972,32.468 -2020-12-27 08:15:00,66.67,203.295,34.972,32.468 -2020-12-27 08:30:00,68.78,205.62,34.972,32.468 -2020-12-27 08:45:00,72.45,205.642,34.972,32.468 -2020-12-27 09:00:00,73.81,200.525,36.709,32.468 -2020-12-27 09:15:00,75.71,198.952,36.709,32.468 -2020-12-27 09:30:00,75.67,196.835,36.709,32.468 -2020-12-27 09:45:00,76.59,193.976,36.709,32.468 -2020-12-27 10:00:00,77.56,191.308,35.812,32.468 -2020-12-27 10:15:00,79.49,188.108,35.812,32.468 -2020-12-27 10:30:00,81.12,186.422,35.812,32.468 -2020-12-27 10:45:00,83.85,184.13299999999998,35.812,32.468 -2020-12-27 11:00:00,84.02,184.101,36.746,32.468 -2020-12-27 11:15:00,86.47,181.56,36.746,32.468 -2020-12-27 11:30:00,87.71,180.338,36.746,32.468 -2020-12-27 11:45:00,88.37,178.52900000000002,36.746,32.468 -2020-12-27 12:00:00,87.6,172.685,35.048,32.468 -2020-12-27 12:15:00,85.66,172.321,35.048,32.468 -2020-12-27 12:30:00,83.94,171.68,35.048,32.468 -2020-12-27 12:45:00,84.23,171.40900000000002,35.048,32.468 -2020-12-27 13:00:00,81.23,169.90400000000002,29.987,32.468 -2020-12-27 13:15:00,79.67,171.08,29.987,32.468 -2020-12-27 13:30:00,78.24,170.083,29.987,32.468 -2020-12-27 13:45:00,77.15,169.65,29.987,32.468 -2020-12-27 14:00:00,73.27,168.938,27.21,32.468 -2020-12-27 14:15:00,75.35,169.808,27.21,32.468 -2020-12-27 14:30:00,74.83,170.398,27.21,32.468 -2020-12-27 14:45:00,74.81,170.515,27.21,32.468 -2020-12-27 15:00:00,75.04,170.331,27.726999999999997,32.468 -2020-12-27 15:15:00,76.57,171.757,27.726999999999997,32.468 -2020-12-27 15:30:00,77.1,174.43400000000003,27.726999999999997,32.468 -2020-12-27 15:45:00,79.95,176.72,27.726999999999997,32.468 -2020-12-27 16:00:00,85.35,178.213,32.23,32.468 -2020-12-27 16:15:00,85.01,180.049,32.23,32.468 -2020-12-27 16:30:00,85.83,182.852,32.23,32.468 -2020-12-27 16:45:00,87.36,185.197,32.23,32.468 -2020-12-27 17:00:00,89.42,187.457,42.016999999999996,32.468 -2020-12-27 17:15:00,89.72,189.315,42.016999999999996,32.468 -2020-12-27 17:30:00,91.66,190.179,42.016999999999996,32.468 -2020-12-27 17:45:00,92.42,191.665,42.016999999999996,32.468 -2020-12-27 18:00:00,93.13,192.858,49.338,32.468 -2020-12-27 18:15:00,92.27,193.541,49.338,32.468 -2020-12-27 18:30:00,93.04,191.763,49.338,32.468 -2020-12-27 18:45:00,92.9,190.16299999999998,49.338,32.468 -2020-12-27 19:00:00,91.41,192.68,52.369,32.468 -2020-12-27 19:15:00,90.74,190.42,52.369,32.468 -2020-12-27 19:30:00,89.58,188.57299999999998,52.369,32.468 -2020-12-27 19:45:00,88.24,185.388,52.369,32.468 -2020-12-27 20:00:00,84.87,184.005,50.405,32.468 -2020-12-27 20:15:00,84.15,181.267,50.405,32.468 -2020-12-27 20:30:00,82.18,178.18099999999998,50.405,32.468 -2020-12-27 20:45:00,80.95,174.613,50.405,32.468 -2020-12-27 21:00:00,77.99,172.61,46.235,32.468 -2020-12-27 21:15:00,79.18,170.606,46.235,32.468 -2020-12-27 21:30:00,78.7,169.965,46.235,32.468 -2020-12-27 21:45:00,80.23,168.764,46.235,32.468 -2020-12-27 22:00:00,76.73,163.412,46.861000000000004,32.468 -2020-12-27 22:15:00,73.06,159.51,46.861000000000004,32.468 -2020-12-27 22:30:00,70.7,155.107,46.861000000000004,32.468 -2020-12-27 22:45:00,70.0,151.71200000000002,46.861000000000004,32.468 -2020-12-27 23:00:00,65.04,145.257,41.302,32.468 -2020-12-27 23:15:00,65.95,141.525,41.302,32.468 -2020-12-27 23:30:00,64.23,139.66899999999998,41.302,32.468 -2020-12-27 23:45:00,63.33,136.99,41.302,32.468 -2020-12-28 00:00:00,57.03,119.344,37.164,32.468 -2020-12-28 00:15:00,57.21,117.274,37.164,32.468 -2020-12-28 00:30:00,56.31,118.925,37.164,32.468 -2020-12-28 00:45:00,56.35,120.848,37.164,32.468 -2020-12-28 01:00:00,52.26,123.51899999999999,34.994,32.468 -2020-12-28 01:15:00,53.0,124.74799999999999,34.994,32.468 -2020-12-28 01:30:00,52.53,125.137,34.994,32.468 -2020-12-28 01:45:00,52.32,125.25399999999999,34.994,32.468 -2020-12-28 02:00:00,51.03,126.993,34.571,32.468 -2020-12-28 02:15:00,52.03,128.454,34.571,32.468 -2020-12-28 02:30:00,50.99,129.18200000000002,34.571,32.468 -2020-12-28 02:45:00,50.55,130.94799999999998,34.571,32.468 -2020-12-28 03:00:00,49.6,134.501,33.934,32.468 -2020-12-28 03:15:00,51.33,136.524,33.934,32.468 -2020-12-28 03:30:00,50.91,138.001,33.934,32.468 -2020-12-28 03:45:00,51.45,138.749,33.934,32.468 -2020-12-28 04:00:00,51.82,151.371,34.107,32.468 -2020-12-28 04:15:00,52.15,163.84,34.107,32.468 -2020-12-28 04:30:00,53.03,166.226,34.107,32.468 -2020-12-28 04:45:00,54.7,167.53099999999998,34.107,32.468 -2020-12-28 05:00:00,57.21,196.475,39.575,32.468 -2020-12-28 05:15:00,58.21,225.044,39.575,32.468 -2020-12-28 05:30:00,58.82,222.28799999999998,39.575,32.468 -2020-12-28 05:45:00,60.69,214.507,39.575,32.468 -2020-12-28 06:00:00,63.74,211.99599999999998,56.156000000000006,32.468 -2020-12-28 06:15:00,64.22,215.868,56.156000000000006,32.468 -2020-12-28 06:30:00,66.48,219.101,56.156000000000006,32.468 -2020-12-28 06:45:00,67.97,222.567,56.156000000000006,32.468 -2020-12-28 07:00:00,71.66,222.68099999999998,67.926,32.468 -2020-12-28 07:15:00,73.71,227.53,67.926,32.468 -2020-12-28 07:30:00,73.74,230.484,67.926,32.468 -2020-12-28 07:45:00,76.15,231.332,67.926,32.468 -2020-12-28 08:00:00,78.92,230.185,72.58,32.468 -2020-12-28 08:15:00,77.89,230.88400000000001,72.58,32.468 -2020-12-28 08:30:00,79.51,229.27,72.58,32.468 -2020-12-28 08:45:00,81.98,226.108,72.58,32.468 -2020-12-28 09:00:00,84.91,219.989,66.984,32.468 -2020-12-28 09:15:00,86.68,215.032,66.984,32.468 -2020-12-28 09:30:00,86.35,211.93599999999998,66.984,32.468 -2020-12-28 09:45:00,87.57,209.25400000000002,66.984,32.468 -2020-12-28 10:00:00,88.75,205.554,63.158,32.468 -2020-12-28 10:15:00,89.35,202.032,63.158,32.468 -2020-12-28 10:30:00,90.53,199.482,63.158,32.468 -2020-12-28 10:45:00,92.46,197.829,63.158,32.468 -2020-12-28 11:00:00,92.51,195.31900000000002,61.141000000000005,32.468 -2020-12-28 11:15:00,93.89,194.49599999999998,61.141000000000005,32.468 -2020-12-28 11:30:00,95.76,194.61599999999999,61.141000000000005,32.468 -2020-12-28 11:45:00,98.55,192.44299999999998,61.141000000000005,32.468 -2020-12-28 12:00:00,96.46,188.148,57.961000000000006,32.468 -2020-12-28 12:15:00,96.89,187.805,57.961000000000006,32.468 -2020-12-28 12:30:00,94.61,187.355,57.961000000000006,32.468 -2020-12-28 12:45:00,93.99,188.53,57.961000000000006,32.468 -2020-12-28 13:00:00,91.48,187.59599999999998,56.843,32.468 -2020-12-28 13:15:00,89.62,187.41299999999998,56.843,32.468 -2020-12-28 13:30:00,87.6,185.91299999999998,56.843,32.468 -2020-12-28 13:45:00,89.12,185.547,56.843,32.468 -2020-12-28 14:00:00,86.23,184.21599999999998,55.992,32.468 -2020-12-28 14:15:00,84.24,184.503,55.992,32.468 -2020-12-28 14:30:00,84.27,184.58700000000002,55.992,32.468 -2020-12-28 14:45:00,84.15,184.78400000000002,55.992,32.468 -2020-12-28 15:00:00,83.99,186.304,57.523,32.468 -2020-12-28 15:15:00,84.98,186.33,57.523,32.468 -2020-12-28 15:30:00,82.61,188.26,57.523,32.468 -2020-12-28 15:45:00,84.31,190.09599999999998,57.523,32.468 -2020-12-28 16:00:00,90.69,191.83599999999998,59.471000000000004,32.468 -2020-12-28 16:15:00,89.04,192.96099999999998,59.471000000000004,32.468 -2020-12-28 16:30:00,91.46,194.821,59.471000000000004,32.468 -2020-12-28 16:45:00,92.1,196.054,59.471000000000004,32.468 -2020-12-28 17:00:00,95.79,198.107,65.066,32.468 -2020-12-28 17:15:00,96.36,199.09599999999998,65.066,32.468 -2020-12-28 17:30:00,94.64,199.445,65.066,32.468 -2020-12-28 17:45:00,94.08,199.503,65.066,32.468 -2020-12-28 18:00:00,92.03,201.101,69.581,32.468 -2020-12-28 18:15:00,92.63,199.59799999999998,69.581,32.468 -2020-12-28 18:30:00,92.68,198.426,69.581,32.468 -2020-12-28 18:45:00,95.26,197.632,69.581,32.468 -2020-12-28 19:00:00,91.26,198.58,73.771,32.468 -2020-12-28 19:15:00,88.66,195.235,73.771,32.468 -2020-12-28 19:30:00,89.79,193.85,73.771,32.468 -2020-12-28 19:45:00,85.86,189.838,73.771,32.468 -2020-12-28 20:00:00,81.83,186.13400000000001,65.035,32.468 -2020-12-28 20:15:00,80.18,181.06099999999998,65.035,32.468 -2020-12-28 20:30:00,78.48,176.19099999999997,65.035,32.468 -2020-12-28 20:45:00,77.08,174.206,65.035,32.468 -2020-12-28 21:00:00,73.88,172.676,58.7,32.468 -2020-12-28 21:15:00,73.68,169.542,58.7,32.468 -2020-12-28 21:30:00,72.87,168.12400000000002,58.7,32.468 -2020-12-28 21:45:00,72.61,166.44099999999997,58.7,32.468 -2020-12-28 22:00:00,69.98,158.253,53.888000000000005,32.468 -2020-12-28 22:15:00,69.76,153.156,53.888000000000005,32.468 -2020-12-28 22:30:00,68.69,139.464,53.888000000000005,32.468 -2020-12-28 22:45:00,68.64,131.43200000000002,53.888000000000005,32.468 -2020-12-28 23:00:00,64.36,125.7,45.501999999999995,32.468 -2020-12-28 23:15:00,66.25,124.46799999999999,45.501999999999995,32.468 -2020-12-28 23:30:00,64.12,125.277,45.501999999999995,32.468 -2020-12-28 23:45:00,64.06,125.116,45.501999999999995,32.468 -2020-12-29 00:00:00,57.18,118.78299999999999,43.537,32.468 -2020-12-29 00:15:00,56.17,118.086,43.537,32.468 -2020-12-29 00:30:00,56.18,118.84700000000001,43.537,32.468 -2020-12-29 00:45:00,56.31,119.844,43.537,32.468 -2020-12-29 01:00:00,53.79,122.324,41.854,32.468 -2020-12-29 01:15:00,53.88,123.124,41.854,32.468 -2020-12-29 01:30:00,50.49,123.676,41.854,32.468 -2020-12-29 01:45:00,53.7,124.057,41.854,32.468 -2020-12-29 02:00:00,51.98,125.79,40.321,32.468 -2020-12-29 02:15:00,53.31,127.184,40.321,32.468 -2020-12-29 02:30:00,53.66,127.334,40.321,32.468 -2020-12-29 02:45:00,53.8,129.121,40.321,32.468 -2020-12-29 03:00:00,53.3,131.494,39.632,32.468 -2020-12-29 03:15:00,58.73,132.754,39.632,32.468 -2020-12-29 03:30:00,61.45,134.694,39.632,32.468 -2020-12-29 03:45:00,56.91,135.592,39.632,32.468 -2020-12-29 04:00:00,58.6,147.961,40.183,32.468 -2020-12-29 04:15:00,58.92,160.092,40.183,32.468 -2020-12-29 04:30:00,58.73,162.172,40.183,32.468 -2020-12-29 04:45:00,60.76,164.65200000000002,40.183,32.468 -2020-12-29 05:00:00,65.7,198.412,43.945,32.468 -2020-12-29 05:15:00,68.23,226.771,43.945,32.468 -2020-12-29 05:30:00,72.46,222.56099999999998,43.945,32.468 -2020-12-29 05:45:00,78.0,214.78,43.945,32.468 -2020-12-29 06:00:00,86.13,211.19,56.048,32.468 -2020-12-29 06:15:00,90.88,216.63400000000001,56.048,32.468 -2020-12-29 06:30:00,98.39,219.25900000000001,56.048,32.468 -2020-12-29 06:45:00,101.09,222.36700000000002,56.048,32.468 -2020-12-29 07:00:00,109.02,222.32,65.74,32.468 -2020-12-29 07:15:00,110.78,227.003,65.74,32.468 -2020-12-29 07:30:00,111.89,229.425,65.74,32.468 -2020-12-29 07:45:00,116.47,230.43099999999998,65.74,32.468 -2020-12-29 08:00:00,119.28,229.387,72.757,32.468 -2020-12-29 08:15:00,119.18,229.077,72.757,32.468 -2020-12-29 08:30:00,121.46,227.28400000000002,72.757,32.468 -2020-12-29 08:45:00,123.85,223.81599999999997,72.757,32.468 -2020-12-29 09:00:00,126.31,216.889,67.692,32.468 -2020-12-29 09:15:00,127.18,213.46900000000002,67.692,32.468 -2020-12-29 09:30:00,129.54,211.058,67.692,32.468 -2020-12-29 09:45:00,127.09,208.2,67.692,32.468 -2020-12-29 10:00:00,130.14,203.892,63.506,32.468 -2020-12-29 10:15:00,131.8,199.34099999999998,63.506,32.468 -2020-12-29 10:30:00,131.24,196.954,63.506,32.468 -2020-12-29 10:45:00,133.38,195.604,63.506,32.468 -2020-12-29 11:00:00,132.11,194.502,60.758,32.468 -2020-12-29 11:15:00,132.81,193.368,60.758,32.468 -2020-12-29 11:30:00,132.74,192.33700000000002,60.758,32.468 -2020-12-29 11:45:00,132.51,190.82,60.758,32.468 -2020-12-29 12:00:00,133.01,185.213,57.519,32.468 -2020-12-29 12:15:00,132.94,184.5,57.519,32.468 -2020-12-29 12:30:00,132.61,184.77200000000002,57.519,32.468 -2020-12-29 12:45:00,131.32,185.69400000000002,57.519,32.468 -2020-12-29 13:00:00,129.11,184.345,56.46,32.468 -2020-12-29 13:15:00,129.11,183.888,56.46,32.468 -2020-12-29 13:30:00,128.11,183.49900000000002,56.46,32.468 -2020-12-29 13:45:00,128.44,183.268,56.46,32.468 -2020-12-29 14:00:00,129.73,182.227,56.207,32.468 -2020-12-29 14:15:00,128.01,182.65400000000002,56.207,32.468 -2020-12-29 14:30:00,127.57,183.363,56.207,32.468 -2020-12-29 14:45:00,128.05,183.468,56.207,32.468 -2020-12-29 15:00:00,125.11,184.584,57.391999999999996,32.468 -2020-12-29 15:15:00,124.05,184.94799999999998,57.391999999999996,32.468 -2020-12-29 15:30:00,122.28,187.088,57.391999999999996,32.468 -2020-12-29 15:45:00,122.29,188.547,57.391999999999996,32.468 -2020-12-29 16:00:00,126.09,190.58,59.955,32.468 -2020-12-29 16:15:00,127.33,192.178,59.955,32.468 -2020-12-29 16:30:00,131.25,194.65200000000002,59.955,32.468 -2020-12-29 16:45:00,132.28,196.199,59.955,32.468 -2020-12-29 17:00:00,135.14,198.761,67.063,32.468 -2020-12-29 17:15:00,134.64,199.817,67.063,32.468 -2020-12-29 17:30:00,135.54,200.817,67.063,32.468 -2020-12-29 17:45:00,135.22,200.755,67.063,32.468 -2020-12-29 18:00:00,135.98,202.24900000000002,71.477,32.468 -2020-12-29 18:15:00,133.12,200.295,71.477,32.468 -2020-12-29 18:30:00,132.74,198.825,71.477,32.468 -2020-12-29 18:45:00,132.76,198.80900000000003,71.477,32.468 -2020-12-29 19:00:00,129.1,199.782,74.32,32.468 -2020-12-29 19:15:00,131.66,196.169,74.32,32.468 -2020-12-29 19:30:00,131.57,194.13,74.32,32.468 -2020-12-29 19:45:00,134.28,190.15400000000002,74.32,32.468 -2020-12-29 20:00:00,124.16,186.60299999999998,66.157,32.468 -2020-12-29 20:15:00,118.77,180.87400000000002,66.157,32.468 -2020-12-29 20:30:00,114.09,176.968,66.157,32.468 -2020-12-29 20:45:00,113.21,174.458,66.157,32.468 -2020-12-29 21:00:00,104.2,172.25900000000001,59.806000000000004,32.468 -2020-12-29 21:15:00,110.79,169.947,59.806000000000004,32.468 -2020-12-29 21:30:00,106.78,167.829,59.806000000000004,32.468 -2020-12-29 21:45:00,106.31,166.38299999999998,59.806000000000004,32.468 -2020-12-29 22:00:00,95.44,159.85399999999998,54.785,32.468 -2020-12-29 22:15:00,92.96,154.50799999999998,54.785,32.468 -2020-12-29 22:30:00,85.11,140.935,54.785,32.468 -2020-12-29 22:45:00,86.87,133.178,54.785,32.468 -2020-12-29 23:00:00,82.77,127.431,47.176,32.468 -2020-12-29 23:15:00,84.86,125.367,47.176,32.468 -2020-12-29 23:30:00,77.52,125.84200000000001,47.176,32.468 -2020-12-29 23:45:00,78.36,125.257,47.176,32.468 -2020-12-30 00:00:00,81.02,118.954,43.42,32.468 -2020-12-30 00:15:00,81.82,118.23299999999999,43.42,32.468 -2020-12-30 00:30:00,81.73,118.985,43.42,32.468 -2020-12-30 00:45:00,76.13,119.964,43.42,32.468 -2020-12-30 01:00:00,70.28,122.458,40.869,32.468 -2020-12-30 01:15:00,74.3,123.25299999999999,40.869,32.468 -2020-12-30 01:30:00,77.22,123.807,40.869,32.468 -2020-12-30 01:45:00,76.85,124.179,40.869,32.468 -2020-12-30 02:00:00,73.1,125.92399999999999,39.541,32.468 -2020-12-30 02:15:00,70.43,127.319,39.541,32.468 -2020-12-30 02:30:00,71.98,127.475,39.541,32.468 -2020-12-30 02:45:00,76.52,129.263,39.541,32.468 -2020-12-30 03:00:00,75.41,131.627,39.052,32.468 -2020-12-30 03:15:00,74.83,132.908,39.052,32.468 -2020-12-30 03:30:00,72.89,134.84799999999998,39.052,32.468 -2020-12-30 03:45:00,79.01,135.749,39.052,32.468 -2020-12-30 04:00:00,76.82,148.096,40.36,32.468 -2020-12-30 04:15:00,72.95,160.222,40.36,32.468 -2020-12-30 04:30:00,72.14,162.297,40.36,32.468 -2020-12-30 04:45:00,80.94,164.775,40.36,32.468 -2020-12-30 05:00:00,88.48,198.503,43.133,32.468 -2020-12-30 05:15:00,91.18,226.833,43.133,32.468 -2020-12-30 05:30:00,90.88,222.62900000000002,43.133,32.468 -2020-12-30 05:45:00,93.94,214.865,43.133,32.468 -2020-12-30 06:00:00,101.26,211.298,54.953,32.468 -2020-12-30 06:15:00,106.96,216.748,54.953,32.468 -2020-12-30 06:30:00,112.28,219.395,54.953,32.468 -2020-12-30 06:45:00,115.14,222.53400000000002,54.953,32.468 -2020-12-30 07:00:00,123.05,222.49900000000002,66.566,32.468 -2020-12-30 07:15:00,123.6,227.174,66.566,32.468 -2020-12-30 07:30:00,126.82,229.58599999999998,66.566,32.468 -2020-12-30 07:45:00,128.84,230.578,66.566,32.468 -2020-12-30 08:00:00,131.65,229.533,72.902,32.468 -2020-12-30 08:15:00,130.78,229.21,72.902,32.468 -2020-12-30 08:30:00,132.01,227.4,72.902,32.468 -2020-12-30 08:45:00,132.85,223.91099999999997,72.902,32.468 -2020-12-30 09:00:00,134.02,216.967,68.465,32.468 -2020-12-30 09:15:00,135.31,213.55200000000002,68.465,32.468 -2020-12-30 09:30:00,136.11,211.15400000000002,68.465,32.468 -2020-12-30 09:45:00,137.25,208.28900000000002,68.465,32.468 -2020-12-30 10:00:00,135.85,203.97799999999998,63.625,32.468 -2020-12-30 10:15:00,138.58,199.425,63.625,32.468 -2020-12-30 10:30:00,138.23,197.024,63.625,32.468 -2020-12-30 10:45:00,136.57,195.674,63.625,32.468 -2020-12-30 11:00:00,133.24,194.555,61.628,32.468 -2020-12-30 11:15:00,126.44,193.417,61.628,32.468 -2020-12-30 11:30:00,130.71,192.387,61.628,32.468 -2020-12-30 11:45:00,129.54,190.87099999999998,61.628,32.468 -2020-12-30 12:00:00,125.24,185.268,58.708999999999996,32.468 -2020-12-30 12:15:00,124.69,184.571,58.708999999999996,32.468 -2020-12-30 12:30:00,123.95,184.84400000000002,58.708999999999996,32.468 -2020-12-30 12:45:00,129.68,185.769,58.708999999999996,32.468 -2020-12-30 13:00:00,133.73,184.407,57.373000000000005,32.468 -2020-12-30 13:15:00,136.53,183.94400000000002,57.373000000000005,32.468 -2020-12-30 13:30:00,133.6,183.545,57.373000000000005,32.468 -2020-12-30 13:45:00,133.51,183.30599999999998,57.373000000000005,32.468 -2020-12-30 14:00:00,132.59,182.271,57.684,32.468 -2020-12-30 14:15:00,131.17,182.69400000000002,57.684,32.468 -2020-12-30 14:30:00,128.89,183.417,57.684,32.468 -2020-12-30 14:45:00,127.23,183.533,57.684,32.468 -2020-12-30 15:00:00,128.01,184.666,58.03,32.468 -2020-12-30 15:15:00,127.89,185.017,58.03,32.468 -2020-12-30 15:30:00,127.38,187.16,58.03,32.468 -2020-12-30 15:45:00,128.01,188.612,58.03,32.468 -2020-12-30 16:00:00,131.71,190.645,59.97,32.468 -2020-12-30 16:15:00,132.63,192.255,59.97,32.468 -2020-12-30 16:30:00,136.55,194.735,59.97,32.468 -2020-12-30 16:45:00,136.97,196.297,59.97,32.468 -2020-12-30 17:00:00,140.13,198.84099999999998,65.661,32.468 -2020-12-30 17:15:00,138.72,199.924,65.661,32.468 -2020-12-30 17:30:00,136.98,200.94799999999998,65.661,32.468 -2020-12-30 17:45:00,138.66,200.9,65.661,32.468 -2020-12-30 18:00:00,138.81,202.41299999999998,70.96300000000001,32.468 -2020-12-30 18:15:00,137.74,200.455,70.96300000000001,32.468 -2020-12-30 18:30:00,135.58,198.99099999999999,70.96300000000001,32.468 -2020-12-30 18:45:00,136.58,198.989,70.96300000000001,32.468 -2020-12-30 19:00:00,133.41,199.937,74.133,32.468 -2020-12-30 19:15:00,132.16,196.324,74.133,32.468 -2020-12-30 19:30:00,129.15,194.283,74.133,32.468 -2020-12-30 19:45:00,128.43,190.3,74.133,32.468 -2020-12-30 20:00:00,120.97,186.738,65.613,32.468 -2020-12-30 20:15:00,116.68,181.007,65.613,32.468 -2020-12-30 20:30:00,112.92,177.08900000000003,65.613,32.468 -2020-12-30 20:45:00,113.77,174.59599999999998,65.613,32.468 -2020-12-30 21:00:00,108.79,172.378,58.583,32.468 -2020-12-30 21:15:00,113.74,170.051,58.583,32.468 -2020-12-30 21:30:00,111.85,167.933,58.583,32.468 -2020-12-30 21:45:00,107.84,166.5,58.583,32.468 -2020-12-30 22:00:00,97.67,159.97,54.411,32.468 -2020-12-30 22:15:00,98.11,154.636,54.411,32.468 -2020-12-30 22:30:00,96.26,141.086,54.411,32.468 -2020-12-30 22:45:00,94.93,133.338,54.411,32.468 -2020-12-30 23:00:00,90.4,127.57,47.878,32.468 -2020-12-30 23:15:00,82.73,125.509,47.878,32.468 -2020-12-30 23:30:00,77.9,125.99799999999999,47.878,32.468 -2020-12-30 23:45:00,78.08,125.40899999999999,47.878,32.468 -2020-12-31 00:00:00,72.56,119.118,44.513000000000005,32.468 -2020-12-31 00:15:00,75.51,118.375,44.513000000000005,32.468 -2020-12-31 00:30:00,76.2,119.11399999999999,44.513000000000005,32.468 -2020-12-31 00:45:00,79.94,120.07600000000001,44.513000000000005,32.468 -2020-12-31 01:00:00,76.22,122.584,43.169,32.468 -2020-12-31 01:15:00,75.52,123.374,43.169,32.468 -2020-12-31 01:30:00,69.71,123.929,43.169,32.468 -2020-12-31 01:45:00,72.15,124.29,43.169,32.468 -2020-12-31 02:00:00,68.46,126.051,41.763999999999996,32.468 -2020-12-31 02:15:00,68.93,127.444,41.763999999999996,32.468 -2020-12-31 02:30:00,69.13,127.60799999999999,41.763999999999996,32.468 -2020-12-31 02:45:00,71.05,129.394,41.763999999999996,32.468 -2020-12-31 03:00:00,77.24,131.754,41.155,32.468 -2020-12-31 03:15:00,78.88,133.054,41.155,32.468 -2020-12-31 03:30:00,79.6,134.993,41.155,32.468 -2020-12-31 03:45:00,73.47,135.89700000000002,41.155,32.468 -2020-12-31 04:00:00,71.66,148.222,41.96,32.468 -2020-12-31 04:15:00,72.6,160.344,41.96,32.468 -2020-12-31 04:30:00,74.43,162.416,41.96,32.468 -2020-12-31 04:45:00,76.31,164.89,41.96,32.468 -2020-12-31 05:00:00,80.9,198.58599999999998,45.206,32.468 -2020-12-31 05:15:00,83.91,226.886,45.206,32.468 -2020-12-31 05:30:00,88.55,222.68900000000002,45.206,32.468 -2020-12-31 05:45:00,92.98,214.942,45.206,32.468 -2020-12-31 06:00:00,101.92,211.398,55.398999999999994,32.468 -2020-12-31 06:15:00,106.02,216.854,55.398999999999994,32.468 -2020-12-31 06:30:00,110.67,219.52200000000002,55.398999999999994,32.468 -2020-12-31 06:45:00,115.36,222.69,55.398999999999994,32.468 -2020-12-31 07:00:00,121.21,222.667,64.627,32.468 -2020-12-31 07:15:00,125.44,227.334,64.627,32.468 -2020-12-31 07:30:00,127.04,229.737,64.627,32.468 -2020-12-31 07:45:00,129.3,230.713,64.627,32.468 -2020-12-31 08:00:00,131.83,229.668,70.895,32.468 -2020-12-31 08:15:00,129.92,229.331,70.895,32.468 -2020-12-31 08:30:00,130.03,227.505,70.895,32.468 -2020-12-31 08:45:00,129.65,223.993,70.895,32.468 -2020-12-31 09:00:00,130.4,217.032,66.382,32.468 -2020-12-31 09:15:00,132.18,213.62400000000002,66.382,32.468 -2020-12-31 09:30:00,133.68,211.239,66.382,32.468 -2020-12-31 09:45:00,133.99,208.36700000000002,66.382,32.468 -2020-12-31 10:00:00,133.44,204.053,62.739,32.468 -2020-12-31 10:15:00,135.64,199.49599999999998,62.739,32.468 -2020-12-31 10:30:00,134.92,197.084,62.739,32.468 -2020-12-31 10:45:00,136.65,195.734,62.739,32.468 -2020-12-31 11:00:00,136.01,194.59799999999998,60.843,32.468 -2020-12-31 11:15:00,135.6,193.455,60.843,32.468 -2020-12-31 11:30:00,135.49,192.428,60.843,32.468 -2020-12-31 11:45:00,136.56,190.91299999999998,60.843,32.468 -2020-12-31 12:00:00,136.24,185.315,58.466,32.468 -2020-12-31 12:15:00,135.89,184.63299999999998,58.466,32.468 -2020-12-31 12:30:00,133.92,184.908,58.466,32.468 -2020-12-31 12:45:00,134.1,185.834,58.466,32.468 -2020-12-31 13:00:00,130.94,184.46,56.883,32.468 -2020-12-31 13:15:00,129.98,183.99,56.883,32.468 -2020-12-31 13:30:00,129.31,183.582,56.883,32.468 -2020-12-31 13:45:00,127.59,183.33599999999998,56.883,32.468 -2020-12-31 14:00:00,126.5,182.30700000000002,56.503,32.468 -2020-12-31 14:15:00,129.81,182.727,56.503,32.468 -2020-12-31 14:30:00,128.98,183.46099999999998,56.503,32.468 -2020-12-31 14:45:00,127.66,183.59,56.503,32.468 -2020-12-31 15:00:00,131.69,184.74,57.803999999999995,32.468 -2020-12-31 15:15:00,134.08,185.078,57.803999999999995,32.468 -2020-12-31 15:30:00,130.49,187.222,57.803999999999995,32.468 -2020-12-31 15:45:00,132.71,188.668,57.803999999999995,32.468 -2020-12-31 16:00:00,134.12,190.7,59.379,32.468 -2020-12-31 16:15:00,134.79,192.322,59.379,32.468 -2020-12-31 16:30:00,137.68,194.80599999999998,59.379,32.468 -2020-12-31 16:45:00,138.3,196.382,59.379,32.468 -2020-12-31 17:00:00,141.23,198.90900000000002,64.71600000000001,32.468 -2020-12-31 17:15:00,140.55,200.021,64.71600000000001,32.468 -2020-12-31 17:30:00,141.63,201.067,64.71600000000001,32.468 -2020-12-31 17:45:00,141.55,201.035,64.71600000000001,32.468 -2020-12-31 18:00:00,140.02,202.56799999999998,68.803,32.468 -2020-12-31 18:15:00,138.28,200.606,68.803,32.468 -2020-12-31 18:30:00,137.19,199.14700000000002,68.803,32.468 -2020-12-31 18:45:00,137.6,199.16,68.803,32.468 -2020-12-31 19:00:00,134.26,200.084,72.934,32.468 -2020-12-31 19:15:00,133.22,196.468,72.934,32.468 -2020-12-31 19:30:00,134.64,194.42700000000002,72.934,32.468 -2020-12-31 19:45:00,136.88,190.43900000000002,72.934,32.468 -2020-12-31 20:00:00,130.24,186.864,65.175,32.468 -2020-12-31 20:15:00,119.33,181.132,65.175,32.468 -2020-12-31 20:30:00,118.31,177.201,65.175,32.468 -2020-12-31 20:45:00,112.7,174.72400000000002,65.175,32.468 -2020-12-31 21:00:00,107.44,172.49,58.55,32.468 -2020-12-31 21:15:00,112.86,170.146,58.55,32.468 -2020-12-31 21:30:00,111.93,168.03,58.55,32.468 -2020-12-31 21:45:00,108.29,166.609,58.55,32.468 -2020-12-31 22:00:00,98.52,160.079,55.041000000000004,32.468 -2020-12-31 22:15:00,94.85,154.757,55.041000000000004,32.468 -2020-12-31 22:30:00,98.1,141.22899999999998,55.041000000000004,32.468 -2020-12-31 22:45:00,96.62,133.488,55.041000000000004,32.468 -2020-12-31 23:00:00,92.22,127.7,48.258,32.468 -2020-12-31 23:15:00,84.12,125.64200000000001,48.258,32.468 -2020-12-31 23:30:00,79.31,126.145,48.258,32.468 -2020-12-31 23:45:00,83.54,125.553,48.258,32.468 diff --git a/examples/05_Two-stage-optimization/two_stage_optimization.py b/examples/05_Two-stage-optimization/two_stage_optimization.py index eee8dd92d..dde3ae069 100644 --- a/examples/05_Two-stage-optimization/two_stage_optimization.py +++ b/examples/05_Two-stage-optimization/two_stage_optimization.py @@ -1,9 +1,9 @@ """ This script demonstrates how to use downsampling of a FlowSystem to effectively reduce the size of a model. -This can be very useful when working with large models or during developement state, +This can be very useful when working with large models or during development, as it can drastically reduce the computational time. This leads to faster results and easier debugging. -A common use case is to do optimize the investments of a model with a downsampled version of the original model, and than fix the computed sizes when calculating th actual dispatch. +A common use case is to optimize the investments of a model with a downsampled version of the original model, and then fix the computed sizes when calculating the actual dispatch. While the final optimum might differ from the global optimum, the solving will be much faster. """ @@ -20,7 +20,9 @@ if __name__ == '__main__': # Data Import - data_import = pd.read_csv(pathlib.Path('Zeitreihen2020.csv'), index_col=0).sort_index() + data_import = pd.read_csv( + pathlib.Path(__file__).parent.parent / 'resources' / 'Zeitreihen2020.csv', index_col=0 + ).sort_index() filtered_data = data_import[:500] filtered_data.index = pd.to_datetime(filtered_data.index) @@ -48,7 +50,9 @@ Q_fu=fx.Flow( label='Q_fu', bus='Gas', - size=fx.InvestParameters(specific_effects={'costs': 1_000}, minimum_size=10, maximum_size=500), + size=fx.InvestParameters( + effects_of_investment_per_size={'costs': 1_000}, minimum_size=10, maximum_size=500 + ), relative_minimum=0.2, previous_flow_rate=20, on_off_parameters=fx.OnOffParameters(effects_per_switch_on=300), @@ -66,7 +70,9 @@ Q_fu=fx.Flow( 'Q_fu', bus='Kohle', - size=fx.InvestParameters(specific_effects={'costs': 3_000}, minimum_size=10, maximum_size=500), + size=fx.InvestParameters( + effects_of_investment_per_size={'costs': 3_000}, minimum_size=10, maximum_size=500 + ), relative_minimum=0.3, previous_flow_rate=100, ), @@ -74,7 +80,7 @@ fx.Storage( 'Speicher', capacity_in_flow_hours=fx.InvestParameters( - minimum_size=10, maximum_size=1000, specific_effects={'costs': 60} + minimum_size=10, maximum_size=1000, effects_of_investment_per_size={'costs': 60} ), initial_charge_state='lastValueOfSim', eta_charge=1, @@ -84,54 +90,59 @@ charging=fx.Flow('Q_th_load', size=137, bus='Fernwärme'), discharging=fx.Flow('Q_th_unload', size=158, bus='Fernwärme'), ), - fx.Sink('Wärmelast', sink=fx.Flow('Q_th_Last', bus='Fernwärme', size=1, fixed_relative_profile=heat_demand)), + fx.Sink( + 'Wärmelast', inputs=[fx.Flow('Q_th_Last', bus='Fernwärme', size=1, fixed_relative_profile=heat_demand)] + ), fx.Source( 'Gastarif', - source=fx.Flow('Q_Gas', bus='Gas', size=1000, effects_per_flow_hour={'costs': gas_price, 'CO2': 0.3}), + outputs=[fx.Flow('Q_Gas', bus='Gas', size=1000, effects_per_flow_hour={'costs': gas_price, 'CO2': 0.3})], ), fx.Source( 'Kohletarif', - source=fx.Flow('Q_Kohle', bus='Kohle', size=1000, effects_per_flow_hour={'costs': 4.6, 'CO2': 0.3}), + outputs=[fx.Flow('Q_Kohle', bus='Kohle', size=1000, effects_per_flow_hour={'costs': 4.6, 'CO2': 0.3})], ), fx.Source( 'Einspeisung', - source=fx.Flow( - 'P_el', bus='Strom', size=1000, effects_per_flow_hour={'costs': electricity_price + 0.5, 'CO2': 0.3} - ), + outputs=[ + fx.Flow( + 'P_el', bus='Strom', size=1000, effects_per_flow_hour={'costs': electricity_price + 0.5, 'CO2': 0.3} + ) + ], ), fx.Sink( 'Stromlast', - sink=fx.Flow('P_el_Last', bus='Strom', size=1, fixed_relative_profile=electricity_demand), + inputs=[fx.Flow('P_el_Last', bus='Strom', size=1, fixed_relative_profile=electricity_demand)], ), fx.Source( 'Stromtarif', - source=fx.Flow( - 'P_el', bus='Strom', size=1000, effects_per_flow_hour={'costs': electricity_price, 'CO2': 0.3} - ), + outputs=[ + fx.Flow('P_el', bus='Strom', size=1000, effects_per_flow_hour={'costs': electricity_price, 'CO2': 0.3}) + ], ), ) # Separate optimization of flow sizes and dispatch start = timeit.default_timer() - calculation_sizing = fx.FullCalculation('Sizing', flow_system.resample('4h')) + calculation_sizing = fx.FullCalculation('Sizing', flow_system.resample('2h')) calculation_sizing.do_modeling() - calculation_sizing.solve(fx.solvers.HighsSolver(0.1 / 100, 600)) + calculation_sizing.solve(fx.solvers.HighsSolver(0.1 / 100, 60)) timer_sizing = timeit.default_timer() - start - calculation_dispatch = fx.FullCalculation('Sizing', flow_system) + start = timeit.default_timer() + calculation_dispatch = fx.FullCalculation('Dispatch', flow_system) calculation_dispatch.do_modeling() calculation_dispatch.fix_sizes(calculation_sizing.results.solution) - calculation_dispatch.solve(fx.solvers.HighsSolver(0.1 / 100, 600)) + calculation_dispatch.solve(fx.solvers.HighsSolver(0.1 / 100, 60)) timer_dispatch = timeit.default_timer() - start - if (calculation_dispatch.results.sizes().round(5) == calculation_sizing.results.sizes().round(5)).all(): - logger.info('Sizes where correctly equalized') + if (calculation_dispatch.results.sizes().round(5) == calculation_sizing.results.sizes().round(5)).all().item(): + logger.info('Sizes were correctly equalized') else: - raise RuntimeError('Sizes where not correctly equalized') + raise RuntimeError('Sizes were not correctly equalized') # Optimization of both flow sizes and dispatch together start = timeit.default_timer() - calculation_combined = fx.FullCalculation('Sizing', flow_system) + calculation_combined = fx.FullCalculation('Combined', flow_system) calculation_combined.do_modeling() calculation_combined.solve(fx.solvers.HighsSolver(0.1 / 100, 600)) timer_combined = timeit.default_timer() - start @@ -145,9 +156,9 @@ comparison_main = comparison[ [ 'Duration [s]', - 'costs|total', - 'costs(invest)|total', - 'costs(operation)|total', + 'costs', + 'costs(periodic)', + 'costs(temporal)', 'BHKW2(Q_fu)|size', 'Kessel(Q_fu)|size', 'Speicher|size', diff --git a/examples/03_Calculation_types/Zeitreihen2020.csv b/examples/resources/Zeitreihen2020.csv similarity index 100% rename from examples/03_Calculation_types/Zeitreihen2020.csv rename to examples/resources/Zeitreihen2020.csv diff --git a/flixopt/__init__.py b/flixopt/__init__.py index aa2bd7129..3a52f7592 100644 --- a/flixopt/__init__.py +++ b/flixopt/__init__.py @@ -2,9 +2,14 @@ This module bundles all common functionality of flixopt and sets up the logging """ -from importlib.metadata import version +import warnings +from importlib.metadata import PackageNotFoundError, version -__version__ = version('flixopt') +try: + __version__ = version('flixopt') +except (PackageNotFoundError, TypeError): + # Package is not installed (development mode without editable install) + __version__ = '0.0.0.dev0' from .commons import ( CONFIG, @@ -37,4 +42,29 @@ solvers, ) -CONFIG.load_config() +# === Runtime warning suppression for third-party libraries === +# These warnings are from dependencies and cannot be fixed by end users. +# They are suppressed at runtime to provide a cleaner user experience. +# These filters match the test configuration in pyproject.toml for consistency. + +# tsam: Time series aggregation library +# - UserWarning: Informational message about minimal value constraints during clustering. +warnings.filterwarnings('ignore', category=UserWarning, message='.*minimal value.*exceeds.*', module='tsam') +# TODO: Might be able to fix it in flixopt? + +# linopy: Linear optimization library +# - UserWarning: Coordinate mismatch warnings that don't affect functionality and are expected. +warnings.filterwarnings( + 'ignore', category=UserWarning, message='Coordinates across variables not equal', module='linopy' +) +# - FutureWarning: join parameter default will change in future versions +warnings.filterwarnings( + 'ignore', + category=FutureWarning, + message="In a future version of xarray the default value for join will change from join='outer' to join='exact'", + module='linopy', +) + +# numpy: Core numerical library +# - RuntimeWarning: Binary incompatibility warnings from compiled extensions (safe to ignore). numpy 1->2 +warnings.filterwarnings('ignore', category=RuntimeWarning, message='numpy\\.ndarray size changed') diff --git a/flixopt/aggregation.py b/flixopt/aggregation.py index 5b9ee8a30..cd0fdde3c 100644 --- a/flixopt/aggregation.py +++ b/flixopt/aggregation.py @@ -20,7 +20,9 @@ except ImportError: TSAM_AVAILABLE = False +from .color_processing import process_colors from .components import Storage +from .config import CONFIG from .structure import ( FlowSystemModel, Submodel, @@ -141,7 +143,7 @@ def describe_clusters(self) -> str: def use_extreme_periods(self): return self.time_series_for_high_peaks or self.time_series_for_low_peaks - def plot(self, colormap: str = 'viridis', show: bool = True, save: pathlib.Path | None = None) -> go.Figure: + def plot(self, colormap: str | None = None, show: bool = True, save: pathlib.Path | None = None) -> go.Figure: from . import plotting df_org = self.original_data.copy().rename( @@ -150,13 +152,20 @@ def plot(self, colormap: str = 'viridis', show: bool = True, save: pathlib.Path df_agg = self.aggregated_data.copy().rename( columns={col: f'Aggregated - {col}' for col in self.aggregated_data.columns} ) - fig = plotting.with_plotly(df_org, 'line', colors=colormap) + colors = list( + process_colors(colormap or CONFIG.Plotting.default_qualitative_colorscale, list(df_org.columns)).values() + ) + fig = plotting.with_plotly(df_org.to_xarray(), 'line', colors=colors, xlabel='Time in h') for trace in fig.data: trace.update(dict(line=dict(dash='dash'))) - fig = plotting.with_plotly(df_agg, 'line', colors=colormap, fig=fig) + fig2 = plotting.with_plotly(df_agg.to_xarray(), 'line', colors=colors, xlabel='Time in h') + for trace in fig2.data: + fig.add_trace(trace) fig.update_layout( - title='Original vs Aggregated Data (original = ---)', xaxis_title='Index', yaxis_title='Value' + title='Original vs Aggregated Data (original = ---)', + xaxis_title='Time in h', + yaxis_title='Value', ) plotting.export_figure( @@ -286,7 +295,7 @@ def use_low_peaks(self) -> bool: class AggregationModel(Submodel): - """The AggregationModel holds equations and variables related to the Aggregation of a FLowSystem. + """The AggregationModel holds equations and variables related to the Aggregation of a FlowSystem. It creates Equations that equates indices of variables, and introduces penalties related to binary variables, that escape the equation to their related binaries in other periods""" @@ -315,8 +324,10 @@ def do_modeling(self): indices = self.aggregation_data.get_equation_indices(skip_first_index_of_period=True) - time_variables: set[str] = {k for k, v in self._model.variables.data.items() if 'time' in v.indexes} - binary_variables: set[str] = {k for k, v in self._model.variables.data.items() if k in self._model.binaries} + time_variables: set[str] = { + name for name in self._model.variables if 'time' in self._model.variables[name].dims + } + binary_variables: set[str] = set(self._model.variables.binaries) binary_time_variables: set[str] = time_variables & binary_variables for component in components: @@ -353,17 +364,11 @@ def _equate_indices(self, variable: linopy.Variable, indices: tuple[np.ndarray, variable.name in self._model.variables.binaries and self.aggregation_parameters.percentage_of_period_freedom > 0 ): - var_k1 = self.add_variables( - binary=True, - coords={'time': variable.isel(time=indices[0]).indexes['time']}, - short_name=f'correction1|{variable.name}', - ) + sel = variable.isel(time=indices[0]) + coords = {d: sel.indexes[d] for d in sel.dims} + var_k1 = self.add_variables(binary=True, coords=coords, short_name=f'correction1|{variable.name}') - var_k0 = self.add_variables( - binary=True, - coords={'time': variable.isel(time=indices[0]).indexes['time']}, - short_name=f'correction0|{variable.name}', - ) + var_k0 = self.add_variables(binary=True, coords=coords, short_name=f'correction0|{variable.name}') # equation extends ... # --> On(p3) can be 0/1 independent of On(p1,t)! @@ -374,13 +379,13 @@ def _equate_indices(self, variable: linopy.Variable, indices: tuple[np.ndarray, con.lhs += 1 * var_k1 - 1 * var_k0 # interlock var_k1 and var_K2: - # eq: var_k0(t)+var_k1(t) <= 1.1 - self.add_constraints(var_k0 + var_k1 <= 1.1, short_name=f'lock_k0_and_k1|{variable.name}') + # eq: var_k0(t)+var_k1(t) <= 1 + self.add_constraints(var_k0 + var_k1 <= 1, short_name=f'lock_k0_and_k1|{variable.name}') # Begrenzung der Korrektur-Anzahl: # eq: sum(K) <= n_Corr_max + limit = int(np.floor(self.aggregation_parameters.percentage_of_period_freedom / 100 * length)) self.add_constraints( - sum(var_k0) + sum(var_k1) - <= round(self.aggregation_parameters.percentage_of_period_freedom / 100 * length), + var_k0.sum(dim='time') + var_k1.sum(dim='time') <= limit, short_name=f'limit_corrections|{variable.name}', ) diff --git a/flixopt/color_processing.py b/flixopt/color_processing.py new file mode 100644 index 000000000..2959acc82 --- /dev/null +++ b/flixopt/color_processing.py @@ -0,0 +1,261 @@ +"""Simplified color handling for visualization. + +This module provides clean color processing that transforms various input formats +into a label-to-color mapping dictionary, without needing to know about the plotting engine. +""" + +from __future__ import annotations + +import logging + +import matplotlib.colors as mcolors +import matplotlib.pyplot as plt +import plotly.express as px +from plotly.exceptions import PlotlyError + +logger = logging.getLogger('flixopt') + + +def _rgb_string_to_hex(color: str) -> str: + """Convert Plotly RGB/RGBA string format to hex. + + Args: + color: Color in format 'rgb(R, G, B)', 'rgba(R, G, B, A)' or already in hex + + Returns: + Color in hex format '#RRGGBB' + """ + color = color.strip() + + # If already hex, return as-is + if color.startswith('#'): + return color + + # Try to parse rgb() or rgba() + try: + if color.startswith('rgb('): + # Extract RGB values from 'rgb(R, G, B)' format + rgb_str = color[4:-1] # Remove 'rgb(' and ')' + elif color.startswith('rgba('): + # Extract RGBA values from 'rgba(R, G, B, A)' format + rgb_str = color[5:-1] # Remove 'rgba(' and ')' + else: + return color + + # Split on commas and parse first three components + components = rgb_str.split(',') + if len(components) < 3: + return color + + # Parse and clamp the first three components + r = max(0, min(255, int(round(float(components[0].strip()))))) + g = max(0, min(255, int(round(float(components[1].strip()))))) + b = max(0, min(255, int(round(float(components[2].strip()))))) + + return f'#{r:02x}{g:02x}{b:02x}' + except (ValueError, IndexError): + # If parsing fails, return original + return color + + +def process_colors( + colors: None | str | list[str] | dict[str, str], + labels: list[str], + default_colorscale: str = 'turbo', +) -> dict[str, str]: + """Process color input and return a label-to-color mapping. + + This function takes flexible color input and always returns a dictionary + mapping each label to a specific color string. The plotting engine can then + use this mapping as needed. + + Args: + colors: Color specification in one of four formats: + - None: Use the default colorscale + - str: Name of a colorscale (e.g., 'turbo', 'plasma', 'Set1', 'portland') + - list[str]: List of color strings (hex, named colors, etc.) + - dict[str, str]: Direct label-to-color mapping + labels: List of labels that need colors assigned + default_colorscale: Fallback colorscale name if requested scale not found + + Returns: + Dictionary mapping each label to a color string + + Examples: + >>> # Using None - applies default colorscale + >>> process_colors(None, ['A', 'B', 'C']) + {'A': '#0d0887', 'B': '#7e03a8', 'C': '#cc4778'} + + >>> # Using a colorscale name + >>> process_colors('plasma', ['A', 'B', 'C']) + {'A': '#0d0887', 'B': '#7e03a8', 'C': '#cc4778'} + + >>> # Using a list of colors + >>> process_colors(['red', 'blue', 'green'], ['A', 'B', 'C']) + {'A': 'red', 'B': 'blue', 'C': 'green'} + + >>> # Using a pre-made mapping + >>> process_colors({'A': 'red', 'B': 'blue'}, ['A', 'B', 'C']) + {'A': 'red', 'B': 'blue', 'C': '#0d0887'} # C gets color from default scale + """ + if not labels: + return {} + + # Case 1: Already a mapping dictionary + if isinstance(colors, dict): + return _fill_missing_colors(colors, labels, default_colorscale) + + # Case 2: None or colorscale name (string) + if colors is None or isinstance(colors, str): + colorscale_name = colors if colors is not None else default_colorscale + color_list = _get_colors_from_scale(colorscale_name, len(labels), default_colorscale) + return dict(zip(labels, color_list, strict=False)) + + # Case 3: List of colors + if isinstance(colors, list): + if len(colors) == 0: + logger.warning(f'Empty color list provided. Using {default_colorscale} instead.') + color_list = _get_colors_from_scale(default_colorscale, len(labels), default_colorscale) + return dict(zip(labels, color_list, strict=False)) + + if len(colors) < len(labels): + logger.debug( + f'Not enough colors provided ({len(colors)}) for all labels ({len(labels)}). Colors will cycle.' + ) + + # Cycle through colors if we don't have enough + return {label: colors[i % len(colors)] for i, label in enumerate(labels)} + + raise TypeError(f'colors must be None, str, list, or dict, got {type(colors)}') + + +def _fill_missing_colors( + color_mapping: dict[str, str], + labels: list[str], + default_colorscale: str, +) -> dict[str, str]: + """Fill in missing labels in a color mapping using a colorscale. + + Args: + color_mapping: Partial label-to-color mapping + labels: All labels that need colors + default_colorscale: Colorscale to use for missing labels + + Returns: + Complete label-to-color mapping + """ + missing_labels = [label for label in labels if label not in color_mapping] + + if not missing_labels: + return color_mapping.copy() + + # Log warning about missing labels + logger.debug(f'Labels missing colors: {missing_labels}. Using {default_colorscale} for these.') + + # Get colors for missing labels + missing_colors = _get_colors_from_scale(default_colorscale, len(missing_labels), default_colorscale) + + # Combine existing and new colors + result = color_mapping.copy() + result.update(dict(zip(missing_labels, missing_colors, strict=False))) + return result + + +def _get_colors_from_scale( + colorscale_name: str, + num_colors: int, + fallback_scale: str, +) -> list[str]: + """Extract a list of colors from a named colorscale. + + Tries to get colors from the named scale (Plotly first, then Matplotlib), + falls back to the fallback scale if not found. + + Args: + colorscale_name: Name of the colorscale to try + num_colors: Number of colors needed + fallback_scale: Fallback colorscale name if first fails + + Returns: + List of color strings (hex format) + """ + # Try to get the requested colorscale + colors = _try_get_colorscale(colorscale_name, num_colors) + + if colors is not None: + return colors + + # Fallback to default + logger.warning(f"Colorscale '{colorscale_name}' not found. Using '{fallback_scale}' instead.") + + colors = _try_get_colorscale(fallback_scale, num_colors) + + if colors is not None: + return colors + + # Ultimate fallback: just use basic colors + logger.warning(f"Fallback colorscale '{fallback_scale}' also not found. Using basic colors.") + basic_colors = [ + '#1f77b4', + '#ff7f0e', + '#2ca02c', + '#d62728', + '#9467bd', + '#8c564b', + '#e377c2', + '#7f7f7f', + '#bcbd22', + '#17becf', + ] + return [basic_colors[i % len(basic_colors)] for i in range(num_colors)] + + +def _try_get_colorscale(colorscale_name: str, num_colors: int) -> list[str] | None: + """Try to get colors from Plotly or Matplotlib colorscales. + + Tries Plotly colorscales first (both qualitative and sequential), + then falls back to Matplotlib colorscales. + + Args: + colorscale_name: Name of the colorscale + num_colors: Number of colors needed + + Returns: + List of color strings (hex format) if successful, None if colorscale not found + """ + # First try Plotly qualitative (discrete) color sequences + colorscale_title = colorscale_name.title() + if hasattr(px.colors.qualitative, colorscale_title): + color_list = getattr(px.colors.qualitative, colorscale_title) + # Convert to hex format for matplotlib compatibility + return [_rgb_string_to_hex(color_list[i % len(color_list)]) for i in range(num_colors)] + + # Then try Plotly sequential/continuous colorscales + try: + colorscale = px.colors.get_colorscale(colorscale_name) + # Sample evenly from the colorscale + if num_colors == 1: + sample_points = [0.5] + else: + sample_points = [i / (num_colors - 1) for i in range(num_colors)] + colors = px.colors.sample_colorscale(colorscale, sample_points) + # Convert to hex format for matplotlib compatibility + return [_rgb_string_to_hex(c) for c in colors] + except (PlotlyError, ValueError): + pass + + # Finally try Matplotlib colorscales + try: + cmap = plt.get_cmap(colorscale_name) + + # Sample evenly from the colorscale + if num_colors == 1: + colors = [cmap(0.5)] + else: + colors = [cmap(i / (num_colors - 1)) for i in range(num_colors)] + + # Convert RGBA tuples to hex strings + return [mcolors.rgb2hex(color[:3]) for color in colors] + + except (ValueError, KeyError): + return None diff --git a/flixopt/config.yaml b/flixopt/config.yaml deleted file mode 100644 index e5336eeef..000000000 --- a/flixopt/config.yaml +++ /dev/null @@ -1,10 +0,0 @@ -# Default configuration of flixopt -config_name: flixopt # Name of the config file. This has no effect on the configuration itself. -logging: - level: INFO - file: flixopt.log - rich: false # logging output is formatted using rich. This is only advisable when using a proper terminal -modeling: - BIG: 10000000 # 1e notation not possible in yaml - EPSILON: 0.00001 - BIG_BINARY_BOUND: 100000 diff --git a/flixopt/utils.py b/flixopt/utils.py deleted file mode 100644 index f1e12b9dc..000000000 --- a/flixopt/utils.py +++ /dev/null @@ -1,88 +0,0 @@ -""" -This module contains several utility functions used throughout the flixopt framework. -""" - -from __future__ import annotations - -import logging -from typing import Literal - -import numpy as np -import xarray as xr - -logger = logging.getLogger('flixopt') - - -def round_nested_floats(obj, decimals=2): - """Recursively round floating point numbers in nested data structures. - - This function traverses nested data structures (dictionaries, lists) and rounds - any floating point numbers to the specified number of decimal places. It handles - various data types including NumPy arrays and xarray DataArrays by converting - them to lists with rounded values. - - Args: - obj: The object to process. Can be a dict, list, float, int, numpy.ndarray, - xarray.DataArray, or any other type. - decimals (int, optional): Number of decimal places to round to. Defaults to 2. - - Returns: - The processed object with the same structure as the input, but with all - floating point numbers rounded to the specified precision. NumPy arrays - and xarray DataArrays are converted to lists. - - Examples: - >>> data = {'a': 3.14159, 'b': [1.234, 2.678]} - >>> round_nested_floats(data, decimals=2) - {'a': 3.14, 'b': [1.23, 2.68]} - - >>> import numpy as np - >>> arr = np.array([1.234, 5.678]) - >>> round_nested_floats(arr, decimals=1) - [1.2, 5.7] - """ - if isinstance(obj, dict): - return {k: round_nested_floats(v, decimals) for k, v in obj.items()} - elif isinstance(obj, list): - return [round_nested_floats(v, decimals) for v in obj] - elif isinstance(obj, float): - return round(obj, decimals) - elif isinstance(obj, int): - return obj - elif isinstance(obj, np.ndarray): - return np.round(obj, decimals).tolist() - elif isinstance(obj, xr.DataArray): - return obj.round(decimals).values.tolist() - return obj - - -def convert_dataarray( - data: xr.DataArray, mode: Literal['py', 'numpy', 'xarray', 'structure'] -) -> list | np.ndarray | xr.DataArray | str: - """ - Convert a DataArray to a different format. - - Args: - data: The DataArray to convert. - mode: The mode to convert to. - - 'py': Convert to python native types (for json) - - 'numpy': Convert to numpy array - - 'xarray': Convert to xarray.DataArray - - 'structure': Convert to strings (for structure, storing variable names) - - Returns: - The converted data. - - Raises: - ValueError: If the mode is unknown. - """ - if mode == 'numpy': - return data.values - elif mode == 'py': - return data.values.tolist() - elif mode == 'xarray': - return data - elif mode == 'structure': - return f':::{data.name}' - else: - raise ValueError(f'Unknown mode {mode}') diff --git a/mkdocs.yml b/mkdocs.yml index 98747d987..7d3490360 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,4 +1,4 @@ -# Options: +# flixOpt Documentation Configuration # https://mkdocstrings.github.io/python/usage/configuration/docstrings/ # https://squidfunk.github.io/mkdocs-material/setup/ @@ -8,129 +8,312 @@ site_url: https://flixopt.github.io/flixopt/ repo_url: https://github.com/flixOpt/flixopt repo_name: flixOpt/flixopt +nav: + - Home: index.md + - User Guide: + - Getting Started: getting-started.md + - Core Concepts: user-guide/core-concepts.md + - Migration to v3.0.0: user-guide/migration-guide-v3.md + - Mathematical Notation: + - Overview: user-guide/mathematical-notation/index.md + - Dimensions: user-guide/mathematical-notation/dimensions.md + - Elements: + - Flow: user-guide/mathematical-notation/elements/Flow.md + - Bus: user-guide/mathematical-notation/elements/Bus.md + - Storage: user-guide/mathematical-notation/elements/Storage.md + - LinearConverter: user-guide/mathematical-notation/elements/LinearConverter.md + - Features: + - InvestParameters: user-guide/mathematical-notation/features/InvestParameters.md + - OnOffParameters: user-guide/mathematical-notation/features/OnOffParameters.md + - Piecewise: user-guide/mathematical-notation/features/Piecewise.md + - Effects, Penalty & Objective: user-guide/mathematical-notation/effects-penalty-objective.md + - Modeling Patterns: + - Overview: user-guide/mathematical-notation/modeling-patterns/index.md + - Bounds and States: user-guide/mathematical-notation/modeling-patterns/bounds-and-states.md + - Duration Tracking: user-guide/mathematical-notation/modeling-patterns/duration-tracking.md + - State Transitions: user-guide/mathematical-notation/modeling-patterns/state-transitions.md + - Recipes: user-guide/recipes/index.md + - Roadmap: roadmap.md + - Examples: examples/ + - Contribute: contribute.md + - API Reference: api-reference/ + - Release Notes: changelog/ theme: name: material + language: en + palette: - # Light mode + # Palette toggle for automatic mode + - media: "(prefers-color-scheme)" + toggle: + icon: material/brightness-auto + name: Switch to light mode + + # Palette toggle for light mode - media: "(prefers-color-scheme: light)" scheme: default primary: teal - accent: blue + accent: cyan toggle: icon: material/brightness-7 name: Switch to dark mode - # Dark mode + + # Palette toggle for dark mode - media: "(prefers-color-scheme: dark)" scheme: slate - primary: teal # Can be different from light mode - accent: blue + primary: teal + accent: cyan toggle: icon: material/brightness-4 - name: Switch to light mode + name: Switch to system preference + + font: + text: Inter # Modern, readable font + code: Fira Code # Beautiful code font with ligatures + logo: images/flixopt-icon.svg favicon: images/flixopt-icon.svg + icon: repo: fontawesome/brands/github + edit: material/pencil + view: material/eye + annotation: material/plus-circle + features: + # Navigation - navigation.instant - navigation.instant.progress + - navigation.instant.prefetch - navigation.tracking - navigation.tabs + - navigation.tabs.sticky - navigation.sections + - navigation.expand # Expand navigation by default + - navigation.path # Show breadcrumb path + - navigation.prune # Only render visible navigation + - navigation.indexes - navigation.top - navigation.footer + + # Table of contents - toc.follow - - navigation.indexes + - toc.integrate # Integrate TOC into navigation (optional) + + # Search - search.suggest - search.highlight + - search.share + + # Content - content.action.edit - content.action.view - content.code.copy + - content.code.select - content.code.annotate - content.tooltips - - content.code.copy - - navigation.footer.version + - content.tabs.link # Link content tabs across pages + + # Header + - announce.dismiss # Allow dismissing announcements markdown_extensions: + # Content formatting + - abbr - admonition - - codehilite - - markdown_include.include: - base_path: docs + - attr_list + - def_list + - footnotes + - md_in_html + - tables + - toc: + permalink: true + permalink_title: Anchor link to this section + toc_depth: 3 + + # Code blocks - pymdownx.highlight: anchor_linenums: true line_spans: __span pygments_lang_class: true + auto_title: true - pymdownx.inlinehilite - - pymdownx.snippets - - pymdownx.superfences - - attr_list - - abbr - - md_in_html - - footnotes - - tables + - pymdownx.snippets: + base_path: .. + check_paths: true + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format + + # Enhanced content + - pymdownx.details - pymdownx.tabbed: alternate_style: true + combine_header_slug: true + - pymdownx.tasklist: + custom_checkbox: true + + # Typography + - pymdownx.betterem: + smart_enable: all + - pymdownx.caret + - pymdownx.mark + - pymdownx.tilde + - pymdownx.smartsymbols + - pymdownx.keys + + # Math - pymdownx.arithmatex: generic: true + + # Icons & emojis - pymdownx.emoji: emoji_index: !!python/name:material.extensions.emoji.twemoji emoji_generator: !!python/name:material.extensions.emoji.to_svg - - pymdownx.snippets: - base_path: .. + options: + custom_icons: + - overrides/.icons + + # Legacy support + - markdown_include.include: + base_path: docs plugins: - - search # Enables the search functionality in the documentation - - table-reader # Allows including tables from external files + - search: + separator: '[\s\u200b\-_,:!=\[\]()"`/]+|\.(?!\d)|&[lg]t;|(?!\b)(?=[A-Z][a-z])' + + - table-reader + - include-markdown + - mike: + alias_type: symlink + redirect_template: null + deploy_prefix: '' + canonical_version: null version_selector: true - - gen-files: - scripts: - - scripts/gen_ref_pages.py + css_dir: css + javascript_dir: js + - literate-nav: nav_file: SUMMARY.md - implicit_index: true # This makes index.md the default landing page - - mkdocstrings: # Handles automatic API documentation generation - default_handler: python # Sets Python as the default language - handlers: - python: # Configuration for Python code documentation - options: - docstring_style: google # Sets google as the docstring style - modernize_annotations: true # Improves type annotations - merge_init_into_class: true # Promotes constructor parameters to class-level documentation - docstring_section_style: table # Renders parameter sections as a table (also: list, spacy) - - members_order: source # Orders members as they appear in the source code - inherited_members: false # Include members inherited from parent classes - show_if_no_docstring: false # Documents objects even if they don't have docstrings - - group_by_category: true - heading_level: 1 # Sets the base heading level for documented objects - line_length: 80 - filters: ["!^_", "^__init__$"] - show_root_heading: true # whether the documented object's name should be displayed as a heading at the beginning of its documentation - show_source: false # Shows the source code implementation from documentation - show_object_full_path: false # Displays simple class names instead of full import paths - show_docstring_attributes: true # Shows class attributes in the documentation - show_category_heading: true # Displays category headings (Methods, Attributes, etc.) for organization - show_signature: true # Shows method signatures with parameters - show_signature_annotations: true # Includes type annotations in the signatures when available - show_root_toc_entry: false # Whether to show a link to the root of the documentation in the sidebar - separate_signature: true # Displays signatures separate from descriptions for cleaner layout - - extra: - infer_type_annotations: true # Uses Python type hints to supplement docstring information + implicit_index: true + + - gen-files: + scripts: + - scripts/gen_ref_pages.py + + - mkdocstrings: + enabled: !ENV [ENABLE_MKDOCSTRINGS, true] + default_handler: python + handlers: + python: + paths: [.] + import: + - https://docs.python.org/3/objects.inv + - https://numpy.org/doc/stable/objects.inv + - https://pandas.pydata.org/docs/objects.inv + options: + # Docstring parsing + docstring_style: google + docstring_section_style: table + + # Member ordering and filtering + members_order: source + inherited_members: false + show_if_no_docstring: false + filters: ["!^_", "^__init__$"] + group_by_category: true + + # Headings and structure + heading_level: 1 + show_root_heading: true + show_root_toc_entry: false + show_category_heading: true + + # Signatures + show_signature: true + show_signature_annotations: true + separate_signature: true + line_length: 80 + + # Source and paths + show_source: false + show_object_full_path: false + + # Attributes and annotations + show_docstring_attributes: true + modernize_annotations: true + merge_init_into_class: true + + # Improved type hints + annotations_path: brief + + # Optional: Add git info + - git-revision-date-localized: + enable_creation_date: true + type: timeago + fallback_to_build_date: true + + # Optional: Add better navigation + - tags: + tags_file: tags.md + + # Optional: Minify HTML in production + - minify: + minify_html: true + minify_js: true + minify_css: true + htmlmin_opts: + remove_comments: true extra: version: provider: mike default: latest + alias: true + + social: + - icon: fontawesome/brands/github + link: https://github.com/flixOpt/flixopt + name: flixOpt on GitHub + - icon: fontawesome/brands/python + link: https://pypi.org/project/flixopt/ + name: flixOpt on PyPI + + analytics: + provider: google + property: !ENV GOOGLE_ANALYTICS_KEY + feedback: + title: Was this page helpful? + ratings: + - icon: material/emoticon-happy-outline + name: This page was helpful + data: 1 + note: >- + Thanks for your feedback! + - icon: material/emoticon-sad-outline + name: This page could be improved + data: 0 + note: >- + Thanks for your feedback! Help us improve by + opening an issue. + + status: + new: Recently added + deprecated: Deprecated + +extra_css: + - stylesheets/extra.css extra_javascript: - - javascripts/mathjax.js # Custom MathJax 3 CDN Configuration - - https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js #MathJax 3 CDN - - https://polyfill.io/v3/polyfill.min.js?features=es6 #Support for older browsers + - javascripts/mathjax.js + - https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js + - https://polyfill.io/v3/polyfill.min.js?features=es6 watch: - flixopt + - docs diff --git a/pyproject.toml b/pyproject.toml index 686772bd1..29c669b3e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ dynamic = ["version"] description = "Vector based energy and material flow optimization framework in Python." readme = "README.md" requires-python = ">=3.10" -license = { text = "MIT License" } +license = "MIT" authors = [ { name = "Chair of Building Energy Systems and Heat Supply, TU Dresden", email = "peter.stange@tu-dresden.de" }, { name = "Felix Bumann", email = "felixbumann387@gmail.com" }, @@ -21,8 +21,7 @@ maintainers = [ ] keywords = ["optimization", "energy systems", "numerical analysis"] classifiers = [ - "Development Status :: 3 - Alpha", - "License :: OSI Approved :: MIT License", + "Development Status :: 4 - Beta", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -30,15 +29,14 @@ classifiers = [ "Intended Audience :: Developers", "Intended Audience :: Science/Research", "Topic :: Scientific/Engineering", - "License :: OSI Approved :: MIT License", ] dependencies = [ # Core scientific computing "numpy >= 1.21.5, < 3", "pandas >= 2.0.0, < 3", - "xarray >= 2024.2.0, < 2026.0", # CalVer: allow through next calendar year + "xarray >= 2024.2.0, < 2026.0", # CalVer: allow through next calendar year # Optimization and data handling - "linopy >= 0.5.1, < 0.6", # Widened from patch pin to minor range + "linopy >= 0.5.1, < 0.6", # Widened from patch pin to minor range "netcdf4 >= 1.6.1, < 2", # Utilities "pyyaml >= 6.0.0, < 7", @@ -46,10 +44,11 @@ dependencies = [ "tomli >= 2.0.1, < 3; python_version < '3.11'", # Only needed with python 3.10 or earlier # Default solver "highspy >= 1.5.3, < 2", - # Visualization "matplotlib >= 3.5.2, < 4", "plotly >= 5.15.0, < 7", + # Fix for numexpr compatibility issue with numpy 1.26.4 on Python 3.10 + "numexpr >= 2.8.4, < 2.14; python_version < '3.11'", # Avoid 2.14.0 on older Python ] [project.optional-dependencies] @@ -80,14 +79,15 @@ full = [ # Development tools and testing dev = [ "pytest==8.4.2", + "pytest-xdist==3.8.0", "nbformat==5.10.4", - "ruff==0.13.0", + "ruff==0.13.3", "pre-commit==4.3.0", "pyvis==0.3.2", - "tsam==2.3.1", + "tsam==2.3.9", "scipy==1.15.1", "gurobipy==12.0.3", - "dash==3.0.0", + "dash==3.2.0", "dash-cytoscape==1.0.2", "dash-daq==0.6.0", "networkx==3.0.0", @@ -96,7 +96,8 @@ dev = [ # Documentation building docs = [ - "mkdocs-material==9.6.19", + "mkdocs==1.6.1", + "mkdocs-material==9.6.22", "mkdocstrings-python==1.18.2", "mkdocs-table-reader-plugin==3.1.0", "mkdocs-gen-files==0.5.0", @@ -106,6 +107,8 @@ docs = [ "pymdown-extensions==10.16.1", "pygments==2.19.2", "mike==2.1.3", + "mkdocs-git-revision-date-localized-plugin==1.4.7", + "mkdocs-minify-plugin==0.8.0", ] [project.urls] @@ -115,10 +118,13 @@ documentation = "https://flixopt.github.io/flixopt/" [tool.setuptools.packages.find] where = ["."] -exclude = ["tests", "docs", "examples", "examples.*", "Tutorials", ".git", ".vscode", "build", ".venv", "venv/"] +include = ["flixopt*"] +exclude = ["tests*", "docs*", "examples*", "Tutorials*"] +[tool.setuptools] +include-package-data = true -[tool.setuptools.package-data] -"flixopt" = ["config.yaml"] +[tool.setuptools.exclude-package-data] +"*" = ["*.md", ".git*", "*.ipynb", "renovate.json"] [tool.setuptools_scm] version_scheme = "post-release" @@ -176,6 +182,34 @@ docstring-code-format = true [tool.ruff.lint.pyupgrade] keep-runtime-typing = false # Allow pyupgrade to drop runtime typing; prefer postponed annotations +[tool.pytest.ini_options] +markers = [ + "slow: marks tests as slow", + "examples: marks example tests (run only on releases)", +] +addopts = '-m "not examples"' # Skip examples by default + +# Warning filter configuration for pytest +# Filters are processed in order; first match wins +# Format: "action:message:category:module" +filterwarnings = [ + # === Default behavior: show all warnings === + "default", + + # === Treat flixopt warnings as errors (strict mode for our code) === + # This ensures we catch deprecations, future changes, and user warnings in our own code + "error::DeprecationWarning:flixopt", + "error::FutureWarning:flixopt", + "error::UserWarning:flixopt", + + # === Third-party warnings (mirrored from __init__.py) === + "ignore:.*minimal value.*exceeds.*:UserWarning:tsam", + "ignore:Coordinates across variables not equal:UserWarning:linopy", + "ignore:.*join will change from join='outer' to join='exact'.*:FutureWarning:linopy", + "ignore:numpy\\.ndarray size changed:RuntimeWarning", + "ignore:.*network visualization is still experimental.*:UserWarning:flixopt", +] + [tool.bandit] skips = ["B101", "B506"] # assert_used and yaml_load exclude_dirs = ["tests/"] diff --git a/renovate.json b/renovate.json index db09a363a..ded1fbf17 100644 --- a/renovate.json +++ b/renovate.json @@ -14,10 +14,38 @@ "automerge": false, "labels": ["dependencies"], "rangeStrategy": "widen", + "minimumReleaseAge": "7 days", "packageRules": [ { - "matchDepTypes": ["dev-dependencies", "devDependencies"], - "rangeStrategy": "pin" + "description": "Group and automerge dev and docs dependencies", + "matchDepTypes": ["dev", "docs"], + "groupName": "dev dependencies", + "rangeStrategy": "pin", + "minimumReleaseAge": "14 days", + "automerge": true, + "automergeType": "pr", + "separateMinorPatch": false + }, + { + "matchUpdateTypes": ["patch"], + "matchCurrentVersion": "!/^0/", + "automerge": true, + "automergeType": "pr" + }, + { + "description": "CalVer packages (xarray, dask) can have breaking changes in any release - never automerge, longer release age", + "matchPackageNames": ["xarray", "dask"], + "minimumReleaseAge": "14 days", + "schedule": ["* * * * *"], + "labels": ["calver", "breaking-change-risk", "dependencies"], + "prPriority": 10 + }, + { + "description": "Automerge ruff patches despite 0.x version", + "matchPackageNames": ["ruff"], + "matchUpdateTypes": ["patch"], + "automerge": true, + "automergeType": "pr" } ] } diff --git a/scripts/extract_changelog.py b/scripts/extract_changelog.py new file mode 100644 index 000000000..d05229896 --- /dev/null +++ b/scripts/extract_changelog.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +""" +Extract individual releases from CHANGELOG.md to docs/changelog/ +Simple script to create one file per release. +""" + +import os +import re +from pathlib import Path + +from packaging.version import InvalidVersion, Version +from packaging.version import parse as parse_version + + +def extract_releases(): + """Extract releases from CHANGELOG.md and save to individual files.""" + + changelog_path = Path('CHANGELOG.md') + output_dir = Path('docs/changelog') + + if not changelog_path.exists(): + print('❌ CHANGELOG.md not found') + return + + # Create output directory + output_dir.mkdir(parents=True, exist_ok=True) + + # Read changelog + with open(changelog_path, encoding='utf-8') as f: + content = f.read() + + # Remove template section (HTML comments) + content = re.sub(r'', '', content, flags=re.DOTALL) + + # Split by release headers + sections = re.split(r'^## \[', content, flags=re.MULTILINE) + + releases = [] + for section in sections[1:]: # Skip first empty section + # Extract version and date from start of section + match = re.match(r'([^\]]+)\] - ([^\n]+)\n(.*)', section, re.DOTALL) + if match: + version, date, release_content = match.groups() + releases.append((version, date.strip(), release_content.strip())) + + print(f'🔍 Found {len(releases)} releases') + + # Sort releases by version (oldest first) to keep existing file prefixes stable. + def version_key(release): + try: + return parse_version(release[0]) + except InvalidVersion: + return parse_version('0.0.0') # fallback for invalid versions + + releases.sort(key=version_key, reverse=False) + + # Show what we captured for debugging + if releases: + print(f'🔧 First release content length: {len(releases[0][2])}') + + for i, (version_str, date, release_content) in enumerate(releases): + # Clean up version for filename with numeric prefix (newest first) + index = 99999 - i # Newest first, while keeping the same file names for old releases + prefix = f'{index:05d}' # Zero-padded 5-digit number + filename = f'{prefix}-v{version_str.replace(" ", "-")}.md' + filepath = output_dir / filename + + # Clean up content - remove trailing --- separators and emojis from headers + cleaned_content = re.sub(r'\s*---\s*$', '', release_content.strip()) + + # Generate navigation links + nav_links = [] + + # Previous version (older release) + if i > 0: + prev_index = 99999 - (i - 1) + prev_version = releases[i - 1][0] + prev_filename = f'{prev_index:05d}-v{prev_version.replace(" ", "-")}.md' + nav_links.append(f'← [Previous: {prev_version}]({prev_filename})') + + # Next version (newer release) + if i < len(releases) - 1: + next_index = 99999 - (i + 1) + next_version = releases[i + 1][0] + next_filename = f'{next_index:05d}-v{next_version.replace(" ", "-")}.md' + nav_links.append(f'[Next: {next_version}]({next_filename}) →') + + # Always add link back to index + nav_links.append('[📋 All Releases](index.md)') + # Add GitHub tag link only for valid PEP 440 versions (skip e.g. "Unreleased") + ver_obj = parse_version(version_str) + if isinstance(ver_obj, Version): + nav_links.append(f'[🏷️ GitHub Release](https://github.com/flixOpt/flixopt/releases/tag/v{version_str})') + # Create content with navigation + content_lines = [ + f'# {version_str} - {date.strip()}', + '', + ' | '.join(nav_links), + '', + '---', + '', + cleaned_content, + '', + '---', + '', + ' | '.join(nav_links), + ] + + # Write file + with open(filepath, 'w', encoding='utf-8') as f: + f.write('\n'.join(content_lines)) + + print(f'✅ Created {filename}') + + print(f'🎉 Extracted {len(releases)} releases to docs/changelog/') + + +def extract_index(): + changelog_path = Path('CHANGELOG.md') + output_dir = Path('docs/changelog') + index_path = output_dir / 'index.md' + + if not changelog_path.exists(): + print('❌ CHANGELOG.md not found') + return + + # Create output directory + output_dir.mkdir(parents=True, exist_ok=True) + + # Read changelog + with open(changelog_path, encoding='utf-8') as f: + content = f.read() + + intro_match = re.search(r'# Changelog\s+([\s\S]*?)(?= Most likely by accident logger.warning( @@ -543,11 +543,11 @@ def label_full(self) -> str: @property def size_is_fixed(self) -> bool: - # Wenn kein InvestParameters existiert --> True; Wenn Investparameter, den Wert davon nehmen - return False if (isinstance(self.size, InvestParameters) and self.size.fixed_size is None) else True + # Wenn kein SizingParameters existiert --> True; Wenn Investparameter, den Wert davon nehmen + return False if (isinstance(self.size, SizingParameters) and self.size.fixed_size is None) else True - def _format_invest_params(self, params: InvestParameters) -> str: - """Format InvestParameters for display.""" + def _format_invest_params(self, params: SizingParameters) -> str: + """Format SizingParameters for display.""" return f'size: {params.format_for_repr()}' @@ -603,7 +603,7 @@ def _create_on_off_model(self): ) def _create_investment_model(self): - if isinstance(self.element.size, InvestParameters): + if isinstance(self.element.size, SizingParameters): self.add_submodels( InvestmentModel( model=self._model, @@ -624,7 +624,7 @@ def _create_investment_model(self): 'investment', ) else: - raise ValueError(f'Invalid InvestParameters type: {type(self.element.size)}') + raise ValueError(f'Invalid SizingParameters type: {type(self.element.size)}') def _constraint_flow_rate(self): if not self.with_investment and not self.with_on_off: @@ -674,7 +674,7 @@ def with_on_off(self) -> bool: @property def with_investment(self) -> bool: - return isinstance(self.element.size, (InvestParameters, InvestTimingParameters)) + return isinstance(self.element.size, (SizingParameters, InvestTimingParameters)) # Properties for clean access to variables @property diff --git a/flixopt/features.py b/flixopt/features.py index 0c0afcf55..c4082d21b 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -16,7 +16,13 @@ if TYPE_CHECKING: from .core import FlowSystemDimensions, Scalar, TemporalData - from .interface import InvestParameters, InvestTimingParameters, OnOffParameters, Piecewise, SizingParameters + from .interface import ( + InvestmentParameters, + InvestTimingParameters, + OnOffParameters, + Piecewise, + SizingParameters, + ) logger = logging.getLogger('flixopt') @@ -119,13 +125,13 @@ class InvestmentModel(Submodel): """ - parameters: InvestParameters + parameters: SizingParameters def __init__( self, model: FlowSystemModel, label_of_element: str, - parameters: InvestParameters, + parameters: SizingParameters, label_of_model: str | None = None, ): self.piecewise_effects: PiecewiseEffectsModel | None = None diff --git a/flixopt/interface.py b/flixopt/interface.py index a069f8ac2..869d01c52 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -670,7 +670,7 @@ def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None @register_class_for_io -class InvestParameters(Interface): +class SizingParameters(Interface): """Define investment decision parameters with flexible sizing and effect modeling. This class models investment decisions in optimization problems, supporting @@ -693,7 +693,7 @@ class InvestParameters(Interface): Mathematical Formulation: See the complete mathematical model in the documentation: - [InvestParameters](../user-guide/mathematical-notation/features/InvestParameters.md) + [SizingParameters](../user-guide/mathematical-notation/features/SizingParameters.md) Args: fixed_size: Creates binary decision at this exact size. None allows continuous sizing. @@ -711,8 +711,6 @@ class InvestParameters(Interface): Dict: {'effect_name': value/unit} (e.g., {'cost': 1200}). piecewise_effects_of_investment: Non-linear costs using PiecewiseEffects. Combinable with effects_of_size and effects_per_size. - effects_of_retirement: Costs incurred if NOT investing (demolition, penalties). - Dict: {'effect_name': value}. linked_periods: Describes which periods are linked. 1 means linked, 0 means size=0. None means no linked periods. For convenience, pass a tuple containing the first and last period (2025, 2039), linking them and those in between @@ -721,8 +719,6 @@ class InvestParameters(Interface): Will be removed in version 4.0. specific_effects: **Deprecated**. Use `effects_per_size` instead. Will be removed in version 4.0. - divest_effects: **Deprecated**. Use `effects_of_retirement` instead. - Will be removed in version 4.0. piecewise_effects: **Deprecated**. Use `piecewise_effects_of_investment` instead. Will be removed in version 4.0. optional: DEPRECATED. Use `mandatory` instead. Opposite of `mandatory`. @@ -742,7 +738,7 @@ class InvestParameters(Interface): Simple binary investment (solar panels): ```python - solar_investment = InvestParameters( + solar_investment = SizingParameters( fixed_size=100, # 100 kW system (binary decision) mandatory=False, # Investment is optional effects_of_size={ @@ -759,7 +755,7 @@ class InvestParameters(Interface): Flexible sizing with economies of scale: ```python - battery_investment = InvestParameters( + battery_investment = SizingParameters( minimum_size=10, # Minimum viable system size (kWh) maximum_size=1000, # Maximum installable capacity mandatory=False, # Investment is optional @@ -791,7 +787,7 @@ class InvestParameters(Interface): Mandatory replacement with retirement costs: ```python - boiler_replacement = InvestParameters( + boiler_replacement = SizingParameters( minimum_size=50, maximum_size=200, mandatory=False, # Can choose not to replace @@ -803,10 +799,6 @@ class InvestParameters(Interface): 'cost': 400, # €400/kW capacity 'maintenance': 25, # Annual maintenance per kW }, - effects_of_retirement={ - 'cost': 8000, # Demolition if not replaced - 'environmental': 100, # Disposal fees - }, ) ``` @@ -814,14 +806,14 @@ class InvestParameters(Interface): ```python # Gas turbine option - gas_turbine = InvestParameters( + gas_turbine = SizingParameters( fixed_size=50, # MW effects_of_size={'cost': 2500000, 'CO2': 1250000}, effects_per_size={'fuel_cost': 45, 'maintenance': 12}, ) # Wind farm option - wind_farm = InvestParameters( + wind_farm = SizingParameters( minimum_size=20, maximum_size=100, effects_of_size={'cost': 1000000, 'CO2': -5000000}, @@ -832,7 +824,7 @@ class InvestParameters(Interface): Technology learning curve: ```python - hydrogen_electrolyzer = InvestParameters( + hydrogen_electrolyzer = SizingParameters( minimum_size=1, maximum_size=50, # MW piecewise_effects_of_investment=PiecewiseEffects( @@ -881,7 +873,6 @@ def __init__( mandatory: bool = False, effects_of_size: PeriodicEffectsUser | None = None, effects_per_size: PeriodicEffectsUser | None = None, - effects_of_retirement: PeriodicEffectsUser | None = None, piecewise_effects_of_investment: PiecewiseEffects | None = None, linked_periods: PeriodicDataUser | tuple[int, int] | None = None, **kwargs, @@ -891,9 +882,6 @@ def __init__( effects_per_size = self._handle_deprecated_kwarg( kwargs, 'specific_effects', 'effects_per_size', effects_per_size ) - effects_of_retirement = self._handle_deprecated_kwarg( - kwargs, 'divest_effects', 'effects_of_retirement', effects_of_retirement - ) piecewise_effects_of_investment = self._handle_deprecated_kwarg( kwargs, 'piecewise_effects', 'piecewise_effects_of_investment', piecewise_effects_of_investment ) @@ -912,9 +900,6 @@ def __init__( self._validate_kwargs(kwargs) self.effects_of_size: PeriodicEffectsUser = effects_of_size if effects_of_size is not None else {} - self.effects_of_retirement: PeriodicEffectsUser = ( - effects_of_retirement if effects_of_retirement is not None else {} - ) self.fixed_size = fixed_size self.mandatory = mandatory self.effects_per_size: PeriodicEffectsUser = effects_per_size if effects_per_size is not None else {} @@ -930,12 +915,6 @@ def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None label_suffix='effects_of_size', dims=['period', 'scenario'], ) - self.effects_of_retirement = flow_system.fit_effects_to_model_coords( - label_prefix=name_prefix, - effect_values=self.effects_of_retirement, - label_suffix='effects_of_retirement', - dims=['period', 'scenario'], - ) self.effects_per_size = flow_system.fit_effects_to_model_coords( label_prefix=name_prefix, effect_values=self.effects_per_size, @@ -1018,16 +997,6 @@ def specific_effects(self) -> PeriodicEffectsUser: ) return self.effects_per_size - @property - def divest_effects(self) -> PeriodicEffectsUser: - """Deprecated property. Use effects_of_retirement instead.""" - warnings.warn( - 'The divest_effects property is deprecated. Use effects_of_retirement instead.', - DeprecationWarning, - stacklevel=2, - ) - return self.effects_of_retirement - @property def piecewise_effects(self) -> PiecewiseEffects | None: """Deprecated property. Use piecewise_effects_of_investment instead.""" @@ -1047,7 +1016,7 @@ def maximum_or_fixed_size(self) -> PeriodicData: return self.fixed_size if self.fixed_size is not None else self.maximum_size def format_for_repr(self) -> str: - """Format InvestParameters for display in repr methods. + """Format SizingParameters for display in repr methods. Returns: Formatted string showing size information @@ -1079,188 +1048,6 @@ def compute_linked_periods(first_period: int, last_period: int, periods: pd.Inde ).rename('linked_periods') -@register_class_for_io -class SizingParameters(Interface): - """Define capacity sizing parameters with flexible bounds and size-dependent effects. - - This class models capacity sizing decisions, supporting both binary (invest/don't invest) - and continuous sizing choices with size-dependent effects. It focuses purely on capacity - constraints without investment timing considerations. - - SizingParameters can be used standalone for simple capacity planning or combined with - InvestmentParameters for complex investment timing optimization. - - Sizing Decision Types: - **Binary Sizing**: Fixed size installations creating yes/no decisions - (e.g., install a 100 kW generator or not) - - **Continuous Sizing**: Variable size decisions with minimum/maximum bounds - (e.g., battery capacity from 10-1000 kWh, pipeline diameter optimization) - - Cost Modeling: - - **Size-Dependent Effects**: Linear costs proportional to size (€/kW, €/m²) - - Mathematical Formulation: - See the complete mathematical model in the documentation: - [SizingParameters](../user-guide/mathematical-notation/features/SizingParameters.md) - - Args: - fixed_size: Creates binary decision at this exact size. None allows continuous sizing. - minimum_size: Lower bound for continuous sizing. Default: CONFIG.Modeling.epsilon. - Ignored if fixed_size is specified. - maximum_size: Upper bound for continuous sizing. Default: CONFIG.Modeling.big. - Ignored if fixed_size is specified. - mandatory: Controls whether sizing is required. When True, forces capacity to be - non-zero (useful for mandatory capacity requirements). When False (default), - optimization can choose zero capacity. - specific_effects: Variable costs proportional to size (per-unit costs). - Dict: {'effect_name': value/unit} (e.g., {'cost': 1200}). - linked_periods: Describes which periods have linked sizes. 1 means linked, 0 means size=0. - None means no linked periods. For convenience, pass a tuple containing the first and - last period (2025, 2039), linking them and those in between. - - Examples: - Simple binary sizing (solar panels): - - ```python - solar_sizing = SizingParameters( - fixed_size=100, # 100 kW system (binary decision) - mandatory=False, # Capacity is optional - specific_effects={ - 'cost': 1200, # €1200/kW for panels (annualized) - 'CO2': -800, # kg CO2 avoided per kW annually - }, - ) - ``` - - Flexible continuous sizing: - - ```python - battery_sizing = SizingParameters( - minimum_size=10, # Minimum viable system size (kWh) - maximum_size=1000, # Maximum installable capacity - mandatory=False, # Capacity is optional - specific_effects={ - 'cost': 600, # €600/kWh annualized cost - 'space': 0.5, # m² per kWh - }, - ) - ``` - - Mandatory capacity with linked periods: - - ```python - turbine_sizing = SizingParameters( - minimum_size=50, - maximum_size=200, - mandatory=True, # Must have capacity - linked_periods=(2025, 2030), # Same size across these years - specific_effects={ - 'cost': 400, # €400/kW capacity - 'maintenance': 25, # Annual maintenance per kW - }, - ) - ``` - - Common Use Cases: - - Power generation: Plant sizing, capacity planning - - Energy storage: Battery sizing, capacity optimization - - Infrastructure: Network capacity, facility sizing - - Industrial equipment: Capacity expansion, production sizing - """ - - def __init__( - self, - fixed_size: PeriodicDataUser | None = None, - minimum_size: PeriodicDataUser | None = None, - maximum_size: PeriodicDataUser | None = None, - mandatory: bool = False, - specific_effects: PeriodicEffectsUser | None = None, - linked_periods: PeriodicDataUser | tuple[int, int] | None = None, - ): - self.fixed_size = fixed_size - self.mandatory = mandatory - self.specific_effects: PeriodicEffectsUser = specific_effects if specific_effects is not None else {} - self.minimum_size = minimum_size if minimum_size is not None else CONFIG.Modeling.epsilon - self.maximum_size = maximum_size if maximum_size is not None else CONFIG.Modeling.big - self.linked_periods = linked_periods - - def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None: - self.specific_effects = flow_system.fit_effects_to_model_coords( - label_prefix=name_prefix, - effect_values=self.specific_effects, - label_suffix='specific_effects', - dims=['period', 'scenario'], - ) - - self.minimum_size = flow_system.fit_to_model_coords( - f'{name_prefix}|minimum_size', self.minimum_size, dims=['period', 'scenario'] - ) - self.maximum_size = flow_system.fit_to_model_coords( - f'{name_prefix}|maximum_size', self.maximum_size, dims=['period', 'scenario'] - ) - - # Convert tuple (first_period, last_period) to DataArray if needed - if isinstance(self.linked_periods, (tuple, list)): - if len(self.linked_periods) != 2: - raise TypeError( - f'If you provide a tuple to "linked_periods", it needs to be len=2. Got {len(self.linked_periods)=}' - ) - if flow_system.periods is None: - raise ValueError( - f'Cannot use linked_periods={self.linked_periods} when FlowSystem has no periods defined. ' - f'Please define periods in FlowSystem or use linked_periods=None.' - ) - logger.debug(f'Computing linked_periods from {self.linked_periods}') - start, end = self.linked_periods - if start not in flow_system.periods.values: - logger.warning( - f'Start of linked periods ({start} not found in periods directly: {flow_system.periods.values}' - ) - if end not in flow_system.periods.values: - logger.warning( - f'End of linked periods ({end} not found in periods directly: {flow_system.periods.values}' - ) - self.linked_periods = InvestParameters.compute_linked_periods(start, end, flow_system.periods) - logger.debug(f'Computed {self.linked_periods=}') - - self.linked_periods = flow_system.fit_to_model_coords( - f'{name_prefix}|linked_periods', self.linked_periods, dims=['period', 'scenario'] - ) - self.fixed_size = flow_system.fit_to_model_coords( - f'{name_prefix}|fixed_size', self.fixed_size, dims=['period', 'scenario'] - ) - - @property - def minimum_or_fixed_size(self) -> PeriodicData: - return self.fixed_size if self.fixed_size is not None else self.minimum_size - - @property - def maximum_or_fixed_size(self) -> PeriodicData: - return self.fixed_size if self.fixed_size is not None else self.maximum_size - - def format_for_repr(self) -> str: - """Format SizingParameters for display in repr methods. - - Returns: - Formatted string showing size information - """ - from .io import numeric_to_str_for_repr - - if self.fixed_size is not None: - val = numeric_to_str_for_repr(self.fixed_size) - status = 'mandatory' if self.mandatory else 'optional' - return f'{val} ({status})' - - # Show range if available - parts = [] - if self.minimum_size is not None: - parts.append(f'min: {numeric_to_str_for_repr(self.minimum_size)}') - if self.maximum_size is not None: - parts.append(f'max: {numeric_to_str_for_repr(self.maximum_size)}') - return ', '.join(parts) if parts else 'sizing' - - InvestmentPeriodData = PeriodicDataUser """This datatype is used to define things related to the period of investment.""" InvestmentPeriodDataBool = bool | InvestmentPeriodData diff --git a/flixopt/structure.py b/flixopt/structure.py index e2aa6ee87..c754d7924 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -429,7 +429,7 @@ def _handle_deprecated_kwarg( """ Handle a deprecated keyword argument by issuing a warning and returning the appropriate value. - This centralizes the deprecation pattern used across multiple classes (Source, Sink, InvestParameters, etc.). + This centralizes the deprecation pattern used across multiple classes (Source, Sink, SizingParameters, etc.). Args: kwargs: Dictionary of keyword arguments to check and modify diff --git a/tests/conftest.py b/tests/conftest.py index 8463581fe..87d28339e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -158,7 +158,7 @@ def complex(): relative_minimum=5 / 50, relative_maximum=1, previous_flow_rate=50, - size=fx.InvestParameters( + size=fx.SizingParameters( effects_of_size=1000, fixed_size=50, mandatory=True, @@ -267,7 +267,7 @@ def simple(timesteps_length=9): 'Speicher', charging=fx.Flow('Q_th_load', bus='Fernwärme', size=1e4), discharging=fx.Flow('Q_th_unload', bus='Fernwärme', size=1e4), - capacity_in_flow_hours=fx.InvestParameters(effects_of_size=20, fixed_size=30, mandatory=True), + capacity_in_flow_hours=fx.SizingParameters(effects_of_size=20, fixed_size=30, mandatory=True), initial_charge_state=0, relative_maximum_charge_state=1 / 100 * np.array(charge_state_values), relative_maximum_final_charge_state=0.8, @@ -280,7 +280,7 @@ def simple(timesteps_length=9): @staticmethod def complex(): """Complex storage with piecewise investment from flow_system_complex""" - invest_speicher = fx.InvestParameters( + invest_speicher = fx.SizingParameters( effects_of_size=0, piecewise_effects_of_investment=fx.PiecewiseEffects( piecewise_origin=fx.Piecewise([fx.Piece(5, 25), fx.Piece(25, 100)]), diff --git a/tests/test_component.py b/tests/test_component.py index 974c06a50..b070eda52 100644 --- a/tests/test_component.py +++ b/tests/test_component.py @@ -423,7 +423,7 @@ def test_transmission_basic(self, basic_flow_system, highs_solver): 'Rohr', relative_losses=0.2, absolute_losses=20, - in1=fx.Flow('Rohr1', 'Wärme lokal', size=fx.InvestParameters(effects_per_size=5, maximum_size=1e6)), + in1=fx.Flow('Rohr1', 'Wärme lokal', size=fx.SizingParameters(effects_per_size=5, maximum_size=1e6)), out1=fx.Flow('Rohr2', 'Fernwärme', size=1000), ) @@ -480,10 +480,10 @@ def test_transmission_balanced(self, basic_flow_system, highs_solver): in1=fx.Flow( 'Rohr1a', bus='Wärme lokal', - size=fx.InvestParameters(effects_per_size=5, maximum_size=1000), + size=fx.SizingParameters(effects_per_size=5, maximum_size=1000), ), out1=fx.Flow('Rohr1b', 'Fernwärme', size=1000), - in2=fx.Flow('Rohr2a', 'Fernwärme', size=fx.InvestParameters()), + in2=fx.Flow('Rohr2a', 'Fernwärme', size=fx.SizingParameters()), out2=fx.Flow('Rohr2b', bus='Wärme lokal', size=1000), balanced=True, ) @@ -554,13 +554,13 @@ def test_transmission_unbalanced(self, basic_flow_system, highs_solver): in1=fx.Flow( 'Rohr1a', bus='Wärme lokal', - size=fx.InvestParameters(effects_per_size=50, maximum_size=1000), + size=fx.SizingParameters(effects_per_size=50, maximum_size=1000), ), out1=fx.Flow('Rohr1b', 'Fernwärme', size=1000), in2=fx.Flow( 'Rohr2a', 'Fernwärme', - size=fx.InvestParameters(effects_per_size=100, minimum_size=10, mandatory=True), + size=fx.SizingParameters(effects_per_size=100, minimum_size=10, mandatory=True), ), out2=fx.Flow('Rohr2b', bus='Wärme lokal', size=1000), balanced=False, diff --git a/tests/test_effect.py b/tests/test_effect.py index e3bf8368b..5e7ce2d9f 100644 --- a/tests/test_effect.py +++ b/tests/test_effect.py @@ -251,7 +251,7 @@ def test_shares(self, basic_flow_system_linopy_coords, coords_config): Q_th=fx.Flow( 'Q_th', bus='Fernwärme', - size=fx.InvestParameters(effects_per_size=10, minimum_size=20, mandatory=True), + size=fx.SizingParameters(effects_per_size=10, minimum_size=20, mandatory=True), ), Q_fu=fx.Flow('Q_fu', bus='Gas'), ), diff --git a/tests/test_flow.py b/tests/test_flow.py index 008a43be0..51f338ece 100644 --- a/tests/test_flow.py +++ b/tests/test_flow.py @@ -149,7 +149,7 @@ def test_flow_invest(self, basic_flow_system_linopy_coords, coords_config): flow = fx.Flow( 'Wärme', bus='Fernwärme', - size=fx.InvestParameters(minimum_size=20, maximum_size=100, mandatory=True), + size=fx.SizingParameters(minimum_size=20, maximum_size=100, mandatory=True), relative_minimum=np.linspace(0.1, 0.5, timesteps.size), relative_maximum=np.linspace(0.5, 1, timesteps.size), ) @@ -212,7 +212,7 @@ def test_flow_invest_optional(self, basic_flow_system_linopy_coords, coords_conf flow = fx.Flow( 'Wärme', bus='Fernwärme', - size=fx.InvestParameters(minimum_size=20, maximum_size=100, mandatory=False), + size=fx.SizingParameters(minimum_size=20, maximum_size=100, mandatory=False), relative_minimum=np.linspace(0.1, 0.5, timesteps.size), relative_maximum=np.linspace(0.5, 1, timesteps.size), ) @@ -287,7 +287,7 @@ def test_flow_invest_optional_wo_min_size(self, basic_flow_system_linopy_coords, flow = fx.Flow( 'Wärme', bus='Fernwärme', - size=fx.InvestParameters(maximum_size=100, mandatory=False), + size=fx.SizingParameters(maximum_size=100, mandatory=False), relative_minimum=np.linspace(0.1, 0.5, timesteps.size), relative_maximum=np.linspace(0.5, 1, timesteps.size), ) @@ -362,7 +362,7 @@ def test_flow_invest_wo_min_size_non_optional(self, basic_flow_system_linopy_coo flow = fx.Flow( 'Wärme', bus='Fernwärme', - size=fx.InvestParameters(maximum_size=100, mandatory=True), + size=fx.SizingParameters(maximum_size=100, mandatory=True), relative_minimum=np.linspace(0.1, 0.5, timesteps.size), relative_maximum=np.linspace(0.5, 1, timesteps.size), ) @@ -420,7 +420,7 @@ def test_flow_invest_fixed_size(self, basic_flow_system_linopy_coords, coords_co flow = fx.Flow( 'Wärme', bus='Fernwärme', - size=fx.InvestParameters(fixed_size=75, mandatory=True), + size=fx.SizingParameters(fixed_size=75, mandatory=True), relative_minimum=0.2, relative_maximum=0.9, ) @@ -455,7 +455,7 @@ def test_flow_invest_with_effects(self, basic_flow_system_linopy_coords, coords_ flow = fx.Flow( 'Wärme', bus='Fernwärme', - size=fx.InvestParameters( + size=fx.SizingParameters( minimum_size=20, maximum_size=100, mandatory=False, @@ -492,7 +492,7 @@ def test_flow_invest_divest_effects(self, basic_flow_system_linopy_coords, coord flow = fx.Flow( 'Wärme', bus='Fernwärme', - size=fx.InvestParameters( + size=fx.SizingParameters( minimum_size=20, maximum_size=100, mandatory=False, @@ -1074,7 +1074,7 @@ def test_flow_on_invest_optional(self, basic_flow_system_linopy_coords, coords_c flow = fx.Flow( 'Wärme', bus='Fernwärme', - size=fx.InvestParameters(minimum_size=20, maximum_size=200, mandatory=False), + size=fx.SizingParameters(minimum_size=20, maximum_size=200, mandatory=False), relative_minimum=0.2, relative_maximum=0.8, on_off_parameters=fx.OnOffParameters(), @@ -1175,7 +1175,7 @@ def test_flow_on_invest_non_optional(self, basic_flow_system_linopy_coords, coor flow = fx.Flow( 'Wärme', bus='Fernwärme', - size=fx.InvestParameters(minimum_size=20, maximum_size=200, mandatory=True), + size=fx.SizingParameters(minimum_size=20, maximum_size=200, mandatory=True), relative_minimum=0.2, relative_maximum=0.8, on_off_parameters=fx.OnOffParameters(), @@ -1302,7 +1302,7 @@ def test_fixed_profile_with_investment(self, basic_flow_system_linopy_coords, co flow = fx.Flow( 'Wärme', bus='Fernwärme', - size=fx.InvestParameters(minimum_size=50, maximum_size=200, mandatory=False), + size=fx.SizingParameters(minimum_size=50, maximum_size=200, mandatory=False), fixed_relative_profile=profile, ) diff --git a/tests/test_functional.py b/tests/test_functional.py index 9b47b40e3..9a8f1e72c 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -146,7 +146,7 @@ def test_fixed_size(solver_fixture, time_steps_fixture): Q_th=fx.Flow( 'Q_th', bus='Fernwärme', - size=fx.InvestParameters(fixed_size=1000, effects_of_size=10, effects_per_size=1), + size=fx.SizingParameters(fixed_size=1000, effects_of_size=10, effects_per_size=1), ), ) ) @@ -187,7 +187,7 @@ def test_optimize_size(solver_fixture, time_steps_fixture): Q_th=fx.Flow( 'Q_th', bus='Fernwärme', - size=fx.InvestParameters(effects_of_size=10, effects_per_size=1), + size=fx.SizingParameters(effects_of_size=10, effects_per_size=1), ), ) ) @@ -228,7 +228,7 @@ def test_size_bounds(solver_fixture, time_steps_fixture): Q_th=fx.Flow( 'Q_th', bus='Fernwärme', - size=fx.InvestParameters(minimum_size=40, effects_of_size=10, effects_per_size=1), + size=fx.SizingParameters(minimum_size=40, effects_of_size=10, effects_per_size=1), ), ) ) @@ -269,7 +269,7 @@ def test_optional_invest(solver_fixture, time_steps_fixture): Q_th=fx.Flow( 'Q_th', bus='Fernwärme', - size=fx.InvestParameters(mandatory=False, minimum_size=40, effects_of_size=10, effects_per_size=1), + size=fx.SizingParameters(mandatory=False, minimum_size=40, effects_of_size=10, effects_per_size=1), ), ), fx.linear_converters.Boiler( @@ -279,7 +279,7 @@ def test_optional_invest(solver_fixture, time_steps_fixture): Q_th=fx.Flow( 'Q_th', bus='Fernwärme', - size=fx.InvestParameters(mandatory=False, minimum_size=50, effects_of_size=10, effects_per_size=1), + size=fx.SizingParameters(mandatory=False, minimum_size=50, effects_of_size=10, effects_per_size=1), ), ), ) diff --git a/tests/test_invest_parameters_deprecation.py b/tests/test_invest_parameters_deprecation.py index 98926be6e..2d922336c 100644 --- a/tests/test_invest_parameters_deprecation.py +++ b/tests/test_invest_parameters_deprecation.py @@ -1,5 +1,5 @@ """ -Test backward compatibility and deprecation warnings for InvestParameters. +Test backward compatibility and deprecation warnings for SizingParameters. This test verifies that: 1. Old parameter names (fix_effects, specific_effects, divest_effects, piecewise_effects) still work with warnings @@ -11,18 +11,18 @@ import pytest -from flixopt.interface import InvestParameters +from flixopt.interface import SizingParameters class TestInvestParametersDeprecation: - """Test suite for InvestParameters parameter deprecation.""" + """Test suite for SizingParameters parameter deprecation.""" def test_new_parameters_no_warnings(self): """Test that new parameter names don't trigger warnings.""" with warnings.catch_warnings(): warnings.simplefilter('error', DeprecationWarning) # Should not raise DeprecationWarning - params = InvestParameters( + params = SizingParameters( fixed_size=100, effects_of_size={'cost': 25000}, effects_per_size={'cost': 1200}, @@ -35,7 +35,7 @@ def test_new_parameters_no_warnings(self): def test_old_fix_effects_deprecation_warning(self): """Test that fix_effects triggers deprecation warning.""" with pytest.warns(DeprecationWarning, match='fix_effects.*deprecated.*effects_of_size'): - params = InvestParameters(fix_effects={'cost': 25000}) + params = SizingParameters(fix_effects={'cost': 25000}) # Verify backward compatibility assert params.effects_of_size == {'cost': 25000} @@ -46,7 +46,7 @@ def test_old_fix_effects_deprecation_warning(self): def test_old_specific_effects_deprecation_warning(self): """Test that specific_effects triggers deprecation warning.""" with pytest.warns(DeprecationWarning, match='specific_effects.*deprecated.*effects_per_size'): - params = InvestParameters(specific_effects={'cost': 1200}) + params = SizingParameters(specific_effects={'cost': 1200}) # Verify backward compatibility assert params.effects_per_size == {'cost': 1200} @@ -57,7 +57,7 @@ def test_old_specific_effects_deprecation_warning(self): def test_old_divest_effects_deprecation_warning(self): """Test that divest_effects triggers deprecation warning.""" with pytest.warns(DeprecationWarning, match='divest_effects.*deprecated.*effects_of_retirement'): - params = InvestParameters(divest_effects={'cost': 5000}) + params = SizingParameters(divest_effects={'cost': 5000}) # Verify backward compatibility assert params.effects_of_retirement == {'cost': 5000} @@ -74,7 +74,7 @@ def test_old_piecewise_effects_deprecation_warning(self): piecewise_shares={'cost': Piecewise([Piece(800, 600)])}, ) with pytest.warns(DeprecationWarning, match='piecewise_effects.*deprecated.*piecewise_effects_of_investment'): - params = InvestParameters(piecewise_effects=test_piecewise) + params = SizingParameters(piecewise_effects=test_piecewise) # Verify backward compatibility assert params.piecewise_effects_of_investment is test_piecewise @@ -92,7 +92,7 @@ def test_all_old_parameters_together(self): ) with warnings.catch_warnings(record=True) as w: warnings.simplefilter('always', DeprecationWarning) - params = InvestParameters( + params = SizingParameters( fixed_size=100, fix_effects={'cost': 25000}, specific_effects={'cost': 1200}, @@ -123,7 +123,7 @@ def test_both_old_and_new_raises_error(self): """Test that specifying both old and new parameter names raises ValueError.""" # fix_effects + effects_of_size with pytest.raises(ValueError, match='Either fix_effects or effects_of_size can be specified, but not both'): - InvestParameters( + SizingParameters( fix_effects={'cost': 10000}, effects_of_size={'cost': 25000}, ) @@ -133,7 +133,7 @@ def test_both_old_and_new_raises_error(self): ValueError, match='Either specific_effects or effects_per_size can be specified, but not both', ): - InvestParameters( + SizingParameters( specific_effects={'cost': 1200}, effects_per_size={'cost': 1500}, ) @@ -142,7 +142,7 @@ def test_both_old_and_new_raises_error(self): with pytest.raises( ValueError, match='Either divest_effects or effects_of_retirement can be specified, but not both' ): - InvestParameters( + SizingParameters( divest_effects={'cost': 5000}, effects_of_retirement={'cost': 6000}, ) @@ -162,7 +162,7 @@ def test_both_old_and_new_raises_error(self): ValueError, match='Either piecewise_effects or piecewise_effects_of_investment can be specified, but not both', ): - InvestParameters( + SizingParameters( piecewise_effects=test_piecewise1, piecewise_effects_of_investment=test_piecewise2, ) @@ -179,7 +179,7 @@ def test_piecewise_effects_of_investment_new_parameter(self): with warnings.catch_warnings(): warnings.simplefilter('error', DeprecationWarning) # Should not raise DeprecationWarning when using new parameter - params = InvestParameters(piecewise_effects_of_investment=test_piecewise) + params = SizingParameters(piecewise_effects_of_investment=test_piecewise) assert params.piecewise_effects_of_investment is test_piecewise # Accessing deprecated property triggers warning @@ -195,7 +195,7 @@ def test_backward_compatibility_with_features(self): piecewise_shares={'cost': Piecewise([Piece(800, 600)])}, ) - params = InvestParameters( + params = SizingParameters( effects_of_size={'cost': 25000}, effects_per_size={'cost': 1200}, effects_of_retirement={'cost': 5000}, @@ -224,7 +224,7 @@ def test_backward_compatibility_with_features(self): def test_empty_parameters(self): """Test that empty/None parameters work correctly.""" - params = InvestParameters() + params = SizingParameters() assert params.effects_of_size == {} assert params.effects_per_size == {} @@ -245,7 +245,7 @@ def test_mixed_old_and_new_parameters(self): """Test mixing old and new parameter names (not recommended but should work).""" with warnings.catch_warnings(record=True) as w: warnings.simplefilter('always', DeprecationWarning) - params = InvestParameters( + params = SizingParameters( effects_of_size={'cost': 25000}, # New specific_effects={'cost': 1200}, # Old effects_of_retirement={'cost': 5000}, # New @@ -263,33 +263,33 @@ def test_unexpected_keyword_arguments(self): """Test that unexpected keyword arguments raise TypeError.""" # Single unexpected argument with pytest.raises( - TypeError, match="InvestParameters.__init__\\(\\) got unexpected keyword argument\\(s\\): 'invalid_param'" + TypeError, match="SizingParameters.__init__\\(\\) got unexpected keyword argument\\(s\\): 'invalid_param'" ): - InvestParameters(invalid_param='value') + SizingParameters(invalid_param='value') # Multiple unexpected arguments with pytest.raises( TypeError, - match="InvestParameters.__init__\\(\\) got unexpected keyword argument\\(s\\): 'param1', 'param2'", + match="SizingParameters.__init__\\(\\) got unexpected keyword argument\\(s\\): 'param1', 'param2'", ): - InvestParameters(param1='value1', param2='value2') + SizingParameters(param1='value1', param2='value2') # Mix of valid and invalid arguments with pytest.raises( - TypeError, match="InvestParameters.__init__\\(\\) got unexpected keyword argument\\(s\\): 'typo'" + TypeError, match="SizingParameters.__init__\\(\\) got unexpected keyword argument\\(s\\): 'typo'" ): - InvestParameters(effects_of_size={'cost': 100}, typo='value') + SizingParameters(effects_of_size={'cost': 100}, typo='value') def test_optional_parameter_deprecation(self): """Test that optional parameter triggers deprecation warning and maps to mandatory.""" # Test optional=True (should map to mandatory=False) with pytest.warns(DeprecationWarning, match='optional.*deprecated.*mandatory'): - params = InvestParameters(optional=True) + params = SizingParameters(optional=True) assert params.mandatory is False # Test optional=False (should map to mandatory=True) with pytest.warns(DeprecationWarning, match='optional.*deprecated.*mandatory'): - params = InvestParameters(optional=False) + params = SizingParameters(optional=False) assert params.mandatory is True def test_mandatory_parameter_no_warning(self): @@ -297,16 +297,16 @@ def test_mandatory_parameter_no_warning(self): with warnings.catch_warnings(): warnings.simplefilter('error', DeprecationWarning) # Test mandatory=True - params = InvestParameters(mandatory=True) + params = SizingParameters(mandatory=True) assert params.mandatory is True # Test mandatory=False (explicit) - params = InvestParameters(mandatory=False) + params = SizingParameters(mandatory=False) assert params.mandatory is False def test_mandatory_default_value(self): """Test that default value of mandatory is False when neither optional nor mandatory is specified.""" - params = InvestParameters() + params = SizingParameters() assert params.mandatory is False def test_both_optional_and_mandatory_no_error(self): @@ -319,18 +319,18 @@ def test_both_optional_and_mandatory_no_error(self): """ # When both are specified, optional takes precedence (with deprecation warning) with pytest.warns(DeprecationWarning, match='optional.*deprecated.*mandatory'): - params = InvestParameters(optional=True, mandatory=False) + params = SizingParameters(optional=True, mandatory=False) # optional=True should result in mandatory=False assert params.mandatory is False with pytest.warns(DeprecationWarning, match='optional.*deprecated.*mandatory'): - params = InvestParameters(optional=False, mandatory=True) + params = SizingParameters(optional=False, mandatory=True) # optional=False should result in mandatory=True (optional takes precedence) assert params.mandatory is True def test_optional_property_deprecation(self): """Test that accessing optional property triggers deprecation warning.""" - params = InvestParameters(mandatory=True) + params = SizingParameters(mandatory=True) # Reading the property triggers warning with pytest.warns(DeprecationWarning, match="Property 'optional' is deprecated"): diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py index a42c95ec0..fbef6e298 100644 --- a/tests/test_scenarios.py +++ b/tests/test_scenarios.py @@ -4,7 +4,7 @@ from linopy.testing import assert_linequal import flixopt as fx -from flixopt.commons import Effect, InvestParameters, Sink, Source, Storage +from flixopt.commons import Effect, Sink, SizingParameters, Source, Storage from flixopt.elements import Bus, Flow from flixopt.flow_system import FlowSystem @@ -62,7 +62,7 @@ def test_system(): power_gen = Flow( label='Generation', bus=electricity_bus.label_full, - size=InvestParameters( + size=SizingParameters( minimum_size=0, maximum_size=20, effects_per_size={'costs': 100}, # €/kW @@ -78,7 +78,7 @@ def test_system(): label='Battery', charging=storage_charge, discharging=storage_discharge, - capacity_in_flow_hours=InvestParameters( + capacity_in_flow_hours=SizingParameters( minimum_size=0, maximum_size=50, effects_per_size={'costs': 50}, # €/kWh @@ -149,7 +149,7 @@ def flow_system_complex_scenarios() -> fx.FlowSystem: relative_minimum=5 / 50, relative_maximum=1, previous_flow_rate=50, - size=fx.InvestParameters( + size=fx.SizingParameters( effects_of_size=1000, fixed_size=50, mandatory=True, @@ -169,7 +169,7 @@ def flow_system_complex_scenarios() -> fx.FlowSystem: Q_fu=fx.Flow('Q_fu', bus='Gas', size=200, relative_minimum=0, relative_maximum=1), ) - invest_speicher = fx.InvestParameters( + invest_speicher = fx.SizingParameters( effects_of_size=0, piecewise_effects_of_investment=fx.PiecewiseEffects( piecewise_origin=fx.Piecewise([fx.Piece(5, 25), fx.Piece(25, 100)]), @@ -459,7 +459,7 @@ def test_size_equality_constraints(): fx.Flow( label='out', bus='grid', - size=fx.InvestParameters( + size=fx.SizingParameters( minimum_size=10, maximum_size=100, effects_per_size={'cost': 100}, @@ -499,7 +499,7 @@ def test_flow_rate_equality_constraints(): fx.Flow( label='out', bus='grid', - size=fx.InvestParameters( + size=fx.SizingParameters( minimum_size=10, maximum_size=100, effects_per_size={'cost': 100}, @@ -539,7 +539,7 @@ def test_selective_scenario_independence(): fx.Flow( label='out', bus='grid', - size=fx.InvestParameters(minimum_size=10, maximum_size=100, effects_per_size={'cost': 100}), + size=fx.SizingParameters(minimum_size=10, maximum_size=100, effects_per_size={'cost': 100}), ) ], ) @@ -565,7 +565,7 @@ def test_selective_scenario_independence(): ] assert len(solar_flow_constraints) == 0 - # Demand should NOT have size constraints (no InvestParameters, size is fixed) + # Demand should NOT have size constraints (no SizingParameters, size is fixed) demand_size_constraints = [c for c in constraint_names if 'demand(in)|size' in c and 'scenario_independent' in c] assert len(demand_size_constraints) == 0 @@ -599,7 +599,7 @@ def test_scenario_parameters_io_persistence(): fx.Flow( label='out', bus='grid', - size=fx.InvestParameters(minimum_size=10, maximum_size=100, effects_per_size={'cost': 100}), + size=fx.SizingParameters(minimum_size=10, maximum_size=100, effects_per_size={'cost': 100}), ) ], ) @@ -640,7 +640,7 @@ def test_scenario_parameters_io_with_calculation(): fx.Flow( label='out', bus='grid', - size=fx.InvestParameters(minimum_size=10, maximum_size=100, effects_per_size={'cost': 100}), + size=fx.SizingParameters(minimum_size=10, maximum_size=100, effects_per_size={'cost': 100}), ) ], ) diff --git a/tests/test_storage.py b/tests/test_storage.py index e8fd6716a..3fde24e0b 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -260,7 +260,7 @@ def test_storage_with_investment(self, basic_flow_system_linopy_coords, coords_c 'InvestStorage', charging=fx.Flow('Q_th_in', bus='Fernwärme', size=20), discharging=fx.Flow('Q_th_out', bus='Fernwärme', size=20), - capacity_in_flow_hours=fx.InvestParameters( + capacity_in_flow_hours=fx.SizingParameters( effects_of_size=100, effects_per_size=10, minimum_size=20, @@ -459,7 +459,7 @@ def test_investment_parameters( 'InvestStorage', charging=fx.Flow('Q_th_in', bus='Fernwärme', size=20), discharging=fx.Flow('Q_th_out', bus='Fernwärme', size=20), - capacity_in_flow_hours=fx.InvestParameters(**invest_params), + capacity_in_flow_hours=fx.SizingParameters(**invest_params), initial_charge_state=0, eta_charge=0.9, eta_discharge=0.9, From 15896b9a01c44505f116ca11602ac115bec46d6d Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 1 Nov 2025 09:29:25 +0100 Subject: [PATCH 403/448] Rename piecewise_effects_of_investment to piecewise_effects_per_size --- examples/02_Complex/complex_example.py | 2 +- flixopt/features.py | 6 ++--- flixopt/interface.py | 28 ++++++++++----------- tests/conftest.py | 2 +- tests/test_invest_parameters_deprecation.py | 28 ++++++++++----------- tests/test_scenarios.py | 2 +- 6 files changed, 34 insertions(+), 34 deletions(-) diff --git a/examples/02_Complex/complex_example.py b/examples/02_Complex/complex_example.py index e73192fc2..77547e932 100644 --- a/examples/02_Complex/complex_example.py +++ b/examples/02_Complex/complex_example.py @@ -132,7 +132,7 @@ charging=fx.Flow('Q_th_load', bus='Fernwärme', size=1e4), discharging=fx.Flow('Q_th_unload', bus='Fernwärme', size=1e4), capacity_in_flow_hours=fx.SizingParameters( - piecewise_effects_of_investment=segmented_investment_effects, # Investment effects + piecewise_effects_per_size=segmented_investment_effects, # Investment effects mandatory=True, # Forced investment minimum_size=0, maximum_size=1000, # Optimizing between 0 and 1000 kWh diff --git a/flixopt/features.py b/flixopt/features.py index c4082d21b..7b1f15010 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -206,14 +206,14 @@ def _add_effects(self): target='periodic', ) - if self.parameters.piecewise_effects_of_investment: + if self.parameters.piecewise_effects_per_size: self.piecewise_effects = self.add_submodels( PiecewiseEffectsModel( model=self._model, label_of_element=self.label_of_element, label_of_model=f'{self.label_of_element}|PiecewiseEffects', - piecewise_origin=(self.size.name, self.parameters.piecewise_effects_of_investment.piecewise_origin), - piecewise_shares=self.parameters.piecewise_effects_of_investment.piecewise_shares, + piecewise_origin=(self.size.name, self.parameters.piecewise_effects_per_size.piecewise_origin), + piecewise_shares=self.parameters.piecewise_effects_per_size.piecewise_shares, zero_point=self.invested, ), short_name='segments', diff --git a/flixopt/interface.py b/flixopt/interface.py index 869d01c52..c8a2b8214 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -709,7 +709,7 @@ class SizingParameters(Interface): Dict: {'effect_name': value} (e.g., {'cost': 10000}). effects_per_size: Variable costs proportional to size (per-unit costs). Dict: {'effect_name': value/unit} (e.g., {'cost': 1200}). - piecewise_effects_of_investment: Non-linear costs using PiecewiseEffects. + piecewise_effects_per_size: Non-linear costs using PiecewiseEffects. Combinable with effects_of_size and effects_per_size. linked_periods: Describes which periods are linked. 1 means linked, 0 means size=0. None means no linked periods. For convenience, pass a tuple containing the first and last period (2025, 2039), linking them and those in between @@ -719,7 +719,7 @@ class SizingParameters(Interface): Will be removed in version 4.0. specific_effects: **Deprecated**. Use `effects_per_size` instead. Will be removed in version 4.0. - piecewise_effects: **Deprecated**. Use `piecewise_effects_of_investment` instead. + piecewise_effects: **Deprecated**. Use `piecewise_effects_per_size` instead. Will be removed in version 4.0. optional: DEPRECATED. Use `mandatory` instead. Opposite of `mandatory`. Will be removed in version 4.0. @@ -763,7 +763,7 @@ class SizingParameters(Interface): 'cost': 5000, # Grid connection and control system 'installation_time': 2, # Days for fixed components }, - piecewise_effects_of_investment=PiecewiseEffects( + piecewise_effects_per_size=PiecewiseEffects( piecewise_origin=Piecewise( [ Piece(0, 100), # Small systems @@ -827,7 +827,7 @@ class SizingParameters(Interface): hydrogen_electrolyzer = SizingParameters( minimum_size=1, maximum_size=50, # MW - piecewise_effects_of_investment=PiecewiseEffects( + piecewise_effects_per_size=PiecewiseEffects( piecewise_origin=Piecewise( [ Piece(0, 5), # Small scale: early adoption @@ -873,7 +873,7 @@ def __init__( mandatory: bool = False, effects_of_size: PeriodicEffectsUser | None = None, effects_per_size: PeriodicEffectsUser | None = None, - piecewise_effects_of_investment: PiecewiseEffects | None = None, + piecewise_effects_per_size: PiecewiseEffects | None = None, linked_periods: PeriodicDataUser | tuple[int, int] | None = None, **kwargs, ): @@ -882,8 +882,8 @@ def __init__( effects_per_size = self._handle_deprecated_kwarg( kwargs, 'specific_effects', 'effects_per_size', effects_per_size ) - piecewise_effects_of_investment = self._handle_deprecated_kwarg( - kwargs, 'piecewise_effects', 'piecewise_effects_of_investment', piecewise_effects_of_investment + piecewise_effects_per_size = self._handle_deprecated_kwarg( + kwargs, 'piecewise_effects', 'piecewise_effects_per_size', piecewise_effects_per_size ) # For mandatory parameter with non-None default, disable conflict checking if 'optional' in kwargs: @@ -903,7 +903,7 @@ def __init__( self.fixed_size = fixed_size self.mandatory = mandatory self.effects_per_size: PeriodicEffectsUser = effects_per_size if effects_per_size is not None else {} - self.piecewise_effects_of_investment = piecewise_effects_of_investment + self.piecewise_effects_per_size = piecewise_effects_per_size self.minimum_size = minimum_size if minimum_size is not None else CONFIG.Modeling.epsilon self.maximum_size = maximum_size if maximum_size is not None else CONFIG.Modeling.big # default maximum self.linked_periods = linked_periods @@ -922,9 +922,9 @@ def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None dims=['period', 'scenario'], ) - if self.piecewise_effects_of_investment is not None: - self.piecewise_effects_of_investment.has_time_dim = False - self.piecewise_effects_of_investment.transform_data(flow_system, f'{name_prefix}|PiecewiseEffects') + if self.piecewise_effects_per_size is not None: + self.piecewise_effects_per_size.has_time_dim = False + self.piecewise_effects_per_size.transform_data(flow_system, f'{name_prefix}|PiecewiseEffects') self.minimum_size = flow_system.fit_to_model_coords( f'{name_prefix}|minimum_size', self.minimum_size, dims=['period', 'scenario'] @@ -999,13 +999,13 @@ def specific_effects(self) -> PeriodicEffectsUser: @property def piecewise_effects(self) -> PiecewiseEffects | None: - """Deprecated property. Use piecewise_effects_of_investment instead.""" + """Deprecated property. Use piecewise_effects_per_size instead.""" warnings.warn( - 'The piecewise_effects property is deprecated. Use piecewise_effects_of_investment instead.', + 'The piecewise_effects property is deprecated. Use piecewise_effects_per_size instead.', DeprecationWarning, stacklevel=2, ) - return self.piecewise_effects_of_investment + return self.piecewise_effects_per_size @property def minimum_or_fixed_size(self) -> PeriodicData: diff --git a/tests/conftest.py b/tests/conftest.py index 87d28339e..7c3818316 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -282,7 +282,7 @@ def complex(): """Complex storage with piecewise investment from flow_system_complex""" invest_speicher = fx.SizingParameters( effects_of_size=0, - piecewise_effects_of_investment=fx.PiecewiseEffects( + piecewise_effects_per_size=fx.PiecewiseEffects( piecewise_origin=fx.Piecewise([fx.Piece(5, 25), fx.Piece(25, 100)]), piecewise_shares={ 'costs': fx.Piecewise([fx.Piece(50, 250), fx.Piece(250, 800)]), diff --git a/tests/test_invest_parameters_deprecation.py b/tests/test_invest_parameters_deprecation.py index 2d922336c..d8503e99b 100644 --- a/tests/test_invest_parameters_deprecation.py +++ b/tests/test_invest_parameters_deprecation.py @@ -3,7 +3,7 @@ This test verifies that: 1. Old parameter names (fix_effects, specific_effects, divest_effects, piecewise_effects) still work with warnings -2. New parameter names (effects_of_size, effects_per_size, effects_of_retirement, piecewise_effects_of_investment) work correctly +2. New parameter names (effects_of_size, effects_per_size, effects_of_retirement, piecewise_effects_per_size) work correctly 3. Both old and new approaches produce equivalent results """ @@ -73,13 +73,13 @@ def test_old_piecewise_effects_deprecation_warning(self): piecewise_origin=Piecewise([Piece(0, 100)]), piecewise_shares={'cost': Piecewise([Piece(800, 600)])}, ) - with pytest.warns(DeprecationWarning, match='piecewise_effects.*deprecated.*piecewise_effects_of_investment'): + with pytest.warns(DeprecationWarning, match='piecewise_effects.*deprecated.*piecewise_effects_per_size'): params = SizingParameters(piecewise_effects=test_piecewise) # Verify backward compatibility - assert params.piecewise_effects_of_investment is test_piecewise + assert params.piecewise_effects_per_size is test_piecewise # Accessing the property also triggers warning - with pytest.warns(DeprecationWarning, match='piecewise_effects.*deprecated.*piecewise_effects_of_investment'): + with pytest.warns(DeprecationWarning, match='piecewise_effects.*deprecated.*piecewise_effects_per_size'): assert params.piecewise_effects is test_piecewise def test_all_old_parameters_together(self): @@ -107,7 +107,7 @@ def test_all_old_parameters_together(self): assert params.effects_of_size == {'cost': 25000} assert params.effects_per_size == {'cost': 1200} assert params.effects_of_retirement == {'cost': 5000} - assert params.piecewise_effects_of_investment is test_piecewise + assert params.piecewise_effects_per_size is test_piecewise # Verify old attributes still work (accessing deprecated properties - triggers warnings) with pytest.warns(DeprecationWarning): @@ -147,7 +147,7 @@ def test_both_old_and_new_raises_error(self): effects_of_retirement={'cost': 6000}, ) - # piecewise_effects + piecewise_effects_of_investment + # piecewise_effects + piecewise_effects_per_size from flixopt.interface import Piece, Piecewise, PiecewiseEffects test_piecewise1 = PiecewiseEffects( @@ -160,15 +160,15 @@ def test_both_old_and_new_raises_error(self): ) with pytest.raises( ValueError, - match='Either piecewise_effects or piecewise_effects_of_investment can be specified, but not both', + match='Either piecewise_effects or piecewise_effects_per_size can be specified, but not both', ): SizingParameters( piecewise_effects=test_piecewise1, - piecewise_effects_of_investment=test_piecewise2, + piecewise_effects_per_size=test_piecewise2, ) def test_piecewise_effects_of_investment_new_parameter(self): - """Test that piecewise_effects_of_investment works correctly.""" + """Test that piecewise_effects_per_size works correctly.""" from flixopt.interface import Piece, Piecewise, PiecewiseEffects test_piecewise = PiecewiseEffects( @@ -179,8 +179,8 @@ def test_piecewise_effects_of_investment_new_parameter(self): with warnings.catch_warnings(): warnings.simplefilter('error', DeprecationWarning) # Should not raise DeprecationWarning when using new parameter - params = SizingParameters(piecewise_effects_of_investment=test_piecewise) - assert params.piecewise_effects_of_investment is test_piecewise + params = SizingParameters(piecewise_effects_per_size=test_piecewise) + assert params.piecewise_effects_per_size is test_piecewise # Accessing deprecated property triggers warning with pytest.warns(DeprecationWarning): @@ -199,7 +199,7 @@ def test_backward_compatibility_with_features(self): effects_of_size={'cost': 25000}, effects_per_size={'cost': 1200}, effects_of_retirement={'cost': 5000}, - piecewise_effects_of_investment=test_piecewise, + piecewise_effects_per_size=test_piecewise, ) # Old properties should still be accessible (for features.py) but with warnings @@ -220,7 +220,7 @@ def test_backward_compatibility_with_features(self): with pytest.warns(DeprecationWarning): assert params.divest_effects is params.effects_of_retirement with pytest.warns(DeprecationWarning): - assert params.piecewise_effects is params.piecewise_effects_of_investment + assert params.piecewise_effects is params.piecewise_effects_per_size def test_empty_parameters(self): """Test that empty/None parameters work correctly.""" @@ -229,7 +229,7 @@ def test_empty_parameters(self): assert params.effects_of_size == {} assert params.effects_per_size == {} assert params.effects_of_retirement == {} - assert params.piecewise_effects_of_investment is None + assert params.piecewise_effects_per_size is None # Old properties should also be empty (but with warnings) with pytest.warns(DeprecationWarning): diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py index fbef6e298..bcd829080 100644 --- a/tests/test_scenarios.py +++ b/tests/test_scenarios.py @@ -171,7 +171,7 @@ def flow_system_complex_scenarios() -> fx.FlowSystem: invest_speicher = fx.SizingParameters( effects_of_size=0, - piecewise_effects_of_investment=fx.PiecewiseEffects( + piecewise_effects_per_size=fx.PiecewiseEffects( piecewise_origin=fx.Piecewise([fx.Piece(5, 25), fx.Piece(25, 100)]), piecewise_shares={ 'costs': fx.Piecewise([fx.Piece(50, 250), fx.Piece(250, 800)]), From 0d0e30180d2f43d02ca45c70a0147778ddf27064 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 1 Nov 2025 09:31:48 +0100 Subject: [PATCH 404/448] Remove linked_periods --- flixopt/features.py | 22 ---------------------- flixopt/interface.py | 42 ------------------------------------------ 2 files changed, 64 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index 7b1f15010..6f1540dfc 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -58,10 +58,6 @@ def _do_modeling(self): def _create_variables_and_constraints(self): size_min, size_max = (self.parameters.minimum_or_fixed_size, self.parameters.maximum_or_fixed_size) - if self.parameters.linked_periods is not None: - # Mask size bounds: linked_periods is a binary DataArray that zeros out non-linked periods - size_min = size_min * self.parameters.linked_periods - size_max = size_max * self.parameters.linked_periods self.add_variables( short_name='size', @@ -83,13 +79,6 @@ def _create_variables_and_constraints(self): bounds=(self.parameters.minimum_or_fixed_size, self.parameters.maximum_or_fixed_size), ) - if self.parameters.linked_periods is not None: - masked_size = self.size.where(self.parameters.linked_periods, drop=True) - self.add_constraints( - masked_size.isel(period=slice(None, -1)) == masked_size.isel(period=slice(1, None)), - short_name='linked_periods', - ) - def _add_effects(self): """Add size-dependent effects""" if self.parameters.specific_effects: @@ -145,10 +134,6 @@ def _do_modeling(self): def _create_variables_and_constraints(self): size_min, size_max = (self.parameters.minimum_or_fixed_size, self.parameters.maximum_or_fixed_size) - if self.parameters.linked_periods is not None: - # Mask size bounds: linked_periods is a binary DataArray that zeros out non-linked periods - size_min = size_min * self.parameters.linked_periods - size_max = size_max * self.parameters.linked_periods self.add_variables( short_name='size', @@ -170,13 +155,6 @@ def _create_variables_and_constraints(self): bounds=(self.parameters.minimum_or_fixed_size, self.parameters.maximum_or_fixed_size), ) - if self.parameters.linked_periods is not None: - masked_size = self.size.where(self.parameters.linked_periods, drop=True) - self.add_constraints( - masked_size.isel(period=slice(None, -1)) == masked_size.isel(period=slice(1, None)), - short_name='linked_periods', - ) - def _add_effects(self): """Add investment effects""" if self.parameters.effects_of_size: diff --git a/flixopt/interface.py b/flixopt/interface.py index c8a2b8214..4a0686839 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -711,8 +711,6 @@ class SizingParameters(Interface): Dict: {'effect_name': value/unit} (e.g., {'cost': 1200}). piecewise_effects_per_size: Non-linear costs using PiecewiseEffects. Combinable with effects_of_size and effects_per_size. - linked_periods: Describes which periods are linked. 1 means linked, 0 means size=0. None means no linked periods. - For convenience, pass a tuple containing the first and last period (2025, 2039), linking them and those in between Deprecated Args: fix_effects: **Deprecated**. Use `effects_of_size` instead. @@ -874,7 +872,6 @@ def __init__( effects_of_size: PeriodicEffectsUser | None = None, effects_per_size: PeriodicEffectsUser | None = None, piecewise_effects_per_size: PiecewiseEffects | None = None, - linked_periods: PeriodicDataUser | tuple[int, int] | None = None, **kwargs, ): # Handle deprecated parameters using centralized helper @@ -906,7 +903,6 @@ def __init__( self.piecewise_effects_per_size = piecewise_effects_per_size self.minimum_size = minimum_size if minimum_size is not None else CONFIG.Modeling.epsilon self.maximum_size = maximum_size if maximum_size is not None else CONFIG.Modeling.big # default maximum - self.linked_periods = linked_periods def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None: self.effects_of_size = flow_system.fit_effects_to_model_coords( @@ -932,33 +928,6 @@ def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None self.maximum_size = flow_system.fit_to_model_coords( f'{name_prefix}|maximum_size', self.maximum_size, dims=['period', 'scenario'] ) - # Convert tuple (first_period, last_period) to DataArray if needed - if isinstance(self.linked_periods, (tuple, list)): - if len(self.linked_periods) != 2: - raise TypeError( - f'If you provide a tuple to "linked_periods", it needs to be len=2. Got {len(self.linked_periods)=}' - ) - if flow_system.periods is None: - raise ValueError( - f'Cannot use linked_periods={self.linked_periods} when FlowSystem has no periods defined. ' - f'Please define periods in FlowSystem or use linked_periods=None.' - ) - logger.debug(f'Computing linked_periods from {self.linked_periods}') - start, end = self.linked_periods - if start not in flow_system.periods.values: - logger.warning( - f'Start of linked periods ({start} not found in periods directly: {flow_system.periods.values}' - ) - if end not in flow_system.periods.values: - logger.warning( - f'End of linked periods ({end} not found in periods directly: {flow_system.periods.values}' - ) - self.linked_periods = self.compute_linked_periods(start, end, flow_system.periods) - logger.debug(f'Computed {self.linked_periods=}') - - self.linked_periods = flow_system.fit_to_model_coords( - f'{name_prefix}|linked_periods', self.linked_periods, dims=['period', 'scenario'] - ) self.fixed_size = flow_system.fit_to_model_coords( f'{name_prefix}|fixed_size', self.fixed_size, dims=['period', 'scenario'] ) @@ -1036,17 +1005,6 @@ def format_for_repr(self) -> str: parts.append(f'max: {numeric_to_str_for_repr(self.maximum_size)}') return ', '.join(parts) if parts else 'invest' - @staticmethod - def compute_linked_periods(first_period: int, last_period: int, periods: pd.Index | list[int]) -> xr.DataArray: - return xr.DataArray( - xr.where( - (first_period <= np.array(periods)) & (np.array(periods) <= last_period), - 1, - 0, - ), - coords=(pd.Index(periods, name='period'),), - ).rename('linked_periods') - InvestmentPeriodData = PeriodicDataUser """This datatype is used to define things related to the period of investment.""" From 1510a5b6cb1ea92797b326cdf8a348526c9c5c15 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 1 Nov 2025 12:20:57 +0100 Subject: [PATCH 405/448] Update --- flixopt/elements.py | 4 -- flixopt/interface.py | 143 ++++++++++--------------------------------- 2 files changed, 33 insertions(+), 114 deletions(-) diff --git a/flixopt/elements.py b/flixopt/elements.py index 2b48bf4d7..62e636889 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -546,10 +546,6 @@ def size_is_fixed(self) -> bool: # Wenn kein SizingParameters existiert --> True; Wenn Investparameter, den Wert davon nehmen return False if (isinstance(self.size, SizingParameters) and self.size.fixed_size is None) else True - def _format_invest_params(self, params: SizingParameters) -> str: - """Format SizingParameters for display.""" - return f'size: {params.format_for_repr()}' - class FlowModel(ElementModel): element: Flow # Type hint diff --git a/flixopt/interface.py b/flixopt/interface.py index 4a0686839..db1adeb90 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -868,34 +868,11 @@ def __init__( fixed_size: PeriodicDataUser | None = None, minimum_size: PeriodicDataUser | None = None, maximum_size: PeriodicDataUser | None = None, - mandatory: bool = False, + mandatory: bool | xr.DataArray = False, effects_of_size: PeriodicEffectsUser | None = None, effects_per_size: PeriodicEffectsUser | None = None, piecewise_effects_per_size: PiecewiseEffects | None = None, - **kwargs, ): - # Handle deprecated parameters using centralized helper - effects_of_size = self._handle_deprecated_kwarg(kwargs, 'fix_effects', 'effects_of_size', effects_of_size) - effects_per_size = self._handle_deprecated_kwarg( - kwargs, 'specific_effects', 'effects_per_size', effects_per_size - ) - piecewise_effects_per_size = self._handle_deprecated_kwarg( - kwargs, 'piecewise_effects', 'piecewise_effects_per_size', piecewise_effects_per_size - ) - # For mandatory parameter with non-None default, disable conflict checking - if 'optional' in kwargs: - warnings.warn( - 'Deprecated parameter "optional" used. Check conflicts with new parameter "mandatory" manually!', - DeprecationWarning, - stacklevel=2, - ) - mandatory = self._handle_deprecated_kwarg( - kwargs, 'optional', 'mandatory', mandatory, transform=lambda x: not x, check_conflict=False - ) - - # Validate any remaining unexpected kwargs - self._validate_kwargs(kwargs) - self.effects_of_size: PeriodicEffectsUser = effects_of_size if effects_of_size is not None else {} self.fixed_size = fixed_size self.mandatory = mandatory @@ -931,50 +908,9 @@ def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None self.fixed_size = flow_system.fit_to_model_coords( f'{name_prefix}|fixed_size', self.fixed_size, dims=['period', 'scenario'] ) - - @property - def optional(self) -> bool: - """DEPRECATED: Use 'mandatory' property instead. Returns the opposite of 'mandatory'.""" - import warnings - - warnings.warn("Property 'optional' is deprecated. Use 'mandatory' instead.", DeprecationWarning, stacklevel=2) - return not self.mandatory - - @optional.setter - def optional(self, value: bool): - """DEPRECATED: Use 'mandatory' property instead. Sets the opposite of the given value to 'mandatory'.""" - warnings.warn("Property 'optional' is deprecated. Use 'mandatory' instead.", DeprecationWarning, stacklevel=2) - self.mandatory = not value - - @property - def fix_effects(self) -> PeriodicEffectsUser: - """Deprecated property. Use effects_of_size instead.""" - warnings.warn( - 'The fix_effects property is deprecated. Use effects_of_size instead.', - DeprecationWarning, - stacklevel=2, + self.mandatory = flow_system.fit_to_model_coords( + f'{name_prefix}|mandatory', self.fixed_size, dims=['period', 'scenario'] ) - return self.effects_of_size - - @property - def specific_effects(self) -> PeriodicEffectsUser: - """Deprecated property. Use effects_per_size instead.""" - warnings.warn( - 'The specific_effects property is deprecated. Use effects_per_size instead.', - DeprecationWarning, - stacklevel=2, - ) - return self.effects_per_size - - @property - def piecewise_effects(self) -> PiecewiseEffects | None: - """Deprecated property. Use piecewise_effects_per_size instead.""" - warnings.warn( - 'The piecewise_effects property is deprecated. Use piecewise_effects_per_size instead.', - DeprecationWarning, - stacklevel=2, - ) - return self.piecewise_effects_per_size @property def minimum_or_fixed_size(self) -> PeriodicData: @@ -984,27 +920,6 @@ def minimum_or_fixed_size(self) -> PeriodicData: def maximum_or_fixed_size(self) -> PeriodicData: return self.fixed_size if self.fixed_size is not None else self.maximum_size - def format_for_repr(self) -> str: - """Format SizingParameters for display in repr methods. - - Returns: - Formatted string showing size information - """ - from .io import numeric_to_str_for_repr - - if self.fixed_size is not None: - val = numeric_to_str_for_repr(self.fixed_size) - status = 'mandatory' if self.mandatory else 'optional' - return f'{val} ({status})' - - # Show range if available - parts = [] - if self.minimum_size is not None: - parts.append(f'min: {numeric_to_str_for_repr(self.minimum_size)}') - if self.maximum_size is not None: - parts.append(f'max: {numeric_to_str_for_repr(self.maximum_size)}') - return ', '.join(parts) if parts else 'invest' - InvestmentPeriodData = PeriodicDataUser """This datatype is used to define things related to the period of investment.""" @@ -1013,7 +928,7 @@ def format_for_repr(self) -> str: @register_class_for_io -class InvestmentParameters(Interface): +class InvestmentParameters(SizingParameters): """Define investment timing parameters with fixed lifetime. This class models WHEN to invest with a fixed lifetime duration. @@ -1040,13 +955,13 @@ class InvestmentParameters(Interface): Once invested, the asset operates for this many periods. allow_investment: Allow investment in specific periods. Default: True (all periods). force_investment: Force investment to occur in a specific period. Default: False. - fixed_effects_by_investment_period: Effects that depend on when investment occurs. + effects_of_investment: Effects that depend on when investment occurs. Dict mapping effect names to xr.DataArray with dimensions [period, scenario, investment_period]. These effects can vary by the investment period, enabling modeling of: - Technology learning curves (costs decrease over time) - Time-varying financing costs - Period-specific subsidies or regulations - specific_effects_by_investment_period: Size-dependent effects that also depend on investment period. + effects_of_investment_per_size: Size-dependent effects that also depend on investment period. Dict mapping effect names to xr.DataArray with dimensions [period, scenario, investment_period]. Examples: @@ -1092,7 +1007,7 @@ class InvestmentParameters(Interface): timing = InvestmentParameters( fixed_lifetime=10, - specific_effects_by_investment_period={ + effects_of_investment_per_size={ 'cost': learning_costs # €/kW depends on investment year }, ) @@ -1122,8 +1037,9 @@ def __init__( fixed_lifetime: Scalar, allow_investment: InvestmentPeriodDataBool = True, force_investment: InvestmentPeriodDataBool = False, - fixed_effects_by_investment_period: dict[str, xr.DataArray] | None = None, - specific_effects_by_investment_period: dict[str, xr.DataArray] | None = None, + effects_of_investment: dict[str, xr.DataArray] | None = None, + effects_of_investment_per_size: dict[str, xr.DataArray] | None = None, + **kwargs, ): if fixed_lifetime is None: raise ValueError('InvestmentParameters requires fixed_lifetime to be specified.') @@ -1132,15 +1048,17 @@ def __init__( self.allow_investment = allow_investment self.force_investment = force_investment - self.fixed_effects_by_investment_period: dict[str, xr.DataArray] = ( - fixed_effects_by_investment_period if fixed_effects_by_investment_period is not None else {} + self.effects_of_investment: dict[str, xr.DataArray] = ( + effects_of_investment if effects_of_investment is not None else {} ) - self.specific_effects_by_investment_period: dict[str, xr.DataArray] = ( - specific_effects_by_investment_period if specific_effects_by_investment_period is not None else {} + self.effects_of_investment_per_size: dict[str, xr.DataArray] = ( + effects_of_investment_per_size if effects_of_investment_per_size is not None else {} ) + super().__init__(**kwargs) def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None: """Transform user data into internal model coordinates.""" + super().transform_data(flow_system, name_prefix) # Transform boolean/data flags to DataArrays self.allow_investment = flow_system.fit_to_model_coords( f'{name_prefix}|allow_investment', self.allow_investment, dims=['period', 'scenario'] @@ -1148,13 +1066,19 @@ def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None self.force_investment = flow_system.fit_to_model_coords( f'{name_prefix}|force_investment', self.force_investment, dims=['period', 'scenario'] ) + self.effects_of_investment = flow_system.fit_to_model_coords( + f'{name_prefix}|effects_of_investment', self.effects_of_investment, dims=['period', 'scenario'] + ) # TODO: investment period dim - # Investment-period-dependent effects are already xr.DataArray - # They should have dimensions [period, scenario, investment_period] - # TODO: Add validation for effect dimensions + self.effects_of_investment_per_size = flow_system.fit_to_model_coords( + f'{name_prefix}|effects_of_investment_per_size', + self.effects_of_investment_per_size, + dims=['period', 'scenario'], + ) # TODO: investment period dim def _plausibility_checks(self, flow_system: FlowSystem) -> None: """Validate parameter consistency.""" + # super()._plausibility_checks(flow_system) if flow_system.periods is None: raise ValueError("InvestmentParameters requires the flow_system to have a 'periods' dimension.") @@ -1163,15 +1087,14 @@ def _plausibility_checks(self, flow_system: FlowSystem) -> None: raise ValueError('force_investment can only be True for a single investment_period per scenario.') # Check lifetime feasibility - if self.fixed_lifetime is not None: - periods = flow_system.periods.values - if len(periods) > 1: - # Warn if investment in late periods would extend beyond model horizon - max_horizon = periods[-1] - periods[0] - if self.fixed_lifetime > max_horizon: - logger.warning( - f'Fixed lifetime ({self.fixed_lifetime}) if Investment exceeds model horizon ({max_horizon}). ' - ) + _periods = flow_system.periods.values + if len(_periods) > 1: + # Warn if investment in late periods would extend beyond model horizon + max_horizon = _periods[-1] - _periods[0] + if self.fixed_lifetime > max_horizon: + logger.warning( + f'Fixed lifetime ({self.fixed_lifetime}) if Investment exceeds model horizon ({max_horizon}). ' + ) YearOfInvestmentData = PeriodicDataUser From c1da66f58eb09dbc9fc5f44886d1e4553a877381 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 1 Nov 2025 13:03:42 +0100 Subject: [PATCH 406/448] Update --- flixopt/features.py | 268 +++++++++++++++++++++++++++++++++++++------ flixopt/interface.py | 5 + 2 files changed, 239 insertions(+), 34 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index 6f1540dfc..8f54bc754 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -6,7 +6,8 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING +import warnings +from typing import TYPE_CHECKING, Protocol import linopy import numpy as np @@ -15,7 +16,7 @@ from .structure import FlowSystemModel, Submodel if TYPE_CHECKING: - from .core import FlowSystemDimensions, Scalar, TemporalData + from .core import FlowSystemDimensions, PeriodicData, Scalar, TemporalData from .interface import ( InvestmentParameters, InvestTimingParameters, @@ -27,7 +28,51 @@ logger = logging.getLogger('flixopt') -class SizingModel(Submodel): +class _SizeModel(Submodel): + """A model that creates the size variable together with a Binary""" + + @staticmethod + def _create_sizing_variables_and_constraints( + submodel: Submodel, + size_min: PeriodicData, + size_max: PeriodicData, + mandatory: bool, + dims: list[FlowSystemDimensions], + ): + """Create timing variables and constraints.""" + + size = submodel.add_variables( + short_name='size', + lower=size_min if mandatory else 0, + upper=size_max, + coords=submodel._model.get_coords(dims), + ) + + if not mandatory: + submodel.add_variables( + binary=True, + coords=submodel._model.get_coords(dims), + short_name='available', + ) + BoundingPatterns.bounds_with_state( + submodel, + variable=size, + variable_state=submodel._variables['available'], + bounds=(size_min, size_max), + ) + + @property + def size(self) -> linopy.Variable: + """Capacity size variable""" + return self._variables['size'] + + @property + def available(self) -> linopy.Variable: + """Capacity size variable""" + return self._variables['available'] + + +class SizingModel(_SizeModel): """ This feature model is used to model capacity sizing decisions. It applies bounds to the size variable and optionally creates a binary investment decision. @@ -57,48 +102,27 @@ def _do_modeling(self): self._add_effects() def _create_variables_and_constraints(self): - size_min, size_max = (self.parameters.minimum_or_fixed_size, self.parameters.maximum_or_fixed_size) - - self.add_variables( - short_name='size', - lower=size_min if self.parameters.mandatory else 0, - upper=size_max, - coords=self._model.get_coords(['period', 'scenario']), + self._create_sizing_variables_and_constraints( + submodel=self, + size_min=self.parameters.minimum_or_fixed_size, + size_max=self.parameters.maximum_or_fixed_size, + mandatory=self.parameters.mandatory, + dims=['period', 'scenario'], ) - if not self.parameters.mandatory: - self.add_variables( - binary=True, - coords=self._model.get_coords(['period', 'scenario']), - short_name='invested', - ) - BoundingPatterns.bounds_with_state( - self, - variable=self.size, - variable_state=self._variables['invested'], - bounds=(self.parameters.minimum_or_fixed_size, self.parameters.maximum_or_fixed_size), - ) - def _add_effects(self): """Add size-dependent effects""" - if self.parameters.specific_effects: + if self.parameters.effects_per_size: self._model.effects.add_share_to_effects( name=self.label_of_element, - expressions={effect: self.size * factor for effect, factor in self.parameters.specific_effects.items()}, + expressions={effect: self.size * factor for effect, factor in self.parameters.effects_per_size.items()}, target='periodic', ) - @property - def size(self) -> linopy.Variable: - """Capacity size variable""" - return self._variables['size'] - @property def invested(self) -> linopy.Variable | None: - """Binary investment decision variable (None if mandatory=True)""" - if 'invested' not in self._variables: - return None - return self._variables['invested'] + warnings.warn('Deprecated, use availlable instead', DeprecationWarning, stacklevel=2) + return self.available class InvestmentModel(Submodel): @@ -210,6 +234,182 @@ def invested(self) -> linopy.Variable | None: return self._variables['invested'] +class InvestmentTimingFeature(_SizeModel): + """ + Model investment timing with fixed lifetime. + + This feature works in conjunction with SizingModel to provide full investment modeling: + - SizingModel: Determines HOW MUCH capacity to install + - InvestmentTimingFeature: Determines WHEN to invest + + The model creates binary variables to track: + - When the investment occurs (one period) + - Which periods the investment is active (based on fixed lifetime) + + The investment capacity (from SizingModel) is only active during the investment's lifetime. + + Args: + model: The optimization model instance + label_of_element: The label of the parent element + parameters: InvestmentParameters defining timing constraints + sizing_model: Reference to the SizingModel for this element (required) + label_of_model: Optional custom label for the model + """ + + parameters: InvestmentParameters + + def __init__( + self, + model: FlowSystemModel, + label_of_element: str, + parameters: InvestmentParameters, + label_of_model: str | None = None, + ): + self.parameters = parameters + super().__init__(model, label_of_element=label_of_element, label_of_model=label_of_model) + + def _do_modeling(self): + super()._do_modeling() + self._create_variables_and_constraints() + self._add_effects() + + def _create_variables_and_constraints(self): + """Create timing variables and constraints.""" + # Regular sizing =============================================================================================== + self._create_sizing_variables_and_constraints( + submodel=self, + size_min=self.parameters.minimum_or_fixed_size, + size_max=self.parameters.maximum_or_fixed_size, + mandatory=self.parameters.mandatory, + dims=['period', 'scenario'], + ) + + self._track_investment_and_decomissioning_period() + self._track_investment_and_decomissioning_size() + self._apply_investment_period_constraints() + + self._add_effects() + + def _track_investment_and_decomissioning_period(self): + """Track investment and decomissioning period absed on binary state variable.""" + self.add_variables( + binary=True, + coords=self._model.get_coords(['year', 'scenario']), + short_name='size|investment_occurs', + ) + self.add_constraints( + self.investment_occurs.sum('year') <= 1, + short_name='invest_once', + ) + + self.add_variables( + binary=True, + coords=self._model.get_coords(['year', 'scenario']), + short_name='size|decommissioning_occurs', + ) + self.add_constraints( + self.decommissioning_occurs.sum('year') <= 1, + short_name='decommission_once', + ) + + BoundingPatterns.state_transition_bounds( + self, + state_variable=self.is_invested, + switch_on=self.investment_occurs, + switch_off=self.decommissioning_occurs, + name=self.is_invested.name, + previous_state=0, + coord='year', + ) + + def _track_investment_and_decomissioning_size(self): + self.add_variables( + coords=self._model.get_coords(['year', 'scenario']), + short_name='size|increase', + lower=0, + upper=self.parameters.maximum_or_fixed_size, + ) + self.add_variables( + coords=self._model.get_coords(['year', 'scenario']), + short_name='size|decrease', + lower=0, + upper=self.parameters.maximum_or_fixed_size, + ) + BoundingPatterns.link_changes_to_level_with_binaries( + self, + level_variable=self.size, + increase_variable=self.size_increase, + decrease_variable=self.size_decrease, + increase_binary=self.investment_occurs, + decrease_binary=self.decommissioning_occurs, + name=f'{self.label_of_element}|size|changes', + max_change=self.parameters.maximum_or_fixed_size, + initial_level=0, + coord='period', + ) + + def _apply_investment_period_constraints(self): + # Constraint: Apply allow_investment restrictions + if (self.parameters.allow_investment == 0).any(): + if (self.parameters.allow_investment == 0).all('period'): + logger.error(f'In "{self.label_full}": Need to allow Investment in at least one period.') + self.add_constraints( + self.investment_occurs <= self.parameters.allow_investment, + short_name='allow_investment', + ) + + # If a specific period is forced, investment must occur there + if (self.parameters.force_investment == 1).any(): + if (self.parameters.force_investment.sum('period') > 1).any(): + raise ValueError('Can not force Investment in more than one period') + self.add_constraints( + self.investment_occurs == self.parameters.force_investment, + short_name='force_investment', + ) + + def _add_effects(self): + """Add investment-period-dependent effects.""" + if self.parameters.effects_per_size: + self._model.effects.add_share_to_effects( + name=self.label_of_element, + expressions={effect: self.size * factor for effect, factor in self.parameters.effects_per_size.items()}, + target='periodic', + ) + # For now, we'll skip the complex investment-period-dependent effects + # These require extending the effect system with the investment_period dimension + # We can add this in a follow-up iteration + if self.parameters.effects_of_investment or self.parameters.effects_of_investment_per_size: + logger.warning( + f'{self.label_full}: Investment-period-dependent effects are not yet implemented. ' + f'These effects will be ignored in this version.' + ) + + @property + def investment_occurs(self) -> linopy.Variable: + """Binary variable indicating when investment occurs (at most one period)""" + return self._variables['size|investment_occurs'] + + @property + def decommissioning_occurs(self) -> linopy.Variable: + """Binary decrease decision variable""" + return self._variables['size|decommissioning_occurs'] + + @property + def is_invested(self) -> linopy.Variable: + """Binary variable indicating which periods have active investment""" + return self._variables['is_invested'] + + @property + def size_decrease(self) -> linopy.Variable: + """Binary decrease decision variable""" + return self._variables['size|decrease'] + + @property + def size_increase(self) -> linopy.Variable: + """Binary increase decision variable""" + return self._variables['size|increase'] + + class InvestmentTimingModel(Submodel): """ This feature model is used to model the timing of investments. diff --git a/flixopt/interface.py b/flixopt/interface.py index db1adeb90..0f84875e1 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -1039,6 +1039,7 @@ def __init__( force_investment: InvestmentPeriodDataBool = False, effects_of_investment: dict[str, xr.DataArray] | None = None, effects_of_investment_per_size: dict[str, xr.DataArray] | None = None, + previous_size: PeriodicDataUser = 0, **kwargs, ): if fixed_lifetime is None: @@ -1047,6 +1048,7 @@ def __init__( self.fixed_lifetime = fixed_lifetime self.allow_investment = allow_investment self.force_investment = force_investment + self.previous_size = previous_size self.effects_of_investment: dict[str, xr.DataArray] = ( effects_of_investment if effects_of_investment is not None else {} @@ -1066,6 +1068,9 @@ def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None self.force_investment = flow_system.fit_to_model_coords( f'{name_prefix}|force_investment', self.force_investment, dims=['period', 'scenario'] ) + self.previous_size = flow_system.fit_to_model_coords( + f'{name_prefix}|previous_size', self.previous_size, dims=['period', 'scenario'] + ) self.effects_of_investment = flow_system.fit_to_model_coords( f'{name_prefix}|effects_of_investment', self.effects_of_investment, dims=['period', 'scenario'] ) # TODO: investment period dim From 3584d3c6d02b1a2bbb80b917fa82a046c1a413a8 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 1 Nov 2025 13:05:48 +0100 Subject: [PATCH 407/448] Update --- flixopt/features.py | 45 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index 8f54bc754..d5663cf6b 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -252,7 +252,6 @@ class InvestmentTimingFeature(_SizeModel): model: The optimization model instance label_of_element: The label of the parent element parameters: InvestmentParameters defining timing constraints - sizing_model: Reference to the SizingModel for this element (required) label_of_model: Optional custom label for the model """ @@ -368,20 +367,48 @@ def _apply_investment_period_constraints(self): ) def _add_effects(self): - """Add investment-period-dependent effects.""" + """Add investment effects to the model.""" if self.parameters.effects_per_size: self._model.effects.add_share_to_effects( name=self.label_of_element, expressions={effect: self.size * factor for effect, factor in self.parameters.effects_per_size.items()}, target='periodic', ) - # For now, we'll skip the complex investment-period-dependent effects - # These require extending the effect system with the investment_period dimension - # We can add this in a follow-up iteration - if self.parameters.effects_of_investment or self.parameters.effects_of_investment_per_size: - logger.warning( - f'{self.label_full}: Investment-period-dependent effects are not yet implemented. ' - f'These effects will be ignored in this version.' + + if self.parameters.effects_of_size: + self._model.effects.add_share_to_effects( + name=self.label_of_element, + expressions={ + effect: self.is_invested * factor if self.is_invested is not None else factor + for effect, factor in self.parameters.effects_of_size.items() + }, + target='periodic', + ) + + if self.parameters.fixed_effects_by_investment_year: + # Effects depending on when the investment is made + remapped_variable = self.investment_occurs.rename({'year': 'year_of_investment'}) + + self._model.effects.add_share_to_effects( + name=self.label_of_element, + expressions={ + effect: (remapped_variable * factor).sum('year_of_investment') + for effect, factor in self.parameters.fixed_effects_by_investment_year.items() + }, + target='invest', + ) + + if self.parameters.specific_effects_by_investment_year: + # Annual effects proportional to investment size + remapped_variable = self.size_increase.rename({'year': 'year_of_investment'}) + + self._model.effects.add_share_to_effects( + name=self.label_of_element, + expressions={ + effect: (remapped_variable * factor).sum('year_of_investment') + for effect, factor in self.parameters.specific_effects_by_investment_year.items() + }, + target='invest', ) @property From 553d7840f9b9df17c1633660327ef1059e669bce Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 1 Nov 2025 13:13:10 +0100 Subject: [PATCH 408/448] Update --- flixopt/features.py | 384 ++------------------------------------------ 1 file changed, 13 insertions(+), 371 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index d5663cf6b..60533552e 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -125,115 +125,6 @@ def invested(self) -> linopy.Variable | None: return self.available -class InvestmentModel(Submodel): - """ - This feature model is used to model the investment of a variable. - It applies the corresponding bounds to the variable and the on/off state of the variable. - - Args: - model: The optimization model instance - label_of_element: The label of the parent (Element). Used to construct the full label of the model. - parameters: The parameters of the feature model. - label_of_model: The label of the model. This is needed to construct the full label of the model. - - """ - - parameters: SizingParameters - - def __init__( - self, - model: FlowSystemModel, - label_of_element: str, - parameters: SizingParameters, - label_of_model: str | None = None, - ): - self.piecewise_effects: PiecewiseEffectsModel | None = None - self.parameters = parameters - super().__init__(model, label_of_element=label_of_element, label_of_model=label_of_model) - - def _do_modeling(self): - super()._do_modeling() - self._create_variables_and_constraints() - self._add_effects() - - def _create_variables_and_constraints(self): - size_min, size_max = (self.parameters.minimum_or_fixed_size, self.parameters.maximum_or_fixed_size) - - self.add_variables( - short_name='size', - lower=size_min if self.parameters.mandatory else 0, - upper=size_max, - coords=self._model.get_coords(['period', 'scenario']), - ) - - if not self.parameters.mandatory: - self.add_variables( - binary=True, - coords=self._model.get_coords(['period', 'scenario']), - short_name='invested', - ) - BoundingPatterns.bounds_with_state( - self, - variable=self.size, - variable_state=self._variables['invested'], - bounds=(self.parameters.minimum_or_fixed_size, self.parameters.maximum_or_fixed_size), - ) - - def _add_effects(self): - """Add investment effects""" - if self.parameters.effects_of_size: - self._model.effects.add_share_to_effects( - name=self.label_of_element, - expressions={ - effect: self.invested * factor if self.invested is not None else factor - for effect, factor in self.parameters.effects_of_size.items() - }, - target='periodic', - ) - - if self.parameters.effects_of_retirement and not self.parameters.mandatory: - self._model.effects.add_share_to_effects( - name=self.label_of_element, - expressions={ - effect: -self.invested * factor + factor - for effect, factor in self.parameters.effects_of_retirement.items() - }, - target='periodic', - ) - - if self.parameters.effects_per_size: - self._model.effects.add_share_to_effects( - name=self.label_of_element, - expressions={effect: self.size * factor for effect, factor in self.parameters.effects_per_size.items()}, - target='periodic', - ) - - if self.parameters.piecewise_effects_per_size: - self.piecewise_effects = self.add_submodels( - PiecewiseEffectsModel( - model=self._model, - label_of_element=self.label_of_element, - label_of_model=f'{self.label_of_element}|PiecewiseEffects', - piecewise_origin=(self.size.name, self.parameters.piecewise_effects_per_size.piecewise_origin), - piecewise_shares=self.parameters.piecewise_effects_per_size.piecewise_shares, - zero_point=self.invested, - ), - short_name='segments', - ) - - @property - def size(self) -> linopy.Variable: - """Investment size variable""" - return self._variables['size'] - - @property - def invested(self) -> linopy.Variable | None: - """Binary investment decision variable""" - if 'invested' not in self._variables: - return None - return self._variables['invested'] - - class InvestmentTimingFeature(_SizeModel): """ Model investment timing with fixed lifetime. @@ -385,30 +276,32 @@ def _add_effects(self): target='periodic', ) - if self.parameters.fixed_effects_by_investment_year: + # New kind of effects ========================================================================================== + + if self.parameters.effects_of_investment: # Effects depending on when the investment is made - remapped_variable = self.investment_occurs.rename({'year': 'year_of_investment'}) + remapped_variable = self.investment_occurs.rename({'period': 'period_of_investment'}) self._model.effects.add_share_to_effects( name=self.label_of_element, expressions={ - effect: (remapped_variable * factor).sum('year_of_investment') - for effect, factor in self.parameters.fixed_effects_by_investment_year.items() + effect: (remapped_variable * factor).sum('period_of_investment') + for effect, factor in self.parameters.effects_of_investment.items() }, - target='invest', + target='periodic', ) - if self.parameters.specific_effects_by_investment_year: - # Annual effects proportional to investment size - remapped_variable = self.size_increase.rename({'year': 'year_of_investment'}) + if self.parameters.effects_of_investment_per_size: + # Effects depending on when the investment is made proportional to investment size + remapped_variable = self.size_increase.rename({'period': 'period_of_investment'}) self._model.effects.add_share_to_effects( name=self.label_of_element, expressions={ - effect: (remapped_variable * factor).sum('year_of_investment') - for effect, factor in self.parameters.specific_effects_by_investment_year.items() + effect: (remapped_variable * factor).sum('period_of_investment') + for effect, factor in self.parameters.effects_of_investment_per_size.items() }, - target='invest', + target='periodic', ) @property @@ -437,257 +330,6 @@ def size_increase(self) -> linopy.Variable: return self._variables['size|increase'] -class InvestmentTimingModel(Submodel): - """ - This feature model is used to model the timing of investments. - - Such an Investment is defined by a size, a year_of_investment, and a year_of_decommissioning. - In between these years, the size of the investment cannot vary. Outside, its 0. - """ - - parameters: InvestTimingParameters - - def __init__( - self, - model: FlowSystemModel, - label_of_element: str, - parameters: InvestTimingParameters, - label_of_model: str | None = None, - ): - self.parameters = parameters - super().__init__(model, label_of_element, label_of_model) - - def _do_modeling(self): - super()._do_modeling() - self._basic_modeling() - self._add_effects() - - self._constraint_investment() - self._constraint_decommissioning() - - def _basic_modeling(self): - size_min, size_max = self.parameters.minimum_or_fixed_size, self.parameters.maximum_or_fixed_size - - ######################################################################## - self.add_variables( - short_name='size', - lower=0, - upper=size_max, - coords=self._model.get_coords(['year', 'scenario']), - ) - self.add_variables( - binary=True, - coords=self._model.get_coords(['year', 'scenario']), - short_name='is_invested', - ) - BoundingPatterns.bounds_with_state( - self, - variable=self.size, - variable_state=self.is_invested, - bounds=(size_min, size_max), - ) - - ######################################################################## - self.add_variables( - binary=True, - coords=self._model.get_coords(['year', 'scenario']), - short_name='size|investment_occurs', - ) - self.add_variables( - binary=True, - coords=self._model.get_coords(['year', 'scenario']), - short_name='size|decommissioning_occurs', - ) - BoundingPatterns.state_transition_bounds( - self, - state_variable=self.is_invested, - switch_on=self.investment_occurs, - switch_off=self.decommissioning_occurs, - name=self.is_invested.name, - previous_state=0, - coord='year', - ) - self.add_constraints( - self.investment_occurs.sum('year') <= 1, - short_name='investment_occurs|once', - ) - self.add_constraints( - self.decommissioning_occurs.sum('year') <= 1, - short_name='decommissioning_occurs|once', - ) - - ######################################################################## - previous_lifetime = self.parameters.previous_lifetime if self.parameters.previous_lifetime is not None else 0 - self.add_variables( - lower=0, - upper=self.parameters.maximum_or_fixed_lifetime - if self.parameters.maximum_or_fixed_lifetime is not None - else self._model.flow_system.years_per_year.sum('year') + previous_lifetime, - coords=self._model.get_coords(['scenario']), - short_name='size|lifetime', - ) - self.add_constraints( - self.lifetime - == (self.is_invested * self._model.flow_system.years_per_year).sum('year') - + self.is_invested.isel(year=0) * previous_lifetime, - short_name='size|lifetime', - ) - if self.parameters.minimum_or_fixed_lifetime is not None: - self.add_constraints( - self.lifetime + self.is_invested.isel(year=-1) * self.parameters.minimum_or_fixed_lifetime - >= self.investment_occurs * self.parameters.minimum_or_fixed_lifetime, - short_name='size|lifetime|lb', - ) - - ######################################################################## - self.add_variables( - coords=self._model.get_coords(['year', 'scenario']), - short_name='size|increase', - lower=0, - upper=size_max, - ) - self.add_variables( - coords=self._model.get_coords(['year', 'scenario']), - short_name='size|decrease', - lower=0, - upper=size_max, - ) - BoundingPatterns.link_changes_to_level_with_binaries( - self, - level_variable=self.size, - increase_variable=self.size_increase, - decrease_variable=self.size_decrease, - increase_binary=self.investment_occurs, - decrease_binary=self.decommissioning_occurs, - name=f'{self.label_of_element}|size|changes', - max_change=size_max, - initial_level=0, - coord='year', - ) - - def _add_effects(self): - """Add investment effects to the model.""" - - if self.parameters.fix_effects: - self._model.effects.add_share_to_effects( - name=self.label_of_element, - expressions={ - effect: self.is_invested * factor if self.is_invested is not None else factor - for effect, factor in self.parameters.fix_effects.items() - }, - target='invest', - ) - - if self.parameters.specific_effects: - self._model.effects.add_share_to_effects( - name=self.label_of_element, - expressions={effect: self.size * factor for effect, factor in self.parameters.specific_effects.items()}, - target='invest', - ) - - if self.parameters.fixed_effects_by_investment_year: - # Effects depending on when the investment is made - remapped_variable = self.investment_occurs.rename({'year': 'year_of_investment'}) - - self._model.effects.add_share_to_effects( - name=self.label_of_element, - expressions={ - effect: (remapped_variable * factor).sum('year_of_investment') - for effect, factor in self.parameters.fixed_effects_by_investment_year.items() - }, - target='invest', - ) - - if self.parameters.specific_effects_by_investment_year: - # Annual effects proportional to investment size - remapped_variable = self.size_increase.rename({'year': 'year_of_investment'}) - - self._model.effects.add_share_to_effects( - name=self.label_of_element, - expressions={ - effect: (remapped_variable * factor).sum('year_of_investment') - for effect, factor in self.parameters.specific_effects_by_investment_year.items() - }, - target='invest', - ) - - def _constraint_investment(self): - if self.parameters.force_investment.sum() > 0: - self.add_constraints( - self.investment_occurs == self.parameters.force_investment, - short_name='size|changes|fixed_start', - ) - else: - self.add_constraints( - self.investment_occurs <= self.parameters.allow_investment, - short_name='size|changes|restricted_start', - ) - - def _constraint_decommissioning(self): - if self.parameters.force_decommissioning.sum() > 0: - self.add_constraints( - self.decommissioning_occurs == self.parameters.force_decommissioning, - short_name='size|changes|fixed_end', - ) - else: - self.add_constraints( - self.decommissioning_occurs <= self.parameters.allow_decommissioning, - short_name='size|changes|restricted_end', - ) - - @property - def size(self) -> linopy.Variable: - """Investment size variable""" - return self._variables['size'] - - @property - def is_invested(self) -> linopy.Variable | None: - """Binary investment decision variable""" - if 'is_invested' not in self._variables: - return None - return self._variables['is_invested'] - - @property - def investment_occurs(self) -> linopy.Variable: - """Binary increase decision variable""" - return self._variables['size|investment_occurs'] - - @property - def decommissioning_occurs(self) -> linopy.Variable: - """Binary decrease decision variable""" - return self._variables['size|decommissioning_occurs'] - - @property - def size_decrease(self) -> linopy.Variable: - """Binary decrease decision variable""" - return self._variables['size|decrease'] - - @property - def size_increase(self) -> linopy.Variable: - """Binary increase decision variable""" - return self._variables['size|increase'] - - @property - def investment_used(self) -> linopy.LinearExpression: - """Binary investment decision variable""" - return self.investment_occurs.sum('year') - - @property - def divestment_used(self) -> linopy.LinearExpression: - """Binary investment decision variable""" - return self.decommissioning_occurs.sum('year') - - @property - def lifetime(self) -> linopy.Variable: - """Lifetime variable""" - return self._variables['size|lifetime'] - - @property - def duration(self) -> linopy.Variable: - """Investment duration variable""" - return self._variables['duration'] - - class OnOffModel(Submodel): """OnOff model using factory patterns""" From b9cf8557410ca4cd94f28acdb63f63b1a863a9e1 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 1 Nov 2025 13:19:41 +0100 Subject: [PATCH 409/448] Improve _SizeModel --- flixopt/features.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index 60533552e..480b1e96b 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -31,9 +31,8 @@ class _SizeModel(Submodel): """A model that creates the size variable together with a Binary""" - @staticmethod def _create_sizing_variables_and_constraints( - submodel: Submodel, + self, size_min: PeriodicData, size_max: PeriodicData, mandatory: bool, @@ -41,23 +40,23 @@ def _create_sizing_variables_and_constraints( ): """Create timing variables and constraints.""" - size = submodel.add_variables( + size = self.add_variables( short_name='size', lower=size_min if mandatory else 0, upper=size_max, - coords=submodel._model.get_coords(dims), + coords=self._model.get_coords(dims), ) if not mandatory: - submodel.add_variables( + self.add_variables( binary=True, - coords=submodel._model.get_coords(dims), + coords=self._model.get_coords(dims), short_name='available', ) BoundingPatterns.bounds_with_state( - submodel, + self, variable=size, - variable_state=submodel._variables['available'], + variable_state=self._variables['available'], bounds=(size_min, size_max), ) @@ -67,9 +66,9 @@ def size(self) -> linopy.Variable: return self._variables['size'] @property - def available(self) -> linopy.Variable: + def available(self) -> linopy.Variable | None: """Capacity size variable""" - return self._variables['available'] + return self._variables.get('available') class SizingModel(_SizeModel): From 4b6eaab86c37a528d9fff5b609c60d239cebd04c Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 1 Nov 2025 13:24:48 +0100 Subject: [PATCH 410/448] Improve _SizeModel --- flixopt/features.py | 55 ++++++++++++++++++++------------------------- 1 file changed, 24 insertions(+), 31 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index 480b1e96b..b3aec7c08 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -17,9 +17,9 @@ if TYPE_CHECKING: from .core import FlowSystemDimensions, PeriodicData, Scalar, TemporalData + from .effects import PeriodicEffects from .interface import ( InvestmentParameters, - InvestTimingParameters, OnOffParameters, Piecewise, SizingParameters, @@ -60,6 +60,24 @@ def _create_sizing_variables_and_constraints( bounds=(size_min, size_max), ) + def _add_sizing_effects(self, effects_per_size: PeriodicEffects, effects_of_size: PeriodicEffects): + if effects_per_size: + self._model.effects.add_share_to_effects( + name=self.label_of_element, + expressions={effect: self.size * factor for effect, factor in effects_per_size.items()}, + target='periodic', + ) + + if effects_of_size: + self._model.effects.add_share_to_effects( + name=self.label_of_element, + expressions={ + effect: self.available * factor if self.available is not None else factor + for effect, factor in effects_of_size.items() + }, + target='periodic', + ) + @property def size(self) -> linopy.Variable: """Capacity size variable""" @@ -97,26 +115,16 @@ def __init__( def _do_modeling(self): super()._do_modeling() - self._create_variables_and_constraints() - self._add_effects() - - def _create_variables_and_constraints(self): self._create_sizing_variables_and_constraints( - submodel=self, size_min=self.parameters.minimum_or_fixed_size, size_max=self.parameters.maximum_or_fixed_size, mandatory=self.parameters.mandatory, dims=['period', 'scenario'], ) - - def _add_effects(self): - """Add size-dependent effects""" - if self.parameters.effects_per_size: - self._model.effects.add_share_to_effects( - name=self.label_of_element, - expressions={effect: self.size * factor for effect, factor in self.parameters.effects_per_size.items()}, - target='periodic', - ) + self._add_sizing_effects( + effects_per_size=self.parameters.effects_per_size, + effects_of_size=self.parameters.effects_of_size, + ) @property def invested(self) -> linopy.Variable | None: @@ -258,22 +266,7 @@ def _apply_investment_period_constraints(self): def _add_effects(self): """Add investment effects to the model.""" - if self.parameters.effects_per_size: - self._model.effects.add_share_to_effects( - name=self.label_of_element, - expressions={effect: self.size * factor for effect, factor in self.parameters.effects_per_size.items()}, - target='periodic', - ) - - if self.parameters.effects_of_size: - self._model.effects.add_share_to_effects( - name=self.label_of_element, - expressions={ - effect: self.is_invested * factor if self.is_invested is not None else factor - for effect, factor in self.parameters.effects_of_size.items() - }, - target='periodic', - ) + self._add_sizing_effects() # New kind of effects ========================================================================================== From b131781645e0e5e2295ea45e1a35b12f2950d454 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 1 Nov 2025 13:25:36 +0100 Subject: [PATCH 411/448] Rename to InvestmentModel --- flixopt/features.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index b3aec7c08..544577626 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -132,13 +132,13 @@ def invested(self) -> linopy.Variable | None: return self.available -class InvestmentTimingFeature(_SizeModel): +class InvestmentModel(_SizeModel): """ Model investment timing with fixed lifetime. This feature works in conjunction with SizingModel to provide full investment modeling: - SizingModel: Determines HOW MUCH capacity to install - - InvestmentTimingFeature: Determines WHEN to invest + - InvestmentModel: Determines WHEN to invest The model creates binary variables to track: - When the investment occurs (one period) From 34bad2a64fab8f0560f7bf9f1279fb799db5f766 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 1 Nov 2025 13:26:53 +0100 Subject: [PATCH 412/448] Reuse code --- flixopt/features.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index 544577626..d7a29054a 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -174,7 +174,6 @@ def _create_variables_and_constraints(self): """Create timing variables and constraints.""" # Regular sizing =============================================================================================== self._create_sizing_variables_and_constraints( - submodel=self, size_min=self.parameters.minimum_or_fixed_size, size_max=self.parameters.maximum_or_fixed_size, mandatory=self.parameters.mandatory, @@ -266,7 +265,10 @@ def _apply_investment_period_constraints(self): def _add_effects(self): """Add investment effects to the model.""" - self._add_sizing_effects() + self._add_sizing_effects( + self.parameters.effects_per_size, + self.parameters.effects_of_size, + ) # New kind of effects ========================================================================================== From 3c305d76fa4b58c06f7d5120f7c74e3f75ed46ce Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 1 Nov 2025 13:28:57 +0100 Subject: [PATCH 413/448] Rename initial_level to previous_level and use it --- flixopt/features.py | 2 +- flixopt/modeling.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index d7a29054a..2e6c92e24 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -240,7 +240,7 @@ def _track_investment_and_decomissioning_size(self): decrease_binary=self.decommissioning_occurs, name=f'{self.label_of_element}|size|changes', max_change=self.parameters.maximum_or_fixed_size, - initial_level=0, + previous_level=self.parameters.previous_size, coord='period', ) diff --git a/flixopt/modeling.py b/flixopt/modeling.py index c7f0bf314..ad4926f1c 100644 --- a/flixopt/modeling.py +++ b/flixopt/modeling.py @@ -689,14 +689,14 @@ def link_changes_to_level_with_binaries( decrease_binary: linopy.Variable, name: str, max_change: float | xr.DataArray, - initial_level: float | xr.DataArray = 0.0, + previous_level: float | xr.DataArray = 0.0, coord: str = 'period', ) -> tuple[linopy.Constraint, linopy.Constraint, linopy.Constraint, linopy.Constraint, linopy.Constraint]: """ Link changes to level evolution with binary control and mutual exclusivity. Creates the complete constraint system for ALL time periods: - 1. level[0] = initial_level + increase[0] - decrease[0] + 1. level[0] = previous_level + increase[0] - decrease[0] 2. level[t] = level[t-1] + increase[t] - decrease[t] ∀t > 0 3. increase[t] <= max_change * increase_binary[t] ∀t 4. decrease[t] <= max_change * decrease_binary[t] ∀t @@ -711,7 +711,7 @@ def link_changes_to_level_with_binaries( level_variable: Level variable for ALL periods name: Base name for constraints max_change: Maximum change per period - initial_level: Starting level before first period + previous_level: Level before first period coord: Time coordinate name Returns: @@ -720,9 +720,9 @@ def link_changes_to_level_with_binaries( if not isinstance(model, Submodel): raise ValueError('BoundingPatterns.link_changes_to_level_with_binaries() can only be used with a Submodel') - # 1. Initial period: level[0] - initial_level = increase[0] - decrease[0] + # 1. Initial period: level[0] - previous_level = increase[0] - decrease[0] initial_constraint = model.add_constraints( - level_variable.isel({coord: 0}) - initial_level + level_variable.isel({coord: 0}) - previous_level == increase_variable.isel({coord: 0}) - decrease_variable.isel({coord: 0}), name=f'{name}|initial_level', ) From 8e48c0bd2e6ee39132a4aef5c0535ba977a37748 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 1 Nov 2025 13:35:01 +0100 Subject: [PATCH 414/448] Remove old InvestmentTiming class --- flixopt/__init__.py | 1 - flixopt/calculation.py | 8 +++----- flixopt/commons.py | 3 +-- flixopt/elements.py | 16 ++++++++-------- flixopt/interface.py | 10 +++++++--- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/flixopt/__init__.py b/flixopt/__init__.py index 67ff973d7..0145ace53 100644 --- a/flixopt/__init__.py +++ b/flixopt/__init__.py @@ -21,7 +21,6 @@ FlowSystem, FullCalculation, InvestmentParameters, - InvestTimingParameters, LinearConverter, OnOffParameters, Piece, diff --git a/flixopt/calculation.py b/flixopt/calculation.py index c84323af8..250be4072 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -26,7 +26,7 @@ from .components import Storage from .config import CONFIG from .core import DataConverter, Scalar, TimeSeriesData, drop_constant_arrays -from .features import InvestmentModel, InvestmentTimingModel +from .features import InvestmentModel, SizingModel, _SizeModel from .flow_system import FlowSystem from .results import CalculationResults, SegmentedCalculationResults @@ -121,15 +121,13 @@ def main_results(self) -> dict[str, Scalar | dict]: model.label_of_element: model.size.solution for component in self.flow_system.components.values() for model in component.submodel.all_submodels - if isinstance(model, (InvestmentModel, InvestmentTimingModel)) - and model.size.solution.max() >= CONFIG.Modeling.epsilon + if isinstance(model, _SizeModel) and model.size.solution.max() >= CONFIG.Modeling.epsilon }, 'Not invested': { model.label_of_element: model.size.solution for component in self.flow_system.components.values() for model in component.submodel.all_submodels - if isinstance(model, (InvestmentModel, InvestmentTimingModel)) - and model.size.solution.max() < CONFIG.Modeling.epsilon + if isinstance(model, _SizeModel) and model.size.solution.max() < CONFIG.Modeling.epsilon }, }, 'Buses with excess': [ diff --git a/flixopt/commons.py b/flixopt/commons.py index 2b8003785..dfd2c268e 100644 --- a/flixopt/commons.py +++ b/flixopt/commons.py @@ -20,7 +20,6 @@ from .flow_system import FlowSystem from .interface import ( InvestmentParameters, - InvestTimingParameters, OnOffParameters, Piece, Piecewise, @@ -59,5 +58,5 @@ 'results', 'linear_converters', 'solvers', - 'InvestTimingParameters', + 'SizingParameters', ] diff --git a/flixopt/elements.py b/flixopt/elements.py index 62e636889..28c6c8f22 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -14,8 +14,8 @@ from . import io as fx_io from .config import CONFIG from .core import PlausibilityError, Scalar, TemporalData, TemporalDataUser -from .features import InvestmentModel, InvestmentTimingModel, OnOffModel -from .interface import InvestTimingParameters, OnOffParameters, SizingParameters +from .features import InvestmentModel, OnOffModel, SizingModel, _SizeModel +from .interface import InvestmentParameters, OnOffParameters, SizingParameters, _SizeParameters from .modeling import BoundingPatterns, ModelingPrimitives, ModelingUtilitiesAbstract from .structure import Element, ElementModel, FlowSystemModel, register_class_for_io @@ -419,7 +419,7 @@ def __init__( self, label: str, bus: str, - size: Scalar | SizingParameters | InvestTimingParameters = None, + size: Scalar | SizingParameters | InvestmentParameters = None, fixed_relative_profile: TemporalDataUser | None = None, relative_minimum: TemporalDataUser = 0, relative_maximum: TemporalDataUser = 1, @@ -493,7 +493,7 @@ def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None if self.on_off_parameters is not None: self.on_off_parameters.transform_data(flow_system, prefix) - if isinstance(self.size, (SizingParameters, InvestTimingParameters)): + if isinstance(self.size, _SizeModel): self.size.transform_data(flow_system, prefix) else: self.size = flow_system.fit_to_model_coords(f'{prefix}|size', self.size, dims=['period', 'scenario']) @@ -503,7 +503,7 @@ def _plausibility_checks(self) -> None: if (self.relative_minimum > self.relative_maximum).any(): raise PlausibilityError(self.label_full + ': Take care, that relative_minimum <= relative_maximum!') - if not isinstance(self.size, (SizingParameters, InvestTimingParameters)) and ( + if not isinstance(self.size, _SizeParameters) and ( np.any(self.size == CONFIG.Modeling.big) and self.fixed_relative_profile is not None ): # Default Size --> Most likely by accident logger.warning( @@ -609,9 +609,9 @@ def _create_investment_model(self): ), 'investment', ) - elif isinstance(self.element.size, InvestTimingParameters): + elif isinstance(self.element.size, InvestmentParameters): self.add_submodels( - InvestmentTimingModel( + InvestmentModel( model=self._model, label_of_element=self.label_of_element, parameters=self.element.size, @@ -670,7 +670,7 @@ def with_on_off(self) -> bool: @property def with_investment(self) -> bool: - return isinstance(self.element.size, (SizingParameters, InvestTimingParameters)) + return isinstance(self.element.size, _SizeParameters) # Properties for clean access to variables @property diff --git a/flixopt/interface.py b/flixopt/interface.py index 0f84875e1..5fbd0a3d5 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -669,8 +669,12 @@ def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None piecewise.transform_data(flow_system, f'{name_prefix}|PiecewiseEffects|{effect}') +class _SizeParameters(Interface): + pass + + @register_class_for_io -class SizingParameters(Interface): +class SizingParameters(_SizeParameters): """Define investment decision parameters with flexible sizing and effect modeling. This class models investment decisions in optimization problems, supporting @@ -1109,7 +1113,7 @@ def _plausibility_checks(self, flow_system: FlowSystem) -> None: @register_class_for_io -class InvestTimingParameters(Interface): +class _InvestTimingParameters(Interface): """ Investment with fixed start and end years. @@ -1195,7 +1199,7 @@ def __init__( def _plausibility_checks(self, flow_system): """Validate parameter consistency.""" if flow_system.years is None: - raise ValueError("InvestTimingParameters requires the flow_system to have a 'years' dimension.") + raise ValueError("InvestmentParameters requires the flow_system to have a 'period' dimension.") if (self.force_investment.sum('year') > 1).any(): raise ValueError('force_investment can only be True for a single year.') From a0c3e3131f7e672d522c5e1174875952cfef1b7e Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 1 Nov 2025 13:36:02 +0100 Subject: [PATCH 415/448] Fix old year dim --- flixopt/features.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index 2e6c92e24..41043a0c7 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -190,21 +190,21 @@ def _track_investment_and_decomissioning_period(self): """Track investment and decomissioning period absed on binary state variable.""" self.add_variables( binary=True, - coords=self._model.get_coords(['year', 'scenario']), + coords=self._model.get_coords(['period', 'scenario']), short_name='size|investment_occurs', ) self.add_constraints( - self.investment_occurs.sum('year') <= 1, + self.investment_occurs.sum('period') <= 1, short_name='invest_once', ) self.add_variables( binary=True, - coords=self._model.get_coords(['year', 'scenario']), + coords=self._model.get_coords(['period', 'scenario']), short_name='size|decommissioning_occurs', ) self.add_constraints( - self.decommissioning_occurs.sum('year') <= 1, + self.decommissioning_occurs.sum('period') <= 1, short_name='decommission_once', ) @@ -215,18 +215,18 @@ def _track_investment_and_decomissioning_period(self): switch_off=self.decommissioning_occurs, name=self.is_invested.name, previous_state=0, - coord='year', + coord='period', ) def _track_investment_and_decomissioning_size(self): self.add_variables( - coords=self._model.get_coords(['year', 'scenario']), + coords=self._model.get_coords(['period', 'scenario']), short_name='size|increase', lower=0, upper=self.parameters.maximum_or_fixed_size, ) self.add_variables( - coords=self._model.get_coords(['year', 'scenario']), + coords=self._model.get_coords(['period', 'scenario']), short_name='size|decrease', lower=0, upper=self.parameters.maximum_or_fixed_size, From 193581e6a436b8fed3d9e88165a06a93d4de8d78 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 1 Nov 2025 13:37:45 +0100 Subject: [PATCH 416/448] Fix in Storage --- flixopt/components.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/flixopt/components.py b/flixopt/components.py index 45b022295..e6d4aeb98 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -14,8 +14,8 @@ from . import io as fx_io from .core import PeriodicDataUser, PlausibilityError, TemporalData, TemporalDataUser from .elements import Component, ComponentModel, Flow -from .features import InvestmentModel, PiecewiseModel -from .interface import OnOffParameters, PiecewiseConversion, SizingParameters +from .features import InvestmentModel, PiecewiseModel, SizingModel +from .interface import InvestmentParameters, OnOffParameters, PiecewiseConversion, SizingParameters from .modeling import BoundingPatterns from .structure import FlowSystemModel, register_class_for_io @@ -865,7 +865,7 @@ def _do_modeling(self): if isinstance(self.element.capacity_in_flow_hours, SizingParameters): self.add_submodels( - InvestmentModel( + SizingModel( model=self._model, label_of_element=self.label_of_element, label_of_model=self.label_of_element, @@ -881,6 +881,9 @@ def _do_modeling(self): relative_bounds=self._relative_charge_state_bounds, ) + elif isinstance(self.element.capacity_in_flow_hours, InvestmentParameters): + raise NotImplementedError + # Initial charge state self._initial_and_final_charge_state() From 63707a5f13e42f3035a0bc7d756834df9a8416bb Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 1 Nov 2025 13:40:42 +0100 Subject: [PATCH 417/448] Re-add old InvestParameters --- flixopt/__init__.py | 1 + flixopt/commons.py | 2 ++ flixopt/interface.py | 11 +++++++++++ 3 files changed, 14 insertions(+) diff --git a/flixopt/__init__.py b/flixopt/__init__.py index 0145ace53..73bdd4e0f 100644 --- a/flixopt/__init__.py +++ b/flixopt/__init__.py @@ -21,6 +21,7 @@ FlowSystem, FullCalculation, InvestmentParameters, + InvestParameters, LinearConverter, OnOffParameters, Piece, diff --git a/flixopt/commons.py b/flixopt/commons.py index dfd2c268e..87edd9d07 100644 --- a/flixopt/commons.py +++ b/flixopt/commons.py @@ -20,6 +20,7 @@ from .flow_system import FlowSystem from .interface import ( InvestmentParameters, + InvestParameters, OnOffParameters, Piece, Piecewise, @@ -59,4 +60,5 @@ 'linear_converters', 'solvers', 'SizingParameters', + 'InvestParameters', ] diff --git a/flixopt/interface.py b/flixopt/interface.py index 5fbd0a3d5..54772c937 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -925,6 +925,17 @@ def maximum_or_fixed_size(self) -> PeriodicData: return self.fixed_size if self.fixed_size is not None else self.maximum_size +@register_class_for_io +class InvestParameters(SizingParameters): + def __init__(self, **kwargs): + warnings.warn( + 'InvestParameters is deprecated, use SizingParameters instead', + DeprecationWarning, + stacklevel=2, + ) + super().__init__(**kwargs) + + InvestmentPeriodData = PeriodicDataUser """This datatype is used to define things related to the period of investment.""" InvestmentPeriodDataBool = bool | InvestmentPeriodData From efd6b3153d1893b8d3cff058d9ffe2aca6f35ecf Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 1 Nov 2025 13:52:25 +0100 Subject: [PATCH 418/448] Unify Contributing guides --- .github/CONTRIBUTING.md | 199 ++++++++++++++++++++++++++++------------ CHANGELOG.md | 1 + CONTRIBUTE.md | 168 --------------------------------- docs/contribute.md | 2 +- 4 files changed, 143 insertions(+), 227 deletions(-) delete mode 100644 CONTRIBUTE.md diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index e9876c089..5c73ba04b 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,85 +1,168 @@ # Contributing to FlixOpt -Thanks for your interest in contributing to FlixOpt! 🚀 +We warmly welcome contributions from the community! Whether you're fixing bugs, adding features, improving documentation, or sharing examples, your contributions are valuable. -## Quick Start +## Ways to Contribute -1. **Fork & Clone** +### 🐛 Report Issues +Found a bug or have a feature request? Please [open an issue](https://github.com/flixOpt/flixopt/issues) on GitHub. + +When reporting issues, please include: +- A clear description of the problem +- Steps to reproduce the issue +- Expected vs. actual behavior +- Your environment (OS, Python version, FlixOpt version) +- Minimal code example if applicable + +### 💡 Share Examples +Help others learn FlixOpt by contributing examples: +- Real-world use cases +- Tutorial notebooks +- Integration examples with other tools +- Add them to the `examples/` directory + +### 📖 Improve Documentation +Documentation improvements are always welcome: +- Fix typos or clarify existing docs +- Add missing documentation +- Translate documentation +- Improve code comments + +### 🔧 Submit Code Contributions +Ready to contribute code? Great! See the sections below for setup and guidelines. + +--- + +## Development Setup + +### Getting Started +1. Fork and clone the repository: ```bash - git clone https://github.com/yourusername/flixopt.git + git clone https://github.com/flixOpt/flixopt.git cd flixopt ``` -2. **Install for Development** +2. Install development dependencies: ```bash - pip install -e ".[full, dev, docs]" + pip install -e ".[full, dev]" ``` -3. **Make Changes & Submit PR** +3. Set up pre-commit hooks (one-time setup): ```bash - git checkout -b feature/your-change - # Make your changes - git commit -m "Add: description of changes" - git push origin feature/your-change - # Create Pull Request on GitHub + pre-commit install ``` -## How to Contribute - -### 🐛 **Found a Bug?** -Use our [bug report template](https://github.com/flixOpt/flixopt/issues/new?template=bug_report.yml) with: -- Minimal code example -- FlixOpt version, Python version, solver used -- Expected vs actual behavior - -### ✨ **Have a Feature Idea?** -Use our [feature request template](https://github.com/flixOpt/flixopt/issues/new?template=feature_request.yml) with: -- Clear energy system use case -- Specific examples of what you want to model +4. Verify your setup: + ```bash + pytest + ``` -### ❓ **Need Help?** -- Check the [documentation](https://flixopt.github.io/flixopt/latest/) first -- Search [existing issues](https://github.com/flixOpt/flixopt/issues) -- Start a [discussion](https://github.com/flixOpt/flixopt/discussions) +### Working with Documentation +FlixOpt uses [mkdocs](https://www.mkdocs.org/) to generate documentation. -## Code Guidelines +To work on documentation: +```bash +pip install -e ".[docs]" +mkdocs serve +``` +Then navigate to http://127.0.0.1:8000/ -- **Style**: Follow PEP 8, use descriptive names -- **Documentation**: Add docstrings with units (kW, kWh, etc.) if applicable -- **Energy Focus**: Use energy domain terminology consistently -- **Testing**: Test with different solvers when applicable +--- -### Example +## Code Quality Standards + +### Automated Checks +We use [Ruff](https://github.com/astral-sh/ruff) for linting and formatting. After the one-time setup above, **code quality checks run automatically on every commit**. + +### Manual Checks +To run checks manually: +- `ruff check --fix .` - Check and fix linting issues +- `ruff format .` - Format code +- `pre-commit run --all-files` - Run all pre-commit checks + +### Testing +All tests are located in the `tests/` directory with a flat structure: +- `test_component.py` - Component tests +- `test_flow.py` - Flow tests +- `test_storage.py` - Storage tests +- etc. + +#### Running Tests +- `pytest` - Run the full test suite (excluding examples by default) +- `pytest tests/test_component.py` - Run a specific test file +- `pytest tests/test_component.py::TestClassName` - Run a specific test class +- `pytest tests/test_component.py::TestClassName::test_method` - Run a specific test +- `pytest -m slow` - Run only slow tests +- `pytest -m examples` - Run example tests (normally skipped) +- `pytest -k "keyword"` - Run tests matching a keyword + +#### Common Test Patterns +The `tests/conftest.py` file provides shared fixtures: +- `solver_fixture` - Parameterized solver fixture (HiGHS, Gurobi) +- `highs_solver` - HiGHS solver instance +- Coordinate configuration fixtures for timesteps, periods, scenarios + +Use these fixtures by adding them as function parameters: ```python -def create_storage( - label: str, - capacity_kwh: float, - charging_power_kw: float -) -> Storage: - """ - Create a battery storage component. - - Args: - label: Unique identifier - capacity_kwh: Storage capacity [kWh] - charging_power_kw: Maximum charging power [kW] - """ +def test_my_feature(solver_fixture): + # solver_fixture is automatically provided by pytest + model = fx.FlowSystem(...) + model.solve(solver_fixture) ``` -## What We Welcome +#### Testing Guidelines +- Write tests for all new functionality +- Ensure all tests pass before submitting a PR +- Aim for 100% test coverage for new code +- Use descriptive test names that explain what's being tested +- Add the `@pytest.mark.slow` decorator for tests that take >5 seconds -- 🔧 New energy components (batteries, heat pumps, etc.) -- 📚 Documentation improvements -- 🐛 Bug fixes -- 🧪 Test cases -- 💡 Energy system examples +### Coding Guidelines +- Follow [PEP 8](https://pep8.org/) style guidelines +- Write clear, self-documenting code with helpful comments +- Include type hints for function signatures +- Create or update tests for new functionality +- Aim for 100% test coverage for new code -## Questions? +--- -- 📖 [Documentation](https://flixopt.github.io/flixopt/latest/) -- 💬 [Discussions](https://github.com/flixOpt/flixopt/discussions) -- 📧 Contact maintainers (see README) +## Workflow + +### Branches & Pull Requests +1. Create a feature branch from `main`: + ```bash + git checkout -b feature/your-feature-name + ``` + +2. Make your changes and commit them with clear messages + +3. Push your branch and open a Pull Request + +4. Ensure all CI checks pass + +### Branch Naming +- Features: `feature/feature-name` +- Bug fixes: `fix/bug-description` +- Documentation: `docs/what-changed` + +### Commit Messages +- Use clear, descriptive commit messages +- Start with a verb (Add, Fix, Update, Remove, etc.) +- Keep the first line under 72 characters --- -**Every contribution helps advance sustainable energy solutions! 🌱⚡** +## Releases + +We follow **Semantic Versioning** (MAJOR.MINOR.PATCH). Releases are created manually from the `main` branch by maintainers. + +--- + +## Questions? + +If you have questions or need help, feel free to: +- Open a discussion on GitHub +- Ask in an issue +- Reach out to the maintainers + +Thank you for contributing to FlixOpt! diff --git a/CHANGELOG.md b/CHANGELOG.md index cf3401514..ca4f80c5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,6 +72,7 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp ### 📦 Dependencies ### 📝 Docs +- Unified contributing guides in docs and on github ### 👷 Development - Added type hints for submodel in all Interface classes diff --git a/CONTRIBUTE.md b/CONTRIBUTE.md deleted file mode 100644 index 5c73ba04b..000000000 --- a/CONTRIBUTE.md +++ /dev/null @@ -1,168 +0,0 @@ -# Contributing to FlixOpt - -We warmly welcome contributions from the community! Whether you're fixing bugs, adding features, improving documentation, or sharing examples, your contributions are valuable. - -## Ways to Contribute - -### 🐛 Report Issues -Found a bug or have a feature request? Please [open an issue](https://github.com/flixOpt/flixopt/issues) on GitHub. - -When reporting issues, please include: -- A clear description of the problem -- Steps to reproduce the issue -- Expected vs. actual behavior -- Your environment (OS, Python version, FlixOpt version) -- Minimal code example if applicable - -### 💡 Share Examples -Help others learn FlixOpt by contributing examples: -- Real-world use cases -- Tutorial notebooks -- Integration examples with other tools -- Add them to the `examples/` directory - -### 📖 Improve Documentation -Documentation improvements are always welcome: -- Fix typos or clarify existing docs -- Add missing documentation -- Translate documentation -- Improve code comments - -### 🔧 Submit Code Contributions -Ready to contribute code? Great! See the sections below for setup and guidelines. - ---- - -## Development Setup - -### Getting Started -1. Fork and clone the repository: - ```bash - git clone https://github.com/flixOpt/flixopt.git - cd flixopt - ``` - -2. Install development dependencies: - ```bash - pip install -e ".[full, dev]" - ``` - -3. Set up pre-commit hooks (one-time setup): - ```bash - pre-commit install - ``` - -4. Verify your setup: - ```bash - pytest - ``` - -### Working with Documentation -FlixOpt uses [mkdocs](https://www.mkdocs.org/) to generate documentation. - -To work on documentation: -```bash -pip install -e ".[docs]" -mkdocs serve -``` -Then navigate to http://127.0.0.1:8000/ - ---- - -## Code Quality Standards - -### Automated Checks -We use [Ruff](https://github.com/astral-sh/ruff) for linting and formatting. After the one-time setup above, **code quality checks run automatically on every commit**. - -### Manual Checks -To run checks manually: -- `ruff check --fix .` - Check and fix linting issues -- `ruff format .` - Format code -- `pre-commit run --all-files` - Run all pre-commit checks - -### Testing -All tests are located in the `tests/` directory with a flat structure: -- `test_component.py` - Component tests -- `test_flow.py` - Flow tests -- `test_storage.py` - Storage tests -- etc. - -#### Running Tests -- `pytest` - Run the full test suite (excluding examples by default) -- `pytest tests/test_component.py` - Run a specific test file -- `pytest tests/test_component.py::TestClassName` - Run a specific test class -- `pytest tests/test_component.py::TestClassName::test_method` - Run a specific test -- `pytest -m slow` - Run only slow tests -- `pytest -m examples` - Run example tests (normally skipped) -- `pytest -k "keyword"` - Run tests matching a keyword - -#### Common Test Patterns -The `tests/conftest.py` file provides shared fixtures: -- `solver_fixture` - Parameterized solver fixture (HiGHS, Gurobi) -- `highs_solver` - HiGHS solver instance -- Coordinate configuration fixtures for timesteps, periods, scenarios - -Use these fixtures by adding them as function parameters: -```python -def test_my_feature(solver_fixture): - # solver_fixture is automatically provided by pytest - model = fx.FlowSystem(...) - model.solve(solver_fixture) -``` - -#### Testing Guidelines -- Write tests for all new functionality -- Ensure all tests pass before submitting a PR -- Aim for 100% test coverage for new code -- Use descriptive test names that explain what's being tested -- Add the `@pytest.mark.slow` decorator for tests that take >5 seconds - -### Coding Guidelines -- Follow [PEP 8](https://pep8.org/) style guidelines -- Write clear, self-documenting code with helpful comments -- Include type hints for function signatures -- Create or update tests for new functionality -- Aim for 100% test coverage for new code - ---- - -## Workflow - -### Branches & Pull Requests -1. Create a feature branch from `main`: - ```bash - git checkout -b feature/your-feature-name - ``` - -2. Make your changes and commit them with clear messages - -3. Push your branch and open a Pull Request - -4. Ensure all CI checks pass - -### Branch Naming -- Features: `feature/feature-name` -- Bug fixes: `fix/bug-description` -- Documentation: `docs/what-changed` - -### Commit Messages -- Use clear, descriptive commit messages -- Start with a verb (Add, Fix, Update, Remove, etc.) -- Keep the first line under 72 characters - ---- - -## Releases - -We follow **Semantic Versioning** (MAJOR.MINOR.PATCH). Releases are created manually from the `main` branch by maintainers. - ---- - -## Questions? - -If you have questions or need help, feel free to: -- Open a discussion on GitHub -- Ask in an issue -- Reach out to the maintainers - -Thank you for contributing to FlixOpt! diff --git a/docs/contribute.md b/docs/contribute.md index e1b93aecb..557d72a03 100644 --- a/docs/contribute.md +++ b/docs/contribute.md @@ -1 +1 @@ -{! ../CONTRIBUTE.md !} +{! ../.github/CONTRIBUTING.md !} From 31844f017009e4dd980ce7a1754af74cf8788533 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 1 Nov 2025 13:56:12 +0100 Subject: [PATCH 419/448] Feature/402 feature silent framework (#407) * Supress solver output in SegmentedCalculation in favor of a progress bar, add method to suppress_output() and add tqdm to dependencies * Add logging.info() about solving * Merge main intop feature/402-feature-silent-framework * Merge main intop feature/402-feature-silent-framework * Add extra log_to_console option to solvers.py * Add extra log_to_console option to solvers.py * Add extra log_to_console option config.py * Add to tests * Use default from console to say if logging to console (gurobipy still has some issues...) * Add rounding duration of solve * Use contextmanager to entirely supress output in SegmentedCalculation * Improve suppress_output() * More options in config.py * Update CHANGELOG.md * Use new Config options in examples * Sett plotting backend in CI directly, overwriting all configs * Fixed tqdm progress bar to respect CONFIG.silent() * Replaced print() with framework logger (examples/05_Two-stage-optimization/two_stage_optimization.py * Added comprehensive tests for suppress_output() * Remove unused import * Ensure progress bar cleanup on exceptions. * Add test * Split method in SegmentedCalculation for better distinction if show or not show solver output * USe config show in exmaples * USe config show in results.plot_network() * Improve readabailty of code * Typo --- .github/workflows/python-app.yaml | 2 + CHANGELOG.md | 12 +- examples/00_Minmal/minimal_example.py | 3 +- examples/01_Simple/simple_example.py | 7 +- examples/02_Complex/complex_example.py | 5 +- .../02_Complex/complex_example_results.py | 7 +- .../example_calculation_types.py | 7 +- examples/04_Scenarios/scenario_example.py | 5 +- .../two_stage_optimization.py | 2 + flixopt/calculation.py | 130 ++++++++++++---- flixopt/config.py | 115 ++++++++++++++ flixopt/io.py | 59 ++++++++ flixopt/results.py | 4 +- flixopt/solvers.py | 16 +- pyproject.toml | 1 + tests/conftest.py | 23 --- tests/test_config.py | 137 +++++++++++++++++ tests/test_io.py | 143 ++++++++++++++++++ 18 files changed, 598 insertions(+), 80 deletions(-) diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml index f4dbc28c5..66ceceab4 100644 --- a/.github/workflows/python-app.yaml +++ b/.github/workflows/python-app.yaml @@ -24,6 +24,8 @@ concurrency: env: PYTHON_VERSION: "3.11" + MPLBACKEND: Agg # Non-interactive matplotlib backend for CI/testing + PLOTLY_RENDERER: json # Headless plotly renderer for CI/testing jobs: lint: diff --git a/CHANGELOG.md b/CHANGELOG.md index ca4f80c5c..6282e8d19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,13 +51,23 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp ## [Unreleased] - ????-??-?? -**Summary**: +**Summary**: Enhanced solver configuration with new CONFIG.Solving section for centralized solver parameter management. If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/). ### ✨ Added +**Solver configuration:** +- **New `CONFIG.Solving` configuration section** for centralized solver parameter management: + - `mip_gap`: Default MIP gap tolerance for solver convergence (default: 0.01) + - `time_limit_seconds`: Default time limit in seconds for solver runs (default: 300) + - `log_to_console`: Whether solver should output to console (default: True) + - `log_main_results`: Whether to log main results after solving (default: True) +- Solvers (`HighsSolver`, `GurobiSolver`) now use `CONFIG.Solving` defaults for parameters, allowing global configuration +- Solver parameters can still be explicitly overridden when creating solver instances + ### 💥 Breaking Changes +- Individual solver output is now hidden in **SegmentedCalculation**. To return to the prior behaviour, set `show_individual_solves=True` in `do_modeling_and_solve()`. ### ♻️ Changed diff --git a/examples/00_Minmal/minimal_example.py b/examples/00_Minmal/minimal_example.py index 6a0ed3831..92e6801b2 100644 --- a/examples/00_Minmal/minimal_example.py +++ b/examples/00_Minmal/minimal_example.py @@ -9,8 +9,7 @@ import flixopt as fx if __name__ == '__main__': - fx.CONFIG.Logging.console = True - fx.CONFIG.apply() + fx.CONFIG.silent() flow_system = fx.FlowSystem(pd.date_range('2020-01-01', periods=3, freq='h')) flow_system.add_elements( diff --git a/examples/01_Simple/simple_example.py b/examples/01_Simple/simple_example.py index 6b62d6712..fd5a3d9b7 100644 --- a/examples/01_Simple/simple_example.py +++ b/examples/01_Simple/simple_example.py @@ -8,9 +8,8 @@ import flixopt as fx if __name__ == '__main__': - # Enable console logging - fx.CONFIG.Logging.console = True - fx.CONFIG.apply() + fx.CONFIG.exploring() + # --- Create Time Series Data --- # Heat demand profile (e.g., kW) over time and corresponding power prices heat_demand_per_h = np.array([30, 0, 90, 110, 110, 20, 20, 20, 20]) @@ -101,7 +100,7 @@ flow_system.add_elements(costs, CO2, boiler, storage, chp, heat_sink, gas_source, power_sink) # Visualize the flow system for validation purposes - flow_system.plot_network(show=True) + flow_system.plot_network() # --- Define and Run Calculation --- # Create a calculation object to model the Flow System diff --git a/examples/02_Complex/complex_example.py b/examples/02_Complex/complex_example.py index 805cb08f6..b8ef76a03 100644 --- a/examples/02_Complex/complex_example.py +++ b/examples/02_Complex/complex_example.py @@ -9,9 +9,8 @@ import flixopt as fx if __name__ == '__main__': - # Enable console logging - fx.CONFIG.Logging.console = True - fx.CONFIG.apply() + fx.CONFIG.exploring() + # --- Experiment Options --- # Configure options for testing various parameters and behaviors check_penalty = False diff --git a/examples/02_Complex/complex_example_results.py b/examples/02_Complex/complex_example_results.py index 96d06dd04..96191c4d8 100644 --- a/examples/02_Complex/complex_example_results.py +++ b/examples/02_Complex/complex_example_results.py @@ -5,9 +5,8 @@ import flixopt as fx if __name__ == '__main__': - # Enable console logging - fx.CONFIG.Logging.console = True - fx.CONFIG.apply() + fx.CONFIG.exploring() + # --- Load Results --- try: results = fx.results.CalculationResults.from_file('results', 'complex example') @@ -19,7 +18,7 @@ ) from e # --- Basic overview --- - results.plot_network(show=True) + results.plot_network() results['Fernwärme'].plot_node_balance() # --- Detailed Plots --- diff --git a/examples/03_Calculation_types/example_calculation_types.py b/examples/03_Calculation_types/example_calculation_types.py index c5df50034..e339c1c24 100644 --- a/examples/03_Calculation_types/example_calculation_types.py +++ b/examples/03_Calculation_types/example_calculation_types.py @@ -11,9 +11,8 @@ import flixopt as fx if __name__ == '__main__': - # Enable console logging - fx.CONFIG.Logging.console = True - fx.CONFIG.apply() + fx.CONFIG.exploring() + # Calculation Types full, segmented, aggregated = True, True, True @@ -165,7 +164,7 @@ a_kwk, a_speicher, ) - flow_system.plot_network(controls=False, show=True) + flow_system.plot_network() # Calculations calculations: list[fx.FullCalculation | fx.AggregatedCalculation | fx.SegmentedCalculation] = [] diff --git a/examples/04_Scenarios/scenario_example.py b/examples/04_Scenarios/scenario_example.py index d258d4142..bf4f24617 100644 --- a/examples/04_Scenarios/scenario_example.py +++ b/examples/04_Scenarios/scenario_example.py @@ -8,6 +8,8 @@ import flixopt as fx if __name__ == '__main__': + fx.CONFIG.exploring() + # Create datetime array starting from '2020-01-01' for one week timesteps = pd.date_range('2020-01-01', periods=24 * 7, freq='h') scenarios = pd.Index(['Base Case', 'High Demand']) @@ -186,7 +188,7 @@ flow_system.add_elements(costs, CO2, boiler, storage, chp, heat_sink, gas_source, power_sink) # Visualize the flow system for validation purposes - flow_system.plot_network(show=True) + flow_system.plot_network() # --- Define and Run Calculation --- # Create a calculation object to model the Flow System @@ -215,7 +217,6 @@ # Convert the results for the storage component to a dataframe and display df = calculation.results['Storage'].node_balance_with_charge_state() - print(df) # Save results to file for later usage calculation.results.to_file() diff --git a/examples/05_Two-stage-optimization/two_stage_optimization.py b/examples/05_Two-stage-optimization/two_stage_optimization.py index dde3ae069..9647e803c 100644 --- a/examples/05_Two-stage-optimization/two_stage_optimization.py +++ b/examples/05_Two-stage-optimization/two_stage_optimization.py @@ -19,6 +19,8 @@ logger = logging.getLogger('flixopt') if __name__ == '__main__': + fx.CONFIG.exploring() + # Data Import data_import = pd.read_csv( pathlib.Path(__file__).parent.parent / 'resources' / 'Zeitreihen2020.csv', index_col=0 diff --git a/flixopt/calculation.py b/flixopt/calculation.py index b1f0d0c6b..d9eb0d628 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -13,6 +13,7 @@ import logging import math import pathlib +import sys import timeit import warnings from collections import Counter @@ -20,6 +21,7 @@ import numpy as np import yaml +from tqdm import tqdm from . import io as fx_io from .aggregation import Aggregation, AggregationModel, AggregationParameters @@ -227,7 +229,7 @@ def fix_sizes(self, ds: xr.Dataset, decimal_rounding: int | None = 5) -> FullCal return self def solve( - self, solver: _Solver, log_file: pathlib.Path | None = None, log_main_results: bool = True + self, solver: _Solver, log_file: pathlib.Path | None = None, log_main_results: bool | None = None ) -> FullCalculation: t_start = timeit.default_timer() @@ -237,6 +239,8 @@ def solve( **solver.options, ) self.durations['solving'] = round(timeit.default_timer() - t_start, 2) + logger.info(f'Model solved with {solver.name} in {self.durations["solving"]:.2f} seconds.') + logger.info(f'Model status after solve: {self.model.status}') if self.model.status == 'warning': # Save the model and the flow_system to file in case of infeasibility @@ -250,7 +254,8 @@ def solve( ) # Log the formatted output - if log_main_results: + should_log = log_main_results if log_main_results is not None else CONFIG.Solving.log_main_results + if should_log: logger.info( f'{" Main Results ":#^80}\n' + yaml.dump( @@ -368,7 +373,7 @@ def _perform_aggregation(self): ) self.aggregation.cluster() - self.aggregation.plot(show=True, save=self.folder / 'aggregation.html') + self.aggregation.plot(show=CONFIG.Plotting.default_show, save=self.folder / 'aggregation.html') if self.aggregation_parameters.aggregate_data_and_fix_non_binary_vars: ds = self.flow_system.to_dataset() for name, series in self.aggregation.aggregated_data.items(): @@ -569,48 +574,111 @@ def _create_sub_calculations(self): f'({timesteps_of_segment[0]} -> {timesteps_of_segment[-1]}):' ) + def _solve_single_segment( + self, + i: int, + calculation: FullCalculation, + solver: _Solver, + log_file: pathlib.Path | None, + log_main_results: bool, + suppress_output: bool, + ) -> None: + """Solve a single segment calculation.""" + if i > 0 and self.nr_of_previous_values > 0: + self._transfer_start_values(i) + + calculation.do_modeling() + + # Warn about Investments, but only in first run + if i == 0: + invest_elements = [ + model.label_full + for component in calculation.flow_system.components.values() + for model in component.submodel.all_submodels + if isinstance(model, InvestmentModel) + ] + if invest_elements: + logger.critical( + f'Investments are not supported in Segmented Calculation! ' + f'Following InvestmentModels were found: {invest_elements}' + ) + + log_path = pathlib.Path(log_file) if log_file is not None else self.folder / f'{self.name}.log' + + if suppress_output: + with fx_io.suppress_output(): + calculation.solve(solver, log_file=log_path, log_main_results=log_main_results) + else: + calculation.solve(solver, log_file=log_path, log_main_results=log_main_results) + def do_modeling_and_solve( - self, solver: _Solver, log_file: pathlib.Path | None = None, log_main_results: bool = False + self, + solver: _Solver, + log_file: pathlib.Path | None = None, + log_main_results: bool = False, + show_individual_solves: bool = False, ) -> SegmentedCalculation: + """Model and solve all segments of the segmented calculation. + + This method creates sub-calculations for each time segment, then iteratively + models and solves each segment. It supports two output modes: a progress bar + for compact output, or detailed individual solve information. + + Args: + solver: The solver instance to use for optimization (e.g., Gurobi, HiGHS). + log_file: Optional path to the solver log file. If None, defaults to + folder/name.log. + log_main_results: Whether to log main results (objective, effects, etc.) + after each segment solve. Defaults to False. + show_individual_solves: If True, shows detailed output for each segment + solve with logger messages. If False (default), shows a compact progress + bar with suppressed solver output for cleaner display. + + Returns: + Self, for method chaining. + + Note: + The method automatically transfers all start values between segments to ensure + continuity of storage states and flow rates across segment boundaries. + """ logger.info(f'{"":#^80}') logger.info(f'{" Segmented Solving ":#^80}') self._create_sub_calculations() - for i, calculation in enumerate(self.sub_calculations): - logger.info( - f'{self.segment_names[i]} [{i + 1:>2}/{len(self.segment_names):<2}] ' - f'({calculation.flow_system.timesteps[0]} -> {calculation.flow_system.timesteps[-1]}):' + if show_individual_solves: + # Path 1: Show individual solves with detailed output + for i, calculation in enumerate(self.sub_calculations): + logger.info( + f'Solving segment {i + 1}/{len(self.sub_calculations)}: ' + f'{calculation.flow_system.timesteps[0]} -> {calculation.flow_system.timesteps[-1]}' + ) + self._solve_single_segment(i, calculation, solver, log_file, log_main_results, suppress_output=False) + else: + # Path 2: Show only progress bar with suppressed output + progress_bar = tqdm( + enumerate(self.sub_calculations), + total=len(self.sub_calculations), + desc='Solving segments', + unit='segment', + file=sys.stdout, + disable=not CONFIG.Solving.log_to_console, ) - if i > 0 and self.nr_of_previous_values > 0: - self._transfer_start_values(i) - - calculation.do_modeling() - - # Warn about Investments, but only in fist run - if i == 0: - invest_elements = [ - model.label_full - for component in calculation.flow_system.components.values() - for model in component.submodel.all_submodels - if isinstance(model, InvestmentModel) - ] - if invest_elements: - logger.critical( - f'Investments are not supported in Segmented Calculation! ' - f'Following InvestmentModels were found: {invest_elements}' + try: + for i, calculation in progress_bar: + progress_bar.set_description( + f'Solving ({calculation.flow_system.timesteps[0]} -> {calculation.flow_system.timesteps[-1]})' ) - - calculation.solve( - solver, - log_file=pathlib.Path(log_file) if log_file is not None else self.folder / f'{self.name}.log', - log_main_results=log_main_results, - ) + self._solve_single_segment(i, calculation, solver, log_file, log_main_results, suppress_output=True) + finally: + progress_bar.close() for calc in self.sub_calculations: for key, value in calc.durations.items(): self.durations[key] += value + logger.info(f'Model solved with {solver.name} in {self.durations["solving"]:.2f} seconds.') + self.results = SegmentedCalculationResults.from_calculation(self) return self diff --git a/flixopt/config.py b/flixopt/config.py index 670f86da2..a74740efb 100644 --- a/flixopt/config.py +++ b/flixopt/config.py @@ -63,6 +63,14 @@ 'default_qualitative_colorscale': 'plotly', } ), + 'solving': MappingProxyType( + { + 'mip_gap': 0.01, + 'time_limit_seconds': 300, + 'log_to_console': True, + 'log_main_results': True, + } + ), } ) @@ -75,6 +83,8 @@ class CONFIG: Attributes: Logging: Logging configuration. Modeling: Optimization modeling parameters. + Solving: Solver configuration and default parameters. + Plotting: Plotting configuration. config_name: Configuration name. Examples: @@ -91,6 +101,9 @@ class CONFIG: level: DEBUG console: true file: app.log + solving: + mip_gap: 0.001 + time_limit_seconds: 600 ``` """ @@ -194,6 +207,30 @@ class Modeling: epsilon: float = _DEFAULTS['modeling']['epsilon'] big_binary_bound: int = _DEFAULTS['modeling']['big_binary_bound'] + class Solving: + """Solver configuration and default parameters. + + Attributes: + mip_gap: Default MIP gap tolerance for solver convergence. + time_limit_seconds: Default time limit in seconds for solver runs. + log_to_console: Whether solver should output to console. + log_main_results: Whether to log main results after solving. + + Examples: + ```python + # Set tighter convergence and longer timeout + CONFIG.Solving.mip_gap = 0.001 + CONFIG.Solving.time_limit_seconds = 600 + CONFIG.Solving.log_to_console = False + CONFIG.apply() + ``` + """ + + mip_gap: float = _DEFAULTS['solving']['mip_gap'] + time_limit_seconds: int = _DEFAULTS['solving']['time_limit_seconds'] + log_to_console: bool = _DEFAULTS['solving']['log_to_console'] + log_main_results: bool = _DEFAULTS['solving']['log_main_results'] + class Plotting: """Plotting configuration. @@ -246,6 +283,12 @@ def reset(cls): for key, value in _DEFAULTS['modeling'].items(): setattr(cls.Modeling, key, value) + for key, value in _DEFAULTS['solving'].items(): + setattr(cls.Solving, key, value) + + for key, value in _DEFAULTS['plotting'].items(): + setattr(cls.Plotting, key, value) + cls.config_name = _DEFAULTS['config_name'] cls.apply() @@ -329,6 +372,12 @@ def _apply_config_dict(cls, config_dict: dict): elif key == 'modeling' and isinstance(value, dict): for nested_key, nested_value in value.items(): setattr(cls.Modeling, nested_key, nested_value) + elif key == 'solving' and isinstance(value, dict): + for nested_key, nested_value in value.items(): + setattr(cls.Solving, nested_key, nested_value) + elif key == 'plotting' and isinstance(value, dict): + for nested_key, nested_value in value.items(): + setattr(cls.Plotting, nested_key, nested_value) elif hasattr(cls, key): setattr(cls, key, value) @@ -366,6 +415,12 @@ def to_dict(cls) -> dict: 'epsilon': cls.Modeling.epsilon, 'big_binary_bound': cls.Modeling.big_binary_bound, }, + 'solving': { + 'mip_gap': cls.Solving.mip_gap, + 'time_limit_seconds': cls.Solving.time_limit_seconds, + 'log_to_console': cls.Solving.log_to_console, + 'log_main_results': cls.Solving.log_main_results, + }, 'plotting': { 'default_show': cls.Plotting.default_show, 'default_engine': cls.Plotting.default_engine, @@ -376,6 +431,66 @@ def to_dict(cls) -> dict: }, } + @classmethod + def silent(cls) -> type[CONFIG]: + """Configure for silent operation. + + Disables console logging, solver output, and result logging + for clean production runs. Does not show plots. Automatically calls apply(). + """ + cls.Logging.console = False + cls.Plotting.default_show = False + cls.Logging.file = None + cls.Solving.log_to_console = False + cls.Solving.log_main_results = False + cls.apply() + return cls + + @classmethod + def debug(cls) -> type[CONFIG]: + """Configure for debug mode with verbose output. + + Enables console logging at DEBUG level and all solver output for + troubleshooting. Automatically calls apply(). + """ + cls.Logging.console = True + cls.Logging.level = 'DEBUG' + cls.Solving.log_to_console = True + cls.Solving.log_main_results = True + cls.apply() + return cls + + @classmethod + def exploring(cls) -> type[CONFIG]: + """Configure for exploring flixopt + + Enables console logging at INFO level and all solver output. + Also enables browser plotting for plotly with showing plots per default + """ + cls.Logging.console = True + cls.Logging.level = 'INFO' + cls.Solving.log_to_console = True + cls.Solving.log_main_results = True + cls.browser_plotting() + cls.apply() + return cls + + @classmethod + def browser_plotting(cls) -> type[CONFIG]: + """Configure for interactive usage with plotly to open plots in browser. + + Sets plotly.io.renderers.default = 'browser'. Useful for running examples + and viewing interactive plots. Does NOT modify CONFIG.Plotting settings. + """ + cls.Plotting.default_show = True + cls.apply() + + import plotly.io as pio + + pio.renderers.default = 'browser' + + return cls + class MultilineFormatter(logging.Formatter): """Formatter that handles multi-line messages with consistent prefixes. diff --git a/flixopt/io.py b/flixopt/io.py index 7f832ed0e..c5f839ed9 100644 --- a/flixopt/io.py +++ b/flixopt/io.py @@ -3,8 +3,11 @@ import inspect import json import logging +import os import pathlib import re +import sys +from contextlib import contextmanager from dataclasses import dataclass from typing import TYPE_CHECKING, Any @@ -931,3 +934,59 @@ def build_metadata_info(parts: list[str], prefix: str = ' | ') -> str: return '' info = ' | '.join(parts) return prefix + info if prefix else info + + +@contextmanager +def suppress_output(): + """ + Suppress all console output including C-level output from solvers. + + WARNING: Not thread-safe. Modifies global file descriptors. + Use only with sequential execution or multiprocessing. + """ + # Save original file descriptors + old_stdout_fd = os.dup(1) + old_stderr_fd = os.dup(2) + devnull_fd = None + + try: + # Open devnull + devnull_fd = os.open(os.devnull, os.O_WRONLY) + + # Flush Python buffers before redirecting + sys.stdout.flush() + sys.stderr.flush() + + # Redirect file descriptors to devnull + os.dup2(devnull_fd, 1) + os.dup2(devnull_fd, 2) + + yield + + finally: + # Restore original file descriptors with nested try blocks + # to ensure all cleanup happens even if one step fails + try: + # Flush any buffered output in the redirected streams + sys.stdout.flush() + sys.stderr.flush() + except (OSError, ValueError): + pass # Stream might be closed or invalid + + try: + os.dup2(old_stdout_fd, 1) + except OSError: + pass # Failed to restore stdout, continue cleanup + + try: + os.dup2(old_stderr_fd, 2) + except OSError: + pass # Failed to restore stderr, continue cleanup + + # Close all file descriptors + for fd in [devnull_fd, old_stdout_fd, old_stderr_fd]: + if fd is not None: + try: + os.close(fd) + except OSError: + pass # FD already closed or invalid diff --git a/flixopt/results.py b/flixopt/results.py index 91a530075..954af6669 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -1031,14 +1031,14 @@ def plot_network( ] ) = True, path: pathlib.Path | None = None, - show: bool = False, + show: bool | None = None, ) -> pyvis.network.Network | None: """Plot interactive network visualization of the system. Args: controls: Enable/disable interactive controls. path: Save path for network HTML. - show: Whether to display the plot. + show: Whether to display the plot. If None, uses CONFIG.Plotting.default_show. """ if path is None: path = self.folder / f'{self.name}--network.html' diff --git a/flixopt/solvers.py b/flixopt/solvers.py index 410d69434..e5db61192 100644 --- a/flixopt/solvers.py +++ b/flixopt/solvers.py @@ -8,6 +8,8 @@ from dataclasses import dataclass, field from typing import Any, ClassVar +from flixopt.config import CONFIG + logger = logging.getLogger('flixopt') @@ -17,14 +19,16 @@ class _Solver: Abstract base class for solvers. Args: - mip_gap: Acceptable relative optimality gap in [0.0, 1.0]. - time_limit_seconds: Time limit in seconds. + mip_gap: Acceptable relative optimality gap in [0.0, 1.0]. Defaults to CONFIG.Solving.mip_gap. + time_limit_seconds: Time limit in seconds. Defaults to CONFIG.Solving.time_limit_seconds. + log_to_console: If False, no output to console. Defaults to CONFIG.Solving.log_to_console. extra_options: Additional solver options merged into `options`. """ name: ClassVar[str] - mip_gap: float - time_limit_seconds: int + mip_gap: float = field(default_factory=lambda: CONFIG.Solving.mip_gap) + time_limit_seconds: int = field(default_factory=lambda: CONFIG.Solving.time_limit_seconds) + log_to_console: bool = field(default_factory=lambda: CONFIG.Solving.log_to_console) extra_options: dict[str, Any] = field(default_factory=dict) @property @@ -45,6 +49,7 @@ class GurobiSolver(_Solver): Args: mip_gap: Acceptable relative optimality gap in [0.0, 1.0]; mapped to Gurobi `MIPGap`. time_limit_seconds: Time limit in seconds; mapped to Gurobi `TimeLimit`. + log_to_console: If False, no output to console. extra_options: Additional solver options merged into `options`. """ @@ -55,6 +60,7 @@ def _options(self) -> dict[str, Any]: return { 'MIPGap': self.mip_gap, 'TimeLimit': self.time_limit_seconds, + 'LogToConsole': 1 if self.log_to_console else 0, } @@ -65,6 +71,7 @@ class HighsSolver(_Solver): Attributes: mip_gap: Acceptable relative optimality gap in [0.0, 1.0]; mapped to HiGHS `mip_rel_gap`. time_limit_seconds: Time limit in seconds; mapped to HiGHS `time_limit`. + log_to_console: If False, no output to console. extra_options: Additional solver options merged into `options`. threads (int | None): Number of threads to use. If None, HiGHS chooses. """ @@ -78,4 +85,5 @@ def _options(self) -> dict[str, Any]: 'mip_rel_gap': self.mip_gap, 'time_limit': self.time_limit_seconds, 'threads': self.threads, + 'log_to_console': self.log_to_console, } diff --git a/pyproject.toml b/pyproject.toml index 29c669b3e..7b86e641a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,7 @@ dependencies = [ # Utilities "pyyaml >= 6.0.0, < 7", "rich >= 13.0.0, < 15", + "tqdm >= 4.66.0, < 5", "tomli >= 2.0.1, < 3; python_version < '3.11'", # Only needed with python 3.10 or earlier # Default solver "highspy >= 1.5.3, < 2", diff --git a/tests/conftest.py b/tests/conftest.py index bd940b843..50c58e1ab 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -828,26 +828,3 @@ def cleanup_figures(): import matplotlib.pyplot as plt plt.close('all') - - -@pytest.fixture(scope='session', autouse=True) -def set_test_environment(): - """ - Configure plotting for test environment. - - This fixture runs once per test session to: - - Set matplotlib to use non-interactive 'Agg' backend - - Set plotly to use non-interactive 'json' renderer - - Prevent GUI windows from opening during tests - """ - import matplotlib - - matplotlib.use('Agg') # Use non-interactive backend - - import plotly.io as pio - - pio.renderers.default = 'json' # Use non-interactive renderer - - fx.CONFIG.Plotting.default_show = False - - yield diff --git a/tests/test_config.py b/tests/test_config.py index 60ed80555..a78330eb4 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -31,6 +31,10 @@ def test_config_defaults(self): assert CONFIG.Modeling.big == 10_000_000 assert CONFIG.Modeling.epsilon == 1e-5 assert CONFIG.Modeling.big_binary_bound == 100_000 + assert CONFIG.Solving.mip_gap == 0.01 + assert CONFIG.Solving.time_limit_seconds == 300 + assert CONFIG.Solving.log_to_console is True + assert CONFIG.Solving.log_main_results is True assert CONFIG.config_name == 'flixopt' def test_module_initialization(self): @@ -106,6 +110,11 @@ def test_config_to_dict(self): assert config_dict['logging']['rich'] is False assert 'modeling' in config_dict assert config_dict['modeling']['big'] == 10_000_000 + assert 'solving' in config_dict + assert config_dict['solving']['mip_gap'] == 0.01 + assert config_dict['solving']['time_limit_seconds'] == 300 + assert config_dict['solving']['log_to_console'] is True + assert config_dict['solving']['log_main_results'] is True def test_config_load_from_file(self, tmp_path): """Test loading configuration from YAML file.""" @@ -119,6 +128,10 @@ def test_config_load_from_file(self, tmp_path): modeling: big: 20000000 epsilon: 1e-6 +solving: + mip_gap: 0.001 + time_limit_seconds: 600 + log_main_results: false """ config_file.write_text(config_content) @@ -130,6 +143,9 @@ def test_config_load_from_file(self, tmp_path): assert CONFIG.Modeling.big == 20000000 # YAML may load epsilon as string, so convert for comparison assert float(CONFIG.Modeling.epsilon) == 1e-6 + assert CONFIG.Solving.mip_gap == 0.001 + assert CONFIG.Solving.time_limit_seconds == 600 + assert CONFIG.Solving.log_main_results is False def test_config_load_from_file_not_found(self): """Test that loading from non-existent file raises error.""" @@ -264,6 +280,10 @@ def test_custom_config_yaml_complete(self, tmp_path): big: 50000000 epsilon: 1e-4 big_binary_bound: 200000 +solving: + mip_gap: 0.005 + time_limit_seconds: 900 + log_main_results: false """ config_file.write_text(config_content) @@ -278,6 +298,9 @@ def test_custom_config_yaml_complete(self, tmp_path): assert CONFIG.Modeling.big == 50000000 assert float(CONFIG.Modeling.epsilon) == 1e-4 assert CONFIG.Modeling.big_binary_bound == 200000 + assert CONFIG.Solving.mip_gap == 0.005 + assert CONFIG.Solving.time_limit_seconds == 900 + assert CONFIG.Solving.log_main_results is False # Verify logging was applied logger = logging.getLogger('flixopt') @@ -426,6 +449,10 @@ def test_config_reset(self): CONFIG.Modeling.big = 99999999 CONFIG.Modeling.epsilon = 1e-8 CONFIG.Modeling.big_binary_bound = 500000 + CONFIG.Solving.mip_gap = 0.0001 + CONFIG.Solving.time_limit_seconds = 1800 + CONFIG.Solving.log_to_console = False + CONFIG.Solving.log_main_results = False CONFIG.config_name = 'test_config' # Reset should restore all defaults @@ -439,6 +466,10 @@ def test_config_reset(self): assert CONFIG.Modeling.big == 10_000_000 assert CONFIG.Modeling.epsilon == 1e-5 assert CONFIG.Modeling.big_binary_bound == 100_000 + assert CONFIG.Solving.mip_gap == 0.01 + assert CONFIG.Solving.time_limit_seconds == 300 + assert CONFIG.Solving.log_to_console is True + assert CONFIG.Solving.log_main_results is True assert CONFIG.config_name == 'flixopt' # Verify logging was also reset @@ -460,11 +491,17 @@ def test_reset_matches_class_defaults(self): CONFIG.Modeling.big = 999999 CONFIG.Modeling.epsilon = 1e-10 CONFIG.Modeling.big_binary_bound = 999999 + CONFIG.Solving.mip_gap = 0.0001 + CONFIG.Solving.time_limit_seconds = 9999 + CONFIG.Solving.log_to_console = False + CONFIG.Solving.log_main_results = False CONFIG.config_name = 'modified' # Verify values are actually different from defaults assert CONFIG.Logging.level != _DEFAULTS['logging']['level'] assert CONFIG.Modeling.big != _DEFAULTS['modeling']['big'] + assert CONFIG.Solving.mip_gap != _DEFAULTS['solving']['mip_gap'] + assert CONFIG.Solving.log_to_console != _DEFAULTS['solving']['log_to_console'] # Now reset CONFIG.reset() @@ -477,4 +514,104 @@ def test_reset_matches_class_defaults(self): assert CONFIG.Modeling.big == _DEFAULTS['modeling']['big'] assert CONFIG.Modeling.epsilon == _DEFAULTS['modeling']['epsilon'] assert CONFIG.Modeling.big_binary_bound == _DEFAULTS['modeling']['big_binary_bound'] + assert CONFIG.Solving.mip_gap == _DEFAULTS['solving']['mip_gap'] + assert CONFIG.Solving.time_limit_seconds == _DEFAULTS['solving']['time_limit_seconds'] + assert CONFIG.Solving.log_to_console == _DEFAULTS['solving']['log_to_console'] + assert CONFIG.Solving.log_main_results == _DEFAULTS['solving']['log_main_results'] assert CONFIG.config_name == _DEFAULTS['config_name'] + + def test_solving_config_defaults(self): + """Test that CONFIG.Solving has correct default values.""" + assert CONFIG.Solving.mip_gap == 0.01 + assert CONFIG.Solving.time_limit_seconds == 300 + assert CONFIG.Solving.log_to_console is True + assert CONFIG.Solving.log_main_results is True + + def test_solving_config_modification(self): + """Test that CONFIG.Solving attributes can be modified.""" + # Modify solving config + CONFIG.Solving.mip_gap = 0.005 + CONFIG.Solving.time_limit_seconds = 600 + CONFIG.Solving.log_main_results = False + CONFIG.apply() + + # Verify modifications + assert CONFIG.Solving.mip_gap == 0.005 + assert CONFIG.Solving.time_limit_seconds == 600 + assert CONFIG.Solving.log_main_results is False + + def test_solving_config_integration_with_solvers(self): + """Test that solvers use CONFIG.Solving defaults.""" + from flixopt import solvers + + # Test with default config + CONFIG.reset() + solver1 = solvers.HighsSolver() + assert solver1.mip_gap == CONFIG.Solving.mip_gap + assert solver1.time_limit_seconds == CONFIG.Solving.time_limit_seconds + + # Modify config and create new solver + CONFIG.Solving.mip_gap = 0.002 + CONFIG.Solving.time_limit_seconds = 900 + CONFIG.apply() + + solver2 = solvers.GurobiSolver() + assert solver2.mip_gap == 0.002 + assert solver2.time_limit_seconds == 900 + + # Explicit values should override config + solver3 = solvers.HighsSolver(mip_gap=0.1, time_limit_seconds=60) + assert solver3.mip_gap == 0.1 + assert solver3.time_limit_seconds == 60 + + def test_solving_config_yaml_loading(self, tmp_path): + """Test loading solving config from YAML file.""" + config_file = tmp_path / 'solving_config.yaml' + config_content = """ +solving: + mip_gap: 0.0001 + time_limit_seconds: 1200 + log_main_results: false +""" + config_file.write_text(config_content) + + CONFIG.load_from_file(config_file) + + assert CONFIG.Solving.mip_gap == 0.0001 + assert CONFIG.Solving.time_limit_seconds == 1200 + assert CONFIG.Solving.log_main_results is False + + def test_solving_config_in_to_dict(self): + """Test that CONFIG.Solving is included in to_dict().""" + CONFIG.Solving.mip_gap = 0.003 + CONFIG.Solving.time_limit_seconds = 450 + CONFIG.Solving.log_main_results = False + + config_dict = CONFIG.to_dict() + + assert 'solving' in config_dict + assert config_dict['solving']['mip_gap'] == 0.003 + assert config_dict['solving']['time_limit_seconds'] == 450 + assert config_dict['solving']['log_main_results'] is False + + def test_solving_config_persistence(self): + """Test that Solving config is independent of other configs.""" + # Set custom solving values + CONFIG.Solving.mip_gap = 0.007 + CONFIG.Solving.time_limit_seconds = 750 + + # Change and apply logging config + CONFIG.Logging.console = True + CONFIG.apply() + + # Solving values should be unchanged + assert CONFIG.Solving.mip_gap == 0.007 + assert CONFIG.Solving.time_limit_seconds == 750 + + # Change modeling config + CONFIG.Modeling.big = 99999999 + CONFIG.apply() + + # Solving values should still be unchanged + assert CONFIG.Solving.mip_gap == 0.007 + assert CONFIG.Solving.time_limit_seconds == 750 diff --git a/tests/test_io.py b/tests/test_io.py index dbbc4cc72..6d225734e 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -80,5 +80,148 @@ def test_flow_system_io(flow_system): flow_system.__str__() +def test_suppress_output_file_descriptors(tmp_path): + """Test that suppress_output() redirects file descriptors to /dev/null.""" + import os + import sys + + from flixopt.io import suppress_output + + # Create temporary files to capture output + test_file = tmp_path / 'test_output.txt' + + # Test that FD 1 (stdout) is redirected during suppression + with open(test_file, 'w') as f: + original_stdout_fd = os.dup(1) # Save original stdout FD + try: + # Redirect FD 1 to our test file + os.dup2(f.fileno(), 1) + os.write(1, b'before suppression\n') + + with suppress_output(): + # Inside suppress_output, writes should go to /dev/null, not our file + os.write(1, b'during suppression\n') + + # After suppress_output, writes should go to our file again + os.write(1, b'after suppression\n') + finally: + # Restore original stdout + os.dup2(original_stdout_fd, 1) + os.close(original_stdout_fd) + + # Read the file and verify content + content = test_file.read_text() + assert 'before suppression' in content + assert 'during suppression' not in content # This should NOT be in the file + assert 'after suppression' in content + + +def test_suppress_output_python_level(): + """Test that Python-level stdout/stderr continue to work after suppress_output().""" + import io + import sys + + from flixopt.io import suppress_output + + # Create a StringIO to capture Python-level output + captured_output = io.StringIO() + + # After suppress_output exits, Python streams should be functional + with suppress_output(): + pass # Just enter and exit the context + + # Redirect sys.stdout to our StringIO + old_stdout = sys.stdout + try: + sys.stdout = captured_output + print('test message') + finally: + sys.stdout = old_stdout + + # Verify Python-level stdout works + assert 'test message' in captured_output.getvalue() + + +def test_suppress_output_exception_handling(): + """Test that suppress_output() properly restores streams even on exception.""" + import sys + + from flixopt.io import suppress_output + + # Save original file descriptors + original_stdout_fd = sys.stdout.fileno() + original_stderr_fd = sys.stderr.fileno() + + try: + with suppress_output(): + raise ValueError('Test exception') + except ValueError: + pass + + # Verify streams are restored after exception + assert sys.stdout.fileno() == original_stdout_fd + assert sys.stderr.fileno() == original_stderr_fd + + # Verify we can still write to stdout/stderr + sys.stdout.write('test after exception\n') + sys.stdout.flush() + + +def test_suppress_output_c_level(): + """Test that suppress_output() suppresses C-level output (file descriptor level).""" + import os + import sys + + from flixopt.io import suppress_output + + # This test verifies that even low-level C writes are suppressed + # by writing directly to file descriptor 1 (stdout) + with suppress_output(): + # Try to write directly to FD 1 (stdout) - should be suppressed + os.write(1, b'C-level stdout write\n') + # Try to write directly to FD 2 (stderr) - should be suppressed + os.write(2, b'C-level stderr write\n') + + # After exiting context, ensure streams work + sys.stdout.write('After C-level test\n') + sys.stdout.flush() + + +def test_tqdm_cleanup_on_exception(): + """Test that tqdm progress bar is properly cleaned up even when exceptions occur. + + This test verifies the pattern used in SegmentedCalculation where a try/finally + block ensures progress_bar.close() is called even if an exception occurs. + """ + from tqdm import tqdm + + # Create a progress bar (disabled to avoid output during tests) + items = enumerate(range(5)) + progress_bar = tqdm(items, total=5, desc='Test progress', disable=True) + + # Track whether cleanup was called + cleanup_called = False + exception_raised = False + + try: + try: + for idx, _ in progress_bar: + if idx == 2: + raise ValueError('Test exception') + finally: + # This should always execute, even with exception + progress_bar.close() + cleanup_called = True + except ValueError: + exception_raised = True + + # Verify both that the exception was raised AND cleanup happened + assert exception_raised, 'Test exception should have been raised' + assert cleanup_called, 'Cleanup should have been called even with exception' + + # Verify that close() is idempotent - calling it again should not raise + progress_bar.close() # Should not raise even if already closed + + if __name__ == '__main__': pytest.main(['-v', '--disable-warnings']) From ca3d88e5100541178a4f8b332c710c01660d7b11 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 1 Nov 2025 13:59:28 +0100 Subject: [PATCH 420/448] Typo Bugfix --- flixopt/interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flixopt/interface.py b/flixopt/interface.py index 54772c937..60c50f21a 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -913,7 +913,7 @@ def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None f'{name_prefix}|fixed_size', self.fixed_size, dims=['period', 'scenario'] ) self.mandatory = flow_system.fit_to_model_coords( - f'{name_prefix}|mandatory', self.fixed_size, dims=['period', 'scenario'] + f'{name_prefix}|mandatory', self.mandatory, dims=['period', 'scenario'] ) @property From e7fe67beb903b0e7fa8925a9fbee5b246f25c404 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 1 Nov 2025 14:05:25 +0100 Subject: [PATCH 421/448] Add proper handling of mandatory size for all cases: all, some, no period(s) --- flixopt/features.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index 41043a0c7..cdc2a74b2 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -35,24 +35,30 @@ def _create_sizing_variables_and_constraints( self, size_min: PeriodicData, size_max: PeriodicData, - mandatory: bool, + mandatory: PeriodicData, dims: list[FlowSystemDimensions], ): """Create timing variables and constraints.""" + if not np.issubdtype(mandatory.dtype, np.bool_): + raise TypeError(f'Expected all bool values, got {mandatory.dtype=}: {mandatory}') size = self.add_variables( short_name='size', - lower=size_min if mandatory else 0, + lower=size_min.where(mandatory, 0), upper=size_max, coords=self._model.get_coords(dims), ) - if not mandatory: + if not mandatory.all(): self.add_variables( binary=True, coords=self._model.get_coords(dims), short_name='available', ) + self.add_constraints( + self.available == mandatory.astype(int), + short_name='mandatory', + ) BoundingPatterns.bounds_with_state( self, variable=size, From 3c65db9cbce4bd57cb488654fc6962c92f379e44 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 1 Nov 2025 16:34:34 +0100 Subject: [PATCH 422/448] Bugfix --- flixopt/elements.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flixopt/elements.py b/flixopt/elements.py index 28c6c8f22..44ce7fd49 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -493,7 +493,7 @@ def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None if self.on_off_parameters is not None: self.on_off_parameters.transform_data(flow_system, prefix) - if isinstance(self.size, _SizeModel): + if isinstance(self.size, _SizeParameters): self.size.transform_data(flow_system, prefix) else: self.size = flow_system.fit_to_model_coords(f'{prefix}|size', self.size, dims=['period', 'scenario']) From 36fae3b0f78bd947b883ff8007545478454dea00 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 1 Nov 2025 16:39:14 +0100 Subject: [PATCH 423/448] Bugfix --- flixopt/elements.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flixopt/elements.py b/flixopt/elements.py index 44ce7fd49..99aea75e1 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -601,7 +601,7 @@ def _create_on_off_model(self): def _create_investment_model(self): if isinstance(self.element.size, SizingParameters): self.add_submodels( - InvestmentModel( + SizingModel( model=self._model, label_of_element=self.label_of_element, parameters=self.element.size, From 2bddd5a1dbad1042a156f02ba13026eae80a7232 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 2 Nov 2025 15:22:52 +0100 Subject: [PATCH 424/448] Rename fixed_lifetime to lifetime --- flixopt/interface.py | 52 +++++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/flixopt/interface.py b/flixopt/interface.py index 60c50f21a..eb8083b7a 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -966,7 +966,7 @@ class InvestmentParameters(SizingParameters): [InvestmentParameters](../user-guide/mathematical-notation/features/InvestmentParameters.md) Args: - fixed_lifetime: REQUIRED. The investment lifetime in number of periods. + lifetime: REQUIRED. The investment lifetime in number of periods. Once invested, the asset operates for this many periods. allow_investment: Allow investment in specific periods. Default: True (all periods). force_investment: Force investment to occur in a specific period. Default: False. @@ -984,7 +984,7 @@ class InvestmentParameters(SizingParameters): ```python timing = InvestmentParameters( - fixed_lifetime=10, # Investment lasts 10 periods + lifetime=10, # Investment lasts 10 periods allow_investment=True, # Can invest in any period ) ``` @@ -993,7 +993,7 @@ class InvestmentParameters(SizingParameters): ```python timing = InvestmentParameters( - fixed_lifetime=10, # Must operate for 10 periods + lifetime=10, # Must operate for 10 periods force_investment=xr.DataArray( [0, 0, 1, 0, 0], # Force in period 3 (2030) coords=[('period', [2020, 2025, 2030, 2035, 2040])], @@ -1021,7 +1021,7 @@ class InvestmentParameters(SizingParameters): ) timing = InvestmentParameters( - fixed_lifetime=10, + lifetime=10, effects_of_investment_per_size={ 'cost': learning_costs # €/kW depends on investment year }, @@ -1032,7 +1032,7 @@ class InvestmentParameters(SizingParameters): ```python timing = InvestmentParameters( - fixed_lifetime=15, + lifetime=15, allow_investment=xr.DataArray( [1, 1, 1, 0, 0], # Only allow investment in first 3 periods coords=[('period', [2020, 2025, 2030, 2035, 2040])], @@ -1049,7 +1049,7 @@ class InvestmentParameters(SizingParameters): def __init__( self, - fixed_lifetime: Scalar, + lifetime: Scalar, allow_investment: InvestmentPeriodDataBool = True, force_investment: InvestmentPeriodDataBool = False, effects_of_investment: dict[str, xr.DataArray] | None = None, @@ -1057,10 +1057,10 @@ def __init__( previous_size: PeriodicDataUser = 0, **kwargs, ): - if fixed_lifetime is None: - raise ValueError('InvestmentParameters requires fixed_lifetime to be specified.') + if lifetime is None: + raise ValueError('InvestmentParameters requires lifetime to be specified.') - self.fixed_lifetime = fixed_lifetime + self.lifetime = lifetime self.allow_investment = allow_investment self.force_investment = force_investment self.previous_size = previous_size @@ -1111,9 +1111,9 @@ def _plausibility_checks(self, flow_system: FlowSystem) -> None: if len(_periods) > 1: # Warn if investment in late periods would extend beyond model horizon max_horizon = _periods[-1] - _periods[0] - if self.fixed_lifetime > max_horizon: + if self.lifetime > max_horizon: logger.warning( - f'Fixed lifetime ({self.fixed_lifetime}) if Investment exceeds model horizon ({max_horizon}). ' + f'Fixed lifetime ({self.lifetime}) if Investment exceeds model horizon ({max_horizon}). ' ) @@ -1138,7 +1138,7 @@ def __init__( allow_decommissioning: YearOfInvestmentDataBool = True, force_investment: YearOfInvestmentDataBool = False, # TODO: Allow to simply pass the year force_decommissioning: YearOfInvestmentDataBool = False, # TODO: Allow to simply pass the year - fixed_lifetime: Scalar | None = None, + lifetime: Scalar | None = None, minimum_lifetime: Scalar | None = None, maximum_lifetime: Scalar | None = None, minimum_size: YearOfInvestmentData | None = None, @@ -1163,7 +1163,7 @@ def __init__( allow_decommissioning: Allow decommissioning in a certain year. By default, allow it in all years. force_investment: Force the investment to occur in a certain year. force_decommissioning: Force the decommissioning to occur in a certain year. - fixed_lifetime: Fix the lifetime of an investment (duration between investment and decommissioning). + lifetime: Fix the lifetime of an investment (duration between investment and decommissioning). minimum_size: Minimum possible size of the investment. Can depend on the year of investment. maximum_size: Maximum possible size of the investment. Can depend on the year of investment. fixed_size: Fix the size of the investment. Can depend on the year of investment. Can still be 0 if not forced. @@ -1195,7 +1195,7 @@ def __init__( self.maximum_lifetime = maximum_lifetime self.minimum_lifetime = minimum_lifetime - self.fixed_lifetime = fixed_lifetime + self.lifetime = lifetime self.previous_lifetime = previous_lifetime self.fix_effects: PeriodicEffectsUser = fix_effects if fix_effects is not None else {} @@ -1238,14 +1238,14 @@ def _plausibility_checks(self, flow_system): # TODO: Might be only a warning raise ValueError('previous_lifetime can only be used if force_investment is True.') - if self.minimum_or_fixed_lifetime is not None and self.maximum_or_fixed_lifetime is not None: + if self.minimum_or_lifetime is not None and self.maximum_or_lifetime is not None: years = flow_system.years.values infeasible_years = [] for i, inv_year in enumerate(years[:-1]): # Exclude last year future_years = years[i + 1 :] # All years after investment - min_decomm = self.minimum_or_fixed_lifetime + inv_year - max_decomm = self.maximum_or_fixed_lifetime + inv_year + min_decomm = self.minimum_or_lifetime + inv_year + max_decomm = self.maximum_or_lifetime + inv_year if max_decomm >= years[-1]: continue @@ -1261,13 +1261,13 @@ def _plausibility_checks(self, flow_system): f' Investment years with no feasible decommissioning: {[int(year) for year in infeasible_years]}\n' f' Consider relaxing the lifetime constraints or including more years into your model.\n' f' Lifetime:\n' - f' min={self.minimum_or_fixed_lifetime}\n' - f' max={self.maximum_or_fixed_lifetime}\n' + f' min={self.minimum_or_lifetime}\n' + f' max={self.maximum_or_lifetime}\n' f' Model years: {list(flow_system.years)}\n' ) specify_timing = ( - (self.fixed_lifetime is not None) + (self.lifetime is not None) + bool((self.force_investment.sum('year') > 1).any()) + bool((self.force_decommissioning.sum('year') > 1).any()) ) @@ -1299,9 +1299,7 @@ def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None self.minimum_lifetime = flow_system.fit_to_model_coords( f'{name_prefix}|minimum_lifetime', self.minimum_lifetime, dims=['scenario'] ) - self.fixed_lifetime = flow_system.fit_to_model_coords( - f'{name_prefix}|fixed_lifetime', self.fixed_lifetime, dims=['scenario'] - ) + self.lifetime = flow_system.fit_to_model_coords(f'{name_prefix}|lifetime', self.lifetime, dims=['scenario']) self.force_investment = flow_system.fit_to_model_coords( f'{name_prefix}|force_investment', self.force_investment, dims=['year', 'scenario'] @@ -1377,14 +1375,14 @@ def is_fixed_size(self) -> bool: return self.fixed_size is not None @property - def minimum_or_fixed_lifetime(self) -> PeriodicDataUser: + def minimum_or_lifetime(self) -> PeriodicDataUser: """Get the effective minimum lifetime (fixed lifetime takes precedence).""" - return self.fixed_lifetime if self.fixed_lifetime is not None else self.minimum_lifetime + return self.lifetime if self.lifetime is not None else self.minimum_lifetime @property - def maximum_or_fixed_lifetime(self) -> PeriodicDataUser: + def maximum_or_lifetime(self) -> PeriodicDataUser: """Get the effective maximum lifetime (fixed lifetime takes precedence).""" - return self.fixed_lifetime if self.fixed_lifetime is not None else self.maximum_lifetime + return self.lifetime if self.lifetime is not None else self.maximum_lifetime @register_class_for_io From 52c1f8a845cbf398db50ed79b032ebbea179361d Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 2 Nov 2025 15:23:31 +0100 Subject: [PATCH 425/448] Fix type hints --- flixopt/interface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flixopt/interface.py b/flixopt/interface.py index eb8083b7a..8dce3c26c 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -1052,8 +1052,8 @@ def __init__( lifetime: Scalar, allow_investment: InvestmentPeriodDataBool = True, force_investment: InvestmentPeriodDataBool = False, - effects_of_investment: dict[str, xr.DataArray] | None = None, - effects_of_investment_per_size: dict[str, xr.DataArray] | None = None, + effects_of_investment: PeriodicEffectsUser | None = None, + effects_of_investment_per_size: PeriodicEffectsUser | None = None, previous_size: PeriodicDataUser = 0, **kwargs, ): From ae14bafe16aeb158aacaad4fb43d674d02fe1e95 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 2 Nov 2025 16:31:05 +0100 Subject: [PATCH 426/448] Small bugfixes --- flixopt/elements.py | 2 +- flixopt/features.py | 4 ++-- flixopt/interface.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/flixopt/elements.py b/flixopt/elements.py index 99aea75e1..df2f80876 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -743,7 +743,7 @@ def absolute_flow_rate_bounds(self) -> tuple[TemporalData, TemporalData]: if not self.with_investment: # Basic case without investment and without OnOff lb = lb_relative * self.element.size - elif self.with_investment and self.element.size.mandatory: + elif self.with_investment and self.element.size.mandatory.all(): # With mandatory Investment lb = lb_relative * self.element.size.minimum_or_fixed_size diff --git a/flixopt/features.py b/flixopt/features.py index cdc2a74b2..933d20f16 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -49,14 +49,14 @@ def _create_sizing_variables_and_constraints( coords=self._model.get_coords(dims), ) - if not mandatory.all(): + if mandatory.any(): self.add_variables( binary=True, coords=self._model.get_coords(dims), short_name='available', ) self.add_constraints( - self.available == mandatory.astype(int), + self.available.where(mandatory) == 1, short_name='mandatory', ) BoundingPatterns.bounds_with_state( diff --git a/flixopt/interface.py b/flixopt/interface.py index 8dce3c26c..dc4d9a2be 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -1086,11 +1086,11 @@ def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None self.previous_size = flow_system.fit_to_model_coords( f'{name_prefix}|previous_size', self.previous_size, dims=['period', 'scenario'] ) - self.effects_of_investment = flow_system.fit_to_model_coords( + self.effects_of_investment = flow_system.fit_effects_to_model_coords( f'{name_prefix}|effects_of_investment', self.effects_of_investment, dims=['period', 'scenario'] ) # TODO: investment period dim - self.effects_of_investment_per_size = flow_system.fit_to_model_coords( + self.effects_of_investment_per_size = flow_system.fit_effects_to_model_coords( f'{name_prefix}|effects_of_investment_per_size', self.effects_of_investment_per_size, dims=['period', 'scenario'], From 3db741d66ebdec95afcdffaec29d5df99891a4d8 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 2 Nov 2025 16:31:11 +0100 Subject: [PATCH 427/448] Small bugfixes --- flixopt/features.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index 933d20f16..4d89eabea 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -190,8 +190,6 @@ def _create_variables_and_constraints(self): self._track_investment_and_decomissioning_size() self._apply_investment_period_constraints() - self._add_effects() - def _track_investment_and_decomissioning_period(self): """Track investment and decomissioning period absed on binary state variable.""" self.add_variables( From 24618d96deecf0024765d0b611e6a02c802ab5f0 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 2 Nov 2025 16:55:11 +0100 Subject: [PATCH 428/448] Fix inherritance issues --- flixopt/components.py | 23 +++--- flixopt/elements.py | 9 +-- flixopt/interface.py | 159 ++++++++++++++++++++++++------------------ 3 files changed, 111 insertions(+), 80 deletions(-) diff --git a/flixopt/components.py b/flixopt/components.py index e6d4aeb98..15a9de7c4 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -470,7 +470,7 @@ def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None self.relative_maximum_final_charge_state, dims=['period', 'scenario'], ) - if isinstance(self.capacity_in_flow_hours, SizingParameters): + if isinstance(self.capacity_in_flow_hours, (SizingParameters, InvestmentParameters)): self.capacity_in_flow_hours.transform_data(flow_system, f'{prefix}|SizingParameters') else: self.capacity_in_flow_hours = flow_system.fit_to_model_coords( @@ -492,7 +492,7 @@ def _plausibility_checks(self) -> None: raise PlausibilityError(f'initial_charge_state has undefined value: {self.initial_charge_state}') # Use new SizingParameters methods to get capacity bounds - if isinstance(self.capacity_in_flow_hours, SizingParameters): + if isinstance(self.capacity_in_flow_hours, (SizingParameters, InvestmentParameters)): minimum_capacity = self.capacity_in_flow_hours.minimum_or_fixed_size maximum_capacity = self.capacity_in_flow_hours.maximum_or_fixed_size else: @@ -517,8 +517,8 @@ def _plausibility_checks(self) -> None: ) if self.balanced: - if not isinstance(self.charging.size, SizingParameters) or not isinstance( - self.discharging.size, SizingParameters + if not isinstance(self.charging.size, (SizingParameters, InvestmentParameters)) or not isinstance( + self.discharging.size, (SizingParameters, InvestmentParameters) ): raise PlausibilityError( f'Balancing charging and discharging Flows in {self.label_full} is only possible with Investments.' @@ -704,7 +704,9 @@ def _plausibility_checks(self): if self.balanced: if self.in2 is None: raise ValueError('Balanced Transmission needs SizingParameters in both in-Flows') - if not isinstance(self.in1.size, SizingParameters) or not isinstance(self.in2.size, SizingParameters): + if not isinstance(self.in1.size, (SizingParameters, InvestmentParameters)) or not isinstance( + self.in2.size, (SizingParameters, InvestmentParameters) + ): raise ValueError('Balanced Transmission needs SizingParameters in both in-Flows') if (self.in1.size.minimum_or_fixed_size > self.in2.size.maximum_or_fixed_size).any() or ( self.in1.size.maximum_or_fixed_size < self.in2.size.minimum_or_fixed_size @@ -863,7 +865,11 @@ def _do_modeling(self): short_name='charge_state', ) - if isinstance(self.element.capacity_in_flow_hours, SizingParameters): + # Check InvestmentParameters first (best practice to check more specific types first) + if isinstance(self.element.capacity_in_flow_hours, InvestmentParameters): + raise NotImplementedError + + elif isinstance(self.element.capacity_in_flow_hours, SizingParameters): self.add_submodels( SizingModel( model=self._model, @@ -881,9 +887,6 @@ def _do_modeling(self): relative_bounds=self._relative_charge_state_bounds, ) - elif isinstance(self.element.capacity_in_flow_hours, InvestmentParameters): - raise NotImplementedError - # Initial charge state self._initial_and_final_charge_state() @@ -921,7 +924,7 @@ def _initial_and_final_charge_state(self): @property def _absolute_charge_state_bounds(self) -> tuple[TemporalData, TemporalData]: relative_lower_bound, relative_upper_bound = self._relative_charge_state_bounds - if not isinstance(self.element.capacity_in_flow_hours, SizingParameters): + if not isinstance(self.element.capacity_in_flow_hours, (SizingParameters, InvestmentParameters)): return ( relative_lower_bound * self.element.capacity_in_flow_hours, relative_upper_bound * self.element.capacity_in_flow_hours, diff --git a/flixopt/elements.py b/flixopt/elements.py index df2f80876..e9916b126 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -599,9 +599,10 @@ def _create_on_off_model(self): ) def _create_investment_model(self): - if isinstance(self.element.size, SizingParameters): + # Check InvestmentParameters first (best practice to check more specific types first) + if isinstance(self.element.size, InvestmentParameters): self.add_submodels( - SizingModel( + InvestmentModel( model=self._model, label_of_element=self.label_of_element, parameters=self.element.size, @@ -609,9 +610,9 @@ def _create_investment_model(self): ), 'investment', ) - elif isinstance(self.element.size, InvestmentParameters): + elif isinstance(self.element.size, SizingParameters): self.add_submodels( - InvestmentModel( + SizingModel( model=self._model, label_of_element=self.label_of_element, parameters=self.element.size, diff --git a/flixopt/interface.py b/flixopt/interface.py index dc4d9a2be..b8e66c2a9 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -670,7 +670,64 @@ def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None class _SizeParameters(Interface): - pass + """Base class for sizing and investment parameters.""" + + def __init__( + self, + fixed_size: PeriodicDataUser | None = None, + minimum_size: PeriodicDataUser | None = None, + maximum_size: PeriodicDataUser | None = None, + mandatory: bool | xr.DataArray = False, + effects_of_size: PeriodicEffectsUser | None = None, + effects_per_size: PeriodicEffectsUser | None = None, + piecewise_effects_per_size: PiecewiseEffects | None = None, + ): + self.effects_of_size: PeriodicEffectsUser = effects_of_size if effects_of_size is not None else {} + self.fixed_size = fixed_size + self.mandatory = mandatory + self.effects_per_size: PeriodicEffectsUser = effects_per_size if effects_per_size is not None else {} + self.piecewise_effects_per_size = piecewise_effects_per_size + self.minimum_size = minimum_size if minimum_size is not None else CONFIG.Modeling.epsilon + self.maximum_size = maximum_size if maximum_size is not None else CONFIG.Modeling.big # default maximum + + def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None: + self.effects_of_size = flow_system.fit_effects_to_model_coords( + label_prefix=name_prefix, + effect_values=self.effects_of_size, + label_suffix='effects_of_size', + dims=['period', 'scenario'], + ) + self.effects_per_size = flow_system.fit_effects_to_model_coords( + label_prefix=name_prefix, + effect_values=self.effects_per_size, + label_suffix='effects_per_size', + dims=['period', 'scenario'], + ) + + if self.piecewise_effects_per_size is not None: + self.piecewise_effects_per_size.has_time_dim = False + self.piecewise_effects_per_size.transform_data(flow_system, f'{name_prefix}|PiecewiseEffects') + + self.minimum_size = flow_system.fit_to_model_coords( + f'{name_prefix}|minimum_size', self.minimum_size, dims=['period', 'scenario'] + ) + self.maximum_size = flow_system.fit_to_model_coords( + f'{name_prefix}|maximum_size', self.maximum_size, dims=['period', 'scenario'] + ) + self.fixed_size = flow_system.fit_to_model_coords( + f'{name_prefix}|fixed_size', self.fixed_size, dims=['period', 'scenario'] + ) + self.mandatory = flow_system.fit_to_model_coords( + f'{name_prefix}|mandatory', self.mandatory, dims=['period', 'scenario'] + ) + + @property + def minimum_or_fixed_size(self) -> PeriodicData: + return self.fixed_size if self.fixed_size is not None else self.minimum_size + + @property + def maximum_or_fixed_size(self) -> PeriodicData: + return self.fixed_size if self.fixed_size is not None else self.maximum_size @register_class_for_io @@ -867,62 +924,8 @@ class SizingParameters(_SizeParameters): """ - def __init__( - self, - fixed_size: PeriodicDataUser | None = None, - minimum_size: PeriodicDataUser | None = None, - maximum_size: PeriodicDataUser | None = None, - mandatory: bool | xr.DataArray = False, - effects_of_size: PeriodicEffectsUser | None = None, - effects_per_size: PeriodicEffectsUser | None = None, - piecewise_effects_per_size: PiecewiseEffects | None = None, - ): - self.effects_of_size: PeriodicEffectsUser = effects_of_size if effects_of_size is not None else {} - self.fixed_size = fixed_size - self.mandatory = mandatory - self.effects_per_size: PeriodicEffectsUser = effects_per_size if effects_per_size is not None else {} - self.piecewise_effects_per_size = piecewise_effects_per_size - self.minimum_size = minimum_size if minimum_size is not None else CONFIG.Modeling.epsilon - self.maximum_size = maximum_size if maximum_size is not None else CONFIG.Modeling.big # default maximum - - def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None: - self.effects_of_size = flow_system.fit_effects_to_model_coords( - label_prefix=name_prefix, - effect_values=self.effects_of_size, - label_suffix='effects_of_size', - dims=['period', 'scenario'], - ) - self.effects_per_size = flow_system.fit_effects_to_model_coords( - label_prefix=name_prefix, - effect_values=self.effects_per_size, - label_suffix='effects_per_size', - dims=['period', 'scenario'], - ) - - if self.piecewise_effects_per_size is not None: - self.piecewise_effects_per_size.has_time_dim = False - self.piecewise_effects_per_size.transform_data(flow_system, f'{name_prefix}|PiecewiseEffects') - - self.minimum_size = flow_system.fit_to_model_coords( - f'{name_prefix}|minimum_size', self.minimum_size, dims=['period', 'scenario'] - ) - self.maximum_size = flow_system.fit_to_model_coords( - f'{name_prefix}|maximum_size', self.maximum_size, dims=['period', 'scenario'] - ) - self.fixed_size = flow_system.fit_to_model_coords( - f'{name_prefix}|fixed_size', self.fixed_size, dims=['period', 'scenario'] - ) - self.mandatory = flow_system.fit_to_model_coords( - f'{name_prefix}|mandatory', self.mandatory, dims=['period', 'scenario'] - ) - - @property - def minimum_or_fixed_size(self) -> PeriodicData: - return self.fixed_size if self.fixed_size is not None else self.minimum_size - - @property - def maximum_or_fixed_size(self) -> PeriodicData: - return self.fixed_size if self.fixed_size is not None else self.maximum_size + # SizingParameters now inherits all functionality from _SizeParameters + # No additional implementation needed @register_class_for_io @@ -943,17 +946,15 @@ def __init__(self, **kwargs): @register_class_for_io -class InvestmentParameters(SizingParameters): +class InvestmentParameters(_SizeParameters): """Define investment timing parameters with fixed lifetime. This class models WHEN to invest with a fixed lifetime duration. - It must be combined with SizingParameters to fully model investment decisions. - - InvestmentParameters focuses on the TIMING aspect (when to invest), - while SizingParameters handles the CAPACITY aspect (how much to install). + It includes all sizing parameters (capacity bounds, effects) plus timing controls. - The model optimizes when to make an investment that will last for a fixed duration. - During the investment's lifetime, the capacity (from SizingParameters) is active. + InvestmentParameters combines both TIMING (when to invest) and CAPACITY (how much) + aspects in a single class, optimizing when to make an investment that will last + for a fixed duration. Investment Timing Features: **Single Investment Decision**: Decide which period to invest in (at most once) @@ -978,6 +979,14 @@ class InvestmentParameters(SizingParameters): - Period-specific subsidies or regulations effects_of_investment_per_size: Size-dependent effects that also depend on investment period. Dict mapping effect names to xr.DataArray with dimensions [period, scenario, investment_period]. + previous_size: Size of existing capacity from previous periods. Default: 0. + fixed_size: Creates binary decision at this exact size. None allows continuous sizing. + minimum_size: Lower bound for continuous sizing. Default: CONFIG.Modeling.epsilon. + maximum_size: Upper bound for continuous sizing. Default: CONFIG.Modeling.big. + mandatory: Controls whether investment is required. When True, forces investment. + effects_of_size: Fixed costs if investment is made, regardless of size. + effects_per_size: Variable costs proportional to size (per-unit costs). + piecewise_effects_per_size: Non-linear costs using PiecewiseEffects. Examples: Basic investment timing: @@ -1055,11 +1064,19 @@ def __init__( effects_of_investment: PeriodicEffectsUser | None = None, effects_of_investment_per_size: PeriodicEffectsUser | None = None, previous_size: PeriodicDataUser = 0, - **kwargs, + # Sizing parameters (inherited from _SizeParameters) + fixed_size: PeriodicDataUser | None = None, + minimum_size: PeriodicDataUser | None = None, + maximum_size: PeriodicDataUser | None = None, + mandatory: bool | xr.DataArray = False, + effects_of_size: PeriodicEffectsUser | None = None, + effects_per_size: PeriodicEffectsUser | None = None, + piecewise_effects_per_size: PiecewiseEffects | None = None, ): if lifetime is None: raise ValueError('InvestmentParameters requires lifetime to be specified.') + # Initialize investment-specific attributes self.lifetime = lifetime self.allow_investment = allow_investment self.force_investment = force_investment @@ -1071,7 +1088,17 @@ def __init__( self.effects_of_investment_per_size: dict[str, xr.DataArray] = ( effects_of_investment_per_size if effects_of_investment_per_size is not None else {} ) - super().__init__(**kwargs) + + # Initialize base sizing parameters + super().__init__( + fixed_size=fixed_size, + minimum_size=minimum_size, + maximum_size=maximum_size, + mandatory=mandatory, + effects_of_size=effects_of_size, + effects_per_size=effects_per_size, + piecewise_effects_per_size=piecewise_effects_per_size, + ) def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None: """Transform user data into internal model coordinates.""" From f8ac1eb1b8c571d4b4f42dc64397195943d33900 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 2 Nov 2025 16:58:23 +0100 Subject: [PATCH 429/448] Fix missing availlable variable --- flixopt/features.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index 4d89eabea..c5427a8d7 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -37,6 +37,7 @@ def _create_sizing_variables_and_constraints( size_max: PeriodicData, mandatory: PeriodicData, dims: list[FlowSystemDimensions], + force_available: bool = False, ): """Create timing variables and constraints.""" if not np.issubdtype(mandatory.dtype, np.bool_): @@ -49,7 +50,7 @@ def _create_sizing_variables_and_constraints( coords=self._model.get_coords(dims), ) - if mandatory.any(): + if force_available or mandatory.any(): self.add_variables( binary=True, coords=self._model.get_coords(dims), @@ -184,6 +185,7 @@ def _create_variables_and_constraints(self): size_max=self.parameters.maximum_or_fixed_size, mandatory=self.parameters.mandatory, dims=['period', 'scenario'], + force_available=True, ) self._track_investment_and_decomissioning_period() @@ -214,10 +216,10 @@ def _track_investment_and_decomissioning_period(self): BoundingPatterns.state_transition_bounds( self, - state_variable=self.is_invested, + state_variable=self.available, switch_on=self.investment_occurs, switch_off=self.decommissioning_occurs, - name=self.is_invested.name, + name=self.available.name, previous_state=0, coord='period', ) @@ -315,7 +317,7 @@ def decommissioning_occurs(self) -> linopy.Variable: @property def is_invested(self) -> linopy.Variable: """Binary variable indicating which periods have active investment""" - return self._variables['is_invested'] + return self._variables['ava'] @property def size_decrease(self) -> linopy.Variable: From 7f28d62f68f989f33503c34d8aaac33a85437523 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 3 Nov 2025 09:14:28 +0100 Subject: [PATCH 430/448] Track lifetime --- flixopt/features.py | 26 +++++++++++++++++++++++++- flixopt/interface.py | 10 +++++----- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index c5427a8d7..fb4e4d98b 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -190,6 +190,7 @@ def _create_variables_and_constraints(self): self._track_investment_and_decomissioning_period() self._track_investment_and_decomissioning_size() + self._track_lifetime() self._apply_investment_period_constraints() def _track_investment_and_decomissioning_period(self): @@ -246,10 +247,33 @@ def _track_investment_and_decomissioning_size(self): decrease_binary=self.decommissioning_occurs, name=f'{self.label_of_element}|size|changes', max_change=self.parameters.maximum_or_fixed_size, - previous_level=self.parameters.previous_size, + previous_level=0 + if self.parameters.previous_lifetime is None + else self.size.isel(period=0), # TODO: What value? coord='period', ) + def _track_lifetime(self): + for i, period in enumerate(self._model.flow_system.periods.values()): + decommissioning_period = ( + period + self.parameters.lifetime - self.parameters.previous_lifetime if i == 0 else 0 + ) + available_decommissioning_period = self._model.flow_system.periods.get_indexer( + [decommissioning_period], + method='bfill', + )[0] + if decommissioning_period != available_decommissioning_period: + logger.warning( + f'For an Investment in period {period}, the decommissioning period would be {decommissioning_period}.' + f'As this period is not part of the Model horizon, the lifetime will effectively be extended until the next period (+{available_decommissioning_period - decommissioning_period}).' + ) + + self.add_constraints( + self.size_increase.sel(period=period) + == self.size_decrease.sel(period=available_decommissioning_period), + short_name='size|lifetime', + ) + def _apply_investment_period_constraints(self): # Constraint: Apply allow_investment restrictions if (self.parameters.allow_investment == 0).any(): diff --git a/flixopt/interface.py b/flixopt/interface.py index b8e66c2a9..2da9a3aaf 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -1058,12 +1058,12 @@ class InvestmentParameters(_SizeParameters): def __init__( self, - lifetime: Scalar, + lifetime: InvestmentPeriodData, allow_investment: InvestmentPeriodDataBool = True, force_investment: InvestmentPeriodDataBool = False, effects_of_investment: PeriodicEffectsUser | None = None, effects_of_investment_per_size: PeriodicEffectsUser | None = None, - previous_size: PeriodicDataUser = 0, + previous_lifetime: int = 0, # Sizing parameters (inherited from _SizeParameters) fixed_size: PeriodicDataUser | None = None, minimum_size: PeriodicDataUser | None = None, @@ -1080,7 +1080,7 @@ def __init__( self.lifetime = lifetime self.allow_investment = allow_investment self.force_investment = force_investment - self.previous_size = previous_size + self.previous_lifetime = previous_lifetime self.effects_of_investment: dict[str, xr.DataArray] = ( effects_of_investment if effects_of_investment is not None else {} @@ -1110,8 +1110,8 @@ def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None self.force_investment = flow_system.fit_to_model_coords( f'{name_prefix}|force_investment', self.force_investment, dims=['period', 'scenario'] ) - self.previous_size = flow_system.fit_to_model_coords( - f'{name_prefix}|previous_size', self.previous_size, dims=['period', 'scenario'] + self.previous_lifetime = flow_system.fit_to_model_coords( + f'{name_prefix}|previous_lifetime', self.previous_lifetime, dims=['scenario'] ) self.effects_of_investment = flow_system.fit_effects_to_model_coords( f'{name_prefix}|effects_of_investment', self.effects_of_investment, dims=['period', 'scenario'] From 190ae492f02993278027308d01a0d76af158e433 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 3 Nov 2025 09:32:23 +0100 Subject: [PATCH 431/448] Temp --- flixopt/features.py | 29 ++++++++++------------------- flixopt/flow_system.py | 20 ++++++++++++++++++++ 2 files changed, 30 insertions(+), 19 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index fb4e4d98b..f4bab1729 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -254,25 +254,16 @@ def _track_investment_and_decomissioning_size(self): ) def _track_lifetime(self): - for i, period in enumerate(self._model.flow_system.periods.values()): - decommissioning_period = ( - period + self.parameters.lifetime - self.parameters.previous_lifetime if i == 0 else 0 - ) - available_decommissioning_period = self._model.flow_system.periods.get_indexer( - [decommissioning_period], - method='bfill', - )[0] - if decommissioning_period != available_decommissioning_period: - logger.warning( - f'For an Investment in period {period}, the decommissioning period would be {decommissioning_period}.' - f'As this period is not part of the Model horizon, the lifetime will effectively be extended until the next period (+{available_decommissioning_period - decommissioning_period}).' - ) - - self.add_constraints( - self.size_increase.sel(period=period) - == self.size_decrease.sel(period=available_decommissioning_period), - short_name='size|lifetime', - ) + ModelingPrimitives.consecutive_duration_tracking( + self, + state_variable=self.available, + minimum_duration=self.parameters.lifetime, + maximum_duration=self.parameters.lifetime, + short_name='lifetime', + duration_dim='period', + duration_per_step=self._model.flow_system.periods_per_period, + previous_duration=self.parameters.previous_lifetime, + ) def _apply_investment_period_constraints(self): # Constraint: Apply allow_investment restrictions diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 9bc7f7f99..dba2291a1 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -165,13 +165,17 @@ def __init__( self.periods = None if periods is None else self._validate_periods(periods) self.scenarios = None if scenarios is None else self._validate_scenarios(scenarios) + self.periods_extra = self._calculate_periods_extra(self.periods) + self.weights = weights hours_per_timestep = self.calculate_hours_per_timestep(self.timesteps_extra) + periods_per_period = self.calculate_periods_per_period(self.periods_extra) self.hours_of_last_timestep = hours_per_timestep[-1].item() self.hours_per_timestep = self.fit_to_model_coords('hours_per_timestep', hours_per_timestep) + self.periods_per_period = self.fit_to_model_coords('periods_per_period', periods_per_period) # Element collections self.components: ElementContainer[Component] = ElementContainer(element_type_name='components') @@ -252,6 +256,14 @@ def _create_timesteps_with_extra( last_date = pd.DatetimeIndex([timesteps[-1] + pd.Timedelta(hours=hours_of_last_timestep)], name='time') return pd.DatetimeIndex(timesteps.append(last_date), name='time') + @staticmethod + def _calculate_periods_extra(periods: pd.Index, periods_of_last_period: int | None) -> pd.Index: + """Create periods with an extra period at the end.""" + if periods_of_last_period is None: + periods_of_last_period = periods[-1] - periods[-2] + + return pd.Index(periods.append(periods[-1] + periods_of_last_period), name='period') + @staticmethod def calculate_hours_per_timestep(timesteps_extra: pd.DatetimeIndex) -> xr.DataArray: """Calculate duration of each timestep as a 1D DataArray.""" @@ -260,6 +272,14 @@ def calculate_hours_per_timestep(timesteps_extra: pd.DatetimeIndex) -> xr.DataAr hours_per_step, coords={'time': timesteps_extra[:-1]}, dims='time', name='hours_per_timestep' ) + @staticmethod + def calculate_periods_per_period(periods: pd.Index) -> xr.DataArray: + """Calculate duration of each period as a 1D DataArray.""" + periods_per_period = np.diff(periods) + return xr.DataArray( + periods_per_period, coords={'period': periods[:-1]}, dims='period', name='periods_per_period' + ) + @staticmethod def _calculate_hours_of_previous_timesteps( timesteps: pd.DatetimeIndex, hours_of_previous_timesteps: float | np.ndarray | None From e9b5287ebdd03fd0eb71899085bc83644fb89038 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 3 Nov 2025 11:43:05 +0100 Subject: [PATCH 432/448] Temp --- flixopt/flow_system.py | 8 +++++--- flixopt/interface.py | 3 +++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index dba2291a1..8c6420d0e 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -165,7 +165,7 @@ def __init__( self.periods = None if periods is None else self._validate_periods(periods) self.scenarios = None if scenarios is None else self._validate_scenarios(scenarios) - self.periods_extra = self._calculate_periods_extra(self.periods) + self.periods_extra = self._calculate_periods_extra(self.periods, None) self.weights = weights @@ -175,7 +175,9 @@ def __init__( self.hours_of_last_timestep = hours_per_timestep[-1].item() self.hours_per_timestep = self.fit_to_model_coords('hours_per_timestep', hours_per_timestep) - self.periods_per_period = self.fit_to_model_coords('periods_per_period', periods_per_period) + self.periods_per_period = self.fit_to_model_coords( + 'periods_per_period', periods_per_period, dims=['period', 'scenario'] + ) # Element collections self.components: ElementContainer[Component] = ElementContainer(element_type_name='components') @@ -262,7 +264,7 @@ def _calculate_periods_extra(periods: pd.Index, periods_of_last_period: int | No if periods_of_last_period is None: periods_of_last_period = periods[-1] - periods[-2] - return pd.Index(periods.append(periods[-1] + periods_of_last_period), name='period') + return pd.Index(periods.append(pd.Index([periods[-1] + periods_of_last_period])), name='period') @staticmethod def calculate_hours_per_timestep(timesteps_extra: pd.DatetimeIndex) -> xr.DataArray: diff --git a/flixopt/interface.py b/flixopt/interface.py index 2da9a3aaf..b837badc6 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -1113,6 +1113,9 @@ def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None self.previous_lifetime = flow_system.fit_to_model_coords( f'{name_prefix}|previous_lifetime', self.previous_lifetime, dims=['scenario'] ) + self.lifetime = flow_system.fit_to_model_coords( + f'{name_prefix}|lifetime', self.lifetime, dims=['period', 'scenario'] + ) self.effects_of_investment = flow_system.fit_effects_to_model_coords( f'{name_prefix}|effects_of_investment', self.effects_of_investment, dims=['period', 'scenario'] ) # TODO: investment period dim From 45c3fadc58e80cbc2f99e5873c9368ecfc9236ad Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 3 Nov 2025 11:43:25 +0100 Subject: [PATCH 433/448] Revert "Temp" This reverts commit e9b5287ebdd03fd0eb71899085bc83644fb89038. --- flixopt/flow_system.py | 8 +++----- flixopt/interface.py | 3 --- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 8c6420d0e..dba2291a1 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -165,7 +165,7 @@ def __init__( self.periods = None if periods is None else self._validate_periods(periods) self.scenarios = None if scenarios is None else self._validate_scenarios(scenarios) - self.periods_extra = self._calculate_periods_extra(self.periods, None) + self.periods_extra = self._calculate_periods_extra(self.periods) self.weights = weights @@ -175,9 +175,7 @@ def __init__( self.hours_of_last_timestep = hours_per_timestep[-1].item() self.hours_per_timestep = self.fit_to_model_coords('hours_per_timestep', hours_per_timestep) - self.periods_per_period = self.fit_to_model_coords( - 'periods_per_period', periods_per_period, dims=['period', 'scenario'] - ) + self.periods_per_period = self.fit_to_model_coords('periods_per_period', periods_per_period) # Element collections self.components: ElementContainer[Component] = ElementContainer(element_type_name='components') @@ -264,7 +262,7 @@ def _calculate_periods_extra(periods: pd.Index, periods_of_last_period: int | No if periods_of_last_period is None: periods_of_last_period = periods[-1] - periods[-2] - return pd.Index(periods.append(pd.Index([periods[-1] + periods_of_last_period])), name='period') + return pd.Index(periods.append(periods[-1] + periods_of_last_period), name='period') @staticmethod def calculate_hours_per_timestep(timesteps_extra: pd.DatetimeIndex) -> xr.DataArray: diff --git a/flixopt/interface.py b/flixopt/interface.py index b837badc6..2da9a3aaf 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -1113,9 +1113,6 @@ def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None self.previous_lifetime = flow_system.fit_to_model_coords( f'{name_prefix}|previous_lifetime', self.previous_lifetime, dims=['scenario'] ) - self.lifetime = flow_system.fit_to_model_coords( - f'{name_prefix}|lifetime', self.lifetime, dims=['period', 'scenario'] - ) self.effects_of_investment = flow_system.fit_effects_to_model_coords( f'{name_prefix}|effects_of_investment', self.effects_of_investment, dims=['period', 'scenario'] ) # TODO: investment period dim From e465a62cb4a14c4830d5c4e61ee4263ffd2c7c0a Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 3 Nov 2025 11:43:25 +0100 Subject: [PATCH 434/448] Revert "Temp" This reverts commit 190ae492f02993278027308d01a0d76af158e433. --- flixopt/features.py | 29 +++++++++++++++++++---------- flixopt/flow_system.py | 20 -------------------- 2 files changed, 19 insertions(+), 30 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index f4bab1729..fb4e4d98b 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -254,16 +254,25 @@ def _track_investment_and_decomissioning_size(self): ) def _track_lifetime(self): - ModelingPrimitives.consecutive_duration_tracking( - self, - state_variable=self.available, - minimum_duration=self.parameters.lifetime, - maximum_duration=self.parameters.lifetime, - short_name='lifetime', - duration_dim='period', - duration_per_step=self._model.flow_system.periods_per_period, - previous_duration=self.parameters.previous_lifetime, - ) + for i, period in enumerate(self._model.flow_system.periods.values()): + decommissioning_period = ( + period + self.parameters.lifetime - self.parameters.previous_lifetime if i == 0 else 0 + ) + available_decommissioning_period = self._model.flow_system.periods.get_indexer( + [decommissioning_period], + method='bfill', + )[0] + if decommissioning_period != available_decommissioning_period: + logger.warning( + f'For an Investment in period {period}, the decommissioning period would be {decommissioning_period}.' + f'As this period is not part of the Model horizon, the lifetime will effectively be extended until the next period (+{available_decommissioning_period - decommissioning_period}).' + ) + + self.add_constraints( + self.size_increase.sel(period=period) + == self.size_decrease.sel(period=available_decommissioning_period), + short_name='size|lifetime', + ) def _apply_investment_period_constraints(self): # Constraint: Apply allow_investment restrictions diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index dba2291a1..9bc7f7f99 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -165,17 +165,13 @@ def __init__( self.periods = None if periods is None else self._validate_periods(periods) self.scenarios = None if scenarios is None else self._validate_scenarios(scenarios) - self.periods_extra = self._calculate_periods_extra(self.periods) - self.weights = weights hours_per_timestep = self.calculate_hours_per_timestep(self.timesteps_extra) - periods_per_period = self.calculate_periods_per_period(self.periods_extra) self.hours_of_last_timestep = hours_per_timestep[-1].item() self.hours_per_timestep = self.fit_to_model_coords('hours_per_timestep', hours_per_timestep) - self.periods_per_period = self.fit_to_model_coords('periods_per_period', periods_per_period) # Element collections self.components: ElementContainer[Component] = ElementContainer(element_type_name='components') @@ -256,14 +252,6 @@ def _create_timesteps_with_extra( last_date = pd.DatetimeIndex([timesteps[-1] + pd.Timedelta(hours=hours_of_last_timestep)], name='time') return pd.DatetimeIndex(timesteps.append(last_date), name='time') - @staticmethod - def _calculate_periods_extra(periods: pd.Index, periods_of_last_period: int | None) -> pd.Index: - """Create periods with an extra period at the end.""" - if periods_of_last_period is None: - periods_of_last_period = periods[-1] - periods[-2] - - return pd.Index(periods.append(periods[-1] + periods_of_last_period), name='period') - @staticmethod def calculate_hours_per_timestep(timesteps_extra: pd.DatetimeIndex) -> xr.DataArray: """Calculate duration of each timestep as a 1D DataArray.""" @@ -272,14 +260,6 @@ def calculate_hours_per_timestep(timesteps_extra: pd.DatetimeIndex) -> xr.DataAr hours_per_step, coords={'time': timesteps_extra[:-1]}, dims='time', name='hours_per_timestep' ) - @staticmethod - def calculate_periods_per_period(periods: pd.Index) -> xr.DataArray: - """Calculate duration of each period as a 1D DataArray.""" - periods_per_period = np.diff(periods) - return xr.DataArray( - periods_per_period, coords={'period': periods[:-1]}, dims='period', name='periods_per_period' - ) - @staticmethod def _calculate_hours_of_previous_timesteps( timesteps: pd.DatetimeIndex, hours_of_previous_timesteps: float | np.ndarray | None From 3d2e52bd13ba1174c422f94dacba3bc18ddb033b Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 3 Nov 2025 12:52:00 +0100 Subject: [PATCH 435/448] Add constraints for lifetime with warnings --- flixopt/features.py | 22 ++++++++++++---------- flixopt/interface.py | 1 + 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index fb4e4d98b..acc35f245 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -254,24 +254,26 @@ def _track_investment_and_decomissioning_size(self): ) def _track_lifetime(self): - for i, period in enumerate(self._model.flow_system.periods.values()): + periods = self._model.flow_system.fit_to_model_coords( + 'periods', self._model.flow_system.periods.values, dims=['period', 'scenario'] + ) + for i, period in enumerate(periods.values): decommissioning_period = ( - period + self.parameters.lifetime - self.parameters.previous_lifetime if i == 0 else 0 + period + self.parameters.lifetime - (self.parameters.previous_lifetime if i == 0 else 0) ) - available_decommissioning_period = self._model.flow_system.periods.get_indexer( - [decommissioning_period], - method='bfill', - )[0] - if decommissioning_period != available_decommissioning_period: + if (decommissioning_period > self._model.flow_system.periods.values[-1]).all(): + continue + available_decommissioning_period = periods.sel(period=decommissioning_period, method='bfill') + if (decommissioning_period != available_decommissioning_period).any(): logger.warning( f'For an Investment in period {period}, the decommissioning period would be {decommissioning_period}.' f'As this period is not part of the Model horizon, the lifetime will effectively be extended until the next period (+{available_decommissioning_period - decommissioning_period}).' ) self.add_constraints( - self.size_increase.sel(period=period) - == self.size_decrease.sel(period=available_decommissioning_period), - short_name='size|lifetime', + self.investment_occurs.sel(period=period) + == self.decommissioning_occurs.sel(period=available_decommissioning_period), + short_name=f'size|lifetime{period}', ) def _apply_investment_period_constraints(self): diff --git a/flixopt/interface.py b/flixopt/interface.py index 2da9a3aaf..da490ec87 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -1113,6 +1113,7 @@ def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None self.previous_lifetime = flow_system.fit_to_model_coords( f'{name_prefix}|previous_lifetime', self.previous_lifetime, dims=['scenario'] ) + self.lifetime = flow_system.fit_to_model_coords(f'{name_prefix}|lifetime', self.lifetime, dims=['scenario']) self.effects_of_investment = flow_system.fit_effects_to_model_coords( f'{name_prefix}|effects_of_investment', self.effects_of_investment, dims=['period', 'scenario'] ) # TODO: investment period dim From c1d501e13ec1fbd966cd8130e0fb3c9dc3489004 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 3 Nov 2025 12:53:24 +0100 Subject: [PATCH 436/448] Improve warning massage --- flixopt/features.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flixopt/features.py b/flixopt/features.py index acc35f245..2b2130f32 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -267,7 +267,7 @@ def _track_lifetime(self): if (decommissioning_period != available_decommissioning_period).any(): logger.warning( f'For an Investment in period {period}, the decommissioning period would be {decommissioning_period}.' - f'As this period is not part of the Model horizon, the lifetime will effectively be extended until the next period (+{available_decommissioning_period - decommissioning_period}).' + f'As this period is not part of the Model horizon, the lifetime will be extended until the next period ({available_decommissioning_period}), which will effectively extend the lifetime by +{available_decommissioning_period - decommissioning_period} periods.' ) self.add_constraints( From 85e7cc733fa7689c90c56b600752f7b90f00382b Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 3 Nov 2025 13:27:52 +0100 Subject: [PATCH 437/448] Improve warning message --- flixopt/features.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index 2b2130f32..20b8b9f02 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -12,6 +12,7 @@ import linopy import numpy as np +from . import io as fx_io from .modeling import BoundingPatterns, ModelingPrimitives, ModelingUtilities from .structure import FlowSystemModel, Submodel @@ -266,8 +267,8 @@ def _track_lifetime(self): available_decommissioning_period = periods.sel(period=decommissioning_period, method='bfill') if (decommissioning_period != available_decommissioning_period).any(): logger.warning( - f'For an Investment in period {period}, the decommissioning period would be {decommissioning_period}.' - f'As this period is not part of the Model horizon, the lifetime will be extended until the next period ({available_decommissioning_period}), which will effectively extend the lifetime by +{available_decommissioning_period - decommissioning_period} periods.' + f'For an Investment in period {period}, the decommissioning period would be {fx_io._format_value_for_repr(decommissioning_period)}.' + f'As this period is not part of the Model horizon, the lifetime will be extended until the next period ({fx_io._format_value_for_repr(available_decommissioning_period)}), which will effectively extend the lifetime by +{fx_io._format_value_for_repr(available_decommissioning_period - decommissioning_period)} periods.' ) self.add_constraints( From 3a8bebdcf9e085318c85b92b5179cb790a5a2672 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 3 Nov 2025 14:30:16 +0100 Subject: [PATCH 438/448] Fix --- flixopt/features.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/flixopt/features.py b/flixopt/features.py index 20b8b9f02..26e3f08d6 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -258,6 +258,8 @@ def _track_lifetime(self): periods = self._model.flow_system.fit_to_model_coords( 'periods', self._model.flow_system.periods.values, dims=['period', 'scenario'] ) + used_decommissioning_periods = set() + for i, period in enumerate(periods.values): decommissioning_period = ( period + self.parameters.lifetime - (self.parameters.previous_lifetime if i == 0 else 0) @@ -265,6 +267,24 @@ def _track_lifetime(self): if (decommissioning_period > self._model.flow_system.periods.values[-1]).all(): continue available_decommissioning_period = periods.sel(period=decommissioning_period, method='bfill') + + # Check if this decommissioning period is already used by a previous investment period + decom_period_key = tuple(available_decommissioning_period.values.flatten()) + if decom_period_key in used_decommissioning_periods: + logger.warning( + f'Investment in period {fx_io._format_value_for_repr(period)} would require decommissioning in period {fx_io._format_value_for_repr(available_decommissioning_period)}, ' + f'but this decommissioning period is already linked to a previous investment period. ' + f'Investment in period {fx_io._format_value_for_repr(period)} will be disabled to avoid conflicting constraints.' + ) + # Disable investment in this period + self.add_constraints( + self.investment_occurs.sel(period=period) == 0, + short_name=f'size|no_investment_due_to_lifetime_conflict{period}', + ) + continue + + used_decommissioning_periods.add(decom_period_key) + if (decommissioning_period != available_decommissioning_period).any(): logger.warning( f'For an Investment in period {period}, the decommissioning period would be {fx_io._format_value_for_repr(decommissioning_period)}.' From e95f6513d9dd83ad5fd25beb624010b8312c7675 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 3 Nov 2025 14:37:24 +0100 Subject: [PATCH 439/448] Fix --- flixopt/features.py | 64 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 54 insertions(+), 10 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index 26e3f08d6..289bf9628 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -258,7 +258,9 @@ def _track_lifetime(self): periods = self._model.flow_system.fit_to_model_coords( 'periods', self._model.flow_system.periods.values, dims=['period', 'scenario'] ) - used_decommissioning_periods = set() + + # First pass: Calculate all mappings and group by decommissioning period + decommissioning_mapping = {} # Maps decom_period_key -> list of (investment_period, decom_period, available_decom_period, lifetime_extension) for i, period in enumerate(periods.values): decommissioning_period = ( @@ -268,14 +270,56 @@ def _track_lifetime(self): continue available_decommissioning_period = periods.sel(period=decommissioning_period, method='bfill') - # Check if this decommissioning period is already used by a previous investment period + # Calculate lifetime extension (how far the decommissioning was pushed forward) + lifetime_extension = (available_decommissioning_period - decommissioning_period).values + max_extension = float(lifetime_extension.max()) if lifetime_extension.size > 0 else 0.0 + decom_period_key = tuple(available_decommissioning_period.values.flatten()) - if decom_period_key in used_decommissioning_periods: - logger.warning( - f'Investment in period {fx_io._format_value_for_repr(period)} would require decommissioning in period {fx_io._format_value_for_repr(available_decommissioning_period)}, ' - f'but this decommissioning period is already linked to a previous investment period. ' - f'Investment in period {fx_io._format_value_for_repr(period)} will be disabled to avoid conflicting constraints.' - ) + if decom_period_key not in decommissioning_mapping: + decommissioning_mapping[decom_period_key] = [] + + decommissioning_mapping[decom_period_key].append( + { + 'investment_period': period, + 'index': i, + 'decommissioning_period': decommissioning_period, + 'available_decommissioning_period': available_decommissioning_period, + 'max_extension': max_extension, + } + ) + + # For each decommissioning period, select the investment period with minimum lifetime extension + selected_investments = set() + disabled_investments = [] + + for candidates in decommissioning_mapping.values(): + if len(candidates) == 1: + # No conflict, use this investment period + selected_investments.add(candidates[0]['investment_period']) + else: + # Conflict: Select the investment period with smallest lifetime extension + best_candidate = min(candidates, key=lambda x: x['max_extension']) + selected_investments.add(best_candidate['investment_period']) + + # Mark others as disabled + for candidate in candidates: + if candidate['investment_period'] != best_candidate['investment_period']: + disabled_investments.append(candidate) + logger.warning( + f'Investment in period {fx_io._format_value_for_repr(candidate["investment_period"])} would require decommissioning in period {fx_io._format_value_for_repr(candidate["available_decommissioning_period"])}, ' + f'but investment in period {fx_io._format_value_for_repr(best_candidate["investment_period"])} is closer to this decommissioning period (requires {best_candidate["max_extension"]:.1f} period extension vs {candidate["max_extension"]:.1f}). ' + f'Investment in period {fx_io._format_value_for_repr(candidate["investment_period"])} will be disabled to avoid conflicting constraints.' + ) + + # Second pass: Add constraints for selected investments and disable others + for i, period in enumerate(periods.values): + decommissioning_period = ( + period + self.parameters.lifetime - (self.parameters.previous_lifetime if i == 0 else 0) + ) + if (decommissioning_period > self._model.flow_system.periods.values[-1]).all(): + continue + + if period not in selected_investments: # Disable investment in this period self.add_constraints( self.investment_occurs.sel(period=period) == 0, @@ -283,11 +327,11 @@ def _track_lifetime(self): ) continue - used_decommissioning_periods.add(decom_period_key) + available_decommissioning_period = periods.sel(period=decommissioning_period, method='bfill') if (decommissioning_period != available_decommissioning_period).any(): logger.warning( - f'For an Investment in period {period}, the decommissioning period would be {fx_io._format_value_for_repr(decommissioning_period)}.' + f'For an Investment in period {period}, the decommissioning period would be {fx_io._format_value_for_repr(decommissioning_period)}. ' f'As this period is not part of the Model horizon, the lifetime will be extended until the next period ({fx_io._format_value_for_repr(available_decommissioning_period)}), which will effectively extend the lifetime by +{fx_io._format_value_for_repr(available_decommissioning_period - decommissioning_period)} periods.' ) From 2bd61d5682fdd4785fa444e25d6d62cd8cacb83f Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 3 Nov 2025 14:45:49 +0100 Subject: [PATCH 440/448] Fix --- flixopt/features.py | 106 +++++++++++++++++++++++++------------------- 1 file changed, 61 insertions(+), 45 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index 289bf9628..ab33fd3f1 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -254,64 +254,79 @@ def _track_investment_and_decomissioning_size(self): coord='period', ) - def _track_lifetime(self): - periods = self._model.flow_system.fit_to_model_coords( - 'periods', self._model.flow_system.periods.values, dims=['period', 'scenario'] - ) - - # First pass: Calculate all mappings and group by decommissioning period - decommissioning_mapping = {} # Maps decom_period_key -> list of (investment_period, decom_period, available_decom_period, lifetime_extension) - + def _select_best_investment_periods(self, periods): + """Select investment periods that don't conflict, preferring those with minimal lifetime extension.""" + # Build list of all investment -> decommissioning mappings + mappings = [] for i, period in enumerate(periods.values): decommissioning_period = ( period + self.parameters.lifetime - (self.parameters.previous_lifetime if i == 0 else 0) ) if (decommissioning_period > self._model.flow_system.periods.values[-1]).all(): continue - available_decommissioning_period = periods.sel(period=decommissioning_period, method='bfill') - # Calculate lifetime extension (how far the decommissioning was pushed forward) - lifetime_extension = (available_decommissioning_period - decommissioning_period).values - max_extension = float(lifetime_extension.max()) if lifetime_extension.size > 0 else 0.0 - - decom_period_key = tuple(available_decommissioning_period.values.flatten()) - if decom_period_key not in decommissioning_mapping: - decommissioning_mapping[decom_period_key] = [] + available_decommissioning_period = periods.sel(period=decommissioning_period, method='bfill') + extension = (available_decommissioning_period - decommissioning_period).max().item() - decommissioning_mapping[decom_period_key].append( + mappings.append( { 'investment_period': period, - 'index': i, - 'decommissioning_period': decommissioning_period, - 'available_decommissioning_period': available_decommissioning_period, - 'max_extension': max_extension, + 'decommissioning_period': available_decommissioning_period, + 'extension': extension, } ) - # For each decommissioning period, select the investment period with minimum lifetime extension - selected_investments = set() - disabled_investments = [] - - for candidates in decommissioning_mapping.values(): - if len(candidates) == 1: - # No conflict, use this investment period - selected_investments.add(candidates[0]['investment_period']) + # Find conflicts: investments with same decommissioning period + selected = set() + disabled = [] + + for i, mapping in enumerate(mappings): + # Check if any previous mapping has the same decommissioning period + conflict = None + for j in range(i): + if mappings[j]['investment_period'] in disabled: + continue + if (mappings[j]['decommissioning_period'] == mapping['decommissioning_period']).all(): + conflict = mappings[j] + break + + if conflict is None: + # No conflict, select this investment + selected.add(mapping['investment_period']) else: - # Conflict: Select the investment period with smallest lifetime extension - best_candidate = min(candidates, key=lambda x: x['max_extension']) - selected_investments.add(best_candidate['investment_period']) - - # Mark others as disabled - for candidate in candidates: - if candidate['investment_period'] != best_candidate['investment_period']: - disabled_investments.append(candidate) - logger.warning( - f'Investment in period {fx_io._format_value_for_repr(candidate["investment_period"])} would require decommissioning in period {fx_io._format_value_for_repr(candidate["available_decommissioning_period"])}, ' - f'but investment in period {fx_io._format_value_for_repr(best_candidate["investment_period"])} is closer to this decommissioning period (requires {best_candidate["max_extension"]:.1f} period extension vs {candidate["max_extension"]:.1f}). ' - f'Investment in period {fx_io._format_value_for_repr(candidate["investment_period"])} will be disabled to avoid conflicting constraints.' - ) + # Conflict found: keep the one with smaller extension + if mapping['extension'] < conflict['extension']: + # Current mapping is better, replace the previous one + selected.discard(conflict['investment_period']) + selected.add(mapping['investment_period']) + disabled.append(conflict['investment_period']) + logger.warning( + f'Investment in period {fx_io._format_value_for_repr(conflict["investment_period"])} conflicts with period ' + f'{fx_io._format_value_for_repr(mapping["investment_period"])} (both map to decommissioning period ' + f'{fx_io._format_value_for_repr(mapping["decommissioning_period"])}). Disabling period {fx_io._format_value_for_repr(conflict["investment_period"])} ' + f'as it requires more lifetime extension ({conflict["extension"]:.1f} vs {mapping["extension"]:.1f} periods).' + ) + else: + # Previous mapping is better, disable current one + disabled.append(mapping['investment_period']) + logger.warning( + f'Investment in period {fx_io._format_value_for_repr(mapping["investment_period"])} conflicts with period ' + f'{fx_io._format_value_for_repr(conflict["investment_period"])} (both map to decommissioning period ' + f'{fx_io._format_value_for_repr(mapping["decommissioning_period"])}). Disabling period {fx_io._format_value_for_repr(mapping["investment_period"])} ' + f'as it requires more lifetime extension ({mapping["extension"]:.1f} vs {conflict["extension"]:.1f} periods).' + ) + + return selected + + def _track_lifetime(self): + periods = self._model.flow_system.fit_to_model_coords( + 'periods', self._model.flow_system.periods.values, dims=['period', 'scenario'] + ) + + # Determine which investment periods to allow (avoiding conflicts) + selected_periods = self._select_best_investment_periods(periods) - # Second pass: Add constraints for selected investments and disable others + # Add constraints for each period for i, period in enumerate(periods.values): decommissioning_period = ( period + self.parameters.lifetime - (self.parameters.previous_lifetime if i == 0 else 0) @@ -319,14 +334,15 @@ def _track_lifetime(self): if (decommissioning_period > self._model.flow_system.periods.values[-1]).all(): continue - if period not in selected_investments: - # Disable investment in this period + # Disable investment if this period conflicts with a better choice + if period not in selected_periods: self.add_constraints( self.investment_occurs.sel(period=period) == 0, short_name=f'size|no_investment_due_to_lifetime_conflict{period}', ) continue + # Link investment to decommissioning available_decommissioning_period = periods.sel(period=decommissioning_period, method='bfill') if (decommissioning_period != available_decommissioning_period).any(): From 75cb42a448b6359f5ef674d079e9f1e5b0f635ad Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 3 Nov 2025 14:47:38 +0100 Subject: [PATCH 441/448] Fix --- flixopt/features.py | 69 +++++++++++++++------------------------------ 1 file changed, 23 insertions(+), 46 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index ab33fd3f1..743cf2f3e 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -256,8 +256,9 @@ def _track_investment_and_decomissioning_size(self): def _select_best_investment_periods(self, periods): """Select investment periods that don't conflict, preferring those with minimal lifetime extension.""" - # Build list of all investment -> decommissioning mappings - mappings = [] + # Map decommissioning_period -> (investment_period, extension, available_decommissioning_period) + decom_to_best = {} + for i, period in enumerate(periods.values): decommissioning_period = ( period + self.parameters.lifetime - (self.parameters.previous_lifetime if i == 0 else 0) @@ -267,56 +268,32 @@ def _select_best_investment_periods(self, periods): available_decommissioning_period = periods.sel(period=decommissioning_period, method='bfill') extension = (available_decommissioning_period - decommissioning_period).max().item() - - mappings.append( - { - 'investment_period': period, - 'decommissioning_period': available_decommissioning_period, - 'extension': extension, - } - ) - - # Find conflicts: investments with same decommissioning period - selected = set() - disabled = [] - - for i, mapping in enumerate(mappings): - # Check if any previous mapping has the same decommissioning period - conflict = None - for j in range(i): - if mappings[j]['investment_period'] in disabled: - continue - if (mappings[j]['decommissioning_period'] == mapping['decommissioning_period']).all(): - conflict = mappings[j] - break - - if conflict is None: - # No conflict, select this investment - selected.add(mapping['investment_period']) - else: - # Conflict found: keep the one with smaller extension - if mapping['extension'] < conflict['extension']: - # Current mapping is better, replace the previous one - selected.discard(conflict['investment_period']) - selected.add(mapping['investment_period']) - disabled.append(conflict['investment_period']) + decom_value = available_decommissioning_period.item() + + # Check if this decommissioning period already has a candidate + if decom_value in decom_to_best: + prev_period, prev_ext, prev_decom = decom_to_best[decom_value] + if extension < prev_ext: + # Current investment is better (smaller extension) + decom_to_best[decom_value] = (period, extension, available_decommissioning_period) logger.warning( - f'Investment in period {fx_io._format_value_for_repr(conflict["investment_period"])} conflicts with period ' - f'{fx_io._format_value_for_repr(mapping["investment_period"])} (both map to decommissioning period ' - f'{fx_io._format_value_for_repr(mapping["decommissioning_period"])}). Disabling period {fx_io._format_value_for_repr(conflict["investment_period"])} ' - f'as it requires more lifetime extension ({conflict["extension"]:.1f} vs {mapping["extension"]:.1f} periods).' + f'Investment in period {fx_io._format_value_for_repr(prev_period)} conflicts with period ' + f'{fx_io._format_value_for_repr(period)} (both map to decommissioning period ' + f'{fx_io._format_value_for_repr(decom_value)}). Disabling period {fx_io._format_value_for_repr(prev_period)} ' + f'as it requires more lifetime extension ({prev_ext:.1f} vs {extension:.1f} periods).' ) else: - # Previous mapping is better, disable current one - disabled.append(mapping['investment_period']) + # Previous investment is better logger.warning( - f'Investment in period {fx_io._format_value_for_repr(mapping["investment_period"])} conflicts with period ' - f'{fx_io._format_value_for_repr(conflict["investment_period"])} (both map to decommissioning period ' - f'{fx_io._format_value_for_repr(mapping["decommissioning_period"])}). Disabling period {fx_io._format_value_for_repr(mapping["investment_period"])} ' - f'as it requires more lifetime extension ({mapping["extension"]:.1f} vs {conflict["extension"]:.1f} periods).' + f'Investment in period {fx_io._format_value_for_repr(period)} conflicts with period ' + f'{fx_io._format_value_for_repr(prev_period)} (both map to decommissioning period ' + f'{fx_io._format_value_for_repr(decom_value)}). Disabling period {fx_io._format_value_for_repr(period)} ' + f'as it requires more lifetime extension ({extension:.1f} vs {prev_ext:.1f} periods).' ) + else: + decom_to_best[decom_value] = (period, extension, available_decommissioning_period) - return selected + return {period for period, _, _ in decom_to_best.values()} def _track_lifetime(self): periods = self._model.flow_system.fit_to_model_coords( From 603f54f57ec690ecda9d6b1140cae42b008cc391 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 3 Nov 2025 15:07:39 +0100 Subject: [PATCH 442/448] Use ffill instead of bfill --- flixopt/features.py | 39 +++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index 743cf2f3e..51fe6d44e 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -255,8 +255,8 @@ def _track_investment_and_decomissioning_size(self): ) def _select_best_investment_periods(self, periods): - """Select investment periods that don't conflict, preferring those with minimal lifetime extension.""" - # Map decommissioning_period -> (investment_period, extension, available_decommissioning_period) + """Select investment periods that don't conflict, preferring those with minimal lifetime deviation.""" + # Map decommissioning_period -> (investment_period, deviation, available_decommissioning_period) decom_to_best = {} for i, period in enumerate(periods.values): @@ -266,21 +266,21 @@ def _select_best_investment_periods(self, periods): if (decommissioning_period > self._model.flow_system.periods.values[-1]).all(): continue - available_decommissioning_period = periods.sel(period=decommissioning_period, method='bfill') - extension = (available_decommissioning_period - decommissioning_period).max().item() + available_decommissioning_period = periods.sel(period=decommissioning_period, method='ffill') + deviation = abs((available_decommissioning_period - decommissioning_period).max().item()) decom_value = available_decommissioning_period.item() # Check if this decommissioning period already has a candidate if decom_value in decom_to_best: - prev_period, prev_ext, prev_decom = decom_to_best[decom_value] - if extension < prev_ext: - # Current investment is better (smaller extension) - decom_to_best[decom_value] = (period, extension, available_decommissioning_period) + prev_period, prev_dev, prev_decom = decom_to_best[decom_value] + if deviation < prev_dev: + # Current investment is better (smaller deviation) + decom_to_best[decom_value] = (period, deviation, available_decommissioning_period) logger.warning( f'Investment in period {fx_io._format_value_for_repr(prev_period)} conflicts with period ' f'{fx_io._format_value_for_repr(period)} (both map to decommissioning period ' f'{fx_io._format_value_for_repr(decom_value)}). Disabling period {fx_io._format_value_for_repr(prev_period)} ' - f'as it requires more lifetime extension ({prev_ext:.1f} vs {extension:.1f} periods).' + f'as it has larger lifetime deviation ({prev_dev:.1f} vs {deviation:.1f} periods).' ) else: # Previous investment is better @@ -288,10 +288,10 @@ def _select_best_investment_periods(self, periods): f'Investment in period {fx_io._format_value_for_repr(period)} conflicts with period ' f'{fx_io._format_value_for_repr(prev_period)} (both map to decommissioning period ' f'{fx_io._format_value_for_repr(decom_value)}). Disabling period {fx_io._format_value_for_repr(period)} ' - f'as it requires more lifetime extension ({extension:.1f} vs {prev_ext:.1f} periods).' + f'as it has larger lifetime deviation ({deviation:.1f} vs {prev_dev:.1f} periods).' ) else: - decom_to_best[decom_value] = (period, extension, available_decommissioning_period) + decom_to_best[decom_value] = (period, deviation, available_decommissioning_period) return {period for period, _, _ in decom_to_best.values()} @@ -320,13 +320,20 @@ def _track_lifetime(self): continue # Link investment to decommissioning - available_decommissioning_period = periods.sel(period=decommissioning_period, method='bfill') + available_decommissioning_period = periods.sel(period=decommissioning_period, method='ffill') if (decommissioning_period != available_decommissioning_period).any(): - logger.warning( - f'For an Investment in period {period}, the decommissioning period would be {fx_io._format_value_for_repr(decommissioning_period)}. ' - f'As this period is not part of the Model horizon, the lifetime will be extended until the next period ({fx_io._format_value_for_repr(available_decommissioning_period)}), which will effectively extend the lifetime by +{fx_io._format_value_for_repr(available_decommissioning_period - decommissioning_period)} periods.' - ) + lifetime_diff = available_decommissioning_period - decommissioning_period + if (lifetime_diff < 0).any(): + logger.warning( + f'For an Investment in period {period}, the decommissioning period would be {fx_io._format_value_for_repr(decommissioning_period)}. ' + f'As this period is not part of the Model horizon, the lifetime will be shortened to the previous period ({fx_io._format_value_for_repr(available_decommissioning_period)}), which will effectively shorten the lifetime by {fx_io._format_value_for_repr(-lifetime_diff)} periods.' + ) + else: + logger.warning( + f'For an Investment in period {period}, the decommissioning period would be {fx_io._format_value_for_repr(decommissioning_period)}. ' + f'As this period is not part of the Model horizon, the lifetime will be extended to the previous period ({fx_io._format_value_for_repr(available_decommissioning_period)}), which will effectively extend the lifetime by +{fx_io._format_value_for_repr(lifetime_diff)} periods.' + ) self.add_constraints( self.investment_occurs.sel(period=period) From 37cebdd74c5a0363ad01400ae5da383d4eee02f0 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 3 Nov 2025 15:07:44 +0100 Subject: [PATCH 443/448] Revert "Use ffill instead of bfill" This reverts commit 603f54f57ec690ecda9d6b1140cae42b008cc391. --- flixopt/features.py | 39 ++++++++++++++++----------------------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index 51fe6d44e..743cf2f3e 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -255,8 +255,8 @@ def _track_investment_and_decomissioning_size(self): ) def _select_best_investment_periods(self, periods): - """Select investment periods that don't conflict, preferring those with minimal lifetime deviation.""" - # Map decommissioning_period -> (investment_period, deviation, available_decommissioning_period) + """Select investment periods that don't conflict, preferring those with minimal lifetime extension.""" + # Map decommissioning_period -> (investment_period, extension, available_decommissioning_period) decom_to_best = {} for i, period in enumerate(periods.values): @@ -266,21 +266,21 @@ def _select_best_investment_periods(self, periods): if (decommissioning_period > self._model.flow_system.periods.values[-1]).all(): continue - available_decommissioning_period = periods.sel(period=decommissioning_period, method='ffill') - deviation = abs((available_decommissioning_period - decommissioning_period).max().item()) + available_decommissioning_period = periods.sel(period=decommissioning_period, method='bfill') + extension = (available_decommissioning_period - decommissioning_period).max().item() decom_value = available_decommissioning_period.item() # Check if this decommissioning period already has a candidate if decom_value in decom_to_best: - prev_period, prev_dev, prev_decom = decom_to_best[decom_value] - if deviation < prev_dev: - # Current investment is better (smaller deviation) - decom_to_best[decom_value] = (period, deviation, available_decommissioning_period) + prev_period, prev_ext, prev_decom = decom_to_best[decom_value] + if extension < prev_ext: + # Current investment is better (smaller extension) + decom_to_best[decom_value] = (period, extension, available_decommissioning_period) logger.warning( f'Investment in period {fx_io._format_value_for_repr(prev_period)} conflicts with period ' f'{fx_io._format_value_for_repr(period)} (both map to decommissioning period ' f'{fx_io._format_value_for_repr(decom_value)}). Disabling period {fx_io._format_value_for_repr(prev_period)} ' - f'as it has larger lifetime deviation ({prev_dev:.1f} vs {deviation:.1f} periods).' + f'as it requires more lifetime extension ({prev_ext:.1f} vs {extension:.1f} periods).' ) else: # Previous investment is better @@ -288,10 +288,10 @@ def _select_best_investment_periods(self, periods): f'Investment in period {fx_io._format_value_for_repr(period)} conflicts with period ' f'{fx_io._format_value_for_repr(prev_period)} (both map to decommissioning period ' f'{fx_io._format_value_for_repr(decom_value)}). Disabling period {fx_io._format_value_for_repr(period)} ' - f'as it has larger lifetime deviation ({deviation:.1f} vs {prev_dev:.1f} periods).' + f'as it requires more lifetime extension ({extension:.1f} vs {prev_ext:.1f} periods).' ) else: - decom_to_best[decom_value] = (period, deviation, available_decommissioning_period) + decom_to_best[decom_value] = (period, extension, available_decommissioning_period) return {period for period, _, _ in decom_to_best.values()} @@ -320,20 +320,13 @@ def _track_lifetime(self): continue # Link investment to decommissioning - available_decommissioning_period = periods.sel(period=decommissioning_period, method='ffill') + available_decommissioning_period = periods.sel(period=decommissioning_period, method='bfill') if (decommissioning_period != available_decommissioning_period).any(): - lifetime_diff = available_decommissioning_period - decommissioning_period - if (lifetime_diff < 0).any(): - logger.warning( - f'For an Investment in period {period}, the decommissioning period would be {fx_io._format_value_for_repr(decommissioning_period)}. ' - f'As this period is not part of the Model horizon, the lifetime will be shortened to the previous period ({fx_io._format_value_for_repr(available_decommissioning_period)}), which will effectively shorten the lifetime by {fx_io._format_value_for_repr(-lifetime_diff)} periods.' - ) - else: - logger.warning( - f'For an Investment in period {period}, the decommissioning period would be {fx_io._format_value_for_repr(decommissioning_period)}. ' - f'As this period is not part of the Model horizon, the lifetime will be extended to the previous period ({fx_io._format_value_for_repr(available_decommissioning_period)}), which will effectively extend the lifetime by +{fx_io._format_value_for_repr(lifetime_diff)} periods.' - ) + logger.warning( + f'For an Investment in period {period}, the decommissioning period would be {fx_io._format_value_for_repr(decommissioning_period)}. ' + f'As this period is not part of the Model horizon, the lifetime will be extended until the next period ({fx_io._format_value_for_repr(available_decommissioning_period)}), which will effectively extend the lifetime by +{fx_io._format_value_for_repr(available_decommissioning_period - decommissioning_period)} periods.' + ) self.add_constraints( self.investment_occurs.sel(period=period) From c9caf96c040cde9d8d5d51a923ce07f6950deacb Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 3 Nov 2025 15:10:52 +0100 Subject: [PATCH 444/448] Update --- flixopt/features.py | 88 ++++++++++++++++----------------------------- 1 file changed, 30 insertions(+), 58 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index 743cf2f3e..c5690dee2 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -254,56 +254,14 @@ def _track_investment_and_decomissioning_size(self): coord='period', ) - def _select_best_investment_periods(self, periods): - """Select investment periods that don't conflict, preferring those with minimal lifetime extension.""" - # Map decommissioning_period -> (investment_period, extension, available_decommissioning_period) - decom_to_best = {} - - for i, period in enumerate(periods.values): - decommissioning_period = ( - period + self.parameters.lifetime - (self.parameters.previous_lifetime if i == 0 else 0) - ) - if (decommissioning_period > self._model.flow_system.periods.values[-1]).all(): - continue - - available_decommissioning_period = periods.sel(period=decommissioning_period, method='bfill') - extension = (available_decommissioning_period - decommissioning_period).max().item() - decom_value = available_decommissioning_period.item() - - # Check if this decommissioning period already has a candidate - if decom_value in decom_to_best: - prev_period, prev_ext, prev_decom = decom_to_best[decom_value] - if extension < prev_ext: - # Current investment is better (smaller extension) - decom_to_best[decom_value] = (period, extension, available_decommissioning_period) - logger.warning( - f'Investment in period {fx_io._format_value_for_repr(prev_period)} conflicts with period ' - f'{fx_io._format_value_for_repr(period)} (both map to decommissioning period ' - f'{fx_io._format_value_for_repr(decom_value)}). Disabling period {fx_io._format_value_for_repr(prev_period)} ' - f'as it requires more lifetime extension ({prev_ext:.1f} vs {extension:.1f} periods).' - ) - else: - # Previous investment is better - logger.warning( - f'Investment in period {fx_io._format_value_for_repr(period)} conflicts with period ' - f'{fx_io._format_value_for_repr(prev_period)} (both map to decommissioning period ' - f'{fx_io._format_value_for_repr(decom_value)}). Disabling period {fx_io._format_value_for_repr(period)} ' - f'as it requires more lifetime extension ({extension:.1f} vs {prev_ext:.1f} periods).' - ) - else: - decom_to_best[decom_value] = (period, extension, available_decommissioning_period) - - return {period for period, _, _ in decom_to_best.values()} - def _track_lifetime(self): periods = self._model.flow_system.fit_to_model_coords( 'periods', self._model.flow_system.periods.values, dims=['period', 'scenario'] ) - # Determine which investment periods to allow (avoiding conflicts) - selected_periods = self._select_best_investment_periods(periods) + # Group investment periods by their decommissioning period + decom_to_investments = {} - # Add constraints for each period for i, period in enumerate(periods.values): decommissioning_period = ( period + self.parameters.lifetime - (self.parameters.previous_lifetime if i == 0 else 0) @@ -311,28 +269,42 @@ def _track_lifetime(self): if (decommissioning_period > self._model.flow_system.periods.values[-1]).all(): continue - # Disable investment if this period conflicts with a better choice - if period not in selected_periods: - self.add_constraints( - self.investment_occurs.sel(period=period) == 0, - short_name=f'size|no_investment_due_to_lifetime_conflict{period}', - ) - continue - - # Link investment to decommissioning available_decommissioning_period = periods.sel(period=decommissioning_period, method='bfill') + decom_value = available_decommissioning_period.item() + if decom_value not in decom_to_investments: + decom_to_investments[decom_value] = [] + + decom_to_investments[decom_value].append((period, decommissioning_period, available_decommissioning_period)) + + # Warn about lifetime extension if (decommissioning_period != available_decommissioning_period).any(): logger.warning( f'For an Investment in period {period}, the decommissioning period would be {fx_io._format_value_for_repr(decommissioning_period)}. ' f'As this period is not part of the Model horizon, the lifetime will be extended until the next period ({fx_io._format_value_for_repr(available_decommissioning_period)}), which will effectively extend the lifetime by +{fx_io._format_value_for_repr(available_decommissioning_period - decommissioning_period)} periods.' ) - self.add_constraints( - self.investment_occurs.sel(period=period) - == self.decommissioning_occurs.sel(period=available_decommissioning_period), - short_name=f'size|lifetime{period}', - ) + # Add constraints: sum of investments equals decommissioning + for decom_value, investment_list in decom_to_investments.items(): + available_decommissioning_period = investment_list[0][2] + + if len(investment_list) > 1: + # Multiple investments map to same decommissioning period - sum them + investment_sum = sum( + self.investment_occurs.sel(period=inv_period) for inv_period, _, _ in investment_list + ) + self.add_constraints( + investment_sum == self.decommissioning_occurs.sel(period=available_decommissioning_period), + short_name=f'size|lifetime{decom_value}', + ) + else: + # Single investment maps to this decommissioning period + inv_period = investment_list[0][0] + self.add_constraints( + self.investment_occurs.sel(period=inv_period) + == self.decommissioning_occurs.sel(period=available_decommissioning_period), + short_name=f'size|lifetime{inv_period}', + ) def _apply_investment_period_constraints(self): # Constraint: Apply allow_investment restrictions From b50b0e558dc35d9bace1950d23ed17c08d6774d6 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 3 Nov 2025 15:23:21 +0100 Subject: [PATCH 445/448] Fix --- flixopt/features.py | 61 +++++++++++++-------------------------------- 1 file changed, 17 insertions(+), 44 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index c5690dee2..9b8906b53 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -11,6 +11,7 @@ import linopy import numpy as np +import xarray as xr from . import io as fx_io from .modeling import BoundingPatterns, ModelingPrimitives, ModelingUtilities @@ -259,52 +260,24 @@ def _track_lifetime(self): 'periods', self._model.flow_system.periods.values, dims=['period', 'scenario'] ) - # Group investment periods by their decommissioning period - decom_to_investments = {} + # Calculate decommissioning periods (vectorized) + is_first = periods == periods.isel(period=0) + decom_period = periods + self.parameters.lifetime - xr.where(is_first, self.parameters.previous_lifetime, 0) - for i, period in enumerate(periods.values): - decommissioning_period = ( - period + self.parameters.lifetime - (self.parameters.previous_lifetime if i == 0 else 0) - ) - if (decommissioning_period > self._model.flow_system.periods.values[-1]).all(): - continue - - available_decommissioning_period = periods.sel(period=decommissioning_period, method='bfill') - decom_value = available_decommissioning_period.item() - - if decom_value not in decom_to_investments: - decom_to_investments[decom_value] = [] - - decom_to_investments[decom_value].append((period, decommissioning_period, available_decommissioning_period)) - - # Warn about lifetime extension - if (decommissioning_period != available_decommissioning_period).any(): - logger.warning( - f'For an Investment in period {period}, the decommissioning period would be {fx_io._format_value_for_repr(decommissioning_period)}. ' - f'As this period is not part of the Model horizon, the lifetime will be extended until the next period ({fx_io._format_value_for_repr(available_decommissioning_period)}), which will effectively extend the lifetime by +{fx_io._format_value_for_repr(available_decommissioning_period - decommissioning_period)} periods.' - ) - - # Add constraints: sum of investments equals decommissioning - for decom_value, investment_list in decom_to_investments.items(): - available_decommissioning_period = investment_list[0][2] + # Map to available periods + valid = decom_period <= self._model.flow_system.periods.values[-1] + avail_decom = periods.sel(period=decom_period.where(valid, drop=True), method='bfill') - if len(investment_list) > 1: - # Multiple investments map to same decommissioning period - sum them - investment_sum = sum( - self.investment_occurs.sel(period=inv_period) for inv_period, _, _ in investment_list - ) - self.add_constraints( - investment_sum == self.decommissioning_occurs.sel(period=available_decommissioning_period), - short_name=f'size|lifetime{decom_value}', - ) - else: - # Single investment maps to this decommissioning period - inv_period = investment_list[0][0] - self.add_constraints( - self.investment_occurs.sel(period=inv_period) - == self.decommissioning_occurs.sel(period=available_decommissioning_period), - short_name=f'size|lifetime{inv_period}', - ) + # One constraint per unique decommissioning period + for decom_val in np.unique(avail_decom.where(valid).values): + if np.isnan(decom_val): + continue + mask = (avail_decom == decom_val) & valid + self.add_constraints( + self.investment_occurs.where(mask, 0).sum('period') + == self.decommissioning_occurs.sel(period=decom_val), + short_name=f'size|lifetime{int(decom_val)}', + ) def _apply_investment_period_constraints(self): # Constraint: Apply allow_investment restrictions From 1a9c5a732ee46aa6e4259af23d495e1ba23b2479 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 3 Nov 2025 19:59:02 +0100 Subject: [PATCH 446/448] Fix --- flixopt/features.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/flixopt/features.py b/flixopt/features.py index 9b8906b53..168078f16 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -264,18 +264,15 @@ def _track_lifetime(self): is_first = periods == periods.isel(period=0) decom_period = periods + self.parameters.lifetime - xr.where(is_first, self.parameters.previous_lifetime, 0) - # Map to available periods - valid = decom_period <= self._model.flow_system.periods.values[-1] - avail_decom = periods.sel(period=decom_period.where(valid, drop=True), method='bfill') + # Map to available periods (drop invalid ones for sel to work) + valid = decom_period.where(decom_period <= self._model.flow_system.periods.values[-1], drop=True) + avail_decom = periods.sel(period=valid, method='bfill').assign_coords(period=valid.period) # One constraint per unique decommissioning period - for decom_val in np.unique(avail_decom.where(valid).values): - if np.isnan(decom_val): - continue - mask = (avail_decom == decom_val) & valid + for decom_val in np.unique(avail_decom.values): + mask = avail_decom == decom_val self.add_constraints( - self.investment_occurs.where(mask, 0).sum('period') - == self.decommissioning_occurs.sel(period=decom_val), + self.investment_occurs.where(mask).sum('period') == self.decommissioning_occurs.sel(period=decom_val), short_name=f'size|lifetime{int(decom_val)}', ) From c750fdcbd2f633d0986136aa64de2bb0406d1f0b Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 3 Nov 2025 20:01:33 +0100 Subject: [PATCH 447/448] Fix --- flixopt/features.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flixopt/features.py b/flixopt/features.py index 168078f16..63e8f92a3 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -270,7 +270,7 @@ def _track_lifetime(self): # One constraint per unique decommissioning period for decom_val in np.unique(avail_decom.values): - mask = avail_decom == decom_val + mask = (avail_decom == decom_val).reindex(period=periods).fillna(0) self.add_constraints( self.investment_occurs.where(mask).sum('period') == self.decommissioning_occurs.sel(period=decom_val), short_name=f'size|lifetime{int(decom_val)}', From 43561edf2e85b0e73f6c2762278e4ed68398fc41 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 4 Nov 2025 09:08:33 +0100 Subject: [PATCH 448/448] Fix for scenarios --- flixopt/features.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flixopt/features.py b/flixopt/features.py index 63e8f92a3..3c143dfb9 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -270,7 +270,7 @@ def _track_lifetime(self): # One constraint per unique decommissioning period for decom_val in np.unique(avail_decom.values): - mask = (avail_decom == decom_val).reindex(period=periods).fillna(0) + mask = (avail_decom == decom_val).reindex_like(periods).fillna(0) self.add_constraints( self.investment_occurs.where(mask).sum('period') == self.decommissioning_occurs.sel(period=decom_val), short_name=f'size|lifetime{int(decom_val)}',